From 872d2d1e1c0d01c965fda7f4e307abb499a30d99 Mon Sep 17 00:00:00 2001 From: Karl Persson Date: Tue, 7 Mar 2023 09:57:25 +0100 Subject: [PATCH 001/288] AuthN: Login error handling (#64239) * Social: Fix type so it appears in error responses * AuthN: construct errutil.Error from social.Error * login: Check for errutil.Error and use public message * Login: redirectURLWithErrorCookie for authn errors Co-authored-by: Jo --- pkg/api/login.go | 6 ++++++ pkg/api/login_oauth.go | 18 ++---------------- pkg/login/social/common.go | 2 +- pkg/services/authn/clients/oauth.go | 18 ++++++++++++++---- 4 files changed, 23 insertions(+), 21 deletions(-) diff --git a/pkg/api/login.go b/pkg/api/login.go index efa715efa38..d22f0a2f88f 100644 --- a/pkg/api/login.go +++ b/pkg/api/login.go @@ -25,6 +25,7 @@ import ( "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" + "github.com/grafana/grafana/pkg/util/errutil" "github.com/grafana/grafana/pkg/web" ) @@ -441,5 +442,10 @@ func getLoginExternalError(err error) string { return createTokenErr.ExternalErr } + gfErr := &errutil.Error{} + if errors.As(err, gfErr) { + return gfErr.Public().Message + } + return err.Error() } diff --git a/pkg/api/login_oauth.go b/pkg/api/login_oauth.go index d302f63ea19..68a2dc7544a 100644 --- a/pkg/api/login_oauth.go +++ b/pkg/api/login_oauth.go @@ -24,7 +24,6 @@ import ( "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" - "github.com/grafana/grafana/pkg/util/errutil" "github.com/grafana/grafana/pkg/web" ) @@ -90,7 +89,7 @@ func (hs *HTTPServer) OAuthLogin(ctx *contextmodel.ReqContext) { if code == "" { redirect, err := hs.authnService.RedirectURL(ctx.Req.Context(), authn.ClientWithPrefix(name), req) if err != nil { - hs.handleAuthnOAuthErr(ctx, "failed to generate oauth redirect url", err) + ctx.Redirect(hs.redirectURLWithErrorCookie(ctx, err)) return } @@ -109,7 +108,7 @@ func (hs *HTTPServer) OAuthLogin(ctx *contextmodel.ReqContext) { cookies.DeleteCookie(ctx.Resp, OauthStateCookieName, hs.CookieOptionsFromCfg) if err != nil { - hs.handleAuthnOAuthErr(ctx, "failed to perform login for oauth request", err) + ctx.Redirect(hs.redirectURLWithErrorCookie(ctx, err)) return } @@ -380,19 +379,6 @@ func (hs *HTTPServer) hashStatecode(code, seed string) string { return hex.EncodeToString(hashBytes[:]) } -func (hs *HTTPServer) handleAuthnOAuthErr(c *contextmodel.ReqContext, msg string, err error) { - gfErr := &errutil.Error{} - if errors.As(err, gfErr) { - if gfErr.Public().Message != "" { - c.Handle(hs.Cfg, gfErr.Public().StatusCode, gfErr.Public().Message, err) - return - } - } - - c.Logger.Warn(msg, "err", err) - c.Redirect(hs.Cfg.AppSubURL + "/login") -} - type LoginError struct { HttpStatus int PublicMessage string diff --git a/pkg/login/social/common.go b/pkg/login/social/common.go index 309ede7cd0e..1dfea668312 100644 --- a/pkg/login/social/common.go +++ b/pkg/login/social/common.go @@ -12,7 +12,7 @@ import ( ) var ( - errMissingGroupMembership = Error{"user not a member of one of the required groups"} + errMissingGroupMembership = &Error{"user not a member of one of the required groups"} ) type httpGetResponse struct { diff --git a/pkg/services/authn/clients/oauth.go b/pkg/services/authn/clients/oauth.go index 480d4b82d14..906c29a0dee 100644 --- a/pkg/services/authn/clients/oauth.go +++ b/pkg/services/authn/clients/oauth.go @@ -6,6 +6,7 @@ import ( "crypto/sha256" "encoding/base64" "encoding/hex" + "errors" "fmt" "net/http" "strings" @@ -42,11 +43,16 @@ var ( errOAuthInvalidState = errutil.NewBase(errutil.StatusUnauthorized, "auth.oauth.state.invalid", errutil.WithPublicMessage("Provided state does not match stored state")) errOAuthTokenExchange = errutil.NewBase(errutil.StatusInternal, "auth.oauth.token.exchange", errutil.WithPublicMessage("Failed to get token from provider")) + errOAuthUserInfo = errutil.NewBase(errutil.StatusInternal, "auth.oauth.userinfo.error") - errOAuthMissingRequiredEmail = errutil.NewBase(errutil.StatusUnauthorized, "auth.oauth.email.missing") - errOAuthEmailNotAllowed = errutil.NewBase(errutil.StatusUnauthorized, "auth.oauth.email.not-allowed") + errOAuthMissingRequiredEmail = errutil.NewBase(errutil.StatusUnauthorized, "auth.oauth.email.missing", errutil.WithPublicMessage("Provider didn't return an email address")) + errOAuthEmailNotAllowed = errutil.NewBase(errutil.StatusUnauthorized, "auth.oauth.email.not-allowed", errutil.WithPublicMessage("Required email domain not fulfilled")) ) +func fromSocialErr(err *social.Error) error { + return errutil.NewBase(errutil.StatusUnauthorized, "auth.oauth.userinfo.failed", errutil.WithPublicMessage(err.Error())).Errorf("%w", err) +} + var _ authn.RedirectClient = new(OAuth) func ProvideOAuth( @@ -106,13 +112,17 @@ func (c *OAuth) Authenticate(ctx context.Context, r *authn.Request) (*authn.Iden // exchange auth code to a valid token token, err := c.connector.Exchange(clientCtx, r.HTTPRequest.URL.Query().Get("code"), opts...) if err != nil { - return nil, err + return nil, errOAuthTokenExchange.Errorf("failed to exchange code to token: %w", err) } token.TokenType = "Bearer" userInfo, err := c.connector.UserInfo(c.connector.Client(clientCtx, token), token) if err != nil { - return nil, errOAuthTokenExchange.Errorf("failed to exchange code to token: %w", err) + var sErr *social.Error + if errors.As(err, &sErr) { + return nil, fromSocialErr(sErr) + } + return nil, errOAuthUserInfo.Errorf("failed to get user info: %w", err) } if userInfo.Email == "" { From 58eb25e47dfbfcc7589751b437298af4c18b1d16 Mon Sep 17 00:00:00 2001 From: Andrej Ocenas Date: Tue, 7 Mar 2023 10:26:51 +0100 Subject: [PATCH 002/288] Phlare: Refactor the pprof transform (#64028) --- pkg/tsdb/phlare/query.go | 253 +++++++++++------- pkg/tsdb/phlare/query_test.go | 157 +++++++---- .../phlare/testdata/profile_response.json | 1 + 3 files changed, 258 insertions(+), 153 deletions(-) create mode 100644 pkg/tsdb/phlare/testdata/profile_response.json diff --git a/pkg/tsdb/phlare/query.go b/pkg/tsdb/phlare/query.go index 7504ba3dec5..29c38f57f9a 100644 --- a/pkg/tsdb/phlare/query.go +++ b/pkg/tsdb/phlare/query.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "math" + "sort" "strings" "time" @@ -151,13 +152,13 @@ func (f Function) String() string { return fmt.Sprintf("%s:%s:%d", f.FileName, f.FunctionName, f.Line) } -func (pt ProfileTree) String() string { +func (pt *ProfileTree) String() string { type branch struct { nodes []*ProfileTree treeprint.Tree } tree := treeprint.New() - for _, n := range []ProfileTree{pt} { + for _, n := range []*ProfileTree{pt} { b := tree.AddBranch(fmt.Sprintf("%s: level %d self %d total %d", n.Function, n.Level, n.Self, n.Value)) remaining := append([]*branch{}, &branch{nodes: n.Nodes, Tree: b}) for len(remaining) > 0 { @@ -179,111 +180,122 @@ func (pt ProfileTree) String() string { return tree.String() } -// merge merges the node into the tree. -// it assumes src has only one leaf. -func (pt *ProfileTree) merge(src *ProfileTree) { - // find the node path where n should be inserted. - var parent, found *ProfileTree - // visit depth first the dst tree following the src tree - remaining := []*ProfileTree{pt} - for len(remaining) > 0 { - n := remaining[0] - remaining = remaining[1:] - if src.locationID == n.locationID { - if len(src.Nodes) == 0 { - // we have found the leaf - found = n - break - } - // move src and last parent visited - parent = n - src = src.Nodes[0] - remaining = n.Nodes - continue - } - } - if found == nil { - if parent == nil { - // Nothing in common can't be merged. - return - } - src.Parent = parent - parent.Nodes = append(parent.Nodes, src) - for p := parent; p != nil; p = p.Parent { - p.Value = p.Value + src.Value - } +// addSample adds a sample to the tree. As sample is just a single stack we just have to traverse the tree until it +// starts to differ from the sample and add a new branch if needed. For example if we have a tree: +// +// root --> func1 -> func2 -> func3 +// \-> func4 +// +// And we add a sample: +// +// func1 -> func2 -> func5 +// +// We will get: +// +// root --> func1 --> func2 --> func3 +// \ \-> func5 +// \-> func4 +// +// While we add the current sample value to root -> func1 -> func2. +func (pt *ProfileTree) addSample(profile *googlev1.Profile, sample *googlev1.Sample) { + if len(sample.LocationId) == 0 { return } - found.Value = found.Value + src.Self - for p := found.Parent; p != nil; p = p.Parent { - p.Value = p.Value + src.Self + + locations := getReversedLocations(profile, sample) + + // Extend root + pt.Value = pt.Value + sample.Value[0] + current := pt + + for index, location := range locations { + if len(current.Nodes) > 0 { + var foundNode *ProfileTree + for _, node := range current.Nodes { + if node.locationID == location.Id { + foundNode = node + } + } + + if foundNode != nil { + // We found node with the same locationID so just add the value it + foundNode.Value = foundNode.Value + sample.Value[0] + current = foundNode + // Continue to next locationID in the sample + continue + } + } + // Either current has no children we can compare to or we have location that does not exist yet in the tree. + + // Create sample with only the locations we did not already attributed to the tree. + subSample := &googlev1.Sample{ + LocationId: sample.LocationId[:len(sample.LocationId)-index], + Value: sample.Value, + Label: sample.Label, + } + newTree := treeFromSample(profile, subSample, index) + // Append the new subtree in the correct place in the tree + current.Nodes = append(current.Nodes, newTree.Nodes[0]) + sort.SliceStable(current.Nodes, func(i, j int) bool { + return current.Nodes[i].Function.String() < current.Nodes[j].Function.String() + }) + newTree.Nodes[0].Parent = current + break } - found.Self = found.Self + src.Self + + // Adjust self of the current node as we may need to add value to its self if we just extended it and did not + // add children + var childrenVal int64 = 0 + for _, node := range current.Nodes { + childrenVal += node.Value + } + current.Self = current.Value - childrenVal } -func treeFromSample(profile *googlev1.Profile, sample *googlev1.Sample) *ProfileTree { - if len(sample.LocationId) == 0 { - return &ProfileTree{ - Level: 0, - Value: sample.Value[0], - Function: &Function{ - FunctionName: "root", - }, - } - } - - // The leaf is at locations[0]. - locations := sample.LocationId - - current := &ProfileTree{ - Self: sample.Value[0], - Level: 0, - } - for len(locations) > 0 { - current.locationID = locations[0] - current.Value = sample.Value[0] - current.Level = len(locations) - - // Ids in pprof format are 1 based. So to get the index in array from the id we need to subtract one. - lines := profile.Location[locations[0]-1].Line - if len(lines) == 0 { - locations = locations[1:] - continue - } - // The leaf is at lines[len(lines)-1]. - current.Function = &Function{ - FunctionName: profile.StringTable[profile.Function[lines[len(lines)-1].FunctionId-1].Name], - FileName: profile.StringTable[profile.Function[lines[len(lines)-1].FunctionId-1].Filename], - Line: lines[len(lines)-1].Line, - } - lines = lines[:len(lines)-1] - - // If there are more than one line, each line inlined into the next line. - for len(lines) > 0 { - current.Inlined = append(current.Inlined, &Function{ - FunctionName: profile.StringTable[profile.Function[lines[0].FunctionId-1].Name], - FileName: profile.StringTable[profile.Function[lines[0].FunctionId-1].Filename], - Line: lines[0].Line, - }) - lines = lines[1:] - } - parent := &ProfileTree{ - Nodes: []*ProfileTree{current}, - } - current.Parent = parent - current = parent - locations = locations[1:] - } - if current.Function == nil { - current.Function = &Function{ +// treeFromSample creates a linked tree form a single pprof sample. As a single sample is just a single stack the tree +// will also be just a simple linked list at this point. +func treeFromSample(profile *googlev1.Profile, sample *googlev1.Sample, startLevel int) *ProfileTree { + root := &ProfileTree{ + Value: sample.Value[0], + Level: startLevel, + locationID: 0, + Function: &Function{ FunctionName: "root", - } - current.Value = sample.Value[0] - current.locationID = 0 - current.Self = 0 - current.Level = 0 + }, } - return current + + if len(sample.LocationId) == 0 { + // Empty profile + return root + } + + locations := getReversedLocations(profile, sample) + parent := root + + // Loop over locations and add a node to the tree for each location + for index, location := range locations { + node := &ProfileTree{ + Self: 0, + Value: sample.Value[0], + Level: index + startLevel + 1, + locationID: location.Id, + Parent: parent, + } + + parent.Nodes = []*ProfileTree{node} + parent = node + + functions := getFunctions(profile, location) + // Last in the list is the main function + node.Function = functions[len(functions)-1] + // If there are more, other are inlined functions + if len(functions) > 1 { + node.Inlined = functions[:len(functions)-1] + } + } + // Last parent is a leaf and as it does not have any children it's value is also self + parent.Self = sample.Value[0] + return root } func profileAsTree(profile *googlev1.Profile) *ProfileTree { @@ -293,13 +305,50 @@ func profileAsTree(profile *googlev1.Profile) *ProfileTree { if len(profile.Sample) == 0 { return nil } - n := treeFromSample(profile, profile.Sample[0]) + n := treeFromSample(profile, profile.Sample[0], 0) for _, sample := range profile.Sample[1:] { - n.merge(treeFromSample(profile, sample)) + n.addSample(profile, sample) } return n } +// getReversedLocations returns all locations from a sample. Location is a one level in the stack trace so single row in +// flamegraph. Returned locations are reversed (so root is 0, leaf is len - 1) which makes it easier to the use with +// tree structure starting from root. +func getReversedLocations(profile *googlev1.Profile, sample *googlev1.Sample) []*googlev1.Location { + locations := make([]*googlev1.Location, len(sample.LocationId)) + for index, locationId := range sample.LocationId { + // profile.Location[locationId-1] is because locationId (and other IDs) is 1 based, so + // locationId == array index + 1 + locations[len(sample.LocationId)-1-index] = profile.Location[locationId-1] + } + return locations +} + +// getFunctions returns all functions for a location. First one is the main function and the rest are inlined functions. +// If there is no info it just returns single placeholder function. +func getFunctions(profile *googlev1.Profile, location *googlev1.Location) []*Function { + if len(location.Line) == 0 { + return []*Function{{ + FunctionName: "", + FileName: "", + Line: 0, + }} + } + functions := make([]*Function, len(location.Line)) + + for index, line := range location.Line { + function := profile.Function[line.FunctionId-1] + + functions[index] = &Function{ + FunctionName: profile.StringTable[function.Name], + FileName: profile.StringTable[function.Filename], + Line: line.Line, + } + } + return functions +} + type CustomMeta struct { ProfileTypeID string } diff --git a/pkg/tsdb/phlare/query_test.go b/pkg/tsdb/phlare/query_test.go index d880a80a24f..e98413c43aa 100644 --- a/pkg/tsdb/phlare/query_test.go +++ b/pkg/tsdb/phlare/query_test.go @@ -2,6 +2,8 @@ package phlare import ( "context" + "encoding/json" + "os" "testing" "time" @@ -154,12 +156,11 @@ func Test_treeFromSample(t *testing.T) { }{ { name: "empty lines", - s: &googlev1.Sample{LocationId: []uint64{1, 2, 3}, Value: []int64{10}}, + s: &googlev1.Sample{LocationId: []uint64{1, 2}, Value: []int64{10}}, p: &googlev1.Profile{ Location: []*googlev1.Location{ {Id: 1, Line: []*googlev1.Line{}}, {Id: 2, Line: []*googlev1.Line{}}, - {Id: 3, Line: []*googlev1.Line{}}, }, Function: []*googlev1.Function{}, }, @@ -168,6 +169,27 @@ func Test_treeFromSample(t *testing.T) { Function: &Function{ FunctionName: "root", }, + Nodes: []*ProfileTree{ + { + Value: 10, + Function: &Function{ + FunctionName: "", + }, + Level: 1, + locationID: 2, + Nodes: []*ProfileTree{ + { + Value: 10, + Function: &Function{ + FunctionName: "", + }, + Level: 2, + Self: 10, + locationID: 1, + }, + }, + }, + }, }, }, { @@ -238,14 +260,14 @@ func Test_treeFromSample(t *testing.T) { } { t.Run(tc.name, func(t *testing.T) { setParents(tc.want) - actual := treeFromSample(tc.p, tc.s) + actual := treeFromSample(tc.p, tc.s, 0) require.Equal(t, tc.want, actual, "want\n%s\n got\n%s", tc.want, actual) }) } } func Test_TreeString(t *testing.T) { - t.Log(treeFromSample(fooProfile, &googlev1.Sample{LocationId: []uint64{3, 2, 1}, Value: []int64{10}})) + t.Log(treeFromSample(fooProfile, &googlev1.Sample{LocationId: []uint64{3, 2, 1}, Value: []int64{10}}, 0)) } func Test_profileAsTree(t *testing.T) { @@ -324,8 +346,9 @@ func Test_profileAsTree(t *testing.T) { Sample: []*googlev1.Sample{ {LocationId: []uint64{3, 2, 1}, Value: []int64{15}}, // foo -> bar -> baz {LocationId: []uint64{3, 2, 1}, Value: []int64{30}}, // foo -> bar -> baz - {LocationId: []uint64{3, 2}, Value: []int64{20}}, // bar -> baz - {LocationId: []uint64{2, 1}, Value: []int64{40}}, // foo -> bar + {LocationId: []uint64{1, 2, 1}, Value: []int64{20}}, // foo -> bar -> foo + {LocationId: []uint64{3, 2}, Value: []int64{20}}, // bar -> baz + {LocationId: []uint64{2, 1}, Value: []int64{40}}, // foo -> bar {LocationId: []uint64{1}, Value: []int64{5}}, // foo {LocationId: []uint64{}, Value: []int64{5}}, }, @@ -334,55 +357,11 @@ func Test_profileAsTree(t *testing.T) { StringTable: fooProfile.StringTable, }, want: &ProfileTree{ - Value: 110, + Value: 130, Function: &Function{ FunctionName: "root", }, Nodes: []*ProfileTree{ - { - Value: 90, - Self: 5, - locationID: 1, - Level: 1, - Function: &Function{ - FunctionName: "foo", - FileName: "file1", - Line: 1, - }, - Inlined: []*Function{ - { - FunctionName: "inline", - FileName: "file2", - Line: 5, - }, - }, - Nodes: []*ProfileTree{ - { - Value: 85, - Self: 40, - locationID: 2, - Level: 2, - Function: &Function{ - FunctionName: "bar", - FileName: "file1", - Line: 2, - }, - Nodes: []*ProfileTree{ - { - Value: 45, - Self: 45, - locationID: 3, - Level: 3, - Function: &Function{ - FunctionName: "baz", - FileName: "file2", - Line: 3, - }, - }, - }, - }, - }, - }, { locationID: 2, Value: 20, @@ -407,6 +386,68 @@ func Test_profileAsTree(t *testing.T) { }, }, }, + { + Value: 110, + Self: 5, + locationID: 1, + Level: 1, + Function: &Function{ + FunctionName: "foo", + FileName: "file1", + Line: 1, + }, + Inlined: []*Function{ + { + FunctionName: "inline", + FileName: "file2", + Line: 5, + }, + }, + Nodes: []*ProfileTree{ + { + Value: 105, + Self: 40, + locationID: 2, + Level: 2, + Function: &Function{ + FunctionName: "bar", + FileName: "file1", + Line: 2, + }, + Nodes: []*ProfileTree{ + { + Value: 20, + Self: 20, + locationID: 1, + Level: 3, + Function: &Function{ + FunctionName: "foo", + FileName: "file1", + Line: 1, + }, + Inlined: []*Function{ + { + FunctionName: "inline", + FileName: "file2", + Line: 5, + }, + }, + }, + { + Value: 45, + Self: 45, + locationID: 3, + Level: 3, + Function: &Function{ + FunctionName: "baz", + FileName: "file2", + Line: 3, + }, + }, + }, + }, + }, + }, }, }, }, @@ -421,6 +462,20 @@ func Test_profileAsTree(t *testing.T) { } } +func Benchmark_profileAsTree(b *testing.B) { + profJson, err := os.ReadFile("./testdata/profile_response.json") + require.NoError(b, err) + var prof *googlev1.Profile + err = json.Unmarshal(profJson, &prof) + require.NoError(b, err) + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + profileAsTree(prof) + } +} + func setParents(root *ProfileTree) { for _, n := range root.Nodes { n.Parent = root diff --git a/pkg/tsdb/phlare/testdata/profile_response.json b/pkg/tsdb/phlare/testdata/profile_response.json new file mode 100644 index 00000000000..f0d2f30694b --- /dev/null +++ b/pkg/tsdb/phlare/testdata/profile_response.json @@ -0,0 +1 @@ +{"sample_type":[{"type":1,"unit":2}],"sample":[{"location_id":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,15,21,15,22,15,23,24,15,25,26],"value":[10000000]},{"location_id":[27,28,29,30,31],"value":[10000000]},{"location_id":[32,33,28,29,30,31],"value":[20000000]},{"location_id":[34,35,36,37,38,39,12,13,14,15,16,17,18,19,20,15,21,15,22,15,23,24,15,25,26],"value":[10000000]},{"location_id":[32,33,40,29,41,31],"value":[10000000]},{"location_id":[42,43,44,45,46,47,48,49,50,51,52,53,54,37,38,39,12,13,14,15,16,17,18,19,20,15,21,15,22,15,23,24,15,25,26],"value":[10000000]},{"location_id":[55,56,57,58,29,30,31],"value":[20000000]},{"location_id":[59,60,61,62,44,63],"value":[10000000]},{"location_id":[64,65,66,67,68,69,70,71],"value":[10000000]},{"location_id":[72,73,74],"value":[10000000]},{"location_id":[75,76,77,78,79,80,81,82,83,84],"value":[10000000]},{"location_id":[85,86,87,88,89,90,70,71],"value":[10000000]},{"location_id":[91,92,93,94,95,38,39,12,13,14,15,16,17,18,19,20,15,21,15,22,15,23,24,15,25,26],"value":[10000000]},{"location_id":[96,60,61,97,44,63],"value":[10000000]},{"location_id":[98,62,44,63],"value":[10000000]},{"location_id":[99,100,29,30,31],"value":[10000000]},{"location_id":[101,92,93,94,9,10,11,12,13,14,15,16,17,18,19,20,15,21,15,22,15,23,24,15,25,26],"value":[10000000]},{"location_id":[102,103,104,105,106,97,44,63],"value":[10000000]},{"location_id":[107,108,109,110,111,112,113,49,114,115,116,15,117],"value":[10000000]},{"location_id":[118,119,120],"value":[10000000]},{"location_id":[121,122,10,11,12,13,14,15,16,17,18,19,20,15,21,15,22,15,23,24,15,25,26],"value":[10000000]},{"location_id":[123,61,97,44,63],"value":[20000000]},{"location_id":[124,60,61,97,44,63],"value":[10000000]},{"location_id":[125,126,127,128,17,18,19,20,15,21,15,22,15,117],"value":[10000000]},{"location_id":[129,130,131,132,110,111,133,134,135,136,137,138,139,140,141,142,143],"value":[10000000]},{"location_id":[144,145,146,147,148,149,150,151,12,13,14,15,16,17,18,19,20,15,21,15,22,15,23,24,15,25,26],"value":[10000000]},{"location_id":[152,153,154,155,156,157,86,87,88,89,90,70,71],"value":[10000000]},{"location_id":[158,159,160,161,162,163,164,165,166,167],"value":[10000000]},{"location_id":[168,41,31],"value":[10000000]},{"location_id":[169,170,171,172,173,174,175,176,177,83,84],"value":[10000000]},{"location_id":[178,179,180,181,182,29,41,31],"value":[10000000]},{"location_id":[183,184,155,185,151,12,13,14,15,16,17,18,19,20,15,21,15,22,15,23,24,15,25,26],"value":[10000000]},{"location_id":[186,187,188,54,37,38,39,12,13,14,15,16,17,18,19,20,15,21,15,22,15,23,24,15,25,26],"value":[10000000]},{"location_id":[189,190,191,192,193,194,195,29,196,197,198,199],"value":[10000000]},{"location_id":[200,201,49,202,203,37,38,39,12,13,14,15,16,17,18,19,20,15,21,15,22,15,23,24,15,25,26],"value":[10000000]},{"location_id":[204,205,206,207,41,31],"value":[10000000]},{"location_id":[208,209,111,210,211,212,213,214,215,216,217,218],"value":[10000000]},{"location_id":[219,220,221,222,44,223,74],"value":[10000000]},{"location_id":[224,225,226,227,228,229,68,230,231,232,233,234,235,236,237,238,128,17,18,19,20,15,21,15,22,15,117],"value":[10000000]},{"location_id":[239,240,241,191,242,221,243,44,244,73,74],"value":[10000000]},{"location_id":[245,246,110,111,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261],"value":[10000000]},{"location_id":[262,263,264,265,266,267,268,176,177,83,84],"value":[10000000]},{"location_id":[178,179,180,181,182,29,30,31],"value":[10000000]},{"location_id":[269,270,109,110,111,271,201,49,114,115,116,15,117],"value":[10000000]},{"location_id":[272,273,274,275,275,276,277,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,15,21,15,22,15,23,24,15,25,26],"value":[10000000]},{"location_id":[42,43,44,45,278,173,174,175,279,280,15,22,15,117],"value":[10000000]},{"location_id":[281,282,283,284,285,15,22,15,117],"value":[10000000]},{"location_id":[158,159,160,286,287,288,289,290,291,292],"value":[10000000]},{"location_id":[293,92,294,295,296,297,298,299,295,300,301,302,303,304,305,306],"value":[10000000]},{"location_id":[307,60,61,97,44,63],"value":[10000000]},{"location_id":[308,309,310,311,232,233,234,235,236,237,238,128,17,18,19,20,15,21,15,22,15,117],"value":[10000000]},{"location_id":[312,313,314,197,198,199],"value":[10000000]},{"location_id":[315,86,87,88,89,90,70,71],"value":[10000000]},{"location_id":[316,317,318,87,88,89,90,70,71],"value":[10000000]},{"location_id":[158,159,160,286,287,288,289,319,320,321],"value":[20000000]},{"location_id":[322,61,62,44,63],"value":[20000000]},{"location_id":[183,153,154,155,323,324,51,52,53,54,37,38,39,12,13,14,15,16,17,18,19,20,15,21,15,22,15,23,24,15,25,26],"value":[10000000]},{"location_id":[325,326,327,328,329,203,37,38,39,12,13,14,15,16,17,18,19,20,15,21,15,22,15,23,24,15,25,26],"value":[10000000]},{"location_id":[330,331,61,62,44,63],"value":[10000000]},{"location_id":[332,333,6,7,8,95,38,39,12,13,14,15,16,17,18,19,20,15,21,15,22,15,23,24,15,25,26],"value":[10000000]},{"location_id":[334],"value":[10000000]}],"mapping":[{"id":1,"memory_start":4194304,"memory_limit":50147328,"filename":3,"has_functions":true}],"location":[{"id":1,"mapping_id":1,"address":6299428,"line":[{"function_id":1,"line":207}]},{"id":2,"mapping_id":1,"address":6300900,"line":[{"function_id":2,"line":308}]},{"id":3,"mapping_id":1,"address":6295972,"line":[{"function_id":3,"line":570}]},{"id":4,"mapping_id":1,"address":6294852,"line":[{"function_id":4,"line":509}]},{"id":5,"mapping_id":1,"address":6277496,"line":[{"function_id":5,"line":362}]},{"id":6,"mapping_id":1,"address":6283829,"line":[{"function_id":6,"line":638}]},{"id":7,"mapping_id":1,"address":6324326,"line":[{"function_id":7,"line":730},{"function_id":8,"line":242}]},{"id":8,"mapping_id":1,"address":15576920,"line":[{"function_id":9,"line":391}]},{"id":9,"mapping_id":1,"address":15598692,"line":[{"function_id":10,"line":65}]},{"id":10,"mapping_id":1,"address":15554500,"line":[{"function_id":11,"line":575}]},{"id":11,"mapping_id":1,"address":15553678,"line":[{"function_id":12,"line":541}]},{"id":12,"mapping_id":1,"address":15546922,"line":[{"function_id":13,"line":330}]},{"id":13,"mapping_id":1,"address":15607940,"line":[{"function_id":14,"line":253}]},{"id":14,"mapping_id":1,"address":15610909,"line":[{"function_id":15,"line":371}]},{"id":15,"mapping_id":1,"address":8310830,"line":[{"function_id":16,"line":2109}]},{"id":16,"mapping_id":1,"address":8317384,"line":[{"function_id":17,"line":2487}]},{"id":17,"mapping_id":1,"address":9400654,"line":[{"function_id":18,"line":210}]},{"id":18,"mapping_id":1,"address":15416248,"line":[{"function_id":19,"line":70}]},{"id":19,"mapping_id":1,"address":15326590,"line":[{"function_id":20,"line":84}]},{"id":20,"mapping_id":1,"address":15415262,"line":[{"function_id":21,"line":39},{"function_id":22,"line":69}]},{"id":21,"mapping_id":1,"address":46101048,"line":[{"function_id":23,"line":163}]},{"id":22,"mapping_id":1,"address":15253005,"line":[{"function_id":24,"line":154}]},{"id":23,"mapping_id":1,"address":15816570,"line":[{"function_id":25,"line":125}]},{"id":24,"mapping_id":1,"address":46114657,"line":[{"function_id":26,"line":32}]},{"id":25,"mapping_id":1,"address":8324139,"line":[{"function_id":27,"line":2947}]},{"id":26,"mapping_id":1,"address":8305414,"line":[{"function_id":28,"line":1991}]},{"id":27,"mapping_id":1,"address":4414857,"line":[{"function_id":29,"line":126}]},{"id":28,"mapping_id":1,"address":4464164,"line":[{"function_id":30,"line":2837}]},{"id":29,"mapping_id":1,"address":4468637,"line":[{"function_id":31,"line":3214}]},{"id":30,"mapping_id":1,"address":4469964,"line":[{"function_id":32,"line":3363}]},{"id":31,"mapping_id":1,"address":4637506,"line":[{"function_id":33,"line":448}]},{"id":32,"mapping_id":1,"address":4654496,"line":[{"function_id":34,"line":706}]},{"id":33,"mapping_id":1,"address":4414843,"line":[{"function_id":29,"line":126}]},{"id":34,"mapping_id":1,"address":4530900,"line":[{"function_id":35,"line":178}]},{"id":35,"mapping_id":1,"address":15581157,"line":[{"function_id":36,"line":601}]},{"id":36,"mapping_id":1,"address":15578362,"line":[{"function_id":37,"line":445}]},{"id":37,"mapping_id":1,"address":15550340,"line":[{"function_id":38,"line":456}]},{"id":38,"mapping_id":1,"address":15560036,"line":[{"function_id":39,"line":731}]},{"id":39,"mapping_id":1,"address":15559372,"line":[{"function_id":40,"line":683}]},{"id":40,"mapping_id":1,"address":4463087,"line":[{"function_id":30,"line":2647}]},{"id":41,"mapping_id":1,"address":4472667,"line":[{"function_id":41,"line":3540}]},{"id":42,"mapping_id":1,"address":4587702,"line":[{"function_id":42,"line":139}]},{"id":43,"mapping_id":1,"address":4600046,"line":[{"function_id":43,"line":925}]},{"id":44,"mapping_id":1,"address":4637640,"line":[{"function_id":44,"line":492}]},{"id":45,"mapping_id":1,"address":4599863,"line":[{"function_id":45,"line":924}]},{"id":46,"mapping_id":1,"address":4393769,"line":[{"function_id":46,"line":420}]},{"id":47,"mapping_id":1,"address":4251012,"line":[{"function_id":47,"line":1238}]},{"id":48,"mapping_id":1,"address":4249912,"line":[{"function_id":48,"line":1114}]},{"id":49,"mapping_id":1,"address":4250630,"line":[{"function_id":49,"line":1202}]},{"id":50,"mapping_id":1,"address":6284142,"line":[{"function_id":50,"line":666}]},{"id":51,"mapping_id":1,"address":6323576,"line":[{"function_id":51,"line":191}]},{"id":52,"mapping_id":1,"address":15564527,"line":[{"function_id":52,"line":146}]},{"id":53,"mapping_id":1,"address":15584182,"line":[{"function_id":36,"line":639}]},{"id":54,"mapping_id":1,"address":15578708,"line":[{"function_id":37,"line":459}]},{"id":55,"mapping_id":1,"address":4652277,"line":[{"function_id":53,"line":104}]},{"id":56,"mapping_id":1,"address":4570714,"line":[{"function_id":54,"line":31}]},{"id":57,"mapping_id":1,"address":4414487,"line":[{"function_id":55,"line":85}]},{"id":58,"mapping_id":1,"address":4464986,"line":[{"function_id":30,"line":2871}]},{"id":59,"mapping_id":1,"address":4335871,"line":[{"function_id":56,"line":1468}]},{"id":60,"mapping_id":1,"address":4334673,"line":[{"function_id":57,"line":1339}]},{"id":61,"mapping_id":1,"address":4332659,"line":[{"function_id":58,"line":1103}]},{"id":62,"mapping_id":1,"address":4317996,"line":[{"function_id":59,"line":1327}]},{"id":63,"mapping_id":1,"address":4317188,"line":[{"function_id":60,"line":1295}]},{"id":64,"mapping_id":1,"address":4249122,"line":[{"function_id":48,"line":1140}]},{"id":65,"mapping_id":1,"address":4546351,"line":[{"function_id":61,"line":114}]},{"id":66,"mapping_id":1,"address":15899588,"line":[{"function_id":62,"line":1239}]},{"id":67,"mapping_id":1,"address":46361617,"line":[{"function_id":63,"line":81}]},{"id":68,"mapping_id":1,"address":46362121,"line":[{"function_id":64,"line":98}]},{"id":69,"mapping_id":1,"address":47527428,"line":[{"function_id":65,"line":195}]},{"id":70,"mapping_id":1,"address":46055914,"line":[{"function_id":66,"line":222}]},{"id":71,"mapping_id":1,"address":46053517,"line":[{"function_id":67,"line":157}]},{"id":72,"mapping_id":1,"address":4313733,"line":[{"function_id":68,"line":19},{"function_id":69,"line":997}]},{"id":73,"mapping_id":1,"address":4312459,"line":[{"function_id":70,"line":918}]},{"id":74,"mapping_id":1,"address":4317508,"line":[{"function_id":60,"line":1367}]},{"id":75,"mapping_id":1,"address":4237967,"line":[{"function_id":71,"line":109}]},{"id":76,"mapping_id":1,"address":4237260,"line":[{"function_id":72,"line":55}]},{"id":77,"mapping_id":1,"address":4241109,"line":[{"function_id":73,"line":418}]},{"id":78,"mapping_id":1,"address":11024592,"line":[{"function_id":74,"line":212}]},{"id":79,"mapping_id":1,"address":15618831,"line":[{"function_id":75,"line":104}]},{"id":80,"mapping_id":1,"address":11023174,"line":[{"function_id":76,"line":156}]},{"id":81,"mapping_id":1,"address":47616230,"line":[{"function_id":77,"line":223}]},{"id":82,"mapping_id":1,"address":47667494,"line":[{"function_id":78,"line":283}]},{"id":83,"mapping_id":1,"address":47667175,"line":[{"function_id":79,"line":311}]},{"id":84,"mapping_id":1,"address":47666381,"line":[{"function_id":80,"line":264}]},{"id":85,"mapping_id":1,"address":46334595,"line":[{"function_id":81,"line":105}]},{"id":86,"mapping_id":1,"address":46256606,"line":[{"function_id":82,"line":679}]},{"id":87,"mapping_id":1,"address":46260129,"line":[{"function_id":83,"line":806}]},{"id":88,"mapping_id":1,"address":46359994,"line":[{"function_id":84,"line":884},{"function_id":85,"line":260}]},{"id":89,"mapping_id":1,"address":46370263,"line":[{"function_id":86,"line":309}]},{"id":90,"mapping_id":1,"address":47528038,"line":[{"function_id":65,"line":206}]},{"id":91,"mapping_id":1,"address":4649307,"line":[{"function_id":87,"line":95}]},{"id":92,"mapping_id":1,"address":4531240,"line":[{"function_id":35,"line":287}]},{"id":93,"mapping_id":1,"address":15595240,"line":[{"function_id":88,"line":94},{"function_id":89,"line":99}]},{"id":94,"mapping_id":1,"address":15576856,"line":[{"function_id":9,"line":389}]},{"id":95,"mapping_id":1,"address":15550116,"line":[{"function_id":38,"line":468}]},{"id":96,"mapping_id":1,"address":4336141,"line":[{"function_id":90,"line":153},{"function_id":56,"line":1508}]},{"id":97,"mapping_id":1,"address":4318052,"line":[{"function_id":59,"line":1308}]},{"id":98,"mapping_id":1,"address":4332527,"line":[{"function_id":91,"line":235},{"function_id":58,"line":1088}]},{"id":99,"mapping_id":1,"address":4466911,"line":[{"function_id":92,"line":3004}]},{"id":100,"mapping_id":1,"address":4463940,"line":[{"function_id":30,"line":2812}]},{"id":101,"mapping_id":1,"address":4649298,"line":[{"function_id":87,"line":93}]},{"id":102,"mapping_id":1,"address":4278803,"line":[{"function_id":93,"line":396}]},{"id":103,"mapping_id":1,"address":4334007,"line":[{"function_id":94,"line":1237}]},{"id":104,"mapping_id":1,"address":4325828,"line":[{"function_id":95,"line":285}]},{"id":105,"mapping_id":1,"address":4325142,"line":[{"function_id":96,"line":176}]},{"id":106,"mapping_id":1,"address":4333118,"line":[{"function_id":58,"line":1069}]},{"id":107,"mapping_id":1,"address":4535224,"line":[{"function_id":97,"line":605}]},{"id":108,"mapping_id":1,"address":4536019,"line":[{"function_id":98,"line":699}]},{"id":109,"mapping_id":1,"address":4589996,"line":[{"function_id":42,"line":345}]},{"id":110,"mapping_id":1,"address":4537972,"line":[{"function_id":99,"line":932}]},{"id":111,"mapping_id":1,"address":4539382,"line":[{"function_id":100,"line":1112}]},{"id":112,"mapping_id":1,"address":4327792,"line":[{"function_id":101,"line":404}]},{"id":113,"mapping_id":1,"address":4248382,"line":[{"function_id":48,"line":906}]},{"id":114,"mapping_id":1,"address":13006764,"line":[{"function_id":102,"line":136}]},{"id":115,"mapping_id":1,"address":13074844,"line":[{"function_id":103,"line":360}]},{"id":116,"mapping_id":1,"address":15251970,"line":[{"function_id":24,"line":122}]},{"id":117,"mapping_id":1,"address":10399714,"line":[{"function_id":104,"line":2299}]},{"id":118,"mapping_id":1,"address":13081348,"line":[{"function_id":105,"line":152}]},{"id":119,"mapping_id":1,"address":13018414,"line":[{"function_id":106,"line":286}]},{"id":120,"mapping_id":1,"address":13018235,"line":[{"function_id":107,"line":298}]},{"id":121,"mapping_id":1,"address":4555805,"line":[{"function_id":108,"line":1100},{"function_id":109,"line":723}]},{"id":122,"mapping_id":1,"address":15599401,"line":[{"function_id":10,"line":38}]},{"id":123,"mapping_id":1,"address":4334568,"line":[{"function_id":57,"line":1312}]},{"id":124,"mapping_id":1,"address":4335919,"line":[{"function_id":110,"line":272},{"function_id":56,"line":1483}]},{"id":125,"mapping_id":1,"address":50003191,"line":[{"function_id":111,"line":81},{"function_id":112,"line":127}]},{"id":126,"mapping_id":1,"address":50025476,"line":[{"function_id":113,"line":434}]},{"id":127,"mapping_id":1,"address":50042886,"line":[{"function_id":114,"line":160}]},{"id":128,"mapping_id":1,"address":9310116,"line":[{"function_id":115,"line":229}]},{"id":129,"mapping_id":1,"address":4560991,"line":[{"function_id":116,"line":1121}]},{"id":130,"mapping_id":1,"address":4557800,"line":[{"function_id":117,"line":918}]},{"id":131,"mapping_id":1,"address":4560019,"line":[{"function_id":118,"line":1041}]},{"id":132,"mapping_id":1,"address":4588456,"line":[{"function_id":42,"line":202}]},{"id":133,"mapping_id":1,"address":13074011,"line":[{"function_id":119,"line":211}]},{"id":134,"mapping_id":1,"address":13068484,"line":[{"function_id":120,"line":208}]},{"id":135,"mapping_id":1,"address":15242966,"line":[{"function_id":121,"line":221}]},{"id":136,"mapping_id":1,"address":15241540,"line":[{"function_id":122,"line":173}]},{"id":137,"mapping_id":1,"address":46098562,"line":[{"function_id":123,"line":59}]},{"id":138,"mapping_id":1,"address":46098110,"line":[{"function_id":124,"line":43}]},{"id":139,"mapping_id":1,"address":8017654,"line":[{"function_id":125,"line":251}]},{"id":140,"mapping_id":1,"address":8015738,"line":[{"function_id":126,"line":175}]},{"id":141,"mapping_id":1,"address":8024827,"line":[{"function_id":127,"line":715}]},{"id":142,"mapping_id":1,"address":8022488,"line":[{"function_id":128,"line":581}]},{"id":143,"mapping_id":1,"address":9295587,"line":[{"function_id":129,"line":244}]},{"id":144,"mapping_id":1,"address":4530060,"line":[{"function_id":35,"line":194}]},{"id":145,"mapping_id":1,"address":15589325,"line":[{"function_id":130,"line":757},{"function_id":131,"line":753}]},{"id":146,"mapping_id":1,"address":15588830,"line":[{"function_id":132,"line":748}]},{"id":147,"mapping_id":1,"address":15587783,"line":[{"function_id":133,"line":648}]},{"id":148,"mapping_id":1,"address":15575060,"line":[{"function_id":134,"line":270}]},{"id":149,"mapping_id":1,"address":15547909,"line":[{"function_id":135,"line":378}]},{"id":150,"mapping_id":1,"address":15563226,"line":[{"function_id":136,"line":877}]},{"id":151,"mapping_id":1,"address":15562140,"line":[{"function_id":137,"line":858}]},{"id":152,"mapping_id":1,"address":4649302,"line":[{"function_id":87,"line":94}]},{"id":153,"mapping_id":1,"address":4250460,"line":[{"function_id":138,"line":1194}]},{"id":154,"mapping_id":1,"address":4250020,"line":[{"function_id":48,"line":1126}]},{"id":155,"mapping_id":1,"address":4529745,"line":[{"function_id":139,"line":103}]},{"id":156,"mapping_id":1,"address":46261978,"line":[{"function_id":140,"line":85}]},{"id":157,"mapping_id":1,"address":46331889,"line":[{"function_id":81,"line":62}]},{"id":158,"mapping_id":1,"address":4208974,"line":[{"function_id":141,"line":36}]},{"id":159,"mapping_id":1,"address":4208946,"line":[{"function_id":142,"line":38}]},{"id":160,"mapping_id":1,"address":4749095,"line":[{"function_id":143,"line":81}]},{"id":161,"mapping_id":1,"address":4740900,"line":[{"function_id":144,"line":696}]},{"id":162,"mapping_id":1,"address":5112676,"line":[{"function_id":145,"line":183},{"function_id":146,"line":794},{"function_id":147,"line":163}]},{"id":163,"mapping_id":1,"address":5460136,"line":[{"function_id":148,"line":55}]},{"id":164,"mapping_id":1,"address":5543332,"line":[{"function_id":149,"line":183}]},{"id":165,"mapping_id":1,"address":4695289,"line":[{"function_id":150,"line":332}]},{"id":166,"mapping_id":1,"address":10320100,"line":[{"function_id":151,"line":351},{"function_id":152,"line":506}]},{"id":167,"mapping_id":1,"address":10364112,"line":[{"function_id":153,"line":818}]},{"id":168,"mapping_id":1,"address":4468968,"line":[{"function_id":31,"line":3245}]},{"id":169,"mapping_id":1,"address":4557759,"line":[{"function_id":117,"line":918}]},{"id":170,"mapping_id":1,"address":4560713,"line":[{"function_id":154,"line":1084}]},{"id":171,"mapping_id":1,"address":4551000,"line":[{"function_id":155,"line":115}]},{"id":172,"mapping_id":1,"address":4235902,"line":[{"function_id":156,"line":223}]},{"id":173,"mapping_id":1,"address":5974308,"line":[{"function_id":157,"line":86}]},{"id":174,"mapping_id":1,"address":5973363,"line":[{"function_id":158,"line":20}]},{"id":175,"mapping_id":1,"address":5968016,"line":[{"function_id":159,"line":162}]},{"id":176,"mapping_id":1,"address":47672436,"line":[{"function_id":160,"line":364}]},{"id":177,"mapping_id":1,"address":47667965,"line":[{"function_id":78,"line":289}]},{"id":178,"mapping_id":1,"address":4653955,"line":[{"function_id":161,"line":560}]},{"id":179,"mapping_id":1,"address":4415541,"line":[{"function_id":162,"line":69}]},{"id":180,"mapping_id":1,"address":4243654,"line":[{"function_id":163,"line":160}]},{"id":181,"mapping_id":1,"address":4459211,"line":[{"function_id":164,"line":1457},{"function_id":165,"line":2247}]},{"id":182,"mapping_id":1,"address":4464999,"line":[{"function_id":30,"line":2874}]},{"id":183,"mapping_id":1,"address":4649312,"line":[{"function_id":87,"line":96}]},{"id":184,"mapping_id":1,"address":4249413,"line":[{"function_id":48,"line":1022}]},{"id":185,"mapping_id":1,"address":15562362,"line":[{"function_id":136,"line":866}]},{"id":186,"mapping_id":1,"address":4248606,"line":[{"function_id":166,"line":413},{"function_id":48,"line":966}]},{"id":187,"mapping_id":1,"address":4531211,"line":[{"function_id":35,"line":284}]},{"id":188,"mapping_id":1,"address":15581010,"line":[{"function_id":36,"line":599}]},{"id":189,"mapping_id":1,"address":4409707,"line":[{"function_id":167,"line":766}]},{"id":190,"mapping_id":1,"address":4290450,"line":[{"function_id":168,"line":272}]},{"id":191,"mapping_id":1,"address":4290872,"line":[{"function_id":169,"line":326}]},{"id":192,"mapping_id":1,"address":4486027,"line":[{"function_id":170,"line":4952}]},{"id":193,"mapping_id":1,"address":4459252,"line":[{"function_id":165,"line":2248}]},{"id":194,"mapping_id":1,"address":4461993,"line":[{"function_id":171,"line":2505}]},{"id":195,"mapping_id":1,"address":4462542,"line":[{"function_id":30,"line":2571}]},{"id":196,"mapping_id":1,"address":4470212,"line":[{"function_id":172,"line":3378}]},{"id":197,"mapping_id":1,"address":4470964,"line":[{"function_id":173,"line":3406}]},{"id":198,"mapping_id":1,"address":4539193,"line":[{"function_id":100,"line":1070}]},{"id":199,"mapping_id":1,"address":4637866,"line":[{"function_id":174,"line":570}]},{"id":200,"mapping_id":1,"address":4282737,"line":[{"function_id":175,"line":1032}]},{"id":201,"mapping_id":1,"address":4249708,"line":[{"function_id":48,"line":1050}]},{"id":202,"mapping_id":1,"address":15573924,"line":[{"function_id":176,"line":232}]},{"id":203,"mapping_id":1,"address":15578340,"line":[{"function_id":37,"line":443}]},{"id":204,"mapping_id":1,"address":4459684,"line":[{"function_id":177,"line":2332}]},{"id":205,"mapping_id":1,"address":4460985,"line":[{"function_id":178,"line":2430}]},{"id":206,"mapping_id":1,"address":4467588,"line":[{"function_id":179,"line":3110}]},{"id":207,"mapping_id":1,"address":4468676,"line":[{"function_id":31,"line":3220}]},{"id":208,"mapping_id":1,"address":4534282,"line":[{"function_id":180,"line":395}]},{"id":209,"mapping_id":1,"address":4537445,"line":[{"function_id":99,"line":873}]},{"id":210,"mapping_id":1,"address":10304746,"line":[{"function_id":181,"line":1}]},{"id":211,"mapping_id":1,"address":4254813,"line":[{"function_id":182,"line":417}]},{"id":212,"mapping_id":1,"address":10302308,"line":[{"function_id":183,"line":104}]},{"id":213,"mapping_id":1,"address":10287128,"line":[{"function_id":184,"line":92}]},{"id":214,"mapping_id":1,"address":10285202,"line":[{"function_id":185,"line":62}]},{"id":215,"mapping_id":1,"address":10489329,"line":[{"function_id":186,"line":195}]},{"id":216,"mapping_id":1,"address":10492596,"line":[{"function_id":187,"line":367}]},{"id":217,"mapping_id":1,"address":10489704,"line":[{"function_id":188,"line":217}]},{"id":218,"mapping_id":1,"address":10364582,"line":[{"function_id":189,"line":847}]},{"id":219,"mapping_id":1,"address":4361199,"line":[{"function_id":190,"line":252}]},{"id":220,"mapping_id":1,"address":4311283,"line":[{"function_id":191,"line":824}]},{"id":221,"mapping_id":1,"address":4454937,"line":[{"function_id":192,"line":1613}]},{"id":222,"mapping_id":1,"address":4311422,"line":[{"function_id":193,"line":814}]},{"id":223,"mapping_id":1,"address":4311991,"line":[{"function_id":70,"line":807}]},{"id":224,"mapping_id":1,"address":46305740,"line":[{"function_id":194,"line":503}]},{"id":225,"mapping_id":1,"address":46290071,"line":[{"function_id":195,"line":356}]},{"id":226,"mapping_id":1,"address":46291006,"line":[{"function_id":196,"line":413}]},{"id":227,"mapping_id":1,"address":46355208,"line":[{"function_id":197,"line":305}]},{"id":228,"mapping_id":1,"address":4696557,"line":[{"function_id":198,"line":409}]},{"id":229,"mapping_id":1,"address":46361508,"line":[{"function_id":199,"line":386},{"function_id":63,"line":75}]},{"id":230,"mapping_id":1,"address":46362404,"line":[{"function_id":200,"line":103}]},{"id":231,"mapping_id":1,"address":49754716,"line":[{"function_id":201,"line":191}]},{"id":232,"mapping_id":1,"address":49768709,"line":[{"function_id":202,"line":164}]},{"id":233,"mapping_id":1,"address":49753939,"line":[{"function_id":203,"line":183}]},{"id":234,"mapping_id":1,"address":49769124,"line":[{"function_id":204,"line":163}]},{"id":235,"mapping_id":1,"address":49754104,"line":[{"function_id":205,"line":187}]},{"id":236,"mapping_id":1,"address":16088016,"line":[{"function_id":206,"line":51}]},{"id":237,"mapping_id":1,"address":45947183,"line":[{"function_id":207,"line":47}]},{"id":238,"mapping_id":1,"address":16086610,"line":[{"function_id":208,"line":75}]},{"id":239,"mapping_id":1,"address":4356880,"line":[{"function_id":209,"line":643}]},{"id":240,"mapping_id":1,"address":4292068,"line":[{"function_id":210,"line":226}]},{"id":241,"mapping_id":1,"address":4290628,"line":[{"function_id":168,"line":290}]},{"id":242,"mapping_id":1,"address":4313020,"line":[{"function_id":211,"line":1079}]},{"id":243,"mapping_id":1,"address":4313092,"line":[{"function_id":212,"line":1078}]},{"id":244,"mapping_id":1,"address":4314582,"line":[{"function_id":69,"line":1077}]},{"id":245,"mapping_id":1,"address":4556976,"line":[{"function_id":213,"line":691},{"function_id":214,"line":835}]},{"id":246,"mapping_id":1,"address":4588823,"line":[{"function_id":42,"line":250}]},{"id":247,"mapping_id":1,"address":6317700,"line":[{"function_id":215,"line":103}]},{"id":248,"mapping_id":1,"address":9289921,"line":[{"function_id":216,"line":126}]},{"id":249,"mapping_id":1,"address":9287532,"line":[{"function_id":217,"line":79}]},{"id":250,"mapping_id":1,"address":9349945,"line":[{"function_id":218,"line":853}]},{"id":251,"mapping_id":1,"address":9332076,"line":[{"function_id":219,"line":817},{"function_id":220,"line":388}]},{"id":252,"mapping_id":1,"address":9318866,"line":[{"function_id":221,"line":178}]},{"id":253,"mapping_id":1,"address":16106841,"line":[{"function_id":222,"line":292}]},{"id":254,"mapping_id":1,"address":16074948,"line":[{"function_id":223,"line":86}]},{"id":255,"mapping_id":1,"address":45947502,"line":[{"function_id":207,"line":42}]},{"id":256,"mapping_id":1,"address":16074295,"line":[{"function_id":224,"line":103}]},{"id":257,"mapping_id":1,"address":16109743,"line":[{"function_id":225,"line":121}]},{"id":258,"mapping_id":1,"address":16068792,"line":[{"function_id":226,"line":115}]},{"id":259,"mapping_id":1,"address":47530247,"line":[{"function_id":227,"line":323}]},{"id":260,"mapping_id":1,"address":47529277,"line":[{"function_id":228,"line":280}]},{"id":261,"mapping_id":1,"address":47529031,"line":[{"function_id":229,"line":266}]},{"id":262,"mapping_id":1,"address":4405191,"line":[{"function_id":230,"line":178}]},{"id":263,"mapping_id":1,"address":4291178,"line":[{"function_id":231,"line":109}]},{"id":264,"mapping_id":1,"address":4289457,"line":[{"function_id":232,"line":181}]},{"id":265,"mapping_id":1,"address":4247684,"line":[{"function_id":233,"line":819}]},{"id":266,"mapping_id":1,"address":4249319,"line":[{"function_id":48,"line":1018}]},{"id":267,"mapping_id":1,"address":4531273,"line":[{"function_id":35,"line":290}]},{"id":268,"mapping_id":1,"address":5967510,"line":[{"function_id":159,"line":152}]},{"id":269,"mapping_id":1,"address":4542990,"line":[{"function_id":108,"line":1100},{"function_id":234,"line":1345}]},{"id":270,"mapping_id":1,"address":4535826,"line":[{"function_id":98,"line":667}]},{"id":271,"mapping_id":1,"address":4285451,"line":[{"function_id":175,"line":844}]},{"id":272,"mapping_id":1,"address":4855940,"line":[{"function_id":235,"line":12}]},{"id":273,"mapping_id":1,"address":4857636,"line":[{"function_id":236,"line":73}]},{"id":274,"mapping_id":1,"address":4857545,"line":[{"function_id":236,"line":125}]},{"id":275,"mapping_id":1,"address":4857476,"line":[{"function_id":236,"line":121}]},{"id":276,"mapping_id":1,"address":4847740,"line":[{"function_id":237,"line":48}]},{"id":277,"mapping_id":1,"address":6300868,"line":[{"function_id":238,"line":332},{"function_id":2,"line":305}]},{"id":278,"mapping_id":1,"address":4235751,"line":[{"function_id":156,"line":219}]},{"id":279,"mapping_id":1,"address":13216050,"line":[{"function_id":239,"line":65}]},{"id":280,"mapping_id":1,"address":46101898,"line":[{"function_id":23,"line":184}]},{"id":281,"mapping_id":1,"address":5266649,"line":[{"function_id":240,"line":443}]},{"id":282,"mapping_id":1,"address":5259117,"line":[{"function_id":241,"line":97}]},{"id":283,"mapping_id":1,"address":8244292,"line":[{"function_id":242,"line":212}]},{"id":284,"mapping_id":1,"address":46107653,"line":[{"function_id":243,"line":193},{"function_id":244,"line":218}]},{"id":285,"mapping_id":1,"address":46100210,"line":[{"function_id":23,"line":118}]},{"id":286,"mapping_id":1,"address":4741348,"line":[{"function_id":245,"line":924}]},{"id":287,"mapping_id":1,"address":5120909,"line":[{"function_id":246,"line":211},{"function_id":146,"line":794},{"function_id":247,"line":383}]},{"id":288,"mapping_id":1,"address":5462312,"line":[{"function_id":248,"line":96}]},{"id":289,"mapping_id":1,"address":5543812,"line":[{"function_id":249,"line":195}]},{"id":290,"mapping_id":1,"address":8412622,"line":[{"function_id":250,"line":1767}]},{"id":291,"mapping_id":1,"address":5930689,"line":[{"function_id":251,"line":629}]},{"id":292,"mapping_id":1,"address":8425003,"line":[{"function_id":252,"line":2408}]},{"id":293,"mapping_id":1,"address":4649545,"line":[{"function_id":87,"line":183}]},{"id":294,"mapping_id":1,"address":5224438,"line":[{"function_id":253,"line":82},{"function_id":254,"line":1018}]},{"id":295,"mapping_id":1,"address":5200600,"line":[{"function_id":255,"line":219}]},{"id":296,"mapping_id":1,"address":7461715,"line":[{"function_id":256,"line":30}]},{"id":297,"mapping_id":1,"address":5210494,"line":[{"function_id":257,"line":651}]},{"id":298,"mapping_id":1,"address":5213786,"line":[{"function_id":258,"line":740}]},{"id":299,"mapping_id":1,"address":5224775,"line":[{"function_id":254,"line":1057}]},{"id":300,"mapping_id":1,"address":10240941,"line":[{"function_id":259,"line":43}]},{"id":301,"mapping_id":1,"address":10948027,"line":[{"function_id":260,"line":1628}]},{"id":302,"mapping_id":1,"address":10908916,"line":[{"function_id":261,"line":254}]},{"id":303,"mapping_id":1,"address":46120330,"line":[{"function_id":262,"line":105},{"function_id":263,"line":73}]},{"id":304,"mapping_id":1,"address":46081463,"line":[{"function_id":264,"line":113}]},{"id":305,"mapping_id":1,"address":49900501,"line":[{"function_id":265,"line":60}]},{"id":306,"mapping_id":1,"address":14455405,"line":[{"function_id":266,"line":62}]},{"id":307,"mapping_id":1,"address":4335933,"line":[{"function_id":267,"line":696},{"function_id":56,"line":1489}]},{"id":308,"mapping_id":1,"address":4273096,"line":[{"function_id":268,"line":192}]},{"id":309,"mapping_id":1,"address":49644309,"line":[{"function_id":269,"line":190}]},{"id":310,"mapping_id":1,"address":49510980,"line":[{"function_id":270,"line":329}]},{"id":311,"mapping_id":1,"address":49754878,"line":[{"function_id":201,"line":199}]},{"id":312,"mapping_id":1,"address":4645286,"line":[{"function_id":271,"line":756}]},{"id":313,"mapping_id":1,"address":4242866,"line":[{"function_id":272,"line":88}]},{"id":314,"mapping_id":1,"address":4470125,"line":[{"function_id":273,"line":22},{"function_id":274,"line":48},{"function_id":172,"line":3374}]},{"id":315,"mapping_id":1,"address":46332277,"line":[{"function_id":81,"line":116}]},{"id":316,"mapping_id":1,"address":46273066,"line":[{"function_id":275,"line":362},{"function_id":276,"line":881}]},{"id":317,"mapping_id":1,"address":46271626,"line":[{"function_id":277,"line":772}]},{"id":318,"mapping_id":1,"address":46256908,"line":[{"function_id":82,"line":688}]},{"id":319,"mapping_id":1,"address":10684363,"line":[{"function_id":278,"line":366}]},{"id":320,"mapping_id":1,"address":10551373,"line":[{"function_id":279,"line":574}]},{"id":321,"mapping_id":1,"address":10590121,"line":[{"function_id":280,"line":414}]},{"id":322,"mapping_id":1,"address":4334590,"line":[{"function_id":57,"line":1324}]},{"id":323,"mapping_id":1,"address":6281988,"line":[{"function_id":281,"line":582}]},{"id":324,"mapping_id":1,"address":6284167,"line":[{"function_id":50,"line":667}]},{"id":325,"mapping_id":1,"address":4208626,"line":[{"function_id":282,"line":125}]},{"id":326,"mapping_id":1,"address":4550087,"line":[{"function_id":283,"line":542}]},{"id":327,"mapping_id":1,"address":4558960,"line":[{"function_id":284,"line":566},{"function_id":285,"line":978}]},{"id":328,"mapping_id":1,"address":4550835,"line":[{"function_id":155,"line":110}]},{"id":329,"mapping_id":1,"address":15573655,"line":[{"function_id":176,"line":215}]},{"id":330,"mapping_id":1,"address":4278829,"line":[{"function_id":93,"line":408}]},{"id":331,"mapping_id":1,"address":4334639,"line":[{"function_id":57,"line":1338}]},{"id":332,"mapping_id":1,"address":6287013,"line":[{"function_id":286,"line":175}]},{"id":333,"mapping_id":1,"address":6277308,"line":[{"function_id":5,"line":356}]},{"id":334,"mapping_id":1,"address":4652828,"line":[{"function_id":287,"line":244}]}],"function":[{"id":1,"name":4,"system_name":4,"filename":5},{"id":2,"name":6,"system_name":6,"filename":5},{"id":3,"name":7,"system_name":7,"filename":8},{"id":4,"name":9,"system_name":9,"filename":8},{"id":5,"name":10,"system_name":10,"filename":11},{"id":6,"name":12,"system_name":12,"filename":11},{"id":7,"name":13,"system_name":13,"filename":11},{"id":8,"name":14,"system_name":14,"filename":15},{"id":9,"name":16,"system_name":16,"filename":17},{"id":10,"name":18,"system_name":18,"filename":19},{"id":11,"name":20,"system_name":20,"filename":21},{"id":12,"name":22,"system_name":22,"filename":21},{"id":13,"name":23,"system_name":23,"filename":21},{"id":14,"name":24,"system_name":24,"filename":25},{"id":15,"name":26,"system_name":26,"filename":25},{"id":16,"name":27,"system_name":27,"filename":28},{"id":17,"name":29,"system_name":29,"filename":28},{"id":18,"name":30,"system_name":30,"filename":31},{"id":19,"name":32,"system_name":32,"filename":33},{"id":20,"name":34,"system_name":34,"filename":35},{"id":21,"name":36,"system_name":36,"filename":35},{"id":22,"name":37,"system_name":37,"filename":33},{"id":23,"name":38,"system_name":38,"filename":39},{"id":24,"name":40,"system_name":40,"filename":41},{"id":25,"name":42,"system_name":42,"filename":43},{"id":26,"name":44,"system_name":44,"filename":45},{"id":27,"name":46,"system_name":46,"filename":28},{"id":28,"name":47,"system_name":47,"filename":28},{"id":29,"name":48,"system_name":48,"filename":49},{"id":30,"name":50,"system_name":50,"filename":51},{"id":31,"name":52,"system_name":52,"filename":51},{"id":32,"name":53,"system_name":53,"filename":51},{"id":33,"name":54,"system_name":54,"filename":55},{"id":34,"name":56,"system_name":56,"filename":57},{"id":35,"name":58,"system_name":58,"filename":59},{"id":36,"name":60,"system_name":60,"filename":17},{"id":37,"name":61,"system_name":61,"filename":17},{"id":38,"name":62,"system_name":62,"filename":21},{"id":39,"name":63,"system_name":63,"filename":21},{"id":40,"name":64,"system_name":64,"filename":21},{"id":41,"name":65,"system_name":65,"filename":51},{"id":42,"name":66,"system_name":66,"filename":67},{"id":43,"name":68,"system_name":68,"filename":67},{"id":44,"name":69,"system_name":69,"filename":55},{"id":45,"name":70,"system_name":70,"filename":67},{"id":46,"name":71,"system_name":71,"filename":72},{"id":47,"name":73,"system_name":73,"filename":74},{"id":48,"name":75,"system_name":75,"filename":74},{"id":49,"name":76,"system_name":76,"filename":74},{"id":50,"name":77,"system_name":77,"filename":11},{"id":51,"name":78,"system_name":78,"filename":15},{"id":52,"name":79,"system_name":79,"filename":17},{"id":53,"name":80,"system_name":80,"filename":57},{"id":54,"name":81,"system_name":81,"filename":82},{"id":55,"name":83,"system_name":83,"filename":49},{"id":56,"name":84,"system_name":84,"filename":85},{"id":57,"name":86,"system_name":86,"filename":85},{"id":58,"name":87,"system_name":87,"filename":85},{"id":59,"name":88,"system_name":88,"filename":89},{"id":60,"name":90,"system_name":90,"filename":89},{"id":61,"name":91,"system_name":91,"filename":92},{"id":62,"name":93,"system_name":93,"filename":94},{"id":63,"name":95,"system_name":95,"filename":96},{"id":64,"name":97,"system_name":97,"filename":96},{"id":65,"name":98,"system_name":98,"filename":99},{"id":66,"name":100,"system_name":100,"filename":101},{"id":67,"name":102,"system_name":102,"filename":101},{"id":68,"name":103,"system_name":103,"filename":82},{"id":69,"name":104,"system_name":104,"filename":89},{"id":70,"name":105,"system_name":105,"filename":89},{"id":71,"name":106,"system_name":106,"filename":107},{"id":72,"name":108,"system_name":108,"filename":107},{"id":73,"name":109,"system_name":109,"filename":107},{"id":74,"name":110,"system_name":110,"filename":111},{"id":75,"name":112,"system_name":112,"filename":113},{"id":76,"name":114,"system_name":114,"filename":111},{"id":77,"name":115,"system_name":115,"filename":116},{"id":78,"name":117,"system_name":117,"filename":118},{"id":79,"name":119,"system_name":119,"filename":118},{"id":80,"name":120,"system_name":120,"filename":118},{"id":81,"name":121,"system_name":121,"filename":122},{"id":82,"name":123,"system_name":123,"filename":124},{"id":83,"name":125,"system_name":125,"filename":124},{"id":84,"name":126,"system_name":126,"filename":124},{"id":85,"name":127,"system_name":127,"filename":128},{"id":86,"name":129,"system_name":129,"filename":96},{"id":87,"name":130,"system_name":130,"filename":131},{"id":88,"name":132,"system_name":132,"filename":133},{"id":89,"name":134,"system_name":134,"filename":133},{"id":90,"name":135,"system_name":135,"filename":136},{"id":91,"name":137,"system_name":137,"filename":136},{"id":92,"name":138,"system_name":138,"filename":51},{"id":93,"name":139,"system_name":139,"filename":140},{"id":94,"name":141,"system_name":141,"filename":85},{"id":95,"name":142,"system_name":142,"filename":85},{"id":96,"name":143,"system_name":143,"filename":85},{"id":97,"name":144,"system_name":144,"filename":145},{"id":98,"name":146,"system_name":146,"filename":145},{"id":99,"name":147,"system_name":147,"filename":145},{"id":100,"name":148,"system_name":148,"filename":145},{"id":101,"name":149,"system_name":149,"filename":85},{"id":102,"name":150,"system_name":150,"filename":151},{"id":103,"name":152,"system_name":152,"filename":153},{"id":104,"name":154,"system_name":154,"filename":155},{"id":105,"name":156,"system_name":156,"filename":157},{"id":106,"name":158,"system_name":158,"filename":159},{"id":107,"name":160,"system_name":160,"filename":159},{"id":108,"name":161,"system_name":161,"filename":162},{"id":109,"name":163,"system_name":163,"filename":162},{"id":110,"name":164,"system_name":164,"filename":140},{"id":111,"name":165,"system_name":165,"filename":166},{"id":112,"name":167,"system_name":167,"filename":168},{"id":113,"name":169,"system_name":169,"filename":170},{"id":114,"name":171,"system_name":171,"filename":172},{"id":115,"name":173,"system_name":173,"filename":172},{"id":116,"name":174,"system_name":174,"filename":162},{"id":117,"name":175,"system_name":175,"filename":162},{"id":118,"name":176,"system_name":176,"filename":162},{"id":119,"name":177,"system_name":177,"filename":153},{"id":120,"name":178,"system_name":178,"filename":153},{"id":121,"name":179,"system_name":179,"filename":180},{"id":122,"name":181,"system_name":181,"filename":180},{"id":123,"name":182,"system_name":182,"filename":39},{"id":124,"name":183,"system_name":183,"filename":39},{"id":125,"name":184,"system_name":184,"filename":185},{"id":126,"name":186,"system_name":186,"filename":185},{"id":127,"name":187,"system_name":187,"filename":185},{"id":128,"name":188,"system_name":188,"filename":185},{"id":129,"name":189,"system_name":189,"filename":190},{"id":130,"name":191,"system_name":191,"filename":17},{"id":131,"name":192,"system_name":192,"filename":17},{"id":132,"name":193,"system_name":193,"filename":17},{"id":133,"name":194,"system_name":194,"filename":17},{"id":134,"name":195,"system_name":195,"filename":17},{"id":135,"name":196,"system_name":196,"filename":21},{"id":136,"name":197,"system_name":197,"filename":21},{"id":137,"name":198,"system_name":198,"filename":21},{"id":138,"name":199,"system_name":199,"filename":74},{"id":139,"name":200,"system_name":200,"filename":59},{"id":140,"name":201,"system_name":201,"filename":202},{"id":141,"name":203,"system_name":203,"filename":204},{"id":142,"name":205,"system_name":205,"filename":206},{"id":143,"name":207,"system_name":207,"filename":208},{"id":144,"name":209,"system_name":209,"filename":210},{"id":145,"name":211,"system_name":211,"filename":212},{"id":146,"name":213,"system_name":213,"filename":214},{"id":147,"name":215,"system_name":215,"filename":214},{"id":148,"name":216,"system_name":216,"filename":217},{"id":149,"name":218,"system_name":218,"filename":219},{"id":150,"name":220,"system_name":220,"filename":221},{"id":151,"name":222,"system_name":222,"filename":221},{"id":152,"name":223,"system_name":223,"filename":224},{"id":153,"name":225,"system_name":225,"filename":155},{"id":154,"name":226,"system_name":226,"filename":162},{"id":155,"name":227,"system_name":227,"filename":162},{"id":156,"name":228,"system_name":228,"filename":229},{"id":157,"name":230,"system_name":230,"filename":231},{"id":158,"name":232,"system_name":232,"filename":231},{"id":159,"name":233,"system_name":233,"filename":234},{"id":160,"name":235,"system_name":235,"filename":118},{"id":161,"name":236,"system_name":236,"filename":57},{"id":162,"name":237,"system_name":237,"filename":238},{"id":163,"name":239,"system_name":239,"filename":240},{"id":164,"name":241,"system_name":241,"filename":51},{"id":165,"name":242,"system_name":242,"filename":51},{"id":166,"name":243,"system_name":243,"filename":244},{"id":167,"name":245,"system_name":245,"filename":246},{"id":168,"name":247,"system_name":247,"filename":248},{"id":169,"name":249,"system_name":249,"filename":248},{"id":170,"name":250,"system_name":250,"filename":51},{"id":171,"name":251,"system_name":251,"filename":51},{"id":172,"name":252,"system_name":252,"filename":51},{"id":173,"name":253,"system_name":253,"filename":51},{"id":174,"name":254,"system_name":254,"filename":55},{"id":175,"name":255,"system_name":255,"filename":140},{"id":176,"name":256,"system_name":256,"filename":17},{"id":177,"name":257,"system_name":257,"filename":51},{"id":178,"name":258,"system_name":258,"filename":51},{"id":179,"name":259,"system_name":259,"filename":51},{"id":180,"name":260,"system_name":260,"filename":145},{"id":181,"name":261,"system_name":261,"filename":262},{"id":182,"name":263,"system_name":263,"filename":264},{"id":183,"name":265,"system_name":265,"filename":266},{"id":184,"name":267,"system_name":267,"filename":268},{"id":185,"name":269,"system_name":269,"filename":268},{"id":186,"name":270,"system_name":270,"filename":271},{"id":187,"name":272,"system_name":272,"filename":271},{"id":188,"name":273,"system_name":273,"filename":271},{"id":189,"name":274,"system_name":274,"filename":155},{"id":190,"name":275,"system_name":275,"filename":136},{"id":191,"name":276,"system_name":276,"filename":89},{"id":192,"name":277,"system_name":277,"filename":51},{"id":193,"name":278,"system_name":278,"filename":89},{"id":194,"name":279,"system_name":279,"filename":280},{"id":195,"name":281,"system_name":281,"filename":282},{"id":196,"name":283,"system_name":283,"filename":282},{"id":197,"name":284,"system_name":284,"filename":285},{"id":198,"name":286,"system_name":286,"filename":221},{"id":199,"name":287,"system_name":287,"filename":221},{"id":200,"name":288,"system_name":288,"filename":96},{"id":201,"name":289,"system_name":289,"filename":290},{"id":202,"name":291,"system_name":291,"filename":290},{"id":203,"name":292,"system_name":292,"filename":290},{"id":204,"name":293,"system_name":293,"filename":290},{"id":205,"name":294,"system_name":294,"filename":290},{"id":206,"name":295,"system_name":295,"filename":172},{"id":207,"name":296,"system_name":296,"filename":297},{"id":208,"name":298,"system_name":298,"filename":172},{"id":209,"name":299,"system_name":299,"filename":300},{"id":210,"name":301,"system_name":301,"filename":302},{"id":211,"name":303,"system_name":303,"filename":89},{"id":212,"name":304,"system_name":304,"filename":89},{"id":213,"name":305,"system_name":305,"filename":162},{"id":214,"name":306,"system_name":306,"filename":162},{"id":215,"name":307,"system_name":307,"filename":308},{"id":216,"name":309,"system_name":309,"filename":310},{"id":217,"name":311,"system_name":311,"filename":310},{"id":218,"name":312,"system_name":312,"filename":313},{"id":219,"name":314,"system_name":314,"filename":313},{"id":220,"name":315,"system_name":315,"filename":313},{"id":221,"name":316,"system_name":316,"filename":317},{"id":222,"name":318,"system_name":318,"filename":319},{"id":223,"name":320,"system_name":320,"filename":321},{"id":224,"name":322,"system_name":322,"filename":321},{"id":225,"name":323,"system_name":323,"filename":321},{"id":226,"name":324,"system_name":324,"filename":325},{"id":227,"name":326,"system_name":326,"filename":99},{"id":228,"name":327,"system_name":327,"filename":99},{"id":229,"name":328,"system_name":328,"filename":99},{"id":230,"name":329,"system_name":329,"filename":330},{"id":231,"name":331,"system_name":331,"filename":302},{"id":232,"name":332,"system_name":332,"filename":248},{"id":233,"name":333,"system_name":333,"filename":74},{"id":234,"name":334,"system_name":334,"filename":145},{"id":235,"name":335,"system_name":335,"filename":336},{"id":236,"name":337,"system_name":337,"filename":336},{"id":237,"name":338,"system_name":338,"filename":339},{"id":238,"name":340,"system_name":340,"filename":5},{"id":239,"name":341,"system_name":341,"filename":342},{"id":240,"name":343,"system_name":343,"filename":344},{"id":241,"name":345,"system_name":345,"filename":344},{"id":242,"name":346,"system_name":346,"filename":347},{"id":243,"name":348,"system_name":348,"filename":347},{"id":244,"name":349,"system_name":349,"filename":39},{"id":245,"name":350,"system_name":350,"filename":210},{"id":246,"name":351,"system_name":351,"filename":212},{"id":247,"name":352,"system_name":352,"filename":214},{"id":248,"name":353,"system_name":353,"filename":217},{"id":249,"name":354,"system_name":354,"filename":219},{"id":250,"name":355,"system_name":355,"filename":356},{"id":251,"name":357,"system_name":357,"filename":358},{"id":252,"name":359,"system_name":359,"filename":356},{"id":253,"name":360,"system_name":360,"filename":361},{"id":254,"name":362,"system_name":362,"filename":361},{"id":255,"name":363,"system_name":363,"filename":361},{"id":256,"name":364,"system_name":364,"filename":365},{"id":257,"name":366,"system_name":366,"filename":361},{"id":258,"name":367,"system_name":367,"filename":361},{"id":259,"name":368,"system_name":368,"filename":369},{"id":260,"name":370,"system_name":370,"filename":371},{"id":261,"name":372,"system_name":372,"filename":371},{"id":262,"name":373,"system_name":373,"filename":371},{"id":263,"name":374,"system_name":374,"filename":375},{"id":264,"name":376,"system_name":376,"filename":377},{"id":265,"name":378,"system_name":378,"filename":379},{"id":266,"name":380,"system_name":380,"filename":381},{"id":267,"name":382,"system_name":382,"filename":383},{"id":268,"name":384,"system_name":384,"filename":385},{"id":269,"name":386,"system_name":386,"filename":387},{"id":270,"name":388,"system_name":388,"filename":389},{"id":271,"name":390,"system_name":390,"filename":55},{"id":272,"name":391,"system_name":391,"filename":240},{"id":273,"name":392,"system_name":392,"filename":393},{"id":274,"name":394,"system_name":394,"filename":240},{"id":275,"name":395,"system_name":395,"filename":396},{"id":276,"name":397,"system_name":397,"filename":398},{"id":277,"name":399,"system_name":399,"filename":398},{"id":278,"name":400,"system_name":400,"filename":401},{"id":279,"name":402,"system_name":402,"filename":403},{"id":280,"name":404,"system_name":404,"filename":405},{"id":281,"name":406,"system_name":406,"filename":11},{"id":282,"name":407,"system_name":407,"filename":408},{"id":283,"name":409,"system_name":409,"filename":92},{"id":284,"name":410,"system_name":410,"filename":92},{"id":285,"name":411,"system_name":411,"filename":162},{"id":286,"name":412,"system_name":412,"filename":413},{"id":287,"name":414,"system_name":414,"filename":57}],"string_table":["","cpu","nanoseconds","/usr/bin/phlare","compress/flate.(*huffmanEncoder).bitCounts","compress/flate/huffman_code.go","compress/flate.(*huffmanEncoder).generate","compress/flate.(*huffmanBitWriter).indexTokens","compress/flate/huffman_bit_writer.go","compress/flate.(*huffmanBitWriter).writeBlockDynamic","compress/flate.(*compressor).encSpeed","compress/flate/deflate.go","compress/flate.(*compressor).close","compress/flate.(*Writer).Close","compress/gzip.(*Writer).Close","compress/gzip/gzip.go","runtime/pprof.(*profileBuilder).build","runtime/pprof/proto.go","runtime/pprof.writeHeapProto","runtime/pprof/protomem.go","runtime/pprof.writeHeapInternal","runtime/pprof/pprof.go","runtime/pprof.writeAlloc","runtime/pprof.(*Profile).WriteTo","net/http/pprof.handler.ServeHTTP","net/http/pprof/pprof.go","net/http/pprof.Index","net/http.HandlerFunc.ServeHTTP","net/http/server.go","net/http.(*ServeMux).ServeHTTP","github.com/gorilla/mux.(*Router).ServeHTTP","github.com/gorilla/mux@v1.8.0/mux.go","github.com/weaveworks/common/middleware.Instrument.Wrap.func1.2","github.com/weaveworks/common@v0.0.0-20221201103051-7c2720a9024d/middleware/instrument.go","github.com/felixge/httpsnoop.(*Metrics).CaptureMetrics","github.com/felixge/httpsnoop@v1.0.3/capture_metrics.go","github.com/felixge/httpsnoop.CaptureMetricsFn","github.com/weaveworks/common/middleware.Instrument.Wrap.func1","github.com/grafana/phlare/pkg/util.Log.Wrap.func1","github.com/grafana/phlare/pkg/util/http.go","github.com/opentracing-contrib/go-stdlib/nethttp.MiddlewareFunc.func5","github.com/opentracing-contrib/go-stdlib@v1.0.0/nethttp/server.go","golang.org/x/net/http2/h2c.h2cHandler.ServeHTTP","golang.org/x/net@v0.5.0/http2/h2c/h2c.go","github.com/grafana/phlare/pkg/util.glob..func2.1","github.com/grafana/phlare/pkg/util/recovery.go","net/http.serverHandler.ServeHTTP","net/http.(*conn).serve","runtime.netpoll","runtime/netpoll_epoll.go","runtime.findRunnable","runtime/proc.go","runtime.schedule","runtime.park_m","runtime.mcall","runtime/asm_amd64.s","runtime.epollwait","runtime/sys_linux_amd64.s","runtime.growslice","runtime/slice.go","runtime/pprof.(*profileBuilder).emitLocation","runtime/pprof.(*profileBuilder).appendLocsForStack","runtime/pprof.printCountProfile","runtime/pprof.writeRuntimeProfile","runtime/pprof.writeGoroutine","runtime.goexit0","runtime.gentraceback","runtime/traceback.go","runtime.callers.func1","runtime.systemstack","runtime.callers","runtime.mProf_Malloc","runtime/mprof.go","runtime.profilealloc","runtime/malloc.go","runtime.mallocgc","runtime.newobject","compress/flate.NewWriter","compress/gzip.(*Writer).Write","runtime/pprof.(*profileBuilder).flush","runtime.write1","runtime.write","runtime/time_nofake.go","runtime.netpollBreak","runtime.greyobject","runtime/mgcmark.go","runtime.scanobject","runtime.gcDrain","runtime.gcBgMarkWorker.func2","runtime/mgc.go","runtime.gcBgMarkWorker","runtime.slicebytetostring","runtime/string.go","github.com/grafana/phlare/api/gen/proto/go/google/v1.(*Profile).UnmarshalVT","github.com/grafana/phlare/api/gen/proto/go/google/v1/profile_vtproto.pb.go","github.com/grafana/phlare/pkg/pprof.fromUncompressedReader","github.com/grafana/phlare/pkg/pprof/pprof.go","github.com/grafana/phlare/pkg/pprof.RawFromBytes","github.com/grafana/phlare/pkg/distributor.(*Distributor).Push","github.com/grafana/phlare/pkg/distributor/distributor.go","github.com/grafana/phlare/pkg/agent.(*Target).scrape","github.com/grafana/phlare/pkg/agent/target.go","github.com/grafana/phlare/pkg/agent.(*Target).start.func1","runtime.nanotime","runtime.gcMarkTermination","runtime.gcMarkDone","runtime.(*itabTableType).find","runtime/iface.go","runtime.getitab","runtime.convI2I","google.golang.org/grpc.newClientStream","google.golang.org/grpc@v1.51.0/stream.go","github.com/grpc-ecosystem/go-grpc-middleware.ChainStreamClient.func1","github.com/grpc-ecosystem/go-grpc-middleware@v1.3.0/chain.go","google.golang.org/grpc.(*ClientConn).NewStream","github.com/grafana/phlare/pkg/scheduler/schedulerpb.(*schedulerForFrontendClient).FrontendLoop","github.com/grafana/phlare/pkg/scheduler/schedulerpb/scheduler_vtproto.pb.go","github.com/grafana/phlare/pkg/frontend.(*frontendSchedulerWorker).runOne.func1","github.com/grafana/phlare/pkg/frontend/frontend_scheduler_worker.go","github.com/grafana/phlare/pkg/frontend.(*frontendSchedulerWorker).runOne","github.com/grafana/phlare/pkg/frontend.(*frontendSchedulerWorker).start.func1","github.com/klauspost/compress/flate.(*fastEncL5).Encode","github.com/klauspost/compress@v1.15.13/flate/level5.go","github.com/klauspost/compress/flate.(*compressor).storeFast","github.com/klauspost/compress@v1.15.13/flate/deflate.go","github.com/klauspost/compress/flate.(*compressor).close","github.com/klauspost/compress/flate.(*Writer).Close","github.com/klauspost/compress/gzip.(*Writer).Close","github.com/klauspost/compress@v1.15.13/gzip/gzip.go","github.com/grafana/phlare/pkg/pprof.(*Profile).WriteTo","runtime.memclrNoHeapPointers","runtime/memclr_amd64.s","runtime/pprof.(*protobuf).string","runtime/pprof/protobuf.go","runtime/pprof.(*protobuf).strings","runtime.(*gcWork).putFast","runtime/mgcwork.go","runtime.(*gcWork).tryGetFast","runtime.checkTimersNoP","runtime.findObject","runtime/mbitmap.go","runtime.scanblock","runtime.markrootBlock","runtime.markroot","runtime.adjustpointers","runtime/stack.go","runtime.adjustframe","runtime.copystack","runtime.newstack","runtime.gcAssistAlloc","github.com/uber/jaeger-client-go.(*TextMapPropagator).Extract","github.com/uber/jaeger-client-go@v2.30.0+incompatible/propagation.go","github.com/uber/jaeger-client-go.(*Tracer).Extract","github.com/uber/jaeger-client-go@v2.30.0+incompatible/tracer.go","golang.org/x/net/http2.(*serverConn).runHandler","golang.org/x/net@v0.5.0/http2/server.go","github.com/uber/jaeger-client-go.(*udpSender).Flush","github.com/uber/jaeger-client-go@v2.30.0+incompatible/transport_udp.go","github.com/uber/jaeger-client-go.(*remoteReporter).processQueue.func1","github.com/uber/jaeger-client-go@v2.30.0+incompatible/reporter.go","github.com/uber/jaeger-client-go.(*remoteReporter).processQueue","runtime.funcdata","runtime/symtab.go","runtime.FuncForPC","runtime.markBits.isMarked","github.com/grafana/phlare/pkg/scheduler/queue.(*queues).len","github.com/grafana/phlare/pkg/scheduler/queue/user_queues.go","github.com/grafana/phlare/pkg/scheduler/queue.(*RequestQueue).GetNextRequestForQuerier","github.com/grafana/phlare/pkg/scheduler/queue/queue.go","github.com/grafana/phlare/pkg/scheduler.(*Scheduler).QuerierLoop","github.com/grafana/phlare/pkg/scheduler/scheduler.go","github.com/bufbuild/connect-go.NewBidiStreamHandler[...].func1","github.com/bufbuild/connect-go@v1.4.1/handler.go","github.com/bufbuild/connect-go.(*Handler).ServeHTTP","runtime.step","runtime.pcvalue","runtime.funcspdelta","github.com/uber/jaeger-client-go.(*Tracer).startSpanWithOptions","github.com/uber/jaeger-client-go.(*Tracer).StartSpan","github.com/opentracing-contrib/go-stdlib/nethttp.(*Tracer).start","github.com/opentracing-contrib/go-stdlib@v1.0.0/nethttp/client.go","github.com/opentracing-contrib/go-stdlib/nethttp.(*Transport).RoundTrip","github.com/grafana/phlare/pkg/util.WrapWithInstrumentedHTTPTransport.func1","github.com/grafana/phlare/pkg/util.RoundTripperFunc.RoundTrip","net/http.send","net/http/client.go","net/http.(*Client).send","net/http.(*Client).do","net/http.(*Client).Do","github.com/bufbuild/connect-go.(*duplexHTTPCall).makeRequest","github.com/bufbuild/connect-go@v1.4.1/duplex_http_call.go","runtime/pprof.(*profileBuilder).addMappingEntry","runtime/pprof.(*profileBuilder).addMapping","runtime/pprof.parseProcSelfMaps","runtime/pprof.(*profileBuilder).readMapping","runtime/pprof.newProfileBuilder","runtime/pprof.printCountCycleProfile","runtime/pprof.writeProfileInternal","runtime/pprof.writeMutex","runtime.memclrNoHeapPointersChunked","runtime.makeslice","github.com/klauspost/compress/flate.(*fastGen).addBlock","github.com/klauspost/compress@v1.15.13/flate/fast_encoder.go","runtime/internal/syscall.Syscall6","runtime/internal/syscall/asm_linux_amd64.s","syscall.RawSyscall6","runtime/internal/syscall/syscall_linux.go","syscall.Syscall","syscall/syscall_linux.go","syscall.read","syscall/zsyscall_linux_amd64.go","syscall.Read","syscall/syscall_unix.go","internal/poll.ignoringEINTRIO","internal/poll/fd_unix.go","internal/poll.(*FD).Read","net.(*netFD).Read","net/fd_posix.go","net.(*conn).Read","net/net.go","io.ReadAtLeast","io/io.go","io.ReadFull","golang.org/x/net/http2.(*Framer).ReadFrame","golang.org/x/net@v0.5.0/http2/frame.go","golang.org/x/net/http2.(*serverConn).readFrames","runtime.pcdatavalue1","runtime.(*Frames).Next","runtime.Caller","runtime/extern.go","github.com/go-kit/log.Caller.func1","github.com/go-kit/log@v0.2.1/value.go","github.com/go-kit/log.bindValues","github.com/go-kit/log.(*context).Log","github.com/go-kit/log@v0.2.1/log.go","github.com/grafana/phlare/pkg/frontend.(*frontendSchedulerWorker).schedulerLoop","runtime.futex","runtime.futexsleep","runtime/os_linux.go","runtime.notesleep","runtime/lock_futex.go","runtime.mPark","runtime.stopm","runtime.alignUp","runtime/stubs.go","runtime.(*consistentHeapStats).acquire","runtime/mstats.go","runtime.(*mcache).releaseAll","runtime/mcache.go","runtime.(*mcache).prepareForSweep","runtime.acquirep","runtime.gcstopm","runtime.goschedImpl","runtime.gopreempt_m","runtime.morestack","runtime.heapBitsSetType","runtime/pprof.allFrames","runtime.startm","runtime.wakep","runtime.resetspinning","runtime.stackalloc","type..hash.golang.org/x/net/http2/hpack.pairNameValue","\u003cautogenerated\u003e","runtime.mapaccess1","runtime/map.go","golang.org/x/net/http2/hpack.(*headerFieldTable).search","golang.org/x/net@v0.5.0/http2/hpack/tables.go","golang.org/x/net/http2/hpack.(*Encoder).searchTable","golang.org/x/net@v0.5.0/http2/hpack/encode.go","golang.org/x/net/http2/hpack.(*Encoder).WriteField","golang.org/x/net/http2.encKV","golang.org/x/net@v0.5.0/http2/write.go","golang.org/x/net/http2.encodeHeaders","golang.org/x/net/http2.(*writeResHeaders).writeFrame","golang.org/x/net/http2.(*serverConn).writeFrameAsync","runtime.(*gcWork).dispose","runtime.gcMarkDone.func1.1","runtime.forEachP","runtime.gcMarkDone.func1","github.com/klauspost/compress/flate.(*decompressor).huffmanBytesReader","github.com/klauspost/compress@v1.15.13/flate/inflate_gen.go","github.com/klauspost/compress/flate.(*decompressor).nextBlock","github.com/klauspost/compress@v1.15.13/flate/inflate.go","github.com/klauspost/compress/flate.(*decompressor).WriteTo","github.com/klauspost/compress/gzip.(*Reader).WriteTo","github.com/klauspost/compress@v1.15.13/gzip/gunzip.go","io.copyBuffer","io.Copy","github.com/grafana/phlare/pkg/pprof.FromBytes","github.com/grafana/phlare/pkg/ingester.(*Ingester).Push.func1","github.com/grafana/phlare/pkg/ingester/ingester.go","github.com/grafana/phlare/pkg/ingester.forInstanceUnary[...].func1","github.com/grafana/phlare/pkg/ingester.(*Ingester).forInstance","github.com/grafana/phlare/pkg/ingester.forInstanceUnary[...]","github.com/grafana/phlare/pkg/ingester.(*Ingester).Push","github.com/bufbuild/connect-go.NewUnaryHandler[...].func1","github.com/grafana/phlare/pkg/tenant.(*authInterceptor).WrapUnary.func1","github.com/grafana/phlare/pkg/tenant/interceptor.go","github.com/bufbuild/connect-go.NewUnaryHandler[...].func2","runtime.(*sweepLocked).sweep","runtime/mgcsweep.go","runtime.(*mcentral).uncacheSpan","runtime/mcentral.go","runtime.gcMarkTermination.func4.1","runtime.gcMarkTermination.func4","runtime.(*moduledata).textOff","runtime.findfunc","compress/gzip.(*Reader).Reset","compress/gzip/gunzip.go","github.com/bufbuild/connect-go.(*compressionPool).getDecompressor","github.com/bufbuild/connect-go@v1.4.1/compression.go","github.com/bufbuild/connect-go.(*compressionPool).Decompress","github.com/bufbuild/connect-go.(*connectUnaryUnmarshaler).UnmarshalFunc","github.com/bufbuild/connect-go@v1.4.1/protocol_connect.go","github.com/bufbuild/connect-go.(*connectUnaryUnmarshaler).Unmarshal","github.com/bufbuild/connect-go.(*connectUnaryClientConn).Receive","github.com/bufbuild/connect-go.(*errorTranslatingClientConn).Receive","github.com/bufbuild/connect-go@v1.4.1/protocol.go","github.com/bufbuild/connect-go.receiveUnaryResponse[...]","github.com/bufbuild/connect-go@v1.4.1/connect.go","github.com/bufbuild/connect-go.NewClient[...].func1","github.com/bufbuild/connect-go@v1.4.1/client.go","github.com/bufbuild/connect-go.NewClient[...].func2","github.com/bufbuild/connect-go.(*Client[...]).CallUnary","github.com/grafana/phlare/api/gen/proto/go/ingester/v1/ingesterv1connect.(*ingesterServiceClient).Push","github.com/grafana/phlare/api/gen/proto/go/ingester/v1/ingesterv1connect/ingester.connect.go","github.com/grafana/phlare/pkg/distributor.(*Distributor).sendProfilesErr","github.com/grafana/phlare/pkg/distributor.(*Distributor).sendProfiles","github.com/grafana/phlare/pkg/distributor.(*Distributor).Push.func1","runtime.(*spanSet).pop","runtime/mspanset.go","runtime.(*mcentral).cacheSpan","runtime.(*mcache).refill","runtime.(*mcache).nextFree","runtime.getStackMap","sort.insertionSort","sort/zsortinterface.go","sort.pdqsort","sort.Sort","sort/sort.go","compress/flate.(*byFreq).sort","github.com/weaveworks/common/logging.gokit.Debugf","github.com/weaveworks/common@v0.0.0-20221201103051-7c2720a9024d/logging/gokit.go","strings.(*byteReplacer).Replace","strings/replace.go","strings.(*Replacer).Replace","net/http.Header.writeSubset","net/http/header.go","net/http.Header.WriteSubset","github.com/grafana/phlare/pkg/util.dumpRequest","syscall.write","syscall.Write","internal/poll.(*FD).Write","net.(*netFD).Write","net.(*conn).Write","net/http.persistConnWriter.Write","net/http/transport.go","bufio.(*Writer).Flush","bufio/bufio.go","net/http.(*persistConn).writeLoop","fmt.(*buffer).writeString","fmt/print.go","fmt.(*pp).doPrintf","fmt.Sprintf","net/url.(*Error).Error","net/url/url.go","fmt.(*pp).handleMethods","fmt.(*pp).printArg","google.golang.org/grpc/internal/channelz.Infof","google.golang.org/grpc@v1.51.0/internal/channelz/logging.go","google.golang.org/grpc.(*ClientConn).parseTargetAndFindResolver","google.golang.org/grpc@v1.51.0/clientconn.go","google.golang.org/grpc.DialContext","google.golang.org/grpc.Dial","github.com/grafana/phlare/pkg/ingester/clientpool.PoolFactoryFn.func1","github.com/grafana/phlare/pkg/ingester/clientpool/ingester_client_pool.go","github.com/grafana/dskit/ring/client.(*Pool).GetClientFor","github.com/grafana/dskit@v0.0.0-20230120165636-649501dde2ca/ring/client/pool.go","github.com/grafana/phlare/pkg/querier.forGivenIngesters[...].func1","github.com/grafana/phlare/pkg/querier/ingester_querier.go","github.com/grafana/dskit/ring.ReplicationSet.Do.func1","github.com/grafana/dskit@v0.0.0-20230120165636-649501dde2ca/ring/replication_set.go","runtime.pageIndexOf","runtime/mheap.go","runtime.mapaccess2_faststr","runtime/map_faststr.go","github.com/grafana/phlare/pkg/phlaredb.(*deduplicatingSlice[...]).ingest","github.com/grafana/phlare/pkg/phlaredb/deduplicating_slice.go","github.com/grafana/phlare/pkg/phlaredb.(*Head).Ingest","github.com/grafana/phlare/pkg/phlaredb/head.go","runtime.procyield","runtime.lock2","runtime.lockWithRank","runtime/lockrank_off.go","runtime.lock","github.com/klauspost/compress/flate.lengthCode","github.com/klauspost/compress@v1.15.13/flate/token.go","github.com/klauspost/compress/flate.(*huffmanBitWriter).writeTokens","github.com/klauspost/compress@v1.15.13/flate/huffman_bit_writer.go","github.com/klauspost/compress/flate.(*huffmanBitWriter).writeBlockDynamic","google.golang.org/grpc/internal/transport.(*bufWriter).Flush","google.golang.org/grpc@v1.51.0/internal/transport/http_util.go","google.golang.org/grpc/internal/transport.(*loopyWriter).run","google.golang.org/grpc@v1.51.0/internal/transport/controlbuf.go","google.golang.org/grpc/internal/transport.newHTTP2Client.func3","google.golang.org/grpc@v1.51.0/internal/transport/http2_client.go","compress/flate.(*compressor).init","indexbytebody","internal/bytealg/indexbyte_amd64.s","runtime.findnull","runtime.gostringnocopy","runtime.funcname","compress/flate.(*deflateFast).encode","compress/flate/deflatefast.go","runtime.nanotime1"],"time_nanos":1677835621684000000,"duration_nanos":3600000000000,"period_type":{"type":1,"unit":2},"period":1000000000,"default_sample_type":1} \ No newline at end of file From 5fdf1f48623571f0ecd50f08f294a728a4562e2e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 7 Mar 2023 09:33:39 +0000 Subject: [PATCH 003/288] Update dependency jest-junit to v15 (#64280) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index e6b295eda44..3ffa1b4d43b 100644 --- a/package.json +++ b/package.json @@ -210,7 +210,7 @@ "jest-date-mock": "1.0.8", "jest-environment-jsdom": "29.3.1", "jest-fail-on-console": "3.0.2", - "jest-junit": "14.0.1", + "jest-junit": "15.0.0", "jest-matcher-utils": "29.3.1", "lerna": "5.5.4", "lint-staged": "13.1.0", diff --git a/yarn.lock b/yarn.lock index dee304cd9a3..065843e9e2a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -22194,7 +22194,7 @@ __metadata: jest-date-mock: 1.0.8 jest-environment-jsdom: 29.3.1 jest-fail-on-console: 3.0.2 - jest-junit: 14.0.1 + jest-junit: 15.0.0 jest-matcher-utils: 29.3.1 jquery: 3.6.1 js-yaml: ^4.1.0 @@ -25213,15 +25213,15 @@ __metadata: languageName: node linkType: hard -"jest-junit@npm:14.0.1": - version: 14.0.1 - resolution: "jest-junit@npm:14.0.1" +"jest-junit@npm:15.0.0": + version: 15.0.0 + resolution: "jest-junit@npm:15.0.0" dependencies: mkdirp: ^1.0.4 strip-ansi: ^6.0.1 uuid: ^8.3.2 xml: ^1.0.1 - checksum: 2a9ccfecbe4c0df1be24e64b3e12a260356db999b3d821578c325bd34367d2f54b27e9560a8d5abe6c19412400268bf55dd41473565903cb8f616d998f7eb9ac + checksum: e8fe4d2f2ab843383ac41820a6fe495739d154ec435cd44ba590b44ec7fd62095676f3eef13f98392f81d4a3727ea58b4f4fad231fe367ac31243952b9ad716f languageName: node linkType: hard From fa0f640d6a0a3406ea9c0e99efd39b9cb13e0cdb Mon Sep 17 00:00:00 2001 From: Pepe Fagoaga Date: Tue, 7 Mar 2023 10:33:59 +0100 Subject: [PATCH 004/288] SQL Engine: Handle one session to connect the DB (#63246) fix(postgresql): Handle one session to the DB --- pkg/tsdb/sqleng/sql_engine.go | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/pkg/tsdb/sqleng/sql_engine.go b/pkg/tsdb/sqleng/sql_engine.go index 70c679e225e..5f7c8167a8c 100644 --- a/pkg/tsdb/sqleng/sql_engine.go +++ b/pkg/tsdb/sqleng/sql_engine.go @@ -97,7 +97,9 @@ type DataSourceHandler struct { log log.Logger dsInfo DataSourceInfo rowLimit int64 + session *xorm.Session } + type QueryJson struct { RawSql string `json:"rawSql"` Fill bool `json:"fill"` @@ -145,6 +147,7 @@ func NewQueryDataHandler(config DataPluginConfiguration, queryResultTransformer queryDataHandler.metricColumnTypes = config.MetricColumnTypes } + // Create the xorm engine engine, err := NewXormEngine(config.DriverName, config.ConnectionString) if err != nil { return nil, err @@ -155,6 +158,11 @@ func NewQueryDataHandler(config DataPluginConfiguration, queryResultTransformer engine.SetConnMaxLifetime(time.Duration(config.DSInfo.JsonData.ConnMaxLifetime) * time.Second) queryDataHandler.engine = engine + + // Create the xorm session + session := engine.NewSession() + queryDataHandler.session = session + return &queryDataHandler, nil } @@ -265,9 +273,7 @@ func (e *DataSourceHandler) executeQuery(query backend.DataQuery, wg *sync.WaitG return } - session := e.engine.NewSession() - defer session.Close() - db := session.DB() + db := e.session.DB() rows, err := db.QueryContext(queryContext, interpolatedQuery) if err != nil { From 7b016687292b33ea6f9ce5ec29d134760e270b75 Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Tue, 7 Mar 2023 11:08:50 +0100 Subject: [PATCH 005/288] Logs sample: Fix scrolling for unwrapped log lines (#64163) Fix scrolling for logs sample --- .../app/features/explore/LogsSamplePanel.tsx | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/public/app/features/explore/LogsSamplePanel.tsx b/public/app/features/explore/LogsSamplePanel.tsx index 78f6e53cf38..61772745311 100644 --- a/public/app/features/explore/LogsSamplePanel.tsx +++ b/public/app/features/explore/LogsSamplePanel.tsx @@ -95,16 +95,18 @@ export function LogsSamplePanel(props: Props) { LogsSamplePanelContent = ( <> - +
+ +
); } @@ -122,4 +124,7 @@ const getStyles = (theme: GrafanaTheme2) => ({ top: ${theme.spacing(1)}; right: ${theme.spacing(1)}; ; `, + logContainer: css` + overflow-x: scroll; + `, }); From f579a63f8e95b26e7595cf6bf31e078b3053e67d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 7 Mar 2023 11:17:38 +0100 Subject: [PATCH 006/288] Update dependency msw to v1 (#64281) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update dependency msw to v1 * Fix breaking change --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Zoltán Bedi --- package.json | 2 +- .../alerting/unified/mocks/alertmanagerApi.ts | 6 +++--- .../alerting/unified/mocks/grafanaApi.ts | 4 ++-- .../alerting/unified/mocks/rulerApi.ts | 10 +++------ yarn.lock | 21 ++++++++++++------- 5 files changed, 23 insertions(+), 20 deletions(-) diff --git a/package.json b/package.json index 3ffa1b4d43b..ee2fe272ead 100644 --- a/package.json +++ b/package.json @@ -215,7 +215,7 @@ "lerna": "5.5.4", "lint-staged": "13.1.0", "mini-css-extract-plugin": "2.7.2", - "msw": "0.49.2", + "msw": "1.1.0", "mutationobserver-shim": "0.3.7", "ngtemplate-loader": "2.1.0", "node-notifier": "10.0.1", diff --git a/public/app/features/alerting/unified/mocks/alertmanagerApi.ts b/public/app/features/alerting/unified/mocks/alertmanagerApi.ts index 48188bedb4c..56555a1e237 100644 --- a/public/app/features/alerting/unified/mocks/alertmanagerApi.ts +++ b/public/app/features/alerting/unified/mocks/alertmanagerApi.ts @@ -1,13 +1,13 @@ import { rest } from 'msw'; -import { SetupServerApi } from 'msw/node'; +import { SetupServer } from 'msw/node'; import { ExternalAlertmanagersResponse } from '../../../../plugins/datasource/alertmanager/types'; import { AlertmanagersChoiceResponse } from '../api/alertmanagerApi'; -export function mockAlertmanagerChoiceResponse(server: SetupServerApi, respose: AlertmanagersChoiceResponse) { +export function mockAlertmanagerChoiceResponse(server: SetupServer, respose: AlertmanagersChoiceResponse) { server.use(rest.get('/api/v1/ngalert', (req, res, ctx) => res(ctx.status(200), ctx.json(respose)))); } -export function mockAlertmanagersResponse(server: SetupServerApi, response: ExternalAlertmanagersResponse) { +export function mockAlertmanagersResponse(server: SetupServer, response: ExternalAlertmanagersResponse) { server.use(rest.get('/api/v1/ngalert/alertmanagers', (req, res, ctx) => res(ctx.status(200), ctx.json(response)))); } diff --git a/public/app/features/alerting/unified/mocks/grafanaApi.ts b/public/app/features/alerting/unified/mocks/grafanaApi.ts index 04c45c7bb65..1062de6749f 100644 --- a/public/app/features/alerting/unified/mocks/grafanaApi.ts +++ b/public/app/features/alerting/unified/mocks/grafanaApi.ts @@ -1,8 +1,8 @@ import { rest } from 'msw'; -import { SetupServerApi } from 'msw/node'; +import { SetupServer } from 'msw/node'; import { DashboardSearchItem } from '../../../search/types'; -export function mockSearchApiResponse(server: SetupServerApi, searchResult: DashboardSearchItem[]) { +export function mockSearchApiResponse(server: SetupServer, searchResult: DashboardSearchItem[]) { server.use(rest.get('/api/search', (req, res, ctx) => res(ctx.json(searchResult)))); } diff --git a/public/app/features/alerting/unified/mocks/rulerApi.ts b/public/app/features/alerting/unified/mocks/rulerApi.ts index 18cd13b6e4c..71339a4cf72 100644 --- a/public/app/features/alerting/unified/mocks/rulerApi.ts +++ b/public/app/features/alerting/unified/mocks/rulerApi.ts @@ -1,13 +1,9 @@ import { rest } from 'msw'; -import { SetupServerApi } from 'msw/node'; +import { SetupServer } from 'msw/node'; import { RulerRuleGroupDTO, RulerRulesConfigDTO } from '../../../../types/unified-alerting-dto'; -export function mockRulerRulesApiResponse( - server: SetupServerApi, - rulesSourceName: string, - response: RulerRulesConfigDTO -) { +export function mockRulerRulesApiResponse(server: SetupServer, rulesSourceName: string, response: RulerRulesConfigDTO) { server.use( rest.get(`/api/ruler/${rulesSourceName}/api/v1/rules`, (req, res, ctx) => res(ctx.json(response)) @@ -16,7 +12,7 @@ export function mockRulerRulesApiResponse( } export function mockRulerRulesGroupApiResponse( - server: SetupServerApi, + server: SetupServer, rulesSourceName: string, namespace: string, group: string, diff --git a/yarn.lock b/yarn.lock index 065843e9e2a..86b93744020 100644 --- a/yarn.lock +++ b/yarn.lock @@ -22217,7 +22217,7 @@ __metadata: mousetrap: 1.6.5 mousetrap-global-bind: 1.1.0 moveable: 0.43.1 - msw: 0.49.2 + msw: 1.1.0 mutationobserver-shim: 0.3.7 ngtemplate-loader: 2.1.0 node-notifier: 10.0.1 @@ -28510,9 +28510,9 @@ __metadata: languageName: node linkType: hard -"msw@npm:0.49.2": - version: 0.49.2 - resolution: "msw@npm:0.49.2" +"msw@npm:1.1.0": + version: 1.1.0 + resolution: "msw@npm:1.1.0" dependencies: "@mswjs/cookies": ^0.2.2 "@mswjs/interceptors": ^0.17.5 @@ -28530,7 +28530,7 @@ __metadata: node-fetch: ^2.6.7 outvariant: ^1.3.0 path-to-regexp: ^6.2.0 - strict-event-emitter: ^0.2.6 + strict-event-emitter: ^0.4.3 type-fest: ^2.19.0 yargs: ^17.3.1 peerDependencies: @@ -28540,7 +28540,7 @@ __metadata: optional: true bin: msw: cli/index.js - checksum: 10ec35671e10e53c2a24ca22c5e7a2262d4cdfb409a403a3ffe221150af404805490e7661611a581986e61790bb8996ce8a0f057d521ef359498304c2431234a + checksum: e555547defbc3f07532bdadcbf9f4e3a32b92156d3869e85c9038ecaac220cfc93901727da097b7f9d3ecf9cbbc5a9178c0ecdd3999a9201db4c85c99b92c361 languageName: node linkType: hard @@ -36107,7 +36107,7 @@ __metadata: languageName: node linkType: hard -"strict-event-emitter@npm:^0.2.4, strict-event-emitter@npm:^0.2.6": +"strict-event-emitter@npm:^0.2.4": version: 0.2.8 resolution: "strict-event-emitter@npm:0.2.8" dependencies: @@ -36116,6 +36116,13 @@ __metadata: languageName: node linkType: hard +"strict-event-emitter@npm:^0.4.3": + version: 0.4.6 + resolution: "strict-event-emitter@npm:0.4.6" + checksum: 4f4f2909613e7811de789991c06bfb770d6d6987e2ec5c66fa7485d0f07cc4e7e32eba0dcf26cee6d86af6c92946d7f4acdfaff57d0c4114df2cfa1bf0e3c091 + languageName: node + linkType: hard + "string-argv@npm:^0.3.1": version: 0.3.1 resolution: "string-argv@npm:0.3.1" From 29982eb19454d58758d0331b31104cedbaffa79f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 7 Mar 2023 10:42:49 +0000 Subject: [PATCH 007/288] Update dependency stylelint to v15 (#64282) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 149 ++++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 119 insertions(+), 32 deletions(-) diff --git a/package.json b/package.json index ee2fe272ead..fa24c977cf5 100644 --- a/package.json +++ b/package.json @@ -235,7 +235,7 @@ "sass-loader": "13.2.0", "sinon": "15.0.1", "style-loader": "3.3.1", - "stylelint": "14.14.0", + "stylelint": "15.2.0", "stylelint-config-prettier": "9.0.3", "stylelint-config-sass-guidelines": "9.0.1", "terser-webpack-plugin": "5.3.6", diff --git a/yarn.lock b/yarn.lock index 86b93744020..85027e3e6ad 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3897,6 +3897,32 @@ __metadata: languageName: node linkType: hard +"@csstools/css-parser-algorithms@npm:^2.0.1": + version: 2.0.1 + resolution: "@csstools/css-parser-algorithms@npm:2.0.1" + peerDependencies: + "@csstools/css-tokenizer": ^2.0.0 + checksum: 9f168cfc8fa30ba19fcbe7632d947b74beea494db60cf0eb250f7b77fe72239fb5e6d292b035144dbc52977fb4768f157064f8358530701fe56e5ef2993174c7 + languageName: node + linkType: hard + +"@csstools/css-tokenizer@npm:^2.0.1": + version: 2.1.0 + resolution: "@csstools/css-tokenizer@npm:2.1.0" + checksum: af3619557e18cd348810cfb41b3b7177b36d216df8dd4cac3979f241c15416baf8cdd55fb764ecc78fab5985b085363cfc8e8b8a18aaa361d00c9b9300b7c0f3 + languageName: node + linkType: hard + +"@csstools/media-query-list-parser@npm:^2.0.1": + version: 2.0.1 + resolution: "@csstools/media-query-list-parser@npm:2.0.1" + peerDependencies: + "@csstools/css-parser-algorithms": ^2.0.0 + "@csstools/css-tokenizer": ^2.0.0 + checksum: f30b2a9e1aa3c2d8e98d3ac79463ab514ba4c4577c4cc6120a69d9f4562e521766adb62fc44ffee2226a967bdcfcf86a46bdaf625d21af8f21caa99553994d60 + languageName: node + linkType: hard + "@csstools/postcss-color-function@npm:^1.0.3": version: 1.1.0 resolution: "@csstools/postcss-color-function@npm:1.1.0" @@ -3988,13 +4014,13 @@ __metadata: languageName: node linkType: hard -"@csstools/selector-specificity@npm:^2.0.2": - version: 2.0.2 - resolution: "@csstools/selector-specificity@npm:2.0.2" +"@csstools/selector-specificity@npm:^2.1.1": + version: 2.1.1 + resolution: "@csstools/selector-specificity@npm:2.1.1" peerDependencies: - postcss: ^8.2 + postcss: ^8.4 postcss-selector-parser: ^6.0.10 - checksum: a2045a27276a6cfe645b6e212afc217d9a43174ea7a1fa1ab8918d5a0ace72380fbd9837fe1920c547985c11a9070dc48c5c80d483d3f581ddf7aa688204d44f + checksum: 392ab62732e93aa8cbea445bf3485c1acbbecc8ec087b200e06c9ddd2acf740fd1fe46abdacf813e7a50a95a60346377ee3eecb4e1fe3709582e2851430b376a languageName: node linkType: hard @@ -16756,6 +16782,18 @@ __metadata: languageName: node linkType: hard +"cosmiconfig@npm:^8.0.0": + version: 8.1.0 + resolution: "cosmiconfig@npm:8.1.0" + dependencies: + import-fresh: ^3.2.1 + js-yaml: ^4.1.0 + parse-json: ^5.0.0 + path-type: ^4.0.0 + checksum: 78a1846acc4935ab4d928e3f768ee2ad2fddbec96377935462749206568423ff4757140ac7f2ccd1f547f86309b8448c04b26588848b5a1520f2e9741cdeecf0 + languageName: node + linkType: hard + "cp-file@npm:^7.0.0": version: 7.0.0 resolution: "cp-file@npm:7.0.0" @@ -17081,6 +17119,16 @@ __metadata: languageName: node linkType: hard +"css-tree@npm:^2.3.1": + version: 2.3.1 + resolution: "css-tree@npm:2.3.1" + dependencies: + mdn-data: 2.0.30 + source-map-js: ^1.0.1 + checksum: 493cc24b5c22b05ee5314b8a0d72d8a5869491c1458017ae5ed75aeb6c3596637dbe1b11dac2548974624adec9f7a1f3a6cf40593dc1f9185eb0e8279543fbc0 + languageName: node + linkType: hard + "css-what@npm:^5.0.0": version: 5.1.0 resolution: "css-what@npm:5.1.0" @@ -22291,7 +22339,7 @@ __metadata: slate-react: 0.22.10 sql-formatter-plus: ^1.3.6 style-loader: 3.3.1 - stylelint: 14.14.0 + stylelint: 15.2.0 stylelint-config-prettier: 9.0.3 stylelint-config-sass-guidelines: 9.0.1 symbol-observable: 4.0.0 @@ -23306,6 +23354,13 @@ __metadata: languageName: node linkType: hard +"ignore@npm:^5.2.4": + version: 5.2.4 + resolution: "ignore@npm:5.2.4" + checksum: 3d4c309c6006e2621659311783eaea7ebcd41fe4ca1d78c91c473157ad6666a57a2df790fe0d07a12300d9aac2888204d7be8d59f9aaf665b1c7fcdb432517ef + languageName: node + linkType: hard + "image-size@npm:~0.5.0": version: 0.5.5 resolution: "image-size@npm:0.5.5" @@ -26402,10 +26457,10 @@ __metadata: languageName: node linkType: hard -"known-css-properties@npm:^0.25.0": - version: 0.25.0 - resolution: "known-css-properties@npm:0.25.0" - checksum: 1e6860b9cb8f671fc913f0a94a04c278769d9d8ac69f7975986440ef19825bdc26d8833e59ef7ef7ec3d4984e28e4f73e7bf99b9deb24803841d39135c26a1e6 +"known-css-properties@npm:^0.26.0": + version: 0.26.0 + resolution: "known-css-properties@npm:0.26.0" + checksum: e706f4af9d2683202df9f717e7d713f0f8c3330f155842c40d8f3b2a5837956c34aeb7ba08760977ccde1afce8b5377e29b40eb3e5c0b42bef28ddd108543cfb languageName: node linkType: hard @@ -27402,6 +27457,13 @@ __metadata: languageName: node linkType: hard +"mdn-data@npm:2.0.30": + version: 2.0.30 + resolution: "mdn-data@npm:2.0.30" + checksum: d6ac5ac7439a1607df44b22738ecf83f48e66a0874e4482d6424a61c52da5cde5750f1d1229b6f5fa1b80a492be89465390da685b11f97d62b8adcc6e88189aa + languageName: node + linkType: hard + "mdurl@npm:^1.0.0": version: 1.0.1 resolution: "mdurl@npm:1.0.1" @@ -31309,6 +31371,16 @@ __metadata: languageName: node linkType: hard +"postcss-selector-parser@npm:^6.0.11": + version: 6.0.11 + resolution: "postcss-selector-parser@npm:6.0.11" + dependencies: + cssesc: ^3.0.0 + util-deprecate: ^1.0.2 + checksum: 0b01aa9c2d2c8dbeb51e9b204796b678284be9823abc8d6d40a8b16d4149514e922c264a8ed4deb4d6dbced564b9be390f5942c058582d8656351516d6c49cde + languageName: node + linkType: hard + "postcss-selector-parser@npm:^6.0.2, postcss-selector-parser@npm:^6.0.4, postcss-selector-parser@npm:^6.0.5, postcss-selector-parser@npm:^6.0.6": version: 6.0.6 resolution: "postcss-selector-parser@npm:6.0.6" @@ -31418,6 +31490,17 @@ __metadata: languageName: node linkType: hard +"postcss@npm:^8.4.21": + version: 8.4.21 + resolution: "postcss@npm:8.4.21" + dependencies: + nanoid: ^3.3.4 + picocolors: ^1.0.0 + source-map-js: ^1.0.2 + checksum: e39ac60ccd1542d4f9d93d894048aac0d686b3bb38e927d8386005718e6793dbbb46930f0a523fe382f1bbd843c6d980aaea791252bf5e176180e5a4336d9679 + languageName: node + linkType: hard + "postcss@npm:^8.4.7": version: 8.4.7 resolution: "postcss@npm:8.4.7" @@ -35627,7 +35710,7 @@ __metadata: languageName: node linkType: hard -"source-map-js@npm:^1.0.2": +"source-map-js@npm:^1.0.1, source-map-js@npm:^1.0.2": version: 1.0.2 resolution: "source-map-js@npm:1.0.2" checksum: c049a7fc4deb9a7e9b481ae3d424cc793cb4845daa690bc5a05d428bf41bf231ced49b4cf0c9e77f9d42fdb3d20d6187619fc586605f5eabe995a316da8d377c @@ -36569,15 +36652,19 @@ __metadata: languageName: node linkType: hard -"stylelint@npm:14.14.0": - version: 14.14.0 - resolution: "stylelint@npm:14.14.0" +"stylelint@npm:15.2.0": + version: 15.2.0 + resolution: "stylelint@npm:15.2.0" dependencies: - "@csstools/selector-specificity": ^2.0.2 + "@csstools/css-parser-algorithms": ^2.0.1 + "@csstools/css-tokenizer": ^2.0.1 + "@csstools/media-query-list-parser": ^2.0.1 + "@csstools/selector-specificity": ^2.1.1 balanced-match: ^2.0.0 colord: ^2.9.3 - cosmiconfig: ^7.0.1 + cosmiconfig: ^8.0.0 css-functions-list: ^3.1.0 + css-tree: ^2.3.1 debug: ^4.3.4 fast-glob: ^3.2.12 fastest-levenshtein: ^1.0.16 @@ -36586,21 +36673,21 @@ __metadata: globby: ^11.1.0 globjoin: ^0.1.4 html-tags: ^3.2.0 - ignore: ^5.2.0 + ignore: ^5.2.4 import-lazy: ^4.0.0 imurmurhash: ^0.1.4 is-plain-object: ^5.0.0 - known-css-properties: ^0.25.0 + known-css-properties: ^0.26.0 mathml-tag-names: ^2.1.3 meow: ^9.0.0 micromatch: ^4.0.5 normalize-path: ^3.0.0 picocolors: ^1.0.0 - postcss: ^8.4.17 + postcss: ^8.4.21 postcss-media-query-parser: ^0.2.3 postcss-resolve-nested-selector: ^0.1.1 postcss-safe-parser: ^6.0.0 - postcss-selector-parser: ^6.0.10 + postcss-selector-parser: ^6.0.11 postcss-value-parser: ^4.2.0 resolve-from: ^5.0.0 string-width: ^4.2.3 @@ -36608,12 +36695,12 @@ __metadata: style-search: ^0.1.0 supports-hyperlinks: ^2.3.0 svg-tags: ^1.0.0 - table: ^6.8.0 + table: ^6.8.1 v8-compile-cache: ^2.3.0 - write-file-atomic: ^4.0.2 + write-file-atomic: ^5.0.0 bin: stylelint: bin/stylelint.js - checksum: 3196f5992262d82f3c8dea1d710a3dda1b6c890697eba7df58a3bc8fcd681d509449fa4be3f03e91155f5014df82973b57266ddbc34f41130aa27cc046113b8b + checksum: 2a52d1b36345659e92280317554d82fc621203341d3a6baf1b19f3c28d06a07b8c5f7a25b9be2383f73a4e6a395d69dfbbe4daeca19bd119b88673dd8b6518ac languageName: node linkType: hard @@ -36770,16 +36857,16 @@ __metadata: languageName: node linkType: hard -"table@npm:^6.8.0": - version: 6.8.0 - resolution: "table@npm:6.8.0" +"table@npm:^6.8.1": + version: 6.8.1 + resolution: "table@npm:6.8.1" dependencies: ajv: ^8.0.1 lodash.truncate: ^4.4.2 slice-ansi: ^4.0.0 string-width: ^4.2.3 strip-ansi: ^6.0.1 - checksum: 5b07fe462ee03d2e1fac02cbb578efd2e0b55ac07e3d3db2e950aa9570ade5a4a2b8d3c15e9f25c89e4e50b646bc4269934601ee1eef4ca7968ad31960977690 + checksum: 08249c7046125d9d0a944a6e96cfe9ec66908d6b8a9db125531be6eb05fa0de047fd5542e9d43b4f987057f00a093b276b8d3e19af162a9c40db2681058fd306 languageName: node linkType: hard @@ -39675,13 +39762,13 @@ __metadata: languageName: node linkType: hard -"write-file-atomic@npm:^4.0.2": - version: 4.0.2 - resolution: "write-file-atomic@npm:4.0.2" +"write-file-atomic@npm:^5.0.0": + version: 5.0.0 + resolution: "write-file-atomic@npm:5.0.0" dependencies: imurmurhash: ^0.1.4 signal-exit: ^3.0.7 - checksum: 5da60bd4eeeb935eec97ead3df6e28e5917a6bd317478e4a85a5285e8480b8ed96032bbcc6ecd07b236142a24f3ca871c924ec4a6575e623ec1b11bf8c1c253c + checksum: 6ee16b195572386cb1c905f9d29808f77f4de2fd063d74a6f1ab6b566363832d8906a493b764ee715e57ab497271d5fc91642a913724960e8e845adf504a9837 languageName: node linkType: hard From 9d9421154e1fe76b1937dc6ef43412a9ed33945c Mon Sep 17 00:00:00 2001 From: George Robinson Date: Tue, 7 Mar 2023 10:52:33 +0000 Subject: [PATCH 008/288] Docs: Update Images in notifications docs to be more readable and instructive (#64227) --- .../images-in-notifications.md | 82 +++++++++---------- 1 file changed, 38 insertions(+), 44 deletions(-) diff --git a/docs/sources/alerting/manage-notifications/images-in-notifications.md b/docs/sources/alerting/manage-notifications/images-in-notifications.md index d061189d8f3..fc27b79983c 100644 --- a/docs/sources/alerting/manage-notifications/images-in-notifications.md +++ b/docs/sources/alerting/manage-notifications/images-in-notifications.md @@ -13,31 +13,26 @@ weight: 500 Images in notifications helps recipients of alert notifications better understand why an alert has fired or resolved by including a screenshot of the panel associated with the alert. -> **Note**: This feature is not supported for Mimir or Loki rules, or when Grafana sends alert notifications to an external Alertmanager. +> **Note**: This feature is not supported in Mimir or Loki, or when Grafana is configured to send alerts to other Alertmanagers such as the Prometheus Alertmanager When an alert is fired or resolved Grafana takes a screenshot of the panel associated with the alert. This is determined via the Dashboard UID and Panel ID annotations of the rule. Grafana cannot take a screenshot for alerts that are not associated with a panel. -Because a number of contact points, such as email, do not support uploading screenshots at the time of sending a notification; Grafana can also upload the screenshot to a cloud storage service such as Amazon S3, Azure Blob Storage and Google Cloud Storage, where a link to the uploaded screenshot can be added to the notification. However, if using a cloud storage service is not an option then Grafana can be its own cloud storage service such that the screenshot is available under the same domain as Grafana. +Grafana takes at most two screenshots for each alert: once when the alert fires and again when the alert is resolved. Screenshots are not re-taken over the lifetime of the alert, instead you should open the panel in Grafana to follow the data in real time. In addition, depending on how alerts are grouped in your notification policies, Grafana might send a notification with many screenshots of the same panel. This happens because Grafana does not know how your alerts are grouped at the time a screenshot is taken, and so acts conservatively by taking a screenshot for every alert. -Should either the cloud storage service, or Grafana if acting as its own cloud storage service, be protected by a firewall, gateway service or VPN, then screenshots might not be shown in notifications. - -How to choose between uploading screenshots at the time of sending the notification, using a cloud storage service, or using Grafana as its own cloud storage service, depends on which contact points you plan to use and whether you use a firewall, gateway service or VPN. - -For example, if a contact point supports uploading images at the time of notification is it not required to use cloud storage. Cloud storage is required when a contact point does not support uploading images at the time of sending a notification, such as email. We don't recommend using cloud storage if the cloud storage service is behind a firewall, gateway service, or VPN, as screenshots might not be shown in notifications. +Once a screenshot has been taken Grafana can either upload it to a cloud storage service such as Amazon S3, Azure Blob Storage or Google Cloud Storage; upload the screenshot to it's internal web server; or upload it to the service that is receiving the notification, such as Slack. Which option you should choose depends on how your Grafana is managed and which integrations you use. More information on this can be found in Requirements. Please refer to the table at the end of this page for a list of contact points and their support for images in notifications. ## Requirements -To use images in notifications, Grafana must be set up to use [image rendering](https://grafana.com/docs/grafana/next/setup-grafana/image-rendering/). You can either install the image rendering plugin or run it as a remote rendering service. - -When a screenshot is taken it is saved to the [data]({{< relref "../../setup-grafana/configure-grafana/#paths" >}}) path. This is where screenshots are stored before being sent in a notification or uploaded to a cloud storage service. Grafana must have write-access to this path. If Grafana cannot write to this path then screenshots cannot be saved to disk and an error will be logged for each failed screenshot attempt. - -If using a [cloud storage service](https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#external_image_storage) such as Amazon S3, Azure Blob Storage or Google Cloud Storage, uploaded images need to be accessible outside of a firewall, gateway service or VPN for screenshots to be shown in notifications. Grafana will not delete screenshots from cloud storage. We recommend configuring a retention policy on the bucket to delete screenshots older than 1 month. - -If using Grafana as its own cloud storage service then screenshots will be saved to `static_root_path/img/attachments`. `static_root_path` is a configuration option for Grafana and can be found in `defaults.ini`. However, like when using a cloud storage service, images need to be accessible outside of a firewall, gateway service or VPN for screenshots to be shown in notifications. - -When using Grafana as its own cloud storage service screenshots are copied from [data]({{< relref "../../setup-grafana/configure-grafana/#paths" >}}) to `static_root_path/img/attachments`. Screenshots older than `temp_data_lifetime` are deleted from [data]({{< relref "../../setup-grafana/configure-grafana/#paths" >}}) but not from `static_root_path/images/attachments`. To delete screenshots from `static_root_path` after a certain amount of time we recommend setting up a CRON job. +1. To use images in notifications, Grafana must be set up to use [image rendering](https://grafana.com/docs/grafana/next/setup-grafana/image-rendering/). You can either install the image rendering plugin or run it as a remote rendering service. +2. When a screenshot is taken it is saved to the [data]({{< relref "../../setup-grafana/configure-grafana/#paths" >}}) folder, even if Grafana is configured to upload screenshots to a cloud storage service. Grafana must have write-access to this folder otherwise screenshots cannot be saved to disk and an error will be logged for each failed screenshot attempt. +3. You should configure Grafana to upload screenshots if sending alerts to integrations other than Discord, Email, Pushover, Slack or Telegram. If Grafana is behind a corporate network or VPN you should use a cloud storage service such as Amazon S3, Azure Blob Storage or Google Cloud Storage, otherwise you can configure Grafana to upload screenshots to its internal web server. +4. If uploading screenshots to a cloud storage service, uploaded images might need to be accessible outside of your VPN or corporate network as some instant messaging and communication platforms rewrite URLs to go via their CDN. If this is not an option for due to security concerns we recommend using integrations that [supports uploading images]({{}}) or [disabling images in notifications]({{}}) altogether. +5. When uploading screenshots to a cloud storage service Grafana uses a random 20 character (30 characters for Azure Blob Storage) filename for each image. This makes URLs hard to guess, but not impossible. If this is a security concern we recommend using integrations that [supports uploading images]({{}}) or [disabling images in notifications]({{}}) altogether. +6. Grafana does not delete screenshots from cloud storage. We recommend configuring a retention policy with your cloud storage service to delete screenshots older than 1 month. +7. If Grafana is configured to upload screenshots to its internal web server then this web server might need to be accessible via the Internet outside your VPN or corporate network as some instant messaging and communication platforms rewrite URLs to go via their CDN. You should understand there are risks involved with this, and at a minimum make sure Grafana is secured with https and that all Grafana users have strong passwords. If this is not an option for you due to security concerns we recommend using integrations that [supports uploading images]({{}}) or [disabling images in notifications]({{}}) altogether. +8. Grafana does not delete screenshots uploaded to its internal web server. To delete screenshots from `static_root_path/images/attachments` after a certain amount of time we recommend setting up a CRON job. ## Configuration @@ -73,36 +68,36 @@ We recommended that `max_concurrent_screenshots` is less than or equal to `concu # the total number of concurrent screenshots across all Grafana services. max_concurrent_screenshots = 5 -## Support for images in contact points +## Supported contact points -Grafana supports a wide range of contact points with varied support for images in notifications. The table below shows the list of all contact points supported in Grafana and their support for uploading images at the time of sending the notification and images uploaded to cloud storage, including when Grafana is acting as its own cloud storage service. +Grafana supports a wide range of contact points with varied support for images in notifications. The table below shows the list of all contact points supported in Grafana and their support for uploading screenshots to the receiving service and referencing screenshots that have been uploaded to a cloud storage service. -| Name | Upload images from disk | Include images from URL | -| ----------------------- | --------------------------- | ------------------------- | -| DingDing | No | No | -| Discord | Yes | Yes | -| Email | Yes | Yes | -| Google Hangouts Chat | No | Yes | -| Kafka | No | No | -| Line | No | No | -| Microsoft Teams | No | Yes | -| Opsgenie | No | Yes | -| Pagerduty | No | Yes | -| Prometheus Alertmanager | No | No | -| Pushover | Yes | No | -| Sensu Go | No | No | -| Slack | Yes (when using Bot tokens) | Yes (when using webhooks) | -| Telegram | Yes | No | -| Threema | No | No | -| VictorOps | No | No | -| Webhook | No | Yes | +| Name | Upload from disk | Reference from cloud storage | +| ----------------------- | ---------------------------------------------------------- | -------------------------------------------------------- | +| DingDing | No | No | +| Discord | Yes (Maximum of 10 per notification) | Yes (Maximum of 10 per notification) | +| Email | Yes (Embedded in the email) | Yes | +| Google Hangouts Chat | No | Yes | +| Kafka | No | No | +| Line | No | No | +| Microsoft Teams | No | Yes | +| Opsgenie | No | Yes | +| Pagerduty | No | Yes | +| Prometheus Alertmanager | No | No | +| Pushover | Yes (Maximum of 1 per notification) | No | +| Sensu Go | No | No | +| Slack | Yes (when using Bot tokens, maximum of 5 per notification) | Yes (when using webhooks, maximum of 1 per notification) | +| Telegram | Yes | No | +| Threema | No | No | +| VictorOps | No | No | +| Webhook | No | Yes | ## Limitations -- This feature is not supported for Mimir or Loki rules, or when Grafana sends alert notifications to an external Alertmanager. -- When multiple alerts are sent in a single notification a screenshot might be included for each alert. The order the images are shown in random. -- Some contact points support at most one image per notification. In this case, the first image associated with an alert will be attached. -- We don't recommend using cloud storage if the cloud storage service is behind a firewall, gateway service, or VPN, as screenshots might not be shown in notifications. +- This feature is not supported in Mimir or Loki, or when Grafana is configured to send alerts to other Alertmanagers such as the Prometheus Alertmanager. +- A number of contact points support at most one image per notification. In this case, just the first image is either uploaded to the receiving service or referenced from cloud storage per notification. +- When multiple alerts are sent in a single notification a screenshot might be included for each alert. The order the images are shown is random. +- Screenshots might not be shown in notifications if your cloud storage is behind a firewall, gateway service, or VPN as some instant messaging and communication platforms rewrite URLs to go via their CDN. ## Troubleshooting @@ -113,9 +108,8 @@ If Grafana has been set up to send images in notifications, however notification 3. If the alert is not associated with a dashboard there will be logs for `Cannot take screenshot for alert rule as it is not associated with a dashboard`. 4. If the alert is associated with a dashboard, but no panel in the dashboard, there will be logs for `Cannot take screenshot for alert rule as it is not associated with a panel`. 5. If images cannot be taken because of mis-configuration or an issue with image rendering there will be logs for `Failed to take an image` including the Dashboard UID, Panel ID, and the error message. -6. Check that the contact point supports images in notifications, and the present configuration, as per the table. -7. If the image was uploaded to cloud storage make sure it is public. -8. If images are made available via Grafana's built in web server make sure it is accessible via the Internet. +6. Check that the contact point supports images in notifications and whether it supports uploading images to the receiving service or referencing images that have been uploaded to a cloud storage service. +7. If the image was uploaded to cloud storage make sure that it is public. Screenshots might not be shown in notifications if your cloud storage is behind a firewall, gateway service, or VPN as some instant messaging and communication platforms rewrite URLs to go via their CDN. ## Metrics From 7211422850eb3e15c566f906c5816ec2fcefa777 Mon Sep 17 00:00:00 2001 From: Jack Baldry Date: Tue, 7 Mar 2023 10:53:03 +0000 Subject: [PATCH 009/288] Add to Docs squad project when PRs are labelled with type/docs (#64285) Signed-off-by: Jack Baldry --- .github/pr-commands.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/pr-commands.json b/.github/pr-commands.json index 81f055a99ce..1eb7b01a620 100644 --- a/.github/pr-commands.json +++ b/.github/pr-commands.json @@ -190,5 +190,13 @@ "ignoreList": ["renovate[bot]","dependabot[bot]"], "action": "updateLabel", "addLabel": "pr/external" + }, + { + "type":"label", + "name":"type/docs", + "action":"addToProject", + "addToProject":{ + "url":"https://github.com/orgs/grafana/projects/69" + } } ] From 5b8c2f494d8a312c153282dff4822e072a3a0cf7 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Tue, 7 Mar 2023 11:13:57 +0000 Subject: [PATCH 010/288] Chore: convert `VariableOptions` to use emotion instead of scss (#64135) convert VariableOptions to use emotion instead of scss --- .../load-options-from-url.spec.ts | 16 +-- .../new-query-variable.spec.ts | 4 +- .../set-options-from-ui.spec.ts | 12 +- .../src/selectors/components.ts | 3 + .../src/themes/_variables.dark.scss.tmpl.ts | 3 - .../src/themes/_variables.light.scss.tmpl.ts | 3 - .../pickers/shared/VariableOptions.tsx | 131 ++++++++++++++---- public/sass/_variables.dark.generated.scss | 3 - public/sass/_variables.light.generated.scss | 3 - public/sass/components/_submenu.scss | 116 ---------------- 10 files changed, 123 insertions(+), 171 deletions(-) diff --git a/e2e/dashboards-suite/load-options-from-url.spec.ts b/e2e/dashboards-suite/load-options-from-url.spec.ts index 4c03e37675b..d4e2f486eed 100644 --- a/e2e/dashboards-suite/load-options-from-url.spec.ts +++ b/e2e/dashboards-suite/load-options-from-url.spec.ts @@ -20,7 +20,7 @@ describe('Variables - Load options from Url', () => { e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() .should('be.visible') .within(() => { - e2e().get('.variable-option').should('have.length', 9); + e2e.components.Variables.variableOption().should('have.length', 9); }); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); @@ -33,7 +33,7 @@ describe('Variables - Load options from Url', () => { e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() .should('be.visible') .within(() => { - e2e().get('.variable-option').should('have.length', 9); + e2e.components.Variables.variableOption().should('have.length', 9); }); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); @@ -46,7 +46,7 @@ describe('Variables - Load options from Url', () => { e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() .should('be.visible') .within(() => { - e2e().get('.variable-option').should('have.length', 9); + e2e.components.Variables.variableOption().should('have.length', 9); }); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); @@ -72,7 +72,7 @@ describe('Variables - Load options from Url', () => { e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() .should('be.visible') .within(() => { - e2e().get('.variable-option').should('have.length', 9); + e2e.components.Variables.variableOption().should('have.length', 9); }); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); @@ -85,7 +85,7 @@ describe('Variables - Load options from Url', () => { e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() .should('be.visible') .within(() => { - e2e().get('.variable-option').should('have.length', 9); + e2e.components.Variables.variableOption().should('have.length', 9); }); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); @@ -98,7 +98,7 @@ describe('Variables - Load options from Url', () => { e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() .should('be.visible') .within(() => { - e2e().get('.variable-option').should('have.length', 9); + e2e.components.Variables.variableOption().should('have.length', 9); }); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); @@ -135,7 +135,7 @@ describe('Variables - Load options from Url', () => { e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() .should('be.visible') .within(() => { - e2e().get('.variable-option').should('have.length', 9); + e2e.components.Variables.variableOption().should('have.length', 9); }); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); @@ -147,7 +147,7 @@ describe('Variables - Load options from Url', () => { e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() .should('be.visible') .within(() => { - e2e().get('.variable-option').should('have.length', 65); + e2e.components.Variables.variableOption().should('have.length', 65); }); }); }); diff --git a/e2e/dashboards-suite/new-query-variable.spec.ts b/e2e/dashboards-suite/new-query-variable.spec.ts index 40956de18f4..4ef03f29168 100644 --- a/e2e/dashboards-suite/new-query-variable.spec.ts +++ b/e2e/dashboards-suite/new-query-variable.spec.ts @@ -123,7 +123,7 @@ describe('Variables - Query - Add variable', () => { e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() .should('be.visible') .within(() => { - e2e().get('.variable-option').should('have.length', 1); + e2e.components.Variables.variableOption().should('have.length', 1); }); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('C').should('be.visible'); @@ -195,7 +195,7 @@ describe('Variables - Query - Add variable', () => { e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() .should('be.visible') .within(() => { - e2e().get('.variable-option').should('have.length', 2); + e2e.components.Variables.variableOption().should('have.length', 2); }); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); diff --git a/e2e/dashboards-suite/set-options-from-ui.spec.ts b/e2e/dashboards-suite/set-options-from-ui.spec.ts index f62eee52e3e..94d1fbe60fd 100644 --- a/e2e/dashboards-suite/set-options-from-ui.spec.ts +++ b/e2e/dashboards-suite/set-options-from-ui.spec.ts @@ -33,7 +33,7 @@ describe('Variables - Set options from ui', () => { e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() .should('be.visible') .within(() => { - e2e().get('.variable-option').should('have.length', 9); + e2e.components.Variables.variableOption().should('have.length', 9); }); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); @@ -46,7 +46,7 @@ describe('Variables - Set options from ui', () => { e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() .should('be.visible') .within(() => { - e2e().get('.variable-option').should('have.length', 65); + e2e.components.Variables.variableOption().should('have.length', 65); }); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); @@ -89,7 +89,7 @@ describe('Variables - Set options from ui', () => { e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() .should('be.visible') .within(() => { - e2e().get('.variable-option').should('have.length', 17); + e2e.components.Variables.variableOption().should('have.length', 17); }); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); @@ -105,7 +105,7 @@ describe('Variables - Set options from ui', () => { e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() .should('be.visible') .within(() => { - e2e().get('.variable-option').should('have.length', 9); + e2e.components.Variables.variableOption().should('have.length', 9); }); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); @@ -147,7 +147,7 @@ describe('Variables - Set options from ui', () => { e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() .should('be.visible') .within(() => { - e2e().get('.variable-option').should('have.length', 9); + e2e.components.Variables.variableOption().should('have.length', 9); }); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); @@ -160,7 +160,7 @@ describe('Variables - Set options from ui', () => { e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() .should('be.visible') .within(() => { - e2e().get('.variable-option').should('have.length', 9); + e2e.components.Variables.variableOption().should('have.length', 9); }); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBA').should('be.visible'); diff --git a/packages/grafana-e2e-selectors/src/selectors/components.ts b/packages/grafana-e2e-selectors/src/selectors/components.ts index 26b53b7168d..50b2645979f 100644 --- a/packages/grafana-e2e-selectors/src/selectors/components.ts +++ b/packages/grafana-e2e-selectors/src/selectors/components.ts @@ -403,4 +403,7 @@ export const Components = { AnalyticsToolbarButton: { button: 'Dashboard insights', }, + Variables: { + variableOption: 'data-testid variable-option', + }, }; diff --git a/packages/grafana-ui/src/themes/_variables.dark.scss.tmpl.ts b/packages/grafana-ui/src/themes/_variables.dark.scss.tmpl.ts index 914343221b0..56122ca09a2 100644 --- a/packages/grafana-ui/src/themes/_variables.dark.scss.tmpl.ts +++ b/packages/grafana-ui/src/themes/_variables.dark.scss.tmpl.ts @@ -354,9 +354,6 @@ $diff-json-changed-num: $text-color; $diff-json-icon: $gray-5; -//Submenu -$variable-option-bg: $dropdownLinkBackgroundHover; - //Switch Slider // ------------------------- $switch-bg: $input-bg; diff --git a/packages/grafana-ui/src/themes/_variables.light.scss.tmpl.ts b/packages/grafana-ui/src/themes/_variables.light.scss.tmpl.ts index 1d88a8a3b24..f1b1b1baa4d 100644 --- a/packages/grafana-ui/src/themes/_variables.light.scss.tmpl.ts +++ b/packages/grafana-ui/src/themes/_variables.light.scss.tmpl.ts @@ -353,9 +353,6 @@ $diff-json-changed-num: $gray-4; $diff-json-icon: $gray-4; -//Submenu -$variable-option-bg: $dropdownLinkBackgroundHover; - //Switch Slider // ------------------------- $switch-bg: $white; diff --git a/public/app/features/variables/pickers/shared/VariableOptions.tsx b/public/app/features/variables/pickers/shared/VariableOptions.tsx index ad538e2a408..e5f7214c9d2 100644 --- a/public/app/features/variables/pickers/shared/VariableOptions.tsx +++ b/public/app/features/variables/pickers/shared/VariableOptions.tsx @@ -1,9 +1,9 @@ import { css, cx } from '@emotion/css'; -import classNames from 'classnames'; import React, { PureComponent } from 'react'; +import { GrafanaTheme2 } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; -import { Tooltip, Themeable2, withTheme2, clearButtonStyles } from '@grafana/ui'; +import { Tooltip, Themeable2, withTheme2, clearButtonStyles, stylesFactory } from '@grafana/ui'; import { Trans, t } from 'app/core/internationalization'; import { ALL_VARIABLE_VALUE } from '../../constants'; @@ -41,13 +41,14 @@ class VariableOptions extends PureComponent { render() { // Don't want to pass faulty rest props to the div - const { multi, values, highlightIndex, selectedValues, onToggle, onToggleAll, ...restProps } = this.props; + const { multi, values, highlightIndex, selectedValues, onToggle, onToggleAll, theme, ...restProps } = this.props; + const styles = getStyles(theme); return ( -
-
+
+
    @@ -60,22 +61,34 @@ class VariableOptions extends PureComponent { } renderOption(option: VariableOption, index: number) { - const { highlightIndex, theme } = this.props; - const selectClass = option.selected ? 'variable-option pointer selected' : 'variable-option pointer'; - const highlightClass = index === highlightIndex ? `${selectClass} highlighted` : selectClass; + const { highlightIndex, multi, theme } = this.props; + const styles = getStyles(theme); const isAllOption = option.value === ALL_VARIABLE_VALUE; return (
  • @@ -115,16 +134,74 @@ class VariableOptions extends PureComponent { } } -const listStyles = cx( - 'variable-options-column', - css` - list-style-type: none; - ` -); +const getStyles = stylesFactory((theme: GrafanaTheme2) => { + const checkboxImageUrl = theme.isDark ? 'public/img/checkbox.png' : 'public/img/checkbox_white.png'; -const noStyledButton = css` - width: 100%; - text-align: left; -`; + return { + hideVariableOptionIcon: css({ + display: 'none', + }), + highlighted: css({ + backgroundColor: theme.colors.action.hover, + }), + noStyledButton: css({ + width: '100%', + textAlign: 'left', + }), + variableOption: css({ + display: 'block', + padding: '2px 27px 0 8px', + position: 'relative', + whiteSpace: 'nowrap', + minWidth: '115px', + ['&:hover']: { + backgroundColor: theme.colors.action.hover, + }, + }), + variableOptionColumnHeader: css({ + paddingTop: '5px', + paddingBottom: '5px', + marginBottom: '5px', + }), + variableOptionIcon: css({ + display: 'inline-block', + width: '24px', + height: '18px', + position: 'relative', + top: '4px', + background: `url(${checkboxImageUrl}) left top no-repeat`, + }), + variableOptionIconManySelected: css({ + background: `url(${checkboxImageUrl}) 0px -36px no-repeat`, + }), + variableOptionIconSelected: css({ + background: `url(${checkboxImageUrl}) 0px -18px no-repeat`, + }), + variableValueDropdown: css({ + backgroundColor: theme.colors.background.primary, + border: `1px solid ${theme.colors.border.weak}`, + borderRadius: theme.shape.borderRadius(2), + boxShadow: theme.shadows.z2, + position: 'absolute', + top: theme.spacing(theme.components.height.md), + maxHeight: '400px', + minHeight: '150px', + minWidth: '150px', + overflowY: 'auto', + overflowX: 'hidden', + zIndex: theme.zIndex.typeahead, + }), + variableOptionsColumn: css({ + maxHeight: '350px', + display: 'table-cell', + lineHeight: '26px', + listStyleType: 'none', + }), + variableOptionsWrapper: css({ + display: 'table', + width: '100%', + }), + }; +}); export default withTheme2(VariableOptions); diff --git a/public/sass/_variables.dark.generated.scss b/public/sass/_variables.dark.generated.scss index 2d8026af48b..e1197d4f35e 100644 --- a/public/sass/_variables.dark.generated.scss +++ b/public/sass/_variables.dark.generated.scss @@ -356,9 +356,6 @@ $diff-json-changed-num: $text-color; $diff-json-icon: $gray-5; -//Submenu -$variable-option-bg: $dropdownLinkBackgroundHover; - //Switch Slider // ------------------------- $switch-bg: $input-bg; diff --git a/public/sass/_variables.light.generated.scss b/public/sass/_variables.light.generated.scss index c7022ca3136..84147e39fc4 100644 --- a/public/sass/_variables.light.generated.scss +++ b/public/sass/_variables.light.generated.scss @@ -353,9 +353,6 @@ $diff-json-changed-num: $gray-4; $diff-json-icon: $gray-4; -//Submenu -$variable-option-bg: $dropdownLinkBackgroundHover; - //Switch Slider // ------------------------- $switch-bg: $white; diff --git a/public/sass/components/_submenu.scss b/public/sass/components/_submenu.scss index b253b8b86be..3b97b28a940 100644 --- a/public/sass/components/_submenu.scss +++ b/public/sass/components/_submenu.scss @@ -8,15 +8,6 @@ padding: 0 0 $space-sm 0; } -.annotation-segment { - padding: 8px 7px; - - label.cr1 { - margin-left: 5px; - margin-top: 3px; - } -} - .submenu-item { display: inline-block; @@ -30,114 +21,7 @@ } } -.variable-value-link { - max-width: 500px; - padding-right: 10px; - padding: 0 $space-sm; - background-color: $input-bg; - border: 1px solid $input-border-color; - border-radius: $input-border-radius; - display: flex; - align-items: center; - color: $text-color; - height: $input-height; - - .label-tag { - margin: 0 5px; - } -} - .variable-link-wrapper { display: inline-block; position: relative; } - -.variable-value-dropdown { - position: absolute; - top: $input-height; - min-width: 150px; - max-height: 400px; - min-height: 150px; - overflow-y: auto; - overflow-x: hidden; - background-color: $dropdownBackground; - box-shadow: $dropdownShadow; - z-index: $zindex-typeahead; - font-size: $font-size-base; - border-radius: 3px 3px 0 0; - border: 1px solid $tight-form-func-bg; - - &.multi { - .selected { - .variable-option-icon { - background: url($checkboxImageUrl) 0px -18px no-repeat; - } - } - } - - &.single { - .variable-option-icon { - display: none; - } - .selected { - background-color: $tight-form-func-highlight-bg; - } - } -} - -.variable-options-wrapper { - display: table; - width: 100%; -} - -.variable-options-column { - max-height: 350px; - display: table-cell; - line-height: 26px; - &:nth-child(2) { - border-left: 1px solid $tight-form-func-bg; - } -} - -.variable-option-tag, -.variable-option, -.variable-options-column-header { - display: block; - padding: 2px 27px 0 8px; - position: relative; - white-space: nowrap; - min-width: 115px; -} - -.variable-options-column-header { - padding-top: 5px; - padding-bottom: 5px; - margin-bottom: 5px; - &.many-selected { - .variable-option-icon { - background: url($checkboxImageUrl) 0px -36px no-repeat; - } - } -} - -.variable-option-icon { - display: inline-block; - width: 24px; - height: 18px; - position: relative; - top: 4px; - background: url($checkboxImageUrl) left top no-repeat; -} - -.variable-option { - &:hover, - &.highlighted { - background-color: $variable-option-bg; - } -} - -.dash-nav-link { - padding: 8px 7px; - display: inline-block; - color: $text-color; -} From 5c13d8eefd8683317bb5ed803147bf8967344172 Mon Sep 17 00:00:00 2001 From: Marcus Andersson Date: Tue, 7 Mar 2023 12:44:45 +0100 Subject: [PATCH 011/288] Plugins: make sure we expose all available field color modes (#64289) expose all available field color modes via fieldcolormodeid enum. --- packages/grafana-data/src/field/fieldColor.ts | 24 +++++++++---------- packages/grafana-data/src/types/fieldColor.ts | 9 +++++++ 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/packages/grafana-data/src/field/fieldColor.ts b/packages/grafana-data/src/field/fieldColor.ts index 003b2e8c684..8cd714e7f7a 100644 --- a/packages/grafana-data/src/field/fieldColor.ts +++ b/packages/grafana-data/src/field/fieldColor.ts @@ -51,70 +51,70 @@ export const fieldColorModeRegistry = new Registry(() => { }, }), new FieldColorSchemeMode({ - id: 'continuous-GrYlRd', + id: FieldColorModeId.ContinuousGrYlRd, name: 'Green-Yellow-Red', isContinuous: true, isByValue: true, getColors: (theme: GrafanaTheme2) => ['green', 'yellow', 'red'], }), new FieldColorSchemeMode({ - id: 'continuous-RdYlGr', + id: FieldColorModeId.ContinuousRdYlGr, name: 'Red-Yellow-Green', isContinuous: true, isByValue: true, getColors: (theme: GrafanaTheme2) => ['red', 'yellow', 'green'], }), new FieldColorSchemeMode({ - id: 'continuous-BlYlRd', + id: FieldColorModeId.ContinuousBlYlRd, name: 'Blue-Yellow-Red', isContinuous: true, isByValue: true, getColors: (theme: GrafanaTheme2) => ['dark-blue', 'super-light-yellow', 'dark-red'], }), new FieldColorSchemeMode({ - id: 'continuous-YlRd', + id: FieldColorModeId.ContinuousYlRd, name: 'Yellow-Red', isContinuous: true, isByValue: true, getColors: (theme: GrafanaTheme2) => ['super-light-yellow', 'dark-red'], }), new FieldColorSchemeMode({ - id: 'continuous-BlPu', + id: FieldColorModeId.ContinuousBlPu, name: 'Blue-Purple', isContinuous: true, isByValue: true, getColors: (theme: GrafanaTheme2) => ['blue', 'purple'], }), new FieldColorSchemeMode({ - id: 'continuous-YlBl', + id: FieldColorModeId.ContinuousYlBl, name: 'Yellow-Blue', isContinuous: true, isByValue: true, getColors: (theme: GrafanaTheme2) => ['super-light-yellow', 'dark-blue'], }), new FieldColorSchemeMode({ - id: 'continuous-blues', + id: FieldColorModeId.ContinuousBlues, name: 'Blues', isContinuous: true, isByValue: true, getColors: (theme: GrafanaTheme2) => ['panel-bg', 'dark-blue'], }), new FieldColorSchemeMode({ - id: 'continuous-reds', + id: FieldColorModeId.ContinuousReds, name: 'Reds', isContinuous: true, isByValue: true, getColors: (theme: GrafanaTheme2) => ['panel-bg', 'dark-red'], }), new FieldColorSchemeMode({ - id: 'continuous-greens', + id: FieldColorModeId.ContinuousGreens, name: 'Greens', isContinuous: true, isByValue: true, getColors: (theme: GrafanaTheme2) => ['panel-bg', 'dark-green'], }), new FieldColorSchemeMode({ - id: 'continuous-purples', + id: FieldColorModeId.ContinuousPurples, name: 'Purples', isContinuous: true, isByValue: true, @@ -124,7 +124,7 @@ export const fieldColorModeRegistry = new Registry(() => { }); interface FieldColorSchemeModeOptions { - id: string; + id: FieldColorModeId; name: string; description?: string; getColors: (theme: GrafanaTheme2) => string[]; @@ -133,7 +133,7 @@ interface FieldColorSchemeModeOptions { } export class FieldColorSchemeMode implements FieldColorMode { - id: string; + id: FieldColorModeId; name: string; description?: string; isContinuous: boolean; diff --git a/packages/grafana-data/src/types/fieldColor.ts b/packages/grafana-data/src/types/fieldColor.ts index 4243f4bed5c..465c268e1a5 100644 --- a/packages/grafana-data/src/types/fieldColor.ts +++ b/packages/grafana-data/src/types/fieldColor.ts @@ -6,6 +6,15 @@ export enum FieldColorModeId { PaletteClassic = 'palette-classic', PaletteSaturated = 'palette-saturated', ContinuousGrYlRd = 'continuous-GrYlRd', + ContinuousRdYlGr = 'continuous-RdYlGr', + ContinuousBlYlRd = 'continuous-BlYlRd', + ContinuousYlRd = 'continuous-YlRd', + ContinuousBlPu = 'continuous-BlPu', + ContinuousYlBl = 'continuous-YlBl', + ContinuousBlues = 'continuous-blues', + ContinuousReds = 'continuous-reds', + ContinuousGreens = 'continuous-greens', + ContinuousPurples = 'continuous-purples', Fixed = 'fixed', } From 981f6fb6cf9cf9bc49b386dd8976f7499dd6a701 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 7 Mar 2023 11:53:52 +0000 Subject: [PATCH 012/288] Update dependency eslint-webpack-plugin to v4 (#64302) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 91 +++++++++++++++++++++++++++++++++++++++------------- 2 files changed, 69 insertions(+), 24 deletions(-) diff --git a/package.json b/package.json index fa24c977cf5..775f1f71beb 100644 --- a/package.json +++ b/package.json @@ -196,7 +196,7 @@ "eslint-plugin-lodash": "7.4.0", "eslint-plugin-react": "7.32.1", "eslint-plugin-react-hooks": "4.6.0", - "eslint-webpack-plugin": "3.2.0", + "eslint-webpack-plugin": "4.0.0", "expose-loader": "4.0.0", "fork-ts-checker-webpack-plugin": "7.3.0", "glob": "9.1.2", diff --git a/yarn.lock b/yarn.lock index 85027e3e6ad..8dd4f7256d1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5874,6 +5874,15 @@ __metadata: languageName: node linkType: hard +"@jest/schemas@npm:^29.4.3": + version: 29.4.3 + resolution: "@jest/schemas@npm:29.4.3" + dependencies: + "@sinclair/typebox": ^0.25.16 + checksum: ac754e245c19dc39e10ebd41dce09040214c96a4cd8efa143b82148e383e45128f24599195ab4f01433adae4ccfbe2db6574c90db2862ccd8551a86704b5bebd + languageName: node + linkType: hard + "@jest/source-map@npm:^27.5.1": version: 27.5.1 resolution: "@jest/source-map@npm:27.5.1" @@ -6093,6 +6102,20 @@ __metadata: languageName: node linkType: hard +"@jest/types@npm:^29.5.0": + version: 29.5.0 + resolution: "@jest/types@npm:29.5.0" + dependencies: + "@jest/schemas": ^29.4.3 + "@types/istanbul-lib-coverage": ^2.0.0 + "@types/istanbul-reports": ^3.0.0 + "@types/node": "*" + "@types/yargs": ^17.0.8 + chalk: ^4.0.0 + checksum: 1811f94b19cf8a9460a289c4f056796cfc373480e0492692a6125a553cd1a63824bd846d7bb78820b7b6f758f6dd3c2d4558293bb676d541b2fa59c70fdf9d39 + languageName: node + linkType: hard + "@jridgewell/gen-mapping@npm:^0.3.0": version: 0.3.1 resolution: "@jridgewell/gen-mapping@npm:0.3.1" @@ -8839,6 +8862,13 @@ __metadata: languageName: node linkType: hard +"@sinclair/typebox@npm:^0.25.16": + version: 0.25.24 + resolution: "@sinclair/typebox@npm:0.25.24" + checksum: 10219c58f40b8414c50b483b0550445e9710d4fe7b2c4dccb9b66533dd90ba8e024acc776026cebe81e87f06fa24b07fdd7bc30dd277eb9cc386ec50151a3026 + languageName: node + linkType: hard + "@sinonjs/commons@npm:^1.7.0": version: 1.8.3 resolution: "@sinonjs/commons@npm:1.8.3" @@ -11149,13 +11179,13 @@ __metadata: languageName: node linkType: hard -"@types/eslint@npm:^7.29.0 || ^8.4.1": - version: 8.4.5 - resolution: "@types/eslint@npm:8.4.5" +"@types/eslint@npm:^8.4.10": + version: 8.21.1 + resolution: "@types/eslint@npm:8.21.1" dependencies: "@types/estree": "*" "@types/json-schema": "*" - checksum: 428b0c971a50adb0d08621e76f21b284580a0052a31341a0e6d553f72b54cd0142d549aa1497c7e3bc56e9f6bcc27286e66e0216e1ba76d1a5ecd2279c40bc8c + checksum: 584068441e4000c7b41c8928274fdcc737bc62f564928c30eb64ec41bbdbac31612f9fedaf490bceab31ec8305e99615166428188ea345d58878394683086fae languageName: node linkType: hard @@ -19886,19 +19916,19 @@ __metadata: languageName: node linkType: hard -"eslint-webpack-plugin@npm:3.2.0": - version: 3.2.0 - resolution: "eslint-webpack-plugin@npm:3.2.0" +"eslint-webpack-plugin@npm:4.0.0": + version: 4.0.0 + resolution: "eslint-webpack-plugin@npm:4.0.0" dependencies: - "@types/eslint": ^7.29.0 || ^8.4.1 - jest-worker: ^28.0.2 + "@types/eslint": ^8.4.10 + jest-worker: ^29.4.1 micromatch: ^4.0.5 normalize-path: ^3.0.0 schema-utils: ^4.0.0 peerDependencies: - eslint: ^7.0.0 || ^8.0.0 + eslint: ^8.0.0 webpack: ^5.0.0 - checksum: 095034c35e773fdb21ec7e597ae1f8a6899679c290db29d8568ca94619e8c7f4971f0f9edccc8a965322ab8af9286c87205985a38f4fdcf17654aee7cd8bb7b5 + checksum: e57a1e6cd23ff72bfdcb96fbf61b8561eb0ed7793ca7922803c4ce9e2aac717168e674d5b08926e6a115126435494adb1f2c9e7cd646b7b56ab3ddf05f8ceec2 languageName: node linkType: hard @@ -22218,7 +22248,7 @@ __metadata: eslint-plugin-lodash: 7.4.0 eslint-plugin-react: 7.32.1 eslint-plugin-react-hooks: 4.6.0 - eslint-webpack-plugin: 3.2.0 + eslint-webpack-plugin: 4.0.0 eventemitter3: 5.0.0 expose-loader: 4.0.0 fast-deep-equal: ^3.1.3 @@ -25738,6 +25768,20 @@ __metadata: languageName: node linkType: hard +"jest-util@npm:^29.5.0": + version: 29.5.0 + resolution: "jest-util@npm:29.5.0" + dependencies: + "@jest/types": ^29.5.0 + "@types/node": "*" + chalk: ^4.0.0 + ci-info: ^3.2.0 + graceful-fs: ^4.2.9 + picomatch: ^2.2.3 + checksum: fd9212950d34d2ecad8c990dda0d8ea59a8a554b0c188b53ea5d6c4a0829a64f2e1d49e6e85e812014933d17426d7136da4785f9cf76fff1799de51b88bc85d3 + languageName: node + linkType: hard + "jest-validate@npm:^27.5.1": version: 27.5.1 resolution: "jest-validate@npm:27.5.1" @@ -25841,17 +25885,6 @@ __metadata: languageName: node linkType: hard -"jest-worker@npm:^28.0.2": - version: 28.1.3 - resolution: "jest-worker@npm:28.1.3" - dependencies: - "@types/node": "*" - merge-stream: ^2.0.0 - supports-color: ^8.0.0 - checksum: e921c9a1b8f0909da9ea07dbf3592f95b653aef3a8bb0cbcd20fc7f9a795a1304adecac31eecb308992c167e8d7e75c522061fec38a5928ace0f9571c90169ca - languageName: node - linkType: hard - "jest-worker@npm:^29.1.2": version: 29.2.1 resolution: "jest-worker@npm:29.2.1" @@ -25876,6 +25909,18 @@ __metadata: languageName: node linkType: hard +"jest-worker@npm:^29.4.1": + version: 29.5.0 + resolution: "jest-worker@npm:29.5.0" + dependencies: + "@types/node": "*" + jest-util: ^29.5.0 + merge-stream: ^2.0.0 + supports-color: ^8.0.0 + checksum: 1151a1ae3602b1ea7c42a8f1efe2b5a7bf927039deaa0827bf978880169899b705744e288f80a63603fb3fc2985e0071234986af7dc2c21c7a64333d8777c7c9 + languageName: node + linkType: hard + "jest@npm:27.5.1": version: 27.5.1 resolution: "jest@npm:27.5.1" From dd12cdec4df751c6ed96f409251db528a2ec7aa9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 7 Mar 2023 11:55:55 +0000 Subject: [PATCH 013/288] Update dependency eslint-plugin-jsdoc to v40 (#64301) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 775f1f71beb..365dde12d77 100644 --- a/package.json +++ b/package.json @@ -191,7 +191,7 @@ "eslint-config-prettier": "8.6.0", "eslint-plugin-import": "^2.26.0", "eslint-plugin-jest": "27.2.1", - "eslint-plugin-jsdoc": "39.8.0", + "eslint-plugin-jsdoc": "40.0.1", "eslint-plugin-jsx-a11y": "6.7.1", "eslint-plugin-lodash": "7.4.0", "eslint-plugin-react": "7.32.1", diff --git a/yarn.lock b/yarn.lock index 8dd4f7256d1..f7308f1f32d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -19728,9 +19728,9 @@ __metadata: languageName: node linkType: hard -"eslint-plugin-jsdoc@npm:39.8.0": - version: 39.8.0 - resolution: "eslint-plugin-jsdoc@npm:39.8.0" +"eslint-plugin-jsdoc@npm:40.0.1": + version: 40.0.1 + resolution: "eslint-plugin-jsdoc@npm:40.0.1" dependencies: "@es-joy/jsdoccomment": ~0.36.1 comment-parser: 1.3.1 @@ -19741,7 +19741,7 @@ __metadata: spdx-expression-parse: ^3.0.1 peerDependencies: eslint: ^7.0.0 || ^8.0.0 - checksum: fe2fec06605c9effe30c3c136f91ba5720d0dec519f5d54dc336f86dc3375793053399e9ca37a33e8eb04cc10571ded800c5babe8b09822922209a0b63751855 + checksum: 19c5de2f8d0dec11c981eccce7255a8cc58e37c8d697c75144913879b16a9ea90995764bfeb00cbfc2b0678e28210cbe62a6b7865b05e756e887fffc892baf61 languageName: node linkType: hard @@ -22243,7 +22243,7 @@ __metadata: eslint-config-prettier: 8.6.0 eslint-plugin-import: ^2.26.0 eslint-plugin-jest: 27.2.1 - eslint-plugin-jsdoc: 39.8.0 + eslint-plugin-jsdoc: 40.0.1 eslint-plugin-jsx-a11y: 6.7.1 eslint-plugin-lodash: 7.4.0 eslint-plugin-react: 7.32.1 From accef84ca57b1fb3e1a52aec42c7eb853b58ec4f Mon Sep 17 00:00:00 2001 From: Matias Chomicki Date: Tue, 7 Mar 2023 13:05:40 +0100 Subject: [PATCH 014/288] Range splitting: Call subscriber.next only when there are new results to report (#64171) --- .../plugins/datasource/loki/querySplitting.ts | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/public/app/plugins/datasource/loki/querySplitting.ts b/public/app/plugins/datasource/loki/querySplitting.ts index 987716eae56..e03bfe5ae13 100644 --- a/public/app/plugins/datasource/loki/querySplitting.ts +++ b/public/app/plugins/datasource/loki/querySplitting.ts @@ -90,7 +90,7 @@ function adjustTargetsFromResponseState(targets: LokiQuery[], response: DataQuer type LokiGroupedRequest = Array<{ request: DataQueryRequest; partition: TimeRange[] }>; export function runGroupedQueries(datasource: LokiDatasource, requests: LokiGroupedRequest) { - let mergedResponse: DataQueryResponse | null; + let mergedResponse: DataQueryResponse = { data: [], state: LoadingState.Streaming }; const totalRequests = Math.max(...requests.map(({ partition }) => partition.length)); let shouldStop = false; @@ -101,23 +101,19 @@ export function runGroupedQueries(datasource: LokiDatasource, requests: LokiGrou return; } - const done = (response: DataQueryResponse) => { - response.state = LoadingState.Done; - subscriber.next(response); + const done = () => { + mergedResponse.state = LoadingState.Done; + subscriber.next(mergedResponse); subscriber.complete(); }; const nextRequest = () => { - mergedResponse = mergedResponse || { data: [] }; const { nextRequestN, nextRequestGroup } = getNextRequestPointers(requests, requestGroup, requestN); if (nextRequestN > 0) { - mergedResponse.state = LoadingState.Streaming; - subscriber.next(mergedResponse); - runNextRequest(subscriber, nextRequestN, nextRequestGroup); return; } - done(mergedResponse); + done(); }; const group = requests[requestGroup]; @@ -125,7 +121,7 @@ export function runGroupedQueries(datasource: LokiDatasource, requests: LokiGrou const range = group.partition[requestN - 1]; const targets = adjustTargetsFromResponseState(group.request.targets, mergedResponse); - if (!targets.length && mergedResponse) { + if (!targets.length) { nextRequest(); return; } @@ -140,6 +136,7 @@ export function runGroupedQueries(datasource: LokiDatasource, requests: LokiGrou mergedResponse = combineResponses(mergedResponse, partialResponse); }, complete: () => { + subscriber.next(mergedResponse); nextRequest(); }, error: (error) => { From 69d3ae8ec11eb98ac3e81695072d47c69c16972b Mon Sep 17 00:00:00 2001 From: Matias Chomicki Date: Tue, 7 Mar 2023 13:09:55 +0100 Subject: [PATCH 015/288] Range Splitting: ignore empty queries from splitting and fix result resetting (#64053) Range splitting: ignore empty queries from splitting --- .../datasource/loki/queryUtils.test.ts | 45 +++++++++++++++++++ .../app/plugins/datasource/loki/queryUtils.ts | 5 ++- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/loki/queryUtils.test.ts b/public/app/plugins/datasource/loki/queryUtils.test.ts index 3fb0d985699..047e1c97a6b 100644 --- a/public/app/plugins/datasource/loki/queryUtils.test.ts +++ b/public/app/plugins/datasource/loki/queryUtils.test.ts @@ -13,6 +13,7 @@ import { obfuscate, combineResponses, cloneQueryResponse, + requestSupportsPartitioning, } from './queryUtils'; import { LokiQuery, LokiQueryType } from './types'; @@ -526,3 +527,47 @@ describe('combineResponses', () => { }); }); }); + +describe('requestSupportsPartitioning', () => { + it('hidden requests are not partitioned', () => { + const requests: LokiQuery[] = [ + { + expr: '{a="b"}', + refId: 'A', + hide: true, + }, + ]; + expect(requestSupportsPartitioning(requests)).toBe(false); + }); + it('special requests are not partitioned', () => { + const requests: LokiQuery[] = [ + { + expr: '{a="b"}', + refId: 'do-not-chunk', + }, + ]; + expect(requestSupportsPartitioning(requests)).toBe(false); + }); + it('empty requests are not partitioned', () => { + const requests: LokiQuery[] = [ + { + expr: '', + refId: 'A', + }, + ]; + expect(requestSupportsPartitioning(requests)).toBe(false); + }); + it('all other requests are partitioned', () => { + const requests: LokiQuery[] = [ + { + expr: '{a="b"}', + refId: 'A', + }, + { + expr: 'count_over_time({a="b"}[1h])', + refId: 'B', + }, + ]; + expect(requestSupportsPartitioning(requests)).toBe(true); + }); +}); diff --git a/public/app/plugins/datasource/loki/queryUtils.ts b/public/app/plugins/datasource/loki/queryUtils.ts index 5e5f2da7c45..a4c2733e0b0 100644 --- a/public/app/plugins/datasource/loki/queryUtils.ts +++ b/public/app/plugins/datasource/loki/queryUtils.ts @@ -305,7 +305,10 @@ export function getStreamSelectorsFromQuery(query: string): string[] { } export function requestSupportsPartitioning(allQueries: LokiQuery[]) { - const queries = allQueries.filter((query) => !query.hide).filter((query) => !query.refId.includes('do-not-chunk')); + const queries = allQueries + .filter((query) => !query.hide) + .filter((query) => !query.refId.includes('do-not-chunk')) + .filter((query) => query.expr); const instantQueries = queries.some((query) => query.queryType === LokiQueryType.Instant); if (instantQueries) { From 8999de431321165fd5a4b4674efc32e2b0ad7490 Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Tue, 7 Mar 2023 14:39:52 +0200 Subject: [PATCH 016/288] Cloudwatch: Use generated TS types (#63166) * Add veneer * Fix queries * Remove redundant type * Fix todos * Sync and fix some todos * Revert unrelated changes * Revert unrelated changes[2] * Revert unrelated changes[3] --- .../kinds/dataquery/types_dataquery_gen.go | 149 +++++++++++++----- .../datasource/cloudwatch/dataquery.cue | 32 ++-- .../datasource/cloudwatch/dataquery.gen.ts | 29 ++-- .../datasource/cloudwatch/expressions.ts | 75 +++------ .../plugins/datasource/cloudwatch/types.ts | 135 ++++------------ 5 files changed, 192 insertions(+), 228 deletions(-) diff --git a/pkg/tsdb/cloudwatch/kinds/dataquery/types_dataquery_gen.go b/pkg/tsdb/cloudwatch/kinds/dataquery/types_dataquery_gen.go index 64aa2b47b89..cdb05267f2d 100644 --- a/pkg/tsdb/cloudwatch/kinds/dataquery/types_dataquery_gen.go +++ b/pkg/tsdb/cloudwatch/kinds/dataquery/types_dataquery_gen.go @@ -54,7 +54,6 @@ const ( // Defines values for CloudWatchMetricsQuerySqlFromPropertyType. const ( CloudWatchMetricsQuerySqlFromPropertyTypeString CloudWatchMetricsQuerySqlFromPropertyType = "string" - CloudWatchMetricsQuerySqlFromPropertyTypeTest CloudWatchMetricsQuerySqlFromPropertyType = "test" ) // Defines values for CloudWatchMetricsQuerySqlGroupByType. @@ -114,6 +113,16 @@ const ( QueryEditorArrayExpressionTypeOr QueryEditorArrayExpressionType = "or" ) +// Defines values for QueryEditorExpressionParametersType. +const ( + QueryEditorExpressionParametersTypeFunctionParameter QueryEditorExpressionParametersType = "functionParameter" +) + +// Defines values for QueryEditorExpressionPropertyType. +const ( + QueryEditorExpressionPropertyTypeString QueryEditorExpressionPropertyType = "string" +) + // Defines values for QueryEditorExpressionType. const ( QueryEditorExpressionTypeAnd QueryEditorExpressionType = "and" @@ -143,7 +152,6 @@ const ( // Defines values for QueryEditorGroupByExpressionPropertyType. const ( QueryEditorGroupByExpressionPropertyTypeString QueryEditorGroupByExpressionPropertyType = "string" - QueryEditorGroupByExpressionPropertyTypeTest QueryEditorGroupByExpressionPropertyType = "test" ) // Defines values for QueryEditorGroupByExpressionType. @@ -154,7 +162,6 @@ const ( // Defines values for QueryEditorOperatorExpressionPropertyType. const ( QueryEditorOperatorExpressionPropertyTypeString QueryEditorOperatorExpressionPropertyType = "string" - QueryEditorOperatorExpressionPropertyTypeTest QueryEditorOperatorExpressionPropertyType = "test" ) // Defines values for QueryEditorOperatorExpressionType. @@ -165,13 +172,11 @@ const ( // Defines values for QueryEditorPropertyType. const ( QueryEditorPropertyTypeString QueryEditorPropertyType = "string" - QueryEditorPropertyTypeTest QueryEditorPropertyType = "test" ) // Defines values for QueryEditorPropertyExpressionPropertyType. const ( QueryEditorPropertyExpressionPropertyTypeString QueryEditorPropertyExpressionPropertyType = "string" - QueryEditorPropertyExpressionPropertyTypeTest QueryEditorPropertyExpressionPropertyType = "test" ) // Defines values for QueryEditorPropertyExpressionType. @@ -187,7 +192,6 @@ const ( // Defines values for SQLExpressionFromPropertyType. const ( SQLExpressionFromPropertyTypeString SQLExpressionFromPropertyType = "string" - SQLExpressionFromPropertyTypeTest SQLExpressionFromPropertyType = "test" ) // Defines values for SQLExpressionGroupByType. @@ -224,18 +228,37 @@ const ( // CloudWatchAnnotationQuery defines model for CloudWatchAnnotationQuery. type CloudWatchAnnotationQuery struct { - AccountId *string `json:"accountId,omitempty"` - ActionPrefix *string `json:"actionPrefix,omitempty"` - AlarmNamePrefix *string `json:"alarmNamePrefix,omitempty"` - Dimensions map[string]interface{} `json:"dimensions,omitempty"` - MatchExact *bool `json:"matchExact,omitempty"` - MetricName *string `json:"metricName,omitempty"` - Namespace string `json:"namespace"` - Period *string `json:"period,omitempty"` - PrefixMatching *bool `json:"prefixMatching,omitempty"` - QueryMode CloudWatchAnnotationQueryQueryMode `json:"queryMode"` - Region string `json:"region"` - Statistic *string `json:"statistic,omitempty"` + AccountId *string `json:"accountId,omitempty"` + ActionPrefix *string `json:"actionPrefix,omitempty"` + AlarmNamePrefix *string `json:"alarmNamePrefix,omitempty"` + + // For mixed data sources the selected datasource is on the query level. + // For non mixed scenarios this is undefined. + // TODO find a better way to do this ^ that's friendly to schema + // TODO this shouldn't be unknown but DataSourceRef | null + Datasource *interface{} `json:"datasource,omitempty"` + Dimensions map[string]interface{} `json:"dimensions,omitempty"` + + // Hide true if query is disabled (ie should not be returned to the dashboard) + Hide *bool `json:"hide,omitempty"` + + // Unique, guid like, string used in explore mode + Key *string `json:"key,omitempty"` + MatchExact *bool `json:"matchExact,omitempty"` + MetricName *string `json:"metricName,omitempty"` + Namespace string `json:"namespace"` + Period *string `json:"period,omitempty"` + PrefixMatching *bool `json:"prefixMatching,omitempty"` + QueryMode CloudWatchAnnotationQueryQueryMode `json:"queryMode"` + + // Specify the query flavor + // TODO make this required and give it a default + QueryType *string `json:"queryType,omitempty"` + + // A - Z + RefId string `json:"refId"` + Region string `json:"region"` + Statistic *string `json:"statistic,omitempty"` // @deprecated use statistic Statistics []string `json:"statistics,omitempty"` @@ -288,15 +311,27 @@ type CloudWatchLogsQueryQueryMode string // CloudWatchMetricsQuery defines model for CloudWatchMetricsQuery. type CloudWatchMetricsQuery struct { - AccountId *string `json:"accountId,omitempty"` - Alias *string `json:"alias,omitempty"` + AccountId *string `json:"accountId,omitempty"` + Alias *string `json:"alias,omitempty"` + + // For mixed data sources the selected datasource is on the query level. + // For non mixed scenarios this is undefined. + // TODO find a better way to do this ^ that's friendly to schema + // TODO this shouldn't be unknown but DataSourceRef | null + Datasource *interface{} `json:"datasource,omitempty"` Dimensions map[string]interface{} `json:"dimensions,omitempty"` // Math expression query Expression *string `json:"expression,omitempty"` + // Hide true if query is disabled (ie should not be returned to the dashboard) + Hide *bool `json:"hide,omitempty"` + // Id common props - Id string `json:"id"` + Id string `json:"id"` + + // Unique, guid like, string used in explore mode + Key *string `json:"key,omitempty"` Label *string `json:"label,omitempty"` MatchExact *bool `json:"matchExact,omitempty"` MetricEditorMode *CloudWatchMetricsQueryMetricEditorMode `json:"metricEditorMode,omitempty"` @@ -305,14 +340,21 @@ type CloudWatchMetricsQuery struct { Namespace string `json:"namespace"` Period *string `json:"period,omitempty"` QueryMode *CloudWatchMetricsQueryQueryMode `json:"queryMode,omitempty"` - Region string `json:"region"` - Sql *struct { + + // Specify the query flavor + // TODO make this required and give it a default + QueryType *string `json:"queryType,omitempty"` + + // A - Z + RefId string `json:"refId"` + Region string `json:"region"` + Sql *struct { From *CloudWatchMetricsQuerySqlFrom `json:"from,omitempty"` GroupBy *struct { - // TODO should be QueryEditorExpression[] | QueryEditorArrayExpression[], extend in veneer + // TS type expressions: QueryEditorExpression[] | QueryEditorArrayExpression[], extended in veneer Expressions interface{} `json:"expressions"` - // TODO this doesn't work + // TODO this doesn't work; temporarily extended in veneer Type CloudWatchMetricsQuerySqlGroupByType `json:"type"` } `json:"groupBy,omitempty"` Limit *int64 `json:"limit,omitempty"` @@ -334,10 +376,10 @@ type CloudWatchMetricsQuery struct { Type CloudWatchMetricsQuerySqlSelectType `json:"type"` } `json:"select,omitempty"` Where *struct { - // TODO should be QueryEditorExpression[] | QueryEditorArrayExpression[], extend in veneer + // TS type expressions: QueryEditorExpression[] | QueryEditorArrayExpression[], extended in veneer Expressions interface{} `json:"expressions"` - // TODO this doesn't work + // TODO this doesn't work; temporarily extended in veneer Type CloudWatchMetricsQuerySqlWhereType `json:"type"` } `json:"where,omitempty"` } `json:"sql,omitempty"` @@ -378,7 +420,7 @@ type CloudWatchMetricsQuerySqlFrom struct { union json.RawMessage } -// TODO this doesn't work +// TODO this doesn't work; temporarily extended in veneer type CloudWatchMetricsQuerySqlGroupByType string // CloudWatchMetricsQuerySqlOrderByParametersType defines model for CloudWatchMetricsQuery.Sql.OrderBy.Parameters.Type. @@ -393,7 +435,7 @@ type CloudWatchMetricsQuerySqlSelectParametersType string // CloudWatchMetricsQuerySqlSelectType defines model for CloudWatchMetricsQuery.Sql.Select.Type. type CloudWatchMetricsQuerySqlSelectType string -// TODO this doesn't work +// TODO this doesn't work; temporarily extended in veneer type CloudWatchMetricsQuerySqlWhereType string // CloudWatchQueryMode defines model for CloudWatchQueryMode. @@ -433,16 +475,43 @@ type MetricStat struct { // QueryEditorArrayExpression defines model for QueryEditorArrayExpression. type QueryEditorArrayExpression struct { - // TODO should be QueryEditorExpression[] | QueryEditorArrayExpression[], extend in veneer + // TS type expressions: QueryEditorExpression[] | QueryEditorArrayExpression[], extended in veneer Expressions interface{} `json:"expressions"` - // TODO this doesn't work + // TODO this doesn't work; temporarily extended in veneer Type QueryEditorArrayExpressionType `json:"type"` } -// TODO this doesn't work +// TODO this doesn't work; temporarily extended in veneer type QueryEditorArrayExpressionType string +// QueryEditorArrayExpression is added in veneer +type QueryEditorExpression struct { + Name *string `json:"name,omitempty"` + + // TS type is operator: QueryEditorOperator, extended in veneer + Operator *struct { + Name *string `json:"name,omitempty"` + Value *interface{} `json:"value,omitempty"` + } `json:"operator,omitempty"` + Parameters []struct { + Name *string `json:"name,omitempty"` + Type QueryEditorExpressionParametersType `json:"type"` + } `json:"parameters,omitempty"` + Property *struct { + Name *string `json:"name,omitempty"` + Type QueryEditorExpressionPropertyType `json:"type"` + } `json:"property,omitempty"` + Type *interface{} `json:"type,omitempty"` + union json.RawMessage +} + +// QueryEditorExpressionParametersType defines model for QueryEditorExpression.Parameters.Type. +type QueryEditorExpressionParametersType string + +// QueryEditorExpressionPropertyType defines model for QueryEditorExpression.Property.Type. +type QueryEditorExpressionPropertyType string + // QueryEditorExpressionType defines model for QueryEditorExpressionType. type QueryEditorExpressionType string @@ -486,7 +555,7 @@ type QueryEditorGroupByExpressionPropertyType string // QueryEditorGroupByExpressionType defines model for QueryEditorGroupByExpression.Type. type QueryEditorGroupByExpressionType string -// TODO , extend in veneer +// TS type is QueryEditorOperator, extended in veneer type QueryEditorOperator struct { Name *string `json:"name,omitempty"` Value *interface{} `json:"value,omitempty"` @@ -494,7 +563,7 @@ type QueryEditorOperator struct { // QueryEditorOperatorExpression defines model for QueryEditorOperatorExpression. type QueryEditorOperatorExpression struct { - // TODO QueryEditorOperator, extend in veneer + // TS type is operator: QueryEditorOperator, extended in veneer Operator struct { Name *string `json:"name,omitempty"` Value *interface{} `json:"value,omitempty"` @@ -540,10 +609,10 @@ type QueryEditorPropertyExpressionType string type SQLExpression struct { From *SQLExpressionFrom `json:"from,omitempty"` GroupBy *struct { - // TODO should be QueryEditorExpression[] | QueryEditorArrayExpression[], extend in veneer + // TS type expressions: QueryEditorExpression[] | QueryEditorArrayExpression[], extended in veneer Expressions interface{} `json:"expressions"` - // TODO this doesn't work + // TODO this doesn't work; temporarily extended in veneer Type SQLExpressionGroupByType `json:"type"` } `json:"groupBy,omitempty"` Limit *int64 `json:"limit,omitempty"` @@ -565,10 +634,10 @@ type SQLExpression struct { Type SQLExpressionSelectType `json:"type"` } `json:"select,omitempty"` Where *struct { - // TODO should be QueryEditorExpression[] | QueryEditorArrayExpression[], extend in veneer + // TS type expressions: QueryEditorExpression[] | QueryEditorArrayExpression[], extended in veneer Expressions interface{} `json:"expressions"` - // TODO this doesn't work + // TODO this doesn't work; temporarily extended in veneer Type SQLExpressionWhereType `json:"type"` } `json:"where,omitempty"` } @@ -594,7 +663,7 @@ type SQLExpressionFrom struct { union json.RawMessage } -// TODO this doesn't work +// TODO this doesn't work; temporarily extended in veneer type SQLExpressionGroupByType string // SQLExpressionOrderByParametersType defines model for SQLExpression.OrderBy.Parameters.Type. @@ -609,5 +678,5 @@ type SQLExpressionSelectParametersType string // SQLExpressionSelectType defines model for SQLExpression.Select.Type. type SQLExpressionSelectType string -// TODO this doesn't work +// TODO this doesn't work; temporarily extended in veneer type SQLExpressionWhereType string diff --git a/public/app/plugins/datasource/cloudwatch/dataquery.cue b/public/app/plugins/datasource/cloudwatch/dataquery.cue index 3c7626da43a..c8a0c2f5cd3 100644 --- a/public/app/plugins/datasource/cloudwatch/dataquery.cue +++ b/public/app/plugins/datasource/cloudwatch/dataquery.cue @@ -46,21 +46,17 @@ composableKinds: DataQuery: { #Dimensions: {[string]: string | [...string]} @cuetsy(kind="type") #CloudWatchMetricsQuery: { - // TODO extend common.DataQuery when the issues with redundant fields is fixed - // common.DataQuery + common.DataQuery #MetricStat queryMode?: #CloudWatchQueryMode metricQueryType?: #MetricQueryType metricEditorMode?: #MetricEditorMode // common props - id: string - + id: string alias?: string label?: string - // Math expression query - expression?: string - + expression?: string sqlExpression?: string sql?: #SQLExpression } @cuetsy(kind="interface") @@ -104,15 +100,15 @@ composableKinds: DataQuery: { #QueryEditorOperatorExpression: { type: #QueryEditorExpressionType & "operator" property: #QueryEditorProperty - // TODO QueryEditorOperator, extend in veneer + // TS type is operator: QueryEditorOperator, extended in veneer operator: #QueryEditorOperator } @cuetsy(kind="interface") - // TODO , extend in veneer + // TS type is QueryEditorOperator, extended in veneer #QueryEditorOperator: { name?: string value?: #QueryEditorOperatorType | [...#QueryEditorOperatorType] - } + } @cuetsy(kind="interface") #QueryEditorOperatorValueType: #QueryEditorOperatorType | [...#QueryEditorOperatorType] @cuetsy(kind="type") #QueryEditorOperatorType: string | bool | int64 @cuetsy(kind="type") @@ -122,15 +118,18 @@ composableKinds: DataQuery: { name?: string } @cuetsy(kind="interface") - #QueryEditorPropertyType: "string" | "test" @cuetsy(kind="enum") + #QueryEditorPropertyType: "string" @cuetsy(kind="enum") #QueryEditorArrayExpression: { - // TODO this doesn't work + // TODO this doesn't work; temporarily extended in veneer type: (#QueryEditorExpressionType & "and") | (#QueryEditorExpressionType & "or") - // TODO should be QueryEditorExpression[] | QueryEditorArrayExpression[], extend in veneer - expressions: _ // TODO modify this in veneer + // TS type expressions: QueryEditorExpression[] | QueryEditorArrayExpression[], extended in veneer + expressions: _ } @cuetsy(kind="interface") + // QueryEditorArrayExpression is added in veneer + #QueryEditorExpression: #QueryEditorPropertyExpression | #QueryEditorGroupByExpression | #QueryEditorFunctionExpression | #QueryEditorFunctionParameterExpression | #QueryEditorOperatorExpression @cuetsy(kind="type") + #CloudWatchLogsQuery: { common.DataQuery queryMode: #CloudWatchQueryMode @@ -153,8 +152,7 @@ composableKinds: DataQuery: { #CloudWatchQueryMode: "Metrics" | "Logs" | "Annotations" @cuetsy(kind="type") #CloudWatchAnnotationQuery: { - // TODO extend common.DataQuery when the issues with redundant fields is fixed - //common.DataQuery + common.DataQuery #MetricStat queryMode: #CloudWatchQueryMode prefixMatching?: bool @@ -162,7 +160,7 @@ composableKinds: DataQuery: { alarmNamePrefix?: string } @cuetsy(kind="interface") - // TODO this doesn't work. Also the type is CloudWatchDefaultQuery = Omit & CloudWatchMetricsQuery; + // TS type is CloudWatchDefaultQuery = Omit & CloudWatchMetricsQuery, declared in veneer // #CloudWatchDefaultQuery: #CloudWatchLogsQuery & #CloudWatchMetricsQuery @cuetsy(kind="type") }, ] diff --git a/public/app/plugins/datasource/cloudwatch/dataquery.gen.ts b/public/app/plugins/datasource/cloudwatch/dataquery.gen.ts index f50fed07594..9a467588bf2 100644 --- a/public/app/plugins/datasource/cloudwatch/dataquery.gen.ts +++ b/public/app/plugins/datasource/cloudwatch/dataquery.gen.ts @@ -33,7 +33,7 @@ export const defaultMetricStat: Partial = { export type Dimensions = Record)>; -export interface CloudWatchMetricsQuery extends MetricStat { +export interface CloudWatchMetricsQuery extends common.DataQuery, MetricStat { alias?: string; /** * Math expression query @@ -110,16 +110,21 @@ export interface QueryEditorGroupByExpression { export interface QueryEditorOperatorExpression { /** - * TODO QueryEditorOperator, extend in veneer + * TS type is operator: QueryEditorOperator, extended in veneer */ - operator: { - name?: string; - value?: (QueryEditorOperatorType | Array); - }; + operator: QueryEditorOperator; property: QueryEditorProperty; type: QueryEditorExpressionType.Operator; } +/** + * TS type is QueryEditorOperator, extended in veneer + */ +export interface QueryEditorOperator { + name?: string; + value?: (QueryEditorOperatorType | Array); +} + export type QueryEditorOperatorValueType = (QueryEditorOperatorType | Array); export type QueryEditorOperatorType = (string | boolean | number); @@ -131,20 +136,24 @@ export interface QueryEditorProperty { export enum QueryEditorPropertyType { String = 'string', - Test = 'test', } export interface QueryEditorArrayExpression { /** - * TODO should be QueryEditorExpression[] | QueryEditorArrayExpression[], extend in veneer + * TS type expressions: QueryEditorExpression[] | QueryEditorArrayExpression[], extended in veneer */ expressions: unknown; /** - * TODO this doesn't work + * TODO this doesn't work; temporarily extended in veneer */ type: QueryEditorExpressionType; } +/** + * QueryEditorArrayExpression is added in veneer + */ +export type QueryEditorExpression = (QueryEditorPropertyExpression | QueryEditorGroupByExpression | QueryEditorFunctionExpression | QueryEditorFunctionParameterExpression | QueryEditorOperatorExpression); + export interface CloudWatchLogsQuery extends common.DataQuery { expression?: string; id: string; @@ -171,7 +180,7 @@ export interface LogGroup { name: string; } -export interface CloudWatchAnnotationQuery extends MetricStat { +export interface CloudWatchAnnotationQuery extends common.DataQuery, MetricStat { actionPrefix?: string; alarmNamePrefix?: string; prefixMatching?: boolean; diff --git a/public/app/plugins/datasource/cloudwatch/expressions.ts b/public/app/plugins/datasource/cloudwatch/expressions.ts index b80ef9b63fc..43fd1a3ff33 100644 --- a/public/app/plugins/datasource/cloudwatch/expressions.ts +++ b/public/app/plugins/datasource/cloudwatch/expressions.ts @@ -1,66 +1,33 @@ -export enum QueryEditorPropertyType { - String = 'string', -} +import { + QueryEditorOperatorExpression as QueryEditorOperatorExpressionBase, + QueryEditorOperator as QueryEditorOperatorBase, + QueryEditorOperatorValueType, + QueryEditorExpressionType, + QueryEditorArrayExpression as QueryEditorArrayExpressionBase, + QueryEditorExpression as QueryEditorExpressionBase, +} from './dataquery.gen'; +export { + QueryEditorPropertyType, + QueryEditorProperty, + QueryEditorPropertyExpression, + QueryEditorGroupByExpression, + QueryEditorFunctionExpression, + QueryEditorFunctionParameterExpression, +} from './dataquery.gen'; -export interface QueryEditorProperty { - type: QueryEditorPropertyType; - name?: string; -} +export { QueryEditorExpressionType }; -export type QueryEditorOperatorType = string | boolean | number; -type QueryEditorOperatorValueType = QueryEditorOperatorType | QueryEditorOperatorType[]; - -export interface QueryEditorOperator { - name?: string; +export interface QueryEditorOperator extends QueryEditorOperatorBase { value?: T; } -export interface QueryEditorOperatorExpression { - type: QueryEditorExpressionType.Operator; - property: QueryEditorProperty; +export interface QueryEditorOperatorExpression extends QueryEditorOperatorExpressionBase { operator: QueryEditorOperator; } -export interface QueryEditorArrayExpression { +export interface QueryEditorArrayExpression extends QueryEditorArrayExpressionBase { type: QueryEditorExpressionType.And | QueryEditorExpressionType.Or; expressions: QueryEditorExpression[] | QueryEditorArrayExpression[]; } -export interface QueryEditorPropertyExpression { - type: QueryEditorExpressionType.Property; - property: QueryEditorProperty; -} - -export enum QueryEditorExpressionType { - Property = 'property', - Operator = 'operator', - Or = 'or', - And = 'and', - GroupBy = 'groupBy', - Function = 'function', - FunctionParameter = 'functionParameter', -} - -export type QueryEditorExpression = - | QueryEditorArrayExpression - | QueryEditorPropertyExpression - | QueryEditorGroupByExpression - | QueryEditorFunctionExpression - | QueryEditorFunctionParameterExpression - | QueryEditorOperatorExpression; - -export interface QueryEditorGroupByExpression { - type: QueryEditorExpressionType.GroupBy; - property: QueryEditorProperty; -} - -export interface QueryEditorFunctionExpression { - type: QueryEditorExpressionType.Function; - name?: string; - parameters?: QueryEditorFunctionParameterExpression[]; -} - -export interface QueryEditorFunctionParameterExpression { - type: QueryEditorExpressionType.FunctionParameter; - name?: string; -} +export type QueryEditorExpression = QueryEditorArrayExpression | QueryEditorExpressionBase; diff --git a/public/app/plugins/datasource/cloudwatch/types.ts b/public/app/plugins/datasource/cloudwatch/types.ts index a9113aa445c..3e9e8098c38 100644 --- a/public/app/plugins/datasource/cloudwatch/types.ts +++ b/public/app/plugins/datasource/cloudwatch/types.ts @@ -1,81 +1,37 @@ import { AwsAuthDataSourceJsonData, AwsAuthDataSourceSecureJsonData } from '@grafana/aws-sdk'; -import { DataFrame, DataQuery, DataSourceRef, SelectableValue } from '@grafana/data'; +import { DataFrame, DataSourceRef } from '@grafana/data'; +import { DataQuery } from '@grafana/schema'; -import { - QueryEditorArrayExpression, - QueryEditorFunctionExpression, - QueryEditorPropertyExpression, -} from './expressions'; +import * as raw from './dataquery.gen'; +import { QueryEditorArrayExpression } from './expressions'; -export interface Dimensions { - [key: string]: string | string[]; +export * from './dataquery.gen'; + +// QueryEditorArrayExpression has a recursive property, so cannot be defined in cue +export interface SQLExpression extends raw.SQLExpression { + where?: QueryEditorArrayExpression; + groupBy?: QueryEditorArrayExpression; } +export type CloudWatchQuery = + | CloudWatchMetricsQuery + | raw.CloudWatchLogsQuery + | raw.CloudWatchAnnotationQuery + | CloudWatchDefaultQuery; + +export interface CloudWatchMetricsQuery extends raw.CloudWatchMetricsQuery { + sql?: SQLExpression; +} + +// We want to allow setting defaults for both Logs and Metrics queries +export type CloudWatchDefaultQuery = Omit & CloudWatchMetricsQuery; + export interface MultiFilters { [key: string]: string[]; } -export type CloudWatchQueryMode = 'Metrics' | 'Logs' | 'Annotations'; - -export enum MetricQueryType { - 'Search', - 'Query', -} - -export enum MetricEditorMode { - 'Builder', - 'Code', -} - export type Direction = 'ASC' | 'DESC'; -export interface SQLExpression { - select?: QueryEditorFunctionExpression; - from?: QueryEditorPropertyExpression | QueryEditorFunctionExpression; - where?: QueryEditorArrayExpression; - groupBy?: QueryEditorArrayExpression; - orderBy?: QueryEditorFunctionExpression; - orderByDirection?: string; - limit?: number; -} - -export interface CloudWatchMetricsQuery extends MetricStat, DataQuery { - queryMode?: CloudWatchQueryMode; - metricQueryType?: MetricQueryType; - metricEditorMode?: MetricEditorMode; - - //common props - id: string; - - alias?: string; - label?: string; - - // Math expression query - expression?: string; - - sqlExpression?: string; - sql?: SQLExpression; -} - -export interface MetricStat { - region: string; - namespace: string; - metricName?: string; - dimensions?: Dimensions; - matchExact?: boolean; - period?: string; - accountId?: string; - statistic?: string; - /** - * @deprecated use statistic - */ - statistics?: string[]; -} - -export interface CloudWatchMathExpressionQuery extends DataQuery { - expression: string; -} - export type LogAction = 'GetQueryResults' | 'GetLogEvents' | 'StartQuery' | 'StopQuery'; export enum CloudWatchLogsQueryStatus { @@ -87,34 +43,6 @@ export enum CloudWatchLogsQueryStatus { Timeout = 'Timeout', } -export interface CloudWatchLogsQuery extends DataQuery { - queryMode: CloudWatchQueryMode; - id: string; - region: string; - expression?: string; - statsGroups?: string[]; - logGroups?: LogGroup[]; - /* deprecated, use logGroups instead */ - logGroupNames?: string[]; -} -// We want to allow setting defaults for both Logs and Metrics queries -export type CloudWatchDefaultQuery = Omit & CloudWatchMetricsQuery; - -export type CloudWatchQuery = - | CloudWatchMetricsQuery - | CloudWatchLogsQuery - | CloudWatchAnnotationQuery - | CloudWatchDefaultQuery; - -export interface CloudWatchAnnotationQuery extends MetricStat, DataQuery { - queryMode: CloudWatchQueryMode; - prefixMatching?: boolean; - actionPrefix?: string; - alarmNamePrefix?: string; -} - -export type SelectableStrings = Array>; - export interface CloudWatchJsonData extends AwsAuthDataSourceJsonData { timeField?: string; database?: string; @@ -125,7 +53,7 @@ export interface CloudWatchJsonData extends AwsAuthDataSourceJsonData { // Used to create links if logs contain traceId. tracingDatasourceUid?: string; - logGroups?: LogGroup[]; + logGroups?: raw.LogGroup[]; /** * @deprecated use logGroups */ @@ -227,7 +155,7 @@ export interface StartQueryRequest { * The list of log groups to be queried. You can include up to 20 log groups. A StartQuery operation must include a logGroupNames or a logGroupName parameter, but not both. */ logGroupNames?: string[] /* not quite deprecated yet, but will be soon */; - logGroups?: LogGroup[]; + logGroups?: raw.LogGroup[]; /** * The query string to use. For more information, see CloudWatch Logs Insights Query Syntax. */ @@ -297,7 +225,7 @@ export interface VariableQuery extends DataQuery { region: string; metricName: string; dimensionKey: string; - dimensionFilters?: Dimensions; + dimensionFilters?: raw.Dimensions; ec2Filters?: MultiFilters; instanceID: string; attributeName: string; @@ -307,13 +235,13 @@ export interface VariableQuery extends DataQuery { accountId?: string; } -export interface LegacyAnnotationQuery extends MetricStat, DataQuery { +export interface LegacyAnnotationQuery extends raw.MetricStat, DataQuery { actionPrefix: string; alarmNamePrefix: string; alias: string; builtIn: number; datasource: any; - dimensions: Dimensions; + dimensions: raw.Dimensions; enable: boolean; expression: string; hide: boolean; @@ -336,10 +264,3 @@ export interface LegacyAnnotationQuery extends MetricStat, DataQuery { }; type: string; } - -export interface LogGroup { - arn: string; - name: string; - accountId?: string; - accountLabel?: string; -} From ede3e9e5c40aa4fc0e04d1cdfe18f6993829ee92 Mon Sep 17 00:00:00 2001 From: Matias Chomicki Date: Tue, 7 Mar 2023 13:44:13 +0100 Subject: [PATCH 017/288] Range Splitting: Process instant queries as an independent query group (#64049) * Query splitting: enable instant queries * Range splitting: send instant queries as another request group * Range splitting: increase grouped splitted requests stability We were defaulting to the `0` index as the first group for the next request batch, but there was no guarantee that the group `0` had a `.partition` entry for `requestN-1`. Now we find the first defined and use that index as the next starting group. * Range splitting: update unit test --- .../datasource/loki/querySplitting.test.ts | 29 ++++++++++++++++++- .../plugins/datasource/loki/querySplitting.ts | 18 ++++++++---- .../app/plugins/datasource/loki/queryUtils.ts | 5 ---- 3 files changed, 41 insertions(+), 11 deletions(-) diff --git a/public/app/plugins/datasource/loki/querySplitting.test.ts b/public/app/plugins/datasource/loki/querySplitting.test.ts index b4c41658680..717024dca61 100644 --- a/public/app/plugins/datasource/loki/querySplitting.test.ts +++ b/public/app/plugins/datasource/loki/querySplitting.test.ts @@ -9,7 +9,7 @@ import * as logsTimeSplit from './logsTimeSplit'; import * as metricTimeSplit from './metricTimeSplit'; import { createLokiDatasource, getMockFrames } from './mocks'; import { runPartitionedQueries } from './querySplitting'; -import { LokiQuery } from './types'; +import { LokiQuery, LokiQueryType } from './types'; describe('runPartitionedQueries()', () => { let datasource: LokiDatasource; @@ -145,6 +145,19 @@ describe('runPartitionedQueries()', () => { expect(datasource.runQuery).toHaveBeenCalledTimes(3); }); }); + test('Groups instant queries', async () => { + const request = getQueryOptions({ + targets: [ + { expr: 'count_over_time({a="b"}[1m])', refId: 'A', queryType: LokiQueryType.Instant }, + { expr: 'count_over_time({c="d"}[1m])', refId: 'B', queryType: LokiQueryType.Instant }, + ], + range, + }); + await expect(runPartitionedQueries(datasource, request)).toEmitValuesWith(() => { + // Instant queries are omitted from splitting + expect(datasource.runQuery).toHaveBeenCalledTimes(1); + }); + }); test('Respects maxLines of logs queries', async () => { const { logFrameA } = getMockFrames(); const request = getQueryOptions({ @@ -162,5 +175,19 @@ describe('runPartitionedQueries()', () => { expect(datasource.runQuery).toHaveBeenCalledTimes(4); }); }); + test('Groups multiple queries into logs, queries, and instant', async () => { + const request = getQueryOptions({ + targets: [ + { expr: 'count_over_time({a="b"}[1m])', refId: 'A', queryType: LokiQueryType.Instant }, + { expr: '{c="d"}', refId: 'B' }, + { expr: 'count_over_time({c="d"}[1m])', refId: 'C' }, + ], + range, + }); + await expect(runPartitionedQueries(datasource, request)).toEmitValuesWith(() => { + // 3 days, 3 chunks, 3x Logs + 3x Metric + 1x Instant, 7 requests. + expect(datasource.runQuery).toHaveBeenCalledTimes(7); + }); + }); }); }); diff --git a/public/app/plugins/datasource/loki/querySplitting.ts b/public/app/plugins/datasource/loki/querySplitting.ts index e03bfe5ae13..b3205d846d4 100644 --- a/public/app/plugins/datasource/loki/querySplitting.ts +++ b/public/app/plugins/datasource/loki/querySplitting.ts @@ -8,7 +8,7 @@ import { LokiDatasource } from './datasource'; import { getRangeChunks as getLogsRangeChunks } from './logsTimeSplit'; import { getRangeChunks as getMetricRangeChunks } from './metricTimeSplit'; import { combineResponses, isLogsQuery } from './queryUtils'; -import { LokiQuery } from './types'; +import { LokiQuery, LokiQueryType } from './types'; /** * Purposely exposing it to support doing tests without needing to update the repo. @@ -109,7 +109,7 @@ export function runGroupedQueries(datasource: LokiDatasource, requests: LokiGrou const nextRequest = () => { const { nextRequestN, nextRequestGroup } = getNextRequestPointers(requests, requestGroup, requestN); - if (nextRequestN > 0) { + if (nextRequestN > 0 && nextRequestGroup >= 0) { runNextRequest(subscriber, nextRequestN, nextRequestGroup); return; } @@ -167,16 +167,18 @@ function getNextRequestPointers(requests: LokiGroupedRequest, requestGroup: numb }; } return { - nextRequestGroup: 0, + // Find the first group where `[requestN - 1]` is defined + nextRequestGroup: requests.findIndex((group) => group?.partition[requestN - 1] !== undefined), nextRequestN: requestN - 1, }; } export function runPartitionedQueries(datasource: LokiDatasource, request: DataQueryRequest) { const queries = request.targets.filter((query) => !query.hide); - const [logQueries, metricQueries] = partition(queries, (query) => isLogsQuery(query.expr)); + const [instantQueries, normalQueries] = partition(queries, (query) => query.queryType === LokiQueryType.Instant); + const [logQueries, metricQueries] = partition(normalQueries, (query) => isLogsQuery(query.expr)); - const requests = []; + const requests: LokiGroupedRequest = []; if (logQueries.length) { requests.push({ request: { ...request, targets: logQueries }, @@ -189,5 +191,11 @@ export function runPartitionedQueries(datasource: LokiDatasource, request: DataQ partition: partitionTimeRange(false, request.range, request.intervalMs, metricQueries[0].resolution ?? 1), }); } + if (instantQueries.length) { + requests.push({ + request: { ...request, targets: instantQueries }, + partition: [request.range], + }); + } return runGroupedQueries(datasource, requests); } diff --git a/public/app/plugins/datasource/loki/queryUtils.ts b/public/app/plugins/datasource/loki/queryUtils.ts index a4c2733e0b0..c63183f0329 100644 --- a/public/app/plugins/datasource/loki/queryUtils.ts +++ b/public/app/plugins/datasource/loki/queryUtils.ts @@ -310,11 +310,6 @@ export function requestSupportsPartitioning(allQueries: LokiQuery[]) { .filter((query) => !query.refId.includes('do-not-chunk')) .filter((query) => query.expr); - const instantQueries = queries.some((query) => query.queryType === LokiQueryType.Instant); - if (instantQueries) { - return false; - } - return queries.length > 0; } From 3bd7217aa2149348deee1a83c548526760c2c90b Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Tue, 7 Mar 2023 13:56:34 +0100 Subject: [PATCH 018/288] Elasticsearch: Remove _source field when processing raw data on backend (#64119) * Elasticsearch: Remove _source field when processing raw data on backend * Update snapshot test --- pkg/tsdb/elasticsearch/response_parser.go | 1 - .../response_parser_frontend_test.go | 47 ------ .../elasticsearch/response_parser_test.go | 58 ++++++- .../testdata_response/raw_data.a.golden.jsonc | 155 ++---------------- 4 files changed, 64 insertions(+), 197 deletions(-) diff --git a/pkg/tsdb/elasticsearch/response_parser.go b/pkg/tsdb/elasticsearch/response_parser.go index ef44bd84ca3..a1952361c6a 100644 --- a/pkg/tsdb/elasticsearch/response_parser.go +++ b/pkg/tsdb/elasticsearch/response_parser.go @@ -154,7 +154,6 @@ func processRawDataResponse(res *es.SearchResponse, target *Query, configuredFie "_index": hit["_index"], "sort": hit["sort"], "highlight": hit["highlight"], - "_source": flattened, } for k, v := range flattened { diff --git a/pkg/tsdb/elasticsearch/response_parser_frontend_test.go b/pkg/tsdb/elasticsearch/response_parser_frontend_test.go index 285c8755898..9b831911471 100644 --- a/pkg/tsdb/elasticsearch/response_parser_frontend_test.go +++ b/pkg/tsdb/elasticsearch/response_parser_frontend_test.go @@ -1356,53 +1356,6 @@ func TestTwoBucketScripts(t *testing.T) { requireFloatAt(t, 48.0, fields[4], 1) } -func TestRawData(t *testing.T) { - query := []byte(` - [ - { - "refId": "A", - "metrics": [{ "type": "raw_data", "id": "1" }], - "bucketAggs": [] - } - ] - `) - - response := []byte(` - { - "responses": [ - { - "hits": { - "total": { "relation": "eq", "value": 1 }, - "hits": [ - { - "_id": "1", - "_type": "_doc", - "_index": "index", - "_source": { "sourceProp": "asd" } - } - ] - } - } - ] - } - `) - - result, err := queryDataTest(query, response) - require.NoError(t, err) - - require.Len(t, result.response.Responses, 1) - // frames := result.response.Responses["A"].Frames - // require.True(t, len(frames) > 0) // FIXME - - // for _, field := range frames[0].Fields { - // trueValue := true - // filterableConfig := data.FieldConfig{Filterable: &trueValue} - - // // we need to test that the only changed setting is `filterable` - // require.Equal(t, filterableConfig, *field.Config) // FIXME - // } -} - func TestLogsAndCount(t *testing.T) { query := []byte(` [ diff --git a/pkg/tsdb/elasticsearch/response_parser_test.go b/pkg/tsdb/elasticsearch/response_parser_test.go index d2d58315f85..94e3fefb7ea 100644 --- a/pkg/tsdb/elasticsearch/response_parser_test.go +++ b/pkg/tsdb/elasticsearch/response_parser_test.go @@ -1348,7 +1348,7 @@ func TestResponseParser(t *testing.T) { require.Len(t, dataframes, 1) frame := dataframes[0] - require.Equal(t, 16, len(frame.Fields)) + require.Equal(t, 15, len(frame.Fields)) // Fields have the correct length require.Equal(t, 2, frame.Fields[0].Len()) // First field is timeField @@ -1356,14 +1356,60 @@ func TestResponseParser(t *testing.T) { // Correctly uses string types require.Equal(t, data.FieldTypeNullableString, frame.Fields[1].Type()) // Correctly detects float64 types - require.Equal(t, data.FieldTypeNullableFloat64, frame.Fields[6].Type()) + require.Equal(t, data.FieldTypeNullableFloat64, frame.Fields[5].Type()) // Correctly detects json types - require.Equal(t, data.FieldTypeNullableJSON, frame.Fields[7].Type()) + require.Equal(t, data.FieldTypeNullableJSON, frame.Fields[6].Type()) // Correctly flattens fields - require.Equal(t, "nested.field.double_nested", frame.Fields[12].Name) - require.Equal(t, data.FieldTypeNullableString, frame.Fields[12].Type()) + require.Equal(t, "nested.field.double_nested", frame.Fields[11].Name) + require.Equal(t, data.FieldTypeNullableString, frame.Fields[11].Type()) // Correctly detects type even if first value is null - require.Equal(t, data.FieldTypeNullableString, frame.Fields[15].Type()) + require.Equal(t, data.FieldTypeNullableString, frame.Fields[14].Type()) + }) + t.Run("Raw data query filterable fields", func(t *testing.T) { + query := []byte(` + [ + { + "refId": "A", + "metrics": [{ "type": "raw_data", "id": "1" }], + "bucketAggs": [] + } + ] + `) + + response := []byte(` + { + "responses": [ + { + "hits": { + "total": { "relation": "eq", "value": 1 }, + "hits": [ + { + "_id": "1", + "_type": "_doc", + "_index": "index", + "_source": { "sourceProp": "asd" } + } + ] + } + } + ] + } + `) + + result, err := queryDataTest(query, response) + require.NoError(t, err) + + require.Len(t, result.response.Responses, 1) + frames := result.response.Responses["A"].Frames + require.True(t, len(frames) > 0) // FIXME + + for _, field := range frames[0].Fields { + trueValue := true + filterableConfig := data.FieldConfig{Filterable: &trueValue} + + // we need to test that the only changed setting is `filterable` + require.Equal(t, filterableConfig, *field.Config) // FIXME + } }) }) diff --git a/pkg/tsdb/elasticsearch/testdata_response/raw_data.a.golden.jsonc b/pkg/tsdb/elasticsearch/testdata_response/raw_data.a.golden.jsonc index 423d4771def..4e4f6774450 100644 --- a/pkg/tsdb/elasticsearch/testdata_response/raw_data.a.golden.jsonc +++ b/pkg/tsdb/elasticsearch/testdata_response/raw_data.a.golden.jsonc @@ -2,18 +2,18 @@ // // Frame[0] // Name: -// Dimensions: 17 Fields by 5 Rows -// +--------------------------+----------------------+-----------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+--------------------------+-----------------+------------------+--------------------+--------------------------+---------------+-----------------+-----------------+---------------------------+-----------------------------------------+------------------------------------+---------------------------------------------------------------------------------+--------------------------+ -// | Name: @timestamp | Name: _id | Name: _index | Name: _source | Name: _type | Name: abc | Name: counter | Name: float | Name: highlight | Name: is_true | Name: label | Name: level | Name: line | Name: location | Name: nested_field.internal.nested | Name: shapes | Name: sort | -// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | -// | Type: []*string | Type: []*string | Type: []*string | Type: []*json.RawMessage | Type: []*json.RawMessage | Type: []*string | Type: []*float64 | Type: []*float64 | Type: []*json.RawMessage | Type: []*bool | Type: []*string | Type: []*string | Type: []*string | Type: []*string | Type: []*string | Type: []*json.RawMessage | Type: []*json.RawMessage | -// +--------------------------+----------------------+-----------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+--------------------------+-----------------+------------------+--------------------+--------------------------+---------------+-----------------+-----------------+---------------------------+-----------------------------------------+------------------------------------+---------------------------------------------------------------------------------+--------------------------+ -// | 2023-02-09T14:40:01.475Z | g2aeNoYB7vaC3bq-ezfK | logs-2023.02.09 | {"@timestamp":"2023-02-09T14:40:01.475Z","abc":null,"counter":81,"float":10.911972180833306,"is_true":true,"label":"val3","level":"info","line":"log text [106619125]","location":"-42.73465234425797, -14.097854057104112","nested_field.internal.nested":"value1","shapes":[{"type":"triangle"},{"type":"triangle"},{"type":"triangle"},{"type":"square"}]} | null | null | 81 | 10.911972180833306 | null | true | val3 | info | log text [106619125] | -42.73465234425797, -14.097854057104112 | value1 | [{"type":"triangle"},{"type":"triangle"},{"type":"triangle"},{"type":"square"}] | [1675953601475,4] | -// | 2023-02-09T14:40:00.513Z | gmaeNoYB7vaC3bq-eDcN | logs-2023.02.09 | {"@timestamp":"2023-02-09T14:40:00.513Z","abc":null,"counter":80,"float":62.94120607636795,"is_true":false,"label":"val3","level":"error","line":"log text with [781660944]","location":"42.07571917624318, 15.95725088484611","nested_field.internal.nested":"value2","shapes":[{"type":"triangle"},{"type":"square"}]} | null | null | 80 | 62.94120607636795 | null | false | val3 | error | log text with [781660944] | 42.07571917624318, 15.95725088484611 | value2 | [{"type":"triangle"},{"type":"square"}] | [1675953600513,7] | -// | 2023-02-09T14:39:59.556Z | gWaeNoYB7vaC3bq-dDdL | logs-2023.02.09 | {"@timestamp":"2023-02-09T14:39:59.556Z","abc":"def","counter":79,"float":53.323706427230455,"is_true":true,"label":"val1","level":"info","line":"log text [894867430]","location":"-38.27341566189766, -23.66739642570781","nested_field.internal.nested":"value3","shapes":[{"type":"triangle"},{"type":"square"}]} | null | def | 79 | 53.323706427230455 | null | true | val1 | info | log text [894867430] | -38.27341566189766, -23.66739642570781 | value3 | [{"type":"triangle"},{"type":"square"}] | [1675953599556,10] | -// | 2023-02-09T14:39:58.608Z | gGaeNoYB7vaC3bq-cDeY | logs-2023.02.09 | {"@timestamp":"2023-02-09T14:39:58.608Z","abc":"def","counter":78,"float":82.72012623471589,"is_true":false,"label":"val1","level":"info","line":"log text [478598889]","location":"12.373240290451287, 43.265493464362024","nested_field.internal.nested":"value4","shapes":[{"type":"triangle"},{"type":"triangle"},{"type":"triangle"},{"type":"square"}]} | null | def | 78 | 82.72012623471589 | null | false | val1 | info | log text [478598889] | 12.373240290451287, 43.265493464362024 | value4 | [{"type":"triangle"},{"type":"triangle"},{"type":"triangle"},{"type":"square"}] | [1675953598608,15] | -// | 2023-02-09T14:39:57.665Z | f2aeNoYB7vaC3bq-bDf7 | logs-2023.02.09 | {"@timestamp":"2023-02-09T14:39:57.665Z","abc":"def","counter":77,"float":35.05784443331803,"is_true":false,"label":"val3","level":"info","line":"log text [526995818]","location":"-31.524344042228194, -32.11254790120572","nested_field.internal.nested":"value5","shapes":[{"type":"triangle"},{"type":"triangle"},{"type":"triangle"},{"type":"square"}]} | null | def | 77 | 35.05784443331803 | null | false | val3 | info | log text [526995818] | -31.524344042228194, -32.11254790120572 | value5 | [{"type":"triangle"},{"type":"triangle"},{"type":"triangle"},{"type":"square"}] | [1675953597665,20] | -// +--------------------------+----------------------+-----------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+--------------------------+-----------------+------------------+--------------------+--------------------------+---------------+-----------------+-----------------+---------------------------+-----------------------------------------+------------------------------------+---------------------------------------------------------------------------------+--------------------------+ +// Dimensions: 16 Fields by 5 Rows +// +--------------------------+----------------------+-----------------+--------------------------+-----------------+------------------+--------------------+--------------------------+---------------+-----------------+-----------------+---------------------------+-----------------------------------------+------------------------------------+---------------------------------------------------------------------------------+--------------------------+ +// | Name: @timestamp | Name: _id | Name: _index | Name: _type | Name: abc | Name: counter | Name: float | Name: highlight | Name: is_true | Name: label | Name: level | Name: line | Name: location | Name: nested_field.internal.nested | Name: shapes | Name: sort | +// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | +// | Type: []*string | Type: []*string | Type: []*string | Type: []*json.RawMessage | Type: []*string | Type: []*float64 | Type: []*float64 | Type: []*json.RawMessage | Type: []*bool | Type: []*string | Type: []*string | Type: []*string | Type: []*string | Type: []*string | Type: []*json.RawMessage | Type: []*json.RawMessage | +// +--------------------------+----------------------+-----------------+--------------------------+-----------------+------------------+--------------------+--------------------------+---------------+-----------------+-----------------+---------------------------+-----------------------------------------+------------------------------------+---------------------------------------------------------------------------------+--------------------------+ +// | 2023-02-09T14:40:01.475Z | g2aeNoYB7vaC3bq-ezfK | logs-2023.02.09 | null | null | 81 | 10.911972180833306 | null | true | val3 | info | log text [106619125] | -42.73465234425797, -14.097854057104112 | value1 | [{"type":"triangle"},{"type":"triangle"},{"type":"triangle"},{"type":"square"}] | [1675953601475,4] | +// | 2023-02-09T14:40:00.513Z | gmaeNoYB7vaC3bq-eDcN | logs-2023.02.09 | null | null | 80 | 62.94120607636795 | null | false | val3 | error | log text with [781660944] | 42.07571917624318, 15.95725088484611 | value2 | [{"type":"triangle"},{"type":"square"}] | [1675953600513,7] | +// | 2023-02-09T14:39:59.556Z | gWaeNoYB7vaC3bq-dDdL | logs-2023.02.09 | null | def | 79 | 53.323706427230455 | null | true | val1 | info | log text [894867430] | -38.27341566189766, -23.66739642570781 | value3 | [{"type":"triangle"},{"type":"square"}] | [1675953599556,10] | +// | 2023-02-09T14:39:58.608Z | gGaeNoYB7vaC3bq-cDeY | logs-2023.02.09 | null | def | 78 | 82.72012623471589 | null | false | val1 | info | log text [478598889] | 12.373240290451287, 43.265493464362024 | value4 | [{"type":"triangle"},{"type":"triangle"},{"type":"triangle"},{"type":"square"}] | [1675953598608,15] | +// | 2023-02-09T14:39:57.665Z | f2aeNoYB7vaC3bq-bDf7 | logs-2023.02.09 | null | def | 77 | 35.05784443331803 | null | false | val3 | info | log text [526995818] | -31.524344042228194, -32.11254790120572 | value5 | [{"type":"triangle"},{"type":"triangle"},{"type":"triangle"},{"type":"square"}] | [1675953597665,20] | +// +--------------------------+----------------------+-----------------+--------------------------+-----------------+------------------+--------------------+--------------------------+---------------+-----------------+-----------------+---------------------------+-----------------------------------------+------------------------------------+---------------------------------------------------------------------------------+--------------------------+ // // // 🌟 This was machine generated. Do not edit. 🌟 @@ -56,17 +56,6 @@ "filterable": true } }, - { - "name": "_source", - "type": "other", - "typeInfo": { - "frame": "json.RawMessage", - "nullable": true - }, - "config": { - "filterable": true - } - }, { "name": "_type", "type": "other", @@ -235,126 +224,6 @@ "logs-2023.02.09", "logs-2023.02.09" ], - [ - { - "@timestamp": "2023-02-09T14:40:01.475Z", - "abc": null, - "counter": 81, - "float": 10.911972180833306, - "is_true": true, - "label": "val3", - "level": "info", - "line": "log text [106619125]", - "location": "-42.73465234425797, -14.097854057104112", - "nested_field.internal.nested": "value1", - "shapes": [ - { - "type": "triangle" - }, - { - "type": "triangle" - }, - { - "type": "triangle" - }, - { - "type": "square" - } - ] - }, - { - "@timestamp": "2023-02-09T14:40:00.513Z", - "abc": null, - "counter": 80, - "float": 62.94120607636795, - "is_true": false, - "label": "val3", - "level": "error", - "line": "log text with [781660944]", - "location": "42.07571917624318, 15.95725088484611", - "nested_field.internal.nested": "value2", - "shapes": [ - { - "type": "triangle" - }, - { - "type": "square" - } - ] - }, - { - "@timestamp": "2023-02-09T14:39:59.556Z", - "abc": "def", - "counter": 79, - "float": 53.323706427230455, - "is_true": true, - "label": "val1", - "level": "info", - "line": "log text [894867430]", - "location": "-38.27341566189766, -23.66739642570781", - "nested_field.internal.nested": "value3", - "shapes": [ - { - "type": "triangle" - }, - { - "type": "square" - } - ] - }, - { - "@timestamp": "2023-02-09T14:39:58.608Z", - "abc": "def", - "counter": 78, - "float": 82.72012623471589, - "is_true": false, - "label": "val1", - "level": "info", - "line": "log text [478598889]", - "location": "12.373240290451287, 43.265493464362024", - "nested_field.internal.nested": "value4", - "shapes": [ - { - "type": "triangle" - }, - { - "type": "triangle" - }, - { - "type": "triangle" - }, - { - "type": "square" - } - ] - }, - { - "@timestamp": "2023-02-09T14:39:57.665Z", - "abc": "def", - "counter": 77, - "float": 35.05784443331803, - "is_true": false, - "label": "val3", - "level": "info", - "line": "log text [526995818]", - "location": "-31.524344042228194, -32.11254790120572", - "nested_field.internal.nested": "value5", - "shapes": [ - { - "type": "triangle" - }, - { - "type": "triangle" - }, - { - "type": "triangle" - }, - { - "type": "square" - } - ] - } - ], [ null, null, From 07ab12c07d1e53fe7563807774320d50f1e29aab Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Tue, 7 Mar 2023 13:57:24 +0100 Subject: [PATCH 019/288] Elasticsearch: Rename time series query to es data query (#64225) --- .../{time_series_query.go => data_query.go} | 10 +- ...eries_query_test.go => data_query_test.go} | 132 +++++++++--------- pkg/tsdb/elasticsearch/elasticsearch.go | 2 +- 3 files changed, 72 insertions(+), 72 deletions(-) rename pkg/tsdb/elasticsearch/{time_series_query.go => data_query.go} (97%) rename pkg/tsdb/elasticsearch/{time_series_query_test.go => data_query_test.go} (94%) diff --git a/pkg/tsdb/elasticsearch/time_series_query.go b/pkg/tsdb/elasticsearch/data_query.go similarity index 97% rename from pkg/tsdb/elasticsearch/time_series_query.go rename to pkg/tsdb/elasticsearch/data_query.go index 95282891d82..6ecf9f00809 100644 --- a/pkg/tsdb/elasticsearch/time_series_query.go +++ b/pkg/tsdb/elasticsearch/data_query.go @@ -16,19 +16,19 @@ const ( defaultSize = 500 ) -type timeSeriesQuery struct { +type elasticsearchDataQuery struct { client es.Client dataQueries []backend.DataQuery } -var newTimeSeriesQuery = func(client es.Client, dataQuery []backend.DataQuery) *timeSeriesQuery { - return &timeSeriesQuery{ +var newElasticsearchDataQuery = func(client es.Client, dataQuery []backend.DataQuery) *elasticsearchDataQuery { + return &elasticsearchDataQuery{ client: client, dataQueries: dataQuery, } } -func (e *timeSeriesQuery) execute() (*backend.QueryDataResponse, error) { +func (e *elasticsearchDataQuery) execute() (*backend.QueryDataResponse, error) { queries, err := parseQuery(e.dataQueries) if err != nil { return &backend.QueryDataResponse{}, err @@ -57,7 +57,7 @@ func (e *timeSeriesQuery) execute() (*backend.QueryDataResponse, error) { return parseResponse(res.Responses, queries, e.client.GetConfiguredFields()) } -func (e *timeSeriesQuery) processQuery(q *Query, ms *es.MultiSearchRequestBuilder, from, to int64) error { +func (e *elasticsearchDataQuery) processQuery(q *Query, ms *es.MultiSearchRequestBuilder, from, to int64) error { err := isQueryWithError(q) if err != nil { return err diff --git a/pkg/tsdb/elasticsearch/time_series_query_test.go b/pkg/tsdb/elasticsearch/data_query_test.go similarity index 94% rename from pkg/tsdb/elasticsearch/time_series_query_test.go rename to pkg/tsdb/elasticsearch/data_query_test.go index c4d26f8bb00..cf6e06fd8e2 100644 --- a/pkg/tsdb/elasticsearch/time_series_query_test.go +++ b/pkg/tsdb/elasticsearch/data_query_test.go @@ -12,7 +12,7 @@ import ( es "github.com/grafana/grafana/pkg/tsdb/elasticsearch/client" ) -func TestExecuteTimeSeriesQuery(t *testing.T) { +func TestExecuteElasticsearchDataQuery(t *testing.T) { from := time.Date(2018, 5, 15, 17, 50, 0, 0, time.UTC) to := time.Date(2018, 5, 15, 17, 55, 0, 0, time.UTC) fromMs := from.UnixNano() / int64(time.Millisecond) @@ -21,7 +21,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) { t.Run("Test execute time series query", func(t *testing.T) { t.Run("With defaults", func(t *testing.T) { c := newFakeClient() - _, err := executeTsdbQuery(c, `{ + _, err := executeElasticsearchDataQuery(c, `{ "bucketAggs": [{ "type": "date_histogram", "field": "@timestamp", "id": "2" }], "metrics": [{"type": "count", "id": "0" }] }`, from, to) @@ -40,7 +40,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) { }) t.Run("Should clean settings from null values (from frontend tests)", func(t *testing.T) { c := newFakeClient() - _, err := executeTsdbQuery(c, `{ + _, err := executeElasticsearchDataQuery(c, `{ "bucketAggs": [{ "type": "date_histogram", "field": "@timestamp", "id": "1" }], "metrics": [{"type": "avg", "id": "0", "settings": {"missing": "null", "script": "1" } }] }`, from, to) @@ -54,7 +54,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) { t.Run("With multiple bucket aggs", func(t *testing.T) { c := newFakeClient() - _, err := executeTsdbQuery(c, `{ + _, err := executeElasticsearchDataQuery(c, `{ "bucketAggs": [ { "type": "terms", "field": "@host", "id": "2", "settings": { "size": "0", "order": "asc" } }, { "type": "date_histogram", "field": "@timestamp", "id": "3" } @@ -75,7 +75,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) { t.Run("With select field", func(t *testing.T) { c := newFakeClient() - _, err := executeTsdbQuery(c, `{ + _, err := executeElasticsearchDataQuery(c, `{ "bucketAggs": [ { "type": "date_histogram", "field": "@timestamp", "id": "2" } ], @@ -94,7 +94,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) { t.Run("With term agg and order by term (from frontend tests)", func(t *testing.T) { c := newFakeClient() - _, err := executeTsdbQuery(c, `{ + _, err := executeElasticsearchDataQuery(c, `{ "bucketAggs": [ { "type": "terms", @@ -117,7 +117,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) { t.Run("With term agg and order by metric agg", func(t *testing.T) { c := newFakeClient() - _, err := executeTsdbQuery(c, `{ + _, err := executeElasticsearchDataQuery(c, `{ "bucketAggs": [ { "type": "terms", @@ -148,7 +148,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) { t.Run("With term agg and order by count metric agg", func(t *testing.T) { c := newFakeClient() - _, err := executeTsdbQuery(c, `{ + _, err := executeElasticsearchDataQuery(c, `{ "bucketAggs": [ { "type": "terms", @@ -171,7 +171,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) { t.Run("With term agg and order by count agg (from frontend tests)", func(t *testing.T) { c := newFakeClient() - _, err := executeTsdbQuery(c, `{ + _, err := executeElasticsearchDataQuery(c, `{ "metrics": [ {"type": "count", "id": "1" }, {"type": "avg", "field": "@value", "id": "5" } @@ -197,7 +197,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) { t.Run("With term agg and order by percentiles agg", func(t *testing.T) { c := newFakeClient() - _, err := executeTsdbQuery(c, `{ + _, err := executeElasticsearchDataQuery(c, `{ "bucketAggs": [ { "type": "terms", @@ -225,7 +225,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) { t.Run("With term agg and order by extended stats agg", func(t *testing.T) { c := newFakeClient() - _, err := executeTsdbQuery(c, `{ + _, err := executeElasticsearchDataQuery(c, `{ "bucketAggs": [ { "type": "terms", @@ -254,7 +254,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) { t.Run("With term agg and order by term", func(t *testing.T) { c := newFakeClient() - _, err := executeTsdbQuery(c, `{ + _, err := executeElasticsearchDataQuery(c, `{ "bucketAggs": [ { "type": "terms", @@ -280,7 +280,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) { t.Run("With term agg and valid min_doc_count (from frontend tests)", func(t *testing.T) { c := newFakeClient() - _, err := executeTsdbQuery(c, `{ + _, err := executeElasticsearchDataQuery(c, `{ "bucketAggs": [ { "type": "terms", @@ -305,7 +305,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) { t.Run("With metric percentiles", func(t *testing.T) { c := newFakeClient() - _, err := executeTsdbQuery(c, `{ + _, err := executeElasticsearchDataQuery(c, `{ "bucketAggs": [ { "type": "date_histogram", "field": "@timestamp", "id": "3" } ], @@ -338,7 +338,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) { t.Run("With filters aggs", func(t *testing.T) { c := newFakeClient() - _, err := executeTsdbQuery(c, `{ + _, err := executeElasticsearchDataQuery(c, `{ "bucketAggs": [ { "id": "2", @@ -368,7 +368,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) { t.Run("With filters aggs and empty label (from frontend tests)", func(t *testing.T) { c := newFakeClient() - _, err := executeTsdbQuery(c, `{ + _, err := executeElasticsearchDataQuery(c, `{ "bucketAggs": [ { "id": "2", @@ -398,7 +398,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) { t.Run("With raw document metric size", func(t *testing.T) { c := newFakeClient() - _, err := executeTsdbQuery(c, `{ + _, err := executeElasticsearchDataQuery(c, `{ "bucketAggs": [], "metrics": [{ "id": "1", "type": "raw_document", "settings": {} }] }`, from, to) @@ -410,7 +410,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) { t.Run("With raw document metric query (from frontend tests)", func(t *testing.T) { c := newFakeClient() - _, err := executeTsdbQuery(c, `{ + _, err := executeElasticsearchDataQuery(c, `{ "bucketAggs": [], "metrics": [{ "id": "1", "type": "raw_document", "settings": {} }] }`, from, to) @@ -431,7 +431,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) { t.Run("With raw data metric query (from frontend tests)", func(t *testing.T) { c := newFakeClient() - _, err := executeTsdbQuery(c, `{ + _, err := executeElasticsearchDataQuery(c, `{ "bucketAggs": [], "metrics": [{ "id": "1", "type": "raw_data", "settings": {} }] }`, from, to) @@ -452,7 +452,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) { t.Run("With raw document metric size set", func(t *testing.T) { c := newFakeClient() - _, err := executeTsdbQuery(c, `{ + _, err := executeElasticsearchDataQuery(c, `{ "bucketAggs": [], "metrics": [{ "id": "1", "type": "raw_document", "settings": { "size": "1337" } }] }`, from, to) @@ -464,7 +464,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) { t.Run("With date histogram agg", func(t *testing.T) { c := newFakeClient() - _, err := executeTsdbQuery(c, `{ + _, err := executeElasticsearchDataQuery(c, `{ "bucketAggs": [ { "id": "2", @@ -488,7 +488,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) { t.Run("Should not include time_zone if not present in the query model (from frontend tests)", func(t *testing.T) { c := newFakeClient() - _, err := executeTsdbQuery(c, `{ + _, err := executeElasticsearchDataQuery(c, `{ "bucketAggs": [ { "id": "2", @@ -510,7 +510,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) { t.Run("Should not include time_zone when timeZone is utc", func(t *testing.T) { c := newFakeClient() - _, err := executeTsdbQuery(c, `{ + _, err := executeElasticsearchDataQuery(c, `{ "bucketAggs": [ { "id": "2", @@ -532,7 +532,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) { t.Run("Should include time_zone when timeZone is not utc", func(t *testing.T) { c := newFakeClient() - _, err := executeTsdbQuery(c, `{ + _, err := executeElasticsearchDataQuery(c, `{ "bucketAggs": [ { "id": "2", @@ -555,7 +555,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) { t.Run("With histogram agg", func(t *testing.T) { c := newFakeClient() - _, err := executeTsdbQuery(c, `{ + _, err := executeElasticsearchDataQuery(c, `{ "bucketAggs": [ { "id": "3", @@ -581,7 +581,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) { t.Run("With histogram (from frontend tests)", func(t *testing.T) { c := newFakeClient() - _, err := executeTsdbQuery(c, `{ + _, err := executeElasticsearchDataQuery(c, `{ "bucketAggs": [ { "id": "3", @@ -606,7 +606,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) { t.Run("With geo hash grid agg", func(t *testing.T) { c := newFakeClient() - _, err := executeTsdbQuery(c, `{ + _, err := executeElasticsearchDataQuery(c, `{ "bucketAggs": [ { "id": "3", @@ -630,7 +630,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) { t.Run("With moving average (from frontend tests)", func(t *testing.T) { c := newFakeClient() - _, err := executeTsdbQuery(c, `{ + _, err := executeElasticsearchDataQuery(c, `{ "bucketAggs": [ { "type": "date_histogram", "field": "@timestamp", "id": "4" } ], @@ -664,7 +664,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) { t.Run("With moving average", func(t *testing.T) { c := newFakeClient() - _, err := executeTsdbQuery(c, `{ + _, err := executeElasticsearchDataQuery(c, `{ "bucketAggs": [ { "type": "date_histogram", "field": "@timestamp", "id": "4" } ], @@ -701,7 +701,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) { t.Run("With moving average doc count (from frontend tests)", func(t *testing.T) { c := newFakeClient() - _, err := executeTsdbQuery(c, `{ + _, err := executeElasticsearchDataQuery(c, `{ "bucketAggs": [ { "type": "date_histogram", "field": "@timestamp", "id": "4" } ], @@ -731,7 +731,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) { t.Run("With moving average doc count", func(t *testing.T) { c := newFakeClient() - _, err := executeTsdbQuery(c, `{ + _, err := executeElasticsearchDataQuery(c, `{ "bucketAggs": [ { "type": "date_histogram", "field": "@timestamp", "id": "4" } ], @@ -762,7 +762,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) { t.Run("With broken moving average (from frontend tests)", func(t *testing.T) { c := newFakeClient() - _, err := executeTsdbQuery(c, `{ + _, err := executeElasticsearchDataQuery(c, `{ "bucketAggs": [ { "type": "date_histogram", "field": "@timestamp", "id": "3" } ], @@ -797,7 +797,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) { t.Run("With broken moving average", func(t *testing.T) { c := newFakeClient() - _, err := executeTsdbQuery(c, `{ + _, err := executeElasticsearchDataQuery(c, `{ "bucketAggs": [ { "type": "date_histogram", "field": "@timestamp", "id": "5" } ], @@ -832,7 +832,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) { t.Run("With top_metrics (from frontend tests)", func(t *testing.T) { c := newFakeClient() - _, err := executeTsdbQuery(c, `{ + _, err := executeElasticsearchDataQuery(c, `{ "bucketAggs": [ { "type": "date_histogram", "field": "@timestamp", "id": "3" } ], @@ -855,7 +855,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) { t.Run("With cumulative sum", func(t *testing.T) { c := newFakeClient() - _, err := executeTsdbQuery(c, `{ + _, err := executeElasticsearchDataQuery(c, `{ "bucketAggs": [ { "type": "date_histogram", "field": "@timestamp", "id": "4" } ], @@ -892,7 +892,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) { t.Run("With cumulative sum doc count", func(t *testing.T) { c := newFakeClient() - _, err := executeTsdbQuery(c, `{ + _, err := executeElasticsearchDataQuery(c, `{ "bucketAggs": [ { "type": "date_histogram", "field": "@timestamp", "id": "4" } ], @@ -923,7 +923,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) { t.Run("With broken cumulative sum", func(t *testing.T) { c := newFakeClient() - _, err := executeTsdbQuery(c, `{ + _, err := executeElasticsearchDataQuery(c, `{ "bucketAggs": [ { "type": "date_histogram", "field": "@timestamp", "id": "5" } ], @@ -958,7 +958,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) { t.Run("With derivative", func(t *testing.T) { c := newFakeClient() - _, err := executeTsdbQuery(c, `{ + _, err := executeElasticsearchDataQuery(c, `{ "bucketAggs": [ { "type": "date_histogram", "field": "@timestamp", "id": "4" } ], @@ -987,7 +987,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) { t.Run("With derivative doc count", func(t *testing.T) { // This test is with pipelineAgg and is passing. Same test without pipelineAgg is failing. c := newFakeClient() - _, err := executeTsdbQuery(c, `{ + _, err := executeElasticsearchDataQuery(c, `{ "bucketAggs": [ { "type": "date_histogram", "field": "@timestamp", "id": "4" } ], @@ -1015,7 +1015,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) { t.Run("With derivative doc count (from frontend tests)", func(t *testing.T) { c := newFakeClient() - _, err := executeTsdbQuery(c, `{ + _, err := executeElasticsearchDataQuery(c, `{ "bucketAggs": [ { "type": "date_histogram", "field": "@timestamp", "id": "4" } ], @@ -1044,7 +1044,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) { t.Run("With serial_diff", func(t *testing.T) { // This test is with pipelineAgg and is passing. Same test without pipelineAgg is failing. c := newFakeClient() - _, err := executeTsdbQuery(c, `{ + _, err := executeElasticsearchDataQuery(c, `{ "bucketAggs": [ { "type": "date_histogram", "field": "@timestamp", "id": "4" } ], @@ -1074,7 +1074,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) { t.Run("With serial_diff (from frontend tests)", func(t *testing.T) { c := newFakeClient() - _, err := executeTsdbQuery(c, `{ + _, err := executeElasticsearchDataQuery(c, `{ "bucketAggs": [ { "type": "date_histogram", "field": "@timestamp", "id": "3" } ], @@ -1103,7 +1103,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) { t.Run("With serial_diff doc count", func(t *testing.T) { c := newFakeClient() - _, err := executeTsdbQuery(c, `{ + _, err := executeElasticsearchDataQuery(c, `{ "bucketAggs": [ { "type": "date_histogram", "field": "@timestamp", "id": "4" } ], @@ -1131,7 +1131,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) { t.Run("With bucket_script", func(t *testing.T) { c := newFakeClient() - _, err := executeTsdbQuery(c, `{ + _, err := executeElasticsearchDataQuery(c, `{ "bucketAggs": [ { "type": "date_histogram", "field": "@timestamp", "id": "2" } ], @@ -1167,7 +1167,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) { t.Run("With bucket_script (from frontend tests)", func(t *testing.T) { c := newFakeClient() - _, err := executeTsdbQuery(c, `{ + _, err := executeElasticsearchDataQuery(c, `{ "bucketAggs": [ { "type": "date_histogram", "field": "@timestamp", "id": "4" } ], @@ -1203,7 +1203,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) { t.Run("With bucket_script doc count", func(t *testing.T) { c := newFakeClient() - _, err := executeTsdbQuery(c, `{ + _, err := executeElasticsearchDataQuery(c, `{ "bucketAggs": [ { "type": "date_histogram", "field": "@timestamp", "id": "4" } ], @@ -1236,7 +1236,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) { t.Run("With bucket_script doc count (from frontend tests)", func(t *testing.T) { c := newFakeClient() - _, err := executeTsdbQuery(c, `{ + _, err := executeElasticsearchDataQuery(c, `{ "bucketAggs": [ { "type": "date_histogram", "field": "@timestamp", "id": "2" } ], @@ -1269,7 +1269,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) { t.Run("With lucene query should add query_string filter when query is not empty (from frontend tests)", func(t *testing.T) { c := newFakeClient() - _, err := executeTsdbQuery(c, `{ + _, err := executeElasticsearchDataQuery(c, `{ "query": "foo", "bucketAggs": [], "metrics": [{ "id": "1", "type": "raw_data", "settings": {} }] @@ -1283,7 +1283,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) { t.Run("With lucene query should add query_string filter when query is not empty (from frontend tests)", func(t *testing.T) { c := newFakeClient() - _, err := executeTsdbQuery(c, `{ + _, err := executeElasticsearchDataQuery(c, `{ "query": "foo", "bucketAggs": [], "metrics": [{ "id": "1", "type": "raw_data", "settings": {} }] @@ -1297,7 +1297,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) { t.Run("With log query should return query with defaults (from frontend tests)", func(t *testing.T) { c := newFakeClient() - _, err := executeTsdbQuery(c, `{ + _, err := executeElasticsearchDataQuery(c, `{ "metrics": [{ "type": "logs", "id": "1"}] }`, from, to) require.NoError(t, err) @@ -1329,7 +1329,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) { t.Run("With log query with limit should return query with correct size", func(t *testing.T) { c := newFakeClient() - _, err := executeTsdbQuery(c, `{ + _, err := executeElasticsearchDataQuery(c, `{ "metrics": [{ "type": "logs", "id": "1", "settings": { "limit": "1000" }}] }`, from, to) require.NoError(t, err) @@ -1339,7 +1339,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) { t.Run("With log query should return highlight properties", func(t *testing.T) { c := newFakeClient() - _, err := executeTsdbQuery(c, `{ + _, err := executeElasticsearchDataQuery(c, `{ "metrics": [{ "type": "logs", "id": "1" }] }`, from, to) require.NoError(t, err) @@ -1356,7 +1356,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) { t.Run("With invalid query should return error", (func(t *testing.T) { c := newFakeClient() - _, err := executeTsdbQuery(c, `{ + _, err := executeElasticsearchDataQuery(c, `{ "query": "foo", }`, from, to) require.Error(t, err) @@ -1370,7 +1370,7 @@ func TestSettingsCasting(t *testing.T) { t.Run("Correctly casts values in moving_avg (from frontend tests)", func(t *testing.T) { c := newFakeClient() - _, err := executeTsdbQuery(c, `{ + _, err := executeElasticsearchDataQuery(c, `{ "metrics": [ { "type": "avg", "id" : "2" }, { @@ -1410,7 +1410,7 @@ func TestSettingsCasting(t *testing.T) { t.Run("Correctly transforms moving_average settings", func(t *testing.T) { // This test is with pipelineAgg and is passing. Same test without pipelineAgg is failing. c := newFakeClient() - _, err := executeTsdbQuery(c, `{ + _, err := executeElasticsearchDataQuery(c, `{ "bucketAggs": [ { "type": "date_histogram", "field": "@timestamp", "id": "2" } ], @@ -1453,7 +1453,7 @@ func TestSettingsCasting(t *testing.T) { t.Run("Correctly transforms serial_diff settings (from frontend tests)", func(t *testing.T) { c := newFakeClient() - _, err := executeTsdbQuery(c, `{ + _, err := executeElasticsearchDataQuery(c, `{ "bucketAggs": [ { "type": "date_histogram", "field": "@timestamp", "id": "1" } ], @@ -1478,7 +1478,7 @@ func TestSettingsCasting(t *testing.T) { t.Run("Correctly transforms serial_diff settings", func(t *testing.T) { // This test is with pipelineAgg and is passing. Same test without pipelineAgg is failing. c := newFakeClient() - _, err := executeTsdbQuery(c, `{ + _, err := executeElasticsearchDataQuery(c, `{ "bucketAggs": [ { "type": "date_histogram", "field": "@timestamp", "id": "2" } ], @@ -1506,7 +1506,7 @@ func TestSettingsCasting(t *testing.T) { t.Run("Date Histogram Settings", func(t *testing.T) { t.Run("Correctly transforms date_histogram settings", func(t *testing.T) { c := newFakeClient() - _, err := executeTsdbQuery(c, `{ + _, err := executeElasticsearchDataQuery(c, `{ "bucketAggs": [ { "type": "date_histogram", @@ -1540,7 +1540,7 @@ func TestSettingsCasting(t *testing.T) { t.Run("Correctly uses already int min_doc_count", func(t *testing.T) { c := newFakeClient() - _, err := executeTsdbQuery(c, `{ + _, err := executeElasticsearchDataQuery(c, `{ "bucketAggs": [ { "type": "date_histogram", @@ -1575,7 +1575,7 @@ func TestSettingsCasting(t *testing.T) { t.Run("interval parameter", func(t *testing.T) { t.Run("Uses fixed_interval", func(t *testing.T) { c := newFakeClient() - _, err := executeTsdbQuery(c, `{ + _, err := executeElasticsearchDataQuery(c, `{ "bucketAggs": [ { "type": "date_histogram", @@ -1603,7 +1603,7 @@ func TestSettingsCasting(t *testing.T) { t.Run("Inline Script", func(t *testing.T) { t.Run("Correctly handles scripts", func(t *testing.T) { c := newFakeClient() - _, err := executeTsdbQuery(c, `{ + _, err := executeElasticsearchDataQuery(c, `{ "bucketAggs": [ { "type": "date_histogram", "field": "@timestamp", "id": "2" } ], @@ -1641,7 +1641,7 @@ func TestSettingsCasting(t *testing.T) { t.Run("Field property (from frontend tests)", func(t *testing.T) { t.Run("Should use timeField from datasource when not specified", func(t *testing.T) { c := newFakeClient() - _, err := executeTsdbQuery(c, `{ + _, err := executeElasticsearchDataQuery(c, `{ "metrics": [{ "type": "count", "id": "1" }], "bucketAggs": [ { "type": "date_histogram", "id": "2", "settings": { "min_doc_count": "1" } } @@ -1656,7 +1656,7 @@ func TestSettingsCasting(t *testing.T) { t.Run("Should use field from bucket agg when specified", func(t *testing.T) { c := newFakeClient() - _, err := executeTsdbQuery(c, `{ + _, err := executeElasticsearchDataQuery(c, `{ "metrics": [{ "type": "count", "id": "1" }], "bucketAggs": [ { "type": "date_histogram", "id": "2", "field": "@time", "settings": { "min_doc_count": "1" } } @@ -1671,7 +1671,7 @@ func TestSettingsCasting(t *testing.T) { t.Run("Should use fixed_interval", func(t *testing.T) { c := newFakeClient() - _, err := executeTsdbQuery(c, `{ + _, err := executeElasticsearchDataQuery(c, `{ "metrics": [{ "type": "count", "id": "1" }], "bucketAggs": [ { "type": "date_histogram", "id": "2", "field": "@time", "settings": { "min_doc_count": "1", "interval": "1d" } } @@ -1737,7 +1737,7 @@ func newDataQuery(body string) (backend.QueryDataRequest, error) { }, nil } -func executeTsdbQuery(c es.Client, body string, from, to time.Time) ( +func executeElasticsearchDataQuery(c es.Client, body string, from, to time.Time) ( *backend.QueryDataResponse, error) { timeRange := backend.TimeRange{ From: from, @@ -1751,6 +1751,6 @@ func executeTsdbQuery(c es.Client, body string, from, to time.Time) ( }, }, } - query := newTimeSeriesQuery(c, dataRequest.Queries) + query := newElasticsearchDataQuery(c, dataRequest.Queries) return query.execute() } diff --git a/pkg/tsdb/elasticsearch/elasticsearch.go b/pkg/tsdb/elasticsearch/elasticsearch.go index 87174570449..8aefd13c393 100644 --- a/pkg/tsdb/elasticsearch/elasticsearch.go +++ b/pkg/tsdb/elasticsearch/elasticsearch.go @@ -58,7 +58,7 @@ func queryData(ctx context.Context, queries []backend.DataQuery, dsInfo *es.Data if err != nil { return &backend.QueryDataResponse{}, err } - query := newTimeSeriesQuery(client, queries) + query := newElasticsearchDataQuery(client, queries) return query.execute() } From 8930ad2046664aa247481d6de687f817e1a07bad Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 7 Mar 2023 13:00:55 +0000 Subject: [PATCH 020/288] Update dependency fork-ts-checker-webpack-plugin to v8 (#64309) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 14 +++++--------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 365dde12d77..0a897829c4f 100644 --- a/package.json +++ b/package.json @@ -198,7 +198,7 @@ "eslint-plugin-react-hooks": "4.6.0", "eslint-webpack-plugin": "4.0.0", "expose-loader": "4.0.0", - "fork-ts-checker-webpack-plugin": "7.3.0", + "fork-ts-checker-webpack-plugin": "8.0.0", "glob": "9.1.2", "html-loader": "4.2.0", "html-webpack-plugin": "5.5.0", diff --git a/yarn.lock b/yarn.lock index f7308f1f32d..eb4bab9c0ba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -21085,9 +21085,9 @@ __metadata: languageName: node linkType: hard -"fork-ts-checker-webpack-plugin@npm:7.3.0": - version: 7.3.0 - resolution: "fork-ts-checker-webpack-plugin@npm:7.3.0" +"fork-ts-checker-webpack-plugin@npm:8.0.0": + version: 8.0.0 + resolution: "fork-ts-checker-webpack-plugin@npm:8.0.0" dependencies: "@babel/code-frame": ^7.16.7 chalk: ^4.1.2 @@ -21103,12 +21103,8 @@ __metadata: tapable: ^2.2.1 peerDependencies: typescript: ">3.6.0" - vue-template-compiler: "*" webpack: ^5.11.0 - peerDependenciesMeta: - vue-template-compiler: - optional: true - checksum: 49c2af801e264349a3fdf0afe4ad33065960c43bd7e56c8351a5e0d32c8c54146cc89d6a0b70b1e0f810de96787bd0c7fd275cc8727a9aea1a077c53de99659a + checksum: aad4cbc5b802e6281a2700a379837697c93ad95288468f9595219d91d9c26674736d37852bb4c4341e9122f26181e9e05fc1a362e8d029fdd88e99de7816037b languageName: node linkType: hard @@ -22254,7 +22250,7 @@ __metadata: fast-deep-equal: ^3.1.3 fast-json-patch: 3.1.1 file-saver: 2.0.5 - fork-ts-checker-webpack-plugin: 7.3.0 + fork-ts-checker-webpack-plugin: 8.0.0 framework-utils: ^1.1.0 glob: 9.1.2 history: 4.10.1 From c5cbcdb420031a9776db60e44271c451ab2da2ee Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 7 Mar 2023 13:02:45 +0000 Subject: [PATCH 021/288] Update dependency react-calendar to v4 (#64310) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- packages/grafana-ui/package.json | 2 +- yarn.lock | 30 +++++++++++++++--------------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 14962337bd2..53f84c3dd6c 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -86,7 +86,7 @@ "rc-time-picker": "^3.7.3", "rc-tooltip": "5.3.1", "react-beautiful-dnd": "13.1.1", - "react-calendar": "3.9.0", + "react-calendar": "4.0.0", "react-colorful": "5.6.1", "react-custom-scrollbars-2": "4.5.0", "react-dropzone": "14.2.3", diff --git a/yarn.lock b/yarn.lock index eb4bab9c0ba..43ec294aa4f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5395,7 +5395,7 @@ __metadata: rc-tooltip: 5.3.1 react: 17.0.2 react-beautiful-dnd: 13.1.1 - react-calendar: 3.9.0 + react-calendar: 4.0.0 react-colorful: 5.6.1 react-custom-scrollbars-2: 4.5.0 react-dom: 17.0.2 @@ -16058,6 +16058,13 @@ __metadata: languageName: node linkType: hard +"clsx@npm:^1.2.1": + version: 1.2.1 + resolution: "clsx@npm:1.2.1" + checksum: 30befca8019b2eb7dbad38cff6266cf543091dae2825c856a62a8ccf2c3ab9c2907c4d12b288b73101196767f66812365400a227581484a05f968b0307cfaf12 + languageName: node + linkType: hard + "cmd-shim@npm:^5.0.0": version: 5.0.0 resolution: "cmd-shim@npm:5.0.0" @@ -27663,13 +27670,6 @@ __metadata: languageName: node linkType: hard -"merge-class-names@npm:^1.1.1": - version: 1.4.2 - resolution: "merge-class-names@npm:1.4.2" - checksum: 569c333ab0d7fa1e06ae6e637d58e0d4623d7b165ea78d085c1bbd042568d8793bb7ce54ec55580679416046b11fc6eaf4956d68d19964cb320edc034e182c1d - languageName: node - linkType: hard - "merge-descriptors@npm:1.0.1": version: 1.0.1 resolution: "merge-descriptors@npm:1.0.1" @@ -32699,18 +32699,18 @@ __metadata: languageName: node linkType: hard -"react-calendar@npm:3.9.0": - version: 3.9.0 - resolution: "react-calendar@npm:3.9.0" +"react-calendar@npm:4.0.0": + version: 4.0.0 + resolution: "react-calendar@npm:4.0.0" dependencies: "@wojtekmaj/date-utils": ^1.0.2 + clsx: ^1.2.1 get-user-locale: ^1.2.0 - merge-class-names: ^1.1.1 prop-types: ^15.6.0 peerDependencies: - react: ^16.3.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.3.0 || ^17.0.0 || ^18.0.0 - checksum: f178e8afef9e427689472c3792a2e4d60ca959657f12e06ab07a7cc27f64cc5e9531e495d087bfd5c9bc5826f7cec5088fb0e7f15fa4f28482180cc344c8c60f + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: a470ea1eab914cda9f76245060e15129a220c30f2942a28c20b3cb999fd994d0c0fadae849184ba123b3b78541fbb3301c5ac0c44e0bd2ab156bfba8cc587593 languageName: node linkType: hard From e5870aa4f11b22b3a065e2af1dee9102da3a5892 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 7 Mar 2023 13:05:16 +0000 Subject: [PATCH 022/288] Update dependency webpack-cli to v5 (#64311) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 86 ++++++++++++++++++++++++++++------------------------ 2 files changed, 47 insertions(+), 41 deletions(-) diff --git a/package.json b/package.json index 0a897829c4f..3a5b2b3b93f 100644 --- a/package.json +++ b/package.json @@ -247,7 +247,7 @@ "wait-on": "7.0.1", "webpack": "5.75.0", "webpack-bundle-analyzer": "4.8.0", - "webpack-cli": "4.10.0", + "webpack-cli": "5.0.1", "webpack-dev-server": "4.11.1", "webpack-manifest-plugin": "5.0.0", "webpack-merge": "5.8.0", diff --git a/yarn.lock b/yarn.lock index 43ec294aa4f..1c8e02539e2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13102,36 +13102,36 @@ __metadata: languageName: node linkType: hard -"@webpack-cli/configtest@npm:^1.2.0": - version: 1.2.0 - resolution: "@webpack-cli/configtest@npm:1.2.0" +"@webpack-cli/configtest@npm:^2.0.1": + version: 2.0.1 + resolution: "@webpack-cli/configtest@npm:2.0.1" peerDependencies: - webpack: 4.x.x || 5.x.x - webpack-cli: 4.x.x - checksum: a2726cd9ec601d2b57e5fc15e0ebf5200a8892065e735911269ac2038e62be4bfc176ea1f88c2c46ff09b4d05d4c10ae045e87b3679372483d47da625a327e28 + webpack: 5.x.x + webpack-cli: 5.x.x + checksum: 15d0ca835f2e16ec99e9f295f07b676435b9e706d7700df0ad088692fea065e34772fc44b96a4f6a86178b9ca8cf1ff941fbce15269587cf0925d70b18928cea languageName: node linkType: hard -"@webpack-cli/info@npm:^1.5.0": - version: 1.5.0 - resolution: "@webpack-cli/info@npm:1.5.0" - dependencies: - envinfo: ^7.7.3 +"@webpack-cli/info@npm:^2.0.1": + version: 2.0.1 + resolution: "@webpack-cli/info@npm:2.0.1" peerDependencies: - webpack-cli: 4.x.x - checksum: 7f56fe037cd7d1fd5c7428588519fbf04a0cad33925ee4202ffbafd00f8ec1f2f67d991245e687d50e0f3e23f7b7814273d56cb9f7da4b05eed47c8d815c6296 + webpack: 5.x.x + webpack-cli: 5.x.x + checksum: b8fba49fee10d297c2affb0b064c9a81e9038d75517c6728fb85f9fb254cae634e5d33e696dac5171e6944ae329d85fddac72f781c7d833f7e9dfe43151ce60d languageName: node linkType: hard -"@webpack-cli/serve@npm:^1.7.0": - version: 1.7.0 - resolution: "@webpack-cli/serve@npm:1.7.0" +"@webpack-cli/serve@npm:^2.0.1": + version: 2.0.1 + resolution: "@webpack-cli/serve@npm:2.0.1" peerDependencies: - webpack-cli: 4.x.x + webpack: 5.x.x + webpack-cli: 5.x.x peerDependenciesMeta: webpack-dev-server: optional: true - checksum: d475e8effa23eb7ff9a48b14d4de425989fd82f906ce71c210921cc3852327c22873be00c35e181a25a6bd03d424ae2b83e7f3b3f410ac7ee31b128ab4ac7713 + checksum: 75c55f8398dd60e4821f81bec6e96287cebb3ab1837ef016779bc2f0c76a1d29c45b99e53daa99ba1fa156b5e2b61c19abf58098de20c2b58391b1f496ecc145 languageName: node linkType: hard @@ -16289,7 +16289,7 @@ __metadata: languageName: node linkType: hard -"commander@npm:7, commander@npm:^7.0.0, commander@npm:^7.2.0": +"commander@npm:7, commander@npm:^7.2.0": version: 7.2.0 resolution: "commander@npm:7.2.0" checksum: 53501cbeee61d5157546c0bef0fedb6cdfc763a882136284bed9a07225f09a14b82d2a84e7637edfd1a679fb35ed9502fd58ef1d091e6287f60d790147f68ddc @@ -22394,7 +22394,7 @@ __metadata: wait-on: 7.0.1 webpack: 5.75.0 webpack-bundle-analyzer: 4.8.0 - webpack-cli: 4.10.0 + webpack-cli: 5.0.1 webpack-dev-server: 4.11.1 webpack-manifest-plugin: 5.0.0 webpack-merge: 5.8.0 @@ -23684,6 +23684,13 @@ __metadata: languageName: node linkType: hard +"interpret@npm:^3.1.1": + version: 3.1.1 + resolution: "interpret@npm:3.1.1" + checksum: 35cebcf48c7351130437596d9ab8c8fe131ce4038da4561e6d665f25640e0034702a031cf7e3a5cea60ac7ac548bf17465e0571ede126f3d3a6933152171ac82 + languageName: node + linkType: hard + "intl-messageformat@npm:^10.1.0": version: 10.1.4 resolution: "intl-messageformat@npm:10.1.4" @@ -33719,12 +33726,12 @@ __metadata: languageName: node linkType: hard -"rechoir@npm:^0.7.0": - version: 0.7.1 - resolution: "rechoir@npm:0.7.1" +"rechoir@npm:^0.8.0": + version: 0.8.0 + resolution: "rechoir@npm:0.8.0" dependencies: - resolve: ^1.9.0 - checksum: 2a04aab4e28c05fcd6ee6768446bc8b859d8f108e71fc7f5bcbc5ef25e53330ce2c11d10f82a24591a2df4c49c4f61feabe1fd11f844c66feedd4cd7bb61146a + resolve: ^1.20.0 + checksum: ad3caed8afdefbc33fbc30e6d22b86c35b3d51c2005546f4e79bcc03c074df804b3640ad18945e6bef9ed12caedc035655ec1082f64a5e94c849ff939dc0a788 languageName: node linkType: hard @@ -34332,7 +34339,7 @@ __metadata: languageName: node linkType: hard -"resolve@npm:^1.10.0, resolve@npm:^1.12.0, resolve@npm:^1.14.2, resolve@npm:^1.19.0, resolve@npm:^1.20.0, resolve@npm:^1.9.0": +"resolve@npm:^1.10.0, resolve@npm:^1.12.0, resolve@npm:^1.14.2, resolve@npm:^1.19.0, resolve@npm:^1.20.0": version: 1.20.0 resolution: "resolve@npm:1.20.0" dependencies: @@ -34391,7 +34398,7 @@ __metadata: languageName: node linkType: hard -"resolve@patch:resolve@^1.10.0#~builtin, resolve@patch:resolve@^1.12.0#~builtin, resolve@patch:resolve@^1.14.2#~builtin, resolve@patch:resolve@^1.19.0#~builtin, resolve@patch:resolve@^1.20.0#~builtin, resolve@patch:resolve@^1.9.0#~builtin": +"resolve@patch:resolve@^1.10.0#~builtin, resolve@patch:resolve@^1.12.0#~builtin, resolve@patch:resolve@^1.14.2#~builtin, resolve@patch:resolve@^1.19.0#~builtin, resolve@patch:resolve@^1.20.0#~builtin": version: 1.20.0 resolution: "resolve@patch:resolve@npm%3A1.20.0#~builtin::version=1.20.0&hash=c3c19d" dependencies: @@ -39194,36 +39201,35 @@ __metadata: languageName: node linkType: hard -"webpack-cli@npm:4.10.0": - version: 4.10.0 - resolution: "webpack-cli@npm:4.10.0" +"webpack-cli@npm:5.0.1": + version: 5.0.1 + resolution: "webpack-cli@npm:5.0.1" dependencies: "@discoveryjs/json-ext": ^0.5.0 - "@webpack-cli/configtest": ^1.2.0 - "@webpack-cli/info": ^1.5.0 - "@webpack-cli/serve": ^1.7.0 + "@webpack-cli/configtest": ^2.0.1 + "@webpack-cli/info": ^2.0.1 + "@webpack-cli/serve": ^2.0.1 colorette: ^2.0.14 - commander: ^7.0.0 + commander: ^9.4.1 cross-spawn: ^7.0.3 + envinfo: ^7.7.3 fastest-levenshtein: ^1.0.12 import-local: ^3.0.2 - interpret: ^2.2.0 - rechoir: ^0.7.0 + interpret: ^3.1.1 + rechoir: ^0.8.0 webpack-merge: ^5.7.3 peerDependencies: - webpack: 4.x.x || 5.x.x + webpack: 5.x.x peerDependenciesMeta: "@webpack-cli/generators": optional: true - "@webpack-cli/migrate": - optional: true webpack-bundle-analyzer: optional: true webpack-dev-server: optional: true bin: webpack-cli: bin/cli.js - checksum: 2ff5355ac348e6b40f2630a203b981728834dca96d6d621be96249764b2d0fc01dd54edfcc37f02214d02935de2cf0eefd6ce689d970d154ef493f01ba922390 + checksum: b1544eea669442e78c3dba9f79c0f8d0136759b8b2fe9cd32c0d410250fd719988ae037778ba88993215d44971169f2c268c0c934068be561711615f1951bd53 languageName: node linkType: hard From 498d7ae914ac4eabe0c46fb2f38fd410925930bb Mon Sep 17 00:00:00 2001 From: juanicabanas Date: Tue, 7 Mar 2023 10:14:31 -0300 Subject: [PATCH 023/288] PublicDashboards: Email validation on submit (#64238) --- .../ConfigPublicDashboard/EmailSharingConfiguration.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ConfigPublicDashboard/EmailSharingConfiguration.tsx b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ConfigPublicDashboard/EmailSharingConfiguration.tsx index afc7a8de5c7..d9026bb3009 100644 --- a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ConfigPublicDashboard/EmailSharingConfiguration.tsx +++ b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ConfigPublicDashboard/EmailSharingConfiguration.tsx @@ -116,14 +116,14 @@ export const EmailSharingConfiguration = () => { control, watch, handleSubmit, - formState: { isValid, errors }, + formState: { errors }, reset, } = useForm({ defaultValues: { shareType: publicDashboard?.share || PublicDashboardShareType.PUBLIC, email: '', }, - mode: 'onChange', + mode: 'onSubmit', }); const onShareTypeChange = (shareType: PublicDashboardShareType) => { @@ -186,7 +186,7 @@ export const EmailSharingConfiguration = () => {
+ )} +
+ ); +}; + +const getStyles = (theme: GrafanaTheme2) => { + return { + listContainer: css` + padding-top: 10px; + `, + extraInfoContainer: css` + display: flex; + justify-content: end; + position: absolute; + right: 5px; + top: 5px; + `, + oldInfoText: css` + font-size: ${theme.typography.bodySmall.fontSize}; + color: ${theme.colors.text.secondary}; + `, + }; +}; + +function logsLevelZoomRatio( + logsVolumeData: DataFrame[] | undefined, + selectedTimeRange: AbsoluteTimeRange +): number | undefined { + const dataRange = logsVolumeData && logsVolumeData[0] && logsVolumeData[0].meta?.custom?.absoluteRange; + return dataRange ? (selectedTimeRange.from - selectedTimeRange.to) / (dataRange.from - dataRange.to) : undefined; +} diff --git a/public/app/features/explore/__mocks__/data.ts b/public/app/features/explore/__mocks__/data.ts new file mode 100644 index 00000000000..aaeef2bb6da --- /dev/null +++ b/public/app/features/explore/__mocks__/data.ts @@ -0,0 +1,37 @@ +import { Observable, of } from 'rxjs'; + +import { getDefaultTimeRange, LoadingState, LogsModel } from '@grafana/data'; + +import { ExplorePanelData } from '../../../types'; + +type MockProps = { + logsResult?: Partial; +}; + +export const mockExplorePanelData = (props?: MockProps): Observable => { + const data: ExplorePanelData = { + flameGraphFrames: [], + graphFrames: [], + graphResult: [], + logsFrames: [], + logsResult: { + hasUniqueLabels: false, + rows: [], + meta: [], + series: [], + queries: [], + ...(props?.logsResult || {}), + }, + nodeGraphFrames: [], + rawPrometheusFrames: [], + rawPrometheusResult: null, + series: [], + state: LoadingState.Done, + tableFrames: [], + tableResult: [], + timeRange: getDefaultTimeRange(), + traceFrames: [], + }; + + return of(data); +}; diff --git a/public/app/features/explore/state/query.ts b/public/app/features/explore/state/query.ts index 1e4a81c6450..13330f34e22 100644 --- a/public/app/features/explore/state/query.ts +++ b/public/app/features/explore/state/query.ts @@ -1,11 +1,12 @@ import { AnyAction, createAction, PayloadAction } from '@reduxjs/toolkit'; import deepEqual from 'fast-deep-equal'; -import { flatten, groupBy, snakeCase } from 'lodash'; +import { flatten, groupBy, head, map, mapValues, snakeCase, zipObject } from 'lodash'; import { combineLatest, identity, Observable, of, SubscriptionLike, Unsubscribable } from 'rxjs'; import { mergeMap, throttleTime } from 'rxjs/operators'; import { AbsoluteTimeRange, + DataFrame, DataQueryErrorType, DataQueryResponse, DataSourceApi, @@ -43,11 +44,14 @@ import { notifyApp } from '../../../core/actions'; import { createErrorNotification } from '../../../core/copy/appNotification'; import { runRequest } from '../../query/state/runRequest'; import { decorateData } from '../utils/decorators'; -import { storeSupplementaryQueryEnabled, supplementaryQueryTypes } from '../utils/supplementaryQueries'; +import { + storeSupplementaryQueryEnabled, + supplementaryQueryTypes, + getSupplementaryQueryProvider, +} from '../utils/supplementaryQueries'; import { addHistoryItem, historyUpdatedAction, loadRichHistory } from './history'; import { stateSave } from './main'; -import { getSupplementaryQueryProvider } from './supplementaryQueries'; import { updateTime } from './time'; import { createCacheKey, getResultsFromCache } from './utils'; @@ -674,21 +678,36 @@ export const runQueries = ( */ function canReuseSupplementaryQueryData( supplementaryQueryData: DataQueryResponse | undefined, - queries: DataQuery[], + newQueries: DataQuery[], selectedTimeRange: AbsoluteTimeRange ): boolean { - if (supplementaryQueryData && supplementaryQueryData.data[0]) { - // check if queries are the same - if (!deepEqual(supplementaryQueryData.data[0].meta?.custom?.targets, queries)) { - return false; - } - const dataRange = supplementaryQueryData.data[0].meta?.custom?.absoluteRange; - // if selected range is within loaded logs volume - if (dataRange && dataRange.from <= selectedTimeRange.from && selectedTimeRange.to <= dataRange.to) { + if (!supplementaryQueryData) { + return false; + } + + const newQueriesByRefId = zipObject(map(newQueries, 'refId'), newQueries); + + const existingDataByRefId = mapValues( + groupBy( + supplementaryQueryData.data.map((dataFrame: DataFrame) => dataFrame.meta?.custom?.sourceQuery), + 'refId' + ), + head + ); + + const allQueriesAreTheSame = deepEqual(newQueriesByRefId, existingDataByRefId); + + const allResultsHaveWiderRange = supplementaryQueryData.data.every((data: DataFrame) => { + const dataRange = data.meta?.custom?.absoluteRange; + // Only first data frame in the response may contain the absolute range + if (!dataRange) { return true; } - } - return false; + const hasWiderRange = dataRange && dataRange.from <= selectedTimeRange.from && selectedTimeRange.to <= dataRange.to; + return hasWiderRange; + }); + + return allQueriesAreTheSame && allResultsHaveWiderRange; } /** diff --git a/public/app/features/explore/state/supplementaryQueries.ts b/public/app/features/explore/state/supplementaryQueries.ts deleted file mode 100644 index e2c22e4a134..00000000000 --- a/public/app/features/explore/state/supplementaryQueries.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { Observable } from 'rxjs'; - -import { - DataSourceApi, - SupplementaryQueryType, - DataQueryResponse, - hasSupplementaryQuerySupport, - DataQueryRequest, - LoadingState, - LogsVolumeType, -} from '@grafana/data'; - -import { ExplorePanelData } from '../../../types'; - -export const getSupplementaryQueryProvider = ( - datasourceInstance: DataSourceApi, - type: SupplementaryQueryType, - request: DataQueryRequest, - explorePanelData: Observable -): Observable | undefined => { - if (hasSupplementaryQuerySupport(datasourceInstance, type)) { - return datasourceInstance.getDataProvider(type, request); - } else if (type === SupplementaryQueryType.LogsVolume) { - // Create a fallback to results based logs volume - return new Observable((observer) => { - explorePanelData.subscribe((exploreData) => { - if (exploreData.logsResult?.series && exploreData.logsResult?.visibleRange) { - observer.next({ - data: exploreData.logsResult.series.map((d) => { - const custom = d.meta?.custom || {}; - return { - ...d, - meta: { - custom: { - ...custom, - logsVolumeType: LogsVolumeType.Limited, - absoluteRange: exploreData.logsResult?.visibleRange, - }, - }, - }; - }), - state: LoadingState.Done, - }); - } - }); - }); - } - return undefined; -}; diff --git a/public/app/features/explore/utils/decorators.test.ts b/public/app/features/explore/utils/decorators.test.ts index f0c59a7a737..154717f1434 100644 --- a/public/app/features/explore/utils/decorators.test.ts +++ b/public/app/features/explore/utils/decorators.test.ts @@ -1,17 +1,6 @@ import { lastValueFrom } from 'rxjs'; -import { - ArrayVector, - DataFrame, - DataQueryRequest, - FieldColorModeId, - FieldType, - LoadingState, - PanelData, - getDefaultTimeRange, - toDataFrame, -} from '@grafana/data'; -import { GraphDrawStyle, StackingMode } from '@grafana/schema'; +import { DataFrame, FieldType, LoadingState, PanelData, getDefaultTimeRange, toDataFrame } from '@grafana/data'; import TableModel from 'app/core/TableModel'; import { ExplorePanelData } from 'app/types'; @@ -314,112 +303,6 @@ describe('decorateWithTableResult', () => { }); describe('decorateWithLogsResult', () => { - it('should correctly transform logs dataFrames', () => { - const { logs } = getTestContext(); - const request = { timezone: 'utc', intervalMs: 60000 } as unknown as DataQueryRequest; - const panelData = createExplorePanelData({ logsFrames: [logs], request }); - expect(decorateWithLogsResult()(panelData).logsResult).toEqual({ - hasUniqueLabels: false, - meta: [], - rows: [ - { - rowIndex: 0, - dataFrame: logs, - entry: 'this is a message', - entryFieldIndex: 3, - hasAnsi: false, - hasUnescapedContent: false, - labels: {}, - logLevel: 'unknown', - raw: 'this is a message', - searchWords: [], - timeEpochMs: 100, - timeEpochNs: '100000002', - timeFromNow: 'fromNow() jest mocked', - timeLocal: 'format() jest mocked', - timeUtc: 'format() jest mocked', - uid: '0', - uniqueLabels: {}, - }, - { - rowIndex: 2, - dataFrame: logs, - entry: 'third', - entryFieldIndex: 3, - hasAnsi: false, - hasUnescapedContent: false, - labels: {}, - logLevel: 'unknown', - raw: 'third', - searchWords: [], - timeEpochMs: 100, - timeEpochNs: '100000001', - timeFromNow: 'fromNow() jest mocked', - timeLocal: 'format() jest mocked', - timeUtc: 'format() jest mocked', - uid: '2', - uniqueLabels: {}, - }, - { - rowIndex: 1, - dataFrame: logs, - entry: 'second message', - entryFieldIndex: 3, - hasAnsi: false, - hasUnescapedContent: false, - labels: {}, - logLevel: 'unknown', - raw: 'second message', - searchWords: [], - timeEpochMs: 100, - timeEpochNs: '100000000', - timeFromNow: 'fromNow() jest mocked', - timeLocal: 'format() jest mocked', - timeUtc: 'format() jest mocked', - uid: '1', - uniqueLabels: {}, - }, - ], - series: [ - { - name: 'unknown', - length: 1, - fields: [ - { name: 'Time', type: 'time', values: new ArrayVector([0]), config: {} }, - { - name: 'Value', - type: 'number', - labels: undefined, - values: new ArrayVector([3]), - config: { - color: { - fixedColor: '#8e8e8e', - mode: FieldColorModeId.Fixed, - }, - min: 0, - decimals: 0, - unit: undefined, - custom: { - drawStyle: GraphDrawStyle.Bars, - barAlignment: 0, - barMaxWidth: 5, - barWidthFactor: 0.9, - lineColor: '#8e8e8e', - fillColor: '#8e8e8e', - pointColor: '#8e8e8e', - lineWidth: 0, - fillOpacity: 100, - stacking: { mode: StackingMode.Normal, group: 'A' }, - }, - }, - }, - ], - }, - ], - visibleRange: undefined, - }); - }); - it('returns null if passed empty array', () => { const panelData = createExplorePanelData({ logsFrames: [] }); expect(decorateWithLogsResult()(panelData).logsResult).toBeNull(); diff --git a/public/app/features/explore/utils/supplementaryQueries.test.ts b/public/app/features/explore/utils/supplementaryQueries.test.ts new file mode 100644 index 00000000000..b90b981f09e --- /dev/null +++ b/public/app/features/explore/utils/supplementaryQueries.test.ts @@ -0,0 +1,357 @@ +import { flatten } from 'lodash'; +import { from, Observable } from 'rxjs'; + +import { + DataFrame, + DataQueryRequest, + DataQueryResponse, + DataSourceApi, + DataSourceWithSupplementaryQueriesSupport, + FieldType, + LoadingState, + LogLevel, + LogsVolumeType, + MutableDataFrame, + SupplementaryQueryType, + toDataFrame, +} from '@grafana/data'; +import { getDataSourceSrv } from '@grafana/runtime'; +import { DataQuery } from '@grafana/schema'; + +import { MockDataSourceApi } from '../../../../test/mocks/datasource_srv'; +import { MockDataQueryRequest, MockQuery } from '../../../../test/mocks/query'; +import { ExplorePanelData } from '../../../types'; +import { mockExplorePanelData } from '../__mocks__/data'; + +import { getSupplementaryQueryProvider } from './supplementaryQueries'; + +class MockDataSourceWithSupplementaryQuerySupport + extends MockDataSourceApi + implements DataSourceWithSupplementaryQueriesSupport +{ + private supplementaryQueriesResults: Record = { + [SupplementaryQueryType.LogsVolume]: undefined, + [SupplementaryQueryType.LogsSample]: undefined, + }; + + withSupplementaryQuerySupport(type: SupplementaryQueryType, data: DataFrame[]) { + this.supplementaryQueriesResults[type] = data; + return this; + } + + getDataProvider( + type: SupplementaryQueryType, + request: DataQueryRequest + ): Observable | undefined { + const data = this.supplementaryQueriesResults[type]; + if (data) { + return from([ + { state: LoadingState.Loading, data: [] }, + { state: LoadingState.Done, data }, + ]); + } + return undefined; + } + + getSupplementaryQuery(type: SupplementaryQueryType, query: DataQuery): DataQuery | undefined { + return query; + } + + getSupportedSupplementaryQueryTypes(): SupplementaryQueryType[] { + return Object.values(SupplementaryQueryType).filter((type) => this.supplementaryQueriesResults[type]); + } +} + +const createSupplementaryQueryResponse = (type: SupplementaryQueryType, id: string) => { + return [ + toDataFrame({ + refId: `1-${type}-${id}`, + fields: [{ name: 'value', type: FieldType.string, values: [1] }], + meta: { + custom: { + logsVolumeType: LogsVolumeType.FullRange, + }, + }, + }), + toDataFrame({ + refId: `2-${type}-${id}`, + fields: [{ name: 'value', type: FieldType.string, values: [2] }], + meta: { + custom: { + logsVolumeType: LogsVolumeType.FullRange, + }, + }, + }), + ]; +}; + +const mockRow = (refId: string) => { + return { + rowIndex: 0, + entryFieldIndex: 0, + dataFrame: new MutableDataFrame({ refId, fields: [{ name: 'A', values: [] }] }), + entry: '', + hasAnsi: false, + hasUnescapedContent: false, + labels: {}, + logLevel: LogLevel.info, + raw: '', + timeEpochMs: 0, + timeEpochNs: '0', + timeFromNow: '', + timeLocal: '', + timeUtc: '', + uid: '1', + }; +}; + +const mockExploreDataWithLogs = () => + mockExplorePanelData({ + logsResult: { + rows: [mockRow('0'), mockRow('1')], + visibleRange: { from: 0, to: 1 }, + bucketSize: 1000, + }, + }); + +const datasources: DataSourceApi[] = [ + new MockDataSourceWithSupplementaryQuerySupport('logs-volume-a').withSupplementaryQuerySupport( + SupplementaryQueryType.LogsVolume, + createSupplementaryQueryResponse(SupplementaryQueryType.LogsVolume, 'logs-volume-a') + ), + new MockDataSourceWithSupplementaryQuerySupport('logs-volume-b').withSupplementaryQuerySupport( + SupplementaryQueryType.LogsVolume, + createSupplementaryQueryResponse(SupplementaryQueryType.LogsVolume, 'logs-volume-b') + ), + new MockDataSourceWithSupplementaryQuerySupport('logs-sample-a').withSupplementaryQuerySupport( + SupplementaryQueryType.LogsSample, + createSupplementaryQueryResponse(SupplementaryQueryType.LogsSample, 'logs-sample-a') + ), + new MockDataSourceWithSupplementaryQuerySupport('logs-sample-b').withSupplementaryQuerySupport( + SupplementaryQueryType.LogsSample, + createSupplementaryQueryResponse(SupplementaryQueryType.LogsSample, 'logs-sample-b') + ), + new MockDataSourceApi('no-data-providers'), + new MockDataSourceApi('no-data-providers-2'), + new MockDataSourceApi('mixed').setupMixed(true), +]; + +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + getDataSourceSrv: () => { + return { + get: async ({ uid }: { uid: string }) => datasources.find((ds) => ds.name === uid) || undefined, + }; + }, +})); + +const setup = async (rootDataSource: string, type: SupplementaryQueryType, targetSources?: string[]) => { + const rootDataSourceApiMock = await getDataSourceSrv().get({ uid: rootDataSource }); + + targetSources = targetSources || [rootDataSource]; + + const requestMock = new MockDataQueryRequest({ + targets: targetSources.map((source, i) => new MockQuery(`${i}`, 'a', { uid: source })), + }); + const explorePanelDataMock: Observable = mockExploreDataWithLogs(); + + return getSupplementaryQueryProvider(rootDataSourceApiMock, type, requestMock, explorePanelDataMock); +}; + +const assertDataFrom = (type: SupplementaryQueryType, ...datasources: string[]) => { + return flatten( + datasources.map((name: string) => { + return [{ refId: `1-${type}-${name}` }, { refId: `2-${type}-${name}` }]; + }) + ); +}; + +const assertDataFromLogsResults = () => { + return [{ meta: { custom: { logsVolumeType: LogsVolumeType.Limited } } }]; +}; + +describe('SupplementaryQueries utils', function () { + describe('Non-mixed data source', function () { + it('Returns result from the provider', async () => { + const testProvider = await setup('logs-volume-a', SupplementaryQueryType.LogsVolume); + + await expect(testProvider).toEmitValuesWith((received) => { + expect(received).toMatchObject([ + { data: [], state: LoadingState.Loading }, + { + data: assertDataFrom(SupplementaryQueryType.LogsVolume, 'logs-volume-a'), + state: LoadingState.Done, + }, + ]); + }); + }); + it('Uses fallback for logs volume', async () => { + const testProvider = await setup('no-data-providers', SupplementaryQueryType.LogsVolume); + + await expect(testProvider).toEmitValuesWith((received) => { + expect(received).toMatchObject([ + { + data: assertDataFromLogsResults(), + state: LoadingState.Done, + }, + ]); + }); + }); + it('Does not use a fallback for logs sample', async () => { + const testProvider = await setup('no-data-providers', SupplementaryQueryType.LogsSample); + await expect(testProvider).toEmitValuesWith((received) => { + expect(received).toMatchObject([ + { + state: LoadingState.NotStarted, + }, + ]); + }); + }); + }); + + describe('Mixed data source', function () { + describe('Logs volume', function () { + describe('All data sources support full range logs volume', function () { + it('Merges all data frames into a single response', async () => { + const testProvider = await setup('mixed', SupplementaryQueryType.LogsVolume, [ + 'logs-volume-a', + 'logs-volume-b', + ]); + await expect(testProvider).toEmitValuesWith((received) => { + expect(received).toMatchObject([ + { data: [], state: LoadingState.Loading }, + { + data: assertDataFrom(SupplementaryQueryType.LogsVolume, 'logs-volume-a'), + state: LoadingState.Done, + }, + { + data: assertDataFrom(SupplementaryQueryType.LogsVolume, 'logs-volume-a', 'logs-volume-b'), + state: LoadingState.Done, + }, + ]); + }); + }); + }); + + describe('All data sources do not support full range logs volume', function () { + it('Creates single fallback result', async () => { + const testProvider = await setup('mixed', SupplementaryQueryType.LogsVolume, [ + 'no-data-providers', + 'no-data-providers-2', + ]); + + await expect(testProvider).toEmitValuesWith((received) => { + expect(received).toMatchObject([ + { + data: assertDataFromLogsResults(), + state: LoadingState.Done, + }, + { + data: [...assertDataFromLogsResults(), ...assertDataFromLogsResults()], + state: LoadingState.Done, + }, + ]); + }); + }); + }); + + describe('Some data sources support full range logs volume, while others do not', function () { + it('Creates merged result containing full range and limited logs volume', async () => { + const testProvider = await setup('mixed', SupplementaryQueryType.LogsVolume, [ + 'logs-volume-a', + 'no-data-providers', + 'logs-volume-b', + 'no-data-providers-2', + ]); + await expect(testProvider).toEmitValuesWith((received) => { + expect(received).toMatchObject([ + { + data: [], + state: LoadingState.Loading, + }, + { + data: assertDataFrom(SupplementaryQueryType.LogsVolume, 'logs-volume-a'), + state: LoadingState.Done, + }, + { + data: [ + ...assertDataFrom(SupplementaryQueryType.LogsVolume, 'logs-volume-a'), + ...assertDataFromLogsResults(), + ], + state: LoadingState.Done, + }, + { + data: [ + ...assertDataFrom(SupplementaryQueryType.LogsVolume, 'logs-volume-a'), + ...assertDataFromLogsResults(), + ...assertDataFrom(SupplementaryQueryType.LogsVolume, 'logs-volume-b'), + ], + state: LoadingState.Done, + }, + ]); + }); + }); + }); + }); + + describe('Logs sample', function () { + describe('All data sources support logs sample', function () { + it('Merges all responses into single result', async () => { + const testProvider = await setup('mixed', SupplementaryQueryType.LogsSample, [ + 'logs-sample-a', + 'logs-sample-b', + ]); + await expect(testProvider).toEmitValuesWith((received) => { + expect(received).toMatchObject([ + { data: [], state: LoadingState.Loading }, + { + data: assertDataFrom(SupplementaryQueryType.LogsSample, 'logs-sample-a'), + state: LoadingState.Done, + }, + { + data: assertDataFrom(SupplementaryQueryType.LogsSample, 'logs-sample-a', 'logs-sample-b'), + state: LoadingState.Done, + }, + ]); + }); + }); + }); + + describe('All data sources do not support full range logs volume', function () { + it('Does not provide fallback result', async () => { + const testProvider = await setup('mixed', SupplementaryQueryType.LogsSample, [ + 'no-data-providers', + 'no-data-providers-2', + ]); + await expect(testProvider).toEmitValuesWith((received) => { + expect(received).toMatchObject([{ state: LoadingState.NotStarted, data: [] }]); + }); + }); + }); + + describe('Some data sources support full range logs volume, while others do not', function () { + it('Returns results only for data sources supporting logs sample', async () => { + const testProvider = await setup('mixed', SupplementaryQueryType.LogsSample, [ + 'logs-sample-a', + 'no-data-providers', + 'logs-sample-b', + 'no-data-providers-2', + ]); + await expect(testProvider).toEmitValuesWith((received) => { + expect(received).toMatchObject([ + { data: [], state: LoadingState.Loading }, + { + data: assertDataFrom(SupplementaryQueryType.LogsSample, 'logs-sample-a'), + state: LoadingState.Done, + }, + { + data: assertDataFrom(SupplementaryQueryType.LogsSample, 'logs-sample-a', 'logs-sample-b'), + state: LoadingState.Done, + }, + ]); + }); + }); + }); + }); + }); +}); diff --git a/public/app/features/explore/utils/supplementaryQueries.ts b/public/app/features/explore/utils/supplementaryQueries.ts index bb00172523f..cc7c75c4f1a 100644 --- a/public/app/features/explore/utils/supplementaryQueries.ts +++ b/public/app/features/explore/utils/supplementaryQueries.ts @@ -1,6 +1,23 @@ -import { SupplementaryQueryType } from '@grafana/data'; +import { cloneDeep, groupBy } from 'lodash'; +import { distinct, from, mergeMap, Observable, of } from 'rxjs'; +import { scan } from 'rxjs/operators'; + +import { + DataQuery, + DataQueryRequest, + DataQueryResponse, + DataSourceApi, + hasSupplementaryQuerySupport, + LoadingState, + LogsVolumeCustomMetaData, + LogsVolumeType, + SupplementaryQueryType, +} from '@grafana/data'; +import { getDataSourceSrv } from '@grafana/runtime'; +import { makeDataFramesForLogs } from 'app/core/logsModel'; import store from 'app/core/store'; -import { SupplementaryQueries } from 'app/types'; +import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource'; +import { ExplorePanelData, SupplementaryQueries } from 'app/types'; export const supplementaryQueryTypes: SupplementaryQueryType[] = [ SupplementaryQueryType.LogsVolume, @@ -49,3 +66,156 @@ export const loadSupplementaryQueries = (): SupplementaryQueries => { } return supplementaryQueries; }; + +const createFallbackLogVolumeProvider = ( + explorePanelData: Observable, + queryTargets: DataQuery[], + datasourceName: string +): Observable => { + return new Observable((observer) => { + explorePanelData.subscribe((exploreData) => { + if ( + exploreData.logsResult && + exploreData.logsResult.rows && + exploreData.logsResult.visibleRange && + exploreData.logsResult.bucketSize !== undefined && + exploreData.state === LoadingState.Done + ) { + const bucketSize = exploreData.logsResult.bucketSize; + const targetRefIds = queryTargets.map((query) => query.refId); + const rowsByRefId = groupBy(exploreData.logsResult.rows, 'dataFrame.refId'); + targetRefIds.forEach((refId) => { + if (rowsByRefId[refId]?.length) { + const series = makeDataFramesForLogs(rowsByRefId[refId], bucketSize); + const logVolumeCustomMetaData: LogsVolumeCustomMetaData = { + logsVolumeType: LogsVolumeType.Limited, + absoluteRange: exploreData.logsResult?.visibleRange!, + datasourceName, + sourceQuery: queryTargets.find((query) => query.refId === refId)!, + }; + + observer.next({ + data: series.map((d) => { + const custom = d.meta?.custom || {}; + return { + ...d, + meta: { + custom: { + ...custom, + ...logVolumeCustomMetaData, + }, + }, + }; + }), + state: exploreData.state, + }); + } + }); + observer.complete(); + } + }); + }); +}; + +const getSupplementaryQueryFallback = ( + type: SupplementaryQueryType, + explorePanelData: Observable, + queryTargets: DataQuery[], + datasourceName: string +) => { + if (type === SupplementaryQueryType.LogsVolume) { + return createFallbackLogVolumeProvider(explorePanelData, queryTargets, datasourceName); + } else { + return of({ + data: [], + state: LoadingState.NotStarted, + }); + } +}; + +export const getSupplementaryQueryProvider = ( + datasourceInstance: DataSourceApi, + type: SupplementaryQueryType, + request: DataQueryRequest, + explorePanelData: Observable +): Observable | undefined => { + if (hasSupplementaryQuerySupport(datasourceInstance, type)) { + return datasourceInstance.getDataProvider(type, request); + } else if (datasourceInstance.meta?.mixed === true) { + const queries = request.targets.filter((t) => { + return t.datasource?.uid !== MIXED_DATASOURCE_NAME; + }); + // Build groups of queries to run in parallel + const sets: { [key: string]: DataQuery[] } = groupBy(queries, 'datasource.uid'); + const mixed: Array<{ datasource: Promise; targets: DataQuery[] }> = []; + + for (const key in sets) { + const targets = sets[key]; + mixed.push({ + datasource: getDataSourceSrv().get(targets[0].datasource, request.scopedVars), + targets, + }); + } + + return from(mixed).pipe( + mergeMap((query, i) => { + return from(query.datasource).pipe( + mergeMap((ds) => { + const dsRequest = cloneDeep(request); + dsRequest.requestId = `mixed-${type}-${i}-${dsRequest.requestId || ''}`; + dsRequest.targets = query.targets; + + if (hasSupplementaryQuerySupport(ds, type)) { + const dsProvider = ds.getDataProvider(type, dsRequest); + if (dsProvider) { + // 1) It provides data for current request - use the provider + return dsProvider; + } else { + // 2) It doesn't provide data for current request -> return nothing + return of({ + data: [], + state: LoadingState.NotStarted, + }); + } + } else { + // 3) Data source doesn't support the supplementary query -> use fallback + // the fallback cannot determine data availability based on request, it + // works on the results once they are available so it never uses the cache + return getSupplementaryQueryFallback(type, explorePanelData, query.targets, ds.name); + } + }) + ); + }), + scan( + (acc, next) => { + if (acc.error || next.state === LoadingState.NotStarted) { + return acc; + } + + if (next.state === LoadingState.Loading && acc.state === LoadingState.NotStarted) { + return { + ...acc, + state: LoadingState.Loading, + }; + } + + if (next.state && next.state !== LoadingState.Done) { + return acc; + } + + return { + ...acc, + data: [...acc.data, ...next.data], + state: LoadingState.Done, + }; + }, + { data: [], state: LoadingState.NotStarted } + ), + distinct() + ); + } else { + // Create a fallback to results based logs volume + return getSupplementaryQueryFallback(type, explorePanelData, request.targets, datasourceInstance.name); + } + return undefined; +}; diff --git a/public/test/mocks/datasource_srv.ts b/public/test/mocks/datasource_srv.ts index 90c182b657c..af815da4d90 100644 --- a/public/test/mocks/datasource_srv.ts +++ b/public/test/mocks/datasource_srv.ts @@ -60,6 +60,12 @@ export class MockDataSourceApi extends DataSourceApi { testDatasource() { return Promise.resolve(); } + + setupMixed(value: boolean) { + this.meta = this.meta || {}; + this.meta.mixed = value; + return this; + } } export class MockObservableDataSourceApi extends DataSourceApi { diff --git a/public/test/mocks/query.ts b/public/test/mocks/query.ts new file mode 100644 index 00000000000..92dcec063fb --- /dev/null +++ b/public/test/mocks/query.ts @@ -0,0 +1,30 @@ +import { CoreApp, DataQueryRequest, getDefaultTimeRange } from '@grafana/data'; +import { DataQuery, DataSourceRef } from '@grafana/schema'; + +export class MockQuery implements DataQuery { + refId: string; + testQuery: string; + datasource?: DataSourceRef; + + constructor(refId = 'A', testQuery = '', datasourceRef?: DataSourceRef) { + this.refId = refId; + this.testQuery = testQuery; + this.datasource = datasourceRef; + } +} + +export class MockDataQueryRequest implements DataQueryRequest { + app = CoreApp.Unknown; + interval = ''; + intervalMs = 0; + range = getDefaultTimeRange(); + requestId = '1'; + scopedVars = {}; + startTime = 0; + targets: MockQuery[]; + timezone = 'utc'; + + constructor({ targets }: { targets: MockQuery[] }) { + this.targets = targets || []; + } +} From 20b79b41ebfe79bc3c49d87eb4eea1dda2e188c8 Mon Sep 17 00:00:00 2001 From: Timur Olzhabayev Date: Tue, 7 Mar 2023 15:16:27 +0100 Subject: [PATCH 026/288] Feat: Changing link destination for get more plugins (#63517) * Changing link destination for get more plugins * Changing codeowners to plugins platform frontend * Remove unused variable --- .github/CODEOWNERS | 2 +- .../components/DataSourceCategories.tsx | 16 +++++++++------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index bf788f4a3c5..d5e76118507 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -354,7 +354,7 @@ lerna.json @grafana/frontend-ops /public/app/features/connections/ @grafana/plugins-platform-frontend /public/app/features/correlations/ @grafana/explore-squad /public/app/features/dashboard/ @grafana/dashboards-squad -/public/app/features/datasources/ @grafana/user-essentials +/public/app/features/datasources/ @grafana/plugins-platform-frontend /public/app/features/dimensions/ @grafana/dataviz-squad /public/app/features/dataframe-import/ @grafana/grafana-bi-squad /public/app/features/datasource-drawer/ @grafana/grafana-bi-squad diff --git a/public/app/features/datasources/components/DataSourceCategories.tsx b/public/app/features/datasources/components/DataSourceCategories.tsx index fb15338e207..812dcfdbe65 100644 --- a/public/app/features/datasources/components/DataSourceCategories.tsx +++ b/public/app/features/datasources/components/DataSourceCategories.tsx @@ -1,9 +1,12 @@ import React from 'react'; import { DataSourcePluginMeta } from '@grafana/data'; +import { config } from '@grafana/runtime'; import { LinkButton } from '@grafana/ui'; import { DataSourcePluginCategory } from 'app/types'; +import { ROUTES } from '../../connections/constants'; + import { DataSourceTypeCardList } from './DataSourceTypeCardList'; export type Props = { @@ -15,6 +18,10 @@ export type Props = { }; export function DataSourceCategories({ categories, onClickDataSourceType }: Props) { + const moreDataSourcesLink = config.featureToggles.dataConnectionsConsole + ? `${ROUTES.ConnectData}?cat=data-source` + : '/plugins?filterBy=all&filterByType=datasource&utm_source=grafana_add_ds'; + return ( <> {/* Categories */} @@ -29,13 +36,8 @@ export function DataSourceCategories({ categories, onClickDataSourceType }: Prop {/* Find more */}
- - Find more data source plugins on grafana.com + + Find more data source plugins
From 9276f1138838c137e33b163a57d50941a9b52a0b Mon Sep 17 00:00:00 2001 From: Dan Cech Date: Tue, 7 Mar 2023 09:24:04 -0500 Subject: [PATCH 027/288] Chore: Add stat for remote cache config (#64276) * add stat for remote cache config --- pkg/infra/usagestats/statscollector/service.go | 3 +++ pkg/infra/usagestats/statscollector/service_test.go | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/pkg/infra/usagestats/statscollector/service.go b/pkg/infra/usagestats/statscollector/service.go index feaf31bdd81..b14e1242374 100644 --- a/pkg/infra/usagestats/statscollector/service.go +++ b/pkg/infra/usagestats/statscollector/service.go @@ -173,6 +173,9 @@ func (s *Service) collectSystemStats(ctx context.Context) (map[string]interface{ } m["stats.avg_auth_token_per_user.count"] = avgAuthTokensPerUser + if s.cfg.RemoteCacheOptions != nil && s.cfg.RemoteCacheOptions.Name != "" { + m["stats.remote_cache."+s.cfg.RemoteCacheOptions.Name+".count"] = 1 + } m["stats.packaging."+s.cfg.Packaging+".count"] = 1 m["stats.distributor."+s.cfg.ReportingDistributor+".count"] = 1 diff --git a/pkg/infra/usagestats/statscollector/service_test.go b/pkg/infra/usagestats/statscollector/service_test.go index 3d35e591ee3..7f298de52e9 100644 --- a/pkg/infra/usagestats/statscollector/service_test.go +++ b/pkg/infra/usagestats/statscollector/service_test.go @@ -152,6 +152,9 @@ func TestCollectingUsageStats(t *testing.T) { AuthProxyEnabled: true, Packaging: "deb", ReportingDistributor: "hosted-grafana", + RemoteCacheOptions: &setting.RemoteCacheOptions{ + Name: "database", + }, }, sqlStore, statsService, withDatasources(mockDatasourceService{datasources: expectedDataSources})) @@ -179,6 +182,7 @@ func TestCollectingUsageStats(t *testing.T) { assert.EqualValues(t, 11, metrics["stats.data_keys.count"]) assert.EqualValues(t, 3, metrics["stats.active_data_keys.count"]) assert.EqualValues(t, 5, metrics["stats.public_dashboards.count"]) + assert.EqualValues(t, 1, metrics["stats.remote_cache.database.count"]) assert.InDelta(t, int64(65), metrics["stats.uptime"], 6) } From e5f6c80379e48df9aafd613e7f717140c4c17d97 Mon Sep 17 00:00:00 2001 From: Joao Silva <100691367+JoaoSilvaGrafana@users.noreply.github.com> Date: Tue, 7 Mar 2023 15:29:01 +0100 Subject: [PATCH 028/288] Stat Panel: Fix issue with clipping text values (#64300) --- .../components/BigValue/BigValueLayout.tsx | 19 ++++++++++++++----- packages/grafana-ui/src/utils/measureText.ts | 15 +++++++++++---- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/packages/grafana-ui/src/components/BigValue/BigValueLayout.tsx b/packages/grafana-ui/src/components/BigValue/BigValueLayout.tsx index 9d2d38d3970..7f0e7dc8d46 100644 --- a/packages/grafana-ui/src/components/BigValue/BigValueLayout.tsx +++ b/packages/grafana-ui/src/components/BigValue/BigValueLayout.tsx @@ -12,6 +12,7 @@ import { BigValueColorMode, Props, BigValueJustifyMode, BigValueTextMode } from const LINE_HEIGHT = 1.2; const MAX_TITLE_SIZE = 30; +const VALUE_FONT_WEIGHT = 500; export abstract class BigValueLayout { titleFontSize: number; @@ -76,7 +77,7 @@ export abstract class BigValueLayout { getValueStyles(): CSSProperties { const styles: CSSProperties = { fontSize: this.valueFontSize, - fontWeight: 500, + fontWeight: VALUE_FONT_WEIGHT, lineHeight: LINE_HEIGHT, position: 'relative', zIndex: 1, @@ -220,7 +221,9 @@ export class WideNoChartLayout extends BigValueLayout { this.valueToAlignTo, this.maxTextWidth * valueWidthPercent, this.maxTextHeight, - LINE_HEIGHT + LINE_HEIGHT, + undefined, + VALUE_FONT_WEIGHT ); } @@ -292,7 +295,9 @@ export class WideWithChartLayout extends BigValueLayout { this.valueToAlignTo, this.maxTextWidth * valueWidthPercent, this.maxTextHeight * chartHeightPercent, - LINE_HEIGHT + LINE_HEIGHT, + undefined, + VALUE_FONT_WEIGHT ); } } @@ -346,7 +351,9 @@ export class StackedWithChartLayout extends BigValueLayout { this.valueToAlignTo, this.maxTextWidth, this.maxTextHeight - this.chartHeight - titleHeight, - LINE_HEIGHT + LINE_HEIGHT, + undefined, + VALUE_FONT_WEIGHT ); } @@ -398,7 +405,9 @@ export class StackedWithNoChartLayout extends BigValueLayout { this.valueToAlignTo, this.maxTextWidth, this.maxTextHeight - titleHeight, - LINE_HEIGHT + LINE_HEIGHT, + undefined, + VALUE_FONT_WEIGHT ); } diff --git a/packages/grafana-ui/src/utils/measureText.ts b/packages/grafana-ui/src/utils/measureText.ts index 7c3bf5397c1..edfc8b2555c 100644 --- a/packages/grafana-ui/src/utils/measureText.ts +++ b/packages/grafana-ui/src/utils/measureText.ts @@ -16,8 +16,8 @@ export function getCanvasContext() { /** * @beta */ -export function measureText(text: string, fontSize: number): TextMetrics { - const fontStyle = `${fontSize}px 'Inter'`; +export function measureText(text: string, fontSize: number, fontWeight = 400): TextMetrics { + const fontStyle = `${fontWeight} ${fontSize}px 'Inter'`; const cacheKey = text + fontStyle; const fromCache = cache.get(cacheKey); @@ -45,9 +45,16 @@ export function measureText(text: string, fontSize: number): TextMetrics { /** * @beta */ -export function calculateFontSize(text: string, width: number, height: number, lineHeight: number, maxSize?: number) { +export function calculateFontSize( + text: string, + width: number, + height: number, + lineHeight: number, + maxSize?: number, + fontWeight?: number +) { // calculate width in 14px - const textSize = measureText(text, 14); + const textSize = measureText(text, 14, fontWeight); // how much bigger than 14px can we make it while staying within our width constraints const fontSizeBasedOnWidth = (width / (textSize.width + 2)) * 14; const fontSizeBasedOnHeight = height / lineHeight; From a31e18f6e33c857b563b38d9423cfda7eacd84eb Mon Sep 17 00:00:00 2001 From: Christopher Moyer <35463610+chri2547@users.noreply.github.com> Date: Tue, 7 Mar 2023 08:36:48 -0600 Subject: [PATCH 029/288] docs: removes doc that has moved to the cloud repo (#64153) removes doc that has moved to the cloud repo --- .../index.md | 228 ------------------ 1 file changed, 228 deletions(-) delete mode 100644 docs/sources/setup-grafana/configure-security/configure-private-datasource-connect/index.md diff --git a/docs/sources/setup-grafana/configure-security/configure-private-datasource-connect/index.md b/docs/sources/setup-grafana/configure-security/configure-private-datasource-connect/index.md deleted file mode 100644 index d91e9f75588..00000000000 --- a/docs/sources/setup-grafana/configure-security/configure-private-datasource-connect/index.md +++ /dev/null @@ -1,228 +0,0 @@ ---- -title: Configure Grafana private data source connect -weight: 200 -_build: - list: false ---- - -# Configure Grafana private data source connect - -Private data source connect (PDC) enables you to securely connect your Grafana Cloud stack to data sources hosted on a private network. - -> **Note:** Private data source connect is available as part of Grafana Cloud Pro, Advanced, and Enterprise. - -> **Note:** Private data source connect is currently in private preview. Grafana Labs offers support on a best-effort basis, and breaking changes might occur prior to the feature being made generally available. - -Observability data is often located within private networks such as on-premise networks and Virtual Private Clouds (VPCs) hosted by AWS, Azure, Google Cloud Platform, or other public cloud providers. For example, you might host your Splunk or Elasticsearch service on your private network, or you might want to visualize data from Amazon RDS hosted in a VPC. - -By using private data source connect, you can query data that lives within your private network without opening your network to inbound traffic from Grafana Cloud. Queries and data are encrypted from the PDC agent to the user’s browser. - -## Key features - -Private data source connect routes queries and responses between your Grafana Cloud stack and your private data source through an agent deployed in your network. - -![Private Data Source Connect diagram](/media/docs/grafana/grafana-pdc-diagram-1.png) - -- The SSH client running in your network is configured with reverse dynamic forwarding* (\_the* [-R <port>](https://man.openbsd.org/ssh.1#R~2) _option_).\_ In this mode, SSH acts as a [SOCKS](https://en.wikipedia.org/wiki/SOCKS) proxy and forwards connections to destinations requested by Grafana. -- You can restrict the destinations reachable by Grafana Cloud over this tunnel using the [PermitRemoteOpen](https://man.openbsd.org/ssh_config.5#PermitRemoteOpen) SSH option. -- The monitoring and supervision of the SSH tunnel are delegated to an agent running inside your private network. At any time, you can shut off the agent, which terminates the connection. -- The agent running inside your private network will be a horizontally scalable component to ensure fault-tolerance. -- Traffic is encrypted all the way from the Grafana Cloud instance to the SSH client running in your private network. If the private data source supports encryption (for example, HTTPS), traffic will be end-to-end encrypted. -- In your Grafana Cloud instance, you will be able to configure compatible datasources to route requests through the SSH tunnel. Each data source is configured using the internal DNS name (for example, mysql.your.domain:3306), as if Grafana were running directly inside the private network. - -## Known limitations - -The preview version of PDC has the following known limitations: - -- You can connect each Grafana instance to just one private network. Once PDC is generally available, you can connect a single Grafana instance to multiple private networks (VPCs, on-premise networks, and so on). -- During early access, Grafana Labs’ engineering team will configure the connection in Grafana, and you will deploy an agent in your private network. Once PDC is generally available, you can set up and manage your own private data source connections. -- Private data source connect is available for the following data sources: - - Elasticsearch - - Graphite - - Influxdb - - Loki - - Opentsdb - - Parca - - Phlare - - Prometheus - - Tempo - - Jaeger - - Zipkin - -> **Note:** Because many data sources are maintained by community members, not all data sources work with private data source connect. - -## Set up a private data source connection - -To set up a private data source connection, deploy the Grafana PDC agent, configure which hosts and ports to allow on your network, and configure your data source with those ports. - -### Before you begin - -- You need the ability to deploy the PDC agent within your network. You can deploy it directly to a Linux or Windows server, or use a container management system like Docker or Kubernetes. -- Private data source connect is available in Grafana Cloud Pro, Cloud Advanced, and Cloud Enterprise. -- You need to know the local host name and port of the data source you would like to connect to, for example `loki:8080`. -- As with all data sources in Grafana, you need a set of credentials to access the data, for example, a username and password, or a token. Refer to the [documentation]({{< relref "../../../datasources/" >}}) for your data source to learn what credentials are needed. -- You need an administrator account for your Grafana Cloud organization. To learn more about Grafana Cloud permissions, refer to [Grafana Cloud user roles and permissions](/docs/grafana-cloud/authentication-and-permissions/cloud-roles/). - -### Steps - -To set up a private data source connection, follow these steps: - -1. In a terminal, create a new folder, navigate to it, and generate an SSH key using the following command: - - ``` - $ ssh-keygen -f ${SLUG} -N "" -t ed25519 - ``` - - The flags used for ssh-keygen command mean the following: - - - -f: The file name of the key file - - -N: The passphrase to use for the key pair, we want this empty - - -t: The encryption algorithm. - - This command generates two files: `${SLUG}` and `${SLUG}.pub`. You will send `${SLUG}.pub` to the Grafana team in the next step. - - You can find more optional SSH flags in the [SSH documentation](https://www.man7.org/linux/man-pages/man1/ssh-keygen.1.html). - -1. Open a support ticket or let your account team know that you would like to try private data source connect. In your ticket, provide the following information: - - - A list of the data sources on which you want to enable PDC. - - The Grafana Cloud stack you want to connect to your private network. - - The `${SLUG}.pub` file you generated in the previous step. - - Grafana Labs will send you a certificate and public CA. - -1. Connect to Grafana Cloud using the ssh or the pdc agent in the same directory as your private key, and the certificate and known_hosts file Grafana Labs provided to you. - - There are three connecting options: Kubernetes, SSH, or the PDC Agent Docker image. - - - **Option 1:** Using Kubernetes - - Create a Kubernetes secret with the private key and the certificate and known_hosts file Grafana Labs provided. - - ``` - $ kubectl create secret generic -n ${NAMESPACE} grafana-pdc-agent \ - --from-file=key=./${SLUG} \ - --from-file=known_hosts=./known_hosts \ - --from-file=cert.pub=./${SLUG}-cert.pub - ``` - - Generate a Kubernetes deployment to deploy the agent. - - ``` - SLUG=${SLUG} PDC_GATEWAY=${PDC_GATEWAY} NAMESPACE=${NAMESPACE} /bin/sh -c "$(curl -fsSL https://raw.githubusercontent.com/grafana/pdc-agent/main/production/kubernetes/install-agent.sh)" - kubectl apply -f deployment.yaml - ``` - - The following list contains the environment variables used in the previous commands: - - - ${PDC_GATEWAY}: The URL of the private data source connect in Grafana Cloud. The Grafana team will give you this URL. The URL follows the format `grafana-private-datasource-connect-.grafana.net` - - ${SLUG}: The name of the stack you want to connect to your data source. For example, the stack `test.grafana.net` has the slug `test.` - - ${NAMESPACE}: The Kubernetes namespace for the pdc-agent. - - - **Option 2:** Using SSH - - ``` - $ ssh -i ${SLUG} ${SLUG}@${PDC_GATEWAY} -p 22 -o UserKnownHostsFile=./known_hosts -o CertificateFile=${SLUG}-cert.pub -R 0 -vv - ``` - - The flags used in the ssh command are as follows: - - - -i ${SLUG}: The private key - - -p 22: The port to connect to - - -o [UserKnownHostsFile](https://man.openbsd.org/ssh_config.5#UserKnownHostsFile): The list of Grafana PDC servers to trust when establishing a connection for the first time - - -o [CertificateFile](https://man.openbsd.org/ssh_config.5#CertificateFile): Your public certificate - - -R 0: Runs ssh with remote port forwarding, which enables it to act as a socks server - - -vv (optional): Sets the verbosity to debug2, so hostnames can be seen. It can be set to -v, if this is not desired. - - -o [PermitRemoteOpen](https://man.openbsd.org/ssh_config.5#PermitRemoteOpen) (optional): This can be specified to restrict the destinations reachable by Grafana Cloud over this connection. - - Refer to the [OpenSSH documentation](https://linux.die.net/man/1/ssh) for a complete list of available ssh command flags. - - Additionally: - - - ${PDC_GATEWAY}: The URL of the private data source connect in Grafana Cloud. The Grafana team will give you this URL. The URL follows the format `private-datasource-connect-.grafana.net` - - ${SLUG}: The name of the stack you want to connect to your data source. For example, the stack `test.grafana.net` has the slug `test.` - - - **Option 3:** Using the [pdc-agent](https://github.com/grafana/pdc-agent) docker [image](https://hub.docker.com/r/grafana/pdc-agent/tags) - - ``` - docker run --rm --name pdc-agent -v $(pwd):/etc/keys grafana/pdc-agent:latest -i /etc/keys/${SLUG} ${SLUG}@${PDC_GATEWAY} -p 22 -o BatchMode=yes -o UserKnownHostsFile=/etc/keys/known_hosts -o CertificateFile=/etc/keys/${SLUG}-cert.pub -R 0 -v - ``` - - The flags used on this are a combination of: - - - –-rm: Remove the docker container when it exits - - --name pdc-agent: This names the docker process pdc-agent - - -v $(pwd):/etc/keys: Copies the working directory into the /etc/keys directory in the Docker container - - -i /etc/keys/${SLUG}: The private key - - -p 22: The port to connect to - - -o [BatchMode](https://man.openbsd.org/ssh_config.5#BatchMode): Skips the passphrase checking - - -o [UserKnownHostsFile](https://man.openbsd.org/ssh_config.5#UserKnownHostsFile): The list of Grafana PDC servers to trust when establishing a connection for the first time - - -o [CertificateFile](https://man.openbsd.org/ssh_config.5#CertificateFile): Your public certificate - - -R 0: Runs ssh with remote port forwarding (which allows it to act as a socks server) - - -v (optional): Sets the verbosity to debug. - - -o [PermitRemoteOpen](https://man.openbsd.org/ssh_config.5#PermitRemoteOpen) (optional): This can be specified to restrict the destinations reachable by Grafana Cloud over this connection. - -1. (Optional) For high availability, you can install additional instances of the agent on your network with the same configuration. - - These can be deployed to different regions, data centers, or providers as long as they are on the same network. - -## Configure a data source to use private data source connect - -After you have set up a private data source connection, set up a data source in Grafana to query your data. - -### Before you begin - -- Ensure the data source you want to connect to supports Private data source connect. Refer to [Known limitations](#known-limitations) for a list of supported data sources. -- [Set up a private data source connection](#set-up-a-private-data-source-connection) - -### Steps - -1. Follow the [Add a data source]({{< relref "../../../administration/data-source-management/#add-a-data-source" >}}) instructions. - -1. Enable the Secure Socks Proxy setting for your data source. - - > **Note:** If you are running Grafana v9.4.0, this setting is only available in Prometheus and Loki, but you can still enable PDC for all the data sources listed as available in this document. Reach out to the Grafana engineering team or support for assistance. - -1. In the URL field for your data source, use the same URL as if you were on your private network, instead of a public URL. - -1. Save, test, and query your data source as usual. - -### Troubleshooting - -If you have trouble connecting to your data source, check the list of destinations reachable by the PDC agent, which might be restricted using the [PermitRemoteOpen](https://man.openbsd.org/ssh_config.5#PermitRemoteOpen) SSH option. You can see this list in the agent’s configuration. - -## Audit activity on the PDC agent - -The PDC agent logs every connection attempt, whether it is successful or denied. You can use these logs to audit traffic and ensure that no unwanted actors are trying to query your data. - -The method you use to access and store the logs depends on where the agent is deployed, and your logging tool. - -The logs will be the standard debug logs from ssh, plus an introduction log when you’ve first connected to Grafana’s PDC Service. - -Once connected, the log will be: \ -`This is Grafana Private Datasource Connect!` - -Successful logs will look like this: - -``` -debug1: client_input_channel_open: ctype forwarded-tcpip rchan 1 win 2097152 max 32768 -debug1: client_request_forwarded_tcpip: listen localhost port 1234, originator ::1 port 61779 -debug1: channel 1: new [::1] -debug1: confirm forwarded-tcpip -debug1: connect_next: host 34.205.150.168 ([34.205.150.168]:443) in progress, fd=10 -debug1: channel 1: connected to 34.205.150.168 port 443 -debug1: channel 1: free: ::1, nchannels 2 -``` - -Failed connections could look like this: - -> **Note:** There are different reasons that may be logged for the failed reason. - -``` -debug1: client_input_channel_open: ctype forwarded-tcpip rchan 1 win 2097152 max 32768 -debug1: client_request_forwarded_tcpip: listen localhost port 1234, originator ::1 port 61917 -debug1: channel 1: new [::1] -debug1: confirm forwarded-tcpip -debug1: rdynamic_connect_finish: requested forward not permitted -debug1: channel 1: free: ::1, nchannels 2 -``` From 4a1c18abf64046a28a214dca18c86dc2d9aeacc2 Mon Sep 17 00:00:00 2001 From: Alexander Weaver Date: Tue, 7 Mar 2023 08:40:55 -0600 Subject: [PATCH 030/288] Alerting: Fix intermittency when seeding database in rule store tests (#64322) Force unique IDs when seeding database --- pkg/services/ngalert/store/alert_rule_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/services/ngalert/store/alert_rule_test.go b/pkg/services/ngalert/store/alert_rule_test.go index 1471109ba4a..fb03060a063 100644 --- a/pkg/services/ngalert/store/alert_rule_test.go +++ b/pkg/services/ngalert/store/alert_rule_test.go @@ -176,7 +176,7 @@ func TestIntegration_CountAlertRules(t *testing.T) { } func createRule(t *testing.T, store *DBstore) *models.AlertRule { - rule := models.AlertRuleGen(withIntervalMatching(store.Cfg.BaseInterval))() + rule := models.AlertRuleGen(withIntervalMatching(store.Cfg.BaseInterval), models.WithUniqueID())() err := store.SQLStore.WithDbSession(context.Background(), func(sess *db.Session) error { _, err := sess.Table(models.AlertRule{}).InsertOne(rule) if err != nil { From 0c0d63b830ecdda3b24f9742d9d3ca2e25ac0ff4 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Tue, 7 Mar 2023 14:53:50 +0000 Subject: [PATCH 031/288] Build: enable caching in eslint plugin (#64325) enable caching in eslint plugin --- scripts/webpack/webpack.dev.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/webpack/webpack.dev.js b/scripts/webpack/webpack.dev.js index c549a5275a1..89d6962f83c 100644 --- a/scripts/webpack/webpack.dev.js +++ b/scripts/webpack/webpack.dev.js @@ -88,8 +88,8 @@ module.exports = (env = {}) => }, }, }), - // next major version of ForkTsChecker is dropping support for ESLint new ESLintPlugin({ + cache: true, lintDirtyModulesOnly: true, // don't lint on start, only lint changed files extensions: ['.ts', '.tsx'], }), From 3b2d5bca3e0d7df78ab8a3041ce1503f6081edcc Mon Sep 17 00:00:00 2001 From: Miguel Alexandre Date: Tue, 7 Mar 2023 16:13:59 +0100 Subject: [PATCH 032/288] Docs: Include OTLP in the tracing references (#56806) --- docs/sources/setup-grafana/set-up-grafana-monitoring.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/sources/setup-grafana/set-up-grafana-monitoring.md b/docs/sources/setup-grafana/set-up-grafana-monitoring.md index 6634404fc9a..d520a4cba2e 100644 --- a/docs/sources/setup-grafana/set-up-grafana-monitoring.md +++ b/docs/sources/setup-grafana/set-up-grafana-monitoring.md @@ -14,13 +14,13 @@ weight: 800 # Set up Grafana monitoring -Grafana supports [Jaeger tracing](https://www.jaegertracing.io/). +Grafana supports tracing. -Grafana can emit Jaeger traces for its HTTP API endpoints and propagate Jaeger trace information to data sources. +Grafana can emit Jaeger or OpenTelemetry Protocol (OTLP) traces for its HTTP API endpoints and propagate Jaeger and [w3c Trace Context](https://www.w3.org/TR/trace-context/) trace information to compatible data sources. All HTTP endpoints are logged evenly (annotations, dashboard, tags, and so on). When a trace ID is propagated, it is reported with operation 'HTTP /datasources/proxy/:id/\*'. -Refer to [Configuration]({{< relref "configure-grafana/#tracingjaeger" >}}) for information about enabling Jaeger tracing. +Refer to [Configuration's OpenTelemetry section]({{< relref "configure-grafana/#tracingopentelemetry" >}}) for a reference of tracing options available in Grafana. ## View Grafana internal metrics From 1b6b5dd7e93f503247a05076b86e537471d99872 Mon Sep 17 00:00:00 2001 From: Ieva Date: Tue, 7 Mar 2023 15:14:36 +0000 Subject: [PATCH 033/288] AuthN: use the default login icon if no icon is specified (#64327) use the default login icon if empty string passed in --- .../core/components/Login/LoginServiceButtons.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/public/app/core/components/Login/LoginServiceButtons.tsx b/public/app/core/components/Login/LoginServiceButtons.tsx index 467405694ba..aee1e887ba0 100644 --- a/public/app/core/components/Login/LoginServiceButtons.tsx +++ b/public/app/core/components/Login/LoginServiceButtons.tsx @@ -32,44 +32,44 @@ const loginServices: () => LoginServices = () => { bgColor: '#e84d3c', enabled: oauthEnabled && Boolean(config.oauth.google), name: config.oauth?.google?.name || 'Google', - icon: config.oauth?.google?.icon ?? ('google' as const), + icon: config.oauth?.google?.icon || ('google' as const), }, azuread: { bgColor: '#2f2f2f', enabled: oauthEnabled && Boolean(config.oauth.azuread), name: config.oauth?.azuread?.name || 'Microsoft', - icon: config.oauth?.azuread?.icon ?? ('microsoft' as const), + icon: config.oauth?.azuread?.icon || ('microsoft' as const), }, github: { bgColor: '#464646', enabled: oauthEnabled && Boolean(config.oauth.github), name: config.oauth?.github?.name || 'GitHub', - icon: config.oauth?.github?.icon ?? ('github' as const), + icon: config.oauth?.github?.icon || ('github' as const), }, gitlab: { bgColor: '#fc6d26', enabled: oauthEnabled && Boolean(config.oauth.gitlab), name: config.oauth?.gitlab?.name || 'GitLab', - icon: config.oauth?.gitlab?.icon ?? ('gitlab' as const), + icon: config.oauth?.gitlab?.icon || ('gitlab' as const), }, grafanacom: { bgColor: '#262628', enabled: oauthEnabled && Boolean(config.oauth.grafana_com), name: config.oauth?.grafana_com?.name || 'Grafana.com', - icon: config.oauth?.grafana_com?.icon ?? ('grafana' as const), + icon: config.oauth?.grafana_com?.icon || ('grafana' as const), hrefName: 'grafana_com', }, okta: { bgColor: '#2f2f2f', enabled: oauthEnabled && Boolean(config.oauth.okta), name: config.oauth?.okta?.name || 'Okta', - icon: config.oauth?.okta?.icon ?? ('okta' as const), + icon: config.oauth?.okta?.icon || ('okta' as const), }, oauth: { bgColor: '#262628', enabled: oauthEnabled && Boolean(config.oauth.generic_oauth), name: config.oauth?.generic_oauth?.name || 'OAuth', - icon: config.oauth?.generic_oauth?.icon ?? ('signin' as const), + icon: config.oauth?.generic_oauth?.icon || ('signin' as const), hrefName: 'generic_oauth', }, }; From 380138f57be471b7e8ef7059e2302cc0af3d45f2 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Tue, 7 Mar 2023 07:36:17 -0800 Subject: [PATCH 034/288] Chore: update yarn lock in loadtest environment (#64278) * update yarn lock * remove loadtest-ts --------- Co-authored-by: Artur Wierzbicki --- .github/CODEOWNERS | 3 - devenv/docker/loadtest-ts/.babelrc | 10 - devenv/docker/loadtest-ts/.gitignore | 3 - devenv/docker/loadtest-ts/README.md | 14 - devenv/docker/loadtest-ts/package.json | 28 - devenv/docker/loadtest-ts/run.sh | 64 - .../scripts/prepareDashboardFileNames.ts | 24 - .../loadtest-ts/src/get-large-dashboard.ts | 116 - .../loadtest-ts/src/object-store-client.ts | 214 -- .../loadtest-ts/src/object-store-test.ts | 145 - devenv/docker/loadtest-ts/src/prepare-data.ts | 57 - devenv/docker/loadtest-ts/tsconfig.json | 26 - devenv/docker/loadtest-ts/webpack.config.js | 37 - devenv/docker/loadtest-ts/yarn.lock | 3167 ----------------- 14 files changed, 3908 deletions(-) delete mode 100644 devenv/docker/loadtest-ts/.babelrc delete mode 100644 devenv/docker/loadtest-ts/.gitignore delete mode 100644 devenv/docker/loadtest-ts/README.md delete mode 100644 devenv/docker/loadtest-ts/package.json delete mode 100755 devenv/docker/loadtest-ts/run.sh delete mode 100644 devenv/docker/loadtest-ts/scripts/prepareDashboardFileNames.ts delete mode 100644 devenv/docker/loadtest-ts/src/get-large-dashboard.ts delete mode 100644 devenv/docker/loadtest-ts/src/object-store-client.ts delete mode 100755 devenv/docker/loadtest-ts/src/object-store-test.ts delete mode 100644 devenv/docker/loadtest-ts/src/prepare-data.ts delete mode 100644 devenv/docker/loadtest-ts/tsconfig.json delete mode 100644 devenv/docker/loadtest-ts/webpack.config.js delete mode 100644 devenv/docker/loadtest-ts/yarn.lock diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index d5e76118507..181967464f9 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -158,9 +158,6 @@ /devenv/docker/blocks/loki* @grafana/observability-logs /devenv/docker/blocks/elastic* @grafana/observability-logs -# Performance tests -/devenv/docker/loadtest-ts/ @grafana/grafana-app-platform-squad - /devenv/bulk-dashboards/ @grafana/dashboards-squad /devenv/bulk_alerting_dashboards/ @grafana/alerting-squad-backend /devenv/create_docker_compose.sh @grafana/backend-platform diff --git a/devenv/docker/loadtest-ts/.babelrc b/devenv/docker/loadtest-ts/.babelrc deleted file mode 100644 index f34ccd9eb31..00000000000 --- a/devenv/docker/loadtest-ts/.babelrc +++ /dev/null @@ -1,10 +0,0 @@ -{ - "presets": [ - "@babel/env", - "@babel/typescript" - ], - "plugins": [ - "@babel/proposal-class-properties", - "@babel/proposal-object-rest-spread" - ] -} diff --git a/devenv/docker/loadtest-ts/.gitignore b/devenv/docker/loadtest-ts/.gitignore deleted file mode 100644 index f8d4b7b3dbe..00000000000 --- a/devenv/docker/loadtest-ts/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -scripts/tmp -dist/ -.yarn diff --git a/devenv/docker/loadtest-ts/README.md b/devenv/docker/loadtest-ts/README.md deleted file mode 100644 index 73075d22342..00000000000 --- a/devenv/docker/loadtest-ts/README.md +++ /dev/null @@ -1,14 +0,0 @@ -# Grafana load tests written in typescript - EXPERIMENTAL - -Runs load tests written in typescript and checks Grafana's performance using [k6](https://k6.io/) - -This is **experimental** - please consider adding new tests to devenv/docker/loadtest while we are testing the typescript approach! - - - -# How to run - -``` -yarn install -GRPC_TOKEN={REPLACE_WITH_SERVICE_ACCOUNT_ADMIN_TOKEN} ./run.sh test=object-store-test grpcAddress=127.0.0.1:10000 execution=local -``` diff --git a/devenv/docker/loadtest-ts/package.json b/devenv/docker/loadtest-ts/package.json deleted file mode 100644 index 20368fd20d3..00000000000 --- a/devenv/docker/loadtest-ts/package.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "private": true, - "license": "Apache-2.0", - "name": "@grafana/perf-tests", - "version": "9.4.0-pre", - "devDependencies": { - "@babel/core": "7.19.6", - "@babel/plugin-proposal-class-properties": "7.18.6", - "@babel/plugin-proposal-object-rest-spread": "7.19.4", - "@babel/preset-env": "7.19.4", - "@babel/preset-typescript": "7.18.6", - "@types/k6": "0.41.0", - "@types/shortid": "0.0.29", - "@types/webpack": "5.28.0", - "babel-loader": "9.1.0", - "shortid": "2.2.16", - "ts-node": "10.9.1", - "typescript": "4.8.4", - "webpack": "5.74.0", - "webpack-cli": "4.10.0", - "webpack-glob-entries": "1.0.1" - }, - "scripts": { - "build": "webpack", - "prepare-testdata": "yarn run prepare-testdata:object-store-test", - "prepare-testdata:object-store-test": "ts-node scripts/prepareDashboardFileNames.ts ../../dev-dashboards ./scripts/tmp/filenames.json" - } -} diff --git a/devenv/docker/loadtest-ts/run.sh b/devenv/docker/loadtest-ts/run.sh deleted file mode 100755 index bde41cb2876..00000000000 --- a/devenv/docker/loadtest-ts/run.sh +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/env bash - -if ((BASH_VERSINFO[0] < 4)); then - echo "Bash ver >= 4 is needed to run this script" - echo "Please upgrade your bash - run 'brew install bash' if you use Homebrew on MacOS" - exit 1; -fi - -declare -A cfg=( - [grpcToken]=$GRPC_TOKEN - [grpcAddress]="127.0.0.1:10000" - [execution]="local" - [test]="object-store-test" - [k6CloudToken]=$K6_CLOUD_TOKEN -) - -for ARGUMENT in "$@" -do - KEY=$(echo $ARGUMENT | cut -f1 -d=) - - KEY_LENGTH=${#KEY} - VALUE="${ARGUMENT:$KEY_LENGTH+1}" - cfg["$KEY"]="$VALUE" -done - -function usage() { - echo "$0 grpcAddress= grpcToken= execution= k6CloudToken= test= -- 'grpcAddress' is the address of Grafana gRPC server. 127.0.0.1:10000 is the default. -- 'grpcToken' is the service account admin token used for Grafana gRPC server authentication. -- 'execution' is the test execution mode; one of 'local', 'cloud-output', 'cloud'. 'local' is the default. -- 'k6CloudToken' is the k6 cloud token required for 'cloud-output' and 'cloud' execution modes. -- 'test' is the filepath of the test to execute relative to ./src, without the extension. example 'object-store-test'" - exit 0 -} - -if [ "${cfg[grpcToken]}" == "" ]; then - usage -fi - - -if [ "${cfg[execution]}" == "cloud" ]; then - echo "cloud execution mode is not yet implemented" - exit 0 -elif [ "${cfg[execution]}" == "cloud-output" ]; then - if [ "${cfg[k6CloudToken]}" == "" ]; then - usage - fi -elif [ "${cfg[execution]}" != "local" ]; then - usage -fi - -yarn run build -yarn run prepare-testdata - -TEST_PATH="./dist/${cfg[test]}.js" -echo "$(date '+%Y-%m-%d %H:%M:%S'): Executing test ${TEST_PATH} in ${cfg[execution]} mode" - -if [ "${cfg[execution]}" == "cloud-output" ]; then - GRPC_TOKEN="${cfg[grpcToken]}" GRPC_ADDRESS="${cfg[grpcAddress]}" K6_CLOUD_TOKEN="${cfg[k6CloudToken]}" k6 run --out cloud "$TEST_PATH" -elif [ "${cfg[execution]}" == "local" ]; then - GRPC_TOKEN="${cfg[grpcToken]}" GRPC_ADDRESS="${cfg[grpcAddress]}" k6 run "$TEST_PATH" -fi - - diff --git a/devenv/docker/loadtest-ts/scripts/prepareDashboardFileNames.ts b/devenv/docker/loadtest-ts/scripts/prepareDashboardFileNames.ts deleted file mode 100644 index 1f571837414..00000000000 --- a/devenv/docker/loadtest-ts/scripts/prepareDashboardFileNames.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { readdirSync, writeFileSync, mkdirSync } from 'fs'; -import { dirname, resolve } from 'path'; - -const args = process.argv.slice(2); - -if (args.length !== 2) { - throw new Error('expected dev dashboards dir and the output file path'); -} - -const devDashboardsDir = args[0]; -const outputFilePath = args[1]; - -const getFiles = (dirPath: string, ext?: string): string[] => - readdirSync(dirPath, { withFileTypes: true }) - .flatMap((dirEntry) => { - const res = resolve(dirPath, dirEntry.name); - return dirEntry.isDirectory() ? getFiles(res) : res; - }) - .filter((path) => (ext?.length ? path.endsWith(ext) : true)); - -const files = getFiles(devDashboardsDir, '.json'); - -mkdirSync(dirname(outputFilePath), { recursive: true }); -writeFileSync(outputFilePath, JSON.stringify(files, null, 2)); diff --git a/devenv/docker/loadtest-ts/src/get-large-dashboard.ts b/devenv/docker/loadtest-ts/src/get-large-dashboard.ts deleted file mode 100644 index 12211dde391..00000000000 --- a/devenv/docker/loadtest-ts/src/get-large-dashboard.ts +++ /dev/null @@ -1,116 +0,0 @@ -const testDash = { - annotations: { list: [] }, - editable: true, - fiscalYearStartMonth: 0, - graphTooltip: 0, - id: 100, - links: [], - liveNow: false, - panels: [ - { - datasource: { - type: 'testdata', - uid: 'testdata', - }, - fieldConfig: { - defaults: { - color: { - mode: 'thresholds', - }, - custom: { - align: 'auto', - displayMode: 'auto', - inspect: false, - }, - mappings: [], - thresholds: { - mode: 'absolute', - steps: [ - { - color: 'green', - value: null, - }, - { - color: 'red', - value: 80, - }, - ], - }, - }, - overrides: [], - }, - gridPos: { - h: 9, - w: 12, - x: 0, - y: 0, - }, - id: 2, - options: { - footer: { - fields: '', - reducer: ['sum'], - show: false, - }, - showHeader: true, - }, - pluginVersion: '9.4.0-pre', - targets: [ - { - csvContent: '', - datasource: { - type: 'testdata', - uid: 'PD8C576611E62080A', - }, - refId: 'A', - scenarioId: 'csv_content', - }, - ], - title: 'Panel Title', - type: 'table', - }, - ], - schemaVersion: 37, - style: 'dark', - tags: [], - templating: { - list: [], - }, - time: { - from: 'now-6h', - to: 'now', - }, - timepicker: {}, - timezone: '', - title: 'New dashboard', - uid: '5v6e5VH4z', - version: 1, - weekStart: '', -} as const; - -const getCsvContent = (lengthInKb: number): string => { - const lines: string[] = ['id,name']; - for (let i = 0; i < lengthInKb; i++) { - const prefix = `${i},`; - lines.push(prefix + 'a'.repeat(1024 - prefix.length)); - } - return lines.join('\n'); -}; - -export const prepareDashboard = (lengthInKb: number): Record => { - const firstPanel = testDash.panels[0]; - return { - ...testDash, - panels: [ - { - ...firstPanel, - targets: [ - { - ...firstPanel.targets[0], - csvContent: getCsvContent(lengthInKb), - }, - ], - }, - ], - }; -}; diff --git a/devenv/docker/loadtest-ts/src/object-store-client.ts b/devenv/docker/loadtest-ts/src/object-store-client.ts deleted file mode 100644 index 7b839a71abf..00000000000 --- a/devenv/docker/loadtest-ts/src/object-store-client.ts +++ /dev/null @@ -1,214 +0,0 @@ -import { check } from 'k6'; -import { b64encode } from 'k6/encoding'; -import grpc from 'k6/net/grpc'; - -import { Object } from './prepare-data'; - -enum GRPCMethods { - ServerHealth = 'grpc.health.v1.Health/Check', - ObjectWrite = 'object.ObjectStore/Write', - ObjectDelete = 'object.ObjectStore/Delete', - ObjectRead = 'object.ObjectStore/Read', -} - -export class GRPCObjectStoreClient { - private connected = false; - constructor(private client: grpc.Client, private grpcAddress: string, private grpcToken: string) {} - - connect = () => { - if (!this.connected) { - this.client.connect(this.grpcAddress, { plaintext: true, reflect: true }); - this.connected = true; - } - }; - - grpcRequestParams = () => { - return { - metadata: { - authorization: `Bearer ${this.grpcToken}`, - }, - }; - }; - - healthCheck = (): boolean => { - this.connect(); - const response = this.client.invoke(GRPCMethods.ServerHealth, {}); - - return check(response, { - 'server is healthy': (r) => { - const statusOK = r && r.status === grpc.StatusOK; - if (!statusOK) { - return false; - } - - const body = r.message; - // @ts-ignore - return 'status' in body && body.status === 'SERVING'; - }, - }); - }; - - deleteObject = (uid: string, kind: string, _?: {}) => { - this.connect(); - - const response = this.client.invoke( - GRPCMethods.ObjectDelete, - { - kind: kind, - UID: uid, - }, - this.grpcRequestParams() - ); - - check(response, { - 'object was deleted': (r) => { - const statusOK = r && r.status === grpc.StatusOK; - if (!statusOK) { - return false; - } - - if (!isDeleteObjectResponse(r.message)) { - console.log( - JSON.stringify({ - type: 'invalid_delete_response', - uid: uid, - kind: kind, - resp: r, - }) - ); - return false; - } - - return true; - }, - }); - }; - - readObject = (uid: string, kind: string, _?: {}) => { - this.connect(); - - const response = this.client.invoke( - GRPCMethods.ObjectRead, - { - kind: kind, - UID: uid, - with_body: true, - with_summary: true, - }, - this.grpcRequestParams() - ); - - check(response, { - 'object exists': (r) => { - const statusOK = r && r.status === grpc.StatusOK; - if (!statusOK) { - return false; - } - - const respBody = r.message; - if (!isReadObjectResponse(respBody)) { - console.log( - JSON.stringify({ - type: 'invalid_read_response', - uid: uid, - kind: kind, - resp: r, - }) - ); - return false; - } - - return typeof respBody.object.body === 'string'; - }, - }); - }; - - writeObject = (object: Object, opts?: { randomizeData?: boolean; checkCreatedOrUpdated?: boolean }) => { - this.connect(); - - const data = opts?.randomizeData - ? { - ...object.data, - __random: `${Date.now() - Math.random()}`, - } - : object.data; - - const response = this.client.invoke( - GRPCMethods.ObjectWrite, - { - body: b64encode(JSON.stringify(data)), - comment: '', - kind: object.kind, - UID: object.uid, - }, - this.grpcRequestParams() - ); - - const checkName = opts?.checkCreatedOrUpdated ? 'object was created or updated' : 'object was created'; - check(response, { - [checkName]: (r) => { - const statusOK = r && r.status === grpc.StatusOK; - if (!statusOK) { - return false; - } - - const respBody = r.message; - if (!isWriteObjectResponse(respBody)) { - console.log( - JSON.stringify({ - type: 'invalid_write_response', - uid: object.uid, - kind: object.kind, - resp: r, - }) - ); - return false; - } - - return opts?.checkCreatedOrUpdated - ? respBody.status === WriteObjectResponseStatus.UPDATED || - respBody.status === WriteObjectResponseStatus.CREATED - : respBody.status === WriteObjectResponseStatus.CREATED; - }, - }); - }; -} - -type DeleteObjectResponse = { - OK: boolean; -}; - -const isDeleteObjectResponse = (resp: object): resp is DeleteObjectResponse => { - return resp.hasOwnProperty('OK'); -}; - -enum WriteObjectResponseStatus { - CREATED = 'CREATED', - UPDATED = 'UPDATED', -} - -type WriteObjectResponse = { - status: WriteObjectResponseStatus; -}; - -const isWriteObjectResponse = (resp: object): resp is WriteObjectResponse => { - return resp.hasOwnProperty('status'); -}; - -type ReadObjectResponse = { - object: { - UID: string; - kind: string; - body: string; - }; -}; - -const isReadObjectResponse = (resp: object): resp is ReadObjectResponse => { - if (!resp.hasOwnProperty('object')) { - return false; - } - - // @ts-ignore - const object = resp.object; - return Boolean(object && typeof object === 'object' && object.hasOwnProperty('body')); -}; diff --git a/devenv/docker/loadtest-ts/src/object-store-test.ts b/devenv/docker/loadtest-ts/src/object-store-test.ts deleted file mode 100755 index 5e69dd448aa..00000000000 --- a/devenv/docker/loadtest-ts/src/object-store-test.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { SharedArray } from 'k6/data'; -import execution from 'k6/execution'; -import grpc from 'k6/net/grpc'; - -import { GRPCObjectStoreClient } from './object-store-client'; -import { Data, prepareData } from './prepare-data'; - -const grpcToken = __ENV.GRPC_TOKEN; -const grpcAddress = __ENV.GRPC_ADDRESS; - -if (typeof grpcToken !== 'string' || !grpcToken.length) { - throw new Error('GRPC_TOKEN env variable is missing'); -} - -if (typeof grpcAddress !== 'string' || !grpcAddress.length) { - throw new Error('GRPC_ADDRESS env variable is missing'); -} - -const client = new grpc.Client(); -const objectStoreClient = new GRPCObjectStoreClient(client, grpcAddress, grpcToken); - -const data: Data = new SharedArray('data', () => { - return [prepareData(JSON.parse(open('../scripts/tmp/filenames.json')), 50)]; -})[0]; - -const scenarioDuration = '2m'; - -export const options = { - setupTimeout: '5m', - teardownTimeout: '5m', - noConnectionReuse: true, - scenarios: { - writer: { - exec: 'writer', - executor: 'constant-arrival-rate', - rate: 1, - timeUnit: '2s', - duration: scenarioDuration, - preAllocatedVUs: 1, - maxVUs: 1, - }, - reader: { - exec: 'reader', - executor: 'constant-arrival-rate', - rate: 10, - timeUnit: '2s', - duration: scenarioDuration, - preAllocatedVUs: 1, - maxVUs: 10, - }, - writer1mb: { - exec: 'writer1mb', - executor: 'constant-arrival-rate', - rate: 1, - timeUnit: '20s', - duration: scenarioDuration, - preAllocatedVUs: 1, - maxVUs: 5, - }, - reader1mb: { - startTime: '2s', - exec: 'reader1mb', - executor: 'constant-arrival-rate', - rate: 1, - timeUnit: '1s', - duration: scenarioDuration, - preAllocatedVUs: 1, - maxVUs: 5, - }, - writer4mb: { - exec: 'writer4mb', - executor: 'constant-arrival-rate', - rate: 1, - timeUnit: '30s', - duration: scenarioDuration, - preAllocatedVUs: 1, - maxVUs: 5, - }, - reader4mb: { - startTime: '3s', - exec: 'reader4mb', - executor: 'constant-arrival-rate', - rate: 1, - timeUnit: '5s', - duration: scenarioDuration, - preAllocatedVUs: 1, - maxVUs: 5, - }, - }, - // thresholds: { http_req_duration: ['avg<100', 'p(95)<200'] }, -}; - -export function setup() { - if (!objectStoreClient.healthCheck()) { - execution.test.abort('server should be healthy'); - } - - console.log('inserting base objects'); - for (let i = 0; i < data.base.length; i++) { - if (i % 100 === 0) { - console.log(`inserted ${i} / ${data.base.length}`); - } - objectStoreClient.writeObject(data.base[i], { randomizeData: false, checkCreatedOrUpdated: false }); - } -} - -export function teardown() { - const toDelete = [...data.base, ...data.toWrite, data.size1mb, data.size4mb, data.size100kb]; - - console.log('deleting base objects'); - for (let i = 0; i < toDelete.length; i++) { - if (i % 100 === 0) { - console.log(`deleted ${i} / ${data.base.length}`); - } - objectStoreClient.deleteObject(toDelete[i].uid, toDelete[i].kind); - } -} - -export function reader() { - const item = data.base[execution.scenario.iterationInTest % data.base.length]; - objectStoreClient.readObject(item.uid, item.kind); -} - -export function writer() { - const item = data.toWrite[execution.scenario.iterationInTest % data.toWrite.length]; - objectStoreClient.writeObject(item, { randomizeData: true, checkCreatedOrUpdated: true }); -} - -export function writer1mb() { - objectStoreClient.writeObject(data.size1mb, { randomizeData: true, checkCreatedOrUpdated: true }); -} - -export function reader1mb() { - const item = data.size1mb; - objectStoreClient.readObject(item.uid, item.kind); -} - -export function writer4mb() { - objectStoreClient.writeObject(data.size4mb, { randomizeData: true, checkCreatedOrUpdated: true }); -} - -export function reader4mb() { - const item = data.size4mb; - objectStoreClient.readObject(item.uid, item.kind); -} diff --git a/devenv/docker/loadtest-ts/src/prepare-data.ts b/devenv/docker/loadtest-ts/src/prepare-data.ts deleted file mode 100644 index cb02b06e4fa..00000000000 --- a/devenv/docker/loadtest-ts/src/prepare-data.ts +++ /dev/null @@ -1,57 +0,0 @@ -import shortid from 'shortid'; - -import { prepareDashboard } from './get-large-dashboard'; - -export type Object = { - data: Record; - kind: string; - uid: string; -}; - -export type Data = { - base: Object[]; // objects that are inserted in the test setup and removed only in the teardown - toWrite: Object[]; // objects that are inserted by scenarios and removed after a short period of time: Object; - size100kb: Object; - size1mb: Object; - size4mb: Object; -}; - -export const readAsObjects = (paths: string[], kind: string): Object[] => { - return paths.map((p) => ({ - data: JSON.parse(open(p)), - uid: shortid.generate(), - kind, - })); -}; - -export const getBase = (uniqueObjects: Object[], no: number): Object[] => { - const base = new Array(no); - for (let i = 0; i < no; i++) { - const obj = uniqueObjects[Math.floor(i % uniqueObjects.length)]; - base[i] = { - ...obj, - uid: `${obj.uid}-${Math.floor(i / uniqueObjects.length)}`, - }; - } - - return base; -}; - -const prepareObject = (lengthInKb: number): Object => { - return { - data: prepareDashboard(lengthInKb), - kind: 'dashboard', - uid: shortid(), - }; -}; - -export const prepareData = (dashboardFilePaths: string[], baseNumber: number): Data => { - const objects = readAsObjects(dashboardFilePaths, 'dashboard'); - return { - base: getBase(objects, baseNumber), - toWrite: objects, - size100kb: prepareObject(100), - size1mb: prepareObject(1000), - size4mb: prepareObject(4000), - }; -}; diff --git a/devenv/docker/loadtest-ts/tsconfig.json b/devenv/docker/loadtest-ts/tsconfig.json deleted file mode 100644 index 7da92cf09e2..00000000000 --- a/devenv/docker/loadtest-ts/tsconfig.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "compilerOptions": { - "target": "es5", - "moduleResolution": "node", - "module": "commonjs", - "noEmit": true, - "allowJs": true, - "removeComments": false, - - "strict": true, - "noImplicitAny": true, - "noImplicitThis": true, - - "noUnusedLocals": true, - "noUnusedParameters": true, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true, - - "allowSyntheticDefaultImports": true, - "esModuleInterop": true, - "experimentalDecorators": true, - "emitDecoratorMetadata": true, - - "skipLibCheck": true - } -} diff --git a/devenv/docker/loadtest-ts/webpack.config.js b/devenv/docker/loadtest-ts/webpack.config.js deleted file mode 100644 index 66891f56cc1..00000000000 --- a/devenv/docker/loadtest-ts/webpack.config.js +++ /dev/null @@ -1,37 +0,0 @@ -const path = require('path'); -const GlobEntries = require('webpack-glob-entries'); - -module.exports = { - mode: 'production', - entry: GlobEntries('./src/*test*.ts'), // Generates multiple entry for each test - output: { - path: path.join(__dirname, 'dist'), - libraryTarget: 'commonjs', - filename: '[name].js', - clean: true, - }, - resolve: { - extensions: ['.ts', '.js'], - }, - module: { - rules: [ - { - test: /\.ts$/, - use: 'babel-loader', - exclude: /node_modules/, - }, - ], - }, - target: 'web', - externals: /^(k6|https?\:\/\/)(\/.*)?/, - // Generate map files for compiled scripts - devtool: 'source-map', - stats: { - colors: true, - }, - plugins: [], - optimization: { - // Don't minimize, as it's not used in the browser - minimize: false, - }, -}; diff --git a/devenv/docker/loadtest-ts/yarn.lock b/devenv/docker/loadtest-ts/yarn.lock deleted file mode 100644 index 6e86007c1a1..00000000000 --- a/devenv/docker/loadtest-ts/yarn.lock +++ /dev/null @@ -1,3167 +0,0 @@ -# This file is generated by running "yarn install" inside your project. -# Manual changes might be lost - proceed with caution! - -__metadata: - version: 6 - cacheKey: 8 - -"@ampproject/remapping@npm:^2.1.0": - version: 2.2.0 - resolution: "@ampproject/remapping@npm:2.2.0" - dependencies: - "@jridgewell/gen-mapping": ^0.1.0 - "@jridgewell/trace-mapping": ^0.3.9 - checksum: d74d170d06468913921d72430259424b7e4c826b5a7d39ff839a29d547efb97dc577caa8ba3fb5cf023624e9af9d09651afc3d4112a45e2050328abc9b3a2292 - languageName: node - linkType: hard - -"@babel/code-frame@npm:^7.18.6": - version: 7.18.6 - resolution: "@babel/code-frame@npm:7.18.6" - dependencies: - "@babel/highlight": ^7.18.6 - checksum: 195e2be3172d7684bf95cff69ae3b7a15a9841ea9d27d3c843662d50cdd7d6470fd9c8e64be84d031117e4a4083486effba39f9aef6bbb2c89f7f21bcfba33ba - languageName: node - linkType: hard - -"@babel/compat-data@npm:^7.17.7, @babel/compat-data@npm:^7.19.3, @babel/compat-data@npm:^7.19.4": - version: 7.19.4 - resolution: "@babel/compat-data@npm:7.19.4" - checksum: 757fdaeb6756c2d323ff56f60fb8e670292108cda6abf762a56c0d40910ecc4d2c7e283dbdfbcee6bc28c74ad659144352609e1cb49d31e101ab13ea5ce90072 - languageName: node - linkType: hard - -"@babel/core@npm:7.19.6": - version: 7.19.6 - resolution: "@babel/core@npm:7.19.6" - dependencies: - "@ampproject/remapping": ^2.1.0 - "@babel/code-frame": ^7.18.6 - "@babel/generator": ^7.19.6 - "@babel/helper-compilation-targets": ^7.19.3 - "@babel/helper-module-transforms": ^7.19.6 - "@babel/helpers": ^7.19.4 - "@babel/parser": ^7.19.6 - "@babel/template": ^7.18.10 - "@babel/traverse": ^7.19.6 - "@babel/types": ^7.19.4 - convert-source-map: ^1.7.0 - debug: ^4.1.0 - gensync: ^1.0.0-beta.2 - json5: ^2.2.1 - semver: ^6.3.0 - checksum: 85c0bd38d0ef180aa2d23c3db6840a0baec88d2e05c30e7ffc3dfeb6b2b89d6e4864922f04997a1f4ce55f9dd469bf2e76518d5c7ae744b98516709d32769b73 - languageName: node - linkType: hard - -"@babel/generator@npm:^7.19.6": - version: 7.19.6 - resolution: "@babel/generator@npm:7.19.6" - dependencies: - "@babel/types": ^7.19.4 - "@jridgewell/gen-mapping": ^0.3.2 - jsesc: ^2.5.1 - checksum: 734fcb1fbef182e7b8967459cb39b81edd2701dd13170c154b368d4e086842f72ef214798c5a37e67e0a695dfb34b13143277bedcd9795b3b1b83da8e1d236c6 - languageName: node - linkType: hard - -"@babel/generator@npm:^7.20.1": - version: 7.20.1 - resolution: "@babel/generator@npm:7.20.1" - dependencies: - "@babel/types": ^7.20.0 - "@jridgewell/gen-mapping": ^0.3.2 - jsesc: ^2.5.1 - checksum: e6846d88c59a5dae4c86f3cc84f84972cf9cc5fa0f4944606303a9df3ba1be388e0cb08a625c86f7282ab03faf54acd72efba34f019b6762f4739a175173783e - languageName: node - linkType: hard - -"@babel/helper-annotate-as-pure@npm:^7.18.6": - version: 7.18.6 - resolution: "@babel/helper-annotate-as-pure@npm:7.18.6" - dependencies: - "@babel/types": ^7.18.6 - checksum: 88ccd15ced475ef2243fdd3b2916a29ea54c5db3cd0cfabf9d1d29ff6e63b7f7cd1c27264137d7a40ac2e978b9b9a542c332e78f40eb72abe737a7400788fc1b - languageName: node - linkType: hard - -"@babel/helper-builder-binary-assignment-operator-visitor@npm:^7.18.6": - version: 7.18.9 - resolution: "@babel/helper-builder-binary-assignment-operator-visitor@npm:7.18.9" - dependencies: - "@babel/helper-explode-assignable-expression": ^7.18.6 - "@babel/types": ^7.18.9 - checksum: b4bc214cb56329daff6cc18a7f7a26aeafb55a1242e5362f3d47fe3808421f8c7cd91fff95d6b9b7ccb67e14e5a67d944e49dbe026942bfcbfda19b1c72a8e72 - languageName: node - linkType: hard - -"@babel/helper-compilation-targets@npm:^7.17.7, @babel/helper-compilation-targets@npm:^7.18.9, @babel/helper-compilation-targets@npm:^7.19.0, @babel/helper-compilation-targets@npm:^7.19.3": - version: 7.19.3 - resolution: "@babel/helper-compilation-targets@npm:7.19.3" - dependencies: - "@babel/compat-data": ^7.19.3 - "@babel/helper-validator-option": ^7.18.6 - browserslist: ^4.21.3 - semver: ^6.3.0 - peerDependencies: - "@babel/core": ^7.0.0 - checksum: aafcb4490c98cddb3255fff98bfbdb881b4def85a1935fd9b1f9b1f0f8b502696839f6b387fb508ca991ea72ba82ce6913bab99f21df4ce80bda2b79e91a09f5 - languageName: node - linkType: hard - -"@babel/helper-create-class-features-plugin@npm:^7.18.6, @babel/helper-create-class-features-plugin@npm:^7.19.0": - version: 7.19.0 - resolution: "@babel/helper-create-class-features-plugin@npm:7.19.0" - dependencies: - "@babel/helper-annotate-as-pure": ^7.18.6 - "@babel/helper-environment-visitor": ^7.18.9 - "@babel/helper-function-name": ^7.19.0 - "@babel/helper-member-expression-to-functions": ^7.18.9 - "@babel/helper-optimise-call-expression": ^7.18.6 - "@babel/helper-replace-supers": ^7.18.9 - "@babel/helper-split-export-declaration": ^7.18.6 - peerDependencies: - "@babel/core": ^7.0.0 - checksum: f0c6fb77b6f113d70f308e7093f60dd465b697818badf5df0519d8dd12b6bfb1f4ad300b923207ce9f9c1c940ef58bff12ac4270c0863eadf9e303b7dd6d01b6 - languageName: node - linkType: hard - -"@babel/helper-create-regexp-features-plugin@npm:^7.18.6, @babel/helper-create-regexp-features-plugin@npm:^7.19.0": - version: 7.19.0 - resolution: "@babel/helper-create-regexp-features-plugin@npm:7.19.0" - dependencies: - "@babel/helper-annotate-as-pure": ^7.18.6 - regexpu-core: ^5.1.0 - peerDependencies: - "@babel/core": ^7.0.0 - checksum: 811cc90afe9fc25a74ed37fc0c1361a4a91b0b940235dd3958e3f03b366d40a903b40fc93b51bcb93be774aba573219f8f215664bea1d1301f58797ca6854f3f - languageName: node - linkType: hard - -"@babel/helper-define-polyfill-provider@npm:^0.3.3": - version: 0.3.3 - resolution: "@babel/helper-define-polyfill-provider@npm:0.3.3" - dependencies: - "@babel/helper-compilation-targets": ^7.17.7 - "@babel/helper-plugin-utils": ^7.16.7 - debug: ^4.1.1 - lodash.debounce: ^4.0.8 - resolve: ^1.14.2 - semver: ^6.1.2 - peerDependencies: - "@babel/core": ^7.4.0-0 - checksum: 8e3fe75513302e34f6d92bd67b53890e8545e6c5bca8fe757b9979f09d68d7e259f6daea90dc9e01e332c4f8781bda31c5fe551c82a277f9bc0bec007aed497c - languageName: node - linkType: hard - -"@babel/helper-environment-visitor@npm:^7.18.9": - version: 7.18.9 - resolution: "@babel/helper-environment-visitor@npm:7.18.9" - checksum: b25101f6162ddca2d12da73942c08ad203d7668e06663df685634a8fde54a98bc015f6f62938e8554457a592a024108d45b8f3e651fd6dcdb877275b73cc4420 - languageName: node - linkType: hard - -"@babel/helper-explode-assignable-expression@npm:^7.18.6": - version: 7.18.6 - resolution: "@babel/helper-explode-assignable-expression@npm:7.18.6" - dependencies: - "@babel/types": ^7.18.6 - checksum: 225cfcc3376a8799023d15dc95000609e9d4e7547b29528c7f7111a0e05493ffb12c15d70d379a0bb32d42752f340233c4115bded6d299bc0c3ab7a12be3d30f - languageName: node - linkType: hard - -"@babel/helper-function-name@npm:^7.18.9, @babel/helper-function-name@npm:^7.19.0": - version: 7.19.0 - resolution: "@babel/helper-function-name@npm:7.19.0" - dependencies: - "@babel/template": ^7.18.10 - "@babel/types": ^7.19.0 - checksum: eac1f5db428ba546270c2b8d750c24eb528b8fcfe50c81de2e0bdebf0e20f24bec688d4331533b782e4a907fad435244621ca2193cfcf80a86731299840e0f6e - languageName: node - linkType: hard - -"@babel/helper-hoist-variables@npm:^7.18.6": - version: 7.18.6 - resolution: "@babel/helper-hoist-variables@npm:7.18.6" - dependencies: - "@babel/types": ^7.18.6 - checksum: fd9c35bb435fda802bf9ff7b6f2df06308a21277c6dec2120a35b09f9de68f68a33972e2c15505c1a1a04b36ec64c9ace97d4a9e26d6097b76b4396b7c5fa20f - languageName: node - linkType: hard - -"@babel/helper-member-expression-to-functions@npm:^7.18.9": - version: 7.18.9 - resolution: "@babel/helper-member-expression-to-functions@npm:7.18.9" - dependencies: - "@babel/types": ^7.18.9 - checksum: fcf8184e3b55051c4286b2cbedf0eccc781d0f3c9b5cbaba582eca19bf0e8d87806cdb7efc8554fcb969ceaf2b187d5ea748d40022d06ec7739fbb18c1b19a7a - languageName: node - linkType: hard - -"@babel/helper-module-imports@npm:^7.18.6": - version: 7.18.6 - resolution: "@babel/helper-module-imports@npm:7.18.6" - dependencies: - "@babel/types": ^7.18.6 - checksum: f393f8a3b3304b1b7a288a38c10989de754f01d29caf62ce7c4e5835daf0a27b81f3ac687d9d2780d39685aae7b55267324b512150e7b2be967b0c493b6a1def - languageName: node - linkType: hard - -"@babel/helper-module-transforms@npm:^7.18.6, @babel/helper-module-transforms@npm:^7.19.6": - version: 7.19.6 - resolution: "@babel/helper-module-transforms@npm:7.19.6" - dependencies: - "@babel/helper-environment-visitor": ^7.18.9 - "@babel/helper-module-imports": ^7.18.6 - "@babel/helper-simple-access": ^7.19.4 - "@babel/helper-split-export-declaration": ^7.18.6 - "@babel/helper-validator-identifier": ^7.19.1 - "@babel/template": ^7.18.10 - "@babel/traverse": ^7.19.6 - "@babel/types": ^7.19.4 - checksum: c28692b37d4b5abacc775bcab52a74f44a493f38c58cb72b56a6c6d67a97485dd8aff6f26905abd1a924d3261a171d0214a9fb76f48d8598f1e35b8b29284792 - languageName: node - linkType: hard - -"@babel/helper-optimise-call-expression@npm:^7.18.6": - version: 7.18.6 - resolution: "@babel/helper-optimise-call-expression@npm:7.18.6" - dependencies: - "@babel/types": ^7.18.6 - checksum: e518fe8418571405e21644cfb39cf694f30b6c47b10b006609a92469ae8b8775cbff56f0b19732343e2ea910641091c5a2dc73b56ceba04e116a33b0f8bd2fbd - languageName: node - linkType: hard - -"@babel/helper-plugin-utils@npm:^7.0.0, @babel/helper-plugin-utils@npm:^7.10.4, @babel/helper-plugin-utils@npm:^7.12.13, @babel/helper-plugin-utils@npm:^7.14.5, @babel/helper-plugin-utils@npm:^7.16.7, @babel/helper-plugin-utils@npm:^7.18.6, @babel/helper-plugin-utils@npm:^7.18.9, @babel/helper-plugin-utils@npm:^7.19.0, @babel/helper-plugin-utils@npm:^7.8.0, @babel/helper-plugin-utils@npm:^7.8.3": - version: 7.19.0 - resolution: "@babel/helper-plugin-utils@npm:7.19.0" - checksum: eedc996c633c8c207921c26ec2989eae0976336ecd9b9f1ac526498f52b5d136f7cd03c32b6fdf8d46a426f907c142de28592f383c42e5fba1e904cbffa05345 - languageName: node - linkType: hard - -"@babel/helper-remap-async-to-generator@npm:^7.18.6, @babel/helper-remap-async-to-generator@npm:^7.18.9": - version: 7.18.9 - resolution: "@babel/helper-remap-async-to-generator@npm:7.18.9" - dependencies: - "@babel/helper-annotate-as-pure": ^7.18.6 - "@babel/helper-environment-visitor": ^7.18.9 - "@babel/helper-wrap-function": ^7.18.9 - "@babel/types": ^7.18.9 - peerDependencies: - "@babel/core": ^7.0.0 - checksum: 4be6076192308671b046245899b703ba090dbe7ad03e0bea897bb2944ae5b88e5e85853c9d1f83f643474b54c578d8ac0800b80341a86e8538264a725fbbefec - languageName: node - linkType: hard - -"@babel/helper-replace-supers@npm:^7.18.6, @babel/helper-replace-supers@npm:^7.18.9": - version: 7.19.1 - resolution: "@babel/helper-replace-supers@npm:7.19.1" - dependencies: - "@babel/helper-environment-visitor": ^7.18.9 - "@babel/helper-member-expression-to-functions": ^7.18.9 - "@babel/helper-optimise-call-expression": ^7.18.6 - "@babel/traverse": ^7.19.1 - "@babel/types": ^7.19.0 - checksum: a0e4bf79ebe7d2bb5947169e47a0b4439c73fb0ec57d446cf3ea81b736721129ec373c3f94d2ebd2716b26dd65f8e6c083dac898170d42905e7ba815a2f52c25 - languageName: node - linkType: hard - -"@babel/helper-simple-access@npm:^7.19.4": - version: 7.19.4 - resolution: "@babel/helper-simple-access@npm:7.19.4" - dependencies: - "@babel/types": ^7.19.4 - checksum: 964cb1ec36b69aabbb02f8d5ee1d680ebbb628611a6740958d9b05107ab16c0492044e430618ae42b1f8ea73e4e1bafe3750e8ebc959d6f3277d9cfbe1a94880 - languageName: node - linkType: hard - -"@babel/helper-skip-transparent-expression-wrappers@npm:^7.18.9": - version: 7.18.9 - resolution: "@babel/helper-skip-transparent-expression-wrappers@npm:7.18.9" - dependencies: - "@babel/types": ^7.18.9 - checksum: 6e93ccd10248293082606a4b3e30eed32c6f796d378f6b662796c88f462f348aa368aadeb48eb410cfcc8250db93b2d6627c2e55662530f08fc25397e588d68a - languageName: node - linkType: hard - -"@babel/helper-split-export-declaration@npm:^7.18.6": - version: 7.18.6 - resolution: "@babel/helper-split-export-declaration@npm:7.18.6" - dependencies: - "@babel/types": ^7.18.6 - checksum: c6d3dede53878f6be1d869e03e9ffbbb36f4897c7cc1527dc96c56d127d834ffe4520a6f7e467f5b6f3c2843ea0e81a7819d66ae02f707f6ac057f3d57943a2b - languageName: node - linkType: hard - -"@babel/helper-string-parser@npm:^7.19.4": - version: 7.19.4 - resolution: "@babel/helper-string-parser@npm:7.19.4" - checksum: b2f8a3920b30dfac81ec282ac4ad9598ea170648f8254b10f475abe6d944808fb006aab325d3eb5a8ad3bea8dfa888cfa6ef471050dae5748497c110ec060943 - languageName: node - linkType: hard - -"@babel/helper-validator-identifier@npm:^7.18.6, @babel/helper-validator-identifier@npm:^7.19.1": - version: 7.19.1 - resolution: "@babel/helper-validator-identifier@npm:7.19.1" - checksum: 0eca5e86a729162af569b46c6c41a63e18b43dbe09fda1d2a3c8924f7d617116af39cac5e4cd5d431bb760b4dca3c0970e0c444789b1db42bcf1fa41fbad0a3a - languageName: node - linkType: hard - -"@babel/helper-validator-option@npm:^7.18.6": - version: 7.18.6 - resolution: "@babel/helper-validator-option@npm:7.18.6" - checksum: f9cc6eb7cc5d759c5abf006402180f8d5e4251e9198197428a97e05d65eb2f8ae5a0ce73b1dfd2d35af41d0eb780627a64edf98a4e71f064eeeacef8de58f2cf - languageName: node - linkType: hard - -"@babel/helper-wrap-function@npm:^7.18.9": - version: 7.19.0 - resolution: "@babel/helper-wrap-function@npm:7.19.0" - dependencies: - "@babel/helper-function-name": ^7.19.0 - "@babel/template": ^7.18.10 - "@babel/traverse": ^7.19.0 - "@babel/types": ^7.19.0 - checksum: 2453a6b134f12cc779179188c4358a66252c29b634a8195c0cf626e17f9806c3c4c40e159cd8056c2ec82b69b9056a088014fa43d6ccc1aca67da8d9605da8fd - languageName: node - linkType: hard - -"@babel/helpers@npm:^7.19.4": - version: 7.20.1 - resolution: "@babel/helpers@npm:7.20.1" - dependencies: - "@babel/template": ^7.18.10 - "@babel/traverse": ^7.20.1 - "@babel/types": ^7.20.0 - checksum: be35f78666bdab895775ed94dbeb098f7b4fa08ce4cfb0c3a9e69b7220cce56960dcdc2b14f5df9d3b80388d4bf7df155c97f6cf6768c0138f4e6931d0f44955 - languageName: node - linkType: hard - -"@babel/highlight@npm:^7.18.6": - version: 7.18.6 - resolution: "@babel/highlight@npm:7.18.6" - dependencies: - "@babel/helper-validator-identifier": ^7.18.6 - chalk: ^2.0.0 - js-tokens: ^4.0.0 - checksum: 92d8ee61549de5ff5120e945e774728e5ccd57fd3b2ed6eace020ec744823d4a98e242be1453d21764a30a14769ecd62170fba28539b211799bbaf232bbb2789 - languageName: node - linkType: hard - -"@babel/parser@npm:^7.18.10, @babel/parser@npm:^7.19.6": - version: 7.19.6 - resolution: "@babel/parser@npm:7.19.6" - bin: - parser: ./bin/babel-parser.js - checksum: 9a3dca4ee3acd7e4fc3b58e1e1526a11fa334acbfe437f8ebf91dfaf48e943c147ef64b1733ba0a55af57d1eccafbf4e4a4afc46a15becd921971fe2ddf309bf - languageName: node - linkType: hard - -"@babel/parser@npm:^7.20.1": - version: 7.20.1 - resolution: "@babel/parser@npm:7.20.1" - bin: - parser: ./bin/babel-parser.js - checksum: 2db7ba692b8b3054df129876b115ed8b265d5c138dee5e1db56a999209df3dd3d508e0985bf3cc44fc432498da094c164906b531d049608e55e208dae0678d42 - languageName: node - linkType: hard - -"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:^7.18.6": - version: 7.18.6 - resolution: "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:7.18.6" - dependencies: - "@babel/helper-plugin-utils": ^7.18.6 - peerDependencies: - "@babel/core": ^7.0.0 - checksum: 845bd280c55a6a91d232cfa54eaf9708ec71e594676fe705794f494bb8b711d833b752b59d1a5c154695225880c23dbc9cab0e53af16fd57807976cd3ff41b8d - languageName: node - linkType: hard - -"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@npm:^7.18.9": - version: 7.18.9 - resolution: "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@npm:7.18.9" - dependencies: - "@babel/helper-plugin-utils": ^7.18.9 - "@babel/helper-skip-transparent-expression-wrappers": ^7.18.9 - "@babel/plugin-proposal-optional-chaining": ^7.18.9 - peerDependencies: - "@babel/core": ^7.13.0 - checksum: 93abb5cb179a13db171bfc2cdf79489598f43c50cc174f97a2b7bb1d44d24ade7109665a20cf4e317ad6c1c730f036f06478f7c7e789b4240be1abdb60d6452f - languageName: node - linkType: hard - -"@babel/plugin-proposal-async-generator-functions@npm:^7.19.1": - version: 7.20.1 - resolution: "@babel/plugin-proposal-async-generator-functions@npm:7.20.1" - dependencies: - "@babel/helper-environment-visitor": ^7.18.9 - "@babel/helper-plugin-utils": ^7.19.0 - "@babel/helper-remap-async-to-generator": ^7.18.9 - "@babel/plugin-syntax-async-generators": ^7.8.4 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 518483a68c5618932109913eb7316ed5e656c575cbd9d22667bc0451e35a1be45f8eaeb8e2065834b36c8a93c4840f78cebf8f1d067b07c422f7be16d58eca60 - languageName: node - linkType: hard - -"@babel/plugin-proposal-class-properties@npm:7.18.6, @babel/plugin-proposal-class-properties@npm:^7.18.6": - version: 7.18.6 - resolution: "@babel/plugin-proposal-class-properties@npm:7.18.6" - dependencies: - "@babel/helper-create-class-features-plugin": ^7.18.6 - "@babel/helper-plugin-utils": ^7.18.6 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 49a78a2773ec0db56e915d9797e44fd079ab8a9b2e1716e0df07c92532f2c65d76aeda9543883916b8e0ff13606afeffa67c5b93d05b607bc87653ad18a91422 - languageName: node - linkType: hard - -"@babel/plugin-proposal-class-static-block@npm:^7.18.6": - version: 7.18.6 - resolution: "@babel/plugin-proposal-class-static-block@npm:7.18.6" - dependencies: - "@babel/helper-create-class-features-plugin": ^7.18.6 - "@babel/helper-plugin-utils": ^7.18.6 - "@babel/plugin-syntax-class-static-block": ^7.14.5 - peerDependencies: - "@babel/core": ^7.12.0 - checksum: b8d7ae99ed5ad784f39e7820e3ac03841f91d6ed60ab4a98c61d6112253da36013e12807bae4ffed0ef3cb318e47debac112ed614e03b403fb8b075b09a828ee - languageName: node - linkType: hard - -"@babel/plugin-proposal-dynamic-import@npm:^7.18.6": - version: 7.18.6 - resolution: "@babel/plugin-proposal-dynamic-import@npm:7.18.6" - dependencies: - "@babel/helper-plugin-utils": ^7.18.6 - "@babel/plugin-syntax-dynamic-import": ^7.8.3 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 96b1c8a8ad8171d39e9ab106be33bde37ae09b22fb2c449afee9a5edf3c537933d79d963dcdc2694d10677cb96da739cdf1b53454e6a5deab9801f28a818bb2f - languageName: node - linkType: hard - -"@babel/plugin-proposal-export-namespace-from@npm:^7.18.9": - version: 7.18.9 - resolution: "@babel/plugin-proposal-export-namespace-from@npm:7.18.9" - dependencies: - "@babel/helper-plugin-utils": ^7.18.9 - "@babel/plugin-syntax-export-namespace-from": ^7.8.3 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 84ff22bacc5d30918a849bfb7e0e90ae4c5b8d8b65f2ac881803d1cf9068dffbe53bd657b0e4bc4c20b4db301b1c85f1e74183cf29a0dd31e964bd4e97c363ef - languageName: node - linkType: hard - -"@babel/plugin-proposal-json-strings@npm:^7.18.6": - version: 7.18.6 - resolution: "@babel/plugin-proposal-json-strings@npm:7.18.6" - dependencies: - "@babel/helper-plugin-utils": ^7.18.6 - "@babel/plugin-syntax-json-strings": ^7.8.3 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 25ba0e6b9d6115174f51f7c6787e96214c90dd4026e266976b248a2ed417fe50fddae72843ffb3cbe324014a18632ce5648dfac77f089da858022b49fd608cb3 - languageName: node - linkType: hard - -"@babel/plugin-proposal-logical-assignment-operators@npm:^7.18.9": - version: 7.18.9 - resolution: "@babel/plugin-proposal-logical-assignment-operators@npm:7.18.9" - dependencies: - "@babel/helper-plugin-utils": ^7.18.9 - "@babel/plugin-syntax-logical-assignment-operators": ^7.10.4 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: dd87fa4a48c6408c5e85dbd6405a65cc8fe909e3090030df46df90df64cdf3e74007381a58ed87608778ee597eff7395d215274009bb3f5d8964b2db5557754f - languageName: node - linkType: hard - -"@babel/plugin-proposal-nullish-coalescing-operator@npm:^7.18.6": - version: 7.18.6 - resolution: "@babel/plugin-proposal-nullish-coalescing-operator@npm:7.18.6" - dependencies: - "@babel/helper-plugin-utils": ^7.18.6 - "@babel/plugin-syntax-nullish-coalescing-operator": ^7.8.3 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 949c9ddcdecdaec766ee610ef98f965f928ccc0361dd87cf9f88cf4896a6ccd62fce063d4494778e50da99dea63d270a1be574a62d6ab81cbe9d85884bf55a7d - languageName: node - linkType: hard - -"@babel/plugin-proposal-numeric-separator@npm:^7.18.6": - version: 7.18.6 - resolution: "@babel/plugin-proposal-numeric-separator@npm:7.18.6" - dependencies: - "@babel/helper-plugin-utils": ^7.18.6 - "@babel/plugin-syntax-numeric-separator": ^7.10.4 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: f370ea584c55bf4040e1f78c80b4eeb1ce2e6aaa74f87d1a48266493c33931d0b6222d8cee3a082383d6bb648ab8d6b7147a06f974d3296ef3bc39c7851683ec - languageName: node - linkType: hard - -"@babel/plugin-proposal-object-rest-spread@npm:7.19.4, @babel/plugin-proposal-object-rest-spread@npm:^7.19.4": - version: 7.19.4 - resolution: "@babel/plugin-proposal-object-rest-spread@npm:7.19.4" - dependencies: - "@babel/compat-data": ^7.19.4 - "@babel/helper-compilation-targets": ^7.19.3 - "@babel/helper-plugin-utils": ^7.19.0 - "@babel/plugin-syntax-object-rest-spread": ^7.8.3 - "@babel/plugin-transform-parameters": ^7.18.8 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 90a2a59da305e6c8c83831e16079193df33d727a77a90972e286af2c8c0295fddb91b0978b88f16f63080d08a82b08ce3ee82a88b0488b3c51decc73c1d35786 - languageName: node - linkType: hard - -"@babel/plugin-proposal-optional-catch-binding@npm:^7.18.6": - version: 7.18.6 - resolution: "@babel/plugin-proposal-optional-catch-binding@npm:7.18.6" - dependencies: - "@babel/helper-plugin-utils": ^7.18.6 - "@babel/plugin-syntax-optional-catch-binding": ^7.8.3 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 7b5b39fb5d8d6d14faad6cb68ece5eeb2fd550fb66b5af7d7582402f974f5bc3684641f7c192a5a57e0f59acfae4aada6786be1eba030881ddc590666eff4d1e - languageName: node - linkType: hard - -"@babel/plugin-proposal-optional-chaining@npm:^7.18.9": - version: 7.18.9 - resolution: "@babel/plugin-proposal-optional-chaining@npm:7.18.9" - dependencies: - "@babel/helper-plugin-utils": ^7.18.9 - "@babel/helper-skip-transparent-expression-wrappers": ^7.18.9 - "@babel/plugin-syntax-optional-chaining": ^7.8.3 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: f2db40e26172f07c50b635cb61e1f36165de3ba868fcf608d967642f0d044b7c6beb0e7ecf17cbd421144b99e1eae7ad6031ded92925343bb0ed1d08707b514f - languageName: node - linkType: hard - -"@babel/plugin-proposal-private-methods@npm:^7.18.6": - version: 7.18.6 - resolution: "@babel/plugin-proposal-private-methods@npm:7.18.6" - dependencies: - "@babel/helper-create-class-features-plugin": ^7.18.6 - "@babel/helper-plugin-utils": ^7.18.6 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 22d8502ee96bca99ad2c8393e8493e2b8d4507576dd054490fd8201a36824373440106f5b098b6d821b026c7e72b0424ff4aeca69ed5f42e48f029d3a156d5ad - languageName: node - linkType: hard - -"@babel/plugin-proposal-private-property-in-object@npm:^7.18.6": - version: 7.18.6 - resolution: "@babel/plugin-proposal-private-property-in-object@npm:7.18.6" - dependencies: - "@babel/helper-annotate-as-pure": ^7.18.6 - "@babel/helper-create-class-features-plugin": ^7.18.6 - "@babel/helper-plugin-utils": ^7.18.6 - "@babel/plugin-syntax-private-property-in-object": ^7.14.5 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: c8e56a972930730345f39f2384916fd8e711b3f4b4eae2ca9740e99958980118120d5cc9b6ac150f0965a5a35f825910e2c3013d90be3e9993ab6111df444569 - languageName: node - linkType: hard - -"@babel/plugin-proposal-unicode-property-regex@npm:^7.18.6, @babel/plugin-proposal-unicode-property-regex@npm:^7.4.4": - version: 7.18.6 - resolution: "@babel/plugin-proposal-unicode-property-regex@npm:7.18.6" - dependencies: - "@babel/helper-create-regexp-features-plugin": ^7.18.6 - "@babel/helper-plugin-utils": ^7.18.6 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: a8575ecb7ff24bf6c6e94808d5c84bb5a0c6dd7892b54f09f4646711ba0ee1e1668032b3c43e3e1dfec2c5716c302e851ac756c1645e15882d73df6ad21ae951 - languageName: node - linkType: hard - -"@babel/plugin-syntax-async-generators@npm:^7.8.4": - version: 7.8.4 - resolution: "@babel/plugin-syntax-async-generators@npm:7.8.4" - dependencies: - "@babel/helper-plugin-utils": ^7.8.0 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 7ed1c1d9b9e5b64ef028ea5e755c0be2d4e5e4e3d6cf7df757b9a8c4cfa4193d268176d0f1f7fbecdda6fe722885c7fda681f480f3741d8a2d26854736f05367 - languageName: node - linkType: hard - -"@babel/plugin-syntax-class-properties@npm:^7.12.13": - version: 7.12.13 - resolution: "@babel/plugin-syntax-class-properties@npm:7.12.13" - dependencies: - "@babel/helper-plugin-utils": ^7.12.13 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 24f34b196d6342f28d4bad303612d7ff566ab0a013ce89e775d98d6f832969462e7235f3e7eaf17678a533d4be0ba45d3ae34ab4e5a9dcbda5d98d49e5efa2fc - languageName: node - linkType: hard - -"@babel/plugin-syntax-class-static-block@npm:^7.14.5": - version: 7.14.5 - resolution: "@babel/plugin-syntax-class-static-block@npm:7.14.5" - dependencies: - "@babel/helper-plugin-utils": ^7.14.5 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 3e80814b5b6d4fe17826093918680a351c2d34398a914ce6e55d8083d72a9bdde4fbaf6a2dcea0e23a03de26dc2917ae3efd603d27099e2b98380345703bf948 - languageName: node - linkType: hard - -"@babel/plugin-syntax-dynamic-import@npm:^7.8.3": - version: 7.8.3 - resolution: "@babel/plugin-syntax-dynamic-import@npm:7.8.3" - dependencies: - "@babel/helper-plugin-utils": ^7.8.0 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: ce307af83cf433d4ec42932329fad25fa73138ab39c7436882ea28742e1c0066626d224e0ad2988724c82644e41601cef607b36194f695cb78a1fcdc959637bd - languageName: node - linkType: hard - -"@babel/plugin-syntax-export-namespace-from@npm:^7.8.3": - version: 7.8.3 - resolution: "@babel/plugin-syntax-export-namespace-from@npm:7.8.3" - dependencies: - "@babel/helper-plugin-utils": ^7.8.3 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 85740478be5b0de185228e7814451d74ab8ce0a26fcca7613955262a26e99e8e15e9da58f60c754b84515d4c679b590dbd3f2148f0f58025f4ae706f1c5a5d4a - languageName: node - linkType: hard - -"@babel/plugin-syntax-import-assertions@npm:^7.18.6": - version: 7.18.6 - resolution: "@babel/plugin-syntax-import-assertions@npm:7.18.6" - dependencies: - "@babel/helper-plugin-utils": ^7.18.6 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 54918a05375325ba0c60bc81abfb261e6f118bed2de94e4c17dca9a2006fc25e13b1a8b5504b9a881238ea394fd2f098f60b2eb3a392585d6348874565445e7b - languageName: node - linkType: hard - -"@babel/plugin-syntax-json-strings@npm:^7.8.3": - version: 7.8.3 - resolution: "@babel/plugin-syntax-json-strings@npm:7.8.3" - dependencies: - "@babel/helper-plugin-utils": ^7.8.0 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: bf5aea1f3188c9a507e16efe030efb996853ca3cadd6512c51db7233cc58f3ac89ff8c6bdfb01d30843b161cfe7d321e1bf28da82f7ab8d7e6bc5464666f354a - languageName: node - linkType: hard - -"@babel/plugin-syntax-logical-assignment-operators@npm:^7.10.4": - version: 7.10.4 - resolution: "@babel/plugin-syntax-logical-assignment-operators@npm:7.10.4" - dependencies: - "@babel/helper-plugin-utils": ^7.10.4 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: aff33577037e34e515911255cdbb1fd39efee33658aa00b8a5fd3a4b903585112d037cce1cc9e4632f0487dc554486106b79ccd5ea63a2e00df4363f6d4ff886 - languageName: node - linkType: hard - -"@babel/plugin-syntax-nullish-coalescing-operator@npm:^7.8.3": - version: 7.8.3 - resolution: "@babel/plugin-syntax-nullish-coalescing-operator@npm:7.8.3" - dependencies: - "@babel/helper-plugin-utils": ^7.8.0 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 87aca4918916020d1fedba54c0e232de408df2644a425d153be368313fdde40d96088feed6c4e5ab72aac89be5d07fef2ddf329a15109c5eb65df006bf2580d1 - languageName: node - linkType: hard - -"@babel/plugin-syntax-numeric-separator@npm:^7.10.4": - version: 7.10.4 - resolution: "@babel/plugin-syntax-numeric-separator@npm:7.10.4" - dependencies: - "@babel/helper-plugin-utils": ^7.10.4 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 01ec5547bd0497f76cc903ff4d6b02abc8c05f301c88d2622b6d834e33a5651aa7c7a3d80d8d57656a4588f7276eba357f6b7e006482f5b564b7a6488de493a1 - languageName: node - linkType: hard - -"@babel/plugin-syntax-object-rest-spread@npm:^7.8.3": - version: 7.8.3 - resolution: "@babel/plugin-syntax-object-rest-spread@npm:7.8.3" - dependencies: - "@babel/helper-plugin-utils": ^7.8.0 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: fddcf581a57f77e80eb6b981b10658421bc321ba5f0a5b754118c6a92a5448f12a0c336f77b8abf734841e102e5126d69110a306eadb03ca3e1547cab31f5cbf - languageName: node - linkType: hard - -"@babel/plugin-syntax-optional-catch-binding@npm:^7.8.3": - version: 7.8.3 - resolution: "@babel/plugin-syntax-optional-catch-binding@npm:7.8.3" - dependencies: - "@babel/helper-plugin-utils": ^7.8.0 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 910d90e72bc90ea1ce698e89c1027fed8845212d5ab588e35ef91f13b93143845f94e2539d831dc8d8ededc14ec02f04f7bd6a8179edd43a326c784e7ed7f0b9 - languageName: node - linkType: hard - -"@babel/plugin-syntax-optional-chaining@npm:^7.8.3": - version: 7.8.3 - resolution: "@babel/plugin-syntax-optional-chaining@npm:7.8.3" - dependencies: - "@babel/helper-plugin-utils": ^7.8.0 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: eef94d53a1453361553c1f98b68d17782861a04a392840341bc91780838dd4e695209c783631cf0de14c635758beafb6a3a65399846ffa4386bff90639347f30 - languageName: node - linkType: hard - -"@babel/plugin-syntax-private-property-in-object@npm:^7.14.5": - version: 7.14.5 - resolution: "@babel/plugin-syntax-private-property-in-object@npm:7.14.5" - dependencies: - "@babel/helper-plugin-utils": ^7.14.5 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: b317174783e6e96029b743ccff2a67d63d38756876e7e5d0ba53a322e38d9ca452c13354a57de1ad476b4c066dbae699e0ca157441da611117a47af88985ecda - languageName: node - linkType: hard - -"@babel/plugin-syntax-top-level-await@npm:^7.14.5": - version: 7.14.5 - resolution: "@babel/plugin-syntax-top-level-await@npm:7.14.5" - dependencies: - "@babel/helper-plugin-utils": ^7.14.5 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: bbd1a56b095be7820029b209677b194db9b1d26691fe999856462e66b25b281f031f3dfd91b1619e9dcf95bebe336211833b854d0fb8780d618e35667c2d0d7e - languageName: node - linkType: hard - -"@babel/plugin-syntax-typescript@npm:^7.18.6": - version: 7.18.6 - resolution: "@babel/plugin-syntax-typescript@npm:7.18.6" - dependencies: - "@babel/helper-plugin-utils": ^7.18.6 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 2cde73725ec51118ebf410bf02d78781c03fa4d3185993fcc9d253b97443381b621c44810084c5dd68b92eb8bdfae0e5b163e91b32bebbb33852383d1815c05d - languageName: node - linkType: hard - -"@babel/plugin-transform-arrow-functions@npm:^7.18.6": - version: 7.18.6 - resolution: "@babel/plugin-transform-arrow-functions@npm:7.18.6" - dependencies: - "@babel/helper-plugin-utils": ^7.18.6 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 900f5c695755062b91eec74da6f9092f40b8fada099058b92576f1e23c55e9813ec437051893a9b3c05cefe39e8ac06303d4a91b384e1c03dd8dc1581ea11602 - languageName: node - linkType: hard - -"@babel/plugin-transform-async-to-generator@npm:^7.18.6": - version: 7.18.6 - resolution: "@babel/plugin-transform-async-to-generator@npm:7.18.6" - dependencies: - "@babel/helper-module-imports": ^7.18.6 - "@babel/helper-plugin-utils": ^7.18.6 - "@babel/helper-remap-async-to-generator": ^7.18.6 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: c2cca47468cf1aeefdc7ec35d670e195c86cee4de28a1970648c46a88ce6bd1806ef0bab27251b9e7fb791bb28a64dcd543770efd899f28ee5f7854e64e873d3 - languageName: node - linkType: hard - -"@babel/plugin-transform-block-scoped-functions@npm:^7.18.6": - version: 7.18.6 - resolution: "@babel/plugin-transform-block-scoped-functions@npm:7.18.6" - dependencies: - "@babel/helper-plugin-utils": ^7.18.6 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 0a0df61f94601e3666bf39f2cc26f5f7b22a94450fb93081edbed967bd752ce3f81d1227fefd3799f5ee2722171b5e28db61379234d1bb85b6ec689589f99d7e - languageName: node - linkType: hard - -"@babel/plugin-transform-block-scoping@npm:^7.19.4": - version: 7.20.0 - resolution: "@babel/plugin-transform-block-scoping@npm:7.20.0" - dependencies: - "@babel/helper-plugin-utils": ^7.19.0 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: ff5ba1a2c481047e3a1fd880e78a4942ad05c0ead1424e2db150fa4009b86707d66e945173abb14451ed5ca605a19620a2b9414d16407d296326ab26219ef511 - languageName: node - linkType: hard - -"@babel/plugin-transform-classes@npm:^7.19.0": - version: 7.19.0 - resolution: "@babel/plugin-transform-classes@npm:7.19.0" - dependencies: - "@babel/helper-annotate-as-pure": ^7.18.6 - "@babel/helper-compilation-targets": ^7.19.0 - "@babel/helper-environment-visitor": ^7.18.9 - "@babel/helper-function-name": ^7.19.0 - "@babel/helper-optimise-call-expression": ^7.18.6 - "@babel/helper-plugin-utils": ^7.19.0 - "@babel/helper-replace-supers": ^7.18.9 - "@babel/helper-split-export-declaration": ^7.18.6 - globals: ^11.1.0 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 5500953031fc3eae73f717c7b59ef406158a4a710d566a0f78a4944240bcf98f817f07cf1d6af0e749e21f0dfee29c36412b75d57b0a753c3ad823b70c596b79 - languageName: node - linkType: hard - -"@babel/plugin-transform-computed-properties@npm:^7.18.9": - version: 7.18.9 - resolution: "@babel/plugin-transform-computed-properties@npm:7.18.9" - dependencies: - "@babel/helper-plugin-utils": ^7.18.9 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: a6bfbea207827d77592628973c0e8cc3319db636506bdc6e81e21582de2e767890e6975b382d0511e9ec3773b9f43691185df90832883bbf9251f688d27fbc1d - languageName: node - linkType: hard - -"@babel/plugin-transform-destructuring@npm:^7.19.4": - version: 7.20.0 - resolution: "@babel/plugin-transform-destructuring@npm:7.20.0" - dependencies: - "@babel/helper-plugin-utils": ^7.19.0 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: ce43dfcc36254ac2aef386465942f46f9901cec5d14e8aef68ebced71d46fed7d9ec88046fe2e47a57a769a26cc20ec61c4c4f13efb733ad3a82edea52aa7bdf - languageName: node - linkType: hard - -"@babel/plugin-transform-dotall-regex@npm:^7.18.6, @babel/plugin-transform-dotall-regex@npm:^7.4.4": - version: 7.18.6 - resolution: "@babel/plugin-transform-dotall-regex@npm:7.18.6" - dependencies: - "@babel/helper-create-regexp-features-plugin": ^7.18.6 - "@babel/helper-plugin-utils": ^7.18.6 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: cbe5d7063eb8f8cca24cd4827bc97f5641166509e58781a5f8aa47fb3d2d786ce4506a30fca2e01f61f18792783a5cb5d96bf5434c3dd1ad0de8c9cc625a53da - languageName: node - linkType: hard - -"@babel/plugin-transform-duplicate-keys@npm:^7.18.9": - version: 7.18.9 - resolution: "@babel/plugin-transform-duplicate-keys@npm:7.18.9" - dependencies: - "@babel/helper-plugin-utils": ^7.18.9 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 220bf4a9fec5c4d4a7b1de38810350260e8ea08481bf78332a464a21256a95f0df8cd56025f346238f09b04f8e86d4158fafc9f4af57abaef31637e3b58bd4fe - languageName: node - linkType: hard - -"@babel/plugin-transform-exponentiation-operator@npm:^7.18.6": - version: 7.18.6 - resolution: "@babel/plugin-transform-exponentiation-operator@npm:7.18.6" - dependencies: - "@babel/helper-builder-binary-assignment-operator-visitor": ^7.18.6 - "@babel/helper-plugin-utils": ^7.18.6 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 7f70222f6829c82a36005508d34ddbe6fd0974ae190683a8670dd6ff08669aaf51fef2209d7403f9bd543cb2d12b18458016c99a6ed0332ccedb3ea127b01229 - languageName: node - linkType: hard - -"@babel/plugin-transform-for-of@npm:^7.18.8": - version: 7.18.8 - resolution: "@babel/plugin-transform-for-of@npm:7.18.8" - dependencies: - "@babel/helper-plugin-utils": ^7.18.6 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: ca64c623cf0c7a80ab6f07ebd3e6e4ade95e2ae806696f70b43eafe6394fa8ce21f2b1ffdd15df2067f7363d2ecfe26472a97c6c774403d2163fa05f50c98f17 - languageName: node - linkType: hard - -"@babel/plugin-transform-function-name@npm:^7.18.9": - version: 7.18.9 - resolution: "@babel/plugin-transform-function-name@npm:7.18.9" - dependencies: - "@babel/helper-compilation-targets": ^7.18.9 - "@babel/helper-function-name": ^7.18.9 - "@babel/helper-plugin-utils": ^7.18.9 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 62dd9c6cdc9714704efe15545e782ee52d74dc73916bf954b4d3bee088fb0ec9e3c8f52e751252433656c09f744b27b757fc06ed99bcde28e8a21600a1d8e597 - languageName: node - linkType: hard - -"@babel/plugin-transform-literals@npm:^7.18.9": - version: 7.18.9 - resolution: "@babel/plugin-transform-literals@npm:7.18.9" - dependencies: - "@babel/helper-plugin-utils": ^7.18.9 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 3458dd2f1a47ac51d9d607aa18f3d321cbfa8560a985199185bed5a906bb0c61ba85575d386460bac9aed43fdd98940041fae5a67dff286f6f967707cff489f8 - languageName: node - linkType: hard - -"@babel/plugin-transform-member-expression-literals@npm:^7.18.6": - version: 7.18.6 - resolution: "@babel/plugin-transform-member-expression-literals@npm:7.18.6" - dependencies: - "@babel/helper-plugin-utils": ^7.18.6 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 35a3d04f6693bc6b298c05453d85ee6e41cc806538acb6928427e0e97ae06059f97d2f07d21495fcf5f70d3c13a242e2ecbd09d5c1fcb1b1a73ff528dcb0b695 - languageName: node - linkType: hard - -"@babel/plugin-transform-modules-amd@npm:^7.18.6": - version: 7.19.6 - resolution: "@babel/plugin-transform-modules-amd@npm:7.19.6" - dependencies: - "@babel/helper-module-transforms": ^7.19.6 - "@babel/helper-plugin-utils": ^7.19.0 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 4236aad970025bc10c772c1589b1e2eab8b7681933bb5ffa6e395d4c1a52532b28c47c553e3011b4272ea81e5ab39fe969eb5349584e8390e59771055c467d42 - languageName: node - linkType: hard - -"@babel/plugin-transform-modules-commonjs@npm:^7.18.6": - version: 7.19.6 - resolution: "@babel/plugin-transform-modules-commonjs@npm:7.19.6" - dependencies: - "@babel/helper-module-transforms": ^7.19.6 - "@babel/helper-plugin-utils": ^7.19.0 - "@babel/helper-simple-access": ^7.19.4 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 85d46945ab5ba3fff89e962d560a5d40253f228b9659a697683db3de07c0236e8cd60e5eb41958007359951a42bc268bf32350fcdb5b4a86f58dff1e032c096e - languageName: node - linkType: hard - -"@babel/plugin-transform-modules-systemjs@npm:^7.19.0": - version: 7.19.6 - resolution: "@babel/plugin-transform-modules-systemjs@npm:7.19.6" - dependencies: - "@babel/helper-hoist-variables": ^7.18.6 - "@babel/helper-module-transforms": ^7.19.6 - "@babel/helper-plugin-utils": ^7.19.0 - "@babel/helper-validator-identifier": ^7.19.1 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 8526431cc81ea3eb232ad50862d0ed1cbb422b5251d14a8d6610d0ca0617f6e75f35179e98eb1235d0cccb980120350b9f112594e5646dd45378d41eaaf87342 - languageName: node - linkType: hard - -"@babel/plugin-transform-modules-umd@npm:^7.18.6": - version: 7.18.6 - resolution: "@babel/plugin-transform-modules-umd@npm:7.18.6" - dependencies: - "@babel/helper-module-transforms": ^7.18.6 - "@babel/helper-plugin-utils": ^7.18.6 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: c3b6796c6f4579f1ba5ab0cdcc73910c1e9c8e1e773c507c8bb4da33072b3ae5df73c6d68f9126dab6e99c24ea8571e1563f8710d7c421fac1cde1e434c20153 - languageName: node - linkType: hard - -"@babel/plugin-transform-named-capturing-groups-regex@npm:^7.19.1": - version: 7.19.1 - resolution: "@babel/plugin-transform-named-capturing-groups-regex@npm:7.19.1" - dependencies: - "@babel/helper-create-regexp-features-plugin": ^7.19.0 - "@babel/helper-plugin-utils": ^7.19.0 - peerDependencies: - "@babel/core": ^7.0.0 - checksum: 8a40f5d04f2140c44fe890a5a3fd72abc2a88445443ac2bd92e1e85d9366d3eb8f1ebb7e2c89d2daeaf213d9b28cb65605502ac9b155936d48045eeda6053494 - languageName: node - linkType: hard - -"@babel/plugin-transform-new-target@npm:^7.18.6": - version: 7.18.6 - resolution: "@babel/plugin-transform-new-target@npm:7.18.6" - dependencies: - "@babel/helper-plugin-utils": ^7.18.6 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: bd780e14f46af55d0ae8503b3cb81ca86dcc73ed782f177e74f498fff934754f9e9911df1f8f3bd123777eed7c1c1af4d66abab87c8daae5403e7719a6b845d1 - languageName: node - linkType: hard - -"@babel/plugin-transform-object-super@npm:^7.18.6": - version: 7.18.6 - resolution: "@babel/plugin-transform-object-super@npm:7.18.6" - dependencies: - "@babel/helper-plugin-utils": ^7.18.6 - "@babel/helper-replace-supers": ^7.18.6 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 0fcb04e15deea96ae047c21cb403607d49f06b23b4589055993365ebd7a7d7541334f06bf9642e90075e66efce6ebaf1eb0ef066fbbab802d21d714f1aac3aef - languageName: node - linkType: hard - -"@babel/plugin-transform-parameters@npm:^7.18.8": - version: 7.18.8 - resolution: "@babel/plugin-transform-parameters@npm:7.18.8" - dependencies: - "@babel/helper-plugin-utils": ^7.18.6 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 2b5863300da60face8a250d91da16294333bd5626e9721b13a3ba2078bd2a5a190e32c6e7a1323d5f547f579aeb2804ff49a62a55fcad2b1d099e55a55b788ea - languageName: node - linkType: hard - -"@babel/plugin-transform-property-literals@npm:^7.18.6": - version: 7.18.6 - resolution: "@babel/plugin-transform-property-literals@npm:7.18.6" - dependencies: - "@babel/helper-plugin-utils": ^7.18.6 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 1c16e64de554703f4b547541de2edda6c01346dd3031d4d29e881aa7733785cd26d53611a4ccf5353f4d3e69097bb0111c0a93ace9e683edd94fea28c4484144 - languageName: node - linkType: hard - -"@babel/plugin-transform-regenerator@npm:^7.18.6": - version: 7.18.6 - resolution: "@babel/plugin-transform-regenerator@npm:7.18.6" - dependencies: - "@babel/helper-plugin-utils": ^7.18.6 - regenerator-transform: ^0.15.0 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 60bd482cb0343c714f85c3e19a13b3b5fa05ee336c079974091c0b35e263307f4e661f4555dff90707a87d5efe19b1d51835db44455405444ac1813e268ad750 - languageName: node - linkType: hard - -"@babel/plugin-transform-reserved-words@npm:^7.18.6": - version: 7.18.6 - resolution: "@babel/plugin-transform-reserved-words@npm:7.18.6" - dependencies: - "@babel/helper-plugin-utils": ^7.18.6 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 0738cdc30abdae07c8ec4b233b30c31f68b3ff0eaa40eddb45ae607c066127f5fa99ddad3c0177d8e2832e3a7d3ad115775c62b431ebd6189c40a951b867a80c - languageName: node - linkType: hard - -"@babel/plugin-transform-shorthand-properties@npm:^7.18.6": - version: 7.18.6 - resolution: "@babel/plugin-transform-shorthand-properties@npm:7.18.6" - dependencies: - "@babel/helper-plugin-utils": ^7.18.6 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: b8e4e8acc2700d1e0d7d5dbfd4fdfb935651913de6be36e6afb7e739d8f9ca539a5150075a0f9b79c88be25ddf45abb912fe7abf525f0b80f5b9d9860de685d7 - languageName: node - linkType: hard - -"@babel/plugin-transform-spread@npm:^7.19.0": - version: 7.19.0 - resolution: "@babel/plugin-transform-spread@npm:7.19.0" - dependencies: - "@babel/helper-plugin-utils": ^7.19.0 - "@babel/helper-skip-transparent-expression-wrappers": ^7.18.9 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: e73a4deb095999185e70b524d0ff4e35df50fcda58299e700a6149a15bbc1a9b369ef1cef384e15a54b3c3ce316cc0f054dbf249dcd0d1ca59f4281dd4df9718 - languageName: node - linkType: hard - -"@babel/plugin-transform-sticky-regex@npm:^7.18.6": - version: 7.18.6 - resolution: "@babel/plugin-transform-sticky-regex@npm:7.18.6" - dependencies: - "@babel/helper-plugin-utils": ^7.18.6 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 68ea18884ae9723443ffa975eb736c8c0d751265859cd3955691253f7fee37d7a0f7efea96c8a062876af49a257a18ea0ed5fea0d95a7b3611ce40f7ee23aee3 - languageName: node - linkType: hard - -"@babel/plugin-transform-template-literals@npm:^7.18.9": - version: 7.18.9 - resolution: "@babel/plugin-transform-template-literals@npm:7.18.9" - dependencies: - "@babel/helper-plugin-utils": ^7.18.9 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 3d2fcd79b7c345917f69b92a85bdc3ddd68ce2c87dc70c7d61a8373546ccd1f5cb8adc8540b49dfba08e1b82bb7b3bbe23a19efdb2b9c994db2db42906ca9fb2 - languageName: node - linkType: hard - -"@babel/plugin-transform-typeof-symbol@npm:^7.18.9": - version: 7.18.9 - resolution: "@babel/plugin-transform-typeof-symbol@npm:7.18.9" - dependencies: - "@babel/helper-plugin-utils": ^7.18.9 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: e754e0d8b8a028c52e10c148088606e3f7a9942c57bd648fc0438e5b4868db73c386a5ed47ab6d6f0594aae29ee5ffc2ffc0f7ebee7fae560a066d6dea811cd4 - languageName: node - linkType: hard - -"@babel/plugin-transform-typescript@npm:^7.18.6": - version: 7.19.3 - resolution: "@babel/plugin-transform-typescript@npm:7.19.3" - dependencies: - "@babel/helper-create-class-features-plugin": ^7.19.0 - "@babel/helper-plugin-utils": ^7.19.0 - "@babel/plugin-syntax-typescript": ^7.18.6 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 094c8c4ad05fee7f1bda243923cadb2b05b58448ea7cf9560e734c7cffd72acfeab3e1e0d2b084eb47a5a950ac316f970b83904b3504c203fa6fe9d8d61526a8 - languageName: node - linkType: hard - -"@babel/plugin-transform-unicode-escapes@npm:^7.18.10": - version: 7.18.10 - resolution: "@babel/plugin-transform-unicode-escapes@npm:7.18.10" - dependencies: - "@babel/helper-plugin-utils": ^7.18.9 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: f5baca55cb3c11bc08ec589f5f522d85c1ab509b4d11492437e45027d64ae0b22f0907bd1381e8d7f2a436384bb1f9ad89d19277314242c5c2671a0f91d0f9cd - languageName: node - linkType: hard - -"@babel/plugin-transform-unicode-regex@npm:^7.18.6": - version: 7.18.6 - resolution: "@babel/plugin-transform-unicode-regex@npm:7.18.6" - dependencies: - "@babel/helper-create-regexp-features-plugin": ^7.18.6 - "@babel/helper-plugin-utils": ^7.18.6 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: d9e18d57536a2d317fb0b7c04f8f55347f3cfacb75e636b4c6fa2080ab13a3542771b5120e726b598b815891fc606d1472ac02b749c69fd527b03847f22dc25e - languageName: node - linkType: hard - -"@babel/preset-env@npm:7.19.4": - version: 7.19.4 - resolution: "@babel/preset-env@npm:7.19.4" - dependencies: - "@babel/compat-data": ^7.19.4 - "@babel/helper-compilation-targets": ^7.19.3 - "@babel/helper-plugin-utils": ^7.19.0 - "@babel/helper-validator-option": ^7.18.6 - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": ^7.18.6 - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": ^7.18.9 - "@babel/plugin-proposal-async-generator-functions": ^7.19.1 - "@babel/plugin-proposal-class-properties": ^7.18.6 - "@babel/plugin-proposal-class-static-block": ^7.18.6 - "@babel/plugin-proposal-dynamic-import": ^7.18.6 - "@babel/plugin-proposal-export-namespace-from": ^7.18.9 - "@babel/plugin-proposal-json-strings": ^7.18.6 - "@babel/plugin-proposal-logical-assignment-operators": ^7.18.9 - "@babel/plugin-proposal-nullish-coalescing-operator": ^7.18.6 - "@babel/plugin-proposal-numeric-separator": ^7.18.6 - "@babel/plugin-proposal-object-rest-spread": ^7.19.4 - "@babel/plugin-proposal-optional-catch-binding": ^7.18.6 - "@babel/plugin-proposal-optional-chaining": ^7.18.9 - "@babel/plugin-proposal-private-methods": ^7.18.6 - "@babel/plugin-proposal-private-property-in-object": ^7.18.6 - "@babel/plugin-proposal-unicode-property-regex": ^7.18.6 - "@babel/plugin-syntax-async-generators": ^7.8.4 - "@babel/plugin-syntax-class-properties": ^7.12.13 - "@babel/plugin-syntax-class-static-block": ^7.14.5 - "@babel/plugin-syntax-dynamic-import": ^7.8.3 - "@babel/plugin-syntax-export-namespace-from": ^7.8.3 - "@babel/plugin-syntax-import-assertions": ^7.18.6 - "@babel/plugin-syntax-json-strings": ^7.8.3 - "@babel/plugin-syntax-logical-assignment-operators": ^7.10.4 - "@babel/plugin-syntax-nullish-coalescing-operator": ^7.8.3 - "@babel/plugin-syntax-numeric-separator": ^7.10.4 - "@babel/plugin-syntax-object-rest-spread": ^7.8.3 - "@babel/plugin-syntax-optional-catch-binding": ^7.8.3 - "@babel/plugin-syntax-optional-chaining": ^7.8.3 - "@babel/plugin-syntax-private-property-in-object": ^7.14.5 - "@babel/plugin-syntax-top-level-await": ^7.14.5 - "@babel/plugin-transform-arrow-functions": ^7.18.6 - "@babel/plugin-transform-async-to-generator": ^7.18.6 - "@babel/plugin-transform-block-scoped-functions": ^7.18.6 - "@babel/plugin-transform-block-scoping": ^7.19.4 - "@babel/plugin-transform-classes": ^7.19.0 - "@babel/plugin-transform-computed-properties": ^7.18.9 - "@babel/plugin-transform-destructuring": ^7.19.4 - "@babel/plugin-transform-dotall-regex": ^7.18.6 - "@babel/plugin-transform-duplicate-keys": ^7.18.9 - "@babel/plugin-transform-exponentiation-operator": ^7.18.6 - "@babel/plugin-transform-for-of": ^7.18.8 - "@babel/plugin-transform-function-name": ^7.18.9 - "@babel/plugin-transform-literals": ^7.18.9 - "@babel/plugin-transform-member-expression-literals": ^7.18.6 - "@babel/plugin-transform-modules-amd": ^7.18.6 - "@babel/plugin-transform-modules-commonjs": ^7.18.6 - "@babel/plugin-transform-modules-systemjs": ^7.19.0 - "@babel/plugin-transform-modules-umd": ^7.18.6 - "@babel/plugin-transform-named-capturing-groups-regex": ^7.19.1 - "@babel/plugin-transform-new-target": ^7.18.6 - "@babel/plugin-transform-object-super": ^7.18.6 - "@babel/plugin-transform-parameters": ^7.18.8 - "@babel/plugin-transform-property-literals": ^7.18.6 - "@babel/plugin-transform-regenerator": ^7.18.6 - "@babel/plugin-transform-reserved-words": ^7.18.6 - "@babel/plugin-transform-shorthand-properties": ^7.18.6 - "@babel/plugin-transform-spread": ^7.19.0 - "@babel/plugin-transform-sticky-regex": ^7.18.6 - "@babel/plugin-transform-template-literals": ^7.18.9 - "@babel/plugin-transform-typeof-symbol": ^7.18.9 - "@babel/plugin-transform-unicode-escapes": ^7.18.10 - "@babel/plugin-transform-unicode-regex": ^7.18.6 - "@babel/preset-modules": ^0.1.5 - "@babel/types": ^7.19.4 - babel-plugin-polyfill-corejs2: ^0.3.3 - babel-plugin-polyfill-corejs3: ^0.6.0 - babel-plugin-polyfill-regenerator: ^0.4.1 - core-js-compat: ^3.25.1 - semver: ^6.3.0 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: f12af25281f3c5e7df60fa1e79ad481ddd7f6a111d4c0fabcffdabf0eaed3a01b4f8c647ae5445ed1f58df70f52083ffd283e8919ade7afa73801a49c733d22c - languageName: node - linkType: hard - -"@babel/preset-modules@npm:^0.1.5": - version: 0.1.5 - resolution: "@babel/preset-modules@npm:0.1.5" - dependencies: - "@babel/helper-plugin-utils": ^7.0.0 - "@babel/plugin-proposal-unicode-property-regex": ^7.4.4 - "@babel/plugin-transform-dotall-regex": ^7.4.4 - "@babel/types": ^7.4.4 - esutils: ^2.0.2 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 8430e0e9e9d520b53e22e8c4c6a5a080a12b63af6eabe559c2310b187bd62ae113f3da82ba33e9d1d0f3230930ca702843aae9dd226dec51f7d7114dc1f51c10 - languageName: node - linkType: hard - -"@babel/preset-typescript@npm:7.18.6": - version: 7.18.6 - resolution: "@babel/preset-typescript@npm:7.18.6" - dependencies: - "@babel/helper-plugin-utils": ^7.18.6 - "@babel/helper-validator-option": ^7.18.6 - "@babel/plugin-transform-typescript": ^7.18.6 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 7fe0da5103eb72d3cf39cf3e138a794c8cdd19c0b38e3e101507eef519c46a87a0d6d0e8bc9e28a13ea2364001ebe7430b9d75758aab4c3c3a8db9a487b9dc7c - languageName: node - linkType: hard - -"@babel/runtime@npm:^7.8.4": - version: 7.19.4 - resolution: "@babel/runtime@npm:7.19.4" - dependencies: - regenerator-runtime: ^0.13.4 - checksum: 66b7e3c13e9ee1d2c9397ea89144f29a875edee266a0eb2d9971be51b32fdbafc85808c7a45e011e6681899bb804b4e2ee2aed6dc07108dbbd6b11b6cc2afba6 - languageName: node - linkType: hard - -"@babel/template@npm:^7.18.10": - version: 7.18.10 - resolution: "@babel/template@npm:7.18.10" - dependencies: - "@babel/code-frame": ^7.18.6 - "@babel/parser": ^7.18.10 - "@babel/types": ^7.18.10 - checksum: 93a6aa094af5f355a72bd55f67fa1828a046c70e46f01b1606e6118fa1802b6df535ca06be83cc5a5e834022be95c7b714f0a268b5f20af984465a71e28f1473 - languageName: node - linkType: hard - -"@babel/traverse@npm:^7.19.0, @babel/traverse@npm:^7.19.1, @babel/traverse@npm:^7.19.6": - version: 7.19.6 - resolution: "@babel/traverse@npm:7.19.6" - dependencies: - "@babel/code-frame": ^7.18.6 - "@babel/generator": ^7.19.6 - "@babel/helper-environment-visitor": ^7.18.9 - "@babel/helper-function-name": ^7.19.0 - "@babel/helper-hoist-variables": ^7.18.6 - "@babel/helper-split-export-declaration": ^7.18.6 - "@babel/parser": ^7.19.6 - "@babel/types": ^7.19.4 - debug: ^4.1.0 - globals: ^11.1.0 - checksum: 3fafa244f7d0b696a9d38f5da016a8f8db4b08ac60a067b299a8f54d91fb7c70c3edf06f921221d333137e65ffb64392526e68fdcf596ec91e95720037789d66 - languageName: node - linkType: hard - -"@babel/traverse@npm:^7.20.1": - version: 7.20.1 - resolution: "@babel/traverse@npm:7.20.1" - dependencies: - "@babel/code-frame": ^7.18.6 - "@babel/generator": ^7.20.1 - "@babel/helper-environment-visitor": ^7.18.9 - "@babel/helper-function-name": ^7.19.0 - "@babel/helper-hoist-variables": ^7.18.6 - "@babel/helper-split-export-declaration": ^7.18.6 - "@babel/parser": ^7.20.1 - "@babel/types": ^7.20.0 - debug: ^4.1.0 - globals: ^11.1.0 - checksum: 6696176d574b7ff93466848010bc7e94b250169379ec2a84f1b10da46a7cc2018ea5e3a520c3078487db51e3a4afab9ecff48f25d1dbad8c1319362f4148fb4b - languageName: node - linkType: hard - -"@babel/types@npm:^7.18.10, @babel/types@npm:^7.18.6, @babel/types@npm:^7.18.9, @babel/types@npm:^7.19.0, @babel/types@npm:^7.19.4, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3": - version: 7.19.4 - resolution: "@babel/types@npm:7.19.4" - dependencies: - "@babel/helper-string-parser": ^7.19.4 - "@babel/helper-validator-identifier": ^7.19.1 - to-fast-properties: ^2.0.0 - checksum: 4032f6407093f80dd4f4764be676f7527d2a5c0381586967cd79683cf8af01cdc16745a381b9cef045f702f0c9b0dffd880d84ee55dad59ba01bd23d5d52a8e0 - languageName: node - linkType: hard - -"@babel/types@npm:^7.20.0": - version: 7.20.0 - resolution: "@babel/types@npm:7.20.0" - dependencies: - "@babel/helper-string-parser": ^7.19.4 - "@babel/helper-validator-identifier": ^7.19.1 - to-fast-properties: ^2.0.0 - checksum: 8729b1114c707a03625cd79e3ae3a28d69b36ddcf804cb0a4599af226e5e9fad71665bdc0e56c43527ecfcabc545d9c797231f5ce718ae1ab52d31a57b6c2024 - languageName: node - linkType: hard - -"@cspotcode/source-map-support@npm:^0.8.0": - version: 0.8.1 - resolution: "@cspotcode/source-map-support@npm:0.8.1" - dependencies: - "@jridgewell/trace-mapping": 0.3.9 - checksum: 5718f267085ed8edb3e7ef210137241775e607ee18b77d95aa5bd7514f47f5019aa2d82d96b3bf342ef7aa890a346fa1044532ff7cc3009e7d24fce3ce6200fa - languageName: node - linkType: hard - -"@discoveryjs/json-ext@npm:^0.5.0": - version: 0.5.7 - resolution: "@discoveryjs/json-ext@npm:0.5.7" - checksum: 2176d301cc258ea5c2324402997cf8134ebb212469c0d397591636cea8d3c02f2b3cf9fd58dcb748c7a0dade77ebdc1b10284fa63e608c033a1db52fddc69918 - languageName: node - linkType: hard - -"@grafana/perf-tests@workspace:.": - version: 0.0.0-use.local - resolution: "@grafana/perf-tests@workspace:." - dependencies: - "@babel/core": 7.19.6 - "@babel/plugin-proposal-class-properties": 7.18.6 - "@babel/plugin-proposal-object-rest-spread": 7.19.4 - "@babel/preset-env": 7.19.4 - "@babel/preset-typescript": 7.18.6 - "@types/k6": 0.41.0 - "@types/shortid": 0.0.29 - "@types/webpack": 5.28.0 - babel-loader: 9.1.0 - shortid: 2.2.16 - ts-node: 10.9.1 - typescript: 4.8.4 - webpack: 5.74.0 - webpack-cli: 4.10.0 - webpack-glob-entries: 1.0.1 - languageName: unknown - linkType: soft - -"@jridgewell/gen-mapping@npm:^0.1.0": - version: 0.1.1 - resolution: "@jridgewell/gen-mapping@npm:0.1.1" - dependencies: - "@jridgewell/set-array": ^1.0.0 - "@jridgewell/sourcemap-codec": ^1.4.10 - checksum: 3bcc21fe786de6ffbf35c399a174faab05eb23ce6a03e8769569de28abbf4facc2db36a9ddb0150545ae23a8d35a7cf7237b2aa9e9356a7c626fb4698287d5cc - languageName: node - linkType: hard - -"@jridgewell/gen-mapping@npm:^0.3.0, @jridgewell/gen-mapping@npm:^0.3.2": - version: 0.3.2 - resolution: "@jridgewell/gen-mapping@npm:0.3.2" - dependencies: - "@jridgewell/set-array": ^1.0.1 - "@jridgewell/sourcemap-codec": ^1.4.10 - "@jridgewell/trace-mapping": ^0.3.9 - checksum: 1832707a1c476afebe4d0fbbd4b9434fdb51a4c3e009ab1e9938648e21b7a97049fa6009393bdf05cab7504108413441df26d8a3c12193996e65493a4efb6882 - languageName: node - linkType: hard - -"@jridgewell/resolve-uri@npm:3.1.0, @jridgewell/resolve-uri@npm:^3.0.3": - version: 3.1.0 - resolution: "@jridgewell/resolve-uri@npm:3.1.0" - checksum: b5ceaaf9a110fcb2780d1d8f8d4a0bfd216702f31c988d8042e5f8fbe353c55d9b0f55a1733afdc64806f8e79c485d2464680ac48a0d9fcadb9548ee6b81d267 - languageName: node - linkType: hard - -"@jridgewell/set-array@npm:^1.0.0, @jridgewell/set-array@npm:^1.0.1": - version: 1.1.2 - resolution: "@jridgewell/set-array@npm:1.1.2" - checksum: 69a84d5980385f396ff60a175f7177af0b8da4ddb81824cb7016a9ef914eee9806c72b6b65942003c63f7983d4f39a5c6c27185bbca88eb4690b62075602e28e - languageName: node - linkType: hard - -"@jridgewell/source-map@npm:^0.3.2": - version: 0.3.2 - resolution: "@jridgewell/source-map@npm:0.3.2" - dependencies: - "@jridgewell/gen-mapping": ^0.3.0 - "@jridgewell/trace-mapping": ^0.3.9 - checksum: 1b83f0eb944e77b70559a394d5d3b3f98a81fcc186946aceb3ef42d036762b52ef71493c6c0a3b7c1d2f08785f53ba2df1277fe629a06e6109588ff4cdcf7482 - languageName: node - linkType: hard - -"@jridgewell/sourcemap-codec@npm:1.4.14, @jridgewell/sourcemap-codec@npm:^1.4.10": - version: 1.4.14 - resolution: "@jridgewell/sourcemap-codec@npm:1.4.14" - checksum: 61100637b6d173d3ba786a5dff019e1a74b1f394f323c1fee337ff390239f053b87266c7a948777f4b1ee68c01a8ad0ab61e5ff4abb5a012a0b091bec391ab97 - languageName: node - linkType: hard - -"@jridgewell/trace-mapping@npm:0.3.9": - version: 0.3.9 - resolution: "@jridgewell/trace-mapping@npm:0.3.9" - dependencies: - "@jridgewell/resolve-uri": ^3.0.3 - "@jridgewell/sourcemap-codec": ^1.4.10 - checksum: d89597752fd88d3f3480845691a05a44bd21faac18e2185b6f436c3b0fd0c5a859fbbd9aaa92050c4052caf325ad3e10e2e1d1b64327517471b7d51babc0ddef - languageName: node - linkType: hard - -"@jridgewell/trace-mapping@npm:^0.3.14, @jridgewell/trace-mapping@npm:^0.3.9": - version: 0.3.17 - resolution: "@jridgewell/trace-mapping@npm:0.3.17" - dependencies: - "@jridgewell/resolve-uri": 3.1.0 - "@jridgewell/sourcemap-codec": 1.4.14 - checksum: 9d703b859cff5cd83b7308fd457a431387db5db96bd781a63bf48e183418dd9d3d44e76b9e4ae13237f6abeeb25d739ec9215c1d5bfdd08f66f750a50074a339 - languageName: node - linkType: hard - -"@tsconfig/node10@npm:^1.0.7": - version: 1.0.9 - resolution: "@tsconfig/node10@npm:1.0.9" - checksum: a33ae4dc2a621c0678ac8ac4bceb8e512ae75dac65417a2ad9b022d9b5411e863c4c198b6ba9ef659e14b9fb609bbec680841a2e84c1172df7a5ffcf076539df - languageName: node - linkType: hard - -"@tsconfig/node12@npm:^1.0.7": - version: 1.0.11 - resolution: "@tsconfig/node12@npm:1.0.11" - checksum: 5ce29a41b13e7897a58b8e2df11269c5395999e588b9a467386f99d1d26f6c77d1af2719e407621412520ea30517d718d5192a32403b8dfcc163bf33e40a338a - languageName: node - linkType: hard - -"@tsconfig/node14@npm:^1.0.0": - version: 1.0.3 - resolution: "@tsconfig/node14@npm:1.0.3" - checksum: 19275fe80c4c8d0ad0abed6a96dbf00642e88b220b090418609c4376e1cef81bf16237bf170ad1b341452feddb8115d8dd2e5acdfdea1b27422071163dc9ba9d - languageName: node - linkType: hard - -"@tsconfig/node16@npm:^1.0.2": - version: 1.0.3 - resolution: "@tsconfig/node16@npm:1.0.3" - checksum: 3a8b657dd047495b7ad23437d6afd20297ce90380ff0bdee93fc7d39a900dbd8d9e26e53ff6b465e7967ce2adf0b218782590ce9013285121e6a5928fbd6819f - languageName: node - linkType: hard - -"@types/eslint-scope@npm:^3.7.3": - version: 3.7.4 - resolution: "@types/eslint-scope@npm:3.7.4" - dependencies: - "@types/eslint": "*" - "@types/estree": "*" - checksum: ea6a9363e92f301cd3888194469f9ec9d0021fe0a397a97a6dd689e7545c75de0bd2153dfb13d3ab532853a278b6572c6f678ce846980669e41029d205653460 - languageName: node - linkType: hard - -"@types/eslint@npm:*": - version: 8.4.8 - resolution: "@types/eslint@npm:8.4.8" - dependencies: - "@types/estree": "*" - "@types/json-schema": "*" - checksum: 5b4708a56adeb5c209bc5d33590499be01286a90d3c324e2aabb1812d405a622ea9dd65eb8a095b2b9eb902bc8a25afddb9832f1f634457f973c07eade86aa5e - languageName: node - linkType: hard - -"@types/estree@npm:*": - version: 1.0.0 - resolution: "@types/estree@npm:1.0.0" - checksum: 910d97fb7092c6738d30a7430ae4786a38542023c6302b95d46f49420b797f21619cdde11fa92b338366268795884111c2eb10356e4bd2c8ad5b92941e9e6443 - languageName: node - linkType: hard - -"@types/estree@npm:^0.0.51": - version: 0.0.51 - resolution: "@types/estree@npm:0.0.51" - checksum: e56a3bcf759fd9185e992e7fdb3c6a5f81e8ff120e871641607581fb3728d16c811702a7d40fa5f869b7f7b4437ab6a87eb8d98ffafeee51e85bbe955932a189 - languageName: node - linkType: hard - -"@types/json-schema@npm:*, @types/json-schema@npm:^7.0.8, @types/json-schema@npm:^7.0.9": - version: 7.0.11 - resolution: "@types/json-schema@npm:7.0.11" - checksum: 527bddfe62db9012fccd7627794bd4c71beb77601861055d87e3ee464f2217c85fca7a4b56ae677478367bbd248dbde13553312b7d4dbc702a2f2bbf60c4018d - languageName: node - linkType: hard - -"@types/k6@npm:0.41.0": - version: 0.41.0 - resolution: "@types/k6@npm:0.41.0" - checksum: efc027b5967f8fa1102eb7d0e4867d90bdbdec32153507da510827ccdc5a7a7fefac30703cfd31a8ad5550313a63a5948a369f7503490e9851f19c347bd531b5 - languageName: node - linkType: hard - -"@types/node@npm:*": - version: 18.11.5 - resolution: "@types/node@npm:18.11.5" - checksum: ac54e9287dd4549ea3dc8aabc0cf7bfa04c52f02925d7fd68414789617ec770f034c8ae2e111e8bd00d446a46fcac42587b5a316a1303e2f6ea094854248c9ff - languageName: node - linkType: hard - -"@types/shortid@npm:0.0.29": - version: 0.0.29 - resolution: "@types/shortid@npm:0.0.29" - checksum: 06d940fabc5774ff2221da6ba3496cef97ae7c2f7f0fee4740144a68cf2b630b109db2f934133096daa58045e4e7aaffba687934166bb0b2b0d5c52679b10d08 - languageName: node - linkType: hard - -"@types/webpack@npm:5.28.0": - version: 5.28.0 - resolution: "@types/webpack@npm:5.28.0" - dependencies: - "@types/node": "*" - tapable: ^2.2.0 - webpack: ^5 - checksum: a038d7e12dd109c6a8d2eb744fd32070ef94f1655e730fb1443b370db98864c3a0e408638b02d12ba08269b9c012b3be8b801117ced2d1102e7676203fd663ed - languageName: node - linkType: hard - -"@webassemblyjs/ast@npm:1.11.1": - version: 1.11.1 - resolution: "@webassemblyjs/ast@npm:1.11.1" - dependencies: - "@webassemblyjs/helper-numbers": 1.11.1 - "@webassemblyjs/helper-wasm-bytecode": 1.11.1 - checksum: 1eee1534adebeece635362f8e834ae03e389281972611408d64be7895fc49f48f98fddbbb5339bf8a72cb101bcb066e8bca3ca1bf1ef47dadf89def0395a8d87 - languageName: node - linkType: hard - -"@webassemblyjs/floating-point-hex-parser@npm:1.11.1": - version: 1.11.1 - resolution: "@webassemblyjs/floating-point-hex-parser@npm:1.11.1" - checksum: b8efc6fa08e4787b7f8e682182d84dfdf8da9d9c77cae5d293818bc4a55c1f419a87fa265ab85252b3e6c1fd323d799efea68d825d341a7c365c64bc14750e97 - languageName: node - linkType: hard - -"@webassemblyjs/helper-api-error@npm:1.11.1": - version: 1.11.1 - resolution: "@webassemblyjs/helper-api-error@npm:1.11.1" - checksum: 0792813f0ed4a0e5ee0750e8b5d0c631f08e927f4bdfdd9fe9105dc410c786850b8c61bff7f9f515fdfb149903bec3c976a1310573a4c6866a94d49bc7271959 - languageName: node - linkType: hard - -"@webassemblyjs/helper-buffer@npm:1.11.1": - version: 1.11.1 - resolution: "@webassemblyjs/helper-buffer@npm:1.11.1" - checksum: a337ee44b45590c3a30db5a8b7b68a717526cf967ada9f10253995294dbd70a58b2da2165222e0b9830cd4fc6e4c833bf441a721128d1fe2e9a7ab26b36003ce - languageName: node - linkType: hard - -"@webassemblyjs/helper-numbers@npm:1.11.1": - version: 1.11.1 - resolution: "@webassemblyjs/helper-numbers@npm:1.11.1" - dependencies: - "@webassemblyjs/floating-point-hex-parser": 1.11.1 - "@webassemblyjs/helper-api-error": 1.11.1 - "@xtuc/long": 4.2.2 - checksum: 44d2905dac2f14d1e9b5765cf1063a0fa3d57295c6d8930f6c59a36462afecc6e763e8a110b97b342a0f13376166c5d41aa928e6ced92e2f06b071fd0db59d3a - languageName: node - linkType: hard - -"@webassemblyjs/helper-wasm-bytecode@npm:1.11.1": - version: 1.11.1 - resolution: "@webassemblyjs/helper-wasm-bytecode@npm:1.11.1" - checksum: eac400113127832c88f5826bcc3ad1c0db9b3dbd4c51a723cfdb16af6bfcbceb608170fdaac0ab7731a7e18b291be7af68a47fcdb41cfe0260c10857e7413d97 - languageName: node - linkType: hard - -"@webassemblyjs/helper-wasm-section@npm:1.11.1": - version: 1.11.1 - resolution: "@webassemblyjs/helper-wasm-section@npm:1.11.1" - dependencies: - "@webassemblyjs/ast": 1.11.1 - "@webassemblyjs/helper-buffer": 1.11.1 - "@webassemblyjs/helper-wasm-bytecode": 1.11.1 - "@webassemblyjs/wasm-gen": 1.11.1 - checksum: 617696cfe8ecaf0532763162aaf748eb69096fb27950219bb87686c6b2e66e11cd0614d95d319d0ab1904bc14ebe4e29068b12c3e7c5e020281379741fe4bedf - languageName: node - linkType: hard - -"@webassemblyjs/ieee754@npm:1.11.1": - version: 1.11.1 - resolution: "@webassemblyjs/ieee754@npm:1.11.1" - dependencies: - "@xtuc/ieee754": ^1.2.0 - checksum: 23a0ac02a50f244471631802798a816524df17e56b1ef929f0c73e3cde70eaf105a24130105c60aff9d64a24ce3b640dad443d6f86e5967f922943a7115022ec - languageName: node - linkType: hard - -"@webassemblyjs/leb128@npm:1.11.1": - version: 1.11.1 - resolution: "@webassemblyjs/leb128@npm:1.11.1" - dependencies: - "@xtuc/long": 4.2.2 - checksum: 33ccc4ade2f24de07bf31690844d0b1ad224304ee2062b0e464a610b0209c79e0b3009ac190efe0e6bd568b0d1578d7c3047fc1f9d0197c92fc061f56224ff4a - languageName: node - linkType: hard - -"@webassemblyjs/utf8@npm:1.11.1": - version: 1.11.1 - resolution: "@webassemblyjs/utf8@npm:1.11.1" - checksum: 972c5cfc769d7af79313a6bfb96517253a270a4bf0c33ba486aa43cac43917184fb35e51dfc9e6b5601548cd5931479a42e42c89a13bb591ffabebf30c8a6a0b - languageName: node - linkType: hard - -"@webassemblyjs/wasm-edit@npm:1.11.1": - version: 1.11.1 - resolution: "@webassemblyjs/wasm-edit@npm:1.11.1" - dependencies: - "@webassemblyjs/ast": 1.11.1 - "@webassemblyjs/helper-buffer": 1.11.1 - "@webassemblyjs/helper-wasm-bytecode": 1.11.1 - "@webassemblyjs/helper-wasm-section": 1.11.1 - "@webassemblyjs/wasm-gen": 1.11.1 - "@webassemblyjs/wasm-opt": 1.11.1 - "@webassemblyjs/wasm-parser": 1.11.1 - "@webassemblyjs/wast-printer": 1.11.1 - checksum: 6d7d9efaec1227e7ef7585a5d7ff0be5f329f7c1c6b6c0e906b18ed2e9a28792a5635e450aca2d136770d0207225f204eff70a4b8fd879d3ac79e1dcc26dbeb9 - languageName: node - linkType: hard - -"@webassemblyjs/wasm-gen@npm:1.11.1": - version: 1.11.1 - resolution: "@webassemblyjs/wasm-gen@npm:1.11.1" - dependencies: - "@webassemblyjs/ast": 1.11.1 - "@webassemblyjs/helper-wasm-bytecode": 1.11.1 - "@webassemblyjs/ieee754": 1.11.1 - "@webassemblyjs/leb128": 1.11.1 - "@webassemblyjs/utf8": 1.11.1 - checksum: 1f6921e640293bf99fb16b21e09acb59b340a79f986c8f979853a0ae9f0b58557534b81e02ea2b4ef11e929d946708533fd0693c7f3712924128fdafd6465f5b - languageName: node - linkType: hard - -"@webassemblyjs/wasm-opt@npm:1.11.1": - version: 1.11.1 - resolution: "@webassemblyjs/wasm-opt@npm:1.11.1" - dependencies: - "@webassemblyjs/ast": 1.11.1 - "@webassemblyjs/helper-buffer": 1.11.1 - "@webassemblyjs/wasm-gen": 1.11.1 - "@webassemblyjs/wasm-parser": 1.11.1 - checksum: 21586883a20009e2b20feb67bdc451bbc6942252e038aae4c3a08e6f67b6bae0f5f88f20bfc7bd0452db5000bacaf5ab42b98cf9aa034a6c70e9fc616142e1db - languageName: node - linkType: hard - -"@webassemblyjs/wasm-parser@npm:1.11.1": - version: 1.11.1 - resolution: "@webassemblyjs/wasm-parser@npm:1.11.1" - dependencies: - "@webassemblyjs/ast": 1.11.1 - "@webassemblyjs/helper-api-error": 1.11.1 - "@webassemblyjs/helper-wasm-bytecode": 1.11.1 - "@webassemblyjs/ieee754": 1.11.1 - "@webassemblyjs/leb128": 1.11.1 - "@webassemblyjs/utf8": 1.11.1 - checksum: 1521644065c360e7b27fad9f4bb2df1802d134dd62937fa1f601a1975cde56bc31a57b6e26408b9ee0228626ff3ba1131ae6f74ffb7d718415b6528c5a6dbfc2 - languageName: node - linkType: hard - -"@webassemblyjs/wast-printer@npm:1.11.1": - version: 1.11.1 - resolution: "@webassemblyjs/wast-printer@npm:1.11.1" - dependencies: - "@webassemblyjs/ast": 1.11.1 - "@xtuc/long": 4.2.2 - checksum: f15ae4c2441b979a3b4fce78f3d83472fb22350c6dc3fd34bfe7c3da108e0b2360718734d961bba20e7716cb8578e964b870da55b035e209e50ec9db0378a3f7 - languageName: node - linkType: hard - -"@webpack-cli/configtest@npm:^1.2.0": - version: 1.2.0 - resolution: "@webpack-cli/configtest@npm:1.2.0" - peerDependencies: - webpack: 4.x.x || 5.x.x - webpack-cli: 4.x.x - checksum: a2726cd9ec601d2b57e5fc15e0ebf5200a8892065e735911269ac2038e62be4bfc176ea1f88c2c46ff09b4d05d4c10ae045e87b3679372483d47da625a327e28 - languageName: node - linkType: hard - -"@webpack-cli/info@npm:^1.5.0": - version: 1.5.0 - resolution: "@webpack-cli/info@npm:1.5.0" - dependencies: - envinfo: ^7.7.3 - peerDependencies: - webpack-cli: 4.x.x - checksum: 7f56fe037cd7d1fd5c7428588519fbf04a0cad33925ee4202ffbafd00f8ec1f2f67d991245e687d50e0f3e23f7b7814273d56cb9f7da4b05eed47c8d815c6296 - languageName: node - linkType: hard - -"@webpack-cli/serve@npm:^1.7.0": - version: 1.7.0 - resolution: "@webpack-cli/serve@npm:1.7.0" - peerDependencies: - webpack-cli: 4.x.x - peerDependenciesMeta: - webpack-dev-server: - optional: true - checksum: d475e8effa23eb7ff9a48b14d4de425989fd82f906ce71c210921cc3852327c22873be00c35e181a25a6bd03d424ae2b83e7f3b3f410ac7ee31b128ab4ac7713 - languageName: node - linkType: hard - -"@xtuc/ieee754@npm:^1.2.0": - version: 1.2.0 - resolution: "@xtuc/ieee754@npm:1.2.0" - checksum: ac56d4ca6e17790f1b1677f978c0c6808b1900a5b138885d3da21732f62e30e8f0d9120fcf8f6edfff5100ca902b46f8dd7c1e3f903728634523981e80e2885a - languageName: node - linkType: hard - -"@xtuc/long@npm:4.2.2": - version: 4.2.2 - resolution: "@xtuc/long@npm:4.2.2" - checksum: 8ed0d477ce3bc9c6fe2bf6a6a2cc316bb9c4127c5a7827bae947fa8ec34c7092395c5a283cc300c05b5fa01cbbfa1f938f410a7bf75db7c7846fea41949989ec - languageName: node - linkType: hard - -"acorn-import-assertions@npm:^1.7.6": - version: 1.8.0 - resolution: "acorn-import-assertions@npm:1.8.0" - peerDependencies: - acorn: ^8 - checksum: 5c4cf7c850102ba7ae0eeae0deb40fb3158c8ca5ff15c0bca43b5c47e307a1de3d8ef761788f881343680ea374631ae9e9615ba8876fee5268dbe068c98bcba6 - languageName: node - linkType: hard - -"acorn-walk@npm:^8.1.1": - version: 8.2.0 - resolution: "acorn-walk@npm:8.2.0" - checksum: 1715e76c01dd7b2d4ca472f9c58968516a4899378a63ad5b6c2d668bba8da21a71976c14ec5f5b75f887b6317c4ae0b897ab141c831d741dc76024d8745f1ad1 - languageName: node - linkType: hard - -"acorn@npm:^8.4.1, acorn@npm:^8.5.0, acorn@npm:^8.7.1": - version: 8.8.1 - resolution: "acorn@npm:8.8.1" - bin: - acorn: bin/acorn - checksum: 4079b67283b94935157698831967642f24a075c52ce3feaaaafe095776dfbe15d86a1b33b1e53860fc0d062ed6c83f4284a5c87c85b9ad51853a01173da6097f - languageName: node - linkType: hard - -"ajv-formats@npm:^2.1.1": - version: 2.1.1 - resolution: "ajv-formats@npm:2.1.1" - dependencies: - ajv: ^8.0.0 - peerDependencies: - ajv: ^8.0.0 - peerDependenciesMeta: - ajv: - optional: true - checksum: 4a287d937f1ebaad4683249a4c40c0fa3beed30d9ddc0adba04859026a622da0d317851316ea64b3680dc60f5c3c708105ddd5d5db8fe595d9d0207fd19f90b7 - languageName: node - linkType: hard - -"ajv-keywords@npm:^3.5.2": - version: 3.5.2 - resolution: "ajv-keywords@npm:3.5.2" - peerDependencies: - ajv: ^6.9.1 - checksum: 7dc5e5931677a680589050f79dcbe1fefbb8fea38a955af03724229139175b433c63c68f7ae5f86cf8f65d55eb7c25f75a046723e2e58296707617ca690feae9 - languageName: node - linkType: hard - -"ajv-keywords@npm:^5.0.0": - version: 5.1.0 - resolution: "ajv-keywords@npm:5.1.0" - dependencies: - fast-deep-equal: ^3.1.3 - peerDependencies: - ajv: ^8.8.2 - checksum: c35193940b853119242c6757787f09ecf89a2c19bcd36d03ed1a615e710d19d450cb448bfda407b939aba54b002368c8bff30529cc50a0536a8e10bcce300421 - languageName: node - linkType: hard - -"ajv@npm:^6.12.5": - version: 6.12.6 - resolution: "ajv@npm:6.12.6" - dependencies: - fast-deep-equal: ^3.1.1 - fast-json-stable-stringify: ^2.0.0 - json-schema-traverse: ^0.4.1 - uri-js: ^4.2.2 - checksum: 874972efe5c4202ab0a68379481fbd3d1b5d0a7bd6d3cc21d40d3536ebff3352a2a1fabb632d4fd2cc7fe4cbdcd5ed6782084c9bbf7f32a1536d18f9da5007d4 - languageName: node - linkType: hard - -"ajv@npm:^8.0.0, ajv@npm:^8.8.0": - version: 8.11.0 - resolution: "ajv@npm:8.11.0" - dependencies: - fast-deep-equal: ^3.1.1 - json-schema-traverse: ^1.0.0 - require-from-string: ^2.0.2 - uri-js: ^4.2.2 - checksum: 5e0ff226806763be73e93dd7805b634f6f5921e3e90ca04acdf8db81eed9d8d3f0d4c5f1213047f45ebbf8047ffe0c840fa1ef2ec42c3a644899f69aa72b5bef - languageName: node - linkType: hard - -"ansi-styles@npm:^3.2.1": - version: 3.2.1 - resolution: "ansi-styles@npm:3.2.1" - dependencies: - color-convert: ^1.9.0 - checksum: d85ade01c10e5dd77b6c89f34ed7531da5830d2cb5882c645f330079975b716438cd7ebb81d0d6e6b4f9c577f19ae41ab55f07f19786b02f9dfd9e0377395665 - languageName: node - linkType: hard - -"arg@npm:^4.1.0": - version: 4.1.3 - resolution: "arg@npm:4.1.3" - checksum: 544af8dd3f60546d3e4aff084d451b96961d2267d668670199692f8d054f0415d86fc5497d0e641e91546f0aa920e7c29e5250e99fc89f5552a34b5d93b77f43 - languageName: node - linkType: hard - -"babel-loader@npm:9.1.0": - version: 9.1.0 - resolution: "babel-loader@npm:9.1.0" - dependencies: - find-cache-dir: ^3.3.2 - schema-utils: ^4.0.0 - peerDependencies: - "@babel/core": ^7.12.0 - webpack: ">=5" - checksum: 774758febd1e8ca804abcae3b8f65634330dc688837424d0946f06d1386914de43435cce691710fa144eccdf1292cf883439ac3598ce7320916acfaaa2372641 - languageName: node - linkType: hard - -"babel-plugin-polyfill-corejs2@npm:^0.3.3": - version: 0.3.3 - resolution: "babel-plugin-polyfill-corejs2@npm:0.3.3" - dependencies: - "@babel/compat-data": ^7.17.7 - "@babel/helper-define-polyfill-provider": ^0.3.3 - semver: ^6.1.1 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 7db3044993f3dddb3cc3d407bc82e640964a3bfe22de05d90e1f8f7a5cb71460011ab136d3c03c6c1ba428359ebf635688cd6205e28d0469bba221985f5c6179 - languageName: node - linkType: hard - -"babel-plugin-polyfill-corejs3@npm:^0.6.0": - version: 0.6.0 - resolution: "babel-plugin-polyfill-corejs3@npm:0.6.0" - dependencies: - "@babel/helper-define-polyfill-provider": ^0.3.3 - core-js-compat: ^3.25.1 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 470bb8c59f7c0912bd77fe1b5a2e72f349b3f65bbdee1d60d6eb7e1f4a085c6f24b2dd5ab4ac6c2df6444a96b070ef6790eccc9edb6a2668c60d33133bfb62c6 - languageName: node - linkType: hard - -"babel-plugin-polyfill-regenerator@npm:^0.4.1": - version: 0.4.1 - resolution: "babel-plugin-polyfill-regenerator@npm:0.4.1" - dependencies: - "@babel/helper-define-polyfill-provider": ^0.3.3 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: ab0355efbad17d29492503230387679dfb780b63b25408990d2e4cf421012dae61d6199ddc309f4d2409ce4e9d3002d187702700dd8f4f8770ebbba651ed066c - languageName: node - linkType: hard - -"balanced-match@npm:^1.0.0": - version: 1.0.2 - resolution: "balanced-match@npm:1.0.2" - checksum: 9706c088a283058a8a99e0bf91b0a2f75497f185980d9ffa8b304de1d9e58ebda7c72c07ebf01dadedaac5b2907b2c6f566f660d62bd336c3468e960403b9d65 - languageName: node - linkType: hard - -"brace-expansion@npm:^1.1.7": - version: 1.1.11 - resolution: "brace-expansion@npm:1.1.11" - dependencies: - balanced-match: ^1.0.0 - concat-map: 0.0.1 - checksum: faf34a7bb0c3fcf4b59c7808bc5d2a96a40988addf2e7e09dfbb67a2251800e0d14cd2bfc1aa79174f2f5095c54ff27f46fb1289fe2d77dac755b5eb3434cc07 - languageName: node - linkType: hard - -"browserslist@npm:^4.14.5, browserslist@npm:^4.21.3, browserslist@npm:^4.21.4": - version: 4.21.4 - resolution: "browserslist@npm:4.21.4" - dependencies: - caniuse-lite: ^1.0.30001400 - electron-to-chromium: ^1.4.251 - node-releases: ^2.0.6 - update-browserslist-db: ^1.0.9 - bin: - browserslist: cli.js - checksum: 4af3793704dbb4615bcd29059ab472344dc7961c8680aa6c4bb84f05340e14038d06a5aead58724eae69455b8fade8b8c69f1638016e87e5578969d74c078b79 - languageName: node - linkType: hard - -"buffer-from@npm:^1.0.0": - version: 1.1.2 - resolution: "buffer-from@npm:1.1.2" - checksum: 0448524a562b37d4d7ed9efd91685a5b77a50672c556ea254ac9a6d30e3403a517d8981f10e565db24e8339413b43c97ca2951f10e399c6125a0d8911f5679bb - languageName: node - linkType: hard - -"caniuse-lite@npm:^1.0.30001400": - version: 1.0.30001425 - resolution: "caniuse-lite@npm:1.0.30001425" - checksum: 4fbf9f5b125b15a3eeaf7b75ca611f417ab9ce1a9fc07ee1023b2a7c0cc9844ad61ff089e814e4af6f747b9b532b6b50e7cb7844e6c29900f68ac9d171193ece - languageName: node - linkType: hard - -"chalk@npm:^2.0.0": - version: 2.4.2 - resolution: "chalk@npm:2.4.2" - dependencies: - ansi-styles: ^3.2.1 - escape-string-regexp: ^1.0.5 - supports-color: ^5.3.0 - checksum: ec3661d38fe77f681200f878edbd9448821924e0f93a9cefc0e26a33b145f1027a2084bf19967160d11e1f03bfe4eaffcabf5493b89098b2782c3fe0b03d80c2 - languageName: node - linkType: hard - -"chrome-trace-event@npm:^1.0.2": - version: 1.0.3 - resolution: "chrome-trace-event@npm:1.0.3" - checksum: cb8b1fc7e881aaef973bd0c4a43cd353c2ad8323fb471a041e64f7c2dd849cde4aad15f8b753331a32dda45c973f032c8a03b8177fc85d60eaa75e91e08bfb97 - languageName: node - linkType: hard - -"clone-deep@npm:^4.0.1": - version: 4.0.1 - resolution: "clone-deep@npm:4.0.1" - dependencies: - is-plain-object: ^2.0.4 - kind-of: ^6.0.2 - shallow-clone: ^3.0.0 - checksum: 770f912fe4e6f21873c8e8fbb1e99134db3b93da32df271d00589ea4a29dbe83a9808a322c93f3bcaf8584b8b4fa6fc269fc8032efbaa6728e0c9886c74467d2 - languageName: node - linkType: hard - -"color-convert@npm:^1.9.0": - version: 1.9.3 - resolution: "color-convert@npm:1.9.3" - dependencies: - color-name: 1.1.3 - checksum: fd7a64a17cde98fb923b1dd05c5f2e6f7aefda1b60d67e8d449f9328b4e53b228a428fd38bfeaeb2db2ff6b6503a776a996150b80cdf224062af08a5c8a3a203 - languageName: node - linkType: hard - -"color-name@npm:1.1.3": - version: 1.1.3 - resolution: "color-name@npm:1.1.3" - checksum: 09c5d3e33d2105850153b14466501f2bfb30324a2f76568a408763a3b7433b0e50e5b4ab1947868e65cb101bb7cb75029553f2c333b6d4b8138a73fcc133d69d - languageName: node - linkType: hard - -"colorette@npm:^2.0.14": - version: 2.0.19 - resolution: "colorette@npm:2.0.19" - checksum: 888cf5493f781e5fcf54ce4d49e9d7d698f96ea2b2ef67906834bb319a392c667f9ec69f4a10e268d2946d13a9503d2d19b3abaaaf174e3451bfe91fb9d82427 - languageName: node - linkType: hard - -"commander@npm:^2.20.0": - version: 2.20.3 - resolution: "commander@npm:2.20.3" - checksum: ab8c07884e42c3a8dbc5dd9592c606176c7eb5c1ca5ff274bcf907039b2c41de3626f684ea75ccf4d361ba004bbaff1f577d5384c155f3871e456bdf27becf9e - languageName: node - linkType: hard - -"commander@npm:^7.0.0": - version: 7.2.0 - resolution: "commander@npm:7.2.0" - checksum: 53501cbeee61d5157546c0bef0fedb6cdfc763a882136284bed9a07225f09a14b82d2a84e7637edfd1a679fb35ed9502fd58ef1d091e6287f60d790147f68ddc - languageName: node - linkType: hard - -"commondir@npm:^1.0.1": - version: 1.0.1 - resolution: "commondir@npm:1.0.1" - checksum: 59715f2fc456a73f68826285718503340b9f0dd89bfffc42749906c5cf3d4277ef11ef1cca0350d0e79204f00f1f6d83851ececc9095dc88512a697ac0b9bdcb - languageName: node - linkType: hard - -"concat-map@npm:0.0.1": - version: 0.0.1 - resolution: "concat-map@npm:0.0.1" - checksum: 902a9f5d8967a3e2faf138d5cb784b9979bad2e6db5357c5b21c568df4ebe62bcb15108af1b2253744844eb964fc023fbd9afbbbb6ddd0bcc204c6fb5b7bf3af - languageName: node - linkType: hard - -"convert-source-map@npm:^1.7.0": - version: 1.9.0 - resolution: "convert-source-map@npm:1.9.0" - checksum: dc55a1f28ddd0e9485ef13565f8f756b342f9a46c4ae18b843fe3c30c675d058d6a4823eff86d472f187b176f0adf51ea7b69ea38be34be4a63cbbf91b0593c8 - languageName: node - linkType: hard - -"core-js-compat@npm:^3.25.1": - version: 3.26.0 - resolution: "core-js-compat@npm:3.26.0" - dependencies: - browserslist: ^4.21.4 - checksum: 120780ec33d441e476810abac9bf57199c2083006b179dc23d0ab0cfea096eff2a2fc3e9cb315d245735df661cfa4b76a8b8c37f5056fd02428a3cd2ea1d9f36 - languageName: node - linkType: hard - -"create-require@npm:^1.1.0": - version: 1.1.1 - resolution: "create-require@npm:1.1.1" - checksum: a9a1503d4390d8b59ad86f4607de7870b39cad43d929813599a23714831e81c520bddf61bcdd1f8e30f05fd3a2b71ae8538e946eb2786dc65c2bbc520f692eff - languageName: node - linkType: hard - -"cross-spawn@npm:^7.0.3": - version: 7.0.3 - resolution: "cross-spawn@npm:7.0.3" - dependencies: - path-key: ^3.1.0 - shebang-command: ^2.0.0 - which: ^2.0.1 - checksum: 671cc7c7288c3a8406f3c69a3ae2fc85555c04169e9d611def9a675635472614f1c0ed0ef80955d5b6d4e724f6ced67f0ad1bb006c2ea643488fcfef994d7f52 - languageName: node - linkType: hard - -"debug@npm:^4.1.0, debug@npm:^4.1.1": - version: 4.3.4 - resolution: "debug@npm:4.3.4" - dependencies: - ms: 2.1.2 - peerDependenciesMeta: - supports-color: - optional: true - checksum: 3dbad3f94ea64f34431a9cbf0bafb61853eda57bff2880036153438f50fb5a84f27683ba0d8e5426bf41a8c6ff03879488120cf5b3a761e77953169c0600a708 - languageName: node - linkType: hard - -"diff@npm:^4.0.1": - version: 4.0.2 - resolution: "diff@npm:4.0.2" - checksum: f2c09b0ce4e6b301c221addd83bf3f454c0bc00caa3dd837cf6c127d6edf7223aa2bbe3b688feea110b7f262adbfc845b757c44c8a9f8c0c5b15d8fa9ce9d20d - languageName: node - linkType: hard - -"electron-to-chromium@npm:^1.4.251": - version: 1.4.284 - resolution: "electron-to-chromium@npm:1.4.284" - checksum: be496e9dca6509dbdbb54dc32146fc99f8eb716d28a7ee8ccd3eba0066561df36fc51418d8bd7cf5a5891810bf56c0def3418e74248f51ea4a843d423603d10a - languageName: node - linkType: hard - -"enhanced-resolve@npm:^5.10.0": - version: 5.10.0 - resolution: "enhanced-resolve@npm:5.10.0" - dependencies: - graceful-fs: ^4.2.4 - tapable: ^2.2.0 - checksum: 0bb9830704db271610f900e8d79d70a740ea16f251263362b0c91af545576d09fe50103496606c1300a05e588372d6f9780a9bc2e30ce8ef9b827ec8f44687ff - languageName: node - linkType: hard - -"envinfo@npm:^7.7.3": - version: 7.8.1 - resolution: "envinfo@npm:7.8.1" - bin: - envinfo: dist/cli.js - checksum: de736c98d6311c78523628ff127af138451b162e57af5293c1b984ca821d0aeb9c849537d2fde0434011bed33f6bca5310ca2aab8a51a3f28fc719e89045d648 - languageName: node - linkType: hard - -"es-module-lexer@npm:^0.9.0": - version: 0.9.3 - resolution: "es-module-lexer@npm:0.9.3" - checksum: 84bbab23c396281db2c906c766af58b1ae2a1a2599844a504df10b9e8dc77ec800b3211fdaa133ff700f5703d791198807bba25d9667392d27a5e9feda344da8 - languageName: node - linkType: hard - -"escalade@npm:^3.1.1": - version: 3.1.1 - resolution: "escalade@npm:3.1.1" - checksum: a3e2a99f07acb74b3ad4989c48ca0c3140f69f923e56d0cba0526240ee470b91010f9d39001f2a4a313841d237ede70a729e92125191ba5d21e74b106800b133 - languageName: node - linkType: hard - -"escape-string-regexp@npm:^1.0.5": - version: 1.0.5 - resolution: "escape-string-regexp@npm:1.0.5" - checksum: 6092fda75c63b110c706b6a9bfde8a612ad595b628f0bd2147eea1d3406723020810e591effc7db1da91d80a71a737a313567c5abb3813e8d9c71f4aa595b410 - languageName: node - linkType: hard - -"eslint-scope@npm:5.1.1": - version: 5.1.1 - resolution: "eslint-scope@npm:5.1.1" - dependencies: - esrecurse: ^4.3.0 - estraverse: ^4.1.1 - checksum: 47e4b6a3f0cc29c7feedee6c67b225a2da7e155802c6ea13bbef4ac6b9e10c66cd2dcb987867ef176292bf4e64eccc680a49e35e9e9c669f4a02bac17e86abdb - languageName: node - linkType: hard - -"esrecurse@npm:^4.3.0": - version: 4.3.0 - resolution: "esrecurse@npm:4.3.0" - dependencies: - estraverse: ^5.2.0 - checksum: ebc17b1a33c51cef46fdc28b958994b1dc43cd2e86237515cbc3b4e5d2be6a811b2315d0a1a4d9d340b6d2308b15322f5c8291059521cc5f4802f65e7ec32837 - languageName: node - linkType: hard - -"estraverse@npm:^4.1.1": - version: 4.3.0 - resolution: "estraverse@npm:4.3.0" - checksum: a6299491f9940bb246124a8d44b7b7a413a8336f5436f9837aaa9330209bd9ee8af7e91a654a3545aee9c54b3308e78ee360cef1d777d37cfef77d2fa33b5827 - languageName: node - linkType: hard - -"estraverse@npm:^5.2.0": - version: 5.3.0 - resolution: "estraverse@npm:5.3.0" - checksum: 072780882dc8416ad144f8fe199628d2b3e7bbc9989d9ed43795d2c90309a2047e6bc5979d7e2322a341163d22cfad9e21f4110597fe487519697389497e4e2b - languageName: node - linkType: hard - -"esutils@npm:^2.0.2": - version: 2.0.3 - resolution: "esutils@npm:2.0.3" - checksum: 22b5b08f74737379a840b8ed2036a5fb35826c709ab000683b092d9054e5c2a82c27818f12604bfc2a9a76b90b6834ef081edbc1c7ae30d1627012e067c6ec87 - languageName: node - linkType: hard - -"events@npm:^3.2.0": - version: 3.3.0 - resolution: "events@npm:3.3.0" - checksum: f6f487ad2198aa41d878fa31452f1a3c00958f46e9019286ff4787c84aac329332ab45c9cdc8c445928fc6d7ded294b9e005a7fce9426488518017831b272780 - languageName: node - linkType: hard - -"fast-deep-equal@npm:^3.1.1, fast-deep-equal@npm:^3.1.3": - version: 3.1.3 - resolution: "fast-deep-equal@npm:3.1.3" - checksum: e21a9d8d84f53493b6aa15efc9cfd53dd5b714a1f23f67fb5dc8f574af80df889b3bce25dc081887c6d25457cce704e636395333abad896ccdec03abaf1f3f9d - languageName: node - linkType: hard - -"fast-json-stable-stringify@npm:^2.0.0": - version: 2.1.0 - resolution: "fast-json-stable-stringify@npm:2.1.0" - checksum: b191531e36c607977e5b1c47811158733c34ccb3bfde92c44798929e9b4154884378536d26ad90dfecd32e1ffc09c545d23535ad91b3161a27ddbb8ebe0cbecb - languageName: node - linkType: hard - -"fastest-levenshtein@npm:^1.0.12": - version: 1.0.16 - resolution: "fastest-levenshtein@npm:1.0.16" - checksum: a78d44285c9e2ae2c25f3ef0f8a73f332c1247b7ea7fb4a191e6bb51aa6ee1ef0dfb3ed113616dcdc7023e18e35a8db41f61c8d88988e877cf510df8edafbc71 - languageName: node - linkType: hard - -"find-cache-dir@npm:^3.3.2": - version: 3.3.2 - resolution: "find-cache-dir@npm:3.3.2" - dependencies: - commondir: ^1.0.1 - make-dir: ^3.0.2 - pkg-dir: ^4.1.0 - checksum: 1e61c2e64f5c0b1c535bd85939ae73b0e5773142713273818cc0b393ee3555fb0fd44e1a5b161b8b6c3e03e98c2fcc9c227d784850a13a90a8ab576869576817 - languageName: node - linkType: hard - -"find-up@npm:^4.0.0": - version: 4.1.0 - resolution: "find-up@npm:4.1.0" - dependencies: - locate-path: ^5.0.0 - path-exists: ^4.0.0 - checksum: 4c172680e8f8c1f78839486e14a43ef82e9decd0e74145f40707cc42e7420506d5ec92d9a11c22bd2c48fb0c384ea05dd30e10dd152fefeec6f2f75282a8b844 - languageName: node - linkType: hard - -"function-bind@npm:^1.1.1": - version: 1.1.1 - resolution: "function-bind@npm:1.1.1" - checksum: b32fbaebb3f8ec4969f033073b43f5c8befbb58f1a79e12f1d7490358150359ebd92f49e72ff0144f65f2c48ea2a605bff2d07965f548f6474fd8efd95bf361a - languageName: node - linkType: hard - -"gensync@npm:^1.0.0-beta.2": - version: 1.0.0-beta.2 - resolution: "gensync@npm:1.0.0-beta.2" - checksum: a7437e58c6be12aa6c90f7730eac7fa9833dc78872b4ad2963d2031b00a3367a93f98aec75f9aaac7220848e4026d67a8655e870b24f20a543d103c0d65952ec - languageName: node - linkType: hard - -"glob-to-regexp@npm:^0.4.1": - version: 0.4.1 - resolution: "glob-to-regexp@npm:0.4.1" - checksum: e795f4e8f06d2a15e86f76e4d92751cf8bbfcf0157cea5c2f0f35678a8195a750b34096b1256e436f0cebc1883b5ff0888c47348443e69546a5a87f9e1eb1167 - languageName: node - linkType: hard - -"glob@npm:^5.0.15": - version: 5.0.15 - resolution: "glob@npm:5.0.15" - dependencies: - inflight: ^1.0.4 - inherits: 2 - minimatch: 2 || 3 - once: ^1.3.0 - path-is-absolute: ^1.0.0 - checksum: f9742448303460672607e569457f1b57e486a79a985e269b69465834d2075b243378225f65dc54c09fcd4b75e4fb34442aec88f33f8c65fa4abccc8ee2dc2f5d - languageName: node - linkType: hard - -"globals@npm:^11.1.0": - version: 11.12.0 - resolution: "globals@npm:11.12.0" - checksum: 67051a45eca3db904aee189dfc7cd53c20c7d881679c93f6146ddd4c9f4ab2268e68a919df740d39c71f4445d2b38ee360fc234428baea1dbdfe68bbcb46979e - languageName: node - linkType: hard - -"graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.9": - version: 4.2.10 - resolution: "graceful-fs@npm:4.2.10" - checksum: 3f109d70ae123951905d85032ebeae3c2a5a7a997430df00ea30df0e3a6c60cf6689b109654d6fdacd28810a053348c4d14642da1d075049e6be1ba5216218da - languageName: node - linkType: hard - -"has-flag@npm:^3.0.0": - version: 3.0.0 - resolution: "has-flag@npm:3.0.0" - checksum: 4a15638b454bf086c8148979aae044dd6e39d63904cd452d970374fa6a87623423da485dfb814e7be882e05c096a7ccf1ebd48e7e7501d0208d8384ff4dea73b - languageName: node - linkType: hard - -"has-flag@npm:^4.0.0": - version: 4.0.0 - resolution: "has-flag@npm:4.0.0" - checksum: 261a1357037ead75e338156b1f9452c016a37dcd3283a972a30d9e4a87441ba372c8b81f818cd0fbcd9c0354b4ae7e18b9e1afa1971164aef6d18c2b6095a8ad - languageName: node - linkType: hard - -"has@npm:^1.0.3": - version: 1.0.3 - resolution: "has@npm:1.0.3" - dependencies: - function-bind: ^1.1.1 - checksum: b9ad53d53be4af90ce5d1c38331e712522417d017d5ef1ebd0507e07c2fbad8686fffb8e12ddecd4c39ca9b9b47431afbb975b8abf7f3c3b82c98e9aad052792 - languageName: node - linkType: hard - -"import-local@npm:^3.0.2": - version: 3.1.0 - resolution: "import-local@npm:3.1.0" - dependencies: - pkg-dir: ^4.2.0 - resolve-cwd: ^3.0.0 - bin: - import-local-fixture: fixtures/cli.js - checksum: bfcdb63b5e3c0e245e347f3107564035b128a414c4da1172a20dc67db2504e05ede4ac2eee1252359f78b0bfd7b19ef180aec427c2fce6493ae782d73a04cddd - languageName: node - linkType: hard - -"inflight@npm:^1.0.4": - version: 1.0.6 - resolution: "inflight@npm:1.0.6" - dependencies: - once: ^1.3.0 - wrappy: 1 - checksum: f4f76aa072ce19fae87ce1ef7d221e709afb59d445e05d47fba710e85470923a75de35bfae47da6de1b18afc3ce83d70facf44cfb0aff89f0a3f45c0a0244dfd - languageName: node - linkType: hard - -"inherits@npm:2": - version: 2.0.4 - resolution: "inherits@npm:2.0.4" - checksum: 4a48a733847879d6cf6691860a6b1e3f0f4754176e4d71494c41f3475553768b10f84b5ce1d40fbd0e34e6bfbb864ee35858ad4dd2cf31e02fc4a154b724d7f1 - languageName: node - linkType: hard - -"interpret@npm:^2.2.0": - version: 2.2.0 - resolution: "interpret@npm:2.2.0" - checksum: f51efef7cb8d02da16408ffa3504cd6053014c5aeb7bb8c223727e053e4235bf565e45d67028b0c8740d917c603807aa3c27d7bd2f21bf20b6417e2bb3e5fd6e - languageName: node - linkType: hard - -"is-core-module@npm:^2.9.0": - version: 2.11.0 - resolution: "is-core-module@npm:2.11.0" - dependencies: - has: ^1.0.3 - checksum: f96fd490c6b48eb4f6d10ba815c6ef13f410b0ba6f7eb8577af51697de523e5f2cd9de1c441b51d27251bf0e4aebc936545e33a5d26d5d51f28d25698d4a8bab - languageName: node - linkType: hard - -"is-plain-object@npm:^2.0.4": - version: 2.0.4 - resolution: "is-plain-object@npm:2.0.4" - dependencies: - isobject: ^3.0.1 - checksum: 2a401140cfd86cabe25214956ae2cfee6fbd8186809555cd0e84574f88de7b17abacb2e477a6a658fa54c6083ecbda1e6ae404c7720244cd198903848fca70ca - languageName: node - linkType: hard - -"isexe@npm:^2.0.0": - version: 2.0.0 - resolution: "isexe@npm:2.0.0" - checksum: 26bf6c5480dda5161c820c5b5c751ae1e766c587b1f951ea3fcfc973bafb7831ae5b54a31a69bd670220e42e99ec154475025a468eae58ea262f813fdc8d1c62 - languageName: node - linkType: hard - -"isobject@npm:^3.0.1": - version: 3.0.1 - resolution: "isobject@npm:3.0.1" - checksum: db85c4c970ce30693676487cca0e61da2ca34e8d4967c2e1309143ff910c207133a969f9e4ddb2dc6aba670aabce4e0e307146c310350b298e74a31f7d464703 - languageName: node - linkType: hard - -"jest-worker@npm:^27.4.5": - version: 27.5.1 - resolution: "jest-worker@npm:27.5.1" - dependencies: - "@types/node": "*" - merge-stream: ^2.0.0 - supports-color: ^8.0.0 - checksum: 98cd68b696781caed61c983a3ee30bf880b5bd021c01d98f47b143d4362b85d0737f8523761e2713d45e18b4f9a2b98af1eaee77afade4111bb65c77d6f7c980 - languageName: node - linkType: hard - -"js-tokens@npm:^4.0.0": - version: 4.0.0 - resolution: "js-tokens@npm:4.0.0" - checksum: 8a95213a5a77deb6cbe94d86340e8d9ace2b93bc367790b260101d2f36a2eaf4e4e22d9fa9cf459b38af3a32fb4190e638024cf82ec95ef708680e405ea7cc78 - languageName: node - linkType: hard - -"jsesc@npm:^2.5.1": - version: 2.5.2 - resolution: "jsesc@npm:2.5.2" - bin: - jsesc: bin/jsesc - checksum: 4dc190771129e12023f729ce20e1e0bfceac84d73a85bc3119f7f938843fe25a4aeccb54b6494dce26fcf263d815f5f31acdefac7cc9329efb8422a4f4d9fa9d - languageName: node - linkType: hard - -"jsesc@npm:~0.5.0": - version: 0.5.0 - resolution: "jsesc@npm:0.5.0" - bin: - jsesc: bin/jsesc - checksum: b8b44cbfc92f198ad972fba706ee6a1dfa7485321ee8c0b25f5cedd538dcb20cde3197de16a7265430fce8277a12db066219369e3d51055038946039f6e20e17 - languageName: node - linkType: hard - -"json-parse-even-better-errors@npm:^2.3.1": - version: 2.3.1 - resolution: "json-parse-even-better-errors@npm:2.3.1" - checksum: 798ed4cf3354a2d9ccd78e86d2169515a0097a5c133337807cdf7f1fc32e1391d207ccfc276518cc1d7d8d4db93288b8a50ba4293d212ad1336e52a8ec0a941f - languageName: node - linkType: hard - -"json-schema-traverse@npm:^0.4.1": - version: 0.4.1 - resolution: "json-schema-traverse@npm:0.4.1" - checksum: 7486074d3ba247769fda17d5181b345c9fb7d12e0da98b22d1d71a5db9698d8b4bd900a3ec1a4ffdd60846fc2556274a5c894d0c48795f14cb03aeae7b55260b - languageName: node - linkType: hard - -"json-schema-traverse@npm:^1.0.0": - version: 1.0.0 - resolution: "json-schema-traverse@npm:1.0.0" - checksum: 02f2f466cdb0362558b2f1fd5e15cce82ef55d60cd7f8fa828cf35ba74330f8d767fcae5c5c2adb7851fa811766c694b9405810879bc4e1ddd78a7c0e03658ad - languageName: node - linkType: hard - -"json5@npm:^2.2.1": - version: 2.2.1 - resolution: "json5@npm:2.2.1" - bin: - json5: lib/cli.js - checksum: 74b8a23b102a6f2bf2d224797ae553a75488b5adbaee9c9b6e5ab8b510a2fc6e38f876d4c77dea672d4014a44b2399e15f2051ac2b37b87f74c0c7602003543b - languageName: node - linkType: hard - -"kind-of@npm:^6.0.2": - version: 6.0.3 - resolution: "kind-of@npm:6.0.3" - checksum: 3ab01e7b1d440b22fe4c31f23d8d38b4d9b91d9f291df683476576493d5dfd2e03848a8b05813dd0c3f0e835bc63f433007ddeceb71f05cb25c45ae1b19c6d3b - languageName: node - linkType: hard - -"loader-runner@npm:^4.2.0": - version: 4.3.0 - resolution: "loader-runner@npm:4.3.0" - checksum: a90e00dee9a16be118ea43fec3192d0b491fe03a32ed48a4132eb61d498f5536a03a1315531c19d284392a8726a4ecad71d82044c28d7f22ef62e029bf761569 - languageName: node - linkType: hard - -"locate-path@npm:^5.0.0": - version: 5.0.0 - resolution: "locate-path@npm:5.0.0" - dependencies: - p-locate: ^4.1.0 - checksum: 83e51725e67517287d73e1ded92b28602e3ae5580b301fe54bfb76c0c723e3f285b19252e375712316774cf52006cb236aed5704692c32db0d5d089b69696e30 - languageName: node - linkType: hard - -"lodash.debounce@npm:^4.0.8": - version: 4.0.8 - resolution: "lodash.debounce@npm:4.0.8" - checksum: a3f527d22c548f43ae31c861ada88b2637eb48ac6aa3eb56e82d44917971b8aa96fbb37aa60efea674dc4ee8c42074f90f7b1f772e9db375435f6c83a19b3bc6 - languageName: node - linkType: hard - -"make-dir@npm:^3.0.2": - version: 3.1.0 - resolution: "make-dir@npm:3.1.0" - dependencies: - semver: ^6.0.0 - checksum: 484200020ab5a1fdf12f393fe5f385fc8e4378824c940fba1729dcd198ae4ff24867bc7a5646331e50cead8abff5d9270c456314386e629acec6dff4b8016b78 - languageName: node - linkType: hard - -"make-error@npm:^1.1.1": - version: 1.3.6 - resolution: "make-error@npm:1.3.6" - checksum: b86e5e0e25f7f777b77fabd8e2cbf15737972869d852a22b7e73c17623928fccb826d8e46b9951501d3f20e51ad74ba8c59ed584f610526a48f8ccf88aaec402 - languageName: node - linkType: hard - -"merge-stream@npm:^2.0.0": - version: 2.0.0 - resolution: "merge-stream@npm:2.0.0" - checksum: 6fa4dcc8d86629705cea944a4b88ef4cb0e07656ebf223fa287443256414283dd25d91c1cd84c77987f2aec5927af1a9db6085757cb43d90eb170ebf4b47f4f4 - languageName: node - linkType: hard - -"mime-db@npm:1.52.0": - version: 1.52.0 - resolution: "mime-db@npm:1.52.0" - checksum: 0d99a03585f8b39d68182803b12ac601d9c01abfa28ec56204fa330bc9f3d1c5e14beb049bafadb3dbdf646dfb94b87e24d4ec7b31b7279ef906a8ea9b6a513f - languageName: node - linkType: hard - -"mime-types@npm:^2.1.27": - version: 2.1.35 - resolution: "mime-types@npm:2.1.35" - dependencies: - mime-db: 1.52.0 - checksum: 89a5b7f1def9f3af5dad6496c5ed50191ae4331cc5389d7c521c8ad28d5fdad2d06fd81baf38fed813dc4e46bb55c8145bb0ff406330818c9cf712fb2e9b3836 - languageName: node - linkType: hard - -"minimatch@npm:2 || 3": - version: 3.1.2 - resolution: "minimatch@npm:3.1.2" - dependencies: - brace-expansion: ^1.1.7 - checksum: c154e566406683e7bcb746e000b84d74465b3a832c45d59912b9b55cd50dee66e5c4b1e5566dba26154040e51672f9aa450a9aef0c97cfc7336b78b7afb9540a - languageName: node - linkType: hard - -"ms@npm:2.1.2": - version: 2.1.2 - resolution: "ms@npm:2.1.2" - checksum: 673cdb2c3133eb050c745908d8ce632ed2c02d85640e2edb3ace856a2266a813b30c613569bf3354fdf4ea7d1a1494add3bfa95e2713baa27d0c2c71fc44f58f - languageName: node - linkType: hard - -"nanoid@npm:^2.1.0": - version: 2.1.11 - resolution: "nanoid@npm:2.1.11" - checksum: 18cd14386816873849787eb4e65667021bfdeb019a8f14c74287c23594c67b7c0e8f42c7d69f6aedf05cd3d100f1ddc41184f9f9b6b17fbaea1c3ee3f0704eec - languageName: node - linkType: hard - -"neo-async@npm:^2.6.2": - version: 2.6.2 - resolution: "neo-async@npm:2.6.2" - checksum: deac9f8d00eda7b2e5cd1b2549e26e10a0faa70adaa6fdadca701cc55f49ee9018e427f424bac0c790b7c7e2d3068db97f3093f1093975f2acb8f8818b936ed9 - languageName: node - linkType: hard - -"node-releases@npm:^2.0.6": - version: 2.0.6 - resolution: "node-releases@npm:2.0.6" - checksum: e86a926dc9fbb3b41b4c4a89d998afdf140e20a4e8dbe6c0a807f7b2948b42ea97d7fd3ad4868041487b6e9ee98409829c6e4d84a734a4215dff060a7fbeb4bf - languageName: node - linkType: hard - -"once@npm:^1.3.0": - version: 1.4.0 - resolution: "once@npm:1.4.0" - dependencies: - wrappy: 1 - checksum: cd0a88501333edd640d95f0d2700fbde6bff20b3d4d9bdc521bdd31af0656b5706570d6c6afe532045a20bb8dc0849f8332d6f2a416e0ba6d3d3b98806c7db68 - languageName: node - linkType: hard - -"p-limit@npm:^2.2.0": - version: 2.3.0 - resolution: "p-limit@npm:2.3.0" - dependencies: - p-try: ^2.0.0 - checksum: 84ff17f1a38126c3314e91ecfe56aecbf36430940e2873dadaa773ffe072dc23b7af8e46d4b6485d302a11673fe94c6b67ca2cfbb60c989848b02100d0594ac1 - languageName: node - linkType: hard - -"p-locate@npm:^4.1.0": - version: 4.1.0 - resolution: "p-locate@npm:4.1.0" - dependencies: - p-limit: ^2.2.0 - checksum: 513bd14a455f5da4ebfcb819ef706c54adb09097703de6aeaa5d26fe5ea16df92b48d1ac45e01e3944ce1e6aa2a66f7f8894742b8c9d6e276e16cd2049a2b870 - languageName: node - linkType: hard - -"p-try@npm:^2.0.0": - version: 2.2.0 - resolution: "p-try@npm:2.2.0" - checksum: f8a8e9a7693659383f06aec604ad5ead237c7a261c18048a6e1b5b85a5f8a067e469aa24f5bc009b991ea3b058a87f5065ef4176793a200d4917349881216cae - languageName: node - linkType: hard - -"path-exists@npm:^4.0.0": - version: 4.0.0 - resolution: "path-exists@npm:4.0.0" - checksum: 505807199dfb7c50737b057dd8d351b82c033029ab94cb10a657609e00c1bc53b951cfdbccab8de04c5584d5eff31128ce6afd3db79281874a5ef2adbba55ed1 - languageName: node - linkType: hard - -"path-is-absolute@npm:^1.0.0": - version: 1.0.1 - resolution: "path-is-absolute@npm:1.0.1" - checksum: 060840f92cf8effa293bcc1bea81281bd7d363731d214cbe5c227df207c34cd727430f70c6037b5159c8a870b9157cba65e775446b0ab06fd5ecc7e54615a3b8 - languageName: node - linkType: hard - -"path-key@npm:^3.1.0": - version: 3.1.1 - resolution: "path-key@npm:3.1.1" - checksum: 55cd7a9dd4b343412a8386a743f9c746ef196e57c823d90ca3ab917f90ab9f13dd0ded27252ba49dbdfcab2b091d998bc446f6220cd3cea65db407502a740020 - languageName: node - linkType: hard - -"path-parse@npm:^1.0.7": - version: 1.0.7 - resolution: "path-parse@npm:1.0.7" - checksum: 49abf3d81115642938a8700ec580da6e830dde670be21893c62f4e10bd7dd4c3742ddc603fe24f898cba7eb0c6bc1777f8d9ac14185d34540c6d4d80cd9cae8a - languageName: node - linkType: hard - -"picocolors@npm:^1.0.0": - version: 1.0.0 - resolution: "picocolors@npm:1.0.0" - checksum: a2e8092dd86c8396bdba9f2b5481032848525b3dc295ce9b57896f931e63fc16f79805144321f72976383fc249584672a75cc18d6777c6b757603f372f745981 - languageName: node - linkType: hard - -"pkg-dir@npm:^4.1.0, pkg-dir@npm:^4.2.0": - version: 4.2.0 - resolution: "pkg-dir@npm:4.2.0" - dependencies: - find-up: ^4.0.0 - checksum: 9863e3f35132bf99ae1636d31ff1e1e3501251d480336edb1c211133c8d58906bed80f154a1d723652df1fda91e01c7442c2eeaf9dc83157c7ae89087e43c8d6 - languageName: node - linkType: hard - -"punycode@npm:^2.1.0": - version: 2.1.1 - resolution: "punycode@npm:2.1.1" - checksum: 823bf443c6dd14f669984dea25757b37993f67e8d94698996064035edd43bed8a5a17a9f12e439c2b35df1078c6bec05a6c86e336209eb1061e8025c481168e8 - languageName: node - linkType: hard - -"randombytes@npm:^2.1.0": - version: 2.1.0 - resolution: "randombytes@npm:2.1.0" - dependencies: - safe-buffer: ^5.1.0 - checksum: d779499376bd4cbb435ef3ab9a957006c8682f343f14089ed5f27764e4645114196e75b7f6abf1cbd84fd247c0cb0651698444df8c9bf30e62120fbbc52269d6 - languageName: node - linkType: hard - -"rechoir@npm:^0.7.0": - version: 0.7.1 - resolution: "rechoir@npm:0.7.1" - dependencies: - resolve: ^1.9.0 - checksum: 2a04aab4e28c05fcd6ee6768446bc8b859d8f108e71fc7f5bcbc5ef25e53330ce2c11d10f82a24591a2df4c49c4f61feabe1fd11f844c66feedd4cd7bb61146a - languageName: node - linkType: hard - -"regenerate-unicode-properties@npm:^10.1.0": - version: 10.1.0 - resolution: "regenerate-unicode-properties@npm:10.1.0" - dependencies: - regenerate: ^1.4.2 - checksum: b1a8929588433ab8b9dc1a34cf3665b3b472f79f2af6ceae00d905fc496b332b9af09c6718fb28c730918f19a00dc1d7310adbaa9b72a2ec7ad2f435da8ace17 - languageName: node - linkType: hard - -"regenerate@npm:^1.4.2": - version: 1.4.2 - resolution: "regenerate@npm:1.4.2" - checksum: 3317a09b2f802da8db09aa276e469b57a6c0dd818347e05b8862959c6193408242f150db5de83c12c3fa99091ad95fb42a6db2c3329bfaa12a0ea4cbbeb30cb0 - languageName: node - linkType: hard - -"regenerator-runtime@npm:^0.13.4": - version: 0.13.10 - resolution: "regenerator-runtime@npm:0.13.10" - checksum: 09893f5a9e82932642d9a999716b6c626dc53ef2a01307c952ebbf8e011802360163a37c304c18a6c358548be5a72b448e37209954a18696f21e438c81cbb4b9 - languageName: node - linkType: hard - -"regenerator-transform@npm:^0.15.0": - version: 0.15.0 - resolution: "regenerator-transform@npm:0.15.0" - dependencies: - "@babel/runtime": ^7.8.4 - checksum: 86e54849ab1167618d28bb56d214c52a983daf29b0d115c976d79840511420049b6b42c9ebdf187defa8e7129bdd74b6dd266420d0d3868c9fa7f793b5d15d49 - languageName: node - linkType: hard - -"regexpu-core@npm:^5.1.0": - version: 5.2.1 - resolution: "regexpu-core@npm:5.2.1" - dependencies: - regenerate: ^1.4.2 - regenerate-unicode-properties: ^10.1.0 - regjsgen: ^0.7.1 - regjsparser: ^0.9.1 - unicode-match-property-ecmascript: ^2.0.0 - unicode-match-property-value-ecmascript: ^2.0.0 - checksum: c1244db79f7a4597414cd7fdf5171fa73905f0cbc684385c78127fc6198f9cade8fe829a1c4036c8ec57ac75b1ffb8c196451abdd2e153f26a4d8043fa10bbb3 - languageName: node - linkType: hard - -"regjsgen@npm:^0.7.1": - version: 0.7.1 - resolution: "regjsgen@npm:0.7.1" - checksum: 7cac399921c58db8e16454869283ff66871531180218064fa938ac05c11c2976792a00706c3c78bbc625e1d793ca373065ea90564e06189a751a7b4ae33acadc - languageName: node - linkType: hard - -"regjsparser@npm:^0.9.1": - version: 0.9.1 - resolution: "regjsparser@npm:0.9.1" - dependencies: - jsesc: ~0.5.0 - bin: - regjsparser: bin/parser - checksum: 5e1b76afe8f1d03c3beaf9e0d935dd467589c3625f6d65fb8ffa14f224d783a0fed4bf49c2c1b8211043ef92b6117313419edf055a098ed8342e340586741afc - languageName: node - linkType: hard - -"require-from-string@npm:^2.0.2": - version: 2.0.2 - resolution: "require-from-string@npm:2.0.2" - checksum: a03ef6895445f33a4015300c426699bc66b2b044ba7b670aa238610381b56d3f07c686251740d575e22f4c87531ba662d06937508f0f3c0f1ddc04db3130560b - languageName: node - linkType: hard - -"resolve-cwd@npm:^3.0.0": - version: 3.0.0 - resolution: "resolve-cwd@npm:3.0.0" - dependencies: - resolve-from: ^5.0.0 - checksum: 546e0816012d65778e580ad62b29e975a642989108d9a3c5beabfb2304192fa3c9f9146fbdfe213563c6ff51975ae41bac1d3c6e047dd9572c94863a057b4d81 - languageName: node - linkType: hard - -"resolve-from@npm:^5.0.0": - version: 5.0.0 - resolution: "resolve-from@npm:5.0.0" - checksum: 4ceeb9113e1b1372d0cd969f3468fa042daa1dd9527b1b6bb88acb6ab55d8b9cd65dbf18819f9f9ddf0db804990901dcdaade80a215e7b2c23daae38e64f5bdf - languageName: node - linkType: hard - -"resolve@npm:^1.14.2, resolve@npm:^1.9.0": - version: 1.22.1 - resolution: "resolve@npm:1.22.1" - dependencies: - is-core-module: ^2.9.0 - path-parse: ^1.0.7 - supports-preserve-symlinks-flag: ^1.0.0 - bin: - resolve: bin/resolve - checksum: 07af5fc1e81aa1d866cbc9e9460fbb67318a10fa3c4deadc35c3ad8a898ee9a71a86a65e4755ac3195e0ea0cfbe201eb323ebe655ce90526fd61917313a34e4e - languageName: node - linkType: hard - -"resolve@patch:resolve@^1.14.2#~builtin, resolve@patch:resolve@^1.9.0#~builtin": - version: 1.22.1 - resolution: "resolve@patch:resolve@npm%3A1.22.1#~builtin::version=1.22.1&hash=07638b" - dependencies: - is-core-module: ^2.9.0 - path-parse: ^1.0.7 - supports-preserve-symlinks-flag: ^1.0.0 - bin: - resolve: bin/resolve - checksum: 5656f4d0bedcf8eb52685c1abdf8fbe73a1603bb1160a24d716e27a57f6cecbe2432ff9c89c2bd57542c3a7b9d14b1882b73bfe2e9d7849c9a4c0b8b39f02b8b - languageName: node - linkType: hard - -"safe-buffer@npm:^5.1.0": - version: 5.2.1 - resolution: "safe-buffer@npm:5.2.1" - checksum: b99c4b41fdd67a6aaf280fcd05e9ffb0813654894223afb78a31f14a19ad220bba8aba1cb14eddce1fcfb037155fe6de4e861784eb434f7d11ed58d1e70dd491 - languageName: node - linkType: hard - -"schema-utils@npm:^3.1.0, schema-utils@npm:^3.1.1": - version: 3.1.1 - resolution: "schema-utils@npm:3.1.1" - dependencies: - "@types/json-schema": ^7.0.8 - ajv: ^6.12.5 - ajv-keywords: ^3.5.2 - checksum: fb73f3d759d43ba033c877628fe9751620a26879f6301d3dbeeb48cf2a65baec5cdf99da65d1bf3b4ff5444b2e59cbe4f81c2456b5e0d2ba7d7fd4aed5da29ce - languageName: node - linkType: hard - -"schema-utils@npm:^4.0.0": - version: 4.0.0 - resolution: "schema-utils@npm:4.0.0" - dependencies: - "@types/json-schema": ^7.0.9 - ajv: ^8.8.0 - ajv-formats: ^2.1.1 - ajv-keywords: ^5.0.0 - checksum: c843e92fdd1a5c145dbb6ffdae33e501867f9703afac67bdf35a685e49f85b1dcc10ea250033175a64bd9d31f0555bc6785b8359da0c90bcea30cf6dfbb55a8f - languageName: node - linkType: hard - -"semver@npm:^6.0.0, semver@npm:^6.1.1, semver@npm:^6.1.2, semver@npm:^6.3.0": - version: 6.3.0 - resolution: "semver@npm:6.3.0" - bin: - semver: ./bin/semver.js - checksum: 1b26ecf6db9e8292dd90df4e781d91875c0dcc1b1909e70f5d12959a23c7eebb8f01ea581c00783bbee72ceeaad9505797c381756326073850dc36ed284b21b9 - languageName: node - linkType: hard - -"serialize-javascript@npm:^6.0.0": - version: 6.0.0 - resolution: "serialize-javascript@npm:6.0.0" - dependencies: - randombytes: ^2.1.0 - checksum: 56f90b562a1bdc92e55afb3e657c6397c01a902c588c0fe3d4c490efdcc97dcd2a3074ba12df9e94630f33a5ce5b76a74784a7041294628a6f4306e0ec84bf93 - languageName: node - linkType: hard - -"shallow-clone@npm:^3.0.0": - version: 3.0.1 - resolution: "shallow-clone@npm:3.0.1" - dependencies: - kind-of: ^6.0.2 - checksum: 39b3dd9630a774aba288a680e7d2901f5c0eae7b8387fc5c8ea559918b29b3da144b7bdb990d7ccd9e11be05508ac9e459ce51d01fd65e583282f6ffafcba2e7 - languageName: node - linkType: hard - -"shebang-command@npm:^2.0.0": - version: 2.0.0 - resolution: "shebang-command@npm:2.0.0" - dependencies: - shebang-regex: ^3.0.0 - checksum: 6b52fe87271c12968f6a054e60f6bde5f0f3d2db483a1e5c3e12d657c488a15474121a1d55cd958f6df026a54374ec38a4a963988c213b7570e1d51575cea7fa - languageName: node - linkType: hard - -"shebang-regex@npm:^3.0.0": - version: 3.0.0 - resolution: "shebang-regex@npm:3.0.0" - checksum: 1a2bcae50de99034fcd92ad4212d8e01eedf52c7ec7830eedcf886622804fe36884278f2be8be0ea5fde3fd1c23911643a4e0f726c8685b61871c8908af01222 - languageName: node - linkType: hard - -"shortid@npm:2.2.16": - version: 2.2.16 - resolution: "shortid@npm:2.2.16" - dependencies: - nanoid: ^2.1.0 - checksum: 0790ce22fe20aacc226915160da178b5a6af7814d1796404684f6699b60f77e291d39ad3b6b2b4c6efcf5553e1deeee7e29a48b8f46955de1425e67ab934e309 - languageName: node - linkType: hard - -"source-map-support@npm:~0.5.20": - version: 0.5.21 - resolution: "source-map-support@npm:0.5.21" - dependencies: - buffer-from: ^1.0.0 - source-map: ^0.6.0 - checksum: 43e98d700d79af1d36f859bdb7318e601dfc918c7ba2e98456118ebc4c4872b327773e5a1df09b0524e9e5063bb18f0934538eace60cca2710d1fa687645d137 - languageName: node - linkType: hard - -"source-map@npm:^0.6.0": - version: 0.6.1 - resolution: "source-map@npm:0.6.1" - checksum: 59ce8640cf3f3124f64ac289012c2b8bd377c238e316fb323ea22fbfe83da07d81e000071d7242cad7a23cd91c7de98e4df8830ec3f133cb6133a5f6e9f67bc2 - languageName: node - linkType: hard - -"supports-color@npm:^5.3.0": - version: 5.5.0 - resolution: "supports-color@npm:5.5.0" - dependencies: - has-flag: ^3.0.0 - checksum: 95f6f4ba5afdf92f495b5a912d4abee8dcba766ae719b975c56c084f5004845f6f5a5f7769f52d53f40e21952a6d87411bafe34af4a01e65f9926002e38e1dac - languageName: node - linkType: hard - -"supports-color@npm:^8.0.0": - version: 8.1.1 - resolution: "supports-color@npm:8.1.1" - dependencies: - has-flag: ^4.0.0 - checksum: c052193a7e43c6cdc741eb7f378df605636e01ad434badf7324f17fb60c69a880d8d8fcdcb562cf94c2350e57b937d7425ab5b8326c67c2adc48f7c87c1db406 - languageName: node - linkType: hard - -"supports-preserve-symlinks-flag@npm:^1.0.0": - version: 1.0.0 - resolution: "supports-preserve-symlinks-flag@npm:1.0.0" - checksum: 53b1e247e68e05db7b3808b99b892bd36fb096e6fba213a06da7fab22045e97597db425c724f2bbd6c99a3c295e1e73f3e4de78592289f38431049e1277ca0ae - languageName: node - linkType: hard - -"tapable@npm:^2.1.1, tapable@npm:^2.2.0": - version: 2.2.1 - resolution: "tapable@npm:2.2.1" - checksum: 3b7a1b4d86fa940aad46d9e73d1e8739335efd4c48322cb37d073eb6f80f5281889bf0320c6d8ffcfa1a0dd5bfdbd0f9d037e252ef972aca595330538aac4d51 - languageName: node - linkType: hard - -"terser-webpack-plugin@npm:^5.1.3": - version: 5.3.6 - resolution: "terser-webpack-plugin@npm:5.3.6" - dependencies: - "@jridgewell/trace-mapping": ^0.3.14 - jest-worker: ^27.4.5 - schema-utils: ^3.1.1 - serialize-javascript: ^6.0.0 - terser: ^5.14.1 - peerDependencies: - webpack: ^5.1.0 - peerDependenciesMeta: - "@swc/core": - optional: true - esbuild: - optional: true - uglify-js: - optional: true - checksum: 8f3448d7fdb0434ce6a0c09d95c462bfd2f4a5a430233d854163337f734a7f5c07c74513d16081e06d4ca33d366d5b1a36f5444219bc41a7403afd6162107bad - languageName: node - linkType: hard - -"terser@npm:^5.14.1": - version: 5.15.1 - resolution: "terser@npm:5.15.1" - dependencies: - "@jridgewell/source-map": ^0.3.2 - acorn: ^8.5.0 - commander: ^2.20.0 - source-map-support: ~0.5.20 - bin: - terser: bin/terser - checksum: 9880a1e0956983a1ce5de204ea35121c0009fa41d582a6904ae850e1953a1a2cc021168439565280c5a8eee67c85a874175627e24989b046c7a72589b81c3979 - languageName: node - linkType: hard - -"to-fast-properties@npm:^2.0.0": - version: 2.0.0 - resolution: "to-fast-properties@npm:2.0.0" - checksum: be2de62fe58ead94e3e592680052683b1ec986c72d589e7b21e5697f8744cdbf48c266fa72f6c15932894c10187b5f54573a3bcf7da0bfd964d5caf23d436168 - languageName: node - linkType: hard - -"ts-node@npm:10.9.1": - version: 10.9.1 - resolution: "ts-node@npm:10.9.1" - dependencies: - "@cspotcode/source-map-support": ^0.8.0 - "@tsconfig/node10": ^1.0.7 - "@tsconfig/node12": ^1.0.7 - "@tsconfig/node14": ^1.0.0 - "@tsconfig/node16": ^1.0.2 - acorn: ^8.4.1 - acorn-walk: ^8.1.1 - arg: ^4.1.0 - create-require: ^1.1.0 - diff: ^4.0.1 - make-error: ^1.1.1 - v8-compile-cache-lib: ^3.0.1 - yn: 3.1.1 - peerDependencies: - "@swc/core": ">=1.2.50" - "@swc/wasm": ">=1.2.50" - "@types/node": "*" - typescript: ">=2.7" - peerDependenciesMeta: - "@swc/core": - optional: true - "@swc/wasm": - optional: true - bin: - ts-node: dist/bin.js - ts-node-cwd: dist/bin-cwd.js - ts-node-esm: dist/bin-esm.js - ts-node-script: dist/bin-script.js - ts-node-transpile-only: dist/bin-transpile.js - ts-script: dist/bin-script-deprecated.js - checksum: 090adff1302ab20bd3486e6b4799e90f97726ed39e02b39e566f8ab674fd5bd5f727f43615debbfc580d33c6d9d1c6b1b3ce7d8e3cca3e20530a145ffa232c35 - languageName: node - linkType: hard - -"typescript@npm:4.8.4": - version: 4.8.4 - resolution: "typescript@npm:4.8.4" - bin: - tsc: bin/tsc - tsserver: bin/tsserver - checksum: 3e4f061658e0c8f36c820802fa809e0fd812b85687a9a2f5430bc3d0368e37d1c9605c3ce9b39df9a05af2ece67b1d844f9f6ea8ff42819f13bcb80f85629af0 - languageName: node - linkType: hard - -"typescript@patch:typescript@4.8.4#~builtin": - version: 4.8.4 - resolution: "typescript@patch:typescript@npm%3A4.8.4#~builtin::version=4.8.4&hash=701156" - bin: - tsc: bin/tsc - tsserver: bin/tsserver - checksum: 301459fc3eb3b1a38fe91bf96d98eb55da88a9cb17b4ef80b4d105d620f4d547ba776cc27b44cc2ef58b66eda23fe0a74142feb5e79a6fb99f54fc018a696afa - languageName: node - linkType: hard - -"unicode-canonical-property-names-ecmascript@npm:^2.0.0": - version: 2.0.0 - resolution: "unicode-canonical-property-names-ecmascript@npm:2.0.0" - checksum: 39be078afd014c14dcd957a7a46a60061bc37c4508ba146517f85f60361acf4c7539552645ece25de840e17e293baa5556268d091ca6762747fdd0c705001a45 - languageName: node - linkType: hard - -"unicode-match-property-ecmascript@npm:^2.0.0": - version: 2.0.0 - resolution: "unicode-match-property-ecmascript@npm:2.0.0" - dependencies: - unicode-canonical-property-names-ecmascript: ^2.0.0 - unicode-property-aliases-ecmascript: ^2.0.0 - checksum: 1f34a7434a23df4885b5890ac36c5b2161a809887000be560f56ad4b11126d433c0c1c39baf1016bdabed4ec54829a6190ee37aa24919aa116dc1a5a8a62965a - languageName: node - linkType: hard - -"unicode-match-property-value-ecmascript@npm:^2.0.0": - version: 2.0.0 - resolution: "unicode-match-property-value-ecmascript@npm:2.0.0" - checksum: 8fe6a09d9085a625cabcead5d95bdbc1a2d5d481712856092ce0347231e81a60b93a68f1b69e82b3076a07e415a72c708044efa2aa40ae23e2e7b5c99ed4a9ea - languageName: node - linkType: hard - -"unicode-property-aliases-ecmascript@npm:^2.0.0": - version: 2.1.0 - resolution: "unicode-property-aliases-ecmascript@npm:2.1.0" - checksum: 243524431893649b62cc674d877bd64ef292d6071dd2fd01ab4d5ad26efbc104ffcd064f93f8a06b7e4ec54c172bf03f6417921a0d8c3a9994161fe1f88f815b - languageName: node - linkType: hard - -"update-browserslist-db@npm:^1.0.9": - version: 1.0.10 - resolution: "update-browserslist-db@npm:1.0.10" - dependencies: - escalade: ^3.1.1 - picocolors: ^1.0.0 - peerDependencies: - browserslist: ">= 4.21.0" - bin: - browserslist-lint: cli.js - checksum: 12db73b4f63029ac407b153732e7cd69a1ea8206c9100b482b7d12859cd3cd0bc59c602d7ae31e652706189f1acb90d42c53ab24a5ba563ed13aebdddc5561a0 - languageName: node - linkType: hard - -"uri-js@npm:^4.2.2": - version: 4.4.1 - resolution: "uri-js@npm:4.4.1" - dependencies: - punycode: ^2.1.0 - checksum: 7167432de6817fe8e9e0c9684f1d2de2bb688c94388f7569f7dbdb1587c9f4ca2a77962f134ec90be0cc4d004c939ff0d05acc9f34a0db39a3c797dada262633 - languageName: node - linkType: hard - -"v8-compile-cache-lib@npm:^3.0.1": - version: 3.0.1 - resolution: "v8-compile-cache-lib@npm:3.0.1" - checksum: 78089ad549e21bcdbfca10c08850022b22024cdcc2da9b168bcf5a73a6ed7bf01a9cebb9eac28e03cd23a684d81e0502797e88f3ccd27a32aeab1cfc44c39da0 - languageName: node - linkType: hard - -"watchpack@npm:^2.4.0": - version: 2.4.0 - resolution: "watchpack@npm:2.4.0" - dependencies: - glob-to-regexp: ^0.4.1 - graceful-fs: ^4.1.2 - checksum: 23d4bc58634dbe13b86093e01c6a68d8096028b664ab7139d58f0c37d962d549a940e98f2f201cecdabd6f9c340338dc73ef8bf094a2249ef582f35183d1a131 - languageName: node - linkType: hard - -"webpack-cli@npm:4.10.0": - version: 4.10.0 - resolution: "webpack-cli@npm:4.10.0" - dependencies: - "@discoveryjs/json-ext": ^0.5.0 - "@webpack-cli/configtest": ^1.2.0 - "@webpack-cli/info": ^1.5.0 - "@webpack-cli/serve": ^1.7.0 - colorette: ^2.0.14 - commander: ^7.0.0 - cross-spawn: ^7.0.3 - fastest-levenshtein: ^1.0.12 - import-local: ^3.0.2 - interpret: ^2.2.0 - rechoir: ^0.7.0 - webpack-merge: ^5.7.3 - peerDependencies: - webpack: 4.x.x || 5.x.x - peerDependenciesMeta: - "@webpack-cli/generators": - optional: true - "@webpack-cli/migrate": - optional: true - webpack-bundle-analyzer: - optional: true - webpack-dev-server: - optional: true - bin: - webpack-cli: bin/cli.js - checksum: 2ff5355ac348e6b40f2630a203b981728834dca96d6d621be96249764b2d0fc01dd54edfcc37f02214d02935de2cf0eefd6ce689d970d154ef493f01ba922390 - languageName: node - linkType: hard - -"webpack-glob-entries@npm:1.0.1": - version: 1.0.1 - resolution: "webpack-glob-entries@npm:1.0.1" - dependencies: - glob: ^5.0.15 - checksum: 2ff68b0784070617370c65552a498967320e3328b62e9adeaa2361aaf499773db2530755c87d2195904a900f09bbda2581ee64d9853a99a37a2b30a4e305a8e4 - languageName: node - linkType: hard - -"webpack-merge@npm:^5.7.3": - version: 5.8.0 - resolution: "webpack-merge@npm:5.8.0" - dependencies: - clone-deep: ^4.0.1 - wildcard: ^2.0.0 - checksum: 88786ab91013f1bd2a683834ff381be81c245a4b0f63304a5103e90f6653f44dab496a0768287f8531761f8ad957d1f9f3ccb2cb55df0de1bd9ee343e079da26 - languageName: node - linkType: hard - -"webpack-sources@npm:^3.2.3": - version: 3.2.3 - resolution: "webpack-sources@npm:3.2.3" - checksum: 989e401b9fe3536529e2a99dac8c1bdc50e3a0a2c8669cbafad31271eadd994bc9405f88a3039cd2e29db5e6d9d0926ceb7a1a4e7409ece021fe79c37d9c4607 - languageName: node - linkType: hard - -"webpack@npm:5.74.0, webpack@npm:^5": - version: 5.74.0 - resolution: "webpack@npm:5.74.0" - dependencies: - "@types/eslint-scope": ^3.7.3 - "@types/estree": ^0.0.51 - "@webassemblyjs/ast": 1.11.1 - "@webassemblyjs/wasm-edit": 1.11.1 - "@webassemblyjs/wasm-parser": 1.11.1 - acorn: ^8.7.1 - acorn-import-assertions: ^1.7.6 - browserslist: ^4.14.5 - chrome-trace-event: ^1.0.2 - enhanced-resolve: ^5.10.0 - es-module-lexer: ^0.9.0 - eslint-scope: 5.1.1 - events: ^3.2.0 - glob-to-regexp: ^0.4.1 - graceful-fs: ^4.2.9 - json-parse-even-better-errors: ^2.3.1 - loader-runner: ^4.2.0 - mime-types: ^2.1.27 - neo-async: ^2.6.2 - schema-utils: ^3.1.0 - tapable: ^2.1.1 - terser-webpack-plugin: ^5.1.3 - watchpack: ^2.4.0 - webpack-sources: ^3.2.3 - peerDependenciesMeta: - webpack-cli: - optional: true - bin: - webpack: bin/webpack.js - checksum: 320c41369a75051b19e18c63f408b3dcc481852e992f83d311771c5ec0f05f2946385e8ebef62030cf3587f0a3d2f12779ffdb191569a966847289ba7313f946 - languageName: node - linkType: hard - -"which@npm:^2.0.1": - version: 2.0.2 - resolution: "which@npm:2.0.2" - dependencies: - isexe: ^2.0.0 - bin: - node-which: ./bin/node-which - checksum: 1a5c563d3c1b52d5f893c8b61afe11abc3bab4afac492e8da5bde69d550de701cf9806235f20a47b5c8fa8a1d6a9135841de2596535e998027a54589000e66d1 - languageName: node - linkType: hard - -"wildcard@npm:^2.0.0": - version: 2.0.0 - resolution: "wildcard@npm:2.0.0" - checksum: 1f4fe4c03dfc492777c60f795bbba597ac78794f1b650d68f398fbee9adb765367c516ebd4220889b6a81e9626e7228bbe0d66237abb311573c2ee1f4902a5ad - languageName: node - linkType: hard - -"wrappy@npm:1": - version: 1.0.2 - resolution: "wrappy@npm:1.0.2" - checksum: 159da4805f7e84a3d003d8841557196034155008f817172d4e986bd591f74aa82aa7db55929a54222309e01079a65a92a9e6414da5a6aa4b01ee44a511ac3ee5 - languageName: node - linkType: hard - -"yn@npm:3.1.1": - version: 3.1.1 - resolution: "yn@npm:3.1.1" - checksum: 2c487b0e149e746ef48cda9f8bad10fc83693cd69d7f9dcd8be4214e985de33a29c9e24f3c0d6bcf2288427040a8947406ab27f7af67ee9456e6b84854f02dd6 - languageName: node - linkType: hard From 68df83c86dc38ce7f51637786c92d4051487f64b Mon Sep 17 00:00:00 2001 From: Will Browne Date: Tue, 7 Mar 2023 15:47:02 +0000 Subject: [PATCH 035/288] Plugins: Add Plugin FS abstraction (#63734) * unexport pluginDir from dto * first pass * tidy * naming + add mutex * add dupe checking * fix func typo * interface + move logic from renderer * remote finder * remote signing * fix tests * tidy up * tidy markdown logic * split changes * fix tests * slim interface down * fix status code * tidy exec path func * fixup * undo changes * remove unused func * remove unused func * fix goimports * fetch remotely * simultaneous support * fix linter * use var * add exception for gosec warning * fixup * fix tests * tidy * rework cfg pattern * simplify * PR feedback * fix dupe field * remove g304 nolint * apply PR feedback * remove unnecessary gosec nolint * fix finder loop and update comment * fix map alloc * fix test * remove commented code --- pkg/api/plugins.go | 9 +- pkg/api/plugins_test.go | 106 ++-- pkg/plugins/ifaces.go | 18 + pkg/plugins/localfiles.go | 91 ++++ pkg/plugins/manager/fakes/fakes.go | 25 + pkg/plugins/manager/installer_test.go | 10 +- pkg/plugins/manager/loader/finder/finder.go | 92 +--- .../manager/loader/finder/finder_test.go | 121 ----- pkg/plugins/manager/loader/finder/fs.go | 286 +++++++++++ pkg/plugins/manager/loader/finder/fs_test.go | 485 ++++++++++++++++++ pkg/plugins/manager/loader/finder/ifaces.go | 11 + .../loader/initializer/initializer_test.go | 13 +- pkg/plugins/manager/loader/loader.go | 224 ++------ pkg/plugins/manager/loader/loader_test.go | 404 ++++++--------- .../manager/manager_integration_test.go | 19 +- pkg/plugins/manager/signature/manifest.go | 149 +++--- .../manager/signature/manifest_test.go | 51 +- pkg/plugins/manager/sources/sources.go | 2 +- pkg/plugins/manager/store/store.go | 5 +- pkg/plugins/manager/store/store_test.go | 12 +- pkg/plugins/plugins.go | 72 ++- pkg/services/updatechecker/plugins_test.go | 5 +- 22 files changed, 1342 insertions(+), 868 deletions(-) create mode 100644 pkg/plugins/localfiles.go delete mode 100644 pkg/plugins/manager/loader/finder/finder_test.go create mode 100644 pkg/plugins/manager/loader/finder/fs.go create mode 100644 pkg/plugins/manager/loader/finder/fs_test.go create mode 100644 pkg/plugins/manager/loader/finder/ifaces.go diff --git a/pkg/api/plugins.go b/pkg/api/plugins.go index ebc4301971c..5bb0e0834c2 100644 --- a/pkg/api/plugins.go +++ b/pkg/api/plugins.go @@ -271,17 +271,20 @@ func (hs *HTTPServer) GetPluginMarkdown(c *contextmodel.ReqContext) response.Res if err != nil { var notFound plugins.NotFoundError if errors.As(err, ¬Found) { - return response.Error(404, notFound.Error(), nil) + return response.Error(http.StatusNotFound, notFound.Error(), nil) } - return response.Error(500, "Could not get markdown file", err) + return response.Error(http.StatusInternalServerError, "Could not get markdown file", err) } // fallback try readme if len(content) == 0 { content, err = hs.pluginMarkdown(c.Req.Context(), pluginID, "readme") if err != nil { - return response.Error(501, "Could not get markdown file", err) + if errors.Is(err, plugins.ErrFileNotExist) { + return response.Error(http.StatusNotFound, plugins.ErrFileNotExist.Error(), nil) + } + return response.Error(http.StatusNotImplemented, "Could not get markdown file", err) } } diff --git a/pkg/api/plugins_test.go b/pkg/api/plugins_test.go index db1d1075ac9..bcaddeba8c5 100644 --- a/pkg/api/plugins_test.go +++ b/pkg/api/plugins_test.go @@ -20,7 +20,6 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/infra/log/logtest" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/config" "github.com/grafana/grafana/pkg/plugins/pluginscdn" @@ -270,14 +269,9 @@ func Test_GetPluginAssets(t *testing.T) { requestedFile := filepath.Clean(tmpFile.Name()) t.Run("Given a request for an existing plugin file", func(t *testing.T) { - p := &plugins.Plugin{ - JSONData: plugins.JSONData{ - ID: pluginID, - }, - PluginDir: pluginDir, - } + p := createPluginDTO(plugins.JSONData{ID: pluginID}, plugins.External, plugins.NewLocalFS(map[string]struct{}{requestedFile: {}}, filepath.Dir(requestedFile))) service := &plugins.FakePluginStore{ - PluginList: []plugins.PluginDTO{p.ToDTO()}, + PluginList: []plugins.PluginDTO{p}, } url := fmt.Sprintf("/public/plugins/%s/%s", pluginID, requestedFile) @@ -291,7 +285,7 @@ func Test_GetPluginAssets(t *testing.T) { }) t.Run("Given a request for a relative path", func(t *testing.T) { - p := createPluginDTO(plugins.JSONData{ID: pluginID}, plugins.External, pluginDir) + p := createPluginDTO(plugins.JSONData{ID: pluginID}, plugins.External, plugins.NewLocalFS(map[string]struct{}{}, "")) service := &plugins.FakePluginStore{ PluginList: []plugins.PluginDTO{p}, } @@ -305,8 +299,26 @@ func Test_GetPluginAssets(t *testing.T) { }) }) + t.Run("Given a request for an existing plugin file that is not listed as a signature covered file", func(t *testing.T) { + p := createPluginDTO(plugins.JSONData{ID: pluginID}, plugins.Core, plugins.NewLocalFS(map[string]struct{}{ + requestedFile: {}, + }, "")) + service := &plugins.FakePluginStore{ + PluginList: []plugins.PluginDTO{p}, + } + + url := fmt.Sprintf("/public/plugins/%s/%s", pluginID, requestedFile) + pluginAssetScenario(t, "When calling GET on", url, "/public/plugins/:pluginId/*", + setting.NewCfg(), service, func(sc *scenarioContext) { + callGetPluginAsset(sc) + + require.Equal(t, 200, sc.resp.Code) + assert.Equal(t, expectedBody, sc.resp.Body.String()) + }) + }) + t.Run("Given a request for an non-existing plugin file", func(t *testing.T) { - p := createPluginDTO(plugins.JSONData{ID: pluginID}, plugins.External, pluginDir) + p := createPluginDTO(plugins.JSONData{ID: pluginID}, plugins.External, plugins.NewLocalFS(map[string]struct{}{}, "")) service := &plugins.FakePluginStore{ PluginList: []plugins.PluginDTO{p}, } @@ -329,7 +341,6 @@ func Test_GetPluginAssets(t *testing.T) { service := &plugins.FakePluginStore{ PluginList: []plugins.PluginDTO{}, } - l := &logtest.Fake{} requestedFile := "nonExistent" url := fmt.Sprintf("/public/plugins/%s/%s", pluginID, requestedFile) @@ -342,29 +353,6 @@ func Test_GetPluginAssets(t *testing.T) { require.NoError(t, err) require.Equal(t, 404, sc.resp.Code) require.Equal(t, "Plugin not found", respJson["message"]) - require.Zero(t, l.WarnLogs.Calls) - }) - }) - - t.Run("Given a request for a core plugin's file", func(t *testing.T) { - service := &plugins.FakePluginStore{ - PluginList: []plugins.PluginDTO{ - { - JSONData: plugins.JSONData{ID: pluginID}, - Class: plugins.Core, - }, - }, - } - l := &logtest.Fake{} - - url := fmt.Sprintf("/public/plugins/%s/%s", pluginID, requestedFile) - pluginAssetScenario(t, "When calling GET on", url, "/public/plugins/:pluginId/*", - setting.NewCfg(), service, func(sc *scenarioContext) { - callGetPluginAsset(sc) - - require.Equal(t, 200, sc.resp.Code) - require.Equal(t, expectedBody, sc.resp.Body.String()) - require.Zero(t, l.WarnLogs.Calls) }) }) } @@ -546,40 +534,19 @@ func (c *fakePluginClient) QueryData(ctx context.Context, req *backend.QueryData } func Test_PluginsList_AccessControl(t *testing.T) { - p1 := &plugins.Plugin{ - PluginDir: "/grafana/plugins/test-app/dist", - Class: plugins.External, - DefaultNavURL: "/plugins/test-app/page/test", - Signature: plugins.SignatureUnsigned, - Module: "plugins/test-app/module", - BaseURL: "public/plugins/test-app", - JSONData: plugins.JSONData{ - ID: "test-app", - Type: plugins.App, - Name: "test-app", - Info: plugins.Info{ - Version: "1.0.0", - }, - }, - } - p2 := &plugins.Plugin{ - PluginDir: "/grafana/public/app/plugins/datasource/mysql", - Class: plugins.Core, - Pinned: false, - Signature: plugins.SignatureInternal, - Module: "app/plugins/datasource/mysql/module", - BaseURL: "public/app/plugins/datasource/mysql", - JSONData: plugins.JSONData{ - ID: "mysql", - Type: plugins.DataSource, - Name: "MySQL", + p1 := createPluginDTO(plugins.JSONData{ + ID: "test-app", Type: "app", Name: "test-app", + Info: plugins.Info{ + Version: "1.0.0", + }}, plugins.External, plugins.NewLocalFS(map[string]struct{}{}, "")) + p2 := createPluginDTO( + plugins.JSONData{ID: "mysql", Type: "datasource", Name: "MySQL", Info: plugins.Info{ Author: plugins.InfoLink{Name: "Grafana Labs", URL: "https://grafana.com"}, Description: "Data source for MySQL databases", - }, - }, - } - pluginStore := plugins.FakePluginStore{PluginList: []plugins.PluginDTO{p1.ToDTO(), p2.ToDTO()}} + }}, plugins.Core, plugins.NewLocalFS(map[string]struct{}{}, "")) + + pluginStore := plugins.FakePluginStore{PluginList: []plugins.PluginDTO{p1, p2}} pluginSettings := pluginsettings.FakePluginSettings{Plugins: map[string]*pluginsettings.DTO{ "test-app": {ID: 0, OrgID: 1, PluginID: "test-app", PluginVersion: "1.0.0", Enabled: true}, @@ -630,11 +597,12 @@ func Test_PluginsList_AccessControl(t *testing.T) { } } -func createPluginDTO(jd plugins.JSONData, class plugins.Class, pluginDir string) plugins.PluginDTO { +func createPluginDTO(jd plugins.JSONData, class plugins.Class, files plugins.FS) plugins.PluginDTO { p := &plugins.Plugin{ - JSONData: jd, - Class: class, - PluginDir: pluginDir, + JSONData: jd, + Class: class, + FS: files, } + return p.ToDTO() } diff --git a/pkg/plugins/ifaces.go b/pkg/plugins/ifaces.go index 9c8d36f03ab..fac68172d8a 100644 --- a/pkg/plugins/ifaces.go +++ b/pkg/plugins/ifaces.go @@ -2,6 +2,7 @@ package plugins import ( "context" + "io/fs" "github.com/grafana/grafana-plugin-sdk-go/backend" @@ -38,6 +39,23 @@ type UpdateInfo struct { PluginZipURL string } +type FS interface { + fs.FS + + Base() string + Files() []string +} + +type FoundBundle struct { + Primary FoundPlugin + Children []*FoundPlugin +} + +type FoundPlugin struct { + JSONData JSONData + FS FS +} + // Client is used to communicate with backend plugin implementations. type Client interface { backend.QueryDataHandler diff --git a/pkg/plugins/localfiles.go b/pkg/plugins/localfiles.go new file mode 100644 index 00000000000..805e62bcb4d --- /dev/null +++ b/pkg/plugins/localfiles.go @@ -0,0 +1,91 @@ +package plugins + +import ( + "io/fs" + "os" + "path/filepath" + "strings" + + "github.com/grafana/grafana/pkg/util" +) + +var _ fs.FS = (*LocalFS)(nil) + +type LocalFS struct { + m map[string]*LocalFile + basePath string +} + +func NewLocalFS(m map[string]struct{}, basePath string) LocalFS { + pfs := make(map[string]*LocalFile, len(m)) + for k := range m { + pfs[k] = &LocalFile{ + path: k, + } + } + + return LocalFS{ + m: pfs, + basePath: basePath, + } +} + +func (f LocalFS) Open(name string) (fs.File, error) { + cleanPath, err := util.CleanRelativePath(name) + if err != nil { + return nil, err + } + + if kv, exists := f.m[filepath.Join(f.basePath, cleanPath)]; exists { + if kv.f != nil { + return kv.f, nil + } + return os.Open(kv.path) + } + return nil, ErrFileNotExist +} + +func (f LocalFS) Base() string { + return f.basePath +} + +func (f LocalFS) Files() []string { + var files []string + for p := range f.m { + r, err := filepath.Rel(f.basePath, p) + if strings.Contains(r, "..") || err != nil { + continue + } + files = append(files, r) + } + + return files +} + +var _ fs.File = (*LocalFile)(nil) + +type LocalFile struct { + f *os.File + path string +} + +func (p *LocalFile) Stat() (fs.FileInfo, error) { + return os.Stat(p.path) +} + +func (p *LocalFile) Read(bytes []byte) (int, error) { + var err error + p.f, err = os.Open(p.path) + if err != nil { + return 0, err + } + return p.f.Read(bytes) +} + +func (p *LocalFile) Close() error { + if p.f != nil { + return p.f.Close() + } + p.f = nil + return nil +} diff --git a/pkg/plugins/manager/fakes/fakes.go b/pkg/plugins/manager/fakes/fakes.go index 6ac997b0138..394cce815eb 100644 --- a/pkg/plugins/manager/fakes/fakes.go +++ b/pkg/plugins/manager/fakes/fakes.go @@ -4,6 +4,7 @@ import ( "archive/zip" "context" "fmt" + "io/fs" "sync" "github.com/grafana/grafana-plugin-sdk-go/backend" @@ -351,6 +352,30 @@ func (f *FakeRoleRegistry) DeclarePluginRoles(_ context.Context, _ string, _ str return f.ExpectedErr } +type FakePluginFiles struct { + FS fs.FS + + base string +} + +func NewFakePluginFiles(base string) *FakePluginFiles { + return &FakePluginFiles{ + base: base, + } +} + +func (f *FakePluginFiles) Open(name string) (fs.File, error) { + return f.FS.Open(name) +} + +func (f *FakePluginFiles) Base() string { + return f.base +} + +func (f *FakePluginFiles) Files() []string { + return []string{} +} + type FakeSources struct { ListFunc func(_ context.Context) []plugins.PluginSource } diff --git a/pkg/plugins/manager/installer_test.go b/pkg/plugins/manager/installer_test.go index f7084ab7d35..0b768eac7f8 100644 --- a/pkg/plugins/manager/installer_test.go +++ b/pkg/plugins/manager/installer_test.go @@ -22,13 +22,11 @@ func TestPluginManager_Add_Remove(t *testing.T) { const ( pluginID, v1 = "test-panel", "1.0.0" zipNameV1 = "test-panel-1.0.0.zip" - pluginDirV1 = "/data/plugin/test-panel-1.0.0" ) // mock a plugin to be returned automatically by the plugin loader pluginV1 := createPlugin(t, pluginID, plugins.External, true, true, func(plugin *plugins.Plugin) { plugin.Info.Version = v1 - plugin.PluginDir = pluginDirV1 }) mockZipV1 := &zip.ReadCloser{Reader: zip.Reader{File: []*zip.File{{ FileHeader: zip.FileHeader{Name: zipNameV1}, @@ -63,7 +61,6 @@ func TestPluginManager_Add_Remove(t *testing.T) { }, RegisterFunc: func(_ context.Context, pluginID, pluginDir string) error { require.Equal(t, pluginV1.ID, pluginID) - require.Equal(t, pluginV1.PluginDir, pluginDir) return nil }, Store: map[string]struct{}{}, @@ -88,14 +85,12 @@ func TestPluginManager_Add_Remove(t *testing.T) { t.Run("Update plugin to different version", func(t *testing.T) { const ( - v2 = "2.0.0" - zipNameV2 = "test-panel-2.0.0.zip" - pluginDirV2 = "/data/plugin/test-panel-2.0.0" + v2 = "2.0.0" + zipNameV2 = "test-panel-2.0.0.zip" ) // mock a plugin to be returned automatically by the plugin loader pluginV2 := createPlugin(t, pluginID, plugins.External, true, true, func(plugin *plugins.Plugin) { plugin.Info.Version = v2 - plugin.PluginDir = pluginDirV2 }) mockZipV2 := &zip.ReadCloser{Reader: zip.Reader{File: []*zip.File{{ @@ -126,7 +121,6 @@ func TestPluginManager_Add_Remove(t *testing.T) { } fs.RegisterFunc = func(_ context.Context, pluginID, pluginDir string) error { require.Equal(t, pluginV2.ID, pluginID) - require.Equal(t, pluginV2.PluginDir, pluginDir) return nil } diff --git a/pkg/plugins/manager/loader/finder/finder.go b/pkg/plugins/manager/loader/finder/finder.go index 8813978dab2..6697e0437c8 100644 --- a/pkg/plugins/manager/loader/finder/finder.go +++ b/pkg/plugins/manager/loader/finder/finder.go @@ -1,90 +1,44 @@ package finder import ( - "errors" - "fmt" - "os" - "path/filepath" + "context" - "github.com/grafana/grafana/pkg/infra/fs" + "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/log" - "github.com/grafana/grafana/pkg/util" ) -var walk = util.Walk - -type Finder struct { - log log.Logger +type Service struct { + local *FS + log log.Logger } -func New() Finder { - return Finder{log: log.New("plugin.finder")} +func NewService() *Service { + logger := log.New("plugin.finder") + return &Service{ + local: newFS(logger), + log: logger, + } } -func (f *Finder) Find(pluginPaths []string) ([]string, error) { - var pluginJSONPaths []string +func (f *Service) Find(ctx context.Context, pluginPaths ...string) ([]*plugins.FoundBundle, error) { + if len(pluginPaths) == 0 { + return []*plugins.FoundBundle{}, nil + } + fbs := make(map[string][]*plugins.FoundBundle) for _, path := range pluginPaths { - exists, err := fs.Exists(path) + local, err := f.local.Find(ctx, path) if err != nil { - f.log.Warn("Error occurred when checking if plugin directory exists", "path", path, "err", err) - } - if !exists { - f.log.Warn("Skipping finding plugins as directory does not exist", "path", path) + f.log.Warn("Error occurred when trying to find plugin", "path", path) continue } - - paths, err := f.getAbsPluginJSONPaths(path) - if err != nil { - return nil, err - } - pluginJSONPaths = append(pluginJSONPaths, paths...) + fbs[path] = local } - return pluginJSONPaths, nil -} - -func (f *Finder) getAbsPluginJSONPaths(path string) ([]string, error) { - var pluginJSONPaths []string - - var err error - path, err = filepath.Abs(path) - if err != nil { - return []string{}, err - } - - if err := walk(path, true, true, - func(currentPath string, fi os.FileInfo, err error) error { - if err != nil { - if errors.Is(err, os.ErrNotExist) { - f.log.Error("Couldn't scan directory since it doesn't exist", "pluginDir", path, "err", err) - return nil - } - if errors.Is(err, os.ErrPermission) { - f.log.Error("Couldn't scan directory due to lack of permissions", "pluginDir", path, "err", err) - return nil - } - - return fmt.Errorf("filepath.Walk reported an error for %q: %w", currentPath, err) - } - - if fi.Name() == "node_modules" { - return util.ErrWalkSkipDir - } - - if fi.IsDir() { - return nil - } - - if fi.Name() != "plugin.json" { - return nil - } - - pluginJSONPaths = append(pluginJSONPaths, currentPath) - return nil - }); err != nil { - return []string{}, err + var found []*plugins.FoundBundle + for _, fb := range fbs { + found = append(found, fb...) } - return pluginJSONPaths, nil + return found, nil } diff --git a/pkg/plugins/manager/loader/finder/finder_test.go b/pkg/plugins/manager/loader/finder/finder_test.go deleted file mode 100644 index 045a42186c0..00000000000 --- a/pkg/plugins/manager/loader/finder/finder_test.go +++ /dev/null @@ -1,121 +0,0 @@ -package finder - -import ( - "errors" - "fmt" - "os" - "strings" - "testing" - - "github.com/grafana/grafana/pkg/plugins/log" - "github.com/grafana/grafana/pkg/util" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestFinder_Find(t *testing.T) { - testCases := []struct { - name string - pluginDirs []string - expectedPathSuffix []string - err error - }{ - { - name: "Dir with single plugin", - pluginDirs: []string{"../../testdata/valid-v2-signature"}, - expectedPathSuffix: []string{"/pkg/plugins/manager/testdata/valid-v2-signature/plugin/plugin.json"}, - }, - { - name: "Dir with nested plugins", - pluginDirs: []string{"../../testdata/duplicate-plugins"}, - expectedPathSuffix: []string{ - "/pkg/plugins/manager/testdata/duplicate-plugins/nested/nested/plugin.json", - "/pkg/plugins/manager/testdata/duplicate-plugins/nested/plugin.json", - }, - }, - { - name: "Dir with single plugin which has symbolic link root directory", - pluginDirs: []string{"../../testdata/symbolic-plugin-dirs"}, - expectedPathSuffix: []string{"/pkg/plugins/manager/testdata/includes-symlinks/plugin.json"}, - }, - { - name: "Multiple plugin dirs", - pluginDirs: []string{"../../testdata/duplicate-plugins", "../../testdata/invalid-v1-signature"}, - expectedPathSuffix: []string{ - "/pkg/plugins/manager/testdata/duplicate-plugins/nested/nested/plugin.json", - "/pkg/plugins/manager/testdata/duplicate-plugins/nested/plugin.json", - "/pkg/plugins/manager/testdata/invalid-v1-signature/plugin/plugin.json"}, - }, - } - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - f := New() - pluginPaths, err := f.Find(tc.pluginDirs) - if (err != nil) && !errors.Is(err, tc.err) { - t.Errorf("Find() error = %v, expected error %v", err, tc.err) - return - } - - assert.Equal(t, len(tc.expectedPathSuffix), len(pluginPaths)) - for i := 0; i < len(tc.expectedPathSuffix); i++ { - assert.True(t, strings.HasSuffix(pluginPaths[i], tc.expectedPathSuffix[i])) - } - }) - } -} - -func TestFinder_getAbsPluginJSONPaths(t *testing.T) { - t.Run("When scanning a folder that doesn't exists shouldn't return an error", func(t *testing.T) { - origWalk := walk - walk = func(path string, followSymlinks, detectSymlinkInfiniteLoop bool, walkFn util.WalkFunc) error { - return walkFn(path, nil, os.ErrNotExist) - } - t.Cleanup(func() { - walk = origWalk - }) - - finder := &Finder{ - log: log.NewTestLogger(), - } - - paths, err := finder.getAbsPluginJSONPaths("test") - require.NoError(t, err) - require.Empty(t, paths) - }) - - t.Run("When scanning a folder that lacks permission shouldn't return an error", func(t *testing.T) { - origWalk := walk - walk = func(path string, followSymlinks, detectSymlinkInfiniteLoop bool, walkFn util.WalkFunc) error { - return walkFn(path, nil, os.ErrPermission) - } - t.Cleanup(func() { - walk = origWalk - }) - - finder := &Finder{ - log: log.NewTestLogger(), - } - - paths, err := finder.getAbsPluginJSONPaths("test") - require.NoError(t, err) - require.Empty(t, paths) - }) - - t.Run("When scanning a folder that returns a non-handled error should return that error", func(t *testing.T) { - origWalk := walk - walk = func(path string, followSymlinks, detectSymlinkInfiniteLoop bool, walkFn util.WalkFunc) error { - return walkFn(path, nil, fmt.Errorf("random error")) - } - t.Cleanup(func() { - walk = origWalk - }) - - finder := &Finder{ - log: log.NewTestLogger(), - } - - paths, err := finder.getAbsPluginJSONPaths("test") - require.Error(t, err) - require.Empty(t, paths) - }) -} diff --git a/pkg/plugins/manager/loader/finder/fs.go b/pkg/plugins/manager/loader/finder/fs.go new file mode 100644 index 00000000000..629ddbbcbe9 --- /dev/null +++ b/pkg/plugins/manager/loader/finder/fs.go @@ -0,0 +1,286 @@ +package finder + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + "runtime" + "strings" + + "github.com/grafana/grafana/pkg/infra/fs" + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/plugins/log" + "github.com/grafana/grafana/pkg/services/org" + "github.com/grafana/grafana/pkg/util" +) + +var walk = util.Walk + +var ( + ErrInvalidPluginJSON = errors.New("did not find valid type or id properties in plugin.json") + ErrInvalidPluginJSONFilePath = errors.New("invalid plugin.json filepath was provided") +) + +type FS struct { + log log.Logger +} + +func newFS(logger log.Logger) *FS { + return &FS{log: logger.New("fs")} +} + +func (f *FS) Find(_ context.Context, pluginPaths ...string) ([]*plugins.FoundBundle, error) { + if len(pluginPaths) == 0 { + return []*plugins.FoundBundle{}, nil + } + + var pluginJSONPaths []string + for _, path := range pluginPaths { + exists, err := fs.Exists(path) + if err != nil { + f.log.Warn("Skipping finding plugins as an error occurred", "path", path, "err", err) + continue + } + if !exists { + f.log.Warn("Skipping finding plugins as directory does not exist", "path", path) + continue + } + + paths, err := f.getAbsPluginJSONPaths(path) + if err != nil { + return nil, err + } + pluginJSONPaths = append(pluginJSONPaths, paths...) + } + + // load plugin.json files and map directory to JSON data + foundPlugins := make(map[string]plugins.JSONData) + for _, pluginJSONPath := range pluginJSONPaths { + plugin, err := f.readPluginJSON(pluginJSONPath) + if err != nil { + f.log.Warn("Skipping plugin loading as its plugin.json could not be read", "path", pluginJSONPath, "err", err) + continue + } + + pluginJSONAbsPath, err := filepath.Abs(pluginJSONPath) + if err != nil { + f.log.Warn("Skipping plugin loading as absolute plugin.json path could not be calculated", "pluginID", plugin.ID, "err", err) + continue + } + + if _, dupe := foundPlugins[filepath.Dir(pluginJSONAbsPath)]; dupe { + f.log.Warn("Skipping plugin loading as it's a duplicate", "pluginID", plugin.ID) + continue + } + foundPlugins[filepath.Dir(pluginJSONAbsPath)] = plugin + } + + var res = make(map[string]*plugins.FoundBundle) + for pluginDir, data := range foundPlugins { + files, err := collectFilesWithin(pluginDir) + if err != nil { + return nil, err + } + + res[pluginDir] = &plugins.FoundBundle{ + Primary: plugins.FoundPlugin{ + JSONData: data, + FS: plugins.NewLocalFS(files, pluginDir), + }, + } + } + + var result []*plugins.FoundBundle + for dir := range foundPlugins { + ancestors := strings.Split(dir, string(filepath.Separator)) + ancestors = ancestors[0 : len(ancestors)-1] + + pluginPath := "" + if runtime.GOOS != "windows" && filepath.IsAbs(dir) { + pluginPath = "/" + } + add := true + for _, ancestor := range ancestors { + pluginPath = filepath.Join(pluginPath, ancestor) + if _, ok := foundPlugins[pluginPath]; ok { + if fp, exists := res[pluginPath]; exists { + fp.Children = append(fp.Children, &res[dir].Primary) + add = false + break + } + } + } + if add { + result = append(result, res[dir]) + } + } + + return result, nil +} + +func (f *FS) getAbsPluginJSONPaths(path string) ([]string, error) { + var pluginJSONPaths []string + + var err error + path, err = filepath.Abs(path) + if err != nil { + return []string{}, err + } + + if err = walk(path, true, true, + func(currentPath string, fi os.FileInfo, err error) error { + if err != nil { + if errors.Is(err, os.ErrNotExist) { + f.log.Error("Couldn't scan directory since it doesn't exist", "pluginDir", path, "err", err) + return nil + } + if errors.Is(err, os.ErrPermission) { + f.log.Error("Couldn't scan directory due to lack of permissions", "pluginDir", path, "err", err) + return nil + } + + return fmt.Errorf("filepath.Walk reported an error for %q: %w", currentPath, err) + } + + if fi.Name() == "node_modules" { + return util.ErrWalkSkipDir + } + + if fi.IsDir() { + return nil + } + + if fi.Name() != "plugin.json" { + return nil + } + + pluginJSONPaths = append(pluginJSONPaths, currentPath) + return nil + }); err != nil { + return []string{}, err + } + + return pluginJSONPaths, nil +} + +func (f *FS) readPluginJSON(pluginJSONPath string) (plugins.JSONData, error) { + f.log.Debug("Loading plugin", "path", pluginJSONPath) + + if !strings.EqualFold(filepath.Ext(pluginJSONPath), ".json") { + return plugins.JSONData{}, ErrInvalidPluginJSONFilePath + } + + absPluginJSONPath, err := filepath.Abs(pluginJSONPath) + if err != nil { + return plugins.JSONData{}, err + } + + // Wrapping in filepath.Clean to properly handle + // gosec G304 Potential file inclusion via variable rule. + reader, err := os.Open(filepath.Clean(absPluginJSONPath)) + if err != nil { + return plugins.JSONData{}, err + } + defer func() { + if reader == nil { + return + } + if err = reader.Close(); err != nil { + f.log.Warn("Failed to close JSON file", "path", pluginJSONPath, "err", err) + } + }() + + plugin := plugins.JSONData{} + if err = json.NewDecoder(reader).Decode(&plugin); err != nil { + return plugins.JSONData{}, err + } + + if err = validatePluginJSON(plugin); err != nil { + return plugins.JSONData{}, err + } + + if plugin.ID == "grafana-piechart-panel" { + plugin.Name = "Pie Chart (old)" + } + + if len(plugin.Dependencies.Plugins) == 0 { + plugin.Dependencies.Plugins = []plugins.Dependency{} + } + + if plugin.Dependencies.GrafanaVersion == "" { + plugin.Dependencies.GrafanaVersion = "*" + } + + for _, include := range plugin.Includes { + if include.Role == "" { + include.Role = org.RoleViewer + } + } + + return plugin, nil +} + +func validatePluginJSON(data plugins.JSONData) error { + if data.ID == "" || !data.Type.IsValid() { + return ErrInvalidPluginJSON + } + return nil +} + +func collectFilesWithin(dir string) (map[string]struct{}, error) { + files := map[string]struct{}{} + err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + if info.Mode()&os.ModeSymlink == os.ModeSymlink { + symlinkPath, err := filepath.EvalSymlinks(path) + if err != nil { + return err + } + + symlink, err := os.Stat(symlinkPath) + if err != nil { + return err + } + + // verify that symlinked file is within plugin directory + p, err := filepath.Rel(dir, symlinkPath) + if err != nil { + return err + } + if p == ".." || strings.HasPrefix(p, ".."+string(filepath.Separator)) { + return fmt.Errorf("file '%s' not inside of plugin directory", p) + } + + // skip adding symlinked directories + if symlink.IsDir() { + return nil + } + } + + // skip directories + if info.IsDir() { + return nil + } + + // verify that file is within plugin directory + file, err := filepath.Rel(dir, path) + if err != nil { + return err + } + if strings.HasPrefix(file, ".."+string(filepath.Separator)) { + return fmt.Errorf("file '%s' not inside of plugin directory", file) + } + + files[path] = struct{}{} + + return nil + }) + + return files, err +} diff --git a/pkg/plugins/manager/loader/finder/fs_test.go b/pkg/plugins/manager/loader/finder/fs_test.go new file mode 100644 index 00000000000..1954d41c146 --- /dev/null +++ b/pkg/plugins/manager/loader/finder/fs_test.go @@ -0,0 +1,485 @@ +package finder + +import ( + "context" + "errors" + "fmt" + "os" + "path/filepath" + "sort" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/plugins/log" + "github.com/grafana/grafana/pkg/services/org" + "github.com/grafana/grafana/pkg/util" +) + +func TestFinder_Find(t *testing.T) { + testData, err := filepath.Abs("../../testdata") + if err != nil { + require.NoError(t, err) + } + testCases := []struct { + name string + pluginDirs []string + expectedBundles []*plugins.FoundBundle + err error + }{ + { + name: "Dir with single plugin", + pluginDirs: []string{filepath.Join(testData, "valid-v2-signature")}, + expectedBundles: []*plugins.FoundBundle{ + { + Primary: plugins.FoundPlugin{ + JSONData: plugins.JSONData{ + ID: "test-datasource", + Type: plugins.DataSource, + Name: "Test", + Info: plugins.Info{ + Author: plugins.InfoLink{ + Name: "Will Browne", + URL: "https://willbrowne.com", + }, + Description: "Test", + Version: "1.0.0", + }, + Dependencies: plugins.Dependencies{ + GrafanaVersion: "*", + Plugins: []plugins.Dependency{}, + }, + State: plugins.AlphaRelease, + Backend: true, + Executable: "test", + }, + FS: plugins.NewLocalFS(map[string]struct{}{ + filepath.Join(testData, "valid-v2-signature/plugin/plugin.json"): {}, + filepath.Join(testData, "valid-v2-signature/plugin/MANIFEST.txt"): {}, + }, filepath.Join(testData, "valid-v2-signature/plugin")), + }, + }, + }, + }, + { + name: "Dir with nested plugins", + pluginDirs: []string{"../../testdata/duplicate-plugins"}, + expectedBundles: []*plugins.FoundBundle{ + { + Primary: plugins.FoundPlugin{ + JSONData: plugins.JSONData{ + ID: "test-app", + Type: plugins.DataSource, + Name: "Parent", + Info: plugins.Info{ + Author: plugins.InfoLink{ + Name: "Grafana Labs", + URL: "http://grafana.com", + }, + Description: "Parent plugin", + Version: "1.0.0", + Updated: "2020-10-20", + }, + Dependencies: plugins.Dependencies{ + GrafanaVersion: "*", + Plugins: []plugins.Dependency{}, + }, + }, + FS: plugins.NewLocalFS(map[string]struct{}{ + filepath.Join(testData, "duplicate-plugins/nested/plugin.json"): {}, + filepath.Join(testData, "duplicate-plugins/nested/MANIFEST.txt"): {}, + filepath.Join(testData, "duplicate-plugins/nested/nested/plugin.json"): {}, + filepath.Join(testData, "duplicate-plugins/nested/nested/MANIFEST.txt"): {}, + }, filepath.Join(testData, "duplicate-plugins/nested")), + }, + Children: []*plugins.FoundPlugin{ + { + JSONData: plugins.JSONData{ + ID: "test-app", + Type: plugins.DataSource, + Name: "Child", + Info: plugins.Info{ + Author: plugins.InfoLink{ + Name: "Grafana Labs", + URL: "http://grafana.com", + }, + Description: "Child plugin", + Version: "1.0.0", + Updated: "2020-10-20", + }, + Dependencies: plugins.Dependencies{ + GrafanaVersion: "*", + Plugins: []plugins.Dependency{}, + }, + }, + FS: plugins.NewLocalFS(map[string]struct{}{ + filepath.Join(testData, "duplicate-plugins/nested/nested/plugin.json"): {}, + filepath.Join(testData, "duplicate-plugins/nested/nested/MANIFEST.txt"): {}, + }, filepath.Join(testData, "duplicate-plugins/nested/nested")), + }, + }, + }, + }, + }, + { + name: "Dir with single plugin which has symbolic link root directory", + pluginDirs: []string{"../../testdata/symbolic-plugin-dirs"}, + expectedBundles: []*plugins.FoundBundle{ + { + Primary: plugins.FoundPlugin{ + JSONData: plugins.JSONData{ + ID: "test-app", + Type: plugins.App, + Name: "Test App", + Info: plugins.Info{ + Author: plugins.InfoLink{ + Name: "Test Inc.", + URL: "http://test.com", + }, + Description: "Official Grafana Test App & Dashboard bundle", + Version: "1.0.0", + Links: []plugins.InfoLink{ + {Name: "Project site", URL: "http://project.com"}, + {Name: "License & Terms", URL: "http://license.com"}, + }, + Updated: "2015-02-10", + Logos: plugins.Logos{ + Small: "img/logo_small.png", + Large: "img/logo_large.png", + }, + Screenshots: []plugins.Screenshots{ + {Name: "img1", Path: "img/screenshot1.png"}, + {Name: "img2", Path: "img/screenshot2.png"}, + }, + }, + Dependencies: plugins.Dependencies{ + GrafanaVersion: "3.x.x", + Plugins: []plugins.Dependency{ + {ID: "graphite", Type: "datasource", Name: "Graphite", Version: "1.0.0"}, + {ID: "graph", Type: "panel", Name: "Graph", Version: "1.0.0"}, + }, + }, + Includes: []*plugins.Includes{ + { + Name: "Nginx Connections", + Path: "dashboards/connections.json", + Type: "dashboard", + Role: "Viewer", + }, + { + Name: "Nginx Memory", + Path: "dashboards/memory.json", + Type: "dashboard", + Role: "Viewer", + }, + {Name: "Nginx Panel", Type: "panel", Role: "Viewer"}, + {Name: "Nginx Datasource", Type: "datasource", Role: "Viewer"}, + }, + }, + FS: plugins.NewLocalFS(map[string]struct{}{ + filepath.Join(testData, "includes-symlinks/MANIFEST.txt"): {}, + filepath.Join(testData, "includes-symlinks/dashboards/connections.json"): {}, + filepath.Join(testData, "includes-symlinks/dashboards/extra/memory.json"): {}, + filepath.Join(testData, "includes-symlinks/plugin.json"): {}, + filepath.Join(testData, "includes-symlinks/symlink_to_txt"): {}, + filepath.Join(testData, "includes-symlinks/text.txt"): {}, + }, filepath.Join(testData, "includes-symlinks")), + }, + }, + }, + }, + { + name: "Multiple plugin dirs", + pluginDirs: []string{"../../testdata/duplicate-plugins", "../../testdata/invalid-v1-signature"}, + expectedBundles: []*plugins.FoundBundle{{ + Primary: plugins.FoundPlugin{ + JSONData: plugins.JSONData{ + ID: "test-app", + Type: plugins.DataSource, + Name: "Parent", + Info: plugins.Info{ + Author: plugins.InfoLink{ + Name: "Grafana Labs", + URL: "http://grafana.com", + }, + Description: "Parent plugin", + Version: "1.0.0", + Updated: "2020-10-20", + }, + Dependencies: plugins.Dependencies{ + GrafanaVersion: "*", + Plugins: []plugins.Dependency{}, + }, + }, + FS: plugins.NewLocalFS(map[string]struct{}{ + filepath.Join(testData, "duplicate-plugins/nested/plugin.json"): {}, + filepath.Join(testData, "duplicate-plugins/nested/MANIFEST.txt"): {}, + filepath.Join(testData, "duplicate-plugins/nested/nested/plugin.json"): {}, + filepath.Join(testData, "duplicate-plugins/nested/nested/MANIFEST.txt"): {}, + }, filepath.Join(testData, "duplicate-plugins/nested")), + }, + Children: []*plugins.FoundPlugin{ + { + JSONData: plugins.JSONData{ + ID: "test-app", + Type: plugins.DataSource, + Name: "Child", + Info: plugins.Info{ + Author: plugins.InfoLink{ + Name: "Grafana Labs", + URL: "http://grafana.com", + }, + Description: "Child plugin", + Version: "1.0.0", + Updated: "2020-10-20", + }, + Dependencies: plugins.Dependencies{ + GrafanaVersion: "*", + Plugins: []plugins.Dependency{}, + }, + }, + FS: plugins.NewLocalFS(map[string]struct{}{ + filepath.Join(testData, "duplicate-plugins/nested/nested/plugin.json"): {}, + filepath.Join(testData, "duplicate-plugins/nested/nested/MANIFEST.txt"): {}, + }, filepath.Join(testData, "duplicate-plugins/nested/nested")), + }, + }, + }, + { + Primary: plugins.FoundPlugin{ + JSONData: plugins.JSONData{ + ID: "test-datasource", + Type: plugins.DataSource, + Name: "Test", + Info: plugins.Info{ + Author: plugins.InfoLink{ + Name: "Grafana Labs", + URL: "https://grafana.com", + }, + Description: "Test", + }, + Dependencies: plugins.Dependencies{ + GrafanaVersion: "*", + Plugins: []plugins.Dependency{}, + }, + State: plugins.AlphaRelease, + Backend: true, + }, + FS: plugins.NewLocalFS(map[string]struct{}{ + filepath.Join(testData, "invalid-v1-signature/plugin/plugin.json"): {}, + filepath.Join(testData, "invalid-v1-signature/plugin/MANIFEST.txt"): {}, + }, filepath.Join(testData, "invalid-v1-signature/plugin")), + }, + }, + }, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + f := newFS(log.NewTestLogger()) + pluginBundles, err := f.Find(context.Background(), tc.pluginDirs...) + if (err != nil) && !errors.Is(err, tc.err) { + t.Errorf("Find() error = %v, expected error %v", err, tc.err) + return + } + + // to ensure we can compare with expected + sort.SliceStable(pluginBundles, func(i, j int) bool { + return pluginBundles[i].Primary.JSONData.ID < pluginBundles[j].Primary.JSONData.ID + }) + + if !cmp.Equal(pluginBundles, tc.expectedBundles, localFSComparer) { + t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(pluginBundles, tc.expectedBundles, localFSComparer)) + } + }) + } +} + +func TestFinder_getAbsPluginJSONPaths(t *testing.T) { + t.Run("When scanning a folder that doesn't exists shouldn't return an error", func(t *testing.T) { + origWalk := walk + walk = func(path string, followSymlinks, detectSymlinkInfiniteLoop bool, walkFn util.WalkFunc) error { + return walkFn(path, nil, os.ErrNotExist) + } + t.Cleanup(func() { + walk = origWalk + }) + + finder := newFS(log.NewTestLogger()) + paths, err := finder.getAbsPluginJSONPaths("test") + require.NoError(t, err) + require.Empty(t, paths) + }) + + t.Run("When scanning a folder that lacks permission shouldn't return an error", func(t *testing.T) { + origWalk := walk + walk = func(path string, followSymlinks, detectSymlinkInfiniteLoop bool, walkFn util.WalkFunc) error { + return walkFn(path, nil, os.ErrPermission) + } + t.Cleanup(func() { + walk = origWalk + }) + + finder := newFS(log.NewTestLogger()) + paths, err := finder.getAbsPluginJSONPaths("test") + require.NoError(t, err) + require.Empty(t, paths) + }) + + t.Run("When scanning a folder that returns a non-handled error should return that error", func(t *testing.T) { + origWalk := walk + walk = func(path string, followSymlinks, detectSymlinkInfiniteLoop bool, walkFn util.WalkFunc) error { + return walkFn(path, nil, fmt.Errorf("random error")) + } + t.Cleanup(func() { + walk = origWalk + }) + + finder := newFS(log.NewTestLogger()) + paths, err := finder.getAbsPluginJSONPaths("test") + require.Error(t, err) + require.Empty(t, paths) + }) +} + +func TestFinder_validatePluginJSON(t *testing.T) { + type args struct { + data plugins.JSONData + } + tests := []struct { + name string + args args + err error + }{ + { + name: "Valid case", + args: args{ + data: plugins.JSONData{ + ID: "grafana-plugin-id", + Type: plugins.DataSource, + }, + }, + }, + { + name: "Invalid plugin ID", + args: args{ + data: plugins.JSONData{ + Type: plugins.Panel, + }, + }, + err: ErrInvalidPluginJSON, + }, + { + name: "Invalid plugin type", + args: args{ + data: plugins.JSONData{ + ID: "grafana-plugin-id", + Type: "test", + }, + }, + err: ErrInvalidPluginJSON, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := validatePluginJSON(tt.args.data); !errors.Is(err, tt.err) { + t.Errorf("validatePluginJSON() = %v, want %v", err, tt.err) + } + }) + } +} + +func TestFinder_readPluginJSON(t *testing.T) { + tests := []struct { + name string + pluginPath string + expected plugins.JSONData + failed bool + }{ + { + name: "Valid plugin", + pluginPath: "../../testdata/test-app/plugin.json", + expected: plugins.JSONData{ + ID: "test-app", + Type: "app", + Name: "Test App", + Info: plugins.Info{ + Author: plugins.InfoLink{ + Name: "Test Inc.", + URL: "http://test.com", + }, + Description: "Official Grafana Test App & Dashboard bundle", + Version: "1.0.0", + Links: []plugins.InfoLink{ + {Name: "Project site", URL: "http://project.com"}, + {Name: "License & Terms", URL: "http://license.com"}, + }, + Logos: plugins.Logos{ + Small: "img/logo_small.png", + Large: "img/logo_large.png", + }, + Screenshots: []plugins.Screenshots{ + {Path: "img/screenshot1.png", Name: "img1"}, + {Path: "img/screenshot2.png", Name: "img2"}, + }, + Updated: "2015-02-10", + }, + Dependencies: plugins.Dependencies{ + GrafanaVersion: "3.x.x", + Plugins: []plugins.Dependency{ + {Type: "datasource", ID: "graphite", Name: "Graphite", Version: "1.0.0"}, + {Type: "panel", ID: "graph", Name: "Graph", Version: "1.0.0"}, + }, + }, + Includes: []*plugins.Includes{ + {Name: "Nginx Connections", Path: "dashboards/connections.json", Type: "dashboard", Role: org.RoleViewer}, + {Name: "Nginx Memory", Path: "dashboards/memory.json", Type: "dashboard", Role: org.RoleViewer}, + {Name: "Nginx Panel", Type: "panel", Role: org.RoleViewer}, + {Name: "Nginx Datasource", Type: "datasource", Role: org.RoleViewer}, + }, + Backend: false, + }, + }, + { + name: "Invalid plugin JSON", + pluginPath: "../testdata/invalid-plugin-json/plugin.json", + failed: true, + }, + { + name: "Non-existing JSON file", + pluginPath: "nonExistingFile.json", + failed: true, + }, + } + + f := newFS(log.NewTestLogger()) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := f.readPluginJSON(tt.pluginPath) + if (err != nil) && !tt.failed { + t.Errorf("readPluginJSON() error = %v, failed %v", err, tt.failed) + return + } + if !cmp.Equal(got, tt.expected) { + t.Errorf("Unexpected pluginJSONData: %v", cmp.Diff(got, tt.expected)) + } + }) + } +} + +var localFSComparer = cmp.Comparer(func(fs1 plugins.LocalFS, fs2 plugins.LocalFS) bool { + fs1Files := fs1.Files() + fs2Files := fs2.Files() + + sort.SliceStable(fs1Files, func(i, j int) bool { + return fs1Files[i] < fs1Files[j] + }) + + sort.SliceStable(fs2Files, func(i, j int) bool { + return fs2Files[i] < fs2Files[j] + }) + + return cmp.Equal(fs1Files, fs2Files) && fs1.Base() == fs2.Base() +}) diff --git a/pkg/plugins/manager/loader/finder/ifaces.go b/pkg/plugins/manager/loader/finder/ifaces.go new file mode 100644 index 00000000000..cddbb7ecc91 --- /dev/null +++ b/pkg/plugins/manager/loader/finder/ifaces.go @@ -0,0 +1,11 @@ +package finder + +import ( + "context" + + "github.com/grafana/grafana/pkg/plugins" +) + +type Finder interface { + Find(ctx context.Context, uris ...string) ([]*plugins.FoundBundle, error) +} diff --git a/pkg/plugins/manager/loader/initializer/initializer_test.go b/pkg/plugins/manager/loader/initializer/initializer_test.go index d6a17b29f10..6cc505b274f 100644 --- a/pkg/plugins/manager/loader/initializer/initializer_test.go +++ b/pkg/plugins/manager/loader/initializer/initializer_test.go @@ -2,7 +2,6 @@ package initializer import ( "context" - "path/filepath" "testing" "github.com/stretchr/testify/assert" @@ -15,9 +14,6 @@ import ( ) func TestInitializer_Initialize(t *testing.T) { - absCurPath, err := filepath.Abs(".") - assert.NoError(t, err) - t.Run("core backend datasource", func(t *testing.T) { p := &plugins.Plugin{ JSONData: plugins.JSONData{ @@ -31,8 +27,7 @@ func TestInitializer_Initialize(t *testing.T) { }, Backend: true, }, - PluginDir: absCurPath, - Class: plugins.Core, + Class: plugins.Core, } i := &Initializer{ @@ -61,8 +56,7 @@ func TestInitializer_Initialize(t *testing.T) { }, Backend: true, }, - PluginDir: absCurPath, - Class: plugins.External, + Class: plugins.External, } i := &Initializer{ @@ -91,8 +85,7 @@ func TestInitializer_Initialize(t *testing.T) { }, Backend: true, }, - PluginDir: absCurPath, - Class: plugins.External, + Class: plugins.External, } i := &Initializer{ diff --git a/pkg/plugins/manager/loader/loader.go b/pkg/plugins/manager/loader/loader.go index 47ca1554c33..937091955a6 100644 --- a/pkg/plugins/manager/loader/loader.go +++ b/pkg/plugins/manager/loader/loader.go @@ -2,16 +2,11 @@ package loader import ( "context" - "encoding/json" "errors" "fmt" - "os" "path" - "path/filepath" - "runtime" "strings" - "github.com/grafana/grafana/pkg/infra/fs" "github.com/grafana/grafana/pkg/infra/metrics" "github.com/grafana/grafana/pkg/infra/slugify" "github.com/grafana/grafana/pkg/plugins" @@ -25,15 +20,9 @@ import ( "github.com/grafana/grafana/pkg/plugins/manager/signature" "github.com/grafana/grafana/pkg/plugins/pluginscdn" "github.com/grafana/grafana/pkg/plugins/storage" - "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/util" ) -var ( - ErrInvalidPluginJSON = errors.New("did not find valid type or id properties in plugin.json") - ErrInvalidPluginJSONFilePath = errors.New("invalid plugin.json filepath was provided") -) - var _ plugins.ErrorResolver = (*Loader)(nil) type Loader struct { @@ -64,7 +53,7 @@ func New(cfg *config.Cfg, license plugins.Licensing, authorizer plugins.PluginLo processManager process.Service, pluginStorage storage.Manager, roleRegistry plugins.RoleRegistry, pluginsCDNService *pluginscdn.Service, assetPath *assetpath.Service) *Loader { return &Loader{ - pluginFinder: finder.New(), + pluginFinder: finder.NewService(), pluginRegistry: pluginRegistry, pluginInitializer: initializer.New(cfg, backendProvider, license), signatureValidator: signature.NewValidator(authorizer), @@ -80,95 +69,65 @@ func New(cfg *config.Cfg, license plugins.Licensing, authorizer plugins.PluginLo } func (l *Loader) Load(ctx context.Context, class plugins.Class, paths []string) ([]*plugins.Plugin, error) { - pluginJSONPaths, err := l.pluginFinder.Find(paths) + found, err := l.pluginFinder.Find(ctx, paths...) if err != nil { return nil, err } - return l.loadPlugins(ctx, class, pluginJSONPaths) + return l.loadPlugins(ctx, class, found) } -func (l *Loader) createPluginsForLoading(class plugins.Class, foundPlugins foundPlugins) map[string]*plugins.Plugin { - loadedPlugins := make(map[string]*plugins.Plugin) - for pluginDir, pluginJSON := range foundPlugins { - plugin, err := l.createPluginBase(pluginJSON, class, pluginDir) - if err != nil { - l.log.Warn("Could not create plugin base", "pluginID", pluginJSON.ID, "err", err) +func (l *Loader) loadPlugins(ctx context.Context, class plugins.Class, found []*plugins.FoundBundle) ([]*plugins.Plugin, error) { + var loadedPlugins []*plugins.Plugin + for _, p := range found { + if _, exists := l.pluginRegistry.Plugin(ctx, p.Primary.JSONData.ID); exists { + l.log.Warn("Skipping plugin loading as it's a duplicate", "pluginID", p.Primary.JSONData.ID) continue } - // calculate initial signature state var sig plugins.Signature - if l.pluginsCDN.PluginSupported(plugin.ID) { + if l.pluginsCDN.PluginSupported(p.Primary.JSONData.ID) { // CDN plugins have no signature checks for now. sig = plugins.Signature{Status: plugins.SignatureValid} } else { - sig, err = signature.Calculate(l.log, plugin) + var err error + sig, err = signature.Calculate(l.log, class, p.Primary) if err != nil { - l.log.Warn("Could not calculate plugin signature state", "pluginID", plugin.ID, "err", err) + l.log.Warn("Could not calculate plugin signature state", "pluginID", p.Primary.JSONData.ID, "err", err) continue } } + plugin, err := l.createPluginBase(p.Primary.JSONData, class, p.Primary.FS) + if err != nil { + l.log.Error("Could not create primary plugin base", "pluginID", p.Primary.JSONData.ID, "err", err) + continue + } + plugin.Signature = sig.Status plugin.SignatureType = sig.Type plugin.SignatureOrg = sig.SigningOrg - loadedPlugins[plugin.PluginDir] = plugin - } - return loadedPlugins -} + loadedPlugins = append(loadedPlugins, plugin) -func (l *Loader) loadPlugins(ctx context.Context, class plugins.Class, pluginJSONPaths []string) ([]*plugins.Plugin, error) { - var foundPlugins = foundPlugins{} - - // load plugin.json files and map directory to JSON data - for _, pluginJSONPath := range pluginJSONPaths { - plugin, err := l.readPluginJSON(pluginJSONPath) - if err != nil { - l.log.Warn("Skipping plugin loading as its plugin.json could not be read", "path", pluginJSONPath, "err", err) - continue - } - - pluginJSONAbsPath, err := filepath.Abs(pluginJSONPath) - if err != nil { - l.log.Warn("Skipping plugin loading as absolute plugin.json path could not be calculated", "pluginID", plugin.ID, "err", err) - continue - } - - if _, dupe := foundPlugins[filepath.Dir(pluginJSONAbsPath)]; dupe { - l.log.Warn("Skipping plugin loading as it's a duplicate", "pluginID", plugin.ID) - continue - } - foundPlugins[filepath.Dir(pluginJSONAbsPath)] = plugin - } - - // get all registered plugins - registeredPlugins := make(map[string]struct{}) - for _, p := range l.pluginRegistry.Plugins(ctx) { - registeredPlugins[p.ID] = struct{}{} - } - - foundPlugins.stripDuplicates(registeredPlugins, l.log) - - // create plugins structs and calculate signatures - loadedPlugins := l.createPluginsForLoading(class, foundPlugins) - - // wire up plugin dependencies - for _, plugin := range loadedPlugins { - ancestors := strings.Split(plugin.PluginDir, string(filepath.Separator)) - ancestors = ancestors[0 : len(ancestors)-1] - pluginPath := "" - - if runtime.GOOS != "windows" && filepath.IsAbs(plugin.PluginDir) { - pluginPath = "/" - } - for _, ancestor := range ancestors { - pluginPath = filepath.Join(pluginPath, ancestor) - if parentPlugin, ok := loadedPlugins[pluginPath]; ok { - plugin.Parent = parentPlugin - plugin.Parent.Children = append(plugin.Parent.Children, plugin) - break + for _, c := range p.Children { + if _, exists := l.pluginRegistry.Plugin(ctx, c.JSONData.ID); exists { + l.log.Warn("Skipping plugin loading as it's a duplicate", "pluginID", p.Primary.JSONData.ID) + continue } + + cp, err := l.createPluginBase(c.JSONData, class, c.FS) + if err != nil { + l.log.Error("Could not create child plugin base", "pluginID", p.Primary.JSONData.ID, "err", err) + continue + } + cp.Parent = plugin + cp.Signature = sig.Status + cp.SignatureType = sig.Type + cp.SignatureOrg = sig.SigningOrg + + plugin.Children = append(plugin.Children, cp) + + loadedPlugins = append(loadedPlugins, cp) } } @@ -191,14 +150,12 @@ func (l *Loader) loadPlugins(ctx context.Context, class plugins.Class, pluginJSO // verify module.js exists for SystemJS to load. // CDN plugins can be loaded with plugin.json only, so do not warn for those. if !plugin.IsRenderer() && !plugin.IsCorePlugin() { - module := filepath.Join(plugin.PluginDir, "module.js") - if exists, err := fs.Exists(module); err != nil { - return nil, err - } else if !exists && !l.pluginsCDN.PluginSupported(plugin.ID) { - l.log.Warn("Plugin missing module.js", - "pluginID", plugin.ID, - "warning", "Missing module.js, If you loaded this plugin from git, make sure to compile it.", - "path", module) + _, err := plugin.FS.Open("module.js") + if err != nil { + if errors.Is(err, plugins.ErrFileNotExist) && !l.pluginsCDN.PluginSupported(plugin.ID) { + l.log.Warn("Plugin missing module.js", "pluginID", plugin.ID, + "warning", "Missing module.js, If you loaded this plugin from git, make sure to compile it.") + } } } @@ -221,7 +178,7 @@ func (l *Loader) loadPlugins(ctx context.Context, class plugins.Class, pluginJSO metrics.SetPluginBuildInformation(p.ID, string(p.Type), p.Info.Version, string(p.Signature)) if errDeclareRoles := l.roleRegistry.DeclarePluginRoles(ctx, p.ID, p.Name, p.Roles); errDeclareRoles != nil { - l.log.Warn("Declare plugin roles failed.", "pluginID", p.ID, "path", p.PluginDir, "error", errDeclareRoles) + l.log.Warn("Declare plugin roles failed.", "pluginID", p.ID, "err", errDeclareRoles) } } @@ -260,7 +217,7 @@ func (l *Loader) load(ctx context.Context, p *plugins.Plugin) error { } if p.IsExternalPlugin() { - if err := l.pluginStorage.Register(ctx, p.ID, p.PluginDir); err != nil { + if err := l.pluginStorage.Register(ctx, p.ID, p.FS.Base()); err != nil { return err } } @@ -271,7 +228,6 @@ func (l *Loader) load(ctx context.Context, p *plugins.Plugin) error { func (l *Loader) unload(ctx context.Context, p *plugins.Plugin) error { l.log.Debug("Stopping plugin process", "pluginId", p.ID) - // TODO confirm the sequence of events is safe if err := l.processManager.Stop(ctx, p.ID); err != nil { return err } @@ -287,70 +243,21 @@ func (l *Loader) unload(ctx context.Context, p *plugins.Plugin) error { return nil } -func (l *Loader) readPluginJSON(pluginJSONPath string) (plugins.JSONData, error) { - l.log.Debug("Loading plugin", "path", pluginJSONPath) - - if !strings.EqualFold(filepath.Ext(pluginJSONPath), ".json") { - return plugins.JSONData{}, ErrInvalidPluginJSONFilePath - } - - // nolint:gosec - // We can ignore the gosec G304 warning on this one because `currentPath` is based - // on plugin the folder structure on disk and not user input. - reader, err := os.Open(pluginJSONPath) - if err != nil { - return plugins.JSONData{}, err - } - - plugin := plugins.JSONData{} - if err = json.NewDecoder(reader).Decode(&plugin); err != nil { - return plugins.JSONData{}, err - } - - if err = reader.Close(); err != nil { - l.log.Warn("Failed to close JSON file", "path", pluginJSONPath, "err", err) - } - - if err = validatePluginJSON(plugin); err != nil { - return plugins.JSONData{}, err - } - - if plugin.ID == "grafana-piechart-panel" { - plugin.Name = "Pie Chart (old)" - } - - if len(plugin.Dependencies.Plugins) == 0 { - plugin.Dependencies.Plugins = []plugins.Dependency{} - } - - if plugin.Dependencies.GrafanaVersion == "" { - plugin.Dependencies.GrafanaVersion = "*" - } - - for _, include := range plugin.Includes { - if include.Role == "" { - include.Role = org.RoleViewer - } - } - - return plugin, nil -} - -func (l *Loader) createPluginBase(pluginJSON plugins.JSONData, class plugins.Class, pluginDir string) (*plugins.Plugin, error) { - baseURL, err := l.assetPath.Base(pluginJSON, class, pluginDir) +func (l *Loader) createPluginBase(pluginJSON plugins.JSONData, class plugins.Class, files plugins.FS) (*plugins.Plugin, error) { + baseURL, err := l.assetPath.Base(pluginJSON, class, files.Base()) if err != nil { return nil, fmt.Errorf("base url: %w", err) } - moduleURL, err := l.assetPath.Module(pluginJSON, class, pluginDir) + moduleURL, err := l.assetPath.Module(pluginJSON, class, files.Base()) if err != nil { return nil, fmt.Errorf("module url: %w", err) } plugin := &plugins.Plugin{ - JSONData: pluginJSON, - PluginDir: pluginDir, - BaseURL: baseURL, - Module: moduleURL, - Class: class, + JSONData: pluginJSON, + FS: files, + BaseURL: baseURL, + Module: moduleURL, + Class: class, } plugin.SetLogger(log.New(fmt.Sprintf("plugin.%s", plugin.ID))) @@ -409,7 +316,7 @@ func configureAppChildPlugin(parent *plugins.Plugin, child *plugins.Plugin) { if !parent.IsApp() { return } - appSubPath := strings.ReplaceAll(strings.Replace(child.PluginDir, parent.PluginDir, "", 1), "\\", "/") + appSubPath := strings.ReplaceAll(strings.Replace(child.FS.Base(), parent.FS.Base(), "", 1), "\\", "/") child.IncludedInAppID = parent.ID child.BaseURL = parent.BaseURL @@ -435,26 +342,3 @@ func (l *Loader) PluginErrors() []*plugins.Error { return errs } - -func validatePluginJSON(data plugins.JSONData) error { - if data.ID == "" || !data.Type.IsValid() { - return ErrInvalidPluginJSON - } - return nil -} - -type foundPlugins map[string]plugins.JSONData - -// stripDuplicates will strip duplicate plugins or plugins that already exist -func (f *foundPlugins) stripDuplicates(existingPlugins map[string]struct{}, log log.Logger) { - pluginsByID := make(map[string]struct{}) - for k, scannedPlugin := range *f { - if _, existing := existingPlugins[scannedPlugin.ID]; existing { - log.Debug("Skipping plugin as it's already installed", "plugin", scannedPlugin.ID) - delete(*f, k) - continue - } - - pluginsByID[scannedPlugin.ID] = struct{}{} - } -} diff --git a/pkg/plugins/manager/loader/loader_test.go b/pkg/plugins/manager/loader/loader_test.go index 3408a3d20dd..887b381bcba 100644 --- a/pkg/plugins/manager/loader/loader_test.go +++ b/pkg/plugins/manager/loader/loader_test.go @@ -2,7 +2,7 @@ package loader import ( "context" - "errors" + "os" "path/filepath" "sort" "testing" @@ -20,11 +20,25 @@ import ( "github.com/grafana/grafana/pkg/plugins/manager/fakes" "github.com/grafana/grafana/pkg/plugins/manager/loader/initializer" "github.com/grafana/grafana/pkg/plugins/manager/signature" - "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/setting" ) -var compareOpts = cmpopts.IgnoreFields(plugins.Plugin{}, "client", "log") +var compareOpts = []cmp.Option{cmpopts.IgnoreFields(plugins.Plugin{}, "client", "log"), localFSComparer} + +var localFSComparer = cmp.Comparer(func(fs1 plugins.LocalFS, fs2 plugins.LocalFS) bool { + fs1Files := fs1.Files() + fs2Files := fs2.Files() + + sort.SliceStable(fs1Files, func(i, j int) bool { + return fs1Files[i] < fs1Files[j] + }) + + sort.SliceStable(fs2Files, func(i, j int) bool { + return fs2Files[i] < fs2Files[j] + }) + + return cmp.Equal(fs1Files, fs2Files) && fs1.Base() == fs2.Base() +}) func TestLoader_Load(t *testing.T) { corePluginDir, err := filepath.Abs("./../../../../public") @@ -86,9 +100,11 @@ func TestLoader_Load(t *testing.T) { Backend: true, QueryOptions: map[string]bool{"minInterval": true}, }, - Module: "app/plugins/datasource/cloudwatch/module", - BaseURL: "public/app/plugins/datasource/cloudwatch", - PluginDir: filepath.Join(corePluginDir, "app/plugins/datasource/cloudwatch"), + Module: "app/plugins/datasource/cloudwatch/module", + BaseURL: "public/app/plugins/datasource/cloudwatch", + FS: plugins.NewLocalFS( + filesInDir(t, filepath.Join(corePluginDir, "app/plugins/datasource/cloudwatch")), + filepath.Join(corePluginDir, "app/plugins/datasource/cloudwatch")), Signature: plugins.SignatureInternal, Class: plugins.Core, }, @@ -125,9 +141,12 @@ func TestLoader_Load(t *testing.T) { Backend: true, State: "alpha", }, - Module: "plugins/test-datasource/module", - BaseURL: "public/plugins/test-datasource", - PluginDir: filepath.Join(parentDir, "testdata/valid-v2-signature/plugin/"), + Module: "plugins/test-datasource/module", + BaseURL: "public/plugins/test-datasource", + FS: plugins.NewLocalFS( + filesInDir(t, filepath.Join(parentDir, "testdata/valid-v2-signature/plugin/")), + filepath.Join(parentDir, "testdata/valid-v2-signature/plugin/"), + ), Signature: "valid", SignatureType: plugins.GrafanaSignature, SignatureOrg: "Grafana Labs", @@ -201,10 +220,20 @@ func TestLoader_Load(t *testing.T) { }, }, }, - Class: plugins.External, - Module: "plugins/test-app/module", - BaseURL: "public/plugins/test-app", - PluginDir: filepath.Join(parentDir, "testdata/includes-symlinks"), + Class: plugins.External, + Module: "plugins/test-app/module", + BaseURL: "public/plugins/test-app", + FS: plugins.NewLocalFS( + map[string]struct{}{ + filepath.Join(parentDir, "testdata/includes-symlinks", "/MANIFEST.txt"): {}, + filepath.Join(parentDir, "testdata/includes-symlinks", "dashboards/connections.json"): {}, + filepath.Join(parentDir, "testdata/includes-symlinks", "dashboards/extra/memory.json"): {}, + filepath.Join(parentDir, "testdata/includes-symlinks", "plugin.json"): {}, + filepath.Join(parentDir, "testdata/includes-symlinks", "symlink_to_txt"): {}, + filepath.Join(parentDir, "testdata/includes-symlinks", "text.txt"): {}, + }, + filepath.Join(parentDir, "testdata/includes-symlinks"), + ), Signature: "valid", SignatureType: plugins.GrafanaSignature, SignatureOrg: "Grafana Labs", @@ -241,10 +270,13 @@ func TestLoader_Load(t *testing.T) { Backend: true, State: plugins.AlphaRelease, }, - Class: plugins.External, - Module: "plugins/test-datasource/module", - BaseURL: "public/plugins/test-datasource", - PluginDir: filepath.Join(parentDir, "testdata/unsigned-datasource/plugin"), + Class: plugins.External, + Module: "plugins/test-datasource/module", + BaseURL: "public/plugins/test-datasource", + FS: plugins.NewLocalFS( + filesInDir(t, filepath.Join(parentDir, "testdata/unsigned-datasource/plugin")), + filepath.Join(parentDir, "testdata/unsigned-datasource/plugin"), + ), Signature: "unsigned", }, }, @@ -292,10 +324,13 @@ func TestLoader_Load(t *testing.T) { Backend: true, State: plugins.AlphaRelease, }, - Class: plugins.External, - Module: "plugins/test-datasource/module", - BaseURL: "public/plugins/test-datasource", - PluginDir: filepath.Join(parentDir, "testdata/unsigned-datasource/plugin"), + Class: plugins.External, + Module: "plugins/test-datasource/module", + BaseURL: "public/plugins/test-datasource", + FS: plugins.NewLocalFS( + filesInDir(t, filepath.Join(parentDir, "testdata/unsigned-datasource/plugin")), + filepath.Join(parentDir, "testdata/unsigned-datasource/plugin"), + ), Signature: plugins.SignatureUnsigned, }, }, @@ -399,11 +434,14 @@ func TestLoader_Load(t *testing.T) { Backend: false, }, DefaultNavURL: "/plugins/test-app/page/root-page-react", - PluginDir: filepath.Join(parentDir, "testdata/test-app-with-includes"), - Class: plugins.External, - Signature: plugins.SignatureUnsigned, - Module: "plugins/test-app/module", - BaseURL: "public/plugins/test-app", + FS: plugins.NewLocalFS(map[string]struct{}{ + filepath.Join(parentDir, "testdata/test-app-with-includes", "dashboards/memory.json"): {}, + filepath.Join(parentDir, "testdata/test-app-with-includes", "plugin.json"): {}, + }, filepath.Join(parentDir, "testdata/test-app-with-includes")), + Class: plugins.External, + Signature: plugins.SignatureUnsigned, + Module: "plugins/test-app/module", + BaseURL: "public/plugins/test-app", }, }, }, @@ -454,7 +492,9 @@ func TestLoader_Load(t *testing.T) { Plugins: []plugins.Dependency{}, }, }, - PluginDir: filepath.Join(parentDir, "testdata/cdn/plugin"), + FS: plugins.NewLocalFS(map[string]struct{}{ + filepath.Join(parentDir, "testdata/cdn/plugin", "plugin.json"): {}, + }, filepath.Join(parentDir, "testdata/cdn/plugin")), Class: plugins.External, Signature: plugins.SignatureValid, BaseURL: "plugin-cdn/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel", @@ -478,8 +518,8 @@ func TestLoader_Load(t *testing.T) { t.Run(tt.name, func(t *testing.T) { got, err := l.Load(context.Background(), tt.class, tt.pluginPaths) require.NoError(t, err) - if !cmp.Equal(got, tt.want, compareOpts) { - t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, tt.want, compareOpts)) + if !cmp.Equal(got, tt.want, compareOpts...) { + t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, tt.want, compareOpts...)) } pluginErrs := l.PluginErrors() @@ -600,10 +640,13 @@ func TestLoader_Load_MultiplePlugins(t *testing.T) { Executable: "test", State: plugins.AlphaRelease, }, - Class: plugins.External, - Module: "plugins/test-datasource/module", - BaseURL: "public/plugins/test-datasource", - PluginDir: filepath.Join(parentDir, "testdata/valid-v2-pvt-signature/plugin"), + Class: plugins.External, + Module: "plugins/test-datasource/module", + BaseURL: "public/plugins/test-datasource", + FS: plugins.NewLocalFS(map[string]struct{}{ + filepath.Join(parentDir, "testdata/valid-v2-pvt-signature/plugin/plugin.json"): {}, + filepath.Join(parentDir, "testdata/valid-v2-pvt-signature/plugin/MANIFEST.txt"): {}, + }, filepath.Join(parentDir, "testdata/valid-v2-pvt-signature/plugin")), Signature: "valid", SignatureType: plugins.PrivateSignature, SignatureOrg: "Will Browne", @@ -641,8 +684,8 @@ func TestLoader_Load_MultiplePlugins(t *testing.T) { sort.SliceStable(got, func(i, j int) bool { return got[i].ID < got[j].ID }) - if !cmp.Equal(got, tt.want, compareOpts) { - t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, tt.want, compareOpts)) + if !cmp.Equal(got, tt.want, compareOpts...) { + t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, tt.want, compareOpts...)) } pluginErrs := l.PluginErrors() require.Equal(t, len(tt.pluginErrors), len(pluginErrs)) @@ -717,7 +760,10 @@ func TestLoader_Load_RBACReady(t *testing.T) { }, Backend: false, }, - PluginDir: pluginDir, + FS: plugins.NewLocalFS(map[string]struct{}{ + filepath.Join(pluginDir, "plugin.json"): {}, + filepath.Join(pluginDir, "MANIFEST.txt"): {}, + }, pluginDir), Class: plugins.External, Signature: plugins.SignatureValid, SignatureType: plugins.PrivateSignature, @@ -749,8 +795,8 @@ func TestLoader_Load_RBACReady(t *testing.T) { got, err := l.Load(context.Background(), plugins.External, tt.pluginPaths) require.NoError(t, err) - if !cmp.Equal(got, tt.want, compareOpts) { - t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, tt.want, compareOpts)) + if !cmp.Equal(got, tt.want, compareOpts...) { + t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, tt.want, compareOpts...)) } pluginErrs := l.PluginErrors() require.Len(t, pluginErrs, 0) @@ -799,7 +845,10 @@ func TestLoader_Load_Signature_RootURL(t *testing.T) { Backend: true, Executable: "test", }, - PluginDir: filepath.Join(parentDir, "/testdata/valid-v2-pvt-signature-root-url-uri/plugin"), + FS: plugins.NewLocalFS(map[string]struct{}{ + filepath.Join(filepath.Join(parentDir, "/testdata/valid-v2-pvt-signature-root-url-uri/plugin"), "plugin.json"): {}, + filepath.Join(filepath.Join(parentDir, "/testdata/valid-v2-pvt-signature-root-url-uri/plugin"), "MANIFEST.txt"): {}, + }, filepath.Join(parentDir, "/testdata/valid-v2-pvt-signature-root-url-uri/plugin")), Class: plugins.External, Signature: plugins.SignatureValid, SignatureType: plugins.PrivateSignature, @@ -822,8 +871,8 @@ func TestLoader_Load_Signature_RootURL(t *testing.T) { got, err := l.Load(context.Background(), plugins.External, paths) require.NoError(t, err) - if !cmp.Equal(got, expected, compareOpts) { - t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, expected, compareOpts)) + if !cmp.Equal(got, expected, compareOpts...) { + t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, expected, compareOpts...)) } verifyState(t, expected, reg, procPrvdr, storage, procMgr) }) @@ -878,7 +927,7 @@ func TestLoader_Load_DuplicatePlugins(t *testing.T) { }, Backend: false, }, - PluginDir: pluginDir, + FS: plugins.NewLocalFS(filesInDir(t, pluginDir), pluginDir), Class: plugins.External, Signature: plugins.SignatureValid, SignatureType: plugins.GrafanaSignature, @@ -901,8 +950,8 @@ func TestLoader_Load_DuplicatePlugins(t *testing.T) { got, err := l.Load(context.Background(), plugins.External, []string{pluginDir, pluginDir}) require.NoError(t, err) - if !cmp.Equal(got, expected, compareOpts) { - t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, expected, compareOpts)) + if !cmp.Equal(got, expected, compareOpts...) { + t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, expected, compareOpts...)) } verifyState(t, expected, reg, procPrvdr, storage, procMgr) @@ -939,9 +988,10 @@ func TestLoader_Load_NestedPlugins(t *testing.T) { }, Backend: true, }, - Module: "plugins/test-datasource/module", - BaseURL: "public/plugins/test-datasource", - PluginDir: filepath.Join(rootDir, "testdata/nested-plugins/parent"), + Module: "plugins/test-datasource/module", + BaseURL: "public/plugins/test-datasource", + FS: plugins.NewLocalFS(filesInDir(t, filepath.Join(rootDir, "testdata/nested-plugins/parent")), + filepath.Join(rootDir, "testdata/nested-plugins/parent")), Signature: plugins.SignatureValid, SignatureType: plugins.GrafanaSignature, SignatureOrg: "Grafana Labs", @@ -971,9 +1021,10 @@ func TestLoader_Load_NestedPlugins(t *testing.T) { Plugins: []plugins.Dependency{}, }, }, - Module: "plugins/test-panel/module", - BaseURL: "public/plugins/test-panel", - PluginDir: filepath.Join(rootDir, "testdata/nested-plugins/parent/nested"), + Module: "plugins/test-panel/module", + BaseURL: "public/plugins/test-panel", + FS: plugins.NewLocalFS(filesInDir(t, filepath.Join(rootDir, "testdata/nested-plugins/parent/nested")), + filepath.Join(rootDir, "testdata/nested-plugins/parent/nested")), Signature: plugins.SignatureValid, SignatureType: plugins.GrafanaSignature, SignatureOrg: "Grafana Labs", @@ -1004,8 +1055,8 @@ func TestLoader_Load_NestedPlugins(t *testing.T) { }) expected := []*plugins.Plugin{parent, child} - if !cmp.Equal(got, expected, compareOpts) { - t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, expected, compareOpts)) + if !cmp.Equal(got, expected, compareOpts...) { + t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, expected, compareOpts...)) } verifyState(t, expected, reg, procPrvdr, storage, procMgr) @@ -1019,8 +1070,8 @@ func TestLoader_Load_NestedPlugins(t *testing.T) { return got[i].ID < got[j].ID }) - if !cmp.Equal(got, []*plugins.Plugin{}, compareOpts) { - t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, expected, compareOpts)) + if !cmp.Equal(got, []*plugins.Plugin{}, compareOpts...) { + t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, expected, compareOpts...)) } verifyState(t, expected, reg, procPrvdr, storage, procMgr) @@ -1098,9 +1149,10 @@ func TestLoader_Load_NestedPlugins(t *testing.T) { }, Backend: false, }, - Module: "plugins/myorgid-simple-app/module", - BaseURL: "public/plugins/myorgid-simple-app", - PluginDir: filepath.Join(rootDir, "testdata/app-with-child/dist"), + Module: "plugins/myorgid-simple-app/module", + BaseURL: "public/plugins/myorgid-simple-app", + FS: plugins.NewLocalFS(filesInDir(t, filepath.Join(rootDir, "testdata/app-with-child/dist")), + filepath.Join(rootDir, "testdata/app-with-child/dist")), DefaultNavURL: "/plugins/myorgid-simple-app/page/root-page-react", Signature: plugins.SignatureValid, SignatureType: plugins.GrafanaSignature, @@ -1136,9 +1188,10 @@ func TestLoader_Load_NestedPlugins(t *testing.T) { Plugins: []plugins.Dependency{}, }, }, - Module: "plugins/myorgid-simple-app/child/module", - BaseURL: "public/plugins/myorgid-simple-app", - PluginDir: filepath.Join(rootDir, "testdata/app-with-child/dist/child"), + Module: "plugins/myorgid-simple-app/child/module", + BaseURL: "public/plugins/myorgid-simple-app", + FS: plugins.NewLocalFS(filesInDir(t, filepath.Join(rootDir, "testdata/app-with-child/dist/child")), + filepath.Join(rootDir, "testdata/app-with-child/dist/child")), IncludedInAppID: parent.ID, Signature: plugins.SignatureValid, SignatureType: plugins.GrafanaSignature, @@ -1168,205 +1221,27 @@ func TestLoader_Load_NestedPlugins(t *testing.T) { return got[i].ID < got[j].ID }) - if !cmp.Equal(got, expected, compareOpts) { - t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, expected, compareOpts)) + if !cmp.Equal(got, expected, compareOpts...) { + t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, expected, compareOpts...)) } verifyState(t, expected, reg, procPrvdr, storage, procMgr) - - t.Run("order of loaded parent and child plugins gives same output", func(t *testing.T) { - parentPluginJSON := filepath.Join(rootDir, "testdata/app-with-child/dist/plugin.json") - childPluginJSON := filepath.Join(rootDir, "testdata/app-with-child/dist/child/plugin.json") - - reg = fakes.NewFakePluginRegistry() - storage = fakes.NewFakePluginStorage() - procPrvdr = fakes.NewFakeBackendProcessProvider() - procMgr = fakes.NewFakeProcessManager() - l = newLoader(&config.Cfg{}, func(l *Loader) { - l.pluginRegistry = reg - l.pluginStorage = storage - l.processManager = procMgr - l.pluginInitializer = initializer.New(&config.Cfg{}, procPrvdr, fakes.NewFakeLicensingService()) - }) - got, err = l.loadPlugins(context.Background(), plugins.External, []string{parentPluginJSON, childPluginJSON}) - require.NoError(t, err) - - // to ensure we can compare with expected - sort.SliceStable(got, func(i, j int) bool { - return got[i].ID < got[j].ID - }) - - if !cmp.Equal(got, expected, compareOpts) { - t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, expected, compareOpts)) - } - - verifyState(t, expected, reg, procPrvdr, storage, procMgr) - - reg = fakes.NewFakePluginRegistry() - storage = fakes.NewFakePluginStorage() - procPrvdr = fakes.NewFakeBackendProcessProvider() - procMgr = fakes.NewFakeProcessManager() - l = newLoader(&config.Cfg{}, func(l *Loader) { - l.pluginRegistry = reg - l.pluginStorage = storage - l.processManager = procMgr - l.pluginInitializer = initializer.New(&config.Cfg{}, procPrvdr, fakes.NewFakeLicensingService()) - }) - got, err = l.loadPlugins(context.Background(), plugins.External, []string{childPluginJSON, parentPluginJSON}) - require.NoError(t, err) - - // to ensure we can compare with expected - sort.SliceStable(got, func(i, j int) bool { - return got[i].ID < got[j].ID - }) - - if !cmp.Equal(got, expected, compareOpts) { - t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, expected, compareOpts)) - } - - verifyState(t, expected, reg, procPrvdr, storage, procMgr) - }) }) } -func TestLoader_readPluginJSON(t *testing.T) { - tests := []struct { - name string - pluginPath string - expected plugins.JSONData - failed bool - }{ - { - name: "Valid plugin", - pluginPath: "../testdata/test-app/plugin.json", - expected: plugins.JSONData{ - ID: "test-app", - Type: "app", - Name: "Test App", - Info: plugins.Info{ - Author: plugins.InfoLink{ - Name: "Test Inc.", - URL: "http://test.com", - }, - Description: "Official Grafana Test App & Dashboard bundle", - Version: "1.0.0", - Links: []plugins.InfoLink{ - {Name: "Project site", URL: "http://project.com"}, - {Name: "License & Terms", URL: "http://license.com"}, - }, - Logos: plugins.Logos{ - Small: "img/logo_small.png", - Large: "img/logo_large.png", - }, - Screenshots: []plugins.Screenshots{ - {Path: "img/screenshot1.png", Name: "img1"}, - {Path: "img/screenshot2.png", Name: "img2"}, - }, - Updated: "2015-02-10", - }, - Dependencies: plugins.Dependencies{ - GrafanaVersion: "3.x.x", - Plugins: []plugins.Dependency{ - {Type: "datasource", ID: "graphite", Name: "Graphite", Version: "1.0.0"}, - {Type: "panel", ID: "graph", Name: "Graph", Version: "1.0.0"}, - }, - }, - Includes: []*plugins.Includes{ - {Name: "Nginx Connections", Path: "dashboards/connections.json", Type: "dashboard", Role: org.RoleViewer}, - {Name: "Nginx Memory", Path: "dashboards/memory.json", Type: "dashboard", Role: org.RoleViewer}, - {Name: "Nginx Panel", Type: "panel", Role: org.RoleViewer}, - {Name: "Nginx Datasource", Type: "datasource", Role: org.RoleViewer}, - }, - Backend: false, - }, - }, - { - name: "Invalid plugin JSON", - pluginPath: "../testdata/invalid-plugin-json/plugin.json", - failed: true, - }, - { - name: "Non-existing JSON file", - pluginPath: "nonExistingFile.json", - failed: true, - }, - } - - l := newLoader(nil) - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := l.readPluginJSON(tt.pluginPath) - if (err != nil) && !tt.failed { - t.Errorf("readPluginJSON() error = %v, failed %v", err, tt.failed) - return - } - if !cmp.Equal(got, tt.expected, compareOpts) { - t.Errorf("Unexpected pluginJSONData: %v", cmp.Diff(got, tt.expected, compareOpts)) - } - }) - } -} - -func Test_validatePluginJSON(t *testing.T) { - type args struct { - data plugins.JSONData - } - tests := []struct { - name string - args args - err error - }{ - { - name: "Valid case", - args: args{ - data: plugins.JSONData{ - ID: "grafana-plugin-id", - Type: plugins.DataSource, - }, - }, - }, - { - name: "Invalid plugin ID", - args: args{ - data: plugins.JSONData{ - Type: plugins.Panel, - }, - }, - err: ErrInvalidPluginJSON, - }, - { - name: "Invalid plugin type", - args: args{ - data: plugins.JSONData{ - ID: "grafana-plugin-id", - Type: "test", - }, - }, - err: ErrInvalidPluginJSON, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if err := validatePluginJSON(tt.args.data); !errors.Is(err, tt.err) { - t.Errorf("validatePluginJSON() = %v, want %v", err, tt.err) - } - }) - } -} - func Test_setPathsBasedOnApp(t *testing.T) { t.Run("When setting paths based on core plugin on Windows", func(t *testing.T) { child := &plugins.Plugin{ - PluginDir: "c:\\grafana\\public\\app\\plugins\\app\\testdata-app\\datasources\\datasource", + FS: fakes.NewFakePluginFiles("c:\\grafana\\public\\app\\plugins\\app\\testdata-app\\datasources\\datasource"), } parent := &plugins.Plugin{ JSONData: plugins.JSONData{ Type: plugins.App, ID: "testdata-app", }, - Class: plugins.Core, - PluginDir: "c:\\grafana\\public\\app\\plugins\\app\\testdata-app", - BaseURL: "public/app/plugins/app/testdata-app", + Class: plugins.Core, + FS: fakes.NewFakePluginFiles("c:\\grafana\\public\\app\\plugins\\app\\testdata-app"), + BaseURL: "public/app/plugins/app/testdata-app", } configureAppChildPlugin(parent, child) @@ -1395,8 +1270,8 @@ func verifyState(t *testing.T, ps []*plugins.Plugin, reg *fakes.FakePluginRegist t.Helper() for _, p := range ps { - if !cmp.Equal(p, reg.Store[p.ID], compareOpts) { - t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(p, reg.Store[p.ID], compareOpts)) + if !cmp.Equal(p, reg.Store[p.ID], compareOpts...) { + t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(p, reg.Store[p.ID], compareOpts...)) } if p.Backend { @@ -1418,3 +1293,40 @@ func verifyState(t *testing.T, ps []*plugins.Plugin, reg *fakes.FakePluginRegist require.Zero(t, procMngr.Stopped[p.ID]) } } + +func filesInDir(t *testing.T, dir string) map[string]struct{} { + files, err := collectFilesWithin(dir) + if err != nil { + t.Logf("Could not collect plugin file info. Err: %v", err) + return map[string]struct{}{} + } + return files +} + +func collectFilesWithin(dir string) (map[string]struct{}, error) { + files := map[string]struct{}{} + err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + // skip directories + if info.IsDir() { + return nil + } + + // verify that file is within plugin directory + //file, err := filepath.Rel(dir, path) + //if err != nil { + // return err + //} + //if strings.HasPrefix(file, ".."+string(filepath.Separator)) { + // return fmt.Errorf("file '%s' not inside of plugin directory", file) + //} + + files[path] = struct{}{} + return nil + }) + + return files, err +} diff --git a/pkg/plugins/manager/manager_integration_test.go b/pkg/plugins/manager/manager_integration_test.go index acd410f8568..37280ffc030 100644 --- a/pkg/plugins/manager/manager_integration_test.go +++ b/pkg/plugins/manager/manager_integration_test.go @@ -4,13 +4,9 @@ import ( "context" "encoding/json" "path/filepath" - "strings" "testing" "time" - "github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath" - "github.com/grafana/grafana/pkg/plugins/pluginscdn" - "github.com/grafana/grafana-azure-sdk-go/azsettings" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" @@ -27,10 +23,12 @@ import ( "github.com/grafana/grafana/pkg/plugins/manager/client" "github.com/grafana/grafana/pkg/plugins/manager/fakes" "github.com/grafana/grafana/pkg/plugins/manager/loader" + "github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath" "github.com/grafana/grafana/pkg/plugins/manager/registry" "github.com/grafana/grafana/pkg/plugins/manager/signature" "github.com/grafana/grafana/pkg/plugins/manager/sources" "github.com/grafana/grafana/pkg/plugins/manager/store" + "github.com/grafana/grafana/pkg/plugins/pluginscdn" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/licensing" "github.com/grafana/grafana/pkg/services/searchV2" @@ -126,7 +124,7 @@ func TestIntegrationPluginManager(t *testing.T) { ctx := context.Background() verifyCorePluginCatalogue(t, ctx, ps) - verifyBundledPlugins(t, ctx, ps, reg) + verifyBundledPlugins(t, ctx, ps) verifyPluginStaticRoutes(t, ctx, ps, reg) verifyBackendProcesses(t, reg.Plugins(ctx)) verifyPluginQuery(t, ctx, client.ProvideService(reg, pCfg)) @@ -255,7 +253,7 @@ func verifyCorePluginCatalogue(t *testing.T, ctx context.Context, ps *store.Serv require.Equal(t, len(expPanels)+len(expDataSources)+len(expApps), len(ps.Plugins(ctx))) } -func verifyBundledPlugins(t *testing.T, ctx context.Context, ps *store.Service, reg registry.Service) { +func verifyBundledPlugins(t *testing.T, ctx context.Context, ps *store.Service) { t.Helper() dsPlugins := make(map[string]struct{}) @@ -268,9 +266,6 @@ func verifyBundledPlugins(t *testing.T, ctx context.Context, ps *store.Service, require.NotEqual(t, plugins.PluginDTO{}, inputPlugin) require.NotNil(t, dsPlugins["input"]) - intInputPlugin, exists := reg.Plugin(ctx, "input") - require.True(t, exists) - pluginRoutes := make(map[string]*plugins.StaticRoute) for _, r := range ps.Routes() { pluginRoutes[r.PluginID] = r @@ -278,7 +273,7 @@ func verifyBundledPlugins(t *testing.T, ctx context.Context, ps *store.Service, for _, pluginID := range []string{"input"} { require.Contains(t, pluginRoutes, pluginID) - require.True(t, strings.HasPrefix(pluginRoutes[pluginID].Directory, intInputPlugin.PluginDir)) + require.Equal(t, pluginRoutes[pluginID].Directory, inputPlugin.Base()) } } @@ -292,11 +287,11 @@ func verifyPluginStaticRoutes(t *testing.T, ctx context.Context, rr plugins.Stat inputPlugin, _ := reg.Plugin(ctx, "input") require.NotNil(t, routes["input"]) - require.Equal(t, routes["input"].Directory, inputPlugin.PluginDir) + require.Equal(t, routes["input"].Directory, inputPlugin.FS.Base()) testAppPlugin, _ := reg.Plugin(ctx, "test-app") require.Contains(t, routes, "test-app") - require.Equal(t, routes["test-app"].Directory, testAppPlugin.PluginDir) + require.Equal(t, routes["test-app"].Directory, testAppPlugin.FS.Base()) } func verifyBackendProcesses(t *testing.T, ps []*plugins.Plugin) { diff --git a/pkg/plugins/manager/signature/manifest.go b/pkg/plugins/manager/signature/manifest.go index a81216b2184..82061863827 100644 --- a/pkg/plugins/manager/signature/manifest.go +++ b/pkg/plugins/manager/signature/manifest.go @@ -11,7 +11,6 @@ import ( "net/url" "os" "path" - "path/filepath" "runtime" "strings" @@ -57,8 +56,8 @@ N1c5v9v/4h6qeA== var runningWindows = runtime.GOOS == "windows" -// pluginManifest holds details for the file manifest -type pluginManifest struct { +// PluginManifest holds details for the file manifest +type PluginManifest struct { Plugin string `json:"plugin"` Version string `json:"version"` KeyID string `json:"keyId"` @@ -73,20 +72,20 @@ type pluginManifest struct { RootURLs []string `json:"rootUrls"` } -func (m *pluginManifest) isV2() bool { +func (m *PluginManifest) isV2() bool { return strings.HasPrefix(m.ManifestVersion, "2.") } // readPluginManifest attempts to read and verify the plugin manifest // if any error occurs or the manifest is not valid, this will return an error -func readPluginManifest(body []byte) (*pluginManifest, error) { +func ReadPluginManifest(body []byte) (*PluginManifest, error) { block, _ := clearsign.Decode(body) if block == nil { return nil, errors.New("unable to decode manifest") } // Convert to a well typed object - var manifest pluginManifest + var manifest PluginManifest err := json.Unmarshal(block.Plaintext, &manifest) if err != nil { return nil, fmt.Errorf("%v: %w", "Error parsing manifest JSON", err) @@ -99,32 +98,54 @@ func readPluginManifest(body []byte) (*pluginManifest, error) { return &manifest, nil } -func Calculate(mlog log.Logger, plugin *plugins.Plugin) (plugins.Signature, error) { - if plugin.IsCorePlugin() { +func Calculate(mlog log.Logger, class plugins.Class, plugin plugins.FoundPlugin) (plugins.Signature, error) { + if class == plugins.Core { return plugins.Signature{ Status: plugins.SignatureInternal, }, nil } - pluginFiles, err := pluginFilesRequiringVerification(plugin) - if err != nil { - mlog.Warn("Could not collect plugin file information in directory", "pluginID", plugin.ID, "dir", plugin.PluginDir) + if len(plugin.FS.Files()) == 0 { + mlog.Warn("No plugin file information in directory", "pluginID", plugin.JSONData.ID) return plugins.Signature{ Status: plugins.SignatureInvalid, - }, err + }, nil } - byteValue := plugin.Manifest() + f, err := plugin.FS.Open("MANIFEST.txt") + if err != nil { + if errors.Is(err, plugins.ErrFileNotExist) { + mlog.Debug("Could not find a MANIFEST.txt", "id", plugin.JSONData.ID, "err", err) + return plugins.Signature{ + Status: plugins.SignatureUnsigned, + }, nil + } + + mlog.Debug("Could not open MANIFEST.txt", "id", plugin.JSONData.ID, "err", err) + return plugins.Signature{ + Status: plugins.SignatureInvalid, + }, nil + } + defer func() { + if f == nil { + return + } + if err = f.Close(); err != nil { + mlog.Warn("Failed to close plugin MANIFEST file", "err", err) + } + }() + + byteValue, err := io.ReadAll(f) if err != nil || len(byteValue) < 10 { - mlog.Debug("Plugin is unsigned", "id", plugin.ID) + mlog.Debug("MANIFEST.TXT is invalid", "id", plugin.JSONData.ID) return plugins.Signature{ Status: plugins.SignatureUnsigned, }, nil } - manifest, err := readPluginManifest(byteValue) + manifest, err := ReadPluginManifest(byteValue) if err != nil { - mlog.Debug("Plugin signature invalid", "id", plugin.ID, "err", err) + mlog.Debug("Plugin signature invalid", "id", plugin.JSONData.ID, "err", err) return plugins.Signature{ Status: plugins.SignatureInvalid, }, nil @@ -137,7 +158,7 @@ func Calculate(mlog log.Logger, plugin *plugins.Plugin) (plugins.Signature, erro } // Make sure the versions all match - if manifest.Plugin != plugin.ID || manifest.Version != plugin.Info.Version { + if manifest.Plugin != plugin.JSONData.ID || manifest.Version != plugin.JSONData.Info.Version { return plugins.Signature{ Status: plugins.SignatureModified, }, nil @@ -146,10 +167,10 @@ func Calculate(mlog log.Logger, plugin *plugins.Plugin) (plugins.Signature, erro // Validate that plugin is running within defined root URLs if len(manifest.RootURLs) > 0 { if match, err := urlMatch(manifest.RootURLs, setting.AppUrl, manifest.SignatureType); err != nil { - mlog.Warn("Could not verify if root URLs match", "plugin", plugin.ID, "rootUrls", manifest.RootURLs) + mlog.Warn("Could not verify if root URLs match", "plugin", plugin.JSONData.ID, "rootUrls", manifest.RootURLs) return plugins.Signature{}, err } else if !match { - mlog.Warn("Could not find root URL that matches running application URL", "plugin", plugin.ID, + mlog.Warn("Could not find root URL that matches running application URL", "plugin", plugin.JSONData.ID, "appUrl", setting.AppUrl, "rootUrls", manifest.RootURLs) return plugins.Signature{ Status: plugins.SignatureInvalid, @@ -161,7 +182,7 @@ func Calculate(mlog log.Logger, plugin *plugins.Plugin) (plugins.Signature, erro // Verify the manifest contents for p, hash := range manifest.Files { - err = verifyHash(mlog, plugin.ID, filepath.Join(plugin.PluginDir, p), hash) + err = verifyHash(mlog, plugin, p, hash) if err != nil { return plugins.Signature{ Status: plugins.SignatureModified, @@ -173,20 +194,28 @@ func Calculate(mlog log.Logger, plugin *plugins.Plugin) (plugins.Signature, erro // Track files missing from the manifest var unsignedFiles []string - for _, f := range pluginFiles { + for _, f := range plugin.FS.Files() { + // Ignoring unsigned Chromium debug.log so it doesn't invalidate the signature for Renderer plugin running on Windows + if runningWindows && plugin.JSONData.Type == plugins.Renderer && f == "chrome-win/debug.log" { + continue + } + + if f == "MANIFEST.txt" { + continue + } if _, exists := manifestFiles[f]; !exists { unsignedFiles = append(unsignedFiles, f) } } if len(unsignedFiles) > 0 { - mlog.Warn("The following files were not included in the signature", "plugin", plugin.ID, "files", unsignedFiles) + mlog.Warn("The following files were not included in the signature", "plugin", plugin.JSONData.ID, "files", unsignedFiles) return plugins.Signature{ Status: plugins.SignatureModified, }, nil } - mlog.Debug("Plugin signature valid", "id", plugin.ID) + mlog.Debug("Plugin signature valid", "id", plugin.JSONData.ID) return plugins.Signature{ Status: plugins.SignatureValid, Type: manifest.SignatureType, @@ -194,17 +223,17 @@ func Calculate(mlog log.Logger, plugin *plugins.Plugin) (plugins.Signature, erro }, nil } -func verifyHash(mlog log.Logger, pluginID string, path string, hash string) error { +func verifyHash(mlog log.Logger, plugin plugins.FoundPlugin, path, hash string) error { // nolint:gosec // We can ignore the gosec G304 warning on this one because `path` is based // on the path provided in a manifest file for a plugin and not user input. - f, err := os.Open(path) + f, err := plugin.FS.Open(path) if err != nil { if os.IsPermission(err) { - mlog.Warn("Could not open plugin file due to lack of permissions", "plugin", pluginID, "path", path) + mlog.Warn("Could not open plugin file due to lack of permissions", "plugin", plugin.JSONData.ID, "path", path) return errors.New("permission denied when attempting to read plugin file") } - mlog.Warn("Plugin file listed in the manifest was not found", "plugin", pluginID, "path", path) + mlog.Warn("Plugin file listed in the manifest was not found", "plugin", plugin.JSONData.ID, "path", path) return errors.New("plugin file listed in the manifest was not found") } defer func() { @@ -219,75 +248,13 @@ func verifyHash(mlog log.Logger, pluginID string, path string, hash string) erro } sum := hex.EncodeToString(h.Sum(nil)) if sum != hash { - mlog.Warn("Plugin file checksum does not match signature checksum", "plugin", pluginID, "path", path) + mlog.Warn("Plugin file checksum does not match signature checksum", "plugin", plugin.JSONData.ID, "path", path) return errors.New("plugin file checksum does not match signature checksum") } return nil } -// pluginFilesRequiringVerification gets plugin filenames that require verification for plugin signing -// returns filenames as a slice of posix style paths relative to plugin directory -func pluginFilesRequiringVerification(plugin *plugins.Plugin) ([]string, error) { - var files []string - err := filepath.Walk(plugin.PluginDir, func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - - if info.Mode()&os.ModeSymlink == os.ModeSymlink { - symlinkPath, err := filepath.EvalSymlinks(path) - if err != nil { - return err - } - - symlink, err := os.Stat(symlinkPath) - if err != nil { - return err - } - - // verify that symlinked file is within plugin directory - p, err := filepath.Rel(plugin.PluginDir, symlinkPath) - if err != nil { - return err - } - if p == ".." || strings.HasPrefix(p, ".."+string(filepath.Separator)) { - return fmt.Errorf("file '%s' not inside of plugin directory", p) - } - - // skip adding symlinked directories - if symlink.IsDir() { - return nil - } - } - - // skip directories and MANIFEST.txt - if info.IsDir() || info.Name() == "MANIFEST.txt" { - return nil - } - - // Ignoring unsigned Chromium debug.log so it doesn't invalidate the signature for Renderer plugin running on Windows - if runningWindows && plugin.IsRenderer() && strings.HasSuffix(path, filepath.Join("chrome-win", "debug.log")) { - return nil - } - - // verify that file is within plugin directory - file, err := filepath.Rel(plugin.PluginDir, path) - if err != nil { - return err - } - if strings.HasPrefix(file, ".."+string(filepath.Separator)) { - return fmt.Errorf("file '%s' not inside of plugin directory", file) - } - - files = append(files, filepath.ToSlash(file)) - - return nil - }) - - return files, err -} - func urlMatch(specs []string, target string, signatureType plugins.SignatureType) (bool, error) { targetURL, err := url.Parse(target) if err != nil { @@ -328,7 +295,7 @@ func (r invalidFieldErr) Error() string { return fmt.Sprintf("valid manifest field %s is required", r.field) } -func validateManifest(m pluginManifest, block *clearsign.Block) error { +func validateManifest(m PluginManifest, block *clearsign.Block) error { if len(m.Plugin) == 0 { return invalidFieldErr{field: "plugin"} } diff --git a/pkg/plugins/manager/signature/manifest_test.go b/pkg/plugins/manager/signature/manifest_test.go index a0591051aa1..de257b59ed5 100644 --- a/pkg/plugins/manager/signature/manifest_test.go +++ b/pkg/plugins/manager/signature/manifest_test.go @@ -46,7 +46,7 @@ NR7DnB0CCQHO+4FlSPtXFTzNepoc+CytQyDAeOLMLmf2Tqhk2YShk+G/YlVX -----END PGP SIGNATURE-----` t.Run("valid manifest", func(t *testing.T) { - manifest, err := readPluginManifest([]byte(txt)) + manifest, err := ReadPluginManifest([]byte(txt)) require.NoError(t, err) require.NotNil(t, manifest) @@ -62,7 +62,7 @@ NR7DnB0CCQHO+4FlSPtXFTzNepoc+CytQyDAeOLMLmf2Tqhk2YShk+G/YlVX t.Run("invalid manifest", func(t *testing.T) { modified := strings.ReplaceAll(txt, "README.md", "xxxxxxxxxx") - _, err := readPluginManifest([]byte(modified)) + _, err := ReadPluginManifest([]byte(modified)) require.Error(t, err) }) } @@ -99,7 +99,7 @@ khdr/tZ1PDgRxMqB/u+Vtbpl0xSxgblnrDOYMSI= -----END PGP SIGNATURE-----` t.Run("valid manifest", func(t *testing.T) { - manifest, err := readPluginManifest([]byte(txt)) + manifest, err := ReadPluginManifest([]byte(txt)) require.NoError(t, err) require.NotNil(t, manifest) @@ -151,15 +151,18 @@ func TestCalculate(t *testing.T) { }) setting.AppUrl = tc.appURL - sig, err := Calculate(log.NewTestLogger(), &plugins.Plugin{ + basePath := filepath.Join(parentDir, "testdata/non-pvt-with-root-url/plugin") + sig, err := Calculate(log.NewTestLogger(), plugins.External, plugins.FoundPlugin{ JSONData: plugins.JSONData{ ID: "test-datasource", Info: plugins.Info{ Version: "1.0.0", }, }, - PluginDir: filepath.Join(parentDir, "testdata/non-pvt-with-root-url/plugin"), - Class: plugins.External, + FS: plugins.NewLocalFS(map[string]struct{}{ + filepath.Join(basePath, "MANIFEST.txt"): {}, + filepath.Join(basePath, "plugin.json"): {}, + }, basePath), }) require.NoError(t, err) require.Equal(t, tc.expectedSignature, sig) @@ -172,8 +175,10 @@ func TestCalculate(t *testing.T) { runningWindows = backup }) + basePath := "../testdata/renderer-added-file/plugin" + runningWindows = true - sig, err := Calculate(log.NewTestLogger(), &plugins.Plugin{ + sig, err := Calculate(log.NewTestLogger(), plugins.External, plugins.FoundPlugin{ JSONData: plugins.JSONData{ ID: "test-renderer", Type: plugins.Renderer, @@ -181,7 +186,11 @@ func TestCalculate(t *testing.T) { Version: "1.0.0", }, }, - PluginDir: "../testdata/renderer-added-file/plugin", + FS: plugins.NewLocalFS(map[string]struct{}{ + filepath.Join(basePath, "MANIFEST.txt"): {}, + filepath.Join(basePath, "plugin.json"): {}, + filepath.Join(basePath, "chrome-win/debug.log"): {}, + }, basePath), }) require.NoError(t, err) require.Equal(t, plugins.Signature{ @@ -192,7 +201,7 @@ func TestCalculate(t *testing.T) { }) } -func fileList(manifest *pluginManifest) []string { +func fileList(manifest *PluginManifest) []string { var keys []string for k := range manifest.Files { keys = append(keys, k) @@ -476,52 +485,52 @@ func Test_urlMatch_private(t *testing.T) { func Test_validateManifest(t *testing.T) { tcs := []struct { name string - manifest *pluginManifest + manifest *PluginManifest expectedErr string }{ { name: "Empty plugin field", - manifest: createV2Manifest(t, func(m *pluginManifest) { m.Plugin = "" }), + manifest: createV2Manifest(t, func(m *PluginManifest) { m.Plugin = "" }), expectedErr: "valid manifest field plugin is required", }, { name: "Empty keyId field", - manifest: createV2Manifest(t, func(m *pluginManifest) { m.KeyID = "" }), + manifest: createV2Manifest(t, func(m *PluginManifest) { m.KeyID = "" }), expectedErr: "valid manifest field keyId is required", }, { name: "Empty signedByOrg field", - manifest: createV2Manifest(t, func(m *pluginManifest) { m.SignedByOrg = "" }), + manifest: createV2Manifest(t, func(m *PluginManifest) { m.SignedByOrg = "" }), expectedErr: "valid manifest field signedByOrg is required", }, { name: "Empty signedByOrgName field", - manifest: createV2Manifest(t, func(m *pluginManifest) { m.SignedByOrgName = "" }), + manifest: createV2Manifest(t, func(m *PluginManifest) { m.SignedByOrgName = "" }), expectedErr: "valid manifest field SignedByOrgName is required", }, { name: "Empty signatureType field", - manifest: createV2Manifest(t, func(m *pluginManifest) { m.SignatureType = "" }), + manifest: createV2Manifest(t, func(m *PluginManifest) { m.SignatureType = "" }), expectedErr: "valid manifest field signatureType is required", }, { name: "Invalid signatureType field", - manifest: createV2Manifest(t, func(m *pluginManifest) { m.SignatureType = "invalidSignatureType" }), + manifest: createV2Manifest(t, func(m *PluginManifest) { m.SignatureType = "invalidSignatureType" }), expectedErr: "valid manifest field signatureType is required", }, { name: "Empty files field", - manifest: createV2Manifest(t, func(m *pluginManifest) { m.Files = map[string]string{} }), + manifest: createV2Manifest(t, func(m *PluginManifest) { m.Files = map[string]string{} }), expectedErr: "valid manifest field files is required", }, { name: "Empty time field", - manifest: createV2Manifest(t, func(m *pluginManifest) { m.Time = 0 }), + manifest: createV2Manifest(t, func(m *PluginManifest) { m.Time = 0 }), expectedErr: "valid manifest field time is required", }, { name: "Empty version field", - manifest: createV2Manifest(t, func(m *pluginManifest) { m.Version = "" }), + manifest: createV2Manifest(t, func(m *PluginManifest) { m.Version = "" }), expectedErr: "valid manifest field version is required", }, } @@ -533,10 +542,10 @@ func Test_validateManifest(t *testing.T) { } } -func createV2Manifest(t *testing.T, cbs ...func(*pluginManifest)) *pluginManifest { +func createV2Manifest(t *testing.T, cbs ...func(*PluginManifest)) *PluginManifest { t.Helper() - m := &pluginManifest{ + m := &PluginManifest{ Plugin: "grafana-test-app", Version: "2.5.3", KeyID: "7e4d0c6a708866e7", diff --git a/pkg/plugins/manager/sources/sources.go b/pkg/plugins/manager/sources/sources.go index 1e228ee45b0..f4c6d37a2c0 100644 --- a/pkg/plugins/manager/sources/sources.go +++ b/pkg/plugins/manager/sources/sources.go @@ -4,9 +4,9 @@ import ( "context" "path/filepath" - "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/config" + "github.com/grafana/grafana/pkg/plugins/log" "github.com/grafana/grafana/pkg/setting" ) diff --git a/pkg/plugins/manager/store/store.go b/pkg/plugins/manager/store/store.go index 872a70a4866..e4f5f6bb58e 100644 --- a/pkg/plugins/manager/store/store.go +++ b/pkg/plugins/manager/store/store.go @@ -18,8 +18,9 @@ type Service struct { func ProvideService(pluginRegistry registry.Service, pluginSources sources.Resolver, pluginLoader loader.Service) (*Service, error) { - for _, ps := range pluginSources.List(context.Background()) { - if _, err := pluginLoader.Load(context.Background(), ps.Class, ps.Paths); err != nil { + ctx := context.Background() + for _, ps := range pluginSources.List(ctx) { + if _, err := pluginLoader.Load(ctx, ps.Class, ps.Paths); err != nil { return nil, err } } diff --git a/pkg/plugins/manager/store/store_test.go b/pkg/plugins/manager/store/store_test.go index 906c7db7e68..f992b0b4e46 100644 --- a/pkg/plugins/manager/store/store_test.go +++ b/pkg/plugins/manager/store/store_test.go @@ -97,11 +97,11 @@ func TestStore_Plugins(t *testing.T) { func TestStore_Routes(t *testing.T) { t.Run("Routes returns all static routes for non-decommissioned plugins", func(t *testing.T) { - p1 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "a-test-renderer", Type: plugins.Renderer}, PluginDir: "/some/dir"} - p2 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "b-test-panel", Type: plugins.Panel}, PluginDir: "/grafana/"} - p3 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "c-test-secrets", Type: plugins.SecretsManager}, PluginDir: "./secrets", Class: plugins.Core} - p4 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "d-test-datasource", Type: plugins.DataSource}, PluginDir: "../test"} - p5 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "e-test-app", Type: plugins.App}} + p1 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "a-test-renderer", Type: plugins.Renderer}, FS: fakes.NewFakePluginFiles("/some/dir")} + p2 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "b-test-panel", Type: plugins.Panel}, FS: fakes.NewFakePluginFiles("/grafana/")} + p3 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "c-test-secrets", Type: plugins.SecretsManager}, FS: fakes.NewFakePluginFiles("./secrets"), Class: plugins.Core} + p4 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "d-test-datasource", Type: plugins.DataSource}, FS: fakes.NewFakePluginFiles("../test")} + p5 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "e-test-app", Type: plugins.App}, FS: fakes.NewFakePluginFiles("any/path")} p6 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "f-test-app", Type: plugins.App}} p6.RegisterClient(&DecommissionedPlugin{}) @@ -115,7 +115,7 @@ func TestStore_Routes(t *testing.T) { })) sr := func(p *plugins.Plugin) *plugins.StaticRoute { - return &plugins.StaticRoute{PluginID: p.ID, Directory: p.PluginDir} + return &plugins.StaticRoute{PluginID: p.ID, Directory: p.FS.Base()} } rs := ps.Routes() diff --git a/pkg/plugins/plugins.go b/pkg/plugins/plugins.go index ff389891a52..cb6310a3851 100644 --- a/pkg/plugins/plugins.go +++ b/pkg/plugins/plugins.go @@ -6,8 +6,7 @@ import ( "errors" "fmt" "io/fs" - "os" - "path/filepath" + "path" "runtime" "strings" @@ -26,8 +25,8 @@ var ErrFileNotExist = errors.New("file does not exist") type Plugin struct { JSONData - PluginDir string - Class Class + FS FS + Class Class // App fields IncludedInAppID string @@ -55,8 +54,8 @@ type Plugin struct { type PluginDTO struct { JSONData + fs FS logger log.Logger - pluginDir string supportsStreaming bool Class Class @@ -81,6 +80,10 @@ func (p PluginDTO) SupportsStreaming() bool { return p.supportsStreaming } +func (p PluginDTO) Base() string { + return p.fs.Base() +} + func (p PluginDTO) IsApp() bool { return p.Type == App } @@ -96,21 +99,15 @@ func (p PluginDTO) File(name string) (fs.File, error) { return nil, err } - absPluginDir, err := filepath.Abs(p.pluginDir) + if p.fs == nil { + return nil, ErrFileNotExist + } + + f, err := p.fs.Open(cleanPath) if err != nil { return nil, err } - absFilePath := filepath.Join(absPluginDir, cleanPath) - // Wrapping in filepath.Clean to properly handle - // gosec G304 Potential file inclusion via variable rule. - f, err := os.Open(filepath.Clean(absFilePath)) - if err != nil { - if os.IsNotExist(err) { - return nil, ErrFileNotExist - } - return nil, err - } return f, nil } @@ -337,6 +334,18 @@ func (p *Plugin) Client() (PluginClient, bool) { } func (p *Plugin) ExecutablePath() string { + if p.IsRenderer() { + return p.executablePath("plugin_start") + } + + if p.IsSecretsManager() { + return p.executablePath("secrets_plugin_start") + } + + return p.executablePath(p.Executable) +} + +func (p *Plugin) executablePath(f string) string { os := strings.ToLower(runtime.GOOS) arch := runtime.GOARCH extension := "" @@ -344,15 +353,7 @@ func (p *Plugin) ExecutablePath() string { if os == "windows" { extension = ".exe" } - if p.IsRenderer() { - return filepath.Join(p.PluginDir, fmt.Sprintf("%s_%s_%s%s", "plugin_start", os, strings.ToLower(arch), extension)) - } - - if p.IsSecretsManager() { - return filepath.Join(p.PluginDir, fmt.Sprintf("%s_%s_%s%s", "secrets_plugin_start", os, strings.ToLower(arch), extension)) - } - - return filepath.Join(p.PluginDir, fmt.Sprintf("%s_%s_%s%s", p.Executable, os, strings.ToLower(arch), extension)) + return path.Join(p.FS.Base(), fmt.Sprintf("%s_%s_%s%s", f, os, strings.ToLower(arch), extension)) } type PluginClient interface { @@ -366,9 +367,10 @@ type PluginClient interface { func (p *Plugin) ToDTO() PluginDTO { return PluginDTO{ logger: p.Logger(), - pluginDir: p.PluginDir, - JSONData: p.JSONData, + fs: p.FS, + supportsStreaming: p.client != nil && p.client.(backend.StreamHandler) != nil, Class: p.Class, + JSONData: p.JSONData, IncludedInAppID: p.IncludedInAppID, DefaultNavURL: p.DefaultNavURL, Pinned: p.Pinned, @@ -378,7 +380,6 @@ func (p *Plugin) ToDTO() PluginDTO { SignatureError: p.SignatureError, Module: p.Module, BaseURL: p.BaseURL, - supportsStreaming: p.client != nil && p.client.(backend.StreamHandler) != nil, } } @@ -387,7 +388,11 @@ func (p *Plugin) StaticRoute() *StaticRoute { return nil } - return &StaticRoute{Directory: p.PluginDir, PluginID: p.ID} + if p.FS == nil { + return nil + } + + return &StaticRoute{Directory: p.FS.Base(), PluginID: p.ID} } func (p *Plugin) IsRenderer() bool { @@ -414,15 +419,6 @@ func (p *Plugin) IsExternalPlugin() bool { return p.Class == External } -func (p *Plugin) Manifest() []byte { - d, err := os.ReadFile(filepath.Join(p.PluginDir, "MANIFEST.txt")) - if err != nil { - return []byte{} - } - - return d -} - type Class string const ( diff --git a/pkg/services/updatechecker/plugins_test.go b/pkg/services/updatechecker/plugins_test.go index 6028e735e8d..8003f61a027 100644 --- a/pkg/services/updatechecker/plugins_test.go +++ b/pkg/services/updatechecker/plugins_test.go @@ -135,6 +135,7 @@ func TestPluginUpdateChecker_checkForUpdates(t *testing.T) { Info: plugins.Info{Version: "0.9.0"}, Type: plugins.DataSource, }, + Class: plugins.External, }, { JSONData: plugins.JSONData{ @@ -142,6 +143,7 @@ func TestPluginUpdateChecker_checkForUpdates(t *testing.T) { Info: plugins.Info{Version: "0.5.0"}, Type: plugins.App, }, + Class: plugins.External, }, { JSONData: plugins.JSONData{ @@ -149,14 +151,15 @@ func TestPluginUpdateChecker_checkForUpdates(t *testing.T) { Info: plugins.Info{Version: "2.5.7"}, Type: plugins.Panel, }, + Class: plugins.Bundled, }, { - Class: plugins.Core, JSONData: plugins.JSONData{ ID: "test-core-panel", Info: plugins.Info{Version: "0.0.1"}, Type: plugins.Panel, }, + Class: plugins.Core, }, }, }, From bb9ae04bd8bbd0177c3b6e4002d62d528d989464 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Tue, 7 Mar 2023 07:54:25 -0800 Subject: [PATCH 036/288] Chore: cleanup entity api from CodeQL scan (#64277) cleanup --- pkg/services/store/entity/sqlstash/querybuilder.go | 2 +- pkg/services/store/entity/sqlstash/sql_storage_server.go | 9 ++------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/pkg/services/store/entity/sqlstash/querybuilder.go b/pkg/services/store/entity/sqlstash/querybuilder.go index 4f4ed45f4eb..e2ed2f4bf51 100644 --- a/pkg/services/store/entity/sqlstash/querybuilder.go +++ b/pkg/services/store/entity/sqlstash/querybuilder.go @@ -5,7 +5,7 @@ import "strings" type selectQuery struct { fields []string // SELECT xyz from string // FROM object - limit int + limit int64 oneExtra bool where []string diff --git a/pkg/services/store/entity/sqlstash/sql_storage_server.go b/pkg/services/store/entity/sqlstash/sql_storage_server.go index 03ddfb59f54..a4fd285f732 100644 --- a/pkg/services/store/entity/sqlstash/sql_storage_server.go +++ b/pkg/services/store/entity/sqlstash/sql_storage_server.go @@ -749,7 +749,7 @@ func (s *sqlEntityServer) Search(ctx context.Context, r *entity.EntitySearchRequ fields: fields, from: "entity", // the table args: []interface{}{}, - limit: int(r.Limit), + limit: r.Limit, oneExtra: true, // request one more than the limit (and show next token if it exists) } entityQuery.addWhere("tenant_id", user.OrgID) @@ -780,11 +780,6 @@ func (s *sqlEntityServer) Search(ctx context.Context, r *entity.EntitySearchRequ query, args := entityQuery.toQuery() - fmt.Printf("\n\n-------------\n") - fmt.Printf("%s\n", query) - fmt.Printf("%v\n", args) - fmt.Printf("\n-------------\n\n") - rows, err := s.sess.Query(ctx, query, args...) if err != nil { return nil, err @@ -820,7 +815,7 @@ func (s *sqlEntityServer) Search(ctx context.Context, r *entity.EntitySearchRequ } // found one more than requested - if len(rsp.Results) >= entityQuery.limit { + if int64(len(rsp.Results)) >= entityQuery.limit { // TODO? should this encode start+offset? rsp.NextPageToken = oid break From 1c2e3993f6675124e8ef16370aa0357d01ac98fc Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Tue, 7 Mar 2023 07:54:46 -0800 Subject: [PATCH 037/288] Chore: update debug and dns dev dependencies (#64279) update yarn lock --- yarn.lock | 32 ++++---------------------------- 1 file changed, 4 insertions(+), 28 deletions(-) diff --git a/yarn.lock b/yarn.lock index f711fb7f9fe..7d838abce8a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17881,19 +17881,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2": - version: 4.3.2 - resolution: "debug@npm:4.3.2" - dependencies: - ms: 2.1.2 - peerDependenciesMeta: - supports-color: - optional: true - checksum: 820ea160e267e23c953c9ed87e7ad93494d8cda2f7349af5e7e3bb236d23707ee3022f477d5a7d2ee86ef2bf7d60aa9ab22d1f58080d7deb9dccd073585e1e43 - languageName: node - linkType: hard - -"debug@npm:4.3.4, debug@npm:^4.0.0, debug@npm:^4.3.4": +"debug@npm:4, debug@npm:4.3.4, debug@npm:^4.0.0, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4": version: 4.3.4 resolution: "debug@npm:4.3.4" dependencies: @@ -17921,18 +17909,6 @@ __metadata: languageName: node linkType: hard -"debug@npm:^4.3.3": - version: 4.3.3 - resolution: "debug@npm:4.3.3" - dependencies: - ms: 2.1.2 - peerDependenciesMeta: - supports-color: - optional: true - checksum: 14472d56fe4a94dbcfaa6dbed2dd3849f1d72ba78104a1a328047bb564643ca49df0224c3a17fa63533fd11dd3d4c8636cd861191232a2c6735af00cc2d4de16 - languageName: node - linkType: hard - "debuglog@npm:^1.0.1": version: 1.0.1 resolution: "debuglog@npm:1.0.1" @@ -18377,11 +18353,11 @@ __metadata: linkType: hard "dns-packet@npm:^5.2.2": - version: 5.3.1 - resolution: "dns-packet@npm:5.3.1" + version: 5.4.0 + resolution: "dns-packet@npm:5.4.0" dependencies: "@leichtgewicht/ip-codec": ^2.0.1 - checksum: 196ff74a0669126cf5fc901a5849b72f625bd7a4cb163b3f4d41fbe19ed0b017cf7674daef5b0acbd448c094fcd795e501d7066f301be428e4acecfcf3c5f336 + checksum: a169963848e8539dfd8a19058562f9e1c15c0f82cbf76fa98942f11c46f3c74e7e7c82e3a8a5182d4c9e6ff19e21be738dbd098a876dde755d3aedd2cc730880 languageName: node linkType: hard From 13650f3dc0cfc17e907f228e760fb75080c85d4e Mon Sep 17 00:00:00 2001 From: Artur Wierzbicki Date: Tue, 7 Mar 2023 20:04:20 +0400 Subject: [PATCH 038/288] Code: codeownership for feature toggles (#64266) * ownership for feature toggles v2 * add traceqlSearch * MT -> app platform * assign publicdashboards --------- Co-authored-by: Ryan McKinley --- pkg/services/featuremgmt/codeowners.go | 15 +++++ pkg/services/featuremgmt/features.go | 3 + pkg/services/featuremgmt/registry.go | 25 ++++++++ pkg/services/featuremgmt/toggles_gen_test.go | 63 ++++++++++++++++++++ 4 files changed, 106 insertions(+) create mode 100644 pkg/services/featuremgmt/codeowners.go diff --git a/pkg/services/featuremgmt/codeowners.go b/pkg/services/featuremgmt/codeowners.go new file mode 100644 index 00000000000..3760429e84d --- /dev/null +++ b/pkg/services/featuremgmt/codeowners.go @@ -0,0 +1,15 @@ +package featuremgmt + +// codeowner string that references a GH team or user +// the value must match the format used in the CODEOWNERS file +type codeowner string + +const ( + grafanaAppPlatformSquad codeowner = "@grafana/grafana-app-platform-squad" + grafanaDashboardsSquad codeowner = "@grafana/dashboards-squad" + grafanaBiSquad codeowner = "@grafana/grafana-bi-squad" + grafanaDatavizSquad codeowner = "@grafana/dataviz-squad" + grafanaUserEssentialsSquad codeowner = "@grafana/user-essentials" + grafanaBackendPlatformSquad codeowner = "@grafana/backend-platform" + grafanaPluginsPlatformSquad codeowner = "@grafana/plugins-platform-backend" +) diff --git a/pkg/services/featuremgmt/features.go b/pkg/services/featuremgmt/features.go index 8a19c794f3b..c74eaacfa32 100644 --- a/pkg/services/featuremgmt/features.go +++ b/pkg/services/featuremgmt/features.go @@ -85,6 +85,9 @@ type FeatureFlag struct { State FeatureFlagState `json:"state,omitempty"` DocsURL string `json:"docsURL,omitempty"` + // Owner person or team that owns this feature flag + Owner codeowner `json:"-"` + // CEL-GO expression. Using the value "true" will mean this is on by default Expression string `json:"expression,omitempty"` diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 57fb12c13f1..198315ee53e 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -33,28 +33,33 @@ var ( Name: "dashboardPreviews", Description: "Create and show thumbnails for dashboard search results", State: FeatureStateAlpha, + Owner: grafanaAppPlatformSquad, }, { Name: "live-pipeline", Description: "Enable a generic live processing pipeline", State: FeatureStateAlpha, + Owner: grafanaAppPlatformSquad, }, { Name: "live-service-web-worker", Description: "This will use a webworker thread to processes events rather than the main thread", State: FeatureStateAlpha, FrontendOnly: true, + Owner: grafanaAppPlatformSquad, }, { Name: "queryOverLive", Description: "Use Grafana Live WebSocket to execute backend queries", State: FeatureStateAlpha, FrontendOnly: true, + Owner: grafanaAppPlatformSquad, }, { Name: "panelTitleSearch", Description: "Search for dashboards using panel title", State: FeatureStateBeta, + Owner: grafanaAppPlatformSquad, }, { Name: "prometheusAzureOverrideAudience", @@ -65,6 +70,7 @@ var ( Name: "publicDashboards", Description: "Enables public access to dashboards", State: FeatureStateAlpha, + Owner: grafanaDashboardsSquad, }, { Name: "publicDashboardsEmailSharing", @@ -72,11 +78,13 @@ var ( State: FeatureStateAlpha, RequiresLicense: true, RequiresDevMode: true, + Owner: grafanaDashboardsSquad, }, { Name: "lokiLive", Description: "Support WebSocket streaming for loki (early prototype)", State: FeatureStateAlpha, + Owner: grafanaAppPlatformSquad, }, { Name: "lokiDataframeApi", @@ -92,11 +100,13 @@ var ( Name: "dashboardComments", Description: "Enable dashboard-wide comments", State: FeatureStateAlpha, + Owner: grafanaAppPlatformSquad, }, { Name: "annotationComments", Description: "Enable annotation comments", State: FeatureStateAlpha, + Owner: grafanaAppPlatformSquad, }, { Name: "migrationLocking", @@ -107,18 +117,21 @@ var ( Name: "storage", Description: "Configurable storage for dashboards, datasources, and resources", State: FeatureStateAlpha, + Owner: grafanaAppPlatformSquad, }, { Name: "k8s", Description: "Explore native k8s integrations", State: FeatureStateAlpha, RequiresDevMode: true, + Owner: grafanaAppPlatformSquad, }, { Name: "dashboardsFromStorage", Description: "Load dashboards from the generic storage interface", State: FeatureStateAlpha, RequiresDevMode: true, // Also a gate on automatic git storage (for now) + Owner: grafanaAppPlatformSquad, }, { Name: "exploreMixedDatasource", @@ -153,6 +166,7 @@ var ( Name: "datasourceQueryMultiStatus", Description: "Introduce HTTP 207 Multi Status for api/ds/query", State: FeatureStateAlpha, + Owner: grafanaPluginsPlatformSquad, }, { Name: "traceToMetrics", @@ -164,6 +178,7 @@ var ( Name: "newDBLibrary", Description: "Use jmoiron/sqlx rather than xorm for a few backend services", State: FeatureStateBeta, + Owner: grafanaBackendPlatformSquad, }, { Name: "validateDashboardsOnSave", @@ -176,6 +191,7 @@ var ( Description: "Replace the angular graph panel with timeseries", State: FeatureStateBeta, FrontendOnly: true, + Owner: grafanaDatavizSquad, }, { Name: "prometheusWideSeries", @@ -187,12 +203,14 @@ var ( Description: "Allow elements nesting", State: FeatureStateAlpha, FrontendOnly: true, + Owner: grafanaDatavizSquad, }, { Name: "scenes", Description: "Experimental framework to build interactive dashboards", State: FeatureStateAlpha, FrontendOnly: true, + Owner: grafanaDashboardsSquad, }, { Name: "disableSecretsCompatibility", @@ -215,23 +233,27 @@ var ( Description: "Enables internationalization", State: FeatureStateStable, Expression: "true", // enabled by default + Owner: grafanaUserEssentialsSquad, }, { Name: "topnav", Description: "Displays new top nav and page layouts", State: FeatureStateBeta, + Owner: grafanaUserEssentialsSquad, }, { Name: "grpcServer", Description: "Run GRPC server", State: FeatureStateAlpha, RequiresDevMode: true, + Owner: grafanaAppPlatformSquad, }, { Name: "entityStore", Description: "SQL-based entity store (requires storage flag also)", State: FeatureStateAlpha, RequiresDevMode: true, + Owner: grafanaAppPlatformSquad, }, { Name: "cloudWatchCrossAccountQuerying", @@ -262,6 +284,7 @@ var ( Description: "Reusable query library", State: FeatureStateAlpha, RequiresDevMode: true, + Owner: grafanaAppPlatformSquad, }, { Name: "showDashboardValidationWarnings", @@ -324,6 +347,7 @@ var ( Description: "Enables drag and drop for CSV and Excel files", FrontendOnly: true, State: FeatureStateAlpha, + Owner: grafanaBiSquad, }, { Name: "alertingNoNormalState", @@ -361,6 +385,7 @@ var ( Description: "Changes the user experience for data source selection to a drawer.", State: FeatureStateAlpha, FrontendOnly: true, + Owner: grafanaBiSquad, }, { Name: "traceqlSearch", diff --git a/pkg/services/featuremgmt/toggles_gen_test.go b/pkg/services/featuremgmt/toggles_gen_test.go index 6e2d29b2ba9..0637408e794 100644 --- a/pkg/services/featuremgmt/toggles_gen_test.go +++ b/pkg/services/featuremgmt/toggles_gen_test.go @@ -41,6 +41,69 @@ func TestFeatureToggleFiles(t *testing.T) { } }) + ownerlessFeatures := map[string]bool{ + "alertingBigTransactions": true, + "trimDefaults": true, + "disableEnvelopeEncryption": true, + "database_metrics": true, + "prometheusAzureOverrideAudience": true, + "lokiDataframeApi": true, + "featureHighlights": true, + "migrationLocking": true, + "exploreMixedDatasource": true, + "tracing": true, + "newTraceView": true, + "correlations": true, + "cloudWatchDynamicLabels": true, + "traceToMetrics": true, + "validateDashboardsOnSave": true, + "prometheusWideSeries": true, + "disableSecretsCompatibility": true, + "logRequestsInstrumentedAsUnknown": true, + "dataConnectionsConsole": true, + "cloudWatchCrossAccountQuerying": true, + "redshiftAsyncQueryDataSupport": true, + "athenaAsyncQueryDataSupport": true, + "newPanelChromeUI": true, + "showDashboardValidationWarnings": true, + "mysqlAnsiQuotes": true, + "accessControlOnCall": true, + "nestedFolders": true, + "accessTokenExpirationCheck": true, + "elasticsearchBackendMigration": true, + "datasourceOnboarding": true, + "secureSocksDatasourceProxy": true, + "authnService": true, + "disablePrometheusExemplarSampling": true, + "alertingBacktesting": true, + "alertingNoNormalState": true, + "logsSampleInExplore": true, + "logsContextDatasourceUi": true, + "lokiQuerySplitting": true, + "individualCookiePreferences": true, + "traceqlSearch": true, + } + + t.Run("all new features should have an owner", func(t *testing.T) { + for _, flag := range standardFeatureFlags { + if flag.Owner == "" { + if _, ok := ownerlessFeatures[flag.Name]; !ok { + t.Errorf("feature %s does not have an owner", flag.Name) + } + } + } + }) + + t.Run("features with assigned owner should not be on the ownerless list", func(t *testing.T) { + for _, flag := range standardFeatureFlags { + if flag.Owner != "" { + if _, ok := ownerlessFeatures[flag.Name]; ok { + t.Errorf("feature %s should be removed from the ownerless list", flag.Name) + } + } + } + }) + t.Run("verify files", func(t *testing.T) { // Typescript files verifyAndGenerateFile(t, From 94f39e69a3df55c9b0c657a19b7d2e7d09fc1f8d Mon Sep 17 00:00:00 2001 From: Will Browne Date: Tue, 7 Mar 2023 16:22:30 +0000 Subject: [PATCH 039/288] Plugins: Migrate `plugincontext`, `adapters` and `pluginsettings` to pkg/services/pluginsintegration package (#64154) * migrate plugincontext, adapter and pluginsettings * add to CODEOWNERS * fix imports * fix CODEOWNERS * take pluginsettings * migrate wire stuff --- .github/CODEOWNERS | 2 +- pkg/api/datasources.go | 2 +- pkg/api/frontendsettings.go | 2 +- pkg/api/frontendsettings_test.go | 20 +++++++++---------- pkg/api/http_server.go | 4 ++-- pkg/api/plugin_proxy.go | 2 +- pkg/api/plugin_resource_test.go | 4 ++-- pkg/api/pluginproxy/pluginproxy.go | 2 +- pkg/api/pluginproxy/pluginproxy_test.go | 2 +- pkg/api/plugins.go | 2 +- pkg/api/plugins_test.go | 2 +- pkg/cmd/grafana-cli/runner/wire.go | 4 ---- pkg/expr/nodes.go | 2 +- pkg/server/wire.go | 7 +------ pkg/services/live/live.go | 2 +- pkg/services/live/liveplugin/plugin.go | 2 +- pkg/services/navtree/navtreeimpl/applinks.go | 2 +- .../navtree/navtreeimpl/applinks_test.go | 2 +- pkg/services/navtree/navtreeimpl/navtree.go | 2 +- .../service/dashboard_updater.go | 2 +- .../service/dashboard_updater_test.go | 4 ++-- .../pluginsintegration}/adapters/adapters.go | 0 .../plugincontext/plugincontext.go | 4 ++-- .../pluginsettings/fake.go | 0 .../pluginsettings/models.go | 0 .../pluginsettings/pluginsettings.go | 0 .../pluginsettings/service/service.go | 2 +- .../pluginsettings/service/service_test.go | 2 +- .../pluginsintegration/pluginsintegration.go | 6 +++++- .../plugins/plugin_provisioner.go | 2 +- .../plugins/plugin_provisioner_test.go | 2 +- pkg/services/provisioning/provisioning.go | 2 +- pkg/services/query/query.go | 2 +- .../supportbundlesimpl/collectors.go | 2 +- .../supportbundlesimpl/service.go | 2 +- pkg/tsdb/legacydata/service/service.go | 2 +- 36 files changed, 48 insertions(+), 53 deletions(-) rename pkg/{plugins => services/pluginsintegration}/adapters/adapters.go (100%) rename pkg/{plugins => services/pluginsintegration}/plugincontext/plugincontext.go (97%) rename pkg/services/{ => pluginsintegration}/pluginsettings/fake.go (100%) rename pkg/services/{ => pluginsintegration}/pluginsettings/models.go (100%) rename pkg/services/{ => pluginsintegration}/pluginsettings/pluginsettings.go (100%) rename pkg/services/{ => pluginsintegration}/pluginsettings/service/service.go (98%) rename pkg/services/{ => pluginsintegration}/pluginsettings/service/service_test.go (99%) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 181967464f9..8dce1268c27 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -111,7 +111,6 @@ /pkg/services/org/ @grafana/backend-platform /pkg/services/playlist/ @grafana/backend-platform /pkg/services/plugindashboards/ @grafana/backend-platform -/pkg/services/pluginsettings/ @grafana/backend-platform /pkg/services/preference/ @grafana/backend-platform /pkg/services/provisioning/ @grafana/backend-platform /pkg/services/publicdashboards/ @grafana/dashboards-squad @@ -281,6 +280,7 @@ /pkg/services/pluginsintegration/ @grafana/plugins-platform-backend /pkg/plugins/pfs/ @grafana/plugins-platform-backend @grafana/grafana-as-code /pkg/tsdb/testdatasource/ @grafana/plugins-platform-backend +/pkg/services/pluginsintegration/pluginsettings/ @grafana/plugins-platform-backend # Dashboard previews / crawler (behind feature flag) /pkg/services/thumbs/ @grafana/grafana-app-platform-squad diff --git a/pkg/api/datasources.go b/pkg/api/datasources.go index 49597f4e57b..85f2029a768 100644 --- a/pkg/api/datasources.go +++ b/pkg/api/datasources.go @@ -17,10 +17,10 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/plugins/adapters" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/datasources/permissions" + "github.com/grafana/grafana/pkg/services/pluginsintegration/adapters" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index cc5ac4594b1..6a83bdc227d 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -11,7 +11,7 @@ import ( contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/licensing" - "github.com/grafana/grafana/pkg/services/pluginsettings" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings" "github.com/grafana/grafana/pkg/services/secrets/kvstore" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/tsdb/grafanads" diff --git a/pkg/api/frontendsettings_test.go b/pkg/api/frontendsettings_test.go index 8d809418f99..0ab25e49009 100644 --- a/pkg/api/frontendsettings_test.go +++ b/pkg/api/frontendsettings_test.go @@ -20,14 +20,14 @@ import ( accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/licensing" - pluginSettings "github.com/grafana/grafana/pkg/services/pluginsettings" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings" "github.com/grafana/grafana/pkg/services/rendering" "github.com/grafana/grafana/pkg/services/updatechecker" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/web" ) -func setupTestEnvironment(t *testing.T, cfg *setting.Cfg, features *featuremgmt.FeatureManager, pstore plugins.Store, psettings pluginSettings.Service) (*web.Mux, *HTTPServer) { +func setupTestEnvironment(t *testing.T, cfg *setting.Cfg, features *featuremgmt.FeatureManager, pstore plugins.Store, psettings pluginsettings.Service) (*web.Mux, *HTTPServer) { t.Helper() db.InitTestDB(t) cfg.IsFeatureToggleEnabled = features.IsEnabled @@ -50,7 +50,7 @@ func setupTestEnvironment(t *testing.T, cfg *setting.Cfg, features *featuremgmt. var pluginsSettings = psettings if pluginsSettings == nil { - pluginsSettings = &pluginSettings.FakePluginSettings{} + pluginsSettings = &pluginsettings.FakePluginSettings{} } hs := &HTTPServer{ @@ -210,7 +210,7 @@ func TestHTTPServer_GetFrontendSettings_apps(t *testing.T) { tests := []struct { desc string pluginStore func() plugins.Store - pluginSettings func() pluginSettings.Service + pluginSettings func() pluginsettings.Service expected settings }{ { @@ -230,8 +230,8 @@ func TestHTTPServer_GetFrontendSettings_apps(t *testing.T) { }, } }, - pluginSettings: func() pluginSettings.Service { - return &pluginSettings.FakePluginSettings{ + pluginSettings: func() pluginsettings.Service { + return &pluginsettings.FakePluginSettings{ Plugins: newAppSettings("test-app", false), } }, @@ -263,8 +263,8 @@ func TestHTTPServer_GetFrontendSettings_apps(t *testing.T) { }, } }, - pluginSettings: func() pluginSettings.Service { - return &pluginSettings.FakePluginSettings{ + pluginSettings: func() pluginsettings.Service { + return &pluginsettings.FakePluginSettings{ Plugins: newAppSettings("test-app", true), } }, @@ -298,8 +298,8 @@ func TestHTTPServer_GetFrontendSettings_apps(t *testing.T) { } } -func newAppSettings(id string, enabled bool) map[string]*pluginSettings.DTO { - return map[string]*pluginSettings.DTO{ +func newAppSettings(id string, enabled bool) map[string]*pluginsettings.DTO { + return map[string]*pluginsettings.DTO{ id: { ID: 0, OrgID: 1, diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index 710d8c3dee6..a24a592e72d 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -32,7 +32,6 @@ import ( "github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/middleware/csrf" "github.com/grafana/grafana/pkg/plugins" - "github.com/grafana/grafana/pkg/plugins/plugincontext" "github.com/grafana/grafana/pkg/plugins/pluginscdn" "github.com/grafana/grafana/pkg/registry/corekind" "github.com/grafana/grafana/pkg/services/accesscontrol" @@ -69,7 +68,8 @@ import ( "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/playlist" "github.com/grafana/grafana/pkg/services/plugindashboards" - pluginSettings "github.com/grafana/grafana/pkg/services/pluginsettings" + "github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext" + pluginSettings "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings" pref "github.com/grafana/grafana/pkg/services/preference" "github.com/grafana/grafana/pkg/services/provisioning" publicdashboardsApi "github.com/grafana/grafana/pkg/services/publicdashboards/api" diff --git a/pkg/api/plugin_proxy.go b/pkg/api/plugin_proxy.go index 0ff09d45bb0..1cec8502e76 100644 --- a/pkg/api/plugin_proxy.go +++ b/pkg/api/plugin_proxy.go @@ -10,7 +10,7 @@ import ( "github.com/grafana/grafana/pkg/api/pluginproxy" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" - "github.com/grafana/grafana/pkg/services/pluginsettings" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings" "github.com/grafana/grafana/pkg/web" ) diff --git a/pkg/api/plugin_resource_test.go b/pkg/api/plugin_resource_test.go index 90de2bcaebc..b0218997446 100644 --- a/pkg/api/plugin_resource_test.go +++ b/pkg/api/plugin_resource_test.go @@ -27,14 +27,14 @@ import ( "github.com/grafana/grafana/pkg/plugins/manager/signature" "github.com/grafana/grafana/pkg/plugins/manager/sources" "github.com/grafana/grafana/pkg/plugins/manager/store" - "github.com/grafana/grafana/pkg/plugins/plugincontext" "github.com/grafana/grafana/pkg/plugins/pluginscdn" "github.com/grafana/grafana/pkg/services/accesscontrol" datasources "github.com/grafana/grafana/pkg/services/datasources/fakes" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/oauthtoken/oauthtokentest" - pluginSettings "github.com/grafana/grafana/pkg/services/pluginsettings/service" "github.com/grafana/grafana/pkg/services/pluginsintegration" + "github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext" + pluginSettings "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings/service" "github.com/grafana/grafana/pkg/services/quota/quotatest" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" diff --git a/pkg/api/pluginproxy/pluginproxy.go b/pkg/api/pluginproxy/pluginproxy.go index a69abb209be..2eac4ac75fd 100644 --- a/pkg/api/pluginproxy/pluginproxy.go +++ b/pkg/api/pluginproxy/pluginproxy.go @@ -10,7 +10,7 @@ import ( "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/plugins" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" - "github.com/grafana/grafana/pkg/services/pluginsettings" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings" "github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" diff --git a/pkg/api/pluginproxy/pluginproxy_test.go b/pkg/api/pluginproxy/pluginproxy_test.go index 4ac71cb9291..6ad082c2756 100644 --- a/pkg/api/pluginproxy/pluginproxy_test.go +++ b/pkg/api/pluginproxy/pluginproxy_test.go @@ -13,7 +13,7 @@ import ( "github.com/grafana/grafana/pkg/plugins" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/org" - "github.com/grafana/grafana/pkg/services/pluginsettings" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings" "github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/services/secrets/fakes" secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager" diff --git a/pkg/api/plugins.go b/pkg/api/plugins.go index 5bb0e0834c2..952e679e755 100644 --- a/pkg/api/plugins.go +++ b/pkg/api/plugins.go @@ -28,7 +28,7 @@ import ( contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/org" - "github.com/grafana/grafana/pkg/services/pluginsettings" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/web" diff --git a/pkg/api/plugins_test.go b/pkg/api/plugins_test.go index bcaddeba8c5..5e4946976f4 100644 --- a/pkg/api/plugins_test.go +++ b/pkg/api/plugins_test.go @@ -27,7 +27,7 @@ import ( contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org/orgtest" - "github.com/grafana/grafana/pkg/services/pluginsettings" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings" "github.com/grafana/grafana/pkg/services/quota/quotatest" "github.com/grafana/grafana/pkg/services/updatechecker" "github.com/grafana/grafana/pkg/services/user" diff --git a/pkg/cmd/grafana-cli/runner/wire.go b/pkg/cmd/grafana-cli/runner/wire.go index af7bb2e7cda..e43ddece6ad 100644 --- a/pkg/cmd/grafana-cli/runner/wire.go +++ b/pkg/cmd/grafana-cli/runner/wire.go @@ -79,8 +79,6 @@ import ( "github.com/grafana/grafana/pkg/services/playlist/playlistimpl" "github.com/grafana/grafana/pkg/services/plugindashboards" plugindashboardsservice "github.com/grafana/grafana/pkg/services/plugindashboards/service" - "github.com/grafana/grafana/pkg/services/pluginsettings" - pluginSettings "github.com/grafana/grafana/pkg/services/pluginsettings/service" "github.com/grafana/grafana/pkg/services/pluginsintegration" "github.com/grafana/grafana/pkg/services/preference/prefimpl" "github.com/grafana/grafana/pkg/services/publicdashboards" @@ -244,8 +242,6 @@ var wireSet = wire.NewSet( dashsnapsvc.ProvideService, datasourceservice.ProvideService, wire.Bind(new(datasources.DataSourceService), new(*datasourceservice.Service)), - pluginSettings.ProvideService, - wire.Bind(new(pluginsettings.Service), new(*pluginSettings.Service)), alerting.ProvideService, ossaccesscontrol.ProvideServiceAccountPermissions, wire.Bind(new(accesscontrol.ServiceAccountPermissionsService), new(*ossaccesscontrol.ServiceAccountPermissionsService)), diff --git a/pkg/expr/nodes.go b/pkg/expr/nodes.go index 82e48d824b9..304f04c8e08 100644 --- a/pkg/expr/nodes.go +++ b/pkg/expr/nodes.go @@ -13,8 +13,8 @@ import ( "github.com/grafana/grafana/pkg/expr/classic" "github.com/grafana/grafana/pkg/expr/mathexp" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/plugins/adapters" "github.com/grafana/grafana/pkg/services/datasources" + "github.com/grafana/grafana/pkg/services/pluginsintegration/adapters" ) var ( diff --git a/pkg/server/wire.go b/pkg/server/wire.go index f98ac36589e..576ea5dfdd7 100644 --- a/pkg/server/wire.go +++ b/pkg/server/wire.go @@ -7,8 +7,6 @@ import ( "github.com/google/wire" sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" - "github.com/grafana/grafana/pkg/services/folder" - "github.com/grafana/grafana/pkg/api" "github.com/grafana/grafana/pkg/api/avatar" "github.com/grafana/grafana/pkg/api/routing" @@ -62,6 +60,7 @@ import ( "github.com/grafana/grafana/pkg/services/encryption" encryptionservice "github.com/grafana/grafana/pkg/services/encryption/service" "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/folder/folderimpl" "github.com/grafana/grafana/pkg/services/grpcserver" grpccontext "github.com/grafana/grafana/pkg/services/grpcserver/context" @@ -92,8 +91,6 @@ import ( "github.com/grafana/grafana/pkg/services/playlist/playlistimpl" "github.com/grafana/grafana/pkg/services/plugindashboards" plugindashboardsservice "github.com/grafana/grafana/pkg/services/plugindashboards/service" - "github.com/grafana/grafana/pkg/services/pluginsettings" - pluginSettings "github.com/grafana/grafana/pkg/services/pluginsettings/service" "github.com/grafana/grafana/pkg/services/pluginsintegration" "github.com/grafana/grafana/pkg/services/preference/prefimpl" "github.com/grafana/grafana/pkg/services/publicdashboards" @@ -280,8 +277,6 @@ var wireBasicSet = wire.NewSet( dashsnapsvc.ProvideService, datasourceservice.ProvideService, wire.Bind(new(datasources.DataSourceService), new(*datasourceservice.Service)), - pluginSettings.ProvideService, - wire.Bind(new(pluginsettings.Service), new(*pluginSettings.Service)), alerting.ProvideService, serviceaccountsretriever.ProvideService, wire.Bind(new(serviceaccountsretriever.ServiceAccountRetriever), new(*serviceaccountsretriever.Service)), diff --git a/pkg/services/live/live.go b/pkg/services/live/live.go index 8e34578396a..c23241020e4 100644 --- a/pkg/services/live/live.go +++ b/pkg/services/live/live.go @@ -31,7 +31,6 @@ import ( "github.com/grafana/grafana/pkg/infra/usagestats" "github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/plugins" - "github.com/grafana/grafana/pkg/plugins/plugincontext" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/annotations" "github.com/grafana/grafana/pkg/services/comments/commentmodel" @@ -51,6 +50,7 @@ import ( "github.com/grafana/grafana/pkg/services/live/runstream" "github.com/grafana/grafana/pkg/services/live/survey" "github.com/grafana/grafana/pkg/services/org" + "github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext" "github.com/grafana/grafana/pkg/services/query" "github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/services/user" diff --git a/pkg/services/live/liveplugin/plugin.go b/pkg/services/live/liveplugin/plugin.go index f35302ef12a..562933c1b6d 100644 --- a/pkg/services/live/liveplugin/plugin.go +++ b/pkg/services/live/liveplugin/plugin.go @@ -7,10 +7,10 @@ import ( "github.com/centrifugal/centrifuge" "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/plugins/plugincontext" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/live/orgchannel" "github.com/grafana/grafana/pkg/services/live/pipeline" + "github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext" "github.com/grafana/grafana/pkg/services/user" ) diff --git a/pkg/services/navtree/navtreeimpl/applinks.go b/pkg/services/navtree/navtreeimpl/applinks.go index 0783b6e38e1..3c44c4f245f 100644 --- a/pkg/services/navtree/navtreeimpl/applinks.go +++ b/pkg/services/navtree/navtreeimpl/applinks.go @@ -10,7 +10,7 @@ import ( contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/navtree" - "github.com/grafana/grafana/pkg/services/pluginsettings" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings" "github.com/grafana/grafana/pkg/util" ) diff --git a/pkg/services/navtree/navtreeimpl/applinks_test.go b/pkg/services/navtree/navtreeimpl/applinks_test.go index 32727630992..6646b3f52a1 100644 --- a/pkg/services/navtree/navtreeimpl/applinks_test.go +++ b/pkg/services/navtree/navtreeimpl/applinks_test.go @@ -16,7 +16,7 @@ import ( "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/navtree" - "github.com/grafana/grafana/pkg/services/pluginsettings" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/web" diff --git a/pkg/services/navtree/navtreeimpl/navtree.go b/pkg/services/navtree/navtreeimpl/navtree.go index 316d9f24cd3..f65b1fc950d 100644 --- a/pkg/services/navtree/navtreeimpl/navtree.go +++ b/pkg/services/navtree/navtreeimpl/navtree.go @@ -16,7 +16,7 @@ import ( "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/navtree" "github.com/grafana/grafana/pkg/services/org" - "github.com/grafana/grafana/pkg/services/pluginsettings" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings" pref "github.com/grafana/grafana/pkg/services/preference" "github.com/grafana/grafana/pkg/services/querylibrary" "github.com/grafana/grafana/pkg/services/star" diff --git a/pkg/services/plugindashboards/service/dashboard_updater.go b/pkg/services/plugindashboards/service/dashboard_updater.go index 03cd21907f4..977f8d19b26 100644 --- a/pkg/services/plugindashboards/service/dashboard_updater.go +++ b/pkg/services/plugindashboards/service/dashboard_updater.go @@ -12,7 +12,7 @@ import ( "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/plugindashboards" - "github.com/grafana/grafana/pkg/services/pluginsettings" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings" ) func ProvideDashboardUpdater(bus bus.Bus, pluginStore plugins.Store, pluginDashboardService plugindashboards.Service, diff --git a/pkg/services/plugindashboards/service/dashboard_updater_test.go b/pkg/services/plugindashboards/service/dashboard_updater_test.go index 5b9e160e9f6..0b873751769 100644 --- a/pkg/services/plugindashboards/service/dashboard_updater_test.go +++ b/pkg/services/plugindashboards/service/dashboard_updater_test.go @@ -14,8 +14,8 @@ import ( "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/plugindashboards" - "github.com/grafana/grafana/pkg/services/pluginsettings" - "github.com/grafana/grafana/pkg/services/pluginsettings/service" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings/service" ) func TestDashboardUpdater(t *testing.T) { diff --git a/pkg/plugins/adapters/adapters.go b/pkg/services/pluginsintegration/adapters/adapters.go similarity index 100% rename from pkg/plugins/adapters/adapters.go rename to pkg/services/pluginsintegration/adapters/adapters.go diff --git a/pkg/plugins/plugincontext/plugincontext.go b/pkg/services/pluginsintegration/plugincontext/plugincontext.go similarity index 97% rename from pkg/plugins/plugincontext/plugincontext.go rename to pkg/services/pluginsintegration/plugincontext/plugincontext.go index d3de7223cf8..4b43561bace 100644 --- a/pkg/plugins/plugincontext/plugincontext.go +++ b/pkg/services/pluginsintegration/plugincontext/plugincontext.go @@ -11,10 +11,10 @@ import ( "github.com/grafana/grafana/pkg/infra/localcache" "github.com/grafana/grafana/pkg/plugins" - "github.com/grafana/grafana/pkg/plugins/adapters" "github.com/grafana/grafana/pkg/plugins/log" "github.com/grafana/grafana/pkg/services/datasources" - "github.com/grafana/grafana/pkg/services/pluginsettings" + "github.com/grafana/grafana/pkg/services/pluginsintegration/adapters" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings" "github.com/grafana/grafana/pkg/services/user" ) diff --git a/pkg/services/pluginsettings/fake.go b/pkg/services/pluginsintegration/pluginsettings/fake.go similarity index 100% rename from pkg/services/pluginsettings/fake.go rename to pkg/services/pluginsintegration/pluginsettings/fake.go diff --git a/pkg/services/pluginsettings/models.go b/pkg/services/pluginsintegration/pluginsettings/models.go similarity index 100% rename from pkg/services/pluginsettings/models.go rename to pkg/services/pluginsintegration/pluginsettings/models.go diff --git a/pkg/services/pluginsettings/pluginsettings.go b/pkg/services/pluginsintegration/pluginsettings/pluginsettings.go similarity index 100% rename from pkg/services/pluginsettings/pluginsettings.go rename to pkg/services/pluginsintegration/pluginsettings/pluginsettings.go diff --git a/pkg/services/pluginsettings/service/service.go b/pkg/services/pluginsintegration/pluginsettings/service/service.go similarity index 98% rename from pkg/services/pluginsettings/service/service.go rename to pkg/services/pluginsintegration/pluginsettings/service/service.go index 8c898822da8..4eb86796c9b 100644 --- a/pkg/services/pluginsettings/service/service.go +++ b/pkg/services/pluginsintegration/pluginsettings/service/service.go @@ -7,7 +7,7 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/services/pluginsettings" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings" "github.com/grafana/grafana/pkg/services/secrets" ) diff --git a/pkg/services/pluginsettings/service/service_test.go b/pkg/services/pluginsintegration/pluginsettings/service/service_test.go similarity index 99% rename from pkg/services/pluginsettings/service/service_test.go rename to pkg/services/pluginsintegration/pluginsettings/service/service_test.go index 35b6cd1fe75..10471bde756 100644 --- a/pkg/services/pluginsettings/service/service_test.go +++ b/pkg/services/pluginsintegration/pluginsettings/service/service_test.go @@ -8,7 +8,7 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/services/pluginsettings" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings" "github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/services/secrets/fakes" secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager" diff --git a/pkg/services/pluginsintegration/pluginsintegration.go b/pkg/services/pluginsintegration/pluginsintegration.go index fa1f53c2056..7dda1f942f4 100644 --- a/pkg/services/pluginsintegration/pluginsintegration.go +++ b/pkg/services/pluginsintegration/pluginsintegration.go @@ -16,11 +16,13 @@ import ( "github.com/grafana/grafana/pkg/plugins/manager/signature" "github.com/grafana/grafana/pkg/plugins/manager/sources" "github.com/grafana/grafana/pkg/plugins/manager/store" - "github.com/grafana/grafana/pkg/plugins/plugincontext" "github.com/grafana/grafana/pkg/plugins/pluginscdn" "github.com/grafana/grafana/pkg/plugins/repo" "github.com/grafana/grafana/pkg/services/oauthtoken" "github.com/grafana/grafana/pkg/services/pluginsintegration/clientmiddleware" + "github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings" + pluginSettings "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings/service" "github.com/grafana/grafana/pkg/setting" ) @@ -53,6 +55,8 @@ var WireSet = wire.NewSet( wire.Bind(new(plugins.Licensing), new(*licensing.Service)), wire.Bind(new(sources.Resolver), new(*sources.Service)), sources.ProvideService, + pluginSettings.ProvideService, + wire.Bind(new(pluginsettings.Service), new(*pluginSettings.Service)), ) // WireExtensionSet provides a wire.ProviderSet of plugin providers that can be diff --git a/pkg/services/provisioning/plugins/plugin_provisioner.go b/pkg/services/provisioning/plugins/plugin_provisioner.go index c154603eb0c..41c63bd8edf 100644 --- a/pkg/services/provisioning/plugins/plugin_provisioner.go +++ b/pkg/services/provisioning/plugins/plugin_provisioner.go @@ -7,7 +7,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/org" - "github.com/grafana/grafana/pkg/services/pluginsettings" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings" ) // Provision scans a directory for provisioning config files diff --git a/pkg/services/provisioning/plugins/plugin_provisioner_test.go b/pkg/services/provisioning/plugins/plugin_provisioner_test.go index b805756a048..f63bdcfba8c 100644 --- a/pkg/services/provisioning/plugins/plugin_provisioner_test.go +++ b/pkg/services/provisioning/plugins/plugin_provisioner_test.go @@ -10,7 +10,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org/orgtest" - "github.com/grafana/grafana/pkg/services/pluginsettings" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings" ) func TestPluginProvisioner(t *testing.T) { diff --git a/pkg/services/provisioning/provisioning.go b/pkg/services/provisioning/provisioning.go index 9f7bff61917..6496c9bbb90 100644 --- a/pkg/services/provisioning/provisioning.go +++ b/pkg/services/provisioning/provisioning.go @@ -21,7 +21,7 @@ import ( "github.com/grafana/grafana/pkg/services/ngalert/store" "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/services/org" - "github.com/grafana/grafana/pkg/services/pluginsettings" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings" prov_alerting "github.com/grafana/grafana/pkg/services/provisioning/alerting" "github.com/grafana/grafana/pkg/services/provisioning/dashboards" "github.com/grafana/grafana/pkg/services/provisioning/datasources" diff --git a/pkg/services/query/query.go b/pkg/services/query/query.go index bdbf99dfd79..426843d3ca2 100644 --- a/pkg/services/query/query.go +++ b/pkg/services/query/query.go @@ -13,8 +13,8 @@ import ( "github.com/grafana/grafana/pkg/expr" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/plugins" - "github.com/grafana/grafana/pkg/plugins/adapters" "github.com/grafana/grafana/pkg/services/datasources" + "github.com/grafana/grafana/pkg/services/pluginsintegration/adapters" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/validations" "github.com/grafana/grafana/pkg/setting" diff --git a/pkg/services/supportbundles/supportbundlesimpl/collectors.go b/pkg/services/supportbundles/supportbundlesimpl/collectors.go index 961933708d1..e5d2346a211 100644 --- a/pkg/services/supportbundles/supportbundlesimpl/collectors.go +++ b/pkg/services/supportbundles/supportbundlesimpl/collectors.go @@ -9,7 +9,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/plugins" - "github.com/grafana/grafana/pkg/services/pluginsettings" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings" "github.com/grafana/grafana/pkg/services/supportbundles" "github.com/grafana/grafana/pkg/setting" ) diff --git a/pkg/services/supportbundles/supportbundlesimpl/service.go b/pkg/services/supportbundles/supportbundlesimpl/service.go index 54bb5f28b84..1bb71575fd3 100644 --- a/pkg/services/supportbundles/supportbundlesimpl/service.go +++ b/pkg/services/supportbundles/supportbundlesimpl/service.go @@ -14,7 +14,7 @@ import ( "github.com/grafana/grafana/pkg/plugins" ac "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/services/pluginsettings" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings" "github.com/grafana/grafana/pkg/services/supportbundles" "github.com/grafana/grafana/pkg/services/supportbundles/bundleregistry" "github.com/grafana/grafana/pkg/services/user" diff --git a/pkg/tsdb/legacydata/service/service.go b/pkg/tsdb/legacydata/service/service.go index edd998632f4..df6a4f39468 100644 --- a/pkg/tsdb/legacydata/service/service.go +++ b/pkg/tsdb/legacydata/service/service.go @@ -7,9 +7,9 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana/pkg/plugins" - "github.com/grafana/grafana/pkg/plugins/adapters" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/oauthtoken" + "github.com/grafana/grafana/pkg/services/pluginsintegration/adapters" "github.com/grafana/grafana/pkg/tsdb/legacydata" ) From 9b6e531549e145d1a1a0ede870a6163c69b0bfd5 Mon Sep 17 00:00:00 2001 From: Brendan O'Handley Date: Tue, 7 Mar 2023 13:41:05 -0500 Subject: [PATCH 040/288] Prometheus: Metric encyclopedia (#63423) * add metric encyclopedia feature toggle and component * remove unused button * move file, add test file * add tests * add pagination and tests * test with 10,000,000 metrics * remove unused import * add filter by type * search alphabetically and add switch to exclude metrics with no metadata * add suggested functions and filter for functions * allow user to select variables in encyclopedia * fix style and tests * add fuzzy search by either metric name or all metadata * if missing metadata, remove metadata fuzzy search option, exclude metadata, and filter by type * add encyclopedia feature tracking * indicate that metrics are filtered by labels * handle metric singular or plural * add tooltips and fix language * add filtering tests * change 'search' to 'browse' * remove functions filter and tests as not part of work flow * add m.e. button and selected metric is a tag * fix hanging search and update styles, padding, labels, and groupings * small performance improvements * fix tests * add backend metrics query option * add loading spinner for start load and backend search * autofocus search input * Update docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com> * run prettier * run prettier * fix text for feature toggle * for license check since https://cla-assistant.io/check/grafana/grafana?pullRequest= is not working * fixing tests * fix feature toggle docs * fix feature toggle * fix feature toggle * add owner to feature toggle --------- Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com> --- .../feature-toggles/index.md | 1 + .../src/types/featureToggles.gen.ts | 1 + pkg/services/featuremgmt/registry.go | 7 + pkg/services/featuremgmt/toggles_gen.go | 4 + .../prometheus/language_provider.ts | 14 + .../MetricEncyclopediaModal.test.tsx | 297 +++++++ .../components/MetricEncyclopediaModal.tsx | 735 ++++++++++++++++++ .../components/PromQueryBuilder.tsx | 68 +- 8 files changed, 1119 insertions(+), 8 deletions(-) create mode 100644 public/app/plugins/datasource/prometheus/querybuilder/components/MetricEncyclopediaModal.test.tsx create mode 100644 public/app/plugins/datasource/prometheus/querybuilder/components/MetricEncyclopediaModal.tsx diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index 776b5ca774e..ce40c22e0d8 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -92,6 +92,7 @@ Alpha features might be changed or removed without prior notice. | `individualCookiePreferences` | Support overriding cookie preferences per user | | `drawerDataSourcePicker` | Changes the user experience for data source selection to a drawer. | | `traceqlSearch` | Enables the 'TraceQL Search' tab for the Tempo datasource which provides a UI to generate TraceQL queries | +| `prometheusMetricEncyclopedia` | Replaces the Prometheus query builder metric select option with a paginated and filterable component | ## Development feature toggles diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index f5688619173..871849c6a98 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -81,4 +81,5 @@ export interface FeatureToggles { individualCookiePreferences?: boolean; drawerDataSourcePicker?: boolean; traceqlSearch?: boolean; + prometheusMetricEncyclopedia?: boolean; } diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 198315ee53e..722d9218855 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -393,5 +393,12 @@ var ( State: FeatureStateAlpha, FrontendOnly: true, }, + { + Name: "prometheusMetricEncyclopedia", + Description: "Replaces the Prometheus query builder metric select option with a paginated and filterable component", + State: FeatureStateAlpha, + FrontendOnly: true, + Owner: "O11y-metrics", + }, } ) diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index c26697b1a81..85f7c502304 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -266,4 +266,8 @@ const ( // FlagTraceqlSearch // Enables the 'TraceQL Search' tab for the Tempo datasource which provides a UI to generate TraceQL queries FlagTraceqlSearch = "traceqlSearch" + + // FlagPrometheusMetricEncyclopedia + // Replaces the Prometheus query builder metric select option with a paginated and filterable component + FlagPrometheusMetricEncyclopedia = "prometheusMetricEncyclopedia" ) diff --git a/public/app/plugins/datasource/prometheus/language_provider.ts b/public/app/plugins/datasource/prometheus/language_provider.ts index 31970bb0db3..4b2251ac7e1 100644 --- a/public/app/plugins/datasource/prometheus/language_provider.ts +++ b/public/app/plugins/datasource/prometheus/language_provider.ts @@ -77,6 +77,20 @@ export function getMetadataString(metric: string, metadata: PromMetricsMetadata) return `${type.toUpperCase()}: ${help}`; } +export function getMetadataHelp(metric: string, metadata: PromMetricsMetadata): string | undefined { + if (!metadata[metric]) { + return undefined; + } + return metadata[metric].help; +} + +export function getMetadataType(metric: string, metadata: PromMetricsMetadata): string | undefined { + if (!metadata[metric]) { + return undefined; + } + return metadata[metric].type; +} + const PREFIX_DELIMITER_REGEX = /(="|!="|=~"|!~"|\{|\[|\(|\+|-|\/|\*|%|\^|\band\b|\bor\b|\bunless\b|==|>=|!=|<=|>|<|=|~|,)/; diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/MetricEncyclopediaModal.test.tsx b/public/app/plugins/datasource/prometheus/querybuilder/components/MetricEncyclopediaModal.test.tsx new file mode 100644 index 00000000000..fc16cf74394 --- /dev/null +++ b/public/app/plugins/datasource/prometheus/querybuilder/components/MetricEncyclopediaModal.test.tsx @@ -0,0 +1,297 @@ +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; + +import { DataSourceInstanceSettings, DataSourcePluginMeta } from '@grafana/data'; + +import { PrometheusDatasource } from '../../datasource'; +import PromQlLanguageProvider from '../../language_provider'; +import { EmptyLanguageProviderMock } from '../../language_provider.mock'; +import { PromOptions } from '../../types'; +import { PromVisualQuery } from '../types'; + +import { MetricEncyclopediaModal, testIds, placeholders } from './MetricEncyclopediaModal'; + +// don't care about interaction tracking in our unit tests +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + reportInteraction: jest.fn(), +})); + +describe('MetricEncyclopediaModal', () => { + it('renders the modal', async () => { + setup(defaultQuery, listOfMetrics); + await waitFor(() => { + expect(screen.getByText('Browse Metrics')).toBeInTheDocument(); + }); + }); + + it('renders a list of metrics', async () => { + setup(defaultQuery, listOfMetrics); + await waitFor(() => { + expect(screen.getByText('all-metrics')).toBeInTheDocument(); + }); + }); + + it('renders a list of metrics filtered by labels in the PromVisualQuery', async () => { + const query: PromVisualQuery = { + metric: 'random_metric', + labels: [ + { + op: '=', + label: 'action', + value: 'add_presence', + }, + ], + operations: [], + }; + + setup(query, listOfMetrics); + await waitFor(() => { + expect(screen.getByText('with-labels')).toBeInTheDocument(); + }); + }); + + it('displays a type for a metric when the metric is clicked', async () => { + setup(defaultQuery, listOfMetrics); + await waitFor(() => { + expect(screen.getByText('all-metrics')).toBeInTheDocument(); + }); + + const interactiveMetric = screen.getByText('all-metrics'); + + await userEvent.click(interactiveMetric); + + expect(screen.getByText('all-metrics-type')).toBeInTheDocument(); + }); + + it('displays a description for a metric', async () => { + setup(defaultQuery, listOfMetrics); + await waitFor(() => { + expect(screen.getByText('all-metrics')).toBeInTheDocument(); + }); + + const interactiveMetric = screen.getByText('all-metrics'); + + await userEvent.click(interactiveMetric); + + expect(screen.getByText('all-metrics-help')).toBeInTheDocument(); + }); + + it('displays no metadata for a metric missing metadata when the metric is clicked', async () => { + setup(defaultQuery, listOfMetrics); + await waitFor(() => { + expect(screen.getByText('b')).toBeInTheDocument(); + }); + + const interactiveMetric = screen.getByText('b'); + + await userEvent.click(interactiveMetric); + + expect(screen.getByText('No metadata available')).toBeInTheDocument(); + }); + + // Filtering + it('has a filter for selected type', async () => { + setup(defaultQuery, listOfMetrics); + + await waitFor(() => { + const selectType = screen.getByText(placeholders.type); + expect(selectType).toBeInTheDocument(); + }); + }); + + it('filters by alphebetical letter choice', async () => { + setup(defaultQuery, listOfMetrics); + // pick the letter J + const letterJ = screen.getByTestId('letter-J'); + await userEvent.click(letterJ); + + // check metrics that start with J + const metricStartingWithJ = screen.getByText('j'); + expect(metricStartingWithJ).toBeInTheDocument(); + // check metrics that don't start with J + const metricStartingWithSomethingElse = screen.queryByText('a'); + expect(metricStartingWithSomethingElse).toBeNull(); + }); + + it('allows a user to select a template variable', async () => { + setup(defaultQuery, listOfMetrics); + + await waitFor(() => { + const selectType = screen.getByText(placeholders.variables); + expect(selectType).toBeInTheDocument(); + }); + }); + + // Pagination + it('shows metrics within a range by pagination', async () => { + // default resultsPerPage is 10 + setup(defaultQuery, listOfMetrics); + await waitFor(() => { + expect(screen.getByText('all-metrics')).toBeInTheDocument(); + expect(screen.getByText('a_bucket')).toBeInTheDocument(); + expect(screen.getByText('a')).toBeInTheDocument(); + expect(screen.getByText('b')).toBeInTheDocument(); + expect(screen.getByText('c')).toBeInTheDocument(); + expect(screen.getByText('d')).toBeInTheDocument(); + expect(screen.getByText('e')).toBeInTheDocument(); + expect(screen.getByText('f')).toBeInTheDocument(); + expect(screen.getByText('g')).toBeInTheDocument(); + expect(screen.getByText('h')).toBeInTheDocument(); + }); + }); + + it('does not show metrics outside a range by pagination', async () => { + // default resultsPerPage is 10 + setup(defaultQuery, listOfMetrics); + await waitFor(() => { + const metricOutsideRange = screen.queryByText('j'); + expect(metricOutsideRange).toBeNull(); + }); + }); + + it('shows results metrics per page chosen by the user', async () => { + setup(defaultQuery, listOfMetrics); + const resultsPerPageInput = screen.getByTestId(testIds.resultsPerPage); + await userEvent.type(resultsPerPageInput, '12'); + const metricInsideRange = screen.getByText('j'); + expect(metricInsideRange).toBeInTheDocument(); + }); + + it('paginates millions of metrics and does not run out of memory', async () => { + const millionsOfMetrics: string[] = [...Array(1000000).keys()].map((i) => '' + i); + setup(defaultQuery, millionsOfMetrics); + await waitFor(() => { + // doesn't break on loading + expect(screen.getByText('0')).toBeInTheDocument(); + }); + const resultsPerPageInput = screen.getByTestId(testIds.resultsPerPage); + // doesn't break on changing results per page + await userEvent.type(resultsPerPageInput, '11'); + const metricInsideRange = screen.getByText('10'); + expect(metricInsideRange).toBeInTheDocument(); + }); + + // Fuzzy search + it('searches and filter by metric name with a fuzzy search', async () => { + // search for a_bucket by name + setup(defaultQuery, listOfMetrics); + let metricAll: HTMLElement | null; + let metricABucket: HTMLElement | null; + await waitFor(() => { + metricAll = screen.getByText('all-metrics'); + metricABucket = screen.getByText('a_bucket'); + expect(metricAll).toBeInTheDocument(); + expect(metricABucket).toBeInTheDocument(); + }); + const searchMetric = screen.getByTestId(testIds.searchMetric); + expect(searchMetric).toBeInTheDocument(); + await userEvent.type(searchMetric, 'a_b'); + + await waitFor(() => { + metricABucket = screen.getByText('a_bucket'); + expect(metricABucket).toBeInTheDocument(); + metricAll = screen.queryByText('all-metrics'); + expect(metricAll).toBeNull(); + }); + }); + + it('searches by all metric metadata with a fuzzy search', async () => { + // search for a_bucket by metadata type counter but only type countt + setup(defaultQuery, listOfMetrics); + let metricABucket: HTMLElement | null; + + await waitFor(() => { + metricABucket = screen.getByText('a_bucket'); + expect(metricABucket).toBeInTheDocument(); + }); + + const metadataSwitch = screen.getByTestId(testIds.searchWithMetadata); + expect(metadataSwitch).toBeInTheDocument(); + await userEvent.click(metadataSwitch); + + const searchMetric = screen.getByTestId(testIds.searchMetric); + expect(searchMetric).toBeInTheDocument(); + await userEvent.type(searchMetric, 'countt'); + + await waitFor(() => { + metricABucket = screen.getByText('a_bucket'); + expect(metricABucket).toBeInTheDocument(); + }); + }); +}); + +const defaultQuery: PromVisualQuery = { + metric: 'random_metric', + labels: [], + operations: [], +}; + +const listOfMetrics: string[] = ['all-metrics', 'a_bucket', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j']; + +function createDatasource(metrics: string[], withLabels?: boolean) { + const languageProvider = new EmptyLanguageProviderMock() as unknown as PromQlLanguageProvider; + + // display different results if their are labels selected in the PromVisualQuery + if (withLabels) { + languageProvider.getSeries = () => Promise.resolve({ __name__: ['with-labels'] }); + languageProvider.metricsMetadata = { + 'with-labels': { + type: 'with-labels-type', + help: 'with-labels-help', + }, + }; + } else { + // all metrics + languageProvider.getLabelValues = () => Promise.resolve(metrics); + languageProvider.metricsMetadata = { + 'all-metrics': { + type: 'all-metrics-type', + help: 'all-metrics-help', + }, + a: { + type: 'counter', + help: 'a-metric-help', + }, + a_bucket: { + type: 'counter', + help: 'for functions', + }, + // missing metadata for other metrics is tested for, see below + }; + } + + const datasource = new PrometheusDatasource( + { + url: '', + jsonData: {}, + meta: {} as DataSourcePluginMeta, + } as DataSourceInstanceSettings, + undefined, + undefined, + languageProvider + ); + return datasource; +} + +function createProps(query: PromVisualQuery, datasource: PrometheusDatasource) { + return { + datasource, + isOpen: true, + onChange: jest.fn(), + onClose: jest.fn(), + query: query, + }; +} + +function setup(query: PromVisualQuery, metrics: string[], withlabels?: boolean) { + const withLabels: boolean = query.labels.length > 0; + const datasource = createDatasource(metrics, withLabels); + const props = createProps(query, datasource); + + // render the modal only + const { container } = render(); + + return container; +} diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/MetricEncyclopediaModal.tsx b/public/app/plugins/datasource/prometheus/querybuilder/components/MetricEncyclopediaModal.tsx new file mode 100644 index 00000000000..969a6135a4a --- /dev/null +++ b/public/app/plugins/datasource/prometheus/querybuilder/components/MetricEncyclopediaModal.tsx @@ -0,0 +1,735 @@ +import { css } from '@emotion/css'; +import uFuzzy from '@leeoniya/ufuzzy'; +import debounce from 'debounce-promise'; +import { debounce as debounceLodash } from 'lodash'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; + +import { GrafanaTheme2, SelectableValue } from '@grafana/data'; +import { reportInteraction } from '@grafana/runtime'; +import { + Button, + Card, + Collapse, + InlineField, + InlineLabel, + InlineSwitch, + Input, + Modal, + MultiSelect, + Select, + Spinner, + useStyles2, +} from '@grafana/ui'; + +import { PrometheusDatasource } from '../../datasource'; +import { getMetadataHelp, getMetadataType } from '../../language_provider'; +import { promQueryModeller } from '../PromQueryModeller'; +import { regexifyLabelValuesQueryString } from '../shared/parsingUtils'; +import { PromVisualQuery } from '../types'; + +type Props = { + datasource: PrometheusDatasource; + isOpen: boolean; + query: PromVisualQuery; + onClose: () => void; + onChange: (query: PromVisualQuery) => void; +}; + +type MetricsData = MetricData[]; + +type MetricData = { + value: string; + type?: string; + description?: string; +}; + +type PromFilterOption = { + value: string; + description: string; +}; + +const promTypes: PromFilterOption[] = [ + { + value: 'counter', + description: + 'A cumulative metric that represents a single monotonically increasing counter whose value can only increase or be reset to zero on restart.', + }, + { + value: 'gauge', + description: 'A metric that represents a single numerical value that can arbitrarily go up and down.', + }, + { + value: 'histogram', + description: + 'A histogram samples observations (usually things like request durations or response sizes) and counts them in configurable buckets.', + }, + { + value: 'summary', + description: + 'A summary samples observations (usually things like request durations and response sizes) and can calculate configurable quantiles over a sliding time window.', + }, +]; + +export const placeholders = { + browse: 'Browse metric names by text', + metadataSearchSwicth: 'Browse by metadata type and description in addition to metric name', + type: 'Counter, gauge, histogram, or summary', + variables: 'Select a template variable for your metric', + excludeNoMetadata: 'Exclude results with no metadata when filtering', + setUseBackend: 'Use the backend to browse metrics and disable fuzzy search metadata browsing', +}; + +export const DEFAULT_RESULTS_PER_PAGE = 10; + +export const MetricEncyclopediaModal = (props: Props) => { + const uf = UseUfuzzy(); + + const { datasource, isOpen, onClose, onChange, query } = props; + + const [variables, setVariables] = useState>>([]); + + const [isLoading, setIsLoading] = useState(true); + + // metric list + const [metrics, setMetrics] = useState([]); + const [hasMetadata, setHasMetadata] = useState(true); + const [haystack, setHaystack] = useState([]); + const [nameHaystack, setNameHaystack] = useState([]); + const [openTabs, setOpenTabs] = useState([]); + + // pagination + const [resultsPerPage, setResultsPerPage] = useState(DEFAULT_RESULTS_PER_PAGE); + const [pageNum, setPageNum] = useState(1); + + // filters + const [fuzzySearchQuery, setFuzzySearchQuery] = useState(''); + const [fuzzyMetaSearchResults, setFuzzyMetaSearchResults] = useState([]); + const [fuzzyNameSearchResults, setNameFuzzySearchResults] = useState([]); + const [fullMetaSearch, setFullMetaSearch] = useState(false); + const [excludeNullMetadata, setExcludeNullMetadata] = useState(false); + const [selectedTypes, setSelectedTypes] = useState>>([]); + const [letterSearch, setLetterSearch] = useState(null); + + // backend search metric names by text + const [useBackend, setUseBackend] = useState(false); + + const updateMetricsMetadata = useCallback(async () => { + // *** Loading Gif + setIsLoading(true); + + // Makes sure we loaded the metadata for metrics. Usually this is done in the start() method of the provider but we + // don't use it with the visual builder and there is no need to run all the start() setup anyway. + if (!datasource.languageProvider.metricsMetadata) { + await datasource.languageProvider.loadMetricsMetadata(); + } + + // Error handling for when metrics metadata returns as undefined + // *** Will have to handle metadata filtering if this happens + // *** only display metrics fuzzy search, filter and pagination + if (!datasource.languageProvider.metricsMetadata) { + setHasMetadata(false); + datasource.languageProvider.metricsMetadata = {}; + } + + // filter by adding the query.labels to the search? + // *** do this in the filter??? + let metrics; + if (query.labels.length > 0) { + const expr = promQueryModeller.renderLabels(query.labels); + metrics = (await datasource.languageProvider.getSeries(expr, true))['__name__'] ?? []; + } else { + metrics = (await datasource.languageProvider.getLabelValues('__name__')) ?? []; + } + + let haystackData: string[] = []; + let haystackNameData: string[] = []; + let metricsData: MetricsData = metrics.map((m) => { + const type = getMetadataType(m, datasource.languageProvider.metricsMetadata!); + const description = getMetadataHelp(m, datasource.languageProvider.metricsMetadata!); + + // string[] = name + type + description + haystackData.push(`${m} ${type} ${description}`); + haystackNameData.push(m); + return { + value: m, + type: type, + description: description, + }; + }); + + // setting this by the backend if useBackend is true + setMetrics(metricsData); + setHaystack(haystackData); + setNameHaystack(haystackNameData); + + setVariables( + datasource.getVariables().map((v) => { + return { + value: v, + label: v, + }; + }) + ); + + setIsLoading(false); + }, [query, datasource]); + + useEffect(() => { + updateMetricsMetadata(); + }, [updateMetricsMetadata]); + + const styles = useStyles2(getStyles); + + const typeOptions: SelectableValue[] = promTypes.map((t: PromFilterOption) => { + return { + value: t.value, + label: t.value, + description: t.description, + }; + }); + + function calculatePageList(metrics: MetricsData, resultsPerPage: number) { + if (!metrics.length) { + return []; + } + + const calcResultsPerPage: number = resultsPerPage === 0 ? 1 : resultsPerPage; + + const pages = Math.floor(filterMetrics(metrics).length / calcResultsPerPage) + 1; + + return [...Array(pages).keys()].map((i) => i + 1); + } + + function sliceMetrics(metrics: MetricsData, pageNum: number, resultsPerPage: number) { + const calcResultsPerPage: number = resultsPerPage === 0 ? 1 : resultsPerPage; + const start: number = pageNum === 1 ? 0 : (pageNum - 1) * calcResultsPerPage; + const end: number = start + calcResultsPerPage; + return metrics.slice(start, end); + } + + function hasMetaDataFilters() { + return selectedTypes.length > 0; + } + + function fuzzySearch(query: string) { + // search either the names or all metadata + // fuzzy search go! + + if (fullMetaSearch) { + // considered simply filtering indexes with reduce and includes + // Performance comparison with 13,000 metrics searching metadata + // Fuzzy 6326ms + // Reduce & Includes 5541ms + const metaIdxs = uf.filter(haystack, query.toLowerCase()); + setFuzzyMetaSearchResults(metaIdxs); + } else { + const nameIdxs = uf.filter(nameHaystack, query.toLowerCase()); + setNameFuzzySearchResults(nameIdxs); + } + } + + const debouncedFuzzySearch = debounceLodash((query: string) => { + fuzzySearch(query); + }, 300); + + /** + * Filter + * + * @param metrics + * @param skipLetterSearch + * @returns + */ + function filterMetrics(metrics: MetricsData, skipLetterSearch?: boolean): MetricsData { + let filteredMetrics: MetricsData = metrics; + + if (fuzzySearchQuery || excludeNullMetadata || (letterSearch && !skipLetterSearch) || selectedTypes.length > 0) { + filteredMetrics = filteredMetrics.filter((m: MetricData, idx) => { + let keepMetric = false; + + // search by text + if (fuzzySearchQuery) { + if (useBackend) { + // skip for backend! + keepMetric = true; + } else if (fullMetaSearch) { + keepMetric = fuzzyMetaSearchResults.includes(idx); + } else { + keepMetric = fuzzyNameSearchResults.includes(idx); + } + } + + // user clicks the alphabet search + // backend and frontend + if (letterSearch && !skipLetterSearch) { + const letters: string[] = [letterSearch, letterSearch.toLowerCase()]; + keepMetric = letters.includes(m.value[0]); + } + + // select by type, counter, gauge, etc + // skip for backend because no metadata is returned + if (selectedTypes.length > 0 && !useBackend) { + // return the metric that matches the type + // return the metric if it has no type AND we are NOT excluding metrics without metadata + + // Matches type + const matchesSelectedType = selectedTypes.some((t) => t.value === m.type); + + // missing type + const hasNoType = !m.type; + + return matchesSelectedType || (hasNoType && !excludeNullMetadata); + } + + return keepMetric; + }); + } + + return filteredMetrics; + } + + /** + * The filtered and paginated metrics displayed in the modal + * */ + function displayedMetrics(metrics: MetricsData) { + const filteredSorted: MetricsData = filterMetrics(metrics).sort(alphabetically(true, hasMetaDataFilters())); + + const displayedMetrics: MetricsData = sliceMetrics(filteredSorted, pageNum, resultsPerPage); + + return displayedMetrics; + } + /** + * The backend debounced search + */ + const debouncedBackendSearch = useMemo( + () => + debounce(async (metricText: string) => { + const queryString = regexifyLabelValuesQueryString(metricText); + + const labelsParams = query.labels.map((label) => { + return `,${label.label}="${label.value}"`; + }); + + const params = `label_values({__name__=~".*${queryString}"${ + query.labels ? labelsParams.join() : '' + }},__name__)`; + + const results = datasource.metricFindQuery(params); + + const metrics = await results.then((results) => { + return results.map((result) => { + return { + value: result.text, + }; + }); + }); + + setMetrics(metrics); + setIsLoading(false); + }, 300), + [datasource, query.labels] + ); + + return ( + +
+ Browse {metrics.length} metric{metrics.length > 1 ? 's' : ''} by text, by type, alphabetically or select a + variable. + {isLoading && ( +
+ +
+ )} +
+ {query.labels.length > 0 && ( +
+ These metrics have been pre-filtered by labels chosen in the label filters. +
+ )} +
+ { + const value = e.currentTarget.value ?? ''; + setFuzzySearchQuery(value); + if (useBackend && value === '') { + // get all metrics data if a user erases everything in the input + updateMetricsMetadata(); + } else if (useBackend) { + setIsLoading(true); + debouncedBackendSearch(value); + } else { + // do the search on the frontend + debouncedFuzzySearch(value); + } + + setPageNum(1); + }} + /> + {hasMetadata && !useBackend && ( + {placeholders.metadataSearchSwicth}
}> + { + setFullMetaSearch(!fullMetaSearch); + setPageNum(1); + }} + /> + + )} + {placeholders.setUseBackend}}> + { + const newVal = !useBackend; + setUseBackend(newVal); + if (newVal === false) { + // rebuild the metrics metadata if we turn off useBackend + updateMetricsMetadata(); + } else { + // check if there is text in the browse search and update + if (fuzzySearchQuery !== '') { + debouncedBackendSearch(fuzzySearchQuery); + } + // otherwise wait for user typing + } + + setPageNum(1); + }} + /> + + + {hasMetadata && !useBackend && ( + <> +
+
Filter by Type
+
+
+ { + // *** Filter by type + // *** always include metrics without metadata but label it as unknown type + // Consider tabs select instead of actual select or multi select + setSelectedTypes(v); + setPageNum(1); + }} + /> + {hasMetadata && ( + {placeholders.excludeNoMetadata}
}> + { + setExcludeNullMetadata(!excludeNullMetadata); + setPageNum(1); + }} + /> + + )} + + + )} +
+
Variables
+
+
+ { + return { value: p, label: '' + p }; + })} + value={pageNum ?? 1} + placeholder="select page" + onChange={(e) => { + const value = e.value ?? 1; + setPageNum(value); + }} + /> + + # results per page + + { + const value = +e.currentTarget.value; + + if (isNaN(value)) { + return; + } + + setResultsPerPage(value); + }} + /> +
+
+ +
+ ); +}; + +function alphabetically(ascending: boolean, metadataFilters: boolean) { + return function (a: MetricData, b: MetricData) { + // equal items sort equally + if (a.value === b.value) { + return 0; + } + + // *** NO METADATA? SORT LAST + // undefined metadata sort after anything else + // if filters are on + if (metadataFilters) { + if (a.type === undefined) { + return 1; + } + if (b.type === undefined) { + return -1; + } + } + + // otherwise, if we're ascending, lowest sorts first + if (ascending) { + return a.value < b.value ? -1 : 1; + } + + // if descending, highest sorts first + return a.value < b.value ? 1 : -1; + }; +} + +function UseUfuzzy(): uFuzzy { + const ref = useRef(); + + if (!ref.current) { + ref.current = new uFuzzy({ + intraMode: 1, + intraIns: 1, + intraSub: 1, + intraTrn: 1, + intraDel: 1, + }); + } + + return ref.current; +} + +const getStyles = (theme: GrafanaTheme2) => { + return { + cardsContainer: css` + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: space-between; + `, + spacing: css` + margin-bottom: ${theme.spacing(1)}; + `, + center: css` + text-align: center; + padding: 4px; + width: 100%; + `, + topPadding: css` + padding: 10px 0 0 0; + `, + bottomPadding: css` + padding: 0 0 4px 0; + `, + card: css` + width: 100%; + display: flex; + flex-direction: column; + `, + selAlpha: css` + font-style: italic; + cursor: pointer; + color: #6e9fff; + `, + active: css` + cursor: pointer; + `, + gray: css` + color: grey; + `, + metadata: css` + color: rgb(204, 204, 220); + `, + labelColor: css` + color: #6e9fff; + `, + inlineSpinner: css` + display: inline-block; + `, + }; +}; + +export const testIds = { + metricModal: 'metric-modal', + searchMetric: 'search-metric', + searchWithMetadata: 'search-with-metadata', + selectType: 'select-type', + metricCard: 'metric-card', + useMetric: 'use-metric', + searchPage: 'search-page', + resultsPerPage: 'results-per-page', + setUseBackend: 'set-use-backend', +}; diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilder.tsx b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilder.tsx index 78e54fe840d..5fab120928d 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilder.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilder.tsx @@ -1,7 +1,10 @@ +import { css } from '@emotion/css'; import React, { useCallback, useState } from 'react'; -import { DataSourceApi, PanelData, SelectableValue } from '@grafana/data'; +import { DataSourceApi, GrafanaTheme2, PanelData, SelectableValue } from '@grafana/data'; import { EditorRow } from '@grafana/experimental'; +import { config } from '@grafana/runtime'; +import { Button, Tag, useStyles2 } from '@grafana/ui'; import { PrometheusDatasource } from '../../datasource'; import { getMetadataString } from '../../language_provider'; @@ -19,6 +22,7 @@ import { QueryBuilderLabelFilter, QueryBuilderOperation } from '../shared/types' import { PromVisualQuery } from '../types'; import { LabelFilters } from './LabelFilters'; +import { MetricEncyclopediaModal } from './MetricEncyclopediaModal'; import { MetricSelect, PROMETHEUS_QUERY_BUILDER_MAX_RESULTS } from './MetricSelect'; import { NestedQueryList } from './NestedQueryList'; import { EXPLAIN_LABEL_FILTER_CONTENT } from './PromQueryBuilderExplained'; @@ -35,10 +39,12 @@ export interface Props { export const PromQueryBuilder = React.memo((props) => { const { datasource, query, onChange, onRunQuery, data, showExplain } = props; const [highlightedOp, setHighlightedOp] = useState(); + const [metricEncyclopediaModalOpen, setMetricEncyclopediaModalOpen] = useState(false); const onChangeLabels = (labels: QueryBuilderLabelFilter[]) => { onChange({ ...query, labels }); }; + const styles = useStyles2(getStyles); /** * Map metric metadata to SelectableValue for Select component and also adds defined template variables to the list. */ @@ -202,17 +208,51 @@ export const PromQueryBuilder = React.memo((props) => { }, [datasource, query, withTemplateVariableOptions]); const lang = { grammar: promqlGrammar, name: 'promql' }; + const MetricEncyclopedia = config.featureToggles.prometheusMetricEncyclopedia; return ( <> - + {MetricEncyclopedia ? ( + <> + + {query.metric && ( + { + onChange({ ...query, metric: '' }); + }} + title="Click to remove metric" + className={styles.metricTag} + /> + )} + {metricEncyclopediaModalOpen && ( + setMetricEncyclopediaModalOpen(false)} + query={query} + onChange={onChange} + /> + )} + + ) : ( + + )} { + return { + button: css` + height: auto; + `, + metricTag: css` + margin: '10px 0 10px 0', + backgroundColor: '#3D71D9', + `, + }; +}; From 1a5ab1b30820b6af26de421f1a8c78d842844f1d Mon Sep 17 00:00:00 2001 From: Ezequiel Victorero Date: Tue, 7 Mar 2023 16:42:24 -0300 Subject: [PATCH 041/288] PublicDashboards: Remove dev mode for share by email toggle (#64330) Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com> --- .../configure-grafana/feature-toggles/index.md | 18 +++++++++--------- pkg/services/featuremgmt/registry.go | 3 +-- pkg/services/featuremgmt/toggles_gen.go | 2 +- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index ce40c22e0d8..75a0e5119f9 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -59,6 +59,7 @@ Alpha features might be changed or removed without prior notice. | `live-service-web-worker` | This will use a webworker thread to processes events rather than the main thread | | `queryOverLive` | Use Grafana Live WebSocket to execute backend queries | | `publicDashboards` | Enables public access to dashboards | +| `publicDashboardsEmailSharing` | Enables public dashboard sharing to be restricted to only allowed emails | | `lokiLive` | Support WebSocket streaming for loki (early prototype) | | `lokiDataframeApi` | Use experimental loki api for WebSocket streaming (early prototype) | | `dashboardComments` | Enable dashboard-wide comments | @@ -98,12 +99,11 @@ Alpha features might be changed or removed without prior notice. The following toggles require explicitly setting Grafana's [app mode]({{< relref "../_index.md/#app_mode" >}}) to 'development' before you can enable this feature toggle. These features tend to be experimental. -| Feature toggle name | Description | -| ------------------------------ | ----------------------------------------------------------------------- | -| `publicDashboardsEmailSharing` | Allows public dashboard sharing to be restricted to only allowed emails | -| `k8s` | Explore native k8s integrations | -| `dashboardsFromStorage` | Load dashboards from the generic storage interface | -| `grpcServer` | Run GRPC server | -| `entityStore` | SQL-based entity store (requires storage flag also) | -| `queryLibrary` | Reusable query library | -| `nestedFolders` | Enable folder nesting | +| Feature toggle name | Description | +| ----------------------- | --------------------------------------------------- | +| `k8s` | Explore native k8s integrations | +| `dashboardsFromStorage` | Load dashboards from the generic storage interface | +| `grpcServer` | Run GRPC server | +| `entityStore` | SQL-based entity store (requires storage flag also) | +| `queryLibrary` | Reusable query library | +| `nestedFolders` | Enable folder nesting | diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 722d9218855..b8376bbd187 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -74,10 +74,9 @@ var ( }, { Name: "publicDashboardsEmailSharing", - Description: "Allows public dashboard sharing to be restricted to only allowed emails", + Description: "Enables public dashboard sharing to be restricted to only allowed emails", State: FeatureStateAlpha, RequiresLicense: true, - RequiresDevMode: true, Owner: grafanaDashboardsSquad, }, { diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 85f7c502304..b1541eb9369 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -52,7 +52,7 @@ const ( FlagPublicDashboards = "publicDashboards" // FlagPublicDashboardsEmailSharing - // Allows public dashboard sharing to be restricted to only allowed emails + // Enables public dashboard sharing to be restricted to only allowed emails FlagPublicDashboardsEmailSharing = "publicDashboardsEmailSharing" // FlagLokiLive From 70f600db1036a1032702a41d7349ea193c765f37 Mon Sep 17 00:00:00 2001 From: Kevin Yu Date: Tue, 7 Mar 2023 14:35:19 -0800 Subject: [PATCH 042/288] Cloudwatch Logs: Make mixed type fields fallback to being strings (#63981) * Cloudwatch Logs: make mixed type fields fallback to being strings * addressing pr comments --- pkg/tsdb/cloudwatch/log_query.go | 20 +++++++++- pkg/tsdb/cloudwatch/log_query_test.go | 56 +++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/pkg/tsdb/cloudwatch/log_query.go b/pkg/tsdb/cloudwatch/log_query.go index 6de78a1c246..7bd4a5390b4 100644 --- a/pkg/tsdb/cloudwatch/log_query.go +++ b/pkg/tsdb/cloudwatch/log_query.go @@ -73,9 +73,14 @@ func logsResultsToDataframes(response *cloudwatchlogs.GetQueryResultsOutput) (*d timeField[i] = &parsedTime } else if numericField, ok := fieldValues[*resultField.Field].([]*float64); ok { parsedFloat, err := strconv.ParseFloat(*resultField.Value, 64) + if err != nil { - return nil, err + // This can happen if a field has a mix of numeric and non-numeric values. + // In that case, we change the field from a numeric field to a string field. + fieldValues[*resultField.Field] = changeToStringField(rowCount, nonEmptyRows[:i+1], *resultField.Field) + continue } + numericField[i] = &parsedFloat } else { fieldValues[*resultField.Field].([]*string)[i] = resultField.Value @@ -145,6 +150,19 @@ func logsResultsToDataframes(response *cloudwatchlogs.GetQueryResultsOutput) (*d return frame, nil } +func changeToStringField(lengthOfValues int, rows [][]*cloudwatchlogs.ResultField, logEventField string) []*string { + fieldValuesAsStrings := make([]*string, lengthOfValues) + for i, resultFields := range rows { + for _, field := range resultFields { + if *field.Field == logEventField { + fieldValuesAsStrings[i] = field.Value + } + } + } + + return fieldValuesAsStrings +} + func groupResults(results *data.Frame, groupingFieldNames []string) ([]*data.Frame, error) { groupingFields := make([]*data.Field, 0) diff --git a/pkg/tsdb/cloudwatch/log_query_test.go b/pkg/tsdb/cloudwatch/log_query_test.go index ab387289e80..ed9be9b6d7c 100644 --- a/pkg/tsdb/cloudwatch/log_query_test.go +++ b/pkg/tsdb/cloudwatch/log_query_test.go @@ -221,6 +221,62 @@ func TestLogsResultsToDataframes(t *testing.T) { assert.ElementsMatch(t, expectedDataframe.Fields, dataframes.Fields) } +func TestLogsResultsToDataframes_MixedTypes_NumericValuesMixedWithStringFallBackToStringValues(t *testing.T) { + dataframes, err := logsResultsToDataframes(&cloudwatchlogs.GetQueryResultsOutput{ + Results: [][]*cloudwatchlogs.ResultField{ + { + &cloudwatchlogs.ResultField{ + Field: aws.String("numberOrString"), + Value: aws.String("-1.234"), + }, + }, + { + &cloudwatchlogs.ResultField{ + Field: aws.String("numberOrString"), + Value: aws.String("1"), + }, + }, + { + &cloudwatchlogs.ResultField{ + Field: aws.String("numberOrString"), + Value: aws.String("not a number"), + }, + }, + { + &cloudwatchlogs.ResultField{ + Field: aws.String("numberOrString"), + Value: aws.String("2.000"), + }, + }, + }, + Status: aws.String("ok"), + }) + require.NoError(t, err) + + expectedDataframe := &data.Frame{ + Name: "CloudWatchLogsResponse", + Fields: []*data.Field{ + data.NewField("numberOrString", nil, []*string{ + aws.String("-1.234"), + aws.String("1"), + aws.String("not a number"), + aws.String("2.000"), + }), + }, + RefID: "", + Meta: &data.FrameMeta{ + Custom: map[string]interface{}{ + "Status": "ok", + }, + }, + } + + assert.Equal(t, expectedDataframe.Name, dataframes.Name) + assert.Equal(t, expectedDataframe.RefID, dataframes.RefID) + assert.Equal(t, expectedDataframe.Meta, dataframes.Meta) + assert.ElementsMatch(t, expectedDataframe.Fields, dataframes.Fields) +} + func TestGroupKeyGeneration(t *testing.T) { logField := data.NewField("@log", data.Labels{}, []*string{ aws.String("fakelog-a"), From 05191d083d3c55d7d3f43ccab76df7142ccc4fb7 Mon Sep 17 00:00:00 2001 From: Timur Olzhabayev Date: Wed, 8 Mar 2023 06:57:50 +0100 Subject: [PATCH 043/288] Chore: Adding backend platform squad owned feature toggles (#64351) Adding backend platform squad owned feature toggles --- pkg/services/featuremgmt/registry.go | 3 +++ pkg/services/featuremgmt/toggles_gen_test.go | 3 --- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index b8376bbd187..6803df1bf72 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -111,6 +111,7 @@ var ( Name: "migrationLocking", Description: "Lock database during migrations", State: FeatureStateBeta, + Owner: grafanaBackendPlatformSquad, }, { Name: "storage", @@ -294,6 +295,7 @@ var ( Name: "mysqlAnsiQuotes", Description: "Use double quotes to escape keyword in a MySQL query", State: FeatureStateAlpha, + Owner: grafanaBackendPlatformSquad, }, { Name: "accessControlOnCall", @@ -305,6 +307,7 @@ var ( Description: "Enable folder nesting", State: FeatureStateAlpha, RequiresDevMode: true, + Owner: grafanaBackendPlatformSquad, }, { Name: "accessTokenExpirationCheck", diff --git a/pkg/services/featuremgmt/toggles_gen_test.go b/pkg/services/featuremgmt/toggles_gen_test.go index 0637408e794..455787a946f 100644 --- a/pkg/services/featuremgmt/toggles_gen_test.go +++ b/pkg/services/featuremgmt/toggles_gen_test.go @@ -49,7 +49,6 @@ func TestFeatureToggleFiles(t *testing.T) { "prometheusAzureOverrideAudience": true, "lokiDataframeApi": true, "featureHighlights": true, - "migrationLocking": true, "exploreMixedDatasource": true, "tracing": true, "newTraceView": true, @@ -66,9 +65,7 @@ func TestFeatureToggleFiles(t *testing.T) { "athenaAsyncQueryDataSupport": true, "newPanelChromeUI": true, "showDashboardValidationWarnings": true, - "mysqlAnsiQuotes": true, "accessControlOnCall": true, - "nestedFolders": true, "accessTokenExpirationCheck": true, "elasticsearchBackendMigration": true, "datasourceOnboarding": true, From 43095d84e4facdf583eb0ae2dbab895d9e7f86c1 Mon Sep 17 00:00:00 2001 From: Sofia Papagiannaki <1632407+papagian@users.noreply.github.com> Date: Wed, 8 Mar 2023 10:12:02 +0200 Subject: [PATCH 044/288] Snapshots: Fix deleting snapshot with non existent dashboard ID (#64345) * Add test for deleting snapshot for non existent dashboard * Add test for failure to fetch guardian other than ErrDashboardNotFound * Fix dashboard snapshot delete --- pkg/api/dashboard_snapshot.go | 22 +++++++------- pkg/api/dashboard_snapshot_test.go | 46 ++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 10 deletions(-) diff --git a/pkg/api/dashboard_snapshot.go b/pkg/api/dashboard_snapshot.go index a367c69dd5c..3dbe4e08657 100644 --- a/pkg/api/dashboard_snapshot.go +++ b/pkg/api/dashboard_snapshot.go @@ -366,17 +366,19 @@ func (hs *HTTPServer) DeleteDashboardSnapshot(c *contextmodel.ReqContext) respon if dashboardID != 0 { g, err := guardian.New(c.Req.Context(), dashboardID, c.OrgID, c.SignedInUser) if err != nil { - return response.Err(err) - } + if !errors.Is(err, dashboards.ErrDashboardNotFound) { + return response.Err(err) + } + } else { + canEdit, err := g.CanEdit() + // check for permissions only if the dashboard is found + if err != nil && !errors.Is(err, dashboards.ErrDashboardNotFound) { + return response.Error(http.StatusInternalServerError, "Error while checking permissions for snapshot", err) + } - canEdit, err := g.CanEdit() - // check for permissions only if the dashboard is found - if err != nil && !errors.Is(err, dashboards.ErrDashboardNotFound) { - return response.Error(http.StatusInternalServerError, "Error while checking permissions for snapshot", err) - } - - if !canEdit && queryResult.UserID != c.SignedInUser.UserID && !errors.Is(err, dashboards.ErrDashboardNotFound) { - return response.Error(http.StatusForbidden, "Access denied to this snapshot", nil) + if !canEdit && queryResult.UserID != c.SignedInUser.UserID && !errors.Is(err, dashboards.ErrDashboardNotFound) { + return response.Error(http.StatusForbidden, "Access denied to this snapshot", nil) + } } } diff --git a/pkg/api/dashboard_snapshot_test.go b/pkg/api/dashboard_snapshot_test.go index 9512506ef7c..5cede0b7c58 100644 --- a/pkg/api/dashboard_snapshot_test.go +++ b/pkg/api/dashboard_snapshot_test.go @@ -21,6 +21,7 @@ import ( "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/team/teamtest" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/util/errutil" ) func TestDashboardSnapshotAPIEndpoint_singleSnapshot(t *testing.T) { @@ -124,6 +125,51 @@ func TestDashboardSnapshotAPIEndpoint_singleSnapshot(t *testing.T) { } dashSvc.On("GetDashboardACLInfoList", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardACLInfoListQuery")).Return(qResult, nil) + loggedInUserScenarioWithRole(t, "Should not be able to delete a snapshot when fetching guardian fails during calling DELETE on", "DELETE", + "/api/snapshots/12345", "/api/snapshots/:key", org.RoleEditor, func(sc *scenarioContext) { + ts := setupRemoteServer(func(rw http.ResponseWriter, req *http.Request) { + rw.WriteHeader(200) + }) + dashSvc := dashboards.NewFakeDashboardService(t) + dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Return(nil, errutil.Error{PublicMessage: "some error"}).Maybe() + + guardian.InitLegacyGuardian(sc.sqlStore, dashSvc, teamSvc) + d := setUpSnapshotTest(t, 0, ts.URL) + hs := buildHttpServer(d, true) + hs.DashboardService = dashSvc + sc.handlerFunc = hs.DeleteDashboardSnapshot + sc.fakeReqWithParams("DELETE", sc.url, map[string]string{"key": "12345"}).exec() + + assert.Equal(t, http.StatusInternalServerError, sc.resp.Code) + }, sqlmock) + + loggedInUserScenarioWithRole(t, "Should be able to delete a snapshot from a deleted dashboard when calling DELETE on", "DELETE", + "/api/snapshots/12345", "/api/snapshots/:key", org.RoleEditor, func(sc *scenarioContext) { + var externalRequest *http.Request + ts := setupRemoteServer(func(rw http.ResponseWriter, req *http.Request) { + rw.WriteHeader(200) + externalRequest = req + }) + dashSvc := dashboards.NewFakeDashboardService(t) + dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Return(nil, dashboards.ErrDashboardNotFound).Maybe() + + guardian.InitLegacyGuardian(sc.sqlStore, dashSvc, teamSvc) + d := setUpSnapshotTest(t, 0, ts.URL) + hs := buildHttpServer(d, true) + hs.DashboardService = dashSvc + sc.handlerFunc = hs.DeleteDashboardSnapshot + sc.fakeReqWithParams("DELETE", sc.url, map[string]string{"key": "12345"}).exec() + + assert.Equal(t, 200, sc.resp.Code) + respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes()) + require.NoError(t, err) + + assert.True(t, strings.HasPrefix(respJSON.Get("message").MustString(), "Snapshot deleted")) + assert.Equal(t, 1, respJSON.Get("id").MustInt()) + assert.Equal(t, ts.URL, fmt.Sprintf("http://%s", externalRequest.Host)) + assert.Equal(t, "/", externalRequest.URL.EscapedPath()) + }, sqlmock) + loggedInUserScenarioWithRole(t, "Should be able to delete a snapshot when calling DELETE on", "DELETE", "/api/snapshots/12345", "/api/snapshots/:key", org.RoleEditor, func(sc *scenarioContext) { var externalRequest *http.Request From ee608c2582ceb86277363bc35ea2da5e4b3191f6 Mon Sep 17 00:00:00 2001 From: Karl Persson Date: Wed, 8 Mar 2023 09:49:02 +0100 Subject: [PATCH 045/288] Chore: Set authnz squad as owner of our feature toggles (#64373) FeatureToggles: Set authnz squad as owner of our feature toggles --- pkg/services/featuremgmt/codeowners.go | 1 + pkg/services/featuremgmt/registry.go | 3 +++ pkg/services/featuremgmt/toggles_gen_test.go | 3 --- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/pkg/services/featuremgmt/codeowners.go b/pkg/services/featuremgmt/codeowners.go index 3760429e84d..7efd1387316 100644 --- a/pkg/services/featuremgmt/codeowners.go +++ b/pkg/services/featuremgmt/codeowners.go @@ -12,4 +12,5 @@ const ( grafanaUserEssentialsSquad codeowner = "@grafana/user-essentials" grafanaBackendPlatformSquad codeowner = "@grafana/backend-platform" grafanaPluginsPlatformSquad codeowner = "@grafana/plugins-platform-backend" + grafanaAuthnzSquad codeowner = "@grafana/grafana-authnz-team" ) diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 6803df1bf72..bfd6486d37b 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -301,6 +301,7 @@ var ( Name: "accessControlOnCall", Description: "Access control primitives for OnCall", State: FeatureStateBeta, + Owner: grafanaAuthnzSquad, }, { Name: "nestedFolders", @@ -313,6 +314,7 @@ var ( Name: "accessTokenExpirationCheck", Description: "Enable OAuth access_token expiration check and token refresh using the refresh_token", State: FeatureStateStable, + Owner: grafanaAuthnzSquad, }, { Name: "elasticsearchBackendMigration", @@ -333,6 +335,7 @@ var ( Name: "authnService", Description: "Use new auth service to perform authentication", State: FeatureStateAlpha, + Owner: grafanaAuthnzSquad, }, { Name: "disablePrometheusExemplarSampling", diff --git a/pkg/services/featuremgmt/toggles_gen_test.go b/pkg/services/featuremgmt/toggles_gen_test.go index 455787a946f..efce45e3a90 100644 --- a/pkg/services/featuremgmt/toggles_gen_test.go +++ b/pkg/services/featuremgmt/toggles_gen_test.go @@ -65,12 +65,9 @@ func TestFeatureToggleFiles(t *testing.T) { "athenaAsyncQueryDataSupport": true, "newPanelChromeUI": true, "showDashboardValidationWarnings": true, - "accessControlOnCall": true, - "accessTokenExpirationCheck": true, "elasticsearchBackendMigration": true, "datasourceOnboarding": true, "secureSocksDatasourceProxy": true, - "authnService": true, "disablePrometheusExemplarSampling": true, "alertingBacktesting": true, "alertingNoNormalState": true, From 96956d825235d9b63986aa2dd8c612f0d3700cbb Mon Sep 17 00:00:00 2001 From: Jack Baldry Date: Wed, 8 Mar 2023 09:00:58 +0000 Subject: [PATCH 046/288] Trigger PR commands on labeled events (#64377) This should enable adding PRs to organization project boards similar to with issues. Signed-off-by: Jack Baldry --- .github/workflows/pr-commands.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/pr-commands.yml b/.github/workflows/pr-commands.yml index ecd7361c550..9a882dbf3d9 100644 --- a/.github/workflows/pr-commands.yml +++ b/.github/workflows/pr-commands.yml @@ -2,6 +2,7 @@ name: PR automation on: pull_request_target: types: + - labeled - opened - synchronize concurrency: From a317e48de82e0cf15a4504500abe9fd890ac1252 Mon Sep 17 00:00:00 2001 From: Jack Westbrook Date: Wed, 8 Mar 2023 10:17:26 +0100 Subject: [PATCH 047/288] Bug: Fix support for Node 18 in grafana/e2e package (#63446) * fix(e2e): replace resolve-as-bin for node 18 support * chore(yarn): refresh lock file --- packages/grafana-e2e/cli.js | 4 ++-- packages/grafana-e2e/package.json | 2 +- yarn.lock | 21 ++++++++++++++------- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/packages/grafana-e2e/cli.js b/packages/grafana-e2e/cli.js index af51c9b31b1..b35338a9195 100644 --- a/packages/grafana-e2e/cli.js +++ b/packages/grafana-e2e/cli.js @@ -1,7 +1,7 @@ const { program } = require('commander'); const execa = require('execa'); const { resolve, sep } = require('path'); -const resolveBin = require('resolve-as-bin'); +const resolveBin = require('resolve-bin'); const cypress = (commandName, { updateScreenshots, browser }) => { // Support running an unpublished dev build @@ -25,7 +25,7 @@ const cypress = (commandName, { updateScreenshots, browser }) => { stdio: 'inherit', }; - return execa(resolveBin('cypress'), cypressOptions, execaOptions) + return execa(resolveBin.sync('cypress'), cypressOptions, execaOptions) .then(() => {}) // no return value .catch((error) => { console.error(error.message); diff --git a/packages/grafana-e2e/package.json b/packages/grafana-e2e/package.json index 94121359c53..192b25ff710 100644 --- a/packages/grafana-e2e/package.json +++ b/packages/grafana-e2e/package.json @@ -76,7 +76,7 @@ "execa": "5.1.1", "lodash": "4.17.21", "mocha": "10.2.0", - "resolve-as-bin": "2.1.0", + "resolve-bin": "1.0.1", "rimraf": "4.2.0", "tracelib": "1.0.1", "ts-loader": "8.4.0", diff --git a/yarn.lock b/yarn.lock index 7d838abce8a..d86bf27774c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4981,7 +4981,7 @@ __metadata: execa: 5.1.1 lodash: 4.17.21 mocha: 10.2.0 - resolve-as-bin: 2.1.0 + resolve-bin: 1.0.1 rimraf: 4.2.0 rollup: 2.79.1 rollup-plugin-dts: ^5.0.0 @@ -16866,7 +16866,7 @@ __metadata: languageName: node linkType: hard -"cross-spawn@npm:^6.0.0, cross-spawn@npm:^6.0.5": +"cross-spawn@npm:^6.0.0": version: 6.0.5 resolution: "cross-spawn@npm:6.0.5" dependencies: @@ -20904,6 +20904,13 @@ __metadata: languageName: node linkType: hard +"find-parent-dir@npm:~0.3.0": + version: 0.3.1 + resolution: "find-parent-dir@npm:0.3.1" + checksum: 55e722584760cfbc6611901c7ced5081345cf629e2ecd6a4f6704b13b5a1876c8d9d9db5fd4965ba23e1ecbc24a8b62af40379cfef1ffa0231719b9d924eebdd + languageName: node + linkType: hard + "find-root@npm:^1.1.0": version: 1.1.0 resolution: "find-root@npm:1.1.0" @@ -34231,12 +34238,12 @@ __metadata: languageName: node linkType: hard -"resolve-as-bin@npm:2.1.0": - version: 2.1.0 - resolution: "resolve-as-bin@npm:2.1.0" +"resolve-bin@npm:1.0.1": + version: 1.0.1 + resolution: "resolve-bin@npm:1.0.1" dependencies: - cross-spawn: ^6.0.5 - checksum: 38cbdc57d50162a6aabca34762932637e9926a5b28e313e2027aa54132fe9894d57a9df061596b4be684b0320ee7ba0aef4537bb3919ee6df753fdc4a8b38222 + find-parent-dir: ~0.3.0 + checksum: 439678433138cbea49b848be68ae24aa2afa1b3bb82c30fa763ebbc5615e8c340debfe635ee902cdbb4be72528e4139e2adb0503370ee755ad3702d03976af4c languageName: node linkType: hard From 523f675dd870e3912f97f0c23dcee099e08da8c3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 8 Mar 2023 09:38:06 +0000 Subject: [PATCH 048/288] Update Monthly patch updates (#64323) * Update Monthly patch updates * don't update dangerously-set-html-content * run prettier * fix types --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Ashley Harrison --- package.json | 122 +- packages/grafana-data/package.json | 18 +- packages/grafana-data/src/types/data.ts | 2 +- packages/grafana-e2e-selectors/package.json | 2 +- packages/grafana-e2e/package.json | 12 +- packages/grafana-runtime/package.json | 4 +- packages/grafana-ui/package.json | 28 +- .../QueryOperationRowHeader.tsx | 4 +- .../GrafanaJavascriptAgentBackend.test.ts | 4 + .../ConfigPublicDashboard.tsx | 4 +- .../app/features/explore/LogsSamplePanel.tsx | 2 +- .../components/model/link-patterns.tsx | 2 +- .../state/reducer.test.ts | 2 +- .../state/reducer.test.ts | 6 +- .../querybuilder/shared/OperationHeader.tsx | 4 +- .../panel/graph/Legend/LegendSeriesItem.tsx | 2 +- public/app/types/explore.ts | 4 +- yarn.lock | 1283 +++++++++-------- 18 files changed, 806 insertions(+), 699 deletions(-) diff --git a/package.json b/package.json index 38ab8c041f8..d02e59f58e5 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,7 @@ "@babel/core": "7.20.5", "@babel/plugin-proposal-class-properties": "7.18.6", "@babel/plugin-proposal-nullish-coalescing-operator": "7.18.6", - "@babel/plugin-proposal-object-rest-spread": "7.20.2", + "@babel/plugin-proposal-object-rest-spread": "7.20.7", "@babel/plugin-proposal-optional-chaining": "7.20.7", "@babel/plugin-syntax-dynamic-import": "7.8.3", "@babel/plugin-transform-react-constant-elements": "7.20.2", @@ -107,14 +107,14 @@ "@grafana/eslint-plugin": "link:./packages/grafana-eslint-rules", "@grafana/toolkit": "workspace:*", "@grafana/tsconfig": "^1.2.0-rc1", - "@pmmmwh/react-refresh-webpack-plugin": "0.5.8", - "@react-types/button": "3.7.0", + "@pmmmwh/react-refresh-webpack-plugin": "0.5.10", + "@react-types/button": "3.7.1", "@react-types/menu": "3.7.2", "@react-types/overlays": "3.6.4", "@react-types/shared": "3.16.0", "@rtsao/plugin-proposal-class-properties": "7.0.1-patch.1", - "@swc/core": "1.3.11", - "@swc/helpers": "0.4.12", + "@swc/core": "1.3.38", + "@swc/helpers": "0.4.14", "@testing-library/dom": "8.20.0", "@testing-library/jest-dom": "5.16.5", "@testing-library/react": "12.1.4", @@ -126,9 +126,9 @@ "@types/d3": "7.4.0", "@types/d3-force": "^3.0.0", "@types/d3-scale-chromatic": "3.0.0", - "@types/debounce-promise": "3.1.5", + "@types/debounce-promise": "3.1.6", "@types/dompurify": "^2", - "@types/eslint": "8.21.0", + "@types/eslint": "8.21.1", "@types/file-saver": "2.0.5", "@types/glob": "^8.0.0", "@types/google.analytics": "^0.0.42", @@ -136,25 +136,25 @@ "@types/history": "4.7.11", "@types/hoist-non-react-statics": "3.3.1", "@types/jest": "29.2.3", - "@types/jquery": "3.5.14", + "@types/jquery": "3.5.16", "@types/js-yaml": "^4.0.5", "@types/jsurl": "^1.2.28", - "@types/lodash": "4.14.187", + "@types/lodash": "4.14.191", "@types/logfmt": "^1.2.3", - "@types/mousetrap": "1.6.10", - "@types/node": "18.14.0", - "@types/ol-ext": "npm:@siedlerchr/types-ol-ext@3.0.6", - "@types/papaparse": "5.3.5", + "@types/mousetrap": "1.6.11", + "@types/node": "18.14.6", + "@types/ol-ext": "npm:@siedlerchr/types-ol-ext@3.0.9", + "@types/papaparse": "5.3.7", "@types/pluralize": "^0.0.29", "@types/prismjs": "1.26.0", "@types/react": "17.0.42", - "@types/react-beautiful-dnd": "13.1.2", + "@types/react-beautiful-dnd": "13.1.3", "@types/react-dom": "17.0.14", "@types/react-grid-layout": "1.3.2", "@types/react-highlight-words": "0.16.4", - "@types/react-redux": "7.1.24", + "@types/react-redux": "7.1.25", "@types/react-router-dom": "5.3.3", - "@types/react-table": "7.7.12", + "@types/react-table": "7.7.14", "@types/react-test-renderer": "17.0.1", "@types/react-transition-group": "4.4.5", "@types/react-virtualized-auto-sizer": "1.0.1", @@ -168,12 +168,12 @@ "@types/testing-library__jest-dom": "5.14.5", "@types/tinycolor2": "1.4.3", "@types/uuid": "9.0.1", - "@types/yargs": "17.0.12", + "@types/yargs": "17.0.22", "@typescript-eslint/eslint-plugin": "5.42.0", "@typescript-eslint/parser": "5.42.0", "autoprefixer": "10.4.13", "babel-jest": "29.3.1", - "babel-loader": "9.1.0", + "babel-loader": "9.1.2", "babel-plugin-angularjs-annotate": "0.10.0", "babel-plugin-macros": "3.1.0", "blob-polyfill": "7.0.20220408", @@ -181,7 +181,7 @@ "chance": "^1.0.10", "codeowners": "^5.1.1", "copy-webpack-plugin": "11.0.0", - "css-loader": "6.7.1", + "css-loader": "6.7.3", "css-minimizer-webpack-plugin": "4.2.2", "cypress": "9.5.1", "esbuild": "0.16.17", @@ -194,7 +194,7 @@ "eslint-plugin-jsdoc": "40.0.1", "eslint-plugin-jsx-a11y": "6.7.1", "eslint-plugin-lodash": "7.4.0", - "eslint-plugin-react": "7.32.1", + "eslint-plugin-react": "7.32.2", "eslint-plugin-react-hooks": "4.6.0", "eslint-webpack-plugin": "4.0.0", "expose-loader": "4.0.0", @@ -203,7 +203,7 @@ "html-loader": "4.2.0", "html-webpack-plugin": "5.5.0", "http-server": "14.1.1", - "husky": "8.0.1", + "husky": "8.0.3", "i18next-parser": "6.6.0", "jest": "29.3.1", "jest-canvas-mock": "2.4.0", @@ -213,17 +213,17 @@ "jest-junit": "15.0.0", "jest-matcher-utils": "29.3.1", "lerna": "5.5.4", - "lint-staged": "13.1.0", + "lint-staged": "13.1.2", "mini-css-extract-plugin": "2.7.2", "msw": "1.1.0", "mutationobserver-shim": "0.3.7", "ngtemplate-loader": "2.1.0", "node-notifier": "10.0.1", - "postcss": "8.4.18", - "postcss-loader": "7.0.1", + "postcss": "8.4.21", + "postcss-loader": "7.0.2", "postcss-reporter": "7.0.5", - "postcss-scss": "4.0.5", - "prettier": "2.8.1", + "postcss-scss": "4.0.6", + "prettier": "2.8.4", "react-refresh": "0.14.0", "react-select-event": "5.5.1", "react-simple-compat": "1.2.3", @@ -231,16 +231,16 @@ "redux-mock-store": "1.5.4", "rimraf": "4.2.0", "rudder-sdk-js": "2.25.0", - "sass": "1.58.2", + "sass": "1.58.3", "sass-loader": "13.2.0", "sinon": "15.0.1", "style-loader": "3.3.1", "stylelint": "15.2.0", - "stylelint-config-prettier": "9.0.3", + "stylelint-config-prettier": "9.0.5", "stylelint-config-sass-guidelines": "9.0.1", "terser-webpack-plugin": "5.3.6", "testing-library-selector": "0.2.1", - "ts-jest": "29.0.3", + "ts-jest": "29.0.5", "ts-loader": "9.3.1", "ts-node": "10.9.1", "typescript": "4.8.4", @@ -254,27 +254,27 @@ "yargs": "^17.5.1" }, "dependencies": { - "@daybrush/utils": "1.10.0", - "@emotion/css": "11.10.5", - "@emotion/react": "11.10.5", - "@grafana/aws-sdk": "0.0.40", + "@daybrush/utils": "1.10.2", + "@emotion/css": "11.10.6", + "@emotion/react": "11.10.6", + "@grafana/aws-sdk": "0.0.44", "@grafana/data": "workspace:*", "@grafana/e2e-selectors": "workspace:*", "@grafana/experimental": "1.1.0", - "@grafana/faro-core": "1.0.0-beta2", - "@grafana/faro-web-sdk": "1.0.0-beta2", + "@grafana/faro-core": "1.0.0", + "@grafana/faro-web-sdk": "1.0.0", "@grafana/google-sdk": "0.0.4", "@grafana/lezer-logql": "0.1.2", - "@grafana/monaco-logql": "^0.0.6", + "@grafana/monaco-logql": "^0.0.7", "@grafana/runtime": "workspace:*", - "@grafana/scenes": "^0.0.14", + "@grafana/scenes": "^0.0.16", "@grafana/schema": "workspace:*", "@grafana/ui": "workspace:*", "@kusto/monaco-kusto": "5.3.6", - "@leeoniya/ufuzzy": "1.0.2", - "@lezer/common": "1.0.1", - "@lezer/highlight": "1.1.2", - "@lezer/lr": "1.3.1", + "@leeoniya/ufuzzy": "1.0.5", + "@lezer/common": "1.0.2", + "@lezer/highlight": "1.1.3", + "@lezer/lr": "1.3.3", "@opentelemetry/api": "1.4.0", "@opentelemetry/exporter-collector": "0.25.0", "@opentelemetry/semantic-conventions": "1.9.1", @@ -290,7 +290,7 @@ "@react-stately/collections": "3.4.1", "@react-stately/menu": "3.4.1", "@react-stately/tree": "3.3.1", - "@reduxjs/toolkit": "1.9.1", + "@reduxjs/toolkit": "1.9.3", "@sentry/browser": "6.19.7", "@sentry/types": "6.19.7", "@sentry/utils": "6.19.7", @@ -315,7 +315,7 @@ "centrifuge": "3.1.0", "classnames": "2.3.2", "combokeys": "^3.0.0", - "comlink": "4.4.0", + "comlink": "4.4.1", "common-tags": "1.8.2", "core-js": "3.28.0", "d3": "7.8.2", @@ -334,38 +334,38 @@ "history": "4.10.1", "hoist-non-react-statics": "3.3.2", "i18next": "^22.0.0", - "immer": "9.0.16", - "immutable": "4.2.2", - "jquery": "3.6.1", + "immer": "9.0.19", + "immutable": "4.2.4", + "jquery": "3.6.3", "js-yaml": "^4.1.0", "json-markup": "^1.1.0", "json-source-map": "0.6.1", "jsurl": "^0.1.5", - "kbar": "0.1.0-beta.36", + "kbar": "0.1.0-beta.40", "lodash": "4.17.21", "logfmt": "^1.3.2", "lru-cache": "7.17.0", "lru-memoize": "^1.1.0", "memoize-one": "6.0.0", "moment": "2.29.4", - "moment-timezone": "0.5.38", + "moment-timezone": "0.5.41", "monaco-editor": "0.34.0", "monaco-promql": "1.7.4", "mousetrap": "1.6.5", "mousetrap-global-bind": "1.1.0", "moveable": "0.43.1", "ol": "7.2.2", - "ol-ext": "4.0.3", + "ol-ext": "4.0.6", "papaparse": "5.3.2", "pluralize": "^8.0.0", "prismjs": "1.29.0", "prop-types": "15.8.1", "pseudoizer": "^0.1.0", "rc-cascader": "3.8.0", - "rc-drawer": "6.1.2", - "rc-slider": "10.1.0", + "rc-drawer": "6.1.3", + "rc-slider": "10.1.1", "rc-time-picker": "3.7.3", - "rc-tree": "5.7.0", + "rc-tree": "5.7.2", "re-resizable": "6.9.9", "react": "17.0.2", "react-awesome-query-builder": "5.4.0", @@ -379,7 +379,7 @@ "react-highlight-words": "0.20.0", "react-hook-form": "7.5.3", "react-i18next": "^12.0.0", - "react-inlinesvg": "3.0.1", + "react-inlinesvg": "3.0.2", "react-moveable": "0.46.1", "react-popper": "2.3.0", "react-popper-tooltip": "4.4.2", @@ -396,10 +396,10 @@ "react-virtualized-auto-sizer": "1.0.7", "react-window": "1.8.8", "react-window-infinite-loader": "1.0.8", - "redux": "4.2.0", - "redux-thunk": "2.4.1", - "regenerator-runtime": "0.13.10", - "reselect": "4.1.6", + "redux": "4.2.1", + "redux-thunk": "2.4.2", + "regenerator-runtime": "0.13.11", + "reselect": "4.1.7", "rst2html": "github:thoward/rst2html#990cb89f2a300cdd9151790be377c4c0840df809", "rxjs": "7.8.0", "sass": "link:./public/sass", @@ -425,14 +425,14 @@ "resolutions": { "underscore": "1.13.6", "@types/slate": "0.47.11", - "@rushstack/rig-package": "0.3.17", - "@rushstack/ts-command-line": "4.13.0", - "@storybook/builder-webpack4/css-loader": "6.7.1", + "@rushstack/rig-package": "0.3.18", + "@rushstack/ts-command-line": "4.13.2", + "@storybook/builder-webpack4/css-loader": "6.7.3", "@storybook/builder-webpack4/html-webpack-plugin": "5.5.0", "@storybook/builder-webpack4/webpack": "5.75.0", "@storybook/core-common/webpack": "5.75.0", "@storybook/core-server/webpack": "5.75.0", - "@storybook/manager-webpack4/css-loader": "6.7.1", + "@storybook/manager-webpack4/css-loader": "6.7.3", "@storybook/manager-webpack4/html-webpack-plugin": "5.5.0", "@storybook/manager-webpack4/webpack": "5.75.0", "@storybook/builder-webpack5/webpack": "5.75.0", diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index a0ac05686ee..9d3d21e7ce8 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -35,7 +35,7 @@ "postpack": "mv package.json.bak package.json" }, "dependencies": { - "@braintree/sanitize-url": "6.0.1", + "@braintree/sanitize-url": "6.0.2", "@grafana/schema": "9.5.0-pre", "@types/d3-interpolate": "^3.0.0", "d3-interpolate": "3.0.1", @@ -44,13 +44,13 @@ "fast_array_intersect": "1.1.0", "history": "4.10.1", "lodash": "4.17.21", - "marked": "4.2.0", + "marked": "4.2.12", "moment": "2.29.4", - "moment-timezone": "0.5.38", + "moment-timezone": "0.5.41", "ol": "7.2.2", "papaparse": "5.3.2", "react-use": "17.4.0", - "regenerator-runtime": "0.13.10", + "regenerator-runtime": "0.13.11", "rxjs": "7.8.0", "tinycolor2": "1.6.0", "tslib": "2.5.0", @@ -69,11 +69,11 @@ "@testing-library/user-event": "14.4.3", "@types/history": "4.7.11", "@types/jest": "29.2.3", - "@types/jquery": "3.5.14", - "@types/lodash": "4.14.187", - "@types/marked": "4.0.7", - "@types/node": "18.14.0", - "@types/papaparse": "5.3.5", + "@types/jquery": "3.5.16", + "@types/lodash": "4.14.191", + "@types/marked": "4.0.8", + "@types/node": "18.14.6", + "@types/papaparse": "5.3.7", "@types/react": "17.0.42", "@types/react-dom": "17.0.14", "@types/sinon": "10.0.13", diff --git a/packages/grafana-data/src/types/data.ts b/packages/grafana-data/src/types/data.ts index 0b647bc6eb1..4a24a248e45 100644 --- a/packages/grafana-data/src/types/data.ts +++ b/packages/grafana-data/src/types/data.ts @@ -30,7 +30,7 @@ export const preferredVisualizationTypes = [ 'flamegraph', 'rawPrometheus', ] as const; -export type PreferredVisualisationType = typeof preferredVisualizationTypes[number]; +export type PreferredVisualisationType = (typeof preferredVisualizationTypes)[number]; /** * @public diff --git a/packages/grafana-e2e-selectors/package.json b/packages/grafana-e2e-selectors/package.json index f4f2437ef94..4ebfcd108ec 100644 --- a/packages/grafana-e2e-selectors/package.json +++ b/packages/grafana-e2e-selectors/package.json @@ -41,7 +41,7 @@ "devDependencies": { "@rollup/plugin-commonjs": "23.0.2", "@rollup/plugin-node-resolve": "15.0.1", - "@types/node": "18.14.0", + "@types/node": "18.14.6", "esbuild": "0.16.17", "rimraf": "4.2.0", "rollup": "2.79.1", diff --git a/packages/grafana-e2e/package.json b/packages/grafana-e2e/package.json index 192b25ff710..29b3996ffd3 100644 --- a/packages/grafana-e2e/package.json +++ b/packages/grafana-e2e/package.json @@ -48,9 +48,9 @@ }, "devDependencies": { "@rollup/plugin-node-resolve": "15.0.1", - "@types/chrome-remote-interface": "0.31.4", - "@types/lodash": "4.14.187", - "@types/node": "18.14.0", + "@types/chrome-remote-interface": "0.31.9", + "@types/lodash": "4.14.191", + "@types/node": "18.14.6", "@types/uuid": "9.0.1", "esbuild": "0.16.17", "rollup": "2.79.1", @@ -66,13 +66,13 @@ "@grafana/e2e-selectors": "9.5.0-pre", "@grafana/tsconfig": "^1.2.0-rc1", "@mochajs/json-file-reporter": "^1.2.0", - "babel-loader": "9.1.0", + "babel-loader": "9.1.2", "blink-diff": "1.0.13", - "chrome-remote-interface": "0.32.0", + "chrome-remote-interface": "0.32.1", "commander": "8.3.0", "cypress": "9.5.1", "cypress-file-upload": "5.0.8", - "devtools-protocol": "0.0.1065144", + "devtools-protocol": "0.0.1113774", "execa": "5.1.1", "lodash": "4.17.21", "mocha": "10.2.0", diff --git a/packages/grafana-runtime/package.json b/packages/grafana-runtime/package.json index e8872abb836..c775a7b7a39 100644 --- a/packages/grafana-runtime/package.json +++ b/packages/grafana-runtime/package.json @@ -39,7 +39,7 @@ "dependencies": { "@grafana/data": "9.5.0-pre", "@grafana/e2e-selectors": "9.5.0-pre", - "@grafana/faro-web-sdk": "1.0.0-beta2", + "@grafana/faro-web-sdk": "1.0.0", "@grafana/ui": "9.5.0-pre", "@sentry/browser": "6.19.7", "history": "4.10.1", @@ -59,7 +59,7 @@ "@types/angular": "1.8.4", "@types/history": "4.7.11", "@types/jest": "29.2.3", - "@types/lodash": "4.14.187", + "@types/lodash": "4.14.191", "@types/react": "17.0.42", "@types/react-dom": "17.0.14", "@types/systemjs": "^0.20.6", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 53f84c3dd6c..ea1f17c84fc 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -47,12 +47,12 @@ "not IE 11" ], "dependencies": { - "@emotion/css": "11.10.5", - "@emotion/react": "11.10.5", + "@emotion/css": "11.10.6", + "@emotion/react": "11.10.6", "@grafana/data": "9.5.0-pre", "@grafana/e2e-selectors": "9.5.0-pre", "@grafana/schema": "9.5.0-pre", - "@leeoniya/ufuzzy": "1.0.2", + "@leeoniya/ufuzzy": "1.0.5", "@monaco-editor/react": "4.4.6", "@popperjs/core": "2.11.6", "@react-aria/button": "3.6.1", @@ -71,9 +71,9 @@ "date-fns": "2.29.3", "hoist-non-react-statics": "3.3.2", "i18next": "^22.0.0", - "immutable": "4.2.2", + "immutable": "4.2.4", "is-hotkey": "0.2.0", - "jquery": "3.6.1", + "jquery": "3.6.3", "lodash": "4.17.21", "memoize-one": "6.0.0", "moment": "2.29.4", @@ -81,8 +81,8 @@ "ol": "7.2.2", "prismjs": "1.29.0", "rc-cascader": "3.8.0", - "rc-drawer": "6.1.2", - "rc-slider": "10.1.0", + "rc-drawer": "6.1.3", + "rc-slider": "10.1.1", "rc-time-picker": "^3.7.3", "rc-tooltip": "5.3.1", "react-beautiful-dnd": "13.1.1", @@ -93,7 +93,7 @@ "react-highlight-words": "0.20.0", "react-hook-form": "7.5.3", "react-i18next": "^12.0.0", - "react-inlinesvg": "3.0.1", + "react-inlinesvg": "3.0.2", "react-popper": "2.3.0", "react-popper-tooltip": "4.4.2", "react-router-dom": "^5.2.0", @@ -143,19 +143,19 @@ "@types/hoist-non-react-statics": "3.3.1", "@types/is-hotkey": "0.1.7", "@types/jest": "29.2.3", - "@types/jquery": "3.5.14", - "@types/lodash": "4.14.187", + "@types/jquery": "3.5.16", + "@types/lodash": "4.14.191", "@types/mock-raf": "1.0.3", - "@types/node": "18.14.0", + "@types/node": "18.14.6", "@types/prismjs": "1.26.0", "@types/react": "17.0.42", - "@types/react-beautiful-dnd": "13.1.2", + "@types/react-beautiful-dnd": "13.1.3", "@types/react-calendar": "3.9.0", "@types/react-color": "3.0.6", "@types/react-dom": "17.0.14", "@types/react-highlight-words": "0.16.4", "@types/react-router-dom": "5.3.3", - "@types/react-table": "7.7.12", + "@types/react-table": "7.7.14", "@types/react-test-renderer": "17.0.1", "@types/react-transition-group": "4.4.5", "@types/react-window": "1.8.5", @@ -166,7 +166,7 @@ "@types/tinycolor2": "1.4.3", "@types/uuid": "9.0.1", "common-tags": "1.8.2", - "css-loader": "6.7.1", + "css-loader": "6.7.3", "csstype": "3.1.1", "esbuild": "0.16.17", "expose-loader": "4.0.0", diff --git a/public/app/core/components/QueryOperationRow/QueryOperationRowHeader.tsx b/public/app/core/components/QueryOperationRow/QueryOperationRowHeader.tsx index 612dbb5076d..f62e343d613 100644 --- a/public/app/core/components/QueryOperationRow/QueryOperationRowHeader.tsx +++ b/public/app/core/components/QueryOperationRow/QueryOperationRowHeader.tsx @@ -1,6 +1,6 @@ import { css, cx } from '@emotion/css'; import React, { MouseEventHandler } from 'react'; -import { DraggableProvidedDragHandleProps } from 'react-beautiful-dnd'; +import { DraggableProvided } from 'react-beautiful-dnd'; import { GrafanaTheme2 } from '@grafana/data'; import { Icon, IconButton, useStyles2 } from '@grafana/ui'; @@ -9,7 +9,7 @@ interface QueryOperationRowHeaderProps { actionsElement?: React.ReactNode; disabled?: boolean; draggable: boolean; - dragHandleProps?: DraggableProvidedDragHandleProps; + dragHandleProps?: DraggableProvided['dragHandleProps']; headerElement?: React.ReactNode; isContentVisible: boolean; onRowToggle: () => void; diff --git a/public/app/core/services/echo/backends/grafana-javascript-agent/GrafanaJavascriptAgentBackend.test.ts b/public/app/core/services/echo/backends/grafana-javascript-agent/GrafanaJavascriptAgentBackend.test.ts index cc0dc5e0ca9..0127f44ae3d 100644 --- a/public/app/core/services/echo/backends/grafana-javascript-agent/GrafanaJavascriptAgentBackend.test.ts +++ b/public/app/core/services/echo/backends/grafana-javascript-agent/GrafanaJavascriptAgentBackend.test.ts @@ -99,6 +99,8 @@ describe('GrafanaJavascriptAgentEchoBackend', () => { getSession: jest.fn(), resetUser: jest.fn(), resetSession: jest.fn(), + setView: jest.fn(), + getView: jest.fn(), }, config: { globalObjectKey: '', @@ -193,6 +195,8 @@ describe('GrafanaJavascriptAgentEchoBackend', () => { getSession: jest.fn(), resetUser: jest.fn(), resetSession: jest.fn(), + setView: jest.fn(), + getView: jest.fn(), }, config: { globalObjectKey: '', diff --git a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ConfigPublicDashboard/ConfigPublicDashboard.tsx b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ConfigPublicDashboard/ConfigPublicDashboard.tsx index d9040289053..70183531042 100644 --- a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ConfigPublicDashboard/ConfigPublicDashboard.tsx +++ b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ConfigPublicDashboard/ConfigPublicDashboard.tsx @@ -199,10 +199,10 @@ const getStyles = (theme: GrafanaTheme2) => ({ margin-bottom: ${theme.spacing(3)}; `, deleteButton: css` - margin-left: ${theme.spacing(3)}; ; + margin-left: ${theme.spacing(3)}; `, deleteButtonMobile: css` - margin-top: ${theme.spacing(2)}; ; + margin-top: ${theme.spacing(2)}; `, }); diff --git a/public/app/features/explore/LogsSamplePanel.tsx b/public/app/features/explore/LogsSamplePanel.tsx index c7cc0726344..f1b4383f0ec 100644 --- a/public/app/features/explore/LogsSamplePanel.tsx +++ b/public/app/features/explore/LogsSamplePanel.tsx @@ -122,7 +122,7 @@ const getStyles = (theme: GrafanaTheme2) => ({ logSamplesButton: css` position: absolute; top: ${theme.spacing(1)}; - right: ${theme.spacing(1)}; ; + right: ${theme.spacing(1)}; `, logContainer: css` overflow-x: scroll; diff --git a/public/app/features/explore/TraceView/components/model/link-patterns.tsx b/public/app/features/explore/TraceView/components/model/link-patterns.tsx index 696c13c690c..b26b18fc910 100644 --- a/public/app/features/explore/TraceView/components/model/link-patterns.tsx +++ b/public/app/features/explore/TraceView/components/model/link-patterns.tsx @@ -239,7 +239,7 @@ export function createGetLinks(linkPatterns: ProcessedLinkPattern[], cache: Weak const processedLinks = (getConfigValue('linkPatterns') || []) .map(processLinkPattern) - .filter((link): link is ProcessedLinkPattern => Boolean(link)); + .filter((link: ProcessedLinkPattern | null): link is ProcessedLinkPattern => Boolean(link)); export const getTraceLinks: (trace: Trace | undefined) => TLinksRV = memoize(10)((trace: Trace | undefined) => { const result: TLinksRV = []; diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/state/reducer.test.ts b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/state/reducer.test.ts index 3f81f08e9ca..95052d9d229 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/state/reducer.test.ts +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/state/reducer.test.ts @@ -139,7 +139,7 @@ describe('Bucket Aggregations Reducer', () => { type: 'date_histogram', }; - const expectedSettings: typeof firstAggregation['settings'] = { + const expectedSettings: (typeof firstAggregation)['settings'] = { min_doc_count: '1', }; diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/state/reducer.test.ts b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/state/reducer.test.ts index 5f86e7e926a..d91c28ca65c 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/state/reducer.test.ts +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/state/reducer.test.ts @@ -170,7 +170,7 @@ describe('Metric Aggregations Reducer', () => { type: 'count', }; - const expectedSettings: typeof firstAggregation['settings'] = { + const expectedSettings: (typeof firstAggregation)['settings'] = { unit: 'Changed unit', }; @@ -195,7 +195,7 @@ describe('Metric Aggregations Reducer', () => { type: 'count', }; - const expectedMeta: typeof firstAggregation['meta'] = { + const expectedMeta: (typeof firstAggregation)['meta'] = { avg: false, }; @@ -215,7 +215,7 @@ describe('Metric Aggregations Reducer', () => { type: 'count', }; - const expectedHide: typeof firstAggregation['hide'] = false; + const expectedHide: (typeof firstAggregation)['hide'] = false; reducerTester() .givenReducer(reducer, [firstAggregation, secondAggregation]) diff --git a/public/app/plugins/datasource/prometheus/querybuilder/shared/OperationHeader.tsx b/public/app/plugins/datasource/prometheus/querybuilder/shared/OperationHeader.tsx index 575f134f501..ede453f0745 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/shared/OperationHeader.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/shared/OperationHeader.tsx @@ -1,6 +1,6 @@ import { css } from '@emotion/css'; import React, { useState } from 'react'; -import { DraggableProvidedDragHandleProps } from 'react-beautiful-dnd'; +import { DraggableProvided } from 'react-beautiful-dnd'; import { GrafanaTheme2, SelectableValue } from '@grafana/data'; import { FlexItem } from '@grafana/experimental'; @@ -14,7 +14,7 @@ export interface Props { def: QueryBuilderOperationDef; index: number; queryModeller: VisualQueryModeller; - dragHandleProps?: DraggableProvidedDragHandleProps; + dragHandleProps?: DraggableProvided['dragHandleProps']; onChange: (index: number, update: QueryBuilderOperation) => void; onRemove: (index: number) => void; } diff --git a/public/app/plugins/panel/graph/Legend/LegendSeriesItem.tsx b/public/app/plugins/panel/graph/Legend/LegendSeriesItem.tsx index 188168ecf58..62a597d4515 100644 --- a/public/app/plugins/panel/graph/Legend/LegendSeriesItem.tsx +++ b/public/app/plugins/panel/graph/Legend/LegendSeriesItem.tsx @@ -6,7 +6,7 @@ import { SeriesColorPicker, SeriesIcon } from '@grafana/ui'; import { TimeSeries } from 'app/core/core'; export const LEGEND_STATS = ['min', 'max', 'avg', 'current', 'total'] as const; -export type LegendStat = typeof LEGEND_STATS[number]; +export type LegendStat = (typeof LEGEND_STATS)[number]; export interface LegendLabelProps { series: TimeSeries; diff --git a/public/app/types/explore.ts b/public/app/types/explore.ts index 38678696ce4..5d97a69b4d0 100644 --- a/public/app/types/explore.ts +++ b/public/app/types/explore.ts @@ -87,7 +87,7 @@ export interface ExploreState { } export const EXPLORE_GRAPH_STYLES = ['lines', 'bars', 'points', 'stacked_lines', 'stacked_bars'] as const; -export type ExploreGraphStyle = typeof EXPLORE_GRAPH_STYLES[number]; +export type ExploreGraphStyle = (typeof EXPLORE_GRAPH_STYLES)[number]; export interface ExploreItemState { /** @@ -264,7 +264,7 @@ export enum TABLE_RESULTS_STYLE { raw = 'raw', } export const TABLE_RESULTS_STYLES = [TABLE_RESULTS_STYLE.table, TABLE_RESULTS_STYLE.raw]; -export type TableResultsStyle = typeof TABLE_RESULTS_STYLES[number]; +export type TableResultsStyle = (typeof TABLE_RESULTS_STYLES)[number]; export interface SupplementaryQuery { enabled: boolean; diff --git a/yarn.lock b/yarn.lock index d86bf27774c..3fd29b7ebef 100644 --- a/yarn.lock +++ b/yarn.lock @@ -106,6 +106,13 @@ __metadata: languageName: node linkType: hard +"@babel/compat-data@npm:^7.20.5": + version: 7.21.0 + resolution: "@babel/compat-data@npm:7.21.0" + checksum: dbf632c532f9c75ba0be7d1dc9f6cd3582501af52f10a6b90415d634ec5878735bd46064c91673b10317af94d4cc99c4da5bd9d955978cdccb7905fc33291e4d + languageName: node + linkType: hard + "@babel/core@npm:7.12.9": version: 7.12.9 resolution: "@babel/core@npm:7.12.9" @@ -541,6 +548,21 @@ __metadata: languageName: node linkType: hard +"@babel/helper-compilation-targets@npm:^7.20.7": + version: 7.20.7 + resolution: "@babel/helper-compilation-targets@npm:7.20.7" + dependencies: + "@babel/compat-data": ^7.20.5 + "@babel/helper-validator-option": ^7.18.6 + browserslist: ^4.21.3 + lru-cache: ^5.1.1 + semver: ^6.3.0 + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 8c32c873ba86e2e1805b30e0807abd07188acbe00ebb97576f0b09061cc65007f1312b589eccb4349c5a8c7f8bb9f2ab199d41da7030bf103d9f347dcd3a3cf4 + languageName: node + linkType: hard + "@babel/helper-create-class-features-plugin@npm:^7.18.6": version: 7.18.6 resolution: "@babel/helper-create-class-features-plugin@npm:7.18.6" @@ -1807,18 +1829,18 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-proposal-object-rest-spread@npm:7.20.2, @babel/plugin-proposal-object-rest-spread@npm:^7.20.2": - version: 7.20.2 - resolution: "@babel/plugin-proposal-object-rest-spread@npm:7.20.2" +"@babel/plugin-proposal-object-rest-spread@npm:7.20.7": + version: 7.20.7 + resolution: "@babel/plugin-proposal-object-rest-spread@npm:7.20.7" dependencies: - "@babel/compat-data": ^7.20.1 - "@babel/helper-compilation-targets": ^7.20.0 + "@babel/compat-data": ^7.20.5 + "@babel/helper-compilation-targets": ^7.20.7 "@babel/helper-plugin-utils": ^7.20.2 "@babel/plugin-syntax-object-rest-spread": ^7.8.3 - "@babel/plugin-transform-parameters": ^7.20.1 + "@babel/plugin-transform-parameters": ^7.20.7 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 9764d1a4735fcd384fdb9b6c6ccb20d1bea2f88f648640d26ce5d9cd5880ce1e389d2f852d7bea7e86ff343726225dc16e1deb92c7b3dc5c5721ed905a602318 + checksum: 1329db17009964bc644484c660eab717cb3ca63ac0ab0f67c651a028d1bc2ead51dc4064caea283e46994f1b7221670a35cbc0b4beb6273f55e915494b5aa0b2 languageName: node linkType: hard @@ -1837,6 +1859,21 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-proposal-object-rest-spread@npm:^7.20.2": + version: 7.20.2 + resolution: "@babel/plugin-proposal-object-rest-spread@npm:7.20.2" + dependencies: + "@babel/compat-data": ^7.20.1 + "@babel/helper-compilation-targets": ^7.20.0 + "@babel/helper-plugin-utils": ^7.20.2 + "@babel/plugin-syntax-object-rest-spread": ^7.8.3 + "@babel/plugin-transform-parameters": ^7.20.1 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 9764d1a4735fcd384fdb9b6c6ccb20d1bea2f88f648640d26ce5d9cd5880ce1e389d2f852d7bea7e86ff343726225dc16e1deb92c7b3dc5c5721ed905a602318 + languageName: node + linkType: hard + "@babel/plugin-proposal-optional-catch-binding@npm:^7.18.6": version: 7.18.6 resolution: "@babel/plugin-proposal-optional-catch-binding@npm:7.18.6" @@ -2090,7 +2127,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-jsx@npm:^7.17.12, @babel/plugin-syntax-jsx@npm:^7.18.6, @babel/plugin-syntax-jsx@npm:^7.7.2": +"@babel/plugin-syntax-jsx@npm:^7.18.6, @babel/plugin-syntax-jsx@npm:^7.7.2": version: 7.18.6 resolution: "@babel/plugin-syntax-jsx@npm:7.18.6" dependencies: @@ -2672,6 +2709,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-parameters@npm:^7.20.7": + version: 7.20.7 + resolution: "@babel/plugin-transform-parameters@npm:7.20.7" + dependencies: + "@babel/helper-plugin-utils": ^7.20.2 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 6ffe0dd9afb2d2b9bc247381aa2e95dd9997ff5568a0a11900528919a4e073ac68f46409431455badb8809644d47cff180045bc2b9700e3f36e3b23554978947 + languageName: node + linkType: hard + "@babel/plugin-transform-property-literals@npm:^7.18.6": version: 7.18.6 resolution: "@babel/plugin-transform-property-literals@npm:7.18.6" @@ -3862,10 +3910,10 @@ __metadata: languageName: node linkType: hard -"@braintree/sanitize-url@npm:6.0.1": - version: 6.0.1 - resolution: "@braintree/sanitize-url@npm:6.0.1" - checksum: 6f9221299aac0c841a17ecb1ebc60eb43c794f05b5136ca9b87116c8472b7e96f21e56ba2da8369f964112c6055fab791a37015bbea4bd5a189cc38206d214ad +"@braintree/sanitize-url@npm:6.0.2": + version: 6.0.2 + resolution: "@braintree/sanitize-url@npm:6.0.2" + checksum: 6a9dfd4081cc96516eeb281d1a83d3b5f1ad3d2837adf968fcc2ba18889ee833554f9c641b4083c36d3360a932e4504ddf25b0b51e9933c3742622df82cf7c9a languageName: node linkType: hard @@ -4094,10 +4142,10 @@ __metadata: languageName: node linkType: hard -"@daybrush/utils@npm:1.10.0, @daybrush/utils@npm:^1.10.0": - version: 1.10.0 - resolution: "@daybrush/utils@npm:1.10.0" - checksum: bd8401971012052c420046e45b143fa9a211a2dc80c782ebc930865df6da8da8544f683a2b8728862a97407ff48bd7eb02657279b19f261e95541818441023db +"@daybrush/utils@npm:1.10.2, @daybrush/utils@npm:^1.6.0": + version: 1.10.2 + resolution: "@daybrush/utils@npm:1.10.2" + checksum: aa3513eb7571fa052de3c5216d41dbc6c7330719052881ea562f75e2bc89c8f00f32dbf6743b379e32deb80477ffe0ecd0c69bb77e3f78cf5ada123b57b3a7bd languageName: node linkType: hard @@ -4108,10 +4156,10 @@ __metadata: languageName: node linkType: hard -"@daybrush/utils@npm:^1.6.0": - version: 1.10.2 - resolution: "@daybrush/utils@npm:1.10.2" - checksum: aa3513eb7571fa052de3c5216d41dbc6c7330719052881ea562f75e2bc89c8f00f32dbf6743b379e32deb80477ffe0ecd0c69bb77e3f78cf5ada123b57b3a7bd +"@daybrush/utils@npm:^1.10.0": + version: 1.10.0 + resolution: "@daybrush/utils@npm:1.10.0" + checksum: bd8401971012052c420046e45b143fa9a211a2dc80c782ebc930865df6da8da8544f683a2b8728862a97407ff48bd7eb02657279b19f261e95541818441023db languageName: node linkType: hard @@ -4159,12 +4207,11 @@ __metadata: languageName: node linkType: hard -"@emotion/babel-plugin@npm:^11.10.5": - version: 11.10.5 - resolution: "@emotion/babel-plugin@npm:11.10.5" +"@emotion/babel-plugin@npm:^11.10.6": + version: 11.10.6 + resolution: "@emotion/babel-plugin@npm:11.10.6" dependencies: "@babel/helper-module-imports": ^7.16.7 - "@babel/plugin-syntax-jsx": ^7.17.12 "@babel/runtime": ^7.18.3 "@emotion/hash": ^0.9.0 "@emotion/memoize": ^0.8.0 @@ -4175,9 +4222,7 @@ __metadata: find-root: ^1.1.0 source-map: ^0.5.7 stylis: 4.1.3 - peerDependencies: - "@babel/core": ^7.0.0 - checksum: e3353499c76c4422d6e900c0dfab73607056d9da86161a3f27c3459c193c4908050c5d252c68fcde231e13f02a9d8e0dc07d260317ae0e5206841e331cc4caae + checksum: 3eed138932e8edf2598352e69ad949b9db3051a4d6fcff190dacbac9aa838d7ef708b9f3e6c48660625d9311dae82d73477ae4e7a31139feef5eb001a5528421 languageName: node linkType: hard @@ -4254,21 +4299,16 @@ __metadata: languageName: node linkType: hard -"@emotion/css@npm:11.10.5": - version: 11.10.5 - resolution: "@emotion/css@npm:11.10.5" +"@emotion/css@npm:11.10.6": + version: 11.10.6 + resolution: "@emotion/css@npm:11.10.6" dependencies: - "@emotion/babel-plugin": ^11.10.5 + "@emotion/babel-plugin": ^11.10.6 "@emotion/cache": ^11.10.5 "@emotion/serialize": ^1.1.1 "@emotion/sheet": ^1.2.1 "@emotion/utils": ^1.2.0 - peerDependencies: - "@babel/core": ^7.0.0 - peerDependenciesMeta: - "@babel/core": - optional: true - checksum: 2f1e953c3519cd69e40d2931cf36c3f1daf33d9f5064cb2612071bd316e38c8debb83d9520db5694ac323b6475f86004f6542919746f005fea0c14a855fc579b + checksum: 010ca5e1d0434923c431eab2c6f8226ab4415310a053ece6e4133011c7dc621de099469e57314b013e682c3b6ffd09a91cc6473538c185095d2afca79d81fb95 languageName: node linkType: hard @@ -4316,12 +4356,12 @@ __metadata: languageName: node linkType: hard -"@emotion/react@npm:11.10.5": - version: 11.10.5 - resolution: "@emotion/react@npm:11.10.5" +"@emotion/react@npm:11.10.6": + version: 11.10.6 + resolution: "@emotion/react@npm:11.10.6" dependencies: "@babel/runtime": ^7.18.3 - "@emotion/babel-plugin": ^11.10.5 + "@emotion/babel-plugin": ^11.10.6 "@emotion/cache": ^11.10.5 "@emotion/serialize": ^1.1.1 "@emotion/use-insertion-effect-with-fallbacks": ^1.0.0 @@ -4329,14 +4369,11 @@ __metadata: "@emotion/weak-memoize": ^0.3.0 hoist-non-react-statics: ^3.3.1 peerDependencies: - "@babel/core": ^7.0.0 react: ">=16.8.0" peerDependenciesMeta: - "@babel/core": - optional: true "@types/react": optional: true - checksum: 32b67b28e9b6d6c53b970072680697f04c2521441050bdeb19a1a7f0164af549b4dad39ff375eda1b6a3cf1cc86ba2c6fa55460ec040e6ebbca3e9ec58353cf7 + checksum: 4762042e39126ffaffe76052dc65c9bb0ba6b8893013687ba3cc13ed4dd834c31597f1230684c3c078e90aecc13ab6cd0e3cde0dec8b7761affd2571f4d80019 languageName: node linkType: hard @@ -4855,10 +4892,22 @@ __metadata: languageName: unknown linkType: soft -"@grafana/aws-sdk@npm:0.0.40": - version: 0.0.40 - resolution: "@grafana/aws-sdk@npm:0.0.40" - checksum: 23d20f8da262b7a80a13d0c60bbefa49760c5b1459c00646cbb4762d861003f8c3a32d8c6660902762c27ba136831b15d420291b105f4d72b971a04854d9027f +"@grafana/async-query-data@npm:0.1.4": + version: 0.1.4 + resolution: "@grafana/async-query-data@npm:0.1.4" + dependencies: + tslib: ^2.4.1 + checksum: e1954d2b77841cdf8a7686a6bd132fbdf647c2fb810d895b721b936bb883fdd00c13257edd4d3352c39cb3e0d2106ba424457e02c7cbe6ab359139c3e1e044a2 + languageName: node + linkType: hard + +"@grafana/aws-sdk@npm:0.0.44": + version: 0.0.44 + resolution: "@grafana/aws-sdk@npm:0.0.44" + dependencies: + "@grafana/async-query-data": 0.1.4 + "@grafana/experimental": 1.1.0 + checksum: 54955767af28109ed976a24224345757c09a369eb636c95dfdb389f70475838ba14b04c6bad147f423b35480e4e68f76fa719e422cbfa470750aef0d5f38b371 languageName: node linkType: hard @@ -4866,7 +4915,7 @@ __metadata: version: 0.0.0-use.local resolution: "@grafana/data@workspace:packages/grafana-data" dependencies: - "@braintree/sanitize-url": 6.0.1 + "@braintree/sanitize-url": 6.0.2 "@grafana/schema": 9.5.0-pre "@grafana/tsconfig": ^1.2.0-rc1 "@rollup/plugin-commonjs": 23.0.2 @@ -4880,11 +4929,11 @@ __metadata: "@types/d3-interpolate": ^3.0.0 "@types/history": 4.7.11 "@types/jest": 29.2.3 - "@types/jquery": 3.5.14 - "@types/lodash": 4.14.187 - "@types/marked": 4.0.7 - "@types/node": 18.14.0 - "@types/papaparse": 5.3.5 + "@types/jquery": 3.5.16 + "@types/lodash": 4.14.191 + "@types/marked": 4.0.8 + "@types/node": 18.14.6 + "@types/papaparse": 5.3.7 "@types/react": 17.0.42 "@types/react-dom": 17.0.14 "@types/sinon": 10.0.13 @@ -4897,16 +4946,16 @@ __metadata: fast_array_intersect: 1.1.0 history: 4.10.1 lodash: 4.17.21 - marked: 4.2.0 + marked: 4.2.12 moment: 2.29.4 - moment-timezone: 0.5.38 + moment-timezone: 0.5.41 ol: 7.2.2 papaparse: 5.3.2 react: 17.0.2 react-dom: 17.0.2 react-test-renderer: 17.0.2 react-use: 17.4.0 - regenerator-runtime: 0.13.10 + regenerator-runtime: 0.13.11 rimraf: 4.2.0 rollup: 2.79.1 rollup-plugin-dts: ^5.0.0 @@ -4932,7 +4981,7 @@ __metadata: "@grafana/tsconfig": ^1.2.0-rc1 "@rollup/plugin-commonjs": 23.0.2 "@rollup/plugin-node-resolve": 15.0.1 - "@types/node": 18.14.0 + "@types/node": 18.14.6 esbuild: 0.16.17 rimraf: 4.2.0 rollup: 2.79.1 @@ -4966,17 +5015,17 @@ __metadata: "@grafana/tsconfig": ^1.2.0-rc1 "@mochajs/json-file-reporter": ^1.2.0 "@rollup/plugin-node-resolve": 15.0.1 - "@types/chrome-remote-interface": 0.31.4 - "@types/lodash": 4.14.187 - "@types/node": 18.14.0 + "@types/chrome-remote-interface": 0.31.9 + "@types/lodash": 4.14.191 + "@types/node": 18.14.6 "@types/uuid": 9.0.1 - babel-loader: 9.1.0 + babel-loader: 9.1.2 blink-diff: 1.0.13 - chrome-remote-interface: 0.32.0 + chrome-remote-interface: 0.32.1 commander: 8.3.0 cypress: 9.5.1 cypress-file-upload: 5.0.8 - devtools-protocol: 0.0.1065144 + devtools-protocol: 0.0.1113774 esbuild: 0.16.17 execa: 5.1.1 lodash: 4.17.21 @@ -5066,26 +5115,25 @@ __metadata: languageName: node linkType: hard -"@grafana/faro-core@npm:1.0.0-beta2, @grafana/faro-core@npm:^1.0.0-beta2": - version: 1.0.0-beta2 - resolution: "@grafana/faro-core@npm:1.0.0-beta2" +"@grafana/faro-core@npm:1.0.0, @grafana/faro-core@npm:^1.0.0": + version: 1.0.0 + resolution: "@grafana/faro-core@npm:1.0.0" dependencies: - "@opentelemetry/api": ^1.1.0 + "@opentelemetry/api": ^1.4.0 "@opentelemetry/api-metrics": ^0.33.0 - "@opentelemetry/otlp-transformer": ^0.33.0 - fast-deep-equal: ^3.1.3 - checksum: 0c807f5212e502313b149b087e3f2c0cebf59bfe95bcd558f6fd421727a6f7eeb735abb12661f9b76e14bbcebca4773763332a1fb992461e4a673f1326a9acce + "@opentelemetry/otlp-transformer": ^0.35.0 + checksum: 6debd3f85e1fd9b4a48c6327fdf5b603356ac4e9da25ea861e04741b3e4cc4539e8c2e8ab9f34a33321df0f5f13fdb1371b6d67d493980befd300acd8b8acc9b languageName: node linkType: hard -"@grafana/faro-web-sdk@npm:1.0.0-beta2": - version: 1.0.0-beta2 - resolution: "@grafana/faro-web-sdk@npm:1.0.0-beta2" +"@grafana/faro-web-sdk@npm:1.0.0": + version: 1.0.0 + resolution: "@grafana/faro-web-sdk@npm:1.0.0" dependencies: - "@grafana/faro-core": ^1.0.0-beta2 + "@grafana/faro-core": ^1.0.0 ua-parser-js: ^1.0.32 - web-vitals: ^3.0.4 - checksum: 7919c4856653880c71d384b14e8e8a7fa2edc17cf57bdc55dc6afd8a8c94b6787a0b9186540b6fb1c4f6d720de7ec8b3ddd00517a2e8117ede94de5b3f8c5559 + web-vitals: ^3.1.1 + checksum: 63734db13f3ccef08c46e638c8f62d0bdcc265c7926d80bbe6637152d34fd2f08be5eebfb74521e55e2be306062bc5126eace1ef3453b0a8dfe72952fd23a2ff languageName: node linkType: hard @@ -5105,12 +5153,12 @@ __metadata: languageName: node linkType: hard -"@grafana/monaco-logql@npm:^0.0.6": - version: 0.0.6 - resolution: "@grafana/monaco-logql@npm:0.0.6" +"@grafana/monaco-logql@npm:^0.0.7": + version: 0.0.7 + resolution: "@grafana/monaco-logql@npm:0.0.7" peerDependencies: monaco-editor: ^0.32.1 - checksum: 81ac76c0eaa020cdac4c2eb5a7b5b4c18c3aac932ecfe2a860e1755597a41fae8421290b96a4f6234cd02e640b6cf3dc7380b475973e20d8e29ab62f4cee5933 + checksum: cce4a8ed8aaefee9211c4f2e283e5b4e551a942cd6673f13c5b9afca750432493388e383815f0f81bd2f119526115c2ecd8dfb23060ea61511eeecfc2a9750a4 languageName: node linkType: hard @@ -5120,7 +5168,7 @@ __metadata: dependencies: "@grafana/data": 9.5.0-pre "@grafana/e2e-selectors": 9.5.0-pre - "@grafana/faro-web-sdk": 1.0.0-beta2 + "@grafana/faro-web-sdk": 1.0.0 "@grafana/tsconfig": ^1.2.0-rc1 "@grafana/ui": 9.5.0-pre "@rollup/plugin-commonjs": 23.0.2 @@ -5133,7 +5181,7 @@ __metadata: "@types/angular": 1.8.4 "@types/history": 4.7.11 "@types/jest": 29.2.3 - "@types/lodash": 4.14.187 + "@types/lodash": 4.14.191 "@types/react": 17.0.42 "@types/react-dom": 17.0.14 "@types/systemjs": ^0.20.6 @@ -5159,9 +5207,9 @@ __metadata: languageName: unknown linkType: soft -"@grafana/scenes@npm:^0.0.14": - version: 0.0.14 - resolution: "@grafana/scenes@npm:0.0.14" +"@grafana/scenes@npm:^0.0.16": + version: 0.0.16 + resolution: "@grafana/scenes@npm:0.0.16" dependencies: "@grafana/e2e-selectors": canary "@grafana/experimental": 1.0.1 @@ -5169,7 +5217,7 @@ __metadata: react-use: 17.4.0 react-virtualized-auto-sizer: 1.0.7 uuid: ^9.0.0 - checksum: 77cb6ff3ec8f716ce35399fb9f51c4d701025a71af2cb9f7d33a26b973ebfdabed49ba9b3d72dfdb5fbf43043a8b944a4f297c027261df14b3e0bd189096c905 + checksum: c6cf9f1571309da4ed0fb5046f8abf18b324e9847890c54307ca093610760d52fdb44002fb1db8e81abca6371121a0623a2f44c9828ef80b11d8d189993f457a languageName: node linkType: hard @@ -5297,13 +5345,13 @@ __metadata: resolution: "@grafana/ui@workspace:packages/grafana-ui" dependencies: "@babel/core": 7.20.5 - "@emotion/css": 11.10.5 - "@emotion/react": 11.10.5 + "@emotion/css": 11.10.6 + "@emotion/react": 11.10.6 "@grafana/data": 9.5.0-pre "@grafana/e2e-selectors": 9.5.0-pre "@grafana/schema": 9.5.0-pre "@grafana/tsconfig": ^1.2.0-rc1 - "@leeoniya/ufuzzy": 1.0.2 + "@leeoniya/ufuzzy": 1.0.5 "@mdx-js/react": 1.6.22 "@monaco-editor/react": 4.4.6 "@popperjs/core": 2.11.6 @@ -5342,19 +5390,19 @@ __metadata: "@types/hoist-non-react-statics": 3.3.1 "@types/is-hotkey": 0.1.7 "@types/jest": 29.2.3 - "@types/jquery": 3.5.14 - "@types/lodash": 4.14.187 + "@types/jquery": 3.5.16 + "@types/lodash": 4.14.191 "@types/mock-raf": 1.0.3 - "@types/node": 18.14.0 + "@types/node": 18.14.6 "@types/prismjs": 1.26.0 "@types/react": 17.0.42 - "@types/react-beautiful-dnd": 13.1.2 + "@types/react-beautiful-dnd": 13.1.3 "@types/react-calendar": 3.9.0 "@types/react-color": 3.0.6 "@types/react-dom": 17.0.14 "@types/react-highlight-words": 0.16.4 "@types/react-router-dom": 5.3.3 - "@types/react-table": 7.7.12 + "@types/react-table": 7.7.14 "@types/react-test-renderer": 17.0.1 "@types/react-transition-group": 4.4.5 "@types/react-window": 1.8.5 @@ -5369,7 +5417,7 @@ __metadata: classnames: 2.3.2 common-tags: 1.8.2 core-js: 3.28.0 - css-loader: 6.7.1 + css-loader: 6.7.3 csstype: 3.1.1 d3: 7.8.2 date-fns: 2.29.3 @@ -5377,9 +5425,9 @@ __metadata: expose-loader: 4.0.0 hoist-non-react-statics: 3.3.2 i18next: ^22.0.0 - immutable: 4.2.2 + immutable: 4.2.4 is-hotkey: 0.2.0 - jquery: 3.6.1 + jquery: 3.6.3 lodash: 4.17.21 memoize-one: 6.0.0 mock-raf: 1.0.1 @@ -5389,8 +5437,8 @@ __metadata: prismjs: 1.29.0 process: ^0.11.10 rc-cascader: 3.8.0 - rc-drawer: 6.1.2 - rc-slider: 10.1.0 + rc-drawer: 6.1.3 + rc-slider: 10.1.1 rc-time-picker: ^3.7.3 rc-tooltip: 5.3.1 react: 17.0.2 @@ -5403,7 +5451,7 @@ __metadata: react-highlight-words: 0.20.0 react-hook-form: 7.5.3 react-i18next: ^12.0.0 - react-inlinesvg: 3.0.1 + react-inlinesvg: 3.0.2 react-popper: 2.3.0 react-popper-tooltip: 4.4.2 react-router-dom: ^5.2.0 @@ -6282,10 +6330,10 @@ __metadata: languageName: node linkType: hard -"@leeoniya/ufuzzy@npm:1.0.2": - version: 1.0.2 - resolution: "@leeoniya/ufuzzy@npm:1.0.2" - checksum: 5460378a8c32d121b0bc7c8e95cde995316516655528e248051b1bf360cdca0311ef3275de14b802587748231333cee6183c931b3abba26f9e4236ecc4959aa3 +"@leeoniya/ufuzzy@npm:1.0.5": + version: 1.0.5 + resolution: "@leeoniya/ufuzzy@npm:1.0.5" + checksum: 49e0633ea71fdfb036980b24c07bc524fc44e5fa44425edf1df87c66bdfa07a1833257508b01aed25485318973d676b48e9e0ced98658783c21582cc14e8f982 languageName: node linkType: hard @@ -7100,28 +7148,28 @@ __metadata: languageName: node linkType: hard -"@lezer/common@npm:1.0.1, @lezer/common@npm:^1.0.0": - version: 1.0.1 - resolution: "@lezer/common@npm:1.0.1" - checksum: 05bda2b0b90713a511502e1e0c67842996cb86415cd701c46eb49f8fa117552229a4bc7244f1e8e0025e3563294a52911eebd70d06a4bc987280fca5d148c34b +"@lezer/common@npm:1.0.2, @lezer/common@npm:^1.0.0": + version: 1.0.2 + resolution: "@lezer/common@npm:1.0.2" + checksum: bbcc58e07be02652bf0700d2856042ec089d5be0b95893d628b3e18192ade864fac83b61b19653e10b9f1472261a178b12318d934e9004edd5483a577c0db56b languageName: node linkType: hard -"@lezer/highlight@npm:1.1.2": - version: 1.1.2 - resolution: "@lezer/highlight@npm:1.1.2" +"@lezer/highlight@npm:1.1.3": + version: 1.1.3 + resolution: "@lezer/highlight@npm:1.1.3" dependencies: "@lezer/common": ^1.0.0 - checksum: b974c3f43c4338840f7f6110948d5f69ba8ce9d58b75e493bbef5183d63b3b531a1416398e9c7844914d134ea4e9ae717fae240629d937554fcc4cf1ab5385e8 + checksum: 90ec143ce46b32f6779c3b245f1b5a540d66686939816d3daed8318821acc4bc719466dc222336cfd483bf04a8de4fdc6f279e904cf114d4d9f786f9feccbbd8 languageName: node linkType: hard -"@lezer/lr@npm:1.3.1": - version: 1.3.1 - resolution: "@lezer/lr@npm:1.3.1" +"@lezer/lr@npm:1.3.3": + version: 1.3.3 + resolution: "@lezer/lr@npm:1.3.3" dependencies: "@lezer/common": ^1.0.0 - checksum: 01b421b9724d43a00a97639344cd89c539e9740b7bedc8270882192e4205f68679e5b7c6837dde7756970a418bd4e710e4a778ab7c6713a2331bea3049232ca8 + checksum: 1804074c794005a31c54d80ab72127f19ae5be29bb627c52bc001a57b1af97a9e62732ff13e3aeb7bc53b330202b6bd3747272c64d87f257dbba533e75a183a3 languageName: node linkType: hard @@ -7724,7 +7772,7 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/api-metrics@npm:0.33.0, @opentelemetry/api-metrics@npm:^0.33.0": +"@opentelemetry/api-metrics@npm:^0.33.0": version: 0.33.0 resolution: "@opentelemetry/api-metrics@npm:0.33.0" dependencies: @@ -7733,14 +7781,14 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/api@npm:1.4.0": +"@opentelemetry/api@npm:1.4.0, @opentelemetry/api@npm:^1.4.0": version: 1.4.0 resolution: "@opentelemetry/api@npm:1.4.0" checksum: 8dc522194e20d2e8aa6cac155dbce19d3fc9cfac59e953ece1064158c6348ccd9560ee99d2f2381e82c2f8c9a129b57fa7b640027383315504de1fa712b6d7f1 languageName: node linkType: hard -"@opentelemetry/api@npm:^1.0.0, @opentelemetry/api@npm:^1.1.0": +"@opentelemetry/api@npm:^1.0.0": version: 1.1.0 resolution: "@opentelemetry/api@npm:1.1.0" checksum: 8be8e8dd20a473639a9bb9b4185b8984f537f86e49829ba1d4c4e909f4480309cb22696b7eb7122882878dac0b5f4ce799d66ed72248568bafed085d6269e1bc @@ -7759,14 +7807,14 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/core@npm:1.7.0": - version: 1.7.0 - resolution: "@opentelemetry/core@npm:1.7.0" +"@opentelemetry/core@npm:1.9.1": + version: 1.9.1 + resolution: "@opentelemetry/core@npm:1.9.1" dependencies: - "@opentelemetry/semantic-conventions": 1.7.0 + "@opentelemetry/semantic-conventions": 1.9.1 peerDependencies: - "@opentelemetry/api": ">=1.0.0 <1.3.0" - checksum: 94fcae57c3c2c3a1cff6311246f32a228b216533449bfcec2f8eb03ea023f0ace4e0929c8cf5145772c6f25263d5f2d5d3485a39ab0ced4e11f5a0fed7497e9c + "@opentelemetry/api": ">=1.0.0 <1.5.0" + checksum: 5581a809e2caff142136734634f45255ce9f1ed701cf38629b9e17d91a8d15449b467fb3a7f3d0d8b076f653090e50cc31d3b1db4cfefeda9b6b901c60581024 languageName: node linkType: hard @@ -7785,18 +7833,17 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/otlp-transformer@npm:^0.33.0": - version: 0.33.0 - resolution: "@opentelemetry/otlp-transformer@npm:0.33.0" +"@opentelemetry/otlp-transformer@npm:^0.35.0": + version: 0.35.1 + resolution: "@opentelemetry/otlp-transformer@npm:0.35.1" dependencies: - "@opentelemetry/api-metrics": 0.33.0 - "@opentelemetry/core": 1.7.0 - "@opentelemetry/resources": 1.7.0 - "@opentelemetry/sdk-metrics": 0.33.0 - "@opentelemetry/sdk-trace-base": 1.7.0 + "@opentelemetry/core": 1.9.1 + "@opentelemetry/resources": 1.9.1 + "@opentelemetry/sdk-metrics": 1.9.1 + "@opentelemetry/sdk-trace-base": 1.9.1 peerDependencies: - "@opentelemetry/api": ">=1.0.0 <1.3.0" - checksum: f2a68957588a5bf7de3974dd210440dc64f29dd93c3f6f79c2e0cf69d3e487432cfec6d2c14752d7ba007f93140a23f8ea39135451e7bf455789b4a2d8ad84d1 + "@opentelemetry/api": ">=1.3.0 <1.5.0" + checksum: e0a68b2be28d5535aaaa58be31a5e85b268b42025c8b3a34498f00b019fbd172530b1b484c4257fd330c5890007130eedec288b8fe8f2b85176198f9ffc2507e languageName: node linkType: hard @@ -7812,15 +7859,15 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/resources@npm:1.7.0": - version: 1.7.0 - resolution: "@opentelemetry/resources@npm:1.7.0" +"@opentelemetry/resources@npm:1.9.1": + version: 1.9.1 + resolution: "@opentelemetry/resources@npm:1.9.1" dependencies: - "@opentelemetry/core": 1.7.0 - "@opentelemetry/semantic-conventions": 1.7.0 + "@opentelemetry/core": 1.9.1 + "@opentelemetry/semantic-conventions": 1.9.1 peerDependencies: - "@opentelemetry/api": ">=1.0.0 <1.3.0" - checksum: 9d669e4170120ef240757f9d82b5ef411335606114d19bd9f7a534a8328638871de0a06487f5da2dc0eb2ea540bb3ccbeea2f41c75a27de4e13e270452dd38eb + "@opentelemetry/api": ">=1.0.0 <1.5.0" + checksum: cf15e5faa698df3f0abcee35f7b4271c019b6cb81cb521b07793fe622c716d9c6873216219879afd57a28202f748a839ecaf28e04268e490004f14bbb850c96e languageName: node linkType: hard @@ -7838,17 +7885,16 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/sdk-metrics@npm:0.33.0": - version: 0.33.0 - resolution: "@opentelemetry/sdk-metrics@npm:0.33.0" +"@opentelemetry/sdk-metrics@npm:1.9.1": + version: 1.9.1 + resolution: "@opentelemetry/sdk-metrics@npm:1.9.1" dependencies: - "@opentelemetry/api-metrics": 0.33.0 - "@opentelemetry/core": 1.7.0 - "@opentelemetry/resources": 1.7.0 + "@opentelemetry/core": 1.9.1 + "@opentelemetry/resources": 1.9.1 lodash.merge: 4.6.2 peerDependencies: - "@opentelemetry/api": ^1.0.0 - checksum: 2c99c7ece4e545a3da0280e8dc9699458fdbb5c2bf9bcf09a9524612a844b28516d864ab1e0b5bedd12f9cccfc5d4f01663686538e8144b4d0185d2f8a581f0e + "@opentelemetry/api": ">=1.3.0 <1.5.0" + checksum: 08e8215841da74ffc36f2e5c414ab4230e5a2676a4329f9c98fbde4b162b6166612ab48bcc701cebf8ded3e2b5a68981f72987751df4393e0839089fa0c5ee13 languageName: node linkType: hard @@ -7866,16 +7912,16 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/sdk-trace-base@npm:1.7.0": - version: 1.7.0 - resolution: "@opentelemetry/sdk-trace-base@npm:1.7.0" +"@opentelemetry/sdk-trace-base@npm:1.9.1": + version: 1.9.1 + resolution: "@opentelemetry/sdk-trace-base@npm:1.9.1" dependencies: - "@opentelemetry/core": 1.7.0 - "@opentelemetry/resources": 1.7.0 - "@opentelemetry/semantic-conventions": 1.7.0 + "@opentelemetry/core": 1.9.1 + "@opentelemetry/resources": 1.9.1 + "@opentelemetry/semantic-conventions": 1.9.1 peerDependencies: - "@opentelemetry/api": ">=1.0.0 <1.3.0" - checksum: f6ebfe1614d481ab11f4ebca4ae45ae92790e9b27a6b30cdeedf968918ac85e6d5cd695dd26ba3d0db0e665830a9c73ed25b4bd286c019c4adf084a05776bb9c + "@opentelemetry/api": ">=1.0.0 <1.5.0" + checksum: f9448132686b1a8c1fde7539845a2b31bcb315c3bbabccb20a18142db80eeed433b3713e2761151348c1b626ad00183f4b7e9b9868d1a8ab8c541dce1d082f38 languageName: node linkType: hard @@ -7886,13 +7932,6 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/semantic-conventions@npm:1.7.0": - version: 1.7.0 - resolution: "@opentelemetry/semantic-conventions@npm:1.7.0" - checksum: 5214501648a002cc92fc6ad2296be8fa95b1803b360fc7732222e74a7f3743f3f72952740bed18ea9207741e450f3a32bf11752d732dc83f8770681eb8a6656b - languageName: node - linkType: hard - "@opentelemetry/semantic-conventions@npm:1.9.1": version: 1.9.1 resolution: "@opentelemetry/semantic-conventions@npm:1.9.1" @@ -7959,7 +7998,46 @@ __metadata: languageName: node linkType: hard -"@pmmmwh/react-refresh-webpack-plugin@npm:0.5.8, @pmmmwh/react-refresh-webpack-plugin@npm:^0.5.3": +"@pmmmwh/react-refresh-webpack-plugin@npm:0.5.10": + version: 0.5.10 + resolution: "@pmmmwh/react-refresh-webpack-plugin@npm:0.5.10" + dependencies: + ansi-html-community: ^0.0.8 + common-path-prefix: ^3.0.0 + core-js-pure: ^3.23.3 + error-stack-parser: ^2.0.6 + find-up: ^5.0.0 + html-entities: ^2.1.0 + loader-utils: ^2.0.4 + schema-utils: ^3.0.0 + source-map: ^0.7.3 + peerDependencies: + "@types/webpack": 4.x || 5.x + react-refresh: ">=0.10.0 <1.0.0" + sockjs-client: ^1.4.0 + type-fest: ">=0.17.0 <4.0.0" + webpack: ">=4.43.0 <6.0.0" + webpack-dev-server: 3.x || 4.x + webpack-hot-middleware: 2.x + webpack-plugin-serve: 0.x || 1.x + peerDependenciesMeta: + "@types/webpack": + optional: true + sockjs-client: + optional: true + type-fest: + optional: true + webpack-dev-server: + optional: true + webpack-hot-middleware: + optional: true + webpack-plugin-serve: + optional: true + checksum: c45beded9c56fbbdc7213a2c36131ace5db360ed704d462cc39d6678f980173a91c9a3f691e6bd3a026f25486644cd0027e8a12a0a4eced8e8b886a0472e7d34 + languageName: node + linkType: hard + +"@pmmmwh/react-refresh-webpack-plugin@npm:^0.5.3": version: 0.5.8 resolution: "@pmmmwh/react-refresh-webpack-plugin@npm:0.5.8" dependencies: @@ -8102,6 +8180,55 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-compose-refs@npm:1.0.0": + version: 1.0.0 + resolution: "@radix-ui/react-compose-refs@npm:1.0.0" + dependencies: + "@babel/runtime": ^7.13.10 + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 + checksum: fb98be2e275a1a758ccac647780ff5b04be8dcf25dcea1592db3b691fecf719c4c0700126da605b2f512dd89caa111352b9fad59528d736b4e0e9a0e134a74a1 + languageName: node + linkType: hard + +"@radix-ui/react-portal@npm:^1.0.1": + version: 1.0.1 + resolution: "@radix-ui/react-portal@npm:1.0.1" + dependencies: + "@babel/runtime": ^7.13.10 + "@radix-ui/react-primitive": 1.0.1 + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + checksum: 3bdcf6e1d918e473e328d45df659853cc0da687e4e885eaf7bd7bb76825a30e6f8384f15db3cbe523d80c5381fa9886f80718a8679ff66a7a10167aab290c4f7 + languageName: node + linkType: hard + +"@radix-ui/react-primitive@npm:1.0.1": + version: 1.0.1 + resolution: "@radix-ui/react-primitive@npm:1.0.1" + dependencies: + "@babel/runtime": ^7.13.10 + "@radix-ui/react-slot": 1.0.1 + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + checksum: 1cc86b72f926be4a42122e7e456e965de0906f16b0dc244b8448bac05905f208598c984a0dd40026f654b4a71d0235335d48a18e377b07b0ec6c6917576a8080 + languageName: node + linkType: hard + +"@radix-ui/react-slot@npm:1.0.1": + version: 1.0.1 + resolution: "@radix-ui/react-slot@npm:1.0.1" + dependencies: + "@babel/runtime": ^7.13.10 + "@radix-ui/react-compose-refs": 1.0.0 + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 + checksum: a20693f8ce532bd6cbff12ba543dfcf90d451f22923bd60b57dc9e639f6e53348915e182002b33444feb6ab753434e78e2a54085bf7092aadda4418f0423763f + languageName: node + linkType: hard + "@rc-component/portal@npm:^1.0.0-6": version: 1.0.3 resolution: "@rc-component/portal@npm:1.0.3" @@ -8123,33 +8250,6 @@ __metadata: languageName: node linkType: hard -"@reach/portal@npm:^0.16.0": - version: 0.16.2 - resolution: "@reach/portal@npm:0.16.2" - dependencies: - "@reach/utils": 0.16.0 - tiny-warning: ^1.0.3 - tslib: ^2.3.0 - peerDependencies: - react: ^16.8.0 || 17.x - react-dom: ^16.8.0 || 17.x - checksum: 7413dcd169cfb9854dd0d3a01f811ec19ef170558fcbd00118d676fc02c197d1c0bce1d1357508879d4775169561d103e13a8e4d74cc677eb0037cc7b04f7a1e - languageName: node - linkType: hard - -"@reach/utils@npm:0.16.0": - version: 0.16.0 - resolution: "@reach/utils@npm:0.16.0" - dependencies: - tiny-warning: ^1.0.3 - tslib: ^2.3.0 - peerDependencies: - react: ^16.8.0 || 17.x - react-dom: ^16.8.0 || 17.x - checksum: 36bc0eb41a71798eb6186b23de265ba709e51dae5bf214fb8505c66bb3f2e6a41bb2401457350436ba89ca9e3a50f93a04fe7c33d15648ce11e568a85622d770 - languageName: node - linkType: hard - "@react-aria/button@npm:3.6.1": version: 3.6.1 resolution: "@react-aria/button@npm:3.6.1" @@ -8504,14 +8604,14 @@ __metadata: languageName: node linkType: hard -"@react-types/button@npm:3.7.0": - version: 3.7.0 - resolution: "@react-types/button@npm:3.7.0" +"@react-types/button@npm:3.7.1": + version: 3.7.1 + resolution: "@react-types/button@npm:3.7.1" dependencies: - "@react-types/shared": ^3.16.0 + "@react-types/shared": ^3.17.0 peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 - checksum: e5c1bc38b20d2e123492ea59b9c1637671b83d1b5081b3ac414bc20858ff3300465c7b4bc9b714f3e062bf471eebfe7a68210e23a8731bba1ae1b0ae7a2cd16d + checksum: 4002e7af86a2673e0d1855ccadda4afd13579b5c3a71cf423465d4f5c2f26270d9ca4ce831a0ae23b503c59c8e3ee21c7f8a792a26abba381a7662b945f43cc3 languageName: node linkType: hard @@ -8595,7 +8695,7 @@ __metadata: languageName: node linkType: hard -"@react-types/shared@npm:3.16.0, @react-types/shared@npm:^3.16.0": +"@react-types/shared@npm:3.16.0": version: 3.16.0 resolution: "@react-types/shared@npm:3.16.0" peerDependencies: @@ -8613,9 +8713,18 @@ __metadata: languageName: node linkType: hard -"@reduxjs/toolkit@npm:1.9.1": - version: 1.9.1 - resolution: "@reduxjs/toolkit@npm:1.9.1" +"@react-types/shared@npm:^3.17.0": + version: 3.17.0 + resolution: "@react-types/shared@npm:3.17.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: 004fc58ab0d3d64a84ce5a98e7e201b88bbb64f8b4a8309d50be30fe6172d0f172f00c666074aa96f13bbbfbced8b986901ad6b35b6d2d32d8dc25e251fcdb31 + languageName: node + linkType: hard + +"@reduxjs/toolkit@npm:1.9.3": + version: 1.9.3 + resolution: "@reduxjs/toolkit@npm:1.9.3" dependencies: immer: ^9.0.16 redux: ^4.2.0 @@ -8629,7 +8738,7 @@ __metadata: optional: true react-redux: optional: true - checksum: e6700a0d45198ab525c96ff0425fa0125fbdc37ce514f0c77c30225837113279ceec9190ac3da35cb20e77553e56342021788bbf17465819068c4db34cb3d87f + checksum: d965fc6197fd420e4b8eb714015aa908d4ea214e6e10889cf4521e64ac4f8e0a7de29ee4e8da63b291acc40ac330dd31682334090d54838add73c746dc590650 languageName: node linkType: hard @@ -10342,137 +10451,95 @@ __metadata: languageName: node linkType: hard -"@swc/core-android-arm-eabi@npm:1.3.11": - version: 1.3.11 - resolution: "@swc/core-android-arm-eabi@npm:1.3.11" - dependencies: - "@swc/wasm": 1.2.122 - conditions: os=android & cpu=arm - languageName: node - linkType: hard - -"@swc/core-android-arm64@npm:1.3.11": - version: 1.3.11 - resolution: "@swc/core-android-arm64@npm:1.3.11" - dependencies: - "@swc/wasm": 1.2.130 - conditions: os=android & cpu=arm64 - languageName: node - linkType: hard - -"@swc/core-darwin-arm64@npm:1.3.11": - version: 1.3.11 - resolution: "@swc/core-darwin-arm64@npm:1.3.11" +"@swc/core-darwin-arm64@npm:1.3.38": + version: 1.3.38 + resolution: "@swc/core-darwin-arm64@npm:1.3.38" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@swc/core-darwin-x64@npm:1.3.11": - version: 1.3.11 - resolution: "@swc/core-darwin-x64@npm:1.3.11" +"@swc/core-darwin-x64@npm:1.3.38": + version: 1.3.38 + resolution: "@swc/core-darwin-x64@npm:1.3.38" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@swc/core-freebsd-x64@npm:1.3.11": - version: 1.3.11 - resolution: "@swc/core-freebsd-x64@npm:1.3.11" - dependencies: - "@swc/wasm": 1.2.130 - conditions: os=freebsd & cpu=x64 - languageName: node - linkType: hard - -"@swc/core-linux-arm-gnueabihf@npm:1.3.11": - version: 1.3.11 - resolution: "@swc/core-linux-arm-gnueabihf@npm:1.3.11" - dependencies: - "@swc/wasm": 1.2.130 +"@swc/core-linux-arm-gnueabihf@npm:1.3.38": + version: 1.3.38 + resolution: "@swc/core-linux-arm-gnueabihf@npm:1.3.38" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@swc/core-linux-arm64-gnu@npm:1.3.11": - version: 1.3.11 - resolution: "@swc/core-linux-arm64-gnu@npm:1.3.11" +"@swc/core-linux-arm64-gnu@npm:1.3.38": + version: 1.3.38 + resolution: "@swc/core-linux-arm64-gnu@npm:1.3.38" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@swc/core-linux-arm64-musl@npm:1.3.11": - version: 1.3.11 - resolution: "@swc/core-linux-arm64-musl@npm:1.3.11" +"@swc/core-linux-arm64-musl@npm:1.3.38": + version: 1.3.38 + resolution: "@swc/core-linux-arm64-musl@npm:1.3.38" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@swc/core-linux-x64-gnu@npm:1.3.11": - version: 1.3.11 - resolution: "@swc/core-linux-x64-gnu@npm:1.3.11" +"@swc/core-linux-x64-gnu@npm:1.3.38": + version: 1.3.38 + resolution: "@swc/core-linux-x64-gnu@npm:1.3.38" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@swc/core-linux-x64-musl@npm:1.3.11": - version: 1.3.11 - resolution: "@swc/core-linux-x64-musl@npm:1.3.11" +"@swc/core-linux-x64-musl@npm:1.3.38": + version: 1.3.38 + resolution: "@swc/core-linux-x64-musl@npm:1.3.38" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@swc/core-win32-arm64-msvc@npm:1.3.11": - version: 1.3.11 - resolution: "@swc/core-win32-arm64-msvc@npm:1.3.11" - dependencies: - "@swc/wasm": 1.2.130 +"@swc/core-win32-arm64-msvc@npm:1.3.38": + version: 1.3.38 + resolution: "@swc/core-win32-arm64-msvc@npm:1.3.38" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@swc/core-win32-ia32-msvc@npm:1.3.11": - version: 1.3.11 - resolution: "@swc/core-win32-ia32-msvc@npm:1.3.11" - dependencies: - "@swc/wasm": 1.2.130 +"@swc/core-win32-ia32-msvc@npm:1.3.38": + version: 1.3.38 + resolution: "@swc/core-win32-ia32-msvc@npm:1.3.38" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@swc/core-win32-x64-msvc@npm:1.3.11": - version: 1.3.11 - resolution: "@swc/core-win32-x64-msvc@npm:1.3.11" +"@swc/core-win32-x64-msvc@npm:1.3.38": + version: 1.3.38 + resolution: "@swc/core-win32-x64-msvc@npm:1.3.38" conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"@swc/core@npm:1.3.11": - version: 1.3.11 - resolution: "@swc/core@npm:1.3.11" +"@swc/core@npm:1.3.38": + version: 1.3.38 + resolution: "@swc/core@npm:1.3.38" dependencies: - "@swc/core-android-arm-eabi": 1.3.11 - "@swc/core-android-arm64": 1.3.11 - "@swc/core-darwin-arm64": 1.3.11 - "@swc/core-darwin-x64": 1.3.11 - "@swc/core-freebsd-x64": 1.3.11 - "@swc/core-linux-arm-gnueabihf": 1.3.11 - "@swc/core-linux-arm64-gnu": 1.3.11 - "@swc/core-linux-arm64-musl": 1.3.11 - "@swc/core-linux-x64-gnu": 1.3.11 - "@swc/core-linux-x64-musl": 1.3.11 - "@swc/core-win32-arm64-msvc": 1.3.11 - "@swc/core-win32-ia32-msvc": 1.3.11 - "@swc/core-win32-x64-msvc": 1.3.11 + "@swc/core-darwin-arm64": 1.3.38 + "@swc/core-darwin-x64": 1.3.38 + "@swc/core-linux-arm-gnueabihf": 1.3.38 + "@swc/core-linux-arm64-gnu": 1.3.38 + "@swc/core-linux-arm64-musl": 1.3.38 + "@swc/core-linux-x64-gnu": 1.3.38 + "@swc/core-linux-x64-musl": 1.3.38 + "@swc/core-win32-arm64-msvc": 1.3.38 + "@swc/core-win32-ia32-msvc": 1.3.38 + "@swc/core-win32-x64-msvc": 1.3.38 dependenciesMeta: - "@swc/core-android-arm-eabi": - optional: true - "@swc/core-android-arm64": - optional: true "@swc/core-darwin-arm64": optional: true "@swc/core-darwin-x64": optional: true - "@swc/core-freebsd-x64": - optional: true "@swc/core-linux-arm-gnueabihf": optional: true "@swc/core-linux-arm64-gnu": @@ -10489,32 +10556,16 @@ __metadata: optional: true "@swc/core-win32-x64-msvc": optional: true - bin: - swcx: run_swcx.js - checksum: 1d617f0707700b1b4137487acad24e2515c99cfcaba45e7ca3032acecc4faa6146cfcf20eca7a9c57e01f3cbea7995d5c46216cf665de897f6e4e2e4d4a7fa86 + checksum: c55d30e57638bcd21f788add8490c3f3e71bfe027aa5a8b153e1b1b9686ecddd6deeaaa6a6b17717c7eab4c1e2a232b465b6755b6c891506fc0d03139badfbf7 languageName: node linkType: hard -"@swc/helpers@npm:0.4.12": - version: 0.4.12 - resolution: "@swc/helpers@npm:0.4.12" +"@swc/helpers@npm:0.4.14": + version: 0.4.14 + resolution: "@swc/helpers@npm:0.4.14" dependencies: tslib: ^2.4.0 - checksum: 3f9112f37d87815b6d4270137fc78d22bb98c75138e9b0eac7cac203ec2cf2bffbf13b20a713067c292affd5e9e70a724eb245b8daf0963e7fe528b901771c28 - languageName: node - linkType: hard - -"@swc/wasm@npm:1.2.122": - version: 1.2.122 - resolution: "@swc/wasm@npm:1.2.122" - checksum: 563345370c5ad18373d3b403590ab880fe52dcd8fc8c8601be263fcd9886520b28a7f4e46236cf49ca2b136c79d4ef50c960bc34b7cdc2068118b0d84dfca1f4 - languageName: node - linkType: hard - -"@swc/wasm@npm:1.2.130": - version: 1.2.130 - resolution: "@swc/wasm@npm:1.2.130" - checksum: 02203bfef3e382c64cbbd63c138c8fdf61865e74d923b317e9d9e9f33f5a3f0a9533b5fdbc9505e76d78e864be04a82fc847eb987a1e47ccac5850146c858292 + checksum: 273fd3f3fc461a92f3790cc551ea054745c6d6959afbe1232e6d7aa1c722bbc114d308aab96bef5c78fc0303c85c7b472ef00e2253251cc89737f3b1af56e5a5 languageName: node linkType: hard @@ -10744,12 +10795,12 @@ __metadata: languageName: node linkType: hard -"@types/chrome-remote-interface@npm:0.31.4": - version: 0.31.4 - resolution: "@types/chrome-remote-interface@npm:0.31.4" +"@types/chrome-remote-interface@npm:0.31.9": + version: 0.31.9 + resolution: "@types/chrome-remote-interface@npm:0.31.9" dependencies: devtools-protocol: 0.0.927104 - checksum: 91c6cf9c749adedc08458b772db12f4142172f36fe885dc741921d259bdb7ec47d64b9e294d793a6c36f0435c6855d6470754fd143711e0b2ff204878dd4f721 + checksum: e1b3402ccda40c44f6849243a5aa6542761dc1ea57529b28fc053c2968288846ebb6cd9d5c19bcbdd466d3b76c0c24f3065b9fa57b12689abad6d6530446b014 languageName: node linkType: hard @@ -11114,10 +11165,10 @@ __metadata: languageName: node linkType: hard -"@types/debounce-promise@npm:3.1.5": - version: 3.1.5 - resolution: "@types/debounce-promise@npm:3.1.5" - checksum: 53ed58c3b9ee2300dcb233b3fcc1eaa4ef2921ee09833deb51143cb42f371e1090414aeccdb53dc2df013edc40b51b9addb50eeb163864ca8dbe94e1b4d8505d +"@types/debounce-promise@npm:3.1.6": + version: 3.1.6 + resolution: "@types/debounce-promise@npm:3.1.6" + checksum: db1e5bf86215dfcdd0b0f94497269b0be5b27e0b9622bbad53c7a24de078229b97927f848549bb661795575d80615fd935f8357d8f23928c0526b158b51fa122 languageName: node linkType: hard @@ -11159,13 +11210,13 @@ __metadata: languageName: node linkType: hard -"@types/eslint@npm:8.21.0": - version: 8.21.0 - resolution: "@types/eslint@npm:8.21.0" +"@types/eslint@npm:8.21.1, @types/eslint@npm:^8.4.10": + version: 8.21.1 + resolution: "@types/eslint@npm:8.21.1" dependencies: "@types/estree": "*" "@types/json-schema": "*" - checksum: 48823b13e1ffbc6fe22c96d99f691a17507ef5a498c4aed95e3a9076ec6d44ff48ce8a632928b6f82bea92701ac8967bba0d78a5c9de4dfa3f2e12d26dae7da4 + checksum: 584068441e4000c7b41c8928274fdcc737bc62f564928c30eb64ec41bbdbac31612f9fedaf490bceab31ec8305e99615166428188ea345d58878394683086fae languageName: node linkType: hard @@ -11179,16 +11230,6 @@ __metadata: languageName: node linkType: hard -"@types/eslint@npm:^8.4.10": - version: 8.21.1 - resolution: "@types/eslint@npm:8.21.1" - dependencies: - "@types/estree": "*" - "@types/json-schema": "*" - checksum: 584068441e4000c7b41c8928274fdcc737bc62f564928c30eb64ec41bbdbac31612f9fedaf490bceab31ec8305e99615166428188ea345d58878394683086fae - languageName: node - linkType: hard - "@types/estree-jsx@npm:^1.0.0": version: 1.0.0 resolution: "@types/estree-jsx@npm:1.0.0" @@ -11484,12 +11525,12 @@ __metadata: languageName: node linkType: hard -"@types/jquery@npm:3.5.14": - version: 3.5.14 - resolution: "@types/jquery@npm:3.5.14" +"@types/jquery@npm:3.5.16": + version: 3.5.16 + resolution: "@types/jquery@npm:3.5.16" dependencies: "@types/sizzle": "*" - checksum: 159d6f804ed1a204b3f79f2d591a271d82e866bd45bd49fb6ef40561a25dbe0f47ec7815681b44cc2db5598425f72811e7e80ab0e983d980470998ac56feb375 + checksum: 13c995f15d1c2f1d322103dc1cb0a22b95eecc3e7546f00279b8731aea21d7ec04550af40e609ee48e755d4e11bf61c25b4aa9f53df3bcbec4b8fe8e81471732 languageName: node linkType: hard @@ -11560,10 +11601,10 @@ __metadata: languageName: node linkType: hard -"@types/lodash@npm:4.14.187": - version: 4.14.187 - resolution: "@types/lodash@npm:4.14.187" - checksum: 5f8a4fe6d8a6785b8ecbd9efdc8b7d3341b873f63c5390ad0f269a517508e92c1b1d7d43e97bdfb6bba5bba9a8501b30a54f791ef4c30b854382745c75c607d4 +"@types/lodash@npm:4.14.191": + version: 4.14.191 + resolution: "@types/lodash@npm:4.14.191" + checksum: ba0d5434e10690869f32d5ea49095250157cae502f10d57de0a723fd72229ce6c6a4979576f0f13e0aa9fbe3ce2457bfb9fa7d4ec3d6daba56730a51906d1491 languageName: node linkType: hard @@ -11597,10 +11638,10 @@ __metadata: languageName: node linkType: hard -"@types/marked@npm:4.0.7": - version: 4.0.7 - resolution: "@types/marked@npm:4.0.7" - checksum: 4907b6a606578cd864bad429aca3c234591e6ed56bd141c575140487269b825a480ace9a85e4d003d1de1f007004c9d9b2fe600038ded5bba75aef59118e58d5 +"@types/marked@npm:4.0.8": + version: 4.0.8 + resolution: "@types/marked@npm:4.0.8" + checksum: 68278fa7acaa5d920cdc239d675b5daf842e0ad4779e4848cd617d9baf2ac1afccb5a264c331e37d80031d647e1640cb983cd31e73d45b28552670b4853fad8e languageName: node linkType: hard @@ -11648,10 +11689,10 @@ __metadata: languageName: node linkType: hard -"@types/mousetrap@npm:1.6.10": - version: 1.6.10 - resolution: "@types/mousetrap@npm:1.6.10" - checksum: ee112ad18c4cd28e67f9400d8b3e36d5bfe00925c03fcd534e5c6bb1c657b8e6ba74dbd3fdc4097dcfcb240a139b7f2dc8a698515ee03b7543c92bfd43348243 +"@types/mousetrap@npm:1.6.11": + version: 1.6.11 + resolution: "@types/mousetrap@npm:1.6.11" + checksum: 4465d16b7c20ab52d2f6b7fe8087e8f16101472f2e8758f01c63591015c1b0a67c9e6c371d978f24d5f37c4b83457515571489284bf7c7c844eb6c8f2527685b languageName: node linkType: hard @@ -11686,10 +11727,10 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:18.14.0": - version: 18.14.0 - resolution: "@types/node@npm:18.14.0" - checksum: d83fcf5e4ed544755dd9028f5cbb6b9d46235043159111bb2ad62223729aee581c0144a9f6df8ba73d74011db9ed4ebd7af2fd5e0996714e3beb508a5da8ac5c +"@types/node@npm:18.14.6": + version: 18.14.6 + resolution: "@types/node@npm:18.14.6" + checksum: 2f88f482cabadc6dbddd627a1674239e68c3c9beab56eb4ae2309fb96fd17fc3a509d99b0309bafe13b58529574f49ecf3a583f2ebe2896dd32fe4be436dc96e languageName: node linkType: hard @@ -11728,21 +11769,21 @@ __metadata: languageName: node linkType: hard -"@types/ol-ext@npm:@siedlerchr/types-ol-ext@3.0.6": - version: 3.0.6 - resolution: "@siedlerchr/types-ol-ext@npm:3.0.6" +"@types/ol-ext@npm:@siedlerchr/types-ol-ext@3.0.9": + version: 3.0.9 + resolution: "@siedlerchr/types-ol-ext@npm:3.0.9" dependencies: jspdf: ^2.5.1 - checksum: d43e5c8730b04d1469407e24dfa017e38289a6f930e9d514be5811f3426b49c879e53e847f63ef5451de4194ee99eea70a7d1884fc4ab6476f00ad03c78d9f33 + checksum: 5f16d09dccd4f553c83f68eb1db854453e2ae93e284eac15055d444a798b2fdbc72e01bb51db7a8e46deb93a2444081302c53f3fd9f0fd3c8692a402974330cf languageName: node linkType: hard -"@types/papaparse@npm:5.3.5": - version: 5.3.5 - resolution: "@types/papaparse@npm:5.3.5" +"@types/papaparse@npm:5.3.7": + version: 5.3.7 + resolution: "@types/papaparse@npm:5.3.7" dependencies: "@types/node": "*" - checksum: f9833662e5536836be9586b9344757d99e22c7cfd6997ab212700c3b623491e28548676e023adfa801fc7c60f3f6d1d417dbf02c5f138442ed9b020b128a7f5f + checksum: 5ffa6fc81c0f41cd18c9c015599b3690f2a19ee553dec85e92c17bd1ef4a045e0ba4973d8bd49aed9f83fcba3b0ec280ebbe0a0def66701c0b4860c691444694 languageName: node linkType: hard @@ -11823,12 +11864,12 @@ __metadata: languageName: node linkType: hard -"@types/react-beautiful-dnd@npm:13.1.2": - version: 13.1.2 - resolution: "@types/react-beautiful-dnd@npm:13.1.2" +"@types/react-beautiful-dnd@npm:13.1.3": + version: 13.1.3 + resolution: "@types/react-beautiful-dnd@npm:13.1.3" dependencies: "@types/react": "*" - checksum: 28372854fcd4b7546aabe55ee6569da59fad10f117929cff8a9c6f928448e4dd08ed1139facca33232833b9de100edf017aab9a0ee34065f8fc0e2a7a1262b2c + checksum: e09860672c15666ee3d3acfad3dfc2ebd8fceb29b5468e12b7543af74d0e19f2eb9c4be187adb33c5d6816f47b0db958a8c0160b15826b92f6df1e8e3e34657a languageName: node linkType: hard @@ -11900,15 +11941,15 @@ __metadata: languageName: node linkType: hard -"@types/react-redux@npm:7.1.24": - version: 7.1.24 - resolution: "@types/react-redux@npm:7.1.24" +"@types/react-redux@npm:7.1.25": + version: 7.1.25 + resolution: "@types/react-redux@npm:7.1.25" dependencies: "@types/hoist-non-react-statics": ^3.3.0 "@types/react": "*" hoist-non-react-statics: ^3.3.0 redux: ^4.0.0 - checksum: 6582246581331ac7fbbd44aa1f1c136c8a9c8febbcf462432ac81302263308c21e1a2e7868beb7f73bbcb52a8e67935d133cb37f5bdcb6564eaff3a811805101 + checksum: a61ec25cbf8bb3720850402d3c49493fcff4afb73ad447d161460b5d4c600c984ad48708e8564d2fd32052eaa3c3b3f655c5b300ce813429637cce9e5958329f languageName: node linkType: hard @@ -11954,12 +11995,12 @@ __metadata: languageName: node linkType: hard -"@types/react-table@npm:7.7.12": - version: 7.7.12 - resolution: "@types/react-table@npm:7.7.12" +"@types/react-table@npm:7.7.14": + version: 7.7.14 + resolution: "@types/react-table@npm:7.7.14" dependencies: "@types/react": "*" - checksum: 287ea68e75b56c2e70e6d6491cd172348c7302b032789047eb2dca1b25b240684e48a60f58140575828b790f95724b9f3ec4d3adce82fc1c05d994def1449009 + checksum: 238047beca9abecc4b3e1e377b823b492b1a00d011b8456012bbcbd2682d5a7e0c0b1cca6384ccc41e724156e991d35e5725a5a3c4b01ff7bbaf095643569da9 languageName: node linkType: hard @@ -12413,12 +12454,12 @@ __metadata: languageName: node linkType: hard -"@types/yargs@npm:17.0.12": - version: 17.0.12 - resolution: "@types/yargs@npm:17.0.12" +"@types/yargs@npm:17.0.22": + version: 17.0.22 + resolution: "@types/yargs@npm:17.0.22" dependencies: "@types/yargs-parser": "*" - checksum: 5b41d21d8624199f89db82209b2adab2e47867b3677e852fde65698be2ca48364b14c2e70cb0adc9bca4a2102c93dad2409cae0ad666ea36ae031ae1cb08a7b5 + checksum: 0773523fda71bafdc52f13f5970039e535a353665a60ba9261149a5c9c2b908242e6e77fbb7a8c06931ec78ce889d64d09673c68ba23eb5f5742d5385d0d1982 languageName: node linkType: hard @@ -14352,16 +14393,16 @@ __metadata: languageName: node linkType: hard -"babel-loader@npm:9.1.0": - version: 9.1.0 - resolution: "babel-loader@npm:9.1.0" +"babel-loader@npm:9.1.2": + version: 9.1.2 + resolution: "babel-loader@npm:9.1.2" dependencies: find-cache-dir: ^3.3.2 schema-utils: ^4.0.0 peerDependencies: "@babel/core": ^7.12.0 webpack: ">=5" - checksum: 774758febd1e8ca804abcae3b8f65634330dc688837424d0946f06d1386914de43435cce691710fa144eccdf1292cf883439ac3598ce7320916acfaaa2372641 + checksum: f0edb8e157f9806b810ba3f2c8ca8fa489d377ae5c2b7b00c2ace900a6925641ce4ec520b9c12f70e37b94aa5366e7003e0f6271b26821643e109966ce741cb7 languageName: node linkType: hard @@ -15805,15 +15846,15 @@ __metadata: languageName: node linkType: hard -"chrome-remote-interface@npm:0.32.0": - version: 0.32.0 - resolution: "chrome-remote-interface@npm:0.32.0" +"chrome-remote-interface@npm:0.32.1": + version: 0.32.1 + resolution: "chrome-remote-interface@npm:0.32.1" dependencies: commander: 2.11.x ws: ^7.2.0 bin: chrome-remote-interface: bin/client.js - checksum: 9be5e5bce0856d3fe4e32339857514d16ca4831310de4475a8e62f604fa346d939d82058aa06411ba61d2ee3cee8d0288599221908d82cd6e548ad09dd215794 + checksum: 2a7c7e42e9a41a44242b7cecafbb5df20dfc594f87bdfbc45c6fa8c1615ff18ed9edc285df6b4b5c0901452fe9f042dbf3867c3722af530f918cb172942bd225 languageName: node linkType: hard @@ -16227,10 +16268,10 @@ __metadata: languageName: node linkType: hard -"comlink@npm:4.4.0": - version: 4.4.0 - resolution: "comlink@npm:4.4.0" - checksum: 429dc83e36d35c3dfd17e86343142ed3526fe2310a18f3816d0380eb9a162b50272bb40f7426e1c5a1f04f44203a178b60a4a0bcabed01d675d91474c53788f7 +"comlink@npm:4.4.1": + version: 4.4.1 + resolution: "comlink@npm:4.4.1" + checksum: 16d58a8f590087fc45432e31d6c138308dfd4b75b89aec0b7f7bb97ad33d810381bd2b1e608a1fb2cf05979af9cbfcdcaf1715996d5fcf77aeb013b6da3260af languageName: node linkType: hard @@ -16970,21 +17011,21 @@ __metadata: languageName: node linkType: hard -"css-loader@npm:6.7.1, css-loader@npm:^6.7.1": - version: 6.7.1 - resolution: "css-loader@npm:6.7.1" +"css-loader@npm:6.7.3": + version: 6.7.3 + resolution: "css-loader@npm:6.7.3" dependencies: icss-utils: ^5.1.0 - postcss: ^8.4.7 + postcss: ^8.4.19 postcss-modules-extract-imports: ^3.0.0 postcss-modules-local-by-default: ^4.0.0 postcss-modules-scope: ^3.0.0 postcss-modules-values: ^4.0.0 postcss-value-parser: ^4.2.0 - semver: ^7.3.5 + semver: ^7.3.8 peerDependencies: webpack: ^5.0.0 - checksum: 170fdbc630a05a43679ef60fa97694766b568dbde37adccc0faafa964fc675f08b976bc68837bb73b61d60240e8d2cbcbf51540fe94ebc9dafc56e7c46ba5527 + checksum: 473cc32b6c837c2848e2051ad1ba331c1457449f47442e75a8c480d9891451434ada241f7e3de2347e57de17fcd84610b3bcfc4a9da41102cdaedd1e17902d31 languageName: node linkType: hard @@ -17008,6 +17049,24 @@ __metadata: languageName: node linkType: hard +"css-loader@npm:^6.7.1": + version: 6.7.1 + resolution: "css-loader@npm:6.7.1" + dependencies: + icss-utils: ^5.1.0 + postcss: ^8.4.7 + postcss-modules-extract-imports: ^3.0.0 + postcss-modules-local-by-default: ^4.0.0 + postcss-modules-scope: ^3.0.0 + postcss-modules-values: ^4.0.0 + postcss-value-parser: ^4.2.0 + semver: ^7.3.5 + peerDependencies: + webpack: ^5.0.0 + checksum: 170fdbc630a05a43679ef60fa97694766b568dbde37adccc0faafa964fc675f08b976bc68837bb73b61d60240e8d2cbcbf51540fe94ebc9dafc56e7c46ba5527 + languageName: node + linkType: hard + "css-minimizer-webpack-plugin@npm:4.2.2": version: 4.2.2 resolution: "css-minimizer-webpack-plugin@npm:4.2.2" @@ -18238,10 +18297,10 @@ __metadata: languageName: node linkType: hard -"devtools-protocol@npm:0.0.1065144": - version: 0.0.1065144 - resolution: "devtools-protocol@npm:0.0.1065144" - checksum: c21ea807cacecde2493a2d5392a573ddec5fdb9cffafecb2e4a504dda70706a465ab36391bb0d71210d8037905f1c60d629eaadd2c55c438ccb747bab209e377 +"devtools-protocol@npm:0.0.1113774": + version: 0.0.1113774 + resolution: "devtools-protocol@npm:0.0.1113774" + checksum: b1026b0ec1490ab5990ca3b5c6fd8c4c7bf345b7d9f8fc7016071c5d1c8cf49b268b4a6e1799504e2dea45dbed4118b5db37c7f6df0c5586c4781df6b82fe61d languageName: node linkType: hard @@ -19809,9 +19868,9 @@ __metadata: languageName: node linkType: hard -"eslint-plugin-react@npm:7.32.1": - version: 7.32.1 - resolution: "eslint-plugin-react@npm:7.32.1" +"eslint-plugin-react@npm:7.32.2": + version: 7.32.2 + resolution: "eslint-plugin-react@npm:7.32.2" dependencies: array-includes: ^3.1.6 array.prototype.flatmap: ^1.3.1 @@ -19830,7 +19889,7 @@ __metadata: string.prototype.matchall: ^4.0.8 peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 - checksum: e20eab61161a3db6211c2bd1eb9be3e407fd14e72c06c5f39a078b6ac37427b2af6056ee70e3954249bca0a04088ae797a0c8ba909fb8802e29712de2a41262d + checksum: 2232b3b8945aa50b7773919c15cd96892acf35d2f82503667a79e2f55def90f728ed4f0e496f0f157acbe1bd4397c5615b676ae7428fe84488a544ca53feb944 languageName: node linkType: hard @@ -22040,7 +22099,7 @@ __metadata: "@babel/core": 7.20.5 "@babel/plugin-proposal-class-properties": 7.18.6 "@babel/plugin-proposal-nullish-coalescing-operator": 7.18.6 - "@babel/plugin-proposal-object-rest-spread": 7.20.2 + "@babel/plugin-proposal-object-rest-spread": 7.20.7 "@babel/plugin-proposal-optional-chaining": 7.20.7 "@babel/plugin-syntax-dynamic-import": 7.8.3 "@babel/plugin-transform-react-constant-elements": 7.20.2 @@ -22054,37 +22113,37 @@ __metadata: "@betterer/cli": 5.4.0 "@betterer/eslint": 5.4.0 "@betterer/regexp": 5.4.0 - "@daybrush/utils": 1.10.0 - "@emotion/css": 11.10.5 + "@daybrush/utils": 1.10.2 + "@emotion/css": 11.10.6 "@emotion/eslint-plugin": 11.10.0 - "@emotion/react": 11.10.5 - "@grafana/aws-sdk": 0.0.40 + "@emotion/react": 11.10.6 + "@grafana/aws-sdk": 0.0.44 "@grafana/data": "workspace:*" "@grafana/e2e": "workspace:*" "@grafana/e2e-selectors": "workspace:*" "@grafana/eslint-config": 5.1.0 "@grafana/eslint-plugin": "link:./packages/grafana-eslint-rules" "@grafana/experimental": 1.1.0 - "@grafana/faro-core": 1.0.0-beta2 - "@grafana/faro-web-sdk": 1.0.0-beta2 + "@grafana/faro-core": 1.0.0 + "@grafana/faro-web-sdk": 1.0.0 "@grafana/google-sdk": 0.0.4 "@grafana/lezer-logql": 0.1.2 - "@grafana/monaco-logql": ^0.0.6 + "@grafana/monaco-logql": ^0.0.7 "@grafana/runtime": "workspace:*" - "@grafana/scenes": ^0.0.14 + "@grafana/scenes": ^0.0.16 "@grafana/schema": "workspace:*" "@grafana/toolkit": "workspace:*" "@grafana/tsconfig": ^1.2.0-rc1 "@grafana/ui": "workspace:*" "@kusto/monaco-kusto": 5.3.6 - "@leeoniya/ufuzzy": 1.0.2 - "@lezer/common": 1.0.1 - "@lezer/highlight": 1.1.2 - "@lezer/lr": 1.3.1 + "@leeoniya/ufuzzy": 1.0.5 + "@lezer/common": 1.0.2 + "@lezer/highlight": 1.1.3 + "@lezer/lr": 1.3.3 "@opentelemetry/api": 1.4.0 "@opentelemetry/exporter-collector": 0.25.0 "@opentelemetry/semantic-conventions": 1.9.1 - "@pmmmwh/react-refresh-webpack-plugin": 0.5.8 + "@pmmmwh/react-refresh-webpack-plugin": 0.5.10 "@popperjs/core": 2.11.6 "@prometheus-io/lezer-promql": ^0.37.0-rc.1 "@react-aria/button": 3.6.1 @@ -22097,17 +22156,17 @@ __metadata: "@react-stately/collections": 3.4.1 "@react-stately/menu": 3.4.1 "@react-stately/tree": 3.3.1 - "@react-types/button": 3.7.0 + "@react-types/button": 3.7.1 "@react-types/menu": 3.7.2 "@react-types/overlays": 3.6.4 "@react-types/shared": 3.16.0 - "@reduxjs/toolkit": 1.9.1 + "@reduxjs/toolkit": 1.9.3 "@rtsao/plugin-proposal-class-properties": 7.0.1-patch.1 "@sentry/browser": 6.19.7 "@sentry/types": 6.19.7 "@sentry/utils": 6.19.7 - "@swc/core": 1.3.11 - "@swc/helpers": 0.4.12 + "@swc/core": 1.3.38 + "@swc/helpers": 0.4.14 "@testing-library/dom": 8.20.0 "@testing-library/jest-dom": 5.16.5 "@testing-library/react": 12.1.4 @@ -22119,9 +22178,9 @@ __metadata: "@types/d3": 7.4.0 "@types/d3-force": ^3.0.0 "@types/d3-scale-chromatic": 3.0.0 - "@types/debounce-promise": 3.1.5 + "@types/debounce-promise": 3.1.6 "@types/dompurify": ^2 - "@types/eslint": 8.21.0 + "@types/eslint": 8.21.1 "@types/file-saver": 2.0.5 "@types/glob": ^8.0.0 "@types/google.analytics": ^0.0.42 @@ -22129,26 +22188,26 @@ __metadata: "@types/history": 4.7.11 "@types/hoist-non-react-statics": 3.3.1 "@types/jest": 29.2.3 - "@types/jquery": 3.5.14 + "@types/jquery": 3.5.16 "@types/js-yaml": ^4.0.5 "@types/jsurl": ^1.2.28 - "@types/lodash": 4.14.187 + "@types/lodash": 4.14.191 "@types/logfmt": ^1.2.3 - "@types/mousetrap": 1.6.10 - "@types/node": 18.14.0 - "@types/ol-ext": "npm:@siedlerchr/types-ol-ext@3.0.6" - "@types/papaparse": 5.3.5 + "@types/mousetrap": 1.6.11 + "@types/node": 18.14.6 + "@types/ol-ext": "npm:@siedlerchr/types-ol-ext@3.0.9" + "@types/papaparse": 5.3.7 "@types/pluralize": ^0.0.29 "@types/prismjs": 1.26.0 "@types/react": 17.0.42 - "@types/react-beautiful-dnd": 13.1.2 + "@types/react-beautiful-dnd": 13.1.3 "@types/react-dom": 17.0.14 "@types/react-grid-layout": 1.3.2 "@types/react-highlight-words": 0.16.4 - "@types/react-redux": 7.1.24 + "@types/react-redux": 7.1.25 "@types/react-resizable": 3.0.3 "@types/react-router-dom": 5.3.3 - "@types/react-table": 7.7.12 + "@types/react-table": 7.7.14 "@types/react-test-renderer": 17.0.1 "@types/react-transition-group": 4.4.5 "@types/react-virtualized-auto-sizer": 1.0.1 @@ -22163,7 +22222,7 @@ __metadata: "@types/tinycolor2": 1.4.3 "@types/uuid": 9.0.1 "@types/webpack-env": 1.18.0 - "@types/yargs": 17.0.12 + "@types/yargs": 17.0.22 "@typescript-eslint/eslint-plugin": 5.42.0 "@typescript-eslint/parser": 5.42.0 "@visx/event": 3.0.1 @@ -22181,7 +22240,7 @@ __metadata: app: "link:./public/app" autoprefixer: 10.4.13 babel-jest: 29.3.1 - babel-loader: 9.1.0 + babel-loader: 9.1.2 babel-plugin-angularjs-annotate: 0.10.0 babel-plugin-macros: 3.1.0 baron: 3.0.3 @@ -22194,11 +22253,11 @@ __metadata: classnames: 2.3.2 codeowners: ^5.1.1 combokeys: ^3.0.0 - comlink: 4.4.0 + comlink: 4.4.1 common-tags: 1.8.2 copy-webpack-plugin: 11.0.0 core-js: 3.28.0 - css-loader: 6.7.1 + css-loader: 6.7.3 css-minimizer-webpack-plugin: 4.2.2 cypress: 9.5.1 d3: 7.8.2 @@ -22219,7 +22278,7 @@ __metadata: eslint-plugin-jsdoc: 40.0.1 eslint-plugin-jsx-a11y: 6.7.1 eslint-plugin-lodash: 7.4.0 - eslint-plugin-react: 7.32.1 + eslint-plugin-react: 7.32.2 eslint-plugin-react-hooks: 4.6.0 eslint-webpack-plugin: 4.0.0 eventemitter3: 5.0.0 @@ -22235,11 +22294,11 @@ __metadata: html-loader: 4.2.0 html-webpack-plugin: 5.5.0 http-server: 14.1.1 - husky: 8.0.1 + husky: 8.0.3 i18next: ^22.0.0 i18next-parser: 6.6.0 - immer: 9.0.16 - immutable: 4.2.2 + immer: 9.0.19 + immutable: 4.2.4 jest: 29.3.1 jest-canvas-mock: 2.4.0 jest-date-mock: 1.0.8 @@ -22247,14 +22306,14 @@ __metadata: jest-fail-on-console: 3.0.2 jest-junit: 15.0.0 jest-matcher-utils: 29.3.1 - jquery: 3.6.1 + jquery: 3.6.3 js-yaml: ^4.1.0 json-markup: ^1.1.0 json-source-map: 0.6.1 jsurl: ^0.1.5 - kbar: 0.1.0-beta.36 + kbar: 0.1.0-beta.40 lerna: 5.5.4 - lint-staged: 13.1.0 + lint-staged: 13.1.2 lodash: 4.17.21 logfmt: ^1.3.2 lru-cache: 7.17.0 @@ -22262,7 +22321,7 @@ __metadata: memoize-one: 6.0.0 mini-css-extract-plugin: 2.7.2 moment: 2.29.4 - moment-timezone: 0.5.38 + moment-timezone: 0.5.41 monaco-editor: 0.34.0 monaco-promql: 1.7.4 mousetrap: 1.6.5 @@ -22273,22 +22332,22 @@ __metadata: ngtemplate-loader: 2.1.0 node-notifier: 10.0.1 ol: 7.2.2 - ol-ext: 4.0.3 + ol-ext: 4.0.6 papaparse: 5.3.2 pluralize: ^8.0.0 - postcss: 8.4.18 - postcss-loader: 7.0.1 + postcss: 8.4.21 + postcss-loader: 7.0.2 postcss-reporter: 7.0.5 - postcss-scss: 4.0.5 - prettier: 2.8.1 + postcss-scss: 4.0.6 + prettier: 2.8.4 prismjs: 1.29.0 prop-types: 15.8.1 pseudoizer: ^0.1.0 rc-cascader: 3.8.0 - rc-drawer: 6.1.2 - rc-slider: 10.1.0 + rc-drawer: 6.1.3 + rc-slider: 10.1.1 rc-time-picker: 3.7.3 - rc-tree: 5.7.0 + rc-tree: 5.7.2 re-resizable: 6.9.9 react: 17.0.2 react-awesome-query-builder: 5.4.0 @@ -22302,7 +22361,7 @@ __metadata: react-highlight-words: 0.20.0 react-hook-form: 7.5.3 react-i18next: ^12.0.0 - react-inlinesvg: 3.0.1 + react-inlinesvg: 3.0.2 react-moveable: 0.46.1 react-popper: 2.3.0 react-popper-tooltip: 4.4.2 @@ -22323,16 +22382,16 @@ __metadata: react-virtualized-auto-sizer: 1.0.7 react-window: 1.8.8 react-window-infinite-loader: 1.0.8 - redux: 4.2.0 + redux: 4.2.1 redux-mock-store: 1.5.4 - redux-thunk: 2.4.1 - regenerator-runtime: 0.13.10 - reselect: 4.1.6 + redux-thunk: 2.4.2 + regenerator-runtime: 0.13.11 + reselect: 4.1.7 rimraf: 4.2.0 rst2html: "github:thoward/rst2html#990cb89f2a300cdd9151790be377c4c0840df809" rudder-sdk-js: 2.25.0 rxjs: 7.8.0 - sass: 1.58.2 + sass: 1.58.3 sass-loader: 13.2.0 selecto: 1.22.0 semver: 7.3.8 @@ -22343,7 +22402,7 @@ __metadata: sql-formatter-plus: ^1.3.6 style-loader: 3.3.1 stylelint: 15.2.0 - stylelint-config-prettier: 9.0.3 + stylelint-config-prettier: 9.0.5 stylelint-config-sass-guidelines: 9.0.1 symbol-observable: 4.0.0 terser-webpack-plugin: 5.3.6 @@ -22351,7 +22410,7 @@ __metadata: testing-library-selector: 0.2.1 tether-drop: "https://github.com/torkelo/drop" tinycolor2: 1.6.0 - ts-jest: 29.0.3 + ts-jest: 29.0.5 ts-loader: 9.3.1 ts-node: 10.9.1 tslib: 2.5.0 @@ -23224,12 +23283,12 @@ __metadata: languageName: node linkType: hard -"husky@npm:8.0.1": - version: 8.0.1 - resolution: "husky@npm:8.0.1" +"husky@npm:8.0.3": + version: 8.0.3 + resolution: "husky@npm:8.0.3" bin: husky: lib/bin.js - checksum: 943a73a13d0201318fd30e83d299bb81d866bd245b69e6277804c3b462638dc1921694cb94c2b8c920a4a187060f7d6058d3365152865406352e934c5fff70dc + checksum: 837bc7e4413e58c1f2946d38fb050f5d7324c6f16b0fd66411ffce5703b294bd21429e8ba58711cd331951ee86ed529c5be4f76805959ff668a337dbfa82a1b0 languageName: node linkType: hard @@ -23373,7 +23432,14 @@ __metadata: languageName: node linkType: hard -"immer@npm:9.0.16, immer@npm:^9.0.16": +"immer@npm:9.0.19": + version: 9.0.19 + resolution: "immer@npm:9.0.19" + checksum: f02ee53989989c287cd548a3d817fccf0bfe56db919755ee94a72ea3ae78a00363fba93ee6c010fe54a664380c29c53d44ed4091c6a86cae60957ad2cfabc010 + languageName: node + linkType: hard + +"immer@npm:^9.0.16": version: 9.0.16 resolution: "immer@npm:9.0.16" checksum: e9a5ca65c929b329da7a3b7beccf7984271cda7bdd47b2cab619eac3277dcd56598c211b55cc340786b6eff0c06652ac018808d9fd744443f06882364dece6bc @@ -23387,10 +23453,10 @@ __metadata: languageName: node linkType: hard -"immutable@npm:4.2.2": - version: 4.2.2 - resolution: "immutable@npm:4.2.2" - checksum: 4d6437ea9388fe8ceca7eed5c768cf438cda7fa14d2831b87b90aa00cc60d536964d107c255b8a2e5dbf4f44a0e1295afbb9d1f0a65fb4f57b936e71df601862 +"immutable@npm:4.2.4": + version: 4.2.4 + resolution: "immutable@npm:4.2.4" + checksum: 3be84eded37b05e65cad57bfba630bc1bf170c498b7472144bc02d2650cc9baef79daf03574a9c2e41d195ebb55a1c12c9b312f41ee324b653927b24ad8bcaa7 languageName: node linkType: hard @@ -25965,10 +26031,10 @@ __metadata: languageName: node linkType: hard -"jquery@npm:3.6.1": - version: 3.6.1 - resolution: "jquery@npm:3.6.1" - checksum: 6177d866a74f1137cad800f142c7cdbd5ab19cd4282546f8bdb4890c9f933b1d542ab96f2aa15d007e43c98de7315b0513e849ec5359d3ac5640f720892fe547 +"jquery@npm:3.6.3": + version: 3.6.3 + resolution: "jquery@npm:3.6.3" + checksum: 0fd366bdcaa0c84a7a8751ce20f8192290141913978b5059574426d9b01f4365daa675f95aab3eec94fd794d27b08d32078a2236bef404b8ba78073009988ce6 languageName: node linkType: hard @@ -26381,11 +26447,11 @@ __metadata: languageName: node linkType: hard -"kbar@npm:0.1.0-beta.36": - version: 0.1.0-beta.36 - resolution: "kbar@npm:0.1.0-beta.36" +"kbar@npm:0.1.0-beta.40": + version: 0.1.0-beta.40 + resolution: "kbar@npm:0.1.0-beta.40" dependencies: - "@reach/portal": ^0.16.0 + "@radix-ui/react-portal": ^1.0.1 command-score: ^0.1.2 fast-equals: ^2.0.3 react-virtual: ^2.8.2 @@ -26393,7 +26459,7 @@ __metadata: peerDependencies: react: ^16.0.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 - checksum: 62534a24789bec899432503debf88056b8f66e742c9dab0ecb6a0615f89acfdd14a14a4da5c5d6db77ed81d84d6634dbe82aa948470b166f6819aac551ed63c4 + checksum: 0c2cbe520e48ba210cbcfd04529178a772c112ea8e674953bbb6343f74109bf3fd3cb8d23bb4235093f04018b28e8e2e485852d23c39dfe96eab6189cc82c361 languageName: node linkType: hard @@ -26707,9 +26773,9 @@ __metadata: languageName: node linkType: hard -"lint-staged@npm:13.1.0": - version: 13.1.0 - resolution: "lint-staged@npm:13.1.0" +"lint-staged@npm:13.1.2": + version: 13.1.2 + resolution: "lint-staged@npm:13.1.2" dependencies: cli-truncate: ^3.1.0 colorette: ^2.0.19 @@ -26726,7 +26792,7 @@ __metadata: yaml: ^2.1.3 bin: lint-staged: bin/lint-staged.js - checksum: adf20c4ca9285c4a93b06598b970d71b04cfe58a1a4c9006f753b83e02c1c622d1866c32a4f1e7e29a98091c501eac3345f7678af247b4f97d5be88b3d8727c1 + checksum: f854ad5c88542b8f06e27f3b4046927a4f3d4a451a04e079526559d819a325762268f65bd2df7156bcc0cb5f531f621c42cdb824b403f537c78305adc9e56a54 languageName: node linkType: hard @@ -27076,6 +27142,15 @@ __metadata: languageName: node linkType: hard +"lru-cache@npm:^5.1.1": + version: 5.1.1 + resolution: "lru-cache@npm:5.1.1" + dependencies: + yallist: ^3.0.2 + checksum: c154ae1cbb0c2206d1501a0e94df349653c92c8cbb25236d7e85190bcaf4567a03ac6eb43166fabfa36fd35623694da7233e88d9601fbf411a9a481d85dbd2cb + languageName: node + linkType: hard + "lru-cache@npm:^6.0.0": version: 6.0.0 resolution: "lru-cache@npm:6.0.0" @@ -27274,12 +27349,12 @@ __metadata: languageName: node linkType: hard -"marked@npm:4.2.0": - version: 4.2.0 - resolution: "marked@npm:4.2.0" +"marked@npm:4.2.12": + version: 4.2.12 + resolution: "marked@npm:4.2.12" bin: marked: bin/marked.js - checksum: d3e3c2d2e192a518060ed6d4a6a78c467d705361ee03f5b3c0ddcf28f530335632340e222662f963935b60f72e40feca992352241bb94c5681106af989337600 + checksum: bd551cd61028ee639d4ca2ccdfcc5a6ba4227c1b143c4538f3cde27f569dcb57df8e6313560394645b418b84a7336c07ab1e438b89b6324c29d7d8cdd3102d63 languageName: node linkType: hard @@ -28488,16 +28563,16 @@ __metadata: languageName: node linkType: hard -"moment-timezone@npm:0.5.38": - version: 0.5.38 - resolution: "moment-timezone@npm:0.5.38" +"moment-timezone@npm:0.5.41": + version: 0.5.41 + resolution: "moment-timezone@npm:0.5.41" dependencies: - moment: ">= 2.9.0" - checksum: ff7077de41f2c41a0026cd2b310154c14df8f918331a4ebe88f5872a599deb5e463b123c96990dc447b7474a81c9f3aef7ac57c3d0dfc3bfb9af2cc5b0bca826 + moment: ^2.29.4 + checksum: 30bf42265f749d4d17e78cf94f49d8354d9fbf2dea060a5b89895979642035734512a23cb7a90c0e93593bc11eb698b78b601b7dd5d9a708eb7c4f733a927a71 languageName: node linkType: hard -"moment@npm:2.29.4, moment@npm:2.x, moment@npm:>= 2.9.0, moment@npm:^2.20.1, moment@npm:^2.29.4": +"moment@npm:2.29.4, moment@npm:2.x, moment@npm:^2.20.1, moment@npm:^2.29.4": version: 2.29.4 resolution: "moment@npm:2.29.4" checksum: 0ec3f9c2bcba38dc2451b1daed5daded747f17610b92427bebe1d08d48d8b7bdd8d9197500b072d14e326dd0ccf3e326b9e3d07c5895d3d49e39b6803b76e80e @@ -29561,12 +29636,12 @@ __metadata: languageName: node linkType: hard -"ol-ext@npm:4.0.3": - version: 4.0.3 - resolution: "ol-ext@npm:4.0.3" +"ol-ext@npm:4.0.6": + version: 4.0.6 + resolution: "ol-ext@npm:4.0.6" peerDependencies: ol: ">= 5.3.0" - checksum: e190899bb45a0e16b6692b6d7e2810f76b320b8493a8e5d724288c75938b9834ec0b718f2718c92f218f4f30e176e64cd4cd4499366f92c7f42fd34031653ba2 + checksum: 09b0ce83cba9ca4fd59fe01e1ace4154850dd0f286b5795760458f79941ef4068b7bad7a59454a4ca0fa4867d0986bfcb4433f286ec8d06a3612f5bbdc05b9f3 languageName: node linkType: hard @@ -30881,17 +30956,17 @@ __metadata: languageName: node linkType: hard -"postcss-loader@npm:7.0.1": - version: 7.0.1 - resolution: "postcss-loader@npm:7.0.1" +"postcss-loader@npm:7.0.2": + version: 7.0.2 + resolution: "postcss-loader@npm:7.0.2" dependencies: cosmiconfig: ^7.0.0 klona: ^2.0.5 - semver: ^7.3.7 + semver: ^7.3.8 peerDependencies: postcss: ^7.0.0 || ^8.0.1 webpack: ^5.0.0 - checksum: 2a3cbcaaade598d4919824d384ae34ffbfc14a9c8db6cc3b154582356f4f44a1c9af9e731b81cf1947b089accf7d0ab7a0c51c717946985f89aa1708d2b4304d + checksum: 2d251537d482eb751f812c96c8b515f46d7c9905cad7afab33f0f34872670619b7440cefc9e2babbf89fb11b4708850d522d79fa5ff788227587645e78f16638 languageName: node linkType: hard @@ -31350,12 +31425,12 @@ __metadata: languageName: node linkType: hard -"postcss-scss@npm:4.0.5": - version: 4.0.5 - resolution: "postcss-scss@npm:4.0.5" +"postcss-scss@npm:4.0.6": + version: 4.0.6 + resolution: "postcss-scss@npm:4.0.6" peerDependencies: - postcss: ^8.3.3 - checksum: 5cba2044db3a57ecec9ac64db28be42e145bae0e1c2e4322ef38674e4302b0854f5e16f91658b2bcff3d0f1dbbfb186a9871f988b16895c51993f431a74ed4db + postcss: ^8.4.19 + checksum: 133a1cba31e2e167f4e841e66ec6a798eaf44c7911f9182ade0b5b1e71a8198814aa390b8c9d5db6b01358115232e5b15b1a4f8c5198acfccfb1f3fdbd328cdf languageName: node linkType: hard @@ -31465,14 +31540,14 @@ __metadata: languageName: node linkType: hard -"postcss@npm:8.4.18, postcss@npm:^8.2.15, postcss@npm:^8.4.17": - version: 8.4.18 - resolution: "postcss@npm:8.4.18" +"postcss@npm:8.4.21, postcss@npm:^8.4.19, postcss@npm:^8.4.21": + version: 8.4.21 + resolution: "postcss@npm:8.4.21" dependencies: nanoid: ^3.3.4 picocolors: ^1.0.0 source-map-js: ^1.0.2 - checksum: 9349fd99849b2e3d2e134ff949b7770ecb12375f352723ce2bcc06167eba3850ea7844c1b191a85cd915d6a396b4e8ee9a5267e6cc5d8d003d0cbc7a97555d39 + checksum: e39ac60ccd1542d4f9d93d894048aac0d686b3bb38e927d8386005718e6793dbbb46930f0a523fe382f1bbd843c6d980aaea791252bf5e176180e5a4336d9679 languageName: node linkType: hard @@ -31486,6 +31561,17 @@ __metadata: languageName: node linkType: hard +"postcss@npm:^8.2.15, postcss@npm:^8.4.17": + version: 8.4.18 + resolution: "postcss@npm:8.4.18" + dependencies: + nanoid: ^3.3.4 + picocolors: ^1.0.0 + source-map-js: ^1.0.2 + checksum: 9349fd99849b2e3d2e134ff949b7770ecb12375f352723ce2bcc06167eba3850ea7844c1b191a85cd915d6a396b4e8ee9a5267e6cc5d8d003d0cbc7a97555d39 + languageName: node + linkType: hard + "postcss@npm:^8.3.11, postcss@npm:^8.3.5": version: 8.3.11 resolution: "postcss@npm:8.3.11" @@ -31508,17 +31594,6 @@ __metadata: languageName: node linkType: hard -"postcss@npm:^8.4.21": - version: 8.4.21 - resolution: "postcss@npm:8.4.21" - dependencies: - nanoid: ^3.3.4 - picocolors: ^1.0.0 - source-map-js: ^1.0.2 - checksum: e39ac60ccd1542d4f9d93d894048aac0d686b3bb38e927d8386005718e6793dbbb46930f0a523fe382f1bbd843c6d980aaea791252bf5e176180e5a4336d9679 - languageName: node - linkType: hard - "postcss@npm:^8.4.7": version: 8.4.7 resolution: "postcss@npm:8.4.7" @@ -31691,12 +31766,12 @@ __metadata: languageName: node linkType: hard -"prettier@npm:2.8.1": - version: 2.8.1 - resolution: "prettier@npm:2.8.1" +"prettier@npm:2.8.4": + version: 2.8.4 + resolution: "prettier@npm:2.8.4" bin: prettier: bin-prettier.js - checksum: 4f21a0f1269f76fb36f54e9a8a1ea4c11e27478958bf860661fb4b6d7ac69aac1581f8724fa98ea3585e56d42a2ea317a17ff6e3324f40cb11ff9e20b73785cc + checksum: c173064bf3df57b6d93d19aa98753b9b9dd7657212e33b41ada8e2e9f9884066bb9ca0b4005b89b3ab137efffdf8fbe0b462785aba20364798ff4303aadda57e languageName: node linkType: hard @@ -32326,9 +32401,9 @@ __metadata: languageName: node linkType: hard -"rc-drawer@npm:6.1.2": - version: 6.1.2 - resolution: "rc-drawer@npm:6.1.2" +"rc-drawer@npm:6.1.3": + version: 6.1.3 + resolution: "rc-drawer@npm:6.1.3" dependencies: "@babel/runtime": ^7.10.1 "@rc-component/portal": ^1.0.0-6 @@ -32338,7 +32413,7 @@ __metadata: peerDependencies: react: ">=16.9.0" react-dom: ">=16.9.0" - checksum: 0d7f5cd56bcad80ebc11dd3d1c5b13ef620e8790fac67af62180938c9baa0e671cbd6aaf9588ebd1978de39a2791dcf4f0da56dd366797bada43d2d31ef575ad + checksum: 09fa3085312f668b27e0a8acae7f560a7d45ad52e4554020a6d3801352331b1173b20f57d32f876cfc1b359bd3088190e90bd7815619144d6d50b83c4ab44196 languageName: node linkType: hard @@ -32418,18 +32493,17 @@ __metadata: languageName: node linkType: hard -"rc-slider@npm:10.1.0": - version: 10.1.0 - resolution: "rc-slider@npm:10.1.0" +"rc-slider@npm:10.1.1": + version: 10.1.1 + resolution: "rc-slider@npm:10.1.1" dependencies: "@babel/runtime": ^7.10.1 classnames: ^2.2.5 - rc-util: ^5.18.1 - shallowequal: ^1.1.0 + rc-util: ^5.27.0 peerDependencies: react: ">=16.9.0" react-dom: ">=16.9.0" - checksum: 002662cd0a59d6e48dd82744dfe2043efffd15229fb39665001bd73d584570458c16c0babcc78dc7b0ccde57d83f86114492348b3e5674a0672c97269957319d + checksum: 8df66142f1be00d31aaa45f3cf266fa30d03b70c74c734502389bbfacdb6741e149cd36dc1d3557d9dbb0194ed2733748366d888651d1120098338086419ba2c languageName: node linkType: hard @@ -32461,7 +32535,23 @@ __metadata: languageName: node linkType: hard -"rc-tree@npm:5.7.0, rc-tree@npm:~5.7.0": +"rc-tree@npm:5.7.2": + version: 5.7.2 + resolution: "rc-tree@npm:5.7.2" + dependencies: + "@babel/runtime": ^7.10.1 + classnames: 2.x + rc-motion: ^2.0.1 + rc-util: ^5.16.1 + rc-virtual-list: ^3.4.8 + peerDependencies: + react: "*" + react-dom: "*" + checksum: 9b465e1937fdd59987d2e69587b10c3d1415072ed6cd8e953d8975c4d31ddfa3f963d6d824b6d5017bd3a4331d9a0af029886a484af70a861ddda02dcfcb964c + languageName: node + linkType: hard + +"rc-tree@npm:~5.7.0": version: 5.7.0 resolution: "rc-tree@npm:5.7.0" dependencies: @@ -32551,7 +32641,7 @@ __metadata: languageName: node linkType: hard -"rc-util@npm:^5.18.1, rc-util@npm:^5.19.2, rc-util@npm:^5.21.0, rc-util@npm:^5.21.2, rc-util@npm:^5.24.4": +"rc-util@npm:^5.19.2, rc-util@npm:^5.21.0, rc-util@npm:^5.21.2, rc-util@npm:^5.24.4": version: 5.24.4 resolution: "rc-util@npm:5.24.4" dependencies: @@ -32579,6 +32669,19 @@ __metadata: languageName: node linkType: hard +"rc-util@npm:^5.27.0": + version: 5.28.0 + resolution: "rc-util@npm:5.28.0" + dependencies: + "@babel/runtime": ^7.18.3 + react-is: ^16.12.0 + peerDependencies: + react: ">=16.9.0" + react-dom: ">=16.9.0" + checksum: e60424c37dad7575bb2429e266a81f96003701d719d1fb40128b42ed1c6972896cec09ece8857b36ce9ac74ba95aa2d0a9bdc0609894ba1b3c12c15504a1a886 + languageName: node + linkType: hard + "rc-virtual-list@npm:^3.4.13": version: 3.4.13 resolution: "rc-virtual-list@npm:3.4.13" @@ -33032,15 +33135,15 @@ __metadata: languageName: node linkType: hard -"react-inlinesvg@npm:3.0.1": - version: 3.0.1 - resolution: "react-inlinesvg@npm:3.0.1" +"react-inlinesvg@npm:3.0.2": + version: 3.0.2 + resolution: "react-inlinesvg@npm:3.0.2" dependencies: exenv: ^1.2.2 react-from-dom: ^0.6.2 peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 - checksum: 756f704bc70dc44b38f446e1d4a92b51dbce42f6b0121b17cdd2255d4403c2428d01b7aac77d7e2120ced476edef839a91ec1454a7a90a1e672974bd542c60b9 + checksum: 05e31af59d19742b63ef91741e3b8974716eae69935420baf3d03c9cdfd234f9b290bb151248d135d70ef54e13fefd66ce45c2d749ca2e319625c0b1c0a0890a languageName: node linkType: hard @@ -33743,16 +33846,7 @@ __metadata: languageName: node linkType: hard -"redux-thunk@npm:2.4.1": - version: 2.4.1 - resolution: "redux-thunk@npm:2.4.1" - peerDependencies: - redux: ^4 - checksum: af5abb425fb9dccda02e5f387d6f3003997f62d906542a3d35fc9420088f550dc1a018bdc246c7d23ee852b4d4ab8b5c64c5be426e45a328d791c4586a3c6b6e - languageName: node - linkType: hard - -"redux-thunk@npm:^2.4.2": +"redux-thunk@npm:2.4.2, redux-thunk@npm:^2.4.2": version: 2.4.2 resolution: "redux-thunk@npm:2.4.2" peerDependencies: @@ -33761,12 +33855,12 @@ __metadata: languageName: node linkType: hard -"redux@npm:4.2.0, redux@npm:^4.2.0": - version: 4.2.0 - resolution: "redux@npm:4.2.0" +"redux@npm:4.2.1": + version: 4.2.1 + resolution: "redux@npm:4.2.1" dependencies: "@babel/runtime": ^7.9.2 - checksum: 75f3955c89b3f18edf5411e5fb482aa2e4f41a416183e8802a6bf6472c4fc3d47675b8b321d147f8af8e0f616436ac507bf5a25f1c4d6180e797b549c7db2c1d + checksum: f63b9060c3a1d930ae775252bb6e579b42415aee7a23c4114e21a0b4ba7ec12f0ec76936c00f546893f06e139819f0e2855e0d55ebfce34ca9c026241a6950dd languageName: node linkType: hard @@ -33779,6 +33873,15 @@ __metadata: languageName: node linkType: hard +"redux@npm:^4.2.0": + version: 4.2.0 + resolution: "redux@npm:4.2.0" + dependencies: + "@babel/runtime": ^7.9.2 + checksum: 75f3955c89b3f18edf5411e5fb482aa2e4f41a416183e8802a6bf6472c4fc3d47675b8b321d147f8af8e0f616436ac507bf5a25f1c4d6180e797b549c7db2c1d + languageName: node + linkType: hard + "refractor@npm:^3.6.0": version: 3.6.0 resolution: "refractor@npm:3.6.0" @@ -33815,10 +33918,10 @@ __metadata: languageName: node linkType: hard -"regenerator-runtime@npm:0.13.10, regenerator-runtime@npm:^0.13.10": - version: 0.13.10 - resolution: "regenerator-runtime@npm:0.13.10" - checksum: 09893f5a9e82932642d9a999716b6c626dc53ef2a01307c952ebbf8e011802360163a37c304c18a6c358548be5a72b448e37209954a18696f21e438c81cbb4b9 +"regenerator-runtime@npm:0.13.11, regenerator-runtime@npm:^0.13.11": + version: 0.13.11 + resolution: "regenerator-runtime@npm:0.13.11" + checksum: 27481628d22a1c4e3ff551096a683b424242a216fee44685467307f14d58020af1e19660bf2e26064de946bad7eff28950eae9f8209d55723e2d9351e632bbb4 languageName: node linkType: hard @@ -33829,10 +33932,10 @@ __metadata: languageName: node linkType: hard -"regenerator-runtime@npm:^0.13.11": - version: 0.13.11 - resolution: "regenerator-runtime@npm:0.13.11" - checksum: 27481628d22a1c4e3ff551096a683b424242a216fee44685467307f14d58020af1e19660bf2e26064de946bad7eff28950eae9f8209d55723e2d9351e632bbb4 +"regenerator-runtime@npm:^0.13.10": + version: 0.13.10 + resolution: "regenerator-runtime@npm:0.13.10" + checksum: 09893f5a9e82932642d9a999716b6c626dc53ef2a01307c952ebbf8e011802360163a37c304c18a6c358548be5a72b448e37209954a18696f21e438c81cbb4b9 languageName: node linkType: hard @@ -34217,14 +34320,7 @@ __metadata: languageName: node linkType: hard -"reselect@npm:4.1.6": - version: 4.1.6 - resolution: "reselect@npm:4.1.6" - checksum: 3ea1058422904063ec93c8f4693fe33dcb2178bbf417ace8db5b2c797a5875cf357d9308d11ed3942ee22507dd34ecfbf1f3a21340a4f31c206cab1d36ceef31 - languageName: node - linkType: hard - -"reselect@npm:^4.1.7": +"reselect@npm:4.1.7, reselect@npm:^4.1.7": version: 4.1.7 resolution: "reselect@npm:4.1.7" checksum: 738d8e2b8f0dca154ad29de6a209c9fbca2d70ae6788fd85df87f2c74b95a65bbf2d16d43a9e2faff39de34d17a29d706ba08a6b2ee5660c09589edbd19af7e1 @@ -34853,16 +34949,16 @@ __metadata: languageName: node linkType: hard -"sass@npm:1.58.2": - version: 1.58.2 - resolution: "sass@npm:1.58.2" +"sass@npm:1.58.3": + version: 1.58.3 + resolution: "sass@npm:1.58.3" dependencies: chokidar: ">=3.0.0 <4.0.0" immutable: ^4.0.0 source-map-js: ">=0.6.2 <2.0.0" bin: sass: sass.js - checksum: e0febe4d274af7b9490b9207ff7f05762d60df6b2ad307f7a823432cb4e1604eced6784ae635a6b80e4a6177c047f5a9623c53a15aaec3b9bf981ea86e8937a9 + checksum: 35a2b98c037ef80fdc93c9b0be846e6ccc7d75596351a37ee79c397e66666d0a754c52c4696e746c0aff32327471e185343ca349e998a58340411adc9d0489a5 languageName: node linkType: hard @@ -36617,15 +36713,15 @@ __metadata: languageName: node linkType: hard -"stylelint-config-prettier@npm:9.0.3": - version: 9.0.3 - resolution: "stylelint-config-prettier@npm:9.0.3" +"stylelint-config-prettier@npm:9.0.5": + version: 9.0.5 + resolution: "stylelint-config-prettier@npm:9.0.5" peerDependencies: - stylelint: ">=11.0.0" + stylelint: ">= 11.x < 15" bin: stylelint-config-prettier: bin/check.js stylelint-config-prettier-check: bin/check.js - checksum: 9ff3f719daf3865878615ba52c31de6ef0a0d25d41cb58c41afe2f1c459a838997ff912cc4a5b4d401f92e2193667ff4d140b6d303cf8192e894b5cb454c41b9 + checksum: 3d04e463e0bb7e42a5ddec49eea6ef4ea07705d887e8a3ff1fcb82278a5e2bec1a36b8498ea7ed2d24878de29d7c94ac75b1d3ac4f8b19c3a84970595b29261f languageName: node linkType: hard @@ -37599,14 +37695,14 @@ __metadata: languageName: node linkType: hard -"ts-jest@npm:29.0.3": - version: 29.0.3 - resolution: "ts-jest@npm:29.0.3" +"ts-jest@npm:29.0.5": + version: 29.0.5 + resolution: "ts-jest@npm:29.0.5" dependencies: bs-logger: 0.x fast-json-stable-stringify: 2.x jest-util: ^29.0.0 - json5: ^2.2.1 + json5: ^2.2.3 lodash.memoize: 4.x make-error: 1.x semver: 7.x @@ -37628,7 +37724,7 @@ __metadata: optional: true bin: ts-jest: cli.js - checksum: 541e51776d367fa2279af47f75af94b03e0538f1839ea9983de0f4ad7f188002f6eb1fc72440651d96daa62d25a7bc679a129c14e6ef291277eea9346751d56b + checksum: f60f129c2287f4c963d9ee2677132496c5c5a5d39c27ad234199a1140c26318a7d5bda34890ab0e30636ec42a8de28f84487c09e9dcec639c9c67812b3a38373 languageName: node linkType: hard @@ -37796,7 +37892,7 @@ __metadata: languageName: node linkType: hard -"tslib@npm:2.5.0": +"tslib@npm:2.5.0, tslib@npm:^2.4.1": version: 2.5.0 resolution: "tslib@npm:2.5.0" checksum: ae3ed5f9ce29932d049908ebfdf21b3a003a85653a9a140d614da6b767a93ef94f460e52c3d787f0e4f383546981713f165037dc2274df212ea9f8a4541004e1 @@ -39109,10 +39205,10 @@ __metadata: languageName: node linkType: hard -"web-vitals@npm:^3.0.4": - version: 3.0.4 - resolution: "web-vitals@npm:3.0.4" - checksum: b618a8e049e0c64948eea09c372db490802bcc8bcb30230a2bc69d9e243b1a2fa54d0f0ae19d5e63f19381df86db2a9260ca15ef620e10024888eab487a77d56 +"web-vitals@npm:^3.1.1": + version: 3.1.1 + resolution: "web-vitals@npm:3.1.1" + checksum: 26d552d7e0bba470e1426bbf55c3c1a701c683bbde4553791a2c7e1bc3a7b7b29bf66ccf67c74872af3aa39e5668728736b90f8b3fee2910c0c566e7eb1b6096 languageName: node linkType: hard @@ -39994,6 +40090,13 @@ __metadata: languageName: node linkType: hard +"yallist@npm:^3.0.2": + version: 3.1.1 + resolution: "yallist@npm:3.1.1" + checksum: 48f7bb00dc19fc635a13a39fe547f527b10c9290e7b3e836b9a8f1ca04d4d342e85714416b3c2ab74949c9c66f9cebb0473e6bc353b79035356103b47641285d + languageName: node + linkType: hard + "yallist@npm:^4.0.0": version: 4.0.0 resolution: "yallist@npm:4.0.0" From b093439b2e56b7f262916d5f35e35e48249075f4 Mon Sep 17 00:00:00 2001 From: Tania Date: Wed, 8 Mar 2023 11:14:37 +0100 Subject: [PATCH 049/288] Chore: Add codeowners for encryption feature toggle (#64384) Chore: Add codeowners for encryption feature toggle --- pkg/services/featuremgmt/codeowners.go | 1 + pkg/services/featuremgmt/registry.go | 1 + pkg/services/featuremgmt/toggles_gen_test.go | 1 - 3 files changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/services/featuremgmt/codeowners.go b/pkg/services/featuremgmt/codeowners.go index 7efd1387316..2f746836f2c 100644 --- a/pkg/services/featuremgmt/codeowners.go +++ b/pkg/services/featuremgmt/codeowners.go @@ -12,5 +12,6 @@ const ( grafanaUserEssentialsSquad codeowner = "@grafana/user-essentials" grafanaBackendPlatformSquad codeowner = "@grafana/backend-platform" grafanaPluginsPlatformSquad codeowner = "@grafana/plugins-platform-backend" + grafanaAsCodeSquad codeowner = "@grafana/grafana-as-code" grafanaAuthnzSquad codeowner = "@grafana/grafana-authnz-team" ) diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index bfd6486d37b..f5b5a9178ad 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -23,6 +23,7 @@ var ( Name: "disableEnvelopeEncryption", Description: "Disable envelope encryption (emergency only)", State: FeatureStateStable, + Owner: grafanaAsCodeSquad, }, { Name: "database_metrics", diff --git a/pkg/services/featuremgmt/toggles_gen_test.go b/pkg/services/featuremgmt/toggles_gen_test.go index efce45e3a90..9da66f771b5 100644 --- a/pkg/services/featuremgmt/toggles_gen_test.go +++ b/pkg/services/featuremgmt/toggles_gen_test.go @@ -44,7 +44,6 @@ func TestFeatureToggleFiles(t *testing.T) { ownerlessFeatures := map[string]bool{ "alertingBigTransactions": true, "trimDefaults": true, - "disableEnvelopeEncryption": true, "database_metrics": true, "prometheusAzureOverrideAudience": true, "lokiDataframeApi": true, From c7a1216cf6dbb660d4ad3bcf467228941b69abe9 Mon Sep 17 00:00:00 2001 From: Andrej Ocenas Date: Wed, 8 Mar 2023 11:20:08 +0100 Subject: [PATCH 050/288] TraceView: Add key and url escaping of json tag values (#64331) --- .../SpanDetail/KeyValuesTable.test.tsx | 26 +++- .../SpanDetail/KeyValuesTable.tsx | 3 +- .../SpanDetail/jsonMarkup.js | 133 ++++++++++++++++++ 3 files changed, 160 insertions(+), 2 deletions(-) create mode 100644 public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/jsonMarkup.js diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/KeyValuesTable.test.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/KeyValuesTable.test.tsx index 130f2c637e0..9818ace12cb 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/KeyValuesTable.test.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/KeyValuesTable.test.tsx @@ -24,7 +24,7 @@ const data = [ { key: 'jsonkey', value: JSON.stringify({ hello: 'world' }) }, ]; -const setup = (propOverrides?: KeyValuesTableProps) => { +const setup = (propOverrides?: Partial) => { const props = { data: data, ...propOverrides, @@ -89,4 +89,28 @@ describe('KeyValuesTable tests', () => { expect(screen.getAllByRole('button')).toHaveLength(4); }); + + it('renders a link in json and properly escapes it', () => { + setup({ + data: [ + { key: 'jsonkey', value: JSON.stringify({ hello: 'https://example.com"id=x tabindex=1 onfocus=alert(1)' }) }, + ], + }); + const link = screen.getByText(/https:\/\/example.com/); + expect(link.tagName).toBe('A'); + expect(link.attributes.getNamedItem('href')?.value).toBe( + 'https://example.com%22id=x%20tabindex=1%20onfocus=alert(1)' + ); + }); + + it('properly escapes json values', () => { + setup({ + data: [ + { key: 'jsonkey', value: JSON.stringify({ '': '' }) }, + ], + }); + const values = screen.getAllByText(/onerror=alert/); + expect(values[0].innerHTML).toBe('"<img src=x onerror=alert(1)>":'); + expect(values[1].innerHTML).toBe('"<img src=x onerror=alert(1)>"'); + }); }); diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/KeyValuesTable.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/KeyValuesTable.tsx index df3675a3d2c..0860b1a62e6 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/KeyValuesTable.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/KeyValuesTable.tsx @@ -14,7 +14,6 @@ import { css } from '@emotion/css'; import cx from 'classnames'; -import jsonMarkup from 'json-markup'; import * as React from 'react'; import { GrafanaTheme2 } from '@grafana/data'; @@ -25,6 +24,8 @@ import CopyIcon from '../../common/CopyIcon'; import { TraceKeyValuePair, TraceLink, TNil } from '../../types'; import { ubInlineBlock, uWidth100 } from '../../uberUtilityStyles'; +import jsonMarkup from './jsonMarkup'; + const copyIconClassName = 'copyIcon'; export const getStyles = (theme: GrafanaTheme2) => { diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/jsonMarkup.js b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/jsonMarkup.js new file mode 100644 index 00000000000..2dcbb1ee895 --- /dev/null +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/jsonMarkup.js @@ -0,0 +1,133 @@ +// The MIT License (MIT) +// +// Copyright (c) 2014 Mathias Buus +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +const INDENT = ' '; + +function inlineRule(objRule) { + let str = ''; + objRule && + Object.keys(objRule).forEach(function (rule) { + str += rule + ':' + objRule[rule] + ';'; + }); + return str; +} + +function Stylize(styleFile) { + function styleClass(cssClass) { + return 'class="' + cssClass + '"'; + } + + function styleInline(cssClass) { + return 'style="' + inlineRule(styleFile['.' + cssClass]) + '"'; + } + + if (!styleFile) { + return styleClass; + } + return styleInline; +} + +function type(doc) { + if (doc === null) { + return 'null'; + } + if (Array.isArray(doc)) { + return 'array'; + } + if (typeof doc === 'string' && /^https?:/.test(doc)) { + return 'link'; + } + if (typeof doc === 'object' && typeof doc.toISOString === 'function') { + return 'date'; + } + + return typeof doc; +} + +function escape(str) { + return str.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); +} + +module.exports = function (doc, styleFile) { + let indent = ''; + const style = Stylize(styleFile); + + let forEach = function (list, start, end, fn) { + if (!list.length) { + return start + ' ' + end; + } + + let out = start + '\n'; + + indent += INDENT; + list.forEach(function (key, i) { + out += indent + fn(key) + (i < list.length - 1 ? ',' : '') + '\n'; + }); + indent = indent.slice(0, -INDENT.length); + + return out + indent + end; + }; + + function visit(obj) { + if (obj === undefined) { + return ''; + } + + switch (type(obj)) { + case 'boolean': + return '' + obj + ''; + + case 'number': + return '' + obj + ''; + + case 'date': + return '"' + escape(obj.toISOString()) + '"'; + + case 'null': + return 'null'; + + case 'string': + return '"' + escape(obj.replace(/\n/g, '\n' + indent)) + '"'; + + case 'link': + return ( + '"' + escape(obj) + '"' + ); + + case 'array': + return forEach(obj, '[', ']', visit); + + case 'object': + const keys = Object.keys(obj).filter(function (key) { + return obj[key] !== undefined; + }); + + return forEach(keys, '{', '}', function (key) { + return '"' + escape(key) + '": ' + visit(obj[key]); + }); + } + + return ''; + } + + return '
' + visit(doc) + '
'; +}; From 42c32504be6940cdfb810bcf2853f36be6cf6a3d Mon Sep 17 00:00:00 2001 From: Sven Grossmann Date: Wed, 8 Mar 2023 11:39:56 +0100 Subject: [PATCH 051/288] Logs: Add millisecond to timestamp in log line (#64372) * add milliseconds to logrow * adjust tests to also have milliseconds --- public/app/features/explore/LogsSample.test.tsx | 4 ++-- public/app/features/logs/components/LogRow.tsx | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/public/app/features/explore/LogsSample.test.tsx b/public/app/features/explore/LogsSample.test.tsx index fe2e0366ac0..8c76cedcf57 100644 --- a/public/app/features/explore/LogsSample.test.tsx +++ b/public/app/features/explore/LogsSample.test.tsx @@ -81,9 +81,9 @@ describe('LogsSamplePanel', () => { render( ); - expect(screen.getByText('2022-02-22 04:28:11')).toBeInTheDocument(); + expect(screen.getByText('2022-02-22 04:28:11.352')).toBeInTheDocument(); expect(screen.getByText('line1')).toBeInTheDocument(); - expect(screen.getByText('2022-02-22 09:42:50')).toBeInTheDocument(); + expect(screen.getByText('2022-02-22 09:42:50.991')).toBeInTheDocument(); expect(screen.getByText('line2')).toBeInTheDocument(); }); diff --git a/public/app/features/logs/components/LogRow.tsx b/public/app/features/logs/components/LogRow.tsx index 3e87d876f4a..ebeac4c7930 100644 --- a/public/app/features/logs/components/LogRow.tsx +++ b/public/app/features/logs/components/LogRow.tsx @@ -117,6 +117,7 @@ class UnThemedLogRow extends PureComponent { renderTimeStamp(epochMs: number) { return dateTimeFormat(epochMs, { timeZone: this.props.timeZone, + defaultWithMS: true, }); } From 4c304039049712e3e41cb484b4e11d220374ea7b Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Wed, 8 Mar 2023 13:12:44 +0200 Subject: [PATCH 052/288] Elastic Search: Fix BasePipelineMetricAggregation schema type (#64335) * Fix type * Tweak BaseBucketAggregation type * Remove comment --- .../schema-reference.md | 104 ++++++++--------- .../kinds/dataquery/types_dataquery_gen.go | 105 +++++++----------- .../datasource/elasticsearch/dataquery.cue | 7 +- .../datasource/elasticsearch/dataquery.gen.ts | 28 ++++- .../plugins/datasource/elasticsearch/types.ts | 69 +----------- 5 files changed, 126 insertions(+), 187 deletions(-) diff --git a/docs/sources/developers/kinds/composable/elasticsearchdataquery/schema-reference.md b/docs/sources/developers/kinds/composable/elasticsearchdataquery/schema-reference.md index 61a6f2af201..5bebae059db 100644 --- a/docs/sources/developers/kinds/composable/elasticsearchdataquery/schema-reference.md +++ b/docs/sources/developers/kinds/composable/elasticsearchdataquery/schema-reference.md @@ -38,90 +38,85 @@ It extends [DataQuery](#dataquery). It extends [BucketAggregationWithField](#bucketaggregationwithfield). -| Property | Type | Required | Description | -|------------|---------------------|----------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `type` | string | **Yes** | *(Inherited from [BucketAggregationWithField](#bucketaggregationwithfield))*
Possible values are: `terms`, `filters`, `geohash_grid`, `date_histogram`, `histogram`, `nested`. | -| `field` | string | No | *(Inherited from [BucketAggregationWithField](#bucketaggregationwithfield))* | -| `id` | string | No | *(Inherited from [BucketAggregationWithField](#bucketaggregationwithfield))* | -| `settings` | [object](#settings) | No | *(Inherited from [BucketAggregationWithField](#bucketaggregationwithfield))* | +| Property | Type | Required | Description | +|------------|--------|----------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `type` | string | **Yes** | *(Inherited from [BucketAggregationWithField](#bucketaggregationwithfield))*
Possible values are: `terms`, `filters`, `geohash_grid`, `date_histogram`, `histogram`, `nested`. | +| `field` | string | No | *(Inherited from [BucketAggregationWithField](#bucketaggregationwithfield))* | +| `id` | string | No | *(Inherited from [BucketAggregationWithField](#bucketaggregationwithfield))* | +| `settings` | | No | *(Inherited from [BucketAggregationWithField](#bucketaggregationwithfield))* | ### BucketAggregationWithField It extends [BaseBucketAggregation](#basebucketaggregation). -| Property | Type | Required | Description | -|------------|---------------------|----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `id` | string | **Yes** | *(Inherited from [BaseBucketAggregation](#basebucketaggregation))* | -| `type` | string | **Yes** | *(Inherited from [BaseBucketAggregation](#basebucketaggregation))*
Possible values are: `terms`, `filters`, `geohash_grid`, `date_histogram`, `histogram`, `nested`. | -| `field` | string | No | | -| `settings` | [object](#settings) | No | *(Inherited from [BaseBucketAggregation](#basebucketaggregation))* | +| Property | Type | Required | Description | +|------------|--------|----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `id` | string | **Yes** | *(Inherited from [BaseBucketAggregation](#basebucketaggregation))* | +| `type` | string | **Yes** | *(Inherited from [BaseBucketAggregation](#basebucketaggregation))*
Possible values are: `terms`, `filters`, `geohash_grid`, `date_histogram`, `histogram`, `nested`. | +| `field` | string | No | | +| `settings` | | No | *(Inherited from [BaseBucketAggregation](#basebucketaggregation))* | ### BaseBucketAggregation -| Property | Type | Required | Description | -|------------|---------------------|----------|---------------------------------------------------------------------------------------------------| -| `id` | string | **Yes** | | -| `type` | string | **Yes** | Possible values are: `terms`, `filters`, `geohash_grid`, `date_histogram`, `histogram`, `nested`. | -| `settings` | [object](#settings) | No | | - -### Settings - -| Property | Type | Required | Description | -|----------|------|----------|-------------| +| Property | Type | Required | Description | +|------------|--------|----------|---------------------------------------------------------------------------------------------------| +| `id` | string | **Yes** | | +| `type` | string | **Yes** | Possible values are: `terms`, `filters`, `geohash_grid`, `date_histogram`, `histogram`, `nested`. | +| `settings` | | No | | ### Filters It extends [BaseBucketAggregation](#basebucketaggregation). -| Property | Type | Required | Description | -|------------|---------------------|----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `id` | string | **Yes** | *(Inherited from [BaseBucketAggregation](#basebucketaggregation))* | -| `type` | string | **Yes** | *(Inherited from [BaseBucketAggregation](#basebucketaggregation))*
Possible values are: `terms`, `filters`, `geohash_grid`, `date_histogram`, `histogram`, `nested`. | -| `settings` | [object](#settings) | No | *(Inherited from [BaseBucketAggregation](#basebucketaggregation))* | +| Property | Type | Required | Description | +|------------|--------|----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `id` | string | **Yes** | *(Inherited from [BaseBucketAggregation](#basebucketaggregation))* | +| `type` | string | **Yes** | *(Inherited from [BaseBucketAggregation](#basebucketaggregation))*
Possible values are: `terms`, `filters`, `geohash_grid`, `date_histogram`, `histogram`, `nested`. | +| `settings` | | No | *(Inherited from [BaseBucketAggregation](#basebucketaggregation))* | ### GeoHashGrid It extends [BucketAggregationWithField](#bucketaggregationwithfield). -| Property | Type | Required | Description | -|------------|---------------------|----------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `type` | string | **Yes** | *(Inherited from [BucketAggregationWithField](#bucketaggregationwithfield))*
Possible values are: `terms`, `filters`, `geohash_grid`, `date_histogram`, `histogram`, `nested`. | -| `field` | string | No | *(Inherited from [BucketAggregationWithField](#bucketaggregationwithfield))* | -| `id` | string | No | *(Inherited from [BucketAggregationWithField](#bucketaggregationwithfield))* | -| `settings` | [object](#settings) | No | *(Inherited from [BucketAggregationWithField](#bucketaggregationwithfield))* | +| Property | Type | Required | Description | +|------------|--------|----------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `type` | string | **Yes** | *(Inherited from [BucketAggregationWithField](#bucketaggregationwithfield))*
Possible values are: `terms`, `filters`, `geohash_grid`, `date_histogram`, `histogram`, `nested`. | +| `field` | string | No | *(Inherited from [BucketAggregationWithField](#bucketaggregationwithfield))* | +| `id` | string | No | *(Inherited from [BucketAggregationWithField](#bucketaggregationwithfield))* | +| `settings` | | No | *(Inherited from [BucketAggregationWithField](#bucketaggregationwithfield))* | ### Histogram It extends [BucketAggregationWithField](#bucketaggregationwithfield). -| Property | Type | Required | Description | -|------------|---------------------|----------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `type` | string | **Yes** | *(Inherited from [BucketAggregationWithField](#bucketaggregationwithfield))*
Possible values are: `terms`, `filters`, `geohash_grid`, `date_histogram`, `histogram`, `nested`. | -| `field` | string | No | *(Inherited from [BucketAggregationWithField](#bucketaggregationwithfield))* | -| `id` | string | No | *(Inherited from [BucketAggregationWithField](#bucketaggregationwithfield))* | -| `settings` | [object](#settings) | No | *(Inherited from [BucketAggregationWithField](#bucketaggregationwithfield))* | +| Property | Type | Required | Description | +|------------|--------|----------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `type` | string | **Yes** | *(Inherited from [BucketAggregationWithField](#bucketaggregationwithfield))*
Possible values are: `terms`, `filters`, `geohash_grid`, `date_histogram`, `histogram`, `nested`. | +| `field` | string | No | *(Inherited from [BucketAggregationWithField](#bucketaggregationwithfield))* | +| `id` | string | No | *(Inherited from [BucketAggregationWithField](#bucketaggregationwithfield))* | +| `settings` | | No | *(Inherited from [BucketAggregationWithField](#bucketaggregationwithfield))* | ### Nested It extends [BucketAggregationWithField](#bucketaggregationwithfield). -| Property | Type | Required | Description | -|------------|---------------------|----------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `type` | string | **Yes** | *(Inherited from [BucketAggregationWithField](#bucketaggregationwithfield))*
Possible values are: `terms`, `filters`, `geohash_grid`, `date_histogram`, `histogram`, `nested`. | -| `field` | string | No | *(Inherited from [BucketAggregationWithField](#bucketaggregationwithfield))* | -| `id` | string | No | *(Inherited from [BucketAggregationWithField](#bucketaggregationwithfield))* | -| `settings` | [object](#settings) | No | *(Inherited from [BucketAggregationWithField](#bucketaggregationwithfield))* | +| Property | Type | Required | Description | +|------------|--------|----------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `type` | string | **Yes** | *(Inherited from [BucketAggregationWithField](#bucketaggregationwithfield))*
Possible values are: `terms`, `filters`, `geohash_grid`, `date_histogram`, `histogram`, `nested`. | +| `field` | string | No | *(Inherited from [BucketAggregationWithField](#bucketaggregationwithfield))* | +| `id` | string | No | *(Inherited from [BucketAggregationWithField](#bucketaggregationwithfield))* | +| `settings` | | No | *(Inherited from [BucketAggregationWithField](#bucketaggregationwithfield))* | ### Terms It extends [BucketAggregationWithField](#bucketaggregationwithfield). -| Property | Type | Required | Description | -|------------|---------------------|----------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `type` | string | **Yes** | *(Inherited from [BucketAggregationWithField](#bucketaggregationwithfield))*
Possible values are: `terms`, `filters`, `geohash_grid`, `date_histogram`, `histogram`, `nested`. | -| `field` | string | No | *(Inherited from [BucketAggregationWithField](#bucketaggregationwithfield))* | -| `id` | string | No | *(Inherited from [BucketAggregationWithField](#bucketaggregationwithfield))* | -| `settings` | [object](#settings) | No | *(Inherited from [BucketAggregationWithField](#bucketaggregationwithfield))* | +| Property | Type | Required | Description | +|------------|--------|----------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `type` | string | **Yes** | *(Inherited from [BucketAggregationWithField](#bucketaggregationwithfield))*
Possible values are: `terms`, `filters`, `geohash_grid`, `date_histogram`, `histogram`, `nested`. | +| `field` | string | No | *(Inherited from [BucketAggregationWithField](#bucketaggregationwithfield))* | +| `id` | string | No | *(Inherited from [BucketAggregationWithField](#bucketaggregationwithfield))* | +| `settings` | | No | *(Inherited from [BucketAggregationWithField](#bucketaggregationwithfield))* | ### DataQuery @@ -222,11 +217,11 @@ It extends [MetricAggregationWithField](#metricaggregationwithfield). | Property | Type | Required | Description | |---------------|---------|----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `type` | string | **Yes** | *(Inherited from [MetricAggregationWithField](#metricaggregationwithfield))*
Possible values are: `count`, `avg`, `sum`, `min`, `max`, `extended_stats`, `percentiles`, `cardinality`, `raw_document`, `raw_data`, `logs`, `rate`, `top_metrics`, `moving_avg`, `moving_fn`, `derivative`, `serial_diff`, `cumulative_sum`, `bucket_script`. | | `field` | string | No | *(Inherited from [MetricAggregationWithField](#metricaggregationwithfield))* | | `hide` | boolean | No | *(Inherited from [MetricAggregationWithField](#metricaggregationwithfield))* | | `id` | string | No | *(Inherited from [MetricAggregationWithField](#metricaggregationwithfield))* | | `pipelineAgg` | string | No | | -| `type` | string | No | *(Inherited from [MetricAggregationWithField](#metricaggregationwithfield))*
Possible values are: `count`, `avg`, `sum`, `min`, `max`, `extended_stats`, `percentiles`, `cardinality`, `raw_document`, `raw_data`, `logs`, `rate`, `top_metrics`, `moving_avg`, `moving_fn`, `derivative`, `serial_diff`, `cumulative_sum`, `bucket_script`. | ### MetricAggregationWithField @@ -279,6 +274,11 @@ It extends [BasePipelineMetricAggregation](#basepipelinemetricaggregation). | `pipelineAgg` | string | No | *(Inherited from [BasePipelineMetricAggregation](#basepipelinemetricaggregation))* | | `settings` | [object](#settings) | No | | +### Settings + +| Property | Type | Required | Description | +|----------|------|----------|-------------| + ### Meta | Property | Type | Required | Description | diff --git a/pkg/tsdb/elasticsearch/kinds/dataquery/types_dataquery_gen.go b/pkg/tsdb/elasticsearch/kinds/dataquery/types_dataquery_gen.go index 76c9aff0afb..84bf27931d0 100644 --- a/pkg/tsdb/elasticsearch/kinds/dataquery/types_dataquery_gen.go +++ b/pkg/tsdb/elasticsearch/kinds/dataquery/types_dataquery_gen.go @@ -62,25 +62,12 @@ const ( // Defines values for BasePipelineMetricAggregationType. const ( - BasePipelineMetricAggregationTypeAvg BasePipelineMetricAggregationType = "avg" BasePipelineMetricAggregationTypeBucketScript BasePipelineMetricAggregationType = "bucket_script" - BasePipelineMetricAggregationTypeCardinality BasePipelineMetricAggregationType = "cardinality" - BasePipelineMetricAggregationTypeCount BasePipelineMetricAggregationType = "count" BasePipelineMetricAggregationTypeCumulativeSum BasePipelineMetricAggregationType = "cumulative_sum" BasePipelineMetricAggregationTypeDerivative BasePipelineMetricAggregationType = "derivative" - BasePipelineMetricAggregationTypeExtendedStats BasePipelineMetricAggregationType = "extended_stats" - BasePipelineMetricAggregationTypeLogs BasePipelineMetricAggregationType = "logs" - BasePipelineMetricAggregationTypeMax BasePipelineMetricAggregationType = "max" - BasePipelineMetricAggregationTypeMin BasePipelineMetricAggregationType = "min" BasePipelineMetricAggregationTypeMovingAvg BasePipelineMetricAggregationType = "moving_avg" BasePipelineMetricAggregationTypeMovingFn BasePipelineMetricAggregationType = "moving_fn" - BasePipelineMetricAggregationTypePercentiles BasePipelineMetricAggregationType = "percentiles" - BasePipelineMetricAggregationTypeRate BasePipelineMetricAggregationType = "rate" - BasePipelineMetricAggregationTypeRawData BasePipelineMetricAggregationType = "raw_data" - BasePipelineMetricAggregationTypeRawDocument BasePipelineMetricAggregationType = "raw_document" BasePipelineMetricAggregationTypeSerialDiff BasePipelineMetricAggregationType = "serial_diff" - BasePipelineMetricAggregationTypeSum BasePipelineMetricAggregationType = "sum" - BasePipelineMetricAggregationTypeTopMetrics BasePipelineMetricAggregationType = "top_metrics" ) // Defines values for BucketAggregationSettingsOrder. @@ -457,7 +444,7 @@ type AverageType string // BaseBucketAggregation defines model for BaseBucketAggregation. type BaseBucketAggregation struct { Id string `json:"id"` - Settings map[string]interface{} `json:"settings,omitempty"` + Settings *interface{} `json:"settings,omitempty"` Type BaseBucketAggregationType `json:"type"` } @@ -498,35 +485,31 @@ type BasePipelineMetricAggregationType string // BucketAggregation defines model for BucketAggregation. type BucketAggregation struct { - Field *string `json:"field,omitempty"` - Id *string `json:"id,omitempty"` - Settings *BucketAggregationSettings `json:"settings,omitempty"` - Type *interface{} `json:"type,omitempty"` - union json.RawMessage + Field *string `json:"field,omitempty"` + Id *string `json:"id,omitempty"` + Settings *struct { + Filters []struct { + Label string `json:"label"` + Query string `json:"query"` + } `json:"filters,omitempty"` + Interval *string `json:"interval,omitempty"` + MinDocCount *string `json:"min_doc_count,omitempty"` + Missing *string `json:"missing,omitempty"` + Offset *string `json:"offset,omitempty"` + Order *BucketAggregationSettingsOrder `json:"order,omitempty"` + OrderBy *string `json:"orderBy,omitempty"` + Precision *string `json:"precision,omitempty"` + Size *string `json:"size,omitempty"` + TimeZone *string `json:"timeZone,omitempty"` + TrimEdges *string `json:"trimEdges,omitempty"` + } `json:"settings,omitempty"` + Type *interface{} `json:"type,omitempty"` + union json.RawMessage } // BucketAggregationSettingsOrder defines model for BucketAggregation.Settings.Order. type BucketAggregationSettingsOrder string -// BucketAggregationSettings defines model for BucketAggregation.Settings. -type BucketAggregationSettings struct { - Filters []struct { - Label string `json:"label"` - Query string `json:"query"` - } `json:"filters,omitempty"` - Interval *string `json:"interval,omitempty"` - MinDocCount *string `json:"min_doc_count,omitempty"` - Missing *string `json:"missing,omitempty"` - Offset *string `json:"offset,omitempty"` - Order *BucketAggregationSettingsOrder `json:"order,omitempty"` - OrderBy *string `json:"orderBy,omitempty"` - Precision *string `json:"precision,omitempty"` - Size *string `json:"size,omitempty"` - TimeZone *string `json:"timeZone,omitempty"` - TrimEdges *string `json:"trimEdges,omitempty"` - AdditionalProperties map[string]interface{} `json:"-"` -} - // BucketAggregationType defines model for BucketAggregationType. type BucketAggregationType string @@ -534,7 +517,7 @@ type BucketAggregationType string type BucketAggregationWithField struct { Field *string `json:"field,omitempty"` Id string `json:"id"` - Settings map[string]interface{} `json:"settings,omitempty"` + Settings *interface{} `json:"settings,omitempty"` Type BucketAggregationWithFieldType `json:"type"` } @@ -664,32 +647,28 @@ type ElasticsearchDataQuery struct { // BucketAggsSettingsOrder defines model for ElasticsearchDataQuery.BucketAggs.Settings.Order. type BucketAggsSettingsOrder string -// BucketAggsSettings defines model for ElasticsearchDataQuery.BucketAggs.Settings. -type BucketAggsSettings struct { - Filters []struct { - Label string `json:"label"` - Query string `json:"query"` - } `json:"filters,omitempty"` - Interval *string `json:"interval,omitempty"` - MinDocCount *string `json:"min_doc_count,omitempty"` - Missing *string `json:"missing,omitempty"` - Offset *string `json:"offset,omitempty"` - Order *BucketAggsSettingsOrder `json:"order,omitempty"` - OrderBy *string `json:"orderBy,omitempty"` - Precision *string `json:"precision,omitempty"` - Size *string `json:"size,omitempty"` - TimeZone *string `json:"timeZone,omitempty"` - TrimEdges *string `json:"trimEdges,omitempty"` - AdditionalProperties map[string]interface{} `json:"-"` -} - // BucketAggsItem defines model for ElasticsearchDataQuery.bucketAggs.Item. type BucketAggsItem struct { - Field *string `json:"field,omitempty"` - Id *string `json:"id,omitempty"` - Settings *BucketAggsSettings `json:"settings,omitempty"` - Type *interface{} `json:"type,omitempty"` - union json.RawMessage + Field *string `json:"field,omitempty"` + Id *string `json:"id,omitempty"` + Settings *struct { + Filters []struct { + Label string `json:"label"` + Query string `json:"query"` + } `json:"filters,omitempty"` + Interval *string `json:"interval,omitempty"` + MinDocCount *string `json:"min_doc_count,omitempty"` + Missing *string `json:"missing,omitempty"` + Offset *string `json:"offset,omitempty"` + Order *BucketAggsSettingsOrder `json:"order,omitempty"` + OrderBy *string `json:"orderBy,omitempty"` + Precision *string `json:"precision,omitempty"` + Size *string `json:"size,omitempty"` + TimeZone *string `json:"timeZone,omitempty"` + TrimEdges *string `json:"trimEdges,omitempty"` + } `json:"settings,omitempty"` + Type *interface{} `json:"type,omitempty"` + union json.RawMessage } // MetricsSettings defines model for ElasticsearchDataQuery.Metrics.Settings. diff --git a/public/app/plugins/datasource/elasticsearch/dataquery.cue b/public/app/plugins/datasource/elasticsearch/dataquery.cue index b96eaa63607..5baed2d2427 100644 --- a/public/app/plugins/datasource/elasticsearch/dataquery.cue +++ b/public/app/plugins/datasource/elasticsearch/dataquery.cue @@ -49,9 +49,9 @@ composableKinds: DataQuery: { #BucketAggregationType: "terms" | "filters" | "geohash_grid" | "date_histogram" | "histogram" | "nested" @cuetsy(kind="type") #BaseBucketAggregation: { - id: string - type: #BucketAggregationType - settings?: {...} + id: string + type: #BucketAggregationType + settings?: _ } @cuetsy(kind="interface") #BucketAggregationWithField: { @@ -287,6 +287,7 @@ composableKinds: DataQuery: { #BasePipelineMetricAggregation: { #MetricAggregationWithField pipelineAgg?: string + type: #PipelineMetricAggregationType } @cuetsy(kind="interface") #PipelineMetricAggregationWithMultipleBucketPaths: { diff --git a/public/app/plugins/datasource/elasticsearch/dataquery.gen.ts b/public/app/plugins/datasource/elasticsearch/dataquery.gen.ts index 21060f60a7b..edc0d5eaf8a 100644 --- a/public/app/plugins/datasource/elasticsearch/dataquery.gen.ts +++ b/public/app/plugins/datasource/elasticsearch/dataquery.gen.ts @@ -20,7 +20,7 @@ export type BucketAggregationType = ('terms' | 'filters' | 'geohash_grid' | 'dat export interface BaseBucketAggregation { id: string; - settings?: Record; + settings?: unknown; type: BucketAggregationType; } @@ -29,6 +29,13 @@ export interface BucketAggregationWithField extends BaseBucketAggregation { } export interface DateHistogram extends BucketAggregationWithField { + settings?: { + interval?: string; + min_doc_count?: string; + trimEdges?: string; + offset?: string; + timeZone?: string; + }; type: 'date_histogram'; } @@ -41,6 +48,10 @@ export interface DateHistogramSettings { } export interface Histogram extends BucketAggregationWithField { + settings?: { + interval?: string; + min_doc_count?: string; + }; type: 'histogram'; } @@ -52,10 +63,18 @@ export interface HistogramSettings { export type TermsOrder = ('desc' | 'asc'); export interface Nested extends BucketAggregationWithField { + settings?: Record; type: 'nested'; } export interface Terms extends BucketAggregationWithField { + settings?: { + order?: TermsOrder; + size?: string; + min_doc_count?: string; + orderBy?: string; + missing?: string; + }; type: 'terms'; } @@ -68,6 +87,9 @@ export interface TermsSettings { } export interface Filters extends BaseBucketAggregation { + settings?: { + filters?: Array; + }; type: 'filters'; } @@ -85,6 +107,9 @@ export const defaultFiltersSettings: Partial = { }; export interface GeoHashGrid extends BucketAggregationWithField { + settings?: { + precision?: string; + }; type: 'geohash_grid'; } @@ -234,6 +259,7 @@ export interface Rate extends MetricAggregationWithField { export interface BasePipelineMetricAggregation extends MetricAggregationWithField { pipelineAgg?: string; + type: PipelineMetricAggregationType; } export interface PipelineMetricAggregationWithMultipleBucketPaths extends BaseMetricAggregation { diff --git a/public/app/plugins/datasource/elasticsearch/types.ts b/public/app/plugins/datasource/elasticsearch/types.ts index 047ed748991..13979f8032c 100644 --- a/public/app/plugins/datasource/elasticsearch/types.ts +++ b/public/app/plugins/datasource/elasticsearch/types.ts @@ -2,7 +2,6 @@ import { DataSourceJsonData } from '@grafana/data'; import { BucketAggregationType, - Filter, MetricAggregation, MetricAggregationType, MovingAverageEWMAModelSettings, @@ -11,18 +10,9 @@ import { MovingAverageLinearModelSettings, MovingAverageModel, MovingAverageSimpleModelSettings, - PipelineMetricAggregationType, - TermsOrder, ExtendedStats, - BasePipelineMetricAggregation as SchemaBasePipelineMetricAggregation, - PipelineMetricAggregationWithMultipleBucketPaths as SchemaPipelineMetricAggregationWithMultipleBucketPaths, MovingAverage as SchemaMovingAverage, - Filters as SchemaFilters, - Terms as SchemaTerms, - DateHistogram as SchemaDateHistogram, - Histogram as SchemaHistogram, - GeoHashGrid as SchemaGeoHashGrid, - Nested as SchemaNested, + BucketAggregation, } from './dataquery.gen'; export * from './dataquery.gen'; @@ -30,17 +20,6 @@ export { Elasticsearch as ElasticsearchQuery } from './dataquery.gen'; export type MetricAggregationWithMeta = ExtendedStats; -// Start of temporary overrides because of incorrect type generation in dataquery.gen.ts -// TODO: Remove this once the type generation is fixed -export interface BasePipelineMetricAggregation extends SchemaBasePipelineMetricAggregation { - type: PipelineMetricAggregationType; -} - -export interface PipelineMetricAggregationWithMultipleBucketPaths - extends SchemaPipelineMetricAggregationWithMultipleBucketPaths { - type: PipelineMetricAggregationType; -} - export type MovingAverageModelSettings = Partial< Extract< | MovingAverageSimpleModelSettings @@ -56,52 +35,6 @@ export interface MovingAverage; } -export interface Filters extends SchemaFilters { - settings?: { - filters?: Filter[]; - }; -} - -export interface Terms extends SchemaTerms { - settings?: { - min_doc_count?: string; - missing?: string; - order?: TermsOrder; - orderBy?: string; - size?: string; - }; -} - -export interface DateHistogram extends SchemaDateHistogram { - settings?: { - interval?: string; - min_doc_count?: string; - offset?: string; - timeZone?: string; - trimEdges?: string; - }; -} - -export interface Histogram extends SchemaHistogram { - settings?: { - interval?: string; - min_doc_count?: string; - }; -} - -interface GeoHashGrid extends SchemaGeoHashGrid { - settings?: { - precision?: string; - }; -} - -interface Nested extends SchemaNested { - settings?: {}; -} - -export type BucketAggregation = DateHistogram | Histogram | Terms | Filters | GeoHashGrid | Nested; -// End of temporary overrides - export type Interval = 'Hourly' | 'Daily' | 'Weekly' | 'Monthly' | 'Yearly'; export interface ElasticsearchOptions extends DataSourceJsonData { From 312117bdfe7c19b731999a172dcc7f4dab759eae Mon Sep 17 00:00:00 2001 From: kay delaney <45561153+kaydelaney@users.noreply.github.com> Date: Wed, 8 Mar 2023 11:24:29 +0000 Subject: [PATCH 053/288] DatasourceVariables: Update query editor when switching datasources from picker (#62617) --- .../query/components/QueryEditorRow.tsx | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/public/app/features/query/components/QueryEditorRow.tsx b/public/app/features/query/components/QueryEditorRow.tsx index 84bee65a618..3c9137fad5d 100644 --- a/public/app/features/query/components/QueryEditorRow.tsx +++ b/public/app/features/query/components/QueryEditorRow.tsx @@ -77,6 +77,7 @@ export class QueryEditorRow extends PureComponent | null = null; angularQueryEditor: AngularComponent | null = null; + dataSourceSrv = getDataSourceSrv(); id = ''; state: State = { @@ -136,25 +137,33 @@ export class QueryEditorRow extends PureComponent, - loadedDataSourceIdentifier: dataSourceIdentifier, + loadedDataSourceIdentifier: interpolatedUID, hasTextEditMode: has(datasource, 'components.QueryCtrl.prototype.toggleEditorMode'), }); } @@ -182,7 +191,7 @@ export class QueryEditorRow extends PureComponent extends PureComponent { From 1d1f58f0eddf7acf39d62754896aed1a763b5464 Mon Sep 17 00:00:00 2001 From: Ieva Date: Wed, 8 Mar 2023 11:32:09 +0000 Subject: [PATCH 054/288] Service accounts: fix usage of `errutil` errors and convert more errors to `errutil` (#64299) * fix usage of errutil errors and convert more errors to errutil * fix tests --- pkg/services/serviceaccounts/api/api.go | 44 ++++--------------- pkg/services/serviceaccounts/api/token.go | 37 +++------------- .../serviceaccounts/api/token_test.go | 4 +- .../serviceaccounts/database/store.go | 6 +-- .../serviceaccounts/database/token_store.go | 8 ++-- pkg/services/serviceaccounts/errors.go | 14 ------ .../serviceaccounts/manager/service.go | 8 ++-- pkg/services/serviceaccounts/models.go | 16 ++++--- pkg/services/user/userimpl/user.go | 2 +- 9 files changed, 39 insertions(+), 100 deletions(-) delete mode 100644 pkg/services/serviceaccounts/errors.go diff --git a/pkg/services/serviceaccounts/api/api.go b/pkg/services/serviceaccounts/api/api.go index e1544f31e25..dc1baddb8f7 100644 --- a/pkg/services/serviceaccounts/api/api.go +++ b/pkg/services/serviceaccounts/api/api.go @@ -2,7 +2,6 @@ package api import ( "context" - "errors" "net/http" "strconv" @@ -114,22 +113,12 @@ func (api *ServiceAccountsAPI) CreateServiceAccount(c *contextmodel.ReqContext) } if err := api.validateRole(cmd.Role, &c.OrgRole); err != nil { - switch { - case errors.Is(err, serviceaccounts.ErrServiceAccountInvalidRole): - return response.Error(http.StatusBadRequest, err.Error(), err) - case errors.Is(err, serviceaccounts.ErrServiceAccountRolePrivilegeDenied): - return response.Error(http.StatusForbidden, err.Error(), err) - default: - return response.Error(http.StatusInternalServerError, "failed to create service account", err) - } + return response.ErrOrFallback(http.StatusInternalServerError, "failed to create service account", err) } serviceAccount, err := api.service.CreateServiceAccount(c.Req.Context(), c.OrgID, &cmd) - switch { - case errors.Is(err, serviceaccounts.ErrServiceAccountAlreadyExists): - return response.Error(http.StatusBadRequest, "Failed to create service account", err) - case err != nil: - return response.Error(http.StatusInternalServerError, "Failed to create service account", err) + if err != nil { + return response.ErrOrFallback(http.StatusInternalServerError, "Failed to create service account", err) } if !api.accesscontrol.IsDisabled() { @@ -169,12 +158,7 @@ func (api *ServiceAccountsAPI) RetrieveServiceAccount(ctx *contextmodel.ReqConte serviceAccount, err := api.service.RetrieveServiceAccount(ctx.Req.Context(), ctx.OrgID, scopeID) if err != nil { - switch { - case errors.Is(err, serviceaccounts.ErrServiceAccountNotFound): - return response.Error(http.StatusNotFound, "Failed to retrieve service account", err) - default: - return response.Error(http.StatusInternalServerError, "Failed to retrieve service account", err) - } + return response.ErrOrFallback(http.StatusInternalServerError, "Failed to retrieve service account", err) } saIDString := strconv.FormatInt(serviceAccount.Id, 10) @@ -220,24 +204,12 @@ func (api *ServiceAccountsAPI) UpdateServiceAccount(c *contextmodel.ReqContext) } if err := api.validateRole(cmd.Role, &c.OrgRole); err != nil { - switch { - case errors.Is(err, serviceaccounts.ErrServiceAccountInvalidRole): - return response.Error(http.StatusBadRequest, err.Error(), err) - case errors.Is(err, serviceaccounts.ErrServiceAccountRolePrivilegeDenied): - return response.Error(http.StatusForbidden, err.Error(), err) - default: - return response.Error(http.StatusInternalServerError, "failed to update service account", err) - } + return response.ErrOrFallback(http.StatusInternalServerError, "failed to update service account", err) } resp, err := api.service.UpdateServiceAccount(c.Req.Context(), c.OrgID, scopeID, &cmd) if err != nil { - switch { - case errors.Is(err, serviceaccounts.ErrServiceAccountNotFound): - return response.Error(http.StatusNotFound, "Failed to retrieve service account", err) - default: - return response.Error(http.StatusInternalServerError, "Failed update service account", err) - } + return response.ErrOrFallback(http.StatusInternalServerError, "Failed update service account", err) } saIDString := strconv.FormatInt(resp.Id, 10) @@ -255,10 +227,10 @@ func (api *ServiceAccountsAPI) UpdateServiceAccount(c *contextmodel.ReqContext) func (api *ServiceAccountsAPI) validateRole(r *org.RoleType, orgRole *org.RoleType) error { if r != nil && !r.IsValid() { - return serviceaccounts.ErrServiceAccountInvalidRole + return serviceaccounts.ErrServiceAccountInvalidRole.Errorf("invalid role specified") } if r != nil && !orgRole.Includes(*r) { - return serviceaccounts.ErrServiceAccountRolePrivilegeDenied + return serviceaccounts.ErrServiceAccountRolePrivilegeDenied.Errorf("can not assign a role higher than user's role") } return nil } diff --git a/pkg/services/serviceaccounts/api/token.go b/pkg/services/serviceaccounts/api/token.go index 9c4bc2918c7..ef2158b4f80 100644 --- a/pkg/services/serviceaccounts/api/token.go +++ b/pkg/services/serviceaccounts/api/token.go @@ -1,7 +1,6 @@ package api import ( - "errors" "net/http" "strconv" "time" @@ -9,7 +8,6 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" apikeygenprefix "github.com/grafana/grafana/pkg/components/apikeygenprefixed" - "github.com/grafana/grafana/pkg/services/apikey" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/serviceaccounts" "github.com/grafana/grafana/pkg/web" @@ -133,17 +131,12 @@ func (api *ServiceAccountsAPI) CreateToken(c *contextmodel.ReqContext) response. } // confirm service account exists - if _, err := api.service.RetrieveServiceAccount(c.Req.Context(), c.OrgID, saID); err != nil { - switch { - case errors.Is(err, serviceaccounts.ErrServiceAccountNotFound): - return response.Error(http.StatusNotFound, "Failed to retrieve service account", err) - default: - return response.Error(http.StatusInternalServerError, "Failed to retrieve service account", err) - } + if _, err = api.service.RetrieveServiceAccount(c.Req.Context(), c.OrgID, saID); err != nil { + return response.ErrOrFallback(http.StatusInternalServerError, "Failed to retrieve service account", err) } cmd := serviceaccounts.AddServiceAccountTokenCommand{} - if err := web.Bind(c.Req, &cmd); err != nil { + if err = web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "Bad request data", err) } @@ -176,13 +169,7 @@ func (api *ServiceAccountsAPI) CreateToken(c *contextmodel.ReqContext) response. apiKey, err := api.service.AddServiceAccountToken(c.Req.Context(), saID, &cmd) if err != nil { - if errors.Is(err, serviceaccounts.ErrInvalidTokenExpiration) { - return response.Error(http.StatusBadRequest, err.Error(), nil) - } - if errors.Is(err, serviceaccounts.ErrDuplicateToken) { - return response.Error(http.StatusConflict, err.Error(), nil) - } - return response.Error(http.StatusInternalServerError, "Failed to add service account token", err) + return response.ErrOrFallback(http.StatusInternalServerError, "failed to add service account token", err) } result := &dtos.NewApiKeyResult{ @@ -218,12 +205,7 @@ func (api *ServiceAccountsAPI) DeleteToken(c *contextmodel.ReqContext) response. // confirm service account exists if _, err := api.service.RetrieveServiceAccount(c.Req.Context(), c.OrgID, saID); err != nil { - switch { - case errors.Is(err, serviceaccounts.ErrServiceAccountNotFound): - return response.Error(http.StatusNotFound, "Failed to retrieve service account", err) - default: - return response.Error(http.StatusInternalServerError, "Failed to retrieve service account", err) - } + return response.ErrOrFallback(http.StatusInternalServerError, "Failed to retrieve service account", err) } tokenID, err := strconv.ParseInt(web.Params(c.Req)[":tokenId"], 10, 64) @@ -232,14 +214,7 @@ func (api *ServiceAccountsAPI) DeleteToken(c *contextmodel.ReqContext) response. } if err = api.service.DeleteServiceAccountToken(c.Req.Context(), c.OrgID, saID, tokenID); err != nil { - status := http.StatusNotFound - if err != nil && !errors.Is(err, apikey.ErrNotFound) { - status = http.StatusInternalServerError - } else { - err = apikey.ErrNotFound - } - - return response.Error(status, failedToDeleteMsg, err) + return response.ErrOrFallback(http.StatusInternalServerError, failedToDeleteMsg, err) } return response.Success("Service account token deleted") diff --git a/pkg/services/serviceaccounts/api/token_test.go b/pkg/services/serviceaccounts/api/token_test.go index bcce9ebf9bb..9796e58d254 100644 --- a/pkg/services/serviceaccounts/api/token_test.go +++ b/pkg/services/serviceaccounts/api/token_test.go @@ -92,7 +92,7 @@ func TestServiceAccountsAPI_CreateToken(t *testing.T) { body: `{"name": "test"}`, tokenTTL: -1, permissions: []accesscontrol.Permission{{Action: serviceaccounts.ActionWrite, Scope: "serviceaccounts:id:1"}}, - expectedErr: serviceaccounts.ErrServiceAccountNotFound, + expectedErr: serviceaccounts.ErrServiceAccountNotFound.Errorf(""), expectedCode: http.StatusNotFound, }, { @@ -155,7 +155,7 @@ func TestServiceAccountsAPI_DeleteToken(t *testing.T) { saID: 1, apikeyID: 1, permissions: []accesscontrol.Permission{{Action: serviceaccounts.ActionWrite, Scope: "serviceaccounts:id:1"}}, - expectedErr: serviceaccounts.ErrServiceAccountNotFound, + expectedErr: serviceaccounts.ErrServiceAccountNotFound.Errorf(""), expectedCode: http.StatusNotFound, }, } diff --git a/pkg/services/serviceaccounts/database/store.go b/pkg/services/serviceaccounts/database/store.go index 5679e87ff12..7dd421f7519 100644 --- a/pkg/services/serviceaccounts/database/store.go +++ b/pkg/services/serviceaccounts/database/store.go @@ -160,7 +160,7 @@ func (s *ServiceAccountsStoreImpl) deleteServiceAccount(sess *db.Session, orgId, return err } if !has { - return serviceaccounts.ErrServiceAccountNotFound + return serviceaccounts.ErrServiceAccountNotFound.Errorf("service account with id %d not found", serviceAccountId) } for _, sql := range ServiceAccountDeletions(s.sqlStore.GetDialect()) { _, err := sess.Exec(sql, user.ID) @@ -211,7 +211,7 @@ func (s *ServiceAccountsStoreImpl) RetrieveServiceAccount(ctx context.Context, o if ok, err := sess.Get(serviceAccount); err != nil { return err } else if !ok { - return serviceaccounts.ErrServiceAccountNotFound + return serviceaccounts.ErrServiceAccountNotFound.Errorf("service account with id %d not found", serviceAccountId) } return nil @@ -248,7 +248,7 @@ func (s *ServiceAccountsStoreImpl) RetrieveServiceAccountIdByName(ctx context.Co if ok, err := sess.Get(serviceAccount); err != nil { return err } else if !ok { - return serviceaccounts.ErrServiceAccountNotFound + return serviceaccounts.ErrServiceAccountNotFound.Errorf("service account with name %s not found", name) } return nil diff --git a/pkg/services/serviceaccounts/database/token_store.go b/pkg/services/serviceaccounts/database/token_store.go index 850d6a90b3e..4674f60c7bc 100644 --- a/pkg/services/serviceaccounts/database/token_store.go +++ b/pkg/services/serviceaccounts/database/token_store.go @@ -61,9 +61,9 @@ func (s *ServiceAccountsStoreImpl) AddServiceAccountToken(ctx context.Context, s if err := s.apiKeyService.AddAPIKey(ctx, addKeyCmd); err != nil { switch { case errors.Is(err, apikey.ErrDuplicate): - return serviceaccounts.ErrDuplicateToken + return serviceaccounts.ErrDuplicateToken.Errorf("service account token with name %s already exists in the organization", cmd.Name) case errors.Is(err, apikey.ErrInvalidExpiration): - return serviceaccounts.ErrInvalidTokenExpiration + return serviceaccounts.ErrInvalidTokenExpiration.Errorf("invalid service account token expiration value %d", cmd.SecondsToLive) } return err @@ -84,7 +84,7 @@ func (s *ServiceAccountsStoreImpl) DeleteServiceAccountToken(ctx context.Context } affected, err := result.RowsAffected() if affected == 0 { - return serviceaccounts.ErrServiceAccountTokenNotFound + return serviceaccounts.ErrServiceAccountTokenNotFound.Errorf("service account token with id %d not found", tokenId) } return err @@ -101,7 +101,7 @@ func (s *ServiceAccountsStoreImpl) RevokeServiceAccountToken(ctx context.Context } affected, err := result.RowsAffected() if affected == 0 { - return serviceaccounts.ErrServiceAccountTokenNotFound + return serviceaccounts.ErrServiceAccountTokenNotFound.Errorf("service account token with id %d not found for service account with id %d", tokenId, serviceAccountId) } return err diff --git a/pkg/services/serviceaccounts/errors.go b/pkg/services/serviceaccounts/errors.go deleted file mode 100644 index a2005fe0d16..00000000000 --- a/pkg/services/serviceaccounts/errors.go +++ /dev/null @@ -1,14 +0,0 @@ -package serviceaccounts - -import "errors" - -var ( - ErrServiceAccountNotFound = errors.New("service account not found") - ErrServiceAccountInvalidRole = errors.New("invalid role specified") - ErrServiceAccountRolePrivilegeDenied = errors.New("can not assign a role higher than user's role") - ErrServiceAccountInvalidOrgID = errors.New("invalid org id specified") - ErrServiceAccountInvalidID = errors.New("invalid service account id specified") - ErrServiceAccountInvalidAPIKeyID = errors.New("invalid api key id specified") - ErrServiceAccountInvalidTokenID = errors.New("invalid service account token id specified") - ErrServiceAccountUpdateForm = errors.New("invalid update form") -) diff --git a/pkg/services/serviceaccounts/manager/service.go b/pkg/services/serviceaccounts/manager/service.go index a415617fd6a..aaf08d8aa47 100644 --- a/pkg/services/serviceaccounts/manager/service.go +++ b/pkg/services/serviceaccounts/manager/service.go @@ -244,25 +244,25 @@ func (sa *ServiceAccountsService) MigrateApiKeysToServiceAccounts(ctx context.Co func validOrgID(orgID int64) error { if orgID == 0 { - return serviceaccounts.ErrServiceAccountInvalidOrgID + return serviceaccounts.ErrServiceAccountInvalidOrgID.Errorf("invalid org ID 0 has been specified") } return nil } func validServiceAccountID(serviceaccountID int64) error { if serviceaccountID == 0 { - return serviceaccounts.ErrServiceAccountInvalidID + return serviceaccounts.ErrServiceAccountInvalidID.Errorf("invalid service account ID 0 has been specified") } return nil } func validServiceAccountTokenID(tokenID int64) error { if tokenID == 0 { - return serviceaccounts.ErrServiceAccountInvalidTokenID + return serviceaccounts.ErrServiceAccountInvalidTokenID.Errorf("invalid service account token ID 0 has been specified") } return nil } func validAPIKeyID(apiKeyID int64) error { if apiKeyID == 0 { - return serviceaccounts.ErrServiceAccountInvalidAPIKeyID + return serviceaccounts.ErrServiceAccountInvalidAPIKeyID.Errorf("invalid API key ID 0 has been specified") } return nil } diff --git a/pkg/services/serviceaccounts/models.go b/pkg/services/serviceaccounts/models.go index 37713d2a7cd..8797bea282b 100644 --- a/pkg/services/serviceaccounts/models.go +++ b/pkg/services/serviceaccounts/models.go @@ -24,11 +24,17 @@ const ( ) var ( - ErrServiceAccountAlreadyExists = errutil.NewBase(errutil.StatusBadRequest, "serviceaccounts.ErrAlreadyExists", errutil.WithPublicMessage("service account already exists")) - ErrServiceAccountTokenNotFound = errutil.NewBase(errutil.StatusNotFound, "serviceaccounts.ErrTokenNotFound", errutil.WithPublicMessage("service account token not found")) - ErrInvalidTokenExpiration = errutil.NewBase(errutil.StatusValidationFailed, "serviceaccounts.ErrInvalidInput", errutil.WithPublicMessage("invalid SecondsToLive value")) - ErrDuplicateToken = errutil.NewBase(errutil.StatusBadRequest, "serviceaccounts.ErrTokenAlreadyExists", errutil.WithPublicMessage("service account token with given name already exists in the organization")) - ErrServiceAccountAndTokenMismatch = errutil.NewBase(errutil.StatusBadRequest, "serviceaccounts.ErrToeknMismatch", errutil.WithPublicMessage("API token does not belong to the given service account")) + ErrServiceAccountNotFound = errutil.NewBase(errutil.StatusNotFound, "serviceaccounts.ErrNotFound", errutil.WithPublicMessage("service account not found")) + ErrServiceAccountInvalidRole = errutil.NewBase(errutil.StatusBadRequest, "serviceaccounts.ErrInvalidRoleSpecified", errutil.WithPublicMessage("invalid role specified")) + ErrServiceAccountRolePrivilegeDenied = errutil.NewBase(errutil.StatusForbidden, "serviceaccounts.ErrRoleForbidden", errutil.WithPublicMessage("can not assign a role higher than user's role")) + ErrServiceAccountInvalidOrgID = errutil.NewBase(errutil.StatusBadRequest, "serviceaccounts.ErrInvalidOrgId", errutil.WithPublicMessage("invalid org id specified")) + ErrServiceAccountInvalidID = errutil.NewBase(errutil.StatusBadRequest, "serviceaccounts.ErrInvalidId", errutil.WithPublicMessage("invalid service account id specified")) + ErrServiceAccountInvalidAPIKeyID = errutil.NewBase(errutil.StatusBadRequest, "serviceaccounts.ErrInvalidAPIKeyId", errutil.WithPublicMessage("invalid api key id specified")) + ErrServiceAccountInvalidTokenID = errutil.NewBase(errutil.StatusBadRequest, "serviceaccounts.ErrInvalidTokenId", errutil.WithPublicMessage("invalid service account token id specified")) + ErrServiceAccountAlreadyExists = errutil.NewBase(errutil.StatusBadRequest, "serviceaccounts.ErrAlreadyExists", errutil.WithPublicMessage("service account already exists")) + ErrServiceAccountTokenNotFound = errutil.NewBase(errutil.StatusNotFound, "serviceaccounts.ErrTokenNotFound", errutil.WithPublicMessage("service account token not found")) + ErrInvalidTokenExpiration = errutil.NewBase(errutil.StatusValidationFailed, "serviceaccounts.ErrInvalidInput", errutil.WithPublicMessage("invalid SecondsToLive value")) + ErrDuplicateToken = errutil.NewBase(errutil.StatusBadRequest, "serviceaccounts.ErrTokenAlreadyExists", errutil.WithPublicMessage("service account token with given name already exists in the organization")) ) type ServiceAccount struct { diff --git a/pkg/services/user/userimpl/user.go b/pkg/services/user/userimpl/user.go index 1ec9e76e5a0..c4146c6c8c0 100644 --- a/pkg/services/user/userimpl/user.go +++ b/pkg/services/user/userimpl/user.go @@ -366,7 +366,7 @@ func (s *Service) CreateServiceAccount(ctx context.Context, cmd *user.CreateUser cmd.Email = cmd.Login err := s.store.LoginConflict(ctx, cmd.Login, cmd.Email, s.cfg.CaseInsensitiveLogin) if err != nil { - return nil, serviceaccounts.ErrServiceAccountAlreadyExists + return nil, serviceaccounts.ErrServiceAccountAlreadyExists.Errorf("service account with login %s already exists", cmd.Login) } // create user From e4d591fc0161ed42d703b46052540eb5ddbe4a86 Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Wed, 8 Mar 2023 03:36:06 -0800 Subject: [PATCH 055/288] OptionsPicker: Correctly highlight template variable value when filtering (#63495) * VariablePicker: Correctly highlight items when filtering * Change interactions for selecting values in variable options picker * Review --- .../pickers/OptionsPicker/OptionsPicker.tsx | 1 - .../pickers/OptionsPicker/actions.test.ts | 57 +++++++++++++------ .../pickers/OptionsPicker/actions.ts | 7 ++- .../pickers/OptionsPicker/reducer.test.ts | 14 ++--- .../pickers/OptionsPicker/reducer.ts | 6 +- .../pickers/shared/VariableInput.tsx | 6 +- 6 files changed, 58 insertions(+), 33 deletions(-) diff --git a/public/app/features/variables/pickers/OptionsPicker/OptionsPicker.tsx b/public/app/features/variables/pickers/OptionsPicker/OptionsPicker.tsx index 0a93e7ca525..942cdedd0e6 100644 --- a/public/app/features/variables/pickers/OptionsPicker/OptionsPicker.tsx +++ b/public/app/features/variables/pickers/OptionsPicker/OptionsPicker.tsx @@ -150,7 +150,6 @@ export const optionPickerFactory = { .whenActionIsDispatched(navigateOptions('key', NavigationKey.moveDown, clearOthers)) .whenAsyncActionIsDispatched(navigateOptions('key', key, clearOthers), true); - const option = createOption(['sameLabel'], ['B'], true); - // Check selecting the second option triggers variables to update tester.thenDispatchedActionsShouldEqual( - toKeyedAction('key', toggleOption({ option: options[1], forceSelect: true, clearOthers })), - toKeyedAction('key', setCurrentVariableValue(toVariablePayload(variable, { option }))), - toKeyedAction('key', changeVariableProp(toVariablePayload(variable, { propName: 'queryValue', propValue: '' }))), - toKeyedAction('key', hideOptions()), - toKeyedAction('key', setCurrentVariableValue(toVariablePayload(variable, { option }))) + toKeyedAction('key', toggleOption({ option: options[1], forceSelect: false, clearOthers })) ); - expect(locationService.partial).toHaveBeenLastCalledWith({ 'var-Constant': ['B'] }); }); describe('when navigateOptions is dispatched with navigation key selectAndClose after highlighting the second option', () => { - it('then correct actions are dispatched', async () => { + it('then correct actions are dispatched for multi-value variable', async () => { const options = [createOption('A'), createOption('B'), createOption('C')]; const variable = createMultiVariable({ options, current: createOption(['A'], ['A'], true), includeAll: false }); const clearOthers = false; const key = NavigationKey.selectAndClose; + const tester = await reduxTester() + .givenRootReducer(getRootReducer()) + .whenActionIsDispatched( + toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable }))) + ) + .whenActionIsDispatched(toKeyedAction('key', showOptions(variable))) + .whenActionIsDispatched(navigateOptions('key', NavigationKey.moveDown, clearOthers)) + .whenActionIsDispatched(navigateOptions('key', NavigationKey.moveDown, clearOthers)) + .whenActionIsDispatched(navigateOptions('key', NavigationKey.moveDown, clearOthers)) + .whenActionIsDispatched(navigateOptions('key', NavigationKey.moveUp, clearOthers)) + .whenAsyncActionIsDispatched(navigateOptions('key', key, clearOthers), true); + + tester.thenDispatchedActionsShouldEqual( + toKeyedAction('key', toggleOption({ option: options[1], forceSelect: false, clearOthers })) + ); + }); + + it('then correct actions are dispatched for single-value variable', async () => { + const options = [createOption('A'), createOption('B'), createOption('C')]; + const variable = createVariable({ options, current: createOption('A', 'A', true), includeAll: false }); + + const clearOthers = false; + const key = NavigationKey.selectAndClose; + const tester = await reduxTester() .givenRootReducer(getRootReducer()) .whenActionIsDispatched( @@ -246,9 +263,9 @@ describe('options picker actions', () => { .whenAsyncActionIsDispatched(navigateOptions('key', key, clearOthers), true); const option = { - ...createOption(['B']), + ...createOption('B'), selected: true, - value: ['B'], + value: 'B', }; tester.thenDispatchedActionsShouldEqual( @@ -261,7 +278,7 @@ describe('options picker actions', () => { toKeyedAction('key', hideOptions()), toKeyedAction('key', setCurrentVariableValue(toVariablePayload(variable, { option }))) ); - expect(locationService.partial).toHaveBeenLastCalledWith({ 'var-Constant': ['B'] }); + expect(locationService.partial).toHaveBeenLastCalledWith({ 'var-Constant': 'B' }); }); }); @@ -526,7 +543,7 @@ describe('options picker actions', () => { .whenActionIsDispatched(toggleOptionByHighlight('key', clearOthers)); const optionA = createOption('A'); - const optionBC = createOption('BC'); + const optionBD = createOption('BD'); tester.thenDispatchedActionsShouldEqual( toKeyedAction('key', toggleOption({ option: optionA, forceSelect: false, clearOthers })), @@ -534,13 +551,21 @@ describe('options picker actions', () => { toKeyedAction('key', updateOptionsAndFilter(variable.options)), toKeyedAction('key', moveOptionsHighlight(1)), toKeyedAction('key', moveOptionsHighlight(1)), - toKeyedAction('key', toggleOption({ option: optionBC, forceSelect: false, clearOthers })) + toKeyedAction('key', toggleOption({ option: optionBD, forceSelect: false, clearOthers })) ); }); }); }); function createMultiVariable(extend?: Partial): QueryVariableModel { + return createVariable({ + multi: true, + includeAll: true, + ...(extend ?? {}), + }); +} + +function createVariable(extend?: Partial): QueryVariableModel { return { ...initialVariableModelState, type: 'query', @@ -556,8 +581,8 @@ function createMultiVariable(extend?: Partial): QueryVariabl sort: VariableSort.alphabeticalAsc, refresh: VariableRefresh.never, regex: '', - multi: true, - includeAll: true, + multi: false, + includeAll: false, ...(extend ?? {}), }; } diff --git a/public/app/features/variables/pickers/OptionsPicker/actions.ts b/public/app/features/variables/pickers/OptionsPicker/actions.ts index 675adcf9376..a4c5b3bd134 100644 --- a/public/app/features/variables/pickers/OptionsPicker/actions.ts +++ b/public/app/features/variables/pickers/OptionsPicker/actions.ts @@ -34,8 +34,13 @@ export const navigateOptions = (rootStateKey: string, key: NavigationKey, clearO } if (key === NavigationKey.selectAndClose) { + const picker = getVariablesState(rootStateKey, getState()).optionsPicker; + + if (picker.multi) { + return dispatch(toggleOptionByHighlight(rootStateKey, clearOthers)); + } dispatch(toggleOptionByHighlight(rootStateKey, clearOthers, true)); - return await dispatch(commitChangesToVariable(rootStateKey)); + return dispatch(commitChangesToVariable(rootStateKey)); } if (key === NavigationKey.moveDown) { diff --git a/public/app/features/variables/pickers/OptionsPicker/reducer.test.ts b/public/app/features/variables/pickers/OptionsPicker/reducer.test.ts index e4da8a26d04..f6ccf40632e 100644 --- a/public/app/features/variables/pickers/OptionsPicker/reducer.test.ts +++ b/public/app/features/variables/pickers/OptionsPicker/reducer.test.ts @@ -535,7 +535,7 @@ describe('optionsPickerReducer', () => { ], selectedValues: [{ text: 'All', value: '$__all', selected: true }], queryValue: 'A', - highlightIndex: -1, + highlightIndex: 0, }); }); @@ -559,7 +559,7 @@ describe('optionsPickerReducer', () => { options: [{ text: 'All', value: '$__all', selected: true }], selectedValues: [{ text: 'All', value: '$__all', selected: true }], queryValue: 'A', - highlightIndex: -1, + highlightIndex: 0, }); }); }); @@ -583,7 +583,7 @@ describe('optionsPickerReducer', () => { options: [{ text: 'option:1337', value: 'option:1337', selected: false }], selectedValues: [], queryValue: 'option:1337', - highlightIndex: -1, + highlightIndex: 0, }); }); }); @@ -635,7 +635,7 @@ describe('optionsPickerReducer', () => { ], selectedValues: [{ text: 'B', value: 'B', selected: true }], queryValue: 'A', - highlightIndex: -1, + highlightIndex: 0, }) .whenActionIsDispatched(updateSearchQuery('')) .thenStateShouldEqual({ @@ -646,7 +646,7 @@ describe('optionsPickerReducer', () => { ], selectedValues: [{ text: 'B', value: 'B', selected: true }], queryValue: '', - highlightIndex: -1, + highlightIndex: 0, }) .whenActionIsDispatched(updateOptionsAndFilter(options)) .thenStateShouldEqual({ @@ -658,7 +658,7 @@ describe('optionsPickerReducer', () => { ], selectedValues: [{ text: 'B', value: 'B', selected: true }], queryValue: '', - highlightIndex: -1, + highlightIndex: 0, }); }); }); @@ -766,7 +766,7 @@ describe('optionsPickerReducer', () => { options: [{ text: 'option:11256', value: 'option:11256', selected: false }], selectedValues: [], queryValue: 'option:11256', - highlightIndex: -1, + highlightIndex: 0, }); }); }); diff --git a/public/app/features/variables/pickers/OptionsPicker/reducer.ts b/public/app/features/variables/pickers/OptionsPicker/reducer.ts index 535a0580d11..4d0d32d4f20 100644 --- a/public/app/features/variables/pickers/OptionsPicker/reducer.ts +++ b/public/app/features/variables/pickers/OptionsPicker/reducer.ts @@ -1,5 +1,5 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { cloneDeep, isString, trim } from 'lodash'; +import { cloneDeep, isString, trimStart } from 'lodash'; import { applyStateChanges } from '../../../../core/utils/applyStateChanges'; import { ALL_VARIABLE_VALUE } from '../../constants'; @@ -197,7 +197,7 @@ const optionsPickerSlice = createSlice({ return state; }, updateOptionsAndFilter: (state, action: PayloadAction): OptionsPickerState => { - const searchQuery = trim((state.queryValue ?? '').toLowerCase()); + const searchQuery = trimStart((state.queryValue ?? '').toLowerCase()); state.options = action.payload.filter((option) => { const optionsText = option.text ?? ''; @@ -205,7 +205,7 @@ const optionsPickerSlice = createSlice({ return text.toLowerCase().indexOf(searchQuery) !== -1; }); - state.highlightIndex = -1; + state.highlightIndex = 0; return applyStateChanges(state, updateDefaultSelection, updateOptions); }, diff --git a/public/app/features/variables/pickers/shared/VariableInput.tsx b/public/app/features/variables/pickers/shared/VariableInput.tsx index 94c8a1762aa..5c586233ae7 100644 --- a/public/app/features/variables/pickers/shared/VariableInput.tsx +++ b/public/app/features/variables/pickers/shared/VariableInput.tsx @@ -8,15 +8,11 @@ export interface Props extends Omit, 'onChange onChange: (value: string) => void; onNavigate: (key: NavigationKey, clearOthers: boolean) => void; value: string | null; - currenthighlightindex?: number; } export class VariableInput extends PureComponent { onKeyDown = (event: React.KeyboardEvent) => { - if ( - NavigationKey[event.keyCode] && - !(event.keyCode === NavigationKey.select && this.props.currenthighlightindex === -1) - ) { + if (NavigationKey[event.keyCode] && event.keyCode !== NavigationKey.select) { const clearOthers = event.ctrlKey || event.metaKey || event.shiftKey; this.props.onNavigate(event.keyCode as NavigationKey, clearOthers); event.preventDefault(); From 8ad9e70ef086c3e380fe1c3f1a66615b202c75c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Jamr=C3=B3z?= Date: Wed, 8 Mar 2023 12:45:45 +0100 Subject: [PATCH 056/288] Feature Toggles: Assign ownership of Explore Squad's feature toggles (#64382) --- pkg/services/featuremgmt/codeowners.go | 1 + pkg/services/featuremgmt/registry.go | 2 ++ pkg/services/featuremgmt/toggles_gen_test.go | 2 -- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pkg/services/featuremgmt/codeowners.go b/pkg/services/featuremgmt/codeowners.go index 2f746836f2c..7b62880638d 100644 --- a/pkg/services/featuremgmt/codeowners.go +++ b/pkg/services/featuremgmt/codeowners.go @@ -7,6 +7,7 @@ type codeowner string const ( grafanaAppPlatformSquad codeowner = "@grafana/grafana-app-platform-squad" grafanaDashboardsSquad codeowner = "@grafana/dashboards-squad" + grafanaExploreSquad codeowner = "@grafana/explore-squad" grafanaBiSquad codeowner = "@grafana/grafana-bi-squad" grafanaDatavizSquad codeowner = "@grafana/dataviz-squad" grafanaUserEssentialsSquad codeowner = "@grafana/user-essentials" diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index f5b5a9178ad..f603daae548 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -139,6 +139,7 @@ var ( Description: "Enable mixed datasource in Explore", State: FeatureStateAlpha, FrontendOnly: true, + Owner: grafanaExploreSquad, }, { Name: "tracing", @@ -156,6 +157,7 @@ var ( Name: "correlations", Description: "Correlations page", State: FeatureStateAlpha, + Owner: grafanaExploreSquad, }, { Name: "cloudWatchDynamicLabels", diff --git a/pkg/services/featuremgmt/toggles_gen_test.go b/pkg/services/featuremgmt/toggles_gen_test.go index 9da66f771b5..9e4b55c8072 100644 --- a/pkg/services/featuremgmt/toggles_gen_test.go +++ b/pkg/services/featuremgmt/toggles_gen_test.go @@ -48,10 +48,8 @@ func TestFeatureToggleFiles(t *testing.T) { "prometheusAzureOverrideAudience": true, "lokiDataframeApi": true, "featureHighlights": true, - "exploreMixedDatasource": true, "tracing": true, "newTraceView": true, - "correlations": true, "cloudWatchDynamicLabels": true, "traceToMetrics": true, "validateDashboardsOnSave": true, From 986a1c2a1b30182910bdc0602ce582cedb727484 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Wed, 8 Mar 2023 11:48:51 +0000 Subject: [PATCH 057/288] Chore: group uLibrary updates in renovate, revert uplot update for now (#64386) * group uLibrary updates in renovate, revert uplot update for now * fix json --- .github/renovate.json5 | 10 +++++++++- package.json | 2 +- packages/grafana-ui/package.json | 2 +- yarn.lock | 12 ++++++------ 4 files changed, 17 insertions(+), 9 deletions(-) diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 11a0b83de88..a652df0a8ad 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -83,7 +83,15 @@ "matchPackagePrefixes": [ "@visx/" ] - } + }, + { + "groupName": "uLibraries", + "matchPackageNames": [ + "@leeoniya/ufuzzy", + "uplot" + ], + "reviewers": ["leeoniya"], + }, ], "pin": { "enabled": false diff --git a/package.json b/package.json index d02e59f58e5..779c727facc 100644 --- a/package.json +++ b/package.json @@ -271,7 +271,7 @@ "@grafana/schema": "workspace:*", "@grafana/ui": "workspace:*", "@kusto/monaco-kusto": "5.3.6", - "@leeoniya/ufuzzy": "1.0.5", + "@leeoniya/ufuzzy": "1.0.2", "@lezer/common": "1.0.2", "@lezer/highlight": "1.1.3", "@lezer/lr": "1.3.3", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index ea1f17c84fc..8c229cc0eae 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -52,7 +52,7 @@ "@grafana/data": "9.5.0-pre", "@grafana/e2e-selectors": "9.5.0-pre", "@grafana/schema": "9.5.0-pre", - "@leeoniya/ufuzzy": "1.0.5", + "@leeoniya/ufuzzy": "1.0.2", "@monaco-editor/react": "4.4.6", "@popperjs/core": "2.11.6", "@react-aria/button": "3.6.1", diff --git a/yarn.lock b/yarn.lock index 3fd29b7ebef..d3103eb224a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5351,7 +5351,7 @@ __metadata: "@grafana/e2e-selectors": 9.5.0-pre "@grafana/schema": 9.5.0-pre "@grafana/tsconfig": ^1.2.0-rc1 - "@leeoniya/ufuzzy": 1.0.5 + "@leeoniya/ufuzzy": 1.0.2 "@mdx-js/react": 1.6.22 "@monaco-editor/react": 4.4.6 "@popperjs/core": 2.11.6 @@ -6330,10 +6330,10 @@ __metadata: languageName: node linkType: hard -"@leeoniya/ufuzzy@npm:1.0.5": - version: 1.0.5 - resolution: "@leeoniya/ufuzzy@npm:1.0.5" - checksum: 49e0633ea71fdfb036980b24c07bc524fc44e5fa44425edf1df87c66bdfa07a1833257508b01aed25485318973d676b48e9e0ced98658783c21582cc14e8f982 +"@leeoniya/ufuzzy@npm:1.0.2": + version: 1.0.2 + resolution: "@leeoniya/ufuzzy@npm:1.0.2" + checksum: 5460378a8c32d121b0bc7c8e95cde995316516655528e248051b1bf360cdca0311ef3275de14b802587748231333cee6183c931b3abba26f9e4236ecc4959aa3 languageName: node linkType: hard @@ -22136,7 +22136,7 @@ __metadata: "@grafana/tsconfig": ^1.2.0-rc1 "@grafana/ui": "workspace:*" "@kusto/monaco-kusto": 5.3.6 - "@leeoniya/ufuzzy": 1.0.5 + "@leeoniya/ufuzzy": 1.0.2 "@lezer/common": 1.0.2 "@lezer/highlight": 1.1.3 "@lezer/lr": 1.3.3 From 0c8876c3a2dcf3bf88a83f11be3e676a2e3b133f Mon Sep 17 00:00:00 2001 From: George Robinson Date: Wed, 8 Mar 2023 12:25:02 +0000 Subject: [PATCH 058/288] Alerting: Return errors when expanding templates (#63662) This commit changes the state package so that errors encountered while expanding templates for custom labels and annotations are returned from the function. This is not used at present, but will be used in the future as we look at how to offer better feedback to users who don't have access to logs, for example our customers who use Hosted Grafana. --- pkg/services/ngalert/state/cache.go | 51 ++++++---- pkg/services/ngalert/state/cache_test.go | 92 +++++++++++++++++++ .../ngalert/state/template/template.go | 14 ++- .../ngalert/state/template/template_test.go | 13 ++- 4 files changed, 145 insertions(+), 25 deletions(-) diff --git a/pkg/services/ngalert/state/cache.go b/pkg/services/ngalert/state/cache.go index 192e40bfa32..34078dd721d 100644 --- a/pkg/services/ngalert/state/cache.go +++ b/pkg/services/ngalert/state/cache.go @@ -6,8 +6,10 @@ import ( "net/url" "strings" "sync" + "time" "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/hashicorp/go-multierror" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/ngalert/eval" @@ -49,7 +51,14 @@ func (c *cache) getOrCreate(ctx context.Context, log log.Logger, alertRule *ngMo } func (rs *ruleStates) getOrCreate(ctx context.Context, log log.Logger, alertRule *ngModels.AlertRule, result eval.Result, extraLabels data.Labels, externalURL *url.URL) *State { - ruleLabels, annotations := rs.expandRuleLabelsAndAnnotations(ctx, log, alertRule, result, extraLabels, externalURL) + // Merge both the extra labels and the labels from the evaluation into a common set + // of labels that can be expanded in custom labels and annotations. + templateData := template.NewData(mergeLabels(extraLabels, result.Instance), result) + + // For now, do nothing with these errors as they are already logged in expand. + // In the future, we want to show these errors to the user somehow. + labels, _ := expand(ctx, log, alertRule.Title, alertRule.Labels, templateData, externalURL, result.EvaluatedAt) + annotations, _ := expand(ctx, log, alertRule.Title, alertRule.Annotations, templateData, externalURL, result.EvaluatedAt) values := make(map[string]float64) for refID, v := range result.Values { @@ -60,12 +69,12 @@ func (rs *ruleStates) getOrCreate(ctx context.Context, log log.Logger, alertRule } } - lbs := make(data.Labels, len(extraLabels)+len(ruleLabels)+len(result.Instance)) + lbs := make(data.Labels, len(extraLabels)+len(labels)+len(result.Instance)) dupes := make(data.Labels) for key, val := range extraLabels { lbs[key] = val } - for key, val := range ruleLabels { + for key, val := range labels { ruleVal, ok := lbs[key] // if duplicate labels exist, reserved label will take precedence if ok { @@ -135,25 +144,27 @@ func (rs *ruleStates) getOrCreate(ctx context.Context, log log.Logger, alertRule return newState } -func (rs *ruleStates) expandRuleLabelsAndAnnotations(ctx context.Context, log log.Logger, alertRule *ngModels.AlertRule, alertInstance eval.Result, extraLabels data.Labels, externalURL *url.URL) (data.Labels, data.Labels) { - // use labels from the result and extra labels to expand the labels and annotations declared by the rule - templateLabels := mergeLabels(extraLabels, alertInstance.Instance) - - expand := func(original map[string]string) map[string]string { - expanded := make(map[string]string, len(original)) - for k, v := range original { - ev, err := template.Expand(ctx, alertRule.Title, v, template.NewData(templateLabels, alertInstance), externalURL, alertInstance.EvaluatedAt) - expanded[k] = ev - if err != nil { - log.Error("Error in expanding template", "name", k, "value", v, "error", err) - // Store the original template on error. - expanded[k] = v - } +// expand returns the expanded templates of all annotations or labels for the template data. +// If a template cannot be expanded due to an error in the template the original template is +// maintained and an error is added to the multierror. All errors in the multierror are +// template.ExpandError errors. +func expand(ctx context.Context, log log.Logger, name string, original map[string]string, data template.Data, externalURL *url.URL, evaluatedAt time.Time) (map[string]string, error) { + var ( + errs *multierror.Error + expanded = make(map[string]string, len(original)) + ) + for k, v := range original { + result, err := template.Expand(ctx, name, v, data, externalURL, evaluatedAt) + if err != nil { + log.Error("Error in expanding template", "error", err) + errs = multierror.Append(errs, err) + // keep the original template on error + expanded[k] = v + } else { + expanded[k] = result } - - return expanded } - return expand(alertRule.Labels), expand(alertRule.Annotations) + return expanded, errs.ErrorOrNil() } func (rs *ruleStates) deleteStates(predicate func(s *State) bool) []*State { diff --git a/pkg/services/ngalert/state/cache_test.go b/pkg/services/ngalert/state/cache_test.go index 5f5a2de3428..5a1442de4b8 100644 --- a/pkg/services/ngalert/state/cache_test.go +++ b/pkg/services/ngalert/state/cache_test.go @@ -2,20 +2,112 @@ package state import ( "context" + "errors" "fmt" "net/url" "testing" + "time" "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/hashicorp/go-multierror" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/ngalert/eval" "github.com/grafana/grafana/pkg/services/ngalert/models" + "github.com/grafana/grafana/pkg/services/ngalert/state/template" "github.com/grafana/grafana/pkg/util" ) +func Test_expand(t *testing.T) { + ctx := context.Background() + logger := log.NewNopLogger() + + // This test asserts that multierror returns a nil error if there are no errors. + // If the expand function forgets to use ErrorOrNil() then the error returned will + // be non-nil even if no errors have been added to the multierror. + t.Run("err is nil if there are no errors", func(t *testing.T) { + result, err := expand(ctx, logger, "test", map[string]string{}, template.Data{}, nil, time.Now()) + require.NoError(t, err) + require.Len(t, result, 0) + }) + + t.Run("original is expanded with template data", func(t *testing.T) { + original := map[string]string{"Summary": `Instance {{ $labels.instance }} has been down for more than 5 minutes`} + expected := map[string]string{"Summary": "Instance host1 has been down for more than 5 minutes"} + data := template.Data{Labels: map[string]string{"instance": "host1"}} + results, err := expand(ctx, logger, "test", original, data, nil, time.Now()) + require.NoError(t, err) + require.Equal(t, expected, results) + }) + + t.Run("original is returned with an error", func(t *testing.T) { + original := map[string]string{ + "Summary": `Instance {{ $labels. }} has been down for more than 5 minutes`, + } + data := template.Data{Labels: map[string]string{"instance": "host1"}} + results, err := expand(ctx, logger, "test", original, data, nil, time.Now()) + require.NotNil(t, err) + require.Equal(t, original, results) + + // err should be an ExpandError that contains the template for the Summary and an error + var expandErr template.ExpandError + require.True(t, errors.As(err, &expandErr)) + require.EqualError(t, expandErr, "failed to expand template '{{- $labels := .Labels -}}{{- $values := .Values -}}{{- $value := .Value -}}Instance {{ $labels. }} has been down for more than 5 minutes': error parsing template __alert_test: template: __alert_test:1: unexpected <.> in operand") + }) + + t.Run("originals are returned with two errors", func(t *testing.T) { + original := map[string]string{ + "Summary": `Instance {{ $labels. }} has been down for more than 5 minutes`, + "Description": "The instance has been down for {{ $value minutes, please check the instance is online", + } + data := template.Data{Labels: map[string]string{"instance": "host1"}} + results, err := expand(ctx, logger, "test", original, data, nil, time.Now()) + require.NotNil(t, err) + require.Equal(t, original, results) + + // TODO: Please update this test in issue https://github.com/grafana/grafana/issues/63686 + var multierr *multierror.Error + require.True(t, errors.As(err, &multierr)) + require.Equal(t, multierr.Len(), 2) + + // assert each error matches the expected error + var expandErr1 template.ExpandError + require.True(t, errors.As(multierr.Errors[0], &expandErr1)) + require.EqualError(t, expandErr1, "failed to expand template '{{- $labels := .Labels -}}{{- $values := .Values -}}{{- $value := .Value -}}Instance {{ $labels. }} has been down for more than 5 minutes': error parsing template __alert_test: template: __alert_test:1: unexpected <.> in operand") + + var expandErr2 template.ExpandError + require.True(t, errors.As(multierr.Errors[1], &expandErr2)) + require.EqualError(t, expandErr2, "failed to expand template '{{- $labels := .Labels -}}{{- $values := .Values -}}{{- $value := .Value -}}The instance has been down for {{ $value minutes, please check the instance is online': error parsing template __alert_test: template: __alert_test:1: function \"minutes\" not defined") + }) + + t.Run("expanded and original is returned when there is one error", func(t *testing.T) { + original := map[string]string{ + "Summary": `Instance {{ $labels.instance }} has been down for more than 5 minutes`, + "Description": "The instance has been down for {{ $value minutes, please check the instance is online", + } + expected := map[string]string{ + "Summary": "Instance host1 has been down for more than 5 minutes", + "Description": "The instance has been down for {{ $value minutes, please check the instance is online", + } + data := template.Data{Labels: map[string]string{"instance": "host1"}} + results, err := expand(ctx, logger, "test", original, data, nil, time.Now()) + require.NotNil(t, err) + require.Equal(t, expected, results) + + // TODO: Please update this test in issue https://github.com/grafana/grafana/issues/63686 + var multierr *multierror.Error + require.True(t, errors.As(err, &multierr)) + require.Equal(t, multierr.Len(), 1) + + // assert each error matches the expected error + var expandErr template.ExpandError + require.True(t, errors.As(err, &expandErr)) + require.EqualError(t, expandErr, "failed to expand template '{{- $labels := .Labels -}}{{- $values := .Values -}}{{- $value := .Value -}}The instance has been down for {{ $value minutes, please check the instance is online': error parsing template __alert_test: template: __alert_test:1: function \"minutes\" not defined") + }) +} + func Test_getOrCreate(t *testing.T) { url := &url.URL{ Scheme: "http", diff --git a/pkg/services/ngalert/state/template/template.go b/pkg/services/ngalert/state/template/template.go index c9b7a0e14ac..86e2dad21d8 100644 --- a/pkg/services/ngalert/state/template/template.go +++ b/pkg/services/ngalert/state/template/template.go @@ -2,6 +2,7 @@ package template import ( "context" + "fmt" "math" "net/url" "sort" @@ -83,6 +84,17 @@ func NewData(labels map[string]string, res eval.Result) Data { } } +// ExpandError is an error containing the template and the error that occurred +// while expanding it. +type ExpandError struct { + Tmpl string + Err error +} + +func (e ExpandError) Error() string { + return fmt.Sprintf("failed to expand template '%s': %s", e.Tmpl, e.Err) +} + func Expand(ctx context.Context, name, tmpl string, data Data, externalURL *url.URL, evaluatedAt time.Time) (string, error) { // add __alert_ to avoid possible conflicts with other templates name = "__alert_" + name @@ -101,7 +113,7 @@ func Expand(ctx context.Context, name, tmpl string, data Data, externalURL *url. result, err := expander.Expand() if err != nil { - return "", err + return "", ExpandError{Tmpl: tmpl, Err: err} } // We need to replace with [no value] as some integrations think is invalid HTML. For example, diff --git a/pkg/services/ngalert/state/template/template_test.go b/pkg/services/ngalert/state/template/template_test.go index d0ae80abc32..11982b45505 100644 --- a/pkg/services/ngalert/state/template/template_test.go +++ b/pkg/services/ngalert/state/template/template_test.go @@ -66,6 +66,11 @@ func TestValueString(t *testing.T) { } } +func TestExpandError(t *testing.T) { + err := ExpandError{Tmpl: "{{", Err: errors.New("unexpected {{")} + assert.Equal(t, "failed to expand template '{{': unexpected {{", err.Error()) +} + func TestExpandTemplate(t *testing.T) { pathPrefix := "/path/prefix" externalURL, err := url.Parse("http://localhost" + pathPrefix) @@ -167,7 +172,7 @@ func TestExpandTemplate(t *testing.T) { alertInstance: eval.Result{ EvaluationString: "invalid", }, - expectedError: errors.New(`error executing template __alert_test: template: __alert_test:1:79: executing "__alert_test" at : error calling humanize: strconv.ParseFloat: parsing "invalid": invalid syntax`), + expectedError: errors.New(`failed to expand template '{{- $labels := .Labels -}}{{- $values := .Values -}}{{- $value := .Value -}}{{ humanize $value }}': error executing template __alert_test: template: __alert_test:1:79: executing "__alert_test" at : error calling humanize: strconv.ParseFloat: parsing "invalid": invalid syntax`), }, { name: "humanize1024 float64", text: "{{ range $key, $val := $values }}{{ humanize1024 .Value }}:{{ end }}", @@ -202,7 +207,7 @@ func TestExpandTemplate(t *testing.T) { alertInstance: eval.Result{ EvaluationString: "invalid", }, - expectedError: errors.New(`error executing template __alert_test: template: __alert_test:1:79: executing "__alert_test" at : error calling humanize1024: strconv.ParseFloat: parsing "invalid": invalid syntax`), + expectedError: errors.New(`failed to expand template '{{- $labels := .Labels -}}{{- $values := .Values -}}{{- $value := .Value -}}{{ humanize1024 $value }}': error executing template __alert_test: template: __alert_test:1:79: executing "__alert_test" at : error calling humanize1024: strconv.ParseFloat: parsing "invalid": invalid syntax`), }, { name: "humanizeDuration - seconds - float64", text: "{{ range $key, $val := $values }}{{ humanizeDuration .Value }}:{{ end }}", @@ -321,7 +326,7 @@ func TestExpandTemplate(t *testing.T) { alertInstance: eval.Result{ EvaluationString: "invalid", }, - expectedError: errors.New(`error executing template __alert_test: template: __alert_test:1:79: executing "__alert_test" at : error calling humanizeDuration: strconv.ParseFloat: parsing "invalid": invalid syntax`), + expectedError: errors.New(`failed to expand template '{{- $labels := .Labels -}}{{- $values := .Values -}}{{- $value := .Value -}}{{ humanizeDuration $value }}': error executing template __alert_test: template: __alert_test:1:79: executing "__alert_test" at : error calling humanizeDuration: strconv.ParseFloat: parsing "invalid": invalid syntax`), }, { name: "humanizePercentage - float64", text: "{{ -0.22222 | humanizePercentage }}:{{ 0.0 | humanizePercentage }}:{{ 0.1234567 | humanizePercentage }}:{{ 1.23456 | humanizePercentage }}", @@ -333,7 +338,7 @@ func TestExpandTemplate(t *testing.T) { }, { name: "humanizePercentage - string with error", text: `{{ "invalid" | humanizePercentage }}`, - expectedError: errors.New(`error executing template __alert_test: template: __alert_test:1:91: executing "__alert_test" at : error calling humanizePercentage: strconv.ParseFloat: parsing "invalid": invalid syntax`), + expectedError: errors.New(`failed to expand template '{{- $labels := .Labels -}}{{- $values := .Values -}}{{- $value := .Value -}}{{ "invalid" | humanizePercentage }}': error executing template __alert_test: template: __alert_test:1:91: executing "__alert_test" at : error calling humanizePercentage: strconv.ParseFloat: parsing "invalid": invalid syntax`), }, { name: "humanizeTimestamp - float64", text: "{{ 1435065584.128 | humanizeTimestamp }}", From 1cb39b35237a8ffd763a0d5c35029a229ad304ab Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Wed, 8 Mar 2023 13:31:50 +0100 Subject: [PATCH 059/288] Elasticsearch: Fix some of the tests duplicated from frontend (#64320) --- .../response_parser_frontend_test.go | 109 +++++------------- 1 file changed, 26 insertions(+), 83 deletions(-) diff --git a/pkg/tsdb/elasticsearch/response_parser_frontend_test.go b/pkg/tsdb/elasticsearch/response_parser_frontend_test.go index 9b831911471..45a772d310f 100644 --- a/pkg/tsdb/elasticsearch/response_parser_frontend_test.go +++ b/pkg/tsdb/elasticsearch/response_parser_frontend_test.go @@ -1,6 +1,7 @@ package elasticsearch import ( + "encoding/json" "fmt" "testing" "time" @@ -1176,24 +1177,23 @@ func TestRawDocumentQuery(t *testing.T) { require.NoError(t, err) require.Len(t, result.response.Responses, 1) - // FIXME: the whole raw_document format is not implemented currently - // frames := result.response.Responses["A"].Frames - // require.Len(t, frames, 1) - // fields := frames[0].Fields + frames := result.response.Responses["A"].Frames + require.Len(t, frames, 1) + fields := frames[0].Fields - // require.Len(t, fields, 1) - // f := fields[0] + require.Len(t, fields, 1) + f := fields[0] - // require.Equal(t, data.FieldTypeJSON, f.Type()) - // require.Equal(t, 2, f.Len()) + require.Equal(t, data.FieldTypeNullableJSON, f.Type()) + require.Equal(t, 2, f.Len()) - // v := f.At(0).(json.RawMessage) - // var jsonData map[string]interface{} - // err = json.Unmarshal(v, &jsonData) - // require.NoError(t, err) + v := f.At(0).(*json.RawMessage) + var jsonData map[string]interface{} + err = json.Unmarshal(*v, &jsonData) + require.NoError(t, err) - // require.Equal(t, "asd", jsonData["sourceProp"]) - // require.Equal(t, "field", jsonData["fieldProp"]) + require.Equal(t, "asd", jsonData["sourceProp"]) + require.Equal(t, "field", jsonData["fieldProp"]) } func TestBucketScript(t *testing.T) { @@ -1361,7 +1361,7 @@ func TestLogsAndCount(t *testing.T) { [ { "refId": "A", - "metrics": [{ "type": "count", "id": "1" }], + "metrics": [{ "type": "logs"}], "bucketAggs": [ { "type": "date_histogram", @@ -1379,14 +1379,7 @@ func TestLogsAndCount(t *testing.T) { { "responses": [ { - "aggregations": { - "2": { - "buckets": [ - { "doc_count": 10, "key": 1000 }, - { "doc_count": 15, "key": 2000 } - ] - } - }, + "aggregations": {}, "hits": { "hits": [ { @@ -1397,7 +1390,7 @@ func TestLogsAndCount(t *testing.T) { "@timestamp": "2019-06-24T09:51:19.765Z", "host": "djisaodjsoad", "number": 1, - "message": "hello, i am a message", + "line": "hello, i am a message", "level": "debug", "fields": { "lvl": "debug" } }, @@ -1415,7 +1408,7 @@ func TestLogsAndCount(t *testing.T) { "@timestamp": "2019-06-24T09:52:19.765Z", "host": "dsalkdakdop", "number": 2, - "message": "hello, i am also message", + "line": "hello, i am also message", "level": "error", "fields": { "lvl": "info" } }, @@ -1522,7 +1515,6 @@ func TestLogsAndCount(t *testing.T) { }) t.Run("level field", func(t *testing.T) { - // FIXME: config datasource with messageField=, levelField="level" result, err := queryDataTest(query, response) require.NoError(t, err) @@ -1536,33 +1528,11 @@ func TestLogsAndCount(t *testing.T) { fieldMap[field.Name] = field } - // require.Contains(t, fieldMap, "level") // FIXME - // field := fieldMap["level"] + require.Contains(t, fieldMap, "level") + field := fieldMap["level"] - // requireStringAt(t, "debug", field, 0) - // requireStringAt(t, "error", field, 1) - }) - - t.Run("level field remap", func(t *testing.T) { - // FIXME: config datasource with messageField=, levelField="fields.lvl" - result, err := queryDataTest(query, response) - require.NoError(t, err) - - require.Len(t, result.response.Responses, 1) - frames := result.response.Responses["A"].Frames - require.True(t, len(frames) > 0) - - requireFrameLength(t, frames[0], 2) - fieldMap := make(map[string]*data.Field) - for _, field := range frames[0].Fields { - fieldMap[field.Name] = field - } - - // require.Contains(t, fieldMap, "level") // FIXME - // field := fieldMap["level"] - - // requireStringAt(t, "debug", field, 0) - // requireStringAt(t, "info", field, 1) + requireStringAt(t, "debug", field, 0) + requireStringAt(t, "error", field, 1) }) } @@ -1572,13 +1542,7 @@ func TestLogsEmptyResponse(t *testing.T) { { "refId": "A", "metrics": [{ "type": "logs", "id": "2" }], - "bucketAggs": [ - { - "type": "date_histogram", - "settings": { "interval": "auto" }, - "id": "1" - } - ], + "bucketAggs": [], "key": "Q-1561369883389-0.7611823271062786-0", "query": "hello AND message" } @@ -1590,38 +1554,17 @@ func TestLogsEmptyResponse(t *testing.T) { "responses": [ { "hits": { "hits": [] }, - "aggregations": { - "1": { - "buckets": [ - { - "key_as_string": "1633676760000", - "key": 1633676760000, - "doc_count": 0 - }, - { - "key_as_string": "1633676770000", - "key": 1633676770000, - "doc_count": 0 - }, - { - "key_as_string": "1633676780000", - "key": 1633676780000, - "doc_count": 0 - } - ] - } - }, + "aggregations": {}, "status": 200 } ] } `) - // FIXME: config datasource with messageField="message", levelField="level" result, err := queryDataTest(query, response) require.NoError(t, err) require.Len(t, result.response.Responses, 1) - // frames := result.response.Responses["A"].Frames - // require.Len(t, frames, 2) // FIXME + frames := result.response.Responses["A"].Frames + require.Len(t, frames, 1) } From aa123e0d50a3c6ddaaa01e3f0872b9be29ff988e Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Wed, 8 Mar 2023 13:32:01 +0100 Subject: [PATCH 060/288] Elasticsearch: Fix where name of frame is set (#64233) * Elasticsearch: Use displayName field for naming * Change solution to frame.Name to be backward compatible * Fix snapshot tests * Use Time and Value for time and value fields * Use variables from grafana-plugin-sdk-go for name --- pkg/tsdb/elasticsearch/response_parser.go | 12 +- .../response_parser_frontend_test.go | 13 +- .../elasticsearch/response_parser_test.go | 178 +++++++++--------- .../testdata/trimedges_string.golden.jsonc | 14 +- .../metric_complex.a.golden.jsonc | 72 +++---- .../metric_multi.a.golden.jsonc | 14 +- .../metric_multi.b.golden.jsonc | 14 +- .../metric_simple.a.golden.jsonc | 36 ++-- 8 files changed, 158 insertions(+), 195 deletions(-) diff --git a/pkg/tsdb/elasticsearch/response_parser.go b/pkg/tsdb/elasticsearch/response_parser.go index a1952361c6a..a4798a4c703 100644 --- a/pkg/tsdb/elasticsearch/response_parser.go +++ b/pkg/tsdb/elasticsearch/response_parser.go @@ -82,7 +82,7 @@ func parseResponse(responses []*es.SearchResponse, targets []*Query, configuredF if err != nil { return &backend.QueryDataResponse{}, err } - nameFields(queryRes, target) + nameFrames(queryRes, target) trimDatapoints(queryRes, target) result.Responses[target.RefID] = queryRes @@ -381,8 +381,8 @@ func processBuckets(aggs map[string]interface{}, target *Query, func newTimeSeriesFrame(timeData []time.Time, tags map[string]string, values []*float64) *data.Frame { frame := data.NewFrame("", - data.NewField("time", nil, timeData), - data.NewField("value", tags, values)) + data.NewField(data.TimeSeriesTimeFieldName, nil, timeData), + data.NewField(data.TimeSeriesValueFieldName, tags, values)) frame.Meta = &data.FrameMeta{ Type: data.FrameTypeTimeSeriesMulti, } @@ -777,7 +777,7 @@ func getSortedLabelValues(labels data.Labels) []string { return values } -func nameFields(queryResult backend.DataResponse, target *Query) { +func nameFrames(queryResult backend.DataResponse, target *Query) { set := make(map[string]struct{}) frames := queryResult.Frames for _, v := range frames { @@ -796,9 +796,7 @@ func nameFields(queryResult backend.DataResponse, target *Query) { // another is "number" valueField := frame.Fields[1] fieldName := getFieldName(*valueField, target, metricTypeCount) - if fieldName != "" { - valueField.SetConfig(&data.FieldConfig{DisplayNameFromDS: fieldName}) - } + frame.Name = fieldName } } } diff --git a/pkg/tsdb/elasticsearch/response_parser_frontend_test.go b/pkg/tsdb/elasticsearch/response_parser_frontend_test.go index 45a772d310f..9ce5311bf3f 100644 --- a/pkg/tsdb/elasticsearch/response_parser_frontend_test.go +++ b/pkg/tsdb/elasticsearch/response_parser_frontend_test.go @@ -61,18 +61,7 @@ func requireFloatAt(t *testing.T, expected float64, field *data.Field, index int } func requireTimeSeriesName(t *testing.T, expected string, frame *data.Frame) { - getField := func() *data.Field { - for _, field := range frame.Fields { - if field.Type() != data.FieldTypeTime { - return field - } - } - return nil - } - - field := getField() - require.NotNil(t, expected, field.Config) - require.Equal(t, expected, field.Config.DisplayNameFromDS) + require.Equal(t, expected, frame.Name) } func TestRefIdMatching(t *testing.T) { diff --git a/pkg/tsdb/elasticsearch/response_parser_test.go b/pkg/tsdb/elasticsearch/response_parser_test.go index 94e3fefb7ea..9c71e55542a 100644 --- a/pkg/tsdb/elasticsearch/response_parser_test.go +++ b/pkg/tsdb/elasticsearch/response_parser_test.go @@ -59,11 +59,11 @@ func TestResponseParser(t *testing.T) { frame := dataframes[0] require.Len(t, frame.Fields, 2) - require.Equal(t, frame.Fields[0].Name, "time") + require.Equal(t, frame.Fields[0].Name, data.TimeSeriesTimeFieldName) require.Equal(t, frame.Fields[0].Len(), 2) - require.Equal(t, frame.Fields[1].Name, "value") + require.Equal(t, frame.Fields[1].Name, data.TimeSeriesValueFieldName) require.Equal(t, frame.Fields[1].Len(), 2) - assert.Equal(t, frame.Fields[1].Config.DisplayNameFromDS, "Count") + assert.Equal(t, frame.Name, "Count") }) t.Run("Simple query count & avg aggregation", func(t *testing.T) { @@ -108,20 +108,20 @@ func TestResponseParser(t *testing.T) { frame := dataframes[0] require.Len(t, frame.Fields, 2) - require.Equal(t, frame.Fields[0].Name, "time") + require.Equal(t, frame.Fields[0].Name, data.TimeSeriesTimeFieldName) require.Equal(t, frame.Fields[0].Len(), 2) - require.Equal(t, frame.Fields[1].Name, "value") + require.Equal(t, frame.Fields[1].Name, data.TimeSeriesValueFieldName) require.Equal(t, frame.Fields[1].Len(), 2) - assert.Equal(t, frame.Fields[1].Config.DisplayNameFromDS, "Count") + assert.Equal(t, frame.Name, "Count") frame = dataframes[1] require.Len(t, frame.Fields, 2) - require.Equal(t, frame.Fields[0].Name, "time") + require.Equal(t, frame.Fields[0].Name, data.TimeSeriesTimeFieldName) require.Equal(t, frame.Fields[0].Len(), 2) - require.Equal(t, frame.Fields[1].Name, "value") + require.Equal(t, frame.Fields[1].Name, data.TimeSeriesValueFieldName) require.Equal(t, frame.Fields[1].Len(), 2) - assert.Equal(t, frame.Fields[1].Config.DisplayNameFromDS, "Average value") + assert.Equal(t, frame.Name, "Average value") }) t.Run("Single group by query one metric", func(t *testing.T) { @@ -171,19 +171,19 @@ func TestResponseParser(t *testing.T) { frame := dataframes[0] require.Len(t, frame.Fields, 2) - require.Equal(t, frame.Fields[0].Name, "time") + require.Equal(t, frame.Fields[0].Name, data.TimeSeriesTimeFieldName) require.Equal(t, frame.Fields[0].Len(), 2) - require.Equal(t, frame.Fields[1].Name, "value") + require.Equal(t, frame.Fields[1].Name, data.TimeSeriesValueFieldName) require.Equal(t, frame.Fields[1].Len(), 2) - assert.Equal(t, frame.Fields[1].Config.DisplayNameFromDS, "server1") + assert.Equal(t, frame.Name, "server1") frame = dataframes[1] require.Len(t, frame.Fields, 2) - require.Equal(t, frame.Fields[0].Name, "time") + require.Equal(t, frame.Fields[0].Name, data.TimeSeriesTimeFieldName) require.Equal(t, frame.Fields[0].Len(), 2) - require.Equal(t, frame.Fields[1].Name, "value") + require.Equal(t, frame.Fields[1].Name, data.TimeSeriesValueFieldName) require.Equal(t, frame.Fields[1].Len(), 2) - assert.Equal(t, frame.Fields[1].Config.DisplayNameFromDS, "server2") + assert.Equal(t, frame.Name, "server2") }) t.Run("Single group by query two metrics", func(t *testing.T) { @@ -240,35 +240,35 @@ func TestResponseParser(t *testing.T) { frame := dataframes[0] require.Len(t, frame.Fields, 2) - require.Equal(t, frame.Fields[0].Name, "time") + require.Equal(t, frame.Fields[0].Name, data.TimeSeriesTimeFieldName) require.Equal(t, frame.Fields[0].Len(), 2) - require.Equal(t, frame.Fields[1].Name, "value") + require.Equal(t, frame.Fields[1].Name, data.TimeSeriesValueFieldName) require.Equal(t, frame.Fields[1].Len(), 2) - assert.Equal(t, frame.Fields[1].Config.DisplayNameFromDS, "server1 Count") + assert.Equal(t, frame.Name, "server1 Count") frame = dataframes[1] require.Len(t, frame.Fields, 2) - require.Equal(t, frame.Fields[0].Name, "time") + require.Equal(t, frame.Fields[0].Name, data.TimeSeriesTimeFieldName) require.Equal(t, frame.Fields[0].Len(), 2) - require.Equal(t, frame.Fields[1].Name, "value") + require.Equal(t, frame.Fields[1].Name, data.TimeSeriesValueFieldName) require.Equal(t, frame.Fields[1].Len(), 2) - assert.Equal(t, frame.Fields[1].Config.DisplayNameFromDS, "server1 Average @value") + assert.Equal(t, frame.Name, "server1 Average @value") frame = dataframes[2] require.Len(t, frame.Fields, 2) - require.Equal(t, frame.Fields[0].Name, "time") + require.Equal(t, frame.Fields[0].Name, data.TimeSeriesTimeFieldName) require.Equal(t, frame.Fields[0].Len(), 2) - require.Equal(t, frame.Fields[1].Name, "value") + require.Equal(t, frame.Fields[1].Name, data.TimeSeriesValueFieldName) require.Equal(t, frame.Fields[1].Len(), 2) - assert.Equal(t, frame.Fields[1].Config.DisplayNameFromDS, "server2 Count") + assert.Equal(t, frame.Name, "server2 Count") frame = dataframes[3] require.Len(t, frame.Fields, 2) - require.Equal(t, frame.Fields[0].Name, "time") + require.Equal(t, frame.Fields[0].Name, data.TimeSeriesTimeFieldName) require.Equal(t, frame.Fields[0].Len(), 2) - require.Equal(t, frame.Fields[1].Name, "value") + require.Equal(t, frame.Fields[1].Name, data.TimeSeriesValueFieldName) require.Equal(t, frame.Fields[1].Len(), 2) - assert.Equal(t, frame.Fields[1].Config.DisplayNameFromDS, "server2 Average @value") + assert.Equal(t, frame.Name, "server2 Average @value") }) t.Run("With percentiles", func(t *testing.T) { @@ -312,19 +312,19 @@ func TestResponseParser(t *testing.T) { frame := dataframes[0] require.Len(t, frame.Fields, 2) - require.Equal(t, frame.Fields[0].Name, "time") + require.Equal(t, frame.Fields[0].Name, data.TimeSeriesTimeFieldName) require.Equal(t, frame.Fields[0].Len(), 2) - require.Equal(t, frame.Fields[1].Name, "value") + require.Equal(t, frame.Fields[1].Name, data.TimeSeriesValueFieldName) require.Equal(t, frame.Fields[1].Len(), 2) - assert.Equal(t, frame.Fields[1].Config.DisplayNameFromDS, "p75") + assert.Equal(t, frame.Name, "p75") frame = dataframes[1] require.Len(t, frame.Fields, 2) - require.Equal(t, frame.Fields[0].Name, "time") + require.Equal(t, frame.Fields[0].Name, data.TimeSeriesTimeFieldName) require.Equal(t, frame.Fields[0].Len(), 2) - require.Equal(t, frame.Fields[1].Name, "value") + require.Equal(t, frame.Fields[1].Name, data.TimeSeriesValueFieldName) require.Equal(t, frame.Fields[1].Len(), 2) - assert.Equal(t, frame.Fields[1].Config.DisplayNameFromDS, "p90") + assert.Equal(t, frame.Name, "p90") }) t.Run("With extended stats", func(t *testing.T) { @@ -393,51 +393,51 @@ func TestResponseParser(t *testing.T) { frame := dataframes[0] require.Len(t, frame.Fields, 2) - require.Equal(t, frame.Fields[0].Name, "time") + require.Equal(t, frame.Fields[0].Name, data.TimeSeriesTimeFieldName) require.Equal(t, frame.Fields[0].Len(), 1) - require.Equal(t, frame.Fields[1].Name, "value") + require.Equal(t, frame.Fields[1].Name, data.TimeSeriesValueFieldName) require.Equal(t, frame.Fields[1].Len(), 1) - assert.Equal(t, frame.Fields[1].Config.DisplayNameFromDS, "server1 Max") + assert.Equal(t, frame.Name, "server1 Max") frame = dataframes[1] require.Len(t, frame.Fields, 2) - require.Equal(t, frame.Fields[0].Name, "time") + require.Equal(t, frame.Fields[0].Name, data.TimeSeriesTimeFieldName) require.Equal(t, frame.Fields[0].Len(), 1) - require.Equal(t, frame.Fields[1].Name, "value") + require.Equal(t, frame.Fields[1].Name, data.TimeSeriesValueFieldName) require.Equal(t, frame.Fields[1].Len(), 1) - assert.Equal(t, frame.Fields[1].Config.DisplayNameFromDS, "server1 Std Dev Lower") + assert.Equal(t, frame.Name, "server1 Std Dev Lower") frame = dataframes[2] require.Len(t, frame.Fields, 2) - require.Equal(t, frame.Fields[0].Name, "time") + require.Equal(t, frame.Fields[0].Name, data.TimeSeriesTimeFieldName) require.Equal(t, frame.Fields[0].Len(), 1) - require.Equal(t, frame.Fields[1].Name, "value") + require.Equal(t, frame.Fields[1].Name, data.TimeSeriesValueFieldName) require.Equal(t, frame.Fields[1].Len(), 1) - assert.Equal(t, frame.Fields[1].Config.DisplayNameFromDS, "server1 Std Dev Upper") + assert.Equal(t, frame.Name, "server1 Std Dev Upper") frame = dataframes[3] require.Len(t, frame.Fields, 2) - require.Equal(t, frame.Fields[0].Name, "time") + require.Equal(t, frame.Fields[0].Name, data.TimeSeriesTimeFieldName) require.Equal(t, frame.Fields[0].Len(), 1) - require.Equal(t, frame.Fields[1].Name, "value") + require.Equal(t, frame.Fields[1].Name, data.TimeSeriesValueFieldName) require.Equal(t, frame.Fields[1].Len(), 1) - assert.Equal(t, frame.Fields[1].Config.DisplayNameFromDS, "server2 Max") + assert.Equal(t, frame.Name, "server2 Max") frame = dataframes[4] require.Len(t, frame.Fields, 2) - require.Equal(t, frame.Fields[0].Name, "time") + require.Equal(t, frame.Fields[0].Name, data.TimeSeriesTimeFieldName) require.Equal(t, frame.Fields[0].Len(), 1) - require.Equal(t, frame.Fields[1].Name, "value") + require.Equal(t, frame.Fields[1].Name, data.TimeSeriesValueFieldName) require.Equal(t, frame.Fields[1].Len(), 1) - assert.Equal(t, frame.Fields[1].Config.DisplayNameFromDS, "server2 Std Dev Lower") + assert.Equal(t, frame.Name, "server2 Std Dev Lower") frame = dataframes[5] require.Len(t, frame.Fields, 2) - require.Equal(t, frame.Fields[0].Name, "time") + require.Equal(t, frame.Fields[0].Name, data.TimeSeriesTimeFieldName) require.Equal(t, frame.Fields[0].Len(), 1) - require.Equal(t, frame.Fields[1].Name, "value") + require.Equal(t, frame.Fields[1].Name, data.TimeSeriesValueFieldName) require.Equal(t, frame.Fields[1].Len(), 1) - assert.Equal(t, frame.Fields[1].Config.DisplayNameFromDS, "server2 Std Dev Upper") + assert.Equal(t, frame.Name, "server2 Std Dev Upper") }) t.Run("Single group by with alias pattern", func(t *testing.T) { @@ -496,27 +496,27 @@ func TestResponseParser(t *testing.T) { frame := dataframes[0] require.Len(t, frame.Fields, 2) - require.Equal(t, frame.Fields[0].Name, "time") + require.Equal(t, frame.Fields[0].Name, data.TimeSeriesTimeFieldName) require.Equal(t, frame.Fields[0].Len(), 2) - require.Equal(t, frame.Fields[1].Name, "value") + require.Equal(t, frame.Fields[1].Name, data.TimeSeriesValueFieldName) require.Equal(t, frame.Fields[1].Len(), 2) - assert.Equal(t, frame.Fields[1].Config.DisplayNameFromDS, "server1 Count and {{not_exist}} server1") + assert.Equal(t, frame.Name, "server1 Count and {{not_exist}} server1") frame = dataframes[1] require.Len(t, frame.Fields, 2) - require.Equal(t, frame.Fields[0].Name, "time") + require.Equal(t, frame.Fields[0].Name, data.TimeSeriesTimeFieldName) require.Equal(t, frame.Fields[0].Len(), 2) - require.Equal(t, frame.Fields[1].Name, "value") + require.Equal(t, frame.Fields[1].Name, data.TimeSeriesValueFieldName) require.Equal(t, frame.Fields[1].Len(), 2) - assert.Equal(t, frame.Fields[1].Config.DisplayNameFromDS, "server2 Count and {{not_exist}} server2") + assert.Equal(t, frame.Name, "server2 Count and {{not_exist}} server2") frame = dataframes[2] require.Len(t, frame.Fields, 2) - require.Equal(t, frame.Fields[0].Name, "time") + require.Equal(t, frame.Fields[0].Name, data.TimeSeriesTimeFieldName) require.Equal(t, frame.Fields[0].Len(), 2) - require.Equal(t, frame.Fields[1].Name, "value") + require.Equal(t, frame.Fields[1].Name, data.TimeSeriesValueFieldName) require.Equal(t, frame.Fields[1].Len(), 2) - assert.Equal(t, frame.Fields[1].Config.DisplayNameFromDS, "0 Count and {{not_exist}} 0") + assert.Equal(t, frame.Name, "0 Count and {{not_exist}} 0") }) t.Run("Histogram response", func(t *testing.T) { @@ -598,19 +598,19 @@ func TestResponseParser(t *testing.T) { frame := dataframes[0] require.Len(t, frame.Fields, 2) - require.Equal(t, frame.Fields[0].Name, "time") + require.Equal(t, frame.Fields[0].Name, data.TimeSeriesTimeFieldName) require.Equal(t, frame.Fields[0].Len(), 2) - require.Equal(t, frame.Fields[1].Name, "value") + require.Equal(t, frame.Fields[1].Name, data.TimeSeriesValueFieldName) require.Equal(t, frame.Fields[1].Len(), 2) - assert.Equal(t, frame.Fields[1].Config.DisplayNameFromDS, "@metric:cpu") + assert.Equal(t, frame.Name, "@metric:cpu") frame = dataframes[1] require.Len(t, frame.Fields, 2) - require.Equal(t, frame.Fields[0].Name, "time") + require.Equal(t, frame.Fields[0].Name, data.TimeSeriesTimeFieldName) require.Equal(t, frame.Fields[0].Len(), 2) - require.Equal(t, frame.Fields[1].Name, "value") + require.Equal(t, frame.Fields[1].Name, data.TimeSeriesValueFieldName) require.Equal(t, frame.Fields[1].Len(), 2) - assert.Equal(t, frame.Fields[1].Config.DisplayNameFromDS, "@metric:logins.count") + assert.Equal(t, frame.Name, "@metric:logins.count") }) t.Run("With drop first and last aggregation (numeric)", func(t *testing.T) { @@ -666,19 +666,19 @@ func TestResponseParser(t *testing.T) { frame := dataframes[0] require.Len(t, frame.Fields, 2) - require.Equal(t, frame.Fields[0].Name, "time") + require.Equal(t, frame.Fields[0].Name, data.TimeSeriesTimeFieldName) require.Equal(t, frame.Fields[0].Len(), 1) - require.Equal(t, frame.Fields[1].Name, "value") + require.Equal(t, frame.Fields[1].Name, data.TimeSeriesValueFieldName) require.Equal(t, frame.Fields[1].Len(), 1) - assert.Equal(t, frame.Fields[1].Config.DisplayNameFromDS, "Average") + assert.Equal(t, frame.Name, "Average") frame = dataframes[1] require.Len(t, frame.Fields, 2) - require.Equal(t, frame.Fields[0].Name, "time") + require.Equal(t, frame.Fields[0].Name, data.TimeSeriesTimeFieldName) require.Equal(t, frame.Fields[0].Len(), 1) - require.Equal(t, frame.Fields[1].Name, "value") + require.Equal(t, frame.Fields[1].Name, data.TimeSeriesValueFieldName) require.Equal(t, frame.Fields[1].Len(), 1) - assert.Equal(t, frame.Fields[1].Config.DisplayNameFromDS, "Count") + assert.Equal(t, frame.Name, "Count") }) t.Run("With drop first and last aggregation (string)", func(t *testing.T) { @@ -734,19 +734,19 @@ func TestResponseParser(t *testing.T) { frame := dataframes[0] require.Len(t, frame.Fields, 2) - require.Equal(t, frame.Fields[0].Name, "time") + require.Equal(t, frame.Fields[0].Name, data.TimeSeriesTimeFieldName) require.Equal(t, frame.Fields[0].Len(), 1) - require.Equal(t, frame.Fields[1].Name, "value") + require.Equal(t, frame.Fields[1].Name, data.TimeSeriesValueFieldName) require.Equal(t, frame.Fields[1].Len(), 1) - assert.Equal(t, frame.Fields[1].Config.DisplayNameFromDS, "Average") + assert.Equal(t, frame.Name, "Average") frame = dataframes[1] require.Len(t, frame.Fields, 2) - require.Equal(t, frame.Fields[0].Name, "time") + require.Equal(t, frame.Fields[0].Name, data.TimeSeriesTimeFieldName) require.Equal(t, frame.Fields[0].Len(), 1) - require.Equal(t, frame.Fields[1].Name, "value") + require.Equal(t, frame.Fields[1].Name, data.TimeSeriesValueFieldName) require.Equal(t, frame.Fields[1].Len(), 1) - assert.Equal(t, frame.Fields[1].Config.DisplayNameFromDS, "Count") + assert.Equal(t, frame.Name, "Count") }) t.Run("Larger trimEdges value", func(t *testing.T) { @@ -945,27 +945,27 @@ func TestResponseParser(t *testing.T) { frame := dataframes[0] require.Len(t, frame.Fields, 2) - require.Equal(t, frame.Fields[0].Name, "time") + require.Equal(t, frame.Fields[0].Name, data.TimeSeriesTimeFieldName) require.Equal(t, frame.Fields[0].Len(), 2) - require.Equal(t, frame.Fields[1].Name, "value") + require.Equal(t, frame.Fields[1].Name, data.TimeSeriesValueFieldName) require.Equal(t, frame.Fields[1].Len(), 2) - assert.Equal(t, frame.Fields[1].Config.DisplayNameFromDS, "Sum @value") + assert.Equal(t, frame.Name, "Sum @value") frame = dataframes[1] require.Len(t, frame.Fields, 2) - require.Equal(t, frame.Fields[0].Name, "time") + require.Equal(t, frame.Fields[0].Name, data.TimeSeriesTimeFieldName) require.Equal(t, frame.Fields[0].Len(), 2) - require.Equal(t, frame.Fields[1].Name, "value") + require.Equal(t, frame.Fields[1].Name, data.TimeSeriesValueFieldName) require.Equal(t, frame.Fields[1].Len(), 2) - assert.Equal(t, frame.Fields[1].Config.DisplayNameFromDS, "Max @value") + assert.Equal(t, frame.Name, "Max @value") frame = dataframes[2] require.Len(t, frame.Fields, 2) - require.Equal(t, frame.Fields[0].Name, "time") + require.Equal(t, frame.Fields[0].Name, data.TimeSeriesTimeFieldName) require.Equal(t, frame.Fields[0].Len(), 2) - require.Equal(t, frame.Fields[1].Name, "value") + require.Equal(t, frame.Fields[1].Name, data.TimeSeriesValueFieldName) require.Equal(t, frame.Fields[1].Len(), 2) - assert.Equal(t, frame.Fields[1].Config.DisplayNameFromDS, "Sum @value * Max @value") + assert.Equal(t, frame.Name, "Sum @value * Max @value") }) t.Run("Terms with two bucket_script", func(t *testing.T) { @@ -1472,7 +1472,7 @@ func TestResponseParser(t *testing.T) { assert.Len(t, frame.Fields, 2) require.Equal(t, frame.Fields[0].Len(), 2) require.Equal(t, frame.Fields[1].Len(), 2) - assert.Equal(t, frame.Fields[1].Config.DisplayNameFromDS, "Top Metrics @value") + assert.Equal(t, frame.Name, "Top Metrics @value") v, _ := frame.FloatAt(0, 0) assert.Equal(t, 1609459200000., v) v, _ = frame.FloatAt(1, 0) @@ -1489,7 +1489,7 @@ func TestResponseParser(t *testing.T) { assert.Len(t, frame.Fields, 2) require.Equal(t, frame.Fields[0].Len(), 2) require.Equal(t, frame.Fields[1].Len(), 2) - assert.Equal(t, frame.Fields[1].Config.DisplayNameFromDS, "Top Metrics @anotherValue") + assert.Equal(t, frame.Name, "Top Metrics @anotherValue") v, _ = frame.FloatAt(0, 0) assert.Equal(t, 1609459200000., v) v, _ = frame.FloatAt(1, 0) diff --git a/pkg/tsdb/elasticsearch/testdata/trimedges_string.golden.jsonc b/pkg/tsdb/elasticsearch/testdata/trimedges_string.golden.jsonc index 007783dfb27..59b2f6fae2b 100644 --- a/pkg/tsdb/elasticsearch/testdata/trimedges_string.golden.jsonc +++ b/pkg/tsdb/elasticsearch/testdata/trimedges_string.golden.jsonc @@ -7,10 +7,10 @@ // 0 // ] // } -// Name: +// Name: Count // Dimensions: 2 Fields by 3 Rows // +-------------------------------+------------------+ -// | Name: time | Name: value | +// | Name: Time | Name: Value | // | Labels: | Labels: | // | Type: []time.Time | Type: []*float64 | // +-------------------------------+------------------+ @@ -26,6 +26,7 @@ "frames": [ { "schema": { + "name": "Count", "meta": { "type": "timeseries-multi", "typeVersion": [ @@ -35,23 +36,20 @@ }, "fields": [ { - "name": "time", + "name": "Time", "type": "time", "typeInfo": { "frame": "time.Time" } }, { - "name": "value", + "name": "Value", "type": "number", "typeInfo": { "frame": "float64", "nullable": true }, - "labels": {}, - "config": { - "displayNameFromDS": "Count" - } + "labels": {} } ] }, diff --git a/pkg/tsdb/elasticsearch/testdata_response/metric_complex.a.golden.jsonc b/pkg/tsdb/elasticsearch/testdata_response/metric_complex.a.golden.jsonc index 9b4728a85d6..44961dd47d8 100644 --- a/pkg/tsdb/elasticsearch/testdata_response/metric_complex.a.golden.jsonc +++ b/pkg/tsdb/elasticsearch/testdata_response/metric_complex.a.golden.jsonc @@ -7,10 +7,10 @@ // 0 // ] // } -// Name: +// Name: val3 Max float // Dimensions: 2 Fields by 3 Rows // +-------------------------------+--------------------+ -// | Name: time | Name: value | +// | Name: Time | Name: Value | // | Labels: | Labels: label=val3 | // | Type: []time.Time | Type: []*float64 | // +-------------------------------+--------------------+ @@ -28,10 +28,10 @@ // 0 // ] // } -// Name: +// Name: val3 Min float // Dimensions: 2 Fields by 3 Rows // +-------------------------------+--------------------+ -// | Name: time | Name: value | +// | Name: Time | Name: Value | // | Labels: | Labels: label=val3 | // | Type: []time.Time | Type: []*float64 | // +-------------------------------+--------------------+ @@ -49,10 +49,10 @@ // 0 // ] // } -// Name: +// Name: val2 Max float // Dimensions: 2 Fields by 3 Rows // +-------------------------------+--------------------+ -// | Name: time | Name: value | +// | Name: Time | Name: Value | // | Labels: | Labels: label=val2 | // | Type: []time.Time | Type: []*float64 | // +-------------------------------+--------------------+ @@ -70,10 +70,10 @@ // 0 // ] // } -// Name: +// Name: val2 Min float // Dimensions: 2 Fields by 3 Rows // +-------------------------------+--------------------+ -// | Name: time | Name: value | +// | Name: Time | Name: Value | // | Labels: | Labels: label=val2 | // | Type: []time.Time | Type: []*float64 | // +-------------------------------+--------------------+ @@ -91,10 +91,10 @@ // 0 // ] // } -// Name: +// Name: val1 Max float // Dimensions: 2 Fields by 3 Rows // +-------------------------------+--------------------+ -// | Name: time | Name: value | +// | Name: Time | Name: Value | // | Labels: | Labels: label=val1 | // | Type: []time.Time | Type: []*float64 | // +-------------------------------+--------------------+ @@ -112,10 +112,10 @@ // 0 // ] // } -// Name: +// Name: val1 Min float // Dimensions: 2 Fields by 3 Rows // +-------------------------------+--------------------+ -// | Name: time | Name: value | +// | Name: Time | Name: Value | // | Labels: | Labels: label=val1 | // | Type: []time.Time | Type: []*float64 | // +-------------------------------+--------------------+ @@ -131,6 +131,7 @@ "frames": [ { "schema": { + "name": "val3 Max float", "meta": { "type": "timeseries-multi", "typeVersion": [ @@ -140,14 +141,14 @@ }, "fields": [ { - "name": "time", + "name": "Time", "type": "time", "typeInfo": { "frame": "time.Time" } }, { - "name": "value", + "name": "Value", "type": "number", "typeInfo": { "frame": "float64", @@ -155,9 +156,6 @@ }, "labels": { "label": "val3" - }, - "config": { - "displayNameFromDS": "val3 Max float" } } ] @@ -179,6 +177,7 @@ }, { "schema": { + "name": "val3 Min float", "meta": { "type": "timeseries-multi", "typeVersion": [ @@ -188,14 +187,14 @@ }, "fields": [ { - "name": "time", + "name": "Time", "type": "time", "typeInfo": { "frame": "time.Time" } }, { - "name": "value", + "name": "Value", "type": "number", "typeInfo": { "frame": "float64", @@ -203,9 +202,6 @@ }, "labels": { "label": "val3" - }, - "config": { - "displayNameFromDS": "val3 Min float" } } ] @@ -227,6 +223,7 @@ }, { "schema": { + "name": "val2 Max float", "meta": { "type": "timeseries-multi", "typeVersion": [ @@ -236,14 +233,14 @@ }, "fields": [ { - "name": "time", + "name": "Time", "type": "time", "typeInfo": { "frame": "time.Time" } }, { - "name": "value", + "name": "Value", "type": "number", "typeInfo": { "frame": "float64", @@ -251,9 +248,6 @@ }, "labels": { "label": "val2" - }, - "config": { - "displayNameFromDS": "val2 Max float" } } ] @@ -275,6 +269,7 @@ }, { "schema": { + "name": "val2 Min float", "meta": { "type": "timeseries-multi", "typeVersion": [ @@ -284,14 +279,14 @@ }, "fields": [ { - "name": "time", + "name": "Time", "type": "time", "typeInfo": { "frame": "time.Time" } }, { - "name": "value", + "name": "Value", "type": "number", "typeInfo": { "frame": "float64", @@ -299,9 +294,6 @@ }, "labels": { "label": "val2" - }, - "config": { - "displayNameFromDS": "val2 Min float" } } ] @@ -323,6 +315,7 @@ }, { "schema": { + "name": "val1 Max float", "meta": { "type": "timeseries-multi", "typeVersion": [ @@ -332,14 +325,14 @@ }, "fields": [ { - "name": "time", + "name": "Time", "type": "time", "typeInfo": { "frame": "time.Time" } }, { - "name": "value", + "name": "Value", "type": "number", "typeInfo": { "frame": "float64", @@ -347,9 +340,6 @@ }, "labels": { "label": "val1" - }, - "config": { - "displayNameFromDS": "val1 Max float" } } ] @@ -371,6 +361,7 @@ }, { "schema": { + "name": "val1 Min float", "meta": { "type": "timeseries-multi", "typeVersion": [ @@ -380,14 +371,14 @@ }, "fields": [ { - "name": "time", + "name": "Time", "type": "time", "typeInfo": { "frame": "time.Time" } }, { - "name": "value", + "name": "Value", "type": "number", "typeInfo": { "frame": "float64", @@ -395,9 +386,6 @@ }, "labels": { "label": "val1" - }, - "config": { - "displayNameFromDS": "val1 Min float" } } ] diff --git a/pkg/tsdb/elasticsearch/testdata_response/metric_multi.a.golden.jsonc b/pkg/tsdb/elasticsearch/testdata_response/metric_multi.a.golden.jsonc index 24e51dd12a9..422064808fe 100644 --- a/pkg/tsdb/elasticsearch/testdata_response/metric_multi.a.golden.jsonc +++ b/pkg/tsdb/elasticsearch/testdata_response/metric_multi.a.golden.jsonc @@ -7,10 +7,10 @@ // 0 // ] // } -// Name: +// Name: Max float // Dimensions: 2 Fields by 3 Rows // +-------------------------------+-------------------+ -// | Name: time | Name: value | +// | Name: Time | Name: Value | // | Labels: | Labels: | // | Type: []time.Time | Type: []*float64 | // +-------------------------------+-------------------+ @@ -26,6 +26,7 @@ "frames": [ { "schema": { + "name": "Max float", "meta": { "type": "timeseries-multi", "typeVersion": [ @@ -35,23 +36,20 @@ }, "fields": [ { - "name": "time", + "name": "Time", "type": "time", "typeInfo": { "frame": "time.Time" } }, { - "name": "value", + "name": "Value", "type": "number", "typeInfo": { "frame": "float64", "nullable": true }, - "labels": {}, - "config": { - "displayNameFromDS": "Max float" - } + "labels": {} } ] }, diff --git a/pkg/tsdb/elasticsearch/testdata_response/metric_multi.b.golden.jsonc b/pkg/tsdb/elasticsearch/testdata_response/metric_multi.b.golden.jsonc index 41fa7ac20b2..83eac404c90 100644 --- a/pkg/tsdb/elasticsearch/testdata_response/metric_multi.b.golden.jsonc +++ b/pkg/tsdb/elasticsearch/testdata_response/metric_multi.b.golden.jsonc @@ -7,10 +7,10 @@ // 0 // ] // } -// Name: +// Name: Min float // Dimensions: 2 Fields by 3 Rows // +-------------------------------+---------------------+ -// | Name: time | Name: value | +// | Name: Time | Name: Value | // | Labels: | Labels: | // | Type: []time.Time | Type: []*float64 | // +-------------------------------+---------------------+ @@ -26,6 +26,7 @@ "frames": [ { "schema": { + "name": "Min float", "meta": { "type": "timeseries-multi", "typeVersion": [ @@ -35,23 +36,20 @@ }, "fields": [ { - "name": "time", + "name": "Time", "type": "time", "typeInfo": { "frame": "time.Time" } }, { - "name": "value", + "name": "Value", "type": "number", "typeInfo": { "frame": "float64", "nullable": true }, - "labels": {}, - "config": { - "displayNameFromDS": "Min float" - } + "labels": {} } ] }, diff --git a/pkg/tsdb/elasticsearch/testdata_response/metric_simple.a.golden.jsonc b/pkg/tsdb/elasticsearch/testdata_response/metric_simple.a.golden.jsonc index e83d1b9c1d7..a264263c2b5 100644 --- a/pkg/tsdb/elasticsearch/testdata_response/metric_simple.a.golden.jsonc +++ b/pkg/tsdb/elasticsearch/testdata_response/metric_simple.a.golden.jsonc @@ -7,10 +7,10 @@ // 0 // ] // } -// Name: +// Name: val3 // Dimensions: 2 Fields by 4 Rows // +-------------------------------+--------------------+ -// | Name: time | Name: value | +// | Name: Time | Name: Value | // | Labels: | Labels: label=val3 | // | Type: []time.Time | Type: []*float64 | // +-------------------------------+--------------------+ @@ -29,10 +29,10 @@ // 0 // ] // } -// Name: +// Name: val2 // Dimensions: 2 Fields by 4 Rows // +-------------------------------+--------------------+ -// | Name: time | Name: value | +// | Name: Time | Name: Value | // | Labels: | Labels: label=val2 | // | Type: []time.Time | Type: []*float64 | // +-------------------------------+--------------------+ @@ -51,10 +51,10 @@ // 0 // ] // } -// Name: +// Name: val1 // Dimensions: 2 Fields by 4 Rows // +-------------------------------+--------------------+ -// | Name: time | Name: value | +// | Name: Time | Name: Value | // | Labels: | Labels: label=val1 | // | Type: []time.Time | Type: []*float64 | // +-------------------------------+--------------------+ @@ -71,6 +71,7 @@ "frames": [ { "schema": { + "name": "val3", "meta": { "type": "timeseries-multi", "typeVersion": [ @@ -80,14 +81,14 @@ }, "fields": [ { - "name": "time", + "name": "Time", "type": "time", "typeInfo": { "frame": "time.Time" } }, { - "name": "value", + "name": "Value", "type": "number", "typeInfo": { "frame": "float64", @@ -95,9 +96,6 @@ }, "labels": { "label": "val3" - }, - "config": { - "displayNameFromDS": "val3" } } ] @@ -121,6 +119,7 @@ }, { "schema": { + "name": "val2", "meta": { "type": "timeseries-multi", "typeVersion": [ @@ -130,14 +129,14 @@ }, "fields": [ { - "name": "time", + "name": "Time", "type": "time", "typeInfo": { "frame": "time.Time" } }, { - "name": "value", + "name": "Value", "type": "number", "typeInfo": { "frame": "float64", @@ -145,9 +144,6 @@ }, "labels": { "label": "val2" - }, - "config": { - "displayNameFromDS": "val2" } } ] @@ -171,6 +167,7 @@ }, { "schema": { + "name": "val1", "meta": { "type": "timeseries-multi", "typeVersion": [ @@ -180,14 +177,14 @@ }, "fields": [ { - "name": "time", + "name": "Time", "type": "time", "typeInfo": { "frame": "time.Time" } }, { - "name": "value", + "name": "Value", "type": "number", "typeInfo": { "frame": "float64", @@ -195,9 +192,6 @@ }, "labels": { "label": "val1" - }, - "config": { - "displayNameFromDS": "val1" } } ] From 6543259a7dd4954da581ec00ac47ab0e6555df52 Mon Sep 17 00:00:00 2001 From: Misi Date: Wed, 8 Mar 2023 13:35:54 +0100 Subject: [PATCH 061/288] Auth: Add SyncPermissions post auth hook (#64205) * Add SyncPermissionsFromDB post auth hook * Delete FromDB prefix * Align tests * Fixes * Change SyncPermissionsHook prio --- pkg/api/http_server.go | 4 +- pkg/services/authn/authn.go | 6 ++ pkg/services/authn/authnimpl/service.go | 1 + .../authn/authnimpl/sync/permission_sync.go | 45 +++++++++++ .../authnimpl/sync/permission_sync_test.go | 81 +++++++++++++++++++ pkg/services/authn/clients/anonymous.go | 2 +- pkg/services/authn/clients/api_key.go | 9 ++- pkg/services/authn/clients/api_key_test.go | 6 ++ pkg/services/authn/clients/grafana.go | 2 +- pkg/services/authn/clients/grafana_test.go | 7 +- pkg/services/authn/clients/jwt.go | 1 + pkg/services/authn/clients/jwt_test.go | 1 + pkg/services/authn/clients/ldap.go | 1 + pkg/services/authn/clients/ldap_test.go | 2 + pkg/services/authn/clients/oauth.go | 1 + pkg/services/authn/clients/proxy.go | 2 +- pkg/services/authn/clients/render.go | 9 ++- pkg/services/authn/clients/render_test.go | 10 ++- pkg/services/authn/clients/session.go | 2 +- pkg/services/authn/clients/session_test.go | 3 + 20 files changed, 177 insertions(+), 18 deletions(-) create mode 100644 pkg/services/authn/authnimpl/sync/permission_sync.go create mode 100644 pkg/services/authn/authnimpl/sync/permission_sync_test.go diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index a24a592e72d..17e72b61182 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -625,7 +625,9 @@ func (hs *HTTPServer) addMiddlewaresAndStaticRoutes() { m.UseMiddleware(hs.ContextHandler.Middleware) m.Use(middleware.OrgRedirect(hs.Cfg, hs.userService)) - m.Use(accesscontrol.LoadPermissionsMiddleware(hs.accesscontrolService)) + if !hs.Features.IsEnabled(featuremgmt.FlagAuthnService) { + m.Use(accesscontrol.LoadPermissionsMiddleware(hs.accesscontrolService)) + } // needs to be after context handler if hs.Cfg.EnforceDomain { diff --git a/pkg/services/authn/authn.go b/pkg/services/authn/authn.go index 0d092d2f583..63eac98ee18 100644 --- a/pkg/services/authn/authn.go +++ b/pkg/services/authn/authn.go @@ -57,6 +57,8 @@ type ClientParams struct { CacheAuthProxyKey string // LookUpParams are the arguments used to look up the entity in the DB. LookUpParams login.UserLookupParams + // SyncPermissions ensure that permissions are loaded from DB and added to the identity + SyncPermissions bool } type PostAuthHookFn func(ctx context.Context, identity *Identity, r *Request) error @@ -221,6 +223,8 @@ type Identity struct { // ClientParams are hints for the auth service on how to handle the identity. // Set by the authenticating client. ClientParams ClientParams + // Permissions is the list of permissions the entity has. + Permissions map[int64]map[string][]string } // Role returns the role of the identity in the active organization. @@ -273,6 +277,7 @@ func (i *Identity) SignedInUser() *user.SignedInUser { HelpFlags1: i.HelpFlags1, LastSeenAt: i.LastSeenAt, Teams: i.Teams, + Permissions: i.Permissions, } namespace, id := i.NamespacedID() @@ -320,6 +325,7 @@ func IdentityFromSignedInUser(id string, usr *user.SignedInUser, params ClientPa LastSeenAt: usr.LastSeenAt, Teams: usr.Teams, ClientParams: params, + Permissions: usr.Permissions, } } diff --git a/pkg/services/authn/authnimpl/service.go b/pkg/services/authn/authnimpl/service.go index a591b02f3fd..6761db3887a 100644 --- a/pkg/services/authn/authnimpl/service.go +++ b/pkg/services/authn/authnimpl/service.go @@ -155,6 +155,7 @@ func ProvideService( } s.RegisterPostAuthHook(userSyncService.FetchSyncedUserHook, 100) + s.RegisterPostAuthHook(sync.ProvidePermissionsSync(accessControlService).SyncPermissionsHook, 110) return s } diff --git a/pkg/services/authn/authnimpl/sync/permission_sync.go b/pkg/services/authn/authnimpl/sync/permission_sync.go new file mode 100644 index 00000000000..06b20270c52 --- /dev/null +++ b/pkg/services/authn/authnimpl/sync/permission_sync.go @@ -0,0 +1,45 @@ +package sync + +import ( + "context" + + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/authn" + "github.com/grafana/grafana/pkg/util/errutil" +) + +var ( + errSyncPermissionsForbidden = errutil.NewBase(errutil.StatusForbidden, "permissions.sync.forbidden") +) + +func ProvidePermissionsSync(acService accesscontrol.Service) *PermissionsSync { + return &PermissionsSync{ + ac: acService, + log: log.New("permissions.sync"), + } +} + +type PermissionsSync struct { + ac accesscontrol.Service + log log.Logger +} + +func (s *PermissionsSync) SyncPermissionsHook(ctx context.Context, identity *authn.Identity, _ *authn.Request) error { + if s.ac.IsDisabled() || !identity.ClientParams.SyncPermissions { + return nil + } + + permissions, err := s.ac.GetUserPermissions(ctx, identity.SignedInUser(), + accesscontrol.Options{ReloadCache: false}) + if err != nil { + s.log.FromContext(ctx).Error("failed to fetch permissions from db", "error", err, "user_id", identity.ID) + return errSyncPermissionsForbidden + } + + if identity.Permissions == nil { + identity.Permissions = make(map[int64]map[string][]string) + } + identity.Permissions[identity.OrgID] = accesscontrol.GroupScopesByAction(permissions) + return nil +} diff --git a/pkg/services/authn/authnimpl/sync/permission_sync_test.go b/pkg/services/authn/authnimpl/sync/permission_sync_test.go new file mode 100644 index 00000000000..c9744ebd8ff --- /dev/null +++ b/pkg/services/authn/authnimpl/sync/permission_sync_test.go @@ -0,0 +1,81 @@ +package sync + +import ( + "context" + "testing" + + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/accesscontrol" + acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" + "github.com/grafana/grafana/pkg/services/authn" + "github.com/grafana/grafana/pkg/services/user" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestPermissionsSync_SyncPermission(t *testing.T) { + type testCase struct { + name string + identity *authn.Identity + rbacDisabled bool + expectedPermissions []accesscontrol.Permission + } + testCases := []testCase{ + { + name: "enriches the identity successfully when SyncPermissions is true", + identity: &authn.Identity{ID: "user:2", OrgID: 1, ClientParams: authn.ClientParams{SyncPermissions: true}}, + rbacDisabled: false, + expectedPermissions: []accesscontrol.Permission{ + {Action: accesscontrol.ActionUsersRead}, + }, + }, + { + name: "does not load the permissions when SyncPermissions is false", + identity: &authn.Identity{ID: "user:2", OrgID: 1, ClientParams: authn.ClientParams{SyncPermissions: true}}, + rbacDisabled: false, + expectedPermissions: []accesscontrol.Permission{ + {Action: accesscontrol.ActionUsersRead}, + }, + }, + { + name: "does not load the permissions when RBAC is disabled", + rbacDisabled: true, + identity: &authn.Identity{ID: "user:2", OrgID: 1, ClientParams: authn.ClientParams{SyncPermissions: true}}, + expectedPermissions: []accesscontrol.Permission{}, + }, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + s := setupTestEnv(tt.rbacDisabled) + + err := s.SyncPermissionsHook(context.Background(), tt.identity, &authn.Request{}) + require.NoError(t, err) + + if !tt.rbacDisabled { + assert.Equal(t, 1, len(tt.identity.Permissions)) + assert.Equal(t, accesscontrol.GroupScopesByAction(tt.expectedPermissions), tt.identity.Permissions[tt.identity.OrgID]) + } else { + assert.Equal(t, 0, len(tt.identity.Permissions)) + } + }) + } +} + +func setupTestEnv(rbacDisabled bool) *PermissionsSync { + acMock := &acmock.Mock{ + IsDisabledFunc: func() bool { + return rbacDisabled + }, + GetUserPermissionsFunc: func(ctx context.Context, siu *user.SignedInUser, o accesscontrol.Options) ([]accesscontrol.Permission, error) { + return []accesscontrol.Permission{ + {Action: accesscontrol.ActionUsersRead}, + }, nil + }, + } + s := &PermissionsSync{ + ac: acMock, + log: log.NewNopLogger(), + } + return s +} diff --git a/pkg/services/authn/clients/anonymous.go b/pkg/services/authn/clients/anonymous.go index 2d5d919954e..bd29a1eab04 100644 --- a/pkg/services/authn/clients/anonymous.go +++ b/pkg/services/authn/clients/anonymous.go @@ -56,7 +56,7 @@ func (a *Anonymous) Authenticate(ctx context.Context, r *authn.Request) (*authn. OrgID: o.ID, OrgName: o.Name, OrgRoles: map[int64]org.RoleType{o.ID: org.RoleType(a.cfg.AnonymousOrgRole)}, - ClientParams: authn.ClientParams{}, + ClientParams: authn.ClientParams{SyncPermissions: true}, }, nil } diff --git a/pkg/services/authn/clients/api_key.go b/pkg/services/authn/clients/api_key.go index 1b712f2fe2f..52da3782397 100644 --- a/pkg/services/authn/clients/api_key.go +++ b/pkg/services/authn/clients/api_key.go @@ -64,9 +64,10 @@ func (s *APIKey) Authenticate(ctx context.Context, r *authn.Request) (*authn.Ide // if the api key don't belong to a service account construct the identity and return it if apiKey.ServiceAccountId == nil || *apiKey.ServiceAccountId < 1 { return &authn.Identity{ - ID: authn.NamespacedID(authn.NamespaceAPIKey, apiKey.ID), - OrgID: apiKey.OrgID, - OrgRoles: map[int64]org.RoleType{apiKey.OrgID: apiKey.Role}, + ID: authn.NamespacedID(authn.NamespaceAPIKey, apiKey.ID), + OrgID: apiKey.OrgID, + OrgRoles: map[int64]org.RoleType{apiKey.OrgID: apiKey.Role}, + ClientParams: authn.ClientParams{SyncPermissions: true}, }, nil } @@ -79,7 +80,7 @@ func (s *APIKey) Authenticate(ctx context.Context, r *authn.Request) (*authn.Ide return nil, err } - return authn.IdentityFromSignedInUser(authn.NamespacedID(authn.NamespaceServiceAccount, usr.UserID), usr, authn.ClientParams{}), nil + return authn.IdentityFromSignedInUser(authn.NamespacedID(authn.NamespaceServiceAccount, usr.UserID), usr, authn.ClientParams{SyncPermissions: true}), nil } func (s *APIKey) getAPIKey(ctx context.Context, token string) (*apikey.APIKey, error) { diff --git a/pkg/services/authn/clients/api_key_test.go b/pkg/services/authn/clients/api_key_test.go index 79d98df756c..2ce35f1eab9 100644 --- a/pkg/services/authn/clients/api_key_test.go +++ b/pkg/services/authn/clients/api_key_test.go @@ -52,6 +52,9 @@ func TestAPIKey_Authenticate(t *testing.T) { ID: "api-key:1", OrgID: 1, OrgRoles: map[int64]org.RoleType{1: org.RoleAdmin}, + ClientParams: authn.ClientParams{ + SyncPermissions: true, + }, }, }, { @@ -82,6 +85,9 @@ func TestAPIKey_Authenticate(t *testing.T) { Name: "test", OrgRoles: map[int64]org.RoleType{1: org.RoleViewer}, IsGrafanaAdmin: boolPtr(false), + ClientParams: authn.ClientParams{ + SyncPermissions: true, + }, }, }, { diff --git a/pkg/services/authn/clients/grafana.go b/pkg/services/authn/clients/grafana.go index af77b62ae84..7919fb82b9a 100644 --- a/pkg/services/authn/clients/grafana.go +++ b/pkg/services/authn/clients/grafana.go @@ -108,7 +108,7 @@ func (c *Grafana) AuthenticatePassword(ctx context.Context, r *authn.Request, us return nil, err } - return authn.IdentityFromSignedInUser(authn.NamespacedID(authn.NamespaceUser, signedInUser.UserID), signedInUser, authn.ClientParams{}), nil + return authn.IdentityFromSignedInUser(authn.NamespacedID(authn.NamespaceUser, signedInUser.UserID), signedInUser, authn.ClientParams{SyncPermissions: true}), nil } func comparePassword(password, salt, hash string) bool { diff --git a/pkg/services/authn/clients/grafana_test.go b/pkg/services/authn/clients/grafana_test.go index b33a00fd5a0..f2cd1503345 100644 --- a/pkg/services/authn/clients/grafana_test.go +++ b/pkg/services/authn/clients/grafana_test.go @@ -142,7 +142,12 @@ func TestGrafana_AuthenticatePassword(t *testing.T) { password: "password", findUser: true, expectedSignedInUser: &user.SignedInUser{UserID: 1, OrgID: 1, OrgRole: "Viewer"}, - expectedIdentity: &authn.Identity{ID: "user:1", OrgID: 1, OrgRoles: map[int64]org.RoleType{1: "Viewer"}, IsGrafanaAdmin: boolPtr(false)}, + expectedIdentity: &authn.Identity{ + ID: "user:1", + OrgID: 1, + OrgRoles: map[int64]org.RoleType{1: "Viewer"}, + IsGrafanaAdmin: boolPtr(false), + ClientParams: authn.ClientParams{SyncPermissions: true}}, }, { desc: "should fail for incorrect password", diff --git a/pkg/services/authn/clients/jwt.go b/pkg/services/authn/clients/jwt.go index fa0a2ebab5e..0ef0fdc14c1 100644 --- a/pkg/services/authn/clients/jwt.go +++ b/pkg/services/authn/clients/jwt.go @@ -70,6 +70,7 @@ func (s *JWT) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identi ClientParams: authn.ClientParams{ SyncUser: true, FetchSyncedUser: true, + SyncPermissions: true, SyncOrgRoles: !s.cfg.JWTAuthSkipOrgRoleSync, AllowSignUp: s.cfg.JWTAuthAutoSignUp, }} diff --git a/pkg/services/authn/clients/jwt_test.go b/pkg/services/authn/clients/jwt_test.go index 6eb6d555a1c..04a4c837bfe 100644 --- a/pkg/services/authn/clients/jwt_test.go +++ b/pkg/services/authn/clients/jwt_test.go @@ -53,6 +53,7 @@ func TestAuthenticateJWT(t *testing.T) { AllowSignUp: true, FetchSyncedUser: true, SyncOrgRoles: true, + SyncPermissions: true, LookUpParams: login.UserLookupParams{ UserID: nil, Email: stringPtr("eai.doe@cor.po"), diff --git a/pkg/services/authn/clients/ldap.go b/pkg/services/authn/clients/ldap.go index c2e166bdfcd..76013eb9c34 100644 --- a/pkg/services/authn/clients/ldap.go +++ b/pkg/services/authn/clients/ldap.go @@ -85,6 +85,7 @@ func (c *LDAP) identityFromLDAPInfo(orgID int64, info *login.ExternalUserInfo) * SyncTeams: true, EnableDisabledUsers: true, FetchSyncedUser: true, + SyncPermissions: true, SyncOrgRoles: !c.cfg.LDAPSkipOrgRoleSync, AllowSignUp: c.cfg.LDAPAllowSignup, LookUpParams: login.UserLookupParams{ diff --git a/pkg/services/authn/clients/ldap_test.go b/pkg/services/authn/clients/ldap_test.go index 0f7a0ace532..0650550ecb8 100644 --- a/pkg/services/authn/clients/ldap_test.go +++ b/pkg/services/authn/clients/ldap_test.go @@ -53,6 +53,7 @@ func TestLDAP_AuthenticateProxy(t *testing.T) { EnableDisabledUsers: true, FetchSyncedUser: true, SyncOrgRoles: true, + SyncPermissions: true, LookUpParams: login.UserLookupParams{ Email: strPtr("test@test.com"), Login: strPtr("test"), @@ -118,6 +119,7 @@ func TestLDAP_AuthenticatePassword(t *testing.T) { EnableDisabledUsers: true, FetchSyncedUser: true, SyncOrgRoles: true, + SyncPermissions: true, LookUpParams: login.UserLookupParams{ Email: strPtr("test@test.com"), Login: strPtr("test"), diff --git a/pkg/services/authn/clients/oauth.go b/pkg/services/authn/clients/oauth.go index 906c29a0dee..e6f4eeb95c6 100644 --- a/pkg/services/authn/clients/oauth.go +++ b/pkg/services/authn/clients/oauth.go @@ -154,6 +154,7 @@ func (c *OAuth) Authenticate(ctx context.Context, r *authn.Request) (*authn.Iden SyncUser: true, SyncTeams: true, FetchSyncedUser: true, + SyncPermissions: true, AllowSignUp: c.connector.IsSignupAllowed(), // skip org role flag is checked and handled in the connector. For now we can skip the hook if no roles are passed SyncOrgRoles: len(orgRoles) > 0, diff --git a/pkg/services/authn/clients/proxy.go b/pkg/services/authn/clients/proxy.go index dc1554c0c73..d4683b4fc81 100644 --- a/pkg/services/authn/clients/proxy.go +++ b/pkg/services/authn/clients/proxy.go @@ -96,7 +96,7 @@ func (c *Proxy) Authenticate(ctx context.Context, r *authn.Request) (*authn.Iden // and perform syncs if usr != nil { c.log.FromContext(ctx).Debug("User was loaded from cache, skip syncs", "userId", usr.UserID) - return authn.IdentityFromSignedInUser(authn.NamespacedID(authn.NamespaceUser, usr.UserID), usr, authn.ClientParams{}), nil + return authn.IdentityFromSignedInUser(authn.NamespacedID(authn.NamespaceUser, usr.UserID), usr, authn.ClientParams{SyncPermissions: true}), nil } } } diff --git a/pkg/services/authn/clients/render.go b/pkg/services/authn/clients/render.go index 52e39b1a981..23affc2c978 100644 --- a/pkg/services/authn/clients/render.go +++ b/pkg/services/authn/clients/render.go @@ -45,9 +45,10 @@ func (c *Render) Authenticate(ctx context.Context, r *authn.Request) (*authn.Ide var identity *authn.Identity if renderUsr.UserID <= 0 { identity = &authn.Identity{ - ID: authn.NamespacedID(authn.NamespaceUser, 0), - OrgID: renderUsr.OrgID, - OrgRoles: map[int64]org.RoleType{renderUsr.OrgID: org.RoleType(renderUsr.OrgRole)}, + ID: authn.NamespacedID(authn.NamespaceUser, 0), + OrgID: renderUsr.OrgID, + OrgRoles: map[int64]org.RoleType{renderUsr.OrgID: org.RoleType(renderUsr.OrgRole)}, + ClientParams: authn.ClientParams{SyncPermissions: true}, } } else { usr, err := c.userService.GetSignedInUserWithCacheCtx(ctx, &user.GetSignedInUserQuery{UserID: renderUsr.UserID, OrgID: renderUsr.OrgID}) @@ -55,7 +56,7 @@ func (c *Render) Authenticate(ctx context.Context, r *authn.Request) (*authn.Ide return nil, err } - identity = authn.IdentityFromSignedInUser(authn.NamespacedID(authn.NamespaceUser, usr.UserID), usr, authn.ClientParams{}) + identity = authn.IdentityFromSignedInUser(authn.NamespacedID(authn.NamespaceUser, usr.UserID), usr, authn.ClientParams{SyncPermissions: true}) } identity.LastSeenAt = time.Now() diff --git a/pkg/services/authn/clients/render_test.go b/pkg/services/authn/clients/render_test.go index d38e2421634..4a1a9728948 100644 --- a/pkg/services/authn/clients/render_test.go +++ b/pkg/services/authn/clients/render_test.go @@ -38,10 +38,11 @@ func TestRender_Authenticate(t *testing.T) { }, }, expectedIdentity: &authn.Identity{ - ID: "user:0", - OrgID: 1, - OrgRoles: map[int64]org.RoleType{1: org.RoleViewer}, - AuthModule: login.RenderModule, + ID: "user:0", + OrgID: 1, + OrgRoles: map[int64]org.RoleType{1: org.RoleViewer}, + AuthModule: login.RenderModule, + ClientParams: authn.ClientParams{SyncPermissions: true}, }, expectedRenderUsr: &rendering.RenderUser{ OrgID: 1, @@ -64,6 +65,7 @@ func TestRender_Authenticate(t *testing.T) { OrgRoles: map[int64]org.RoleType{1: org.RoleAdmin}, IsGrafanaAdmin: boolPtr(false), AuthModule: login.RenderModule, + ClientParams: authn.ClientParams{SyncPermissions: true}, }, expectedRenderUsr: &rendering.RenderUser{ OrgID: 1, diff --git a/pkg/services/authn/clients/session.go b/pkg/services/authn/clients/session.go index 5a2ece98854..071ab01ad25 100644 --- a/pkg/services/authn/clients/session.go +++ b/pkg/services/authn/clients/session.go @@ -62,7 +62,7 @@ func (s *Session) Authenticate(ctx context.Context, r *authn.Request) (*authn.Id return nil, err } - identity := authn.IdentityFromSignedInUser(authn.NamespacedID(authn.NamespaceUser, signedInUser.UserID), signedInUser, authn.ClientParams{}) + identity := authn.IdentityFromSignedInUser(authn.NamespacedID(authn.NamespaceUser, signedInUser.UserID), signedInUser, authn.ClientParams{SyncPermissions: true}) identity.SessionToken = token return identity, nil diff --git a/pkg/services/authn/clients/session_test.go b/pkg/services/authn/clients/session_test.go index bd640ff9837..f19973eda7e 100644 --- a/pkg/services/authn/clients/session_test.go +++ b/pkg/services/authn/clients/session_test.go @@ -108,6 +108,9 @@ func TestSession_Authenticate(t *testing.T) { OrgID: 1, OrgRoles: map[int64]roletype.RoleType{1: roletype.RoleEditor}, IsGrafanaAdmin: boolPtr(false), + ClientParams: authn.ClientParams{ + SyncPermissions: true, + }, }, wantErr: false, }, From 7aca818aae26474cdaacfd8c21e3a1534dfa8678 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Farkas?= Date: Wed, 8 Mar 2023 13:42:47 +0100 Subject: [PATCH 062/288] feature flags: assign flags to observability-logs squad (#64380) --- pkg/services/featuremgmt/codeowners.go | 21 ++++++++++---------- pkg/services/featuremgmt/registry.go | 7 ++++++- pkg/services/featuremgmt/toggles_gen_test.go | 5 ----- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/pkg/services/featuremgmt/codeowners.go b/pkg/services/featuremgmt/codeowners.go index 7b62880638d..a31e27a7d7c 100644 --- a/pkg/services/featuremgmt/codeowners.go +++ b/pkg/services/featuremgmt/codeowners.go @@ -5,14 +5,15 @@ package featuremgmt type codeowner string const ( - grafanaAppPlatformSquad codeowner = "@grafana/grafana-app-platform-squad" - grafanaDashboardsSquad codeowner = "@grafana/dashboards-squad" - grafanaExploreSquad codeowner = "@grafana/explore-squad" - grafanaBiSquad codeowner = "@grafana/grafana-bi-squad" - grafanaDatavizSquad codeowner = "@grafana/dataviz-squad" - grafanaUserEssentialsSquad codeowner = "@grafana/user-essentials" - grafanaBackendPlatformSquad codeowner = "@grafana/backend-platform" - grafanaPluginsPlatformSquad codeowner = "@grafana/plugins-platform-backend" - grafanaAsCodeSquad codeowner = "@grafana/grafana-as-code" - grafanaAuthnzSquad codeowner = "@grafana/grafana-authnz-team" + grafanaAppPlatformSquad codeowner = "@grafana/grafana-app-platform-squad" + grafanaDashboardsSquad codeowner = "@grafana/dashboards-squad" + grafanaExploreSquad codeowner = "@grafana/explore-squad" + grafanaBiSquad codeowner = "@grafana/grafana-bi-squad" + grafanaDatavizSquad codeowner = "@grafana/dataviz-squad" + grafanaUserEssentialsSquad codeowner = "@grafana/user-essentials" + grafanaBackendPlatformSquad codeowner = "@grafana/backend-platform" + grafanaPluginsPlatformSquad codeowner = "@grafana/plugins-platform-backend" + grafanaAsCodeSquad codeowner = "@grafana/grafana-as-code" + grafanaAuthnzSquad codeowner = "@grafana/grafana-authnz-team" + grafanaObservabilityLogsSquad codeowner = "@grafana/observability-logs" ) diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index f603daae548..81a7c443b4d 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -84,12 +84,13 @@ var ( Name: "lokiLive", Description: "Support WebSocket streaming for loki (early prototype)", State: FeatureStateAlpha, - Owner: grafanaAppPlatformSquad, + Owner: grafanaObservabilityLogsSquad, }, { Name: "lokiDataframeApi", Description: "Use experimental loki api for WebSocket streaming (early prototype)", State: FeatureStateAlpha, + Owner: grafanaObservabilityLogsSquad, }, { Name: "featureHighlights", @@ -323,6 +324,7 @@ var ( Name: "elasticsearchBackendMigration", Description: "Use Elasticsearch as backend data source", State: FeatureStateAlpha, + Owner: grafanaObservabilityLogsSquad, }, { Name: "datasourceOnboarding", @@ -370,18 +372,21 @@ var ( State: FeatureStateStable, Expression: "true", //turned on by default FrontendOnly: true, + Owner: grafanaObservabilityLogsSquad, }, { Name: "logsContextDatasourceUi", Description: "Allow datasource to provide custom UI for context view", State: FeatureStateAlpha, FrontendOnly: true, + Owner: grafanaObservabilityLogsSquad, }, { Name: "lokiQuerySplitting", Description: "Split large interval queries into subqueries with smaller time intervals", State: FeatureStateAlpha, FrontendOnly: true, + Owner: grafanaObservabilityLogsSquad, }, { Name: "individualCookiePreferences", diff --git a/pkg/services/featuremgmt/toggles_gen_test.go b/pkg/services/featuremgmt/toggles_gen_test.go index 9e4b55c8072..acfb3a15f89 100644 --- a/pkg/services/featuremgmt/toggles_gen_test.go +++ b/pkg/services/featuremgmt/toggles_gen_test.go @@ -46,7 +46,6 @@ func TestFeatureToggleFiles(t *testing.T) { "trimDefaults": true, "database_metrics": true, "prometheusAzureOverrideAudience": true, - "lokiDataframeApi": true, "featureHighlights": true, "tracing": true, "newTraceView": true, @@ -62,15 +61,11 @@ func TestFeatureToggleFiles(t *testing.T) { "athenaAsyncQueryDataSupport": true, "newPanelChromeUI": true, "showDashboardValidationWarnings": true, - "elasticsearchBackendMigration": true, "datasourceOnboarding": true, "secureSocksDatasourceProxy": true, "disablePrometheusExemplarSampling": true, "alertingBacktesting": true, "alertingNoNormalState": true, - "logsSampleInExplore": true, - "logsContextDatasourceUi": true, - "lokiQuerySplitting": true, "individualCookiePreferences": true, "traceqlSearch": true, } From b63c56903daa7963b02ee319dd4f6a0534e3ce49 Mon Sep 17 00:00:00 2001 From: Marcus Andersson Date: Wed, 8 Mar 2023 14:23:29 +0100 Subject: [PATCH 063/288] Plugins: Extend panel menu with commands from plugins (#63802) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(plugins): introduce dashboard panel menu placement for adding menu items * test: add test for getPanelMenu() * added an unique identifier for each extension. * added context to getPluginExtensions. * wip * Wip * wiwip * Wip * feat: WWWIIIIPPPP 🧨 * Wip * Renamed some of the types to align a bit better. * added limit to how many extensions a plugin can register per placement. * decreased number of items to 2 * will trim the lenght of titles to max 25 chars. * wrapping configure function with error handling. * added error handling for all scenarios. * moved extension menu items to the bottom of the more sub menu. * added tests for configuring the title. * minor refactorings. * changed so you need to specify the full path in package.json. * wip * removed unused type. * big refactor to make things simpler and to centralize all configure error/validation handling. * added missing import. * fixed failing tests. * fixed tests. * revert(extensions): remove static extensions config in favour of registering via AppPlugin APIs * removed the compose that didn't work for some reason. * added tests just to verify that validation and error handling is tied together in configuration function. * adding some more values to the context. * draft validation. * added missing tests for getPanelMenu. * added more tests. * refactor(extensions): move logic for validating extension link config to function * Fixed ts errors. * Started to add structure for supporting commands. * fixed tests. * adding commands to the registry * tests: group test cases in describe blocks * tests: add a little bit more refactoring to the tests * tests: add a test case for checking correct placements * feat: first version of the command handler * feat: register panel menu items with commands * refactor: make the 'configure' function not optional on `PluginExtensionRegistryItem` * Wip * Wip * Wip * added test to verify the default configure function. * added some more tests to verify that commands have the proper error handling for its configure function. * tests: fix TS errors in tests * tests: add auxiliary functions * refactor: small refactoring in tests * refactor: refactoring tests for registryFactory * refactor: refactoring tests for registryFactory * refactor: refactoring tests for registryFactory * refactor: refactoring tests for registryFactory * refactor: refactoring tests for registryFactory * refactor: refactoring tests for registryFactory * refactor: refactoring tests for registryFactory * refactor: refactoring tests for registryFactory * draft of wrapping command handler with error handling. * refactor: refactoring tests for registryFactory * added test for edge case. * replaced the registry item with a configure function. * renamed the configure function type. * refactoring of the registryfactory. * added tests for handler error handling. * fixed issue with assert function. * added comment about the limited type. * Update public/app/features/plugins/extensions/errorHandling.test.ts Co-authored-by: Levente Balogh * Update public/app/features/plugins/extensions/errorHandling.test.ts Co-authored-by: Levente Balogh * Update public/app/features/plugins/extensions/errorHandling.test.ts Co-authored-by: Levente Balogh * added missing tests. --------- Co-authored-by: Jack Westbrook Co-authored-by: Levente Balogh --- .betterer.results | 3 +- packages/grafana-data/src/types/app.ts | 30 +- packages/grafana-data/src/types/index.ts | 4 + .../src/types/pluginExtensions.ts | 34 +- .../grafana-runtime/src/services/index.ts | 1 - .../pluginExtensions/extensions.test.ts | 27 +- .../services/pluginExtensions/extensions.ts | 11 +- .../src/services/pluginExtensions/registry.ts | 9 +- .../dashboard/utils/getPanelMenu.test.ts | 23 +- .../features/dashboard/utils/getPanelMenu.ts | 11 +- .../plugins/extensions/errorHandling.test.ts | 155 ++-- .../plugins/extensions/errorHandling.ts | 35 +- .../plugins/extensions/placementsPerPlugin.ts | 15 + .../extensions/registryFactory.test.ts | 769 +++++++++++------- .../plugins/extensions/registryFactory.ts | 193 +++-- .../app/features/plugins/extensions/types.ts | 4 + .../plugins/extensions/validateLink.ts | 6 +- .../app/features/plugins/pluginPreloader.ts | 9 +- 18 files changed, 855 insertions(+), 484 deletions(-) create mode 100644 public/app/features/plugins/extensions/placementsPerPlugin.ts create mode 100644 public/app/features/plugins/extensions/types.ts diff --git a/.betterer.results b/.betterer.results index e04d60a60a3..ec5f720494e 100644 --- a/.betterer.results +++ b/.betterer.results @@ -334,7 +334,8 @@ exports[`better eslint`] = { ], "packages/grafana-data/src/types/app.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Do not use any type assertions.", "1"] + [0, 0, 0, "Do not use any type assertions.", "1"], + [0, 0, 0, "Do not use any type assertions.", "2"] ], "packages/grafana-data/src/types/config.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] diff --git a/packages/grafana-data/src/types/app.ts b/packages/grafana-data/src/types/app.ts index 4bafc33f92e..49e6ef78883 100644 --- a/packages/grafana-data/src/types/app.ts +++ b/packages/grafana-data/src/types/app.ts @@ -3,7 +3,7 @@ import { ComponentType } from 'react'; import { KeyValue } from './data'; import { NavModel } from './navModel'; import { PluginMeta, GrafanaPlugin, PluginIncludeType } from './plugin'; -import { extensionLinkConfigIsValid, PluginExtensionLink } from './pluginExtensions'; +import { extensionLinkConfigIsValid, type PluginExtensionCommand, type PluginExtensionLink } from './pluginExtensions'; /** * @public @@ -51,23 +51,32 @@ export interface AppPluginMeta extends PluginMeta } /** - * These types are towards the plugin developer when extending Grafana or other - * plugins from the module.ts + * The `configure()` function can only update certain properties of the extension, and due to this + * it only receives a subset of the original extension object. */ -export type AppConfigureExtension = (extension: T, context: C) => Partial | undefined; - export type AppPluginExtensionLink = Pick; +export type AppPluginExtensionCommand = Pick; + export type AppPluginExtensionLinkConfig = { title: string; description: string; placement: string; path: string; - configure?: AppConfigureExtension; + configure?: (extension: AppPluginExtensionLink, context?: C) => Partial | undefined; +}; + +export type AppPluginExtensionCommandConfig = { + title: string; + description: string; + placement: string; + handler: (context?: C) => void; + configure?: (extension: AppPluginExtensionCommand, context?: C) => Partial | undefined; }; export class AppPlugin extends GrafanaPlugin> { private linkExtensions: AppPluginExtensionLinkConfig[] = []; + private commandExtensions: AppPluginExtensionCommandConfig[] = []; // Content under: /a/${plugin-id}/* root?: ComponentType>; @@ -113,6 +122,10 @@ export class AppPlugin extends GrafanaPlugin(config: AppPluginExtensionLinkConfig) { const { path, description, title, placement } = config; @@ -124,6 +137,11 @@ export class AppPlugin extends GrafanaPlugin(config: AppPluginExtensionCommandConfig) { + this.commandExtensions.push(config as AppPluginExtensionCommandConfig); + return this; + } } /** diff --git a/packages/grafana-data/src/types/index.ts b/packages/grafana-data/src/types/index.ts index d39a5b6fc9b..0a3020aff99 100644 --- a/packages/grafana-data/src/types/index.ts +++ b/packages/grafana-data/src/types/index.ts @@ -56,5 +56,9 @@ export { type PluginExtension, type PluginExtensionLink, isPluginExtensionLink, + assertPluginExtensionLink, + type PluginExtensionCommand, + isPluginExtensionCommand, + assertPluginExtensionCommand, PluginExtensionTypes, } from './pluginExtensions'; diff --git a/packages/grafana-data/src/types/pluginExtensions.ts b/packages/grafana-data/src/types/pluginExtensions.ts index e1635a9fbdc..c2a4215dd6d 100644 --- a/packages/grafana-data/src/types/pluginExtensions.ts +++ b/packages/grafana-data/src/types/pluginExtensions.ts @@ -4,6 +4,7 @@ export enum PluginExtensionTypes { link = 'link', + command = 'command', } export type PluginExtension = { @@ -18,10 +19,41 @@ export type PluginExtensionLink = PluginExtension & { path: string; }; -export function isPluginExtensionLink(extension: PluginExtension): extension is PluginExtensionLink { +export type PluginExtensionCommand = PluginExtension & { + type: PluginExtensionTypes.command; + callHandlerWithContext: () => void; +}; + +export function isPluginExtensionLink(extension: PluginExtension | undefined): extension is PluginExtensionLink { + if (!extension) { + return false; + } return extension.type === PluginExtensionTypes.link && 'path' in extension; } +export function assertPluginExtensionLink( + extension: PluginExtension | undefined +): asserts extension is PluginExtensionLink { + if (!isPluginExtensionLink(extension)) { + throw new Error(`extension is not a link extension`); + } +} + +export function isPluginExtensionCommand(extension: PluginExtension | undefined): extension is PluginExtensionCommand { + if (!extension) { + return false; + } + return extension.type === PluginExtensionTypes.command; +} + +export function assertPluginExtensionCommand( + extension: PluginExtension | undefined +): asserts extension is PluginExtensionCommand { + if (!isPluginExtensionCommand(extension)) { + throw new Error(`extension is not a command extension`); + } +} + export function extensionLinkConfigIsValid(props: { path?: string; description?: string; diff --git a/packages/grafana-runtime/src/services/index.ts b/packages/grafana-runtime/src/services/index.ts index 7cf78413085..793fe5eb363 100644 --- a/packages/grafana-runtime/src/services/index.ts +++ b/packages/grafana-runtime/src/services/index.ts @@ -11,7 +11,6 @@ export * from './appEvents'; export { type PluginExtensionRegistry, type PluginExtensionRegistryItem, - type RegistryConfigureExtension, setPluginsExtensionRegistry, } from './pluginExtensions/registry'; export { diff --git a/packages/grafana-runtime/src/services/pluginExtensions/extensions.test.ts b/packages/grafana-runtime/src/services/pluginExtensions/extensions.test.ts index 1a17c244e32..ac13c016df2 100644 --- a/packages/grafana-runtime/src/services/pluginExtensions/extensions.test.ts +++ b/packages/grafana-runtime/src/services/pluginExtensions/extensions.test.ts @@ -1,4 +1,4 @@ -import { isPluginExtensionLink, PluginExtension, PluginExtensionLink, PluginExtensionTypes } from '@grafana/data'; +import { assertPluginExtensionLink, PluginExtensionLink, PluginExtensionTypes } from '@grafana/data'; import { getPluginExtensions } from './extensions'; import { PluginExtensionRegistryItem, setPluginsExtensionRegistry } from './registry'; @@ -33,7 +33,7 @@ describe('getPluginExtensions', () => { const { extensions } = getPluginExtensions({ placement }); const [extension] = extensions; - assertLinkExtension(extension); + assertPluginExtensionLink(extension); expect(extension.path).toBe(`/a/${pluginId}/declare-incident`); expect(extensions.length).toBe(1); @@ -43,7 +43,7 @@ describe('getPluginExtensions', () => { const { extensions } = getPluginExtensions({ placement }); const [extension] = extensions; - assertLinkExtension(extension); + assertPluginExtensionLink(extension); expect(extension.description).toBe('Declaring an incident in the app'); expect(extensions.length).toBe(1); @@ -53,13 +53,13 @@ describe('getPluginExtensions', () => { const { extensions } = getPluginExtensions({ placement }); const [extension] = extensions; - assertLinkExtension(extension); + assertPluginExtensionLink(extension); expect(extension.title).toBe('Declare incident'); expect(extensions.length).toBe(1); }); - it('should return an empty array when extensions cannot be found', () => { + it('should return an empty array when extensions can be found', () => { const { extensions } = getPluginExtensions({ placement: 'plugins/not-installed-app/news', }); @@ -72,17 +72,8 @@ describe('getPluginExtensions', () => { function createRegistryLinkItem( link: Omit ): PluginExtensionRegistryItem { - return { - configure: undefined, - extension: { - ...link, - type: PluginExtensionTypes.link, - }, - }; -} - -function assertLinkExtension(extension: PluginExtension): asserts extension is PluginExtensionLink { - if (!isPluginExtensionLink(extension)) { - throw new Error(`extension is not a link extension`); - } + return (context?: object) => ({ + ...link, + type: PluginExtensionTypes.link, + }); } diff --git a/packages/grafana-runtime/src/services/pluginExtensions/extensions.ts b/packages/grafana-runtime/src/services/pluginExtensions/extensions.ts index 13052977204..a5dee9d514a 100644 --- a/packages/grafana-runtime/src/services/pluginExtensions/extensions.ts +++ b/packages/grafana-runtime/src/services/pluginExtensions/extensions.ts @@ -16,15 +16,10 @@ export function getPluginExtensions( ): PluginExtensionsResult { const { placement, context } = options; const registry = getPluginsExtensionRegistry(); - const items = registry[placement] ?? []; + const configureFuncs = registry[placement] ?? []; - const extensions = items.reduce((result, item) => { - if (!context || !item.configure) { - result.push(item.extension); - return result; - } - - const extension = item.configure(context); + const extensions = configureFuncs.reduce((result, configure) => { + const extension = configure(context); if (extension) { result.push(extension); } diff --git a/packages/grafana-runtime/src/services/pluginExtensions/registry.ts b/packages/grafana-runtime/src/services/pluginExtensions/registry.ts index 28beac6bddd..98b9ccd0848 100644 --- a/packages/grafana-runtime/src/services/pluginExtensions/registry.ts +++ b/packages/grafana-runtime/src/services/pluginExtensions/registry.ts @@ -1,14 +1,9 @@ import { PluginExtension } from '@grafana/data'; -export type RegistryConfigureExtension = ( - context: C +export type PluginExtensionRegistryItem = ( + context?: C ) => T | undefined; -export type PluginExtensionRegistryItem = { - extension: T; - configure?: RegistryConfigureExtension; -}; - export type PluginExtensionRegistry = Record; let registry: PluginExtensionRegistry | undefined; diff --git a/public/app/features/dashboard/utils/getPanelMenu.test.ts b/public/app/features/dashboard/utils/getPanelMenu.test.ts index 4bced60f435..ca637b459f1 100644 --- a/public/app/features/dashboard/utils/getPanelMenu.test.ts +++ b/public/app/features/dashboard/utils/getPanelMenu.test.ts @@ -2,7 +2,6 @@ import { PanelMenuItem, PluginExtension, PluginExtensionLink, PluginExtensionTyp import { PluginExtensionPanelContext, PluginExtensionRegistryItem, - RegistryConfigureExtension, setPluginsExtensionRegistry, } from '@grafana/runtime'; import { LoadingState } from '@grafana/schema'; @@ -194,7 +193,7 @@ describe('getPanelMenu()', () => { }); it('should use extension for panel menu returned by configure function', () => { - const configure = () => ({ + const configure: PluginExtensionRegistryItem = () => ({ title: 'Wohoo', type: PluginExtensionTypes.link, description: 'Declaring an incident in the app', @@ -334,7 +333,7 @@ describe('getPanelMenu()', () => { }); it('should pass context that can not be edited in configure function', () => { - const configure = (context: PluginExtensionPanelContext) => { + const configure: PluginExtensionRegistryItem = (context) => { // trying to change values in the context // @ts-ignore context.pluginId = 'changed'; @@ -507,18 +506,10 @@ describe('getPanelMenu()', () => { }); }); -function createRegistryItem( +function createRegistryItem( extension: T, - configure?: (context: PluginExtensionPanelContext) => T | undefined -): PluginExtensionRegistryItem { - if (!configure) { - return { - extension, - }; - } - - return { - extension, - configure: configure as RegistryConfigureExtension, - }; + configure?: PluginExtensionRegistryItem +): PluginExtensionRegistryItem { + const defaultConfigure = () => extension; + return configure || defaultConfigure; } diff --git a/public/app/features/dashboard/utils/getPanelMenu.ts b/public/app/features/dashboard/utils/getPanelMenu.ts index 80419a4c9a8..8f2a2610a7a 100644 --- a/public/app/features/dashboard/utils/getPanelMenu.ts +++ b/public/app/features/dashboard/utils/getPanelMenu.ts @@ -1,4 +1,4 @@ -import { isPluginExtensionLink, PanelMenuItem } from '@grafana/data'; +import { isPluginExtensionCommand, isPluginExtensionLink, PanelMenuItem } from '@grafana/data'; import { AngularComponent, getDataSourceSrv, @@ -297,6 +297,15 @@ export function getPanelMenu( text: truncateTitle(extension.title, 25), href: extension.path, }); + continue; + } + + if (isPluginExtensionCommand(extension)) { + subMenu.push({ + text: truncateTitle(extension.title, 25), + onClick: extension.callHandlerWithContext, + }); + continue; } } diff --git a/public/app/features/plugins/extensions/errorHandling.test.ts b/public/app/features/plugins/extensions/errorHandling.test.ts index 2b0ab145375..a6cb6bdcdcc 100644 --- a/public/app/features/plugins/extensions/errorHandling.test.ts +++ b/public/app/features/plugins/extensions/errorHandling.test.ts @@ -1,79 +1,122 @@ -import { AppConfigureExtension, AppPluginExtensionLink } from '@grafana/data'; +import { AppPluginExtensionLink } from '@grafana/data'; -import { createErrorHandling } from './errorHandling'; +import { handleErrorsInConfigure, handleErrorsInHandler } from './errorHandling'; +import type { CommandHandlerFunc, ConfigureFunc } from './types'; -describe('extension error handling', () => { - const pluginId = 'grafana-basic-app'; - const errorHandler = createErrorHandling({ - pluginId: pluginId, - title: 'Go to page one', - logger: jest.fn(), - }); +describe('error handling for extensions', () => { + describe('error handling for configure', () => { + const pluginId = 'grafana-basic-app'; + const errorHandler = handleErrorsInConfigure({ + pluginId: pluginId, + title: 'Go to page one', + logger: jest.fn(), + }); - const context = {}; - const extension: AppPluginExtensionLink = { - title: 'Go to page one', - description: 'Will navigate the user to page one', - path: `/a/${pluginId}/one`, - }; + const context = {}; + const extension: AppPluginExtensionLink = { + title: 'Go to page one', + description: 'Will navigate the user to page one', + path: `/a/${pluginId}/one`, + }; - it('should return configured link if configure is successful', () => { - const configureWithErrorHandling = errorHandler(() => { - return { + it('should return configured link if configure is successful', () => { + const configureWithErrorHandling = errorHandler(() => { + return { + title: 'This is a new title', + }; + }); + + const configured = configureWithErrorHandling(extension, context); + + expect(configured).toEqual({ title: 'This is a new title', - }; + }); }); - const configured = configureWithErrorHandling(extension, context); + it('should return undefined if configure throws error', () => { + const configureWithErrorHandling = errorHandler(() => { + throw new Error(); + }); - expect(configured).toEqual({ - title: 'This is a new title', + const configured = configureWithErrorHandling(extension, context); + + expect(configured).toBeUndefined(); + }); + + it('should return undefined if configure is promise/async-based', () => { + const promisebased = (async () => {}) as ConfigureFunc; + const configureWithErrorHandling = errorHandler(promisebased); + + const configured = configureWithErrorHandling(extension, context); + + expect(configured).toBeUndefined(); + }); + + it('should return undefined if configure is not a function', () => { + const objectbased = {} as ConfigureFunc; + const configureWithErrorHandling = errorHandler(objectbased); + + const configured = configureWithErrorHandling(extension, context); + + expect(configured).toBeUndefined(); + }); + + it('should return undefined if configure returns other than an object', () => { + const returnString = (() => '') as ConfigureFunc; + const configureWithErrorHandling = errorHandler(returnString); + + const configured = configureWithErrorHandling(extension, context); + + expect(configured).toBeUndefined(); + }); + + it('should return undefined if configure returns undefined', () => { + const returnUndefined = () => undefined; + const configureWithErrorHandling = errorHandler(returnUndefined); + + const configured = configureWithErrorHandling(extension, context); + + expect(configured).toBeUndefined(); }); }); - it('should return undefined if configure throws error', () => { - const configureWithErrorHandling = errorHandler(() => { - throw new Error(); + describe('error handling for command handler', () => { + const pluginId = 'grafana-basic-app'; + const errorHandler = handleErrorsInHandler({ + pluginId: pluginId, + title: 'open modal', + logger: jest.fn(), }); - const configured = configureWithErrorHandling(extension, context); + it('should be called successfully when handler is a normal synchronous function', () => { + const handler = jest.fn(); + const handlerWithErrorHandling = errorHandler(handler); - expect(configured).toBeUndefined(); - }); + handlerWithErrorHandling(); - it('should return undefined if configure is promise/async-based', () => { - const promisebased = (async () => {}) as AppConfigureExtension; - const configureWithErrorHandling = errorHandler(promisebased); + expect(handler).toBeCalled(); + }); - const configured = configureWithErrorHandling(extension, context); + it('should not error out even if the handler throws an error', () => { + const handlerWithErrorHandling = errorHandler(() => { + throw new Error(); + }); - expect(configured).toBeUndefined(); - }); + expect(handlerWithErrorHandling).not.toThrowError(); + }); - it('should return undefined if configure is not a function', () => { - const objectbased = {} as AppConfigureExtension; - const configureWithErrorHandling = errorHandler(objectbased); + it('should be called successfully when handler is an async function / promise', () => { + const promisebased = (async () => {}) as CommandHandlerFunc; + const configureWithErrorHandling = errorHandler(promisebased); - const configured = configureWithErrorHandling(extension, context); + expect(configureWithErrorHandling).not.toThrowError(); + }); - expect(configured).toBeUndefined(); - }); + it('should be called successfully when handler is not a function', () => { + const objectbased = {} as CommandHandlerFunc; + const configureWithErrorHandling = errorHandler(objectbased); - it('should return undefined if configure returns other than an object', () => { - const returnString = (() => '') as AppConfigureExtension; - const configureWithErrorHandling = errorHandler(returnString); - - const configured = configureWithErrorHandling(extension, context); - - expect(configured).toBeUndefined(); - }); - - it('should return undefined if configure returns undefined', () => { - const returnUndefined = () => undefined; - const configureWithErrorHandling = errorHandler(returnUndefined); - - const configured = configureWithErrorHandling(extension, context); - - expect(configured).toBeUndefined(); + expect(configureWithErrorHandling).not.toThrowError(); + }); }); }); diff --git a/public/app/features/plugins/extensions/errorHandling.ts b/public/app/features/plugins/extensions/errorHandling.ts index 3e7e4db4e1e..9ef7b5ef0ee 100644 --- a/public/app/features/plugins/extensions/errorHandling.ts +++ b/public/app/features/plugins/extensions/errorHandling.ts @@ -1,6 +1,6 @@ import { isFunction, isObject } from 'lodash'; -import type { AppConfigureExtension } from '@grafana/data'; +import type { CommandHandlerFunc, ConfigureFunc } from './types'; type Options = { pluginId: string; @@ -8,10 +8,10 @@ type Options = { logger: (msg: string, error?: unknown) => void; }; -export function createErrorHandling(options: Options) { +export function handleErrorsInConfigure(options: Options) { const { pluginId, title, logger } = options; - return (configure: AppConfigureExtension): AppConfigureExtension => { + return (configure: ConfigureFunc): ConfigureFunc => { return function handleErrors(extension, context) { try { if (!isFunction(configure)) { @@ -41,3 +41,32 @@ export function createErrorHandling(options: Options) { }; }; } + +export function handleErrorsInHandler(options: Options) { + const { pluginId, title, logger } = options; + + return (handler: CommandHandlerFunc): CommandHandlerFunc => { + return function handleErrors(context) { + try { + if (!isFunction(handler)) { + logger(`[Plugins] ${pluginId} provided invalid handler function for command extension '${title}'.`); + return; + } + + const result = handler(context); + if (result instanceof Promise) { + logger( + `[Plugins] ${pluginId} provided an unsupported async/promise-based handler function for command extension '${title}'.` + ); + result.catch(() => {}); + return; + } + + return result; + } catch (error) { + logger(`[Plugins] ${pluginId} thow an error while handling command extension '${title}'`, error); + return; + } + }; + }; +} diff --git a/public/app/features/plugins/extensions/placementsPerPlugin.ts b/public/app/features/plugins/extensions/placementsPerPlugin.ts new file mode 100644 index 00000000000..62ea2e867eb --- /dev/null +++ b/public/app/features/plugins/extensions/placementsPerPlugin.ts @@ -0,0 +1,15 @@ +export class PlacementsPerPlugin { + private counter: Record = {}; + private limit = 2; + + allowedToAdd(placement: string): boolean { + const count = this.counter[placement] ?? 0; + + if (count >= this.limit) { + return false; + } + + this.counter[placement] = count + 1; + return true; + } +} diff --git a/public/app/features/plugins/extensions/registryFactory.test.ts b/public/app/features/plugins/extensions/registryFactory.test.ts index 50d1eca6957..903a2e09ad4 100644 --- a/public/app/features/plugins/extensions/registryFactory.test.ts +++ b/public/app/features/plugins/extensions/registryFactory.test.ts @@ -1,15 +1,27 @@ -import { PluginExtensionTypes } from '@grafana/data'; +import { + AppPluginExtensionCommandConfig, + AppPluginExtensionLinkConfig, + assertPluginExtensionCommand, + PluginExtensionTypes, +} from '@grafana/data'; +import { PluginExtensionRegistry } from '@grafana/runtime'; import { createPluginExtensionRegistry } from './registryFactory'; const validateLink = jest.fn((configure, extension, context) => configure?.(extension, context)); -const errorHandler = jest.fn((configure, extension, context) => configure?.(extension, context)); +const configureErrorHandler = jest.fn((configure, extension, context) => configure?.(extension, context)); +const commandErrorHandler = jest.fn((configure, context) => configure?.(context)); jest.mock('./errorHandling', () => ({ ...jest.requireActual('./errorHandling'), - createErrorHandling: jest.fn(() => { + handleErrorsInConfigure: jest.fn(() => { return jest.fn((configure) => { - return jest.fn((extension, context) => errorHandler(configure, extension, context)); + return jest.fn((extension, context) => configureErrorHandler(configure, extension, context)); + }); + }), + handleErrorsInHandler: jest.fn(() => { + return jest.fn((configure) => { + return jest.fn((context) => commandErrorHandler(configure, context)); }); }), })); @@ -23,304 +35,489 @@ jest.mock('./validateLink', () => ({ }), })); -describe('Creating extensions registry', () => { +describe('createPluginExtensionRegistry()', () => { beforeEach(() => { validateLink.mockClear(); - errorHandler.mockClear(); + configureErrorHandler.mockClear(); + commandErrorHandler.mockClear(); }); - it('should register an extension', () => { - const registry = createPluginExtensionRegistry([ - { - pluginId: 'belugacdn-app', - linkExtensions: [ - { - placement: 'grafana/dashboard/panel/menu', - title: 'Open incident', - description: 'You can create an incident from this context', - path: '/a/belugacdn-app/incidents/declare', - }, - ], - }, - ]); - - const numberOfPlacements = Object.keys(registry).length; - const extensions = registry['grafana/dashboard/panel/menu']; - - expect(numberOfPlacements).toBe(1); - expect(extensions).toEqual([ - { - configure: undefined, - extension: { - title: 'Open incident', - type: PluginExtensionTypes.link, - description: 'You can create an incident from this context', - path: '/a/belugacdn-app/incidents/declare', - key: -68154691, - }, - }, - ]); - }); - - it('should register extensions from one plugin with multiple placements', () => { - const registry = createPluginExtensionRegistry([ - { - pluginId: 'belugacdn-app', - linkExtensions: [ - { - placement: 'grafana/dashboard/panel/menu', - title: 'Open incident', - description: 'You can create an incident from this context', - path: '/a/belugacdn-app/incidents/declare', - }, - { - placement: 'plugins/grafana-slo-app/slo-breached', - title: 'Open incident', - description: 'You can create an incident from this context', - path: '/a/belugacdn-app/incidents/declare', - }, - ], - }, - ]); - - const numberOfPlacements = Object.keys(registry).length; - const panelExtensions = registry['grafana/dashboard/panel/menu']; - const sloExtensions = registry['plugins/grafana-slo-app/slo-breached']; - - expect(numberOfPlacements).toBe(2); - expect(panelExtensions).toEqual([ - { - configure: undefined, - extension: { - title: 'Open incident', - type: PluginExtensionTypes.link, - description: 'You can create an incident from this context', - path: '/a/belugacdn-app/incidents/declare', - key: -68154691, - }, - }, - ]); - expect(sloExtensions).toEqual([ - { - configure: undefined, - extension: { - title: 'Open incident', - type: PluginExtensionTypes.link, - description: 'You can create an incident from this context', - path: '/a/belugacdn-app/incidents/declare', - key: -1638987831, - }, - }, - ]); - }); - - it('should register extensions from multiple plugins with multiple placements', () => { - const registry = createPluginExtensionRegistry([ - { - pluginId: 'belugacdn-app', - linkExtensions: [ - { - placement: 'grafana/dashboard/panel/menu', - title: 'Open incident', - description: 'You can create an incident from this context', - path: '/a/belugacdn-app/incidents/declare', - }, - { - placement: 'plugins/grafana-slo-app/slo-breached', - title: 'Open incident', - description: 'You can create an incident from this context', - path: '/a/belugacdn-app/incidents/declare', - }, - ], - }, - { - pluginId: 'grafana-monitoring-app', - linkExtensions: [ - { - placement: 'grafana/dashboard/panel/menu', - title: 'Open Incident', - description: 'You can create an incident from this context', - path: '/a/grafana-monitoring-app/incidents/declare', - }, - ], - }, - ]); - - const numberOfPlacements = Object.keys(registry).length; - const panelExtensions = registry['grafana/dashboard/panel/menu']; - const sloExtensions = registry['plugins/grafana-slo-app/slo-breached']; - - expect(numberOfPlacements).toBe(2); - expect(panelExtensions).toEqual([ - { - configure: undefined, - extension: { - title: 'Open incident', - type: PluginExtensionTypes.link, - description: 'You can create an incident from this context', - path: '/a/belugacdn-app/incidents/declare', - key: -68154691, - }, - }, - { - configure: undefined, - extension: { - title: 'Open Incident', - type: PluginExtensionTypes.link, - description: 'You can create an incident from this context', - path: '/a/grafana-monitoring-app/incidents/declare', - key: -540306829, - }, - }, - ]); - - expect(sloExtensions).toEqual([ - { - configure: undefined, - extension: { - title: 'Open incident', - type: PluginExtensionTypes.link, - description: 'You can create an incident from this context', - path: '/a/belugacdn-app/incidents/declare', - key: -1638987831, - }, - }, - ]); - }); - - it('should register maximum 2 extensions per plugin and placement', () => { - const registry = createPluginExtensionRegistry([ - { - pluginId: 'belugacdn-app', - linkExtensions: [ - { - placement: 'grafana/dashboard/panel/menu', - title: 'Open incident', - description: 'You can create an incident from this context', - path: '/a/belugacdn-app/incidents/declare', - }, - { - placement: 'grafana/dashboard/panel/menu', - title: 'Open incident 2', - description: 'You can create an incident from this context', - path: '/a/belugacdn-app/incidents/declare', - }, - { - placement: 'grafana/dashboard/panel/menu', - title: 'Open incident 3', - description: 'You can create an incident from this context', - path: '/a/belugacdn-app/incidents/declare', - }, - ], - }, - ]); - - const numberOfPlacements = Object.keys(registry).length; - const panelExtensions = registry['grafana/dashboard/panel/menu']; - - expect(numberOfPlacements).toBe(1); - expect(panelExtensions).toEqual([ - { - configure: undefined, - extension: { - title: 'Open incident', - type: PluginExtensionTypes.link, - description: 'You can create an incident from this context', - path: '/a/belugacdn-app/incidents/declare', - key: -68154691, - }, - }, - { - configure: undefined, - extension: { - title: 'Open incident 2', - type: PluginExtensionTypes.link, - description: 'You can create an incident from this context', - path: '/a/belugacdn-app/incidents/declare', - key: -1072147569, - }, - }, - ]); - }); - - it('should not register extensions with invalid path configured', () => { - const registry = createPluginExtensionRegistry([ - { - pluginId: 'belugacdn-app', - linkExtensions: [ - { - placement: 'grafana/dashboard/panel/menu', - title: 'Open incident', - description: 'You can create an incident from this context', - path: '/incidents/declare', - }, - ], - }, - ]); - - const numberOfPlacements = Object.keys(registry).length; - expect(numberOfPlacements).toBe(0); - }); - - it('should wrap configure function with link extension validator', () => { - const registry = createPluginExtensionRegistry([ - { - pluginId: 'belugacdn-app', - linkExtensions: [ - { - placement: 'grafana/dashboard/panel/menu', - title: 'Open incident', - description: 'You can create an incident from this context', - path: '/a/belugacdn-app/incidents/declare', - configure: () => ({}), - }, - ], - }, - ]); - - const extensions = registry['grafana/dashboard/panel/menu']; - const [extension] = extensions; - - const context = {}; - const configurable = { + describe('when registering links', () => { + const placement1 = 'grafana/dashboard/panel/menu'; + const placement2 = 'plugins/grafana-slo-app/slo-breached'; + const pluginId = 'belugacdn-app'; + // Sample link configurations that can be used in tests + const linkConfig = { + placement: placement1, title: 'Open incident', description: 'You can create an incident from this context', path: '/a/belugacdn-app/incidents/declare', }; - extension?.configure?.(context); + it('should register a link extension', () => { + const registry = createPluginExtensionRegistry([ + { + pluginId, + linkExtensions: [linkConfig], + commandExtensions: [], + }, + ]); - expect(validateLink).toBeCalledWith(expect.any(Function), configurable, context); + shouldHaveExtensionsAtPlacement({ configs: [linkConfig], placement: placement1, registry }); + }); + + it('should only register a link extension to a single placement', () => { + const registry = createPluginExtensionRegistry([ + { + pluginId, + linkExtensions: [linkConfig], + commandExtensions: [], + }, + ]); + + shouldHaveNumberOfPlacements(registry, 1); + expect(registry[placement1]).toBeDefined(); + }); + + it('should register link extensions from one plugin with multiple placements', () => { + const registry = createPluginExtensionRegistry([ + { + pluginId, + linkExtensions: [ + { ...linkConfig, placement: placement1 }, + { ...linkConfig, placement: placement2 }, + ], + commandExtensions: [], + }, + ]); + + shouldHaveNumberOfPlacements(registry, 2); + shouldHaveExtensionsAtPlacement({ placement: placement1, configs: [linkConfig], registry }); + shouldHaveExtensionsAtPlacement({ placement: placement2, configs: [linkConfig], registry }); + }); + + it('should register link extensions from multiple plugins with multiple placements', () => { + const registry = createPluginExtensionRegistry([ + { + pluginId, + linkExtensions: [ + { ...linkConfig, placement: placement1 }, + { ...linkConfig, placement: placement2 }, + ], + commandExtensions: [], + }, + { + pluginId: 'grafana-monitoring-app', + linkExtensions: [ + { ...linkConfig, placement: placement1, path: '/a/grafana-monitoring-app/incidents/declare' }, + ], + commandExtensions: [], + }, + ]); + + shouldHaveNumberOfPlacements(registry, 2); + shouldHaveExtensionsAtPlacement({ + placement: placement1, + configs: [linkConfig, { ...linkConfig, path: '/a/grafana-monitoring-app/incidents/declare' }], + registry, + }); + shouldHaveExtensionsAtPlacement({ placement: placement2, configs: [linkConfig], registry }); + }); + + it('should register maximum 2 extensions per plugin and placement', () => { + const registry = createPluginExtensionRegistry([ + { + pluginId, + linkExtensions: [ + { ...linkConfig, title: 'Link 1' }, + { ...linkConfig, title: 'Link 2' }, + { ...linkConfig, title: 'Link 3' }, + ], + commandExtensions: [], + }, + ]); + + shouldHaveNumberOfPlacements(registry, 1); + + // The 3rd link is being ignored + shouldHaveExtensionsAtPlacement({ + placement: linkConfig.placement, + configs: [ + { ...linkConfig, title: 'Link 1' }, + { ...linkConfig, title: 'Link 2' }, + ], + registry, + }); + }); + + it('should not register link extensions with invalid path configured', () => { + const registry = createPluginExtensionRegistry([ + { + pluginId, + linkExtensions: [ + { + ...linkConfig, + path: '/incidents/declare', // invalid path, should always be prefixed with the plugin id + }, + ], + commandExtensions: [], + }, + ]); + + shouldHaveNumberOfPlacements(registry, 0); + }); + + it('should add default configure function when none provided via extension config', () => { + const registry = createPluginExtensionRegistry([ + { + pluginId, + linkExtensions: [linkConfig], + commandExtensions: [], + }, + ]); + + const [configure] = registry[linkConfig.placement]; + const configured = configure(); + + // The default configure() function returns the same extension config + expect(configured).toEqual({ + key: expect.any(Number), + type: PluginExtensionTypes.link, + title: linkConfig.title, + description: linkConfig.description, + path: linkConfig.path, + }); + }); + + it('should wrap the configure function with link extension validator', () => { + const registry = createPluginExtensionRegistry([ + { + pluginId, + linkExtensions: [ + { + ...linkConfig, + configure: () => ({}), + }, + ], + commandExtensions: [], + }, + ]); + + const [configure] = registry[linkConfig.placement]; + const context = {}; + const configurable = { + title: linkConfig.title, + description: linkConfig.description, + path: linkConfig.path, + }; + + configure(context); + + expect(validateLink).toBeCalledWith(expect.any(Function), configurable, context); + }); + + it('should wrap configure function with extension error handling', () => { + const registry = createPluginExtensionRegistry([ + { + pluginId, + linkExtensions: [ + { + ...linkConfig, + configure: () => ({}), + }, + ], + commandExtensions: [], + }, + ]); + + const [configure] = registry[linkConfig.placement]; + const context = {}; + const configurable = { + title: linkConfig.title, + description: linkConfig.description, + path: linkConfig.path, + }; + + configure(context); + + expect(configureErrorHandler).toBeCalledWith(expect.any(Function), configurable, context); + }); + + it('should return undefined if returned by the provided extension config', () => { + const registry = createPluginExtensionRegistry([ + { + pluginId, + linkExtensions: [ + { + ...linkConfig, + configure: () => undefined, + }, + ], + commandExtensions: [], + }, + ]); + + const [configure] = registry[linkConfig.placement]; + const context = {}; + + expect(configure(context)).toBeUndefined(); + }); }); - it('should wrap configure function with extension error handling', () => { - const registry = createPluginExtensionRegistry([ - { - pluginId: 'belugacdn-app', - linkExtensions: [ - { - placement: 'grafana/dashboard/panel/menu', - title: 'Open incident', - description: 'You can create an incident from this context', - path: '/a/belugacdn-app/incidents/declare', - configure: () => ({}), - }, - ], - }, - ]); - - const extensions = registry['grafana/dashboard/panel/menu']; - const [extension] = extensions; - - const context = {}; - const configurable = { + // Command extensions + // ------------------ + describe('when registering commands', () => { + const pluginId = 'belugacdn-app'; + // Sample command configurations to be used in tests + const commandConfig1 = { + placement: 'grafana/dashboard/panel/menu', title: 'Open incident', description: 'You can create an incident from this context', - path: '/a/belugacdn-app/incidents/declare', + handler: () => {}, + }; + const commandConfig2 = { + placement: 'plugins/grafana-slo-app/slo-breached', + title: 'Open incident', + description: 'You can create an incident from this context', + handler: () => {}, }; - extension?.configure?.(context); + it('should register a command extension', () => { + const registry = createPluginExtensionRegistry([ + { + pluginId, + linkExtensions: [], + commandExtensions: [commandConfig1], + }, + ]); - expect(errorHandler).toBeCalledWith(expect.any(Function), configurable, context); + shouldHaveNumberOfPlacements(registry, 1); + shouldHaveExtensionsAtPlacement({ + placement: commandConfig1.placement, + configs: [commandConfig1], + registry, + }); + }); + + it('should register command extensions from a SINGLE PLUGIN with MULTIPLE PLACEMENTS', () => { + const registry = createPluginExtensionRegistry([ + { + pluginId, + linkExtensions: [], + commandExtensions: [commandConfig1, commandConfig2], + }, + ]); + + shouldHaveNumberOfPlacements(registry, 2); + shouldHaveExtensionsAtPlacement({ + placement: commandConfig1.placement, + configs: [commandConfig1], + registry, + }); + shouldHaveExtensionsAtPlacement({ + placement: commandConfig2.placement, + configs: [commandConfig2], + registry, + }); + }); + + it('should register command extensions from MULTIPLE PLUGINS with MULTIPLE PLACEMENTS', () => { + const registry = createPluginExtensionRegistry([ + { + pluginId, + linkExtensions: [], + commandExtensions: [commandConfig1, commandConfig2], + }, + { + pluginId: 'grafana-monitoring-app', + linkExtensions: [], + commandExtensions: [commandConfig1], + }, + ]); + + shouldHaveNumberOfPlacements(registry, 2); + + // Both plugins register commands to the same placement + shouldHaveExtensionsAtPlacement({ + placement: commandConfig1.placement, + configs: [commandConfig1, commandConfig1], + registry, + }); + + // The 'beluga-cdn-app' plugin registers a command to an other placement as well + shouldHaveExtensionsAtPlacement({ + placement: commandConfig2.placement, + configs: [commandConfig2], + registry, + }); + }); + + it('should add default configure function when none is provided via the extension config', () => { + const registry = createPluginExtensionRegistry([ + { + pluginId, + linkExtensions: [], + commandExtensions: [commandConfig1], + }, + ]); + + const [configure] = registry[commandConfig1.placement]; + const configured = configure(); + + // The default configure() function returns the extension config as is + expect(configured).toEqual({ + type: PluginExtensionTypes.command, + key: expect.any(Number), + title: commandConfig1.title, + description: commandConfig1.description, + callHandlerWithContext: expect.any(Function), + }); + }); + + it('should wrap the configure function with error handling', () => { + const registry = createPluginExtensionRegistry([ + { + pluginId, + linkExtensions: [], + commandExtensions: [ + { + ...commandConfig1, + configure: () => ({}), + }, + ], + }, + ]); + + const [configure] = registry[commandConfig1.placement]; + const context = {}; + const configurable = { + title: commandConfig1.title, + description: commandConfig2.description, + }; + + configure(context); + + // The error handler is wrapping (decorating) the configure function, so it can provide standard error messages + expect(configureErrorHandler).toBeCalledWith(expect.any(Function), configurable, context); + }); + + it('should return undefined if returned by the provided extension config', () => { + const registry = createPluginExtensionRegistry([ + { + pluginId, + linkExtensions: [], + commandExtensions: [ + { + ...commandConfig1, + configure: () => undefined, + }, + ], + }, + ]); + + const [configure] = registry[commandConfig1.placement]; + const context = {}; + + expect(configure(context)).toBeUndefined(); + }); + + it('should wrap handler function with extension error handling', () => { + const registry = createPluginExtensionRegistry([ + { + pluginId, + linkExtensions: [], + commandExtensions: [ + { + placement: 'grafana/dashboard/panel/menu', + title: 'Open incident', + description: 'You can create an incident from this context', + handler: () => {}, + configure: () => ({}), + }, + ], + }, + ]); + + const extensions = registry['grafana/dashboard/panel/menu']; + const [configure] = extensions; + const context = {}; + const extension = configure?.(context); + + assertPluginExtensionCommand(extension); + + extension.callHandlerWithContext(); + + expect(commandErrorHandler).toBeCalledWith(expect.any(Function), context); + }); + + it('should wrap handler function with extension error handling when no configure function is added', () => { + const registry = createPluginExtensionRegistry([ + { + pluginId, + linkExtensions: [], + commandExtensions: [ + { + placement: 'grafana/dashboard/panel/menu', + title: 'Open incident', + description: 'You can create an incident from this context', + handler: () => {}, + }, + ], + }, + ]); + + const extensions = registry['grafana/dashboard/panel/menu']; + const [configure] = extensions; + const context = {}; + const extension = configure?.(context); + + assertPluginExtensionCommand(extension); + + extension.callHandlerWithContext(); + + expect(commandErrorHandler).toBeCalledWith(expect.any(Function), context); + }); }); }); + +// Checks the number of total placements in the registry +function shouldHaveNumberOfPlacements(registry: PluginExtensionRegistry, numberOfPlacements: number) { + expect(Object.keys(registry).length).toBe(numberOfPlacements); +} + +// Checks if the registry has exactly the same extensions at the expected placement +function shouldHaveExtensionsAtPlacement({ + configs, + placement, + registry, +}: { + configs: Array; + placement: string; + registry: PluginExtensionRegistry; +}) { + const extensions = registry[placement].map((configure) => configure()); + + expect(extensions).toEqual( + configs.map((extension) => { + // Command extension + if ('handler' in extension) { + return { + key: expect.any(Number), + title: extension.title, + description: extension.description, + type: PluginExtensionTypes.command, + callHandlerWithContext: expect.any(Function), + }; + } + + // Link extension + return { + key: expect.any(Number), + title: extension.title, + description: extension.description, + type: PluginExtensionTypes.link, + path: extension.path, + }; + }) + ); +} diff --git a/public/app/features/plugins/extensions/registryFactory.ts b/public/app/features/plugins/extensions/registryFactory.ts index 22c20660bcb..e2bc4db28ca 100644 --- a/public/app/features/plugins/extensions/registryFactory.ts +++ b/public/app/features/plugins/extensions/registryFactory.ts @@ -1,41 +1,40 @@ import { - AppConfigureExtension, - AppPluginExtensionLink, - AppPluginExtensionLinkConfig, - PluginExtensionLink, + type AppPluginExtensionCommand, + type AppPluginExtensionCommandConfig, + type AppPluginExtensionLink, + type AppPluginExtensionLinkConfig, + type PluginExtension, + type PluginExtensionCommand, + type PluginExtensionLink, PluginExtensionTypes, } from '@grafana/data'; -import type { - PluginExtensionRegistry, - PluginExtensionRegistryItem, - RegistryConfigureExtension, -} from '@grafana/runtime'; +import type { PluginExtensionRegistry, PluginExtensionRegistryItem } from '@grafana/runtime'; -import { PluginPreloadResult } from '../pluginPreloader'; +import type { PluginPreloadResult } from '../pluginPreloader'; -import { createErrorHandling } from './errorHandling'; +import { handleErrorsInHandler, handleErrorsInConfigure } from './errorHandling'; +import { PlacementsPerPlugin } from './placementsPerPlugin'; +import { ConfigureFunc } from './types'; import { createLinkValidator, isValidLinkPath } from './validateLink'; export function createPluginExtensionRegistry(preloadResults: PluginPreloadResult[]): PluginExtensionRegistry { const registry: PluginExtensionRegistry = {}; for (const result of preloadResults) { - const pluginPlacementCount: Record = {}; - const { pluginId, linkExtensions, error } = result; + const { pluginId, linkExtensions, commandExtensions, error } = result; - if (!Array.isArray(linkExtensions) || error) { + if (error) { continue; } - for (const extension of linkExtensions) { - const placement = extension.placement; + const placementsPerPlugin = new PlacementsPerPlugin(); + const configs = [...linkExtensions, ...commandExtensions]; - pluginPlacementCount[placement] = (pluginPlacementCount[placement] ?? 0) + 1; - const item = createRegistryLink(pluginId, extension); + for (const config of configs) { + const placement = config.placement; + const item = createRegistryItem(pluginId, config); - // If there was an issue initialising the plugin, skip adding its extensions to the registry - // or if the plugin already have placed 2 items at the extension point. - if (!item || pluginPlacementCount[placement] > 2) { + if (!item || !placementsPerPlugin.allowedToAdd(placement)) { continue; } @@ -55,41 +54,21 @@ export function createPluginExtensionRegistry(preloadResults: PluginPreloadResul return Object.freeze(registry); } -function createRegistryLink( +function createRegistryItem( pluginId: string, - config: AppPluginExtensionLinkConfig -): PluginExtensionRegistryItem | undefined { - if (!isValidLinkPath(pluginId, config.path)) { - return undefined; + config: AppPluginExtensionCommandConfig | AppPluginExtensionLinkConfig +): PluginExtensionRegistryItem | undefined { + if ('handler' in config) { + return createCommandRegistryItem(pluginId, config); } - - const id = `${pluginId}${config.placement}${config.title}`; - const extension = Object.freeze({ - type: PluginExtensionTypes.link, - title: config.title, - description: config.description, - key: hashKey(id), - path: config.path, - }); - - return Object.freeze({ - extension: extension, - configure: createLinkConfigure(pluginId, config, extension), - }); + return createLinkRegistryItem(pluginId, config); } -function hashKey(key: string): number { - return Array.from(key).reduce((s, c) => (Math.imul(31, s) + c.charCodeAt(0)) | 0, 0); -} - -function createLinkConfigure( +function createCommandRegistryItem( pluginId: string, - config: AppPluginExtensionLinkConfig, - extension: PluginExtensionLink -): RegistryConfigureExtension | undefined { - if (!config.configure) { - return undefined; - } + config: AppPluginExtensionCommandConfig +): PluginExtensionRegistryItem | undefined { + const configure = config.configure ?? defaultConfigure; const options = { pluginId: pluginId, @@ -97,36 +76,102 @@ function createLinkConfigure( logger: console.warn, }; - const mapper = mapToRegistryType(extension); - const validator = createLinkValidator(options); - const errorHandler = createErrorHandling(options); + const catchErrorsInHandler = handleErrorsInHandler(options); + const handler = catchErrorsInHandler(config.handler); - return mapper(validator(errorHandler(config.configure))); -} + const extensionFactory = createCommandFactory(pluginId, config, handler); -function mapToRegistryType( - extension: PluginExtensionLink -): (configure: AppConfigureExtension) => RegistryConfigureExtension { - const configurable: AppPluginExtensionLink = { - title: extension.title, - description: extension.description, - path: extension.path, + const configurable: AppPluginExtensionCommand = { + title: config.title, + description: config.description, }; - return (configure) => { - return function mapper(context: object): PluginExtensionLink | undefined { - const configured = configure(configurable, context); + const mapper = mapToConfigure(extensionFactory, configurable); + const catchErrorsInConfigure = handleErrorsInConfigure(options); - if (!configured) { + return mapper(catchErrorsInConfigure(configure)); +} + +function createLinkRegistryItem( + pluginId: string, + config: AppPluginExtensionLinkConfig +): PluginExtensionRegistryItem | undefined { + if (!isValidLinkPath(pluginId, config.path)) { + return undefined; + } + + const configure = config.configure ?? defaultConfigure; + const options = { pluginId: pluginId, title: config.title, logger: console.warn }; + + const extensionFactory = createLinkFactory(pluginId, config); + + const configurable: AppPluginExtensionLink = { + title: config.title, + description: config.description, + path: config.path, + }; + + const mapper = mapToConfigure(extensionFactory, configurable); + const withConfigureErrorHandling = handleErrorsInConfigure(options); + const validateLink = createLinkValidator(options); + + return mapper(validateLink(withConfigureErrorHandling(configure))); +} + +function createLinkFactory(pluginId: string, config: AppPluginExtensionLinkConfig) { + return (override: Partial, context?: object): PluginExtensionLink => { + const title = override?.title ?? config.title; + const description = override?.description ?? config.description; + const path = override?.path ?? config.path; + + return Object.freeze({ + type: PluginExtensionTypes.link, + title: title, + description: description, + path: path, + key: hashKey(`${pluginId}${config.placement}${title}`), + }); + }; +} + +function createCommandFactory( + pluginId: string, + config: AppPluginExtensionCommandConfig, + handler: (context?: object) => void +) { + return (override: Partial, context?: object): PluginExtensionCommand => { + const title = override?.title ?? config.title; + const description = override?.description ?? config.description; + + return Object.freeze({ + type: PluginExtensionTypes.command, + title: title, + description: description, + key: hashKey(`${pluginId}${config.placement}${title}`), + callHandlerWithContext: () => handler(context), + }); + }; +} + +function mapToConfigure( + commandFactory: (override: Partial, context?: object) => T | undefined, + configurable: C +): (configure: ConfigureFunc) => PluginExtensionRegistryItem { + return (configure) => { + return function mapToExtension(context?: object): T | undefined { + const override = configure(configurable, context); + if (!override) { return undefined; } - - return { - ...extension, - title: configured.title ?? extension.title, - description: configured.description ?? extension.description, - path: configured.path ?? extension.path, - }; + return commandFactory(override, context); }; }; } + +function hashKey(key: string): number { + return Array.from(key).reduce((s, c) => (Math.imul(31, s) + c.charCodeAt(0)) | 0, 0); +} + +function defaultConfigure() { + return {}; +} diff --git a/public/app/features/plugins/extensions/types.ts b/public/app/features/plugins/extensions/types.ts new file mode 100644 index 00000000000..fa04cb7b346 --- /dev/null +++ b/public/app/features/plugins/extensions/types.ts @@ -0,0 +1,4 @@ +import type { AppPluginExtensionCommandConfig } from '@grafana/data'; + +export type CommandHandlerFunc = AppPluginExtensionCommandConfig['handler']; +export type ConfigureFunc = (extension: T, context?: object) => Partial | undefined; diff --git a/public/app/features/plugins/extensions/validateLink.ts b/public/app/features/plugins/extensions/validateLink.ts index 6d74733f3cd..15b61399e03 100644 --- a/public/app/features/plugins/extensions/validateLink.ts +++ b/public/app/features/plugins/extensions/validateLink.ts @@ -1,4 +1,6 @@ -import type { AppConfigureExtension, AppPluginExtensionLink } from '@grafana/data'; +import type { AppPluginExtensionLink } from '@grafana/data'; + +import type { ConfigureFunc } from './types'; type Options = { pluginId: string; @@ -9,7 +11,7 @@ type Options = { export function createLinkValidator(options: Options) { const { pluginId, title, logger } = options; - return (configure: AppConfigureExtension): AppConfigureExtension => { + return (configure: ConfigureFunc): ConfigureFunc => { return function validateLink(link, context) { const configured = configure(link, context); diff --git a/public/app/features/plugins/pluginPreloader.ts b/public/app/features/plugins/pluginPreloader.ts index 13dc6e28111..dbed70d0316 100644 --- a/public/app/features/plugins/pluginPreloader.ts +++ b/public/app/features/plugins/pluginPreloader.ts @@ -1,4 +1,4 @@ -import { AppPluginExtensionLinkConfig } from '@grafana/data'; +import type { AppPluginExtensionCommandConfig, AppPluginExtensionLinkConfig } from '@grafana/data'; import type { AppPluginConfig } from '@grafana/runtime'; import * as pluginLoader from './plugin_loader'; @@ -6,6 +6,7 @@ import * as pluginLoader from './plugin_loader'; export type PluginPreloadResult = { pluginId: string; linkExtensions: AppPluginExtensionLinkConfig[]; + commandExtensions: AppPluginExtensionCommandConfig[]; error?: unknown; }; @@ -18,10 +19,10 @@ async function preload(config: AppPluginConfig): Promise { const { path, version, id: pluginId } = config; try { const { plugin } = await pluginLoader.importPluginModule(path, version); - const { linkExtensions = [] } = plugin; - return { pluginId, linkExtensions }; + const { linkExtensions = [], commandExtensions = [] } = plugin; + return { pluginId, linkExtensions, commandExtensions }; } catch (error) { console.error(`[Plugins] Failed to preload plugin: ${path} (version: ${version})`, error); - return { pluginId, linkExtensions: [], error }; + return { pluginId, linkExtensions: [], commandExtensions: [], error }; } } From ee38bbe03049279ad52679d760eb4dac4db7fd30 Mon Sep 17 00:00:00 2001 From: Joey <90795735+joey-grafana@users.noreply.github.com> Date: Wed, 8 Mar 2023 13:29:28 +0000 Subject: [PATCH 064/288] Phlare: Allow variables in labelSelector (in query) (#64324) applyTemplateVariables and tests --- .../datasource/phlare/datasource.test.ts | 42 +++++++++++++++++++ .../plugins/datasource/phlare/datasource.ts | 22 ++++++++-- 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/public/app/plugins/datasource/phlare/datasource.test.ts b/public/app/plugins/datasource/phlare/datasource.test.ts index 10f6d6d1469..f11c4ebc9bb 100644 --- a/public/app/plugins/datasource/phlare/datasource.test.ts +++ b/public/app/plugins/datasource/phlare/datasource.test.ts @@ -1,5 +1,7 @@ import { AbstractLabelOperator, DataSourceInstanceSettings, PluginMetaInfo, PluginType } from '@grafana/data'; +import { TemplateSrv } from 'app/features/templating/template_srv'; +import { defaultPhlareQueryType } from './dataquery.gen'; import { PhlareDataSource } from './datasource'; describe('Phlare data source', () => { @@ -45,8 +47,48 @@ describe('Phlare data source', () => { ]); }); }); + + describe('applyTemplateVariables', () => { + const interpolationVar = '$interpolationVar'; + const interpolationText = 'interpolationText'; + const noInterpolation = 'noInterpolation'; + + it('should not update labelSelector if there are no template variables', () => { + const templateSrv = new TemplateSrv(); + templateSrv.replace = jest.fn((query: string): string => { + return query.replace(/\$interpolationVar/g, interpolationText); + }); + ds = new PhlareDataSource(defaultSettings, templateSrv); + const query = ds.applyTemplateVariables(defaultQuery(`{${noInterpolation}}`), {}); + expect(templateSrv.replace).toBeCalledTimes(1); + expect(query.labelSelector).toBe(`{${noInterpolation}}`); + }); + + it('should update labelSelector if there are template variables', () => { + const templateSrv = new TemplateSrv(); + templateSrv.replace = jest.fn((query: string): string => { + return query.replace(/\$interpolationVar/g, interpolationText); + }); + ds = new PhlareDataSource(defaultSettings, templateSrv); + const query = ds.applyTemplateVariables(defaultQuery(`{${interpolationVar}="${interpolationVar}"}`), { + interpolationVar: { text: interpolationText, value: interpolationText }, + }); + expect(templateSrv.replace).toBeCalledTimes(1); + expect(query.labelSelector).toBe(`{${interpolationText}="${interpolationText}"}`); + }); + }); }); +const defaultQuery = (query: string) => { + return { + refId: 'x', + groupBy: [], + labelSelector: query, + profileTypeId: '', + queryType: defaultPhlareQueryType, + }; +}; + const defaultSettings: DataSourceInstanceSettings = { id: 0, uid: 'phlare', diff --git a/public/app/plugins/datasource/phlare/datasource.ts b/public/app/plugins/datasource/phlare/datasource.ts index 95da2c417de..cb5d7c93c7f 100644 --- a/public/app/plugins/datasource/phlare/datasource.ts +++ b/public/app/plugins/datasource/phlare/datasource.ts @@ -1,8 +1,14 @@ import Prism, { Grammar } from 'prismjs'; import { Observable, of } from 'rxjs'; -import { AbstractQuery, DataQueryRequest, DataQueryResponse, DataSourceInstanceSettings } from '@grafana/data'; -import { DataSourceWithBackend } from '@grafana/runtime'; +import { + AbstractQuery, + DataQueryRequest, + DataQueryResponse, + DataSourceInstanceSettings, + ScopedVars, +} from '@grafana/data'; +import { DataSourceWithBackend, getTemplateSrv, TemplateSrv } from '@grafana/runtime'; import { extractLabelMatchers, toPromLikeExpr } from '../prometheus/language_utils'; @@ -10,7 +16,10 @@ import { normalizeQuery } from './QueryEditor/QueryEditor'; import { PhlareDataSourceOptions, Query, ProfileTypeMessage, SeriesMessage } from './types'; export class PhlareDataSource extends DataSourceWithBackend { - constructor(instanceSettings: DataSourceInstanceSettings) { + constructor( + instanceSettings: DataSourceInstanceSettings, + private readonly templateSrv: TemplateSrv = getTemplateSrv() + ) { super(instanceSettings); } @@ -49,6 +58,13 @@ export class PhlareDataSource extends DataSourceWithBackend { return abstractQueries.map((abstractQuery) => this.importFromAbstractQuery(abstractQuery)); } From af9a0dbe393af082c595fc76a6316a06dfb525fb Mon Sep 17 00:00:00 2001 From: Isabella Siu Date: Wed, 8 Mar 2023 08:56:44 -0500 Subject: [PATCH 065/288] CloudWatch Logs: Fix interpolation of scoped variables in queryString (#64267) --- .../cloudwatch/__mocks__/Request.ts | 2 +- .../CloudWatchLogsQueryRunner.test.ts | 21 ++++++++++++++++++- .../query-runner/CloudWatchLogsQueryRunner.ts | 2 +- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/public/app/plugins/datasource/cloudwatch/__mocks__/Request.ts b/public/app/plugins/datasource/cloudwatch/__mocks__/Request.ts index 3366db848c1..dfcdfbeff0d 100644 --- a/public/app/plugins/datasource/cloudwatch/__mocks__/Request.ts +++ b/public/app/plugins/datasource/cloudwatch/__mocks__/Request.ts @@ -24,7 +24,7 @@ export const LogsRequestMock: DataQueryRequest = { requestId: '', interval: '', intervalMs: 0, - scopedVars: {}, + scopedVars: { __interval: { value: '20s' } }, timezone: '', app: '', startTime: 0, diff --git a/public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchLogsQueryRunner.test.ts b/public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchLogsQueryRunner.test.ts index fbb7910545b..79e379a79ad 100644 --- a/public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchLogsQueryRunner.test.ts +++ b/public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchLogsQueryRunner.test.ts @@ -216,6 +216,16 @@ describe('CloudWatchLogsQueryRunner', () => { expression: `fields @timestamp, @message | sort @timestamp desc | limit 1`, }; + const logsScopedVarQuery: CloudWatchLogsQuery = { + queryMode: 'Logs', + logGroups: [{ arn: logGroupNamesVariable.name, name: logGroupNamesVariable.name }], + hide: false, + id: '', + region: '$' + regionVariable.name, + refId: 'A', + expression: `stats count(*) by queryType, bin($__interval)`, + }; + describe('handleLogQueries', () => { it('should map log queries to start query requests correctly', async () => { const { runner } = setupMockedLogsQueryRunner({ @@ -229,7 +239,9 @@ describe('CloudWatchLogsQueryRunner', () => { }, }); const spy = jest.spyOn(runner, 'makeLogActionRequest'); - await lastValueFrom(runner.handleLogQueries([legacyLogGroupNamesQuery, logGroupNamesQuery], LogsRequestMock)); + await lastValueFrom( + runner.handleLogQueries([legacyLogGroupNamesQuery, logGroupNamesQuery, logsScopedVarQuery], LogsRequestMock) + ); const startQueryRequests: StartQueryRequest[] = [ { queryString: `fields @timestamp, @message | sort @timestamp desc | limit ${limitVariable.current.value}`, @@ -251,6 +263,13 @@ describe('CloudWatchLogsQueryRunner', () => { refId: legacyLogGroupNamesQuery.refId, region: regionVariable.current.value as string, }, + { + queryString: `stats count(*) by queryType, bin(20s)`, + logGroupNames: [], + logGroups: [...(logGroupNamesVariable.current.value as string[]).map((v) => ({ arn: v, name: v }))], + refId: legacyLogGroupNamesQuery.refId, + region: regionVariable.current.value as string, + }, ]; expect(spy).toHaveBeenNthCalledWith(1, 'StartQuery', startQueryRequests); }); diff --git a/public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchLogsQueryRunner.ts b/public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchLogsQueryRunner.ts index 3b09feec20b..108a620aa09 100644 --- a/public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchLogsQueryRunner.ts +++ b/public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchLogsQueryRunner.ts @@ -103,7 +103,7 @@ export class CloudWatchLogsQueryRunner extends CloudWatchRequest { return { refId: target.refId, region: this.templateSrv.replace(this.getActualRegion(target.region)), - queryString: this.templateSrv.replace(target.expression || ''), + queryString: this.templateSrv.replace(target.expression || '', options.scopedVars), logGroups, logGroupNames, }; From 09341a0cd625eb155d5a2f42a103f43dcf41ae27 Mon Sep 17 00:00:00 2001 From: Jev Forsberg <46619047+baldm0mma@users.noreply.github.com> Date: Wed, 8 Mar 2023 06:57:01 -0700 Subject: [PATCH 066/288] TablePanel: fix footer bug; no footer calculated values after "hidden" column override (#64269) * baldm0mma/bug/tableFooter/ first commit * baldm0mma/bug/tableFooter/ investigation annotations * baldm0mma/bug/tableFooter/ solution * baldm0mma/bug/tableFooter/ rem conlogs in footerrow.txs * baldm0mma/bug/tableFooter/ rem conlogs in tablepanel.tsx * baldm0mma/bug/tableFooter/ rem conlgs in table.tsx * baldm0mma/bug/tableFooter/ rem conlogs in utils.ts * baldm0mma/bug/tableFooter/ reset return in footerRow.tsx * baldm0mma/bug/tableFooter/ rem unused anno in table.tsx * baldm0mma/bug/tableFooter/ rem unsed annos in utils.ts * baldm0mma/bug/tableFooter/ add addMissingColumnIndex * baldm0mma/bug/tableFooter/ add annos * baldm0mma/bug/tableFooter/ add annos * baldm0mma/bug/tableFooter/ / add annos * baldm0mma/bug/tableFooterFix/ update annos * baldm0mma/bug/tableFooterFix/ update spelling in utils * baldm0mma/bug/tableFooterFix/ rem unused condition in utils.ts * baldm0mma/bug/tableFooterFix/ update anno in utils.ts * Wrap comments and fix misspelling. * baldm0mma/bug/tableFooterFix/ add TSDoc * baldm0mma/bug/tableFooterFix/ update annotations in utils.ts --------- Co-authored-by: Kyle Cunningham --- .../grafana-ui/src/components/Table/utils.ts | 80 ++++++++++++++++--- 1 file changed, 68 insertions(+), 12 deletions(-) diff --git a/packages/grafana-ui/src/components/Table/utils.ts b/packages/grafana-ui/src/components/Table/utils.ts index 5f794438b66..d4ed918755b 100644 --- a/packages/grafana-ui/src/components/Table/utils.ts +++ b/packages/grafana-ui/src/components/Table/utils.ts @@ -99,7 +99,6 @@ export function getColumns( for (const [fieldIndex, field] of data.fields.entries()) { const fieldTableOptions = (field.config.custom || {}) as TableFieldOptions; - if (fieldTableOptions.hidden) { continue; } @@ -319,43 +318,73 @@ function toNumber(value: any): number { } export function getFooterItems( - filterFields: Array<{ id: string; field: Field }>, + filterFields: Array<{ id: string; field?: Field } | undefined>, values: any[number], options: TableFooterCalc, theme2: GrafanaTheme2 ): FooterItem[] { /* - Here, `filterFields` is passed to as the `headerGroups[0].headers` array that was destrcutured from the `useTable` hook. - Unfortunately, since the `headerGroups` object is data based ONLY on the rendered "non-hidden" column headers, - it will NOT include the Row Number column if it has been toggled off. This will shift the rendering of the footer left 1 column, - creating an off-by-one issue. This is why we test for a `field.id` of "0". If the condition is truthy, the togglable Row Number column is being rendered, - and we can proceed normally. If not, we must add the field data in its place so that the footer data renders in the expected column. + Here, `filterFields` is passed as the `headerGroups[0].headers` array + that was destructured from the `useTable` hook. Unfortunately, since + the `headerGroups` object is data based ONLY on the rendered "non-hidden" + column headers, it will NOT include the Row Number column if it has been + toggled off. This will shift the rendering of the footer left 1 column, + creating an off-by-one issue. This is why we test for a `field.id` of "0". + If the condition is truthy, the togglable Row Number column is being rendered, + and we can proceed normally. If not, we must add the field data in its place + so that the footer data renders in the expected column. */ - if (!filterFields.some((field) => field.id === '0')) { + if (!filterFields.some((field) => field?.id === '0')) { const length = values.length; // Build the additional field that will correct the off-by-one footer issue. const fieldToAdd = { id: '0', field: buildFieldsForOptionalRowNums(length) }; filterFields = [fieldToAdd, ...filterFields]; } + /* + The FooterItems[] are calculated using both the `headerGroups[0].headers` + (filterFields) and `rows` (values) destructured from the useTable() hook. + This cacluation is based on the data from each index in `filterFields` + array as well as the corresponding index in the `values` array. + When the user hides a column through an override, the getColumns() + hook is invoked, removes said hidden column, sends the updated column + data to the useTable() hook, which then builds `headerGroups[0].headers` + without the hidden column. However, it doesn't remove the hidden column + from the `row` data, instead it substututes the hidden column row data + with an `undefined` value. Therefore, the `row` array length never changes, + despite the `headerGroups[0].headers` length changing at every column removal. + This makes all footer reduce calculations AFTER the first hidden column + in the `headerGroups[0].headers` break, since the indexing of both + arrays is no longer in parity. + + So, here we simply recursively test for the "hidden" columns + from `headerGroups[0].headers`. Each column has an ID property that corresponds + to its own index, therefore if (`filterField.id` !== `String(index)`), + we know there is one or more hidden columns; at which point we update + the index with an ersatz placeholder with just an `id` property. + */ + addMissingColumnIndex(filterFields); + return filterFields.map((data, i) => { - if (data.field.type !== FieldType.number) { + // Then test for numerical data - this will filter out placeholder `filterFields` as well. + if (data?.field?.type !== FieldType.number) { // Show the reducer type ("Total", "Range", "Count", "Delta", etc) in the first non "Row Number" column, only if it cannot be numerically reduced. if (i === 1 && options.reducer && options.reducer.length > 0) { const reducer = fieldReducers.get(options.reducer[0]); return reducer.name; } - // Otherwise return `undefined`, which will render an . + // Render an . return undefined; } let newField = clone(data.field); - newField.values = new ArrayVector(values[i]); + newField.values = new ArrayVector(values[data.id]); newField.state = undefined; data.field = newField; + if (options.fields && options.fields.length > 0) { - const f = options.fields.find((f) => f === data.field.name); + const f = options.fields.find((f) => f === data?.field?.name); if (f) { return getFormattedValue(data.field, options.reducer, theme2); } @@ -477,3 +506,30 @@ export const defaultRowNumberColumnFieldData: Omit = { }, }, }; + +/** + * This recurses through an array of `filterFields` (Array<{ id: string; field?: Field } | undefined>) + * and adds back the missing indecies that are removed due to hiding a column through an panel override. + * This is necessary to create Array.length parity between the `filterFields` array and the `values` array (any[number]), + * since the footer value calculations are based on the corresponding index values of both arrays. + * + * @remarks + * This function uses the splice() method, and therefore mutates the array. + * + * @param columns - An array of `filterFields` (Array<{ id: string; field?: Field } | undefined>). + * @returns void; this function returns nothing; it only mutates values as a side effect. + */ +function addMissingColumnIndex(columns: Array<{ id: string; field?: Field } | undefined>): void { + const missingIndex = columns.findIndex((field, index) => field?.id !== String(index)); + + // Base case + if (missingIndex === -1) { + return; + } + + // Splice in missing column + columns.splice(missingIndex, 0, { id: String(missingIndex) }); + + // Recurse + addMissingColumnIndex(columns); +} From d44dc0f10079c7973c2799586b774acff2a4c3ef Mon Sep 17 00:00:00 2001 From: Levente Balogh Date: Wed, 8 Mar 2023 15:44:48 +0100 Subject: [PATCH 067/288] Plugins: Allow command extensions to open modals (#64029) feat: make it possible to open modals from commands --- packages/grafana-data/src/types/app.ts | 13 ++- .../plugins/extensions/getModalWrapper.tsx | 27 ++++++ .../extensions/registryFactory.test.ts | 84 ++++++++++++------- .../plugins/extensions/registryFactory.ts | 18 +++- 4 files changed, 111 insertions(+), 31 deletions(-) create mode 100644 public/app/features/plugins/extensions/getModalWrapper.tsx diff --git a/packages/grafana-data/src/types/app.ts b/packages/grafana-data/src/types/app.ts index 49e6ef78883..a37a9c85fb3 100644 --- a/packages/grafana-data/src/types/app.ts +++ b/packages/grafana-data/src/types/app.ts @@ -56,6 +56,17 @@ export interface AppPluginMeta extends PluginMeta */ export type AppPluginExtensionLink = Pick; +// A list of helpers that can be used in the command handler +export type AppPluginExtensionCommandHelpers = { + // Opens a modal dialog and renders the provided React component inside it + openModal: (options: { + // The title of the modal + title: string; + // A React element that will be rendered inside the modal + body: React.ElementType<{ onDismiss?: () => void }>; + }) => void; +}; + export type AppPluginExtensionCommand = Pick; export type AppPluginExtensionLinkConfig = { @@ -70,7 +81,7 @@ export type AppPluginExtensionCommandConfig = { title: string; description: string; placement: string; - handler: (context?: C) => void; + handler: (context?: C, helpers?: AppPluginExtensionCommandHelpers) => void; configure?: (extension: AppPluginExtensionCommand, context?: C) => Partial | undefined; }; diff --git a/public/app/features/plugins/extensions/getModalWrapper.tsx b/public/app/features/plugins/extensions/getModalWrapper.tsx new file mode 100644 index 00000000000..8961ecdf49f --- /dev/null +++ b/public/app/features/plugins/extensions/getModalWrapper.tsx @@ -0,0 +1,27 @@ +import React from 'react'; + +import { AppPluginExtensionCommandHelpers } from '@grafana/data'; +import { Modal } from '@grafana/ui'; + +export type ModalWrapperProps = { + onDismiss: () => void; +}; + +// Wraps a component with a modal. +// This way we can make sure that the modal is closable, and we also make the usage simpler. +export const getModalWrapper = ({ + // The title of the modal (appears in the header) + title, + // A component that serves the body of the modal + body: Body, +}: Parameters[0]) => { + const ModalWrapper = ({ onDismiss }: ModalWrapperProps) => { + return ( + + + + ); + }; + + return ModalWrapper; +}; diff --git a/public/app/features/plugins/extensions/registryFactory.test.ts b/public/app/features/plugins/extensions/registryFactory.test.ts index 903a2e09ad4..891594dba9d 100644 --- a/public/app/features/plugins/extensions/registryFactory.test.ts +++ b/public/app/features/plugins/extensions/registryFactory.test.ts @@ -269,18 +269,22 @@ describe('createPluginExtensionRegistry()', () => { describe('when registering commands', () => { const pluginId = 'belugacdn-app'; // Sample command configurations to be used in tests - const commandConfig1 = { - placement: 'grafana/dashboard/panel/menu', - title: 'Open incident', - description: 'You can create an incident from this context', - handler: () => {}, - }; - const commandConfig2 = { - placement: 'plugins/grafana-slo-app/slo-breached', - title: 'Open incident', - description: 'You can create an incident from this context', - handler: () => {}, - }; + let commandConfig1: AppPluginExtensionCommandConfig, commandConfig2: AppPluginExtensionCommandConfig; + + beforeEach(() => { + commandConfig1 = { + placement: 'grafana/dashboard/panel/menu', + title: 'Open incident', + description: 'You can create an incident from this context', + handler: jest.fn(), + }; + commandConfig2 = { + placement: 'plugins/grafana-slo-app/slo-breached', + title: 'Open incident', + description: 'You can create an incident from this context', + handler: jest.fn(), + }; + }); it('should register a command extension', () => { const registry = createPluginExtensionRegistry([ @@ -428,26 +432,25 @@ describe('createPluginExtensionRegistry()', () => { linkExtensions: [], commandExtensions: [ { - placement: 'grafana/dashboard/panel/menu', - title: 'Open incident', - description: 'You can create an incident from this context', - handler: () => {}, + ...commandConfig1, configure: () => ({}), }, ], }, ]); - const extensions = registry['grafana/dashboard/panel/menu']; + const extensions = registry[commandConfig1.placement]; const [configure] = extensions; const context = {}; - const extension = configure?.(context); + const extension = configure(context); assertPluginExtensionCommand(extension); extension.callHandlerWithContext(); + expect(commandErrorHandler).toBeCalledTimes(1); expect(commandErrorHandler).toBeCalledWith(expect.any(Function), context); + expect(commandConfig1.handler).toBeCalledTimes(1); }); it('should wrap handler function with extension error handling when no configure function is added', () => { @@ -455,27 +458,52 @@ describe('createPluginExtensionRegistry()', () => { { pluginId, linkExtensions: [], - commandExtensions: [ - { - placement: 'grafana/dashboard/panel/menu', - title: 'Open incident', - description: 'You can create an incident from this context', - handler: () => {}, - }, - ], + commandExtensions: [commandConfig1], }, ]); - const extensions = registry['grafana/dashboard/panel/menu']; + const extensions = registry[commandConfig1.placement]; const [configure] = extensions; const context = {}; - const extension = configure?.(context); + const extension = configure(context); assertPluginExtensionCommand(extension); extension.callHandlerWithContext(); + expect(commandErrorHandler).toBeCalledTimes(1); expect(commandErrorHandler).toBeCalledWith(expect.any(Function), context); + expect(commandConfig1.handler).toBeCalledTimes(1); + }); + + it('should call the `handler()` function with the context and a `helpers` object', () => { + const registry = createPluginExtensionRegistry([ + { + pluginId, + linkExtensions: [], + commandExtensions: [commandConfig1, { ...commandConfig2, configure: () => ({}) }], + }, + ]); + + const context = {}; + const command1 = registry[commandConfig1.placement][0](context); + const command2 = registry[commandConfig2.placement][0](context); + + assertPluginExtensionCommand(command1); + assertPluginExtensionCommand(command2); + + command1.callHandlerWithContext(); + command2.callHandlerWithContext(); + + expect(commandConfig1.handler).toBeCalledTimes(1); + expect(commandConfig1.handler).toBeCalledWith(context, { + openModal: expect.any(Function), + }); + + expect(commandConfig2.handler).toBeCalledTimes(1); + expect(commandConfig2.handler).toBeCalledWith(context, { + openModal: expect.any(Function), + }); }); }); }); diff --git a/public/app/features/plugins/extensions/registryFactory.ts b/public/app/features/plugins/extensions/registryFactory.ts index e2bc4db28ca..807d7f6d7b7 100644 --- a/public/app/features/plugins/extensions/registryFactory.ts +++ b/public/app/features/plugins/extensions/registryFactory.ts @@ -1,6 +1,7 @@ import { type AppPluginExtensionCommand, type AppPluginExtensionCommandConfig, + type AppPluginExtensionCommandHelpers, type AppPluginExtensionLink, type AppPluginExtensionLinkConfig, type PluginExtension, @@ -9,12 +10,15 @@ import { PluginExtensionTypes, } from '@grafana/data'; import type { PluginExtensionRegistry, PluginExtensionRegistryItem } from '@grafana/runtime'; +import appEvents from 'app/core/app_events'; +import { ShowModalReactEvent } from 'app/types/events'; import type { PluginPreloadResult } from '../pluginPreloader'; import { handleErrorsInHandler, handleErrorsInConfigure } from './errorHandling'; +import { getModalWrapper } from './getModalWrapper'; import { PlacementsPerPlugin } from './placementsPerPlugin'; -import { ConfigureFunc } from './types'; +import { CommandHandlerFunc, ConfigureFunc } from './types'; import { createLinkValidator, isValidLinkPath } from './validateLink'; export function createPluginExtensionRegistry(preloadResults: PluginPreloadResult[]): PluginExtensionRegistry { @@ -69,6 +73,7 @@ function createCommandRegistryItem( config: AppPluginExtensionCommandConfig ): PluginExtensionRegistryItem | undefined { const configure = config.configure ?? defaultConfigure; + const helpers = getCommandHelpers(); const options = { pluginId: pluginId, @@ -76,8 +81,9 @@ function createCommandRegistryItem( logger: console.warn, }; + const handlerWithHelpers: CommandHandlerFunc = (context) => config.handler(context, helpers); const catchErrorsInHandler = handleErrorsInHandler(options); - const handler = catchErrorsInHandler(config.handler); + const handler = catchErrorsInHandler(handlerWithHelpers); const extensionFactory = createCommandFactory(pluginId, config, handler); @@ -175,3 +181,11 @@ function hashKey(key: string): number { function defaultConfigure() { return {}; } + +function getCommandHelpers() { + const openModal: AppPluginExtensionCommandHelpers['openModal'] = ({ title, body }) => { + appEvents.publish(new ShowModalReactEvent({ component: getModalWrapper({ title, body }) })); + }; + + return { openModal }; +} From 5ba2ca83d5ee92ea6a6af4cab566830cbbb28802 Mon Sep 17 00:00:00 2001 From: Joey <90795735+joey-grafana@users.noreply.github.com> Date: Wed, 8 Mar 2023 14:54:42 +0000 Subject: [PATCH 068/288] Assign Traces & Profiling feature flags to team (#64406) --- pkg/services/featuremgmt/codeowners.go | 23 ++++++++++---------- pkg/services/featuremgmt/registry.go | 3 +++ pkg/services/featuremgmt/toggles_gen_test.go | 3 --- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/pkg/services/featuremgmt/codeowners.go b/pkg/services/featuremgmt/codeowners.go index a31e27a7d7c..84d3cc267f8 100644 --- a/pkg/services/featuremgmt/codeowners.go +++ b/pkg/services/featuremgmt/codeowners.go @@ -5,15 +5,16 @@ package featuremgmt type codeowner string const ( - grafanaAppPlatformSquad codeowner = "@grafana/grafana-app-platform-squad" - grafanaDashboardsSquad codeowner = "@grafana/dashboards-squad" - grafanaExploreSquad codeowner = "@grafana/explore-squad" - grafanaBiSquad codeowner = "@grafana/grafana-bi-squad" - grafanaDatavizSquad codeowner = "@grafana/dataviz-squad" - grafanaUserEssentialsSquad codeowner = "@grafana/user-essentials" - grafanaBackendPlatformSquad codeowner = "@grafana/backend-platform" - grafanaPluginsPlatformSquad codeowner = "@grafana/plugins-platform-backend" - grafanaAsCodeSquad codeowner = "@grafana/grafana-as-code" - grafanaAuthnzSquad codeowner = "@grafana/grafana-authnz-team" - grafanaObservabilityLogsSquad codeowner = "@grafana/observability-logs" + grafanaAppPlatformSquad codeowner = "@grafana/grafana-app-platform-squad" + grafanaDashboardsSquad codeowner = "@grafana/dashboards-squad" + grafanaExploreSquad codeowner = "@grafana/explore-squad" + grafanaBiSquad codeowner = "@grafana/grafana-bi-squad" + grafanaDatavizSquad codeowner = "@grafana/dataviz-squad" + grafanaUserEssentialsSquad codeowner = "@grafana/user-essentials" + grafanaBackendPlatformSquad codeowner = "@grafana/backend-platform" + grafanaPluginsPlatformSquad codeowner = "@grafana/plugins-platform-backend" + grafanaAsCodeSquad codeowner = "@grafana/grafana-as-code" + grafanaAuthnzSquad codeowner = "@grafana/grafana-authnz-team" + grafanaObservabilityLogsSquad codeowner = "@grafana/observability-logs" + grafanaObservabilityTracesAndProfilingSquad codeowner = "@grafana/observability-traces-and-profiling" ) diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 81a7c443b4d..5fff9cef8c9 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -153,6 +153,7 @@ var ( Description: "Shows the new trace view design", State: FeatureStateAlpha, FrontendOnly: true, + Owner: grafanaObservabilityTracesAndProfilingSquad, }, { Name: "correlations", @@ -177,6 +178,7 @@ var ( Description: "Enable trace to metrics links", State: FeatureStateAlpha, FrontendOnly: true, + Owner: grafanaObservabilityTracesAndProfilingSquad, }, { Name: "newDBLibrary", @@ -405,6 +407,7 @@ var ( Description: "Enables the 'TraceQL Search' tab for the Tempo datasource which provides a UI to generate TraceQL queries", State: FeatureStateAlpha, FrontendOnly: true, + Owner: grafanaObservabilityTracesAndProfilingSquad, }, { Name: "prometheusMetricEncyclopedia", diff --git a/pkg/services/featuremgmt/toggles_gen_test.go b/pkg/services/featuremgmt/toggles_gen_test.go index acfb3a15f89..c6832e9bbe7 100644 --- a/pkg/services/featuremgmt/toggles_gen_test.go +++ b/pkg/services/featuremgmt/toggles_gen_test.go @@ -48,9 +48,7 @@ func TestFeatureToggleFiles(t *testing.T) { "prometheusAzureOverrideAudience": true, "featureHighlights": true, "tracing": true, - "newTraceView": true, "cloudWatchDynamicLabels": true, - "traceToMetrics": true, "validateDashboardsOnSave": true, "prometheusWideSeries": true, "disableSecretsCompatibility": true, @@ -67,7 +65,6 @@ func TestFeatureToggleFiles(t *testing.T) { "alertingBacktesting": true, "alertingNoNormalState": true, "individualCookiePreferences": true, - "traceqlSearch": true, } t.Run("all new features should have an owner", func(t *testing.T) { From 3292cb86ae3d5a2776202538d101fb667bbc696c Mon Sep 17 00:00:00 2001 From: Matias Chomicki Date: Wed, 8 Mar 2023 16:08:32 +0100 Subject: [PATCH 069/288] Log Details: Display all field options and remove "show more" (#64334) * Log details: stop hiding field options * back to iconbutton --------- Co-authored-by: Sven Grossmann --- .../logs/components/LogDetailsRow.tsx | 111 ++++-------------- 1 file changed, 22 insertions(+), 89 deletions(-) diff --git a/public/app/features/logs/components/LogDetailsRow.tsx b/public/app/features/logs/components/LogDetailsRow.tsx index 924f9ca049d..f1230d64001 100644 --- a/public/app/features/logs/components/LogDetailsRow.tsx +++ b/public/app/features/logs/components/LogDetailsRow.tsx @@ -5,7 +5,7 @@ import React, { PureComponent } from 'react'; import { CoreApp, Field, GrafanaTheme2, LinkModel, LogLabelStatsModel, LogRowModel } from '@grafana/data'; import { reportInteraction } from '@grafana/runtime'; -import { ClipboardButton, DataLinkButton, Themeable2, ToolbarButton, ToolbarButtonRow, withTheme2 } from '@grafana/ui'; +import { ClipboardButton, DataLinkButton, IconButton, Themeable2, withTheme2 } from '@grafana/ui'; import { LogLabelStats } from './LogLabelStats'; import { getLogRowStyles } from './getLogRowStyles'; @@ -36,23 +36,10 @@ interface State { const getStyles = memoizeOne((theme: GrafanaTheme2) => { return { - noHoverBackground: css` - label: noHoverBackground; - :hover { - background-color: transparent; - } - `, - hoverCursor: css` - label: hoverCursor; - cursor: pointer; - `, wordBreakAll: css` label: wordBreakAll; word-break: break-all; `, - showingField: css` - color: ${theme.colors.primary.text}; - `, copyButton: css` & > button { color: ${theme.colors.text.secondary}; @@ -77,38 +64,6 @@ const getStyles = memoizeOne((theme: GrafanaTheme2) => { label: wrapLine; white-space: pre-wrap; `, - toolbarButtonRow: css` - label: toolbarButtonRow; - gap: ${theme.spacing(0.5)}; - - max-width: calc(3 * ${theme.spacing(theme.components.height.sm)}); - & > div { - height: ${theme.spacing(theme.components.height.sm)}; - width: ${theme.spacing(theme.components.height.sm)}; - & > button { - border: 0; - background-color: transparent; - height: inherit; - - &:hover { - box-shadow: none; - border-radius: 50%; - } - } - } - `, - toolbarButtonRowActive: css` - & div:last-child > button:not(.stats-button) { - color: ${theme.v1.palette.orangeDark}; - border-color: ${theme.v1.palette.orangeDark}; - background-color: transparent; - - &:hover { - color: ${theme.colors.text.primary}; - background: ${theme.colors.emphasize(theme.colors.background.canvas, 0.03)}; - } - } - `, logDetailsStats: css` padding: 0 ${theme.spacing(1)}; `, @@ -127,6 +82,12 @@ const getStyles = memoizeOne((theme: GrafanaTheme2) => { } } `, + buttonRow: css` + display: flex; + flex-direction: row; + gap: ${theme.spacing(0.5)}; + margin-left: ${theme.spacing(0.5)}; + `, }; }); @@ -242,61 +203,37 @@ class UnThemedLogDetailsRow extends PureComponent { onClickFilterOutLabel, } = this.props; const { showFieldsStats, fieldStats, fieldCount } = this.state; - const activeButton = displayedFields?.includes(parsedKey) || showFieldsStats; const styles = getStyles(theme); const style = getLogRowStyles(theme); const hasFilteringFunctionality = onClickFilterLabel && onClickFilterOutLabel; const toggleFieldButton = displayedFields && displayedFields.includes(parsedKey) ? ( - + ) : ( - + ); return ( <> - +
{hasFilteringFunctionality && ( - + )} {hasFilteringFunctionality && ( - + )} {displayedFields && toggleFieldButton} - - +
{/* Key - value columns */} @@ -328,16 +265,12 @@ class UnThemedLogDetailsRow extends PureComponent { {showFieldsStats && ( - - - +
From 15aae5e8a92604712531ac0e77cee6f3dea8e877 Mon Sep 17 00:00:00 2001 From: Andres Martinez Gotor Date: Wed, 8 Mar 2023 16:11:38 +0100 Subject: [PATCH 070/288] Panel: Show multiple errors info in the inspector (#64340) --- .../src/utils/queryResponse.test.ts | 3 + .../src/utils/queryResponse.ts | 6 +- .../components/Inspector/InspectContent.tsx | 7 ++- .../PanelEditor/PanelEditorTableView.test.tsx | 61 ++++++++++++++++++- .../PanelEditor/PanelEditorTableView.tsx | 8 ++- .../dashgrid/PanelStateWrapper.test.tsx | 43 ++++++++++++- .../dashboard/dashgrid/PanelStateWrapper.tsx | 10 ++- .../explore/ExploreQueryInspector.tsx | 9 ++- .../inspector/InspectErrorTab.test.tsx | 49 +++++++++++++-- .../features/inspector/InspectErrorTab.tsx | 35 ++++++++--- 10 files changed, 207 insertions(+), 24 deletions(-) diff --git a/packages/grafana-runtime/src/utils/queryResponse.test.ts b/packages/grafana-runtime/src/utils/queryResponse.test.ts index 3cd421ac4be..56be442fd27 100644 --- a/packages/grafana-runtime/src/utils/queryResponse.test.ts +++ b/packages/grafana-runtime/src/utils/queryResponse.test.ts @@ -54,6 +54,7 @@ const resWithError = { results: { A: { error: 'Hello Error', + status: 400, frames: [ { schema: { @@ -354,12 +355,14 @@ describe('Query Response parser', () => { { "message": "Hello Error", "refId": "A", + "status": 400, } `); expect(res.errors).toEqual([ { message: 'Hello Error', refId: 'A', + status: 400, }, ]); diff --git a/packages/grafana-runtime/src/utils/queryResponse.ts b/packages/grafana-runtime/src/utils/queryResponse.ts index 902c9b28da7..919c003ef03 100644 --- a/packages/grafana-runtime/src/utils/queryResponse.ts +++ b/packages/grafana-runtime/src/utils/queryResponse.ts @@ -33,6 +33,7 @@ export interface DataResponse { error?: string; refId?: string; frames?: DataFrameJSON[]; + status?: number; // Legacy TSDB format... series?: TimeSeries[]; @@ -86,12 +87,13 @@ export function toDataQueryResponse( rsp.error = { refId: dr.refId, message: dr.error, + status: dr.status, }; } if (rsp.errors) { - rsp.errors.push({ refId: dr.refId, message: dr.error }); + rsp.errors.push({ refId: dr.refId, message: dr.error, status: dr.status }); } else { - rsp.errors = [{ refId: dr.refId, message: dr.error }]; + rsp.errors = [{ refId: dr.refId, message: dr.error, status: dr.status }]; } rsp.state = LoadingState.Error; } diff --git a/public/app/features/dashboard/components/Inspector/InspectContent.tsx b/public/app/features/dashboard/components/Inspector/InspectContent.tsx index a5388e9c753..198dfec4f51 100644 --- a/public/app/features/dashboard/components/Inspector/InspectContent.tsx +++ b/public/app/features/dashboard/components/Inspector/InspectContent.tsx @@ -50,7 +50,10 @@ export const InspectContent = ({ return null; } - const error = data?.error; + let errors = data?.errors; + if (!errors?.length && data?.error) { + errors = [data.error]; + } // Validate that the active tab is actually valid and allowed let activeTab = currentTab; @@ -102,7 +105,7 @@ export const InspectContent = ({ {activeTab === InspectTab.JSON && ( )} - {activeTab === InspectTab.Error && } + {activeTab === InspectTab.Error && } {data && activeTab === InspectTab.Stats && } {data && activeTab === InspectTab.Query && ( panel.refresh()} /> diff --git a/public/app/features/dashboard/components/PanelEditor/PanelEditorTableView.test.tsx b/public/app/features/dashboard/components/PanelEditor/PanelEditorTableView.test.tsx index 1bdc4169ed9..e303ba5671a 100644 --- a/public/app/features/dashboard/components/PanelEditor/PanelEditorTableView.test.tsx +++ b/public/app/features/dashboard/components/PanelEditor/PanelEditorTableView.test.tsx @@ -1,4 +1,4 @@ -import { act, render, screen } from '@testing-library/react'; +import { act, fireEvent, render, screen } from '@testing-library/react'; import React, { FC } from 'react'; import { Provider } from 'react-redux'; import configureMockStore from 'redux-mock-store'; @@ -15,6 +15,7 @@ import { PanelProps, TimeRange, } from '@grafana/data'; +import { selectors } from '@grafana/e2e-selectors'; import { getTimeSrv, TimeSrv, setTimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { PanelQueryRunner } from '../../../query/state/PanelQueryRunner'; @@ -174,6 +175,64 @@ describe('PanelEditorTableView', () => { width: 100, }); }); + + it('should render an error', async () => { + const { rerender, props, subject, store } = setupTestContext({}); + + // only render the panel when loading is done + act(() => { + subject.next({ state: LoadingState.Loading, series: [], timeRange: getDefaultTimeRange() }); + subject.next({ + state: LoadingState.Error, + series: [], + errors: [{ message: 'boom!' }], + timeRange: getDefaultTimeRange(), + }); + }); + + const newProps = { ...props, isInView: true }; + rerender( + + + + ); + + const button = screen.getByRole('button', { name: selectors.components.Panels.Panel.headerCornerInfo('error') }); + expect(button).toBeInTheDocument(); + await act(async () => { + fireEvent.focus(button); + }); + expect(await screen.findByText('boom!')).toBeInTheDocument(); + }); + + it('should render a description for multiple errors', async () => { + const { rerender, props, subject, store } = setupTestContext({}); + + // only render the panel when loading is done + act(() => { + subject.next({ state: LoadingState.Loading, series: [], timeRange: getDefaultTimeRange() }); + subject.next({ + state: LoadingState.Error, + series: [], + errors: [{ message: 'boom 1!' }, { message: 'boom 2!' }], + timeRange: getDefaultTimeRange(), + }); + }); + + const newProps = { ...props, isInView: true }; + rerender( + + + + ); + + const button = screen.getByRole('button', { name: selectors.components.Panels.Panel.headerCornerInfo('error') }); + expect(button).toBeInTheDocument(); + await act(async () => { + fireEvent.focus(button); + }); + expect(await screen.findByText('Multiple errors found. Click for more details')).toBeInTheDocument(); + }); }); const TestPanelComponent: FC = () =>
Plugin Panel to Render
; diff --git a/public/app/features/dashboard/components/PanelEditor/PanelEditorTableView.tsx b/public/app/features/dashboard/components/PanelEditor/PanelEditorTableView.tsx index b116db21f21..904ae31ec5d 100644 --- a/public/app/features/dashboard/components/PanelEditor/PanelEditorTableView.tsx +++ b/public/app/features/dashboard/components/PanelEditor/PanelEditorTableView.tsx @@ -49,11 +49,17 @@ export function PanelEditorTableView({ width, height, panel, dashboard }: Props) if (!data) { return null; } + + const errorMessage = data?.errors + ? data.errors.length > 1 + ? 'Multiple errors found. Click for more details' + : data.errors[0].message + : data?.error?.message; return ( {(innerWidth, innerHeight) => ( <> - + { expect(screen.getByText(/plugin panel to render/i)).toBeInTheDocument(); }); }); + + describe('when there are error(s)', () => { + [ + { errors: [{ message: 'boom!' }], expectedMessage: 'boom!' }, + { + errors: [{ message: 'boom!' }, { message: 'boom2!' }], + expectedMessage: 'Multiple errors found. Click for more details', + }, + ].forEach((scenario) => { + it(`then it should show the error message: ${scenario.expectedMessage}`, async () => { + const { rerender, props, subject, store } = setupTestContext({}); + + act(() => { + subject.next({ state: LoadingState.Loading, series: [], timeRange: getDefaultTimeRange() }); + subject.next({ + state: LoadingState.Error, + series: [], + errors: scenario.errors, + timeRange: getDefaultTimeRange(), + }); + }); + + const newProps = { ...props, isInView: true }; + rerender( + + + + ); + + const button = screen.getByRole('button', { + name: selectors.components.Panels.Panel.headerCornerInfo('error'), + }); + expect(button).toBeInTheDocument(); + await act(async () => { + fireEvent.focus(button); + }); + expect(await screen.findByText(scenario.expectedMessage)).toBeInTheDocument(); + }); + }); + }); }); const TestPanelComponent: FC = () =>
Plugin Panel to Render
; diff --git a/public/app/features/dashboard/dashgrid/PanelStateWrapper.tsx b/public/app/features/dashboard/dashgrid/PanelStateWrapper.tsx index e6d50b68379..6e2c72fc71a 100644 --- a/public/app/features/dashboard/dashgrid/PanelStateWrapper.tsx +++ b/public/app/features/dashboard/dashgrid/PanelStateWrapper.tsx @@ -300,8 +300,14 @@ export class PanelStateWrapper extends PureComponent { } break; case LoadingState.Error: - const { error } = data; - if (error) { + const { error, errors } = data; + if (errors?.length) { + if (errors.length === 1) { + errorMessage = errors[0].message; + } else { + errorMessage = 'Multiple errors found. Click for more details'; + } + } else if (error) { if (errorMessage !== error.message) { errorMessage = error.message; } diff --git a/public/app/features/explore/ExploreQueryInspector.tsx b/public/app/features/explore/ExploreQueryInspector.tsx index 681f7e7a747..6084734bbf8 100644 --- a/public/app/features/explore/ExploreQueryInspector.tsx +++ b/public/app/features/explore/ExploreQueryInspector.tsx @@ -26,7 +26,10 @@ type Props = DispatchProps & ConnectedProps; export function ExploreQueryInspector(props: Props) { const { loading, width, onClose, queryResponse, timeZone } = props; const dataFrames = queryResponse?.series || []; - const error = queryResponse?.error; + let errors = queryResponse?.errors; + if (!errors?.length && queryResponse?.error) { + errors = [queryResponse.error]; + } useEffect(() => { reportInteraction('grafana_explore_query_inspector_opened'); @@ -69,12 +72,12 @@ export function ExploreQueryInspector(props: Props) { }; const tabs = [statsTab, queryTab, jsonTab, dataTab]; - if (error) { + if (errors?.length) { const errorTab: TabConfig = { label: 'Error', value: 'error', icon: 'exclamation-triangle', - content: , + content: , }; tabs.push(errorTab); } diff --git a/public/app/features/inspector/InspectErrorTab.test.tsx b/public/app/features/inspector/InspectErrorTab.test.tsx index d86db092516..222cbb09304 100644 --- a/public/app/features/inspector/InspectErrorTab.test.tsx +++ b/public/app/features/inspector/InspectErrorTab.test.tsx @@ -16,7 +16,7 @@ describe('InspectErrorTab', () => { error: 'my error', }, }; - render(); + render(); expect(screen.getByText('This is an error')).toBeInTheDocument(); expect(screen.getByText('error:')).toBeInTheDocument(); expect(screen.getByText('"my error"')).toBeInTheDocument(); @@ -27,7 +27,7 @@ describe('InspectErrorTab', () => { message: '{ "error": { "code": "BadRequest", "message": "Please provide below info when asking for support.", "details": [] } }', }; - const { container } = render(); + const { container } = render(); expect(container.childElementCount).toEqual(1); expect(screen.getByText('code:')).toBeInTheDocument(); expect(screen.getByText('"BadRequest"')).toBeInTheDocument(); @@ -39,7 +39,7 @@ describe('InspectErrorTab', () => { message: '400 BadRequest, Error from Azure: { "error": { "code": "BadRequest", "message": "Please provide below info when asking for support.", "details": [] } }', }; - const { container } = render(); + const { container } = render(); expect(container.childElementCount).toEqual(2); expect(screen.getByRole('heading', { name: '400 BadRequest, Error from Azure:' })).toBeInTheDocument(); expect(screen.getByText('code:')).toBeInTheDocument(); @@ -55,7 +55,7 @@ describe('InspectErrorTab', () => { const error = { message: errMsg, }; - render(); + render(); expect(screen.queryByRole('heading')).toBeNull(); expect(screen.getByText(errMsg)).toBeInTheDocument(); }); @@ -65,9 +65,48 @@ describe('InspectErrorTab', () => { const error = { status: 400, }; - const { container } = render(); + const { container } = render(); expect(container.childElementCount).toEqual(1); expect(screen.getByText('status:')).toBeInTheDocument(); expect(screen.getByText('400')).toBeInTheDocument(); }); + + it('should return a message along with a status', () => { + const error = { + status: 400, + message: 'This is an error', + }; + render(); + expect(screen.getByText(/This is an error/)).toBeInTheDocument(); + expect(screen.getByText(/Status: 400/)).toBeInTheDocument(); + }); + + it('should return a JSON encoded object along with a status', () => { + const error = { + status: 400, + message: + '{ "error": { "code": "BadRequest", "message": "Please provide below info when asking for support.", "details": [] } }', + }; + render(); + expect(screen.getByText('"BadRequest"')).toBeInTheDocument(); + expect(screen.getByText(/Status: 400/)).toBeInTheDocument(); + }); + + it('should return multiple errors', () => { + const errors = [ + { + status: 400, + message: 'This is one error', + }, + { + status: 401, + message: 'This is another error', + }, + ]; + render(); + expect(screen.getByText(/This is one error/)).toBeInTheDocument(); + expect(screen.getByText(/Status: 400/)).toBeInTheDocument(); + expect(screen.getByText(/This is another error/)).toBeInTheDocument(); + expect(screen.getByText(/Status: 401/)).toBeInTheDocument(); + }); }); diff --git a/public/app/features/inspector/InspectErrorTab.tsx b/public/app/features/inspector/InspectErrorTab.tsx index d149e66c5cf..9c9b4b71aa1 100644 --- a/public/app/features/inspector/InspectErrorTab.tsx +++ b/public/app/features/inspector/InspectErrorTab.tsx @@ -1,10 +1,10 @@ import React from 'react'; import { DataQueryError } from '@grafana/data'; -import { JSONFormatter } from '@grafana/ui'; +import { Alert, JSONFormatter } from '@grafana/ui'; interface InspectErrorTabProps { - error?: DataQueryError; + errors?: DataQueryError[]; } const parseErrorMessage = (message: string): { msg: string; json?: any } => { @@ -20,10 +20,7 @@ const parseErrorMessage = (message: string): { msg: string; json?: any } => { } }; -export const InspectErrorTab = ({ error }: InspectErrorTabProps) => { - if (!error) { - return null; - } +function renderError(error: DataQueryError) { if (error.data) { return ( <> @@ -35,15 +32,39 @@ export const InspectErrorTab = ({ error }: InspectErrorTabProps) => { if (error.message) { const { msg, json } = parseErrorMessage(error.message); if (!json) { - return
{msg}
; + return ( + <> + {error.status && <>Status: {error.status}. Message: } + {msg} + + ); } else { return ( <> {msg !== '' &&

{msg}

} + {error.status && <>Status: {error.status}} ); } } return ; +} + +export const InspectErrorTab = ({ errors }: InspectErrorTabProps) => { + if (!errors?.length) { + return null; + } + if (errors.length === 1) { + return renderError(errors[0]); + } + return ( + <> + {errors.map((error, index) => ( + + {renderError(error)} + + ))} + + ); }; From 06f4cc08804b5dcf91e639067569dd41b44e6959 Mon Sep 17 00:00:00 2001 From: mikkancso Date: Wed, 8 Mar 2023 16:28:53 +0100 Subject: [PATCH 071/288] Admin/Plugins: Set category filter in connections link (#64393) set category filter in connections link --- .../ConnectionsRedirectNotice/ConnectionsRedirectNotice.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/public/app/features/connections/components/ConnectionsRedirectNotice/ConnectionsRedirectNotice.tsx b/public/app/features/connections/components/ConnectionsRedirectNotice/ConnectionsRedirectNotice.tsx index fb959d6e15e..8a14aadebb8 100644 --- a/public/app/features/connections/components/ConnectionsRedirectNotice/ConnectionsRedirectNotice.tsx +++ b/public/app/features/connections/components/ConnectionsRedirectNotice/ConnectionsRedirectNotice.tsx @@ -28,7 +28,8 @@ export enum DestinationPage { const destinationLinks = { [DestinationPage.dataSources]: ROUTES.DataSources, - [DestinationPage.connectData]: ROUTES.ConnectData, + // Set category filter for the cloud version of ConnectData page + [DestinationPage.connectData]: `${ROUTES.ConnectData}?cat=data-source`, }; export function ConnectionsRedirectNotice({ destinationPage }: { destinationPage: DestinationPage }) { From f0529430ff385b5ae839be34a1efa63a16415fab Mon Sep 17 00:00:00 2001 From: Tania Date: Wed, 8 Mar 2023 16:38:52 +0100 Subject: [PATCH 072/288] Chore: Assign ownership for as-code feature flags (#64424) Chore: Add owners for as-code feature toggles --- pkg/services/featuremgmt/registry.go | 3 +++ pkg/services/featuremgmt/toggles_gen_test.go | 3 --- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 5fff9cef8c9..b04b75c2360 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -18,6 +18,7 @@ var ( Name: "trimDefaults", Description: "Use cue schema to remove values that will be applied automatically", State: FeatureStateBeta, + Owner: grafanaAsCodeSquad, }, { Name: "disableEnvelopeEncryption", @@ -96,6 +97,7 @@ var ( Name: "featureHighlights", Description: "Highlight Grafana Enterprise features", State: FeatureStateStable, + Owner: grafanaAsCodeSquad, }, { Name: "dashboardComments", @@ -191,6 +193,7 @@ var ( Description: "Validate dashboard JSON POSTed to api/dashboards/db", State: FeatureStateBeta, RequiresRestart: true, + Owner: grafanaAsCodeSquad, }, { Name: "autoMigrateGraphPanels", diff --git a/pkg/services/featuremgmt/toggles_gen_test.go b/pkg/services/featuremgmt/toggles_gen_test.go index c6832e9bbe7..6bbba1461af 100644 --- a/pkg/services/featuremgmt/toggles_gen_test.go +++ b/pkg/services/featuremgmt/toggles_gen_test.go @@ -43,13 +43,10 @@ func TestFeatureToggleFiles(t *testing.T) { ownerlessFeatures := map[string]bool{ "alertingBigTransactions": true, - "trimDefaults": true, "database_metrics": true, "prometheusAzureOverrideAudience": true, - "featureHighlights": true, "tracing": true, "cloudWatchDynamicLabels": true, - "validateDashboardsOnSave": true, "prometheusWideSeries": true, "disableSecretsCompatibility": true, "logRequestsInstrumentedAsUnknown": true, From a5133d61b5cc50841b03415dbbb40fa62515b944 Mon Sep 17 00:00:00 2001 From: Yuri Tseretyan Date: Wed, 8 Mar 2023 11:03:06 -0500 Subject: [PATCH 073/288] Code: Ownership for alerting feature toggles (#64426) * add alerting squad as an owner of some feature flags * remove flags from owneless --------- Co-authored-by: Artur Wierzbicki --- pkg/services/featuremgmt/codeowners.go | 1 + pkg/services/featuremgmt/registry.go | 3 +++ pkg/services/featuremgmt/toggles_gen_test.go | 3 --- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/pkg/services/featuremgmt/codeowners.go b/pkg/services/featuremgmt/codeowners.go index 84d3cc267f8..b53d08c58fc 100644 --- a/pkg/services/featuremgmt/codeowners.go +++ b/pkg/services/featuremgmt/codeowners.go @@ -17,4 +17,5 @@ const ( grafanaAuthnzSquad codeowner = "@grafana/grafana-authnz-team" grafanaObservabilityLogsSquad codeowner = "@grafana/observability-logs" grafanaObservabilityTracesAndProfilingSquad codeowner = "@grafana/observability-traces-and-profiling" + grafanaAlertingSquad codeowner = "@grafana/alerting-squad" ) diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index b04b75c2360..293e107d81c 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -13,6 +13,7 @@ var ( Name: "alertingBigTransactions", Description: "Use big transactions for alerting database writes", State: FeatureStateAlpha, + Owner: grafanaAlertingSquad, }, { Name: "trimDefaults", @@ -356,6 +357,7 @@ var ( Name: "alertingBacktesting", Description: "Rule backtesting API for alerting", State: FeatureStateAlpha, + Owner: grafanaAlertingSquad, }, { Name: "editPanelCSVDragAndDrop", @@ -369,6 +371,7 @@ var ( Description: "Stop maintaining state of alerts that are not firing", State: FeatureStateBeta, RequiresRestart: false, + Owner: grafanaAlertingSquad, }, { diff --git a/pkg/services/featuremgmt/toggles_gen_test.go b/pkg/services/featuremgmt/toggles_gen_test.go index 6bbba1461af..28e5b2cbfa2 100644 --- a/pkg/services/featuremgmt/toggles_gen_test.go +++ b/pkg/services/featuremgmt/toggles_gen_test.go @@ -42,7 +42,6 @@ func TestFeatureToggleFiles(t *testing.T) { }) ownerlessFeatures := map[string]bool{ - "alertingBigTransactions": true, "database_metrics": true, "prometheusAzureOverrideAudience": true, "tracing": true, @@ -59,8 +58,6 @@ func TestFeatureToggleFiles(t *testing.T) { "datasourceOnboarding": true, "secureSocksDatasourceProxy": true, "disablePrometheusExemplarSampling": true, - "alertingBacktesting": true, - "alertingNoNormalState": true, "individualCookiePreferences": true, } From 7c55dbf37d15a647525ecad0f705dcade51e005f Mon Sep 17 00:00:00 2001 From: Carl Bergquist Date: Wed, 8 Mar 2023 17:08:57 +0100 Subject: [PATCH 074/288] Remotecache: Migrates get/set calls to use bytearrays and remove get/set functions (#63525) Signed-off-by: bergquist --- pkg/infra/remotecache/remotecache.go | 24 +-------------- pkg/infra/remotecache/remotecache_test.go | 37 +++++++++++------------ pkg/services/anonymous/anonimpl/impl.go | 2 +- pkg/services/authn/clients/proxy.go | 19 +++++++----- pkg/services/authn/clients/proxy_test.go | 6 ++-- 5 files changed, 34 insertions(+), 54 deletions(-) diff --git a/pkg/infra/remotecache/remotecache.go b/pkg/infra/remotecache/remotecache.go index a5193283641..0d9aa6f04fa 100644 --- a/pkg/infra/remotecache/remotecache.go +++ b/pkg/infra/remotecache/remotecache.go @@ -55,12 +55,6 @@ func ProvideService(cfg *setting.Cfg, sqlStore db.DB, secretsService secrets.Ser // so any struct added to the cache needs to be registered with `remotecache.Register` // ex `remotecache.Register(CacheableStruct{})` type CacheStorage interface { - // Get reads object from Cache - Get(ctx context.Context, key string) (interface{}, error) - - // Set sets an object into the cache. if `expire` is set to zero it will default to 24h - Set(ctx context.Context, key string, value interface{}, expire time.Duration) error - // GetByteArray gets the cache value as an byte array GetByteArray(ctx context.Context, key string) ([]byte, error) @@ -83,11 +77,6 @@ type RemoteCache struct { Cfg *setting.Cfg } -// Get reads object from Cache -func (ds *RemoteCache) Get(ctx context.Context, key string) (interface{}, error) { - return ds.client.Get(ctx, key) -} - // GetByteArray returns the cached value as an byte array func (ds *RemoteCache) GetByteArray(ctx context.Context, key string) ([]byte, error) { return ds.client.GetByteArray(ctx, key) @@ -95,16 +84,11 @@ func (ds *RemoteCache) GetByteArray(ctx context.Context, key string) ([]byte, er // SetByteArray stored the byte array in the cache func (ds *RemoteCache) SetByteArray(ctx context.Context, key string, value []byte, expire time.Duration) error { - return ds.client.SetByteArray(ctx, key, value, expire) -} - -// Set sets an object into the cache. if `expire` is set to zero it will default to 24h -func (ds *RemoteCache) Set(ctx context.Context, key string, value interface{}, expire time.Duration) error { if expire == 0 { expire = defaultMaxCacheExpiration } - return ds.client.Set(ctx, key, value, expire) + return ds.client.SetByteArray(ctx, key, value, expire) } // Delete object from cache @@ -208,15 +192,9 @@ type prefixCacheStorage struct { prefix string } -func (pcs *prefixCacheStorage) Get(ctx context.Context, key string) (interface{}, error) { - return pcs.cache.Get(ctx, pcs.prefix+key) -} func (pcs *prefixCacheStorage) GetByteArray(ctx context.Context, key string) ([]byte, error) { return pcs.cache.GetByteArray(ctx, pcs.prefix+key) } -func (pcs *prefixCacheStorage) Set(ctx context.Context, key string, value interface{}, expire time.Duration) error { - return pcs.cache.Set(ctx, pcs.prefix+key, value, expire) -} func (pcs *prefixCacheStorage) SetByteArray(ctx context.Context, key string, value []byte, expire time.Duration) error { return pcs.cache.SetByteArray(ctx, pcs.prefix+key, value, expire) } diff --git a/pkg/infra/remotecache/remotecache_test.go b/pkg/infra/remotecache/remotecache_test.go index 502f08946c4..1c6bb6281f0 100644 --- a/pkg/infra/remotecache/remotecache_test.go +++ b/pkg/infra/remotecache/remotecache_test.go @@ -65,15 +65,15 @@ func runCountTestsForClient(t *testing.T, opts *setting.RemoteCacheOptions, sqls } t.Run("can count items", func(t *testing.T) { - cacheableStruct := CacheableStruct{String: "hej", Int64: 2000} + cacheableValue := []byte("hej hej") - err := client.Set(context.Background(), "pref-key1", cacheableStruct, 0) + err := client.SetByteArray(context.Background(), "pref-key1", cacheableValue, 0) require.NoError(t, err) - err = client.Set(context.Background(), "pref-key2", cacheableStruct, 0) + err = client.SetByteArray(context.Background(), "pref-key2", cacheableValue, 0) require.NoError(t, err) - err = client.Set(context.Background(), "key3-not-pref", cacheableStruct, 0) + err = client.SetByteArray(context.Background(), "key3-not-pref", cacheableValue, 0) require.NoError(t, err) n, errC := client.Count(context.Background(), "pref-") @@ -89,37 +89,34 @@ func runCountTestsForClient(t *testing.T, opts *setting.RemoteCacheOptions, sqls } func canPutGetAndDeleteCachedObjects(t *testing.T, client CacheStorage) { - cacheableStruct := CacheableStruct{String: "hej", Int64: 2000} + dataToCache := []byte("some bytes") - err := client.Set(context.Background(), "key1", cacheableStruct, 0) + err := client.SetByteArray(context.Background(), "key1", dataToCache, 0) assert.Equal(t, err, nil, "expected nil. got: ", err) - data, err := client.Get(context.Background(), "key1") + data, err := client.GetByteArray(context.Background(), "key1") assert.Equal(t, err, nil) - s, ok := data.(CacheableStruct) - assert.Equal(t, ok, true) - assert.Equal(t, s.String, "hej") - assert.Equal(t, s.Int64, int64(2000)) + assert.Equal(t, string(data), "some bytes") err = client.Delete(context.Background(), "key1") assert.Equal(t, err, nil) - _, err = client.Get(context.Background(), "key1") + _, err = client.GetByteArray(context.Background(), "key1") assert.Equal(t, err, ErrCacheItemNotFound) } func canNotFetchExpiredItems(t *testing.T, client CacheStorage) { - cacheableStruct := CacheableStruct{String: "hej", Int64: 2000} + dataToCache := []byte("some bytes") - err := client.Set(context.Background(), "key1", cacheableStruct, time.Second) + err := client.SetByteArray(context.Background(), "key1", dataToCache, time.Second) assert.Equal(t, err, nil) // not sure how this can be avoided when testing redis/memcached :/ <-time.After(time.Second + time.Millisecond) // should not be able to read that value since its expired - _, err = client.Get(context.Background(), "key1") + _, err = client.GetByteArray(context.Background(), "key1") assert.Equal(t, err, ErrCacheItemNotFound) } @@ -133,16 +130,16 @@ func TestCachePrefix(t *testing.T) { prefixCache := &prefixCacheStorage{cache: cache, prefix: "test/"} // Set a value (with a prefix) - err := prefixCache.Set(context.Background(), "foo", "bar", time.Hour) + err := prefixCache.SetByteArray(context.Background(), "foo", []byte("bar"), time.Hour) require.NoError(t, err) // Get a value (with a prefix) - v, err := prefixCache.Get(context.Background(), "foo") + v, err := prefixCache.GetByteArray(context.Background(), "foo") require.NoError(t, err) - require.Equal(t, "bar", v) + require.Equal(t, "bar", string(v)) // Get a value directly from the underlying cache, ensure the prefix is in the key - v, err = cache.Get(context.Background(), "test/foo") + v, err = cache.GetByteArray(context.Background(), "test/foo") require.NoError(t, err) - require.Equal(t, "bar", v) + require.Equal(t, "bar", string(v)) // Get a value directly from the underlying cache without a prefix, should not be there _, err = cache.Get(context.Background(), "foo") require.Error(t, err) diff --git a/pkg/services/anonymous/anonimpl/impl.go b/pkg/services/anonymous/anonimpl/impl.go index 3c40f38d4d8..c6677086e49 100644 --- a/pkg/services/anonymous/anonimpl/impl.go +++ b/pkg/services/anonymous/anonimpl/impl.go @@ -96,5 +96,5 @@ func (a *AnonSessionService) TagSession(ctx context.Context, httpReq *http.Reque a.localCache.SetDefault(key, struct{}{}) - return a.remoteCache.Set(ctx, key, key, thirtyDays) + return a.remoteCache.SetByteArray(ctx, key, []byte(key), thirtyDays) } diff --git a/pkg/services/authn/clients/proxy.go b/pkg/services/authn/clients/proxy.go index d4683b4fc81..a53fb754edb 100644 --- a/pkg/services/authn/clients/proxy.go +++ b/pkg/services/authn/clients/proxy.go @@ -2,6 +2,7 @@ package clients import ( "context" + "encoding/binary" "encoding/hex" "fmt" "hash/fnv" @@ -49,8 +50,8 @@ func ProvideProxy(cfg *setting.Cfg, cache proxyCache, userSrv user.Service, clie } type proxyCache interface { - Get(ctx context.Context, key string) (interface{}, error) - Set(ctx context.Context, key string, value interface{}, expire time.Duration) error + GetByteArray(ctx context.Context, key string) ([]byte, error) + SetByteArray(ctx context.Context, key string, value []byte, expire time.Duration) error } type Proxy struct { @@ -82,14 +83,16 @@ func (c *Proxy) Authenticate(ctx context.Context, r *authn.Request) (*authn.Iden if ok { // See if we have cached the user id, in that case we can fetch the signed-in user and skip sync. // Error here means that we could not find anything in cache, so we can proceed as usual - if entry, err := c.cache.Get(ctx, cacheKey); err == nil { + if entry, err := c.cache.GetByteArray(ctx, cacheKey); err == nil { + uid := int64(binary.LittleEndian.Uint64(entry)) + usr, err := c.userSrv.GetSignedInUserWithCacheCtx(ctx, &user.GetSignedInUserQuery{ - UserID: entry.(int64), + UserID: uid, OrgID: r.OrgID, }) if err != nil { - c.log.FromContext(ctx).Warn("Could not resolved cached user", "error", err, "userId", entry.(int64)) + c.log.FromContext(ctx).Warn("Could not resolved cached user", "error", err, "userId", string(entry)) } // if we for some reason cannot find the user we proceed with the normal flow, authenticate with ProxyClient @@ -133,8 +136,10 @@ func (c *Proxy) Hook(ctx context.Context, identity *authn.Identity, r *authn.Req } c.log.FromContext(ctx).Debug("Cache proxy user", "userId", id) - if err := c.cache.Set(ctx, identity.ClientParams.CacheAuthProxyKey, id, time.Duration(c.cfg.AuthProxySyncTTL)*time.Minute); err != nil { - c.log.FromContext(ctx).Warn("Failed to cache proxy user", "error", err, "userId", id) + bytes := make([]byte, 8) + binary.LittleEndian.PutUint64(bytes, uint64(id)) + if err := c.cache.SetByteArray(ctx, identity.ClientParams.CacheAuthProxyKey, bytes, time.Duration(c.cfg.AuthProxySyncTTL)*time.Minute); err != nil { + c.log.Warn("failed to cache proxy user", "error", err, "userId", id) } return nil diff --git a/pkg/services/authn/clients/proxy_test.go b/pkg/services/authn/clients/proxy_test.go index bb1be4d6ee5..a0f39ec0425 100644 --- a/pkg/services/authn/clients/proxy_test.go +++ b/pkg/services/authn/clients/proxy_test.go @@ -178,13 +178,13 @@ var _ proxyCache = new(fakeCache) type fakeCache struct { expectedErr error - expectedItem interface{} + expectedItem []byte } -func (f fakeCache) Get(ctx context.Context, key string) (interface{}, error) { +func (f fakeCache) GetByteArray(ctx context.Context, key string) ([]byte, error) { return f.expectedItem, f.expectedErr } -func (f fakeCache) Set(ctx context.Context, key string, value interface{}, expire time.Duration) error { +func (f fakeCache) SetByteArray(ctx context.Context, key string, value []byte, expire time.Duration) error { return f.expectedErr } From 11bc66a0e8b5a1bb0ce893665f0320088fab113d Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Wed, 8 Mar 2023 16:12:54 +0000 Subject: [PATCH 075/288] Chore: use `React.PropsWithChildren` to explicitly define the `children` prop (#64433) * use React.PropsWithChildren to explicitly define the children prop * fix ThemeDemo as well * provide empty generics --- .../src/components/ThemeDemos/ThemeDemo.tsx | 11 ++++++++--- .../components/SplitPaneWrapper/SplitPaneWrapper.tsx | 2 +- .../alerting/unified/components/PluginBridge.tsx | 9 +++++++-- .../features/alerting/unified/components/Strong.tsx | 6 ++++-- .../components/rule-editor/LabelsField.test.tsx | 2 +- .../query-and-alert-condition/AlertType.test.tsx | 2 +- .../unified/hooks/useAlertManagerSourceName.test.tsx | 12 ++++++------ .../unified/hooks/useExternalAMSelector.test.tsx | 12 ++++++------ .../unified/hooks/useIsRuleEditable.test.tsx | 2 +- 9 files changed, 35 insertions(+), 23 deletions(-) diff --git a/packages/grafana-ui/src/components/ThemeDemos/ThemeDemo.tsx b/packages/grafana-ui/src/components/ThemeDemos/ThemeDemo.tsx index d4b28d4f39c..03827b83006 100644 --- a/packages/grafana-ui/src/components/ThemeDemos/ThemeDemo.tsx +++ b/packages/grafana-ui/src/components/ThemeDemos/ThemeDemo.tsx @@ -1,5 +1,5 @@ import { css, cx } from '@emotion/css'; -import React, { FC, useState } from 'react'; +import React, { useState } from 'react'; import { GrafanaTheme2, ThemeRichColor } from '@grafana/data'; @@ -23,7 +23,7 @@ interface DemoBoxProps { textColor?: string; } -const DemoBox: FC = ({ bg, border, children }) => { +const DemoBox = ({ bg, border, children }: React.PropsWithChildren) => { const style = cx( css` padding: 16px; @@ -40,7 +40,12 @@ const DemoBox: FC = ({ bg, border, children }) => { return
{children}
; }; -const DemoText: FC<{ color?: string; bold?: boolean; size?: number }> = ({ color, bold, size, children }) => { +const DemoText = ({ + color, + bold, + size, + children, +}: React.PropsWithChildren<{ color?: string; bold?: boolean; size?: number }>) => { const style = css` padding: 4px; color: ${color ?? 'inherit'}; diff --git a/public/app/core/components/SplitPaneWrapper/SplitPaneWrapper.tsx b/public/app/core/components/SplitPaneWrapper/SplitPaneWrapper.tsx index dc8702752dd..bc7f3d13f91 100644 --- a/public/app/core/components/SplitPaneWrapper/SplitPaneWrapper.tsx +++ b/public/app/core/components/SplitPaneWrapper/SplitPaneWrapper.tsx @@ -17,7 +17,7 @@ interface Props { secondaryPaneStyle?: React.CSSProperties; } -export class SplitPaneWrapper extends PureComponent { +export class SplitPaneWrapper extends PureComponent> { //requestAnimationFrame reference rafToken: MutableRefObject = createRef(); diff --git a/public/app/features/alerting/unified/components/PluginBridge.tsx b/public/app/features/alerting/unified/components/PluginBridge.tsx index 7003314e720..564231580f9 100644 --- a/public/app/features/alerting/unified/components/PluginBridge.tsx +++ b/public/app/features/alerting/unified/components/PluginBridge.tsx @@ -1,4 +1,4 @@ -import React, { FC, ReactElement } from 'react'; +import React, { ReactElement } from 'react'; import { usePluginBridge } from '../hooks/usePluginBridge'; import { SupportedPlugin } from '../types/pluginBridges'; @@ -13,7 +13,12 @@ export interface PluginBridgeProps { loadingComponent?: ReactElement; } -export const PluginBridge: FC = ({ children, plugin, loadingComponent, notInstalledFallback }) => { +export const PluginBridge = ({ + children, + plugin, + loadingComponent, + notInstalledFallback, +}: React.PropsWithChildren) => { const { loading, installed } = usePluginBridge(plugin); if (loading) { diff --git a/public/app/features/alerting/unified/components/Strong.tsx b/public/app/features/alerting/unified/components/Strong.tsx index 90a408819b7..99c6bcc42fc 100644 --- a/public/app/features/alerting/unified/components/Strong.tsx +++ b/public/app/features/alerting/unified/components/Strong.tsx @@ -1,8 +1,10 @@ -import React, { FC } from 'react'; +import React from 'react'; import { useTheme2 } from '@grafana/ui'; -const Strong: FC = ({ children }) => { +interface Props {} + +const Strong = ({ children }: React.PropsWithChildren) => { const theme = useTheme2(); return {children}; }; diff --git a/public/app/features/alerting/unified/components/rule-editor/LabelsField.test.tsx b/public/app/features/alerting/unified/components/rule-editor/LabelsField.test.tsx index 23d9e11ea18..2dd636d6187 100644 --- a/public/app/features/alerting/unified/components/rule-editor/LabelsField.test.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/LabelsField.test.tsx @@ -13,7 +13,7 @@ const labels = [ { key: 'key2', value: 'value2' }, ]; -const FormProviderWrapper: React.FC = ({ children }) => { +const FormProviderWrapper = ({ children }: React.PropsWithChildren<{}>) => { const methods = useForm({ defaultValues: { labels } }); return {children}; }; diff --git a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/AlertType.test.tsx b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/AlertType.test.tsx index 851e4dc79a8..dd1b173c071 100644 --- a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/AlertType.test.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/AlertType.test.tsx @@ -18,7 +18,7 @@ const ui = { }, }; -const FormProviderWrapper: React.FC = ({ children }) => { +const FormProviderWrapper = ({ children }: React.PropsWithChildren<{}>) => { const methods = useForm({}); return {children}; }; diff --git a/public/app/features/alerting/unified/hooks/useAlertManagerSourceName.test.tsx b/public/app/features/alerting/unified/hooks/useAlertManagerSourceName.test.tsx index aaf5a8a774c..95d8f4a29bf 100644 --- a/public/app/features/alerting/unified/hooks/useAlertManagerSourceName.test.tsx +++ b/public/app/features/alerting/unified/hooks/useAlertManagerSourceName.test.tsx @@ -27,7 +27,7 @@ const externalAmMimir: AlertManagerDataSource = { describe('useAlertManagerSourceName', () => { it('Should return undefined alert manager name when there are no available alert managers', () => { - const wrapper: React.FC = ({ children }) => {children}; + const wrapper = ({ children }: React.PropsWithChildren<{}>) => {children}; const { result } = renderHook(() => useAlertManagerSourceName([]), { wrapper }); const [alertManager] = result.current; @@ -36,7 +36,7 @@ describe('useAlertManagerSourceName', () => { }); it('Should return Grafana AM when it is available and no alert manager query param exists', () => { - const wrapper: React.FC = ({ children }) => {children}; + const wrapper = ({ children }: React.PropsWithChildren<{}>) => {children}; const availableAMs = [grafanaAm, externalAmProm, externalAmMimir]; const { result } = renderHook(() => useAlertManagerSourceName(availableAMs), { wrapper }); @@ -49,7 +49,7 @@ describe('useAlertManagerSourceName', () => { it('Should return alert manager included in the query param when available', () => { const history = createMemoryHistory(); history.push({ search: `alertmanager=${externalAmProm.name}` }); - const wrapper: React.FC = ({ children }) => {children}; + const wrapper = ({ children }: React.PropsWithChildren<{}>) => {children}; const availableAMs = [grafanaAm, externalAmProm, externalAmMimir]; const { result } = renderHook(() => useAlertManagerSourceName(availableAMs), { wrapper }); @@ -62,7 +62,7 @@ describe('useAlertManagerSourceName', () => { it('Should return undefined if alert manager included in the query is not available', () => { const history = createMemoryHistory(); history.push({ search: `alertmanager=Not available external AM` }); - const wrapper: React.FC = ({ children }) => {children}; + const wrapper = ({ children }: React.PropsWithChildren<{}>) => {children}; const availableAMs = [grafanaAm, externalAmProm, externalAmMimir]; @@ -74,7 +74,7 @@ describe('useAlertManagerSourceName', () => { }); it('Should return alert manager from store if available and query is empty', () => { - const wrapper: React.FC = ({ children }) => {children}; + const wrapper = ({ children }: React.PropsWithChildren<{}>) => {children}; const availableAMs = [grafanaAm, externalAmProm, externalAmMimir]; store.set(ALERTMANAGER_NAME_LOCAL_STORAGE_KEY, externalAmProm.name); @@ -89,7 +89,7 @@ describe('useAlertManagerSourceName', () => { it('Should prioritize the alert manager from query over store', () => { const history = createMemoryHistory(); history.push({ search: `alertmanager=${externalAmProm.name}` }); - const wrapper: React.FC = ({ children }) => {children}; + const wrapper = ({ children }: React.PropsWithChildren<{}>) => {children}; const availableAMs = [grafanaAm, externalAmProm, externalAmMimir]; store.set(ALERTMANAGER_NAME_LOCAL_STORAGE_KEY, externalAmMimir.name); diff --git a/public/app/features/alerting/unified/hooks/useExternalAMSelector.test.tsx b/public/app/features/alerting/unified/hooks/useExternalAMSelector.test.tsx index b5dc9e6a228..a18f8fe3c55 100644 --- a/public/app/features/alerting/unified/hooks/useExternalAMSelector.test.tsx +++ b/public/app/features/alerting/unified/hooks/useExternalAMSelector.test.tsx @@ -45,7 +45,7 @@ describe('useExternalDataSourceAlertmanagers', () => { mockAlertmanagersResponse(server, { data: { activeAlertManagers: [], droppedAlertManagers: [] } }); - const wrapper: React.FC = ({ children }) => {children}; + const wrapper = ({ children }: React.PropsWithChildren<{}>) => {children}; // Act const { result, waitForNextUpdate } = renderHook(() => useExternalDataSourceAlertmanagers(), { wrapper }); @@ -78,7 +78,7 @@ describe('useExternalDataSourceAlertmanagers', () => { }, }); - const wrapper: React.FC = ({ children }) => {children}; + const wrapper = ({ children }: React.PropsWithChildren<{}>) => {children}; // Act const { result, waitForValueToChange } = renderHook(() => useExternalDataSourceAlertmanagers(), { wrapper }); @@ -111,7 +111,7 @@ describe('useExternalDataSourceAlertmanagers', () => { }, }); - const wrapper: React.FC = ({ children }) => {children}; + const wrapper = ({ children }: React.PropsWithChildren<{}>) => {children}; // Act const { result, waitForValueToChange } = renderHook(() => useExternalDataSourceAlertmanagers(), { wrapper }); @@ -144,7 +144,7 @@ describe('useExternalDataSourceAlertmanagers', () => { }, }); - const wrapper: React.FC = ({ children }) => {children}; + const wrapper = ({ children }: React.PropsWithChildren<{}>) => {children}; // Act const { result, waitForNextUpdate } = renderHook(() => useExternalDataSourceAlertmanagers(), { wrapper }); @@ -177,7 +177,7 @@ describe('useExternalDataSourceAlertmanagers', () => { }, }); - const wrapper: React.FC = ({ children }) => {children}; + const wrapper = ({ children }: React.PropsWithChildren<{}>) => {children}; // Act const { result, waitForValueToChange } = renderHook(() => useExternalDataSourceAlertmanagers(), { wrapper }); @@ -210,7 +210,7 @@ describe('useExternalDataSourceAlertmanagers', () => { state.dataSources.dataSources = [dsSettings]; }); - const wrapper: React.FC = ({ children }) => {children}; + const wrapper = ({ children }: React.PropsWithChildren<{}>) => {children}; // Act const { result, waitForValueToChange } = renderHook(() => useExternalDataSourceAlertmanagers(), { diff --git a/public/app/features/alerting/unified/hooks/useIsRuleEditable.test.tsx b/public/app/features/alerting/unified/hooks/useIsRuleEditable.test.tsx index 369006b7026..ee0bf681b0c 100644 --- a/public/app/features/alerting/unified/hooks/useIsRuleEditable.test.tsx +++ b/public/app/features/alerting/unified/hooks/useIsRuleEditable.test.tsx @@ -173,7 +173,7 @@ function mockPermissions(grantedPermissions: AccessControlAction[]) { function getProviderWrapper() { const dataSources = getMockedDataSources(); const store = mockUnifiedAlertingStore({ dataSources }); - const wrapper: React.FC = ({ children }) => {children}; + const wrapper = ({ children }: React.PropsWithChildren<{}>) => {children}; return wrapper; } From 6827f97b78717ddb29ece6fd3b35a8c8fa5a486c Mon Sep 17 00:00:00 2001 From: Gilles De Mey Date: Wed, 8 Mar 2023 17:18:17 +0100 Subject: [PATCH 076/288] Docs: update the current documentation on notification policies (#64316) Co-authored-by: Eve Meelan <81647476+Eve832@users.noreply.github.com> --- .../alerting/fundamentals/notifications.md | 10 +++-- .../create-notification-policy.md | 37 +++++++++++++------ .../manage-notifications/mute-timings.md | 3 +- .../manage-notifications/view-alert-groups.md | 4 +- .../tutorials/grafana-fundamentals/index.md | 4 +- 5 files changed, 38 insertions(+), 20 deletions(-) diff --git a/docs/sources/alerting/fundamentals/notifications.md b/docs/sources/alerting/fundamentals/notifications.md index 78eae2708d4..54694626641 100644 --- a/docs/sources/alerting/fundamentals/notifications.md +++ b/docs/sources/alerting/fundamentals/notifications.md @@ -21,13 +21,15 @@ Grafana uses Alertmanagers to send notifications for firing and resolved alerts. Notification policies control when and where notifications are sent. A notification policy can choose to send all alerts together in the same notification, send alerts in grouped notifications based on a set of labels, or send alerts as separate notifications. You can configure each notification policy to control how often notifications should be sent as well as having one or more mute timings to inhibit notifications at certain times of the day and on certain days of the week. -Notification policies are organized in a tree structure where at the root of the tree there is a notification policy called the root policy. There can be only one root policy and the root policy cannot be deleted. +Notification policies are organized in a tree structure where at the root of the tree there is a notification policy called the default policy. There can be only one default policy and the default policy cannot be deleted. -Specific routing policies are children of the root policy and can be used to match either all alerts or a subset of alerts based on a set of matching labels. A notification policy matches an alert when its matching labels match the labels in the alert. +Specific routing policies are children of the default policy and can be used to match either all alerts or a subset of alerts based on a set of matching labels. A notification policy matches an alert when its matching labels match the labels in the alert. -A specific routing policy can have its own child policies, called nested policies, which allow for additional matching of alerts. An example of a specific routing policy could be sending infrastructure alerts to the Ops team; while a child policy might send high priority alerts to Pagerduty and low priority alerts as emails. +A nested policy can have its own nested policies, which allow for additional matching of alerts. An example of a nested policy could be sending infrastructure alerts to the Ops team; while a nested policy might send high priority alerts to Pagerduty and low priority alerts as emails. -All alerts, irrespective of their labels, match the root policy. However, when the root policy receives an alert it looks at each specific routing policy and sends the alert to the first specific routing policy that matches the alert. If the specific routing policy has further child policies, then it can attempt to the match the alert against one of its nested policies. If no nested policies match the alert then the specific routing policy is the matching policy. If there are no specific routing policies, or no specific routing policies match the alert, then the root policy is the matching policy. +All alerts, irrespective of their labels, match the default policy. However, when the default policy receives an alert it looks at each nested policy and sends the alert to the first nested policy that matches the alert. If the nested policy has further nested policies, then it can attempt to the match the alert against one of its nested policies. If no nested policies match the alert then the policy itself is the matching policy. If there are no nested policies, or no nested policies match the alert, then the default policy is the matching policy. + + ## Contact points diff --git a/docs/sources/alerting/manage-notifications/create-notification-policy.md b/docs/sources/alerting/manage-notifications/create-notification-policy.md index fb3382c5c27..80acd1e3b81 100644 --- a/docs/sources/alerting/manage-notifications/create-notification-policy.md +++ b/docs/sources/alerting/manage-notifications/create-notification-policy.md @@ -16,14 +16,12 @@ weight: 300 # Manage notification policies -Notification policies determine how alerts are routed to contact points. Policies have a tree structure, where each policy can have one or more child policies. Each policy, except for the root policy, can also match specific alert labels. Each alert is evaluated by the root policy and subsequently by each child policy. If the `Continue matching subsequent sibling nodes` option is enabled for a specific policy, then evaluation continues even after one or more matches. A parent policy’s configuration settings and contact point information govern the behavior of an alert that does not match any of the child policies. A root policy governs any alert that does not match a specific policy. +Notification policies determine how alerts are routed to contact points. Policies have a tree structure, where each policy can have one or more nested policies. Each policy, except for the default policy, can also match specific alert labels. Each alert is evaluated by the default policy and subsequently by each nested policy. If the `Continue matching subsequent sibling nodes` option is enabled for a nested policy, then evaluation continues even after one or more matches. A parent policy’s configuration settings and contact point information govern the behavior of an alert that does not match any of the nested policies. A default policy governs any alert that does not match a nested policy. You can configure Grafana managed notification policies as well as notification policies for an external Alertmanager data source. ## Grouping -{{< figure max-width="40%" src="/static/img/docs/alerting/unified/notification-policies-grouping.png" max-width="650px" caption="Notification policies grouping" >}} - Grouping is a new and key concept of Grafana Alerting that categorizes alert notifications of similar nature into a single funnel. This allows you to properly route alert notifications during larger outages when many parts of a system fail at once causing a high number of alerts to fire simultaneously. For example, suppose you have 100 services connected to a database in different environments. These services are differentiated by the label `env=environmentname`. An alert rule is in place to monitor whether your services can reach the database named `alertname=DatabaseUnreachable`. @@ -34,14 +32,14 @@ You can configure grouping to be `group_by: [alertname]` (take note that the `en > **Note:** Grafana also has a special label named `...` that you can use to group all alerts by all labels (effectively disabling grouping), therefore each alert will go into its own group. It is different from the default of `group_by: null` where **all** alerts go into a single group. -## Edit root notification policy +## Edit default notification policy > **Note:** Before Grafana v8.2, the configuration of the embedded Alertmanager was shared across organizations. Users of Grafana 8.0 and 8.1 are advised to use the new Grafana 8 Alerts only if they have one organization. Otherwise, silences for the Grafana managed alerts will be visible by all organizations. 1. In the Grafana menu, click the **Alerting** (bell) icon to open the Alerting page listing existing alerts. 1. Click **Notification policies**. 1. From the **Alertmanager** dropdown, select an external Alertmanager. By default, the Grafana Alertmanager is selected. -1. In the Root policy section, click **Edit** (pen icon). +1. In the Default policy section, click **...** › **Edit** (pen icon). 1. In **Default contact point**, update the contact point to whom notifications should be sent for rules when alert rules do not match any specific policy. 1. In **Group by**, choose labels to group alerts by. If multiple alerts are matched for this policy, then they are grouped by these labels. A notification is sent per group. If the field is empty (default), then all notifications are sent in a single group. Use a special label `...` to group alerts by all labels (which effectively disables grouping). 1. In **Timing options**, select from the following options: @@ -50,7 +48,7 @@ You can configure grouping to be `group_by: [alertname]` (take note that the `en - **Repeat interval** Minimum time interval for re-sending a notification if no new alerts were added to the group. Default is 4 hours. 1. Click **Save** to save your changes. -## Add new specific policy +## Add new nested policy 1. In the Grafana menu, click the **Alerting** (bell) icon to open the Alerting page listing existing alerts. 1. Click **Notification policies**. @@ -59,7 +57,7 @@ You can configure grouping to be `group_by: [alertname]` (take note that the `en 1. In **Matching labels** section, add one or more rules for matching alert labels. 1. In **Contact point**, add the contact point to send notification to if alert matches only this specific policy and not any of the nested policies. 1. Optionally, enable **Continue matching subsequent sibling nodes** to continue matching sibling policies even after the alert matched the current policy. When this option is enabled, you can get more than one notification for one alert. -1. Optionally, enable **Override grouping** to specify the same grouping as the root policy. If this option is not enabled, the root policy grouping is used. +1. Optionally, enable **Override grouping** to specify the same grouping as the default policy. If this option is not enabled, the default policy grouping is used. 1. Optionally, enable **Override general timings** to override the timing options configured in the group notification policy. 1. Click **Save policy** to save your changes. @@ -76,14 +74,29 @@ You can configure grouping to be `group_by: [alertname]` (take note that the `en 1. Make any changes using instructions in [Add new specific policy](#add-new-specific-policy). 1. Click **Save policy**. +## Searching for policies + +Grafana allows you to search within the tree of policies by the following: + +- **Label matchers** +- **Contact Points** + +To search by contact point simply select a contact point from the **Search by contact point** dropdown. The policies that use that contact point will be highlighted in the user interface. + +To search by label matchers simply enter a valid matcher in the **Search by matchers** input field. Multiple matchers can be combined with a comma (`,`). + +An example of a valid matchers search input is: + +`severity=high, region=~EMEA|NASA` + +> All matched policies will be **exact** matches, we currently do not support regex-style or partial matching. + ## Example An example of an alert configuration. -- Create a "default" contact point for slack notifications, and set it on root policy. -- Edit the root policy grouping to group alerts by `cluster`, `namespace` and `severity` so that you get a notification per alert rule and specific kubernetes cluster and namespace. +- Create a "default" contact point for slack notifications, and set it on the default policy. +- Edit the default policy grouping to group alerts by `cluster`, `namespace` and `severity` so that you get a notification per alert rule and specific kubernetes cluster and namespace. - Create specific route for alerts coming from the development cluster with an appropriate contact point. - Create a specific route for alerts with "critical" severity with a more invasive contact point integration, like pager duty notification. -- Create specific routes for particular teams that handle their own onduty rotations. - -{{< figure max-width="40%" src="/static/img/docs/alerting/unified/notification-policies-8-0.png" max-width="650px" caption="Notification policies" >}} +- Create specific routes for particular teams that handle their own on-call rotations. diff --git a/docs/sources/alerting/manage-notifications/mute-timings.md b/docs/sources/alerting/manage-notifications/mute-timings.md index 5c617318848..21842100f2a 100644 --- a/docs/sources/alerting/manage-notifications/mute-timings.md +++ b/docs/sources/alerting/manage-notifications/mute-timings.md @@ -36,7 +36,8 @@ The following table highlights the key differences between mute timings and sile 1. In the Grafana menu, click the **Alerting** (bell) icon to open the Alerting page listing existing alerts. 1. Click **Notification policies**. 1. From the **Alertmanager** dropdown, select an external Alertmanager. By default, the Grafana Alertmanager is selected. -1. At the bottom of the page there will be a section titled **Mute timings**. Click the **Add mute timing** button. +1. Click the **Mute Timings** tab. +1. Click **Add mute timing**. 1. You will be redirected to a form to create a [time interval](#time-intervals) to match against for your mute timing. 1. Click **Submit** to create the mute timing. diff --git a/docs/sources/alerting/manage-notifications/view-alert-groups.md b/docs/sources/alerting/manage-notifications/view-alert-groups.md index aa70d723269..20e78107953 100644 --- a/docs/sources/alerting/manage-notifications/view-alert-groups.md +++ b/docs/sources/alerting/manage-notifications/view-alert-groups.md @@ -17,7 +17,7 @@ weight: 800 # View and filter by alert groups -Alert groups show grouped alerts from an Alertmanager instance. By default, alert rules are grouped by the label keys for the root policy in notification policies. Grouping common alert rules into a single alert group prevents duplicate alert rules from being fired. +Alert groups show grouped alerts from an Alertmanager instance. By default, alert rules are grouped by the label keys for the default policy in notification policies. Grouping common alert rules into a single alert group prevents duplicate alert rules from being fired. You can view alert groups and also filter for alert rules that match specific criteria. @@ -30,7 +30,7 @@ To view alert groups, complete the following steps. 1. From the **Alertmanager** drop-down, select an external Alertmanager as your data source. By default, the `Grafana` Alertmanager is selected. 1. From **custom group by** drop-down, select a combination of labels to view a grouping other than the default. This is useful for debugging and verifying your grouping of notification policies. -If an alert does not contain labels specified either in the grouping of the root policy or the custom grouping, then the alert is added to a catch all group with a header of `No grouping`. +If an alert does not contain labels specified either in the grouping of the default policy or the custom grouping, then the alert is added to a catch all group with a header of `No grouping`. ## Filter alerts diff --git a/docs/sources/tutorials/grafana-fundamentals/index.md b/docs/sources/tutorials/grafana-fundamentals/index.md index 5374ac91ae0..8de83359849 100644 --- a/docs/sources/tutorials/grafana-fundamentals/index.md +++ b/docs/sources/tutorials/grafana-fundamentals/index.md @@ -323,7 +323,9 @@ Now that Grafana knows how to notify us, it's time to set up an alert rule: 1. In **Section 4**, you can add some sample text to your summary message. [Read more about message templating here](/docs/grafana/latest/alerting/unified-alerting/message-templating/). 1. Click **Save and exit** at the top of the page. 1. In Grafana's sidebar, hover the cursor over the **Alerting** (bell) icon and then click **Notification policies**. -1. Under **Root policy**, press **Edit** and change the **Default contact point** to **RequestBin**. As a system grows, admins can use the **Notification policies** setting to organize and match alert rules to specific contact points. +1. Under **Default policy**, select **...** › **Edit** and change the **Default contact point** to **RequestBin**. + As a system grows, admins can use the **Notification policies** setting to organize and match alert rules to + specific contact points. ### Trigger a Grafana Managed Alert From a186f036dd45bd5efc015107669a9e807e282cbf Mon Sep 17 00:00:00 2001 From: Levente Balogh Date: Wed, 8 Mar 2023 17:23:20 +0100 Subject: [PATCH 077/288] Chore: Assign ownership to the Connections feature (#64427) * chore: assign the plugins platform team as the owner for the data-connections feature * chore: remove the toggle from the list of unassigned features --- pkg/services/featuremgmt/registry.go | 1 + pkg/services/featuremgmt/toggles_gen_test.go | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 293e107d81c..b200795ee6d 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -237,6 +237,7 @@ var ( Name: "dataConnectionsConsole", Description: "Enables a new top-level page called Connections. This page is an experiment that provides a better experience when you install and configure data sources and other plugins.", State: FeatureStateAlpha, + Owner: grafanaPluginsPlatformSquad, }, { Name: "internationalization", diff --git a/pkg/services/featuremgmt/toggles_gen_test.go b/pkg/services/featuremgmt/toggles_gen_test.go index 28e5b2cbfa2..ceab6979600 100644 --- a/pkg/services/featuremgmt/toggles_gen_test.go +++ b/pkg/services/featuremgmt/toggles_gen_test.go @@ -49,7 +49,6 @@ func TestFeatureToggleFiles(t *testing.T) { "prometheusWideSeries": true, "disableSecretsCompatibility": true, "logRequestsInstrumentedAsUnknown": true, - "dataConnectionsConsole": true, "cloudWatchCrossAccountQuerying": true, "redshiftAsyncQueryDataSupport": true, "athenaAsyncQueryDataSupport": true, From 5f2fecfda7f658acdef0a8b084032b325c7d36b8 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Wed, 8 Mar 2023 08:26:38 -0800 Subject: [PATCH 078/288] Dashboards: Avoid adding unused revision property (#64362) --- devenv/dev-dashboards-without-uid/panel_tests_graph.json | 1 - devenv/dev-dashboards/panel-graph/graph_tests.json | 1 - devenv/dev-dashboards/panel-heatmap/heatmap-x.json | 1 - devenv/dev-dashboards/panel-histogram/histogram_tests.json | 1 - devenv/dev-dashboards/panel-table/table_tests.json | 1 - .../panel-timeline/timeline-thresholds-mappings.json | 1 - .../panel-timeseries/timeseries-yaxis-ticks.json | 1 - pkg/services/dashboardimport/dashboardimport.go | 4 ++-- pkg/services/dashboardimport/service/service.go | 5 +++-- public/app/features/dashboard/state/DashboardModel.ts | 4 ++-- .../dashboard/state/__fixtures__/dashboardFixtures.ts | 1 - 11 files changed, 7 insertions(+), 14 deletions(-) diff --git a/devenv/dev-dashboards-without-uid/panel_tests_graph.json b/devenv/dev-dashboards-without-uid/panel_tests_graph.json index 6612f9b80f3..2ddb8683484 100644 --- a/devenv/dev-dashboards-without-uid/panel_tests_graph.json +++ b/devenv/dev-dashboards-without-uid/panel_tests_graph.json @@ -1629,7 +1629,6 @@ } ], "refresh": false, - "revision": 8, "schemaVersion": 16, "style": "dark", "tags": ["gdev", "panel-tests"], diff --git a/devenv/dev-dashboards/panel-graph/graph_tests.json b/devenv/dev-dashboards/panel-graph/graph_tests.json index e0e31323052..68475600f11 100644 --- a/devenv/dev-dashboards/panel-graph/graph_tests.json +++ b/devenv/dev-dashboards/panel-graph/graph_tests.json @@ -1629,7 +1629,6 @@ } ], "refresh": false, - "revision": 8, "schemaVersion": 16, "style": "dark", "tags": ["gdev", "panel-tests", "graph"], diff --git a/devenv/dev-dashboards/panel-heatmap/heatmap-x.json b/devenv/dev-dashboards/panel-heatmap/heatmap-x.json index 7c9260f0017..95c413f0fa3 100644 --- a/devenv/dev-dashboards/panel-heatmap/heatmap-x.json +++ b/devenv/dev-dashboards/panel-heatmap/heatmap-x.json @@ -305,7 +305,6 @@ "type": "heatmap" } ], - "revision": 1, "schemaVersion": 37, "style": "dark", "tags": [], diff --git a/devenv/dev-dashboards/panel-histogram/histogram_tests.json b/devenv/dev-dashboards/panel-histogram/histogram_tests.json index 1bb343f7088..cb50285c29e 100644 --- a/devenv/dev-dashboards/panel-histogram/histogram_tests.json +++ b/devenv/dev-dashboards/panel-histogram/histogram_tests.json @@ -748,7 +748,6 @@ "type": "histogram" } ], - "revision": 1, "schemaVersion": 37, "style": "dark", "tags": [ diff --git a/devenv/dev-dashboards/panel-table/table_tests.json b/devenv/dev-dashboards/panel-table/table_tests.json index c301dd23b6a..8aee9afeda7 100644 --- a/devenv/dev-dashboards/panel-table/table_tests.json +++ b/devenv/dev-dashboards/panel-table/table_tests.json @@ -430,7 +430,6 @@ } ], "refresh": false, - "revision": 8, "schemaVersion": 16, "style": "dark", "tags": ["gdev", "panel-tests"], diff --git a/devenv/dev-dashboards/panel-timeline/timeline-thresholds-mappings.json b/devenv/dev-dashboards/panel-timeline/timeline-thresholds-mappings.json index be7a3567bfa..9d15ac1d64c 100644 --- a/devenv/dev-dashboards/panel-timeline/timeline-thresholds-mappings.json +++ b/devenv/dev-dashboards/panel-timeline/timeline-thresholds-mappings.json @@ -736,7 +736,6 @@ } ], "refresh": false, - "revision": 1, "schemaVersion": 38, "style": "dark", "tags": [ diff --git a/devenv/dev-dashboards/panel-timeseries/timeseries-yaxis-ticks.json b/devenv/dev-dashboards/panel-timeseries/timeseries-yaxis-ticks.json index 7650a8aab56..6de22c2b6ed 100644 --- a/devenv/dev-dashboards/panel-timeseries/timeseries-yaxis-ticks.json +++ b/devenv/dev-dashboards/panel-timeseries/timeseries-yaxis-ticks.json @@ -1343,7 +1343,6 @@ } ], "refresh": "", - "revision": 1, "schemaVersion": 38, "style": "dark", "tags": [ diff --git a/pkg/services/dashboardimport/dashboardimport.go b/pkg/services/dashboardimport/dashboardimport.go index e3fccb093ef..cafa322845d 100644 --- a/pkg/services/dashboardimport/dashboardimport.go +++ b/pkg/services/dashboardimport/dashboardimport.go @@ -40,8 +40,8 @@ type ImportDashboardResponse struct { DashboardId int64 `json:"dashboardId"` FolderId int64 `json:"folderId"` FolderUID string `json:"folderUid"` - ImportedRevision int64 `json:"importedRevision"` - Revision int64 `json:"revision"` + ImportedRevision int64 `json:"importedRevision,omitempty"` // Only used for plugin imports + Revision int64 `json:"revision,omitempty"` // Only used for plugin imports Description string `json:"description"` Path string `json:"path"` Removed bool `json:"removed"` diff --git a/pkg/services/dashboardimport/service/service.go b/pkg/services/dashboardimport/service/service.go index bad30433a76..3851dca86a9 100644 --- a/pkg/services/dashboardimport/service/service.go +++ b/pkg/services/dashboardimport/service/service.go @@ -136,17 +136,18 @@ func (s *ImportDashboardService) ImportDashboard(ctx context.Context, req *dashb return nil, err } + revision := savedDashboard.Data.Get("revision").MustInt64(0) return &dashboardimport.ImportDashboardResponse{ UID: savedDashboard.UID, PluginId: req.PluginId, Title: savedDashboard.Title, Path: req.Path, - Revision: savedDashboard.Data.Get("revision").MustInt64(1), + Revision: revision, // only used for plugin version tracking FolderId: savedDashboard.FolderID, FolderUID: req.FolderUid, ImportedUri: "db/" + savedDashboard.Slug, ImportedUrl: savedDashboard.GetURL(), - ImportedRevision: savedDashboard.Data.Get("revision").MustInt64(1), + ImportedRevision: revision, Imported: true, DashboardId: savedDashboard.ID, Slug: savedDashboard.Slug, diff --git a/public/app/features/dashboard/state/DashboardModel.ts b/public/app/features/dashboard/state/DashboardModel.ts index bcbdf6e7aa0..a88c02a66fe 100644 --- a/public/app/features/dashboard/state/DashboardModel.ts +++ b/public/app/features/dashboard/state/DashboardModel.ts @@ -90,7 +90,7 @@ export class DashboardModel implements TimeModel { snapshot: any; schemaVersion: number; version: number; - revision: number; + revision?: number; // Only used for dashboards managed by plugins links: DashboardLink[]; gnetId: any; panels: PanelModel[]; @@ -131,7 +131,7 @@ export class DashboardModel implements TimeModel { this.id = data.id || null; // UID is not there for newly created dashboards this.uid = data.uid || null; - this.revision = data.revision || 1; + this.revision = data.revision ?? undefined; this.title = data.title ?? 'No Title'; this.description = data.description; this.tags = data.tags ?? []; diff --git a/public/app/features/dashboard/state/__fixtures__/dashboardFixtures.ts b/public/app/features/dashboard/state/__fixtures__/dashboardFixtures.ts index 20cc4af46e1..4741059fa22 100644 --- a/public/app/features/dashboard/state/__fixtures__/dashboardFixtures.ts +++ b/public/app/features/dashboard/state/__fixtures__/dashboardFixtures.ts @@ -22,7 +22,6 @@ export function createDashboardModelFixture( editable: true, graphTooltip: defaultDashboardCursorSync, schemaVersion: 1, - revision: 1, style: 'dark', timezone: '', ...dashboardInput, From 74ed7ead16ede08185f2e90967f79073c98345df Mon Sep 17 00:00:00 2001 From: Brendan O'Handley Date: Wed, 8 Mar 2023 11:51:42 -0500 Subject: [PATCH 079/288] FeatureFlags: Add "O11y-metrics" as Owner for metrics feature flags (#64438) add o11y-metrics as Owner for metrics feature flags --- pkg/services/featuremgmt/registry.go | 2 ++ pkg/services/featuremgmt/toggles_gen_test.go | 30 +++++++++----------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index b200795ee6d..1022d660ab1 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -207,6 +207,7 @@ var ( Name: "prometheusWideSeries", Description: "Enable wide series responses in the Prometheus datasource", State: FeatureStateAlpha, + Owner: "O11y-metrics", }, { Name: "canvasPanelNesting", @@ -353,6 +354,7 @@ var ( Name: "disablePrometheusExemplarSampling", Description: "Disable Prometheus examplar sampling", State: FeatureStateStable, + Owner: "O11y-metrics", }, { Name: "alertingBacktesting", diff --git a/pkg/services/featuremgmt/toggles_gen_test.go b/pkg/services/featuremgmt/toggles_gen_test.go index ceab6979600..b798dcce42e 100644 --- a/pkg/services/featuremgmt/toggles_gen_test.go +++ b/pkg/services/featuremgmt/toggles_gen_test.go @@ -42,22 +42,20 @@ func TestFeatureToggleFiles(t *testing.T) { }) ownerlessFeatures := map[string]bool{ - "database_metrics": true, - "prometheusAzureOverrideAudience": true, - "tracing": true, - "cloudWatchDynamicLabels": true, - "prometheusWideSeries": true, - "disableSecretsCompatibility": true, - "logRequestsInstrumentedAsUnknown": true, - "cloudWatchCrossAccountQuerying": true, - "redshiftAsyncQueryDataSupport": true, - "athenaAsyncQueryDataSupport": true, - "newPanelChromeUI": true, - "showDashboardValidationWarnings": true, - "datasourceOnboarding": true, - "secureSocksDatasourceProxy": true, - "disablePrometheusExemplarSampling": true, - "individualCookiePreferences": true, + "database_metrics": true, + "prometheusAzureOverrideAudience": true, + "tracing": true, + "cloudWatchDynamicLabels": true, + "disableSecretsCompatibility": true, + "logRequestsInstrumentedAsUnknown": true, + "cloudWatchCrossAccountQuerying": true, + "redshiftAsyncQueryDataSupport": true, + "athenaAsyncQueryDataSupport": true, + "newPanelChromeUI": true, + "showDashboardValidationWarnings": true, + "datasourceOnboarding": true, + "secureSocksDatasourceProxy": true, + "individualCookiePreferences": true, } t.Run("all new features should have an owner", func(t *testing.T) { From 3e89ffa2e69713e364fc0218f9bbcdf6faaf43f7 Mon Sep 17 00:00:00 2001 From: Artur Wierzbicki Date: Wed, 8 Mar 2023 21:25:12 +0400 Subject: [PATCH 080/288] Chore: assign feature flag ownership for GaaS (#64420) * ownership for GaaS * fix tests * merge --- pkg/services/featuremgmt/codeowners.go | 1 + pkg/services/featuremgmt/registry.go | 4 ++++ pkg/services/featuremgmt/toggles_gen_test.go | 24 ++++++++------------ 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/pkg/services/featuremgmt/codeowners.go b/pkg/services/featuremgmt/codeowners.go index b53d08c58fc..956ef9766a6 100644 --- a/pkg/services/featuremgmt/codeowners.go +++ b/pkg/services/featuremgmt/codeowners.go @@ -18,4 +18,5 @@ const ( grafanaObservabilityLogsSquad codeowner = "@grafana/observability-logs" grafanaObservabilityTracesAndProfilingSquad codeowner = "@grafana/observability-traces-and-profiling" grafanaAlertingSquad codeowner = "@grafana/alerting-squad" + hostedGrafanaTeam codeowner = "@grafana/hosted-grafana-team" ) diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 1022d660ab1..8c94209c18d 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -31,6 +31,7 @@ var ( Name: "database_metrics", Description: "Add Prometheus metrics for database tables", State: FeatureStateStable, + Owner: hostedGrafanaTeam, }, { Name: "dashboardPreviews", @@ -228,11 +229,13 @@ var ( Description: "Disable duplicated secret storage in legacy tables", State: FeatureStateAlpha, RequiresRestart: true, + Owner: hostedGrafanaTeam, }, { Name: "logRequestsInstrumentedAsUnknown", Description: "Logs the path for requests that are instrumented as unknown", State: FeatureStateAlpha, + Owner: hostedGrafanaTeam, }, { Name: "dataConnectionsConsole", @@ -343,6 +346,7 @@ var ( Name: "secureSocksDatasourceProxy", Description: "Enable secure socks tunneling for supported core datasources", State: FeatureStateAlpha, + Owner: hostedGrafanaTeam, }, { Name: "authnService", diff --git a/pkg/services/featuremgmt/toggles_gen_test.go b/pkg/services/featuremgmt/toggles_gen_test.go index b798dcce42e..733f9b25811 100644 --- a/pkg/services/featuremgmt/toggles_gen_test.go +++ b/pkg/services/featuremgmt/toggles_gen_test.go @@ -42,20 +42,16 @@ func TestFeatureToggleFiles(t *testing.T) { }) ownerlessFeatures := map[string]bool{ - "database_metrics": true, - "prometheusAzureOverrideAudience": true, - "tracing": true, - "cloudWatchDynamicLabels": true, - "disableSecretsCompatibility": true, - "logRequestsInstrumentedAsUnknown": true, - "cloudWatchCrossAccountQuerying": true, - "redshiftAsyncQueryDataSupport": true, - "athenaAsyncQueryDataSupport": true, - "newPanelChromeUI": true, - "showDashboardValidationWarnings": true, - "datasourceOnboarding": true, - "secureSocksDatasourceProxy": true, - "individualCookiePreferences": true, + "prometheusAzureOverrideAudience": true, + "tracing": true, + "cloudWatchDynamicLabels": true, + "cloudWatchCrossAccountQuerying": true, + "redshiftAsyncQueryDataSupport": true, + "athenaAsyncQueryDataSupport": true, + "newPanelChromeUI": true, + "showDashboardValidationWarnings": true, + "datasourceOnboarding": true, + "individualCookiePreferences": true, } t.Run("all new features should have an owner", func(t *testing.T) { From 5a4ebe3a67d50bf1fcf79afeff58f50881bc2f07 Mon Sep 17 00:00:00 2001 From: gotjosh Date: Wed, 8 Mar 2023 17:34:17 +0000 Subject: [PATCH 081/288] Alerting: Update Prometheus Alertmanager (#64434) This includes a fix to duplicate notification under certain conditions, the details can be found at: https://github.com/grafana/prometheus-alertmanager/pull/30 --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 0b0c9681fa5..fdc268ef937 100644 --- a/go.mod +++ b/go.mod @@ -416,7 +416,7 @@ replace xorm.io/xorm => github.com/grafana/xorm v0.8.3-0.20220614223926-2fcda756 // Use our fork of the upstream alertmanagers. // This is required in order to get notification delivery errors from the receivers API. -replace github.com/prometheus/alertmanager => github.com/grafana/prometheus-alertmanager v0.25.1-0.20230119183635-ec19b0a443b7 +replace github.com/prometheus/alertmanager => github.com/grafana/prometheus-alertmanager v0.25.1-0.20230308154952-78fedf89728b // grpc v1.46.0 removed "WithBalancerName()" API, still in use by weaveworks/commons. replace google.golang.org/grpc => google.golang.org/grpc v1.45.0 diff --git a/go.sum b/go.sum index 86f37dd3b9f..e26c33bdc7b 100644 --- a/go.sum +++ b/go.sum @@ -1279,8 +1279,8 @@ github.com/grafana/grafana-plugin-sdk-go v0.153.0 h1:5Z3NU/W32BsElkiqalvh6ow0Mx8 github.com/grafana/grafana-plugin-sdk-go v0.153.0/go.mod h1:4f/8Gf6xMwqXhOmS5U2RPmKQ2UgyA0bVteM/gxGFaCI= github.com/grafana/phlare/api v0.1.3 h1:mYTaE9mLsAW/uzPXlW/PQSLsZ4ojBFA+oAMfR/PDdw8= github.com/grafana/phlare/api v0.1.3/go.mod h1:29vcLwFDmZBDce2jwFIMtzvof7fzPadT8VMKw9ks7FU= -github.com/grafana/prometheus-alertmanager v0.25.1-0.20230119183635-ec19b0a443b7 h1:ma1CfisUaAXQzL24tCao9yMleZYsFJ853m2l0rgahyE= -github.com/grafana/prometheus-alertmanager v0.25.1-0.20230119183635-ec19b0a443b7/go.mod h1:MnBfDPXJqXmmfPwQlCLvVUdqfnvrAw+hSPtDeaaFwj4= +github.com/grafana/prometheus-alertmanager v0.25.1-0.20230308154952-78fedf89728b h1:VQOGGGJ2lKcVPANyzIESKYhSeA0QIvUQwfA3CbrkDfA= +github.com/grafana/prometheus-alertmanager v0.25.1-0.20230308154952-78fedf89728b/go.mod h1:MnBfDPXJqXmmfPwQlCLvVUdqfnvrAw+hSPtDeaaFwj4= github.com/grafana/saml v0.4.13-0.20230203140620-5f476db5c00a h1:aWSTt/pTOI4uGY9DhBMG1l0GOnGjIYtaqxzYR3/q82o= github.com/grafana/saml v0.4.13-0.20230203140620-5f476db5c00a/go.mod h1:igEejV+fihTIlHXYP8zOec3V5A8y3lws5bQBFsTm4gA= github.com/grafana/sqlds/v2 v2.3.10 h1:HWKhE0vR6LoEiE+Is8CSZOgaB//D1yqb2ntkass9Fd4= From 4625958aeafb221d58bc55715af3a2e0287108e6 Mon Sep 17 00:00:00 2001 From: Andre Pereira Date: Wed, 8 Mar 2023 17:41:03 +0000 Subject: [PATCH 082/288] Trace View: Removed part of flaky test (#64445) Removed part of flaky test on Trace View --- .../TraceTimelineViewer/VirtualizedTraceView.test.tsx | 9 --------- 1 file changed, 9 deletions(-) diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/VirtualizedTraceView.test.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/VirtualizedTraceView.test.tsx index e387616ce86..5d168086483 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/VirtualizedTraceView.test.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/VirtualizedTraceView.test.tsx @@ -77,16 +77,7 @@ describe('', () => { durationSpan0 = Math.floor(trace.spans[0].duration / 1000); } - let durationSpan1 = trace.spans[1].duration; - - if (trace.spans[1].duration >= 1_000_000) { - durationSpan1 = Math.floor(trace.spans[1].duration / 1000000); - } else if (trace.spans[1].duration >= 1000) { - durationSpan1 = Math.floor(trace.spans[1].duration / 1000); - } - expect(screen.getAllByText(durationSpan0, { exact: false })).toBeTruthy(); - expect(screen.getAllByText(durationSpan1, { exact: false })).toBeTruthy(); }); it('renders without exploding', () => { From 0b0140b761f67c7424915131cd8693479c959cdb Mon Sep 17 00:00:00 2001 From: "lean.dev" <34773040+leandro-deveikis@users.noreply.github.com> Date: Wed, 8 Mar 2023 14:44:04 -0300 Subject: [PATCH 083/288] Licensing: Send the app url to plugin (#64258) --- pkg/plugins/ifaces.go | 2 ++ pkg/plugins/licensing/licensing.go | 6 ++++++ pkg/plugins/manager/fakes/fakes.go | 5 +++++ pkg/plugins/manager/loader/initializer/initializer.go | 1 + pkg/plugins/manager/loader/initializer/initializer_test.go | 6 ++++-- 5 files changed, 18 insertions(+), 2 deletions(-) diff --git a/pkg/plugins/ifaces.go b/pkg/plugins/ifaces.go index fac68172d8a..0f17544da52 100644 --- a/pkg/plugins/ifaces.go +++ b/pkg/plugins/ifaces.go @@ -99,6 +99,8 @@ type Licensing interface { Edition() string Path() string + + AppURL() string } // RoleRegistry handles the plugin RBAC roles and their assignments diff --git a/pkg/plugins/licensing/licensing.go b/pkg/plugins/licensing/licensing.go index 9a00e5828c2..e2d4c9c76f7 100644 --- a/pkg/plugins/licensing/licensing.go +++ b/pkg/plugins/licensing/licensing.go @@ -9,12 +9,14 @@ import ( type Service struct { licensePath string + appURL string license licensing.Licensing } func ProvideLicensing(cfg *setting.Cfg, l licensing.Licensing) *Service { return &Service{ licensePath: cfg.EnterpriseLicensePath, + appURL: cfg.AppURL, license: l, } } @@ -36,3 +38,7 @@ func (l Service) Edition() string { func (l Service) Path() string { return l.licensePath } + +func (l Service) AppURL() string { + return l.appURL +} diff --git a/pkg/plugins/manager/fakes/fakes.go b/pkg/plugins/manager/fakes/fakes.go index 394cce815eb..e1d765dd4b6 100644 --- a/pkg/plugins/manager/fakes/fakes.go +++ b/pkg/plugins/manager/fakes/fakes.go @@ -322,6 +322,7 @@ type FakeLicensingService struct { LicenseEdition string TokenRaw string LicensePath string + LicenseAppURL string } func NewFakeLicensingService() *FakeLicensingService { @@ -336,6 +337,10 @@ func (s *FakeLicensingService) Path() string { return s.LicensePath } +func (s *FakeLicensingService) AppURL() string { + return s.LicenseAppURL +} + func (s *FakeLicensingService) Environment() []string { return []string{fmt.Sprintf("GF_ENTERPRISE_LICENSE_TEXT=%s", s.TokenRaw)} } diff --git a/pkg/plugins/manager/loader/initializer/initializer.go b/pkg/plugins/manager/loader/initializer/initializer.go index 2373b0abc74..4c9a8e06322 100644 --- a/pkg/plugins/manager/loader/initializer/initializer.go +++ b/pkg/plugins/manager/loader/initializer/initializer.go @@ -57,6 +57,7 @@ func (i *Initializer) envVars(plugin *plugins.Plugin) []string { hostEnv, fmt.Sprintf("GF_EDITION=%s", i.license.Edition()), fmt.Sprintf("GF_ENTERPRISE_LICENSE_PATH=%s", i.license.Path()), + fmt.Sprintf("GF_ENTERPRISE_APP_URL=%s", i.license.AppURL()), ) hostEnv = append(hostEnv, i.license.Environment()...) } diff --git a/pkg/plugins/manager/loader/initializer/initializer_test.go b/pkg/plugins/manager/loader/initializer/initializer_test.go index 6cc505b274f..4b0d662acd0 100644 --- a/pkg/plugins/manager/loader/initializer/initializer_test.go +++ b/pkg/plugins/manager/loader/initializer/initializer_test.go @@ -140,6 +140,7 @@ func TestInitializer_envVars(t *testing.T) { LicenseEdition: "test", TokenRaw: "token", LicensePath: "/path/to/ent/license", + LicenseAppURL: "https://myorg.com/", } i := &Initializer{ @@ -158,12 +159,13 @@ func TestInitializer_envVars(t *testing.T) { } envVars := i.envVars(p) - assert.Len(t, envVars, 5) + assert.Len(t, envVars, 6) assert.Equal(t, "GF_PLUGIN_CUSTOM_ENV_VAR=customVal", envVars[0]) assert.Equal(t, "GF_VERSION=", envVars[1]) assert.Equal(t, "GF_EDITION=test", envVars[2]) assert.Equal(t, "GF_ENTERPRISE_LICENSE_PATH=/path/to/ent/license", envVars[3]) - assert.Equal(t, "GF_ENTERPRISE_LICENSE_TEXT=token", envVars[4]) + assert.Equal(t, "GF_ENTERPRISE_APP_URL=https://myorg.com/", envVars[4]) + assert.Equal(t, "GF_ENTERPRISE_LICENSE_TEXT=token", envVars[5]) }) } From a40f95e8a60dd57ed44814a863ea9e91da53c68d Mon Sep 17 00:00:00 2001 From: MichaIng Date: Wed, 8 Mar 2023 18:47:11 +0100 Subject: [PATCH 084/288] Packaging: Start Grafana service after InfluxDB (#64090) as done with other database services, supported as Grafana data sources. Signed-off-by: MichaIng --- packaging/deb/systemd/grafana-server.service | 2 +- packaging/rpm/systemd/grafana-server.service | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packaging/deb/systemd/grafana-server.service b/packaging/deb/systemd/grafana-server.service index 2d66ce889e3..e9b6ccb7957 100644 --- a/packaging/deb/systemd/grafana-server.service +++ b/packaging/deb/systemd/grafana-server.service @@ -3,7 +3,7 @@ Description=Grafana instance Documentation=http://docs.grafana.org Wants=network-online.target After=network-online.target -After=postgresql.service mariadb.service mysql.service +After=postgresql.service mariadb.service mysql.service influxdb.service [Service] EnvironmentFile=/etc/default/grafana-server diff --git a/packaging/rpm/systemd/grafana-server.service b/packaging/rpm/systemd/grafana-server.service index 9752271c6e4..e3adc3f4697 100644 --- a/packaging/rpm/systemd/grafana-server.service +++ b/packaging/rpm/systemd/grafana-server.service @@ -3,7 +3,7 @@ Description=Grafana instance Documentation=http://docs.grafana.org Wants=network-online.target After=network-online.target -After=postgresql.service mariadb.service mysqld.service +After=postgresql.service mariadb.service mysqld.service influxdb.service [Service] EnvironmentFile=/etc/sysconfig/grafana-server From 2ddf105257a438c6c0cce4440df1da416ee57e39 Mon Sep 17 00:00:00 2001 From: juanicabanas Date: Wed, 8 Mar 2023 14:52:51 -0300 Subject: [PATCH 085/288] PublicDashboards: Not available page wording modifications (#64413) --- .../PublicDashboardNotAvailable.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/public/app/features/dashboard/components/PublicDashboardNotAvailable/PublicDashboardNotAvailable.tsx b/public/app/features/dashboard/components/PublicDashboardNotAvailable/PublicDashboardNotAvailable.tsx index 363afd720ba..9e493da1eb7 100644 --- a/public/app/features/dashboard/components/PublicDashboardNotAvailable/PublicDashboardNotAvailable.tsx +++ b/public/app/features/dashboard/components/PublicDashboardNotAvailable/PublicDashboardNotAvailable.tsx @@ -21,12 +21,12 @@ export const PublicDashboardNotAvailable = ({ paused }: { paused?: boolean }) =>

{paused - ? 'The dashboard has been temporarily paused by the administrator.' - : 'The dashboard your are trying to access does not exist.'} + ? 'This dashboard has been paused by the administrator' + : 'The dashboard your are trying to access does not exist'}

{paused && (

- Please check again soon. + Try again later

)}
From fbe3bdc8f5343b99ad61b4137fcbc05a7444806b Mon Sep 17 00:00:00 2001 From: Artur Wierzbicki Date: Wed, 8 Mar 2023 22:12:22 +0400 Subject: [PATCH 086/288] Chore: feature toggle ownership for aws plugins, observability metrics squad and backend platform (#64448) aws plugins, backend platform --- pkg/services/featuremgmt/codeowners.go | 2 ++ pkg/services/featuremgmt/registry.go | 11 ++++++++--- pkg/services/featuremgmt/toggles_gen_test.go | 5 ----- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/pkg/services/featuremgmt/codeowners.go b/pkg/services/featuremgmt/codeowners.go index 956ef9766a6..89774222968 100644 --- a/pkg/services/featuremgmt/codeowners.go +++ b/pkg/services/featuremgmt/codeowners.go @@ -17,6 +17,8 @@ const ( grafanaAuthnzSquad codeowner = "@grafana/grafana-authnz-team" grafanaObservabilityLogsSquad codeowner = "@grafana/observability-logs" grafanaObservabilityTracesAndProfilingSquad codeowner = "@grafana/observability-traces-and-profiling" + grafanaObservabilityMetricsSquad codeowner = "@grafana/observability-metrics" grafanaAlertingSquad codeowner = "@grafana/alerting-squad" hostedGrafanaTeam codeowner = "@grafana/hosted-grafana-team" + awsPluginsSquad codeowner = "@grafana/aws-plugins" ) diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 8c94209c18d..9b021b2f59b 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -170,6 +170,7 @@ var ( Description: "Use dynamic labels instead of alias patterns in CloudWatch datasource", State: FeatureStateStable, Expression: "true", // enabled by default + Owner: awsPluginsSquad, }, { Name: "datasourceQueryMultiStatus", @@ -208,7 +209,7 @@ var ( Name: "prometheusWideSeries", Description: "Enable wide series responses in the Prometheus datasource", State: FeatureStateAlpha, - Owner: "O11y-metrics", + Owner: grafanaObservabilityMetricsSquad, }, { Name: "canvasPanelNesting", @@ -275,18 +276,21 @@ var ( Description: "Enables cross-account querying in CloudWatch datasources", State: FeatureStateStable, Expression: "true", //enabled by default + Owner: awsPluginsSquad, }, { Name: "redshiftAsyncQueryDataSupport", Description: "Enable async query data support for Redshift", State: FeatureStateAlpha, FrontendOnly: true, + Owner: awsPluginsSquad, }, { Name: "athenaAsyncQueryDataSupport", Description: "Enable async query data support for Athena", State: FeatureStateAlpha, FrontendOnly: true, + Owner: awsPluginsSquad, }, { Name: "newPanelChromeUI", @@ -358,7 +362,7 @@ var ( Name: "disablePrometheusExemplarSampling", Description: "Disable Prometheus examplar sampling", State: FeatureStateStable, - Owner: "O11y-metrics", + Owner: grafanaObservabilityMetricsSquad, }, { Name: "alertingBacktesting", @@ -407,6 +411,7 @@ var ( Name: "individualCookiePreferences", Description: "Support overriding cookie preferences per user", State: FeatureStateAlpha, + Owner: grafanaBackendPlatformSquad, }, { Name: "drawerDataSourcePicker", @@ -427,7 +432,7 @@ var ( Description: "Replaces the Prometheus query builder metric select option with a paginated and filterable component", State: FeatureStateAlpha, FrontendOnly: true, - Owner: "O11y-metrics", + Owner: grafanaObservabilityMetricsSquad, }, } ) diff --git a/pkg/services/featuremgmt/toggles_gen_test.go b/pkg/services/featuremgmt/toggles_gen_test.go index 733f9b25811..d233d40a58f 100644 --- a/pkg/services/featuremgmt/toggles_gen_test.go +++ b/pkg/services/featuremgmt/toggles_gen_test.go @@ -44,14 +44,9 @@ func TestFeatureToggleFiles(t *testing.T) { ownerlessFeatures := map[string]bool{ "prometheusAzureOverrideAudience": true, "tracing": true, - "cloudWatchDynamicLabels": true, - "cloudWatchCrossAccountQuerying": true, - "redshiftAsyncQueryDataSupport": true, - "athenaAsyncQueryDataSupport": true, "newPanelChromeUI": true, "showDashboardValidationWarnings": true, "datasourceOnboarding": true, - "individualCookiePreferences": true, } t.Run("all new features should have an owner", func(t *testing.T) { From 39a4634ae9e336eab505680416ca700c19c04d10 Mon Sep 17 00:00:00 2001 From: Stephanie Hingtgen Date: Wed, 8 Mar 2023 12:21:01 -0600 Subject: [PATCH 087/288] Plugins: Fix bug with copying grafanaData (#64446) --- pkg/services/datasources/service/datasource.go | 5 +++-- pkg/services/datasources/service/datasource_test.go | 4 ++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/pkg/services/datasources/service/datasource.go b/pkg/services/datasources/service/datasource.go index 4be24fa75dc..2d3b01be8eb 100644 --- a/pkg/services/datasources/service/datasource.go +++ b/pkg/services/datasources/service/datasource.go @@ -424,10 +424,11 @@ func (s *Service) httpClientOptions(ctx context.Context, ds *datasources.DataSou if ds.JsonData != nil { opts.CustomOptions = ds.JsonData.MustMap() // allow the plugin sdk to get the json data in JSONDataFromHTTPClientOptions - opts.CustomOptions["grafanaData"] = make(map[string]interface{}) + deepJsonDataCopy := make(map[string]interface{}, len(opts.CustomOptions)) for k, v := range opts.CustomOptions { - opts.CustomOptions[k] = v + deepJsonDataCopy[k] = v } + opts.CustomOptions["grafanaData"] = deepJsonDataCopy } if ds.BasicAuth { password, err := s.DecryptedBasicAuthPassword(ctx, ds) diff --git a/pkg/services/datasources/service/datasource_test.go b/pkg/services/datasources/service/datasource_test.go index 1c8b0cf4228..44327ca0d42 100644 --- a/pkg/services/datasources/service/datasource_test.go +++ b/pkg/services/datasources/service/datasource_test.go @@ -427,6 +427,10 @@ func TestService_GetHttpTransport(t *testing.T) { require.NotNil(t, rt) tr := configuredTransport + opts, err := dsService.httpClientOptions(context.Background(), &ds) + require.NoError(t, err) + require.Equal(t, ds.JsonData.MustMap()["grafanaData"], opts.CustomOptions["grafanaData"]) + // make sure we can still marshal the JsonData after httpClientOptions (avoid cycles) _, err = ds.JsonData.MarshalJSON() require.NoError(t, err) From 42e7ec9fe4d9da2ec26f9e67bd3a6880f96576e3 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Wed, 8 Mar 2023 11:37:45 -0800 Subject: [PATCH 088/288] Chore: cleanup dashboard service names (#64442) --- pkg/api/dashboard_permission_test.go | 2 +- pkg/api/dashboard_test.go | 4 ++-- pkg/api/folder_permission_test.go | 2 +- pkg/cmd/grafana-cli/runner/wire.go | 4 ++-- pkg/server/wire.go | 8 ++++---- pkg/services/dashboards/service/dashboard_service.go | 2 +- .../service/dashboard_service_integration_test.go | 10 +++++----- pkg/services/dashboards/service/service.go | 6 +++--- pkg/services/libraryelements/libraryelements_test.go | 4 ++-- pkg/services/librarypanels/librarypanels_test.go | 2 +- pkg/services/ngalert/tests/util.go | 2 +- 11 files changed, 23 insertions(+), 23 deletions(-) diff --git a/pkg/api/dashboard_permission_test.go b/pkg/api/dashboard_permission_test.go index 7bbfe2e481e..32784bb4130 100644 --- a/pkg/api/dashboard_permission_test.go +++ b/pkg/api/dashboard_permission_test.go @@ -46,7 +46,7 @@ func TestDashboardPermissionAPIEndpoint(t *testing.T) { Cfg: settings, SQLStore: mockSQLStore, Features: features, - DashboardService: dashboardservice.ProvideDashboardService( + DashboardService: dashboardservice.ProvideDashboardServiceImpl( settings, dashboardStore, foldertest.NewFakeFolderStore(t), nil, features, folderPermissions, dashboardPermissions, ac, folderSvc, ), diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index 80f4f151375..f43c59da28e 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -986,7 +986,7 @@ func getDashboardShouldReturn200WithConfig(t *testing.T, sc *scenarioContext, pr cfg, dashboardStore, folderStore, db.InitTestDB(t), featuremgmt.WithFeatures()) if dashboardService == nil { - dashboardService = service.ProvideDashboardService( + dashboardService = service.ProvideDashboardServiceImpl( cfg, dashboardStore, folderStore, nil, features, folderPermissions, dashboardPermissions, ac, folderSvc, ) @@ -999,7 +999,7 @@ func getDashboardShouldReturn200WithConfig(t *testing.T, sc *scenarioContext, pr SQLStore: sc.sqlStore, ProvisioningService: provisioningService, AccessControl: accesscontrolmock.New(), - dashboardProvisioningService: service.ProvideDashboardService( + dashboardProvisioningService: service.ProvideDashboardServiceImpl( cfg, dashboardStore, folderStore, nil, features, folderPermissions, dashboardPermissions, ac, folderSvc, ), diff --git a/pkg/api/folder_permission_test.go b/pkg/api/folder_permission_test.go index 9f2190aa1f0..aa9f64988c5 100644 --- a/pkg/api/folder_permission_test.go +++ b/pkg/api/folder_permission_test.go @@ -44,7 +44,7 @@ func TestFolderPermissionAPIEndpoint(t *testing.T) { folderService: folderService, folderPermissionsService: folderPermissions, dashboardPermissionsService: dashboardPermissions, - DashboardService: service.ProvideDashboardService( + DashboardService: service.ProvideDashboardServiceImpl( settings, dashboardStore, foldertest.NewFakeFolderStore(t), nil, features, folderPermissions, dashboardPermissions, ac, folderService, ), diff --git a/pkg/cmd/grafana-cli/runner/wire.go b/pkg/cmd/grafana-cli/runner/wire.go index e43ddece6ad..4bfe0f4f9a0 100644 --- a/pkg/cmd/grafana-cli/runner/wire.go +++ b/pkg/cmd/grafana-cli/runner/wire.go @@ -251,9 +251,9 @@ var wireSet = wire.NewSet( teamguardianDatabase.ProvideTeamGuardianStore, wire.Bind(new(teamguardian.Store), new(*teamguardianDatabase.TeamGuardianStoreImpl)), teamguardianManager.ProvideService, - dashboardservice.ProvideDashboardService, //DashboardServiceImpl + dashboardservice.ProvideDashboardServiceImpl, dashboardstore.ProvideDashboardStore, - dashboardservice.ProvideSimpleDashboardService, + dashboardservice.ProvideDashboardService, dashboardservice.ProvideDashboardProvisioningService, dashboardservice.ProvideDashboardPluginService, folderimpl.ProvideDashboardFolderStore, diff --git a/pkg/server/wire.go b/pkg/server/wire.go index 576ea5dfdd7..00bee848218 100644 --- a/pkg/server/wire.go +++ b/pkg/server/wire.go @@ -290,13 +290,13 @@ var wireBasicSet = wire.NewSet( teamguardianManager.ProvideService, featuremgmt.ProvideManagerService, featuremgmt.ProvideToggles, - dashboardservice.ProvideDashboardService, // DashboardServiceImpl + dashboardservice.ProvideDashboardServiceImpl, + dashboardservice.ProvideDashboardService, + dashboardservice.ProvideDashboardProvisioningService, + dashboardservice.ProvideDashboardPluginService, dashboardstore.ProvideDashboardStore, folderimpl.ProvideService, folderimpl.ProvideDashboardFolderStore, - dashboardservice.ProvideSimpleDashboardService, - dashboardservice.ProvideDashboardProvisioningService, - dashboardservice.ProvideDashboardPluginService, wire.Bind(new(folder.FolderStore), new(*folderimpl.DashboardFolderStoreImpl)), dashboardimportservice.ProvideService, wire.Bind(new(dashboardimport.Service), new(*dashboardimportservice.ImportDashboardService)), diff --git a/pkg/services/dashboards/service/dashboard_service.go b/pkg/services/dashboards/service/dashboard_service.go index ade2cca5048..42a9d9fbbcc 100644 --- a/pkg/services/dashboards/service/dashboard_service.go +++ b/pkg/services/dashboards/service/dashboard_service.go @@ -50,7 +50,7 @@ type DashboardServiceImpl struct { } // This is the uber service that implements a three smaller services -func ProvideDashboardService( +func ProvideDashboardServiceImpl( cfg *setting.Cfg, dashboardStore dashboards.Store, folderStore folder.FolderStore, dashAlertExtractor alerting.DashAlertExtractor, features featuremgmt.FeatureToggles, folderPermissionsService accesscontrol.FolderPermissionsService, dashboardPermissionsService accesscontrol.DashboardPermissionsService, ac accesscontrol.AccessControl, diff --git a/pkg/services/dashboards/service/dashboard_service_integration_test.go b/pkg/services/dashboards/service/dashboard_service_integration_test.go index f91ffbe27c3..f6f1f4a5020 100644 --- a/pkg/services/dashboards/service/dashboard_service_integration_test.go +++ b/pkg/services/dashboards/service/dashboard_service_integration_test.go @@ -828,7 +828,7 @@ func permissionScenario(t *testing.T, desc string, canSave bool, fn permissionSc dashboardStore, err := database.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotaService) require.NoError(t, err) folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore) - service := ProvideDashboardService( + service := ProvideDashboardServiceImpl( cfg, dashboardStore, folderStore, &dummyDashAlertExtractor{}, featuremgmt.WithFeatures(), accesscontrolmock.NewMockedPermissionsService(), @@ -889,7 +889,7 @@ func callSaveWithResult(t *testing.T, cmd dashboards.SaveDashboardCommand, sqlSt dashboardStore, err := database.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotaService) require.NoError(t, err) folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore) - service := ProvideDashboardService( + service := ProvideDashboardServiceImpl( cfg, dashboardStore, folderStore, &dummyDashAlertExtractor{}, featuremgmt.WithFeatures(), accesscontrolmock.NewMockedPermissionsService(), @@ -912,7 +912,7 @@ func callSaveWithError(t *testing.T, cmd dashboards.SaveDashboardCommand, sqlSto dashboardStore, err := database.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotaService) require.NoError(t, err) folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore) - service := ProvideDashboardService( + service := ProvideDashboardServiceImpl( cfg, dashboardStore, folderStore, &dummyDashAlertExtractor{}, featuremgmt.WithFeatures(), accesscontrolmock.NewMockedPermissionsService(), @@ -953,7 +953,7 @@ func saveTestDashboard(t *testing.T, title string, orgID, folderID int64, sqlSto dashboardStore, err := database.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotaService) require.NoError(t, err) folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore) - service := ProvideDashboardService( + service := ProvideDashboardServiceImpl( cfg, dashboardStore, folderStore, &dummyDashAlertExtractor{}, featuremgmt.WithFeatures(), accesscontrolmock.NewMockedPermissionsService(), @@ -995,7 +995,7 @@ func saveTestFolder(t *testing.T, title string, orgID int64, sqlStore db.DB) *da dashboardStore, err := database.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotaService) require.NoError(t, err) folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore) - service := ProvideDashboardService( + service := ProvideDashboardServiceImpl( cfg, dashboardStore, folderStore, &dummyDashAlertExtractor{}, featuremgmt.WithFeatures(), accesscontrolmock.NewMockedPermissionsService(), diff --git a/pkg/services/dashboards/service/service.go b/pkg/services/dashboards/service/service.go index d08fe73b0fd..f526404dc0e 100644 --- a/pkg/services/dashboards/service/service.go +++ b/pkg/services/dashboards/service/service.go @@ -5,11 +5,11 @@ import ( "github.com/grafana/grafana/pkg/services/featuremgmt" ) -func ProvideSimpleDashboardService( +func ProvideDashboardService( features featuremgmt.FeatureToggles, - svc *DashboardServiceImpl, + orig *DashboardServiceImpl, ) dashboards.DashboardService { - return svc + return orig } func ProvideDashboardProvisioningService( diff --git a/pkg/services/libraryelements/libraryelements_test.go b/pkg/services/libraryelements/libraryelements_test.go index 516484efe7f..366e5ec98e3 100644 --- a/pkg/services/libraryelements/libraryelements_test.go +++ b/pkg/services/libraryelements/libraryelements_test.go @@ -295,7 +295,7 @@ func createDashboard(t *testing.T, sqlStore db.DB, user user.SignedInUser, dash folderPermissions := acmock.NewMockedPermissionsService() dashboardPermissions := acmock.NewMockedPermissionsService() folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore) - service := dashboardservice.ProvideDashboardService( + service := dashboardservice.ProvideDashboardServiceImpl( cfg, dashboardStore, folderStore, dashAlertExtractor, features, folderPermissions, dashboardPermissions, ac, foldertest.NewFakeService(), @@ -441,7 +441,7 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo folderPermissions := acmock.NewMockedPermissionsService() dashboardPermissions := acmock.NewMockedPermissionsService() folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore) - dashboardService := dashboardservice.ProvideDashboardService( + dashboardService := dashboardservice.ProvideDashboardServiceImpl( sqlStore.Cfg, dashboardStore, folderStore, nil, features, folderPermissions, dashboardPermissions, ac, foldertest.NewFakeService(), diff --git a/pkg/services/librarypanels/librarypanels_test.go b/pkg/services/librarypanels/librarypanels_test.go index 57451f99261..8570e20fef1 100644 --- a/pkg/services/librarypanels/librarypanels_test.go +++ b/pkg/services/librarypanels/librarypanels_test.go @@ -707,7 +707,7 @@ func createDashboard(t *testing.T, sqlStore db.DB, user *user.SignedInUser, dash dashAlertService := alerting.ProvideDashAlertExtractorService(nil, nil, nil) ac := acmock.New() folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore) - service := dashboardservice.ProvideDashboardService( + service := dashboardservice.ProvideDashboardServiceImpl( cfg, dashboardStore, folderStore, dashAlertService, featuremgmt.WithFeatures(), acmock.NewMockedPermissionsService(), acmock.NewMockedPermissionsService(), ac, foldertest.NewFakeService(), diff --git a/pkg/services/ngalert/tests/util.go b/pkg/services/ngalert/tests/util.go index 7c8728a3485..ce058c37a46 100644 --- a/pkg/services/ngalert/tests/util.go +++ b/pkg/services/ngalert/tests/util.go @@ -81,7 +81,7 @@ func SetupTestEnv(tb testing.TB, baseInterval time.Duration) (*ngalert.AlertNG, folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore) - dashboardService := dashboardservice.ProvideDashboardService( + dashboardService := dashboardservice.ProvideDashboardServiceImpl( cfg, dashboardStore, folderStore, nil, features, folderPermissions, dashboardPermissions, ac, foldertest.NewFakeService(), From 154fa2dd000dc9a6088a54639fe8bf52f0455742 Mon Sep 17 00:00:00 2001 From: Melori Arellano Date: Wed, 8 Mar 2023 13:15:11 -0700 Subject: [PATCH 089/288] Docs: Update canvas panel data links section with additional steps (#64456) * Update index.md Add additional steps to the Data Links section needed to associate a field value with a data link. A dashboard data link (previously referenced) displays for all elements. * Fix linting issues --------- Co-authored-by: nmarrs --- .../panels-visualizations/visualizations/canvas/index.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/sources/panels-visualizations/visualizations/canvas/index.md b/docs/sources/panels-visualizations/visualizations/canvas/index.md index 6d9b5b2d28b..21db09f8b19 100644 --- a/docs/sources/panels-visualizations/visualizations/canvas/index.md +++ b/docs/sources/panels-visualizations/visualizations/canvas/index.md @@ -84,11 +84,17 @@ The inline editing toggle enables you to lock or unlock the canvas panel. When t ### Data links -Canvas supports [data links](https://grafana.com/docs/grafana/latest/panels-visualizations/configure-data-links/). Once you've added a data link to the panel, you can display it by following these steps: +Canvas supports [data links](https://grafana.com/docs/grafana/latest/panels-visualizations/configure-data-links/). You can create a data link for a metric-value element and display it by following these steps: 1. Set an element to be tied to a field value. 1. Turn off the inline editing toggle. +1. Create an override for **Fields with name** and select the element field name from the list. +1. Click the **+ Add override property** button. +1. Select `Datalinks > Datalinks` from the list. +1. Click **+Add link** add a title and URL for the data link. 1. Hover over the element to display the data link tooltip. 1. Click on the element to be able to open the data link. +If multiple elements use the same field, you can create a unique field name using the [add field from calculation transform](https://grafana.com/docs/grafana/latest/panels-visualizations/query-transform-data/transform-data/#add-field-from-calculation). The alias you create in the transformation will appear as a field you can use with an element. + {{< video-embed src="/media/docs/grafana/canvas-data-links-9-4-0.mp4" max-width="750px" caption="Data links demo" >}} From 1a5a280c86616038284e27deddece51c8240f04a Mon Sep 17 00:00:00 2001 From: owensmallwood Date: Wed, 8 Mar 2023 14:54:35 -0600 Subject: [PATCH 090/288] Pubdash: Email sharing handle dashboard deleted (#64247) dashboard service calls pubdash service when dashboard deleted --- pkg/api/dashboard.go | 6 ++ pkg/api/dashboard_test.go | 38 ++++----- pkg/services/dashboards/database/database.go | 2 - .../dashboards/database/database_test.go | 78 ------------------ pkg/services/publicdashboards/api/api.go | 2 +- .../publicdashboards/database/database.go | 22 +++++- .../database/database_test.go | 43 +++++++++- .../public_dashboard_service_mock.go | 26 ++++-- .../public_dashboard_service_wrapper_mock.go | 16 +++- .../public_dashboard_store_mock.go | 39 +++++++-- .../publicdashboards/publicdashboard.go | 7 +- .../publicdashboards/service/service.go | 36 +++++++-- .../publicdashboards/service/service_test.go | 79 ++++++++++++++----- .../service/service_wapper.go | 9 +++ 14 files changed, 251 insertions(+), 152 deletions(-) diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index bd4f5d79a46..09a9344b122 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -326,6 +326,12 @@ func (hs *HTTPServer) deleteDashboard(c *contextmodel.ReqContext) response.Respo hs.log.Error("Failed to disconnect library elements", "dashboard", dash.ID, "user", c.SignedInUser.UserID, "error", err) } + // deletes all related public dashboard entities + err = hs.PublicDashboardsApi.PublicDashboardService.DeleteByDashboard(c.Req.Context(), dash) + if err != nil { + hs.log.Error("Failed to delete public dashboard") + } + err = hs.DashboardService.DeleteDashboard(c.Req.Context(), dash.ID, c.OrgID) if err != nil { var dashboardErr dashboards.DashboardErr diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index f43c59da28e..a53b25cbce5 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -9,6 +9,8 @@ import ( "os" "testing" + "github.com/grafana/grafana/pkg/services/publicdashboards" + "github.com/grafana/grafana/pkg/services/publicdashboards/api" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -304,7 +306,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { setUp() sc.sqlStore = mockSQLStore - hs.callDeleteDashboardByUID(t, sc, dashboardService) + hs.callDeleteDashboardByUID(t, sc, dashboardService, nil) assert.Equal(t, 403, sc.resp.Code) }, mockSQLStore) @@ -342,7 +344,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { setUp() - hs.callDeleteDashboardByUID(t, sc, dashboardService) + hs.callDeleteDashboardByUID(t, sc, dashboardService, nil) assert.Equal(t, 403, sc.resp.Code) }, mockSQLStore) @@ -400,23 +402,9 @@ func TestDashboardAPIEndpoint(t *testing.T) { dashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Return(qResult, nil) dashboardService.On("DeleteDashboard", mock.Anything, mock.AnythingOfType("int64"), mock.AnythingOfType("int64")).Return(nil) - hs.callDeleteDashboardByUID(t, sc, dashboardService) - - assert.Equal(t, 200, sc.resp.Code) - }, mockSQLStore) - - loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) { - setUpInner() - sc.sqlStore = mockSQLStore - sc.dashboardVersionService = fakeDashboardVersionService - hs.callGetDashboardVersion(sc) - - assert.Equal(t, 200, sc.resp.Code) - }, mockSQLStore) - - loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/id/2/versions", "/api/dashboards/id/:dashboardId/versions", role, func(sc *scenarioContext) { - setUpInner() - hs.callGetDashboardVersions(sc) + pubdashService := publicdashboards.NewFakePublicDashboardService(t) + pubdashService.On("DeleteByDashboard", mock.Anything, mock.Anything).Return(nil) + hs.callDeleteDashboardByUID(t, sc, dashboardService, pubdashService) assert.Equal(t, 200, sc.resp.Code) }, mockSQLStore) @@ -455,7 +443,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { setUpInner() - hs.callDeleteDashboardByUID(t, sc, dashboardService) + hs.callDeleteDashboardByUID(t, sc, dashboardService, nil) assert.Equal(t, 403, sc.resp.Code) }, mockSQLStore) }) @@ -495,7 +483,9 @@ func TestDashboardAPIEndpoint(t *testing.T) { qResult := dashboards.NewDashboard("test") dashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Return(qResult, nil) dashboardService.On("DeleteDashboard", mock.Anything, mock.AnythingOfType("int64"), mock.AnythingOfType("int64")).Return(nil) - hs.callDeleteDashboardByUID(t, sc, dashboardService) + pubdashService := publicdashboards.NewFakePublicDashboardService(t) + pubdashService.On("DeleteByDashboard", mock.Anything, mock.Anything).Return(nil) + hs.callDeleteDashboardByUID(t, sc, dashboardService, pubdashService) assert.Equal(t, 200, sc.resp.Code) }, mockSQLStore) @@ -538,7 +528,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { setUpInner() - hs.callDeleteDashboardByUID(t, sc, dashboardService) + hs.callDeleteDashboardByUID(t, sc, dashboardService, nil) assert.Equal(t, 403, sc.resp.Code) }, mockSQLStore) @@ -1035,8 +1025,10 @@ func (hs *HTTPServer) callGetDashboardVersions(sc *scenarioContext) { } func (hs *HTTPServer) callDeleteDashboardByUID(t *testing.T, - sc *scenarioContext, mockDashboard *dashboards.FakeDashboardService) { + sc *scenarioContext, mockDashboard *dashboards.FakeDashboardService, mockPubdashService *publicdashboards.FakePublicDashboardService) { hs.DashboardService = mockDashboard + pubdashApi := api.ProvideApi(mockPubdashService, nil, nil, featuremgmt.WithFeatures()) + hs.PublicDashboardsApi = pubdashApi sc.handlerFunc = hs.DeleteDashboardByUID sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() } diff --git a/pkg/services/dashboards/database/database.go b/pkg/services/dashboards/database/database.go index 657ebc34e45..4126fe1e8b0 100644 --- a/pkg/services/dashboards/database/database.go +++ b/pkg/services/dashboards/database/database.go @@ -702,7 +702,6 @@ func (d *dashboardStore) deleteDashboard(cmd *dashboards.DeleteDashboardCommand, deletes := []string{ "DELETE FROM dashboard_tag WHERE dashboard_id = ? ", "DELETE FROM star WHERE dashboard_id = ? ", - "DELETE FROM dashboard_public WHERE dashboard_uid = (SELECT uid FROM dashboard WHERE id = ?)", "DELETE FROM dashboard WHERE id = ?", "DELETE FROM playlist_item WHERE type = 'dashboard_by_id' AND value = ?", "DELETE FROM dashboard_version WHERE dashboard_id = ?", @@ -751,7 +750,6 @@ func (d *dashboardStore) deleteDashboard(cmd *dashboards.DeleteDashboardCommand, "DELETE FROM annotation WHERE dashboard_id IN (SELECT id FROM dashboard WHERE org_id = ? AND folder_id = ?)", "DELETE FROM dashboard_provisioning WHERE dashboard_id IN (SELECT id FROM dashboard WHERE org_id = ? AND folder_id = ?)", "DELETE FROM dashboard_acl WHERE dashboard_id IN (SELECT id FROM dashboard WHERE org_id = ? AND folder_id = ?)", - "DELETE FROM dashboard_public WHERE dashboard_uid IN (SELECT uid FROM dashboard WHERE org_id = ? AND folder_id = ?)", } for _, sql := range childrenDeletes { _, err := sess.Exec(sql, dashboard.OrgID, dashboard.ID) diff --git a/pkg/services/dashboards/database/database_test.go b/pkg/services/dashboards/database/database_test.go index 05ee1636359..3f36a7c8f39 100644 --- a/pkg/services/dashboards/database/database_test.go +++ b/pkg/services/dashboards/database/database_test.go @@ -16,8 +16,6 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/org" - "github.com/grafana/grafana/pkg/services/publicdashboards/database" - publicDashboardModels "github.com/grafana/grafana/pkg/services/publicdashboards/models" "github.com/grafana/grafana/pkg/services/quota/quotatest" "github.com/grafana/grafana/pkg/services/search/model" "github.com/grafana/grafana/pkg/services/sqlstore" @@ -27,7 +25,6 @@ import ( "github.com/grafana/grafana/pkg/services/tag/tagimpl" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" - "github.com/grafana/grafana/pkg/util" ) func TestIntegrationDashboardDataAccess(t *testing.T) { @@ -39,7 +36,6 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { var savedFolder, savedDash, savedDash2 *dashboards.Dashboard var dashboardStore dashboards.Store var starService star.Service - var publicDashboardStore *database.PublicDashboardStoreImpl setup := func() { sqlStore, cfg = db.InitTestDBwithCfg(t) @@ -53,8 +49,6 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { insertTestDashboard(t, dashboardStore, "test dash 45", 1, savedFolder.ID, false, "prod") savedDash2 = insertTestDashboard(t, dashboardStore, "test dash 67", 1, 0, false, "prod") insertTestRule(t, sqlStore, savedFolder.OrgID, savedFolder.UID) - - publicDashboardStore = database.ProvideStore(sqlStore) } t.Run("Should return dashboard model", func(t *testing.T) { @@ -246,78 +240,6 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { require.True(t, errors.Is(err, dashboards.ErrFolderContainsAlertRules)) }) - t.Run("Should be able to delete dashboard and related public dashboard", func(t *testing.T) { - setup() - - uid := util.GenerateShortUID() - cmd := publicDashboardModels.SavePublicDashboardCommand{ - PublicDashboard: publicDashboardModels.PublicDashboard{ - Uid: uid, - DashboardUid: savedDash.UID, - OrgId: savedDash.OrgID, - IsEnabled: true, - TimeSettings: &publicDashboardModels.TimeSettings{}, - CreatedBy: 1, - CreatedAt: time.Now(), - AccessToken: "an-access-token", - }, - } - _, err := publicDashboardStore.Create(context.Background(), cmd) - require.NoError(t, err) - pubdashConfig, _ := publicDashboardStore.FindByAccessToken(context.Background(), "an-access-token") - require.NotNil(t, pubdashConfig) - - deleteCmd := &dashboards.DeleteDashboardCommand{ID: savedDash.ID, OrgID: savedDash.OrgID} - err = dashboardStore.DeleteDashboard(context.Background(), deleteCmd) - require.NoError(t, err) - - query := dashboards.GetDashboardQuery{UID: savedDash.UID, OrgID: savedDash.OrgID} - dash, getErr := dashboardStore.GetDashboard(context.Background(), &query) - require.Equal(t, getErr, dashboards.ErrDashboardNotFound) - assert.Nil(t, dash) - - pubdashConfig, err = publicDashboardStore.FindByAccessToken(context.Background(), "an-access-token") - require.Nil(t, err) - require.Nil(t, pubdashConfig) - }) - - t.Run("Should be able to delete a dashboard folder, with its dashboard and related public dashboard", func(t *testing.T) { - setup() - - uid := util.GenerateShortUID() - cmd := publicDashboardModels.SavePublicDashboardCommand{ - PublicDashboard: publicDashboardModels.PublicDashboard{ - Uid: uid, - DashboardUid: savedDash.UID, - OrgId: savedDash.OrgID, - IsEnabled: true, - TimeSettings: &publicDashboardModels.TimeSettings{}, - CreatedBy: 1, - CreatedAt: time.Now(), - AccessToken: "an-access-token", - }, - } - _, err := publicDashboardStore.Create(context.Background(), cmd) - require.NoError(t, err) - pubdashConfig, _ := publicDashboardStore.FindByAccessToken(context.Background(), "an-access-token") - require.NotNil(t, pubdashConfig) - - deleteCmd := &dashboards.DeleteDashboardCommand{ID: savedFolder.ID, ForceDeleteFolderRules: true} - err = dashboardStore.DeleteDashboard(context.Background(), deleteCmd) - require.NoError(t, err) - - query := dashboards.GetDashboardsQuery{ - DashboardIDs: []int64{savedFolder.ID, savedDash.ID}, - } - queryResult, err := dashboardStore.GetDashboards(context.Background(), &query) - require.NoError(t, err) - require.Equal(t, len(queryResult), 0) - - pubdashConfig, err = publicDashboardStore.FindByAccessToken(context.Background(), "an-access-token") - require.Nil(t, err) - require.Nil(t, pubdashConfig) - }) - t.Run("Should be able to delete a dashboard folder and its children if force delete rules is enabled", func(t *testing.T) { setup() deleteCmd := &dashboards.DeleteDashboardCommand{ID: savedFolder.ID, ForceDeleteFolderRules: true} diff --git a/pkg/services/publicdashboards/api/api.go b/pkg/services/publicdashboards/api/api.go index 8ac11743ed6..921876e1949 100644 --- a/pkg/services/publicdashboards/api/api.go +++ b/pkg/services/publicdashboards/api/api.go @@ -196,7 +196,7 @@ func (api *Api) DeletePublicDashboard(c *contextmodel.ReqContext) response.Respo return response.Err(ErrInvalidUid.Errorf("UpdatePublicDashboard: invalid Uid %s", uid)) } - err := api.PublicDashboardService.Delete(c.Req.Context(), c.OrgID, uid) + err := api.PublicDashboardService.Delete(c.Req.Context(), uid) if err != nil { return response.Err(err) } diff --git a/pkg/services/publicdashboards/database/database.go b/pkg/services/publicdashboards/database/database.go index 180612ca12c..669f6c1d679 100644 --- a/pkg/services/publicdashboards/database/database.go +++ b/pkg/services/publicdashboards/database/database.go @@ -9,6 +9,7 @@ import ( "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/publicdashboards" . "github.com/grafana/grafana/pkg/services/publicdashboards/models" + "github.com/grafana/grafana/pkg/services/sqlstore" ) // Define the storage implementation. We're generating the mock implementation @@ -255,8 +256,8 @@ func (d *PublicDashboardStoreImpl) Update(ctx context.Context, cmd SavePublicDas } // Deletes a public dashboard -func (d *PublicDashboardStoreImpl) Delete(ctx context.Context, orgId int64, uid string) (int64, error) { - dashboard := &PublicDashboard{OrgId: orgId, Uid: uid} +func (d *PublicDashboardStoreImpl) Delete(ctx context.Context, uid string) (int64, error) { + dashboard := &PublicDashboard{Uid: uid} var affectedRows int64 err := d.sqlStore.WithDbSession(ctx, func(sess *db.Session) error { var err error @@ -267,3 +268,20 @@ func (d *PublicDashboardStoreImpl) Delete(ctx context.Context, orgId int64, uid return affectedRows, err } + +func (d *PublicDashboardStoreImpl) FindByDashboardFolder(ctx context.Context, dashboard *dashboards.Dashboard) ([]*PublicDashboard, error) { + if dashboard == nil || !dashboard.IsFolder { + return nil, nil + } + + var pubdashes []*PublicDashboard + + err := d.sqlStore.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { + return sess.SQL("SELECT * from dashboard_public WHERE (dashboard_uid, org_id) IN (SELECT uid, org_id FROM dashboard WHERE folder_id = ?)", dashboard.ID).Find(&pubdashes) + }) + if err != nil { + return nil, err + } + + return pubdashes, nil +} diff --git a/pkg/services/publicdashboards/database/database_test.go b/pkg/services/publicdashboards/database/database_test.go index 4dbb7eaff9c..e67da63d104 100644 --- a/pkg/services/publicdashboards/database/database_test.go +++ b/pkg/services/publicdashboards/database/database_test.go @@ -652,7 +652,7 @@ func TestIntegrationDelete(t *testing.T) { t.Run("Delete success", func(t *testing.T) { setup() // Do the deletion - affectedRows, err := publicdashboardStore.Delete(context.Background(), savedPublicDashboard.OrgId, savedPublicDashboard.Uid) + affectedRows, err := publicdashboardStore.Delete(context.Background(), savedPublicDashboard.Uid) require.NoError(t, err) assert.EqualValues(t, affectedRows, 1) @@ -665,12 +665,51 @@ func TestIntegrationDelete(t *testing.T) { t.Run("Non-existent public dashboard deletion doesn't throw an error", func(t *testing.T) { setup() - affectedRows, err := publicdashboardStore.Delete(context.Background(), 15, "non-existent-uid") + affectedRows, err := publicdashboardStore.Delete(context.Background(), "non-existent-uid") require.NoError(t, err) assert.EqualValues(t, affectedRows, 0) }) } +func TestGetDashboardByFolder(t *testing.T) { + t.Run("returns nil when dashboard is not a folder", func(t *testing.T) { + sqlStore, _ := db.InitTestDBwithCfg(t) + dashboard := &dashboards.Dashboard{IsFolder: false} + store := ProvideStore(sqlStore) + pubdashes, err := store.FindByDashboardFolder(context.Background(), dashboard) + + require.NoError(t, err) + assert.Nil(t, pubdashes) + }) + + t.Run("returns nil when dashboard is nil", func(t *testing.T) { + sqlStore, _ := db.InitTestDBwithCfg(t) + store := ProvideStore(sqlStore) + pubdashes, err := store.FindByDashboardFolder(context.Background(), nil) + + require.NoError(t, err) + assert.Nil(t, pubdashes) + }) + + t.Run("can get all pubdashes for dashboard folder and org", func(t *testing.T) { + sqlStore, cfg := db.InitTestDBwithCfg(t) + quotaService := quotatest.New(false, nil) + dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotaService) + require.NoError(t, err) + pubdashStore := ProvideStore(sqlStore) + dashboard := insertTestDashboard(t, dashboardStore, "title", 1, 1, true) + pubdash := insertPublicDashboard(t, pubdashStore, dashboard.UID, dashboard.OrgID, true) + dashboard2 := insertTestDashboard(t, dashboardStore, "title", 1, 2, true) + _ = insertPublicDashboard(t, pubdashStore, dashboard2.UID, dashboard2.OrgID, true) + + pubdashes, err := pubdashStore.FindByDashboardFolder(context.Background(), dashboard) + + require.NoError(t, err) + assert.Len(t, pubdashes, 1) + assert.Equal(t, pubdash, pubdashes[0]) + }) +} + // helper function to insert a dashboard func insertTestDashboard(t *testing.T, dashboardStore dashboards.Store, title string, orgId int64, folderId int64, isFolder bool, tags ...interface{}) *dashboards.Dashboard { diff --git a/pkg/services/publicdashboards/public_dashboard_service_mock.go b/pkg/services/publicdashboards/public_dashboard_service_mock.go index 3fcf2bf2f87..a0bd21ac7e5 100644 --- a/pkg/services/publicdashboards/public_dashboard_service_mock.go +++ b/pkg/services/publicdashboards/public_dashboard_service_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.14.0. DO NOT EDIT. +// Code generated by mockery v2.16.0. DO NOT EDIT. package publicdashboards @@ -46,13 +46,27 @@ func (_m *FakePublicDashboardService) Create(ctx context.Context, u *user.Signed return r0, r1 } -// Delete provides a mock function with given fields: ctx, orgId, uid -func (_m *FakePublicDashboardService) Delete(ctx context.Context, orgId int64, uid string) error { - ret := _m.Called(ctx, orgId, uid) +// Delete provides a mock function with given fields: ctx, uid +func (_m *FakePublicDashboardService) Delete(ctx context.Context, uid string) error { + ret := _m.Called(ctx, uid) var r0 error - if rf, ok := ret.Get(0).(func(context.Context, int64, string) error); ok { - r0 = rf(ctx, orgId, uid) + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, uid) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// DeleteByDashboard provides a mock function with given fields: ctx, dashboard +func (_m *FakePublicDashboardService) DeleteByDashboard(ctx context.Context, dashboard *dashboards.Dashboard) error { + ret := _m.Called(ctx, dashboard) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *dashboards.Dashboard) error); ok { + r0 = rf(ctx, dashboard) } else { r0 = ret.Error(0) } diff --git a/pkg/services/publicdashboards/public_dashboard_service_wrapper_mock.go b/pkg/services/publicdashboards/public_dashboard_service_wrapper_mock.go index fa33d059e6a..5b96a227f48 100644 --- a/pkg/services/publicdashboards/public_dashboard_service_wrapper_mock.go +++ b/pkg/services/publicdashboards/public_dashboard_service_wrapper_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.14.0. DO NOT EDIT. +// Code generated by mockery v2.16.0. DO NOT EDIT. package publicdashboards @@ -14,6 +14,20 @@ type FakePublicDashboardServiceWrapper struct { mock.Mock } +// Delete provides a mock function with given fields: ctx, uid +func (_m *FakePublicDashboardServiceWrapper) Delete(ctx context.Context, uid string) error { + ret := _m.Called(ctx, uid) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, uid) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // FindByDashboardUid provides a mock function with given fields: ctx, orgId, dashboardUid func (_m *FakePublicDashboardServiceWrapper) FindByDashboardUid(ctx context.Context, orgId int64, dashboardUid string) (*models.PublicDashboard, error) { ret := _m.Called(ctx, orgId, dashboardUid) diff --git a/pkg/services/publicdashboards/public_dashboard_store_mock.go b/pkg/services/publicdashboards/public_dashboard_store_mock.go index 7e0bcdf3ae5..07e5f97cfdc 100644 --- a/pkg/services/publicdashboards/public_dashboard_store_mock.go +++ b/pkg/services/publicdashboards/public_dashboard_store_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.14.0. DO NOT EDIT. +// Code generated by mockery v2.16.0. DO NOT EDIT. package publicdashboards @@ -37,20 +37,20 @@ func (_m *FakePublicDashboardStore) Create(ctx context.Context, cmd models.SaveP return r0, r1 } -// Delete provides a mock function with given fields: ctx, orgId, uid -func (_m *FakePublicDashboardStore) Delete(ctx context.Context, orgId int64, uid string) (int64, error) { - ret := _m.Called(ctx, orgId, uid) +// Delete provides a mock function with given fields: ctx, uid +func (_m *FakePublicDashboardStore) Delete(ctx context.Context, uid string) (int64, error) { + ret := _m.Called(ctx, uid) var r0 int64 - if rf, ok := ret.Get(0).(func(context.Context, int64, string) int64); ok { - r0 = rf(ctx, orgId, uid) + if rf, ok := ret.Get(0).(func(context.Context, string) int64); ok { + r0 = rf(ctx, uid) } else { r0 = ret.Get(0).(int64) } var r1 error - if rf, ok := ret.Get(1).(func(context.Context, int64, string) error); ok { - r1 = rf(ctx, orgId, uid) + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, uid) } else { r1 = ret.Error(1) } @@ -169,6 +169,29 @@ func (_m *FakePublicDashboardStore) FindByAccessToken(ctx context.Context, acces return r0, r1 } +// FindByDashboardFolder provides a mock function with given fields: ctx, dashboard +func (_m *FakePublicDashboardStore) FindByDashboardFolder(ctx context.Context, dashboard *dashboards.Dashboard) ([]*models.PublicDashboard, error) { + ret := _m.Called(ctx, dashboard) + + var r0 []*models.PublicDashboard + if rf, ok := ret.Get(0).(func(context.Context, *dashboards.Dashboard) []*models.PublicDashboard); ok { + r0 = rf(ctx, dashboard) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*models.PublicDashboard) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *dashboards.Dashboard) error); ok { + r1 = rf(ctx, dashboard) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // FindByDashboardUid provides a mock function with given fields: ctx, orgId, dashboardUid func (_m *FakePublicDashboardStore) FindByDashboardUid(ctx context.Context, orgId int64, dashboardUid string) (*models.PublicDashboard, error) { ret := _m.Called(ctx, orgId, dashboardUid) diff --git a/pkg/services/publicdashboards/publicdashboard.go b/pkg/services/publicdashboards/publicdashboard.go index 404dc5501cf..93bb2e96b61 100644 --- a/pkg/services/publicdashboards/publicdashboard.go +++ b/pkg/services/publicdashboards/publicdashboard.go @@ -24,7 +24,8 @@ type Service interface { Find(ctx context.Context, uid string) (*PublicDashboard, error) Create(ctx context.Context, u *user.SignedInUser, dto *SavePublicDashboardDTO) (*PublicDashboard, error) Update(ctx context.Context, u *user.SignedInUser, dto *SavePublicDashboardDTO) (*PublicDashboard, error) - Delete(ctx context.Context, orgId int64, uid string) error + Delete(ctx context.Context, uid string) error + DeleteByDashboard(ctx context.Context, dashboard *dashboards.Dashboard) error GetMetricRequest(ctx context.Context, dashboard *dashboards.Dashboard, publicDashboard *PublicDashboard, panelId int64, reqDTO PublicDashboardQueryDTO) (dtos.MetricRequest, error) GetQueryDataResponse(ctx context.Context, skipCache bool, reqDTO PublicDashboardQueryDTO, panelId int64, accessToken string) (*backend.QueryDataResponse, error) @@ -41,6 +42,7 @@ type Service interface { //go:generate mockery --name ServiceWrapper --structname FakePublicDashboardServiceWrapper --inpackage --filename public_dashboard_service_wrapper_mock.go type ServiceWrapper interface { FindByDashboardUid(ctx context.Context, orgId int64, dashboardUid string) (*PublicDashboard, error) + Delete(ctx context.Context, uid string) error } //go:generate mockery --name Store --structname FakePublicDashboardStore --inpackage --filename public_dashboard_store_mock.go @@ -52,9 +54,10 @@ type Store interface { FindAll(ctx context.Context, orgId int64) ([]PublicDashboardListResponse, error) Create(ctx context.Context, cmd SavePublicDashboardCommand) (int64, error) Update(ctx context.Context, cmd SavePublicDashboardCommand) (int64, error) - Delete(ctx context.Context, orgId int64, uid string) (int64, error) + Delete(ctx context.Context, uid string) (int64, error) GetOrgIdByAccessToken(ctx context.Context, accessToken string) (int64, error) + FindByDashboardFolder(ctx context.Context, dashboard *dashboards.Dashboard) ([]*PublicDashboard, error) ExistsEnabledByAccessToken(ctx context.Context, accessToken string) (bool, error) ExistsEnabledByDashboardUid(ctx context.Context, dashboardUid string) (bool, error) } diff --git a/pkg/services/publicdashboards/service/service.go b/pkg/services/publicdashboards/service/service.go index a1c0a45b7a8..2c292f9e9f8 100644 --- a/pkg/services/publicdashboards/service/service.go +++ b/pkg/services/publicdashboards/service/service.go @@ -329,17 +329,37 @@ func (pd *PublicDashboardServiceImpl) GetOrgIdByAccessToken(ctx context.Context, return pd.store.GetOrgIdByAccessToken(ctx, accessToken) } -func (pd *PublicDashboardServiceImpl) Delete(ctx context.Context, orgId int64, uid string) error { - affectedRows, err := pd.store.Delete(ctx, orgId, uid) +func (pd *PublicDashboardServiceImpl) Delete(ctx context.Context, uid string) error { + return pd.serviceWrapper.Delete(ctx, uid) +} + +func (pd *PublicDashboardServiceImpl) DeleteByDashboard(ctx context.Context, dashboard *dashboards.Dashboard) error { + if dashboard.IsFolder { + // get all pubdashes for the folder + pubdashes, err := pd.store.FindByDashboardFolder(ctx, dashboard) + if err != nil { + return err + } + // delete each pubdash + for _, pubdash := range pubdashes { + err = pd.serviceWrapper.Delete(ctx, pubdash.Uid) + if err != nil { + return err + } + } + + return nil + } + + pubdash, err := pd.store.FindByDashboardUid(ctx, dashboard.OrgID, dashboard.UID) if err != nil { - return ErrInternalServerError.Errorf("Delete: failed to delete a public dashboard by orgId: %d and Uid: %s %w", orgId, uid, err) + return ErrInternalServerError.Errorf("DeleteByDashboard: error finding a public dashboard by dashboard orgId: %d and Uid: %s %w", dashboard.OrgID, dashboard.UID, err) + } + if pubdash == nil { + return nil } - if affectedRows == 0 { - return ErrPublicDashboardNotFound.Errorf("Delete: Public dashboard not found by orgId: %d and Uid: %s", orgId, uid) - } - - return nil + return pd.serviceWrapper.Delete(ctx, pubdash.Uid) } // intervalMS and maxQueryData values are being calculated on the frontend for regular dashboards diff --git a/pkg/services/publicdashboards/service/service_test.go b/pkg/services/publicdashboards/service/service_test.go index 560d8690927..6ae0ef9568f 100644 --- a/pkg/services/publicdashboards/service/service_test.go +++ b/pkg/services/publicdashboards/service/service_test.go @@ -498,13 +498,13 @@ func TestDeletePublicDashboard(t *testing.T) { { Name: "Public dashboard not found", AffectedRowsResp: 0, - ExpectedErrResp: ErrPublicDashboardNotFound.Errorf("Delete: Public dashboard not found by orgId: 13 and Uid: uid"), + ExpectedErrResp: nil, StoreRespErr: nil, }, { Name: "Database error", AffectedRowsResp: 0, - ExpectedErrResp: ErrInternalServerError.Errorf("Delete: failed to delete a public dashboard by orgId: 13 and Uid: uid db error!"), + ExpectedErrResp: ErrInternalServerError.Errorf("Delete: failed to delete a public dashboard by Uid: uid db error!"), StoreRespErr: errors.New("db error!"), }, } @@ -512,14 +512,18 @@ func TestDeletePublicDashboard(t *testing.T) { for _, tt := range testCases { t.Run(tt.Name, func(t *testing.T) { store := NewFakePublicDashboardStore(t) - store.On("Delete", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(tt.AffectedRowsResp, tt.StoreRespErr) - - service := &PublicDashboardServiceImpl{ + store.On("Delete", mock.Anything, mock.Anything).Return(tt.AffectedRowsResp, tt.StoreRespErr) + serviceWrapper := &PublicDashboardServiceWrapperImpl{ log: log.New("test.logger"), store: store, } + service := &PublicDashboardServiceImpl{ + log: log.New("test.logger"), + store: store, + serviceWrapper: serviceWrapper, + } - err := service.Delete(context.Background(), 13, "uid") + err := service.Delete(context.Background(), "uid") if tt.ExpectedErrResp != nil { assert.Equal(t, tt.ExpectedErrResp.Error(), err.Error()) assert.Equal(t, tt.ExpectedErrResp.Error(), err.Error()) @@ -966,6 +970,56 @@ func TestPublicDashboardServiceImpl_NewPublicDashboardAccessToken(t *testing.T) } } +func TestDeleteByDashboard(t *testing.T) { + t.Run("will return nil when pubdash not found", func(t *testing.T) { + store := NewFakePublicDashboardStore(t) + pd := &PublicDashboardServiceImpl{store: store, serviceWrapper: ProvideServiceWrapper(store)} + dashboard := &dashboards.Dashboard{UID: "1", OrgID: 1, IsFolder: false} + store.On("FindByDashboardUid", mock.Anything, mock.Anything, mock.Anything).Return(nil, nil) + + err := pd.DeleteByDashboard(context.Background(), dashboard) + assert.Nil(t, err) + }) + t.Run("will delete pubdash when dashboard deleted", func(t *testing.T) { + store := NewFakePublicDashboardStore(t) + pd := &PublicDashboardServiceImpl{store: store, serviceWrapper: ProvideServiceWrapper(store)} + dashboard := &dashboards.Dashboard{UID: "1", OrgID: 1, IsFolder: false} + pubdash := &PublicDashboard{Uid: "2", OrgId: 1, DashboardUid: dashboard.UID} + store.On("FindByDashboardUid", mock.Anything, mock.Anything, mock.Anything).Return(pubdash, nil) + store.On("Delete", mock.Anything, mock.Anything, mock.Anything).Return(int64(1), nil) + + err := pd.DeleteByDashboard(context.Background(), dashboard) + require.NoError(t, err) + }) + + t.Run("will delete pubdashes when dashboard folder deleted", func(t *testing.T) { + store := NewFakePublicDashboardStore(t) + pd := &PublicDashboardServiceImpl{store: store, serviceWrapper: ProvideServiceWrapper(store)} + dashboard := &dashboards.Dashboard{UID: "1", OrgID: 1, IsFolder: true} + pubdash1 := &PublicDashboard{Uid: "2", OrgId: 1, DashboardUid: dashboard.UID} + pubdash2 := &PublicDashboard{Uid: "3", OrgId: 1, DashboardUid: dashboard.UID} + store.On("FindByDashboardFolder", mock.Anything, mock.Anything).Return([]*PublicDashboard{pubdash1, pubdash2}, nil) + store.On("Delete", mock.Anything, mock.Anything, mock.Anything).Return(int64(1), nil) + store.On("Delete", mock.Anything, mock.Anything, mock.Anything).Return(int64(1), nil) + + err := pd.DeleteByDashboard(context.Background(), dashboard) + require.NoError(t, err) + }) +} + +func TestGenerateAccessToken(t *testing.T) { + accessToken, err := GenerateAccessToken() + + t.Run("length", func(t *testing.T) { + require.NoError(t, err) + assert.Equal(t, 32, len(accessToken)) + }) + + t.Run("no - ", func(t *testing.T) { + assert.False(t, strings.Contains("-", accessToken)) + }) +} + func CreateDatasource(dsType string, uid string) struct { Type *string `json:"type,omitempty"` Uid *string `json:"uid,omitempty"` @@ -1070,16 +1124,3 @@ func insertTestDashboard(t *testing.T, dashboardStore dashboards.Store, title st dash.Data.Set("uid", dash.UID) return dash } - -func TestGenerateAccessToken(t *testing.T) { - accessToken, err := GenerateAccessToken() - - t.Run("length", func(t *testing.T) { - require.NoError(t, err) - assert.Equal(t, 32, len(accessToken)) - }) - - t.Run("no - ", func(t *testing.T) { - assert.False(t, strings.Contains("-", accessToken)) - }) -} diff --git a/pkg/services/publicdashboards/service/service_wapper.go b/pkg/services/publicdashboards/service/service_wapper.go index c29919072c7..6dcb35ee82f 100644 --- a/pkg/services/publicdashboards/service/service_wapper.go +++ b/pkg/services/publicdashboards/service/service_wapper.go @@ -43,3 +43,12 @@ func (pd *PublicDashboardServiceWrapperImpl) FindByDashboardUid(ctx context.Cont return pubdash, nil } + +func (pd *PublicDashboardServiceWrapperImpl) Delete(ctx context.Context, uid string) error { + _, err := pd.store.Delete(ctx, uid) + if err != nil { + return ErrInternalServerError.Errorf("Delete: failed to delete a public dashboard by Uid: %s %w", uid, err) + } + + return nil +} From 4b94c7e5d200ae064adcccd6ce4f2cb0a823cd18 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Wed, 8 Mar 2023 13:52:31 -0800 Subject: [PATCH 091/288] Schema: Remove `key` from root DataQuery type (#64467) --- .../schema-reference.md | 10 +++--- .../lokidataquery/schema-reference.md | 10 +++--- .../parcadataquery/schema-reference.md | 10 +++--- .../phlaredataquery/schema-reference.md | 10 +++--- .../testdatadataquery/schema-reference.md | 10 +++--- .../grafana-schema/src/common/common.gen.ts | 10 +++--- .../src/common/dataquery_gen.cue | 9 +++--- .../grafana-schema/src/veneer/common.types.ts | 5 +++ pkg/kindsys/common_dataquery.cue | 9 +++--- .../kinds/dataquery/types_dataquery_gen.go | 11 ++++--- .../kinds/dataquery/types_dataquery_gen.go | 31 ++++++++++--------- .../kinds/dataquery/types_dataquery_gen.go | 9 +++--- .../kinds/dataquery/types_dataquery_gen.go | 9 +++--- .../kinds/dataquery/types_dataquery_gen.go | 9 +++--- .../kinds/dataquery/types_dataquery_gen.go | 9 +++--- .../kinds/dataquery/types_dataquery_gen.go | 9 +++--- .../kinds/dataquery/types_dataquery_gen.go | 11 ++++--- 17 files changed, 94 insertions(+), 87 deletions(-) diff --git a/docs/sources/developers/kinds/composable/elasticsearchdataquery/schema-reference.md b/docs/sources/developers/kinds/composable/elasticsearchdataquery/schema-reference.md index 5bebae059db..dda50523b93 100644 --- a/docs/sources/developers/kinds/composable/elasticsearchdataquery/schema-reference.md +++ b/docs/sources/developers/kinds/composable/elasticsearchdataquery/schema-reference.md @@ -17,12 +17,11 @@ It extends [DataQuery](#dataquery). | Property | Type | Required | Description | |--------------|-------------------------------------------|----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `refId` | string | **Yes** | *(Inherited from [DataQuery](#dataquery))*
A - Z | +| `refId` | string | **Yes** | *(Inherited from [DataQuery](#dataquery))*
A unique identifier for the query within the list of targets.
In server side expressions, the refId is used as a variable name to identify results.
By default, the UI will assign A->Z; however setting meaningful names may be useful. | | `alias` | string | No | Alias pattern | | `bucketAggs` | [BucketAggregation](#bucketaggregation)[] | No | List of bucket aggregations | | `datasource` | | No | *(Inherited from [DataQuery](#dataquery))*
For mixed data sources the selected datasource is on the query level.
For non mixed scenarios this is undefined.
TODO find a better way to do this ^ that's friendly to schema
TODO this shouldn't be unknown but DataSourceRef | null | -| `hide` | boolean | No | *(Inherited from [DataQuery](#dataquery))*
true if query is disabled (ie should not be returned to the dashboard) | -| `key` | string | No | *(Inherited from [DataQuery](#dataquery))*
Unique, guid like, string used in explore mode | +| `hide` | boolean | No | *(Inherited from [DataQuery](#dataquery))*
true if query is disabled (ie should not be returned to the dashboard)
Note this does not always imply that the query should not be executed since
the results from a hidden query may be used as the input to other queries (SSE etc) | | `metrics` | [MetricAggregation](#metricaggregation)[] | No | List of metric aggregations | | `queryType` | string | No | *(Inherited from [DataQuery](#dataquery))*
Specify the query flavor
TODO make this required and give it a default | | `query` | string | No | Lucene query | @@ -126,10 +125,9 @@ properties for the given context. | Property | Type | Required | Description | |--------------|---------|----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `refId` | string | **Yes** | A - Z | +| `refId` | string | **Yes** | A unique identifier for the query within the list of targets.
In server side expressions, the refId is used as a variable name to identify results.
By default, the UI will assign A->Z; however setting meaningful names may be useful. | | `datasource` | | No | For mixed data sources the selected datasource is on the query level.
For non mixed scenarios this is undefined.
TODO find a better way to do this ^ that's friendly to schema
TODO this shouldn't be unknown but DataSourceRef | null | -| `hide` | boolean | No | true if query is disabled (ie should not be returned to the dashboard) | -| `key` | string | No | Unique, guid like, string used in explore mode | +| `hide` | boolean | No | true if query is disabled (ie should not be returned to the dashboard)
Note this does not always imply that the query should not be executed since
the results from a hidden query may be used as the input to other queries (SSE etc) | | `queryType` | string | No | Specify the query flavor
TODO make this required and give it a default | ### MetricAggregation diff --git a/docs/sources/developers/kinds/composable/lokidataquery/schema-reference.md b/docs/sources/developers/kinds/composable/lokidataquery/schema-reference.md index b57bbd97795..6b022166568 100644 --- a/docs/sources/developers/kinds/composable/lokidataquery/schema-reference.md +++ b/docs/sources/developers/kinds/composable/lokidataquery/schema-reference.md @@ -18,12 +18,11 @@ It extends [DataQuery](#dataquery). | Property | Type | Required | Description | |----------------|---------|----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `expr` | string | **Yes** | The LogQL query. | -| `refId` | string | **Yes** | *(Inherited from [DataQuery](#dataquery))*
A - Z | +| `refId` | string | **Yes** | *(Inherited from [DataQuery](#dataquery))*
A unique identifier for the query within the list of targets.
In server side expressions, the refId is used as a variable name to identify results.
By default, the UI will assign A->Z; however setting meaningful names may be useful. | | `datasource` | | No | *(Inherited from [DataQuery](#dataquery))*
For mixed data sources the selected datasource is on the query level.
For non mixed scenarios this is undefined.
TODO find a better way to do this ^ that's friendly to schema
TODO this shouldn't be unknown but DataSourceRef | null | | `editorMode` | string | No | Possible values are: `code`, `builder`. | -| `hide` | boolean | No | *(Inherited from [DataQuery](#dataquery))*
true if query is disabled (ie should not be returned to the dashboard) | +| `hide` | boolean | No | *(Inherited from [DataQuery](#dataquery))*
true if query is disabled (ie should not be returned to the dashboard)
Note this does not always imply that the query should not be executed since
the results from a hidden query may be used as the input to other queries (SSE etc) | | `instant` | boolean | No | @deprecated, now use queryType. | -| `key` | string | No | *(Inherited from [DataQuery](#dataquery))*
Unique, guid like, string used in explore mode | | `legendFormat` | string | No | Used to override the name of the series. | | `maxLines` | integer | No | Used to limit the number of log rows returned. | | `queryType` | string | No | *(Inherited from [DataQuery](#dataquery))*
Specify the query flavor
TODO make this required and give it a default | @@ -38,10 +37,9 @@ properties for the given context. | Property | Type | Required | Description | |--------------|---------|----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `refId` | string | **Yes** | A - Z | +| `refId` | string | **Yes** | A unique identifier for the query within the list of targets.
In server side expressions, the refId is used as a variable name to identify results.
By default, the UI will assign A->Z; however setting meaningful names may be useful. | | `datasource` | | No | For mixed data sources the selected datasource is on the query level.
For non mixed scenarios this is undefined.
TODO find a better way to do this ^ that's friendly to schema
TODO this shouldn't be unknown but DataSourceRef | null | -| `hide` | boolean | No | true if query is disabled (ie should not be returned to the dashboard) | -| `key` | string | No | Unique, guid like, string used in explore mode | +| `hide` | boolean | No | true if query is disabled (ie should not be returned to the dashboard)
Note this does not always imply that the query should not be executed since
the results from a hidden query may be used as the input to other queries (SSE etc) | | `queryType` | string | No | Specify the query flavor
TODO make this required and give it a default | diff --git a/docs/sources/developers/kinds/composable/parcadataquery/schema-reference.md b/docs/sources/developers/kinds/composable/parcadataquery/schema-reference.md index 486fb6ef4cb..3bc24556ada 100644 --- a/docs/sources/developers/kinds/composable/parcadataquery/schema-reference.md +++ b/docs/sources/developers/kinds/composable/parcadataquery/schema-reference.md @@ -19,10 +19,9 @@ It extends [DataQuery](#dataquery). |-----------------|---------|----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `labelSelector` | string | **Yes** | Specifies the query label selectors. Default: `{}`. | | `profileTypeId` | string | **Yes** | Specifies the type of profile to query. | -| `refId` | string | **Yes** | *(Inherited from [DataQuery](#dataquery))*
A - Z | +| `refId` | string | **Yes** | *(Inherited from [DataQuery](#dataquery))*
A unique identifier for the query within the list of targets.
In server side expressions, the refId is used as a variable name to identify results.
By default, the UI will assign A->Z; however setting meaningful names may be useful. | | `datasource` | | No | *(Inherited from [DataQuery](#dataquery))*
For mixed data sources the selected datasource is on the query level.
For non mixed scenarios this is undefined.
TODO find a better way to do this ^ that's friendly to schema
TODO this shouldn't be unknown but DataSourceRef | null | -| `hide` | boolean | No | *(Inherited from [DataQuery](#dataquery))*
true if query is disabled (ie should not be returned to the dashboard) | -| `key` | string | No | *(Inherited from [DataQuery](#dataquery))*
Unique, guid like, string used in explore mode | +| `hide` | boolean | No | *(Inherited from [DataQuery](#dataquery))*
true if query is disabled (ie should not be returned to the dashboard)
Note this does not always imply that the query should not be executed since
the results from a hidden query may be used as the input to other queries (SSE etc) | | `queryType` | string | No | *(Inherited from [DataQuery](#dataquery))*
Specify the query flavor
TODO make this required and give it a default | ### DataQuery @@ -33,10 +32,9 @@ properties for the given context. | Property | Type | Required | Description | |--------------|---------|----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `refId` | string | **Yes** | A - Z | +| `refId` | string | **Yes** | A unique identifier for the query within the list of targets.
In server side expressions, the refId is used as a variable name to identify results.
By default, the UI will assign A->Z; however setting meaningful names may be useful. | | `datasource` | | No | For mixed data sources the selected datasource is on the query level.
For non mixed scenarios this is undefined.
TODO find a better way to do this ^ that's friendly to schema
TODO this shouldn't be unknown but DataSourceRef | null | -| `hide` | boolean | No | true if query is disabled (ie should not be returned to the dashboard) | -| `key` | string | No | Unique, guid like, string used in explore mode | +| `hide` | boolean | No | true if query is disabled (ie should not be returned to the dashboard)
Note this does not always imply that the query should not be executed since
the results from a hidden query may be used as the input to other queries (SSE etc) | | `queryType` | string | No | Specify the query flavor
TODO make this required and give it a default | diff --git a/docs/sources/developers/kinds/composable/phlaredataquery/schema-reference.md b/docs/sources/developers/kinds/composable/phlaredataquery/schema-reference.md index e8779c916e4..bafb2005be1 100644 --- a/docs/sources/developers/kinds/composable/phlaredataquery/schema-reference.md +++ b/docs/sources/developers/kinds/composable/phlaredataquery/schema-reference.md @@ -20,10 +20,9 @@ It extends [DataQuery](#dataquery). | `groupBy` | string[] | **Yes** | Allows to group the results. | | `labelSelector` | string | **Yes** | Specifies the query label selectors. Default: `{}`. | | `profileTypeId` | string | **Yes** | Specifies the type of profile to query. | -| `refId` | string | **Yes** | *(Inherited from [DataQuery](#dataquery))*
A - Z | +| `refId` | string | **Yes** | *(Inherited from [DataQuery](#dataquery))*
A unique identifier for the query within the list of targets.
In server side expressions, the refId is used as a variable name to identify results.
By default, the UI will assign A->Z; however setting meaningful names may be useful. | | `datasource` | | No | *(Inherited from [DataQuery](#dataquery))*
For mixed data sources the selected datasource is on the query level.
For non mixed scenarios this is undefined.
TODO find a better way to do this ^ that's friendly to schema
TODO this shouldn't be unknown but DataSourceRef | null | -| `hide` | boolean | No | *(Inherited from [DataQuery](#dataquery))*
true if query is disabled (ie should not be returned to the dashboard) | -| `key` | string | No | *(Inherited from [DataQuery](#dataquery))*
Unique, guid like, string used in explore mode | +| `hide` | boolean | No | *(Inherited from [DataQuery](#dataquery))*
true if query is disabled (ie should not be returned to the dashboard)
Note this does not always imply that the query should not be executed since
the results from a hidden query may be used as the input to other queries (SSE etc) | | `queryType` | string | No | *(Inherited from [DataQuery](#dataquery))*
Specify the query flavor
TODO make this required and give it a default | ### DataQuery @@ -34,10 +33,9 @@ properties for the given context. | Property | Type | Required | Description | |--------------|---------|----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `refId` | string | **Yes** | A - Z | +| `refId` | string | **Yes** | A unique identifier for the query within the list of targets.
In server side expressions, the refId is used as a variable name to identify results.
By default, the UI will assign A->Z; however setting meaningful names may be useful. | | `datasource` | | No | For mixed data sources the selected datasource is on the query level.
For non mixed scenarios this is undefined.
TODO find a better way to do this ^ that's friendly to schema
TODO this shouldn't be unknown but DataSourceRef | null | -| `hide` | boolean | No | true if query is disabled (ie should not be returned to the dashboard) | -| `key` | string | No | Unique, guid like, string used in explore mode | +| `hide` | boolean | No | true if query is disabled (ie should not be returned to the dashboard)
Note this does not always imply that the query should not be executed since
the results from a hidden query may be used as the input to other queries (SSE etc) | | `queryType` | string | No | Specify the query flavor
TODO make this required and give it a default | diff --git a/docs/sources/developers/kinds/composable/testdatadataquery/schema-reference.md b/docs/sources/developers/kinds/composable/testdatadataquery/schema-reference.md index 0d576fbdd84..f571a4274b5 100644 --- a/docs/sources/developers/kinds/composable/testdatadataquery/schema-reference.md +++ b/docs/sources/developers/kinds/composable/testdatadataquery/schema-reference.md @@ -17,7 +17,7 @@ It extends [DataQuery](#dataquery). | Property | Type | Required | Description | |-------------------|-------------------------------------|----------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `refId` | string | **Yes** | *(Inherited from [DataQuery](#dataquery))*
A - Z | +| `refId` | string | **Yes** | *(Inherited from [DataQuery](#dataquery))*
A unique identifier for the query within the list of targets.
In server side expressions, the refId is used as a variable name to identify results.
By default, the UI will assign A->Z; however setting meaningful names may be useful. | | `alias` | string | No | | | `channel` | string | No | | | `csvContent` | string | No | | @@ -25,8 +25,7 @@ It extends [DataQuery](#dataquery). | `csvWave` | [CSVWave](#csvwave)[] | No | | | `datasource` | | No | *(Inherited from [DataQuery](#dataquery))*
For mixed data sources the selected datasource is on the query level.
For non mixed scenarios this is undefined.
TODO find a better way to do this ^ that's friendly to schema
TODO this shouldn't be unknown but DataSourceRef | null | | `errorType` | string | No | Possible values are: `server_panic`, `frontend_exception`, `frontend_observable`. | -| `hide` | boolean | No | *(Inherited from [DataQuery](#dataquery))*
true if query is disabled (ie should not be returned to the dashboard) | -| `key` | string | No | *(Inherited from [DataQuery](#dataquery))*
Unique, guid like, string used in explore mode | +| `hide` | boolean | No | *(Inherited from [DataQuery](#dataquery))*
true if query is disabled (ie should not be returned to the dashboard)
Note this does not always imply that the query should not be executed since
the results from a hidden query may be used as the input to other queries (SSE etc) | | `labels` | string | No | | | `levelColumn` | boolean | No | | | `lines` | integer | No | | @@ -60,10 +59,9 @@ properties for the given context. | Property | Type | Required | Description | |--------------|---------|----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `refId` | string | **Yes** | A - Z | +| `refId` | string | **Yes** | A unique identifier for the query within the list of targets.
In server side expressions, the refId is used as a variable name to identify results.
By default, the UI will assign A->Z; however setting meaningful names may be useful. | | `datasource` | | No | For mixed data sources the selected datasource is on the query level.
For non mixed scenarios this is undefined.
TODO find a better way to do this ^ that's friendly to schema
TODO this shouldn't be unknown but DataSourceRef | null | -| `hide` | boolean | No | true if query is disabled (ie should not be returned to the dashboard) | -| `key` | string | No | Unique, guid like, string used in explore mode | +| `hide` | boolean | No | true if query is disabled (ie should not be returned to the dashboard)
Note this does not always imply that the query should not be executed since
the results from a hidden query may be used as the input to other queries (SSE etc) | | `queryType` | string | No | Specify the query flavor
TODO make this required and give it a default | ### NodesQuery diff --git a/packages/grafana-schema/src/common/common.gen.ts b/packages/grafana-schema/src/common/common.gen.ts index 4282f9d3c27..b256ed8941c 100644 --- a/packages/grafana-schema/src/common/common.gen.ts +++ b/packages/grafana-schema/src/common/common.gen.ts @@ -34,19 +34,19 @@ export interface DataQuery { datasource?: unknown; /** * true if query is disabled (ie should not be returned to the dashboard) + * Note this does not always imply that the query should not be executed since + * the results from a hidden query may be used as the input to other queries (SSE etc) */ hide?: boolean; - /** - * Unique, guid like, string used in explore mode - */ - key?: string; /** * Specify the query flavor * TODO make this required and give it a default */ queryType?: string; /** - * A - Z + * A unique identifier for the query within the list of targets. + * In server side expressions, the refId is used as a variable name to identify results. + * By default, the UI will assign A->Z; however setting meaningful names may be useful. */ refId: string; } diff --git a/packages/grafana-schema/src/common/dataquery_gen.cue b/packages/grafana-schema/src/common/dataquery_gen.cue index 25b5556961b..3d6c8f721da 100644 --- a/packages/grafana-schema/src/common/dataquery_gen.cue +++ b/packages/grafana-schema/src/common/dataquery_gen.cue @@ -18,15 +18,16 @@ package common // Specific implementations will *extend* this interface, adding the required // properties for the given context. DataQuery: { - // A - Z + // A unique identifier for the query within the list of targets. + // In server side expressions, the refId is used as a variable name to identify results. + // By default, the UI will assign A->Z; however setting meaningful names may be useful. refId: string // true if query is disabled (ie should not be returned to the dashboard) + // Note this does not always imply that the query should not be executed since + // the results from a hidden query may be used as the input to other queries (SSE etc) hide?: bool - // Unique, guid like, string used in explore mode - key?: string - // Specify the query flavor // TODO make this required and give it a default queryType?: string diff --git a/packages/grafana-schema/src/veneer/common.types.ts b/packages/grafana-schema/src/veneer/common.types.ts index 704e37ecef4..f5a38ac2502 100644 --- a/packages/grafana-schema/src/veneer/common.types.ts +++ b/packages/grafana-schema/src/veneer/common.types.ts @@ -9,6 +9,11 @@ export interface MapLayerOptions extends raw.MapLayerOptions { } export interface DataQuery extends raw.DataQuery { + /** + * Unique, guid like, string (used only in explore mode) + */ + key?: string; + // TODO remove explicit nulls datasource?: raw.DataSourceRef | null; } diff --git a/pkg/kindsys/common_dataquery.cue b/pkg/kindsys/common_dataquery.cue index 4d56c22f497..bb11488efc8 100644 --- a/pkg/kindsys/common_dataquery.cue +++ b/pkg/kindsys/common_dataquery.cue @@ -9,15 +9,16 @@ package kindsys // Specific implementations will *extend* this interface, adding the required // properties for the given context. DataQuery: { - // A - Z + // A unique identifier for the query within the list of targets. + // In server side expressions, the refId is used as a variable name to identify results. + // By default, the UI will assign A->Z; however setting meaningful names may be useful. refId: string // true if query is disabled (ie should not be returned to the dashboard) + // Note this does not always imply that the query should not be executed since + // the results from a hidden query may be used as the input to other queries (SSE etc) hide?: bool - // Unique, guid like, string used in explore mode - key?: string - // Specify the query flavor // TODO make this required and give it a default queryType?: string diff --git a/pkg/tsdb/azuremonitor/kinds/dataquery/types_dataquery_gen.go b/pkg/tsdb/azuremonitor/kinds/dataquery/types_dataquery_gen.go index 7eb0ab4ca2f..96600703b7d 100644 --- a/pkg/tsdb/azuremonitor/kinds/dataquery/types_dataquery_gen.go +++ b/pkg/tsdb/azuremonitor/kinds/dataquery/types_dataquery_gen.go @@ -362,17 +362,18 @@ type AzureMonitorQuery struct { GrafanaTemplateVariableFn *AzureMonitorQueryGrafanaTemplateVariableFn `json:"grafanaTemplateVariableFn,omitempty"` // Hide true if query is disabled (ie should not be returned to the dashboard) - Hide *bool `json:"hide,omitempty"` - - // Unique, guid like, string used in explore mode - Key *string `json:"key,omitempty"` + // Note this does not always imply that the query should not be executed since + // the results from a hidden query may be used as the input to other queries (SSE etc) + Hide *bool `json:"hide,omitempty"` Namespace *string `json:"namespace,omitempty"` // Specify the query flavor // TODO make this required and give it a default QueryType *string `json:"queryType,omitempty"` - // A - Z + // A unique identifier for the query within the list of targets. + // In server side expressions, the refId is used as a variable name to identify results. + // By default, the UI will assign A->Z; however setting meaningful names may be useful. RefId string `json:"refId"` // Azure Monitor query type. diff --git a/pkg/tsdb/cloudwatch/kinds/dataquery/types_dataquery_gen.go b/pkg/tsdb/cloudwatch/kinds/dataquery/types_dataquery_gen.go index cdb05267f2d..0fb508c16ec 100644 --- a/pkg/tsdb/cloudwatch/kinds/dataquery/types_dataquery_gen.go +++ b/pkg/tsdb/cloudwatch/kinds/dataquery/types_dataquery_gen.go @@ -240,10 +240,9 @@ type CloudWatchAnnotationQuery struct { Dimensions map[string]interface{} `json:"dimensions,omitempty"` // Hide true if query is disabled (ie should not be returned to the dashboard) - Hide *bool `json:"hide,omitempty"` - - // Unique, guid like, string used in explore mode - Key *string `json:"key,omitempty"` + // Note this does not always imply that the query should not be executed since + // the results from a hidden query may be used as the input to other queries (SSE etc) + Hide *bool `json:"hide,omitempty"` MatchExact *bool `json:"matchExact,omitempty"` MetricName *string `json:"metricName,omitempty"` Namespace string `json:"namespace"` @@ -255,7 +254,9 @@ type CloudWatchAnnotationQuery struct { // TODO make this required and give it a default QueryType *string `json:"queryType,omitempty"` - // A - Z + // A unique identifier for the query within the list of targets. + // In server side expressions, the refId is used as a variable name to identify results. + // By default, the UI will assign A->Z; however setting meaningful names may be useful. RefId string `json:"refId"` Region string `json:"region"` Statistic *string `json:"statistic,omitempty"` @@ -280,12 +281,11 @@ type CloudWatchLogsQuery struct { Expression *string `json:"expression,omitempty"` // Hide true if query is disabled (ie should not be returned to the dashboard) + // Note this does not always imply that the query should not be executed since + // the results from a hidden query may be used as the input to other queries (SSE etc) Hide *bool `json:"hide,omitempty"` Id string `json:"id"` - // Unique, guid like, string used in explore mode - Key *string `json:"key,omitempty"` - // LogGroupNames deprecated, use logGroups instead LogGroupNames []string `json:"logGroupNames,omitempty"` LogGroups []struct { @@ -300,7 +300,9 @@ type CloudWatchLogsQuery struct { // TODO make this required and give it a default QueryType *string `json:"queryType,omitempty"` - // A - Z + // A unique identifier for the query within the list of targets. + // In server side expressions, the refId is used as a variable name to identify results. + // By default, the UI will assign A->Z; however setting meaningful names may be useful. RefId string `json:"refId"` Region string `json:"region"` StatsGroups []string `json:"statsGroups,omitempty"` @@ -325,13 +327,12 @@ type CloudWatchMetricsQuery struct { Expression *string `json:"expression,omitempty"` // Hide true if query is disabled (ie should not be returned to the dashboard) + // Note this does not always imply that the query should not be executed since + // the results from a hidden query may be used as the input to other queries (SSE etc) Hide *bool `json:"hide,omitempty"` // Id common props - Id string `json:"id"` - - // Unique, guid like, string used in explore mode - Key *string `json:"key,omitempty"` + Id string `json:"id"` Label *string `json:"label,omitempty"` MatchExact *bool `json:"matchExact,omitempty"` MetricEditorMode *CloudWatchMetricsQueryMetricEditorMode `json:"metricEditorMode,omitempty"` @@ -345,7 +346,9 @@ type CloudWatchMetricsQuery struct { // TODO make this required and give it a default QueryType *string `json:"queryType,omitempty"` - // A - Z + // A unique identifier for the query within the list of targets. + // In server side expressions, the refId is used as a variable name to identify results. + // By default, the UI will assign A->Z; however setting meaningful names may be useful. RefId string `json:"refId"` Region string `json:"region"` Sql *struct { diff --git a/pkg/tsdb/elasticsearch/kinds/dataquery/types_dataquery_gen.go b/pkg/tsdb/elasticsearch/kinds/dataquery/types_dataquery_gen.go index 84bf27931d0..ee8ce6a6ff0 100644 --- a/pkg/tsdb/elasticsearch/kinds/dataquery/types_dataquery_gen.go +++ b/pkg/tsdb/elasticsearch/kinds/dataquery/types_dataquery_gen.go @@ -622,11 +622,10 @@ type ElasticsearchDataQuery struct { Datasource *interface{} `json:"datasource,omitempty"` // Hide true if query is disabled (ie should not be returned to the dashboard) + // Note this does not always imply that the query should not be executed since + // the results from a hidden query may be used as the input to other queries (SSE etc) Hide *bool `json:"hide,omitempty"` - // Unique, guid like, string used in explore mode - Key *string `json:"key,omitempty"` - // List of metric aggregations Metrics []MetricsItem `json:"metrics,omitempty"` @@ -637,7 +636,9 @@ type ElasticsearchDataQuery struct { // TODO make this required and give it a default QueryType *string `json:"queryType,omitempty"` - // A - Z + // A unique identifier for the query within the list of targets. + // In server side expressions, the refId is used as a variable name to identify results. + // By default, the UI will assign A->Z; however setting meaningful names may be useful. RefId string `json:"refId"` // Name of time field diff --git a/pkg/tsdb/loki/kinds/dataquery/types_dataquery_gen.go b/pkg/tsdb/loki/kinds/dataquery/types_dataquery_gen.go index f8f4ec30948..9dadf73ad60 100644 --- a/pkg/tsdb/loki/kinds/dataquery/types_dataquery_gen.go +++ b/pkg/tsdb/loki/kinds/dataquery/types_dataquery_gen.go @@ -54,14 +54,13 @@ type LokiDataQuery struct { Expr string `json:"expr"` // Hide true if query is disabled (ie should not be returned to the dashboard) + // Note this does not always imply that the query should not be executed since + // the results from a hidden query may be used as the input to other queries (SSE etc) Hide *bool `json:"hide,omitempty"` // @deprecated, now use queryType. Instant *bool `json:"instant,omitempty"` - // Unique, guid like, string used in explore mode - Key *string `json:"key,omitempty"` - // Used to override the name of the series. LegendFormat *string `json:"legendFormat,omitempty"` @@ -75,7 +74,9 @@ type LokiDataQuery struct { // @deprecated, now use queryType. Range *bool `json:"range,omitempty"` - // A - Z + // A unique identifier for the query within the list of targets. + // In server side expressions, the refId is used as a variable name to identify results. + // By default, the UI will assign A->Z; however setting meaningful names may be useful. RefId string `json:"refId"` // Used to scale the interval value. diff --git a/pkg/tsdb/parca/kinds/dataquery/types_dataquery_gen.go b/pkg/tsdb/parca/kinds/dataquery/types_dataquery_gen.go index 20936bf2a1c..72cb7ff1492 100644 --- a/pkg/tsdb/parca/kinds/dataquery/types_dataquery_gen.go +++ b/pkg/tsdb/parca/kinds/dataquery/types_dataquery_gen.go @@ -25,11 +25,10 @@ type ParcaDataQuery struct { Datasource *interface{} `json:"datasource,omitempty"` // Hide true if query is disabled (ie should not be returned to the dashboard) + // Note this does not always imply that the query should not be executed since + // the results from a hidden query may be used as the input to other queries (SSE etc) Hide *bool `json:"hide,omitempty"` - // Unique, guid like, string used in explore mode - Key *string `json:"key,omitempty"` - // Specifies the query label selectors. LabelSelector string `json:"labelSelector"` @@ -40,7 +39,9 @@ type ParcaDataQuery struct { // TODO make this required and give it a default QueryType *string `json:"queryType,omitempty"` - // A - Z + // A unique identifier for the query within the list of targets. + // In server side expressions, the refId is used as a variable name to identify results. + // By default, the UI will assign A->Z; however setting meaningful names may be useful. RefId string `json:"refId"` } diff --git a/pkg/tsdb/phlare/kinds/dataquery/types_dataquery_gen.go b/pkg/tsdb/phlare/kinds/dataquery/types_dataquery_gen.go index a4e30e565d8..9f84d4839ee 100644 --- a/pkg/tsdb/phlare/kinds/dataquery/types_dataquery_gen.go +++ b/pkg/tsdb/phlare/kinds/dataquery/types_dataquery_gen.go @@ -28,11 +28,10 @@ type PhlareDataQuery struct { GroupBy []string `json:"groupBy"` // Hide true if query is disabled (ie should not be returned to the dashboard) + // Note this does not always imply that the query should not be executed since + // the results from a hidden query may be used as the input to other queries (SSE etc) Hide *bool `json:"hide,omitempty"` - // Unique, guid like, string used in explore mode - Key *string `json:"key,omitempty"` - // Specifies the query label selectors. LabelSelector string `json:"labelSelector"` @@ -43,7 +42,9 @@ type PhlareDataQuery struct { // TODO make this required and give it a default QueryType *string `json:"queryType,omitempty"` - // A - Z + // A unique identifier for the query within the list of targets. + // In server side expressions, the refId is used as a variable name to identify results. + // By default, the UI will assign A->Z; however setting meaningful names may be useful. RefId string `json:"refId"` } diff --git a/pkg/tsdb/tempo/kinds/dataquery/types_dataquery_gen.go b/pkg/tsdb/tempo/kinds/dataquery/types_dataquery_gen.go index 06fb1fb74e2..eba542a487c 100644 --- a/pkg/tsdb/tempo/kinds/dataquery/types_dataquery_gen.go +++ b/pkg/tsdb/tempo/kinds/dataquery/types_dataquery_gen.go @@ -69,11 +69,10 @@ type TempoQuery struct { } `json:"filters"` // Hide true if query is disabled (ie should not be returned to the dashboard) + // Note this does not always imply that the query should not be executed since + // the results from a hidden query may be used as the input to other queries (SSE etc) Hide *bool `json:"hide,omitempty"` - // Unique, guid like, string used in explore mode - Key *string `json:"key,omitempty"` - // Defines the maximum number of traces that are returned from Tempo Limit *int64 `json:"limit,omitempty"` @@ -90,7 +89,9 @@ type TempoQuery struct { // TODO make this required and give it a default QueryType *string `json:"queryType,omitempty"` - // A - Z + // A unique identifier for the query within the list of targets. + // In server side expressions, the refId is used as a variable name to identify results. + // By default, the UI will assign A->Z; however setting meaningful names may be useful. RefId string `json:"refId"` // Logfmt query to filter traces by their tags. Example: http.status_code=200 error=true diff --git a/pkg/tsdb/testdatasource/kinds/dataquery/types_dataquery_gen.go b/pkg/tsdb/testdatasource/kinds/dataquery/types_dataquery_gen.go index e772bf88582..b5525e6267e 100644 --- a/pkg/tsdb/testdatasource/kinds/dataquery/types_dataquery_gen.go +++ b/pkg/tsdb/testdatasource/kinds/dataquery/types_dataquery_gen.go @@ -191,10 +191,9 @@ type TestDataDataQuery struct { ErrorType *ErrorType `json:"errorType,omitempty"` // Hide true if query is disabled (ie should not be returned to the dashboard) - Hide *bool `json:"hide,omitempty"` - - // Unique, guid like, string used in explore mode - Key *string `json:"key,omitempty"` + // Note this does not always imply that the query should not be executed since + // the results from a hidden query may be used as the input to other queries (SSE etc) + Hide *bool `json:"hide,omitempty"` Labels *string `json:"labels,omitempty"` LevelColumn *bool `json:"levelColumn,omitempty"` Lines *int64 `json:"lines,omitempty"` @@ -216,7 +215,9 @@ type TestDataDataQuery struct { QueryType *string `json:"queryType,omitempty"` RawFrameContent *string `json:"rawFrameContent,omitempty"` - // A - Z + // A unique identifier for the query within the list of targets. + // In server side expressions, the refId is used as a variable name to identify results. + // By default, the UI will assign A->Z; however setting meaningful names may be useful. RefId string `json:"refId"` ScenarioId *ScenarioId `json:"scenarioId,omitempty"` SeriesCount *int32 `json:"seriesCount,omitempty"` From f23c8e5cd1d890c309e661b8d139b222b13fc787 Mon Sep 17 00:00:00 2001 From: Artur Wierzbicki Date: Thu, 9 Mar 2023 10:01:44 +0400 Subject: [PATCH 092/288] Chore: move `sessionId` from Live service (#64465) * remove sessionid from live * remove sessionid from live * use uuid rather than math.random --- .betterer.results | 6 ++---- public/app/features/live/centrifuge/service.ts | 1 - public/app/features/live/dashboard/dashboardWatcher.ts | 6 +++++- public/app/features/live/index.ts | 8 -------- 4 files changed, 7 insertions(+), 14 deletions(-) diff --git a/.betterer.results b/.betterer.results index ec5f720494e..9ffb427a6de 100644 --- a/.betterer.results +++ b/.betterer.results @@ -1,5 +1,5 @@ // BETTERER RESULTS V2. -// +// // If this file contains merge conflicts, use `betterer merge` to automatically resolve them: // https://phenomnomnominal.github.io/betterer/docs/results-file/#merge // @@ -3675,9 +3675,7 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "1"] ], "public/app/features/live/index.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Do not use any type assertions.", "2"] + [0, 0, 0, "Do not use any type assertions.", "0"] ], "public/app/features/live/pages/AddNewRule.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] diff --git a/public/app/features/live/centrifuge/service.ts b/public/app/features/live/centrifuge/service.ts index 41042f10e8b..affb722ac9e 100644 --- a/public/app/features/live/centrifuge/service.ts +++ b/public/app/features/live/centrifuge/service.ts @@ -29,7 +29,6 @@ export type CentrifugeSrvDeps = { appUrl: string; orgId: number; orgRole: string; - sessionId: string; liveEnabled: boolean; dataStreamSubscriberReadiness: Observable; }; diff --git a/public/app/features/live/dashboard/dashboardWatcher.ts b/public/app/features/live/dashboard/dashboardWatcher.ts index e7cccd2b721..3572a77011f 100644 --- a/public/app/features/live/dashboard/dashboardWatcher.ts +++ b/public/app/features/live/dashboard/dashboardWatcher.ts @@ -1,4 +1,5 @@ import { Unsubscribable } from 'rxjs'; +import { v4 as uuidv4 } from 'uuid'; import { AppEvents, @@ -11,7 +12,6 @@ import { } from '@grafana/data'; import { getGrafanaLiveSrv, locationService } from '@grafana/runtime'; import { appEvents, contextSrv } from 'app/core/core'; -import { sessionId } from 'app/features/live'; import { ShowModalReactEvent } from '../../../types/events'; import { getDashboardSrv } from '../../dashboard/services/DashboardSrv'; @@ -19,6 +19,10 @@ import { getDashboardSrv } from '../../dashboard/services/DashboardSrv'; import { DashboardChangedModal } from './DashboardChangedModal'; import { DashboardEvent, DashboardEventAction } from './types'; +// sessionId is not a security-sensitive value. +// It is used for filtering out dashboard edit events from the same browsing session +const sessionId = uuidv4(); + class DashboardWatcher { channel?: LiveChannelAddress; // path to the channel uid?: string; diff --git a/public/app/features/live/index.ts b/public/app/features/live/index.ts index 6e6ec497752..9b1c10e3573 100644 --- a/public/app/features/live/index.ts +++ b/public/app/features/live/index.ts @@ -8,20 +8,12 @@ import { CentrifugeService } from './centrifuge/service'; import { CentrifugeServiceWorkerProxy } from './centrifuge/serviceWorkerProxy'; import { GrafanaLiveService } from './live'; -export const sessionId = - (window as any)?.grafanaBootData?.user?.id + - '/' + - Date.now().toString(16) + - '/' + - Math.random().toString(36).substring(2, 15); - export function initGrafanaLive() { const centrifugeServiceDeps = { appUrl: `${window.location.origin}${config.appSubUrl}`, orgId: contextSrv.user.orgId, orgRole: contextSrv.user.orgRole, liveEnabled: config.liveEnabled, - sessionId, dataStreamSubscriberReadiness: liveTimer.ok.asObservable(), grafanaAuthToken: loadUrlToken(), }; From 14208c4c423f1f57dfae4bdebe6a7413e7a74fe0 Mon Sep 17 00:00:00 2001 From: brendamuir <100768211+brendamuir@users.noreply.github.com> Date: Thu, 9 Mar 2023 09:18:36 +0100 Subject: [PATCH 093/288] Docs: Updates default template link (#64485) Updates default template link --- .../alerting/fundamentals/alert-rules/message-templating.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/sources/alerting/fundamentals/alert-rules/message-templating.md b/docs/sources/alerting/fundamentals/alert-rules/message-templating.md index c8d2679771f..f816e690a07 100644 --- a/docs/sources/alerting/fundamentals/alert-rules/message-templating.md +++ b/docs/sources/alerting/fundamentals/alert-rules/message-templating.md @@ -16,7 +16,9 @@ weight: 415 # Notification templating -Notifications sent via contact points are built using notification templates. Grafana's default templates are based on the [Go templating system](https://golang.org/pkg/text/template) where some fields are evaluated as text, while others are evaluated as HTML (which can affect escaping). The default template, defined in [default_template.go](https://github.com/grafana/alerting/blob/main/templates/default_template.go), is a useful reference for custom templates. +Notifications sent via contact points are built using notification templates. Grafana's default templates are based on the [Go templating system](https://golang.org/pkg/text/template) where some fields are evaluated as text, while others are evaluated as HTML (which can affect escaping). + +The default template [default_template.go](https://github.com/grafana/alerting/blob/main/templates/default_template.go) is a useful reference for custom templates. Since most of the contact point fields can be templated, you can create reusable custom templates and use them in multiple contact points. From 0bd1ae99dfde41b5c9f1e953ad77952e637a9e25 Mon Sep 17 00:00:00 2001 From: Laura Benz <48948963+L-M-K-B@users.noreply.github.com> Date: Thu, 9 Mar 2023 09:24:45 +0100 Subject: [PATCH 094/288] Laura/standardise border radius in explore (#64338) * refactor: remove border radius in ExploreGraph * refactor: remove border radius in NoData * refactor: remove border radius in TracePageSearchBar * refactor: replace border radius in TracePageSearchBar and TraceViewContainer --- public/app/features/explore/Graph/ExploreGraph.tsx | 6 ++---- public/app/features/explore/NoData.tsx | 2 +- .../app/features/explore/TraceView/TraceViewContainer.tsx | 2 +- .../components/TracePageHeader/TracePageSearchBar.tsx | 2 +- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/public/app/features/explore/Graph/ExploreGraph.tsx b/public/app/features/explore/Graph/ExploreGraph.tsx index 86af63d3d4e..20fff365c49 100644 --- a/public/app/features/explore/Graph/ExploreGraph.tsx +++ b/public/app/features/explore/Graph/ExploreGraph.tsx @@ -1,4 +1,4 @@ -import { css, cx } from '@emotion/css'; +import { css } from '@emotion/css'; import { identity } from 'lodash'; import React, { useEffect, useMemo, useState } from 'react'; @@ -159,7 +159,7 @@ export function ExploreGraph({ return ( {data.length > MAX_NUMBER_OF_TIME_SERIES && !showAllTimeSeries && ( -
+
Showing only {MAX_NUMBER_OF_TIME_SERIES} time series. Date: Thu, 9 Mar 2023 16:43:13 +0200 Subject: [PATCH 117/288] Query Editor: Internal context to actions (#64518) loadedDataSourceIdentifier is not always loaded, but always queried; comment adds understanding to a specific use case --- .../app/features/query/components/QueryEditorRow.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/public/app/features/query/components/QueryEditorRow.tsx b/public/app/features/query/components/QueryEditorRow.tsx index 3c9137fad5d..efcec635deb 100644 --- a/public/app/features/query/components/QueryEditorRow.tsx +++ b/public/app/features/query/components/QueryEditorRow.tsx @@ -64,7 +64,7 @@ interface Props { interface State { /** DatasourceUid or ds variable expression used to resolve current datasource */ - loadedDataSourceIdentifier?: string | null; + queriedDataSourceIdentifier?: string | null; datasource: DataSourceApi | null; datasourceUid?: string | null; hasTextEditMode: boolean; @@ -158,18 +158,19 @@ export class QueryEditorRow extends PureComponent, - loadedDataSourceIdentifier: interpolatedUID, + queriedDataSourceIdentifier: interpolatedUID, hasTextEditMode: has(datasource, 'components.QueryCtrl.prototype.toggleEditorMode'), }); } componentDidUpdate(prevProps: Props) { - const { datasource, loadedDataSourceIdentifier } = this.state; + const { datasource, queriedDataSourceIdentifier } = this.state; const { data, query } = this.props; if (prevProps.id !== this.props.id) { @@ -191,7 +192,7 @@ export class QueryEditorRow extends PureComponent extends PureComponent { From e8c131eb6f77a36b8f7d07df4e4fd725999af502 Mon Sep 17 00:00:00 2001 From: Yahima Duarte Date: Thu, 9 Mar 2023 16:12:38 +0100 Subject: [PATCH 118/288] GrafanaUI: Implement new component Toggletip (#64459) --- .../src/components/Toggletip/Toggletip.mdx | 81 ++++++++ .../components/Toggletip/Toggletip.story.tsx | 178 ++++++++++++++++++ .../components/Toggletip/Toggletip.test.tsx | 90 +++++++++ .../src/components/Toggletip/Toggletip.tsx | 144 ++++++++++++++ .../src/components/Toggletip/index.ts | 2 + .../src/components/Toggletip/types.ts | 9 + .../src/components/Tooltip/Tooltip.tsx | 165 ++-------------- packages/grafana-ui/src/utils/tooltipUtils.ts | 167 ++++++++++++++++ 8 files changed, 689 insertions(+), 147 deletions(-) create mode 100644 packages/grafana-ui/src/components/Toggletip/Toggletip.mdx create mode 100644 packages/grafana-ui/src/components/Toggletip/Toggletip.story.tsx create mode 100644 packages/grafana-ui/src/components/Toggletip/Toggletip.test.tsx create mode 100644 packages/grafana-ui/src/components/Toggletip/Toggletip.tsx create mode 100644 packages/grafana-ui/src/components/Toggletip/index.ts create mode 100644 packages/grafana-ui/src/components/Toggletip/types.ts create mode 100644 packages/grafana-ui/src/utils/tooltipUtils.ts diff --git a/packages/grafana-ui/src/components/Toggletip/Toggletip.mdx b/packages/grafana-ui/src/components/Toggletip/Toggletip.mdx new file mode 100644 index 00000000000..da6bdbdbfa3 --- /dev/null +++ b/packages/grafana-ui/src/components/Toggletip/Toggletip.mdx @@ -0,0 +1,81 @@ +import { Props } from '@storybook/addon-docs/blocks'; +import { Toggletip } from './Toggletip'; + +# Toggletip + +Toggletips, similar to Tooltips, provide contextual support for users when needed. They are hidden by default, a UI trigger or text link are clicked to set them to their visible state. +Toggletips, unlike tooltips, are persistent until a user takes action to dismiss them by clicking on the required “X” (close) trigger. +Toggletips are capable of containing varying types of complex content including interactive components, buttons, and dropdowns. + +## When to use + +- Users need further context to understand or learn a topic +- Links to supporting documentation or content are needed to provide +- When an interactive element must be placed within the popover +- When content needs to persist for consumption until the user dismisses it + +## When not to use + +- When only a primary label or an auxiliary clarification is needed to be displayed (see: Tooltip) +- Do not house information critical to user’s task completion +- Do not request required information from a user to complete a task or workflow + +## Content + +Toggletips are able to house various types of content. Below is a potential list: + +- Buttons +- Text links +- Dropdowns +- Selects +- Images/gifs/videos +- Various combinations of elements — ex: strings of text with buttons and an image + +## Theme + +There are currently 2 themes available for the Toggletip. + +- Info +- Error + +### Info + +This is the default theme, usually used in forms to show more information. + +### Error + +Tooltip with a red background. + +## Triggers + +- Toggletips display on + - user click of UI trigger + - pressing ENTER or SPACE on a keyboard while the trigger element has focus +- Toggletips dismiss by: + - user click of close icon (x) — optional + - clicking outside of the popover container + - pressing the ESC key + +### Usage + +```tsx +function onClose() { + // code to execute when the toggletip is closed +} + +return ( + + + +); +``` + + diff --git a/packages/grafana-ui/src/components/Toggletip/Toggletip.story.tsx b/packages/grafana-ui/src/components/Toggletip/Toggletip.story.tsx new file mode 100644 index 00000000000..326e00b232b --- /dev/null +++ b/packages/grafana-ui/src/components/Toggletip/Toggletip.story.tsx @@ -0,0 +1,178 @@ +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import React from 'react'; + +import { SelectableValue } from '@grafana/data'; + +import { withCenteredStory } from '../../utils/storybook/withCenteredStory'; +import { Button } from '../Button'; +import { ButtonSelect } from '../Dropdown/ButtonSelect'; +import { InlineField } from '../Forms/InlineField'; +import { Icon } from '../Icon/Icon'; +import { Input } from '../Input/Input'; +import { Select } from '../Select/Select'; +import mdx from '../Toggletip/Toggletip.mdx'; + +import { Toggletip } from './Toggletip'; + +const meta: ComponentMeta = { + title: 'Overlays/Toggletip', + component: Toggletip, + decorators: [withCenteredStory], + parameters: { + docs: { + page: mdx, + }, + controls: { + exclude: ['onClose', 'children'], + }, + }, + argTypes: { + title: { + control: { + type: 'text', + }, + }, + content: { + control: { + type: 'text', + }, + }, + footer: { + control: { + type: 'text', + }, + }, + theme: { + control: { + type: 'select', + }, + }, + closeButton: { + control: { + type: 'boolean', + }, + }, + placement: { + control: { + type: 'select', + }, + }, + }, +}; + +export const Basic: ComponentStory = ({ + title, + content, + footer, + theme, + closeButton, + placement, + ...args +}) => { + return ( + + + + ); +}; +Basic.args = { + title: 'Title of the Toggletip', + content: 'This is the content of the Toggletip', + footer: 'Footer of the Toggletip', + placement: 'auto', + closeButton: true, + theme: 'info', +}; + +export const HostingMultiElements: ComponentStory = ({ theme, closeButton, placement }) => { + const selectOptions: Array> = [ + { label: 'Sharilyn Markowitz', value: 1 }, + { label: 'Naomi Striplin', value: 2 }, + { label: 'Beau Bevel', value: 3 }, + { label: 'Garrett Starkes', value: 4 }, + ]; + const dropdownOptions: Array> = [ + { label: 'Option A', value: 'a' }, + { label: 'Option B', value: 'b' }, + { label: 'Option C', value: 'c' }, + ]; + const header = ( +
+ +  Header title with icon +
+ ); + const body = ( +
+ + + + + {}} + style={{ width: '160px' }} + > + +
+
+ Wants to know more?  + + +  Click here! + +
+
+ ); + const footer = ( +
+ +   + +
+ ); + + return ( + + + + ); +}; + +HostingMultiElements.parameters = { + controls: { + hideNoControlsWarning: true, + exclude: ['title', 'content', 'footer', 'onClose', 'children'], + }, +}; +HostingMultiElements.args = { + placement: 'top', + closeButton: true, + theme: 'info', +}; + +export default meta; diff --git a/packages/grafana-ui/src/components/Toggletip/Toggletip.test.tsx b/packages/grafana-ui/src/components/Toggletip/Toggletip.test.tsx new file mode 100644 index 00000000000..bc54d8579bb --- /dev/null +++ b/packages/grafana-ui/src/components/Toggletip/Toggletip.test.tsx @@ -0,0 +1,90 @@ +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; + +import { Button } from '../Button'; + +import { Toggletip } from './Toggletip'; + +describe('Toggletip', () => { + it('should display toogletip after click on "Click me!" button', async () => { + render( + + + + ); + expect(screen.getByText('Click me!')).toBeInTheDocument(); + const button = screen.getByTestId('myButton'); + button.click(); + + await waitFor(() => expect(screen.getByTestId('toggletip-content')).toBeInTheDocument()); + }); + + it('should close toogletip after click on close button', async () => { + const closeSpy = jest.fn(); + render( + + + + ); + const button = screen.getByTestId('myButton'); + button.click(); + + await waitFor(() => expect(screen.getByTestId('toggletip-content')).toBeInTheDocument()); + + const closeButton = screen.getByTestId('toggletip-header-close'); + expect(closeButton).toBeInTheDocument(); + closeButton.click(); + + await waitFor(() => { + expect(closeSpy).toHaveBeenCalledTimes(1); + }); + }); + + it('should close toogletip after press ESC', async () => { + const closeSpy = jest.fn(); + render( + + + + ); + const button = screen.getByTestId('myButton'); + button.click(); + + await waitFor(() => expect(screen.getByTestId('toggletip-content')).toBeInTheDocument()); + + fireEvent.keyDown(global.document, { + code: 'Escape', + key: 'Escape', + keyCode: 27, + }); + + await waitFor(() => expect(closeSpy).toHaveBeenCalledTimes(1)); + }); + + it('should display the toogletip after press ENTER', async () => { + const closeSpy = jest.fn(); + render( + + + + ); + + expect(screen.queryByTestId('toggletip-content')).not.toBeInTheDocument(); + + // open toggletip with enter + const button = screen.getByTestId('myButton'); + button.focus(); + userEvent.keyboard('{enter}'); + + await waitFor(() => expect(screen.getByTestId('toggletip-content')).toBeInTheDocument()); + }); +}); diff --git a/packages/grafana-ui/src/components/Toggletip/Toggletip.tsx b/packages/grafana-ui/src/components/Toggletip/Toggletip.tsx new file mode 100644 index 00000000000..fd5c50de7d6 --- /dev/null +++ b/packages/grafana-ui/src/components/Toggletip/Toggletip.tsx @@ -0,0 +1,144 @@ +import { Placement } from '@popperjs/core'; +import React, { useCallback, useEffect, useRef } from 'react'; +import { usePopperTooltip } from 'react-popper-tooltip'; + +import { GrafanaTheme2 } from '@grafana/data'; + +import { useStyles2 } from '../../themes/ThemeContext'; +import { buildTooltipTheme } from '../../utils/tooltipUtils'; +import { IconButton } from '../IconButton/IconButton'; +import { Portal } from '../Portal/Portal'; + +import { ToggletipContent } from './types'; + +export interface ToggletipProps { + /** The theme used to display the toggletip */ + theme?: 'info' | 'error'; + /** The title to be displayed on the header */ + title?: JSX.Element | string; + /** determine whether to show or not the close button **/ + closeButton?: boolean; + /** Callback function to be called when the toggletip is closed */ + onClose?: Function; + /** The preferred placement of the toggletip */ + placement?: Placement; + /** The text or component that houses the content of the toggleltip */ + content: ToggletipContent; + /** The text or component to be displayed on the toggletip's bottom */ + footer?: JSX.Element | string; + /** The UI control users interact with to display toggletips */ + children: JSX.Element; +} + +export const Toggletip = React.memo( + ({ + children, + theme = 'info', + placement = 'auto', + content, + title, + closeButton = true, + onClose, + footer, + }: ToggletipProps) => { + const styles = useStyles2(getStyles); + const style = styles[theme]; + const contentRef = useRef(null); + const [controlledVisible, setControlledVisible] = React.useState(false); + + const closeToggletip = useCallback(() => { + setControlledVisible(false); + onClose?.(); + }, [onClose]); + + useEffect(() => { + if (controlledVisible) { + const handleKeyDown = (enterKey: KeyboardEvent) => { + if (enterKey.key === 'Escape') { + closeToggletip(); + } + }; + document.addEventListener('keydown', handleKeyDown); + return () => { + document.removeEventListener('keydown', handleKeyDown); + }; + } + return; + }, [controlledVisible, closeToggletip]); + + const { getArrowProps, getTooltipProps, setTooltipRef, setTriggerRef, visible, update } = usePopperTooltip({ + visible: controlledVisible, + placement: placement, + interactive: true, + offset: [0, 8], + trigger: 'click', + onVisibleChange: (value: boolean) => { + setControlledVisible(value); + if (!value) { + onClose?.(); + } + }, + }); + + return ( + <> + {React.cloneElement(children, { + ref: setTriggerRef, + tabIndex: 0, + })} + {visible && ( + +
+ {Boolean(title) &&
{title}
} + {closeButton && ( +
+ +
+ )} +
+
+ {(typeof content === 'string' || React.isValidElement(content)) && content} + {typeof content === 'function' && update && content({ update })} +
+ {Boolean(footer) &&
{footer}
} +
+ + )} + + ); + } +); + +Toggletip.displayName = 'Toggletip'; + +export const getStyles = (theme: GrafanaTheme2) => { + const info = buildTooltipTheme( + theme, + theme.components.tooltip.background, + theme.components.tooltip.background, + theme.components.tooltip.text, + { topBottom: 3, rightLeft: 3 } + ); + const error = buildTooltipTheme( + theme, + theme.colors.error.main, + theme.colors.error.main, + theme.colors.error.contrastText, + { topBottom: 3, rightLeft: 3 } + ); + + return { + info, + error, + }; +}; diff --git a/packages/grafana-ui/src/components/Toggletip/index.ts b/packages/grafana-ui/src/components/Toggletip/index.ts new file mode 100644 index 00000000000..f8dd556ecb7 --- /dev/null +++ b/packages/grafana-ui/src/components/Toggletip/index.ts @@ -0,0 +1,2 @@ +export { Toggletip, type ToggletipProps } from './Toggletip'; +export type { ToggletipContent, ToggletipContentProps } from './types'; diff --git a/packages/grafana-ui/src/components/Toggletip/types.ts b/packages/grafana-ui/src/components/Toggletip/types.ts new file mode 100644 index 00000000000..4f3332b0810 --- /dev/null +++ b/packages/grafana-ui/src/components/Toggletip/types.ts @@ -0,0 +1,9 @@ +/** + * This API allows popovers to update Popper's position when e.g. popover content changes + * update is delivered to content by react-popper. + */ +export interface ToggletipContentProps { + update?: () => void; +} + +export type ToggletipContent = string | React.ReactElement | ((props: ToggletipContentProps) => JSX.Element); diff --git a/packages/grafana-ui/src/components/Tooltip/Tooltip.tsx b/packages/grafana-ui/src/components/Tooltip/Tooltip.tsx index b5e3a78cab7..8ff8c6fb3b5 100644 --- a/packages/grafana-ui/src/components/Tooltip/Tooltip.tsx +++ b/packages/grafana-ui/src/components/Tooltip/Tooltip.tsx @@ -1,10 +1,10 @@ -import { css } from '@emotion/css'; import React, { useEffect } from 'react'; import { usePopperTooltip } from 'react-popper-tooltip'; -import { colorManipulator, GrafanaTheme2 } from '@grafana/data'; +import { GrafanaTheme2 } from '@grafana/data'; import { useStyles2 } from '../../themes/ThemeContext'; +import { buildTooltipTheme } from '../../utils/tooltipUtils'; import { Portal } from '../Portal/Portal'; import { PopoverContent, TooltipPlacement } from './types'; @@ -52,7 +52,7 @@ export const Tooltip = React.memo(({ children, theme, interactive, show, placeme }); const styles = useStyles2(getStyles); - const containerStyle = styles[theme ?? 'info']; + const style = styles[theme ?? 'info']; return ( <> @@ -62,8 +62,8 @@ export const Tooltip = React.memo(({ children, theme, interactive, show, placeme })} {visible && ( -
-
+
+
{typeof content === 'string' && content} {React.isValidElement(content) && React.cloneElement(content)} {typeof content === 'function' && @@ -80,154 +80,25 @@ export const Tooltip = React.memo(({ children, theme, interactive, show, placeme Tooltip.displayName = 'Tooltip'; -function getStyles(theme: GrafanaTheme2) { - function buildTooltipTheme(tooltipBg: string, tooltipBorder: string, tooltipText: string) { - return css` - background-color: ${tooltipBg}; - border-radius: 3px; - border: 1px solid ${tooltipBorder}; - box-shadow: ${theme.shadows.z2}; - color: ${tooltipText}; - font-size: ${theme.typography.bodySmall.fontSize}; - padding: ${theme.spacing(0.5, 1)}; - transition: opacity 0.3s; - z-index: ${theme.zIndex.tooltip}; - max-width: 400px; - overflow-wrap: break-word; - - &[data-popper-interactive='false'] { - pointer-events: none; - } - - .tooltip-arrow { - height: 1rem; - position: absolute; - width: 1rem; - pointer-events: none; - } - - .tooltip-arrow::before { - border-style: solid; - content: ''; - display: block; - height: 0; - margin: auto; - width: 0; - } - - .tooltip-arrow::after { - border-style: solid; - content: ''; - display: block; - height: 0; - margin: auto; - position: absolute; - width: 0; - } - - &[data-popper-placement*='bottom'] .tooltip-arrow { - left: 0; - margin-top: -10px; - top: 0; - } - - &[data-popper-placement*='bottom'] .tooltip-arrow::before { - border-color: transparent transparent ${tooltipBorder} transparent; - border-width: 0 8px 7px 8px; - position: absolute; - top: -1px; - } - - &[data-popper-placement*='bottom'] .tooltip-arrow::after { - border-color: transparent transparent ${tooltipBg} transparent; - border-width: 0 8px 7px 8px; - } - - &[data-popper-placement*='top'] .tooltip-arrow { - bottom: 0; - left: 0; - margin-bottom: -11px; - } - - &[data-popper-placement*='top'] .tooltip-arrow::before { - border-color: ${tooltipBorder} transparent transparent transparent; - border-width: 7px 8px 0 7px; - position: absolute; - top: 1px; - } - - &[data-popper-placement*='top'] .tooltip-arrow::after { - border-color: ${tooltipBg} transparent transparent transparent; - border-width: 7px 8px 0 7px; - } - - &[data-popper-placement*='right'] .tooltip-arrow { - left: 0; - margin-left: -11px; - } - - &[data-popper-placement*='right'] .tooltip-arrow::before { - border-color: transparent ${tooltipBorder} transparent transparent; - border-width: 7px 6px 7px 0; - } - - &[data-popper-placement*='right'] .tooltip-arrow::after { - border-color: transparent ${tooltipBg} transparent transparent; - border-width: 6px 7px 7px 0; - left: 2px; - top: 1px; - } - - &[data-popper-placement*='left'] .tooltip-arrow { - margin-right: -10px; - right: 0; - } - - &[data-popper-placement*='left'] .tooltip-arrow::before { - border-color: transparent transparent transparent ${tooltipBorder}; - border-width: 7px 0px 6px 7px; - } - - &[data-popper-placement*='left'] .tooltip-arrow::after { - border-color: transparent transparent transparent ${tooltipBg}; - border-width: 6px 0 5px 5px; - left: 1px; - top: 1px; - } - - code { - border: none; - display: inline; - background: ${colorManipulator.darken(tooltipBg, 0.1)}; - color: ${tooltipText}; - } - - pre { - background: ${colorManipulator.darken(tooltipBg, 0.1)}; - color: ${tooltipText}; - } - - a { - color: ${tooltipText}; - text-decoration: underline; - } - - a:hover { - text-decoration: none; - } - `; - } - +export const getStyles = (theme: GrafanaTheme2) => { const info = buildTooltipTheme( + theme, theme.components.tooltip.background, theme.components.tooltip.background, - theme.components.tooltip.text + theme.components.tooltip.text, + { topBottom: 0.5, rightLeft: 1 } + ); + const error = buildTooltipTheme( + theme, + theme.colors.error.main, + theme.colors.error.main, + theme.colors.error.contrastText, + { topBottom: 0.5, rightLeft: 1 } ); - const error = buildTooltipTheme(theme.colors.error.main, theme.colors.error.main, theme.colors.error.contrastText); return { - info: info, + info, ['info-alt']: info, error, }; -} +}; diff --git a/packages/grafana-ui/src/utils/tooltipUtils.ts b/packages/grafana-ui/src/utils/tooltipUtils.ts new file mode 100644 index 00000000000..15a9f125858 --- /dev/null +++ b/packages/grafana-ui/src/utils/tooltipUtils.ts @@ -0,0 +1,167 @@ +import { css } from '@emotion/css'; + +import { colorManipulator, GrafanaTheme2 } from '@grafana/data'; + +export function buildTooltipTheme( + theme: GrafanaTheme2, + tooltipBg: string, + toggletipBorder: string, + tooltipText: string, + tooltipPadding: { topBottom: number; rightLeft: number } +) { + return { + arrow: css` + height: 1rem; + width: 1rem; + position: absolute; + pointer-events: none; + + &::before { + border-style: solid; + content: ''; + display: block; + height: 0; + margin: auto; + width: 0; + } + + &::after { + border-style: solid; + content: ''; + display: block; + height: 0; + margin: auto; + position: absolute; + width: 0; + } + `, + container: css` + background-color: ${tooltipBg}; + border-radius: 3px; + border: 1px solid ${toggletipBorder}; + box-shadow: ${theme.shadows.z2}; + color: ${tooltipText}; + font-size: ${theme.typography.bodySmall.fontSize}; + padding: ${theme.spacing(tooltipPadding.topBottom, tooltipPadding.rightLeft)}; + transition: opacity 0.3s; + z-index: ${theme.zIndex.tooltip}; + max-width: 400px; + overflow-wrap: break-word; + + &[data-popper-interactive='false'] { + pointer-events: none; + } + + &[data-popper-placement*='bottom'] > div[data-popper-arrow='true'] { + left: 0; + margin-top: -7px; + top: 0; + + &::before { + border-color: transparent transparent ${toggletipBorder} transparent; + border-width: 0 8px 7px 8px; + position: absolute; + top: -1px; + } + + &::after { + border-color: transparent transparent ${tooltipBg} transparent; + border-width: 0 8px 7px 8px; + } + } + + &[data-popper-placement*='top'] > div[data-popper-arrow='true'] { + bottom: 0; + left: 0; + margin-bottom: -14px; + + &::before { + border-color: ${toggletipBorder} transparent transparent transparent; + border-width: 7px 8px 0 7px; + position: absolute; + top: 1px; + } + + &::after { + border-color: ${tooltipBg} transparent transparent transparent; + border-width: 7px 8px 0 7px; + } + } + + &[data-popper-placement*='right'] > div[data-popper-arrow='true'] { + left: 0; + margin-left: -10px; + + &::before { + border-color: transparent ${toggletipBorder} transparent transparent; + border-width: 7px 6px 7px 0; + } + + &::after { + border-color: transparent ${tooltipBg} transparent transparent; + border-width: 6px 7px 7px 0; + left: 2px; + top: 1px; + } + } + + &[data-popper-placement*='left'] > div[data-popper-arrow='true'] { + margin-right: -11px; + right: 0; + + &::before { + border-color: transparent transparent transparent ${toggletipBorder}; + border-width: 7px 0 6px 7px; + } + + &::after { + border-color: transparent transparent transparent ${tooltipBg}; + border-width: 6px 0 5px 5px; + left: 1px; + top: 1px; + } + } + + code { + border: none; + display: inline; + background: ${colorManipulator.darken(tooltipBg, 0.1)}; + color: ${tooltipText}; + } + + pre { + background: ${colorManipulator.darken(tooltipBg, 0.1)}; + color: ${tooltipText}; + } + + a { + color: ${tooltipText}; + text-decoration: underline; + } + + a:hover { + text-decoration: none; + } + `, + headerClose: css` + color: ${theme.colors.text.secondary}; + position: absolute; + right: ${theme.spacing(1)}; + top: ${theme.spacing(1.5)}; + background-color: transparent; + border: 0; + `, + header: css` + padding-top: ${theme.spacing(1)}; + padding-bottom: ${theme.spacing(2)}; + `, + body: css` + padding-top: ${theme.spacing(1)}; + padding-bottom: ${theme.spacing(1)}; + `, + footer: css` + padding-top: ${theme.spacing(2)}; + padding-bottom: ${theme.spacing(1)}; + `, + }; +} From 5179a830efaafd0eb105ba510fef5b5a526058cc Mon Sep 17 00:00:00 2001 From: Konrad Lalik Date: Thu, 9 Mar 2023 16:24:32 +0100 Subject: [PATCH 119/288] Alerting: Add fuzzy search to alert list view (#63931) * Add basic fuzzy search * Add fuzzy search to rule name, group and namespace filters * Add tests * Apply sort order when filtering * Filter rules on Enter instead of onChange * Add minor rule stats performance improvements * Fix tests * Remove unused code, add ufuzzy inline docs * Use form submit to set query string, add debounce docs --- .betterer.results | 3 - .../alerting/unified/RuleList.test.tsx | 12 +- .../features/alerting/unified/RuleList.tsx | 6 +- .../unified/components/rules/RuleStats.tsx | 66 ++++++----- .../unified/components/rules/RulesFilter.tsx | 85 ++++++++------ .../hooks/useCombinedRuleNamespaces.ts | 74 ++++++------ .../unified/hooks/useFilteredRules.test.ts | 52 ++++++--- .../unified/hooks/useFilteredRules.ts | 110 +++++++++++------- 8 files changed, 245 insertions(+), 163 deletions(-) diff --git a/.betterer.results b/.betterer.results index 9ffb427a6de..e06e9ab5725 100644 --- a/.betterer.results +++ b/.betterer.results @@ -2688,9 +2688,6 @@ exports[`better eslint`] = { "public/app/features/alerting/unified/components/rules/RuleDetailsDataSources.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], - "public/app/features/alerting/unified/components/rules/RulesFilter.tsx:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], "public/app/features/alerting/unified/components/silences/SilencesEditor.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], diff --git a/public/app/features/alerting/unified/RuleList.test.tsx b/public/app/features/alerting/unified/RuleList.test.tsx index ca7bd53c184..3c9041b39f9 100644 --- a/public/app/features/alerting/unified/RuleList.test.tsx +++ b/public/app/features/alerting/unified/RuleList.test.tsx @@ -322,6 +322,9 @@ describe('RuleList', () => { const groups = await ui.ruleGroup.findAll(); expect(groups).toHaveLength(2); + + await waitFor(() => expect(groups[0]).toHaveTextContent(/firing|pending|normal/)); + expect(groups[0]).toHaveTextContent('1 firing'); expect(groups[1]).toHaveTextContent('1 firing'); expect(groups[1]).toHaveTextContent('1 pending'); @@ -489,11 +492,12 @@ describe('RuleList', () => { }); await renderRuleList(); + const groups = await ui.ruleGroup.findAll(); expect(groups).toHaveLength(2); const filterInput = ui.rulesFilterInput.get(); - await userEvent.type(filterInput, 'label:foo=bar'); + await userEvent.type(filterInput, 'label:foo=bar{Enter}'); // Input is debounced so wait for it to be visible await waitFor(() => expect(filterInput).toHaveValue('label:foo=bar')); @@ -512,17 +516,17 @@ describe('RuleList', () => { // Check for different label matchers await userEvent.clear(filterInput); - await userEvent.type(filterInput, 'label:foo!=bar label:foo!=baz'); + await userEvent.type(filterInput, 'label:foo!=bar label:foo!=baz{Enter}'); // Group doesn't contain matching labels await waitFor(() => expect(ui.ruleGroup.queryAll()).toHaveLength(1)); await waitFor(() => expect(ui.ruleGroup.get()).toHaveTextContent('group-2')); await userEvent.clear(filterInput); - await userEvent.type(filterInput, 'label:"foo=~b.+"'); + await userEvent.type(filterInput, 'label:"foo=~b.+"{Enter}'); await waitFor(() => expect(ui.ruleGroup.queryAll()).toHaveLength(2)); await userEvent.clear(filterInput); - await userEvent.type(filterInput, 'label:region=US'); + await userEvent.type(filterInput, 'label:region=US{Enter}'); await waitFor(() => expect(ui.ruleGroup.queryAll()).toHaveLength(1)); await waitFor(() => expect(ui.ruleGroup.get()).toHaveTextContent('group-2')); }); diff --git a/public/app/features/alerting/unified/RuleList.tsx b/public/app/features/alerting/unified/RuleList.tsx index feb0c0d79bf..ca3236c7be4 100644 --- a/public/app/features/alerting/unified/RuleList.tsx +++ b/public/app/features/alerting/unified/RuleList.tsx @@ -1,5 +1,5 @@ import { css } from '@emotion/css'; -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useLocation } from 'react-router-dom'; import { useAsyncFn, useInterval } from 'react-use'; @@ -42,6 +42,8 @@ const RuleList = withErrorBoundary( const location = useLocation(); const [expandAll, setExpandAll] = useState(false); + const onFilterCleared = useCallback(() => setExpandAll(false), []); + const [queryParams] = useQueryParams(); const { filterState, hasActiveFilters } = useRulesFilter(); @@ -90,7 +92,7 @@ const RuleList = withErrorBoundary( // We show separate indicators for Grafana-managed and Cloud rules - setExpandAll(false)} /> + {!hasNoAlertRulesCreatedYet && ( <>
diff --git a/public/app/features/alerting/unified/components/rules/RuleStats.tsx b/public/app/features/alerting/unified/components/rules/RuleStats.tsx index 0655643d5ca..7a104e44d93 100644 --- a/public/app/features/alerting/unified/components/rules/RuleStats.tsx +++ b/public/app/features/alerting/unified/components/rules/RuleStats.tsx @@ -1,5 +1,6 @@ import pluralize from 'pluralize'; -import React, { FC, Fragment, useMemo } from 'react'; +import React, { FC, Fragment, useState } from 'react'; +import { useDebounce } from 'react-use'; import { Stack } from '@grafana/experimental'; import { Badge } from '@grafana/ui'; @@ -26,39 +27,48 @@ const emptyStats = { export const RuleStats: FC = ({ group, namespaces, includeTotal }) => { const evaluationInterval = group?.interval; + const [calculated, setCalculated] = useState(emptyStats); - const calculated = useMemo(() => { - const stats = { ...emptyStats }; + // Performance optimization allowing reducing number of stats calculation + // The problem occurs when we load many data sources. + // Then redux store gets updated multiple times in a pretty short period, triggering calculating stats many times. + // debounce allows to skip calculations which results would be abandoned in milliseconds + useDebounce( + () => { + const stats = { ...emptyStats }; - const calcRule = (rule: CombinedRule) => { - if (rule.promRule && isAlertingRule(rule.promRule)) { - if (isGrafanaRulerRulePaused(rule)) { - stats.paused += 1; + const calcRule = (rule: CombinedRule) => { + if (rule.promRule && isAlertingRule(rule.promRule)) { + if (isGrafanaRulerRulePaused(rule)) { + stats.paused += 1; + } + stats[rule.promRule.state] += 1; } - stats[rule.promRule.state] += 1; - } - if (ruleHasError(rule)) { - stats.error += 1; - } - if ( - (rule.promRule && isRecordingRule(rule.promRule)) || - (rule.rulerRule && isRecordingRulerRule(rule.rulerRule)) - ) { - stats.recording += 1; - } - stats.total += 1; - }; + if (ruleHasError(rule)) { + stats.error += 1; + } + if ( + (rule.promRule && isRecordingRule(rule.promRule)) || + (rule.rulerRule && isRecordingRulerRule(rule.rulerRule)) + ) { + stats.recording += 1; + } + stats.total += 1; + }; - if (group) { - group.rules.forEach(calcRule); - } + if (group) { + group.rules.forEach(calcRule); + } - if (namespaces) { - namespaces.forEach((namespace) => namespace.groups.forEach((group) => group.rules.forEach(calcRule))); - } + if (namespaces) { + namespaces.forEach((namespace) => namespace.groups.forEach((group) => group.rules.forEach(calcRule))); + } - return stats; - }, [group, namespaces]); + setCalculated(stats); + }, + 400, + [group, namespaces] + ); const statsComponents: React.ReactNode[] = []; diff --git a/public/app/features/alerting/unified/components/rules/RulesFilter.tsx b/public/app/features/alerting/unified/components/rules/RulesFilter.tsx index 66b0d43f653..e2ec4b7103c 100644 --- a/public/app/features/alerting/unified/components/rules/RulesFilter.tsx +++ b/public/app/features/alerting/unified/components/rules/RulesFilter.tsx @@ -1,6 +1,6 @@ import { css } from '@emotion/css'; -import { debounce } from 'lodash'; -import React, { FormEvent, useState } from 'react'; +import React, { useRef, useState } from 'react'; +import { useForm } from 'react-hook-form'; import { DataSourceInstanceSettings, GrafanaTheme2, SelectableValue } from '@grafana/data'; import { Stack } from '@grafana/experimental'; @@ -54,22 +54,21 @@ interface RulesFilerProps { onFilterCleared?: () => void; } +const RuleStateOptions = Object.entries(PromAlertingRuleState).map(([key, value]) => ({ + label: alertStateToReadable(value), + value, +})); + const RulesFilter = ({ onFilterCleared = () => undefined }: RulesFilerProps) => { + const styles = useStyles2(getStyles); const [queryParams, setQueryParams] = useQueryParams(); + const { filterState, hasActiveFilters, searchQuery, setSearchQuery, updateFilters } = useRulesFilter(); // This key is used to force a rerender on the inputs when the filters are cleared const [filterKey, setFilterKey] = useState(Math.floor(Math.random() * 100)); const dataSourceKey = `dataSource-${filterKey}`; const queryStringKey = `queryString-${filterKey}`; - const { filterState, hasActiveFilters, searchQuery, setSearchQuery, updateFilters } = useRulesFilter(); - - const styles = useStyles2(getStyles); - const stateOptions = Object.entries(PromAlertingRuleState).map(([key, value]) => ({ - label: alertStateToReadable(value), - value, - })); - const handleDataSourceChange = (dataSourceValue: DataSourceInstanceSettings) => { updateFilters({ ...filterState, dataSourceName: dataSourceValue.name }); setFilterKey((key) => key + 1); @@ -80,11 +79,6 @@ const RulesFilter = ({ onFilterCleared = () => undefined }: RulesFilerProps) => setFilterKey((key) => key + 1); }; - const handleQueryStringChange = debounce((e: FormEvent) => { - const target = e.target as HTMLInputElement; - setSearchQuery(target.value); - }, 600); - const handleAlertStateChange = (value: PromAlertingRuleState) => { logInfo(LogMessages.clickingAlertStateFilters); updateFilters({ ...filterState, ruleState: value }); @@ -112,6 +106,10 @@ const RulesFilter = ({ onFilterCleared = () => undefined }: RulesFilerProps) => setTimeout(() => setFilterKey(filterKey + 1), 100); }; + const searchQueryRef = useRef(null); + const { handleSubmit, register } = useForm<{ searchQuery: string }>({ defaultValues: { searchQuery } }); + const { ref, ...rest } = register('searchQuery'); + const searchIcon = ; return (
@@ -130,7 +128,11 @@ const RulesFilter = ({ onFilterCleared = () => undefined }: RulesFilerProps) =>
- +
@@ -147,28 +149,39 @@ const RulesFilter = ({ onFilterCleared = () => undefined }: RulesFilerProps) => - - - Search - }> - - - - - } + onSubmit={handleSubmit((data) => { + setSearchQuery(data.searchQuery); + searchQueryRef.current?.blur(); + })} > - - + + + Search + }> + + + + + } + > + { + ref(e); + searchQueryRef.current = e; + }} + {...rest} + placeholder="Search" + data-testid="search-query-input" + /> + + +
- rulesSources - .map((rulesSource): CombinedRuleNamespace[] => { - const rulesSourceName = isCloudRulesSource(rulesSource) ? rulesSource.name : rulesSource; - const promRules = promRulesResponses[rulesSourceName]?.result; - const rulerRules = rulerRulesResponses[rulesSourceName]?.result; + return useMemo(() => { + return rulesSources + .map((rulesSource): CombinedRuleNamespace[] => { + const rulesSourceName = isCloudRulesSource(rulesSource) ? rulesSource.name : rulesSource; + const promRules = promRulesResponses[rulesSourceName]?.result; + const rulerRules = rulerRulesResponses[rulesSourceName]?.result; - const cached = cache.current[rulesSourceName]; - if (cached && cached.promRules === promRules && cached.rulerRules === rulerRules) { - return cached.result; - } - const namespaces: Record = {}; + const cached = cache.current[rulesSourceName]; + if (cached && cached.promRules === promRules && cached.rulerRules === rulerRules) { + return cached.result; + } + const namespaces: Record = {}; - // first get all the ruler rules in - Object.entries(rulerRules || {}).forEach(([namespaceName, groups]) => { - const namespace: CombinedRuleNamespace = { - rulesSource, - name: namespaceName, - groups: [], - }; - namespaces[namespaceName] = namespace; - addRulerGroupsToCombinedNamespace(namespace, groups); + // first get all the ruler rules in + Object.entries(rulerRules || {}).forEach(([namespaceName, groups]) => { + const namespace: CombinedRuleNamespace = { + rulesSource, + name: namespaceName, + groups: [], + }; + namespaces[namespaceName] = namespace; + addRulerGroupsToCombinedNamespace(namespace, groups); + }); + + // then correlate with prometheus rules + promRules?.forEach(({ name: namespaceName, groups }) => { + const ns = (namespaces[namespaceName] = namespaces[namespaceName] || { + rulesSource, + name: namespaceName, + groups: [], }); - // then correlate with prometheus rules - promRules?.forEach(({ name: namespaceName, groups }) => { - const ns = (namespaces[namespaceName] = namespaces[namespaceName] || { - rulesSource, - name: namespaceName, - groups: [], - }); + addPromGroupsToCombinedNamespace(ns, groups); + }); - addPromGroupsToCombinedNamespace(ns, groups); - }); + const result = Object.values(namespaces); - const result = Object.values(namespaces); - - cache.current[rulesSourceName] = { promRules, rulerRules, result }; - return result; - }) - .flat(), - [promRulesResponses, rulerRulesResponses, rulesSources] - ); + cache.current[rulesSourceName] = { promRules, rulerRules, result }; + return result; + }) + .flat(); + }, [promRulesResponses, rulerRulesResponses, rulesSources]); } // merge all groups in case of grafana managed, essentially treating namespaces (folders) as groups diff --git a/public/app/features/alerting/unified/hooks/useFilteredRules.test.ts b/public/app/features/alerting/unified/hooks/useFilteredRules.test.ts index a809e148a61..2bda8046d5b 100644 --- a/public/app/features/alerting/unified/hooks/useFilteredRules.test.ts +++ b/public/app/features/alerting/unified/hooks/useFilteredRules.test.ts @@ -26,32 +26,37 @@ beforeAll(() => { }); describe('filterRules', function () { - it('should filter out rules by name filter', function () { + // Typos there are deliberate to test the fuzzy search + it.each(['cpu', 'hi usage', 'usge'])('should filter out rules by name filter = "%s"', function (nameFilter) { const rules = [mockCombinedRule({ name: 'High CPU usage' }), mockCombinedRule({ name: 'Memory too low' })]; const ns = mockCombinedRuleNamespace({ groups: [mockCombinedRuleGroup('Resources usage group', rules)], }); - const filtered = filterRules([ns], getFilter({ ruleName: 'cpu' })); + const filtered = filterRules([ns], getFilter({ ruleName: nameFilter })); expect(filtered[0].groups[0].rules).toHaveLength(1); expect(filtered[0].groups[0].rules[0].name).toBe('High CPU usage'); }); - it('should filter out rules by evaluation group name', function () { - const ns = mockCombinedRuleNamespace({ - groups: [ - mockCombinedRuleGroup('Performance group', [mockCombinedRule({ name: 'High CPU usage' })]), - mockCombinedRuleGroup('Availability group', [mockCombinedRule({ name: 'Memory too low' })]), - ], - }); + // Typos there are deliberate to test the fuzzy search + it.each(['availability', 'avialability', 'avail group'])( + 'should filter out rules by evaluation group name = "%s"', + function (groupFilter) { + const ns = mockCombinedRuleNamespace({ + groups: [ + mockCombinedRuleGroup('Performance group', [mockCombinedRule({ name: 'High CPU usage' })]), + mockCombinedRuleGroup('Availability group', [mockCombinedRule({ name: 'Memory too low' })]), + ], + }); - const filtered = filterRules([ns], getFilter({ groupName: 'availability' })); + const filtered = filterRules([ns], getFilter({ groupName: groupFilter })); - expect(filtered[0].groups).toHaveLength(1); - expect(filtered[0].groups[0].rules[0].name).toBe('Memory too low'); - }); + expect(filtered[0].groups).toHaveLength(1); + expect(filtered[0].groups[0].rules[0].name).toBe('Memory too low'); + } + ); it('should filter out rules by label filter', function () { const rules = [ @@ -160,4 +165,25 @@ describe('filterRules', function () { expect(filtered[0].groups[0].rules).toHaveLength(1); expect(filtered[0].groups[0].rules[0].name).toBe('Memory too low'); }); + + // Typos there are deliberate to test the fuzzy search + it.each(['nasa', 'alrt rul', 'nasa ruls'])('should filter out rules by namespace = "%s"', (namespaceFilter) => { + const cpuRule = mockCombinedRule({ name: 'High CPU usage' }); + const memoryRule = mockCombinedRule({ name: 'Memory too low' }); + + const teamEmeaNs = mockCombinedRuleNamespace({ + name: 'EMEA Alerting', + groups: [mockCombinedRuleGroup('CPU group', [cpuRule])], + }); + + const teamNasaNs = mockCombinedRuleNamespace({ + name: 'NASA Alert Rules', + groups: [mockCombinedRuleGroup('Memory group', [memoryRule])], + }); + + const filtered = filterRules([teamEmeaNs, teamNasaNs], getFilter({ namespace: namespaceFilter })); + + expect(filtered[0].groups[0].rules).toHaveLength(1); + expect(filtered[0].groups[0].rules[0].name).toBe('Memory too low'); + }); }); diff --git a/public/app/features/alerting/unified/hooks/useFilteredRules.ts b/public/app/features/alerting/unified/hooks/useFilteredRules.ts index 80666cb91ed..f7b259706a6 100644 --- a/public/app/features/alerting/unified/hooks/useFilteredRules.ts +++ b/public/app/features/alerting/unified/hooks/useFilteredRules.ts @@ -1,3 +1,4 @@ +import uFuzzy from '@leeoniya/ufuzzy'; import produce from 'immer'; import { compact, isEmpty } from 'lodash'; import { useCallback, useEffect, useMemo } from 'react'; @@ -18,8 +19,8 @@ export function useRulesFilter() { const [queryParams, updateQueryParams] = useURLSearchParams(); const searchQuery = queryParams.get('search') ?? ''; - const filterState = getSearchFilterFromQuery(searchQuery); - const hasActiveFilters = Object.values(filterState).some((filter) => !isEmpty(filter)); + const filterState = useMemo(() => getSearchFilterFromQuery(searchQuery), [searchQuery]); + const hasActiveFilters = useMemo(() => Object.values(filterState).some((filter) => !isEmpty(filter)), [filterState]); const updateFilters = useCallback( (newFilter: RulesFilter) => { @@ -76,37 +77,67 @@ export const useFilteredRules = (namespaces: CombinedRuleNamespace[], filterStat return useMemo(() => filterRules(namespaces, filterState), [namespaces, filterState]); }; +// Options details can be found here https://github.com/leeoniya/uFuzzy#options +// The following configuration complies with Damerau-Levenshtein distance +// https://en.wikipedia.org/wiki/Damerau%E2%80%93Levenshtein_distance +const ufuzzy = new uFuzzy({ + intraMode: 1, + intraIns: 1, + intraSub: 1, + intraTrn: 1, + intraDel: 1, +}); + export const filterRules = ( namespaces: CombinedRuleNamespace[], filterState: RulesFilter = { labels: [], freeFormWords: [] } ): CombinedRuleNamespace[] => { - return ( - namespaces - .filter((ns) => - filterState.namespace ? ns.name.toLowerCase().includes(filterState.namespace.toLowerCase()) : true - ) - .filter(({ rulesSource }) => - filterState.dataSourceName && isCloudRulesSource(rulesSource) - ? rulesSource.name === filterState.dataSourceName - : true - ) - // If a namespace and group have rules that match the rules filters then keep them. - .reduce(reduceNamespaces(filterState), [] as CombinedRuleNamespace[]) - ); + let filteredNamespaces = namespaces; + + const dataSourceFilter = filterState.dataSourceName; + if (dataSourceFilter) { + filteredNamespaces = filteredNamespaces.filter(({ rulesSource }) => + isCloudRulesSource(rulesSource) ? rulesSource.name === dataSourceFilter : true + ); + } + + const namespaceFilter = filterState.namespace; + if (namespaceFilter) { + const namespaceHaystack = filteredNamespaces.map((ns) => ns.name); + + const [idxs, info, order] = ufuzzy.search(namespaceHaystack, namespaceFilter); + if (info && order) { + filteredNamespaces = order.map((idx) => filteredNamespaces[info.idx[idx]]); + } else { + filteredNamespaces = idxs.map((idx) => filteredNamespaces[idx]); + } + } + + // If a namespace and group have rules that match the rules filters then keep them. + return filteredNamespaces.reduce(reduceNamespaces(filterState), [] as CombinedRuleNamespace[]); }; -const reduceNamespaces = (filterStateFilters: RulesFilter) => { +const reduceNamespaces = (filterState: RulesFilter) => { return (namespaceAcc: CombinedRuleNamespace[], namespace: CombinedRuleNamespace) => { - const groups = namespace.groups - .filter((g) => - filterStateFilters.groupName ? g.name.toLowerCase().includes(filterStateFilters.groupName.toLowerCase()) : true - ) - .reduce(reduceGroups(filterStateFilters), [] as CombinedRuleGroup[]); + const groupNameFilter = filterState.groupName; + let filteredGroups = namespace.groups; - if (groups.length) { + if (groupNameFilter) { + const groupsHaystack = filteredGroups.map((g) => g.name); + const [idxs, info, order] = ufuzzy.search(groupsHaystack, groupNameFilter); + if (info && order) { + filteredGroups = order.map((idx) => filteredGroups[info.idx[idx]]); + } else { + filteredGroups = idxs.map((idx) => filteredGroups[idx]); + } + } + + filteredGroups = filteredGroups.reduce(reduceGroups(filterState), [] as CombinedRuleGroup[]); + + if (filteredGroups.length) { namespaceAcc.push({ ...namespace, - groups, + groups: filteredGroups, }); } @@ -116,8 +147,22 @@ const reduceNamespaces = (filterStateFilters: RulesFilter) => { // Reduces groups to only groups that have rules matching the filters const reduceGroups = (filterState: RulesFilter) => { + const ruleNameQuery = filterState.ruleName ?? filterState.freeFormWords.join(' '); + return (groupAcc: CombinedRuleGroup[], group: CombinedRuleGroup) => { - const rules = group.rules.filter((rule) => { + let filteredRules = group.rules; + + if (ruleNameQuery) { + const rulesHaystack = filteredRules.map((r) => r.name); + const [idxs, info, order] = ufuzzy.search(rulesHaystack, ruleNameQuery); + if (info && order) { + filteredRules = order.map((idx) => filteredRules[info.idx[idx]]); + } else { + filteredRules = idxs.map((idx) => filteredRules[idx]); + } + } + + filteredRules = filteredRules.filter((rule) => { if (filterState.ruleType && filterState.ruleType !== rule.promRule?.type) { return false; } @@ -127,19 +172,6 @@ const reduceGroups = (filterState: RulesFilter) => { return false; } - const ruleNameLc = rule.name?.toLocaleLowerCase(); - // Free Form Query is used to filter by rule name - if ( - filterState.freeFormWords.length > 0 && - !filterState.freeFormWords.every((w) => ruleNameLc.includes(w.toLocaleLowerCase())) - ) { - return false; - } - - if (filterState.ruleName && !rule.name?.toLocaleLowerCase().includes(filterState.ruleName.toLocaleLowerCase())) { - return false; - } - if (filterState.ruleHealth && rule.promRule) { const ruleHealth = getRuleHealth(rule.promRule.health); return filterState.ruleHealth === ruleHealth; @@ -171,10 +203,10 @@ const reduceGroups = (filterState: RulesFilter) => { return true; }); // Add rules to the group that match the rule list filters - if (rules.length) { + if (filteredRules.length) { groupAcc.push({ ...group, - rules, + rules: filteredRules, }); } return groupAcc; From 6f67529a0aa6d354578a7908d11418a8d6aec5dc Mon Sep 17 00:00:00 2001 From: Tania Date: Thu, 9 Mar 2023 16:47:50 +0100 Subject: [PATCH 120/288] Docs: Add a note on uploading report branding logos (#64532) * Docs: Add a note on uploading report branding logos * Fix wording --- docs/sources/dashboards/create-reports/index.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/sources/dashboards/create-reports/index.md b/docs/sources/dashboards/create-reports/index.md index 7def573355e..3d11ae5e0cc 100644 --- a/docs/sources/dashboards/create-reports/index.md +++ b/docs/sources/dashboards/create-reports/index.md @@ -236,15 +236,17 @@ You can customize the branding options. Report branding: -- **Company logo URL:** Company logo displayed in the report PDF. Defaults to the Grafana logo. +- **Company logo:** Company logo displayed in the report PDF. It can be configured by specifying a URL, or by uploading a file. Defaults to the Grafana logo. Email branding: -- **Company logo URL:** Company logo displayed in the report PDF. Defaults to the Grafana logo. +- **Company logo:** Company logo displayed in the report email. It can be configured by specifying a URL, or by uploading a file. Defaults to the Grafana logo. - **Email footer:** Toggle to enable the report email footer. Select **Sent by** or **None**. - **Footer link text:** Text of the link in the report email footer. Defaults to `Grafana`. - **Footer link URL:** Link of the report email footer. +> Note: Currently, the API does not allow for the simultaneous upload of files with identical names for both the email logo and report logo. You can still upload the same file for each logo separately in two distinct steps. + ## Troubleshoot reporting To troubleshoot and get more log information, enable debug logging in the configuration file. Refer to [Configuration]({{< relref "../../setup-grafana/configure-grafana/#filters" >}}) for more information. From 424e33146c3010b09885fd74f8cc40d4f64b0baa Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Thu, 9 Mar 2023 17:20:36 +0100 Subject: [PATCH 121/288] Elasticsearch: Reintroduce log tests from frontend (#64521) * Elasticsearch: Reintroduce log tests from frontend * Fix linting --- pkg/tsdb/elasticsearch/response_parser.go | 2 +- .../response_parser_frontend_test.go | 135 ++++++++---------- 2 files changed, 60 insertions(+), 77 deletions(-) diff --git a/pkg/tsdb/elasticsearch/response_parser.go b/pkg/tsdb/elasticsearch/response_parser.go index 7d279cff764..352d623ac47 100644 --- a/pkg/tsdb/elasticsearch/response_parser.go +++ b/pkg/tsdb/elasticsearch/response_parser.go @@ -150,7 +150,7 @@ func processLogsResponse(res *es.SearchResponse, target *Query, configuredFields frames := data.Frames{} frame := data.NewFrame("", fields...) - setPreferredVisType(frame, "logs") + setPreferredVisType(frame, data.VisTypeLogs) setSearchWords(frame, searchWords) frames = append(frames, frame) diff --git a/pkg/tsdb/elasticsearch/response_parser_frontend_test.go b/pkg/tsdb/elasticsearch/response_parser_frontend_test.go index 9ce5311bf3f..13f9f0f0ce8 100644 --- a/pkg/tsdb/elasticsearch/response_parser_frontend_test.go +++ b/pkg/tsdb/elasticsearch/response_parser_frontend_test.go @@ -1345,7 +1345,7 @@ func TestTwoBucketScripts(t *testing.T) { requireFloatAt(t, 48.0, fields[4], 1) } -func TestLogsAndCount(t *testing.T) { +func TestLogs(t *testing.T) { query := []byte(` [ { @@ -1376,7 +1376,7 @@ func TestLogsAndCount(t *testing.T) { "_type": "_doc", "_index": "mock-index", "_source": { - "@timestamp": "2019-06-24T09:51:19.765Z", + "testtime": "2019-06-24T09:51:19.765Z", "host": "djisaodjsoad", "number": 1, "line": "hello, i am a message", @@ -1394,7 +1394,7 @@ func TestLogsAndCount(t *testing.T) { "_type": "_doc", "_index": "mock-index", "_source": { - "@timestamp": "2019-06-24T09:52:19.765Z", + "testtime": "2019-06-24T09:52:19.765Z", "host": "dsalkdakdop", "number": 2, "line": "hello, i am also message", @@ -1415,92 +1415,75 @@ func TestLogsAndCount(t *testing.T) { `) t.Run("response", func(t *testing.T) { - // FIXME: config datasource with messageField=, levelField= result, err := queryDataTest(query, response) require.NoError(t, err) require.Len(t, result.response.Responses, 1) frames := result.response.Responses["A"].Frames - // require.Len(t, frames, 2) // FIXME + require.Len(t, frames, 1) - // logsFrame := frames[0] + logsFrame := frames[0] - // m := logsFrame.Meta - // require.Equal(t, "['hello', 'message']", m.SearchWords) // FIXME - // require.Equal(t, data.VisTypeLogs, m.PreferredVisualization) // FIXME + meta := logsFrame.Meta + require.Equal(t, map[string]interface{}{"searchWords": []string{"hello", "message"}}, meta.Custom) + require.Equal(t, data.VisTypeLogs, string(meta.PreferredVisualization)) - // logsFieldMap := make(map[string]*data.Field) - // for _, field := range logsFrame.Fields { - // logsFieldMap[field.Name] = field - // } - - // require.Contains(t, logsFieldMap, "@timestamp") - // require.Equal(t, data.FieldTypeTime, logsFieldMap["@timestamp"].Type()) - - // require.Contains(t, logsFieldMap, "host") - // require.Equal(t, data.FieldTypeString, logsFieldMap["host"].Type()) - - // require.Contains(t, logsFieldMap, "message") - // require.Equal(t, data.FieldTypeString, logsFieldMap["message"].Type()) - - // require.Contains(t, logsFieldMap, "number") - // require.Equal(t, data.FieldTypeNullableFloat64, logsFieldMap["number"].Type()) - - // requireStringAt(t, "fdsfs", logsFieldMap["_id"], 0) - // requireStringAt(t, "kdospaidopa", logsFieldMap["_id"], 1) - // requireStringAt(t, "_doc", logsFieldMap["_type"], 0) - // requireStringAt(t, "_doc", logsFieldMap["_type"], 1) - // requireStringAt(t, "mock-index", logsFieldMap["_index"], 0) - // requireStringAt(t, "mock-index", logsFieldMap["_index"], 1) - - // actualJson1 := logsFieldMap["_source"].At(0).(*json.RawMessage) - // actualJson2 := logsFieldMap["_source"].At(1).(*json.RawMessage) - - // expectedJson1 := []byte(` - // { - // "@timestamp": "2019-06-24T09:51:19.765Z", - // "host": "djisaodjsoad", - // "number": 1, - // "message": "hello, i am a message", - // "level": "debug", - // "fields.lvl": "debug" - // } - // `) - - // expectedJson2 := []byte(` - // { - // "@timestamp": "2019-06-24T09:52:19.765Z", - // "host": "dsalkdakdop", - // "number": 2, - // "message": "hello, i am also message", - // "level": "error", - // "fields.lvl": "info" - // } - // `) - - // require.Equal(t, expectedJson1, actualJson1) - // require.Equal(t, expectedJson2, actualJson2) - - histogramFrame := frames[len(frames)-1] // the "last" frame - - histFieldMap := make(map[string]*data.Field) - for _, field := range histogramFrame.Fields { - histFieldMap[field.Name] = field + logsFieldMap := make(map[string]*data.Field) + for _, field := range logsFrame.Fields { + logsFieldMap[field.Name] = field } - // FIXME: the go-version uses lowercase-names, `time` and `value` - // t1 := histFieldMap["Time"].At(0).(time.Time) - // t2 := histFieldMap["Time"].At(1).(time.Time) + require.Contains(t, logsFieldMap, "testtime") + require.Equal(t, data.FieldTypeNullableTime, logsFieldMap["testtime"].Type()) - // v1 := histFieldMap["Value"].At(0).(*float64) - // v2 := histFieldMap["Value"].At(1).(*float64) + require.Contains(t, logsFieldMap, "host") + require.Equal(t, data.FieldTypeNullableString, logsFieldMap["host"].Type()) - // testData := make(map[int64]float64) - // testData[(t1).UnixMilli()] = *v1 - // testData[(t2).UnixMilli()] = *v2 + require.Contains(t, logsFieldMap, "line") + require.Equal(t, data.FieldTypeNullableString, logsFieldMap["line"].Type()) - // require.Equal(t, 10.0, testData[1000]) - // require.Equal(t, 15.0, testData[2000]) + require.Contains(t, logsFieldMap, "number") + require.Equal(t, data.FieldTypeNullableFloat64, logsFieldMap["number"].Type()) + + require.Contains(t, logsFieldMap, "_source") + require.Equal(t, data.FieldTypeNullableJSON, logsFieldMap["_source"].Type()) + + requireStringAt(t, "fdsfs", logsFieldMap["_id"], 0) + requireStringAt(t, "kdospaidopa", logsFieldMap["_id"], 1) + requireStringAt(t, "_doc", logsFieldMap["_type"], 0) + requireStringAt(t, "_doc", logsFieldMap["_type"], 1) + requireStringAt(t, "mock-index", logsFieldMap["_index"], 0) + requireStringAt(t, "mock-index", logsFieldMap["_index"], 1) + + actualJson1, err := json.Marshal(logsFieldMap["_source"].At(0).(*json.RawMessage)) + require.NoError(t, err) + actualJson2, err := json.Marshal(logsFieldMap["_source"].At(1).(*json.RawMessage)) + require.NoError(t, err) + + expectedJson1 := ` + { + "fields.lvl": "debug", + "host": "djisaodjsoad", + "level": "debug", + "line": "hello, i am a message", + "number": 1, + "testtime": "2019-06-24T09:51:19.765Z", + "line": "hello, i am a message" + } + ` + + expectedJson2 := ` + { + "testtime": "2019-06-24T09:52:19.765Z", + "host": "dsalkdakdop", + "number": 2, + "line": "hello, i am also message", + "level": "error", + "fields.lvl": "info" + }` + + require.JSONEq(t, expectedJson1, string(actualJson1)) + require.JSONEq(t, expectedJson2, string(actualJson2)) }) t.Run("level field", func(t *testing.T) { From cd6d6d1daf64d8afe53074b35bc4bb1ac7ba9418 Mon Sep 17 00:00:00 2001 From: Gilles De Mey Date: Thu, 9 Mar 2023 17:24:47 +0100 Subject: [PATCH 122/288] Deps: bump react-enable to v3.1 (#64501) --- package.json | 2 +- yarn.lock | 24 ++++++++++++------------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index 779c727facc..448494bac54 100644 --- a/package.json +++ b/package.json @@ -374,7 +374,7 @@ "react-dom": "17.0.2", "react-draggable": "4.4.5", "react-dropzone": "^14.2.3", - "react-enable": "^3.0.1", + "react-enable": "^3.1.0", "react-grid-layout": "1.3.4", "react-highlight-words": "0.20.0", "react-hook-form": "7.5.3", diff --git a/yarn.lock b/yarn.lock index d3103eb224a..97eb18e03a8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13195,7 +13195,7 @@ __metadata: languageName: node linkType: hard -"@xstate/react@npm:^3.0.0": +"@xstate/react@npm:^3.2.1": version: 3.2.1 resolution: "@xstate/react@npm:3.2.1" dependencies: @@ -22356,7 +22356,7 @@ __metadata: react-dom: 17.0.2 react-draggable: 4.4.5 react-dropzone: ^14.2.3 - react-enable: ^3.0.1 + react-enable: ^3.1.0 react-grid-layout: 1.3.4 react-highlight-words: 0.20.0 react-hook-form: 7.5.3 @@ -33019,18 +33019,18 @@ __metadata: languageName: node linkType: hard -"react-enable@npm:^3.0.1": - version: 3.0.1 - resolution: "react-enable@npm:3.0.1" +"react-enable@npm:^3.1.0": + version: 3.1.0 + resolution: "react-enable@npm:3.1.0" dependencies: "@headlessui/react": ^1.5.0 - "@xstate/react": ^3.0.0 + "@xstate/react": ^3.2.1 tslib: ^1.14.1 - xstate: ^4.31.0 + xstate: ^4.37.0 peerDependencies: react: ^17 || ^18 react-dom: ^17 || ^18 - checksum: 1426b2433785eab34054b82594c085772a8ce334e71ca8d49d4171cd3f37dc8d5273e4717f1c70efb1411ef89220a4bba156a9fae5c6eb2a9a80c178307567f4 + checksum: fc6509bac91bff78529df3699799bb02ce6d5fcc594eea844779834fa8cd8827f328c4456c157e2aca6bebbd0969846e184625f2fa3e2edde97074e19e10d5e0 languageName: node linkType: hard @@ -40069,10 +40069,10 @@ __metadata: languageName: node linkType: hard -"xstate@npm:^4.31.0": - version: 4.36.0 - resolution: "xstate@npm:4.36.0" - checksum: c8c4c7bb02b0a1f402dc967ce29489551457f0bb5e021328b4cddf34d7eb6b66ad5c131003d33808f5a9ef2cbd2f6ae31ac5904314ee5e6adc7616589b746310 +"xstate@npm:^4.37.0": + version: 4.37.0 + resolution: "xstate@npm:4.37.0" + checksum: 8eba107721c91ba08934b68a2881f01dd9ab6f23cc2ebcdd91145ce5999db8f690b38cf1570b928c058755150fc5024bed1cafe731ff7e6750d4e64752a7ab5b languageName: node linkType: hard From 3336327306606950192f93e512c9e4ef77a0961e Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Thu, 9 Mar 2023 16:42:45 +0000 Subject: [PATCH 123/288] Navigation: Fix Home logo always going to `/login` (#62658) * only redirect to /login when anonymous access is disabled * only search for dashboards when not logged in if anon access is enabled * fix go logic * add unit tests --- packages/grafana-data/src/types/config.ts | 1 + packages/grafana-runtime/src/config.ts | 1 + pkg/api/dtos/frontend_settings.go | 1 + pkg/api/frontendsettings.go | 1 + pkg/services/navtree/navtreeimpl/navtree.go | 10 +- .../components/AppChrome/TopSearchBar.tsx | 12 +- public/app/core/components/NavBar/NavBar.tsx | 9 +- .../actions/dashboardActions.test.ts | 168 ++++++++++++++++++ .../actions/dashboardActions.ts | 8 +- 9 files changed, 203 insertions(+), 8 deletions(-) create mode 100644 public/app/features/commandPalette/actions/dashboardActions.test.ts diff --git a/packages/grafana-data/src/types/config.ts b/packages/grafana-data/src/types/config.ts index 73112ebff8e..07c8186f92d 100644 --- a/packages/grafana-data/src/types/config.ts +++ b/packages/grafana-data/src/types/config.ts @@ -200,6 +200,7 @@ export interface GrafanaConfig { /** @deprecated Use `theme2` instead. */ theme: GrafanaTheme; theme2: GrafanaTheme2; + anonymousEnabled: boolean; featureToggles: FeatureToggles; licenseInfo: LicenseInfo; http2Enabled: boolean; diff --git a/packages/grafana-runtime/src/config.ts b/packages/grafana-runtime/src/config.ts index 019cf255d59..50706bef145 100644 --- a/packages/grafana-runtime/src/config.ts +++ b/packages/grafana-runtime/src/config.ts @@ -85,6 +85,7 @@ export class GrafanaBootConfig implements GrafanaConfig { theme: GrafanaTheme; theme2: GrafanaTheme2; featureToggles: FeatureToggles = {}; + anonymousEnabled = false; licenseInfo: LicenseInfo = {} as LicenseInfo; rendererAvailable = false; dashboardPreviews: { diff --git a/pkg/api/dtos/frontend_settings.go b/pkg/api/dtos/frontend_settings.go index e54cc01accd..c956ae87069 100644 --- a/pkg/api/dtos/frontend_settings.go +++ b/pkg/api/dtos/frontend_settings.go @@ -181,6 +181,7 @@ type FrontendSettingsDTO struct { LicenseInfo FrontendSettingsLicenseInfoDTO `json:"licenseInfo"` FeatureToggles map[string]bool `json:"featureToggles"` + AnonymousEnabled bool `json:"anonymousEnabled"` RendererAvailable bool `json:"rendererAvailable"` RendererVersion string `json:"rendererVersion"` SecretsManagerPluginEnabled bool `json:"secretsManagerPluginEnabled"` diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index 6a83bdc227d..d93715b483a 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -174,6 +174,7 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro }, FeatureToggles: hs.Features.GetEnabled(c.Req.Context()), + AnonymousEnabled: hs.Cfg.AnonymousEnabled, RendererAvailable: hs.RenderService.IsAvailable(c.Req.Context()), RendererVersion: hs.RenderService.Version(), SecretsManagerPluginEnabled: secretsManagerPluginEnabled, diff --git a/pkg/services/navtree/navtreeimpl/navtree.go b/pkg/services/navtree/navtreeimpl/navtree.go index f65b1fc950d..5216bed869f 100644 --- a/pkg/services/navtree/navtreeimpl/navtree.go +++ b/pkg/services/navtree/navtreeimpl/navtree.go @@ -214,10 +214,14 @@ func (s *ServiceImpl) GetNavTree(c *contextmodel.ReqContext, hasEditPerm bool, p func (s *ServiceImpl) getHomeNode(c *contextmodel.ReqContext, prefs *pref.Preference) *navtree.NavLink { homeUrl := s.cfg.AppSubURL + "/" - homePage := s.cfg.HomePage + if !c.IsSignedIn && !s.cfg.AnonymousEnabled { + homeUrl = s.cfg.AppSubURL + "/login" + } else { + homePage := s.cfg.HomePage - if prefs.HomeDashboardID == 0 && len(homePage) > 0 { - homeUrl = homePage + if prefs.HomeDashboardID == 0 && len(homePage) > 0 { + homeUrl = homePage + } } homeNode := &navtree.NavLink{ diff --git a/public/app/core/components/AppChrome/TopSearchBar.tsx b/public/app/core/components/AppChrome/TopSearchBar.tsx index 068199fae71..e1708f07f64 100644 --- a/public/app/core/components/AppChrome/TopSearchBar.tsx +++ b/public/app/core/components/AppChrome/TopSearchBar.tsx @@ -1,8 +1,10 @@ import { css } from '@emotion/css'; import React from 'react'; +import { useLocation } from 'react-router-dom'; -import { GrafanaTheme2 } from '@grafana/data'; +import { GrafanaTheme2, locationUtil } from '@grafana/data'; import { Dropdown, ToolbarButton, useStyles2 } from '@grafana/ui'; +import { config } from 'app/core/config'; import { contextSrv } from 'app/core/core'; import { useSelector } from 'app/types'; @@ -20,14 +22,20 @@ import { TOP_BAR_LEVEL_HEIGHT } from './types'; export function TopSearchBar() { const styles = useStyles2(getStyles); const navIndex = useSelector((state) => state.navIndex); + const location = useLocation(); const helpNode = navIndex['help']; const profileNode = navIndex['profile']; + let homeUrl = config.appSubUrl || '/'; + if (!config.bootData.user.isSignedIn && !config.anonymousEnabled) { + homeUrl = locationUtil.getUrlForPartial(location, { forceLogin: 'true' }); + } + return (
- + diff --git a/public/app/core/components/NavBar/NavBar.tsx b/public/app/core/components/NavBar/NavBar.tsx index 32dcd416788..f9019684061 100644 --- a/public/app/core/components/NavBar/NavBar.tsx +++ b/public/app/core/components/NavBar/NavBar.tsx @@ -5,7 +5,7 @@ import { cloneDeep } from 'lodash'; import React, { useState } from 'react'; import { useLocation } from 'react-router-dom'; -import { GrafanaTheme2, NavModelItem, NavSection } from '@grafana/data'; +import { GrafanaTheme2, locationUtil, NavModelItem, NavSection } from '@grafana/data'; import { config, locationSearchToObject, locationService, reportInteraction } from '@grafana/runtime'; import { useTheme2, CustomScrollbar, IconButton } from '@grafana/ui'; import { getKioskMode } from 'app/core/navigation/kiosk'; @@ -51,11 +51,16 @@ export const NavBar = React.memo(() => { menuOpen ); + let homeUrl = config.appSubUrl || '/'; + if (!config.bootData.user.isSignedIn && !config.anonymousEnabled) { + homeUrl = locationUtil.getUrlForPartial(location, { forceLogin: 'true' }); + } + const homeItem: NavModelItem = enrichWithInteractionTracking( { id: 'home', text: 'Home', - url: config.bootData.user.isSignedIn ? config.appSubUrl || '/' : '/login', + url: homeUrl, icon: 'grafana', }, menuOpen diff --git a/public/app/features/commandPalette/actions/dashboardActions.test.ts b/public/app/features/commandPalette/actions/dashboardActions.test.ts new file mode 100644 index 00000000000..9675b0d9027 --- /dev/null +++ b/public/app/features/commandPalette/actions/dashboardActions.test.ts @@ -0,0 +1,168 @@ +import { ArrayVector, DataFrame, DataFrameView, FieldType } from '@grafana/data'; +import { config } from '@grafana/runtime'; +import { ContextSrv, contextSrv } from 'app/core/services/context_srv'; +import impressionSrv from 'app/core/services/impression_srv'; +import { DashboardQueryResult, getGrafanaSearcher, QueryResponse } from 'app/features/search/service'; + +import { getRecentDashboardActions, getSearchResultActions } from './dashboardActions'; + +describe('dashboardActions', () => { + let grafanaSearcherSpy: jest.SpyInstance; + let mockContextSrv: jest.MockedObjectDeep; + const mockRecentDashboardUids = ['1', '2', '3', '4', '5', '6', '7', '8', '9']; + + const searchData: DataFrame = { + fields: [ + { name: 'kind', type: FieldType.string, config: {}, values: new ArrayVector(['dashboard']) }, + { name: 'name', type: FieldType.string, config: {}, values: new ArrayVector(['My dashboard 1']) }, + { name: 'uid', type: FieldType.string, config: {}, values: new ArrayVector(['my-dashboard-1']) }, + { name: 'url', type: FieldType.string, config: {}, values: new ArrayVector(['/my-dashboard-1']) }, + { name: 'tags', type: FieldType.other, config: {}, values: new ArrayVector([['foo', 'bar']]) }, + { name: 'location', type: FieldType.string, config: {}, values: new ArrayVector(['my-folder-1']) }, + ], + meta: { + custom: { + locationInfo: { + 'my-folder-1': { + name: 'My folder 1', + kind: 'folder', + url: '/my-folder-1', + }, + }, + }, + }, + length: 1, + }; + + const mockSearchResult: QueryResponse = { + isItemLoaded: jest.fn(), + loadMoreItems: jest.fn(), + totalRows: searchData.length, + view: new DataFrameView(searchData), + }; + + beforeAll(() => { + mockContextSrv = jest.mocked(contextSrv); + grafanaSearcherSpy = jest.spyOn(getGrafanaSearcher(), 'search').mockResolvedValue(mockSearchResult); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('getRecentDashboardActions', () => { + let impressionSrvSpy: jest.SpyInstance; + + beforeAll(() => { + impressionSrvSpy = jest.spyOn(impressionSrv, 'getDashboardOpened').mockResolvedValue(mockRecentDashboardUids); + }); + + describe('when not signed in', () => { + beforeAll(() => { + mockContextSrv.user.isSignedIn = false; + }); + + it('returns an empty array, does not call the impressionSrv and does not call the search backend', async () => { + const results = await getRecentDashboardActions(); + expect(impressionSrvSpy).not.toHaveBeenCalled(); + expect(grafanaSearcherSpy).not.toHaveBeenCalled(); + expect(results).toEqual([]); + }); + }); + + describe('when signed in', () => { + beforeAll(() => { + mockContextSrv.user.isSignedIn = true; + }); + + it('calls the search backend with recent dashboards and returns an array of CommandPaletteActions', async () => { + const results = await getRecentDashboardActions(); + expect(impressionSrvSpy).toHaveBeenCalled(); + expect(grafanaSearcherSpy).toHaveBeenCalledWith({ + kind: ['dashboard'], + limit: 5, + uid: ['1', '2', '3', '4', '5'], + }); + expect(results).toEqual([ + { + id: 'recent-dashboards/my-dashboard-1', + name: 'My dashboard 1', + priority: 5, + section: 'Recent dashboards', + url: '/my-dashboard-1', + }, + ]); + }); + }); + }); + + describe('getSearchResultActions', () => { + it('returns an empty array if the search query is empty', async () => { + const searchQuery = ''; + const results = await getSearchResultActions(searchQuery); + expect(grafanaSearcherSpy).not.toHaveBeenCalled(); + expect(results).toEqual([]); + }); + + describe('when not signed in', () => { + beforeAll(() => { + mockContextSrv.user.isSignedIn = false; + }); + + it('returns an empty array if anonymous access is not enabled', async () => { + config.bootData.settings.anonymousEnabled = false; + const searchQuery = 'mySearchQuery'; + const results = await getSearchResultActions(searchQuery); + expect(grafanaSearcherSpy).not.toHaveBeenCalled(); + expect(results).toEqual([]); + }); + + it('calls the search backend and returns an array of CommandPaletteActions if anonymous access is enabled', async () => { + config.bootData.settings.anonymousEnabled = true; + const searchQuery = 'mySearchQuery'; + const results = await getSearchResultActions(searchQuery); + expect(grafanaSearcherSpy).toHaveBeenCalledWith({ + kind: ['dashboard', 'folder'], + query: searchQuery, + limit: 100, + }); + expect(results).toEqual([ + { + id: 'go/dashboard/my-dashboard-1', + name: 'My dashboard 1', + priority: 1, + section: 'Dashboards', + subtitle: 'My folder 1', + url: '/my-dashboard-1', + }, + ]); + }); + }); + + describe('when signed in', () => { + beforeAll(() => { + mockContextSrv.user.isSignedIn = true; + }); + + it('calls the search backend with recent dashboards and returns an array of CommandPaletteActions', async () => { + const searchQuery = 'mySearchQuery'; + const results = await getSearchResultActions(searchQuery); + expect(grafanaSearcherSpy).toHaveBeenCalledWith({ + kind: ['dashboard', 'folder'], + query: searchQuery, + limit: 100, + }); + expect(results).toEqual([ + { + id: 'go/dashboard/my-dashboard-1', + name: 'My dashboard 1', + priority: 1, + section: 'Dashboards', + subtitle: 'My folder 1', + url: '/my-dashboard-1', + }, + ]); + }); + }); + }); +}); diff --git a/public/app/features/commandPalette/actions/dashboardActions.ts b/public/app/features/commandPalette/actions/dashboardActions.ts index 4e6b3a9f52a..69016f4d80e 100644 --- a/public/app/features/commandPalette/actions/dashboardActions.ts +++ b/public/app/features/commandPalette/actions/dashboardActions.ts @@ -2,7 +2,9 @@ import debounce from 'debounce-promise'; import { useEffect, useState } from 'react'; import { locationUtil } from '@grafana/data'; +import { config } from '@grafana/runtime'; import { t } from 'app/core/internationalization'; +import { contextSrv } from 'app/core/services/context_srv'; import impressionSrv from 'app/core/services/impression_srv'; import { getGrafanaSearcher } from 'app/features/search/service'; @@ -15,6 +17,10 @@ const MAX_RECENT_DASHBOARDS = 5; const debouncedSearch = debounce(getSearchResultActions, 200); export async function getRecentDashboardActions(): Promise { + if (!contextSrv.user.isSignedIn) { + return []; + } + const recentUids = (await impressionSrv.getDashboardOpened()).slice(0, MAX_RECENT_DASHBOARDS); const resultsDataFrame = await getGrafanaSearcher().search({ kind: ['dashboard'], @@ -46,7 +52,7 @@ export async function getRecentDashboardActions(): Promise { // Empty strings should not come through to here - if (searchQuery.length === 0) { + if (searchQuery.length === 0 || (!contextSrv.user.isSignedIn && !config.bootData.settings.anonymousEnabled)) { return []; } From ed11c32c1dbeb7da9f9d525ebe2f7a0501dd9899 Mon Sep 17 00:00:00 2001 From: Ieva Date: Thu, 9 Mar 2023 16:43:18 +0000 Subject: [PATCH 124/288] Support bundles: fix user collector permissions and format collector output (#64531) * fix user collector permissions and format collector output * lint --- pkg/infra/usagestats/service/service.go | 2 +- .../supportbundles/supportbundlesimpl/service_bundle.go | 2 +- pkg/services/user/userimpl/user.go | 6 ++++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/pkg/infra/usagestats/service/service.go b/pkg/infra/usagestats/service/service.go index 9c024b253cb..4097eb20da1 100644 --- a/pkg/infra/usagestats/service/service.go +++ b/pkg/infra/usagestats/service/service.go @@ -137,7 +137,7 @@ func (uss *UsageStats) supportBundleCollector() supportbundles.Collector { return nil, err } - data, err := json.Marshal(report) + data, err := json.MarshalIndent(report, "", " ") if err != nil { return nil, err } diff --git a/pkg/services/supportbundles/supportbundlesimpl/service_bundle.go b/pkg/services/supportbundles/supportbundlesimpl/service_bundle.go index 903b7bc6097..fa98f1a3f43 100644 --- a/pkg/services/supportbundles/supportbundlesimpl/service_bundle.go +++ b/pkg/services/supportbundles/supportbundlesimpl/service_bundle.go @@ -79,7 +79,7 @@ func (s *Service) bundle(ctx context.Context, collectors []string, uid string) ( } item, err := collector.Fn(ctx) if err != nil { - s.log.Warn("Failed to collect support bundle item", "error", err) + s.log.Warn("Failed to collect support bundle item", "error", err, "collector", collector.UID) } // write item to file diff --git a/pkg/services/user/userimpl/user.go b/pkg/services/user/userimpl/user.go index c4146c6c8c0..9be2516aa7b 100644 --- a/pkg/services/user/userimpl/user.go +++ b/pkg/services/user/userimpl/user.go @@ -419,7 +419,9 @@ func (s *Service) supportBundleCollector() supportbundles.Collector { Login: "sa-supportbundle", OrgRole: "Admin", IsGrafanaAdmin: true, - IsServiceAccount: true}, + IsServiceAccount: true, + Permissions: map[int64]map[string][]string{ac.GlobalOrgID: {ac.ActionUsersRead: {ac.ScopeGlobalUsersAll}}}, + }, OrgID: 0, Query: "", Page: 0, @@ -433,7 +435,7 @@ func (s *Service) supportBundleCollector() supportbundles.Collector { return nil, err } - userBytes, err := json.Marshal(res.Users) + userBytes, err := json.MarshalIndent(res.Users, "", " ") if err != nil { return nil, err } From 8ef2afda870d16451545f5a686ace348d67f0665 Mon Sep 17 00:00:00 2001 From: Karl Persson Date: Thu, 9 Mar 2023 18:08:56 +0100 Subject: [PATCH 125/288] ContextHandler: Always initiate permission map on signed in user (#64541) --- pkg/services/contexthandler/contexthandler.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/services/contexthandler/contexthandler.go b/pkg/services/contexthandler/contexthandler.go index 302ab8de371..f8e97b8fb92 100644 --- a/pkg/services/contexthandler/contexthandler.go +++ b/pkg/services/contexthandler/contexthandler.go @@ -122,8 +122,10 @@ func (h *ContextHandler) Middleware(next http.Handler) http.Handler { defer span.End() reqContext := &contextmodel.ReqContext{ - Context: mContext, - SignedInUser: &user.SignedInUser{}, + Context: mContext, + SignedInUser: &user.SignedInUser{ + Permissions: map[int64]map[string][]string{}, + }, IsSignedIn: false, AllowAnonymous: false, SkipCache: false, From 4d0e309d4eb2894619aa7142af8dbfc8b80a8d73 Mon Sep 17 00:00:00 2001 From: juanicabanas Date: Thu, 9 Mar 2023 14:17:54 -0300 Subject: [PATCH 126/288] PublicDashboards: New method created to get access through Request access page when its paused (#64451) --- pkg/services/publicdashboards/api/query.go | 2 +- .../publicdashboards/api/query_test.go | 2 +- .../public_dashboard_service_mock.go | 32 ++++++++ .../publicdashboards/publicdashboard.go | 1 + .../publicdashboards/service/query.go | 4 +- .../publicdashboards/service/service.go | 18 ++++- .../publicdashboards/service/service_test.go | 74 ++++++++++++++++++- 7 files changed, 121 insertions(+), 12 deletions(-) diff --git a/pkg/services/publicdashboards/api/query.go b/pkg/services/publicdashboards/api/query.go index a5ef7eea240..39ea9ccfb60 100644 --- a/pkg/services/publicdashboards/api/query.go +++ b/pkg/services/publicdashboards/api/query.go @@ -21,7 +21,7 @@ func (api *Api) ViewPublicDashboard(c *contextmodel.ReqContext) response.Respons return response.Err(ErrInvalidAccessToken.Errorf("ViewPublicDashboard: invalid access token")) } - pubdash, dash, err := api.PublicDashboardService.FindPublicDashboardAndDashboardByAccessToken( + pubdash, dash, err := api.PublicDashboardService.FindEnabledPublicDashboardAndDashboardByAccessToken( c.Req.Context(), accessToken, ) diff --git a/pkg/services/publicdashboards/api/query_test.go b/pkg/services/publicdashboards/api/query_test.go index 5db699d08c0..7b44066d721 100644 --- a/pkg/services/publicdashboards/api/query_test.go +++ b/pkg/services/publicdashboards/api/query_test.go @@ -83,7 +83,7 @@ func TestAPIViewPublicDashboard(t *testing.T) { for _, test := range testCases { t.Run(test.Name, func(t *testing.T) { service := publicdashboards.NewFakePublicDashboardService(t) - service.On("FindPublicDashboardAndDashboardByAccessToken", mock.Anything, mock.AnythingOfType("string")). + service.On("FindEnabledPublicDashboardAndDashboardByAccessToken", mock.Anything, mock.AnythingOfType("string")). Return(&PublicDashboard{Uid: "pubdashuid"}, test.DashboardResult, test.Err).Maybe() cfg := setting.NewCfg() diff --git a/pkg/services/publicdashboards/public_dashboard_service_mock.go b/pkg/services/publicdashboards/public_dashboard_service_mock.go index a0bd21ac7e5..3b5d0c6815e 100644 --- a/pkg/services/publicdashboards/public_dashboard_service_mock.go +++ b/pkg/services/publicdashboards/public_dashboard_service_mock.go @@ -254,6 +254,38 @@ func (_m *FakePublicDashboardService) FindDashboard(ctx context.Context, orgId i return r0, r1 } +// FindEnabledDashboardAndDashboardByAccessToken provides a mock function with given fields: ctx, accessToken +func (_m *FakePublicDashboardService) FindEnabledPublicDashboardAndDashboardByAccessToken(ctx context.Context, accessToken string) (*models.PublicDashboard, *dashboards.Dashboard, error) { + ret := _m.Called(ctx, accessToken) + + var r0 *models.PublicDashboard + if rf, ok := ret.Get(0).(func(context.Context, string) *models.PublicDashboard); ok { + r0 = rf(ctx, accessToken) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.PublicDashboard) + } + } + + var r1 *dashboards.Dashboard + if rf, ok := ret.Get(1).(func(context.Context, string) *dashboards.Dashboard); ok { + r1 = rf(ctx, accessToken) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*dashboards.Dashboard) + } + } + + var r2 error + if rf, ok := ret.Get(2).(func(context.Context, string) error); ok { + r2 = rf(ctx, accessToken) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + // FindPublicDashboardAndDashboardByAccessToken provides a mock function with given fields: ctx, accessToken func (_m *FakePublicDashboardService) FindPublicDashboardAndDashboardByAccessToken(ctx context.Context, accessToken string) (*models.PublicDashboard, *dashboards.Dashboard, error) { ret := _m.Called(ctx, accessToken) diff --git a/pkg/services/publicdashboards/publicdashboard.go b/pkg/services/publicdashboards/publicdashboard.go index 93bb2e96b61..a1b69547dfa 100644 --- a/pkg/services/publicdashboards/publicdashboard.go +++ b/pkg/services/publicdashboards/publicdashboard.go @@ -16,6 +16,7 @@ import ( //go:generate mockery --name Service --structname FakePublicDashboardService --inpackage --filename public_dashboard_service_mock.go type Service interface { FindPublicDashboardAndDashboardByAccessToken(ctx context.Context, accessToken string) (*PublicDashboard, *dashboards.Dashboard, error) + FindEnabledPublicDashboardAndDashboardByAccessToken(ctx context.Context, accessToken string) (*PublicDashboard, *dashboards.Dashboard, error) FindByAccessToken(ctx context.Context, accessToken string) (*PublicDashboard, error) FindByDashboardUid(ctx context.Context, orgId int64, dashboardUid string) (*PublicDashboard, error) FindAnnotations(ctx context.Context, reqDTO AnnotationsQueryDTO, accessToken string) ([]AnnotationEvent, error) diff --git a/pkg/services/publicdashboards/service/query.go b/pkg/services/publicdashboards/service/query.go index 9706253f163..2831090b93f 100644 --- a/pkg/services/publicdashboards/service/query.go +++ b/pkg/services/publicdashboards/service/query.go @@ -18,7 +18,7 @@ import ( // FindAnnotations returns annotations for a public dashboard func (pd *PublicDashboardServiceImpl) FindAnnotations(ctx context.Context, reqDTO models.AnnotationsQueryDTO, accessToken string) ([]models.AnnotationEvent, error) { - pub, dash, err := pd.FindPublicDashboardAndDashboardByAccessToken(ctx, accessToken) + pub, dash, err := pd.FindEnabledPublicDashboardAndDashboardByAccessToken(ctx, accessToken) if err != nil { return nil, err } @@ -119,7 +119,7 @@ func (pd *PublicDashboardServiceImpl) GetMetricRequest(ctx context.Context, dash // GetQueryDataResponse returns a query data response for the given panel and query func (pd *PublicDashboardServiceImpl) GetQueryDataResponse(ctx context.Context, skipCache bool, queryDto models.PublicDashboardQueryDTO, panelId int64, accessToken string) (*backend.QueryDataResponse, error) { - publicDashboard, dashboard, err := pd.FindPublicDashboardAndDashboardByAccessToken(ctx, accessToken) + publicDashboard, dashboard, err := pd.FindEnabledPublicDashboardAndDashboardByAccessToken(ctx, accessToken) if err != nil { return nil, err } diff --git a/pkg/services/publicdashboards/service/service.go b/pkg/services/publicdashboards/service/service.go index 2c292f9e9f8..6cca9ab0ab0 100644 --- a/pkg/services/publicdashboards/service/service.go +++ b/pkg/services/publicdashboards/service/service.go @@ -105,6 +105,20 @@ func (pd *PublicDashboardServiceImpl) FindByAccessToken(ctx context.Context, acc return pubdash, nil } +// FindEnabledPublicDashboardAndDashboardByAccessToken Gets public dashboard and a dashboard by access token if public dashboard is enabled +func (pd *PublicDashboardServiceImpl) FindEnabledPublicDashboardAndDashboardByAccessToken(ctx context.Context, accessToken string) (*PublicDashboard, *dashboards.Dashboard, error) { + pubdash, dash, err := pd.FindPublicDashboardAndDashboardByAccessToken(ctx, accessToken) + if err != nil { + return pubdash, dash, err + } + + if !pubdash.IsEnabled { + return nil, nil, ErrPublicDashboardNotEnabled.Errorf("FindEnabledPublicDashboardAndDashboardByAccessToken: Public dashboard is not enabled accessToken: %s", accessToken) + } + + return pubdash, dash, err +} + // FindPublicDashboardAndDashboardByAccessToken Gets public dashboard and a dashboard by access token func (pd *PublicDashboardServiceImpl) FindPublicDashboardAndDashboardByAccessToken(ctx context.Context, accessToken string) (*PublicDashboard, *dashboards.Dashboard, error) { pubdash, err := pd.FindByAccessToken(ctx, accessToken) @@ -112,10 +126,6 @@ func (pd *PublicDashboardServiceImpl) FindPublicDashboardAndDashboardByAccessTok return nil, nil, err } - if !pubdash.IsEnabled { - return nil, nil, ErrPublicDashboardNotEnabled.Errorf("FindPublicDashboardAndDashboardByAccessToken: Public dashboard is paused accessToken: %s", accessToken) - } - dash, err := pd.store.FindDashboard(ctx, pubdash.OrgId, pubdash.DashboardUid) if err != nil { return nil, nil, err diff --git a/pkg/services/publicdashboards/service/service_test.go b/pkg/services/publicdashboards/service/service_test.go index 6ae0ef9568f..15fe107312c 100644 --- a/pkg/services/publicdashboards/service/service_test.go +++ b/pkg/services/publicdashboards/service/service_test.go @@ -67,15 +67,15 @@ func TestGetPublicDashboard(t *testing.T) { DashResp: &dashboards.Dashboard{UID: "mydashboard", Data: dashboardData}, }, { - Name: "returns ErrPublicDashboardNotFound when isEnabled is false", + Name: "returns dashboard when isEnabled is false", AccessToken: "abc123", StoreResp: &storeResp{ pd: &PublicDashboard{AccessToken: "abcdToken", IsEnabled: false}, - d: &dashboards.Dashboard{UID: "mydashboard"}, + d: &dashboards.Dashboard{UID: "mydashboard", Data: dashboardData}, err: nil, }, - ErrResp: ErrPublicDashboardNotFound, - DashResp: nil, + ErrResp: nil, + DashResp: &dashboards.Dashboard{UID: "mydashboard", Data: dashboardData}, }, { Name: "returns ErrPublicDashboardNotFound if PublicDashboard missing", @@ -121,6 +121,72 @@ func TestGetPublicDashboard(t *testing.T) { } } +func TestGetEnabledPublicDashboard(t *testing.T) { + type storeResp struct { + pd *PublicDashboard + d *dashboards.Dashboard + err error + } + + testCases := []struct { + Name string + AccessToken string + StoreResp *storeResp + ErrResp error + DashResp *dashboards.Dashboard + }{ + { + Name: "returns a dashboard", + AccessToken: "abc123", + StoreResp: &storeResp{ + pd: &PublicDashboard{AccessToken: "abcdToken", IsEnabled: true}, + d: &dashboards.Dashboard{UID: "mydashboard", Data: dashboardData}, + err: nil, + }, + ErrResp: nil, + DashResp: &dashboards.Dashboard{UID: "mydashboard", Data: dashboardData}, + }, + { + Name: "returns ErrPublicDashboardNotFound when isEnabled is false", + AccessToken: "abc123", + StoreResp: &storeResp{ + pd: &PublicDashboard{AccessToken: "abcdToken", IsEnabled: false}, + d: &dashboards.Dashboard{UID: "mydashboard"}, + err: nil, + }, + ErrResp: ErrPublicDashboardNotFound, + DashResp: nil, + }, + } + + for _, test := range testCases { + t.Run(test.Name, func(t *testing.T) { + fakeStore := FakePublicDashboardStore{} + service := &PublicDashboardServiceImpl{ + log: log.New("test.logger"), + store: &fakeStore, + } + + fakeStore.On("FindByAccessToken", mock.Anything, mock.Anything).Return(test.StoreResp.pd, test.StoreResp.err) + fakeStore.On("FindDashboard", mock.Anything, mock.Anything, mock.Anything).Return(test.StoreResp.d, test.StoreResp.err) + + pdc, dash, err := service.FindEnabledPublicDashboardAndDashboardByAccessToken(context.Background(), test.AccessToken) + if test.ErrResp != nil { + assert.Error(t, test.ErrResp, err) + } else { + require.NoError(t, err) + } + + assert.Equal(t, test.DashResp, dash) + + if test.DashResp != nil { + assert.NotNil(t, dash.CreatedBy) + assert.Equal(t, test.StoreResp.pd, pdc) + } + }) + } +} + // We're using sqlite here because testing all of the behaviors with mocks in // the correct order is convoluted. func TestCreatePublicDashboard(t *testing.T) { From f9b5dbb473b2a1d6849712c87dccde37288464ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agn=C3=A8s=20Toulet?= <35176601+AgnesToulet@users.noreply.github.com> Date: Thu, 9 Mar 2023 18:24:03 +0100 Subject: [PATCH 127/288] Codegen: fix jenny_eachmajor.go (#64287) Update jenny_eachmajor.go --- pkg/codegen/jenny_eachmajor.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pkg/codegen/jenny_eachmajor.go b/pkg/codegen/jenny_eachmajor.go index ba06bf84016..a9c94425cfb 100644 --- a/pkg/codegen/jenny_eachmajor.go +++ b/pkg/codegen/jenny_eachmajor.go @@ -56,7 +56,13 @@ func (j *lmox) Generate(kind kindsys.Kind) (codejen.Files, error) { } var fl codejen.Files + major := -1 for sch := kind.Lineage().First(); sch != nil; sch = sch.Successor() { + if int(sch.Version()[0]) == major { + continue + } + major = int(sch.Version()[0]) + sfg.Schema = sch.LatestInMajor() files, err := do(sfg, fmt.Sprintf("v%v", sch.Version()[0])) if err != nil { From a134b47e019fe8129ee6530e3ba091dcffb8645c Mon Sep 17 00:00:00 2001 From: juanicabanas Date: Thu, 9 Mar 2023 16:58:29 -0300 Subject: [PATCH 128/288] PublicDashboards: Show email sharing feature depending on featureEnabled flag (#64555) --- packages/grafana-e2e-selectors/src/selectors/pages.ts | 1 + .../ConfigPublicDashboard/ConfigPublicDashboard.tsx | 5 ++--- .../ConfigPublicDashboard/EmailSharingConfiguration.tsx | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/grafana-e2e-selectors/src/selectors/pages.ts b/packages/grafana-e2e-selectors/src/selectors/pages.ts index 15fece4f375..5f6bace7288 100644 --- a/packages/grafana-e2e-selectors/src/selectors/pages.ts +++ b/packages/grafana-e2e-selectors/src/selectors/pages.ts @@ -201,6 +201,7 @@ export const Pages = { NoUpsertPermissionsWarningAlert: 'data-testid public dashboard no upsert permissions alert', EnableTimeRangeSwitch: 'data-testid public dashboard on off switch for time range', EmailSharingConfiguration: { + Container: 'data-testid email sharing config container', ShareType: 'data-testid public dashboard share type', EmailSharingInput: 'data-testid public dashboard email sharing input', EmailSharingInviteButton: 'data-testid public dashboard email sharing invite button', diff --git a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ConfigPublicDashboard/ConfigPublicDashboard.tsx b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ConfigPublicDashboard/ConfigPublicDashboard.tsx index 70183531042..1897cb0cc16 100644 --- a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ConfigPublicDashboard/ConfigPublicDashboard.tsx +++ b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ConfigPublicDashboard/ConfigPublicDashboard.tsx @@ -4,9 +4,8 @@ import React, { useContext } from 'react'; import { useForm } from 'react-hook-form'; import { GrafanaTheme2 } from '@grafana/data/src'; -import { GrafanaEdition } from '@grafana/data/src/types/config'; import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src'; -import { config, reportInteraction } from '@grafana/runtime/src'; +import { config, featureEnabled, reportInteraction } from '@grafana/runtime/src'; import { ClipboardButton, Field, @@ -55,7 +54,7 @@ const ConfigPublicDashboard = () => { const hasWritePermissions = contextSrv.hasAccess(AccessControlAction.DashboardsPublicWrite, isOrgAdmin()); const hasEmailSharingEnabled = - config.licenseInfo.edition === GrafanaEdition.Enterprise && !!config.featureToggles.publicDashboardsEmailSharing; + !!config.featureToggles.publicDashboardsEmailSharing && featureEnabled('publicDashboardsEmailSharing'); const dashboardState = useSelector((store) => store.dashboard); const dashboard = dashboardState.getModel()!; const dashboardVariables = dashboard.getVariables(); diff --git a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ConfigPublicDashboard/EmailSharingConfiguration.tsx b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ConfigPublicDashboard/EmailSharingConfiguration.tsx index d9026bb3009..370300f6ae2 100644 --- a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ConfigPublicDashboard/EmailSharingConfiguration.tsx +++ b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ConfigPublicDashboard/EmailSharingConfiguration.tsx @@ -144,7 +144,7 @@ export const EmailSharingConfiguration = () => { }; return ( -
+ Date: Fri, 10 Mar 2023 05:35:39 -0300 Subject: [PATCH 129/288] Docs: Remove Alertmanager configuration rollback from What's New (#64564) * Docs: Remove Alertmanager configuration rollback from What's New * Trigger Build --------- Co-authored-by: Jack Baldry --- docs/sources/whatsnew/whats-new-in-v9-4.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/docs/sources/whatsnew/whats-new-in-v9-4.md b/docs/sources/whatsnew/whats-new-in-v9-4.md index a7a980be37f..d47d7a20c23 100644 --- a/docs/sources/whatsnew/whats-new-in-v9-4.md +++ b/docs/sources/whatsnew/whats-new-in-v9-4.md @@ -254,12 +254,6 @@ We've added Discord as a contact point receiver for Grafana Cloud alert rules. We've made the following changes to alert administration. -#### Better guidance to configure your Alertmanagers - -Get additional help while configuring your Alertmanager. If you enter an invalid Alertmanager configuration, an error message displays, and you can choose from a previous working configuration to restart it. - -{{< figure src="/media/docs/alerting/alertmanager-config.png" max-width="750px" caption="Better guidance to configure your Alertmanager" >}} - #### Alerting landing page Introduces a new landing page that helps you get started quickly with Alerting. It also provides you with at a glance information on how Alerting works and a video to introduce you to key concepts. From 6cbc956b5cfad6a2eb47fca02d136a63c5ff138b Mon Sep 17 00:00:00 2001 From: Sven Grossmann Date: Fri, 10 Mar 2023 09:58:29 +0100 Subject: [PATCH 130/288] LogContext: Fix border radius to be consistent (#64589) fix border radius in LogRowContext --- public/app/features/logs/components/LogRowContext.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/features/logs/components/LogRowContext.tsx b/public/app/features/logs/components/LogRowContext.tsx index 66eb5b60670..cbb683455cf 100644 --- a/public/app/features/logs/components/LogRowContext.tsx +++ b/public/app/features/logs/components/LogRowContext.tsx @@ -98,7 +98,7 @@ const getLogRowContextStyles = (theme: GrafanaTheme2, wrapLogMessage?: boolean, background: ${theme.colors.background.canvas}; `, top: css` - border-radius: 0 0 ${theme.shape.borderRadius(2)} ${theme.shape.borderRadius(2)}; + border-radius: 0 0 ${theme.shape.borderRadius()} ${theme.shape.borderRadius()}; box-shadow: 0 0 ${theme.spacing(1.25)} ${theme.v1.palette.black}; clip-path: inset(0px -${theme.spacing(1.25)} -${theme.spacing(1.25)} -${theme.spacing(1.25)}); `, @@ -110,7 +110,7 @@ const getLogRowContextStyles = (theme: GrafanaTheme2, wrapLogMessage?: boolean, height: ${headerHeight}px; background: ${theme.colors.background.secondary}; border: 1px solid ${theme.colors.background.secondary}; - border-radius: ${theme.shape.borderRadius(2)} ${theme.shape.borderRadius(2)} 0 0; + border-radius: ${theme.shape.borderRadius()} ${theme.shape.borderRadius()} 0 0; box-shadow: 0 0 ${theme.spacing(1.25)} ${theme.v1.palette.black}; clip-path: inset(-${theme.spacing(1.25)} -${theme.spacing(1.25)} 0px -${theme.spacing(1.25)}); font-family: ${theme.typography.fontFamily}; From 1667ea118ffc32a4cf953115b6ce0e2a5d647120 Mon Sep 17 00:00:00 2001 From: Andre Pereira Date: Fri, 10 Mar 2023 11:11:53 +0000 Subject: [PATCH 131/288] Trace View: Copy Trace ID action button (#64416) * Added button to trace view to copy trace ID * Added dummy Export button --- .../TracePageHeader/Actions/ActionButton.tsx | 59 +++++++++++++++++++ .../Actions/TracePageActions.tsx | 51 ++++++++++++++++ .../TracePageHeader/NewTracePageHeader.tsx | 23 ++++++-- 3 files changed, 129 insertions(+), 4 deletions(-) create mode 100644 public/app/features/explore/TraceView/components/TracePageHeader/Actions/ActionButton.tsx create mode 100644 public/app/features/explore/TraceView/components/TracePageHeader/Actions/TracePageActions.tsx diff --git a/public/app/features/explore/TraceView/components/TracePageHeader/Actions/ActionButton.tsx b/public/app/features/explore/TraceView/components/TracePageHeader/Actions/ActionButton.tsx new file mode 100644 index 00000000000..8be7be68cd4 --- /dev/null +++ b/public/app/features/explore/TraceView/components/TracePageHeader/Actions/ActionButton.tsx @@ -0,0 +1,59 @@ +import { css } from '@emotion/css'; +import React from 'react'; + +import { GrafanaTheme2, IconName } from '@grafana/data'; +import { Button, useStyles2 } from '@grafana/ui'; + +export const getStyles = (theme: GrafanaTheme2) => { + return { + ActionButton: css` + label: ActionButton; + overflow: hidden; + position: relative; + width: 110px; + justify-content: center; + &:after { + content: ''; + background: ${theme.colors.primary.main}; + display: block; + position: absolute; + right: 0; + width: 100%; + height: 100%; + opacity: 0; + transition: all 0.8s; + } + &:active:after { + margin: 0; + opacity: 0.3; + transition: 0s; + } + `, + }; +}; + +export type ActionButtonProps = { + onClick: () => void; + ariaLabel: string; + label: string; + icon: IconName; +}; + +export default function ActionButton(props: ActionButtonProps) { + const { onClick, ariaLabel, label, icon } = props; + const styles = useStyles2(getStyles); + + return ( + + ); +} diff --git a/public/app/features/explore/TraceView/components/TracePageHeader/Actions/TracePageActions.tsx b/public/app/features/explore/TraceView/components/TracePageHeader/Actions/TracePageActions.tsx new file mode 100644 index 00000000000..7475fec0f99 --- /dev/null +++ b/public/app/features/explore/TraceView/components/TracePageHeader/Actions/TracePageActions.tsx @@ -0,0 +1,51 @@ +import { css } from '@emotion/css'; +import React, { useState } from 'react'; + +import { useStyles2 } from '@grafana/ui'; + +import ActionButton from './ActionButton'; + +export const getStyles = () => { + return { + TracePageActions: css` + label: TracePageActions; + display: flex; + gap: 4px; + `, + }; +}; + +export type TracePageActionsProps = { + traceId: string; +}; + +export default function TracePageActions(props: TracePageActionsProps) { + const { traceId } = props; + const styles = useStyles2(getStyles); + const [copyTraceIdClicked, setCopyTraceIdClicked] = useState(false); + + const copyTraceId = () => { + navigator.clipboard.writeText(traceId); + setCopyTraceIdClicked(true); + setTimeout(() => { + setCopyTraceIdClicked(false); + }, 5000); + }; + + return ( +
+ + alert('not implemented')} + ariaLabel={'Export Trace'} + label={'Export'} + icon={'save'} + /> +
+ ); +} diff --git a/public/app/features/explore/TraceView/components/TracePageHeader/NewTracePageHeader.tsx b/public/app/features/explore/TraceView/components/TracePageHeader/NewTracePageHeader.tsx index 1e4abc6b62b..99674c1c482 100644 --- a/public/app/features/explore/TraceView/components/TracePageHeader/NewTracePageHeader.tsx +++ b/public/app/features/explore/TraceView/components/TracePageHeader/NewTracePageHeader.tsx @@ -26,15 +26,29 @@ import { getTraceLinks } from '../model/link-patterns'; import { getHeaderTags, getTraceName } from '../model/trace-viewer'; import { formatDuration } from '../utils/date'; +import TracePageActions from './Actions/TracePageActions'; import SpanGraph from './SpanGraph'; import { TracePageHeaderEmbedProps, timestamp, getStyles } from './TracePageHeader'; const getNewStyles = (theme: GrafanaTheme2) => { return { + titleRow: css` + label: TracePageHeaderTitleRow; + align-items: center; + display: flex; + padding: 0 0.5em 0 0.5em; + `, + title: css` + label: TracePageHeaderTitle; + color: inherit; + flex: 1; + font-size: 1.7em; + line-height: 1em; + `, subtitle: css` flex: 1; line-height: 1em; - margin: -0.5em 0 1.5em 0.5em; + margin: -0.5em 0.5em 1.5em 0.5em; `, tag: css` margin: 0 0.5em 0 0; @@ -56,7 +70,7 @@ const getNewStyles = (theme: GrafanaTheme2) => { position: sticky; top: 0; z-index: 5; - padding: 10px 5px 0 5px; + padding: 0.5em 0.25em 0 0.25em; & > :last-child { border-bottom: 1px solid ${autoColor(theme, '#ccc')}; } @@ -82,7 +96,7 @@ export function NewTracePageHeader(props: TracePageHeaderEmbedProps) { const { method, status, url } = getHeaderTags(trace.spans); const title = ( -

+

| @@ -102,9 +116,10 @@ export function NewTracePageHeader(props: TracePageHeaderEmbedProps) { return (
-
+
{links && links.length > 0 && } {title} +
From 4c8855ed2dd97dd5dda63323c4e1d58919be928f Mon Sep 17 00:00:00 2001 From: Sonia Aguilar <33540275+soniaAguilarPeiron@users.noreply.github.com> Date: Fri, 10 Mar 2023 12:20:28 +0100 Subject: [PATCH 132/288] Alerting: Fix contact point name being URL-encoded in the title (#64590) Fix contact point name being encoded in the title text --- public/app/features/alerting/unified/Receivers.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/public/app/features/alerting/unified/Receivers.tsx b/public/app/features/alerting/unified/Receivers.tsx index 0ed184c1d4a..6eff867f0f1 100644 --- a/public/app/features/alerting/unified/Receivers.tsx +++ b/public/app/features/alerting/unified/Receivers.tsx @@ -96,8 +96,7 @@ const Receivers = () => { const integrationsErrorCount = contactPointsState?.errorCount ?? 0; const disableAmSelect = !isRoot; - - let pageNav = getPageNavigationModel(type, id, isduplicatingTemplate); + let pageNav = getPageNavigationModel(type, id ? decodeURIComponent(id) : undefined, isduplicatingTemplate); if (!alertManagerSourceName) { return isRoot ? ( From af5ee9c66d9e6b2ee0e86ac1500266edb5781357 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 10 Mar 2023 12:25:25 +0100 Subject: [PATCH 133/288] PanelChrome: Fixes title max width, and make make menu more prominent (#64492) * PanelChrome: Fixes title max width, and make make menu more prominent * Add fix for wrapping timeshift --- .../grafana-ui/src/components/PanelChrome/PanelChrome.tsx | 5 ++++- .../dashboard/dashgrid/PanelHeader/PanelHeaderTitleItems.tsx | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx index 3ff8f3b0e2b..d6d0c40c2c7 100644 --- a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx +++ b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx @@ -285,7 +285,6 @@ const getStyles = (theme: GrafanaTheme2) => { textOverflow: 'ellipsis', overflow: 'hidden', whiteSpace: 'nowrap', - maxWidth: theme.spacing(50), fontSize: theme.typography.h6.fontSize, fontWeight: theme.typography.h6.fontWeight, }), @@ -301,6 +300,10 @@ const getStyles = (theme: GrafanaTheme2) => { label: 'panel-menu', visibility: 'hidden', border: 'none', + background: theme.colors.secondary.main, + '&:hover': { + background: theme.colors.secondary.shade, + }, }), errorContainerFloating: css({ label: 'error-container', diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderTitleItems.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderTitleItems.tsx index 8f43db0f959..64cc2795c96 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderTitleItems.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderTitleItems.tsx @@ -74,6 +74,7 @@ const getStyles = (theme: GrafanaTheme2) => { timeshift: css({ color: theme.colors.text.link, gap: theme.spacing(0.5), + whiteSpace: 'nowrap', '&:hover': { color: theme.colors.emphasize(theme.colors.text.link, 0.03), From 3ff380f40f64f492ce017023aebf02fc7d7b0c60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 10 Mar 2023 12:25:35 +0100 Subject: [PATCH 134/288] Alerting: Minor style fix for home page (#64585) * Minor fix for home alerting page * Minor border fix * Remove outline --- public/app/features/alerting/unified/Home.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/public/app/features/alerting/unified/Home.tsx b/public/app/features/alerting/unified/Home.tsx index 1001c74b4a5..bcb07444adc 100644 --- a/public/app/features/alerting/unified/Home.tsx +++ b/public/app/features/alerting/unified/Home.tsx @@ -250,8 +250,7 @@ const getContentBoxStyles = (theme: GrafanaTheme2) => ({ box: css` padding: ${theme.spacing(2)}; background-color: ${theme.colors.background.secondary}; - border-radius: 3px; - outline: 1px solid ${theme.colors.border.strong}; + border-radius: ${theme.shape.borderRadius()}; `, }); From 14251db9bad9846f88a6816f04681751a639431e Mon Sep 17 00:00:00 2001 From: Leon Sorokin Date: Fri, 10 Mar 2023 05:49:02 -0600 Subject: [PATCH 135/288] Chore: uFuzzy 1.0.6 (#64575) --- package.json | 2 +- packages/grafana-ui/package.json | 2 +- .../unified/hooks/useFilteredRules.ts | 6 +- .../app/features/search/service/frontend.ts | 2 +- .../components/MetricEncyclopediaModal.tsx | 78 ++++++++----------- .../components/FlameGraph/FlameGraph.tsx | 8 +- yarn.lock | 12 +-- 7 files changed, 49 insertions(+), 61 deletions(-) diff --git a/package.json b/package.json index 448494bac54..a696c38a518 100644 --- a/package.json +++ b/package.json @@ -271,7 +271,7 @@ "@grafana/schema": "workspace:*", "@grafana/ui": "workspace:*", "@kusto/monaco-kusto": "5.3.6", - "@leeoniya/ufuzzy": "1.0.2", + "@leeoniya/ufuzzy": "1.0.6", "@lezer/common": "1.0.2", "@lezer/highlight": "1.1.3", "@lezer/lr": "1.3.3", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 8c229cc0eae..2ad949e3324 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -52,7 +52,7 @@ "@grafana/data": "9.5.0-pre", "@grafana/e2e-selectors": "9.5.0-pre", "@grafana/schema": "9.5.0-pre", - "@leeoniya/ufuzzy": "1.0.2", + "@leeoniya/ufuzzy": "1.0.6", "@monaco-editor/react": "4.4.6", "@popperjs/core": "2.11.6", "@react-aria/button": "3.6.1", diff --git a/public/app/features/alerting/unified/hooks/useFilteredRules.ts b/public/app/features/alerting/unified/hooks/useFilteredRules.ts index f7b259706a6..75cc2226bee 100644 --- a/public/app/features/alerting/unified/hooks/useFilteredRules.ts +++ b/public/app/features/alerting/unified/hooks/useFilteredRules.ts @@ -108,7 +108,7 @@ export const filterRules = ( const [idxs, info, order] = ufuzzy.search(namespaceHaystack, namespaceFilter); if (info && order) { filteredNamespaces = order.map((idx) => filteredNamespaces[info.idx[idx]]); - } else { + } else if (idxs) { filteredNamespaces = idxs.map((idx) => filteredNamespaces[idx]); } } @@ -127,7 +127,7 @@ const reduceNamespaces = (filterState: RulesFilter) => { const [idxs, info, order] = ufuzzy.search(groupsHaystack, groupNameFilter); if (info && order) { filteredGroups = order.map((idx) => filteredGroups[info.idx[idx]]); - } else { + } else if (idxs) { filteredGroups = idxs.map((idx) => filteredGroups[idx]); } } @@ -157,7 +157,7 @@ const reduceGroups = (filterState: RulesFilter) => { const [idxs, info, order] = ufuzzy.search(rulesHaystack, ruleNameQuery); if (info && order) { filteredRules = order.map((idx) => filteredRules[info.idx[idx]]); - } else { + } else if (idxs) { filteredRules = idxs.map((idx) => filteredRules[idx]); } } diff --git a/public/app/features/search/service/frontend.ts b/public/app/features/search/service/frontend.ts index b11fe34d77d..51b5af4c657 100644 --- a/public/app/features/search/service/frontend.ts +++ b/public/app/features/search/service/frontend.ts @@ -130,7 +130,7 @@ class FullResultCache { } } // > 1000 matches (unranked) - else { + else if (idxs) { for (let i = 0; i < idxs.length; i++) { let haystackIdx = idxs[i]; dst.push(src[haystackIdx]); diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/MetricEncyclopediaModal.tsx b/public/app/plugins/datasource/prometheus/querybuilder/components/MetricEncyclopediaModal.tsx index 969a6135a4a..60e5a6dcb3a 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/components/MetricEncyclopediaModal.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/components/MetricEncyclopediaModal.tsx @@ -2,7 +2,7 @@ import { css } from '@emotion/css'; import uFuzzy from '@leeoniya/ufuzzy'; import debounce from 'debounce-promise'; import { debounce as debounceLodash } from 'lodash'; -import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { GrafanaTheme2, SelectableValue } from '@grafana/data'; import { reportInteraction } from '@grafana/runtime'; @@ -81,9 +81,24 @@ export const placeholders = { export const DEFAULT_RESULTS_PER_PAGE = 10; -export const MetricEncyclopediaModal = (props: Props) => { - const uf = UseUfuzzy(); +const uf = new uFuzzy({ + intraMode: 1, + intraIns: 1, + intraSub: 1, + intraTrn: 1, + intraDel: 1, +}); +function fuzzySearch(haystack: string[], query: string, setter: React.Dispatch>) { + // console.log('fuzzySearch'); + + const idxs = uf.filter(haystack, query); + idxs && setter(idxs); +} + +const debouncedFuzzySearch = debounceLodash(fuzzySearch, 300); + +export const MetricEncyclopediaModal = (props: Props) => { const { datasource, isOpen, onClose, onChange, query } = props; const [variables, setVariables] = useState>>([]); @@ -93,7 +108,7 @@ export const MetricEncyclopediaModal = (props: Props) => { // metric list const [metrics, setMetrics] = useState([]); const [hasMetadata, setHasMetadata] = useState(true); - const [haystack, setHaystack] = useState([]); + const [metaHaystack, setMetaHaystack] = useState([]); const [nameHaystack, setNameHaystack] = useState([]); const [openTabs, setOpenTabs] = useState([]); @@ -104,7 +119,7 @@ export const MetricEncyclopediaModal = (props: Props) => { // filters const [fuzzySearchQuery, setFuzzySearchQuery] = useState(''); const [fuzzyMetaSearchResults, setFuzzyMetaSearchResults] = useState([]); - const [fuzzyNameSearchResults, setNameFuzzySearchResults] = useState([]); + const [fuzzyNameSearchResults, setFuzzyNameSearchResults] = useState([]); const [fullMetaSearch, setFullMetaSearch] = useState(false); const [excludeNullMetadata, setExcludeNullMetadata] = useState(false); const [selectedTypes, setSelectedTypes] = useState>>([]); @@ -141,14 +156,14 @@ export const MetricEncyclopediaModal = (props: Props) => { metrics = (await datasource.languageProvider.getLabelValues('__name__')) ?? []; } - let haystackData: string[] = []; + let haystackMetaData: string[] = []; let haystackNameData: string[] = []; let metricsData: MetricsData = metrics.map((m) => { const type = getMetadataType(m, datasource.languageProvider.metricsMetadata!); const description = getMetadataHelp(m, datasource.languageProvider.metricsMetadata!); // string[] = name + type + description - haystackData.push(`${m} ${type} ${description}`); + haystackMetaData.push(`${m} ${type} ${description}`); haystackNameData.push(m); return { value: m, @@ -159,7 +174,7 @@ export const MetricEncyclopediaModal = (props: Props) => { // setting this by the backend if useBackend is true setMetrics(metricsData); - setHaystack(haystackData); + setMetaHaystack(haystackMetaData); setNameHaystack(haystackNameData); setVariables( @@ -211,27 +226,6 @@ export const MetricEncyclopediaModal = (props: Props) => { return selectedTypes.length > 0; } - function fuzzySearch(query: string) { - // search either the names or all metadata - // fuzzy search go! - - if (fullMetaSearch) { - // considered simply filtering indexes with reduce and includes - // Performance comparison with 13,000 metrics searching metadata - // Fuzzy 6326ms - // Reduce & Includes 5541ms - const metaIdxs = uf.filter(haystack, query.toLowerCase()); - setFuzzyMetaSearchResults(metaIdxs); - } else { - const nameIdxs = uf.filter(nameHaystack, query.toLowerCase()); - setNameFuzzySearchResults(nameIdxs); - } - } - - const debouncedFuzzySearch = debounceLodash((query: string) => { - fuzzySearch(query); - }, 300); - /** * Filter * @@ -367,8 +361,14 @@ export const MetricEncyclopediaModal = (props: Props) => { setIsLoading(true); debouncedBackendSearch(value); } else { - // do the search on the frontend - debouncedFuzzySearch(value); + // search either the names or all metadata + // fuzzy search go! + + if (fullMetaSearch) { + debouncedFuzzySearch(metaHaystack, value, setFuzzyMetaSearchResults); + } else { + debouncedFuzzySearch(nameHaystack, value, setFuzzyNameSearchResults); + } } setPageNum(1); @@ -656,22 +656,6 @@ function alphabetically(ascending: boolean, metadataFilters: boolean) { }; } -function UseUfuzzy(): uFuzzy { - const ref = useRef(); - - if (!ref.current) { - ref.current = new uFuzzy({ - intraMode: 1, - intraIns: 1, - intraSub: 1, - intraTrn: 1, - intraDel: 1, - }); - } - - return ref.current; -} - const getStyles = (theme: GrafanaTheme2) => { return { cardsContainer: css` diff --git a/public/app/plugins/panel/flamegraph/components/FlameGraph/FlameGraph.tsx b/public/app/plugins/panel/flamegraph/components/FlameGraph/FlameGraph.tsx index 22d150e2463..c72381de55a 100644 --- a/public/app/plugins/panel/flamegraph/components/FlameGraph/FlameGraph.tsx +++ b/public/app/plugins/panel/flamegraph/components/FlameGraph/FlameGraph.tsx @@ -92,8 +92,12 @@ const FlameGraph = ({ const foundLabels = new Set(); if (search) { - for (let idx of ufuzzy.filter(uniqueLabels, search)) { - foundLabels.add(uniqueLabels[idx]); + let idxs = ufuzzy.filter(uniqueLabels, search); + + if (idxs) { + for (let idx of idxs) { + foundLabels.add(uniqueLabels[idx]); + } } } diff --git a/yarn.lock b/yarn.lock index 97eb18e03a8..b72d883bfb7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5351,7 +5351,7 @@ __metadata: "@grafana/e2e-selectors": 9.5.0-pre "@grafana/schema": 9.5.0-pre "@grafana/tsconfig": ^1.2.0-rc1 - "@leeoniya/ufuzzy": 1.0.2 + "@leeoniya/ufuzzy": 1.0.6 "@mdx-js/react": 1.6.22 "@monaco-editor/react": 4.4.6 "@popperjs/core": 2.11.6 @@ -6330,10 +6330,10 @@ __metadata: languageName: node linkType: hard -"@leeoniya/ufuzzy@npm:1.0.2": - version: 1.0.2 - resolution: "@leeoniya/ufuzzy@npm:1.0.2" - checksum: 5460378a8c32d121b0bc7c8e95cde995316516655528e248051b1bf360cdca0311ef3275de14b802587748231333cee6183c931b3abba26f9e4236ecc4959aa3 +"@leeoniya/ufuzzy@npm:1.0.6": + version: 1.0.6 + resolution: "@leeoniya/ufuzzy@npm:1.0.6" + checksum: e09672848e094745726331feebe6744d42563ccf160b38796eed4d72105daa052838fdec1944d5b44a27e328fc74a00547d00b894710e2720aa692ed5d3d6e3b languageName: node linkType: hard @@ -22136,7 +22136,7 @@ __metadata: "@grafana/tsconfig": ^1.2.0-rc1 "@grafana/ui": "workspace:*" "@kusto/monaco-kusto": 5.3.6 - "@leeoniya/ufuzzy": 1.0.2 + "@leeoniya/ufuzzy": 1.0.6 "@lezer/common": 1.0.2 "@lezer/highlight": 1.1.3 "@lezer/lr": 1.3.3 From f1a17d54cdd59c421c88877b39a9748427ea9bb0 Mon Sep 17 00:00:00 2001 From: Ivan Ortega Alba Date: Fri, 10 Mar 2023 12:56:41 +0100 Subject: [PATCH 136/288] PanelHeader: Add analytics (#64533) --- .../app/features/dashboard/utils/getPanelMenu.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/public/app/features/dashboard/utils/getPanelMenu.ts b/public/app/features/dashboard/utils/getPanelMenu.ts index 8f2a2610a7a..339281c4c45 100644 --- a/public/app/features/dashboard/utils/getPanelMenu.ts +++ b/public/app/features/dashboard/utils/getPanelMenu.ts @@ -43,6 +43,7 @@ export function getPanelMenu( locationService.partial({ viewPanel: panel.id, }); + reportInteraction('dashboards_panelheader_view_clicked'); }; const onEditPanel = (event: React.MouseEvent) => { @@ -50,21 +51,25 @@ export function getPanelMenu( locationService.partial({ editPanel: panel.id, }); + reportInteraction('dashboards_panelheader_edit_clicked'); }; const onSharePanel = (event: React.MouseEvent) => { event.preventDefault(); sharePanel(dashboard, panel); + reportInteraction('dashboards_panelheader_share_clicked'); }; const onAddLibraryPanel = (event: React.MouseEvent) => { event.preventDefault(); addLibraryPanel(dashboard, panel); + reportInteraction('dashboards_panelheader_createlibrarypanel_clicked'); }; const onUnlinkLibraryPanel = (event: React.MouseEvent) => { event.preventDefault(); unlinkLibraryPanel(panel); + reportInteraction('dashboards_panelheader_unlinklibrarypanel_clicked'); }; const onInspectPanel = (tab?: InspectTab) => { @@ -72,10 +77,7 @@ export function getPanelMenu( inspect: panel.id, inspectTab: tab, }); - - reportInteraction('grafana_panel_menu_inspect', { - tab: tab ?? InspectTab.Data, - }); + reportInteraction('dashboards_panelheader_inspect_clicked', { tab: tab ?? InspectTab.Data }); }; const onMore = (event: React.MouseEvent) => { @@ -85,16 +87,19 @@ export function getPanelMenu( const onDuplicatePanel = (event: React.MouseEvent) => { event.preventDefault(); duplicatePanel(dashboard, panel); + reportInteraction('dashboards_panelheader_duplicate_clicked'); }; const onCopyPanel = (event: React.MouseEvent) => { event.preventDefault(); copyPanel(panel); + reportInteraction('dashboards_panelheader_copy_clicked'); }; const onRemovePanel = (event: React.MouseEvent) => { event.preventDefault(); removePanel(dashboard, panel, true); + reportInteraction('dashboards_panelheader_remove_clicked'); }; const onNavigateToExplore = (event: React.MouseEvent) => { @@ -102,16 +107,19 @@ export function getPanelMenu( const openInNewWindow = event.ctrlKey || event.metaKey ? (url: string) => window.open(`${config.appSubUrl}${url}`) : undefined; store.dispatch(navigateToExplore(panel, { getDataSourceSrv, getTimeSrv, getExploreUrl, openInNewWindow }) as any); + reportInteraction('dashboards_panelheader_explore_clicked'); }; const onToggleLegend = (event: React.MouseEvent) => { event.preventDefault(); toggleLegend(panel); + reportInteraction('dashboards_panelheader_togglelegend_clicked'); }; const onCancelStreaming = (event: React.MouseEvent) => { event.preventDefault(); panel.getQueryRunner().cancelQuery(); + reportInteraction('dashboards_panelheader_cancelstreaming_clicked'); }; const menu: PanelMenuItem[] = []; From b46771cbfe1e7561b44133e7f375aafbe28930dc Mon Sep 17 00:00:00 2001 From: Sven Grossmann Date: Fri, 10 Mar 2023 13:13:51 +0100 Subject: [PATCH 137/288] Loki: Always fetch for new label keys in the QueryBuilder (#64597) * always fetch new labels * remove refreshLogLabels --- public/app/plugins/datasource/loki/LanguageProvider.ts | 6 ------ .../loki/querybuilder/components/LokiQueryBuilder.tsx | 3 +-- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/public/app/plugins/datasource/loki/LanguageProvider.ts b/public/app/plugins/datasource/loki/LanguageProvider.ts index 3e27c5f87ba..637bf49a2bc 100644 --- a/public/app/plugins/datasource/loki/LanguageProvider.ts +++ b/public/app/plugins/datasource/loki/LanguageProvider.ts @@ -386,12 +386,6 @@ export default class LokiLanguageProvider extends LanguageProvider { return []; } - async refreshLogLabels(forceRefresh?: boolean) { - if ((this.labelKeys && Date.now().valueOf() - this.labelFetchTs > LABEL_REFRESH_INTERVAL) || forceRefresh) { - await this.fetchLabels(); - } - } - /** * Fetch labels for a selector. This is cached by its args but also by the global timeRange currently selected as * they can change over requested time. diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilder.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilder.tsx index 9ff76b04b3f..0e92e3cd358 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilder.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilder.tsx @@ -49,8 +49,7 @@ export const LokiQueryBuilder = React.memo(({ datasource, query, onChange const labelsToConsider = query.labels.filter((x) => x !== forLabel); if (labelsToConsider.length === 0) { - await datasource.languageProvider.refreshLogLabels(); - return datasource.languageProvider.getLabelKeys(); + return await datasource.languageProvider.fetchLabels(); } const expr = lokiQueryModeller.renderLabels(labelsToConsider); From c955c20670e4723592ba52bce67fa56ad10ea1a7 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Fri, 10 Mar 2023 12:15:41 +0000 Subject: [PATCH 138/288] Chore: assign ownership of tracing feature flag to user essentials (#64598) assign ownership of tracing feature flag to user essentials --- pkg/services/featuremgmt/registry.go | 1 + pkg/services/featuremgmt/toggles_gen_test.go | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 46a822cdba4..cacfb8a7864 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -151,6 +151,7 @@ var ( Description: "Adds trace ID to error notifications", State: FeatureStateAlpha, FrontendOnly: true, + Owner: grafanaUserEssentialsSquad, }, { Name: "newTraceView", diff --git a/pkg/services/featuremgmt/toggles_gen_test.go b/pkg/services/featuremgmt/toggles_gen_test.go index 03a329238e1..85caf10901e 100644 --- a/pkg/services/featuremgmt/toggles_gen_test.go +++ b/pkg/services/featuremgmt/toggles_gen_test.go @@ -43,7 +43,6 @@ func TestFeatureToggleFiles(t *testing.T) { ownerlessFeatures := map[string]bool{ "prometheusAzureOverrideAudience": true, - "tracing": true, } t.Run("all new features should have an owner", func(t *testing.T) { From 548a5054ad8fa5c199bd01e20a02fbbe558d52fe Mon Sep 17 00:00:00 2001 From: Domas Date: Fri, 10 Mar 2023 14:41:06 +0200 Subject: [PATCH 139/288] Table: Introduce sparkline cell type (#63182) --- .../panel-table/table_tests_new.json | 1766 ++++++++++------- .../tablepanelcfg/schema-reference.md | 1 + .../transform-data/index.md | 6 + .../visualizations/table/index.md | 8 + .../feature-toggles/index.md | 1 + packages/grafana-data/src/dataframe/index.ts | 2 +- packages/grafana-data/src/dataframe/utils.ts | 6 +- packages/grafana-data/src/field/index.ts | 2 +- .../src/transformations/transformers/ids.ts | 1 + .../transformers/seriesToRows.ts | 4 +- packages/grafana-data/src/types/dataFrame.ts | 1 + .../src/types/featureToggles.gen.ts | 1 + .../grafana-schema/src/common/common.gen.ts | 21 +- .../grafana-schema/src/common/mudball.cue | 3 + packages/grafana-schema/src/common/table.cue | 10 +- .../src/components/Sparkline/Sparkline.tsx | 7 +- .../src/components/Table/FooterRow.tsx | 4 +- .../src/components/Table/SparklineCell.tsx | 114 ++ .../grafana-ui/src/components/Table/Table.tsx | 10 +- .../grafana-ui/src/components/Table/styles.ts | 37 +- .../grafana-ui/src/components/Table/types.ts | 2 + .../grafana-ui/src/components/Table/utils.ts | 15 + pkg/services/featuremgmt/codeowners.go | 1 + pkg/services/featuremgmt/registry.go | 7 + pkg/services/featuremgmt/toggles_gen.go | 4 + .../components/expressions/Expression.tsx | 5 +- .../unified/components/rule-editor/util.ts | 5 +- .../page/components/SearchResultsTable.tsx | 7 +- .../transformers/standardTransformers.ts | 3 + .../TimeSeriesTableTransformEditor.tsx | 25 + .../timeSeriesTableTransformer.test.ts | 104 + .../timeSeriesTableTransformer.ts | 127 ++ .../panel/table/TableCellOptionEditor.tsx | 11 + public/app/plugins/panel/table/TablePanel.tsx | 1 + .../cells/SparklineCellOptionsEditor.tsx | 79 + public/app/plugins/panel/table/module.tsx | 20 +- public/app/plugins/panel/table/panelcfg.cue | 2 + .../app/plugins/panel/table/panelcfg.gen.ts | 5 + 38 files changed, 1714 insertions(+), 714 deletions(-) create mode 100644 packages/grafana-ui/src/components/Table/SparklineCell.tsx create mode 100644 public/app/features/transformers/timeSeriesTable/TimeSeriesTableTransformEditor.tsx create mode 100644 public/app/features/transformers/timeSeriesTable/timeSeriesTableTransformer.test.ts create mode 100644 public/app/features/transformers/timeSeriesTable/timeSeriesTableTransformer.ts create mode 100644 public/app/plugins/panel/table/cells/SparklineCellOptionsEditor.tsx diff --git a/devenv/dev-dashboards/panel-table/table_tests_new.json b/devenv/dev-dashboards/panel-table/table_tests_new.json index 11c6c8c0eb8..a373c5080b3 100644 --- a/devenv/dev-dashboards/panel-table/table_tests_new.json +++ b/devenv/dev-dashboards/panel-table/table_tests_new.json @@ -1,688 +1,1100 @@ { - "annotations": { - "list": [ - { - "builtIn": 1, - "datasource": "-- Grafana --", - "enable": true, - "hide": true, - "iconColor": "rgba(0, 211, 255, 1)", - "name": "Annotations & Alerts", - "type": "dashboard" - } - ] - }, - "editable": true, - "gnetId": null, - "graphTooltip": 0, - "links": [], - "panels": [ - { - "datasource": null, - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 0 - }, - "id": 7, - "title": "Cell styles", - "type": "row" + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] }, - { - "datasource": "gdev-testdata", - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "custom": { - "align": "center", - "displayMode": "color-background", - "filterable": false - }, - "mappings": [], - "thresholds": { - "mode": "percentage", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "blue", - "value": 20 - }, - { - "color": "orange", - "value": 60 - }, - { - "color": "red", - "value": 70 - } - ] - }, - "unit": "degree" + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "liveNow": false, + "panels": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 7, + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "refId": "A" + } + ], + "title": "Cell styles", + "type": "row" }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "Max" - }, - "properties": [ - { - "id": "custom.width", - "value": 84 - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "Last" - }, - "properties": [ - { - "id": "custom.width", - "value": 78 - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "Mean" - }, - "properties": [ - { - "id": "custom.width", - "value": 74 - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "Field" - }, - "properties": [ - { - "id": "custom.align", - "value": "left" - } - ] - } - ] - }, - "gridPos": { - "h": 16, - "w": 7, - "x": 0, - "y": 1 - }, - "id": 4, - "options": { - "showHeader": true, - "sortBy": [ - { - "desc": true, - "displayName": "Last" - } - ] - }, - "pluginVersion": "", - "targets": [ { - "refId": "A", - "scenarioId": "random_walk", - "seriesCount": 15, - "stringInput": "" - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Colored background", - "transformations": [ - { - "id": "reduce", - "options": { - "reducers": [ - "max", - "mean", - "last" - ] - } - } - ], - "type": "table" - }, - { - "datasource": "gdev-testdata", - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "custom": { - "align": null, - "filterable": false - }, - "mappings": [], - "thresholds": { - "mode": "percentage", - "steps": [ - { - "color": "orange", - "value": null - }, - { - "color": "red", - "value": 50 - } - ] - } - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "A" + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" }, - "properties": [ - { - "id": "custom.displayMode", - "value": "gradient-gauge" - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "Info" - }, - "properties": [ - { - "id": "custom.width", - "value": 92 - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "Min" - }, - "properties": [ - { - "id": "custom.width", - "value": 76 - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "Max" - }, - "properties": [ - { - "id": "custom.width", - "value": 89 - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "Time" - }, - "properties": [ - { - "id": "custom.width", - "value": 165 - } - ] - } - ] - }, - "gridPos": { - "h": 16, - "w": 8, - "x": 7, - "y": 1 - }, - "id": 2, - "options": { - "showHeader": true, - "sortBy": [ - { - "desc": false, - "displayName": "Min" - } - ] - }, - "pluginVersion": "", - "targets": [ - { - "refId": "A", - "scenarioId": "random_walk_table", - "stringInput": "" - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Bar gauge cells", - "transformations": [ - { - "id": "organize", - "options": { - "excludeByName": { - "Time": true - }, - "indexByName": { - "Info": 1, - "Max": 3, - "Min": 2, - "Time": 0, - "Value": 4 - }, - "renameByName": {} - } - } - ], - "type": "table" - }, - { - "datasource": "gdev-testdata", - "description": "", - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "custom": { - "align": null, - "filterable": false - }, - "mappings": [], - "thresholds": { - "mode": "percentage", - "steps": [ - { - "color": "blue", - "value": null - }, - { - "color": "green", - "value": 50 - } - ] - } - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "A" - }, - "properties": [ - { - "id": "custom.displayMode", - "value": "lcd-gauge" - }, - { - "id": "custom.align", - "value": "center" - } - ] - } - ] - }, - "gridPos": { - "h": 16, - "w": 9, - "x": 15, - "y": 1 - }, - "id": 5, - "options": { - "showHeader": true, - "sortBy": [] - }, - "pluginVersion": "", - "targets": [ - { - "refId": "A", - "scenarioId": "random_walk_table", - "stringInput": "" - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Retro LCD cell", - "transformations": [ - { - "id": "organize", - "options": { - "excludeByName": { - "Info": false, - "Max": true, - "Min": true, - "Time": false - }, - "indexByName": { - "Info": 1, - "Max": 3, - "Min": 2, - "Time": 0, - "Value": 4 - }, - "renameByName": {} - } - } - ], - "type": "table" - }, - { - "collapsed": false, - "datasource": "gdev-testdata", - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 17 - }, - "id": 9, - "panels": [], - "title": "Data links", - "type": "row" - }, - { - "datasource": "gdev-testdata", - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "custom": { - "align": "center", - "displayMode": "color-text", - "filterable": false - }, - "decimals": 2, - "mappings": [], - "thresholds": { - "mode": "percentage", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "blue", - "value": 20 - }, - { - "color": "orange", - "value": 50 - }, - { - "color": "red", - "value": 70 - } - ] - }, - "unit": "percent" - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "Time" - }, - "properties": [ - { - "id": "custom.align", - "value": null - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "{name=\"S1\", server=\"A\"}" - }, - "properties": [ - { - "id": "links", - "value": [ - { - "title": "Details", - "url": "http://detail?serverLabel=${__field.labels.server}&valueNumeric=${__value.numeric}" - } + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "center", + "cellOptions": { + "mode": "gradient", + "type": "color-background" + }, + "filterable": false, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "percentage", + "steps": [ + { + "color": "green" + }, + { + "color": "blue", + "value": 20 + }, + { + "color": "orange", + "value": 60 + }, + { + "color": "red", + "value": 70 + } + ] + }, + "unit": "degree" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Max" + }, + "properties": [ + { + "id": "custom.width", + "value": 84 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Last" + }, + "properties": [ + { + "id": "custom.width", + "value": 78 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Mean" + }, + "properties": [ + { + "id": "custom.width", + "value": 74 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Field" + }, + "properties": [ + { + "id": "custom.align", + "value": "left" + } + ] + } ] - } - ] - } - ] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 18 - }, - "id": 3, - "maxDataPoints": "10", - "options": { - "showHeader": true - }, - "pluginVersion": "", - "targets": [ - { - "alias": "S1", - "labels": "server=A", - "refId": "A", - "scenarioId": "random_walk", - "seriesCount": 1, - "stringInput": "" - }, - { - "alias": "S2", - "labels": "server=B", - "refId": "B", - "scenarioId": "random_walk", - "seriesCount": 1, - "stringInput": "" - }, - { - "alias": "S3", - "labels": "server=C", - "refId": "C", - "scenarioId": "random_walk", - "seriesCount": 1, - "stringInput": "" - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Data link with labels and numeric value", - "transformations": [ - { - "id": "seriesToColumns", - "options": {} - } - ], - "type": "table" - }, - { - "datasource": "gdev-testdata", - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "custom": { - "align": "center", - "displayMode": "auto", - "filterable": false - }, - "mappings": [], - "thresholds": { - "mode": "percentage", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "blue", - "value": 20 - }, - { - "color": "orange", - "value": 60 - }, - { - "color": "red", - "value": 70 - } - ] - }, - "unit": "degree" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 5, - "x": 12, - "y": 18 - }, - "id": 10, - "options": { - "showHeader": false, - "sortBy": [ - { - "desc": true, - "displayName": "Last" - } - ] - }, - "pluginVersion": "", - "targets": [ - { - "refId": "A", - "scenarioId": "random_walk_table", - "seriesCount": 5, - "stringInput": "" - } - ], - "timeFrom": null, - "timeShift": null, - "title": "No header", - "transformations": [ - { - "id": "organize", - "options": { - "excludeByName": { - "Min": true, - "Time": true, - "Value": true }, - "indexByName": { - "Info": 2, - "Max": 4, - "Min": 3, - "Time": 0, - "Value": 1 + "gridPos": { + "h": 16, + "w": 7, + "x": 0, + "y": 1 }, - "renameByName": {} - } + "id": 4, + "options": { + "cellHeight": "md", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true, + "showRowNums": false, + "sortBy": [ + { + "desc": true, + "displayName": "Last" + } + ] + }, + "pluginVersion": "9.5.0-pre", + "targets": [ + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "refId": "A", + "scenarioId": "random_walk", + "seriesCount": 15, + "stringInput": "" + } + ], + "title": "Colored background", + "transformations": [ + { + "id": "reduce", + "options": { + "reducers": [ + "max", + "mean", + "last" + ] + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "filterable": false, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "percentage", + "steps": [ + { + "color": "orange" + }, + { + "color": "red", + "value": 50 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "A" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "mode": "gradient", + "type": "gauge" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Info" + }, + "properties": [ + { + "id": "custom.width", + "value": 92 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Min" + }, + "properties": [ + { + "id": "custom.width", + "value": 76 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Max" + }, + "properties": [ + { + "id": "custom.width", + "value": 89 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Time" + }, + "properties": [ + { + "id": "custom.width", + "value": 165 + } + ] + } + ] + }, + "gridPos": { + "h": 16, + "w": 8, + "x": 7, + "y": 1 + }, + "id": 2, + "options": { + "cellHeight": "md", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true, + "showRowNums": false, + "sortBy": [ + { + "desc": false, + "displayName": "Min" + } + ] + }, + "pluginVersion": "9.5.0-pre", + "targets": [ + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "refId": "A", + "scenarioId": "random_walk_table", + "stringInput": "" + } + ], + "title": "Bar gauge cells", + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "Time": true + }, + "indexByName": { + "Info": 1, + "Max": 3, + "Min": 2, + "Time": 0, + "Value": 4 + }, + "renameByName": {} + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "filterable": false, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "percentage", + "steps": [ + { + "color": "blue" + }, + { + "color": "green", + "value": 50 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "A" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "mode": "lcd", + "type": "gauge" + } + }, + { + "id": "custom.align", + "value": "center" + } + ] + } + ] + }, + "gridPos": { + "h": 16, + "w": 9, + "x": 15, + "y": 1 + }, + "id": 5, + "options": { + "cellHeight": "md", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true, + "showRowNums": false, + "sortBy": [] + }, + "pluginVersion": "9.5.0-pre", + "targets": [ + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "refId": "A", + "scenarioId": "random_walk_table", + "stringInput": "" + } + ], + "title": "Retro LCD cell", + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "Info": false, + "Max": true, + "Min": true, + "Time": false + }, + "indexByName": { + "Info": 1, + "Max": 3, + "Min": 2, + "Time": 0, + "Value": 4 + }, + "renameByName": {} + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "rate" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "type": "sparkline" + } + }, + { + "id": "color", + "value": { + "mode": "continuous-GrYlRd" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 9, + "x": 0, + "y": 17 + }, + "id": 14, + "options": { + "cellHeight": "md", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true, + "showRowNums": false + }, + "pluginVersion": "9.5.0-pre", + "targets": [ + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "labels": "cluster=eu,service=checkout", + "min": 0.1, + "refId": "A", + "scenarioId": "random_walk", + "seriesCount": 3 + } + ], + "title": "Sparkline cell", + "transformations": [ + { + "id": "timeSeriesTable", + "options": {} + }, + { + "id": "organize", + "options": { + "excludeByName": {}, + "indexByName": {}, + "renameByName": { + "Trend": "rate" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "rate" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "drawStyle": "bars", + "fillOpacity": 40, + "gradientMode": "opacity", + "lineWidth": 1, + "showPoints": "auto", + "type": "sparkline" + } + }, + { + "id": "color", + "value": { + "mode": "continuous-GrYlRd" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "latency" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "fillOpacity": 40, + "gradientMode": "hue", + "type": "sparkline" + } + }, + { + "id": "color", + "value": { + "fixedColor": "orange", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 9, + "x": 9, + "y": 17 + }, + "id": 15, + "maxDataPoints": 100, + "options": { + "cellHeight": "md", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true, + "showRowNums": false + }, + "pluginVersion": "9.5.0-pre", + "targets": [ + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "labels": "cluster=eu,service=checkout", + "min": 0.1, + "refId": "A", + "scenarioId": "random_walk", + "seriesCount": 3 + }, + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "hide": false, + "labels": "cluster=eu,service=checkout", + "min": 0.1, + "refId": "B", + "scenarioId": "random_walk", + "seriesCount": 3 + } + ], + "title": "Multiple sparkline cells per row", + "transformations": [ + { + "id": "timeSeriesTable", + "options": {} + }, + { + "id": "joinByField", + "options": { + "byField": "service", + "mode": "outer" + } + }, + { + "id": "organize", + "options": { + "excludeByName": { + "cluster 2": true + }, + "indexByName": {}, + "renameByName": { + "Trend": "rate", + "Trend #A": "rate", + "Trend #B": "latency", + "cluster 1": "cluster" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 25 + }, + "id": 9, + "targets": [ + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "refId": "A" + } + ], + "title": "Data links", + "type": "row" + }, + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "center", + "cellOptions": { + "type": "color-text" + }, + "filterable": false, + "inspect": false + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "percentage", + "steps": [ + { + "color": "green" + }, + { + "color": "blue", + "value": 20 + }, + { + "color": "orange", + "value": 50 + }, + { + "color": "red", + "value": 70 + } + ] + }, + "unit": "percent" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Time" + }, + "properties": [ + { + "id": "custom.align" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "{name=\"S1\", server=\"A\"}" + }, + "properties": [ + { + "id": "links", + "value": [ + { + "title": "Details", + "url": "http://detail?serverLabel=${__field.labels.server}&valueNumeric=${__value.numeric}" + } + ] + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 26 + }, + "id": 3, + "options": { + "cellHeight": "md", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true, + "showRowNums": false + }, + "pluginVersion": "9.5.0-pre", + "targets": [ + { + "alias": "S1", + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "labels": "server=A", + "refId": "A", + "scenarioId": "random_walk", + "seriesCount": 1, + "stringInput": "" + }, + { + "alias": "S2", + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "labels": "server=B", + "refId": "B", + "scenarioId": "random_walk", + "seriesCount": 1, + "stringInput": "" + }, + { + "alias": "S3", + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "labels": "server=C", + "refId": "C", + "scenarioId": "random_walk", + "seriesCount": 1, + "stringInput": "" + } + ], + "title": "Data link with labels and numeric value", + "transformations": [ + { + "id": "seriesToColumns", + "options": {} + } + ], + "type": "table" + }, + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "center", + "cellOptions": { + "type": "auto" + }, + "filterable": false, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "percentage", + "steps": [ + { + "color": "green" + }, + { + "color": "blue", + "value": 20 + }, + { + "color": "orange", + "value": 60 + }, + { + "color": "red", + "value": 70 + } + ] + }, + "unit": "degree" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 5, + "x": 12, + "y": 26 + }, + "id": 10, + "options": { + "cellHeight": "md", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": false, + "showRowNums": false, + "sortBy": [ + { + "desc": true, + "displayName": "Last" + } + ] + }, + "pluginVersion": "9.5.0-pre", + "targets": [ + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "refId": "A", + "scenarioId": "random_walk_table", + "seriesCount": 5, + "stringInput": "" + } + ], + "title": "No header", + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "Min": true, + "Time": true, + "Value": true + }, + "indexByName": { + "Info": 2, + "Max": 4, + "Min": 3, + "Time": 0, + "Value": 1 + }, + "renameByName": {} + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 34 + }, + "id": 12, + "options": { + "cellHeight": "md", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": true + }, + "showHeader": true + }, + "pluginVersion": "9.4.0-pre", + "targets": [ + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "refId": "A" + } + ], + "title": "Footer", + "type": "table" } - ], - "type": "table" + ], + "refresh": "", + "revision": 1, + "schemaVersion": 38, + "style": "dark", + "tags": [ + "gdev", + "panel-tests" + ], + "templating": { + "list": [] }, - { - "datasource": "gdev-testdata", - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "custom": { - "align": "auto", - "displayMode": "auto" - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": 0 - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 26 - }, - "id": 12, - "options": { - "footer": { - "show": true, - "fields": "", - "reducer": [ - "sum" - ] - }, - "showHeader": true - }, - "pluginVersion": "", - "title": "Footer", - "type": "table" - } - ], - "schemaVersion": 27, - "style": "dark", - "tags": [ - "gdev", - "panel-tests" - ], - "templating": { - "list": [] - }, - "time": { - "from": "now-6h", - "to": "now" - }, - "timepicker": { - "refresh_intervals": [ - "10s", - "30s", - "1m", - "5m", - "15m", - "30m", - "1h", - "2h", - "1d" - ] - }, - "timezone": "", - "title": "Panel Tests - React Table", - "uid": "U_bZIMRMk", - "version": 6 + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ] + }, + "timezone": "", + "title": "Panel Tests - React Table", + "uid": "U_bZIMRMk", + "version": 33, + "weekStart": "" } \ No newline at end of file diff --git a/docs/sources/developers/kinds/composable/tablepanelcfg/schema-reference.md b/docs/sources/developers/kinds/composable/tablepanelcfg/schema-reference.md index 33e565a7039..f012f709d25 100644 --- a/docs/sources/developers/kinds/composable/tablepanelcfg/schema-reference.md +++ b/docs/sources/developers/kinds/composable/tablepanelcfg/schema-reference.md @@ -23,6 +23,7 @@ title: TablePanelCfg kind |-----------------|---------------------------------------------------|----------|--------------------------------------------------------------------------------------| | `frameIndex` | number | **Yes** | Represents the index of the selected frame Default: `0`. | | `showHeader` | boolean | **Yes** | Controls whether the panel should show the header Default: `true`. | +| `cellHeight` | string | No | Height of a table cell
Possible values are: `sm`, `md`, `lg`. | | `footer` | [object](#footer) | No | Controls footer options Default: `map[countRows:false reducer:[] show:false]`. | | `showRowNums` | boolean | No | Controls whether the columns should be numbered Default: `false`. | | `showTypeIcons` | boolean | No | Controls whether the header should show icons for the column types Default: `false`. | diff --git a/docs/sources/panels-visualizations/query-transform-data/transform-data/index.md b/docs/sources/panels-visualizations/query-transform-data/transform-data/index.md index 8fa2a5dd58e..0099b631dfb 100644 --- a/docs/sources/panels-visualizations/query-transform-data/transform-data/index.md +++ b/docs/sources/panels-visualizations/query-transform-data/transform-data/index.md @@ -732,3 +732,9 @@ Here is the result after adding a Limit transformation with a value of '3': | 2020-07-07 11:34:20 | Temperature | 25 | | 2020-07-07 11:34:20 | Humidity | 22 | | 2020-07-07 10:32:20 | Humidity | 29 | + +### Time series to table transform + +> **Note:** This transformation is available in Grafana 9.5+ as an opt-in beta feature. Modify Grafana [configuration file]({{< relref "../../../setup-grafana/configure-grafana/#configuration-file-location" >}}) to enable the `timeSeriesTable` [feature toggle]({{< relref "../../../setup-grafana/configure-grafana/#feature_toggles" >}}) to use it. + +Use this transformation to convert time series result into a table, converting time series data frame into a "Trend" field. "Trend" field can then be rendered using [sparkline cell type]({{< relref "../../visualizations/table/#sparkline" >}}), producing an inline sparkline for each table row. If there are multiple time series queries, each will result in a separate table data frame. These can be joined using join or merge transforms to produce a single table with multiple sparklines per row. diff --git a/docs/sources/panels-visualizations/visualizations/table/index.md b/docs/sources/panels-visualizations/visualizations/table/index.md index 8e3e6b13d6c..089d7d3ffe4 100644 --- a/docs/sources/panels-visualizations/visualizations/table/index.md +++ b/docs/sources/panels-visualizations/visualizations/table/index.md @@ -122,6 +122,14 @@ If you have a field value that is an image URL or a base64 encoded image you can {{< figure src="/static/img/docs/v73/table_hover.gif" max-width="900px" caption="Table hover" >}} +### Sparkline + +> **Note:** This cell type is available in Grafana 9.5+ as an opt-in beta feature. Modify Grafana [configuration file]({{< relref "../../../setup-grafana/configure-grafana/#configuration-file-location" >}}) to enable the `timeSeriesTable` [feature toggle]({{< relref "../../../setup-grafana/configure-grafana/#feature_toggles" >}}) to use it. + +Shows value rendered as a sparkline. Requires [time series to table]({{< relref "../../query-transform-data/transform-data/#time-series-to-table-transform" >}}) data transform. + +{{< figure src="/static/img/docs/tables/sparkline.png" max-width="500px" caption="Sparkline" class="docs-image--no-shadow" >}} + ## Cell value inspect Enables value inspection from table cell. The raw value is presented in a modal window. diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index 75a0e5119f9..deb9903ffa8 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -94,6 +94,7 @@ Alpha features might be changed or removed without prior notice. | `drawerDataSourcePicker` | Changes the user experience for data source selection to a drawer. | | `traceqlSearch` | Enables the 'TraceQL Search' tab for the Tempo datasource which provides a UI to generate TraceQL queries | | `prometheusMetricEncyclopedia` | Replaces the Prometheus query builder metric select option with a paginated and filterable component | +| `timeSeriesTable` | Enable time series table transformer & sparkline cell type | ## Development feature toggles diff --git a/packages/grafana-data/src/dataframe/index.ts b/packages/grafana-data/src/dataframe/index.ts index 3267885b2c0..3213eb70dab 100644 --- a/packages/grafana-data/src/dataframe/index.ts +++ b/packages/grafana-data/src/dataframe/index.ts @@ -7,4 +7,4 @@ export * from './dimensions'; export * from './ArrayDataFrame'; export * from './DataFrameJSON'; export * from './frameComparisons'; -export { anySeriesWithTimeField } from './utils'; +export { anySeriesWithTimeField, isTimeSeriesFrame, isTimeSeriesFrames } from './utils'; diff --git a/packages/grafana-data/src/dataframe/utils.ts b/packages/grafana-data/src/dataframe/utils.ts index 0869d2ec135..774c6cd4b6d 100644 --- a/packages/grafana-data/src/dataframe/utils.ts +++ b/packages/grafana-data/src/dataframe/utils.ts @@ -2,15 +2,15 @@ import { DataFrame, FieldType } from '../types/dataFrame'; import { getTimeField } from './processDataFrame'; -export function isTimeSerie(frame: DataFrame) { +export function isTimeSeriesFrame(frame: DataFrame) { if (frame.fields.length > 2) { return false; } return Boolean(frame.fields.find((field) => field.type === FieldType.time)); } -export function isTimeSeries(data: DataFrame[]) { - return !data.find((frame) => !isTimeSerie(frame)); +export function isTimeSeriesFrames(data: DataFrame[]) { + return !data.find((frame) => !isTimeSeriesFrame(frame)); } /** diff --git a/packages/grafana-data/src/field/index.ts b/packages/grafana-data/src/field/index.ts index 8ae807d42bc..9462ca10687 100644 --- a/packages/grafana-data/src/field/index.ts +++ b/packages/grafana-data/src/field/index.ts @@ -15,4 +15,4 @@ export { sortThresholds, getActiveThreshold } from './thresholds'; export { applyFieldOverrides, validateFieldConfig, applyRawFieldOverrides, useFieldOverrides } from './fieldOverrides'; export { getFieldDisplayValuesProxy } from './getFieldDisplayValuesProxy'; export { getFieldDisplayName, getFrameDisplayName } from './fieldState'; -export { getScaleCalculator, getFieldConfigWithMinMax } from './scale'; +export { getScaleCalculator, getFieldConfigWithMinMax, getMinMaxAndDelta } from './scale'; diff --git a/packages/grafana-data/src/transformations/transformers/ids.ts b/packages/grafana-data/src/transformations/transformers/ids.ts index 968958d9668..bce099ea1ca 100644 --- a/packages/grafana-data/src/transformations/transformers/ids.ts +++ b/packages/grafana-data/src/transformations/transformers/ids.ts @@ -36,4 +36,5 @@ export enum DataTransformerID { groupingToMatrix = 'groupingToMatrix', limit = 'limit', partitionByValues = 'partitionByValues', + timeSeriesTable = 'timeSeriesTable', } diff --git a/packages/grafana-data/src/transformations/transformers/seriesToRows.ts b/packages/grafana-data/src/transformations/transformers/seriesToRows.ts index 8121784b476..3e32de48faa 100644 --- a/packages/grafana-data/src/transformations/transformers/seriesToRows.ts +++ b/packages/grafana-data/src/transformations/transformers/seriesToRows.ts @@ -2,7 +2,7 @@ import { omit } from 'lodash'; import { map } from 'rxjs/operators'; import { MutableDataFrame, sortDataFrame } from '../../dataframe'; -import { isTimeSeries } from '../../dataframe/utils'; +import { isTimeSeriesFrames } from '../../dataframe/utils'; import { getFrameDisplayName } from '../../field/fieldState'; import { Field, @@ -30,7 +30,7 @@ export const seriesToRowsTransformer: DataTransformerInfo { pxAlign: false, scaleKey, theme, + colorMode, + thresholds: config.thresholds, drawStyle: customConfig.drawStyle!, lineColor: customConfig.lineColor ?? seriesColor, lineWidth: customConfig.lineWidth, @@ -188,7 +191,9 @@ export class Sparkline extends PureComponent { showPoints: pointsMode, pointSize: customConfig.pointSize, fillOpacity: customConfig.fillOpacity, - fillColor: customConfig.fillColor ?? seriesColor, + fillColor: customConfig.fillColor, + lineStyle: customConfig.lineStyle, + gradientMode: customConfig.gradientMode, }); } diff --git a/packages/grafana-ui/src/components/Table/FooterRow.tsx b/packages/grafana-ui/src/components/Table/FooterRow.tsx index 8f598b680b1..83c8c0e6cd5 100644 --- a/packages/grafana-ui/src/components/Table/FooterRow.tsx +++ b/packages/grafana-ui/src/components/Table/FooterRow.tsx @@ -16,7 +16,7 @@ export interface FooterRowProps { tableStyles: TableStyles; } -export const FooterRow = (props: FooterRowProps) => { +export function FooterRow(props: FooterRowProps) { const { totalColumnsWidth, footerGroups, isPaginationVisible, tableStyles } = props; const e2eSelectorsTable = selectors.components.Panels.Visualization.Table; @@ -38,7 +38,7 @@ export const FooterRow = (props: FooterRowProps) => { })}
); -}; +} function renderFooterCell(column: ColumnInstance, tableStyles: TableStyles) { const footerProps = column.getHeaderProps(); diff --git a/packages/grafana-ui/src/components/Table/SparklineCell.tsx b/packages/grafana-ui/src/components/Table/SparklineCell.tsx new file mode 100644 index 00000000000..7da625fe5a7 --- /dev/null +++ b/packages/grafana-ui/src/components/Table/SparklineCell.tsx @@ -0,0 +1,114 @@ +import { isArray } from 'lodash'; +import React, { FC } from 'react'; + +import { + ArrayVector, + FieldType, + FieldConfig, + getMinMaxAndDelta, + FieldSparkline, + isDataFrame, + Field, +} from '@grafana/data'; +import { + BarAlignment, + GraphDrawStyle, + GraphFieldConfig, + GraphGradientMode, + LineInterpolation, + TableSparklineCellOptions, + TableCellDisplayMode, + VisibilityMode, +} from '@grafana/schema'; + +import { Sparkline } from '../Sparkline/Sparkline'; + +import { TableCellProps } from './types'; +import { getCellOptions } from './utils'; + +export const defaultSparklineCellConfig: GraphFieldConfig = { + drawStyle: GraphDrawStyle.Line, + lineInterpolation: LineInterpolation.Smooth, + lineWidth: 1, + fillOpacity: 17, + gradientMode: GraphGradientMode.Hue, + pointSize: 2, + barAlignment: BarAlignment.Center, + showPoints: VisibilityMode.Never, +}; + +export const SparklineCell: FC = (props) => { + const { field, innerWidth, tableStyles, cell, cellProps } = props; + + const sparkline = getSparkline(cell.value); + + if (!sparkline) { + return ( +
+ no data +
+ ); + } + + const range = getMinMaxAndDelta(sparkline.y); + sparkline.y.config.min = range.min; + sparkline.y.config.max = range.max; + sparkline.y.state = { range }; + + const cellOptions = getTableSparklineCellOptions(field); + + const config: FieldConfig = { + color: field.config.color, + custom: { + ...defaultSparklineCellConfig, + ...cellOptions, + }, + }; + + return ( +
+ +
+ ); +}; + +function getSparkline(value: unknown): FieldSparkline | undefined { + if (isArray(value)) { + return { + y: { + name: 'test', + type: FieldType.number, + values: new ArrayVector(value), + config: {}, + }, + }; + } + + if (isDataFrame(value)) { + const timeField = value.fields.find((x) => x.type === FieldType.time); + const numberField = value.fields.find((x) => x.type === FieldType.number); + + if (timeField && numberField) { + return { x: timeField, y: numberField }; + } + } + + return; +} + +function getTableSparklineCellOptions(field: Field): TableSparklineCellOptions { + let options = getCellOptions(field); + if (options.type === TableCellDisplayMode.Auto) { + options = { ...options, type: TableCellDisplayMode.Sparkline }; + } + if (options.type === TableCellDisplayMode.Sparkline) { + return options; + } + throw new Error(`Excpected options type ${TableCellDisplayMode.Sparkline} but got ${options.type}`); +} diff --git a/packages/grafana-ui/src/components/Table/Table.tsx b/packages/grafana-ui/src/components/Table/Table.tsx index 364d15dd283..90d2633025f 100644 --- a/packages/grafana-ui/src/components/Table/Table.tsx +++ b/packages/grafana-ui/src/components/Table/Table.tsx @@ -13,8 +13,9 @@ import { import { VariableSizeList } from 'react-window'; import { DataFrame, Field, ReducerID } from '@grafana/data'; +import { TableCellHeight } from '@grafana/schema'; -import { useStyles2, useTheme2 } from '../../themes'; +import { useTheme2 } from '../../themes'; import { CustomScrollbar } from '../CustomScrollbar/CustomScrollbar'; import { Pagination } from '../Pagination/Pagination'; @@ -23,7 +24,7 @@ import { HeaderRow } from './HeaderRow'; import { TableCell } from './TableCell'; import { useFixScrollbarContainer, useResetVariableListSizeCache } from './hooks'; import { getInitialState, useTableStateReducer } from './reducer'; -import { getTableStyles } from './styles'; +import { useTableStyles } from './styles'; import { FooterItem, GrafanaTableState, Props } from './types'; import { getColumns, @@ -56,13 +57,14 @@ export const Table = memo((props: Props) => { showTypeIcons, footerValues, enablePagination, + cellHeight = TableCellHeight.Md, } = props; const listRef = useRef(null); const tableDivRef = useRef(null); const variableSizeListScrollbarRef = useRef(null); - const tableStyles = useStyles2(getTableStyles); const theme = useTheme2(); + const tableStyles = useTableStyles(theme, cellHeight); const headerHeight = noHeader ? 0 : tableStyles.rowHeight; const [footerItems, setFooterItems] = useState(footerValues); @@ -385,6 +387,8 @@ export const Table = memo((props: Props) => {
{ +export function useTableStyles(theme: GrafanaTheme2, cellHeightOption: TableCellHeight) { const borderColor = theme.colors.border.weak; const resizerColor = theme.colors.primary.border; const cellPadding = 6; - const lineHeight = theme.typography.body.lineHeight; - const bodyFontSize = 14; - const cellHeight = cellPadding * 2 + bodyFontSize * lineHeight; + const cellHeight = getCellHeight(theme, cellHeightOption, cellPadding); const rowHeight = cellHeight + 2; + const headerHeight = 28; const rowHoverBg = theme.colors.emphasize(theme.colors.background.primary, 0.03); const buildCellContainerStyle = (color?: string, background?: string, overflowOnHover?: boolean) => { @@ -95,7 +95,7 @@ export const getTableStyles = (theme: GrafanaTheme2) => { cellHeight, buildCellContainerStyle, cellPadding, - cellHeightInner: bodyFontSize * lineHeight, + cellHeightInner: cellHeight - cellPadding * 2, rowHeight, table: css` height: 100%; @@ -106,14 +106,14 @@ export const getTableStyles = (theme: GrafanaTheme2) => { `, thead: css` label: thead; - height: ${rowHeight}px; + height: ${headerHeight}px; overflow-y: auto; overflow-x: hidden; position: relative; `, tfoot: css` label: tfoot; - height: ${rowHeight}px; + height: ${headerHeight}px; border-top: 1px solid ${borderColor}; overflow-y: auto; overflow-x: hidden; @@ -124,10 +124,12 @@ export const getTableStyles = (theme: GrafanaTheme2) => { border-bottom: 1px solid ${borderColor}; `, headerCell: css` - padding: ${cellPadding}px; + height: 100%; + padding: 0 ${cellPadding}px; overflow: hidden; white-space: nowrap; display: flex; + align-items: center; font-weight: ${theme.typography.fontWeightMedium}; &:last-child { @@ -285,6 +287,21 @@ export const getTableStyles = (theme: GrafanaTheme2) => { cursor: pointer; `, }; -}; +} -export type TableStyles = ReturnType; +export type TableStyles = ReturnType; + +function getCellHeight(theme: GrafanaTheme2, cellHeightOption: TableCellHeight, cellPadding: number) { + const bodyFontSize = theme.typography.fontSize; + const lineHeight = theme.typography.body.lineHeight; + + switch (cellHeightOption) { + case 'md': + return 42; + case 'lg': + return 48; + case 'sm': + default: + return cellPadding * 2 + bodyFontSize * lineHeight; + } +} diff --git a/packages/grafana-ui/src/components/Table/types.ts b/packages/grafana-ui/src/components/Table/types.ts index ba7f9ad3d6d..7ee4f584753 100644 --- a/packages/grafana-ui/src/components/Table/types.ts +++ b/packages/grafana-ui/src/components/Table/types.ts @@ -3,6 +3,7 @@ import { FC } from 'react'; import { CellProps, Column, Row, TableState, UseExpandedRowProps } from 'react-table'; import { DataFrame, Field, KeyValue, SelectableValue } from '@grafana/data'; +import { TableCellHeight } from '@grafana/schema'; import { TableStyles } from './styles'; @@ -84,6 +85,7 @@ export interface Props { footerOptions?: TableFooterCalc; footerValues?: FooterItem[]; enablePagination?: boolean; + cellHeight?: TableCellHeight; /** @alpha */ subData?: DataFrame[]; } diff --git a/packages/grafana-ui/src/components/Table/utils.ts b/packages/grafana-ui/src/components/Table/utils.ts index d4ed918755b..051da3e546f 100644 --- a/packages/grafana-ui/src/components/Table/utils.ts +++ b/packages/grafana-ui/src/components/Table/utils.ts @@ -15,6 +15,8 @@ import { reduceField, GrafanaTheme2, ArrayVector, + isDataFrame, + isTimeSeriesFrame, } from '@grafana/data'; import { BarGaugeDisplayMode, @@ -30,6 +32,7 @@ import { GeoCell } from './GeoCell'; import { ImageCell } from './ImageCell'; import { JSONViewCell } from './JSONViewCell'; import { RowExpander } from './RowExpander'; +import { SparklineCell } from './SparklineCell'; import { CellComponent, TableCellDisplayMode, @@ -190,6 +193,8 @@ export function getCellComponent(displayMode: TableCellDisplayMode, field: Field return ImageCell; case TableCellDisplayMode.Gauge: return BarGaugeCell; + case TableCellDisplayMode.Sparkline: + return SparklineCell; case TableCellDisplayMode.JSONView: return JSONViewCell; } @@ -198,10 +203,20 @@ export function getCellComponent(displayMode: TableCellDisplayMode, field: Field return GeoCell; } + if (field.type === FieldType.frame) { + const firstValue = field.values.get(0); + if (isDataFrame(firstValue) && isTimeSeriesFrame(firstValue)) { + return SparklineCell; + } + + return JSONViewCell; + } + // Default or Auto if (field.type === FieldType.other) { return JSONViewCell; } + return DefaultCell; } diff --git a/pkg/services/featuremgmt/codeowners.go b/pkg/services/featuremgmt/codeowners.go index 89774222968..52eb0bd3489 100644 --- a/pkg/services/featuremgmt/codeowners.go +++ b/pkg/services/featuremgmt/codeowners.go @@ -21,4 +21,5 @@ const ( grafanaAlertingSquad codeowner = "@grafana/alerting-squad" hostedGrafanaTeam codeowner = "@grafana/hosted-grafana-team" awsPluginsSquad codeowner = "@grafana/aws-plugins" + appO11ySquad codeowner = "@grafana/app-o11y" ) diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index cacfb8a7864..74b8985acd7 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -438,5 +438,12 @@ var ( FrontendOnly: true, Owner: grafanaObservabilityMetricsSquad, }, + { + Name: "timeSeriesTable", + Description: "Enable time series table transformer & sparkline cell type", + State: FeatureStateAlpha, + FrontendOnly: true, + Owner: appO11ySquad, + }, } ) diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index b1541eb9369..d4aea01c0e1 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -270,4 +270,8 @@ const ( // FlagPrometheusMetricEncyclopedia // Replaces the Prometheus query builder metric select option with a paginated and filterable component FlagPrometheusMetricEncyclopedia = "prometheusMetricEncyclopedia" + + // FlagTimeSeriesTable + // Enable time series table transformer & sparkline cell type + FlagTimeSeriesTable = "timeSeriesTable" ) diff --git a/public/app/features/alerting/unified/components/expressions/Expression.tsx b/public/app/features/alerting/unified/components/expressions/Expression.tsx index b2767a1ea3d..7504a8e5e70 100644 --- a/public/app/features/alerting/unified/components/expressions/Expression.tsx +++ b/public/app/features/alerting/unified/components/expressions/Expression.tsx @@ -2,8 +2,7 @@ import { css, cx } from '@emotion/css'; import { capitalize, uniqueId } from 'lodash'; import React, { FC, useCallback, useState } from 'react'; -import { DataFrame, dateTimeFormat, GrafanaTheme2, LoadingState, PanelData } from '@grafana/data'; -import { isTimeSeries } from '@grafana/data/src/dataframe/utils'; +import { DataFrame, dateTimeFormat, GrafanaTheme2, LoadingState, PanelData, isTimeSeriesFrames } from '@grafana/data'; import { Stack } from '@grafana/experimental'; import { AutoSizeInput, clearButtonStyles, Icon, IconButton, Select, useStyles2 } from '@grafana/ui'; import { ClassicConditions } from 'app/features/expressions/components/ClassicConditions'; @@ -138,7 +137,7 @@ export const ExpressionResult: FC = ({ series, isAlertCon // sometimes we receive results where every value is just "null" when noData occurs const emptyResults = isEmptySeries(series); - const isTimeSeriesResults = !emptyResults && isTimeSeries(series); + const isTimeSeriesResults = !emptyResults && isTimeSeriesFrames(series); return (
diff --git a/public/app/features/alerting/unified/components/rule-editor/util.ts b/public/app/features/alerting/unified/components/rule-editor/util.ts index a16eee6e922..89dfe3e2717 100644 --- a/public/app/features/alerting/unified/components/rule-editor/util.ts +++ b/public/app/features/alerting/unified/components/rule-editor/util.ts @@ -1,7 +1,6 @@ import { ValidateResult } from 'react-hook-form'; -import { DataFrame, ThresholdsConfig, ThresholdsMode } from '@grafana/data'; -import { isTimeSeries } from '@grafana/data/src/dataframe/utils'; +import { DataFrame, ThresholdsConfig, ThresholdsMode, isTimeSeriesFrames } from '@grafana/data'; import { GraphTresholdsStyleMode } from '@grafana/schema'; import { config } from 'app/core/config'; import { EvalFunction } from 'app/features/alerting/state/alertDef'; @@ -98,7 +97,7 @@ export function errorFromSeries(series: DataFrame[]): Error | undefined { return; } - const isTimeSeriesResults = isTimeSeries(series); + const isTimeSeriesResults = isTimeSeriesFrames(series); let error; if (isTimeSeriesResults) { diff --git a/public/app/features/search/page/components/SearchResultsTable.tsx b/public/app/features/search/page/components/SearchResultsTable.tsx index 7b37837b294..c3d5d96882c 100644 --- a/public/app/features/search/page/components/SearchResultsTable.tsx +++ b/public/app/features/search/page/components/SearchResultsTable.tsx @@ -7,9 +7,10 @@ import InfiniteLoader from 'react-window-infinite-loader'; import { Observable } from 'rxjs'; import { Field, GrafanaTheme2 } from '@grafana/data'; -import { useStyles2 } from '@grafana/ui'; +import { TableCellHeight } from '@grafana/schema'; +import { useStyles2, useTheme2 } from '@grafana/ui'; import { TableCell } from '@grafana/ui/src/components/Table/TableCell'; -import { getTableStyles } from '@grafana/ui/src/components/Table/styles'; +import { useTableStyles } from '@grafana/ui/src/components/Table/styles'; import { useSearchKeyboardNavigation } from '../../hooks/useSearchKeyboardSelection'; import { QueryResponse } from '../../service'; @@ -51,7 +52,7 @@ export const SearchResultsTable = React.memo( }: SearchResultsProps) => { const styles = useStyles2(getStyles); const columnStyles = useStyles2(getColumnStyles); - const tableStyles = useStyles2(getTableStyles); + const tableStyles = useTableStyles(useTheme2(), TableCellHeight.Md); const infiniteLoaderRef = useRef(null); const [listEl, setListEl] = useState(null); const highlightIndex = useSearchKeyboardNavigation(keyboardEvents, 0, response); diff --git a/public/app/features/transformers/standardTransformers.ts b/public/app/features/transformers/standardTransformers.ts index c047c59ab14..8716a48a20a 100644 --- a/public/app/features/transformers/standardTransformers.ts +++ b/public/app/features/transformers/standardTransformers.ts @@ -1,4 +1,5 @@ import { TransformerRegistryItem } from '@grafana/data'; +import { config } from '@grafana/runtime'; import { filterByValueTransformRegistryItem } from './FilterByValueTransformer/FilterByValueTransformerEditor'; import { heatmapTransformRegistryItem } from './calculateHeatmap/HeatmapTransformerEditor'; @@ -27,6 +28,7 @@ import { partitionByValuesTransformRegistryItem } from './partitionByValues/Part import { prepareTimeseriesTransformerRegistryItem } from './prepareTimeSeries/PrepareTimeSeriesEditor'; import { rowsToFieldsTransformRegistryItem } from './rowsToFields/RowsToFieldsTransformerEditor'; import { spatialTransformRegistryItem } from './spatial/SpatialTransformerEditor'; +import { timeSeriesTableTransformRegistryItem } from './timeSeriesTable/TimeSeriesTableTransformEditor'; export const getStandardTransformers = (): Array> => { return [ @@ -57,5 +59,6 @@ export const getStandardTransformers = (): Array> = limitTransformRegistryItem, joinByLabelsTransformRegistryItem, partitionByValuesTransformRegistryItem, + ...(config.featureToggles.timeSeriesTable ? [timeSeriesTableTransformRegistryItem] : []), ]; }; diff --git a/public/app/features/transformers/timeSeriesTable/TimeSeriesTableTransformEditor.tsx b/public/app/features/transformers/timeSeriesTable/TimeSeriesTableTransformEditor.tsx new file mode 100644 index 00000000000..eb01f21de1b --- /dev/null +++ b/public/app/features/transformers/timeSeriesTable/TimeSeriesTableTransformEditor.tsx @@ -0,0 +1,25 @@ +import React from 'react'; + +import { PluginState, TransformerRegistryItem, TransformerUIProps } from '@grafana/data'; + +import { timeSeriesTableTransformer, TimeSeriesTableTransformerOptions } from './timeSeriesTableTransformer'; + +export interface Props extends TransformerUIProps<{}> {} + +export function TimeSeriesTableTransformEditor({ input, options, onChange }: Props) { + if (input.length === 0) { + return null; + } + + return
; +} + +export const timeSeriesTableTransformRegistryItem: TransformerRegistryItem = { + id: timeSeriesTableTransformer.id, + editor: TimeSeriesTableTransformEditor, + transformation: timeSeriesTableTransformer, + name: timeSeriesTableTransformer.name, + description: timeSeriesTableTransformer.description, + state: PluginState.beta, + help: ``, +}; diff --git a/public/app/features/transformers/timeSeriesTable/timeSeriesTableTransformer.test.ts b/public/app/features/transformers/timeSeriesTable/timeSeriesTableTransformer.test.ts new file mode 100644 index 00000000000..952fcdf0d9b --- /dev/null +++ b/public/app/features/transformers/timeSeriesTable/timeSeriesTableTransformer.test.ts @@ -0,0 +1,104 @@ +import { toDataFrame, FieldType, Labels, DataFrame, Field } from '@grafana/data'; + +import { timeSeriesToTableTransform } from './timeSeriesTableTransformer'; + +describe('timeSeriesTableTransformer', () => { + it('Will transform a single query', () => { + const series = [ + getTimeSeries('A', { instance: 'A', pod: 'B' }), + getTimeSeries('A', { instance: 'A', pod: 'C' }), + getTimeSeries('A', { instance: 'A', pod: 'D' }), + ]; + + const results = timeSeriesToTableTransform({}, series); + expect(results).toHaveLength(1); + const result = results[0]; + expect(result.refId).toBe('A'); + expect(result.fields).toHaveLength(3); + expect(result.fields[0].values.toArray()).toEqual(['A', 'A', 'A']); + expect(result.fields[1].values.toArray()).toEqual(['B', 'C', 'D']); + assertDataFrameField(result.fields[2], series); + }); + + it('Will pass through non time series frames', () => { + const series = [ + getTable('B', ['foo', 'bar']), + getTimeSeries('A', { instance: 'A', pod: 'B' }), + getTimeSeries('A', { instance: 'A', pod: 'C' }), + getTable('C', ['bar', 'baz', 'bad']), + ]; + + const results = timeSeriesToTableTransform({}, series); + expect(results).toHaveLength(3); + expect(results[0]).toEqual(series[0]); + expect(results[1].refId).toBe('A'); + expect(results[1].fields).toHaveLength(3); + expect(results[1].fields[0].values.toArray()).toEqual(['A', 'A']); + expect(results[1].fields[1].values.toArray()).toEqual(['B', 'C']); + expect(results[2]).toEqual(series[3]); + }); + + it('Will group by refId', () => { + const series = [ + getTimeSeries('A', { instance: 'A', pod: 'B' }), + getTimeSeries('A', { instance: 'A', pod: 'C' }), + getTimeSeries('A', { instance: 'A', pod: 'D' }), + getTimeSeries('B', { instance: 'B', pod: 'F', cluster: 'A' }), + getTimeSeries('B', { instance: 'B', pod: 'G', cluster: 'B' }), + ]; + + const results = timeSeriesToTableTransform({}, series); + expect(results).toHaveLength(2); + expect(results[0].refId).toBe('A'); + expect(results[0].fields).toHaveLength(3); + expect(results[0].fields[0].values.toArray()).toEqual(['A', 'A', 'A']); + expect(results[0].fields[1].values.toArray()).toEqual(['B', 'C', 'D']); + assertDataFrameField(results[0].fields[2], series.slice(0, 3)); + expect(results[1].refId).toBe('B'); + expect(results[1].fields).toHaveLength(4); + expect(results[1].fields[0].values.toArray()).toEqual(['B', 'B']); + expect(results[1].fields[1].values.toArray()).toEqual(['F', 'G']); + expect(results[1].fields[2].values.toArray()).toEqual(['A', 'B']); + assertDataFrameField(results[1].fields[3], series.slice(3, 5)); + }); +}); + +function assertFieldsEqual(field1: Field, field2: Field) { + expect(field1.type).toEqual(field2.type); + expect(field1.name).toEqual(field2.name); + expect(field1.values.toArray()).toEqual(field2.values.toArray()); + expect(field1.labels ?? {}).toEqual(field2.labels ?? {}); +} + +function assertDataFrameField(field: Field, matchesFrames: DataFrame[]) { + const frames: DataFrame[] = field.values.toArray(); + expect(frames).toHaveLength(matchesFrames.length); + frames.forEach((frame, idx) => { + const matchingFrame = matchesFrames[idx]; + expect(frame.fields).toHaveLength(matchingFrame.fields.length); + frame.fields.forEach((field, fidx) => assertFieldsEqual(field, matchingFrame.fields[fidx])); + }); +} + +function getTimeSeries(refId: string, labels: Labels) { + return toDataFrame({ + refId, + fields: [ + { name: 'Time', type: FieldType.time, values: [10] }, + { + name: 'Value', + type: FieldType.number, + values: [10], + labels, + }, + ], + }); +} + +function getTable(refId: string, fields: string[]) { + return toDataFrame({ + refId, + fields: fields.map((f) => ({ name: f, type: FieldType.string, values: ['value'] })), + labels: {}, + }); +} diff --git a/public/app/features/transformers/timeSeriesTable/timeSeriesTableTransformer.ts b/public/app/features/transformers/timeSeriesTable/timeSeriesTableTransformer.ts new file mode 100644 index 00000000000..d3e6b68a12c --- /dev/null +++ b/public/app/features/transformers/timeSeriesTable/timeSeriesTableTransformer.ts @@ -0,0 +1,127 @@ +import { map } from 'rxjs/operators'; + +import { + ArrayVector, + DataFrame, + DataTransformerID, + DataTransformerInfo, + Field, + FieldType, + MutableDataFrame, + isTimeSeriesFrame, +} from '@grafana/data'; + +export interface TimeSeriesTableTransformerOptions {} + +export const timeSeriesTableTransformer: DataTransformerInfo = { + id: DataTransformerID.timeSeriesTable, + name: 'Time series to table transform', + description: 'Time series to table rows', + defaultOptions: {}, + + operator: (options) => (source) => + source.pipe( + map((data) => { + return timeSeriesToTableTransform(options, data); + }) + ), +}; + +/** + * Converts time series frames to table frames for use with sparkline chart type. + * + * @remarks + * For each refId (queryName) convert all time series frames into a single table frame, adding each series + * as values of a "Trend" frame field. This allows "Trend" to be rendered as area chart type. + * Any non time series frames are returned as is. + * + * @param options - Transform options, currently not used + * @param data - Array of data frames to transform + * @returns Array of transformed data frames + * + * @alpha + */ +export function timeSeriesToTableTransform(options: TimeSeriesTableTransformerOptions, data: DataFrame[]): DataFrame[] { + // initialize fields from labels for each refId + const refId2LabelFields = getLabelFields(data); + + const refId2frameField: Record> = {}; + + const result: DataFrame[] = []; + + for (const frame of data) { + if (!isTimeSeriesFrame(frame)) { + result.push(frame); + continue; + } + + const refId = frame.refId ?? ''; + + const labelFields = refId2LabelFields[refId] ?? {}; + // initialize a new frame for this refId with fields per label and a Trend frame field, if it doesn't exist yet + let frameField = refId2frameField[refId]; + if (!frameField) { + frameField = { + name: 'Trend' + (refId && Object.keys(refId2LabelFields).length > 1 ? ` #${refId}` : ''), + type: FieldType.frame, + config: {}, + values: new ArrayVector(), + }; + refId2frameField[refId] = frameField; + const table = new MutableDataFrame(); + for (const label of Object.values(labelFields)) { + table.addField(label); + } + table.addField(frameField); + table.refId = refId; + result.push(table); + } + + // add values to each label based field of this frame + const labels = frame.fields[1].labels; + for (const labelKey of Object.keys(labelFields)) { + const labelValue = labels?.[labelKey] ?? null; + labelFields[labelKey].values.add(labelValue); + } + + frameField.values.add(frame); + } + return result; +} + +// For each refId, initialize a field for each label name +function getLabelFields(frames: DataFrame[]): Record>> { + // refId -> label name -> field + const labelFields: Record>> = {}; + + for (const frame of frames) { + if (!isTimeSeriesFrame(frame)) { + continue; + } + + const refId = frame.refId ?? ''; + + if (!labelFields[refId]) { + labelFields[refId] = {}; + } + + for (const field of frame.fields) { + if (!field.labels) { + continue; + } + + for (const labelName of Object.keys(field.labels)) { + if (!labelFields[refId][labelName]) { + labelFields[refId][labelName] = { + name: labelName, + type: FieldType.string, + config: {}, + values: new ArrayVector(), + }; + } + } + } + } + + return labelFields; +} diff --git a/public/app/plugins/panel/table/TableCellOptionEditor.tsx b/public/app/plugins/panel/table/TableCellOptionEditor.tsx index c1bf6854791..ba23e886ddd 100644 --- a/public/app/plugins/panel/table/TableCellOptionEditor.tsx +++ b/public/app/plugins/panel/table/TableCellOptionEditor.tsx @@ -2,11 +2,13 @@ import { merge } from 'lodash'; import React, { useState } from 'react'; import { SelectableValue } from '@grafana/data'; +import { config } from '@grafana/runtime'; import { TableCellOptions } from '@grafana/schema'; import { Field, Select, TableCellDisplayMode } from '@grafana/ui'; import { BarGaugeCellOptionsEditor } from './cells/BarGaugeCellOptionsEditor'; import { ColorBackgroundCellOptionsEditor } from './cells/ColorBackgroundCellOptionsEditor'; +import { SparklineCellOptionsEditor } from './cells/SparklineCellOptionsEditor'; // The props that any cell type editor are expected // to handle. In this case the generic type should @@ -64,12 +66,21 @@ export const TableCellOptionEditor = ({ value, onChange }: Props) => { {cellType === TableCellDisplayMode.ColorBackground && ( )} + {cellType === TableCellDisplayMode.Sparkline && ( + + )} ); }; +const SparklineDisplayModeOption: SelectableValue = { + value: { type: TableCellDisplayMode.Sparkline }, + label: 'Sparkline', +}; + const cellDisplayModeOptions: Array> = [ { value: { type: TableCellDisplayMode.Auto }, label: 'Auto' }, + ...(config.featureToggles.timeSeriesTable ? [SparklineDisplayModeOption] : []), { value: { type: TableCellDisplayMode.ColorText }, label: 'Colored text' }, { value: { type: TableCellDisplayMode.ColorBackground }, label: 'Colored background' }, { value: { type: TableCellDisplayMode.Gauge }, label: 'Gauge' }, diff --git a/public/app/plugins/panel/table/TablePanel.tsx b/public/app/plugins/panel/table/TablePanel.tsx index 3ae842d3d59..485024b55d9 100644 --- a/public/app/plugins/panel/table/TablePanel.tsx +++ b/public/app/plugins/panel/table/TablePanel.tsx @@ -56,6 +56,7 @@ export function TablePanel(props: Props) { footerOptions={options.footer} enablePagination={options.footer?.enablePagination} subData={subData} + cellHeight={options.cellHeight} /> ); diff --git a/public/app/plugins/panel/table/cells/SparklineCellOptionsEditor.tsx b/public/app/plugins/panel/table/cells/SparklineCellOptionsEditor.tsx new file mode 100644 index 00000000000..2e7ade9d591 --- /dev/null +++ b/public/app/plugins/panel/table/cells/SparklineCellOptionsEditor.tsx @@ -0,0 +1,79 @@ +import { css } from '@emotion/css'; +import React, { useMemo } from 'react'; + +import { createFieldConfigRegistry } from '@grafana/data'; +import { GraphFieldConfig, TableSparklineCellOptions } from '@grafana/schema'; +import { VerticalGroup, Field, useStyles2 } from '@grafana/ui'; +import { defaultSparklineCellConfig } from '@grafana/ui/src/components/Table/SparklineCell'; + +import { getGraphFieldConfig } from '../../timeseries/config'; +import { TableCellEditorProps } from '../TableCellOptionEditor'; + +type OptionKey = keyof TableSparklineCellOptions; + +const optionIds: Array = [ + 'drawStyle', + 'lineInterpolation', + 'barAlignment', + 'lineWidth', + 'fillOpacity', + 'gradientMode', + 'lineStyle', + 'spanNulls', + 'showPoints', + 'pointSize', +]; + +export const SparklineCellOptionsEditor = (props: TableCellEditorProps) => { + const { cellOptions, onChange } = props; + + const registry = useMemo(() => { + const config = getGraphFieldConfig(defaultSparklineCellConfig); + return createFieldConfigRegistry(config, 'ChartCell'); + }, []); + + const style = useStyles2(getStyles); + + const values = { ...defaultSparklineCellConfig, ...cellOptions }; + + return ( + + {registry.list(optionIds.map((id) => `custom.${id}`)).map((item) => { + if (item.showIf && !item.showIf(values)) { + return null; + } + const Editor = item.editor; + const path = item.path; + + return ( + + onChange({ ...cellOptions, [path]: val })} + value={(isOptionKey(path, values) ? values[path] : undefined) ?? item.defaultValue} + item={item} + context={{ data: [] }} + /> + + ); + })} + + ); +}; + +// jumping through hoops to avoid using "any" +function isOptionKey(key: string, options: TableSparklineCellOptions): key is OptionKey { + return key in options; +} + +const getStyles = () => ({ + field: css` + width: 100%; + + // @TODO don't show "scheme" option for custom gradient mode. + // it needs thresholds to work, which are not supported + // for area chart cell right now + [title='Use color scheme to define gradient'] { + display: none; + } + `, +}); diff --git a/public/app/plugins/panel/table/module.tsx b/public/app/plugins/panel/table/module.tsx index 9604f5c2df7..6a11e5ec321 100644 --- a/public/app/plugins/panel/table/module.tsx +++ b/public/app/plugins/panel/table/module.tsx @@ -7,7 +7,13 @@ import { standardEditorsRegistry, identityOverrideProcessor, } from '@grafana/data'; -import { TableFieldOptions, TableCellOptions, TableCellDisplayMode, defaultTableFieldOptions } from '@grafana/schema'; +import { + TableFieldOptions, + TableCellOptions, + TableCellDisplayMode, + defaultTableFieldOptions, + TableCellHeight, +} from '@grafana/schema'; import { PaginationEditor } from './PaginationEditor'; import { TableCellOptionEditor } from './TableCellOptionEditor'; @@ -108,6 +114,18 @@ export const plugin = new PanelPlugin(TablePane name: 'Show table header', defaultValue: defaultPanelOptions.showHeader, }) + .addRadio({ + path: 'cellHeight', + name: 'Cell height', + defaultValue: defaultPanelOptions.cellHeight, + settings: { + options: [ + { value: TableCellHeight.Sm, label: 'Small' }, + { value: TableCellHeight.Md, label: 'Medium' }, + { value: TableCellHeight.Lg, label: 'Large' }, + ], + }, + }) .addBooleanSwitch({ path: 'showRowNums', name: 'Show row numbers', diff --git a/public/app/plugins/panel/table/panelcfg.cue b/public/app/plugins/panel/table/panelcfg.cue index a574c08d6b2..563561e380d 100644 --- a/public/app/plugins/panel/table/panelcfg.cue +++ b/public/app/plugins/panel/table/panelcfg.cue @@ -45,6 +45,8 @@ composableKinds: PanelCfg: { // Represents the selected calculations reducer: [] } + // Controls the height of the rows + cellHeight?: ui.TableCellHeight | *"md" } @cuetsy(kind="interface") }, ] diff --git a/public/app/plugins/panel/table/panelcfg.gen.ts b/public/app/plugins/panel/table/panelcfg.gen.ts index c0533fcb87a..85d617bc4ba 100644 --- a/public/app/plugins/panel/table/panelcfg.gen.ts +++ b/public/app/plugins/panel/table/panelcfg.gen.ts @@ -13,6 +13,10 @@ import * as ui from '@grafana/schema'; export const PanelCfgModelVersion = Object.freeze([0, 0]); export interface PanelOptions { + /** + * Controls the height of the rows + */ + cellHeight?: ui.TableCellHeight; /** * Controls footer options */ @@ -40,6 +44,7 @@ export interface PanelOptions { } export const defaultPanelOptions: Partial = { + cellHeight: ui.TableCellHeight.Md, footer: { /** * Controls whether the footer should be shown From 79152969f31e107384847d12bc70944305c014a5 Mon Sep 17 00:00:00 2001 From: Levente Balogh Date: Fri, 10 Mar 2023 13:55:38 +0100 Subject: [PATCH 140/288] Extensions: Expose an enum for available placements (#64586) * feat: expose an enum type for extension placements * fix: export the enum properly --- packages/grafana-data/src/types/index.ts | 1 + .../src/types/pluginExtensions.ts | 4 ++++ .../dashboard/utils/getPanelMenu.test.ts | 21 ++++++++++++------- .../features/dashboard/utils/getPanelMenu.ts | 10 ++++++--- .../features/plugins/extensions/placements.ts | 3 --- 5 files changed, 25 insertions(+), 14 deletions(-) delete mode 100644 public/app/features/plugins/extensions/placements.ts diff --git a/packages/grafana-data/src/types/index.ts b/packages/grafana-data/src/types/index.ts index 0a3020aff99..67328bc5fd8 100644 --- a/packages/grafana-data/src/types/index.ts +++ b/packages/grafana-data/src/types/index.ts @@ -61,4 +61,5 @@ export { isPluginExtensionCommand, assertPluginExtensionCommand, PluginExtensionTypes, + PluginExtensionPlacements, } from './pluginExtensions'; diff --git a/packages/grafana-data/src/types/pluginExtensions.ts b/packages/grafana-data/src/types/pluginExtensions.ts index c2a4215dd6d..f366ed23a94 100644 --- a/packages/grafana-data/src/types/pluginExtensions.ts +++ b/packages/grafana-data/src/types/pluginExtensions.ts @@ -2,6 +2,10 @@ * These types are exposed when rendering extension points */ +export enum PluginExtensionPlacements { + DashboardPanelMenu = 'grafana/dashboard/panel/menu', +} + export enum PluginExtensionTypes { link = 'link', command = 'command', diff --git a/public/app/features/dashboard/utils/getPanelMenu.test.ts b/public/app/features/dashboard/utils/getPanelMenu.test.ts index ca637b459f1..c548185062c 100644 --- a/public/app/features/dashboard/utils/getPanelMenu.test.ts +++ b/public/app/features/dashboard/utils/getPanelMenu.test.ts @@ -1,4 +1,10 @@ -import { PanelMenuItem, PluginExtension, PluginExtensionLink, PluginExtensionTypes } from '@grafana/data'; +import { + PanelMenuItem, + PluginExtension, + PluginExtensionLink, + PluginExtensionTypes, + PluginExtensionPlacements, +} from '@grafana/data'; import { PluginExtensionPanelContext, PluginExtensionRegistryItem, @@ -7,7 +13,6 @@ import { import { LoadingState } from '@grafana/schema'; import config from 'app/core/config'; import * as actions from 'app/features/explore/state/main'; -import { GrafanaExtensions } from 'app/features/plugins/extensions/placements'; import { setStore } from 'app/store/store'; import { PanelModel } from '../state'; @@ -138,7 +143,7 @@ describe('getPanelMenu()', () => { describe('when extending panel menu from plugins', () => { it('should contain menu item from link extension', () => { setPluginsExtensionRegistry({ - [GrafanaExtensions.DashboardPanelMenu]: [ + [PluginExtensionPlacements.DashboardPanelMenu]: [ createRegistryItem({ type: PluginExtensionTypes.link, title: 'Declare incident', @@ -166,7 +171,7 @@ describe('getPanelMenu()', () => { it('should truncate menu item title to 25 chars', () => { setPluginsExtensionRegistry({ - [GrafanaExtensions.DashboardPanelMenu]: [ + [PluginExtensionPlacements.DashboardPanelMenu]: [ createRegistryItem({ type: PluginExtensionTypes.link, title: 'Declare incident when pressing this amazing menu item', @@ -202,7 +207,7 @@ describe('getPanelMenu()', () => { }); setPluginsExtensionRegistry({ - [GrafanaExtensions.DashboardPanelMenu]: [ + [PluginExtensionPlacements.DashboardPanelMenu]: [ createRegistryItem( { type: PluginExtensionTypes.link, @@ -233,7 +238,7 @@ describe('getPanelMenu()', () => { it('should hide menu item if configure function returns undefined', () => { setPluginsExtensionRegistry({ - [GrafanaExtensions.DashboardPanelMenu]: [ + [PluginExtensionPlacements.DashboardPanelMenu]: [ createRegistryItem( { type: PluginExtensionTypes.link, @@ -266,7 +271,7 @@ describe('getPanelMenu()', () => { const configure = jest.fn(); setPluginsExtensionRegistry({ - [GrafanaExtensions.DashboardPanelMenu]: [ + [PluginExtensionPlacements.DashboardPanelMenu]: [ createRegistryItem( { type: PluginExtensionTypes.link, @@ -348,7 +353,7 @@ describe('getPanelMenu()', () => { }; setPluginsExtensionRegistry({ - [GrafanaExtensions.DashboardPanelMenu]: [ + [PluginExtensionPlacements.DashboardPanelMenu]: [ createRegistryItem( { type: PluginExtensionTypes.link, diff --git a/public/app/features/dashboard/utils/getPanelMenu.ts b/public/app/features/dashboard/utils/getPanelMenu.ts index 339281c4c45..a4195ce3eb3 100644 --- a/public/app/features/dashboard/utils/getPanelMenu.ts +++ b/public/app/features/dashboard/utils/getPanelMenu.ts @@ -1,4 +1,9 @@ -import { isPluginExtensionCommand, isPluginExtensionLink, PanelMenuItem } from '@grafana/data'; +import { + isPluginExtensionCommand, + isPluginExtensionLink, + PanelMenuItem, + PluginExtensionPlacements, +} from '@grafana/data'; import { AngularComponent, getDataSourceSrv, @@ -26,7 +31,6 @@ import { } from 'app/features/dashboard/utils/panel'; import { InspectTab } from 'app/features/inspector/types'; import { isPanelModelLibraryPanel } from 'app/features/library-panels/guard'; -import { GrafanaExtensions } from 'app/features/plugins/extensions/placements'; import { store } from 'app/store/store'; import { navigateToExplore } from '../../explore/state/main'; @@ -295,7 +299,7 @@ export function getPanelMenu( } const { extensions } = getPluginExtensions({ - placement: GrafanaExtensions.DashboardPanelMenu, + placement: PluginExtensionPlacements.DashboardPanelMenu, context: createExtensionContext(panel, dashboard), }); diff --git a/public/app/features/plugins/extensions/placements.ts b/public/app/features/plugins/extensions/placements.ts deleted file mode 100644 index 27af2a0bc78..00000000000 --- a/public/app/features/plugins/extensions/placements.ts +++ /dev/null @@ -1,3 +0,0 @@ -export enum GrafanaExtensions { - DashboardPanelMenu = 'grafana/dashboard/panel/menu', -} From 93b32eec4b435005cb881e9008131f60782b8ec0 Mon Sep 17 00:00:00 2001 From: Virginia Cepeda Date: Fri, 10 Mar 2023 09:56:01 -0300 Subject: [PATCH 141/288] Alerting: fix users call 403 by calling /user instead of /users/{id} (#64544) Fetch user data with calling /user endpoint This avoids a permission error we were getting by calling /users/{id} --- public/app/features/alerting/unified/Analytics.test.ts | 8 ++++---- public/app/features/alerting/unified/Analytics.ts | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/public/app/features/alerting/unified/Analytics.test.ts b/public/app/features/alerting/unified/Analytics.test.ts index f884c8d40eb..bea69a3ade4 100644 --- a/public/app/features/alerting/unified/Analytics.test.ts +++ b/public/app/features/alerting/unified/Analytics.test.ts @@ -18,10 +18,10 @@ describe('isNewUser', function () { getBackendSrv().get = jest.fn().mockResolvedValue(newUser); - const isNew = await isNewUser(1); + const isNew = await isNewUser(); expect(isNew).toBe(true); expect(getBackendSrv().get).toHaveBeenCalledTimes(1); - expect(getBackendSrv().get).toHaveBeenCalledWith('/api/users/1'); + expect(getBackendSrv().get).toHaveBeenCalledWith('/api/user'); }); it('should return false if the user has been created prior to the last two weeks', async () => { @@ -32,9 +32,9 @@ describe('isNewUser', function () { getBackendSrv().get = jest.fn().mockResolvedValue(oldUser); - const isNew = await isNewUser(2); + const isNew = await isNewUser(); expect(isNew).toBe(false); expect(getBackendSrv().get).toHaveBeenCalledTimes(1); - expect(getBackendSrv().get).toHaveBeenCalledWith('/api/users/2'); + expect(getBackendSrv().get).toHaveBeenCalledWith('/api/user'); }); }); diff --git a/public/app/features/alerting/unified/Analytics.ts b/public/app/features/alerting/unified/Analytics.ts index ca8481e5136..da31b83ebf1 100644 --- a/public/app/features/alerting/unified/Analytics.ts +++ b/public/app/features/alerting/unified/Analytics.ts @@ -45,9 +45,9 @@ export function withPerformanceLogging Promise }; } -export async function isNewUser(userId: number) { +export async function isNewUser() { try { - const { createdAt } = await getBackendSrv().get(`/api/users/${userId}`); + const { createdAt } = await getBackendSrv().get(`/api/user`); const limitDateForNewUser = dateTime().subtract(USER_CREATION_MIN_DAYS, 'days'); const userCreationDate = dateTime(createdAt); @@ -61,7 +61,7 @@ export async function isNewUser(userId: number) { } export const trackNewAlerRuleFormSaved = async (props: AlertRuleTrackingProps) => { - const isNew = await isNewUser(props.user_id); + const isNew = await isNewUser(); if (isNew) { return; } @@ -69,7 +69,7 @@ export const trackNewAlerRuleFormSaved = async (props: AlertRuleTrackingProps) = }; export const trackNewAlerRuleFormCancelled = async (props: AlertRuleTrackingProps) => { - const isNew = await isNewUser(props.user_id); + const isNew = await isNewUser(); if (isNew) { return; } @@ -77,7 +77,7 @@ export const trackNewAlerRuleFormCancelled = async (props: AlertRuleTrackingProp }; export const trackNewAlerRuleFormError = async (props: AlertRuleTrackingProps & { error: string }) => { - const isNew = await isNewUser(props.user_id); + const isNew = await isNewUser(); if (isNew) { return; } From eb507dca899a2f9e5f7fde5d60dfa619282d6601 Mon Sep 17 00:00:00 2001 From: Carl Bergquist Date: Fri, 10 Mar 2023 13:57:29 +0100 Subject: [PATCH 142/288] Remotecache: rename setbytearray/getbytearray to set/get and remove codec (#64470) Signed-off-by: bergquist --- pkg/infra/remotecache/database_storage.go | 32 +---- .../remotecache/database_storage_test.go | 9 +- pkg/infra/remotecache/memcached_storage.go | 39 +----- pkg/infra/remotecache/redis_storage.go | 43 +------ pkg/infra/remotecache/remotecache.go | 114 +++++++----------- pkg/infra/remotecache/remotecache_test.go | 112 ++++++++++++----- pkg/middleware/middleware_test.go | 2 +- pkg/services/anonymous/anonimpl/impl.go | 2 +- pkg/services/auth/jwt/key_sets.go | 4 +- pkg/services/authn/clients/proxy.go | 8 +- pkg/services/authn/clients/proxy_test.go | 4 +- .../contexthandler/auth_proxy_test.go | 4 +- .../contexthandler/authproxy/authproxy.go | 6 +- .../authproxy/authproxy_test.go | 4 +- pkg/services/rendering/auth.go | 4 +- 15 files changed, 164 insertions(+), 223 deletions(-) diff --git a/pkg/infra/remotecache/database_storage.go b/pkg/infra/remotecache/database_storage.go index 2d2ed1d34ca..af834da3b61 100644 --- a/pkg/infra/remotecache/database_storage.go +++ b/pkg/infra/remotecache/database_storage.go @@ -14,14 +14,12 @@ const databaseCacheType = "database" type databaseCache struct { SQLStore db.DB - codec codec log log.Logger } -func newDatabaseCache(sqlstore db.DB, codec codec) *databaseCache { +func newDatabaseCache(sqlstore db.DB) *databaseCache { dc := &databaseCache{ SQLStore: sqlstore, - codec: codec, log: log.New("remotecache.database"), } @@ -54,7 +52,7 @@ func (dc *databaseCache) internalRunGC() { } } -func (dc *databaseCache) GetByteArray(ctx context.Context, key string) ([]byte, error) { +func (dc *databaseCache) Get(ctx context.Context, key string) ([]byte, error) { cacheHit := CacheData{} err := dc.SQLStore.WithDbSession(ctx, func(session *db.Session) error { @@ -85,21 +83,7 @@ func (dc *databaseCache) GetByteArray(ctx context.Context, key string) ([]byte, return cacheHit.Data, err } -func (dc *databaseCache) Get(ctx context.Context, key string) (interface{}, error) { - bytes, err := dc.GetByteArray(ctx, key) - if err != nil { - return nil, err - } - - item := &cachedItem{} - if err = dc.codec.Decode(ctx, bytes, item); err != nil { - return nil, err - } - - return item.Val, err -} - -func (dc *databaseCache) SetByteArray(ctx context.Context, key string, data []byte, expire time.Duration) error { +func (dc *databaseCache) Set(ctx context.Context, key string, data []byte, expire time.Duration) error { return dc.SQLStore.WithDbSession(ctx, func(session *db.Session) error { var expiresInSeconds int64 if expire != 0 { @@ -129,16 +113,6 @@ func (dc *databaseCache) SetByteArray(ctx context.Context, key string, data []by }) } -func (dc *databaseCache) Set(ctx context.Context, key string, value interface{}, expire time.Duration) error { - item := &cachedItem{Val: value} - data, err := dc.codec.Encode(ctx, item) - if err != nil { - return err - } - - return dc.SetByteArray(ctx, key, data, expire) -} - func (dc *databaseCache) Delete(ctx context.Context, key string) error { return dc.SQLStore.WithDbSession(ctx, func(session *db.Session) error { sql := "DELETE FROM cache_data WHERE cache_key=?" diff --git a/pkg/infra/remotecache/database_storage_test.go b/pkg/infra/remotecache/database_storage_test.go index 57769a05647..b3a64e79bb0 100644 --- a/pkg/infra/remotecache/database_storage_test.go +++ b/pkg/infra/remotecache/database_storage_test.go @@ -17,11 +17,10 @@ func TestDatabaseStorageGarbageCollection(t *testing.T) { db := &databaseCache{ SQLStore: sqlstore, - codec: &gobCodec{}, log: log.New("remotecache.database"), } - obj := &CacheableStruct{String: "foolbar"} + obj := []byte("foolbar") // set time.now to 2 weeks ago var err error @@ -66,11 +65,10 @@ func TestSecondSet(t *testing.T) { db := &databaseCache{ SQLStore: sqlstore, - codec: &gobCodec{}, log: log.New("remotecache.database"), } - obj := &CacheableStruct{String: "hey!"} + obj := []byte("hey!") err = db.Set(context.Background(), "killa-gorilla", obj, 0) assert.Equal(t, err, nil) @@ -84,11 +82,10 @@ func TestDatabaseStorageCount(t *testing.T) { db := &databaseCache{ SQLStore: sqlstore, - codec: &gobCodec{}, log: log.New("remotecache.database"), } - obj := &CacheableStruct{String: "foolbar"} + obj := []byte("foolbar") // set time.now to 2 weeks ago var err error diff --git a/pkg/infra/remotecache/memcached_storage.go b/pkg/infra/remotecache/memcached_storage.go index ef3ba900d12..6149c82245d 100644 --- a/pkg/infra/remotecache/memcached_storage.go +++ b/pkg/infra/remotecache/memcached_storage.go @@ -15,14 +15,12 @@ const memcachedCacheType = "memcached" var ErrNotImplemented = errors.New("not implemented") type memcachedStorage struct { - c *memcache.Client - codec codec + c *memcache.Client } -func newMemcachedStorage(opts *setting.RemoteCacheOptions, codec codec) *memcachedStorage { +func newMemcachedStorage(opts *setting.RemoteCacheOptions) *memcachedStorage { return &memcachedStorage{ - c: memcache.New(opts.ConnStr), - codec: codec, + c: memcache.New(opts.ConnStr), } } @@ -34,19 +32,8 @@ func newItem(sid string, data []byte, expire int32) *memcache.Item { } } -// Set sets value to given key in the cache. -func (s *memcachedStorage) Set(ctx context.Context, key string, val interface{}, expires time.Duration) error { - item := &cachedItem{Val: val} - bytes, err := s.codec.Encode(ctx, item) - if err != nil { - return err - } - - return s.SetByteArray(ctx, key, bytes, expires) -} - // SetByteArray stores an byte array in the cache -func (s *memcachedStorage) SetByteArray(ctx context.Context, key string, data []byte, expires time.Duration) error { +func (s *memcachedStorage) Set(ctx context.Context, key string, data []byte, expires time.Duration) error { var expiresInSeconds int64 if expires != 0 { expiresInSeconds = int64(expires) / int64(time.Second) @@ -56,24 +43,8 @@ func (s *memcachedStorage) SetByteArray(ctx context.Context, key string, data [] return s.c.Set(memcachedItem) } -// Get gets value by given key in the cache. -func (s *memcachedStorage) Get(ctx context.Context, key string) (interface{}, error) { - bytes, err := s.GetByteArray(ctx, key) - if err != nil { - return nil, err - } - - item := &cachedItem{} - err = s.codec.Decode(ctx, bytes, item) - if err != nil { - return nil, err - } - - return item.Val, nil -} - // GetByteArray returns the cached value as an byte array -func (s *memcachedStorage) GetByteArray(ctx context.Context, key string) ([]byte, error) { +func (s *memcachedStorage) Get(ctx context.Context, key string) ([]byte, error) { memcachedItem, err := s.c.Get(key) if errors.Is(err, memcache.ErrCacheMiss) { return nil, ErrCacheItemNotFound diff --git a/pkg/infra/remotecache/redis_storage.go b/pkg/infra/remotecache/redis_storage.go index 6d386b592f8..13684530525 100644 --- a/pkg/infra/remotecache/redis_storage.go +++ b/pkg/infra/remotecache/redis_storage.go @@ -16,8 +16,7 @@ import ( const redisCacheType = "redis" type redisStorage struct { - c *redis.Client - codec codec + c *redis.Client } // parseRedisConnStr parses k=v pairs in csv and builds a redis Options object @@ -78,54 +77,22 @@ func parseRedisConnStr(connStr string) (*redis.Options, error) { return options, nil } -func newRedisStorage(opts *setting.RemoteCacheOptions, codec codec) (*redisStorage, error) { +func newRedisStorage(opts *setting.RemoteCacheOptions) (*redisStorage, error) { opt, err := parseRedisConnStr(opts.ConnStr) if err != nil { return nil, err } - return &redisStorage{c: redis.NewClient(opt), codec: codec}, nil -} - -// Set sets value to given key in session. -func (s *redisStorage) Set(ctx context.Context, key string, val interface{}, expires time.Duration) error { - item := &cachedItem{Val: val} - value, err := s.codec.Encode(ctx, item) - if err != nil { - return err - } - - return s.SetByteArray(ctx, key, value, expires) + return &redisStorage{c: redis.NewClient(opt)}, nil } // Set sets value to a given key -func (s *redisStorage) SetByteArray(ctx context.Context, key string, data []byte, expires time.Duration) error { +func (s *redisStorage) Set(ctx context.Context, key string, data []byte, expires time.Duration) error { status := s.c.Set(ctx, key, data, expires) return status.Err() } -// Get gets value by given key in session. -func (s *redisStorage) Get(ctx context.Context, key string) (interface{}, error) { - v, err := s.GetByteArray(ctx, key) - - if err != nil { - if err.Error() == "EOF" { - return nil, ErrCacheItemNotFound - } - return nil, err - } - - item := &cachedItem{} - err = s.codec.Decode(ctx, v, item) - - if err == nil { - return item.Val, nil - } - - return nil, err -} - // GetByteArray returns the value as byte array -func (s *redisStorage) GetByteArray(ctx context.Context, key string) ([]byte, error) { +func (s *redisStorage) Get(ctx context.Context, key string) ([]byte, error) { return s.c.Get(ctx, key).Bytes() } diff --git a/pkg/infra/remotecache/remotecache.go b/pkg/infra/remotecache/remotecache.go index 6ecf712e586..039b962d087 100644 --- a/pkg/infra/remotecache/remotecache.go +++ b/pkg/infra/remotecache/remotecache.go @@ -1,9 +1,7 @@ package remotecache import ( - "bytes" "context" - "encoding/gob" "errors" "time" @@ -30,13 +28,7 @@ const ( func ProvideService(cfg *setting.Cfg, sqlStore db.DB, usageStats usagestats.Service, secretsService secrets.Service) (*RemoteCache, error) { - var codec codec - if cfg.RemoteCacheOptions.Encryption { - codec = &encryptionCodec{secretsService} - } else { - codec = &gobCodec{} - } - client, err := createClient(cfg.RemoteCacheOptions, sqlStore, codec) + client, err := createClient(cfg.RemoteCacheOptions, sqlStore) if err != nil { return nil, err } @@ -69,11 +61,11 @@ func (ds *RemoteCache) getUsageStats(ctx context.Context) (map[string]interface{ // so any struct added to the cache needs to be registered with `remotecache.Register` // ex `remotecache.Register(CacheableStruct{})` type CacheStorage interface { - // GetByteArray gets the cache value as an byte array - GetByteArray(ctx context.Context, key string) ([]byte, error) + // Get gets the cache value as an byte array + Get(ctx context.Context, key string) ([]byte, error) - // SetByteArray saves the value as an byte array. if `expire` is set to zero it will default to 24h - SetByteArray(ctx context.Context, key string, value []byte, expire time.Duration) error + // Set saves the value as an byte array. if `expire` is set to zero it will default to 24h + Set(ctx context.Context, key string, value []byte, expire time.Duration) error // Delete object from cache Delete(ctx context.Context, key string) error @@ -91,18 +83,18 @@ type RemoteCache struct { Cfg *setting.Cfg } -// GetByteArray returns the cached value as an byte array -func (ds *RemoteCache) GetByteArray(ctx context.Context, key string) ([]byte, error) { - return ds.client.GetByteArray(ctx, key) +// Get returns the cached value as an byte array +func (ds *RemoteCache) Get(ctx context.Context, key string) ([]byte, error) { + return ds.client.Get(ctx, key) } -// SetByteArray stored the byte array in the cache -func (ds *RemoteCache) SetByteArray(ctx context.Context, key string, value []byte, expire time.Duration) error { +// Set stored the byte array in the cache +func (ds *RemoteCache) Set(ctx context.Context, key string, value []byte, expire time.Duration) error { if expire == 0 { expire = defaultMaxCacheExpiration } - return ds.client.SetByteArray(ctx, key, value, expire) + return ds.client.Set(ctx, key, value, expire) } // Delete object from cache @@ -127,14 +119,14 @@ func (ds *RemoteCache) Run(ctx context.Context) error { return ctx.Err() } -func createClient(opts *setting.RemoteCacheOptions, sqlstore db.DB, codec codec) (cache CacheStorage, err error) { +func createClient(opts *setting.RemoteCacheOptions, sqlstore db.DB) (cache CacheStorage, err error) { switch opts.Name { case redisCacheType: - cache, err = newRedisStorage(opts, codec) + cache, err = newRedisStorage(opts) case memcachedCacheType: - cache = newMemcachedStorage(opts, codec) + cache = newMemcachedStorage(opts) case databaseCacheType: - cache = newDatabaseCache(sqlstore, codec) + cache = newDatabaseCache(sqlstore) default: return nil, ErrInvalidCacheType } @@ -144,61 +136,45 @@ func createClient(opts *setting.RemoteCacheOptions, sqlstore db.DB, codec codec) if opts.Prefix != "" { cache = &prefixCacheStorage{cache: cache, prefix: opts.Prefix} } + + if opts.Encryption { + cache = &encryptedCacheStorage{cache: cache} + } return cache, nil } -// Register records a type, identified by a value for that type, under its -// internal type name. That name will identify the concrete type of a value -// sent or received as an interface variable. Only types that will be -// transferred as implementations of interface values need to be registered. -// Expecting to be used only during initialization, it panics if the mapping -// between types and names is not a bijection. -func Register(value interface{}) { - gob.Register(value) +type encryptedCacheStorage struct { + cache CacheStorage + secretsService encryptionService } -type cachedItem struct { - Val interface{} +type encryptionService interface { + Encrypt(ctx context.Context, payload []byte, opt secrets.EncryptionOptions) ([]byte, error) + Decrypt(ctx context.Context, payload []byte) ([]byte, error) } -type codec interface { - Encode(context.Context, *cachedItem) ([]byte, error) - Decode(context.Context, []byte, *cachedItem) error -} - -type gobCodec struct{} - -func (c *gobCodec) Encode(_ context.Context, item *cachedItem) ([]byte, error) { - buf := bytes.NewBuffer(nil) - err := gob.NewEncoder(buf).Encode(item) - return buf.Bytes(), err -} - -func (c *gobCodec) Decode(_ context.Context, data []byte, out *cachedItem) error { - buf := bytes.NewBuffer(data) - return gob.NewDecoder(buf).Decode(&out) -} - -type encryptionCodec struct { - secretsService secrets.Service -} - -func (c *encryptionCodec) Encode(ctx context.Context, item *cachedItem) ([]byte, error) { - buf := bytes.NewBuffer(nil) - err := gob.NewEncoder(buf).Encode(item) +func (pcs *encryptedCacheStorage) Get(ctx context.Context, key string) ([]byte, error) { + data, err := pcs.cache.Get(ctx, key) if err != nil { return nil, err } - return c.secretsService.Encrypt(ctx, buf.Bytes(), secrets.WithoutScope()) -} -func (c *encryptionCodec) Decode(ctx context.Context, data []byte, out *cachedItem) error { - decrypted, err := c.secretsService.Decrypt(ctx, data) + return pcs.secretsService.Decrypt(ctx, data) +} +func (pcs *encryptedCacheStorage) Set(ctx context.Context, key string, value []byte, expire time.Duration) error { + encrypted, err := pcs.secretsService.Encrypt(ctx, value, secrets.WithoutScope()) if err != nil { return err } - buf := bytes.NewBuffer(decrypted) - return gob.NewDecoder(buf).Decode(&out) + + return pcs.cache.Set(ctx, key, encrypted, expire) +} +func (pcs *encryptedCacheStorage) Delete(ctx context.Context, key string) error { + return pcs.cache.Delete(ctx, key) +} + +func (pcs *encryptedCacheStorage) Count(ctx context.Context, prefix string) (int64, error) { + return pcs.cache.Count(ctx, prefix) } type prefixCacheStorage struct { @@ -206,16 +182,16 @@ type prefixCacheStorage struct { prefix string } -func (pcs *prefixCacheStorage) GetByteArray(ctx context.Context, key string) ([]byte, error) { - return pcs.cache.GetByteArray(ctx, pcs.prefix+key) +func (pcs *prefixCacheStorage) Get(ctx context.Context, key string) ([]byte, error) { + return pcs.cache.Get(ctx, pcs.prefix+key) } -func (pcs *prefixCacheStorage) SetByteArray(ctx context.Context, key string, value []byte, expire time.Duration) error { - return pcs.cache.SetByteArray(ctx, pcs.prefix+key, value, expire) +func (pcs *prefixCacheStorage) Set(ctx context.Context, key string, value []byte, expire time.Duration) error { + return pcs.cache.Set(ctx, pcs.prefix+key, value, expire) } func (pcs *prefixCacheStorage) Delete(ctx context.Context, key string) error { return pcs.cache.Delete(ctx, pcs.prefix+key) } func (pcs *prefixCacheStorage) Count(ctx context.Context, prefix string) (int64, error) { - return pcs.cache.Count(ctx, pcs.prefix) + return pcs.cache.Count(ctx, pcs.prefix+prefix) } diff --git a/pkg/infra/remotecache/remotecache_test.go b/pkg/infra/remotecache/remotecache_test.go index 585154af647..7c99f3d6a90 100644 --- a/pkg/infra/remotecache/remotecache_test.go +++ b/pkg/infra/remotecache/remotecache_test.go @@ -9,21 +9,12 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/usagestats" + "github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/services/secrets/fakes" "github.com/grafana/grafana/pkg/setting" ) -type CacheableStruct struct { - String string - Int64 int64 -} - -func init() { - Register(CacheableStruct{}) -} - func createTestClient(t *testing.T, opts *setting.RemoteCacheOptions, sqlstore db.DB) CacheStorage { t.Helper() @@ -49,7 +40,7 @@ func TestCachedBasedOnConfig(t *testing.T) { } func TestInvalidCacheTypeReturnsError(t *testing.T) { - _, err := createClient(&setting.RemoteCacheOptions{Name: "invalid"}, nil, &gobCodec{}) + _, err := createClient(&setting.RemoteCacheOptions{Name: "invalid"}, nil) assert.Equal(t, err, ErrInvalidCacheType) } @@ -68,13 +59,13 @@ func runCountTestsForClient(t *testing.T, opts *setting.RemoteCacheOptions, sqls t.Run("can count items", func(t *testing.T) { cacheableValue := []byte("hej hej") - err := client.SetByteArray(context.Background(), "pref-key1", cacheableValue, 0) + err := client.Set(context.Background(), "pref-key1", cacheableValue, 0) require.NoError(t, err) - err = client.SetByteArray(context.Background(), "pref-key2", cacheableValue, 0) + err = client.Set(context.Background(), "pref-key2", cacheableValue, 0) require.NoError(t, err) - err = client.SetByteArray(context.Background(), "key3-not-pref", cacheableValue, 0) + err = client.Set(context.Background(), "key3-not-pref", cacheableValue, 0) require.NoError(t, err) n, errC := client.Count(context.Background(), "pref-") @@ -92,10 +83,10 @@ func runCountTestsForClient(t *testing.T, opts *setting.RemoteCacheOptions, sqls func canPutGetAndDeleteCachedObjects(t *testing.T, client CacheStorage) { dataToCache := []byte("some bytes") - err := client.SetByteArray(context.Background(), "key1", dataToCache, 0) + err := client.Set(context.Background(), "key1", dataToCache, 0) assert.Equal(t, err, nil, "expected nil. got: ", err) - data, err := client.GetByteArray(context.Background(), "key1") + data, err := client.Get(context.Background(), "key1") assert.Equal(t, err, nil) assert.Equal(t, string(data), "some bytes") @@ -103,21 +94,21 @@ func canPutGetAndDeleteCachedObjects(t *testing.T, client CacheStorage) { err = client.Delete(context.Background(), "key1") assert.Equal(t, err, nil) - _, err = client.GetByteArray(context.Background(), "key1") + _, err = client.Get(context.Background(), "key1") assert.Equal(t, err, ErrCacheItemNotFound) } func canNotFetchExpiredItems(t *testing.T, client CacheStorage) { dataToCache := []byte("some bytes") - err := client.SetByteArray(context.Background(), "key1", dataToCache, time.Second) + err := client.Set(context.Background(), "key1", dataToCache, time.Second) assert.Equal(t, err, nil) // not sure how this can be avoided when testing redis/memcached :/ <-time.After(time.Second + time.Millisecond) // should not be able to read that value since its expired - _, err = client.GetByteArray(context.Background(), "key1") + _, err = client.Get(context.Background(), "key1") assert.Equal(t, err, ErrCacheItemNotFound) } @@ -140,26 +131,91 @@ func TestCollectUsageStats(t *testing.T) { } func TestCachePrefix(t *testing.T) { - db := db.InitTestDB(t) - cache := &databaseCache{ - SQLStore: db, - log: log.New("remotecache.database"), - codec: &gobCodec{}, - } + cache := NewFakeCacheStorage() prefixCache := &prefixCacheStorage{cache: cache, prefix: "test/"} // Set a value (with a prefix) - err := prefixCache.SetByteArray(context.Background(), "foo", []byte("bar"), time.Hour) + err := prefixCache.Set(context.Background(), "foo", []byte("bar"), time.Hour) require.NoError(t, err) // Get a value (with a prefix) - v, err := prefixCache.GetByteArray(context.Background(), "foo") + v, err := prefixCache.Get(context.Background(), "foo") require.NoError(t, err) require.Equal(t, "bar", string(v)) // Get a value directly from the underlying cache, ensure the prefix is in the key - v, err = cache.GetByteArray(context.Background(), "test/foo") + v, err = cache.Get(context.Background(), "test/foo") require.NoError(t, err) require.Equal(t, "bar", string(v)) // Get a value directly from the underlying cache without a prefix, should not be there _, err = cache.Get(context.Background(), "foo") require.Error(t, err) } + +func TestEncryptedCache(t *testing.T) { + cache := NewFakeCacheStorage() + encryptedCache := &encryptedCacheStorage{cache: cache, secretsService: &fakeSecretsService{}} + + // Set a value in the encrypted cache + err := encryptedCache.Set(context.Background(), "foo", []byte("bar"), time.Hour) + require.NoError(t, err) + + // make sure the stored value is not equal to input + v, err := cache.Get(context.Background(), "foo") + require.NoError(t, err) + require.NotEqual(t, "bar", string(v)) + + // make sure the returned value is the same as orignial + v, err = encryptedCache.Get(context.Background(), "foo") + require.NoError(t, err) + require.Equal(t, "bar", string(v)) +} + +type fakeCacheStorage struct { + storage map[string][]byte +} + +func (fcs fakeCacheStorage) Set(_ context.Context, key string, value []byte, exp time.Duration) error { + fcs.storage[key] = value + return nil +} + +func (fcs fakeCacheStorage) Get(_ context.Context, key string) ([]byte, error) { + value, exist := fcs.storage[key] + if !exist { + return nil, ErrCacheItemNotFound + } + + return value, nil +} + +func (fcs fakeCacheStorage) Delete(_ context.Context, key string) error { + delete(fcs.storage, key) + return nil +} + +func (fcs fakeCacheStorage) Count(_ context.Context, prefix string) (int64, error) { + return int64(len(fcs.storage)), nil +} + +func NewFakeCacheStorage() CacheStorage { + return fakeCacheStorage{ + storage: map[string][]byte{}, + } +} + +type fakeSecretsService struct{} + +func (f fakeSecretsService) Encrypt(_ context.Context, payload []byte, _ secrets.EncryptionOptions) ([]byte, error) { + return f.reverse(payload), nil +} + +func (f fakeSecretsService) Decrypt(_ context.Context, payload []byte) ([]byte, error) { + return f.reverse(payload), nil +} + +func (f fakeSecretsService) reverse(input []byte) []byte { + r := []rune(string(input)) + for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 { + r[i], r[j] = r[j], r[i] + } + return []byte(string(r)) +} diff --git a/pkg/middleware/middleware_test.go b/pkg/middleware/middleware_test.go index 0331a31b82e..fcbe9edc190 100644 --- a/pkg/middleware/middleware_test.go +++ b/pkg/middleware/middleware_test.go @@ -615,7 +615,7 @@ func TestMiddlewareContext(t *testing.T) { require.NoError(t, err) key := fmt.Sprintf(authproxy.CachePrefix, h) userIdBytes := []byte(strconv.FormatInt(userID, 10)) - err = sc.remoteCacheService.SetByteArray(context.Background(), key, userIdBytes, 0) + err = sc.remoteCacheService.Set(context.Background(), key, userIdBytes, 0) require.NoError(t, err) sc.fakeReq("GET", "/") diff --git a/pkg/services/anonymous/anonimpl/impl.go b/pkg/services/anonymous/anonimpl/impl.go index c6677086e49..df055628821 100644 --- a/pkg/services/anonymous/anonimpl/impl.go +++ b/pkg/services/anonymous/anonimpl/impl.go @@ -96,5 +96,5 @@ func (a *AnonSessionService) TagSession(ctx context.Context, httpReq *http.Reque a.localCache.SetDefault(key, struct{}{}) - return a.remoteCache.SetByteArray(ctx, key, []byte(key), thirtyDays) + return a.remoteCache.Set(ctx, key, []byte(key), thirtyDays) } diff --git a/pkg/services/auth/jwt/key_sets.go b/pkg/services/auth/jwt/key_sets.go index 6fc366191f0..05b746f4ce0 100644 --- a/pkg/services/auth/jwt/key_sets.go +++ b/pkg/services/auth/jwt/key_sets.go @@ -171,7 +171,7 @@ func (ks *keySetHTTP) getJWKS(ctx context.Context) (keySetJWKS, error) { var jwks keySetJWKS if ks.cacheExpiration > 0 { - if val, err := ks.cache.GetByteArray(ctx, ks.cacheKey); err == nil { + if val, err := ks.cache.Get(ctx, ks.cacheKey); err == nil { err := json.Unmarshal(val, &jwks) return jwks, err } @@ -200,7 +200,7 @@ func (ks *keySetHTTP) getJWKS(ctx context.Context) (keySetJWKS, error) { } if ks.cacheExpiration > 0 { - err = ks.cache.SetByteArray(ctx, ks.cacheKey, jsonBuf.Bytes(), ks.cacheExpiration) + err = ks.cache.Set(ctx, ks.cacheKey, jsonBuf.Bytes(), ks.cacheExpiration) } return jwks, err } diff --git a/pkg/services/authn/clients/proxy.go b/pkg/services/authn/clients/proxy.go index a53fb754edb..c7384eb898b 100644 --- a/pkg/services/authn/clients/proxy.go +++ b/pkg/services/authn/clients/proxy.go @@ -50,8 +50,8 @@ func ProvideProxy(cfg *setting.Cfg, cache proxyCache, userSrv user.Service, clie } type proxyCache interface { - GetByteArray(ctx context.Context, key string) ([]byte, error) - SetByteArray(ctx context.Context, key string, value []byte, expire time.Duration) error + Get(ctx context.Context, key string) ([]byte, error) + Set(ctx context.Context, key string, value []byte, expire time.Duration) error } type Proxy struct { @@ -83,7 +83,7 @@ func (c *Proxy) Authenticate(ctx context.Context, r *authn.Request) (*authn.Iden if ok { // See if we have cached the user id, in that case we can fetch the signed-in user and skip sync. // Error here means that we could not find anything in cache, so we can proceed as usual - if entry, err := c.cache.GetByteArray(ctx, cacheKey); err == nil { + if entry, err := c.cache.Get(ctx, cacheKey); err == nil { uid := int64(binary.LittleEndian.Uint64(entry)) usr, err := c.userSrv.GetSignedInUserWithCacheCtx(ctx, &user.GetSignedInUserQuery{ @@ -138,7 +138,7 @@ func (c *Proxy) Hook(ctx context.Context, identity *authn.Identity, r *authn.Req c.log.FromContext(ctx).Debug("Cache proxy user", "userId", id) bytes := make([]byte, 8) binary.LittleEndian.PutUint64(bytes, uint64(id)) - if err := c.cache.SetByteArray(ctx, identity.ClientParams.CacheAuthProxyKey, bytes, time.Duration(c.cfg.AuthProxySyncTTL)*time.Minute); err != nil { + if err := c.cache.Set(ctx, identity.ClientParams.CacheAuthProxyKey, bytes, time.Duration(c.cfg.AuthProxySyncTTL)*time.Minute); err != nil { c.log.Warn("failed to cache proxy user", "error", err, "userId", id) } diff --git a/pkg/services/authn/clients/proxy_test.go b/pkg/services/authn/clients/proxy_test.go index a0f39ec0425..a0b19dc1a0d 100644 --- a/pkg/services/authn/clients/proxy_test.go +++ b/pkg/services/authn/clients/proxy_test.go @@ -181,10 +181,10 @@ type fakeCache struct { expectedItem []byte } -func (f fakeCache) GetByteArray(ctx context.Context, key string) ([]byte, error) { +func (f fakeCache) Get(ctx context.Context, key string) ([]byte, error) { return f.expectedItem, f.expectedErr } -func (f fakeCache) SetByteArray(ctx context.Context, key string, value []byte, expire time.Duration) error { +func (f fakeCache) Set(ctx context.Context, key string, value []byte, expire time.Duration) error { return f.expectedErr } diff --git a/pkg/services/contexthandler/auth_proxy_test.go b/pkg/services/contexthandler/auth_proxy_test.go index 2a233b97b62..124fa98a980 100644 --- a/pkg/services/contexthandler/auth_proxy_test.go +++ b/pkg/services/contexthandler/auth_proxy_test.go @@ -58,7 +58,7 @@ func TestInitContextWithAuthProxy_CachedInvalidUserID(t *testing.T) { t.Logf("Injecting stale user ID in cache with key %q", key) userIdPayload := []byte(strconv.FormatInt(int64(33), 10)) - err = svc.RemoteCache.SetByteArray(context.Background(), key, userIdPayload, 0) + err = svc.RemoteCache.Set(context.Background(), key, userIdPayload, 0) require.NoError(t, err) authEnabled := svc.initContextWithAuthProxy(ctx, orgID) @@ -67,7 +67,7 @@ func TestInitContextWithAuthProxy_CachedInvalidUserID(t *testing.T) { require.Equal(t, userID, ctx.SignedInUser.UserID) require.True(t, ctx.IsSignedIn) - cachedByteArray, err := svc.RemoteCache.GetByteArray(context.Background(), key) + cachedByteArray, err := svc.RemoteCache.Get(context.Background(), key) require.NoError(t, err) cacheUserId, err := strconv.ParseInt(string(cachedByteArray), 10, 64) diff --git a/pkg/services/contexthandler/authproxy/authproxy.go b/pkg/services/contexthandler/authproxy/authproxy.go index 847ae84c0b9..5f42274e6c7 100644 --- a/pkg/services/contexthandler/authproxy/authproxy.go +++ b/pkg/services/contexthandler/authproxy/authproxy.go @@ -191,7 +191,7 @@ func (auth *AuthProxy) getUserViaCache(reqCtx *contextmodel.ReqContext) (int64, return 0, err } auth.logger.Debug("Getting user ID via auth cache", "cacheKey", cacheKey) - cachedValue, err := auth.remoteCache.GetByteArray(reqCtx.Req.Context(), cacheKey) + cachedValue, err := auth.remoteCache.Get(reqCtx.Req.Context(), cacheKey) if err != nil { return 0, err } @@ -353,7 +353,7 @@ func (auth *AuthProxy) Remember(reqCtx *contextmodel.ReqContext, id int64) error } // Check if user already in cache - cachedValue, err := auth.remoteCache.GetByteArray(reqCtx.Req.Context(), key) + cachedValue, err := auth.remoteCache.Get(reqCtx.Req.Context(), key) if err == nil && len(cachedValue) != 0 { return nil } @@ -361,7 +361,7 @@ func (auth *AuthProxy) Remember(reqCtx *contextmodel.ReqContext, id int64) error expiration := time.Duration(auth.cfg.AuthProxySyncTTL) * time.Minute userIdPayload := []byte(strconv.FormatInt(id, 10)) - if err := auth.remoteCache.SetByteArray(reqCtx.Req.Context(), key, userIdPayload, expiration); err != nil { + if err := auth.remoteCache.Set(reqCtx.Req.Context(), key, userIdPayload, expiration); err != nil { return err } diff --git a/pkg/services/contexthandler/authproxy/authproxy_test.go b/pkg/services/contexthandler/authproxy/authproxy_test.go index 361d0972a5c..57f3f55b170 100644 --- a/pkg/services/contexthandler/authproxy/authproxy_test.go +++ b/pkg/services/contexthandler/authproxy/authproxy_test.go @@ -61,7 +61,7 @@ func TestMiddlewareContext(t *testing.T) { require.NoError(t, err) key := fmt.Sprintf(CachePrefix, h) userIdPayload := []byte(strconv.FormatInt(id, 10)) - err = cache.SetByteArray(context.Background(), key, userIdPayload, 0) + err = cache.Set(context.Background(), key, userIdPayload, 0) require.NoError(t, err) // Set up the middleware auth, reqCtx := prepareMiddleware(t, cache, nil) @@ -84,7 +84,7 @@ func TestMiddlewareContext(t *testing.T) { require.NoError(t, err) key := fmt.Sprintf(CachePrefix, h) userIdPayload := []byte(strconv.FormatInt(id, 10)) - err = cache.SetByteArray(context.Background(), key, userIdPayload, 0) + err = cache.Set(context.Background(), key, userIdPayload, 0) require.NoError(t, err) auth, reqCtx := prepareMiddleware(t, cache, func(req *http.Request, cfg *setting.Cfg) { diff --git a/pkg/services/rendering/auth.go b/pkg/services/rendering/auth.go index 307430333a8..32ef4e26d79 100644 --- a/pkg/services/rendering/auth.go +++ b/pkg/services/rendering/auth.go @@ -21,7 +21,7 @@ type RenderUser struct { } func (rs *RenderingService) GetRenderUser(ctx context.Context, key string) (*RenderUser, bool) { - val, err := rs.RemoteCacheService.GetByteArray(ctx, fmt.Sprintf(renderKeyPrefix, key)) + val, err := rs.RemoteCacheService.Get(ctx, fmt.Sprintf(renderKeyPrefix, key)) if err != nil { rs.log.Error("Failed to get render key from cache", "error", err) } @@ -46,7 +46,7 @@ func setRenderKey(cache *remotecache.RemoteCache, ctx context.Context, opts Auth return err } - return cache.SetByteArray(ctx, fmt.Sprintf(renderKeyPrefix, renderKey), buf.Bytes(), expiry) + return cache.Set(ctx, fmt.Sprintf(renderKeyPrefix, renderKey), buf.Bytes(), expiry) } func generateAndSetRenderKey(cache *remotecache.RemoteCache, ctx context.Context, opts AuthOpts, expiry time.Duration) (string, error) { From a8f201f8ab1a5500671a1f70a582c701fbc94fc7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 10 Mar 2023 14:00:25 +0100 Subject: [PATCH 143/288] Update dependency rimraf to v4.4.0 (#64601) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- packages/grafana-data/package.json | 2 +- packages/grafana-e2e-selectors/package.json | 2 +- packages/grafana-e2e/package.json | 2 +- packages/grafana-runtime/package.json | 2 +- packages/grafana-schema/package.json | 2 +- packages/grafana-ui/package.json | 2 +- yarn.lock | 22 ++++++++++----------- 8 files changed, 18 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index a696c38a518..9028fbb7caf 100644 --- a/package.json +++ b/package.json @@ -229,7 +229,7 @@ "react-simple-compat": "1.2.3", "react-test-renderer": "17.0.2", "redux-mock-store": "1.5.4", - "rimraf": "4.2.0", + "rimraf": "4.4.0", "rudder-sdk-js": "2.25.0", "sass": "1.58.3", "sass-loader": "13.2.0", diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index 9d3d21e7ce8..ff16037a19c 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -83,7 +83,7 @@ "react": "17.0.2", "react-dom": "17.0.2", "react-test-renderer": "17.0.2", - "rimraf": "4.2.0", + "rimraf": "4.4.0", "rollup": "2.79.1", "rollup-plugin-dts": "^5.0.0", "rollup-plugin-esbuild": "5.0.0", diff --git a/packages/grafana-e2e-selectors/package.json b/packages/grafana-e2e-selectors/package.json index 4ebfcd108ec..3035cfb0ba5 100644 --- a/packages/grafana-e2e-selectors/package.json +++ b/packages/grafana-e2e-selectors/package.json @@ -43,7 +43,7 @@ "@rollup/plugin-node-resolve": "15.0.1", "@types/node": "18.14.6", "esbuild": "0.16.17", - "rimraf": "4.2.0", + "rimraf": "4.4.0", "rollup": "2.79.1", "rollup-plugin-dts": "^5.0.0", "rollup-plugin-esbuild": "5.0.0", diff --git a/packages/grafana-e2e/package.json b/packages/grafana-e2e/package.json index 29b3996ffd3..6c720439698 100644 --- a/packages/grafana-e2e/package.json +++ b/packages/grafana-e2e/package.json @@ -77,7 +77,7 @@ "lodash": "4.17.21", "mocha": "10.2.0", "resolve-bin": "1.0.1", - "rimraf": "4.2.0", + "rimraf": "4.4.0", "tracelib": "1.0.1", "ts-loader": "8.4.0", "tslib": "2.5.0", diff --git a/packages/grafana-runtime/package.json b/packages/grafana-runtime/package.json index c775a7b7a39..0565cd2747b 100644 --- a/packages/grafana-runtime/package.json +++ b/packages/grafana-runtime/package.json @@ -67,7 +67,7 @@ "lodash": "4.17.21", "react": "17.0.2", "react-dom": "17.0.2", - "rimraf": "4.2.0", + "rimraf": "4.4.0", "rollup": "2.79.1", "rollup-plugin-dts": "^5.0.0", "rollup-plugin-esbuild": "5.0.0", diff --git a/packages/grafana-schema/package.json b/packages/grafana-schema/package.json index 1d8c6522569..542b6a1428a 100644 --- a/packages/grafana-schema/package.json +++ b/packages/grafana-schema/package.json @@ -41,7 +41,7 @@ "@rollup/plugin-json": "5.0.1", "@rollup/plugin-node-resolve": "15.0.1", "esbuild": "0.16.17", - "rimraf": "4.2.0", + "rimraf": "4.4.0", "rollup": "2.79.1", "rollup-plugin-dts": "^5.0.0", "rollup-plugin-esbuild": "5.0.0", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 2ad949e3324..c92286e62b4 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -175,7 +175,7 @@ "react": "17.0.2", "react-dom": "17.0.2", "react-test-renderer": "17.0.2", - "rimraf": "4.2.0", + "rimraf": "4.4.0", "rollup": "2.79.1", "rollup-plugin-dts": "^5.0.0", "rollup-plugin-esbuild": "5.0.0", diff --git a/yarn.lock b/yarn.lock index b72d883bfb7..c7d83ff4868 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4956,7 +4956,7 @@ __metadata: react-test-renderer: 17.0.2 react-use: 17.4.0 regenerator-runtime: 0.13.11 - rimraf: 4.2.0 + rimraf: 4.4.0 rollup: 2.79.1 rollup-plugin-dts: ^5.0.0 rollup-plugin-esbuild: 5.0.0 @@ -4983,7 +4983,7 @@ __metadata: "@rollup/plugin-node-resolve": 15.0.1 "@types/node": 18.14.6 esbuild: 0.16.17 - rimraf: 4.2.0 + rimraf: 4.4.0 rollup: 2.79.1 rollup-plugin-dts: ^5.0.0 rollup-plugin-esbuild: 5.0.0 @@ -5031,7 +5031,7 @@ __metadata: lodash: 4.17.21 mocha: 10.2.0 resolve-bin: 1.0.1 - rimraf: 4.2.0 + rimraf: 4.4.0 rollup: 2.79.1 rollup-plugin-dts: ^5.0.0 rollup-plugin-esbuild: 5.0.0 @@ -5190,7 +5190,7 @@ __metadata: lodash: 4.17.21 react: 17.0.2 react-dom: 17.0.2 - rimraf: 4.2.0 + rimraf: 4.4.0 rollup: 2.79.1 rollup-plugin-dts: ^5.0.0 rollup-plugin-esbuild: 5.0.0 @@ -5230,7 +5230,7 @@ __metadata: "@rollup/plugin-json": 5.0.1 "@rollup/plugin-node-resolve": 15.0.1 esbuild: 0.16.17 - rimraf: 4.2.0 + rimraf: 4.4.0 rollup: 2.79.1 rollup-plugin-dts: ^5.0.0 rollup-plugin-esbuild: 5.0.0 @@ -5462,7 +5462,7 @@ __metadata: react-transition-group: 4.4.5 react-use: 17.4.0 react-window: 1.8.8 - rimraf: 4.2.0 + rimraf: 4.4.0 rollup: 2.79.1 rollup-plugin-dts: ^5.0.0 rollup-plugin-esbuild: 5.0.0 @@ -22387,7 +22387,7 @@ __metadata: redux-thunk: 2.4.2 regenerator-runtime: 0.13.11 reselect: 4.1.7 - rimraf: 4.2.0 + rimraf: 4.4.0 rst2html: "github:thoward/rst2html#990cb89f2a300cdd9151790be377c4c0840df809" rudder-sdk-js: 2.25.0 rxjs: 7.8.0 @@ -34600,14 +34600,14 @@ __metadata: languageName: node linkType: hard -"rimraf@npm:4.2.0": - version: 4.2.0 - resolution: "rimraf@npm:4.2.0" +"rimraf@npm:4.4.0": + version: 4.4.0 + resolution: "rimraf@npm:4.4.0" dependencies: glob: ^9.2.0 bin: rimraf: dist/cjs/src/bin.js - checksum: 885baaec6d8e3a771bb3060f110c29070073a47fdcb26775ae165703173e79e845974dc8ed07e8eb4c3c85f126777968febdb385d0c274ad3b167db326e7f82e + checksum: 0cedaf9d138589d1bb0ab851f05804c6d30827aa66563472b04ab76245f83537e23e7b94f1f79ea6c368c0d84a18fcde6a756fca3a44c967e08792671b3a0a6e languageName: node linkType: hard From 2f55911fa3163818308bc4f5cb7002a918da0619 Mon Sep 17 00:00:00 2001 From: Joey <90795735+joey-grafana@users.noreply.github.com> Date: Fri, 10 Mar 2023 13:04:51 +0000 Subject: [PATCH 144/288] Fix: Top table rendering and update docs (#64497) Fix flame graph in test data and update docs --- docs/sources/datasources/testdata/_index.md | 2 ++ .../components/TopTable/FlameGraphTopTableContainer.tsx | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/sources/datasources/testdata/_index.md b/docs/sources/datasources/testdata/_index.md index d81f41d6f85..de1e9953e0d 100644 --- a/docs/sources/datasources/testdata/_index.md +++ b/docs/sources/datasources/testdata/_index.md @@ -60,6 +60,7 @@ You can assign an **Alias** to each scenario, and many have their own options th - **CSV Metric Values** - **Datapoints Outside Range** - **Exponential heatmap bucket data** +- **Flame Graph** - **Grafana API** - **Grafana Live** - **Linear heatmap bucket data** @@ -77,6 +78,7 @@ You can assign an **Alias** to each scenario, and many have their own options th - **Slow Query** - **Streaming Client** - **Table Static** +- **Trace** - **USA generated data** ## Import a pre-configured dashboard diff --git a/public/app/plugins/panel/flamegraph/components/TopTable/FlameGraphTopTableContainer.tsx b/public/app/plugins/panel/flamegraph/components/TopTable/FlameGraphTopTableContainer.tsx index 4b75ba35a2a..b3ac0f1cbf8 100644 --- a/public/app/plugins/panel/flamegraph/components/TopTable/FlameGraphTopTableContainer.tsx +++ b/public/app/plugins/panel/flamegraph/components/TopTable/FlameGraphTopTableContainer.tsx @@ -45,7 +45,7 @@ const FlameGraphTopTableContainer = ({ let label, self, value; let table: { [key: string]: TableData } = {}; - if (data.fields.length === 6) { + if (data.fields.length > 3) { const valueValues = data.fields[1].values; const selfValues = data.fields[2].values; const labelValues = data.fields[3].values; From a05cb1e78e77fefad7d474001f59b38819904b1f Mon Sep 17 00:00:00 2001 From: William Assis <35489495+gassiss@users.noreply.github.com> Date: Fri, 10 Mar 2023 08:18:12 -0500 Subject: [PATCH 145/288] Frontend: Fix broken links in /plugins when pathname has a trailing slash (#64348) Fix broken links in /plugins when pathname has a trailing slash --- public/app/features/plugins/admin/components/PluginList.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/features/plugins/admin/components/PluginList.tsx b/public/app/features/plugins/admin/components/PluginList.tsx index 9e12fbf3824..afa87074e14 100644 --- a/public/app/features/plugins/admin/components/PluginList.tsx +++ b/public/app/features/plugins/admin/components/PluginList.tsx @@ -18,8 +18,8 @@ interface Props { export const PluginList = ({ plugins, displayMode }: Props) => { const isList = displayMode === PluginListDisplayMode.List; const styles = useStyles2(getStyles); - const location = useLocation(); - const pathName = config.appSubUrl + location.pathname; + const { pathname } = useLocation(); + const pathName = config.appSubUrl + (pathname.endsWith('/') ? pathname.slice(0, -1) : pathname); return (
From 0a4d9f01e87493871b3ce9d0ab3665134f5c15a7 Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Fri, 10 Mar 2023 05:40:38 -0800 Subject: [PATCH 146/288] Chore: Use latest version of scenes (#64609) --- package.json | 2 +- yarn.lock | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 9028fbb7caf..c348fe55688 100644 --- a/package.json +++ b/package.json @@ -267,7 +267,7 @@ "@grafana/lezer-logql": "0.1.2", "@grafana/monaco-logql": "^0.0.7", "@grafana/runtime": "workspace:*", - "@grafana/scenes": "^0.0.16", + "@grafana/scenes": "^0.0.18", "@grafana/schema": "workspace:*", "@grafana/ui": "workspace:*", "@kusto/monaco-kusto": "5.3.6", diff --git a/yarn.lock b/yarn.lock index c7d83ff4868..a7c8bef8c1e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4993,14 +4993,14 @@ __metadata: languageName: unknown linkType: soft -"@grafana/e2e-selectors@npm:canary": - version: 9.4.0-96193pre - resolution: "@grafana/e2e-selectors@npm:9.4.0-96193pre" +"@grafana/e2e-selectors@npm:^9.4.3": + version: 9.4.3 + resolution: "@grafana/e2e-selectors@npm:9.4.3" dependencies: "@grafana/tsconfig": ^1.2.0-rc1 tslib: 2.4.1 typescript: 4.8.4 - checksum: 1ce663607c0c816ec02cad33d80c38731c4dfafdc7d1693baa989b8eff6588e3cc1d8a031c4f30f888bbd8aa8768cce7c5dd4cb2455306040f39dcac933ceae4 + checksum: 85a88cdf4adb643ff863b15f96fc6c04ecb7567c27cc526a00c157eb02575e55adc1e7701d58e1b48f00f24951c332fbb191cd2b5a8e74cd0c545543777e82af languageName: node linkType: hard @@ -5207,17 +5207,17 @@ __metadata: languageName: unknown linkType: soft -"@grafana/scenes@npm:^0.0.16": - version: 0.0.16 - resolution: "@grafana/scenes@npm:0.0.16" +"@grafana/scenes@npm:^0.0.18": + version: 0.0.18 + resolution: "@grafana/scenes@npm:0.0.18" dependencies: - "@grafana/e2e-selectors": canary + "@grafana/e2e-selectors": ^9.4.3 "@grafana/experimental": 1.0.1 react-grid-layout: 1.3.4 react-use: 17.4.0 react-virtualized-auto-sizer: 1.0.7 uuid: ^9.0.0 - checksum: c6cf9f1571309da4ed0fb5046f8abf18b324e9847890c54307ca093610760d52fdb44002fb1db8e81abca6371121a0623a2f44c9828ef80b11d8d189993f457a + checksum: f863cbf410d71e579cd4e343fb7b8ad014bc66a250f4d31f9d43c43fe8e97f5b6e3e8d01ed69818c347b3db31b6ecb153d1dc7a0e2946d360e7e1806eb6f5d8c languageName: node linkType: hard @@ -22130,7 +22130,7 @@ __metadata: "@grafana/lezer-logql": 0.1.2 "@grafana/monaco-logql": ^0.0.7 "@grafana/runtime": "workspace:*" - "@grafana/scenes": ^0.0.16 + "@grafana/scenes": ^0.0.18 "@grafana/schema": "workspace:*" "@grafana/toolkit": "workspace:*" "@grafana/tsconfig": ^1.2.0-rc1 From 73ce20ab48ff9e8e97ae021ba8da3457eee3402e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 10 Mar 2023 14:41:46 +0100 Subject: [PATCH 147/288] Table Panel: Add ability to use text color for value or hide value in gauge cell (#61477) * BarGauge: New value options * Fix typings for cell options, add new value mode option for bar gauge cells * Add BarGauge panel option, tests, and update test dashboard * Updated * Added default * Goodbye trusty console.log * Update * Merge changes from main * Update docs * Add valuemode doc changes * Update gdev dashboard * Update valueMode symbol name to valueDisplayMode * Use Enums as Opposed to literals, don't calculate values when hidden * Remove double import * Fix tests * One more test fix * Remove erroneous targets field, fix type of maxDataPoints * Strip nulls and add index field to Thresholds * Gen cue * remove bad targets again * Fixes --------- Co-authored-by: Kyle Cunningham Co-authored-by: sam boyer --- .../panel-bargauge/panel_tests_bar_gauge.json | 1382 ++++++++--- .../panel-table/table_tests_new.json | 2127 ++++++++--------- .../bargaugepanelcfg/schema-reference.md | 1 + .../kinds/core/dashboard/schema-reference.md | 11 +- .../visualizations/table/index.md | 16 + kinds/dashboard/dashboard_kind.cue | 6 +- .../grafana-schema/src/common/common.gen.ts | 10 + .../grafana-schema/src/common/mudball.cue | 3 + packages/grafana-schema/src/common/table.cue | 1 + .../raw/dashboard/x/dashboard_types.gen.ts | 4 + .../src/components/BarGauge/BarGauge.test.tsx | 53 +- .../src/components/BarGauge/BarGauge.tsx | 71 +- .../src/components/Table/BarGaugeCell.tsx | 30 +- pkg/kinds/dashboard/dashboard_types_gen.go | 3 + pkg/kindsys/report.json | 2 +- .../api/alerting/api_alertmanager_test.go | 3 +- .../panel/bargauge/BarGaugePanel.test.tsx | 3 +- .../plugins/panel/bargauge/BarGaugePanel.tsx | 1 + public/app/plugins/panel/bargauge/module.tsx | 14 +- .../app/plugins/panel/bargauge/panelcfg.cue | 1 + .../plugins/panel/bargauge/panelcfg.gen.ts | 2 + .../panel/table/TableCellOptionEditor.tsx | 17 +- .../table/cells/BarGaugeCellOptionsEditor.tsx | 54 +- .../ColorBackgroundCellOptionsEditor.tsx | 10 +- public/app/plugins/panel/table/module.tsx | 4 +- 25 files changed, 2259 insertions(+), 1570 deletions(-) diff --git a/devenv/dev-dashboards/panel-bargauge/panel_tests_bar_gauge.json b/devenv/dev-dashboards/panel-bargauge/panel_tests_bar_gauge.json index 6230bb6c305..9366c4dc238 100644 --- a/devenv/dev-dashboards/panel-bargauge/panel_tests_bar_gauge.json +++ b/devenv/dev-dashboards/panel-bargauge/panel_tests_bar_gauge.json @@ -3,24 +3,74 @@ "list": [ { "builtIn": 1, - "datasource": "-- Grafana --", + "datasource": { + "type": "datasource", + "uid": "grafana" + }, "enable": true, "hide": true, "iconColor": "rgba(0, 211, 255, 1)", "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, "type": "dashboard" } ] }, "editable": true, - "gnetId": null, + "fiscalYearStartMonth": 0, "graphTooltip": 0, "links": [], + "liveNow": false, "panels": [ { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "max": 100, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "blue", + "index": 0 + }, + { + "color": "green", + "index": 1, + "value": 20 + }, + { + "color": "orange", + "index": 2, + "value": 40 + }, + { + "color": "red", + "index": 3, + "value": 80 + } + ] + }, + "unit": "celsius" + }, + "overrides": [] + }, "gridPos": { "h": 7, - "w": 6, + "w": 9, "x": 0, "y": 0 }, @@ -28,789 +78,1333 @@ "links": [], "options": { "displayMode": "gradient", - "fieldOptions": { - "calcs": ["mean"], - "defaults": { - "decimals": null, - "max": 100, - "min": 0, - "unit": "celsius" - }, - "mappings": [], - "override": {}, - "thresholds": [ - { - "color": "blue", - "index": 0, - "value": null - }, - { - "color": "green", - "index": 1, - "value": 20 - }, - { - "color": "orange", - "index": 2, - "value": 40 - }, - { - "color": "red", - "index": 3, - "value": 80 - } + "minVizHeight": 10, + "minVizWidth": 0, + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "mean" ], + "fields": "", "values": false }, - "orientation": "horizontal" + "showUnfilled": true, + "valueMode": "color" }, - "pluginVersion": "6.2.0-pre", + "pluginVersion": "9.4.0-pre", "targets": [ { "alias": "Inside", + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "H", "scenarioId": "csv_metric_values", "stringInput": "100,100,100" }, { "alias": "Outhouse", + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "A", "scenarioId": "random_walk" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "F", "scenarioId": "random_walk" } ], - "timeFrom": null, - "timeShift": null, "title": "Title above bar", "type": "bargauge" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "max": 100, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "blue", + "index": 0 + }, + { + "color": "green", + "index": 1, + "value": 20 + }, + { + "color": "orange", + "index": 2, + "value": 40 + }, + { + "color": "red", + "index": 3, + "value": 80 + } + ] + }, + "unit": "celsius" + }, + "overrides": [] + }, "gridPos": { "h": 7, - "w": 5, - "x": 6, + "w": 8, + "x": 9, "y": 0 }, "id": 12, "links": [], "options": { "displayMode": "gradient", - "fieldOptions": { - "calcs": ["mean"], - "defaults": { - "decimals": null, - "max": 100, - "min": 0, - "unit": "celsius" - }, - "mappings": [], - "override": {}, - "thresholds": [ - { - "color": "blue", - "index": 0, - "value": null - }, - { - "color": "green", - "index": 1, - "value": 20 - }, - { - "color": "orange", - "index": 2, - "value": 40 - }, - { - "color": "red", - "index": 3, - "value": 80 - } + "minVizHeight": 10, + "minVizWidth": 0, + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "mean" ], + "fields": "", "values": false }, - "orientation": "horizontal" + "showUnfilled": true, + "valueMode": "color" }, - "pluginVersion": "6.2.0-pre", + "pluginVersion": "9.4.0-pre", "targets": [ { "alias": "Inside", + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "H", "scenarioId": "csv_metric_values", "stringInput": "100,100,100" }, { "alias": "Outhouse", + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "A", "scenarioId": "random_walk" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "F", "scenarioId": "random_walk" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "B", "scenarioId": "random_walk" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "C", "scenarioId": "random_walk" } ], - "timeFrom": null, - "timeShift": null, "title": "Title to left of bar", "type": "bargauge" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "max": 100, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "blue", + "index": 0 + }, + { + "color": "green", + "index": 1, + "value": 20 + }, + { + "color": "orange", + "index": 2, + "value": 40 + }, + { + "color": "red", + "index": 3, + "value": 80 + } + ] + }, + "unit": "celsius" + }, + "overrides": [] + }, "gridPos": { "h": 7, "w": 7, - "x": 11, + "x": 17, "y": 0 }, - "id": 13, + "id": 23, "links": [], "options": { - "displayMode": "basic", - "fieldOptions": { - "calcs": ["mean"], - "defaults": { - "decimals": null, - "max": 100, - "min": 0, - "unit": "celsius" - }, - "mappings": [], - "override": {}, - "thresholds": [ - { - "color": "blue", - "index": 0, - "value": null - }, - { - "color": "green", - "index": 1, - "value": 20 - }, - { - "color": "orange", - "index": 2, - "value": 40 - }, - { - "color": "red", - "index": 3, - "value": 80 - } + "displayMode": "gradient", + "minVizHeight": 10, + "minVizWidth": 0, + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "mean" ], + "fields": "", "values": false }, - "orientation": "horizontal" + "showUnfilled": true, + "valueMode": "hidden" }, - "pluginVersion": "6.2.0-pre", + "pluginVersion": "9.4.0-pre", "targets": [ { "alias": "Inside", + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "H", "scenarioId": "csv_metric_values", "stringInput": "100,100,100" }, { "alias": "Outhouse", + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "A", "scenarioId": "random_walk" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "F", "scenarioId": "random_walk" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "B", "scenarioId": "random_walk" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "C", "scenarioId": "random_walk" } ], - "timeFrom": null, - "timeShift": null, - "title": "Basic mode", + "title": "Gradient hidden value", "type": "bargauge" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "max": 100, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "blue", + "index": 0 + }, + { + "color": "green", + "index": 1, + "value": 20 + }, + { + "color": "orange", + "index": 2, + "value": 40 + }, + { + "color": "red", + "index": 3, + "value": 80 + } + ] + }, + "unit": "celsius" + }, + "overrides": [] + }, "gridPos": { "h": 7, - "w": 6, - "x": 18, - "y": 0 + "w": 9, + "x": 0, + "y": 7 + }, + "id": 21, + "links": [], + "options": { + "displayMode": "basic", + "minVizHeight": 10, + "minVizWidth": 0, + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "mean" + ], + "fields": "", + "values": false + }, + "showUnfilled": true, + "valueMode": "text" + }, + "pluginVersion": "9.4.0-pre", + "targets": [ + { + "alias": "Inside", + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "H", + "scenarioId": "csv_metric_values", + "stringInput": "100,100,100" + }, + { + "alias": "Outhouse", + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "A", + "scenarioId": "random_walk" + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "F", + "scenarioId": "random_walk" + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "B", + "scenarioId": "random_walk" + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "C", + "scenarioId": "random_walk" + } + ], + "title": "Basic mode + text color", + "type": "bargauge" + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "max": 100, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "blue", + "index": 0 + }, + { + "color": "green", + "index": 1, + "value": 20 + }, + { + "color": "orange", + "index": 2, + "value": 40 + }, + { + "color": "red", + "index": 3, + "value": 80 + } + ] + }, + "unit": "celsius" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 9, + "y": 7 + }, + "id": 13, + "links": [], + "options": { + "displayMode": "gradient", + "minVizHeight": 10, + "minVizWidth": 0, + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "mean" + ], + "fields": "", + "values": false + }, + "showUnfilled": true, + "valueMode": "text" + }, + "pluginVersion": "9.4.0-pre", + "targets": [ + { + "alias": "Inside", + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "H", + "scenarioId": "csv_metric_values", + "stringInput": "100,100,100" + }, + { + "alias": "Outhouse", + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "A", + "scenarioId": "random_walk" + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "F", + "scenarioId": "random_walk" + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "B", + "scenarioId": "random_walk" + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "C", + "scenarioId": "random_walk" + } + ], + "title": "gradient + text color", + "type": "bargauge" + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "max": 100, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "blue", + "index": 0 + }, + { + "color": "green", + "index": 1, + "value": 20 + }, + { + "color": "orange", + "index": 2, + "value": 40 + }, + { + "color": "red", + "index": 3, + "value": 80 + } + ] + }, + "unit": "celsius" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 7, + "x": 17, + "y": 7 }, "id": 14, "links": [], "options": { "displayMode": "lcd", - "fieldOptions": { - "calcs": ["mean"], - "defaults": { - "decimals": null, - "max": 100, - "min": 0, - "unit": "celsius" - }, - "mappings": [], - "override": {}, - "thresholds": [ - { - "color": "blue", - "index": 0, - "value": null - }, - { - "color": "green", - "index": 1, - "value": 20 - }, - { - "color": "orange", - "index": 2, - "value": 40 - }, - { - "color": "red", - "index": 3, - "value": 80 - } + "minVizHeight": 10, + "minVizWidth": 0, + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "mean" ], + "fields": "", "values": false }, - "orientation": "horizontal" + "showUnfilled": true, + "valueMode": "hidden" }, - "pluginVersion": "6.2.0-pre", + "pluginVersion": "9.4.0-pre", "targets": [ { "alias": "Inside", + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "H", "scenarioId": "csv_metric_values", "stringInput": "100,100,100" }, { "alias": "Outhouse", + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "A", "scenarioId": "random_walk" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "F", "scenarioId": "random_walk" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "B", "scenarioId": "random_walk" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "C", "scenarioId": "random_walk" } ], - "timeFrom": null, - "timeShift": null, - "title": "LED", + "title": "LED + hidden value", "type": "bargauge" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "max": 100, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "index": 0 + }, + { + "color": "orange", + "index": 1, + "value": 40 + }, + { + "color": "red", + "index": 2, + "value": 80 + } + ] + }, + "unit": "watt" + }, + "overrides": [] + }, "gridPos": { "h": 9, "w": 11, "x": 0, - "y": 7 + "y": 14 }, "id": 7, "links": [], "options": { "displayMode": "lcd", - "fieldOptions": { - "calcs": ["mean"], - "defaults": { - "decimals": null, - "max": 100, - "min": 0, - "unit": "watt" - }, - "mappings": [], - "override": {}, - "thresholds": [ - { - "color": "green", - "index": 0, - "value": null - }, - { - "color": "orange", - "index": 1, - "value": 40 - }, - { - "color": "red", - "index": 2, - "value": 80 - } + "minVizHeight": 10, + "minVizWidth": 0, + "orientation": "vertical", + "reduceOptions": { + "calcs": [ + "mean" ], + "fields": "", "values": false }, - "orientation": "vertical" + "showUnfilled": true, + "valueMode": "color" }, - "pluginVersion": "6.2.0-pre", + "pluginVersion": "9.4.0-pre", "targets": [ { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "E", "scenarioId": "csv_metric_values", "stringInput": "10003,33333" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "F", "scenarioId": "random_walk" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "G", "scenarioId": "random_walk" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "H", "scenarioId": "csv_metric_values", "stringInput": "100,100,100" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "I", "scenarioId": "random_walk" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "J", "scenarioId": "random_walk" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "K", "scenarioId": "random_walk" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "L", "scenarioId": "random_walk" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "M", "scenarioId": "random_walk" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "N", "scenarioId": "random_walk" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "O", "scenarioId": "random_walk" - }, - { - "refId": "P", - "scenarioId": "random_walk" - }, - { - "refId": "Q", - "scenarioId": "random_walk" } ], - "timeFrom": null, - "timeShift": null, "title": "LED Vertical", "type": "bargauge" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "max": 100, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "index": 0 + }, + { + "color": "purple", + "index": 1, + "value": 50 + }, + { + "color": "blue", + "index": 2, + "value": 70 + } + ] + }, + "unit": "watt" + }, + "overrides": [] + }, "gridPos": { "h": 9, "w": 13, "x": 11, - "y": 7 + "y": 14 }, "id": 8, "links": [], "options": { "displayMode": "basic", - "fieldOptions": { - "calcs": ["mean"], - "defaults": { - "decimals": null, - "max": 100, - "min": 0, - "unit": "watt" - }, - "mappings": [], - "override": {}, - "thresholds": [ - { - "color": "green", - "index": 0, - "value": null - }, - { - "color": "purple", - "index": 1, - "value": 50 - }, - { - "color": "blue", - "index": 2, - "value": 70 - } + "minVizHeight": 10, + "minVizWidth": 0, + "orientation": "vertical", + "reduceOptions": { + "calcs": [ + "mean" ], + "fields": "", "values": false }, - "orientation": "vertical" + "showUnfilled": true, + "valueMode": "color" }, - "pluginVersion": "6.2.0-pre", + "pluginVersion": "9.4.0-pre", "targets": [ { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "H", "scenarioId": "csv_metric_values", "stringInput": "100,100,100" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "A", "scenarioId": "random_walk" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "B", "scenarioId": "random_walk" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "C", "scenarioId": "random_walk" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "D", "scenarioId": "random_walk" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "I", "scenarioId": "random_walk" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "J", "scenarioId": "random_walk" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "K", "scenarioId": "random_walk" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "L", "scenarioId": "random_walk" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "M", "scenarioId": "random_walk" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "N", "scenarioId": "random_walk" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "O", "scenarioId": "random_walk" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "P", "scenarioId": "random_walk" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "Q", "scenarioId": "random_walk" } ], - "timeFrom": null, - "timeShift": null, "title": "Basic vertical ", "type": "bargauge" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "max": 100, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "index": 0 + }, + { + "color": "blue", + "index": 1, + "value": 40 + }, + { + "color": "red", + "index": 2, + "value": 80 + } + ] + } + }, + "overrides": [] + }, "gridPos": { "h": 7, "w": 11, "x": 0, - "y": 16 + "y": 23 }, "id": 16, "links": [], "options": { "displayMode": "lcd", - "fieldOptions": { - "calcs": ["last"], - "defaults": { - "max": 100, - "min": 0 - }, - "mappings": [], - "override": {}, - "thresholds": [ - { - "color": "green", - "index": 0, - "value": null - }, - { - "color": "blue", - "index": 1, - "value": 40 - }, - { - "color": "red", - "index": 2, - "value": 80 - } + "minVizHeight": 10, + "minVizWidth": 0, + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "last" ], + "fields": "", "values": false }, - "orientation": "horizontal" + "showUnfilled": true, + "valueMode": "color" }, - "pluginVersion": "6.3.0-pre", + "pluginVersion": "9.4.0-pre", "targets": [ { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "A", "scenarioId": "csv_metric_values", "stringInput": "1,20,90,30,5,0,-100" } ], - "timeFrom": null, - "timeShift": null, "title": "Negative value below min", "type": "bargauge" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "max": 100, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "index": 0 + }, + { + "color": "blue", + "index": 1, + "value": 40 + }, + { + "color": "red", + "index": 2, + "value": 80 + } + ] + } + }, + "overrides": [] + }, "gridPos": { "h": 7, "w": 3, "x": 11, - "y": 16 + "y": 23 }, "id": 17, "links": [], "options": { "displayMode": "lcd", - "fieldOptions": { - "calcs": ["last"], - "defaults": { - "max": 100, - "min": 0 - }, - "mappings": [], - "override": {}, - "thresholds": [ - { - "color": "green", - "index": 0, - "value": null - }, - { - "color": "blue", - "index": 1, - "value": 40 - }, - { - "color": "red", - "index": 2, - "value": 80 - } + "minVizHeight": 10, + "minVizWidth": 0, + "orientation": "vertical", + "reduceOptions": { + "calcs": [ + "last" ], + "fields": "", "values": false }, - "orientation": "vertical" + "showUnfilled": true, + "valueMode": "color" }, - "pluginVersion": "6.3.0-pre", + "pluginVersion": "9.4.0-pre", "targets": [ { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "A", "scenarioId": "csv_metric_values", "stringInput": "1,20,90,30,5,0,-100" } ], - "timeFrom": null, - "timeShift": null, "title": "Negative value below min", "type": "bargauge" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "max": 100, + "min": -10, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "index": 0 + }, + { + "color": "blue", + "index": 1, + "value": 40 + }, + { + "color": "red", + "index": 2, + "value": 80 + } + ] + } + }, + "overrides": [] + }, "gridPos": { "h": 7, "w": 3, "x": 14, - "y": 16 + "y": 23 }, "id": 18, "links": [], "options": { "displayMode": "lcd", - "fieldOptions": { - "calcs": ["last"], - "defaults": { - "max": 100, - "min": -10 - }, - "mappings": [], - "override": {}, - "thresholds": [ - { - "color": "green", - "index": 0, - "value": null - }, - { - "color": "blue", - "index": 1, - "value": 40 - }, - { - "color": "red", - "index": 2, - "value": 80 - } + "minVizHeight": 10, + "minVizWidth": 0, + "orientation": "vertical", + "reduceOptions": { + "calcs": [ + "last" ], + "fields": "", "values": false }, - "orientation": "vertical" + "showUnfilled": true, + "valueMode": "color" }, - "pluginVersion": "6.3.0-pre", + "pluginVersion": "9.4.0-pre", "targets": [ { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "A", "scenarioId": "csv_metric_values", "stringInput": "1,20,90,30,5,6" } ], - "timeFrom": null, - "timeShift": null, "title": "Positive value above min", "type": "bargauge" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "max": 35, + "min": -20, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "blue", + "index": 0 + }, + { + "color": "green", + "index": 1, + "value": 5 + }, + { + "color": "#EAB839", + "index": 2, + "value": 25 + }, + { + "color": "red", + "index": 3, + "value": 30 + } + ] + } + }, + "overrides": [] + }, "gridPos": { "h": 7, "w": 3, "x": 17, - "y": 16 + "y": 23 }, "id": 19, "links": [], "options": { "displayMode": "lcd", - "fieldOptions": { - "calcs": ["last"], - "defaults": { - "max": 35, - "min": -20 - }, - "mappings": [], - "override": {}, - "thresholds": [ - { - "color": "blue", - "index": 0, - "value": null - }, - { - "color": "green", - "index": 1, - "value": 5 - }, - { - "color": "#EAB839", - "index": 2, - "value": 25 - }, - { - "color": "red", - "index": 3, - "value": 30 - } + "minVizHeight": 10, + "minVizWidth": 0, + "orientation": "vertical", + "reduceOptions": { + "calcs": [ + "last" ], + "fields": "", "values": false }, - "orientation": "vertical" + "showUnfilled": true, + "valueMode": "color" }, - "pluginVersion": "6.3.0-pre", + "pluginVersion": "9.4.0-pre", "targets": [ { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "A", "scenarioId": "csv_metric_values", "stringInput": "1,20,90,30,5,6" } ], - "timeFrom": null, - "timeShift": null, "title": "Negative min ", "type": "bargauge" }, { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "max": 35, + "min": -20, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "blue", + "index": 0 + }, + { + "color": "green", + "index": 1, + "value": 5 + }, + { + "color": "#EAB839", + "index": 2, + "value": 25 + }, + { + "color": "red", + "index": 3, + "value": 30 + } + ] + } + }, + "overrides": [] + }, "gridPos": { "h": 7, "w": 4, "x": 20, - "y": 16 + "y": 23 }, "id": 20, "links": [], "options": { "displayMode": "gradient", - "fieldOptions": { - "calcs": ["last"], - "defaults": { - "max": 35, - "min": -20 - }, - "mappings": [], - "override": {}, - "thresholds": [ - { - "color": "blue", - "index": 0, - "value": null - }, - { - "color": "green", - "index": 1, - "value": 5 - }, - { - "color": "#EAB839", - "index": 2, - "value": 25 - }, - { - "color": "red", - "index": 3, - "value": 30 - } + "minVizHeight": 10, + "minVizWidth": 0, + "orientation": "vertical", + "reduceOptions": { + "calcs": [ + "last" ], + "fields": "", "values": false }, - "orientation": "vertical" + "showUnfilled": true, + "valueMode": "color" }, - "pluginVersion": "6.3.0-pre", + "pluginVersion": "9.4.0-pre", "targets": [ { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, "refId": "A", "scenarioId": "csv_metric_values", "stringInput": "30,30" } ], - "timeFrom": null, - "timeShift": null, "title": "Negative min", "type": "bargauge" } ], - "schemaVersion": 18, + "revision": 1, + "schemaVersion": 38, "style": "dark", - "tags": ["gdev", "panel-tests"], + "tags": [ + "gdev", + "panel-tests" + ], "templating": { "list": [] }, @@ -819,11 +1413,33 @@ "to": "now" }, "timepicker": { - "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"], - "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"] + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ] }, "timezone": "", "title": "Panel Tests - Bar Gauge", "uid": "O6f11TZWk", - "version": 12 + "version": 1, + "weekStart": "" } diff --git a/devenv/dev-dashboards/panel-table/table_tests_new.json b/devenv/dev-dashboards/panel-table/table_tests_new.json index a373c5080b3..92143922121 100644 --- a/devenv/dev-dashboards/panel-table/table_tests_new.json +++ b/devenv/dev-dashboards/panel-table/table_tests_new.json @@ -1,1100 +1,1043 @@ { - "annotations": { - "list": [ - { - "builtIn": 1, - "datasource": { - "type": "datasource", - "uid": "grafana" - }, - "enable": true, - "hide": true, - "iconColor": "rgba(0, 211, 255, 1)", - "name": "Annotations & Alerts", - "type": "dashboard" - } - ] - }, - "editable": true, - "fiscalYearStartMonth": 0, - "graphTooltip": 0, - "links": [], - "liveNow": false, - "panels": [ - { - "datasource": { - "type": "datasource", - "uid": "grafana" - }, - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 0 - }, - "id": 7, - "targets": [ - { - "datasource": { - "type": "datasource", - "uid": "grafana" - }, - "refId": "A" - } - ], - "title": "Cell styles", - "type": "row" + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "datasource", + "uid": "grafana" }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "liveNow": false, + "panels": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 7, + "targets": [ { - "datasource": { - "type": "testdata", - "uid": "gdev-testdata" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "custom": { - "align": "center", - "cellOptions": { - "mode": "gradient", - "type": "color-background" - }, - "filterable": false, - "inspect": false - }, - "mappings": [], - "thresholds": { - "mode": "percentage", - "steps": [ - { - "color": "green" - }, - { - "color": "blue", - "value": 20 - }, - { - "color": "orange", - "value": 60 - }, - { - "color": "red", - "value": 70 - } - ] - }, - "unit": "degree" - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "Max" - }, - "properties": [ - { - "id": "custom.width", - "value": 84 - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "Last" - }, - "properties": [ - { - "id": "custom.width", - "value": 78 - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "Mean" - }, - "properties": [ - { - "id": "custom.width", - "value": 74 - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "Field" - }, - "properties": [ - { - "id": "custom.align", - "value": "left" - } - ] - } - ] - }, - "gridPos": { - "h": 16, - "w": 7, - "x": 0, - "y": 1 - }, - "id": 4, - "options": { - "cellHeight": "md", - "footer": { - "countRows": false, - "fields": "", - "reducer": [ - "sum" - ], - "show": false - }, - "showHeader": true, - "showRowNums": false, - "sortBy": [ - { - "desc": true, - "displayName": "Last" - } - ] - }, - "pluginVersion": "9.5.0-pre", - "targets": [ - { - "datasource": { - "type": "testdata", - "uid": "gdev-testdata" - }, - "refId": "A", - "scenarioId": "random_walk", - "seriesCount": 15, - "stringInput": "" - } - ], - "title": "Colored background", - "transformations": [ - { - "id": "reduce", - "options": { - "reducers": [ - "max", - "mean", - "last" - ] - } - } - ], - "type": "table" - }, - { - "datasource": { - "type": "testdata", - "uid": "gdev-testdata" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "custom": { - "align": "auto", - "cellOptions": { - "type": "auto" - }, - "filterable": false, - "inspect": false - }, - "mappings": [], - "thresholds": { - "mode": "percentage", - "steps": [ - { - "color": "orange" - }, - { - "color": "red", - "value": 50 - } - ] - } - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "A" - }, - "properties": [ - { - "id": "custom.cellOptions", - "value": { - "mode": "gradient", - "type": "gauge" - } - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "Info" - }, - "properties": [ - { - "id": "custom.width", - "value": 92 - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "Min" - }, - "properties": [ - { - "id": "custom.width", - "value": 76 - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "Max" - }, - "properties": [ - { - "id": "custom.width", - "value": 89 - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "Time" - }, - "properties": [ - { - "id": "custom.width", - "value": 165 - } - ] - } - ] - }, - "gridPos": { - "h": 16, - "w": 8, - "x": 7, - "y": 1 - }, - "id": 2, - "options": { - "cellHeight": "md", - "footer": { - "countRows": false, - "fields": "", - "reducer": [ - "sum" - ], - "show": false - }, - "showHeader": true, - "showRowNums": false, - "sortBy": [ - { - "desc": false, - "displayName": "Min" - } - ] - }, - "pluginVersion": "9.5.0-pre", - "targets": [ - { - "datasource": { - "type": "testdata", - "uid": "gdev-testdata" - }, - "refId": "A", - "scenarioId": "random_walk_table", - "stringInput": "" - } - ], - "title": "Bar gauge cells", - "transformations": [ - { - "id": "organize", - "options": { - "excludeByName": { - "Time": true - }, - "indexByName": { - "Info": 1, - "Max": 3, - "Min": 2, - "Time": 0, - "Value": 4 - }, - "renameByName": {} - } - } - ], - "type": "table" - }, - { - "datasource": { - "type": "testdata", - "uid": "gdev-testdata" - }, - "description": "", - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "custom": { - "align": "auto", - "cellOptions": { - "type": "auto" - }, - "filterable": false, - "inspect": false - }, - "mappings": [], - "thresholds": { - "mode": "percentage", - "steps": [ - { - "color": "blue" - }, - { - "color": "green", - "value": 50 - } - ] - } - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "A" - }, - "properties": [ - { - "id": "custom.cellOptions", - "value": { - "mode": "lcd", - "type": "gauge" - } - }, - { - "id": "custom.align", - "value": "center" - } - ] - } - ] - }, - "gridPos": { - "h": 16, - "w": 9, - "x": 15, - "y": 1 - }, - "id": 5, - "options": { - "cellHeight": "md", - "footer": { - "countRows": false, - "fields": "", - "reducer": [ - "sum" - ], - "show": false - }, - "showHeader": true, - "showRowNums": false, - "sortBy": [] - }, - "pluginVersion": "9.5.0-pre", - "targets": [ - { - "datasource": { - "type": "testdata", - "uid": "gdev-testdata" - }, - "refId": "A", - "scenarioId": "random_walk_table", - "stringInput": "" - } - ], - "title": "Retro LCD cell", - "transformations": [ - { - "id": "organize", - "options": { - "excludeByName": { - "Info": false, - "Max": true, - "Min": true, - "Time": false - }, - "indexByName": { - "Info": 1, - "Max": 3, - "Min": 2, - "Time": 0, - "Value": 4 - }, - "renameByName": {} - } - } - ], - "type": "table" - }, - { - "datasource": { - "type": "testdata", - "uid": "gdev-testdata" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "custom": { - "align": "auto", - "cellOptions": { - "type": "auto" - }, - "inspect": false - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "rate" - }, - "properties": [ - { - "id": "custom.cellOptions", - "value": { - "type": "sparkline" - } - }, - { - "id": "color", - "value": { - "mode": "continuous-GrYlRd" - } - } - ] - } - ] - }, - "gridPos": { - "h": 8, - "w": 9, - "x": 0, - "y": 17 - }, - "id": 14, - "options": { - "cellHeight": "md", - "footer": { - "countRows": false, - "fields": "", - "reducer": [ - "sum" - ], - "show": false - }, - "showHeader": true, - "showRowNums": false - }, - "pluginVersion": "9.5.0-pre", - "targets": [ - { - "datasource": { - "type": "testdata", - "uid": "gdev-testdata" - }, - "labels": "cluster=eu,service=checkout", - "min": 0.1, - "refId": "A", - "scenarioId": "random_walk", - "seriesCount": 3 - } - ], - "title": "Sparkline cell", - "transformations": [ - { - "id": "timeSeriesTable", - "options": {} - }, - { - "id": "organize", - "options": { - "excludeByName": {}, - "indexByName": {}, - "renameByName": { - "Trend": "rate" - } - } - } - ], - "type": "table" - }, - { - "datasource": { - "type": "testdata", - "uid": "gdev-testdata" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "custom": { - "align": "auto", - "cellOptions": { - "type": "auto" - }, - "inspect": false - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "rate" - }, - "properties": [ - { - "id": "custom.cellOptions", - "value": { - "drawStyle": "bars", - "fillOpacity": 40, - "gradientMode": "opacity", - "lineWidth": 1, - "showPoints": "auto", - "type": "sparkline" - } - }, - { - "id": "color", - "value": { - "mode": "continuous-GrYlRd" - } - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "latency" - }, - "properties": [ - { - "id": "custom.cellOptions", - "value": { - "fillOpacity": 40, - "gradientMode": "hue", - "type": "sparkline" - } - }, - { - "id": "color", - "value": { - "fixedColor": "orange", - "mode": "fixed" - } - } - ] - } - ] - }, - "gridPos": { - "h": 8, - "w": 9, - "x": 9, - "y": 17 - }, - "id": 15, - "maxDataPoints": 100, - "options": { - "cellHeight": "md", - "footer": { - "countRows": false, - "fields": "", - "reducer": [ - "sum" - ], - "show": false - }, - "showHeader": true, - "showRowNums": false - }, - "pluginVersion": "9.5.0-pre", - "targets": [ - { - "datasource": { - "type": "testdata", - "uid": "gdev-testdata" - }, - "labels": "cluster=eu,service=checkout", - "min": 0.1, - "refId": "A", - "scenarioId": "random_walk", - "seriesCount": 3 - }, - { - "datasource": { - "type": "testdata", - "uid": "gdev-testdata" - }, - "hide": false, - "labels": "cluster=eu,service=checkout", - "min": 0.1, - "refId": "B", - "scenarioId": "random_walk", - "seriesCount": 3 - } - ], - "title": "Multiple sparkline cells per row", - "transformations": [ - { - "id": "timeSeriesTable", - "options": {} - }, - { - "id": "joinByField", - "options": { - "byField": "service", - "mode": "outer" - } - }, - { - "id": "organize", - "options": { - "excludeByName": { - "cluster 2": true - }, - "indexByName": {}, - "renameByName": { - "Trend": "rate", - "Trend #A": "rate", - "Trend #B": "latency", - "cluster 1": "cluster" - } - } - } - ], - "type": "table" - }, - { - "datasource": { - "type": "testdata", - "uid": "gdev-testdata" - }, - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 25 - }, - "id": 9, - "targets": [ - { - "datasource": { - "type": "testdata", - "uid": "gdev-testdata" - }, - "refId": "A" - } - ], - "title": "Data links", - "type": "row" - }, - { - "datasource": { - "type": "testdata", - "uid": "gdev-testdata" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "custom": { - "align": "center", - "cellOptions": { - "type": "color-text" - }, - "filterable": false, - "inspect": false - }, - "decimals": 2, - "mappings": [], - "thresholds": { - "mode": "percentage", - "steps": [ - { - "color": "green" - }, - { - "color": "blue", - "value": 20 - }, - { - "color": "orange", - "value": 50 - }, - { - "color": "red", - "value": 70 - } - ] - }, - "unit": "percent" - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "Time" - }, - "properties": [ - { - "id": "custom.align" - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "{name=\"S1\", server=\"A\"}" - }, - "properties": [ - { - "id": "links", - "value": [ - { - "title": "Details", - "url": "http://detail?serverLabel=${__field.labels.server}&valueNumeric=${__value.numeric}" - } - ] - } - ] - } - ] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 26 - }, - "id": 3, - "options": { - "cellHeight": "md", - "footer": { - "countRows": false, - "fields": "", - "reducer": [ - "sum" - ], - "show": false - }, - "showHeader": true, - "showRowNums": false - }, - "pluginVersion": "9.5.0-pre", - "targets": [ - { - "alias": "S1", - "datasource": { - "type": "testdata", - "uid": "gdev-testdata" - }, - "labels": "server=A", - "refId": "A", - "scenarioId": "random_walk", - "seriesCount": 1, - "stringInput": "" - }, - { - "alias": "S2", - "datasource": { - "type": "testdata", - "uid": "gdev-testdata" - }, - "labels": "server=B", - "refId": "B", - "scenarioId": "random_walk", - "seriesCount": 1, - "stringInput": "" - }, - { - "alias": "S3", - "datasource": { - "type": "testdata", - "uid": "gdev-testdata" - }, - "labels": "server=C", - "refId": "C", - "scenarioId": "random_walk", - "seriesCount": 1, - "stringInput": "" - } - ], - "title": "Data link with labels and numeric value", - "transformations": [ - { - "id": "seriesToColumns", - "options": {} - } - ], - "type": "table" - }, - { - "datasource": { - "type": "testdata", - "uid": "gdev-testdata" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "custom": { - "align": "center", - "cellOptions": { - "type": "auto" - }, - "filterable": false, - "inspect": false - }, - "mappings": [], - "thresholds": { - "mode": "percentage", - "steps": [ - { - "color": "green" - }, - { - "color": "blue", - "value": 20 - }, - { - "color": "orange", - "value": 60 - }, - { - "color": "red", - "value": 70 - } - ] - }, - "unit": "degree" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 5, - "x": 12, - "y": 26 - }, - "id": 10, - "options": { - "cellHeight": "md", - "footer": { - "countRows": false, - "fields": "", - "reducer": [ - "sum" - ], - "show": false - }, - "showHeader": false, - "showRowNums": false, - "sortBy": [ - { - "desc": true, - "displayName": "Last" - } - ] - }, - "pluginVersion": "9.5.0-pre", - "targets": [ - { - "datasource": { - "type": "testdata", - "uid": "gdev-testdata" - }, - "refId": "A", - "scenarioId": "random_walk_table", - "seriesCount": 5, - "stringInput": "" - } - ], - "title": "No header", - "transformations": [ - { - "id": "organize", - "options": { - "excludeByName": { - "Min": true, - "Time": true, - "Value": true - }, - "indexByName": { - "Info": 2, - "Max": 4, - "Min": 3, - "Time": 0, - "Value": 1 - }, - "renameByName": {} - } - } - ], - "type": "table" - }, - { - "datasource": { - "type": "testdata", - "uid": "gdev-testdata" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "custom": { - "align": "auto", - "cellOptions": { - "type": "auto" - }, - "inspect": false - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 34 - }, - "id": 12, - "options": { - "cellHeight": "md", - "footer": { - "countRows": false, - "fields": "", - "reducer": [ - "sum" - ], - "show": true - }, - "showHeader": true - }, - "pluginVersion": "9.4.0-pre", - "targets": [ - { - "datasource": { - "type": "testdata", - "uid": "gdev-testdata" - }, - "refId": "A" - } - ], - "title": "Footer", - "type": "table" + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "refId": "A" } - ], - "refresh": "", - "revision": 1, - "schemaVersion": 38, - "style": "dark", - "tags": [ - "gdev", - "panel-tests" - ], - "templating": { - "list": [] + ], + "title": "Cell styles", + "type": "row" }, - "time": { - "from": "now-6h", - "to": "now" - }, - "timepicker": { - "refresh_intervals": [ - "10s", - "30s", - "1m", - "5m", - "15m", - "30m", - "1h", - "2h", - "1d" + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "center", + "cellOptions": { + "mode": "gradient", + "type": "color-background" + }, + "filterable": false, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "percentage", + "steps": [ + { + "color": "green" + }, + { + "color": "blue", + "value": 20 + }, + { + "color": "orange", + "value": 60 + }, + { + "color": "red", + "value": 70 + } + ] + }, + "unit": "degree" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Max" + }, + "properties": [ + { + "id": "custom.width", + "value": 84 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Last" + }, + "properties": [ + { + "id": "custom.width", + "value": 78 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Mean" + }, + "properties": [ + { + "id": "custom.width", + "value": 74 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Field" + }, + "properties": [ + { + "id": "custom.align", + "value": "left" + } + ] + } ] + }, + "gridPos": { + "h": 16, + "w": 7, + "x": 0, + "y": 1 + }, + "id": 4, + "options": { + "cellHeight": "md", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true, + "showRowNums": false, + "sortBy": [ + { + "desc": true, + "displayName": "Last" + } + ] + }, + "pluginVersion": "9.5.0-pre", + "targets": [ + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "refId": "A", + "scenarioId": "random_walk", + "seriesCount": 15, + "stringInput": "" + } + ], + "title": "Colored background", + "transformations": [ + { + "id": "reduce", + "options": { + "reducers": [ + "max", + "mean", + "last" + ] + } + } + ], + "type": "table" }, - "timezone": "", - "title": "Panel Tests - React Table", - "uid": "U_bZIMRMk", - "version": 33, - "weekStart": "" + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "center", + "cellOptions": { + "type": "auto" + }, + "filterable": false, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "percentage", + "steps": [ + { + "color": "orange" + }, + { + "color": "red", + "value": 50 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Basic " + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "mode": "basic", + "type": "gauge" + } + }, + { + "id": "custom.inspect", + "value": false + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Gradient text color value" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "mode": "gradient", + "type": "gauge", + "valueMode": "text" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "LCD hidden value" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "mode": "lcd", + "type": "gauge", + "valueMode": "hidden" + } + } + ] + } + ] + }, + "gridPos": { + "h": 16, + "w": 17, + "x": 7, + "y": 1 + }, + "id": 2, + "options": { + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true, + "showRowNums": false, + "sortBy": [ + { + "desc": false, + "displayName": "Min" + } + ] + }, + "pluginVersion": "9.5.0-pre", + "targets": [ + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "A", + "scenarioId": "random_walk_table", + "stringInput": "" + } + ], + "title": "Bar gauge cells", + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "Info": true, + "Time": true + }, + "indexByName": { + "Info": 1, + "Max": 3, + "Min": 2, + "Time": 0, + "Value": 4 + }, + "renameByName": { + "A": "LCD hidden value", + "Max": "Gradient text color value", + "Min": "Basic " + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "rate" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "fillOpacity": 29, + "gradientMode": "opacity", + "lineWidth": 1, + "type": "sparkline" + } + }, + { + "id": "color", + "value": { + "mode": "continuous-GrYlRd" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "cluster" + }, + "properties": [ + { + "id": "custom.width", + "value": 120 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "service" + }, + "properties": [ + { + "id": "custom.width", + "value": 120 + } + ] + } + ] + }, + "gridPos": { + "h": 7, + "w": 9, + "x": 0, + "y": 17 + }, + "id": 14, + "maxDataPoints": 50, + "options": { + "cellHeight": "md", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true, + "showRowNums": false, + "sortBy": [] + }, + "pluginVersion": "9.5.0-pre", + "targets": [ + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "labels": "cluster=eu,service=checkout", + "min": 0.1, + "refId": "A", + "scenarioId": "random_walk", + "seriesCount": 3 + } + ], + "title": "Sparkline cell", + "transformations": [ + { + "id": "timeSeriesTable", + "options": {} + }, + { + "id": "organize", + "options": { + "excludeByName": {}, + "indexByName": {}, + "renameByName": { + "Trend": "rate" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "rate" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "drawStyle": "bars", + "fillOpacity": 100, + "gradientMode": "hue", + "lineWidth": 0, + "showPoints": "auto", + "type": "sparkline" + } + }, + { + "id": "color", + "value": { + "mode": "continuous-GrYlRd" + } + }, + { + "id": "color", + "value": { + "fixedColor": "red", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "latency" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "fillOpacity": 40, + "gradientMode": "opacity", + "lineInterpolation": "stepBefore", + "lineWidth": 1, + "type": "sparkline" + } + }, + { + "id": "color", + "value": { + "fixedColor": "purple", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "service" + }, + "properties": [ + { + "id": "custom.width", + "value": 120 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "cluster" + }, + "properties": [ + { + "id": "custom.width", + "value": 120 + } + ] + } + ] + }, + "gridPos": { + "h": 7, + "w": 15, + "x": 9, + "y": 17 + }, + "id": 15, + "maxDataPoints": 30, + "options": { + "cellHeight": "md", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true, + "showRowNums": false + }, + "pluginVersion": "9.5.0-pre", + "targets": [ + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "labels": "cluster=eu,service=checkout", + "min": 0.1, + "refId": "A", + "scenarioId": "random_walk", + "seriesCount": 3 + }, + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "hide": false, + "labels": "cluster=eu,service=checkout", + "min": 0.1, + "refId": "B", + "scenarioId": "random_walk", + "seriesCount": 3 + } + ], + "title": "Multiple sparkline cells per row", + "transformations": [ + { + "id": "timeSeriesTable", + "options": {} + }, + { + "id": "joinByField", + "options": { + "byField": "service", + "mode": "outer" + } + }, + { + "id": "organize", + "options": { + "excludeByName": { + "cluster 2": true + }, + "indexByName": {}, + "renameByName": { + "Trend": "rate", + "Trend #A": "rate", + "Trend #B": "latency", + "cluster 1": "cluster" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 24 + }, + "id": 9, + "targets": [ + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "refId": "A" + } + ], + "title": "Data links", + "type": "row" + }, + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "center", + "cellOptions": { + "type": "color-text" + }, + "filterable": false, + "inspect": false + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "percentage", + "steps": [ + { + "color": "green" + }, + { + "color": "blue", + "value": 20 + }, + { + "color": "orange", + "value": 50 + }, + { + "color": "red", + "value": 70 + } + ] + }, + "unit": "percent" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Time" + }, + "properties": [ + { + "id": "custom.align" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "{name=\"S1\", server=\"A\"}" + }, + "properties": [ + { + "id": "links", + "value": [ + { + "title": "Details", + "url": "http://detail?serverLabel=${__field.labels.server}&valueNumeric=${__value.numeric}" + } + ] + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 25 + }, + "id": 3, + "options": { + "cellHeight": "md", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true, + "showRowNums": false + }, + "pluginVersion": "9.5.0-pre", + "targets": [ + { + "alias": "S1", + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "labels": "server=A", + "refId": "A", + "scenarioId": "random_walk", + "seriesCount": 1, + "stringInput": "" + }, + { + "alias": "S2", + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "labels": "server=B", + "refId": "B", + "scenarioId": "random_walk", + "seriesCount": 1, + "stringInput": "" + }, + { + "alias": "S3", + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "labels": "server=C", + "refId": "C", + "scenarioId": "random_walk", + "seriesCount": 1, + "stringInput": "" + } + ], + "title": "Data link with labels and numeric value", + "transformations": [ + { + "id": "seriesToColumns", + "options": {} + } + ], + "type": "table" + }, + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "center", + "cellOptions": { + "type": "auto" + }, + "filterable": false, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "percentage", + "steps": [ + { + "color": "green" + }, + { + "color": "blue", + "value": 20 + }, + { + "color": "orange", + "value": 60 + }, + { + "color": "red", + "value": 70 + } + ] + }, + "unit": "degree" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 5, + "x": 12, + "y": 25 + }, + "id": 10, + "options": { + "cellHeight": "md", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": false, + "showRowNums": false, + "sortBy": [ + { + "desc": true, + "displayName": "Last" + } + ] + }, + "pluginVersion": "9.5.0-pre", + "targets": [ + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "refId": "A", + "scenarioId": "random_walk_table", + "seriesCount": 5, + "stringInput": "" + } + ], + "title": "No header", + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "Min": true, + "Time": true, + "Value": true + }, + "indexByName": { + "Info": 2, + "Max": 4, + "Min": 3, + "Time": 0, + "Value": 1 + }, + "renameByName": {} + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 33 + }, + "id": 12, + "options": { + "cellHeight": "md", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": true + }, + "showHeader": true, + "showRowNums": false + }, + "pluginVersion": "9.5.0-pre", + "targets": [ + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "refId": "A" + } + ], + "title": "Footer", + "type": "table" + } + ], + "refresh": "", + "revision": 1, + "schemaVersion": 38, + "style": "dark", + "tags": [ + "gdev", + "panel-tests" + ], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ] + }, + "timezone": "", + "title": "Panel Tests - React Table", + "uid": "U_bZIMRMk", + "version": 7, + "weekStart": "" } \ No newline at end of file diff --git a/docs/sources/developers/kinds/composable/bargaugepanelcfg/schema-reference.md b/docs/sources/developers/kinds/composable/bargaugepanelcfg/schema-reference.md index 1e8bf65de88..a4ecf690870 100644 --- a/docs/sources/developers/kinds/composable/bargaugepanelcfg/schema-reference.md +++ b/docs/sources/developers/kinds/composable/bargaugepanelcfg/schema-reference.md @@ -27,6 +27,7 @@ It extends [SingleStatBaseOptions](#singlestatbaseoptions). | `minVizHeight` | uint32 | **Yes** | Default: `10`. | | `minVizWidth` | uint32 | **Yes** | Default: `0`. | | `showUnfilled` | boolean | **Yes** | Default: `true`. | +| `valueMode` | string | **Yes** | Allows for the table cell gauge display type to set the gauge mode.
Possible values are: `color`, `text`, `hidden`. | | `orientation` | string | No | *(Inherited from [SingleStatBaseOptions](#singlestatbaseoptions))*
TODO docs
Possible values are: `auto`, `vertical`, `horizontal`. | | `reduceOptions` | [ReduceDataOptions](#reducedataoptions) | No | *(Inherited from [SingleStatBaseOptions](#singlestatbaseoptions))*
TODO docs | | `text` | [VizTextDisplayOptions](#viztextdisplayoptions) | No | *(Inherited from [SingleStatBaseOptions](#singlestatbaseoptions))*
TODO docs | diff --git a/docs/sources/developers/kinds/core/dashboard/schema-reference.md b/docs/sources/developers/kinds/core/dashboard/schema-reference.md index 2733eb423c0..92f5834fd29 100644 --- a/docs/sources/developers/kinds/core/dashboard/schema-reference.md +++ b/docs/sources/developers/kinds/core/dashboard/schema-reference.md @@ -195,11 +195,12 @@ TODO docs TODO docs -| Property | Type | Required | Description | -|----------|--------|----------|-----------------------------------------------------------------------------------------------------------------------------------------------------| -| `color` | string | **Yes** | TODO docs | -| `state` | string | No | TODO docs
TODO are the values here enumerable into a disjunction?
Some seem to be listed in typescript comment | -| `value` | number | No | TODO docs
FIXME the corresponding typescript field is required/non-optional, but nulls currently appear here when serializing -Infinity to JSON | +| Property | Type | Required | Description | +|----------|---------|----------|-----------------------------------------------------------------------------------------------------------------------------------------------------| +| `color` | string | **Yes** | TODO docs | +| `index` | integer | No | Threshold index, an old property that is not needed an should only appear in older dashboards | +| `state` | string | No | TODO docs
TODO are the values here enumerable into a disjunction?
Some seem to be listed in typescript comment | +| `value` | number | No | TODO docs
FIXME the corresponding typescript field is required/non-optional, but nulls currently appear here when serializing -Infinity to JSON | ### ValueMapping diff --git a/docs/sources/panels-visualizations/visualizations/table/index.md b/docs/sources/panels-visualizations/visualizations/table/index.md index 089d7d3ffe4..c4b22966547 100644 --- a/docs/sources/panels-visualizations/visualizations/table/index.md +++ b/docs/sources/panels-visualizations/visualizations/table/index.md @@ -108,6 +108,22 @@ The gauge is split up in small cells that are lit or unlit. {{< figure src="/static/img/docs/tables/lcd-gauge.png" max-width="500px" caption="LCD gauge" class="docs-image--no-shadow" >}} +#### Label Options + +Additionally, labels displayed alongside of the gauges can be set to be colored by value, match the theme text color, or be hidden. + +**Value Color** + +{{< figure src="/static/img/docs/tables/value-color-mode.png" max-width="500px" caption="Color Label by Value" class="docs-image--no-shadow" >}} + +**Text Color** + +{{< figure src="/static/img/docs/tables/text-color-mode.png" max-width="500px" caption="Color Label by theme color" class="docs-image--no-shadow" >}} + +**Hidden** + +{{< figure src="/static/img/docs/tables/hidden-mode.png" max-width="500px" caption="Hide Label" class="docs-image--no-shadow" >}} + ### JSON view Shows value formatted as code. If a value is an object the JSON view allowing browsing the JSON object will appear on hover. diff --git a/kinds/dashboard/dashboard_kind.cue b/kinds/dashboard/dashboard_kind.cue index f399d9a0d38..72d44fa1798 100644 --- a/kinds/dashboard/dashboard_kind.cue +++ b/kinds/dashboard/dashboard_kind.cue @@ -25,8 +25,8 @@ lineage: seqs: [ // Description of dashboard. description?: string // This property should only be used in dashboards defined by plugins. It is a quick check - // to see if the version has changed since the last time. Unclear why using the version property - // is insufficient. + // to see if the version has changed since the last time. Unclear why using the version property + // is insufficient. revision?: int64 @grafanamaturity(NeedsExpertReview) // For dashboards imported from the https://grafana.com/grafana/dashboards/ portal gnetId?: string @grafanamaturity(NeedsExpertReview) @@ -220,6 +220,8 @@ lineage: seqs: [ value?: number @grafanamaturity(NeedsExpertReview) // TODO docs color: string @grafanamaturity(NeedsExpertReview) + // Threshold index, an old property that is not needed an should only appear in older dashboards + index?: int32 @grafanamaturity(NeedsExpertReview) // TODO docs // TODO are the values here enumerable into a disjunction? // Some seem to be listed in typescript comment diff --git a/packages/grafana-schema/src/common/common.gen.ts b/packages/grafana-schema/src/common/common.gen.ts index f19c9ddbb2f..04b419b58e0 100644 --- a/packages/grafana-schema/src/common/common.gen.ts +++ b/packages/grafana-schema/src/common/common.gen.ts @@ -593,6 +593,15 @@ export enum BarGaugeDisplayMode { Lcd = 'lcd', } +/** + * Allows for the table cell gauge display type to set the gauge mode. + */ +export enum BarGaugeValueMode { + Color = 'color', + Hidden = 'hidden', + Text = 'text', +} + /** * TODO docs */ @@ -697,6 +706,7 @@ export interface TableImageCellOptions { export interface TableBarGaugeCellOptions { mode?: BarGaugeDisplayMode; type: TableCellDisplayMode.Gauge; + valueDisplayMode?: BarGaugeValueMode; } /** diff --git a/packages/grafana-schema/src/common/mudball.cue b/packages/grafana-schema/src/common/mudball.cue index 0981e652ffd..9286e5c5783 100644 --- a/packages/grafana-schema/src/common/mudball.cue +++ b/packages/grafana-schema/src/common/mudball.cue @@ -242,6 +242,9 @@ VizLegendOptions: { // for the bar gauge component of Grafana UI BarGaugeDisplayMode: "basic" | "lcd" | "gradient" @cuetsy(kind="enum") +// Allows for the table cell gauge display type to set the gauge mode. +BarGaugeValueMode: "color" | "text" | "hidden" @cuetsy(kind="enum") + // TODO docs VizTooltipOptions: { mode: TooltipDisplayMode diff --git a/packages/grafana-schema/src/common/table.cue b/packages/grafana-schema/src/common/table.cue index 84c819b5511..1522e61ed87 100644 --- a/packages/grafana-schema/src/common/table.cue +++ b/packages/grafana-schema/src/common/table.cue @@ -52,6 +52,7 @@ TableImageCellOptions: { TableBarGaugeCellOptions: { type: TableCellDisplayMode & "gauge" mode?: BarGaugeDisplayMode + valueDisplayMode?: BarGaugeValueMode } @cuetsy(kind="interface") // Sparkline cell options diff --git a/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts b/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts index 18e8c544351..b1332f1b72c 100644 --- a/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts +++ b/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts @@ -238,6 +238,10 @@ export interface Threshold { * TODO docs */ color: string; + /** + * Threshold index, an old property that is not needed an should only appear in older dashboards + */ + index?: number; /** * TODO docs * TODO are the values here enumerable into a disjunction? diff --git a/packages/grafana-ui/src/components/BarGauge/BarGauge.test.tsx b/packages/grafana-ui/src/components/BarGauge/BarGauge.test.tsx index 0a8e36dec9d..b4bfb2ca3d4 100644 --- a/packages/grafana-ui/src/components/BarGauge/BarGauge.test.tsx +++ b/packages/grafana-ui/src/components/BarGauge/BarGauge.test.tsx @@ -11,18 +11,18 @@ import { getDisplayProcessor, createTheme, } from '@grafana/data'; -import { BarGaugeDisplayMode } from '@grafana/schema'; +import { BarGaugeDisplayMode, BarGaugeValueMode } from '@grafana/schema'; import { BarGauge, Props, - getCellColor, - getValueColor, + getTextValueColor, getBasicAndGradientStyles, getBarGradient, getTitleStyles, getValuePercent, calculateBarAndValueDimensions, + getCellColor, } from './BarGauge'; const green = '#73BF69'; @@ -63,7 +63,7 @@ function getProps(propOverrides?: Partial): Props { } function getValue(value: number, title?: string): DisplayValue { - return { numeric: value, text: value.toString(), title: title }; + return { numeric: value, text: value.toString(), title: title, color: '#FF0000' }; } describe('BarGauge', () => { @@ -134,12 +134,12 @@ describe('BarGauge', () => { it('should get the threshold color if value is same as a threshold', () => { const props = getProps(); props.value = props.display!(70); - expect(getValueColor(props)).toEqual(orange); + expect(getTextValueColor(props)).toEqual(orange); }); it('should get the base threshold', () => { const props = getProps(); props.value = props.display!(-10); - expect(getValueColor(props)).toEqual(green); + expect(getTextValueColor(props)).toEqual(green); }); }); @@ -325,5 +325,46 @@ describe('BarGauge', () => { ); expect(result.valueWidth).toBe(21); }); + + it('valueWidth be zero if valueMode is hideen', () => { + const result = calculateBarAndValueDimensions( + getProps({ + height: 30, + width: 100, + value: getValue(1, 'AA'), + orientation: VizOrientation.Horizontal, + valueDisplayMode: BarGaugeValueMode.Hidden, + }) + ); + expect(result.valueWidth).toBe(0); + }); + }); + + describe('With valueMode set to text color', () => { + it('should color value using text color', () => { + const props = getProps({ + width: 150, + value: getValue(100), + orientation: VizOrientation.Vertical, + valueDisplayMode: BarGaugeValueMode.Text, + }); + const styles = getBasicAndGradientStyles(props); + expect(styles.bar.background).toBe('rgba(255, 0, 0, 0.35)'); + expect(styles.value.color).toBe('rgb(204, 204, 220)'); + }); + }); + + describe('With valueMode set to text value', () => { + it('should color value value color', () => { + const props = getProps({ + width: 150, + value: getValue(100), + orientation: VizOrientation.Vertical, + valueDisplayMode: BarGaugeValueMode.Color, + }); + const styles = getBasicAndGradientStyles(props); + expect(styles.bar.background).toBe('rgba(255, 0, 0, 0.35)'); + expect(styles.value.color).toBe('#FF0000'); + }); }); }); diff --git a/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx b/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx index a38179b6417..47ab798514d 100644 --- a/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx +++ b/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx @@ -20,7 +20,7 @@ import { VizOrientation, } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; -import { BarGaugeDisplayMode, VizTextDisplayOptions } from '@grafana/schema'; +import { BarGaugeDisplayMode, BarGaugeValueMode, VizTextDisplayOptions } from '@grafana/schema'; import { Themeable2 } from '../../types'; import { calculateFontSize, measureText } from '../../utils/measureText'; @@ -49,6 +49,7 @@ export interface Props extends Themeable2 { className?: string; showUnfilled?: boolean; alignmentFactors?: DisplayValueAlignmentFactors; + valueDisplayMode?: BarGaugeValueMode; } export class BarGauge extends PureComponent { @@ -111,17 +112,19 @@ export class BarGauge extends PureComponent { } renderBasicAndGradientBars(): ReactNode { - const { value, showUnfilled } = this.props; + const { value, showUnfilled, valueDisplayMode } = this.props; const styles = getBasicAndGradientStyles(this.props); return (
- + {valueDisplayMode !== BarGaugeValueMode.Hidden && ( + + )} {showUnfilled &&
}
@@ -129,7 +132,8 @@ export class BarGauge extends PureComponent { } renderRetroBars(): ReactNode { - const { display, field, value, itemSpacing, alignmentFactors, orientation, lcdCellWidth, text } = this.props; + const { display, field, value, itemSpacing, alignmentFactors, orientation, lcdCellWidth, text, valueDisplayMode } = + this.props; const { valueHeight, valueWidth, maxBarHeight, maxBarWidth, wrapperWidth, wrapperHeight } = calculateBarAndValueDimensions(this.props); const minValue = field.min ?? GAUGE_DEFAULT_MINIMUM; @@ -141,7 +145,7 @@ export class BarGauge extends PureComponent { const cellSpacing = itemSpacing!; const cellCount = Math.floor(maxSize / lcdCellWidth!); const cellSize = Math.floor((maxSize - cellSpacing * cellCount) / cellCount); - const valueColor = getValueColor(this.props); + const valueColor = getTextValueColor(this.props); const valueToBaseSizeOn = alignmentFactors ? alignmentFactors : value; const valueStyles = getValueStyles(valueToBaseSizeOn, valueColor, valueWidth, valueHeight, orientation, text); @@ -192,11 +196,13 @@ export class BarGauge extends PureComponent { return (
{cells} - + {valueDisplayMode !== BarGaugeValueMode.Hidden && ( + + )}
); } @@ -338,7 +344,7 @@ interface BarAndValueDimensions { * Only exported for unit tests **/ export function calculateBarAndValueDimensions(props: Props): BarAndValueDimensions { - const { height, width, orientation, text, alignmentFactors } = props; + const { height, width, orientation, text, alignmentFactors, valueDisplayMode } = props; const titleDim = calculateTitleDimensions(props); const value = alignmentFactors ?? props.value; const valueString = formattedValueToString(value); @@ -363,13 +369,25 @@ export function calculateBarAndValueDimensions(props: Props): BarAndValueDimensi } valueWidth = width; + + if (valueDisplayMode === BarGaugeValueMode.Hidden) { + valueHeight = 0; + valueWidth = 0; + } + maxBarHeight = height - (titleDim.height + valueHeight); maxBarWidth = width; wrapperWidth = width; wrapperHeight = height - titleDim.height; } else { - valueHeight = height - titleDim.height; - valueWidth = Math.max(Math.min(width * 0.2, MAX_VALUE_WIDTH), realValueWidth); + // Calculate the width and the height of the given values + if (valueDisplayMode === BarGaugeValueMode.Hidden) { + valueHeight = 0; + valueWidth = 0; + } else { + valueHeight = height - titleDim.height; + valueWidth = Math.max(Math.min(width * 0.2, MAX_VALUE_WIDTH), realValueWidth); + } maxBarHeight = height - titleDim.height; maxBarWidth = width - valueWidth - titleDim.width; @@ -447,10 +465,11 @@ export function getBasicAndGradientStyles(props: Props): BasicAndGradientStyles const minValue = field.min ?? GAUGE_DEFAULT_MINIMUM; const maxValue = field.max ?? GAUGE_DEFAULT_MAXIMUM; const valuePercent = getValuePercent(value.numeric, minValue, maxValue); - const valueColor = getValueColor(props); + const textColor = getTextValueColor(props); + const barColor = value.color ?? FALLBACK_COLOR; const valueToBaseSizeOn = alignmentFactors ? alignmentFactors : value; - const valueStyles = getValueStyles(valueToBaseSizeOn, valueColor, valueWidth, valueHeight, orientation, text); + const valueStyles = getValueStyles(valueToBaseSizeOn, textColor, valueWidth, valueHeight, orientation, text); const isBasic = displayMode === 'basic'; const wrapperStyles: CSSProperties = { @@ -491,8 +510,8 @@ export function getBasicAndGradientStyles(props: Props): BasicAndGradientStyles if (isBasic) { // Basic styles - barStyles.background = `${tinycolor(valueColor).setAlpha(0.35).toRgbString()}`; - barStyles.borderTop = `2px solid ${valueColor}`; + barStyles.background = `${tinycolor(barColor).setAlpha(0.35).toRgbString()}`; + barStyles.borderTop = `2px solid ${barColor}`; } else { // Gradient styles barStyles.background = getBarGradient(props, maxBarHeight); @@ -517,8 +536,8 @@ export function getBasicAndGradientStyles(props: Props): BasicAndGradientStyles if (isBasic) { // Basic styles - barStyles.background = `${tinycolor(valueColor).setAlpha(0.35).toRgbString()}`; - barStyles.borderRight = `2px solid ${valueColor}`; + barStyles.background = `${tinycolor(barColor).setAlpha(0.35).toRgbString()}`; + barStyles.borderRight = `2px solid ${barColor}`; } else { // Gradient styles barStyles.background = getBarGradient(props, maxBarWidth); @@ -598,7 +617,11 @@ export function getBarGradient(props: Props, maxSize: number): string { /** * Only exported to for unit test */ -export function getValueColor(props: Props): string { +export function getTextValueColor(props: Props): string { + if (props.valueDisplayMode === 'text') { + return props.theme.colors.text.primary; + } + const { value } = props; if (value.color) { return value.color; diff --git a/packages/grafana-ui/src/components/Table/BarGaugeCell.tsx b/packages/grafana-ui/src/components/Table/BarGaugeCell.tsx index 3bbb3b2761a..f3d1f5ef9e2 100644 --- a/packages/grafana-ui/src/components/Table/BarGaugeCell.tsx +++ b/packages/grafana-ui/src/components/Table/BarGaugeCell.tsx @@ -2,7 +2,7 @@ import { isFunction } from 'lodash'; import React, { FC } from 'react'; import { ThresholdsConfig, ThresholdsMode, VizOrientation, getFieldConfigWithMinMax } from '@grafana/data'; -import { BarGaugeDisplayMode } from '@grafana/schema'; +import { BarGaugeDisplayMode, BarGaugeValueMode } from '@grafana/schema'; import { BarGauge } from '../BarGauge/BarGauge'; import { DataLinksContextMenu, DataLinksContextMenuApi } from '../DataLinks/DataLinksContextMenu'; @@ -26,6 +26,8 @@ const defaultScale: ThresholdsConfig = { export const BarGaugeCell: FC = (props) => { const { field, innerWidth, tableStyles, cell, cellProps, row } = props; + const displayValue = field.display!(cell.value); + const cellOptions = getCellOptions(field); let config = getFieldConfigWithMinMax(field, false); if (!config.thresholds) { @@ -35,14 +37,15 @@ export const BarGaugeCell: FC = (props) => { }; } - const displayValue = field.display!(cell.value); - - // Set default display mode + // Set default display mode and update if defined + // and update the valueMode if defined let barGaugeMode: BarGaugeDisplayMode = BarGaugeDisplayMode.Gradient; + let valueDisplayMode: BarGaugeValueMode | undefined = undefined; - const cellOptions = getCellOptions(field); if (cellOptions.type === TableCellDisplayMode.Gauge) { barGaugeMode = cellOptions.mode ?? BarGaugeDisplayMode.Gradient; + valueDisplayMode = + cellOptions.valueDisplayMode !== undefined ? cellOptions.valueDisplayMode : BarGaugeValueMode.Text; } const getLinks = () => { @@ -73,6 +76,7 @@ export const BarGaugeCell: FC = (props) => { itemSpacing={1} lcdCellWidth={8} displayMode={barGaugeMode} + valueDisplayMode={valueDisplayMode} /> ); }; @@ -84,21 +88,7 @@ export const BarGaugeCell: FC = (props) => { {(api) => renderComponent(api)} )} - {!hasLinks && ( - - )} + {!hasLinks && renderComponent({})}
); }; diff --git a/pkg/kinds/dashboard/dashboard_types_gen.go b/pkg/kinds/dashboard/dashboard_types_gen.go index 31fe5859db1..792535a1451 100644 --- a/pkg/kinds/dashboard/dashboard_types_gen.go +++ b/pkg/kinds/dashboard/dashboard_types_gen.go @@ -701,6 +701,9 @@ type Threshold struct { // TODO docs Color string `json:"color"` + // Threshold index, an old property that is not needed an should only appear in older dashboards + Index *int32 `json:"index,omitempty"` + // TODO docs // TODO are the values here enumerable into a disjunction? // Some seem to be listed in typescript comment diff --git a/pkg/kindsys/report.json b/pkg/kindsys/report.json index 0627ecec5fc..f900e8cd829 100644 --- a/pkg/kindsys/report.json +++ b/pkg/kindsys/report.json @@ -296,7 +296,7 @@ 0 ], "description": "A Grafana dashboard.", - "grafanaMaturityCount": 139, + "grafanaMaturityCount": 140, "lineageIsGroup": false, "links": { "docs": "https://grafana.com/docs/grafana/next/developers/kinds/core/dashboard/schema-reference", diff --git a/pkg/tests/api/alerting/api_alertmanager_test.go b/pkg/tests/api/alerting/api_alertmanager_test.go index cb01fc9b87b..99a5ed13567 100644 --- a/pkg/tests/api/alerting/api_alertmanager_test.go +++ b/pkg/tests/api/alerting/api_alertmanager_test.go @@ -12,12 +12,11 @@ import ( "testing" "time" + "github.com/grafana/grafana/pkg/expr" "github.com/prometheus/common/model" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/grafana/grafana/pkg/expr" - apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" ngstore "github.com/grafana/grafana/pkg/services/ngalert/store" diff --git a/public/app/plugins/panel/bargauge/BarGaugePanel.test.tsx b/public/app/plugins/panel/bargauge/BarGaugePanel.test.tsx index 3b74a48a67c..bd40346e0f2 100644 --- a/public/app/plugins/panel/bargauge/BarGaugePanel.test.tsx +++ b/public/app/plugins/panel/bargauge/BarGaugePanel.test.tsx @@ -4,7 +4,7 @@ import React from 'react'; import { dateMath, dateTime, EventBus, LoadingState, TimeRange, toDataFrame, VizOrientation } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; -import { BarGaugeDisplayMode } from '@grafana/schema'; +import { BarGaugeDisplayMode, BarGaugeValueMode } from '@grafana/schema'; import { BarGaugePanel, BarGaugePanelProps } from './BarGaugePanel'; @@ -101,6 +101,7 @@ function buildPanelData(overrideValues?: Partial): BarGaugeP showUnfilled: true, minVizHeight: 10, minVizWidth: 0, + valueMode: BarGaugeValueMode.Color, }, transparent: false, timeRange, diff --git a/public/app/plugins/panel/bargauge/BarGaugePanel.tsx b/public/app/plugins/panel/bargauge/BarGaugePanel.tsx index 0165cf3677f..326e5b3fb60 100644 --- a/public/app/plugins/panel/bargauge/BarGaugePanel.tsx +++ b/public/app/plugins/panel/bargauge/BarGaugePanel.tsx @@ -49,6 +49,7 @@ export class BarGaugePanel extends PureComponent { className={targetClassName} alignmentFactors={count > 1 ? alignmentFactors : undefined} showUnfilled={options.showUnfilled} + valueDisplayMode={options.valueMode} /> ); }; diff --git a/public/app/plugins/panel/bargauge/module.tsx b/public/app/plugins/panel/bargauge/module.tsx index bfe41e90853..778abfd49ab 100644 --- a/public/app/plugins/panel/bargauge/module.tsx +++ b/public/app/plugins/panel/bargauge/module.tsx @@ -1,5 +1,5 @@ import { PanelPlugin, VizOrientation } from '@grafana/data'; -import { BarGaugeDisplayMode } from '@grafana/schema'; +import { BarGaugeDisplayMode, BarGaugeValueMode } from '@grafana/schema'; import { commonOptionsBuilder, sharedSingleStatPanelChangedHandler } from '@grafana/ui'; import { addOrientationOption, addStandardDataReduceOptions } from '../stat/common'; @@ -29,6 +29,18 @@ export const plugin = new PanelPlugin(BarGaugePanel) }, defaultValue: defaultPanelOptions.displayMode, }) + .addRadio({ + path: 'valueMode', + name: 'Value display', + settings: { + options: [ + { value: BarGaugeValueMode.Color, label: 'Value color' }, + { value: BarGaugeValueMode.Text, label: 'Text color' }, + { value: BarGaugeValueMode.Hidden, label: 'Hidden' }, + ], + }, + defaultValue: defaultPanelOptions.valueMode, + }) .addBooleanSwitch({ path: 'showUnfilled', name: 'Show unfilled area', diff --git a/public/app/plugins/panel/bargauge/panelcfg.cue b/public/app/plugins/panel/bargauge/panelcfg.cue index 73f133fb39c..ffb83db0724 100644 --- a/public/app/plugins/panel/bargauge/panelcfg.cue +++ b/public/app/plugins/panel/bargauge/panelcfg.cue @@ -29,6 +29,7 @@ composableKinds: PanelCfg: { PanelOptions: { common.SingleStatBaseOptions displayMode: common.BarGaugeDisplayMode | *"gradient" + valueMode: common.BarGaugeValueMode | *"color" showUnfilled: bool | *true minVizWidth: uint32 | *0 minVizHeight: uint32 | *10 diff --git a/public/app/plugins/panel/bargauge/panelcfg.gen.ts b/public/app/plugins/panel/bargauge/panelcfg.gen.ts index f42c6814cfc..9d2b6b5135b 100644 --- a/public/app/plugins/panel/bargauge/panelcfg.gen.ts +++ b/public/app/plugins/panel/bargauge/panelcfg.gen.ts @@ -17,6 +17,7 @@ export interface PanelOptions extends common.SingleStatBaseOptions { minVizHeight: number; minVizWidth: number; showUnfilled: boolean; + valueMode: common.BarGaugeValueMode; } export const defaultPanelOptions: Partial = { @@ -24,4 +25,5 @@ export const defaultPanelOptions: Partial = { minVizHeight: 10, minVizWidth: 0, showUnfilled: true, + valueMode: common.BarGaugeValueMode.Color, }; diff --git a/public/app/plugins/panel/table/TableCellOptionEditor.tsx b/public/app/plugins/panel/table/TableCellOptionEditor.tsx index ba23e886ddd..8de4e823063 100644 --- a/public/app/plugins/panel/table/TableCellOptionEditor.tsx +++ b/public/app/plugins/panel/table/TableCellOptionEditor.tsx @@ -1,10 +1,11 @@ +import { css } from '@emotion/css'; import { merge } from 'lodash'; import React, { useState } from 'react'; -import { SelectableValue } from '@grafana/data'; +import { GrafanaTheme2, SelectableValue } from '@grafana/data'; import { config } from '@grafana/runtime'; import { TableCellOptions } from '@grafana/schema'; -import { Field, Select, TableCellDisplayMode } from '@grafana/ui'; +import { Field, Select, TableCellDisplayMode, useStyles2 } from '@grafana/ui'; import { BarGaugeCellOptionsEditor } from './cells/BarGaugeCellOptionsEditor'; import { ColorBackgroundCellOptionsEditor } from './cells/ColorBackgroundCellOptionsEditor'; @@ -25,8 +26,8 @@ interface Props { export const TableCellOptionEditor = ({ value, onChange }: Props) => { const cellType = value.type; + const styles = useStyles2(getStyles); const currentMode = cellDisplayModeOptions.find((o) => o.value!.type === cellType)!; - let [settingCache, setSettingCache] = useState>({}); // Update display mode on change @@ -56,7 +57,7 @@ export const TableCellOptionEditor = ({ value, onChange }: Props) => { // Setup and inject editor return ( - <> +
+ + + + + + ); +} + const barGaugeOpts: SelectableValue[] = [ { value: BarGaugeDisplayMode.Basic, label: 'Basic' }, { value: BarGaugeDisplayMode.Gradient, label: 'Gradient' }, { value: BarGaugeDisplayMode.Lcd, label: 'Retro LCD' }, ]; -export const BarGaugeCellOptionsEditor = ({ - cellOptions, - onChange, -}: TableCellEditorProps) => { - // Set the display mode on change - const onCellOptionsChange = (v: SelectableValue) => { - cellOptions.mode = v.value; - onChange(cellOptions); - }; - - return ( - - - - - + + + {opts.reducer && !isBool && ( + <> + + + )} +
+ ); +}; + +const getStyles = (theme: GrafanaTheme2) => { + return { + spot: css` + display: flex; + flex-direction: row; + align-items: center; + align-content: flex-end; + gap: 4px; + `, + }; +}; + +export const fieldValueMatcherItem: FieldMatcherUIRegistryItem = { + id: FieldMatcherID.byValue, + component: FieldValueMatcherEditor, + matcher: fieldMatchers.get(FieldMatcherID.byValue), + name: 'Fields with values', + description: 'Set properties for fields with reducer condition', + optionsToLabel: (options) => `${options?.reducer} ${options?.op} ${options?.value}`, +}; diff --git a/packages/grafana-ui/src/components/MatchersUI/fieldMatchersUI.ts b/packages/grafana-ui/src/components/MatchersUI/fieldMatchersUI.ts index 72c22e193c5..fe105151e5d 100644 --- a/packages/grafana-ui/src/components/MatchersUI/fieldMatchersUI.ts +++ b/packages/grafana-ui/src/components/MatchersUI/fieldMatchersUI.ts @@ -4,6 +4,7 @@ import { fieldNameByRegexMatcherItem } from './FieldNameByRegexMatcherEditor'; import { fieldNameMatcherItem } from './FieldNameMatcherEditor'; import { fieldNamesMatcherItem } from './FieldNamesMatcherEditor'; import { fieldTypeMatcherItem } from './FieldTypeMatcherEditor'; +import { fieldValueMatcherItem } from './FieldValueMatcher'; import { fieldsByFrameRefIdItem } from './FieldsByFrameRefIdMatcher'; import { FieldMatcherUIRegistryItem } from './types'; @@ -13,4 +14,5 @@ export const fieldMatchersUI = new Registry>(() fieldTypeMatcherItem, fieldsByFrameRefIdItem, fieldNamesMatcherItem, + fieldValueMatcherItem, ]); diff --git a/public/app/features/dashboard/components/PanelEditor/getFieldOverrideElements.tsx b/public/app/features/dashboard/components/PanelEditor/getFieldOverrideElements.tsx index 0da94a6fafd..93b40a518f5 100644 --- a/public/app/features/dashboard/components/PanelEditor/getFieldOverrideElements.tsx +++ b/public/app/features/dashboard/components/PanelEditor/getFieldOverrideElements.tsx @@ -10,6 +10,7 @@ import { DynamicConfigValue, ConfigOverrideRule, GrafanaTheme2, + fieldMatchers, } from '@grafana/data'; import { fieldMatchersUI, useStyles2, ValuePicker } from '@grafana/ui'; import { getDataLinksVariableSuggestions } from 'app/features/panel/panellinks/link_srv'; @@ -46,13 +47,19 @@ export function getFieldOverrideCategories( }; const onOverrideAdd = (value: SelectableValue) => { + const info = fieldMatchers.get(value.value!); + if (!info) { + return; + } + props.onFieldConfigsChange({ ...currentFieldConfig, overrides: [ ...currentFieldConfig.overrides, { matcher: { - id: value.value!, + id: info.id, + options: info.defaultOptions, }, properties: [], }, diff --git a/public/app/plugins/panel/geomap/editor/StyleRuleEditor.tsx b/public/app/plugins/panel/geomap/editor/StyleRuleEditor.tsx index 1f4294db808..d395569d2b7 100644 --- a/public/app/plugins/panel/geomap/editor/StyleRuleEditor.tsx +++ b/public/app/plugins/panel/geomap/editor/StyleRuleEditor.tsx @@ -5,12 +5,14 @@ import { useObservable } from 'react-use'; import { Observable } from 'rxjs'; import { GrafanaTheme2, SelectableValue, StandardEditorProps, StandardEditorsRegistryItem } from '@grafana/data'; +import { ComparisonOperation } from '@grafana/schema'; import { Button, InlineField, InlineFieldRow, Select, useStyles2 } from '@grafana/ui'; +import { comparisonOperationOptions } from '@grafana/ui/src/components/MatchersUI/FieldValueMatcher'; import { NumberInput } from 'app/core/components/OptionsUI/NumberInput'; import { DEFAULT_STYLE_RULE } from '../layers/data/geojsonLayer'; import { defaultStyleConfig, StyleConfig } from '../style/types'; -import { ComparisonOperation, FeatureStyleConfig } from '../types'; +import { FeatureStyleConfig } from '../types'; import { getUniqueFeatureValues, LayerContentInfo } from '../utils/getFeatures'; import { getSelectionInfo } from '../utils/selection'; @@ -21,15 +23,6 @@ export interface StyleRuleEditorSettings { layerInfo: Observable; } -const comparators = [ - { label: '==', value: ComparisonOperation.EQ }, - { label: '!=', value: ComparisonOperation.NEQ }, - { label: '>', value: ComparisonOperation.GT }, - { label: '>=', value: ComparisonOperation.GTE }, - { label: '<', value: ComparisonOperation.LT }, - { label: '<=', value: ComparisonOperation.LTE }, -]; - type Props = StandardEditorProps; export const StyleRuleEditor = ({ value, onChange, item, context }: Props) => { @@ -148,8 +141,8 @@ export const StyleRuleEditor = ({ value, onChange, item, context }: Props) => {