From 46a77c007414b2623ab5b05789d6d67efe646d81 Mon Sep 17 00:00:00 2001 From: Matthew Jacobson Date: Fri, 16 Feb 2024 15:17:07 -0500 Subject: [PATCH 0001/1406] Alerting: Validate upgraded receivers early to display in preview (#82956) Previously receivers were only validated before saving the alertmanager configuration. This is a suboptimal experience for those upgrading with preview as the failed channel upgrade will return an API error instead of being summarized in the table. --- pkg/services/ngalert/migration/channel.go | 30 +++++++++++- .../ngalert/migration/channel_test.go | 48 +++++++++++++++++-- pkg/services/ngalert/migration/persist.go | 21 +------- 3 files changed, 72 insertions(+), 27 deletions(-) diff --git a/pkg/services/ngalert/migration/channel.go b/pkg/services/ngalert/migration/channel.go index 562f1ff0fa..8de88d310e 100644 --- a/pkg/services/ngalert/migration/channel.go +++ b/pkg/services/ngalert/migration/channel.go @@ -3,10 +3,12 @@ package migration import ( "context" "encoding/base64" + "encoding/json" "errors" "fmt" "time" + alertingNotify "github.com/grafana/alerting/notify" "github.com/prometheus/alertmanager/pkg/labels" "github.com/prometheus/common/model" @@ -74,14 +76,38 @@ func (om *OrgMigration) createReceiver(c *legacymodels.AlertNotification) (*apim return nil, err } - return &apimodels.PostableGrafanaReceiver{ + recv := &apimodels.PostableGrafanaReceiver{ UID: c.UID, Name: c.Name, Type: c.Type, DisableResolveMessage: c.DisableResolveMessage, Settings: data, SecureSettings: secureSettings, - }, nil + } + err = validateReceiver(recv, om.encryptionService.GetDecryptedValue) + if err != nil { + return nil, err + } + return recv, nil +} + +// validateReceiver validates a receiver by building the configuration and checking for errors. +func validateReceiver(receiver *apimodels.PostableGrafanaReceiver, decrypt func(ctx context.Context, sjd map[string][]byte, key, fallback string) string) error { + var ( + cfg = &alertingNotify.GrafanaIntegrationConfig{ + UID: receiver.UID, + Name: receiver.Name, + Type: receiver.Type, + DisableResolveMessage: receiver.DisableResolveMessage, + Settings: json.RawMessage(receiver.Settings), + SecureSettings: receiver.SecureSettings, + } + ) + + _, err := alertingNotify.BuildReceiverConfiguration(context.Background(), &alertingNotify.APIReceiver{ + GrafanaIntegrations: alertingNotify.GrafanaIntegrations{Integrations: []*alertingNotify.GrafanaIntegrationConfig{cfg}}, + }, decrypt) + return err } // createRoute creates a route from a legacy notification channel, and matches using a label based on the channel UID. diff --git a/pkg/services/ngalert/migration/channel_test.go b/pkg/services/ngalert/migration/channel_test.go index 08cb54ec2d..240a2fcf1c 100644 --- a/pkg/services/ngalert/migration/channel_test.go +++ b/pkg/services/ngalert/migration/channel_test.go @@ -4,6 +4,7 @@ import ( "context" "encoding/base64" "encoding/json" + "errors" "fmt" "testing" "time" @@ -118,7 +119,7 @@ func createNotChannel(t *testing.T, uid string, id int64, name string, isDefault Type: "email", SendReminder: frequency > 0, Frequency: frequency, - Settings: simplejson.New(), + Settings: simplejson.NewFromAny(map[string]any{"addresses": "example"}), IsDefault: isDefault, Created: now, Updated: now, @@ -132,6 +133,20 @@ func createBasicNotChannel(t *testing.T, notType string) *legacymodels.AlertNoti return a } +func createBrokenNotChannel(t *testing.T) *legacymodels.AlertNotification { + t.Helper() + return &legacymodels.AlertNotification{ + UID: "uid", + ID: 1, + Name: "broken email", + Type: "email", + Settings: simplejson.NewFromAny(map[string]any{ + "something": "some value", // Missing required field addresses. + }), + SecureSettings: map[string][]byte{}, + } +} + func TestCreateReceivers(t *testing.T) { tc := []struct { name string @@ -154,6 +169,11 @@ func TestCreateReceivers(t *testing.T) { channel: createBasicNotChannel(t, "sensu"), expErr: fmt.Errorf("'sensu': %w", ErrDiscontinued), }, + { + name: "when channel is misconfigured return error", + channel: createBrokenNotChannel(t), + expErr: errors.New(`failed to validate integration "broken email" (UID uid) of type "email": could not find addresses in settings`), + }, } sqlStore := db.InitTestDB(t) @@ -266,7 +286,7 @@ func TestMigrateNotificationChannelSecureSettings(t *testing.T) { t.Run(tt.name, func(t *testing.T) { service := NewTestMigrationService(t, sqlStore, nil) m := service.newOrgMigration(1) - recv, err := m.createReceiver(tt.channel) + settings, secureSettings, err := m.migrateSettingsToSecureSettings(tt.channel.Type, tt.channel.Settings, tt.channel.SecureSettings) if tt.expErr != nil { require.Error(t, err) require.EqualError(t, err, tt.expErr.Error()) @@ -274,6 +294,8 @@ func TestMigrateNotificationChannelSecureSettings(t *testing.T) { } require.NoError(t, err) + recv := createReceiverNoValidation(t, tt.channel, settings, secureSettings) + if len(tt.expRecv.SecureSettings) > 0 { require.NotEqual(t, tt.expRecv, recv) // Make sure they were actually encrypted at first. } @@ -300,8 +322,9 @@ func TestMigrateNotificationChannelSecureSettings(t *testing.T) { channel.SecureSettings[key] = []byte(legacyEncryptFn("secure " + key)) } }) - recv, err := m.createReceiver(channel) + settings, secure, err := m.migrateSettingsToSecureSettings(channel.Type, channel.Settings, channel.SecureSettings) require.NoError(t, err) + recv := createReceiverNoValidation(t, channel, settings, secure) require.Equal(t, nType, recv.Type) if len(secureSettings) > 0 { @@ -335,8 +358,9 @@ func TestMigrateNotificationChannelSecureSettings(t *testing.T) { channel.Settings.Set(key, "secure "+key) } }) - recv, err := m.createReceiver(channel) + settings, secure, err := m.migrateSettingsToSecureSettings(channel.Type, channel.Settings, channel.SecureSettings) require.NoError(t, err) + recv := createReceiverNoValidation(t, channel, settings, secure) require.Equal(t, nType, recv.Type) if len(secureSettings) > 0 { @@ -439,12 +463,26 @@ func TestSetupAlertmanagerConfig(t *testing.T) { } } +func createReceiverNoValidation(t *testing.T, c *legacymodels.AlertNotification, settings *simplejson.Json, secureSettings map[string]string) *apimodels.PostableGrafanaReceiver { + data, err := settings.MarshalJSON() + require.NoError(t, err) + + return &apimodels.PostableGrafanaReceiver{ + UID: c.UID, + Name: c.Name, + Type: c.Type, + DisableResolveMessage: c.DisableResolveMessage, + Settings: data, + SecureSettings: secureSettings, + } +} + func createPostableGrafanaReceiver(uid string, name string) *apimodels.PostableGrafanaReceiver { return &apimodels.PostableGrafanaReceiver{ UID: uid, Type: "email", Name: name, - Settings: apimodels.RawMessage("{}"), + Settings: apimodels.RawMessage(`{"addresses":"example"}`), SecureSettings: map[string]string{}, } } diff --git a/pkg/services/ngalert/migration/persist.go b/pkg/services/ngalert/migration/persist.go index ada1247afb..032d37a7c2 100644 --- a/pkg/services/ngalert/migration/persist.go +++ b/pkg/services/ngalert/migration/persist.go @@ -7,8 +7,6 @@ import ( "fmt" "sort" - alertingNotify "github.com/grafana/alerting/notify" - "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/accesscontrol" legacymodels "github.com/grafana/grafana/pkg/services/alerting/models" @@ -558,24 +556,7 @@ func (sync *sync) extractChannels(ctx context.Context, alert *legacymodels.Alert func (sync *sync) validateAlertmanagerConfig(config *apiModels.PostableUserConfig) error { for _, r := range config.AlertmanagerConfig.Receivers { for _, gr := range r.GrafanaManagedReceivers { - data, err := gr.Settings.MarshalJSON() - if err != nil { - return err - } - var ( - cfg = &alertingNotify.GrafanaIntegrationConfig{ - UID: gr.UID, - Name: gr.Name, - Type: gr.Type, - DisableResolveMessage: gr.DisableResolveMessage, - Settings: data, - SecureSettings: gr.SecureSettings, - } - ) - - _, err = alertingNotify.BuildReceiverConfiguration(context.Background(), &alertingNotify.APIReceiver{ - GrafanaIntegrations: alertingNotify.GrafanaIntegrations{Integrations: []*alertingNotify.GrafanaIntegrationConfig{cfg}}, - }, sync.getDecryptedValue) + err := validateReceiver(gr, sync.getDecryptedValue) if err != nil { return err } From f23f50f58d7ab5cb1fd88b42b6c58ec09c1a159d Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Fri, 16 Feb 2024 16:59:11 -0800 Subject: [PATCH 0002/1406] Expressions: Add model struct for the query types (not map[string]any) (#82745) --- .../feature-toggles/index.md | 1 + .../src/types/featureToggles.gen.ts | 1 + pkg/expr/classic/classic.go | 27 +- pkg/expr/commands.go | 9 +- pkg/expr/commands_test.go | 2 +- pkg/expr/graph_test.go | 5 +- pkg/expr/mathexp/reduce.go | 42 +- pkg/expr/mathexp/reduce_test.go | 6 +- pkg/expr/models.go | 94 + pkg/expr/nodes.go | 39 +- pkg/expr/reader.go | 156 ++ pkg/services/featuremgmt/registry.go | 7 + pkg/services/featuremgmt/toggles_gen.csv | 1 + pkg/services/featuremgmt/toggles_gen.go | 4 + pkg/services/featuremgmt/toggles_gen.json | 2094 ++++++++--------- 15 files changed, 1393 insertions(+), 1095 deletions(-) create mode 100644 pkg/expr/models.go create mode 100644 pkg/expr/reader.go 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 6e80b789cb..10380c910e 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -179,6 +179,7 @@ Experimental features might be changed or removed without prior notice. | `nodeGraphDotLayout` | Changed the layout algorithm for the node graph | | `newPDFRendering` | New implementation for the dashboard to PDF rendering | | `kubernetesAggregator` | Enable grafana aggregator | +| `expressionParser` | Enable new expression parser | ## Development feature toggles diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 4a43b10aa2..f5a2e721c1 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -180,6 +180,7 @@ export interface FeatureToggles { groupToNestedTableTransformation?: boolean; newPDFRendering?: boolean; kubernetesAggregator?: boolean; + expressionParser?: boolean; groupByVariable?: boolean; alertingUpgradeDryrunOnStart?: boolean; } diff --git a/pkg/expr/classic/classic.go b/pkg/expr/classic/classic.go index 01288e0b01..b920fc8a2f 100644 --- a/pkg/expr/classic/classic.go +++ b/pkg/expr/classic/classic.go @@ -275,21 +275,12 @@ type ConditionReducerJSON struct { // Params []any `json:"params"` (Unused) } -// UnmarshalConditionsCmd creates a new ConditionsCmd. -func UnmarshalConditionsCmd(rawQuery map[string]any, refID string) (*ConditionsCmd, error) { - jsonFromM, err := json.Marshal(rawQuery["conditions"]) - if err != nil { - return nil, fmt.Errorf("failed to remarshal classic condition body: %w", err) - } - var ccj []ConditionJSON - if err = json.Unmarshal(jsonFromM, &ccj); err != nil { - return nil, fmt.Errorf("failed to unmarshal remarshaled classic condition body: %w", err) - } - +func NewConditionCmd(refID string, ccj []ConditionJSON) (*ConditionsCmd, error) { c := &ConditionsCmd{ RefID: refID, } + var err error for i, cj := range ccj { cond := condition{} @@ -316,6 +307,18 @@ func UnmarshalConditionsCmd(rawQuery map[string]any, refID string) (*ConditionsC c.Conditions = append(c.Conditions, cond) } - return c, nil } + +// UnmarshalConditionsCmd creates a new ConditionsCmd. +func UnmarshalConditionsCmd(rawQuery map[string]any, refID string) (*ConditionsCmd, error) { + jsonFromM, err := json.Marshal(rawQuery["conditions"]) + if err != nil { + return nil, fmt.Errorf("failed to remarshal classic condition body: %w", err) + } + var ccj []ConditionJSON + if err = json.Unmarshal(jsonFromM, &ccj); err != nil { + return nil, fmt.Errorf("failed to unmarshal remarshaled classic condition body: %w", err) + } + return NewConditionCmd(refID, ccj) +} diff --git a/pkg/expr/commands.go b/pkg/expr/commands.go index d68a6e6d16..32bc9b8636 100644 --- a/pkg/expr/commands.go +++ b/pkg/expr/commands.go @@ -77,14 +77,14 @@ func (gm *MathCommand) Execute(ctx context.Context, _ time.Time, vars mathexp.Va // ReduceCommand is an expression command for reduction of a timeseries such as a min, mean, or max. type ReduceCommand struct { - Reducer string + Reducer mathexp.ReducerID VarToReduce string refID string seriesMapper mathexp.ReduceMapper } // NewReduceCommand creates a new ReduceCMD. -func NewReduceCommand(refID, reducer, varToReduce string, mapper mathexp.ReduceMapper) (*ReduceCommand, error) { +func NewReduceCommand(refID string, reducer mathexp.ReducerID, varToReduce string, mapper mathexp.ReduceMapper) (*ReduceCommand, error) { _, err := mathexp.GetReduceFunc(reducer) if err != nil { return nil, err @@ -114,10 +114,11 @@ func UnmarshalReduceCommand(rn *rawNode) (*ReduceCommand, error) { if !ok { return nil, errors.New("no reducer specified") } - redFunc, ok := rawReducer.(string) + redString, ok := rawReducer.(string) if !ok { return nil, fmt.Errorf("expected reducer to be a string, got %T", rawReducer) } + redFunc := mathexp.ReducerID(strings.ToLower(redString)) var mapper mathexp.ReduceMapper = nil settings, ok := rn.Query["settings"] @@ -163,7 +164,7 @@ func (gr *ReduceCommand) Execute(ctx context.Context, _ time.Time, vars mathexp. _, span := tracer.Start(ctx, "SSE.ExecuteReduce") defer span.End() - span.SetAttributes(attribute.String("reducer", gr.Reducer)) + span.SetAttributes(attribute.String("reducer", string(gr.Reducer))) newRes := mathexp.Results{} for i, val := range vars[gr.VarToReduce].Values { diff --git a/pkg/expr/commands_test.go b/pkg/expr/commands_test.go index 6fa837eb48..7fd5832917 100644 --- a/pkg/expr/commands_test.go +++ b/pkg/expr/commands_test.go @@ -210,7 +210,7 @@ func TestReduceExecute(t *testing.T) { }) } -func randomReduceFunc() string { +func randomReduceFunc() mathexp.ReducerID { res := mathexp.GetSupportedReduceFuncs() return res[rand.Intn(len(res))] } diff --git a/pkg/expr/graph_test.go b/pkg/expr/graph_test.go index 11be8e5a35..fafca8f687 100644 --- a/pkg/expr/graph_test.go +++ b/pkg/expr/graph_test.go @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/services/datasources" + "github.com/grafana/grafana/pkg/services/featuremgmt" ) func TestServicebuildPipeLine(t *testing.T) { @@ -231,7 +232,9 @@ func TestServicebuildPipeLine(t *testing.T) { expectedOrder: []string{"B", "A"}, }, } - s := Service{} + s := Service{ + features: featuremgmt.WithFeatures(featuremgmt.FlagExpressionParser), + } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { nodes, err := s.buildPipeline(tt.req) diff --git a/pkg/expr/mathexp/reduce.go b/pkg/expr/mathexp/reduce.go index b4b6611eb8..76d74ca459 100644 --- a/pkg/expr/mathexp/reduce.go +++ b/pkg/expr/mathexp/reduce.go @@ -3,13 +3,30 @@ package mathexp import ( "fmt" "math" - "strings" "github.com/grafana/grafana-plugin-sdk-go/data" ) type ReducerFunc = func(fv *Float64Field) *float64 +// The reducer function +// +enum +type ReducerID string + +const ( + ReducerSum ReducerID = "sum" + ReducerMean ReducerID = "mean" + ReducerMin ReducerID = "min" + ReducerMax ReducerID = "max" + ReducerCount ReducerID = "count" + ReducerLast ReducerID = "last" +) + +// GetSupportedReduceFuncs returns collection of supported function names +func GetSupportedReduceFuncs() []ReducerID { + return []ReducerID{ReducerSum, ReducerMean, ReducerMin, ReducerMax, ReducerCount, ReducerLast} +} + func Sum(fv *Float64Field) *float64 { var sum float64 for i := 0; i < fv.Len(); i++ { @@ -81,34 +98,29 @@ func Last(fv *Float64Field) *float64 { return fv.GetValue(fv.Len() - 1) } -func GetReduceFunc(rFunc string) (ReducerFunc, error) { - switch strings.ToLower(rFunc) { - case "sum": +func GetReduceFunc(rFunc ReducerID) (ReducerFunc, error) { + switch rFunc { + case ReducerSum: return Sum, nil - case "mean": + case ReducerMean: return Avg, nil - case "min": + case ReducerMin: return Min, nil - case "max": + case ReducerMax: return Max, nil - case "count": + case ReducerCount: return Count, nil - case "last": + case ReducerLast: return Last, nil default: return nil, fmt.Errorf("reduction %v not implemented", rFunc) } } -// GetSupportedReduceFuncs returns collection of supported function names -func GetSupportedReduceFuncs() []string { - return []string{"sum", "mean", "min", "max", "count", "last"} -} - // Reduce turns the Series into a Number based on the given reduction function // if ReduceMapper is defined it applies it to the provided series and performs reduction of the resulting series. // Otherwise, the reduction operation is done against the original series. -func (s Series) Reduce(refID, rFunc string, mapper ReduceMapper) (Number, error) { +func (s Series) Reduce(refID string, rFunc ReducerID, mapper ReduceMapper) (Number, error) { var l data.Labels if s.GetLabels() != nil { l = s.GetLabels().Copy() diff --git a/pkg/expr/mathexp/reduce_test.go b/pkg/expr/mathexp/reduce_test.go index 2f9c7a2a81..c4d786cad8 100644 --- a/pkg/expr/mathexp/reduce_test.go +++ b/pkg/expr/mathexp/reduce_test.go @@ -30,7 +30,7 @@ var seriesEmpty = Vars{ func TestSeriesReduce(t *testing.T) { var tests = []struct { name string - red string + red ReducerID vars Vars varToReduce string errIs require.ErrorAssertionFunc @@ -217,7 +217,7 @@ var seriesNonNumbers = Vars{ func TestSeriesReduceDropNN(t *testing.T) { var tests = []struct { name string - red string + red ReducerID vars Vars varToReduce string results Results @@ -304,7 +304,7 @@ func TestSeriesReduceReplaceNN(t *testing.T) { replaceWith := rand.Float64() var tests = []struct { name string - red string + red ReducerID vars Vars varToReduce string results Results diff --git a/pkg/expr/models.go b/pkg/expr/models.go new file mode 100644 index 0000000000..4480331a51 --- /dev/null +++ b/pkg/expr/models.go @@ -0,0 +1,94 @@ +package expr + +import ( + "github.com/grafana/grafana/pkg/expr/classic" + "github.com/grafana/grafana/pkg/expr/mathexp" +) + +// Supported expression types +// +enum +type QueryType string + +const ( + // Apply a mathematical expression to results + QueryTypeMath QueryType = "math" + + // Reduce query results + QueryTypeReduce QueryType = "reduce" + + // Resample query results + QueryTypeResample QueryType = "resample" + + // Classic query + QueryTypeClassic QueryType = "classic_conditions" + + // Threshold + QueryTypeThreshold QueryType = "threshold" +) + +type MathQuery struct { + // General math expression + Expression string `json:"expression" jsonschema:"minLength=1,example=$A + 1,example=$A/$B"` +} + +type ReduceQuery struct { + // Reference to single query result + Expression string `json:"expression" jsonschema:"minLength=1,example=$A"` + + // The reducer + Reducer mathexp.ReducerID `json:"reducer"` + + // Reducer Options + Settings *ReduceSettings `json:"settings,omitempty"` +} + +// QueryType = resample +type ResampleQuery struct { + // The math expression + Expression string `json:"expression" jsonschema:"minLength=1,example=$A + 1,example=$A"` + + // The time durration + Window string `json:"window" jsonschema:"minLength=1,example=1w,example=10m"` + + // The downsample function + Downsampler string `json:"downsampler"` + + // The upsample function + Upsampler string `json:"upsampler"` +} + +type ThresholdQuery struct { + // Reference to single query result + Expression string `json:"expression" jsonschema:"minLength=1,example=$A"` + + // Threshold Conditions + Conditions []ThresholdConditionJSON `json:"conditions"` +} + +type ClassicQuery struct { + Conditions []classic.ConditionJSON `json:"conditions"` +} + +//------------------------------- +// Non-query commands +//------------------------------- + +type ReduceSettings struct { + // Non-number reduce behavior + Mode ReduceMode `json:"mode"` + + // Only valid when mode is replace + ReplaceWithValue *float64 `json:"replaceWithValue,omitempty"` +} + +// Non-Number behavior mode +// +enum +type ReduceMode string + +const ( + // Drop non-numbers + ReduceModeDrop ReduceMode = "dropNN" + + // Replace non-numbers + ReduceModeReplace ReduceMode = "replaceNN" +) diff --git a/pkg/expr/nodes.go b/pkg/expr/nodes.go index 32883d305d..3f20267d71 100644 --- a/pkg/expr/nodes.go +++ b/pkg/expr/nodes.go @@ -10,6 +10,8 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/data" + jsonitersdk "github.com/grafana/grafana-plugin-sdk-go/data/utils/jsoniter" + jsoniter "github.com/json-iterator/go" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" "gonum.org/v1/gonum/graph/simple" @@ -46,14 +48,22 @@ type rawNode struct { idx int64 } -func GetExpressionCommandType(rawQuery map[string]any) (c CommandType, err error) { +func getExpressionCommandTypeString(rawQuery map[string]any) (string, error) { rawType, ok := rawQuery["type"] if !ok { - return c, errors.New("no expression command type in query") + return "", errors.New("no expression command type in query") } typeString, ok := rawType.(string) if !ok { - return c, fmt.Errorf("expected expression command type to be a string, got type %T", rawType) + return "", fmt.Errorf("expected expression command type to be a string, got type %T", rawType) + } + return typeString, nil +} + +func GetExpressionCommandType(rawQuery map[string]any) (c CommandType, err error) { + typeString, err := getExpressionCommandTypeString(rawQuery) + if err != nil { + return c, err } return ParseCommandType(typeString) } @@ -111,6 +121,29 @@ func buildCMDNode(rn *rawNode, toggles featuremgmt.FeatureToggles) (*CMDNode, er CMDType: commandType, } + if toggles.IsEnabledGlobally(featuremgmt.FlagExpressionParser) { + rn.QueryType, err = getExpressionCommandTypeString(rn.Query) + if err != nil { + return nil, err // should not happen because the command was parsed first thing + } + + // NOTE: this structure of this is weird now, because it is targeting a structure + // where this is actually run in the root loop, however we want to verify the individual + // node parsing before changing the full tree parser + reader, err := NewExpressionQueryReader(toggles) + if err != nil { + return nil, err + } + + iter := jsoniter.ParseBytes(jsoniter.ConfigDefault, rn.QueryRaw) + q, err := reader.ReadQuery(rn, jsonitersdk.NewIterator(iter)) + if err != nil { + return nil, err + } + node.Command = q.Command + return node, err + } + switch commandType { case TypeMath: node.Command, err = UnmarshalMathCommand(rn) diff --git a/pkg/expr/reader.go b/pkg/expr/reader.go new file mode 100644 index 0000000000..e5563bb95d --- /dev/null +++ b/pkg/expr/reader.go @@ -0,0 +1,156 @@ +package expr + +import ( + "fmt" + "strings" + + "github.com/grafana/grafana-plugin-sdk-go/data/utils/jsoniter" + + "github.com/grafana/grafana/pkg/expr/classic" + "github.com/grafana/grafana/pkg/expr/mathexp" + "github.com/grafana/grafana/pkg/services/featuremgmt" +) + +// Once we are comfortable with the parsing logic, this struct will +// be merged/replace the existing Query struct in grafana/pkg/expr/transform.go +type ExpressionQuery struct { + RefID string + Command Command +} + +type ExpressionQueryReader struct { + features featuremgmt.FeatureToggles +} + +func NewExpressionQueryReader(features featuremgmt.FeatureToggles) (*ExpressionQueryReader, error) { + h := &ExpressionQueryReader{ + features: features, + } + return h, nil +} + +// ReadQuery implements query.TypedQueryHandler. +func (h *ExpressionQueryReader) ReadQuery( + // Properties that have been parsed off the same node + common *rawNode, // common query.CommonQueryProperties + // An iterator with context for the full node (include common values) + iter *jsoniter.Iterator, +) (eq ExpressionQuery, err error) { + referenceVar := "" + eq.RefID = common.RefID + qt := QueryType(common.QueryType) + switch qt { + case QueryTypeMath: + q := &MathQuery{} + err = iter.ReadVal(q) + if err == nil { + eq.Command, err = NewMathCommand(common.RefID, q.Expression) + } + + case QueryTypeReduce: + var mapper mathexp.ReduceMapper = nil + q := &ReduceQuery{} + err = iter.ReadVal(q) + if err == nil { + referenceVar, err = getReferenceVar(q.Expression, common.RefID) + } + if err == nil && q.Settings != nil { + switch q.Settings.Mode { + case ReduceModeDrop: + mapper = mathexp.DropNonNumber{} + case ReduceModeReplace: + if q.Settings.ReplaceWithValue == nil { + err = fmt.Errorf("setting replaceWithValue must be specified when mode is '%s'", q.Settings.Mode) + } + mapper = mathexp.ReplaceNonNumberWithValue{Value: *q.Settings.ReplaceWithValue} + default: + err = fmt.Errorf("unsupported reduce mode") + } + } + if err == nil { + eq.Command, err = NewReduceCommand(common.RefID, + q.Reducer, referenceVar, mapper) + } + + case QueryTypeResample: + q := &ResampleQuery{} + err = iter.ReadVal(q) + if err == nil && common.TimeRange == nil { + err = fmt.Errorf("missing time range in query") + } + if err == nil { + referenceVar, err = getReferenceVar(q.Expression, common.RefID) + } + if err == nil { + // tr := legacydata.NewDataTimeRange(common.TimeRange.From, common.TimeRange.To) + // AbsoluteTimeRange{ + // From: tr.GetFromAsTimeUTC(), + // To: tr.GetToAsTimeUTC(), + // }) + eq.Command, err = NewResampleCommand(common.RefID, + q.Window, + referenceVar, + q.Downsampler, + q.Upsampler, + common.TimeRange) + } + + case QueryTypeClassic: + q := &ClassicQuery{} + err = iter.ReadVal(q) + if err == nil { + eq.Command, err = classic.NewConditionCmd(common.RefID, q.Conditions) + } + + case QueryTypeThreshold: + q := &ThresholdQuery{} + err = iter.ReadVal(q) + if err == nil { + referenceVar, err = getReferenceVar(q.Expression, common.RefID) + } + if err == nil { + // we only support one condition for now, we might want to turn this in to "OR" expressions later + if len(q.Conditions) != 1 { + return eq, fmt.Errorf("threshold expression requires exactly one condition") + } + firstCondition := q.Conditions[0] + + threshold, err := NewThresholdCommand(common.RefID, referenceVar, firstCondition.Evaluator.Type, firstCondition.Evaluator.Params) + if err != nil { + return eq, fmt.Errorf("invalid condition: %w", err) + } + eq.Command = threshold + + if firstCondition.UnloadEvaluator != nil && h.features.IsEnabledGlobally(featuremgmt.FlagRecoveryThreshold) { + unloading, err := NewThresholdCommand(common.RefID, referenceVar, firstCondition.UnloadEvaluator.Type, firstCondition.UnloadEvaluator.Params) + unloading.Invert = true + if err != nil { + return eq, fmt.Errorf("invalid unloadCondition: %w", err) + } + var d Fingerprints + if firstCondition.LoadedDimensions != nil { + d, err = FingerprintsFromFrame(firstCondition.LoadedDimensions) + if err != nil { + return eq, fmt.Errorf("failed to parse loaded dimensions: %w", err) + } + } + eq.Command, err = NewHysteresisCommand(common.RefID, referenceVar, *threshold, *unloading, d) + if err != nil { + return eq, err + } + } + } + + default: + err = fmt.Errorf("unknown query type (%s)", common.QueryType) + } + return eq, err +} + +func getReferenceVar(exp string, refId string) (string, error) { + exp = strings.TrimPrefix(exp, "%") + if exp == "" { + return "", fmt.Errorf("no variable specified to reference for refId %v", refId) + } + return exp, nil +} diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 7497dce628..38e6caeaec 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1205,6 +1205,13 @@ var ( Owner: grafanaAppPlatformSquad, RequiresRestart: true, }, + { + Name: "expressionParser", + Description: "Enable new expression parser", + Stage: FeatureStageExperimental, + Owner: grafanaAppPlatformSquad, + RequiresRestart: true, + }, { Name: "groupByVariable", Description: "Enable groupBy variable support in scenes dashboards", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 7cdbf878fe..ab21b586a4 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -161,5 +161,6 @@ nodeGraphDotLayout,experimental,@grafana/observability-traces-and-profiling,fals groupToNestedTableTransformation,preview,@grafana/dataviz-squad,false,false,true newPDFRendering,experimental,@grafana/sharing-squad,false,false,false kubernetesAggregator,experimental,@grafana/grafana-app-platform-squad,false,true,false +expressionParser,experimental,@grafana/grafana-app-platform-squad,false,true,false groupByVariable,experimental,@grafana/dashboards-squad,false,false,false alertingUpgradeDryrunOnStart,GA,@grafana/alerting-squad,false,true,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index fc74cbdf50..e5ec782a98 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -655,6 +655,10 @@ const ( // Enable grafana aggregator FlagKubernetesAggregator = "kubernetesAggregator" + // FlagExpressionParser + // Enable new expression parser + FlagExpressionParser = "expressionParser" + // FlagGroupByVariable // Enable groupBy variable support in scenes dashboards FlagGroupByVariable = "groupByVariable" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index f2b2625553..2e32e1e954 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -5,287 +5,254 @@ "items": [ { "metadata": { - "name": "kubernetesQueryServiceRewrite", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "recoveryThreshold", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Rewrite requests targeting /ds/query to the query service", - "stage": "experimental", - "codeowner": "@grafana/grafana-app-platform-squad", - "requiresDevMode": true, + "description": "Enables feature recovery threshold (aka hysteresis) for threshold server-side expression", + "stage": "GA", + "codeowner": "@grafana/alerting-squad", "requiresRestart": true } }, { "metadata": { - "name": "managedPluginsInstall", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "teamHttpHeaders", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Install managed plugins directly from plugins catalog", - "stage": "preview", - "codeowner": "@grafana/plugins-platform-backend" + "description": "Enables datasources to apply team headers to the client requests", + "stage": "experimental", + "codeowner": "@grafana/identity-access-team" } }, { "metadata": { - "name": "canvasPanelPanZoom", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "logsExploreTableVisualisation", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Allow pan and zoom in canvas panel", - "stage": "preview", - "codeowner": "@grafana/dataviz-squad", + "description": "A table visualisation for logs in Explore", + "stage": "GA", + "codeowner": "@grafana/observability-logs", "frontend": true } }, { "metadata": { - "name": "queryOverLive", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "disableSecretsCompatibility", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Use Grafana Live WebSocket to execute backend queries", + "description": "Disable duplicated secret storage in legacy tables", "stage": "experimental", - "codeowner": "@grafana/grafana-app-platform-squad", - "frontend": true + "codeowner": "@grafana/hosted-grafana-team", + "requiresRestart": true } }, { "metadata": { - "name": "correlations", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "alertingBacktesting", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Correlations page", - "stage": "GA", - "codeowner": "@grafana/explore-squad", - "allowSelfServe": true + "description": "Rule backtesting API for alerting", + "stage": "experimental", + "codeowner": "@grafana/alerting-squad" } }, { "metadata": { - "name": "renderAuthJWT", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "alertStateHistoryLokiPrimary", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Uses JWT-based auth for rendering instead of relying on remote cache", - "stage": "preview", - "codeowner": "@grafana/grafana-as-code", - "hideFromAdminPage": true + "description": "Enable a remote Loki instance as the primary source for state history reads.", + "stage": "experimental", + "codeowner": "@grafana/alerting-squad" } }, { "metadata": { - "name": "sqlDatasourceDatabaseSelection", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "pluginsInstrumentationStatusSource", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Enables previous SQL data source dataset dropdown behavior", - "stage": "preview", - "codeowner": "@grafana/dataviz-squad", - "frontend": true, - "hideFromAdminPage": true + "description": "Include a status source label for plugin request metrics and logs", + "stage": "experimental", + "codeowner": "@grafana/plugins-platform-backend" } }, { "metadata": { - "name": "alertingInsights", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "enablePluginsTracingByDefault", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Show the new alerting insights landing page", - "stage": "GA", - "codeowner": "@grafana/alerting-squad", - "frontend": true, - "hideFromAdminPage": true + "description": "Enable plugin tracing for all external plugins", + "stage": "experimental", + "codeowner": "@grafana/plugins-platform-backend", + "requiresRestart": true } }, { "metadata": { - "name": "logsInfiniteScrolling", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "newFolderPicker", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Enables infinite scrolling for the Logs panel in Explore and Dashboards", + "description": "Enables the nested folder picker without having nested folders enabled", "stage": "experimental", - "codeowner": "@grafana/observability-logs", + "codeowner": "@grafana/grafana-frontend-platform", "frontend": true } }, { "metadata": { - "name": "kubernetesFeatureToggles", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "correlations", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Use the kubernetes API for feature toggle management in the frontend", - "stage": "experimental", - "codeowner": "@grafana/grafana-operator-experience-squad", - "frontend": true, - "hideFromAdminPage": true + "description": "Correlations page", + "stage": "GA", + "codeowner": "@grafana/explore-squad", + "allowSelfServe": true } }, { "metadata": { - "name": "dashboardSceneSolo", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "datasourceQueryMultiStatus", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Enables rendering dashboards using scenes for solo panels", + "description": "Introduce HTTP 207 Multi Status for api/ds/query", "stage": "experimental", - "codeowner": "@grafana/dashboards-squad", - "frontend": true - } - }, - { - "metadata": { - "name": "panelTitleSearch", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" - }, - "spec": { - "description": "Search for dashboards using panel title", - "stage": "preview", - "codeowner": "@grafana/grafana-app-platform-squad", - "hideFromAdminPage": true + "codeowner": "@grafana/plugins-platform-backend" } }, { "metadata": { - "name": "autoMigrateGraphPanel", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "sqlDatasourceDatabaseSelection", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Migrate old graph panel to supported time series panel - broken out from autoMigrateOldPanels to enable granular tracking", + "description": "Enables previous SQL data source dataset dropdown behavior", "stage": "preview", "codeowner": "@grafana/dataviz-squad", - "frontend": true - } - }, - { - "metadata": { - "name": "prometheusDataplane", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" - }, - "spec": { - "description": "Changes responses to from Prometheus to be compliant with the dataplane specification. In particular, when this feature toggle is active, the numeric `Field.Name` is set from 'Value' to the value of the `__name__` label.", - "stage": "GA", - "codeowner": "@grafana/observability-metrics", - "allowSelfServe": true + "frontend": true, + "hideFromAdminPage": true } }, { "metadata": { - "name": "cloudWatchLogsMonacoEditor", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "cloudWatchWildCardDimensionValues", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Enables the Monaco editor for CloudWatch Logs queries", + "description": "Fetches dimension values from CloudWatch to correctly label wildcard dimensions", "stage": "GA", "codeowner": "@grafana/aws-datasources", - "frontend": true, "allowSelfServe": true } }, { "metadata": { - "name": "metricsSummary", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "addFieldFromCalculationStatFunctions", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Enables metrics summary queries in the Tempo data source", - "stage": "experimental", - "codeowner": "@grafana/observability-traces-and-profiling", + "description": "Add cumulative and window functions to the add field from calculation transformation", + "stage": "preview", + "codeowner": "@grafana/dataviz-squad", "frontend": true } }, { "metadata": { - "name": "showDashboardValidationWarnings", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "canvasPanelPanZoom", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Show warnings when dashboards do not validate against the schema", - "stage": "experimental", - "codeowner": "@grafana/dashboards-squad" + "description": "Allow pan and zoom in canvas panel", + "stage": "preview", + "codeowner": "@grafana/dataviz-squad", + "frontend": true } }, { "metadata": { - "name": "recordedQueriesMulti", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "cloudRBACRoles", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Enables writing multiple items from a single query within Recorded Queries", - "stage": "GA", - "codeowner": "@grafana/observability-metrics" + "description": "Enabled grafana cloud specific RBAC roles", + "stage": "experimental", + "codeowner": "@grafana/identity-access-team", + "requiresRestart": true, + "hideFromDocs": true } }, { "metadata": { - "name": "reportingRetries", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "panelTitleSearch", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Enables rendering retries for the reporting feature", + "description": "Search for dashboards using panel title", "stage": "preview", - "codeowner": "@grafana/sharing-squad", - "requiresRestart": true + "codeowner": "@grafana/grafana-app-platform-squad", + "hideFromAdminPage": true } }, { "metadata": { - "name": "regressionTransformation", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "mysqlAnsiQuotes", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Enables regression analysis transformation", - "stage": "preview", - "codeowner": "@grafana/dataviz-squad", - "frontend": true + "description": "Use double quotes to escape keyword in a MySQL query", + "stage": "experimental", + "codeowner": "@grafana/backend-platform" } }, { "metadata": { - "name": "newFolderPicker", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "alertStateHistoryLokiSecondary", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Enables the nested folder picker without having nested folders enabled", + "description": "Enable Grafana to write alert state history to an external Loki instance in addition to Grafana annotations.", "stage": "experimental", - "codeowner": "@grafana/grafana-frontend-platform", - "frontend": true + "codeowner": "@grafana/alerting-squad" } }, { "metadata": { - "name": "grafanaAPIServerEnsureKubectlAccess", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "grafanaAPIServerWithExperimentalAPIs", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Start an additional https handler and write kubectl options", + "description": "Register experimental APIs with the k8s API server", "stage": "experimental", "codeowner": "@grafana/grafana-app-platform-squad", "requiresDevMode": true, @@ -294,51 +261,24 @@ }, { "metadata": { - "name": "datatrails", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" - }, - "spec": { - "description": "Enables the new core app datatrails", - "stage": "experimental", - "codeowner": "@grafana/dashboards-squad", - "frontend": true, - "hideFromDocs": true - } - }, - { - "metadata": { - "name": "alertingQueryOptimization", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "alertingSimplifiedRouting", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Optimizes eligible queries in order to reduce load on datasources", - "stage": "GA", + "description": "Enables users to easily configure alert notifications by specifying a contact point directly when editing or creating an alert rule", + "stage": "preview", "codeowner": "@grafana/alerting-squad" } }, { "metadata": { - "name": "disableEnvelopeEncryption", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" - }, - "spec": { - "description": "Disable envelope encryption (emergency only)", - "stage": "GA", - "codeowner": "@grafana/grafana-as-code", - "hideFromAdminPage": true - } - }, - { - "metadata": { - "name": "autoMigrateOldPanels", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "autoMigratePiechartPanel", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Migrate old angular panels to supported versions (graph, table-old, worldmap, etc)", + "description": "Migrate old piechart panel to supported piechart panel - broken out from autoMigrateOldPanels to enable granular tracking", "stage": "preview", "codeowner": "@grafana/dataviz-squad", "frontend": true @@ -346,578 +286,605 @@ }, { "metadata": { - "name": "dataConnectionsConsole", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "autoMigrateWorldmapPanel", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "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.", - "stage": "GA", - "codeowner": "@grafana/plugins-platform-backend", - "allowSelfServe": true + "description": "Migrate old worldmap panel to supported geomap panel - broken out from autoMigrateOldPanels to enable granular tracking", + "stage": "preview", + "codeowner": "@grafana/dataviz-squad", + "frontend": true } }, { "metadata": { - "name": "externalServiceAuth", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "prometheusIncrementalQueryInstrumentation", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Starts an OAuth2 authentication provider for external services", + "description": "Adds RudderStack events to incremental queries", "stage": "experimental", - "codeowner": "@grafana/identity-access-team", - "requiresDevMode": true + "codeowner": "@grafana/observability-metrics", + "frontend": true } }, { "metadata": { - "name": "grafanaAPIServerWithExperimentalAPIs", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "prometheusConfigOverhaulAuth", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Register experimental APIs with the k8s API server", - "stage": "experimental", - "codeowner": "@grafana/grafana-app-platform-squad", - "requiresDevMode": true, - "requiresRestart": true + "description": "Update the Prometheus configuration page with the new auth component", + "stage": "GA", + "codeowner": "@grafana/observability-metrics" } }, { "metadata": { - "name": "groupToNestedTableTransformation", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "prometheusPromQAIL", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Enables the group to nested table transformation", - "stage": "preview", - "codeowner": "@grafana/dataviz-squad", + "description": "Prometheus and AI/ML to assist users in creating a query", + "stage": "experimental", + "codeowner": "@grafana/observability-metrics", "frontend": true } }, { "metadata": { - "name": "lokiQuerySplittingConfig", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "panelFilterVariable", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Give users the option to configure split durations for Loki queries", + "description": "Enables use of the `systemPanelFilterVar` variable to filter panels in a dashboard", "stage": "experimental", - "codeowner": "@grafana/observability-logs", - "frontend": true + "codeowner": "@grafana/dashboards-squad", + "frontend": true, + "hideFromDocs": true } }, { "metadata": { - "name": "logsExploreTableVisualisation", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "pluginsSkipHostEnvVars", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "A table visualisation for logs in Explore", - "stage": "GA", - "codeowner": "@grafana/observability-logs", - "frontend": true + "description": "Disables passing host environment variable to plugin processes", + "stage": "experimental", + "codeowner": "@grafana/plugins-platform-backend" } }, { "metadata": { - "name": "awsAsyncQueryCaching", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "publicDashboardsEmailSharing", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Enable caching for async queries for Redshift and Athena. Requires that the datasource has caching and async query support enabled", - "stage": "GA", - "codeowner": "@grafana/aws-datasources" + "description": "Enables public dashboard sharing to be restricted to only allowed emails", + "stage": "preview", + "codeowner": "@grafana/sharing-squad", + "hideFromAdminPage": true, + "hideFromDocs": true } }, { "metadata": { - "name": "prometheusConfigOverhaulAuth", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "metricsSummary", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Update the Prometheus configuration page with the new auth component", - "stage": "GA", - "codeowner": "@grafana/observability-metrics" + "description": "Enables metrics summary queries in the Tempo data source", + "stage": "experimental", + "codeowner": "@grafana/observability-traces-and-profiling", + "frontend": true } }, { "metadata": { - "name": "alertingPreviewUpgrade", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "tableSharedCrosshair", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Show Unified Alerting preview and upgrade page in legacy alerting", - "stage": "GA", - "codeowner": "@grafana/alerting-squad", - "requiresRestart": true + "description": "Enables shared crosshair in table panel", + "stage": "experimental", + "codeowner": "@grafana/dataviz-squad", + "frontend": true } }, { "metadata": { - "name": "alertStateHistoryLokiOnly", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "regressionTransformation", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Disable Grafana alerts from emitting annotations when a remote Loki instance is available.", - "stage": "experimental", - "codeowner": "@grafana/alerting-squad" + "description": "Enables regression analysis transformation", + "stage": "preview", + "codeowner": "@grafana/dataviz-squad", + "frontend": true } }, { "metadata": { - "name": "permissionsFilterRemoveSubquery", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "queryOverLive", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Alternative permission filter implementation that does not use subqueries for fetching the dashboard folder", + "description": "Use Grafana Live WebSocket to execute backend queries", "stage": "experimental", - "codeowner": "@grafana/backend-platform" + "codeowner": "@grafana/grafana-app-platform-squad", + "frontend": true } }, { "metadata": { - "name": "externalCorePlugins", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "prometheusDataplane", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Allow core plugins to be loaded as external", - "stage": "experimental", - "codeowner": "@grafana/plugins-platform-backend" + "description": "Changes responses to from Prometheus to be compliant with the dataplane specification. In particular, when this feature toggle is active, the numeric `Field.Name` is set from 'Value' to the value of the `__name__` label.", + "stage": "GA", + "codeowner": "@grafana/observability-metrics", + "allowSelfServe": true } }, { "metadata": { - "name": "unifiedStorage", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "unifiedRequestLog", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "SQL-based k8s storage", + "description": "Writes error logs to the request logger", "stage": "experimental", - "codeowner": "@grafana/grafana-app-platform-squad", - "requiresDevMode": true, - "requiresRestart": true + "codeowner": "@grafana/backend-platform" } }, { "metadata": { - "name": "influxdbSqlSupport", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "wargamesTesting", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Enable InfluxDB SQL query language support with new querying UI", - "stage": "GA", - "codeowner": "@grafana/observability-metrics", - "requiresRestart": true, - "allowSelfServe": true + "description": "Placeholder feature flag for internal testing", + "stage": "experimental", + "codeowner": "@grafana/hosted-grafana-team" } }, { "metadata": { - "name": "angularDeprecationUI", - "resourceVersion": "1708080773145", - "creationTimestamp": "2024-02-14T16:41:35Z", - "annotations": { - "grafana.app/updatedTimestamp": "2024-02-16 10:52:53.145203323 +0000 UTC" - } + "name": "alertmanagerRemotePrimary", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Display Angular warnings in dashboards and panels", - "stage": "GA", - "codeowner": "@grafana/plugins-platform-backend", - "frontend": true + "description": "Enable Grafana to have a remote Alertmanager instance as the primary Alertmanager.", + "stage": "experimental", + "codeowner": "@grafana/alerting-squad" } }, { "metadata": { - "name": "libraryPanelRBAC", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "ssoSettingsApi", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Enables RBAC support for library panels", - "stage": "experimental", - "codeowner": "@grafana/dashboards-squad", - "requiresRestart": true + "description": "Enables the SSO settings API and the OAuth configuration UIs in Grafana", + "stage": "preview", + "codeowner": "@grafana/identity-access-team" } }, { "metadata": { - "name": "promQLScope", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "logsInfiniteScrolling", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "In-development feature that will allow injection of labels into prometheus queries.", + "description": "Enables infinite scrolling for the Logs panel in Explore and Dashboards", "stage": "experimental", - "codeowner": "@grafana/observability-metrics" + "codeowner": "@grafana/observability-logs", + "frontend": true } }, { "metadata": { - "name": "publicDashboards", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "displayAnonymousStats", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "[Deprecated] Public dashboards are now enabled by default; to disable them, use the configuration setting. This feature toggle will be removed in the next major version.", + "description": "Enables anonymous stats to be shown in the UI for Grafana", "stage": "GA", - "codeowner": "@grafana/sharing-squad", - "allowSelfServe": true + "codeowner": "@grafana/identity-access-team", + "frontend": true } }, { "metadata": { - "name": "unifiedRequestLog", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "storage", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Writes error logs to the request logger", + "description": "Configurable storage for dashboards, datasources, and resources", "stage": "experimental", - "codeowner": "@grafana/backend-platform" + "codeowner": "@grafana/grafana-app-platform-squad" } }, { "metadata": { - "name": "teamHttpHeaders", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "newPDFRendering", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Enables datasources to apply team headers to the client requests", + "description": "New implementation for the dashboard to PDF rendering", "stage": "experimental", - "codeowner": "@grafana/identity-access-team" + "codeowner": "@grafana/sharing-squad" } }, { "metadata": { - "name": "groupByVariable", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "pluginsFrontendSandbox", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Enable groupBy variable support in scenes dashboards", + "description": "Enables the plugins frontend sandbox", "stage": "experimental", - "codeowner": "@grafana/dashboards-squad", - "hideFromAdminPage": true, - "hideFromDocs": true + "codeowner": "@grafana/plugins-platform-backend", + "frontend": true } }, { "metadata": { - "name": "dashboardScene", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "logRequestsInstrumentedAsUnknown", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Enables dashboard rendering using scenes for all roles", + "description": "Logs the path for requests that are instrumented as unknown", "stage": "experimental", - "codeowner": "@grafana/dashboards-squad", - "frontend": true + "codeowner": "@grafana/hosted-grafana-team" } }, { "metadata": { - "name": "lokiQueryHints", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "lokiStructuredMetadata", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Enables query hints for Loki", + "description": "Enables the loki data source to request structured metadata from the Loki server", "stage": "GA", - "codeowner": "@grafana/observability-logs", - "frontend": true + "codeowner": "@grafana/observability-logs" } }, { "metadata": { - "name": "autoMigrateTablePanel", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "managedPluginsInstall", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Migrate old table panel to supported table panel - broken out from autoMigrateOldPanels to enable granular tracking", + "description": "Install managed plugins directly from plugins catalog", "stage": "preview", - "codeowner": "@grafana/dataviz-squad", - "frontend": true + "codeowner": "@grafana/plugins-platform-backend" } }, { "metadata": { - "name": "logsContextDatasourceUi", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "dashboardSceneForViewers", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Allow datasource to provide custom UI for context view", - "stage": "GA", - "codeowner": "@grafana/observability-logs", - "frontend": true, - "allowSelfServe": true + "description": "Enables dashboard rendering using Scenes for viewer roles", + "stage": "experimental", + "codeowner": "@grafana/dashboards-squad", + "frontend": true } }, { "metadata": { - "name": "disableSSEDataplane", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "alertingDetailsViewV2", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Disables dataplane specific processing in server side expressions.", + "description": "Enables the preview of the new alert details view", "stage": "experimental", - "codeowner": "@grafana/observability-metrics" + "codeowner": "@grafana/alerting-squad", + "frontend": true, + "hideFromDocs": true } }, { "metadata": { - "name": "refactorVariablesTimeRange", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "jitterAlertRulesWithinGroups", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Refactor time range variables flow to reduce number of API calls made when query variables are chained", + "description": "Distributes alert rule evaluations more evenly over time, including spreading out rules within the same group", "stage": "preview", - "codeowner": "@grafana/dashboards-squad", - "hideFromAdminPage": true + "codeowner": "@grafana/alerting-squad", + "requiresRestart": true, + "hideFromDocs": true } }, { "metadata": { - "name": "awsDatasourcesTempCredentials", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "unifiedStorage", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Support temporary security credentials in AWS plugins for Grafana Cloud customers", + "description": "SQL-based k8s storage", "stage": "experimental", - "codeowner": "@grafana/aws-datasources" + "codeowner": "@grafana/grafana-app-platform-squad", + "requiresDevMode": true, + "requiresRestart": true } }, { "metadata": { - "name": "prometheusMetricEncyclopedia", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "enableElasticsearchBackendQuerying", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Adds the metrics explorer component to the Prometheus query builder as an option in metric select", + "description": "Enable the processing of queries and responses in the Elasticsearch data source through backend", "stage": "GA", - "codeowner": "@grafana/observability-metrics", + "codeowner": "@grafana/observability-logs", + "allowSelfServe": true + } + }, + { + "metadata": { + "name": "cloudWatchLogsMonacoEditor", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" + }, + "spec": { + "description": "Enables the Monaco editor for CloudWatch Logs queries", + "stage": "GA", + "codeowner": "@grafana/aws-datasources", "frontend": true, "allowSelfServe": true } }, { "metadata": { - "name": "wargamesTesting", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "alertmanagerRemoteSecondary", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Placeholder feature flag for internal testing", + "description": "Enable Grafana to sync configuration and state with a remote Alertmanager.", "stage": "experimental", - "codeowner": "@grafana/hosted-grafana-team" + "codeowner": "@grafana/alerting-squad" } }, { "metadata": { - "name": "idForwarding", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "disableAngular", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Generate signed id token for identity that can be forwarded to plugins and external services", - "stage": "experimental", - "codeowner": "@grafana/identity-access-team" + "description": "Dynamic flag to disable angular at runtime. The preferred method is to set `angular_support_enabled` to `false` in the [security] settings, which allows you to change the state at runtime.", + "stage": "preview", + "codeowner": "@grafana/dataviz-squad", + "frontend": true, + "hideFromAdminPage": true } }, { "metadata": { - "name": "kubernetesSnapshots", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "grafanaAPIServerEnsureKubectlAccess", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Routes snapshot requests from /api to the /apis endpoint", + "description": "Start an additional https handler and write kubectl options", "stage": "experimental", "codeowner": "@grafana/grafana-app-platform-squad", + "requiresDevMode": true, "requiresRestart": true } }, { "metadata": { - "name": "alertmanagerRemoteSecondary", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "alertingNoDataErrorExecution", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Enable Grafana to sync configuration and state with a remote Alertmanager.", - "stage": "experimental", - "codeowner": "@grafana/alerting-squad" + "description": "Changes how Alerting state manager handles execution of NoData/Error", + "stage": "GA", + "codeowner": "@grafana/alerting-squad", + "requiresRestart": true } }, { "metadata": { - "name": "alertingBacktesting", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "angularDeprecationUI", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Rule backtesting API for alerting", + "description": "Display Angular warnings in dashboards and panels", + "stage": "GA", + "codeowner": "@grafana/plugins-platform-backend", + "frontend": true + } + }, + { + "metadata": { + "name": "lokiFormatQuery", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" + }, + "spec": { + "description": "Enables the ability to format Loki queries", "stage": "experimental", - "codeowner": "@grafana/alerting-squad" + "codeowner": "@grafana/observability-logs", + "frontend": true } }, { "metadata": { - "name": "influxdbRunQueriesInParallel", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "nestedFolderPicker", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Enables running InfluxDB Influxql queries in parallel", - "stage": "privatePreview", - "codeowner": "@grafana/observability-metrics" + "description": "Enables the new folder picker to work with nested folders. Requires the nestedFolders feature toggle", + "stage": "GA", + "codeowner": "@grafana/grafana-frontend-platform", + "frontend": true, + "allowSelfServe": true } }, { "metadata": { - "name": "pluginsAPIMetrics", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "individualCookiePreferences", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Sends metrics of public grafana packages usage by plugins", + "description": "Support overriding cookie preferences per user", "stage": "experimental", - "codeowner": "@grafana/plugins-platform-backend", - "frontend": true + "codeowner": "@grafana/backend-platform" } }, { "metadata": { - "name": "cloudWatchBatchQueries", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "externalServiceAccounts", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Runs CloudWatch metrics queries as separate batches", + "description": "Automatic service account and token setup for plugins", "stage": "preview", - "codeowner": "@grafana/aws-datasources" + "codeowner": "@grafana/identity-access-team", + "hideFromAdminPage": true } }, { "metadata": { - "name": "extractFieldsNameDeduplication", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "groupByVariable", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Make sure extracted field names are unique in the dataframe", + "description": "Enable groupBy variable support in scenes dashboards", "stage": "experimental", - "codeowner": "@grafana/dataviz-squad", - "frontend": true + "codeowner": "@grafana/dashboards-squad", + "hideFromAdminPage": true, + "hideFromDocs": true } }, { "metadata": { - "name": "exploreScrollableLogsContainer", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "autoMigrateGraphPanel", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Improves the scrolling behavior of logs in Explore", - "stage": "experimental", - "codeowner": "@grafana/observability-logs", + "description": "Migrate old graph panel to supported time series panel - broken out from autoMigrateOldPanels to enable granular tracking", + "stage": "preview", + "codeowner": "@grafana/dataviz-squad", "frontend": true } }, { "metadata": { - "name": "logRowsPopoverMenu", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "dashgpt", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Enable filtering menu displayed when text of a log line is selected", - "stage": "GA", - "codeowner": "@grafana/observability-logs", + "description": "Enable AI powered features in dashboards", + "stage": "preview", + "codeowner": "@grafana/dashboards-squad", "frontend": true } }, { "metadata": { - "name": "storage", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "kubernetesSnapshots", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Configurable storage for dashboards, datasources, and resources", + "description": "Routes snapshot requests from /api to the /apis endpoint", "stage": "experimental", - "codeowner": "@grafana/grafana-app-platform-squad" + "codeowner": "@grafana/grafana-app-platform-squad", + "requiresRestart": true } }, { "metadata": { - "name": "returnToPrevious", - "resourceVersion": "1708097934301", - "creationTimestamp": "2024-02-14T16:41:35Z", - "annotations": { - "grafana.app/updatedTimestamp": "2024-02-16 15:38:54.301079 +0000 UTC" - } + "name": "influxdbBackendMigration", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Enables the return to previous context functionality", - "stage": "preview", - "codeowner": "@grafana/grafana-frontend-platform", + "description": "Query InfluxDB InfluxQL without the proxy", + "stage": "GA", + "codeowner": "@grafana/observability-metrics", "frontend": true } }, { "metadata": { - "name": "influxqlStreamingParser", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "redshiftAsyncQueryDataSupport", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Enable streaming JSON parser for InfluxDB datasource InfluxQL query language", - "stage": "experimental", - "codeowner": "@grafana/observability-metrics" + "description": "Enable async query data support for Redshift", + "stage": "GA", + "codeowner": "@grafana/aws-datasources" } }, { "metadata": { - "name": "enableDatagridEditing", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "topnav", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Enables the edit functionality in the datagrid panel", - "stage": "preview", - "codeowner": "@grafana/dataviz-squad", - "frontend": true + "description": "Enables topnav support in external plugins. The new Grafana navigation cannot be disabled.", + "stage": "deprecated", + "codeowner": "@grafana/grafana-frontend-platform" } }, { "metadata": { - "name": "lokiPredefinedOperations", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "lokiQuerySplittingConfig", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Adds predefined query operations to Loki query editor", + "description": "Give users the option to configure split durations for Loki queries", "stage": "experimental", "codeowner": "@grafana/observability-logs", "frontend": true @@ -925,271 +892,281 @@ }, { "metadata": { - "name": "nestedFolders", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "awsDatasourcesTempCredentials", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Enable folder nesting", - "stage": "preview", - "codeowner": "@grafana/backend-platform" + "description": "Support temporary security credentials in AWS plugins for Grafana Cloud customers", + "stage": "experimental", + "codeowner": "@grafana/aws-datasources" } }, { "metadata": { - "name": "panelMonitoring", - "resourceVersion": "1707938948387", - "creationTimestamp": "2024-02-14T16:41:35Z", - "annotations": { - "grafana.app/updatedTimestamp": "2024-02-14 19:29:08.387414 +0000 UTC" - } + "name": "influxdbSqlSupport", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Enables panel monitoring through logs and measurements", + "description": "Enable InfluxDB SQL query language support with new querying UI", "stage": "GA", + "codeowner": "@grafana/observability-metrics", + "requiresRestart": true, + "allowSelfServe": true + } + }, + { + "metadata": { + "name": "autoMigrateTablePanel", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" + }, + "spec": { + "description": "Migrate old table panel to supported table panel - broken out from autoMigrateOldPanels to enable granular tracking", + "stage": "preview", "codeowner": "@grafana/dataviz-squad", "frontend": true } }, { "metadata": { - "name": "mysqlAnsiQuotes", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "migrationLocking", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Use double quotes to escape keyword in a MySQL query", - "stage": "experimental", + "description": "Lock database during migrations", + "stage": "preview", "codeowner": "@grafana/backend-platform" } }, { "metadata": { - "name": "pluginsSkipHostEnvVars", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "accessControlOnCall", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Disables passing host environment variable to plugin processes", - "stage": "experimental", - "codeowner": "@grafana/plugins-platform-backend" + "description": "Access control primitives for OnCall", + "stage": "preview", + "codeowner": "@grafana/identity-access-team", + "hideFromAdminPage": true } }, { "metadata": { - "name": "newPDFRendering", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "faroDatasourceSelector", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "New implementation for the dashboard to PDF rendering", - "stage": "experimental", - "codeowner": "@grafana/sharing-squad" + "description": "Enable the data source selector within the Frontend Apps section of the Frontend Observability", + "stage": "preview", + "codeowner": "@grafana/app-o11y", + "frontend": true } }, { "metadata": { - "name": "dashboardEmbed", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "featureToggleAdminPage", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Allow embedding dashboard for external use in Code editors", + "description": "Enable admin page for managing feature toggles from the Grafana front-end", "stage": "experimental", - "codeowner": "@grafana/grafana-as-code", - "frontend": true + "codeowner": "@grafana/grafana-operator-experience-squad", + "requiresRestart": true } }, { "metadata": { - "name": "cloudWatchWildCardDimensionValues", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "awsAsyncQueryCaching", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Fetches dimension values from CloudWatch to correctly label wildcard dimensions", + "description": "Enable caching for async queries for Redshift and Athena. Requires that the datasource has caching and async query support enabled", "stage": "GA", - "codeowner": "@grafana/aws-datasources", - "allowSelfServe": true + "codeowner": "@grafana/aws-datasources" } }, { "metadata": { - "name": "addFieldFromCalculationStatFunctions", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "reportingRetries", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Add cumulative and window functions to the add field from calculation transformation", + "description": "Enables rendering retries for the reporting feature", "stage": "preview", - "codeowner": "@grafana/dataviz-squad", - "frontend": true + "codeowner": "@grafana/sharing-squad", + "requiresRestart": true } }, { "metadata": { - "name": "panelTitleSearchInV1", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "kubernetesAggregator", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Enable searching for dashboards using panel title in search v1", + "description": "Enable grafana aggregator", "stage": "experimental", - "codeowner": "@grafana/backend-platform", - "requiresDevMode": true + "codeowner": "@grafana/grafana-app-platform-squad", + "requiresRestart": true } }, { "metadata": { - "name": "pdfTables", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "publicDashboards", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Enables generating table data as PDF in reporting", - "stage": "preview", - "codeowner": "@grafana/sharing-squad" + "description": "[Deprecated] Public dashboards are now enabled by default; to disable them, use the configuration setting. This feature toggle will be removed in the next major version.", + "stage": "GA", + "codeowner": "@grafana/sharing-squad", + "allowSelfServe": true } }, { "metadata": { - "name": "jitterAlertRulesWithinGroups", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "transformationsRedesign", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Distributes alert rule evaluations more evenly over time, including spreading out rules within the same group", - "stage": "preview", - "codeowner": "@grafana/alerting-squad", - "requiresRestart": true, - "hideFromDocs": true + "description": "Enables the transformations redesign", + "stage": "GA", + "codeowner": "@grafana/observability-metrics", + "frontend": true, + "allowSelfServe": true } }, { "metadata": { - "name": "publicDashboardsEmailSharing", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "traceQLStreaming", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Enables public dashboard sharing to be restricted to only allowed emails", - "stage": "preview", - "codeowner": "@grafana/sharing-squad", - "hideFromAdminPage": true, - "hideFromDocs": true + "description": "Enables response streaming of TraceQL queries of the Tempo data source", + "stage": "experimental", + "codeowner": "@grafana/observability-traces-and-profiling", + "frontend": true } }, { "metadata": { - "name": "migrationLocking", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "formatString", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Lock database during migrations", + "description": "Enable format string transformer", "stage": "preview", - "codeowner": "@grafana/backend-platform" + "codeowner": "@grafana/dataviz-squad", + "frontend": true } }, { "metadata": { - "name": "autoMigrateStatPanel", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "alertingQueryOptimization", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Migrate old stat panel to supported stat panel - broken out from autoMigrateOldPanels to enable granular tracking", - "stage": "preview", - "codeowner": "@grafana/dataviz-squad", - "frontend": true + "description": "Optimizes eligible queries in order to reduce load on datasources", + "stage": "GA", + "codeowner": "@grafana/alerting-squad" } }, { "metadata": { - "name": "canvasPanelNesting", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "nestedFolders", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Allow elements nesting", - "stage": "experimental", - "codeowner": "@grafana/dataviz-squad", - "frontend": true, - "hideFromAdminPage": true + "description": "Enable folder nesting", + "stage": "preview", + "codeowner": "@grafana/backend-platform" } }, { "metadata": { - "name": "externalServiceAccounts", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "scenes", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Automatic service account and token setup for plugins", - "stage": "preview", - "codeowner": "@grafana/identity-access-team", - "hideFromAdminPage": true + "description": "Experimental framework to build interactive dashboards", + "stage": "experimental", + "codeowner": "@grafana/dashboards-squad", + "frontend": true } }, { "metadata": { - "name": "alertingSaveStatePeriodic", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "dataConnectionsConsole", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Writes the state periodically to the database, asynchronous to rule evaluation", - "stage": "privatePreview", - "codeowner": "@grafana/alerting-squad" + "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.", + "stage": "GA", + "codeowner": "@grafana/plugins-platform-backend", + "allowSelfServe": true } }, { "metadata": { - "name": "traceToMetrics", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "extraThemes", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Enable trace to metrics links", + "description": "Enables extra themes", "stage": "experimental", - "codeowner": "@grafana/observability-traces-and-profiling", + "codeowner": "@grafana/grafana-frontend-platform", "frontend": true } }, { "metadata": { - "name": "cloudWatchCrossAccountQuerying", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "annotationPermissionUpdate", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Enables cross-account querying in CloudWatch datasources", - "stage": "GA", - "codeowner": "@grafana/aws-datasources", - "allowSelfServe": true + "description": "Separate annotation permissions from dashboard permissions to allow for more granular control.", + "stage": "experimental", + "codeowner": "@grafana/identity-access-team" } }, { "metadata": { - "name": "pluginsFrontendSandbox", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "extractFieldsNameDeduplication", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Enables the plugins frontend sandbox", + "description": "Make sure extracted field names are unique in the dataframe", "stage": "experimental", - "codeowner": "@grafana/plugins-platform-backend", + "codeowner": "@grafana/dataviz-squad", "frontend": true } }, { "metadata": { - "name": "traceQLStreaming", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "flameGraphItemCollapsing", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Enables response streaming of TraceQL queries of the Tempo data source", + "description": "Allow collapsing of flame graph items", "stage": "experimental", "codeowner": "@grafana/observability-traces-and-profiling", "frontend": true @@ -1197,12 +1174,12 @@ }, { "metadata": { - "name": "alertingNoDataErrorExecution", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "alertingPreviewUpgrade", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Changes how Alerting state manager handles execution of NoData/Error", + "description": "Show Unified Alerting preview and upgrade page in legacy alerting", "stage": "GA", "codeowner": "@grafana/alerting-squad", "requiresRestart": true @@ -1210,935 +1187,940 @@ }, { "metadata": { - "name": "logRequestsInstrumentedAsUnknown", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "disableEnvelopeEncryption", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Logs the path for requests that are instrumented as unknown", - "stage": "experimental", - "codeowner": "@grafana/hosted-grafana-team" + "description": "Disable envelope encryption (emergency only)", + "stage": "GA", + "codeowner": "@grafana/grafana-as-code", + "hideFromAdminPage": true } }, { "metadata": { - "name": "transformationsRedesign", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "groupToNestedTableTransformation", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Enables the transformations redesign", - "stage": "GA", - "codeowner": "@grafana/observability-metrics", - "frontend": true, - "allowSelfServe": true + "description": "Enables the group to nested table transformation", + "stage": "preview", + "codeowner": "@grafana/dataviz-squad", + "frontend": true } }, { "metadata": { - "name": "prometheusPromQAIL", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "promQLScope", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Prometheus and AI/ML to assist users in creating a query", + "description": "In-development feature that will allow injection of labels into prometheus queries.", "stage": "experimental", - "codeowner": "@grafana/observability-metrics", - "frontend": true + "codeowner": "@grafana/observability-metrics" } }, { "metadata": { - "name": "kubernetesAggregator", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "logsContextDatasourceUi", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Enable grafana aggregator", - "stage": "experimental", - "codeowner": "@grafana/grafana-app-platform-squad", - "requiresRestart": true + "description": "Allow datasource to provide custom UI for context view", + "stage": "GA", + "codeowner": "@grafana/observability-logs", + "frontend": true, + "allowSelfServe": true } }, { "metadata": { - "name": "live-service-web-worker", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "alertStateHistoryLokiOnly", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "This will use a webworker thread to processes events rather than the main thread", + "description": "Disable Grafana alerts from emitting annotations when a remote Loki instance is available.", "stage": "experimental", - "codeowner": "@grafana/grafana-app-platform-squad", - "frontend": true + "codeowner": "@grafana/alerting-squad" } }, { "metadata": { - "name": "grpcServer", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "transformationsVariableSupport", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Run the GRPC server", + "description": "Allows using variables in transformations", "stage": "preview", - "codeowner": "@grafana/grafana-app-platform-squad", - "hideFromAdminPage": true + "codeowner": "@grafana/dataviz-squad", + "frontend": true } }, { "metadata": { - "name": "clientTokenRotation", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z", - "deletionTimestamp": "2024-02-16T15:38:59Z" + "name": "dashboardSceneSolo", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Replaces the current in-request token rotation so that the client initiates the rotation", - "stage": "GA", - "codeowner": "@grafana/identity-access-team" + "description": "Enables rendering dashboards using scenes for solo panels", + "stage": "experimental", + "codeowner": "@grafana/dashboards-squad", + "frontend": true } }, { "metadata": { - "name": "lokiStructuredMetadata", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "athenaAsyncQueryDataSupport", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Enables the loki data source to request structured metadata from the Loki server", + "description": "Enable async query data support for Athena", "stage": "GA", - "codeowner": "@grafana/observability-logs" + "codeowner": "@grafana/aws-datasources", + "frontend": true } }, { "metadata": { - "name": "tableSharedCrosshair", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "lokiLogsDataplane", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Enables shared crosshair in table panel", + "description": "Changes logs responses from Loki to be compliant with the dataplane specification.", "stage": "experimental", + "codeowner": "@grafana/observability-logs" + } + }, + { + "metadata": { + "name": "enableDatagridEditing", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" + }, + "spec": { + "description": "Enables the edit functionality in the datagrid panel", + "stage": "preview", "codeowner": "@grafana/dataviz-squad", "frontend": true } }, { "metadata": { - "name": "lokiQuerySplitting", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "exploreScrollableLogsContainer", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Split large interval queries into subqueries with smaller time intervals", - "stage": "GA", + "description": "Improves the scrolling behavior of logs in Explore", + "stage": "experimental", "codeowner": "@grafana/observability-logs", - "frontend": true, - "allowSelfServe": true + "frontend": true } }, { "metadata": { - "name": "lokiMetricDataplane", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "lokiRunQueriesInParallel", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Changes metric responses from Loki to be compliant with the dataplane specification.", - "stage": "GA", - "codeowner": "@grafana/observability-logs", - "allowSelfServe": true + "description": "Enables running Loki queries in parallel", + "stage": "privatePreview", + "codeowner": "@grafana/observability-logs" } }, { "metadata": { - "name": "transformationsVariableSupport", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "panelTitleSearchInV1", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Allows using variables in transformations", - "stage": "preview", - "codeowner": "@grafana/dataviz-squad", - "frontend": true + "description": "Enable searching for dashboards using panel title in search v1", + "stage": "experimental", + "codeowner": "@grafana/backend-platform", + "requiresDevMode": true } }, { "metadata": { - "name": "newVizTooltips", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "logRowsPopoverMenu", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "New visualizations tooltips UX", - "stage": "preview", - "codeowner": "@grafana/dataviz-squad", + "description": "Enable filtering menu displayed when text of a log line is selected", + "stage": "GA", + "codeowner": "@grafana/observability-logs", "frontend": true } }, { "metadata": { - "name": "topnav", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "lokiQueryHints", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Enables topnav support in external plugins. The new Grafana navigation cannot be disabled.", - "stage": "deprecated", - "codeowner": "@grafana/grafana-frontend-platform" + "description": "Enables query hints for Loki", + "stage": "GA", + "codeowner": "@grafana/observability-logs", + "frontend": true } }, { "metadata": { - "name": "ssoSettingsApi", - "resourceVersion": "1707991575438", - "creationTimestamp": "2024-02-14T16:41:35Z", - "annotations": { - "grafana.app/updatedTimestamp": "2024-02-15 10:06:15.438523 +0000 UTC" - } + "name": "lokiExperimentalStreaming", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Enables the SSO settings API and the OAuth configuration UIs in Grafana", - "stage": "preview", - "codeowner": "@grafana/identity-access-team" + "description": "Support new streaming approach for loki (prototype, needs special loki build)", + "stage": "experimental", + "codeowner": "@grafana/observability-logs" } }, { "metadata": { - "name": "onPremToCloudMigrations", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "traceToMetrics", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "In-development feature that will allow users to easily migrate their on-prem Grafana instances to Grafana Cloud.", + "description": "Enable trace to metrics links", "stage": "experimental", - "codeowner": "@grafana/grafana-operator-experience-squad" + "codeowner": "@grafana/observability-traces-and-profiling", + "frontend": true } }, { "metadata": { - "name": "disableSecretsCompatibility", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "grpcServer", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Disable duplicated secret storage in legacy tables", - "stage": "experimental", - "codeowner": "@grafana/hosted-grafana-team", - "requiresRestart": true + "description": "Run the GRPC server", + "stage": "preview", + "codeowner": "@grafana/grafana-app-platform-squad", + "hideFromAdminPage": true } }, { "metadata": { - "name": "athenaAsyncQueryDataSupport", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "cloudWatchCrossAccountQuerying", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Enable async query data support for Athena", + "description": "Enables cross-account querying in CloudWatch datasources", "stage": "GA", "codeowner": "@grafana/aws-datasources", - "frontend": true + "allowSelfServe": true } }, { "metadata": { - "name": "lokiLogsDataplane", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "vizAndWidgetSplit", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Changes logs responses from Loki to be compliant with the dataplane specification.", + "description": "Split panels between visualizations and widgets", "stage": "experimental", - "codeowner": "@grafana/observability-logs" + "codeowner": "@grafana/dashboards-squad", + "frontend": true } }, { "metadata": { - "name": "enableNativeHTTPHistogram", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "panelMonitoring", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Enables native HTTP Histograms", - "stage": "experimental", - "codeowner": "@grafana/hosted-grafana-team" + "description": "Enables panel monitoring through logs and measurements", + "stage": "GA", + "codeowner": "@grafana/dataviz-squad", + "frontend": true } }, { "metadata": { - "name": "panelFilterVariable", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "live-service-web-worker", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Enables use of the `systemPanelFilterVar` variable to filter panels in a dashboard", + "description": "This will use a webworker thread to processes events rather than the main thread", "stage": "experimental", - "codeowner": "@grafana/dashboards-squad", - "frontend": true, - "hideFromDocs": true - } - }, - { - "metadata": { - "name": "featureHighlights", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" - }, - "spec": { - "description": "Highlight Grafana Enterprise features", - "stage": "GA", - "codeowner": "@grafana/grafana-as-code", - "allowSelfServe": true + "codeowner": "@grafana/grafana-app-platform-squad", + "frontend": true } }, { "metadata": { - "name": "alertingNoNormalState", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "returnToPrevious", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Stop maintaining state of alerts that are not firing", + "description": "Enables the return to previous context functionality", "stage": "preview", - "codeowner": "@grafana/alerting-squad", - "hideFromAdminPage": true + "codeowner": "@grafana/grafana-frontend-platform", + "frontend": true } }, { "metadata": { - "name": "enableElasticsearchBackendQuerying", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "cachingOptimizeSerializationMemoryUsage", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Enable the processing of queries and responses in the Elasticsearch data source through backend", - "stage": "GA", - "codeowner": "@grafana/observability-logs", - "allowSelfServe": true + "description": "If enabled, the caching backend gradually serializes query responses for the cache, comparing against the configured `[caching]max_value_mb` value as it goes. This can can help prevent Grafana from running out of memory while attempting to cache very large query responses.", + "stage": "experimental", + "codeowner": "@grafana/grafana-operator-experience-squad" } }, { "metadata": { - "name": "displayAnonymousStats", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "onPremToCloudMigrations", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Enables anonymous stats to be shown in the UI for Grafana", - "stage": "GA", - "codeowner": "@grafana/identity-access-team", - "frontend": true + "description": "In-development feature that will allow users to easily migrate their on-prem Grafana instances to Grafana Cloud.", + "stage": "experimental", + "codeowner": "@grafana/grafana-operator-experience-squad" } }, { "metadata": { - "name": "nestedFolderPicker", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "featureHighlights", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Enables the new folder picker to work with nested folders. Requires the nestedFolders feature toggle", + "description": "Highlight Grafana Enterprise features", "stage": "GA", - "codeowner": "@grafana/grafana-frontend-platform", - "frontend": true, + "codeowner": "@grafana/grafana-as-code", "allowSelfServe": true } }, { "metadata": { - "name": "dataplaneFrontendFallback", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "splitScopes", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Support dataplane contract field name change for transformations and field name matchers where the name is different", - "stage": "GA", - "codeowner": "@grafana/observability-metrics", - "frontend": true, - "allowSelfServe": true + "description": "Support faster dashboard and folder search by splitting permission scopes into parts", + "stage": "deprecated", + "codeowner": "@grafana/identity-access-team", + "requiresRestart": true, + "hideFromAdminPage": true } }, { "metadata": { - "name": "vizAndWidgetSplit", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "nodeGraphDotLayout", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Split panels between visualizations and widgets", + "description": "Changed the layout algorithm for the node graph", "stage": "experimental", - "codeowner": "@grafana/dashboards-squad", + "codeowner": "@grafana/observability-traces-and-profiling", "frontend": true } }, { "metadata": { - "name": "featureToggleAdminPage", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "refactorVariablesTimeRange", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Enable admin page for managing feature toggles from the Grafana front-end", - "stage": "experimental", - "codeowner": "@grafana/grafana-operator-experience-squad", - "requiresRestart": true + "description": "Refactor time range variables flow to reduce number of API calls made when query variables are chained", + "stage": "preview", + "codeowner": "@grafana/dashboards-squad", + "hideFromAdminPage": true } }, { "metadata": { - "name": "sseGroupByDatasource", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "externalServiceAuth", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Send query to the same datasource in a single request when using server side expressions. The `cloudWatchBatchQueries` feature toggle should be enabled if this used with CloudWatch.", + "description": "Starts an OAuth2 authentication provider for external services", "stage": "experimental", - "codeowner": "@grafana/observability-metrics" + "codeowner": "@grafana/identity-access-team", + "requiresDevMode": true } }, { "metadata": { - "name": "lokiRunQueriesInParallel", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "frontendSandboxMonitorOnly", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Enables running Loki queries in parallel", - "stage": "privatePreview", - "codeowner": "@grafana/observability-logs" + "description": "Enables monitor only in the plugin frontend sandbox (if enabled)", + "stage": "experimental", + "codeowner": "@grafana/plugins-platform-backend", + "frontend": true } }, { "metadata": { - "name": "awsDatasourcesNewFormStyling", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "mlExpressions", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Applies new form styling for configuration and query editors in AWS plugins", - "stage": "preview", - "codeowner": "@grafana/aws-datasources", - "frontend": true + "description": "Enable support for Machine Learning in server-side expressions", + "stage": "experimental", + "codeowner": "@grafana/alerting-squad" } }, { "metadata": { - "name": "lokiExperimentalStreaming", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "libraryPanelRBAC", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Support new streaming approach for loki (prototype, needs special loki build)", + "description": "Enables RBAC support for library panels", "stage": "experimental", - "codeowner": "@grafana/observability-logs" + "codeowner": "@grafana/dashboards-squad", + "requiresRestart": true } }, { "metadata": { - "name": "disableAngular", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "kubernetesFeatureToggles", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Dynamic flag to disable angular at runtime. The preferred method is to set `angular_support_enabled` to `false` in the [security] settings, which allows you to change the state at runtime.", - "stage": "preview", - "codeowner": "@grafana/dataviz-squad", + "description": "Use the kubernetes API for feature toggle management in the frontend", + "stage": "experimental", + "codeowner": "@grafana/grafana-operator-experience-squad", "frontend": true, "hideFromAdminPage": true } }, { "metadata": { - "name": "scenes", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "expressionParser", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Experimental framework to build interactive dashboards", + "description": "Enable new expression parser", "stage": "experimental", - "codeowner": "@grafana/dashboards-squad", - "frontend": true + "codeowner": "@grafana/grafana-app-platform-squad", + "requiresRestart": true } }, { "metadata": { - "name": "editPanelCSVDragAndDrop", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "autoMigrateStatPanel", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Enables drag and drop for CSV and Excel files", - "stage": "experimental", + "description": "Migrate old stat panel to supported stat panel - broken out from autoMigrateOldPanels to enable granular tracking", + "stage": "preview", "codeowner": "@grafana/dataviz-squad", "frontend": true } }, { "metadata": { - "name": "extraThemes", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "renderAuthJWT", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Enables extra themes", - "stage": "experimental", - "codeowner": "@grafana/grafana-frontend-platform", - "frontend": true + "description": "Uses JWT-based auth for rendering instead of relying on remote cache", + "stage": "preview", + "codeowner": "@grafana/grafana-as-code", + "hideFromAdminPage": true } }, { "metadata": { - "name": "annotationPermissionUpdate", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "permissionsFilterRemoveSubquery", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Separate annotation permissions from dashboard permissions to allow for more granular control.", + "description": "Alternative permission filter implementation that does not use subqueries for fetching the dashboard folder", "stage": "experimental", - "codeowner": "@grafana/identity-access-team" + "codeowner": "@grafana/backend-platform" } }, { "metadata": { - "name": "exploreContentOutline", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "pdfTables", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Content outline sidebar", - "stage": "GA", - "codeowner": "@grafana/explore-squad", - "frontend": true, - "allowSelfServe": true + "description": "Enables generating table data as PDF in reporting", + "stage": "preview", + "codeowner": "@grafana/sharing-squad" } }, { "metadata": { - "name": "lokiFormatQuery", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "influxqlStreamingParser", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Enables the ability to format Loki queries", + "description": "Enable streaming JSON parser for InfluxDB datasource InfluxQL query language", "stage": "experimental", - "codeowner": "@grafana/observability-logs", - "frontend": true + "codeowner": "@grafana/observability-metrics" } }, { "metadata": { - "name": "prometheusIncrementalQueryInstrumentation", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "showDashboardValidationWarnings", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Adds RudderStack events to incremental queries", + "description": "Show warnings when dashboards do not validate against the schema", "stage": "experimental", - "codeowner": "@grafana/observability-metrics", - "frontend": true + "codeowner": "@grafana/dashboards-squad" } }, { "metadata": { - "name": "alertmanagerRemotePrimary", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "lokiQuerySplitting", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Enable Grafana to have a remote Alertmanager instance as the primary Alertmanager.", - "stage": "experimental", - "codeowner": "@grafana/alerting-squad" + "description": "Split large interval queries into subqueries with smaller time intervals", + "stage": "GA", + "codeowner": "@grafana/observability-logs", + "frontend": true, + "allowSelfServe": true } }, { "metadata": { - "name": "cloudRBACRoles", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" - }, - "spec": { - "description": "Enabled grafana cloud specific RBAC roles", - "stage": "experimental", - "codeowner": "@grafana/identity-access-team", - "requiresRestart": true, - "hideFromDocs": true + "name": "lokiMetricDataplane", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" + }, + "spec": { + "description": "Changes metric responses from Loki to be compliant with the dataplane specification.", + "stage": "GA", + "codeowner": "@grafana/observability-logs", + "allowSelfServe": true } }, { "metadata": { - "name": "nodeGraphDotLayout", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "dashboardEmbed", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Changed the layout algorithm for the node graph", + "description": "Allow embedding dashboard for external use in Code editors", "stage": "experimental", - "codeowner": "@grafana/observability-traces-and-profiling", + "codeowner": "@grafana/grafana-as-code", "frontend": true } }, { "metadata": { - "name": "autoMigratePiechartPanel", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "pluginsAPIMetrics", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Migrate old piechart panel to supported piechart panel - broken out from autoMigrateOldPanels to enable granular tracking", - "stage": "preview", - "codeowner": "@grafana/dataviz-squad", + "description": "Sends metrics of public grafana packages usage by plugins", + "stage": "experimental", + "codeowner": "@grafana/plugins-platform-backend", "frontend": true } }, { "metadata": { - "name": "influxdbBackendMigration", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "awsDatasourcesNewFormStyling", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Query InfluxDB InfluxQL without the proxy", - "stage": "GA", - "codeowner": "@grafana/observability-metrics", + "description": "Applies new form styling for configuration and query editors in AWS plugins", + "stage": "preview", + "codeowner": "@grafana/aws-datasources", "frontend": true } }, { "metadata": { - "name": "alertStateHistoryLokiPrimary", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "dashboardScene", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Enable a remote Loki instance as the primary source for state history reads.", + "description": "Enables dashboard rendering using scenes for all roles", "stage": "experimental", - "codeowner": "@grafana/alerting-squad" + "codeowner": "@grafana/dashboards-squad", + "frontend": true } }, { "metadata": { - "name": "configurableSchedulerTick", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "canvasPanelNesting", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Enable changing the scheduler base interval via configuration option unified_alerting.scheduler_tick_interval", + "description": "Allow elements nesting", "stage": "experimental", - "codeowner": "@grafana/alerting-squad", - "requiresRestart": true, - "hideFromDocs": true + "codeowner": "@grafana/dataviz-squad", + "frontend": true, + "hideFromAdminPage": true } }, { "metadata": { - "name": "dashgpt", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "alertingSaveStatePeriodic", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Enable AI powered features in dashboards", - "stage": "preview", - "codeowner": "@grafana/dashboards-squad", - "frontend": true + "description": "Writes the state periodically to the database, asynchronous to rule evaluation", + "stage": "privatePreview", + "codeowner": "@grafana/alerting-squad" } }, { "metadata": { - "name": "alertmanagerRemoteOnly", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "sseGroupByDatasource", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Disable the internal Alertmanager and only use the external one defined.", + "description": "Send query to the same datasource in a single request when using server side expressions. The `cloudWatchBatchQueries` feature toggle should be enabled if this used with CloudWatch.", "stage": "experimental", - "codeowner": "@grafana/alerting-squad" + "codeowner": "@grafana/observability-metrics" } }, { "metadata": { - "name": "alertStateHistoryLokiSecondary", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "kubernetesQueryServiceRewrite", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Enable Grafana to write alert state history to an external Loki instance in addition to Grafana annotations.", + "description": "Rewrite requests targeting /ds/query to the query service", "stage": "experimental", - "codeowner": "@grafana/alerting-squad" + "codeowner": "@grafana/grafana-app-platform-squad", + "requiresDevMode": true, + "requiresRestart": true } }, { "metadata": { - "name": "frontendSandboxMonitorOnly", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "exploreContentOutline", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Enables monitor only in the plugin frontend sandbox (if enabled)", - "stage": "experimental", - "codeowner": "@grafana/plugins-platform-backend", - "frontend": true + "description": "Content outline sidebar", + "stage": "GA", + "codeowner": "@grafana/explore-squad", + "frontend": true, + "allowSelfServe": true } }, { "metadata": { - "name": "pluginsDynamicAngularDetectionPatterns", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "recordedQueriesMulti", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Enables fetching Angular detection patterns for plugins from GCOM and fallback to hardcoded ones", - "stage": "experimental", - "codeowner": "@grafana/plugins-platform-backend" + "description": "Enables writing multiple items from a single query within Recorded Queries", + "stage": "GA", + "codeowner": "@grafana/observability-metrics" } }, { "metadata": { - "name": "mlExpressions", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "alertingInsights", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Enable support for Machine Learning in server-side expressions", - "stage": "experimental", - "codeowner": "@grafana/alerting-squad" + "description": "Show the new alerting insights landing page", + "stage": "GA", + "codeowner": "@grafana/alerting-squad", + "frontend": true, + "hideFromAdminPage": true } }, { "metadata": { - "name": "pluginsInstrumentationStatusSource", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "idForwarding", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Include a status source label for plugin request metrics and logs", + "description": "Generate signed id token for identity that can be forwarded to plugins and external services", "stage": "experimental", - "codeowner": "@grafana/plugins-platform-backend" + "codeowner": "@grafana/identity-access-team" } }, { "metadata": { - "name": "autoMigrateWorldmapPanel", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "enableNativeHTTPHistogram", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Migrate old worldmap panel to supported geomap panel - broken out from autoMigrateOldPanels to enable granular tracking", - "stage": "preview", - "codeowner": "@grafana/dataviz-squad", - "frontend": true + "description": "Enables native HTTP Histograms", + "stage": "experimental", + "codeowner": "@grafana/hosted-grafana-team" } }, { "metadata": { - "name": "recoveryThreshold", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "alertmanagerRemoteOnly", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Enables feature recovery threshold (aka hysteresis) for threshold server-side expression", - "stage": "GA", - "codeowner": "@grafana/alerting-squad", - "requiresRestart": true + "description": "Disable the internal Alertmanager and only use the external one defined.", + "stage": "experimental", + "codeowner": "@grafana/alerting-squad" } }, { "metadata": { - "name": "cachingOptimizeSerializationMemoryUsage", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "influxdbRunQueriesInParallel", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "If enabled, the caching backend gradually serializes query responses for the cache, comparing against the configured `[caching]max_value_mb` value as it goes. This can can help prevent Grafana from running out of memory while attempting to cache very large query responses.", - "stage": "experimental", - "codeowner": "@grafana/grafana-operator-experience-squad" + "description": "Enables running InfluxDB Influxql queries in parallel", + "stage": "privatePreview", + "codeowner": "@grafana/observability-metrics" } }, { "metadata": { - "name": "dashboardSceneForViewers", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "newVizTooltips", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Enables dashboard rendering using Scenes for viewer roles", - "stage": "experimental", - "codeowner": "@grafana/dashboards-squad", + "description": "New visualizations tooltips UX", + "stage": "preview", + "codeowner": "@grafana/dataviz-squad", "frontend": true } }, { "metadata": { - "name": "flameGraphItemCollapsing", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "prometheusMetricEncyclopedia", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Allow collapsing of flame graph items", - "stage": "experimental", - "codeowner": "@grafana/observability-traces-and-profiling", - "frontend": true + "description": "Adds the metrics explorer component to the Prometheus query builder as an option in metric select", + "stage": "GA", + "codeowner": "@grafana/observability-metrics", + "frontend": true, + "allowSelfServe": true } }, { "metadata": { - "name": "datasourceQueryMultiStatus", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "pluginsDynamicAngularDetectionPatterns", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Introduce HTTP 207 Multi Status for api/ds/query", + "description": "Enables fetching Angular detection patterns for plugins from GCOM and fallback to hardcoded ones", "stage": "experimental", "codeowner": "@grafana/plugins-platform-backend" } }, { "metadata": { - "name": "redshiftAsyncQueryDataSupport", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "cloudWatchBatchQueries", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Enable async query data support for Redshift", - "stage": "GA", + "description": "Runs CloudWatch metrics queries as separate batches", + "stage": "preview", "codeowner": "@grafana/aws-datasources" } }, { "metadata": { - "name": "individualCookiePreferences", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "alertingUpgradeDryrunOnStart", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Support overriding cookie preferences per user", - "stage": "experimental", - "codeowner": "@grafana/backend-platform" + "description": "When activated in legacy alerting mode, this initiates a dry-run of the Unified Alerting upgrade during each startup. It logs any issues detected without implementing any actual changes.", + "stage": "GA", + "codeowner": "@grafana/alerting-squad", + "requiresRestart": true } }, { "metadata": { - "name": "faroDatasourceSelector", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "autoMigrateOldPanels", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Enable the data source selector within the Frontend Apps section of the Frontend Observability", + "description": "Migrate old angular panels to supported versions (graph, table-old, worldmap, etc)", "stage": "preview", - "codeowner": "@grafana/app-o11y", + "codeowner": "@grafana/dataviz-squad", "frontend": true } }, { "metadata": { - "name": "kubernetesPlaylists", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "externalCorePlugins", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Use the kubernetes API in the frontend for playlists, and route /api/playlist requests to k8s", + "description": "Allow core plugins to be loaded as external", "stage": "experimental", - "codeowner": "@grafana/grafana-app-platform-squad", - "requiresRestart": true + "codeowner": "@grafana/plugins-platform-backend" } }, { "metadata": { - "name": "alertingDetailsViewV2", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "alertingNoNormalState", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Enables the preview of the new alert details view", - "stage": "experimental", + "description": "Stop maintaining state of alerts that are not firing", + "stage": "preview", "codeowner": "@grafana/alerting-squad", - "frontend": true, - "hideFromDocs": true + "hideFromAdminPage": true } }, { "metadata": { - "name": "accessControlOnCall", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "dataplaneFrontendFallback", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Access control primitives for OnCall", - "stage": "preview", - "codeowner": "@grafana/identity-access-team", - "hideFromAdminPage": true + "description": "Support dataplane contract field name change for transformations and field name matchers where the name is different", + "stage": "GA", + "codeowner": "@grafana/observability-metrics", + "frontend": true, + "allowSelfServe": true } }, { "metadata": { - "name": "splitScopes", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "disableSSEDataplane", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Support faster dashboard and folder search by splitting permission scopes into parts", - "stage": "deprecated", - "codeowner": "@grafana/identity-access-team", - "requiresRestart": true, - "hideFromAdminPage": true + "description": "Disables dataplane specific processing in server side expressions.", + "stage": "experimental", + "codeowner": "@grafana/observability-metrics" } }, { "metadata": { - "name": "formatString", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "lokiPredefinedOperations", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Enable format string transformer", - "stage": "preview", - "codeowner": "@grafana/dataviz-squad", + "description": "Adds predefined query operations to Loki query editor", + "stage": "experimental", + "codeowner": "@grafana/observability-logs", "frontend": true } }, { "metadata": { - "name": "alertingSimplifiedRouting", - "resourceVersion": "1708033557575", - "creationTimestamp": "2024-02-14T16:41:35Z", - "annotations": { - "grafana.app/updatedTimestamp": "2024-02-15 21:45:57.5750554 +0000 UTC" - } + "name": "configurableSchedulerTick", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Enables users to easily configure alert notifications by specifying a contact point directly when editing or creating an alert rule", - "stage": "preview", - "codeowner": "@grafana/alerting-squad" + "description": "Enable changing the scheduler base interval via configuration option unified_alerting.scheduler_tick_interval", + "stage": "experimental", + "codeowner": "@grafana/alerting-squad", + "requiresRestart": true, + "hideFromDocs": true } }, { "metadata": { - "name": "enablePluginsTracingByDefault", - "resourceVersion": "1707928895402", - "creationTimestamp": "2024-02-14T16:41:35Z" + "name": "kubernetesPlaylists", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "Enable plugin tracing for all external plugins", + "description": "Use the kubernetes API in the frontend for playlists, and route /api/playlist requests to k8s", "stage": "experimental", - "codeowner": "@grafana/plugins-platform-backend", + "codeowner": "@grafana/grafana-app-platform-squad", "requiresRestart": true } }, { "metadata": { - "name": "alertingUpgradeDryrunOnStart", - "resourceVersion": "1708098586429", - "creationTimestamp": "2024-02-15T21:01:16Z", - "annotations": { - "grafana.app/updatedTimestamp": "2024-02-16 15:49:46.429030423 +0000 UTC" - } + "name": "datatrails", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" }, "spec": { - "description": "When activated in legacy alerting mode, this initiates a dry-run of the Unified Alerting upgrade during each startup. It logs any issues detected without implementing any actual changes.", - "stage": "GA", - "codeowner": "@grafana/alerting-squad", - "requiresRestart": true + "description": "Enables the new core app datatrails", + "stage": "experimental", + "codeowner": "@grafana/dashboards-squad", + "frontend": true, + "hideFromDocs": true + } + }, + { + "metadata": { + "name": "editPanelCSVDragAndDrop", + "resourceVersion": "1708108588074", + "creationTimestamp": "2024-02-16T18:36:28Z" + }, + "spec": { + "description": "Enables drag and drop for CSV and Excel files", + "stage": "experimental", + "codeowner": "@grafana/dataviz-squad", + "frontend": true } } ] From edb799bf82556b8d8581be7d28be271a63d43c43 Mon Sep 17 00:00:00 2001 From: Leon Sorokin Date: Sat, 17 Feb 2024 00:35:41 -0600 Subject: [PATCH 0003/1406] VizTooltips: Fix drag-zoom causing annotation init in other shared-cursor panels (#82986) --- .../src/components/uPlot/plugins/TooltipPlugin2.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx b/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx index e9590c434b..3b755bc379 100644 --- a/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx +++ b/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx @@ -341,8 +341,10 @@ export const TooltipPlugin2 = ({ }); config.addHook('setSelect', (u) => { - if (clientZoom || queryZoom != null) { - if (maybeZoomAction(u.cursor!.event)) { + let e = u.cursor!.event; + + if (e != null && (clientZoom || queryZoom != null)) { + if (maybeZoomAction(e)) { if (clientZoom && yDrag) { if (u.select.height >= MIN_ZOOM_DIST) { for (let key in u.scales!) { From a02519895e2954a74a839bfd40707299ac4af27d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 17 Feb 2024 04:19:58 +0000 Subject: [PATCH 0004/1406] Update dependency msw to v2.2.1 --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 21f152ef70..bb8cd46ff1 100644 --- a/package.json +++ b/package.json @@ -184,7 +184,7 @@ "jest-matcher-utils": "29.7.0", "lerna": "7.4.1", "mini-css-extract-plugin": "2.8.0", - "msw": "2.2.0", + "msw": "2.2.1", "mutationobserver-shim": "0.3.7", "ngtemplate-loader": "2.1.0", "node-notifier": "10.0.1", diff --git a/yarn.lock b/yarn.lock index fe0f1545c5..2bd42ad888 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18479,7 +18479,7 @@ __metadata: mousetrap: "npm:1.6.5" mousetrap-global-bind: "npm:1.1.0" moveable: "npm:0.53.0" - msw: "npm:2.2.0" + msw: "npm:2.2.1" mutationobserver-shim: "npm:0.3.7" nanoid: "npm:^5.0.4" ngtemplate-loader: "npm:2.1.0" @@ -22981,9 +22981,9 @@ __metadata: languageName: node linkType: hard -"msw@npm:2.2.0": - version: 2.2.0 - resolution: "msw@npm:2.2.0" +"msw@npm:2.2.1": + version: 2.2.1 + resolution: "msw@npm:2.2.1" dependencies: "@bundled-es-modules/cookie": "npm:^2.0.0" "@bundled-es-modules/statuses": "npm:^1.0.1" @@ -23009,7 +23009,7 @@ __metadata: optional: true bin: msw: cli/index.js - checksum: 10/36e6a1f0bc89d1c84bf24c1cf7ca7c3fe5d5b032c825d52a832e9b815830976a86c17a8c3f1f506db720f44e99d04c9302e014cf569b51b2d2af62562a9fe62a + checksum: 10/0b07a987cc2ab950ce6c1a3c69a3e4027f0b7cdc9d2e971c3efc1fed0993eaaef6714bd0f2b473752334277c7dbeda3227284b132e519dcc951c29de312e862f languageName: node linkType: hard From 94a274635b92898a04de6d29c30e1a97f0326043 Mon Sep 17 00:00:00 2001 From: Leon Sorokin Date: Sat, 17 Feb 2024 00:38:13 -0600 Subject: [PATCH 0005/1406] VizTooltips: Fix series labels after zooming (#82985) --- .../transformations/transformers/joinDataFrames.ts | 11 +++++++++-- public/app/core/components/GraphNG/utils.ts | 5 +++++ public/app/core/components/TimelineChart/utils.ts | 4 ++++ public/app/plugins/panel/timeseries/utils.ts | 3 +++ 4 files changed, 21 insertions(+), 2 deletions(-) diff --git a/packages/grafana-data/src/transformations/transformers/joinDataFrames.ts b/packages/grafana-data/src/transformations/transformers/joinDataFrames.ts index f19b6b0ccb..121a039181 100644 --- a/packages/grafana-data/src/transformations/transformers/joinDataFrames.ts +++ b/packages/grafana-data/src/transformations/transformers/joinDataFrames.ts @@ -56,6 +56,11 @@ export interface JoinOptions { */ keepOriginIndices?: boolean; + /** + * @internal -- keep any pre-cached state.displayName + */ + keepDisplayNames?: boolean; + /** * @internal -- Optionally specify a join mode (outer or inner) */ @@ -223,8 +228,10 @@ export function joinDataFrames(options: JoinOptions): DataFrame | undefined { for (const field of fields) { a.push(field.values); originalFields.push(field); - // clear field displayName state - delete field.state?.displayName; + if (!options.keepDisplayNames) { + // clear field displayName state + delete field.state?.displayName; + } // store frame field order for tabular data join frameFieldsOrder.push(fieldsOrder); fieldsOrder++; diff --git a/public/app/core/components/GraphNG/utils.ts b/public/app/core/components/GraphNG/utils.ts index 030ea9722d..64cd07e1b6 100644 --- a/public/app/core/components/GraphNG/utils.ts +++ b/public/app/core/components/GraphNG/utils.ts @@ -110,6 +110,11 @@ export function preparePlotFrame(frames: DataFrame[], dimFields: XYFieldMatchers joinBy: dimFields.x, keep: dimFields.y, keepOriginIndices: true, + + // the join transformer force-deletes our state.displayName cache unless keepDisplayNames: true + // https://github.com/grafana/grafana/pull/31121 + // https://github.com/grafana/grafana/pull/71806 + keepDisplayNames: true, }); if (alignedFrame) { diff --git a/public/app/core/components/TimelineChart/utils.ts b/public/app/core/components/TimelineChart/utils.ts index 6aab7e2478..0b71fb5907 100644 --- a/public/app/core/components/TimelineChart/utils.ts +++ b/public/app/core/components/TimelineChart/utils.ts @@ -21,6 +21,7 @@ import { getFieldConfigWithMinMax, ThresholdsMode, TimeRange, + cacheFieldDisplayNames, } from '@grafana/data'; import { maybeSortFrame } from '@grafana/data/src/transformations/transformers/joinDataFrames'; import { applyNullInsertThreshold } from '@grafana/data/src/transformations/transformers/nulls/nullInsertThreshold'; @@ -437,6 +438,9 @@ export function prepareTimelineFields( if (!series?.length) { return { warn: 'No data in response' }; } + + cacheFieldDisplayNames(series); + let hasTimeseries = false; const frames: DataFrame[] = []; diff --git a/public/app/plugins/panel/timeseries/utils.ts b/public/app/plugins/panel/timeseries/utils.ts index 9a235db97d..73d74ec00c 100644 --- a/public/app/plugins/panel/timeseries/utils.ts +++ b/public/app/plugins/panel/timeseries/utils.ts @@ -10,6 +10,7 @@ import { isBooleanUnit, SortedVector, TimeRange, + cacheFieldDisplayNames, } from '@grafana/data'; import { convertFieldType } from '@grafana/data/src/transformations/transformers/convertFieldType'; import { applyNullInsertThreshold } from '@grafana/data/src/transformations/transformers/nulls/nullInsertThreshold'; @@ -82,6 +83,8 @@ export function prepareGraphableFields( return null; } + cacheFieldDisplayNames(series); + let useNumericX = xNumFieldIdx != null; // Make sure the numeric x field is first in the frame From 1aff748e8f735d4610d4f13d1a4d1c933c585882 Mon Sep 17 00:00:00 2001 From: Serge Zaitsev Date: Sun, 18 Feb 2024 22:26:08 +0100 Subject: [PATCH 0006/1406] Use split scopes instead of substr in search v1 (#82092) * use split scopes instead of substr in search v1 * tests, of course * yet, some test helpers dont use split scopes * another test helper to fix * add permission.identifier to group by * check if attribute is uid * fix tests * use SplitScope() * fix more tests --- pkg/services/accesscontrol/actest/common.go | 2 +- pkg/services/annotations/testutil/testutil.go | 6 +- .../sqlstore/permissions/dashboard.go | 12 ++-- .../dashboard_filter_no_subquery.go | 12 ++-- .../sqlstore/permissions/dashboard_test.go | 64 ++++++++++--------- 5 files changed, 49 insertions(+), 47 deletions(-) diff --git a/pkg/services/accesscontrol/actest/common.go b/pkg/services/accesscontrol/actest/common.go index 6be44a6911..c5c95462f5 100644 --- a/pkg/services/accesscontrol/actest/common.go +++ b/pkg/services/accesscontrol/actest/common.go @@ -116,7 +116,7 @@ func AddUserPermissionToDB(t testing.TB, db db.DB, user *user.SignedInUser) { p := accesscontrol.Permission{ RoleID: role.ID, Action: action, Scope: scope, Created: time.Now(), Updated: time.Now(), } - //p.Kind, p.Attribute, p.Identifier = p.SplitScope() + p.Kind, p.Attribute, p.Identifier = p.SplitScope() permissions = append(permissions, p) } diff --git a/pkg/services/annotations/testutil/testutil.go b/pkg/services/annotations/testutil/testutil.go index 40bd57b614..2e24e3a2c9 100644 --- a/pkg/services/annotations/testutil/testutil.go +++ b/pkg/services/annotations/testutil/testutil.go @@ -60,9 +60,9 @@ func SetupRBACPermission(t *testing.T, db *sqlstore.SQLStore, role *accesscontro var acPermission []accesscontrol.Permission for action, scopes := range user.Permissions[user.OrgID] { for _, scope := range scopes { - acPermission = append(acPermission, accesscontrol.Permission{ - RoleID: role.ID, Action: action, Scope: scope, Created: time.Now(), Updated: time.Now(), - }) + p := accesscontrol.Permission{RoleID: role.ID, Action: action, Scope: scope, Created: time.Now(), Updated: time.Now()} + p.Kind, p.Attribute, p.Identifier = p.SplitScope() + acPermission = append(acPermission, p) } } diff --git a/pkg/services/sqlstore/permissions/dashboard.go b/pkg/services/sqlstore/permissions/dashboard.go index 1f8bee79bd..a91f2ff910 100644 --- a/pkg/services/sqlstore/permissions/dashboard.go +++ b/pkg/services/sqlstore/permissions/dashboard.go @@ -148,7 +148,7 @@ func (f *accessControlDashboardPermissionFilter) buildClauses() { if len(toCheck) > 0 { if !useSelfContainedPermissions { - builder.WriteString("(dashboard.uid IN (SELECT substr(scope, 16) FROM permission WHERE scope LIKE 'dashboards:uid:%'") + builder.WriteString("(dashboard.uid IN (SELECT identifier FROM permission WHERE kind = 'dashboards' AND attribute = 'uid'") builder.WriteString(rolesFilter) args = append(args, params...) @@ -156,7 +156,7 @@ func (f *accessControlDashboardPermissionFilter) buildClauses() { builder.WriteString(" AND action = ?") args = append(args, toCheck[0]) } else { - builder.WriteString(" AND action IN (?" + strings.Repeat(", ?", len(toCheck)-1) + ") GROUP BY role_id, scope HAVING COUNT(action) = ?") + builder.WriteString(" AND action IN (?" + strings.Repeat(", ?", len(toCheck)-1) + ") GROUP BY role_id, scope, identifier HAVING COUNT(action) = ?") args = append(args, toCheck...) args = append(args, len(toCheck)) } @@ -178,7 +178,7 @@ func (f *accessControlDashboardPermissionFilter) buildClauses() { builder.WriteString(" OR ") if !useSelfContainedPermissions { - permSelector.WriteString("(SELECT substr(scope, 13) FROM permission WHERE scope LIKE 'folders:uid:%' ") + permSelector.WriteString("(SELECT identifier FROM permission WHERE kind = 'folders' AND attribute = 'uid'") permSelector.WriteString(rolesFilter) permSelectorArgs = append(permSelectorArgs, params...) @@ -186,7 +186,7 @@ func (f *accessControlDashboardPermissionFilter) buildClauses() { permSelector.WriteString(" AND action = ?") permSelectorArgs = append(permSelectorArgs, toCheck[0]) } else { - permSelector.WriteString(" AND action IN (?" + strings.Repeat(", ?", len(toCheck)-1) + ") GROUP BY role_id, scope HAVING COUNT(action) = ?") + permSelector.WriteString(" AND action IN (?" + strings.Repeat(", ?", len(toCheck)-1) + ") GROUP BY role_id, scope, identifier HAVING COUNT(action) = ?") permSelectorArgs = append(permSelectorArgs, toCheck...) permSelectorArgs = append(permSelectorArgs, len(toCheck)) } @@ -258,14 +258,14 @@ func (f *accessControlDashboardPermissionFilter) buildClauses() { toCheck := actionsToCheck(f.folderActions, f.user.GetPermissions(), folderWildcards) if len(toCheck) > 0 { if !useSelfContainedPermissions { - permSelector.WriteString("(SELECT substr(scope, 13) FROM permission WHERE scope LIKE 'folders:uid:%'") + permSelector.WriteString("(SELECT identifier FROM permission WHERE kind = 'folders' AND attribute = 'uid'") permSelector.WriteString(rolesFilter) permSelectorArgs = append(permSelectorArgs, params...) if len(toCheck) == 1 { permSelector.WriteString(" AND action = ?") permSelectorArgs = append(permSelectorArgs, toCheck[0]) } else { - permSelector.WriteString(" AND action IN (?" + strings.Repeat(", ?", len(toCheck)-1) + ") GROUP BY role_id, scope HAVING COUNT(action) = ?") + permSelector.WriteString(" AND action IN (?" + strings.Repeat(", ?", len(toCheck)-1) + ") GROUP BY role_id, scope, identifier HAVING COUNT(action) = ?") permSelectorArgs = append(permSelectorArgs, toCheck...) permSelectorArgs = append(permSelectorArgs, len(toCheck)) } diff --git a/pkg/services/sqlstore/permissions/dashboard_filter_no_subquery.go b/pkg/services/sqlstore/permissions/dashboard_filter_no_subquery.go index f2b5f432d7..120b432a57 100644 --- a/pkg/services/sqlstore/permissions/dashboard_filter_no_subquery.go +++ b/pkg/services/sqlstore/permissions/dashboard_filter_no_subquery.go @@ -59,7 +59,7 @@ func (f *accessControlDashboardPermissionFilterNoFolderSubquery) buildClauses() if len(toCheck) > 0 { if !useSelfContainedPermissions { - builder.WriteString("(dashboard.uid IN (SELECT substr(scope, 16) FROM permission WHERE scope LIKE 'dashboards:uid:%'") + builder.WriteString("(dashboard.uid IN (SELECT identifier FROM permission WHERE kind = 'dashboards' AND attribute = 'uid'") builder.WriteString(rolesFilter) args = append(args, params...) @@ -67,7 +67,7 @@ func (f *accessControlDashboardPermissionFilterNoFolderSubquery) buildClauses() builder.WriteString(" AND action = ?") args = append(args, toCheck[0]) } else { - builder.WriteString(" AND action IN (?" + strings.Repeat(", ?", len(toCheck)-1) + ") GROUP BY role_id, scope HAVING COUNT(action) = ?") + builder.WriteString(" AND action IN (?" + strings.Repeat(", ?", len(toCheck)-1) + ") GROUP BY role_id, scope, identifier HAVING COUNT(action) = ?") args = append(args, toCheck...) args = append(args, len(toCheck)) } @@ -89,7 +89,7 @@ func (f *accessControlDashboardPermissionFilterNoFolderSubquery) buildClauses() builder.WriteString(" OR ") if !useSelfContainedPermissions { - permSelector.WriteString("(SELECT substr(scope, 13) FROM permission WHERE scope LIKE 'folders:uid:%' ") + permSelector.WriteString("(SELECT identifier FROM permission WHERE kind = 'folders' AND attribute = 'uid'") permSelector.WriteString(rolesFilter) permSelectorArgs = append(permSelectorArgs, params...) @@ -97,7 +97,7 @@ func (f *accessControlDashboardPermissionFilterNoFolderSubquery) buildClauses() permSelector.WriteString(" AND action = ?") permSelectorArgs = append(permSelectorArgs, toCheck[0]) } else { - permSelector.WriteString(" AND action IN (?" + strings.Repeat(", ?", len(toCheck)-1) + ") GROUP BY role_id, scope HAVING COUNT(action) = ?") + permSelector.WriteString(" AND action IN (?" + strings.Repeat(", ?", len(toCheck)-1) + ") GROUP BY role_id, scope, identifier HAVING COUNT(action) = ?") permSelectorArgs = append(permSelectorArgs, toCheck...) permSelectorArgs = append(permSelectorArgs, len(toCheck)) } @@ -169,14 +169,14 @@ func (f *accessControlDashboardPermissionFilterNoFolderSubquery) buildClauses() toCheck := actionsToCheck(f.folderActions, f.user.GetPermissions(), folderWildcards) if len(toCheck) > 0 { if !useSelfContainedPermissions { - permSelector.WriteString("(SELECT substr(scope, 13) FROM permission WHERE scope LIKE 'folders:uid:%'") + permSelector.WriteString("(SELECT identifier FROM permission WHERE kind = 'folders' AND attribute = 'uid'") permSelector.WriteString(rolesFilter) permSelectorArgs = append(permSelectorArgs, params...) if len(toCheck) == 1 { permSelector.WriteString(" AND action = ?") permSelectorArgs = append(permSelectorArgs, toCheck[0]) } else { - permSelector.WriteString(" AND action IN (?" + strings.Repeat(", ?", len(toCheck)-1) + ") GROUP BY role_id, scope HAVING COUNT(action) = ?") + permSelector.WriteString(" AND action IN (?" + strings.Repeat(", ?", len(toCheck)-1) + ") GROUP BY role_id, scope, identifier HAVING COUNT(action) = ?") permSelectorArgs = append(permSelectorArgs, toCheck...) permSelectorArgs = append(permSelectorArgs, len(toCheck)) } diff --git a/pkg/services/sqlstore/permissions/dashboard_test.go b/pkg/services/sqlstore/permissions/dashboard_test.go index a586e78b05..48613ab14c 100644 --- a/pkg/services/sqlstore/permissions/dashboard_test.go +++ b/pkg/services/sqlstore/permissions/dashboard_test.go @@ -80,12 +80,12 @@ func TestIntegration_DashboardPermissionFilter(t *testing.T) { desc: "Should be able to view a subset of dashboards with dashboard scopes", permission: dashboardaccess.PERMISSION_VIEW, permissions: []accesscontrol.Permission{ - {Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:110"}, - {Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:40"}, - {Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:22"}, - {Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:13"}, - {Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:55"}, - {Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:99"}, + {Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:110", Kind: "dashboards", Identifier: "110"}, + {Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:40", Kind: "dashboards", Identifier: "40"}, + {Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:22", Kind: "dashboards", Identifier: "22"}, + {Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:13", Kind: "dashboards", Identifier: "13"}, + {Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:55", Kind: "dashboards", Identifier: "55"}, + {Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:99", Kind: "dashboards", Identifier: "99"}, }, expectedResult: 6, }, @@ -101,9 +101,9 @@ func TestIntegration_DashboardPermissionFilter(t *testing.T) { desc: "Should be able to view a subset folders", permission: dashboardaccess.PERMISSION_VIEW, permissions: []accesscontrol.Permission{ - {Action: dashboards.ActionFoldersRead, Scope: "folders:uid:3"}, - {Action: dashboards.ActionFoldersRead, Scope: "folders:uid:6"}, - {Action: dashboards.ActionFoldersRead, Scope: "folders:uid:9"}, + {Action: dashboards.ActionFoldersRead, Scope: "folders:uid:3", Kind: "folders", Identifier: "3"}, + {Action: dashboards.ActionFoldersRead, Scope: "folders:uid:6", Kind: "folders", Identifier: "6"}, + {Action: dashboards.ActionFoldersRead, Scope: "folders:uid:9", Kind: "folders", Identifier: "9"}, }, expectedResult: 3, }, @@ -111,10 +111,10 @@ func TestIntegration_DashboardPermissionFilter(t *testing.T) { desc: "Should return folders and dashboard with 'edit' permission", permission: dashboardaccess.PERMISSION_EDIT, permissions: []accesscontrol.Permission{ - {Action: dashboards.ActionFoldersRead, Scope: "folders:uid:3"}, - {Action: dashboards.ActionDashboardsCreate, Scope: "folders:uid:3"}, - {Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:33"}, - {Action: dashboards.ActionDashboardsWrite, Scope: "dashboards:uid:33"}, + {Action: dashboards.ActionFoldersRead, Scope: "folders:uid:3", Kind: "folders", Identifier: "3"}, + {Action: dashboards.ActionDashboardsCreate, Scope: "folders:uid:3", Kind: "folders", Identifier: "3"}, + {Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:33", Kind: "dashboards", Identifier: "33"}, + {Action: dashboards.ActionDashboardsWrite, Scope: "dashboards:uid:33", Kind: "dashboards", Identifier: "33"}, }, expectedResult: 2, }, @@ -122,11 +122,11 @@ func TestIntegration_DashboardPermissionFilter(t *testing.T) { desc: "Should return the dashboards that the User has dashboards:write permission on in case of 'edit' permission", permission: dashboardaccess.PERMISSION_EDIT, permissions: []accesscontrol.Permission{ - {Action: dashboards.ActionFoldersRead, Scope: "folders:uid:3"}, - {Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:31"}, - {Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:32"}, - {Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:33"}, - {Action: dashboards.ActionDashboardsWrite, Scope: "dashboards:uid:33"}, + {Action: dashboards.ActionFoldersRead, Scope: "folders:uid:3", Kind: "folders", Identifier: "3"}, + {Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:31", Kind: "dashboards", Identifier: "31"}, + {Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:32", Kind: "dashboards", Identifier: "32"}, + {Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:33", Kind: "dashboards", Identifier: "33"}, + {Action: dashboards.ActionDashboardsWrite, Scope: "dashboards:uid:33", Kind: "dashboards", Identifier: "33"}, }, expectedResult: 1, }, @@ -134,11 +134,11 @@ func TestIntegration_DashboardPermissionFilter(t *testing.T) { desc: "Should return the folders that the User has dashboards:create permission on in case of 'edit' permission", permission: dashboardaccess.PERMISSION_EDIT, permissions: []accesscontrol.Permission{ - {Action: dashboards.ActionFoldersRead, Scope: "folders:uid:3"}, - {Action: dashboards.ActionDashboardsCreate, Scope: "folders:uid:3"}, - {Action: dashboards.ActionFoldersRead, Scope: "folders:uid:4"}, - {Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:32"}, - {Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:33"}, + {Action: dashboards.ActionFoldersRead, Scope: "folders:uid:3", Kind: "folders", Identifier: "3"}, + {Action: dashboards.ActionDashboardsCreate, Scope: "folders:uid:3", Kind: "folders", Identifier: "3"}, + {Action: dashboards.ActionFoldersRead, Scope: "folders:uid:4", Kind: "folders", Identifier: "4"}, + {Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:32", Kind: "dashboards", Identifier: "32"}, + {Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:33", Kind: "dashboards", Identifier: "33"}, }, expectedResult: 1, }, @@ -147,10 +147,10 @@ func TestIntegration_DashboardPermissionFilter(t *testing.T) { permission: dashboardaccess.PERMISSION_VIEW, queryType: searchstore.TypeAlertFolder, permissions: []accesscontrol.Permission{ - {Action: dashboards.ActionFoldersRead, Scope: "folders:uid:3"}, - {Action: accesscontrol.ActionAlertingRuleRead, Scope: "folders:uid:3"}, - {Action: dashboards.ActionFoldersRead, Scope: "folders:uid:8"}, - {Action: accesscontrol.ActionAlertingRuleRead, Scope: "folders:uid:8"}, + {Action: dashboards.ActionFoldersRead, Scope: "folders:uid:3", Kind: "folders", Identifier: "3"}, + {Action: accesscontrol.ActionAlertingRuleRead, Scope: "folders:uid:3", Kind: "folders", Identifier: "3"}, + {Action: dashboards.ActionFoldersRead, Scope: "folders:uid:8", Kind: "folders", Identifier: "8"}, + {Action: accesscontrol.ActionAlertingRuleRead, Scope: "folders:uid:8", Kind: "folders", Identifier: "8"}, }, expectedResult: 2, }, @@ -160,8 +160,8 @@ func TestIntegration_DashboardPermissionFilter(t *testing.T) { queryType: searchstore.TypeAlertFolder, permissions: []accesscontrol.Permission{ {Action: dashboards.ActionFoldersRead, Scope: "*"}, - {Action: accesscontrol.ActionAlertingRuleRead, Scope: "folders:uid:3"}, - {Action: accesscontrol.ActionAlertingRuleRead, Scope: "folders:uid:8"}, + {Action: accesscontrol.ActionAlertingRuleRead, Scope: "folders:uid:3", Kind: "folders", Identifier: "3"}, + {Action: accesscontrol.ActionAlertingRuleRead, Scope: "folders:uid:8", Kind: "folders", Identifier: "8"}, }, expectedResult: 2, }, @@ -423,7 +423,7 @@ func TestIntegration_DashboardNestedPermissionFilter(t *testing.T) { queryType: searchstore.TypeFolder, permission: dashboardaccess.PERMISSION_VIEW, permissions: []accesscontrol.Permission{ - {Action: dashboards.ActionFoldersRead, Scope: "folders:uid:parent"}, + {Action: dashboards.ActionFoldersRead, Scope: "folders:uid:parent", Kind: "folders", Identifier: "parent"}, }, features: []any{featuremgmt.FlagNestedFolders}, expectedResult: []string{"parent", "subfolder"}, @@ -433,7 +433,7 @@ func TestIntegration_DashboardNestedPermissionFilter(t *testing.T) { queryType: searchstore.TypeFolder, permission: dashboardaccess.PERMISSION_VIEW, permissions: []accesscontrol.Permission{ - {Action: dashboards.ActionFoldersRead, Scope: "folders:uid:parent"}, + {Action: dashboards.ActionFoldersRead, Scope: "folders:uid:parent", Kind: "folders", Identifier: "parent"}, }, features: []any{}, expectedResult: []string{"parent"}, @@ -678,6 +678,7 @@ func setupTest(t *testing.T, numFolders, numDashboards int, permissions []access permissions[i].RoleID = role.ID permissions[i].Created = time.Now() permissions[i].Updated = time.Now() + permissions[i].Kind, permissions[i].Attribute, permissions[i].Identifier = permissions[i].SplitScope() } if len(permissions) > 0 { _, err = sess.InsertMulti(&permissions) @@ -769,6 +770,7 @@ func setupNestedTest(t *testing.T, usr *user.SignedInUser, perms []accesscontrol perms[i].RoleID = role.ID perms[i].Created = time.Now() perms[i].Updated = time.Now() + perms[i].Kind, perms[i].Attribute, perms[i].Identifier = perms[i].SplitScope() } if len(perms) > 0 { _, err = sess.InsertMulti(&perms) From dcc977005cff0225fdb72bf0f040e945fe45cab5 Mon Sep 17 00:00:00 2001 From: Hugo Kiyodi Oshiro Date: Mon, 19 Feb 2024 09:38:06 +0100 Subject: [PATCH 0007/1406] Plugins: Disable update button when cloud install is not completed (#81716) --- .../InstallControlsButton.test.tsx | 77 +++++++++++++++++++ .../InstallControls/InstallControlsButton.tsx | 7 +- public/app/features/plugins/admin/helpers.ts | 19 +++-- public/app/features/plugins/admin/types.ts | 2 + 4 files changed, 98 insertions(+), 7 deletions(-) diff --git a/public/app/features/plugins/admin/components/InstallControls/InstallControlsButton.test.tsx b/public/app/features/plugins/admin/components/InstallControls/InstallControlsButton.test.tsx index eda80c9c82..77bdb1f096 100644 --- a/public/app/features/plugins/admin/components/InstallControls/InstallControlsButton.test.tsx +++ b/public/app/features/plugins/admin/components/InstallControls/InstallControlsButton.test.tsx @@ -4,7 +4,9 @@ import { TestProvider } from 'test/helpers/TestProvider'; import { PluginSignatureStatus } from '@grafana/data'; import { config } from '@grafana/runtime'; +import { configureStore } from 'app/store/configureStore'; +import { getPluginsStateMock } from '../../__mocks__'; import { CatalogPlugin, PluginStatus } from '../../types'; import { InstallControlsButton } from './InstallControlsButton'; @@ -91,4 +93,79 @@ describe('InstallControlsButton', () => { ); expect(screen.queryByRole('button')).not.toBeInTheDocument(); }); + + describe('update button on prem', () => { + const store = configureStore({ + plugins: getPluginsStateMock([]), + }); + + it('should be disabled when is Installing', () => { + store.dispatch({ type: 'plugins/install/pending' }); + render( + + + + ); + const button = screen.getByText('Updating').closest('button'); + expect(button).toBeDisabled(); + }); + + it('should be enabled when it is not Installing', () => { + store.dispatch({ type: 'plugins/install/fulfilled', payload: { id: '', changes: {} } }); + render( + + + + ); + const button = screen.getByText('Update').closest('button'); + expect(button).toBeEnabled(); + }); + }); + + describe('update button on managed instance', () => { + const oldFeatureTogglesManagedPluginsInstall = config.featureToggles.managedPluginsInstall; + const oldPluginAdminExternalManageEnabled = config.pluginAdminExternalManageEnabled; + + beforeAll(() => { + config.featureToggles.managedPluginsInstall = true; + config.pluginAdminExternalManageEnabled = true; + }); + + afterAll(() => { + config.featureToggles.managedPluginsInstall = oldFeatureTogglesManagedPluginsInstall; + config.pluginAdminExternalManageEnabled = oldPluginAdminExternalManageEnabled; + }); + + const store = configureStore({ + plugins: getPluginsStateMock([]), + }); + + it('should be disabled when isInstalling=false but isUpdatingFromInstance=true', () => { + store.dispatch({ type: 'plugins/install/fulfilled', payload: { id: '', changes: {} } }); + render( + + + + ); + const button = screen.getByText('Update').closest('button'); + expect(button).toBeDisabled(); + }); + + it('should be enabled when isInstalling=false and isUpdatingFromInstance=false', () => { + store.dispatch({ type: 'plugins/install/fulfilled', payload: { id: '', changes: {} } }); + render( + + + + ); + const button = screen.getByText('Update').closest('button'); + expect(button).toBeEnabled(); + }); + }); }); diff --git a/public/app/features/plugins/admin/components/InstallControls/InstallControlsButton.tsx b/public/app/features/plugins/admin/components/InstallControls/InstallControlsButton.tsx index 74689dabd8..68411e2fbd 100644 --- a/public/app/features/plugins/admin/components/InstallControls/InstallControlsButton.tsx +++ b/public/app/features/plugins/admin/components/InstallControls/InstallControlsButton.tsx @@ -140,9 +140,14 @@ export function InstallControlsButton({ } if (pluginStatus === PluginStatus.UPDATE) { + const disableUpdate = + config.pluginAdminExternalManageEnabled && configCore.featureToggles.managedPluginsInstall + ? plugin.isUpdatingFromInstance + : isInstalling; + return ( - diff --git a/public/app/features/plugins/admin/helpers.ts b/public/app/features/plugins/admin/helpers.ts index 25f32871f2..86c0c92b3e 100644 --- a/public/app/features/plugins/admin/helpers.ts +++ b/public/app/features/plugins/admin/helpers.ts @@ -60,6 +60,8 @@ export function mergeLocalsAndRemotes({ instancesMap.has(remotePlugin.slug) && catalogPlugin.hasUpdate && catalogPlugin.installedVersion !== instancePlugin?.version; + + catalogPlugin.isUninstallingFromInstance = Boolean(localCounterpart) && !instancesMap.has(remotePlugin.slug); } catalogPlugins.push(catalogPlugin); diff --git a/public/app/features/plugins/admin/types.ts b/public/app/features/plugins/admin/types.ts index 833cb1bc07..7c54128e00 100644 --- a/public/app/features/plugins/admin/types.ts +++ b/public/app/features/plugins/admin/types.ts @@ -62,6 +62,7 @@ export interface CatalogPlugin extends WithAccessControlMetadata { // instance plugins may not be fully installed, which means a new instance // running the plugin didn't started yet isFullyInstalled?: boolean; + isUninstallingFromInstance?: boolean; isUpdatingFromInstance?: boolean; iam?: IdentityAccessManagement; } From 2175509929c632f3e52a4332ae9debf337e47dd1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 19 Feb 2024 10:47:12 +0000 Subject: [PATCH 0010/1406] Update React Aria --- package.json | 8 +- packages/grafana-ui/package.json | 8 +- yarn.lock | 251 +++++++++++++++++-------------- 3 files changed, 149 insertions(+), 118 deletions(-) diff --git a/package.json b/package.json index bb8cd46ff1..56581b1195 100644 --- a/package.json +++ b/package.json @@ -266,10 +266,10 @@ "@opentelemetry/semantic-conventions": "1.21.0", "@popperjs/core": "2.11.8", "@prometheus-io/lezer-promql": "^0.37.0-rc.1", - "@react-aria/dialog": "3.5.11", - "@react-aria/focus": "3.16.1", - "@react-aria/overlays": "3.21.0", - "@react-aria/utils": "3.23.1", + "@react-aria/dialog": "3.5.12", + "@react-aria/focus": "3.16.2", + "@react-aria/overlays": "3.21.1", + "@react-aria/utils": "3.23.2", "@react-awesome-query-builder/core": "6.4.2", "@react-awesome-query-builder/ui": "6.4.2", "@reduxjs/toolkit": "1.9.5", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 6036034baf..ca0fbb6aee 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -57,10 +57,10 @@ "@leeoniya/ufuzzy": "1.0.14", "@monaco-editor/react": "4.6.0", "@popperjs/core": "2.11.8", - "@react-aria/dialog": "3.5.11", - "@react-aria/focus": "3.16.1", - "@react-aria/overlays": "3.21.0", - "@react-aria/utils": "3.23.1", + "@react-aria/dialog": "3.5.12", + "@react-aria/focus": "3.16.2", + "@react-aria/overlays": "3.21.1", + "@react-aria/utils": "3.23.2", "ansicolor": "1.1.100", "calculate-size": "1.1.1", "classnames": "2.5.1", diff --git a/yarn.lock b/yarn.lock index 2bd42ad888..3b4ca5c400 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4143,10 +4143,10 @@ __metadata: "@leeoniya/ufuzzy": "npm:1.0.14" "@monaco-editor/react": "npm:4.6.0" "@popperjs/core": "npm:2.11.8" - "@react-aria/dialog": "npm:3.5.11" - "@react-aria/focus": "npm:3.16.1" - "@react-aria/overlays": "npm:3.21.0" - "@react-aria/utils": "npm:3.23.1" + "@react-aria/dialog": "npm:3.5.12" + "@react-aria/focus": "npm:3.16.2" + "@react-aria/overlays": "npm:3.21.1" + "@react-aria/utils": "npm:3.23.2" "@rollup/plugin-node-resolve": "npm:15.2.3" "@storybook/addon-a11y": "npm:7.4.5" "@storybook/addon-actions": "npm:7.4.5" @@ -4352,40 +4352,40 @@ __metadata: languageName: node linkType: hard -"@internationalized/date@npm:^3.5.1": - version: 3.5.1 - resolution: "@internationalized/date@npm:3.5.1" +"@internationalized/date@npm:^3.5.2": + version: 3.5.2 + resolution: "@internationalized/date@npm:3.5.2" dependencies: "@swc/helpers": "npm:^0.5.0" - checksum: 10/38bce4ca2123dc4a3a7ef62ea44a86e0619764d42c3895ae5f0c4f17e7320dc945d61691dd0bc0f4dff6cda834113d1fe55253afebf4a46bf21e7b0e9f890096 + checksum: 10/e37cdea4efa6214e72148f55f42782b3e8cd40bdca29705e52e6c490855f9ccbf38d0182632be005d9555463b50e8bf5fdb0d759cadff1baf7bae4fdaa28e96f languageName: node linkType: hard -"@internationalized/message@npm:^3.1.1": - version: 3.1.1 - resolution: "@internationalized/message@npm:3.1.1" +"@internationalized/message@npm:^3.1.2": + version: 3.1.2 + resolution: "@internationalized/message@npm:3.1.2" dependencies: "@swc/helpers": "npm:^0.5.0" intl-messageformat: "npm:^10.1.0" - checksum: 10/b73b443e75ab1d95e0d406a75107b1899d221883463de95769f3d63836bf91e7ac1ce07bd141121b9ccb89ff24d469aa424ba47e85b02dc8a8e0827b991bf801 + checksum: 10/c6b8f9983f1922f27c45586d82500a8fd4e75cab622c367b70047bb9f45749ab8153c77b02fd3da635e3d6649d8609ae6d1df6da710a166361078e32b4516d2e languageName: node linkType: hard -"@internationalized/number@npm:^3.5.0": - version: 3.5.0 - resolution: "@internationalized/number@npm:3.5.0" +"@internationalized/number@npm:^3.5.1": + version: 3.5.1 + resolution: "@internationalized/number@npm:3.5.1" dependencies: "@swc/helpers": "npm:^0.5.0" - checksum: 10/8272b5da8afd4e1379767765f9ef24283e7ccb7c077646ded17fe7de11d72d2fd8f6e41f4ea21f101d084133f670059f062c3929ff18cf171f8f768151502bf5 + checksum: 10/4ad68d98285a18a910d19455a0fa9c3960a919a139f0b01d2d589bfda1a2ebb8378b8c912e17c0d82cf756e7b3f48b0bff8a6decef1644c6c2f894da4e1e7c79 languageName: node linkType: hard -"@internationalized/string@npm:^3.2.0": - version: 3.2.0 - resolution: "@internationalized/string@npm:3.2.0" +"@internationalized/string@npm:^3.2.1": + version: 3.2.1 + resolution: "@internationalized/string@npm:3.2.1" dependencies: "@swc/helpers": "npm:^0.5.0" - checksum: 10/ebe3cf9394baa5cc134eb6956f57785ddaaae79e9f66400783a0560541747fef170a59fd05923dc0c41e3f860343cc1175b3435412a616570d3247199f10c0e0 + checksum: 10/69603641a90fee37fc539adc8f3f5cbdd61909da486515bd4580fcce05495a9f0f303e6d8a36a8accb86c95845d84e78b088e4680ca087928b6b588756eb879b languageName: node linkType: hard @@ -6686,129 +6686,129 @@ __metadata: languageName: node linkType: hard -"@react-aria/dialog@npm:3.5.11": - version: 3.5.11 - resolution: "@react-aria/dialog@npm:3.5.11" +"@react-aria/dialog@npm:3.5.12": + version: 3.5.12 + resolution: "@react-aria/dialog@npm:3.5.12" dependencies: - "@react-aria/focus": "npm:^3.16.1" - "@react-aria/overlays": "npm:^3.21.0" - "@react-aria/utils": "npm:^3.23.1" - "@react-types/dialog": "npm:^3.5.7" - "@react-types/shared": "npm:^3.22.0" + "@react-aria/focus": "npm:^3.16.2" + "@react-aria/overlays": "npm:^3.21.1" + "@react-aria/utils": "npm:^3.23.2" + "@react-types/dialog": "npm:^3.5.8" + "@react-types/shared": "npm:^3.22.1" "@swc/helpers": "npm:^0.5.0" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 - checksum: 10/0abbf480c71cbffd334815714231d975ae235f4e4349a2408f134c283cd97e1b57df9ef77e6ee1a9a79da4a87b5c1d8b1c58b53597e59f015fba9295e2d1551f + checksum: 10/3de699980e8582056675fef747746c23f8875940d85bd6dafadacf9e59c0edd0d9b1dc2f011fb1cfbcbdebdf4a2796fa9bc19e953c4c809f671151975031d6bf languageName: node linkType: hard -"@react-aria/focus@npm:3.16.1, @react-aria/focus@npm:^3.16.1": - version: 3.16.1 - resolution: "@react-aria/focus@npm:3.16.1" +"@react-aria/focus@npm:3.16.2, @react-aria/focus@npm:^3.16.2": + version: 3.16.2 + resolution: "@react-aria/focus@npm:3.16.2" dependencies: - "@react-aria/interactions": "npm:^3.21.0" - "@react-aria/utils": "npm:^3.23.1" - "@react-types/shared": "npm:^3.22.0" + "@react-aria/interactions": "npm:^3.21.1" + "@react-aria/utils": "npm:^3.23.2" + "@react-types/shared": "npm:^3.22.1" "@swc/helpers": "npm:^0.5.0" clsx: "npm:^2.0.0" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 - checksum: 10/3711311e0c54f311b8959b55d57b66a3f8f5e83e80cbd5ef4dbe6137b6386dc4608009d035e4e72076a97f9dd8e8f4d6b898ea7e54f1b706220392ad14dea1fb + checksum: 10/da25d79534443652ed2ad560ce1e56653a28ac5ccbd5a7be2822c11b748f46e8a544f37bea0bff8ad1a82493c77c6f17c418c86c995abe45df36fbe33bae0156 languageName: node linkType: hard -"@react-aria/i18n@npm:^3.10.1": - version: 3.10.1 - resolution: "@react-aria/i18n@npm:3.10.1" +"@react-aria/i18n@npm:^3.10.2": + version: 3.10.2 + resolution: "@react-aria/i18n@npm:3.10.2" dependencies: - "@internationalized/date": "npm:^3.5.1" - "@internationalized/message": "npm:^3.1.1" - "@internationalized/number": "npm:^3.5.0" - "@internationalized/string": "npm:^3.2.0" - "@react-aria/ssr": "npm:^3.9.1" - "@react-aria/utils": "npm:^3.23.1" - "@react-types/shared": "npm:^3.22.0" + "@internationalized/date": "npm:^3.5.2" + "@internationalized/message": "npm:^3.1.2" + "@internationalized/number": "npm:^3.5.1" + "@internationalized/string": "npm:^3.2.1" + "@react-aria/ssr": "npm:^3.9.2" + "@react-aria/utils": "npm:^3.23.2" + "@react-types/shared": "npm:^3.22.1" "@swc/helpers": "npm:^0.5.0" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 - checksum: 10/a5cd01aaeee2b3d12eff6128657c3c29b633edbe8acaf7ce91d0a65e76dbe1ed3bbe90dd3baaa818c78f4564d48c8d0065afe33aadeb18c29e9cd16529dfc036 + checksum: 10/e24558e3f659246b59e5a2862a99debec7cd9ec152c74fbfbfc15c0816a77448d455a131790b954697fcc0bf8633bc102c1b27121a8b7043820563c7b5987095 languageName: node linkType: hard -"@react-aria/interactions@npm:^3.21.0": - version: 3.21.0 - resolution: "@react-aria/interactions@npm:3.21.0" +"@react-aria/interactions@npm:^3.21.1": + version: 3.21.1 + resolution: "@react-aria/interactions@npm:3.21.1" dependencies: - "@react-aria/ssr": "npm:^3.9.1" - "@react-aria/utils": "npm:^3.23.1" - "@react-types/shared": "npm:^3.22.0" + "@react-aria/ssr": "npm:^3.9.2" + "@react-aria/utils": "npm:^3.23.2" + "@react-types/shared": "npm:^3.22.1" "@swc/helpers": "npm:^0.5.0" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 - checksum: 10/90bc18aee6f4e2d388b921a2e953bd1a3556c080cf51ef03b347710c4067afe94229e03765bb7c54fc51c17bdb29b0396add1da47cb3a8d8dec2333c7e77b7ef + checksum: 10/ca0918dca1ee41e7ac9129eeb5a23f02a9043cae55f0ee381dc93bd763ac31928a809e029e8bd144223b0f44275736b29079d99fbd22891c244f09c50d16665b languageName: node linkType: hard -"@react-aria/overlays@npm:3.21.0, @react-aria/overlays@npm:^3.21.0": - version: 3.21.0 - resolution: "@react-aria/overlays@npm:3.21.0" - dependencies: - "@react-aria/focus": "npm:^3.16.1" - "@react-aria/i18n": "npm:^3.10.1" - "@react-aria/interactions": "npm:^3.21.0" - "@react-aria/ssr": "npm:^3.9.1" - "@react-aria/utils": "npm:^3.23.1" - "@react-aria/visually-hidden": "npm:^3.8.9" - "@react-stately/overlays": "npm:^3.6.4" - "@react-types/button": "npm:^3.9.1" - "@react-types/overlays": "npm:^3.8.4" - "@react-types/shared": "npm:^3.22.0" +"@react-aria/overlays@npm:3.21.1, @react-aria/overlays@npm:^3.21.1": + version: 3.21.1 + resolution: "@react-aria/overlays@npm:3.21.1" + dependencies: + "@react-aria/focus": "npm:^3.16.2" + "@react-aria/i18n": "npm:^3.10.2" + "@react-aria/interactions": "npm:^3.21.1" + "@react-aria/ssr": "npm:^3.9.2" + "@react-aria/utils": "npm:^3.23.2" + "@react-aria/visually-hidden": "npm:^3.8.10" + "@react-stately/overlays": "npm:^3.6.5" + "@react-types/button": "npm:^3.9.2" + "@react-types/overlays": "npm:^3.8.5" + "@react-types/shared": "npm:^3.22.1" "@swc/helpers": "npm:^0.5.0" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 - checksum: 10/bb8180f5857be628ac88c26e847c0eb06e31c6ec0466410345a568e3bb902b43da8d302fa419b09c9e8251e83a0f97f45b3bac565e327a631a19605599db2bfe + checksum: 10/3143558dfb6e266194c0581475d10827d1296bb517e3cb3b50e4fe09a5e44a5616440a8f857389ab83572bbb507d738976651fcbf8eec9df0730a93aca159eb7 languageName: node linkType: hard -"@react-aria/ssr@npm:^3.9.1": - version: 3.9.1 - resolution: "@react-aria/ssr@npm:3.9.1" +"@react-aria/ssr@npm:^3.9.2": + version: 3.9.2 + resolution: "@react-aria/ssr@npm:3.9.2" dependencies: "@swc/helpers": "npm:^0.5.0" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 - checksum: 10/a42bf23241b022e2e55ca95aeec5cafb3aa276b4586373f4b85834655ab05068d5af81707bf1d4548f2f5b29c80a02ef920c0711b2d1a8b189effca2c72ca5f9 + checksum: 10/fe4ce0ccc647d14f158724c0605433291f1403a73c82cb6654c323b5153fa3afbf0d36618bb3ecac38217b56837c27490c32b7d2082034b1171de6e95a4382a8 languageName: node linkType: hard -"@react-aria/utils@npm:3.23.1, @react-aria/utils@npm:^3.23.1": - version: 3.23.1 - resolution: "@react-aria/utils@npm:3.23.1" +"@react-aria/utils@npm:3.23.2, @react-aria/utils@npm:^3.23.2": + version: 3.23.2 + resolution: "@react-aria/utils@npm:3.23.2" dependencies: - "@react-aria/ssr": "npm:^3.9.1" - "@react-stately/utils": "npm:^3.9.0" - "@react-types/shared": "npm:^3.22.0" + "@react-aria/ssr": "npm:^3.9.2" + "@react-stately/utils": "npm:^3.9.1" + "@react-types/shared": "npm:^3.22.1" "@swc/helpers": "npm:^0.5.0" clsx: "npm:^2.0.0" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 - checksum: 10/57d5f8b3a3bf932efabc329f665d9ff6aa62a4f40a1ce7694a858b4280a0f81bd1a628737b2f283ac911f7588946b63abb6899002082adbb55848ada108bb0c8 + checksum: 10/132ac6e2e6f5eb7469a52ebc5a909ad2bdb8606b835c0cc8e5320447dc3cd34f8d0ed3441a75827ae1cd91bef435c0c6e463fec72fe4fa5fe565c7d87576301d languageName: node linkType: hard -"@react-aria/visually-hidden@npm:^3.8.9": - version: 3.8.9 - resolution: "@react-aria/visually-hidden@npm:3.8.9" +"@react-aria/visually-hidden@npm:^3.8.10": + version: 3.8.10 + resolution: "@react-aria/visually-hidden@npm:3.8.10" dependencies: - "@react-aria/interactions": "npm:^3.21.0" - "@react-aria/utils": "npm:^3.23.1" - "@react-types/shared": "npm:^3.22.0" + "@react-aria/interactions": "npm:^3.21.1" + "@react-aria/utils": "npm:^3.23.2" + "@react-types/shared": "npm:^3.22.1" "@swc/helpers": "npm:^0.5.0" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 - checksum: 10/04e305447b365fabfa35477d1641292e54a22870631ff9dec68fbd59373364ce81de86e2a5f7098b55b3bfc2227b0045ad1040cad190abe3856abecb0a99a924 + checksum: 10/a7f9d8dccfeefb035d01ad8d9db4576f6acf7f0fcb94aad717cec177f113f6507f0dca0c7ee157abe40b358685b4cb84f9bce0c24dab2af753698ec8c1504264 languageName: node linkType: hard @@ -6844,31 +6844,31 @@ __metadata: languageName: node linkType: hard -"@react-stately/overlays@npm:^3.6.4": - version: 3.6.4 - resolution: "@react-stately/overlays@npm:3.6.4" +"@react-stately/overlays@npm:^3.6.5": + version: 3.6.5 + resolution: "@react-stately/overlays@npm:3.6.5" dependencies: - "@react-stately/utils": "npm:^3.9.0" - "@react-types/overlays": "npm:^3.8.4" + "@react-stately/utils": "npm:^3.9.1" + "@react-types/overlays": "npm:^3.8.5" "@swc/helpers": "npm:^0.5.0" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 - checksum: 10/80205ef3a99c80ebc43c14f55287de45a71185acdb5243d0194ee8fe963f3ea4acc4abc877ed445e457df5b0c8cc5812b7d8181d425f9b28c895cfd7d05e6a55 + checksum: 10/83805f078eb42290ddb9f88d8cbd7403a4d5f15177fce4c9f8cec91acf177af1d5a414472c58029fc1f8bf6730d5ca9716a8b3cd750f2afd6b57e592a7f09ef7 languageName: node linkType: hard -"@react-stately/utils@npm:^3.9.0": - version: 3.9.0 - resolution: "@react-stately/utils@npm:3.9.0" +"@react-stately/utils@npm:^3.9.1": + version: 3.9.1 + resolution: "@react-stately/utils@npm:3.9.1" dependencies: "@swc/helpers": "npm:^0.5.0" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 - checksum: 10/66bb72441c289c334cf626ac789bb601db8b765e3f522181f8ff38b281bede9d1b2474dc6d5f17b6b31c12f48425797151eb2d4df5922e05c2e467ee195b7ade + checksum: 10/17ddef6415db0950c474c6ad87a0d7b20a98aac817771887922ea6c6a90b9b91eb49205adf021349034f8da012fc0e3c30f6c9b378265ae6d0df93c3b4104b53 languageName: node linkType: hard -"@react-types/button@npm:3.9.1, @react-types/button@npm:^3.9.1": +"@react-types/button@npm:3.9.1": version: 3.9.1 resolution: "@react-types/button@npm:3.9.1" dependencies: @@ -6879,15 +6879,26 @@ __metadata: languageName: node linkType: hard -"@react-types/dialog@npm:^3.5.7": - version: 3.5.7 - resolution: "@react-types/dialog@npm:3.5.7" +"@react-types/button@npm:^3.9.2": + version: 3.9.2 + resolution: "@react-types/button@npm:3.9.2" dependencies: - "@react-types/overlays": "npm:^3.8.4" - "@react-types/shared": "npm:^3.22.0" + "@react-types/shared": "npm:^3.22.1" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 - checksum: 10/4f79ee1eb4e935435c2f04571900ac83fd0638f0d15fe4ac1b5acac6057718a37fdec0b735c3b44439f09b905552c241107c1b14fac57dcb7e14bc68127f0e9c + checksum: 10/8393ba87dfd6ca73fedf8f7ab3567361f1d6057f640346f2a0cc631e9659ad7c1aa2ddb255e1df6b880d8f6cd209e8c9d1d01c73e2ee2a149f180d8ebaabf1db + languageName: node + linkType: hard + +"@react-types/dialog@npm:^3.5.8": + version: 3.5.8 + resolution: "@react-types/dialog@npm:3.5.8" + dependencies: + "@react-types/overlays": "npm:^3.8.5" + "@react-types/shared": "npm:^3.22.1" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: 10/c0c387367fd697dff96fa7252cdd1d63fe7c871c93f57ed313c890ef1366e0dd85763966e1e9adc16aa9486414075b349757198572c5c5feb010897f6af9d0bf languageName: node linkType: hard @@ -6903,7 +6914,7 @@ __metadata: languageName: node linkType: hard -"@react-types/overlays@npm:3.8.4, @react-types/overlays@npm:^3.8.4": +"@react-types/overlays@npm:3.8.4": version: 3.8.4 resolution: "@react-types/overlays@npm:3.8.4" dependencies: @@ -6914,7 +6925,18 @@ __metadata: languageName: node linkType: hard -"@react-types/shared@npm:3.22.0, @react-types/shared@npm:^3.22.0": +"@react-types/overlays@npm:^3.8.4, @react-types/overlays@npm:^3.8.5": + version: 3.8.5 + resolution: "@react-types/overlays@npm:3.8.5" + dependencies: + "@react-types/shared": "npm:^3.22.1" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: 10/6c952fdbe7724b07cade95e8d3fe6bf61cb6e993b730051c1ada33da2afe246e3124a8981127977cc55f6df32124b049504fda7d19593446895559ca00a9f0b9 + languageName: node + linkType: hard + +"@react-types/shared@npm:3.22.0": version: 3.22.0 resolution: "@react-types/shared@npm:3.22.0" peerDependencies: @@ -6923,6 +6945,15 @@ __metadata: languageName: node linkType: hard +"@react-types/shared@npm:^3.22.0, @react-types/shared@npm:^3.22.1": + version: 3.22.1 + resolution: "@react-types/shared@npm:3.22.1" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 + checksum: 10/da5fc6775a79ae8148d80a6cd7025ff0d44462c5b8923cdd072ac34626ac7416049f297ec078ebed29fd49d65fd356f21ede9587517b88f20f9d6236107c1333 + languageName: node + linkType: hard + "@reduxjs/toolkit@npm:1.9.5": version: 1.9.5 resolution: "@reduxjs/toolkit@npm:1.9.5" @@ -18295,10 +18326,10 @@ __metadata: "@pmmmwh/react-refresh-webpack-plugin": "npm:0.5.11" "@popperjs/core": "npm:2.11.8" "@prometheus-io/lezer-promql": "npm:^0.37.0-rc.1" - "@react-aria/dialog": "npm:3.5.11" - "@react-aria/focus": "npm:3.16.1" - "@react-aria/overlays": "npm:3.21.0" - "@react-aria/utils": "npm:3.23.1" + "@react-aria/dialog": "npm:3.5.12" + "@react-aria/focus": "npm:3.16.2" + "@react-aria/overlays": "npm:3.21.1" + "@react-aria/utils": "npm:3.23.2" "@react-awesome-query-builder/core": "npm:6.4.2" "@react-awesome-query-builder/ui": "npm:6.4.2" "@react-types/button": "npm:3.9.1" From bfd5475e1e13464801229bbd1d1bfea7659c18a8 Mon Sep 17 00:00:00 2001 From: Jan Garaj Date: Mon, 19 Feb 2024 12:04:23 +0100 Subject: [PATCH 0011/1406] CloudWatch: Update AWS/Kafka metrics (#83035) --- pkg/tsdb/cloudwatch/constants/metrics.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/tsdb/cloudwatch/constants/metrics.go b/pkg/tsdb/cloudwatch/constants/metrics.go index efc7aaee8d..aee31d2d53 100644 --- a/pkg/tsdb/cloudwatch/constants/metrics.go +++ b/pkg/tsdb/cloudwatch/constants/metrics.go @@ -353,7 +353,7 @@ var NamespaceMetricsMap = map[string][]string{ "AWS/IoTAnalytics": {"ActionExecution", "ActivityExecutionError", "IncomingMessages"}, "AWS/IoTSiteWise": {"Gateway.Heartbeat", "Gateway.PublishSuccessCount", "Gateway.PublishFailureCount", "Gateway.ProcessFailureCount", "OPCUACollector.Heartbeat", "OPCUACollector.ActiveDataStreamCount", "OPCUACollector.IncomingValuesCount"}, "AWS/KMS": {"SecondsUntilKeyMaterialExpiration"}, - "AWS/Kafka": {"ActiveControllerCount", "BytesInPerSec", "BytesOutPerSec", "CpuIdle", "CpuSystem", "CpuUser", "EstimatedMaxTimeLag", "EstimatedTimeLag", "FetchConsumerLocalTimeMsMean", "FetchConsumerRequestQueueTimeMsMean", "FetchConsumerResponseQueueTimeMsMean", "FetchConsumerResponseSendTimeMsMean", "FetchConsumerTotalTimeMsMean", "FetchFollowerLocalTimeMsMean", "FetchFollowerRequestQueueTimeMsMean", "FetchFollowerResponseQueueTimeMsMean", "FetchFollowerResponseSendTimeMsMean", "FetchFollowerTotalTimeMsMean", "FetchMessageConversionsPerSec", "FetchThrottleByteRate", "FetchThrottleQueueSize", "FetchThrottleTime", "GlobalPartitionCount", "GlobalTopicCount", "KafkaAppLogsDiskUsed", "KafkaDataLogsDiskUsed", "LeaderCount", "MaxOffsetLag", "MemoryBuffered", "MemoryCached", "MemoryFree", "MemoryUsed", "MessagesInPerSec", "NetworkProcessorAvgIdlePercent", "NetworkRxDropped", "NetworkRxErrors", "NetworkRxPackets", "NetworkTxDropped", "NetworkTxErrors", "NetworkTxPackets", "OfflinePartitionsCount", "PartitionCount", "ProduceLocalTimeMsMean", "ProduceMessageConversionsPerSec", "ProduceMessageConversionsTimeMsMean", "ProduceRequestQueueTimeMsMean", "ProduceResponseQueueTimeMsMean", "ProduceResponseSendTimeMsMean", "ProduceThrottleByteRate", "ProduceThrottleQueueSize", "ProduceThrottleTime", "ProduceTotalTimeMsMean", "ReplicationBytesInPerSec", "ReplicationBytesOutPerSec", "RequestBytesMean", "RequestExemptFromThrottleTime", "RequestHandlerAvgIdlePercent", "RequestThrottleQueueSize", "RequestThrottleTime", "RequestTime", "RootDiskUsed", "SumOffsetLag", "SwapFree", "SwapUsed", "OffsetLag", "UnderMinIsrPartitionCount", "UnderReplicatedPartitions", "ZooKeeperRequestLatencyMsMean", "ZooKeeperSessionState"}, + "AWS/Kafka": {"ActiveControllerCount", "BurstBalance", "BwInAllowanceExceeded", "BwOutAllowanceExceeded", "BytesInPerSec", "BytesOutPerSec", "ClientConnectionCount", "ConnectionCloseRate", "ConnectionCount", "ConnectionCreationRate", "ConnTrackAllowanceExceeded", "CPUCreditBalance", "CpuCreditUsage", "CpuIdle", "CpuIoWait", "CpuSystem", "CpuUser", "EstimatedMaxTimeLag", "EstimatedTimeLag", "FetchConsumerLocalTimeMsMean", "FetchConsumerRequestQueueTimeMsMean", "FetchConsumerResponseQueueTimeMsMean", "FetchConsumerResponseSendTimeMsMean", "FetchConsumerTotalTimeMsMean", "FetchFollowerLocalTimeMsMean", "FetchFollowerRequestQueueTimeMsMean", "FetchFollowerResponseQueueTimeMsMean", "FetchFollowerResponseSendTimeMsMean", "FetchFollowerTotalTimeMsMean", "FetchMessageConversionsPerSec", "FetchThrottleByteRate", "FetchThrottleQueueSize", "FetchThrottleTime", "GlobalPartitionCount", "GlobalTopicCount", "HeapMemoryAfterGC", "KafkaAppLogsDiskUsed", "KafkaDataLogsDiskUsed", "LeaderCount", "MaxOffsetLag", "MemoryBuffered", "MemoryCached", "MemoryFree", "MemoryUsed", "MessagesInPerSec", "NetworkProcessorAvgIdlePercent", "NetworkRxDropped", "NetworkRxErrors", "NetworkRxPackets", "NetworkTxDropped", "NetworkTxErrors", "NetworkTxPackets", "OfflinePartitionsCount", "OffsetLag", "PartitionCount", "PpsAllowanceExceeded", "ProduceLocalTimeMsMean", "ProduceMessageConversionsPerSec", "ProduceMessageConversionsTimeMsMean", "ProduceRequestQueueTimeMsMean", "ProduceResponseQueueTimeMsMean", "ProduceResponseSendTimeMsMean", "ProduceThrottleByteRate", "ProduceThrottleQueueSize", "ProduceThrottleTime", "ProduceTotalTimeMsMean", "RemoteCopyBytesPerSec", "RemoteCopyErrorsPerSec", "RemoteCopyLagBytes", "RemoteFetchBytesPerSec", "RemoteFetchErrorsPerSec", "RemoteFetchRequestsPerSec", "RemoteLogManagerTasksAvgIdlePercent", "RemoteLogReaderAvgIdlePercent", "RemoteLogReaderTaskQueueSize", "ReplicationBytesInPerSec", "ReplicationBytesOutPerSec", "RequestBytesMean", "RequestExemptFromThrottleTime", "RequestHandlerAvgIdlePercent", "RequestThrottleQueueSize", "RequestThrottleTime", "RequestTime", "RootDiskUsed", "SumOffsetLag", "SwapFree", "SwapUsed", "TcpConnections", "TrafficBytes", "TrafficShaping", "UnderMinIsrPartitionCount", "UnderReplicatedPartitions", "VolumeQueueLength", "VolumeReadBytes", "VolumeReadOps", "VolumeTotalReadTime", "VolumeTotalWriteTime", "VolumeWriteBytes", "VolumeWriteOps", "ZooKeeperRequestLatencyMsMean", "ZooKeeperSessionState"}, "AWS/KafkaConnect": {"BytesInPerSec", "BytesOutPerSec", "CpuUtilization", "ErroredTaskCount", "MemoryUtilization", "RebalanceCompletedTotal", "RebalanceTimeAvg", "RebalanceTimeMax", "RebalanceTimeSinceLast", "RunningTaskCount", "SinkRecordReadRate", "SinkRecordSendRate", "SourceRecordPollRate", "SourceRecordWriteRate", "TaskStartupAttemptsTotal", "TaskStartupSuccessPercentage", "WorkerCount"}, "AWS/Kinesis": {"GetRecords.Bytes", "GetRecords.IteratorAge", "GetRecords.IteratorAgeMilliseconds", "GetRecords.Latency", "GetRecords.Records", "GetRecords.Success", "IncomingBytes", "IncomingRecords", "IteratorAgeMilliseconds", "OutgoingBytes", "OutgoingRecords", "PutRecord.Bytes", "PutRecord.Latency", "PutRecord.Success", "PutRecords.Bytes", "PutRecords.Latency", "PutRecords.Records", "PutRecords.Success", "ReadProvisionedThroughputExceeded", "SubscribeToShard.RateExceeded", "SubscribeToShard.Success", "SubscribeToShardEvent.Bytes", "SubscribeToShardEvent.MillisBehindLatest", "SubscribeToShardEvent.Records", "SubscribeToShardEvent.Success", "WriteProvisionedThroughputExceeded"}, "AWS/KinesisAnalytics": {"Bytes", "InputProcessing.DroppedRecords", "InputProcessing.Duration", "InputProcessing.OkBytes", "InputProcessing.OkRecords", "InputProcessing.ProcessingFailedRecords", "InputProcessing.Success", "KPUs", "LambdaDelivery.DeliveryFailedRecords", "LambdaDelivery.Duration", "LambdaDelivery.OkRecords", "MillisBehindLatest", "Records", "Success"}, From d54141661502efdcfef288f43bb04d3e5e1c5ce0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 19 Feb 2024 13:16:52 +0200 Subject: [PATCH 0012/1406] Update `make docs` procedure (#83045) Co-authored-by: grafanabot --- docs/make-docs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/docs/make-docs b/docs/make-docs index 81616c5af0..756e33b62f 100755 --- a/docs/make-docs +++ b/docs/make-docs @@ -6,6 +6,12 @@ # [Semantic versioning](https://semver.org/) is used to help the reader identify the significance of changes. # Changes are relevant to this script and the support docs.mk GNU Make interface. # +# ## 6.0.0 (2024-02-16) +# +# ### Changed +# +# - Require `jq` for human readable `make doc-validator` output. +# # ## 5.4.0 (2024-02-12) # # ### Changed @@ -702,6 +708,13 @@ POSIX_HERESTRING case "${image}" in 'grafana/doc-validator') + if ! command -v jq >/dev/null 2>&1; then + errr '`jq` must be installed for the `doc-validator` target to work.' + note 'To install `jq`, refer to https://jqlang.github.io/jq/download/,' + + exit 1 + fi + proj="$(new_proj "$1")" printf '\r\n' "${PODMAN}" run \ @@ -714,8 +727,10 @@ case "${image}" in "${DOCS_IMAGE}" \ "--include=${DOC_VALIDATOR_INCLUDE}" \ "--skip-checks=${DOC_VALIDATOR_SKIP_CHECKS}" \ - /hugo/content/docs \ - "$(proj_canonical "${proj}")" | sed "s#$(proj_dst "${proj}")#sources#" + "/hugo/content$(proj_canonical "${proj}")" \ + "$(proj_canonical "${proj}")" \ + | sed "s#$(proj_dst "${proj}")#sources#" \ + | jq -r '"ERROR: \(.location.path):\(.location.range.start.line // 1):\(.location.range.start.column // 1): \(.message)" + if .suggestions[0].text then "\nSuggestion: \(.suggestions[0].text)" else "" end' ;; 'grafana/vale') proj="$(new_proj "$1")" From 72c8ae2d8a85dcdd493641b9adcff415fba1f01b Mon Sep 17 00:00:00 2001 From: Jack Baldry Date: Mon, 19 Feb 2024 11:21:51 +0000 Subject: [PATCH 0013/1406] Remove duplicate paragraph and wrap in note (#82575) --- .../alerting-rules/create-grafana-managed-rule.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md b/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md index 4f691345da..1d43355576 100644 --- a/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md +++ b/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md @@ -111,11 +111,11 @@ To do this, you need to make sure that your alert rule is in the right evaluatio 1. Turn on pause alert notifications, if required. - **Note**: - - Pause alert rule evaluation to prevent noisy alerting while tuning your alerts. Pausing stops alert rule evaluation and does not create any alert instances. This is different to mute timings, which stop notifications from being delivered, but still allow for alert rule evaluation and the creation of alert instances. - - You can pause alert rule evaluation to prevent noisy alerting while tuning your alerts. Pausing stops alert rule evaluation and does not create any alert instances. This is different to mute timings, which stop notifications from being delivered, but still allow for alert rule evaluation and the creation of alert instances. + {{< admonition type="note" >}} + You can pause alert rule evaluation to prevent noisy alerting while tuning your alerts. + Pausing stops alert rule evaluation and doesn't create any alert instances. + This is different to mute timings, which stop notifications from being delivered, but still allows for alert rule evaluation and the creation of alert instances. + {{< /admonition >}} 1. In **Configure no data and error handling**, configure alerting behavior in the absence of data. From b4a77937fd962314cd6222e29e01839cac8f9794 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 19 Feb 2024 11:12:24 +0000 Subject: [PATCH 0014/1406] Update dependency @react-types/button to v3.9.2 --- package.json | 2 +- yarn.lock | 15 ++------------- 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index 56581b1195..8ee2c17f2d 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,7 @@ "@grafana/eslint-plugin": "link:./packages/grafana-eslint-rules", "@grafana/tsconfig": "^1.3.0-rc1", "@pmmmwh/react-refresh-webpack-plugin": "0.5.11", - "@react-types/button": "3.9.1", + "@react-types/button": "3.9.2", "@react-types/menu": "3.9.6", "@react-types/overlays": "3.8.4", "@react-types/shared": "3.22.0", diff --git a/yarn.lock b/yarn.lock index 3b4ca5c400..b67e5f305b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6868,18 +6868,7 @@ __metadata: languageName: node linkType: hard -"@react-types/button@npm:3.9.1": - version: 3.9.1 - resolution: "@react-types/button@npm:3.9.1" - dependencies: - "@react-types/shared": "npm:^3.22.0" - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 - checksum: 10/712eb4dbd3bf1afe96a4d1438c95e54b44117bb1cef31a1984d678ef716672d47ebbbbbb1736a2083e47e435e652aa70384f4cf9e9e302d7f1d0878d176ace68 - languageName: node - linkType: hard - -"@react-types/button@npm:^3.9.2": +"@react-types/button@npm:3.9.2, @react-types/button@npm:^3.9.2": version: 3.9.2 resolution: "@react-types/button@npm:3.9.2" dependencies: @@ -18332,7 +18321,7 @@ __metadata: "@react-aria/utils": "npm:3.23.2" "@react-awesome-query-builder/core": "npm:6.4.2" "@react-awesome-query-builder/ui": "npm:6.4.2" - "@react-types/button": "npm:3.9.1" + "@react-types/button": "npm:3.9.2" "@react-types/menu": "npm:3.9.6" "@react-types/overlays": "npm:3.8.4" "@react-types/shared": "npm:3.22.0" From 6ca26dd043573673cb491c3f362a0ead0ab7870e Mon Sep 17 00:00:00 2001 From: Tobias Skarhed <1438972+tskarhed@users.noreply.github.com> Date: Mon, 19 Feb 2024 13:49:34 +0100 Subject: [PATCH 0015/1406] Grafana UI: Add code variant to Text component (#82318) * Text: Add code variant * Add TextLinkVariants * Add don't --- packages/grafana-data/src/themes/createTypography.ts | 2 ++ .../grafana-ui/src/components/Link/TextLink.story.tsx | 5 ++++- packages/grafana-ui/src/components/Link/TextLink.tsx | 8 +++++--- packages/grafana-ui/src/components/Text/Text.mdx | 1 + packages/grafana-ui/src/components/Text/Text.story.tsx | 5 ++++- 5 files changed, 16 insertions(+), 5 deletions(-) diff --git a/packages/grafana-data/src/themes/createTypography.ts b/packages/grafana-data/src/themes/createTypography.ts index 1473adc2bb..3f11990c0e 100644 --- a/packages/grafana-data/src/themes/createTypography.ts +++ b/packages/grafana-data/src/themes/createTypography.ts @@ -114,6 +114,7 @@ export function createTypography(colors: ThemeColors, typographyInput: ThemeTypo h6: buildVariant(fontWeightMedium, 14, 22, 0.15), body: buildVariant(fontWeightRegular, fontSize, 22, 0.15), bodySmall: buildVariant(fontWeightRegular, 12, 18, 0.15), + code: { ...buildVariant(fontWeightRegular, 14, 16, 0.15), fontFamily: fontFamilyMonospace }, }; const size = { @@ -152,4 +153,5 @@ export interface ThemeTypographyVariantTypes { h6: ThemeTypographyVariant; body: ThemeTypographyVariant; bodySmall: ThemeTypographyVariant; + code: ThemeTypographyVariant; } diff --git a/packages/grafana-ui/src/components/Link/TextLink.story.tsx b/packages/grafana-ui/src/components/Link/TextLink.story.tsx index a4dcdc74a3..354a9d7ae0 100644 --- a/packages/grafana-ui/src/components/Link/TextLink.story.tsx +++ b/packages/grafana-ui/src/components/Link/TextLink.story.tsx @@ -18,7 +18,10 @@ const meta: Meta = { controls: { exclude: ['href', 'external'] }, }, argTypes: { - variant: { control: 'select', options: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'body', 'bodySmall', undefined] }, + variant: { + control: 'select', + options: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'body', 'bodySmall', undefined], + }, weight: { control: 'select', options: ['bold', 'medium', 'light', 'regular', undefined], diff --git a/packages/grafana-ui/src/components/Link/TextLink.tsx b/packages/grafana-ui/src/components/Link/TextLink.tsx index 2a50bc9e2d..2d5a253bee 100644 --- a/packages/grafana-ui/src/components/Link/TextLink.tsx +++ b/packages/grafana-ui/src/components/Link/TextLink.tsx @@ -10,6 +10,8 @@ import { customWeight } from '../Text/utils'; import { Link } from './Link'; +type TextLinkVariants = keyof Omit; + interface TextLinkProps extends Omit, 'target' | 'rel'> { /** url to which redirect the user, external or internal */ href: string; @@ -19,8 +21,8 @@ interface TextLinkProps extends Omit, 't external?: boolean; /** True when the link will be displayed inline with surrounding text, false if it will be displayed as a block. Depending on this prop correspondant default styles will be applied */ inline?: boolean; - /** The default variant is 'body'. To fit another styles set the correspondent variant as it is necessary also to adjust the icon size */ - variant?: keyof ThemeTypographyVariantTypes; + /** The default variant is 'body'. To fit another styles set the correspondent variant as it is necessary also to adjust the icon size. `code` is excluded, as it is not fit for links. */ + variant?: TextLinkVariants; /** Override the default weight for the used variant */ weight?: 'light' | 'regular' | 'medium' | 'bold'; /** Set the icon to be shown. An external link will show the 'external-link-alt' icon as default.*/ @@ -29,7 +31,7 @@ interface TextLinkProps extends Omit, 't } const svgSizes: { - [key in keyof ThemeTypographyVariantTypes]: IconSize; + [key in TextLinkVariants]: IconSize; } = { h1: 'xl', h2: 'xl', diff --git a/packages/grafana-ui/src/components/Text/Text.mdx b/packages/grafana-ui/src/components/Text/Text.mdx index 8d35135973..30b148afc3 100644 --- a/packages/grafana-ui/src/components/Text/Text.mdx +++ b/packages/grafana-ui/src/components/Text/Text.mdx @@ -47,6 +47,7 @@ In this documentation you can find: - Do not use the `element` prop because of its appearance, use it to organize the structure of the page. - Do not use color for emphasis as colors are related to states such as `error`, `success`, `disabled` and so on. +- Do not use the `code` variant for anything other than code snippets.

diff --git a/packages/grafana-ui/src/components/Text/Text.story.tsx b/packages/grafana-ui/src/components/Text/Text.story.tsx index 01988abef6..0ab0427e8c 100644 --- a/packages/grafana-ui/src/components/Text/Text.story.tsx +++ b/packages/grafana-ui/src/components/Text/Text.story.tsx @@ -16,7 +16,10 @@ const meta: Meta = { }, }, argTypes: { - variant: { control: 'select', options: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'body', 'bodySmall', undefined] }, + variant: { + control: 'select', + options: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'body', 'bodySmall', 'code', undefined], + }, weight: { control: 'select', options: ['bold', 'medium', 'light', 'regular', undefined], From d90774e8c23339db0df785ba414f43e63780f3ee Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 19 Feb 2024 12:18:04 +0000 Subject: [PATCH 0016/1406] Update dependency @react-types/menu to v3.9.7 --- package.json | 2 +- yarn.lock | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 8ee2c17f2d..56caeb9dfe 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@ "@grafana/tsconfig": "^1.3.0-rc1", "@pmmmwh/react-refresh-webpack-plugin": "0.5.11", "@react-types/button": "3.9.2", - "@react-types/menu": "3.9.6", + "@react-types/menu": "3.9.7", "@react-types/overlays": "3.8.4", "@react-types/shared": "3.22.0", "@rtsao/plugin-proposal-class-properties": "7.0.1-patch.1", diff --git a/yarn.lock b/yarn.lock index b67e5f305b..a169b4be33 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6891,15 +6891,15 @@ __metadata: languageName: node linkType: hard -"@react-types/menu@npm:3.9.6": - version: 3.9.6 - resolution: "@react-types/menu@npm:3.9.6" +"@react-types/menu@npm:3.9.7": + version: 3.9.7 + resolution: "@react-types/menu@npm:3.9.7" dependencies: - "@react-types/overlays": "npm:^3.8.4" - "@react-types/shared": "npm:^3.22.0" + "@react-types/overlays": "npm:^3.8.5" + "@react-types/shared": "npm:^3.22.1" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 - checksum: 10/14812700f810a75a1b6f85077c35292c50783fbaf404d6ee3ab21ca73151cb95f527824e45cb4c0dd02ce5b99d5a4eba21f78da53093000a003e848b06690b86 + checksum: 10/97cec66432e6c53909dab25d9a7d5c2646d484caeb6c4eff402f152baf667079ef5774c31098f29d66045a1b0f841b0cd579aaa1948353631739ddf61042a0e7 languageName: node linkType: hard @@ -6914,7 +6914,7 @@ __metadata: languageName: node linkType: hard -"@react-types/overlays@npm:^3.8.4, @react-types/overlays@npm:^3.8.5": +"@react-types/overlays@npm:^3.8.5": version: 3.8.5 resolution: "@react-types/overlays@npm:3.8.5" dependencies: @@ -18322,7 +18322,7 @@ __metadata: "@react-awesome-query-builder/core": "npm:6.4.2" "@react-awesome-query-builder/ui": "npm:6.4.2" "@react-types/button": "npm:3.9.2" - "@react-types/menu": "npm:3.9.6" + "@react-types/menu": "npm:3.9.7" "@react-types/overlays": "npm:3.8.4" "@react-types/shared": "npm:3.22.0" "@reduxjs/toolkit": "npm:1.9.5" From 2768c92519870b7dc870423641e5dd45d89cbe34 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 19 Feb 2024 12:59:21 +0000 Subject: [PATCH 0017/1406] Update dependency @react-types/overlays to v3.8.5 --- package.json | 2 +- yarn.lock | 17 +++-------------- 2 files changed, 4 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index 56caeb9dfe..64f064cbcf 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,7 @@ "@pmmmwh/react-refresh-webpack-plugin": "0.5.11", "@react-types/button": "3.9.2", "@react-types/menu": "3.9.7", - "@react-types/overlays": "3.8.4", + "@react-types/overlays": "3.8.5", "@react-types/shared": "3.22.0", "@rtsao/plugin-proposal-class-properties": "7.0.1-patch.1", "@swc/core": "1.4.1", diff --git a/yarn.lock b/yarn.lock index a169b4be33..3dc292d7aa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6903,18 +6903,7 @@ __metadata: languageName: node linkType: hard -"@react-types/overlays@npm:3.8.4": - version: 3.8.4 - resolution: "@react-types/overlays@npm:3.8.4" - dependencies: - "@react-types/shared": "npm:^3.22.0" - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 - checksum: 10/14b7ab6b24d322c37fc23571dacb2cba7a144902f7421f65c9809029a573e40144ad4db1a583d807e52d4f4e40f1a8785eca303d1a79edf9bb390bb482a5707f - languageName: node - linkType: hard - -"@react-types/overlays@npm:^3.8.5": +"@react-types/overlays@npm:3.8.5, @react-types/overlays@npm:^3.8.5": version: 3.8.5 resolution: "@react-types/overlays@npm:3.8.5" dependencies: @@ -6934,7 +6923,7 @@ __metadata: languageName: node linkType: hard -"@react-types/shared@npm:^3.22.0, @react-types/shared@npm:^3.22.1": +"@react-types/shared@npm:^3.22.1": version: 3.22.1 resolution: "@react-types/shared@npm:3.22.1" peerDependencies: @@ -18323,7 +18312,7 @@ __metadata: "@react-awesome-query-builder/ui": "npm:6.4.2" "@react-types/button": "npm:3.9.2" "@react-types/menu": "npm:3.9.7" - "@react-types/overlays": "npm:3.8.4" + "@react-types/overlays": "npm:3.8.5" "@react-types/shared": "npm:3.22.0" "@reduxjs/toolkit": "npm:1.9.5" "@remix-run/router": "npm:^1.5.0" From 1f980289623597a6f0493a4ff40bae5fa04cdbc8 Mon Sep 17 00:00:00 2001 From: ismail simsek Date: Mon, 19 Feb 2024 15:12:46 +0100 Subject: [PATCH 0018/1406] Chore: Bump grafana-plugin-sdk-go version to v0.212.0 (#83064) bump version v0.212.0 --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index afbd6c7367..7cb945f126 100644 --- a/go.mod +++ b/go.mod @@ -63,7 +63,7 @@ require ( github.com/grafana/cuetsy v0.1.11 // @grafana/grafana-as-code github.com/grafana/grafana-aws-sdk v0.23.1 // @grafana/aws-datasources github.com/grafana/grafana-azure-sdk-go v1.12.0 // @grafana/partner-datasources - github.com/grafana/grafana-plugin-sdk-go v0.211.0 // @grafana/plugins-platform-backend + github.com/grafana/grafana-plugin-sdk-go v0.212.0 // @grafana/plugins-platform-backend github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // @grafana/backend-platform github.com/hashicorp/go-hclog v1.6.2 // @grafana/plugins-platform-backend github.com/hashicorp/go-plugin v1.6.0 // @grafana/plugins-platform-backend diff --git a/go.sum b/go.sum index cc8f434506..af3d2952d8 100644 --- a/go.sum +++ b/go.sum @@ -2532,6 +2532,8 @@ github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79/ github.com/grafana/grafana-plugin-sdk-go v0.114.0/go.mod h1:D7x3ah+1d4phNXpbnOaxa/osSaZlwh9/ZUnGGzegRbk= github.com/grafana/grafana-plugin-sdk-go v0.211.0 h1:hYtieOoYvsv/BcFbtspml4OzfuYrv1d14nESdf13qxQ= github.com/grafana/grafana-plugin-sdk-go v0.211.0/go.mod h1:qsI4ktDf0lig74u8SLPJf9zRdVxWV/W4Wi+Ox6gifgs= +github.com/grafana/grafana-plugin-sdk-go v0.212.0 h1:ohgMktFAasLTzAhKhcIzk81O60E29Za6ly02GhEqGIU= +github.com/grafana/grafana-plugin-sdk-go v0.212.0/go.mod h1:qsI4ktDf0lig74u8SLPJf9zRdVxWV/W4Wi+Ox6gifgs= github.com/grafana/kindsys v0.0.0-20230508162304-452481b63482 h1:1YNoeIhii4UIIQpCPU+EXidnqf449d0C3ZntAEt4KSo= github.com/grafana/kindsys v0.0.0-20230508162304-452481b63482/go.mod h1:GNcfpy5+SY6RVbNGQW264gC0r336Dm+0zgQ5vt6+M8Y= github.com/grafana/prometheus-alertmanager v0.25.1-0.20240208102907-e82436ce63e6 h1:CBm0rwLCPDyarg9/bHJ50rBLYmyMDoyCWpgRMITZhdA= From b56f6ed0dcdc8a36e5dc2d98489f118cfa8bf81c Mon Sep 17 00:00:00 2001 From: Oscar Kilhed Date: Mon, 19 Feb 2024 15:25:45 +0100 Subject: [PATCH 0019/1406] Dashboard scenes: Fixes inspect library panels (#82879) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * working except for links * Make sure the links are present on the library panels * add tests, add empty links before panel is loaded, refactor legacy representation * Update --------- Co-authored-by: Torkel Ödegaard --- .../inspect/InspectJsonTab.test.tsx | 74 ++++++++++++++++++- .../inspect/InspectJsonTab.tsx | 29 +++++++- .../dashboard-scene/scene/LibraryVizPanel.tsx | 56 +++++++++----- .../transformSceneToSaveModel.ts | 15 +++- 4 files changed, 151 insertions(+), 23 deletions(-) diff --git a/public/app/features/dashboard-scene/inspect/InspectJsonTab.test.tsx b/public/app/features/dashboard-scene/inspect/InspectJsonTab.test.tsx index 903ce6dadd..0ba2851eae 100644 --- a/public/app/features/dashboard-scene/inspect/InspectJsonTab.test.tsx +++ b/public/app/features/dashboard-scene/inspect/InspectJsonTab.test.tsx @@ -9,10 +9,13 @@ import { SceneGridLayout, VizPanel, } from '@grafana/scenes'; +import * as libpanels from 'app/features/library-panels/state/api'; import { getStandardTransformers } from 'app/features/transformers/standardTransformers'; import { DashboardScene } from '../scene/DashboardScene'; +import { LibraryVizPanel } from '../scene/LibraryVizPanel'; import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks'; +import { vizPanelToPanel } from '../serialization/transformSceneToSaveModel'; import { activateFullSceneTree } from '../utils/test-utils'; import { findVizPanelByKey } from '../utils/utils'; @@ -25,6 +28,14 @@ setPluginImportUtils({ getPanelPluginFromCache: (id: string) => undefined, }); +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + setPluginExtensionGetter: jest.fn(), + getPluginLinkExtensions: jest.fn(() => ({ + extensions: [], + })), +})); + describe('InspectJsonTab', () => { it('Can show panel json', async () => { const { tab } = await buildTestScene(); @@ -34,6 +45,15 @@ describe('InspectJsonTab', () => { expect(tab.isEditable()).toBe(true); }); + it('Can show panel json for library panels', async () => { + const { tab } = await buildTestSceneWithLibraryPanel(); + + const obj = JSON.parse(tab.state.jsonText); + expect(obj.gridPos).toEqual({ x: 0, y: 0, w: 10, h: 12 }); + expect(obj.type).toEqual('table'); + expect(tab.isEditable()).toBe(false); + }); + it('Can show panel data with field config', async () => { const { tab } = await buildTestScene(); tab.onChangeSource({ value: 'panel-data' }); @@ -86,8 +106,8 @@ describe('InspectJsonTab', () => { }); }); -async function buildTestScene() { - const panel = new VizPanel({ +function buildTestPanel() { + return new VizPanel({ title: 'Panel A', pluginId: 'table', key: 'panel-12', @@ -129,7 +149,10 @@ async function buildTestScene() { }), }), }); +} +async function buildTestScene() { + const panel = buildTestPanel(); const scene = new DashboardScene({ title: 'hello', uid: 'dash-1', @@ -161,3 +184,50 @@ async function buildTestScene() { return { scene, tab, panel }; } + +async function buildTestSceneWithLibraryPanel() { + const panel = vizPanelToPanel(buildTestPanel()); + + const libraryPanelState = { + name: 'LibraryPanel A', + title: 'LibraryPanel A title', + uid: '111', + key: 'panel-22', + model: panel, + version: 1, + }; + + jest.spyOn(libpanels, 'getLibraryPanel').mockResolvedValue({ ...libraryPanelState, ...panel }); + const libraryPanel = new LibraryVizPanel(libraryPanelState); + + const scene = new DashboardScene({ + title: 'hello', + uid: 'dash-1', + meta: { + canEdit: true, + }, + body: new SceneGridLayout({ + children: [ + new SceneGridItem({ + key: 'griditem-1', + x: 0, + y: 0, + width: 10, + height: 12, + body: libraryPanel, + }), + ], + }), + }); + + activateFullSceneTree(scene); + + await new Promise((r) => setTimeout(r, 1)); + + const tab = new InspectJsonTab({ + panelRef: libraryPanel.state.panel!.getRef(), + onClose: jest.fn(), + }); + + return { scene, tab, panel }; +} diff --git a/public/app/features/dashboard-scene/inspect/InspectJsonTab.tsx b/public/app/features/dashboard-scene/inspect/InspectJsonTab.tsx index 0b3d2ac23c..f8abf99f08 100644 --- a/public/app/features/dashboard-scene/inspect/InspectJsonTab.tsx +++ b/public/app/features/dashboard-scene/inspect/InspectJsonTab.tsx @@ -26,9 +26,10 @@ import { InspectTab } from 'app/features/inspector/types'; import { getPrettyJSON } from 'app/features/inspector/utils/utils'; import { reportPanelInspectInteraction } from 'app/features/search/page/reporting'; +import { LibraryVizPanel } from '../scene/LibraryVizPanel'; import { PanelRepeaterGridItem } from '../scene/PanelRepeaterGridItem'; import { buildGridItemForPanel } from '../serialization/transformSaveModelToScene'; -import { gridItemToPanel } from '../serialization/transformSceneToSaveModel'; +import { gridItemToPanel, vizPanelToPanel } from '../serialization/transformSceneToSaveModel'; import { getDashboardSceneFor, getPanelIdForVizPanel, getQueryRunnerFor } from '../utils/utils'; export type ShowContent = 'panel-json' | 'panel-data' | 'data-frames'; @@ -202,6 +203,8 @@ function getJsonText(show: ShowContent, panel: VizPanel): string { if (panel.parent instanceof SceneGridItem || panel.parent instanceof PanelRepeaterGridItem) { objToStringify = gridItemToPanel(panel.parent); + } else if (panel.parent instanceof LibraryVizPanel) { + objToStringify = libraryPanelChildToLegacyRepresentation(panel); } break; } @@ -234,6 +237,30 @@ function getJsonText(show: ShowContent, panel: VizPanel): string { return getPrettyJSON(objToStringify); } +/** + * + * @param panel Must be child of a LibraryVizPanel that is in turn the child of a SceneGridItem + * @returns object representation of the legacy library panel structure. + */ +function libraryPanelChildToLegacyRepresentation(panel: VizPanel<{}, {}>) { + if (!(panel.parent instanceof LibraryVizPanel)) { + throw 'Panel not child of LibraryVizPanel'; + } + if (!(panel.parent.parent instanceof SceneGridItem)) { + throw 'LibraryPanel not child of SceneGridItem'; + } + const gridItem = panel.parent.parent; + const libraryPanelObj = gridItemToPanel(gridItem); + const panelObj = vizPanelToPanel(panel); + panelObj.gridPos = { + x: gridItem.state.x || 0, + y: gridItem.state.y || 0, + h: gridItem.state.height || 0, + w: gridItem.state.width || 0, + }; + return { ...libraryPanelObj, ...panelObj }; +} + function hasGridPosChanged(a: SceneGridItemStateLike, b: SceneGridItemStateLike) { return a.x !== b.x || a.y !== b.y || a.width !== b.width || a.height !== b.height; } diff --git a/public/app/features/dashboard-scene/scene/LibraryVizPanel.tsx b/public/app/features/dashboard-scene/scene/LibraryVizPanel.tsx index c7ae879fe6..61beed8fff 100644 --- a/public/app/features/dashboard-scene/scene/LibraryVizPanel.tsx +++ b/public/app/features/dashboard-scene/scene/LibraryVizPanel.tsx @@ -6,31 +6,26 @@ import { getLibraryPanel } from 'app/features/library-panels/state/api'; import { createPanelDataProvider } from '../utils/createPanelDataProvider'; -import { panelMenuBehavior } from './PanelMenuBehavior'; +import { VizPanelLinks, VizPanelLinksMenu } from './PanelLinks'; +import { panelLinksBehavior, panelMenuBehavior } from './PanelMenuBehavior'; +import { PanelNotices } from './PanelNotices'; interface LibraryVizPanelState extends SceneObjectState { // Library panels use title from dashboard JSON's panel model, not from library panel definition, hence we pass it. title: string; uid: string; name: string; - panel: VizPanel; + panel?: VizPanel; + _loadedVersion?: number; } export class LibraryVizPanel extends SceneObjectBase { static Component = LibraryPanelRenderer; - constructor({ uid, title, key, name }: Pick) { + constructor(state: LibraryVizPanelState) { super({ - uid, - title, - key, - name, - panel: new VizPanel({ - title, - menu: new VizPanelMenu({ - $behaviors: [panelMenuBehavior], - }), - }), + panel: state.panel ?? getLoadingPanel(state.title), + ...state, }); this.addActivationHandler(this._onActivate); @@ -41,29 +36,54 @@ export class LibraryVizPanel extends SceneObjectBase { }; private async loadLibraryPanelFromPanelModel() { - let vizPanel = this.state.panel; + let vizPanel = this.state.panel!; + try { const libPanel = await getLibraryPanel(this.state.uid, true); + + if (this.state._loadedVersion === libPanel.version) { + return; + } + const libPanelModel = new PanelModel(libPanel.model); - vizPanel = vizPanel.clone({ + + const panel = new VizPanel({ + title: this.state.title, options: libPanelModel.options ?? {}, fieldConfig: libPanelModel.fieldConfig, + pluginId: libPanelModel.type, pluginVersion: libPanelModel.pluginVersion, displayMode: libPanelModel.transparent ? 'transparent' : undefined, description: libPanelModel.description, - pluginId: libPanel.type, $data: createPanelDataProvider(libPanelModel), + menu: new VizPanelMenu({ $behaviors: [panelMenuBehavior] }), + titleItems: [ + new VizPanelLinks({ + rawLinks: libPanelModel.links, + menu: new VizPanelLinksMenu({ $behaviors: [panelLinksBehavior] }), + }), + new PanelNotices(), + ], }); + + this.setState({ panel, _loadedVersion: libPanel.version }); } catch (err) { vizPanel.setState({ _pluginLoadError: 'Unable to load library panel: ' + this.state.uid, }); } - - this.setState({ panel: vizPanel }); } } +function getLoadingPanel(title: string) { + return new VizPanel({ + title, + menu: new VizPanelMenu({ + $behaviors: [panelMenuBehavior], + }), + }); +} + function LibraryPanelRenderer({ model }: SceneComponentProps) { const { panel } = model.useState(); diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts index e6301ac4ef..b47bd847fd 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts @@ -22,6 +22,7 @@ import { defaultDashboard, defaultTimePickerConfig, FieldConfigSource, + GridPos, Panel, RowPanel, TimePickerConfig, @@ -200,11 +201,22 @@ export function gridItemToPanel(gridItem: SceneGridItemLike, isSnapshot = false) throw new Error('Unsupported grid item type'); } + const panel: Panel = vizPanelToPanel(vizPanel, { x, y, h, w }, isSnapshot, gridItem); + + return panel; +} + +export function vizPanelToPanel( + vizPanel: VizPanel, + gridPos?: GridPos, + isSnapshot = false, + gridItem?: SceneGridItemLike +) { const panel: Panel = { id: getPanelIdForVizPanel(vizPanel), type: vizPanel.state.pluginId, title: vizPanel.state.title, - gridPos: { x, y, w, h }, + gridPos, options: vizPanel.state.options, fieldConfig: (vizPanel.state.fieldConfig as FieldConfigSource) ?? { defaults: {}, overrides: [] }, transformations: [], @@ -241,7 +253,6 @@ export function gridItemToPanel(gridItem: SceneGridItemLike, isSnapshot = false) if (!panel.transparent) { delete panel.transparent; } - return panel; } From 87ab98ea95ddddf647b73e557b79d0f2d6e944b8 Mon Sep 17 00:00:00 2001 From: Matthew Jacobson Date: Mon, 19 Feb 2024 10:30:13 -0500 Subject: [PATCH 0020/1406] Alerting: Fix panic in provisioning filter contacts by unknown name (#83070) --- pkg/services/ngalert/provisioning/contactpoints.go | 2 +- pkg/services/ngalert/provisioning/contactpoints_test.go | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/pkg/services/ngalert/provisioning/contactpoints.go b/pkg/services/ngalert/provisioning/contactpoints.go index c9886e97cb..37a3c03925 100644 --- a/pkg/services/ngalert/provisioning/contactpoints.go +++ b/pkg/services/ngalert/provisioning/contactpoints.go @@ -80,7 +80,7 @@ func (ecp *ContactPointService) GetContactPoints(ctx context.Context, q ContactP return nil, convertRecSvcErr(err) } grafanaReceivers := []*apimodels.GettableGrafanaReceiver{} - if q.Name != "" { + if q.Name != "" && len(res) > 0 { grafanaReceivers = res[0].GettableGrafanaReceivers.GrafanaManagedReceivers // we only expect one receiver group } else { for _, r := range res { diff --git a/pkg/services/ngalert/provisioning/contactpoints_test.go b/pkg/services/ngalert/provisioning/contactpoints_test.go index 995b9bb6c4..12081f1ba5 100644 --- a/pkg/services/ngalert/provisioning/contactpoints_test.go +++ b/pkg/services/ngalert/provisioning/contactpoints_test.go @@ -53,6 +53,15 @@ func TestContactPointService(t *testing.T) { require.Equal(t, "slack receiver", cps[0].Name) }) + t.Run("service filters contact points by name, returns empty when no match", func(t *testing.T) { + sut := createContactPointServiceSut(t, secretsService) + + cps, err := sut.GetContactPoints(context.Background(), cpsQueryWithName(1, "unknown"), nil) + require.NoError(t, err) + + require.Len(t, cps, 0) + }) + t.Run("service stitches contact point into org's AM config", func(t *testing.T) { sut := createContactPointServiceSut(t, secretsService) newCp := createTestContactPoint() From 57499845c25d927fb239016c4a205fe31f983c14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Farkas?= Date: Tue, 20 Feb 2024 08:41:11 +0100 Subject: [PATCH 0021/1406] envvars: improve tests (#83071) * envvars: improve tests * removed commented-out code Co-authored-by: Will Browne --------- Co-authored-by: Will Browne --- pkg/plugins/envvars/envvars_test.go | 56 +++++++++++++++++++---------- 1 file changed, 37 insertions(+), 19 deletions(-) diff --git a/pkg/plugins/envvars/envvars_test.go b/pkg/plugins/envvars/envvars_test.go index f782ddefba..2fb0b52aca 100644 --- a/pkg/plugins/envvars/envvars_test.go +++ b/pkg/plugins/envvars/envvars_test.go @@ -751,7 +751,7 @@ func TestService_GetConfigMap(t *testing.T) { s := &Service{ cfg: tc.cfg, } - require.Equal(t, tc.expected, s.GetConfigMap(context.Background(), "", nil)) + require.Subset(t, s.GetConfigMap(context.Background(), "", nil), tc.expected) }) } } @@ -790,7 +790,7 @@ func TestService_GetConfigMap_featureToggles(t *testing.T) { Features: tc.features, }, } - require.Equal(t, tc.expectedConfig, s.GetConfigMap(context.Background(), "", nil)) + require.Subset(t, s.GetConfigMap(context.Background(), "", nil), tc.expectedConfig) } }) } @@ -802,7 +802,7 @@ func TestService_GetConfigMap_appURL(t *testing.T) { GrafanaAppURL: "https://myorg.com/", }, } - require.Equal(t, map[string]string{"GF_APP_URL": "https://myorg.com/"}, s.GetConfigMap(context.Background(), "", nil)) + require.Subset(t, s.GetConfigMap(context.Background(), "", nil), map[string]string{"GF_APP_URL": "https://myorg.com/"}) }) } @@ -813,14 +813,14 @@ func TestService_GetConfigMap_concurrentQueryCount(t *testing.T) { ConcurrentQueryCount: 42, }, } - require.Equal(t, map[string]string{"GF_CONCURRENT_QUERY_COUNT": "42"}, s.GetConfigMap(context.Background(), "", nil)) + require.Subset(t, s.GetConfigMap(context.Background(), "", nil), map[string]string{"GF_CONCURRENT_QUERY_COUNT": "42"}) }) t.Run("Doesn't set the concurrent query count if it is not in the config", func(t *testing.T) { s := &Service{ cfg: &config.Cfg{}, } - require.Equal(t, map[string]string{}, s.GetConfigMap(context.Background(), "", nil)) + require.NotContains(t, s.GetConfigMap(context.Background(), "", nil), "GF_CONCURRENT_QUERY_COUNT") }) t.Run("Doesn't set the concurrent query count if it is zero", func(t *testing.T) { @@ -829,7 +829,7 @@ func TestService_GetConfigMap_concurrentQueryCount(t *testing.T) { ConcurrentQueryCount: 0, }, } - require.Equal(t, map[string]string{}, s.GetConfigMap(context.Background(), "", nil)) + require.NotContains(t, s.GetConfigMap(context.Background(), "", nil), "GF_CONCURRENT_QUERY_COUNT") }) } @@ -840,14 +840,14 @@ func TestService_GetConfigMap_azureAuthEnabled(t *testing.T) { AzureAuthEnabled: true, }, } - require.Equal(t, map[string]string{"GFAZPL_AZURE_AUTH_ENABLED": "true"}, s.GetConfigMap(context.Background(), "", nil)) + require.Subset(t, s.GetConfigMap(context.Background(), "", nil), map[string]string{"GFAZPL_AZURE_AUTH_ENABLED": "true"}) }) t.Run("Doesn't set the azureAuthEnabled if it is not in the config", func(t *testing.T) { s := &Service{ cfg: &config.Cfg{}, } - require.Equal(t, map[string]string{}, s.GetConfigMap(context.Background(), "", nil)) + require.NotContains(t, s.GetConfigMap(context.Background(), "", nil), "GFAZPL_AZURE_AUTH_ENABLED") }) t.Run("Doesn't set the azureAuthEnabled if it is false", func(t *testing.T) { @@ -856,7 +856,7 @@ func TestService_GetConfigMap_azureAuthEnabled(t *testing.T) { AzureAuthEnabled: false, }, } - require.Equal(t, map[string]string{}, s.GetConfigMap(context.Background(), "", nil)) + require.NotContains(t, s.GetConfigMap(context.Background(), "", nil), "GFAZPL_AZURE_AUTH_ENABLED") }) } @@ -887,7 +887,7 @@ func TestService_GetConfigMap_azure(t *testing.T) { Azure: azSettings, }, } - require.Equal(t, map[string]string{ + require.Subset(t, s.GetConfigMap(context.Background(), "grafana-azure-monitor-datasource", nil), map[string]string{ "GFAZPL_AZURE_CLOUD": "AzureCloud", "GFAZPL_MANAGED_IDENTITY_ENABLED": "true", "GFAZPL_MANAGED_IDENTITY_CLIENT_ID": "mock_managed_identity_client_id", "GFAZPL_WORKLOAD_IDENTITY_ENABLED": "true", @@ -899,7 +899,7 @@ func TestService_GetConfigMap_azure(t *testing.T) { "GFAZPL_USER_IDENTITY_CLIENT_ID": "mock_user_identity_client_id", "GFAZPL_USER_IDENTITY_CLIENT_SECRET": "mock_user_identity_client_secret", "GFAZPL_USER_IDENTITY_ASSERTION": "username", - }, s.GetConfigMap(context.Background(), "grafana-azure-monitor-datasource", nil)) + }) }) t.Run("does not use the azure settings for a non-Azure plugin", func(t *testing.T) { @@ -908,7 +908,20 @@ func TestService_GetConfigMap_azure(t *testing.T) { Azure: azSettings, }, } - require.Equal(t, map[string]string{}, s.GetConfigMap(context.Background(), "", nil)) + + m := s.GetConfigMap(context.Background(), "", nil) + require.NotContains(t, m, "GFAZPL_AZURE_CLOUD") + require.NotContains(t, m, "GFAZPL_MANAGED_IDENTITY_ENABLED") + require.NotContains(t, m, "GFAZPL_MANAGED_IDENTITY_CLIENT_ID") + require.NotContains(t, m, "GFAZPL_WORKLOAD_IDENTITY_ENABLED") + require.NotContains(t, m, "GFAZPL_WORKLOAD_IDENTITY_TENANT_ID") + require.NotContains(t, m, "GFAZPL_WORKLOAD_IDENTITY_CLIENT_ID") + require.NotContains(t, m, "GFAZPL_WORKLOAD_IDENTITY_TOKEN_FILE") + require.NotContains(t, m, "GFAZPL_USER_IDENTITY_ENABLED") + require.NotContains(t, m, "GFAZPL_USER_IDENTITY_TOKEN_URL") + require.NotContains(t, m, "GFAZPL_USER_IDENTITY_CLIENT_ID") + require.NotContains(t, m, "GFAZPL_USER_IDENTITY_CLIENT_SECRET") + require.NotContains(t, m, "GFAZPL_USER_IDENTITY_ASSERTION") }) t.Run("uses the azure settings for a non-Azure user-specified plugin", func(t *testing.T) { @@ -918,7 +931,7 @@ func TestService_GetConfigMap_azure(t *testing.T) { Azure: azSettings, }, } - require.Equal(t, map[string]string{ + require.Subset(t, s.GetConfigMap(context.Background(), "test-datasource", nil), map[string]string{ "GFAZPL_AZURE_CLOUD": "AzureCloud", "GFAZPL_MANAGED_IDENTITY_ENABLED": "true", "GFAZPL_MANAGED_IDENTITY_CLIENT_ID": "mock_managed_identity_client_id", "GFAZPL_WORKLOAD_IDENTITY_ENABLED": "true", @@ -930,7 +943,7 @@ func TestService_GetConfigMap_azure(t *testing.T) { "GFAZPL_USER_IDENTITY_CLIENT_ID": "mock_user_identity_client_id", "GFAZPL_USER_IDENTITY_CLIENT_SECRET": "mock_user_identity_client_secret", "GFAZPL_USER_IDENTITY_ASSERTION": "username", - }, s.GetConfigMap(context.Background(), "test-datasource", nil)) + }) }) } @@ -948,20 +961,25 @@ func TestService_GetConfigMap_aws(t *testing.T) { s := &Service{ cfg: cfg, } - require.Equal(t, map[string]string{ + require.Subset(t, s.GetConfigMap(context.Background(), "cloudwatch", nil), map[string]string{ "AWS_AUTH_AssumeRoleEnabled": "false", "AWS_AUTH_AllowedAuthProviders": "grafana_assume_role,keys", "AWS_AUTH_EXTERNAL_ID": "mock_external_id", "AWS_AUTH_SESSION_DURATION": "10m", "AWS_CW_LIST_METRICS_PAGE_LIMIT": "100", - }, s.GetConfigMap(context.Background(), "cloudwatch", nil)) + }) }) t.Run("does not use the aws settings for a non-aws plugin", func(t *testing.T) { s := &Service{ cfg: cfg, } - require.Equal(t, map[string]string{}, s.GetConfigMap(context.Background(), "", nil)) + m := s.GetConfigMap(context.Background(), "", nil) + require.NotContains(t, m, "AWS_AUTH_AssumeRoleEnabled") + require.NotContains(t, m, "AWS_AUTH_AllowedAuthProviders") + require.NotContains(t, m, "AWS_AUTH_EXTERNAL_ID") + require.NotContains(t, m, "AWS_AUTH_SESSION_DURATION") + require.NotContains(t, m, "AWS_CW_LIST_METRICS_PAGE_LIMIT") }) t.Run("uses the aws settings for a non-aws user-specified plugin", func(t *testing.T) { @@ -969,12 +987,12 @@ func TestService_GetConfigMap_aws(t *testing.T) { s := &Service{ cfg: cfg, } - require.Equal(t, map[string]string{ + require.Subset(t, s.GetConfigMap(context.Background(), "test-datasource", nil), map[string]string{ "AWS_AUTH_AssumeRoleEnabled": "false", "AWS_AUTH_AllowedAuthProviders": "grafana_assume_role,keys", "AWS_AUTH_EXTERNAL_ID": "mock_external_id", "AWS_AUTH_SESSION_DURATION": "10m", "AWS_CW_LIST_METRICS_PAGE_LIMIT": "100", - }, s.GetConfigMap(context.Background(), "test-datasource", nil)) + }) }) } From 6db2d1a411693ad7c9a64f9836e0ee966b93c16c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 20 Feb 2024 08:43:02 +0100 Subject: [PATCH 0022/1406] DashboardScene: Simplify controls a bit (#82908) * DashboardScene: Simplify controls a bit * update tests * more test updates * Update * improvements * Fix * Fix merge * Update * update --- .../embedding/EmbeddedDashboard.tsx | 8 +- .../panel-edit/PanelEditorRenderer.tsx | 8 +- .../scene/DashboardControls.tsx | 82 +++++++++++++++---- .../scene/DashboardLinksControls.tsx | 13 +-- .../scene/DashboardScene.test.tsx | 18 +--- .../dashboard-scene/scene/DashboardScene.tsx | 2 +- .../scene/DashboardSceneRenderer.tsx | 32 +------- .../transformSaveModelToScene.test.ts | 26 ++---- .../transformSaveModelToScene.ts | 24 ++---- .../transformSceneToSaveModel.ts | 20 +---- .../settings/DashboardLinksEditView.test.tsx | 23 +----- .../settings/GeneralSettingsEditView.test.tsx | 33 +------- .../settings/GeneralSettingsEditView.tsx | 9 +- ...DashboardModelCompatibilityWrapper.test.ts | 25 ++---- .../DashboardModelCompatibilityWrapper.ts | 5 +- .../utils/dashboardSceneGraph.test.ts | 77 +---------------- .../utils/dashboardSceneGraph.ts | 41 +--------- 17 files changed, 115 insertions(+), 331 deletions(-) diff --git a/public/app/features/dashboard-scene/embedding/EmbeddedDashboard.tsx b/public/app/features/dashboard-scene/embedding/EmbeddedDashboard.tsx index a13376d311..a0e9854173 100644 --- a/public/app/features/dashboard-scene/embedding/EmbeddedDashboard.tsx +++ b/public/app/features/dashboard-scene/embedding/EmbeddedDashboard.tsx @@ -65,13 +65,7 @@ function EmbeddedDashboardRenderer({ model, initialState, onStateChange }: Rende return (
- {controls && ( -
- {controls.map((control) => ( - - ))} -
- )} + {controls && }
diff --git a/public/app/features/dashboard-scene/panel-edit/PanelEditorRenderer.tsx b/public/app/features/dashboard-scene/panel-edit/PanelEditorRenderer.tsx index 6b0bce117f..a464b0a538 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelEditorRenderer.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelEditorRenderer.tsx @@ -37,13 +37,7 @@ export function PanelEditorRenderer({ model }: SceneComponentProps) >
- {controls && ( -
- {controls.map((control) => ( - - ))} -
- )} + {controls && } { static Component = DashboardControlsRenderer; + + public constructor(state: Partial) { + super({ + variableControls: [], + timePicker: state.timePicker ?? new SceneTimePicker({}), + refreshPicker: state.refreshPicker ?? new SceneRefreshPicker({}), + ...state, + }); + } } function DashboardControlsRenderer({ model }: SceneComponentProps) { - const { variableControls, linkControls, timeControls, hideTimeControls } = model.useState(); + const { variableControls, refreshPicker, timePicker, hideTimeControls } = model.useState(); + const dashboard = getDashboardSceneFor(model); + const { links, meta, editPanel } = dashboard.useState(); + const styles = useStyles2(getStyles); + const showDebugger = location.search.includes('scene-debugger'); return ( - +
{variableControls.map((c) => ( ))} - - - - {!hideTimeControls && timeControls.map((c) => )} + {!editPanel && } - + {!hideTimeControls && ( + + + + + )} + {showDebugger && } +
); } + +function getStyles(theme: GrafanaTheme2) { + return { + controls: css({ + display: 'flex', + alignItems: 'flex-start', + gap: theme.spacing(1), + position: 'sticky', + top: 0, + background: theme.colors.background.canvas, + zIndex: theme.zIndex.activePanel, + padding: theme.spacing(2, 0), + width: '100%', + marginLeft: 'auto', + [theme.breakpoints.down('sm')]: { + flexDirection: 'column-reverse', + alignItems: 'stretch', + }, + }), + embedded: css({ + background: 'unset', + position: 'unset', + }), + }; +} diff --git a/public/app/features/dashboard-scene/scene/DashboardLinksControls.tsx b/public/app/features/dashboard-scene/scene/DashboardLinksControls.tsx index d557b4efcc..a535b3517f 100644 --- a/public/app/features/dashboard-scene/scene/DashboardLinksControls.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardLinksControls.tsx @@ -2,7 +2,6 @@ import React from 'react'; import { sanitizeUrl } from '@grafana/data/src/text/sanitize'; import { selectors } from '@grafana/e2e-selectors'; -import { SceneComponentProps, SceneObjectBase, SceneObjectState } from '@grafana/scenes'; import { DashboardLink } from '@grafana/schema'; import { Tooltip } from '@grafana/ui'; import { @@ -12,17 +11,13 @@ import { import { getLinkSrv } from 'app/features/panel/panellinks/link_srv'; import { LINK_ICON_MAP } from '../settings/links/utils'; -import { getDashboardSceneFor } from '../utils/utils'; -interface DashboardLinksControlsState extends SceneObjectState {} - -export class DashboardLinksControls extends SceneObjectBase { - static Component = DashboardLinksControlsRenderer; +export interface Props { + links: DashboardLink[]; + uid?: string; } -function DashboardLinksControlsRenderer({ model }: SceneComponentProps) { - const { links, uid } = getDashboardSceneFor(model).useState(); - +export function DashboardLinksControls({ links, uid }: Props) { if (!links || !uid) { return null; } diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx index 320fa0d7c4..ed36aba044 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx @@ -3,7 +3,6 @@ import { sceneGraph, SceneGridItem, SceneGridLayout, - SceneRefreshPicker, SceneTimeRange, SceneQueryRunner, SceneVariableSet, @@ -22,7 +21,6 @@ import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; import { djb2Hash } from '../utils/djb2Hash'; import { DashboardControls } from './DashboardControls'; -import { DashboardLinksControls } from './DashboardLinksControls'; import { DashboardScene, DashboardSceneState } from './DashboardScene'; jest.mock('../settings/version-history/HistorySrv'); @@ -94,14 +92,14 @@ describe('DashboardScene', () => { }); it('A change to time picker visibility settings should set isDirty true', () => { - const dashboardControls = dashboardSceneGraph.getDashboardControls(scene)!; + const dashboardControls = scene.state.controls!; const prevState = dashboardControls.state.hideTimeControls; dashboardControls.setState({ hideTimeControls: true }); expect(scene.state.isDirty).toBe(true); scene.exitEditMode({ skipConfirm: true }); - expect(dashboardSceneGraph.getDashboardControls(scene)!.state.hideTimeControls).toEqual(prevState); + expect(scene.state.controls!.state.hideTimeControls).toEqual(prevState); }); it('A change to time zone should set isDirty true', () => { @@ -217,17 +215,7 @@ function buildTestScene(overrides?: Partial) { $timeRange: new SceneTimeRange({ timeZone: 'browser', }), - controls: [ - new DashboardControls({ - variableControls: [], - linkControls: new DashboardLinksControls({}), - timeControls: [ - new SceneRefreshPicker({ - intervals: ['1s'], - }), - ], - }), - ], + controls: new DashboardControls({}), body: new SceneGridLayout({ children: [ new SceneGridItem({ diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index 701cdbfc70..e78c453fa8 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -75,7 +75,7 @@ export interface DashboardSceneState extends SceneObjectState { /** NavToolbar actions */ actions?: SceneObject[]; /** Fixed row at the top of the canvas with for example variables and time range controls */ - controls?: SceneObject[]; + controls?: DashboardControls; /** True when editing */ isEditing?: boolean; /** True when user made a change */ diff --git a/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx b/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx index 84950c6c99..cd58d311fd 100644 --- a/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { useLocation } from 'react-router-dom'; import { GrafanaTheme2, PageLayoutType } from '@grafana/data'; -import { SceneComponentProps, SceneDebugger } from '@grafana/scenes'; +import { SceneComponentProps } from '@grafana/scenes'; import { CustomScrollbar, useStyles2 } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; import { getNavModel } from 'app/core/selectors/navModel'; @@ -21,7 +21,6 @@ export function DashboardSceneRenderer({ model }: SceneComponentProps -
{showDebugger && }
- - - ); + const emptyState = ; const withPanels = ( <> - {controls && ( -
- {controls.map((control) => ( - - ))} - {showDebugger && } -
- )} + {controls && }
@@ -88,18 +75,5 @@ function getStyles(theme: GrafanaTheme2) { gap: '8px', marginBottom: theme.spacing(2), }), - - controls: css({ - display: 'flex', - flexWrap: 'wrap', - alignItems: 'center', - gap: theme.spacing(1), - position: 'sticky', - top: 0, - background: theme.colors.background.canvas, - zIndex: theme.zIndex.activePanel, - padding: theme.spacing(2, 0), - marginLeft: 'auto', - }), }; } diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts index 490732bf0e..2567575f3b 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts @@ -26,8 +26,6 @@ import { SceneGridLayout, SceneGridRow, SceneQueryRunner, - SceneRefreshPicker, - SceneTimePicker, VizPanel, } from '@grafana/scenes'; import { @@ -44,7 +42,6 @@ import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard'; import { DASHBOARD_DATASOURCE_PLUGIN_ID } from 'app/plugins/datasource/dashboard/types'; import { DashboardDataDTO } from 'app/types'; -import { DashboardControls } from '../scene/DashboardControls'; import { PanelRepeaterGridItem } from '../scene/PanelRepeaterGridItem'; import { PanelTimeRange } from '../scene/PanelTimeRange'; import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior'; @@ -112,7 +109,7 @@ describe('transformSaveModelToScene', () => { const oldModel = new DashboardModel(dash); const scene = createDashboardSceneFromDashboardModel(oldModel); - const dashboardControls = scene.state.controls![0] as DashboardControls; + const dashboardControls = scene.state.controls!; expect(scene.state.title).toBe('test'); expect(scene.state.uid).toBe('test-uid'); @@ -127,13 +124,8 @@ describe('transformSaveModelToScene', () => { expect(scene.state?.$variables?.getByName('constant')).toBeInstanceOf(ConstantVariable); expect(scene.state?.$variables?.getByName('CoolFilters')).toBeInstanceOf(AdHocFiltersVariable); expect(dashboardControls).toBeDefined(); - expect(dashboardControls).toBeInstanceOf(DashboardControls); - expect(dashboardControls.state.timeControls).toHaveLength(2); - expect(dashboardControls.state.timeControls[0]).toBeInstanceOf(SceneTimePicker); - expect(dashboardControls.state.timeControls[1]).toBeInstanceOf(SceneRefreshPicker); - expect((dashboardControls.state.timeControls[1] as SceneRefreshPicker).state.intervals).toEqual( - defaultTimePickerConfig.refresh_intervals - ); + + expect(dashboardControls.state.refreshPicker.state.intervals).toEqual(defaultTimePickerConfig.refresh_intervals); expect(dashboardControls.state.hideTimeControls).toBe(true); }); @@ -1039,9 +1031,7 @@ describe('transformSaveModelToScene', () => { const scene = transformSaveModelToScene({ dashboard: dashboard_to_load1 as any, meta: {} }); expect(scene.state.$data).toBeInstanceOf(SceneDataLayers); - expect((scene.state.controls![0] as DashboardControls)!.state.variableControls[1]).toBeInstanceOf( - SceneDataLayerControls - ); + expect(scene.state.controls!.state.variableControls[1]).toBeInstanceOf(SceneDataLayerControls); const dataLayers = scene.state.$data as SceneDataLayers; expect(dataLayers.state.layers).toHaveLength(4); @@ -1069,9 +1059,7 @@ describe('transformSaveModelToScene', () => { const scene = transformSaveModelToScene({ dashboard: dashboard_to_load1 as any, meta: {} }); expect(scene.state.$data).toBeInstanceOf(SceneDataLayers); - expect((scene.state.controls![0] as DashboardControls)!.state.variableControls[1]).toBeInstanceOf( - SceneDataLayerControls - ); + expect(scene.state.controls!.state.variableControls[1]).toBeInstanceOf(SceneDataLayerControls); const dataLayers = scene.state.$data as SceneDataLayers; expect(dataLayers.state.layers).toHaveLength(5); @@ -1085,9 +1073,7 @@ describe('transformSaveModelToScene', () => { const scene = transformSaveModelToScene({ dashboard: dashboard_to_load1 as any, meta: {} }); expect(scene.state.$data).toBeInstanceOf(SceneDataLayers); - expect((scene.state.controls![0] as DashboardControls)!.state.variableControls[1]).toBeInstanceOf( - SceneDataLayerControls - ); + expect(scene.state.controls!.state.variableControls[1]).toBeInstanceOf(SceneDataLayerControls); const dataLayers = scene.state.$data as SceneDataLayers; expect(dataLayers.state.layers).toHaveLength(5); diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts index b85744efcf..9b7e0c988c 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts @@ -36,7 +36,6 @@ import { DashboardDTO } from 'app/types'; import { AlertStatesDataLayer } from '../scene/AlertStatesDataLayer'; import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer'; import { DashboardControls } from '../scene/DashboardControls'; -import { DashboardLinksControls } from '../scene/DashboardLinksControls'; import { registerDashboardMacro } from '../scene/DashboardMacro'; import { DashboardScene } from '../scene/DashboardScene'; import { LibraryVizPanel } from '../scene/LibraryVizPanel'; @@ -268,21 +267,16 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel) layers, }) : undefined, - controls: [ - new DashboardControls({ - variableControls: [new VariableValueSelectors({}), new SceneDataLayerControls()], - timeControls: [ - new SceneTimePicker({}), - new SceneRefreshPicker({ - refresh: oldModel.refresh, - intervals: oldModel.timepicker.refresh_intervals, - withText: true, - }), - ], - linkControls: new DashboardLinksControls({}), - hideTimeControls: oldModel.timepicker.hidden, + controls: new DashboardControls({ + variableControls: [new VariableValueSelectors({}), new SceneDataLayerControls()], + timePicker: new SceneTimePicker({}), + refreshPicker: new SceneRefreshPicker({ + refresh: oldModel.refresh, + intervals: oldModel.timepicker.refresh_intervals, + withText: true, }), - ], + hideTimeControls: oldModel.timepicker.hidden, + }), }); return dashboardScene; diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts index b47bd847fd..d452f99539 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts @@ -12,7 +12,6 @@ import { SceneDataTransformer, SceneVariableSet, LocalValueVariable, - SceneRefreshPicker, } from '@grafana/scenes'; import { AnnotationQuery, @@ -34,7 +33,6 @@ import { getPanelDataFrames } from 'app/features/dashboard/components/HelpWizard import { DASHBOARD_SCHEMA_VERSION } from 'app/features/dashboard/state/DashboardMigrator'; import { GrafanaQueryType } from 'app/plugins/datasource/grafana/types'; -import { DashboardControls } from '../scene/DashboardControls'; import { DashboardScene } from '../scene/DashboardScene'; import { LibraryVizPanel } from '../scene/LibraryVizPanel'; import { PanelRepeaterGridItem } from '../scene/PanelRepeaterGridItem'; @@ -54,9 +52,6 @@ export function transformSceneToSaveModel(scene: DashboardScene, isSnapshot = fa const variablesSet = state.$variables; const body = state.body; - let refreshIntervals: string[] | undefined; - let hideTimePicker: boolean | undefined; - let panels: Panel[] = []; let graphTooltip = defaultDashboard.graphTooltip; let variables: VariableModel[] = []; @@ -92,16 +87,7 @@ export function transformSceneToSaveModel(scene: DashboardScene, isSnapshot = fa variables = sceneVariablesSetToVariables(variablesSet); } - if (state.controls && state.controls[0] instanceof DashboardControls) { - hideTimePicker = state.controls[0].state.hideTimeControls; - - const timeControls = state.controls[0].state.timeControls; - for (const control of timeControls) { - if (control instanceof SceneRefreshPicker && control.state.intervals) { - refreshIntervals = control.state.intervals; - } - } - } + const controlsState = state.controls?.state; if (state.$behaviors) { for (const behavior of state.$behaviors!) { @@ -113,8 +99,8 @@ export function transformSceneToSaveModel(scene: DashboardScene, isSnapshot = fa const timePickerWithoutDefaults = removeDefaults( { - refresh_intervals: refreshIntervals, - hidden: hideTimePicker, + refresh_intervals: controlsState?.refreshPicker.state.intervals, + hidden: controlsState?.hideTimeControls, nowDelay: timeRange.UNSAFE_nowDelay, }, defaultTimePickerConfig diff --git a/public/app/features/dashboard-scene/settings/DashboardLinksEditView.test.tsx b/public/app/features/dashboard-scene/settings/DashboardLinksEditView.test.tsx index 0143dfd43e..e9e04c6a70 100644 --- a/public/app/features/dashboard-scene/settings/DashboardLinksEditView.test.tsx +++ b/public/app/features/dashboard-scene/settings/DashboardLinksEditView.test.tsx @@ -2,18 +2,10 @@ import { render as RTLRender } from '@testing-library/react'; import React from 'react'; import { TestProvider } from 'test/helpers/TestProvider'; -import { - behaviors, - SceneGridLayout, - SceneGridItem, - SceneRefreshPicker, - SceneTimeRange, - SceneTimePicker, -} from '@grafana/scenes'; +import { behaviors, SceneGridLayout, SceneGridItem, SceneTimeRange } from '@grafana/scenes'; import { DashboardCursorSync } from '@grafana/schema'; import { DashboardControls } from '../scene/DashboardControls'; -import { DashboardLinksControls } from '../scene/DashboardLinksControls'; import { DashboardScene } from '../scene/DashboardScene'; import { activateFullSceneTree } from '../utils/test-utils'; @@ -225,18 +217,7 @@ async function buildTestScene() { const dashboard = new DashboardScene({ $timeRange: new SceneTimeRange({}), $behaviors: [new behaviors.CursorSync({ sync: DashboardCursorSync.Off })], - controls: [ - new DashboardControls({ - variableControls: [], - linkControls: new DashboardLinksControls({}), - timeControls: [ - new SceneTimePicker({}), - new SceneRefreshPicker({ - intervals: ['1s'], - }), - ], - }), - ], + controls: new DashboardControls({}), title: 'hello', uid: 'dash-1', meta: { diff --git a/public/app/features/dashboard-scene/settings/GeneralSettingsEditView.test.tsx b/public/app/features/dashboard-scene/settings/GeneralSettingsEditView.test.tsx index 245f9334fd..4becbbb2c7 100644 --- a/public/app/features/dashboard-scene/settings/GeneralSettingsEditView.test.tsx +++ b/public/app/features/dashboard-scene/settings/GeneralSettingsEditView.test.tsx @@ -1,15 +1,7 @@ -import { - behaviors, - SceneGridLayout, - SceneGridItem, - SceneRefreshPicker, - SceneTimeRange, - SceneTimePicker, -} from '@grafana/scenes'; +import { behaviors, SceneGridLayout, SceneGridItem, SceneTimeRange } from '@grafana/scenes'; import { DashboardCursorSync } from '@grafana/schema'; import { DashboardControls } from '../scene/DashboardControls'; -import { DashboardLinksControls } from '../scene/DashboardLinksControls'; import { DashboardScene } from '../scene/DashboardScene'; import { activateFullSceneTree } from '../utils/test-utils'; @@ -38,19 +30,9 @@ describe('GeneralSettingsEditView', () => { expect(settings.getTimeRange()).toBe(dashboard.state.$timeRange); }); - it('should return the dashboard refresh picker', () => { - expect(settings.getRefreshPicker()).toBe( - (dashboard.state?.controls?.[0] as DashboardControls)?.state?.timeControls?.[1] - ); - }); - it('should return the cursor sync', () => { expect(settings.getCursorSync()).toBe(dashboard.state.$behaviors?.[0]); }); - - it('should return the dashboard controls', () => { - expect(settings.getDashboardControls()).toBe(dashboard.state.controls?.[0]); - }); }); describe('Dashboard updates', () => { @@ -133,18 +115,7 @@ async function buildTestScene() { const dashboard = new DashboardScene({ $timeRange: new SceneTimeRange({}), $behaviors: [new behaviors.CursorSync({ sync: DashboardCursorSync.Off })], - controls: [ - new DashboardControls({ - variableControls: [], - linkControls: new DashboardLinksControls({}), - timeControls: [ - new SceneTimePicker({}), - new SceneRefreshPicker({ - intervals: ['1s'], - }), - ], - }), - ], + controls: new DashboardControls({}), title: 'hello', uid: 'dash-1', meta: { diff --git a/public/app/features/dashboard-scene/settings/GeneralSettingsEditView.tsx b/public/app/features/dashboard-scene/settings/GeneralSettingsEditView.tsx index fc84309fdc..3c1315b0a7 100644 --- a/public/app/features/dashboard-scene/settings/GeneralSettingsEditView.tsx +++ b/public/app/features/dashboard-scene/settings/GeneralSettingsEditView.tsx @@ -22,7 +22,6 @@ import { DeleteDashboardButton } from 'app/features/dashboard/components/DeleteD import { DashboardScene } from '../scene/DashboardScene'; import { NavToolbarActions } from '../scene/NavToolbarActions'; -import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; import { getDashboardSceneFor } from '../utils/utils'; import { DashboardEditView, DashboardEditViewState, useDashboardEditPageNav } from './utils'; @@ -61,7 +60,7 @@ export class GeneralSettingsEditView } public getRefreshPicker() { - return dashboardSceneGraph.getRefreshPicker(this._dashboard); + return this.getDashboardControls().state.refreshPicker; } public getCursorSync() { @@ -75,7 +74,7 @@ export class GeneralSettingsEditView } public getDashboardControls() { - return dashboardSceneGraph.getDashboardControls(this._dashboard); + return this._dashboard.state.controls!; } public onTitleChange = (value: string) => { @@ -151,8 +150,8 @@ export class GeneralSettingsEditView const { title, description, tags, meta, editable } = model.getDashboard().useState(); const { sync: graphTooltip } = model.getCursorSync()?.useState() || {}; const { timeZone, weekStart, UNSAFE_nowDelay: nowDelay } = model.getTimeRange().useState(); - const { intervals } = model.getRefreshPicker()?.useState() || {}; - const { hideTimeControls } = model.getDashboardControls()?.useState() || {}; + const { intervals } = model.getRefreshPicker().useState(); + const { hideTimeControls } = model.getDashboardControls().useState(); return ( diff --git a/public/app/features/dashboard-scene/utils/DashboardModelCompatibilityWrapper.test.ts b/public/app/features/dashboard-scene/utils/DashboardModelCompatibilityWrapper.test.ts index 93c7a45b45..be5d199260 100644 --- a/public/app/features/dashboard-scene/utils/DashboardModelCompatibilityWrapper.test.ts +++ b/public/app/features/dashboard-scene/utils/DashboardModelCompatibilityWrapper.test.ts @@ -3,11 +3,9 @@ import { behaviors, SceneGridItem, SceneGridLayout, - SceneRefreshPicker, SceneQueryRunner, SceneTimeRange, VizPanel, - SceneTimePicker, SceneDataTransformer, SceneDataLayers, } from '@grafana/scenes'; @@ -17,7 +15,6 @@ import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard'; import { AlertStatesDataLayer } from '../scene/AlertStatesDataLayer'; import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer'; import { DashboardControls } from '../scene/DashboardControls'; -import { DashboardLinksControls } from '../scene/DashboardLinksControls'; import { DashboardScene } from '../scene/DashboardScene'; import { NEW_LINK } from '../settings/links/utils'; @@ -37,7 +34,7 @@ describe('DashboardModelCompatibilityWrapper', () => { expect(wrapper.time.from).toBe('now-6h'); expect(wrapper.timezone).toBe('America/New_York'); expect(wrapper.weekStart).toBe('friday'); - expect(wrapper.timepicker.refresh_intervals).toEqual(['1s']); + expect(wrapper.timepicker.refresh_intervals![0]).toEqual('5s'); expect(wrapper.timepicker.hidden).toEqual(true); expect(wrapper.panels).toHaveLength(5); @@ -60,9 +57,7 @@ describe('DashboardModelCompatibilityWrapper', () => { expect(wrapper.panels[3].datasource).toEqual({ uid: 'gdev-testdata', type: 'grafana-testdata-datasource' }); expect(wrapper.panels[4].datasource).toEqual({ uid: SHARED_DASHBOARD_QUERY, type: 'datasource' }); - (scene.state.controls![0] as DashboardControls).setState({ - hideTimeControls: false, - }); + scene.state.controls!.setState({ hideTimeControls: false }); const wrapper2 = new DashboardModelCompatibilityWrapper(scene); expect(wrapper2.timepicker.hidden).toEqual(false); @@ -175,19 +170,9 @@ function setup() { }), ], }), - controls: [ - new DashboardControls({ - variableControls: [], - linkControls: new DashboardLinksControls({}), - timeControls: [ - new SceneTimePicker({}), - new SceneRefreshPicker({ - intervals: ['1s'], - }), - ], - hideTimeControls: true, - }), - ], + controls: new DashboardControls({ + hideTimeControls: true, + }), body: new SceneGridLayout({ children: [ new SceneGridItem({ diff --git a/public/app/features/dashboard-scene/utils/DashboardModelCompatibilityWrapper.ts b/public/app/features/dashboard-scene/utils/DashboardModelCompatibilityWrapper.ts index 4e4b6c12ed..21b83ce910 100644 --- a/public/app/features/dashboard-scene/utils/DashboardModelCompatibilityWrapper.ts +++ b/public/app/features/dashboard-scene/utils/DashboardModelCompatibilityWrapper.ts @@ -17,7 +17,6 @@ import { DashboardScene } from '../scene/DashboardScene'; import { dataLayersToAnnotations } from '../serialization/dataLayersToAnnotations'; import { PanelModelCompatibilityWrapper } from './PanelModelCompatibilityWrapper'; -import { dashboardSceneGraph } from './dashboardSceneGraph'; import { findVizPanelByKey, getVizPanelKeyForPanelId } from './utils'; /** @@ -68,8 +67,8 @@ export class DashboardModelCompatibilityWrapper { public get timepicker() { return { - refresh_intervals: dashboardSceneGraph.getRefreshPicker(this._scene)?.state.intervals, - hidden: dashboardSceneGraph.getDashboardControls(this._scene)?.state.hideTimeControls ?? false, + refresh_intervals: this._scene.state.controls!.state.refreshPicker.state.intervals, + hidden: this._scene.state.controls!.state.hideTimeControls ?? false, }; } diff --git a/public/app/features/dashboard-scene/utils/dashboardSceneGraph.test.ts b/public/app/features/dashboard-scene/utils/dashboardSceneGraph.test.ts index 927e9bf631..5e106d15d9 100644 --- a/public/app/features/dashboard-scene/utils/dashboardSceneGraph.test.ts +++ b/public/app/features/dashboard-scene/utils/dashboardSceneGraph.test.ts @@ -4,8 +4,6 @@ import { SceneGridLayout, SceneGridRow, SceneQueryRunner, - SceneRefreshPicker, - SceneTimePicker, SceneTimeRange, VizPanel, } from '@grafana/scenes'; @@ -13,7 +11,6 @@ import { import { AlertStatesDataLayer } from '../scene/AlertStatesDataLayer'; import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer'; import { DashboardControls } from '../scene/DashboardControls'; -import { DashboardLinksControls } from '../scene/DashboardLinksControls'; import { DashboardScene, DashboardSceneState } from '../scene/DashboardScene'; import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks'; @@ -21,67 +18,6 @@ import { dashboardSceneGraph } from './dashboardSceneGraph'; import { findVizPanelByKey } from './utils'; describe('dashboardSceneGraph', () => { - describe('getTimePicker', () => { - it('should return null if no time picker', () => { - const scene = buildTestScene({ - controls: [ - new DashboardControls({ - variableControls: [], - linkControls: new DashboardLinksControls({}), - timeControls: [], - }), - ], - }); - - const timePicker = dashboardSceneGraph.getTimePicker(scene); - expect(timePicker).toBeNull(); - }); - - it('should return time picker', () => { - const scene = buildTestScene(); - const timePicker = dashboardSceneGraph.getTimePicker(scene); - expect(timePicker).not.toBeNull(); - }); - }); - - describe('getRefreshPicker', () => { - it('should return null if no refresh picker', () => { - const scene = buildTestScene({ - controls: [ - new DashboardControls({ - variableControls: [], - linkControls: new DashboardLinksControls({}), - timeControls: [], - }), - ], - }); - - const refreshPicker = dashboardSceneGraph.getRefreshPicker(scene); - expect(refreshPicker).toBeNull(); - }); - - it('should return refresh picker', () => { - const scene = buildTestScene(); - const refreshPicker = dashboardSceneGraph.getRefreshPicker(scene); - expect(refreshPicker).not.toBeNull(); - }); - }); - - describe('getDashboardControls', () => { - it('should return null if no dashboard controls', () => { - const scene = buildTestScene({ controls: [] }); - - const dashboardControls = dashboardSceneGraph.getDashboardControls(scene); - expect(dashboardControls).toBeNull(); - }); - - it('should return dashboard controls', () => { - const scene = buildTestScene(); - const dashboardControls = dashboardSceneGraph.getDashboardControls(scene); - expect(dashboardControls).not.toBeNull(); - }); - }); - describe('getPanelLinks', () => { it('should throw if no links object defined', () => { const scene = buildTestScene(); @@ -155,18 +91,7 @@ function buildTestScene(overrides?: Partial) { title: 'hello', uid: 'dash-1', $timeRange: new SceneTimeRange({}), - controls: [ - new DashboardControls({ - variableControls: [], - linkControls: new DashboardLinksControls({}), - timeControls: [ - new SceneTimePicker({}), - new SceneRefreshPicker({ - intervals: ['1s'], - }), - ], - }), - ], + controls: new DashboardControls({}), $data: new SceneDataLayers({ layers: [ new DashboardAnnotationsDataLayer({ diff --git a/public/app/features/dashboard-scene/utils/dashboardSceneGraph.ts b/public/app/features/dashboard-scene/utils/dashboardSceneGraph.ts index cfea3809e6..3047692da5 100644 --- a/public/app/features/dashboard-scene/utils/dashboardSceneGraph.ts +++ b/public/app/features/dashboard-scene/utils/dashboardSceneGraph.ts @@ -1,48 +1,14 @@ -import { - SceneTimePicker, - SceneRefreshPicker, - VizPanel, - SceneGridItem, - SceneGridRow, - SceneDataLayers, - sceneGraph, -} from '@grafana/scenes'; +import { VizPanel, SceneGridItem, SceneGridRow, SceneDataLayers, sceneGraph } from '@grafana/scenes'; -import { DashboardControls } from '../scene/DashboardControls'; import { DashboardScene } from '../scene/DashboardScene'; import { VizPanelLinks } from '../scene/PanelLinks'; function getTimePicker(scene: DashboardScene) { - const dashboardControls = getDashboardControls(scene); - - if (dashboardControls) { - const timePicker = dashboardControls.state.timeControls.find((c) => c instanceof SceneTimePicker); - if (timePicker && timePicker instanceof SceneTimePicker) { - return timePicker; - } - } - - return null; + return scene.state.controls?.state.timePicker; } function getRefreshPicker(scene: DashboardScene) { - const dashboardControls = getDashboardControls(scene); - - if (dashboardControls) { - for (const control of dashboardControls.state.timeControls) { - if (control instanceof SceneRefreshPicker) { - return control; - } - } - } - return null; -} - -function getDashboardControls(scene: DashboardScene) { - if (scene.state.controls?.[0] instanceof DashboardControls) { - return scene.state.controls[0]; - } - return null; + return scene.state.controls?.state.refreshPicker; } function getPanelLinks(panel: VizPanel) { @@ -92,7 +58,6 @@ function getDataLayers(scene: DashboardScene): SceneDataLayers { export const dashboardSceneGraph = { getTimePicker, getRefreshPicker, - getDashboardControls, getPanelLinks, getVizPanels, getDataLayers, From dc718a7d9dc581b595c93040a895096ff925e443 Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Tue, 20 Feb 2024 10:07:42 +0100 Subject: [PATCH 0023/1406] Loki: Pass time range variable in variable editor (#82900) * Loki: Pass time range variable in variable editor * Remove not needed type * Update * Add tests, not re-run if type does not change * Add range as dependency --- .betterer.results.json | 6 - .../components/VariableQueryEditor.test.tsx | 129 ++++++++++++++++++ .../loki/components/VariableQueryEditor.tsx | 11 +- 3 files changed, 136 insertions(+), 10 deletions(-) diff --git a/.betterer.results.json b/.betterer.results.json index 4dd7405a36..abeb9276cf 100644 --- a/.betterer.results.json +++ b/.betterer.results.json @@ -1028,12 +1028,6 @@ "count": 5 } ], - "/packages/grafana-ui/src/components/Splitter/Splitter.tsx": [ - { - "message": "Do not use any type assertions.", - "count": 1 - } - ], "/packages/grafana-ui/src/components/StatsPicker/StatsPicker.story.tsx": [ { "message": "Unexpected any. Specify a different type.", diff --git a/public/app/plugins/datasource/loki/components/VariableQueryEditor.test.tsx b/public/app/plugins/datasource/loki/components/VariableQueryEditor.test.tsx index 48613c882e..758dfc7055 100644 --- a/public/app/plugins/datasource/loki/components/VariableQueryEditor.test.tsx +++ b/public/app/plugins/datasource/loki/components/VariableQueryEditor.test.tsx @@ -3,6 +3,7 @@ import userEvent from '@testing-library/user-event'; import React from 'react'; import { select } from 'react-select-event'; +import { TimeRange, dateTime } from '@grafana/data'; import { TemplateSrv } from '@grafana/runtime'; import { createLokiDatasource } from '../__mocks__/datasource'; @@ -133,4 +134,132 @@ describe('LokiVariableQueryEditor', () => { await select(screen.getByLabelText('Label'), 'luna', { container: document.body }); await screen.findByText('luna'); }); + + test('Calls language provider fetchLabels with the time range received in props', async () => { + const now = dateTime('2023-09-16T21:26:00Z'); + const range: TimeRange = { + from: dateTime(now).subtract(2, 'days'), + to: now, + raw: { + from: 'now-2d', + to: 'now', + }, + }; + props.range = range; + props.query = { + refId: 'test', + type: LokiVariableQueryType.LabelValues, + label: 'luna', + }; + + render(); + await waitFor(() => + expect(props.datasource.languageProvider.fetchLabels).toHaveBeenCalledWith({ timeRange: range }) + ); + }); + + test('does not re-run fetch labels when type does not change', async () => { + const now = dateTime('2023-09-16T21:26:00Z'); + const range: TimeRange = { + from: dateTime(now).subtract(2, 'days'), + to: now, + raw: { + from: 'now-2d', + to: 'now', + }, + }; + props.range = range; + props.query = { + refId: 'test', + type: LokiVariableQueryType.LabelValues, + }; + + props.datasource.languageProvider.fetchLabels = jest.fn().mockResolvedValue([]); + const { rerender } = render(); + rerender( + + ); + + await waitFor(() => { + expect(props.datasource.languageProvider.fetchLabels).toHaveBeenCalledTimes(1); + }); + }); + + test('runs fetch labels when type changes to from LabelNames to LabelValues', async () => { + const now = dateTime('2023-09-16T21:26:00Z'); + const range: TimeRange = { + from: dateTime(now).subtract(2, 'days'), + to: now, + raw: { + from: 'now-2d', + to: 'now', + }, + }; + props.range = range; + props.query = { + refId: 'test', + type: LokiVariableQueryType.LabelNames, + }; + + props.datasource.languageProvider.fetchLabels = jest.fn().mockResolvedValue([]); + const { rerender } = render(); + rerender( + + ); + + await waitFor(() => { + expect(props.datasource.languageProvider.fetchLabels).toHaveBeenCalledTimes(1); + }); + }); + + test('runs fetch labels when type changes to LabelValues', async () => { + const now = dateTime('2023-09-16T21:26:00Z'); + const range: TimeRange = { + from: dateTime(now).subtract(2, 'days'), + to: now, + raw: { + from: 'now-2d', + to: 'now', + }, + }; + props.range = range; + props.query = { + refId: 'test', + type: LokiVariableQueryType.LabelNames, + }; + + props.datasource.languageProvider.fetchLabels = jest.fn().mockResolvedValue([]); + // Starting with LabelNames + const { rerender } = render(); + + // Changing to LabelValues, should run fetchLabels + rerender( + + ); + await waitFor(() => { + expect(props.datasource.languageProvider.fetchLabels).toHaveBeenCalledTimes(1); + }); + + // Keeping the type of LabelValues, should not run additional fetchLabels + rerender( + + ); + await waitFor(() => { + expect(props.datasource.languageProvider.fetchLabels).toHaveBeenCalledTimes(1); + }); + + // Changing to LabelNames, should not run additional fetchLabels + rerender(); + await waitFor(() => { + expect(props.datasource.languageProvider.fetchLabels).toHaveBeenCalledTimes(1); + }); + + // Changing to LabelValues, should run additional fetchLabels + rerender( + + ); + await waitFor(() => { + expect(props.datasource.languageProvider.fetchLabels).toHaveBeenCalledTimes(2); + }); + }); }); diff --git a/public/app/plugins/datasource/loki/components/VariableQueryEditor.tsx b/public/app/plugins/datasource/loki/components/VariableQueryEditor.tsx index 749b564b80..63246b7365 100644 --- a/public/app/plugins/datasource/loki/components/VariableQueryEditor.tsx +++ b/public/app/plugins/datasource/loki/components/VariableQueryEditor.tsx @@ -1,4 +1,5 @@ import React, { FormEvent, useState, useEffect } from 'react'; +import { usePrevious } from 'react-use'; import { QueryEditorProps, SelectableValue } from '@grafana/data'; import { InlineField, InlineFieldRow, Input, Select } from '@grafana/ui'; @@ -16,11 +17,12 @@ export type Props = QueryEditorProps { +export const LokiVariableQueryEditor = ({ onChange, query, datasource, range }: Props) => { const [type, setType] = useState(undefined); const [label, setLabel] = useState(''); const [labelOptions, setLabelOptions] = useState>>([]); const [stream, setStream] = useState(''); + const previousType = usePrevious(type); useEffect(() => { if (!query) { @@ -34,14 +36,15 @@ export const LokiVariableQueryEditor = ({ onChange, query, datasource }: Props) }, [query]); useEffect(() => { - if (type !== QueryType.LabelValues) { + // Fetch label names when the query type is LabelValues, and the previous type was not the same + if (type !== QueryType.LabelValues || previousType === type) { return; } - datasource.languageProvider.fetchLabels().then((labelNames: string[]) => { + datasource.languageProvider.fetchLabels({ timeRange: range }).then((labelNames) => { setLabelOptions(labelNames.map((labelName) => ({ label: labelName, value: labelName }))); }); - }, [datasource, type]); + }, [datasource, type, range, previousType]); const onQueryTypeChange = (newType: SelectableValue) => { setType(newType.value); From 941881266ade31cba9d38093128a0d661b01811a Mon Sep 17 00:00:00 2001 From: Will Browne Date: Tue, 20 Feb 2024 11:11:50 +0100 Subject: [PATCH 0024/1406] Plugins: Remove unused metadata.md file (#83086) * remove unused metadata * remove CODEOWNERS ref --- .github/CODEOWNERS | 1 - metadata.md | 0 2 files changed, 1 deletion(-) delete mode 100644 metadata.md diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index f9e5d9fdf1..da322681cf 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -359,7 +359,6 @@ /.vim @zoltanbedi /jest.config.js @grafana/frontend-ops /latest.json @grafana/frontend-ops -/metadata.md @grafana/plugins-platform /stylelint.config.js @grafana/frontend-ops /tools/ @grafana/frontend-ops /lefthook.yml @grafana/frontend-ops diff --git a/metadata.md b/metadata.md deleted file mode 100644 index e69de29bb2..0000000000 From 914f033497a1985b407c8199b243c2dd1d913260 Mon Sep 17 00:00:00 2001 From: Jan Garaj Date: Tue, 20 Feb 2024 11:22:28 +0100 Subject: [PATCH 0025/1406] CloudWatch: Update AWS/AutoScaling metrics (#83036) --- pkg/tsdb/cloudwatch/constants/metrics.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/tsdb/cloudwatch/constants/metrics.go b/pkg/tsdb/cloudwatch/constants/metrics.go index aee31d2d53..3fc12897c0 100644 --- a/pkg/tsdb/cloudwatch/constants/metrics.go +++ b/pkg/tsdb/cloudwatch/constants/metrics.go @@ -10,7 +10,7 @@ var NamespaceMetricsMap = map[string][]string{ "AWS/AppSync": {"4XXError", "5XXError", "Latency", "Requests", "TokensConsumed", "ActiveConnections", "ActiveSubscriptions", "ConnectClientError", "ConnectionDuration", "ConnectServerError", "ConnectSuccess", "DisconnectClientError", "DisconnectServerError", "DisconnectSuccess", "PublishDataMessageClientError", "PublishDataMessageServerError", "PublishDataMessageSize", "PublishDataMessageSuccess", "SubscribeClientError", "SubscribeServerError", "SubscribeSuccess", "UnsubscribeClientError", "UnsubscribeServerError", "UnsubscribeSuccess"}, "AWS/ApplicationELB": {"ActiveConnectionCount", "ClientTLSNegotiationErrorCount", "ConsumedLCUs", "DesyncMitigationMode_NonCompliant_Request_Count", "DroppedInvalidHeaderRequestCount", "ELBAuthError", "ELBAuthFailure", "ELBAuthLatency", "ELBAuthRefreshTokenSuccess", "ELBAuthSuccess", "ELBAuthUserClaimsSizeExceeded", "ForwardedInvalidHeaderRequestCount", "GrpcRequestCount", "HTTPCode_ELB_3XX_Count", "HTTPCode_ELB_4XX_Count", "HTTPCode_ELB_5XX_Count", "HTTPCode_ELB_500_Count", "HTTPCode_ELB_502_Count", "HTTPCode_ELB_503_Count", "HTTPCode_ELB_504_Count", "HTTPCode_Target_2XX_Count", "HTTPCode_Target_3XX_Count", "HTTPCode_Target_4XX_Count", "HTTPCode_Target_5XX_Count", "HTTP_Fixed_Response_Count", "HTTP_Redirect_Count", "HTTP_Redirect_Url_Limit_Exceeded_Count", "HealthyHostCount", "IPv6ProcessedBytes", "IPv6RequestCount", "LambdaInternalError", "LambdaTargetProcessedBytes", "LambdaUserError", "NewConnectionCount", "NonStickyRequestCount", "ProcessedBytes", "RejectedConnectionCount", "RequestCount", "RequestCountPerTarget", "RuleEvaluations", "StandardProcessedBytes", "TargetConnectionErrorCount", "TargetResponseTime", "TargetTLSNegotiationErrorCount", "UnHealthyHostCount"}, "AWS/Athena": {"EngineExecutionTime", "QueryPlanningTime", "QueryQueueTime", "ProcessedBytes", "ServiceProcessingTime", "TotalExecutionTime"}, - "AWS/AutoScaling": {"GroupDesiredCapacity", "GroupInServiceInstances", "GroupMaxSize", "GroupMinSize", "GroupPendingInstances", "GroupStandbyInstances", "GroupTerminatingInstances", "GroupTotalInstances"}, + "AWS/AutoScaling": {"GroupAndWarmPoolDesiredCapacity", "GroupAndWarmPoolTotalCapacity", "GroupDesiredCapacity", "GroupInServiceCapacity", "GroupInServiceInstances", "GroupMaxSize", "GroupMinSize", "GroupPendingCapacity", "GroupPendingInstances", "GroupStandbyCapacity", "GroupStandbyInstances", "GroupTerminatingCapacity", "GroupTerminatingInstances", "GroupTotalCapacity", "GroupTotalInstances", "PredictiveScalingCapacityForecast", "PredictiveScalingLoadForecast", "PredictiveScalingMetricPairCorrelation", "WarmPoolDesiredCapacity", "WarmPoolMinSize", "WarmPoolPendingCapacity", "WarmPoolTerminatingCapacity", "WarmPoolTotalCapacity", "WarmPoolWarmedCapacity"}, "AWS/Bedrock": {"Invocations", "InvocationLatency", "InvocationClientErrors", "InvocationServerErrors", "InvocationThrottles", "InputTokenCount", "OutputImageCount", "OutputTokenCount"}, "AWS/Billing": {"EstimatedCharges"}, "AWS/Backup": {"NumberOfBackupJobsAborted", "NumberOfBackupJobsCompleted", "NumberOfBackupJobsCreated", "NumberOfBackupJobsExpired", "NumberOfBackupJobsFailed", "NumberOfBackupJobsPending", "NumberOfBackupJobsRunning", "NumberOfCopyJobsCompleted", "NumberOfCopyJobsCreated", "NumberOfCopyJobsFailed", "NumberOfCopyJobsRunning", "NumberOfRecoveryPointsCold", "NumberOfRecoveryPointsCompleted", "NumberOfRecoveryPointsDeleting", "NumberOfRecoveryPointsExpired", "NumberOfRecoveryPointsPartial", "NumberOfRestoreJobsCompleted", "NumberOfRestoreJobsFailed", "NumberOfRestoreJobsPending", "NumberOfRestoreJobsRunning"}, @@ -426,7 +426,7 @@ var NamespaceDimensionKeysMap = map[string][]string{ "AWS/AppSync": {"GraphQLAPIId"}, "AWS/ApplicationELB": {"AvailabilityZone", "LoadBalancer", "TargetGroup"}, "AWS/Athena": {"QueryState", "QueryType", "WorkGroup"}, - "AWS/AutoScaling": {"AutoScalingGroupName"}, + "AWS/AutoScaling": {"AutoScalingGroupName", "PairIndex", "PolicyName"}, "AWS/Backup": {"BackupVaultName", "ResourceType"}, "AWS/Bedrock": {"BucketedStepSize", "ImageSize", "ModelId"}, "AWS/Billing": {"Currency", "LinkedAccount", "ServiceName"}, From b6098f2ddec6edf0ea7e873d4cd8d7ece2f59458 Mon Sep 17 00:00:00 2001 From: Pepe Cano <825430+ppcano@users.noreply.github.com> Date: Tue, 20 Feb 2024 11:49:39 +0100 Subject: [PATCH 0026/1406] Alerting docs: clean up Cloud links (#83073) * Use fixed `/docs/grafana-cloud/alerting-and-irm` URLs for cloud references * Fix `docs/grafana-cloud/` data sources links * Fix `docs/grafana-cloud/` Panel & Visualization links * Fix `/docs/grafana-cloud/` link to Dashboard page * Set root directory `docs/reference` for non-cloud pages * Fix `admonition` cannot use a `docs/reference` relative link * Update `doc-validator` https://github.com/grafana/technical-documentation/releases/tag/doc-validator%2Fv4.1.0 Signed-off-by: Jack Baldry --------- Signed-off-by: Jack Baldry Co-authored-by: Jack Baldry --- .github/workflows/doc-validator.yml | 2 +- .../create-grafana-managed-rule.md | 6 ++-- .../alert-rules/queries-conditions/_index.md | 2 +- .../annotation-label/how-to-use-labels.md | 5 +-- .../variables-label-annotation.md | 3 +- .../fundamentals/data-source-alerting.md | 34 +++++++++---------- .../fundamentals/evaluate-grafana-alerts.md | 4 +-- .../images-in-notifications.md | 6 ++-- .../manage-notifications/mute-timings.md | 2 +- docs/sources/alerting/set-up/_index.md | 7 ++-- .../legacy-alerting-deprecation.md | 3 +- .../provision-alerting-resources/_index.md | 15 ++++---- .../export-alerting-resources/index.md | 8 +++-- .../file-provisioning/index.md | 10 +++--- .../terraform-provisioning/index.md | 9 +++-- 15 files changed, 56 insertions(+), 60 deletions(-) diff --git a/.github/workflows/doc-validator.yml b/.github/workflows/doc-validator.yml index 9e28a128b9..e8241e41c6 100644 --- a/.github/workflows/doc-validator.yml +++ b/.github/workflows/doc-validator.yml @@ -7,7 +7,7 @@ jobs: doc-validator: runs-on: "ubuntu-latest" container: - image: "grafana/doc-validator:v4.0.0" + image: "grafana/doc-validator:v4.1.0" steps: - name: "Checkout code" uses: "actions/checkout@v4" diff --git a/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md b/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md index 1d43355576..7b21847e4b 100644 --- a/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md +++ b/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md @@ -225,7 +225,7 @@ This will open the alert rule form, allowing you to configure and create your al {{% docs/reference %}} [add-a-query]: "/docs/grafana/ -> /docs/grafana//panels-visualizations/query-transform-data#add-a-query" -[add-a-query]: "/docs/grafana-cloud/ -> /docs/grafana//panels-visualizations/query-transform-data#add-a-query" +[add-a-query]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/panels-visualizations/query-transform-data#add-a-query" [alerting-on-numeric-data]: "/docs/grafana/ -> /docs/grafana//alerting/fundamentals/evaluate-grafana-alerts#alerting-on-numeric-data-1" [alerting-on-numeric-data]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/fundamentals/evaluate-grafana-alerts#alerting-on-numeric-data-1" @@ -234,11 +234,11 @@ This will open the alert rule form, allowing you to configure and create your al [annotation-label]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/fundamentals/annotation-label" [expression-queries]: "/docs/grafana/ -> /docs/grafana//panels-visualizations/query-transform-data/expression-queries" -[expression-queries]: "/docs/grafana-cloud/ -> /docs/grafana//panels-visualizations/query-transform-data/expression-queries" +[expression-queries]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/panels-visualizations/query-transform-data/expression-queries" [fundamentals]: "/docs/grafana/ -> /docs/grafana//alerting/fundamentals" [fundamentals]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/fundamentals" [time-units-and-relative-ranges]: "/docs/grafana/ -> /docs/grafana//dashboards/use-dashboards#time-units-and-relative-ranges" -[time-units-and-relative-ranges]: "/docs/grafana-cloud/ -> /docs/grafana//dashboards/use-dashboards#time-units-and-relative-ranges" +[time-units-and-relative-ranges]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/dashboards/use-dashboards#time-units-and-relative-ranges" {{% /docs/reference %}} diff --git a/docs/sources/alerting/fundamentals/alert-rules/queries-conditions/_index.md b/docs/sources/alerting/fundamentals/alert-rules/queries-conditions/_index.md index c4191ad384..12f9718f40 100644 --- a/docs/sources/alerting/fundamentals/alert-rules/queries-conditions/_index.md +++ b/docs/sources/alerting/fundamentals/alert-rules/queries-conditions/_index.md @@ -153,5 +153,5 @@ For example, you could set a threshold of 1000ms and a recovery threshold of 900 [data-source-alerting]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/fundamentals/data-source-alerting" [query-transform-data]: "/docs/grafana/ -> /docs/grafana//panels-visualizations/query-transform-data" -[query-transform-data]: "/docs/grafana-cloud/ -> /docs/grafana//panels-visualizations/query-transform-data" +[query-transform-data]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/panels-visualizations/query-transform-data" {{% /docs/reference %}} diff --git a/docs/sources/alerting/fundamentals/annotation-label/how-to-use-labels.md b/docs/sources/alerting/fundamentals/annotation-label/how-to-use-labels.md index f287f23a7a..58e6cbd185 100644 --- a/docs/sources/alerting/fundamentals/annotation-label/how-to-use-labels.md +++ b/docs/sources/alerting/fundamentals/annotation-label/how-to-use-labels.md @@ -45,7 +45,7 @@ Example: A label key/value pair `Alert! 🔔="🔥"` will become `Alert_0x1f514= {{% admonition type="note" %}} Labels prefixed with `grafana_` are reserved by Grafana for special use. If a manually configured label is added beginning with `grafana_` it may be overwritten in case of collision. -To stop the Grafana Alerting engine from adding a reserved label, you can disable it via the `disabled_labels` option in [unified_alerting.reserved_labels][unified-alerting-reserved-labels] configuration. +To stop the Grafana Alerting engine from adding a reserved label, you can disable it via the `disabled_labels` option in [unified_alerting.reserved_labels](/docs/grafana//setup-grafana/configure-grafana#unified_alertingreserved_labels) configuration. {{% /admonition %}} Grafana reserved labels can be used in the same way as manually configured labels. The current list of available reserved labels are: @@ -57,7 +57,4 @@ Grafana reserved labels can be used in the same way as manually configured label {{% docs/reference %}} [alerting-rules]: "/docs/grafana/ -> /docs/grafana//alerting/alerting-rules" [alerting-rules]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/alerting-rules" - -[unified-alerting-reserved-labels]: "/docs/grafana/ -> /docs/grafana//setup-grafana/configure-grafana#unified_alertingreserved_labels" -[unified-alerting-reserved-labels]: "/docs/grafana-cloud/ -> /docs/grafana//setup-grafana/configure-grafana#unified_alertingreserved_labels" {{% /docs/reference %}} diff --git a/docs/sources/alerting/fundamentals/annotation-label/variables-label-annotation.md b/docs/sources/alerting/fundamentals/annotation-label/variables-label-annotation.md index c3d67b9eac..40eed07410 100644 --- a/docs/sources/alerting/fundamentals/annotation-label/variables-label-annotation.md +++ b/docs/sources/alerting/fundamentals/annotation-label/variables-label-annotation.md @@ -445,6 +445,5 @@ example.com:8080 ``` {{% docs/reference %}} -[explore]: "/docs/grafana/ -> /docs/grafana//explore" -[explore]: "/docs/grafana-cloud/ -> /docs/grafana//explore" +[explore]: "/docs/ -> /docs/grafana//explore" {{% /docs/reference %}} diff --git a/docs/sources/alerting/fundamentals/data-source-alerting.md b/docs/sources/alerting/fundamentals/data-source-alerting.md index b7471e23f4..cae5474a88 100644 --- a/docs/sources/alerting/fundamentals/data-source-alerting.md +++ b/docs/sources/alerting/fundamentals/data-source-alerting.md @@ -43,53 +43,53 @@ These are the data sources that are compatible with and supported by Grafana Ale {{% docs/reference %}} [Grafana data sources]: "/docs/grafana/ -> /docs/grafana//datasources" -[Grafana data sources]: "/docs/grafana-cloud/ -> /docs/grafana//datasources" +[Grafana data sources]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/connect-externally-hosted/data-sources" [AWS CloudWatch]: "/docs/grafana/ -> /docs/grafana//datasources/aws-cloudwatch" -[AWS CloudWatch]: "/docs/grafana-cloud/ -> /docs/grafana//datasources/aws-cloudwatch" +[AWS CloudWatch]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/connect-externally-hosted/data-sources/aws-cloudwatch" [Azure Monitor]: "/docs/grafana/ -> /docs/grafana//datasources/azure-monitor" -[Azure Monitor]: "/docs/grafana-cloud/ -> /docs/grafana//datasources/azure-monitor" +[Azure Monitor]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/connect-externally-hosted/data-sources/azure-monitor" [Elasticsearch]: "/docs/grafana/ -> /docs/grafana//datasources/elasticsearch" -[Elasticsearch]: "/docs/grafana-cloud/ -> /docs/grafana//datasources/elasticsearch" +[Elasticsearch]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/connect-externally-hosted/data-sources/elasticsearch" [Google Cloud Monitoring]: "/docs/grafana/ -> /docs/grafana//datasources/google-cloud-monitoring" -[Google Cloud Monitoring]: "/docs/grafana-cloud/ -> /docs/grafana//datasources/google-cloud-monitoring" +[Google Cloud Monitoring]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/connect-externally-hosted/data-sources/google-cloud-monitoring" [Graphite]: "/docs/grafana/ -> /docs/grafana//datasources/graphite" -[Graphite]: "/docs/grafana-cloud/ -> /docs/grafana//datasources/graphite" +[Graphite]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/connect-externally-hosted/data-sources/graphite" [InfluxDB]: "/docs/grafana/ -> /docs/grafana//datasources/influxdb" -[InfluxDB]: "/docs/grafana-cloud/ -> /docs/grafana//datasources/influxdb" +[InfluxDB]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/connect-externally-hosted/data-sources/influxdb" [Loki]: "/docs/grafana/ -> /docs/grafana//datasources/loki" -[Loki]: "/docs/grafana-cloud/ -> /docs/grafana//datasources/loki" +[Loki]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/connect-externally-hosted/data-sources/loki" [Microsoft SQL Server (MSSQL)]: "/docs/grafana/ -> /docs/grafana//datasources/mssql" -[Microsoft SQL Server (MSSQL)]: "/docs/grafana-cloud/ -> /docs/grafana//datasources/mssql" +[Microsoft SQL Server (MSSQL)]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/connect-externally-hosted/data-sources/mssql" [MySQL]: "/docs/grafana/ -> /docs/grafana//datasources/mysql" -[MySQL]: "/docs/grafana-cloud/ -> /docs/grafana//datasources/mysql" +[MySQL]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/connect-externally-hosted/data-sources/mysql" [Open TSDB]: "/docs/grafana/ -> /docs/grafana//datasources/opentsdb" -[Open TSDB]: "/docs/grafana-cloud/ -> /docs/grafana//datasources/opentsdb" +[Open TSDB]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/connect-externally-hosted/data-sources/opentsdb" [PostgreSQL]: "/docs/grafana/ -> /docs/grafana//datasources/postgres" -[PostgreSQL]: "/docs/grafana-cloud/ -> /docs/grafana//datasources/postgres" +[PostgreSQL]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/connect-externally-hosted/data-sources/postgres" [Prometheus]: "/docs/grafana/ -> /docs/grafana//datasources/prometheus" -[Prometheus]: "/docs/grafana-cloud/ -> /docs/grafana//datasources/prometheus" +[Prometheus]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/connect-externally-hosted/data-sources/prometheus" [Jaeger]: "/docs/grafana/ -> /docs/grafana//datasources/jaeger" -[Jaeger]: "/docs/grafana-cloud/ -> /docs/grafana//datasources/jaeger" +[Jaeger]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/connect-externally-hosted/data-sources/jaeger" [Zipkin]: "/docs/grafana/ -> /docs/grafana//datasources/zipkin" -[Zipkin]: "/docs/grafana-cloud/ -> /docs/grafana//datasources/zipkin" +[Zipkin]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/connect-externally-hosted/data-sources/zipkin" [Tempo]: "/docs/grafana/ -> /docs/grafana//datasources/tempo" -[Tempo]: "/docs/grafana-cloud/ -> /docs/grafana//datasources/tempo" +[Tempo]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/connect-externally-hosted/data-sources/tempo" [Testdata]: "/docs/grafana/ -> /docs/grafana//datasources/testdata" -[Testdata]: "/docs/grafana-cloud/ -> /docs/grafana//datasources/testdata" +[Testdata]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/connect-externally-hosted/data-sources/testdata" {{% /docs/reference %}} diff --git a/docs/sources/alerting/fundamentals/evaluate-grafana-alerts.md b/docs/sources/alerting/fundamentals/evaluate-grafana-alerts.md index 2a5ca6996a..c7ec0565b2 100644 --- a/docs/sources/alerting/fundamentals/evaluate-grafana-alerts.md +++ b/docs/sources/alerting/fundamentals/evaluate-grafana-alerts.md @@ -108,7 +108,5 @@ When this query is used as the **condition** in an alert rule, then the non-zero | {Host=web3,disk=/var} | Normal | {{% docs/reference %}} - -[set-up-grafana-monitoring]: "/docs/grafana/ -> /docs/grafana//setup-grafana/set-up-grafana-monitoring" -[set-up-grafana-monitoring]: "/docs/grafana-cloud/ -> /docs/grafana//setup-grafana/set-up-grafana-monitoring" +[set-up-grafana-monitoring]: "/docs/ -> /docs/grafana//setup-grafana/set-up-grafana-monitoring" {{% /docs/reference %}} diff --git a/docs/sources/alerting/manage-notifications/images-in-notifications.md b/docs/sources/alerting/manage-notifications/images-in-notifications.md index 3559abbc66..548b76e448 100644 --- a/docs/sources/alerting/manage-notifications/images-in-notifications.md +++ b/docs/sources/alerting/manage-notifications/images-in-notifications.md @@ -139,9 +139,7 @@ For example, if a screenshot could not be taken within the expected time (10 sec - `grafana_screenshot_upload_successes_total` {{% docs/reference %}} -[image-rendering]: "/docs/grafana/ -> /docs/grafana//setup-grafana/image-rendering" -[image-rendering]: "/docs/grafana-cloud/ -> /docs/grafana//setup-grafana/image-rendering" +[image-rendering]: "/docs/ -> /docs/grafana//setup-grafana/image-rendering" -[paths]: "/docs/grafana/ -> /docs/grafana//setup-grafana/configure-grafana#paths" -[paths]: "/docs/grafana-cloud/ -> /docs/grafana//setup-grafana/configure-grafana#paths" +[paths]: "/docs/ -> /docs/grafana//setup-grafana/configure-grafana#paths" {{% /docs/reference %}} diff --git a/docs/sources/alerting/manage-notifications/mute-timings.md b/docs/sources/alerting/manage-notifications/mute-timings.md index 86c55ef564..e3337f7877 100644 --- a/docs/sources/alerting/manage-notifications/mute-timings.md +++ b/docs/sources/alerting/manage-notifications/mute-timings.md @@ -82,7 +82,7 @@ If you want to specify an exact duration, specify all the options. For example, {{% docs/reference %}} [datasources/alertmanager]: "/docs/grafana/ -> /docs/grafana//datasources/alertmanager" -[datasources/alertmanager]: "/docs/grafana-cloud/ -> /docs/grafana//datasources/alertmanager" +[datasources/alertmanager]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/connect-externally-hosted/data-sources/alertmanager" [fundamentals/alertmanager]: "/docs/grafana/ -> /docs/grafana//alerting/fundamentals/alertmanager" [fundamentals/alertmanager]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/fundamentals/alertmanager" diff --git a/docs/sources/alerting/set-up/_index.md b/docs/sources/alerting/set-up/_index.md index 250239f4e8..5a4092bf2d 100644 --- a/docs/sources/alerting/set-up/_index.md +++ b/docs/sources/alerting/set-up/_index.md @@ -54,7 +54,7 @@ Grafana Alerting supports many additional configuration options, from configurin The following topics provide you with advanced configuration options for Grafana Alerting. -- [Provision alert rules using file provisioning](/docs/grafana//alerting/set-up/provision-alerting-resources/file-provisioning) +- [Provision alert rules using file provisioning][file-provisioning] - [Provision alert rules using Terraform][terraform-provisioning] - [Add an external Alertmanager][configure-alertmanager] - [Configure high availability][configure-high-availability] @@ -69,8 +69,9 @@ The following topics provide you with advanced configuration options for Grafana [data-source-alerting]: "/docs/grafana/ -> /docs/grafana//alerting/fundamentals/data-source-alerting" [data-source-alerting]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/fundamentals/data-source-alerting" -[data-source-management]: "/docs/grafana/ -> /docs/grafana//administration/data-source-management" -[data-source-management]: "/docs/grafana-cloud/ -> /docs/grafana//administration/data-source-management" +[data-source-management]: "/docs/ -> /docs/grafana//administration/data-source-management" + +[file-provisioning]: "/docs/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/file-provisioning" [terraform-provisioning]: "/docs/grafana/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/terraform-provisioning" [terraform-provisioning]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/set-up/provision-alerting-resources/terraform-provisioning" diff --git a/docs/sources/alerting/set-up/migrating-alerts/legacy-alerting-deprecation.md b/docs/sources/alerting/set-up/migrating-alerts/legacy-alerting-deprecation.md index 8d9514acf8..d9955483b3 100644 --- a/docs/sources/alerting/set-up/migrating-alerts/legacy-alerting-deprecation.md +++ b/docs/sources/alerting/set-up/migrating-alerts/legacy-alerting-deprecation.md @@ -52,8 +52,7 @@ Refer to our [upgrade instructions][migrating-alerts]. - [Angular support deprecation][angular_deprecation] {{% docs/reference %}} -[angular_deprecation]: "/docs/grafana/ -> /docs/grafana//developers/angular_deprecation" -[angular_deprecation]: "/docs/grafana-cloud/ -> /docs/grafana//developers/angular_deprecation" +[angular_deprecation]: "/docs/ -> /docs/grafana//developers/angular_deprecation" [migrating-alerts]: "/docs/grafana/ -> /docs/grafana//alerting/set-up/migrating-alerts" [migrating-alerts]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/set-up/migrating-alerts" diff --git a/docs/sources/alerting/set-up/provision-alerting-resources/_index.md b/docs/sources/alerting/set-up/provision-alerting-resources/_index.md index 269d85aea3..ff3b8270fd 100644 --- a/docs/sources/alerting/set-up/provision-alerting-resources/_index.md +++ b/docs/sources/alerting/set-up/provision-alerting-resources/_index.md @@ -30,7 +30,7 @@ You cannot edit imported alerting resources in the Grafana UI in the same way as Choose from the options below to import (or provision) your Grafana Alerting resources. -1. [Use configuration files to provision your alerting resources](/docs/grafana//alerting/set-up/provision-alerting-resources/file-provisioning), such as alert rules and contact points, through files on disk. +1. [Use configuration files to provision your alerting resources][alerting_file_provisioning], such as alert rules and contact points, through files on disk. {{< admonition type="note" >}} File provisioning is not available in Grafana Cloud instances. @@ -67,15 +67,16 @@ Provisioned resources are labeled **Provisioned**, so that it is clear that they {{% docs/reference %}} [alerting_tf_provisioning]: "/docs/grafana/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/terraform-provisioning" -[alerting_tf_provisioning]: "/docs/grafana-cloud/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/terraform-provisioning" +[alerting_tf_provisioning]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/set-up/provision-alerting-resources/terraform-provisioning" [alerting_http_provisioning]: "/docs/grafana/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/http-api-provisioning" -[alerting_http_provisioning]: "/docs/grafana-cloud/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/http-api-provisioning" +[alerting_http_provisioning]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/set-up/provision-alerting-resources/http-api-provisioning" [alerting_export]: "/docs/grafana/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/export-alerting-resources" -[alerting_export]: "/docs/grafana-cloud/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/export-alerting-resources" +[alerting_export]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/set-up/provision-alerting-resources/export-alerting-resources" [alerting_export_http]: "/docs/grafana/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/export-alerting-resources#export-api-endpoints" -[alerting_export_http]: "/docs/grafana-cloud/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/export-alerting-resources#export-api-endpoints" +[alerting_export_http]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/set-up/provision-alerting-resources/export-alerting-resources#export-api-endpoints" -[provisioning]: "/docs/grafana/ -> /docs/grafana//administration/provisioning" -[provisioning]: "/docs/grafana-cloud/ -> /docs/grafana//administration/provisioning" +[alerting_file_provisioning]: "/docs/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/file-provisioning" + +[provisioning]: "/docs/ -> /docs/grafana//administration/provisioning" {{% /docs/reference %}} diff --git a/docs/sources/alerting/set-up/provision-alerting-resources/export-alerting-resources/index.md b/docs/sources/alerting/set-up/provision-alerting-resources/export-alerting-resources/index.md index d01f54a9fc..283a122d80 100644 --- a/docs/sources/alerting/set-up/provision-alerting-resources/export-alerting-resources/index.md +++ b/docs/sources/alerting/set-up/provision-alerting-resources/export-alerting-resources/index.md @@ -22,7 +22,7 @@ weight: 300 Export your alerting resources, such as alert rules, contact points, and notification policies for provisioning, automatically importing single folders and single groups. -The export options listed below enable you to download resources in YAML, JSON, or Terraform format, facilitating their provisioning through [configuration files](/docs/grafana//alerting/set-up/provision-alerting-resources/file-provisioning) or [Terraform][alerting_tf_provisioning]. +The export options listed below enable you to download resources in YAML, JSON, or Terraform format, facilitating their provisioning through [configuration files][alerting_file_provisioning] or [Terraform][alerting_tf_provisioning]. ## Export alert rules @@ -84,10 +84,12 @@ These endpoints accept a `download` parameter to download a file containing the {{% docs/reference %}} [alerting_tf_provisioning]: "/docs/grafana/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/terraform-provisioning" -[alerting_tf_provisioning]: "/docs/grafana-cloud/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/terraform-provisioning" +[alerting_tf_provisioning]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/set-up/provision-alerting-resources/terraform-provisioning" [alerting_http_provisioning]: "/docs/grafana/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/http-api-provisioning" -[alerting_http_provisioning]: "/docs/grafana-cloud/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/http-api-provisioning" +[alerting_http_provisioning]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/set-up/provision-alerting-resources/http-api-provisioning" + +[alerting_file_provisioning]: "/docs/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/file-provisioning" [export_rule]: "/docs/grafana/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/http-api-provisioning/#span-idroute-get-alert-rule-exportspan-export-an-alert-rule-in-provisioning-file-format-\_routegetalertruleexport*" [export_rule]: "/docs/grafana-cloud/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/http-api-provisioning/#span-idroute-get-alert-rule-exportspan-export-an-alert-rule-in-provisioning-file-format-\_routegetalertruleexport*" diff --git a/docs/sources/alerting/set-up/provision-alerting-resources/file-provisioning/index.md b/docs/sources/alerting/set-up/provision-alerting-resources/file-provisioning/index.md index b817314bbf..e0ce89688f 100644 --- a/docs/sources/alerting/set-up/provision-alerting-resources/file-provisioning/index.md +++ b/docs/sources/alerting/set-up/provision-alerting-resources/file-provisioning/index.md @@ -32,7 +32,7 @@ For a complete guide about how Grafana provisions resources, refer to the [Provi - You cannot edit provisioned resources from files in Grafana. You can only change the resource properties by changing the provisioning file and restarting Grafana or carrying out a hot reload. This prevents changes being made to the resource that would be overwritten if a file is provisioned again or a hot reload is carried out. -- Importing takes place during the initial set up of your Grafana system, but you can re-run it at any time using the [Grafana Admin API](/docs/grafana//developers/http_api/admin#reload-provisioning-configurations). +- Importing takes place during the initial set up of your Grafana system, but you can re-run it at any time using the [Grafana Admin API][reload-provisioning-configurations]. - Importing an existing alerting resource results in a conflict. First, when present, remove the resources you plan to import. {{< /admonition >}} @@ -813,7 +813,9 @@ This eliminates the need for a persistent database to use Grafana Alerting in Ku {{% docs/reference %}} [alerting_export]: "/docs/grafana/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/export-alerting-resources" -[alerting_export]: "/docs/grafana-cloud/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/export-alerting-resources" -[provisioning]: "/docs/grafana/ -> /docs/grafana//administration/provisioning" -[provisioning]: "/docs/grafana-cloud/ -> /docs/grafana//administration/provisioning" +[alerting_export]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/set-up/provision-alerting-resources/export-alerting-resources" + +[provisioning]: "/docs/ -> /docs/grafana//administration/provisioning" + +[reload-provisioning-configurations]: "/docs/ -> /docs/grafana//developers/http_api/admin#reload-provisioning-configurations" {{% /docs/reference %}} diff --git a/docs/sources/alerting/set-up/provision-alerting-resources/terraform-provisioning/index.md b/docs/sources/alerting/set-up/provision-alerting-resources/terraform-provisioning/index.md index f7bd77c872..936887d3e7 100644 --- a/docs/sources/alerting/set-up/provision-alerting-resources/terraform-provisioning/index.md +++ b/docs/sources/alerting/set-up/provision-alerting-resources/terraform-provisioning/index.md @@ -378,14 +378,13 @@ resource "grafana_mute_timing" "mute_all" { [alerting-rules]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/alerting-rules" [alerting_export]: "/docs/grafana/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/export-alerting-resources" -[alerting_export]: "/docs/grafana-cloud/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/export-alerting-resources" +[alerting_export]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/set-up/provision-alerting-resources/export-alerting-resources" [alerting_http_provisioning]: "/docs/grafana/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/http-api-provisioning" -[alerting_http_provisioning]: "/docs/grafana-cloud/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/http-api-provisioning" +[alerting_http_provisioning]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/set-up/provision-alerting-resources/http-api-provisioning" -[service-accounts]: "/docs/grafana/ -> /docs/grafana//administration/service-accounts" -[service-accounts]: "/docs/grafana-cloud/ -> /docs/grafana//administration/service-accounts" +[service-accounts]: "/docs/ -> /docs/grafana//administration/service-accounts" [testdata]: "/docs/grafana/ -> /docs/grafana//datasources/testdata" -[testdata]: "/docs/grafana-cloud/ -> /docs/grafana//datasources/testdata" +[testdata]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/connect-externally-hosted/data-sources/testdata" {{% /docs/reference %}} From f683ba8bfc6374d21799c0024ab20eba069181b3 Mon Sep 17 00:00:00 2001 From: Fabrizio <135109076+fabrizio-grafana@users.noreply.github.com> Date: Tue, 20 Feb 2024 12:21:38 +0100 Subject: [PATCH 0027/1406] Run downstream patch check only for `grafana/grafana` (#83050) --- .github/workflows/pr-patch-check.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/pr-patch-check.yml b/.github/workflows/pr-patch-check.yml index a7896130e3..ef1009b754 100644 --- a/.github/workflows/pr-patch-check.yml +++ b/.github/workflows/pr-patch-check.yml @@ -18,6 +18,7 @@ on: jobs: trigger_downstream_patch_check: uses: grafana/security-patch-actions/.github/workflows/test-patches.yml@main + if: github.repository == 'grafana/grafana' with: src_repo: "${{ github.repository }}" src_ref: "${{ github.head_ref }}" # this is the source branch name, Ex: "feature/newthing" From 8586893731aa200a5b10c07621988686abe10729 Mon Sep 17 00:00:00 2001 From: Yulia Shanyrova Date: Tue, 20 Feb 2024 12:48:51 +0100 Subject: [PATCH 0028/1406] Plugins: Fix enable button to appear after installing APP plugin (#82511) * Fix enable button to appear after installing APP plugin * add plugin isFullyInstalled check for grafana cloud --- .../GetStartedWithPlugin/GetStartedWithApp.tsx | 3 +-- .../app/features/plugins/admin/hooks/usePluginConfig.tsx | 9 ++++++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/public/app/features/plugins/admin/components/GetStartedWithPlugin/GetStartedWithApp.tsx b/public/app/features/plugins/admin/components/GetStartedWithPlugin/GetStartedWithApp.tsx index ea95689b71..92a8356b69 100644 --- a/public/app/features/plugins/admin/components/GetStartedWithPlugin/GetStartedWithApp.tsx +++ b/public/app/features/plugins/admin/components/GetStartedWithPlugin/GetStartedWithApp.tsx @@ -19,9 +19,8 @@ export function GetStartedWithApp({ plugin }: Props): React.ReactElement | null if (!pluginConfig) { return null; } - // Enforce RBAC - if (!contextSrv.hasPermissionInMetadata(AccessControlAction.PluginsWrite, plugin)) { + if (!contextSrv.hasPermission(AccessControlAction.PluginsWrite)) { return null; } diff --git a/public/app/features/plugins/admin/hooks/usePluginConfig.tsx b/public/app/features/plugins/admin/hooks/usePluginConfig.tsx index 4511b2dc39..bf28f69ecf 100644 --- a/public/app/features/plugins/admin/hooks/usePluginConfig.tsx +++ b/public/app/features/plugins/admin/hooks/usePluginConfig.tsx @@ -1,5 +1,7 @@ import { useAsync } from 'react-use'; +import { config } from '@grafana/runtime'; + import { loadPlugin } from '../../utils'; import { CatalogPlugin } from '../types'; @@ -9,7 +11,12 @@ export const usePluginConfig = (plugin?: CatalogPlugin) => { return null; } - if (plugin.isFullyInstalled && !plugin.isDisabled) { + const isPluginInstalled = + config.pluginAdminExternalManageEnabled && config.featureToggles.managedPluginsInstall + ? plugin.isFullyInstalled + : plugin.isInstalled; + + if (isPluginInstalled && !plugin.isDisabled) { return loadPlugin(plugin.id); } return null; From 5b918f555cc6f3d044677d80e9a7d48249549a81 Mon Sep 17 00:00:00 2001 From: Pepe Cano <825430+ppcano@users.noreply.github.com> Date: Tue, 20 Feb 2024 13:50:31 +0100 Subject: [PATCH 0029/1406] Alerting docs: correct `notification-policies` link (#83099) --- .../alerting/alerting-rules/create-notification-policy.md | 4 ++-- docs/sources/alerting/fundamentals/_index.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/sources/alerting/alerting-rules/create-notification-policy.md b/docs/sources/alerting/alerting-rules/create-notification-policy.md index 7c439f75a0..3df48d66ae 100644 --- a/docs/sources/alerting/alerting-rules/create-notification-policy.md +++ b/docs/sources/alerting/alerting-rules/create-notification-policy.md @@ -111,6 +111,6 @@ An example of an alert configuration. - Create specific routes for particular teams that handle their own on-call rotations. {{% docs/reference %}} -[notification-policies]: "/docs/grafana/ -> /docs/grafana//alerting/fundamentals/notification-policies" -[notification-policies]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/fundamentals/notification-policies" +[notification-policies]: "/docs/grafana/ -> /docs/grafana//alerting/fundamentals/notification-policies/notifications" +[notification-policies]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/fundamentals/notification-policies/notifications" {{% /docs/reference %}} diff --git a/docs/sources/alerting/fundamentals/_index.md b/docs/sources/alerting/fundamentals/_index.md index 1f86d0dc95..45c1427cf8 100644 --- a/docs/sources/alerting/fundamentals/_index.md +++ b/docs/sources/alerting/fundamentals/_index.md @@ -71,6 +71,6 @@ You can create your alerting resources (alert rules, notification policies, and {{% docs/reference %}} [external-alertmanagers]: "/docs/grafana/ -> /docs/grafana//alerting/set-up/configure-alertmanager" [external-alertmanagers]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/set-up/configure-alertmanager" -[notification-policies]: "/docs/grafana/ -> /docs/grafana//alerting/fundamentals/notification-policies" -[notification-policies]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/fundamentals/notification-policies" +[notification-policies]: "/docs/grafana/ -> /docs/grafana//alerting/fundamentals/notification-policies/notifications" +[notification-policies]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/fundamentals/notification-policies/notifications" {{% /docs/reference %}} From 5e65820beead89efc7248f9b3c7eb3556f92e51c Mon Sep 17 00:00:00 2001 From: Carl Bergquist Date: Tue, 20 Feb 2024 13:54:46 +0100 Subject: [PATCH 0030/1406] Cleanup root folder by moving a few files intro /contribute (#83103) Signed-off-by: bergquist --- .github/CODEOWNERS | 3 +-- ISSUE_TRIAGE.md => contribute/ISSUE_TRIAGE.md | 0 .../UPGRADING_DEPENDENCIES.md | 0 3 files changed, 1 insertion(+), 2 deletions(-) rename ISSUE_TRIAGE.md => contribute/ISSUE_TRIAGE.md (100%) rename UPGRADING_DEPENDENCIES.md => contribute/UPGRADING_DEPENDENCIES.md (100%) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index da322681cf..8e0daaf983 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -19,7 +19,6 @@ /CONTRIBUTING.md @grafana/grafana-community-support /GOVERNANCE.md @RichiH /HALL_OF_FAME.md @grafana/grafana-community-support -/ISSUE_TRIAGE.md @grafana/grafana-community-support /LICENSE @torkelo /LICENSING.md @torkelo /MAINTAINERS.md @RichiH @@ -28,9 +27,9 @@ /ROADMAP.md @torkelo /SECURITY.md @grafana/security-team /SUPPORT.md @torkelo -/UPGRADING_DEPENDENCIES.md @grafana/docs-grafana /WORKFLOW.md @torkelo /contribute/ @grafana/grafana-community-support +/contribute/UPGRADING_DEPENDENCIES.md @grafana/docs-grafana /devenv/README.md @grafana/docs-grafana # Technical documentation diff --git a/ISSUE_TRIAGE.md b/contribute/ISSUE_TRIAGE.md similarity index 100% rename from ISSUE_TRIAGE.md rename to contribute/ISSUE_TRIAGE.md diff --git a/UPGRADING_DEPENDENCIES.md b/contribute/UPGRADING_DEPENDENCIES.md similarity index 100% rename from UPGRADING_DEPENDENCIES.md rename to contribute/UPGRADING_DEPENDENCIES.md From 8138ca34a468d169134eddc8cc715646f534fec4 Mon Sep 17 00:00:00 2001 From: Lev Zakharov Date: Tue, 20 Feb 2024 15:58:47 +0300 Subject: [PATCH 0031/1406] Parca: Apply template variables for labelSelector in query (#82910) * Parca: Apply template variables for labelSelector in query * Remove unused imports --------- Co-authored-by: Joey Tawadrous --- .../datasource/parca/datasource.test.ts | 71 +++++++++++++++++++ .../plugins/datasource/parca/datasource.ts | 16 ++++- 2 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 public/app/plugins/datasource/parca/datasource.test.ts diff --git a/public/app/plugins/datasource/parca/datasource.test.ts b/public/app/plugins/datasource/parca/datasource.test.ts new file mode 100644 index 0000000000..1a0a216273 --- /dev/null +++ b/public/app/plugins/datasource/parca/datasource.test.ts @@ -0,0 +1,71 @@ +import { DataSourceInstanceSettings, PluginMetaInfo, PluginType } from '@grafana/data'; +import { getTemplateSrv } from '@grafana/runtime'; + +import { defaultParcaQueryType } from './dataquery.gen'; +import { ParcaDataSource } from './datasource'; +import { Query } from './types'; + +jest.mock('@grafana/runtime', () => { + const actual = jest.requireActual('@grafana/runtime'); + return { + ...actual, + getTemplateSrv: () => { + return { + replace: (query: string): string => { + return query.replace(/\$var/g, 'interpolated'); + }, + }; + }, + }; +}); + +describe('Parca data source', () => { + let ds: ParcaDataSource; + beforeEach(() => { + ds = new ParcaDataSource(defaultSettings); + }); + + describe('applyTemplateVariables', () => { + const templateSrv = getTemplateSrv(); + + it('should not update labelSelector if there are no template variables', () => { + ds = new ParcaDataSource(defaultSettings, templateSrv); + const query = ds.applyTemplateVariables(defaultQuery({ labelSelector: `no var` }), {}); + expect(query).toMatchObject({ labelSelector: `no var` }); + }); + + it('should update labelSelector if there are template variables', () => { + ds = new ParcaDataSource(defaultSettings, templateSrv); + const query = ds.applyTemplateVariables(defaultQuery({ labelSelector: `{$var="$var"}` }), {}); + expect(query).toMatchObject({ labelSelector: `{interpolated="interpolated"}` }); + }); + }); +}); + +const defaultSettings: DataSourceInstanceSettings = { + id: 0, + uid: 'parca', + type: 'profiling', + name: 'parca', + access: 'proxy', + meta: { + id: 'parca', + name: 'parca', + type: PluginType.datasource, + info: {} as PluginMetaInfo, + module: '', + baseUrl: '', + }, + jsonData: {}, + readOnly: false, +}; + +const defaultQuery = (query: Partial): Query => { + return { + refId: 'x', + labelSelector: '', + profileTypeId: '', + queryType: defaultParcaQueryType, + ...query, + }; +}; diff --git a/public/app/plugins/datasource/parca/datasource.ts b/public/app/plugins/datasource/parca/datasource.ts index 6418e15d6d..128e7c91d9 100644 --- a/public/app/plugins/datasource/parca/datasource.ts +++ b/public/app/plugins/datasource/parca/datasource.ts @@ -1,12 +1,15 @@ import { Observable, of } from 'rxjs'; -import { DataQueryRequest, DataQueryResponse, DataSourceInstanceSettings } from '@grafana/data'; -import { DataSourceWithBackend } from '@grafana/runtime'; +import { DataQueryRequest, DataQueryResponse, DataSourceInstanceSettings, ScopedVars } from '@grafana/data'; +import { DataSourceWithBackend, getTemplateSrv, TemplateSrv } from '@grafana/runtime'; import { ParcaDataSourceOptions, Query, ProfileTypeMessage } from './types'; export class ParcaDataSource extends DataSourceWithBackend { - constructor(instanceSettings: DataSourceInstanceSettings) { + constructor( + instanceSettings: DataSourceInstanceSettings, + private readonly templateSrv: TemplateSrv = getTemplateSrv() + ) { super(instanceSettings); } @@ -19,6 +22,13 @@ export class ParcaDataSource extends DataSourceWithBackend { return await super.getResource('profileTypes'); } From 1c8a2f136d7f177692f0ff70a0aaa47edb8bdb82 Mon Sep 17 00:00:00 2001 From: Timur Olzhabayev Date: Tue, 20 Feb 2024 14:18:32 +0100 Subject: [PATCH 0032/1406] Docs: making docs clearer on subpath (#82239) * making docs clearer on subpath * Update docs/sources/tutorials/run-grafana-behind-a-proxy/index.md Co-authored-by: Joseph Perez <45749060+josmperez@users.noreply.github.com> * Update docs/sources/tutorials/run-grafana-behind-a-proxy/index.md Co-authored-by: Joseph Perez <45749060+josmperez@users.noreply.github.com> * Update docs/sources/tutorials/run-grafana-behind-a-proxy/index.md Co-authored-by: Joseph Perez <45749060+josmperez@users.noreply.github.com> * Update docs/sources/tutorials/run-grafana-behind-a-proxy/index.md Co-authored-by: Joseph Perez <45749060+josmperez@users.noreply.github.com> --------- Co-authored-by: Joseph Perez <45749060+josmperez@users.noreply.github.com> --- .../run-grafana-behind-a-proxy/index.md | 57 ++++++++----------- 1 file changed, 24 insertions(+), 33 deletions(-) diff --git a/docs/sources/tutorials/run-grafana-behind-a-proxy/index.md b/docs/sources/tutorials/run-grafana-behind-a-proxy/index.md index a9ca71db3c..10008a7c04 100644 --- a/docs/sources/tutorials/run-grafana-behind-a-proxy/index.md +++ b/docs/sources/tutorials/run-grafana-behind-a-proxy/index.md @@ -34,23 +34,9 @@ domain = example.com - Restart Grafana for the new changes to take effect. -You can also serve Grafana behind a _sub path_, such as `http://example.com/grafana`. +## Configure reverse proxy -To serve Grafana behind a sub path: - -1. Include the sub path at the end of the `root_url`. -1. Set `serve_from_sub_path` to `true`. Or, let proxy rewrite the path for you (refer to examples below). - -```bash -[server] -domain = example.com -root_url = %(protocol)s://%(domain)s:%(http_port)s/grafana/ -serve_from_sub_path = true -``` - -Next, you need to configure your reverse proxy. - -## Configure NGINX +### Configure NGINX [NGINX](https://www.nginx.com) is a high performance load balancer, web server, and reverse proxy. @@ -129,7 +115,7 @@ server { } ``` -If your Grafana configuration does not set `serve_from_sub_path` to true then you need to add a rewrite rule to each location block: +Add a rewrite rule to each location block: ``` rewrite ^/grafana/(.*) /$1 break; @@ -139,7 +125,7 @@ If your Grafana configuration does not set `serve_from_sub_path` to true then yo If Grafana is being served from behind a NGINX proxy with TLS termination enabled, then the `root_url` should be set accordingly. For example, if Grafana is being served from `https://example.com/grafana` then the `root_url` should be set to `https://example.com/grafana/` or `https://%(domain)s/grafana/` (and the corresponding `domain` should be set to `example.com`) in the `server` section of the Grafana configuration file. The `protocol` setting should be set to `http`, because the TLS handshake is being handled by NGINX. {{% /admonition %}} -## Configure HAProxy +### Configure HAProxy To configure HAProxy to serve Grafana under a _sub path_: @@ -150,22 +136,15 @@ frontend http-in backend grafana_backend server grafana localhost:3000 -``` - -If your Grafana configuration doesn't set `server.serve_from_sub_path` to `true`, then you must add a rewrite rule to the `backend grafana_backend` block: - -```diff -backend grafana_backend -+ # Requires haproxy >= 1.6 -+ http-request set-path %[path,regsub(^/grafana/?,/)] - -+ # Works for haproxy < 1.6 -+ # reqrep ^([^\ ]*\ /)grafana[/]?(.*) \1\2 + # Requires haproxy >= 1.6 + http-request set-path %[path,regsub(^/grafana/?,/)] + # Works for haproxy < 1.6 + # reqrep ^([^\ ]*\ /)grafana[/]?(.*) \1\2 server grafana localhost:3000 ``` -## Configure IIS +### Configure IIS > IIS requires that the URL Rewrite module is installed. @@ -192,7 +171,7 @@ This is the rewrite rule that is generated in the `web.config`: See the [tutorial on IIS URL Rewrites](/tutorials/iis/) for more in-depth instructions. -## Configure Traefik +### Configure Traefik [Traefik](https://traefik.io/traefik/) Cloud Native Reverse Proxy / Load Balancer / Edge Router @@ -240,6 +219,18 @@ http: - url: http://192.168.30.10:3000 ``` -## Summary +## Alternative for serving Grafana under a sub path + +**Warning:** You only need this, if you do not handle the sub path serving via your reverse proxy configuration. + +If you don't want or can't use the reverse proxy to handle serving Grafana from a _sub path_, you can set the config variable `server_from_sub_path` to `true`. + +1. Include the sub path at the end of the `root_url`. +2. Set `serve_from_sub_path` to `true`: -In this tutorial you learned how to run Grafana behind a reverse proxy. +```bash +[server] +domain = example.com +root_url = %(protocol)s://%(domain)s:%(http_port)s/grafana/ +serve_from_sub_path = true +``` From 9d6da82e36b6fc1456e6112928cbbf41e41cbe4d Mon Sep 17 00:00:00 2001 From: Kristina Date: Tue, 20 Feb 2024 07:32:40 -0600 Subject: [PATCH 0033/1406] Query History: Count using SQL, not post query (#82208) * Count in SQL, not externally * Fix linter --- pkg/services/queryhistory/database.go | 10 ++++----- pkg/services/queryhistory/writers.go | 29 ++++++++++++++++++--------- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/pkg/services/queryhistory/database.go b/pkg/services/queryhistory/database.go index 3d780481a0..a6357d4801 100644 --- a/pkg/services/queryhistory/database.go +++ b/pkg/services/queryhistory/database.go @@ -45,7 +45,7 @@ func (s QueryHistoryService) createQuery(ctx context.Context, user *user.SignedI // searchQueries searches for queries in query history based on provided parameters func (s QueryHistoryService) searchQueries(ctx context.Context, user *user.SignedInUser, query SearchInQueryHistoryQuery) (QueryHistorySearchResult, error) { var dtos []QueryHistoryDTO - var allQueries []any + var totalCount int if query.To <= 0 { query.To = s.now().Unix() @@ -73,7 +73,7 @@ func (s QueryHistoryService) searchQueries(ctx context.Context, user *user.Signe query_history.comment, query_history.queries, `) - writeStarredSQL(query, s.store, &dtosBuilder) + writeStarredSQL(query, s.store, &dtosBuilder, false) writeFiltersSQL(query, user, s.store, &dtosBuilder) writeSortSQL(query, s.store, &dtosBuilder) writeLimitSQL(query, s.store, &dtosBuilder) @@ -87,9 +87,9 @@ func (s QueryHistoryService) searchQueries(ctx context.Context, user *user.Signe countBuilder := db.SQLBuilder{} countBuilder.Write(`SELECT `) - writeStarredSQL(query, s.store, &countBuilder) + writeStarredSQL(query, s.store, &countBuilder, true) writeFiltersSQL(query, user, s.store, &countBuilder) - err = session.SQL(countBuilder.GetSQLString(), countBuilder.GetParams()...).Find(&allQueries) + _, err = session.SQL(countBuilder.GetSQLString(), countBuilder.GetParams()...).Get(&totalCount) return err }) @@ -99,7 +99,7 @@ func (s QueryHistoryService) searchQueries(ctx context.Context, user *user.Signe response := QueryHistorySearchResult{ QueryHistory: dtos, - TotalCount: len(allQueries), + TotalCount: totalCount, Page: query.Page, PerPage: query.Limit, } diff --git a/pkg/services/queryhistory/writers.go b/pkg/services/queryhistory/writers.go index 00199f4716..b1b6806b64 100644 --- a/pkg/services/queryhistory/writers.go +++ b/pkg/services/queryhistory/writers.go @@ -8,18 +8,29 @@ import ( "github.com/grafana/grafana/pkg/services/user" ) -func writeStarredSQL(query SearchInQueryHistoryQuery, sqlStore db.DB, builder *db.SQLBuilder) { +func writeStarredSQL(query SearchInQueryHistoryQuery, sqlStore db.DB, builder *db.SQLBuilder, isCount bool) { + var sql bytes.Buffer + if isCount { + sql.WriteString(`COUNT(`) + } + if query.OnlyStarred { + sql.WriteString(sqlStore.GetDialect().BooleanStr(true)) + } else { + sql.WriteString(`CASE WHEN query_history_star.query_uid IS NULL THEN ` + sqlStore.GetDialect().BooleanStr(false) + ` ELSE ` + sqlStore.GetDialect().BooleanStr(true) + ` END`) + } + if isCount { + sql.WriteString(`)`) + } + sql.WriteString(` AS starred FROM query_history `) + if query.OnlyStarred { - builder.Write(sqlStore.GetDialect().BooleanStr(true) + ` AS starred - FROM query_history - INNER JOIN query_history_star ON query_history_star.query_uid = query_history.uid - `) + sql.WriteString(`INNER`) } else { - builder.Write(` CASE WHEN query_history_star.query_uid IS NULL THEN ` + sqlStore.GetDialect().BooleanStr(false) + ` ELSE ` + sqlStore.GetDialect().BooleanStr(true) + ` END AS starred - FROM query_history - LEFT JOIN query_history_star ON query_history_star.query_uid = query_history.uid - `) + sql.WriteString(`LEFT`) } + + sql.WriteString(` JOIN query_history_star ON query_history_star.query_uid = query_history.uid `) + builder.Write(sql.String()) } func writeFiltersSQL(query SearchInQueryHistoryQuery, user *user.SignedInUser, sqlStore db.DB, builder *db.SQLBuilder) { From f18b9ddac6ec46ed5eee5fda727e99322616123f Mon Sep 17 00:00:00 2001 From: Isabel <76437239+imatwawana@users.noreply.github.com> Date: Tue, 20 Feb 2024 08:46:38 -0500 Subject: [PATCH 0034/1406] Docs: add information about filtering for annotations (#82957) * Added information about filtering for annotations * Update generate-transformations.ts --- .../query-transform-data/transform-data/index.md | 4 +++- scripts/docs/generate-transformations.ts | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) 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 b7afd6ef35..e23162e277 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 @@ -101,7 +101,9 @@ You can disable or hide one or more transformations by clicking on the eye icon If your panel uses more than one query, you can filter these and apply the selected transformation to only one of the queries. To do this, click the filter icon on the top right of the transformation row. This opens a drop-down with a list of queries used on the panel. From here, you can select the query you want to transform. -Note that the filter icon is always displayed if your panel has more than one query, but it may not work if previous transformations for merging the queries' outputs are applied. This is because one transformation takes the output of the previous one. +You can also filter by annotations (which includes exemplars) to apply transformations to them. When you do so, the list of fields changes to reflect those in the annotation or exemplar tooltip. + +The filter icon is always displayed if your panel has more than one query or source of data (that is, panel or annotation data) but it may not work if previous transformations for merging the queries’ outputs are applied. This is because one transformation takes the output of the previous one. ## Delete a transformation diff --git a/scripts/docs/generate-transformations.ts b/scripts/docs/generate-transformations.ts index 321344c731..0e9cabcb02 100644 --- a/scripts/docs/generate-transformations.ts +++ b/scripts/docs/generate-transformations.ts @@ -107,7 +107,9 @@ You can disable or hide one or more transformations by clicking on the eye icon If your panel uses more than one query, you can filter these and apply the selected transformation to only one of the queries. To do this, click the filter icon on the top right of the transformation row. This opens a drop-down with a list of queries used on the panel. From here, you can select the query you want to transform. -Note that the filter icon is always displayed if your panel has more than one query, but it may not work if previous transformations for merging the queries' outputs are applied. This is because one transformation takes the output of the previous one. +You can also filter by annotations (which includes exemplars) to apply transformations to them. When you do so, the list of fields changes to reflect those in the annotation or exemplar tooltip. + +The filter icon is always displayed if your panel has more than one query or source of data (that is, panel or annotation data) but it may not work if previous transformations for merging the queries’ outputs are applied. This is because one transformation takes the output of the previous one. ## Delete a transformation From 5431c51490465cbed0f2f7267f484a1422ec39b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ida=20=C5=A0tambuk?= Date: Tue, 20 Feb 2024 14:52:11 +0100 Subject: [PATCH 0035/1406] Cloudwatch: Add linting to restrict imports from core (#82538) --------- Co-authored-by: Kevin Yu --- .eslintrc | 4 +++- .golangci.toml | 2 ++ pkg/tsdb/cloudwatch/cloudwatch.go | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.eslintrc b/.eslintrc index e8cfd52260..7624896887 100644 --- a/.eslintrc +++ b/.eslintrc @@ -115,7 +115,9 @@ "public/app/plugins/datasource/loki/*.{ts,tsx}", "public/app/plugins/datasource/loki/**/*.{ts,tsx}", "public/app/plugins/datasource/elasticsearch/*.{ts,tsx}", - "public/app/plugins/datasource/elasticsearch/**/*.{ts,tsx}" + "public/app/plugins/datasource/elasticsearch/**/*.{ts,tsx}", + "public/app/plugins/datasource/cloudwatch/*.{ts,tsx}", + "public/app/plugins/datasource/cloudwatch/**/*.{ts,tsx}" ], "settings": { "import/resolver": { diff --git a/.golangci.toml b/.golangci.toml index ef893498d7..b722776e84 100644 --- a/.golangci.toml +++ b/.golangci.toml @@ -67,6 +67,8 @@ files = [ "**/pkg/tsdb/parca/**/*", "**/pkg/tsdb/tempo/*", "**/pkg/tsdb/tempo/**/*", + "**/pkg/tsdb/cloudwatch/*", + "**/pkg/tsdb/cloudwatch/**/*", ] [linters-settings.gocritic] diff --git a/pkg/tsdb/cloudwatch/cloudwatch.go b/pkg/tsdb/cloudwatch/cloudwatch.go index dc97554323..735c65d217 100644 --- a/pkg/tsdb/cloudwatch/cloudwatch.go +++ b/pkg/tsdb/cloudwatch/cloudwatch.go @@ -122,7 +122,7 @@ func NewInstanceSettings(httpClientProvider *httpclient.Provider) datasource.Ins } } -// cloudWatchExecutor executes CloudWatch requests. +// cloudWatchExecutor executes CloudWatch requests type cloudWatchExecutor struct { im instancemgmt.InstanceManager sessions SessionCache From c237a39020898a3da5d4f8d5a9fc5220892d7758 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Fern=C3=A1ndez?= Date: Tue, 20 Feb 2024 15:05:12 +0100 Subject: [PATCH 0036/1406] ReturnToPrevious: Check the state of the RTP feature toggle in the hook (#83087) --- .../components/AppChrome/AppChromeService.tsx | 8 ++++ .../rules/RuleDetailsActionButtons.tsx | 39 +++++++------------ 2 files changed, 22 insertions(+), 25 deletions(-) diff --git a/public/app/core/components/AppChrome/AppChromeService.tsx b/public/app/core/components/AppChrome/AppChromeService.tsx index 810ce5336c..c6bd540245 100644 --- a/public/app/core/components/AppChrome/AppChromeService.tsx +++ b/public/app/core/components/AppChrome/AppChromeService.tsx @@ -90,6 +90,10 @@ export class AppChromeService { } public setReturnToPrevious = (returnToPrevious: ReturnToPreviousProps) => { + const isReturnToPreviousEnabled = config.featureToggles.returnToPrevious; + if (!isReturnToPreviousEnabled) { + return; + } const previousPage = this.state.getValue().returnToPrevious; reportInteraction('grafana_return_to_previous_button_created', { page: returnToPrevious.href, @@ -101,6 +105,10 @@ export class AppChromeService { }; public clearReturnToPrevious = (interactionAction: 'clicked' | 'dismissed' | 'auto_dismissed') => { + const isReturnToPreviousEnabled = config.featureToggles.returnToPrevious; + if (!isReturnToPreviousEnabled) { + return; + } const existingRtp = this.state.getValue().returnToPrevious; if (existingRtp) { reportInteraction('grafana_return_to_previous_button_dismissed', { diff --git a/public/app/features/alerting/unified/components/rules/RuleDetailsActionButtons.tsx b/public/app/features/alerting/unified/components/rules/RuleDetailsActionButtons.tsx index 48bbf05603..ca3fed1f82 100644 --- a/public/app/features/alerting/unified/components/rules/RuleDetailsActionButtons.tsx +++ b/public/app/features/alerting/unified/components/rules/RuleDetailsActionButtons.tsx @@ -135,33 +135,22 @@ export const RuleDetailsActionButtons = ({ rule, rulesSource, isViewMode }: Prop } if (rule.annotations[Annotation.dashboardUID]) { const dashboardUID = rule.annotations[Annotation.dashboardUID]; + const isReturnToPreviousEnabled = config.featureToggles.returnToPrevious; if (dashboardUID) { buttons.push( - config.featureToggles.returnToPrevious ? ( - { - setReturnToPrevious(rule.name); - }} - > - Go to dashboard - - ) : ( - - Go to dashboard - - ) + { + setReturnToPrevious(rule.name); + }} + > + Go to dashboard + ); const panelId = rule.annotations[Annotation.panelID]; if (panelId) { From 8c963ad90cc93ba950b34657edad1d7ed96c24c0 Mon Sep 17 00:00:00 2001 From: Jack Baldry Date: Tue, 20 Feb 2024 14:05:55 +0000 Subject: [PATCH 0037/1406] Use updated default branch for links to Grafana repository (#83026) --- docs/sources/release-notes/release-notes-7-4-0.md | 2 +- docs/sources/whatsnew/whats-new-in-v10-0.md | 2 +- docs/sources/whatsnew/whats-new-in-v10-1.md | 2 +- docs/sources/whatsnew/whats-new-in-v10-2.md | 2 +- docs/sources/whatsnew/whats-new-in-v10-3.md | 2 +- docs/sources/whatsnew/whats-new-in-v7-0.md | 4 ++-- docs/sources/whatsnew/whats-new-in-v7-1.md | 4 ++-- docs/sources/whatsnew/whats-new-in-v7-2.md | 4 ++-- docs/sources/whatsnew/whats-new-in-v7-3.md | 4 ++-- docs/sources/whatsnew/whats-new-in-v7-4.md | 4 ++-- docs/sources/whatsnew/whats-new-in-v7-5.md | 2 +- docs/sources/whatsnew/whats-new-in-v8-0.md | 2 +- docs/sources/whatsnew/whats-new-in-v8-1.md | 2 +- docs/sources/whatsnew/whats-new-in-v8-2.md | 2 +- docs/sources/whatsnew/whats-new-in-v8-3.md | 2 +- docs/sources/whatsnew/whats-new-in-v8-4.md | 2 +- docs/sources/whatsnew/whats-new-in-v9-1.md | 2 +- docs/sources/whatsnew/whats-new-in-v9-2.md | 2 +- docs/sources/whatsnew/whats-new-in-v9-3.md | 2 +- docs/sources/whatsnew/whats-new-in-v9-4.md | 2 +- docs/sources/whatsnew/whats-new-in-v9-5.md | 2 +- 21 files changed, 26 insertions(+), 26 deletions(-) diff --git a/docs/sources/release-notes/release-notes-7-4-0.md b/docs/sources/release-notes/release-notes-7-4-0.md index 57e16aca6f..7f40ad03db 100644 --- a/docs/sources/release-notes/release-notes-7-4-0.md +++ b/docs/sources/release-notes/release-notes-7-4-0.md @@ -163,7 +163,7 @@ Issue [#29407](https://github.com/grafana/grafana/issues/29407) We have upgraded AngularJS from version 1.6.6 to 1.8.2. Due to this upgrade some old angular plugins might stop working and will require a small update. This is due to the deprecation and removal of pre-assigned bindings. So if your custom angular controllers expect component bindings in the controller constructor you need to move this code to an `$onInit` function. For more details on how to migrate AngularJS code open the [migration guide](https://docs.angularjs.org/guide/migration) and search for **pre-assigning bindings**. -In order not to break all angular panel plugins and data sources we have some custom [angular inject behavior](https://github.com/grafana/grafana/blob/master/public/app/core/injectorMonkeyPatch.ts) that makes sure that bindings for these controllers are still set before constructor is called so many old angular panels and data source plugins will still work. Issue [#28736](https://github.com/grafana/grafana/issues/28736) +In order not to break all angular panel plugins and data sources we have some custom [angular inject behavior](https://github.com/grafana/grafana/blob/main/public/app/core/injectorMonkeyPatch.ts) that makes sure that bindings for these controllers are still set before constructor is called so many old angular panels and data source plugins will still work. Issue [#28736](https://github.com/grafana/grafana/issues/28736) ### Deprecations diff --git a/docs/sources/whatsnew/whats-new-in-v10-0.md b/docs/sources/whatsnew/whats-new-in-v10-0.md index 8fd36a5bca..4ed11cd5d7 100644 --- a/docs/sources/whatsnew/whats-new-in-v10-0.md +++ b/docs/sources/whatsnew/whats-new-in-v10-0.md @@ -19,7 +19,7 @@ weight: -37 Welcome to Grafana 10.0! Read on to learn about changes to search and navigation, dashboards and visualizations, and security and authentication. -For even more detail about all the changes in this release, refer to the [changelog](https://github.com/grafana/grafana/blob/master/CHANGELOG.md). For the specific steps we recommend when you upgrade to v10.0, check out our [Upgrade Guide]({{< relref "../upgrade-guide/upgrade-v10.0/index.md" >}}). +For even more detail about all the changes in this release, refer to the [changelog](https://github.com/grafana/grafana/blob/main/CHANGELOG.md). For the specific steps we recommend when you upgrade to v10.0, check out our [Upgrade Guide]({{< relref "../upgrade-guide/upgrade-v10.0/index.md" >}}). + {{% docs/reference %}} [alerting_tf_provisioning]: "/docs/grafana/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/terraform-provisioning" [alerting_tf_provisioning]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/set-up/provision-alerting-resources/terraform-provisioning" @@ -91,18 +93,20 @@ These endpoints accept a `download` parameter to download a file containing the [alerting_file_provisioning]: "/docs/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/file-provisioning" -[export_rule]: "/docs/grafana/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/http-api-provisioning/#span-idroute-get-alert-rule-exportspan-export-an-alert-rule-in-provisioning-file-format-\_routegetalertruleexport*" -[export_rule]: "/docs/grafana-cloud/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/http-api-provisioning/#span-idroute-get-alert-rule-exportspan-export-an-alert-rule-in-provisioning-file-format-\_routegetalertruleexport*" +[export_rule]: "/docs/grafana/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/http-api-provisioning/#span-idroute-get-alert-rule-exportspan-export-an-alert-rule-in-provisioning-file-format-_routegetalertruleexport_" +[export_rule]: "/docs/grafana-cloud/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/http-api-provisioning/#span-idroute-get-alert-rule-exportspan-export-an-alert-rule-in-provisioning-file-format-_routegetalertruleexport_" -[export_rule_group]: "/docs/grafana/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/http-api-provisioning/#span-idroute-get-alert-rule-group-exportspan-export-an-alert-rule-group-in-provisioning-file-format-\_routegetalertrulegroupexport*" -[export_rule_group]: "/docs/grafana-cloud/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/http-api-provisioning/#span-idroute-get-alert-rule-group-exportspan-export-an-alert-rule-group-in-provisioning-file-format-\_routegetalertrulegroupexport*" +[export_rule_group]: "/docs/grafana/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/http-api-provisioning/#span-idroute-get-alert-rule-group-exportspan-export-an-alert-rule-group-in-provisioning-file-format-_routegetalertrulegroupexport_" +[export_rule_group]: "/docs/grafana-cloud/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/http-api-provisioning/#span-idroute-get-alert-rule-group-exportspan-export-an-alert-rule-group-in-provisioning-file-format-_routegetalertrulegroupexport_" -[export_rules]: "/docs/grafana/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/http-api-provisioning/#span-idroute-get-alert-rules-exportspan-export-all-alert-rules-in-provisioning-file-format-\_routegetalertrulesexport*" -[export_rules]: "/docs/grafana-cloud/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/http-api-provisioning/#span-idroute-get-alert-rules-exportspan-export-all-alert-rules-in-provisioning-file-format-\_routegetalertrulesexport*" +[export_rules]: "/docs/grafana/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/http-api-provisioning/#span-idroute-get-alert-rules-exportspan-export-all-alert-rules-in-provisioning-file-format-_routegetalertrulesexport_" +[export_rules]: "/docs/grafana-cloud/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/http-api-provisioning/#span-idroute-get-alert-rules-exportspan-export-all-alert-rules-in-provisioning-file-format-_routegetalertrulesexport_" -[export_contacts]: "/docs/grafana/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/http-api-provisioning/#span-idroute-get-contactpoints-exportspan-export-all-contact-points-in-provisioning-file-format-\_routegetcontactpointsexport*" -[export_contacts]: "/docs/grafana-cloud/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/http-api-provisioning/#span-idroute-get-contactpoints-exportspan-export-all-contact-points-in-provisioning-file-format-\_routegetcontactpointsexport*" +[export_contacts]: "/docs/grafana/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/http-api-provisioning/#span-idroute-get-contactpoints-exportspan-export-all-contact-points-in-provisioning-file-format-_routegetcontactpointsexport_" +[export_contacts]: "/docs/grafana-cloud/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/http-api-provisioning/#span-idroute-get-contactpoints-exportspan-export-all-contact-points-in-provisioning-file-format-_routegetcontactpointsexport_" -[export_notifications]: "/docs/grafana/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/http-api-provisioning/#span-idroute-get-policy-tree-exportspan-export-the-notification-policy-tree-in-provisioning-file-format-\_routegetpolicytreeexport*" -[export_notifications]: "/docs/grafana-cloud/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/http-api-provisioning/#span-idroute-get-policy-tree-exportspan-export-the-notification-policy-tree-in-provisioning-file-format-\_routegetpolicytreeexport*" +[export_notifications]: "/docs/grafana/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/http-api-provisioning/#span-idroute-get-policy-tree-exportspan-export-the-notification-policy-tree-in-provisioning-file-format-_routegetpolicytreeexport_" +[export_notifications]: "/docs/grafana-cloud/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/http-api-provisioning/#span-idroute-get-policy-tree-exportspan-export-the-notification-policy-tree-in-provisioning-file-format-_routegetpolicytreeexport_" {{% /docs/reference %}} + + From 1352072338a063c77668a187402729685f1ff713 Mon Sep 17 00:00:00 2001 From: Wilbert Guo Date: Wed, 21 Feb 2024 01:55:06 -0800 Subject: [PATCH 0058/1406] Cloudwatch: Fix filter button issue in Variable Editor (#83082) --- .../components/VariableQueryEditor/VariableQueryEditor.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/datasource/cloudwatch/components/VariableQueryEditor/VariableQueryEditor.tsx b/public/app/plugins/datasource/cloudwatch/components/VariableQueryEditor/VariableQueryEditor.tsx index f333cab612..7d4eb75a70 100644 --- a/public/app/plugins/datasource/cloudwatch/components/VariableQueryEditor/VariableQueryEditor.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/VariableQueryEditor/VariableQueryEditor.tsx @@ -267,7 +267,7 @@ export const VariableQueryEditor = ({ query, datasource, onChange }: Props) => { } > { onChange({ ...parsedQuery, ec2Filters: filters }); }} @@ -294,7 +294,7 @@ export const VariableQueryEditor = ({ query, datasource, onChange }: Props) => { } > { onChange({ ...parsedQuery, ec2Filters: filters }); }} From 778d80f922aa3a36d8dc45649a2061ad294288b0 Mon Sep 17 00:00:00 2001 From: Jack Westbrook Date: Wed, 21 Feb 2024 11:02:16 +0100 Subject: [PATCH 0059/1406] Chore: Remove React from o11y dependencies and align zipkin and tempo dependencies (#83143) chore(zipkin): remove react from o11y dependencies, tidy tempo and zipkin dependencies --- .../grafana-o11y-ds-frontend/package.json | 3 +- .../app/plugins/datasource/tempo/package.json | 1 - .../plugins/datasource/zipkin/package.json | 12 +- yarn.lock | 145 +----------------- 4 files changed, 14 insertions(+), 147 deletions(-) diff --git a/packages/grafana-o11y-ds-frontend/package.json b/packages/grafana-o11y-ds-frontend/package.json index 725f961dd7..2ffab0e1a7 100644 --- a/packages/grafana-o11y-ds-frontend/package.json +++ b/packages/grafana-o11y-ds-frontend/package.json @@ -24,13 +24,12 @@ "@grafana/runtime": "11.0.0-pre", "@grafana/schema": "11.0.0-pre", "@grafana/ui": "11.0.0-pre", - "react": "18.2.0", "react-use": "17.5.0", "rxjs": "7.8.1", "tslib": "2.6.2" }, "devDependencies": { - "@grafana/tsconfig": "^1.2.0-rc1", + "@grafana/tsconfig": "^1.3.0-rc1", "@testing-library/jest-dom": "^6.1.2", "@testing-library/react": "14.2.1", "@testing-library/user-event": "14.5.2", diff --git a/public/app/plugins/datasource/tempo/package.json b/public/app/plugins/datasource/tempo/package.json index 710f04e0dc..80adea389e 100644 --- a/public/app/plugins/datasource/tempo/package.json +++ b/public/app/plugins/datasource/tempo/package.json @@ -30,7 +30,6 @@ "prismjs": "1.29.0", "react": "18.2.0", "react-dom": "18.2.0", - "react-router": "6.21.3", "react-use": "17.5.0", "redux": "4.2.1", "rxjs": "7.8.1", diff --git a/public/app/plugins/datasource/zipkin/package.json b/public/app/plugins/datasource/zipkin/package.json index 921b897604..65f01284a5 100644 --- a/public/app/plugins/datasource/zipkin/package.json +++ b/public/app/plugins/datasource/zipkin/package.json @@ -6,7 +6,7 @@ "dependencies": { "@emotion/css": "11.11.2", "@grafana/data": "workspace:*", - "@grafana/experimental": "1.7.8", + "@grafana/experimental": "1.7.10", "@grafana/o11y-ds-frontend": "workspace:*", "@grafana/runtime": "workspace:*", "@grafana/ui": "workspace:*", @@ -18,13 +18,13 @@ }, "devDependencies": { "@grafana/plugin-configs": "workspace:*", - "@testing-library/jest-dom": "6.3.0", - "@testing-library/react": "14.1.2", - "@types/jest": "29.5.11", + "@testing-library/jest-dom": "6.4.2", + "@testing-library/react": "14.2.1", + "@types/jest": "29.5.12", "@types/lodash": "4.14.202", - "@types/react": "18.2.48", + "@types/react": "18.2.55", "ts-node": "10.9.2", - "webpack": "5.90.0" + "webpack": "5.90.2" }, "peerDependencies": { "@grafana/runtime": "*" diff --git a/yarn.lock b/yarn.lock index 80a0878313..8eb8ac7371 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3467,7 +3467,6 @@ __metadata: prismjs: "npm:1.29.0" react: "npm:18.2.0" react-dom: "npm:18.2.0" - react-router: "npm:6.21.3" react-select-event: "npm:5.5.1" react-use: "npm:17.5.0" redux: "npm:4.2.1" @@ -3491,23 +3490,23 @@ __metadata: dependencies: "@emotion/css": "npm:11.11.2" "@grafana/data": "workspace:*" - "@grafana/experimental": "npm:1.7.8" + "@grafana/experimental": "npm:1.7.10" "@grafana/o11y-ds-frontend": "workspace:*" "@grafana/plugin-configs": "workspace:*" "@grafana/runtime": "workspace:*" "@grafana/ui": "workspace:*" - "@testing-library/jest-dom": "npm:6.3.0" - "@testing-library/react": "npm:14.1.2" - "@types/jest": "npm:29.5.11" + "@testing-library/jest-dom": "npm:6.4.2" + "@testing-library/react": "npm:14.2.1" + "@types/jest": "npm:29.5.12" "@types/lodash": "npm:4.14.202" - "@types/react": "npm:18.2.48" + "@types/react": "npm:18.2.55" lodash: "npm:4.17.21" react: "npm:18.2.0" react-use: "npm:17.5.0" rxjs: "npm:7.8.1" ts-node: "npm:10.9.2" tslib: "npm:2.6.2" - webpack: "npm:5.90.0" + webpack: "npm:5.90.2" peerDependencies: "@grafana/runtime": "*" languageName: unknown @@ -3750,31 +3749,6 @@ __metadata: languageName: node linkType: hard -"@grafana/experimental@npm:1.7.8": - version: 1.7.8 - resolution: "@grafana/experimental@npm:1.7.8" - dependencies: - "@types/uuid": "npm:^8.3.3" - lodash: "npm:^4.17.21" - prismjs: "npm:^1.29.0" - react-beautiful-dnd: "npm:^13.1.1" - react-popper-tooltip: "npm:^4.4.2" - react-use: "npm:^17.4.2" - semver: "npm:^7.5.4" - uuid: "npm:^8.3.2" - peerDependencies: - "@emotion/css": 11.11.2 - "@grafana/data": ^10.0.0 - "@grafana/runtime": ^10.0.0 - "@grafana/ui": ^10.0.0 - react: 17.0.2 - react-dom: 17.0.2 - react-select: ^5.2.1 - rxjs: 7.8.0 - checksum: 10/6bcf4a04b07fb1a34f7fa5332c0ab10760675e153d6d855b49f3cfe9fdc1b63223162aa82bac569b30f10ad4b2c434058bc03d50c650f50f86ddad01de1f4cce - languageName: node - linkType: hard - "@grafana/faro-core@npm:^1.3.6, @grafana/faro-core@npm:^1.3.7": version: 1.3.7 resolution: "@grafana/faro-core@npm:1.3.7" @@ -3911,7 +3885,7 @@ __metadata: "@grafana/experimental": "npm:1.7.10" "@grafana/runtime": "npm:11.0.0-pre" "@grafana/schema": "npm:11.0.0-pre" - "@grafana/tsconfig": "npm:^1.2.0-rc1" + "@grafana/tsconfig": "npm:^1.3.0-rc1" "@grafana/ui": "npm:11.0.0-pre" "@testing-library/jest-dom": "npm:^6.1.2" "@testing-library/react": "npm:14.2.1" @@ -8707,39 +8681,6 @@ __metadata: languageName: node linkType: hard -"@testing-library/jest-dom@npm:6.3.0": - version: 6.3.0 - resolution: "@testing-library/jest-dom@npm:6.3.0" - dependencies: - "@adobe/css-tools": "npm:^4.3.2" - "@babel/runtime": "npm:^7.9.2" - aria-query: "npm:^5.0.0" - chalk: "npm:^3.0.0" - css.escape: "npm:^1.5.1" - dom-accessibility-api: "npm:^0.6.3" - lodash: "npm:^4.17.15" - redent: "npm:^3.0.0" - peerDependencies: - "@jest/globals": ">= 28" - "@types/bun": "*" - "@types/jest": ">= 28" - jest: ">= 28" - vitest: ">= 0.32" - peerDependenciesMeta: - "@jest/globals": - optional: true - "@types/bun": - optional: true - "@types/jest": - optional: true - jest: - optional: true - vitest: - optional: true - checksum: 10/d96e552cfe5a72fa0a4c21655a9fabe6ffce6a066323c8a0f5847f39ff88229cd2455c9af41d3381b672d65469e74752d29e35dd04c15d8241a9f6a1e7cb78c6 - languageName: node - linkType: hard - "@testing-library/jest-dom@npm:6.4.2, @testing-library/jest-dom@npm:^6.1.2": version: 6.4.2 resolution: "@testing-library/jest-dom@npm:6.4.2" @@ -8795,20 +8736,6 @@ __metadata: languageName: node linkType: hard -"@testing-library/react@npm:14.1.2": - version: 14.1.2 - resolution: "@testing-library/react@npm:14.1.2" - dependencies: - "@babel/runtime": "npm:^7.12.5" - "@testing-library/dom": "npm:^9.0.0" - "@types/react-dom": "npm:^18.0.0" - peerDependencies: - react: ^18.0.0 - react-dom: ^18.0.0 - checksum: 10/1664990ad9673403ee1d74c1c1b60ec30591d42a3fe1e2175c28cb935cd49bc9a4ba398707f702acc3278c3b0cb492ee57fe66f41ceb040c5da57de98cba5414 - languageName: node - linkType: hard - "@testing-library/react@npm:14.2.1": version: 14.2.1 resolution: "@testing-library/react@npm:14.2.1" @@ -9648,16 +9575,6 @@ __metadata: languageName: node linkType: hard -"@types/jest@npm:29.5.11": - version: 29.5.11 - resolution: "@types/jest@npm:29.5.11" - dependencies: - expect: "npm:^29.0.0" - pretty-format: "npm:^29.0.0" - checksum: 10/798f4c89407d9457bea1256de74c26f2b279f6c789c0e3311cd604cc75cdab333b9a29b00c51b0090d31abdf11cc788b4103282851a653bef6daf72edf97eef2 - languageName: node - linkType: hard - "@types/jquery@npm:3.5.29": version: 3.5.29 resolution: "@types/jquery@npm:3.5.29" @@ -10158,17 +10075,6 @@ __metadata: languageName: node linkType: hard -"@types/react@npm:18.2.48": - version: 18.2.48 - resolution: "@types/react@npm:18.2.48" - dependencies: - "@types/prop-types": "npm:*" - "@types/scheduler": "npm:*" - csstype: "npm:^3.0.2" - checksum: 10/2e56ea6bd821ae96bd943f727a59d85384eaf5f8a3e6fce4fa1d34453e32d8eedda742432b3857fa0de7a4214bf84ce4239757eb52918e76452c00384731e585 - languageName: node - linkType: hard - "@types/reactcss@npm:*": version: 1.2.6 resolution: "@types/reactcss@npm:1.2.6" @@ -31635,43 +31541,6 @@ __metadata: languageName: node linkType: hard -"webpack@npm:5.90.0": - version: 5.90.0 - resolution: "webpack@npm:5.90.0" - dependencies: - "@types/eslint-scope": "npm:^3.7.3" - "@types/estree": "npm:^1.0.5" - "@webassemblyjs/ast": "npm:^1.11.5" - "@webassemblyjs/wasm-edit": "npm:^1.11.5" - "@webassemblyjs/wasm-parser": "npm:^1.11.5" - acorn: "npm:^8.7.1" - acorn-import-assertions: "npm:^1.9.0" - browserslist: "npm:^4.21.10" - chrome-trace-event: "npm:^1.0.2" - enhanced-resolve: "npm:^5.15.0" - es-module-lexer: "npm:^1.2.1" - eslint-scope: "npm:5.1.1" - events: "npm:^3.2.0" - glob-to-regexp: "npm:^0.4.1" - graceful-fs: "npm:^4.2.9" - json-parse-even-better-errors: "npm:^2.3.1" - loader-runner: "npm:^4.2.0" - mime-types: "npm:^2.1.27" - neo-async: "npm:^2.6.2" - schema-utils: "npm:^3.2.0" - tapable: "npm:^2.1.1" - terser-webpack-plugin: "npm:^5.3.10" - watchpack: "npm:^2.4.0" - webpack-sources: "npm:^3.2.3" - peerDependenciesMeta: - webpack-cli: - optional: true - bin: - webpack: bin/webpack.js - checksum: 10/7ff6286be54e00b2580274d8009b014fd03c6d8ade898434376c739e460da1f3a63a51006966024710061f440d6723813365b8a54ae6bcb93b94867c42cf017e - languageName: node - linkType: hard - "webpack@npm:5.90.2": version: 5.90.2 resolution: "webpack@npm:5.90.2" From a62dccb0c045a0c0cd581e8ed55d0568e80e96a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Farkas?= Date: Wed, 21 Feb 2024 11:32:10 +0100 Subject: [PATCH 0060/1406] plugins: add more configuration parameters to the plugin-config (#83060) * envvars: add more configs * made row-limit optional * more consistent naming --- pkg/plugins/config/config.go | 72 +++++++++++-------- pkg/plugins/envvars/envvars.go | 14 ++++ pkg/plugins/envvars/envvars_test.go | 48 +++++++++++++ .../pluginsintegration/config/config.go | 5 ++ 4 files changed, 111 insertions(+), 28 deletions(-) diff --git a/pkg/plugins/config/config.go b/pkg/plugins/config/config.go index 86be201158..3c5d8908ad 100644 --- a/pkg/plugins/config/config.go +++ b/pkg/plugins/config/config.go @@ -54,39 +54,55 @@ type Cfg struct { HideAngularDeprecation []string ConcurrentQueryCount int + + UserFacingDefaultError string + + DataProxyRowLimit int64 + + SQLDatasourceMaxOpenConnsDefault int + SQLDatasourceMaxIdleConnsDefault int + SQLDatasourceMaxConnLifetimeDefault int } func NewCfg(devMode bool, pluginsPath string, pluginSettings setting.PluginSettings, pluginsAllowUnsigned []string, awsAllowedAuthProviders []string, awsAssumeRoleEnabled bool, awsExternalId string, awsSessionDuration string, awsListMetricsPageLimit string, AWSForwardSettingsPlugins []string, azure *azsettings.AzureSettings, secureSocksDSProxy setting.SecureSocksDSProxySettings, grafanaVersion string, logDatasourceRequests bool, pluginsCDNURLTemplate string, appURL string, appSubURL string, tracing Tracing, features featuremgmt.FeatureToggles, angularSupportEnabled bool, - grafanaComURL string, disablePlugins []string, hideAngularDeprecation []string, forwardHostEnvVars []string, concurrentQueryCount int, azureAuthEnabled bool) *Cfg { + grafanaComURL string, disablePlugins []string, hideAngularDeprecation []string, forwardHostEnvVars []string, concurrentQueryCount int, azureAuthEnabled bool, + userFacingDefaultError string, dataProxyRowLimit int64, + sqlDatasourceMaxOpenConnsDefault int, sqlDatasourceMaxIdleConnsDefault int, sqlDatasourceMaxConnLifetimeDefault int, +) *Cfg { return &Cfg{ - log: log.New("plugin.cfg"), - PluginsPath: pluginsPath, - BuildVersion: grafanaVersion, - DevMode: devMode, - PluginSettings: pluginSettings, - PluginsAllowUnsigned: pluginsAllowUnsigned, - DisablePlugins: disablePlugins, - AWSAllowedAuthProviders: awsAllowedAuthProviders, - AWSAssumeRoleEnabled: awsAssumeRoleEnabled, - AWSExternalId: awsExternalId, - AWSSessionDuration: awsSessionDuration, - AWSListMetricsPageLimit: awsListMetricsPageLimit, - AWSForwardSettingsPlugins: AWSForwardSettingsPlugins, - Azure: azure, - ProxySettings: secureSocksDSProxy, - LogDatasourceRequests: logDatasourceRequests, - PluginsCDNURLTemplate: pluginsCDNURLTemplate, - Tracing: tracing, - GrafanaComURL: grafanaComURL, - GrafanaAppURL: appURL, - GrafanaAppSubURL: appSubURL, - Features: features, - AngularSupportEnabled: angularSupportEnabled, - HideAngularDeprecation: hideAngularDeprecation, - ForwardHostEnvVars: forwardHostEnvVars, - ConcurrentQueryCount: concurrentQueryCount, - AzureAuthEnabled: azureAuthEnabled, + log: log.New("plugin.cfg"), + PluginsPath: pluginsPath, + BuildVersion: grafanaVersion, + DevMode: devMode, + PluginSettings: pluginSettings, + PluginsAllowUnsigned: pluginsAllowUnsigned, + DisablePlugins: disablePlugins, + AWSAllowedAuthProviders: awsAllowedAuthProviders, + AWSAssumeRoleEnabled: awsAssumeRoleEnabled, + AWSExternalId: awsExternalId, + AWSSessionDuration: awsSessionDuration, + AWSListMetricsPageLimit: awsListMetricsPageLimit, + AWSForwardSettingsPlugins: AWSForwardSettingsPlugins, + Azure: azure, + ProxySettings: secureSocksDSProxy, + LogDatasourceRequests: logDatasourceRequests, + PluginsCDNURLTemplate: pluginsCDNURLTemplate, + Tracing: tracing, + GrafanaComURL: grafanaComURL, + GrafanaAppURL: appURL, + GrafanaAppSubURL: appSubURL, + Features: features, + AngularSupportEnabled: angularSupportEnabled, + HideAngularDeprecation: hideAngularDeprecation, + ForwardHostEnvVars: forwardHostEnvVars, + ConcurrentQueryCount: concurrentQueryCount, + AzureAuthEnabled: azureAuthEnabled, + UserFacingDefaultError: userFacingDefaultError, + DataProxyRowLimit: dataProxyRowLimit, + SQLDatasourceMaxOpenConnsDefault: sqlDatasourceMaxOpenConnsDefault, + SQLDatasourceMaxIdleConnsDefault: sqlDatasourceMaxIdleConnsDefault, + SQLDatasourceMaxConnLifetimeDefault: sqlDatasourceMaxConnLifetimeDefault, } } diff --git a/pkg/plugins/envvars/envvars.go b/pkg/plugins/envvars/envvars.go index 62c769290f..9913b1a9b7 100644 --- a/pkg/plugins/envvars/envvars.go +++ b/pkg/plugins/envvars/envvars.go @@ -100,6 +100,8 @@ func (s *Service) Get(ctx context.Context, p *plugins.Plugin) []string { } // GetConfigMap returns a map of configuration that should be passed in a plugin request. +// +//nolint:gocyclo func (s *Service) GetConfigMap(ctx context.Context, pluginID string, _ *auth.ExternalService) map[string]string { m := make(map[string]string) @@ -110,6 +112,18 @@ func (s *Service) GetConfigMap(ctx context.Context, pluginID string, _ *auth.Ext m[backend.ConcurrentQueryCount] = strconv.Itoa(s.cfg.ConcurrentQueryCount) } + if s.cfg.UserFacingDefaultError != "" { + m[backend.UserFacingDefaultError] = s.cfg.UserFacingDefaultError + } + + if s.cfg.DataProxyRowLimit != 0 { + m[backend.SQLRowLimit] = strconv.FormatInt(s.cfg.DataProxyRowLimit, 10) + } + + m[backend.SQLMaxOpenConnsDefault] = strconv.Itoa(s.cfg.SQLDatasourceMaxOpenConnsDefault) + m[backend.SQLMaxIdleConnsDefault] = strconv.Itoa(s.cfg.SQLDatasourceMaxIdleConnsDefault) + m[backend.SQLMaxConnLifetimeSecondsDefault] = strconv.Itoa(s.cfg.SQLDatasourceMaxConnLifetimeDefault) + // TODO add support via plugin SDK // if externalService != nil { // m[oauthtokenretriever.AppURL] = s.cfg.GrafanaAppURL diff --git a/pkg/plugins/envvars/envvars_test.go b/pkg/plugins/envvars/envvars_test.go index 2fb0b52aca..ed775d0ff7 100644 --- a/pkg/plugins/envvars/envvars_test.go +++ b/pkg/plugins/envvars/envvars_test.go @@ -671,6 +671,18 @@ func TestInitalizer_azureEnvVars(t *testing.T) { }) } +func TestService_GetConfigMap_Defaults(t *testing.T) { + s := &Service{ + cfg: &config.Cfg{}, + } + + require.Equal(t, map[string]string{ + "GF_SQL_MAX_OPEN_CONNS_DEFAULT": "0", + "GF_SQL_MAX_IDLE_CONNS_DEFAULT": "0", + "GF_SQL_MAX_CONN_LIFETIME_SECONDS_DEFAULT": "0", + }, s.GetConfigMap(context.Background(), "", nil)) +} + func TestService_GetConfigMap(t *testing.T) { tcs := []struct { name string @@ -806,6 +818,42 @@ func TestService_GetConfigMap_appURL(t *testing.T) { }) } +func TestService_GetConfigMap_SQL(t *testing.T) { + t.Run("Uses the configured values", func(t *testing.T) { + s := &Service{ + cfg: &config.Cfg{ + DataProxyRowLimit: 23, + SQLDatasourceMaxOpenConnsDefault: 24, + SQLDatasourceMaxIdleConnsDefault: 25, + SQLDatasourceMaxConnLifetimeDefault: 26, + }, + } + + require.Subset(t, s.GetConfigMap(context.Background(), "", nil), map[string]string{ + "GF_SQL_ROW_LIMIT": "23", + "GF_SQL_MAX_OPEN_CONNS_DEFAULT": "24", + "GF_SQL_MAX_IDLE_CONNS_DEFAULT": "25", + "GF_SQL_MAX_CONN_LIFETIME_SECONDS_DEFAULT": "26", + }) + }) + + t.Run("Uses the configured max-default-values, even when they are zero", func(t *testing.T) { + s := &Service{ + cfg: &config.Cfg{ + SQLDatasourceMaxOpenConnsDefault: 0, + SQLDatasourceMaxIdleConnsDefault: 0, + SQLDatasourceMaxConnLifetimeDefault: 0, + }, + } + + require.Equal(t, map[string]string{ + "GF_SQL_MAX_OPEN_CONNS_DEFAULT": "0", + "GF_SQL_MAX_IDLE_CONNS_DEFAULT": "0", + "GF_SQL_MAX_CONN_LIFETIME_SECONDS_DEFAULT": "0", + }, s.GetConfigMap(context.Background(), "", nil)) + }) +} + func TestService_GetConfigMap_concurrentQueryCount(t *testing.T) { t.Run("Uses the configured concurrent query count", func(t *testing.T) { s := &Service{ diff --git a/pkg/services/pluginsintegration/config/config.go b/pkg/services/pluginsintegration/config/config.go index cc5cb7e5ea..7c269dd31c 100644 --- a/pkg/services/pluginsintegration/config/config.go +++ b/pkg/services/pluginsintegration/config/config.go @@ -63,6 +63,11 @@ func ProvideConfig(settingProvider setting.Provider, grafanaCfg *setting.Cfg, fe grafanaCfg.ForwardHostEnvVars, grafanaCfg.ConcurrentQueryCount, grafanaCfg.AzureAuthEnabled, + grafanaCfg.UserFacingDefaultError, + grafanaCfg.DataProxyRowLimit, + grafanaCfg.SqlDatasourceMaxOpenConnsDefault, + grafanaCfg.SqlDatasourceMaxIdleConnsDefault, + grafanaCfg.SqlDatasourceMaxConnLifetimeDefault, ), nil } From d091e4c264fa66dd8387628ff3f09c9d89c334cf Mon Sep 17 00:00:00 2001 From: brendamuir <100768211+brendamuir@users.noreply.github.com> Date: Wed, 21 Feb 2024 12:21:44 +0100 Subject: [PATCH 0061/1406] Alerting docs: adds simplified alert routing (#82158) * Alerting docs: adds simplified alert routing * adds to preview section * adds numbering * adds indent * deletes fullstop * ran prettier * adds feature toggle notes * fixes spelling error --- .../create-grafana-managed-rule.md | 61 +++++++++++++++++-- 1 file changed, 57 insertions(+), 4 deletions(-) diff --git a/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md b/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md index 7b21847e4b..6a96c58f47 100644 --- a/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md +++ b/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md @@ -123,11 +123,15 @@ To do this, you need to make sure that your alert rule is in the right evaluatio ## Configure notifications -Add labels to your alert rules to set which notification policy should handle your firing alert instances. +{{< admonition type="note" >}} +To try out a simplified version of routing your alerts, enable the alertingSimplifiedRouting feature toggle and refer to the following section Configure notifications (simplified). +{{< /admonition >}} -All alert rules and instances, irrespective of their labels, match the default notification policy. If there are no nested policies, or no nested policies match the labels in the alert rule or alert instance, then the default notification policy is the matching policy. +1. Add labels to your alert rules to set which notification policy should handle your firing alert instances. -1. Add labels if you want to change the way your notifications are routed. + All alert rules and instances, irrespective of their labels, match the default notification policy. If there are no nested policies, or no nested policies match the labels in the alert rule or alert instance, then the default notification policy is the matching policy. + + Add labels if you want to change the way your notifications are routed. Add custom labels by selecting existing key-value pairs from the drop down, or add new labels by entering the new key or value. @@ -137,7 +141,56 @@ All alert rules and instances, irrespective of their labels, match the default n Expand each notification policy below to view more details. -1. Click **See details** to view alert routing details and an email preview. +1. Click See details to view alert routing details and an email preview. + +1. Click **Save rule**. + +## Configure notifications (simplified) + +{{< admonition type="note" >}} +To try this out, enable the alertingSimplifiedRouting feature toggle. + +This feature is currently not available for Grafana Cloud. +{{< /admonition >}} + +In the **Labels** section, you can optionally choose whether to add labels to organize your alert rules, make searching easier, as well as set which notification policy should handle your firing alert instance. + +In the **Configure notifications** section, you can choose to select a contact point directly from the alert rule form or choose to use notification policy routing as well as set up mute timings and groupings. + +Complete the following steps to set up labels and notifications. + +1. Add labels, if required. + + Add custom labels by selecting existing key-value pairs from the drop down, or add new labels by entering the new key or value. + +2. Configure who receives a notification when an alert rule fires by either choosing **Select contact point** or **Use notification policy**. + + **Select contact point** + + 1. Choose this option to select an existing contact point. + + All notifications for this alert rule are sent to this contact point automatically and notification policies are not used. + + 2. You can also optionally select a mute timing as well as groupings and timings to define when not to send notifications. + + {{% admonition type="note" %}} + An auto-generated notification policy is generated. Only admins can view these auto-generated policies from the **Notification policies** list view. Any changes have to be made in the alert rules form. {{% /admonition %}} + + **Use notification policy** + + 3. Choose this option to use the notification policy tree to direct your notifications. + + {{% admonition type="note" %}} + All alert rules and instances, irrespective of their labels, match the default notification policy. If there are no nested policies, or no nested policies match the labels in the alert rule or alert instance, then the default notification policy is the matching policy. + {{% /admonition %}} + + 4. Preview your alert instance routing set up. + + Based on the labels added, alert instances are routed to the following notification policies displayed. + + 5. Expand each notification policy below to view more details. + + 6. Click **See details** to view alert routing details and an email preview. ## Add annotations From 6b31b1fc037a473d9a3a3d7f2b084ae45473392a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Fern=C3=A1ndez?= Date: Wed, 21 Feb 2024 12:28:57 +0100 Subject: [PATCH 0062/1406] TimeRangeList: absolute time range list not scrollable (#82887) --- .../DateTimePickers/TimeRangePicker/TimePickerContent.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimePickerContent.tsx b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimePickerContent.tsx index 7a84251843..d7245a9bbc 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimePickerContent.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimePickerContent.tsx @@ -280,6 +280,8 @@ const getStyles = ( borderRadius: theme.shape.radius.default, border: `1px solid ${theme.colors.border.weak}`, [`${isReversed ? 'left' : 'right'}`]: 0, + display: 'flex', + flexDirection: 'column', }), body: css({ display: 'flex', @@ -292,7 +294,7 @@ const getStyles = ( flexDirection: 'column', borderRight: `${isReversed ? 'none' : `1px solid ${theme.colors.border.weak}`}`, width: `${!hideQuickRanges ? '60%' : '100%'}`, - overflow: 'hidden', + overflow: 'auto', order: isReversed ? 1 : 0, }), rightSide: css({ From bfdb4625a052a0a2220b9a03287bc1a798f132eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 21 Feb 2024 12:34:19 +0100 Subject: [PATCH 0063/1406] Grafana/UI: Replace Splitter with useSplitter hook and refactor PanelEdit snapping logic to useSnappingSplitter hook (#82895) * Hook refactor * Update * Test * Update * Both directions work * fixes * refactoring * Update * Update * update * Remove consolo.log * Update * Fix --- .../src/components/Splitter/Splitter.mdx | 23 --- .../components/Splitter/Splitter.story.tsx | 55 ----- .../src/components/Splitter/useSplitter.mdx | 34 +++ .../components/Splitter/useSplitter.story.tsx | 71 +++++++ .../Splitter/{Splitter.tsx => useSplitter.ts} | 195 +++++------------- packages/grafana-ui/src/components/index.ts | 4 +- .../panel-edit/PanelEditor.tsx | 51 ----- .../panel-edit/PanelEditorRenderer.tsx | 157 ++++++++++---- .../panel-edit/PanelOptionsPane.tsx | 56 +---- .../splitter/useSnappingSplitter.ts | 104 ++++++++++ 10 files changed, 392 insertions(+), 358 deletions(-) delete mode 100644 packages/grafana-ui/src/components/Splitter/Splitter.mdx delete mode 100644 packages/grafana-ui/src/components/Splitter/Splitter.story.tsx create mode 100644 packages/grafana-ui/src/components/Splitter/useSplitter.mdx create mode 100644 packages/grafana-ui/src/components/Splitter/useSplitter.story.tsx rename packages/grafana-ui/src/components/Splitter/{Splitter.tsx => useSplitter.ts} (78%) create mode 100644 public/app/features/dashboard-scene/panel-edit/splitter/useSnappingSplitter.ts diff --git a/packages/grafana-ui/src/components/Splitter/Splitter.mdx b/packages/grafana-ui/src/components/Splitter/Splitter.mdx deleted file mode 100644 index af7d3fc015..0000000000 --- a/packages/grafana-ui/src/components/Splitter/Splitter.mdx +++ /dev/null @@ -1,23 +0,0 @@ -import { Meta, Preview, ArgTypes } from '@storybook/blocks'; -import { Box, Splitter, Text } from '@grafana/ui'; - - - -# Splitter - -The splitter creates two resizable panes, either horizontally or vertically. - - -
- - - Primary - - - Secondary - - -
-
- - diff --git a/packages/grafana-ui/src/components/Splitter/Splitter.story.tsx b/packages/grafana-ui/src/components/Splitter/Splitter.story.tsx deleted file mode 100644 index 7248f78f3d..0000000000 --- a/packages/grafana-ui/src/components/Splitter/Splitter.story.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { css } from '@emotion/css'; -import { Meta, StoryFn } from '@storybook/react'; -import React from 'react'; - -import { Splitter, useTheme2 } from '@grafana/ui'; - -import { DashboardStoryCanvas } from '../../utils/storybook/DashboardStoryCanvas'; - -import mdx from './Splitter.mdx'; - -const meta: Meta = { - title: 'General/Layout/Splitter', - component: Splitter, - parameters: { - docs: { - page: mdx, - }, - controls: { - exclude: [], - }, - }, - argTypes: { - initialSize: { control: { type: 'number', min: 0.1, max: 1 } }, - }, -}; - -export const Basic: StoryFn = (args) => { - const theme = useTheme2(); - const paneStyles = css({ - display: 'flex', - flexGrow: 1, - background: theme.colors.background.primary, - padding: theme.spacing(2), - border: `1px solid ${theme.colors.border.weak}`, - height: '100%', - }); - - return ( - -
- -
Primary
-
Secondary
-
-
-
- ); -}; - -Basic.args = { - direction: 'row', - dragPosition: 'middle', -}; - -export default meta; diff --git a/packages/grafana-ui/src/components/Splitter/useSplitter.mdx b/packages/grafana-ui/src/components/Splitter/useSplitter.mdx new file mode 100644 index 0000000000..332782e1d9 --- /dev/null +++ b/packages/grafana-ui/src/components/Splitter/useSplitter.mdx @@ -0,0 +1,34 @@ +import { Meta, ArgTypes } from '@storybook/blocks'; +import { Box, useSplitter, Text } from '@grafana/ui'; + +# useSplitter + +The splitter creates two resizable panes, either horizontally or vertically. + +### Usage + +```tsx +import { useSplitter } from '@grafana/ui'; + +const { containerProps, primaryProps, secondaryProps, splitterProps } = useSplitter({ + direction: 'row', + initialSize: 0.5, + dragPosition: 'end', +}); + +return ( +
+
+ + Primary + +
+
+
+ + Secondary + +
+
+); +``` diff --git a/packages/grafana-ui/src/components/Splitter/useSplitter.story.tsx b/packages/grafana-ui/src/components/Splitter/useSplitter.story.tsx new file mode 100644 index 0000000000..ca929b79ef --- /dev/null +++ b/packages/grafana-ui/src/components/Splitter/useSplitter.story.tsx @@ -0,0 +1,71 @@ +import { Meta, StoryFn } from '@storybook/react'; +import React from 'react'; + +import { Box } from '@grafana/ui'; + +import { DashboardStoryCanvas } from '../../utils/storybook/DashboardStoryCanvas'; + +import { UseSplitterOptions, useSplitter } from './useSplitter'; +import mdx from './useSplitter.mdx'; + +const meta: Meta = { + title: 'General/Layout/useSplitter', + parameters: { + docs: { page: mdx }, + controls: { + exclude: [], + }, + }, + argTypes: { + initialSize: { control: { type: 'number', min: 0.1, max: 1 } }, + direction: { control: { type: 'radio' }, options: ['row', 'column'] }, + dragPosition: { control: { type: 'radio' }, options: ['start', 'middle', 'end'] }, + hasSecondPane: { type: 'boolean', options: [true, false] }, + }, +}; + +interface StoryOptions extends UseSplitterOptions { + hasSecondPane: boolean; +} + +export const Basic: StoryFn = (options) => { + const { containerProps, primaryProps, secondaryProps, splitterProps } = useSplitter({ + ...options, + }); + + if (!options.hasSecondPane) { + primaryProps.style.flexGrow = 1; + } + + return ( + +
+
+
+ + Primary + +
+ {options.hasSecondPane && ( + <> +
+
+ + Secondary + +
+ + )} +
+
+ + ); +}; + +Basic.args = { + direction: 'row', + dragPosition: 'middle', + hasSecondPane: true, +}; + +export default meta; diff --git a/packages/grafana-ui/src/components/Splitter/Splitter.tsx b/packages/grafana-ui/src/components/Splitter/useSplitter.ts similarity index 78% rename from packages/grafana-ui/src/components/Splitter/Splitter.tsx rename to packages/grafana-ui/src/components/Splitter/useSplitter.ts index ded1e13d65..5f3b0b8e4a 100644 --- a/packages/grafana-ui/src/components/Splitter/Splitter.tsx +++ b/packages/grafana-ui/src/components/Splitter/useSplitter.ts @@ -7,137 +7,19 @@ import { GrafanaTheme2 } from '@grafana/data'; import { useStyles2 } from '../../themes'; import { DragHandlePosition, getDragStyles } from '../DragHandle/DragHandle'; -export interface Props { +export interface UseSplitterOptions { /** * The initial size of the primary pane between 0-1, defaults to 0.5 */ initialSize?: number; - direction?: 'row' | 'column'; + direction: 'row' | 'column'; dragPosition?: DragHandlePosition; - primaryPaneStyles?: React.CSSProperties; - secondaryPaneStyles?: React.CSSProperties; /** * Called when ever the size of the primary pane changes * @param flexSize (float from 0-1) */ onSizeChanged?: (flexSize: number, pixelSize: number) => void; onResizing?: (flexSize: number, pixelSize: number) => void; - children: [React.ReactNode, React.ReactNode]; -} - -/** - * Splits two children into two resizable panes - * @alpha - */ -export function Splitter(props: Props) { - const { - direction = 'row', - initialSize = 0.5, - primaryPaneStyles, - secondaryPaneStyles, - onSizeChanged, - onResizing, - dragPosition = 'middle', - children, - } = props; - - const { containerRef, firstPaneRef, minDimProp, splitterProps, secondPaneRef } = useSplitter( - direction, - onSizeChanged, - onResizing - ); - - const kids = React.Children.toArray(children); - const styles = useStyles2(getStyles, direction); - const dragStyles = useStyles2(getDragStyles, dragPosition); - const dragHandleStyle = direction === 'column' ? dragStyles.dragHandleHorizontal : dragStyles.dragHandleVertical; - const id = useId(); - - const secondAvailable = kids.length === 2; - const visibilitySecond = secondAvailable ? 'visible' : 'hidden'; - let firstChildSize = initialSize; - - // If second child is missing let first child have all the space - if (!children[1]) { - firstChildSize = 1; - } - - return ( -
-
- {kids[0]} -
- - {kids[1] && ( - <> -
- -
- {kids[1]} -
- - )} -
- ); -} - -function getStyles(theme: GrafanaTheme2, direction: Props['direction']) { - return { - container: css({ - display: 'flex', - flexDirection: direction === 'row' ? 'row' : 'column', - width: '100%', - flexGrow: 1, - overflow: 'hidden', - }), - panel: css({ display: 'flex', position: 'relative', flexBasis: 0 }), - dragEdge: { - second: css({ - top: 0, - left: theme.spacing(-1), - bottom: 0, - position: 'absolute', - zIndex: theme.zIndex.modal, - }), - first: css({ - top: 0, - left: theme.spacing(-1), - bottom: 0, - position: 'absolute', - zIndex: theme.zIndex.modal, - }), - }, - }; } const PIXELS_PER_MS = 0.3 as const; @@ -159,11 +41,9 @@ const propsForDirection = { }, } as const; -function useSplitter( - direction: 'row' | 'column', - onSizeChanged: Props['onSizeChanged'], - onResizing: Props['onResizing'] -) { +export function useSplitter(options: UseSplitterOptions) { + const { direction, initialSize = 0.5, dragPosition = 'middle', onResizing, onSizeChanged } = options; + const handleSize = 16; const splitterRef = useRef(null); const firstPaneRef = useRef(null); @@ -171,7 +51,6 @@ function useSplitter( const containerRef = useRef(null); const containerSize = useRef(null); const primarySizeRef = useRef<'1fr' | number>('1fr'); - const firstPaneMeasurements = useRef(undefined); const savedPos = useRef(undefined); @@ -197,11 +76,7 @@ function useSplitter( const curSize = firstPaneRef.current!.getBoundingClientRect()[measurementProp]; const newDims = measureElement(firstPaneRef.current); - splitterRef.current!.ariaValueNow = `${clamp( - ((curSize - newDims[minDimProp]) / (newDims[maxDimProp] - newDims[minDimProp])) * 100, - 0, - 100 - )}`; + splitterRef.current!.ariaValueNow = ariaValue(curSize, newDims[minDimProp], newDims[maxDimProp]); } }, 500, @@ -239,10 +114,8 @@ function useSplitter( firstPaneRef.current!.style.flexGrow = `${newFlex}`; secondPaneRef.current!.style.flexGrow = `${1 - newFlex}`; + splitterRef.current!.ariaValueNow = ariaValue(newSize, dims[minDimProp], dims[maxDimProp]); - const ariaValueNow = ariaValue(newSize, dims[minDimProp], dims[maxDimProp] - dims[minDimProp]); - - splitterRef.current!.ariaValueNow = `${ariaValueNow}`; onResizing?.(newFlex, newSize); } }, @@ -406,7 +279,7 @@ function useSplitter( const dim = measureElement(firstPaneRef.current); firstPaneMeasurements.current = dim; primarySizeRef.current = firstPaneRef.current!.getBoundingClientRect()[measurementProp]; - splitterRef.current!.ariaValueNow = `${((primarySizeRef.current - dim[minDimProp]) / (dim[maxDimProp] - dim[minDimProp])) * 100}`; + splitterRef.current!.ariaValueNow = `${ariaValue(primarySizeRef.current, dim[minDimProp], dim[maxDimProp])}`; }, [maxDimProp, measurementProp, minDimProp]); const onBlur = useCallback(() => { @@ -421,10 +294,32 @@ function useSplitter( } }, [onSizeChanged]); + const styles = useStyles2(getStyles, direction); + const dragStyles = useStyles2(getDragStyles, dragPosition); + const dragHandleStyle = direction === 'column' ? dragStyles.dragHandleHorizontal : dragStyles.dragHandleVertical; + const id = useId(); + return { - containerRef, - firstPaneRef, - minDimProp, + containerProps: { + ref: containerRef, + className: styles.container, + }, + primaryProps: { + ref: firstPaneRef, + className: styles.panel, + style: { + [minDimProp]: 'min-content', + flexGrow: clamp(initialSize ?? 0.5, 0, 1), + }, + }, + secondaryProps: { + ref: secondPaneRef, + className: styles.panel, + style: { + flexGrow: clamp(1 - initialSize, 0, 1), + [minDimProp]: 'min-content', + }, + }, splitterProps: { onPointerUp, onPointerDown, @@ -435,8 +330,15 @@ function useSplitter( onBlur, ref: splitterRef, style: { [measurementProp]: `${handleSize}px` }, + role: 'separator', + 'aria-valuemin': 0, + 'aria-valuemax': 100, + 'aria-valuenow': initialSize * 100, + 'aria-controls': `start-panel-${id}`, + 'aria-label': 'Pane resize widget', + tabIndex: 0, + className: dragHandleStyle, }, - secondPaneRef, }; } @@ -456,8 +358,10 @@ function measureElement(ref: T): MeasureResult { const savedWidth = ref.style.width; const savedHeight = ref.style.height; const savedFlex = ref.style.flexGrow; + document.body.style.overflow = 'hidden'; ref.style.flexGrow = '0'; + const { width: minWidth, height: minHeight } = ref.getBoundingClientRect(); ref.style.flexGrow = '100'; @@ -491,3 +395,16 @@ function useResizeObserver( // eslint-disable-next-line react-hooks/exhaustive-deps }, deps); } + +function getStyles(theme: GrafanaTheme2, direction: UseSplitterOptions['direction']) { + return { + container: css({ + display: 'flex', + flexDirection: direction === 'row' ? 'row' : 'column', + width: '100%', + flexGrow: 1, + overflow: 'hidden', + }), + panel: css({ display: 'flex', position: 'relative', flexBasis: 0 }), + }; +} diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index 435d63f244..ec094bea02 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -264,8 +264,8 @@ export { Avatar } from './UsersIndicator/Avatar'; // Export this until we've figured out a good approach to inline form styles. export { InlineFormLabel } from './FormLabel/FormLabel'; export { Divider } from './Divider/Divider'; -export { getDragStyles } from './DragHandle/DragHandle'; -export { Splitter } from './Splitter/Splitter'; +export { getDragStyles, type DragHandlePosition } from './DragHandle/DragHandle'; +export { useSplitter } from './Splitter/useSplitter'; export { LayoutItemContext, type LayoutItemContextProps } from './Layout/LayoutItemContext'; diff --git a/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx b/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx index 6b2ceaa457..9184064160 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx @@ -21,8 +21,6 @@ export interface PanelEditorState extends SceneObjectState { isDirty?: boolean; panelId: number; optionsPane: PanelOptionsPane; - optionsCollapsed?: boolean; - optionsPaneSize: number; dataPane?: PanelDataPane; vizManager: VizPanelManager; } @@ -102,56 +100,8 @@ export class PanelEditor extends SceneObjectBase { sourcePanel!.parent.setState({ body: this.state.vizManager.state.panel.clone() }); } } - - public toggleOptionsPane() { - this.setState({ optionsCollapsed: !this.state.optionsCollapsed, optionsPaneSize: OPTIONS_PANE_FLEX_DEFAULT }); - } - - public onOptionsPaneResizing = (flexSize: number, pixelSize: number) => { - if (flexSize <= 0 && pixelSize <= 0) { - return; - } - - const optionsPixelSize = (pixelSize / flexSize) * (1 - flexSize); - - if (this.state.optionsCollapsed && optionsPixelSize > OPTIONS_PANE_PIXELS_MIN) { - this.setState({ optionsCollapsed: false }); - } - - if (!this.state.optionsCollapsed && optionsPixelSize < OPTIONS_PANE_PIXELS_MIN) { - this.setState({ optionsCollapsed: true }); - } - }; - - public onOptionsPaneSizeChanged = (flexSize: number, pixelSize: number) => { - if (flexSize <= 0 && pixelSize <= 0) { - return; - } - - const optionsPaneSize = 1 - flexSize; - const isSnappedClosed = this.state.optionsPaneSize === 0; - const fullWidth = pixelSize / flexSize; - const snapWidth = OPTIONS_PANE_PIXELS_SNAP / fullWidth; - - if (this.state.optionsCollapsed) { - if (isSnappedClosed) { - this.setState({ - optionsPaneSize: Math.max(optionsPaneSize, snapWidth), - optionsCollapsed: false, - }); - } else { - this.setState({ optionsPaneSize: 0 }); - } - } else if (isSnappedClosed) { - this.setState({ optionsPaneSize: optionsPaneSize }); - } - }; } -export const OPTIONS_PANE_PIXELS_MIN = 300; -export const OPTIONS_PANE_PIXELS_SNAP = 400; -export const OPTIONS_PANE_FLEX_DEFAULT = 0.25; - export function buildPanelEditScene(panel: VizPanel): PanelEditor { const panelClone = panel.clone(); const vizPanelMgr = new VizPanelManager(panelClone); @@ -160,6 +110,5 @@ export function buildPanelEditScene(panel: VizPanel): PanelEditor { panelId: getPanelIdForVizPanel(panel), optionsPane: new PanelOptionsPane({}), vizManager: vizPanelMgr, - optionsPaneSize: OPTIONS_PANE_FLEX_DEFAULT, }); } diff --git a/public/app/features/dashboard-scene/panel-edit/PanelEditorRenderer.tsx b/public/app/features/dashboard-scene/panel-edit/PanelEditorRenderer.tsx index a464b0a538..2b81f59779 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelEditorRenderer.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelEditorRenderer.tsx @@ -1,56 +1,109 @@ -import { css } from '@emotion/css'; -import React, { useMemo } from 'react'; +import { css, cx } from '@emotion/css'; +import React from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { SceneComponentProps } from '@grafana/scenes'; -import { Splitter, useStyles2 } from '@grafana/ui'; +import { Button, ToolbarButton, useStyles2 } from '@grafana/ui'; import { NavToolbarActions } from '../scene/NavToolbarActions'; import { getDashboardSceneFor } from '../utils/utils'; import { PanelEditor } from './PanelEditor'; +import { useSnappingSplitter } from './splitter/useSnappingSplitter'; export function PanelEditorRenderer({ model }: SceneComponentProps) { const dashboard = getDashboardSceneFor(model); - const { optionsPane, vizManager, dataPane, optionsPaneSize } = model.useState(); - const { controls } = dashboard.useState(); + const { optionsPane } = model.useState(); const styles = useStyles2(getStyles); - const [vizPaneStyles, optionsPaneStyles] = useMemo(() => { - if (optionsPaneSize > 0) { - return [{ flexGrow: 1 - optionsPaneSize }, { minWidth: 'unset', overflow: 'hidden', flexGrow: optionsPaneSize }]; - } else { - return [{ flexGrow: 1 }, { minWidth: 'unset', flexGrow: 0 }]; - } - }, [optionsPaneSize]); + + const { containerProps, primaryProps, secondaryProps, splitterProps, splitterState, onToggleCollapse } = + useSnappingSplitter({ + direction: 'row', + dragPosition: 'end', + initialSize: 0.75, + paneOptions: { + collapseBelowPixels: 250, + snapOpenToPixels: 400, + }, + }); return ( <> - -
-
- {controls && } - - - {dataPane && } - -
+
+
+
- {optionsPane && } - +
+
+ {splitterState.collapsed && ( +
+ +
+ )} + {!splitterState.collapsed && } +
+
+ + ); +} + +function VizAndDataPane({ model }: SceneComponentProps) { + const dashboard = getDashboardSceneFor(model); + const { vizManager, dataPane } = model.useState(); + const { controls } = dashboard.useState(); + const styles = useStyles2(getStyles); + + const { containerProps, primaryProps, secondaryProps, splitterProps, splitterState, onToggleCollapse } = + useSnappingSplitter({ + direction: 'column', + dragPosition: 'start', + initialSize: 0.5, + paneOptions: { + collapseBelowPixels: 150, + }, + }); + + if (!dataPane) { + primaryProps.style.flexGrow = 1; + } + + return ( + <> + {controls && } +
+
+ +
+ {dataPane && ( + <> +
+
+ {splitterState.collapsed && ( +
+
+ )} + {!splitterState.collapsed && } +
+ + )} +
); } @@ -70,7 +123,7 @@ function getStyles(theme: GrafanaTheme2) { label: 'body', flexGrow: 1, display: 'flex', - position: 'relative', + flexDirection: 'column', minHeight: 0, gap: '8px', }), @@ -81,5 +134,35 @@ function getStyles(theme: GrafanaTheme2) { gap: theme.spacing(1), padding: theme.spacing(2, 0, 2, 2), }), + optionsPane: css({ + flexDirection: 'column', + borderLeft: `1px solid ${theme.colors.border.weak}`, + background: theme.colors.background.primary, + }), + expandOptionsWrapper: css({ + display: 'flex', + flexDirection: 'column', + padding: theme.spacing(2, 1), + }), + expandDataPane: css({ + display: 'flex', + flexDirection: 'row', + padding: theme.spacing(1), + borderTop: `1px solid ${theme.colors.border.weak}`, + borderRight: `1px solid ${theme.colors.border.weak}`, + background: theme.colors.background.primary, + flexGrow: 1, + justifyContent: 'space-around', + }), + rotate180: css({ + rotate: '180deg', + }), + openDataPaneButton: css({ + width: theme.spacing(8), + justifyContent: 'center', + svg: { + rotate: '-90deg', + }, + }), }; } diff --git a/public/app/features/dashboard-scene/panel-edit/PanelOptionsPane.tsx b/public/app/features/dashboard-scene/panel-edit/PanelOptionsPane.tsx index 98d37e26d2..0678f10734 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelOptionsPane.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelOptionsPane.tsx @@ -39,47 +39,18 @@ export class PanelOptionsPane extends SceneObjectBase { this.setState({ listMode }); }; - onCollapsePane = () => { - const editor = sceneGraph.getAncestor(this, PanelEditor); - editor.toggleOptionsPane(); - }; - static Component = ({ model }: SceneComponentProps) => { const { isVizPickerOpen, searchQuery, listMode } = model.useState(); - const editor = sceneGraph.getAncestor(model, PanelEditor); - const { optionsCollapsed, vizManager } = editor.useState(); + const vizManager = sceneGraph.getAncestor(model, PanelEditor).state.vizManager; const { pluginId } = vizManager.state.panel.useState(); const styles = useStyles2(getStyles); - if (optionsCollapsed) { - return ( -
-
- -
-
- ); - } - return ( -
+ <> {!isVizPickerOpen && ( <>
- + { )} {isVizPickerOpen && } -
+ ); }; } function getStyles(theme: GrafanaTheme2) { return { - pane: css({ - display: 'flex', - flexDirection: 'column', - flexGrow: '1', - borderLeft: `1px solid ${theme.colors.border.weak}`, - background: theme.colors.background.primary, - }), top: css({ display: 'flex', flexDirection: 'column', @@ -137,11 +101,9 @@ function getStyles(theme: GrafanaTheme2) { interface VisualizationButtonProps { pluginId: string; onOpen: () => void; - isOpen?: boolean; - onTogglePane?: () => void; } -export function VisualizationButton({ pluginId, onOpen, isOpen, onTogglePane }: VisualizationButtonProps) { +export function VisualizationButton({ pluginId, onOpen }: VisualizationButtonProps) { const styles = useStyles2(getVizButtonStyles); const pluginMeta = useMemo(() => getAllPanelPluginMeta().filter((p) => p.id === pluginId)[0], [pluginId]); @@ -160,14 +122,6 @@ export function VisualizationButton({ pluginId, onOpen, isOpen, onTogglePane }: > {pluginMeta.name} - {/* */} ); } diff --git a/public/app/features/dashboard-scene/panel-edit/splitter/useSnappingSplitter.ts b/public/app/features/dashboard-scene/panel-edit/splitter/useSnappingSplitter.ts new file mode 100644 index 0000000000..95e78cac99 --- /dev/null +++ b/public/app/features/dashboard-scene/panel-edit/splitter/useSnappingSplitter.ts @@ -0,0 +1,104 @@ +import React, { useCallback } from 'react'; + +import { DragHandlePosition, useSplitter } from '@grafana/ui'; + +export interface UseSnappingSplitterOptions { + /** + * The initial size of the primary pane between 0-1, defaults to 0.5 + */ + initialSize?: number; + direction: 'row' | 'column'; + dragPosition?: DragHandlePosition; + paneOptions: PaneOptions; +} + +interface PaneOptions { + collapseBelowPixels: number; + snapOpenToPixels?: number; +} + +interface PaneState { + collapsed: boolean; + snapSize?: number; +} + +export function useSnappingSplitter(options: UseSnappingSplitterOptions) { + const { paneOptions } = options; + + const [state, setState] = React.useState({ collapsed: false }); + + const onResizing = useCallback( + (flexSize: number, pixelSize: number) => { + if (flexSize <= 0 && pixelSize <= 0) { + return; + } + + const optionsPixelSize = (pixelSize / flexSize) * (1 - flexSize); + + if (state.collapsed && optionsPixelSize > paneOptions.collapseBelowPixels) { + setState({ collapsed: false }); + } + + if (!state.collapsed && optionsPixelSize < paneOptions.collapseBelowPixels) { + setState({ collapsed: true }); + } + }, + [state, paneOptions.collapseBelowPixels] + ); + + const onSizeChanged = useCallback( + (flexSize: number, pixelSize: number) => { + if (flexSize <= 0 && pixelSize <= 0) { + return; + } + + const newSecondPaneSize = 1 - flexSize; + const isSnappedClosed = state.snapSize === 0; + const sizeOfBothPanes = pixelSize / flexSize; + const snapOpenToPixels = paneOptions.snapOpenToPixels ?? sizeOfBothPanes / 2; + const snapSize = snapOpenToPixels / sizeOfBothPanes; + + if (state.collapsed) { + if (isSnappedClosed) { + setState({ snapSize: Math.max(newSecondPaneSize, snapSize), collapsed: false }); + } else { + setState({ snapSize: 0, collapsed: true }); + } + } else if (isSnappedClosed) { + setState({ snapSize: newSecondPaneSize, collapsed: false }); + } + }, + [state, paneOptions.snapOpenToPixels] + ); + + const onToggleCollapse = useCallback(() => { + setState({ collapsed: !state.collapsed }); + }, [state.collapsed]); + + const { containerProps, primaryProps, secondaryProps, splitterProps } = useSplitter({ + ...options, + onResizing, + onSizeChanged, + }); + + // This is to allow resizing it beyond the content dimensions + secondaryProps.style.overflow = 'hidden'; + secondaryProps.style.minWidth = 'unset'; + secondaryProps.style.minHeight = 'unset'; + + if (state.snapSize) { + primaryProps.style = { + ...primaryProps.style, + flexGrow: 1 - state.snapSize, + }; + secondaryProps.style.flexGrow = state.snapSize; + } else if (state.snapSize === 0) { + primaryProps.style.flexGrow = 1; + secondaryProps.style.flexGrow = 0; + secondaryProps.style.minWidth = 'unset'; + secondaryProps.style.minHeight = 'unset'; + secondaryProps.style.overflow = 'unset'; + } + + return { containerProps, primaryProps, secondaryProps, splitterProps, splitterState: state, onToggleCollapse }; +} From b6b5935992f4aa8a962ab075ff780855cb66aeff Mon Sep 17 00:00:00 2001 From: Dan83 Date: Wed, 21 Feb 2024 12:56:04 +0100 Subject: [PATCH 0064/1406] Chore: Remove Form usage in NewFolderForm components (#83028) * Chore: Remove Form usage from NewFolderForm * Chore: Remove Form usage from NewFolderForm * Chore: Remove Form usage from NewFolderForm * Replace HorizontalGroup with Stack * add Default Values and launch prettier --- .../components/NewFolderForm.tsx | 66 ++++++++++--------- 1 file changed, 35 insertions(+), 31 deletions(-) diff --git a/public/app/features/browse-dashboards/components/NewFolderForm.tsx b/public/app/features/browse-dashboards/components/NewFolderForm.tsx index d201f8a4e0..a31594be7f 100644 --- a/public/app/features/browse-dashboards/components/NewFolderForm.tsx +++ b/public/app/features/browse-dashboards/components/NewFolderForm.tsx @@ -1,7 +1,8 @@ import React from 'react'; +import { useForm } from 'react-hook-form'; import { selectors } from '@grafana/e2e-selectors'; -import { Button, Input, Form, Field, HorizontalGroup } from '@grafana/ui'; +import { Button, Input, Field, Stack } from '@grafana/ui'; import { Trans, t } from 'app/core/internationalization'; import { validationSrv } from '../../manage-dashboards/services/ValidationSrv'; @@ -18,6 +19,12 @@ interface FormModel { const initialFormModel: FormModel = { folderName: '' }; export function NewFolderForm({ onCancel, onConfirm }: Props) { + const { + handleSubmit, + register, + formState: { errors }, + } = useForm({ defaultValues: initialFormModel }); + const translatedFolderNameRequiredPhrase = t( 'browse-dashboards.action.new-folder-name-required-phrase', 'Folder name is required.' @@ -38,37 +45,34 @@ export function NewFolderForm({ onCancel, onConfirm }: Props) { const fieldNameLabel = t('browse-dashboards.new-folder-form.name-label', 'Folder name'); return ( -
onConfirm(form.folderName)} + onConfirm(form.folderName))} data-testid={selectors.pages.BrowseDashboards.NewFolderForm.form} > - {({ register, errors }) => ( - <> - - await validateFolderName(v), - })} - /> - - - - - - - )} -
+ + await validateFolderName(v), + })} + /> + + + + + + ); } From 68fe045ec7433feee025f3e7c3f26c5f78cbb7d6 Mon Sep 17 00:00:00 2001 From: Giuseppe Guerra Date: Wed, 21 Feb 2024 12:57:40 +0100 Subject: [PATCH 0065/1406] Plugins: Remove pluginsInstrumentationStatusSource feature toggle (#83067) * Plugins: Remove pluginsInstrumentationStatusSource feature toggle * update tests * Inline pluginRequestDurationWithLabels, pluginRequestCounterWithLabels, pluginRequestDurationSecondsWithLabels --- .../feature-toggles/index.md | 1 - .../src/types/featureToggles.gen.ts | 1 - pkg/services/featuremgmt/registry.go | 7 -- pkg/services/featuremgmt/toggles_gen.csv | 1 - pkg/services/featuremgmt/toggles_gen.go | 4 -- pkg/services/featuremgmt/toggles_gen.json | 3 +- .../clientmiddleware/logger_middleware.go | 12 ++-- .../clientmiddleware/metrics_middleware.go | 21 ++---- .../metrics_middleware_test.go | 71 +++++-------------- .../pluginsintegration/pluginsintegration.go | 16 ++--- 10 files changed, 34 insertions(+), 103 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 10380c910e..6231b09a31 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -157,7 +157,6 @@ Experimental features might be changed or removed without prior notice. | `kubernetesSnapshots` | Routes snapshot requests from /api to the /apis endpoint | | `teamHttpHeaders` | Enables datasources to apply team headers to the client requests | | `cachingOptimizeSerializationMemoryUsage` | If enabled, the caching backend gradually serializes query responses for the cache, comparing against the configured `[caching]max_value_mb` value as it goes. This can can help prevent Grafana from running out of memory while attempting to cache very large query responses. | -| `pluginsInstrumentationStatusSource` | Include a status source label for plugin request metrics and logs | | `prometheusPromQAIL` | Prometheus and AI/ML to assist users in creating a query | | `alertmanagerRemoteSecondary` | Enable Grafana to sync configuration and state with a remote Alertmanager. | | `alertmanagerRemotePrimary` | Enable Grafana to have a remote Alertmanager instance as the primary Alertmanager. | diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index f5a2e721c1..74d38c26b5 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -139,7 +139,6 @@ export interface FeatureToggles { awsDatasourcesNewFormStyling?: boolean; cachingOptimizeSerializationMemoryUsage?: boolean; panelTitleSearchInV1?: boolean; - pluginsInstrumentationStatusSource?: boolean; managedPluginsInstall?: boolean; prometheusPromQAIL?: boolean; addFieldFromCalculationStatFunctions?: boolean; diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 38e6caeaec..cf57c68952 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -905,13 +905,6 @@ var ( Stage: FeatureStageExperimental, Owner: grafanaBackendPlatformSquad, }, - { - Name: "pluginsInstrumentationStatusSource", - Description: "Include a status source label for plugin request metrics and logs", - FrontendOnly: false, - Stage: FeatureStageExperimental, - Owner: grafanaPluginsPlatformSquad, - }, { Name: "managedPluginsInstall", Description: "Install managed plugins directly from plugins catalog", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index ab21b586a4..3098345d97 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -120,7 +120,6 @@ teamHttpHeaders,experimental,@grafana/identity-access-team,false,false,false awsDatasourcesNewFormStyling,preview,@grafana/aws-datasources,false,false,true cachingOptimizeSerializationMemoryUsage,experimental,@grafana/grafana-operator-experience-squad,false,false,false panelTitleSearchInV1,experimental,@grafana/backend-platform,true,false,false -pluginsInstrumentationStatusSource,experimental,@grafana/plugins-platform-backend,false,false,false managedPluginsInstall,preview,@grafana/plugins-platform-backend,false,false,false prometheusPromQAIL,experimental,@grafana/observability-metrics,false,false,true addFieldFromCalculationStatFunctions,preview,@grafana/dataviz-squad,false,false,true diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index e5ec782a98..10a8223f5c 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -491,10 +491,6 @@ const ( // Enable searching for dashboards using panel title in search v1 FlagPanelTitleSearchInV1 = "panelTitleSearchInV1" - // FlagPluginsInstrumentationStatusSource - // Include a status source label for plugin request metrics and logs - FlagPluginsInstrumentationStatusSource = "pluginsInstrumentationStatusSource" - // FlagManagedPluginsInstall // Install managed plugins directly from plugins catalog FlagManagedPluginsInstall = "managedPluginsInstall" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 2e32e1e954..a04e47e1fe 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -82,7 +82,8 @@ "metadata": { "name": "pluginsInstrumentationStatusSource", "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "creationTimestamp": "2024-02-16T18:36:28Z", + "deletionTimestamp": "2024-02-19T14:18:02Z" }, "spec": { "description": "Include a status source label for plugin request metrics and logs", diff --git a/pkg/services/pluginsintegration/clientmiddleware/logger_middleware.go b/pkg/services/pluginsintegration/clientmiddleware/logger_middleware.go index 5dc5eea609..9982aefcb6 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/logger_middleware.go +++ b/pkg/services/pluginsintegration/clientmiddleware/logger_middleware.go @@ -50,9 +50,7 @@ func (m *LoggerMiddleware) logRequest(ctx context.Context, fn func(ctx context.C if err != nil { logParams = append(logParams, "error", err) } - if m.features.IsEnabled(ctx, featuremgmt.FlagPluginsInstrumentationStatusSource) { - logParams = append(logParams, "statusSource", pluginrequestmeta.StatusSourceFromContext(ctx)) - } + logParams = append(logParams, "statusSource", pluginrequestmeta.StatusSourceFromContext(ctx)) ctxLogger := m.logger.FromContext(ctx) logFunc := ctxLogger.Info @@ -81,9 +79,11 @@ func (m *LoggerMiddleware) QueryData(ctx context.Context, req *backend.QueryData ctxLogger := m.logger.FromContext(ctx) for refID, dr := range resp.Responses { if dr.Error != nil { - logParams := []any{"refID", refID, "status", int(dr.Status), "error", dr.Error} - if m.features.IsEnabled(ctx, featuremgmt.FlagPluginsInstrumentationStatusSource) { - logParams = append(logParams, "statusSource", pluginrequestmeta.StatusSourceFromPluginErrorSource(dr.ErrorSource)) + logParams := []any{ + "refID", refID, + "status", int(dr.Status), + "error", dr.Error, + "statusSource", pluginrequestmeta.StatusSourceFromPluginErrorSource(dr.ErrorSource), } ctxLogger.Error("Partial data response error", logParams...) } diff --git a/pkg/services/pluginsintegration/clientmiddleware/metrics_middleware.go b/pkg/services/pluginsintegration/clientmiddleware/metrics_middleware.go index 3ea7f56611..8b7cd2dc02 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/metrics_middleware.go +++ b/pkg/services/pluginsintegration/clientmiddleware/metrics_middleware.go @@ -32,10 +32,7 @@ type MetricsMiddleware struct { } func newMetricsMiddleware(promRegisterer prometheus.Registerer, pluginRegistry registry.Service, features featuremgmt.FeatureToggles) *MetricsMiddleware { - var additionalLabels []string - if features.IsEnabledGlobally(featuremgmt.FlagPluginsInstrumentationStatusSource) { - additionalLabels = []string{"status_source"} - } + additionalLabels := []string{"status_source"} pluginRequestCounter := prometheus.NewCounterVec(prometheus.CounterOpts{ Namespace: "grafana", Name: "plugin_request_total", @@ -119,19 +116,11 @@ func (m *MetricsMiddleware) instrumentPluginRequest(ctx context.Context, pluginC status, err := fn(ctx) elapsed := time.Since(start) - pluginRequestDurationLabels := []string{pluginCtx.PluginID, endpoint, target} - pluginRequestCounterLabels := []string{pluginCtx.PluginID, endpoint, status.String(), target} - pluginRequestDurationSecondsLabels := []string{"grafana-backend", pluginCtx.PluginID, endpoint, status.String(), target} - if m.features.IsEnabled(ctx, featuremgmt.FlagPluginsInstrumentationStatusSource) { - statusSource := pluginrequestmeta.StatusSourceFromContext(ctx) - pluginRequestDurationLabels = append(pluginRequestDurationLabels, string(statusSource)) - pluginRequestCounterLabels = append(pluginRequestCounterLabels, string(statusSource)) - pluginRequestDurationSecondsLabels = append(pluginRequestDurationSecondsLabels, string(statusSource)) - } + statusSource := pluginrequestmeta.StatusSourceFromContext(ctx) - pluginRequestDurationWithLabels := m.pluginRequestDuration.WithLabelValues(pluginRequestDurationLabels...) - pluginRequestCounterWithLabels := m.pluginRequestCounter.WithLabelValues(pluginRequestCounterLabels...) - pluginRequestDurationSecondsWithLabels := m.pluginRequestDurationSeconds.WithLabelValues(pluginRequestDurationSecondsLabels...) + pluginRequestDurationWithLabels := m.pluginRequestDuration.WithLabelValues(pluginCtx.PluginID, endpoint, target, string(statusSource)) + pluginRequestCounterWithLabels := m.pluginRequestCounter.WithLabelValues(pluginCtx.PluginID, endpoint, status.String(), target, string(statusSource)) + pluginRequestDurationSecondsWithLabels := m.pluginRequestDurationSeconds.WithLabelValues("grafana-backend", pluginCtx.PluginID, endpoint, status.String(), target, string(statusSource)) if traceID := tracing.TraceIDFromContext(ctx, true); traceID != "" { pluginRequestDurationWithLabels.(prometheus.ExemplarObserver).ObserveWithExemplar( diff --git a/pkg/services/pluginsintegration/clientmiddleware/metrics_middleware_test.go b/pkg/services/pluginsintegration/clientmiddleware/metrics_middleware_test.go index 66a1bc813d..478a65469d 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/metrics_middleware_test.go +++ b/pkg/services/pluginsintegration/clientmiddleware/metrics_middleware_test.go @@ -90,7 +90,7 @@ func TestInstrumentationMiddleware(t *testing.T) { require.Equal(t, 1, testutil.CollectAndCount(promRegistry, metricRequestDurationMs)) require.Equal(t, 1, testutil.CollectAndCount(promRegistry, metricRequestDurationS)) - counter := mw.pluginMetrics.pluginRequestCounter.WithLabelValues(pluginID, tc.expEndpoint, requestStatusOK.String(), string(backendplugin.TargetUnknown)) + counter := mw.pluginMetrics.pluginRequestCounter.WithLabelValues(pluginID, tc.expEndpoint, requestStatusOK.String(), string(backendplugin.TargetUnknown), string(pluginrequestmeta.DefaultStatusSource)) require.Equal(t, 1.0, testutil.ToFloat64(counter)) for _, m := range []string{metricRequestDurationMs, metricRequestDurationS} { require.NoError(t, checkHistogram(promRegistry, m, map[string]string{ @@ -115,12 +115,6 @@ func TestInstrumentationMiddleware(t *testing.T) { func TestInstrumentationMiddlewareStatusSource(t *testing.T) { const labelStatusSource = "status_source" - queryDataOKCounterLabels := prometheus.Labels{ - "plugin_id": pluginID, - "endpoint": endpointQueryData, - "status": requestStatusOK.String(), - "target": string(backendplugin.TargetUnknown), - } queryDataErrorCounterLabels := prometheus.Labels{ "plugin_id": pluginID, "endpoint": endpointQueryData, @@ -159,8 +153,7 @@ func TestInstrumentationMiddlewareStatusSource(t *testing.T) { require.NoError(t, pluginsRegistry.Add(context.Background(), &plugins.Plugin{ JSONData: plugins.JSONData{ID: pluginID, Backend: true}, })) - features := featuremgmt.WithFeatures(featuremgmt.FlagPluginsInstrumentationStatusSource) - metricsMw := newMetricsMiddleware(promRegistry, pluginsRegistry, features) + metricsMw := newMetricsMiddleware(promRegistry, pluginsRegistry, featuremgmt.WithFeatures()) cdt := clienttest.NewClientDecoratorTest(t, clienttest.WithMiddlewares( NewPluginRequestMetaMiddleware(), plugins.ClientMiddlewareFunc(func(next plugins.Client) plugins.Client { @@ -171,53 +164,21 @@ func TestInstrumentationMiddlewareStatusSource(t *testing.T) { )) t.Run("Metrics", func(t *testing.T) { - t.Run("Should ignore ErrorSource if feature flag is disabled", func(t *testing.T) { - // Use different middleware without feature flag - metricsMw := newMetricsMiddleware(prometheus.NewRegistry(), pluginsRegistry, featuremgmt.WithFeatures()) - cdt := clienttest.NewClientDecoratorTest(t, clienttest.WithMiddlewares( - plugins.ClientMiddlewareFunc(func(next plugins.Client) plugins.Client { - metricsMw.next = next - return metricsMw - }), - )) - - cdt.TestClient.QueryDataFunc = func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { - return &backend.QueryDataResponse{Responses: map[string]backend.DataResponse{"A": downstreamErrorResponse}}, nil - } - _, err := cdt.Decorator.QueryData(context.Background(), &backend.QueryDataRequest{PluginContext: pCtx}) - require.NoError(t, err) - counter, err := metricsMw.pluginMetrics.pluginRequestCounter.GetMetricWith(newLabels(queryDataErrorCounterLabels, nil)) - require.NoError(t, err) - require.Equal(t, 1.0, testutil.ToFloat64(counter)) - - // error_source should not be defined at all - _, err = metricsMw.pluginMetrics.pluginRequestCounter.GetMetricWith(newLabels( - queryDataOKCounterLabels, - prometheus.Labels{ - labelStatusSource: string(backend.ErrorSourceDownstream), - }), - ) - require.Error(t, err) - require.ErrorContains(t, err, "inconsistent label cardinality") - }) - - t.Run("Should add error_source label if feature flag is enabled", func(t *testing.T) { - metricsMw.pluginMetrics.pluginRequestCounter.Reset() + metricsMw.pluginMetrics.pluginRequestCounter.Reset() - cdt.TestClient.QueryDataFunc = func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { - return &backend.QueryDataResponse{Responses: map[string]backend.DataResponse{"A": downstreamErrorResponse}}, nil - } - _, err := cdt.Decorator.QueryData(context.Background(), &backend.QueryDataRequest{PluginContext: pCtx}) - require.NoError(t, err) - counter, err := metricsMw.pluginMetrics.pluginRequestCounter.GetMetricWith(newLabels( - queryDataErrorCounterLabels, - prometheus.Labels{ - labelStatusSource: string(backend.ErrorSourceDownstream), - }), - ) - require.NoError(t, err) - require.Equal(t, 1.0, testutil.ToFloat64(counter)) - }) + cdt.TestClient.QueryDataFunc = func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { + return &backend.QueryDataResponse{Responses: map[string]backend.DataResponse{"A": downstreamErrorResponse}}, nil + } + _, err := cdt.Decorator.QueryData(context.Background(), &backend.QueryDataRequest{PluginContext: pCtx}) + require.NoError(t, err) + counter, err := metricsMw.pluginMetrics.pluginRequestCounter.GetMetricWith(newLabels( + queryDataErrorCounterLabels, + prometheus.Labels{ + labelStatusSource: string(backend.ErrorSourceDownstream), + }), + ) + require.NoError(t, err) + require.Equal(t, 1.0, testutil.ToFloat64(counter)) }) t.Run("Priority", func(t *testing.T) { diff --git a/pkg/services/pluginsintegration/pluginsintegration.go b/pkg/services/pluginsintegration/pluginsintegration.go index 32f74d7d40..0a10913d60 100644 --- a/pkg/services/pluginsintegration/pluginsintegration.go +++ b/pkg/services/pluginsintegration/pluginsintegration.go @@ -153,12 +153,8 @@ func NewClientDecorator( } func CreateMiddlewares(cfg *setting.Cfg, oAuthTokenService oauthtoken.OAuthTokenService, tracer tracing.Tracer, cachingService caching.CachingService, features *featuremgmt.FeatureManager, promRegisterer prometheus.Registerer, registry registry.Service) []plugins.ClientMiddleware { - var middlewares []plugins.ClientMiddleware - - if features.IsEnabledGlobally(featuremgmt.FlagPluginsInstrumentationStatusSource) { - middlewares = []plugins.ClientMiddleware{ - clientmiddleware.NewPluginRequestMetaMiddleware(), - } + middlewares := []plugins.ClientMiddleware{ + clientmiddleware.NewPluginRequestMetaMiddleware(), } skipCookiesNames := []string{cfg.LoginCookieName} @@ -189,11 +185,9 @@ func CreateMiddlewares(cfg *setting.Cfg, oAuthTokenService oauthtoken.OAuthToken middlewares = append(middlewares, clientmiddleware.NewHTTPClientMiddleware()) - if features.IsEnabledGlobally(featuremgmt.FlagPluginsInstrumentationStatusSource) { - // StatusSourceMiddleware should be at the very bottom, or any middlewares below it won't see the - // correct status source in their context.Context - middlewares = append(middlewares, clientmiddleware.NewStatusSourceMiddleware()) - } + // StatusSourceMiddleware should be at the very bottom, or any middlewares below it won't see the + // correct status source in their context.Context + middlewares = append(middlewares, clientmiddleware.NewStatusSourceMiddleware()) return middlewares } From 49a3553b9444a8628715b2ef69777fa8c0cf41ad Mon Sep 17 00:00:00 2001 From: Esteban Beltran Date: Wed, 21 Feb 2024 13:31:16 +0100 Subject: [PATCH 0066/1406] Sandbox: Fix custom variable query editors not working inside the sandbox (#83152) --- public/app/features/plugins/sandbox/document_sandbox.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/features/plugins/sandbox/document_sandbox.ts b/public/app/features/plugins/sandbox/document_sandbox.ts index 59de3c0c3d..9dcb1a3a1a 100644 --- a/public/app/features/plugins/sandbox/document_sandbox.ts +++ b/public/app/features/plugins/sandbox/document_sandbox.ts @@ -2,7 +2,7 @@ import { isNearMembraneProxy, ProxyTarget } from '@locker/near-membrane-shared'; import { cloneDeep } from 'lodash'; import Prism from 'prismjs'; -import { DataSourceApi } from '@grafana/data'; +import { CustomVariableSupport, DataSourceApi } from '@grafana/data'; import { config } from '@grafana/runtime'; import { forbiddenElements } from './constants'; @@ -138,7 +138,7 @@ export function patchObjectAsLiveTarget(obj: unknown) { !(obj instanceof Function) && // conditions for allowed objects // react class components - (isReactClassComponent(obj) || obj instanceof DataSourceApi) + (isReactClassComponent(obj) || obj instanceof DataSourceApi || obj instanceof CustomVariableSupport) ) { Reflect.defineProperty(obj, SANDBOX_LIVE_VALUE, {}); } else { From 809c1eaddb7f66260c799b402486a19748c65108 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Wed, 21 Feb 2024 08:04:15 -0500 Subject: [PATCH 0067/1406] Snapshots: delete from same org (#83111) delete in org --- pkg/api/dashboard_snapshot.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pkg/api/dashboard_snapshot.go b/pkg/api/dashboard_snapshot.go index b217d854a5..f5fc19de81 100644 --- a/pkg/api/dashboard_snapshot.go +++ b/pkg/api/dashboard_snapshot.go @@ -192,10 +192,9 @@ func (hs *HTTPServer) DeleteDashboardSnapshot(c *contextmodel.ReqContext) respon return response.Error(http.StatusNotFound, "Failed to get dashboard snapshot", nil) } - // TODO: enforce org ID same - // if queryResult.OrgID != c.OrgID { - // return response.Error(http.StatusUnauthorized, "OrgID mismatch", nil) - // } + if queryResult.OrgID != c.OrgID { + return response.Error(http.StatusUnauthorized, "OrgID mismatch", nil) + } if queryResult.External { err := dashboardsnapshots.DeleteExternalDashboardSnapshot(queryResult.ExternalDeleteURL) From be71277d33e1e7e1bfe3a4ca2922a33aa02b6c90 Mon Sep 17 00:00:00 2001 From: Ben Sully Date: Wed, 21 Feb 2024 13:25:00 +0000 Subject: [PATCH 0068/1406] Plugins: fix loading of modules which resolve to Promises (#82299) * Plugins: fix loading of modules which resolve to Promises Prior to this commit we expected the default export of a plugin module to be an object with a `plugin` field. This is the case for the vast majority of plugins, but if a plugin uses webpack's `asyncWebAssembly` feature then the default export will actually be a promise which resolves to such an object. This commit checks the result of the SystemJS import to make sure it has a `plugin` field. If not, and if the `default` field looks like a Promise, it recursively attempts to resolve the Promise until the object looks like a plugin. I think this may have broken with the SystemJS upgrade (#70068) because it used to work without this change in Grafana 10.1, but it's difficult to say for sure. * Use Promise.resolve instead of await to clean up some logic * Override systemJSPrototype.import instead of handling defaults inside importPluginModule * Add comment to explain why we're overriding systemJS' import Co-authored-by: Jack Westbrook --------- Co-authored-by: Jack Westbrook --- public/app/features/plugins/plugin_loader.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/public/app/features/plugins/plugin_loader.ts b/public/app/features/plugins/plugin_loader.ts index c594b4f0a1..0a2172f0b9 100644 --- a/public/app/features/plugins/plugin_loader.ts +++ b/public/app/features/plugins/plugin_loader.ts @@ -29,6 +29,17 @@ const systemJSPrototype: SystemJSWithLoaderHooks = SystemJS.constructor.prototyp // the content of the plugin code at runtime which can only be done with fetch/eval. systemJSPrototype.shouldFetch = () => true; +const originalImport = systemJSPrototype.import; +// Hook Systemjs import to support plugins that only have a default export. +systemJSPrototype.import = function (...args: Parameters) { + return originalImport.apply(this, args).then((module) => { + if (module && module.__useDefault) { + return module.default; + } + return module; + }); +}; + const systemJSFetch = systemJSPrototype.fetch; systemJSPrototype.fetch = function (url: string, options?: Record) { return decorateSystemJSFetch(systemJSFetch, url, options); From 9bbb7f67e0d2bb884d40d0064de0dfdb5c2f3966 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Wed, 21 Feb 2024 16:32:54 +0300 Subject: [PATCH 0069/1406] Chore: Move store interface to top level (#83153) * Chore: Move store interface to top level * Update store mock --- pkg/services/accesscontrol/accesscontrol.go | 10 +++++ pkg/services/accesscontrol/acimpl/service.go | 14 +------ .../accesscontrol/actest/store_mock.go | 41 +++++++++++++++---- 3 files changed, 44 insertions(+), 21 deletions(-) diff --git a/pkg/services/accesscontrol/accesscontrol.go b/pkg/services/accesscontrol/accesscontrol.go index cc0c60bc27..a4d61ed99c 100644 --- a/pkg/services/accesscontrol/accesscontrol.go +++ b/pkg/services/accesscontrol/accesscontrol.go @@ -46,6 +46,16 @@ type Service interface { SyncUserRoles(ctx context.Context, orgID int64, cmd SyncUserRolesCommand) error } +//go:generate mockery --name Store --structname MockStore --outpkg actest --filename store_mock.go --output ./actest/ +type Store interface { + GetUserPermissions(ctx context.Context, query GetUserPermissionsQuery) ([]Permission, error) + SearchUsersPermissions(ctx context.Context, orgID int64, options SearchOptions) (map[int64][]Permission, error) + GetUsersBasicRoles(ctx context.Context, userFilter []int64, orgID int64) (map[int64][]string, error) + DeleteUserPermissions(ctx context.Context, orgID, userID int64) error + SaveExternalServiceRole(ctx context.Context, cmd SaveExternalServiceRoleCommand) error + DeleteExternalServiceRole(ctx context.Context, externalServiceID string) error +} + type RoleRegistry interface { // RegisterFixedRoles registers all roles declared to AccessControl RegisterFixedRoles(ctx context.Context) error diff --git a/pkg/services/accesscontrol/acimpl/service.go b/pkg/services/accesscontrol/acimpl/service.go index d6118feb16..004e700554 100644 --- a/pkg/services/accesscontrol/acimpl/service.go +++ b/pkg/services/accesscontrol/acimpl/service.go @@ -61,7 +61,7 @@ func ProvideService(cfg *setting.Cfg, db db.DB, routeRegister routing.RouteRegis return service, nil } -func ProvideOSSService(cfg *setting.Cfg, store store, cache *localcache.CacheService, features featuremgmt.FeatureToggles) *Service { +func ProvideOSSService(cfg *setting.Cfg, store accesscontrol.Store, cache *localcache.CacheService, features featuremgmt.FeatureToggles) *Service { s := &Service{ cache: cache, cfg: cfg, @@ -74,16 +74,6 @@ func ProvideOSSService(cfg *setting.Cfg, store store, cache *localcache.CacheSer return s } -//go:generate mockery --name store --structname MockStore --outpkg actest --filename store_mock.go --output ../actest/ -type store interface { - GetUserPermissions(ctx context.Context, query accesscontrol.GetUserPermissionsQuery) ([]accesscontrol.Permission, error) - SearchUsersPermissions(ctx context.Context, orgID int64, options accesscontrol.SearchOptions) (map[int64][]accesscontrol.Permission, error) - GetUsersBasicRoles(ctx context.Context, userFilter []int64, orgID int64) (map[int64][]string, error) - DeleteUserPermissions(ctx context.Context, orgID, userID int64) error - SaveExternalServiceRole(ctx context.Context, cmd accesscontrol.SaveExternalServiceRoleCommand) error - DeleteExternalServiceRole(ctx context.Context, externalServiceID string) error -} - // Service is the service implementing role based access control. type Service struct { cache *localcache.CacheService @@ -92,7 +82,7 @@ type Service struct { log log.Logger registrations accesscontrol.RegistrationList roles map[string]*accesscontrol.RoleDTO - store store + store accesscontrol.Store } func (s *Service) GetUsageStats(_ context.Context) map[string]any { diff --git a/pkg/services/accesscontrol/actest/store_mock.go b/pkg/services/accesscontrol/actest/store_mock.go index 6f38eea198..12ef1560a8 100644 --- a/pkg/services/accesscontrol/actest/store_mock.go +++ b/pkg/services/accesscontrol/actest/store_mock.go @@ -1,16 +1,16 @@ -// Code generated by mockery v2.20.0. DO NOT EDIT. +// Code generated by mockery v2.42.0. DO NOT EDIT. package actest import ( - accesscontrol "github.com/grafana/grafana/pkg/services/accesscontrol" - context "context" + accesscontrol "github.com/grafana/grafana/pkg/services/accesscontrol" + mock "github.com/stretchr/testify/mock" ) -// MockStore is an autogenerated mock type for the store type +// MockStore is an autogenerated mock type for the Store type type MockStore struct { mock.Mock } @@ -19,6 +19,10 @@ type MockStore struct { func (_m *MockStore) DeleteExternalServiceRole(ctx context.Context, externalServiceID string) error { ret := _m.Called(ctx, externalServiceID) + if len(ret) == 0 { + panic("no return value specified for DeleteExternalServiceRole") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { r0 = rf(ctx, externalServiceID) @@ -33,6 +37,10 @@ func (_m *MockStore) DeleteExternalServiceRole(ctx context.Context, externalServ func (_m *MockStore) DeleteUserPermissions(ctx context.Context, orgID int64, userID int64) error { ret := _m.Called(ctx, orgID, userID) + if len(ret) == 0 { + panic("no return value specified for DeleteUserPermissions") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, int64, int64) error); ok { r0 = rf(ctx, orgID, userID) @@ -47,6 +55,10 @@ func (_m *MockStore) DeleteUserPermissions(ctx context.Context, orgID int64, use func (_m *MockStore) GetUserPermissions(ctx context.Context, query accesscontrol.GetUserPermissionsQuery) ([]accesscontrol.Permission, error) { ret := _m.Called(ctx, query) + if len(ret) == 0 { + panic("no return value specified for GetUserPermissions") + } + var r0 []accesscontrol.Permission var r1 error if rf, ok := ret.Get(0).(func(context.Context, accesscontrol.GetUserPermissionsQuery) ([]accesscontrol.Permission, error)); ok { @@ -73,6 +85,10 @@ func (_m *MockStore) GetUserPermissions(ctx context.Context, query accesscontrol func (_m *MockStore) GetUsersBasicRoles(ctx context.Context, userFilter []int64, orgID int64) (map[int64][]string, error) { ret := _m.Called(ctx, userFilter, orgID) + if len(ret) == 0 { + panic("no return value specified for GetUsersBasicRoles") + } + var r0 map[int64][]string var r1 error if rf, ok := ret.Get(0).(func(context.Context, []int64, int64) (map[int64][]string, error)); ok { @@ -99,6 +115,10 @@ func (_m *MockStore) GetUsersBasicRoles(ctx context.Context, userFilter []int64, func (_m *MockStore) SaveExternalServiceRole(ctx context.Context, cmd accesscontrol.SaveExternalServiceRoleCommand) error { ret := _m.Called(ctx, cmd) + if len(ret) == 0 { + panic("no return value specified for SaveExternalServiceRole") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, accesscontrol.SaveExternalServiceRoleCommand) error); ok { r0 = rf(ctx, cmd) @@ -113,6 +133,10 @@ func (_m *MockStore) SaveExternalServiceRole(ctx context.Context, cmd accesscont func (_m *MockStore) SearchUsersPermissions(ctx context.Context, orgID int64, options accesscontrol.SearchOptions) (map[int64][]accesscontrol.Permission, error) { ret := _m.Called(ctx, orgID, options) + if len(ret) == 0 { + panic("no return value specified for SearchUsersPermissions") + } + var r0 map[int64][]accesscontrol.Permission var r1 error if rf, ok := ret.Get(0).(func(context.Context, int64, accesscontrol.SearchOptions) (map[int64][]accesscontrol.Permission, error)); ok { @@ -135,13 +159,12 @@ func (_m *MockStore) SearchUsersPermissions(ctx context.Context, orgID int64, op return r0, r1 } -type mockConstructorTestingTNewMockStore interface { +// NewMockStore creates a new instance of MockStore. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockStore(t interface { mock.TestingT Cleanup(func()) -} - -// NewMockStore creates a new instance of MockStore. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewMockStore(t mockConstructorTestingTNewMockStore) *MockStore { +}) *MockStore { mock := &MockStore{} mock.Mock.Test(t) From 2258e6bd16b830c12af7fc830a253b02d171418f Mon Sep 17 00:00:00 2001 From: Joey <90795735+joey-grafana@users.noreply.github.com> Date: Wed, 21 Feb 2024 13:49:41 +0000 Subject: [PATCH 0070/1406] Traces: Add traces panel suggestion (#83089) * Add traces panel suggestion * Render suggestion * Update styling * Update styling --- .betterer.results | 10 +--- .betterer.results.json | 8 +-- .../Actions/TracePageActions.tsx | 31 +++++------ .../TracePageHeader/TracePageHeader.tsx | 52 ++++++++++--------- .../features/panel/state/getAllSuggestions.ts | 1 + public/app/plugins/panel/traces/module.tsx | 3 +- .../app/plugins/panel/traces/suggestions.ts | 29 +++++++++++ public/app/types/suggestions.ts | 1 + 8 files changed, 79 insertions(+), 56 deletions(-) create mode 100644 public/app/plugins/panel/traces/suggestions.ts diff --git a/.betterer.results b/.betterer.results index 8666780475..bc20a91089 100644 --- a/.betterer.results +++ b/.betterer.results @@ -3339,10 +3339,6 @@ exports[`better eslint`] = { "public/app/features/explore/TraceView/components/TracePageHeader/Actions/ActionButton.tsx:5381": [ [0, 0, 0, "Styles should be written using objects.", "0"] ], - "public/app/features/explore/TraceView/components/TracePageHeader/Actions/TracePageActions.tsx:5381": [ - [0, 0, 0, "Styles should be written using objects.", "0"], - [0, 0, 0, "Styles should be written using objects.", "1"] - ], "public/app/features/explore/TraceView/components/TracePageHeader/SearchBar/NextPrevResult.tsx:5381": [ [0, 0, 0, "Styles should be written using objects.", "0"], [0, 0, 0, "Styles should be written using objects.", "1"], @@ -3402,11 +3398,7 @@ exports[`better eslint`] = { [0, 0, 0, "Styles should be written using objects.", "5"], [0, 0, 0, "Styles should be written using objects.", "6"], [0, 0, 0, "Styles should be written using objects.", "7"], - [0, 0, 0, "Styles should be written using objects.", "8"], - [0, 0, 0, "Styles should be written using objects.", "9"], - [0, 0, 0, "Styles should be written using objects.", "10"], - [0, 0, 0, "Styles should be written using objects.", "11"], - [0, 0, 0, "Styles should be written using objects.", "12"] + [0, 0, 0, "Styles should be written using objects.", "8"] ], "public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanBarRow.tsx:5381": [ [0, 0, 0, "Styles should be written using objects.", "0"], diff --git a/.betterer.results.json b/.betterer.results.json index 599a975db1..d5db475d6b 100644 --- a/.betterer.results.json +++ b/.betterer.results.json @@ -3838,12 +3838,6 @@ "count": 1 } ], - "/public/app/features/explore/TraceView/components/TracePageHeader/Actions/TracePageActions.tsx": [ - { - "message": "Styles should be written using objects.", - "count": 2 - } - ], "/public/app/features/explore/TraceView/components/TracePageHeader/SearchBar/NextPrevResult.tsx": [ { "message": "Styles should be written using objects.", @@ -3895,7 +3889,7 @@ "/public/app/features/explore/TraceView/components/TracePageHeader/TracePageHeader.tsx": [ { "message": "Styles should be written using objects.", - "count": 13 + "count": 9 } ], "/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanBarRow.tsx": [ diff --git a/public/app/features/explore/TraceView/components/TracePageHeader/Actions/TracePageActions.tsx b/public/app/features/explore/TraceView/components/TracePageHeader/Actions/TracePageActions.tsx index d71b4977ce..5595caf1e9 100644 --- a/public/app/features/explore/TraceView/components/TracePageHeader/Actions/TracePageActions.tsx +++ b/public/app/features/explore/TraceView/components/TracePageHeader/Actions/TracePageActions.tsx @@ -12,21 +12,22 @@ import ActionButton from './ActionButton'; export const getStyles = (theme: GrafanaTheme2) => { return { - TracePageActions: css` - label: TracePageActions; - display: flex; - align-items: center; - justify-content: center; - gap: 4px; - `, - feedback: css` - margin: 6px; - color: ${theme.colors.text.secondary}; - font-size: ${theme.typography.bodySmall.fontSize}; - &:hover { - color: ${theme.colors.text.link}; - } - `, + TracePageActions: css({ + label: 'TracePageActions', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + gap: '4px', + marginBottom: '10px', + }), + feedback: css({ + margin: '6px 6px 6px 0', + color: theme.colors.text.secondary, + fontSize: theme.typography.bodySmall.fontSize, + '&:hover': { + color: theme.colors.text.link, + }, + }), }; }; diff --git a/public/app/features/explore/TraceView/components/TracePageHeader/TracePageHeader.tsx b/public/app/features/explore/TraceView/components/TracePageHeader/TracePageHeader.tsx index dc310cc4bc..a39672f965 100644 --- a/public/app/features/explore/TraceView/components/TracePageHeader/TracePageHeader.tsx +++ b/public/app/features/explore/TraceView/components/TracePageHeader/TracePageHeader.tsx @@ -202,30 +202,34 @@ const getNewStyles = (theme: GrafanaTheme2) => { color: unset; } `, - header: css` - label: TracePageHeader; - background-color: ${theme.colors.background.primary}; - padding: 0.5em 0 0 0; - position: sticky; - top: 0; - z-index: 5; - `, - titleRow: css` - align-items: flex-start; - display: flex; - padding: 0 8px; - `, - title: css` - color: inherit; - flex: 1; - font-size: 1.7em; - line-height: 1em; - `, - subtitle: css` - flex: 1; - line-height: 1em; - margin: -0.5em 0.5em 0.75em 0.5em; - `, + header: css({ + label: 'TracePageHeader', + backgroundColor: theme.colors.background.primary, + padding: '0.5em 0 0 0', + position: 'sticky', + top: 0, + zIndex: 5, + textAlign: 'left', + }), + titleRow: css({ + alignItems: 'flex-start', + display: 'flex', + padding: '0 8px', + flexWrap: 'wrap', + }), + title: css({ + color: 'inherit', + flex: 1, + fontSize: '1.7em', + lineHeight: '1em', + marginBottom: 0, + minWidth: '200px', + }), + subtitle: css({ + flex: 1, + lineHeight: '1em', + margin: '-0.5em 0.5em 0.75em 0.5em', + }), tag: css` margin: 0 0.5em 0 0; `, diff --git a/public/app/features/panel/state/getAllSuggestions.ts b/public/app/features/panel/state/getAllSuggestions.ts index a9d8efdb85..ee092d3b58 100644 --- a/public/app/features/panel/state/getAllSuggestions.ts +++ b/public/app/features/panel/state/getAllSuggestions.ts @@ -21,6 +21,7 @@ export const panelsToCheckFirst = [ 'logs', 'candlestick', 'flamegraph', + 'traces', ]; export async function getAllSuggestions(data?: PanelData, panel?: PanelModel): Promise { diff --git a/public/app/plugins/panel/traces/module.tsx b/public/app/plugins/panel/traces/module.tsx index 06b2f4fee4..19fae657d5 100644 --- a/public/app/plugins/panel/traces/module.tsx +++ b/public/app/plugins/panel/traces/module.tsx @@ -1,5 +1,6 @@ import { PanelPlugin } from '@grafana/data'; import { TracesPanel } from './TracesPanel'; +import { TracesSuggestionsSupplier } from './suggestions'; -export const plugin = new PanelPlugin(TracesPanel); +export const plugin = new PanelPlugin(TracesPanel).setSuggestionsSupplier(new TracesSuggestionsSupplier()); diff --git a/public/app/plugins/panel/traces/suggestions.ts b/public/app/plugins/panel/traces/suggestions.ts new file mode 100644 index 0000000000..bda2e46392 --- /dev/null +++ b/public/app/plugins/panel/traces/suggestions.ts @@ -0,0 +1,29 @@ +import { VisualizationSuggestionsBuilder, VisualizationSuggestionScore } from '@grafana/data'; +import { SuggestionName } from 'app/types/suggestions'; + +export class TracesSuggestionsSupplier { + getListWithDefaults(builder: VisualizationSuggestionsBuilder) { + return builder.getListAppender<{}, {}>({ + name: SuggestionName.Trace, + pluginId: 'traces', + }); + } + + getSuggestionsForData(builder: VisualizationSuggestionsBuilder) { + if (!builder.data) { + return; + } + + const dataFrame = builder.data.series[0]; + if (!dataFrame) { + return; + } + + if (builder.data.series[0].meta?.preferredVisualisationType === 'trace') { + this.getListWithDefaults(builder).append({ + name: SuggestionName.Trace, + score: VisualizationSuggestionScore.Best, + }); + } + } +} diff --git a/public/app/types/suggestions.ts b/public/app/types/suggestions.ts index 95a10a419f..5d6c813d34 100644 --- a/public/app/types/suggestions.ts +++ b/public/app/types/suggestions.ts @@ -28,4 +28,5 @@ export enum SuggestionName { DashboardList = 'Dashboard list', Logs = 'Logs', FlameGraph = 'Flame graph', + Trace = 'Trace', } From 028d0d0c2cdc490303d82717b2a55e9fd6fee645 Mon Sep 17 00:00:00 2001 From: Carl Bergquist Date: Wed, 21 Feb 2024 15:32:57 +0100 Subject: [PATCH 0071/1406] Rename scope.name to scope.title since name exists in metadata. (#83172) name is part of metadata which is confusing Signed-off-by: bergquist --- .../v0alpha1/zz_generated.openapi.go | 10 +++++- .../query/v0alpha1/zz_generated.openapi.go | 35 +++++++++++++++++++ ...enerated.openapi_violation_exceptions.list | 3 ++ pkg/apis/scope/v0alpha1/types.go | 2 +- .../scope/v0alpha1/zz_generated.openapi.go | 4 +-- 5 files changed, 50 insertions(+), 4 deletions(-) diff --git a/pkg/apis/featuretoggle/v0alpha1/zz_generated.openapi.go b/pkg/apis/featuretoggle/v0alpha1/zz_generated.openapi.go index 94ddee37f4..784d4201be 100644 --- a/pkg/apis/featuretoggle/v0alpha1/zz_generated.openapi.go +++ b/pkg/apis/featuretoggle/v0alpha1/zz_generated.openapi.go @@ -391,6 +391,14 @@ func schema_pkg_apis_featuretoggle_v0alpha1_ToggleStatus(ref common.ReferenceCal Format: "", }, }, + "stage": { + SchemaProps: spec.SchemaProps{ + Description: "The feature toggle stage", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, "enabled": { SchemaProps: spec.SchemaProps{ Description: "Is the flag enabled", @@ -421,7 +429,7 @@ func schema_pkg_apis_featuretoggle_v0alpha1_ToggleStatus(ref common.ReferenceCal }, }, }, - Required: []string{"name", "enabled", "writeable"}, + Required: []string{"name", "stage", "enabled", "writeable"}, }, }, Dependencies: []string{ diff --git a/pkg/apis/query/v0alpha1/zz_generated.openapi.go b/pkg/apis/query/v0alpha1/zz_generated.openapi.go index a0339d8804..48619ed2dc 100644 --- a/pkg/apis/query/v0alpha1/zz_generated.openapi.go +++ b/pkg/apis/query/v0alpha1/zz_generated.openapi.go @@ -28,6 +28,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/grafana/grafana/pkg/apis/query/v0alpha1/template.Target": schema_apis_query_v0alpha1_template_Target(ref), "github.com/grafana/grafana/pkg/apis/query/v0alpha1/template.TemplateVariable": schema_apis_query_v0alpha1_template_TemplateVariable(ref), "github.com/grafana/grafana/pkg/apis/query/v0alpha1/template.VariableReplacement": schema_apis_query_v0alpha1_template_VariableReplacement(ref), + "github.com/grafana/grafana/pkg/apis/query/v0alpha1/template.replacement": schema_apis_query_v0alpha1_template_replacement(ref), } } @@ -584,3 +585,37 @@ func schema_apis_query_v0alpha1_template_VariableReplacement(ref common.Referenc "github.com/grafana/grafana/pkg/apis/query/v0alpha1/template.Position"}, } } + +func schema_apis_query_v0alpha1_template_replacement(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "Position": { + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/grafana/grafana/pkg/apis/query/v0alpha1/template.Position"), + }, + }, + "TemplateVariable": { + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/grafana/grafana/pkg/apis/query/v0alpha1/template.TemplateVariable"), + }, + }, + "format": { + SchemaProps: spec.SchemaProps{ + Description: "Possible enum values:\n - `\"csv\"` Formats variables with multiple values as a comma-separated string.\n - `\"doublequote\"` Formats single- and multi-valued variables into a comma-separated string\n - `\"json\"` Formats variables with multiple values as a comma-separated string.\n - `\"pipe\"` Formats variables with multiple values into a pipe-separated string.\n - `\"raw\"` Formats variables with multiple values into comma-separated string. This is the default behavior when no format is specified\n - `\"singlequote\"` Formats single- and multi-valued variables into a comma-separated string", + Default: "", + Type: []string{"string"}, + Format: "", + Enum: []interface{}{"csv", "doublequote", "json", "pipe", "raw", "singlequote"}, + }, + }, + }, + Required: []string{"Position", "TemplateVariable", "format"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/pkg/apis/query/v0alpha1/template.Position", "github.com/grafana/grafana/pkg/apis/query/v0alpha1/template.TemplateVariable"}, + } +} diff --git a/pkg/apis/query/v0alpha1/zz_generated.openapi_violation_exceptions.list b/pkg/apis/query/v0alpha1/zz_generated.openapi_violation_exceptions.list index 00e608f512..dfff7a33ba 100644 --- a/pkg/apis/query/v0alpha1/zz_generated.openapi_violation_exceptions.list +++ b/pkg/apis/query/v0alpha1/zz_generated.openapi_violation_exceptions.list @@ -4,3 +4,6 @@ API rule violation: names_match,github.com/grafana/grafana/pkg/apis/query/v0alph API rule violation: names_match,github.com/grafana/grafana/pkg/apis/query/v0alpha1,GenericDataQuery,RefID API rule violation: names_match,github.com/grafana/grafana/pkg/apis/query/v0alpha1,QueryDataResponse,QueryDataResponse API rule violation: names_match,github.com/grafana/grafana/pkg/apis/query/v0alpha1/template,QueryTemplate,Variables +API rule violation: names_match,github.com/grafana/grafana/pkg/apis/query/v0alpha1/template,replacement,Position +API rule violation: names_match,github.com/grafana/grafana/pkg/apis/query/v0alpha1/template,replacement,TemplateVariable +API rule violation: names_match,github.com/grafana/grafana/pkg/apis/query/v0alpha1/template,replacement,format diff --git a/pkg/apis/scope/v0alpha1/types.go b/pkg/apis/scope/v0alpha1/types.go index 39430b309e..4610e66c52 100644 --- a/pkg/apis/scope/v0alpha1/types.go +++ b/pkg/apis/scope/v0alpha1/types.go @@ -13,7 +13,7 @@ type Scope struct { } type ScopeSpec struct { - Name string `json:"name"` + Title string `json:"title"` Type string `json:"type"` Description string `json:"description"` Category string `json:"category"` diff --git a/pkg/apis/scope/v0alpha1/zz_generated.openapi.go b/pkg/apis/scope/v0alpha1/zz_generated.openapi.go index 561cb14961..c519a61609 100644 --- a/pkg/apis/scope/v0alpha1/zz_generated.openapi.go +++ b/pkg/apis/scope/v0alpha1/zz_generated.openapi.go @@ -178,7 +178,7 @@ func schema_pkg_apis_scope_v0alpha1_ScopeSpec(ref common.ReferenceCallback) comm SchemaProps: spec.SchemaProps{ Type: []string{"object"}, Properties: map[string]spec.Schema{ - "name": { + "title": { SchemaProps: spec.SchemaProps{ Default: "", Type: []string{"string"}, @@ -220,7 +220,7 @@ func schema_pkg_apis_scope_v0alpha1_ScopeSpec(ref common.ReferenceCallback) comm }, }, }, - Required: []string{"name", "type", "description", "category", "filters"}, + Required: []string{"title", "type", "description", "category", "filters"}, }, }, Dependencies: []string{ From 1394b3341fd729db069d244aec227b9b2cd070dc Mon Sep 17 00:00:00 2001 From: Khushi Jain Date: Wed, 21 Feb 2024 20:07:40 +0530 Subject: [PATCH 0072/1406] Chore: Remove gf-form from datasource/loki and datasource/cloud-monitoring (#80117) * Chore: Remove gf-form from datasource/loki * remove query field * update betterer --- .betterer.results | 15 --------- .../components/AnnotationsHelp.tsx | 2 +- .../components/AnnotationsQueryEditor.tsx | 11 +++---- .../loki/components/LokiOptionFields.tsx | 33 ++++--------------- 4 files changed, 13 insertions(+), 48 deletions(-) diff --git a/.betterer.results b/.betterer.results index bc20a91089..5c20e6abc9 100644 --- a/.betterer.results +++ b/.betterer.results @@ -5231,10 +5231,6 @@ exports[`better eslint`] = { [0, 0, 0, "Styles should be written using objects.", "12"], [0, 0, 0, "Styles should be written using objects.", "13"] ], - "public/app/plugins/datasource/loki/components/LokiOptionFields.tsx:5381": [ - [0, 0, 0, "Styles should be written using objects.", "0"], - [0, 0, 0, "Styles should be written using objects.", "1"] - ], "public/app/plugins/datasource/loki/components/monaco-query-field/MonacoQueryField.tsx:5381": [ [0, 0, 0, "Styles should be written using objects.", "0"], [0, 0, 0, "Styles should be written using objects.", "1"] @@ -6843,9 +6839,6 @@ exports[`no gf-form usage`] = { [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"] ], - "public/app/plugins/datasource/cloud-monitoring/components/AnnotationsHelp.tsx:5381": [ - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"] - ], "public/app/plugins/datasource/cloudwatch/components/ConfigEditor/ConfigEditor.tsx:5381": [ [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"] ], @@ -6935,14 +6928,6 @@ exports[`no gf-form usage`] = { [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"] ], - "public/app/plugins/datasource/loki/components/AnnotationsQueryEditor.tsx:5381": [ - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"] - ], - "public/app/plugins/datasource/loki/components/LokiOptionFields.tsx:5381": [ - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"] - ], "public/app/plugins/datasource/loki/components/LokiQueryField.tsx:5381": [ [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], diff --git a/public/app/plugins/datasource/cloud-monitoring/components/AnnotationsHelp.tsx b/public/app/plugins/datasource/cloud-monitoring/components/AnnotationsHelp.tsx index a9ba642e39..8970adbe0a 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/AnnotationsHelp.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/AnnotationsHelp.tsx @@ -2,7 +2,7 @@ import React from 'react'; export const AnnotationsHelp = () => { return ( -
+
Annotation Query Format

diff --git a/public/app/plugins/datasource/loki/components/AnnotationsQueryEditor.tsx b/public/app/plugins/datasource/loki/components/AnnotationsQueryEditor.tsx index 3970e45497..670288224a 100644 --- a/public/app/plugins/datasource/loki/components/AnnotationsQueryEditor.tsx +++ b/public/app/plugins/datasource/loki/components/AnnotationsQueryEditor.tsx @@ -3,7 +3,7 @@ import React, { memo } from 'react'; import { AnnotationQuery } from '@grafana/data'; import { EditorField, EditorRow } from '@grafana/experimental'; -import { Input } from '@grafana/ui'; +import { Input, Stack } from '@grafana/ui'; // Types import { getNormalizedLokiQuery } from '../queryUtils'; @@ -49,8 +49,8 @@ export const LokiAnnotationsQueryEditor = memo(function LokiAnnotationQueryEdito queryType: annotation.queryType, }; return ( - <> -

+ + } /> -
- + - + ); }); diff --git a/public/app/plugins/datasource/loki/components/LokiOptionFields.tsx b/public/app/plugins/datasource/loki/components/LokiOptionFields.tsx index 9b025f29b5..8bada9ae24 100644 --- a/public/app/plugins/datasource/loki/components/LokiOptionFields.tsx +++ b/public/app/plugins/datasource/loki/components/LokiOptionFields.tsx @@ -1,12 +1,11 @@ // Libraries -import { css, cx } from '@emotion/css'; import { map } from 'lodash'; import React, { memo } from 'react'; // Types import { SelectableValue } from '@grafana/data'; import { config } from '@grafana/runtime'; -import { InlineFormLabel, RadioButtonGroup, InlineField, Input, Select } from '@grafana/ui'; +import { InlineFormLabel, RadioButtonGroup, InlineField, Input, Select, Stack } from '@grafana/ui'; import { getLokiQueryType } from '../queryUtils'; import { LokiQuery, LokiQueryType } from '../types'; @@ -82,18 +81,9 @@ export function LokiOptionFields(props: LokiOptionFieldsProps) { } return ( -
+ {/*Query type field*/} -
+ Query type -
+
{/*Line limit field*/} -
+ -
-
+ + ); } From 7e8b679237328c6df9d6f52d61751b980c38b902 Mon Sep 17 00:00:00 2001 From: linoman <2051016+linoman@users.noreply.github.com> Date: Wed, 21 Feb 2024 08:40:18 -0600 Subject: [PATCH 0073/1406] OAuth: Improve domain validation (#83110) * enforce hd claim validation * add tests --- pkg/login/social/connectors/google_oauth.go | 20 +++++++++ .../social/connectors/google_oauth_test.go | 44 +++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/pkg/login/social/connectors/google_oauth.go b/pkg/login/social/connectors/google_oauth.go index 6709d4e05c..052691bb75 100644 --- a/pkg/login/social/connectors/google_oauth.go +++ b/pkg/login/social/connectors/google_oauth.go @@ -17,6 +17,7 @@ import ( ssoModels "github.com/grafana/grafana/pkg/services/ssosettings/models" "github.com/grafana/grafana/pkg/services/ssosettings/validation" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/util/errutil" ) const ( @@ -37,6 +38,7 @@ type googleUserData struct { Email string `json:"email"` Name string `json:"name"` EmailVerified bool `json:"email_verified"` + HD string `json:"hd"` rawJSON []byte `json:"-"` } @@ -115,6 +117,10 @@ func (s *SocialGoogle) UserInfo(ctx context.Context, client *http.Client, token return nil, fmt.Errorf("user email is not verified") } + if err := s.isHDAllowed(data.HD); err != nil { + return nil, err + } + groups, errPage := s.retrieveGroups(ctx, client, data) if errPage != nil { s.log.Warn("Error retrieving groups", "error", errPage) @@ -290,3 +296,17 @@ func (s *SocialGoogle) getGroupsPage(ctx context.Context, client *http.Client, u return &data, nil } + +func (s *SocialGoogle) isHDAllowed(hd string) error { + if len(s.info.AllowedDomains) == 0 { + return nil + } + + for _, allowedDomain := range s.info.AllowedDomains { + if hd == allowedDomain { + return nil + } + } + + return errutil.Forbidden("the hd claim found in the ID token is not present in the allowed domains", errutil.WithPublicMessage("Invalid domain")) +} diff --git a/pkg/login/social/connectors/google_oauth_test.go b/pkg/login/social/connectors/google_oauth_test.go index 95345c94e5..7fe0d8b024 100644 --- a/pkg/login/social/connectors/google_oauth_test.go +++ b/pkg/login/social/connectors/google_oauth_test.go @@ -890,3 +890,47 @@ func TestSocialGoogle_Reload(t *testing.T) { }) } } + +func TestIsHDAllowed(t *testing.T) { + testCases := []struct { + name string + email string + allowedDomains []string + expectedErrorMessage string + }{ + { + name: "should not fail if no allowed domains are set", + email: "mycompany.com", + allowedDomains: []string{}, + expectedErrorMessage: "", + }, + { + name: "should not fail if email is from allowed domain", + email: "mycompany.com", + allowedDomains: []string{"grafana.com", "mycompany.com", "example.com"}, + expectedErrorMessage: "", + }, + { + name: "should fail if email is not from allowed domain", + email: "mycompany.com", + allowedDomains: []string{"grafana.com", "example.com"}, + expectedErrorMessage: "the hd claim found in the ID token is not present in the allowed domains", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + info := &social.OAuthInfo{} + info.AllowedDomains = tc.allowedDomains + s := NewGoogleProvider(info, &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures()) + err := s.isHDAllowed(tc.email) + + if tc.expectedErrorMessage != "" { + require.Error(t, err) + require.Contains(t, err.Error(), tc.expectedErrorMessage) + } else { + require.NoError(t, err) + } + }) + } +} From e1edec02d01e5b0ca34972d6d040f84ded91925f Mon Sep 17 00:00:00 2001 From: Sergej-Vlasov <37613182+Sergej-Vlasov@users.noreply.github.com> Date: Wed, 21 Feb 2024 17:16:57 +0200 Subject: [PATCH 0074/1406] DashboardScene: Validate variable name in scenes variable editor (#82415) * add variable name validation * adjust variable name validation logic * move variable name validation logic to model * add tests for onValidateVariableName * extract variable name validation itest into separate describe --- .../settings/VariablesEditView.test.tsx | 44 +++++++++++++++++++ .../settings/VariablesEditView.tsx | 42 +++++++++++++++++- .../settings/variables/VariableEditorForm.tsx | 36 ++++++++++++--- .../settings/variables/utils.ts | 3 ++ 4 files changed, 117 insertions(+), 8 deletions(-) diff --git a/public/app/features/dashboard-scene/settings/VariablesEditView.test.tsx b/public/app/features/dashboard-scene/settings/VariablesEditView.test.tsx index 8f67633d5a..02d0232e89 100644 --- a/public/app/features/dashboard-scene/settings/VariablesEditView.test.tsx +++ b/public/app/features/dashboard-scene/settings/VariablesEditView.test.tsx @@ -17,6 +17,7 @@ import { SceneGridLayout, VizPanel, AdHocFiltersVariable, + SceneVariableState, } from '@grafana/scenes'; import { mockDataSource } from 'app/features/alerting/unified/mocks'; import { LegacyVariableQueryEditor } from 'app/features/variables/editor/LegacyVariableQueryEditor'; @@ -206,6 +207,49 @@ describe('VariablesEditView', () => { }); }); + describe('Variables name validation', () => { + let variableView: VariablesEditView; + let variable1: SceneVariableState; + let variable2: SceneVariableState; + + beforeAll(async () => { + const result = await buildTestScene(); + variableView = result.variableView; + + const variables = variableView.getVariables(); + variable1 = variables[0].state; + variable2 = variables[1].state; + }); + + it('should not return error on same name and key', () => { + expect(variableView.onValidateVariableName(variable1.name, variable1.key)[0]).toBe(false); + }); + + it('should not return error if name is unique', () => { + expect(variableView.onValidateVariableName('unique_variable_name', variable1.key)[0]).toBe(false); + }); + + it('should return error if global variable name is used', () => { + expect(variableView.onValidateVariableName('__', variable1.key)[0]).toBe(true); + }); + + it('should not return error if global variable name is used not at the beginning ', () => { + expect(variableView.onValidateVariableName('test__', variable1.key)[0]).toBe(false); + }); + + it('should return error if name is empty', () => { + expect(variableView.onValidateVariableName('', variable1.key)[0]).toBe(true); + }); + + it('should return error if non word characters are used', () => { + expect(variableView.onValidateVariableName('-', variable1.key)[0]).toBe(true); + }); + + it('should return error if variable name is taken', () => { + expect(variableView.onValidateVariableName(variable2.name, variable1.key)[0]).toBe(true); + }); + }); + describe('Dashboard Variables dependencies', () => { let variableView: VariablesEditView; let dashboard: DashboardScene; diff --git a/public/app/features/dashboard-scene/settings/VariablesEditView.tsx b/public/app/features/dashboard-scene/settings/VariablesEditView.tsx index 84fe03a8f9..c6c97d9655 100644 --- a/public/app/features/dashboard-scene/settings/VariablesEditView.tsx +++ b/public/app/features/dashboard-scene/settings/VariablesEditView.tsx @@ -12,7 +12,13 @@ import { EditListViewSceneUrlSync } from './EditListViewSceneUrlSync'; import { DashboardEditView, DashboardEditViewState, useDashboardEditPageNav } from './utils'; import { VariableEditorForm } from './variables/VariableEditorForm'; import { VariableEditorList } from './variables/VariableEditorList'; -import { EditableVariableType, getVariableDefault, getVariableScene } from './variables/utils'; +import { + EditableVariableType, + RESERVED_GLOBAL_VARIABLE_NAME_REGEX, + WORD_CHARACTERS_REGEX, + getVariableDefault, + getVariableScene, +} from './variables/utils'; export interface VariablesEditViewState extends DashboardEditViewState { editIndex?: number | undefined; } @@ -168,6 +174,29 @@ export class VariablesEditView extends SceneObjectBase i public onGoBack = () => { this.setState({ editIndex: undefined }); }; + + public onValidateVariableName = (name: string, key: string | undefined): [true, string] | [false, null] => { + let errorText = null; + if (!RESERVED_GLOBAL_VARIABLE_NAME_REGEX.test(name)) { + errorText = "Template names cannot begin with '__', that's reserved for Grafana's global variables"; + } + + if (!WORD_CHARACTERS_REGEX.test(name)) { + errorText = 'Only word characters are allowed in variable names'; + } + + const variable = this.getVariableSet().getByName(name)?.state; + + if (variable && variable.key !== key) { + errorText = 'Variable with the same name already exists'; + } + + if (errorText) { + return [true, errorText]; + } + + return [false, null]; + }; } function VariableEditorSettingsListView({ model }: SceneComponentProps) { @@ -190,6 +219,7 @@ function VariableEditorSettingsListView({ model }: SceneComponentProps ); } @@ -218,6 +248,7 @@ interface VariableEditorSettingsEditViewProps { onTypeChange: (variableType: EditableVariableType) => void; onGoBack: () => void; onDelete: (variableName: string) => void; + onValidateVariableName: (name: string, key: string | undefined) => [true, string] | [false, null]; } function VariableEditorSettingsView({ @@ -228,6 +259,7 @@ function VariableEditorSettingsView({ onTypeChange, onGoBack, onDelete, + onValidateVariableName, }: VariableEditorSettingsEditViewProps) { const parentTab = pageNav.children!.find((p) => p.active)!; parentTab.parentItem = pageNav; @@ -240,7 +272,13 @@ function VariableEditorSettingsView({ return ( - + ); } diff --git a/public/app/features/dashboard-scene/settings/variables/VariableEditorForm.tsx b/public/app/features/dashboard-scene/settings/variables/VariableEditorForm.tsx index 4f0ab76a58..d04e228acc 100644 --- a/public/app/features/dashboard-scene/settings/variables/VariableEditorForm.tsx +++ b/public/app/features/dashboard-scene/settings/variables/VariableEditorForm.tsx @@ -1,5 +1,5 @@ import { css } from '@emotion/css'; -import React, { FormEvent } from 'react'; +import React, { FormEvent, useCallback, useState } from 'react'; import { useAsyncFn } from 'react-use'; import { lastValueFrom } from 'rxjs'; @@ -24,23 +24,44 @@ interface VariableEditorFormProps { onTypeChange: (type: EditableVariableType) => void; onGoBack: () => void; onDelete: (variableName: string) => void; + onValidateVariableName: (name: string, key: string | undefined) => [true, string] | [false, null]; } - -export function VariableEditorForm({ variable, onTypeChange, onGoBack, onDelete }: VariableEditorFormProps) { +export function VariableEditorForm({ + variable, + onTypeChange, + onGoBack, + onDelete, + onValidateVariableName, +}: VariableEditorFormProps) { const styles = useStyles2(getStyles); - const { name, type, label, description, hide } = variable.useState(); + const [nameError, setNameError] = useState(null); + const { name, type, label, description, hide, key } = variable.useState(); const EditorToRender = isEditableVariableType(type) ? getVariableEditor(type) : undefined; const [runQueryState, onRunQuery] = useAsyncFn(async () => { await lastValueFrom(variable.validateAndUpdate!()); }, [variable]); - const onVariableTypeChange = (option: SelectableValue) => { if (option.value) { onTypeChange(option.value); } }; - const onNameBlur = (e: FormEvent) => variable.setState({ name: e.currentTarget.value }); + const onNameChange = useCallback( + (e: FormEvent) => { + const [, errorMessage] = onValidateVariableName(e.currentTarget.value, key); + if (nameError !== errorMessage) { + setNameError(errorMessage); + } + }, + [key, nameError, onValidateVariableName] + ); + + const onNameBlur = (e: FormEvent) => { + if (!nameError) { + variable.setState({ name: e.currentTarget.value }); + } + }; + const onLabelBlur = (e: FormEvent) => variable.setState({ label: e.currentTarget.value }); const onDescriptionBlur = (e: FormEvent) => variable.setState({ description: e.currentTarget.value }); @@ -64,10 +85,13 @@ export function VariableEditorForm({ variable, onTypeChange, onGoBack, onDelete description="The name of the template variable. (Max. 50 characters)" placeholder="Variable name" defaultValue={name ?? ''} + onChange={onNameChange} onBlur={onNameBlur} testId={selectors.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2} maxLength={VariableNameConstraints.MaxSize} required + invalid={!!nameError} + error={nameError} /> Date: Wed, 21 Feb 2024 15:50:13 +0000 Subject: [PATCH 0075/1406] Cloud migration UI: Add `migrate-to-cloud` route (#83072) * add migrate-to-cloud route * fix chunk name * gate route behind feature toggle * update permission checks --- pkg/services/navtree/models.go | 1 + pkg/services/navtree/navtreeimpl/admin.go | 10 ++++++++++ public/app/core/utils/navBarItem-translations.ts | 7 +++++++ .../features/admin/migrate-to-cloud/MigrateToCloud.tsx | 7 +++++++ public/app/routes/routes.tsx | 7 +++++++ public/locales/en-US/grafana.json | 4 ++++ public/locales/pseudo-LOCALE/grafana.json | 4 ++++ 7 files changed, 40 insertions(+) create mode 100644 public/app/features/admin/migrate-to-cloud/MigrateToCloud.tsx diff --git a/pkg/services/navtree/models.go b/pkg/services/navtree/models.go index 99abe7ac4e..96a249efb5 100644 --- a/pkg/services/navtree/models.go +++ b/pkg/services/navtree/models.go @@ -142,6 +142,7 @@ func (root *NavTreeRoot) ApplyAdminIA() { generalNodeLinks = AppendIfNotNil(generalNodeLinks, root.FindById("global-orgs")) generalNodeLinks = AppendIfNotNil(generalNodeLinks, root.FindById("feature-toggles")) generalNodeLinks = AppendIfNotNil(generalNodeLinks, root.FindById("storage")) + generalNodeLinks = AppendIfNotNil(generalNodeLinks, root.FindById("migrate-to-cloud")) generalNode := &NavLink{ Text: "General", diff --git a/pkg/services/navtree/navtreeimpl/admin.go b/pkg/services/navtree/navtreeimpl/admin.go index ae85a68a9d..ce30488a36 100644 --- a/pkg/services/navtree/navtreeimpl/admin.go +++ b/pkg/services/navtree/navtreeimpl/admin.go @@ -135,6 +135,16 @@ func (s *ServiceImpl) getAdminNode(c *contextmodel.ReqContext) (*navtree.NavLink configNodes = append(configNodes, storage) } + if s.features.IsEnabled(ctx, featuremgmt.FlagOnPremToCloudMigrations) && c.SignedInUser.IsGrafanaAdmin { + migrateToCloud := &navtree.NavLink{ + Text: "Migrate to Grafana Cloud", + Id: "migrate-to-cloud", + SubTitle: "Copy data sources, dashboards, and alerts from this installation to a cloud stack", + Url: s.cfg.AppSubURL + "/admin/migrate-to-cloud", + } + configNodes = append(configNodes, migrateToCloud) + } + configNode := &navtree.NavLink{ Id: navtree.NavIDCfg, Text: "Administration", diff --git a/public/app/core/utils/navBarItem-translations.ts b/public/app/core/utils/navBarItem-translations.ts index 7b246a49f8..c1353ed8cb 100644 --- a/public/app/core/utils/navBarItem-translations.ts +++ b/public/app/core/utils/navBarItem-translations.ts @@ -118,6 +118,8 @@ export function getNavTitle(navId: string | undefined) { return t('nav.server-settings.title', 'Settings'); case 'storage': return t('nav.storage.title', 'Storage'); + case 'migrate-to-cloud': + return t('nav.migrate-to-cloud.title', 'Migrate to Grafana Cloud'); case 'upgrading': return t('nav.upgrading.title', 'Stats and license'); case 'monitoring': @@ -248,6 +250,11 @@ export function getNavSubTitle(navId: string | undefined) { return t('nav.server-settings.subtitle', 'View the settings defined in your Grafana config'); case 'storage': return t('nav.storage.subtitle', 'Manage file storage'); + case 'migrate-to-cloud': + return t( + 'nav.migrate-to-cloud.subtitle', + 'Copy data sources, dashboards, and alerts from this installation to a cloud stack' + ); case 'support-bundles': return t('nav.support-bundles.subtitle', 'Download support bundles'); case 'admin': diff --git a/public/app/features/admin/migrate-to-cloud/MigrateToCloud.tsx b/public/app/features/admin/migrate-to-cloud/MigrateToCloud.tsx new file mode 100644 index 0000000000..a213fe2ae6 --- /dev/null +++ b/public/app/features/admin/migrate-to-cloud/MigrateToCloud.tsx @@ -0,0 +1,7 @@ +import React from 'react'; + +import { Page } from 'app/core/components/Page/Page'; + +export default function MigrateToCloud() { + return TODO; +} diff --git a/public/app/routes/routes.tsx b/public/app/routes/routes.tsx index 5f2cf1423c..1f64f90774 100644 --- a/public/app/routes/routes.tsx +++ b/public/app/routes/routes.tsx @@ -362,6 +362,13 @@ export function getAppRoutes(): RouteDescriptor[] { () => import(/* webpackChunkName: "ServerStats" */ 'app/features/admin/ServerStats') ), }, + config.featureToggles.onPremToCloudMigrations && { + path: '/admin/migrate-to-cloud', + roles: () => ['ServerAdmin'], + component: SafeDynamicImport( + () => import(/* webpackChunkName: "MigrateToCloud" */ 'app/features/admin/migrate-to-cloud/MigrateToCloud') + ), + }, // LOGIN / SIGNUP { path: '/login', diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 4a5f64e0b0..2edc000e59 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -876,6 +876,10 @@ "manage-folder": { "subtitle": "Manage folder dashboards and permissions" }, + "migrate-to-cloud": { + "subtitle": "Copy data sources, dashboards, and alerts from this installation to a cloud stack", + "title": "Migrate to Grafana Cloud" + }, "monitoring": { "subtitle": "Monitoring and infrastructure apps", "title": "Observability" diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index 2e45e4f038..9d894e7f4d 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -876,6 +876,10 @@ "manage-folder": { "subtitle": "Mäʼnäģę ƒőľđęř đäşĥþőäřđş äʼnđ pęřmįşşįőʼnş" }, + "migrate-to-cloud": { + "subtitle": "Cőpy đäŧä şőūřčęş, đäşĥþőäřđş, äʼnđ äľęřŧş ƒřőm ŧĥįş įʼnşŧäľľäŧįőʼn ŧő ä čľőūđ şŧäčĸ", + "title": "Mįģřäŧę ŧő Ğřäƒäʼnä Cľőūđ" + }, "monitoring": { "subtitle": "Mőʼnįŧőřįʼnģ äʼnđ įʼnƒřäşŧřūčŧūřę äppş", "title": "Øþşęřväþįľįŧy" From db4b4c4b0a3b01db11366f0754b5e920bb53934c Mon Sep 17 00:00:00 2001 From: Darren Janeczek <38694490+darrenjaneczek@users.noreply.github.com> Date: Wed, 21 Feb 2024 10:52:49 -0500 Subject: [PATCH 0076/1406] datatrails: refactor: auto query generator, use more suitable queries (#82494) * refactor: datatrails auto query generator * fix: use "per-second rate" instead --- .../trails/ActionTabs/BreakdownScene.tsx | 2 +- .../AutoQueryEngine.test.ts | 247 ++++++++++++++---- .../query-generators/common/generator.ts | 58 ++++ .../query-generators/common/queries.test.ts | 14 +- .../query-generators/common/queries.ts | 52 +--- .../query-generators/common/rules.ts | 14 +- .../{bucket/index.ts => histogram.ts} | 18 +- .../query-generators/index.ts | 6 +- .../query-generators/summary.ts | 39 +++ 9 files changed, 329 insertions(+), 121 deletions(-) create mode 100644 public/app/features/trails/AutomaticMetricQueries/query-generators/common/generator.ts rename public/app/features/trails/AutomaticMetricQueries/query-generators/{bucket/index.ts => histogram.ts} (83%) create mode 100644 public/app/features/trails/AutomaticMetricQueries/query-generators/summary.ts diff --git a/public/app/features/trails/ActionTabs/BreakdownScene.tsx b/public/app/features/trails/ActionTabs/BreakdownScene.tsx index 37782657a1..28a9dc7579 100644 --- a/public/app/features/trails/ActionTabs/BreakdownScene.tsx +++ b/public/app/features/trails/ActionTabs/BreakdownScene.tsx @@ -195,7 +195,7 @@ export function buildAllLayout(options: Array>, queryDef continue; } - const expr = queryDef.queries[0].expr.replace(VAR_GROUP_BY_EXP, String(option.value)); + const expr = queryDef.queries[0].expr.replaceAll(VAR_GROUP_BY_EXP, String(option.value)); const unit = queryDef.unit; children.push( diff --git a/public/app/features/trails/AutomaticMetricQueries/AutoQueryEngine.test.ts b/public/app/features/trails/AutomaticMetricQueries/AutoQueryEngine.test.ts index 6a444b4d11..b8176b51c3 100644 --- a/public/app/features/trails/AutomaticMetricQueries/AutoQueryEngine.test.ts +++ b/public/app/features/trails/AutomaticMetricQueries/AutoQueryEngine.test.ts @@ -5,26 +5,159 @@ function expandExpr(shortenedExpr: string) { } describe('getAutoQueriesForMetric', () => { + describe('for the summary/histogram types', () => { + const etc = '{${filters}}[$__rate_interval]'; + const byGroup = 'by(${groupby})'; + + describe('metrics with _sum suffix', () => { + const result = getAutoQueriesForMetric('SUM_OR_HIST_sum'); + + test('main query is the mean', () => { + const [{ expr }] = result.main.queries; + const mean = `sum(rate(SUM_OR_HIST_sum${etc}))/sum(rate(SUM_OR_HIST_count${etc}))`; + expect(expr).toBe(mean); + }); + + test('preview query is the mean', () => { + const [{ expr }] = result.preview.queries; + const mean = `sum(rate(SUM_OR_HIST_sum${etc}))/sum(rate(SUM_OR_HIST_count${etc}))`; + expect(expr).toBe(mean); + }); + + test('breakdown query is the mean by group', () => { + const [{ expr }] = result.breakdown.queries; + const meanBreakdown = `sum(rate(SUM_OR_HIST_sum${etc}))${byGroup}/sum(rate(SUM_OR_HIST_count${etc}))${byGroup}`; + expect(expr).toBe(meanBreakdown); + }); + + test('there are no variants', () => { + expect(result.variants.length).toBe(0); + }); + }); + + describe('metrics with _count suffix', () => { + const result = getAutoQueriesForMetric('SUM_OR_HIST_count'); + + test('main query is an overall rate', () => { + const [{ expr }] = result.main.queries; + const overallRate = `sum(rate(\${metric}${etc}))`; + expect(expr).toBe(overallRate); + }); + + test('preview query is an overall rate', () => { + const [{ expr }] = result.preview.queries; + const overallRate = `sum(rate(\${metric}${etc}))`; + expect(expr).toBe(overallRate); + }); + + test('breakdown query is an overall rate by group', () => { + const [{ expr }] = result.breakdown.queries; + const overallRateBreakdown = `sum(rate(\${metric}${etc}))${byGroup}`; + expect(expr).toBe(overallRateBreakdown); + }); + + test('there are no variants', () => { + expect(result.variants.length).toBe(0); + }); + }); + + describe('metrics with _bucket suffix', () => { + const result = getAutoQueriesForMetric('HIST_bucket'); + + const percentileQueries = new Map(); + percentileQueries.set(99, expandExpr('histogram_quantile(0.99, sum by(le) (rate(...[$__rate_interval])))')); + percentileQueries.set(90, expandExpr('histogram_quantile(0.9, sum by(le) (rate(...[$__rate_interval])))')); + percentileQueries.set(50, expandExpr('histogram_quantile(0.5, sum by(le) (rate(...[$__rate_interval])))')); + + test('there are 2 variants', () => { + expect(result.variants.length).toBe(2); + }); + + const percentilesVariant = result.variants.find((variant) => variant.variant === 'percentiles'); + test('there is a percentiles variant', () => { + expect(percentilesVariant).not.toBeNull(); + }); + + const heatmap = result.variants.find((variant) => variant.variant === 'heatmap'); + test('there is a heatmap variant', () => { + expect(heatmap).not.toBeNull(); + }); + + [99, 90, 50].forEach((percentile) => { + const percentileQuery = percentileQueries.get(percentile); + test(`main panel has ${percentile}th percentile query`, () => { + const found = result.main.queries.find((query) => query.expr === percentileQuery); + expect(found).not.toBeNull(); + }); + }); + + [99, 90, 50].forEach((percentile) => { + const percentileQuery = percentileQueries.get(percentile); + test(`percentiles variant panel has ${percentile}th percentile query`, () => { + const found = percentilesVariant?.queries.find((query) => query.expr === percentileQuery); + expect(found).not.toBeNull(); + }); + }); + + test('preview panel has 50th percentile query', () => { + const [{ expr }] = result.preview.queries; + expect(expr).toBe(percentileQueries.get(50)); + }); + + const percentileGroupedQueries = new Map(); + percentileGroupedQueries.set( + 99, + expandExpr('histogram_quantile(0.99, sum by(le, ${groupby}) (rate(...[$__rate_interval])))') + ); + percentileGroupedQueries.set( + 90, + expandExpr('histogram_quantile(0.9, sum by(le, ${groupby}) (rate(...[$__rate_interval])))') + ); + percentileGroupedQueries.set( + 50, + expandExpr('histogram_quantile(0.5, sum by(le, ${groupby}) (rate(...[$__rate_interval])))') + ); + + [99, 90, 50].forEach((percentile) => { + const percentileGroupedQuery = percentileGroupedQueries.get(percentile); + test(`breakdown panel has ${percentile}th query with \${groupby} appended`, () => { + const found = result.breakdown.queries.find((query) => query.expr === percentileGroupedQuery); + expect(found).not.toBeNull(); + }); + }); + }); + }); describe('Consider result.main query (only first)', () => { it.each([ // no rate - ['my_metric_general', 'avg(...)', 'short', 1], - ['my_metric_bytes', 'avg(...)', 'bytes', 1], - ['my_metric_seconds', 'avg(...)', 's', 1], + ['PREFIX_general', 'avg(...)', 'short', 1], + ['PREFIX_bytes', 'avg(...)', 'bytes', 1], + ['PREFIX_seconds', 'avg(...)', 's', 1], // rate with counts per second - ['my_metric_count', 'sum(rate(...[$__rate_interval]))', 'cps', 1], // cps = counts per second - ['my_metric_total', 'sum(rate(...[$__rate_interval]))', 'cps', 1], - ['my_metric_seconds_count', 'sum(rate(...[$__rate_interval]))', 'cps', 1], + ['PREFIX_count', 'sum(rate(...[$__rate_interval]))', 'cps', 1], // cps = counts per second + ['PREFIX_total', 'sum(rate(...[$__rate_interval]))', 'cps', 1], + ['PREFIX_seconds_count', 'sum(rate(...[$__rate_interval]))', 'cps', 1], // rate with seconds per second - ['my_metric_seconds_total', 'sum(rate(...[$__rate_interval]))', 'short', 1], // s/s - ['my_metric_seconds_sum', 'avg(rate(...[$__rate_interval]))', 'short', 1], + ['PREFIX_seconds_total', 'sum(rate(...[$__rate_interval]))', 'short', 1], // s/s // rate with bytes per second - ['my_metric_bytes_total', 'sum(rate(...[$__rate_interval]))', 'Bps', 1], // bytes/s - ['my_metric_bytes_sum', 'avg(rate(...[$__rate_interval]))', 'Bps', 1], + ['PREFIX_bytes_total', 'sum(rate(...[$__rate_interval]))', 'Bps', 1], // bytes/s + // mean with non-rated units + [ + 'PREFIX_seconds_sum', + 'sum(rate(PREFIX_seconds_sum{${filters}}[$__rate_interval]))/sum(rate(PREFIX_seconds_count{${filters}}[$__rate_interval]))', + 's', + 1, + ], + [ + 'PREFIX_bytes_sum', + 'sum(rate(PREFIX_bytes_sum{${filters}}[$__rate_interval]))/sum(rate(PREFIX_bytes_count{${filters}}[$__rate_interval]))', + 'bytes', + 1, + ], // Bucket - ['my_metric_bucket', 'histogram_quantile(0.99, sum by(le) (rate(...[$__rate_interval])))', 'short', 3], - ['my_metric_seconds_bucket', 'histogram_quantile(0.99, sum by(le) (rate(...[$__rate_interval])))', 's', 3], - ['my_metric_bytes_bucket', 'histogram_quantile(0.99, sum by(le) (rate(...[$__rate_interval])))', 'bytes', 3], + ['PREFIX_bucket', 'histogram_quantile(0.99, sum by(le) (rate(...[$__rate_interval])))', 'short', 3], + ['PREFIX_seconds_bucket', 'histogram_quantile(0.99, sum by(le) (rate(...[$__rate_interval])))', 's', 3], + ['PREFIX_bytes_bucket', 'histogram_quantile(0.99, sum by(le) (rate(...[$__rate_interval])))', 'bytes', 3], ])('Given metric %p expect %p with unit %p', (metric, expr, unit, queryCount) => { const result = getAutoQueriesForMetric(metric); @@ -40,23 +173,32 @@ describe('getAutoQueriesForMetric', () => { describe('Consider result.preview query (only first)', () => { it.each([ // no rate - ['my_metric_general', 'avg(...)', 'short'], - ['my_metric_bytes', 'avg(...)', 'bytes'], - ['my_metric_seconds', 'avg(...)', 's'], + ['PREFIX_general', 'avg(...)', 'short'], + ['PREFIX_bytes', 'avg(...)', 'bytes'], + ['PREFIX_seconds', 'avg(...)', 's'], // rate with counts per second - ['my_metric_count', 'sum(rate(...[$__rate_interval]))', 'cps'], // cps = counts per second - ['my_metric_total', 'sum(rate(...[$__rate_interval]))', 'cps'], - ['my_metric_seconds_count', 'sum(rate(...[$__rate_interval]))', 'cps'], + ['PREFIX_count', 'sum(rate(...[$__rate_interval]))', 'cps'], // cps = counts per second + ['PREFIX_total', 'sum(rate(...[$__rate_interval]))', 'cps'], + ['PREFIX_seconds_count', 'sum(rate(...[$__rate_interval]))', 'cps'], // rate with seconds per second - ['my_metric_seconds_total', 'sum(rate(...[$__rate_interval]))', 'short'], // s/s - ['my_metric_seconds_sum', 'avg(rate(...[$__rate_interval]))', 'short'], + ['PREFIX_seconds_total', 'sum(rate(...[$__rate_interval]))', 'short'], // s/s // rate with bytes per second - ['my_metric_bytes_total', 'sum(rate(...[$__rate_interval]))', 'Bps'], // bytes/s - ['my_metric_bytes_sum', 'avg(rate(...[$__rate_interval]))', 'Bps'], + ['PREFIX_bytes_total', 'sum(rate(...[$__rate_interval]))', 'Bps'], // bytes/s + // mean with non-rated units + [ + 'PREFIX_seconds_sum', + 'sum(rate(PREFIX_seconds_sum{${filters}}[$__rate_interval]))/sum(rate(PREFIX_seconds_count{${filters}}[$__rate_interval]))', + 's', + ], + [ + 'PREFIX_bytes_sum', + 'sum(rate(PREFIX_bytes_sum{${filters}}[$__rate_interval]))/sum(rate(PREFIX_bytes_count{${filters}}[$__rate_interval]))', + 'bytes', + ], // Bucket - ['my_metric_bucket', 'histogram_quantile(0.5, sum by(le) (rate(...[$__rate_interval])))', 'short'], - ['my_metric_seconds_bucket', 'histogram_quantile(0.5, sum by(le) (rate(...[$__rate_interval])))', 's'], - ['my_metric_bytes_bucket', 'histogram_quantile(0.5, sum by(le) (rate(...[$__rate_interval])))', 'bytes'], + ['PREFIX_bucket', 'histogram_quantile(0.5, sum by(le) (rate(...[$__rate_interval])))', 'short'], + ['PREFIX_seconds_bucket', 'histogram_quantile(0.5, sum by(le) (rate(...[$__rate_interval])))', 's'], + ['PREFIX_bytes_bucket', 'histogram_quantile(0.5, sum by(le) (rate(...[$__rate_interval])))', 'bytes'], ])('Given metric %p expect %p with unit %p', (metric, expr, unit) => { const result = getAutoQueriesForMetric(metric); @@ -74,31 +216,32 @@ describe('getAutoQueriesForMetric', () => { describe('Consider result.breakdown query (only first)', () => { it.each([ // no rate - ['my_metric_general', 'avg(...) by(${groupby})', 'short'], - ['my_metric_bytes', 'avg(...) by(${groupby})', 'bytes'], - ['my_metric_seconds', 'avg(...) by(${groupby})', 's'], + ['PREFIX_general', 'avg(...)by(${groupby})', 'short'], + ['PREFIX_bytes', 'avg(...)by(${groupby})', 'bytes'], + ['PREFIX_seconds', 'avg(...)by(${groupby})', 's'], // rate with counts per second - ['my_metric_count', 'sum(rate(...[$__rate_interval])) by(${groupby})', 'cps'], // cps = counts per second - ['my_metric_total', 'sum(rate(...[$__rate_interval])) by(${groupby})', 'cps'], - ['my_metric_seconds_count', 'sum(rate(...[$__rate_interval])) by(${groupby})', 'cps'], + ['PREFIX_count', 'sum(rate(...[$__rate_interval]))by(${groupby})', 'cps'], // cps = counts per second + ['PREFIX_total', 'sum(rate(...[$__rate_interval]))by(${groupby})', 'cps'], + ['PREFIX_seconds_count', 'sum(rate(...[$__rate_interval]))by(${groupby})', 'cps'], // rate with seconds per second - ['my_metric_seconds_total', 'sum(rate(...[$__rate_interval])) by(${groupby})', 'short'], // s/s - ['my_metric_seconds_sum', 'avg(rate(...[$__rate_interval])) by(${groupby})', 'short'], + ['PREFIX_seconds_total', 'sum(rate(...[$__rate_interval]))by(${groupby})', 'short'], // s/s // rate with bytes per second - ['my_metric_bytes_total', 'sum(rate(...[$__rate_interval])) by(${groupby})', 'Bps'], // bytes/s - ['my_metric_bytes_sum', 'avg(rate(...[$__rate_interval])) by(${groupby})', 'Bps'], - // Bucket - ['my_metric_bucket', 'histogram_quantile(0.5, sum by(le, ${groupby}) (rate(...[$__rate_interval])))', 'short'], + ['PREFIX_bytes_total', 'sum(rate(...[$__rate_interval]))by(${groupby})', 'Bps'], // bytes/s + // mean with non-rated units [ - 'my_metric_seconds_bucket', - 'histogram_quantile(0.5, sum by(le, ${groupby}) (rate(...[$__rate_interval])))', + 'PREFIX_seconds_sum', + 'sum(rate(PREFIX_seconds_sum{${filters}}[$__rate_interval]))by(${groupby})/sum(rate(PREFIX_seconds_count{${filters}}[$__rate_interval]))by(${groupby})', 's', ], [ - 'my_metric_bytes_bucket', - 'histogram_quantile(0.5, sum by(le, ${groupby}) (rate(...[$__rate_interval])))', + 'PREFIX_bytes_sum', + 'sum(rate(PREFIX_bytes_sum{${filters}}[$__rate_interval]))by(${groupby})/sum(rate(PREFIX_bytes_count{${filters}}[$__rate_interval]))by(${groupby})', 'bytes', ], + // Bucket + ['PREFIX_bucket', 'histogram_quantile(0.5, sum by(le, ${groupby}) (rate(...[$__rate_interval])))', 'short'], + ['PREFIX_seconds_bucket', 'histogram_quantile(0.5, sum by(le, ${groupby}) (rate(...[$__rate_interval])))', 's'], + ['PREFIX_bytes_bucket', 'histogram_quantile(0.5, sum by(le, ${groupby}) (rate(...[$__rate_interval])))', 'bytes'], ])('Given metric %p expect %p with unit %p', (metric, expr, unit) => { const result = getAutoQueriesForMetric(metric); @@ -116,16 +259,16 @@ describe('getAutoQueriesForMetric', () => { describe('Consider result.variant', () => { it.each([ // No variants - ['my_metric_count', []], - ['my_metric_seconds_count', []], - ['my_metric_bytes', []], - ['my_metric_seconds', []], - ['my_metric_general', []], - ['my_metric_seconds_total', []], - ['my_metric_seconds_sum', []], + ['PREFIX_count', []], + ['PREFIX_seconds_count', []], + ['PREFIX_bytes', []], + ['PREFIX_seconds', []], + ['PREFIX_general', []], + ['PREFIX_seconds_total', []], + ['PREFIX_seconds_sum', []], // Bucket variants [ - 'my_metric_bucket', + 'PREFIX_bucket', [ { variant: 'percentiles', @@ -144,7 +287,7 @@ describe('getAutoQueriesForMetric', () => { ], ], [ - 'my_metric_seconds_bucket', + 'PREFIX_seconds_bucket', [ { variant: 'percentiles', @@ -163,7 +306,7 @@ describe('getAutoQueriesForMetric', () => { ], ], [ - 'my_metric_bytes_bucket', + 'PREFIX_bytes_bucket', [ { variant: 'percentiles', diff --git a/public/app/features/trails/AutomaticMetricQueries/query-generators/common/generator.ts b/public/app/features/trails/AutomaticMetricQueries/query-generators/common/generator.ts new file mode 100644 index 0000000000..fbc7ff5d3c --- /dev/null +++ b/public/app/features/trails/AutomaticMetricQueries/query-generators/common/generator.ts @@ -0,0 +1,58 @@ +import { VAR_GROUP_BY_EXP, VAR_METRIC_EXPR } from '../../../shared'; +import { simpleGraphBuilder } from '../../graph-builders/simple'; + +export type CommonQueryInfoParams = { + description: string; + mainQueryExpr: string; + breakdownQueryExpr: string; + unit: string; +}; + +export function generateCommonAutoQueryInfo({ + description, + mainQueryExpr, + breakdownQueryExpr, + unit, +}: CommonQueryInfoParams) { + const common = { + title: `${VAR_METRIC_EXPR}`, + unit, + }; + + const mainQuery = { + refId: 'A', + expr: mainQueryExpr, + legendFormat: description, + }; + + const main = { + ...common, + title: description, + queries: [mainQuery], + variant: 'main', + vizBuilder: () => simpleGraphBuilder({ ...main }), + }; + + const preview = { + ...main, + title: `${VAR_METRIC_EXPR}`, + queries: [{ ...mainQuery, legendFormat: description }], + vizBuilder: () => simpleGraphBuilder(preview), + variant: 'preview', + }; + + const breakdown = { + ...common, + queries: [ + { + refId: 'A', + expr: breakdownQueryExpr, + legendFormat: `{{${VAR_GROUP_BY_EXP}}}`, + }, + ], + vizBuilder: () => simpleGraphBuilder(breakdown), + variant: 'breakdown', + }; + + return { preview, main, breakdown, variants: [] }; +} diff --git a/public/app/features/trails/AutomaticMetricQueries/query-generators/common/queries.test.ts b/public/app/features/trails/AutomaticMetricQueries/query-generators/common/queries.test.ts index 03d8bf899e..bb8650338e 100644 --- a/public/app/features/trails/AutomaticMetricQueries/query-generators/common/queries.test.ts +++ b/public/app/features/trails/AutomaticMetricQueries/query-generators/common/queries.test.ts @@ -4,7 +4,7 @@ import { AutoQueryDef, AutoQueryInfo } from '../../types'; import { generateQueries, getGeneralBaseQuery } from './queries'; describe('generateQueries', () => { - const agg = 'mockagg'; + const agg = 'sum'; const unit = 'mockunit'; type QueryInfoKey = keyof AutoQueryInfo; @@ -34,7 +34,8 @@ describe('generateQueries', () => { const expectedBaseQuery = getGeneralBaseQuery(rate); const detectedBaseQuery = query.expr.substring(firstParen + 1, firstParen + 1 + expectedBaseQuery.length); - const description = rate ? 'mockagg of rates per second' : 'mockagg'; + const inParentheses = rate ? 'overall per-second rate' : 'overall'; + const description = `\${metric} (${inParentheses})`; describe(`since rate is ${rate}`, () => { test(`base query must be "${expectedBaseQuery}"`, () => expect(detectedBaseQuery).toBe(expectedBaseQuery)); @@ -47,16 +48,11 @@ describe('generateQueries', () => { expect(queryDef.title).not.toContain(description)); } - if (key === 'preview') { - test(`preview query uses "${description}" as legend`, () => expect(query.legendFormat).toBe(description)); - } else if (key === 'breakdown') { + if (key === 'breakdown') { test(`breakdown query uses "{{\${groupby}}}" as legend`, () => expect(query.legendFormat).toBe('{{${groupby}}}')); } else { - test(`${key} query doesn't only use "${description}" in legend`, () => - expect(query.legendFormat).not.toBe(description)); - test(`${key} query does contain "${description}" in legend`, () => - expect(query.legendFormat).toContain(description)); + test(`preview query uses "${description}" as legend`, () => expect(query.legendFormat).toBe(description)); } }); } diff --git a/public/app/features/trails/AutomaticMetricQueries/query-generators/common/queries.ts b/public/app/features/trails/AutomaticMetricQueries/query-generators/common/queries.ts index 5967927d39..eb43d8537c 100644 --- a/public/app/features/trails/AutomaticMetricQueries/query-generators/common/queries.ts +++ b/public/app/features/trails/AutomaticMetricQueries/query-generators/common/queries.ts @@ -1,6 +1,6 @@ import { VAR_FILTERS_EXPR, VAR_GROUP_BY_EXP, VAR_METRIC_EXPR } from '../../../shared'; -import { simpleGraphBuilder } from '../../graph-builders/simple'; import { AutoQueryInfo } from '../../types'; +import { generateCommonAutoQueryInfo } from '../common/generator'; import { AutoQueryParameters } from './types'; @@ -13,7 +13,7 @@ export function getGeneralBaseQuery(rate: boolean) { const aggLabels: Record = { avg: 'average', - sum: 'sum', + sum: 'overall', }; function getAggLabel(agg: string) { @@ -23,45 +23,17 @@ function getAggLabel(agg: string) { export function generateQueries({ agg, rate, unit }: AutoQueryParameters): AutoQueryInfo { const baseQuery = getGeneralBaseQuery(rate); - const description = rate ? `${getAggLabel(agg)} of rates per second` : `${getAggLabel(agg)}`; + const aggregationDescription = rate ? `${getAggLabel(agg)} per-second rate` : `${getAggLabel(agg)}`; - const common = { - title: `${VAR_METRIC_EXPR}`, - unit, - variant: description, - }; - - const mainQuery = { - refId: 'A', - expr: `${agg}(${baseQuery})`, - legendFormat: `${VAR_METRIC_EXPR} (${description})`, - }; - - const main = { - ...common, - title: `${VAR_METRIC_EXPR} (${description})`, - queries: [mainQuery], - vizBuilder: () => simpleGraphBuilder({ ...main }), - }; + const description = `${VAR_METRIC_EXPR} (${aggregationDescription})`; - const preview = { - ...main, - title: `${VAR_METRIC_EXPR}`, - queries: [{ ...mainQuery, legendFormat: description }], - vizBuilder: () => simpleGraphBuilder(preview), - }; + const mainQueryExpr = `${agg}(${baseQuery})`; + const breakdownQueryExpr = `${agg}(${baseQuery})by(${VAR_GROUP_BY_EXP})`; - const breakdown = { - ...common, - queries: [ - { - refId: 'A', - expr: `${agg}(${baseQuery}) by(${VAR_GROUP_BY_EXP})`, - legendFormat: `{{${VAR_GROUP_BY_EXP}}}`, - }, - ], - vizBuilder: () => simpleGraphBuilder(breakdown), - }; - - return { preview, main, breakdown, variants: [] }; + return generateCommonAutoQueryInfo({ + description, + mainQueryExpr, + breakdownQueryExpr, + unit, + }); } diff --git a/public/app/features/trails/AutomaticMetricQueries/query-generators/common/rules.ts b/public/app/features/trails/AutomaticMetricQueries/query-generators/common/rules.ts index 01a42bdaaa..96e0302089 100644 --- a/public/app/features/trails/AutomaticMetricQueries/query-generators/common/rules.ts +++ b/public/app/features/trails/AutomaticMetricQueries/query-generators/common/rules.ts @@ -3,34 +3,34 @@ import { getUnit, getPerSecondRateUnit } from '../../units'; import { AutoQueryParameters } from './types'; /** These suffixes will set rate to true */ -const RATE_SUFFIXES = new Set(['count', 'total', 'sum']); +const RATE_SUFFIXES = new Set(['count', 'total']); + +const UNSUPPORTED_SUFFIXES = new Set(['sum', 'bucket']); /** Non-default aggregattion keyed by suffix */ const SPECIFIC_AGGREGATIONS_FOR_SUFFIX: Record = { count: 'sum', total: 'sum', - sum: 'avg', }; function checkPreviousForUnit(suffix: string) { - return suffix === 'total' || suffix === 'sum'; + return suffix === 'total'; } export function getGeneratorParameters(metricParts: string[]): AutoQueryParameters { const suffix = metricParts.at(-1); - if (suffix == null) { - throw new Error('Invalid metric parameter'); + if (suffix == null || UNSUPPORTED_SUFFIXES.has(suffix)) { + throw new Error(`This function does not support a metric suffix of "${suffix}"`); } const rate = RATE_SUFFIXES.has(suffix); + const agg = SPECIFIC_AGGREGATIONS_FOR_SUFFIX[suffix] || 'avg'; const unitSuffix = checkPreviousForUnit(suffix) ? metricParts.at(-2) : suffix; const unit = rate ? getPerSecondRateUnit(unitSuffix) : getUnit(unitSuffix); - const agg = SPECIFIC_AGGREGATIONS_FOR_SUFFIX[suffix] || 'avg'; - return { agg, unit, diff --git a/public/app/features/trails/AutomaticMetricQueries/query-generators/bucket/index.ts b/public/app/features/trails/AutomaticMetricQueries/query-generators/histogram.ts similarity index 83% rename from public/app/features/trails/AutomaticMetricQueries/query-generators/bucket/index.ts rename to public/app/features/trails/AutomaticMetricQueries/query-generators/histogram.ts index 66dd0b6ba9..ad51a4769b 100644 --- a/public/app/features/trails/AutomaticMetricQueries/query-generators/bucket/index.ts +++ b/public/app/features/trails/AutomaticMetricQueries/query-generators/histogram.ts @@ -1,13 +1,13 @@ import { PromQuery } from 'app/plugins/datasource/prometheus/types'; -import { VAR_FILTERS_EXPR, VAR_GROUP_BY_EXP, VAR_METRIC_EXPR } from '../../../shared'; -import { heatmapGraphBuilder } from '../../graph-builders/heatmap'; -import { percentilesGraphBuilder } from '../../graph-builders/percentiles'; -import { simpleGraphBuilder } from '../../graph-builders/simple'; -import { AutoQueryDef } from '../../types'; -import { getUnit } from '../../units'; - -function generator(metricParts: string[]) { +import { VAR_FILTERS_EXPR, VAR_GROUP_BY_EXP, VAR_METRIC_EXPR } from '../../shared'; +import { heatmapGraphBuilder } from '../graph-builders/heatmap'; +import { percentilesGraphBuilder } from '../graph-builders/percentiles'; +import { simpleGraphBuilder } from '../graph-builders/simple'; +import { AutoQueryDef } from '../types'; +import { getUnit } from '../units'; + +export function createHistogramQueryDefs(metricParts: string[]) { const title = `${VAR_METRIC_EXPR}`; const unitSuffix = metricParts.at(-2); @@ -59,8 +59,6 @@ function fixRefIds(queryDef: PromQuery, index: number): PromQuery { }; } -export default { generator }; - const BASE_QUERY = `rate(${VAR_METRIC_EXPR}${VAR_FILTERS_EXPR}[$__rate_interval])`; function baseQuery(groupings: string[] = []) { diff --git a/public/app/features/trails/AutomaticMetricQueries/query-generators/index.ts b/public/app/features/trails/AutomaticMetricQueries/query-generators/index.ts index c450908099..569f4e8e0b 100644 --- a/public/app/features/trails/AutomaticMetricQueries/query-generators/index.ts +++ b/public/app/features/trails/AutomaticMetricQueries/query-generators/index.ts @@ -1,9 +1,11 @@ -import bucket from './bucket'; import general from './common'; +import { createHistogramQueryDefs } from './histogram'; +import { createSummaryQueryDefs } from './summary'; import { MetricQueriesGenerator } from './types'; const SUFFIX_TO_ALTERNATIVE_GENERATOR: Record = { - bucket: bucket.generator, + sum: createSummaryQueryDefs, + bucket: createHistogramQueryDefs, }; export function getQueryGeneratorFor(suffix?: string) { diff --git a/public/app/features/trails/AutomaticMetricQueries/query-generators/summary.ts b/public/app/features/trails/AutomaticMetricQueries/query-generators/summary.ts new file mode 100644 index 0000000000..b6a625b089 --- /dev/null +++ b/public/app/features/trails/AutomaticMetricQueries/query-generators/summary.ts @@ -0,0 +1,39 @@ +import { VAR_GROUP_BY_EXP, VAR_METRIC_EXPR } from '../../shared'; +import { AutoQueryInfo } from '../types'; +import { getUnit } from '../units'; + +import { generateCommonAutoQueryInfo } from './common/generator'; +import { getGeneralBaseQuery } from './common/queries'; + +export function createSummaryQueryDefs(metricParts: string[]): AutoQueryInfo { + const suffix = metricParts.at(-1); + if (suffix !== 'sum') { + throw new Error('createSummaryQueryDefs is only to be used for metrics that end in "_sum"'); + } + + const unitSuffix = metricParts.at(-2); + const unit = getUnit(unitSuffix); + + const rate = true; + const baseQuery = getGeneralBaseQuery(rate); + + const subMetric = metricParts.slice(0, -1).join('_'); + const mainQueryExpr = createMeanExpr(`sum(${baseQuery})`); + const breakdownQueryExpr = createMeanExpr(`sum(${baseQuery})by(${VAR_GROUP_BY_EXP})`); + + const operationDescription = `average`; + const description = `${subMetric} (${operationDescription})`; + + function createMeanExpr(expr: string) { + const numerator = expr.replace(VAR_METRIC_EXPR, `${subMetric}_sum`); + const denominator = expr.replace(VAR_METRIC_EXPR, `${subMetric}_count`); + return `${numerator}/${denominator}`; + } + + return generateCommonAutoQueryInfo({ + description, + mainQueryExpr, + breakdownQueryExpr, + unit, + }); +} From 5460d75e74b0f6f477deed177683a2160477a8ba Mon Sep 17 00:00:00 2001 From: Ivan Ortega Alba Date: Wed, 21 Feb 2024 16:57:53 +0100 Subject: [PATCH 0077/1406] QueryVariableEditor: Select a variable ds does not work (#83144) --- .../components/QueryVariableForm.test.tsx | 41 +++++---- .../components/QueryVariableForm.tsx | 24 ++++-- .../variables/editors/QueryVariableEditor.tsx | 16 +--- .../query/QueryVariableEditor.test.tsx | 85 ++++++++----------- .../variables/query/QueryVariableEditor.tsx | 6 +- public/test/mocks/datasource_srv.ts | 6 +- 6 files changed, 83 insertions(+), 95 deletions(-) diff --git a/public/app/features/dashboard-scene/settings/variables/components/QueryVariableForm.test.tsx b/public/app/features/dashboard-scene/settings/variables/components/QueryVariableForm.test.tsx index 9590377e41..0e154ed447 100644 --- a/public/app/features/dashboard-scene/settings/variables/components/QueryVariableForm.test.tsx +++ b/public/app/features/dashboard-scene/settings/variables/components/QueryVariableForm.test.tsx @@ -1,8 +1,7 @@ -import { render, screen, waitFor } from '@testing-library/react'; +import { act, render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React, { FormEvent } from 'react'; import { of } from 'rxjs'; -import { MockDataSourceApi } from 'test/mocks/datasource_srv'; import { LoadingState, @@ -17,6 +16,7 @@ import { setRunRequest } from '@grafana/runtime'; import { VariableRefresh, VariableSort } from '@grafana/schema'; import { mockDataSource } from 'app/features/alerting/unified/mocks'; import { LegacyVariableQueryEditor } from 'app/features/variables/editor/LegacyVariableQueryEditor'; +import { getVariableQueryEditor } from 'app/features/variables/editor/getVariableQueryEditor'; import { QueryVariableEditorForm } from './QueryVariableForm'; @@ -42,7 +42,7 @@ jest.mock('@grafana/runtime/src/services/dataSourceSrv', () => ({ }, }), getList: () => [defaultDatasource, promDatasource], - getInstanceSettings: () => ({ ...defaultDatasource }), + getInstanceSettings: (uid: string) => (uid === promDatasource.uid ? promDatasource : defaultDatasource), }), })); @@ -60,6 +60,11 @@ const runRequestMock = jest.fn().mockReturnValue( setRunRequest(runRequestMock); +jest.mock('app/features/variables/editor/getVariableQueryEditor', () => ({ + ...jest.requireActual('app/features/variables/editor/getVariableQueryEditor'), + getVariableQueryEditor: jest.fn(), +})); + describe('QueryVariableEditorForm', () => { const mockOnDataSourceChange = jest.fn(); const mockOnQueryChange = jest.fn(); @@ -71,14 +76,13 @@ describe('QueryVariableEditorForm', () => { const mockOnIncludeAllChange = jest.fn(); const mockOnAllValueChange = jest.fn(); - const defaultProps = { - datasource: new MockDataSourceApi(promDatasource.name, undefined, promDatasource.meta), + const defaultProps: React.ComponentProps = { + datasource: { uid: defaultDatasource.uid, type: defaultDatasource.type }, onDataSourceChange: mockOnDataSourceChange, query: 'my-query', onQueryChange: mockOnQueryChange, onLegacyQueryChange: mockOnLegacyQueryChange, timeRange: getDefaultTimeRange(), - VariableQueryEditor: LegacyVariableQueryEditor, regex: '.*', onRegExChange: mockOnRegExChange, sort: VariableSort.alphabeticalAsc, @@ -93,9 +97,10 @@ describe('QueryVariableEditorForm', () => { onAllValueChange: mockOnAllValueChange, }; - function setup(props?: React.ComponentProps) { + async function setup(props?: React.ComponentProps) { + jest.mocked(getVariableQueryEditor).mockResolvedValue(LegacyVariableQueryEditor); return { - renderer: render(), + renderer: await act(() => render()), user: userEvent.setup(), }; } @@ -104,10 +109,10 @@ describe('QueryVariableEditorForm', () => { jest.clearAllMocks(); }); - it('should render the component with initializing the components correctly', () => { + it('should render the component with initializing the components correctly', async () => { const { renderer: { getByTestId, getByRole }, - } = setup(); + } = await setup(); const dataSourcePicker = getByTestId(selectors.components.DataSourcePicker.inputV2); //const queryEditor = getByTestId('query-editor'); const regexInput = getByTestId( @@ -149,7 +154,7 @@ describe('QueryVariableEditorForm', () => { it('should call onDataSourceChange when changing the datasource', async () => { const { renderer: { getByTestId }, - } = setup(); + } = await setup(); const dataSourcePicker = getByTestId(selectors.components.DataSourcePicker.inputV2); await userEvent.click(dataSourcePicker); await userEvent.click(screen.getByText(/prometheus/i)); @@ -161,7 +166,7 @@ describe('QueryVariableEditorForm', () => { it('should call onQueryChange when changing the query', async () => { const { renderer: { getByTestId }, - } = setup(); + } = await setup(); const queryEditor = getByTestId( selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsQueryInput ); @@ -178,7 +183,7 @@ describe('QueryVariableEditorForm', () => { it('should call onRegExChange when changing the regex', async () => { const { renderer: { getByTestId }, - } = setup(); + } = await setup(); const regexInput = getByTestId( selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInputV2 ); @@ -193,7 +198,7 @@ describe('QueryVariableEditorForm', () => { it('should call onSortChange when changing the sort', async () => { const { renderer: { getByTestId }, - } = setup(); + } = await setup(); const sortSelect = getByTestId( selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsSortSelectV2 ); @@ -211,7 +216,7 @@ describe('QueryVariableEditorForm', () => { it('should call onRefreshChange when changing the refresh', async () => { const { renderer: { getByTestId }, - } = setup(); + } = await setup(); const refreshSelect = getByTestId( selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRefreshSelectV2 ); @@ -226,7 +231,7 @@ describe('QueryVariableEditorForm', () => { it('should call onMultiChange when changing the multi switch', async () => { const { renderer: { getByTestId }, - } = setup(); + } = await setup(); const multiSwitch = getByTestId( selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsMultiSwitch ); @@ -240,7 +245,7 @@ describe('QueryVariableEditorForm', () => { it('should call onIncludeAllChange when changing the include all switch', async () => { const { renderer: { getByTestId }, - } = setup(); + } = await setup(); const includeAllSwitch = getByTestId( selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsIncludeAllSwitch ); @@ -254,7 +259,7 @@ describe('QueryVariableEditorForm', () => { it('should call onAllValueChange when changing the all value', async () => { const { renderer: { getByTestId }, - } = setup(); + } = await setup(); const allValueInput = getByTestId( selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsCustomAllInput ); diff --git a/public/app/features/dashboard-scene/settings/variables/components/QueryVariableForm.tsx b/public/app/features/dashboard-scene/settings/variables/components/QueryVariableForm.tsx index 0e7d86740c..a1a9cfbc3f 100644 --- a/public/app/features/dashboard-scene/settings/variables/components/QueryVariableForm.tsx +++ b/public/app/features/dashboard-scene/settings/variables/components/QueryVariableForm.tsx @@ -1,16 +1,18 @@ import React, { FormEvent } from 'react'; +import { useAsync } from 'react-use'; -import { DataSourceApi, DataSourceInstanceSettings, SelectableValue, TimeRange } from '@grafana/data'; +import { DataSourceInstanceSettings, SelectableValue, TimeRange } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; +import { getDataSourceSrv } from '@grafana/runtime'; import { QueryVariable } from '@grafana/scenes'; -import { VariableRefresh, VariableSort } from '@grafana/schema'; +import { DataSourceRef, VariableRefresh, VariableSort } from '@grafana/schema'; import { Field } from '@grafana/ui'; import { QueryEditor } from 'app/features/dashboard-scene/settings/variables/components/QueryEditor'; import { SelectionOptionsForm } from 'app/features/dashboard-scene/settings/variables/components/SelectionOptionsForm'; import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker'; +import { getVariableQueryEditor } from 'app/features/variables/editor/getVariableQueryEditor'; import { QueryVariableRefreshSelect } from 'app/features/variables/query/QueryVariableRefreshSelect'; import { QueryVariableSortSelect } from 'app/features/variables/query/QueryVariableSortSelect'; -import { VariableQueryEditorType } from 'app/features/variables/types'; import { VariableLegend } from './VariableLegend'; import { VariableTextAreaField } from './VariableTextAreaField'; @@ -18,12 +20,11 @@ import { VariableTextAreaField } from './VariableTextAreaField'; type VariableQueryType = QueryVariable['state']['query']; interface QueryVariableEditorFormProps { - datasource: DataSourceApi | undefined; + datasource?: DataSourceRef; onDataSourceChange: (dsSettings: DataSourceInstanceSettings) => void; query: VariableQueryType; onQueryChange: (query: VariableQueryType) => void; onLegacyQueryChange: (query: VariableQueryType, definition: string) => void; - VariableQueryEditor: VariableQueryEditorType | undefined; timeRange: TimeRange; regex: string | null; onRegExChange: (event: FormEvent) => void; @@ -40,12 +41,11 @@ interface QueryVariableEditorFormProps { } export function QueryVariableEditorForm({ - datasource, + datasource: datasourceRef, onDataSourceChange, query, onQueryChange, onLegacyQueryChange, - VariableQueryEditor, timeRange, regex, onRegExChange, @@ -60,11 +60,19 @@ export function QueryVariableEditorForm({ allValue, onAllValueChange, }: QueryVariableEditorFormProps) { + const { value: dsConfig } = useAsync(async () => { + const datasource = await getDataSourceSrv().get(datasourceRef ?? ''); + const VariableQueryEditor = await getVariableQueryEditor(datasource); + + return { datasource, VariableQueryEditor }; + }, [datasourceRef]); + const { datasource, VariableQueryEditor } = dsConfig ?? {}; + return ( <> Query options - + {datasource && VariableQueryEditor && ( diff --git a/public/app/features/dashboard-scene/settings/variables/editors/QueryVariableEditor.tsx b/public/app/features/dashboard-scene/settings/variables/editors/QueryVariableEditor.tsx index 0c577d74b6..259c69bcbd 100644 --- a/public/app/features/dashboard-scene/settings/variables/editors/QueryVariableEditor.tsx +++ b/public/app/features/dashboard-scene/settings/variables/editors/QueryVariableEditor.tsx @@ -1,11 +1,8 @@ import React, { FormEvent } from 'react'; -import { useAsync } from 'react-use'; import { SelectableValue, DataSourceInstanceSettings } from '@grafana/data'; -import { getDataSourceSrv } from '@grafana/runtime'; import { QueryVariable, sceneGraph } from '@grafana/scenes'; import { DataSourceRef, VariableRefresh, VariableSort } from '@grafana/schema'; -import { getVariableQueryEditor } from 'app/features/variables/editor/getVariableQueryEditor'; import { QueryVariableEditorForm } from '../components/QueryVariableForm'; @@ -16,17 +13,9 @@ interface QueryVariableEditorProps { type VariableQueryType = QueryVariable['state']['query']; export function QueryVariableEditor({ variable, onRunQuery }: QueryVariableEditorProps) { - const { datasource: datasourceRef, regex, sort, refresh, isMulti, includeAll, allValue, query } = variable.useState(); + const { datasource, regex, sort, refresh, isMulti, includeAll, allValue, query } = variable.useState(); const { value: timeRange } = sceneGraph.getTimeRange(variable).useState(); - const { value: dsConfig } = useAsync(async () => { - const datasource = await getDataSourceSrv().get(datasourceRef ?? ''); - const VariableQueryEditor = await getVariableQueryEditor(datasource); - - return { datasource, VariableQueryEditor }; - }, [datasourceRef]); - const { datasource, VariableQueryEditor } = dsConfig ?? {}; - const onRegExChange = (event: React.FormEvent) => { variable.setState({ regex: event.currentTarget.value }); }; @@ -56,12 +45,11 @@ export function QueryVariableEditor({ variable, onRunQuery }: QueryVariableEdito return ( ) => { +const mockDS = mockDataSource({ + name: 'CloudManager', + type: DataSourceType.Alertmanager, +}); +const ds = new MockDataSourceApi(mockDS); +const editor = jest.fn().mockImplementation(LegacyVariableQueryEditor); + +ds.variables = { + getType: () => VariableSupportType.Custom, + query: jest.fn(), + editor: editor, + getDefaultQuery: jest.fn(), +}; + +const setupTestContext = async (options: Partial) => { const variableDefaults: Partial = { rootStateKey: 'key' }; const extended = { VariableQueryEditor: LegacyVariableQueryEditor, - dataSource: {} as unknown as DataSourceApi, + dataSource: ds, }; const defaults: Props = { @@ -33,20 +48,15 @@ const setupTestContext = (options: Partial) => { }; const props: Props & Record = { ...defaults, ...options }; - const { rerender } = render(); + const { rerender } = await act(() => render()); return { rerender, props }; }; -const mockDS = mockDataSource({ - name: 'CloudManager', - type: DataSourceType.Alertmanager, -}); - jest.mock('@grafana/runtime/src/services/dataSourceSrv', () => { return { getDataSourceSrv: () => ({ - get: () => Promise.resolve(mockDS), + get: async () => ds, getList: () => [mockDS], getInstanceSettings: () => mockDS, }), @@ -57,8 +67,8 @@ const defaultIdentifier: KeyedVariableIdentifier = { type: 'query', rootStateKey describe('QueryVariableEditor', () => { describe('when the component is mounted', () => { - it('then it should call initQueryVariableEditor', () => { - const { props } = setupTestContext({}); + it('then it should call initQueryVariableEditor', async () => { + const { props } = await setupTestContext({}); expect(props.initQueryVariableEditor).toHaveBeenCalledTimes(1); expect(props.initQueryVariableEditor).toHaveBeenCalledWith(defaultIdentifier); @@ -66,50 +76,29 @@ describe('QueryVariableEditor', () => { }); describe('when the editor is rendered', () => { - const extendedCustom = { - extended: { - VariableQueryEditor: jest.fn().mockImplementation(LegacyVariableQueryEditor), - dataSource: { - variables: { - getType: () => VariableSupportType.Custom, - query: jest.fn(), - editor: jest.fn(), - }, - } as unknown as DataSourceApi, - }, - }; - it('should pass down the query with default values if the datasource config defines it', () => { - const extended = { ...extendedCustom }; - extended.extended.dataSource.variables!.getDefaultQuery = jest - .fn() - .mockImplementation(() => 'some default query'); - const { props } = setupTestContext(extended); - expect(props.extended?.dataSource?.variables?.getDefaultQuery).toBeDefined(); - expect(props.extended?.dataSource?.variables?.getDefaultQuery).toHaveBeenCalledTimes(1); - expect(props.extended?.VariableQueryEditor).toHaveBeenCalledWith( - expect.objectContaining({ query: 'some default query' }), - expect.anything() - ); + beforeEach(() => { + jest.clearAllMocks(); }); - it('should not pass down a default query if the datasource config doesnt define it', () => { - extendedCustom.extended.dataSource.variables!.getDefaultQuery = undefined; - const { props } = setupTestContext(extendedCustom); - expect(props.extended?.dataSource?.variables?.getDefaultQuery).not.toBeDefined(); - expect(props.extended?.VariableQueryEditor).toHaveBeenCalledWith( - expect.objectContaining({ query: '' }), - expect.anything() - ); + + it('should pass down the query with default values if the datasource config defines it', async () => { + ds.variables!.getDefaultQuery = jest.fn().mockImplementationOnce(() => 'some default query'); + + await setupTestContext({}); + expect(ds.variables?.getDefaultQuery).toBeDefined(); + expect(ds.variables?.getDefaultQuery).toHaveBeenCalledTimes(1); + expect(editor.mock.calls[0][0].query).toBe('some default query'); }); }); + describe('when the user changes', () => { it.each` fieldName | propName | expectedArgs - ${'query'} | ${'changeQueryVariableQuery'} | ${[defaultIdentifier, 't', 't']} + ${'query'} | ${'changeQueryVariableQuery'} | ${[defaultIdentifier, 't', '']} ${'regex'} | ${'onPropChange'} | ${[{ propName: 'regex', propValue: 't', updateOptions: true }]} `( '$fieldName field and tabs away then $propName should be called with correct args', async ({ fieldName, propName, expectedArgs }) => { - const { props } = setupTestContext({}); + const { props } = await setupTestContext({}); const propUnderTest = props[propName]; const fieldAccessor = fieldAccessors[fieldName]; @@ -130,7 +119,7 @@ describe('QueryVariableEditor', () => { `( '$fieldName field but reverts the change and tabs away then $propName should not be called', async ({ fieldName, propName }) => { - const { props } = setupTestContext({}); + const { props } = await setupTestContext({}); const propUnderTest = props[propName]; const fieldAccessor = fieldAccessors[fieldName]; diff --git a/public/app/features/variables/query/QueryVariableEditor.tsx b/public/app/features/variables/query/QueryVariableEditor.tsx index 1f4e001681..24ad37c2a6 100644 --- a/public/app/features/variables/query/QueryVariableEditor.tsx +++ b/public/app/features/variables/query/QueryVariableEditor.tsx @@ -125,23 +125,19 @@ export class QueryVariableEditorUnConnected extends PureComponent render() { const { extended, variable } = this.props; - if (!extended || !extended.dataSource || !extended.VariableQueryEditor) { return null; } - const datasource = extended.dataSource; - const VariableQueryEditor = extended.VariableQueryEditor; const timeRange = getTimeSrv().timeRange(); return ( Date: Wed, 21 Feb 2024 17:14:49 +0100 Subject: [PATCH 0078/1406] Chore: Remove Form usage from notification policies (#81758) * Chore: Replace Form component usage in EditDefaultPolicyForm.tsx * Chore: Replace Form component usage in EditNotificationPolicyForm.tsx * Remove ts-ignore --- .../EditDefaultPolicyForm.tsx | 227 +++++----- .../EditNotificationPolicyForm.tsx | 412 +++++++++--------- 2 files changed, 324 insertions(+), 315 deletions(-) diff --git a/public/app/features/alerting/unified/components/notification-policies/EditDefaultPolicyForm.tsx b/public/app/features/alerting/unified/components/notification-policies/EditDefaultPolicyForm.tsx index fbfab072ad..2ae8f5f58d 100644 --- a/public/app/features/alerting/unified/components/notification-policies/EditDefaultPolicyForm.tsx +++ b/public/app/features/alerting/unified/components/notification-policies/EditDefaultPolicyForm.tsx @@ -1,6 +1,7 @@ import React, { ReactNode, useState } from 'react'; +import { useForm, Controller } from 'react-hook-form'; -import { Collapse, Field, Form, InputControl, Link, MultiSelect, Select, useStyles2 } from '@grafana/ui'; +import { Collapse, Field, Link, MultiSelect, Select, useStyles2 } from '@grafana/ui'; import { RouteWithID } from 'app/plugins/datasource/alertmanager/types'; import { FormAmRoute } from '../../types/amroutes'; @@ -41,125 +42,131 @@ export const AmRootRouteForm = ({ const [groupByOptions, setGroupByOptions] = useState(stringsToSelectableValues(route.group_by)); const defaultValues = amRouteToFormAmRoute(route); - + const { + handleSubmit, + register, + control, + formState: { errors }, + setValue, + getValues, + } = useForm({ + defaultValues: { + ...defaultValues, + overrideTimings: true, + overrideGrouping: true, + }, + }); return ( -
- {({ register, control, errors, setValue, getValues }) => ( + + <> - - <> -
- ( - - {/* @ts-ignore-check: react-hook-form made me do this */} - - {({ fields, append, remove }) => ( - <> - -
Matching labels
- {fields.length === 0 && ( - + + +
Matching labels
+ {fields.length === 0 && ( + + )} + {fields.length > 0 && ( +
+ {fields.map((field, index) => { + return ( + + + + + + ( + - - - ( - - - remove(index)}> - Remove - - - ); - })} -
- )} - + + + remove(index)}> + Remove +
- - )} -
- - ( - onChange(mapSelectValueToString(value))} + options={receiversWithOnCallOnTop} + isClearable + /> + )} + control={control} + name="receiver" + /> + + + + + + + + {watch().overrideGrouping && ( + + { + if (!value || value.length === 0) { + return 'At least one group by option is required.'; + } + return true; + }, + }} + render={({ field: { onChange, ref, ...field }, fieldState: { error } }) => ( + <> + onChange(mapSelectValueToString(value))} - options={receiversWithOnCallOnTop} - isClearable + onCreateOption={(opt: string) => { + setGroupByOptions((opts) => [...opts, stringToSelectableValue(opt)]); + setValue('groupBy', [...(field.value || []), opt]); + }} + onChange={(value) => onChange(mapMultiSelectValueToStrings(value))} + options={[...commonGroupByOptions, ...groupByOptions]} /> - )} - control={control} - name="receiver" + {error && {error.message}} + + )} + control={control} + name="groupBy" + /> + + )} + + + + {watch().overrideTimings && ( + <> + + - - - - - - - {watch().overrideGrouping && ( - - { - if (!value || value.length === 0) { - return 'At least one group by option is required.'; - } - return true; - }, - }} - render={({ field: { onChange, ref, ...field }, fieldState: { error } }) => ( - <> - { - setGroupByOptions((opts) => [...opts, stringToSelectableValue(opt)]); - - // @ts-ignore-check: react-hook-form made me do this - setValue('groupBy', [...field.value, opt]); - }} - onChange={(value) => onChange(mapMultiSelectValueToStrings(value))} - options={[...commonGroupByOptions, ...groupByOptions]} - /> - {error && {error.message}} - - )} - control={control} - name="groupBy" - /> - - )} - - + + - {watch().overrideTimings && ( - <> - - - - - - - - { - const groupInterval = getValues('groupIntervalValue'); - return repeatIntervalValidator(value, groupInterval); - }, - })} - aria-label={routeTimingsFields.repeatInterval.ariaLabel} - className={formStyles.promDurationInput} - /> - - - )} - ( - onChange(mapMultiSelectValueToStrings(value))} - options={muteTimingOptions} - /> - )} - control={control} - name="muteTimeIntervals" + { + const groupInterval = getValues('groupIntervalValue'); + return repeatIntervalValidator(value, groupInterval); + }, + })} + aria-label={routeTimingsFields.repeatInterval.ariaLabel} + className={formStyles.promDurationInput} /> - {actionButtons} )} - + + ( + onChange(mapMultiSelectValueToStrings(value))} + options={muteTimingOptions} + /> + )} + control={control} + name="muteTimeIntervals" + /> + + {actionButtons} + ); }; From ac88cfbdbb8123d2a60353ee719c0105c759f285 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 21 Feb 2024 18:23:53 +0200 Subject: [PATCH 0079/1406] I18n: Download translations from Crowdin (#83182) New Crowdin translations by GitHub Action Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- public/locales/de-DE/grafana.json | 4 ++++ public/locales/es-ES/grafana.json | 4 ++++ public/locales/fr-FR/grafana.json | 4 ++++ public/locales/zh-Hans/grafana.json | 4 ++++ 4 files changed, 16 insertions(+) diff --git a/public/locales/de-DE/grafana.json b/public/locales/de-DE/grafana.json index 9bcd31e064..4fe5020886 100644 --- a/public/locales/de-DE/grafana.json +++ b/public/locales/de-DE/grafana.json @@ -876,6 +876,10 @@ "manage-folder": { "subtitle": "Ordner-Dashboards und Berechtigungen verwalten" }, + "migrate-to-cloud": { + "subtitle": "", + "title": "" + }, "monitoring": { "subtitle": "Überwachungs- und Infrastruktur-Apps", "title": "Beobachtbarkeit" diff --git a/public/locales/es-ES/grafana.json b/public/locales/es-ES/grafana.json index 0c9fbc75b1..678a9c7a20 100644 --- a/public/locales/es-ES/grafana.json +++ b/public/locales/es-ES/grafana.json @@ -876,6 +876,10 @@ "manage-folder": { "subtitle": "Gestionar paneles de control de carpetas y permisos" }, + "migrate-to-cloud": { + "subtitle": "", + "title": "" + }, "monitoring": { "subtitle": "Aplicaciones de supervisión e infraestructura", "title": "Observabilidad" diff --git a/public/locales/fr-FR/grafana.json b/public/locales/fr-FR/grafana.json index f59fe0416e..5a8a02ea6f 100644 --- a/public/locales/fr-FR/grafana.json +++ b/public/locales/fr-FR/grafana.json @@ -876,6 +876,10 @@ "manage-folder": { "subtitle": "Gérer les tableaux de bord et les autorisations des dossiers" }, + "migrate-to-cloud": { + "subtitle": "", + "title": "" + }, "monitoring": { "subtitle": "Applications de suivi et d'infrastructure", "title": "Observabilité" diff --git a/public/locales/zh-Hans/grafana.json b/public/locales/zh-Hans/grafana.json index bcac82798d..94bca5f93a 100644 --- a/public/locales/zh-Hans/grafana.json +++ b/public/locales/zh-Hans/grafana.json @@ -870,6 +870,10 @@ "manage-folder": { "subtitle": "管理文件夹仪表板和权限" }, + "migrate-to-cloud": { + "subtitle": "", + "title": "" + }, "monitoring": { "subtitle": "监控和基础设施应用", "title": "可观测性" From d883af08dd58257d3ec0658971c5cab5598a147a Mon Sep 17 00:00:00 2001 From: Jev Forsberg <46619047+baldm0mma@users.noreply.github.com> Date: Wed, 21 Feb 2024 10:26:49 -0700 Subject: [PATCH 0080/1406] Dependencies(Dev): Add @types/eslint-scope (#83118) baldm0mma/add_eslint-scope_dep/ add dep and yarn add --- package.json | 2 ++ yarn.lock | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/package.json b/package.json index d94c06fed9..3d3677e24d 100644 --- a/package.json +++ b/package.json @@ -96,6 +96,7 @@ "@types/debounce-promise": "3.1.9", "@types/diff": "^5", "@types/eslint": "8.56.2", + "@types/eslint-scope": "^3.7.7", "@types/file-saver": "2.0.7", "@types/glob": "^8.0.0", "@types/google.analytics": "^0.0.46", @@ -167,6 +168,7 @@ "eslint-plugin-lodash": "7.4.0", "eslint-plugin-react": "7.33.2", "eslint-plugin-react-hooks": "4.6.0", + "eslint-scope": "^8.0.0", "eslint-webpack-plugin": "4.0.1", "expose-loader": "5.0.0", "fork-ts-checker-webpack-plugin": "9.0.2", diff --git a/yarn.lock b/yarn.lock index 8eb8ac7371..dff382da06 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9354,6 +9354,16 @@ __metadata: languageName: node linkType: hard +"@types/eslint-scope@npm:^3.7.7": + version: 3.7.7 + resolution: "@types/eslint-scope@npm:3.7.7" + dependencies: + "@types/eslint": "npm:*" + "@types/estree": "npm:*" + checksum: 10/e2889a124aaab0b89af1bab5959847c5bec09809209255de0e63b9f54c629a94781daa04adb66bffcdd742f5e25a17614fb933965093c0eea64aacda4309380e + languageName: node + linkType: hard + "@types/eslint@npm:*, @types/eslint@npm:8.56.2, @types/eslint@npm:^8.37.0, @types/eslint@npm:^8.4.10": version: 8.56.2 resolution: "@types/eslint@npm:8.56.2" @@ -16658,6 +16668,16 @@ __metadata: languageName: node linkType: hard +"eslint-scope@npm:^8.0.0": + version: 8.0.0 + resolution: "eslint-scope@npm:8.0.0" + dependencies: + esrecurse: "npm:^4.3.0" + estraverse: "npm:^5.2.0" + checksum: 10/c02f2d675a98ba74a33c4824858a75951cad59a3ff323994b25bfa9cecad0a8d515656af6fb4ef25da1e6f01099abcfd99efe32f512e8fea4876b70a81787b7b + languageName: node + linkType: hard + "eslint-visitor-keys@npm:^3.3.0, eslint-visitor-keys@npm:^3.4.1, eslint-visitor-keys@npm:^3.4.3": version: 3.4.3 resolution: "eslint-visitor-keys@npm:3.4.3" @@ -18528,6 +18548,7 @@ __metadata: "@types/debounce-promise": "npm:3.1.9" "@types/diff": "npm:^5" "@types/eslint": "npm:8.56.2" + "@types/eslint-scope": "npm:^3.7.7" "@types/file-saver": "npm:2.0.7" "@types/glob": "npm:^8.0.0" "@types/google.analytics": "npm:^0.0.46" @@ -18630,6 +18651,7 @@ __metadata: eslint-plugin-lodash: "npm:7.4.0" eslint-plugin-react: "npm:7.33.2" eslint-plugin-react-hooks: "npm:4.6.0" + eslint-scope: "npm:^8.0.0" eslint-webpack-plugin: "npm:4.0.1" eventemitter3: "npm:5.0.1" expose-loader: "npm:5.0.0" From 030b83d8f2278a7d46283c9c474636d530ff348c Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Wed, 21 Feb 2024 18:02:37 +0000 Subject: [PATCH 0081/1406] NestedFolderPicker: Seperate state from Browse Dashboards (#82672) * initial very very early stab at isolated state for nested folder picker * more * complete state rework. still need to do search * tidy up some comments * split api hook into seperate file, start to try and get search results back (its not working) * Fix loading status * Reset files * cleanup * fix tests * return object * restore hiding items * restore * restore * remove those comments * rename hooks * rename hooks * simplify selectors - thanks ash!!! * more ci please? --- .../NestedFolderPicker.test.tsx | 57 +++++ .../NestedFolderPicker/NestedFolderPicker.tsx | 119 +++++------ .../NestedFolderPicker/useFoldersQuery.ts | 201 ++++++++++++++++++ .../{hooks.ts => useTreeInteractions.ts} | 0 .../api/browseDashboardsAPI.ts | 13 ++ public/app/types/folders.ts | 5 + 6 files changed, 324 insertions(+), 71 deletions(-) create mode 100644 public/app/core/components/NestedFolderPicker/useFoldersQuery.ts rename public/app/core/components/NestedFolderPicker/{hooks.ts => useTreeInteractions.ts} (100%) diff --git a/public/app/core/components/NestedFolderPicker/NestedFolderPicker.test.tsx b/public/app/core/components/NestedFolderPicker/NestedFolderPicker.test.tsx index 73368d5175..a6cb752fb5 100644 --- a/public/app/core/components/NestedFolderPicker/NestedFolderPicker.test.tsx +++ b/public/app/core/components/NestedFolderPicker/NestedFolderPicker.test.tsx @@ -46,14 +46,37 @@ describe('NestedFolderPicker', () => { beforeAll(() => { window.HTMLElement.prototype.scrollIntoView = function () {}; + server = setupServer( http.get('/api/folders/:uid', () => { return HttpResponse.json({ title: folderA.item.title, uid: folderA.item.uid, }); + }), + + http.get('/api/folders', ({ request }) => { + const url = new URL(request.url); + const parentUid = url.searchParams.get('parentUid') ?? undefined; + + const limit = parseInt(url.searchParams.get('limit') ?? '1000', 10); + const page = parseInt(url.searchParams.get('page') ?? '1', 10); + + // reconstruct a folder API response from the flat tree fixture + const folders = mockTree + .filter((v) => v.item.kind === 'folder' && v.item.parentUID === parentUid) + .map((folder) => { + return { + uid: folder.item.uid, + title: folder.item.kind === 'folder' ? folder.item.title : "invalid - this shouldn't happen", + }; + }) + .slice(limit * (page - 1), limit * page); + + return HttpResponse.json(folders); }) ); + server.listen(); }); @@ -127,6 +150,40 @@ describe('NestedFolderPicker', () => { expect(mockOnChange).toHaveBeenCalledWith(folderA.item.uid, folderA.item.title); }); + it('shows the root folder by default', async () => { + render(); + + // Open the picker and wait for children to load + const button = await screen.findByRole('button', { name: 'Select folder' }); + await userEvent.click(button); + await screen.findByLabelText(folderA.item.title); + + await userEvent.click(screen.getByLabelText('Dashboards')); + expect(mockOnChange).toHaveBeenCalledWith('', 'Dashboards'); + }); + + it('hides the root folder if the prop says so', async () => { + render(); + + // Open the picker and wait for children to load + const button = await screen.findByRole('button', { name: 'Select folder' }); + await userEvent.click(button); + await screen.findByLabelText(folderA.item.title); + + expect(screen.queryByLabelText('Dashboards')).not.toBeInTheDocument(); + }); + + it('hides folders specififed by UID', async () => { + render(); + + // Open the picker and wait for children to load + const button = await screen.findByRole('button', { name: 'Select folder' }); + await userEvent.click(button); + await screen.findByLabelText(folderB.item.title); + + expect(screen.queryByLabelText(folderA.item.title)).not.toBeInTheDocument(); + }); + describe('when nestedFolders is enabled', () => { let originalToggles = { ...config.featureToggles }; diff --git a/public/app/core/components/NestedFolderPicker/NestedFolderPicker.tsx b/public/app/core/components/NestedFolderPicker/NestedFolderPicker.tsx index 7dd455e078..1f2ab2739d 100644 --- a/public/app/core/components/NestedFolderPicker/NestedFolderPicker.tsx +++ b/public/app/core/components/NestedFolderPicker/NestedFolderPicker.tsx @@ -8,25 +8,15 @@ import { config } from '@grafana/runtime'; import { Alert, Icon, Input, LoadingBar, useStyles2 } from '@grafana/ui'; import { t } from 'app/core/internationalization'; import { skipToken, useGetFolderQuery } from 'app/features/browse-dashboards/api/browseDashboardsAPI'; -import { PAGE_SIZE } from 'app/features/browse-dashboards/api/services'; -import { - childrenByParentUIDSelector, - createFlatTree, - fetchNextChildrenPage, - rootItemsSelector, - useBrowseLoadingStatus, - useLoadNextChildrenPage, -} from 'app/features/browse-dashboards/state'; -import { getPaginationPlaceholders } from 'app/features/browse-dashboards/state/utils'; -import { DashboardViewItemCollection } from 'app/features/browse-dashboards/types'; +import { DashboardViewItemWithUIItems, DashboardsTreeItem } from 'app/features/browse-dashboards/types'; import { QueryResponse, getGrafanaSearcher } from 'app/features/search/service'; import { queryResultToViewItem } from 'app/features/search/service/utils'; import { DashboardViewItem } from 'app/features/search/types'; -import { useDispatch, useSelector } from 'app/types/store'; import { getDOMId, NestedFolderList } from './NestedFolderList'; import Trigger from './Trigger'; -import { useTreeInteractions } from './hooks'; +import { ROOT_FOLDER_ITEM, useFoldersQuery } from './useFoldersQuery'; +import { useTreeInteractions } from './useTreeInteractions'; export interface NestedFolderPickerProps { /* Folder UID to show as selected */ @@ -48,8 +38,6 @@ export interface NestedFolderPickerProps { clearable?: boolean; } -const EXCLUDED_KINDS = ['empty-folder' as const, 'dashboard' as const]; - const debouncedSearch = debounce(getSearchResults, 300); async function getSearchResults(searchQuery: string) { @@ -72,10 +60,8 @@ export function NestedFolderPicker({ onChange, }: NestedFolderPickerProps) { const styles = useStyles2(getStyles); - const dispatch = useDispatch(); const selectedFolder = useGetFolderQuery(value || skipToken); - const rootStatus = useBrowseLoadingStatus(undefined); const nestedFoldersEnabled = Boolean(config.featureToggles.nestedFolders); const [search, setSearch] = useState(''); @@ -83,18 +69,27 @@ export function NestedFolderPicker({ const [isFetchingSearchResults, setIsFetchingSearchResults] = useState(false); const [autoFocusButton, setAutoFocusButton] = useState(false); const [overlayOpen, setOverlayOpen] = useState(false); - const [folderOpenState, setFolderOpenState] = useState>({}); + const [foldersOpenState, setFoldersOpenState] = useState>({}); const overlayId = useId(); const [error] = useState(undefined); // TODO: error not populated anymore const lastSearchTimestamp = useRef(0); + const isBrowsing = Boolean(overlayOpen && !(search && searchResults)); + const { + items: browseFlatTree, + isLoading: isBrowseLoading, + requestNextPage: fetchFolderPage, + } = useFoldersQuery(isBrowsing, foldersOpenState); + useEffect(() => { if (!search) { setSearchResults(null); return; } + const timestamp = Date.now(); setIsFetchingSearchResults(true); + debouncedSearch(search).then((queryResponse) => { // Only keep the results if it's was issued after the most recently resolved search. // This prevents results showing out of order if first request is slower than later ones. @@ -109,9 +104,6 @@ export function NestedFolderPicker({ }); }, [search]); - const rootCollection = useSelector(rootItemsSelector); - const childrenCollections = useSelector(childrenByParentUIDSelector); - // the order of middleware is important! const middleware = [ flip({ @@ -143,13 +135,13 @@ export function NestedFolderPicker({ const handleFolderExpand = useCallback( async (uid: string, newOpenState: boolean) => { - setFolderOpenState((old) => ({ ...old, [uid]: newOpenState })); + setFoldersOpenState((old) => ({ ...old, [uid]: newOpenState })); - if (newOpenState && !folderOpenState[uid]) { - dispatch(fetchNextChildrenPage({ parentUID: uid, pageSize: PAGE_SIZE, excludeKinds: EXCLUDED_KINDS })); + if (newOpenState && !foldersOpenState[uid]) { + fetchFolderPage(uid); } }, - [dispatch, folderOpenState] + [fetchFolderPage, foldersOpenState] ); const handleFolderSelect = useCallback( @@ -175,69 +167,53 @@ export function NestedFolderPicker({ const handleCloseOverlay = useCallback(() => setOverlayOpen(false), [setOverlayOpen]); - const baseHandleLoadMore = useLoadNextChildrenPage(EXCLUDED_KINDS); const handleLoadMore = useCallback( (folderUID: string | undefined) => { if (search) { return; } - baseHandleLoadMore(folderUID); + fetchFolderPage(folderUID); }, - [search, baseHandleLoadMore] + [search, fetchFolderPage] ); const flatTree = useMemo(() => { - if (search && searchResults) { - const searchCollection: DashboardViewItemCollection = { - isFullyLoaded: true, //searchResults.items.length === searchResults.totalRows, - lastKindHasMoreItems: false, // TODO: paginate search - lastFetchedKind: 'folder', // TODO: paginate search - lastFetchedPage: 1, // TODO: paginate search - items: searchResults.items ?? [], - }; - - return createFlatTree(undefined, searchCollection, childrenCollections, {}, 0, EXCLUDED_KINDS, excludeUIDs); + let flatTree: Array> = []; + + if (isBrowsing) { + flatTree = browseFlatTree; + } else { + flatTree = + searchResults?.items.map((item) => ({ + isOpen: false, + level: 0, + item: { + kind: 'folder' as const, + title: item.title, + uid: item.uid, + }, + })) ?? []; } - const allExcludedUIDs = config.sharedWithMeFolderUID - ? [...(excludeUIDs || []), config.sharedWithMeFolderUID] - : excludeUIDs; - - let flatTree = createFlatTree( - undefined, - rootCollection, - childrenCollections, - folderOpenState, - 0, - EXCLUDED_KINDS, - allExcludedUIDs - ); + // It's not super optimal to filter these in an additional iteration, but + // these options are used infrequently that its not a big deal + if (!showRootFolder || excludeUIDs?.length) { + flatTree = flatTree.filter((item) => { + if (!showRootFolder && item === ROOT_FOLDER_ITEM) { + return false; + } - if (showRootFolder) { - // Increase the level of each item to 'make way' for the fake root Dashboards item - for (const item of flatTree) { - item.level += 1; - } + if (excludeUIDs?.includes(item.item.uid)) { + return false; + } - flatTree.unshift({ - isOpen: true, - level: 0, - item: { - kind: 'folder', - title: 'Dashboards', - uid: '', - }, + return true; }); } - // If the root collection hasn't loaded yet, create loading placeholders - if (!rootCollection) { - flatTree = flatTree.concat(getPaginationPlaceholders(PAGE_SIZE, undefined, 0)); - } - return flatTree; - }, [search, searchResults, rootCollection, childrenCollections, folderOpenState, excludeUIDs, showRootFolder]); + }, [browseFlatTree, excludeUIDs, isBrowsing, searchResults?.items, showRootFolder]); const isItemLoaded = useCallback( (itemIndex: number) => { @@ -245,6 +221,7 @@ export function NestedFolderPicker({ if (!treeItem) { return false; } + const item = treeItem.item; const result = !(item.kind === 'ui' && item.uiKind === 'pagination-placeholder'); @@ -253,7 +230,7 @@ export function NestedFolderPicker({ [flatTree] ); - const isLoading = rootStatus === 'pending' || isFetchingSearchResults; + const isLoading = isBrowseLoading || isFetchingSearchResults; const { focusedItemIndex, handleKeyDown } = useTreeInteractions({ tree: flatTree, diff --git a/public/app/core/components/NestedFolderPicker/useFoldersQuery.ts b/public/app/core/components/NestedFolderPicker/useFoldersQuery.ts new file mode 100644 index 0000000000..528ca80ab1 --- /dev/null +++ b/public/app/core/components/NestedFolderPicker/useFoldersQuery.ts @@ -0,0 +1,201 @@ +import { createSelector } from '@reduxjs/toolkit'; +import { QueryDefinition, BaseQueryFn } from '@reduxjs/toolkit/dist/query'; +import { QueryActionCreatorResult } from '@reduxjs/toolkit/dist/query/core/buildInitiate'; +import { RequestOptions } from 'http'; +import { useCallback, useEffect, useMemo, useRef } from 'react'; + +import { ListFolderQueryArgs, browseDashboardsAPI } from 'app/features/browse-dashboards/api/browseDashboardsAPI'; +import { PAGE_SIZE } from 'app/features/browse-dashboards/api/services'; +import { getPaginationPlaceholders } from 'app/features/browse-dashboards/state/utils'; +import { DashboardViewItemWithUIItems, DashboardsTreeItem } from 'app/features/browse-dashboards/types'; +import { RootState } from 'app/store/configureStore'; +import { FolderListItemDTO } from 'app/types'; +import { useDispatch, useSelector } from 'app/types/store'; + +type ListFoldersQuery = ReturnType>; +type ListFoldersRequest = QueryActionCreatorResult< + QueryDefinition< + ListFolderQueryArgs, + BaseQueryFn, + 'getFolder', + FolderListItemDTO[], + 'browseDashboardsAPI' + > +>; + +const listFoldersSelector = createSelector( + (state: RootState) => state, + ( + state: RootState, + parentUid: ListFolderQueryArgs['parentUid'], + page: ListFolderQueryArgs['page'], + limit: ListFolderQueryArgs['limit'] + ) => browseDashboardsAPI.endpoints.listFolders.select({ parentUid, page, limit }), + (state, selectFolderList) => selectFolderList(state) +); + +const listAllFoldersSelector = createSelector( + [(state: RootState) => state, (state: RootState, requests: ListFoldersRequest[]) => requests], + (state: RootState, requests: ListFoldersRequest[]) => { + const seenRequests = new Set(); + + const rootPages: ListFoldersQuery[] = []; + const pagesByParent: Record = {}; + let isLoading = false; + + for (const req of requests) { + if (seenRequests.has(req.requestId)) { + continue; + } + + const page = listFoldersSelector(state, req.arg.parentUid, req.arg.page, req.arg.limit); + if (page.status === 'pending') { + isLoading = true; + } + + const parentUid = page.originalArgs?.parentUid; + if (parentUid) { + if (!pagesByParent[parentUid]) { + pagesByParent[parentUid] = []; + } + + pagesByParent[parentUid].push(page); + } else { + rootPages.push(page); + } + } + + return { + isLoading, + rootPages, + pagesByParent, + }; + } +); + +/** + * Returns the whether the set of pages are 'fully loaded', and the last page number + */ +function getPagesLoadStatus(pages: ListFoldersQuery[]): [boolean, number | undefined] { + const lastPage = pages.at(-1); + const lastPageNumber = lastPage?.originalArgs?.page; + + if (!lastPage?.data) { + // If there's no pages yet, or the last page is still loading + return [false, lastPageNumber]; + } else { + return [lastPage.data.length < lastPage.originalArgs.limit, lastPageNumber]; + } +} + +/** + * Returns a loaded folder hierarchy as a flat list and a function to load more pages. + */ +export function useFoldersQuery(isBrowsing: boolean, openFolders: Record) { + const dispatch = useDispatch(); + + // Keep a list of all requests so we can + // a) unsubscribe from them when the component is unmounted + // b) use them to select the responses out of the state + const requestsRef = useRef([]); + + const state = useSelector((rootState: RootState) => { + return listAllFoldersSelector(rootState, requestsRef.current); + }); + + // Loads the next page of folders for the given parent UID by inspecting the + // state to determine what the next page is + const requestNextPage = useCallback( + (parentUid: string | undefined) => { + const pages = parentUid ? state.pagesByParent[parentUid] : state.rootPages; + const [fullyLoaded, pageNumber] = getPagesLoadStatus(pages ?? []); + if (fullyLoaded) { + return; + } + + const args = { parentUid, page: (pageNumber ?? 0) + 1, limit: PAGE_SIZE }; + const promise = dispatch(browseDashboardsAPI.endpoints.listFolders.initiate(args)); + + // It's important that we create a new array so we can correctly memoize with it + requestsRef.current = requestsRef.current.concat([promise]); + }, + [state, dispatch] + ); + + // Unsubscribe from all requests when the component is unmounted + useEffect(() => { + return () => { + for (const req of requestsRef.current) { + req.unsubscribe(); + } + }; + }, []); + + // Convert the individual responses into a flat list of folders, with level indicating + // the depth in the hierarchy. + const treeList = useMemo(() => { + if (!isBrowsing) { + return []; + } + + function createFlatList( + parentUid: string | undefined, + pages: ListFoldersQuery[], + level: number + ): Array> { + const flatList = pages.flatMap((page) => { + const pageItems = page.data ?? []; + + return pageItems.flatMap((item) => { + const folderIsOpen = openFolders[item.uid]; + + const flatItem: DashboardsTreeItem = { + isOpen: Boolean(folderIsOpen), + level: level, + item: { + kind: 'folder' as const, + title: item.title, + uid: item.uid, + }, + }; + + const childPages = folderIsOpen && state.pagesByParent[item.uid]; + if (childPages) { + const childFlatItems = createFlatList(item.uid, childPages, level + 1); + return [flatItem, ...childFlatItems]; + } + + return flatItem; + }); + }); + + const [fullyLoaded] = getPagesLoadStatus(pages); + if (!fullyLoaded) { + flatList.push(...getPaginationPlaceholders(PAGE_SIZE, parentUid, level)); + } + + return flatList; + } + + const rootFlatTree = createFlatList(undefined, state.rootPages, 1); + rootFlatTree.unshift(ROOT_FOLDER_ITEM); + + return rootFlatTree; + }, [state, isBrowsing, openFolders]); + + return { + items: treeList, + isLoading: state.isLoading, + requestNextPage, + }; +} + +export const ROOT_FOLDER_ITEM = { + isOpen: true, + level: 0, + item: { + kind: 'folder' as const, + title: 'Dashboards', + uid: '', + }, +}; diff --git a/public/app/core/components/NestedFolderPicker/hooks.ts b/public/app/core/components/NestedFolderPicker/useTreeInteractions.ts similarity index 100% rename from public/app/core/components/NestedFolderPicker/hooks.ts rename to public/app/core/components/NestedFolderPicker/useTreeInteractions.ts diff --git a/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts b/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts index be06306717..4e60a5c2a4 100644 --- a/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts +++ b/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts @@ -14,6 +14,7 @@ import { DescendantCount, DescendantCountDTO, FolderDTO, + FolderListItemDTO, ImportDashboardResponseDTO, SaveDashboardResponseDTO, } from 'app/types'; @@ -69,11 +70,22 @@ function createBackendSrvBaseQuery({ baseURL }: { baseURL: string }): BaseQueryF return backendSrvBaseQuery; } +export interface ListFolderQueryArgs { + page: number; + parentUid: string | undefined; + limit: number; +} + export const browseDashboardsAPI = createApi({ tagTypes: ['getFolder'], reducerPath: 'browseDashboardsAPI', baseQuery: createBackendSrvBaseQuery({ baseURL: '/api' }), endpoints: (builder) => ({ + listFolders: builder.query({ + providesTags: (result) => result?.map((folder) => ({ type: 'getFolder', id: folder.uid })) ?? [], + query: ({ page, parentUid, limit }) => ({ url: '/folders', params: { page, parentUid, limit } }), + }), + // get folder info (e.g. title, parents) but *not* children getFolder: builder.query({ providesTags: (_result, _error, folderUID) => [{ type: 'getFolder', id: folderUID }], @@ -360,4 +372,5 @@ export const { useSaveDashboardMutation, useSaveFolderMutation, } = browseDashboardsAPI; + export { skipToken } from '@reduxjs/toolkit/query/react'; diff --git a/public/app/types/folders.ts b/public/app/types/folders.ts index 6c80fe7ca0..83d01c5c27 100644 --- a/public/app/types/folders.ts +++ b/public/app/types/folders.ts @@ -1,5 +1,10 @@ import { WithAccessControlMetadata } from '@grafana/data'; +export interface FolderListItemDTO { + uid: string; + title: string; +} + export interface FolderDTO extends WithAccessControlMetadata { canAdmin: boolean; canDelete: boolean; From cfcf03bf7af4f1928ed728aa38eb86c874ba46cc Mon Sep 17 00:00:00 2001 From: Pablo <2617411+thepalbi@users.noreply.github.com> Date: Wed, 21 Feb 2024 16:57:31 -0300 Subject: [PATCH 0082/1406] CloudWatch: Add Firehose kms-related metrics (#83192) Add KMS firehose metrics --- pkg/tsdb/cloudwatch/constants/metrics.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/tsdb/cloudwatch/constants/metrics.go b/pkg/tsdb/cloudwatch/constants/metrics.go index bf8555d648..4d95496735 100644 --- a/pkg/tsdb/cloudwatch/constants/metrics.go +++ b/pkg/tsdb/cloudwatch/constants/metrics.go @@ -394,7 +394,7 @@ var NamespaceMetricsMap = map[string][]string{ "AWS/ElasticTranscoder": {"Billed Audio Output", "Billed HD Output", "Billed SD Output", "Errors", "Jobs Completed", "Jobs Errored", "Outputs per Job", "Standby Time", "Throttles"}, "AWS/EventBridge/Pipes": {"Concurrency", "Duration", "EnrichmentStageDuration", "EnrichmentStageFailed", "EventCount", "EventSize", "ExecutionFailed", "ExecutionPartiallyFailed", "ExecutionThrottled", "ExecutionTimeout", "Invocations", "TargetStageDuration", "TargetStageFailed", "TargetStagePartiallyFailed", "TargetStageSkipped"}, "AWS/Events": {"DeadLetterInvocations", "Events", "FailedInvocations", "IngestionToInvocationStartLatency", "Invocations", "InvocationsFailedToBeSentToDlq", "InvocationsSentToDlq", "MatchedEvents", "ThrottledRules", "TriggeredRules"}, - "AWS/Firehose": {"ActivePartitionsLimit", "BackupToS3.Bytes", "BackupToS3.DataFreshness", "BackupToS3.Records", "BackupToS3.Success", "DataReadFromKinesisStream.Bytes", "DataReadFromKinesisStream.Records", "DeliveryToElasticsearch.Bytes", "DeliveryToElasticsearch.Records", "DeliveryToElasticsearch.Success", "DeliveryToRedshift.Bytes", "DeliveryToRedshift.Records", "DeliveryToRedshift.Success", "DeliveryToS3.Bytes", "DeliveryToS3.DataFreshness", "DeliveryToS3.ObjectCount", "DeliveryToS3.Records", "DeliveryToS3.Success", "DeliveryToSplunk.Bytes", "DeliveryToSplunk.DataFreshness", "DeliveryToSplunk.Records", "DeliveryToSplunk.Success", "DescribeDeliveryStream.Latency", "DescribeDeliveryStream.Requests", "ExecuteProcessing.Duration", "ExecuteProcessing.Success", "FailedConversion.Bytes", "FailedConversion.Records", "IncomingBytes", "IncomingRecords", "JQProcessing.Duration", "KinesisMillisBehindLatest", "ListDeliveryStreams.Latency", "ListDeliveryStreams.Requests", "PartitionCount", "PartitionCountExceeded", "PerPartitionThroughput", "PutRecord.Bytes", "PutRecord.Latency", "PutRecord.Requests", "PutRecordBatch.Bytes", "PutRecordBatch.Latency", "PutRecordBatch.Records", "PutRecordBatch.Requests", "SucceedConversion.Bytes", "SucceedConversion.Records", "SucceedProcessing.Bytes", "SucceedProcessing.Records", "ThrottledDescribeStream", "ThrottledGetRecords", "ThrottledGetShardIterator", "UpdateDeliveryStream.Latency", "UpdateDeliveryStream.Requests"}, + "AWS/Firehose": {"ActivePartitionsLimit", "BackupToS3.Bytes", "BackupToS3.DataFreshness", "BackupToS3.Records", "BackupToS3.Success", "DataReadFromKinesisStream.Bytes", "DataReadFromKinesisStream.Records", "DeliveryToElasticsearch.Bytes", "DeliveryToElasticsearch.Records", "DeliveryToElasticsearch.Success", "DeliveryToRedshift.Bytes", "DeliveryToRedshift.Records", "DeliveryToRedshift.Success", "DeliveryToS3.Bytes", "DeliveryToS3.DataFreshness", "DeliveryToS3.ObjectCount", "DeliveryToS3.Records", "DeliveryToS3.Success", "DeliveryToSplunk.Bytes", "DeliveryToSplunk.DataFreshness", "DeliveryToSplunk.Records", "DeliveryToSplunk.Success", "DescribeDeliveryStream.Latency", "DescribeDeliveryStream.Requests", "ExecuteProcessing.Duration", "ExecuteProcessing.Success", "FailedConversion.Bytes", "FailedConversion.Records", "IncomingBytes", "IncomingRecords", "JQProcessing.Duration", "KinesisMillisBehindLatest", "KMSKeyAccessDenied", "KMSKeyDisabled", "KMSKeyInvalidState", "KMSKeyNotFound", "ListDeliveryStreams.Latency", "ListDeliveryStreams.Requests", "PartitionCount", "PartitionCountExceeded", "PerPartitionThroughput", "PutRecord.Bytes", "PutRecord.Latency", "PutRecord.Requests", "PutRecordBatch.Bytes", "PutRecordBatch.Latency", "PutRecordBatch.Records", "PutRecordBatch.Requests", "SucceedConversion.Bytes", "SucceedConversion.Records", "SucceedProcessing.Bytes", "SucceedProcessing.Records", "ThrottledDescribeStream", "ThrottledGetRecords", "ThrottledGetShardIterator", "UpdateDeliveryStream.Latency", "UpdateDeliveryStream.Requests"}, "AWS/FSx": {"ClientConnections", "CPUUtilization", "DataReadBytes", "DataReadOperations", "DataWriteBytes", "DataWriteOperations", "DeduplicationSavedStorage", "DiskIopsUtilization", "DiskReadBytes", "DiskReadOperations", "DiskThroughputBalance", "DiskThroughputUtilization", "DiskWriteBytes", "DiskWriteOperations", "FileServerDiskIopsBalance", "FileServerDiskIopsUtilization", "FileServerDiskThroughputBalance", "FileServerDiskThroughputUtilization", "FreeDataStorageCapacity", "FreeStorageCapacity", "MemoryUtilization", "MetadataOperations", "NetworkThroughputUtilization", "StorageCapacityUtilization"}, "AWS/FraudDetector": {"GetEventPrediction", "GetEventPrediction4xxError", "GetEventPrediction5xxError", "GetEventPredictionLatency", "ModelInvocation", "ModelInvocationError", "ModelInvocationLatency", "OutcomeReturned", "Prediction", "PredictionError", "PredictionLatency", "RuleEvaluateError", "RuleEvaluateFalse", "RuleEvaluateTrue", "RuleNotEvaluated", "VariableDefaultReturned", "VariableUsed"}, "AWS/GameLift": {"ActivatingGameSessions", "ActiveGameSessions", "ActiveInstances", "ActiveServerProcesses", "AvailableGameServers", "AvailableGameSessions", "AverageWaitTime", "CurrentPlayerSessions", "CurrentTickets", "DesiredInstances", "DrainingAvailableGameServers", "DrainingUtilizedGameServers", "FirstChoiceNotViable", "FirstChoiceOutOfCapacity", "GameSessionInterruptions", "HealthyServerProcesses", "IdleInstances", "InstanceInterruptions", "LowestLatencyPlacement", "LowestPricePlacement", "MatchAcceptancesTimedOut", "MatchesAccepted", "MatchesCreated", "MatchesPlaced", "MatchesRejected", "MaxInstances", "MinInstances", "PercentAvailableGameSessions", "PercentHealthyServerProcesses", "PercentIdleInstances", "Placement", "PlacementsCanceled", "PlacementsFailed", "PlacementsStarted", "PlacementsSucceeded", "PlacementsTimedOut", "PlayerSessionActivations", "PlayersStarted", "QueueDepth", "RuleEvaluationsFailed", "RuleEvaluationsPassed", "ServerProcessAbnormalTerminations", "ServerProcessActivations", "ServerProcessTerminations", "TicketsFailed", "TicketsStarted", "TicketsTimedOut", "TimeToMatch", "TimeToTicketSuccess", "UtilizedGameServers"}, From 8d68159b523f96aadbb8c811d3125577df0a89db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agn=C3=A8s=20Toulet?= <35176601+AgnesToulet@users.noreply.github.com> Date: Thu, 22 Feb 2024 09:34:14 +0100 Subject: [PATCH 0083/1406] Public Dashboards: Disable email-sharing when there is no license (#80887) * PublicDashboards: Disable email-shared dashboards when feature is disabled * fix pubdash creation when it was email-shared * add feature name const in OSS * update doc * Update service.go * fix test & linter * fix test * Update query_test.go * update tests * fix imports * fix doc linter issues * Update docs/sources/administration/enterprise-licensing/_index.md * fix after merge --- .../enterprise-licensing/_index.md | 7 + pkg/api/dashboard.go | 2 +- pkg/api/dashboard_test.go | 6 +- pkg/services/publicdashboards/api/api.go | 8 +- .../publicdashboards/api/common_test.go | 6 +- .../publicdashboards/api/query_test.go | 5 +- .../publicdashboards/models/models.go | 9 +- .../publicdashboards/service/common_test.go | 51 ++++ .../publicdashboards/service/query_test.go | 141 +++-------- .../publicdashboards/service/service.go | 14 ++ .../publicdashboards/service/service_test.go | 223 ++++-------------- 11 files changed, 174 insertions(+), 298 deletions(-) create mode 100644 pkg/services/publicdashboards/service/common_test.go diff --git a/docs/sources/administration/enterprise-licensing/_index.md b/docs/sources/administration/enterprise-licensing/_index.md index 6dd8c9e2f7..5d79309768 100644 --- a/docs/sources/administration/enterprise-licensing/_index.md +++ b/docs/sources/administration/enterprise-licensing/_index.md @@ -196,6 +196,13 @@ The active users limit is turned off immediately. Settings updates at runtime are not affected by an expired license. +#### Email sharing + +External users can't access dashboards shared via email anymore. +These dashboards are now private but you can make them public and accessible to everyone if you want to. + +Grafana keeps your sharing configurations and restores them after you update your license. + ## Grafana Enterprise license restrictions When you become a Grafana Enterprise customer, you receive a license that governs your use of Grafana Enterprise. diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 1462e330b3..03cb854a38 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -98,7 +98,7 @@ func (hs *HTTPServer) GetDashboard(c *contextmodel.ReqContext) response.Response return response.Error(http.StatusInternalServerError, "Error while retrieving public dashboards", err) } - if publicDashboard != nil { + if publicDashboard != nil && (hs.License.FeatureEnabled(publicdashboardModels.FeaturePublicDashboardsEmailSharing) || publicDashboard.Share != publicdashboardModels.EmailShareType) { publicDashboardEnabled = publicDashboard.IsEnabled } } diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index 2f41f7b7ad..4f759ae100 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -44,6 +44,7 @@ import ( "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/libraryelements/model" "github.com/grafana/grafana/pkg/services/librarypanels" + "github.com/grafana/grafana/pkg/services/licensing/licensingtest" "github.com/grafana/grafana/pkg/services/live" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" @@ -52,6 +53,7 @@ import ( "github.com/grafana/grafana/pkg/services/provisioning" "github.com/grafana/grafana/pkg/services/publicdashboards" "github.com/grafana/grafana/pkg/services/publicdashboards/api" + publicdashboardModels "github.com/grafana/grafana/pkg/services/publicdashboards/models" "github.com/grafana/grafana/pkg/services/quota/quotatest" "github.com/grafana/grafana/pkg/services/star/startest" "github.com/grafana/grafana/pkg/services/tag/tagimpl" @@ -272,7 +274,9 @@ func TestHTTPServer_DeleteDashboardByUID_AccessControl(t *testing.T) { pubDashService := publicdashboards.NewFakePublicDashboardService(t) pubDashService.On("DeleteByDashboard", mock.Anything, mock.Anything).Return(nil).Maybe() middleware := publicdashboards.NewFakePublicDashboardMiddleware(t) - hs.PublicDashboardsApi = api.ProvideApi(pubDashService, nil, hs.AccessControl, featuremgmt.WithFeatures(), middleware, hs.Cfg) + license := licensingtest.NewFakeLicensing() + license.On("FeatureEnabled", publicdashboardModels.FeaturePublicDashboardsEmailSharing).Return(false) + hs.PublicDashboardsApi = api.ProvideApi(pubDashService, nil, hs.AccessControl, featuremgmt.WithFeatures(), middleware, hs.Cfg, license) guardian.InitAccessControlGuardian(hs.Cfg, hs.AccessControl, hs.DashboardService) }) diff --git a/pkg/services/publicdashboards/api/api.go b/pkg/services/publicdashboards/api/api.go index d65ac5b13e..2aa8dea58d 100644 --- a/pkg/services/publicdashboards/api/api.go +++ b/pkg/services/publicdashboards/api/api.go @@ -14,6 +14,7 @@ import ( contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/licensing" "github.com/grafana/grafana/pkg/services/publicdashboards" . "github.com/grafana/grafana/pkg/services/publicdashboards/models" "github.com/grafana/grafana/pkg/services/publicdashboards/validation" @@ -28,6 +29,7 @@ type Api struct { accessControl accesscontrol.AccessControl cfg *setting.Cfg features featuremgmt.FeatureToggles + license licensing.Licensing log log.Logger routeRegister routing.RouteRegister } @@ -39,6 +41,7 @@ func ProvideApi( features featuremgmt.FeatureToggles, md publicdashboards.Middleware, cfg *setting.Cfg, + license licensing.Licensing, ) *Api { api := &Api{ PublicDashboardService: pd, @@ -46,6 +49,7 @@ func ProvideApi( accessControl: ac, cfg: cfg, features: features, + license: license, log: log.New("publicdashboards.api"), routeRegister: rr, } @@ -158,8 +162,8 @@ func (api *Api) GetPublicDashboard(c *contextmodel.ReqContext) response.Response return response.Err(err) } - if pd == nil { - response.Err(ErrPublicDashboardNotFound.Errorf("GetPublicDashboard: public dashboard not found")) + if pd == nil || (!api.license.FeatureEnabled(FeaturePublicDashboardsEmailSharing) && pd.Share == EmailShareType) { + return response.Err(ErrPublicDashboardNotFound.Errorf("GetPublicDashboard: public dashboard not found")) } return response.JSON(http.StatusOK, pd) diff --git a/pkg/services/publicdashboards/api/common_test.go b/pkg/services/publicdashboards/api/common_test.go index 7a857bc6f1..5e07e1ecff 100644 --- a/pkg/services/publicdashboards/api/common_test.go +++ b/pkg/services/publicdashboards/api/common_test.go @@ -26,10 +26,12 @@ import ( "github.com/grafana/grafana/pkg/services/datasources/guardian" datasourceService "github.com/grafana/grafana/pkg/services/datasources/service" "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/licensing/licensingtest" "github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext" pluginSettings "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings/service" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" "github.com/grafana/grafana/pkg/services/publicdashboards" + publicdashboardModels "github.com/grafana/grafana/pkg/services/publicdashboards/models" "github.com/grafana/grafana/pkg/services/query" fakeSecrets "github.com/grafana/grafana/pkg/services/secrets/fakes" "github.com/grafana/grafana/pkg/services/user" @@ -73,7 +75,9 @@ func setupTestServer( } // build api, this will mount the routes at the same time if the feature is enabled - ProvideApi(service, rr, ac, features, &Middleware{}, cfg) + license := licensingtest.NewFakeLicensing() + license.On("FeatureEnabled", publicdashboardModels.FeaturePublicDashboardsEmailSharing).Return(false) + ProvideApi(service, rr, ac, features, &Middleware{}, cfg, license) // connect routes to mux rr.Register(m.Router) diff --git a/pkg/services/publicdashboards/api/query_test.go b/pkg/services/publicdashboards/api/query_test.go index 7371d03659..b2a456d84e 100644 --- a/pkg/services/publicdashboards/api/query_test.go +++ b/pkg/services/publicdashboards/api/query_test.go @@ -32,6 +32,7 @@ import ( "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder/folderimpl" "github.com/grafana/grafana/pkg/services/folder/foldertest" + "github.com/grafana/grafana/pkg/services/licensing/licensingtest" "github.com/grafana/grafana/pkg/services/publicdashboards" publicdashboardsStore "github.com/grafana/grafana/pkg/services/publicdashboards/database" . "github.com/grafana/grafana/pkg/services/publicdashboards/models" @@ -331,7 +332,9 @@ func TestIntegrationUnauthenticatedUserCanGetPubdashPanelQueryData(t *testing.T) ) require.NoError(t, err) - pds := publicdashboardsService.ProvideService(cfg, store, qds, annotationsService, ac, ws, dashService) + license := licensingtest.NewFakeLicensing() + license.On("FeatureEnabled", FeaturePublicDashboardsEmailSharing).Return(false) + pds := publicdashboardsService.ProvideService(cfg, store, qds, annotationsService, ac, ws, dashService, license) pubdash, err := pds.Create(context.Background(), &user.SignedInUser{}, savePubDashboardCmd) require.NoError(t, err) diff --git a/pkg/services/publicdashboards/models/models.go b/pkg/services/publicdashboards/models/models.go index e646be7f54..676eb96c96 100644 --- a/pkg/services/publicdashboards/models/models.go +++ b/pkg/services/publicdashboards/models/models.go @@ -24,10 +24,11 @@ func (e PublicDashboardErr) Error() string { } const ( - QuerySuccess = "success" - QueryFailure = "failure" - EmailShareType ShareType = "email" - PublicShareType ShareType = "public" + QuerySuccess = "success" + QueryFailure = "failure" + EmailShareType ShareType = "email" + PublicShareType ShareType = "public" + FeaturePublicDashboardsEmailSharing = "publicDashboardsEmailSharing" ) var ( diff --git a/pkg/services/publicdashboards/service/common_test.go b/pkg/services/publicdashboards/service/common_test.go new file mode 100644 index 0000000000..5840346d5a --- /dev/null +++ b/pkg/services/publicdashboards/service/common_test.go @@ -0,0 +1,51 @@ +package service + +import ( + "testing" + + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/annotations" + "github.com/grafana/grafana/pkg/services/annotations/annotationsimpl" + "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/licensing/licensingtest" + "github.com/grafana/grafana/pkg/services/publicdashboards" + "github.com/grafana/grafana/pkg/services/publicdashboards/database" + . "github.com/grafana/grafana/pkg/services/publicdashboards/models" + "github.com/grafana/grafana/pkg/services/publicdashboards/service/intervalv2" + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/services/tag/tagimpl" +) + +func newPublicDashboardServiceImpl( + t *testing.T, + publicDashboardStore publicdashboards.Store, + dashboardService dashboards.DashboardService, + annotationsRepo annotations.Repository, +) (*PublicDashboardServiceImpl, *sqlstore.SQLStore) { + t.Helper() + + sqlStore := sqlstore.InitTestDB(t) + tagService := tagimpl.ProvideService(sqlStore) + if annotationsRepo == nil { + annotationsRepo = annotationsimpl.ProvideService(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagService) + } + + if publicDashboardStore == nil { + publicDashboardStore = database.ProvideStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures()) + } + serviceWrapper := ProvideServiceWrapper(publicDashboardStore) + + license := licensingtest.NewFakeLicensing() + license.On("FeatureEnabled", FeaturePublicDashboardsEmailSharing).Return(false) + + return &PublicDashboardServiceImpl{ + AnnotationsRepo: annotationsRepo, + log: log.New("test.logger"), + intervalCalculator: intervalv2.NewCalculator(), + dashboardService: dashboardService, + store: publicDashboardStore, + serviceWrapper: serviceWrapper, + license: license, + }, sqlStore +} diff --git a/pkg/services/publicdashboards/service/query_test.go b/pkg/services/publicdashboards/service/query_test.go index f33a3cfaf5..ec1eaa0c21 100644 --- a/pkg/services/publicdashboards/service/query_test.go +++ b/pkg/services/publicdashboards/service/query_test.go @@ -12,23 +12,17 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/infra/log" dashboard2 "github.com/grafana/grafana/pkg/kinds/dashboard" "github.com/grafana/grafana/pkg/services/annotations" - "github.com/grafana/grafana/pkg/services/annotations/annotationsimpl" "github.com/grafana/grafana/pkg/services/dashboards" dashboardsDB "github.com/grafana/grafana/pkg/services/dashboards/database" "github.com/grafana/grafana/pkg/services/featuremgmt" . "github.com/grafana/grafana/pkg/services/publicdashboards" - "github.com/grafana/grafana/pkg/services/publicdashboards/database" "github.com/grafana/grafana/pkg/services/publicdashboards/internal" . "github.com/grafana/grafana/pkg/services/publicdashboards/models" - "github.com/grafana/grafana/pkg/services/publicdashboards/service/intervalv2" "github.com/grafana/grafana/pkg/services/query" "github.com/grafana/grafana/pkg/services/quota/quotatest" - "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/tag/tagimpl" - "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/tsdb/legacydata" "github.com/grafana/grafana/pkg/util" @@ -682,23 +676,14 @@ const ( ) func TestGetQueryDataResponse(t *testing.T) { - sqlStore := sqlstore.InitTestDB(t) - dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore), quotatest.New(false, nil)) - require.NoError(t, err) - publicdashboardStore := database.ProvideStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures()) - serviceWrapper := ProvideServiceWrapper(publicdashboardStore) + fakeDashboardService := &dashboards.FakeDashboardService{} + service, sqlStore := newPublicDashboardServiceImpl(t, nil, fakeDashboardService, nil) fakeQueryService := &query.FakeQueryService{} fakeQueryService.On("QueryData", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&backend.QueryDataResponse{}, nil) - fakeDashboardService := &dashboards.FakeDashboardService{} + service.QueryDataService = fakeQueryService - service := &PublicDashboardServiceImpl{ - log: log.New("test.logger"), - store: publicdashboardStore, - intervalCalculator: intervalv2.NewCalculator(), - QueryDataService: fakeQueryService, - serviceWrapper: serviceWrapper, - dashboardService: fakeDashboardService, - } + dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore), quotatest.New(false, nil)) + require.NoError(t, err) publicDashboardQueryDTO := PublicDashboardQueryDTO{ IntervalMs: int64(1), @@ -748,21 +733,12 @@ func TestFindAnnotations(t *testing.T) { color := "red" name := "annoName" t.Run("will build anonymous user with correct permissions to get annotations", func(t *testing.T) { - sqlStore := sqlstore.InitTestDB(t) - config := setting.NewCfg() - tagService := tagimpl.ProvideService(sqlStore) - annotationsRepo := annotationsimpl.ProvideService(sqlStore, config, featuremgmt.WithFeatures(), tagService) - fakeStore := FakePublicDashboardStore{} + fakeStore := &FakePublicDashboardStore{} fakeStore.On("FindByAccessToken", mock.Anything, mock.AnythingOfType("string")). Return(&PublicDashboard{Uid: "uid1", IsEnabled: true}, nil) fakeDashboardService := &dashboards.FakeDashboardService{} fakeDashboardService.On("GetDashboard", mock.Anything, mock.Anything, mock.Anything).Return(dashboards.NewDashboard("dash1"), nil) - service := &PublicDashboardServiceImpl{ - log: log.New("test.logger"), - store: &fakeStore, - AnnotationsRepo: annotationsRepo, - dashboardService: fakeDashboardService, - } + service, _ := newPublicDashboardServiceImpl(t, fakeStore, fakeDashboardService, nil) reqDTO := AnnotationsQueryDTO{ From: 1, @@ -807,20 +783,15 @@ func TestFindAnnotations(t *testing.T) { annos := []DashAnnotation{grafanaAnnotation, grafanaTagAnnotation} dashboard := AddAnnotationsToDashboard(t, dash, annos) - annotationsRepo := annotations.FakeAnnotationsRepo{} pubdash := &PublicDashboard{Uid: "uid1", IsEnabled: true, OrgId: 1, DashboardUid: dashboard.UID, AnnotationsEnabled: true} - fakeStore := FakePublicDashboardStore{} + annotationsRepo := &annotations.FakeAnnotationsRepo{} + fakeStore := &FakePublicDashboardStore{} fakeStore.On("FindByAccessToken", mock.Anything, mock.AnythingOfType("string")).Return(pubdash, nil) fakeDashboardService := &dashboards.FakeDashboardService{} fakeDashboardService.On("GetDashboard", mock.Anything, mock.Anything, mock.Anything).Return(dashboard, nil) - service := &PublicDashboardServiceImpl{ - log: log.New("test.logger"), - store: &fakeStore, - AnnotationsRepo: &annotationsRepo, - dashboardService: fakeDashboardService, - } + service, _ := newPublicDashboardServiceImpl(t, fakeStore, fakeDashboardService, annotationsRepo) annotationsRepo.On("Find", mock.Anything, mock.Anything).Return([]*annotations.ItemDTO{ { @@ -870,19 +841,14 @@ func TestFindAnnotations(t *testing.T) { annos := []DashAnnotation{grafanaAnnotation} dashboard := AddAnnotationsToDashboard(t, dash, annos) - annotationsRepo := annotations.FakeAnnotationsRepo{} - fakeStore := FakePublicDashboardStore{} + annotationsRepo := &annotations.FakeAnnotationsRepo{} + fakeStore := &FakePublicDashboardStore{} pubdash := &PublicDashboard{Uid: "uid1", IsEnabled: true, OrgId: 1, DashboardUid: dashboard.UID, AnnotationsEnabled: true} fakeStore.On("FindByAccessToken", mock.Anything, mock.AnythingOfType("string")).Return(pubdash, nil) fakeDashboardService := &dashboards.FakeDashboardService{} fakeDashboardService.On("GetDashboard", mock.Anything, mock.Anything, mock.Anything).Return(dashboard, nil) - service := &PublicDashboardServiceImpl{ - log: log.New("test.logger"), - store: &fakeStore, - AnnotationsRepo: &annotationsRepo, - dashboardService: fakeDashboardService, - } + service, _ := newPublicDashboardServiceImpl(t, fakeStore, fakeDashboardService, annotationsRepo) annotationsRepo.On("Find", mock.Anything, mock.Anything).Return([]*annotations.ItemDTO{ { @@ -944,19 +910,14 @@ func TestFindAnnotations(t *testing.T) { annos := []DashAnnotation{grafanaAnnotation, queryAnnotation, disabledGrafanaAnnotation} dashboard := AddAnnotationsToDashboard(t, dash, annos) - annotationsRepo := annotations.FakeAnnotationsRepo{} + annotationsRepo := &annotations.FakeAnnotationsRepo{} pubdash := &PublicDashboard{Uid: "uid1", IsEnabled: true, OrgId: 1, DashboardUid: dashboard.UID, AnnotationsEnabled: true} - fakeStore := FakePublicDashboardStore{} + fakeStore := &FakePublicDashboardStore{} fakeStore.On("FindByAccessToken", mock.Anything, mock.AnythingOfType("string")).Return(pubdash, nil) fakeDashboardService := &dashboards.FakeDashboardService{} fakeDashboardService.On("GetDashboard", mock.Anything, mock.Anything, mock.Anything).Return(dashboard, nil) - service := &PublicDashboardServiceImpl{ - log: log.New("test.logger"), - store: &fakeStore, - AnnotationsRepo: &annotationsRepo, - dashboardService: fakeDashboardService, - } + service, _ := newPublicDashboardServiceImpl(t, fakeStore, fakeDashboardService, annotationsRepo) annotationsRepo.On("Find", mock.Anything, mock.Anything).Return([]*annotations.ItemDTO{ { @@ -990,19 +951,13 @@ func TestFindAnnotations(t *testing.T) { }) t.Run("test will return nothing when dashboard has no annotations", func(t *testing.T) { - annotationsRepo := annotations.FakeAnnotationsRepo{} dashboard := dashboards.NewDashboard("dashWithNoAnnotations") pubdash := &PublicDashboard{Uid: "uid1", IsEnabled: true, OrgId: 1, DashboardUid: dashboard.UID, AnnotationsEnabled: true} - fakeStore := FakePublicDashboardStore{} + fakeStore := &FakePublicDashboardStore{} fakeStore.On("FindByAccessToken", mock.Anything, mock.AnythingOfType("string")).Return(pubdash, nil) fakeDashboardService := &dashboards.FakeDashboardService{} fakeDashboardService.On("GetDashboard", mock.Anything, mock.Anything, mock.Anything).Return(dashboard, nil) - service := &PublicDashboardServiceImpl{ - log: log.New("test.logger"), - store: &fakeStore, - AnnotationsRepo: &annotationsRepo, - dashboardService: fakeDashboardService, - } + service, _ := newPublicDashboardServiceImpl(t, fakeStore, fakeDashboardService, nil) items, err := service.FindAnnotations(context.Background(), AnnotationsQueryDTO{}, "abc123") @@ -1028,17 +983,11 @@ func TestFindAnnotations(t *testing.T) { annos := []DashAnnotation{grafanaAnnotation} dashboard := AddAnnotationsToDashboard(t, dash, annos) pubdash := &PublicDashboard{Uid: "uid1", IsEnabled: true, OrgId: 1, DashboardUid: dashboard.UID, AnnotationsEnabled: false} - annotationsRepo := annotations.FakeAnnotationsRepo{} - fakeStore := FakePublicDashboardStore{} + fakeStore := &FakePublicDashboardStore{} fakeStore.On("FindByAccessToken", mock.Anything, mock.AnythingOfType("string")).Return(pubdash, nil) fakeDashboardService := &dashboards.FakeDashboardService{} fakeDashboardService.On("GetDashboard", mock.Anything, mock.Anything, mock.Anything).Return(dashboard, nil) - service := &PublicDashboardServiceImpl{ - log: log.New("test.logger"), - store: &fakeStore, - AnnotationsRepo: &annotationsRepo, - dashboardService: fakeDashboardService, - } + service, _ := newPublicDashboardServiceImpl(t, fakeStore, fakeDashboardService, nil) items, err := service.FindAnnotations(context.Background(), AnnotationsQueryDTO{}, "abc123") @@ -1060,23 +1009,17 @@ func TestFindAnnotations(t *testing.T) { }, } dash := dashboards.NewDashboard("test") - annotationsRepo := annotations.FakeAnnotationsRepo{} + annotationsRepo := &annotations.FakeAnnotationsRepo{} + annotationsRepo.On("Find", mock.Anything, mock.Anything).Return(nil, errors.New("failed")).Maybe() annos := []DashAnnotation{grafanaAnnotation} dash = AddAnnotationsToDashboard(t, dash, annos) pubdash := &PublicDashboard{Uid: "uid1", IsEnabled: true, OrgId: 1, DashboardUid: dash.UID, AnnotationsEnabled: true} - fakeStore := FakePublicDashboardStore{} + fakeStore := &FakePublicDashboardStore{} fakeStore.On("FindByAccessToken", mock.Anything, mock.AnythingOfType("string")).Return(pubdash, nil) fakeDashboardService := &dashboards.FakeDashboardService{} fakeDashboardService.On("GetDashboard", mock.Anything, mock.Anything, mock.Anything).Return(dash, nil) - service := &PublicDashboardServiceImpl{ - log: log.New("test.logger"), - store: &fakeStore, - AnnotationsRepo: &annotationsRepo, - dashboardService: fakeDashboardService, - } - - annotationsRepo.On("Find", mock.Anything, mock.Anything).Return(nil, errors.New("failed")).Maybe() + service, _ := newPublicDashboardServiceImpl(t, fakeStore, fakeDashboardService, annotationsRepo) items, err := service.FindAnnotations(context.Background(), AnnotationsQueryDTO{}, "abc123") @@ -1099,12 +1042,12 @@ func TestFindAnnotations(t *testing.T) { dashboard := AddAnnotationsToDashboard(t, dash, annos) pubdash := &PublicDashboard{Uid: "uid1", IsEnabled: true, OrgId: 1, DashboardUid: dashboard.UID, AnnotationsEnabled: true} - fakeStore := FakePublicDashboardStore{} + fakeStore := &FakePublicDashboardStore{} fakeStore.On("FindByAccessToken", mock.Anything, mock.AnythingOfType("string")).Return(pubdash, nil) fakeStore.On("FindByAccessToken", mock.Anything, mock.AnythingOfType("string")).Return(pubdash, nil) fakeDashboardService := &dashboards.FakeDashboardService{} fakeDashboardService.On("GetDashboard", mock.Anything, mock.Anything, mock.Anything).Return(dashboard, nil) - annotationsRepo := annotations.FakeAnnotationsRepo{} + annotationsRepo := &annotations.FakeAnnotationsRepo{} annotationsRepo.On("Find", mock.Anything, mock.Anything).Return([]*annotations.ItemDTO{ { ID: 1, @@ -1117,12 +1060,7 @@ func TestFindAnnotations(t *testing.T) { }, }, nil).Maybe() - service := &PublicDashboardServiceImpl{ - log: log.New("test.logger"), - store: &fakeStore, - AnnotationsRepo: &annotationsRepo, - dashboardService: fakeDashboardService, - } + service, _ := newPublicDashboardServiceImpl(t, fakeStore, fakeDashboardService, annotationsRepo) items, err := service.FindAnnotations(context.Background(), AnnotationsQueryDTO{}, "abc123") @@ -1145,22 +1083,17 @@ func TestFindAnnotations(t *testing.T) { } func TestGetMetricRequest(t *testing.T) { - sqlStore := db.InitTestDB(t) + service, sqlStore := newPublicDashboardServiceImpl(t, nil, nil, nil) dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore), quotatest.New(false, nil)) require.NoError(t, err) - publicdashboardStore := database.ProvideStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures()) dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, "", true, []map[string]interface{}{}, nil) + publicDashboard := &PublicDashboard{ Uid: "1", DashboardUid: dashboard.UID, IsEnabled: true, AccessToken: "abc123", } - service := &PublicDashboardServiceImpl{ - log: log.New("test.logger"), - store: publicdashboardStore, - intervalCalculator: intervalv2.NewCalculator(), - } t.Run("will return an error when validation fails", func(t *testing.T) { publicDashboardQueryDTO := PublicDashboardQueryDTO{ @@ -1230,24 +1163,16 @@ func TestGetUniqueDashboardDatasourceUids(t *testing.T) { } func TestBuildMetricRequest(t *testing.T) { - sqlStore := db.InitTestDB(t) + fakeDashboardService := &dashboards.FakeDashboardService{} + service, sqlStore := newPublicDashboardServiceImpl(t, nil, fakeDashboardService, nil) + dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore), quotatest.New(false, nil)) require.NoError(t, err) - publicdashboardStore := database.ProvideStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures()) - serviceWrapper := ProvideServiceWrapper(publicdashboardStore) publicDashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, "", true, []map[string]interface{}{}, nil) nonPublicDashboard := insertTestDashboard(t, dashboardStore, "testNonPublicDashie", 1, 0, "", true, []map[string]interface{}{}, nil) - fakeDashboardService := &dashboards.FakeDashboardService{} - fakeDashboardService.On("GetDashboard", mock.Anything, mock.Anything, mock.Anything).Return(publicDashboard, nil) from, to := internal.GetTimeRangeFromDashboard(t, publicDashboard.Data) - service := &PublicDashboardServiceImpl{ - log: log.New("test.logger"), - store: publicdashboardStore, - intervalCalculator: intervalv2.NewCalculator(), - serviceWrapper: serviceWrapper, - dashboardService: fakeDashboardService, - } + fakeDashboardService.On("GetDashboard", mock.Anything, mock.Anything, mock.Anything).Return(publicDashboard, nil) publicDashboardQueryDTO := PublicDashboardQueryDTO{ IntervalMs: int64(10000000), diff --git a/pkg/services/publicdashboards/service/service.go b/pkg/services/publicdashboards/service/service.go index eb8b3fcb0a..b0aff99150 100644 --- a/pkg/services/publicdashboards/service/service.go +++ b/pkg/services/publicdashboards/service/service.go @@ -15,6 +15,7 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/annotations" "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/licensing" "github.com/grafana/grafana/pkg/services/publicdashboards" . "github.com/grafana/grafana/pkg/services/publicdashboards/models" "github.com/grafana/grafana/pkg/services/publicdashboards/service/intervalv2" @@ -38,6 +39,7 @@ type PublicDashboardServiceImpl struct { ac accesscontrol.AccessControl serviceWrapper publicdashboards.ServiceWrapper dashboardService dashboards.DashboardService + license licensing.Licensing } var LogPrefix = "publicdashboards.service" @@ -56,6 +58,7 @@ func ProvideService( ac accesscontrol.AccessControl, serviceWrapper publicdashboards.ServiceWrapper, dashboardService dashboards.DashboardService, + license licensing.Licensing, ) *PublicDashboardServiceImpl { return &PublicDashboardServiceImpl{ log: log.New(LogPrefix), @@ -67,6 +70,7 @@ func ProvideService( ac: ac, serviceWrapper: serviceWrapper, dashboardService: dashboardService, + license: license, } } @@ -154,6 +158,10 @@ func (pd *PublicDashboardServiceImpl) FindEnabledPublicDashboardAndDashboardByAc return nil, nil, ErrPublicDashboardNotEnabled.Errorf("FindEnabledPublicDashboardAndDashboardByAccessToken: Public dashboard is not enabled accessToken: %s", accessToken) } + if !pd.license.FeatureEnabled(FeaturePublicDashboardsEmailSharing) && pubdash.Share == EmailShareType { + return nil, nil, ErrPublicDashboardNotFound.Errorf("FindEnabledPublicDashboardAndDashboardByAccessToken: Dashboard not found accessToken: %s", accessToken) + } + return pubdash, dash, err } @@ -197,6 +205,12 @@ func (pd *PublicDashboardServiceImpl) Create(ctx context.Context, u *user.Signed } if existingPubdash != nil { + // If there is no license and the public dashboard was email-shared, we should update it to public + if !pd.license.FeatureEnabled(FeaturePublicDashboardsEmailSharing) && existingPubdash.Share == EmailShareType { + dto.Uid = existingPubdash.Uid + dto.PublicDashboard.Share = PublicShareType + return pd.Update(ctx, u, dto) + } return nil, ErrDashboardIsPublic.Errorf("Create: public dashboard for dashboard %s already exists", dto.DashboardUid) } diff --git a/pkg/services/publicdashboards/service/service_test.go b/pkg/services/publicdashboards/service/service_test.go index bf982dcd5d..6216e926ad 100644 --- a/pkg/services/publicdashboards/service/service_test.go +++ b/pkg/services/publicdashboards/service/service_test.go @@ -16,14 +16,11 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" "github.com/grafana/grafana/pkg/services/dashboards" dashboardsDB "github.com/grafana/grafana/pkg/services/dashboards/database" "github.com/grafana/grafana/pkg/services/featuremgmt" . "github.com/grafana/grafana/pkg/services/publicdashboards" - "github.com/grafana/grafana/pkg/services/publicdashboards/database" . "github.com/grafana/grafana/pkg/services/publicdashboards/models" "github.com/grafana/grafana/pkg/services/publicdashboards/service/intervalv2" "github.com/grafana/grafana/pkg/services/publicdashboards/validation" @@ -386,16 +383,11 @@ func TestGetPublicDashboardForView(t *testing.T) { for _, test := range testCases { t.Run(test.Name, func(t *testing.T) { - fakeStore := FakePublicDashboardStore{} - fakeDashboardService := &dashboards.FakeDashboardService{} - service := &PublicDashboardServiceImpl{ - log: log.New("test.logger"), - store: &fakeStore, - dashboardService: fakeDashboardService, - } - + fakeStore := &FakePublicDashboardStore{} fakeStore.On("FindByAccessToken", mock.Anything, mock.Anything).Return(test.StoreResp.pd, test.StoreResp.err) + fakeDashboardService := &dashboards.FakeDashboardService{} fakeDashboardService.On("GetDashboard", mock.Anything, mock.Anything, mock.Anything).Return(test.StoreResp.d, test.StoreResp.err) + service, _ := newPublicDashboardServiceImpl(t, fakeStore, fakeDashboardService, nil) dashboardFullWithMeta, err := service.GetPublicDashboardForView(context.Background(), test.AccessToken) if test.ErrResp != nil { @@ -501,15 +493,10 @@ func TestGetPublicDashboard(t *testing.T) { for _, test := range testCases { t.Run(test.Name, func(t *testing.T) { fakeDashboardService := &dashboards.FakeDashboardService{} - fakeStore := FakePublicDashboardStore{} - service := &PublicDashboardServiceImpl{ - log: log.New("test.logger"), - store: &fakeStore, - dashboardService: fakeDashboardService, - } - - fakeStore.On("FindByAccessToken", mock.Anything, mock.Anything).Return(test.StoreResp.pd, test.StoreResp.err) fakeDashboardService.On("GetDashboard", mock.Anything, mock.Anything, mock.Anything).Return(test.StoreResp.d, test.StoreResp.err) + fakeStore := &FakePublicDashboardStore{} + fakeStore.On("FindByAccessToken", mock.Anything, mock.Anything).Return(test.StoreResp.pd, test.StoreResp.err) + service, _ := newPublicDashboardServiceImpl(t, fakeStore, fakeDashboardService, nil) pdc, dash, err := service.FindPublicDashboardAndDashboardByAccessToken(context.Background(), test.AccessToken) if test.ErrResp != nil { @@ -568,16 +555,11 @@ func TestGetEnabledPublicDashboard(t *testing.T) { for _, test := range testCases { t.Run(test.Name, func(t *testing.T) { - fakeStore := FakePublicDashboardStore{} - fakeDashboardService := &dashboards.FakeDashboardService{} - service := &PublicDashboardServiceImpl{ - log: log.New("test.logger"), - store: &fakeStore, - dashboardService: fakeDashboardService, - } - + fakeStore := &FakePublicDashboardStore{} fakeStore.On("FindByAccessToken", mock.Anything, mock.Anything).Return(test.StoreResp.pd, test.StoreResp.err) + fakeDashboardService := &dashboards.FakeDashboardService{} fakeDashboardService.On("GetDashboard", mock.Anything, mock.Anything, mock.Anything).Return(test.StoreResp.d, test.StoreResp.err) + service, _ := newPublicDashboardServiceImpl(t, fakeStore, fakeDashboardService, nil) pdc, dash, err := service.FindEnabledPublicDashboardAndDashboardByAccessToken(context.Background(), test.AccessToken) if test.ErrResp != nil { @@ -600,24 +582,15 @@ func TestGetEnabledPublicDashboard(t *testing.T) { // the correct order is convoluted. func TestCreatePublicDashboard(t *testing.T) { t.Run("Create public dashboard", func(t *testing.T) { - sqlStore := db.InitTestDB(t) + fakeDashboardService := &dashboards.FakeDashboardService{} + service, sqlStore := newPublicDashboardServiceImpl(t, nil, fakeDashboardService, nil) + quotaService := quotatest.New(false, nil) dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore), quotaService) require.NoError(t, err) - publicdashboardStore := database.ProvideStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures()) dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, "", true, []map[string]any{}, nil) - serviceWrapper := ProvideServiceWrapper(publicdashboardStore) - - fakeDashboardService := &dashboards.FakeDashboardService{} fakeDashboardService.On("GetDashboard", mock.Anything, mock.Anything, mock.Anything).Return(dashboard, nil) - service := &PublicDashboardServiceImpl{ - log: log.New("test.logger"), - store: publicdashboardStore, - serviceWrapper: serviceWrapper, - dashboardService: fakeDashboardService, - } - isEnabled, annotationsEnabled, timeSelectionEnabled := true, false, true dto := &SavePublicDashboardDTO{ @@ -690,23 +663,14 @@ func TestCreatePublicDashboard(t *testing.T) { for _, tt := range testCases { t.Run(fmt.Sprintf("Create public dashboard with %s null boolean fields stores them as false", tt.Name), func(t *testing.T) { - sqlStore := db.InitTestDB(t) + fakeDashboardService := &dashboards.FakeDashboardService{} + service, sqlStore := newPublicDashboardServiceImpl(t, nil, fakeDashboardService, nil) quotaService := quotatest.New(false, nil) dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore), quotaService) require.NoError(t, err) - publicdashboardStore := database.ProvideStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures()) dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, "", true, []map[string]any{}, nil) - serviceWrapper := ProvideServiceWrapper(publicdashboardStore) - fakeDashboardService := &dashboards.FakeDashboardService{} fakeDashboardService.On("GetDashboard", mock.Anything, mock.Anything, mock.Anything).Return(dashboard, nil) - service := &PublicDashboardServiceImpl{ - log: log.New("test.logger"), - store: publicdashboardStore, - serviceWrapper: serviceWrapper, - dashboardService: fakeDashboardService, - } - dto := &SavePublicDashboardDTO{ DashboardUid: dashboard.UID, UserId: 7, @@ -731,24 +695,14 @@ func TestCreatePublicDashboard(t *testing.T) { } t.Run("Validate pubdash has default time setting value", func(t *testing.T) { - sqlStore := db.InitTestDB(t) + fakeDashboardService := &dashboards.FakeDashboardService{} + service, sqlStore := newPublicDashboardServiceImpl(t, nil, fakeDashboardService, nil) quotaService := quotatest.New(false, nil) dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore), quotaService) require.NoError(t, err) - publicdashboardStore := database.ProvideStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures()) dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, "", true, []map[string]any{}, nil) - serviceWrapper := ProvideServiceWrapper(publicdashboardStore) - - fakeDashboardService := &dashboards.FakeDashboardService{} fakeDashboardService.On("GetDashboard", mock.Anything, mock.Anything, mock.Anything).Return(dashboard, nil) - service := &PublicDashboardServiceImpl{ - log: log.New("test.logger"), - store: publicdashboardStore, - serviceWrapper: serviceWrapper, - dashboardService: fakeDashboardService, - } - isEnabled := true dto := &SavePublicDashboardDTO{ DashboardUid: dashboard.UID, @@ -768,24 +722,16 @@ func TestCreatePublicDashboard(t *testing.T) { }) t.Run("Creates pubdash whose dashboard has template variables successfully", func(t *testing.T) { - sqlStore := db.InitTestDB(t) + fakeDashboardService := &dashboards.FakeDashboardService{} + service, sqlStore := newPublicDashboardServiceImpl(t, nil, fakeDashboardService, nil) quotaService := quotatest.New(false, nil) dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore), quotaService) require.NoError(t, err) - publicdashboardStore := database.ProvideStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures()) + templateVars := make([]map[string]any, 1) dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, "", true, templateVars, nil) - serviceWrapper := ProvideServiceWrapper(publicdashboardStore) - fakeDashboardService := &dashboards.FakeDashboardService{} fakeDashboardService.On("GetDashboard", mock.Anything, mock.Anything, mock.Anything).Return(dashboard, nil) - service := &PublicDashboardServiceImpl{ - log: log.New("test.logger"), - store: publicdashboardStore, - serviceWrapper: serviceWrapper, - dashboardService: fakeDashboardService, - } - isEnabled := true dto := &SavePublicDashboardDTO{ DashboardUid: dashboard.UID, @@ -823,14 +769,7 @@ func TestCreatePublicDashboard(t *testing.T) { fakeDashboardService := &dashboards.FakeDashboardService{} fakeDashboardService.On("GetDashboard", mock.Anything, mock.Anything, mock.Anything).Return(dashboard, nil) - serviceWrapper := ProvideServiceWrapper(publicDashboardStore) - - service := &PublicDashboardServiceImpl{ - log: log.New("test.logger"), - store: publicDashboardStore, - serviceWrapper: serviceWrapper, - dashboardService: fakeDashboardService, - } + service, _ := newPublicDashboardServiceImpl(t, publicDashboardStore, fakeDashboardService, nil) isEnabled := true dto := &SavePublicDashboardDTO{ @@ -849,23 +788,14 @@ func TestCreatePublicDashboard(t *testing.T) { }) t.Run("Create public dashboard with given pubdash uid", func(t *testing.T) { - sqlStore := db.InitTestDB(t) + fakeDashboardService := &dashboards.FakeDashboardService{} + service, sqlStore := newPublicDashboardServiceImpl(t, nil, fakeDashboardService, nil) quotaService := quotatest.New(false, nil) dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore), quotaService) require.NoError(t, err) - publicdashboardStore := database.ProvideStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures()) dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, "", true, []map[string]any{}, nil) - serviceWrapper := ProvideServiceWrapper(publicdashboardStore) - fakeDashboardService := &dashboards.FakeDashboardService{} fakeDashboardService.On("GetDashboard", mock.Anything, mock.Anything, mock.Anything).Return(dashboard, nil) - service := &PublicDashboardServiceImpl{ - log: log.New("test.logger"), - store: publicdashboardStore, - serviceWrapper: serviceWrapper, - dashboardService: fakeDashboardService, - } - isEnabled := true dto := &SavePublicDashboardDTO{ @@ -905,14 +835,7 @@ func TestCreatePublicDashboard(t *testing.T) { publicDashboardStore.On("FindByDashboardUid", mock.Anything, mock.Anything, mock.Anything).Return(nil, ErrPublicDashboardNotFound.Errorf("")) fakeDashboardService := &dashboards.FakeDashboardService{} fakeDashboardService.On("GetDashboard", mock.Anything, mock.Anything, mock.Anything).Return(dashboard, nil) - serviceWrapper := ProvideServiceWrapper(publicDashboardStore) - - service := &PublicDashboardServiceImpl{ - log: log.New("test.logger"), - store: publicDashboardStore, - serviceWrapper: serviceWrapper, - dashboardService: fakeDashboardService, - } + service, _ := newPublicDashboardServiceImpl(t, publicDashboardStore, fakeDashboardService, nil) isEnabled := true dto := &SavePublicDashboardDTO{ @@ -931,23 +854,14 @@ func TestCreatePublicDashboard(t *testing.T) { }) t.Run("Create public dashboard with given pubdash access token", func(t *testing.T) { - sqlStore := db.InitTestDB(t) + fakeDashboardService := &dashboards.FakeDashboardService{} + service, sqlStore := newPublicDashboardServiceImpl(t, nil, fakeDashboardService, nil) quotaService := quotatest.New(false, nil) dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore), quotaService) require.NoError(t, err) - publicdashboardStore := database.ProvideStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures()) dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, "", true, []map[string]interface{}{}, nil) - serviceWrapper := ProvideServiceWrapper(publicdashboardStore) - fakeDashboardService := &dashboards.FakeDashboardService{} fakeDashboardService.On("GetDashboard", mock.Anything, mock.Anything, mock.Anything).Return(dashboard, nil) - service := &PublicDashboardServiceImpl{ - log: log.New("test.logger"), - store: publicdashboardStore, - serviceWrapper: serviceWrapper, - dashboardService: fakeDashboardService, - } - isEnabled := true dto := &SavePublicDashboardDTO{ @@ -981,14 +895,7 @@ func TestCreatePublicDashboard(t *testing.T) { publicDashboardStore := &FakePublicDashboardStore{} publicDashboardStore.On("FindByAccessToken", mock.Anything, mock.Anything).Return(pubdash, nil) - - serviceWrapper := ProvideServiceWrapper(publicDashboardStore) - - service := &PublicDashboardServiceImpl{ - log: log.New("test.logger"), - store: publicDashboardStore, - serviceWrapper: serviceWrapper, - } + service, _ := newPublicDashboardServiceImpl(t, publicDashboardStore, nil, nil) _, err := service.NewPublicDashboardAccessToken(context.Background()) require.Error(t, err) @@ -996,25 +903,16 @@ func TestCreatePublicDashboard(t *testing.T) { }) t.Run("Returns error if public dashboard exists", func(t *testing.T) { - sqlStore := db.InitTestDB(t) - dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore), quotatest.New(false, nil)) - require.NoError(t, err) - - dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, "", true, []map[string]any{}, nil) - publicdashboardStore := &FakePublicDashboardStore{} publicdashboardStore.On("FindByDashboardUid", mock.Anything, mock.Anything, mock.Anything).Return(&PublicDashboard{Uid: "newPubdashUid"}, nil) publicdashboardStore.On("Find", mock.Anything, mock.Anything).Return(nil, nil) fakeDashboardService := &dashboards.FakeDashboardService{} - fakeDashboardService.On("GetDashboard", mock.Anything, mock.Anything, mock.Anything).Return(dashboard, nil) - serviceWrapper := ProvideServiceWrapper(publicdashboardStore) + service, sqlStore := newPublicDashboardServiceImpl(t, publicdashboardStore, fakeDashboardService, nil) - service := &PublicDashboardServiceImpl{ - log: log.New("test.logger"), - store: publicdashboardStore, - serviceWrapper: serviceWrapper, - dashboardService: fakeDashboardService, - } + dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore), quotatest.New(false, nil)) + require.NoError(t, err) + dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, "", true, []map[string]any{}, nil) + fakeDashboardService.On("GetDashboard", mock.Anything, mock.Anything, mock.Anything).Return(dashboard, nil) isEnabled, annotationsEnabled := true, false dto := &SavePublicDashboardDTO{ @@ -1033,23 +931,15 @@ func TestCreatePublicDashboard(t *testing.T) { }) t.Run("Validate pubdash has default share value", func(t *testing.T) { - sqlStore := db.InitTestDB(t) + fakeDashboardService := &dashboards.FakeDashboardService{} + service, sqlStore := newPublicDashboardServiceImpl(t, nil, fakeDashboardService, nil) + quotaService := quotatest.New(false, nil) dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore), quotaService) require.NoError(t, err) - publicdashboardStore := database.ProvideStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures()) dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, "", true, []map[string]any{}, nil) - serviceWrapper := ProvideServiceWrapper(publicdashboardStore) - fakeDashboardService := &dashboards.FakeDashboardService{} fakeDashboardService.On("GetDashboard", mock.Anything, mock.Anything, mock.Anything).Return(dashboard, nil) - service := &PublicDashboardServiceImpl{ - log: log.New("test.logger"), - store: publicdashboardStore, - serviceWrapper: serviceWrapper, - dashboardService: fakeDashboardService, - } - isEnabled := true dto := &SavePublicDashboardDTO{ DashboardUid: dashboard.UID, @@ -1079,22 +969,15 @@ func assertFalseIfNull(t *testing.T, expectedValue bool, nullableValue *bool) { } func TestUpdatePublicDashboard(t *testing.T) { - sqlStore := db.InitTestDB(t) + fakeDashboardService := &dashboards.FakeDashboardService{} + service, sqlStore := newPublicDashboardServiceImpl(t, nil, fakeDashboardService, nil) + quotaService := quotatest.New(false, nil) dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore), quotaService) require.NoError(t, err) - publicdashboardStore := database.ProvideStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures()) - serviceWrapper := ProvideServiceWrapper(publicdashboardStore) dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, "", true, []map[string]any{}, nil) dashboard2 := insertTestDashboard(t, dashboardStore, "testDashie2", 1, 0, "", true, []map[string]any{}, nil) - fakeDashboardService := &dashboards.FakeDashboardService{} fakeDashboardService.On("GetDashboard", mock.Anything, mock.Anything, mock.Anything).Return(dashboard, nil) - service := &PublicDashboardServiceImpl{ - log: log.New("test.logger"), - store: publicdashboardStore, - serviceWrapper: serviceWrapper, - dashboardService: fakeDashboardService, - } t.Run("Updating public dashboard", func(t *testing.T) { isEnabled, annotationsEnabled, timeSelectionEnabled := true, false, false @@ -1269,23 +1152,15 @@ func TestUpdatePublicDashboard(t *testing.T) { for _, tt := range testCases { t.Run(fmt.Sprintf("Update public dashboard with %s null boolean fields let those fields with old persisted value", tt.Name), func(t *testing.T) { - sqlStore := db.InitTestDB(t) + fakeDashboardService := &dashboards.FakeDashboardService{} + service, sqlStore := newPublicDashboardServiceImpl(t, nil, fakeDashboardService, nil) + quotaService := quotatest.New(false, nil) dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore), quotaService) require.NoError(t, err) - publicdashboardStore := database.ProvideStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures()) - serviceWrapper := ProvideServiceWrapper(publicdashboardStore) dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, "", true, []map[string]any{}, nil) - fakeDashboardService := &dashboards.FakeDashboardService{} fakeDashboardService.On("GetDashboard", mock.Anything, mock.Anything, mock.Anything).Return(dashboard, nil) - service := &PublicDashboardServiceImpl{ - log: log.New("test.logger"), - store: publicdashboardStore, - serviceWrapper: serviceWrapper, - dashboardService: fakeDashboardService, - } - isEnabled, annotationsEnabled, timeSelectionEnabled := true, true, false dto := &SavePublicDashboardDTO{ @@ -1398,15 +1273,7 @@ func TestDeletePublicDashboard(t *testing.T) { if tt.ExpectedErrResp == nil || tt.mockDeleteStore.StoreRespErr != nil { store.On("Delete", mock.Anything, mock.Anything).Return(tt.mockDeleteStore.AffectedRowsResp, tt.mockDeleteStore.StoreRespErr) } - serviceWrapper := &PublicDashboardServiceWrapperImpl{ - log: log.New("test.logger"), - store: store, - } - service := &PublicDashboardServiceImpl{ - log: log.New("test.logger"), - store: store, - serviceWrapper: serviceWrapper, - } + service, _ := newPublicDashboardServiceImpl(t, store, nil, nil) err := service.Delete(context.Background(), "pubdashUID", "uid") if tt.ExpectedErrResp != nil { @@ -1623,12 +1490,8 @@ func TestPublicDashboardServiceImpl_ListPublicDashboards(t *testing.T) { store := NewFakePublicDashboardStore(t) store.On("FindAllWithPagination", mock.Anything, mock.Anything). Return(tt.mockResponse.PublicDashboardListResponseWithPagination, tt.mockResponse.Err) - - pd := &PublicDashboardServiceImpl{ - log: log.New("test.logger"), - store: store, - ac: ac, - } + pd, _ := newPublicDashboardServiceImpl(t, store, nil, nil) + pd.ac = ac got, err := pd.FindAllWithPagination(tt.args.ctx, tt.args.query) if !tt.wantErr(t, err, fmt.Sprintf("FindAllWithPagination(%v, %v)", tt.args.ctx, tt.args.query)) { From 9282c7a7a409e19aa0295980fe97eba556143644 Mon Sep 17 00:00:00 2001 From: Klesh Wong Date: Thu, 22 Feb 2024 17:02:31 +0800 Subject: [PATCH 0084/1406] AuthProxy: Invalidate previous cached item for user when changes are made to any header (#81445) * fix: sign in using auth_proxy with role a -> b -> a would end up with role b * Update pkg/services/authn/clients/proxy.go Co-authored-by: Karl Persson * Update pkg/services/authn/clients/proxy.go Co-authored-by: Karl Persson --- pkg/services/authn/clients/proxy.go | 24 ++++++++- pkg/services/authn/clients/proxy_test.go | 64 +++++++++++++++++++++--- 2 files changed, 80 insertions(+), 8 deletions(-) diff --git a/pkg/services/authn/clients/proxy.go b/pkg/services/authn/clients/proxy.go index fd19789274..46c9ee1b04 100644 --- a/pkg/services/authn/clients/proxy.go +++ b/pkg/services/authn/clients/proxy.go @@ -54,6 +54,7 @@ func ProvideProxy(cfg *setting.Cfg, cache proxyCache, userSrv user.Service, clie type proxyCache interface { Get(ctx context.Context, key string) ([]byte, error) Set(ctx context.Context, key string, value []byte, expire time.Duration) error + Delete(ctx context.Context, key string) error } type Proxy struct { @@ -146,13 +147,32 @@ func (c *Proxy) Hook(ctx context.Context, identity *authn.Identity, r *authn.Req c.log.Warn("Failed to cache proxy user", "error", err, "userId", identifier, "err", err) return nil } + + // User's role would not be updated if the cache hit. If requests arrive in the following order: + // 1. Name = x; Role = Admin # cache missed, new user created and cached with key Name=x;Role=Admin + // 2. Name = x; Role = Editor # cache missed, the user got updated and cached with key Name=x;Role=Editor + // 3. Name = x; Role = Admin # cache hit with key Name=x;Role=Admin, no update, the user stays with Role=Editor + // To avoid such a problem we also cache the key used using `prefix:[username]`. + // Then whenever we get a cache miss due to changes in any header we use it to invalidate the previous item. + username := getProxyHeader(r, c.cfg.AuthProxyHeaderName, c.cfg.AuthProxyHeadersEncoded) + userKey := fmt.Sprintf("%s:%s", proxyCachePrefix, username) + + // invalidate previously cached user id + if prevCacheKey, err := c.cache.Get(ctx, userKey); err == nil && len(prevCacheKey) > 0 { + if err := c.cache.Delete(ctx, string(prevCacheKey)); err != nil { + return err + } + } + c.log.FromContext(ctx).Debug("Cache proxy user", "userId", id) bytes := []byte(strconv.FormatInt(id, 10)) - if err := c.cache.Set(ctx, identity.ClientParams.CacheAuthProxyKey, bytes, time.Duration(c.cfg.AuthProxySyncTTL)*time.Minute); err != nil { + duration := time.Duration(c.cfg.AuthProxySyncTTL) * time.Minute + if err := c.cache.Set(ctx, identity.ClientParams.CacheAuthProxyKey, bytes, duration); err != nil { c.log.Warn("Failed to cache proxy user", "error", err, "userId", id) } - return nil + // store current cacheKey for the user + return c.cache.Set(ctx, userKey, []byte(identity.ClientParams.CacheAuthProxyKey), duration) } func (c *Proxy) isAllowedIP(r *authn.Request) bool { diff --git a/pkg/services/authn/clients/proxy_test.go b/pkg/services/authn/clients/proxy_test.go index a0b19dc1a0..408f5b3200 100644 --- a/pkg/services/authn/clients/proxy_test.go +++ b/pkg/services/authn/clients/proxy_test.go @@ -3,6 +3,7 @@ package clients import ( "context" "errors" + "fmt" "net/http" "testing" "time" @@ -112,7 +113,7 @@ func TestProxy_Authenticate(t *testing.T) { calledAdditional = additional return nil, nil }} - c, err := ProvideProxy(cfg, fakeCache{expectedErr: errors.New("")}, usertest.NewUserServiceFake(), proxyClient) + c, err := ProvideProxy(cfg, &fakeCache{expectedErr: errors.New("")}, usertest.NewUserServiceFake(), proxyClient) require.NoError(t, err) _, err = c.Authenticate(context.Background(), tt.req) @@ -177,14 +178,65 @@ func TestProxy_Test(t *testing.T) { var _ proxyCache = new(fakeCache) type fakeCache struct { - expectedErr error - expectedItem []byte + data map[string][]byte + expectedErr error } -func (f fakeCache) Get(ctx context.Context, key string) ([]byte, error) { - return f.expectedItem, f.expectedErr +func (f *fakeCache) Get(ctx context.Context, key string) ([]byte, error) { + return f.data[key], f.expectedErr } -func (f fakeCache) Set(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 { + f.data[key] = value return f.expectedErr } + +func (f fakeCache) Delete(ctx context.Context, key string) error { + delete(f.data, key) + return f.expectedErr +} + +func TestProxy_Hook(t *testing.T) { + cfg := setting.NewCfg() + cfg.AuthProxyHeaderName = "X-Username" + cfg.AuthProxyHeaders = map[string]string{ + proxyFieldRole: "X-Role", + } + cache := &fakeCache{data: make(map[string][]byte)} + userId := 1 + userID := fmt.Sprintf("%s:%d", authn.NamespaceUser, userId) + + // withRole creates a test case for a user with a specific role. + withRole := func(role string) func(t *testing.T) { + cacheKey := fmt.Sprintf("users:johndoe-%s", role) + return func(t *testing.T) { + c, err := ProvideProxy(cfg, cache, usertest.NewUserServiceFake(), authntest.MockProxyClient{}) + require.NoError(t, err) + userIdentity := &authn.Identity{ + ID: userID, + ClientParams: authn.ClientParams{ + CacheAuthProxyKey: cacheKey, + }, + } + userReq := &authn.Request{ + HTTPRequest: &http.Request{ + Header: map[string][]string{ + "X-Username": {"johndoe"}, + "X-Role": {role}, + }, + }, + } + err = c.Hook(context.Background(), userIdentity, userReq) + assert.NoError(t, err) + expectedCache := map[string][]byte{ + cacheKey: []byte(fmt.Sprintf("%d", userId)), + fmt.Sprintf("%s:%s", proxyCachePrefix, "johndoe"): []byte(fmt.Sprintf("users:johndoe-%s", role)), + } + assert.Equal(t, expectedCache, cache.data) + } + } + + t.Run("step 1: new user with role Admin", withRole("Admin")) + t.Run("step 2: cached user with new Role Viewer", withRole("Viewer")) + t.Run("step 3: cached user get changed back to Admin", withRole("Admin")) +} From 3dea5e30c38aa0900197d402852fa590fe3f4507 Mon Sep 17 00:00:00 2001 From: Fabrizio <135109076+fabrizio-grafana@users.noreply.github.com> Date: Thu, 22 Feb 2024 10:04:58 +0100 Subject: [PATCH 0085/1406] Yarn: Clean up PnP fragments (#83138) --- .gitignore | 3 --- ...tether-drop-https-3382d2649f-178c3afb88.zip | Bin 317732 -> 0 bytes 2 files changed, 3 deletions(-) delete mode 100644 .yarn/cache/tether-drop-https-3382d2649f-178c3afb88.zip diff --git a/.gitignore b/.gitignore index 771a993d2a..8127687253 100644 --- a/.gitignore +++ b/.gitignore @@ -24,9 +24,6 @@ __debug_bin* !.yarn/plugins !.yarn/sdks !.yarn/versions -# we temporarily commit this file because yarn downloading it -# somehow produces different checksum values -!.yarn/cache/pa11y-ci-https-1e9675e9e1-668c9119bd.zip .pnp.* # Enterprise emails diff --git a/.yarn/cache/tether-drop-https-3382d2649f-178c3afb88.zip b/.yarn/cache/tether-drop-https-3382d2649f-178c3afb88.zip deleted file mode 100644 index be3f3c66ca98c5ee035086c783e83040ec05ae57..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 317732 zcmb@t1C(vek}g`dZQI5!+qP}nwr$(4UAt`Cw!LeYUAO+zr|)}z_ju>t?tW{Gxz-$O zWoFD3nP0>gk&*IJz#u39f4%r?mm&Uf^UpWf?@wDhV-r0aJ7X7X6DNB4|IenF|FNmF ziL<$hBdxKco&7(%0Rce#LjvRCvd+eL^AqlD>p1w@MIMNR``H#Bqyws|+4SD<%)0~GG3b?8~oe*SGu zyI)u_>kR=p10~fHkR(x+ziE9#5zS#w za4$GM+Zczb<4%q_SFoE$mXbxhx_Je&$L#HiJ8!^MBO85UjdC;LK`U^$M;_z~X=Ru{ zhv0c|H}Jz-%4RXZtm#FzlLn2*7oyS>sFVDflBhn6cBr^3Xzq<^1E5G%fuL;kXXbp^ zK;bhXX-_gxRn9dFrMewkygNg(CsD4l>YHSz-y9+Ad^x$)4M&}-oEVRjIN!vrhErZv z;X_O3*c6#NoyvUy%vyS*CNBP^s-atrUf&!w@Wms&e(IkeSK@@)5$4UcpDXQ|>Uucw z`{<>u#GbD_Qz8zovo&3?E|T(Gpl)rLp8|FbW=O^e7I*eutotJlp2>AT-HB5R$4N3ED**xk zVEB!HihntKaxiB@{K9YBJhc-hu4!&=@~qQ?7rfCDx7Tw5@~C!){|W`&8|9P zz)nu4lZ-kqp;2M(``q1UT3Ew)a(Zv9;P1M}wk*RqxO1fBuqa8e5!gnu(63WrVv@8A z81KLs zdvQ@&Rd;Ua_bdsP>vP^2xqz$3dWi|eRKTSuJY zMD?ULc1l};W%=uvgy9B3*t@uK_m;PJbQ~huLOFKPZARk>1~4#c4`?l}IZ|wll%?W1 zUV}eY31e%U#$F6W0j|+%w?7V)(dEa(ofF;nE^q*YmOG3CxO6q_ld3_xbRedC5A|Va zH$%BALv6+l1iMwY6ITHgyXehx1#lYw{8_6p2E`Gl_g;BKG`#4ZbEvm)0 zPAV4%)s77O%d&1+!&Cb75|6dVIf#Ol zC^2~UZ$$6p{%j)q1{!lE>qvCx_a-{oG77cM?F~P76cI3(Ffl!ptg};#hK$&5IBR^? z-?Cll3Dbr9#gE-EX83ReV;)2xnTFlTAqioAvLvJw=S@Eg6msyGEG0Zejyy-F61^DjTH)9T+s8~L|eO-LK81wSxuAGoBv2Q9`f3920cBiRE zcc1tW*5%$%hP`b~CJHW4yuq5`FSs7gkVb0EkK&K1R1)VYZ^~6h+;<6&8xglr<*k2F zQ?e9HQ@;G1VxfYkQYsuPmqWuaaqodo&yA6;56)<@ULbN~3iYx+=M)85rZI}WO4|Z> z$)!Ubgd&3%}_#JK~;KKk6<( zFPUdq_ghIBBgEwmIDGHARg^tw=?#X~_PLHm4H8bf{oboq-!Re8nq#>yhKXiqP{?

bjo-;RXn+awYED{~|*AtWNJ zB%-4z6}QfS(0xZOn%$zVj>u{J}f+J)YUWHJ!O1bX0gE`>pMolP^oAH}f z=$wu|iRS?jZ(Z)@YGUPg{7l&Ez95jHH9mUOxU|_tmkY5vd{@Tg9;mDmGuQ&DeRedU zTX2hAX_5p1hxssiYx)>*FRq}G7*}q709uMU)4lk?7Nd67*seWz%v0@8*js^<^%v{m zS&(_I^*(oZh^_z=A2Av}k%Va$;8n!(r}e-p*sd&K>EW=kmcfKwd9F!Z4|f~*N9*5E zAaQ?q5~lHx<7YsoYn#MgGchih@WC$LKz>LqhH>i8DK)SX7ch*Nu?MgZ@WY__j8xlQ z@55ARn-}bL7nXZnZM_&FY0o+2iYBulE^umVD8JjR^P^(%wHw&0MJ~5DG$oIIsnf8u zWCpz%8K!Qyh@sD(PXXcX-nuS%O+3MR@}w-OIDBkBTWmIqd>jliD0F7oaY_IBo*LGg;IBg05x1j z1tOMJp>&81`3SihK}`}iWECQ&SQ7|xy8@&x!g@d@^B}24nDCxA;O8u_-{hGobMh!WKYd^<9$ zoIj<)g5a_w5l;;=B2ywk?IAmwCaZTVMC`1W)*U-NrWvT?5{NI~d#n08=-<~U9LkxV z#P=FS{$8X1lEx~E2nfrF{N*f_Wb76h5W3#fU^MmF%P;sz;ZPj>w}2`nqgHD?!z&jh z$Re~x;67Io2tT=88)>?fd4zAxw2Z8>Ett{`U5$;WNH)&6)ck7g>SBfUfa3+?mGOscff*3u^}i!n0)EEe)7?GO z#Z)DR+}je+A)1XPitQK$+5;B99HCZ!Cj&@5!@7b@uC%9>l=AzrH~W{1T2hiNS)$oWj1kpJ-9z$$ZRZ()yrw0@&85 z`jeo((MZyxfL^E&p|dqFiEpl1|MicTqyE_Co-zKfxsLSO1{JyWtB%apywo+Vo?9u) z+zxxQU~OXei9_-kB zhFL<$fJs!|+LcyfPJBe3(H&grE8acYwvc$LOe_x0)-XI9d-^|ksE!4_eU2*8v-AORX0Y(9F$>|YHK2GP6AiANbzurHuURC%Rl-WIsJQF{_jyFr%YfX^tV;2p#cC8`ajD4tN&k7I_-D7Zev1g;OJ=Q z=0s~~=VEJQLThZ`Xhrwk;z(03DT@HXZ>CIt{cQ3l5A*T*J>d(PkI+zXL}XC$Q<>9F z7X$`tAmV_?Z_lJKqmZLtZyP!=Sth~~*99OY6h=?_YdE(0lL$QMU=(C1ITC%pV);8V zqmDdp+yMFcb31{0cCCNyeJwyJ#6`92x-^*AQIVe%_rfGu1MADjACAtZN)c-xLV9oe zM;n)GL#|_w1CA+>C9|M`KtjTAbshR&z4QhpCyoRf{V*lGB@%}hlz__X6vJuw0IT-? znox~+g!ug^Jti$49eH#RZ(PlUq0C#r&_7}by!^7DI%=gRpUfZnML-$%Vo4uLDckD9 zu|XV};f>ULQ(xrWVGhju)=ghRC;@R4$Pt@jh#?XfgDQzl#VHm(kxFh6$gI54q3zVh z@C=odXZC{#7-TYYH3gj|PPVA9GM-(bQ z*`~e%6rwlI6VsG2ODFV*GLT=3GjF?t3h&)9MviVU~-Wmy5Q4$7X@QWGD zJfkkuj54+mKa$yd)G4%v|LF$3fnfJrl^JMhT1t zFbA0*JM3N@EI1Mms(8t+7vP|Gw*E)47_amuAtlt@p-3 zL`5D}SiJT$s)fyzXiiJeP_3N;4j|72j46`|7B>o`i9r&J1GwrxCHkRb%(vM&Bz~I) zxc%`-G>zAeo^cc$`o|G17$fPGaF})+g*$GaE0)v=&pb|=p2}shhYW+4Z7h_uKO&`* zlJ%u0Er5l?s*PB0()ged@PKhV0X_S|w_l#}-)ZoDYkp5$pR^xEwICYZn{(?0GYD)J zm_liiV3fv9DC^vH{@U=>&>1+Qh>Fg6L0lh-gUgI}A%VUZ9X~HC^Vf=@!-eH;N4O;P zT9RrBy-vYy&v_`~O7#d{q0Wxbh2D)H=x8hs_+7Y~5%|(yNa@w}0oL+E%tzGYVU0y& zV$oW1^1cisBO6vTXpIdZn*UI_3@`$=@WJuRU=}$nY zO}C%kzFYWMOvbLuANapJA8{IEB7a~207n7<0I~lcY>kbD?O*n0OJl<^ivi&Sl`LbL zq{iT|`5O3LVh4n#Wf+4|(01Sb_V*H~bv-Fex<0#|hJ}FFbS~;e3_5dWM|?jR6Yt%5 z>fOiSqeFi{4qa%xJlK>kjI<+b3bx)yiXog|?yQr(mK}E);v9}x@NXPDU69nHTRZ%RfZfxp za8qq2b;SvL>6Vb5(ValxG8kd+Lwb;i8yDnG!n0}eH7JZdf$*Y-#E6yQPhvreaC6Cw zuXGZL_ydkqjjRDtKB`8D&&?!PNhS`4W7@$;z*o$u*rz7@LK|@+_Fy-lOUS9&Nw1f# z^oTT?+zSvFzf*1vkzgf+7kg_BW-gero_S9bUTzRr8?Y0gm2)8h2_mR*m~)WwR>#S~ ziOLwoT6&_NN0NPRl+XJ#@b5!PXl5bm|pJ?NQST1B1ya{35A=#jS28+KGv zSqKz%+y)XE_PZP`vuq7p?D_Ell&C4jt8&1jZ1C8*$cxJ^KGnN#Hztcab3$Iuc+mt4 zLf=!!mKjKc$IHFgVYy!+#2npioZMHTGxc)3lI=R5wHm5-CpA#_f;v#wKqekKeeRuk zqF6wlIJUDN8_w;SsOaR&G2a1JwPf=;rbrFLBUW#>r5~(RQuqklEXYx`lHinRYr7EV z`uM=9$;BYjjX7y>5vzH$khkTkKT5{QlDR8-k={|HxZViBb-ktVw0Y4jq+`o$bG_BwL1g=J5Lf z|L-7Qe-!7V`z92|--Lqqe+2S>U|Unc?i<^_zw7GRWX4;N66n@|Kghk9^^vOl6C74t z7e3#jZIFOkf+ED!i&UY)e7fJdJD8svWhpCwl8b;+F7c{6(!P0pp^rf{lc0heR5uLB z`kFS)NzjfPz~CWk`5$`iYvRwG(r_^C`){Od<;+-usTB{yBryRo)Jl9#&Jw4Uw$?g6 zqXi@;muEmuqbH*m3zo>wXCR!Acwp6%(ys>Sfy67`DmrE5P6%si_pa=MmR%wQ$YA(- zDi;FxzX2;TB_aOExs?@^TyBTdB%zT0NDe?TPB_4qkZk7d8s7(WvPrFt zhcG0)9bs_WT;^TE0)*d0lQbWE80^^R+MyC@kO>9HR(@K3I7vXKgiYvf|fxTVAT+{>OHa4+?pK!d4W1)%aT# zFW_~3AM>J9_cBvE&XcsdD5bP?9&A+pVSPt_%APR{N*H!z-;i>)b!hRYR^^410$J_n z#{dl=6Wv3#6I*8ru8B)TxX*%NA+P~9Kr&~EHfule`` zyHuMcXwdilCitxjWVEnPP}!xNjf8S4ln)_YRjfYuq=h<{pz=beA?jh?MSX!>HdU(+ zqwLf&V7i>&V~Ag=EiLlz_6dAaUi`ND5W4aoTIp{IdDPUrf0 z!WB8RM3#BX-F;(RoEs1KI$#vLCP!a-nQQohDaXpa=AHmI^=IU=3`?o1Z6p)nz(|`g0OzASx}|!NRNE`PtfN`o4S_y+S{5P3u5L0Ujj-j55F|-hx=rY_Zp1 zoAg~Z=#X<*0-;C`V{WOz>)FR`r&-9YU9@bTT_g_d{zm}kzIyCs9abopSxK#r?bR#!HLTH!8|ZN|U$Ad*Rr2?}Q?fSQDnyrCdKUn^{cu&kztgtt5fZi+Evx$DQ9_j-Q{oSB;H*UNOV0n{;WO2_30Ivbv&9f$}qHIF>i1 zZhl%O*roKz2gCjC7EiC$M$Ig>3b6Okm%(XUeeCa}8rkR}NAvMz!R_s9`=&?9hPOVH z!G`l+*i=>~aVn#(M6iE8rN3N*O&sktMulMD^g&GM6$JiXa8HG@ARy$~>J=2^%gYwyAb6s5 zGI@rJScmis4szT{ZWJqel)zvo;IZl?By*wqqM+FqYOljW;MYlgEGvyo4tsQ7EVqrH z#R&<<&u&wDPTjk0V|}d>3g|n9^)>r!vhTOy7i91K4Z=J}0@U=fzhX(&u3lwv8FeCUwu zSLwX$Zw)%doFH>DkKDIs=)YS5)N-7&_wQ`i6Al0X`+v_0{DX+7E5sFk6Oku17O%F! zE*h|>d%T;xO|60;79i4++-sfd9zBaFA|UEy5xW?&N2@aCyD90ivq1Y)P6Q#K3U`61 z^Vxv{09|k&2+T07$Zj3Ln0ch-+tV2s!aUrZ?9bA(<)kl8z-w{ypwDU3G{vF)=p$wM zR8NRE&Zs9KHY8G(8N>JEg5(l;@j|H(&t4|+B{Eb%KGVKy%{07laX6ewPOd{qhCZBQgCmbWY(7`91sx1<9_0 z*|X}e0t*lvsGh?keYO*8yrg)qK>EwzG^W}>RZHX6(U1z&^n?}GltzF^TG_MdrS^{w z4Rj7C0H3i?V)og`OxY1(i+UC$@7WzQo+>YAx+>SG^XkV@VJLha!H`^&I?a=@=A?e! z7YA)`fs<~3UbU6x^|V8(%z7)=sxp=sJUywVDoE1>vYLK9r&l{pI7mD}GOQT{mSapZ zq-k0~kKey5l+IX?q1!dUR4+=6$!K^oUg;CsBLF50QG!Tmk^yr9ZKv-*9&2S{IT-EP zT*jG*^VkNOoCvCBz!e;N`+!AHVYy+6qguM@sFz6!oj6Iqov+`}s=$zGd6`c+^C6$J zm4R0id@fkTJWPU^MA0pS(Sj!N^k)2$QU zm9sJx6=@XciSi6y+7; zxyLmW4nJxz<1j&=n#Nh1ypT~wlS-EOja*F!z|YWFi$eUD_#&x`$bef z0*#xfWEI8NX@giYfdcS6M36E;XDzxJ#L>`=xB#?P*Zav+G5mrYu`O^|c9=rOKpHxE zG)#?tjS3Spz)jj*pnm2aa<~e-@a4C+mwUB@NoThH{OsJi6GT_&Y)BI8&9Nf*^~th~ zukL90;Ez0dbfNH&;Jc;Tq@_L|iWc3zPsVP+f_RkT*!>XPH*FMY#y&(y=!9BS{qaa{ zQm#SJFuKNYoab&{lu{zk4iQS&;pGj6N+cHeK>St|$0&AUolkOog ze3D|}h8}+-pXDft;dxv`Zon}VIEnj~M8hdcPcQ(S2xam6PIVf4SqfT(Od-=c%SC{x z4@Ftbu%e%zigoVT5IgC&$t0b#{AQ!?Whw)C77PO`%NJgZQZA$M4wd2%s96>a87CWK zf?tPJBn!JHhF|Y1YcNmTJMhz))IXo^gJ%q*Hb5E@VC+F9{j0*&`D11{6J8A-L{U9+ zKprFuTqy}YxJARt#dNOfU6sjOO$;)c%`HyN6_-YJ7bHz=u~UHAHkNS#P}{)6!RLTVuer>_%l;TX`_bjS$sTkE*?xcZlhvSl zJCV+065E<`h`BMDoU7b$<4A05o>}$h((P)5ZKh4E>#7Zon;v2<_|EZ=`6jm4u4nig zkC~5H$?K2PoR7ZcJ-s?Q#}D6-yxEgTTeYmkU7j&aqjqhME(Y(R?$=!Olc1$tr{sy1 z`&8&F3T}6_OSP9C4Z6*X{*~#F<&jjoRES?!_&m^_K8~EsJsvDKd`p6mf1_^ z3B%;vb{^=MpTE-#Pl>BXLcjUn!T%195R_(Q`0xD{@o#{z{a3+ZXy9aF^p~@7NieV+ zV1NnvIzb6!Topy0pYOMqJs6ZEI#(ZduxOdTN(y5svKZ=?n zRq>3?ur-Xe1?MKVe*^e=ftKTCj~ck#V_fW&WQEcLJctloxsP(fR_DZUhFHX~jDLlA zLugR4ykJi2qoB88pb6O)o2(CSi`3YloR_wH*$ecd(Yw}k zk=Vs2d(CeuO(TNka^HofRTi(gV4z(8q|Ea8maB`+TUow;Snh_7-T9|4r5Zz?;az5> z*4U&LPkhSS=mQ4DG-vyoTLAntg}dOOFm?+Li1n`xkWRMa#t2rpDDgFTC~Dtw(y*Q& zbgn)7@ovDst9V~QJ?;0mYS+Hi{Nul;;(wZ-koc>=)GVS$Di5|*E~Kc4@wo6T6d{5; zu?i=H{(3EyX-iYW5bc*e(MC|gk^lq@&hzX|@um3Z&!Bb-=Dt&TehI|JeaZ_0xd^e; zb4d3=WMRPQ@nMTX89yX*;)x=uqY#(@11NdjW>Q#9ClKh(LR>M<*&Q|Y24;F9oJb)H2Z=9KQLCLrNda_Sq!C8c%*%-En2(FP zu7@}J)uw69<(v+uCp2P{ykcI5co4fcIDlYM(WsluLO4N;My;w)kLn3EP~AibuIDpu z54V8d8`$5~2;Ui15%(P(^$`F72>e&oXzpU@WN+t8Yj0<7=W62U^w*+2($KNnAVBe% zspGF8vr)n^v8Ys;Uf6zY{_=;7RbW(4Z$2yicrxh4cd%x6Ro{1!p-u^E%l4W9#kod; z%=Jv)2VrglnDAT(OBgJK1moBTVwb;p0Wrs|(Pzec&jduc2}(=I3%>Gx=;2p)hguKZ zXn^ykOKG|;Zo|*-@$CE~&0SO8`6AtpArVo?_aSuoWOL{tO8{5=SR_c~;^*(kUh$0%PZ@nJG_hz%RI z6*f`v&~so?rV1&d>T07fr9rEs`%Aw4)gSY`3kidCOpM7BEofHfCUY;vLq63)l{vc8 zRDald*g%wc6G{}zKuz0}P|$Mm@<~XXnt$5D}|J z68TGrrUBZOg2>jw#YM&2W=+Z;UiQV#?&Saz+q=3_`6Vs3i9d6D^VeCU52)Fh%3nmS z^tW8ddl{IfPSq$KA^?UZd%)Hm#DK!RYh3S1K?oz?NDzi(FJ*%Zf-{-YXO~50_kJw# z57;0bqP>D541t{p2YPqUL7`l?BkL37n7)f#+2c|Av(44{J8ZN8C^8P5*a*5p)=i9Z zNx%28Y&M*319ueNyQbbk_3l+}WYi;8F8i z>$HY-=?siY~oN$>*Yl4H?Vhw_(}&f@Jxt`#ZfXfyf>{ zr>|^!_g9mdQ~? zf$f#aHK4HeQk|=dY;< z$Yp+}d^Fp=HGk>fAXNWYWs=RJ8}F%W0|T-ULQhmKhY!N_n$B)xd`M3n2L;3qk4b|j zYYfD;`4E;kpsF~8J{`2Ob*^A!3HNX7W)ZcOr2$g~xhr=r?%nA!53pHEtrBDq4$9(34Pq?&N&)hOygqT^YiE&?p}mxHhC0}QyvS(rZbZM z5RR+?>~}8&@fm1{E|^Iv4&*R^Vk!R!X+^AaZrS$wwrsfpNUR8IehXCUOLC6YX*7M%_L)5&+8uj}!Q zon;2k?jtjEVhgLlrgAA*tPzwMux$-Hm#$+vu=e?r~3{-n)DJKojks-iIJOhHjV6&X0SDbDRwDX z>S5#FMSJQQtG89wW<6@CD)(O>mIN87AUf#f@QjCAZvuy*=}djuuJJ^~z+&aFsYvXWLEP9;2(?fS5`(@06uj74pjOiPZ*#bhvY-rHCvr>B1CU|FF#lR(V zN_y2Ucy_gG5`Zg&W}UwedHItR7rVP@+qh!|%7(_=4Zs~LmxCd{TP#82>q zqVux){0LI&{XpE0B!H{0|3G1OLY4Ux{@MuruJ-sZ=OmonEbN^;{*jZ=*ogh!9P!zy zt5;O9tU@5&h-oL1^SMy^>f0q$hsMA(OKLvvSy?^C(kGNpv}H?J9&x8Lb0{x3odBI})$I4=dHwK@ACOfQ^qVucoA^#k*h-j(eR6Xv ztChfy&=dqVj*6~IsM{mdCs7ng;OsWV5TOaQBdMUC14a=l3Q9s}`?t}jwcqnUJL*Re z$0RjrV(cfm6bnDItyiz$yMf&XT>BW*vt6MYU^!}#FpbC`bBzS!Az|AbEXeMKOyB^~ zoKl-_CRNeGhq1)_&W?i1hU7Zbzcrp*A|PV>xUU!cdG?zC9w6Ou&x{TSQ|z-Nq!y3c z`wyyeV$DK8v8!oCQ1gL%YQlB2hz*!GtYVg0f1x8jK6j+pU}C9uFOB*t%s^ zMUF8m45i~Sx>KxZMrn~v7F+6K_PXWu$^*mqzLUu2O*&x`r81;0 z7ed7C4vo}jM*Xq#1oy1G1$MV1PjH{jxF}(_PgfT7j>{wQ40~pTuo=KY}@mk zi{lRnjWiCYX#ml7R|~R`V85WNj6SME`l`EW_scOko)Tpv00#}BiV<~r8ye70Q;lw1 zwKU*Dl!z1lAaM%PR5LUq+5jAss)kv?N>QUPPY#l65+XBs4Gktq&dmiN(#DOdG;6jV zW+1!tS%3Xsi(KlOc;RFQEo)C!i4ke&(o{dNtndm}5MJ%)0qwpP1HHD|`<{?76NFFh z&L$v`2n#-2Y|Ppm&?98xoBU{U3dF^s6vh4w#_EtcmZpm%MmYAVK{1$k43D9e5QVTc zhQrKX^s;gRlS*io`zK#-?jfT8*+)WU4bRe_D$le68K7ScccMUCY8(}xTto}uP#0>} zuPlT~i3%EXYMuPk79vHKE&Sl03L=Z*1&Wj4&X{DFqIiGrWQd0YO-JG>nFz80F$sxk zg#zbmoYhDFk<=9K4J&qxDc7^d#F8lAGCs$$u`6r^jnruD9Tn>5-xFxai1{9Z3L^&Y zaj(VHFV^NtGb+#sViv53Y+ZCJk~g3M1>UQ>*_VmvPnI;1l7HPnoPr!Cqzz4G%TxiO z6E1};cwz|{4=W*B%sWZjs&1t;!QQs+$p6YMYNyb;{#xp!tQHt(O4c#gbd)z`K@F}# zo$~Q3#&aw{sC%s)``QP+h;2Y;iW0`pt>kyVKkzZHzU`^nj*{U(`|`SYxdslY7m`bPPd|& zZ1l1CSn%scJC4P?mJf^?FYlvv?sL&1U%-aH=x$za_ciJ9c`P=Af`k6|#jnR2ne@Bg}lzOTQXH2i-bAN=2Cj{h9y z->MbeES#Oc4;UKS**QCXk8b~$_oVylCju`h0s>E33tM9o_a6j|3~UTs1U^uI*@6E@ zwiIx2HuAU3zpnqgvZa45`=2)9AM(BaUG`t1eg4hrOCR4AzeC(scElqII}g;F0SOs@9L?NtPoU4b26A12v2R47 zwZIPu5C-EHkjSN`ZjK3Q?TWr~J@36dDX7@&1q_|qt17EHwwG!;Umvya8+XPZP1e}F z{*ZlHeQJ8YonO*)VV=wAvAV3Y$?zew50%=j3z2o_LMm*9H}hSZI1x*FC@m>=`n;)< zV38vFeKuu|Ms*}Q1Y=3meGO87T3A%j0qiP_>X7TK-%fN}yv0VaU-*s6ynOX??*>*jqe=z;aiTy6QC>{;WMmLVL*~O8C7|YG?1pIc;8Ut7c>DvD9g`u@A6& zd?^A|g$V}I_pvz90+6~;Xk%6=2tWjs=kM92ePafT@53ejpfRty2eHU5E)ByfRjdtg z2#9W}nK17Ry1;+)x-ziUzIK$N>6Bs0tnWrRt)xkyvo}^(*_o>M$}(}=SYgsqUq$RT zs?m+#PRoDtG-S&Uk?a^^DcHoNvGb^v$ZT!CHNJlbB>YIhCepJOsoX-VKs8lr4$Kg( zUms@pYXJ6!E&h?S-hZ*`qB!Mac6p5Hu6uq`Or#jK+_J|e3!Gr)f|-S}tv*kcTNNx^ z?slRo_A|x_bDDxgr2o|2zPYe+h_u2z93!Qj@Il2vdji}6H1eUq29WBdA~dO1QsKU< zbK|MduJ@gvGYob%xzP4Jt@+jyd~;DpZxK5KVM**+46Llk*$KG+^k}H|w4FF}Eb<;w zTC&B5w4<4RT?^6{7{zzTq}H~;vI`6pf$ydmi8s2_y&wfseXDs1FYbs|NJlMy)s-Xd z!M<$OkBjkh#?Law?U-H0M`;TVD=7tXC?VdIi~508)v)hP(E(f=QnF?$9{rfr3Vs&kWLX@L6EZshHc{exD=fM!Kp%>s z1frpWzOSfIOShEw~q;$ceh$;``3NHC$*tvM&FO1;_+ zs*&G0_Q&D)dCKo;2~LRG@A$rr0AjR)q$Cc{e0l&zF}Vb8iZ?!2zg~5l^@$1YY?AS& z<#a#ZGq8_$Gsr96=y9>t${x4)^!ID`sx{$Zb9SH8{>HBz=Qo`YTQJ{~doCH_Km(Fs zfwbeY@rs=6h~G5RGE1%~#5C6r7e|6qu1G9}Fr_;*kk~5wKi0;wcrzl3Yixh{G)@w# zr0MY=3bP}B0YXO8355@-ZtI^<1)Ftp(~{Fr0~EOx!cR6#P_8;F@Ku-4QX+EgMkP_A^4C1QFcq}knf zqXJG}AY#op5NGsCcPgl)Z6yol(-MGV1AIxh3u=?x=KwIHh!RY*446WHwp?*$vS2gZ zV>Qw~91O_r_oU;8VK^mooMvb=*+sQi-Q4glmS|u@lJtx{oK*HO0gP#r!1rMgkTQdO zuD<8NTR=*|I-LozAjW?}#59czq0IC;0)dv&E}!qi1u0p3wP?%r@S2+U$zJy-Fa@$2 zWoA?zs*8HB8l*2L>hske#*@8~7+tv%p%5SG_^pmv6}BD9o1n}u-Vt>#K)6LB&_9lr z{K#7x$@6_Vx=mZ^sH$>9=c5`cq@DRmdC;&xk6}O@7SO#maMFJEb{q@#OdeUhw$Q7i zl8V8F^eJG3eeZ~ng!E|u3T75r13FKGUzY zQ5aIpg${I=SlNwQc>^Q`z~BHqCQS}E+2KTdM1b=r&~!DQ^CB)wM1n^3xF0y!#jX@C$iTlLRqyp=-rDzB?zTf75b@`$*>mr@&fNvX6SN2V(HGz^D2Q3Z7n1e-Qiw&P zRlX*C^XxUX1r33BJ0(ex9k@bUTWlWtOg_14$?q$~)Lf~&Wk%+YdiB3$oq}Lis5EEn z@`0X+t#y7g?WT5aw>n(b?JTdBD{>7!FBQl_T1zm=)BxDCmzV-zp61bbcsYILBdM&% z`CTy{w`dIQeY&@t?=P)e7;|>mcd-Ed%ie>R(g@g{p_c0rJA+!K!Tj0=_PWC!_}6X> zHWZyYl%d-+R44)%*NlJyE zk8;_mhKv8umGmNbSSS{2bq8f$6idoEWEp&cCqgpu-0!w$d~v68P(ZD&I!Q=Qc%{jt z$@BbI0W#(Lka-|=#2#HkJ0j(TkSHNAJtLj<^Q{VzT|W-Z8gO$Z^?6f^&6+6KE9l?$ zi`n%y=w=&?9BFB%pZQTcu4xFMn%uB7Y0ezOOescojoRt6%&uT~pbL&_+kN$7W7#^# z!9s*p15J-;0AGn3I|4dtGOQfc7YKHkTx@?xVhuLv-Y{}WEGozWP&oww1E1OpbuMND zS)n~_W>hTkPjD8b2hlL7X|r(K94gg#ta9K$@anBfs$7^kNUt8|n+zr;`2=%klXcFu z*zcP|yQuc|ot~7KH}55w=`mgFqWWRf^Kgu3;~Q(_-C`n@)Mi`-_8L`~l$dLUOlMOn z3+^q&h7Yf^<4&sNTb=fhGr*gVU%^erGb{TANC_(W)NohHE?M+RAVQhtQ50ZXpsF0dy)55lS6SIAuTdRR+PhO;EW@`N!Qb(*DYB=sp6xS z))RQgbjkJ09?^f1TsWdecTRuXU}aF-WPusn9Ah_!zlL_WXir$(MKkRxP7U90xYW$r zF1xv!gHQ7X&(@j4q2HZjQVr=^yjDP8%YF4AjB)`6q)u7Z03gd_M86;h zU~L`L*?fYf#UAa5P5aBd-vu$UV}os=HhK(OO?l!(qP6+%;WQ#n1Ww1=7DB%PAVW$S z7nl8b{a74q0Szgo?=WHbG#WjZ=nWHtoV0j0R4yPQeysZxAsA2q6hX1D$oyK}1LXSj%T>-xhzs0UDpugE7 zU!Owpax-Z)oLd1r(htmM4$x}+MW@r7C*M<@!o?I}lMvbpt}!@fiA%=C#V_G-?8?q8 zwBi7rMe1l-w;`LK0!AU52(E3ZA3@13;@E6_PlDKV!z(HU(k{zaI?5KxQKCGuJ=i{1 zQ*=h^mGvj^PxB`<_LZB+&`4eC+R;y|-W~R*758KRi?w%*t}R^CL}O>iwr$(Vj&0kv zZQHhO+t{&f+sVE;UFY<;RkymTd)!|C){ptlx#sw=-sgM3bk-wa>{u7wQr@Dc>{#eU zn)Ry#DI{72VU^|rhnp`U{DL|L3!Bm^aOFd9Lns!pw5V3n3HtWs(ljEvG4WT*N5UP@ z9*dVrCQtMkRAQI3_l+c!|iP}I`Qpai&itJoH_KeFZW*Ro}D*QFPUHzS}>-Dh{~htJ`?>M~w3 z4`5NVgP=>rf_fJBZ!M-HlUp#F%OK_udS?;+2+g`%z4q`^%_DuVx zl)Bb%`0jUClexP4md+)fC-d{EP8 z#t@#|X;N3zU;_u@zcDYW0!a4E4eqC!1CpZy)i~9&QYn zM(MReCliqxV26OQN4YC7vJvC9Zp7~yc4Z?+70XV>O((*{jARqxb;86zjIPJY4h?Sx z4S^cap2&WaqY|1&n}tPrMjzJr@2A9>EZ=qwdkbIx=Eb5>v+Z+@P>Ge%w#B@b5X}ny9jFd?+)0REdh*8W4KgBkqv~^Ttuh8=v>Gdef zDnXOZIFf-onb&@l@*J+@-9-N88|0&DgW?z`pdN9T?f?-i-6w+hcI2YTuDoP>KD1 zsn(cq^R0Rq-4}SR&9s9ccLCoD(VJ-1_}-9z`fhQX{|irHwOc(QG+kRBmX9a2{)V+F zUgwy5jhmAxA&&!>v76bz(!ovPe5uC7AIae}u61-LN}(45XJ>!U)Hf5eKZtE&cifg9 zCAKi(8?M5Ca~xup1~S+bUu+{s;ZjW(UOHrTNHs5!mm2m>} zqB~iOJ~BZm8CPoJBxsLpbKgm?~xqxrHhV$*jxTSiMCVm-TmltAm3-OBiDlvi{_)h*n$n01bfS<9gJP z3ce#e)B)eVb-)DOPr#^9V-g|W*zq26v zqcp$Q8wzz}II#yx zqlgdsg<9StB=_``ad@)C&x2bt1 zbp1qT@8lq8@@}~+EUEpBwH5eqDazZ-0x`OC54f&8j~$`BVaF9Gx9=S`mFE!(tkLI| zorZ#tf^{YGyDip4z$%o#0zsh{&-|d_*dURu_o4^kO&Ql9Y1=jO;53HD73#DaAWqkA zgA_eh7>29SU0TaN!~&`QDyk z3UfnMu_6&djBx0V-{YpZ2=E5S#r+$-jj0Elog6gFB!lJH*1so1zZT&!BF-WEy=z>} z09%wK8cB63++TLaL>IY>iad3!t`x`{3MmTg+~b}(%)Qn;P0CnAr>>OPYM+H642Y-4 z7EeH3+O8(F+-#K)v1HlqE@m4m78pLiM>W>NbqZa5=R7Qik)SS2cba-!qY4-TJ=k7Wj~& zvI2cNX^n)x7WQt=FONe;?YLME?3zXIW+TXB-~jA1Y4nhGINeo+L4!(RsMG`_2XKwfhr8p-3hfI5IztdI zf`hQ4|7bg%hLw_<_e!$EB`)q*u8=ApXWs$!!oE``rk^ms!r4ksfp^au_rsq}*Ap{e zD=oltPVWS<5%~*<;Qku^_6#3tHA8s5U_T(en`7vgJnC%@`jVni#5J7}mEeTVE+pK< zJQ2pU)kB}|oL`5$EVQq`Vq}@Kc2k_maL+nYG@4#)eS?%Yi9fbyBV9^+dU`Trp^FfU zUen+KdR)>(CXN?FP7f&b5)Ea^u?DFOATO-%1m1fMliORX$X~Uz{a}j&`|r+L=WqTSK51C zfwADSKB|b>e>8~NSyUMSvqb^psxW+98dVS(^@zzL%wUxxhM6IqmHcmCQ~9a#2^C-P zmJKhovkMLu*~A&eij(ntxSGkth|wH(Ue(>hhV;o1$grP*ie=xi?eq}md_ z_TW0^*96zC#%l*#T|oZo+HDIZkmZoO9)ui8)qv4(k--Ks&e+l*qXnW0+Dm2wrCd0E zEM|Y615>mAgeZZ2j`(o4!9mq@8sceoZ7xEE_+4q?6>z^|X5)6u%pRwk;Q?EXZ~x;O zdN}liPIndqa@ga6)K~r=LDU;)t;`={OO67kvgFnhtJUvc6J2lwYsuu(#9Bo3BAUM) zC?1YP#n8OeEK%n&7O5}y9|`lW7r$gA*>F0;@;Ey z1E=&YTqZqzofW_t?x434AG!+o`=-7Mu-- zV=81bL9G!Nb;}$KG0q4}iBn@M27Iw|bA5R^xxz}!=&S8@x(`3Vc*RRwR?YIc2=Y9s zTccX%1q_>S4%UJc(dm06%Lwaj*Q{iJnDY697wP;vWJqd@9R?B~6^6@e_krbr@d@%) z(XMdK^oNsL_7KNS(KxZkx}98)>yMiI2lh=H+!bFf4jG*O%1cINE-)HGfGRy3aG+{u z9ot`3O*Y>}P{6Z2Sj5irzV_+Rlib&f>S@6i+71t@(QeHLk)Jl|^;+!HhxU|Zr4+r? zbL(;e%z9h_{b3e0N%#FI2KQTW&0Q^)YS?U=8bCcI(-vZ^ia`+!`GhhLkwKjG8Z#)x z6<3+WPN(3qr`VW%2>C*;;VC0bBHp5z1rJk-*1?)F3@O{9l(M!$5T4GX=s#{j-R`hq zYk5^YWPi*qlo6*>BoDRU&t)tLbO|TDg#jE8#@siAU+<^{(L_~JC3^G6o#LWba#fdF zI&d~Om!a47v^esvHYblTpFzq9^o+^mVn_WKjY^pgd)@kDM&(PrbVSW;zIq-kHeJu< zY&?k{64Jbox)so0=HK>G*o4gCI_&h;`_n@5C@ooGsdU9jvm~@~qsO-~qr-F$zX&U7 zdBjTbX*`h(6UHW86Qd`K!e8UP)}V@c6SMZ%_*e%H?B6OKaHEE!nnt=e0<^5gxu3iG z*QvTNGKb?v-;-FhoMDbW@7NhGaE*=NBAM>LnfnqFL9VT1rAl!x&W9T=afS~oOhq~m zWZ7$3VjXAd(JC~)SBZ2s_BHEE^Bb4nS)Q`cEA%hXrhCY-F7^|_cx#@czSIDYK?l&Q zhN!iZs-c67D81Cn$SP4M&5=69tUzSa^M(H{lHAp|K^a7^2Fy zhpu9kwg7#!cDmL^#k(qxS$}{7QzU}M^TI}0UCr`WznDkL%-KDba4J{1(>;J0jj1L! z7$2TT0tFn?Q!G)u{>F3zNbm6S!l!drSz&;|amwptZd`fDoY*X9v8|84O%@77`RJ>~ z8#%lsI8BQ{Z%IYueVbO1{Ge(uddvYyxr_rD61JLC4J?G_Bam|H+B6a8G>;GNVv6?lQomD3F1QnUm!3yIdg5hG2%tySJaZtZaEfX}iH z2D0GGcq1Ez7_JG{P-Yvd#N&+(lQlrX*RsNbj9v68qb3AZa*MB>3)CC7_z6N&1x=0{ z!v?qfuLE~QX2wvjw-O>;bAX^7q`VlnJzKH_*U&Z)y6vzCfQ>QF3{X_{4z4Z&W%he_O3Tonmbp6_*b#dj7La7rP1|(aJfOCg|cWJTA zg+0Q9y!D_vG01j*rlH{#Z?nXu- zA^nbi*z6t!3Ko~isS*D;r7ezv!J>gz6B)K;1_T>TCDFer#!GQH*7p6eeF||vv12iu zOz|W-Rwi>dR9&|BBVF7FB4TtZaXdD}Qh0o`fniLv+l?J?B!*O!XJD(*KLTg^C} z!@Z_Jq8yeD(b3*KT3I|ded|gS9=|c$Yger*Y1L7kqQMK#V;}Qqbrn6cWgc}cZdMtt z995gzVwV1TfS@N>tK)wa<-fs=E13Vz!(I5Rcf~dAbp|c6v?o5}^?!CP*?-we{Nq~LAHundxv}d% zjgbD;kMyLT6nyrjlU69i%xf7BN@Zzw6p za&ac1ANVrRzwwFVb{ho4I>NbNH^PPXudV*_$jl3v?Tyh$?7#5xdA0_!o%m-x?19uG ziQNM7P6$Ktz7pu)Cqg8@|}vrj2iw5fEDf`3{i#sWPk?*iCz0P^f} z%rt@PB)m~hHNLiaYxMGX)^BmE(s0pPfTFWPJ2zpg?$$HnMy*}B*l`)ofZoKQg@uEY zsP6>`K;>rBXF(uCS$(M4q)OFM_ARwe!rw|AQIKMO5xI@81ayn5Wg;%k&nL+5aFDc} zbpkha+0DS5X6?6h6ew$fyGSs}=Yc2p)dg%A9G0bC7h^1osyj&zi{^Rxi66kai(kPW zQ9L!1jpidslSMVQ0-@0*_RpEr8!dvg%=rm1{o#VWg|;l$dF=v^e9+o?&!~|O5^Ti| z!YvME8XuC`@K!y1G9^@3FiRV5_v-AAB+R6o1BT-pQ!!fl#o`2e6!ik)j=}h)^*dC_ zs{rDnZ~4KftqQ( zt`Vl`>z+zqy*Mb z02Qfjv7VRHPOkTqeW?>EI)#{oj+z`6q{J0o`}H#2MYITiIeuS_tsU7>h2zrE(ZH00 z`J1~*M`};B(oqE}-UaMN)M$Z0e`0Lrv78QFSqvxsp~HInGa-W$?i zSi=Z)=_xRBGYNl&dqt3H7^53nxM;!19LGHiPSmqv&ZSB|Owizju&kp-psYC?!EFJy zG2PBPz&$v6h}JSS89W#?UF=cAdJ5CbPQ0evY@jq~Ue2wVr}kO0e$TX}cSfqYEYVl3 z*JKFoe)KOI>bvF2*^>k*43tcp*+0Kd>lf zDJR(bn2Yke&<1bSZ%=EIe=d9NXQe_aVDWtjXT1vAsG zU1flZVC5)o1D5Z2<`6BO>GVLEIII8+$DU@=@h{v-IT9qSz66Mx($P}sc z*xA%RXEc}x4qI>SGb>q5F=9xqMZkmC03T~f{RQnF@50KXvthb>qBZQv@{%WNQ8TzQ zFh+8w#sWns1l`ls9Ne&(g8U^`z@SYh#%n3{as5wj*1wj_z6Gv+>>nC3EI0rF)Bpau z%S7YoL;c`*B_ZN_VM6J53CB)<92O7wk^~!M2zk3|9lWp;Bi@cjAZYqJVn2c8Bh9A&W?4I&4>C ze%p~1UlW~HK*3`LiBKMKHbrV-v%;<;CqT#nKpL>fpUAlWGiv9*0XiTqff)H) zQ?@u_#ROfG zrzBRPf8kM<)g7hBu!PYj^$g*J^M`L(03&#K6ygr@^a@sG-A**YrKetOy!dJ2!DgFT(3Em1o6_JUV3<_ z=QaKfh|H76I%~(F-i?EDiYF0Rf_&ZXr(!#2B2EiY;uPz`vznzdxc~Z$-aLukC6|ws zF)&JD>$rT{OP(=NC*_R+MnDGk4c_OTNQR95$aTu75k?KGl{kaN6*okdx1rA^C#%Dz z&fo^t(@WFpAEZ2BkFJzn$VEXbzI7qp#NIp}>2VFGPiYYxd zcDx4~mR1b~Zhpu1onNIL5#6R_|MHg)1GY|BP9(OZI|=hV;74D}?j(f@fo4XJ0blm5 z<{0UjS51O45ctHY7y6`r!q`LP@KOpbt3=Qitj?i~pye!FXd;U(dmGnvetyhLlmW2) z0T`0rgatz`IWApo6wNckH$5Q5plg)*DY&#Ko$-vhv{D=wb^4_+Zn5l~_;Q(y8Rw19 z9b1TFt}LuBBrp707LyF_Ix zyS?@A#zsc=jbK=$yIw8Xkd_*6$&&k5q8A6^-aQwc4QCL5Uw2 zxsra7+;ge6lv?v2uJLa!prwnWeH@)?CS= z+c`*ALA6BScV#QlrCt$5+u+(r>R8Hlps99$;@{HEZ0$CmczEfpR@y8`f^S#&T7Fm7 zps~2>zEichlP^O(2w!%WRNWgJdFtLMUsw7v5&~p>m)STj_B76%zp^)FOH0J6iJVz_ zg5Cl=IIyl^nRe_bDYL#aL0BeO_;O6pZ>M#xokbJ49d>w=Mtx>BsmrypMU8^&Fg8u(J>tb=qU=+B4}F@28uH3S4z0m*E22&X z|E(W+0{jr40&Gf`x2bS>&h$cU-6>~>KfXVd7hIZEp-r{_DjJgY_tmhh1RAK_xs&Y# z1(K5;c;(HV?$^-;)D(nwmnKD7fxIU`zQoH+Us+!3)m&0Gh5B4VJ9r|yVO{cS;jL0; z>+43s?;3Fh)DA!?I}lkZc=b&R(utrLHW^qIHY*_CPenD7V4+`J_4@x%(8NCq30pwS zA^80GZLAbP6cDeI&O=&dAZrRIBu;;vB_oCHG!REnGNKN?>_NyXy@Wrt-bn4^6n5^{% zDxa-)l1HnFE$vmQ%rPNw9CYqHS26`mT@ig=79!Uj%Gvq(Kud=r?OQ@ztN-?|5CPQR z0dplln%^3E#(s&fA&)2%Gh$7~wOHjvntZ#3=1otGJ_3>HQJI5w8CdS4(#Nvl$@en_ z-@mipJ@*GOL*Sq`A&{PA;kR&Y%dAwLg}c(9qUC*IVi^u;&Ho%xx=8)Sk6dd9jS#lAzRz zn0?yt=(?LY1kZw)}>Gy1qfMte5(i{VZ2rH(AtDa{V!esh};;62AD0G(ti zFOqCFm!#wn^Y~o%)3ktfzT``yJL*EpRj3arER0H(AJ#lYO=MH$+#C;u8D23%F`~v3 z#HegHm>}l3rF)m{9rk%E=n}`Na zJ^IAJ3-&~z0Ow9QmZIDb%G7Exlt2}{HhdUmL5%W9fn`bbp7u})&`N=km{wvDE1N4z zDZBX#lTW{&X$AIF1Tz`*I6L6 z+%H|_?>RwLd6+TThg4pZf)BZ0+-wsvyB4@YaASmm++sM&A8oPdKMCov9Y|lYUYV!N+Db;Enk)E zti$(Sc4Qf^Y}+|)wOS8k^b-hE3}e#Jh7A9>A12F>G#WkCL!nT1muX@vboy z0ER|_!5Kw(kjtXDMB-`|k7>-&VOt*La%rYChs!F&?U}2j50f$KEkw_5-t5W0_c*EG zBI&y)QH965MSRB~w+ForPc-HNVFJztRUI+nZGC?VvW%3l0J$_7oXqNkuR zjDXs*rtEq(JEt1h9qTxv_T?kPYL_XgCyPy3WjCF{DSjek4j2au$vnHxCfh7 zo9Sf#kT)f)He;$YPW)T$g;N1hUb@V%S2|IYrA&H3t$T_&2-G_kd`XQ>(YcX6UqA@* z#V}gZCgi#(`tay4TO+$_dkT(<{dJp}$@Tf`<=c0P>~z2BE+{qEkdp#)R!o1uK5Uoc z=*f!(3z&x25{en|+B*huovFK`<*F z%6ejv6O=9&otNnRE@nT0h!2j0GMb4zITnyR3pVc|EO^n^l0lNP2!PM?R_02DI@0wouqUgsBj%&&l#iFr& zxWAnk0}9iRLrO_8B?$$=Ir1|-QB|2m+SHtwG~YlAiL>2Jq0QkyLh1FRTI9#iCYT#@ z$nK^{es@2|V{kE0e7F{~JMlW;SX#?lmJ62T>QC3mAL~ef0+iU|C5~~MZ3|3~>cxxC zLZl0=&#~AT;a|ul-1;NBozq7$h3Xf)H-`NV;w7FGcGxoan8%2VXCd*r_06}v_E1_| zH0wq21YfeiLS;*6#gAj-^@bvpzbDolmhYkh8&+w4r}oFJq%T&M_GM*+&ob>s`;)@3 zS*d;KtAHtdu))(d1>YEBmq1F23hl{JV>$Ma=!(lC#z=c6taj*23Ax9wH|2Ka3)r5T zYYB~uLNi*Q*l_av@;I4R6U=dhcGi-Ws)jRa<|@<5Adn;tr3XnB4q~|-opncqfM z8UYhqM~1%*89Q#2M6cfQon-KP&4d)#V1Cw)z;{H3>;zPzWD9K}A9r9R20xq@DCsW5rC9lumHK#AI4aob6BVA?%K z0Iv2Mi33HKUvH5`8bSx{Ya^GzD<4OhW$T{vGTgR*lmS#xmK+;a!Hvo?#+Y zkS`(*>i6|1vqk|^Z1H3+%)_1b)JIa00WMOwIVpg4O!xFa=^ZHS`pSV58!OMLo)y>P zqqSZZ+X4hAo=IHF-I;&4(t75&Uld;f>wS&_s!ftWM---agMv`*Dkrg&mj!HKiCCFG zeMOLVu?$2>QaL-8}9rP^JT=n}~w24pD zwH$Gq?k8z-Wf~Z}?+z<}1RxI&@gbfnSOD$bd>&lkgjd>KR;Du?H+V70_Nx$t!&d=h zH$~)p9gEom1#bo~M}0w}X$JKYFf`4AxVSD9eAbHoU1W-zpj!KyyxRNsdH%Rr%zmxu z&hwle;q2oDMm(+l;ujHKQUlSJ(24>5ynWi?q zb~?x}EFHute&tZ?mg*D?bkZuXp`0a9MdYwdo&9MWA>xoJE{JUTHW+A4nuF>d?x4|? z6>^YgF0d!7S^^|iROybX3J=z5T^B*bb5IUo%~(m&X$T#HnN{Va~!f^uq`aP{sZ5zMn-I>Da%!~5Y^pWLgft5?!$E;ZVeS*we}SDceM zAWPcmoV07ws@u&OjJlC^uTKx)o0PBYuio44C70yMwnFq+TZ2vS`a^#(hbb6!dfmWo z4(}uUTSLX4A5N_kxvJ6Ym6_8D-5BtF_q%xhP(}z~g|#7jT|itr#-Q;1@^mshcbhqE z7%Hu*sPQHcbLn@$l-*)$-#W0gC|Y@x+!ZI79pbi$8RkN|{e8Up{R1!m_W<1goCH60 z>>SJ;t^c87%>E~?d_xs>J;xfhg>0!YGy(?0bw#iDq7S}qf=!cP_S;FZ^|V*kflJez zOAlSIk5q)G=SBF<0B|S!p7igLzWrUzafr_%X1A^v_aCzjT+Im?AKH9JTiQOM=?rba zayfFqYvo*;KuONmn&R$Y=+pNCQnjRMmjF zY@;$|ETpDn#9v$_Nzq3lMM#X784USWntsNna;b3`J+mN{Q9#}BclKWZ@ zTQcB4_2@n?FOe=LE3avvDXAyb{rEyK{hUyL3yq6w%b`Rq>ygqsyt?P!F=6t&pM z*`u>p$QL=W5&LRdn;+3b%_}^6*Q`bmBSS_o0>EPuiS%V2GM!ovVzOWqh5FN_hN~d2 zkTGSryjcSM6^&bW&rjB;vN6$n9GM(T})E~Molq* z!t_)KOWZiU=EPJye=FnB3AN?gy7r2c)_pp=< zj^ND`Y&&Pi6=@UFx!0%O$z1Dz42KeiuXzTEK?y*^0 z>mj;@ToX-vQM@6$sqL>1v52^rc!M$5)?l_gGa|(d%n6SW?R?)2rs5$t7GDgEAz=3# zVK?)8%G2@0>&qW`1|&bX>)_N7=U`neeIopz!9th_=;N?F!)4hMf?6mu|_dL9}w(O=#$iuzKEQ$KIAFf6NY|!^RApfx?t`d zQi{kCdR=obf@Y|5CFCx8QT`&bq5)pB3_2T+Vx*>HVs0^56mc*5dXJug2ry77H5eaa zsK$;^xe83t3@=*9;He{w5qjLfki?0+Qi|xA;vgo-S=rXXHTJtO!}w2fW2?gC>LZW@ zB!W6&XbJhvCq>XwV{!HV^KouF1gMU-S${-cG|EI~M+&h`J${AZQFwx&Qhw;tbtR!K z1C5w5vCvb5Sb2n;5fOg(H^w8z60`z$ow`P+P=Q;kiaG%+$@RS)tBEqDMGCVtI-4hS zS|u_Wa0eQrT~2lQqaK^F*~_J65h}Pv;I@qRWi}1ys_R2vupa4~Z~{lB%K+p|ZDyt2 ze3JOZfJYhHoPSzj^Ei`Vd$0LlPSfwV^kF=JK5tn0Mr65)vdYCjjVg~Z78iUIspqlW z`KP1tho`o;qrb22RqKf=_Ittv-F*jyRSd>O$c@$7-B6^x-$q13-Ni+V8_a+~`=#~` zmuUHsK#2qg9~@Rz8puV9uhnkCrR}RVNa=H&2(cN*5PS?{SZ{{mXBjZRQ0S!>I56*E zwq{*P0sL;}+qq8zpFB=@TpMjDjF(L}G7&OV12LEaKT1rZWd5Sgk?pTvVp;9o&MVZn zqZ2my?M)_68NB7-5m#9p>k~j#>9My$#XLb%1(z9wX>RgKEA)EoI2cY?LOp@u%#yl6*ok~h0=eJ;t8gS)9eoj`n{KY75FAUG!v{O zTA22&#cEuIKgJc5w*%HdB-PKNNSi0wzE!6P!5yHcDhn@%s^m!~3(>!8>E%_eRX#|Q zd9b5l=*wHq7j_2B(=O139cZN;)CVU3#C032WOL)pBYelzW1wm`)lSUsEIw^}AA2V< zYJg1geyv%E5e!Ll?dqEIuX~+O=~2Ve-TjNi?t}dIEMObA#R9F)QmUbPGl)yJ?Gznr zX?b|q(PN0*UyRrhjK6Ji7->WYubNLLaF|HuO>``mr)oC5^_xp8=DZGWbGx6X!Sulj z;hAX4iUys=s#5jHK#@MIC}GjwJunD9EIl)z+U!zq|4mKc4WhtMUR_bu98NDb@*HHQ zP;<=E!vFVMJfW}hEK}h+wOz-UERW7@StV%vW9DL-;j4K$R8B#n1ilG-O@*jIu(5NY|W;U(wwI6z#<>qC7Bi4qccNr?y2HEA!Gvgz~9-an(wq`9IoV^Xg38moxSZW90Oskd&(ax&YsRV_U2StbI&De0Sn@ z*B3o>Gf$P@HD#SC+`T^B1&3C#%YKT*!ys!hL2cVJhDU zSA7@pkaNYI@qPaC&;zBX|K3+P@Mf+DFSkwY#Xd4fTCGa&6yR!Y&)-;`ueLphVuKwH zzxR{FUP%_98#sptsq=JWE>DH3ZsJs4Y^_|P8l)Q|AW=WtFw8E70p;1{x;z1o?9Z&r zMBrJ>bVd2|#bhkl(c6=%!9ys_rHKsMbu!H+pA&QKPxx#QfXsG=Hb;=%6Oz>83E#DL z%}OQkYf*eH0ZmDTFC4?mM%SqKIoPfFs~AP7uu%|lq@m9E5|4}hTzmdyfjmoBA9HW5 z?&8b-YwuzG{o@K-`u65~a%H%)*72U5`0ti9Z#S8*EjK&R;of?$&)?C`+SjwIv$L~s zJ~wvfvDZVq+}_XY-dwMD`mbmHW!)FwkI(CsTDF%NzTPd~sopJ}sZd*4GT#q3M)l6Y z)?AtD*vO^fv%};4uP4fPJG^YU*F@Lv+Fm{%bss1j8%|I0(e;OvS$OnN-;(Wz2HqV- znoeLADA%b6q4fdQ8HtJ5zh0>J_jiZC+6Oz|%AH-iJ-R$U?7dw*`Es|vS$)$sWkyyi zeR;sbpKc3K>oG#z>r=hH-3?BpiAqoUmg@Wt2%sutH*0$;Q8o4Shm) zG1vtRn{5ka-N)giKbgya4f&pJ=7U`M2L4~0mC*ff|Bp0w)3>(!ufyX1>kOIyH#qP= zi#ggFn(JH9IGX7@{IC@M3k*Z>|NKG!L+zgq>_70(NX*3KkA6T6-=DW&`hTVVKWJ$* z|NL?P4Bq?qPw|gF@G4`=W`h-Z^yUFMAP`U-zC9+HGA7cXb8T2hUl)Mn4=O^$qA;Zl zns`8X>@dPLdk@Z1{gT*Qg&ec0z+mJr)Q(QbPz#SDF7>$Z}wRzdQRNGHPpmCmk8 zrR#z@`zlt(#rO6OTF!~B-J@_3-W{559NWOMB;CG90QAzM2nFN!vSEu_moF{tl=+es z2ilJ-YYJ`zNNMqqMOaW+R>4Z~DEnYVsi}t+-z|2d1$?!Tu=}Ud^wUBF(^;*YRq#f%StS+ncf?UKa8ax80oQilpENzK<=gLV=u>4j27GZ^F+ z3-p`BT}|^k4|+oXaUD>-m5KS7N^ zmAc~E84)`2K;Tp08jNSqSjd8&-}>2W9V&glEwxVCwF4>=$n=xwt4acliZ!u>vWNR_ zqoBnN(9r-d;)t0Nh?^whe?)GSB&s27GmXWrs^%I86)kPlAap5lZjfE7@+4Nb2HLlX zE#UX22MSZ=3Dyep664lrj!DGIHM#B&>OTPy=85tH$L;5HHdTo*?t$2iSFHr$QvENx z`93MHA~w2oBgvh0pF*m-bWANG(bVurlxVcw=efw@pBGD)9;(Za(89ghFBzVl=ie-# z2($81hm~kSjZ$g9G*G&Bwk@qN^@vJiV`JZaxXZrgD(mtsZEF`egR?*4)+-HC)S&z0 znUsPXuHW-%Dz*z+T3$q%>?8N=E^M%_McsE!Gs~7e#SUGc*S+h`gE)H!1XThF+Nc`d)IO zsvCV*BM-btGSgf8;=`7RJqaY#bu_}$l?1d`$Xd9M0@g7xZ=jX5vEp5u84Nj zFQ4UVT10aMBnK2bC}iUh;~9D z6QIGK(`<)rl}}~8CvFcdy5E8xxM2Wd1MRwr8q-G8gJHw}Y_{Fu9SXQ|_Cjm#99!sR zzndVRJyE0CQgCqBn7Y_w9xy_Ao6))zbmFQ$IQOtUNn>{yp)xH$0<;hvajZ+17v) zuUSsh8aP0wo*-{LmdD=z^_DCA|9ht~v9)pfk9%1gr+9c5>Q9y%5deVf|D`*a|C}q> z+S+d55iT1F9zxhpSiDSus`ovln=cHwPxwsJK-7DSX`IjJCsBqhyq?i@x{Xc%iOUaK05~8wBiNLE?_kd z+>c(7h#s-8C8y#cnu$dmZMj_#v>*blZ|Ur&&yP9PJl zxLnSUmxHlc>~LI}fgPncydK||$D~m~74%>UH$6A5GF`XBx}R8r2Dl-e7dZ!FH}6ZO zCn#<&oZN$+d4gV`-7wzTAEl8ENEu^5vNm)rJEi(Y&$+5o1e*~QbuBmen^RMr7j zd{-Lp=NmD+oDijKBlZj%%p& zLrg)xpTdH0BQm+lE}k}J&6QBz_c?zagYlr0Th@aC7Nw5|B^weoKg36K@7o8Q0Kj#m zR|PboDS%W05*16cA%fbYO6i=wl2TEUW)ohJSTi4e`ovG1K7tmsFZj`NjKXb&5I_EX zdEnL|w7&P=E{HQiU7S3kpYit$M}TCKdtx%Z_wNy@EA)R6DY^H%T8^IaLk2#x-T zK8e;%b8f^kr0RZS;_iFNj~%3N@bszWx^xzkO#2IzD!*JZH5B!ZOSP|qW)WIQ!g&4; zXuRmSH`+Ob6Qr~9;)aH_GJqNU{)*xS{zbki^iG2vj6h7IRmq6ib}+ur3woIW!dc=> z2-T9N9EJ}sG{>xIlCXaS-~JAJjck0TJevFL4+`=c?AUL zB%<;k2!e(Q;_-wsD?@=eJ@5UXiq4kW3gNlB0vYzVDg_COoYY!X1#=A4AXp}C)>IJ6 z35{P^NemcB1S9Q!jIWBXERVc>?(F;Q5YPJ-)tJ{m(J4SZ8g6rVq(u2rU!{ z@geFIiNj)~ z$O#YuJ0PT#M8X%3p$n$YC&E=Ujebdqk;ns5GAK6K$zbFvLH-2S-tOd|z&pIo>lYfP z1X#np6Nmx+eFyX!)xu!X)(VMM^&i*EOnT#q!E}R*2JjY0Z9s@xIEb1w=-Hq4g6kuE z2B7-_1a?~Tldq&vsB}pA88XVyK|tqkW!RxjEzzrKq3)CC%rzlRTm2IV=@{}C;};IGzL)=3+X^% zAlE4nQG^nGd48e-AYD*%$zdQKNa7?)F;0~F!yr&M%Kkzqka*U~4$Tt*=n_DvO6;H+ zeF`z-Fcpeo${;Ghl&clxl>wxt*1(FqNFn+RDS93-g@z%H+qTiMZQHhO=Re~+zQbDe z09C87w=8+K%82B$H+K!An7l`(Ep~&kc5V_~W$DFRW;#4z(})xf>QqZi)`UzI!>I@` zFBgOd0!@sL7th>KlNfkV83;2p4t^6%)_O|r%E9s!JVAQRyu(eUp84O zoSxw5UtVeps*co-$Z4ENB~@6RE*l1ns83e~Jg&l7UPx70ZCfQng~DfN9*BgC0l3uy z+y85h@^tI#j1`M!Z%&&a5f9$k66KE=+wdmnP)E`C4(tJnF2q5(kjhA#IG=yA+?BhE z`vk%HXn(oqog~Fdr*&}lG5F|(;K{|Opk%NoxA|tiwu!$^HDA~mZ-E#5#qO!A7bFL}1pd%y|KyH$# zG!jT3B#CkgfLRP6XlyQt_Er}sMj=b5wzwXOfH)(MH=vrhNu-rdW5rd$KKYPlBt6eb zsG;9zKkw(z}a^5OU=cIZE9pZYZcE!8&S8-m2c-kYGq)V4~P-Mwnr>Rgy_R# z0f)n)0x+`b%dg(*b#V~5eew$+4}+2676}m}A8WQ*dU6*YeZ(^In4T!=%dt|{aXS`i z6xD$5iB(@KKr~20EsvA~6*f&KP2`bOPLr@X*507TWmCgz6OyZd3;hEPVQ||nENuB8 zUl7hv{?-^)W2J&z6anZ}*k)H6^UIv0)-@?iLW-!oSJJ?g$0uU_b+`?VUv+$z2k7O| z9+QgtcjRU@eC0Jju*+#GV<}jLT)-U~)c?^D(y)!!qEsgyI1?Q$q=pK8dq66AkYfR- znye=SpNkdav}IjLCiBtP_bVQ-4O>g%XiW)H$0%w}H0ea@4q%P(6kIn?W=cHz?T z?5`XEUf4&wZF8Fw9!deI1X`Tcft5vMd!%X>23qi+ zgSEFGQn9_cPTARw4%9PB9vtAB89}zbM&Gx5Q{h?oraA+HBkW=Tv@H3K(0!cKLo^1S zk<9%ruJ?{Cy_2~I&go9QcNH+#iQ-DRz-AQ9zXom2HOQ}VenvUX?=+kuey}^b2<8ii z81~lHO0Z5h1hE4^SM4{tcu~jxi&YrCR(CKzmaJGlow^A_mO1mLRr8i*(pvwxR@d0l zEySZ{S+#2-FWcs=8}SboSvpTLYu109ld5@$=)fMZJ6XBMFY@R<^VolZFa>TUQC#^C zyC&&BDZ@+Z~%{xy?;>re+Ewf7gjBsL0+^`+jd_Z37&p{^sAyF$w8y+c`VZYaL{s)EcDt>zNJ7 zG5VO2!r7@BwRKOl61FHMZs&Jok)_wMhBz@Z_7JsF8ajz5Gs=K?viq`t7 zV`6YsZ$E8)Q2%}~$Z&%h5Hf}xoJB3iR(MzCFyth()=7)h&Ue0$b_gzzxo38mVpF}h zNq_r!s_ovcnc9fFvuas$Hr-7_H*3Ma=G?V1F~JbYl4wJat!n0%{#*!2!l0dzQ&Y}8 zs1oBUqR?N>V+wO|H+*g21r4+N?<#){<05xhwX?ZvVFJQfud#WcFdSHRWgw7Ik|$+7 z=I_c?HTV;9L~X06kn07S^daXsMs1@x!L>*j(Bf?d*5(g&)@O)@YEgHrmh6f19_H%zMWFDHc)_TK@CB)-)M73@MDy4>l{f zk-`ke^hT0R8SyM+9X>U44r4l0Xwez0`2~jlMPZR93zDicp)LOSPWA@|xVn3h$ay(< zQp=6@l5(f)iG#FzK#)rHiO=iq3Cv{^!B-ijRxZr2CvNycf=#eP((TI~MEa*J#pFoS z8w+mvbVpEs| zRxS++8O@tk8mPN?wIn`wG0UFv#?Hxu_RKTc7xMyGl_RG2kN>fK)Ts3e zp`#eZaPHLt1@__oJy+DgfRfYv5j9zvz(G2-KT1*4n1UwQYd8NiWMo1PPl*m|xyGFN z%U7q5P@wJ0L)d-ZWOK{oI%pVYh_N_tQs;o}?X|M*kgHVM?LJ+IPgtzp{wMMHAuP!o zm8?&@?ysYoITzgIS9-P6F-h9~QTnEW6|VGw+b!q>UPzlEs@(jteJGN8nmSZgzugt~ zABIExzdT1SO(MB5u6=JAci;O*<_6M=6RWA{9;*Z86_==xkC_Q^g|~}PL9GZre#$ae2re0TLmt>q5g%zHXp#s=Uk1< zuTK901N=~ZP+pNTu8L55))<;VwLGsg9$NVDR=i4}y3$8M=q;r0 zBnlzKD)(oTYqk)#a9Xp6x?x^#dtGu3=d{k9*3`JV72z)zgm}T3~i^yUyG zys;$M>~2AI`l>L^`PLU|%9v>3T+DaUST2!;MqnFYVD>WbU4NkJuw)?rW;|d;O;yCI5s`#C z5Yvc9n|38v>N&e{2&YPUy*vR+{R*UCZvb%st2^sE4HOe>g8(omFcLWW&w+&8Wfv;I z8Bkyk@S|{=2r|)vnc#|)iw*dS&t-ZbF|e+(R)2I^wm_Px_PAHhAgDo*NNgOm67J{i z_xityz&_7|tgUuy7>gV2x?Xu^4S|b}7U(l}5Ichw>}jM&d+)^&w>9m(?y(>q2va%R z<|Q3p3Z&oA*IzyDM5tWutoQdj^+U#-lv|h^yPHey+CTEszkb2#mcM#FGnl1eIF`g@QA^ZjR0hcF2X228)_*G< zqTD$~2e+4MT&idvjvEp+KHpjA2rOtb=0kOKpyh> zisfRz5^4*MkR9ZN!RI8DVzg$BwTcNGIx+xgiqRff=87Tl>kDlfk>pjqD+1;2Q(7Ya zvFpqCup`(Kwnx&x(rWwhBWpVwZxjqo&)cz(yiBI~K&D*KKT^Qzer^!m5!MTv{Mxes@%yyC(}6 zMWmCV)kLl9eVH+e>mO=>iY5;lg+^YC8Qn4Ls99*OO}MjHOl*!WHG%GVoF0aY>oYVc zq>qq$#voXYHlPvcS(iF0hQ~4BZn=2{x>X~YDd|?M5vW{Gu~$I%)U8o0atLq7ghjBQ zBX&f40t=Ub=DP`8^)4I1_lb_(F2HXeoLj$GfXGF9GMmir`oTU7w27oZOQ5AQ|ExvP z1q#{+aZ8k>oM{C$8_}jiqez|g);EIRC^}{faPCl6mi;HZC|0)|Y=~^hv7%%#S-D4; zO1*p^gBN+|tJ!9eb)eX5ULDLkB1?eV)(&!I{VLLMTx7lOY=<{aX`MrrK?2y;_fPKS z^0;oM_{Ya@%^C{dwQ}-o6Ji61#;s;HIb3M&8U}^mG5j>8&CR>pl;;#W1DzwKwtbu%Yv%rxQ`zX;abF_WfE33DMq<^#}U)h$2`Aj z%Go}mXmJ7ZmevxX#B&h~^umetyF49SR9VZc_#QJ>WwwW{Fel#@hS{Lp1fvgA7?u!Y z)YKVtWeiIW*&e+_Z{~^dma!1sVxK?O%H;BeiZE5aX!yBf7{9o5@crw~(L$6H;>=LoNo>Lwx z?|DE!jJfR4w_zu4SPW=_rQ?Y&1T~Y}_1l|j?~F_9ta0@y$ew~zaO+t=XYIU3X!7hl z?w%G9u;uf;`RZnrEL-;d?6Gsuj`cm%Lp&5{lmXhY0|7+(b9MM=&)#0_ew6maZ!Kdg zLZiO)oqHiRZC*1C6XVDm!Z=2nB&y!=DE#CBIe5u3#pwAS$xyuSiW|}V{vS>RYlVjN zqW}<4cE$hypxnvT?AS-=AO2{{cH=t@H(Y#~ZNp_Jbv+(wcE3IlH@09Bn8y;{LW{rE z_|x$k!S}~i^GfXvzj_z5$z*yM5Xx=Ee^L(D>y74KtJT@x%RJxDy0883Tf*L-mn;U| zQ1#ErNIhqo|2hTo^&pU+J{ z@5Zhz-QFL8^Xbb~zt>s6R`2)TUjyiW1OJ7;0=o1JevkKuw?B{ngmu4>9xH1#UiLPxUr}dVPNH ztK9T_zAi8se%?9s1n6IO)hDm@2nBpUwFB>~?0VnZzCOM=ZvB3jM_+GK{oY%T3GH@& z9yaZIJ)Zw`zr9~RUg!0C{Z>hQ4r2EBz79|R`!Btc>(%~svbpQ`z5TM>DPZmQabN8#{v3Wm9{E3TyuAmBtkE2;^xoJ)Yrzw z8~f0MLqA8A&|;h!O6U=$Ro_3tw0r6TU`7H)TUzaK$UQv!^(UV{S6R=tM4OlOw4VA= zKqsJO1)4U{M%L&#eob7Pc`+nDYk%|#<&#vw&6<1+P>KfJm$stWYPak^NVnDi2}=y+;PbsNCt;P3g&_?}L1MRM!W3D38C;X=+` zAn+G%(-Q?0*n<;8WXf!5V9;eCrLY94@?(%(wV{a2z%1DHu5Xv+$^lL*0L7b0Fc@mv+I ziLSo*F?XTU?@U=ltDw%$*D!J4=U@yQ4c7aT zr8c^jKSw=F39)+Va41!*u=9jTUdK9wgNxmcX8`hy*Tc^zV%VF&zzj%s($@R<-I4tF zFeXisBEt|4GM4Ju<||qj<_DPVLZ%Y&(x;SqZ6n}Pyle+I{9Z-HOPd5Jh7O2_5{)5S zv(>_lMJq{AffvEs_4RAf)?=`W%y}a-geF8MdW48sDBB&42f0GQ2Kh@MncA!Z2Iu!j zbk%Hg;?`#TPePHADdt$#7{qZB6UOtx=5h>CkWQ(rFi+h(%gvU>!}*QiafV*+ zJtQZRB3)&JKxh)ea#=|ubVg%k5=r^J*Iuqll1ZSqI8IYwNwO zZSC1+SE3`SNo&!fP+WJZIkU<3O$2a1j3q0=Fd19BFS6WV8>xu4Z$es&T4ZrUF0fpt zC=`&{?c(iW}&qrGxZrH*16I^AI1*!)W?y5VRPFYW;3Ib^5hG|k@TE}?byK%7AR;#6jz(P$ZC`Oh9#kW^B~iLt^9U!1t-LP%xZDYk?7|H$56{7IP;n-KB5W8c3#W4Zqi%X02+0@d<)Jm3Q21QpvqH1CW zl%@DkBVXJ~;*r~EzdNe5)GJED6_mE##?ZzbigB$LtF!48thJT9cHV8Ds(_&aRwp zowLnc-<|O=VSOKajM73fO&nvHt{nWrPw*pt80&wT!KKJ|4)e#wcyrX0meU#>%M+AT zEf$m!@4>Y;OIVbRB~46woeHE5h8zpsEeGDJysn#bPfR&Sq8^hj-&8otApiu=n)d(3 z5cXN`2L=~r30^=oahC(HxKFAgXn6V7qe&!r9olqQ-cd5R6KMDpHkrr&gHhmX#Np89 z#kczqr_4Q*LwjHYXI(aaAY?)skpA1PjMF5`8ylb>I1z@=f-{5Rj|UAa#-2JAgI`!O zR<>rA=BH>^8^hTgW?OFV zV~Ezbc2y^5-Ce|L*|;R96Ud6I8NLydi~VKi5pPo80wvsmo-(Jo3lFtxO6BYF2CpbI zm1!dijJFKw9mtA3`pff*_z*h6e3pfq4~*d@dgOJ=Kl5ho-m|r7qIt3@k|jlw3kZA zB9*jhWn`q@8E2VhUk#GPHk@g6YAunOufj8-m3V>!h*XqYM!F_06Zpv_HxzIk_bMZ{ z$o^^HiUfYzqk^O?^)znhD9OvUid0w>2gD8ldwra{fhJjRCi|Xi`uGip%Q5fO%n2}< z7{IxPkANj4?!VHsEaO({ow<_$1`VIMtP`r}tsBeyA*04^Mo?NE8CHlYFXvWw!1;{9 zM7v;=%qSx#hQc{_hX^^gPoEp8|6U$}Kfs1}(FmX-?m*|*Zn3~2X#j3M)6N1(0H7G& z93l0HE=tV}?{#!mHZ=yOOF!9&6Nt)|ceEAo(8D;nXi2tsj|YTB{tG3Q>vE)Y2t1F^ z)J1k7)$J@^1ecncO#R-Yrrt*1B2XNt+;y;aQ}9l|VT(BNg{i(!5GR(&Rj$*a&d7fI z)18qgaMsv1%l>f9GQ z3>rv4ugjtMMV*);RpFw0be%N*E;KSeBvLz5C`fnCC`7uoNL5>(D=#J(G;>#-L@NW0 zlm(x@klBvW>H}g;boI~ia;FZS1xKJ)lT!Z_mqz2M?(~5`&&12L1+@#@XUo_chiSEG zAQ+#^dXTg;#=?3KDY>y#@9WBpgQPSa5>m?o2+3bpEgleS5aOU@7aE}0ULqc-Y>K!h z*x#W9dPM^fV>2mmA!z3nadgMi!SW!34KP7OCeb!?A)B5$QBudL&+^~^r-X2SPvBm3X3h&0T0{)#Um`$Ar*kpe< z?J4Wj=ShSWCMoL@V`-0JF$_C{iXfI4T9F2&8LsUJNs!@9bL++|;42jE@XaqlTQy@J z0gtMjq5H-xRe31bSY(x3G3oE{6HnY?kSMz|HP{ephho?=u2?qErPSbBE#VogU0JZ_ z>u^=(lZ4`v8dx=9n>V8=0B)wW0lvxvVtS0raAqY&sF)3* zU{0M!KIx^#$%F^+x_KxNX!B^Tk>D5pO`%PiaO616nQEMgrcvO5p4C3)Pv&0Lw`>2I z`O3>l{)y)rCF3?VHY`%gvm?9m);UboH`8BvSa0V?4VDGwutyW#7=yJeTSJEG`{Tf~ zZX(DN{obVgRV$}P&%e61wtU~sl%Vn}(F(NW0nFC0o53a+8!<(Ocu_2!Doq~gzU8s`V5$)y@Ux->D4l371 z#AEAc)j)tH0+T`X{>_VH5nWQfR2T3WD|4xt{rN%=nk8Yyw^h;uiQk0c;D%EQJ1j81 zdh8yJ(F8WABBw%GuJjHv1jHzh;RJ*+W9K!Uqf26J4)2MV7`>!(ysVu)#g+g&)ilR4 zTqdUbXbK$VN4<5&bkl4&-~lbPXQULLBx9| z6bb#;ir1zky71FC+9o;KsWhB6zWl{ZrLcl5IxES=l9mJl>LQWGv#|!0B1(#@{BC5r zpdLiw5+#v!llgsg=gSBPGp7hg{%ph<3w>wQl(@w>5k8P>EOw z5FAF^UsJ!sRxbb5K#4c~APqjv5@cxWWDjZVW&Y_;CFzKxjT3t`ye06~$!{Td10*eR zJZ8Qq_}kN=HEuZ+7dNzlJT5_{i>YYOs=IH0FWrtQs%I-9+k^g6jQ_VWK!g4F6`K1JQ17~&kut58$v4Cys7op9EKQ!8i* z`!N39Kz!4zdUbH+c;W9jz=BzfD;fLy(v9E(vWcZlOqF1&{jTnTHgH4-)nn+fw)*rqknh_PFU^@#&`h$!+v;&+s`P7jqjn)5m zNxou$4Q>hsvN*sfH;x}|mm2psXjK$)Tl#-S!Z1 zS`X9qnVOV0uZ2(rf{|^PCRp=Iw|G$p3?ES{gHuDtA&MDoBl7B>Fi@L7$D-6>*DkT8 zRvwGG9S_wGUUL$TtKP_O;TYe+0t(Xw{{##*Ms%a4qO8B1DW^|PK_xe?h%~v1aZj_1 z=uD7fjzSfl#NKZE#{jFJ_VkQ!A6h)BW65Zn>iT%k%h)Hw+UNw#HKf6uM19FBNdHnz_)NcK(?X3m!d zy@a$)#hSj!!zLgmDxbs*-xw7M9lCQd3p6sTF&)SY41l>ou%(-#h5XfRP;%?HwI^&7 zIlk~T|-5|LWvVxi5aUga%7XV&t%gD4AG+-z1MdLeQP?m`)bV ziT6ZOXXnKnVQGfyff3&lPk_yh9CHfh2h?J4%bQ z=t~*dAh0vRxO#Cd=?jjdkC2rcn)k=!+?$cV5*?Y7kgB?IoDkeK_?mwTz+%uGlzD() zyos{*5qpEN11<^5LrkHcOrie@yS9&Tb;rAQj$qA))$EK3s1{(2<0yUDq{cNNbdoe3 zh2Xq6!OIo=>S|DyyPWA`xsRJR&3c6(;KXTdTJO!eh5(HvhheobQdGd$x9d@%`ep3uVK+KM~U3cf&e zPnAXVcr2^dA0p8_{RZ-F?iBim4mV}6Q?i}V+pj$)G7SEZ@>eHBeqz?IP zw-lQb1TIoICw+uj1*n=+S=2KuX|#`#Ea<(S{5)FDD#XmoRBgCpedADL|B43ahudq)9zLYh9cJlQjHrWGVry(IGzdydjV&J|77ihk9$l z{&@8K&7l__lXsBA6b%6qh!rlW8Lg>BANb8nI4Iu#ZhnKdk4IKWAcaW+1MF{s@jiK^ zP`151UW;oivx=?VnM zba`Nr9!`RsL20DhP$3DrJ}0A%j~S>Y=?Qq@uvIk8gMs^;3=iue0z0@&hICQ)94b=5 zYZ-o1lbLrdj}P!uah!rK0DZ?>-;|IdQGYWaLI3n9hfASm5!6P->>seTNB}aJ(iO~;ZbCW58yt@KTc?Rb&VY>KsI&&Ggi&|FRC1rwgf78Jo_4H z>0*))DBm23wI9wbm3`qm2<~kFy0QGAP*7cgjXp`yf__QJ0|WsN%Z_>*7$cYc>YBBe zGbbCGPMZRTIURNdjtSNyYcI{{`gxB(ac13kqXW{R6*3L@4*H%im11qz6j{J3$z8Lk z6~YgL2>BQK9&C8YnwD7&HtI#yQgNTY$iPjcf?t8|U`P>$!lJACM_-1{Mfh<|LYw!|v83wi?v<6Y|N&%EO zf}Ee}@i-Q&JoZ~C5rGhrcF=F6QQuIMNZ|)6DYp89#R~) zPBy_91OW`@Y~k0&nls*Ir{G@GsPHqIeZH7Lu!yu(Y_m;MdhRZtnG(Y=Ip{x}mW2O@ ze;*p#o_XW#+#OL#M-%AK>Yi}H8v9ZLuQkAY-XMOgW5_$uj9$4|t^6FCDmuO%Nj^vq z0F{Y`U=e#R?8O0z?G6&y-!J1S8nPj6M{w}br!aO<#;F|Am$?p=^HFxM816qpD>J0G zSB(oLGN8F?&Z}S1fyNBeb-oBiQAJJR2ZQN*Bh!R+#<(hDJs`+NHbhH~nzR*Bmy{o% z-zjv_K*QA?y71Bm12;bmW)u37TDx`e#u*z&%rkuhJfX!9Q_W$2+J1?iP%{9Jpfh%7 z07G){a+lXn0S3N&CG_LV)AoG!K!Nkfgo(fw2xW=)9P6Mby7A7W1VV&jm1G{RGN&G3 zPj-O%H$;2^>xmH^yI^RWMHJlyXT%4E(YOX-(&(2cl0fgbV;$TF2r&<#{CQ_P@zfn0u68_ zLJt#f1&6{UPc~>=wRT$;CPO1leyEL zzHY!CT`y;3L+3=^WX@f;=d@K4^2KtbU$gNQVHCPdD4E%h}nn)1h=@XPKAzgYkgyXS1 zd{b?X%*MHGiZVKN-7>8n6%#kS4;5oldOsPeAhjAnN@3XP?MycV39nEnYf(F%2{f?c zzjskxLu?S|mb2q=itd7}ruF-l&phC^G&&k-*&)hMc)Q~>@<%IC` zps&6V@*73p6+w6~`IIpgwo;ipFYX z5E*4jrsNl;rjY_GG4ulzPuElIQuja&-B*q_DdVPc!>kwDHx-|%9C464{N;$)QE{Cg zTLwC4bNGjA@69e$cV>s6+A~I<>d`&Vk<;;!z(!8vwR4h=@zWXURD@+p46r`IvZ%Ru zIbtBB0-53O;BrT0cORgEYbfyGaqHu$XqE0*`+zeHFansSOvRBnI~>SN|3grl>w+t5 z#jOQ0iD5N{${Tfum*6H%Imer7cqnk@RRcZhXD0r)_;xj0>Pydt)Iw<%revDN{aXDV z_jH`U{RoNReq@r-x}MIv$M_Yy0Ut`RkwCp>{ft>{_}kyG6C~d3GAjUO)dtFrr|%82 z=KxtJAZ-s{Ow(n~-51f52HOWseG0zE^Ok%1cxWGRlR`x@bvhOis?DHj5gWvX&|(=t z>@dvO4IJVl<7Y2K4U5JGOS>U4QW2G`f%P<&*}#b~&?f$$fmvDPGTm%1hJEi_6hacn zt+*4|*m4t#0O-h6&_~K{m@a?Q*aB4{)j=DSbc&Bk!pf0X`d}} z!!X1gSmMt0@cCHz`_PO!cnJ9?0Da^>tuWZ)jRAcRX`@<5>w(HUR~1lG-82MUe+Dd| zkPdf24VabVBr4*s0AA>$Sa>MGa+e$c3c(UK=ur#V!$NX`Gu*bEp({ z$;3Dx;b@#OLVTakR88zn7Z$6@9FdE5gL3XCz6K0OC@>;HH|LYQiDM{$OPw5HrA+dD z2Xj{krNkAB5KtIx&LM^WE*Z*jaz|>)$H)>+*I;LyaM8si3c+aJ=)X*ut^z#WLs4w8 z;i)Ao8(e2Gnray_+7S)kcMJ5*ir|45t#@+=R__r63`J3asv{E0gldYx0-dl5tjd21 z=?Jj~b#mw@nCcUvl}9{rQ})g~_;I1HA%=%t*~7Hwi8sAVdfZyLW>V-Wmx6dECim~+4UzQc59h2`{)7qu@on6omO7`DFCQ;4wq ztUE`x>1jW=@a@&n^-vKJuuID$eBt5XeJe#H;;}CXFjC8lJ@xWuV6jiS$nHCZ<6?Qd zn{o75IIS8vY)p@D@PzDUTuhb`xd}sLad-rwb_tuI<-RP~tA~8U^!P~ev*muSlo!N)d|5mB9~7A`bKeSi*z7fJxQP*uz60z;r-?x6g}vxJGlxX1ZjhG z7hfLhru8|+hL|E{C@bgv!Pj8)2Zs%f*xjy>#qjoa5P-J*!^JnD*CtDtcWSeLA@>d- zcqnlX2W{F37&W0Z)V^Mt{)v|Pp`v)^YYD$XM2^j~;RFgU!`0sgj>hiWjIH@c_C|$` z0p!W^8D`}nQNBOxSTlQ|@v5M#d-aJL02!tbF7ZM8kf9Nn z`y*jrE$XwM%iJL%F?5b>Ksa@^t>bXb~qXZ+D9q-9M8N3gP zYYJn}@-{s&ULW+i*jKJ>v>RqZ~|sh>e|L{i=$#1wAsY_B&lxNa^AH9##`Rw zLvtqtk&Q{p-RV}UczX_959WRT8ilSX2P}+7Gzi+B%yl!R$SG6WguKr%+Pk?&W|_4_ zv4?QGaw1D`#YieLp~DcnXM!bkf>UDQDUZG(erzlz)0;N6>sjyUoYwj?MzX=XZm-{0 z^qnG;jTJ!s$P!q*^q}0RW0HSrOfM-QbkE5KA^z(~M=eNr{_>Lf;fxaOX!UfB*Xf(^ zV}FLgkNONMKe1z{d`YXL4cX8p>2MO+8kND$4r5(Yn@?XA0tC+p?_)P zxRuQ!mXQv~9vj#vHZ$jRRz2wmZ_s(8#>~u}2+uhttm`@F`0PhedO1m>{ZuA?b7DKeW*e{~rAPTIDwp$^X-R@eEX z+C}gZbF@HA9R9PN2NUn&w!G)3N+{5%^B7%Y|0h`c5#tjcANnw1ShjqsJRDV4b^1_! zj8P|?HscK+&rKpYKJ@(>Z*PhKH)}Oqq`* zvFO3wNE4Vu9>wS0%%RPYt9y5WjPCs`L?)NqL&KXw7fylKShf zwyJ4Q;h^W)TlMiw%z;4F!pYkX=ds$y4x&m#2~ss{=#=D^Ydy2|wrk{6;~h9nOw$@; zgs(arrVE~uNiOdtXj+leWXeR7lPiS6_J?$giRJrGlV+RI*&W-I6T#z{8$E5@Ju#d^ zLZ*(H5ohQGmHw|F*m7|JulPc!pb*Y)KP6p7RtV`{Nt}oUoP$70)i~W6+AP=G(YRAz zaPO?s0U{;HVFw`=aPGlLcwG=Kps?@eu-*F?5U-&ME$8r3Vj%SCZaaE;rcPmXl?Okg z2N9WFUC+gNZ|k#s)B`Jk!{DVDLy*-DieF^ky2P-Fh9IULN1DTfdAzJXUnJ8xaw~TV--?2EJ7OuEZrc3yjWklW;!qc-Rx#gbJ5Sdb zd0AWJA~_c{Q%Y%|Z>e(E$J7(SoQ5nOQmoAP2k!*Iuu;WHt<^waa*8(&?cx3|sAb;? z2XcA3Y9c@sb?DZ4#yG(cyBJ6D-%cA8;#3{nJPnKdJXC~sNa@c-xlXtOd5$Kx*}qOW zQN{Yn&qYb`tOoJace+kwx9+mqNIGHl~}E#=p8W^h@bIgXrY}liI$=p zCOy~@_+)dfi~fc0A1v(BmdYBi(5Iv>5rdWavcJek1L0Y&w+9|Tu?XmRhg<2TP(9?` zKn(<3D9pKqm5^`42>GD2_>F=caM)_%eFioJg^jWat_ZAIrM6i0da*;MsU~e&fvbK3 zlYQXb!Mv~}tf}99f#T$6gPf4<{i0$W{2mar)LuCgQLH(jOfe9)WTu+h^a~{7hj<2p zQMEK0BBY)bDr&5iqOq&_3nLreMtntlr}v^h{DVAe9L<$u5bliW7BZx?4NU1I!eL}Q zR{OCX%t9GeuUpyNKq%jPquZOjm{U*m%tfNb^`%FZm9<#{wJULKOGx-BKc}HuILbn= zcNH5qj&0@BDXCD67dFwzP0vMXOMKM@ zXLcuR#*!pH%~xR^yke+Xagyo9AKjC=w_E*tOGgN_L3QHzbk4Z6QM8P8H6Kh5@MieZ zy~Z?s)DgA^o~V8bq*KSF*<8kusmDZRJdOAXXWqR%vovV8zc9+)o@IW~TtBx4k$?Yw z!h#+C{wsYmdq=@He5DZoSL(}&CSc+83yv^6=9pI|;eg|_ix~Rb>^pp_8sz&Ar>nrO zg*lnSo8$Ij(PyC? zl;KpP@pW=eKBm8y0FLV?7kn6UvH_~s3|9B5w3kZU6r$~I)@AL~xhf?CBAFA<|!EwM$)XqI<3rV?v;)@yD47sX7Kb}|}IETpI$ zdU{0=BsNWi)kDw@IhM9)4x6pp06ew2Hs=N_J`(*XT4vV{g$`aVS;oQG?^*|`%7{6?FuhmeCj@Oep90+vlG7vaP z`cZ!l3H1={bAD-RN5*DS?o;Y=T$TP6ZR`Bkc)bUd@@dkY%A#H{xIrEp@O@AcZEV$x zOldU!9m}C+BsVxIw8@%)9EXx>nZRnguYBhqQ-3H*UkhBc8ao@o=&T8k<@XBmiLx?( zYqr-{k+Y}jW+EwZhw-DrfS>FQ{WNqTPn$;J`-I6q|~|i3M?N6`;VV-*x-^sPN%&Zzc7)%S2U$rv)wt(*TjI;#Hjc_yTHVw4 zTFoi>xsO_nIsdauUpOQeDX!z!a3j$oIW^1g==!vsk)&N{##7l4bqX{v2+Kl?3!glv za#Ff)|5?`LM>RM0ktd)WBf>Vn)s!%WAemCz7P!z&89CXIU#{I(1Sjd(Dl^AcKhU5K zYdMk?C-{!O=LC{?%$ufWimi(mGcqZB;$M*`0DxI6K7-A z-1o_fu8w7I*uuEVTfoQ=R!|w`a@kv8MVCBmmr_&Gr$+a2K)1D z%g*_iQ^V`7S|Ezs!W6NpyUPIo58A6>#)`0fD4|r9BAMVV^~%nPBC$$tAF_NZzN`Z! z1iOCOG5^laap)PApEy&9W1gh{<1_7|D<-ml7^&lfqR045(*=Dg@FoG?gRR!=qAw?G zYavwdG*6$D;Hg~TPs3^Fh6|QT-613J7g;mE1)n3D_VAza7^h=3mtfM_>oHpybSU*~MWr{y43HN3 zx=mWUac8IMhYLj2sDpfgQ}U}VLoS-s(AS@B=dO!C6J)9V#WiT~R@;yfmVe#-Q)FAc zk{Y^evv(UIE!AEVu9%G44w4gGpH|iV&{k1d2R31+uPlCaR^W5O!3j(yNG}3<^B_LF!WbFq=4iWQmibgvb%u3!oeZ90t-iQIezSd znbr^tG9u^h6$Keoj80+1v23E}vaUAgJ;(}-r$L=yNOUYx3?ByJ7GgN!4c2m$LI4md z4T9&Wt`XB+5t*cp`tZFgitNMp7j_&a@~e9BKiH|2yy5*WC0Lfub>I8WY{t>0|aibC(eR_%Bzb|sUCKyec7UfMRJQfe!y!AUy}Z#$IGL#bhK6^%OG&6b ztqRO|WqaZ(>U;UXBnU;7To6MeK>8j;8@;z_!`no}^i6sDp3YXcZ>qI{ywgce^Sd6- z>*^e!m6XtGG@mq^+HDZkz@YX5hRLPOM6&x>mLUSLnm zZIOw?-)M+xd$6YYgF!7mmwW%9!<1|CKicjo*0#S(7xlDl+qP}2Y1_7KdrjN6ZQC~1 zv~8UA|GuhocI~8c_C@WUjP#z7k&K)7=GS^_Pt(BATe*Cxs9^dA4#}__c3#HEZj@cT z6DB5ULPjlT>jEaHTBq}0|D%CHzH~()Zsewh13IP*Qo3g(?|_2_#b~2~E9qQ1dE$tf zXT?KZ9_Vjmz^OI#?9X23IEGf|)?edB(meULezkpTPqtC;ol1kiEfN{IPGu0gupfW6 zgMvk`yd|vWcmpN4CWB6GH6*S}nP`xoCabA2LeX#_O*B~LtdB+K(UcRS$ShaogbbC- zZ4nLq`)_6PRE@Fv9yG>|dp4>gIg~xP(wV5^z6Ww^Scti>k?C6?0&r0XO9xSmHJ$VY z3geerc?ouH{;}(og#Rqdt_CcAF_?gyvWxt{oUc$Q3bcw*WD7U3&p~~(CoFD`{kD;HUFyK%8lXz@vLK1(AZK9S5gbJZ zY9o1_rYV&e0VE7s`c`ol`;SGgiTcJ%DYH-q@LhE1Scc}n&KAQEmWfrxne^e)Te`yW z_!TJN+=^l3ZuQ6;`f_!G3Jqr0?DkhbbA}aI26#44l;0*n;p%Iy4;EP{V`W_T`A>z=gBy>9_ zC3S%IMs0eRfp733myGf{LXbH3QmE}iAv6KlIDkCb^@c$rI%e|aoa+jv`n?2MwoDjD z1BmmT%<4HUxvlQ8o&4(hXrQc96Pj)cPRK*?P-gr0F0Zm*>dZ{WR^n#<#$aO? zyxk5035CmhLA5+k<)ex@)^;3Z9vBc3jUii{0Og|s0Ave3n%OPn@b++teU!4%+{X76 zg$u({TR)2BbwSwOhO!=l3*M`?-WO*TpUf&1iuyTc?Shp!bm_NI?!lOG0N~#W6C!;@ zt06RyXgsJbH!G!f;GD+X?_ok$6qhKPQGAE9K=yQIVA7F7L#lxY3a}&v-j1{RR=F^Z zV?Y%S3;A$M{EopvX#PObi$|1e)w71!z2UOc`89lt9%Z}$fw#$yMhRO{kGL7jX9j9@ znO9IJqx6`83FalJ_`FT~c{s5mr4KxeVjGMW;2Wz;Dn`g3)bF`M-aI4KvsL}2vd`&B zBbG!lWZ*h-Tq2@Ov2e{NE~-6|V<+KHR60yzkn6XZ$nJWEVR8;PZUT=Sjp9ZcpaYRJ zXW;v2dyZ}YJ><5PyhQ9Nwk%UfSw=f}`e88LT>6uw$O+2{OqWDGuIZ)nF)t!zxpY$B zr4bFB=NK(XeWScC9@sIF%+HKId(fPCDG(J83ggvg<)q~iX6{IPwWWy}2uV~FLxTho zZ*)sO98eJKu7QZ++M^sH-jxZ`O3G<6U1%keMgoKz&a+=g;Z>&lH{~)1LI=p#Q^avt z7l8Icuj#fMADBG7!Ecf#QH5yAJl^uUN#I=6kxK-kFb)q?a*3DVmco&#ENFY<6>Wf8 zMfZH=M2oKSJp;@jGvL%YJ&=|7J>-&E6xw7K$F*9g*`;G(FZj^UCwM> z5);ETzeYSW8wxN~MN)u=)?%YzLKSJhd>KHVq&QlYFy_`7i@!X^>M$T#=#t-elaIJM%u_Y^(k4 zSoYCIdmeIZbS91P%(iYIve9L;hd{)Oq#KS&pi+sR%$!rYN_{MBcVUYinL3M~{41<( ztzCynD0ZN&s1Iy~KY&&#_a~6a56Mp#96qOj-rHuPOQiI_lzxIfDCHdyT|B|191mnM zx4_pf1)$)fgc8MzJw#~tZ$g1l>CV}dUx^DHsUiWqwu!aN5N>8maS`LOv`3hMJ08ej z)Uo%_zzDC|H&n{h=jL&((vY1i2dPd~2Q1zEAv%)lHkuv%VWHysEl5k50SAv9LC-v8 z0z{Pmh(`-edv)mH%HlCIeqZI$0ol5!Yv4ZmGcbpSPE5j`5sB1*EI07|Z3fD_mo>`L zB`Z2$Yx~BIH@;NT?FiUt=n6ceo5?@6Y0pcLy+*+>%LAVW+yc1?b|P}V3vsna-(OhG zrl!GQ7e5gqU>7$Forwny;!t<{7?4I%t@)VWNoG8s_V;}3G5|%xx&A1aWUG-gf9TZU z25OxpgdVDZgrgt_=G4ahD6j8QPDm_phielhO!KRAV+;Y`?g90r5<^_ePG`MSiNNwk zjTmV26vt*Xyg8?%0=q)A8j@)acmg$`b2iQ=-#Cajd980oR1;MPM@QYOpf1{~m(UVH z0(R$E@IzKdA~by8237<{s#pY~St&8YtdmvKK1c?U@ogGNI7e&rxP3#$ZlMsGIG4}} zPm*~NpHm$Y)ws>19hd;C(g*5;0z6okUCIkA6-(O}g=>?MPlZ9Cm#{cH^p#oF0$@a= zB{g)9NK)bO(%++cASxTHT)5l<=B;5k2mdV7$OlGS)C<8dAd0^X$poiEECb$3BI;iB zTEryIx2j|mL}|qZqPNRUm~?=FdS{#QoXU*1hu7BK;rp7mv*`Xw$Udwv+W5sJglYzb zy>?xtIY(4|SCGlS=VK5R8{(S9cj6JOW64nS$AadiWYcXcVob;xT`qS9qLHL|+jTPV zJdz1@_T7wf%32&;qjD;0Pcsn}@riU$Y-;$Ak4-#4UMzi6aj16|p3WqrgZds?fL`zd zmV-QrD{Re+5)T6F7_XR%;FiO}?ss}i1>Hkr+n94c;DIn$XN@C?Lq;R=<~Tza;$YVb ztSrTriQp$2Yo6KFLle{-Hws`KI*>4e#jT-&+Jr{irvWEUZkG<(e37Vv!CuSRW2t;+ zL+lT5t)L)Ks(dFxGY=flXjkkeLNkrg?1i1m;)9CO{K%RzuyYtFgbybA_8o$&(IxsD zXGhK-IF;Fpk)sOiRN`TP)|nKp+OXgZNISJBb9`+uh`(p|!|MiP{ds+n;9V;vx#KKy8F8sd zBwXSxIi=PeAIGP47%W@+^`oSSm@oW;89Te4zKox=x{2sv0?81M1m?%M^*r^->g-*K zA6hAhiWf}YOmpN^mgG{Ug1BzFc`C(=X%~+f=n8b?Ya==<6U?N6xs+oU?6)~4aIA@~ zU0{n;gEQ&%d)`VK@(e+O%}K7o0aMdSwp`O&M=})%e_S&sMhqW=imj)=qO#n}=Y-wB zSRGL?#*fE_2?<}_xt&e5Eloi_@&?-+vR3!*4RZ<(yD;5P+3a)J>5O%!Fx>S_Y?;jM zi%%Y{S?N+)z~sw&2Tzt z$}r$q!A4e?jaT+~xiL!(>c}T%&MOD)T1CGRv=d;t72PK1Tm3N@V$+7UttfH_`DSV3 zu+kS-j1T4qS3W?09;mLFA+w&^@*o1~y{V`f^yqHHR63NiLfjJy(#&Q$D}U|exS4v1 z9^}w-hFV%6Vl9(}O(tqkcW%rs#LfJf@WCD3Qem#}V**~(K{FfJcc@*Cx93Qdjvbe+ zsO4K$XgA=?{0q+ zZY<}wJBDJWDdc&pAld88$a{k9)0Gxw50ndIS&TPOjc`9vi<^An){%AkWTAff1Nb8u z)V;Uw!7MTVLr&e!BeyXcgL^Lb>+s6Ao6R@pFZb8UFImMCtA8T7GRuP>i=Lyc#1CIm z4s$+B$Le`f{L-!m-sY7(yZdz8eUi;=T5HXJt?BmAlRM#3Gog?r?$%MtDqNN`-6j@G3HPoFz;P?DFfQv3Q^@pHc&x`@QV-@Ln60!C|w7<&`TTOtd;!@3; zSS#aXa98){EXrA!@G`(FvEY1)Z0B-eFM9QMvDe22H)2y5a}3EuYTqXOuhxE&cS{Rd># zJEORpe?Q&fM?^sz_Ld>_$xC>a`(U>JLyrIT=!kp?s{0SzDjv8nuG_9CSm)g$Qc2IK z^v16cte!vS6Qhm2ovG`Y59VODk>r~(@o~t6E$Qwdrm&5}{73Pv^h-r#-jb<$E_48>0KqrrhIlCNNXFzV9L7iU&Ti=z2HR*28 z+6443ztO`!VS}99>@!z$=<)4$h2K7}xh|4*Z&uuyv)`0#)@yfOzaF>0riB1H+{$iV z6#UU{3IWv8j^f%6(E}hp6qXf^&czh(!oCjtnu18;M}1}$%if4Q@Vb|_OWe$kInc4UTy9bEo0jf)_dk864bA&;8+4#!ysza5xS%&Cg;;}kKaG2hE_P6IQ45K@n6&QE*od6>(=1^UqC zZws`1B_JDmZFmcPb@cW^3l$eK@42aZmB*sw>bpSI&oICk)6>R<3UhG5U=83X9jxpu zeR@iX7sI>Lull({=E=^mupQHBs6U?>vMCB%`_i=2QL<=j2QC0tyV&dFK6q?>szbxsD(q(OK1jfswjtJcAynN4$ zpY+~7_z6o)4cfcy_#CWd_kJ#Q0qyDrxw*3|htn6P=G8(Uy>FM^c-fc7t>%52Q21T$ zG>*pkJnn(}MgP8)lh@_-x$Jj3g?T6Q{s+x zUfut|9C`t}IYlO?x@ku93t$O+y}{T0P-)Ayv$3)HeR2n;M~~$f`>_{O4ZB1{Z);;i zbnnfxaIME$Q(l1n8!J_ED{7V2E-t_?1YaB};m+liBnh7Mjb0wjC)n+GaQ3{tbMp z1~4NGhQJ7#Bq25_V(|7cVLm8%u-mktb|?lbM43D!DqyiwxB>*PPrV9C>;n;Xt9k&Q#I0ha;F{n;V%VyA`GGn_Au$$GWJ z&7&X18ESq7^5z0pg^vVZg`e1X=2(mu%vnr+_6t+?5mnENQg{CwQT!04!{<+3x3!Dd z6b8TlUe4XuIe-(Yz3}Raim)<>5%T$j{093g=ZgPdlXJ)ZcR82f|BjrS_^R@#`yX=d zpMT`s!T$p}_Y>CLj$Z;{Do+)FN1{q(ml#RG%_+TrgcAu+Inv(%t>8iHJzX1S*hW zk*D|+m75yU->2e@4^2nvDKMw8s8&9{=(Z^>!Gbe;0LrN2ulpNcC?tu1n=UUz0_2RC zRu+j=K8-GtzMPy`-7JY?@>4w z==%ixmi>4l?d*Wbx(n(T*k;kxjl)u(n>olSbVpK*eiojYHrfNK-LV${T0OY?89YW_ zC0MkkO}u7uBh9F8wI%_I{H*v9M^Q8G&{?^oWLm0kSj$kIi-|aq!muFo^)yuT~@}`Lv9TCJ53S{xUXGVR$f&! zVQ8B~y7?T2S0&b9?>b7=mt`#C==FF zSH|jtXV4CS8J;a}h8xD*x4mIXFGw3in#H>sv_Y8uCCrfTGu z#nI+bbMrk`PZxVe#%Snu(MxD^dPo@pr=#jTO*cn+W(s@n@Y=E$R0yv+lCoq;!zaZV zj&=NVzvPZrHBSocg%4EzXVk@?bS+-wFD$Rm6`*rzxP8a7sVdT=UL7}It3sntvVyht`vTXcnOn-%! zz6eS=*1OBw6)&*7Lqo17efr(N?oT>(`CKu5k3R+?gYGI-q{gU!)E2VO;=vV%ww!4CjJDIs7n%ve2^!fT=b)S|q}DkdY25{8-nc z0*+vga5KY5$JikO+yZ_|5S2?$orB*D<9FyRT5}~%%OlEzbR|keu2l_Gsj=J%Xe`b` zKETk?5->|FDl}#TWEL`Nw9=$pD~$2RG-nmjg7Yc@eE|Z%8W4(7%t{r(pmPNnC$h&B zUim9Ica5G$RNLmqP*QZLER}AD3uoHz5gfz0WpW0LCX*E-!xojgc5+8o-5EK_B-^M- zDCUY}lqNK%OyMLBGsO(Jr;J)!6Ju_sC`1Z@SHol zvNRx8eGmfLX?u}t(V`}!aZd~Ob7#P?ycp!s`f1zCQ*>_J#IXBc3pV9gJ0iwfY zQ#*6nx)BXf;qc|20;qjNjLO8hNWmL;T6A84d0hCjZT-h#61EK@&M`&Wun3?iC`xO6 zDsoO2oONyjsp^)fM#FU|^7u!N`4t{5IiS47T2nJzl+-vf`%XP1X;P}LAeRe})P8&C zm1xiI1!=iBu+MIuBln$yNMiyz3NAePkQLP7iQS$yZ|>!i&q?~pD5b!JsVyvjk8oNt zN9pM15lj=Uk|SaWGfJ|h$=l>)Ysa_w(j!VDAzp7?I(Cbw{=`)Z4tn=WSm3L(ZR!s@A_qN}?;F%6h!!^w+@1%K8Pt!Y@dt@DN)B7<2ahoWrkoRjwR24I5cdUeejLxc?*?b|*%Ih>5T3Cf!#ZNA`Z_>Jebf2 zYvZzg)HE=>XtbF#-K~3GZ->5!>JT`C?3ISg#ZYpv#CgzzyQzWVHxuc6D0OcOA${5O zcKV@uo+f(yT4GW&jXAORfvZ;{ceZ*L?z4`8Wk#*4t3JCph}VzvHC&#-HRwGF?i-TV z*CQ*Ke=VESgP=}z7?tF~>1|-Pq~_iFOwdKz6kcO$H)~B&S6d6fv1n1#L}JVt^U^>B zH7TWUcUe!*XbRxRpL$B87?;ZCv&1G^|KW-{o(FWsVECeQhM|F|!vQF+H+T?>u0N*< zujPt zJa}B@7*4hSPdRt;zms#hWJl$YvZd^a1Gade6G-$6&G~Qf&1HXwE;CB2SeSuJ+Vlbg znS=$U{B05y?UOnmktIcEBCr*JifVmIg5-H6qOY41H3Jr_K;IKi!)hw`0HI`i$FYP%q9Hb#@B1)&}%#y0Y%_zsR|5 z|H!%EiQdrvMb5Rh|F@jWEpG+>kDLoSffMw<$hme%Y(aXP%p$rY+$pZ(7v`xv*jIsf zi~An~yjwr>er^Etn8<# z%Ok2pm=VhA!$V=if1e>VBSwMaIqCqK1)c@X2SfE2yxF@DT|sn^N3d+`*T4`GhNBw@ zDza>*OS~0y&Jva=^Oa>HtKUO+7*9eCA@&sj=R%_a?qk`HiwwAr0SZB*3^+v!F$QNF zLwrc=B>@iCAtc$rwuRIKA=DZL$(b8)Lf~FMQ0lrnee2BiJimU3 z`*x$lS+Y8FuI4lU^^2Y^Wx^}9?fUrn>w-ZJj^UBbncWk=BOEWv#CFW1eMA)D<&OJ! ztqy!2iHr^Q4~6rdH%WipJVy;H6jEZCg9&Q=?@A9e!B>A!o46S7nqzT5_t3&J-taEK zpyO^2d+2^a|NbBko@bhq{I*}7(8;3U+nl^QD-gEyIo=fgQAivOc^Ppud(cC%sfC*ArT!U zT+5jr+RUNVpRcCm@ri8a!Dnk7yWm|~F&wGSV$I)WrfLI%DyJUJVlg9x$GS8eeS%?Q z3UkN=c%+YQ(7F%VSZDd>`M_F6MD5tDp;KvNEjQP%`NKs>qR|W!u_v#k7@ye)1K{ zqwJ0pu^%S;B{q@bUj_9@_ZaLR)_ccLb&cEb<1KLwHjCZ9EQNKjCC0IiERuV+LLhL%Rfq z2%gn-ImC}-_Hup6M@LUjuiafR8Xgah9zJh(T4-pt9yoEN)>$BIp zpAMu`M*~}O2PlJd?6LMsxR|BbR#tjb!?_1$qqf7?b!gXmDYfoNn@f=d2$^eSg9>)? z(04NQ_lbH5&NLf`Yaj`>eG7d!J`aPF`F7S2sY{s+!|{qNvh z(*F<6jk*>b?Kk6qz0r8(2XdahYb>430o}v3ck@B-z~6?}UGbpEX}kFLiUX{}YVD+- znH7#g7HFqC?PfcKzU^`7a?nAqnkEx*<@)WKZvw8^r${~(>w`%WNhl+ z@h~l0x^Cl^w``}mL(6dI;SAUlS9&%iEzp|9;kS>6QM73JbNp{p_o^3^Mu5C}hmP%QdTqj$<)}zYvii>g*T3CPJJ}f~< zfXCX>>kqx}pUkGFjK3B7Q7~+zK>bQG6#Xn9Fe!f|07=Q@WZ;tZ{x%pG zEGa81l9J~US%zxJ5oUcKRrjQj%Hn$JUqL_Y=tuavdVm7b+;-4uC13!14SkAfPpVnh z8tK@XfB^3Kb!mR>9(apy3f1~VzpUQnWC8ra48RynO>T}s7mxL`;6o)4={ShF#g)`? z_(pn+*kgpFgemd;I`%CyCI%a0CJqAz_~BtDE~L$aq(W+mhqSf^6qN9^ z6M+LEm;qqbvqgj;hYbt%zn@o`UmJ(G zUs`o*eS#7}!V?=20FH1}x(7~CWeC^bJ&Qf@VJ3(qOol(?9(#LOkC+kzciIc93=WFN zq)YHOMOT<^K7ysixr(oOf#BpjalW?U2p8 z)8OE(q3_~o9>+$7LucU>%RG9k59zuk$U@oYQp%{9td)c988SHzY(9!J3>I$C@KW4( zcs^oRvWAVAJ)(Y|mSY^drdYeEV-5`3ChSs+%}uePi7D}_uKeTuB4bl+R=eND=SF z%_r1%2uqpp!XaTdK#^3glv5Bz_Oj}GLhGbtqWZYxQr4O4A-gO0U^Niu!dHm|2bDtM zlXXOtglCQC)NliEEdpG6Gw14&bvxL>0NyU?gvuknomLn@69c^1Z4Ly)h#o~CM1~~k zj6tl{;f6mp9oNL^D_x{vuA`*Uc|gogb~!zzuu>R_V=9%hd_G-}2(MmO7XHnI<%imEDgU0I&9w)DpLZx@;)Zjn`m(HYnaJks1YwWiwOWt)|ADY^vNhFTO1qymFCc7|ZH-?7%PVGhwL`k#?74a~~>Hs>~Jcwk9;s+8oi7kj( zQYBc#n_WNRcymUZA$YpFJfFuI0MP_)3R?v)n97MDm0&q4E4N( zyDS|Y48_%<@D1`pku=4~Li2VMqg0tg8EA3|T7a&vG$di+bq|E^M0qsLKCZKN(@`X- zg)UQ3(4R(=^yH7;V{EEtLh1!|GUo9S*_cnhVg5*cpT0zEp5nd%VM#G*)|f=Mrj$l`YTM@nz=C;(b7~3AU28z_&^-chgpdCa4B4lAbin?285S+t*-jK{u?eLgsNHAfg(!Pq&H8 zaZ~c^_sSUHqSht!9=n|5`{3fAsa4s76iQC(+AG!YQ*xjtSQO=4J8mdvD4YIFi3!Cc zpgIWDIiEi9Ig7GUTN<}$wjt(JjKmtOQC}t0>aIZ=CnV79^1PwdqAvuKeelLqvN2zR zW2bj@;C(fBeY_7moT7W2RNrPHx#DD36xJ0LP@&spZ^+(DTmKyitQ2u#mGQJa#z(ya zz7ZQ57=iV;T0u0bARB6uj^UQU8+{41$$FetxTo?(wzGPiM5##{J`JDVPm4Y{qslC2 z_eF~*1V+h3jqY$Lr!8%_IdmudyJy6@pedD{qP#)zO>^u=^-SyW zS1dRjT2>2iQF!mr`5*;7zU%hy4@??90OZweR_zV!M7DGn z3+BiGZ7 z3EzzTs9MUPfsnvwJy|*WpOkCbw@6nyLY!*#_3Lwai>JA#AB`h3gJhGALc9`u(w5lN z=w%PBvw*n3I3r{_@!8jHDk4==bk=#Y8k4u*7lOKTS1sI$iGox`E8lHFU&!#4*FAcH zIX4%&7zsCu3;I{#H6|T~4Z=2>M~o2NdCXAw6>bZ5&YT@w-mjA(_=i&+oNHf84!GXa zek}8{v=b2nfsxiTew!leIY>k(~RN82ZvoJqg-u-n#CXylIE5qfznr&Wx z2d5NhGF2W{G4wa=>|pZcT!pT?e)@lB1A{zF9!fgX_4_D2ckAQq(lpM9eYP8pB)7YN zW6}xZJM%o#m;%eANvUBNyjH*EB8N9YU-NX$Nmb^HnNIaEp+P(>ckYeDdcd~I7=r>8ogYRap}-ONr!NS!K255Q|yGK+kI7I zEoghJ?3DhjyAH9mQgb^EwW{>zgm&Hw-nC$x7gR1jOh60Re?!85jsPvTwJbWoFa(s( z-==tv9lWFuGOzDGp9!wK{F~e)Irr(k?#wBw(V6arsFml(kO{jlz2FU(ibaqIz6kTI z63TjQtEq1fZZ%Ot?A^x}t!Kjf4XtPPQMTcQjNpT+=Dzu%eR>FAniLbc<_>IBVn9VSK{I&Rl-zp&P*FXgWe5g9Q-Pnx z$+1;e3)3ZRrQOpQ3procB8dQE32Wzqen1+5fPDyv50#`%&O>$3x< z6kcbLFeIS|lTDB1AQ{|35*D9F#b{Oi1h(OnhG6Br*z&B6mw;qAC3g^dFMYZ)T13{P zb!;_BFT8W<@{OvIu!=?SOTNkw*L@{P+?qmYpS{|2zc>H(OueZ7Mun{BstlK)CEO1| z{5!K~4T-^> zak61fn=A%h?}u_XgunKH1il6}seHm2iNhZJE4rpYwbfK(FqoOOosiR_MA?OS5skpMB(3oC`CSDOhmp zf?Q^7QpWA&&a4U?DL9tWIEe9v1ZT`35Js!R%_io(+Ej8}xHs7gm|Pg6JUU{yMQcomzXmpKtV@z-noM zJGZcW9U`M%_7S5t2fzso{#zv%nE5o}w!_S|{e_`fp1ABT%hjLK-vmK4oe)X%YuxBn z8*ZNmgLD8{^v#QDt5A4$rT-h(qK_J5_F~m}_5x3ctHIMr>ZjYUZ%Xp|+#&c!!xV3L zce9tCm3P_3PdZ5CI%`+vI0m+Ctw95Ko-kG8yrpaf`uX&wzO|jd$~qCQX~T0Ohrk&D z<@@cz<3m0p-pSSJBKbIriL6A)n(Gfl6Onh46cuBz>2c^wz>mJ^j~E(IT6u%RrY%{X zYN{d!EFJ&iM35)@m1l7`EK95_jc^Tz;lL>lY)bZCU^mLsL zC4aBXK92D#TA<^&a|!9M7;qipc(yu^aU8X@T+d(6*}{`rB{1tUG4FwcAB^&RjA0xZ z&McX6BEjitBFdyO7>K$*+y36>L2DG?A3@fuyNCj9h-WY{uT3Jz^HFRBJ0eN{a9+yZ z5%6IeB-UB|j_natnm4=^l2&{A2a7dY|&Ud>rmUTspmz$_AKwVatw&$LnrPtm6rEwF|vwLUaD7EVa&1mxW$=iGb?O0y2W zQ^$|X8%6J`BHJi|2Htm2&2WR;b~>X;e{yIG6rB69Gy|~wD#&Shtzn^)z4utch*$=5 zR9ap25hcdYTL>M1z&CLUh9Lecg z^loFTPjRwSG-o~@M>I%9wBU9zwiIc~&(WUYn4vOiy3sm+v2-K0>2P|^W6ARA!#` z6ZZqB{Wjj~CmbO6o;^8YHg`d9=n$GZVb+*hogh?0gg@Uiyk4&_6fqCF`U!hi26*GI;dyLdq zr_gxqXm-_A;(0aOOjr zvf|I*Sv~tLqtlh#nm91EL2L4dDOu+!qtdlx5h1$?xU){gfSpFwzNzEkPPg405Z9Rq zC~EyiU%}tGHB$K3sb(VAwK{su{4s~Vy;2#ri_5y}_Is935WIuE?m3+3EU7$)IWjpq zw9Pcr$NUt?a-*oog?NnCEig#33Fs1aw$Pu14{3skHRc|9z)wWI&KotyW5R#3Ev6ul znFP8|Jpb&;SzGqA7c*8(x6Yodq8ZDGhB>;Ofi{h83E!th42TLzaY3LpmSjPsMs{gn zIy~~P6WbpI2LV+cC?=WX*h!)NeVta%HPb{yu1s z5BMs^P}#c>#e4EAa;%xQo^Bq0nBt|6AY}JVwdYcD-WbqO=Q5v;7d`0{S@9ey znG4{G|BcvxV~z65_A$QOcadgxb?zpfXL^ivDCgvrG=4k`$<>_Xpl_? zjF{g|I1v6d?ItVgq||NPWYjF@Ncr$0#5{#uVenBXMBQ3@{v>r=%5Q$`Rvh=uYstIY zT`Nj0>>N@%!-un6e_`sPEla}nHVYQI-{^+2V{N$`{;86*kfCq?(ayBtb?mLFSJJPn zaz{j_uiDKc&aOpRpHjrIpT^Ng8J<2swXp{e>HcsXtaXyEAh4DzhKNNkpq_uQ1|!#sL>v_VI$&*)-T>tXgLTouMA)ZO3P<{>|* zV=SqKGCH%C4m{Nsuojy2WkcLm$vsgsVd2rxLUzXLQ*AVclMj0+2l21G@PYxajsnyZ zHl2MnPLzmz(Rs!zTk7`4wQ|L2QTthCpR;3oUXXK*b?7$(B;~|5dXZ=2ekVJ%)a{CG z1S3US?l3NUxDI6!G$RR2n8`tlJ_+*X>la2--=M`}FdH*Trx+aUuuS$dS$`xi6Zqp& zzlC9Gvg3Jqgx|iRws*B?Q^dD^Wl7Me#9JQEehl*9b)#RpZ&JgkaQ9fsohPAl^H`mK z+z-uQP*i#HmXKG>Ztr0D^!Yww$P$My(CO`G`#un0*yTO3(oy0DU?yIOG3D7SMl=fl&{JaZoQ`07C>d;|a&4^mo4j zOCB>r2)XyO;3IEemdMfuJFv8>IxsLE0FLho_ z%Ekiiv;ozG1Bgu8^Za!tV@VFYq#G>q(9div02mgxT&d8H zm`$HemB-CFp)In6mzQcXlpt8OEcejh?a5OKF$qJv9z&?{_WQD-aLpFRRN`T*zzbY zy?yf>U9TGzf;F0H)Qct2k7tMlJjn$@;QMOU&K?GZ1`6O26CK?H^c+)OY=3UPjcVoI3gO)4%ym|%FJ$*|3^(ys@$H$o z`aAx=``Q0i@&8rY|L6Xnm+;>QTRUSDJsUe?7i$wIdS?@7a}!5eV@ErCdSg2yCwey% zYa=@w6M7SO0~`B)e&J+iWMN=U>tt@=Xklwc?_}WQME_qK=0o9c0*3(s0KCBf0A&AX z{y@*d$j;V@&dJEhDI{suwvYi~WOkntp_@f9@mlDoB<$J*{sYd7(H($#MA$|tu%hQ9 z!-gm>-oBmjoALelVf3>?VO?jMN`gOO`mGq*k*g@ZtWH>YUAlp>gR3YgWp&-xk6Y>Z z*}DV0Y5iI2@JUL5R(74OTET5f*K97ikMkDyIqz~wxTRZ?Ei)PIxVydM^p01X^?f&Lt3 zy060_yVEbKl;(@4k(2@VH#8f0l?Qh43IQa39elHypwKRpnx4@O?#@+y?ky745>k z3r~DrZm3}gmWxZ~_Ey2+C#G3dByhZOSzRR9K5OiU*88fghst0}9^8hl#tcZ-VV}oM|FY=mg0@%MwwM8{FkecX!mk z-!~X;D~gOI@YwfnacRsN3*J}Oa|Omc`V?U`MO#T-Sp3ff=AQu5{zqnM|3=!zg!aD%m+qh7POEL&ZZg35yr@B}(`uZI zM6_D7GU(^loxpD9tvS;ur@%zmX8L@QY&vTR=P>;}8b(^Z@{jymuK3s7T{|RIdKwTFuiY`|JG)+glc2T(~?mroQC)YDiNG zHneZ9F|4lf@HmI+qP}v zY}>YN+qP}nwr$(C)&KMkZbbJBtkH_9h^)-~Iw+D(XaN=U52tUj;5JK`sWuaVzTe`r zfIdGw459{fzTMx{o?o8=l^b(c0Dh*|##G@n+d#Y+X#59eGJ_dqFxea2jcR2|Q0z)q zSn)X5l`E4UEmpoF-qoh4iHP;?f?MvTB#dW`s9 zfn5rFneAJdWoi`Wm3T|l#*G5HsY)x@VmO8ae3|=8V$OQ7$fcaL1I+wpi#~60D&?qH zKcn zt1>h(UZz^6seQ88=+0by;5K^M<&c4tfhl(>nlRer#@aV@;T+xYB>#Yt^XSAZcx(o) zd&)QMh?r5Vvt00|qBL^#V7|An`S}xfSP=g505PYHU;F~35G~RLj#J~}*>*Q@_)CAs zaI*-nlN^83EAs?edtf$f@zVcD^t-0)VdbA5K!15A-f~xB&Lq0t_)E9D+J$}r{{IV# z_J2JNENl$S{##G~?_?Q7Q>Umu1_XdS0R#~GzdmJRZDHUC!3nm8s3HoKq7fZ$;5i{3rW_EY( zF|{->F){U3niaN0`=@pAFhI9fw+H#ui|dY^=exHsLDxXQMsn!tEa@)G$;Zh*xH-v3 z$jw7C$8dp{X%hg1(J2SL%M+OE58ustQIAsKEd$py_seV2U z7NRdH=S)rvc2xm64RAfk8#dAL4m@B)uZGxEsTbgfX5;X?70Ep>e^* z#c4qmDG{xBu8u@I<G4ZjdU~WJ}X7*vmZ~yOE zw7MjLEHP?gM}fI3P<;!fjv}DHEI9pOna572?CfP=Mr3NNNG4c>k_21cl7bPcwF%k99?FIIwu17t&(129&F#%Oi1?w|$ z!Gj|KVce6(ObA9@I6r(TKf9vUU6|@j`PZWR>*;yXl?IURpu*z&wxWQ10BjYF*6?@k zgafw9G!ni$XZjq4b`1<;fPi!Hv(>5!Dh4gh(;^Mt8zAq}&C55*t20A*$P|Pb!mHC}uak;*C;oL$caj=T_ zjrqy+be`G9CPxW5*U4I4QoCa)yK%Sc^P9Kk~X7p%5}qEd3K-~kzF`&zhc<`z)A z2ApcD?{yAAf4fbn zocHxSa)w2(30c>{hobndU0XvPL*gp=Y-K@Z<111MT8KOxFLK)yMiQ*3_Trs*$EL-Z zC7`^?wo(la2=Ct?z{|I<`JfJJo})UCRab)I zXga&#ShNOYU^}OBbDb@ct6i@eCF(*dy?X)RSj>cr=gpL<*DLxW;_QD6Wj)`to0Zr! z+nwQF)|}fE-;yJ9i3Yf%8oj@zxI+fvouAGrGtN033cLl@tNV@R_@k%-5k<%rOe%`IowOo(ggBeU=`@`O4X(9?Y(tcp4y ziXVCQ5Ex9K%0b{ zbVB3-{O)4Jfj*|qHqXm+9RIRddh|=cuW$Sng*rv@GMZ!5eMJ}3%GtpIVOLF2BIf?G zuV71d)f6Vg6cLouZ~--7G3v6jdS7}1@2cnZszd8s90dM^5v1&QZ?zF_T<%cwK)}Uh zVJRq?#d-e6m(}si)iHwY0J9rYekRZCKf?x!s63Feb~JpdZWy9y6IN0<)q8H{x^^8O zxbBO$>@Z#{Z@cHcCAMddll1JC+P%%ZeI^*>?Ba9yS?mtzCq2^hCl)AdEluVwe@rT) zuk6&z{dN_4r&g?^PMr;R)aElK!u_I>U{eD!PJF$8!}AnpBd5F6n%1o*6Yo+D{vH%_l!=CS+q->oGgukrZaZB&P6U$}a0hJc&L~hHQ_4(QE?`gwHi%PI;k83s^D+c~Q{^2d$l^e8@yAMV9 zUYTj@=Mq_y8r}3efqCgJ3!OLVZjwB@8PUvaL|cP+GLU^ASa_P|Fw0WS7V!Krj&V4X zXLJN-70iY^`qGJ0)V^wb_Mc2*vKNuC+CPqcbgp+k*D$gGZwbI8hxteUUYOAg3^nsG zHXG4!Q&zfAt>Jrc9IPGKQU{aQf44Os&K++HJCD+1hUZO(hi-_V z0u32KpcglH;2hIB&2Ac_s7|q6)0YjCVE1^e&7YmM4Fj#5oKp(Kma|FDUed5HLH>ci z#SBjwV6UzMSWy4zs}<2rN5`3WZls9tth+0BVau7y za7>qlCNIi~8m24Gy(6b$ACiM8bEtlvB0Y36{=?AEV;N>9*A zaftP}4!RoiG%MhZ3ED2kGi?7N3T8G=&j?_%AQb2awR+~*WwC98Tb9rD3hmvI?E^BQ zr(0}K?`x6tKyihSfNshE;Gmpe*MtXB>*9284Kp_DnF{RE($Y@kZqJd!k1d{w2lase z)4G`H3a-?L*RG>Gvn1E>rMrh5^M}~O=Hjf&M9sqs^)%-CX~O7sg+5(fFa59)lSz0e zJ-!;RYC@0Cb^)+L^l@=yX(+EMpVdE@4-1q(z4Uwojc4u#96v|VTxvLOh3^7sVRdF7 zkhP;3(Z?`^RLQaNr182KowMehg|&XAC3yABm#!{XDfZJs@pfxxTFwugoek!E^$}IW@ zkP(8QVy#bQ3 z6!g3U)$i#RhyrWG%vw1KXy+_$$41T*?vSN8o=91Gvr&c7R8Du;jeEflNRUD%KSp8F zWu?(KzgCdbPi2ko{*KEEzg|Ck9V%_^?r|~b@5K#Qzj2R~k8Ph{p9@xw}>J5@H$`6Fh48iKD_@2-rND_d?^3nZ`x=ECdz&}A$55d#! z5O+cLA5(6wR3vZaY@N6ibV{@{fNCq>sO42EIYuXLnCM94C7*V0<0|-{J*}n6e3i=b z?r6QJl=hF~#5PL|m43VZ`g6R(wZRXIJUWBAhAwoPCn79tIMLFmi9>BwJlif+ZmYj` zE`yc=Uh1sv!Y>$>>^+mQ5ms<{pLc?&fZlsiS<2;lHDZ0n2I(}vCqj%wZy9BJ6$)vw zv3fsdnD2TLVpiAHznKRTp#eC>(-GMAx2pZ5Tuj-H%`taV0-vULqW(Mj3iu5o zxT-4pibwPQn2<~fv`a=3@%_v-T+HJ2%=5iSLF!n4p43kOuvG^RZJWiC%iWoOk`KZ{ z4lAV-bGPaZM|o}m^X{BN`4xra7UyBr|5EZyqZ-y*CR3?h98$*Y~D6QZjG!x;= zkW0!ruEP6~9dPQ(HP+&OK2>(~eX4PJt0qGFGR_}PHfOyFn9P2(m)74j_l(@plK7mS zKm#_-{rPJK($LQ;2JREiqj^QQq<9txC^b_rQ<`&om*~%r*}WYZy^BKoUo)ps=P$L% z>%*gza#(>mDR8A({Z!2U8Hl>LWG0QePUvt4UF<4zNZPvqc@G{#t`4_9qzbSu$yh=$x*Dxv#M%8b94o6snsr4zz)pzA6MK}e|#aToeh zUfjI;d9eG+0yuEzb1>Tr|29pX;CD=6S*4$Q-e!~({sOe#P5#{Lx7!+|4WhgmuAhB{ z)};y|u7Ob*?!CLuog;6Sta26NHpp*c6$x~G+}mu|aUFDBwtn>06(adS$h6~bRByzc zlpiA)BLK8sh=wMEN4L3C=vz#mWr4dCNRg8Zo`?2IkW&@SJ*~$bdsY~GwXD{8q4y|}-5eW=b90!DDc4VpUc=%L!n)nriJmYOJD=#!aFb&cm1TM#%U#a1{!6 zn&*(zAmNZ3{u7U@&;V*IEo`|9GU?x*5mtj}x)0KFLnlg*MqUlnmsnoWO$?OtD6@5C zzXFIpi$Q9{6g@c!#H^eB76n+_>B%|4Ihay`=;@P6L&pWud@fDNVqfPjovVw+|()a$$=kZ}DX9_rFglN^|t5$N5m9OLAqGNsIM3 zU~$4LIQiJAH$VBa(bwnU=c{f0TKYjfofgY_%mN&R1<{{Gp9(PsUO;~khI2v)K+|j;sKee~7-DdtlaNdW+U}hzKxQoUgQq#cI`~KAy^|~; zYy)%`K0ao1#y}QAw7w|vg?%(aa_1!lA*dS3;xTuJ5MVh1`0iCa@#^TvffW$3C0Vv5nRXq`>eY7sJO zCNh@4Au|X7>kzS?t7XJf29gSk)4;v*reRW_Wv4Q*m4RUQ! zi@gpLGCOUMeiFtT3f*$_p00h%pt$AWMxl2o1xD5XC~3`Pib z*b}HEkd--&Ff!(AsX~V6U}@g=co()*!gKmrlc&+31YN;qUW4db^pI~0>&&1z$05a= zG@ai`LPzjXMyp9?+AKUSuLXbO3E40x0ybjOYlDIVTm^6DvmV~_{hF!CR+UB~%|Q^z zPj-5kELAWl*g!;5gjhY$-Hz=1D1g%nh6b^93zV7PB7PcL)y^u#GYJRF?GJS^7Yq^l zA;H3la10HoTMS$vsh@{l$d?O?!Jc+-$x+&1FQ+|A%6G}H+=Rn~lD%}!+~bO~1PlGn zx`qiKa3+kj%pioC|4BfhAYD7o3%%mc9A$u`{*$jWt0j9O1*)3(ii9tW5HS!;cjG7^ zGL?4-_iHmtP6FGb%2iASzYycqP2V~={_3rbgG>57r_qySKd?B}br8(7>%)7pD7KrPfKOWA_QquX zW1Li?J*pEp@t_-+NEcxStqr$sU@4Nb4PO`15{^S(MxV``ZPDN)z;r#ahAx>$0rT&t zn@F(lm{A|y7`6eEvcqnVBY%}DM(I&#$Ou6!4*HjDiPgrpJ*-(g8#%L&ETAhk$jOs+ zDOX4_dqh9B?s*~)W34Us5u5kS7JT<(soBmRhYmfwpa-?Sgdj@hXELbk|5opL?2}}s>_+MALsss)2w}P8%OlYmTjOW zZ@nG{)6)PIW14BtKzPKy8B`i}R)GeO0xi(6@3)Mz;n3`aJWA`ZktMBeTYZoGuVA<% zOCWnqcnc56y%|I%%>}*O7r&((U$5(#b_Vx_lM+z?XR^yFuLm#lHB=8fE>1`!wUg)M zc7W_LzCe#&c`xfMN&x!}+o!9?+q$AM`|Cw&tZ;9-W_$4%abd z47D2%AD1OBzN4q}wMT>rKF|63{QI;oBf08=Ot~q$>gfU!$?0N27jdvG>}#eq&Cu-V_d3!!W31s8odD0#h#^dj^DJdm_{i; zQhlfP`k7#YI*>uzj4L2FxOVZHR+|XUBgA zM5Ll#KmJ!jcarupDwYoq7iE!_rZSd+(Q$CWN4bjYD&<{A=6e@;U72>!=V z_lAan-G^$f78Er9=I91ig)};J+q*`A=(E30B2U zY;%A`^xoGlUmIHBS}k6jr0BoKzN*|92kZ3<6WD0(#Mf4Fz(s=%7r!Z_@A;mC3m#tc zI>YYvuyttPVGt3uc{0j3f?P|FMsd%na(aEjhrwU_6IfV-+lSh8(g#tn<2Nz8 zjyI()&D0*}K__^u8|mJTZD{VEEeXZRN0mKiYGsJ*LT2zUfD4XibL9aSG29nqv76mxY-E-1;o2#2T6 zp+M+tvJC=mX8Sldi%Gy zvc-Y^@;j^X7=f|1yQ&d1%iHh=_7D$%^8DJW^+avUK^?bmUeNZns5J|2CbN9o(GeH zET;Si6tvL07ngXw@&aVoWOqw3kQ!^x`|V1MD>1l^3abbx@!-u&n?9hPZknrK=~p18 zjb0(dhz>QXo@lKI)vt5ru4~9R*yLba&@Wd^gt#%r9zr-G5?rh6GS@TPs@6zHMG7P- zFU-6y>>QEQ%Zz{9gw@Cb8jn;@-k5SMF7$1zn_Wx(GsjxreoOl^p@0gG zr);D@(T_HVNVH(c!j+0G1MCY`Z~-jXqJ+3^;#BuzgZho(+WwN+O)H*VbnCRIX#%-5 zTQt&OZ0l0qAjAZ~H=ySFffG`6LZUMrZUsG7AB8V20zVJzzc zGbaTi0cA8l2Z-|)~(*HUdCxroa~yD2v%n ztihlG0VmU~1Os zHUX%SZ*DSxf8tq1 zgwj?@Yl4yDDr%g}LN|GKPb`r+IW?<7TD~`~BzDfI4>$2@=-94o|Iw{W@T-Q>Hbj;2 z##N@K8Zoa~j0+AjCcghR3-4Lu^O;}ToD=Q+a<6-a9w1T>tMLoySdwzijTN-at%8q> z|HmGWfJfEL@^kTeoMa)24M(4-zhw5u?h9)E-V70DF8C~DX%3-@+f8%-{EZ2i;u1ww zl7=eFoj~mV@$JNtjg$@T^Ta#nAro7RC=zu0=V{*jUw!_qd=&@(w1M*^E<|Y;*cL~6 zij*&p0jHU~T%Zk2_bib>u8xHDAXk3}@8e0uO#SBb`^sb}sl~rEVD=)if8*^htZ?Ge z9GYy1fv-m4Kr;gj6z?kIrrR)#-9Mq!MHibx{?s7dwOE3$WH>i7jThq~BM%d=JUV?q zf*3>e5r7t%U0%UZ!R96wFVe@ycR8O?bb6g7KqM!;xPYUbLV-Ndn`V2y2)YO`g7DMx zNZmP)RVa_$q4Lf#xh%c>kc}#Z5aR(B8Fay?6ySmJ;S)i3IYjkfL>#P!N-Kkx$Z?d2 z+ORZr(Wet`KRriYC;o{_(*~NB5!fd<()E4ueppLcpbWm)luI+s{(4Y`#aCauoIZ4%V zxBZ3j?rGW!WQz{ytw~(g{a^3dgJOAye<+Cj5f-sPhdc@>Ab0QiA1i$mR!k+nt=jJ zH!m6TPx@zo5UfQtMw!^ z_ybVc`w@j?5#N#BCFeN`uF}(VoRf`G8&X7r@EeH(PD%?5Ai)OuJ1pfN_ZAy$I>y*6FMC2^{a=z`4aU7?LqYC?h;m%~0>NklCpKb=JfW>i@l8xH&EQ-L9J4gVd zt}O~~3JT>{FXtw(NXJLk>9!dt6rHv$dc2>}9Rpj-9WV;EhJVKj_6iZKMeT?t8(z$& zynd$T%gb@%Z!58e(6pwxgZ*Ap7gAInU}=1($UV@_UW9F}R5sM=s=AJLKy+nM=v-3C zC7>S*4c-wk%k9#mSNtx^5;S=i{oTF8?=sXbW--+8gk(D+wm#rQ5Lf33<%^y4O5Wh- zLjXIq$L%iknr%aZ^0-!R?6&YvU4o&aJ{aU_7<2zv+rmn`-7W_`ADruN6zmTpkzpdH zU%Iplc$6U)36COdJyV$cIQ)ubKI94iX$-jm>8Bp(^$yZ0(MZh_J8}DOZNHVIDR=H& z4W%JnxP9~<(>7L|A;SZ#;o%xkIY+^v%=`hNeo4uuh1UPjJ>gz|FgFg*kZ%tfXAQP? zg3z83pS>bjea#^d3k%(tz_CbBR!erb13U|91qpl?yM=s*`8lpe*8~!Km3}#B?6l!M zH_%q#G*W!4R`TB=hm!Qp5LYxi*^~HYmrl3}LH`E6b&ej0n$Cx?DuNEt)oiju1gl4; zU=8Dy?I~yFvctg@U(Hg9btWTMy0dyk(un-f@$uHM(N{<3VRO92HHSn4r&fyydZ~MX z$~O+V9_@$&hvWOd#Mc`Gf?}#`xls(rW%^G}pe;-PrS=MlQXc9r2#{ZYI&v`ZDI9rj z5u_Wz?&(W_bYGGECdIZH?&`2flpxvx%}8P_?;0ouBW!oyfDl9naSF1f)nv^34c-v< z%38+`lshvh89g)N3QUl$>s&o{7y~JI6`#c#Iejb!?rK?v2MRF=GAm-cw#5A!PUllp z8tA~~4d^j^}WoteChvv}s%d9d;9fMYZ+0dtyZOaKF3qKotRfdddJkmTU zB`?mHo-kov5gwk`D}m@>aoW`tbdrRpT^bHb^+WZ4XUP7xn^DaEih3DWDw-C=j$eHG z7QLWm3_zb!YkZ4*KMxtEcfld63zWsajy?!gx?{p6L+~-6WW!Ixf^5u~=*J(|x@cn2 zKd9rdJ^F-KrO$jd61u4x<>sTTW}iQ9M0V~pG@jXgJ8px%x{_w>5SfI;ptT^Hh_{(q zsQ>1^8vL$2*50~6yJ?aF?gd4B~_|F*p zn}{*3wG~}m+^8nDdZq%JKNHpS+^Eqn@uN7POKIdLK28H| zYT?M1(F~s^bk8SP8CJfDWUY_lm7apx>i8u}iI2*>ZD{ZpGe#&Cej<%gAZhObfrWNE zCb(|rI*L=ti6!f_)@Q9>d~KX)xb<=C+x%YJSn4n5M#*Qz8LXBcq%?rxBddN71nxO? zR4n5rGjO*ml`uvE6R%1>NFr>j8?4w56jSQ5mTDBC%a{Oj0-5v;jm&z64+xb5)_#bK ziW3z~+@Cr$+2(pJoAB|M3-k-#!f;Ht5qRW2<`mii@fo_R)zZH=LufM`-+MWm&`xIt-iDF z-UDxwvMyXc_gEZecSY)2VIX}6dl*7Xzl|m(%or#<^gKFK3o$K?2Epw&hx7kh3%)*_ zBDy8n*<6jPt1ued4prSg6_a0$JAxjB%Dxhz15rWlvg)p_neUKxZ!O6lAsB8i_+tDC ze%tN1!Jk^vBp+23jPx+h;}ck3kodeA+A|7)sQuF@_^LB8kzvjRf^?HjM`nzngRMC7 zTjwUmx>V(0ZCao#jbO332Zd!FTEpyiOJ47INbm&e8PfZq#BCMBFq9oHJSRawWf&oF zOT&spwOB~xY4sh?fbMx-@T97+EuDw13%B09|9DYLR=_bK`gqG{rQzB^2laP4=Z@7- z0IVk3`;-B`eE=$`?`(cN+UedRB9`%hf_n*JY*{8*7~1%e(SLdBf)aZS3IU&5ng)?U zKb>PGQ#D{i$>>A?C5~VIlIcy%wom+>a+0e7C8hy>20xx+B-f;r2j8q|ByIkJE$Gw( z>KUHnz};;iQb9x!cs^2^DHJa z!QU7Sk$iG^t~p*suZjDwV=T$E2-|drsR5L>p@ua>`_Q^Pv8VoLlFAMvQI-q0th+{l z#J!VYjlgRS&0uR85}}t98a=+x8u#(r3(MIcfvOk5OrHT-w9I}mp zP=%JVz#=`F&Xoux_tKfjM6xFiH7gs+G3&_{nyfsWzbO2n5d-FoNm(O!-~;w(RtZ;= zEppjmC@qA{{Gk2a#+;hGARTv2roR49XN%!D*_o|I_vZK?#p!~td-_1flyjbwO zHIvkXUU0`W9_G%d>`Jy^2RM|g%L5IY@q+8yYC6i(F0^Kp{f}QOJcmV`&eKK=pp)A< zNG*qMC3qo|ZDvKWQV@vuNFNx45b~-8!zstX!1tZf$_vzP8}EZ-Jjc$@K)tT&fNwZd zk};_23GgIht?d^=3o35j{o=k$gXwb=nUHDrEEmGeT;bJi9npoJ}wVi zG+6aqBPm_+)L?_o4%>E+M&qX zAVKLH02HSX1Sk+~x)a{h!Q7H;w&1)ZD`;fkQI?Hj^KY=R(X zidNgWjL)zc>kZX+d%J#nZ_nPnI&0d0FxmxOLb!Pz!mAwcJI@g$nTeE)?iiM|8V{pP zbU|(Dq;$A2|1}>$R4EZv_GTG5FzYFgcOny}NFJQ0G#@;_T1bmm2N1{PG9r{mhmQ;L(ykP|-A22ihd)STb z8!O-V^N@EuLG(TQ*lRqf?vuZFDx*OwU!%pgFe)1)*%L~wdsEe2?-?vu@vB+MyVt7Z zT4Tm;-G|lKUz+iyH;t*)AXHz|fp5+V`c)#m`kj0>3YMm^MYrMMXLH1Zr|J1~zv!B9 z-L~Ne08H!Gl#K!^Me)PJd0Ixl!CxNSBNAm30RC&eVaN+WW34|Jec9heaxGcH=h~eK+#~E2Cp~=DYX$ONW!?Bs`db#Z;frXKcBpGhHW{A<#R_)_%vC z(Z}P61ws`vr5mxi1`lOBXIYz94u>VeTa5Rm{Jwd)jQbcE`cI0tj&Tg;ipFcN`U!D0 z_3oMJQ4k8F(yBNDLxuu??v2D6qpU2UNapG~Yr2<_Ppk)jN?glyaFG~Zuw|_=Cs|H} zQ~Lsc%ROBOd;zKVNEcM$e1(t4TNsg0T7dIvAi7uk*z4Wbvfnyue!BtG(A0)ke876Zsj>w1< z+ouB1|AOE30qXKXUETpqVk(1919TN^A>fZ)KD`%0LLC+Pkisfg-WIPS@E?_;9jN$n z@;I~+1=bE()4hxsw3^fM+m6F3{~-OFAHs|NMl&3>1;{N&g0Bkw6mYS>f4^+RDB%~Q z*9Hs#v79ikC6_w}i(ZXilXr7h0M9=AMW_pN<0{o=2K|ITKgj0A6=rS^lgHSBxT^Cc zg-Gz-?~N`fSfzGICUL%>AAgrgDm@Up62A9@i+mZS75`P#2dl!ULJtqfqC(T4pxM>Cs>*saqkrepU0(gi(5Ft9lN301RvyCq18kxE|jkz$8_CW}idpz&;(>$UERWtK+JAmNu%hLWxG48R-^^ONJq4tC z8+Vaji?%9*W{!0`c{ZJAgg6u41SlVF-^Aeb{Kx#IxaW-=8#PmUK*&ksg<3v80}^(K z%3V{`lTS*t7!S`wwnVPtPohz~2a=zh9~mrOE97#zNJ;ng09GsD#)b4muji0VZ~Zwm z4{{XPu8*)A3ZCcsT=MATc>}NQT2n^UqP}e&yp=<0h6_I)PCD-?D3>KcSThk5X9L?1 zc!R`NSysHmrDlgMOm7;`92o`ISPzuw-*JNs!Yi>bLp0&Y2D&{$y`>czxcm1r*4DK( z3Auk}M^mtsCzRGOl}1EkQhH5R{`%ZxKYn-DWQ^~OfkAMBD{`y2 zqjmlgJ=rsX`}(GWgPFz<8~yx&ZR*5ER=AB`sM#OW-}wi$+^+oI%K{}_cE`rmlm%=> zoZmzZE5;eE2KDBrhTXK7krn?AYIcN}zFxst_Mgtf3_Vuh@#`XtWO5@3I&SLpy9E%Dl4? zQlnbjyZ7)jf8WWd6!LX{7PYbo438oT&Ix{G)60~fh45R*0Du&-Sze4Ouvv7A|!TMy(v5tR=dnc{P!G$Gx@%HE*uV}QnKu!SfABtS#(&3teUfByK27%+Mo zWj+mUv=Wa!e`=5rSh&QOs~iJ9q;qm!rq zK~Wjds2F1xggW;&>g^r@2|1y`Cq15Z*Tsr_bM?F|oC4os3}$bKWq+t{zcY6cCKj|3 z!Spsj2;${VO-a>9Q87=^H5qyRa53*t7vi(Us}M5v8A5XG8jU<<0T$_;Y*KwxC95uX zHYbPdIW!)n>okpy0sU(IAT~t7LoxVq^1)c6#XboLfzMw`9H?OaLE^d?Fh9-RqPH#w z>z8i9v`V*62A0z$>dKg{K>qdQL0tG}=I#6Dd7IA7&hj6?lBq5A3pl%cXQG>fetDtR zp}5S;aQsDIaQeY!T8(0VsXnq0v0;+?-Yod|W-j8m^Pn+#MPM>fbSWZ@<7=mmM>}Aw zWePkI=Nn3oiVI|7CE~aP)rdp{@NlsYVT$Xu+TB+U!a05r5?uGdWng@p@K<<9v3 zOZ}yulzE2+-R-M|@2E!f2+2##|CK8EKF9(LJu;Tq8>6=-`pgiUUFq&^e)qfIH*baS zApWWWg(o1zs{_E~Ak(I*F?`)RCzh%#!3s#>-}8g;)cVUAqWxpOUD=G9oEc`;Y>(d( zy9(W>PCJv6n^OWEl@DPd@ndU0Oq{Y}o7t2924-~i7HzFP!yp4uSo zok_ZA1$8;grmBgFrNx5CiH=$LB-h3y$C%V~C0gj2%N z!R-Od773opvh8f)1!&TSU0LaSnl9)f|Mv*?`IIur4F&HvI8L*Dx4HuH*C;33b2kpf??V$mT|5I)?r{;46cganjItxSVg0kG zXS;_-Vxq-RIXwk2C?^q@#8(|IB1f9t&365@idaWUO?9DM;*3~6lE*eNe@&Zd_xcVcd7aH0pVz|f8Fvg-Z=n8zYMfH{u zV6%@SFX+9pP2g&vfKsleti=<);*RDMqZ^2p=FsuAwxSvfIXw%kMl~|W^=f5J}PW;;;I?aZfimfN1^3XDf7Vm#+pakoohT= z4B~L5-tCz9-%|$oZjx#K!u8Ci~=|%%XdNt{*HpWj6yc3!3SrANV?i0h~ERq$ROobjWbl0Bm8`tCOW{A6x(Ftio zguPWC#m|n3QqAG}=6yJSmO?c69{}<9%Srmd(COddcOdQE{>6AFUJa@ui&e9YzJVCv z7J1eYI1|!vhTgQbN#=MX8!5lAYiN5NL?dc+Z3j(4{K=lN;)6AADIXLB$)`CJ9IX#6 z<{M={zcRZ?CB1}ja3ZL$`&yPUf1GS$&*S(vv3e8Le^ECW(DtI&T3ih@Yv5|%dlC3L zapqMF$A1@C_OLGZR>wh&F;BQMM_z2r>3t~O*IK9=y3j9`sDv?v%jH*^zz@f6VKjZY zIh*(=H+15nMY;dQo+Ui-sB|jxUhg&?2f1{N8&l~QPVZ@A?U%~JCqp5T?w6HgQljlQ z7v*$;+af>WJ@xN}rzkvs0Yo;XhFxc(ZY1u|mCa6`?24ao4cIRY`jfZ3m$x#AbZ22) zK_p^GklfT~_EXrnznS-!yaH0e3_b(SPCKEWVgAcy^P=J?(o0GncQabEo^~d(q|Uyi zUN0^dQYlWJo|58>>(?XE5LApn%1%r9>q87y=XnpGvr|WRt83A#qTcr2^?p^^pB=Bh zem9NjbvOU+_IwPv^oLK+(&%8eHjh6GVoUeFGzcNlF$f=-0CMeF`?mfzUl}x1cEPEA zL=I)u*Zk+qIt-m zHAMWArx9y77J}lonEqI)vsSE9nSnEs6o2_Vg#W^$f3BVTo6JA+ z1)TwxZClR9>XoQjT{imsXpN%_FFH}DQ+bD>a|<|hK1n5t#P1x2AxqF!-X08=d%p8> zg?jh3nmDmWC1`UrweJZcY%u-vuhk;>37X~p1ZML%v0V#rRa;pKn48=qP|E{$BHw5X(Xt!8C792*X((RsO73D=ww5@Qb{mxr3S2}c=kJ&!%4Yk2H1Gf^?kw}TI~ zI#x9eT+#xjsSPmE$a|&`wVUL&P`B%-YX3?^Jrp5V2)rX7-qCr(?tMSFTRJaV5>otu z{zhbZ_}Id#ci{+Xz{Qv}O=r>P+eZu>>ny{mn*(6Y|eU9Os?7ncTPJn z1+({AK=+aT(AhWOr<6%As=Iut*Aw|b4t@4Rktd*}*&hq-+D%>)A^uZIZhr3_vCH0u zYNO$Mz$K^yxFv(UG+oGnnW$w-anOB}-JE5f5a0yb7kstQfEaIdc zco~plr)*`RXTupSLUcCASIC>0`rDZLX`Xfw_xoYU5-we;{W7$5Zg$E)lE(VYJZi>8EB7pYkShWT<1L&u zX$79G|JnXJ?-0G7JlYs{1**TDRe$^YH z;>f|LuOf92n6<_2whXXBB@t^(+5M>9qfj==EdFBI<_!sxEqGCIgRXC5iJtG5Q@AiU z4sA@!`u#9g%}62Ij3Y+gQi0}8jfZP~3XDpN#l;i_tE=PREVX;ri6A-f6QTipPd_+RW;g zkMrpqzy56X-Sc^zxHjWIX0Hh>E;1035(tNvO@s~BBeJoZp6!}nqykXP>8sg8c_ z{i(;|@FTWili6fZN;{5h%UbkZ`yp`i8643NT>a!s{P)?X#&b_jedTF;1sb&y&{@Wx&Eg97jyG)iyyMo?@y}j4h zV}BpM-aozgIt4))i{q&+QSUFii|90yb6Kn(g{3>%EpNqtdGX@Q+}zwlZrt}<7!VSt z01#lJ+oeB1TJ{Od{g(GgXB zy>0OKRO@@RBHg!Fr2)iSi%nD$>kGoR_qQ5uzgQn+qV{rO_q7$jbht-yvg;A?%1R@% zo||8Kd0E@{Lc4%gSp2b-D*wC74)SpMz2)y}4dAWEPlQ}t1s^~8_EiLjqSSt-Vn|F_ zjW4vj23hI!b~JgmKura7E~hjdReKO|-Dnjb>3P;!c>nZamcju3;pdX=9H`yL6} zmQwGDf4~Trpc#CX+6M|6jgtM0R^jGrX2Fod#|*@`WXqK>Z@t%kjnE}^D^b1QZ(yFu zwmV23epTjyuMjxOW)BQ-|4U02lY1FYC=j^#^$&T@u=Cm#o_PUvcJJY7Iz1OuO5PjZ zEcnf>tuI<BA*|fnvEpA(a4Bb+QHJ`89%UUIgSPAIzO_fc!=T0s98MD zG>9ej7qM!%A1ZxmwYLOs9|>cEL74EU?&F28UcWBZynm&ue4YKQ0R?JBB&jBRzIVs7 zHmS|7`F|+6GA*^9cJ%rkIF!ex)B}~+5A}Jrght>zJ(tIa%b(uI@y2_Xk8peE|F?*6 z-=!E{<25ud!Zt*B0JTGSQC8>dZ%*Nxk>42Z+FZzuZaPNLWonN;vAlENPOEl1UuIBg zPmd}UGja&mFz!?2k}X7(CL2Q@ZafDEN1v)kKOWBQneDjs_sWYTVMp8r-@)$2w|Ujh zjt*6*P)}5J)A65p4YrboZTe)#dr^o}@rp-iCnH{W6gphU<+_P)_!9f9Nr=F<65{}>_$=TT&jP|k z&C_zjH{1k=(8lX$R7_?fTQoN5Nn!M*QpI?6?USBHE;*OY=!)krGSSn;+xJ zM{y%|dBgUbI^ff_@&zQce3UsBuv4Qop#9;uzf+2n(dE&V%d@pw#@z&5x>0(Lp zte|!8QLHP(sG-W#%6rJ5GbJm8|J9bYzlkvt+OSLpj_PcWau~}!d>7VXB z>9K75{Ky3fNbIBjGvwk_PKVpWPC!Vm{r`4<)KibtnH^Y1j`909M3W+;h<+xrsyGmF z9oIvgj;JHDfU!U_9*_&8=`g&k;pcH*m|NzlqrVtISez>1{90PPXqqI)x7Cb6Dt^?> zf0})xZqvEv(n|>Lvv)03iNYTzmGz`ix#^D?d^<;K5_)Q{i_U!MUnqY(wJQhV4-z{0 zTkrCHqx-JbhY;EQtx_?yPyrk#=pP@8BtRd&9s@NDbDW?!Ex4($g!rhx*JL5f37@I0 z4>OYclSWB}c4>q99zcLJ@&RQ^1QpJ_neHv9ApU-*Gz8C*119`@WgeVYzD~{%W28%! z$rb%C(45cNf9q{cBiQ>2U1fFyiF`wtI@R!k*h9~NXH@-Z5tqj=IY5cKxD)r<%HJ!9 zxBR~+(VKM#a!((uJ(=(DM}Ah&nv{C0O>QcgXl*F->;0v!QuEW)xF2@w`bi(}T{v<* zK)HNWTXUt#%hdGjfI3Qb!el1Nigx9w_*3F-D$87Q9Ss~#*d;%A$PsZ^rVdZLWe7bf z8T4KByq5+%?t`Oh^q}#j;j*+9lc)>+;{nnz@)wMSVlNwXhn44Dk?hy3ADrAgsD|NB zA#4C94Y+GrIW7EWampRz_0k~{M5R+kMI>JC{bbG3fd4MlJ8nzDSox`G&-PnTKQuhL ztFW(PCz{{DBqhYE$HS4`(2M5Iv{eIUp4{?{s#j$8AXTN3yr7K&bFQ_z__csk23ZSM z^b%XF<_V9><=FRS1AS!1#->u9KxJ;oBPd|H(5hbeQ>8S=ifpkZDAa$Qm%IC$H&s%UZ+~7 z6?)DJ2n$=xdqcLMrtmYD=O*O|zEcYCZtz@P>;SvlVj*g4J(8X<1@u;dI$w> zrPHiGta%_0+9gT!ds^vnZS#ru9-7WG&gvS5pC4x5{*Pvt^TUIxw?FLc?5<31(Cwpj z04kW_EOb9knUhTz->V&7yW+w82JeL4OOyiy*^SaR=P$d(_yNnZYPHbKb;Y4l$?8NRxXqdYdey&XB=U>xI&&eVJZ zO8`%i-{#irV_tty&~_GM!cWFvoSLcfu};Mo20U zyrsViIKvRly|#@1zB5{Ys^vjaE;Zz;{oQ_DUf=M!`(8sj19!jH+a2dY$%{M=*EfZf zS4F^s&anavo~8P|uTAep>EYj+nVCVz<)7u@C2@&UUcymsA3+Exx5;X}}o1vcmTnHG_+Ha~9pH6AUS%Idp1_ z?L18um;Uf%bC6SLHxfxi5iM2jLi*h*j5@u#lEF?M$g=K=AT7zVUMdO%$H6S5L`O^F z3V*FmKrT)4hQP)&%M#kzP652ASPhE^WvC z2{@Slp}~JmC}Q5l6F)h;B9!y|dCuv@%1^4hi1wW0jFjDQ5b7=Z0X&G@9SP~bU{9}2 zX~~l({Q24fqDsCWwLfsHVoOe$Y=8Lq~aB4eNA$MsYRb(DUaompqn~-m zZkhMzhk)U4bEXG$RlA~0lx0Ux-Ha4&8Nd-bDFL5Qm|+hp~S`D#G_RhYwr(N*b3?{h!T>^cz?D$ht(G1yBEXOdiwAxt^79JPuFQ+#Zn6(T%-U zx7_iXo5`Vif_~5d+}FXOosbikuZv-P%dniK4twK=Ro0#OAX=GDG+ex4F6yZvo7}X= zxqjAozlKm;{jL|PbU!dyQCsc4e0zAjqmA2Ayhc;M@Tddj7|yto$`Tso`Mk8Xj@kV1 zwbK=47Flnn-#Y`ho_y(Vp1-ruc$t%0|DGSv~ zKTMIwpIe?k=J5ItH;YDqd0MWkQVTIsx^p`3nQdPK3F6~Lo!znyA)HIFxI5p}PM1Su) zF^izpg7-_q9CfkeTzc2Twu-J4q92p&#>R`u5MMk?U|wUkX>gN(&TCb$NJReEw_nPr zs{dOP!z8)Bp%POGh<5Z{X5?Jwe5&#kod-NZV4|u#EHlyBYm_e0th;&qNYNAzL%m;L zz6Ki$vRr?FvCAVag}v=Ic|dK>>0v)6_MG#wD}V4qugu-v{^ShuGh4f_Z>TX}gINBF z#3dD|};wbBuK-mTtS^ z(;>cM!c;;|MM?XBNF=f)*YXwI6YQ`WS2Awoip;jsDk)ocq+anrXk<(Jc3+)~ChraZ zNsN%NryY-@qSI#Tluybt3+P6zNHXHy3O11bOSwU^66IKz8WzXO4W*4K;W=-~YMlkd zWz7#o`!3?r)1AHUUVlLTNSVC;$dIRRN$yrV1RhGo_uI^(K+%*B=R^*%rYnsO`)o7+ zr!;E5lL5(m`d~*MYd`-%Cdcdug5Z_<=dA@ykcQ!dqAi6&i%h|*g1-}|bb9x8#)ae& z!bDr%=8fotwYC1AiO)pxdXZ$mLC8akT1;>SVh9qhVsWgX#rQ9L)6ggCd>K3&G2Rqp zCW8gWsX;_lp2-GwHByMiX}obp8?-0x3B;`~{~pJ3{|8UF&Sl5%G@3lLMaD^uukoZ6 z32cE(X5kp^-JcIqWFhyKKHvAUy6$8DkP&eS*Vt2+ysbRlRGe_bz%nYKIzZF11x7VD z6Zv_t>^XZ${v2bD=ezMwVGu~OKu6@Uk?_ijXRa|0?^Q;W6kdpTEu$qMQJR_V3|~=a zTZ@ZNVq=cCePb`=Z~Vp|dcw=76R(ViM0obW5^sKh)&k!;^PV5h$LtZ!NAQ{lco1xn zTFie>%Qb&zQHst`<3Y1<5r`SldlK$oRUJ+n!$y`=zG`rle;1>T+7g%bGLgAvC0x0O zPO10ynbA?U_Wm&{i~`Y$>58%uLxR~Mhx__b3~yBXs8oq??Ug61)Ryqz=!Rg0`^k?i zHM=^B!OaEr-`v+TesEe%yjD|7e}6JQ$FUCf2m$qzqAqqvy4OTgC;}@ZL|zBBhXbna zTC-?*ZroIiG?nNvOOFU@TPP>Aq~JJd`9ll{$=>`tOCEr#I6twffuqI}_25gi-|-Jn z2GX5-VtUlyjilX&#T#?If#mfPZX7Bu*zD@NZ=e9YJ=W^%n`t$c?fDOI^^6-*#n(TF z*FZ@M$okmZ+(u}`x5da^f8m-p$n3E+>W$spuN)?=Cf=`SA9Rz|)~`$OZ>pd1*@B`h zD|dAxF8g`_^acejcUL?m?v>nlw15Rwv`0rI2)e%9qK7SmQAD`!Ed;PYO;-Y#_sPKH zqj2$YwR4^ob0Nnke+KTc2z}B1vsqM~RvvrT_;VB6HodzR3~m^E*_egSP&&3>e~zPL z*K!QD#FRX#v*RvZuG_bQBQZz{6R3=8I4X2~^z!EUjDOpNw!n!Z;tW^e16aLYj$2;^ zy?^}iwwYpCU@Rq)?=XafBxGOuV8_Uu-Bb~^@#>c`_5u?O5BL?&vhw@4{o`|SrkmDk zXTjPjX7?T?RWN%n>B#AZsUx7bG&l;Au0mO;C@xFX|B?e1j5^}*k9|kGy9I5WbthyD z+(*0m>BkQlf7@dJ{wLL$6ml4&7x^S=Z<jj8OzW=^J z5IA$Ku0Zo$%k{HAzrBG3<_TTP*H`v8%9>m_w_3?+G`lPPyPF{~>JnNcI6Ej_u_7v* z0W7>J#@3o(-UBZ=O|OlwQUXXCREaQ-UnqYR zY;l;D?x7ixmPFS%m;_q-D>Z45_{IdGu_1gDW2^hpQH3pUzX217XBOSIg5RI-Y6@t5 z6}F#H^lA8$tT0w2CjwW=+{mMe1zYHCTG>0JfD>=d_y z0bP9Dreb&2YJ5Drdi|C99*dY};2(kMkH6BYdKuQ_uzSmP@xjJK=>0Fw4F)c=#zP@~ zr1&b6gzXKYoIrT(%(QEue{F8&;}E59D+1Uuc*O#shH~Uz3bWvJz_rLwtTU2Y$Z~0ohd0Xh_2|+CtPpor>WQ*p(p#yQQ^zeN0H^}PSEh-Tb_4M?uM91n*_ZC zfBDmzD!(X_g;-+0hGl|e)8-op5WiBzsdBv6ei{&$|8YcZTYEl7VTA4Rx5xn%Bl0zl zJ0CmhOY|01zZr=OAXHv>jL`f*uvp*U{0VYb{Frb8;#huEyjYNt^#B24U0JnxnV8Qr z6~U%G+icdxA@Opt>@ zN=&SQ3y7y&EN;k6z_OInXkZ^gE_f8ffWMeV*(Uf#vR2e{XIcP6M)ao0RyixRyX2p; zEk8`H-y79S+-TK1!u_HW=lDLP<0Dh!x56s^X)2PAjEboxCUL6I6}huG z@&%=FD$2;cT@r<$(IW9hw2rDmFLFm7`uA)szpC3fRn6f1{pGzAZs|?bVlL_wF-@13qShd9Uxmr}3onxO|dspxHk)8vS#_}nAF<#yF z$G{r-)6}U6Q2ljrk<`2Mdw=kc6KnZ@Y$#<8xYV_z21I+4Zo7yQ7@04>t6p7=rW)L_ zcagz+|4#X~F1ydNti$MaCrHZ(sbDQG4h&BAdvcrjIiJRkvLo~rUYDNH=s!=s(Zh&p zdz5|rj3!I8L;q=&=MB^_+n&I($X+;6ofuW@HxgSV$7&%@FXZqL{ufK!`1R}(({G{X zT_89_6(^@|*!P#PgG_z7QM36{yC=F+TRldX?lg7&^>5_)t^Tkd|K{V8`2E4pv6Kkm z3SJmqRZxX8!9{qRfO>b!pdEzeUm1SkbaeXb)qPJiPLX z?AmwCC-A3J!Bjc4UP1&DG7$ut?@I*lKNvSfE^$JWkDbzi_9rmi)%ft|K59b9Q|!Lj zj(ij54pME88wxWdra`@G6IP_yc$1qb3oL8Yl>*8uwqG0mWM?A&Wa?K~wbxt;j{3=D z5~6-)Z{h1=Bq1|TY- zf3z5v%fh~8_pMc$^Vhf7wghg|zj~hJ)en~!fTs_RYYeZRHkNV+@f(bTC0F3Dv}S5T31JC-!@A>?N?r3(jisv$(>kOpBmjG!Ygv;@d#_p z3gtt?NKSNAD10gd#nET#abPt01?1JON>LA=j|}@IsDQJqu~Cxdf3N>W z8~(jjM#L-rUH0!L8G+vMVV%zvX_<_qEV1!XY$zP$z#2qexv6+B#gS7>l(#L7g&n^Z z`We;rh#E$R%5GDLx%_lNr2ha*8L3`Zzz;fPTw%;!C})BVrO1v(HEEP5qm*drb{}50 zU-GMs4|%spZe1$($MU;dNR}V=de}#@8MQa_wuVduq#)# zZj}*zC%%M4gwmlx_)AUs?qSxx-qizD?16cLM_&%F{SXkP8Ej6DlN2^@eW7^0EJGKz zf0F@vKPD5g8;w_Gc_&%Hgqti7ccz1jwodXv%btEtlc&|%z%z=MuiVxhzKCa06?*og zE2h$(8~Ia`Hys2^?|m-)I{wkLI~^2l<@y3cH{t~!KQMZ!81pG!;@Q)uxBhv!F8|W) z+Igm7;@p|=?>pPToX)Gex$pH%DLinUl~~IIW~!S3T-SHpz1xV4Y>yYuX<^>I(DQz% zuh1nE!Q=0^nuj!F*hGE2&buqn&J4^4V|KmlrIgs`j=P9o~dQt{4#m~0#;*KYuz14}(_Dpzg zH9|k z4xJJmj(-_NjD-QF!(mK1KJ1O*K2%a7t{zSEFZ6*i68tTyVjLXjYU~ zs#sU+ysmiwS@6;9&rDNY3f;OoX|i6Od7`%H6nuE^Z0_3ku5@T*-qq49%%gp%QT7v1 zY=ls!OpYvTV;?yFF}=n6kTjhXjB@kD-j=>YRW-wHhOlJEwi4?|=KSL?ZNz}LtQUwZJ^A9SA% zaL}>jg3C03DL~`)-Kp8#UP|x7?|3<_ntK=TdE%IqK@QG?I`Ub` zGx9euxTUgyx1^c_P3 z|MZd{F4!YwX2gc6Ia;NG57wC)zn`C-(x1MgPckj#G=K#2X+B~RU@^OCbX7LVE9n#W z?WBUPj0Bp;J$~ORO}xJKwBL<&Aw)IB)_3o*(g44iSrNE{lN%`UVKq`Uh|r6?5b?3^ zT8}r@3_m*YoOm6|0nAVqL(iw`I2e({al4iom`Jmk_l`dP`iU0*XhRkE z+zDIf_@7sNW&_^aFbeptqx~HXV67GsEjDX51N5er1>b~lXH&cb3<1+Lc;p-bMU$-~i>f@Anzr~)n#+J@R^7$G;u>#IMvVWi}S7i~j zRFd!|qptg}&l2vMQn;cdJ~F&t1bE9CgfztI)j_ko%yt$#3VJHi3W#SpCFMH}bisSk zq+jZ@F)iZw5Zk9eHw?}09WKYQ0#BK;zqa~}Y?Xg)>re=!mg3EZmf8H>%c55Nh3|bm z@hg}0HFZ$x9#W%%wDaQ7_i^R}t}@(PZl29LYg6+Aa(~97U>EC?l58qTL|9x>aQ(Ub=w@c0@vmRU?_N0msVSS5HAYTk?~FR?j4pRQA`rcaC-ei(8^J$ zlg74zwvfsvJI;nA(qTfT5k1KPmOgYI_4!BlExyZ{D~*|LYfF8J>VL9k$Jp`2z_h0a z!&#s!_DE!E_hu6y#NZK`Ce4>yl zT#l!;lm9H?7exFLlbRipWBI$D?s;WP&VLFWF-S?;k6WO=l_dk3`zFw^a|<4<+e{0n zmpZQw`1~6k3i#On1jet-*2RDFU2}tn&yK_9Y%$*KY|!zH+{fSCk@~d<$JDHQ32-YY zj)sVie~ptFK@&347s|g2`ab7lZSiHQJ;>$e+g4($Qrfje0oX%+41|SdrS`WRJk8xxBtX}$_}49Z9tXN3f>-#&G_Ooxub@T zmj0x^O-dBcWXyV<*+)eq1rjDj{k<@$_T0_GP~IPPAA>5czELi$`@}o}@`^XcbkllF zy=2OE{1+g-zp_W=nud(@SgAuY@2{3yyt#{_bwQc7@>eVZfbV~6yZ2n*wBA|^$6EF|%g`1n8cvZGqGzbahYT(C2O_=5v(FX= zPRQ7n?OrC{yUCZ>5$XL*aBn;wAj<78;>Riy5;g@iR%O&~(`>Y***G`Ggq)B|%B94J zRMDR`v~V3d@Cgi&U63{O#lDhzbvy|6!b1{F7zVRGvluc=cr|%8K1vEU3tOb==Im#3 zM*?NXqyTpH-f}uh^_mVe(|-=|`g3YGq&=@?OD`}gaQ(;KDk`!&753lcdco{1)F^Y= zE&N9+QP}uPSAH+Y&ke8k1(}1bRoJ`I_&9~+Ec>8}hK}34)j^1<+F8b=UeUJ+muse^ zI3$qaV-U`4cJ`MfH1D`tV}kVi$7(GQARxy9M6#*!dbV()@$Xqo+wn_#NtME@D6MrBsJcXVGC@%3>SNLf<9h`BX6WvSw?pqNXN>3PBPDSv@cK@~j zN*xCeg!RE1Z!AS{DXv2Y%gGO=Ixtln|E$DpNyAt?kCu&=p8Qt&09GxHByG=jof>BP zNJI?1k}Is=Yb7Q_g!Dr{6q&;CxzR>LdvN~-uCRM7)2)~OOwafGSkRIfP`Uq#U86}@ z;O3QB8;1M;bp~JJH8eHL$PZ%>q9R>c{4JQ53C+EjEIy|~c|{UYBF`g8UsFlw1sw>m z8W4O;eR}gQnTf_r^({Vax%>lqOd)%v%JX)^H*TSdR9hy5O2)wt%vJaUJ|vFpx<8q! z^*72iWK@FdWnh5f<+|&oN-|>N?837!b;kU{DGctKQ8BMj=^2FhaQz`0t6bVWGWX3& z-3Pt#D$GwZ#NM-@9BmE`-4Yl!u4 z6+Ybeg}K|t+~RNB-PH`L;6KB1u<`b7jau>6;K4t_tHbKB_5;>w$w8*x zQ~N6zaaKX^-mJlQE-!xG{6!1MWtcODWM+i8uecW4diz}Z3%{n^jRz4vE?OiL+!Z43 z9bR`;4h5q@7&8)fJsMwf;c+o4!);1)OE(&yt8UAJR4WdY<dP)WCtFdjv+ZG08W!!@oz4&fk>>>ADlhz$40KESQFXr=u3 zQDmnVT^w04kC9Lz7l)5>DYmi|j}yL9TmJSejae-`d{a(Esi&b#hKvV&5x#RQk)XF# zqvR(*MZzLwzXIU%QoQ#%r|~1^yYNQ?!9DT({vHUsc3pZUoCy@khYWhAN*X|p@HWai zWhOa6&$B;;;CyOz&~-!P@0H_czl%1$HA~E>A_L7~Qdet9V+=yytKT^XA4RObj{Up( z>n{a&w@63&xhAmc>2gA@tiGR^vGH?iL;d%BN>_j1bTB#8z{?B2z&{NwL#Q7iKPAH9 z=vAR_*SRoZXP#t!{DlH0gq@iczaq_9_ST7UDQZR9eA?#`x&6kAB-RaR5OmjKo0c?} zbo{u;(aeIoQ4C6l%`l?ZloqW{$wMgITDNTwHlE4gp_nI>WZlQgZ%UFyq9} zM(*-9F%k^h7+>P0auavsSGcul@6#nNd)k6`bOY+eJ11^*s4BNF+J8^fGX?N)-_IQI zNwe(uz@^=G)@dW6s%6S<+kLIvHV*m%gZhMaXWC1OpneOC&zFhtEJDJ z!VIsgA7!X#8G_L2^bCqI5`lgJ*Mrd`r+oD=;dTzAKyM9-(bx0x{>4L?RguE`nOSLZ zG>8|qHF5iW{?~re(143R_CJk|rH7@^+vc$RzQSe8VfgDe&gOY|amZUnS#^sKrW%(q z*kKX9+Kc$~5yZ(gW*FLqk-gy*zL0&p9??ky%#V<*iTmH1$6D|@CvJfTJH+X;k z(i))rQN(18P>{?E7#2J(hvqg8;{aj7gNNaAxm7pWdu1tseVFhYcM#RWx&!eic{5R1+W5&yk<^&!{Jh40=-vClHz9{>K`CF&I)nw2j3 zJ=GJKo;es2)M`hmhi3^BasaUvm7Y_k=l1_S>H-Oe6Gy27QUY0&Jy_!6qi4`$=r=Hq z^Z2luFB2x5k<`6GyX4srHBP%)p120nq9TJmW*{s4({Y>k=^Tza_Rtw#+wL~CWF+Pe zHGv0_po#=1Uyz;D_5}ME07MWsa(pf;@AB4GxIv`(a1GyE)v!yQhE~M6=D&kyYqGxs zgwojI|7Hni3OVmR?(tJQ6E38@|5L5T;EPaIFcm`8NE(ljmuKQlC%Of5Yp{gD9JHiWC5=#~YsPhX=u@PVr9AL!`> zu0pF7V5@(rftPxp@9-g7Sc1tnB|lvH#@c<#1Et(P#5wie?xJ^gdqBa7?6eq5#AivsQ1>pf zmI?=1SJtojhW2xDy~p6-ITQ9L+pgY`Sy801+S)`@Q#2Wo38oy}`iy{SuO_MDBwwC$ z{25OquuDkGO#M0&e?4$Fap3ZvcY%0d30iTa7A$`2k^mq@0RU&=W2m zPvYSUWWwv#FPKZ?(ZBFsHp>eze(!o)k%JN64mR% z6GVm^^SSrRJ|?&_4Q?(U`ST^+=#HUy_r8@!xDS$o2FIwUJ#BX-yr9ORmDkO0W zQ6Zs;6W3N3`j<^=u9#w^y!q$hsKN50=?Ht8%+vPqd2a=f+B5_!hqb$@fcn?r;hzv6 z=;3C~pytk;{k?lDn?eK}*mH8;SaHCgSKnHne`dO{IFxr0e;h&v5}n_FYUosl-LH)q zYeEbd$m&?Tvbt`1OMQm^msW1%?IT}!{nOVQh|q_|yV#R49zxTPLXW1rmt0AEL^u?J zfn+AZ{=yk|;3=;b-4T$0UcG9!pw_7e2jUNKculSAp>rw{L_x%VWZtA5SDrdauc(7@ zeg1!4Fp;qm%oC86$_hq`(Vm8Y7v>cBYsS2UhUq>wGw=Di7%z)wuY_N_SN>nK&@)d7 ztJBjiwDD^+E}D5Mp8xPh%hNUE>#kbv&8%V7@j!=k2~ABl4ttLB`QxJ#Y3zA@u#Lbs$iQNQ83tq zLNJ1%biEBbOAAZ4pGjl6GxL)pK>!t%QL_V6XjgcfFrBO@$-+Jnky_Yh~N zALdFUkLwYj`*UnhXA-|ud4vjx(s%4Pe44QcR|VooHuao(S{XCKAyDGo#d2@-dLVRn z$ikeB97wM2xEKslHu;*EfG=AFuF{SKt19XGu`&yr$u^a8;rNL!SXr_4H&B?aKd+9^ z87@YaZReods1ZT^z)Te4UU?K)VMWhPOGldcmK|m5@OOFc=)Jla-F@^lqHHcGYgc?a zibilPZ`<|v#hm_!jDu_6<{2S?Yhw_H&C^Q&!w;a4@6+1XYl3<6B;9PJ{ZZ{BdYJT4 zRy6fgvx$TY51h@!VRJThb_1-HXeHfDHcZ-NC!0R7_>o!v%W%Wq&e-ujmjQaxQ>QQf zOy4?{+3o~q2TqQ&XQ#B;xCM=(=Yrlb{Ho-+<@NLgs&K~sQQ3a=biK03=^kDC6bcKP z5QYdY{A0Z!tgpS9GfRLB;AELaMOGj00uj>0wEiXh5H;K%oWNO0Ec8fv7*X>lmE}n| zy%4>gEer4|$W$*SsZOX6JhHcBBE9#Kg$nW_D@1r3cM2kVFVSQWlAu2r!1{b-_iX0J z3YVp%S;)fFB%eZJ8gnSwyalc=DomEWU-+J5l-DforS_0>o`_{I)`H>8JevM{O#jQ1 z;4jJafecQfQ}XP8`>~)M)WG7;!HaG1i+Zlu^Q2A%TLdJKF00>SxJXp1 z9?l5SGN*#DHVHOQK?a}cqZ18J?F$0=P)#N?%>%Z4&&I^1iCs8x%MQ$f2$+g}Q*`7Z zSdsrgt3Ir;;vVPeg2?T7-_Ow{@qv|Wm7JyJ^r*I$g{ZOz&{u+iAr~SLn8Ofilb zadVB?1FfzZm@P0Uw}S0Sd@~^PGDw#(W=S}u%1cAd8H1}jmy+8>*sIdt@ayRPc=>Tq zEpMhC6d{P$w2;Cl4^tv9~FKP;AL`;2}_ zM9cAy!h19^p>5ZNzX;ccI=hnS~7FBAIc=8Wzfn7$b~-`%a|M0}Bv5LQilqYw_G_G0}rX1wys!Sf<~ z?bm2vuf6X5HeLbliGD9?lQJv${PephDwa%C$d_XNN*Sa#gMQYX0G>8x0J(<$r@-*^ zU-2f-V>D0nK3AByjVGIj!+fY%X@!~hZY%)Q(ASr#aX*h?_l;knJ+8meJT-?drupj} z3DJ|BLz%pSTnmnOGcs)&lfENwoL77trx6W=p%jt+Cb!}S&Pk2U{P=^76!G*(1EDnl zjLVGoGA(58e`>6^GJg4DfYrE{bn{#2Y#?efyxL7}dyC(%yVT|gImC;pU7d#o@k=8{ zjdv`-QtlnoMc%T?n3wcO`4W6dlEGM* zNnzd5)W9$`;VU1!|6th@+%DYyyj)B}I_Zr_att&GG_RnAGB(10V$&pgEaKYZ9egL1=DiK;Tzwcr>*FbbrL@w^D8~ELQk!aeG-$ zeWwm0#!vi>%STm!`t^f*0A4T5TeRv1jn`P)geE6>3`(uo9A(Lw&w;u*tlKUzd+!uD zzqHrME*lykIHMFQ_FHZ#K#`5sBfOr5Z&?2zM;zn*TppFLAIbQ=a^rkt3knZTzm(Ro z@RE9>4||H5CeZ*+U&Eb#n%cuE#$OG2vB3UK^zU-Up1(w73x&WBW#QHM6Mi&k9p2w| z-DOggab;!-rA+yDA&ws#{kW!lyw+9Cs&mhSth5m5Co{c>e-PUX^{9EZvzSwHaY{>z zE5F}bm_*qLe9Rui&DmVc!lWCIYz;9u3jinQe_cGFyGwqkfUoy$8yPSBL<}FbvvH9;itCj$e5#5H7>X+-No2;LD{(a(;=QC*6n`K)u>>0d4{d zaj{;g_qT(o@6(COX;JdU3-RRHH%u?M2V0M+2RRLzsPQWZdr9g>$EGgLHN?$Q{2Wm!ZKPCcx`s+3BTpYL_}0wn-mI}Sr>F8Q20KR%u;0Tx8r19aK%mK zf%1#R{Bp&F3D#o*q-XKuuP*KU`8~V|;Wv21;sA8Cq zI;-od4jnB^b{y`S3`k0_kuBZ99=8R~U1C964q@zv)1}*Kh<<6w({+@xCooa%xsx9S zOz`nB)*I4#dBil(mpB~;a&Zj&|9rx#bmE%i+zp%B1V&1uHwfx5haQcjAr~gz0xFzU zk$G9w5fmc7W%FJd_nqM2h_3j_?Bp^6+hKI>gSDGmkLrl3{F|VG%NRPuK3rR(6hmR$ zGa{t$eCe?Kh|hePXf=6#^=QF!naqGD$&-YEe)`&s7{LKL^?YO54=X1 zWhZ7281iSaq)yUdZWcVN(2o0Q;4Pj6JY9&_wAJs-$Gi-%h9-CB=wO@5nI~@WKD5Gi z(`kZA2cYer&$ehW$vj)tapn)|6s=RT{qgp>%*YNJ$vGXr7f(9;sQ+~y`?l$I1(`&? zMWlCTWU^9S5x!1d1YkkfS_ues>46L|S?|^8jvV+D*H?ao5U#$y$nTZmcMuN|I!W3s z)i8Mh5WATSjHA56V(D~yS@B+&#!z_iEiFxug%psN34%xgo^Ui(iths^FUvXAa=K>3 z?P^3?*LAqC1#C2&Y%mJhJIky3=z8EvFRYq6$!0Ry!SA!9z!uxK8@B*5$x+QRIsVwy zYrw!zz-044vtYUN#lKY+Tq=uCqjH z2L^;kg0UQK!c=5Yl{Zevpo~e5cN7&Wfsf&y+BGk&f;P38PMF%SLb~8 z&M{+TSN=3<6E6GRI&As-0L%pJs-%=R^WTZ>8yiG;j#%Ui!>beD zGIu!(->q|)0Chl$zmtDqbW>ii^shQ^eeE-HP&WtpqnC-U*0o=ZMY*Rq6O=fo63n6M z*Ty>r!ADAg-V=-IwsV>b%g^^sP%A?}RTOkauZ9QgwmK@Kaj981f^#@NyiZ}pY(qtl zm95OKyB22)7n}lFMG}9ICTKma|FpK4mBCioYLmvdwqAQT(7)?)0w=I~+P$ZFxZ>R= zDZzKQbHfLD2~^E+)mjtB^tWDq5FUqU*59M?vYf3?zgDoTWU_-=7Vzp57^JANF%#Gy1>w^Q zOWQxj$ttm&+dnFg$;+iaoca!4Ph0<@-0w19L#HS$5Km0_b=Owmq3sk4jskNi4rORB zqYHYUm>mbw|1F^6NMM6fVzz$Yv^!##NZ)J%lbC?iPBW$N0kmM3ATvJBRgs28eASSt zG{9Q<*;!lpD%pNSR3TeqN&vpH*uhQGfPDiCFX`Uc147!`n%Y|`g}huA?Z1`ge<$`P zj)+G9`EljhH}T;gIfDO!s81xBJ>t~uW|Q7RAN~(Xfwq1Wg@d4>pAFqX`Uwe0oDYvf z#sYGxmjARGlZSa5*=OP@0 z^MFK>c!=&k0FaZjC#PBtLaqiSR9Ia0~XHWtU}H4ywFzP*@{5J0M>Jeddg^OmH; z9VBk)i##Myn>tw}fUMoyJ&?IfThfgl3pO+yq!tTGEBK)+!N{>UB1$F|ys@$t<0@G6$m@zJ-R(mZT{7#@O2br6n| zL!UO)BxE%ck}6k8LB`lB)(V8Udyth59ob$K59y50MIEb8XB-7ix(Wv=@koWB>mYJV z*FvWI)F@ksFnxB)>NZzA1aLVO5uH09)O(J|HfH)#X2YBEz03r$piBKe#d>^z4 z2}E!W%eF)81)FAPdqG5!Y9xrLffyk3PRC(@%+8J_1TtLj6@v_xTMjZzlvF~z+mJj( zxy$0JuV774$134*9V2c#lA;km`IbX+M*x)_q}*A}Cg*x^|L9I-;j|a&kU(YIREom0 z)JW40iZ`=wtUbFRvuzn-s1pIA-W1gk&00g;#;2?qr+m{WHUke|r06_9`+y8tA` zAmnB953gW30imJqIr_)ADuj%85OOYT)Ul?bg*EpMMh67jOhh4Bx$+h%6j4Z%5N{*^ z54Oacr~LPuYCvs?6SgUPp#ssM1C@WK0eg1*d#;Y;f789^+dJ|Mzz5fCjURt%Q~(nF zAej*M&Icbu+D`}|5CS3!iH9{#M?%C2vA#q4O|HtYUjR}65F(oLxwha@V~}lKrNR3@ z6c6&RrKOkMN75|Jry%agpdExnW0sA4_2&s>H_3pU$sXb-84zR+qGi46RMA)ayFOg{ znTeLhI0*2Dm9#inE-Jks9I9t5CqRbCdl&W1p(p_9WyEsS)sKFv+uJ^4-oeg}GyMvP zdhZBAIBAswl$on4$~nBwl5P$((@C?FM)79cL=!{iP^e_#Ow1s|b{g%E+m2DhMV4{xy+oB(pO35X7G&pjnl z#j*gsbfA0swVDWgd>cM69r2ta;t7cmvdR%LMx@Ai-(dzMTgXaUUN->AI@K=&A^S%l zvgk-<10nU-v7+kLThm(h6dfVaneJ6Aa=)fjc}VfuPn~cWx|$&;G>}X;U*I86vvs)6kXLA5XQzDZ^MwBk z&+gs%{$-w>Q)Utx1Z=sAg={8a0FwF%N%RRiAXtb4(vJx8)?ZugUFo{UxOlUWE+F9_ zF~WTtK*}7#SCAnid1XUVzq)zfX21i8D9^*o7J>-XLt`C8Q*Zw{8j*Ve!9UbNJOatf zmv{?r01|pHK=jb>Ah}Kz7NUI~9p#~m;|U=KSrd@k3tuAkBcE%Fc3P`N37=Y9$9vL9|D3AgO zgm~+f`p45tRIqNX>-vrW;w34sYa%HKIXC3Z0T7YBV69y|D4oEz}Q<4%Q1`?}@0*LZq9%49n&VcAAY-L{h*7=;R zSbFObK{|Y(79e1ufFLF0ybpuVBTNuWAU_(j!g3h{TKiEO+ENr8uVG@7{DRzVqcBc0PBt#)s z2_Z9|``q$LA*5>qkl^C${8I6VGVaik$hL8hrPA`2Y`fwuqh$% zQM)%huDI?F(q|&t6Il(%WMi|`KyG9{G9XMSd8E>oUL!i$ z8-pxV)r(ftUIUM!8fQ#dswb|~cJOzy?Cl-{sVab+{Q57Zq#~25Q{^mp&>EFzO=dY( z$mP%sa$1NqVDJ-fqs?$6N$eoQZr|+h5?Svpt`17j?PP?e++QWom^;{i{r2_ix2iWw zqrIKIy}4t@R#w*5KtzJ{^@Ck$47&W#7Hfb9C#3S>@dp`2H@aj4#6W-#k3q`vaQ;Te zKt#6UwV{E00aEx#gw!Wa0f-VJoTNBiVst!1@F+4sF7w=jb=9pSUVI@0*(!ylqSe$8 z6JaNL@+7G}ka1ZEfiu3Pi%6^K%2$OmD%grRm&(;FaLmraLKWv-2U+M*v)#AjASisI zSISByCkb}QBR29W~7aCS2grGu?2HxO;~QpZ_GT6@xRUu1Nk3dk*Fd#hz# zE`Zno6H7|S(@Sa~wyL85vY>!0)aBW&=nt);#d0~(MdBJ7Kt>36r%5B48;XwfOh^Vq zROb#-!5U4BRR*LBhosfzyqMfL2MHS3EbGo4#Nl8ISz9PK1RhaB0~3nn^2tyB;{>gv z@`573Ek4FWZq}ZN0EAp@gsf^+k}@G&^|nqSrz69H+%4fl0Kq~`dqmw11GOq>AQvf3 zh!5<(5kPD#h$8amzhWc@5ca#Z6^Y&jkn=K^^#!wSAw&T|?J>OIfr5_`r~?RSsB8oY zNdgepI6$;nSc7^>v8~@Dd^7=(jW(^|19{K;eY}P`mIIO`19*Q!1ae>b;46=KiR%I) zc!W;ZjDt8fPRN;7MXRM+tOJnXqgl?FY#YGGy~A zMD~i7c(dO@cwO+YT2?SJL$)+k!t~iM{^>i*Vj)}zJk?;PLbY5+*&kOw%7k#P7S_H4 z_1Yl{1DW63aXzq+h2>>yx@&7d<;wV&xIq0%;P@+i@MHh>E&Uo$LZxee^VPE*>|{%q zm*yGFJUWMGukrNG!bFZfR9?jjI*NmQH;>X)K;%Mm8xC>>gb&K&JAHJb=CYc5aDqyV z{s7&Uc~4;IXgVex5lN0jUe|&4OJhUJ42)n1=tr^du4k6CqX;Kr2aFX8c@1Yq85kmkn8%^GM zlnt9XuQXNdy?Oigw-ja7TN^!sKxFIYM^D#Z1IQD7`uMHK=N~_Mcnt>u3P=z|>lJL6 zQX{V&91xr7;o&w%*l;B}GO%^#^qoTo0+5zcBUYKS3nJ*zjS7gx9#A5g z?!s@YgGlvCg83sWMo8%d7Yjj?E+a&CkYSmkQl%}y18>+0MEU?(01(1&1Rt_>BQX2z8PNij%-=K)Bt5qO}n4&rh8R5FlOP0MxSV;{S+;4P?SGKC_L z#TVtsBIkFpG);n$arX@4ENOs7OX?q6Y((kvqtyX&e98<=8-R$kEqAp0`mK&z|La@j zgEPT{)7M+~H}5kA1VGT%liQCT-M)4)FZ82u1RwEKkUUfoc`R{ti%sc3Y9}712OuT~ zM4uyl@#=#YHnapjv5-DM{Ar$#Zb64@9RZfdAQpm1S0R}&L|S>V5Lp{V0XYUCs%W1L zLfjyRWU&y-g^*aka8kBG!NULvdHDs9#l^FvQ>4Yc<dCq!5{a?6nF?#S?UD6%J-18s= zabk32@$~@mq>2c=I>;6|2=VHbSFnV|-+uS@i$=56`Hr%I=&aqs2!LFsALQEw5J_Zu z3}u|+<@$w}kE|S9X|By14O;U05L*9gQPMzVgNi+ zJ^fCPt(+j<;;ATbZk0P^&XE;5H45LwcYrtSKisIs)XXc$%^I%EF_ ztlq_YdOB&ftkT-M+6E+wHU=Vy07=Mw!}qiJhbFCK1s$cHW%absDaaTFAT34!Uh`Oadx25(`5$BdkEiPL0a)&u13P9v~ z5Qd01Pk$@?tG<*7=#kM(tNJJ1463pKa(7^ba8NZdr8#PFY)O~T)wva0>zmJ>LCDCT zvSPl$|1D`r1SI+r4|W-Wxc2J#4!!6$#oVQ*){(8PvJ9oQmR;uq0Z1qUbq_w=iCq2+ zXnHeZMQVLct^_0&j5MPUTO9~am5>A$S-eH&K6#jvh%1mU@UT@0$S$`yfU)K<+RO z#R%Cv;==(kkV;6APq`(X^ye#kb5JY-eNvY)w4LG6Hzz~_gwmUiyr7##r>b?J3AR-| zQpM_uci>}QWwDDP24GD_fWs7o6mgF+n4%35&eo?eff9+?-3lP2H`{GT* z$AJj>l>+U+#JGIVvBhe6axB5NMHAXZD5af(4In>%c2AyWS7z48PuT?s+S%DX(2ftB zW_|nTKfkjhOjSa>2V110X%!F*#L(!9mop!wR+aIv{_yLzhL3D4Q~iP!exHaOkiY|( zg;<1s>+$0jAOz}Tf^-^ry%9^Mu&-MT5*>wr;e!^*0Aj28%0mtTL^aU?1Qjf@maBxdB1dlEtl@8$0t$+A?r9+|)tV}!vLIR2wAfRDsPE-IHmG3u5LLETjL|e2xk(=?Z zDg)wd=t0w1$@uvneDX^HqL=;_A?_wlLQaVn1R==3L2*f#2N@Da*s;pWJ`Y@(+k+1U zgn})A{BC<`eop?CCDs_tUD%T@a~W|3Ac*$cTbp+_Z`pH~e-voMv~VOv7(mjjD3qt6 z*C+%)oQ|*%#e;9cKV)K{J>}pr9V(A*)0jLu$hxX_ zrCTeO55;nU2o~~G08!`!kQD`FMWjFbWRh|+iNrs=+0>f>N#3;OcudM`)j<}zgkT@u zYhkZujlw_VPW|6WN_=GH2ZziR;UfY_Xd>YOAVuEP-?W9!aI9fb)QT!Ip`l~ajUcR4 z&kld~%PZ=(QLpMcNY`EB8#r{{@}n}6v`kKIP_?C&r!xx%rzrm#fV_qfsnMLjv%R~! zyELzcP3x8{O7i-xb^#&*pT4@4@!`*eP9B0IDQWQG z&-Mq8^APd~3qeXq;*wvB06V)?5b>cN*0(Y=l?Y+IaYb z5jup7nt<@RM2Kr3fD9Tx;F`*?#ONX{>N-dS@(u?lS@G-7bh{t^<4f@eW+RE|Co!Bh zZs;P@Vnn%W!sQKht%bs{4f+M?@%SnM2nK?DVEL_^>+3QV@Pho?UD(+N3K2aW*4bg* zcQVD|I)IqUi1cC}*F_45bsu&J$=xApApR_9#NG_V#$VBcXMo(`7n{Y`Ur3*Y0-^>| zLm+P+um*yMk8V5$4^!d;1s(+u)LcAE*E_k>m~U&N-W`xT0f+?yO+eVO9Gn5tw**42 zc%5lzRSYN;fp#beNg^hnWz+R{wNhZRQS#&YaA}~=@ZT8uyU-=14TuRsJaOK15GAJH zLYxi`p$Ml1kj2F>edb%kv@S|d5YjIEan?n}0~U7O_m?vu>}8XRlZ{kAxbT&2dI;&& zL-uR@gkY8aLdM~4f92WI{%ajQBIQquuLEMw^#=i$P1qo-w84qpH65WChy)+5;KS7H z0D&~Vr&x###+8Q?U?Das1VB6j@~%MzL>IKy7GA4g$x}MJfZPlRF-929X!hX?n-m8@ zU?Cntl+KjfkF<_Okgl^6j|d)L5k&HOyAqUOBw0>HRC4n$UL-H{3?K|(c+(?7)TVBYV$Q7*2xj z-W5XB47BuKRHqO?TqW=@=ior8w#R|w2d-s4=o@rAT6`!^{26tQt;Qg%N+MIr0YpP; zJVfdAW$3(i1|K+?jKO*o0s1DoS>$|#gScMAj9Y(vVSO+mGD=hc@@)XYm3ZPOgB8D` zcmT*sMWn>-O+qGk=n@p(^)`&22ZL0-HcVJ^5Ajo=I43dc;#KI7= zDFAT**c^`@K$h}_^6DN3oz1CP*c z8~djgk_jOh9sx+@1H}WO?a@-#%fr;{L2F)?AJMO;fEXWjv3UMoA}UzH2TBeSbW}ow z8aQzYKxs)io0x7ZH%PFq4)XLj(wWZiOS%(}C?9Opv24NIcK|{*fd{#%)&9}vP!U=- zS`r`~LNXxze2fD``&kr{)*@PjCAp$yDj|r&q2}Nk;~-JFu?RiVFf7C)Y$>$g_{_IX zL*y7l2BJ8nplb>s(ksiY7wocXL1qXrgK6XC%a%m`zR8yZ5*_5*kO5+A|B(Ih zQXj>zcLjt=={M+*VZ=Ltoc*d+v%Yfn>{pV5obp?ZViBH** z5YBagqXuGBR4m>FNS5)T0+3~P6_Hku?ooPXVT^-B4GKKMKZaZ-3#o)G{N;O}`3A!| z`Wvy%7#1BB3@o$dcl-@M3qY1=liFLl$27l}&-Q7e-Cu_d(M17d{pqXczuUa8YkFF7 zg9j;T0CMxvx-|X@In9MeD7FwAeT_8XUT%?m#YdSri@4gCO6DU0HoHih|z_Q z`%=kcuyA#dQq!sqf{vL$#G()&QBlO(Y6G$w?vWNu0F_k~b*wHR@!Hw&k8w4uQ<^yT z15!m0(gDQ#)axL>jDo+EUu=Za#dUlK6zp1o!R&uXJJZ;zx+)C!mw(j%M)MhCqDEsd ziZRiM7=trt&?qV@ny5i*9VseR+tS<#(R3pr~B!<*PhO!UjE@1!DTo|{@e&L7G@WZWmxk0GcXWsqoVu! z`n9dsXp%x(DD}h~R7dF0*sSZI?L2eHB5EHh(gz^BKf7$hp+A5MpGZMuD<4yM^%b&S zR7F;`3VAaYVRI%V0MYJ22P7@mi#SSoR|8Sf_*L;lB|T$?1)Bev4{=YrM^%rSeS^l@(yg+DBbx0A~6UVKG;wi2cm$egRogq zxAIQd5gfQ#bPCUzT9TW-vV#O5-SvQ|$ww$|0!i?Zl=yf?(?ME&q@gZkWQ6L(-2AZE zM*#Aa7d_|^PXUlFF^D?I|0Uu6tNiEP#ROFJfA7937k?5!bTJSX{Mfl!n`K_TbN6b) z!1&PNhPytCvrrO*o#>uG3J5`gyncC5dbTYh@jh>_UU@TYC4|!Nj1)ffhI*1eVA3c@ z_y7%8@IgVQsh0|l*%N%g+xHi*t2PE7Vj#LSqBDX3MRa7PA(F)0-kUnS5Tnkl_n7dK3a+xgR8HRm`dqLIR6+rRtTvAz?n!W1*!v*4M-Iwl4MmqJ1gw>f|hz zjSqg`sz-hCp)insI?ieg(yi>1J^gz`Sl{r50kTEA`T8uTxZ|sZPzH9cT*Nx%3J5Ju zz7kcR>ndfRNLi=LqQeRYQa}is%|U1uc0RPr!T^D+w1$EKfGiYoJ}esNs~wQw14qCL zc2(`mfS{cl`2|9>0`kMd0J6^c&;Y~$fsjthUIHEt2w&`rTLTDu0Ei+2AnG7{NDc_A z!DAo87!QD`mgruqTBNyi$x`a#Jq8E9c>qQf2MDNw4J3E0e@_=SF<3Y=2XR0EMd2_& zQf!_~!?lVHM3&46K)MlPkNfy84yY3#)+3^?#+@C5&i;{WHgVGvS=k5!dB-!>JmX`} zdTKnINy*qeC7}4G%C2T7gxVE&tm^&C9~6x($Vl-Ssp2726L!j5T;&Y_>L7;$kmWwH zn`mSA>a}^mmHwmyqU}i{YjLu&EZo?;{_`BB{C4!<<8Yk<=zMr52>&#p4Y4mZtkM!S zOpHt;vQ)9ugqE)@?aK^jv@yKq7p^0onv4V~1njjLO9%lG7(vuC1rXqn?CZvTmiLM5 zB;;2I$6q#Th9kTR$+@Th7yNE4QNU-($MM#kK3 zk(6}U36Xk&weD=#x0`o2$tPaL@|oQ{BJqTL#En?x5NtdG(Y9V)U&jYcnfME@fW$0u zn=+CXHbKBp`QRhF<;`UJZ5Wb&Wf=pK0MT|g!x-zZ7|0D|Ss|lc?khavwJ-M&Cq!d+ zF2qMP3R$+-=7re8)wr#!i^WG68Zk&aA;4idwV0kNqer>|BFTE-1rj#8;zN~>3`i-^ zk)VS4>HuPRYz-7@Bi1V2*vmBb0Azms5(nfN?SP=!0m;^YQmnvN*zi@d4o>fL2pkYZC_sHl z1L7p@jN65S&{U-ni1A^;0vb^Zv9Y!>f-r)0_(0Bww{!848wfWmAV~J`L3u2IXcQv< zDESSx3@@BE0b%Qi2P?v{6H3@r3WzQ9F7b;A3i%&qgN!*5KvWnAH7o-}|EKE&2#A13 z5`Uk|v_qfYkr1g0MideDuwW`D5zfaDtSs>kK}7a&VrZNO3le97275LndmbcZ2T3u= z{|<;1;)a}#XE-Djf7?j5%kri()>kwdP_*JAxO0_If=w5^m7Vqd2Oil*oTggAVsLXW zL5P9jf5kH&5?A8@)V65=vPLJA9mKje00cQ6cZfj*5H%2cdnSM|lfwBJK?LsrQhr9< zC%t)1;0QjVFQf300NJIf8i)fTwIH1s0wX#c1IX(RSZdIs21g!@*hfs1SdKU7(7Vu z;l1od5yyMv!M@=jN{Cgla1e#U0 z)2uI}!zE$0%{%gftqr>VOo_Oo6$?qZJOm#Gi2kK_KyZ)%#KM;xibph_0YMEwOu2^T z79w%NpwBd{T{8TH15&gywPqpc5IlG*CvWD2lEMdoKnP)oc72o=v*8mE;()}`)w(X0 zw(bca4W-soN+5gHoa5U>l;%!=*rAmXGG&q=-rB$PBt|QGNKJ93a&#)kAYiFU{R2j* zW@Q0Gm5||pwDiZP2Vy?1TbH+EQT3l45BnK1+c$BJ3R_@eo?<07$saTesWq^U;SB6m zLbUkVH2hfz5kP_u_8?OF0?F3@kpp->jERIT4kCbP4a+(%ED8}qc8hpU5#nLt z!#p4eX;!bOFqwpqy$*YPSD*n>I9DN+bvV0`d+|3Pbz) z4elTAAh5yZHWD|jT$4C@2;x4n$`V+}BV^yrCvoWRATk*%142)XwI8*K@B~Q00$isg zXZL7>#t$_@kgHO=d&-9%CfCk_VCQr>e`F+8uxG4U!&)PqsMNK3Eo8F^3n_d6h_aJ=*o_aQLC9_Z*;^uX zvUY<=)k~IkN9G`o!B(=MA&CQg07xZd&Z#-fAkTuAMzo%lr${+QAquuQ$f@}x!v$FE zt)8pYK`8xZA7qI~B$awqXjTV_@{~P9WldE@k0(1FwHQ*vCy^zUoAXJXMXiU#u>yh~ z`k{yWhlpS9844_k_$RHp`)BD2Un4WcVY-dMcFDw<2j~NliGt<5`-Ks28AU!kqZo~B0LgMv>VGqE7td7N%-s4DmpiLNvs{D?OPx^x^ zWzu@oyGi*mG%S&Y5O#7_KS8q;VFmZW@1Uf=@I#L)czC63ez5A__>f0Fgw8&CpMQAg z&v%zln?;Q;_xL>hng<|hDXS_V$~lCfGIzp4bmxPEHdHP4u!DoL5pQd9gp?sghFjW; zXCa8(=hh(!kWJ9R-QD|$LlPj4L5z8f`zbVXIUu_NkP&KFO+b)F@9R>vUgMi zF+yO&1R+`VXJrX@T>xm=A^{{`q9gAIxz6ttTn7pub3H*wMMZ9#mekww=RBenooOXG zsAwp&k%yO*sZCBrXl|5l6Ld5b@sAv8r$@br2OA|=2+`$xRY8h3-y%mqrRR7DQbIzB zpK2bh;-f#`S@U?7)_Ll~p1_CIR_-$FcFn5Cdno4r+_P(#63)WV)JjZzj|_<6;ekyVu$~D)MAimKnd_9vutQei0+o~WOmrRU z*KhD~CLyb3F*sN)r^jtU*{FB~HHe)awr<@DDogA!lBt`O1Q=C$dX&v>LY)j@?)(f$ zB?6^$j3>T4O9B%%Nd`u^i0!{=>QvibRaLFxl%!KpFtLC_h-Ol!O3*j|@QWK~san49 zp-)vnXp;quM?LFBzy8VHyN_oT3jmRSECqeef$K79Nh&4mSaj#lEWab`OIJeXSA~Ht z>6QKu=s>ZuLiGNLwA9PsrB@FRuJ&jFDHEV$K$vT&jUc;KmQqjyv2R~|)yAICvLt*c zd27>mMitAfA{vw1ErbtBkiM@1QWj??$-xLw<*Bs6M^vnuG9;qix0M_D=@O4{zSsHa z@9!V$?_co5enaFaKE!S_fCwMXLz58LAP@-wOGDB0^#E~pNm=R@Ubc0-mF&0-ooaeW zmq4e%13nb@qZFy5S@&V1udqxIiI=uzG@44Q{yxN7!Xie8inP-pyh++a0OSOXwk+QI z6fu-{5QIPKqwjuhPtS9IwC2<^WGTySbF>A0?rCQA*@QV?be46gAm-0EmMExfIp`{U zu*Mtlx$v>tXO4TlNt$jcd|HnAP*+9RH<*KkKEos0cCA^-)>l_X0uXymCZfMYhWGl| z6a59@AK?_04iSjN2lXQ#B}Vs#)Uc|7Y%l{cDJEq;V5Y-bc)iK0n@Y5yfFS75?nAA3 zm5&Xvqsm7=Xt?^-MO4yI3?TE}K?09ZCPFXRa*EI;SAEDK3WKWRpwcqjBKdc8*8I+j z?!^SiD)Oqb@~?NN+cqCSqRUV=&H|3ke4vTWYy9~z4*61zy0mJp6C90$to5i+jd}^{&kvj`r09x_27Vrw*2W&P;#C3 z35jxbvxpj)4l%Li)e9kR2b5)*t3<*nQbZCVm5=IC8$~vZ7Xt~|WRAShl`xa}$T%Q7@hfwP{G{Sxp-FZW zTeSf^79{nw z66#SP77#o?Em65TrzCc15}O*D&NNRt-WdcXE(#zVMNE ze*b3xVLrg4KJ@q{y);@c{mJKge)-$y_RQ<~@U~uAM`mnn+wr-*TgR?kxN7~+-i@Z^ z=}!Q%W~m_(vtlwIYlzT|51qd45k=u+p8=wLlyN0|R2(EGm@|%7Lk4d76kD+26g(;+ zd^Ksc1R(%|IJ2>YmATZ6rr)pD6o_!!=m|c|Kmrg2LE?rc1rFIkjES6D^uR2Fq2s{eP(*#2A~1_YdkHP-`@XE8pnO=%)jA360}v%dEkwZM zo@uuOlI47?Rk@3F;G@>z1P|rI0Ray(Mf<|Q1>+Nj%>8kg}oKN+< z_hkym*%f64S1wTc?%eS~uuwu%)?sOmWxINYwcRigY$UKK!a!JvLG43@hluQL znDW=3wx$4tFU(`vgOEYd;_v@_OUyw>HYhL%k$w;ls{w?2z!CFpL-so?nkkIrYL)@w z4FttN2tcfV1L+VfB?KuZt{LY8IUf{xV}l&RN5I1YK{X5!K0=%Wha~D(?i?!?oLFIL z#fb%H7m&4@1t8rKi6EU_I7lNEmCh5{l@wAy`1F*fIW$#O2eC>-M?h9}1|(Xss9!-y zdK5tTZVUuILfr_F0za!VM;Qy$r433Alib&Smy0AVx#W}bUW3EKyZ(_;Nh5LHb}8{Z&x4D9`&}azxIplAnn{daQyhz z(?{0y&iUhuKe6e;U#?&H%U{0M^P8TYp4Z&`z}9O=uGhU>X_Ppp%*dnCbM=q_ za!mvlYwliS0}mJWqbm=P!Go;rx?lnus$Fs|H85M_Hg}>U17r|MZ^mK(5kBfD5+@`a zL}K&+#41_V#oh!&!9z+2YI%DzTL5BHODZ5Xgm*-ck@E}I3gwJ&@D|(LUEXw&dRo8| zrmTA9fFLLlP8<@cVp-K{{iYS9^`6lQA*5#;213r9WK|aIXqO?lsPbE_gh;B|y5yAP zQ{2mH0HS3-kYMW!h*YoGd=4R;K?euZQX|BY>`)z3B|(LQp$9$54q{jKL6Z}YY5W+l zBypQ&yW?B8o<4ox;>90*?L8m)&QC9{Kf3eSr(d)G6(9WZ^!N5&-+5K|fDlvx(U)j! z(YO5xAQXNOK{rBy$6x-!EJ5U844SO(&0l%!()S4TMSRY_)Ub4_6c?O1BK#~rS^3Zl z0mS$aK#Y;P>Q7&51fs8tLf*>~h{RMaTe2Dl@mV0=7gzz20qq)~myjd+{8Stb5LF<; zy>f(>+fa$eas2T;Yg>*odZesC^rUYb+ZpeUidhD>`Slt z)O$XDak^*T_kJ)j^pdH`$;r$6cWjWBAqkx8bX4T=huorB$pG0+GZlwcN;cfMz7sl- z0^*gdM2Pa?BdOTxpZc;uyN4i@QJrgR0lSxeGY4PYr(V+`?*=kb5aD(+PFFs}#eD5) zQL{m-F9A|sMj(hJKv+Od{lxijK+H9Q1p{OuTe?|QFXb5|Z#e)EQ4qp?xKRr+LIesB zslq~fwuzv`SYY2xvjfsedY8E+dgv?%q)CWRtLhF&JZvKv9S_+ekqa&YkUnPhwno&E zRIX}GEDgXy`Im4^-l97ffSl&f*usl1dF6LEE<1m7|M!OW+?u+1^X4}`{o`XyaI#fM z6p$2ud|y`u^`5({A{$FWZENRF!DIuv4j3V=NNJIF860eWIcR`H?7_|qWVacJ71^@d zE)QGrb-S=059ELtAw)RpAvwH3(iW!Dh^P{>xW*#7DQ!jwGD0?ZHwYGn1!_~)fE$R* zD^(#Y`syO?8|8j^K+MDNfQJ~6=!7^RRI&^ai$g*Ih!K+PgN^iG0fL-{8l%kN(*B{w*L9kdvhCid;;C-0mO>iFUd-YEL>1m>nJEInH?pNi|ZbQNj{FN_bJx$Q5Ag$IqW)d31FJOCh-kF*Z8R+H=#i%57hMq;5h zKt>86ik>a2ko;p^^)Ksi%!q`P#{dLE9FSoFguD9)N}?CTuk|W&uPe2L}~# zh#U@5zOA*n%3k#YH=2s#R~Ca(ts#?~kaj!}i%nd+aACza_V;4|yM`uj-@1MK-1NL2 z2>Hejes%Fz-+kg@2L!*+PLK+S-eSB4y0ct%Y&Itx5i|phEj{faMht3SXFNomO9$W% zmi8u#JyhP{<&)7XAbQ2Qe6>T;0T6BlAbtEt)TK}k5qa|f#hxc}tPUcb2vJ0$q1TpF zB#P38EDN#~jX+{ioNCc8j5MgRgGc7>9*GYp1VM;-NS5%y-T?_hjuWwW1fnY-0MIUh z!yQB!Q6NGMK0sz^@DYRr46_4*q%*BleG(gNmcl_QAT7;TdP@P}0Tbu^!T==ru!Mq; zEc^GIzIJ`t3trjtz1@9#Zoi~wyhqqb8eiY{{o){p43#s@}_DCrK!w)t}s zA1Dv#ivpXjv#Ns#9%XcH9qTBXUs8HwHDoy+y=$`U-`-{=O9hY%XZ|?;%bsI>3-^z| z@|8cn`9|!+^qYClo%hS0sY3z?Fo^0jinN#lMakooJ38Eab!o8m7tkyYK&~!kXJy}B z#~(q&;GyN-P0x1$NRTl|LQaK)0~GcxZZqla2mYlNfdhy@ux|CtKmU#p2ue!a_VwFC8IDhyzjr5alB}`r>UjTEtxePGQ3vu7HAMWaxR71LCzT zMk$({4jtHA9RxxoiS%6vB_7e#T^$|7nW!ThMW_8IDJ0zCmdF7?cOb-De=MM0D-FXe%wvE+4XX1w43OQWA8wZ} z$g8rYP6P}#SMIqwS%-jNpCn=I0+lc zgm}#3A(J@>fTSp$d&6GCLh&9bSAF?EazLU^Wy-$MZ{jQn2|$F9`jq+#$M}h!1E#=) z1%Bo;RY6W;4TluGH`LKVipPYToYvja(~87gJ)T}d;sZgXwE?(ENP`xqB#JIso>wI5 zvo$*DXdyOLgeyBVoI`dzmjQ7;3L#5_kWdPFc=X7HAM`vIuKh~cB72|AcQy||NQ&p%<{GSM#`#IY_jx8xQIcV-Un*HK}gG-TDK5=LuVOug`^|H zaveAjg5?zqAnVZ|6jg zTPrUR5ob$no7)=}66!vBaSquXBtLJ{qyQoSQDC5lV?%rf1zlxrU3#n;Ktw_ZNzu@a zJBCeA;tyW9@T8uX!NhOOI6k=lO|J<)=ul zzSSecK2Tlvf{2?)00JLwhuK4{FWu~CA2(Q^n)+2~neuX9i@g_TEAzEiT&qhiK8tBk zqbiFjP%9tEnc#Av!H5cU)j=AJC?Us9-RXEB0g@GTl<`_iPt%?S4`v>{LWU*o1RzOW z010J2yo%Lu5H8F>ya9?t?ymB=tgE6MxM_qTS=m5NN2uN{h^%ptt{?wk&wJ;|6vF2U zBA?EH5U;;-gSBGy9sc2#nS-=(&CpOv4j9Z`3=mtfN;36)EBllqZ!K~gFhT&Nbg1*l z?zIkvYAxbpz(DLc6Ez6ozJ6Qj5b=lXMClMb%pnE`{{bMEF8>`uW)=?)59guj0~T~c zNCKn}d4IZvHQs7f<$!1lmiKnF(_v8pvcL7tPjU_yV5}G&T`!6sAvqzAfsv(aVxglJ z^Dk03dk&0|0HmW5AlS*3NL-P+I!Mew5jY7HuvP$I_#X;Cb-N$6D zOwBccASA-~=rYWT&>4<_=; z#0pNEWT1dV_V#m1ha^>o;I8i)XlRn$%cnPfO+8BVs<(dk`3gmfL4Mcs(?5M5Al3~p zdh1w{A=X?Tvfz1BwgWk8S-;zlBTI3WN+TwgoT6%YX|m3X5|A#tCuX@lM% z=7Z8n@~FY#4Ng-0Y2O79?BgH*_~$={2S==J)?}|7FhT^77 zK%5Vc5h@zGTY&(8D@P>Q$aJhv;wRihf)AZ`F0#Hx4|!ImvBMFAbh8jJ0Sw(sb_-El z9$*tWA4zqnFF9OA0nvkp?Y*)EAZ3lA1|W7hyH;tXs?qRdJ(Des%oPx0!08Bqhs(*( z)SzQl9XWFBwGy6x>8<@|uD_&5t6EXR`pvuxKisl!*`C`|_r{+O98=Tx?u`%Gbcwz+ zM!eS7DO=u-WJ~0#biz@Z9LiBjBBBXMnJNM&&Cyx_ zQnZvUi2y9yxOD!op+g z7aYBQ=xX1YFHomS0+*YIe*gQmx2C4Y=l$-vueqlhKeyUOyP6_)&{z6fVFeHP01)aS z()we;NC2|i`6%%=fS5RHfwca<>>(WhK?(@J)?x&azv|!TvZ8^RnVG--!yN?83^*Sx z?;OxTeJ}vY4O!-A@S%9b>(Zonf=6}_10y7kp!KHytWg&v@G)8Fa21}6sHkAUN1fZg zXy`FR6Q6(QgaNYG02zajZJCh_2MO;8L^yiZrmp!3i8eqWLrtW$4Or_~ib+9)3>Ks! z0i-Dkyc1x_32B3*7!gE4h<-Uq-3qU2hCIfIDVqR>KzYANipKv3KCl?!LssY0S4A#9 z^j!xe^!)Fx*iGNvuF10oipui2y=TL_M3(0v62!N>g>?$-)DglK8kzQ2v~LiHfT zRaUig>|#x6t6e!B?jA-(LqUky2I07?J`EXc?jRAgSYWRKNM7Y+&znEL{^*;}9rSsK zeAF|>x&neo5fHGAu@6W{9UTNwNIR)^w39L&h+|Q&q^5rW6{?I#Jb>I50OOQrT?vKE zE=z|-G82-duq?HtBtnOXlcr*9V=pp3BK_BVTvI?8cuVP^D|_+cix!T|d)dog`LbHk z8b7ye%MX|DTDx}7FJ2+~4Z$Haq(W(_vnLFTkLh|!7VC?DZb zxJR*&ocQ(GJfNdNNCM;t+qGWGznpdlz2>Vtfg&t@cxdQxX8_^iFH~43?yoObXL zFRiTuOZ*Ku84u?J2_MJEHEFrGkDMJxM1O#6XrP9HC?5uh;~}QC{s*IrmVJEBLJ!gTuQ%0uN+-;33sLFaMO>Co>e70P>ny74hIAo4)RMKojObyK&lQrc+BiW z(t#SOmzozKbQ6#qmjhAi3}s`wmta3gAW88BqwA} zyOGS-%oaT-5CS6!ldXUw2et}Gp(8sp+VRMMSh{cnLKKkUfkhJs51xDjUFc2Cd&M_yjz9O_)cKS5 zXJ$qQM`qAkSyfkyb9wa*g|$vzVozx*Z)1`IJA#7Tl~%T9ib9 z%t{_2a0(rYp%{q0U6vgZLO=r|0pd-)ZXu|Sj`S}VKKxsS5AU-p^EbdF8;BF) zF$G(JBJki*lk}gg zhy=ZZYY7_a!XP9R5rP16Mi8O2RsjkUM`;kkxexUftBA&9BOn$7>I*{j7<_05qC>wb zAcBVDu{i_6dBuYiug|nmS-R-8kRHP$y-8Uk5UXB+M~cm(dKD2!0i=3HdLnpGLXdZm2GBEN! z8wlosveDyxq1zLq_wUc_1CjwD#PDbtLra$O zWZib!bKuM6AU)dP!NDZ2VnxMYnecu`Uro*d2@i2Nki{nUie}@(|JJ+iw>ZfC`_!(I zY9&hmQ9?}4t{S5Khd3Xl-`obA2OlmEID8-7ex(-H)ad$ARl`84g(ND}EL?e=cTvxY zvnR-k$>TF38jp-6b?I?Gkkvh^hml0g6891_joSH90bvBOiDsxJy?^}r+b$juq)@~r z(BVGiwvhlSG{ILf5;H46;Z|=mAn%As_=qdc!$I=i5rL3v?jSQKF9X2Msr|1QFHs0b zt6{zJC8IOjry=AX`5sMJQ$r&gB(M+MLp$O$lVuB=lZD1*Yd9p0u7KBv0 zC@c2X#d^sGlmAlNd(UAL5D7O3(f=AXEC4y$5Qr!s!U%+z__}zPwDu4hA0B+{L0(2P zA0Ci%LPBX|Zho5Eg%25O!VW@0c^q#HfV6!-VrNZaQD?(J9!V}X;!Xl84#$H7kaj_b z#42?|od>K0Bet@l>9{uvgNWY=2dRkQF@JpRueP=HzuV{$gM(D0-jW;5YI~V5ar!jn zyKE@a(Oo!ArNr%ni|`IRWCJk)$g2V5*3EJH|1Q7M!9Z_+-Yc~s>rMObotzn(0*=u1 z^!bw~C-!AL7^>^?vTCAaxiTR#p*u7vo71L7QhdY0^*R(1LZTy4+qCSL)sgL$kOm<4 z??Z_5@mDU43hMOK>NWOFZnV*<$8?!*-8_3Q_g`@{K1dkJsR4ajx*>u1{`A&%{SyZ-?GB(*vTwSJ_94Bcg z@~bbOFe7Np>P=%Ki<}Q1Yac{G#pxnWySPQ@G)OhsS(7A)9O1ye*VxGEt^z`lxPSZJ z_-n?eFI^so5ar@!FL~_;sfA=j zd~jAK#9vP6e4qjd^5M1itJoVL$bY4`ViiE(13`!KQCGmti}CoHRx2eA84wMl0e^#( z_sU}**05`e!XAWZ&1#gwOu;VlQxNIzGFHp$ST%f4gyi~Et} zd73;oHQqD++E=_EK;CQhtT)|0e{=lY!O3X`DZa7NadYa@1P#MdG&2>9z>>sCw7D7| zRc<0mh&BovDYZK!=7pD6q{BxO5I!!m**Bv22b5@GgO9>0fEXeMhFzMCOt>5e$;S^I-*&1aAF6HT#84456cL7=jBVQHgXcrK7=*J> zbbQHi05Jy%1L-A8%KmXAY#}@%ZYoX5LDEcgM}jRfgfuEx5`zp3?;9MrbyH`0+}g3f zr{|^5c~y*6{PFEc={i@9KX+V>gfQgX#DFjCNsdTjQ0Qm{#K-q393&@10b$rw8j2Vb zpgc%#z7PsNA_Q?h(j*YXrSY*LIS9qx0r`jcN6T6xhKK@UL&!HT8<3uNS!ve^lPfZ( zo5gsnYK4@K6a+-yct0K7HjX6_$%Gghl@5}9k6r{K9@$fi%g@)}1sNnv1SudwhwCg^ zRRedsZ#U3vbujB_1djv=ncXQ4S%O0=Aa)iKJiLy@A9%UW?OOmKS4{~YbIm|%8?uQq zZ*x)f7=cK_gN?)kvI{U`%zG$%3EAl&U(o86q-6smtJlt)o4&^=tIOLb&IuJ+mnU!D zyL@}{{&V+wbVA7Yh8D`&&8eQWFO_y1^5H6gP-n?5VvC7b+E_bJAWQ+te1wIR z5JY!A2UcgB#Ba87CKyNnq9qkXT#HKoUM2)Y${>!-0|P5f{^Ml6Lk=2$2p`_&gntw~ zf)M7E?^(pt-eJ;gn>7cCA0b!yS2Q4rSUd=F`|!Ox$O!}@6%*xy6nw-7smlbNB{w}p zyHdhHj1EiUAfbkX#Hr&#Ml#lsyOeAvgF`4UAyFz`aAU*l6CKE~(98x0WUpSijE$y) zI2%nsddV@?L@ng5xW^6c0=b5?@n-bg!zDT=8`i~9u7w1qULe{$Le3*fVg*56t(rC5k<3~5= zp}wd(NCt!kFREmnwJ9iBKbwv@Adfon!cCHAl>?$&Xk_0|V3GwQC8PP8G9a9svK6`X zE>$f}r;@=qHc9{&0Z4WnLCFwtVHN=E9_+}<52;NJ`v^FWL?0nbu3tAq zmaQD^2ENiYiDM5QE=DIEbO1rsS!G4q@po`GA89r zJ>=ECK!PRNX(g*7BAjvXDYQ*M^t^^3YYQN|0ubL>qm%K`Trbz+YcH)vhX&Gt2#g$j z*R!`TdbI`Q*5Q#QY)SnJK8o5`aN|1{5ziDpl*dr;fnuaq6OZI1waZcEP7*zlC_Bg| z$+HP6EIS=t0U=t?s$;pX5Qub;BI2itMN1Q~B~BEK1|TGd!?eT^vEIk6q`D|q@ZohR z5NIG`kRbLv)7Dv-gY;m9nNh_+?(%kX7SO#e>q}o-KlJ8v=g#ezoE~RD$<52R4!)u1 zHH0JcoDa^P+cQlM`o7`&(&08p(e%iBWM`%W$(hZKDtuU9p*IUD9LU@xfGWrAAVG-D zb1$c;0unf~0}!L5@PX=xL^*iOY;?p5>fX+z$NpmnPi{90A>cs9R7ieM`OsLRljIAJ z5`n;`0^)ci3n_pct6E=^ixVILkZcp4qZK(b!b#62*9qIZHu)jwv8W{LL?fLI3-M!9 zjjnjGur0tbPZ2rCl-dPYWfejn2N4qme)QnM{yn3k3k4|ULmZ>?}yJeT_fDLAYa> zAa=rs19FI0{k;aPzgA)e?+1t(Cqe%R0)UXmCPC%Ao(1<{|*)WYf zo&`XPoCEebWdk9Sr$6z9XE~Ufwlele3*589K02BSCk+`IdLT(QKuXoCs*8g}0AiIa z)#=QKxJR&%@lY^qhbZ}J2%~$4AIKb?5%rI|5OQ6-0D6~&CgAb}feGON9A)q!q%nMo-LAvEQ!Uup{yvQT<6R%|MiPG#>%_})&M zUK7==h})a`4+y#96XCPTTshFJenCQ4K(yC{U6@HgVh)m}yjUn6-G71vS85<${!JXk zON<7Ggsh@!WqW2x0mvE512767`=kYQ^!D_5@*X_8qckk%l zU59`8{l&YqGPR-ILNXwL!!5F?AY!qB8OSmufI!HGN{G`G%YP~qX~1I0x(SsJCqylz zjN^^>J?Z?rr~(BaY;p`fWGs|*`(;1`4|NG9=9+4^UvZAQih}VFZDO@H7i73R`e2Kg zyqklB;ym0(>Q$alI0#(C{)A=ZS6KiQ`~V1pRaD-W!Y-tQgaUxpdxi&FNP>9!PEKV$ zP#Yc&h{;3wxdsP09E2pJ>%&hJk2L;}O(}`;4+`m`?&@c7A%K{HpsUc4^oxPfb&GdR zOikaM+A%>()%}U>w_n0I_iw!EjpMI)=60&wM@NU(jZiC+ztBi}#4{lFN`0dws}SM? z{sxc(k_gd9K!mJ-L|>uvVA8Y72L|F6RVVj)gr5B)x3XqHssfM&TFDYVJk(YUY`V8m zKF2!CCOm9;r9EYeYa148%#35Bf+PfTp5eCA5VCkPLuRk}V@U z4Y^tAQb3~5@vNsHB%=0u)}$umq12E#1R%3J2m%jsQ8RJ~)2)HN+qv{Koe_KeTA& z$nISu{_cG7kXN?@Vrsg}h{|7|92Sy1gh%4d=etlL1UVmEglzE?flj_XO?)p6yupV6 z5^LeYhfD4l2Za#iMpEawE;ItdC-_DHapa&!R6gV>t4`%nx6o0vzExo$T^&RSNLoO0 zIUW6V##I0^tDKHtBpSMmg)oq2pkmc5OJe~`$Cj?p7>HQPF-d1WNE(DxK#;{6TSG=j zi-S}HF*ypMV5055*oKl44#Mj-51E04w(Q+JJUlUZd20Idj-iQ(QPIT2_MvC*mmb46 zK4$OWh|KV?DvTP4lMsYdK;nZ$2tP-b!b_gaseBMwZGj?ITwYm<>WfrcQ-E96*bOlTK*2SV%*yqQO+t{NTkcX3)vR)Aq-xa=La zmCf=-0wi=eA95$B_f zNr_ORyMidtaMgf4zohWLaF8ZJVIf)JHR*m=R2C#Z5hyhGLq@+|agyToE9@PTR&8Ug zV0KB}-(50SHgy)OVfmn}((BjG2k-^WCJZtVWUnS;+hcj-1YseM}lkwZh< zZ_}H;4V zW<;Ej3Pzw2^TS8NzQQ_8;Ng6f$RhyZ|Fr5LTHu?DNi$I0b=>2P#x(HsC`!GDK0UB0 zwGfmGHJY6er<>u^wglb4qncyVRK|)PH${XWY zSGz?9gLWcjIxrOJT(`DgcaWt3g4!84tvLtk84c=IoN#91i1GTEYzA%~FqZTegg;>wd0*`d@S=wt*OW**pw@&N}4k02C7Uh9`CZ>iAg^Gr$2v3y|6vMIp*xf;b2-$T6#JK4>CIB3>0?xsK zU~??u5XuCE8g|k-93hpH1#HIO+;3%+Z6D;Nf2!-n^Z~;ASTFsCf5; zGax&&5TU>TiBX5v7Dd|NWtQg=2g&9q;UF_n3sR|tc<3N!WgvVkO=?Vd3}pN0mrrQq z)j)H{;FT!dlC^w>iJ0`v+ix=|nzJs1D@#}kxBN~yA7T#$o~IB!NFZOV?~aCEP3X|9au7OM_vLhLdiSrrs? zMRPkbWyVNr%wD?-tAlVXDKJ6VLAvY0?k&In{nc0RW|`{4(t-Zj(OZ|Nr!NofnA-Dm zu6Ipr?%Oi6M?zBJBmPPYAj2x*AOJxCQslR@Qp*;f6$cSjKupoL<;$KlWlhB>=y1}C zZ|uz@uC7?!>WYyeNR0I_IJ;oM*n%gX#YXxqTDK=rr2=tPz$pc~yZUuik`9FAXr#=X zCD$U|aF9*M{^1>aShvHIof7WBOSX+Qeo+2eEP9_hVy_cX;$c@VJ& z9zAz-I5Zt39EOVkC%Z_ixK{!3q*`Esdv%l?z&9At3Kwb_@W`c|Gzlq#;@e#$tJiEJ zCvvYj2!~vwYS+qF@4O>DgFinkb5@r#7Hi9^4^B=^pFck|_zUi@8|nM`y8F^MrqOc* z8^fm336cRpK?qjRL?o0eLz`WkCL7?IUz>Uv!NL_wKSazCYqUp;Q?y%sytv1hOu_^^bo2t6%*JOiU#r zi83D5vY?`^w$((WX(9%PkrJv-LMF8fND3w#bORm;xl)9wbWmqRjtC;X4Mw;f;=G$E zj2j>XfyTOFCU-36jmUd$Pft%x?peEh@xY$dzxe*ZBJh9@0VD!(6qn`Q8wC(Uq|C!$ zD{29zkl>3yBf8l_c0+zM-zr-}X%U4Cns;M^M;=r{QbycCk`tVDO!9ceIv!DeG?ZUy z;QOZdI9`O)iI9ltBbJX4BsmDVQPqkWwy6Gc2-*Ia?>*-w&w0Zap7h=I{p)}F`8Vu; z#U~&0`5y2PMqdBr4}bmZUwPV#zx|_&Qr)3kpJfjd7k~9DW#!ib$}s_gyQON>h&nnd z$RK4cunZXqn2ZQ2Ck-^nC8;=Yu7Z^Rl#onGmOBW1Bv!hyAv;Rc(Se`WAtv8n#~`d% zuU>xmvlHj0r!HN(eEIy)zyNsEx{>IDB4G6`vdbxOK#=p%T@fyLGn42jOvsh-LQ1u+ zfGd7n_Wm`BQ~{*Eq2D|p_Y02Y=(_Qj9f3$!_0I+f$_|s`=*&=mkdmee)=5(zvaP+2 z<&DGr=453w@Ub5}^L1}{&U2pghBrKOegDbtz3EM_c*QIB?*|a|kf%NEv0r}Q`@a0X z*FX6!pLp>{U;NQu|MA4ww&T6mZ(Q4Y;Or&{`O%Ai6mF7Ay`8pEkt`+EtMYwO1nle@ z%BYCi5kVOf?|M&o%nr!ue@}{G5p}MtWfc#ibOuF4M>>rB@aK!~-`};4@v#GwljqJ! z2g%9-p zqd)r5uP>fBuuT>hVVby9xW4`EZ~q!dF4j;aN`6$s$a#h$*#XfFr3T;?Wc89xN-Ge7 zM=K;;8yr#F>f#`5?TA>1duJ&Zl0#iAWMw_dEVpF@>Mi==T1F_YUak#9GFZwQqbeV4 z;6V;NtQO^OW(w)OcEO77-8_)N5M9=bLJ*R*GM!c^Dl#8@~7X9q$^F zWrap34i0VK@ys13hlX~%^ZYwMf9~_&JKv*hI3GBPhzkTPZ+pwzKJn2XDH9jj{R%>$ zhANj!NU(8O1oS+D?0(P_AcdlZOVe!f(OlmgJMfgSkSC5oM>t3jlJL--B|Frn;Z9tX z)J1!b>}|z^%eW;qk~7spqO#RDGBB*G3C7G=^@L&`(BUt{Y&C>>;Zd`zLkZWLg^#?j zA==tPND&pU*_Geap#lzFkRfVVgdkuLh_stPcMEZ{x;RKwoFuC%9+{9oxX2xc$v9cnn~?^W3HL=Pyr9edF@fH(v6+bK7mK+`(IN`x2Yz()r5< z2pKsgV8R_hdGg!d_T(qSi-sw0d+|p<`cVLZkYCHD&|Gv#HUWn?%ts0KGd(C@c7>C|NmqH?-e~5^sAOaU3O(KHJNJ;l!|rS* z;UUsRcLze6T05{(N~b!4K~T)%f+pEH3XX>>0pp{fqV~hFStqK^O@Ur zJ1YKE%pQMYv5@*oe*htMA;iOjE*27jb<;sei4S6QKDUG6;VIn2FO-Uy=)?tVn3M3p z&%1zZ{8=umg`ID0#B$Z7+VY!vZPlEk6PfA>`Y?zIc(q{@33YIx62p<39?A%@( zsp2^9U*cPQ5*J^DLLS1RB5YGpaZxed3Z`gP62(@^LUHke;;lw>Eq2jnv|_fnuGuuJ zqG-^{N>#iBo%o=eRzzkyk;R9#Pkw&C^P4&87%%wE%$YNnOnNz=e1E@lCX>aprz$5W zTBTZ{kRxP@!rl%Lh!hU1A;6$u6BUte?vaCdJXAL%gj`}P&O=NN8DV64dU|I1fcXl} z;RG<~%=9H!9654e=JErYw6Jw=V5WtoASjK;tTyyXELqyEhTgvT`?oJ1b`WZA)zJ7? zfyW*|?AAf!#t0QkOWaw?TzvWEA`3@|F+v4Gvf0Ve@%O*E=eHL>G#mNy#Wx;3y}RbW zHQZ{JHc08w@NVIc<4C`jjekU+?DYim4VGHXvcXo$>`sO*Q&8N3HRL;YK||q?rD@m) z+?+|A*(Az1h)-NR9}W=zEIq5Cr^=3l*vN<#sLMRuXG<4u!iwzqU^BP>;4SBNdaV-a zhP3!7+9HC20|@XiSjBBQhh#;L9tOl=-~qYB145PAg90On0!9)@(03|{wpgcNmjq;I zj?By)`3_8!6di(mLHdYYhZxslX@_xUlszl+k;FnMAw)%(rF2L^^Zz$OJRk;#@saom z!aU@GbXf!sBLpNU6E>_cLgv2VO5QKE6YJea-#FB5toeLF3!DTmK^ZHtO)TxGVeNs! zGhs_=-V~G&ziL!4iENL|{#dL0Re}Z&N`R2r-)OM0Z0gzzyW zM@!1dx!bP4?OX!Lq_QzN>6Qk_n21#eh*WjmXYo{0E13b3=>sfS9&xNVPDoOc2<(V- ziiy)4m_Bp%_wU|(^OaxUK6~cC!v{1Zv!NCL0d^e#aRVf5(}@S7W>nZGN3=SenX3u> z10af!M+8xTi3gIy$c}PYF`!%m}M7|LigKj z^4SN~(Mqi3FP*+DEOCbcCPV{2^z3V=5A8NEk2ThP@2NaYKH8F+&Rz)?%-NpW@ubc& z?VcLmpNMJ(gwcRxwwThp-tLX3Bs*s#v)|U>K|RBVgTeTa-3=;U{z!_yj4U=#0*Lbr zT*E$GOT~O*h#;ph*!+&xRL)))+Rz_6!`J5VS&eva%v+{pHXirXrg(p|^~l04q&Y z==C}>`Q$U7tL_y&^UU$%3s1hfyu7}`GiI(>9y|MyfTAh>{r6)fv~1FGZRY?&#==;T z>g&xr(Wx_cUUTQ>#_x;rm%a2#;oxPBhlCf$Lpm}-g@+M>GNU4wG0Ci86hY#B7Kw?Z zjEJy~#3kNyA(S*vBZ1lEI8an5w2pk5o#L_AE7GRzQwy#7P$}aiY6Y%_{L)xr%Ftj> z%K+p78M<&+)OsBP@3-5XIA3Y?Rs{tJFhY<4vJD{777C-DqT>^B6BV`t$P?$!*P8uK zVH;G?MR4InWwb1B_jEA*qX0#wL>hcKN)VvnBUGj*7oNWN-p@b(^iyW^)sH{@^yxFV zUVZhgPoh_kjh#7j-^|MTu~&~hvOfRteJ5-@hFI*%`_FD}-nX*!(jzZD^2pS!H-2{e z$&;%m(dq=(2|PkNR@wJp;m%Xl&967{r4O#W^k>pWSohIBI5HAH$#TW8;5b> z^?te|)ln?xlu2d za}rAJpP1;;u){$F2j|aycJ8(xZaXM}lXXnFs4A@sIMHAbkywbAm_sNUDy4}#Y;2dx zbO&z)+j@Wj;vxArK)~Z40nq`J1Qb_dEHDBovaV_U^fV4LGq(J|0y=*D_|wnaE0BDC zd>Ncp)|Z!;j~!b+cJ)m+U3=|Kci(v9?YG~4^{rRm{@IQDU)q1`lT-62_D%7}nE(pY zs*2cC#h(tb-|AD9{GF#Z&wlVT;gE()*1D;CK1M6);?=?&1oJq`>}-S$qm=rJ5u%Ty zUH~$Z5LUcrB(;B%9s6(Mti)yitn3XxwQvr?f z0byGq@qAE4Vjlw(nuTPt^ljOu)tLFVU@v4QLal3o0lVxG1X1Wgk%#X|t~PXZyLl4D>Z|nReLH zHeU(j4&hMTX$R4u(=8sp`Ow^j3*&L`ybvOQC?5t$*zVUckn`-c*<{%|>t0Ws)3789 z2sn%e#79`nqrga|U@%EeqI)Q_ykrr(T!Jw4@WWi%;?ni+POKbL?SE3jE&x#)w8c+M zwYY?)*XlWG+J$`N*@BQ>Mm&$Vob#)#RHaI7U#?KyAjkQcZ@=^M0}jKNm#Ak59Y=8@ z+W{>}67jdF?5=tF;A>A?9UWwuGFSkEuiEC# z7#%(((a9`?jsI0*PZ=h&5T>tWcz6fVWTU7f?f&M`9M^3Joo*-U^+GOzZ3n!eYbvca zBIlhYRN~LL(e4Ccn9qfk-fFKF*Xw!GVOpg&V0o*zw>T_BE){Io>xD34+yX!bal3o? z+rvk@3~X@mEhbXXcSLoNLA&1$CCus{Iy$SjRYHePkH_2R4jy!_K+`9-IB5eN;u{BV zyY04v2M<2+ghpbhR|#|KTUFHQ&;Z))CX*=RAj_aJv}Mc#;t4Tjox|kgEAJrAP*(gC z^vo=?onaD)Q8KCY_@xzmg~S9@_7htj6DM&QhNd3aVDb*(SefEsKx|QK@z_GGEioP2 zV-MhA2LnhP_IE0k_63_~B_M{BqtZUoA?{z$&{&^j-zGl~`G?pAJG%TKx~x#g61cQ) z(A$ZE!4`nDyZtz>ciIrrUz8L=YTZ~8V%3g+CyxvW@r*EThdVovAo)TR^BlhLaJLAJ z4>d!5(*u%DpYzlP9bV24bE6$hLGC02Q+;{8K0h111bwbJ)Et~*$R4D!+UwD7XhBnz zS}m6kf^r;}s{o}NZO0_(eTPKYi?$CUwC=Kn<%_h0c!q8=hvQQ!%DQvM%Lmk>1cNOiv zn{H?XWE@&W2#|)%Msb=H7;E!}jjm?x!LB*jJ zt$4q=TqhHC4~w}HauwZ}Ij=bg3rC3Q;rkw*p*->Ci4zY#3@h{d_RXV8rBYJ09&wqu zVWPN@tWR0iPGX+{VM?vV7!lcd43knxPc8pR5}XMN#Qs`{_K9-2T!FuBu+ir;YINE( zuY72nrc8@JdRVMMzW~`uTD-d&ajmh)TW`RBw^|P5e-Q-vFsxN7VL&iakIP&nF)Qu1 z(QkLC6Vva_#A*GkHD)j6AV)^G-=$q^ls4Y&&w}D%#7ij&91DEziMp=O?#nx6OjOiQ zl5Mou3EJa2P*ZGJy3KnshH_~?sOY-|GdS?$)5w_|AJ{?+A3AW?j@!*n5a1qh==QQ- z8nnbupwOwu0D|y_LHF@H-WunL-M&Ea*5TboTWINz^R7@FNQPiuL7$a`LbV>F*|x&m#X| zE_czg;eiB=>@eYanvIMeC>#Zd?qAUv?LFG-24s%m_SiEsGFn_!moo?MJ8}R|G0hKY zN$q9*B^5j%O37CsM%P*|L6PogF*^)eLI>!SWZgJnQo%t%zv3Ym?49)!B&s{AdYO(H^df3KGxP|F^d56ppiN|MF7wlgrUH}Q+71OeXQg-9(+(C!8LNJ03?Cq0a1EkmaAC0 z&MK7$79MbK_gZ8f#4AHk0*j%Q;;|?BkljAnVIz-3nvl5Xt@DGhmr2qTJGD|`ixirb zrTL{RmQ?GYVj|&3Lp4VOXg~>?nmEbFkkxh&ds0)8RK+cw8yQB-5uzEe$C~>D|1PI= z0E#F&qQ?>cZ_?>f@6a|wr=+<=Ru2PFSYpedzgA?Ztv~2FkSvQ;T524?(Y1m^{ed(` zCK}FmR6hT6heUyDD-n}s14uXC&+~!1baC`T{ z93z~K`j#6?u(Uyco@n+$pBGC9q9d|^ft+^00Z4}+06yw8}e9xbStX* zCP~DdgiORu6oE?XE`UM;2eaaVw9I)CBV42{pAVyR&xVDC2hxnhOu-mIACsJ<$-1Ip z8Z~f|23B5U>ojn}HSZt^Jw}LH)mQ>e=3*9huS1uedt`Dm&rh~Aw$4MOi3-O$OmGzw zbrICa{8Hi+6<5^wqs*5oL~W%OZlg|Ph$s%)Qd4RwqG*eUszSrH_)HfX*|A{h0`*d} zc=+3!d25Vm21tuYL~CG*iNcG;xx>X6Y6KBBU_65aqXarxLP<{E`J0!Dopb_u4T(K^ zQ7la(i1ZkOdC-uFloR!ncoKC*SBv!8bx*cY3Wcd$`Fvn+?JOeP}*hH{g@9uUDT_zza6)$AbT&q&U zJ2d2>n@L2xq-<{_nS}CyNl>6z#9@|I6yO2DhY+GUMqULKNR|cl50hZw@c^tyz%-|-GRJCH@T_QxEGVW@PpwX znk}QCDanH{64)=N3y1{qEt2#&-cf1hjJDwgjkWrv5}yk9}IB1LQt>5U_!=>Q$c0=a-_7#AiphgQK0_%(StS~h$pPWwWf;*z zWff;4gpUGg^@W(`Z<1QjCbgyi%6(dWPSWqXv=ZjnwYSjofpjI*AW4KvtUdMGTC}aP zRoHmrjW?cqj=RInY8xLEAA9WeZ{Iq7`mM*_{o>0HA6;uObiw$wr0B6zbC=sY-u%3L z!{;4$eB0RF?O&7M2>qt=oD*SBDQ7{N0cD7j_pnMo9VD^MT zBt5t#4X>oobHkPH1_s2L2635y2?+$u)TBr=3Q7S&J%kbkD#hc}&^nzSf&uyVU*!3C z&{7VMhm&y;rhq(seBn68@#^s>j}u`zCXm^LtQa5D(=<)lp!=7zy}|7GIFYRFNi!() zYLbNoP;|lE$NC~{ec<>r&pf>_Y3-Z@!i}m=Uw*?RypDMXVXt|Qp44XDVIkkh3(6){ zqbY_C(b$s2VnApy*h`dB-N!YOC+^(*`c!TniOVb*C)&r%L<)~|B*hZ-F)uXPZj-CL>^NZ5REfg|s6jbZ7kS`F%tb46894lo8uKzQ&=uoKkDP$Yrwpk^PD&{JRw%x#n5U`e$W*6J;^ zGA`n>lM9(-Z*V74Hn0xetYMP_0_9EGOJx-$#QTS!@h4&D(%N1XMe%-_bR>jK0)Y&I zC={X&3^=mINdpOjHTWlVQc{dFsnUTGnhyFHDK>~vM>>#Fsnmgj_{QfRXJ0YXzKLeIPyxvG9QivJ}DNcC~30_=J5 z;&ci@vPZO%3Zr5jrLKHvKnC_5GJs`LYuDW+?MD3F$);S27@=|453$V4l6IJY$cu5R*>HfG1HFL0rKPhq~U=0TWDiWapGzl#*8I(uM=bBJUYOxc7 zm=%f3U_c@_QWcUGdAQmyehgMSFp5CKRM-iW6k)vx#&CFzA7HwIAV_Z#$;qD6z(U!niV-j$wzPo#Mslj;>!T@};pqn=hB{+8T}|Oa}AF2G1=NcZ&d%*3^R)>#xR$r=_hg#VDF}j|cq?eaJ&Ty#d$>lFHhfoOmD!c9V8N6ZiMmm4g=-5_@2PD*P~rpEIh~22fe%Uy~S6f|UV3)}jhAL-oNGt4+xM<5U^E2K{zm zK+;2u*xwoGO3@>)sQ8n75f=I!k`@!c>OkD!XBxR)j=Cqk)l5nxO$;izWg}&2$ccg? z$&j82(givOAP7~hyeoVsqqo`Gi%npB0*diZ`5CQ7chHzkQ|oV+kDt;jN49f(#zgJz~v zMKdD^0!n^(V`q7=9E}!g?XbN`Z9+3QnoRk0kRXVX9~-5%5~x&-T|&)Bn|4*QlLP|{ zurM;W1hii{(-Qy0kI)ti^orw|9kO>X#M=Cbp+JF@jE}L+|M1>BP*Q`JuHL@wv2b}$Wrb>N4qj)#6j}4*qWeG74uB%0q;hltlCl^q zCNKnkgxrHkd4l}4^7O>i#bg!jTKpJE8WRR-a)h9QS4Z~Es^g04CE9_=^ibgrYy|S$ zs_Pkq!%uPg_tuktSWXTK4e{CtPE>VoX*>(yKye4|KQF;^{j4<3AqU&` z@p;L@3t-+@(3mM0ctMiwM)*&e z{ub=p^LrT?=i`3$U|ox{P!PF%tNgIyAu4tdS$t^=h_4BeBKcv69x8859;gyOB|}tF zPm-(>WMg=d3&WRdHvabNy#Wop3c z*OHJVIVkFsKt&fifY1$_IyxAH*o0)r>B#@ZB>BM)tHhcTRFq&puLQXt%*9ZZ?IZ$_ zvJ|H-e3~Ai*nfZP*$<^4$q!c9lNaGgGR0vD;D8wtPFC_l3<(2HGY15D zJY2&Kwz-aYUWNTTR=wbzuh0cjg2JRLm@F!E1_Dt?F{K@pWANOI$rA0xZ3vEKwARS1aDE3-wLEvHNsw<;{U5t-aHI_?0G)H~=f zpEDGn6p?`Kr`ejHAI+iWHhq?XajPFOOwLAU97B*04vsYPeakpKW9kAc^njs)k^<)4 z7|b2Lhvjoh{tdwFr3#yWZu}g4@mbOkifB~4W24KS$gHG`JWJ##@ku-2PoGu54Q(e~i!ei*OL8ND+b5`s zNtsfm%s>j-+_-Ru`=7r5#h>3NKadejA3y|vpscXwgFpUvAf<`{*uMt<+0@EFZ^By|H@A|;0`ioYRSk5e#87)^A)U6qNUT^Xeken>4_6df zT*|ScpYT5<~I3eJ&_+RUa|l-9=qN_AhNl)zciUR)Pb1}#dmc4 z0f-sEZkVbT3F(bkAog>bXBW8pI-E2v*%_HT+yV(P+;k+^ux{E{Lx}9EZf#cx0f8YW z6*8i>gGAi!&N_h)C&EyQA_6ZO#S-0wO&8>0{1lz;mQ+^J+`E3B+2XEWe;q3|{I*US z%gTDUs^8q32fRE7oyp*?_J%1Edn{k;-d#fH4%r(T5(-k#e{8X1M-t>q2!cL@)?`AW zP#v}5qW(**IPzEOnr}EFPg~NXHpw&l6>ge& zMC1FxE3f7x!90lg#K+6YN9V7<&by45$d9p{j8K01?Z@DRgaI(DS3R6<0(0hAc=XgK zuRekg;G2LZ!g};6)Huj6dLQFgB6p`^5}TapCi0F+UR%0gG* z!D5MKzzj}GEb2^ZaQ@oiWcJ!io%FXBEMQ<%QHghI2P|3pevmyOgUz12?6jm1lezm7 zGUKYqgmW{$C4zviE!Fl=h7Wd?jQG#9Enp)UhrJ?=x1XP1&o7P_3SqZBGVG0)KmUq` z{N!Dwtn@2LbhaD4K2X7RtrMrrT1iE{YXkGLX;zVXm|5la6~NSO>n!L{!%T3YtQ_;8 z8e%E$W`pHGxQv;mo(I*qM0YiH#{Ne|l7c^rGCOD7b19C7%UUob{8t0pF2o~BkUcgK zqM9mc=*uWDaj^DJ4tJC_U6O~`eas9aY5Hh>#@Z1JV&RNh_gGrdojRWQ_CYg^d%|%h zT(Yj1IitY?+VLC6$zEvk4<@to^N*Ty-4Ih6oigaZ&A@bx0J#e%3Q)Ys@_Wv}4k%Uq z{qFMan^&1hOBWyp#2|O!YZ+QEg;apMuP-k#eHawIe$c^Sr-BvJgDVXCO>hb&IGmm7 zVV*N}{!J&rb{Tw~zc=8NnVC>>Bvr-EnxL_FyIbxGsF0m*)t5_Y2v&AEBDUmauVCjEglYI+?1 zrep+uCtfF2ZB!G5Kim-|P^F#q2al;Q2W5^*>{@Vdh4j(qrYZPg2j5KrL589vDserZ zaoqs6X|{|RP-bT`czF2s;oIf6vd9TT%pU)c@h@$4w|Ar*wayh2TRcj?3P6)L6R_uQ zzjGk&z!Q0GpK7XIVtqzFp(HV!eTf_xp#fG6^8r+yBkM^njxK8ac8b+&_1SBWGa~eb z*JicOBKitp2OvqY{2FhOn`UCzf>FTzh0F@_+Bvfn@-4UT^K9vsSg=kH&}|#W=Z>q{ghYU{C?cREmhk!+(mKNg>8o3;*KjQF#>p zxFPE$H!Xr4@t1{jqsEZYbXrnO^IMc|^wQwX>HF>B3aT zVV9>7W;G=k97}LV2ojN)r~)@Q5}s5Q)VqVJ4m-|c!VeZCNCW}v1i2W7xKl4k&<8Pu zgvCP+V;G7JT5w~XP4ikaot{XeFlxiDmWoKIHhEt}tHo%Q>kt#=KjQ*@j)L3Eu58BW z6H3LXueE#LjB8mBt=3YoyhUej4WmkS71}}4|=xt%SH9^=o!pH0OTGp177Cp+M z0V^$O5`YC@SV(5FGW|N~ng5qPdrFwFA@1Z81TCnd2!+TZIctde{9&01RB?SofJmSQ zA~s|X`4la$%lo5haOU+#N{d~*{^HB8^N8AT@vX3Qcn&a)@O!^DmiHpi^d8BH|FM2a zPFX(4@E-K9U*3$M5zGKKTdIOu@<`HB0!+W7(Oxx0=pNgv?hd43KR|!{KD6juG5OAb z2Le2Ih)H#|)Wtc>WMH$E94ugo1`H7)vKj@l`Qo`8Bnc`OJ5T1F708rI}-<>G?2Q}r{* zScgquwe8C|IrBxKEN{@VI#vn0ugH>lT>A@ssM3_LUWF>45B=U{dh4&41OBV8e);8B z@XO~fk9!J81QRA?NsO_8IFZ&6vQI<%L0GW;F5r#ytiJsGy+u~QzIq#7) zys`zbe)2!k{Wu1-OZ42+YW@bg{B4T1Kqj+fn)f%E&E+1z&5{G`y&#A~IbJsrxpn(7 z>B|K?O68n-P{3ZxslW?;fJAW!u~<&z_PPinW${yB7Yj)RUlXx{2ks122uT!e%+Eg} zqP)hbLN;y`7>yLC4=8Z4rW42cqe0cMGe(fCDPKRZ@6i16fG;9ILnI|vbd{W*UL`>s zk>FhM{jqd^>~j!G>>wjz!;&>PC}(cF^GJC0fp^;V-Lq3dcIwNe7zi~E6w6CP00%lG znUuKRsn2$2hp^)_rE(wOSB!9Xcc@ZhcD%|SLful@GF5mYjEr1m6Jkl>vd&hcoIM)2 z7sNdqjV{(|$yVz|MW1J?#Zbs7e3dep-;D^hJkfSz9buWLwz%7S-ePpFbi54XZydWv zSPD_n(~GfC(VD)2K){su#FEcXjv_Wzq0Ct1i&E;=IGZt1DjExSG`K6sRvvCq7p1I_^Bi`70vPTj0cMVgSoO{aIfTBp5YHUp$z>phP%3e5h79}hPwaw% z_VF0mN{XYQ1hE@3=o0bx{2Yk^F*2-qflbGFmc>0`!+lp%=Ff&x#ejCVWen5VnF9DJ z_g>AV$Vj--r%a|fUWvCj1GLKO6ruZ&Zjl37H*LGS0VCjGb=m$t#v*>kfj3`$=ap9o z&tDbkH```bNnWWU-@0=gg9b{4A1Lumv~0g^x%f=OzI3#-uVp8a%^s5 zK^aBl@jVwME+cG*!ZEK2iDC8qM|`iVGb=1;Mf?u%#WYwEmKgla6e!iq1SvV9K~Zk< zL3EW25s4`BfKva&Mp0T>anHkb97dA%ODWxkZ_%I3n6SqR|+KhM%SXr z8G#lKlJre1bEbJD4D9`LGn*H^q(iWnn`-D;YM}Z9uV&_SRN@tkp2?Hz{zU! zsQqVp9K<~*eBZ1lwSMF;0ig>-V4^)jcBBW<;lg%JU6Jj9BnUjfAu3CPma8_yBx}3O zZT9$w^??P%VUP!yDATmFR1j})OhbBuU{rx(pMkmHR792d)lPU&pR(vSGbQ|jAP!@I z9%lQOt#f&aE6?J%|BP7;NHzt60g;XoGNUlTAfXf+G+-h4nC__vjo7rcF@**f(}-0I zqqvx07a>|%O)sQT5)7mRy~qQS>6u+;x}llP?B?_Ro^z|5$7!v5>(;GXW#@eFd413M z)c}ahF!6ZNwdXL_V%J;^;a;z=yA|2rsG;!IOg5R) zMXFZf(^$M`X%(O?NcR>(rd9Ow}ZKsIC<^el~zGCk-6ZH7HHX!xZ^bc;?S?T=q6=i_G^;h z_h%K60|YmC?Tg2|=oFTwvo8FN7W`QcYBNHT!TaO4o(IiOQ88pyK{$(Nk>hoMj_kWA z$ooz`_D%57$P6y2fMYT6JjZaR8Y43&NvWMJMl6DzNlv?VFG=9RAaay%S=Z_=?SV`j$*T9{fiwprFxsCargOEx@;jf7DoZ;_@^Xcj|bm= zGkt~c20rWt6+(FW-6|?@e!h9-rr{8Ez>eEi+?K;1k|9_{cLiV^rMuy{SQka67dphZ zif|-lao1 zs8J$BlQ?gA-AtBNrhpZylle@Q$=vBTU~cN*V5)>n9ZXe-Q?NPvF4wfRAaM@g~8%2a6j6}*To++LQU+P$itH}8JdEg_>2dP~}`Fkcx zXCIn=6r|?_tp6URi$wmayC`~Y3;Urb8DwTiWU?(qaXSEcd}#C%z!n7BU4jD7^v9=zxJ zT+(QqW|bHSQC&3;EiJA?b)jE%30V|>1ez=qthFShVOf-(oyfoVHTj2lU+cj|q+o%o zwC;l8&Xnww?Fsr4j(9d_wK%J?deA7j7T-?lupExwh3V08=I9SCY=aK~f+(jOG{f0b zIy+ty3X`|8>Ob6{ed-Q$uct$GP-CpmzWw~GuRi~TWoWSvTtN{5wGY-aQW=D0a!^H{ z&Ju$d2m*^j5zY;X7D0vei)`&-i8jJS06P6^m=~vp!#HI3$Oyn0$T@Lq108B25_w(h zpa5dgrq|%m4tr`y=M42*^`r)09h43@cZn?y>cjJ8CFi&* z+%A{NZ$5kVPc6y+8S}B9hf&q|XOk9@G(|`m(5-#lJV?d)^;j^(5gJ8c_cI2By-b>WT?VUd?3D++d3~@ z*|tT5oKo8aLqy>S=s-Y>fDsppfaAb4*~N(SdU7XVEK8x=^Vc zvqaT!rB*MON%&6fwcwdJZb*tcIV4o}Wb}|Rx}QG&`04#$RcfBx9yxnze&Vdx4+J0@ zfu-PKVE^d@Y=}zfWO*qMs(gqT4>BB+llmHpI?~6o3+9t>yD}`nLwo82;B-!*`HrKf zBI3{)$i+jxVvrbRon)i91t`*%El7^LvBj?G{W?hc%EixcxeOe)#puCPNVj_$k|ex$ z+A|~$LfrWfc~tJ9s@Do9cRNu>mg0({IO1sC6e996mdHg~apMl?%55SqAGm^}?Mnp1 zC}$UyM|}wLUG**%yz}?(-W|QzQuM6TT?}WNx|a$qNM;2jJSp>a9r2>BTRh8}qWD(^ zOWw3^ZJu+^X4T~7Y|B@Islm%rk^2?3Dzbi!QsTR4gp9j4qJW%^mCPBdYbC~jnXbdd zVJO}Lnq0r1DSW0#;r1t2F!K{(hB9TS=b3}W9xz;m8@(QOcMTMW9ejgC2z8GjP@yw+ zwOW;!y0X(p)O~?Ag@7T_Ofoa#GVS4uudKTO-vV%i!b8*BgQA*8?^OipuK|e%<_}Fr z2^_@+L+G$7=VG*Lb%#MAMr%Hag~gP3mgf15SiHpZz1!2HGao;`y>sXGN4AH)(l+(P zY^rkp%WWi22<-s}%qp%14t7blnzIF179bL5;Fk67K**|5A?Xgc(3Vh2vOxVX zXUnAkKbP4D9HMKPSiaRzPZf#b?lvS-A>R5HjIc?nt2Usio8dPWI&FRFUTG__W8#;- zab86scxkE$AG^`%@Kw|woD2w2Rg~6p?wYnd92)>al=Gt_-Tkox?|Lln-o1Z8l{?{- z?iD)Zdu{lEyfFgyBygmGv%%p9aTtE|;luZQ9#3_gIE6Yl7R)?-A77O~W#)Q~tGckK zS1@W%wR?Y4>3h{D$<2rP=!$4sOpj&>g*P!Ki1s!rn|g-|goM@PV}sNXK5vmE56N`8 zZ4Q}Rvkr|8k8<4&YXUyR<|!hLkz{SiM8H8raR8R7?JK==Pg#wB&_h8$2*PsnoU*>s z))Lwy_VO_~wHmX$v}hl0zU|F%#LXC##Y{I@l|c|mELe+#>lhJ_VB7~CUKH9k7y1Ha%sNe3QVrY<1=4;G4q(^Vta%Q z53e>vA|m-N&=DR^Rtu&aY2=Bn%cYp;;U*{qZHXWk)x)3Jw2z|%s6=;0k>Ge`>!8C$ zF>OQ2Obq}1hj)?>WPOn>m$UVvF=B3Sb<_ToyU%aA#2%f1PEooF*O6OA6I8H&ku*@3 zTGm&IO6&u${lze=ns_@!uk>=al%dD`5id3s|o4> z2zk0HNKhxUN-V_oHfMpZM?gde_fUQGK9z{~`{ZN*o#%YjOJFEsVB~>45NKRmr#={D z#&o=9M`n8`r@Vy-Tr)8k-lNs;QEEW3X^_~b@kIexHSWbUNvV^eabQ{zQgmTOH-!kZm%fVL zF4fa?6!WvvaYyA+VO~9x)v_^*2G%TCB7Vpk&1DH5ezIY@wTf`EsvN1(FHyLtdX zLc}d!6vu;!#ml^Ny>G4MyY=GYD`12RHs_*6^rw$tG$9)N*;)8T)NNF&;AqoU`&D1LIm552_`5f>IP5d#Y_bYTzXs$zZ1yp&{; z!gsR&mvc>uPF=Sf3L`tv7p40(xm~@Y{ECI;#8uII+ks23xX($7?;(eY;JWyxeROmL z8wpW3YnzaT!eSP0Dwgn(p1qr#UVIPr;~j1f0Li`4EUwX~ ziLv|5#wuHJvt8EZkSlVcOb52E!g?p8D+D`+PcY!gDGyw9RdD2g0b*6lTBlvJ47|x4 z;a1f}7$e?L{@H_5BYmfgYkosSWKBFnzMnX?_PAi(21m=d;1ayW+1eCQryR8)F$9T& zhP1&Rp?#L}QiO$svk1<-|E{~mko6R0Z1ps_=z9_lb^7D_7+nv!acN^?etv#wxZYkz z35YV#GM&E+^V`l9CPb!1rNG+s4hkZI6WZc{_IPf6cW>n4Lh&n*lK-TrM(q@9Yje~Y ztf*r2s_RyZSRr<*6t&-r23lC=*LA923Yj3B$yn7ir870n-SyIVx0-qz*W zKWcC15)R`5NL9r(;p6?0bsXnD?mQTD+sg_GfwL2%fmfK6k2d%MB_Q(A^T<(PK&DT*EoY2 zXtXpmiT23sE{z=>-547iyKyuzd{6^M?I9MPA1~MX)EjZJ-Yjoz%}?BW`}W$~dpE`= zCT1q)%cFH166mqyfp!k+yI>}|)HNt&MAZl!F)D~(OdnGUi8V1}0Z;aVp~&suV;)dn z9_H|t^km=pVcQPQc{n?uCU?`ZJmbVOs$E`mzo821?D`q6%El0(DDK{I9Mb21UX{%sfjR z*X1je2ss|{cD6AgTi85Io`ZQ4rjl>(h{LH~gZQq&cA2gyz+^(1&$>mW(Uaud7t*k9 z!xk5@m%gXuRhFjQY>e2gc091*b?GP zeIL~kwwuGMM9%Yu`4<}-8xBu4{i?z`{c@I8S7AO<+a)C%P`=x2`f zmB%s!{R_?s_4JFmz8Y8B-C6TAgH{849F+XV-TH7@ow{vU7N&A&k?rw~`7$3ImC0pg zgQ68r&3vtkCxihSWYSswjG*He0ueT^J(E82T_n5uMix&2f_n;Q2PQ{#C#NY%OEPmu zf31P3;&^!`s#(EE*cEuZo-yiTyoWmspup)zi4w_?oTV6r!bN3z*F3$4(>NTns!h1^ z0v3A3)j*+Qq>z?IRIXWkeQXH1gdTPG#SKxc>MKrWW=D~m5G?#7MyHO6;i07(Cz4b= zyGBZmHf=7w|L`6`IYmKHT`>S+GXXAxh$6hwg@`j2?Il-93xnO#6;CKGIZ1&eO>mPO z7H9#8Atppsr}zpkzjw0|OXj0+?V{$%F20oS8ZI7Xn!?_rT{ZZI?P}eg=Ygx3!}Vr+ z$8?W9>S>t78n_l*#GQz=vum(8EPeRVu% zin~1vH$S!3d@(-|$B2IfyXjkaAfJEs?UTP=pjRrOh{)Gy)^^k%L1}k3ERSo3kiJ z?ypowbpGgTIQX& zUze?fDeY)~3T9B-6y$;Rvzb!_`u>k-d63X@P{8v{z=N`T~>kUP)l` z1vY@DilBj-IM;8#D$@WS^Mfl`Vd2G+qGsuvh_zN_ECi`2RsW>7T!IftaruP2Q|5|H z3;+?>N|72X9dlKQY*M89vNo8B1-Mn1lYUQ7Feevj)~U=j+w@DT2UgT{MLLaxCSKJt z?@AICGN%U@LY(RqK={IIkiY_=j0f`-9>EvSPq9m$ZkXikqx$o^BgOah4g zt61%zn1TiU(#RQrkGn#9%KZP@^ldcAf?PPi!r(#_bcSoW<-S%W=wfby9fF}`)saX8< za0iyYC`Xu{ZjJ}Yo1bj2SyOFeCB%x6Fma@u`PB61lQ_r`2Q7gTsWF~cku764D%#zl zJ5>=#aJ$_bp%L#o82x^(8gPZe#R7Ei9WqhEkY$l=;ewn*SwnFYAaT;b+%K8Lg5rU$ z@R!l%tLv$}Zug~K!IwiinbZV24wl>fOx{$#@pl-NE6RhHf}~3w;xvx`->#PLH3vN{>5FsZvI5~=7P8QI3H#s3Wyw_HVy33r|EZ~>Z z?B-!MFdya<+k(6z3x~N73Es^7p;7+O@frS-^3e3Im!ZsA_%TKgGCPUk-K>gmVLO9> zWSL?NC*z7y+5sR^Q+dKj^8nYl$s-=r)Gd!>i;SXUzr9RhB|Xu49bwB0vRDKzp^ah$ zhQ1V;*O-rTRd?{58iIcUCer)Tvj|xN5MZrj61v~r@0X!z=&tOyzTjm0TyX`S2n;eD zM=*j_iA&&tP}e==^T$!#D>DWOKopZ7xcHL`CsJOs1eLjC(M&?|UgizZc*kUolJl-~ zLF%tllRCcWLG-v}TcqCL5p$Brr`LI>)RntGnx`JAcDJCh+n^(wyL=q#uVX`ddR{I~ zp{1>FZ~=@3FXV&y$>G;LEUUk69WttgOEv4Gr&%6u*dODqyfREai}5#n zl4I3oe4vS41R?Vi^K|Ks&fLCp|Mul{H*nK zn6DVie`p@oS6}_^>#zUvTh`Bkj)j4Fz|U`f`}Nnq``z!Ttv^Wn@^IkfaOU=>pWc6d zII88HPw4*x9}BPXo-PfyRxO_}1r2xHotfN=qS5_=(I@y|i_0RGAsE8)2Q=fVit%&DJTvsAx8v#&@%L1a9_v4^lh7^R5 zzVNW^Do-rIMx4^eV;f8bmh+8kZp_%y=re$hPETD{!M6gy^Qe zG{14<-XE?Ze|UM1e~)tcprY(lX=;_&0~$&(c-M9#cZz|%;+rZG4;{fd+0`B{ zjoV<0(3P9!IjJ74`BnC{jSC0d4lne?6580E-_&V1N26kzbT-W*yn-gtJ6JJAB_wa> zH-q}5J}V?B>8e16D|f5(sII(IKv~ub$=P>P!B57!Qt1LvderWVT4M> z*bU-<>({Pdzuwbx{kkx6?`T6#7*}Rs0<}M;?6~pojEI8WIrTy95z5&A-P>!{L6q1N zwnHR$56i=4(JC+xd#&T;bvM+-e5+U4NB9b11`d=h#)5E7oq-VG_Tthzq?MI`z=FA` zv{hhR(G7TEbbMywK6m{)_s2(fcD%?Hi@;gl01(YKL0CWT&EJ20{&-p}m03sFPv|D} z#2+z$A4nA5t9Gb15ZpEcLiI#qd7^qRXw_?HOAtUt{B%H^7sLl$O_WlPesS&E+xz3P zL)b>hB@(yo3Qw~dpek=H)^R|#F)H|dN<4VkZuxJp173q9N-jXgcBLl8$YkxC z7ev}s?pM@!-M6XEC^6*Jka>n0c*q#6?wdq}FWC1Uj^))m#VHe>6=on<4TtX6U!Jd) zD&|CDqxiF>oN-?ukwo+C&1_Kwv{R3OUEs>gKvXRbl@O)mZmO{7DjUF1Ew9udVwjhc zD5*Q6k(M#w5ijuG`l$~2Zr6q-v?vn@b>OaArP;N&Qjh{HxwIFo`uGvu~ z`x3pgQC0*WjzhLp9y$Et_7~3|57ksL-?6WCLY<$ckYgL1K+<5YqK<4&0}t?{db#Zj z2;oCPwg6&$@X$l?ui|QL2}%Xtu_li$b;#hI)swVgUs+aU33AC+wZBt?lYS!+cs~-s z@Um35B+d+P;#7Yix?W13B~4DcZk;N3ygv;<4!cIt;wBt6uTe5rKHnmChxo>Jap)cQ z9wsD)w3Jpi>p}|vD}9-F`^pZ0Cwa**5aB~o1!BTDCd`Cg`%I?a`Z9`vGD!fO+!ZX+ zR#ZOT_oR>(aA0MWAcX_jaC4<9@G*>7-JQ>zhW$)w*fX^NwQ|+GiX)N+bK4@baO``#OjStgr`))AmE3YJ%*!*Rag)JjuAsCrC1x9voI(Eh?XSg3e$)X0$Ajt zX%WaCug_PCVOUPCyQwU0eqzy?4IkpeL2mUnle{Qt_6&u`9uiH|2@SPNcr>VQ>POV< zzu(k|PAg-E^SVlm8R2*W#gluqwR;qrs*pE}Ex3Spu}^tov1>;!rSlu4 zqwn2&|B_-1d*|OHZbX#Dm@>yiQ%I7og1Sfc_L=%_Jq?;>B$cXy^Pu3PjZFwotkQN( z96?7hGupC|xgov=IZuWRw6Jce~ z9AMswM}kwot9rxG*utiRhT{!ueuio#reoYA0mHIBbb_LSAwiknIQvm>d$tWN3TP+$tnF>`A}^ zBzB+7sf_h+bqWT;-RfO{#2U92w#no|R9M*+L~8O5bDd#s2R*AvEF~^%l<<~?hNc0a z+a)o{fmy!MoPMjjqMubV#~fz3;n>^-vQ*`p(BdhH}Y5AF=hO6|* z2i-Q-!72urg1om zP%8mq9}5%2qx~WK^$2}|xG=x@{FBcwJ| z(Y6<^o~$7!+F5|-(O0X%7G}Vq^)6PB<~6ixds3k>3+@hT9uW3`RO~($;|DjQfAu@$ zw=tOXVT9y3c28_W?fvjyt`Qy918we=&vOV$KYjjqRHWWqK|5tYF}&B~4Gvc7zm?x{ zeagOr8VgvG*>NsQqDRW1UTBBxE?Hjx$#jkMMD{V0SP z3yPEABCq3eJGJTBNr=z3L&2+1p_b}YvZQ^$K_@WLLyQ_Zrz z&c+L(!8+;ZE&~yTCV?-i#PleExVTm8ZZS`oX%n=g|JQU{YYkDP6;@t^D7YPYvpE-Mu7o4Z(K{eqv6 zUuLyXC~Y80UM;?6qBZjyo)T*?y+qE`Cte4^p-0-2q*^^uE-Q%Zn4g)^LuTRg$0uol$1)E&da zuV#x5aQyML9^~4c`PvHPDbw6h`{22gmpTO?uOI&K=Lb)nu=@OWfBDOAkw5dw!=Jt0 z=c$BJp+fX;fy+YxOB1@fNQNF0_!C@ngOAvGaxeqzUWW#Xtex?-?01Bi>%SZ*BzN^{ z#w#IoSJ?#v4gW`YrH~%E0Nj!#p4FC<$#PkOs?>r&GfuLUB8ydsyS^0~uc{g@zf5OJ z%d=|~!QGCM#MKx)=$V;8HRq@wI}JfMqmS1~EnmX4*(+_v(mLwJ>4zNk4$00chX*{! z?SGL;DRTu8HeIP#+ZwFtk0eO%;3hvq4aWG8c}L*4A*PpFZ+)LAroLxM1m8{(_A59k zjRR2`W9f9IUE8i#qjV~}2!t{MobOr$h45gtD>j$d#hPpFkf7CsLjrfb9Ot=^vtIeK zwMyH7#zRlrCcX0*1UC!iC&4H_>~RZzg3e(~cE#Y!;FT z$z*PdBk{`?MCeFyIEM7;zYvaI0!TE}ZAN0IGrdG6B0O-b7MV%KA(kkMOM|*27+3u~ z7oXYmQ+9$?NH9+%o$D#i z6pIxGpk3oY%xj6dPFK+NgSP7!Um7G@Y#~Nscef$7c39M~7+HV^BF-LET>H=^tmYoW z=CWjKxgv%L<#ExQ&X>yNt*(f0Vs|Vj9)lO&Y`|E^^e$5g-_`y5=6XL7%DVn2C%je< zhBwA0prJn=CJW_eI89vc3-S7U$L_36IgWkDrMZ2yLli(1SeZU@Ez=9nKaX(+4*k*FYc0-0E6IT+T60#j%Ev7CM z%eY%&j~95Gp9miUNUm_|DZDA2iCEsT^w@2%C=kz{XeTreV(PgO;$B?gC8$NKDVOoaqyA_FF=M4IFNtusyfN=w)0ET0rY34KTQ%{EDfFppxk@(=d(;#rZh(XN zrH!$pw{MTsz4r3M%X=;!-W!_`r3md7ug;#bWyi8Y5=v>>%d(gdIaB5|d=b=Q&?_y~ z02&TG3C%S{@&T*mrWm>~iuXcMI9Do6-gflYkDuQD_}Hv9vYG|24!h;AN%@(~uBU?b2^NoMyk@O0 z3MDD~i3EO)-N@N0I4AaqGv*AOB=ROc*3x|B8mAp2!dp4&*9!}R#w)z&H$8Pq3s7{J zgmsQZ$Mh5dNir3!4enY`m~_VT6pQ_V4`PGlo|_d8zwh#pY7>}Tb1c$a;$sPc!6iBy&;mb(K=pKe@- zP4fTjtL#C>@;KNP^E;lsx=Ud35E|C&lG>9QNvOMYvNwyMc z-hWIJl{kYd^}bMx2iPF8ARG1Mhp$Y%I@^Ce`8R6o+X|y0Cps*LeU!Q1+M}?I!d65d?gz>AR^eWS zfB;`!0O7|dF=!CXit}3n$kMsW=b?pmY4n@q zMr1bIH=4&be0~H6cI?Kz57+Opxp8CTVBMAo;(D%$*2zDo&1$u%jYs*~q6uwkz?h4% z>dktSfMf*#sRTj=yjMLu9twrHY>~s5bA?JVSt&I3$vsjjUKBkl*pJcelRoor!Gh4C zOc#TtXr(}XR9}!cUz={yUx>AF6r-u>G(!YK{p0h)h5bbUK|sF0xK$X3>|%0_!n?*D z@Yt7g^%H?QV@!Vg$FKkV!vl4cldqLL#EAk69yjVNmaUBqi*1WQBpC7c2S8>u(T-fP zM#fPG2Mu%1rdiT*@e|_g6euvFEuLtVo>%a9)8h(!*fgvYc^Dw#x2J^RG(wh_WyK=> z+q0ELF#P0(4G6GM*@OMOt?~+DaO8OXBq&YUDbsnrw_8QOcScFka;Av&!U5*r3+1xkDjKS{<-BLJUI^KyQ!S*fUYA4b!&Bm9K|k}=DSy&Q}Eg}Fb9ni z85$Wu|7n>=ySzMZ65OGwy9>mvl+~QpjZ!DExYXdF+Ud=f-z(gaplhcH9DNb){K_wy}+JP3_^8%GxuMQOe2Kjnbe6 z=V?k%GdV8 zn(tgLzeYw%5ur!JEm;jqPk?}5#~@Ks_dOTYPk^|CD8 zr+$3<@AtIY%ypr(IXu2<&V09pB>JS{5|7*fqRymDWw{O>8WtTFZW1qgbG_q|1%xPt z964bEkXQNgS9xQEJq$t*No1t^l^gE`bIc(J**cY0eDU>4KUZwL0OYG^ex=c%@;*N{ z-o_A!a9pr!+cgT_HM)>+mipac1S+#@#ia|`4BnPL^6vPJF?b`*omfdJ7`ke2q8q5DNL#*3sPkDaynJziLW&%2 zI=PN=#X#Ie*s~{yHs9hAeexs94^Q`}UPi)k@3(YkDlnRNVOwLY0FPdEN1;Cuo8Ys9 zmaF^}K{y@f(Ipb5BL^xnxKyRU6Kuw|B8g6AgODR60iq4@Jd*Hr+SYAc$c`9j6!-YU zJ!b_0AmBs%M$WMfQFL!7)hS-t=;GIr%t%# zJg#BHZV3-4xKF&1Ss*kCMZ^04tOlJ2b>c1ez@|gzb)=*6ZvKiP#n7EwZoDlfpSlS= zpG16~A&s?9S5}Hh%f-4Zu?lfroZm-YW+7ZJ%#~7E4daR!WV*b$YqSlSXqu84+liiMJw4A zOVKpN`7sD9MI!0l0GLbr@hD1ILwCA*c&IlE7zHZ)Aq zYHa2)FUVhslqjt7$sLOEgD^=Hf_u3@m?cGm_!U7n;wSdCfPhI>qMWM+$UE#*1?13R zRoM^!@$0X@{vAhbAG$1Pp>RP2u1;J8gQ6+DzwzTG56h#eJFA!RD(NfN zDSlJ57xQ>Lf;x*CPG$vvchaORbKL=lLw$<@f+V+u!;-imCHGsJwi65`;XkX%ohjSNhVPj$6@RpyV(bE$~pB~jydmn|Pu zOkn9iB(9i`lsR5}_;9`FojMr5KZj*Yd0Ptv&`W0CZ1l`-iCfYxLF`lao>M^;gbF*x zrjFUk&jum150=DaC!rLL;g)4B=-;TTWS1gp)Vv_DU2C7ECM|phwi#vZ%=ZNL^ zY5|1A%G@Walax7{)0}euHgX4x^cH#seuh*g+zu#B$mnD*VKwGtq<2M*3=w%QvF6;A z<#>SpR!tt&>S{2pdLus1`n7{=fG{XtJ4E+4CkyH-O!3lEcv~tD5_*Jd_Asa<1N+zv zgqn#7=uR9D;#Z|X(Svu94l**t2zQDyxMOH@5-}ObZh!)W01HX7QS4e}9MX-`?TIBi z(wExvPh!O8V^V8@_CTz#$%qUuJ5_}thKw1k;&`Mn5n7sIN+c@!C70Ihq+5Y5`A;@L zfvb$ZAvZz#druGSqg<#X78Btep6F~llOvnOcUpxb>-mbBE>cC$9kyUZ{8bT|t7MX= ztX8DdcRAW9_)l8c5CI^KMnpG_t5N3fuGQXU%?F3x`-~ZqfWxWH7h>vCl0?2A+klBd zd{y4!W>2iZy&h)zR@vPi6)XchDD+Gm^=IpqDKUj1@DS8AD*36N9lGph@vM7mPmVvM z*}hyCw%bY>M{_LVW_7Y*0!X7NLdg8Y{ZH@w{`c2g znFe`1k}-yw2PJ(`{fqKNXtf<6vcOnS1V%EMZ}krf_Y{f_K?u<3%h24|Q{99;@DTt> zgk&O1_6Lal5937mFf*%%)>_kK0fU!diwX!LkE*|LJz%%0D&iPep-OiNwtz@Q(ywmU zY=`B+kI_i0GM$(l*U4@1)Khl>NO1i;(wekc6pqSR<#9c zRUz+=#Mr{Ks;hyRNSllS^b@q%$tpgd6*}1!(x_%+w_$N#zw2ZJQY7+#Wk_AwtuYd= zO3xiJlSiO~CWs0TgO+htl9hdM(BE3e%49Ms|tv!9J+6# zSLNknlDc$rCp@0A@R^cjO1FyT@#7-L72GO}U0&OPFW#qSZ7su3mtT2E9fRP2GTgu~ zzx@34^k-scR!ZBC)+GrE+MgBXD3*D`JfGf$(dofcwF&}~*V~hC{_*!WhvOgV3b`G4 zzV`OcbCz-2KaGxCVQ%^{6hz{L{lJGg!DJ9Z%k|SIAIkb#bEn`ae>wN2vD^3l@cXgh zAz93c+t7{uzd|D%oD`0U)J_{GK(yn^2j9H_$hMe?lATL}<(Q1mtMxDi&;*MdH>=Dk zDV$&^A^--R@f;@|Q)5wj>dq*++tiK;&ph=o(x$=qSj}5J%1Y)JcA!^(vPmfgkBck0 zL~wcSHc51J-_;0o&j>~$K^r686k(_jHykqfq_qTM=rtl+9;S;6>$AkDYH#!Y^f#(>c$N z0wLf}0}LfNP}6_=No|3ufmdgbHb_^oqkzBLVzG6%tGY)3GoglpDtn zPqttGGU&bniTOEQm-6E0@vAAvr62nArFx_4OYQCGBdM505)E3a<3R3 zpMdo;ta^=;!?qU+;h7w)Sj6#o*bV>^Wq9YUw$+HcAl_~w10ffP7#_ADR}7CBID&q- zer)mD{y_}%@DEH#eF@Glq!b6}v8QSK0QRKd(FGOFqB)gn=$5U6f|)aehW-zq>-ZX)uTn3Yv-?m)eg^s|Dp zP~~=It-js2j>@-n&xtUb9Sv{?6iNl-lQsJRKnn_%D2>!S4mR%n;W|x^F}0hn@e4?>|B~yfMeISPp{9vHL?s;HVP6AFv|i`a z#leNq=eHjpp6s7_jObHG6m*S%CVfW8zMAc7^D`i#;`+}8A8PEZ`HGH?GP}?nn zfTTH8R&-_tblU~f5v{XRa|57T(ZPe@pfx_=jM2eDBg>F35mrM8Su0rX+u>nJC^{u5 zI_Q{|1##U>k>nO4a1_0!MX$Ok;?cf=_*JHX1a*SI0y~gUe@t{A`qK!6)~!tGkc0vaaS;V7!E^9gPbF=|SHKn;~%oNUU8jFl#Fc*i$K?Lmj~?%%`uT6XPKc`RuA69`LieL9bnkUTxH*UV$dO_^!yG$KxH{WZ zga>*+oYqFf+LXq7LN%AMsMFlU#u~OuP3ofPI50O#oThUA7bz@=@nP{+V4DUYQ4RhaSl$jDVzlkL#NwPI)tI;7q#UK8lna4D+VINqqh?YErkq_%g;p2v@KO2wTnHfpXNz z0iW`ZS3zE#NDdn|nHF~yn;u{%0?1-{XFqTFEiTezmXnsIj(3yMt|e<_q0!Gm5a^S5 z16+*vEJ!rDe7eC8-ddcR^Y}^m`vL?ZU2Aa9Qg`8>f0F}D1i*MG@kh2z;dNag$d<{` zBPh#92Fw~0rBUZ_(cXbtAyU;hg-C-fhorZ=IVO) zKzS4wqhq+gO>uf($LE(>Nythe7I7RvNR6WLZ!FT{qtL@ zwnGtPSAZLuM29i-q;!s{yS;XP-v3@@e=Si^i%1Us*%MMc6yhVRGKk!ctpyQqS&>~t zQJWcm90avDHr5QHBH}$i>g7uC5P9<)SO!?fF*7@Y@()N^6bRz7xEn9ha|e1>6B@E0 z1$5^P^*?UiSrkFEaNDs%hU!6(Tu`OuGV*_b6G$OHXs#i7n8(r`TTNHxQZ&wnF0wh` z@k>E(Q*4XNx%7Qq%H*hCB#(J3G!{!+7#n9^37-S5x}R1$*>F17ee8PkKq7Uwv%&0v zR*OD}Mgd1yiY8ogVc6AvlFEJ*In=Go^*N~If=1+LV~{Hn-5W}tE2gl`xugjK-YO-v zrg1X9++hWgPl}T;YHq1G?CWJVq)C@|lwqnD+Z?>@9#I7EZDWt#PC_x(=uyB}%BvA8P}H3H^v48|vGMn13B^c$3?&+s_$E$=;A9it_M+g^9Kts` zleagw-*RT(zK{!s%M3i`mhkBmKVqAK9r$mhn4;V>GD*s2nFUge-}EWvg6_B-GXgar zhWTL6$9BUmzvJPMNaTlmC{|KkUV?@~ZuSv7DXsQ=#XlARrZ7iVH0BJmnX9p&?Zcw! zRCU82qd!y!h+q`0+UIwbmc2KSY58YI!$oRV*fIbux9Gqe&W6$stii47Lc zhx;d$r|uU|q`ra~={6%UCZQ?94ctE1c5))i0^ znA2tToE=H`DxfwI;$hEe+?J}KW>`PJ2&73TSsuPZQ|X>}qW4hZ)L0SRbA`?iEb-F_ zfVMpq1-5gk4Zj-v<$GRi{}Mv^Q`S=uB0j>m`654@5h(u0yhsXa7z0xApe-ay;6uo>zy)q*bEtMD*(DnD#BpmRPyO;HL3i z2)DIx^=dyx<9$VI3d<-to?mV2UpT=#Ln^374;d8>DQ<-kaogp z%0_W*kx~Pfcj%)-O92tvfrABYHa%f$QH{-uesSfp6n99B-9OA;>mTz z>}X2lpp|GqnQ%`<3mh-*`8f?K??Rgar;5qJ>514LfQe(Dtb+MV6W{(4F4|?p-ieNIOop@DG2Qa}D8s`1f5&!<+3<=Su?Ds)YW6V$9*`MK>g+I>Nu z=vHBnz--@m2ibruAsn zLh(Uel8IgaKZ;1?adC$rpWH<6!U=lTRQPyx;$N0nr22kg!2}6+YmneTViEg!h3B;m zLbgqL%Z>NhDq4X}sz)vi4-Z!=eFR^8Z4A#i$}a>m)aQN&8w7!g%7Ya@M(k^Aa-wQ< z)EdU~^PB^NSlp@q)G!f;5H=WQelLnLV1{%zFMggw&xwr%8WH61FM$ojVR!Xe83I`@%ts>f z2)8NKG<3C+(=K>uv}tWqKLQg9AF!3^6O%%_jqNb6)twj$#$M5(3aVvKqUS}t2uDjj z@|qwIC3e`7^NL*Hu>`g>QcL7L4cm6AdNPNFK@b#ZR2{0qDSi4#A2h3Gtdb?`yFrGE zE--+c=mfi1WlUs{+#J7-=q^Bymq?`GsDv=3yWH2?JJsJo#h^aZsRjJjz2d*-0>?NP zK$3!5tf_k6JN{XN#}aWdf1&7sv#k~?)e04-?{q6bQDFZIeRE{HVx>oPu{*14Kn;;2 z_Sdo;1V%0NUyaD6I|88YGPXn%8Ge6q0!1u0x9@O7XtadD2~%PuCk2(lO2O_xGwTSI zL&F&ZYWXj&IDKX1(M*L!m3&b7V{SnY@k6p<1}xP7bteW^n{*~Q_l)O&av3Yq*C#oH zbZKN2T&@!3#DfXEb8YJhqamEUMd42?cVdAPYB%NMakCHs&}2SrNUYPy!pY z@fO%9ae0x6N&{l+)_+{SjeR0Iq^xU!4f0fA-98g;L^h%1LC%xREiar9 zTi{5nHuN1YqufRHgg*8>YZ5`T31U<@MJ~=VbbwcL#uUW%)A(EFhR5!41!oD(%L@??J7H#T*|^xmbIl>|-YQ`{$QOzpHYm|hA^Dr!gB!tnFof?hu;nP^vJTX>*QM z29)k5qGYYr1hKT*3l&WD4Ms-n9)ZglMUY5v;z2D2{4tx^bRM<#2lTEZ+vAYkL$hHw z()5HCQ}q&9a#W3Gt^o2&sLh0L_G*piq_qmGn^M29$`Ww~$t3k+tr6$vK#H9-h4eqt zB7*r*4X3L_(pO$QxTJiuOgKwJ=&~5ys%u7yyO!F+ieisCTLo7+_o^qa<5`vZ7y+KA z6N572=4eOn6f^G%H4(~gOB!_ysCEK#D9O0`v+=(rD6ZJ zv5W;w6Cc*I^DVX>QX`}czG2ya=!mn$q_=axWV6l)$lpxi1W8fw66acwqeR@J(*1^) zuoFh{*%%se=R8y~gEmsS*X@@4&(;=*#V;s_om%QaeSl+mhQ9jQ*;^+7wnsO)cP-Kk z7b}(q0Wn0Zj&wHsa3>@l;(SlXk9%Ax*ViMQyTY;>iAeR#Yo&?ZP#&A2W~4f{d+_W= z`A*%VCWzOri6bK$tNJ8vlQBhQasJ0~D>1pyhZ$f{O1iv`;w45Mvbkuj z1Bos}m%T%`h__)%XCpYWk@zmuW+R6GL>MM#SwB>a2Ort#wrPout|zg!`{Nv0$Fzyrev0^3Nn zYeP8zvP%Ja>H%a5D`txm8Q$EG2D`Nxw&2{XGX_Kho`F|vb;m(fAae}c5v1v;)5~aJ zJ!8{4lZKSQ{ly58b};`m=>yG(pz|2eOAmpgrP7P5`s;iN$jcFRqD#+z#^xz}R`7z_;D;5g^3+bK->}h+I5Sj@xjR_>dKps_E~%2 z$A%icl{Xrl2YvGhK4s`Z=N-gd1iI;2Xd%v`Edtcwl=RpFVSMtSHTV&Tw`891X6&fx zP`(X6y3Izz41Ey?k|B&fPM_|lA%G%ClOAXi>GJwZetQzuK&Mfjm5McPXG*6m+Q!V0 z)NscU8j*x2hr-?E&*x&+-cFR=C_fOc_e;1`Q2U4<8oOfB@^-!cLFp^nUl4yp6r0-^ zZ|a?w>-j*cC|-1`KUH7Xhc3$7ZMkW-+A@?cu~}@RCC6;CGIZ;n_n#Tx)&WNm1xgIa z`RpKZr~9%op@PgzuGfE%De%PO+<_gwjfp5xLOlifbIN64+-@@l&u@W+0aGP1Uv;f5 zE3dYXGyI-NIXbJi?aMP#txj>YWa7!#((U54Cg+tcMhPKF#QK>vc4W@LI+`axJ&rIX zq_D&dUYb?!W>*6B4c%L}s`$BS^Oj-^51H#|Yn&FY@CriqZGpciB0iO`ggAn^qC50%)0RW?QD)fWu-DtTaK!p+&Z4I@(uKkOR&18Sw-wqL~ZMfuHe9Qa3yxTv*#Q{X`L|D0Lp8{~Y7WOm)#!_f@;~(>+^Kofzfr%@THdL*XRIIX0iHlK z$|c($N}e=#l3vr?6vv)v-OJJS>m^3`QCF|N#fw~5Grfg__MnA|?bb+vyW}Te0);UW z0PAw`;sOOf0JBmB{uIf_iwQrQpyP$)A3ul>vP^1IqmP%r{rt8CAgA0=^> z$5_!1x3C6riCrGzG{3LZt;Pu9#^aDov|58Q z#;x2LDPOD+;R&AJzP>XoOYWuh2IsMQcUk3il-E~_LpVu8J2HF*Z|O(_y~DNY7B)m1 z4{`!AeJpdNfv^tX!$%v_>i+Wte>Mf=nyx&$f7tc?-@ktS@Zlf+@cQ?MNACbzh6?IQ zWZN`pSWpDwL+8XPC1s zOtxK3_%?>GcdA31L`Vu0r#qXA@}kjMk|5(<69)bno%u5E55)N?~4rTm)fZ;F!NYY=E{0L;em-7oZ)C%mjn?8f<}}~ zf>oOSE3GH{YvrS$^XShC7XFP_J)q&z>5rLU_ZlH;=X~}W7_c?bl#&WfD^xT{-Eume zT=xGMbP->4^4%>H#eAp1+)1N!f*up6_;R+<_&f~37D&WF`C!idCm2Sv3bF<9xWuzU z%E@s_0S&CoMJ3YxB^%K;IdS=0GHs*?azlc`lRloE`GK{$iEqEs_A5X?8Mbr11pCi) z>fbteIV;_9%>TP6F{pVWJrU5B^7LL4se}2A00-(EEC1C|a#=5m=G_V`~q2f~)+obhXJJCe`x!K#_Yq&!)p{AE!H54l>8dAj&zl&?h1v_H642U3R zM&JIu#42VnoU#U%e+)PV(%>#h39ZQOASA+CFmGHck}uebO5|5A}nSN?uR|;;>7)7h5Mq7p>0viaLs4 zG_`?k1y%F0qRQ&>T7*TGQL%q3U0LZ_QnyT*eLK6PfcM$>e4zbo}73F z#F(sh7YmQSd)zwIE$q8Q;sj|cV;s!8KN+%8>6m>NR+*~;Na-DRSPNcJyKTth zCJ9&qh;>klz-w#6XIrRsk4eD@QdMgU{RBG0YHb{@(2v5(PVweMAO85BV5GEBskaC{Ji~Uz+xa>+LnKFb7avpDs*v60F(_#DVqR?W%Kx zRMP4S2MO*yWRJNyx_9{fuRr_#l~;3`6LO8x1j&4m91}+~sU{tx5Z3=w^H<*DHkWbP z4+_Cif16hp2v`G0FryM$j-C~`%YG?fV8)dg>N2hg{+Wvv3AYA-DDZ(7g8S{` zPSnIT>S&qV0H4NpA)?TV4i2}4fU+6b?K6=kmJ`H__g2MRA|Zv`GHy&jC@%Q`D*?fL zsCBBpNWHuXdu5#Q^QYP(?6)K@`a=G&Ht2lIYNTuqQmX1eD!g!wU`kV464K_UXxeaM z!XYT7-holl1C6OGrfu$Q&XLHvf*iI0| zTN`WKWx@UFkfhc&saQCO+Xc2T6!?7S=hgi&4GePc3&Z{pBq5$x=CuefAx~@$sr2`H zP4Lx!B?-Yve+~bv1gxPtS`O%esNi>V=4&jmjHXeO%nqw}C=(SCsT^@AH&i?* zJSPt_w|X$Wu|f!<vvD*R>hCpJYC$( zSiPi#XODWflMthF&dm^Rgp^;6v%66Aoghy!upjZR)@iI@Q-Jd@Fxfeu3DW6Qi7x+jNOw5hf zpDK}RZMU10`*RUD!8O#*T4WrYX#UoVuovOddAjzk@g^H~V#t}n0C#?E@Q*aQ2S6x0 z73Zg}{_@8!wpnBM>Dz9-#Ae!fl<=}%5DgiTpI2uY-PmU@SW?6N!EU4XQ3?-q(c_EJ${uk&>hH20E~O6w2v?=;5RHc-iCZ#e z+{9K?hWjghcu4}u6 zmS5Lhvf2N86TX^dlQhw5hIBrktMh>Du@G{#glI=!NALNRk}-X1ejrjWj30Sb(UA(R zPjulrZJE9MP8HYeftx9oqq5?2ui7nVKnFc7L7&15f6~)FV6=++Y`HXD8a#)m>I>kb zd!PUSEV}p7Y`861 z39j0k9|mQ@{9twrS6ws#B0Z86YK##Q^sm=Ak*YPS#zJT``eV}~;XJmh_IExqYSi}J z1#fJuDckYVBf3Refp)4LXJi9@{Fc{F4sT;J^0L`>zO8y?`g_XnEy9ilwtI-77Vm7o zebe*u&b-HT0FEuh;KJOi$DiEGe@koi_f$2r*gQfZQ3_U;5K^olMI9-lFr)?K-WR%0 z-f!&;(I1R(Ni;ZiwvTI#l;P~JUSU=6w8pl|T>3e{CuDj(2*|-rU3ZSG#EWo{$mLo+ zn-{=La9rP`q6ZW#r0ghM9dp1!yfU2O$+`Lj__Fh@*L+iEMo&94~28NuB!gTsk!|>>F`|_8V$HTMP~{p#UP!=5b32yAqU{Fty%<@Y zeoj5{IS%07I6W!TC)NHPjldIg+;Td%Ggd^&-nihttEqSi%)3o-NXPkvMbR=A4KYLu zzPU*%yKu++nW&Jb5VZ<;jR2x>k+9{aty$dyK>)I-{@+*KB9P|X6H?1NY>*?mu|hSa z=vX^YAN+Z9)8kgOi~jT{5p)0N?K8S-@ndgfgJ`_8I=$Y#xmccOYeGk6KkRyT|23Jg?|tucfBoLm*N5+xx7>=x^FuL!nVo>X5T^F#)*ah9Iq0=iQBo|KeJpUzR-)1#|N_FeK8b_(3js4BFno- zbbT#Vc*!COxqJl9{Ag~DAI4T{r-+3-=HSi1q9Q2@2js5NMTuqmBDYN}-`OH$Ax0Nm3L=AuS;WM7AgfL%i`nCd;8u z3Mo=l%mtibhe8xii-snYw9@zGhnEv($cJde{ft9~EJ$n9jW2)UI$9HiOwD8bPW)AL zs}Q0yj>a=`{EyR#X$lXe`SL2ykNc!|m+t=b=$O9>eb?h72`zOUcKx>Nrs(mzzwz?B zuReTz*s{wnPYH>!o&G*Le_$~$_)7p^5|410Bm)7|6y}4M1;r@U6Hqll*5YcJAtlp4KOBW^wBY zr~c;>u!0!TTN3nuA3>1L2_yO#%@0#^Jp>m0b{g8{z$8Z@ zkncAG?d|PoIAI#?AYWp0)gna7v<%x|lGI;MwUbU;pjVs}VH!4komn zVosya$I5zHOI1|1#>~5=E3@}+Jmjvv{Pe3|{mGYa+@HO&B+u*nKm5)=Ud;`ll_?Gw zP@^K@OicqQB{5Y|9%bRg+lR({Hy;I{IGKr*am)^AAyekqzELw zP$=RT^&(yRJmwsh(7}sYxTfkj2cJ;>64?br8Ph4FEkt2T6nHf34nZ(x1nMx?b??%s zEG)smx^_QX;aCga2S_ZF%_FW;kImYIQeHa#O8NFp5kA)7%||#J-CUWAoC1qP0T-jX zdQRn8%E#bkKm!?dE# zuG7|CAaS((M4}N-#ymW$=o`PfTGWu%6MJk4?IyHpV?w(S%#=o3m0pZ&oNAnIF~q+b zWD#WQjP%L8%oL2bH(TVZko-u5x8<8@a<~$8bET*+H+9W+0dG{KQLTL}qo8SqZFtg- zv|Ibi(&^01p4Zef40=Rge{OHLfEbS=!kN2hc{9_z^wS$({l+(*KD^m-{M~z0A929` z@Ot*$Nc&(7dr^TK!=Rtj%?sE5w#oB;LXT`4fFY{!)K@tSJ?H;=Lh~IE@94XGNpr)E z?MW?oA{)%W6K5sY>u2wV7fpZMXsAQ%JtZOd^D=Wrz!ir0(mjI5oGYGB#x`B}VFjeob9eINAXV*4?Vi_WqOPljA2eQ%AiK>!B z3#!rD;}l!S&-CD&N(6O)JDczXyS9QZZmR2n3P(O6~LNz|{b`Rmmp)Pq6z zQ3btjjh1$(@jL|8Wj5T9%em!!rJy_>tMR=OY5EEmyy4YOF9DTK-3-YstO(yizG>f4 z?sU=fGIZea0`vqhV~vyBfz7`O6^_$$@jiNP5ujq_ZZj5|2mvKI%>f_9k=cmkw6+A zR%Y70peH=IRfQ7#uT{jJD1kDYGCy_Bv-&>)Jw&$(-@p;@8F(udDUA+8lUQRDwGC%; z?Jm(W8<+QFz_!baAQ$$?+T?7a5!NK3_ZuJk_xJPWe51w)mmCyP)UnGYjzq_ehk0{a zQ3>4Ju1?7J%BWr(NqLj?J3Vl6fDx(#e=V!o*gk?oqxhiYzt8QB_L{aWxWNEaVGev- z;=dcK5QGP3SdF)MqeS9zMkm9~!rTl*g=Nn_{#5<(I!d%pSSQ7PjQx%Jx_XWHB!L2v z>=qc}%FmTp*`l&-ezm7>ahmm3rSqoq`MM7!syp3wzpBd3R%({+DdP6*t7zCwSEGIa zcj3X>>1LHe>}>6c9wrElj2hFD2AoUZ|3Q#OKUK_c0B4%z1SY0I$X%}t`Iq4r5Iy`Y ziHPX^cVRPUM6ngW47o|5Ndsbcp@%y#fA!Pd_92^)x_YNOUm=Y#t?YZDu&%u0j6fev zW{{^bAGntm(J(*85e+}Q@$eg;`rTi@@r=l3sikXHLH*(V*EjBe^X`@L{T);+YUeuR zG`qnE4mioK%5aoM%R5|+YGkqh58CjrI{K>fwl-M7MUMh{svZ-f_Hxj1RRL)3+1>mK zht~%x1ct&syvcJ*v0UQ9QuC700S}C*EedIYJft7EAJrdE$=-I z7P*?4sV;aCtWbjMOY*lopm}NHx#dr8uTxu9%x^nLh%Q%1ia+=(1GXXSYhYzOX`IY217!a-EoHDu%khXBuK*gi5Jj$4_Arz z1EGo(OlcONM6mLGAq9JWBjm|O&T3r@^&~4tkCpN?1kIN^NR}e#Odk8 zs+!+A=U`0q8SAxV#R8soc?CtB+Qw39QQiGd{q=V@+5WrR($&?%|La|c_a8od_=PV# zyLUQGsnz<(#gi&=g}f`5IzSnt(w~iU%3A+d8$!lW)pl7Ojqda)_>j$@&9F@po^pre zjz?{Mk+vs-ZjxJIp6fe#AHyWE+PfDtTgK#?7D*YN!)oj(wKHRgBOfD)6Uf`6;7&9= zd!CC?FeE2vXpxULUy`nvF5I~R8?BNx_ZjL7qEtw(VevLiT5~R(cm#m%9HlKb7>R1< zS*~-=$t!U$G-pv(XNnRsM6*?b<4TctW}={^Fl#@NSX|sh=ZUCWeGWz16t!8T?8Zh* z2JJ~;{&-Km0wWbgjl^1DM>%vJ+{up0)xP2khhz+09&w~FK5l6_l1~c=TTPKP=_4V~ z5l>rxSuI+eyZ37M$TpLLb|>5#e?qZwfv0L=2bsFZMD9Bk28Ip9g9cUqQ{0iEG~f5X z;AX>oYmX|?oJg%vf!DEmP)JIB#E;de)HhwNB41%2OJlkk%5B%<+S*Ip$19J4dbNJ8>GneyCaSZxYk2TnCMrD`mi(k zfFjpuQKsvsE<-}D!VifaG-`pR^#X<4iNe86O7)4A8X)Uz1rD(8Hh^>d? z>W-&Atd0^50(LDG#-d#A-Fc zm5pe)<(&!seFW-u5QyT0j-?UWSBaG26*aspUXXtkcO92|wP=N=2~4joKoGbAEF~v` zG`H(-XvO2Iyx(#aQBpDPkFxV3f{b@g6d;J#*H25}RS~-5HZ>%7-a6Irz7$_}-DT4< zd;Qt7Z?=5ld*6F{Z|QDJSIb?RQdn_%kBso);r&Nl_wF51PkgjH!o983<)^-{)tue3 z{kYoOOF!BqA$-up@Vk6$5}$g?g{-Fkfw!0#Ob91ma0DJ_FvI5{N8-aR%n40`a<*=( zVwh^xuq48L$I4~`FCW9y@yDe%6f!TKDNBvu#BGkVUUcUN;>ZQWLLG2BNbazmFOiFq zucd&S|7ME1c-fyO->3OG`n>Q$cc|9~-i>JYiVR!?X+0$`H=N)M7%N^Pt#&TDOTE1Z zQERP)R)!}!TtyZJ9j4_&kmdA7cFc{isuXikImPaRCIcBwM_VBzLRA`BUM*J7U1kC2 z3}%2bu?j;VULOilxtb0vKIrw+k2zGZm*%a2&YU7%q<3TZ)HG%+BFh+<`Tu>fT4oEc*MeDNlW3lL-w1|ugiwo{|) zg>ic4j}I1epuDL1KGI8^<5hl?=!uzEVc*iPT4hZH_4vD9_eiL(mGW0CufMx{^Xa4G z<<6z9*`?He@x(Gx8Fv{{xO=Fo$v2+9eso>iD&8%ZWf3y|#&>d+gK%D4F)1!U=>)iM z=s!U^mgYag0DLcNLBEkbAp*%3zNE;1!pR-aJ$_65Riem2fYYmP5hF%ZV)1nnCApJw z7LLCSj08RD*JiE(VvgsIpJ#wJ;N(|1R#y^OHpX|4LFtPw%+UeohpB@|vRXlR{XimC z?o4&`2`mLb1RWLyFyx_M4L|OAuHezz&V1`d2|NKyUYHJku-D$)sziO_Rc=v_Er9K^ zGj;AhVPy^8r-R?^BD+d6+umb}pkhc_^aD|TiRUG1F{GPfQogIi|Y=wwJ zELLqShib`1FXEsLM?%J7ODM|6%0?{+!XL>Z?apN!L4xEhPxDS}q>Mr=mn`DAqH4@n zd8U`-b^uvGroS=3sEpQ)?j36zUCX^C2l_`m{EFAPJara2k7w_9?P_-A31)_doTDYO z#?p?ReMk=Y;p<0-htD1zKDznMfb|Xq6-supw`+FPKrOC)?jx!U2 z&jayeUeP;~sTaF;9Ha;SC5?;xuLV@7hto8YXB@biM_E5Adu!RpIKY1cW_Us|+WXxK z2BJG%Ei4izrQ2l0)8IO3nwMQmIPnXQAPsk_$c1vYye{SxwIyMccV4`th=?wiPJg!+ zs!3&8awN_=nF9t-gmdvg*+%a~XSgTzZe-N z?QH$PyGSGpqzQPdn1l#ix>c>V3z{tK3`pud2_ldcCL|#CaMenw{%0$rau3moqE+n1fn^me^i7}2+06)}= zd&Y`oSx((nu5WsJWOtW-UafkVCA9b{au(xuwy2`QJ8fl@Z8_# zs2Iq_C8OIjzyPT`}w z-|U*b!j`|WDYmwOnXAov_oyOF-y#;H%t+; z1b7X)6JBn4mz1}l@Z|$R)FqPd(1ak^erS3;_e2{IL=2)ol;szSrVr*e7CrmIFxP{{ zJ1_K{QI+3ZaV()DP>f|Da-I0zB#0T}x`q)1NhxcK$DINUiTkiUyT_o$AoJx~_eo>< z)yWQBKD&`;clB##S!9mf%<87#b5NxbLeC9twyewKa<8WgDycp~Ji7GLqoZ+oKkbpS zF(f0e7WgNfzJ#WA)fbu?7(O9c-SmWGtbL>3J-(&KiVJs5 zJx!b)8~j}0EmivBgo+(#hPQjz*G_QzW5%VrT8@6uD$B-$-A)AgD@l`r4CE?;ezqP{YB#d4($SRNl=tKXP%8jA=8 zcF6s5ZNB$RQ1cXT-jMTEf0TFdz!;2077i^#63mEIhWzu;eC+)c2v~$|nIafMeL7p} zgD9%p$=@=}izJUHP8119+Iw^H2~Tjih(6s*C>LlFka@~CD)1}71-IJfX~5k^mlgSXYd3IWUDR;5vD7$#@X zbZ_^VUQ6y|f5$G22{H;nT+|{f2&9#;TAv=Fmt~JKk8&AY<%bBR#_ulCmqk7t(_}JslNptJWXt=CGX_07!=T)IQv}jb?uVybfz}bQt^OE{i ztxm|`+W4@KQN39?+NJi9?NBTsSPpUEtbng(P?9lYGXWtfh*RW8j| zj!$Gf!~M#u8QM`&w0o>strYkX41RD93fwQxrN_8ssD33leFZ^mviKtWAqOuIL_XF? zX9zVUK|s8x!IK0Asi+{KK2}gtcYjOe`QH44PTU>NtC0Rd*aVkFv3S7^@u7v~X(&bA ziGBMuc6D<%q1U<;czULb+?6xtlf$)g6HQqBY5SpKuQ`oyICYV=?eHrIqAvQ3`in7< z)jWQT`Pl6o(^T@ld^o)`=7t@NF8sh1^%q;s#WBgzENH<-EL|ZQIea#I+*;UEx=~x_ z@4r60_g#6$Y~()Xx^sBr<|DFU_pW!%&SF)#D#hshJ4>TYuXb0NAaJw4=1!mwuiF%m z1m1WZrWaz8=+$48%q!Af>Xvw7&*H`$Adcm#J14m%W#0q)K>VhjviAFl3-;+0*8`>x27+2)53*v#j z)hP#tdFt#OtZ2l%^`RzPSCKTiC&V*#cUz&?LR3Or-@y4&In&M zlY6c&NHVy|5ZzK?-&z`Z&Mm#m#e>U=FA~JLs_-|Bqv*9u`G04t zoRISy43W}nw~Oimwm|%#(u0m`wb*`qw}q*Y5u_it#t1YNWnhb*G-EvCx7o+#3N~Rv zXt!aoj1(771IczgE<*TY-c84VbQm!nd$YY;{)zN0cE5SOiwgYBXOC{azVYzJjT;Xi z;)F4M^7{4bKRkTYHG4F^i~;F8>FXJ$C)f}l&Qxsg%OwfQXi*I`H5!(KSJ^`H?_m8I z#eF0ki9r;zzz8guak)+`0?e$`dkNhNNS(4SLnV zd!0sFo*UZ!AW46T591=QKBo`Zz0{o67>K|KKozd{P^3hK90SxCgTqts3V^=;eg$uR zmcocx?L_V?S6YxZ^%KpKYEe2VEG!9CVU{3r1p9bo2SgBE!pTnavolc!PTyI!7e2L; z^KZu#c~;QT1#jR;fDa4eh8e>9`VtFWH#U(3PZX?+9u7pD(o$5RS|C21a+@+4y_IsY zAup{!dw*FI@V7iVoaUe=Oe3L(BvnGm$8kVzAGhWSFCYk1;FeNaR8t!hVZAJOT)HL=26 zOntDkxs!#y7=a}s^})=tSusy~tTJx6f32O(+6CE+YD)9o?%jL$Aj`3hb|)LXwLAOR z3n&QeoMid#m07xs?JgrFvu(d4+Kr~Za@KC@_ofu z&pUe!fY=czY5rp%`w|lg2Pg&{QNA~NT6z+(VJN5SF+&Z7NpoV|$QxNKOgsRo$0GY< z0%3XRV9$Cw@}OV_8?aL63-rvC`Vf{PPl!3d=F4y(Q5|zX-i_hPh6K^@J;V(7QSIoc zReH}SfsLr!-RN(M&c+^JJkE0Pw4b@EQ9iL_H9{muBos2dpj8^)^><4KXbeBU+FgLc z-fjj&k0`3lXc)rRH3CSk^n%1mEb4B2*H)xK4sT2U%U=|QBx5Aw`(5$Lu(WB)ngl^O z@_Mi-HPbGeDfm8r2Nl}?sPI5fM<#QRsSA;70vSSbnO=T)|2 zwb>1qJSJ3AX=7N1w9~jwBy(FhyVnmN&F+r>+=02xji7`UM0c=NfbQY${s1>s=BJ%a zV7t&{gap{Bhxw%u{!>Wtt9y)LVI-`rM~-4d*A^@Fxzr^eSG4J(%2HS@mKPQ{%33f_ z(7-|FR}VbkD1$T}DLv%!`gTro@DI4@79c`wSWpFi7|D`(P3D^*VVAR~L|DH-)cXMX zSmqLl6-6>?sT@6Essy4}x8Rv}>-tb8Cr17!Yv=b8X*;X&rrOt03$7jCR_x^p~wOIwoSElyF{^Vp_Mr7C zEFdD-06lGAaBlKIiJr@SGUEaD&a4`O8dq3&ZNMCUfx>s8zq3N(1#O)I8BlqiF$Yu0 zvfs4emJo~GNOBmgeTIPk;xBY1^@B-<2T?Th)c@tDzlj9-!ykVAr$2r9YhR*z4eR739aDewV0L`5!jfR0cgDN*%FO!U zYSMh#)R6l(VUm<3PZl9bG1e6;8gv6nh81!$dPC6t(7J?s1ZkK^2^HVlw(%0}%JNU7 zu*ohByGRoJDzY^M-$!nE;+kuIEDr? zBBa_-Ow)YTCe`%8A$_J z9ZO<{;~?Bn+WZ(RN!^Q71CO5j@YxSv@Hs2f8B#3bsK+?GL|WFi<`5qE)HY@<(>x^a zIP*Y)Dn@J0n$4C`J- zmbf%G=ERWe>c-c2sA6bt1qV)Ur$h)WNbCVF5_+7%;kISdbQ3#vUI>!Rh*4c4QHYO8 z6BZE!#(_QGOrj+`s&Jgyi+SAoScxUFz?WQCh0;w&gbh}eRtADd+TS`{YpbFXZBV*2 zYV`WZs|ux4prDKbm0J)opr%7e__c16OI28`1qSo^@YEA4{gd5o?{3!YxS(i+d@X_{ zs-==pM<*x=;_FnKAV%D(@cm?nACxnnyp}8tLK071#FS^#|=1Iu>IgDq1d3F6Fp zez!N=^`sMQ4D_n+W5+Mb`=*|fSnVGps-)~S zC#3|jkT5pyp*=Cu!GVLY37aB?IpVBZIG~zeHF+283TRKzh%g3^Sq|YK1j;3nYxp~l z^yjGrRDA_7i`rcPA&xu_fRlgL)jofxX>09}lw*%?cv{du2~8V&7O!50jWovsAS|71 zQ{gu-%4z0_N^IPFj`)Bg#X5R&G-}WPC1Vz@q zGkZ%=I!M;+Jlvy{Uv3{!R$%zNb#t3UtwH~#kKxf1pe zyw~Cft?^N_ik|yS78oRRmy}k1@oSjNFW?o%2T!>h9rOU6=RS)rjdS^-W{ABYv-e{! zF11~hon01R{NT%9`%zC374_}MA7R=n0}cfoiB1O7C553s`WiXF80l1&$mRRDH&5BL zRUdIluc2>$>?OJ`M@Lmt%$WI=OK#hkGGx_WITHlH5F<^H2s&~j_z@KyvM~MyK|}{J z!5h0>eAcbpZWWS1j}Tpfs15yB+DP{h1h8|#zz~kW)&8`QeQ*+EVZTQ9Gz#H52fH1NDGAT%5|0QEjL(yQj-X$=P*R)Twved`sE)ioeOEC1MZ(^20jzwrqvr+{VV-7WxX*=#dh<&o6cQ zYwp8vs9bhX$d$^o0p&DxVF?*ZXuQp7im;PVQPfpvh1WD^qG7h1O$?c8 z$X$&(a$WbO!W~i6glTw5P!S5+__4S7w44~{H5J9T{1zBYckBYR8jcPYwyHKyl=8KA zj-vH%fTBJReDl1a`2G>PrT=)b@%Ht09Z99QJJ}OwAF7Kr0RRp31ae8j3WSQy4?}lA zSSE;!StT|h?XTwM$Ze0o96X5b-tV|QfeA5-Y0@hc`qL)L%d~f@!=v#)Ax7E!B-`HL zhLaC>i8mFQ_v#(({O2|R#=Jb_u5pAQF{fQKMV;Ee zEj9DaYsL`k6M0-qH+|iJAb>8u4jAcYSGmT$LaubjM(OWjtrEW#m4oBP-ngi3JA%9` zMV4d<_dV(rD&mS|)fuiE5eQGl@$K08* ztaR|4PF;LDUW^_N@k0SCNqG;*S^x$EqM%k)@~dR}y;5E_Q^~uG)9=;si2= zSt96Y27wSsp$jKvlNwnBZ3(0=Cs?- zajM&*h*H(+rxF~IvGN5c!l{&Jw=Q3TLQU#{Pdiq!{Ua6aTXPrv<-O*?_N$jaIywOo zLSRe^972zy$Rv5yJ{lL?74pQ1&5c$*!mnI33? zInP5C{;BNw)}8>~b(eu58?P9FP#aB0tVplc=s)xEa+W7J7p&okYl3HEQQ?l!Ea2ND7 zD4le{o4)EJd1r!xB3&kDE3o3G-GhOA0!l5C69a~L97F<61wpdkk+dT%kF#Zl+~wkUv)SqPHDq)Pe&D8kK`AQI&6dT>NjtQ% zj(;&K@?;(`a-sK2v~wBVT2=`6wfa$HdRK8%+-X-BN`yF-KVaED~gWq$4{SM ze(L1-h}~o3^7+xJyaIJB7y>2qp#d9W7L3uS#fhYZI7Vhq#zC&)x{0B|gYKTw7{d?D zrmE3D{~0ub$1)zG?+_H5a}LF`ImCd>2+4rn7TUw1M-%?}p$M||yT9Iixbp6U5tVQQ zpq@?Zs0j&E^c~=+G5uMui{+xfw=|MR0v|BR0c~VOTd~h+xrk3!joGX?A-vo`$KJ$c zP67lOthAie%15O>|6OJcC6ui!R@JepZ8~Ie6@+kpg&im|@wLMUm?5B&z=_769U0zlw7OV;1N-5giz=zJ~=~5dqCKZZu6YvdH z11rN)YXA{a;|8BSbfWhLGZ}sY$YG-O8gEVsLfbWT>PA=kItCYRih&@XW|J#a?WQaP zu$iVn&U6FfjE#xdT!o8LbX3?36-yC5F`fAInR5RndwuEvA{^s_S6O4?NN33pvL69? z8zm+|`Ah>s_}JoaG??^`w3e$_RZ3&da=p=S4lp=!jJk^4Yoy219`)$)`&e8ERF%8S zw>B0-=v>C`!(eDmo$IZgEJ@*t6P5+U&6rtQNM2PUBjOZDv}WPflrZE!^8?joaHm$# z7pgEPj~=reQ^7IkU%(b=QE%q)>Ru%aJLx1ygXa?1K)!o_v`AT)^k>~}_93ej7=(;8?U+Xt>?;s&y-h*{$o zxRFf|r9As1w{5Z-0ac~QD^m`Q6P?H0@Th5PqD4ErEW8%OVeVNtov+T^!376g6B;fc zl}QF_&=lpd993T+rs5~T{4sVMx^?H%Go9hPo8 zy)SMPtwO*6JEx;kOC$dZzJ#ne1lG|wxI0QF8j4~)Wg-@5IxGpfaq6hyN|Ku| z^UD5b_3Ca@Ft9MszJ2?v+hu*;2bkp2gtm{C4oE%49YW~q@y6@FVWq!9cOqe;2RIn1 zlx*jsM}YL$ryCm+Gjp(5Q5dlY3E!bgaosL2a_OG=88U?^p&j| z@Pm}(rc`LjAUKkkSU{bw+)FG;HD_SwPwPQ|CZL9+zpXr|`NtDYbka~!XVWWg);?q2oZ~Sv(e_7;)79?zOjDXYo9Jc+g^3)DtqPFu%@j-e%DtY| z@1Oa=XcrMyR-N5$R;aours4$Byh_@PPaCqT&3j5gTh@$GBwJGik*pAV zDSB`vg1{h@XtFzyFuXB5pba2eI?8^05b4KHoMHAsv~k3>q%o_ER(#!zSZA4EgVNRGPvCbidhv$&LrDe#?49eMT z2Fe(;f_*{JUw{H?jAsWFBu`YngEr()%}j;KgSV;ljE7{zpQjd9_&sWaOBqS7RW zG|0!`zX3&nny<=ubI3L{0-!C_;GS5+k5d^R&h`|8u@3{q;1OrO!AEkAgl&fLTO2(_ zV|#m(*ZKanaOY8anOg- z+cKxZ#Zr9I79h-92RrkYA3-2@bzoLNX%+ZNFk33H)Da44hky%^RvKOzcMH7m_=!;v z#9l>CWOP(bFvJN7BH#X|$RSEK3~ZIN>_FDtC$oNKM;~ZMny_j;oZ3>SV;?g)Q6@c6 zPKuIR@>(NOU9kU96S3&}V2eVxUEIzx_!?iH#v1%szMIbwq|29Xov1*~Ac-cs(uyS@t{!!8H zw02-llnZ*Wtw&7}+(-ezfDGrb^SC|56?h)Ob{ADbeWfE#`2nbnQC(^zJ5P79m6taL z5@)+VHY5imv?KD&(_tMFWK&)ZxN_Dok!#xLax5OM2)hilQVO1Kc-Cj-<ROlO1w#P$D~ls8%?};PgYrVLLNNnLkfsT;qZMFLM50_>9HC(> zhS$c1CXT%SS+{6p$$A`#$I~G&9L9$mZcW_IX~SX0dFKAuI@m$@C6wvX++W7^xRe|e z%o6?MikJFdjW7-_hO7nP6hTDRhh&K6Hczr=!^__b^8ja-r$Ol@@@oQ&cH;rS-Rpcp zZ^mgPROqO*rFS7Yx={Mdv=-O(y8BGviTjBFxOqn`g;vOneORHy_MG19o@!3vWLXa& z;k|yHfL+FMGnOr==O?@xug3c^?oz>d_NOx({AYuu|3` zC=O~q9+g4zcSK?21bU6kngJAWp$~oB>>&I%xteimBe*0LO9(@DouK z*8(Zn0i>N%;V{_;C=_s|I+s?Y9X}obG=*D8k>J^m`<57Gx-JTlHhF)sF9?0X+BU0( zRm43jbZ3Tzy}c3jI@+XOxT!L;*-5fbjV0z!~;=1oA23nxbP9`_?mNZ_vqQf+yNQyG0i5B_^{(Y&rZwkwb;b zNlHv@ejTZBA9r*By%)MQJPe*h8ZC~*a5_hB26b;xCiweW($(|}d6=INYhYA1( zZ#_&6B^)z#ZxxOV!Hk&f&gxJ^$&(l$E$VLW;F=zlfZEwgP(!bC|=tXX@Vc62RKzATUx+Y z0)QcUE=gkB%=sb0rk6nM{Xj*^bPqH^*i*YrfViz%dg(glQd7wjh9wB(kaIUiZJ8jM z4&Pzv*+P-hI>sZ8`%p=^6QAJxK@TxV z?S+`+bGac8k?d$?TSu=3YX&(FPgYXeb%w9nk)76EU*W$JL*y~#_Ea;bkej;1E?Jud zG{9V5OCeJlm3x)0rJ8kLLlQ4OD&GD21*~ViLkW*I&?W~6SLuo-!>xtNN^kl@haROH zzl%PS*g?sy7o@wl+ei>KLm0_3CRm18f^eIy!VDRy7)b}RLf}fTbq*5>9G|P%6pN~1 z_Q)!+K_MTQJEp14*>I_7oYTSZJ$bP{4o5)Y{zML3dtSy*$f|hngH!k8RHn##QXSCN zoK3z28`Z&`CGT)&27IMnb<`xD;W|~O?m0^IpsC4};zcmz)_^*tO4Z~GGzWor+zheF z5Gy32-`o*j(ki#R@1nkR13~yOMrvzkW_+lN5vLD`aHiZy$MgAtbCSR|@A&^6|&n z4P8~!WnP^uxkl`Mt5%r(v1O3R@GU;1AT%r63+9HKz-3Jx_va6uU^c*nD1<7ZEE6aq zm$1o#5JCL3Z$2^~P>?<IR-pCw@6MC99{ir ztofd&-a(LS*rA7&-`&UE|9Q<4fBGJ_r~B^6lS$o*ET~hp#L?mU3UAnCqz~4w%#K?P za)YRdOnj8$MiPXa0qk?d}Z)A%)HIE8((2=sh46Ky2T88Gq=Ry8CS-SooIc?aQ~tgu?oyPQzV07a&I+2`#}d4QO0mH=bj_5Mv0w@&SXM62JYa z`%=-B%1fc88iCsds1Z&T8{zyQK2oDB8}^zXg0Ai3jka<=h53>NoS6Y1iKk4qk|N6$ zQWpz*Wh6=IQv_VzY`GEc4c$yRVswlc50@w-Ont#cr6l~>Z0T;UI!ic}9!SI-IzD45 zHhY|g>#R08Ky#|8?}IWMIEAsy71$Lu>T%9Pcn2iV53q z-&K9;p4j0`t`J86dtXhba_(}@swwJ18WcCqewAR1` zfu@^Q)zhC3=p*V~^QfcRXqMbvD*XkG(L5GB8I+cGCa?_xC)&n8GF17Y8V-kd=(;$t zK^ud8WAcU@r$?lQ(j}A}|7W1+Dc?B!%{tQTkb7+rBnTP&kUZuN2-SI{?`}U}2?7}$ z112tMR|xfdx=RK{1Y+F&*_p9wPa$(i*%8Ibdwb_8-+rU1`nVIoSijSVz5|rHKoBcT z>5~;s zZNji$SHB81d>4Y`Fx>2{o4Qt(99o2Es*_*oQJc)=(Yjf8x(KR?o%4od!?Y7?o)gwi zX-5XUqb@~=w3{ya4PJt^K4w%d=&OQfx#gcAJ0P-ccaMOdPnztIB!o1_ysA|%weD=* z7$_7ob1fKo`!P~?n{zl^(;&K72_lGnoXSUX%s3cg^bJ0W$j8f1ra?^vopG7+&HdS6>4ffV(qj)wFhoqbZWZ#1?dw00XEq4}MHJ-Pr@KUfnI8dl;CX_*!oda!G4qSS zNzr4QAG?LI#^6NQGEo?knF((K_7{x-_lgW!D7&_=O>1TmxLqSmH(bQ)Q^v+pEM3UA zmHbecSZ-MfWw)vnHbFnLxVwR@z7|hDQ0HitDYdMt$47j`9+-4|%;gieQjiLrnMWt; zM1TtmfB(l1JYSPyhDTA=VH-AKUL-+6(ehAGBqV?s8Q02E(_6k$Zl?sPvWuWnX{Y5Q zo+_Pa@txr4P^9D8(^7ZS6}~QlAVfrvk`r-hOjN81bw=5(o}&;g3W7*y>uU}$%x_^X z4#Mv-*lCsRxGp3~4xB|0pJmy9M(K&9d@hbr=3AbWAKCkIOEe#GKFfTQBG-p^RmZ2< z+2?BB35E2=`l1{XFbpI?rW{hqkAQCGfEyD*w2Sp0c1HfiE!!oEx0@e1@IQINdW+*~ zp#K`Fgq&T+3KR2qE@sJpv^n!&w|AAj5sRK-TMF0NSivdJ|JNL6xtR?=i_|%A3mr(=d`w`McOw%D(jO5s51eTv*E6#8a!(wL%t$+`7YEeR>au zUj4SGbLyrW24l!$b*H`oxO8Y+g-cljnpHT07q!(9)?mes!0a@p2@|vZgBHP}YFiAvFjKmdigDO=F0eAeswun0Y^X{hd z3SS_D3TF|fn1@-&Ek)GfR%@@JYr(Q$1r2{Nr=Rd5VO1KAQQ zUQ!Vf&Q&IM{Kq1oDceL6q-AS+J@)hpSyO;Am!9)do(Q}a)+D#$i9}~|rS4^Y4jXnz z@-4>BRr3`EO(Zo@c4DR({X}qrOIcPy=c1Y%FcFQD=gh?2ZI`e)T{h6y?_rnjA%S}* zRD~Wo>eDdD-68ctWQ}bZmq{o`_@>|L^xOOCGuctbliJ%czq#gDK`@P0GpWRHJy?#E zQYi*|dIn<4L0l3cMh}P~=Epd{Y%Aap0jhvPGM8ZXsXA@V6d)v3l%^vH5l~4GN5~?8 zg{CYdLF#fQ+8FP%9J#^k1c*`gPgfR%aL}k2vdJaToezQ_Nk1;tQw@5Yj*sQ(dZPBX zSoo8Ayya5Ax`%VL^$H!4Nx$U4hNV)dRR00_f-KylZbV_zj@R#+uASppY zmjbS8Q2mQcH>Va$g1~?zNFZWnZ9B4nuDIRX$3`u6s}Du!T=Z|X7eqkU67lzg!o12W!Veu!>pO8+H6UVj z`pUNgVh^g6VS_y`)A*3+aKKjdN|EO>2{b=*YoN5iy*M8NOjoy93TWt^IKUg&ZQ#tk zICEXyHf@#}aIhyduqNgnB3U%TBg$ot6uU^sg}4G^{`EG?|>hF}&u(_R(kql7a=^ z!<-yPL`>j^ylcVU2_YW1{{we{yTvIo?=AUM8J-NP%T#NGSMtM72zX?SOSq%eNgG?% z*oRjj7!o$fNyQMC%0dv#v!&IPAQeGCH(*7nA2%gFd@}@yFFaf1iR0)VOIZ%vZ&Y7F zkmN}EYFynFfEz;D;xOfglS1yJ**Wjn95Fi*79i1F^g!khn&FTxBI@iS29* zTH;BF;Q7m8y7;hC4&5HN3~)M;j9Zo@FtmZg(GLlbkR`5U zwH2bGMFA~IzFe2LTI)*KxcHzX6nx9(Xoj+8{cM^nS{T`0_8vEW98#sz6hEX~nar>V zeuVlSj@zBE)^i<09JzwE!bw6gG-M}l+B-boYW9JBvMVyjg9}yamnBC^j1p5*cokLk zw_)9MIZ6}!s`XZezyhWf&gcxa*vFxke~z>2LTLBRy0gwUV05ba*tInv(YJ$R||5tz&lV=$%;{q88q@#PIo*{u13el5tYU zYHrk^z9M01KCI{ zR-7n3it~2`AV>jBR%1yg7|v&gXI~6SE_n4$nGJ9wkXfK2h>eh#-sFMerh;I@q=Fd0 zcfd3-xc0%c_VJ175pGBSWf)>s{J8u!sz~=36h#1YMFfiArUX8a28}BvBkhUsno52Q?g-5!rWsaXOS;=;hMz z82j@m6GhZmI4Y-|W_5rfIvT+RM}i;M>(UCvc9Ih1UAnt-?wxXw(hlZ|_E+kCrLoYpNYmv}q)2LF){~fr zBRB20oMdAAA#C*Rql>0w=sxcp0+~AV^AV_knpcTG?rx3)bsQ;WwBsV3r^WZ2H2$)jXA&$ zeBoHJyzeT(k!YzH!MG`blbotx@vhw#LEws&Eqlqh^joW_s>Bcto<7ceLrV_LPA4Yh z7`vpy5OQE{sB@WSvt8xnFIJXxY`Br1KYOZpVTdUYr8) zA{#_!#XaOI8OmMyqic%h+2KR@;qIXlO_0PxM!m`NN8pCs(jFB>3`YaOHT;Kp7QS(~ zHk!AzLJ=ZQ#Z-uhNjjE?YPVSM28;S8H8R>JJV|iqKnMSgcZef~5A?>X?vM0tWtx)r z-Xs*-Gh*0q6dbvDU;9IAk^s5iwg{VX3b)dD&I}VI?3}C+NF)oaa5>duvM|f2If!N< zrHeK}K-`e|`Qr+vN5cF#7w06Nyuphh!NSy}qJ25PmkddE@LC>E3Bn}rRGxPvIKsxR z$dT+wH71YFmnKS0SD8W(LzCk{e*|DiMnhQVl*h_7Fz{r=M20mYI4RZ4_Td2j4ZB!?KD`4sZrNF8x)EKYjiHu4i$%Ttut$2@*Os+j<{{ehI4mq;gsVAh z^=BrC$Ix!W3JWZyy70)xw(8$5MiL`3@3GEnbDo3_FT{@_f@n(#vz)Si`9ym9UEBrEa7i<-=PnXo+UJosYgOuFMW7PB-6rof{G?c8On2w14@z^Vvt!MhiZ0= zDjp+>R3URQ$nXg@$@Aq)#i(ukGYLnp{?@~x%&09$uUUcn7sXbO zu&9O}DXXucIb0@3f^)=wBiTx~56vL5f6oO2G*7ROs?tPuueHOT!|O4*6Vwe1c|d9G zN);JfX)o1hv=sN9Ok#*4&bK!GcyoN%RkEs?MAx)#j9Ry;k4W zon%0y90B(DT4f3;kpe1n%z-bGCff3IEhj-bnbPZ&r}c=)hfy%S!8w zk!^ZEGTcO_6*U-+{_MBcY@CNI$w>LGolTzpv_$-GK3d^++wA2CHu#Jr0YQ{GbiPAR zfu1F3zqQoPfz0!Gx9dlbnS7>k*ypUJy@oQbj+|rWE>zZf-1A$?$ROG~*^n3>i9Mt2 zvzTd*@DG?9nZpZjqjMq+00;AvEnqMsuVj4ck#Ops*2#~)tALCQ`?f%8-P)kh)vcOH z>8VJAxs`*J=B!1xB~1Hrn0Ew#BeDwmOlILU8SlDy?R&X zd_+Ig-JyrVACs7?9D}>tbCeHtSkY2~F$I1oqf9jnwaQ!RZAs$hh5kZqcy*S2vW#_< zr|6@@mkTP6HY+6 zZ-N+|$k$5bL*DMw{l!OsJkpoqEIb#oA|*LNqfSdr! z5co5?Sz5~+d4(otdlw_kq{7m)t9xI&VGu-?k8{W1jO0eLW11jp(I#+)(=^2Opra4*VbLcgbSv%YTr?gFqj-+!v;VVMzH-8M?ojBC6i-Y_y?=LP(}bmowSo39laNDMjZ;QR6(TU>-S^+V zeDn19SW=T#c|#>-9%(MU;u&hH#y+A(wE!DUW;>Qjw-t-8y|(3uipF~SoHtcdmGf>O z2@Jtet~4|;MDu_~$S#|IDt=!y|Sywf_h$IzaV?Oyi)B7F5g1Dblsi* z2KJs49eGBR`15B69$`-XEha7v2uq`8!uQ3S~+M}Jq znt3DT=ya{!V2vPGTnyG`mHOgG3_*bB0iGJpXy-PA+;7v#ir)y9DIT*B1Pm-djA%*q zEJMGt=XK$pT?*kM^neGq@YNeqjA`hxc}@7esso|x_X0O;@SPUz0IF4>0EI?UnRHN@ zdYHuW0)t6gwO;8Bjzv_8kLv|b+;>_5^|HUJeA1SmZTf+W1q)~DDB2$%$pTQpotpGt z^CaB|jfW@%xLI#f2#g=?-$566YpN!`B)9Aw?myEY%Np|h^(Wsi2l>)8Z@%*YC=D82 zRPT#jwQQ27%{h_y3vMuo_8jx1FY*!Ayfmj|!#hi{6fG39B|(fh=o@LF^A;gH-4FzJ zNLdC+^Y9McZ!BNI{8+-PQ+ssrDu5nG=}eZA4}bf!?meB6DhF`+7ZOvml@GVZj@zlW zRb{chG=UPK2tq3)OEfh8vO9bfY~%)J?F)B(On4iVFVN23NCqEY@&`lgVAB_B=M>e8 zSz0GwJilXJ0~7nHCixVE_li_SKL;?{+J`}^mZQfnE`n$e zA1s9Gn|~N*FLzspuyEzf)m`q4W#)S0%ck#epEKXBG$i%*KIxvBrvL_6$x0(DZcPwJ zC44v#bz9$Ug%VAWP6vyvOn1wUs_J;imx+=SxzT7^MUYm2gb%@}MfKG}B&2;3T;Y0& z5lG5u4Yvy_>!)`F8Jrn!J+D41{Flr12-`7{N_weZe|7j~z~9qEz>H)+2m(VEc3Ixu zbp(gZlg*s8&!DSI2Rpxwo^(OPz@DP-5YKMpO&sD8^+MZf&w7w^A9w-XwiDK22>)~p z=p5$=$M}n{+$BOo2o%DQJL|yxr6oe(NM#}rH4II5urmr+Y3mf^HCDPQ@P{Dd)&!YE zhKyNwX=kuP5NC=Axx;FV*&G>=2FVFJ#?ST8N;N^EJPD>|JZSbdLBMLTl8ZUgw>_hV zS(c3^$Xr-?yS}|A^H*W!LIy)*bx#Ed^2ze@PU&1+fwn~79dY1+;GB+dC0R|yjTbHO z?=$1ZCi=*ok`NQ_w7RzT9gFuDFgdk2!Fls^4CC%s6^lQiJDDequ?bq-SBV1O_ zWz_Du0%jzntD0nIrV-w=0eb5_{M)oUJuxEk+F53J1jG@{k!K|0FUXekYypfv$1vNp z#DizTjv`q6uuYf&oyrdi7=oA>VOb}99(_`E)ItQc7e; zYNpy>;k0|$Dx{Ps1WAm#4E36a)B0+QdyAzEF^?=)%6FUD+ZH9>5tsQAf?pnR@M0a- zEkU$PcaBRmL|JWyRV<{UksZIHj$n>eUeMj2GkZS}J}f+1>>+Wq4W zdW&eDI^*yg>MgnG)aNaQfZrYM$^(T;IIfn)d+reTheT0f&(kqhaCGQU;Yjfsa_d#M z{F?c2@R~a&E=IL$xy|qkp*Eo`pP62#{PzupoB(c1Q2j`~W!t`7RDMPFn0Sqvu^fTY z%0Wo5>PB_&E-v5)3DV^syEPfYRXhN$d!mstc6w;zDBSppe4aMK!6A1Q#4EmBU-D2CbHvGTuONhsuLW zGA=caRWe91Eb5B8pY3BT6S>=WL;TUfOOAe=Og)s0WJ$G{(!85LSP68s|1(fO;YF!y zceo{9quI)dQ+08TD98R#Teu^`W6OMrcdZ#KnAY)?cT}a=Rr{k(${7%Yq(M@z&uvXwNLB#B4D!dXCJM(UlLHlgoV1M1_?f!mUh zMdJS58t=gaXaQ(D#CtU*#hC{*sCtq8iNZNgM`v$Qt!CwFn=6x-?L1);{z-s(3wl0* z8}YRv3r+Zxq-w3y`_z<6&E-~3B2n_n>Satf9ds7g5oM4Nu^^sQ_}q;TeOuk8h;>4MbEl0+_bEQ2oeFTHWeR#j03-BGpgA)$t40@ zDZE0PO!O!=qR_vrFgzaWQh7Frjq+s7kRjKo7?j3ZLU4>;tNWZ!4fY7a~At{PvgP*X*78{AhCcXHAwRDrRS-+$B$NQ+mYwu1&kS+J55TtIZ(-yIt7E;R74a@rn!XuE`o`$PU`=c~oI4#t? zY!6Ff$}pRh2on6WK|_Pk{)#?ll;B+-qZAQjrjkBKKMr(t@h3Liez_g zZOwU0_EtxZs}51h6pStpX~qMc&_IHrLQ+R3l+(Iu-08|RoH%w@h2sw} z-#<9so$UOlF^k2Kw#;eYq`QhJWVLhX3;^bW#F@3k&p_S&XffxMu~zmfJhHe}*IWstIJ!u=pFkoggx>bAYghMUSZ&Iq zEYx@IGa0QGxIBA$BayO35M6G5UU*)i{o#=yh7NLX?i&XU9~L0cHY{+rD3ENehn@M2 z*-%Ei9X_o!lC=-=1e{!xK5W=^m6^C9nqfzp2r`sm4xGsrjody%2fp?LO~^fJCR71u z!5_J?9zSpdo^YVMZeg`QA?a1?*;W1y$t-Vx!Xupxj1ouAq7HD=0+ZhcJM%Vfa|uYKuNmH zNS(wF#gN0vNaJ4VEJ9>%I_^3FeMEfysd&1(_vr*NJF@@#46B3dKsL$3J}y4FRf60A zDB{6NVN@6^n$ggSuV61dvpIX?UDy2Zyg)Z+Rl`Y7Uaz$D>i$)=`X2DW9pFYL0DibU z-ra@KuGaR0tKJ2ze$@LZT>;UzG68x-l<+46LA!D|P#L2<2pO}~14QYLQJu((_xuW0 zF`PG$h0?HNLMqKf?{PREKybcwML%k!o^34%qJV%rdX<9V;iKpn05M@N@p?$<4i|D7 zhs>t*DE=!V0RCy_-i(P97VOeN|ZB z5Rf80EpYU#On$P)?cV1KHMtPy*l*+ag>HoTY79@8V=C#TvBLDv#LJU;Qq42r|fGDMfUcA`$Q5j?!a0&aG7N~H*G z2;fPz{*jjp5kigbC2m+zE6xD8QKJW71=sS|0>#7tcCz{?oY*N_N?=tlsM-+kie!&9 zf-xHo%<#Y^Zd{v??iFRNNDp&tOt%&t-bFo0t=o>aqhvA~6|zQ6^a76g0CxTo+-h|E z{^#oZ^7Unp%+4(df-gI*Q|+NfGOk*twX9cY(%h3t3gAUPjgTeP*+Lm1>=*iP+#_I> zAik*?d<$3ZIdy)0Q(WUmG-7b5AE0S6g$Lhp+gm--XBK`~f^co3zzqk_ISfC-kR__e zZhM$M*qzn9-%W}kq35wZRa208cx!dx&OOxDIPc__1xj@eSnh1Z;DdlDNKRYfKvg5rp#MNeHqsIjAetc#I&c+mfRR&Ly2nc&K0`QX$|?wYV+8 zkvrYa53`?UKH|6pMK}Vw5<_Nsm|7JvDOk7{Df{#YhGw2^=0Zx6sQaV8OxNRQ%4lMt z6zVq@p@atn`KiEbv@OL6#!j*4=w0lg6fLl}iNW2m0O^H8mr}-v0QrKM>_LG828W$T zfhdv|(jGZuN3MrJYms|xXy*H#gXJ01>YxlP(v2rZSEm2M3F@}Mo{Kax>^;yyS>OCm z*X8f4RRx>TIc0j~{&!?{`!Nwa3V)%7{^94$h&gwMgR$-z#|vu-p}L<(CT@;Li@D;M zJ&BPLhGmv$<#=g@B|=LO{v~dlY|TtR4i*?7$Pg36>1=_7PR>YXA9``vZd+m zg(^#L(6H|kVL@X=Na3F1Sv%M#Vn^koku-f*_!8M5q9QruilN=i!gTa&TrG%PILx24 zhIWlebSfDK*W;l|ANDqX6E(GbXIuRA&1WB-U!0wK71ZMRF%;{cy1Y$O{Z%^V1dDb9_tIFGAdwxd zgO^b-xk!A#kK#mQmtvu4OV0%Z6of^1&BP{#hMmQNdO zBuMxXf`FUg=R^Yn{=Y@?x*!O}F;?igFh_$$E0T{IO z5F$nL2~;4+uqs*tF8haH&>s`T?jJ|YtyN8>o`9nY6pr1Jg3nx1qE`L7Tc4vGBQP3v zm>c{&RQ=s8iCFo!Yq^;1hsag?a{G?)l1Ix%rsnf&X>{Ga!BG$Y^2-F$ufq`Hls&8y z*aPOBHO^v1ShKzx$^hAtB;7tqnANy^u63-;o9|EezyAF5uP?u`U}@3iDK~%?IRWWu z*1JduB^z`fQ8SXM2344h{hBM*?25O&fyB)4mdvMc5YSlK;28o619KCgd*z#B< zh?r476)xK_CA{yGrKW*dJ$W>^AOph6$jFWR;=bwl*dw`2W%BlWPd;+0dd=~CnEm9LP%`gOf~ri3TA&Ho7>6PE zhr+;P3un#XEfLKjcUjpU%DAB#Z^G;q)%DWv_8Hdw$*7YtwCJFlJSN*e{Vg8ZP7#6? z-Oez;Ap2ee0S0HEeMZXv#XH~cJQ)MeS21>5m>~Etv{?5#>uabxp7tO}4CfZZb%NB2 zf4-3*h!f|V7)b!PpDVM&b1IR$7%Du}Tg3?<^PaZL z(s>L&Rb?Fc+x%d~q!`Mph@H06 z0`InUBsA2e9e?$Rr|pkaLXb|Q=7!M}Kdby}oXA)k;_6_s@7N?WhQS7cxX;;ml^|37 zgd!~>O?P^zD^Jjf$)Tt<3A-IQxFtCf?-o36Ks|ht;0+_ z9&a}^pnEt+GS0Ev;mGIjet!1Okp~J*`ihzVbquj9?r%hwSWKs2RQ-jV|}E{PC`5Y6?5IL`>AA z*b?}F_R;;}wy$_}p+Xu^l-M_nND#~+;c=G`@s*v)+Ie($mwW3JVX2smS&jzrDFKu0^vIL?+K z_GcMMA_GdE@szdR!W>55}r+ISQ^k7S4mD>Mth(36o)F1Pk-P|`+4_whU?5t@zIMF4K_nO{q6t55YTaZ z`pwRyL&7i`R!!w#ue*210MMn6?daf`xlb>7OyRr72ct6Q<`XGUag!aPT9b9A>|tVI8;hxCV>av#s>#u2| zun96=vcXsFMJRqikRW$(@*2Ou17w;~B zCm{&e;RrWmt3eaM7lROS0r8HFaIv99?(00O{U!vNILSQ;iYk%{7F;+VB`Yu$OcEaq z>VOCIxyd6!k#-zllM}qAOqnHt_v182c??a~@);{@*X69Us_3mgDP)ZMc&@_5KX&7G zHlDW&gzK?$6@$s3Ogsfq)C$1>FM5_W~)@-BVtrrOgKGNlpdIt zk-w~K`Dgbhazh@CR4ev?cBS#Sy$LVtwewGzjiW(y%HJeph$$0^f>{G0#LVimANtT3 zIGRsO4i19QU@8_ZcCrFn!Ah7eYA(@WE%aaL`hh_$80Vt+I_ z0tZ><#gHqr1HtYVB>3PvDw6U`_(Nxue82bmk=CF%&z%IwI@S7_V334H%Mp>}8=37t zEiD@6aWGM2mnB?)(BPVoTe;6001XJ001@s003@pWMyA%Z)A0BWpgidWprp|axG+X zZ*VVUZ)0;WcV%p2Z*65SX>DO=WpgiOY-wS0E^uyV0VhG&zKKvyM-2)Z3IG5A4M|8u zQUCw|$N&HU$Or=f005eXUaSBB0Kia8R7I|?u6=!dii(P+rlz^Mxv{aaudlDOv$M6e zwX3VEczAe*hK9SlyLEMSaBy&BWMsIwxUjLXfPjFuwzjFMse^-qZEbBPCMH)`SFEh8 zo1B}MmzQ5(UzC-Ua&mH0Q&VYbX-iB?v9hs{kdSL@Yhhwxjg5^eDk^AbXsN2Hl9G}y zFE6*Zw^>br|k~qQAz11(M@1rCZ1zs_hvs#Lh z;1gs8kZtggT!Z{@AQ;~HwsIg*bPF;IXAXoM1&(BxK>?Ix5g{M}@g#QWvt>dK{%(>+ zqB(RdVh<7r9C`2@R2q*UD;lzJAf99d5e^SR4ngsb%$y^_K}(X@MiwK+UV^?NA%t+u zd+{T&ffhL7zw~%a<2%t07?NC)B;JwjY)g>z%8?PoTOzlx2LwP!!aoiWxIqvhDe%L1 zHhT-hj~(=VI^BH@(@%{1-0I08hKP?&F^#=bL693uEP%pcTi+d6}(a#-+2LXu_bLmzrzoiFp3RbJ|Eqr@>o6(R( zQlxVhqmvI_47n`!6uwh7wYFO+lGQ9&1mO-4!|AkMugj{d%Bm{MLF=I`b^i1*OXbFp z=*kc35(IvTDOa}GYh>y@NC*sqAcO@SVUY-lV;JKfudN}+OAzZwxY`D*=^jp0r&DPL z%0RkpOJG$UPU}3+A3u0s#F3sN#;qgda4MX}%MpZ)E{c|sIIuRV5)3vQ;ewkJo^;npTN8og&WR)8d)LqKeUm1}8%B$3N|kjR60N3y7wkR1ozA5C$F zMC=+KBuRta?t4&>Ro;OOX=f`$E)Gw2-QWWgYBJYUwrDV)3 za@>QYk>!9`BS7(vAY?{_uqaQ!DFHLLoFMd%RsLW)v~*K*-JbeBjcb zk`!#e_;Lku?E7Qd&$%fsqwO>=wMI!))fx%qx>dQB8sPy~n8r1#&-Z(!^IRjp zCcA=;GAKcj9g$NvAlzSFkywyynPw0<1cdS+G!!Irhk`LWauI|sSU|u!sFEXo8f8(m zd(JykF-%m2;D{w9&rvhFr5aO}1MxattIgx1>a=Rgp(~$1grbKqI1psG2N4w%IUEU4 zJ;+i(P$XLml2XhVd2pMv%eqC3R$Pbj6^NbY;UP}g4(L!e z51(7j_+7K|g+UU>(zRN5wodeU(+v15rs~7%=6q1K*6N_tB%sw4@3FmA*$7fxb4Yf@ zHz29sM}pviU5j=Rg7O?B8HW+1U*+8}AoUS4I0OR>W2Ix-m=3u`53nt+jcN_~Fxlv4 zb5^=DZLL&3&9Gu5AW1Ba2#4enIsCpNGemJDY2rb^K@iA-I0&{q4-!xgBuZCiaf7Mw}y_10*Ae6H_aDk0+a#M}&iWEnU~DlIe$AJ&gb*%p;4S_Te^fMpz7!EK?ac|;ov|>A{_VPJmXF7L8!lj#DbFc z)4ejd#mF8TJadZC6GiTIhdO{K#5#Qn7f7)h`J~n6uz5Xiz;vjau7xE^>-=R3x2+|q z2Pp*Pk0kCpFG0dAK1(+~SziiAJw;7v*MDH+o6b$#cLfLEs*6CUU8rG)i z6gpU2*NswUjXh=(^!<|IWiAH!>wktAzrDx*07*dML4@N057^;0e42dy)}Tx~2FKvt z_Qx~HE*=E}GY00Et~G|!l+Z!kR?rPvK2Wkw(9ZN`8puxKYUD2*3r7+&ixPr_zStKx zAkGnIs(@@irqgGIWbY8cTJCmLefHe7=p?eciJ@(d%Fcm_$%O*ZO^XA+Kz6uBBW?-2 zd73f6B%vSwQefpk_^bsG`9G8k5Es6ylQ<9`xA^At@ujYfeY~hn>`UAV5uR_o0t5y? z@XcwS@w0&EfXaq|p~*xU)mkuBH|NP$7|)#f9|%Hv;O9szAVRXZ1X;$OVXJ*@+CJ;s8s8ciA0Y)cMY#4AeI+8HZ2?@)7 zaEAZ?uX@hC3#e`Lp{J1qYMc9T?#r@1rwASOfMV?s>*d!ElH790Q1AFQ$D!V#6l&A4 zHQ+BW?15%ld1gd40Tn+xIE#0*DE<%}xVudDq0kYT*M@r0Yp{b6Jxn0Vv0vVTyxf3z zM;5c%${6G`Q^}-4t_;$?#Z2<^8>GNIyS8r6L(RZM_`;rR8eA+JbU2`ZnizPs;Q~={ zn{2Pmo-4e8WVsQ2;{owN02X@|0HN-Roqk+{s8*k|+&Y9W9A_QY0)hjfoD!9Oy8&Ji z!*=Y+Qsl0|2t|2e8OBvg$4N;PWEhHc*g57kY#G#yvn+0tae`-HHPf4zV}8i}V8g z8`>A|K3TfZ{t3TSKsMJPJS{xI0BNVw^OSiKeTL7zXTx)pzzWrCI%rmL@U5$)1qXULOM{+vQp%~GHS5^XEcMQ4*RkI5MJmJ5&IHz~MLOUbIC1o?5G2&0cfSxq)!lHkm?FB(eqP zhdau=p*1-5m&?`-vxY9BzVY{^aEXCp4sQn_Tcp zG(x)OaDCqzfb@oqnA1Tgi$jDqvAQ|Lc%iXMh8YT#>}i)a{(0lbl6v4lK&HGR#{{C2 zMAupha&HL;Wr}1PeaYZR(m6IE2CFI^_eyLDKP=8{TLP(Bf6f|VZzw?eW{`$uY(`H*hTu#P7XiU??ILrOrNz9 zwANA#kHQ1T&+6gX4kM%Ffw99Dfn!vRKS>Nssj3zk1|l3C7+h=eJW7+=ROvCQYH`L9 zr6eRLqZA65*xd0DIUgNa-GV%b85BX3$#J*aFtG)1!;9Z?STK%uTdUhhEk z3GsG`au4#bh@`>zKGSo@P&0(Gd6K4SaflC592WQqKvhzNacZn7vLFLMxL&XMLlA_A zCO{O5x)vli=^kWiwme;Z0m6BR8hN=}#E(2nRQu1m9G19e8C)m=GBomuL6sI^ z7#_k5_Z$kt_*4|(dz_mz&EA19o2~gL%K(#QAgt50s;k7bisEa&#LT(a^^jT<$163F z$oG2?k|6yy6Ud{Wj2r^;EFiMY>)7pW&*aH1j|OQIAi!gm1;LDAzlX;-2n)Cp2RNm8 zq$vWDg=rl{X_c_7&=P>a|C_=S*75W8Zy+ogPa@a8c7#%piG=-keHl5}LoG|VO6t01 zaYP|=244tz$JciKzI!jys0ac8Y`3$mun?2Bkfo{$l2mz%VzDI)MK)Z^rpe!)6l5xk z!H^YBz6Mc8rsTRMSw3aX&%Ms|4Qv2MdOD>QDm;ziY>fbE1+{|^`5LK)$f2!#ts#&S zM@iBsijt%;DGV4D;Uaml5$zpbUU>GP#Q3j_fIvhgy!2mVr zooF^=j%69n<*GO`u#zM=9!@7@S8_^n!}YsNTd2I)WDbb&GeIR8QtKK-Q3$L{lzR}8 z+&S|2)13WYlx!FzIT9;6r74mM3Pv}!-vwhg^B2Bvx4W&EkZ6WUMZu7w!0q6ubR9A1 zIX`dQ4FOLe>c}X~H!lTI%d|%Rkz1SPA6ZbcoKqyZ*Tu~5Bt=m0p{24I_vc5t)oJ#1UtHfeDG;#m6+}E)*}Zg|&nxj@!UTuRPqfBmU!&D@O!JL7rx|BM2v; zO`-_5F(U_~fh1wn4;90ni@-2m^SDEqkfbO;0+2-Ze@j+825q(+Hfr~OqWEgLVZ$ai zYeYdF1%xQ~3zf}pM$U#kQ6q}N3LDeKG80KP#DIVSYuyK*5EeJvZL|OXU-g`OM^MwG zJs4xR?aG^TKjsdY^<<8pmu%(onm~)rjef^-Aw}rw>M030Ho~F8cZ8aF;<8>iLPL_= zniAE66F*7tTuyFfuNKl}yu{h4=g`933m{Uv1`r`R3oqfB&F)v^;LZ)&Ra@f3jS(v# z_<`@rF?ET-XO#mXn|KmInkgEU<#xNRjjj21RT16kJ%1k7C1c z9Gl0@W+Nc@rXWfJ@#%f+dVC98!7z6@&xg zJB|?Kw^FtVS+j}^ARuWJW8iSB6Xi&EkB=Zx9AFSdK{U)XyzqAbWfdBL6d1<37YKbM z{`^Wycp46cq9c8wdYdBxf+6xvvb)29=%D=!5X}*Q1Qg?Hz|e%)?9w#dZD5Pg5am2! z2;@9ri~=N&V=PoUArX+*8=&AUCcjLs&q$y@{KTbhwBvEp=D@s z^f(JF-3zt&>^6D;#iYt=%*F!zl z1d-$s5`vIp01=X#6HjjSTFzl|pjrP@9YBt5xIIvS$j!^pgaaWEl;l+E$TH(_%g8D+ zTbi(01uDje6s$<23IPQ}mdgiX@Ie&$Q+$zRqor6us1+PRFKJsW=?Q|R!(sWn zAqHQdSPPIi!wzEj}nL3A@{ zq9Af#%Uh7i{emC|f@E#JK*>}~hDPWC!kbEdkb@uuqBTppLns=e;s`0Bg};a#TY|J% zjWmG{<5vY4N)LOzf&hX5`lLHu_XJ^oagN-)3o;t52qGL=B94dzL^w#o*m>j$lS4t4 zc%?K_ksU>J9DYLu!AhE~#J0%yJ>KPc{B&svL7>T**eFTjK%kF6pxJ0P<~?EnL1+yi zZi(eUrsPO9u!uqseaY7hu-=eJIT)S}L{aKi-sBLxD#&SYDaecmy_kt9vc=wm=*8)e zKxS87x37uO#cgdDLc&hsEFlUTx~7OTLLE^il1OtLCPo3F5LXB;)O6DV1?>c(^#q-P z+)_P>Tri<}5U6h>J;*tV%)09xLAV2ywTb21$}*fFx_WmYY=S$Sx2gwG4%bHJ3@u17 z`n5cU&7ed#DR@&vKo02gT)-jHZ*Cbz5ZOFJvQORz(PSq`h6t`RfZzv6lp_e@1e&|* zyOVr}G_W8|5m`pMRj`t%A12iCD#r_%ONwlN2%p|2iROIIF&Qi&$p3O=jn=;0?*$`^ z9Y_ceNG+i!9!MbRBM#-k1(rCnAqmxkKqL`I7_9(dj_ixhygCr-3`9wk<^rU@qX9s?pU^nK@hi`->^axXaf+gQktd-00J|_5rb?eNai0Mfi5uK2T@06C!s|RjzHKg zTaUY1HqR5j9Y8?BEu8=wazsE|_$W)uMN0MkZ|Hw! zJKOxfgA4`u6vQiP%nYc#G(@r;Ida^;<~Ty3^d8fLQVtY>6}^2MmD9 zvINMXdiwhoANYCtIJ5?k3rBSEdzvO90U2HSS#5LMS|cFz1WD4@UpKAyAad>`aT-{5 z2sF0$2>F4bDm0V@h*6NZ*uTVoK6z>Cf7fLH5aeniAih6<;L}D5;=1B_BboeWGZRH6 z4MB=P015k53e#6X8~~sB{i`k@c_C8D zCGccuh|ZDsK~Ss1EH_bLWGf0{CxHS1afSc{i9SS>dvO#%C^Qt72V4fqZ4xu>c$Y_i38#Nv*#c;7a&;lPrNLT01}EN6yBMJ;7ex`7FTw4 zck1edgOL|30RrNYqcK(n5Z-8jke-&4vUJPw(4Ru zr|1)s{auig*~(Qd&W8Xw&ylmyT|m&=+d2xoG>nWmA`88RvNVL@+QVZnWYravfrTg- z+#hzFgArKBj=YT_Fi@Coxv9bHyob!hT@o}+w0`=2wx{mY+cD@k>Jns4LoPsiOV-%N zUix8eqBIR66n>CkrH6;$gsUcqbhA{>^aSMGlgHgA^*p3`Ue~f^0s_uacyM#vR#lxH zOqq2C+o_R%4nmY$2XX|!ap}nV!VzFm&XH?BYHf(x3y=UlaDL#0jigSRu zCvd3Tgm~lU$v!p!Sw>;FWlW*YFvRNwFEDhoczJB=^ut*)@T7DK7}KCiVw`rDVcgV2 zhhT^x#We<9gEX;C0Fd1$-IE`5OAyD=>kMQvfH;zKkWap`_1?C0Bt&Q-nz>mkZ=S{0 zTGN1pp{4yCPv?ekl;6M&j5G({EMG`O6)0CpQj-XDMTWp?!#2(q4GfMvpCH#KlF+)u zU;(~xWWWH(fC7+PKZGTjZ9O;=EJ9v~?I1>YhCty(-rTI<3TZZ}8sn@{jt!z}8w36Y zBnT#OwANOZm5LLaP}Mc^q=-X+pq?+EE*;_iyl{;ALp0JG9~{VqAA={{X>3eY@4d|U zUN{XA=5T|tE&KC@R3AVg(5f6b^XcY@!e`VgRJ3ulg?+s3j4G^=VrRHeA~qexKDpO%O(*)v#uUcD!lMr7VLVF!{Kyx>1-O$2dZ2ZgPYA3uJ?a=dhH+u3jhL!x>5{a44aJ_Wgd zqXA@c4sw|!v$c=PZ|lw;4hV%j5eE)>Rpy{TdfPbuA&aYR;CV8+Yq8p)!O>G%wh0(!?0B^l~3LTgp7 zY*n;v5hDa_4UhxGfYS0q9^AwQFl0=%9eX>V=!li>rda68sar!AP?&z~i(!#c*thL34L9DP(?B|3<8=q;^L~Zu%_S2g-LI*HZ~gr zp6ssd4e^Q*K~CshkpHU@=KW;q108~#r#Lb3z*})2iL@9kJVkDwTJxKI+OoghRBjql z)r*|gO-VAX;TWnL*GGFbZbdAGp7bEsjAyaF$AjJsj@^&P|tv~0sb%J|nz#!TWv3Xw7B(0=qh*3sCgG^xYG zJ%+J6CKp2U=;8Dvp{JOUardD3U+G`q+R~EJx?Do0f@Xw6{HCj6kvUS!e)Se7W?6F- zhFhGNgsT3L3$ZaR1Tn82c|$^WJU~$_vfk)Bw04O#I&p#@2%ptHj4)o&UmSorv6r{A%@m3DZumq?kw$bU=Zg~aaZ9o{}m$Qf$*JV(tsFgWxZ z=K?uZ?^~@wurk-j?d{4I&n8=;4FUUz`|n>0K#=y=gL6f!k{Y;zzP~Lyh0iykjH0mH zgtlEkN93ZRiCki=Ympt98&kpqVVjU5N3`>q5rxfv|BgxFN-qV{Ah2) zYCa(!lgG!8E+z#!BU_oz^NSsJSjd>)pb+d<1GL}z?tuCJcJ}<(HM+G1uo(JO^inBK zPTKUcX~9aBbDnL74SC{@Q*ap{!Px!xz{gKScTY=H1;I-$$%*X5?-2=qRku|J{QrNA2h zrV&FMn_qzl;*f_(Ko4ieV>=^Ywfykk!K`Z0U`^f@!Zs4OlVEcJ=j(%>YAl ziAaYq`>(D^dD0l7q}TCn-t42Lqu150mZ^u>!x8_MVe}{zyX`PyJR-2!MgoA}N{(j< zPv{mlMVl2Ek!H|Jj1i(tXbFiJ#kjYsN_mKsKNb{i)*nM^f;_gY$jG)-olLB-F?inSz@D7uSZgG{1~-U{ltep4 zI}`7p!3u5ust-D{Ndn5I%jC8{!;GmR1AW(+2VrB#Jlw$}ZDPUhD<$i{O1g>Gc|Rji z-Tqp;lHy)4jiYN!EIj(N|Kpswn;SxNF*~GL>l|U*qKAHKlJ6DK^J~BV`p>=2#frC8)J*$a83$UEPB%*AuXY}=W_~s=2Y5pPAWJ;V3Kip{Mhp`J zff-H`gvG3};eE!+YEiBMFHN7znI}s}3@BN3_uKg!+CAUN0k)M)O(L_HT^?B$jzjbO z7UgC&pR$(OKKP@8UWws8aru${0D5fhN1i2aTuU)f2|$b4QBN5Zt$kaJy&d*=?Kmm-v*pe zFY3NCl^7HP#rO}1n@Q#_#S4%ow4L6&&`Du@^MqF;93VGJ5|hKs-1GSP6v>-k->}9L zd@-6qOqxO@Y5wE=3o*bbZUkM5Da_Z$xzID4B-YiKtSb(P6;7V z8!RCKLxEQUe--A=NXW62QwpDN4zYQ@w>NjZFQQ*G1Qlcu1Zbv@LVryW%b(amLr&_5Fj#;>j5_<80|`1IK=in%g5i=zNn`C?8|eB91cp$0z&v?Gy_&Xk9k~#|N7BM$r={_&O5Y z;O@v}OEuZ13jmqy@xr?amBl>cK#z(i7>4FChHi2X7h8|{zZ(WxVqqm6sF`fpLPm<} z%0gh4`!4?huI#^Ben~p)AbT=D*l@R<2mCWT?=4hB2dQ*5tF|BLWuFJ1jW$V8n_Y(^ z#Cu_jm8Vf8??Cf0L#HYSuzla1IC(l?eO8j;{$MTb1iGbHN=$UJJmx!Ko0V(_N5V*T z>wIF1RDH(uoU^~MzY1Tp^9v1ZR|bC%c0x9Aoc=>jB#-66D1^T6xcnjl2rzV<9Feaq z&Psjw)60kmLFc~TuLwpB_aqs~3M&I&GmtMSU}=RV%nt`tmqD0;2JW%>v{TSw2)97 zd2&))zbF0TY?+Dz=Rg-4Je3Nt^&)r}HnBZ2GQ@-<3Q2}=HEG}!!VTp}Tvi$At?|Hz zK%5E8!cL(&VlAU;65H`?a4zEZ0mXR6%t5z!Vk&L^lD~n9iKUk741Y5bIW4F))V4hU z|IkB+NXy74#FZ7fPWt$zdHXa305Zel|4ZS6K#UWF1GCy7MxgsJF|6g(H!P7crg8nD z@pqmxpQ$==U4}zg)<>XQ#ei zYhrut!rmhM#F2s;+kXYV`7+AQ=K;;SB1OvYYP*^^nzEXqiDAZKT70Tl8`Xgna{xcA zz2)8&U!d+Pg!NnPboE96tuBq-dw&Z&Rp;b{bmigI}p-VPLv@YmTC*_h}#GjQGiA5M`Tv8@{AR z(EiEaIXzBozfm-p6x(e_D;Xd9ULHa5X^i!FR`7AMId_}Qpdq7hVW`~#HAlb2z^2qM ziMTBF)q+@z6w(d_)G`Ekw@0f!-U=tpV4x6#k`=v~L_35XyM>C>MX!Hw-g=B%c;KfT zNp$~fdinM~dxS9u1i*knC@uH2Rel_Gx+P-;!*1?OrZ{`w!PnYx=}{QBs=)XZ24HKo zJ9t;W&oI1w3aB1AYi|!tm+LsMT~b$FyCz}D0C6k;sC$w?FIVJ+p}-Wq50+A1j9w!c z1FfX?PaW*zg_)Yo=ZFcv;YFrQd_dm@;#iVn!kcZ=bAD9C(scW7o9XneCEHaYfE2$! zPlm>NiEM*7mB4gf@}B@K~ltrvyO(T-P7=TKloB12JLV}0mGtk zTETq)SQbVE_MJ%!lrSkEJIlYpS8L@~d6Gmd6>lD7Y*Q6xn_urReR5Ng!uuQ2tlQ5L zXn3!OSqL@eix%!QYz`#&24z=c0F1;CZI9V2W;WmbLNN9Uv9EOg2k3D(P`H~h>;grAhg)moDlAvS&$o}N2&aIl96NXdHGvZm&Hw+@%QWrTJ^d8~slKXw;219fAF zUPApV+VHD>J&Onxrk0W?A;jDZx(Ce-9x1L5r#~AWj1&y!tJzbM4iAJor^XSln95;M ztA4i!SjLSP;TR}}Az^l|BNh$|{*pNthbfzrwTXsM>Ism>OtObizgHzrF4R_;I=H=I zRWYPv5k2;eQxU{2gqpKp+BZq^IYSzmk^mJUNYd`WgdNb2)kR!4vL&2+@IV^xl2n@C<*Z2hi4qc`K6GD$>?;QrlXpy>AXE)c$> z2hBa?Pp=eA=j?i#BMj!V{jkt5FA%0|Gy-Z2GW9dmGt}2%`bG5Rm4!oA2ia1X_cxC0 zKdA0F@$J)B*YAvTJTSME^1aDO?D!4DNtdl;fI=_gy6i$4TykQ7*5QD8ct`a0o#-O4d zoyFep$5g&sGK;0M7u0TgOL@X*IVf(TtIt1w`0*rd_a)qza4luaGj4tv7> zsF88c{_z^u4e7@XR(W(2lb)ocsiIGBoI6U%Bl+ChZ`(ivuhHAk`bn{y9r6($8$sih z!VT#2;%$frBmDdv3pH(#?vEhPB2ZRW79?^6Ta5l zDv>@ATK+Aqyo9#t-;RR?rCj?eCvILDoA^3-G9C;Zor`%-_{+;Gi z2uE3Wrp$s#P4c@v_3BImMPXvku`-KY3g<;I*0*PPWKn#C5Fj2p2-?5FaG{vf4P&8A zbUn+Bl1P)yiz8YAsJ~(mqZiU_XQ@BkhSi;OtAJ!7>hpGd%ySizZ!}B>%!|L{~L%#-a&8 zY{;N`SqMb(+dUM^8)=LPSAf)52SwoLi8|3DF0nT}DeW#oSuPESF-Ve96c93}GXjy{ zskUYIquIjiZ;3fEZ=^j?+T1=bf;9=k{U0~{jd$!^{|Xmwu1%)&eEnw+=ZerOmP5;-5MWI-?o z+8362bv0-PnhqjR%MlNkZVAzcZuY<;;{U{^#|AUXO;%Od`+k?~&B#v^yXD<0k0EQt6)1x11|`!+!Y}$Q_ef7e=#VR#^0pHC zZrK2g4)v@ibzcY|hz{yq`F8oNsW!8B$g1>p1^&>3=A?pV-0k{4PJ1^~i9h?`0s z_Uo)(kk8~?#G2y(8rHC1L>w#W5~;O!<%DVAAg z>`=l43`W3ZGWY)!tqvpxz$6uDM0q`cl6ZjhYRsabF{`?A)}Py)O?Y7vQWmaE@0w|u z{8KHsUq~e9at^qRsB4{94v|qqj>nc~OxopcCpA$Vez8$a)Z(_3CwhzS zv!a(?h_@e@lHNYJlx-ZcjS%huFn-?P#%C}9l`vH_tH`+dn??GDSBr759`kdg%rbF? zs;LPtQZtFaXs7H6%MCL{Bu*^Mzr*PT-dm&a9V`W)kx zpnsulivOBch;(1r+zCr{_a)Rte>O>3P@~74(53_0TKQleWv@efMSKKiq! z#GR}3(%80qZEEivq$is-rY&oHqX*qu;M{1R&3jw2bTgH^LDQz8Ec?&Z66#THM zK>de30u|i?NQwYxAIuYf_VO2aO!8lI;TA?w)95jP-7FGO#pYj7-mRLcbjP$9lKBdJ zi@~L1T(X*$DGpl`i3HH8Zq5w)xfIJ{6jQFGahie0Z&MDj*l?5g-Iy9u>h48S1VH@Z zz#?D)&^g5Q{kL1=*?307GnCT_5yW)-GzA@-B_~GDO_m(4r6Yq@a2M*x?emi`6y^k8Sso> zOe-f2EvIQYZ~BQfHsk<6)3rNsSh2y`9+}S29yJZc=I!aX%P%>kffnPQV~4zvWzSv& z^oV}*2`f0N!vW9u*rhfmW!hjwf-nx%@`|hAQm`c*tH#o(#_aNBZ8>D;Gq>8O{2c;W z6;FINRW;-q2`fDiW6lK>%ZixwG7fx*3Ixpmp_mQnAc0-@Nwjr zPnei+*2P2)08AM+<2&P+JUrs4|2b0Rgtt)YXn$3NT{#19srPOcah=pZulXE9JL~)7VC2q z@UETe5ja_K!vMsT3)KR6)liMwCFs&N%fI}P(L)ACt1$C6g^KbSr*@( z{N@ViMrsD01YVIfFzon+2fheLM25;GWwlaD-$#Ixh+Q$GKq#t&JZdWc)`d< zV|`t)|4sBKd+}94&b%%N@>ce4Es`ZAzrX-?Zhf3f7R9^wQb;T-pT{Fs!pNVDc#n?zFk*H_Q!80;=%z*vNA7fxy(Abg7~}F6^YBNgA;X}avT+G8 zSu!OAxBfm-dv*xGMfd-hvYUfE>;z*dDmxeo=~lEzEe`FU~3 zdg-ysV+nt7ChjyY&%)$a)hjE7`?J$t#dOdtVvQaZWylNUOb3e;y} zVrAF;SwZ?s%x^X2Y+&FI4{eO2{sobwzYDPs5eoKr2GFB%lrlA^%(i|+XOp=a2@e=} z_B^c$>lb!;8&_o?OZd{I5`iK2dnnZQj6SIwmYmgVAv>~ci5q8FW=EYt8Zqx`_2#>q zXv2z{7+i~3Bz8s1mTf01A!8p{tOh5|qt>?LXNdMrs@e`coZ#xy>aN@aV@Y z$4-n-(<@j{H$LufJBVr$+BnGo4_O4mR?a)VftUb0ieZx25O2xlM2P2`OpVQkN+^0} z40Q_(XPo}_Pn>5@g#fKnjlH)s1w!se)JV`Sw7w|sutQOdg1c^gJFQ!NYslSu=fZ)+ zLj@6OyycKG7l5-O)3D%GC+(RRT;TG`hFe+9u@aKnb-J5)AQITt20ZbySt#26vrvrn zg^*iSVCVbAM;av}FWLqnFD5l1>^;**$rTO^0}H|crq9Mdk8W={P~fcDrbT$_qy}&|jF@eqjrp$Nx+v=m7QQ|kTH)MEFLa=p zhE0W*TWz0UrBxOYO@H1VcT|F!X0{PSp^QXLEN3tY_FL|Abo|yS;q*-|vvRuvw6|KV zvpemL+WbwY%4k?tTmx|bBGM}s4h61u{~^b7p-tsB6g%wKJK(Px4I;RYp?Xe?>@;La zj#6*iim(_Jwz)LF&;+-##oR^M0as#DdC{wi!uAVL^kMv1_PVd@4h0i!y+3>a_kuh3 zvZ}olwNoqWI3pFCqzLA;h%Al-P`QIgSs*cX#$%ckn4fIA{PJttF0I|I5s4?GTojeW zzfk8#KwQ0pWQkk4D;74lT=O%A`-cx4{*k6{-vd0fBg=Hc8O1oINq4%!d_)tRo;n|X zejoY1cCmK?gp5gL-4jh3Ycs9@WG`_Oz1tfcKaVsG62^rzrG6kp`mHaX4! zl-kP~h}{mVPnvrfzeprmAL2KJS<5Wq>jbH1^@ke?k)n|&ls5S-to_tSGxnu1Ha8Bw zCjw`}%$;3NxZuq7d?oYBM@>!>1w=_XEK)WG1NE$+sV*_a)iKMLvfe*GCr(JdE#W`v zANl)S2v2r%v}{;Sc(thrP1Mb{{#qs%c(_t9%#W%%HY|k^Y+HXH_3Xh9#hvz$gHz%^ z$s8$;DMzmIa?|TsGV%#;hf#(hvKeN`mBgzeXN#?#O-5EL_46@Y_I_HffghWIhDnEx zqIiO&Kw0kWFOK)}#bc#FY-ra~*_v1U(l-nr>!(x8yEJNy7|(Ub{tN4$mPIg(L7W5& zj}EKjO#-LqPg-WHQ2KcAJ1ufXaM;mdZE_f)udCP3YgkLvRv?Dc5%W1&%9s=iV|28{ z3L2s$V?sjuT+1ctGuW$6HcvLPIYc}&g_x>r%QkYYOX!rY1AaAUciL6g94EOPdzuF*n%Afv>ddxEkTa9LH09#$eKo;*U*G}t14jmD>?N! zTUnS>NK=f_!>q>YRIB(FZ-EC8d%9PZ6j+8aK>?3oB8qSHiP-g=z`kYLH~53>;273u{(<1@KzSB zpn$%E^3huVrINL#?W9X-*!(0v`mZ=yADJa;&Eo;V%uDq+zp~f@&h!o57B;={;E*V^ zFUkD*OLeM?)Cw*LTRiJH&U#~F;`Xpoi2M@RfDVpULu*1VrPRcJk?k37fvtAP)9s0i z>-Taqs@KrKJ=vu{Ryy6|-`;?j+}>^M(P)&KFKYi%8XnQ-vEb=`boi6(8#$Cx_dd_w zYNKBb)0H)z4O#(=ctmW75?6+iV`O`#e(D(Htxe)B64JiS9G;gXS<4Xq_%qmjdI%|D zrs?-DhXun|b80kO)C^fuy(PHyfszPGAI_TD^@>NbkvZw#8Phl#pBDzUe@+Acwh_(L z(S%yn+dwr>7u-?|z@cd*fY(1d*2#bkpHOoo$YI4X27o&I=sW2Tuk6{}Gi@Xo9M$k4 zQ049S^e?%3I`BxKfUeo|eh*nmX?jmsd|rLx_*sJ%AnfSxn`$>t%X`$s2A){Sld!8M z(?4T398W?(G-~)6jV+BhJ>K9@**~VlQz+t#T2N&K2m~rkLsrTTNW;dW?7cXF)HEzG zD)KKD;ngBD33#xWr(AXF{+iI1Nl_Xbn;1GUfLs5l&lM;c!-K#7LLes-V)F04<~8r1 zb4oq+ z=QE%kL&~r>8sY+MvyT@xSU$?j%S9jbbU00qHDR-2h4@dU(1D{P$MR`XktIrDkfz&m z8JD0Mm2cOT{La5C^G-5s_=g3st^3;D57`(LOnoDtvQ(S%Z-S(4PWXu`4199*4>|s2 zBhL-Ul4y4f1`~(}IBc=HOh%m?FK%wi)}AfzDbA2a~yhd2wZ*Gx4-Q zAOCL7qpsfJz(T=SBNig5f=PnGr-E5`+pr4TWv3tC8NwCXEYwzVU(N3eurVseOnvK4 zG)6uFvDyg%Y3(z--zcwj;$0!nEhcx~8nUbL=hvoI} zSv7$fTl7nsnEjllLRf3Avp#DKN*+9jXIneW_oObv&F+&I-YT83iAJgXR$?*vK%kS` z_1> z2z8Q-bbW{s-wWr?AEa8V4??H1#fx=X#I()#>`h%w>3{}COWI+wpTH{6GQj1Jc%=6I z9rqA`j)~2JZ(8D?t-m3RNbv<}Q<nRXK45H$2V!laQGlt_Smn=v9I(a4Sply zJn3nzow|lQoR!USC%6DZL=8b&+tGqgE23qV$&WDy=Mp^)^;s&m{fgix;c7Gou!5HPlB(8pUjz;5iDm_9OXUPgE0SX?*wnWS?O+LP*8NjqCF ztxQpb;ff}ZzzMwbyiKI~>MWkEx`D+9+^Ln@etiwSB ztus>pVkign%yC|Dgv3}6l~6Qkn{XX)aQy{#E<>Y(yc-+)la->h9}pisZRMh^wo;uf z7Ux7(TqJ!uK*3UwO=qjB8ZL(aJXp!7og!?h7+-I^5c`XyWM^TmFKxrIjzUt*4XY1(Act z2KQ}1Fzhe%l*L=K6Sf82lFM7ic{~67rT`z0Y(;nqa!hVHRtQ>sx3?V^NH8Xq8urC% z=C~SJf!-uVq zEpVtIhS&Sxa`JdGt&nl7JFh3sh~21|igMf&aFNDJ;SKP}!2E2NCa+Q)yT`G49_E-q zT_k{0WdKp^a%QGmp+?#xl|Y%@a~SVxpoY2PmSk2|(iU?hJPP^g6nu8 zs8OTg>=Mlb^-4ig6=tP%xX|V2`4)hSqWcr$vmq)rL(8{`S+I+c`yjpym<`9Le*`I{ zDxBp<$WG}XBa%L4oIpa>hqo8aPa%X(Hj{}Mxj!1}F2ve|RtjA!QJEO$YsFwj4ei+m zl9&<QNllO=sydR%FPop3M z5CXr0AGKmwPinEJ6L1J@nTFr?nH#lp+}$%;VDWd5PW15`Sln`G4!Lhl!>;91LkR%M z_?&}SjI&X2Xv!_NN~*A`7_Ep);>&#~#$~u-QQ-y)6}Dr1IA@7cor&fC#(UYm)Qw8k zjQ@K05}l%A;a?!S*%+W1fk>Ff0jt9J(bfces6f;h2i8)}Ny;LOH)tMk*fHMH`td)u z(2opfDFx`pp9ks0CkC#B+`E)31@Lq-xkij54zTcrdA zzrW1VF-rRDAFzNKW^*mak)BW3L|JJ@27A^uQi$QIQ-?e$-1C-@t0+dZ4?-vLp@%{= zgv9(HMFC*Q4+elj41&7c#iZaS#yY~jB3ny^h#KOT(?qaxw?xTHRMPUd9{p17+Bg!L ze!*Jm8vZRa0_aVYU=1#In^4e@+#G| zKChN_G@FsPnPok!vbd{vIud)gfcR7S^b@qn`+MT$xI+ ze186t(Man-=|n|52M(kMD0c@O^pL-9EY?hYqS}Vkor>Eecu-uXF>bkaKpK6%q4rEu zqNSN+i(%KWL`RS`bj-%D(ojCwl*qlJUA06fe~+C@N)B7v@#b^$RyA zi~@B{H2PV z_;jOvs;jn5b%~1YCp7Bma-y5n>lEs-w6VC4q?f&2OMx+w?px!W3<9e3?&;#P z@Bgh6u|}L`u3gWB&R`9OBGJkc6H~YGugCl$aC7@9CE8hTF#Yt7E}C^tg2j&ygRaH= zs-C@A4ns2;=iGL2-B|5olu@1wo)GW%S?HXWrR(lOcH27dHj|4n4kP%eda|7`W7;(+ zbe?w`rYGI1-a;@MYO9q@E#84$bE41=Zpas2fM-+o0O=xA0nt zjN0Ashp^^opT8v2LSyk9Ry0r+ob-Tlo@AEVW~OgYOFw4C#L4gopQ;5PI*JiCLIW52iI3ZAOLn>g!?>Mx*_ zxK&e8^;@W#7GCWH3M_JV7OWJK0KHNwGpDmw*g%b^whMs=J8hK`t+aR{!B#VWjk1F` z62~`}om5ct3p^rNUF#t|A^Ot!riO@ta6y@kf3Cx|P1K8PX}T7yj^nIT_mxQ#YgUI} zOhq=%m3*xoN8Vly#0fP9A*uAlyy?XQ8L%9^f$tZdXf;!GblrG&0cQVPr>2v?vdGXh9?jAt>`1TH*QEp=#spEl&Kb>lheyqxtOs;2Z93Fbwe+M-4}-+hJ<@RTiWv6Zohh z_H>JV4gx2j!#n5tp-Bs2n~A>0Y>Rff(%tc4V__SQyB1BM{}pevKn*+i;k)S}-LltD znjjsmyCc9(c$wYh_wJJH=VQ!LyNN^52t%f@RdFEk7LM9r+hMy}YS+v|$_0HARwQ{s zOy7wrUg}F-d{;D90~qM4xt#t`GFrT6I0->OEoP(PyVF{nbJ0gC@4WnGqJ)sr4Sdn+ zeeHngpes(=10iPs1w&iF)HZNu1h~yfj9O34Fsa&QJCjc8$b&+gi3_9n zy$`3dvD4RcEn(?o&SYBtPQ;#*n&`*qVEJiw99EqmeXpgP?%<1d_H{wds^iECz0JHP zANjjH*op#&w)2k7<;_T`wp?8kK1WKuCFKWHi@ZHc#5ND26;RjJl<=w&d@+-w{oRFY zF_heC_(C^#?4OZaBi^#oIULYlxXpnQnT{Ogoy5xbwQ4#Ew&vI!NeW}e8P|ITT|hz* z?|NL3YccQr8U8+oto3dfkURA%m5PU0BgPE9U9zFH zd2>$XMsPn(QznalQ@$rL8|8aw86zmaA(j+KKzNk|A60$2$4|DVs;yLnl;`!U;loWBQzVCiJA zBz!jLsL^JNLFu)2z#ih0L6zll$c@2tF5Gb_>$m>$4y$#q0VQB@G`zRfzxFDymz|vX z39A%x3{+3l2k8Y|c)iz88SwCr)a_q{Fi$*N-Gq!@jW#45FFlt$)$`{i@6Eqn?3M0u z3ww0x<|T9U?0i6zk&rIgkV%edSBR={+;wCAoibA~MPb-{fz zJq>C*bmLm_<~@F8sTrQ>D6#9Mbz>0M?7ri#mQld*S{y!9UXdsMxX0$V?alH_!WAo z@`sWlD8Qs)wKq^in8hB*xy8DKqD}MyFEl!VcW19Q-_rLBx}wh2T}grWJBtC~%Y5ri zc~DTu=b7gwCp`SfPm7$c{XoOL>EW{x0iV--=bKlVSIl|DU^A!yAIKIogCLYbp>R^o zG;38(FE_|L!2-x5M!)5m^YC#47HU+^t`OkVJkzP$5XTBt(vnwu)@yCm^tH4kBxz)Y-I1oF@}Y@lg(9Ed2XkeTi8E0SeoVr95c{E>Sw~JZT%(7JY%iqCC4NS|J{* zixYgzzKg*g!gaXFZxFLrBXo{k$Hd7S5(Qf4U$tT|?-r z@L-@9sHT(!9n^zgDEL!n^va?AZ<|iTsiWp)G$;V+WVQ6wr}fp^V8Lyq5Tf3&u%Dh1 zKJ>u(b-J`p%`GYt8vY`1p!?Z@;6Nl}X|C(!A25qkRkvz;<<*6G`z_G*)h*;Ra}>U< zzV~$COJan<`$uN(r6x29#Dzo}utd>e6=BxNxn+Q(C{Rean7i+?r=xuJMMe%H2$*bma_d`cpSu!@>1?-YZAMc_#~ zM=Fe)d?u=sBsO%4m-26pGw8hA2hZF64!aJ1W`Ai`f+U)>lP531$_avmmw)+h9EU^1 zB7$|JY0TTKE=OI*%7k*FR0zG5{0c83dJOhIdmhzJ)GK!B5Z+o6k2;9gIn@O?f77%O zd?W}|Im>9H@cnKfIVNyrxY{$GgSC62Bc*l4psvXCg!k5ihjq%Jz;8g%IQwhszu>eQ zODEr*enyjAu>R%mZvrKihzaO(D{uAAI-5>hZVd107#+k;ns&n(0Zi^vZgZ z@Wjhi7k_EqE4BeHOyD8!DNaVa8S`sCb$WO%Z|EB7`wZ}0G0w_p!QxfqiMtTr_C4<- zM;F{?x|BB24lA|`RW}dAe~k$!2J}IcXEQ;XnC! z7?Wy^ewB>`;*B8x3<}G>oHKddfFFx+k);g!?3Tr3@-9hUU(f8h2^8)LL_hi(=|N)g|Z07c;bGmD5wx>7yC)92{dx9*zT|WxW2j!YXTYg_4Fu zB$FDDQqKga}yQ!iE#`CnMk~JFfdq6m~C$tWH z{J;UkWjQ-6CIm!O77he8(Yf!imZdEF#x%&QFtFTe({9)&UJIJh3(Y_D2v7jHS+1#9*GM>iue8yr zZmTQ&ZFn_3dcWFJTANP*nH*2o>NV_33O&PSJ}uYbiac7PHQ_F{@~~f*DV*({!17>%Q;R~=>X14*e$_uw{X+F~g*2Cj+2&NHo12cdF zVRq1TW@-l;FQg%u8^0T9u=qLnJ+T2N{f1(TWE}jkw5fwLFZqjxM2H&*{P4Lzf!+Y9jN^ylSI(aW_J-XJmXum%riKw!nnx!dH1$+vdnS3 zSZI27^>QKo6fjoC`@$&1`s(HwvSXf%EyPaW`i8rTndE{L;1u&R%9-f?GFJ>C@)a`T zeCQo&;)>QO%dHJJWR2`^NyVC7$OgaB*w^(5rQJe#VPJ45-K_9&mvQ$SUEt0B_kxN- zDGYQ$X46`F*|`!PF+(>c%u{~Z`vO6vuG)%AsmUvvU|Yk@vSk0|Gb10KDaVC~W32bgQxm3-7{4g%g7%$_8TbSnExaj;d#{NT{?^^+~!WB+ss6`*m# z>;xtw$KEr;~#3jIDpxFL`| z`wcmOt%-q z0Y8+$A(aeyC_rc0=9G;1HvOwmFuK9~z|rON0+OxkJ!UjNaYh5AQ`^LR+%Ix<6aJLv07%#t+CF4(Zy#Tn z3=!O`B%8i;Y(~4#&46z98$KP|C2@mK7@H%BeNE%8UVGh1uLOia3ZKT!K=bD~SG&P0 zHUlt*A5l~)1SE#GWVpA(GrNn6iryp0Kgctu{d{(%URokZ5e4gO9?KKSW)WSvME{hE z7I2fW$bONDKQ5v8^cYR1y3Qj!&Ugrcz>>cE;W1+zhYNw&l}(09o3$?`kv1d(SC)nz z_d;wU`ZrS8M&S+!Q82q_$<2ZE+GyVw@*Q1^c5S0jdcDoeW=}gO{fN&SI|yIEI(B50 z^>_pOX-*SREVh8vN&emDl!Uj|0b47OB{DFgaKnyht~J4S)T-qtJTNg$G_0=!(&gGG zQhzAr+qh+g$w(H~gS>^NbfRXAh`+u6F!VG^Um+3KZ)_vs^KmwJ%4OFlIg>6niEM(p$_e0vvVh}vUOn%9wS34d9wRq5{ne}E?iP6lJ-eUU zRxe3GAWIWV8s=XSl9Lwy*065#1$&*MAeM=KrzNcQzZt~BUbE45mgKya5uk+GKmerb zus-8}d&5XYxd5$QYG{uQty|#^ZwwQa9<=WyM7g{iA!Kq}bzM0j8M>myxeQ=J)Yq}4lQRrUV>Pe8E0z`y}mx(XH)ww$b`oDeo>GBO={RBo4t z84NGB9y3CWX%c@{V*vCa@k*ej>?Thce5l=a!%Mcm_-=~+)4x6jeALl}&gu!fTOHn^ zVvrn4AHxH^$DN$j5bkjd5{9SF(>{rUOf+hic6TD$z=^oPAlqNR%p0$CBYMHSYqVhx)yc%BV;`+zg5joqBr6G^kT#z z)E`AH31xp*oYtu2&o0?tf!8m_6&n%{Ay|E0>br{|qzY7)5(3>{LD>8k559W1ZPm4( z#~28*Z9n^GRKh+X8&<`7<2f?y4`6*QQ@M65g(bZ7h!!F#uz0)4M;~O(+GE;F- zfegdqi!2SC{XtN^+j*MF&>ZsQ5X5I3#)4c&=)^jg^9qMWhXbD-<0(&(Z^vaC$Ci-} zsn>fTK!l?&MwS!OtY&nZT=a{LaXqYYnLa?U6zZ^O!32GCU_#fy_XS%QQ==dD=A=u9l42*7uCYQr9v1&JT z)qt-gmhCZW&evM07cMC&@j-?RCiM((Lziqvau|;!kHBY}diuWn8Kg%WoHqa+To~9v zXj^*?ryn|7kzvpW;xNE4B;c6h+3BtHjr%Vj{;)niHa}W6C$b?7vjEw* zT5O``3MoKdnWQZ2~OC@2BC4_ciuk^mS^XS3DzVd19h|4zMcx ziZ-9%mRVKsNBFX?W|ZvvNhxfZqJfk0ZWi*h;*&LA8h%vVJ8H-Z&HQZ+tc`+{x(ov93o1bULgz%j2dQnr5p z>l}o*CFg`wEMWd$#u~U%tZP1mZ4e3XH=H~rLpcMCAvdpe`)j}Ge$ATcpgEu_{q^#= z+i#0bNGOtyP9fn!V0lfMCSE*$ICxb2Tv6Qp9H^lDa8!!*0hlgueRPt7bcJbfLLgr} zdidz8>#YzB z>mBHsuOt{Z_#O^^z^!w7b7{hM+3KhsK#F$ep|A~Kqj5cr83ydc5%VbW^&)ecQIK>sFJ?M!NdniZe<1{2h|~Z z;lCAX?Lo^lo&$#%bTEzx!x20Q0&%5xb6;|6dTlb0@6(5LV(BR4JV5_`pz}P~r}O}3 z?uCRLIf5ZE444&eFnR}!B_@^ud?p}(Q2rm?n=`jgT8`)`YxTjO9NV5-rszTKUyEf4 z@9(YuYY65V&{d8Ia)EQ_z&_;1 z>hX7t-~>6WcGR&@60KK8u*0(gvJbFd^omJJrnO7G{at-h9f2|meEtFtVes=C42lLc zJVcTF7tAJ)D`VVrD=aXngp{uYHa5c<3 zsfow=*u{sLNqh^Gs;VOm1=bxo@g_XijpA3ufXVW1rfKZKlOyDGf767L?)TLZEJ}U6 z7{bg$_qs*8XTx!GIYX~pm(I~{`XNk7gmz;7mK4}i@;dj_NTXRf;RZg4xmNgDJ*?&Y z!|Qn2CL=W8T*68H1IU^%WM=S0yf9jgpv#zxLmDUg3`snBltB;t4KM?DqaW?z26Lg8 z299uYcr)Bjg+YZU)B2E59q1I+r+=>J@5Xa9ur=gvB?uUdaHh09$Q<~;02%OXUNuMc z)JUwt^)AZ8X?@ZR-vY4Ea8@oh@#p459^M?!;i!FIZUPL@qfPw(4>wEb+Xg<)9oyhO zZMc2nOE`$u-D`Qh^~AYHLrQ)7nYZ6xz|cnEB>VBE4qE}m?UW70GY8v6aC(C9OkoR7KRs(&_Jrg2_KPk~bGJAUjZ2Y^ zk~;}FcZL96Zd=d5V7bube#)wmg!&zh*aJc#(@Mie5^52=aLZ|F!ut$=Kz;3`Ch6&P zc}Afg)f_CM&`UHv5HGmZ0uBJz1GIrIiH~~EU=9pi zkbr);-6kdO_)!$cMLK7q50pNmubJc@y#N8c-+>4rkOC~6MVF%^kAfB}HZ?opSmP58 z$V$dZlCs?rEQx0zIdy$)10!p;{7WXZ2+c65_`J%v>g6xAT^m!Szi*>{m^)QVV8A-D zD(?SiV5o~}Or69?LYF8C#m0f=gB7HW-Ca%7ZwU`L1kLGP&f&j8Hp+z_@R+NdUE_b_ zP<of)#6du3A9?z4)a=J^khPwj0jCvHFH(5Bv-O`(N5+sPa|`jA`s4qd)buYu z;FhR|$O9pWZ=$G+Q-hF^%&AVfmKccRr6uDtBL_ zJ%X2VfpNZ}3ObO+6A)DtzwX&!_L5KU(}Nh;4*y0^WK_O1abaAx5Epii8qVuuot60} z)ht>P7N%%nl6e8CB}h)a2esGY1ahB~vC-E}bbc%D2WKk43tk$VD?C*YDWh2m01#>q?V6c(B-GDrODj2q9gd zs-ZC>CT5k46`i$ojqB(cp~xMOhRkL{)ON(|oKWJ-!#)v<%r7zvdUNMmDw{KLoFIzM zd`Y?Dpoc*_k8`vTo5%aVY(mmg^DR)$0z)hyXmJMq9fUdZ&tg~pV8mfn)zL$Vlo}g; zo~Y^ePk(vw%*0jWEJ}{ZzBVPU#{6FO!K&{c*dPcEkO_x>QCA2^ zc6RA-0!bxf*UG&95Twbbk$-W!bE+@BB5sz|F8Z8@g!j@KlASpVxi~d z@t5sC5&lgB#%he?Eazv4gj^jv>0?NVW6%W$8V7-v3;)QdI0)kQ#( z6k0)v`9cNeSP#RM6Q~)Kjhs=ul0FVUCg5y2rC^amAPRGbY(l#+q=H#;0PlAX5?aU{ z?4~O(%#^5|FbI>GtWev%mJicdUDYp$aS)6Kr1^qC-77$)QS4O(qi@jmiZO_qNF(5H%r;a|?M zVbBhAqLfzH5O5B9VzZ&%JDd2h)adI85>d6d**Ke@8eml+ac1Kd1|~j`#p&tJ`py7tIYJW3F%;tytXd)1U6AoOFFlM3KUyN7IG?3E8S>3Q$Fq}pxQOV? z$MNGhPR_XDZR8rmR^+{rq>G6pFb=Q_2tOoI$;Tds#i-g@gz7dV3(imF@c6JkUve*FU z6DQhwqDO>H?m5HSGQ%1fMiTlVFWNC4I8X=c-oV{HMlE1Mlkp{XKK5GWyd)Y*1J(FrOy&#?EDTP&|bEgp`!!K=t7=;E`DGSBs7b~6x=`^#_{lEmH z8$j=2r}xuSh<12I?Tj=m;&6#E5w?&LXQpj#-?{Vf&KKy-f`N>-?`HtZ*GF5onSvF9 zQbAA+$esWa-%Kfo6DATOlQm|NMnXA5n74Gva_)2-=KL|VSUwNVJHjMN3F5FQa70$B z(d48e#7OAlLJAJRSi_7l8i3Jge$24klW*jz*c{&)&? zGJwb-t|pbRo9NFvkno&+2a%FIW(ch)r?_1Vj(YYy!ib<5#XN!;4^K5twk@>hXX8td z9Z@@=eb9`LC6R_~KM-|(7My%`Y8Tf7qW>pN=wdaNeU!&>tU#)WPBSiOl;J~y$UxGy z(G3CMN-}C~E@71Ksp9dX4xv`^@qi3~cW!GjQJ*vnZPzBW**XAzs9NaK;kdZcPt~xR zcTMT$@DH>DK}L1POkz+V3w?|*c6+S|4>uC|z%3Zrm#fjFb>CxAn0Lr`pH<~aUHk}{ zd$VfQ%NnC=!p?@X3ngTw&l)NuRAS|7u_~QTW1N@fz5TQD#DR#wx$^vwmU9-T=yHnA^L%FF{L<{DpQ>JN>Z0p{1K-wX9RT#uxiok z7FblmNLdvJk11`q>6YDn0TaaFP>L|SO_AMbo=nAwkyK~)X&im!KvA_uarux}tYEw0 zTRxy`J{vJkw)*Ha;H5XBgtLu|C=ogv=VWy{L5{1NFs$L)9H(v5TAkcN4=!m7Ug!kQ zb?3J62ind(W&}CY3Kz-HSQ=OdqN*oos#dB}xFdvm#K==DuZKk)^~O`Gx5r3R9Vd8`8`|L4@L<%22r`=+ckj+5 z;k8+yxIiEb_$5PVoGA2?8|g(TOw=Pqw{*LGH&F9 z+^|8wp{{*%Y0!bgv9YLiv@ke+zHrSCAvv(s6kF>OFhv*Agu$|j1>zQPe4(3wzV)NR zs_Ar?>6syrpDV|~m&=(+%ke)+X3iNKIO%(=-t`1hQ1#8ewhn+CHP#dITE~<*L7{;q zKoG9S0!s3vn`~G*l^r14x<|?Ba~9Q|HyVY^8A0$1mVIG@bB+tTR-XWf3$iurgL1ecyJQa{@%ndQ z>!B2GBrY_Ia6K(pPuAkx=nYd7RP)p@?HlP(jA}j9vkj7srx9 zKllW-=Z_C5!Ydj`bJhzGh_P|lKn%uJS(r{XnYlqPY))bR zMBTtQIsO77ZGgn=qM3IXVj2QU^thxQPAS3RKrL5oyan?%WiqdX3Bw-YL%D#OsFyTq zXdvxpv5UZgS~_dEhqhAB^4;C^&F@r>#TiLP;R&(ieLg$93@{G2Z(n;K*m3V=tlA92 zSz!d0+t7i!KyjA#L2^78$e-I(qsYcYBS?+Rt;2h>7Id>9EMJpbu%(L04Koq>X<~|PwH?qSxlFo{*7Wh_v^rk&`o;RHq+@`T3C(@W zY}TFY`F{|`bqK~{+_=|Dj!p3qdx{2Vs00?f8=L$Ps@yrDNkp{%Wa6u)NpY8ZEQ&;& zNe)tay(M?~EaCGsW5H_H>X5w5nH+?<)C+9cuvTMA;1WN$M&H}ZiG_O$8>B$v z6W!8ibTnFeF4iYQNiSv?a)aSB2tWxAoayDt*OVYWqeNPctMt8qm7N^@Kck}e1k;CJ znV^P%Zok~yx>eM8j~ek_o_)$=D;VcmC#ahOi#m*mh!$}eZRhem1QyD@j2oW~Af#Ve z)u^+IdRB1eSXLm0v9sdH&{jV!)u^@V*2h6yj)lZhx3C7PHA#NUBJ5|g+>p6xDA%<( z97Xq_Ad{a>3RUk>h%!rcY8sqLkt6uVfVw)6N028yrpgd2_B+ueu(L0~trE6E4Tqzx0bJKAd z$O>cVPT-WVRW(qPc5EKQgkKQsBQD-Py+1JUJEtW{*Q8G`tmVftbM{8MF$wrTc&4SY zOcXr{VIkpO!f-R$^|XUVkVeom8jQokqo}hiOPWrde&@Log`%urN-Z+-HJ0tqDjrgE zlN#svRHBj36d&5Ss93UIuVbny8{H`pG5ZM}2ITB&p*@-GrTVu@f>vlz?C7e`eOC*h zB^!mX@TV~h736pKBQa~QW>&H~g3L?Hbg;O&;v~MFuJcK=jH3zXn6Irl!!*{FoP#XD z-p(-gTVW+y$ryv{^>B@hASf_-#e+2LKC7BYEH?ckX{VV-_Wjm|6hI9u!~Vd+aQyVa zPKy9RzN{N;vw=^la^9+*uW?keN^TU2tZ2Nl+yaYhtxf#F=Z3DnFQ5k$kYDvxyA5}V zQb(dx*Jtsf*Va($_nz=@)nrR93JtLnfOT>V#AduTw-wV*)-Au|`D}3p$o(ExUZ986 z)!`kL@K+Rez5oYCNn0Tm&O4@82c&6DCCX}G{t|)om=XeFrdd$#bXVZYA&#hvMclfk z^1Ly`^txniWyXk}eSHTUVu_;j)FuU}4-ogNny%R-zLIYe4#?{G5}Sk&2S~|mnq;%u zVsYR#613TH%ptVJMFP71Z5t_?JVzF*sNX~AfyWc4L_9{0{aT~tiO`PJ+;eu-5NCT0 zO})L8a@;jkd0k4~yahtqGpem}3?|$P5lv=4%Cdk%tOacYlDYgMNv%B$BW<0cqvZu% zUElV_JIU1zAufe3u4Vt?t%gQ+e1)ytE0qU|d{1ULY3eE&<>l8(oljSOAECIx5?0`P z!#}zspg}bF!~b1CtSX6<0;>jN0=?zxU-(h zXPR`25!ngV+a}0+eVbtTYOlZ6YxqCefjYBV`JuQ*3%p~U7AT738V%^$JB=`_T+6z- zN-lB#r|s|SUVu<7Jl=t0OKD;>@#+HX+5VV`l|s7%*8BGDqB3~LHE3dmR^Apt*m93q zlMig^ziY>`7UPr}%VRSujP?Xmr5i2v$_uVpfF04`?={sW^}&MPToiD&x;Mmdic zonTl)ddQI75Ra<3axn@*8v-(Zr1eKSpLC-~=qs_8ps27hJiQ5|GZFm1}x<5Bzzdah|XwGDgsj^4p$R-os2 zXSSXWI(+y;IQ2oN?F`=H)oh*Y#5+muY_{B<*}tRD)6RA}YxhG*QuRW&#P3dACyNpf zw7-Uh#tc9sp3SpSJ{IsW)@Mn@LSyMHDE$@bUJ3;!*=^eP!23!D=a0hAXXTAByjNe1 z@}*V2h1{y?xkzXify~`30gW4EI@}o`e-AK~bKqY6Ar!nbS<6C(f92S_-PX+XOun<% zgo&*VDWB`l0D=Rjt=lXWr_)X76@n~9=9BWV8x%E-%`}GE)6K z1LFWF6S@#XJcIp-!g4GU*GjR5065Im2c8+yAiRTR!=-eFxueb*B#mv)8`vn$E{uEp zm-7ieYa#*Yj06L+VKHd2Gp9x(qjyOS;KX@RQI&zW&@|(w)p{SLSm*1#Vw(cMmC-wG zK&UfiQKNUM!TM1u z7x*06S@Lj-f^`#!c6J`6JwxG0)_Erw>pHJoCqrDcT8G6j+2{1z@WC}S-TX@}IS-`60Xe*6tS`1WKPMH_l)3oN@!WZ&Q{I)zWKpz79XW)R-oi7R5 z5aaI#rj(O?0K&NjoN->p7t@xG27cp#OK9kH4l6~{ z6j0T7Z*GonJ|2I)Iez>#jgq3fc5hPjJtSG)+oat0 zo>i0-!}01S^?&xhg3Na7bV1>Dm-?D4%+S51d70zZ$Hv`0*(`S)r~_;oqv^){-|2Li zPfgR@C0m1H2!KuC^w4XuP=VR(vVi+b7jIUulWHajmy z+vw>DGXp$b5P^I-1refYUNe+d7b22J+vgv4jKdt=p1qg4j4bH{y7y4IB&FEkz1vRKJM&?FyUTa`Psa|pQKz9S}#A-@$R70 zQf6|&<&R~dw9xB#xu_w{r8=Tq1i;QIktqZtkfLpPkIKVyN*_gT6trEfq)T6c=#e~K z&=cK$tir@yM$TY+j5t=%1%Q&n>?RaRAy9L{85Cv?JpwtU)cJobvA{R%)r&cj`dnK? zw%}Bt!LoC&w0Vqw$qxbhl^MaG1Uife9lpR>M5Hia2+WB?KkjQ-q-u%aC1!6Rj1eq< z-$}=~#CViV*fE<@-Dx~@+k;JxkKi6{oz=hm^qpCLfWfLbOL-D@hpR=yTu^+(p-Jtt zJwgWkgFXdr)v!Bwzbo)ro8xY4r*utZ^GliTO ztIxpYpUcDlz?#;Gzar({?{D9`?;MiMcZ$tlwublZ95c_-js9;6XULz zuG8D?Jv~X)eRTvBE#EYB&5lN|ASF6{ldkz)cxo!qYVxI?j+>aTS!G=j@Fu-xZ;SMqr!-yGqOq4vNZ(QA?zP?vy(qX2R} zbWo_ITxh5n^WZC}{cc{9Cy7;LPEC9d7==6;Ph!Kfcr>kN9Ee6-=)TzvlNugD5tmC5 z!=xQf10dm_3GE0_9h@^n!>tBvglrbbzv&3{xZVAy@LfDpGQFRTLEal;=Sr++*yC#N zP|4hUjE=moNAMsBZuC#E%YD&y2sHUULCZXQ{^L*2pFVm0{28v?_V6KEcJM?jrQ)$p zKVWA znVYkk=8K?U!h6*W&V=$CG*ABk0!H)Q_!4Gm#MEmaYXHt^_U>mmhv?BJdSG5oD?CuY z2m4Aj`w3&&!%xBJUsb;Wx%?#0h7X*BU^9jE=t|15bJnaAE#vKA+V5yD0K5OP5eFD- zO+=-^;ZCf$H`Hg{tT$a+nVqNR5qTw|9%&@i7nV~_O9wij0au&}b`eH3w z^urz{`U!DX9Wa5&n2$UrEDI2c{X>}8cen2g?U@ZPBD*8uhiVNzgL0*r3YOxrl4rFdR%T?T9Jpq*0u?7$GF!X(%PC3G5 zV)%>Jp$_0_C@UGgOIJsG7I2r4m#6>%=O!`;;yxaWD;>R|nwgO|-pA+#VrRJXbaE1` zEV_av(OEN#x)yT@ESH1urOq)=-(>WpqK6&@tq=;l!$BhR3!(Ri9iyu~+SuOWW*Tkh zkmTR@)wpn4U{pew@k=>yfuv4OU_mGv6-HJe3YRrpgdK2sR&bAC(x`#%An!p9&J7t> zRQuZXE3&#BF9LNn_M?J~DP!a` zM-DolAS@+`9xb@|1z?UCFnmT5-m~gG(88n%f^@3)Ka~xYm4F(s6SM)1ws!ap_yZj~ zms$YJ(Mf%BGF8yC63?HL;){Ftx9`xv&KL#F*bDnZkF9%76ZMrW=J07pQ$U$f>&}7coMqBPLOl75(pJ^; zJheDeWzRp4vl-^k>zF(?+*|Rf;oMd7K(CI_1^(%uRm?>11&s0EGQ1RSID>GENo)uq zlER@m%R>`+&-Kb0%^HO5AL_*m5K)&CF|@pxeFI#W7mHdWrp(Q~252dchzeE(jm-k4 z3mXcpO;{a+MGeqZ$zu1mK~PlS0qCe1zHL}VB4iwrv}=L}59+hz8qJB&p23H@pJ{<+ zX`n@mK#cnx>T?elGQjTkzjb$#ABap&*ZWApt6g+qy}MWve0X7&h_1!KvSvHtkw+>C zNR=SyqbrZzWpHKL1RD(c02V~m?ZWm>a1$O$H~qIvReEk%>IJbB05;#d@V|c89OmE9 zsV4fSU|xS)9u{}DwzrEJwmhwZ2r#Rkegmw3`|jO)cfp7dEwjSiU4T#eM3iPaA)H?O z7+NYy3VUD6p-Yla7`kCvinzoieT~G__BPI}DHBtqgS$crMRzNpy-EDYc1$WE*Zto8 zSQJ}wgX&k33#Hdaz>aJl>=kz~>4*=v@RS|HZ#A$1++-X`2YO7lUCu&8_;lk zPSuU%RBK$42Rh*HhX)cH^1+{Y2Lg#xb^1a)ti0&`=I~jCR-!sga`a=+?>)2`%gCh ze+T`|6Az`GKk@TYYG904121gDzLVVuiW;)qXdSRKRs2L?RiP>K$$Y@A{;{*0jXgN zAKQRWkhOU{cXBEg(TQkuxGoJprpg+|&|!~~9W z;bJ;Lfnao1OlUa)-)z2sItW?@l`0Q{Z~L^M;0m2MK6pSXW+Zj9026m4*!6vn1`lLF z;Z@L5oeP#tJRz>YF>{D!O_mD^wE=tG%mt%ki_-j30Om1(inNICy?A_q%h&O0-`_+W-k$X3AGecMH3+aQSR5Vmoy;88;M!7_3!U|PoT3{5THE-yR@oa{}W zyqGk@^ZLBkPgI#~jofAua)}`_W00RmQvEfyO%}_rFL+qwakxum~qasbCVBXPiIOInmgURUt8K7@@XD5 z>@I{kgsIlEdDU_=p)+iXE5uPfiV2yl3$3G|D=G0PmgFe%Or!|2sp;&*Ph#y3G=N!I z3JdTdPA=HU(QSBGE-uhK?X^LNHJr%v4PjH-DBc`88!cQ9zG_B1Oag8Pw{3-tKsa?L z&VMInk_^;=ejOmWCrOk$kn_67@+?}h#J5xXd4vNK7 zC_BOEgS>oYXEEKRf^(R%T~PrnY|`l;BR0vD+g1}*?AKbVXsd-gIcO_YUT0o%L6k^l zXZXNaa(9XS4-n453qBnGv~`W9K&3dhs)Hd$*28@&&g1;Xdk)k1($ZuJ4r?nn~zGn zoA*=1uZciZx(U;LIN!pS#bGIcc>~P|ts`*frmeoQBqGXal4a*ma>t!VDFE~h648cr5m=O6-vxjlT zQ_OAV(HPV^xfulPCwiD;C_8yL!1ZTgt(jO0en(oNN@%vU+NS{oW5vQ?etkRGCrEQCzU8cmXcc;#Vjx^{dj!SRiu-0zjQ=j?Q)#@9`b31Eq;v zaAk;=xvw5RNWDv0J~RZv2msdy3H?NyJf{Y57ggqXIPQYO6da9hU0NvB*OT)nq~191 zHNYX!fGz}sD5m-WlcJ32%K1!X2^kK=1}IG(lJ&<_@lkhOh*~fdMU~4VHc5e+eldnQ z!KG#vA5|#DWtf%=`<4-d7gQI5^m&Yet1kmkB~80$&R#Xo(W#C{wA7dz*?eodC71g? zU(drJtSZykx$Z%o7N}(vTFxTE-$yf<%mZUaabO5fnJ~=-sROQ*AM32ApaxR33YUdO z+ZEvx>*`gDNM{Wu)>DrXGZ;rp`SbP68AYH(CiG+75?H*F<*$8~z(CvMNIffmf)u@x$n`o)#_2 z4N(S%(suizJK@DtnT)LD2rPzPV0ICv(&dX7A}S>jRRXvTDH^~y z=lI+iRepz>Q4l8!C-~-@XPYOS4q&_d+N)q_9STrB94%y&_drnuSz zF(8)b{vOJ1fi4Sr;HJ#K{b)oZGOkaE77A15N5s8E8HL;xDOIARfOydUu%Rm zBTt6djv$2Gzn1`36C0-EX100wsW{(2J#j2h3FO2CrJCy0 zYGL?w;_TZl1uIQ(U7BtS;t)-x(xT1DNGq%^S`xiDT!onwXi4-E=pjS=-BQ1u1yoXb zsB+9pfC1~`@HV<@-~_i?45SFb{eIC_KwcLJ=P%~GY9nZ`d%ynGl?&)|;5NXc6g%=u zVSWsZkwEFDMskYW#mA#mZ`?l_-bZ9lz^twCmmb+OoF#rCREk@1ksK2)LQe@$aRTBl z-kdf!Grx-iQ+SCIFq+?->Z{`IVeJqN`#In}+Sq#$NfD2WIDkwAVdKTn#=#nY)_fZV z+cNE}qR5kjB*dRSk`fcr;s_5gl0()Qj0|M%NGeJ4`RIXUSx-T72|NKzOD(bbj25V4 z%7L`&K42@YLlmx)&6X>|D7*V~ZR1v$xNAILJ`qNO2D5pOib;r&tUO_gx5>_B6e}C$ z1dL?k%k3@&YCH0Sn-}Q_BYxtwO1bIiRPB4VPNJX29(>Nejzj|mt(ka6og60?|1wHe z_%s81ta=>D5}zhN*C=Pfrhp7FUy{-Y&w~Ou=a?It4$O(4);OPR?>2q?HY&9Q-;2NY zo!!l&@n8Ml>^jGXsc-)-(akRx^U{cn|7Q; zTJ;?0a{+iD*-Vy)AV|2zIJ7xY{*Pk6SMRBez2*SXy-w_*AWFq{x*n9IW7?z+@8iQM zvz!`W!|C(C!uVnogVJ4FZ;4ldK`uTf?N4B7T9&cWN#HfHj*Ih7Z-fjcQv8USa4}n{ zObg0L_xJ;j12!5p5=0}@H7f&pk`=X(3e!HvNGyn>nw0!H_^wVQs2u}{0SWBr9VY%C zvj-IZKj8f5wY#U!QT#!djwRnHWn1?cRxeImPh@nG`BwL4QsLQoKmSGc8uZ z@Y79+45gd~fa^2nR1lo4<(y*t^=OiIa+B>=@^lU+n#Li<)?y`^ou~^be?6BIkGhXX zxmKR{=7Hq1!!nVF;^P_WWOdrgV}D2&h~`OH&UhMvSzG0VBZC!kBC4l|RnWNO4JO+u zt#lF|Kb}2&EPy$l3GlXPvQ(dsDYhjK>4v*E;AoN;g32RYu0lj{1o0FYtL2@*K zwopa}AESuXzC2^hH%w}S1O$YhhGn(;$b_8KbCc!f)@z5f4z_^WSJvPt$TLW{x}w0^>T(_Wxp`!?^+wW7@$LM(Ni965Rb@z2o&57gEsznLPM|x- zO5k3k;RjOxno58EJY<#GU6ypYmU2eg!C3abiTGv zsuL%7Ncx{$A+}hPMMfHlxW={Z6)QRxbb&mLlvsRxOpsT2JWrRDv42?r<_xgAKSK~j ziDqz$r8-C7OM?zq1LuUGLVzg*{1x&8Fpg*1AkyI`SyT}@X!Fk2)}zg>uQuQQ1N#U8 z!;mVsDRY1clI{#ttcSmMo){ zI6EH%y6cj1|F#o##w=aAl~qD_4+RIX%P02tI6jzUNu5>eYYTvINAp_Q#4RVtkpJ4d zfdsZtPY>2$N1uEU;*__XgIPn%>Rkv?4Vcgnsj^pCHUjqL_zG;Z zi+Xm&!Z75uR_1G9o^W3uU*Yr46WBcnw$l58F?gkz##gBQP$%dw%p<=VHuGaE({ZI3rdJG8bOq**^DFfsT|tn|>s_A4&cE0l-R6qYi_p}-Z6NJLNjpxSB>M43 zcuv>_!u^a=pn{%Y;M5;97ueJ%&&7|4es~@59l!W3DAQn^wHU!Lb_I|1r;g+DH_4bT zp)3srx>FY69Mhix31$lNN3U5PopOyN=rZHYnmn9ZK@j_EI7&!`(7q>`cOu+39laj9 z0dflseDf*ttWj&BFoGh)$FSlQD~+In5AfP=zrjkc47-4JZabS{x{gXlROW-v0n*iz z4^9cG8s;CBi9$>|fc5>=MY`6iz%#M}MEW^RmV7}BEJE#^9ORVu=&$M8=2S_Ufdm<1 zbR_WKx9Z%`SZ&I2V+9p~icndq4y!5F9OxljoB%~&e|+MTl_c9bPZgg2T|%oNnxSCM zIT&uJ^axuz_QO)BK^?7pK!_Wmldho6w!ciQiv+>gdEHA&V_sd_}JD*iiaVMk$Mo7HEEOpvLm~6_cvj6t>?SuA!7yYagC__W%mSn`R zDqAbVX0Y$Kcik8NnCO;Z5e$EZt$ZdkmYB{pDtSdEPW|KJUy7AQryN7|*&OPBO(y5e zNBz(}4m z{}`}k90KeR!0)OiON4?ULxhDg2o0omIO$~C_}sIO6v*V;4JL80xUb9(M% zXL(;<--8q&Ia9j0@tqTz3dgrL7Q32XGqGHp*3;6VlTNN0vZ2Blic(}5e=Fz1Un0RF zyBZW_>kJG*zstoMk<`3tyTATw-u?C0ZY;8_oF_k?!i5JPJ9CD9X;WEN;CdurF_}HR zKvTREf8neFHP@>&7Z+nt!Mn|IW#dD9p&6q!zgqN@3Dz%7*2f{C2%v)*+6VeU$RBJD z10$+>*icdYCk9cqdzM0qDpI!yM%rfa?h{pv zh;**xlqSOs6A@j|O>$jGUKX*s11IbwxW7i;me&pb8Z2ZN%+_5DJ~dZ*jEKW-Nw{lL zM6@tgzXd)iv4HasbUDNF__H*cP!A1Bf~7e|6>=n$y&OxIbilJ3Vw*s3I>R~#%tXkl zn(Mu#V*W7Z4)^nc-SQRJP|L)I+2IfpzDZL?1)VA2JNH=wl_Y#9n!se7AceR+XSnGAm8lW)T)RR=<;ahb?0T_9 zAs4jjw6-e3?loebI5a@p2F@%vI7}u0(*z}pC*u7zjmU%1O7|!_jRJX!RVB?>z>jvb zt=(>KE(9BTPfA+8YYtKgl8xcYMT#vaHohLz-uB>&x6?{#d+=kH=MrJI@LWT^~FR@rTG-=QRkS6^e$*B%gmYrj#5-W*) zpE_?c478;10;E-8En_O9m7f_E9R)dkQvK&I@M5OznE|PD>emH}ct0?zcy1p-d6tcM!@z zP$SEeQ%V4*Y%eW4EuHPOvh}XECN}4s3{y`h;9S-mwB`8(jR2-cTn3X0 zOwXz4LGOz@_wR4_^Pp%w`L3Z7T52Kyo*RTdZZ(A()FRnkN~c`Sd+rq6B{&SO_bC^o z!gg!>5e$z2U_hV0TSj+}%IISMO`}Rg0S z@Nv)yW6b09gS=2>4u!@Q%QS;4p%(-OY;3%-YJ zDA7QnNNk<){d<0T7VkV@TARqYtuRBVTojy)nlV}$5lYY8XRMdN)IqWa1~?g`&|UxM zL;vTUHNf3G)MCj};hZWsBPov9>9dwwG{|hp5Y{NkJY!03&#}M`B-bd}dtM@hd2w`F zhm%*$-@gA2YFj7JS1`8>PK{ugQ&cU`rr;sQ%*ThoMKRXKimaS)i5T+GkC=48pICc8uPsfi@27PuO z*0vhD6Ode^G#^&f3GH8R z*tQ5wjM>$dP7=tCE%(r>Zg5+M05E(yfVQ|6ruQ|QOOg{AwH`fq^hgnFr{fs`2y=QL zf_US^W5hvED=8A9)WGo*LzKz&ARHXwz!#X(2T`Kyv^y8{$hTK@{n|K8R&jy6y}LbPuHDG+aV>h3Bf_J`@gW(;Al3%{BvsUL z^lPFMMdFMn2qopB^}=RHsNd7Y*%ZKG-8oP(-n`lxyd9cluIadkQ72okx*X!${EzuPJO8TL^(G!Wnr~69#nyc_51{|7wQv4S$Md)wY>>g3j!y$ArjsO?5qt< z6x`5*W`ix48;I+$@pk-v+#JoT^TlS;WNbK(TX)LUZt;4rt8I7t`5+#@DWD+lg6lX< z9>0WC2t{#w!F#f=wzsmG2R8Fr)CC_Hn_THOy*cxYYLcf=Xtu>ixYgnz%%oVx1wzQW z4a-huvy3ukENr_%o+l{@HcDZIH4XVN7QY}S$ub|Ktd|Cc&YnHudk3YVt|^*Dx)r7* zo%_7k@5>;vdwfeEr?4j=qZf{HAl2z*WDu|+{rmwh1~>^6ZoXG|sjL7-F>P&ZFD76& z(VOeG`r1hr`5H@kv}1#^`}KGXakNK_NHz0=Ua2SyBClX%?Akgx)hMcG$5U!k65ltC z52nJRoY;zr9qE0O)3f*fAT2w;Rc_p%=^G_=T}8zW#3IJ~vXT_K3xM<1Co`(DnxR*t z6+}!ewig@F1|lLX+Dm^8t=J&p#E}FOL=5=@5BP$_f#fyP#I_D>G5jm1srM;zgt6OnKK|+U28IszC%7nwflnQdq zT(sFjQy00oac{Q^S=Y2mg@TK!~^~vn&G{ zb<%9ZeImuLa8zisgy3Q~Gz@=hWLOF}$BhNM1|JAh+VYc$q27zKWr<^^+LYdOmBcGi&GKWB%E8^)`O4lBxqG7%oWya;5Q&!1bSE_|)Oj%S$~ zO6P<-pRO+eNEM7flm;{8{znpcKb800p55S@Xwk9FLFwTfegofo2Xb9ggUgJ39kj8I zrZqAP1BnLAT(}kOMc-Nc=)t`Q{S*;ex{`2eFKsNVZX^inIz-iR0oshW&NF9PH(hPq zHVrxn@7;OS?}Xlh+Sewi${i~i^EomR`n#Gz8&C)Ey*cKh!#(Uo#1#xfJ#j;ouDvy~ ze3{q6Db0{$*U34&s3wb364Xx1>g1HmFaQV{<@tkM9lc~*IHB%r^@?wnC&d>Jx9@{( z!DFn#f_3)YDWj0M{h~`>#FTyhU>9-foXncU@UxLNyM-TbpM7%h2ROC7$zBmX7YCwr zaZ!vyu9PO1Ijxt|2}Mj0 z^!k#j@tLLg%TM1?+{B$nk2WzLXj|feT9#(*b%@*v9xuGk^g1T(ccmtKMNL! z`S=?Ak6}E7r>G$YoNjQlDf}2|^ERs%Q1dxSd(f(}gaj05wof7=Jm6#Tql0UbD>5*t zcP%{3rctEQQ{~xYxEI~>L&npQUAE{E5hEJ5QT)89=kG-I)AWr7(@-I9=l)k)+v(F- zzG{1&J(8m*!>)cpX%)XX+((z?7}b{!+7ea)y~FTtjRyQQRp?U*iU9MQxebzsw$s&b z{e*EY4V$&m>ULOv;D9;sR%|kGe)^mA^vhZ9aOTJyI0Cd){~SoLe?KMkGq^wccWlvG zVp5z!4(#aX^6(wd4Q%*IeTl?qjjRf_9J*WuSJNWVn`ji=^^Y3SZk!yZJ#KuI4b;h; zLZ3FX`B_c#gz!O^BV2>w=8~D0xiCVXLyJQtpUcA8GHx!50lrvaSi-p!;O9B!-93kv znpVVS2lE(;Ydn9?+-~z1_^~P1&j5*~w0EE-x5v z2nvCP&wh*AU-+TRVe@$P=ph{+{dtfHjUrJK&fLQv-pk z#ZV>2V=S77jUg;BW$8adm&xR!z!rgDq^Dy&)?g7QK^c zeTYyczdQK2>54LNP%zfSTuXCjF=P+|Uq%S;>U&V83_RaWU#13W;#|)E7{? zV$8+#Fg2i`h>m)eDwNiqk%V<~5Ofii9I=)t(Nw>ID-84xwVb%P3EZ1jjR-m?=23{34EmVjyEbf$){gz~!bP7X0Oc@caFIF{>6XRu;&qytgn(<7h!8Wk6Io zL$WX%$EmAFI-^%lS1K_G?d=-!r`qDQyqPpmH6;F-JaK>`AD@&&Do`H2 z9#8U;h{Rv;*E2~r=mWUn7BKWW2@l|FB>@cNViLR;AG?a)0mQ) z%~}|{-L4bVw~#QM7|l%6br#ujA-*UALS%U){n%d|SZv2c5feSl@Z)Fa``f`&_Tb~8 z!6SDL%-9+T}$bXQ&T56 z(;exV2wyJm7Z%{=_m3IS8aX!%UX2Y1^E)*@ue1=4amEsYW!x)MTh+9_r;QDekTt*^$&AOt%KE~bsi(pkqw+^|W)eOC75J|~!6@TyynX?pGpFl98Ct(^cRr+X; zw3-AU59OvTB5{h4J%ltz8IenC)s0w2i~;Akb0c#k#({2d>W;h~l`Y4I*7LCm6cVN(_({ZWB%G1{^tiarm~bW20p1?NCFyK`5#sS;6Ik<3007xoj_;QGATDNm)ORR&>(ij5;5QT^mQ$|wah35(aP zvq+I?Z%a33_zGJU$ALMuJ7uTBE`vg%uwT3jUO7*;F3$dkQu?t5!(d0if%!#p!=RSY8bISf<<_vg;_#wY@sPc}F04nWZ5L7`iYv_Gio zJ4-Ntq#_<|UK9)v(>=YLZ$oD;S;d=@ z>Iuk~vJ+6x8*o-BYYpHrIIDh(Q~kUHm3OJb+p?jG=oSLIV|9GV^0FfkJK}J|5l)=| zn$=pUog%=`O0pvuHKnF1u;iQK5mJHX@yufvtii4}znDW0v`cVek(AOzR+zW0B_%FP zi03!0Jeg?3T9dZU1eTYKFuJ}K29&dBjvdB1lXLc&z>Gp*gzM|c?N*kTSdy4%3s($B zIJ5>tsT$erFcqBva?~NX7|*JOi~sWf=@}V4<1aImqOEWWCbODxUTy1S6#?ZGfc&8z zo>4GT=O~jwR6NwjsN)pjnSO9ZHC<*Eyz_$E(maQ-HV4VQhuma!rAti)t!66_Td$`T z{1bGW^(4d;H4zyKd0<>Cgbw~xo;?2mX*b>8FIR8g4Eq{G?#aq<1FZ7HTk!8yAN)Vy zUhd)FU-r8xL0J+9Xj54mdYY->G}_}i-d66`dYjlrC1I?3GKbfcNGQZGHuhc1<@&8n7_veM+Zc7dZe$q^L8%-{oDw4atk7c&NKL_=TL`o<|>AI&6|BsD-5jL>u82v zVv^`b_8>{LxfUvkRJ`=r?;14;?Ami|H=r6oY5+`4g~|o2E`%n7>Do`6@O{zGF?{MJ zxtV(Q7!8N79rQ;Rvu&OilG`WRw!j*5shPnpX?9`7ad~}!f_tBqf7Z|fi{FN1*}5SO&#xSC$_qH5L^UM z5nq0IP4$7o5H>Lg0r}?nDS3@0P+=2%5<9>Z$(_k|JaqSi9ZzE10MeL3!Dsb_-(4s8 zLC$RvN;7V#YbiYjZO#-&p_NG%MM2eAmvN$dV#nr*%*zwltT<+p%Y?0~49*)M2$u9n zin?LRBPrQBf@5=99WO$hNcX-dA~B2PJ*Zza10Qv{awy_USew?)Ni_iZARtvkG3eiY$1F{(?;vm|^3%RY5G8H5A8Hd+BtiZFiecPAPE%)d zpEE=zrmA_(s~cHLg5B(|h%tL-p9k)%qjVbCi$?PKIzu=;WSQ|+l2VSB=Z?Kyk)WB>&a!>eQ#XiBC50lku-Srzf%JAT==37cT}pClmTzRXCN6dxls^8sj|j{#*NA|^2GBe27Kd{F>P zY(Uee&Q9_0?|hyH%v0|j!$9Ol;1iOLFBuQK@ei~Ld+mq7GvlG;DM4TR_=FR}&hgYn zmN9vc)jR=(68iJ=-KWO!4gJC_utzSYJ$f5RHXvfH%~>c=d2ofRtnWY>GGx1yjj4VK zFC~ry5orFaJNNo4Ce4V>vsylm7mZl>Gkwum?ILhoE6iXC#FPOdX={+I!05npnq#J% z(HL#nQa7+VuPbRK19)R6`EoitQ3Wuub4yL<$ahMD3^u(&KO-L*dNz0+MI}3_7w&2o zy>fUm9B_=F2ACD(8b|Xo9LUdJexFdCs24!Wy*NYC6|DMejTB7>noC^)n-*&V&ll4K zUXyY3*Hclxty~qeo;eH|_e;maW{s_(J*h0LT@{Q}Bv(DmezaTg)uKkTOfNyRj`$13 zA&SRfXNaSC97-Hzy->!amJJq=K%+1;&)Or%i050vsZ_VsRhan|{e8FN2eAR2U*lH0 z$uwM}-{$OI${(pIW{)e;gE>T~b$}vqQ>clS_3dl@l)~X3Qo3P@kz1YCz1QmAs{kKWeKf z;%1|y;p)HYM;(KRUUN@FpJBxFm$Vh$=-DDW6vdsy8dc?cOcnjoOPf1h9AO;u)0x(Q z{}pJxswc`sM8cxbHHt651i6{{&$zLyW|(fDiqK&6!mM!X*7c=iE+t4DmPccJh7(YZ z0f9$~!{FI9>}2c7OzL3iiTN?_~TE$Om{}v6wdJn+gTiDyR{gi#WIKW z&+tS6oDG$sF!p%%?aPmONMN znsrxTn6iVMff5wLu_Yc6vq7IfLMXeI3!jL znupxWnTugjP%6wt%0x8~^jRBS zK(-%@(MacP{3pe-?M9W;fK?l_VuP+8u_`(-(1V;`vX?HEyh>Ce|K;FXp4aewFDlbO z;$V2*hYg@WZvh~p`IG>$$9y{VkacX}2I+fi(s8E*3jL~Z8yZ!NI;D059CmE2;!L7> z`Ao=&i?yh0oxPM|D?+0J*v*lP9U`P0VI4Ifo(J9ml^EPs9j)5A?3FoHqWyf$I*Mxz zh|60%<1UK&V#8TNv&B!#nG#8rY&s$zyD0V*&n#Mz!Pm1H63q;e+kK z+Ut*%?Jw5z$$4%xD8bw_Ty3G4`=Zw;*q38cMYv&-Nz$fOG>&2PU+#2ZrIQtH9H~K2 z!wI>wmC!rPOK>J5dPqUNBz49VHFa8f@MwE`fPh*L?!!OZ+jk${p?|-6a4(})d-KG# zn{c-9viSf`55)tnn6)BV(wAsAlY3#OmxT%{!0WALdnw+0AX~m9ufG-u;anYwMTH%l zh?lXbX^0NkF)z{G{7C@2wn#>effKLjwseNpLFQ?nnmJgbuy|sU&CBk=>?HrRz)AAFbIAS2MMZ4W!1}J~alOwPfVt>a!#_YirKas$l+4In2T?ttyWi z@^9|b@3fgmY}vI(hi;;e{hdGbY^4m1Q;L-X_Y3mb-lW1>3e(FC5+(aFi%+oIuZvaj zYx$8fS=gc4=tvHZ^uN^irXW6t-S7D4@G;f|N?9BHIGRkbLIzGMP+|kYMT1C;@#3gH zH~Gn~twJIbwdf#$*`CM9@nW68I|FJf85U(3bes`U&BoK`Zd``{2+WZn)lxxW`RRn` z1?GEwK8oB=y^X2#+J2m~Zt+vxfFKOEqQ>YpHJ2)-sGuy#SK(&v#Waint&-_1fhRTE z82vz~POw1sf=4j;%RUqaprHLWif8fH#7hYw!_oO=He;engmp`02z*Ztz(|hgDtNRS z-pBQNj+mX|o?k!e1T}`lDPdCZ5k>mW{r+dfk9d6>0(#(cG1Gd1b1S;~6rfmPC*8RV z$zKfKw&y-476k!1M04#S?p8V|soFvBi~C#mP<*<(eeZ5R!6>A^Vc^}W*ckWKlv97* znXd&JU=v8(0MdYiV*DTF>0Q(%~IP;^c10>(I>`H@quxkp`vy z<7$e~6nnu1qvixeu7Q|QQrU~y(R7KpGsORFqzPO&9nTp#;Xf392|4EXhG&>EL~wv~ z$I}Ilfc+PUp8I#<~i&I(g{2bYu^T)EtZQ z$N!Kn1x@`;{{1iVCn%$-kB`C9_BZ+WzX*5bZ}i{)DxjPb<{{1ho!pU2ovs6WZ zs)%lNu|0sZ8hz+TgGB6xAjq4EMq@ks9IHsK@ z5N<$`tBN*Rj*@u(l#)QjwB#VGaAOsf!8C;IGaLL9%gGb_HoP#@2hHH4sl~7c=LP(o zP~yDN#e=$y!ZLgrOW8mpKr!`n`W+ZJu(R|DxP$)&w=h(B!+)2XOup;i+Dhp&wx9nM z_w&Ef{d^4)L8hO2E8EZHyZ)`M)iF27uRLZ<>6Eh)O(B2?*U4G1eFhGncga7`#{AcI z{Y|s$)1J%G3aH|`BC7dN%qI_>lSp1%0+7-|OpH*zoCflGJ&OZVW?9k{eb zeuMsJ^M5hsy38M8vPN;-zeYw_=X!MpyJLutdui;(25G`l>6rjjpF>JVWG72u70c?z zH9B$S1;BHFZn?mlttmyA$+kdNd{F_91W7ffgE*xpn?@EzE{T8H6t4jiAYH}@don1a zdXG)CIRl4%q}L8spQ*&1wO}|0D+Y9=<}{i`%)zeV+(Ic3W)x#UHtB6J903#!TBZ?d zjSESB>ZFpn40nv4Ekg`~f}}a&4LWc9;q1J%#>dzO+B`7rGkeV#nzRCv3bvsjUoLy0 z;Wqh5n?gcu25n8E>)yf41L!(84WRinV_4ct!njXe$!+SU=ncsxB)99*$L?zSpa`Yb zZG}(#A-ihzR3Pr0I~R5`z!?LXB(oHIbzNyIVtky%MpcgE?syDb+ln0BL`U`@Qdbc1 zLO`9z|DHm4_85p_k)C|dZ@Qk-m0$5q5pK_R6K=oj7c6h8Gm^30d9s*xr$M&3AT4i8Wg&apvp;Y;}0dUl|D7}Y@KC<=GLAb)=wmr`j>uhMSrWBS2_Sb z$I}3DW}Z;7&n$62$lyt=UM|WHNLAe=3KY|dQj`YZbThyc#e3|w$Ol_rCvM=QQvxwg z7%v=_=Ug`EU~HC4;Q|d(k^DB}HPu)@Q2;XaV9)!211=VfN$kdJ_YNsnN~)(*E^YTY z&*gcVDZ+mG;^F;!=*RtP>;Aog?k1E(J=iAF9T+QFwi}nK+jtnZ=Y9|Uf?lx-b3hN! z&_*L5SH*atEQoNU@NtNL1=mQ;X@zk0Pfrs`I+CR6mD5au(B`V8In`g{$mc*c2&YpE zyu_FUws@5n$>`$zg1LE6s7?R(j+gewfeZ_$8X6ps#K3L>Ke~w~H7G;D%7MTaG9

I6qOh=J}o(ED7FNwM{J zg0(YoOA&t$)*)Q_AA$6o()Nln|CIe~hoXn7!NsHJu&56sxncRhMFmn|+eqz?6*a#i00JyBu;SKyqOHWRK}P4|}S zyxy+IlxPLDgQh9v++hl4R0KBd&ASk z+0^4tPDqMYLy1YnBO?OB)@I#_K7mluNh_i z;yCCqqy7M4qe59NR0cKnct2=e1f`&fNX|%zM;Sk!htKC0+Ld4o%aB4;bH9giCGm6# z>_BS?RKt&6O>|$<{{vPwCaBL|+W=A9@A4?vsUzCW=c#UbU7Z#C5M}Oz6k=W0oj*ou?FTI(PYS} zM7KBa6c8{aukg|D*3Cz4f}O%N(H+>E^eJ-_jTj%@@`E_j11i-X)~)gb#__qJ1eI?k zfhHUSkz`5B4aCNXVErMg2vE$G`#b3YwZbNDO zXg+RE*+_uUDG#^4>L&}+Dqxv7JO{bx?K5kqi&Ei@L&=rpgJ;+oD9~!C$Wxo6TqPJo z1N4})t0h$uFJsABmx#M9c%1QqcrBzCU^S>bH7D=>dKjPBk+nEu&tGG3>!S6BpCI_c zpw_I(1809R?NNjCH5J=g(jlJ}UUb7S?k3ar=}&?=CNGzsQB zwMZ`dWbph>-go0>dbpW_3M{r3fvJtWr;FGfgpC`U|C+0sTn)tn{a?<91?lGrYYoaarzvKy&;hm;&486<(SWe{fTw^RiN9?HdnoKw}SqPj_SI`IV0 z)lbE@*E?+GOS`VEICa{_>trikYLGP2oGx2tp?G*|z@gX#=>d70XoNC62+oJ>YV=F) zs7q9oFFMvzO`D2lq6Bf%!{WKr#lyxni-9)}tF-b-zr*4&PvoOD;F z&d~UNMcBt*wK!JWY1WcD(|*on<)TlYiA2}0!@gT*pe4kjJ$=b|6iocl!1137y=qkf zh}FKowUtjj%bhT0&OC&G+_92dosZYz;9;7b;oKaz1?4M^WG$mq$r^JStyE2tW3!&< z%&`a*O#z=u(@JuE6Jl--{yyTM23%J-Xe^0}eCu)e>q(+s3!l9QGV+V@dVkWkSc~?q zBi(!!`)Hf_PZ5(pexemei7K5)tv106uC6PgYW}9D*h6247`XUMHg1uSmSf4rk4aP5 z>3Gqdi@N4$K_T+CdA@R>3H+9u0Ia8AtFRZCnXYLM`D78dUAhwzjX)Zt&o@>HO1v#Osw}|QU#)e(D z_4=HQs`<2TcY-><$b&zc7>(nZt>@WcxbrYvu5pVOF@p0k1gdS`dhx^S=Rf`Mjiu6J|#Tlkek+aY|mQ{-kRTbncu6rO`4R>1s4RLaedNim_1;Zka`D#|1t z@ItO(5NG(XT#Wz^EF5!0X4y zgRzw|^pRWgT=hivBD~vK?bppG}k%gBvBd>Orj}}UTxU+-UVjKFki8fLDHtwydtG@h zvO^=vAcX@{$uCTLu~jLg`V{+%`8&bHXuVSD1pPu8*1KSw6!;4ACwWe(8 zXrqQrD9ziNUCtN#p5GqGVXXr#6)xzp&df2mT?F4dK5qS9@FiRAhpTORE>FXk2*R1` zJf25^a(vx^TuE_QrD+2Y8URjai+%^K?{rd_HvCY4G0Ehecagg_JyP&-x2ZcceIZPXxOw!+<_tR1vh2eM>0^;Zb<4;~x1+c;0&_{U;sr>Egj|XlMEq z)$PzW1vvKO*}?h(z>1Uqk2#R?-0M>mV~2mO;9MPzc}eh+>u@a2cCc@FtZF&Se4(vv zr7Y99*KEfP<8fOK$nbOdZZwj$z8QRth|h)Bq%9V_71O13(j38x%)~;+o-h6}RFvb*EeQL+e=eDt1R2|`Vw7bMR(VH?46M;N$9P8p#ajnEapp!& z=3CueNgfH5Vv{}t*EVn6=oGh#SJiL7fz{>dt5*;@D3kwu2ELpU?>+^HAFi!-XKVY8 z*53FQA-k*xvL@776!hj5#8}j*uHFKVD)>z?<{8P6@iV`F@wz~?tyx2jODJA=muw6uESt3?41Q`gLokfoMt_Eq*<{m&NSTP5o*VK9FxRwj^3ypjJtrxaG z^TQ=%*Zt?KC+^Sh1&g!e;K^zCS)!ImOfcoWoWl&e05_ZivLUhMM?@2JSHsm&nWJe( zkz%8VnIO;5=GB+nHjt|zjSq~zjD;cT=>D+8j)=x9*#v;;hU;L>aT8KM^w-f8%Dz3t z#yH$(;kFCK<$B=aebn&$3?o<+^(7Z>K$j$orLOcB5C{+l-+?rgnzvv~7n z{^rf>&70$cu35*H<}YCzf7txO_JxUDe?Euw7EIE>Pe84{H*c^$AAIX#%4lXPh8!hcH=)oPa!&c@caN$r%H){;{mlOGDXC3RXr2 z$>8<1CJZdRbQYBMqDdyA*nonEKT0|77nj?LKSlevc)Q`?Cti2 z9L#(4z?(Pw$b21I0u}F853j{E0Jr+#(`G-wTYDqCYda9PyEp3X-+a?O==Ur!{eu4< z^l$C;-@LhdMW1$N6Tf){|1Z%?c*`NVAL+1gtT0SUW)BpckBwL{a(3a6&YN;Msi95^ z4E0^PXu`+MY!!Cu5zCl8yr<=&!o%3v=urc+ORmRVYUT>X?37I@#~Z?NfUgWos9~j` z1%`=3sTB?boGs4|F;>88Z;fpqBiR_K1x_r4(iB+yA74HH%d?l*{I`U~#hjl9ifcY< zSzcO}UT@_p1vh$q;0Xr@SJ<#6FiZA#2ULp}zi_L6HM|9l;F>xN1H8PpYB^v1#7fH4XQ*b16gcT~GXEGae=;E1xRGLzyzr-|ln@0MJ8NLo1 zMv-{FDxjrHQQ;PLJMKIpf8gXtq^UWrB@ z^0-{w4}aT!X(xgtM-+)YA7l5$v+gKp=8IO=<6s_ipMLk`)vM@zEGfHg3qL&h{&}K- zeN;bpX#?!Fw8ak~5Aqs){p3$ko9R}fLxI%MxSLO2zy2v|(%)#_^Su3&Zv^Yf^Z4f% z-^J@N>S0}*D=M6F1rqb(3b-Bq-v<8ZZ}g79O+LA*k2g?u6}FK*8{~XIP4TLpOkh*( z-v$Aq57mArxB9arcc-(0o?6u`G;$lbN&1K&HUp9Q-keIJ&yq`tOe^62bIc#E&dwPX z02dh|M0y@(|IZYsZDWUjeEEtgTOj-Y&OG|>c5Yob^5lX9-QCB1dNr~WRsQ4Z1m>S- z-!6uo@38!fH-qpL2xpW29!+y@dU|`i`@g_Ee`zMlGjz-mM~WrD&3)o43*aguLG%rAiFfk+Bu(0g+M{{=1}7y4hC9&(JGzCypQ zxZx{YAUJ}@SDc*zFNt11KZS#-TeO<@ukqo>P{wwv0Qkrc&mL}V4fx`} z!_T|le#@7iZW(AvNkvVm?X2XtD(x1F&8^?&8eu{l(IK4z80QF9Q|JZQ4-GstM6@JO z*VNWvX{kyYBnN@P5izI2vKpq^m21J|;=!@Sk{Xaw2ocgyCrT}($9LQ~w_CT+T8PKS zF&KQ7b4-C)lr#vu@qd7qjn9hTwZ!O0v>Cs~N^)|6@|z*9j*bqQbunI%xzRjVHC%hQ z=i0`#WBj57=)ry9;uO(mdnnO(kWsh6TJ#Y|Ie`u_tsuRFvYvh;)93%;RvX*v9tT$p zq&7(ZBb-%I`T!(FlN`>;4^TL?oE{TS3^s2Qz9~nKO6Jkov(YS%3CW^g z@b^gU?@2-|^%oOsr|7ekdV9$1hjiyVG641I1C-gM;(a}x;Pe7nB2=n7qn?RQCKL&c z+Ow1fQ>G;5&Ux=hEPa+{;mLv6C<5mNZoM8NO;Z_^z6BRM0vOq_?tZt@6fSRk`bN2rS7%W1t0Ag^B z8D7GA{1M>$^M%)UBwuXA!5ZVXg){fMaJQ?WzCOyP8C{Q>(1lTMT_&7YPVNH?2~%vQ zmp;m4Z@IU2+I$7}VSH`t%k%M~LCGaK6UojfDJ~Hwh7kv*U z)!USg%f^1e2OiR^yXLP5_uHP1`WF>Ob^CwAb!o00S(!Wy-C41vAk3`jB_1`E*D_Dx zl}D*B=&9~De#7%oQL=_10uO5B6gK6Nn>@pPBLcxWG0Fl&)s{z2kVFFg0NXo^310Pqv8pZ?-Gh$LuN<{Gy@mxu4Fg>lv_AZMYiO-1-aSUOPWP!xLc zY(+jH_8PG^q%+|_0=Ygmw2-HQG_%B- zdjY+B52UCcQ_iyEwGb!f=nxm5Qs7!^dA)V~#7XX~<~V3X?5Bc`ZOyg)JLv*&*maFd zwWp`m=VJcSd~^AFe8TzcmN$Yv*KY}cHG^I~m?2wg^W$+C>0K^MtjR&p$!{*7qQjn2 zkg(8Ys{8hdKWs)!lSF&#PR2S_=ujqsXwTCia!*G5k7i6qor%U^RZrI?K4w6SNv0;x zC+no?f_-QCrL{Gf3p?tvshU?>tNY3c~0iy#*(07=UKF55sDS@rZ#BR)Ma_<5N zeWYu!ffXa5@qIXZsJJs3TY-Ob5Xq%V+#jHEvJ>)|gw2MH;W>dV30@~97%u2A>AePx z@*RO}KbcMD7~~^-fQJSmfcgnNkU&u_OxkIe0JdE+*6cc{ zsqq~mfj)&Jug9WOHbj+zib#mXg%{sn&x>A63nD$H6^GuRsIjuwttQ=mK3A8i3EsC( z3o(EW(1-lxzju>aPi^OkcB65-QD8qvEHBnd&E|e}_p8b7O|&=SZ_vDN9wbYUVq^)= zT(%NtTfsSh@+p)=KDelLyY!s9>y5zOjB0Ba`;J{wmuCD5yvh9=?_nACTQEoVx`S?e z=@BMc=lx_*bGCBBDu(Nfd5E0gQojQb0ln;L>+~o2On<;-Ra)x=WY22RetW=wpJPdf z)QD4>Z;Z=^o2*U7&}HN>C4&F=og1A{pB|L%d&d7 zYZiG%R?y-DV7NL45MlUNY%=`FveXk>7Neef;v-l)C^Ql%mBogk{>%i6A-=b7i$1g=#>+iqoV)Z39%aGY$j8r8SAdr_01{!p` z8dH9CjX6;U1(>ufE)VD~Ms^kV;h}?I8I3+D#~_xjXh;QBh*z&=V{+CuT?2EXFPk05 zNMgyuzi5eM=Bk*oS|Y;0tl~fM`pmdGpKG+S;L8BpHLL9)QL0D~o+$`7kR`Qt4C;Vu z0Al;iFtCciRkV17WRkL$_|_l0*%5TLUe0~3Y+`3=QVXWGgaY%Wm@qp&wksJtn>cJ3 zUZ+jULv-+}*&G3bPwE&DbkbKFM1esDzTl*S@PZ%zp?(T6G!)OZt9DSk+GM_T!Ah(5 zcD~i29lA1#T%;^ zlDF0%s~ONRasiZnzGgoD_-hFdeB9lMlD=(_F}{v51T;f|5D1t9iwfMB?m4n{!S{v#ntQA~-9N*JB-4H1{B2YUT zVjbDry>4kJIG4#0VPty-Rw^c3+#TLgS0eS;U3cb+e7>^bk-gM^pdJl>LjEHdpDb1vYLQVBN*{qlsJ@UH-<-M5E zQA*;Mvw?`V9JL;!5_VcBZ%o5u&B(seMH(A3?Pc!=G$aCMdO*SD_o_1mbmMCzzRI4>Z+Vj;A1!*7gjqh=ibtUpE8x;&4Tko68dAKCvG z?mt|hcwa%4oz1W8&?hWvTQ*=4pXfjnL;fcnXiy&ogt6V5Uofer8H7V{a(>9^W^Z`M z8=Wp*;!QwmE${Za*dY)dD0lV?b2vFMM8AJ@0)u=m%P)`G5??|9DO{-&-!ejt41hvI z<=u~!>EcX$=Et~=JS>$%6VK#mg#SCX(dM2_(<5THA4Dto`aXsNANXNGlY#Y!wqS## z{bi}O4n2xFguFH{T-q-6hq>TB-~Fb$6UXbh-S&$8vjfm_+yJU17Ll_sxSYwQNrJ%P zrhkE_Q{`F-HGXj5gc7DR_&5C{Zd)Iy{{mds*?!N@-QlUJY*j=|++U1YWKcy?O2M7yUG`qP5D z=#b!=Lr4d%lA)YRerZx}=#SV+)i=?2p0$P$*Mmmt#{_a)1^P&qCNb;K2njW}(w?R; z$W}U+iL;GkC<5PCn`Im(Y7Ni(N{_m0YB)PztB^?G6maC5A5=edHUQ-fxjVqBcZjRD znqF3t`+BtAM(0c+vm9`zQF&3X4#&s-`e=ZjAKs4A=<=U9R@>|+9F%QS;JEOuy`gO~ z&@+#6tl%6N@0Uht>vO^V-iQlZqg4~1YXs7U1V6&Q=)HLUXp^l;+uOtY{TN`u4Lc?; z`nWeDu+f%ApPtUq{B~!%Z*opSTIBKu5Oq8SSTO{ihQGZpF)W&lL26DF8f{?wMg~`b zLe?+Hq&bJ;)D@+uLYOW3OfbQr=^U}%f_FurA37<({;Dzg056_H&FKXiso9NkOvY2LbP0tn z!`I;}7c{x58o*J#QmgtEZV5`O z)GuFgWPG>iuc{2f5s$(|HVXZeE~DqzMc*FJc`_s=U%qpDd-JE~Pd9uRg$=yE_2|wc z1c{)JrWR^(iuSRhMBh0lvQzvG+RAX1Frxuok#onHw9XpoIIp!r&Rbal&y}098$L>& zU>3f$v3+Ngsq>fJ;>+FQ)-DJ0)BdL=jb{86uM^J&@eKtASh0&JO_2^z<02?3*h8DH zRm?gY)0AHQ0&iTG%n>@`U^h+aZ@O=~S2u62L%EQqc6WFmX||lvuGk#u{-9Y?pNLw| zd*KD#>0IAL8tDH|5}*RG7u#(C9#cbe#>x6sTrEXQ0hW zMt4L+@knkBMjyfr5pJBp@YblCf)`62)S;N<9NH3nUy7BV?9SoiPR(iaNd4RQtDm|c zR=CE4^%~EdHNiuDO$)fTJisPEihH2I)@lLgy3xQE7(@_?j$sp2Xb8v!PV^Zwa#G_# zek>mORIiKRKK#98Jjh2R*ovGqlnsj<-PncwWH1b}Wa^5WZpyu-b`FEPvY`N> zFdo0AUGZKu>Fg4$VPJ;GGm5&hrOQbYJYHh(n3+0*Ho(GjQ&H(JDhZ=rKNQ6=5NY7# znb`*9bCP%I!LkJ+G|{@=>q`iVTs%W7vBtyvST_}k4k;TBWM3Sn?Pru?45#2Qh!x(0 zOJPLI2-=A_=;0ELB!{T-9Xe*~q_u5KjpG~W`M%MkKk>!o`7jQ(jUx)Fw`Mjn_DcUD z{zgAznH*wK)d&^dP6>APi0DM6TD-U;B$g#e>zaZwzTLHRPc ztvphCPd-WO^eI%eisVvRZ<49Q9|m&7eKij1C%M0aUACRieH&}&n}#a$W^zOA21H*u z3JXy7+Ud%n*%hBWT>Z)IYm^4tTEog{=l344#f@#?gWE|uP%f7zUGHt>5UWUe831i3 zjL&0Y!tP4@_k~Gk?~lpQ4eJ@$x)1sKp|SYrvH1aOZI0(~C`#@>dp~iS7)uI-g+F}j zxjp!iMgoDcjq~ZUA}CoswQxgfKbIOP{rL&bQ>hKa%w zmEa}l3b(~ zpc>++EIC_1i{6+9{inLdVr6Xc?+-f(j^_wW`v4OJKs@--o+A?pnHkNI@;08?e(rir z`WPrSSg zDQq~Xem|AbN_V`;jT6dHZUM@POd9UWr~C%RBe1-L!|*K>Sba6Lvm~s7_;suhmbJDx zCZ(|6Erg1eROuo54j;SYik)+WLAWB99@>MXq!Td~&=9jA5=P-Rp(6$&#>+X$$nC}{ zzQ{*MaIPEz#dry4$z5*X+GVHjW0wwW)a_&Btxv#kImfRNF;o)@9uQ|CAeA&j>Ua{#=*(6D395lBkmy#3Z4vNg!j3HAe=-d z72XIt<)&6SU2^uRi;NmKnFy|R10}pQHGNLUV9ASpo#te-7KK7bN)iWo@%Q{$96350 zRq2C&hEhNQxW^v-=4h^w;3+wz#m*0xw$7RpGXv~KK+7g&xPm?P6``N233oXI2WowS z+2j)k(Hg3(g4c%{l1xL#1|!xN^0iQ4IsUM+k9KTH+4QftPJ`{820FQ9@5fM0T=B#W zp+fC-+y2C=SX*|x{AETe9jFqw-p%TZTSl|tTvglZz>(iTzfcJ-C1Bmi2~HjeQ%dTx z1nV{FQl?fhh@z|x`Yja=_~BjqI84h1h^}d55S$}BatJM=a>%7RK4t}sWR&8+4`Dr)^P}ebZf!)(DruKgzKUBXmXGafhMqA zHV6y0CDIn1h7yYB49=ZbJa;)m-*TQYqO=&?KDI95m^CJPi)%vvWYPs)a6SQCRu>LZ zz4dQ5Az5cV+KJ z>v%7#li_23#r^9lVwMaw#8{~iPn3CmglY;EV8GaMW)4iBozR?t0;W_AFfbx&l-t#i zAE_se`)Bf&kOVNH8~F>IXmeZak1>`<#r-Wo?3aU#j<1n-wzySLMz$D=Tt23PpTC=W z3y9eQVH+d2Qp#gU==>ck+krM_T?<&45?6Y}x}=i`3Xssr7rbX~yU#!g5aa2| z*(Y_!uqepPvCaCOxr?1q{Kb0*NZI`Nefk@b*ss||AJ35($bavq6LFj_bXg_W3j{UZ z8d+%M9e9IF{Po~4!ky^wpFt-s3bSgFHg6d@i2~{kZSPAH2hljTS;FP|GFaARI2^_W z5$?dsfKU^ZnTZ~|Yh@~1GzE&C;Q!62d<@ox*-L-pUhx5|PMiVup9J22nBBf~TEzGF zu%)Z`<@=zap3L|q+eq}-gW|)=XTCB`UA8>+u()h{1O%5=yC={Ha)by^S`xWiNX|Q~ zRGKa^?F7mK-O}`s#!O&Zz`SATHYZHF)-3JRRt8Ql*&X=$9O{jJAhXnELGoTn1;OyB zbW%izM5L)UKtw31^cs0_$Y-!gtPoJeFt-IIZRFArx&Z8CYjZoZU+>`x+;(`zo@E)g zB}{hz0kMy5tVMhW1k|3h*Kz8Ru)CJo=w%@FZW{2;e1Sf#&3$!Xn8rh#{7i!}6XnXf zTzMG|+|%l?TG%>!D`09f>%86?x3IAc<1)psR&g$}HGV+hA=brA8OmLyR+vl7e-=ia z-8&xV6A(u`wl;f<#m3JEhjW=oJ*sS>!*R1wHM!k<@1W&0xnudmy@LdnFIJF|S#rzj z5vwRnpd-eEwzg)$+PE<6{`(`hG2rK7h*op1Bmngl)qQrv{mPxB}QS66%@9VVYdFrc1ed8ck3=Ed(ZXpVb!$h$K4>70dVtoGF# z*Lw9AdX=BY^PSqTC!elAKD)J6gSXae@Nqg0Hg50BWP5UOY|wR^+jqTb`)_KxCknYP z85iXGj2>U57ZP@--}x=sUfTWJ1h8RWfSzo0GhYl6ELu5tWDHo4*%>u->(mg7(V#$> z$0SE@DB(!}Z~`6khU%EHG!8o3#dtmm-rKmRZ{hb6uhqw5UnfHXD>!UM=%&vM>b}hz zqG2%A?j;2=Ej&a@<%o3P-YgbHugB|e+z9IBq@c=2ofThkkh=kwJNr#r0}s(+@NB2Q z$(2FgY5tpBK;X)P3aN`hHV+6ag6QwzHO7U}b=$)_xW?j)u+_1(G21Fngj%k1i3gq- z#Uhj}f4dlyZv||B8@OYMBh#+xq4k$a zl_KB`?P@Av3JdUeZvF4=`%OCt(`Wd?DvG0^&GKFGcic{YXXC}VP{s!RhANiO z_3afcY0y?{BX`#5j&LFF6}%~wV`~)eJNMhDD8=7y-}c&e;q`>=p|ZIS8~7Gt*bm1B zDuB{?Tq>bJ4%{3o8|%r`xofdSS{%JUJg?7t{eJs#qO=8zLh0k}kcq3naP`B)H#Dw1C{YbF2e1J|wR(@6J!ynxDox{wFuPqzn8p zl9%1h>uz2Ou|OJKD33wUW;w>Vw%HLdAlTb>PpmIVu6;Or^UCYs?~?GEze{{pa~=Nj zv9tbYK8-P6gNywUqV31dr*Xj-u+D&qf}ng{y#4qX(oisz4bC98UW`vLnFf3>mq*i5 zYSyN;`*4R>-#6!2{K5>v1+m? zJ+r}uH)d3w3UkP?#MK7YWSC*}?E7*poHrBmwE2bGY>Zj<X$GS z6M}o`8sr%+fH!KOFZK&?C(8*#2|?0CIj85Kl>DNcH?f<@^|S*KaL5qmy@J~rgWibs zrhu*pdS-r-@ZcWLG|fof+gD!`?QgncKczPV&X;A&zVo^ZT!2$UO2y;ioEiCI5_O5A z$-s$qNrBRC>=>Rrz!W)21Sch`Y-v5e^HM!(h&NA-&~_4!ni5&q0>-uBNyh`B%;ebT zbv$!9V4K6s3&LLI7KCVb7k~^I;1g8~G8?Kh_vTTv1UBBC>3(bF2v4uM|pb8)cB;(<4R;+ zeF#AdgxSE?s%doMAoB408Z(=b8KG(mz^xNa5rjbUY8ql*B0ep;QZbMPn>~agn|M4$ zte#Va(opIaGd*>eX@ydxxZ5jhUq)QpjXHqZ0uE=~>wL=AzlMoE4J&Vhl`IdC-sz_} z;ToEUtm}g|Zcs_4Mmp&vpL}A!4Fba7JfrfHQ0ONW z;Jh}E=&Z#xKFRSyl+1xSd272_OnTZ|O_Mm=TI}|QDQAzW;ZCcJ+INLiIQKq*VO%@l zL~#PNMNOCYkO9`}ux8%pd!4M=aP1!V;-n8v{I)qJa3x5caGk-@k;id{WoB@&>$*Qn zl)3`~3GrXRxwyijRG2JAQz*IzB1sd%i((E;BV}J3fYRoo?8w=rIl#-ncbAwQLqYQH ztA{1>Z;0-NlynJ-V{|UaG@BPs2kx_&J7ry3RvALowdzp$=tI&JzH~b30NFC8Cxn=1@W?{FBZE2yhOgNQ6i!WxHN6XZUz~m zD>p~&jp9=)OnOd#_0P6h%*6e13%k5Z+GT3XFfO{o5hW7Mv$icHDk2j&^ZTBg-Zd^} zRkao2D**%j9w2xCi)TD5!RT&xAlOY|RaV!^LxO@c5wVpoU_R=L;j{+rT+lXyOQ-Q8LQE}uGi?Mv!040_}r5&yf(QA$EP&PSVCO%L_vKC63X0z z5q^u!3(&BCu_u5~H+nX+TsH~UIK&_S&9}yfWa-WXAEGfq3PYT;j$S!-gRVlbUuTFz z15Hl`!T^NUojwEV{^`y8u5+(-Enu*+aWbVAPhY*dJIEDKO;5a1;o=kKt{ zjx@D#c)0 z2KSiytvo(X505Y3TNNjniQZZRS-geo^2S-4qu=lx3_|$_alU-8XXMD-bdhhY>J8Si za=vyK=KQ>y8ekJyGNWWBy7m|7@#b1z5cHU}&`&!C#O%q0&ZgaH)+RV>Iv5Pxi5K@fHpCn9|1;8xj2gI+n|TY%j=N( z8m5F89YLt;c2?!}B%J$p2Ghrhol%;e)N>%&3#?(bmUQ1FVIa6X+)ebvelEXz6etL^ zddP&HPD|gx)g|TyYnXu*a+9-CleG^{-h-T(i6YHcH|+uEKsz;Le-c-`r#iHIgJy*f;8BVeova^;aRoN=F!N$xJL6no}aYeaj@|;Bi%YaK=N$L zm#E6Pm}27m5})CK-vsiZ^eYSBTYo!-CQjz{^1K;X&>bf%MuJQw8(zX@jU&g;q-Bgq zejx$WWP7@Y`F!%E$>s{6gQ;)iT;y-@P424Hy!C*B8WXmoSEN^JLF5KKizum~SQ8IL zFOJm$>vAUnI#N>2d-e??$SRf68v^|vnT9i%nQ06O15bqs8weyuNV4MNCarNyPHnQz zM{k~WJfX6CnM!DOGo>Y*pb=K1g1L-5@N61)=S?43HG6?? zddOuj0`|f^mhPwQ`QYprncdJyhLggoOYO8V6=E|zq(EMbFNh1X!!{b(8IJ-1FpD$5 zvze3I+m zn3|sqz@d**+SpA91f-xoWX~F4;}%dE2>dsCkO?K+R78;V=%jhPf6C|_jQq=iZ_M`_ zI`IOdPBseM>4Zlof}LPbN;y0F-ihNMuck1&vO@Pv4RAPNKzb}H>{2>2SDieDs7{ZP-I zYv|4{w?I+Ol6LL_3p#~TXKK91Ke1?#U-Q6xkB{EHPE-=r1abq24q7X;GSDV4nWK_S zv0i^kE^O9WG z&Rg4A5k{Cwag`Az^4yTXSH?C|yF|k_*{yHud7#6aZtlwM$y(WSZhDHo-oE-Pl5f_@ zf6LB}<~QMl9ITNn$JZdBz*l`l%jJ$MG>6G^+d^QghKObiMj||jsLpF zlDQ)m)hq@3?VN8kuH5Rw?VK&Kb?+oIWO$`J+n9B3Rbo=&S&sv`)6j8%x6-=N~j&J2?`Q5-oj*X z!z{qv4VKqIB0B-r;(IOy9He4kzcP2Awkg&ntSFFC z1*7_7*E^vzvynsx0>6cCRFERazYUG>Z!Tv?ptys*cpN42Nn_kx5F z@DzW5;*)$G$Y#{maBT|UVhn&11p^38FEa-(KQzi^LA{vm1a?TUo1cTp6SXt3nsrjj zOvbotg_;+$fwS~^W!SfaJ~=-JOOu^!|J&Xo_@74>L?#cVV)xt1Yl73qHrEpthE;mP zj)wdAFS89cejbZ$-*6oWIFqV(RLWOzUCe|;CD-wzpDvK8G=QzN=0w@4*w?ARTqrj= z3VIja%VbJ@T+`%9dnzfL_`A<;y6i}u7jk2ilYsM`!RhL<9ZXnfx-cP{-X?_~wfhaB zfF^BfW+yk`v(xO$s0FoVN0DeR0u>R`h?5?)S!$L{(M)K=e(iq z`CCpppSbYrEPC65CjzdFzeY&Yq2#?}=CuT}jpT@@b_tKPHMxW`D)AG{H?+*8UTMjz zT&*vAN5I5c6uT7cv{;^;y5obH5;WuX(n%iKm@}f2XOwOw&1_D;S!}I$>BtO&m>7y& zijRHLyB*BtI-D@xF=qDM^DwbnvPL4@=fg1`%e_=%D{Dao9vOM_Ti9%`$s>|<%#gY8 zsUE~eb$h#}uSU|$f0F7+8b_ejl=jsb1CCzyXF4kV8LnluaNZ4DQ_K+492ma?qd@oy zSH#@%qZcvZkPz0!94BN%E9e^G;}Zyq*$Rm7F-Ke^DlaMp5Y9KI5NkKpBdgc+eanc< zAx01Rexz$=`nU_?hGRzar3DoRem|bfE`}(@x%?YPd$M+&sgGuf!emmWctYW66o{1gv+k8u*T`OBm zgZXcySw{`2 zazKcwPne?^XUiLtxMc<@`zSu}4H$Kn=S6&)Cqof`7F>q$^kRJ3uxYcb2)={5XN0?q znL1^vwkr*TlM`z;*{32#;Fu#@wLd0wZ;SETNf~vdlYX-Ef1#q?i}IwLn@A4?LDUMv z1l4FJGIfSYun4~Sms$0XB>~Zzppo5hP9-OZg=L_T)8QNsy|W&PNn6A1WT8Uah16LR z30=FMOr{Cx&K$)Dj^n%EjrZ%yN^Pz_i_z+i&CswFwcjycX!d>IR_2ULAqO8UHBJpR+1S4ZoCJV`Bpb&vMRJ$1CJ(K(bC=) zS+EEbbYcavV4C(p+*o1s`a=S41SzT2^LY)7)$7>2)*$I%XW2L`-edWGcyxUG_6kaF z6SY$80%}u(z1wnNrz-uN!oH4!80Ub3@SJP0?!LyLFd}2(q9<4mK`lZU5(naRQ=w+8 z2mWf)nnpAz(YBL@yP-BVa&wjIK39k?Ih2=c(Nl$Tr~+Zq)D0#gySEnk5hQKv$%AkW zD!zt!&hyeZYl>_uUinV;R0rN)C9GRa1h5(vF=y|U)MzuzB*91=@o5DUF}%YG8?hqG zIzgSC>n|DUigeZz2(pTgs#-3Pw^wJV^p>LJNyYl6rmX(X3RWm{_!uLkTd3w*z@8Lk z(sM&mC_TkY)mr`uUm#>30wC0hm1hOC-KWQIm|3)k|=+}%V0d-5r(KwmbhY;?DzDGbbLpYM;xwOV{zxFYo7j;>{UI+4J zX``#07)DpWW*)uQN#dLOYNcy!bZ?L%F5s)SkfXg2<4a1a2bP$Bff_d&%}_m|yp&s; zeo}{*Li_KLr_d}FC%IQl* z)}_#Yu7(h#MetX7UP8LztGkKx(%5{W3N^hG`7PUy1T#$u>k+k^Dm-M!6H zj85^IuB*@v(%MKSAjM`S2`x-wAoVo4^y1*d>l>R=0ek$p9ZIUU=B#W0n-N(W2P<|} z5|qB;(Zfnpe+=TP?Eoc2NVVReL8GTQ1+Wz*%`r0b#jHHLOq2xLn4eyk1SiaE`p2#1Tj?jwY(%lwk?Dp zna%Sj3&aJqgTRvDv*N|GL~gB90X$U0)D1gNyliVbFIw z$)FcXIqsrb(g+9Y9tcd78+A%30Uj+~FYYZMxauG+%|1EAIucStvBJhU3+ijA!em_LKi^{Mt`N5I@$25Dpwpb{2HPt7s}$^2oNA3SPoLq- z{g};;e<~^io{ETbuw*%#Y*M%lJV}j0@F6@GXo`*Ff42oHV1O(Ku>QuJZIzh=DtZ(Oi4FM!N1aeF19w>PkRCxhbH8a|Q*PhAW`kqnN8!mM;HW+#^ zb@Qr(GqWSgs36$A{m&)XXNL^%CqIOJ^MmFp@a4JHWeswvcYnV-q8WEC*}==jrTWPt zJB4{fq!@WX$_tpg6_OH++&)29%zZ77;40pH1tzKUvELI6Yosa^r-92aswq6Z`4{}- z%T1=rIkV^nt5@R9UwV5Zxpals;3qwQ^<+Bj_vNJuihN7i_sw61`@f97c(eazI5@av zT6HCeV5V4Nb{`n%zCMGB2=$~X0Foa(h6{<|1wxxFkB}qCq@rMRfi`#phANeK?R|zM zGR+vTO-`gG2nl*ha;Qf*Yds z%&}%3JaFtE!4fG6PTRPm3(NMf7wco&G@9PK>|*RjhYYqa%_Xro={tDxyjp@fjy4cKknU#E;yK4W<0QPt0+w#>^#kTkIV2pzuT zo;5fg85*{pO{w{TYr-AEA%J;9UnbOP_$1t}v63L%HW!0WI`KO^oN%p?(kNBA@R(VT z`v;PL&`8hzVh8!UGCkE8 zCZz8vAtg5%zZ35>cXO;F|Lc){n(XmI1MLo3;Z|v(X`0UpVts#x!CASIOJx`YoHn!i zqQ^b9lu5bb*8U~m;ko;RKZQe&zEm~qe&vRh$k8U!32~`h*1GGm)?5yx#@DzZqMIh7 z)7iXa8|H$Up!T05QWzP6s9&ter#=DEAz${o=OaePg>pm<{vtfvKH$zH?21DaM2};X zt3=qkr~w_wsm^1j!#$c4x_>$xBsd;;cMUBIB2OF>G}^2BTzYlX(wJ_rpiQroRH6ZQ zZ@Qi?8k414)}Y~N^ME89|G1A= zDO@3EXxN&!T8U}ha=4&pKTfkWm>n5=Y}`@f5I%#pg5`WzUx0VznH$5kWclUD34Md- zjG+9NV&7cAku^?n-3p{np>#ONk zRF%z@#7)*sRtlHSs>(Nef&oRQ>jSfCwd7o`Ki}0y8dBn}7%S9Gl`6cR{&Xik#wt8U zj!HPQ@F?Twi~8^_jC&w?x`+|ygi52+GeT(?zWoQixyMLQTZpv4VSUWieL{rvkWtcq zoMXogOS0EHr{hzrjRne7VKEq4Cs$xFc6<+4!aRY)wWpEK^D*qr=*VX!2zw$zgVfjS zlPl4S_h?nobelt?{lu(d2*I`FE_kQsPN>B8cA_&EH_%HW3-k^6SM)&EcB}?se26HD zI@&Xs0$;I;>RjXQyY5o4rx6<-5YUy5!z&4A7_5L{$wyO1k$=RPc~_t9lA|8fpaCvi z(eRnKwAPtGI_1aWa%npWOlCO?`LF2g+tWuz0zf1yZ9CJn_Qq!bFfAHp>taSa6y&r^ z7NJp;X9u=)Y_8uVzE?q=xlFV8ML0Y@alTjd z_tP6*qt}1TQo18ZuO)Wn4AAl6t--vl54lD)nUxURmXGq*^P8KF1p4aFfzDSaFXjkb3H_pW-&@xw&dtS@D(ZK z1_SwlN_JT`KX68XC4Aub%#6bD`h70%bKpgh(K-+Ewo}%q%rSkg_LJk8LnDC<$!2j; zvFMxql&w^4TlDl!3s#C12wG*4gdpe~b&=wmHw-!!JV)lo2#nDBZKkwMA&6|sal@|1 zpncxBZ>WyUF+Sd}`?QuGW9VkblMe#ZdT2{RH^V^TuMid`N@ET$(l%hvPj>`2dx(Nd z7rNOG>2}^24YdvSYfxriRGv>F5A)Vm3 zPH~EaU4kVj2S3)KlvPp`MFjQ4|FnynNWeZQC?J-(5aUcmb5gI-w52`H;3Rs@*f^|A z@<(2G|JX??=KC<_gH*(&hx9q|XJ2@`y8^R8S}dH4S0S>ofXNsw(zDhQeN5Bg_+ip# zG^6YPDP9b^#%eGq zur4`OwH`2Tk1-ZdC?}dWF$0fv5op*_6!;4TVFK0jZ$b4vu0Is_Iwt&Pfsbras_#3n zwMq&e*T&`OJJC%;9_k>M^D##n@qEhB?~uO^U{Kt^$?2~E!Enhp5GN9iu1dnc_LP|w z4ozB2r|v|M10Y;=h8eQpzf^k(O6iv0&c`Pd17cHs-C*HqMLFOq^KwiVJlwtVpcxuz z$teUsCdy7o<7A(3 zOEjZHp*uc2oL^zI#L=|88aKeDuf|X@x4t@@R96r%_kP@5Axb&@2fu$cDZvRjZLW^1 z6Nm`J<-k7y(CF$IB9zNH%;(TI~8 zd}n70e~tl1Kb0pCiPYxH(V1&gm#bY$|4T91^6J+pegd zTmMUa6^>LX*S%}%n-d6vQ#O3Ab;_YF`SVS43mV=3W%uA}7yj0g-$T=-Irr9d;mfNx zoAAcl@%!;rd2}}J^Zmr*-;W#l)-UMxgx7|*%sd`pVfk0|tgP&jZlJ?`Si*z;xU1T?I;t@)9Z6{5Ho2TpCMVJj!m@)n3-0^sJ-hD_ zHJSX2EWMhF;qVj$!_CMES1qh}|5pjDH11EK)FF1`mFX}lpa6E%#esKO1Et0M+t=`A znGX7^C=NVxn{z~)nu)n{n_oGWJI^W%JaiktU}qp{vV--)gIW*GE_9KGzws<{ek-ZCm-z5=UqUi>+B|jdP4Yp2i@=^uxg9OS^_Wt0jTNiq zJ?CA%>8sf}&)1Ma;v}Lj6H7s{_TU!fMd_tItG)vt3#ORWmUYA6RzJmiBxC{GC-Cw9P`blyvtq_;Rz)HC zANzUhp$(4ty%VEmcwnmdMXs^NL2%`dw%$oIw6-uu?^+G7+pqTke7h6}H!eGrxTT4K^`aA2)dzFr4 z*4ef*;Cyp0MF68{pyAM#vtV%9a8Tkua4fZx2s?Qqj~Mr;i^4!R6H^${0dxFs%Uu0KlWZc4P#0;a92;rAvJTI`6!$Tp#bY^%*cv^J9s8L()EWoKK9*n{~Jc< zdt25WDadJP$$W;z{wl^xs}$t~Q#KGsU}FOUlhFNW65Mzmpx{WEj(9Uoh8#XF(!|LI zu@kB1$V#ix0E6^Y*c(~DG52M3(uuD* z0Ix8%qcsu9?QoQxK`nVwwDIn>Wha3(=1+tw^c=JueXd+sz3!}|vh*{MIPn^{9zUJ73>Dte11VX+5JnLAJE_2rf{3@Aa1Zv( zJ9qCs=!Z?cVq4M(tYBY1%1k(0Vj5>7MFC3OiJU;@wqoKfLfF9T(J<+{F4slp$CKIQ z`FlQnCbIj@C9h-N!{|}+e;X8BrjwC>-*!9x$ZqPIY0m%5D1J60(^Ei{@vQaQ{g7-0 z5IK4EtmFlH6!`ynHm|@AfaV1X)k{N7ClIKo*_2vCy(xoQGMFOkDbD0f(UTQw%jhg{ z@HkxO*MQR`(}_~1L#y^Wzyb;w0%Bt<5@1Ac5#D-9NYT=vgoMSOcRL}*>yGheuFpyQ z-nwWB_@7?89U4P`T$f(Z;F@j5?mTKYx*W8gZ2X2;cmY(#`09oTR$H`nBbiSA*P#M+HKNV^%Wd#He0k?_H;MbWfU_f%{4rppG8J2CfFn4Amt0iRd>*loF9`Ij{BTs#aW|li zNvSqqPN8yyjFb=ybg;i65Up1dc1KJ1%$(=dq|oj!RGvL=uW7<}3@YvCgEeNQ4OymN zuJ7!7WN}uMS-9qNF2ICzK9TZEaQE5E??n;EpO^FyYlluQ6Xm*LEeKeh({c)>U8@f= z5UB;j85!iEBl}Q{q0EYq49JEF^C(&&8~HrL}-A?ne+(3q9p+G#&PSPGz9uMFMkC!6?lQ-Mqx}o z-F5c~$H#%jFd#-hK!61fjtENaHgXY-76aqB{k-(&#ssbh?fg(bMd1^xuVKlZL2?h^ zSTJtde|3e)JIN`2F2!q4A}?74bbt!w$_l^6v<%S4{&RWwAJrlZ`ZJ$LF#|%uim%n^ znBC;ox}ri0*9@a6AZbNoV?r5aeZP&QtL7YO;zEH1K^LGtoIyi$2AMmaWo?W|!-1;e z1i;3H)IpaaCkVprZnt;VF&*3^NrP{G0@AB^tF13kjJ2N~>~CEgY(M1E82xCD+UmipqEg z@iV~mfi-n!z+=Y~O{C_zQTUyTA)Y}pDjY~y=Sy5A3WcbOf_H!^OKBKnC2%?+OMz7F zr2x+MzIeFx75Lw?wc6GY;&Cw?n%e2)4VzqEjvhs~TEgtPo>>I#ygW)3u^KOW9% z1@lOh*=;3C6mBehrkjOev7BW#c{zXLgcZo^>g^o07loD=TFD`Qw@RaXu5X@SV@)T7 ziYIHhAMAT7(-&~!old@5M>(UZmqyyW55%wxh8VDg^MOx~{(QqX0e zof!WIHxc_*NYb3Kf2I`}*2(FEt~K+cwQ7~Xy8IcUE{pNo@dvVizlT_NT$Wy)t@3m-5iXd+QGqEuizvDM%tgAzwWO00#5L2eo5Hadhd;~9QsIPm9^gB)w|X! zT{dfU9kj%@|9xClDJBn~&<_w~d z$t2{aM%a;ha%o^DL5)gKXLY1c^m7#d+Qlw(gH-BeN%rRBpBx~Cg7Jq`}SivSz${{=Pkc?e7yQG7I2&#nVO^NTWuB>PA6G|%j zN1ix0og)uY`P^<;6M($o++@+sKQ1kng0uNZn0{KHUy@4>?;vg`Dl<)TyUx>Nn5v2HC(Z`Rz#21e z7w*G!E6XE0E{|TvU8hbFRIf1;Ok5ibP2EJR;S6{V?9naJ=$#uD=%TPbOEeqQO|&M08tiLB7Wd2`T$iSMJ#CNU=CqAp`o$) zFjM9jIHR&b$?AM653qNrYS5nr%6U!D(LPTM-jLJFrfB2L$giC!!}TkwnhLz}mMk}#D^Be) z{IWvDdReEb$PexWl|b0Hvv?qSePn+}vjGqp*11Ii1vBH(T}ROnieU`7-qTWQwvHQ# zr5GSbLj4j%K>}e1CdN3tJ}?-DSonpxB=}!YdyCJ7x-*KAx~NRaOxLDnX(4677&z;+ zq-sYJh@kb^Qf4^8U5D`jBX)AgTVG<^8yZS)#V)4cql5#k5O^@Ol^+aWX#JQI6YJ~W4yVuFUg&NRpSOZ(=LK<5{BN0o`P(`SU&MX%rB1%aR%*N{^CLmM5|(bYwN17!O!ygg53dH-HbLHatyw~eWBTOczr5HTRk;FMs43!1cSv+)kg-y9TXlt1=KDs zW>-+{W#rMYGVsqCDLo)0HgH;F8u{XUw7L1V!Mj*-eiK+51XygmZ#K9n;O3o34KN1x zU3;c-@Qc43&gO6)jcDERz#)J?$O|eWS7g4-7lZ1lC*FR@KSyrgu9Bw#+RA2rRI_UV zpk~FQzzuy%jPZ*{_aARRQ*i?Dq@rIOnz zx86U3sj{5EFO371t*A)IARRAU=XKleb)&X+I$7dw%TEqGh5pfbLykn?XX|9@e(dCW z?UR-)F8kg)UPO|yJ7|Dh8C7keI{}J-D-wkQge~?our_Q~`@%?G6dv9VbV23#4@&Bt zos&=p|G}8D0RfW4q2Det_8{V$!pO$#Yu6Jlg3HW+iJ3YrLBpJ|l+bxIxyLOYIZw>z zV5v+BjhumW^lhc1m9UukWFp-IENO}aZ7l+b!ljW-vj*|J$WNad1c&pU&DbBMPc*}f zXYbKSfbKHFY$1#BnEV>lbSD#%BdGdL65xuAcSapCbo^4VDe}BXujzH+^o!KYP9UzxoK57H}l4qE5ys3ZpmfSR40 z3IO7~(eod~fzyRL=+Zkud$yH{aMV=$w;LO+;ev)DhG?_O0DiSZ|5EhI3_y=Txd4ZL zA(^N6ea%SKOtiyG5FzvN+Ba6Fwqj|9TxRPUs1)n$!$kWoMKX3>|GG0IIj1k4-vndZ za#3sAT_Hf6A_=k|PE&K%pDBiXY>gqs3~6>cT%aMa5FpS&aqDb+d5ZyGoAc!y%g$)8 zRwloSsYJuJDMgxI_1C05m>WsY(fCH~aj@Yy4l#@IYl5qMjQ*&v3GVW7Ya6{L)JaUzGvoZU| z>DLk}eE0r?QSlVMId?){k-Ip!hHQ zgV>0iGsXD;HAJRTQn6KPc?r>XPWr+1Sp4W>&Lh*_w%{O6MnAp;h6crAAsw zYL~pXbdh6mpPTi~3*jfOVZ-piMQM0R`;%{q2=-MP+~p|%RHG_w^NSLoS4~S!)!P_H zDMhZ(8Hg*i#CnaiA?q&Dw4qjjG)d^nyS>`dBv$f5UiiTB;uO&cF`@)rBw&n572H+0f? zw}V7|%tIJE6>2#?SuVHWxwSY*WBPRF&U%ULMNfr^?Isgbo6McFgz_hmuN477(=_el zkCZlQ49o4r-WD1g+jcdOeBHO~{^jY`@b9%mN^iXsox)OLYs#)070C#3iWY@-Aj&jK zG!Gga{CAED#~+%%OT#9mwE zIr#qube>w?X=@oE%gzv*Ox74ZZJdwa;JBNzY0qHw+1{#6qw|pBB%Kws92R~LgU>Tj zE~~nf%$xH1)?eMX)t?B4o6HJaV{@3$D4xm5i;14%)#Q&j!)@9jp+ZqXTMBD=g{4&* z$}@TT>XiiMji=|Q$n7`jk2k&jU-}2P-q;#LHCV8)G$A}8e?qk%tVhI8nyNwPHQ+Qb zstgoE0(I_+OpsTHHN>u-T}{j51yn>b1(BKykR!=j1vDWDn(UUHp)(ULz;jG2K#(Z5 zYBAE6nu;{lf8(f}VzL;#q8GRnN!NU-bR6aSB?{dXA9({#@(HS*xS@w<^>5X5Iv&>Z zlg)Cr@t0SdkY(L$!h~(g%RF91{k$MsF$@`dennk?l2;o?`{nqcKeW}UpoSN!Cip1- zX{t~y&!`TYUy!Tl{zCOtIff3Np?X|crs+$r#-(wH_&$94{KH!B@7*~6(BB_#{Pu4L zx5I|!<@AT;S=`M2?Tv%}ZlX2+ecT~By^tSPeDm_ze_?>=v_5**wUT1fygCJ*iccuX zV65n2Q%_;nLX+<+C<$6&IZU{IT1}u-W_O?q=QSwp{eY1V)7KF4-Cz)eS33$e80^?r z=Y%~r@`L@LU~`EiIx6mMZTaGC(=azgCt1;bU7zDnf5M0Hk8g;i@vrZIs!g3Fcnee= z4zr^7z{2ycqBmaRf4;AO!+*}2F0o1=O_8U^_^JuRg1{;&Wkjx|^BS!_71_q&Se%eW zguaoA+u|pFgzIdUlUhnrdDs>QHDU@Sdr`GFQg~54#$zw|l9Qu@Eg>WeHk`uv!Uw{9 zAu?Ier@|4?r%i|^Y+T6|!|Ow=*Ysq8)m9euU(mN92$8q>b_1z-WpCSw#zp1dX}}Nj zbQZ_QzEK6P3XGpKZpnF%vk7l&KLbB{erl4_lM5cyLF{;We^v8m+6XGwl{PjSCRpzl zXXNxbED2CWf%{mh`a;UeYLh$W<(U9@jWO>+gSak$2Vz4|gs`4X3{GLk^ZBYzcPCK| z^QdV|41nz2uKH+^Q+D0>s`X44?t}mZU%;SCw;V2B^J0GGyXvQBUNY!-FlnJ5oPoVO zEz`g#mwuRp5Rmbb)0Ek?Ae(4~fr5iL2X-bN%LmxZAkW8~t`l2id z*oEzy?6_HU!oBVHhjKCKp*Po&8IwvJ6F#qLvlbw2B{1M_XkML(RuEg@7#fP#43z_* z&!aJ#HRsDIW>w6oI&$pT3a0=Q2z6YR__Em9-k`=@>$?nLrx>+c4n;~JQLgxF^x<1$UjY)%3pk^4OvA{HB zP>~%*Xc*}KqZ~0P(}CPdgK!fxZ&ozFDN=z?5woE0>_r=c&AB7_PW$<<6S3pOzK10e zfi^}_($uG7#tZIirVUd%8V*ijO|gyRj0?8r0OeC5VJ7HV93CawMSRrW%#2Ael` zPicdbe3%6|^rG6zS$l}+8OHx$do27pOwQS$=Dq>5ls@;bQ}_-()q2!;r7Lf&SGtn= zkzV6bwv>lIu&@H9Z6pC-{TLx3cD$OWuqE2-8d*_}Ni)C$7S>%}r;*_GMSXHI^$Q14 z_G##UIIQcbP5&!`Ysf-&5%{2OrQC3gu`W?eTiKwIb&2q|jj5vpgMEKY-ZsY|c158Xx^f+i(8Qq_; z{gGDn;b^*?2w;AVW^RJ7 zuyzQ6?ZAM%^AE<$>hJ*Z5xzNqY;S&CyuTet8u#z7%#+e=%wMiAAgns~3j}S=WS;sUmdGfx@?biz>Tde-Lz38A#7GyuD2~qwY9(BhH{s8VdQLEcY^63)w>a!hu=CXy zNBq(klpMR2aZR6k5ZLECn5go({-X;x|(Qi<($7h$b?9+U}xC5di!5{%xN4ULZhvIjV0+G1J4=$fQ zd+ZhrDjMBrt!N~uelBo*9R6}7k@sL;hoBcQ49LO32p)iGeR~%h8V!={iGEeW4zoF=ER1EAO-@@f<&2E;vXXqmpjX&pY@NK!h6R6TPg6m^!^Unhv(~ z;qnBs>+al*v+HF3x;)mEmX=@-{xJT5*aVgQ5@Q|$9x+21Vm+r3%-;6)gM7Uzep!kS zh%(5XVKWGP!!0L}D`>Sw895^`pac9IP@*VFdq4;5&GQd8HC(SJelFZbihvFRgLC5a z+layG{SCWXd~SPtJWmWjkKOYRD_29(CLcc9dN@#|_d|r11FPQ|B8gfsci80u)QRw5 z-0PzBcS2?5yL~6MDXGIc;3W_y z2`)+S4Xs5u#8!!H=Jk6R*FUFQ2sbXb_oFCTt(hphMkQ55*WZ-VUvbt4KRU* z58RPR_fzGtoj*n<{O8DMRvi3aFQ~29D62O~>jp{G3`=bmuG=AdC8rk{svqDt$I#zP zLaa;owEO@8&y8i@k;u8CgeLdYHz6?H#}-GgpDKiQS(ofmX%Vk)>8_aP<>Zr7I}EwV z^fP)ewXx&8?t#k8q#@TTI6=pt4Fo*Jw20y#kTHm7+uWxSlnC+jQ(9_x70%fse{j5{ za-^fO7-^=oNhub_m430=K+sjIy=jrGmo9|M58Q(uwFkLi^e;TH1v>cq@igVMu$JDa zbovu1YDk;+AIs|K9f(&HOT}n_o0tdUi;GH*idqLE(UHLRutbEf$Lv(l_-vIZt&JA2 zRMKtzcnd+d4pE}RlplPAk9AJP5Xdz;2_XUr5m$q-X{A6dlN>G?wCJ4RmLJAs=euy6 zm!#=dLKQ=v7OfF*GoCh@BTPCz2pR8B<$E2T9r-l}nC`M2bsLJbw&n3cJG1349=Dd;+gy>GRCLEFi1A;kWrWGJiTouX#ZqzmEqe)!bc#$_Z^*s zHHq`X4(+~7duFW0&EtXmMO7AMRGHQcb42ydg2r_Oh#PmvZbpLmHE9z@QU^04fHXjq z&Fp9SYy5%#3jC4s{;ty&L!0>is~GPA9{Eu;n7;~2DaTGD%AO>Ik@kI)!+CD@4T3I) zIYR2+{XoqYTGejsnLmf&w~P~xaNPJILuF3gxog;>ViS8T#X6R4S7%)#A}A-XI*A*h z`)hTc(33(#kwqWxjxWllhF4F~&<|qJGgL>PPc8tSpy@_wMxe#OvtJk6UlC6OK#uXe zD((-r@XT+}T<#vaT@S#O;9})Et~AGqZjb;OMV)BF+e1j|1L+eq*Jja|ZP6ei)Dgla zoN(Oh81N(^YjsYVq1vhh$C1by>F2DG6hbf`RS?Jusx?S@(?oKdL~>NZv>3c5{3r0N_rH*KT-le&x;mY zvYzA4sw7hbl9c!+#RAey>@rag=~t`)>h?F!>4n6j!XNCTav_b;@#AvA;dp!iipnd; z_c)X#fvKkL48ypGVLx=BNgtBWGISc!lWUGNh}N8)l#|@d`k&3rY}EVw%tqs~GrMk$ z9(x`_tc8=FpjF0(`iFp+4B^~BIQ7pttvEg2Y3C0*nJ~H@%C=zXmIh;zKlwmf>*)B* zQvtaw1rn+Kea^9N9)90On`SoxdHJ6l^{ZNmS*^VR0|!{PmV!|nT< zUx?!iiV)nn^JqKNjM>gW3CZPnYHSUdO>_c+{hYbBXic5Ks%p&8WU@FV1dNNaniEDj zR;R{k*1907HC8m&!C@c`5;mmfexPU4)Qv*19s|qKJt}rPG_t~T7N$>#kjNHd2_?$wyZdE zu5;Yrjy3EUe*}7Eqq_&vpcd6x>2M4C%y-<#oQxdvM%wB8p~s3OE7PZv0vCVQV1GEw zEitZ-F`LlWX9nvW0|NlzvgBBY@+=wc;Oy8TQ6}~9wzw`jpL9}r{fRrj{#5Wo*E)-w z@D5~Z-k3iI!S5is6Ggr+*|8HC<(uUpm?MrBaXB+OrcB0>?;rG{`1R>McPsvBiI7|> z^;s)bS3d|5t>y4!IMC`mXpZq5J*Ktqp@s+^-#sh_B_G?znxuf^(6a*|J4v4qv3mBg zI}%d~EA0iciOJrbs_~LA4M;S2Ip>Iyq`|mqzNzPch5k+bA+jD*8XXe2vGVlX=W)pn z=?BGC2VOmhuKQkec-cu&Tu`Odo`;T0-uKRJ|y^5zo8B_ zEhz71&aNGU}<@$Z1b#O@z`?tm_m`>%E;r2g!84^+NWa|Ft*K=6Kv%QSQSC}r$a&Ucoj7AQP z1Q;^G{%o7vLlkHX12i8qT?j;_LZ6+Vw z?xdu7-3THUpz9zip=|3Ei1yY^?suc$bd7D+Dd^f_IHcDYSW}Z4BCBOjev13GV7J;AZJLKo+wV`{kk`a(aMg3ZwVQX1q0Ya;o#D|*wO37cAKZI%=i&X1al0-~0ax>y zZQOgb{oujFj_2X}8sjxUl3;I_7>oUb>{|p=pcr4*W40jqW@dh)-UF@9O3bksJAg*l zaoE{zwfD-l+ved2al@sj%UQKh_;Xh1AW#aj^*Kczl`UT6VXJZkpYP{#Tmz?8$H%E) z)l%z(ajF`pvCSw26#*F~y^Z&+8Z?(Pu}YSqs-*JafFq1&b-N%=Z*S^37H*jdQ@ zoI)Pfk*f!tC}_=DuY@Ja(hA4U^^Km$6;oc%FFC*rTqe&TM~Hf_u8d6Xt^C@zZ5()&di3d_2Bx$Cs|ddFs2>CRct47?ps*Y$b~!C6 zdVxxyVGZc9B#?u)2cC5tHe-o&l39{LW_PNG^2xs_L9!~e!2r4wX`O0e)=)(Aa2m!u z%{gqQr+;2u#-U&|x(UnZj6n_D;8@=Sl760PE&n~%Hws3AT?F=8H(eY-Xbb42(P zfOvm~TYpRh>Al?_YEd<^C5L9@BnThpm0UnXATmI-(r_KjcZd*!8h>3dm;~{KZvh(p z81@HSLyu1DcZjeAD+XKL$1@{h)A<7A;h}7zpUG>A3l-fmutR;*^PRBKA@vky`3C%; z*i)1f9)HdMCFI|3KYj}U`JHmV*QeW<8irhkrk4%Rd1Y$aYxqO3t-VCl#J0oD>Sn#^F1-BVyZ_>?g92%l;tr05A{ewCa8SL>sYOSV=>e=8n`A(IjEq zB;chQ?{orWrJipga)<8L6$IQ9-deb4L@GXT6HY4!ki<_r^y9@`7?;-Uu0bY%XLW)X zM(0b2wUKc#jG2rx6R^6@8baTg9oM4ALp&s78*V47o_yDEJFb!nTm)LAX% zj+N{w$QjHt;0Q*SK4-O&kywUqJjBHp4<6jVllYpObTeRUi(1qfR!F-65pNG3CbHvn zg&#}#JuizVS-8*P3RII_I_p$xAwn4_GP8+U%d@cv-7*)X^hzezBM=IFo=)k55W)`s z4*<=<6a;}@Fb`dRYSdNNu4pQ$=3z#h$1zr#O6s7Id5j;RtS3s8t+4J=gBFa*z_=|O z;Fp9vLbbe8gN3$$m|sHlB8QowiTa$K_EUm-Bm?&mmiRhHZ{Tp)S!))1I+$**))AL| zpG%oPr8;IZ7_3`yeN=|sLpUk)ku7IvHTKDi<^U})lMt;bN+i@+@{kqWNb!Q4(3DCh zOl9?lJEpct2sjATALUC~C%55UOxAJ+nf~o%m|eUb?8!RHG059Qy|?wz)WOYW7y_*2 z91Re|k;h>@gUN*KmO%WJbk6=$4fi191t0PSj-eygKx^fM01A1V3X=20aF_y=#R4Kkgy!CWL%j^C}WGQzw`+mOKkO#sh@F+7XwRp1OQs$z-(=fcSKK{Ue)x{-80+<`RNdmS6*on8@%+U58Oo4iSx zTJ`xRMwe_dpzfw!B?J;3(+LcUB!+IhEA>P`6EE_&Nl8^6YCbB&L&o=r$5F1!#!_>>V~gKlxG~NO*+$5{S>@PtCZ_7YRwryd)X9l0?!BRb{A-EKMe&QNdQ`lGwHTvUaQn`1 z`;HfALwDfts}YsjvAb|p9@n|sfP~gGycd5f=XF8$+cS%>*I8xC`C2SFk>6Mymfjrp zawio-OueM{IV-s5z@KRQ9)?}t&(cg-i(ClTAg?C10&FcS*9wllkzYdJsFD;IP)wtU zGdPu?GX2?@Q#cr|-wk|nc@g)4j|QvbNr;5Kz*dX@e{Wyf-PUoW{T;sogflT9fgmM$ zQNS=dvX)pgw&juJiF0U<4oH9!ED~U_NMbDh_qU#^>h9|80(3I3=S)Q0+qd_ny6UN? zh7Re_y0$x(IK%>VcLvIzs#;Uh(W=sf@!lo%wA)twx?G5Y60(?#m5Z+=858Zq8azeB zwnYs*pBUFl4tJpF>+I7@CS#A8PGa&AWj!1)u#mXIJJ8r1Y05M*KnfwN5b(JMnZC=w zC!sTo8xU7q($dWKzjhslP@n9lzJX=#Jy)TE1Z&R9*gr+9-fI#T3zGv}3&6Lg3d4hh z-hXj-ADr_4ad&_3jqh$s*@(IRw}z33l}fWKOg+N1xtJ zuHH;$6j$J&%4_K(z&3OJ>DM%LqYGiU8~87UPO_Jh5Js)ic zyUMrDmA>Ql_l~fC@zn>eS7R37F7o}~T6Z+!wQCz??D!B(z7^+V_xS@Kp^!vC6M0Mn z=nOeSd*b>(b-S&$8MSm|N9Z5?eNeLNl8&w089E&TZ34@=m^Vm{Js;EjIdbognJHme zY7iH}R-oV!=&1~+Fw2G#%E}x&Ep;7Arokl7!2WphyObRErQh<6|Rza0C znupK;q11?uuCxh-8ve#62~}PjsyOzGH;M-ATeJ**S^UD_(d!$UivIOtu6s-i(M#N? zUxt9yv>l+sfDVb4x-b=p&n109d*GhBUudZCf1nTDscSd3NMafRTS+k>)py2PhE@6q zn)igy9=ngtimFi4#0whaz4JRB`LFsgdje#j_yZlkDYY-B6>^M0ekwi9BpQ_!y4aFZ zDy|1Yl^ppb__iIjJ<}{Z_|Kp{{OQFrT&77Ng&q7Ig8ks?!kYY2iY)8vgP0@R^fcgN z3)PNDgZOqZxHq`#8BGV_=(nHNi9DiIo}T&F(MMGSFU?iQJ|374QV0O%sM0!P-Xe1el(j3_ll#A{=7sWV_55tc7OfOG!bYE0r7(8bK94%=bLB34 za@mvYaoL-)NmO>)V+3t#o{l-+bSsStw_^t(Y=EeMn+Z`Js?$OeB=aJnSK;h7t~cKg z{GGOxzI70#O-r!4_k_yo2*#C-W(U{ROCFK-qD}mR=WE2tK3y;6f2nc&FBn(Iu#dBP zDdjB4^_31Jet1f;lisz*C+G4EE;n(s)Tqa85w&qXa{=@(%#cg`LFF*%pSZSsT+yMh?(~c*p#~pwyW6)C_fQB0 zPo^{PqvJ1{22~ca;`p-dvspdSk5VlpN69RBCayF=~+_Lp4EiQ$}~;iuu4NRjMPO@aO$N7jS;+w;AuCUyNBKm+NV-z=hQ8Ia`6Kn~(%k;~@F{A3lBcZ^EbRTP(^5m!XDQBjF4Y{y5Bnx>@E=1 z@Ju4hv)q5lIc6->jyS>mMtOejRH&U>dmZ5EsS~amQEH|E1xJq zws%He@oB%{%9{XfNp4wEXES;^c@rISqKRQUK+B$WfX2|qsvHSXcB}b!gzt9oD0@Qp zCu95#CQ65+V=dOFbOdya?tpLeexu~6WM23cXM6+31mq7p>+?lem7wHD31CC9qL5oD zWF^tb$sBT*Dp;dT^+Z_e0lA(J0+)9EccB@}BeJO-S8uE|r5F!Fm0e)dBLNxmcf95B11f5S~m*+{rf5= z(Y#%)ZWyD-ZB)FKkni24o{oN3ICCL%<=n=S4yT=o87rS|+&&D|t0OFW?jHR5kDtGN z5tcj~pCwywW)6_=34|?Q1UsIEJxaK7{h90rE3IvqaT&*%$O+c?>Z_8B4J5LIvP!67 z*z6_qbwBj*^TpEfKHAv zxC zQ&z4$562)rE&&={zcKTP>pe+bnk#+Nf*x6U>L7?AmN9bhT8F2I8Np=Fc zsTWjQ{KBzS3%0;(xsvLJ{dT^9D>yBg8PMgcNUow3XNm|gQP|J$43e`ClG5(`*fcmDbG7ln%B>6G?z|(NtkoO9V`o-3~P`8gZQrF7uj}+(ldZ@%hVF zokA~tn21Y}H(t{=A~(5y02Qme7`hOX!0Jc~s|343H(t0jZbP2uC10ZsYTCgh6f%wJ z((sImH&BPvV;#qZ?}F6_#e!+^FqyPgQY9M(T-H1J+oATR#Q?ww#9%ets({x`kqh!A zI@6`%U61)GXRJ9Ozy8A1k=3eS9ERnYh_>A4sChT6n+HYdmjh||G_VYNRC!;nz?OwB zjsZPhUtC;~ee-=OV;4V?3=7jsnJeNXL^^=ae*}$&+|?dWW>nVa8r7%H*FeP(XMlRI zcMZmCIROte^$)!IomWoyOk{TQ{t5T6|$buo&0p z7_aM`x}V2PgwPOC!H+d0P|09mtKf2TGU9P+-Yy(5jPbe_bp$z0v00LKl-cQ9w)eYW zSV9vB!wCoeBz+i-*2DjN__LT%Vw@~6`+T-TIS1sv{16mB{(hu=<99840L3Wq0Nyy70KQ|;pI7JJlc4pK7qgNHAN(3Z#1n?EbE#B3fbiPmiU0G z)0S?tzvuh0^kQvCF7MVu>EU=q$+Q~g+vs&=>>b#&RSW(f?bqZ+` zOjnLHQyv9|Ov>jwqTb*A9!Dj-b;L?VwK^4`3#q3+uA+nUoYK4GLw}1VDw8+EpsTjB zv<(Q$U7OqBLjUsFXzAmSkiLnnRtj#gDo-I`HEbbo&gUoiGN!&W_K)<{Xn8d|O&=<; zj2^h`fGHK<`?&kz!-rj9fp)1?f>{_Jur{6)*oQy8db)exv}EsAXep6%khN`Sb$T*S z*bLjs=T-ci&Vq%&0^{evO%X12oIdBlOdqleACbtI>7az~%HF93a~P>k$Yc?E@D$d7 zWg6a^u82c#VMrDg%`WO!{Ihon7*-5Oj*P06G)0)^QzB+Fq&ZTVo~lCeag7wVu2l(=I8If>lFLc9>wJr z!EBBVQ<>5yi-?x)zb3@$v;>J8D$E6@*d46Rk_8{WCa&uChyUFjuXGjf5PMIp+8sUW)yhdhK^ z^~weH;!4LHSUlVK3m;rN&Grf4dMti&xM$qYFq-hOC~3PTN2CZLu30+#&zH}C$aDV@ zuYA@t+=a|Oa_wwhG!bf}{FJ#oL~`Q$pM->GUfe|05-~I*BKWDg3(((`37KTo^zHH! zU-h$$^&S@;{+Om9Ssp~|O9yLp$SfG#L=)n^5%@^a<{ zRjSj~@!})W#9hS!c(@YY#xvqF0J%(MzMGTFuT!|hc8DzPPcOdnQQe3iWV$PoE-Oru zdj>tH)crgfgI;?bpbL75-1E>Kzu!tKcR;1ls6-T$yKG!{ef5g|SriVodHWrq)G+QM z=<3avQAMgkC%;#YIuPrX&WM2Vul&g}$<|NjjcsmfY4)+~tfIZ(h$3z>6n2_i9ls_I z9g;W0g30sO8`RQm^~L+*BY6j<`9RY#tx>-qfbdJE<&C-)j4w%U!fM1QD^PR5MXMd$ zE$F6lZjg8J&EyF(E!B!idXs|K`Ri43F?R3Nr=jHQaRpWCCDWbn!ryv0XPRf603Lg#RN?`ysxee@lC@%z z!#-prm;;m$n+wF=AkOYL>9>oACt}O(koa~wfv;hf&r@i{Kfa9#_oX_AMcX8?&R_^8 zaeR$bnppaM5%^f~txZsFG26w9X!{t(?p{^AhK~w!Q0K$YK0HiAd=wM!B->cy7lmz4 z1I9Le;XyB4abUExs}!eIJTkyqu&FAA>KsY$9Njri5MRd}Zw{~EM&4ZE>( zq0vmMvMujM8r#P>+=(w}Z7Nil1YV3j)W`y8LOwh{kLToaxS2B4M}QAg?m=ZhMH91i zWgm?=m=VJnIiH+q42#I@Id%;hpV7v(bgp6D@lQNV4GU~Xn%guzHQU0Tc%)?>Z+cDJ zwm4hWr@sB#cuQzZr5jJO*wuW)_`n}wF+~g=ZxB&c(XOydmj`XGpM6jAohbRttu9r- z|DK*FuL{jtb?X1u9kJ#49G}s)+2oDgI7gvjZJ3QspC@O+pEsNc0qG&0XtRx(+v~{a zpI5t3nYf^J-ekd+$q$U59<7AQZ~+$&_$)*b!>eQJ`|U_-hFN}8cnqWWrBCzwxA`V` zVRV@X)L)tLFF^o7moZ}_Zf=>F4oA!*-<68XBeg~zH_=jE{~yolM=U^P@PSBAal)#C zFp;I+-nL$w&wLwtrQ(11gvnM#f@_%Vt41VQcBLC|CA+4~-c9xch7SOxvhn-oK28a= zqPVJJ?;U414fh1jD`c9VbRFo>TeUURtG!?hXayP^rOYQWy5a7@ztO5RjX3kU6o@Ag z*YbsU6teSIaLVd__Vt(d?hHlgC&xT0JCC)?XrwT&h%b~f7t!zcQ`UNZ_T~Mrz8Vq? z;cL26F4sz->(cKsHy3GWmQj}KKnP>al_-w{f}N@^#WUpI5XaM!SZ>K=b)_7=wEuXO zQZK~~of8d;gC8J!)|DXI2yW@>i@z|P2l_|k=b%i@)<+w?+D{+4 z=JyNsKFVQluam|Fc}N^$OK0#F#Q~cU*a?S8>qxu8G($weyTAX1sk_>s9UfBipsKqk z&N^-1IZl~Ytl#8_Gy$l8J~exXfm(*EuyFT=R|-3)-q;?`-{~ADok1#iqg~TY3_1%q z6LVBNR@OTN)yU7cAw|Dk4SA2DWyggkP{#ph{aA9Yg?qZpJyLi=q^>9lUq+mFduOMs zITg8WF2kGvrZAr=fiLTBF^zC)m2vd$%X^YfR2nNe`4m*Ud4b6DM% zQ`@!AIj?v_0lhk@e5+tm6OTY;OVd!E8EcvjET=`}>BwU+;Po^`qmzuLIZQ~ef@Tcj2mJz zG-NJ}93iqhiPce|IOQ69PjLggst)f@m*5Z`>G`tq&X{&LmOp5};Kc4)8riXZ^sC?& z#14gN*is^894&*l@=owwR!oT&n&kh|MZAUmz(sqLSmvwwl@DW%6wl5)nr8PUJPQ-k z=pKy%rTKwmkKONiToiKy3lEuCXRXu(!9Bp18k^Pk2oB_}unr4{F?Pc$b(+nyPS7i) z?TQix^qFG3px$=Jd|y40YX9LV5- z&~~`|4@wgIXMg(Y{@tPD+~BBBLlqSfq=qEW`1H1xzV%Qvg~> zcgCtj1+Y9;RDZe?lv83+G1{isXHKM|eXYvONR&mqP z?Xw*SPR$-ihwy}h^iVbLi&Q?kiCTo+QD|k^;Yn{ci3*VXaT%i;`vr#qD1w6XA?XH$f}tnB9TLpwlyJm&mc^9|K)T{ z^n+BR#qs9>?zT4#^k0Rm(Q>+2Z5_h)Hs5Xn^k}==jl(=Lyn`ddP^8}7!Zg~cD;7lw zxJw<;`C%SNRw8QOqQ=OOEZBO?+>pXj*;$pvI%TqH(T8Vs>Yna)&y%=we*eJ!A%5Pz^$56L* z#vIa_G$%!D&D}!n5KYXZ@&w(*A>oz<;!V4vi&tF^O{S|nNxrEKkLlhgDInS_&QKN!<%Ce1fb~A@mBD}ohu;*P5G>zY-~$x} zap>n{C72*Y-f(1WI#1M8$V7+jFA#7u&+)IEqt+CnW) zFHf0#HjOkL&bsUfc-~hesMspVzBHYsC>6g@&|I3t`y6gXAN;v}Z-3v(Z)@?VzPJBH zQ}Dk3gX`}0*IthratWN_Edz#iJCUPWrwNyGv8gztv8>*O4}CT_F_do85ZrVBZS~JC z%{Q<2buAEKL3LP~g}h(uBrCSb9WWWaP4yh17(H5Y=b&D@m2Yb8v`(ve@84d{m8(Lf zqK2D5%pP=s_#d2U|2C08&~&6pEtqsLG`RE)b1}@|114<7p?AcJ&y$8-V2Q1orSBZZ z;+niP5>L}o0iDhk=#Y4laUq=zKv&-U_nQ%6x(;wQ{;q&$#WR`}XigDzrl8^ywm-u3 zxGp=WCehY+Gnq0RZ`y~q%kqOvuRZqW&ri@W|4LovAt$t^=I8GxejT6|EhF+@{7LhbaQ8EFKH;WltqVwzs08rMoc>2^tZ;|#j8S!7fw}+IyLzFN~ z%qH5lZQHhO+qP}nw(YlV+qP}neZRS9{>AKOYE!k!s*;?OJP7>Ho1RJF&iouJN}20f zlYesD)@JC2d%i)0rIQtAe(`2Ze)5NZ-WfwGnoL(bhF}m#?FZV~G6!l%z*Cr*WAxU} z1lw*c@KScw@bKXLr_S5iPQ@L8FR4SXk=*^%EgezmBm^NMjW`n4UY=m=ydvJ?(p4gV zAlrRirmjJ?gA!jnoHu++5+Yxl%SPIHBjBhap@on#i8r%`FQAHDh^xu#vlxk{Gu|ty92(>DhNS{b zn@T5Es{kn?VyCrBAG3^Ii*0JfSiEpE?26YSsP*vGxb!uw~DahWAl&ybiaWrQ)F_1ms^ zKz+WjC{$~AuXIZ;hAZ4^xuFHlCU$Vef(<0nrppRKa+sKs&IRuwPNJaYO5G&T8iJcx zt?(lAkY_Y`p#$72X8nsP?~hhnS9`$2=cj}^D_}=k9#6#m>p7Fz{~r~%UEr5fss{Rw zKY59Wr~CZ$MV1T13<$@W{2YuC0}+&Qv|lG|Gn1NLF5F5cPVIIC9*0Ob%?P>YNL;!P z_2GH#Z`Y*9@>*h!v!pNPEhYtxi;Pan`*4*eR&KMd=~~QaAY^K^Tep-Wn`mXbOFyob zF=1j0;LnN7W+D=orRX0mE2+%R9%Swv9WitpeZXA}xx7~8-S#lbKUNMuzm zykQlaJFCq+CiSI#E$kI74U9o`y^uy3f0n07M+qf zhZb-~960Meo6w6NnWJm-L^hlm>C&}kYx%_2;OatDL8)CHFRGb$7BF1Go|FSo7E#(k zx72#s1=dpJfV25`-X*rZ7jm4~w&q#CSh4k}L#_@Q`#krAjSHW2(;JW~!-pAp*~Z|# z`+zLV?it4#nBqWT3}PFW9}ZHY5h&BXV?y8Q8A^1H>{oY+sHfQ>Rr~p*Zm!S5`A#n{ zZ&^PYrTHjzz&2mFX1*A5pr&H@gU-;3GhJYwUQoPXfusnGY=`l1G8TIxzDpi8%ZStl zH1p!~HB8UkB?T?f-Y6%$Is@+eZ(FsBt}nl5IIPQLJ2}5r2Pu@XJ4P&L`avA6Ivl<| z8!95L=v_6J+^|YIY|os|nhXGU?GA1SU(1wH#<2UA-_(~LMM8M@FX1;G=xIK@A_x6ZI6N7y4?`2Z;xsUDqwSk9X)89Mwq%BtaE zn|uYuxJ(wW*e*a8{;k9=bQ`u(#fN;$?iY#yqgt0bt0ss2r)A3ck(I|FKanaNK|9u?(XUB2xfAC^6w z#aA;w>Ii0ks#SruwHi*tUwU@#xqnmBa)Wx+d@Dr@6@uCRSq=7i2%yZ7;}8mbnp@;o z$YQh5SY&8w^jb5>xMe%VaS5z$aoFY|h@$tF?!~+o#qRc)q|fxJ9k6s27Qi4P7M)R`V=M4M;>L^!5{kNDxK+hkN(kGW@a9S{mAN6+$-aU?xMn7w zCN4xDKE`(L%Z`>SSsP{5LwTdXB7*o1w(@v(r1{uDk{Rub(oh>{Q)nFJD66B~pj7`| zz)2OX{>SIbRptt4a0LP;a*Z z58ujJ&59hNGO@Gclm-weNy{=xCSb!sC@oZC#*=SZf%KRCVB8&QRlhgn0Yj1U78xwK zCU2P@Mf;W10$8@N#GAs6>F-oF$a`jyL3(T3y?-u8gklMSFqK0zm$xkY%WlzKkHm`l zZx!H`;~<93p;p4s#b~MII}mmpTkh*RI?&^Rmq>bW-E@5#Y*FP8LZzbv(J#~X788Eo z_0SZghF(AD=G!Xemi3OV-tWg@^N(9Yv2H5*dcj2y8(O@?@Aa4($zbms%{z#BN__L& z&+YB%hyMZdbp!paw|CDvzrMVci!jwm%tQ{*g~9M}#Dc9yCqEuXG&tk{V1>cr zqiDiR#s?PkP3F2ExFgYbXqL_{4dT?Z-^wD7FEfUXd$ z0(-slrZ9>)%7Y7Ncl(IDlzw412cVzPjp>ROW!-2E!`ZeUFe~hKQq=VVqe@4#X5vI4KtVw{v z8Rc(OFR|=Xu{Ym^#;2?{V8|!IoFI&2rxn7Jho4Tfi)d3%Pj^i$AyNc$9+Q++`R(CzQ7}JuX1N~6w?uJPHpWU;Tgt+J zv+|G?ge;FyEH=F?0EJ%cjT-ev6>GoBSxgNle#}sl__!)aA$y3AxsV;O`?iETMVqh> z%4ZSd4N2JhrP%<6y!b&Tst^T9m3jA#Jj_-*eR%Gnl-1ZYE+t zPN=)}3Il)abf-Ev@6@N8Z8ynVy|P0~t~+O-Brq?)J`yH7z=<@r(xGN5RU~Lv3Wg}M zOGX4J8OSG+20$@$kDBoC_*lDt7J{f1K;0oQ5k#*kw#yr0imzjf zMx*^fu2xEpYf?`%9UqV0y`@1GO_AUZ>-Fh%n~B^Z;?I&vgM{XX$^Ee0e1h>%!AgU) z-8%)#z-?Wfn-FVpN~N1)pE~?oPwV@xg3t|U791*$H}zBl6OA6Dj4_#ELn8+anVBSH zcf=`WhWi(d;~}~TCItwSi?lT-v9VONP#%P6LXpPDy=Td{F1!N?g z`LTSzy*hfkO)kk2Q_d%v905oIa89SX-rgb&;lr{qa}QARzjIdSg?J8j2_YbBCpJWm zrV~rvcMWsLGw+?nyJUx2%mDXLpL|}C|J?GZoiH-gJ=yu)M4)u`qWqXIV9bn)v zIgznsBm#nmH~%$Q*s7ZZ+v;2Oua9O&#-Gjvy8ABkV&TJ{31Mbg>^vrD0~4B8zeP0>)R_Ru^{ctz9~}xpP5tF4OXl3 zyIq=&F~Z=ii}B*@TTXaQP!0;nWU}I6QL$caCQx9|!wi65l8D76r}!|DRzIo!Q;iaY z;g6b>K{AYI=gnSE*WsfL9&?`?4YIif@VSy&5c!!UH~8g*=D1tLjQ zof@#T3lzN&ju63AkH}49MG=yL^n|XFn0yrb+$`~E301Cv@?l)Pyyo?G%8zo!RNt~r-!wscH7KMOL(jDsUt-s+mF4HWL zVX0@-TsyF%zBZO#>vM%rhuk!EX_0EMk_!61q@D^sk_0!oxfS8O+pQ0vhMYcY0C1H^ zC*hKi6kkt*c5P#t7wX;@-j>(d}NQPS=6%!W}DOEi-O(|f>ocPwjeY5NRTleD2zpeJDqXp7SrP~K+C z%y+~_HS1$32(C>&pZ>B`$vH0A$F7*Iph{jVC4#?@`?71SBK=CF>*1KpKwB22y+(HP z0ox)6-{Q(o-d`nKL5Li0?ou9&MtqKX>-%h6+rMXX=wP*yuS5mb5@#N}TuYbM-(z%t zmKA&70#*3l0eg}KU6o9?)~$F4H3U%#k3}iexFD*M83-?kX;rc+zTsO)gBqDB{E5Ne z(V^DOHJeP_1FO?lAVY-n!Nl5pmD#vH|;Q^9&qD!ujZ~#zvi#C0|l=hXs#{HJnX(SoV zd>08%6H|R7wK3lxSB0Zu=L?f1L?M#Rosu0_Qk@*P4t6ZNa{5E=-F6%fuujusdCu}oi}78X$Lp*drU;AObf9L_+uNS}EPc3# zi6FfWdDDdufw-|BZ;J&i-Jbq!Urrj}Y~!*8!Xf#=TBprI-v>5(^#0{{>}!~~&;M*T zcWnn>uh;MKaO4U7pTEcN<;&2Y=lgY6_@2*ONgJSP8zj#+yM?)@J=#xAMlFyktG5&i zPs-H15IgNVn*jk#MoOA`xXP8j%^>MIF!tfAW$1zB(02`I!WueH%1TWGCarFba}<`$ ze8hyCgi-0{2Ab(qkRT*SA$EVvpn_d^mup+DhGnxYFATL~&Dz4N4didnuRDP^=*n#K zWC9?TBE0DObZabJOfq!`39&j3BJz@Lr3FUQ93&1mt$IRu zyl@6{1VY z4c=xLBEngynO8QGV}4PiC+a{6SLdnCzJ{=!Z5O6b`ClD-_R#Cimd)bcexdhKwW`g* zN*9UEHW-3dL7v4p+zxG+L1Z>LfX|E8x>2$P%bEHuDH?Y7%t}9KI65?QAjevi=Iqzq z+bshb9o!7wt<4(*iRx15Sxi=Vke!;& zZou%e-9L$`S%H$Rteh0@uTT}1%8Unoe^c|F{oQ-P#IL zg~*hlB<=1vo?tn2>HIZ-WqgOd!W@|U(+7WF4a&=5SH3cHlo03;64N~jqoSW=4!jw@ zz@E_QZbO-X1Dc;FKOp-+A>6+aksOHTM~HPwe>pU#h4)h*E^6#aP4Y=NRbEF5pzo^r za#u@<`_kuPrQ23{&AD)wZw@4=EC*hdiid^_M`E1pJu7^QWyLhm3iu6fmMJh9|V<^H!SAPk*WKdYNE8H~A0p#5LcZ3}Jl^#9Z$xEsL6 z&>vLf<_OCT%JvSsU=T{$vY5clwnY1mu^G-r_|JWU4jkHjIY{Ie zMhC{g=)}(?3j!CVlKl_Z7~dP0Y#2N_%z)_7NCAK?h+cIq5G+64op@P0WfNFqm{3I^MmiN?ZubgQ1eaf{yK(ss{x++#wWSA`NK{P~owBJeU& zolK|FU_anmb=Z+R4-RZE1Mv(W6Wk*Hb_S&boY@CQ8$#3hTvD~DcjlUNn>gUFAV130tA+rOD^GcaOvRRGO*uKSoHSHxD z+$^H7$D2?beF`+cHTxF!t{c=#nq59{FT%&LVred_=YO&GQnS~ZBBR@=#)$IMw8f$+ zBfAY5er&E=hteoUE$~gL4=0IlKqj($kz;M-0F`8Oa)jK!>gXiuH_yUC)hbJeXk1K| zP#|@BL^fsZh@65P-|g&@i>+{_E;Q_sVH5_F>d3@Eq|TNG4&Q`t?!AirlU^l-CGe$* z6nCXO2qSz#Qe&AsZN1Lx>;Qg*AkmS;LVF7t$m}V7KF8=J;(YF=!lpKM+2qHpQ#ss< zlb}WS2m1t?)0C@VM(V@Inybn%eWV64$0kV=kKj(kO5uA=B6B)JVier_-j~)s1|XJD zsl9Yx-lSZqr(Pl|X<`&8;5kn$slEg}gWx=rH9v6+Q-Q-4Z3}hx(2KUR&2ZVkH?O*S zL1%u+^EmTc{?rG%>7d^zHuXr_dM{>gMiGb^gd21=LQMw5+m6 zjM_Pb?ZOLnHs|Re-j-vl!=PYFu^wAobkr4f|Vi|Q2oloJU_y$S~;Ae>p z`#hO~A`{;|>kwOK?f;XyzFd7NrnjsJcn@d9_l=j2V20 z%(#I*gd$(5liwCWkSVL?=Cf7r$^3UJkHuAGh};zAU1s7v^ym=5Ia9v|UyOMKOkfa) z;mojbP-A*748mmfimRv+fx%Q|p#N=lx%pVoS}V2QC>4?ZiD<>0VbvIEFepK}A|c*q zExu=@-q&=Z8VQTo%^wAs4AS<={BWXwJ*8sGRuMEoaor+HzC2yLVrVSt8`TQDED_5(3Ff zt=bJsNFaK=r!yCyyQlX4#)my?YF;q0U2FW@2SlV7+QpBTfY5LRz zBtN`|`1y#kloMRoJu5#tlZzjZjn9@}j^A@`)NriyJ zh8$^_#!x-M`@1mZ1l^&L=ts%3T_dKhbK!(PtGrG@u77z7d}5_uMeCiwti)oZC1H8~ z8g*xaWiFFgcAo}b3jIV&TQ-mwbb>T>9kpCQS5uGxeu7&#UfZx!}>&S&!Gbn~(CdQ8184i%djz8ZS-M4Nv z37KJ%lQ~>C>VpFASaVwtmpk;UEPuh&I5$=$RrYtYP(NE}qe5&@r5j^#jLF$v;9V67 zElRpJ;lmXIz3}O(u;Z8XY+%SCqOwdjHSB`bR5ManmHXNX*CybXO4np(wpDvo!w!P{ zPU)#nbocctV>8)af_*ViYBjU*Xw>MTRKaEC70f#I^{sv=Dy;3J3BkK0q_rc?C{>s1 zU^31vg1rpJ@53WqPX*=yyGj{rRf&#qNUBhD@D5F=h`j92j)pkOq~hdu=QFiRVhgkO z;R2&;D}gp$ifW%cjyqX-q$Y1Gps?3x$^sHh1Y?IHOk(byFT*$>FRFz(3a$R~h`Lpz zaMP#|`mW$mqNtXGy?IFDMZhA4u^OHu9TRXRhyB{+gSIc(uYoAb$V#ObnU7W#6%%eJ zm*7^kiGXuM54=7MJ8*VmOpYvN%CjZU(aCD0(5s7-nOHV$H>t&X2oPeKHQb1Y;uv|C zjj~QSIeF1qJY21)D|5dA1(t&D_|>-Q!XVE^7@YL9H`Rl^m$*fzKqu)*g;XE3&=Vu@ z-MX|Rmm#~%`>RK2CnJ#hOG}0B_G7MHXWf1$y01wI zc6YZ*$#`E9rupDzFg*Y`?~UcJBLDlB)A#;Og3FaKyjn z1X{HB1B`RX*!Kh1e+=sNBI=<9+`Vw)qxjkEIN~bIo+cRGSD+9=~N{fBsB>#z1 zZkJmdl<#Ol-i{`iF(%w$mXF}F zrd0rBH9WFiO`YI`imTxUa2{5&7$cD>?o@AzbJy}F*J4(k6TZ}VW?osAz@O4lUlT=K zosGeZ%MvZ(rGrGyG+ksMY9&Tde6n7ZTW4z!;cMy15gP848IC1OKVCK)j&XcDIj zFx&JR-U4*(R&WL%1Un(Uc_$&34AAY%bg?mly1YkQ%Jt$(7>fEws}Z3(5*l#~$DntB z+?AcvD3Zz9QCz|atR}kDh`MM-%C_-6-Do(e10_9Z-M^zl8BU6Ghxc2kJUSg)EDmeb zY751v_hJ16Z&zXQD7{DZzdgtJHPmI7WOZUO$iJI6K=yE-mvDg~9Ftd@Vx)0CMiUdu zy%gigyO3Qr={hui4yvp9LnP`&Z_~7NfUwxi6AFn7Z<{tMVdbSEW^x<;0_187zMfr& zaqS*o6LWU}AY%jfPKz}uVdjbaL5_y3qjN&Xh-M}2W00Z1mPh4a{m9hrRFjJxG>oDi z4d4dWlwqeA@kF)arpo(XBA=OrhASX@>>{3sk2i3!+|=#|DX$-3^}G}C_91de+~uQr zRT|Gbv0VS@cK{^+sF$KGY3P#+6hnoNj==TZ>FR#4L@jzOBODIjaQq_amE&+buGyns zkJ#RAbwD+L1qFrqr#hW(<<-}c_CVz78*)YJ@pC*&7u*Ue%UKXZvoBB8R{KJnOtS!B zA=(`zlHd;|vS?5?Ih8S6^JT|2FuN=D$u3kRgz-B$Qq7dBGUql0Q`%yesI1B@SY+3= zS$i=BXSueSqtRcIwDKt@{Y?}r*4L}{JL{$RQ}GFuG?_W2aq_>i>@&5?_RA1j^MxuQ z42%3(cadF_Vpt|g1O7y;ciH8Y;joBeRTR)LWEN#X04ImQ?-bZw6!FitTNduBEt(2| zPVNJ0&Y1@5(z&E>74=Q)L9$hh_t%De z;pm~gCbi(HDVgM=jqi*T0py61w%gnhi|ynboCkEhr64KIdC*>^r)a3$y)y^OOrg`r z3$yV5=fvo{VY`wrD`A*jQq^ z=?(SozgH)1Wx4Toxp{)Rk^&Ls^)F1)**AO~Sr)gwNXTd6t3XiPKLekn-pin?ID^It z(k7s++U-#IEBdi(*Zf-JGzoM59W%)Lq3j5&G}LQ?5b_&JF&F8gMWD`a8;d>R@UG7!+D&<%)gNPm^IFk0!H1}iacvp zUI4RPF#yY@Paeuw7MlJCvH}!BNiv?KMzff@>&?!o{q}5dlq^B+&)S(j3bXKFzf#-^ zUu`=duP*(pGQZ>=S!#IU$a*y%hUV6wNtcJ!+P6UyR&E?atR(?NB|EiK41qnr9hU6r zfDg|V%UwKcR?eM1gUDT!Br&Nw2-qPM^*awIpvi2XE{dnR=_<(elZwL+YC^(a@#|Rx zpB0aVo$ft>6C{-7A{I+he=^*KtoAG=CJJ5AHe>H{az85=Oji)(sy$0DUaG|4e3b@T z?9c4XbwsZ-;0WaUB$W;D{bx7{=Y4~3K8!d*8Z6gehEf`a16@vhv#_VL688S?gcAjf@s8FCTkdfR!*!3|Q7EU=*K(WoB~-%Frf}B{q}oP#nCiLy;hV zmacBly)T6b1(hjX+26g2hJIa8#>(Pswsf*3 z8~d0gsFt@ngn`DZD%uDs#{RmV>O?vT8C)${*4dsBgNR?de@64FHcPR@CN1wmGA0My zY@LnPjJ2tJ&HlvDcl(7kWk6wbI3G^!onWtah-*cz<0!9;+%eOJ?~hvBzFa9(aUpN% zulwERqYX3!{df2M*jKvA#vuqALUF+h1I!<_KeTC$iL0OPq(D10G~U*m`=4`1`bMFp zosNY^oO2p|>oKj~!cE-VQi-A{f8O^H zD`cFy>KT7+c~|zNeUULFK%QJ+Ac%%D@LS^EPZ1F(GeL*6KV5A#W;NLGz!{*fq-!kR zy-O9isp}KRjM)>NLb5-ybSaV>d&0MkvcJeENp5Z!g$wTDFqBJjnyo0*a+Q!Ryz4%A z;uiaX=cO?jeu!9<)rW#hrw@11iz&&#adf>Uk!cV8;@z~H{<^}U>;!FUkiKaSZ?LR9 zE>o|*v$@r3T?80H<0{N_>}4#it3nGnH^;+BmIp2YuEYK?OmXRzLT)my>R@GOgDfzy z$9?xX^~itpf;98kcc*_)gse_axp*c{ zw`Jm*2Ux9!Z>hXHcHP)13EJ1~Or&;=;HQy0A}OgjHzT>Pj(ssT)aB_tUC#@xU2S8q z618~aeTmgZT+YMOcEepwPYc;>+`}?^!W3~pW1Y8;=HJiV;mf)1}IET|`7FUPnlndf5}hiE#T*IeXk0e(60^W`l6 z!L(Wu?m@tj>SnxzkKce+RF}FfuMD4=f#`AwnV4l+v)uIW`%t<+=`2N}eMA3_S@>aM zC4?Lt&l08XgPKj2Fg4Impet^-yQUUArqjv4n6^V`;2O2yXRY^DRI}dpgIn3BraQce zRsu)YpOK?cRHIQt$Iy(NVtvulTuW|LPG~qCWbo$r6B@M3GAQ_7LQdNFbwAx}U|R0< z8hV%e)L$yg0^;s4O>LTWq4v7qAoKpIJ3=Y8XQ9-=WJ^E~9yVraE-0XF6#u%eSl1xK z&Q`L|2UVrD>iureXw8+BA*3rp@@HOokqVJrD}lzQl@PN=SVkvXhf_c~oneCCKdvVy zIT-XT>*N`@9aXhCjm963h(JmwC2i{)xYDB>KM4J1r>&SP%zZ==RV$`e659HoeK1b~-6>W5eQYs9oh@|z&K z-~+o}`vz)l^&$rbF}>~m4nMy|u-fB|piSq48*&@xCuIr{H|G66VhigpBiVvrSz1-i z$F6Z{1k#CpdzV051w1mt0C*uNDPxQ|LY5cWZqlT6L7|vN-#8p}cPX4p-Pj(t zgF_(qTAT4Kz(uef*Q3#UK0a*Ki9_$vj~g_`LNPg%b+8%fi;IN4IU-Ax#tFxD_h8qH zLeSYA{bukpom3hLQmaK8?xI^F!ESPwFdn=HQV} zd@)s(swT+0lpQ=0bn?SM_#)Z+DH97U9`;^dXUmsy=>>)WZK;r7fxT|Y3v44}?0KW# z6ACH;rJ5ld^awve+?BY?_E@uN%3x0T`*+8EhMzeFEq7&p>6TE8ieDQ6U!ON+S}*i+ zoK)|HI@oE6sZWWd`t|=y(KV@kYrlUAKz#{F->nF(#(mTj7{l}}VCTOq%+p91o>+TW zHQ<%r*vcnEuL7iB@|y8bywkTA5I9dyQ@nOP&Q~W2Ge~GLaE!Cu2f_EL2*@-^CuHjY zKG+#r?l{!0q_>*n|gGrG)M z>*7v*YHGi+b~F#-7d;I=r2n~XIFQEX{x_BFu-v05I<2Pik9ORxRB(lGjEks$K<_F{ z2+X*&w;od^M7?#+Q=B2qlQt*YTW6XpRksiC(6crV_pDmyWKCU5g2Q!~tYGFP-!H=? z1_ygh`bKtUq!5G;9Tq1)uNSm$I3lf#veDl0HUhBhq4W z718tqxUhtFCg6RlPz%3sr$SYby$*MIoinS<3ZI1ASld z!IjT0(M_wicanZPl`wOQLD;F_azZC4{k%Nv278b;3;XF-aS%| znJjLwP^juK5%I>=mg72U*HxS)3b)Hynl$OrADWfGms7MwAX3KX1op)m7?B?^fF|v6TM){%wW1SxS9>CAMli2KQkjb8`*1yo__jboo1I{*@S8Pwxj1&Nh*j_=g&u*mX|e~I05?M7r> zczl_#r}i@=w7$Fdh9Zlh2#n!fWRjqPqA94kQX(1SP>wD2ueMIi5^uLRU+;&Qt}{i9 zRMC^QottP6oP?NS3h?K8!Tyu{o(9Dcz5QYhY}-Zt!`N}nF%nB2!)_s5s^45?FxI_I zV-XJQ8PI)Ve)KlPIy_&v6o8_WoXzhGO6vQ1YPa=2s4+c+y%n!OXfEN8%+H28Cx0t1 zHjE^by%8OlKfM$ZZ$xU$(7E~a(QCQLze!4!zaRc3Nbp#OUL7R`CSDXNFWc8p%1RjY zJ*JRSdj4NBgQT`knFi9~tdqqDu)#i1N5x&i%{?l*(L}@NtsI|oOlGM2Ac)1(oVxi zvY*VtaJAMs*4;jmr80p$h&5t|ks%%6x$vyoUPq;+&sz~at58$+gFCn;8=aFNS8k%z zLvnZ^BlB~UZl2o{SvFJ51}@dc%Nyv0Un5uH#u}_56F!~6r;DkcFK_p^yKU7^i~hKO zWynl9-kEZ^iJ&)6Ki?osVG+T;VMI-14y>KvL&f4GS@Qx22o9mp8dd_#E7}Hr)Im@=4ronXJwbjpBXWJxGCy1s}Js~HR+Hkb7fA{3p$MBW%~HG zp-g<7e+XX& za*Q2Q3?!bajV({^mU2*?<~h`2oqrlMmndPKv#R^YaaX zKlC8U*LmwsME93DCxMiD&bq(Pe0+zH$OJyMh?uj_lZ00dVSf*3D}jh0oPk6kMh(9I z9KDYCbc=O$vqQ1jbPH&Bf#D|i){a2q^9ORBO36rKgWExz(v_BEprX6<%4AZ8C_%kk zWu~ng5viJydKPo>)O-#)9Xk4kZb}6PFN|=w;FSac235+Z-~YFnK&zQ+xFVkoo>j@`hZ{gX zg%U(H=c(JK#KDhrYkG=Y+|ir=1nFQ=b2o%}Ip zPW@rU{K7jrn})i;6CJNGxE%{V=kA8mti@UF8clDO{9HteXja|nuu0FWZwTfV_)@mH zF8HUBF2O0RThntD_1fq(E~YLP zrcSgbPWBG;Cice8^zNoM#`d^P-TxW^Fl3?C7RQg=k6?HK(fg6tI0}Z9E{e&;_``Mv4LVn0~Q~s;^StG_;Kw zh4J``Um*ak#k7JE%!-dHNO$}22rPo$r&hs~5?Iyj9l$jkI(@5D#sDdh(xL|l_Y=4; zYwc#B^-{ zc@?3uk9m+YpUEi?$IXY!9lPb#p&2)*fGapu^A2fDvJo%6(LG+<+!^~^x}$rwfuAxy zW8$%TkkC(pi5Iy32KdF(0fI$ut<}}Lp&rnhj9kw7q9KL~u@>ltzF;cmkq4nxHFJs6SVkjvo9wVQ*Va|>fZsF> zZ4a}tRxVWMIXA|#{*%&v0m>1;Q~XMUz8od+ebQ5ix`{V4sUa070Y=iAvGFILw+}qj}kl zn!Mr)@&slt+>_q9M(**}mX(C7FVrWSH}V{~RSra6;RjQPYbLhI)yrPut5J{5e03lCYeGsYY`x{r zV#dH@FU1rOhEs)2?wLbx546z?v*_9huaBK`nkbyWRZu^LuLp6VX+o-I;j0EUE)S}l zW;57|h7IdY11n9H4^XzWOM99UB=o7Dlhj$d#o?Wgc2(gx@(27s3l!Aj zJH`+Y001Ev008se)pVnfb#ga_~h*S{W z3PBMWr=eUTu;>~yI<;GPb|uMGLLu(BZDzd0Y`@$6!KqavVw!=n0OE%S_)P$nVL%2} z#y^qdh!tFBw&LqSU^`a%%1>XJ#JJs~-k6PRnBzTwbFHpLDFxG$FgOp6tf>_cgD70Ym7OWGJ}^;-W^dP%!dr z(0gwc@dgO4VFPWst`1RkC!PGflYT`V)F7g>+$^edsJS7gGL)=`GslPX1I@eS3Sg!8 zbC{*M6jD*D&Q(cDMKC$5qEtFnqsk%Ty2MP4C@gIeV?#)^q0I`sqGVnSk<9MIe65F) z24d|w;IB+6>B+h%!4RI5o2t$RX>iS?cYRH0j&(M|wr9FnD7=81qAg}bs*ON~ogS#Kf@%#Z46zwTIsCYVs0v_nOC zn>faRjKWUWCNGg_hPk}DzjM|`7uRq7kTDT^n}WW$HH5Qq431;!5CoX-WqS=<7vj68U3pk{-3F$)7JbQb4Uf%if>qlH{41lhwU zr2eo*E37N0r{8fN2pvP(*G<&>K4tkO6j$n)V|zufop@vr}g zq2lH@%{&|Wyqm%1*2__NAm3+p`E+Y@t~h)8b^ab>tLc1t!e4t<+D^^6C8&6V^??7A z6aRhx|G#Pfuls)v^?#55yQb}I=yEXg-CS$bs6TRn?OrB20=T9SM&zCTGTu_Y6eE{XbK z>ctkMc#aPa-0OHQtx-lfCSbtB86}n~lgpw9tSue#3js_a%9QcN7yt*BAqgaST(Dr= zG%;dgY2Er@VIH>xkM9~=7@KPPV8K=#Adv7yt%r>4@#2`4fC-^`Yh%$JgXAed%tGMz zN7g+Y;e&yv6M$pD+rv#rKozaZ7zB3>7#xvtvKayhV;a9nw-V8jNg(;kPSNg&!Qs}~ z&^dAMn326>4$EQ${OP2Q01|$PaRejnd%O4LNWwLpH`w}Tm059s*F+Qh*iO}as>Is* zJs*)f?>0RyIN&zSy5^X0U_sO0}_9-c3q41nM4~8Z;P^f`MfyR=ist zV8TXinDhth!`?M0j4e;v&F@n(HHG${9gyl`eQ}0{0Vz$E0n9LErdhz?j@l{Y2Gu&| z2O!50;-H2z7`{kK7l$!KO(?OsiP*Og9j}of$RwkF1FVf3sY;B`YGq6t35G~nkkLs% zp?JI=KY0H!h2hFL@Ha7F+RJsoCB|NM6^fDh+RZmLG-Suewr36B0{}ikqQKR_?XoRhe{Dj1I4?UE>CmFqE^oTY#-(l zfRHz_K%t?W?_VKwLSP8TdHA+O$rlT@$ki5>HK~%o!9XT}(#HcSM9%4qnYUi<51}wi zy1#39s>Ud%bjbn%9}0j|5DKiKLv$Cl$7U6h0dE1U9zGzd3H5?yjP$YIdt6PkPTrFk zzJ)T>MOq(1_NR#y`+|Ww0rbSQd3;JcA&v+F-jGFw{O}C{74lS#mvdB4=V+Qn2G>*s z<(TaO6^WxMt3D4_Xhy^c5@G8xQX9KXn34UW!PGV=erHZ|0{3Q!tjB277YIre!cc-U zR@29hi6{=sqTA%1XR@!`DI+n5!uex)XCTE;dq!DAmjMS06yO^OF}o>@jv#c{0erom zD(zO0cC#6+6v0%$Qe3t-c6X)~CLEc{zvXZ7Q&d5@WVO+Ly3-17w@51P%nz%}_S7tu zN>)+g!&x4KM_!Qki<;~>v}$zD<73mSBLK`5!>66qUN2b8x1`f$B`n$IBWNNe(mr7{ z#7|^fcfMm_x>+3yB_NFnBPJ1rV;=I)>Sg71A?0`6cdPCY4%r>^R34jb+Ac@LCK11F zi#gDC8yHT6P{lW&v1}oA3m=(8c@*!roR3sHtb;L6FsgoR&~50;>_dh)0w}vO&Q6Z` z5D~N({zo`P#FO@z!7I2LKqO~MaI({*(&c`A^(y^uB1y});GBTLOH&gT6Vu!faH^YH zmafL!8;zPFBX=m+dum%mN_;^FkrYoiM?p)1#$4^_y-B;A9viVOET}N8JI<6?F|p>k zs49kSGC7mYC;*eT=RFIl2t*>vqCr5j;U zN?JN3m2LziC8WD!mqu_wQlvx_5D@87S}BpT=q?EX=?+1u|3TkX!Fcii-nm?!3oqur z=bSk+b7tn8X9oypU20#YXk251oVTID#%$xJU*}=yea!fA(S#IRrwE6yC2yeqbd(Symew zw>?|nIwZD(F2JDq(R7BXMaM@$gbBupoJYz|QK@l{@*Pj0gxm$w%Pz4|oENr7EW31` zXL8K9W0)x#uR7T+$exK_Ur9RMW2ac*qg8|B2L--B=ghrHy5&*5{GFu74Fof)6`$wA z?l1T8G2IQx73pPEH4X9(oqQZE&#IzEMvu=AihkZIc79Yxd+w=05_HpP^cL!tm5dry zH!D^Pvc}t7st{2$xisJfAuk{|tSa5Jo^xZIu`x_y)nw%#D)W0i>hSoCOU-9!qq%FY zst|92-t5e6+$yQN_-e|IQp=W2F+~VWyN`&<>HQq-r!r~X2=?wMAqnomY8t6(% z4yjsXwv?iNF0&kJru8PW7bsa`$)CPL#X2cVFEqQ$P}@%DWY#j0=`?we7}K=!mLhf3 ze#BQ$=b&PJHE@?g*mt0MZ@fz;Vc(_Z({}j2*R3&v_Mn8ZsbL3)n#fGPuGtN)`yxtu zg3mu~WwzCzF7r~3I@AbvZt@iy2v_fYYFhN)Nq*uMvENsD_4%H8#i!|@rk0N4QSS#j zl&(QnyIK}=-S~pG_awaEY&(pgiX432@HpVCswQ_~*cjivM97mc{3x+Cv9;6AzStuB zYNEitH`()DM~{e0J0r!^x*A50337 z7~ZViCr1K-XwX3*+CMow7B04q=2o_5!1f_uqt>~L1Fw3AxmnIRe(-@VKRynJL=$kW zi+4V6v<^2QKD+jMTl#bwoug1LE-x7}9aBHJ&L~CX& z4|BbdS^fI6O(t@KN*)cM1 zRSu!ltDK%Ix39?cEsPjNq@?xbZ+TUv`SCz2@PI#lKEZ%}=^4thYjha9g1V{{Ghvh+p_kgx88!o+K z^$mWIS+O>+!f+W{D^H*k-hD<7gN`SX4$X~zV~?s+*RBrm>M>`vTRS7~Vl+H4>azb( z(>dF_b?~Swkz~bVqW8`o{lInT`s&bH*Je*ATS@qgGZJ!wSBLzLc-cy{f%LrQ%YI}I z(dX4SUsZDQeo*iBgn*M13me*R$pi$t%-kePb5_Mk!bA=TS6G)F;B3gUd5hW0No29< ze6U8lHtP32UeteEMFd*#GH@fQfT{C}wTH8F!nAPh^J?PaTTS@e^;d|(Cx>4Q%vre& zaJFPlch~td+oN6xYh9deOYzBK?VZ9k(`&ylTdrwjB-Q4AA&uge-wlvzIR~b*BU11_ z=;(fe1+f;h3IPO40)`mfpN5yCk%^6w#o_33u(wldI^3RDO*5k<+Psa8I*lFn$j(_7 zcact@)Dj`|ngWzDdW-c#zZdI4KNRr-!zmHndcyeKr z#$8^#b@>`DHTg*s-|pD^}2(@=10*|o_h4&#=7%lMqPXmFR$jj0^l_%1vCw5jcOCVZ zThb;!S~E;i+<}ZXE8($%G(VP^ere*Bi3uO$lc+MR(iaU`|O>vbl0*2QQoD@l% zr265RHY;$tZD+=6LB$wqZNm)F!Ib+WT{0RZlZN3a@WROS#(* zQ!8DpQk*+=nVO&OJYEd4%I5rZS%LV~%_~gu3z-(vpFHyCd$p7&?oVf&+M&Tl#O4`s z!hMe1lz8`=;46hJodTN6y%CyRRn~@eI$c6*eHOIDy(O1&;v=0lP9u17RZYk0ck?-@3n;&w9?@*CAKP`RDG@@u`E1euoOAR|q zkWPdyc#5*$S1q(|1FZ4LIcw}g-oD3U)A#@y3*PsMe!)r%&l-Z2ZyFdk9L&~8M{;99 zd^l_3Es}y}@>ld<7rpGc&h+7h@&PRZT4sWswAnqIH_rT)Q0kDG)rQE=hK{)BRL1{fW&n4kkXq7&!{;s2#0{^^4zpbzk>c9`>G zBvcUi@HbvCS$o+te?U5K(p!7p(DV%chxy9)9in6(@X+HD2L?75JacoT&&Z?(n^hpK zv+@vJ=x*gY^%zv2hkseC34`$r2ccrwmS&*4I-au$)Z&dnvRurW;hR;O#BTC^Da2LC z;#JRQnr^5E#XMGe)FbP&f)PpfIUvs72I2nvsuDTp(-EFi<7U@(7qBzd`Sh&sbe>|O zTWvra=cm`Vc~7ppjFB|Ymfl+8zST9h<1}j&zAIOJe^FXYbb6$XbB%j+n)H+JM|N;` zSTUAw1IYwSVTA67RwotpZOdd;9mjQ{kW=q+Jd;&B%;j%_&l20e7kHS-K3GMIpeG|Y zuJ&@qrpEPMPw8|e1pjK4-j%HQi{UV3pJN`*>^#e*MnLiHKqvh1Nat`29uB0RM#5Lk zM~GqiiQ)w;uQSK4MXIyGpesb(0*Jn%{etN+;mwap_dW;msQTmRxsL9*7w?$gAt5eQ z6MLxKO+|@~&R7W56oP__%4|ytH7(q8BS%Sj50uzGFgnI@sF@&C-Mhq*wC@nHD!bufoU=^~Hl z#cZ01axbED*7sjp!h$?SBj{5ta(eqPyIOdNHyZ-_sm?Wc;(H_^EliONZRMuOYbujY z1o^VpaXcAVezTe=uS937ynjt-6vu4c}V zui~q!I3#f30Y`lWofmn?aOB9ciIo$}f-Y&?`HV#Gk6U1E+r&#Z^kfu_hB9c^X-n{I zS$Ooi+qBGp)&%14a@RZPm=6&u0=D2bk7!nH`UtVr`|CRXMPrXSJa$L#U>%Sdz7y#3 z5HG9#G%Uu%4jx9cSTILUc1J?%v0KVVZzi+R=ftVlsc%~&sfak`MLqO@g#9je{%M0S zF1A#@$XAMGjh4x67DaZKyc?0*E|6q2S-!b@N?aJb&i*CcSPmsuH(3_RsdxI7P%(<1>3hNNh}Ig{-(Zde2J44}ePN`4ri zD9%8YlAJhsJ|)Sf?IORn5@#n=_C~4w@a9IXn?~ z{m>90-^N?(O>pk}$IdU4}QiPT_yXB7dEO1MYcw;ac?sWAJHco0TKP;~#TTj2NQAt6pcmLZ)%1 zCw$Q^_p$e2&ZUi#oDz|TbsPIq{c05%c`tp)6%srSPY8TRGMNMEP`DyX zZRZl3StRD`MAHX>xZ?2}gKM6C=mEFr!OMfiA$-a6FdX1r1GJr$l3)h&vxy?HtF(`% zDC?tqQ$i}0-u0lgEr0YWUXhV%kxXe8F568CU$O0sspvN5h^5W)e4FetSt;t?~MlX&^;f@xMXT!WxXL|5Nz$4Sv*FjhH zKO-u&7`<&UL}6OYyT^W2MV#ThQ&?j4v?kxeAN(%HlS~k1?oel=V4;qACD*aH(-9+uIEn=$Z7GgF z;5EP9`%+~g&qP_7o1cXsGIevH)gj%GD8x2K&>ak^0b6Q1l#fagd?1ke5XW8QX&d90 zh>KL`vwH6SSU@n{09sfb6%AKi`eyBW@Ic=4l8Xk9196Ezzea z37I<4bHx&utZ5|gY-=S{4tgJC?yXorr*OIN6srvly?2+5k#I^J**DJ7$}O*nuN%QP z_mUiYfT!Q*jpeGdz<`8$N&pcBFEnEyW<}BJKp;hX`r^zV)X#Jh^;??RIs$X?v3s0P z1i4Cw)BHHr3H1+$G5FF0M)p>AMo=pUdsc{v5pd>l=>9gUD>@7vy1!TPtb}MSdl*SQ z(c9IQhi)vEBp7#vT+!yD#nDoktWk~@Vy9GMU~rwdqu9~y6%paxoS%Kay@-hRwmBOi z218%TOM)70mdRz5Fp1V}oVghi72{wdY9A&HBQe?{myr19rvoH(KwBzAN^^JH)Kyap zCT7ugA-vvYEIWM*ismrWH=W5rvku6C- zi!$}#-s3m!w>)2@()aq&aK~S)qc^K@_eWk5sjO+nEw;%kt@ExpRUvoKjrc-6%WdvM zr-r=y*r4U^LCpfCPPd0wdOFv@`cAr}m?dtltpfh4EhR5On~cz^WSnke0wu8nuRntU zVm?v|gwZUC8M<8TxlWmD7bc)fc1|tB)O_`^QY`w#`-RSQ)jGJls25Xh$L&0jp5o0d zC==Rdkgwj!7RK1UNj|n1+Iv-3P2PC8xoedkjhx@*=97YLybU|~<+{z01EZXoox$2j zaPA%>mPs;AgIqr`1y-$IpY+%(%8bqGw`rhDsD(a2g5Oow9;xPD1uvqzyQr~h2; zYiwehw{M!Rrl#33S|PJTqMBt=AN1U&il7mnxhghqj960yeDy$4M(yYW`+lSg5%YPR zHKP~Xb=r-gR3u~v8>5f%$X-;~&R>hkqsN{YR}E${$P|UJplhf{4w=0fH@o&EELWwLpS$Z4Lj>HRq(ouCR4$gcNacq-jJXjV^Vl27mDXnmcm%}+T(7+mA4T=OwZnKKHh&Z5bIo+Jf*FX9(9Q@ z#PJ=I!I~)F5Fc^-h4?oDEyz8da(D8b@ucJzp2o0lW?0dDB(9ciJ8qEPC;+#t)uSLn;eqRLT6A_>1RpsSXp1GT0j|Jy1YM|ooqBz9h)??O|tvoXn zFSzZU;`2zbU-Nl?jFgIsdy3C%H-VR6oaJuZTo$}po5&|6+}>Mr#I+XD78kT1Mz7_t zTEYhFW?tF^-_$4{1G8Ik(@kRF#cYN$uIj@qK&0AtaZ9MZdZWWwYM=H7^H~gCLf|9` zro1RH$&GDKxX!OiU?aZm1dC&v2!2$Y}n(wT0U*I7Eud+DBy^A3)=`DwQudJC> zjovcvCh1V^!HauyHrLdt^S!DcjwME>X(7d`J=N%8l`V7E?B!e7dQhD6InoJl6DzDb z9si*+p2+~4mh=+S%6(GL^EAz`xpK+o);`8-4){p7F6tY~)$AsCU{0y`8ZzMRVHR-k zrwu4@mEad+l2@4Mr{=Ywd+DJ)yGu&0;J-nM>-)-eChWQ> z5Y|6@dJOl#v~gcP4ngq222TK%x(AXUOv`@>XATBM6wB|I(5M~LYB?Rll6a}F17&{- zimCAmr>USQryyo5?&f%&UVc<9&eM%cJ^)%?XvN#qqCR>6I6lf^;;LA!;JFKsz zUt-fnlH4e*<2zX1Eo4hz;zrP@Y@09=MN*!p_}28hA??DV;D`?%_9*>wyp;kD2M}ep1wdXz#lcLrL9Mino5^7#gIR zZ~Co!v8#~JvUE9DQ*CWSKh~QtuGR0o8>Ji0emUaj#?XCp%-36&O-D@a*1O18gq4-P zyB#Ev`bhY%(Pk(wp-!)HIljb-jkgJYa@Nf6g2%m?f>IT2*7Ageha+x0xDwN`iPKVA2quNjv3^`8gU4~A+xfE!p@DWd5O(!=Knf$H< zow)6@sLZ80-rhPcHcwdAEvoSQ^t93s9je~$nMTIg;V*GrsfYCq6%HWNHyXV6)2eNJ!InABf?n^ke{!qV$5|=*4M=PMYvs6IeSf$n` zU<&k3ueWh|?0KNa(>X3PXi#^F;+X~h%5quy>1*w_ViyC#uM8#m*DBEIobB6PY}s)$ z>h0#JxD`a4j_9>8Hskcb9oncm{z}?FgM$JQY{_eUf2vhe@9hK01lIkB93O?^cv~-_ zA{4Wx<&R?o>PnH(2!uO=J1?oqT?@#u>&?p#y>A~CPrn!gPGW$f#3zIr_GXgFurO2O z%CNSDko4Ago}(<=2!4GkowS~SfLwIKnl+G~f-Fs9i+_cwagagzQx($u) z2qoN>QH&co9Qt+5n7lo_+g?K$w^VViwLgCmW69)UUl3nQQ52g>epBo1eyCF6!!LC?RCI)qc?Er*nR{;7JFG!u-L)vtD=`wh*sEJT zzudu^;OQtcfTL^Pi629{$3m2xu<@w0F8xl8?mQ>yJ)Tx2Zow#tq#{ki8gwUV82vDK zK1!6?%;3U|aAE7kh0~VNVGjf7@`$spYG?4s@xA`kOV|(`p@jD^o~aSDJyW~?&9E}& z%mLe(mVVCc8GBRa1;+Ml1Pfl&fv)T^4#>{w${C7xv`s zYLFwUdKur&x^m{j+RrH7nXA0J#AQT*cb;{$k&Ae)t?BWrmV$HAGYzj^FMJ>66?7&n z+_NPTp>d>s$sgQu(G*n%Wn{9UPWf6cxs03Za8^xAY2|8^Tx5kyAI|0ODyyZu+9Agd z$j}BmSd!NOO4YCq6lof^C!bDIeyHPEl^hBtmtQHXlE8tQDt5cV0-S`S?h` zgiEOXO5GBA&wZh4LLQUl)g;y1)wUON60`=RW6oJIQ=E;)_AgVkkJH+M1b-gtelQ=G zfT+XjBVf8X%cCQafb&W+<=VWd3ge1J@cRwyz`&BM>9fA>{=Os@T-8@g!I$?`Tf8q! zMGL(*W~Ext;JZG!iecKWJh2`lo44ui*t>`~Ntc4{-HnipT3B3$dggp91s4v=X8|8a zH-ZEi>p_yTqJg(c8+@AXQWb6{1C-OUi4D1CTO-QEpV-5PcfbYNbH<&*gZ+7*X%$5v z6LDLgV!3^U3Cd38Z)kTH(_QO3)hvX-{j3KD{$P!=7cFFUHtDX2GP>SGJPy`Aw%wH4 zZQp)8eWXSFl`pD_Fat{XL# zPw%i6CsH?HchY6lO6w;%T6)$&CfrQbS8h|gT%lty^mzV3Ti0tuUu0w>bL+j-9K?=? zj5F;;jqbzqZ_TOnu!0_?q=XabM?`yGXMuR(?Qh!WU0jK76dk{vZ=)2r{-iz{d+b$C zTzk`jXSAc%4fbwii=CA^UCjAPu&7MB1*p(U>R`eT78kJp6jOqd{QffNtojIP>JBOO z>ABQVnwto$Yb`Gs1^HdbM7R9?M!dYPj_Pt5Ks@ThMrssEcUKy&X}#hbY}39dx@kA` zdS4d_*-7}bZ$8erZ+@7$Noc434S9Won!bCv%UY<(77}en3Z;Gi_y`KQhtUKGh$2Wt zz)t7q-4phg|NZ)7@$j(qXk6x>s)CPH{bNWc90s1i!P(ubHw08K1GfL?et=1X86R)K zkLet2W)NE|d#JO?aXgm;*s2duvk>@D9>Kc~F#I;sl+6+XWQZIG+q+1suA zt|}+4p)RNND^zY$)n;>`N#_7$DmWzYOoIggJikS~EGHqQq$YK|jW|TJ266xzA%J%7 z2pSXSAJ9~##3U7^ej&4>H!_|φwljaCs5nyz`ZJn`$o0;>MD|UDT59=Og{rrFd zqWIyLMKKxNKLDCqL7>0^!0(5^kHRjB`Tr}{*Nfb*cyNOjQ1pw&1QeIg^7SkW-n=we z{fY2?y3aatvkNf71%MOEvY4K`0oaZ+6$m5)X9J#TFm_;||5m$Sl<~_8v?GLeR`zVa zARYWDE5ZgyX$nYrcuxv1X)q$v|Aq8dTx}`i=^224E>s|pIGhN0roocV{!d)Lk&QU( z?CdUJs#^g1;oSkeq`}gt{$G5*63S4d6}n-9htYKV5>)tW|JOJQ;6HUhZv=0Av<`uF(X>?JNhfPKIi57SxUB@HI(^f$PU&Q=h+U-?B!&Ef|>!0Gq> zYsDRt?U3)wg8YwW{|RnA`d2?1%pw2r-jw{IQHx?e0sm{G%pL5ZKda%Hpj=4RX<+X| z0|F`kz?cRb`;Qo{fVWgQIM}n9IY8m|1@8l%Y%Te95fJD>_HlCb#Q#QII61`C;yAhO zIOf@4K=K#oj+0vr|F`5&sQGbnY!4MXJ7Bkz27Im@QM>-ee@pHLOx|$zL!#qm_M^|a zC?8d?BhE)IQ%cb;l9hCH${iX?C`QE#u{}}f?v)y$KRv?2Tp^z{v9y9?LUrR zxO$FfFMcPni2pG#uJGdtTi+31CjJ5O$Mh{Y(csv>AJK^h;6SV(# zgkRz_k8>T*`}oc!m-_$V`X$Nn6pHU8CTV|1@;#yAxFE+<7QS;_P5(QNpN=ShHKvXS zx__sO%=|mLAHv?@G97{)9}_=bS}lsXXP_3Td zbjjMvIe{4Ppkp{Jd|V!nUj8nJaPD8g{yvr&PWY?tpn|gj&qXnh{C^?*#fp181{MyO h277t}v_tiOS!PiHKlN}|pbXNc0D(#?fCB>1{{g#4GqwN# From b02183e9ac415c2305875f68597ecea19e4e5a24 Mon Sep 17 00:00:00 2001 From: Konrad Lalik Date: Thu, 22 Feb 2024 10:16:27 +0100 Subject: [PATCH 0086/1406] Alerting: Fix dashboard nav drawers disappearing (#82890) Add DashNav modal renderer to handle modals rendered from Toolbar buttons --- .../integration/AlertRulesToolbarButton.tsx | 19 ++++++-------- .../dashboard/components/DashNav/DashNav.tsx | 25 +++++++++++++++++-- 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/public/app/features/alerting/unified/integration/AlertRulesToolbarButton.tsx b/public/app/features/alerting/unified/integration/AlertRulesToolbarButton.tsx index a71ce47023..963ceec182 100644 --- a/public/app/features/alerting/unified/integration/AlertRulesToolbarButton.tsx +++ b/public/app/features/alerting/unified/integration/AlertRulesToolbarButton.tsx @@ -1,9 +1,9 @@ import React from 'react'; -import { useToggle } from 'react-use'; import { ToolbarButton } from '@grafana/ui'; import { t } from '../../../../core/internationalization'; +import { useDashNavModalController } from '../../../dashboard/components/DashNav/DashNav'; import { alertRuleApi } from '../api/alertRuleApi'; import { GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource'; @@ -14,7 +14,7 @@ interface AlertRulesToolbarButtonProps { } export default function AlertRulesToolbarButton({ dashboardUid }: AlertRulesToolbarButtonProps) { - const [showDrawer, toggleShowDrawer] = useToggle(false); + const { showModal, hideModal } = useDashNavModalController(); const { data: namespaces = [] } = alertRuleApi.endpoints.prometheusRuleNamespaces.useQuery({ ruleSourceName: GRAFANA_RULES_SOURCE_NAME, @@ -26,14 +26,11 @@ export default function AlertRulesToolbarButton({ dashboardUid }: AlertRulesTool } return ( - <> - - {showDrawer && } - + showModal()} + key="button-alerting" + /> ); } diff --git a/public/app/features/dashboard/components/DashNav/DashNav.tsx b/public/app/features/dashboard/components/DashNav/DashNav.tsx index 57fa6be055..c2d9fd8e8a 100644 --- a/public/app/features/dashboard/components/DashNav/DashNav.tsx +++ b/public/app/features/dashboard/components/DashNav/DashNav.tsx @@ -2,6 +2,7 @@ import { css } from '@emotion/css'; import React, { ReactNode } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import { useLocation } from 'react-router-dom'; +import { createStateContext } from 'react-use'; import { textUtil } from '@grafana/data'; import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src'; @@ -48,6 +49,25 @@ const mapDispatchToProps = { updateTimeZoneForSession, }; +const [useDashNavModelContext, DashNavModalContextProvider] = createStateContext<{ component: React.ReactNode }>({ + component: null, +}); + +export function useDashNavModalController() { + const [_, setContextState] = useDashNavModelContext(); + + return { + showModal: (component: React.ReactNode) => setContextState({ component }), + hideModal: () => setContextState({ component: null }), + }; +} + +function DashNavModalRoot() { + const [contextState] = useDashNavModelContext(); + + return <>{contextState.component}; +} + const connector = connect(null, mapDispatchToProps); const selectors = e2eSelectors.pages.Dashboard.DashNav; @@ -341,11 +361,12 @@ export const DashNav = React.memo((props) => { return ( + {renderLeftActions()} {renderRightActions()} - + + } /> ); From 8852b1dcc573ca3a201ac072a92f1a3227065e1b Mon Sep 17 00:00:00 2001 From: Fabrizio <135109076+fabrizio-grafana@users.noreply.github.com> Date: Thu, 22 Feb 2024 10:26:50 +0100 Subject: [PATCH 0087/1406] Run analysis steps only on `grafana/grafana` (#83185) --- .github/workflows/codeql-analysis.yml | 1 + .github/workflows/pr-codeql-analysis-go.yml | 1 + .github/workflows/pr-codeql-analysis-javascript.yml | 1 + .github/workflows/pr-codeql-analysis-python.yml | 1 + 4 files changed, 4 insertions(+) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 455633da8e..1bbd4387cc 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -67,3 +67,4 @@ jobs: - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v2 + if: github.repository == 'grafana/grafana' diff --git a/.github/workflows/pr-codeql-analysis-go.yml b/.github/workflows/pr-codeql-analysis-go.yml index 4d9cc3760a..59dd298c91 100644 --- a/.github/workflows/pr-codeql-analysis-go.yml +++ b/.github/workflows/pr-codeql-analysis-go.yml @@ -50,3 +50,4 @@ jobs: - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v2 + if: github.repository == 'grafana/grafana' diff --git a/.github/workflows/pr-codeql-analysis-javascript.yml b/.github/workflows/pr-codeql-analysis-javascript.yml index 304b2798fb..e09b3a71c7 100644 --- a/.github/workflows/pr-codeql-analysis-javascript.yml +++ b/.github/workflows/pr-codeql-analysis-javascript.yml @@ -33,3 +33,4 @@ jobs: - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v2 + if: github.repository == 'grafana/grafana' diff --git a/.github/workflows/pr-codeql-analysis-python.yml b/.github/workflows/pr-codeql-analysis-python.yml index acd2352a2a..e990d71391 100644 --- a/.github/workflows/pr-codeql-analysis-python.yml +++ b/.github/workflows/pr-codeql-analysis-python.yml @@ -31,3 +31,4 @@ jobs: - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v2 + if: github.repository == 'grafana/grafana' From a35dc9ad4e3886bc97ea1fc11319c3554ec1c140 Mon Sep 17 00:00:00 2001 From: Fabrizio <135109076+fabrizio-grafana@users.noreply.github.com> Date: Thu, 22 Feb 2024 10:49:51 +0100 Subject: [PATCH 0088/1406] Allow build and release process for Zipkin plugin (#83116) --- .github/workflows/core-plugins-build-and-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/core-plugins-build-and-release.yml b/.github/workflows/core-plugins-build-and-release.yml index 108ac0db33..3d3f6148f1 100644 --- a/.github/workflows/core-plugins-build-and-release.yml +++ b/.github/workflows/core-plugins-build-and-release.yml @@ -14,7 +14,7 @@ on: - parca - stackdriver - tempo - # - zipkin + - zipkin concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}-${{ inputs.plugin_id }} From 9c42826c30eafcc929d3c16e5efd995e5759b0ec Mon Sep 17 00:00:00 2001 From: Pepe Cano <825430+ppcano@users.noreply.github.com> Date: Thu, 22 Feb 2024 11:31:59 +0100 Subject: [PATCH 0089/1406] Alerting docs: fix broken link and apply the Writer toolkit guideline to other links (#83209) * Fix `/docs/reference` cannot be used within admonitions * Remove `relref` links to apply Writers toolkit guidelines --- .../create-mimir-loki-managed-recording-rule.md | 3 +-- .../images-in-notifications.md | 4 ++-- .../using-go-templating-language.md | 2 +- .../alerting/set-up/migrating-alerts/_index.md | 17 ++++++++++------- .../legacy-alerting-deprecation.md | 3 +-- .../file-provisioning/index.md | 3 +-- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/docs/sources/alerting/alerting-rules/create-mimir-loki-managed-recording-rule.md b/docs/sources/alerting/alerting-rules/create-mimir-loki-managed-recording-rule.md index bedc33a981..08fbbbc492 100644 --- a/docs/sources/alerting/alerting-rules/create-mimir-loki-managed-recording-rule.md +++ b/docs/sources/alerting/alerting-rules/create-mimir-loki-managed-recording-rule.md @@ -68,6 +68,5 @@ To create recording rules, follow these steps. [annotation-label]: "/docs/grafana/ -> /docs/grafana//alerting/fundamentals/annotation-label" [annotation-label]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/fundamentals/annotation-label" -[configure-grafana]: "/docs/grafana/ -> /docs/grafana//setup-grafana/configure-grafana" -[configure-grafana]: "/docs/grafana-cloud/ -> /docs/grafana//setup-grafana/configure-grafana" +[configure-grafana]: "/docs/ -> /docs/grafana//setup-grafana/configure-grafana" {{% /docs/reference %}} diff --git a/docs/sources/alerting/manage-notifications/images-in-notifications.md b/docs/sources/alerting/manage-notifications/images-in-notifications.md index 548b76e448..89807f1c8b 100644 --- a/docs/sources/alerting/manage-notifications/images-in-notifications.md +++ b/docs/sources/alerting/manage-notifications/images-in-notifications.md @@ -39,13 +39,13 @@ Refer to the table at the end of this page for a list of contact points and thei 3. You should use a cloud storage service unless sending alerts to Discord, Email, Pushover, Slack or Telegram. These integrations support either embedding screenshots in the email or attaching screenshots to the notification, while other integrations must link screenshots uploaded to a cloud storage bucket. If a cloud storage service has been configured then integrations that support both will link screenshots from the cloud storage bucket instead of embedding or attaching screenshots to the notification. -4. If uploading screenshots to a cloud storage service such as Amazon S3, Azure Blob Storage or Google Cloud Storage; and accessing screenshots in the bucket requires authentication, logging into a VPN or corporate network; then image previews might not work in all instant messaging and communication platforms as some services rewrite URLs to use their CDN. If this happens we recommend using [integrations which support uploading images]({{< relref "#supported-contact-points" >}}) or [disabling images in notifications]({{< relref "#configuration" >}}) altogether. +4. If uploading screenshots to a cloud storage service such as Amazon S3, Azure Blob Storage or Google Cloud Storage; and accessing screenshots in the bucket requires authentication, logging into a VPN or corporate network; then image previews might not work in all instant messaging and communication platforms as some services rewrite URLs to use their CDN. If this happens we recommend using [integrations which support uploading images](#supported-contact-points) or [disabling images in notifications](#configuration) 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. 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, and accessing Grafana requires logging into a VPN or corporate network; image previews might not work in all instant messaging and communication platforms as some services rewrite URLs to use their CDN. If this happens we recommend using [integrations which support uploading images]({{< relref "#supported-contact-points" >}}) or [disabling images in notifications]({{< relref "#configuration" >}}) altogether. +7. If Grafana is configured to upload screenshots to its internal web server, and accessing Grafana requires logging into a VPN or corporate network; image previews might not work in all instant messaging and communication platforms as some services rewrite URLs to use their CDN. If this happens we recommend using [integrations which support uploading images](#supported-contact-points) or [disabling images in notifications](#configuration) 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. diff --git a/docs/sources/alerting/manage-notifications/template-notifications/using-go-templating-language.md b/docs/sources/alerting/manage-notifications/template-notifications/using-go-templating-language.md index 2580962b85..50115d3f23 100644 --- a/docs/sources/alerting/manage-notifications/template-notifications/using-go-templating-language.md +++ b/docs/sources/alerting/manage-notifications/template-notifications/using-go-templating-language.md @@ -32,7 +32,7 @@ In text/template, templates start with `{{` and end with `}}` irrespective of wh ## Print -To print the value of something use `{{` and `}}`. You can print the value of dot, a field of dot, the result of a function, and the value of a [variable]({{< relref "#variables" >}}). For example, to print the `Alerts` field where dot refers to `ExtendedData` you would write the following: +To print the value of something use `{{` and `}}`. You can print the value of dot, a field of dot, the result of a function, and the value of a [variable](#variables). For example, to print the `Alerts` field where dot refers to `ExtendedData` you would write the following: ``` {{ .Alerts }} diff --git a/docs/sources/alerting/set-up/migrating-alerts/_index.md b/docs/sources/alerting/set-up/migrating-alerts/_index.md index abb489c723..42da3a7fcd 100644 --- a/docs/sources/alerting/set-up/migrating-alerts/_index.md +++ b/docs/sources/alerting/set-up/migrating-alerts/_index.md @@ -15,7 +15,7 @@ weight: 150 {{% admonition type="note" %}} Legacy alerting will be removed in Grafana v11.0.0. We recommend that you upgrade to Grafana Alerting as soon as possible. -For more information, refer to [Legacy alerting deprecation]({{< relref "./legacy-alerting-deprecation" >}}). +For more information, refer to [Legacy alerting deprecation](/docs/grafana//alerting/set-up/migrating-alerts/legacy-alerting-deprecation). {{% /admonition %}} Grafana provides two methods for a seamless automatic upgrade of legacy alert rules and notification channels to Grafana Alerting: @@ -24,7 +24,7 @@ Grafana provides two methods for a seamless automatic upgrade of legacy alert ru 2. **Simple Upgrade**: One-step upgrade method for specific needs where a preview environment is not essential. {{% admonition type="note" %}} -When upgrading with either method, your legacy dashboard alerts and notification channels are copied to a new format. This is non-destructive and can be [rolled back easily]({{< relref "#rolling-back-to-legacy-alerting" >}}). +When upgrading with either method, your legacy dashboard alerts and notification channels are copied to a new format. This is non-destructive and can be [rolled back easily](#rolling-back-to-legacy-alerting). {{% /admonition %}} ## Key Considerations @@ -46,7 +46,7 @@ When upgrading with either method, your legacy dashboard alerts and notification - Grafana `v10.3.0 or later`. - Grafana administrator access. -- Enable `alertingPreviewUpgrade` [feature toggle]({{< relref "../../../setup-grafana/configure-grafana/feature-toggles" >}}) (enabled by default in v10.4.0 or later). +- Enable `alertingPreviewUpgrade` [feature toggle][feature-toggles] (enabled by default in v10.4.0 or later). ### Suited for @@ -83,7 +83,7 @@ Alerts generated by the new alerting system are visible in the **Alerting** sect - **Export upgraded resources**: If you use provisioning methods to manage alert rules and notification channels, you can export the upgraded versions to generate provisioning files compatible with Grafana Alerting. - **Test new provisioning definitions**: Ensure your as-code setup aligns with the new system before completing the upgrade process. Both legacy and Grafana Alerting alerts can be provisioned simultaneously to facilitate a smooth transition. 1. **Finalize the Upgrade**: - - **Contact your Grafana server administrator**: Once you're confident in the state of your previewed upgrade, request to [enable Grafana Alerting]({{< relref "#enable-grafana-alerting" >}}). + - **Contact your Grafana server administrator**: Once you're confident in the state of your previewed upgrade, request to [enable Grafana Alerting](#enable-grafana-alerting). - **Continued use for upgraded organizations**: Organizations that have already completed the preview upgrade will seamlessly continue using their configured setup. - **Automatic upgrade for others**: Organizations that haven't initiated the upgrade with preview process will undergo the traditional automatic upgrade during this restart. - **Address issues before restart**: Exercise caution, as Grafana will not start if any traditional automatic upgrades encounter errors. Ensure all potential issues are resolved before initiating this step. @@ -111,11 +111,11 @@ Once Grafana Alerting is enabled, you can review and adjust your upgraded alerts ### To perform the simple upgrade, complete the following steps. {{% admonition type="note" %}} -Any errors encountered during the upgrade process will fail the upgrade and prevent Grafana from starting. If this occurs, you can [roll back to legacy alerting]({{< relref "#rolling-back-to-legacy-alerting" >}}). +Any errors encountered during the upgrade process will fail the upgrade and prevent Grafana from starting. If this occurs, you can [roll back to legacy alerting](#rolling-back-to-legacy-alerting). {{% /admonition %}} 1. **Upgrade to Grafana Alerting**: - - **Enable Grafana Alerting**: [Modify custom configuration file]({{< relref "#enable-grafana-alerting" >}}). + - **Enable Grafana Alerting**: [Modify custom configuration file](#enable-grafana-alerting). - **Restart Grafana**: Restart Grafana for the configuration changes to take effect. Grafana will automatically upgrade your existing alert rules and notification channels to the new Grafana Alerting system. 1. **Review and Adjust Upgraded Alerts**: - **Review the upgraded alerts**: Go to the `Alerting` section of the navigation panel to review the upgraded alerts. @@ -136,7 +136,7 @@ enabled = true ``` {{% admonition type="note" %}} -If you have existing legacy alerts we advise using the [Upgrade with Preview]({{< relref "#upgrade-with-preview-recommended" >}}) method first to ensure a smooth transition. Any organizations that have not completed the preview upgrade will automatically undergo the simple upgrade during the next restart. +If you have existing legacy alerts we advise using the [Upgrade with Preview](#upgrade-with-preview-recommended) method first to ensure a smooth transition. Any organizations that have not completed the preview upgrade will automatically undergo the simple upgrade during the next restart. {{% /admonition %}} ### Rolling back to legacy alerting @@ -196,6 +196,9 @@ There are some differences between Grafana Alerting and legacy dashboard alerts, 1. Since `Hipchat` and `Sensu` notification channels are no longer supported, legacy alerts associated with these channels are not automatically upgraded to Grafana Alerting. Assign the legacy alerts to a supported notification channel so that you continue to receive notifications for those alerts. {{% docs/reference %}} + +[feature-toggles]: "/docs/ -> /docs/grafana//setup-grafana/configure-grafana/feature-toggles" + [alerting_config_error_handling]: "/docs/grafana/ -> /docs/grafana//alerting/alerting-rules/create-grafana-managed-rule#configure-no-data-and-error-handling" [alerting_config_error_handling]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/alerting-rules/create-grafana-managed-rule#configure-no-data-and-error-handling" diff --git a/docs/sources/alerting/set-up/migrating-alerts/legacy-alerting-deprecation.md b/docs/sources/alerting/set-up/migrating-alerts/legacy-alerting-deprecation.md index d9955483b3..2bdef82ee9 100644 --- a/docs/sources/alerting/set-up/migrating-alerts/legacy-alerting-deprecation.md +++ b/docs/sources/alerting/set-up/migrating-alerts/legacy-alerting-deprecation.md @@ -54,6 +54,5 @@ Refer to our [upgrade instructions][migrating-alerts]. {{% docs/reference %}} [angular_deprecation]: "/docs/ -> /docs/grafana//developers/angular_deprecation" -[migrating-alerts]: "/docs/grafana/ -> /docs/grafana//alerting/set-up/migrating-alerts" -[migrating-alerts]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/set-up/migrating-alerts" +[migrating-alerts]: "/docs/ -> /docs/grafana//alerting/set-up/migrating-alerts" {{% /docs/reference %}} diff --git a/docs/sources/alerting/set-up/provision-alerting-resources/file-provisioning/index.md b/docs/sources/alerting/set-up/provision-alerting-resources/file-provisioning/index.md index e0ce89688f..c07d922047 100644 --- a/docs/sources/alerting/set-up/provision-alerting-resources/file-provisioning/index.md +++ b/docs/sources/alerting/set-up/provision-alerting-resources/file-provisioning/index.md @@ -32,7 +32,7 @@ For a complete guide about how Grafana provisions resources, refer to the [Provi - You cannot edit provisioned resources from files in Grafana. You can only change the resource properties by changing the provisioning file and restarting Grafana or carrying out a hot reload. This prevents changes being made to the resource that would be overwritten if a file is provisioned again or a hot reload is carried out. -- Importing takes place during the initial set up of your Grafana system, but you can re-run it at any time using the [Grafana Admin API][reload-provisioning-configurations]. +- Importing takes place during the initial set up of your Grafana system, but you can re-run it at any time using the [Grafana Admin API](/docs/grafana//developers/http_api/admin#reload-provisioning-configurations). - Importing an existing alerting resource results in a conflict. First, when present, remove the resources you plan to import. {{< /admonition >}} @@ -817,5 +817,4 @@ This eliminates the need for a persistent database to use Grafana Alerting in Ku [provisioning]: "/docs/ -> /docs/grafana//administration/provisioning" -[reload-provisioning-configurations]: "/docs/ -> /docs/grafana//developers/http_api/admin#reload-provisioning-configurations" {{% /docs/reference %}} From 5561ace46750dd25467736375481ca143b693288 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Thu, 22 Feb 2024 11:04:18 +0000 Subject: [PATCH 0090/1406] Navigation: Scroll the active menu item into view when menu is an overlay (#83212) scroll the active menu item into view even when not docked --- .../app/core/components/AppChrome/MegaMenu/MegaMenuItem.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/core/components/AppChrome/MegaMenu/MegaMenuItem.tsx b/public/app/core/components/AppChrome/MegaMenu/MegaMenuItem.tsx index d91514f878..528c0c9a04 100644 --- a/public/app/core/components/AppChrome/MegaMenu/MegaMenuItem.tsx +++ b/public/app/core/components/AppChrome/MegaMenu/MegaMenuItem.tsx @@ -48,12 +48,12 @@ export function MegaMenuItem({ link, activeItem, level = 0, onClick }: Props) { // scroll active element into center if it's offscreen useEffect(() => { - if (menuIsDocked && isActive && item.current && isElementOffscreen(item.current)) { + if (isActive && item.current && isElementOffscreen(item.current)) { item.current.scrollIntoView({ block: 'center', }); } - }, [isActive, menuIsDocked]); + }, [isActive]); if (!link.url) { return null; From 0dbf2da2549d20848266e91dd367d15cb9f17eec Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Thu, 22 Feb 2024 11:12:00 +0000 Subject: [PATCH 0091/1406] MigrateToCloud: Add API interface for frontend (#83215) Add RTK Query mocks --- public/app/core/reducers/root.ts | 2 + .../admin/migrate-to-cloud/MigrateToCloud.tsx | 14 +++++- .../features/admin/migrate-to-cloud/api.ts | 45 +++++++++++++++++++ .../admin/migrate-to-cloud/fixtures/mswAPI.ts | 15 +++++++ 4 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 public/app/features/admin/migrate-to-cloud/api.ts create mode 100644 public/app/features/admin/migrate-to-cloud/fixtures/mswAPI.ts diff --git a/public/app/core/reducers/root.ts b/public/app/core/reducers/root.ts index f2c436062b..33d86889ae 100644 --- a/public/app/core/reducers/root.ts +++ b/public/app/core/reducers/root.ts @@ -2,6 +2,7 @@ import { ReducersMapObject } from '@reduxjs/toolkit'; import { AnyAction, combineReducers } from 'redux'; import sharedReducers from 'app/core/reducers'; +import { migrateToCloudAPI } from 'app/features/admin/migrate-to-cloud/api'; import ldapReducers from 'app/features/admin/state/reducers'; import alertingReducers from 'app/features/alerting/state/reducers'; import apiKeysReducers from 'app/features/api-keys/state/reducers'; @@ -55,6 +56,7 @@ const rootReducers = { [alertingApi.reducerPath]: alertingApi.reducer, [publicDashboardApi.reducerPath]: publicDashboardApi.reducer, [browseDashboardsAPI.reducerPath]: browseDashboardsAPI.reducer, + [migrateToCloudAPI.reducerPath]: migrateToCloudAPI.reducer, }; const addedReducers = {}; diff --git a/public/app/features/admin/migrate-to-cloud/MigrateToCloud.tsx b/public/app/features/admin/migrate-to-cloud/MigrateToCloud.tsx index a213fe2ae6..f3d09ecc0a 100644 --- a/public/app/features/admin/migrate-to-cloud/MigrateToCloud.tsx +++ b/public/app/features/admin/migrate-to-cloud/MigrateToCloud.tsx @@ -1,7 +1,19 @@ import React from 'react'; +import { Stack, Text } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; +import { useGetStatusQuery } from './api'; + export default function MigrateToCloud() { - return TODO; + const { data } = useGetStatusQuery(); + + return ( + + + TODO +
{JSON.stringify(data)}
+
+
+ ); } diff --git a/public/app/features/admin/migrate-to-cloud/api.ts b/public/app/features/admin/migrate-to-cloud/api.ts new file mode 100644 index 0000000000..d1972ce85c --- /dev/null +++ b/public/app/features/admin/migrate-to-cloud/api.ts @@ -0,0 +1,45 @@ +import { BaseQueryFn, createApi } from '@reduxjs/toolkit/query/react'; +import { lastValueFrom } from 'rxjs'; + +import { BackendSrvRequest, getBackendSrv } from '@grafana/runtime'; + +interface RequestOptions extends BackendSrvRequest { + manageError?: (err: unknown) => { error: unknown }; + showErrorAlert?: boolean; +} + +function createBackendSrvBaseQuery({ baseURL }: { baseURL: string }): BaseQueryFn { + async function backendSrvBaseQuery(requestOptions: RequestOptions) { + try { + const { data: responseData, ...meta } = await lastValueFrom( + getBackendSrv().fetch({ + ...requestOptions, + url: baseURL + requestOptions.url, + showErrorAlert: requestOptions.showErrorAlert, + }) + ); + return { data: responseData, meta }; + } catch (error) { + return requestOptions.manageError ? requestOptions.manageError(error) : { error }; + } + } + + return backendSrvBaseQuery; +} + +interface MigrateToCloudStatusDTO { + enabled: boolean; +} + +export const migrateToCloudAPI = createApi({ + reducerPath: 'migrateToCloudAPI', + baseQuery: createBackendSrvBaseQuery({ baseURL: '/api' }), + endpoints: (builder) => ({ + // TODO :) + getStatus: builder.query({ + queryFn: () => ({ data: { enabled: false } }), + }), + }), +}); + +export const { useGetStatusQuery } = migrateToCloudAPI; diff --git a/public/app/features/admin/migrate-to-cloud/fixtures/mswAPI.ts b/public/app/features/admin/migrate-to-cloud/fixtures/mswAPI.ts new file mode 100644 index 0000000000..d8b9748e52 --- /dev/null +++ b/public/app/features/admin/migrate-to-cloud/fixtures/mswAPI.ts @@ -0,0 +1,15 @@ +import { HttpResponse, http } from 'msw'; +import { SetupServer, setupServer } from 'msw/node'; + +export function registerAPIHandlers(): SetupServer { + const server = setupServer( + // TODO + http.get('/api/cloudmigration/status', () => { + return HttpResponse.json({ + enabled: false, + }); + }) + ); + + return server; +} From 0dcdfc261b781df8c604ccac904dc500a63d327b Mon Sep 17 00:00:00 2001 From: Jack Westbrook Date: Thu, 22 Feb 2024 12:31:40 +0100 Subject: [PATCH 0092/1406] Monaco Editor: Load via ESM (#78261) * chore(monaco): bump monaco-editor to latest version * feat(codeeditor): use esm to load monaco editor * revert(monaco): put back previous version * feat(monaco): setup MonacoEnvironment when bootstrapping app * feat(monaco): load monaco languages from registry as workers * feat(webpack): clean up warnings, remove need to copy monaco into lib * fix(plugins): wip - remove amd loader workaround in systemjs hooks * chore(azure): clean up so QueryField passes typecheck * test(jest): update config to fix failing tests due to missing monaco-editor * test(jest): update config to work with monaco-editor and kusto * test(jest): prevent message eventlistener in nodeGraph/layout.worker tripping up monaco tests * test(plugins): wip - remove amd related tests from systemjs hooks * test(alerting): prefer clearAllMocks to prevent monaco editor failing due to missing matchMedia * test(parca): fix failing test due to undefined backendSrv * chore: move monacoEnv to app/core * test: increase testing-lib timeout to 2secs, fix parca test to assert dom element * feat(plugins): share kusto via systemjs * test(e2e): increase timeout for checking monaco editor in exemplars spec * test(e2e): assert monaco has loaded by checking the spinner is gone and window.monaco exists * test(e2e): check for monaco editor textarea * test(e2e): check monaco editor is loaded before assertions * test(e2e): add waitForMonacoToLoad util to reduce duplication * test(e2e): fix failing mysql spec * chore(jest): add comment to setupTests explaining need to incresae default timeout * chore(nodegraph): improve comment in layout.worker.utils to better explain the need for file --- .betterer.results | 4 - e2e/utils/support/monaco.ts | 7 + e2e/various-suite/exemplars.spec.ts | 9 +- e2e/various-suite/loki-editor.spec.ts | 7 +- e2e/various-suite/mysql.spec.ts | 7 +- e2e/various-suite/query-editor.spec.ts | 10 +- jest.config.js | 7 +- .../src/monaco/languageRegistry.ts | 2 +- packages/grafana-runtime/src/utils/plugin.ts | 8 - .../src/components/Monaco/CodeEditor.tsx | 4 +- .../components/Monaco/ReactMonacoEditor.tsx | 16 +- public/app/app.ts | 3 + public/app/core/monacoEnv.ts | 32 ++ .../admin/AlertmanagerConfig.test.tsx | 2 +- .../app/features/plugins/loader/constants.ts | 2 - .../plugins/loader/pluginLoader.mock.ts | 134 ----- .../plugins/loader/sharedDependencies.ts | 2 + .../plugins/loader/systemjsHooks.test.ts | 124 +---- .../features/plugins/loader/systemjsHooks.ts | 14 +- .../components/LogsQueryEditor/QueryField.tsx | 16 +- .../parca/QueryEditor/QueryEditor.test.tsx | 15 + .../plugins/panel/nodeGraph/layout.worker.js | 171 +------ .../panel/nodeGraph/layout.worker.utils.js | 174 +++++++ public/lib/monaco-languages/kusto.ts | 6 +- public/test/mocks/monaco.ts | 1 - public/test/mocks/workers.ts | 2 +- public/test/setupTests.ts | 5 + scripts/webpack/webpack.common.js | 28 +- yarn.lock | 463 ++++++++++++++++-- 29 files changed, 702 insertions(+), 573 deletions(-) create mode 100644 e2e/utils/support/monaco.ts create mode 100644 public/app/core/monacoEnv.ts create mode 100644 public/app/plugins/panel/nodeGraph/layout.worker.utils.js delete mode 100644 public/test/mocks/monaco.ts diff --git a/.betterer.results b/.betterer.results index 5c20e6abc9..f04979e6be 100644 --- a/.betterer.results +++ b/.betterer.results @@ -4581,10 +4581,6 @@ exports[`better eslint`] = { "public/app/plugins/datasource/azuremonitor/azure_monitor/azure_monitor_datasource.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], - "public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/QueryField.tsx:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"], - [0, 0, 0, "Do not use any type assertions.", "1"] - ], "public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryEditor.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], diff --git a/e2e/utils/support/monaco.ts b/e2e/utils/support/monaco.ts new file mode 100644 index 0000000000..54c528877e --- /dev/null +++ b/e2e/utils/support/monaco.ts @@ -0,0 +1,7 @@ +import { e2e } from '../index'; + +export function waitForMonacoToLoad() { + e2e.components.QueryField.container().children('[data-testid="Spinner"]').should('not.exist'); + cy.window().its('monaco').should('exist'); + cy.get('.monaco-editor textarea:first').should('exist'); +} diff --git a/e2e/various-suite/exemplars.spec.ts b/e2e/various-suite/exemplars.spec.ts index b83273832a..0a7981b03a 100644 --- a/e2e/various-suite/exemplars.spec.ts +++ b/e2e/various-suite/exemplars.spec.ts @@ -1,4 +1,5 @@ import { e2e } from '../utils'; +import { waitForMonacoToLoad } from '../utils/support/monaco'; const dataSourceName = 'PromExemplar'; const addDataSource = () => { @@ -57,12 +58,8 @@ describe('Exemplars', () => { // Switch to code editor e2e.components.RadioButton.container().filter(':contains("Code")').click(); - // we need to wait for the query-field being lazy-loaded, in two steps: - // 1. first we wait for the text 'Loading...' to appear - // 1. then we wait for the text 'Loading...' to disappear - const monacoLoadingText = 'Loading...'; - e2e.components.QueryField.container().should('be.visible').should('have.text', monacoLoadingText); - e2e.components.QueryField.container().should('be.visible').should('not.have.text', monacoLoadingText); + // Wait for lazy loading Monaco + waitForMonacoToLoad(); e2e.components.TimePicker.openButton().click(); e2e.components.TimePicker.fromField().clear().type('2021-07-10 17:10:00'); diff --git a/e2e/various-suite/loki-editor.spec.ts b/e2e/various-suite/loki-editor.spec.ts index 189c9d5a13..42bf063c13 100644 --- a/e2e/various-suite/loki-editor.spec.ts +++ b/e2e/various-suite/loki-editor.spec.ts @@ -1,4 +1,5 @@ import { e2e } from '../utils'; +import { waitForMonacoToLoad } from '../utils/support/monaco'; const dataSourceName = 'LokiEditor'; const addDataSource = () => { @@ -39,11 +40,7 @@ describe('Loki Query Editor', () => { e2e.components.RadioButton.container().filter(':contains("Code")').click(); - // Wait for lazy loading - const monacoLoadingText = 'Loading...'; - - e2e.components.QueryField.container().should('be.visible').should('have.text', monacoLoadingText); - e2e.components.QueryField.container().should('be.visible').should('not.have.text', monacoLoadingText); + waitForMonacoToLoad(); // adds closing braces around empty value e2e.components.QueryField.container().type('time('); diff --git a/e2e/various-suite/mysql.spec.ts b/e2e/various-suite/mysql.spec.ts index e35f5c6fd1..b78db4bfe1 100644 --- a/e2e/various-suite/mysql.spec.ts +++ b/e2e/various-suite/mysql.spec.ts @@ -37,6 +37,9 @@ describe('MySQL datasource', () => { it.skip('code editor autocomplete should handle table name escaping/quoting', () => { e2e.components.RadioButton.container().filter(':contains("Code")').click(); + e2e.components.CodeEditor.container().children('[data-testid="Spinner"]').should('not.exist'); + cy.window().its('monaco').should('exist'); + cy.get('textarea').type('S{downArrow}{enter}'); cy.wait('@tables'); cy.get('.suggest-widget').contains(tableNameWithSpecialCharacter).should('be.visible'); @@ -88,8 +91,10 @@ describe('MySQL datasource', () => { cy.get("[aria-label='Macros value selector']").should('be.visible').click(); selectOption('timeFilter'); - // Validate that the timeFilter macro was added + e2e.components.CodeEditor.container().children('[data-testid="Spinner"]').should('not.exist'); + cy.window().its('monaco').should('exist'); + // Validate that the timeFilter macro was added e2e.components.CodeEditor.container() .get('textarea') .should( diff --git a/e2e/various-suite/query-editor.spec.ts b/e2e/various-suite/query-editor.spec.ts index 2263f1ffac..3441f2b4c0 100644 --- a/e2e/various-suite/query-editor.spec.ts +++ b/e2e/various-suite/query-editor.spec.ts @@ -1,4 +1,5 @@ import { e2e } from '../utils'; +import { waitForMonacoToLoad } from '../utils/support/monaco'; describe('Query editor', () => { beforeEach(() => { @@ -14,13 +15,8 @@ describe('Query editor', () => { e2e.components.RadioButton.container().filter(':contains("Code")').click(); - // we need to wait for the query-field being lazy-loaded, in two steps: - // it is a two-step process: - // 1. first we wait for the text 'Loading...' to appear - // 1. then we wait for the text 'Loading...' to disappear - const monacoLoadingText = 'Loading...'; - e2e.components.QueryField.container().should('be.visible').should('have.text', monacoLoadingText); - e2e.components.QueryField.container().should('be.visible').should('not.have.text', monacoLoadingText); + waitForMonacoToLoad(); + e2e.components.QueryField.container().type(queryText, { parseSpecialCharSequences: false }).type('{backspace}'); cy.contains(queryText.slice(0, -1)).should('be.visible'); diff --git a/jest.config.js b/jest.config.js index 17579560e6..14b6e1084f 100644 --- a/jest.config.js +++ b/jest.config.js @@ -15,6 +15,9 @@ const esModules = [ 'leven', 'nanoid', 'monaco-promql', + '@kusto/monaco-kusto', + 'monaco-editor', + 'lodash-es', ].join('|'); module.exports = { @@ -41,7 +44,9 @@ module.exports = { '\\.svg': '/public/test/mocks/svg.ts', '\\.css': '/public/test/mocks/style.ts', 'react-inlinesvg': '/public/test/mocks/react-inlinesvg.tsx', - 'monaco-editor/esm/vs/editor/editor.api': '/public/test/mocks/monaco.ts', + // resolve directly as monaco and kusto don't have main property in package.json which jest needs + '^monaco-editor$': 'monaco-editor/esm/vs/editor/editor.api.js', + '@kusto/monaco-kusto': '@kusto/monaco-kusto/release/esm/monaco.contribution.js', // near-membrane-dom won't work in a nodejs environment. '@locker/near-membrane-dom': '/public/test/mocks/nearMembraneDom.ts', '^@grafana/schema/dist/esm/(.*)$': '/packages/grafana-schema/src/$1', diff --git a/packages/grafana-data/src/monaco/languageRegistry.ts b/packages/grafana-data/src/monaco/languageRegistry.ts index fcfc843ab3..f12cca2932 100644 --- a/packages/grafana-data/src/monaco/languageRegistry.ts +++ b/packages/grafana-data/src/monaco/languageRegistry.ts @@ -4,7 +4,7 @@ import { Registry, RegistryItem } from '../utils/Registry'; * @alpha */ export interface MonacoLanguageRegistryItem extends RegistryItem { - init: () => Promise; + init: () => Worker; } /** diff --git a/packages/grafana-runtime/src/utils/plugin.ts b/packages/grafana-runtime/src/utils/plugin.ts index ce2ab40b5b..9a368e8496 100644 --- a/packages/grafana-runtime/src/utils/plugin.ts +++ b/packages/grafana-runtime/src/utils/plugin.ts @@ -62,11 +62,3 @@ export function getPluginImportUtils(): PluginImportUtils { return pluginImportUtils; } - -// Grafana relies on RequireJS for Monaco Editor to load. -// The SystemJS AMD extra creates a global define which causes RequireJS to silently bail. -// Here we move and reset global define so Monaco Editor loader script continues to work. -// @ts-ignore -window.__grafana_amd_define = window.define; -// @ts-ignore -window.define = undefined; diff --git a/packages/grafana-ui/src/components/Monaco/CodeEditor.tsx b/packages/grafana-ui/src/components/Monaco/CodeEditor.tsx index a7ac0d7fb0..7ab4854ec2 100644 --- a/packages/grafana-ui/src/components/Monaco/CodeEditor.tsx +++ b/packages/grafana-ui/src/components/Monaco/CodeEditor.tsx @@ -119,14 +119,12 @@ class UnthemedCodeEditor extends PureComponent { } }); - const languagePromise = this.loadCustomLanguage(); - if (onChange) { editor.getModel()?.onDidChangeContent(() => onChange(editor.getValue())); } if (onEditorDidMount) { - languagePromise.then(() => onEditorDidMount(editor, monaco)); + onEditorDidMount(editor, monaco); } }; diff --git a/packages/grafana-ui/src/components/Monaco/ReactMonacoEditor.tsx b/packages/grafana-ui/src/components/Monaco/ReactMonacoEditor.tsx index a6db1ce5fc..0f0ab1b3ee 100644 --- a/packages/grafana-ui/src/components/Monaco/ReactMonacoEditor.tsx +++ b/packages/grafana-ui/src/components/Monaco/ReactMonacoEditor.tsx @@ -1,4 +1,5 @@ -import MonacoEditor, { loader as monacoEditorLoader, Monaco } from '@monaco-editor/react'; +import Editor, { loader as monacoEditorLoader, Monaco } from '@monaco-editor/react'; +import * as monaco from 'monaco-editor'; import React, { useCallback } from 'react'; import { useTheme2 } from '../../themes'; @@ -6,11 +7,8 @@ import { useTheme2 } from '../../themes'; import defineThemes from './theme'; import type { ReactMonacoEditorProps } from './types'; -monacoEditorLoader.config({ - paths: { - vs: (window.__grafana_public_path__ ?? 'public/') + 'lib/monaco/min/vs', - }, -}); +// pass the monaco editor to the loader to bypass requirejs +monacoEditorLoader.config({ monaco }); export const ReactMonacoEditor = (props: ReactMonacoEditorProps) => { const { beforeMount } = props; @@ -25,10 +23,6 @@ export const ReactMonacoEditor = (props: ReactMonacoEditorProps) => { ); return ( - + ); }; diff --git a/public/app/app.ts b/public/app/app.ts index cb31f9c58f..30229d999c 100644 --- a/public/app/app.ts +++ b/public/app/app.ts @@ -56,6 +56,7 @@ import { PluginPage } from './core/components/Page/PluginPage'; import { GrafanaContextType, useReturnToPreviousInternal } from './core/context/GrafanaContext'; import { initIconCache } from './core/icons/iconBundle'; import { initializeI18n } from './core/internationalization'; +import { setMonacoEnv } from './core/monacoEnv'; import { interceptLinkClicks } from './core/navigation/patch/interceptLinkClicks'; import { ModalManager } from './core/services/ModalManager'; import { NewFrontendAssetsChecker } from './core/services/NewFrontendAssetsChecker'; @@ -170,7 +171,9 @@ export class GrafanaApp { createAdHocVariableAdapter(), createSystemVariableAdapter(), ]); + monacoLanguageRegistry.setInit(getDefaultMonacoLanguages); + setMonacoEnv(); setQueryRunnerFactory(() => new QueryRunner()); setVariableQueryRunner(new VariableQueryRunner()); diff --git a/public/app/core/monacoEnv.ts b/public/app/core/monacoEnv.ts new file mode 100644 index 0000000000..db63c867a1 --- /dev/null +++ b/public/app/core/monacoEnv.ts @@ -0,0 +1,32 @@ +import { monacoLanguageRegistry } from '@grafana/data'; +import { CorsWorker as Worker } from 'app/core/utils/CorsWorker'; + +export function setMonacoEnv() { + self.MonacoEnvironment = { + getWorker(_moduleId, label) { + const language = monacoLanguageRegistry.getIfExists(label); + + if (language) { + return language.init(); + } + + if (label === 'json') { + return new Worker(new URL('monaco-editor/esm/vs/language/json/json.worker', import.meta.url)); + } + + if (label === 'css' || label === 'scss' || label === 'less') { + return new Worker(new URL('monaco-editor/esm/vs/language/css/css.worker', import.meta.url)); + } + + if (label === 'html' || label === 'handlebars' || label === 'razor') { + return new Worker(new URL('monaco-editor/esm/vs/language/html/html.worker', import.meta.url)); + } + + if (label === 'typescript' || label === 'javascript') { + return new Worker(new URL('monaco-editor/esm/vs/language/typescript/ts.worker', import.meta.url)); + } + + return new Worker(new URL('monaco-editor/esm/vs/editor/editor.worker', import.meta.url)); + }, + }; +} diff --git a/public/app/features/alerting/unified/components/admin/AlertmanagerConfig.test.tsx b/public/app/features/alerting/unified/components/admin/AlertmanagerConfig.test.tsx index bb9fe55c4a..598a95f867 100644 --- a/public/app/features/alerting/unified/components/admin/AlertmanagerConfig.test.tsx +++ b/public/app/features/alerting/unified/components/admin/AlertmanagerConfig.test.tsx @@ -89,7 +89,7 @@ const ui = { describe('Admin config', () => { beforeEach(() => { - jest.resetAllMocks(); + jest.clearAllMocks(); // FIXME: scope down grantUserPermissions(Object.values(AccessControlAction)); mocks.getAllDataSources.mockReturnValue(Object.values(dataSources)); diff --git a/public/app/features/plugins/loader/constants.ts b/public/app/features/plugins/loader/constants.ts index daf840dff2..0bcc4f9737 100644 --- a/public/app/features/plugins/loader/constants.ts +++ b/public/app/features/plugins/loader/constants.ts @@ -1,5 +1,3 @@ export const SHARED_DEPENDENCY_PREFIX = 'package'; export const LOAD_PLUGIN_CSS_REGEX = /^plugins.+\.css$/i; export const JS_CONTENT_TYPE_REGEX = /^(text|application)\/(x-)?javascript(;|$)/; -export const AMD_MODULE_REGEX = - /(?:^\uFEFF?|[^$_a-zA-Z\xA0-\uFFFF.])define\s*\(\s*("[^"]+"\s*,\s*|'[^']+'\s*,\s*)?\s*(\[(\s*(("[^"]+"|'[^']+')\s*,|\/\/.*\r?\n))*(\s*("[^"]+"|'[^']+')\s*,?)?(\s*(\/\/.*\r?\n|\/\*\/))*\s*\]|function\s*|{|[_$a-zA-Z\xA0-\uFFFF][_$a-zA-Z0-9\xA0-\uFFFF]*\))/; diff --git a/public/app/features/plugins/loader/pluginLoader.mock.ts b/public/app/features/plugins/loader/pluginLoader.mock.ts index c683090ad6..b743371656 100644 --- a/public/app/features/plugins/loader/pluginLoader.mock.ts +++ b/public/app/features/plugins/loader/pluginLoader.mock.ts @@ -23,68 +23,6 @@ export const mockAmdModule = `define([], function() { } });`; -export const mockAmdModuleNamedNoDeps = `define("named", function() { - return function() { - console.log('AMD module loaded'); - var pluginPath = "/public/plugins/"; - } -});`; - -export const mockAmdModuleNamedWithDeps = `define("named", ["dep"], function(dep) { - return function() { - console.log('AMD module loaded'); - var pluginPath = "/public/plugins/"; - } -});`; - -export const mockAmdModuleNamedWithDeps2 = `define("named", ["dep", "dep2"], function(dep, dep2) { - return function() { - console.log('AMD module loaded'); - var pluginPath = "/public/plugins/"; - } -});`; - -export const mockAmdModuleNamedWithDeps3 = `define("named", ["dep", -"dep2" -], function(dep, dep2) { - return function() { - console.log('AMD module loaded'); - var pluginPath = "/public/plugins/"; - } -});`; - -export const mockAmdModuleOnlyFunction = `define(function() { - return function() { - console.log('AMD module loaded'); - var pluginPath = "/public/plugins/"; - } -});`; - -export const mockAmdModuleWithComments = `/*! For license information please see module.js.LICENSE.txt */ -define(function(react) { - return function() { - console.log('AMD module loaded'); - var pluginPath = "/public/plugins/"; - } -});`; - -export const mockAmdModuleWithComments2 = `/*! This is a commment */ -define(["dep"], - /*! This is a commment */ - function(dep) { - return function() { - console.log('AMD module loaded'); - var pluginPath = "/public/plugins/"; - } -});`; - -export const mockModuleWithDefineMethod = `ace.define(function() { - return function() { - console.log('AMD module loaded'); - var pluginPath = "/public/plugins/"; - } -});`; - const server = setupServer( http.get( '/public/plugins/mockAmdModule/module.js', @@ -112,78 +50,6 @@ const server = setupServer( 'Content-Type': 'text/javascript', }, }) - ), - http.get( - '/public/plugins/mockAmdModuleNamedNoDeps/module.js', - () => - new HttpResponse(mockAmdModuleNamedNoDeps, { - headers: { - 'Content-Type': 'text/javascript', - }, - }) - ), - http.get( - '/public/plugins/mockAmdModuleNamedWithDeps/module.js', - () => - new HttpResponse(mockAmdModuleNamedWithDeps, { - headers: { - 'Content-Type': 'text/javascript', - }, - }) - ), - http.get( - '/public/plugins/mockAmdModuleNamedWithDeps2/module.js', - () => - new HttpResponse(mockAmdModuleNamedWithDeps2, { - headers: { - 'Content-Type': 'text/javascript', - }, - }) - ), - http.get( - '/public/plugins/mockAmdModuleNamedWithDeps3/module.js', - () => - new HttpResponse(mockAmdModuleNamedWithDeps3, { - headers: { - 'Content-Type': 'text/javascript', - }, - }) - ), - http.get( - '/public/plugins/mockAmdModuleOnlyFunction/module.js', - () => - new HttpResponse(mockAmdModuleOnlyFunction, { - headers: { - 'Content-Type': 'text/javascript', - }, - }) - ), - http.get( - '/public/plugins/mockAmdModuleWithComments/module.js', - () => - new HttpResponse(mockAmdModuleWithComments, { - headers: { - 'Content-Type': 'text/javascript', - }, - }) - ), - http.get( - '/public/plugins/mockAmdModuleWithComments2/module.js', - () => - new HttpResponse(mockAmdModuleWithComments2, { - headers: { - 'Content-Type': 'text/javascript', - }, - }) - ), - http.get( - '/public/plugins/mockModuleWithDefineMethod/module.js', - () => - new HttpResponse(mockModuleWithDefineMethod, { - headers: { - 'Content-Type': 'text/javascript', - }, - }) ) ); diff --git a/public/app/features/plugins/loader/sharedDependencies.ts b/public/app/features/plugins/loader/sharedDependencies.ts index d1c29ba9b3..3f9c3d1034 100644 --- a/public/app/features/plugins/loader/sharedDependencies.ts +++ b/public/app/features/plugins/loader/sharedDependencies.ts @@ -1,5 +1,6 @@ import * as emotion from '@emotion/css'; import * as emotionReact from '@emotion/react'; +import * as kusto from '@kusto/monaco-kusto'; import * as d3 from 'd3'; import * as i18next from 'i18next'; import jquery from 'jquery'; @@ -71,6 +72,7 @@ export const sharedDependenciesMap: Record = { '@grafana/runtime': grafanaRuntime, '@grafana/slate-react': slateReact, // for backwards compatibility with older plugins '@grafana/ui': grafanaUI, + '@kusto/monaco-kusto': kusto, 'app/core/app_events': { default: appEvents, __useDefault: true, diff --git a/public/app/features/plugins/loader/systemjsHooks.test.ts b/public/app/features/plugins/loader/systemjsHooks.test.ts index 9cd499b0c0..a0da31252a 100644 --- a/public/app/features/plugins/loader/systemjsHooks.test.ts +++ b/public/app/features/plugins/loader/systemjsHooks.test.ts @@ -7,19 +7,7 @@ jest.mock('./cache', () => ({ resolveWithCache: (url: string) => `${url}?_cache=1234`, })); -import { - server, - mockAmdModule, - mockSystemModule, - mockAmdModuleNamedNoDeps, - mockAmdModuleNamedWithDeps, - mockAmdModuleNamedWithDeps2, - mockAmdModuleNamedWithDeps3, - mockAmdModuleOnlyFunction, - mockAmdModuleWithComments, - mockModuleWithDefineMethod, - mockAmdModuleWithComments2, -} from './pluginLoader.mock'; +import { server } from './pluginLoader.mock'; import { decorateSystemJSFetch, decorateSystemJSResolve } from './systemjsHooks'; import { SystemJSWithLoaderHooks } from './types'; @@ -44,116 +32,6 @@ describe('SystemJS Loader Hooks', () => { }); describe('decorateSystemJSFetch', () => { - it('wraps AMD modules in an AMD iife', async () => { - const basicResult = await decorateSystemJSFetch( - systemJSPrototype.fetch, - '/public/plugins/mockAmdModule/module.js', - {} - ); - const basicSource = await basicResult.text(); - const basicExpected = `(function(define) { - ${mockAmdModule} -})(window.__grafana_amd_define);`; - expect(basicSource).toBe(basicExpected); - - const mockAmdModuleNamedNoDepsResult = await decorateSystemJSFetch( - systemJSPrototype.fetch, - '/public/plugins/mockAmdModuleNamedNoDeps/module.js', - {} - ); - const mockAmdModuleNamedNoDepsSource = await mockAmdModuleNamedNoDepsResult.text(); - const mockAmdModuleNamedNoDepsExpected = `(function(define) { - ${mockAmdModuleNamedNoDeps} -})(window.__grafana_amd_define);`; - - expect(mockAmdModuleNamedNoDepsSource).toBe(mockAmdModuleNamedNoDepsExpected); - - const mockAmdModuleNamedWithDepsResult = await decorateSystemJSFetch( - systemJSPrototype.fetch, - '/public/plugins/mockAmdModuleNamedWithDeps/module.js', - {} - ); - const mockAmdModuleNamedWithDepsSource = await mockAmdModuleNamedWithDepsResult.text(); - const mockAmdModuleNamedWithDepsExpected = `(function(define) { - ${mockAmdModuleNamedWithDeps} -})(window.__grafana_amd_define);`; - - expect(mockAmdModuleNamedWithDepsSource).toBe(mockAmdModuleNamedWithDepsExpected); - - const mockAmdModuleNamedWithDeps2Result = await decorateSystemJSFetch( - systemJSPrototype.fetch, - '/public/plugins/mockAmdModuleNamedWithDeps2/module.js', - {} - ); - const mockAmdModuleNamedWithDeps2Source = await mockAmdModuleNamedWithDeps2Result.text(); - const mockAmdModuleNamedWithDeps2Expected = `(function(define) { - ${mockAmdModuleNamedWithDeps2} -})(window.__grafana_amd_define);`; - - expect(mockAmdModuleNamedWithDeps2Source).toBe(mockAmdModuleNamedWithDeps2Expected); - - const mockAmdModuleNamedWithDeps3Result = await decorateSystemJSFetch( - systemJSPrototype.fetch, - '/public/plugins/mockAmdModuleNamedWithDeps3/module.js', - {} - ); - const mockAmdModuleNamedWithDeps3Source = await mockAmdModuleNamedWithDeps3Result.text(); - const mockAmdModuleNamedWithDeps3Expected = `(function(define) { - ${mockAmdModuleNamedWithDeps3} -})(window.__grafana_amd_define);`; - - expect(mockAmdModuleNamedWithDeps3Source).toBe(mockAmdModuleNamedWithDeps3Expected); - - const mockAmdModuleOnlyFunctionResult = await decorateSystemJSFetch( - systemJSPrototype.fetch, - '/public/plugins/mockAmdModuleOnlyFunction/module.js', - {} - ); - const mockAmdModuleOnlyFunctionSource = await mockAmdModuleOnlyFunctionResult.text(); - const mockAmdModuleOnlyFunctionExpected = `(function(define) { - ${mockAmdModuleOnlyFunction} -})(window.__grafana_amd_define);`; - - expect(mockAmdModuleOnlyFunctionSource).toBe(mockAmdModuleOnlyFunctionExpected); - - const mockAmdModuleWithCommentsResult = await decorateSystemJSFetch( - systemJSPrototype.fetch, - '/public/plugins/mockAmdModuleWithComments/module.js', - {} - ); - const mockAmdModuleWithCommentsSource = await mockAmdModuleWithCommentsResult.text(); - const mockAmdModuleWithCommentsExpected = `(function(define) { - ${mockAmdModuleWithComments} -})(window.__grafana_amd_define);`; - - expect(mockAmdModuleWithCommentsSource).toBe(mockAmdModuleWithCommentsExpected); - - const mockAmdModuleWithComments2Result = await decorateSystemJSFetch( - systemJSPrototype.fetch, - '/public/plugins/mockAmdModuleWithComments2/module.js', - {} - ); - const mockAmdModuleWithComments2Source = await mockAmdModuleWithComments2Result.text(); - const mockAmdModuleWithComments2Expected = `(function(define) { - ${mockAmdModuleWithComments2} -})(window.__grafana_amd_define);`; - - expect(mockAmdModuleWithComments2Source).toBe(mockAmdModuleWithComments2Expected); - }); - it("doesn't wrap system modules in an AMD iife", async () => { - const url = '/public/plugins/mockSystemModule/module.js'; - const result = await decorateSystemJSFetch(systemJSPrototype.fetch, url, {}); - const source = await result.text(); - - expect(source).toBe(mockSystemModule); - }); - it("doesn't wrap modules with a define method in an AMD iife", async () => { - const url = '/public/plugins/mockModuleWithDefineMethod/module.js'; - const result = await decorateSystemJSFetch(systemJSPrototype.fetch, url, {}); - const source = await result.text(); - - expect(source).toBe(mockModuleWithDefineMethod); - }); it('only transforms plugin source code hosted on cdn with cdn paths', async () => { config.pluginsCDNBaseURL = 'http://my-cdn.com/plugins'; const cdnUrl = 'http://my-cdn.com/plugins/my-plugin/v1.0.0/public/plugins/my-plugin/module.js'; diff --git a/public/app/features/plugins/loader/systemjsHooks.ts b/public/app/features/plugins/loader/systemjsHooks.ts index 8bdbd723b1..fd8ac2e10e 100644 --- a/public/app/features/plugins/loader/systemjsHooks.ts +++ b/public/app/features/plugins/loader/systemjsHooks.ts @@ -3,7 +3,7 @@ import { config, SystemJS } from '@grafana/runtime'; import { transformPluginSourceForCDN } from '../cdn/utils'; import { resolveWithCache } from './cache'; -import { LOAD_PLUGIN_CSS_REGEX, JS_CONTENT_TYPE_REGEX, AMD_MODULE_REGEX, SHARED_DEPENDENCY_PREFIX } from './constants'; +import { LOAD_PLUGIN_CSS_REGEX, JS_CONTENT_TYPE_REGEX, SHARED_DEPENDENCY_PREFIX } from './constants'; import { SystemJSWithLoaderHooks } from './types'; import { isHostedOnCDN } from './utils'; @@ -19,10 +19,6 @@ export async function decorateSystemJSFetch( const source = await res.text(); let transformedSrc = source; - if (AMD_MODULE_REGEX.test(transformedSrc)) { - transformedSrc = preventAMDLoaderCollision(source); - } - // JS files on the CDN need their asset paths transformed in the source if (isHostedOnCDN(res.url)) { const cdnTransformedSrc = transformPluginSourceForCDN({ url: res.url, source: transformedSrc }); @@ -84,11 +80,3 @@ function getBackWardsCompatibleUrl(url: string) { return hasValidFileExtension ? url : url + '.js'; } - -// This transform prevents a conflict between systemjs and requirejs which Monaco Editor -// depends on. See packages/grafana-runtime/src/utils/plugin.ts for more. -function preventAMDLoaderCollision(source: string) { - return `(function(define) { - ${source} -})(window.__grafana_amd_define);`; -} diff --git a/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/QueryField.tsx b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/QueryField.tsx index 2311c73342..2c978f0a8f 100644 --- a/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/QueryField.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/QueryField.tsx @@ -1,5 +1,4 @@ -import { EngineSchema, Schema } from '@kusto/monaco-kusto'; -import { Uri } from 'monaco-editor'; +import { EngineSchema, getKustoWorker } from '@kusto/monaco-kusto'; import React, { useCallback, useEffect, useState } from 'react'; import { CodeEditor, Monaco, MonacoEditor } from '@grafana/ui'; @@ -13,16 +12,6 @@ interface MonacoEditorValues { monaco: Monaco; } -interface MonacoLanguages { - kusto: { - getKustoWorker: () => Promise< - (url: Uri) => Promise<{ - setSchema: (schema: Schema) => void; - }> - >; - }; -} - const QueryField = ({ query, onQueryChange, schema }: AzureQueryEditorFieldProps) => { const [monaco, setMonaco] = useState(); @@ -33,10 +22,9 @@ const QueryField = ({ query, onQueryChange, schema }: AzureQueryEditorFieldProps const setupEditor = async ({ monaco, editor }: MonacoEditorValues, schema: EngineSchema) => { try { - const languages = monaco.languages as unknown as MonacoLanguages; const model = editor.getModel(); if (model) { - const kustoWorker = await languages.kusto.getKustoWorker(); + const kustoWorker = await getKustoWorker(); const kustoMode = await kustoWorker(model?.uri); await kustoMode.setSchema(schema); } diff --git a/public/app/plugins/datasource/parca/QueryEditor/QueryEditor.test.tsx b/public/app/plugins/datasource/parca/QueryEditor/QueryEditor.test.tsx index 8837006aed..d9bcac8a2f 100644 --- a/public/app/plugins/datasource/parca/QueryEditor/QueryEditor.test.tsx +++ b/public/app/plugins/datasource/parca/QueryEditor/QueryEditor.test.tsx @@ -1,8 +1,10 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; +import { of } from 'rxjs'; import { CoreApp, DataSourcePluginMeta, PluginType } from '@grafana/data'; +import { BackendSrv, getBackendSrv, setBackendSrv } from '@grafana/runtime'; import { ParcaDataSource } from '../datasource'; import { ProfileTypeMessage } from '../types'; @@ -10,6 +12,17 @@ import { ProfileTypeMessage } from '../types'; import { Props, QueryEditor } from './QueryEditor'; describe('QueryEditor', () => { + let origBackendSrv: BackendSrv; + const fetchMock = jest.fn().mockReturnValue(of({ data: [] })); + + beforeEach(() => { + origBackendSrv = getBackendSrv(); + }); + + afterEach(() => { + setBackendSrv(origBackendSrv); + }); + it('should render without error', async () => { setup(); @@ -17,6 +30,7 @@ describe('QueryEditor', () => { }); it('should render options', async () => { + setBackendSrv({ ...origBackendSrv, fetch: fetchMock }); setup(); await openOptions(); expect(screen.getByText(/Metric/)).toBeDefined(); @@ -25,6 +39,7 @@ describe('QueryEditor', () => { }); it('should render correct options outside of explore', async () => { + setBackendSrv({ ...origBackendSrv, fetch: fetchMock }); setup({ props: { app: CoreApp.Dashboard } }); await openOptions(); expect(screen.getByText(/Metric/)).toBeDefined(); diff --git a/public/app/plugins/panel/nodeGraph/layout.worker.js b/public/app/plugins/panel/nodeGraph/layout.worker.js index 205289a1fc..d0fafa0c54 100644 --- a/public/app/plugins/panel/nodeGraph/layout.worker.js +++ b/public/app/plugins/panel/nodeGraph/layout.worker.js @@ -1,176 +1,7 @@ -import { forceSimulation, forceLink, forceCollide, forceX } from 'd3-force'; +import { layout } from './layout.worker.utils'; addEventListener('message', (event) => { const { nodes, edges, config } = event.data; layout(nodes, edges, config); postMessage({ nodes, edges }); }); - -/** - * Use d3 force layout to lay the nodes in a sensible way. This function modifies the nodes adding the x,y positions - * and also fills in node references in edges instead of node ids. - */ -export function layout(nodes, edges, config) { - // Start with some hardcoded positions so it starts laid out from left to right - let { roots, secondLevelRoots } = initializePositions(nodes, edges); - - // There always seems to be one or more root nodes each with single edge and we want to have them static on the - // left neatly in something like grid layout - [...roots, ...secondLevelRoots].forEach((n, index) => { - n.fx = n.x; - }); - - const simulation = forceSimulation(nodes) - .force( - 'link', - forceLink(edges) - .id((d) => d.id) - .distance(config.linkDistance) - .strength(config.linkStrength) - ) - // to keep the left to right layout we add force that pulls all nodes to right but because roots are fixed it will - // apply only to non root nodes - .force('x', forceX(config.forceX).strength(config.forceXStrength)) - // Make sure nodes don't overlap - .force('collide', forceCollide(config.forceCollide)); - - // 300 ticks for the simulation are recommended but less would probably work too, most movement is done in first - // few iterations and then all the forces gets smaller https://github.com/d3/d3-force#simulation_alphaDecay - simulation.tick(config.tick); - simulation.stop(); - - // We do centering here instead of using centering force to keep this more stable - centerNodes(nodes); -} - -/** - * This initializes positions of the graph by going from the root to its children and laying it out in a grid from left - * to right. This works only so, so because service map graphs can have cycles and children levels are not ordered in a - * way to minimize the edge lengths. Nevertheless this seems to make the graph easier to nudge with the forces later on - * than with the d3 default initial positioning. Also we can fix the root positions later on for a bit more neat - * organisation. - * - * This function directly modifies the nodes given and only returns references to root nodes so they do not have to be - * found again later on. - * - * How the spacing could look like approximately: - * 0 - 0 - 0 - 0 - * \- 0 - 0 | - * \- 0 -/ - * 0 - 0 -/ - */ -function initializePositions(nodes, edges) { - // To prevent going in cycles - const alreadyPositioned = {}; - - const nodesMap = nodes.reduce((acc, node) => { - acc[node.id] = node; - return acc; - }, {}); - const edgesMap = edges.reduce((acc, edge) => { - const sourceId = edge.source; - acc[sourceId] = [...(acc[sourceId] || []), edge]; - return acc; - }, {}); - - let roots = nodes.filter((n) => n.incoming === 0); - - // For things like service maps we assume there is some root (client) node but if there is none then selecting - // any node as a starting point should work the same. - if (!roots.length) { - roots = [nodes[0]]; - } - - let secondLevelRoots = roots.reduce((acc, r) => { - acc.push(...(edgesMap[r.id] ? edgesMap[r.id].map((e) => nodesMap[e.target]) : [])); - return acc; - }, []); - - const rootYSpacing = 300; - const nodeYSpacing = 200; - const nodeXSpacing = 200; - - let rootY = 0; - for (const root of roots) { - let graphLevel = [root]; - let x = 0; - while (graphLevel.length > 0) { - const nextGraphLevel = []; - let y = rootY; - for (const node of graphLevel) { - if (alreadyPositioned[node.id]) { - continue; - } - // Initialize positions based on the spacing in the grid - node.x = x; - node.y = y; - alreadyPositioned[node.id] = true; - - // Move to next Y position for next node - y += nodeYSpacing; - if (edgesMap[node.id]) { - nextGraphLevel.push(...edgesMap[node.id].map((edge) => nodesMap[edge.target])); - } - } - - graphLevel = nextGraphLevel; - // Move to next X position for next level - x += nodeXSpacing; - // Reset Y back to baseline for this root - y = rootY; - } - rootY += rootYSpacing; - } - return { roots, secondLevelRoots }; -} - -/** - * Makes sure that the center of the graph based on its bound is in 0, 0 coordinates. - * Modifies the nodes directly. - */ -function centerNodes(nodes) { - const bounds = graphBounds(nodes); - for (let node of nodes) { - node.x = node.x - bounds.center.x; - node.y = node.y - bounds.center.y; - } -} - -/** - * Get bounds of the graph meaning the extent of the nodes in all directions. - */ -function graphBounds(nodes) { - if (nodes.length === 0) { - return { top: 0, right: 0, bottom: 0, left: 0, center: { x: 0, y: 0 } }; - } - - const bounds = nodes.reduce( - (acc, node) => { - if (node.x > acc.right) { - acc.right = node.x; - } - if (node.x < acc.left) { - acc.left = node.x; - } - if (node.y > acc.bottom) { - acc.bottom = node.y; - } - if (node.y < acc.top) { - acc.top = node.y; - } - return acc; - }, - { top: Infinity, right: -Infinity, bottom: -Infinity, left: Infinity } - ); - - const y = bounds.top + (bounds.bottom - bounds.top) / 2; - const x = bounds.left + (bounds.right - bounds.left) / 2; - - return { - ...bounds, - center: { - x, - y, - }, - }; -} diff --git a/public/app/plugins/panel/nodeGraph/layout.worker.utils.js b/public/app/plugins/panel/nodeGraph/layout.worker.utils.js new file mode 100644 index 0000000000..9c30af6a1a --- /dev/null +++ b/public/app/plugins/panel/nodeGraph/layout.worker.utils.js @@ -0,0 +1,174 @@ +// This file is a workaround so the layout function can be imported in Jest mocks. If the jest mock imports the +// layout.worker.js file it will attach the eventlistener and then call the layout function with undefined data +// which causes tests to fail. + +import { forceSimulation, forceLink, forceCollide, forceX } from 'd3-force'; + +/** + * Use d3 force layout to lay the nodes in a sensible way. This function modifies the nodes adding the x,y positions + * and also fills in node references in edges instead of node ids. + */ +export function layout(nodes, edges, config) { + // Start with some hardcoded positions so it starts laid out from left to right + let { roots, secondLevelRoots } = initializePositions(nodes, edges); + + // There always seems to be one or more root nodes each with single edge and we want to have them static on the + // left neatly in something like grid layout + [...roots, ...secondLevelRoots].forEach((n, index) => { + n.fx = n.x; + }); + + const simulation = forceSimulation(nodes) + .force( + 'link', + forceLink(edges) + .id((d) => d.id) + .distance(config.linkDistance) + .strength(config.linkStrength) + ) + // to keep the left to right layout we add force that pulls all nodes to right but because roots are fixed it will + // apply only to non root nodes + .force('x', forceX(config.forceX).strength(config.forceXStrength)) + // Make sure nodes don't overlap + .force('collide', forceCollide(config.forceCollide)); + + // 300 ticks for the simulation are recommended but less would probably work too, most movement is done in first + // few iterations and then all the forces gets smaller https://github.com/d3/d3-force#simulation_alphaDecay + simulation.tick(config.tick); + simulation.stop(); + + // We do centering here instead of using centering force to keep this more stable + centerNodes(nodes); +} + +/** + * This initializes positions of the graph by going from the root to its children and laying it out in a grid from left + * to right. This works only so, so because service map graphs can have cycles and children levels are not ordered in a + * way to minimize the edge lengths. Nevertheless this seems to make the graph easier to nudge with the forces later on + * than with the d3 default initial positioning. Also we can fix the root positions later on for a bit more neat + * organisation. + * + * This function directly modifies the nodes given and only returns references to root nodes so they do not have to be + * found again later on. + * + * How the spacing could look like approximately: + * 0 - 0 - 0 - 0 + * \- 0 - 0 | + * \- 0 -/ + * 0 - 0 -/ + */ +function initializePositions(nodes, edges) { + // To prevent going in cycles + const alreadyPositioned = {}; + + const nodesMap = nodes.reduce((acc, node) => { + acc[node.id] = node; + return acc; + }, {}); + const edgesMap = edges.reduce((acc, edge) => { + const sourceId = edge.source; + acc[sourceId] = [...(acc[sourceId] || []), edge]; + return acc; + }, {}); + + let roots = nodes.filter((n) => n.incoming === 0); + + // For things like service maps we assume there is some root (client) node but if there is none then selecting + // any node as a starting point should work the same. + if (!roots.length) { + roots = [nodes[0]]; + } + + let secondLevelRoots = roots.reduce((acc, r) => { + acc.push(...(edgesMap[r.id] ? edgesMap[r.id].map((e) => nodesMap[e.target]) : [])); + return acc; + }, []); + + const rootYSpacing = 300; + const nodeYSpacing = 200; + const nodeXSpacing = 200; + + let rootY = 0; + for (const root of roots) { + let graphLevel = [root]; + let x = 0; + while (graphLevel.length > 0) { + const nextGraphLevel = []; + let y = rootY; + for (const node of graphLevel) { + if (alreadyPositioned[node.id]) { + continue; + } + // Initialize positions based on the spacing in the grid + node.x = x; + node.y = y; + alreadyPositioned[node.id] = true; + + // Move to next Y position for next node + y += nodeYSpacing; + if (edgesMap[node.id]) { + nextGraphLevel.push(...edgesMap[node.id].map((edge) => nodesMap[edge.target])); + } + } + + graphLevel = nextGraphLevel; + // Move to next X position for next level + x += nodeXSpacing; + // Reset Y back to baseline for this root + y = rootY; + } + rootY += rootYSpacing; + } + return { roots, secondLevelRoots }; +} + +/** + * Makes sure that the center of the graph based on its bound is in 0, 0 coordinates. + * Modifies the nodes directly. + */ +function centerNodes(nodes) { + const bounds = graphBounds(nodes); + for (let node of nodes) { + node.x = node.x - bounds.center.x; + node.y = node.y - bounds.center.y; + } +} + +/** + * Get bounds of the graph meaning the extent of the nodes in all directions. + */ +function graphBounds(nodes) { + if (nodes.length === 0) { + return { top: 0, right: 0, bottom: 0, left: 0, center: { x: 0, y: 0 } }; + } + + const bounds = nodes.reduce( + (acc, node) => { + if (node.x > acc.right) { + acc.right = node.x; + } + if (node.x < acc.left) { + acc.left = node.x; + } + if (node.y > acc.bottom) { + acc.bottom = node.y; + } + if (node.y < acc.top) { + acc.top = node.y; + } + return acc; + }, + { top: Infinity, right: -Infinity, bottom: -Infinity, left: Infinity } + ); + + const y = bounds.top + (bounds.bottom - bounds.top) / 2; + const x = bounds.left + (bounds.right - bounds.left) / 2; + + return { + ...bounds, + center: { + x, + y, + }, + }; +} diff --git a/public/lib/monaco-languages/kusto.ts b/public/lib/monaco-languages/kusto.ts index 92adf5f801..27a10c4a83 100644 --- a/public/lib/monaco-languages/kusto.ts +++ b/public/lib/monaco-languages/kusto.ts @@ -1,5 +1,5 @@ +import { CorsWorker as Worker } from 'app/core/utils/CorsWorker'; + export default function loadKusto() { - return new Promise((resolve) => - __non_webpack_require__(['vs/language/kusto/monaco.contribution'], () => resolve()) - ); + return new Worker(new URL('@kusto/monaco-kusto/release/esm/kusto.worker', import.meta.url)); } diff --git a/public/test/mocks/monaco.ts b/public/test/mocks/monaco.ts deleted file mode 100644 index 2de6c5f4a2..0000000000 --- a/public/test/mocks/monaco.ts +++ /dev/null @@ -1 +0,0 @@ -export const monaco = 'monaco'; diff --git a/public/test/mocks/workers.ts b/public/test/mocks/workers.ts index ddba6e1e38..34ae57a586 100644 --- a/public/test/mocks/workers.ts +++ b/public/test/mocks/workers.ts @@ -1,7 +1,7 @@ import { Config } from 'app/plugins/panel/nodeGraph/layout'; import { EdgeDatum, NodeDatum } from 'app/plugins/panel/nodeGraph/types'; -const { layout } = jest.requireActual('../../app/plugins/panel/nodeGraph/layout.worker.js'); +const { layout } = jest.requireActual('../../app/plugins/panel/nodeGraph/layout.worker.utils.js'); class LayoutMockWorker { timeout: number | undefined; diff --git a/public/test/setupTests.ts b/public/test/setupTests.ts index b345588c35..650614f512 100644 --- a/public/test/setupTests.ts +++ b/public/test/setupTests.ts @@ -1,4 +1,5 @@ import '@testing-library/jest-dom'; +import { configure } from '@testing-library/react'; import i18next from 'i18next'; import failOnConsole from 'jest-fail-on-console'; import { initReactI18next } from 'react-i18next'; @@ -20,3 +21,7 @@ i18next.use(initReactI18next).init({ returnEmptyString: false, lng: 'en-US', // this should be the locale of the phrases in our source JSX }); + +// our tests are heavy in CI due to parallelisation and monaco and kusto +// so we increase the default timeout to 2secs to avoid flakiness +configure({ asyncUtilTimeout: 2000 }); diff --git a/scripts/webpack/webpack.common.js b/scripts/webpack/webpack.common.js index c9db4d4820..b3c9cc178e 100644 --- a/scripts/webpack/webpack.common.js +++ b/scripts/webpack/webpack.common.js @@ -1,4 +1,3 @@ -const CopyWebpackPlugin = require('copy-webpack-plugin'); const path = require('path'); const webpack = require('webpack'); @@ -52,7 +51,13 @@ module.exports = { string_decoder: false, }, }, - ignoreWarnings: [/export .* was not found in/], + ignoreWarnings: [ + /export .* was not found in/, + { + module: /@kusto\/language-service\/bridge\.min\.js$/, + message: /^Critical dependency: the request of a dependency is an expression$/, + }, + ], stats: { children: false, source: false, @@ -65,25 +70,6 @@ module.exports = { new webpack.ProvidePlugin({ Buffer: ['buffer', 'Buffer'], }), - new CopyWebpackPlugin({ - patterns: [ - { - context: path.join(require.resolve('monaco-editor/package.json'), '../min/vs/'), - from: '**/*', - to: '../lib/monaco/min/vs/', // inside the public/build folder - globOptions: { - ignore: [ - '**/*.map', // debug files - ], - }, - }, - { - context: path.join(require.resolve('@kusto/monaco-kusto/package.json'), '../release/min'), - from: '**/*', - to: '../lib/monaco/min/vs/language/kusto/', - }, - ], - }), ], module: { rules: [ diff --git a/yarn.lock b/yarn.lock index dff382da06..b7d742e7a2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -399,6 +399,15 @@ __metadata: languageName: node linkType: hard +"@babel/parser@npm:^7.22.15": + version: 7.23.6 + resolution: "@babel/parser@npm:7.23.6" + bin: + parser: ./bin/babel-parser.js + checksum: 10/6be3a63d3c9d07b035b5a79c022327cb7e16cbd530140ecb731f19a650c794c315a72c699a22413ebeafaff14aa8f53435111898d59e01a393d741b85629fa7d + languageName: node + linkType: hard + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:^7.22.15, @babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:^7.23.3": version: 7.23.3 resolution: "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:7.23.3" @@ -1679,7 +1688,18 @@ __metadata: languageName: node linkType: hard -"@babel/template@npm:^7.22.15, @babel/template@npm:^7.22.5, @babel/template@npm:^7.23.9, @babel/template@npm:^7.3.3": +"@babel/template@npm:^7.22.15, @babel/template@npm:^7.3.3": + version: 7.22.15 + resolution: "@babel/template@npm:7.22.15" + dependencies: + "@babel/code-frame": "npm:^7.22.13" + "@babel/parser": "npm:^7.22.15" + "@babel/types": "npm:^7.22.15" + checksum: 10/21e768e4eed4d1da2ce5d30aa51db0f4d6d8700bc1821fec6292587df7bba2fe1a96451230de8c64b989740731888ebf1141138bfffb14cacccf4d05c66ad93f + languageName: node + linkType: hard + +"@babel/template@npm:^7.22.5, @babel/template@npm:^7.23.9": version: 7.23.9 resolution: "@babel/template@npm:7.23.9" dependencies: @@ -1708,7 +1728,18 @@ __metadata: languageName: node linkType: hard -"@babel/types@npm:^7.0.0, @babel/types@npm:^7.2.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.22.15, @babel/types@npm:^7.22.19, @babel/types@npm:^7.22.5, @babel/types@npm:^7.23.0, @babel/types@npm:^7.23.6, @babel/types@npm:^7.23.9, @babel/types@npm:^7.3.0, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3": +"@babel/types@npm:^7.0.0, @babel/types@npm:^7.2.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.22.15, @babel/types@npm:^7.22.19, @babel/types@npm:^7.22.5, @babel/types@npm:^7.23.0, @babel/types@npm:^7.23.6, @babel/types@npm:^7.3.0, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3": + version: 7.23.6 + resolution: "@babel/types@npm:7.23.6" + dependencies: + "@babel/helper-string-parser": "npm:^7.23.4" + "@babel/helper-validator-identifier": "npm:^7.22.20" + to-fast-properties: "npm:^2.0.0" + checksum: 10/07e70bb94d30b0231396b5e9a7726e6d9227a0a62e0a6830c0bd3232f33b024092e3d5a7d1b096a65bbf2bb43a9ab4c721bf618e115bfbb87b454fa060f88cbf + languageName: node + linkType: hard + +"@babel/types@npm:^7.23.9": version: 7.23.9 resolution: "@babel/types@npm:7.23.9" dependencies: @@ -3749,23 +3780,23 @@ __metadata: languageName: node linkType: hard -"@grafana/faro-core@npm:^1.3.6, @grafana/faro-core@npm:^1.3.7": - version: 1.3.7 - resolution: "@grafana/faro-core@npm:1.3.7" +"@grafana/faro-core@npm:^1.3.6, @grafana/faro-core@npm:^1.3.8": + version: 1.3.8 + resolution: "@grafana/faro-core@npm:1.3.8" dependencies: "@opentelemetry/api": "npm:^1.7.0" - "@opentelemetry/otlp-transformer": "npm:^0.45.1" - checksum: 10/fa3ff8dce1e6fe5ad91a4d42bb9bdb13f36d594074566a100645ffb4bc509265d18c78c5cda1ef8f39a3f043c1901baee620d3e044b3a0a6e9d1c516bf71f74f + "@opentelemetry/otlp-transformer": "npm:^0.48.0" + checksum: 10/c156b2bd9528f9033b17f720d59235ca24ca2c8016a8dff6c6eb0290f13cf8ab4fcbc08b6fbe5f5e3dba7db0a8ca4ff08bae46f94e615a502c5cd04bdd6c62da languageName: node linkType: hard -"@grafana/faro-core@npm:^1.3.8": - version: 1.3.8 - resolution: "@grafana/faro-core@npm:1.3.8" +"@grafana/faro-core@npm:^1.3.7": + version: 1.3.7 + resolution: "@grafana/faro-core@npm:1.3.7" dependencies: "@opentelemetry/api": "npm:^1.7.0" - "@opentelemetry/otlp-transformer": "npm:^0.48.0" - checksum: 10/c156b2bd9528f9033b17f720d59235ca24ca2c8016a8dff6c6eb0290f13cf8ab4fcbc08b6fbe5f5e3dba7db0a8ca4ff08bae46f94e615a502c5cd04bdd6c62da + "@opentelemetry/otlp-transformer": "npm:^0.45.1" + checksum: 10/fa3ff8dce1e6fe5ad91a4d42bb9bdb13f36d594074566a100645ffb4bc509265d18c78c5cda1ef8f39a3f043c1901baee620d3e044b3a0a6e9d1c516bf71f74f languageName: node linkType: hard @@ -4737,6 +4768,13 @@ __metadata: languageName: node linkType: hard +"@jridgewell/resolve-uri@npm:3.1.0": + version: 3.1.0 + resolution: "@jridgewell/resolve-uri@npm:3.1.0" + checksum: 10/320ceb37af56953757b28e5b90c34556157676d41e3d0a3ff88769274d62373582bb0f0276a4f2d29c3f4fdd55b82b8be5731f52d391ad2ecae9b321ee1c742d + languageName: node + linkType: hard + "@jridgewell/resolve-uri@npm:^3.0.3, @jridgewell/resolve-uri@npm:^3.1.0": version: 3.1.1 resolution: "@jridgewell/resolve-uri@npm:3.1.1" @@ -4751,6 +4789,16 @@ __metadata: 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": "npm:^0.3.0" + "@jridgewell/trace-mapping": "npm:^0.3.9" + checksum: 10/1aaa42075bac32a551708025da0c07b11c11fb05ccd10fb70df2cb0db88773338ab0f33f175d9865379cb855bb3b1cda478367747a1087309fda40a7b9214bfa + languageName: node + linkType: hard + "@jridgewell/source-map@npm:^0.3.3": version: 0.3.5 resolution: "@jridgewell/source-map@npm:0.3.5" @@ -4761,6 +4809,13 @@ __metadata: languageName: node linkType: hard +"@jridgewell/sourcemap-codec@npm:1.4.14": + version: 1.4.14 + resolution: "@jridgewell/sourcemap-codec@npm:1.4.14" + checksum: 10/26e768fae6045481a983e48aa23d8fcd23af5da70ebd74b0649000e815e7fbb01ea2bc088c9176b3fffeb9bec02184e58f46125ef3320b30eaa1f4094cfefa38 + languageName: node + linkType: hard + "@jridgewell/sourcemap-codec@npm:^1.4.10, @jridgewell/sourcemap-codec@npm:^1.4.14, @jridgewell/sourcemap-codec@npm:^1.4.15": version: 1.4.15 resolution: "@jridgewell/sourcemap-codec@npm:1.4.15" @@ -4778,7 +4833,27 @@ __metadata: languageName: node linkType: hard -"@jridgewell/trace-mapping@npm:^0.3.12, @jridgewell/trace-mapping@npm:^0.3.17, @jridgewell/trace-mapping@npm:^0.3.18, @jridgewell/trace-mapping@npm:^0.3.20, @jridgewell/trace-mapping@npm:^0.3.21, @jridgewell/trace-mapping@npm:^0.3.9": +"@jridgewell/trace-mapping@npm:^0.3.12, @jridgewell/trace-mapping@npm:^0.3.17, @jridgewell/trace-mapping@npm:^0.3.18, @jridgewell/trace-mapping@npm:^0.3.9": + version: 0.3.18 + resolution: "@jridgewell/trace-mapping@npm:0.3.18" + dependencies: + "@jridgewell/resolve-uri": "npm:3.1.0" + "@jridgewell/sourcemap-codec": "npm:1.4.14" + checksum: 10/f4fabdddf82398a797bcdbb51c574cd69b383db041a6cae1a6a91478681d6aab340c01af655cfd8c6e01cde97f63436a1445f08297cdd33587621cf05ffa0d55 + languageName: node + linkType: hard + +"@jridgewell/trace-mapping@npm:^0.3.20": + version: 0.3.21 + resolution: "@jridgewell/trace-mapping@npm:0.3.21" + dependencies: + "@jridgewell/resolve-uri": "npm:^3.1.0" + "@jridgewell/sourcemap-codec": "npm:^1.4.14" + checksum: 10/925dda0620887e5a24f11b5a3a106f4e8b1a66155b49be6ceee61432174df33a17c243d8a89b2cd79ccebd281d817878759236a2fc42c47325ae9f73dfbfb90d + languageName: node + linkType: hard + +"@jridgewell/trace-mapping@npm:^0.3.21": version: 0.3.22 resolution: "@jridgewell/trace-mapping@npm:0.3.22" dependencies: @@ -7096,13 +7171,20 @@ __metadata: languageName: node linkType: hard -"@remix-run/router@npm:1.14.2, @remix-run/router@npm:^1.5.0": +"@remix-run/router@npm:1.14.2": version: 1.14.2 resolution: "@remix-run/router@npm:1.14.2" checksum: 10/422844e88b985f1e287301b302c6cf8169c9eea792f80d40464f97b25393bb2e697228ebd7a7b61444d5a51c5873c4a637aad20acde5886a5caf62e833c5ceee languageName: node linkType: hard +"@remix-run/router@npm:^1.5.0": + version: 1.11.0 + resolution: "@remix-run/router@npm:1.11.0" + checksum: 10/629ec578b9dfd3c5cb5de64a0798dd7846ec5ba0351aa66f42b1c65efb43da8f30366be59b825303648965b0df55b638c110949b24ef94fd62e98117fdfb0c0f + languageName: node + linkType: hard + "@rollup/plugin-commonjs@npm:25.0.7": version: 25.0.7 resolution: "@rollup/plugin-commonjs@npm:25.0.7" @@ -8410,6 +8492,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-darwin-arm64@npm:1.3.90": + version: 1.3.90 + resolution: "@swc/core-darwin-arm64@npm:1.3.90" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + "@swc/core-darwin-arm64@npm:1.4.0": version: 1.4.0 resolution: "@swc/core-darwin-arm64@npm:1.4.0" @@ -8424,6 +8513,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-darwin-x64@npm:1.3.90": + version: 1.3.90 + resolution: "@swc/core-darwin-x64@npm:1.3.90" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + "@swc/core-darwin-x64@npm:1.4.0": version: 1.4.0 resolution: "@swc/core-darwin-x64@npm:1.4.0" @@ -8438,6 +8534,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-linux-arm-gnueabihf@npm:1.3.90": + version: 1.3.90 + resolution: "@swc/core-linux-arm-gnueabihf@npm:1.3.90" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + "@swc/core-linux-arm-gnueabihf@npm:1.4.0": version: 1.4.0 resolution: "@swc/core-linux-arm-gnueabihf@npm:1.4.0" @@ -8452,6 +8555,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-linux-arm64-gnu@npm:1.3.90": + version: 1.3.90 + resolution: "@swc/core-linux-arm64-gnu@npm:1.3.90" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + "@swc/core-linux-arm64-gnu@npm:1.4.0": version: 1.4.0 resolution: "@swc/core-linux-arm64-gnu@npm:1.4.0" @@ -8466,6 +8576,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-linux-arm64-musl@npm:1.3.90": + version: 1.3.90 + resolution: "@swc/core-linux-arm64-musl@npm:1.3.90" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + "@swc/core-linux-arm64-musl@npm:1.4.0": version: 1.4.0 resolution: "@swc/core-linux-arm64-musl@npm:1.4.0" @@ -8480,6 +8597,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-linux-x64-gnu@npm:1.3.90": + version: 1.3.90 + resolution: "@swc/core-linux-x64-gnu@npm:1.3.90" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + "@swc/core-linux-x64-gnu@npm:1.4.0": version: 1.4.0 resolution: "@swc/core-linux-x64-gnu@npm:1.4.0" @@ -8494,6 +8618,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-linux-x64-musl@npm:1.3.90": + version: 1.3.90 + resolution: "@swc/core-linux-x64-musl@npm:1.3.90" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + "@swc/core-linux-x64-musl@npm:1.4.0": version: 1.4.0 resolution: "@swc/core-linux-x64-musl@npm:1.4.0" @@ -8508,6 +8639,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-win32-arm64-msvc@npm:1.3.90": + version: 1.3.90 + resolution: "@swc/core-win32-arm64-msvc@npm:1.3.90" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + "@swc/core-win32-arm64-msvc@npm:1.4.0": version: 1.4.0 resolution: "@swc/core-win32-arm64-msvc@npm:1.4.0" @@ -8522,6 +8660,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-win32-ia32-msvc@npm:1.3.90": + version: 1.3.90 + resolution: "@swc/core-win32-ia32-msvc@npm:1.3.90" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + "@swc/core-win32-ia32-msvc@npm:1.4.0": version: 1.4.0 resolution: "@swc/core-win32-ia32-msvc@npm:1.4.0" @@ -8536,6 +8681,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-win32-x64-msvc@npm:1.3.90": + version: 1.3.90 + resolution: "@swc/core-win32-x64-msvc@npm:1.3.90" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@swc/core-win32-x64-msvc@npm:1.4.0": version: 1.4.0 resolution: "@swc/core-win32-x64-msvc@npm:1.4.0" @@ -8550,7 +8702,7 @@ __metadata: languageName: node linkType: hard -"@swc/core@npm:1.4.0, @swc/core@npm:^1.3.49": +"@swc/core@npm:1.4.0": version: 1.4.0 resolution: "@swc/core@npm:1.4.0" dependencies: @@ -8642,6 +8794,52 @@ __metadata: languageName: node linkType: hard +"@swc/core@npm:^1.3.49": + version: 1.3.90 + resolution: "@swc/core@npm:1.3.90" + dependencies: + "@swc/core-darwin-arm64": "npm:1.3.90" + "@swc/core-darwin-x64": "npm:1.3.90" + "@swc/core-linux-arm-gnueabihf": "npm:1.3.90" + "@swc/core-linux-arm64-gnu": "npm:1.3.90" + "@swc/core-linux-arm64-musl": "npm:1.3.90" + "@swc/core-linux-x64-gnu": "npm:1.3.90" + "@swc/core-linux-x64-musl": "npm:1.3.90" + "@swc/core-win32-arm64-msvc": "npm:1.3.90" + "@swc/core-win32-ia32-msvc": "npm:1.3.90" + "@swc/core-win32-x64-msvc": "npm:1.3.90" + "@swc/counter": "npm:^0.1.1" + "@swc/types": "npm:^0.1.5" + peerDependencies: + "@swc/helpers": ^0.5.0 + dependenciesMeta: + "@swc/core-darwin-arm64": + optional: true + "@swc/core-darwin-x64": + optional: true + "@swc/core-linux-arm-gnueabihf": + optional: true + "@swc/core-linux-arm64-gnu": + optional: true + "@swc/core-linux-arm64-musl": + optional: true + "@swc/core-linux-x64-gnu": + optional: true + "@swc/core-linux-x64-musl": + optional: true + "@swc/core-win32-arm64-msvc": + optional: true + "@swc/core-win32-ia32-msvc": + optional: true + "@swc/core-win32-x64-msvc": + optional: true + peerDependenciesMeta: + "@swc/helpers": + optional: true + checksum: 10/214af37af77b968203d495745a86db985734527f4696243bda5fb9ce868830d70e7a2cdbb268da2ee994d9fcedded25073d7b709fa09b75e96f9ba7d13a63da0 + languageName: node + linkType: hard + "@swc/counter@npm:^0.1.1, @swc/counter@npm:^0.1.2, @swc/counter@npm:^0.1.3": version: 0.1.3 resolution: "@swc/counter@npm:0.1.3" @@ -8649,7 +8847,7 @@ __metadata: languageName: node linkType: hard -"@swc/helpers@npm:0.5.6, @swc/helpers@npm:^0.5.0": +"@swc/helpers@npm:0.5.6": version: 0.5.6 resolution: "@swc/helpers@npm:0.5.6" dependencies: @@ -8658,6 +8856,15 @@ __metadata: languageName: node linkType: hard +"@swc/helpers@npm:^0.5.0": + version: 0.5.1 + resolution: "@swc/helpers@npm:0.5.1" + dependencies: + tslib: "npm:^2.4.0" + checksum: 10/4954c4d2dd97bf965e863a10ffa44c3fdaf7653f2fa9ef1a6cf7ffffd67f3f832216588f9751afd75fdeaea60c4688c75c01e2405119c448f1a109c9a7958c54 + languageName: node + linkType: hard + "@swc/types@npm:^0.1.5": version: 0.1.5 resolution: "@swc/types@npm:0.1.5" @@ -8681,7 +8888,7 @@ __metadata: languageName: node linkType: hard -"@testing-library/jest-dom@npm:6.4.2, @testing-library/jest-dom@npm:^6.1.2": +"@testing-library/jest-dom@npm:6.4.2": version: 6.4.2 resolution: "@testing-library/jest-dom@npm:6.4.2" dependencies: @@ -8714,6 +8921,39 @@ __metadata: languageName: node linkType: hard +"@testing-library/jest-dom@npm:^6.1.2": + version: 6.2.1 + resolution: "@testing-library/jest-dom@npm:6.2.1" + dependencies: + "@adobe/css-tools": "npm:^4.3.2" + "@babel/runtime": "npm:^7.9.2" + aria-query: "npm:^5.0.0" + chalk: "npm:^3.0.0" + css.escape: "npm:^1.5.1" + dom-accessibility-api: "npm:^0.6.3" + lodash: "npm:^4.17.15" + redent: "npm:^3.0.0" + peerDependencies: + "@jest/globals": ">= 28" + "@types/bun": "*" + "@types/jest": ">= 28" + jest: ">= 28" + vitest: ">= 0.32" + peerDependenciesMeta: + "@jest/globals": + optional: true + "@types/bun": + optional: true + "@types/jest": + optional: true + jest: + optional: true + vitest: + optional: true + checksum: 10/e522314c4623d2030146570ed5907c71126477272760fa6bce7a2db4ee5dd1edf492ebc9a5f442e99f69d4d6e2a9d0aac49fae41323211dbcee186a1eca04707 + languageName: node + linkType: hard + "@testing-library/react-hooks@npm:^8.0.1": version: 8.0.1 resolution: "@testing-library/react-hooks@npm:8.0.1" @@ -9800,7 +10040,7 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:20.11.19, @types/node@npm:^20.11.16": +"@types/node@npm:20.11.19": version: 20.11.19 resolution: "@types/node@npm:20.11.19" dependencies: @@ -9823,6 +10063,15 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:^20.11.16": + version: 20.11.18 + resolution: "@types/node@npm:20.11.18" + dependencies: + undici-types: "npm:~5.26.4" + checksum: 10/eeaa55032e6702867e96d7b6f98df1d60af09d37ab72f2b905b349ec7e458dfb9c4d9cfc562962f5a51b156a968eea773d8025688f88b735944c81e3ac0e3b7f + languageName: node + linkType: hard + "@types/normalize-package-data@npm:^2.4.0": version: 2.4.1 resolution: "@types/normalize-package-data@npm:2.4.1" @@ -9939,7 +10188,16 @@ __metadata: languageName: node linkType: hard -"@types/react-dom@npm:*, @types/react-dom@npm:18.2.19, @types/react-dom@npm:^18.0.0": +"@types/react-dom@npm:*, @types/react-dom@npm:^18.0.0": + version: 18.2.7 + resolution: "@types/react-dom@npm:18.2.7" + dependencies: + "@types/react": "npm:*" + checksum: 10/9b70ef66cbe2d2898ea37eb79ee3697e0e4ad3d950e769a601f79be94097d43b8ef45b98a0b29528203c7d731c81666f637b2b7032deeced99214b4bc0662614 + languageName: node + linkType: hard + +"@types/react-dom@npm:18.2.19": version: 18.2.19 resolution: "@types/react-dom@npm:18.2.19" dependencies: @@ -11386,6 +11644,15 @@ __metadata: languageName: node linkType: hard +"acorn@npm:^8.5.0": + version: 8.10.0 + resolution: "acorn@npm:8.10.0" + bin: + acorn: bin/acorn + checksum: 10/522310c20fdc3c271caed3caf0f06c51d61cb42267279566edd1d58e83dbc12eebdafaab666a0f0be1b7ad04af9c6bc2a6f478690a9e6391c3c8b165ada917dd + languageName: node + linkType: hard + "add-dom-event-listener@npm:^1.1.0": version: 1.1.0 resolution: "add-dom-event-listener@npm:1.1.0" @@ -12080,13 +12347,20 @@ __metadata: languageName: node linkType: hard -"axe-core@npm:=4.7.0, axe-core@npm:^4.2.0": +"axe-core@npm:=4.7.0": version: 4.7.0 resolution: "axe-core@npm:4.7.0" checksum: 10/615c0f7722c3c9fcf353dbd70b00e2ceae234d4c17cbc839dd85c01d16797c4e4da45f8d27c6118e9e6b033fb06efd196106e13651a1b2f3a10e0f11c7b2f660 languageName: node linkType: hard +"axe-core@npm:^4.2.0": + version: 4.6.3 + resolution: "axe-core@npm:4.6.3" + checksum: 10/280f6a7067129875380f733ae84093ce29c4b8cfe36e1a8ff46bd5d2bcd57d093f11b00223ddf5fef98ca147e0e6568ddd0ada9415cf8ae15d379224bf3cbb51 + languageName: node + linkType: hard + "axios@npm:^1.0.0": version: 1.5.1 resolution: "axios@npm:1.5.1" @@ -12651,7 +12925,21 @@ __metadata: languageName: node linkType: hard -"browserslist@npm:^4.0.0, browserslist@npm:^4.14.5, browserslist@npm:^4.21.10, browserslist@npm:^4.21.4, browserslist@npm:^4.22.2": +"browserslist@npm:^4.0.0, browserslist@npm:^4.14.5, browserslist@npm:^4.21.4, browserslist@npm:^4.22.2": + version: 4.22.2 + resolution: "browserslist@npm:4.22.2" + dependencies: + caniuse-lite: "npm:^1.0.30001565" + electron-to-chromium: "npm:^1.4.601" + node-releases: "npm:^2.0.14" + update-browserslist-db: "npm:^1.0.13" + bin: + browserslist: cli.js + checksum: 10/e3590793db7f66ad3a50817e7b7f195ce61e029bd7187200244db664bfbe0ac832f784e4f6b9c958aef8ea4abe001ae7880b7522682df521f4bc0a5b67660b5e + languageName: node + linkType: hard + +"browserslist@npm:^4.21.10": version: 4.22.3 resolution: "browserslist@npm:4.22.3" dependencies: @@ -12968,6 +13256,13 @@ __metadata: languageName: node linkType: hard +"caniuse-lite@npm:^1.0.30001565": + version: 1.0.30001579 + resolution: "caniuse-lite@npm:1.0.30001579" + checksum: 10/2cd0c02e5d66b09888743ad2b624dbde697ace5c76b55bfd6065ea033f6abea8ac3f5d3c9299c042f91b396e2141b49bc61f5e17086dc9ba3a866cc6790134c0 + languageName: node + linkType: hard + "canvas-hypertxt@npm:^1.0.3": version: 1.0.3 resolution: "canvas-hypertxt@npm:1.0.3" @@ -13239,14 +13534,14 @@ __metadata: languageName: node linkType: hard -"cli-spinners@npm:2.6.1, cli-spinners@npm:^2.5.0": +"cli-spinners@npm:2.6.1": version: 2.6.1 resolution: "cli-spinners@npm:2.6.1" checksum: 10/3e2dc5df72cf02120bebe256881fc8e3ec49867e5023d39f1e7340d7da57964f5236f4c75e568aa9dea6460b56f7a6d5870b89453c743c6c15e213cb52be2122 languageName: node linkType: hard -"cli-spinners@npm:^2.9.2": +"cli-spinners@npm:^2.5.0, cli-spinners@npm:^2.9.2": version: 2.9.2 resolution: "cli-spinners@npm:2.9.2" checksum: 10/a0a863f442df35ed7294424f5491fa1756bd8d2e4ff0c8736531d886cec0ece4d85e8663b77a5afaf1d296e3cbbebff92e2e99f52bbea89b667cbe789b994794 @@ -14168,7 +14463,7 @@ __metadata: languageName: node linkType: hard -"css-loader@npm:6.10.0, css-loader@npm:^6.7.1": +"css-loader@npm:6.10.0": version: 6.10.0 resolution: "css-loader@npm:6.10.0" dependencies: @@ -14192,6 +14487,24 @@ __metadata: languageName: node linkType: hard +"css-loader@npm:^6.7.1": + version: 6.9.1 + resolution: "css-loader@npm:6.9.1" + dependencies: + icss-utils: "npm:^5.1.0" + postcss: "npm:^8.4.33" + postcss-modules-extract-imports: "npm:^3.0.0" + postcss-modules-local-by-default: "npm:^4.0.4" + postcss-modules-scope: "npm:^3.1.1" + postcss-modules-values: "npm:^4.0.0" + postcss-value-parser: "npm:^4.2.0" + semver: "npm:^7.5.4" + peerDependencies: + webpack: ^5.0.0 + checksum: 10/6f897406188ed7f6db03daab0602ed86df1e967b48a048ab72d0ee223e59ab9e13c5235481b12deb79e12aadf0be43bc3bdee71e1dc1e875e4bcd91c05b464af + languageName: node + linkType: hard + "css-minimizer-webpack-plugin@npm:6.0.0": version: 6.0.0 resolution: "css-minimizer-webpack-plugin@npm:6.0.0" @@ -15696,6 +16009,13 @@ __metadata: languageName: node linkType: hard +"electron-to-chromium@npm:^1.4.601": + version: 1.4.625 + resolution: "electron-to-chromium@npm:1.4.625" + checksum: 10/610a4eaabf6a064d8f6d4dfa25c55a3940f09a3b25edc8a271821d1b270bb28c4c9f19225d81bfc59deaa12c1f8f0144f3b4510631c6b6b47e0b6216737e216a + languageName: node + linkType: hard + "electron-to-chromium@npm:^1.4.648": version: 1.4.648 resolution: "electron-to-chromium@npm:1.4.648" @@ -24128,14 +24448,7 @@ __metadata: languageName: node linkType: hard -"outvariant@npm:^1.2.1, outvariant@npm:^1.4.0": - version: 1.4.0 - resolution: "outvariant@npm:1.4.0" - checksum: 10/07b9bcb9b3a2ff1b3db02af6b07d70e663082b30ddc08ff475d7c85fc623fdcc4433a4ab5b88f6902b62dbb284eef1be386aa537e14cef0519fad887ec483054 - languageName: node - linkType: hard - -"outvariant@npm:^1.4.2": +"outvariant@npm:^1.2.1, outvariant@npm:^1.4.0, outvariant@npm:^1.4.2": version: 1.4.2 resolution: "outvariant@npm:1.4.2" checksum: 10/f16ba035fb65d1cbe7d2e06693dd42183c46bc8456713d9ddb5182d067defa7d78217edab0a2d3e173d3bacd627b2bd692195c7087c225b82548fbf52c677b38 @@ -25167,7 +25480,17 @@ __metadata: languageName: node linkType: hard -"postcss-selector-parser@npm:^6.0.11, postcss-selector-parser@npm:^6.0.13, postcss-selector-parser@npm:^6.0.15, postcss-selector-parser@npm:^6.0.2, postcss-selector-parser@npm:^6.0.4": +"postcss-selector-parser@npm:^6.0.11, postcss-selector-parser@npm:^6.0.13, postcss-selector-parser@npm:^6.0.2, postcss-selector-parser@npm:^6.0.4": + version: 6.0.13 + resolution: "postcss-selector-parser@npm:6.0.13" + dependencies: + cssesc: "npm:^3.0.0" + util-deprecate: "npm:^1.0.2" + checksum: 10/e779aa1f8ca9ee45d562400aac6109a2bccc59559b6e15adec8bc2a71d395ca563a378fd68f6a61963b4ef2ca190e0c0486e6dc6c41d755f3b82dd6e480e6941 + languageName: node + linkType: hard + +"postcss-selector-parser@npm:^6.0.15": version: 6.0.15 resolution: "postcss-selector-parser@npm:6.0.15" dependencies: @@ -26841,7 +27164,7 @@ __metadata: languageName: node linkType: hard -"react-use@npm:17.5.0, react-use@npm:^17.4.2": +"react-use@npm:17.5.0": version: 17.5.0 resolution: "react-use@npm:17.5.0" dependencies: @@ -26866,6 +27189,31 @@ __metadata: languageName: node linkType: hard +"react-use@npm:^17.4.2": + version: 17.4.2 + resolution: "react-use@npm:17.4.2" + dependencies: + "@types/js-cookie": "npm:^2.2.6" + "@xobotyi/scrollbar-width": "npm:^1.9.5" + copy-to-clipboard: "npm:^3.3.1" + fast-deep-equal: "npm:^3.1.3" + fast-shallow-equal: "npm:^1.0.0" + js-cookie: "npm:^2.2.1" + nano-css: "npm:^5.6.1" + react-universal-interface: "npm:^0.6.2" + resize-observer-polyfill: "npm:^1.5.1" + screenfull: "npm:^5.1.0" + set-harmonic-interval: "npm:^1.0.1" + throttle-debounce: "npm:^3.0.1" + ts-easing: "npm:^0.2.0" + tslib: "npm:^2.1.0" + peerDependencies: + react: "*" + react-dom: "*" + checksum: 10/56d2da474d949d22eb34ff3ffccf5526986d51ed68a8f4e64f4b79bdcff3f0ea55d322c104e3fc0819b08b8765e8eb3fa47d8b506e9d61ff1fdc7bd1374c17d6 + languageName: node + linkType: hard + "react-virtual@npm:2.10.4, react-virtual@npm:^2.8.2": version: 2.10.4 resolution: "react-virtual@npm:2.10.4" @@ -28260,14 +28608,7 @@ __metadata: languageName: node linkType: hard -"signal-exit@npm:^4.0.1": - version: 4.0.2 - resolution: "signal-exit@npm:4.0.2" - checksum: 10/99d49eab7f24aeed79e44999500d5ff4b9fbb560b0e1f8d47096c54d625b995aeaec3032cce44527adf2de0c303731a8356e234a348d6801214a8a3385a1ff8e - languageName: node - linkType: hard - -"signal-exit@npm:^4.1.0": +"signal-exit@npm:^4.0.1, signal-exit@npm:^4.1.0": version: 4.1.0 resolution: "signal-exit@npm:4.1.0" checksum: 10/c9fa63bbbd7431066174a48ba2dd9986dfd930c3a8b59de9c29d7b6854ec1c12a80d15310869ea5166d413b99f041bfa3dd80a7947bcd44ea8e6eb3ffeabfa1f @@ -29695,7 +30036,7 @@ __metadata: languageName: node linkType: hard -"terser-webpack-plugin@npm:5.3.10, terser-webpack-plugin@npm:^5.1.3, terser-webpack-plugin@npm:^5.3.1, terser-webpack-plugin@npm:^5.3.10, terser-webpack-plugin@npm:^5.3.7": +"terser-webpack-plugin@npm:5.3.10, terser-webpack-plugin@npm:^5.3.10": version: 5.3.10 resolution: "terser-webpack-plugin@npm:5.3.10" dependencies: @@ -29717,6 +30058,28 @@ __metadata: languageName: node linkType: hard +"terser-webpack-plugin@npm:^5.1.3, terser-webpack-plugin@npm:^5.3.1, terser-webpack-plugin@npm:^5.3.7": + version: 5.3.9 + resolution: "terser-webpack-plugin@npm:5.3.9" + dependencies: + "@jridgewell/trace-mapping": "npm:^0.3.17" + jest-worker: "npm:^27.4.5" + schema-utils: "npm:^3.1.1" + serialize-javascript: "npm:^6.0.1" + terser: "npm:^5.16.8" + peerDependencies: + webpack: ^5.1.0 + peerDependenciesMeta: + "@swc/core": + optional: true + esbuild: + optional: true + uglify-js: + optional: true + checksum: 10/339737a407e034b7a9d4a66e31d84d81c10433e41b8eae2ca776f0e47c2048879be482a9aa08e8c27565a2a949bc68f6e07f451bf4d9aa347dd61b3d000f5353 + languageName: node + linkType: hard + "terser@npm:^5.0.0, terser@npm:^5.15.1, terser@npm:^5.26.0, terser@npm:^5.7.2": version: 5.27.0 resolution: "terser@npm:5.27.0" @@ -29731,6 +30094,20 @@ __metadata: languageName: node linkType: hard +"terser@npm:^5.16.8": + version: 5.17.2 + resolution: "terser@npm:5.17.2" + dependencies: + "@jridgewell/source-map": "npm:^0.3.2" + acorn: "npm:^8.5.0" + commander: "npm:^2.20.0" + source-map-support: "npm:~0.5.20" + bin: + terser: bin/terser + checksum: 10/6df529586a4913657547dd8bfe2b5a59704b7acbe4e49ac938a16f829a62226f98dafb19c88b7af66b245ea281ee5dbeec33a41349ac3c035855417b06ebd646 + languageName: node + linkType: hard + "test-exclude@npm:^6.0.0": version: 6.0.0 resolution: "test-exclude@npm:6.0.0" From dbbbfa282ded4c751ff1792a85cab885afe8d0f4 Mon Sep 17 00:00:00 2001 From: Leon Sorokin Date: Thu, 22 Feb 2024 06:14:53 -0600 Subject: [PATCH 0093/1406] StateTimeline: Properly type tooltip prop, remove ts-ignore (#83189) --- public/app/core/components/TimelineChart/TimelineChart.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/public/app/core/components/TimelineChart/TimelineChart.tsx b/public/app/core/components/TimelineChart/TimelineChart.tsx index 9fb1c4ed74..c22e3230da 100644 --- a/public/app/core/components/TimelineChart/TimelineChart.tsx +++ b/public/app/core/components/TimelineChart/TimelineChart.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { DataFrame, FALLBACK_COLOR, FieldType, TimeRange } from '@grafana/data'; -import { VisibilityMode, TimelineValueAlignment, TooltipDisplayMode } from '@grafana/schema'; +import { VisibilityMode, TimelineValueAlignment, TooltipDisplayMode, VizTooltipOptions } from '@grafana/schema'; import { PanelContext, PanelContextRoot, UPlotConfigBuilder, VizLayout, VizLegend, VizLegendItem } from '@grafana/ui'; import { GraphNG, GraphNGProps } from '../GraphNG/GraphNG'; @@ -18,6 +18,7 @@ export interface TimelineProps extends Omit { // When there is only one row, use the full space rowHeight: alignedFrame.fields.length > 2 ? this.props.rowHeight : 1, getValueColor: this.getValueColor, - // @ts-ignore + hoverMulti: this.props.tooltip?.mode === TooltipDisplayMode.Multi, }); }; From 74c0463cd347e8b7a239dbb36ec6a2ef4926bc8e Mon Sep 17 00:00:00 2001 From: Marie Cruz Date: Thu, 22 Feb 2024 12:27:29 +0000 Subject: [PATCH 0094/1406] Docs: update grafana fundamentals (#83075) * docs: update grafana fundamentals * fix: lint issues * docs: added more content on grafana fundamentals tutorial --- .../tutorials/grafana-fundamentals/index.md | 87 ++++++++++++++----- 1 file changed, 67 insertions(+), 20 deletions(-) diff --git a/docs/sources/tutorials/grafana-fundamentals/index.md b/docs/sources/tutorials/grafana-fundamentals/index.md index 1aa2197e4e..dc7a25d609 100644 --- a/docs/sources/tutorials/grafana-fundamentals/index.md +++ b/docs/sources/tutorials/grafana-fundamentals/index.md @@ -27,6 +27,13 @@ In this tutorial, you'll learn how to use Grafana to set up a monitoring solutio - Annotate dashboards - Set up alerts +Alternatively, you can also watch our Grafana for Beginners series where we discuss fundamental concepts to help you get started with Grafana. + +
+ +
+ {{% class "prerequisite-section" %}} ### Prerequisites @@ -128,7 +135,7 @@ To be able to visualize the metrics from Prometheus, you first need to add it as 1. In the URL box, enter **http\://prometheus:9090**. 1. Scroll to the bottom of the page and click **Save & test**. - Prometheus is now available as a data source in Grafana. +You should see the message "Successfully queried the Prometheus API." This means Prometheus is now available as a data source in Grafana. ## Explore your metrics @@ -136,7 +143,7 @@ Grafana Explore is a workflow for troubleshooting and data exploration. In this > Ad-hoc queries are queries that are made interactively, with the purpose of exploring data. An ad-hoc query is commonly followed by another, more specific query. -1. Click the menu icon and, in the sidebar, click **Explore**. The Prometheus data source that you added will already be selected. +1. Click the menu icon and, in the sidebar, click **Explore**. A dropdown menu for the list of available data sources is on the upper-left side. The Prometheus data source that you added will already be selected. If not, choose Prometheus. 1. Confirm that you're in code mode by checking the **Builder/Code** toggle at the top right corner of the query panel. 1. In the query editor, where it says _Enter a PromQL query…_, enter `tns_request_duration_seconds_count` and then press Shift + Enter. A graph appears. @@ -166,7 +173,7 @@ Grafana Explore is a workflow for troubleshooting and data exploration. In this 1. Back in Grafana, in the upper-right corner, click the _time picker_, and select **Last 5 minutes**. By zooming in on the last few minutes, it's easier to see when you receive new data. -Depending on your use case, you might want to group on other labels. Try grouping by other labels, such as `status_code`, by changing the `by(route)` part of the query. +Depending on your use case, you might want to group on other labels. Try grouping by other labels, such as `status_code`, by changing the `by(route)` part of the query to `by(status_code)`. ## Add a logging data source @@ -178,7 +185,7 @@ Grafana supports log data sources, like [Loki](/oss/loki/). Just like for metric 1. In the URL box, enter [http://loki:3100](http://loki:3100). 1. Scroll to the bottom of the page and click **Save & Test** to save your changes. -Loki is now available as a data source in Grafana. +You should see the message "Data source successfully connected." Loki is now available as a data source in Grafana. ## Explore your logs @@ -237,6 +244,10 @@ Every panel consists of a _query_ and a _visualization_. The query defines _what 1. Click the **Save dashboard** (disk) icon at the top of the dashboard to save your dashboard. 1. Enter a name in the **Dashboard name** field and then click **Save**. + You should now have a panel added to your dashboard. + + {{< figure src="/media/tutorials/grafana-fundamentals-dashboard.png" alt="A panel in a Grafana dashboard" caption="A panel in a Grafana dashboard" >}} + ## Annotate events When things go bad, it often helps if you understand the context in which the failure occurred. Time of last deploy, system changes, or database migration can offer insight into what might have caused an outage. Annotations allow you to represent such events directly on your graphs. @@ -254,11 +265,13 @@ Grafana also lets you annotate a time interval, with _region annotations_. Add a region annotation: -1. Press Ctrl (or Cmd on macOS), then click and drag across the graph to select an area. +1. Press Ctrl (or Cmd on macOS) and hold, then click and drag across the graph to select an area. 1. In **Description**, enter **Performed load tests**. 1. In **Tags**, enter **testing**. 1. Click **Save**. +### Using annotations to correlate logs with metrics + Manually annotating your dashboard is fine for those single events. For regularly occurring events, such as deploying a new release, Grafana supports querying annotations from one of your data sources. Let's create an annotation using the Loki data source we added earlier. 1. At the top of the dashboard, click the **Dashboard settings** (gear) icon. @@ -274,9 +287,13 @@ Manually annotating your dashboard is fine for those single events. For regularl 1. Click **Apply**. Grafana displays the Annotations list, with your new annotation. 1. Click on your dashboard name to return to your dashboard. 1. At the top of your dashboard, there is now a toggle to display the results of the newly created annotation query. Press it if it's not already enabled. +1. Click the **Save dashboard** icon to save the changes. +1. To test the changes, go back to the [sample application](http://localhost:8081), post a new link without a URL to generate an error in your browser that says `empty url`. The log lines returned by your query are now displayed as annotations in the graph. +{{< figure src="/media/tutorials/annotations-grafana-dashboard.png" alt="A panel in a Grafana dashboard with log queries from Loki displayed as annotations" caption="Displaying log queries from Loki as annotations" >}} + Being able to combine data from multiple data sources in one graph allows you to correlate information from both Prometheus and Loki. Annotations also work very well alongside alerts. In the next and final section, we will set up an alert for our app `grafana.news` and then we will trigger it. This will provide a quick intro to our new Alerting platform. @@ -289,7 +306,16 @@ Grafana's new alerting platform debuted with Grafana 8. A year later, with Grafa The most basic alert consists of two parts: -1. A _Contact point_ - A Contact point defines how Grafana delivers an alert. When the conditions of an _alert rule_ are met, Grafana notifies the contact points, or channels, configured for that alert. Some popular channels include email, webhooks, Slack notifications, and PagerDuty notifications. +1. A _Contact point_ - A Contact point defines how Grafana delivers an alert. When the conditions of an _alert rule_ are met, Grafana notifies the contact points, or channels, configured for that alert. + + Some popular channels include: + + - Email + - [Webhooks](#create-a-contact-point-for-grafana-managed-alerts) + - [Telegram](https://grafana.com/blog/2023/12/28/how-to-integrate-grafana-alerting-and-telegram/) + - Slack + - PagerDuty + 1. An _Alert rule_ - An Alert rule defines one or more _conditions_ that Grafana regularly evaluates. When these evaluations meet the rule's criteria, the alert is triggered. To begin, let's set up a webhook contact point. Once we have a usable endpoint, we'll write an alert rule and trigger a notification. @@ -299,12 +325,10 @@ To begin, let's set up a webhook contact point. Once we have a usable endpoint, In this step, we'll set up a new contact point. This contact point will use the _webhooks_ channel. In order to make this work, we also need an endpoint for our webhook channel to receive the alert. We will use [requestbin.com](https://requestbin.com) to quickly set up that test endpoint. This way we can make sure that our alert is actually sending a notification somewhere. 1. Browse to [requestbin.com](https://requestbin.com). -1. Under the **Create Request Bin** button -1. From RequestBin, Copy the endpoint URL. +1. Under the **Create Request Bin** button, click the link to create a **public bin** instead. +1. From Request Bin, copy the endpoint URL. -Your request bin is now waiting for the first request. - - +Your Request Bin is now waiting for the first request. Next, let's configure a Contact Point in Grafana's Alerting UI to send notifications to our Request Bin. @@ -314,8 +338,8 @@ Next, let's configure a Contact Point in Grafana's Alerting UI to send notificat 1. In **Integration**, choose **Webhook**. 1. In **URL**, paste the endpoint to your request bin. -1. Click **Test** to send a test alert to your request bin. -1. Navigate back to the request bin you created earlier. On the left side, there's now a `POST /` entry. Click it to see what information Grafana sent. +1. Click **Test**, and then click **Send test notification** to send a test alert to your request bin. +1. Navigate back to the Request Bin you created earlier. On the left side, there's now a `POST /` entry. Click it to see what information Grafana sent. 1. Return to Grafana and click **Save contact point**. We have now created a dummy webhook endpoint and created a new Alerting Contact Point in Grafana. Now we can create an alert rule and link it to this new channel. @@ -329,17 +353,24 @@ Now that Grafana knows how to notify us, it's time to set up an alert rule: 1. For **Section 1**, name the rule `fundamentals-test`. 1. For **Section 2**, Find the **query A** box. Choose your Prometheus datasource. Note that the rule type should automatically switch to Grafana-managed alert. 1. Switch to code mode by checking the Builder/Code toggle. -1. Enter the same query that we used in our earlier panel `sum(rate(tns_request_duration_seconds_count[5m])) by(route)` +1. Enter the same Prometheus query that we used in our earlier panel: + + ``` + sum(rate(tns_request_duration_seconds_count[5m])) by(route) + ``` + 1. Press **Preview**. You should see some data returned. -1. Keep expressions “B” and "C" as they are. These expressions (Reduce and Threshold, respectively) come by default when creating a new rule. Expression "B", selects the last value of our query “A”, while the Threshold expression "C" will check if the last value from expression "B" is above a specific value. In addition, the Threshold expression is the alert condition by default. Enter `0.2` as threshold value [You can read more about queries and conditions here](https://grafana.com/docs/grafana/latest/alerting/fundamentals/alert-rules/queries-conditions/#expression-queries). -1. In **Section 3**, in Folder, create a new folder, by typing a name for the folder. This folder will contain our alerts. For example: `fundamentals`. Then, click + add new or hit enter twice. +1. Keep expressions “B” and "C" as they are. These expressions (Reduce and Threshold, respectively) come by default when creating a new rule. Expression "B", selects the last value of our query “A”, while the Threshold expression "C" will check if the last value from expression "B" is above a specific value. In addition, the Threshold expression is the alert condition by default. Enter `0.2` as threshold value. [You can read more about queries and conditions here](https://grafana.com/docs/grafana/latest/alerting/fundamentals/alert-rules/queries-conditions/#expression-queries). +1. In **Section 3**, in Folder, create a new folder, by clicking `New folder` and typing a name for the folder. This folder will contain our alerts. For example: `fundamentals`. Then, click `create`. 1. In the Evaluation group, repeat the above step to create a new one. We will name it `fundamentals` too. -1. Choose an Evaluation interval (how often the alert will be evaluated). For example, every `30s` (30 seconds). -1. Set the pending period . This is the time that a condition has to be met until the alert enters in Firing state and a notification is sent. Enter `0s`. For the purposes of this tutorial, the evaluation interval is intentionally short. This makes it easier to test. This setting makes Grafana wait until an alert has fired for a given time before Grafana sends the notification. +1. Choose an Evaluation interval (how often the alert will be evaluated). For example, every `10s` (10 seconds). +1. Set the pending period. This is the time that a condition has to be met until the alert enters in Firing state and a notification is sent. Enter `0s`. For the purposes of this tutorial, the evaluation interval is intentionally short. This makes it easier to test. This setting makes Grafana wait until an alert has fired for a given time before Grafana sends the notification. 1. In **Section 4**, you can optionally 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 rule and exit** at the top of the page. 1. In Grafana's sidebar, navigate to **Notification policies**. -1. Under **Default policy**, select **...** › **Edit** and change the **Default contact point** to **RequestBin**. +1. Under **Default policy**, select **...** › **Edit** and change the **Default contact point** from **grafana-default-email** to **RequestBin**. +1. Expand the **Timing options** dropdown and under **Group wait** and **Group interval** update the value to `30s` for testing purposes. Group wait is the time Grafana waits before sending the first notification for a new group of alerts. In contrast, group interval is the time Grafana waits before sending notifications about changes to the group. +1. Click **Update default policy**. As a system grows, admins can use the **Notification policies** setting to organize and match alert rules to specific contact points. @@ -349,10 +380,26 @@ Now that Grafana knows how to notify us, it's time to set up an alert rule: We have now configured an alert rule and a contact point. Now let's see if we can trigger a Grafana Managed Alert by generating some traffic on our sample application. 1. Browse to [localhost:8081](http://localhost:8081). -1. Repeatedly click the vote button or refresh the page to generate a traffic spike. +1. Add a new title and URL, repeatedly click the vote button, or refresh the page to generate a traffic spike. Once the query `sum(rate(tns_request_duration_seconds_count[5m])) by(route)` returns a value greater than `0.2` Grafana will trigger our alert. Browse to the Request Bin we created earlier and find the sent Grafana alert notification with details and metadata. +### Display Grafana Alerts to your dashboard + +In most cases, it's also valuable to display Grafana Alerts as annotations to your dashboard. Let's see how we can configure this. + +1. In Grafana's sidebar, hover over the **Alerting** (bell) icon and then click **Alert rules**. +1. Expand the `fundamentals > fundamentals` folder to view our created alert rule. +1. Click the **Edit** icon and scroll down to **Section 4**. +1. Click the **Link dashboard and panel** button and select the dashboard and panel to which you want the alert to be added as an annotation. +1. Click **Confirm** and **Save rule and exit** to save all the changes. +1. In Grafana's sidebar, navigate to the dashboard by clicking **Dashboards** and selecting the dashboard you created. +1. To test the changes, follow the steps listed to [trigger a Grafana Managed Alert](#trigger-a-grafana-managed-alert). + + You should now see a red, broken heart icon beside the panel name, signifying that the alert has been triggered. An annotation for the alert, represented as a vertical red line, is also displayed. + + {{< figure src="/media/tutorials/grafana-alert-on-dashboard.png" alt="A panel in a Grafana dashboard with alerting and annotations configured" caption="Displaying Grafana Alerts on a dashboard" >}} + ## Summary In this tutorial you learned about fundamental features of Grafana. To do so, we ran several Docker containers on your local machine. When you are ready to clean up this local tutorial environment, run the following command: From 3ba33fe27834cd62f53799945c5d65fe3fdd54a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Farkas?= Date: Thu, 22 Feb 2024 14:22:14 +0100 Subject: [PATCH 0095/1406] mysql: do not use unexported grafana-core config (#83062) * mysql: do not use unexported grafana-core config * updated test --- .../plugins_integration_test.go | 2 +- pkg/tsdb/mysql/macros.go | 5 ++- pkg/tsdb/mysql/macros_test.go | 3 +- pkg/tsdb/mysql/mysql.go | 33 +++++++++++-------- pkg/tsdb/mysql/mysql_test.go | 5 ++- 5 files changed, 26 insertions(+), 22 deletions(-) diff --git a/pkg/services/pluginsintegration/plugins_integration_test.go b/pkg/services/pluginsintegration/plugins_integration_test.go index 778d0c1301..d7d82018ca 100644 --- a/pkg/services/pluginsintegration/plugins_integration_test.go +++ b/pkg/services/pluginsintegration/plugins_integration_test.go @@ -86,7 +86,7 @@ func TestIntegrationPluginManager(t *testing.T) { tmpo := tempo.ProvideService(hcp) td := testdatasource.ProvideService() pg := postgres.ProvideService(cfg) - my := mysql.ProvideService(cfg, hcp) + my := mysql.ProvideService() ms := mssql.ProvideService(cfg) sv2 := searchV2.ProvideService(cfg, db.InitTestDB(t), nil, nil, tracer, features, nil, nil, nil) graf := grafanads.ProvideService(sv2, nil) diff --git a/pkg/tsdb/mysql/macros.go b/pkg/tsdb/mysql/macros.go index 4cff452e41..05e93c0aee 100644 --- a/pkg/tsdb/mysql/macros.go +++ b/pkg/tsdb/mysql/macros.go @@ -8,7 +8,6 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/gtime" "github.com/grafana/grafana-plugin-sdk-go/backend/log" - "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/tsdb/sqleng" ) @@ -23,11 +22,11 @@ type mySQLMacroEngine struct { userError string } -func newMysqlMacroEngine(logger log.Logger, cfg *setting.Cfg) sqleng.SQLMacroEngine { +func newMysqlMacroEngine(logger log.Logger, userFacingDefaultError string) sqleng.SQLMacroEngine { return &mySQLMacroEngine{ SQLMacroEngineBase: sqleng.NewSQLMacroEngineBase(), logger: logger, - userError: cfg.UserFacingDefaultError, + userError: userFacingDefaultError, } } diff --git a/pkg/tsdb/mysql/macros_test.go b/pkg/tsdb/mysql/macros_test.go index d8babbfbc3..595e06c7dc 100644 --- a/pkg/tsdb/mysql/macros_test.go +++ b/pkg/tsdb/mysql/macros_test.go @@ -7,7 +7,6 @@ import ( "time" "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/setting" "github.com/stretchr/testify/require" ) @@ -194,7 +193,7 @@ func TestMacroEngine(t *testing.T) { } func TestMacroEngineConcurrency(t *testing.T) { - engine := newMysqlMacroEngine(backend.NewLoggerWith("logger", "test"), setting.NewCfg()) + engine := newMysqlMacroEngine(backend.NewLoggerWith("logger", "test"), "error") query1 := backend.DataQuery{ JSON: []byte{}, } diff --git a/pkg/tsdb/mysql/mysql.go b/pkg/tsdb/mysql/mysql.go index 91ed62cd2b..2e908afc93 100644 --- a/pkg/tsdb/mysql/mysql.go +++ b/pkg/tsdb/mysql/mysql.go @@ -22,8 +22,6 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/data/sqlutil" "github.com/grafana/grafana-plugin-sdk-go/backend/log" - "github.com/grafana/grafana/pkg/infra/httpclient" - "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/tsdb/sqleng" ) @@ -34,7 +32,6 @@ const ( ) type Service struct { - Cfg *setting.Cfg im instancemgmt.InstanceManager logger log.Logger } @@ -43,25 +40,30 @@ func characterEscape(s string, escapeChar string) string { return strings.ReplaceAll(s, escapeChar, url.QueryEscape(escapeChar)) } -func ProvideService(cfg *setting.Cfg, httpClientProvider httpclient.Provider) *Service { +func ProvideService() *Service { logger := backend.NewLoggerWith("logger", "tsdb.mysql") return &Service{ - im: datasource.NewInstanceManager(newInstanceSettings(cfg, logger)), + im: datasource.NewInstanceManager(newInstanceSettings(logger)), logger: logger, } } -func newInstanceSettings(cfg *setting.Cfg, logger log.Logger) datasource.InstanceFactoryFunc { +func newInstanceSettings(logger log.Logger) datasource.InstanceFactoryFunc { return func(ctx context.Context, settings backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { + cfg := backend.GrafanaConfigFromContext(ctx) + sqlCfg, err := cfg.SQL() + if err != nil { + return nil, err + } jsonData := sqleng.JsonData{ - MaxOpenConns: cfg.SqlDatasourceMaxOpenConnsDefault, - MaxIdleConns: cfg.SqlDatasourceMaxIdleConnsDefault, - ConnMaxLifetime: cfg.SqlDatasourceMaxConnLifetimeDefault, + MaxOpenConns: sqlCfg.DefaultMaxOpenConns, + MaxIdleConns: sqlCfg.DefaultMaxIdleConns, + ConnMaxLifetime: sqlCfg.DefaultMaxConnLifetimeSeconds, SecureDSProxy: false, AllowCleartextPasswords: false, } - err := json.Unmarshal(settings.JSONData, &jsonData) + err = json.Unmarshal(settings.JSONData, &jsonData) if err != nil { return nil, fmt.Errorf("error reading settings: %w", err) } @@ -144,11 +146,16 @@ func newInstanceSettings(cfg *setting.Cfg, logger log.Logger) datasource.Instanc DSInfo: dsInfo, TimeColumnNames: []string{"time", "time_sec"}, MetricColumnTypes: []string{"CHAR", "VARCHAR", "TINYTEXT", "TEXT", "MEDIUMTEXT", "LONGTEXT"}, - RowLimit: cfg.DataProxyRowLimit, + RowLimit: sqlCfg.RowLimit, + } + + userFacingDefaultError, err := cfg.UserFacingDefaultError() + if err != nil { + return nil, err } rowTransformer := mysqlQueryResultTransformer{ - userError: cfg.UserFacingDefaultError, + userError: userFacingDefaultError, } db, err := sql.Open("mysql", cnnstr) @@ -160,7 +167,7 @@ func newInstanceSettings(cfg *setting.Cfg, logger log.Logger) datasource.Instanc db.SetMaxIdleConns(config.DSInfo.JsonData.MaxIdleConns) db.SetConnMaxLifetime(time.Duration(config.DSInfo.JsonData.ConnMaxLifetime) * time.Second) - return sqleng.NewQueryDataHandler(cfg.UserFacingDefaultError, db, config, &rowTransformer, newMysqlMacroEngine(logger, cfg), logger) + return sqleng.NewQueryDataHandler(userFacingDefaultError, db, config, &rowTransformer, newMysqlMacroEngine(logger, userFacingDefaultError), logger) } } diff --git a/pkg/tsdb/mysql/mysql_test.go b/pkg/tsdb/mysql/mysql_test.go index c41766db6c..c01ec8bae6 100644 --- a/pkg/tsdb/mysql/mysql_test.go +++ b/pkg/tsdb/mysql/mysql_test.go @@ -13,7 +13,6 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/stretchr/testify/require" - "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/tsdb/sqleng" ) @@ -67,7 +66,7 @@ func TestIntegrationMySQL(t *testing.T) { db := InitMySQLTestDB(t, config.DSInfo.JsonData) - exe, err := sqleng.NewQueryDataHandler("", db, config, &rowTransformer, newMysqlMacroEngine(logger, setting.NewCfg()), logger) + exe, err := sqleng.NewQueryDataHandler("", db, config, &rowTransformer, newMysqlMacroEngine(logger, ""), logger) require.NoError(t, err) @@ -1179,7 +1178,7 @@ func TestIntegrationMySQL(t *testing.T) { queryResultTransformer := mysqlQueryResultTransformer{} - handler, err := sqleng.NewQueryDataHandler("", db, config, &queryResultTransformer, newMysqlMacroEngine(logger, setting.NewCfg()), logger) + handler, err := sqleng.NewQueryDataHandler("", db, config, &queryResultTransformer, newMysqlMacroEngine(logger, ""), logger) require.NoError(t, err) t.Run("When doing a table query that returns 2 rows should limit the result to 1 row", func(t *testing.T) { From 2a1873f03815f5ea26da31be7e5157a2c3f171a4 Mon Sep 17 00:00:00 2001 From: Sonia Aguilar <33540275+soniaAguilarPeiron@users.noreply.github.com> Date: Thu, 22 Feb 2024 14:29:57 +0100 Subject: [PATCH 0096/1406] Alerting: Fix saving evaluation group. (#83188) fix saving evaluation group --- .../rule-editor/GrafanaEvaluationBehavior.tsx | 2 +- .../app/features/alerting/unified/state/actions.ts | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/public/app/features/alerting/unified/components/rule-editor/GrafanaEvaluationBehavior.tsx b/public/app/features/alerting/unified/components/rule-editor/GrafanaEvaluationBehavior.tsx index d0f6672c41..22953dbb92 100644 --- a/public/app/features/alerting/unified/components/rule-editor/GrafanaEvaluationBehavior.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/GrafanaEvaluationBehavior.tsx @@ -18,7 +18,7 @@ import { } from '@grafana/ui'; import { CombinedRuleGroup, CombinedRuleNamespace } from '../../../../../types/unified-alerting'; -import { logInfo, LogMessages } from '../../Analytics'; +import { LogMessages, logInfo } from '../../Analytics'; import { useCombinedRuleNamespaces } from '../../hooks/useCombinedRuleNamespaces'; import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector'; import { RuleFormValues } from '../../types/rule-form'; diff --git a/public/app/features/alerting/unified/state/actions.ts b/public/app/features/alerting/unified/state/actions.ts index a609c09e06..0035acdcec 100644 --- a/public/app/features/alerting/unified/state/actions.ts +++ b/public/app/features/alerting/unified/state/actions.ts @@ -65,6 +65,7 @@ import { FetchRulerRulesFilter, setRulerRuleGroup, } from '../api/ruler'; +import { encodeGrafanaNamespace } from '../components/expressions/util'; import { RuleFormType, RuleFormValues } from '../types/rule-form'; import { addDefaultsToAlertmanagerConfig, removeMuteTimingFromRoute } from '../utils/alertmanager'; import { @@ -803,11 +804,14 @@ export const updateLotexNamespaceAndGroupAction: AsyncThunk< } const newNamespaceAlreadyExists = Boolean(rulesResult[newNamespaceName]); - if (newNamespaceName !== namespaceName && newNamespaceAlreadyExists) { + const isGrafanaManagedGroup = rulesSourceName === GRAFANA_RULES_SOURCE_NAME; + const originalNamespace = isGrafanaManagedGroup ? encodeGrafanaNamespace(namespaceName) : namespaceName; + + if (newNamespaceName !== originalNamespace && newNamespaceAlreadyExists) { throw new Error(`Namespace "${newNamespaceName}" already exists.`); } if ( - newNamespaceName === namespaceName && + newNamespaceName === originalNamespace && groupName === newGroupName && groupInterval === existingGroup.interval ) { @@ -829,8 +833,8 @@ export const updateLotexNamespaceAndGroupAction: AsyncThunk< } } // if renaming namespace - make new copies of all groups, then delete old namespace - - if (newNamespaceName !== namespaceName) { + // this is only possible for cloud rules + if (newNamespaceName !== originalNamespace) { for (const group of rulesResult[namespaceName]) { await setRulerRuleGroup( rulerConfig, From ee5dc14e171eec6c56248d32d75890659bc0e195 Mon Sep 17 00:00:00 2001 From: Joey <90795735+joey-grafana@users.noreply.github.com> Date: Thu, 22 Feb 2024 13:30:41 +0000 Subject: [PATCH 0097/1406] Tempo: Remove trace to metrics feature toggle (#82884) * Remove trace to metrics feature toggle * Fix after merge --- docs/sources/datasources/jaeger/_index.md | 5 ----- .../datasources/tempo/configure-tempo-data-source.md | 5 ----- docs/sources/datasources/zipkin/_index.md | 5 ----- .../configure-grafana/feature-toggles/index.md | 1 - packages/grafana-data/src/types/featureToggles.gen.ts | 1 - pkg/services/featuremgmt/registry.go | 7 ------- pkg/services/featuremgmt/toggles_gen.csv | 1 - pkg/services/featuremgmt/toggles_gen.go | 4 ---- pkg/services/featuremgmt/toggles_gen.json | 5 +++-- .../datasource/jaeger/configuration/ConfigEditor.tsx | 9 ++------- .../datasource/tempo/configuration/ConfigEditor.tsx | 8 ++------ public/app/plugins/datasource/zipkin/ConfigEditor.tsx | 9 ++------- 12 files changed, 9 insertions(+), 51 deletions(-) diff --git a/docs/sources/datasources/jaeger/_index.md b/docs/sources/datasources/jaeger/_index.md index 4f0e9ee0fa..36dc7d0131 100644 --- a/docs/sources/datasources/jaeger/_index.md +++ b/docs/sources/datasources/jaeger/_index.md @@ -123,11 +123,6 @@ The following table describes the ways in which you can configure your trace to ### Trace to metrics -{{% admonition type="note" %}} -This feature is behind the `traceToMetrics` [feature toggle][configure-grafana-feature-toggles]. -If you use Grafana Cloud, open a [support ticket in the Cloud Portal](/profile/org#support) to access this feature. -{{% /admonition %}} - The **Trace to metrics** setting configures the [trace to metrics feature](/blog/2022/08/18/new-in-grafana-9.1-trace-to-metrics-allows-users-to-navigate-from-a-trace-span-to-a-selected-data-source/) available when integrating Grafana with Jaeger. To configure trace to metrics: diff --git a/docs/sources/datasources/tempo/configure-tempo-data-source.md b/docs/sources/datasources/tempo/configure-tempo-data-source.md index 6107947d13..66cbeaa713 100644 --- a/docs/sources/datasources/tempo/configure-tempo-data-source.md +++ b/docs/sources/datasources/tempo/configure-tempo-data-source.md @@ -91,11 +91,6 @@ The following table describes the ways in which you can configure your trace to ## Trace to metrics -{{% admonition type="note" %}} -This feature is behind the `traceToMetrics` [feature toggle][configure-grafana-feature-toggles]. -If you use Grafana Cloud, open a [support ticket in the Cloud Portal](/profile/org#support) to access this feature. -{{% /admonition %}} - The **Trace to metrics** setting configures the [trace to metrics feature](/blog/2022/08/18/new-in-grafana-9.1-trace-to-metrics-allows-users-to-navigate-from-a-trace-span-to-a-selected-data-source/) available when integrating Grafana with Tempo. {{< youtube id="TkapvLeMMpc" >}} diff --git a/docs/sources/datasources/zipkin/_index.md b/docs/sources/datasources/zipkin/_index.md index c1d96a8625..b12037fbe2 100644 --- a/docs/sources/datasources/zipkin/_index.md +++ b/docs/sources/datasources/zipkin/_index.md @@ -121,11 +121,6 @@ The following table describes the ways in which you can configure your trace to ### Trace to metrics -{{% admonition type="note" %}} -This feature is behind the `traceToMetrics` [feature toggle][configure-grafana-feature-toggles]. -If you use Grafana Cloud, open a [support ticket in the Cloud Portal](/profile/org/#support) to access this feature. -{{% /admonition %}} - The **Trace to metrics** setting configures the [trace to metrics feature](/blog/2022/08/18/new-in-grafana-9.1-trace-to-metrics-allows-users-to-navigate-from-a-trace-span-to-a-selected-data-source/) available when integrating Grafana with Zipkin. To configure trace to metrics: 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 6231b09a31..1b8c404a11 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -112,7 +112,6 @@ Experimental features might be changed or removed without prior notice. | `lokiExperimentalStreaming` | Support new streaming approach for loki (prototype, needs special loki build) | | `storage` | Configurable storage for dashboards, datasources, and resources | | `datasourceQueryMultiStatus` | Introduce HTTP 207 Multi Status for api/ds/query | -| `traceToMetrics` | Enable trace to metrics links | | `canvasPanelNesting` | Allow elements nesting | | `scenes` | Experimental framework to build interactive dashboards | | `disableSecretsCompatibility` | Disable duplicated secret storage in legacy tables | diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 74d38c26b5..5f00693294 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -31,7 +31,6 @@ export interface FeatureToggles { correlations?: boolean; exploreContentOutline?: boolean; datasourceQueryMultiStatus?: boolean; - traceToMetrics?: boolean; autoMigrateOldPanels?: boolean; autoMigrateGraphPanel?: boolean; autoMigrateTablePanel?: boolean; diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index cf57c68952..4845193014 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -108,13 +108,6 @@ var ( Stage: FeatureStageExperimental, Owner: grafanaPluginsPlatformSquad, }, - { - Name: "traceToMetrics", - Description: "Enable trace to metrics links", - Stage: FeatureStageExperimental, - FrontendOnly: true, - Owner: grafanaObservabilityTracesAndProfilingSquad, - }, { Name: "autoMigrateOldPanels", Description: "Migrate old angular panels to supported versions (graph, table-old, worldmap, etc)", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 3098345d97..a9ebfc7ea8 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -12,7 +12,6 @@ storage,experimental,@grafana/grafana-app-platform-squad,false,false,false correlations,GA,@grafana/explore-squad,false,false,false exploreContentOutline,GA,@grafana/explore-squad,false,false,true datasourceQueryMultiStatus,experimental,@grafana/plugins-platform-backend,false,false,false -traceToMetrics,experimental,@grafana/observability-traces-and-profiling,false,false,true autoMigrateOldPanels,preview,@grafana/dataviz-squad,false,false,true autoMigrateGraphPanel,preview,@grafana/dataviz-squad,false,false,true autoMigrateTablePanel,preview,@grafana/dataviz-squad,false,false,true diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 10a8223f5c..746eec1bd6 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -59,10 +59,6 @@ const ( // Introduce HTTP 207 Multi Status for api/ds/query FlagDatasourceQueryMultiStatus = "datasourceQueryMultiStatus" - // FlagTraceToMetrics - // Enable trace to metrics links - FlagTraceToMetrics = "traceToMetrics" - // FlagAutoMigrateOldPanels // Migrate old angular panels to supported versions (graph, table-old, worldmap, etc) FlagAutoMigrateOldPanels = "autoMigrateOldPanels" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index a04e47e1fe..b877d2e9af 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -1394,7 +1394,8 @@ "metadata": { "name": "traceToMetrics", "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "creationTimestamp": "2024-02-16T18:36:28Z", + "deletionTimestamp": "2024-02-16T10:09:14Z" }, "spec": { "description": "Enable trace to metrics links", @@ -2125,4 +2126,4 @@ } } ] -} \ No newline at end of file +} diff --git a/public/app/plugins/datasource/jaeger/configuration/ConfigEditor.tsx b/public/app/plugins/datasource/jaeger/configuration/ConfigEditor.tsx index f97f2ddd6f..0eb57f2a38 100644 --- a/public/app/plugins/datasource/jaeger/configuration/ConfigEditor.tsx +++ b/public/app/plugins/datasource/jaeger/configuration/ConfigEditor.tsx @@ -33,15 +33,10 @@ export const ConfigEditor = ({ options, onOptionsChange }: Props) => { /> - - {config.featureToggles.traceToMetrics ? ( - <> - - - - ) : null} + + { + + - {config.featureToggles.traceToMetrics ? ( - <> - - - - ) : null} diff --git a/public/app/plugins/datasource/zipkin/ConfigEditor.tsx b/public/app/plugins/datasource/zipkin/ConfigEditor.tsx index 8916303e9a..3bd0a07632 100644 --- a/public/app/plugins/datasource/zipkin/ConfigEditor.tsx +++ b/public/app/plugins/datasource/zipkin/ConfigEditor.tsx @@ -31,15 +31,10 @@ export const ConfigEditor = ({ options, onOptionsChange }: Props) => { /> - - {config.featureToggles.traceToMetrics ? ( - <> - - - - ) : null} + + Date: Thu, 22 Feb 2024 15:30:20 +0100 Subject: [PATCH 0098/1406] Chore: Align usage of tsconfig in yarn workspaces to 1.3.0-rc1 (#83160) chore(tsconfig): align all usage in workspaces to 1.3.0-rc1 --- packages/grafana-data/package.json | 2 +- packages/grafana-e2e-selectors/package.json | 2 +- packages/grafana-e2e/package.json | 2 +- packages/grafana-flamegraph/package.json | 2 +- packages/grafana-plugin-configs/package.json | 2 +- packages/grafana-runtime/package.json | 2 +- packages/grafana-schema/package.json | 2 +- packages/grafana-ui/package.json | 2 +- .../internal/input-datasource/package.json | 2 +- yarn.lock | 18 +++++++++--------- 10 files changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index bba7872c61..7db50de039 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -62,7 +62,7 @@ "xss": "^1.0.14" }, "devDependencies": { - "@grafana/tsconfig": "^1.2.0-rc1", + "@grafana/tsconfig": "^1.3.0-rc1", "@rollup/plugin-commonjs": "25.0.7", "@rollup/plugin-json": "6.1.0", "@rollup/plugin-node-resolve": "15.2.3", diff --git a/packages/grafana-e2e-selectors/package.json b/packages/grafana-e2e-selectors/package.json index 4df085c1fb..d2b355d609 100644 --- a/packages/grafana-e2e-selectors/package.json +++ b/packages/grafana-e2e-selectors/package.json @@ -50,7 +50,7 @@ "rollup-plugin-node-externals": "^5.0.0" }, "dependencies": { - "@grafana/tsconfig": "^1.2.0-rc1", + "@grafana/tsconfig": "^1.3.0-rc1", "tslib": "2.6.2", "typescript": "5.3.3" } diff --git a/packages/grafana-e2e/package.json b/packages/grafana-e2e/package.json index f583d73009..09bb0bb405 100644 --- a/packages/grafana-e2e/package.json +++ b/packages/grafana-e2e/package.json @@ -65,7 +65,7 @@ "@cypress/webpack-preprocessor": "5.17.1", "@grafana/e2e-selectors": "11.0.0-pre", "@grafana/schema": "11.0.0-pre", - "@grafana/tsconfig": "^1.2.0-rc1", + "@grafana/tsconfig": "^1.3.0-rc1", "@mochajs/json-file-reporter": "^1.2.0", "babel-loader": "9.1.3", "blink-diff": "1.0.13", diff --git a/packages/grafana-flamegraph/package.json b/packages/grafana-flamegraph/package.json index a41d2f859d..c5c692db8f 100644 --- a/packages/grafana-flamegraph/package.json +++ b/packages/grafana-flamegraph/package.json @@ -59,7 +59,7 @@ "@babel/core": "7.23.9", "@babel/preset-env": "7.23.9", "@babel/preset-react": "7.23.3", - "@grafana/tsconfig": "^1.2.0-rc1", + "@grafana/tsconfig": "^1.3.0-rc1", "@rollup/plugin-node-resolve": "15.2.3", "@testing-library/jest-dom": "^6.1.2", "@testing-library/react": "14.2.1", diff --git a/packages/grafana-plugin-configs/package.json b/packages/grafana-plugin-configs/package.json index d2176eb0fe..25745892f5 100644 --- a/packages/grafana-plugin-configs/package.json +++ b/packages/grafana-plugin-configs/package.json @@ -7,7 +7,7 @@ "tslib": "2.6.2" }, "devDependencies": { - "@grafana/tsconfig": "^1.2.0-rc1", + "@grafana/tsconfig": "^1.3.0-rc1", "copy-webpack-plugin": "12.0.2", "eslint-webpack-plugin": "4.0.1", "fork-ts-checker-webpack-plugin": "9.0.2", diff --git a/packages/grafana-runtime/package.json b/packages/grafana-runtime/package.json index a6f747ea37..84d6f4d680 100644 --- a/packages/grafana-runtime/package.json +++ b/packages/grafana-runtime/package.json @@ -50,7 +50,7 @@ "tslib": "2.6.2" }, "devDependencies": { - "@grafana/tsconfig": "^1.2.0-rc1", + "@grafana/tsconfig": "^1.3.0-rc1", "@rollup/plugin-commonjs": "25.0.7", "@rollup/plugin-node-resolve": "15.2.3", "@testing-library/dom": "9.3.4", diff --git a/packages/grafana-schema/package.json b/packages/grafana-schema/package.json index 520b370aca..198f376c72 100644 --- a/packages/grafana-schema/package.json +++ b/packages/grafana-schema/package.json @@ -36,7 +36,7 @@ "postpack": "mv package.json.bak package.json" }, "devDependencies": { - "@grafana/tsconfig": "^1.2.0-rc1", + "@grafana/tsconfig": "^1.3.0-rc1", "@rollup/plugin-commonjs": "25.0.7", "@rollup/plugin-json": "6.1.0", "@rollup/plugin-node-resolve": "15.2.3", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 4fb8cddce7..352785ae36 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -111,7 +111,7 @@ }, "devDependencies": { "@babel/core": "7.23.9", - "@grafana/tsconfig": "^1.2.0-rc1", + "@grafana/tsconfig": "^1.3.0-rc1", "@rollup/plugin-node-resolve": "15.2.3", "@storybook/addon-a11y": "7.4.5", "@storybook/addon-actions": "7.4.5", diff --git a/plugins-bundled/internal/input-datasource/package.json b/plugins-bundled/internal/input-datasource/package.json index e1ddd77476..d893ab319d 100644 --- a/plugins-bundled/internal/input-datasource/package.json +++ b/plugins-bundled/internal/input-datasource/package.json @@ -14,7 +14,7 @@ }, "author": "Grafana Labs", "devDependencies": { - "@grafana/tsconfig": "^1.2.0-rc1", + "@grafana/tsconfig": "^1.3.0-rc1", "@types/jest": "26.0.15", "@types/react": "18.0.28", "copy-webpack-plugin": "11.0.0", diff --git a/yarn.lock b/yarn.lock index b7d742e7a2..9cf178efdb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3365,7 +3365,7 @@ __metadata: resolution: "@grafana-plugins/input-datasource@workspace:plugins-bundled/internal/input-datasource" dependencies: "@grafana/data": "npm:11.0.0-pre" - "@grafana/tsconfig": "npm:^1.2.0-rc1" + "@grafana/tsconfig": "npm:^1.3.0-rc1" "@grafana/ui": "npm:11.0.0-pre" "@types/jest": "npm:26.0.15" "@types/react": "npm:18.0.28" @@ -3568,7 +3568,7 @@ __metadata: dependencies: "@braintree/sanitize-url": "npm:7.0.0" "@grafana/schema": "npm:11.0.0-pre" - "@grafana/tsconfig": "npm:^1.2.0-rc1" + "@grafana/tsconfig": "npm:^1.3.0-rc1" "@rollup/plugin-commonjs": "npm:25.0.7" "@rollup/plugin-json": "npm:6.1.0" "@rollup/plugin-node-resolve": "npm:15.2.3" @@ -3642,7 +3642,7 @@ __metadata: version: 0.0.0-use.local resolution: "@grafana/e2e-selectors@workspace:packages/grafana-e2e-selectors" dependencies: - "@grafana/tsconfig": "npm:^1.2.0-rc1" + "@grafana/tsconfig": "npm:^1.3.0-rc1" "@rollup/plugin-commonjs": "npm:25.0.7" "@rollup/plugin-node-resolve": "npm:15.2.3" "@types/node": "npm:20.11.19" @@ -3666,7 +3666,7 @@ __metadata: "@cypress/webpack-preprocessor": "npm:5.17.1" "@grafana/e2e-selectors": "npm:11.0.0-pre" "@grafana/schema": "npm:11.0.0-pre" - "@grafana/tsconfig": "npm:^1.2.0-rc1" + "@grafana/tsconfig": "npm:^1.3.0-rc1" "@mochajs/json-file-reporter": "npm:^1.2.0" "@rollup/plugin-node-resolve": "npm:15.2.3" "@types/chrome-remote-interface": "npm:0.31.10" @@ -3831,7 +3831,7 @@ __metadata: "@babel/preset-react": "npm:7.23.3" "@emotion/css": "npm:11.11.2" "@grafana/data": "npm:11.0.0-pre" - "@grafana/tsconfig": "npm:^1.2.0-rc1" + "@grafana/tsconfig": "npm:^1.3.0-rc1" "@grafana/ui": "npm:11.0.0-pre" "@leeoniya/ufuzzy": "npm:1.0.14" "@rollup/plugin-node-resolve": "npm:15.2.3" @@ -3943,7 +3943,7 @@ __metadata: version: 0.0.0-use.local resolution: "@grafana/plugin-configs@workspace:packages/grafana-plugin-configs" dependencies: - "@grafana/tsconfig": "npm:^1.2.0-rc1" + "@grafana/tsconfig": "npm:^1.3.0-rc1" copy-webpack-plugin: "npm:12.0.2" eslint-webpack-plugin: "npm:4.0.1" fork-ts-checker-webpack-plugin: "npm:9.0.2" @@ -4077,7 +4077,7 @@ __metadata: "@grafana/e2e-selectors": "npm:11.0.0-pre" "@grafana/faro-web-sdk": "npm:^1.3.6" "@grafana/schema": "npm:11.0.0-pre" - "@grafana/tsconfig": "npm:^1.2.0-rc1" + "@grafana/tsconfig": "npm:^1.3.0-rc1" "@grafana/ui": "npm:11.0.0-pre" "@rollup/plugin-commonjs": "npm:25.0.7" "@rollup/plugin-node-resolve": "npm:15.2.3" @@ -4138,7 +4138,7 @@ __metadata: version: 0.0.0-use.local resolution: "@grafana/schema@workspace:packages/grafana-schema" dependencies: - "@grafana/tsconfig": "npm:^1.2.0-rc1" + "@grafana/tsconfig": "npm:^1.3.0-rc1" "@rollup/plugin-commonjs": "npm:25.0.7" "@rollup/plugin-json": "npm:6.1.0" "@rollup/plugin-node-resolve": "npm:15.2.3" @@ -4220,7 +4220,7 @@ __metadata: "@grafana/e2e-selectors": "npm:11.0.0-pre" "@grafana/faro-web-sdk": "npm:^1.3.6" "@grafana/schema": "npm:11.0.0-pre" - "@grafana/tsconfig": "npm:^1.2.0-rc1" + "@grafana/tsconfig": "npm:^1.3.0-rc1" "@leeoniya/ufuzzy": "npm:1.0.14" "@monaco-editor/react": "npm:4.6.0" "@popperjs/core": "npm:2.11.8" From 5f41cc632ea404f5b6adf6b60a021fc75182d63c Mon Sep 17 00:00:00 2001 From: Isabel <76437239+imatwawana@users.noreply.github.com> Date: Thu, 22 Feb 2024 09:34:16 -0500 Subject: [PATCH 0099/1406] Docs: update import troubleshoot dashboards links (#83124) * Updated links to former manage dashboards content * Removed links to manage dashboards and added export content to Sharing page * Replaced grafana links with cloud docs links * Removed trailing slash from link * trigger CI --------- Co-authored-by: Jack Baldry --- docs/sources/dashboards/_index.md | 34 +++++++++++-------- .../dashboards/create-reports/index.md | 8 ++--- .../share-dashboards-panels/index.md | 24 ++++++++++--- 3 files changed, 42 insertions(+), 24 deletions(-) diff --git a/docs/sources/dashboards/_index.md b/docs/sources/dashboards/_index.md index 297c26cd7a..0207994cd7 100644 --- a/docs/sources/dashboards/_index.md +++ b/docs/sources/dashboards/_index.md @@ -28,46 +28,50 @@ Before you begin, ensure that you have configured a data source. See also: - [Playlist][] - [Reporting][] - [Version history][] -- [Export and import][] +- [Import][] +- [Export and share][] - [JSON model][] {{% docs/reference %}} [data source]: "/docs/grafana/ -> /docs/grafana//datasources" -[data source]: "/docs/grafana-cloud/ -> /docs/grafana//datasources" +[data source]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/connect-externally-hosted/data-sources" [Reporting]: "/docs/grafana/ -> /docs/grafana//dashboards/create-reports" -[Reporting]: "/docs/grafana-cloud/ -> /docs/grafana//dashboards/create-reports" +[Reporting]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/dashboards/create-reports" [Public dashboards]: "/docs/grafana/ -> /docs/grafana//dashboards/dashboard-public" -[Public dashboards]: "/docs/grafana-cloud/ -> /docs/grafana//dashboards/dashboard-public" +[Public dashboards]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/dashboards/dashboard-public" [Version history]: "/docs/grafana/ -> /docs/grafana//dashboards/build-dashboards/manage-version-history" -[Version history]: "/docs/grafana-cloud/ -> /docs/grafana//dashboards/build-dashboards/manage-version-history" +[Version history]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/dashboards/build-dashboards/manage-version-history" [panels]: "/docs/grafana/ -> /docs/grafana//panels-visualizations" -[panels]: "/docs/grafana-cloud/ -> /docs/grafana//panels-visualizations" +[panels]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/panels-visualizations" [Annotations]: "/docs/grafana/ -> /docs/grafana//dashboards/build-dashboards/annotate-visualizations" -[Annotations]: "/docs/grafana-cloud/ -> /docs/grafana//dashboards/build-dashboards/annotate-visualizations" +[Annotations]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/dashboards/build-dashboards/annotate-visualizations" [Create dashboard folders]: "/docs/grafana/ -> /docs/grafana//dashboards/manage-dashboards#create-a-dashboard-folder" -[Create dashboard folders]: "/docs/grafana-cloud/ -> /docs/grafana//dashboards/manage-dashboards#create-a-dashboard-folder" +[Create dashboard folders]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/dashboards/manage-dashboards#create-a-dashboard-folder" [JSON model]: "/docs/grafana/ -> /docs/grafana//dashboards/build-dashboards/view-dashboard-json-model" -[JSON model]: "/docs/grafana-cloud/ -> /docs/grafana//dashboards/build-dashboards/view-dashboard-json-model" +[JSON model]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/dashboards/build-dashboards/view-dashboard-json-model" -[Export and import]: "/docs/grafana/ -> /docs/grafana//dashboards/manage-dashboards#export-and-import-dashboards" -[Export and import]: "/docs/grafana-cloud/ -> /docs/grafana//dashboards/manage-dashboards#export-and-import-dashboards" +[Import]: "/docs/grafana/ -> /docs/grafana//dashboards/build-dashboards/import-dashboards" +[Import]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/dashboards/build-dashboards/import-dashboards" + +[Export and share]: "/docs/grafana/ -> /docs/grafana//dashboards/share-dashboards-panels" +[Export and share]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/dashboards/share-dashboards-panels" [Manage dashboards]: "/docs/grafana/ -> /docs/grafana//dashboards/manage-dashboards" -[Manage dashboards]: "/docs/grafana-cloud/ -> /docs/grafana//dashboards/manage-dashboards" +[Manage dashboards]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/dashboards/manage-dashboards" [Build dashboards]: "/docs/grafana/ -> /docs/grafana//dashboards/build-dashboards" -[Build dashboards]: "/docs/grafana-cloud/ -> /docs/grafana//dashboards/build-dashboards" +[Build dashboards]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/dashboards/build-dashboards" [Use dashboards]: "/docs/grafana/ -> /docs/grafana//dashboards/use-dashboards" -[Use dashboards]: "/docs/grafana-cloud/ -> /docs/grafana//dashboards/use-dashboards" +[Use dashboards]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/dashboards/use-dashboards" [Playlist]: "/docs/grafana/ -> /docs/grafana//dashboards/create-manage-playlists" -[Playlist]: "/docs/grafana-cloud/ -> /docs/grafana//dashboards/create-manage-playlists" +[Playlist]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/dashboards/create-manage-playlists" {{% /docs/reference %}} diff --git a/docs/sources/dashboards/create-reports/index.md b/docs/sources/dashboards/create-reports/index.md index e0026f9055..aebdae63d0 100644 --- a/docs/sources/dashboards/create-reports/index.md +++ b/docs/sources/dashboards/create-reports/index.md @@ -284,8 +284,8 @@ filters = report:debug ``` {{% docs/reference %}} -[time range controls]: "/docs/grafana/ -> /docs/grafana//dashboards/manage-dashboards" -[time range controls]: "/docs/grafana-cloud/ -> /docs/grafana//dashboards/manage-dashboards" +[time range controls]: "/docs/grafana/ -> /docs/grafana//dashboards/use-dashboards#set-dashboard-time-range" +[time range controls]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/dashboards/use-dashboards#set-dashboard-time-range" [image rendering]: "/docs/grafana/ -> /docs/grafana//setup-grafana/image-rendering" [image rendering]: "/docs/grafana-cloud/ -> /docs/grafana//setup-grafana/image-rendering" @@ -306,10 +306,10 @@ filters = report:debug [SMTP]: "/docs/grafana-cloud/ -> /docs/grafana//setup-grafana/configure-grafana#smtp" [Repeat panels or rows]: "/docs/grafana/ -> /docs/grafana//panels-visualizations/configure-panel-options#configure-repeating-rows-or-panels" -[Repeat panels or rows]: "/docs/grafana-cloud/ -> /docs/grafana//panels-visualizations/configure-panel-options#configure-repeating-rows-or-panels" +[Repeat panels or rows]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/panels-visualizations/configure-panel-options#configure-repeating-rows-or-panels" [Templates and variables]: "/docs/grafana/ -> /docs/grafana//dashboards/variables" -[Templates and variables]: "/docs/grafana-cloud/ -> /docs/grafana//dashboards/variables" +[Templates and variables]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/dashboards/variables" [temp-data-lifetime]: "/docs/grafana/ -> /docs/grafana//setup-grafana/configure-grafana#temp-data-lifetime" [temp-data-lifetime]: "/docs/grafana-cloud/ -> /docs/grafana//setup-grafana/configure-grafana#temp-data-lifetime" diff --git a/docs/sources/dashboards/share-dashboards-panels/index.md b/docs/sources/dashboards/share-dashboards-panels/index.md index a6f66b8a51..84ef034629 100644 --- a/docs/sources/dashboards/share-dashboards-panels/index.md +++ b/docs/sources/dashboards/share-dashboards-panels/index.md @@ -93,9 +93,26 @@ You can publish snapshots to your local instance or to [snapshots.raintank.io](h If you created a snapshot by mistake, click **Delete snapshot** to remove the snapshot from your Grafana instance. -### Dashboard export +### Export a dashboard as JSON -Grafana dashboards can easily be exported and imported. For more information, refer to [Export and import dashboards][]. +The dashboard export action creates a Grafana JSON file that contains everything you need, including layout, variables, styles, data sources, queries, and so on, so that you can later import the dashboard. + +1. Click **Dashboards** in the main menu. +1. Open the dashboard you want to export. +1. Click the **Share** icon in the top navigation bar. +1. Click **Export**. + + If you're exporting the dashboard to use in another instance, with different data source UIDs, enable the **Export for sharing externally** switch. + +1. Click **Save to file**. + +Grafana downloads a JSON file to your local machine. + +#### Make a dashboard portable + +If you want to export a dashboard for others to use, you can add template variables for things like a metric prefix (use a constant variable) and server name. + +A template variable of the type `Constant` is automatically hidden in the dashboard, and is also added as a required input when the dashboard is imported. ## Export dashboard as PDF @@ -192,9 +209,6 @@ To create a library panel from the **Share Panel** dialog: 1. Save the dashboard. {{% docs/reference %}} -[Export and import dashboards]: "/docs/grafana/ -> /docs/grafana//dashboards/manage-dashboards#export-and-import-dashboards" -[Export and import dashboards]: "/docs/grafana-cloud/ -> /docs/grafana//dashboards/manage-dashboards#export-and-import-dashboards" - [Grafana Enterprise]: "/docs/grafana/ -> /docs/grafana//introduction/grafana-enterprise" [Grafana Enterprise]: "/docs/grafana-cloud/ -> /docs/grafana//introduction/grafana-enterprise" From 5f89c69b66e0bd19c415b6133dbc87ba96fddff5 Mon Sep 17 00:00:00 2001 From: Jack Westbrook Date: Thu, 22 Feb 2024 16:22:23 +0100 Subject: [PATCH 0100/1406] Chore: Bump Lerna 8.x.x (#83233) * chore(lerna): bump lerna to 8.x.x * chore(lerna): run lerna repair command to update lerna.json * ci(drone): use raw output (no quotes) when updating package.json version * ci(drone): update config file --- .drone.yml | 4 +- lerna.json | 1 + package.json | 2 +- scripts/drone/steps/lib.star | 2 +- yarn.lock | 882 ++++++++++++++++++++++------------- 5 files changed, 555 insertions(+), 336 deletions(-) diff --git a/.drone.yml b/.drone.yml index 6461078c56..1e4a58497c 100644 --- a/.drone.yml +++ b/.drone.yml @@ -1821,7 +1821,7 @@ steps: name: yarn-install - commands: - apk add --update jq - - new_version=$(cat package.json | jq .version | sed s/pre/${DRONE_BUILD_NUMBER}/g) + - new_version=$(cat package.json | jq -r .version | sed s/pre/${DRONE_BUILD_NUMBER}/g) - 'echo "New version: $new_version"' - yarn run lerna version $new_version --exact --no-git-tag-version --no-push --force-publish -y @@ -4804,6 +4804,6 @@ kind: secret name: gcr_credentials --- kind: signature -hmac: 14033c6fa9749db7c17aca8b404569c8e4bcb51e39cdb39b300870758db10c48 +hmac: 043eca32327fd48d9b479c3a51549b30f3825553c55df6220581def9bd22f513 ... diff --git a/lerna.json b/lerna.json index 4a0012aadc..48d6b6b8c4 100644 --- a/lerna.json +++ b/lerna.json @@ -1,4 +1,5 @@ { + "$schema": "node_modules/lerna/schemas/lerna-schema.json", "npmClient": "yarn", "version": "11.0.0-pre" } diff --git a/package.json b/package.json index 3d3677e24d..d6c7c8d04c 100644 --- a/package.json +++ b/package.json @@ -184,7 +184,7 @@ "jest-fail-on-console": "3.1.2", "jest-junit": "16.0.0", "jest-matcher-utils": "29.7.0", - "lerna": "7.4.1", + "lerna": "8.1.2", "mini-css-extract-plugin": "2.8.0", "msw": "2.2.1", "mutationobserver-shim": "0.3.7", diff --git a/scripts/drone/steps/lib.star b/scripts/drone/steps/lib.star index 36344a5a98..f94f0ee4f3 100644 --- a/scripts/drone/steps/lib.star +++ b/scripts/drone/steps/lib.star @@ -438,7 +438,7 @@ def update_package_json_version(): ], "commands": [ "apk add --update jq", - "new_version=$(cat package.json | jq .version | sed s/pre/${DRONE_BUILD_NUMBER}/g)", + "new_version=$(cat package.json | jq -r .version | sed s/pre/${DRONE_BUILD_NUMBER}/g)", "echo \"New version: $new_version\"", "yarn run lerna version $new_version --exact --no-git-tag-version --no-push --force-publish -y", "yarn install --mode=update-lockfile", diff --git a/yarn.lock b/yarn.lock index 9cf178efdb..e4a7b4cf5a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3221,7 +3221,7 @@ __metadata: languageName: node linkType: hard -"@gar/promisify@npm:^1.0.1, @gar/promisify@npm:^1.1.3": +"@gar/promisify@npm:^1.0.1": version: 1.1.3 resolution: "@gar/promisify@npm:1.1.3" checksum: 10/052dd232140fa60e81588000cbe729a40146579b361f1070bce63e2a761388a22a16d00beeffc504bd3601cb8e055c57b21a185448b3ed550cf50716f4fd442e @@ -4931,24 +4931,12 @@ __metadata: languageName: node linkType: hard -"@lerna/child-process@npm:7.4.1": - version: 7.4.1 - resolution: "@lerna/child-process@npm:7.4.1" - dependencies: - chalk: "npm:^4.1.0" - execa: "npm:^5.0.0" - strong-log-transformer: "npm:^2.1.0" - checksum: 10/6be434a3d8aaf41e290dd0169133417cdb3b33ffd59fe77c7a927f28302fb8712a0be63fd261cf1b9c601000ed4dba1f86f8c0a8c3fa97fc665cd4e3458fc1ba - languageName: node - linkType: hard - -"@lerna/create@npm:7.4.1": - version: 7.4.1 - resolution: "@lerna/create@npm:7.4.1" +"@lerna/create@npm:8.1.2": + version: 8.1.2 + resolution: "@lerna/create@npm:8.1.2" dependencies: - "@lerna/child-process": "npm:7.4.1" - "@npmcli/run-script": "npm:6.0.2" - "@nx/devkit": "npm:>=16.5.1 < 17" + "@npmcli/run-script": "npm:7.0.2" + "@nx/devkit": "npm:>=17.1.2 < 19" "@octokit/plugin-enterprise-rest": "npm:6.0.1" "@octokit/rest": "npm:19.0.11" byte-size: "npm:8.1.1" @@ -4985,12 +4973,12 @@ __metadata: npm-packlist: "npm:5.1.1" npm-registry-fetch: "npm:^14.0.5" npmlog: "npm:^6.0.2" - nx: "npm:>=16.5.1 < 17" + nx: "npm:>=17.1.2 < 19" p-map: "npm:4.0.0" p-map-series: "npm:2.1.0" p-queue: "npm:6.6.2" p-reduce: "npm:^2.1.0" - pacote: "npm:^15.2.0" + pacote: "npm:^17.0.5" pify: "npm:5.0.0" read-cmd-shim: "npm:4.0.0" read-package-json: "npm:6.0.4" @@ -5009,9 +4997,9 @@ __metadata: validate-npm-package-name: "npm:5.0.0" write-file-atomic: "npm:5.0.1" write-pkg: "npm:4.0.0" - yargs: "npm:16.2.0" - yargs-parser: "npm:20.2.4" - checksum: 10/b475e41761a77c519d84711d80799db97c2844d1d100c91e0a7c9d8c500c3f34654d4915d7bbb08cd1c95d703a8b2b8cc92257ee14aaa64474ce8662977a4bcd + yargs: "npm:17.7.2" + yargs-parser: "npm:21.1.1" + checksum: 10/d12b378cec4396d01f05127f9921dba83aae5f9c682d9cc01a15092bcd806625ad97126cd7b0dcc3cbfa851a9886c398d7562106ce9972603e00d02ddf6a6c61 languageName: node linkType: hard @@ -5404,6 +5392,19 @@ __metadata: languageName: node linkType: hard +"@npmcli/agent@npm:^2.0.0": + version: 2.2.1 + resolution: "@npmcli/agent@npm:2.2.1" + dependencies: + agent-base: "npm:^7.1.0" + http-proxy-agent: "npm:^7.0.0" + https-proxy-agent: "npm:^7.0.1" + lru-cache: "npm:^10.0.1" + socks-proxy-agent: "npm:^8.0.1" + checksum: 10/d4a48128f61e47f2f5c89315a5350e265dc619987e635bd62b52b29c7ed93536e724e721418c0ce352ceece86c13043c67aba1b70c3f5cc72fce6bb746706162 + languageName: node + linkType: hard + "@npmcli/fs@npm:^1.0.0": version: 1.0.0 resolution: "@npmcli/fs@npm:1.0.0" @@ -5414,16 +5415,6 @@ __metadata: languageName: node linkType: hard -"@npmcli/fs@npm:^2.1.0": - version: 2.1.0 - resolution: "@npmcli/fs@npm:2.1.0" - dependencies: - "@gar/promisify": "npm:^1.1.3" - semver: "npm:^7.3.5" - checksum: 10/1fe97efb5c1250c5986b46b6c8256b1eab8159a6d50fc8ace9f90937b3195541272faf77f18bdbf5eeb89bab68332c7846ac5ab9337e6099e63c6007388ebe84 - languageName: node - linkType: hard - "@npmcli/fs@npm:^3.1.0": version: 3.1.0 resolution: "@npmcli/fs@npm:3.1.0" @@ -5433,19 +5424,19 @@ __metadata: languageName: node linkType: hard -"@npmcli/git@npm:^4.0.0": - version: 4.1.0 - resolution: "@npmcli/git@npm:4.1.0" +"@npmcli/git@npm:^5.0.0": + version: 5.0.4 + resolution: "@npmcli/git@npm:5.0.4" dependencies: - "@npmcli/promise-spawn": "npm:^6.0.0" - lru-cache: "npm:^7.4.4" - npm-pick-manifest: "npm:^8.0.0" + "@npmcli/promise-spawn": "npm:^7.0.0" + lru-cache: "npm:^10.0.1" + npm-pick-manifest: "npm:^9.0.0" proc-log: "npm:^3.0.0" promise-inflight: "npm:^1.0.1" promise-retry: "npm:^2.0.1" semver: "npm:^7.3.5" - which: "npm:^3.0.0" - checksum: 10/33512ce12758d67c0322eca25019c4d5ef03e83f5829e09a05389af485bab216cc4df408b8eba98f2d12c119c6dff84f0d8ff25a1ac5d8a46184e55ae8f53754 + which: "npm:^4.0.0" + checksum: 10/136e71f4de73ef315285ebaf172b4681d1d22aff4c87ec526af1e57ab88ad7c864272523382009a2e3fab00f932bea204ed90cbdf187c7b7bd3d5c6e3d6c6d1a languageName: node linkType: hard @@ -5471,16 +5462,6 @@ __metadata: languageName: node linkType: hard -"@npmcli/move-file@npm:^2.0.0": - version: 2.0.0 - resolution: "@npmcli/move-file@npm:2.0.0" - dependencies: - mkdirp: "npm:^1.0.4" - rimraf: "npm:^3.0.2" - checksum: 10/1388777b507b0c592d53f41b9d182e1a8de7763bc625fc07999b8edbc22325f074e5b3ec90af79c89d6987fdb2325bc66d59f483258543c14a43661621f841b0 - languageName: node - linkType: hard - "@npmcli/node-gyp@npm:^3.0.0": version: 3.0.0 resolution: "@npmcli/node-gyp@npm:3.0.0" @@ -5488,132 +5469,161 @@ __metadata: languageName: node linkType: hard -"@npmcli/promise-spawn@npm:^6.0.0, @npmcli/promise-spawn@npm:^6.0.1": - version: 6.0.2 - resolution: "@npmcli/promise-spawn@npm:6.0.2" +"@npmcli/package-json@npm:^5.0.0": + version: 5.0.0 + resolution: "@npmcli/package-json@npm:5.0.0" dependencies: - which: "npm:^3.0.0" - checksum: 10/cc94a83ff1626ad93d42c2ea583dba1fb2d24cdab49caf0af77a3a0ff9bdbba34e09048b6821d4060ea7a58d4a41d49bece4ae3716929e2077c2fff0f5e94d94 + "@npmcli/git": "npm:^5.0.0" + glob: "npm:^10.2.2" + hosted-git-info: "npm:^7.0.0" + json-parse-even-better-errors: "npm:^3.0.0" + normalize-package-data: "npm:^6.0.0" + proc-log: "npm:^3.0.0" + semver: "npm:^7.5.3" + checksum: 10/bb907e934e96dae3d3aa26aa45cbaa87b318cb64c4aaaacfa3596b1ca5147ad1b51c3281eb529df12116a163d33ca99f48c4593b0c168e38412dfbf2c5cced72 languageName: node linkType: hard -"@npmcli/run-script@npm:6.0.2, @npmcli/run-script@npm:^6.0.0": - version: 6.0.2 - resolution: "@npmcli/run-script@npm:6.0.2" +"@npmcli/promise-spawn@npm:^7.0.0": + version: 7.0.1 + resolution: "@npmcli/promise-spawn@npm:7.0.1" + dependencies: + which: "npm:^4.0.0" + checksum: 10/7cbfc3c5e0bcad28e362dc34418b7507afea4fa82d692b802d9b8999ebdc99ceb2686f5959b5b9890e424983cee801401d3e972638f6942f75a2976a2c61774c + languageName: node + linkType: hard + +"@npmcli/run-script@npm:7.0.2": + version: 7.0.2 + resolution: "@npmcli/run-script@npm:7.0.2" dependencies: "@npmcli/node-gyp": "npm:^3.0.0" - "@npmcli/promise-spawn": "npm:^6.0.0" - node-gyp: "npm:^9.0.0" + "@npmcli/promise-spawn": "npm:^7.0.0" + node-gyp: "npm:^10.0.0" read-package-json-fast: "npm:^3.0.0" - which: "npm:^3.0.0" - checksum: 10/9b22c4c53d4b2e014e7f990cf2e1d32d1830c5629d37a4ee56011bcdfb51424ca8dc3fb3fa550b4abe7e8f0efdd68468d733b754db371b06a5dd300663cf13a2 + which: "npm:^4.0.0" + checksum: 10/4549311f3b937ca81d147b72fbfd41aa6ed7daf70ecc4e9ee3838f9cce1749e9c62c301943a8a67364a96c31bbc67c49ee31526fb12ec2f4b15148f0ef472f98 languageName: node linkType: hard -"@nrwl/devkit@npm:16.10.0": - version: 16.10.0 - resolution: "@nrwl/devkit@npm:16.10.0" +"@npmcli/run-script@npm:^7.0.0": + version: 7.0.4 + resolution: "@npmcli/run-script@npm:7.0.4" dependencies: - "@nx/devkit": "npm:16.10.0" - checksum: 10/2727b9927f8a7f3561c5eae72d3ca91d3ff0ea7c47da1d096d1654c85763acd580bbabb182db9e6f4f5df93152ae0972a0df7393f2dbad9d17b68549af313769 + "@npmcli/node-gyp": "npm:^3.0.0" + "@npmcli/package-json": "npm:^5.0.0" + "@npmcli/promise-spawn": "npm:^7.0.0" + node-gyp: "npm:^10.0.0" + which: "npm:^4.0.0" + checksum: 10/f09268051f74af7d7be46e9911a23126d531160c338d3c05d53e6cd7994b88271fb4ec524139fe7f2d826525f15a281eafef3be02831adc1f68556a8a668621a languageName: node linkType: hard -"@nrwl/tao@npm:16.10.0": - version: 16.10.0 - resolution: "@nrwl/tao@npm:16.10.0" +"@nrwl/devkit@npm:18.0.4": + version: 18.0.4 + resolution: "@nrwl/devkit@npm:18.0.4" dependencies: - nx: "npm:16.10.0" + "@nx/devkit": "npm:18.0.4" + checksum: 10/0ae56acdae6be74de5609fbb873b00e65605bca772ab22d13721b8465dff1926b3473522c836b904552214f79672bdfa19a5e9ce5e8ca30a1f6cf3ee8b15b29d + languageName: node + linkType: hard + +"@nrwl/tao@npm:18.0.4": + version: 18.0.4 + resolution: "@nrwl/tao@npm:18.0.4" + dependencies: + nx: "npm:18.0.4" tslib: "npm:^2.3.0" bin: tao: index.js - checksum: 10/df495b60f98112ffbeb19ae3d9385ecc18b3b9a2dbde1c50a91d5111408afba218c32ba55e6a3adf7c935262f4c18417672712e14185de2ec61beb5ef23186dc + checksum: 10/e4ae950c12388bf3c209486ca724f76c466c39653481ddf5335ef7c38db087c48ec25457ea3b08fefbb66b67c436097cb6fd92d4bd55ef5672b388de692d40e3 languageName: node linkType: hard -"@nx/devkit@npm:16.10.0, @nx/devkit@npm:>=16.5.1 < 17": - version: 16.10.0 - resolution: "@nx/devkit@npm:16.10.0" +"@nx/devkit@npm:18.0.4, @nx/devkit@npm:>=17.1.2 < 19": + version: 18.0.4 + resolution: "@nx/devkit@npm:18.0.4" dependencies: - "@nrwl/devkit": "npm:16.10.0" + "@nrwl/devkit": "npm:18.0.4" ejs: "npm:^3.1.7" enquirer: "npm:~2.3.6" ignore: "npm:^5.0.4" - semver: "npm:7.5.3" + semver: "npm:^7.5.3" tmp: "npm:~0.2.1" tslib: "npm:^2.3.0" + yargs-parser: "npm:21.1.1" peerDependencies: - nx: ">= 15 <= 17" - checksum: 10/d703e74d8360395dcafdc531e81c25ac6bbe46142169a5185f841067336b7dadce7f2102cfc2ef1c1826e3f9b92ca5b740a62ca4c32064265494dcd5a359ee21 + nx: ">= 16 <= 18" + checksum: 10/a3368c2d58e92aa96b0432930b302aa02c6e4b3a73a5f61f58e513f1a8f10606b1973c588277f48a2d37e3a4ede5d27aa837c4a9b4de25ae4c475cafe6c6305d languageName: node linkType: hard -"@nx/nx-darwin-arm64@npm:16.10.0": - version: 16.10.0 - resolution: "@nx/nx-darwin-arm64@npm:16.10.0" +"@nx/nx-darwin-arm64@npm:18.0.4": + version: 18.0.4 + resolution: "@nx/nx-darwin-arm64@npm:18.0.4" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@nx/nx-darwin-x64@npm:16.10.0": - version: 16.10.0 - resolution: "@nx/nx-darwin-x64@npm:16.10.0" +"@nx/nx-darwin-x64@npm:18.0.4": + version: 18.0.4 + resolution: "@nx/nx-darwin-x64@npm:18.0.4" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@nx/nx-freebsd-x64@npm:16.10.0": - version: 16.10.0 - resolution: "@nx/nx-freebsd-x64@npm:16.10.0" +"@nx/nx-freebsd-x64@npm:18.0.4": + version: 18.0.4 + resolution: "@nx/nx-freebsd-x64@npm:18.0.4" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard -"@nx/nx-linux-arm-gnueabihf@npm:16.10.0": - version: 16.10.0 - resolution: "@nx/nx-linux-arm-gnueabihf@npm:16.10.0" +"@nx/nx-linux-arm-gnueabihf@npm:18.0.4": + version: 18.0.4 + resolution: "@nx/nx-linux-arm-gnueabihf@npm:18.0.4" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@nx/nx-linux-arm64-gnu@npm:16.10.0": - version: 16.10.0 - resolution: "@nx/nx-linux-arm64-gnu@npm:16.10.0" +"@nx/nx-linux-arm64-gnu@npm:18.0.4": + version: 18.0.4 + resolution: "@nx/nx-linux-arm64-gnu@npm:18.0.4" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@nx/nx-linux-arm64-musl@npm:16.10.0": - version: 16.10.0 - resolution: "@nx/nx-linux-arm64-musl@npm:16.10.0" +"@nx/nx-linux-arm64-musl@npm:18.0.4": + version: 18.0.4 + resolution: "@nx/nx-linux-arm64-musl@npm:18.0.4" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@nx/nx-linux-x64-gnu@npm:16.10.0": - version: 16.10.0 - resolution: "@nx/nx-linux-x64-gnu@npm:16.10.0" +"@nx/nx-linux-x64-gnu@npm:18.0.4": + version: 18.0.4 + resolution: "@nx/nx-linux-x64-gnu@npm:18.0.4" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@nx/nx-linux-x64-musl@npm:16.10.0": - version: 16.10.0 - resolution: "@nx/nx-linux-x64-musl@npm:16.10.0" +"@nx/nx-linux-x64-musl@npm:18.0.4": + version: 18.0.4 + resolution: "@nx/nx-linux-x64-musl@npm:18.0.4" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@nx/nx-win32-arm64-msvc@npm:16.10.0": - version: 16.10.0 - resolution: "@nx/nx-win32-arm64-msvc@npm:16.10.0" +"@nx/nx-win32-arm64-msvc@npm:18.0.4": + version: 18.0.4 + resolution: "@nx/nx-win32-arm64-msvc@npm:18.0.4" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@nx/nx-win32-x64-msvc@npm:16.10.0": - version: 16.10.0 - resolution: "@nx/nx-win32-x64-msvc@npm:16.10.0" +"@nx/nx-win32-x64-msvc@npm:18.0.4": + version: 18.0.4 + resolution: "@nx/nx-win32-x64-msvc@npm:18.0.4" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -6097,17 +6107,6 @@ __metadata: languageName: node linkType: hard -"@parcel/watcher@npm:2.0.4": - version: 2.0.4 - resolution: "@parcel/watcher@npm:2.0.4" - dependencies: - node-addon-api: "npm:^3.2.1" - node-gyp: "npm:latest" - node-gyp-build: "npm:^4.3.0" - checksum: 10/ec3ba32c16856c34460d79bc95887f68869201e0cae68c5d1d4cd1f0358673d76dea56e194ede1e83af78656bde4eef2b17716a7396b54f63a40e4655c7a63c4 - languageName: node - linkType: hard - "@petamoriken/float16@npm:^3.4.7": version: 3.5.0 resolution: "@petamoriken/float16@npm:3.5.0" @@ -7347,6 +7346,22 @@ __metadata: languageName: node linkType: hard +"@sigstore/bundle@npm:^2.2.0": + version: 2.2.0 + resolution: "@sigstore/bundle@npm:2.2.0" + dependencies: + "@sigstore/protobuf-specs": "npm:^0.3.0" + checksum: 10/c7a3b0488f298df7d3089886d2f84213c336e0e151073a2f52e1583f783c6e08a54ffde1f436cf5953d5e30e9d0f5e41039124e359cf1171c184a53058e6fac9 + languageName: node + linkType: hard + +"@sigstore/core@npm:^1.0.0": + version: 1.0.0 + resolution: "@sigstore/core@npm:1.0.0" + checksum: 10/2e9dff65c6c00927e2e20c344d1437ace0398ce061f4aca458d63193a80cc884623b97d1eb0249ced4373ec83c0f1843937f47acec35c98b5b970956d866d6e9 + languageName: node + linkType: hard + "@sigstore/protobuf-specs@npm:^0.2.0": version: 0.2.1 resolution: "@sigstore/protobuf-specs@npm:0.2.1" @@ -7354,6 +7369,13 @@ __metadata: languageName: node linkType: hard +"@sigstore/protobuf-specs@npm:^0.3.0": + version: 0.3.0 + resolution: "@sigstore/protobuf-specs@npm:0.3.0" + checksum: 10/779583cc669f6e16f312a671a9902577e6744344a554e74dc0c8ad706211fc9bc44e03c933d6fb44d8388e63d3582875f8bad8027aac7fb4603c597af3189b2e + languageName: node + linkType: hard + "@sigstore/sign@npm:^1.0.0": version: 1.0.0 resolution: "@sigstore/sign@npm:1.0.0" @@ -7365,6 +7387,18 @@ __metadata: languageName: node linkType: hard +"@sigstore/sign@npm:^2.2.3": + version: 2.2.3 + resolution: "@sigstore/sign@npm:2.2.3" + dependencies: + "@sigstore/bundle": "npm:^2.2.0" + "@sigstore/core": "npm:^1.0.0" + "@sigstore/protobuf-specs": "npm:^0.3.0" + make-fetch-happen: "npm:^13.0.0" + checksum: 10/92da5cd20781b02c72cd4cc512dbd03cb7cf55ae46436255910f0d3122db2acbeca544daa108cf092322e5fd0ae4d22b912d7345b425c97ee2f6f97a15c3d009 + languageName: node + linkType: hard + "@sigstore/tuf@npm:^1.0.3": version: 1.0.3 resolution: "@sigstore/tuf@npm:1.0.3" @@ -7375,6 +7409,27 @@ __metadata: languageName: node linkType: hard +"@sigstore/tuf@npm:^2.3.1": + version: 2.3.1 + resolution: "@sigstore/tuf@npm:2.3.1" + dependencies: + "@sigstore/protobuf-specs": "npm:^0.3.0" + tuf-js: "npm:^2.2.0" + checksum: 10/40597098d379c05615beee048f2c7dfd43b2bd6ef7fdb1be69d8a2a65715ba8b0c2e9107515fe2570a8c93b75e52e8336a4f0333f62942f0ec9801924496ab0c + languageName: node + linkType: hard + +"@sigstore/verify@npm:^1.1.0": + version: 1.1.0 + resolution: "@sigstore/verify@npm:1.1.0" + dependencies: + "@sigstore/bundle": "npm:^2.2.0" + "@sigstore/core": "npm:^1.0.0" + "@sigstore/protobuf-specs": "npm:^0.3.0" + checksum: 10/c9e100df8c4e918aadfeb133c228e5963fb9e0712cc2840760a1269dfdd27edcb51772321b36198f34f9b9a88f736b3ab5ad6c5bd40bba8d411392a97c888766 + languageName: node + linkType: hard + "@sinclair/typebox@npm:^0.27.8": version: 0.27.8 resolution: "@sinclair/typebox@npm:0.27.8" @@ -9055,6 +9110,13 @@ __metadata: languageName: node linkType: hard +"@tufjs/canonical-json@npm:2.0.0": + version: 2.0.0 + resolution: "@tufjs/canonical-json@npm:2.0.0" + checksum: 10/cc719a1d0d0ae1aa1ba551a82c87dcbefac088e433c03a3d8a1d547ea721350e47dab4ab5b0fca40d5c7ab1f4882e72edc39c9eae15bf47c45c43bcb6ee39f4f + languageName: node + linkType: hard + "@tufjs/models@npm:1.0.4": version: 1.0.4 resolution: "@tufjs/models@npm:1.0.4" @@ -9065,6 +9127,16 @@ __metadata: languageName: node linkType: hard +"@tufjs/models@npm:2.0.0": + version: 2.0.0 + resolution: "@tufjs/models@npm:2.0.0" + dependencies: + "@tufjs/canonical-json": "npm:2.0.0" + minimatch: "npm:^9.0.3" + checksum: 10/d89d618c74c4eed3906d9ba5bd1bd9d0fa7a73ad6266b11c74c13102ee00bfdbd8e73fe786bd2e8e3ae347f9a66f044d973a7466dc7c2c2f98a7ff926ff275c4 + languageName: node + linkType: hard + "@types/angular-route@npm:1.7.6": version: 1.7.6 resolution: "@types/angular-route@npm:1.7.6" @@ -11574,6 +11646,13 @@ __metadata: languageName: node linkType: hard +"abbrev@npm:^2.0.0": + version: 2.0.0 + resolution: "abbrev@npm:2.0.0" + checksum: 10/ca0a54e35bea4ece0ecb68a47b312e1a9a6f772408d5bcb9051230aaa94b0460671c5b5c9cb3240eb5b7bc94c52476550eb221f65a0bbd0145bdc9f3113a6707 + languageName: node + linkType: hard + "accepts@npm:~1.3.4, accepts@npm:~1.3.5, accepts@npm:~1.3.8": version: 1.3.8 resolution: "accepts@npm:1.3.8" @@ -11699,6 +11778,15 @@ __metadata: languageName: node linkType: hard +"agent-base@npm:^7.0.2, agent-base@npm:^7.1.0": + version: 7.1.0 + resolution: "agent-base@npm:7.1.0" + dependencies: + debug: "npm:^4.3.4" + checksum: 10/f7828f991470a0cc22cb579c86a18cbae83d8a3cbed39992ab34fc7217c4d126017f1c74d0ab66be87f71455318a8ea3e757d6a37881b8d0f2a2c6aa55e5418f + languageName: node + linkType: hard + "agentkeepalive@npm:^4.1.3, agentkeepalive@npm:^4.2.1": version: 4.2.1 resolution: "agentkeepalive@npm:4.2.1" @@ -12361,14 +12449,14 @@ __metadata: languageName: node linkType: hard -"axios@npm:^1.0.0": - version: 1.5.1 - resolution: "axios@npm:1.5.1" +"axios@npm:^1.6.0": + version: 1.6.7 + resolution: "axios@npm:1.6.7" dependencies: - follow-redirects: "npm:^1.15.0" + follow-redirects: "npm:^1.15.4" form-data: "npm:^4.0.0" proxy-from-env: "npm:^1.1.0" - checksum: 10/67633db5867c789a6edb6e5229884501bef89584a6718220c243fd5a64de4ea7dcdfdf4f8368a672d582db78aaa9f8d7b619d39403b669f451e1242bbd4c7ee2 + checksum: 10/a1932b089ece759cd261f175d9ebf4d41c8994cf0c0767cda86055c7a19bcfdade8ae3464bf4cec4c8b142f4a657dc664fb77a41855e8376cf38b86d7a86518f languageName: node linkType: hard @@ -13106,49 +13194,43 @@ __metadata: languageName: node linkType: hard -"cacache@npm:^16.1.0": - version: 16.1.1 - resolution: "cacache@npm:16.1.1" +"cacache@npm:^17.0.0": + version: 17.1.4 + resolution: "cacache@npm:17.1.4" dependencies: - "@npmcli/fs": "npm:^2.1.0" - "@npmcli/move-file": "npm:^2.0.0" - chownr: "npm:^2.0.0" - fs-minipass: "npm:^2.1.0" - glob: "npm:^8.0.1" - infer-owner: "npm:^1.0.4" + "@npmcli/fs": "npm:^3.1.0" + fs-minipass: "npm:^3.0.0" + glob: "npm:^10.2.2" lru-cache: "npm:^7.7.1" - minipass: "npm:^3.1.6" + minipass: "npm:^7.0.3" minipass-collect: "npm:^1.0.2" minipass-flush: "npm:^1.0.5" minipass-pipeline: "npm:^1.2.4" - mkdirp: "npm:^1.0.4" p-map: "npm:^4.0.0" - promise-inflight: "npm:^1.0.1" - rimraf: "npm:^3.0.2" - ssri: "npm:^9.0.0" + ssri: "npm:^10.0.0" tar: "npm:^6.1.11" - unique-filename: "npm:^1.1.1" - checksum: 10/8356f969767ff11ed5e9dc6fcb3fc47d227431c6e68086a34ae08b2f3744909e6e22ae1868dc5ab094132a3d8dfc174f08bd7f3122abf50cf56fd789553d3d1f + unique-filename: "npm:^3.0.0" + checksum: 10/6e26c788bc6a18ff42f4d4f97db30d5c60a5dfac8e7c10a03b0307a92cf1b647570547cf3cd96463976c051eb9c7258629863f156e224c82018862c1a8ad0e70 languageName: node linkType: hard -"cacache@npm:^17.0.0": - version: 17.1.4 - resolution: "cacache@npm:17.1.4" +"cacache@npm:^18.0.0": + version: 18.0.2 + resolution: "cacache@npm:18.0.2" dependencies: "@npmcli/fs": "npm:^3.1.0" fs-minipass: "npm:^3.0.0" glob: "npm:^10.2.2" - lru-cache: "npm:^7.7.1" + lru-cache: "npm:^10.0.1" minipass: "npm:^7.0.3" - minipass-collect: "npm:^1.0.2" + minipass-collect: "npm:^2.0.1" minipass-flush: "npm:^1.0.5" minipass-pipeline: "npm:^1.2.4" p-map: "npm:^4.0.0" ssri: "npm:^10.0.0" tar: "npm:^6.1.11" unique-filename: "npm:^3.0.0" - checksum: 10/6e26c788bc6a18ff42f4d4f97db30d5c60a5dfac8e7c10a03b0307a92cf1b647570547cf3cd96463976c051eb9c7258629863f156e224c82018862c1a8ad0e70 + checksum: 10/5ca58464f785d4d64ac2019fcad95451c8c89bea25949f63acd8987fcc3493eaef1beccc0fa39e673506d879d3fc1ab420760f8a14f8ddf46ea2d121805a5e96 languageName: node linkType: hard @@ -14039,12 +14121,12 @@ __metadata: languageName: node linkType: hard -"conventional-changelog-angular@npm:6.0.0": - version: 6.0.0 - resolution: "conventional-changelog-angular@npm:6.0.0" +"conventional-changelog-angular@npm:7.0.0": + version: 7.0.0 + resolution: "conventional-changelog-angular@npm:7.0.0" dependencies: compare-func: "npm:^2.0.0" - checksum: 10/ddc59ead53a45b817d83208200967f5340866782b8362d5e2e34105fdfa3d3a31585ebbdec7750bdb9de53da869f847e8ca96634a9801f51e27ecf4e7ffe2bad + checksum: 10/e7966d2fee5475e76263f30f8b714b2b592b5bf556df225b7091e5090831fc9a20b99598a7d2997e19c2ef8118c0a3150b1eba290786367b0f55a5ccfa804ec9 languageName: node linkType: hard @@ -17356,6 +17438,13 @@ __metadata: languageName: node linkType: hard +"exponential-backoff@npm:^3.1.1": + version: 3.1.1 + resolution: "exponential-backoff@npm:3.1.1" + checksum: 10/2d9bbb6473de7051f96790d5f9a678f32e60ed0aa70741dc7fdc96fec8d631124ec3374ac144387604f05afff9500f31a1d45bd9eee4cdc2e4f9ad2d9b9d5dbd + languageName: node + linkType: hard + "expose-loader@npm:5.0.0": version: 5.0.0 resolution: "expose-loader@npm:5.0.0" @@ -17866,7 +17955,7 @@ __metadata: languageName: node linkType: hard -"follow-redirects@npm:^1.0.0, follow-redirects@npm:^1.15.0": +"follow-redirects@npm:^1.0.0": version: 1.15.3 resolution: "follow-redirects@npm:1.15.3" peerDependenciesMeta: @@ -17876,6 +17965,16 @@ __metadata: languageName: node linkType: hard +"follow-redirects@npm:^1.15.4": + version: 1.15.5 + resolution: "follow-redirects@npm:1.15.5" + peerDependenciesMeta: + debug: + optional: true + checksum: 10/d467f13c1c6aa734599b8b369cd7a625b20081af358f6204ff515f6f4116eb440de9c4e0c49f10798eeb0df26c95dd05d5e0d9ddc5786ab1a8a8abefe92929b4 + languageName: node + linkType: hard + "for-each@npm:^0.3.3": version: 0.3.3 resolution: "for-each@npm:0.3.3" @@ -18091,7 +18190,7 @@ __metadata: languageName: node linkType: hard -"fs-minipass@npm:^2.0.0, fs-minipass@npm:^2.1.0": +"fs-minipass@npm:^2.0.0": version: 2.1.0 resolution: "fs-minipass@npm:2.1.0" dependencies: @@ -18563,7 +18662,7 @@ __metadata: languageName: node linkType: hard -"glob@npm:10.3.10, glob@npm:^10.0.0, glob@npm:^10.2.2, glob@npm:^10.2.5, glob@npm:^10.2.7, glob@npm:^10.3.7": +"glob@npm:10.3.10, glob@npm:^10.0.0, glob@npm:^10.2.2, glob@npm:^10.2.5, glob@npm:^10.2.7, glob@npm:^10.3.10, glob@npm:^10.3.7": version: 10.3.10 resolution: "glob@npm:10.3.10" dependencies: @@ -18578,20 +18677,6 @@ __metadata: languageName: node linkType: hard -"glob@npm:7.1.4": - version: 7.1.4 - resolution: "glob@npm:7.1.4" - dependencies: - fs.realpath: "npm:^1.0.0" - inflight: "npm:^1.0.4" - inherits: "npm:2" - minimatch: "npm:^3.0.4" - once: "npm:^1.3.0" - path-is-absolute: "npm:^1.0.0" - checksum: 10/776bcc31371797eb5cf6b58c4618378f8df83d23f00aef8e98af5e7f0e59f5ee8b470c4e95e71cfa7a8682634849e21ea1f1ad38639c1828a2dbc2757bf7a63b - languageName: node - linkType: hard - "glob@npm:7.2.0": version: 7.2.0 resolution: "glob@npm:7.2.0" @@ -19004,7 +19089,7 @@ __metadata: json-source-map: "npm:0.6.1" jsurl: "npm:^0.1.5" kbar: "npm:0.1.0-beta.45" - lerna: "npm:7.4.1" + lerna: "npm:8.1.2" leven: "npm:^4.0.0" lodash: "npm:4.17.21" logfmt: "npm:^1.3.2" @@ -19477,6 +19562,15 @@ __metadata: languageName: node linkType: hard +"hosted-git-info@npm:^7.0.0": + version: 7.0.1 + resolution: "hosted-git-info@npm:7.0.1" + dependencies: + lru-cache: "npm:^10.0.1" + checksum: 10/5f740ecf3c70838e27446ff433a9a9a583de8747f7b661390b373ad12ca47edb937136e79999a4f953d0953079025a11df173f1fd9f7d52b0277b2fb9433e1c7 + languageName: node + linkType: hard + "hpack.js@npm:^2.1.6": version: 2.1.6 resolution: "hpack.js@npm:2.1.6" @@ -19704,6 +19798,16 @@ __metadata: languageName: node linkType: hard +"http-proxy-agent@npm:^7.0.0": + version: 7.0.2 + resolution: "http-proxy-agent@npm:7.0.2" + dependencies: + agent-base: "npm:^7.1.0" + debug: "npm:^4.3.4" + checksum: 10/d062acfa0cb82beeb558f1043c6ba770ea892b5fb7b28654dbc70ea2aeea55226dd34c02a294f6c1ca179a5aa483c4ea641846821b182edbd9cc5d89b54c6848 + languageName: node + linkType: hard + "http-proxy-middleware@npm:^2.0.3": version: 2.0.4 resolution: "http-proxy-middleware@npm:2.0.4" @@ -19798,6 +19902,16 @@ __metadata: languageName: node linkType: hard +"https-proxy-agent@npm:^7.0.1": + version: 7.0.4 + resolution: "https-proxy-agent@npm:7.0.4" + dependencies: + agent-base: "npm:^7.0.2" + debug: "npm:4" + checksum: 10/405fe582bba461bfe5c7e2f8d752b384036854488b828ae6df6a587c654299cbb2c50df38c4b6ab303502c3c5e029a793fbaac965d1e86ee0be03faceb554d63 + languageName: node + linkType: hard + "human-signals@npm:^1.1.1": version: 1.1.1 resolution: "human-signals@npm:1.1.1" @@ -19916,12 +20030,12 @@ __metadata: languageName: node linkType: hard -"ignore-walk@npm:^6.0.0": - version: 6.0.3 - resolution: "ignore-walk@npm:6.0.3" +"ignore-walk@npm:^6.0.4": + version: 6.0.4 + resolution: "ignore-walk@npm:6.0.4" dependencies: minimatch: "npm:^9.0.0" - checksum: 10/3cbc0b52c7dc405a3525898d705029f084ef6218df2a82b95520d72e3a6fb3ff893a4c22b73f36d2b7cefbe786a9687c4396de3c628be2844bc0728dc4e455cf + checksum: 10/a56c3f929bb0890ffb6e87dfaca7d5ce97f9e179fd68d49711edea55760aaee367cea3845d7620689b706249053c4b1805e21158f6751c7333f9b2ffb3668272 languageName: node linkType: hard @@ -20156,6 +20270,16 @@ __metadata: languageName: node linkType: hard +"ip-address@npm:^9.0.5": + version: 9.0.5 + resolution: "ip-address@npm:9.0.5" + dependencies: + jsbn: "npm:1.1.0" + sprintf-js: "npm:^1.1.3" + checksum: 10/1ed81e06721af012306329b31f532b5e24e00cb537be18ddc905a84f19fe8f83a09a1699862bf3a1ec4b9dea93c55a3fa5faf8b5ea380431469df540f38b092c + languageName: node + linkType: hard + "ip@npm:^1.1.5": version: 1.1.5 resolution: "ip@npm:1.1.5" @@ -20805,6 +20929,13 @@ __metadata: languageName: node linkType: hard +"isexe@npm:^3.1.1": + version: 3.1.1 + resolution: "isexe@npm:3.1.1" + checksum: 10/7fe1931ee4e88eb5aa524cd3ceb8c882537bc3a81b02e438b240e47012eef49c86904d0f0e593ea7c3a9996d18d0f1f3be8d3eaa92333977b0c3a9d353d5563e + languageName: node + linkType: hard + "isobject@npm:^3.0.1": version: 3.0.1 resolution: "isobject@npm:3.0.1" @@ -21594,6 +21725,13 @@ __metadata: languageName: node linkType: hard +"jsbn@npm:1.1.0": + version: 1.1.0 + resolution: "jsbn@npm:1.1.0" + checksum: 10/bebe7ae829bbd586ce8cbe83501dd8cb8c282c8902a8aeeed0a073a89dc37e8103b1244f3c6acd60278bcbfe12d93a3f83c9ac396868a3b3bbc3c5e5e3b648ef + languageName: node + linkType: hard + "jsbn@npm:~0.1.0": version: 0.1.1 resolution: "jsbn@npm:0.1.1" @@ -22043,14 +22181,13 @@ __metadata: languageName: node linkType: hard -"lerna@npm:7.4.1": - version: 7.4.1 - resolution: "lerna@npm:7.4.1" +"lerna@npm:8.1.2": + version: 8.1.2 + resolution: "lerna@npm:8.1.2" dependencies: - "@lerna/child-process": "npm:7.4.1" - "@lerna/create": "npm:7.4.1" - "@npmcli/run-script": "npm:6.0.2" - "@nx/devkit": "npm:>=16.5.1 < 17" + "@lerna/create": "npm:8.1.2" + "@npmcli/run-script": "npm:7.0.2" + "@nx/devkit": "npm:>=17.1.2 < 19" "@octokit/plugin-enterprise-rest": "npm:6.0.1" "@octokit/rest": "npm:19.0.11" byte-size: "npm:8.1.1" @@ -22058,7 +22195,7 @@ __metadata: clone-deep: "npm:4.0.1" cmd-shim: "npm:6.0.1" columnify: "npm:1.6.0" - conventional-changelog-angular: "npm:6.0.0" + conventional-changelog-angular: "npm:7.0.0" conventional-changelog-core: "npm:5.0.1" conventional-recommended-bump: "npm:7.0.1" cosmiconfig: "npm:^8.2.0" @@ -22093,14 +22230,14 @@ __metadata: npm-packlist: "npm:5.1.1" npm-registry-fetch: "npm:^14.0.5" npmlog: "npm:^6.0.2" - nx: "npm:>=16.5.1 < 17" + nx: "npm:>=17.1.2 < 19" p-map: "npm:4.0.0" p-map-series: "npm:2.1.0" p-pipe: "npm:3.1.0" p-queue: "npm:6.6.2" p-reduce: "npm:2.1.0" p-waterfall: "npm:2.1.1" - pacote: "npm:^15.2.0" + pacote: "npm:^17.0.5" pify: "npm:5.0.0" read-cmd-shim: "npm:4.0.0" read-package-json: "npm:6.0.4" @@ -22120,11 +22257,11 @@ __metadata: validate-npm-package-name: "npm:5.0.0" write-file-atomic: "npm:5.0.1" write-pkg: "npm:4.0.0" - yargs: "npm:16.2.0" - yargs-parser: "npm:20.2.4" + yargs: "npm:17.7.2" + yargs-parser: "npm:21.1.1" bin: lerna: dist/cli.js - checksum: 10/3b837bd48adefc962cd0c5ce79e6e7716580ec88784cbb3b8ecc9cbbfea79de6536576f42432e87b677c7c1267bfe47a13ce0ec4ab8687ecb721be58cedd547a + checksum: 10/5f4267bb059e00b294985e5cc4e783155e5be58ecd73c1791be0c24ff77561f2699550cdad4faee8a3a5d2866ae18bd6d7c4bba4b0dae1b26056d1187616c8a6 languageName: node linkType: hard @@ -22519,7 +22656,7 @@ __metadata: languageName: node linkType: hard -"lru-cache@npm:10.2.0, lru-cache@npm:^9.1.1 || ^10.0.0": +"lru-cache@npm:10.2.0, lru-cache@npm:^10.0.1, lru-cache@npm:^9.1.1 || ^10.0.0": version: 10.2.0 resolution: "lru-cache@npm:10.2.0" checksum: 10/502ec42c3309c0eae1ce41afca471f831c278566d45a5273a0c51102dee31e0e250a62fa9029c3370988df33a14188a38e682c16143b794de78668de3643e302 @@ -22544,7 +22681,7 @@ __metadata: languageName: node linkType: hard -"lru-cache@npm:^7.4.4, lru-cache@npm:^7.5.1, lru-cache@npm:^7.7.1": +"lru-cache@npm:^7.5.1, lru-cache@npm:^7.7.1": version: 7.12.0 resolution: "lru-cache@npm:7.12.0" checksum: 10/ac4e78bb5a04174389db377d325c9baecb5d5c5cf619894aa237891b3f5605e51426823b118753229b0bf4073db7091cc7593c096181e2ba20b4b5e2e59c9c9d @@ -22618,50 +22755,45 @@ __metadata: languageName: node linkType: hard -"make-fetch-happen@npm:^10.0.3": - version: 10.1.8 - resolution: "make-fetch-happen@npm:10.1.8" +"make-fetch-happen@npm:^11.0.0, make-fetch-happen@npm:^11.0.1, make-fetch-happen@npm:^11.1.1": + version: 11.1.1 + resolution: "make-fetch-happen@npm:11.1.1" dependencies: agentkeepalive: "npm:^4.2.1" - cacache: "npm:^16.1.0" - http-cache-semantics: "npm:^4.1.0" + cacache: "npm:^17.0.0" + http-cache-semantics: "npm:^4.1.1" http-proxy-agent: "npm:^5.0.0" https-proxy-agent: "npm:^5.0.0" is-lambda: "npm:^1.0.1" lru-cache: "npm:^7.7.1" - minipass: "npm:^3.1.6" - minipass-collect: "npm:^1.0.2" - minipass-fetch: "npm:^2.0.3" + minipass: "npm:^5.0.0" + minipass-fetch: "npm:^3.0.0" minipass-flush: "npm:^1.0.5" minipass-pipeline: "npm:^1.2.4" negotiator: "npm:^0.6.3" promise-retry: "npm:^2.0.1" socks-proxy-agent: "npm:^7.0.0" - ssri: "npm:^9.0.0" - checksum: 10/0f83e6814d2aae5e37fc20dc4e4657419ad6e79771d78f7689ad037633836e37a1e65f799bce47a71f0202e5fdbc346aca78804c3b4ccfee621f00b1cd3176a3 + ssri: "npm:^10.0.0" + checksum: 10/b4b442cfaaec81db159f752a5f2e3ee3d7aa682782868fa399200824ec6298502e01bdc456e443dc219bcd5546c8e4471644d54109c8599841dc961d17a805fa languageName: node linkType: hard -"make-fetch-happen@npm:^11.0.0, make-fetch-happen@npm:^11.0.1, make-fetch-happen@npm:^11.1.1": - version: 11.1.1 - resolution: "make-fetch-happen@npm:11.1.1" +"make-fetch-happen@npm:^13.0.0": + version: 13.0.0 + resolution: "make-fetch-happen@npm:13.0.0" dependencies: - agentkeepalive: "npm:^4.2.1" - cacache: "npm:^17.0.0" + "@npmcli/agent": "npm:^2.0.0" + cacache: "npm:^18.0.0" http-cache-semantics: "npm:^4.1.1" - http-proxy-agent: "npm:^5.0.0" - https-proxy-agent: "npm:^5.0.0" is-lambda: "npm:^1.0.1" - lru-cache: "npm:^7.7.1" - minipass: "npm:^5.0.0" + minipass: "npm:^7.0.2" minipass-fetch: "npm:^3.0.0" minipass-flush: "npm:^1.0.5" minipass-pipeline: "npm:^1.2.4" negotiator: "npm:^0.6.3" promise-retry: "npm:^2.0.1" - socks-proxy-agent: "npm:^7.0.0" ssri: "npm:^10.0.0" - checksum: 10/b4b442cfaaec81db159f752a5f2e3ee3d7aa682782868fa399200824ec6298502e01bdc456e443dc219bcd5546c8e4471644d54109c8599841dc961d17a805fa + checksum: 10/ded5a91a02b76381b06a4ec4d5c1d23ebbde15d402b3c3e4533b371dac7e2f7ca071ae71ae6dae72aa261182557b7b1b3fd3a705b39252dc17f74fa509d3e76f languageName: node linkType: hard @@ -23108,6 +23240,15 @@ __metadata: languageName: node linkType: hard +"minipass-collect@npm:^2.0.1": + version: 2.0.1 + resolution: "minipass-collect@npm:2.0.1" + dependencies: + minipass: "npm:^7.0.3" + checksum: 10/b251bceea62090f67a6cced7a446a36f4cd61ee2d5cea9aee7fff79ba8030e416327a1c5aa2908dc22629d06214b46d88fdab8c51ac76bacbf5703851b5ad342 + languageName: node + linkType: hard + "minipass-fetch@npm:^1.3.2": version: 1.4.1 resolution: "minipass-fetch@npm:1.4.1" @@ -23123,21 +23264,6 @@ __metadata: languageName: node linkType: hard -"minipass-fetch@npm:^2.0.3": - version: 2.1.0 - resolution: "minipass-fetch@npm:2.1.0" - dependencies: - encoding: "npm:^0.1.13" - minipass: "npm:^3.1.6" - minipass-sized: "npm:^1.0.3" - minizlib: "npm:^2.1.2" - dependenciesMeta: - encoding: - optional: true - checksum: 10/33b6927ef8a4516e27878e1e9966a6dee5c2efb844584b39712a8c222cf7cc586ae00c09897ce3b21e77b6600ad4c7503f8bd732ef1a8bf98137f18c45c6d6c4 - languageName: node - linkType: hard - "minipass-fetch@npm:^3.0.0": version: 3.0.4 resolution: "minipass-fetch@npm:3.0.4" @@ -23190,7 +23316,7 @@ __metadata: languageName: node linkType: hard -"minipass@npm:^3.0.0, minipass@npm:^3.1.0, minipass@npm:^3.1.1, minipass@npm:^3.1.3, minipass@npm:^3.1.6": +"minipass@npm:^3.0.0, minipass@npm:^3.1.0, minipass@npm:^3.1.1, minipass@npm:^3.1.3": version: 3.3.4 resolution: "minipass@npm:3.3.4" dependencies: @@ -23213,7 +23339,7 @@ __metadata: languageName: node linkType: hard -"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.3": +"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.2, minipass@npm:^7.0.3": version: 7.0.4 resolution: "minipass@npm:7.0.4" checksum: 10/e864bd02ceb5e0707696d58f7ce3a0b89233f0d686ef0d447a66db705c0846a8dc6f34865cd85256c1472ff623665f616b90b8ff58058b2ad996c5de747d2d18 @@ -23698,15 +23824,6 @@ __metadata: languageName: node linkType: hard -"node-addon-api@npm:^3.2.1": - version: 3.2.1 - resolution: "node-addon-api@npm:3.2.1" - dependencies: - node-gyp: "npm:latest" - checksum: 10/681b52dfa3e15b0a8e5cf283cc0d8cd5fd2a57c559ae670fcfd20544cbb32f75de7648674110defcd17ab2c76ebef630aa7d2d2f930bc7a8cc439b20fe233518 - languageName: node - linkType: hard - "node-dir@npm:^0.1.10, node-dir@npm:^0.1.17": version: 0.1.17 resolution: "node-dir@npm:0.1.17" @@ -23758,34 +23875,23 @@ __metadata: languageName: node linkType: hard -"node-gyp-build@npm:^4.3.0": - version: 4.5.0 - resolution: "node-gyp-build@npm:4.5.0" - bin: - node-gyp-build: bin.js - node-gyp-build-optional: optional.js - node-gyp-build-test: build-test.js - checksum: 10/1f6c2b519cfbf13fc60589d40b65d9aa8c8bfaefe99763a9a982a6518a9292c83f41adf558628cfcb748e2a55418ac91718b68bb6be7e02cfac90c82e412de9b - languageName: node - linkType: hard - -"node-gyp@npm:^9.0.0": - version: 9.0.0 - resolution: "node-gyp@npm:9.0.0" +"node-gyp@npm:^10.0.0": + version: 10.0.1 + resolution: "node-gyp@npm:10.0.1" dependencies: env-paths: "npm:^2.2.0" - glob: "npm:^7.1.4" + exponential-backoff: "npm:^3.1.1" + glob: "npm:^10.3.10" graceful-fs: "npm:^4.2.6" - make-fetch-happen: "npm:^10.0.3" - nopt: "npm:^5.0.0" - npmlog: "npm:^6.0.0" - rimraf: "npm:^3.0.2" + make-fetch-happen: "npm:^13.0.0" + nopt: "npm:^7.0.0" + proc-log: "npm:^3.0.0" semver: "npm:^7.3.5" tar: "npm:^6.1.2" - which: "npm:^2.0.2" + which: "npm:^4.0.0" bin: node-gyp: bin/node-gyp.js - checksum: 10/7a9f184dda7bd53970ac52e138b091b417505bef5be0a7d9a902137a55246afaebbae1263a0545b6d7d94af131bcd49ac99f18db0b801c5b4c627dd291c08a7f + checksum: 10/578cf0c821f258ce4b6ebce4461eca4c991a4df2dee163c0624f2fe09c7d6d37240be4942285a0048d307230248ee0b18382d6623b9a0136ce9533486deddfa8 languageName: node linkType: hard @@ -23855,6 +23961,17 @@ __metadata: languageName: node linkType: hard +"nopt@npm:^7.0.0": + version: 7.2.0 + resolution: "nopt@npm:7.2.0" + dependencies: + abbrev: "npm:^2.0.0" + bin: + nopt: bin/nopt.js + checksum: 10/1e7489f17cbda452c8acaf596a8defb4ae477d2a9953b76eb96f4ec3f62c6b421cd5174eaa742f88279871fde9586d8a1d38fb3f53fa0c405585453be31dff4c + languageName: node + linkType: hard + "normalize-package-data@npm:^2.3.2, normalize-package-data@npm:^2.5.0": version: 2.5.0 resolution: "normalize-package-data@npm:2.5.0" @@ -23891,6 +24008,18 @@ __metadata: languageName: node linkType: hard +"normalize-package-data@npm:^6.0.0": + version: 6.0.0 + resolution: "normalize-package-data@npm:6.0.0" + dependencies: + hosted-git-info: "npm:^7.0.0" + is-core-module: "npm:^2.8.1" + semver: "npm:^7.3.5" + validate-npm-package-license: "npm:^3.0.4" + checksum: 10/e31e31a2ebaef93ef107feb9408f105044eeae9cb7d0d4619544ab2323cd4b15ca648b0d558ac29db2fece161c7b8658206bb27ebe9340df723f7174b3e2759d + languageName: node + linkType: hard + "normalize-path@npm:3.0.0, normalize-path@npm:^3.0.0, normalize-path@npm:~3.0.0": version: 3.0.0 resolution: "normalize-path@npm:3.0.0" @@ -23978,6 +24107,18 @@ __metadata: languageName: node linkType: hard +"npm-package-arg@npm:^11.0.0": + version: 11.0.1 + resolution: "npm-package-arg@npm:11.0.1" + dependencies: + hosted-git-info: "npm:^7.0.0" + proc-log: "npm:^3.0.0" + semver: "npm:^7.3.5" + validate-npm-package-name: "npm:^5.0.0" + checksum: 10/a16e632703e106b3e9a6b4902d14a3493c8371745bcf8ba8f4ea9f152e12d5ed927487931e9adf817d05ba97b04941b33fec1d140dbd7da09181b546fde35b3c + languageName: node + linkType: hard + "npm-packlist@npm:5.1.1": version: 5.1.1 resolution: "npm-packlist@npm:5.1.1" @@ -23992,28 +24133,28 @@ __metadata: languageName: node linkType: hard -"npm-packlist@npm:^7.0.0": - version: 7.0.4 - resolution: "npm-packlist@npm:7.0.4" +"npm-packlist@npm:^8.0.0": + version: 8.0.2 + resolution: "npm-packlist@npm:8.0.2" dependencies: - ignore-walk: "npm:^6.0.0" - checksum: 10/b24644eefa21d33c55a8f49c64eda4b06edfb7d25853be8ded7346e73c6c447be8a0482314b74f04f94e3f5712e467505dc030826ba55a71d1b948459fad6486 + ignore-walk: "npm:^6.0.4" + checksum: 10/707206e5c09a1b8aa04e590592715ba5ab8732add1bbb5eeaff54b9c6b2740764c9e94c99e390c13245970b51c2cc92b8d44594c2784fcd96f255c7109622322 languageName: node linkType: hard -"npm-pick-manifest@npm:^8.0.0": - version: 8.0.2 - resolution: "npm-pick-manifest@npm:8.0.2" +"npm-pick-manifest@npm:^9.0.0": + version: 9.0.0 + resolution: "npm-pick-manifest@npm:9.0.0" dependencies: npm-install-checks: "npm:^6.0.0" npm-normalize-package-bin: "npm:^3.0.0" - npm-package-arg: "npm:^10.0.0" + npm-package-arg: "npm:^11.0.0" semver: "npm:^7.3.5" - checksum: 10/3f10a34e12cbb576edb694562a32730c6c0244b2929b91202d1be62ece76bc8b282dc7e9535d313d598963f8e3d06d19973611418a191fe3102be149a8fa0910 + checksum: 10/29dca2a838ed35c714df1a76f76616df2df51ce31bc3ca5943a0668b2eca2a5aab448f9f89cadf7a77eb5e3831c554cebaf7802f3e432838acb34c1a74fa2786 languageName: node linkType: hard -"npm-registry-fetch@npm:^14.0.0, npm-registry-fetch@npm:^14.0.3, npm-registry-fetch@npm:^14.0.5": +"npm-registry-fetch@npm:^14.0.3, npm-registry-fetch@npm:^14.0.5": version: 14.0.5 resolution: "npm-registry-fetch@npm:14.0.5" dependencies: @@ -24028,6 +24169,21 @@ __metadata: languageName: node linkType: hard +"npm-registry-fetch@npm:^16.0.0": + version: 16.1.0 + resolution: "npm-registry-fetch@npm:16.1.0" + dependencies: + make-fetch-happen: "npm:^13.0.0" + minipass: "npm:^7.0.2" + minipass-fetch: "npm:^3.0.0" + minipass-json-stream: "npm:^1.0.1" + minizlib: "npm:^2.1.2" + npm-package-arg: "npm:^11.0.0" + proc-log: "npm:^3.0.0" + checksum: 10/ba760c9cdacb1219ac5d8fecc26b1c55d502b55d45ab85ad556353b9bc5ba664c226fda54284c06df8c7eecfdcacb1aa065838ea7d1b0189d24c4d3f186309d2 + languageName: node + linkType: hard + "npm-run-path@npm:^4.0.0, npm-run-path@npm:^4.0.1": version: 4.0.1 resolution: "npm-run-path@npm:4.0.1" @@ -24061,7 +24217,7 @@ __metadata: languageName: node linkType: hard -"npmlog@npm:^6.0.0, npmlog@npm:^6.0.2": +"npmlog@npm:^6.0.2": version: 6.0.2 resolution: "npmlog@npm:6.0.2" dependencies: @@ -24096,26 +24252,25 @@ __metadata: languageName: node linkType: hard -"nx@npm:16.10.0, nx@npm:>=16.5.1 < 17": - version: 16.10.0 - resolution: "nx@npm:16.10.0" +"nx@npm:18.0.4, nx@npm:>=17.1.2 < 19": + version: 18.0.4 + resolution: "nx@npm:18.0.4" dependencies: - "@nrwl/tao": "npm:16.10.0" - "@nx/nx-darwin-arm64": "npm:16.10.0" - "@nx/nx-darwin-x64": "npm:16.10.0" - "@nx/nx-freebsd-x64": "npm:16.10.0" - "@nx/nx-linux-arm-gnueabihf": "npm:16.10.0" - "@nx/nx-linux-arm64-gnu": "npm:16.10.0" - "@nx/nx-linux-arm64-musl": "npm:16.10.0" - "@nx/nx-linux-x64-gnu": "npm:16.10.0" - "@nx/nx-linux-x64-musl": "npm:16.10.0" - "@nx/nx-win32-arm64-msvc": "npm:16.10.0" - "@nx/nx-win32-x64-msvc": "npm:16.10.0" - "@parcel/watcher": "npm:2.0.4" + "@nrwl/tao": "npm:18.0.4" + "@nx/nx-darwin-arm64": "npm:18.0.4" + "@nx/nx-darwin-x64": "npm:18.0.4" + "@nx/nx-freebsd-x64": "npm:18.0.4" + "@nx/nx-linux-arm-gnueabihf": "npm:18.0.4" + "@nx/nx-linux-arm64-gnu": "npm:18.0.4" + "@nx/nx-linux-arm64-musl": "npm:18.0.4" + "@nx/nx-linux-x64-gnu": "npm:18.0.4" + "@nx/nx-linux-x64-musl": "npm:18.0.4" + "@nx/nx-win32-arm64-msvc": "npm:18.0.4" + "@nx/nx-win32-x64-msvc": "npm:18.0.4" "@yarnpkg/lockfile": "npm:^1.1.0" "@yarnpkg/parsers": "npm:3.0.0-rc.46" "@zkochan/js-yaml": "npm:0.0.6" - axios: "npm:^1.0.0" + axios: "npm:^1.6.0" chalk: "npm:^4.1.0" cli-cursor: "npm:3.1.0" cli-spinners: "npm:2.6.1" @@ -24126,28 +24281,27 @@ __metadata: figures: "npm:3.2.0" flat: "npm:^5.0.2" fs-extra: "npm:^11.1.0" - glob: "npm:7.1.4" ignore: "npm:^5.0.4" jest-diff: "npm:^29.4.1" js-yaml: "npm:4.1.0" jsonc-parser: "npm:3.2.0" lines-and-columns: "npm:~2.0.3" - minimatch: "npm:3.0.5" + minimatch: "npm:9.0.3" node-machine-id: "npm:1.1.12" npm-run-path: "npm:^4.0.1" open: "npm:^8.4.0" - semver: "npm:7.5.3" + ora: "npm:5.3.0" + semver: "npm:^7.5.3" string-width: "npm:^4.2.3" strong-log-transformer: "npm:^2.1.0" tar-stream: "npm:~2.2.0" tmp: "npm:~0.2.1" tsconfig-paths: "npm:^4.1.2" tslib: "npm:^2.3.0" - v8-compile-cache: "npm:2.3.0" yargs: "npm:^17.6.2" yargs-parser: "npm:21.1.1" peerDependencies: - "@swc-node/register": ^1.6.7 + "@swc-node/register": ^1.8.0 "@swc/core": ^1.3.85 dependenciesMeta: "@nx/nx-darwin-arm64": @@ -24177,7 +24331,8 @@ __metadata: optional: true bin: nx: bin/nx.js - checksum: 10/748a28491ac607e6d3ab1878dc17b0a78a74be1a272a2775336a8110660d6c39e3e993793391b5810d2e482156421a247cce47b9c3035e4f156129a4b595dd2e + nx-cloud: bin/nx-cloud.js + checksum: 10/21017002e647b4496267867f0a5c83c78c20beb3a211257b1c852efb715867d28906af5c4d1e5ae58975cf2dde835adbaeb7148cdf29ac015c1515d723b07589 languageName: node linkType: hard @@ -24417,6 +24572,22 @@ __metadata: languageName: node linkType: hard +"ora@npm:5.3.0": + version: 5.3.0 + resolution: "ora@npm:5.3.0" + dependencies: + bl: "npm:^4.0.3" + chalk: "npm:^4.1.0" + cli-cursor: "npm:^3.1.0" + cli-spinners: "npm:^2.5.0" + is-interactive: "npm:^1.0.0" + log-symbols: "npm:^4.0.0" + strip-ansi: "npm:^6.0.0" + wcwidth: "npm:^1.0.1" + checksum: 10/989a075b596c297acfee647010e555709bd657dedd9eee9ff99d923cbc65c68b6189c2c9ea58167675b101433509f87d1674a84047c7b766babab15d9220f1d5 + languageName: node + linkType: hard + "ora@npm:^5.4.1": version: 5.4.1 resolution: "ora@npm:5.4.1" @@ -24634,31 +24805,31 @@ __metadata: languageName: node linkType: hard -"pacote@npm:^15.2.0": - version: 15.2.0 - resolution: "pacote@npm:15.2.0" +"pacote@npm:^17.0.5": + version: 17.0.6 + resolution: "pacote@npm:17.0.6" dependencies: - "@npmcli/git": "npm:^4.0.0" + "@npmcli/git": "npm:^5.0.0" "@npmcli/installed-package-contents": "npm:^2.0.1" - "@npmcli/promise-spawn": "npm:^6.0.1" - "@npmcli/run-script": "npm:^6.0.0" - cacache: "npm:^17.0.0" + "@npmcli/promise-spawn": "npm:^7.0.0" + "@npmcli/run-script": "npm:^7.0.0" + cacache: "npm:^18.0.0" fs-minipass: "npm:^3.0.0" - minipass: "npm:^5.0.0" - npm-package-arg: "npm:^10.0.0" - npm-packlist: "npm:^7.0.0" - npm-pick-manifest: "npm:^8.0.0" - npm-registry-fetch: "npm:^14.0.0" + minipass: "npm:^7.0.2" + npm-package-arg: "npm:^11.0.0" + npm-packlist: "npm:^8.0.0" + npm-pick-manifest: "npm:^9.0.0" + npm-registry-fetch: "npm:^16.0.0" proc-log: "npm:^3.0.0" promise-retry: "npm:^2.0.1" - read-package-json: "npm:^6.0.0" + read-package-json: "npm:^7.0.0" read-package-json-fast: "npm:^3.0.0" - sigstore: "npm:^1.3.0" + sigstore: "npm:^2.2.0" ssri: "npm:^10.0.0" tar: "npm:^6.1.11" bin: pacote: lib/bin.js - checksum: 10/57e18f4f963abb5f67f794158a55c01ad23f76e56dcdc74e6b843dfdda017515b0e8c0f56e60e842cd5af5ab9b351afdc49fc70633994f0e5fc0c6c9f4bcaebc + checksum: 10/fe96b362623128c67b4974bc2d0e8721515927c3546f04e9f3b0df0fe93ab74a8ed59c2896dec3ad1ed5395a8e439b3b64007b32d31b4b86796b50c75dffc924 languageName: node linkType: hard @@ -27316,6 +27487,18 @@ __metadata: languageName: node linkType: hard +"read-package-json@npm:^7.0.0": + version: 7.0.0 + resolution: "read-package-json@npm:7.0.0" + dependencies: + glob: "npm:^10.2.2" + json-parse-even-better-errors: "npm:^3.0.0" + normalize-package-data: "npm:^6.0.0" + npm-normalize-package-bin: "npm:^3.0.0" + checksum: 10/b395d5330e9096cb533553e51c6dd123284a744e65c771fbd4d868ca600d2a61b867a4f10723e360608e839101fbe805448dd0079267b3232637ec8bb62bb080 + languageName: node + linkType: hard + "read-pkg-up@npm:^3.0.0": version: 3.0.0 resolution: "read-pkg-up@npm:3.0.0" @@ -28379,17 +28562,6 @@ __metadata: languageName: node linkType: hard -"semver@npm:7.5.3": - version: 7.5.3 - resolution: "semver@npm:7.5.3" - dependencies: - lru-cache: "npm:^6.0.0" - bin: - semver: bin/semver.js - checksum: 10/80b4b3784abff33bacf200727e012dc66768ed5835441e0a802ba9f3f5dd6b10ee366294711f5e7e13d73b82a6127ea55f11f9884d35e76a6a618dc11bc16ccf - languageName: node - linkType: hard - "semver@npm:7.6.0, semver@npm:7.x, semver@npm:^7.0.0, semver@npm:^7.1.1, semver@npm:^7.3.2, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.3.8, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0": version: 7.6.0 resolution: "semver@npm:7.6.0" @@ -28615,7 +28787,7 @@ __metadata: languageName: node linkType: hard -"sigstore@npm:^1.3.0, sigstore@npm:^1.4.0": +"sigstore@npm:^1.4.0": version: 1.9.0 resolution: "sigstore@npm:1.9.0" dependencies: @@ -28630,6 +28802,20 @@ __metadata: languageName: node linkType: hard +"sigstore@npm:^2.2.0": + version: 2.2.2 + resolution: "sigstore@npm:2.2.2" + dependencies: + "@sigstore/bundle": "npm:^2.2.0" + "@sigstore/core": "npm:^1.0.0" + "@sigstore/protobuf-specs": "npm:^0.3.0" + "@sigstore/sign": "npm:^2.2.3" + "@sigstore/tuf": "npm:^2.3.1" + "@sigstore/verify": "npm:^1.1.0" + checksum: 10/e0e4fcc889b7351908aceaa19508cc49ac6d7c4ff014c113d41bf53566db3e878934a00487e9a6deb2d71a375b530af232e7be9dab11c79b89eaa61308fed92f + languageName: node + linkType: hard + "simple-git@npm:^3.6.0": version: 3.16.0 resolution: "simple-git@npm:3.16.0" @@ -28868,6 +29054,17 @@ __metadata: languageName: node linkType: hard +"socks-proxy-agent@npm:^8.0.1": + version: 8.0.2 + resolution: "socks-proxy-agent@npm:8.0.2" + dependencies: + agent-base: "npm:^7.0.2" + debug: "npm:^4.3.4" + socks: "npm:^2.7.1" + checksum: 10/ea727734bd5b2567597aa0eda14149b3b9674bb44df5937bbb9815280c1586994de734d965e61f1dd45661183d7b41f115fb9e432d631287c9063864cfcc2ecc + languageName: node + linkType: hard + "socks@npm:^2.6.1, socks@npm:^2.6.2": version: 2.6.2 resolution: "socks@npm:2.6.2" @@ -28878,6 +29075,16 @@ __metadata: languageName: node linkType: hard +"socks@npm:^2.7.1": + version: 2.8.0 + resolution: "socks@npm:2.8.0" + dependencies: + ip-address: "npm:^9.0.5" + smart-buffer: "npm:^4.2.0" + checksum: 10/ed0224ce2c7daaa7690cb87cf53d9703ffc4e983aca221f6f5b46767b232658df49494fd86acd0bf97ada6de05248ea8ea625c2343d48155d8463fc40d4a340f + languageName: node + linkType: hard + "sort-asc@npm:^0.1.0": version: 0.1.0 resolution: "sort-asc@npm:0.1.0" @@ -29111,6 +29318,13 @@ __metadata: languageName: node linkType: hard +"sprintf-js@npm:^1.1.3": + version: 1.1.3 + resolution: "sprintf-js@npm:1.1.3" + checksum: 10/e7587128c423f7e43cc625fe2f87e6affdf5ca51c1cc468e910d8aaca46bb44a7fbcfa552f787b1d3987f7043aeb4527d1b99559e6621e01b42b3f45e5a24cbb + languageName: node + linkType: hard + "sprintf-js@npm:~1.0.2": version: 1.0.3 resolution: "sprintf-js@npm:1.0.3" @@ -29174,7 +29388,7 @@ __metadata: languageName: node linkType: hard -"ssri@npm:^9.0.0, ssri@npm:^9.0.1": +"ssri@npm:^9.0.1": version: 9.0.1 resolution: "ssri@npm:9.0.1" dependencies: @@ -30650,6 +30864,17 @@ __metadata: languageName: node linkType: hard +"tuf-js@npm:^2.2.0": + version: 2.2.0 + resolution: "tuf-js@npm:2.2.0" + dependencies: + "@tufjs/models": "npm:2.0.0" + debug: "npm:^4.3.4" + make-fetch-happen: "npm:^13.0.0" + checksum: 10/a513ce533c06390b7d8767fe68250adac2535bc63c460e9ab8cbae8253da5ccd6fd204448a460536a6e77f7cf5fcf5a3b104971610f9f319a9b8f95b3b574b95 + languageName: node + linkType: hard + "tunnel-agent@npm:^0.6.0": version: 0.6.0 resolution: "tunnel-agent@npm:0.6.0" @@ -31326,13 +31551,6 @@ __metadata: languageName: node linkType: hard -"v8-compile-cache@npm:2.3.0": - version: 2.3.0 - resolution: "v8-compile-cache@npm:2.3.0" - checksum: 10/7de7423db6f48d76cffae93d70d503e160c97fc85e55945036d719111e20b33c4be5c21aa8b123a3da203bbb3bc4c8180f9667d5ccafcff11d749fae204ec7be - languageName: node - linkType: hard - "v8-to-istanbul@npm:^9.0.0, v8-to-istanbul@npm:^9.0.1": version: 9.1.0 resolution: "v8-to-istanbul@npm:9.1.0" @@ -32118,14 +32336,14 @@ __metadata: languageName: node linkType: hard -"which@npm:^3.0.0": - version: 3.0.1 - resolution: "which@npm:3.0.1" +"which@npm:^4.0.0": + version: 4.0.0 + resolution: "which@npm:4.0.0" dependencies: - isexe: "npm:^2.0.0" + isexe: "npm:^3.1.1" bin: node-which: bin/which.js - checksum: 10/adf720fe9d84be2d9190458194f814b5e9015ae4b88711b150f30d0f4d0b646544794b86f02c7ebeec1db2029bc3e83a7ff156f542d7521447e5496543e26890 + checksum: 10/f17e84c042592c21e23c8195108cff18c64050b9efb8459589116999ea9da6dd1509e6a1bac3aeebefd137be00fabbb61b5c2bc0aa0f8526f32b58ee2f545651 languageName: node linkType: hard @@ -32442,7 +32660,7 @@ __metadata: languageName: node linkType: hard -"yargs@npm:^17.3.1, yargs@npm:^17.5.1, yargs@npm:^17.6.2, yargs@npm:^17.7.2": +"yargs@npm:17.7.2, yargs@npm:^17.3.1, yargs@npm:^17.5.1, yargs@npm:^17.6.2, yargs@npm:^17.7.2": version: 17.7.2 resolution: "yargs@npm:17.7.2" dependencies: From ae77fe36021024de869398dce923383a3b128c93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Farkas?= Date: Thu, 22 Feb 2024 16:47:03 +0100 Subject: [PATCH 0101/1406] postgres: do not use unexported grafana-core config (#83241) * postgres: do not use unexported grafana-core config * fixed wrong value --- .../grafana-postgresql-datasource/postgres.go | 32 ++++++++++++------- .../postgres_snapshot_test.go | 7 +--- .../postgres_test.go | 10 ++---- 3 files changed, 24 insertions(+), 25 deletions(-) diff --git a/pkg/tsdb/grafana-postgresql-datasource/postgres.go b/pkg/tsdb/grafana-postgresql-datasource/postgres.go index 84f26ab00f..53e4630885 100644 --- a/pkg/tsdb/grafana-postgresql-datasource/postgres.go +++ b/pkg/tsdb/grafana-postgresql-datasource/postgres.go @@ -28,7 +28,7 @@ func ProvideService(cfg *setting.Cfg) *Service { tlsManager: newTLSManager(logger, cfg.DataPath), logger: logger, } - s.im = datasource.NewInstanceManager(s.newInstanceSettings(cfg)) + s.im = datasource.NewInstanceManager(s.newInstanceSettings()) return s } @@ -55,7 +55,7 @@ func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest) return dsInfo.QueryData(ctx, req) } -func newPostgres(ctx context.Context, cfg *setting.Cfg, dsInfo sqleng.DataSourceInfo, cnnstr string, logger log.Logger, settings backend.DataSourceInstanceSettings) (*sql.DB, *sqleng.DataSourceHandler, error) { +func newPostgres(ctx context.Context, userFacingDefaultError string, rowLimit int64, dsInfo sqleng.DataSourceInfo, cnnstr string, logger log.Logger, settings backend.DataSourceInstanceSettings) (*sql.DB, *sqleng.DataSourceHandler, error) { connector, err := pq.NewConnector(cnnstr) if err != nil { logger.Error("postgres connector creation failed", "error", err) @@ -82,7 +82,7 @@ func newPostgres(ctx context.Context, cfg *setting.Cfg, dsInfo sqleng.DataSource config := sqleng.DataPluginConfiguration{ DSInfo: dsInfo, MetricColumnTypes: []string{"UNKNOWN", "TEXT", "VARCHAR", "CHAR"}, - RowLimit: cfg.DataProxyRowLimit, + RowLimit: rowLimit, } queryResultTransformer := postgresQueryResultTransformer{} @@ -93,7 +93,7 @@ func newPostgres(ctx context.Context, cfg *setting.Cfg, dsInfo sqleng.DataSource db.SetMaxIdleConns(config.DSInfo.JsonData.MaxIdleConns) db.SetConnMaxLifetime(time.Duration(config.DSInfo.JsonData.ConnMaxLifetime) * time.Second) - handler, err := sqleng.NewQueryDataHandler(cfg.UserFacingDefaultError, db, config, &queryResultTransformer, newPostgresMacroEngine(dsInfo.JsonData.Timescaledb), + handler, err := sqleng.NewQueryDataHandler(userFacingDefaultError, db, config, &queryResultTransformer, newPostgresMacroEngine(dsInfo.JsonData.Timescaledb), logger) if err != nil { logger.Error("Failed connecting to Postgres", "err", err) @@ -104,20 +104,25 @@ func newPostgres(ctx context.Context, cfg *setting.Cfg, dsInfo sqleng.DataSource return db, handler, nil } -func (s *Service) newInstanceSettings(cfg *setting.Cfg) datasource.InstanceFactoryFunc { +func (s *Service) newInstanceSettings() datasource.InstanceFactoryFunc { logger := s.logger return func(ctx context.Context, settings backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { - logger.Debug("Creating Postgres query endpoint") + cfg := backend.GrafanaConfigFromContext(ctx) + sqlCfg, err := cfg.SQL() + if err != nil { + return nil, err + } + jsonData := sqleng.JsonData{ - MaxOpenConns: cfg.SqlDatasourceMaxOpenConnsDefault, - MaxIdleConns: cfg.SqlDatasourceMaxIdleConnsDefault, - ConnMaxLifetime: cfg.SqlDatasourceMaxConnLifetimeDefault, + MaxOpenConns: sqlCfg.DefaultMaxOpenConns, + MaxIdleConns: sqlCfg.DefaultMaxIdleConns, + ConnMaxLifetime: sqlCfg.DefaultMaxConnLifetimeSeconds, Timescaledb: false, ConfigurationMethod: "file-path", SecureDSProxy: false, } - err := json.Unmarshal(settings.JSONData, &jsonData) + err = json.Unmarshal(settings.JSONData, &jsonData) if err != nil { return nil, fmt.Errorf("error reading settings: %w", err) } @@ -143,7 +148,12 @@ func (s *Service) newInstanceSettings(cfg *setting.Cfg) datasource.InstanceFacto return nil, err } - _, handler, err := newPostgres(ctx, cfg, dsInfo, cnnstr, logger, settings) + userFacingDefaultError, err := cfg.UserFacingDefaultError() + if err != nil { + return nil, err + } + + _, handler, err := newPostgres(ctx, userFacingDefaultError, sqlCfg.RowLimit, dsInfo, cnnstr, logger, settings) if err != nil { logger.Error("Failed connecting to Postgres", "err", err) diff --git a/pkg/tsdb/grafana-postgresql-datasource/postgres_snapshot_test.go b/pkg/tsdb/grafana-postgresql-datasource/postgres_snapshot_test.go index e217763600..0713e8cdac 100644 --- a/pkg/tsdb/grafana-postgresql-datasource/postgres_snapshot_test.go +++ b/pkg/tsdb/grafana-postgresql-datasource/postgres_snapshot_test.go @@ -16,7 +16,6 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/experimental" "github.com/stretchr/testify/require" - "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/tsdb/sqleng" _ "github.com/lib/pq" @@ -143,10 +142,6 @@ func TestIntegrationPostgresSnapshots(t *testing.T) { return sql } - cfg := setting.NewCfg() - cfg.DataPath = t.TempDir() - cfg.DataProxyRowLimit = 10000 - jsonData := sqleng.JsonData{ MaxOpenConns: 0, MaxIdleConns: 2, @@ -164,7 +159,7 @@ func TestIntegrationPostgresSnapshots(t *testing.T) { cnnstr := getCnnStr() - db, handler, err := newPostgres(context.Background(), cfg, dsInfo, cnnstr, logger, backend.DataSourceInstanceSettings{}) + db, handler, err := newPostgres(context.Background(), "error", 10000, dsInfo, cnnstr, logger, backend.DataSourceInstanceSettings{}) t.Cleanup((func() { _, err := db.Exec("DROP TABLE tbl") diff --git a/pkg/tsdb/grafana-postgresql-datasource/postgres_test.go b/pkg/tsdb/grafana-postgresql-datasource/postgres_test.go index f41f7f3c8a..2dec40dc83 100644 --- a/pkg/tsdb/grafana-postgresql-datasource/postgres_test.go +++ b/pkg/tsdb/grafana-postgresql-datasource/postgres_test.go @@ -191,10 +191,6 @@ func TestIntegrationPostgres(t *testing.T) { return sql } - cfg := setting.NewCfg() - cfg.DataPath = t.TempDir() - cfg.DataProxyRowLimit = 10000 - jsonData := sqleng.JsonData{ MaxOpenConns: 0, MaxIdleConns: 2, @@ -212,7 +208,7 @@ func TestIntegrationPostgres(t *testing.T) { cnnstr := postgresTestDBConnString() - db, exe, err := newPostgres(context.Background(), cfg, dsInfo, cnnstr, logger, backend.DataSourceInstanceSettings{}) + db, exe, err := newPostgres(context.Background(), "error", 10000, dsInfo, cnnstr, logger, backend.DataSourceInstanceSettings{}) require.NoError(t, err) @@ -1266,9 +1262,7 @@ func TestIntegrationPostgres(t *testing.T) { t.Run("When row limit set to 1", func(t *testing.T) { dsInfo := sqleng.DataSourceInfo{} - conf := setting.NewCfg() - conf.DataProxyRowLimit = 1 - _, handler, err := newPostgres(context.Background(), conf, dsInfo, cnnstr, logger, backend.DataSourceInstanceSettings{}) + _, handler, err := newPostgres(context.Background(), "error", 1, dsInfo, cnnstr, logger, backend.DataSourceInstanceSettings{}) require.NoError(t, err) From 0b62c15e4b894c5f91ae0678e73222232e490f7f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 22 Feb 2024 15:48:37 +0000 Subject: [PATCH 0102/1406] Update dependency rc-drawer to v7 (#83162) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- packages/grafana-ui/package.json | 2 +- yarn.lock | 738 ++----------------------------- 3 files changed, 48 insertions(+), 694 deletions(-) diff --git a/package.json b/package.json index d6c7c8d04c..03525b6afa 100644 --- a/package.json +++ b/package.json @@ -354,7 +354,7 @@ "prop-types": "15.8.1", "pseudoizer": "^0.1.0", "rc-cascader": "3.21.2", - "rc-drawer": "6.5.2", + "rc-drawer": "7.1.0", "rc-slider": "10.5.0", "rc-time-picker": "3.7.3", "rc-tree": "5.8.5", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 352785ae36..a10d7c5ff2 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -79,7 +79,7 @@ "ol": "7.4.0", "prismjs": "1.29.0", "rc-cascader": "3.21.2", - "rc-drawer": "6.5.2", + "rc-drawer": "7.1.0", "rc-slider": "10.5.0", "rc-time-picker": "^3.7.3", "rc-tooltip": "6.1.3", diff --git a/yarn.lock b/yarn.lock index e4a7b4cf5a..10a54e0147 100644 --- a/yarn.lock +++ b/yarn.lock @@ -399,15 +399,6 @@ __metadata: languageName: node linkType: hard -"@babel/parser@npm:^7.22.15": - version: 7.23.6 - resolution: "@babel/parser@npm:7.23.6" - bin: - parser: ./bin/babel-parser.js - checksum: 10/6be3a63d3c9d07b035b5a79c022327cb7e16cbd530140ecb731f19a650c794c315a72c699a22413ebeafaff14aa8f53435111898d59e01a393d741b85629fa7d - languageName: node - linkType: hard - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:^7.22.15, @babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:^7.23.3": version: 7.23.3 resolution: "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:7.23.3" @@ -1679,7 +1670,7 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:7.23.9, @babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.11.1, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.14.0, @babel/runtime@npm:^7.14.5, @babel/runtime@npm:^7.15.4, @babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.18.0, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.20.0, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.7.2, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2": +"@babel/runtime@npm:7.23.9, @babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.11.1, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.14.0, @babel/runtime@npm:^7.14.5, @babel/runtime@npm:^7.15.4, @babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.18.0, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.20.0, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.7.2, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2": version: 7.23.9 resolution: "@babel/runtime@npm:7.23.9" dependencies: @@ -1688,18 +1679,7 @@ __metadata: languageName: node linkType: hard -"@babel/template@npm:^7.22.15, @babel/template@npm:^7.3.3": - version: 7.22.15 - resolution: "@babel/template@npm:7.22.15" - dependencies: - "@babel/code-frame": "npm:^7.22.13" - "@babel/parser": "npm:^7.22.15" - "@babel/types": "npm:^7.22.15" - checksum: 10/21e768e4eed4d1da2ce5d30aa51db0f4d6d8700bc1821fec6292587df7bba2fe1a96451230de8c64b989740731888ebf1141138bfffb14cacccf4d05c66ad93f - languageName: node - linkType: hard - -"@babel/template@npm:^7.22.5, @babel/template@npm:^7.23.9": +"@babel/template@npm:^7.22.15, @babel/template@npm:^7.22.5, @babel/template@npm:^7.23.9, @babel/template@npm:^7.3.3": version: 7.23.9 resolution: "@babel/template@npm:7.23.9" dependencies: @@ -1728,18 +1708,7 @@ __metadata: languageName: node linkType: hard -"@babel/types@npm:^7.0.0, @babel/types@npm:^7.2.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.22.15, @babel/types@npm:^7.22.19, @babel/types@npm:^7.22.5, @babel/types@npm:^7.23.0, @babel/types@npm:^7.23.6, @babel/types@npm:^7.3.0, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3": - version: 7.23.6 - resolution: "@babel/types@npm:7.23.6" - dependencies: - "@babel/helper-string-parser": "npm:^7.23.4" - "@babel/helper-validator-identifier": "npm:^7.22.20" - to-fast-properties: "npm:^2.0.0" - checksum: 10/07e70bb94d30b0231396b5e9a7726e6d9227a0a62e0a6830c0bd3232f33b024092e3d5a7d1b096a65bbf2bb43a9ab4c721bf618e115bfbb87b454fa060f88cbf - languageName: node - linkType: hard - -"@babel/types@npm:^7.23.9": +"@babel/types@npm:^7.0.0, @babel/types@npm:^7.2.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.22.15, @babel/types@npm:^7.22.19, @babel/types@npm:^7.22.5, @babel/types@npm:^7.23.0, @babel/types@npm:^7.23.6, @babel/types@npm:^7.23.9, @babel/types@npm:^7.3.0, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3": version: 7.23.9 resolution: "@babel/types@npm:7.23.9" dependencies: @@ -3790,17 +3759,7 @@ __metadata: languageName: node linkType: hard -"@grafana/faro-core@npm:^1.3.7": - version: 1.3.7 - resolution: "@grafana/faro-core@npm:1.3.7" - dependencies: - "@opentelemetry/api": "npm:^1.7.0" - "@opentelemetry/otlp-transformer": "npm:^0.45.1" - checksum: 10/fa3ff8dce1e6fe5ad91a4d42bb9bdb13f36d594074566a100645ffb4bc509265d18c78c5cda1ef8f39a3f043c1901baee620d3e044b3a0a6e9d1c516bf71f74f - languageName: node - linkType: hard - -"@grafana/faro-web-sdk@npm:1.3.8": +"@grafana/faro-web-sdk@npm:1.3.8, @grafana/faro-web-sdk@npm:^1.3.6": version: 1.3.8 resolution: "@grafana/faro-web-sdk@npm:1.3.8" dependencies: @@ -3811,17 +3770,6 @@ __metadata: languageName: node linkType: hard -"@grafana/faro-web-sdk@npm:^1.3.6": - version: 1.3.7 - resolution: "@grafana/faro-web-sdk@npm:1.3.7" - dependencies: - "@grafana/faro-core": "npm:^1.3.7" - ua-parser-js: "npm:^1.0.32" - web-vitals: "npm:^3.1.1" - checksum: 10/5dc5f38a4bcd31f5ee9ece7e2dd4253020ecc872b23c8294e2b84639d8f82c8a895b15743f24092c5b6ea99438e98b1c396ab9368df4ffc472109e005c7451d6 - languageName: node - linkType: hard - "@grafana/flamegraph@workspace:*, @grafana/flamegraph@workspace:packages/grafana-flamegraph": version: 0.0.0-use.local resolution: "@grafana/flamegraph@workspace:packages/grafana-flamegraph" @@ -4301,7 +4249,7 @@ __metadata: prismjs: "npm:1.29.0" process: "npm:^0.11.10" rc-cascader: "npm:3.21.2" - rc-drawer: "npm:6.5.2" + rc-drawer: "npm:7.1.0" rc-slider: "npm:10.5.0" rc-time-picker: "npm:^3.7.3" rc-tooltip: "npm:6.1.3" @@ -4768,13 +4716,6 @@ __metadata: languageName: node linkType: hard -"@jridgewell/resolve-uri@npm:3.1.0": - version: 3.1.0 - resolution: "@jridgewell/resolve-uri@npm:3.1.0" - checksum: 10/320ceb37af56953757b28e5b90c34556157676d41e3d0a3ff88769274d62373582bb0f0276a4f2d29c3f4fdd55b82b8be5731f52d391ad2ecae9b321ee1c742d - languageName: node - linkType: hard - "@jridgewell/resolve-uri@npm:^3.0.3, @jridgewell/resolve-uri@npm:^3.1.0": version: 3.1.1 resolution: "@jridgewell/resolve-uri@npm:3.1.1" @@ -4789,16 +4730,6 @@ __metadata: 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": "npm:^0.3.0" - "@jridgewell/trace-mapping": "npm:^0.3.9" - checksum: 10/1aaa42075bac32a551708025da0c07b11c11fb05ccd10fb70df2cb0db88773338ab0f33f175d9865379cb855bb3b1cda478367747a1087309fda40a7b9214bfa - languageName: node - linkType: hard - "@jridgewell/source-map@npm:^0.3.3": version: 0.3.5 resolution: "@jridgewell/source-map@npm:0.3.5" @@ -4809,13 +4740,6 @@ __metadata: languageName: node linkType: hard -"@jridgewell/sourcemap-codec@npm:1.4.14": - version: 1.4.14 - resolution: "@jridgewell/sourcemap-codec@npm:1.4.14" - checksum: 10/26e768fae6045481a983e48aa23d8fcd23af5da70ebd74b0649000e815e7fbb01ea2bc088c9176b3fffeb9bec02184e58f46125ef3320b30eaa1f4094cfefa38 - languageName: node - linkType: hard - "@jridgewell/sourcemap-codec@npm:^1.4.10, @jridgewell/sourcemap-codec@npm:^1.4.14, @jridgewell/sourcemap-codec@npm:^1.4.15": version: 1.4.15 resolution: "@jridgewell/sourcemap-codec@npm:1.4.15" @@ -4833,27 +4757,7 @@ __metadata: languageName: node linkType: hard -"@jridgewell/trace-mapping@npm:^0.3.12, @jridgewell/trace-mapping@npm:^0.3.17, @jridgewell/trace-mapping@npm:^0.3.18, @jridgewell/trace-mapping@npm:^0.3.9": - version: 0.3.18 - resolution: "@jridgewell/trace-mapping@npm:0.3.18" - dependencies: - "@jridgewell/resolve-uri": "npm:3.1.0" - "@jridgewell/sourcemap-codec": "npm:1.4.14" - checksum: 10/f4fabdddf82398a797bcdbb51c574cd69b383db041a6cae1a6a91478681d6aab340c01af655cfd8c6e01cde97f63436a1445f08297cdd33587621cf05ffa0d55 - languageName: node - linkType: hard - -"@jridgewell/trace-mapping@npm:^0.3.20": - version: 0.3.21 - resolution: "@jridgewell/trace-mapping@npm:0.3.21" - dependencies: - "@jridgewell/resolve-uri": "npm:^3.1.0" - "@jridgewell/sourcemap-codec": "npm:^1.4.14" - checksum: 10/925dda0620887e5a24f11b5a3a106f4e8b1a66155b49be6ceee61432174df33a17c243d8a89b2cd79ccebd281d817878759236a2fc42c47325ae9f73dfbfb90d - languageName: node - linkType: hard - -"@jridgewell/trace-mapping@npm:^0.3.21": +"@jridgewell/trace-mapping@npm:^0.3.12, @jridgewell/trace-mapping@npm:^0.3.17, @jridgewell/trace-mapping@npm:^0.3.18, @jridgewell/trace-mapping@npm:^0.3.20, @jridgewell/trace-mapping@npm:^0.3.21, @jridgewell/trace-mapping@npm:^0.3.9": version: 0.3.22 resolution: "@jridgewell/trace-mapping@npm:0.3.22" dependencies: @@ -5829,15 +5733,6 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/api-logs@npm:0.45.1": - version: 0.45.1 - resolution: "@opentelemetry/api-logs@npm:0.45.1" - dependencies: - "@opentelemetry/api": "npm:^1.0.0" - checksum: 10/0f78a131d640a09f2a4c837014378f6b5f6db1e32d90a70a7f4c5191dc2f767330887fc16126d7ae788b122e828e4f3b1aec09be284f633a151d6a319e03e2a4 - languageName: node - linkType: hard - "@opentelemetry/api-logs@npm:0.48.0": version: 0.48.0 resolution: "@opentelemetry/api-logs@npm:0.48.0" @@ -5875,17 +5770,6 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/core@npm:1.18.1": - version: 1.18.1 - resolution: "@opentelemetry/core@npm:1.18.1" - dependencies: - "@opentelemetry/semantic-conventions": "npm:1.18.1" - peerDependencies: - "@opentelemetry/api": ">=1.0.0 <1.8.0" - checksum: 10/b8c08c40d07d8b2afefc3c97ea83d8e8dc2e5e5a139007ba7fc4cc25fc38b6fe0d1380d4bdaf390381f114dbfbed5b3c45a395972cf25a1a174c8e5b0bd830fb - languageName: node - linkType: hard - "@opentelemetry/core@npm:1.21.0": version: 1.21.0 resolution: "@opentelemetry/core@npm:1.21.0" @@ -5912,22 +5796,6 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/otlp-transformer@npm:^0.45.1": - version: 0.45.1 - resolution: "@opentelemetry/otlp-transformer@npm:0.45.1" - dependencies: - "@opentelemetry/api-logs": "npm:0.45.1" - "@opentelemetry/core": "npm:1.18.1" - "@opentelemetry/resources": "npm:1.18.1" - "@opentelemetry/sdk-logs": "npm:0.45.1" - "@opentelemetry/sdk-metrics": "npm:1.18.1" - "@opentelemetry/sdk-trace-base": "npm:1.18.1" - peerDependencies: - "@opentelemetry/api": ">=1.3.0 <1.8.0" - checksum: 10/fadc67d1f4ff613d6b737a4400a286afe34a460f47374b16b34d9344d4ff89ce308ba79e22774c2dd8e00cb5fc76d034cb9369fe09e0e4af6ba49588a5647816 - languageName: node - linkType: hard - "@opentelemetry/otlp-transformer@npm:^0.48.0": version: 0.48.0 resolution: "@opentelemetry/otlp-transformer@npm:0.48.0" @@ -5956,18 +5824,6 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/resources@npm:1.18.1": - version: 1.18.1 - resolution: "@opentelemetry/resources@npm:1.18.1" - dependencies: - "@opentelemetry/core": "npm:1.18.1" - "@opentelemetry/semantic-conventions": "npm:1.18.1" - peerDependencies: - "@opentelemetry/api": ">=1.0.0 <1.8.0" - checksum: 10/f7d168a82c2fc602364a54977f41ce9f873b5156d5e36bf0f078b289f6bb1c41eaae700bcdddb7f32d15cb7e937d81239eb0301c7d0aa9b2a6c85a4cb0ff5ded - languageName: node - linkType: hard - "@opentelemetry/resources@npm:1.21.0": version: 1.21.0 resolution: "@opentelemetry/resources@npm:1.21.0" @@ -5980,19 +5836,6 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/sdk-logs@npm:0.45.1": - version: 0.45.1 - resolution: "@opentelemetry/sdk-logs@npm:0.45.1" - dependencies: - "@opentelemetry/core": "npm:1.18.1" - "@opentelemetry/resources": "npm:1.18.1" - peerDependencies: - "@opentelemetry/api": ">=1.4.0 <1.8.0" - "@opentelemetry/api-logs": ">=0.39.1" - checksum: 10/47cc1aa1d867bf6b0fe5120fa5e7839620a5843a93b1725ae7f35bdf10f6998fe89376c9524eff438f92b74733a751e1c6e7c0e90ec13aa6a1bfa8ca28d1f2e4 - languageName: node - linkType: hard - "@opentelemetry/sdk-logs@npm:0.48.0": version: 0.48.0 resolution: "@opentelemetry/sdk-logs@npm:0.48.0" @@ -6020,19 +5863,6 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/sdk-metrics@npm:1.18.1": - version: 1.18.1 - resolution: "@opentelemetry/sdk-metrics@npm:1.18.1" - dependencies: - "@opentelemetry/core": "npm:1.18.1" - "@opentelemetry/resources": "npm:1.18.1" - lodash.merge: "npm:^4.6.2" - peerDependencies: - "@opentelemetry/api": ">=1.3.0 <1.8.0" - checksum: 10/fe728c7383b5c7e7647bf7ea9881c41f4e11f48a57cf9e81efeda5eaaf784b092e1df3684e1101b34410a9401ba36e63e81bed1da5da048da5ad91acbaf51606 - languageName: node - linkType: hard - "@opentelemetry/sdk-metrics@npm:1.21.0": version: 1.21.0 resolution: "@opentelemetry/sdk-metrics@npm:1.21.0" @@ -6060,19 +5890,6 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/sdk-trace-base@npm:1.18.1": - version: 1.18.1 - resolution: "@opentelemetry/sdk-trace-base@npm:1.18.1" - dependencies: - "@opentelemetry/core": "npm:1.18.1" - "@opentelemetry/resources": "npm:1.18.1" - "@opentelemetry/semantic-conventions": "npm:1.18.1" - peerDependencies: - "@opentelemetry/api": ">=1.0.0 <1.8.0" - checksum: 10/99e576f538a06feff11e1a7c63224864ef4875e36bd0b7284087307f6dd87554aa2089f0a51f8a4cdc55c44f298befedf94b40987af9a0875e931bd3fe2e77c5 - languageName: node - linkType: hard - "@opentelemetry/sdk-trace-base@npm:1.21.0": version: 1.21.0 resolution: "@opentelemetry/sdk-trace-base@npm:1.21.0" @@ -6093,13 +5910,6 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/semantic-conventions@npm:1.18.1": - version: 1.18.1 - resolution: "@opentelemetry/semantic-conventions@npm:1.18.1" - checksum: 10/00d46e3b61eeac8a6752d50a0fb55ddf32f6f716a7fe4bf35b6d001da89398b4d8a5623a17044b24ab159acb892b2ac2586731b375176b94806cb0013f629dd5 - languageName: node - linkType: hard - "@opentelemetry/semantic-conventions@npm:1.21.0": version: 1.21.0 resolution: "@opentelemetry/semantic-conventions@npm:1.21.0" @@ -7170,20 +6980,13 @@ __metadata: languageName: node linkType: hard -"@remix-run/router@npm:1.14.2": +"@remix-run/router@npm:1.14.2, @remix-run/router@npm:^1.5.0": version: 1.14.2 resolution: "@remix-run/router@npm:1.14.2" checksum: 10/422844e88b985f1e287301b302c6cf8169c9eea792f80d40464f97b25393bb2e697228ebd7a7b61444d5a51c5873c4a637aad20acde5886a5caf62e833c5ceee languageName: node linkType: hard -"@remix-run/router@npm:^1.5.0": - version: 1.11.0 - resolution: "@remix-run/router@npm:1.11.0" - checksum: 10/629ec578b9dfd3c5cb5de64a0798dd7846ec5ba0351aa66f42b1c65efb43da8f30366be59b825303648965b0df55b638c110949b24ef94fd62e98117fdfb0c0f - languageName: node - linkType: hard - "@rollup/plugin-commonjs@npm:25.0.7": version: 25.0.7 resolution: "@rollup/plugin-commonjs@npm:25.0.7" @@ -8547,13 +8350,6 @@ __metadata: languageName: node linkType: hard -"@swc/core-darwin-arm64@npm:1.3.90": - version: 1.3.90 - resolution: "@swc/core-darwin-arm64@npm:1.3.90" - conditions: os=darwin & cpu=arm64 - languageName: node - linkType: hard - "@swc/core-darwin-arm64@npm:1.4.0": version: 1.4.0 resolution: "@swc/core-darwin-arm64@npm:1.4.0" @@ -8568,13 +8364,6 @@ __metadata: languageName: node linkType: hard -"@swc/core-darwin-x64@npm:1.3.90": - version: 1.3.90 - resolution: "@swc/core-darwin-x64@npm:1.3.90" - conditions: os=darwin & cpu=x64 - languageName: node - linkType: hard - "@swc/core-darwin-x64@npm:1.4.0": version: 1.4.0 resolution: "@swc/core-darwin-x64@npm:1.4.0" @@ -8589,13 +8378,6 @@ __metadata: languageName: node linkType: hard -"@swc/core-linux-arm-gnueabihf@npm:1.3.90": - version: 1.3.90 - resolution: "@swc/core-linux-arm-gnueabihf@npm:1.3.90" - conditions: os=linux & cpu=arm - languageName: node - linkType: hard - "@swc/core-linux-arm-gnueabihf@npm:1.4.0": version: 1.4.0 resolution: "@swc/core-linux-arm-gnueabihf@npm:1.4.0" @@ -8610,13 +8392,6 @@ __metadata: languageName: node linkType: hard -"@swc/core-linux-arm64-gnu@npm:1.3.90": - version: 1.3.90 - resolution: "@swc/core-linux-arm64-gnu@npm:1.3.90" - conditions: os=linux & cpu=arm64 & libc=glibc - languageName: node - linkType: hard - "@swc/core-linux-arm64-gnu@npm:1.4.0": version: 1.4.0 resolution: "@swc/core-linux-arm64-gnu@npm:1.4.0" @@ -8631,13 +8406,6 @@ __metadata: languageName: node linkType: hard -"@swc/core-linux-arm64-musl@npm:1.3.90": - version: 1.3.90 - resolution: "@swc/core-linux-arm64-musl@npm:1.3.90" - conditions: os=linux & cpu=arm64 & libc=musl - languageName: node - linkType: hard - "@swc/core-linux-arm64-musl@npm:1.4.0": version: 1.4.0 resolution: "@swc/core-linux-arm64-musl@npm:1.4.0" @@ -8652,13 +8420,6 @@ __metadata: languageName: node linkType: hard -"@swc/core-linux-x64-gnu@npm:1.3.90": - version: 1.3.90 - resolution: "@swc/core-linux-x64-gnu@npm:1.3.90" - conditions: os=linux & cpu=x64 & libc=glibc - languageName: node - linkType: hard - "@swc/core-linux-x64-gnu@npm:1.4.0": version: 1.4.0 resolution: "@swc/core-linux-x64-gnu@npm:1.4.0" @@ -8673,13 +8434,6 @@ __metadata: languageName: node linkType: hard -"@swc/core-linux-x64-musl@npm:1.3.90": - version: 1.3.90 - resolution: "@swc/core-linux-x64-musl@npm:1.3.90" - conditions: os=linux & cpu=x64 & libc=musl - languageName: node - linkType: hard - "@swc/core-linux-x64-musl@npm:1.4.0": version: 1.4.0 resolution: "@swc/core-linux-x64-musl@npm:1.4.0" @@ -8694,13 +8448,6 @@ __metadata: languageName: node linkType: hard -"@swc/core-win32-arm64-msvc@npm:1.3.90": - version: 1.3.90 - resolution: "@swc/core-win32-arm64-msvc@npm:1.3.90" - conditions: os=win32 & cpu=arm64 - languageName: node - linkType: hard - "@swc/core-win32-arm64-msvc@npm:1.4.0": version: 1.4.0 resolution: "@swc/core-win32-arm64-msvc@npm:1.4.0" @@ -8715,13 +8462,6 @@ __metadata: languageName: node linkType: hard -"@swc/core-win32-ia32-msvc@npm:1.3.90": - version: 1.3.90 - resolution: "@swc/core-win32-ia32-msvc@npm:1.3.90" - conditions: os=win32 & cpu=ia32 - languageName: node - linkType: hard - "@swc/core-win32-ia32-msvc@npm:1.4.0": version: 1.4.0 resolution: "@swc/core-win32-ia32-msvc@npm:1.4.0" @@ -8736,13 +8476,6 @@ __metadata: languageName: node linkType: hard -"@swc/core-win32-x64-msvc@npm:1.3.90": - version: 1.3.90 - resolution: "@swc/core-win32-x64-msvc@npm:1.3.90" - conditions: os=win32 & cpu=x64 - languageName: node - linkType: hard - "@swc/core-win32-x64-msvc@npm:1.4.0": version: 1.4.0 resolution: "@swc/core-win32-x64-msvc@npm:1.4.0" @@ -8803,7 +8536,7 @@ __metadata: languageName: node linkType: hard -"@swc/core@npm:1.4.1": +"@swc/core@npm:1.4.1, @swc/core@npm:^1.3.49": version: 1.4.1 resolution: "@swc/core@npm:1.4.1" dependencies: @@ -8849,52 +8582,6 @@ __metadata: languageName: node linkType: hard -"@swc/core@npm:^1.3.49": - version: 1.3.90 - resolution: "@swc/core@npm:1.3.90" - dependencies: - "@swc/core-darwin-arm64": "npm:1.3.90" - "@swc/core-darwin-x64": "npm:1.3.90" - "@swc/core-linux-arm-gnueabihf": "npm:1.3.90" - "@swc/core-linux-arm64-gnu": "npm:1.3.90" - "@swc/core-linux-arm64-musl": "npm:1.3.90" - "@swc/core-linux-x64-gnu": "npm:1.3.90" - "@swc/core-linux-x64-musl": "npm:1.3.90" - "@swc/core-win32-arm64-msvc": "npm:1.3.90" - "@swc/core-win32-ia32-msvc": "npm:1.3.90" - "@swc/core-win32-x64-msvc": "npm:1.3.90" - "@swc/counter": "npm:^0.1.1" - "@swc/types": "npm:^0.1.5" - peerDependencies: - "@swc/helpers": ^0.5.0 - dependenciesMeta: - "@swc/core-darwin-arm64": - optional: true - "@swc/core-darwin-x64": - optional: true - "@swc/core-linux-arm-gnueabihf": - optional: true - "@swc/core-linux-arm64-gnu": - optional: true - "@swc/core-linux-arm64-musl": - optional: true - "@swc/core-linux-x64-gnu": - optional: true - "@swc/core-linux-x64-musl": - optional: true - "@swc/core-win32-arm64-msvc": - optional: true - "@swc/core-win32-ia32-msvc": - optional: true - "@swc/core-win32-x64-msvc": - optional: true - peerDependenciesMeta: - "@swc/helpers": - optional: true - checksum: 10/214af37af77b968203d495745a86db985734527f4696243bda5fb9ce868830d70e7a2cdbb268da2ee994d9fcedded25073d7b709fa09b75e96f9ba7d13a63da0 - languageName: node - linkType: hard - "@swc/counter@npm:^0.1.1, @swc/counter@npm:^0.1.2, @swc/counter@npm:^0.1.3": version: 0.1.3 resolution: "@swc/counter@npm:0.1.3" @@ -8902,7 +8589,7 @@ __metadata: languageName: node linkType: hard -"@swc/helpers@npm:0.5.6": +"@swc/helpers@npm:0.5.6, @swc/helpers@npm:^0.5.0": version: 0.5.6 resolution: "@swc/helpers@npm:0.5.6" dependencies: @@ -8911,15 +8598,6 @@ __metadata: languageName: node linkType: hard -"@swc/helpers@npm:^0.5.0": - version: 0.5.1 - resolution: "@swc/helpers@npm:0.5.1" - dependencies: - tslib: "npm:^2.4.0" - checksum: 10/4954c4d2dd97bf965e863a10ffa44c3fdaf7653f2fa9ef1a6cf7ffffd67f3f832216588f9751afd75fdeaea60c4688c75c01e2405119c448f1a109c9a7958c54 - languageName: node - linkType: hard - "@swc/types@npm:^0.1.5": version: 0.1.5 resolution: "@swc/types@npm:0.1.5" @@ -8943,7 +8621,7 @@ __metadata: languageName: node linkType: hard -"@testing-library/jest-dom@npm:6.4.2": +"@testing-library/jest-dom@npm:6.4.2, @testing-library/jest-dom@npm:^6.1.2": version: 6.4.2 resolution: "@testing-library/jest-dom@npm:6.4.2" dependencies: @@ -8976,39 +8654,6 @@ __metadata: languageName: node linkType: hard -"@testing-library/jest-dom@npm:^6.1.2": - version: 6.2.1 - resolution: "@testing-library/jest-dom@npm:6.2.1" - dependencies: - "@adobe/css-tools": "npm:^4.3.2" - "@babel/runtime": "npm:^7.9.2" - aria-query: "npm:^5.0.0" - chalk: "npm:^3.0.0" - css.escape: "npm:^1.5.1" - dom-accessibility-api: "npm:^0.6.3" - lodash: "npm:^4.17.15" - redent: "npm:^3.0.0" - peerDependencies: - "@jest/globals": ">= 28" - "@types/bun": "*" - "@types/jest": ">= 28" - jest: ">= 28" - vitest: ">= 0.32" - peerDependenciesMeta: - "@jest/globals": - optional: true - "@types/bun": - optional: true - "@types/jest": - optional: true - jest: - optional: true - vitest: - optional: true - checksum: 10/e522314c4623d2030146570ed5907c71126477272760fa6bce7a2db4ee5dd1edf492ebc9a5f442e99f69d4d6e2a9d0aac49fae41323211dbcee186a1eca04707 - languageName: node - linkType: hard - "@testing-library/react-hooks@npm:^8.0.1": version: 8.0.1 resolution: "@testing-library/react-hooks@npm:8.0.1" @@ -9656,17 +9301,7 @@ __metadata: languageName: node linkType: hard -"@types/eslint-scope@npm:^3.7.3": - version: 3.7.3 - resolution: "@types/eslint-scope@npm:3.7.3" - dependencies: - "@types/eslint": "npm:*" - "@types/estree": "npm:*" - checksum: 10/6772b05e1b92003d1f295e81bc847a61f4fbe8ddab77ffa49e84ed3f9552513bdde677eb53ef167753901282857dd1d604d9f82eddb34a233495932b2dc3dc17 - languageName: node - linkType: hard - -"@types/eslint-scope@npm:^3.7.7": +"@types/eslint-scope@npm:^3.7.3, @types/eslint-scope@npm:^3.7.7": version: 3.7.7 resolution: "@types/eslint-scope@npm:3.7.7" dependencies: @@ -10096,12 +9731,12 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:*, @types/node@npm:>=13.7.0": - version: 20.11.17 - resolution: "@types/node@npm:20.11.17" +"@types/node@npm:*, @types/node@npm:20.11.19, @types/node@npm:>=13.7.0, @types/node@npm:^20.11.16": + version: 20.11.19 + resolution: "@types/node@npm:20.11.19" dependencies: undici-types: "npm:~5.26.4" - checksum: 10/3342df87258d1c56154bcd4b85180f48675427b235971e6e6e2e037353f5a2ae9aaa05ba5df0fe1e2d2f1022c8d856fd39056b9d7f50ea30c0ca3214137cae1d + checksum: 10/c7f4705d6c84aa21679ad180c33c13ca9567f650e66e14bcee77c7c43d14619c7cd3b4d7b2458947143030b7b1930180efa6d12d999b45366abff9fed7a17472 languageName: node linkType: hard @@ -10112,15 +9747,6 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:20.11.19": - version: 20.11.19 - resolution: "@types/node@npm:20.11.19" - dependencies: - undici-types: "npm:~5.26.4" - checksum: 10/c7f4705d6c84aa21679ad180c33c13ca9567f650e66e14bcee77c7c43d14619c7cd3b4d7b2458947143030b7b1930180efa6d12d999b45366abff9fed7a17472 - languageName: node - linkType: hard - "@types/node@npm:^14.14.31": version: 14.18.36 resolution: "@types/node@npm:14.18.36" @@ -10135,15 +9761,6 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:^20.11.16": - version: 20.11.18 - resolution: "@types/node@npm:20.11.18" - dependencies: - undici-types: "npm:~5.26.4" - checksum: 10/eeaa55032e6702867e96d7b6f98df1d60af09d37ab72f2b905b349ec7e458dfb9c4d9cfc562962f5a51b156a968eea773d8025688f88b735944c81e3ac0e3b7f - languageName: node - linkType: hard - "@types/normalize-package-data@npm:^2.4.0": version: 2.4.1 resolution: "@types/normalize-package-data@npm:2.4.1" @@ -10260,16 +9877,7 @@ __metadata: languageName: node linkType: hard -"@types/react-dom@npm:*, @types/react-dom@npm:^18.0.0": - version: 18.2.7 - resolution: "@types/react-dom@npm:18.2.7" - dependencies: - "@types/react": "npm:*" - checksum: 10/9b70ef66cbe2d2898ea37eb79ee3697e0e4ad3d950e769a601f79be94097d43b8ef45b98a0b29528203c7d731c81666f637b2b7032deeced99214b4bc0662614 - languageName: node - linkType: hard - -"@types/react-dom@npm:18.2.19": +"@types/react-dom@npm:*, @types/react-dom@npm:18.2.19, @types/react-dom@npm:^18.0.0": version: 18.2.19 resolution: "@types/react-dom@npm:18.2.19" dependencies: @@ -10454,20 +10062,13 @@ __metadata: languageName: node linkType: hard -"@types/semver@npm:7.5.7": +"@types/semver@npm:7.5.7, @types/semver@npm:^7.3.12, @types/semver@npm:^7.3.4, @types/semver@npm:^7.5.0": version: 7.5.7 resolution: "@types/semver@npm:7.5.7" checksum: 10/535d88ec577fe59e38211881f79a1e2ba391e9e1516f8fff74e7196a5ba54315bace9c67a4616c334c830c89027d70a9f473a4ceb634526086a9da39180f2f9a languageName: node linkType: hard -"@types/semver@npm:^7.3.12, @types/semver@npm:^7.3.4, @types/semver@npm:^7.5.0": - version: 7.5.6 - resolution: "@types/semver@npm:7.5.6" - checksum: 10/e77282b17f74354e17e771c0035cccb54b94cc53d0433fa7e9ba9d23fd5d7edcd14b6c8b7327d58bbd89e83b1c5eda71dfe408e06b929007e2b89586e9b63459 - languageName: node - linkType: hard - "@types/serve-index@npm:^1.9.1": version: 1.9.1 resolution: "@types/serve-index@npm:1.9.1" @@ -11723,15 +11324,6 @@ __metadata: languageName: node linkType: hard -"acorn@npm:^8.5.0": - version: 8.10.0 - resolution: "acorn@npm:8.10.0" - bin: - acorn: bin/acorn - checksum: 10/522310c20fdc3c271caed3caf0f06c51d61cb42267279566edd1d58e83dbc12eebdafaab666a0f0be1b7ad04af9c6bc2a6f478690a9e6391c3c8b165ada917dd - languageName: node - linkType: hard - "add-dom-event-listener@npm:^1.1.0": version: 1.1.0 resolution: "add-dom-event-listener@npm:1.1.0" @@ -11979,17 +11571,7 @@ __metadata: languageName: node linkType: hard -"anymatch@npm:^3.0.3, anymatch@npm:~3.1.2": - version: 3.1.2 - resolution: "anymatch@npm:3.1.2" - dependencies: - normalize-path: "npm:^3.0.0" - picomatch: "npm:^2.0.4" - checksum: 10/985163db2292fac9e5a1e072bf99f1b5baccf196e4de25a0b0b81865ebddeb3b3eb4480734ef0a2ac8c002845396b91aa89121f5b84f93981a4658164a9ec6e9 - languageName: node - linkType: hard - -"anymatch@npm:^3.1.3": +"anymatch@npm:^3.0.3, anymatch@npm:^3.1.3, anymatch@npm:~3.1.2": version: 3.1.3 resolution: "anymatch@npm:3.1.3" dependencies: @@ -12435,20 +12017,13 @@ __metadata: languageName: node linkType: hard -"axe-core@npm:=4.7.0": +"axe-core@npm:=4.7.0, axe-core@npm:^4.2.0": version: 4.7.0 resolution: "axe-core@npm:4.7.0" checksum: 10/615c0f7722c3c9fcf353dbd70b00e2ceae234d4c17cbc839dd85c01d16797c4e4da45f8d27c6118e9e6b033fb06efd196106e13651a1b2f3a10e0f11c7b2f660 languageName: node linkType: hard -"axe-core@npm:^4.2.0": - version: 4.6.3 - resolution: "axe-core@npm:4.6.3" - checksum: 10/280f6a7067129875380f733ae84093ce29c4b8cfe36e1a8ff46bd5d2bcd57d093f11b00223ddf5fef98ca147e0e6568ddd0ada9415cf8ae15d379224bf3cbb51 - languageName: node - linkType: hard - "axios@npm:^1.6.0": version: 1.6.7 resolution: "axios@npm:1.6.7" @@ -13013,21 +12588,7 @@ __metadata: languageName: node linkType: hard -"browserslist@npm:^4.0.0, browserslist@npm:^4.14.5, browserslist@npm:^4.21.4, browserslist@npm:^4.22.2": - version: 4.22.2 - resolution: "browserslist@npm:4.22.2" - dependencies: - caniuse-lite: "npm:^1.0.30001565" - electron-to-chromium: "npm:^1.4.601" - node-releases: "npm:^2.0.14" - update-browserslist-db: "npm:^1.0.13" - bin: - browserslist: cli.js - checksum: 10/e3590793db7f66ad3a50817e7b7f195ce61e029bd7187200244db664bfbe0ac832f784e4f6b9c958aef8ea4abe001ae7880b7522682df521f4bc0a5b67660b5e - languageName: node - linkType: hard - -"browserslist@npm:^4.21.10": +"browserslist@npm:^4.0.0, browserslist@npm:^4.14.5, browserslist@npm:^4.21.10, browserslist@npm:^4.21.4, browserslist@npm:^4.22.2": version: 4.22.3 resolution: "browserslist@npm:4.22.3" dependencies: @@ -13338,13 +12899,6 @@ __metadata: languageName: node linkType: hard -"caniuse-lite@npm:^1.0.30001565": - version: 1.0.30001579 - resolution: "caniuse-lite@npm:1.0.30001579" - checksum: 10/2cd0c02e5d66b09888743ad2b624dbde697ace5c76b55bfd6065ea033f6abea8ac3f5d3c9299c042f91b396e2141b49bc61f5e17086dc9ba3a866cc6790134c0 - languageName: node - linkType: hard - "canvas-hypertxt@npm:^1.0.3": version: 1.0.3 resolution: "canvas-hypertxt@npm:1.0.3" @@ -14308,7 +13862,7 @@ __metadata: languageName: node linkType: hard -"core-js@npm:3.36.0": +"core-js@npm:3.36.0, core-js@npm:^3.6.0, core-js@npm:^3.8.3": version: 3.36.0 resolution: "core-js@npm:3.36.0" checksum: 10/896326c6391c1607dc645293c214cd31c6c535d4a77a88b15fc29e787199f9b06dc15986ddfbc798335bf7a7afd1e92152c94aa5a974790a7f97a98121774302 @@ -14322,13 +13876,6 @@ __metadata: languageName: node linkType: hard -"core-js@npm:^3.6.0, core-js@npm:^3.8.3": - version: 3.35.1 - resolution: "core-js@npm:3.35.1" - checksum: 10/5d31f22eb05cf66bd1a2088a04b7106faa5d0b91c1ffa5d72c5203e4974c31bd7e11969297f540a806c00c74c23991eaad5639592df8b5dbe4412fff3c075cd5 - languageName: node - linkType: hard - "core-util-is@npm:1.0.2": version: 1.0.2 resolution: "core-util-is@npm:1.0.2" @@ -14545,7 +14092,7 @@ __metadata: languageName: node linkType: hard -"css-loader@npm:6.10.0": +"css-loader@npm:6.10.0, css-loader@npm:^6.7.1": version: 6.10.0 resolution: "css-loader@npm:6.10.0" dependencies: @@ -14569,24 +14116,6 @@ __metadata: languageName: node linkType: hard -"css-loader@npm:^6.7.1": - version: 6.9.1 - resolution: "css-loader@npm:6.9.1" - dependencies: - icss-utils: "npm:^5.1.0" - postcss: "npm:^8.4.33" - postcss-modules-extract-imports: "npm:^3.0.0" - postcss-modules-local-by-default: "npm:^4.0.4" - postcss-modules-scope: "npm:^3.1.1" - postcss-modules-values: "npm:^4.0.0" - postcss-value-parser: "npm:^4.2.0" - semver: "npm:^7.5.4" - peerDependencies: - webpack: ^5.0.0 - checksum: 10/6f897406188ed7f6db03daab0602ed86df1e967b48a048ab72d0ee223e59ab9e13c5235481b12deb79e12aadf0be43bc3bdee71e1dc1e875e4bcd91c05b464af - languageName: node - linkType: hard - "css-minimizer-webpack-plugin@npm:6.0.0": version: 6.0.0 resolution: "css-minimizer-webpack-plugin@npm:6.0.0" @@ -16091,13 +15620,6 @@ __metadata: languageName: node linkType: hard -"electron-to-chromium@npm:^1.4.601": - version: 1.4.625 - resolution: "electron-to-chromium@npm:1.4.625" - checksum: 10/610a4eaabf6a064d8f6d4dfa25c55a3940f09a3b25edc8a271821d1b270bb28c4c9f19225d81bfc59deaa12c1f8f0144f3b4510631c6b6b47e0b6216737e216a - languageName: node - linkType: hard - "electron-to-chromium@npm:^1.4.648": version: 1.4.648 resolution: "electron-to-chromium@npm:1.4.648" @@ -17576,13 +17098,6 @@ __metadata: languageName: node linkType: hard -"fast-fifo@npm:^1.0.0": - version: 1.1.0 - resolution: "fast-fifo@npm:1.1.0" - checksum: 10/895f4c9873a4d5059dfa244aa0dde2b22ee563fd673d85b638869715f92244f9d6469bc0873bcb40554d28c51cbc7590045718462cfda1da503b1c6985815209 - languageName: node - linkType: hard - "fast-fifo@npm:^1.1.0": version: 1.3.2 resolution: "fast-fifo@npm:1.3.2" @@ -17673,7 +17188,7 @@ __metadata: languageName: node linkType: hard -"fastq@npm:^1.13.0": +"fastq@npm:^1.13.0, fastq@npm:^1.6.0": version: 1.17.1 resolution: "fastq@npm:1.17.1" dependencies: @@ -17682,15 +17197,6 @@ __metadata: languageName: node linkType: hard -"fastq@npm:^1.6.0": - version: 1.13.0 - resolution: "fastq@npm:1.13.0" - dependencies: - reusify: "npm:^1.0.4" - checksum: 10/0902cb9b81accf34e5542612c8a1df6c6ea47674f85bcc9cdc38795a28b53e4a096f751cfcf4fb25d2ea42fee5447499ba6cf5af5d0209297e1d1fd4dd551bb6 - languageName: node - linkType: hard - "fault@npm:^1.0.0": version: 1.0.4 resolution: "fault@npm:1.0.4" @@ -17955,17 +17461,7 @@ __metadata: languageName: node linkType: hard -"follow-redirects@npm:^1.0.0": - version: 1.15.3 - resolution: "follow-redirects@npm:1.15.3" - peerDependenciesMeta: - debug: - optional: true - checksum: 10/60d98693f4976892f8c654b16ef6d1803887a951898857ab0cdc009570b1c06314ad499505b7a040ac5b98144939f8597766e5e6a6859c0945d157b473aa6f5f - languageName: node - linkType: hard - -"follow-redirects@npm:^1.15.4": +"follow-redirects@npm:^1.0.0, follow-redirects@npm:^1.15.4": version: 1.15.5 resolution: "follow-redirects@npm:1.15.5" peerDependenciesMeta: @@ -19128,7 +18624,7 @@ __metadata: prop-types: "npm:15.8.1" pseudoizer: "npm:^0.1.0" rc-cascader: "npm:3.21.2" - rc-drawer: "npm:6.5.2" + rc-drawer: "npm:7.1.0" rc-slider: "npm:10.5.0" rc-time-picker: "npm:3.7.3" rc-tree: "npm:5.8.5" @@ -20280,13 +19776,6 @@ __metadata: languageName: node linkType: hard -"ip@npm:^1.1.5": - version: 1.1.5 - resolution: "ip@npm:1.1.5" - checksum: 10/40a00572cf06b53f4c7b7fe6270a8427ef4c6c0820a380f9f1eb48a323eb09c7dbd16245b472cf5a2d083911d0deae4d712b6e6c88b346fa274e8ce07756a7d6 - languageName: node - linkType: hard - "ip@npm:^2.0.0": version: 2.0.0 resolution: "ip@npm:2.0.0" @@ -25651,17 +25140,7 @@ __metadata: languageName: node linkType: hard -"postcss-selector-parser@npm:^6.0.11, postcss-selector-parser@npm:^6.0.13, postcss-selector-parser@npm:^6.0.2, postcss-selector-parser@npm:^6.0.4": - version: 6.0.13 - resolution: "postcss-selector-parser@npm:6.0.13" - dependencies: - cssesc: "npm:^3.0.0" - util-deprecate: "npm:^1.0.2" - checksum: 10/e779aa1f8ca9ee45d562400aac6109a2bccc59559b6e15adec8bc2a71d395ca563a378fd68f6a61963b4ef2ca190e0c0486e6dc6c41d755f3b82dd6e480e6941 - languageName: node - linkType: hard - -"postcss-selector-parser@npm:^6.0.15": +"postcss-selector-parser@npm:^6.0.11, postcss-selector-parser@npm:^6.0.13, postcss-selector-parser@npm:^6.0.15, postcss-selector-parser@npm:^6.0.2, postcss-selector-parser@npm:^6.0.4": version: 6.0.15 resolution: "postcss-selector-parser@npm:6.0.15" dependencies: @@ -26130,7 +25609,7 @@ __metadata: languageName: node linkType: hard -"queue-tick@npm:^1.0.0, queue-tick@npm:^1.0.1": +"queue-tick@npm:^1.0.1": version: 1.0.1 resolution: "queue-tick@npm:1.0.1" checksum: 10/f447926c513b64a857906f017a3b350f7d11277e3c8d2a21a42b7998fa1a613d7a829091e12d142bb668905c8f68d8103416c7197856efb0c72fa835b8e254b5 @@ -26296,19 +25775,19 @@ __metadata: languageName: node linkType: hard -"rc-drawer@npm:6.5.2": - version: 6.5.2 - resolution: "rc-drawer@npm:6.5.2" +"rc-drawer@npm:7.1.0": + version: 7.1.0 + resolution: "rc-drawer@npm:7.1.0" dependencies: - "@babel/runtime": "npm:^7.10.1" + "@babel/runtime": "npm:^7.23.9" "@rc-component/portal": "npm:^1.1.1" classnames: "npm:^2.2.6" rc-motion: "npm:^2.6.1" - rc-util: "npm:^5.36.0" + rc-util: "npm:^5.38.1" peerDependencies: react: ">=16.9.0" react-dom: ">=16.9.0" - checksum: 10/e5023a24494fb9414393ce0f0ee41529a4fd7bd0f0d1186fcc6d2ce0682a11e88428f51362ba81c85063a9c0f1b88d4f4b46e4d2d5ddf3f4c6759857cccfa3fe + checksum: 10/6d37b35e3a9abd0e105f91f388c818dcd7d6e9a5805a9bb0909b01e96e47437af2010e29c05648d15ece37e0768ff752e15f498ced69a476c20b9ea06c5e636f languageName: node linkType: hard @@ -26460,16 +25939,16 @@ __metadata: languageName: node linkType: hard -"rc-util@npm:^5.15.0, rc-util@npm:^5.16.1, rc-util@npm:^5.21.0, rc-util@npm:^5.24.4, rc-util@npm:^5.27.0, rc-util@npm:^5.36.0, rc-util@npm:^5.37.0, rc-util@npm:^5.38.0": - version: 5.38.0 - resolution: "rc-util@npm:5.38.0" +"rc-util@npm:^5.15.0, rc-util@npm:^5.16.1, rc-util@npm:^5.21.0, rc-util@npm:^5.24.4, rc-util@npm:^5.27.0, rc-util@npm:^5.37.0, rc-util@npm:^5.38.0, rc-util@npm:^5.38.1": + version: 5.38.2 + resolution: "rc-util@npm:5.38.2" dependencies: "@babel/runtime": "npm:^7.18.3" react-is: "npm:^18.2.0" peerDependencies: react: ">=16.9.0" react-dom: ">=16.9.0" - checksum: 10/e5d33ee488497079d89371246847a229628d7e0845436be5c0fc52d233120dfd59332c4a4e9b94b6beb466f3c49c778137c6812870f5cdaaa10c5443d371a329 + checksum: 10/f8d8b21d0ed09de6fcf6c24dc19bf82f8f1fd089a625d35fd399626280ed33e73b9a703aa78f1a09ccd40b83f50e73bbc993adf892355a01857d3a1bb83e0958 languageName: node linkType: hard @@ -27335,7 +26814,7 @@ __metadata: languageName: node linkType: hard -"react-use@npm:17.5.0": +"react-use@npm:17.5.0, react-use@npm:^17.4.2": version: 17.5.0 resolution: "react-use@npm:17.5.0" dependencies: @@ -27360,31 +26839,6 @@ __metadata: languageName: node linkType: hard -"react-use@npm:^17.4.2": - version: 17.4.2 - resolution: "react-use@npm:17.4.2" - dependencies: - "@types/js-cookie": "npm:^2.2.6" - "@xobotyi/scrollbar-width": "npm:^1.9.5" - copy-to-clipboard: "npm:^3.3.1" - fast-deep-equal: "npm:^3.1.3" - fast-shallow-equal: "npm:^1.0.0" - js-cookie: "npm:^2.2.1" - nano-css: "npm:^5.6.1" - react-universal-interface: "npm:^0.6.2" - resize-observer-polyfill: "npm:^1.5.1" - screenfull: "npm:^5.1.0" - set-harmonic-interval: "npm:^1.0.1" - throttle-debounce: "npm:^3.0.1" - ts-easing: "npm:^0.2.0" - tslib: "npm:^2.1.0" - peerDependencies: - react: "*" - react-dom: "*" - checksum: 10/56d2da474d949d22eb34ff3ffccf5526986d51ed68a8f4e64f4b79bdcff3f0ea55d322c104e3fc0819b08b8765e8eb3fa47d8b506e9d61ff1fdc7bd1374c17d6 - languageName: node - linkType: hard - "react-virtual@npm:2.10.4, react-virtual@npm:^2.8.2": version: 2.10.4 resolution: "react-virtual@npm:2.10.4" @@ -29065,17 +28519,7 @@ __metadata: languageName: node linkType: hard -"socks@npm:^2.6.1, socks@npm:^2.6.2": - version: 2.6.2 - resolution: "socks@npm:2.6.2" - dependencies: - ip: "npm:^1.1.5" - smart-buffer: "npm:^4.2.0" - checksum: 10/820232ddaeb847ef33312c429fb51aae03e1b774917f189ef491048bb4c4d7742924064f72d7730e3aa08a3ddb6cc2bdcd5949d34c35597e4f6a66eefd994f14 - languageName: node - linkType: hard - -"socks@npm:^2.7.1": +"socks@npm:^2.6.1, socks@npm:^2.6.2, socks@npm:^2.7.1": version: 2.8.0 resolution: "socks@npm:2.8.0" dependencies: @@ -29311,14 +28755,7 @@ __metadata: languageName: node linkType: hard -"sprintf-js@npm:^1.1.1": - version: 1.1.2 - resolution: "sprintf-js@npm:1.1.2" - checksum: 10/0044322a252b36bffc3d8a462a4882de57830e18d37d1cc000104ff4744b512d6a9b1ca6240e7ad141a987a1eaad071668fe12d11c496c11d3641c4797a6cf3f - languageName: node - linkType: hard - -"sprintf-js@npm:^1.1.3": +"sprintf-js@npm:^1.1.1, sprintf-js@npm:^1.1.3": version: 1.1.3 resolution: "sprintf-js@npm:1.1.3" checksum: 10/e7587128c423f7e43cc625fe2f87e6affdf5ca51c1cc468e910d8aaca46bb44a7fbcfa552f787b1d3987f7043aeb4527d1b99559e6621e01b42b3f45e5a24cbb @@ -29586,7 +29023,7 @@ __metadata: languageName: node linkType: hard -"streamx@npm:^2.12.0, streamx@npm:^2.13.2, streamx@npm:^2.14.0": +"streamx@npm:^2.12.0, streamx@npm:^2.12.5, streamx@npm:^2.13.2, streamx@npm:^2.14.0": version: 2.15.7 resolution: "streamx@npm:2.15.7" dependencies: @@ -29596,16 +29033,6 @@ __metadata: languageName: node linkType: hard -"streamx@npm:^2.12.5": - version: 2.12.5 - resolution: "streamx@npm:2.12.5" - dependencies: - fast-fifo: "npm:^1.0.0" - queue-tick: "npm:^1.0.0" - checksum: 10/daa5789ca31101684d9266f7ea77294908bd3e55607805ac1657f0cef1ee0a1966bc3988d2ec12c5f68a718d481147fa3ace2525486a1e39ca7155c598917cd1 - languageName: node - linkType: hard - "strict-event-emitter@npm:^0.2.4": version: 0.2.8 resolution: "strict-event-emitter@npm:0.2.8" @@ -30250,7 +29677,7 @@ __metadata: languageName: node linkType: hard -"terser-webpack-plugin@npm:5.3.10, terser-webpack-plugin@npm:^5.3.10": +"terser-webpack-plugin@npm:5.3.10, terser-webpack-plugin@npm:^5.1.3, terser-webpack-plugin@npm:^5.3.1, terser-webpack-plugin@npm:^5.3.10, terser-webpack-plugin@npm:^5.3.7": version: 5.3.10 resolution: "terser-webpack-plugin@npm:5.3.10" dependencies: @@ -30272,28 +29699,6 @@ __metadata: languageName: node linkType: hard -"terser-webpack-plugin@npm:^5.1.3, terser-webpack-plugin@npm:^5.3.1, terser-webpack-plugin@npm:^5.3.7": - version: 5.3.9 - resolution: "terser-webpack-plugin@npm:5.3.9" - dependencies: - "@jridgewell/trace-mapping": "npm:^0.3.17" - jest-worker: "npm:^27.4.5" - schema-utils: "npm:^3.1.1" - serialize-javascript: "npm:^6.0.1" - terser: "npm:^5.16.8" - peerDependencies: - webpack: ^5.1.0 - peerDependenciesMeta: - "@swc/core": - optional: true - esbuild: - optional: true - uglify-js: - optional: true - checksum: 10/339737a407e034b7a9d4a66e31d84d81c10433e41b8eae2ca776f0e47c2048879be482a9aa08e8c27565a2a949bc68f6e07f451bf4d9aa347dd61b3d000f5353 - languageName: node - linkType: hard - "terser@npm:^5.0.0, terser@npm:^5.15.1, terser@npm:^5.26.0, terser@npm:^5.7.2": version: 5.27.0 resolution: "terser@npm:5.27.0" @@ -30308,20 +29713,6 @@ __metadata: languageName: node linkType: hard -"terser@npm:^5.16.8": - version: 5.17.2 - resolution: "terser@npm:5.17.2" - dependencies: - "@jridgewell/source-map": "npm:^0.3.2" - acorn: "npm:^8.5.0" - commander: "npm:^2.20.0" - source-map-support: "npm:~0.5.20" - bin: - terser: bin/terser - checksum: 10/6df529586a4913657547dd8bfe2b5a59704b7acbe4e49ac938a16f829a62226f98dafb19c88b7af66b245ea281ee5dbeec33a41349ac3c035855417b06ebd646 - languageName: node - linkType: hard - "test-exclude@npm:^6.0.0": version: 6.0.0 resolution: "test-exclude@npm:6.0.0" @@ -32047,9 +31438,9 @@ __metadata: languageName: node linkType: hard -"webpack@npm:5, webpack@npm:^5": - version: 5.90.1 - resolution: "webpack@npm:5.90.1" +"webpack@npm:5, webpack@npm:5.90.2, webpack@npm:^5": + version: 5.90.2 + resolution: "webpack@npm:5.90.2" dependencies: "@types/eslint-scope": "npm:^3.7.3" "@types/estree": "npm:^1.0.5" @@ -32080,7 +31471,7 @@ __metadata: optional: true bin: webpack: bin/webpack.js - checksum: 10/6ad23518123f1742238177920cefa61152d981f986adac5901236845c86ba9bb375a3ba75e188925c856c3d2a76a2ba119e95b8a608a51424968389041089075 + checksum: 10/4eaeed1255c9c7738921c4ce4facdb3b78dbfcb3441496942f6d160a41fbcebd24fb2c6dbb64739b357c5ff78e5a298f6c82eca482438b95130a3ba4e16d084a languageName: node linkType: hard @@ -32158,43 +31549,6 @@ __metadata: languageName: node linkType: hard -"webpack@npm:5.90.2": - version: 5.90.2 - resolution: "webpack@npm:5.90.2" - dependencies: - "@types/eslint-scope": "npm:^3.7.3" - "@types/estree": "npm:^1.0.5" - "@webassemblyjs/ast": "npm:^1.11.5" - "@webassemblyjs/wasm-edit": "npm:^1.11.5" - "@webassemblyjs/wasm-parser": "npm:^1.11.5" - acorn: "npm:^8.7.1" - acorn-import-assertions: "npm:^1.9.0" - browserslist: "npm:^4.21.10" - chrome-trace-event: "npm:^1.0.2" - enhanced-resolve: "npm:^5.15.0" - es-module-lexer: "npm:^1.2.1" - eslint-scope: "npm:5.1.1" - events: "npm:^3.2.0" - glob-to-regexp: "npm:^0.4.1" - graceful-fs: "npm:^4.2.9" - json-parse-even-better-errors: "npm:^2.3.1" - loader-runner: "npm:^4.2.0" - mime-types: "npm:^2.1.27" - neo-async: "npm:^2.6.2" - schema-utils: "npm:^3.2.0" - tapable: "npm:^2.1.1" - terser-webpack-plugin: "npm:^5.3.10" - watchpack: "npm:^2.4.0" - webpack-sources: "npm:^3.2.3" - peerDependenciesMeta: - webpack-cli: - optional: true - bin: - webpack: bin/webpack.js - checksum: 10/4eaeed1255c9c7738921c4ce4facdb3b78dbfcb3441496942f6d160a41fbcebd24fb2c6dbb64739b357c5ff78e5a298f6c82eca482438b95130a3ba4e16d084a - languageName: node - linkType: hard - "websocket-driver@npm:>=0.5.1, websocket-driver@npm:^0.7.4": version: 0.7.4 resolution: "websocket-driver@npm:0.7.4" From 13cb09b17871497fc41ad28a378b1d25fb87a3da Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 22 Feb 2024 15:52:20 +0000 Subject: [PATCH 0103/1406] Update dependency postcss-loader to v8 (#83161) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 39 +++++++++++++++++++++++++++++++-------- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 03525b6afa..7db6967868 100644 --- a/package.json +++ b/package.json @@ -191,7 +191,7 @@ "ngtemplate-loader": "2.1.0", "node-notifier": "10.0.1", "postcss": "8.4.35", - "postcss-loader": "7.3.4", + "postcss-loader": "8.1.0", "postcss-reporter": "7.1.0", "postcss-scss": "4.0.9", "prettier": "3.2.5", diff --git a/yarn.lock b/yarn.lock index 10a54e0147..23e43ba0a5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13923,7 +13923,7 @@ __metadata: languageName: node linkType: hard -"cosmiconfig@npm:^8.2.0, cosmiconfig@npm:^8.3.5": +"cosmiconfig@npm:^8.2.0": version: 8.3.6 resolution: "cosmiconfig@npm:8.3.6" dependencies: @@ -13940,6 +13940,23 @@ __metadata: languageName: node linkType: hard +"cosmiconfig@npm:^9.0.0": + version: 9.0.0 + resolution: "cosmiconfig@npm:9.0.0" + dependencies: + env-paths: "npm:^2.2.1" + import-fresh: "npm:^3.3.0" + js-yaml: "npm:^4.1.0" + parse-json: "npm:^5.2.0" + peerDependencies: + typescript: ">=4.9.5" + peerDependenciesMeta: + typescript: + optional: true + checksum: 10/8bdf1dfbb6fdb3755195b6886dc0649a3c742ec75afa4cb8da7b070936aed22a4f4e5b7359faafe03180358f311dbc300d248fd6586c458203d376a40cc77826 + languageName: node + linkType: hard + "create-emotion@npm:^10.0.14, create-emotion@npm:^10.0.27": version: 10.0.27 resolution: "create-emotion@npm:10.0.27" @@ -15766,7 +15783,7 @@ __metadata: languageName: node linkType: hard -"env-paths@npm:^2.2.0": +"env-paths@npm:^2.2.0, env-paths@npm:^2.2.1": version: 2.2.1 resolution: "env-paths@npm:2.2.1" checksum: 10/65b5df55a8bab92229ab2b40dad3b387fad24613263d103a97f91c9fe43ceb21965cd3392b1ccb5d77088021e525c4e0481adb309625d0cb94ade1d1fb8dc17e @@ -18616,7 +18633,7 @@ __metadata: papaparse: "npm:5.4.1" pluralize: "npm:^8.0.0" postcss: "npm:8.4.35" - postcss-loader: "npm:7.3.4" + postcss-loader: "npm:8.1.0" postcss-reporter: "npm:7.1.0" postcss-scss: "npm:4.0.9" prettier: "npm:3.2.5" @@ -24831,17 +24848,23 @@ __metadata: languageName: node linkType: hard -"postcss-loader@npm:7.3.4": - version: 7.3.4 - resolution: "postcss-loader@npm:7.3.4" +"postcss-loader@npm:8.1.0": + version: 8.1.0 + resolution: "postcss-loader@npm:8.1.0" dependencies: - cosmiconfig: "npm:^8.3.5" + cosmiconfig: "npm:^9.0.0" jiti: "npm:^1.20.0" semver: "npm:^7.5.4" peerDependencies: + "@rspack/core": 0.x || 1.x postcss: ^7.0.0 || ^8.0.1 webpack: ^5.0.0 - checksum: 10/234b01149a966a6190290c6d265b8e3df10f43262dd679451c1e7370bae74e27b746b02e660d204b901e3cf1ad28759c2679a93c64a3eb499169d8dec39df1c1 + peerDependenciesMeta: + "@rspack/core": + optional: true + webpack: + optional: true + checksum: 10/06040d1b3819bae0c1d6b209646e1e9c1924d7a6acb7dd8bde2fc6e57b0b330e5386eb11a62f81f6f80de15e57cdab49191125ab464debb721e94e6d39c1da2e languageName: node linkType: hard From 1ed1242358c0d3639724e57d86d45903e7a779b2 Mon Sep 17 00:00:00 2001 From: George Robinson Date: Thu, 22 Feb 2024 15:58:56 +0000 Subject: [PATCH 0104/1406] Alerting: Basic support for time_intervals (#83216) This commit adds basic support for time_intervals, as mute_time_intervals is deprecated in Alertmanager and scheduled to be removed before 1.0. It does not add support for time_intervals in API or file provisioning, nor does it support exporting time intervals. This will be added in later commits to keep the changes as simple as possible. --- go.mod | 2 +- go.sum | 6 +- .../api/tooling/definitions/alertmanager.go | 14 ++ .../tooling/definitions/alertmanager_test.go | 163 +++++++++++++++++- pkg/services/ngalert/notifier/config.go | 4 + pkg/services/ngalert/notifier/validation.go | 18 +- .../api_alertmanager_configuration_test.go | 64 +++++++ 7 files changed, 257 insertions(+), 14 deletions(-) diff --git a/go.mod b/go.mod index 7cb945f126..7720d65450 100644 --- a/go.mod +++ b/go.mod @@ -59,7 +59,7 @@ require ( github.com/google/uuid v1.6.0 // @grafana/backend-platform github.com/google/wire v0.5.0 // @grafana/backend-platform github.com/gorilla/websocket v1.5.0 // @grafana/grafana-app-platform-squad - github.com/grafana/alerting v0.0.0-20240213130827-92f64f0f2a12 // @grafana/alerting-squad-backend + github.com/grafana/alerting v0.0.0-20240222104113-abfafef9a7d2 // @grafana/alerting-squad-backend github.com/grafana/cuetsy v0.1.11 // @grafana/grafana-as-code github.com/grafana/grafana-aws-sdk v0.23.1 // @grafana/aws-datasources github.com/grafana/grafana-azure-sdk-go v1.12.0 // @grafana/partner-datasources diff --git a/go.sum b/go.sum index af3d2952d8..23d3e67d15 100644 --- a/go.sum +++ b/go.sum @@ -2505,8 +2505,8 @@ github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWm github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gotestyourself/gotestyourself v1.3.0/go.mod h1:zZKM6oeNM8k+FRljX1mnzVYeS8wiGgQyvST1/GafPbY= github.com/gotestyourself/gotestyourself v2.2.0+incompatible/go.mod h1:zZKM6oeNM8k+FRljX1mnzVYeS8wiGgQyvST1/GafPbY= -github.com/grafana/alerting v0.0.0-20240213130827-92f64f0f2a12 h1:QepaY7wUP3U1hFiU1Lnv+tymnovzK21KQ/evMDpYsEw= -github.com/grafana/alerting v0.0.0-20240213130827-92f64f0f2a12/go.mod h1:brTFeACal/cSZAR8XO/4LPKs7rzNfS86okl6QjSP1eY= +github.com/grafana/alerting v0.0.0-20240222104113-abfafef9a7d2 h1:fmUMdtP7ditGgJFdXCwVxDrKnondHNNe0TkhN5YaIAI= +github.com/grafana/alerting v0.0.0-20240222104113-abfafef9a7d2/go.mod h1:brTFeACal/cSZAR8XO/4LPKs7rzNfS86okl6QjSP1eY= github.com/grafana/codejen v0.0.3 h1:tAWxoTUuhgmEqxJPOLtJoxlPBbMULFwKFOcRsPRPXDw= github.com/grafana/codejen v0.0.3/go.mod h1:zmwwM/DRyQB7pfuBjTWII3CWtxcXh8LTwAYGfDfpR6s= github.com/grafana/cue v0.0.0-20230926092038-971951014e3f h1:TmYAMnqg3d5KYEAaT6PtTguL2GjLfvr6wnAX8Azw6tQ= @@ -2530,8 +2530,6 @@ github.com/grafana/grafana-google-sdk-go v0.1.0/go.mod h1:Vo2TKWfDVmNTELBUM+3lkr github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79 h1:r+mU5bGMzcXCRVAuOrTn54S80qbfVkvTdUJZfSfTNbs= github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79/go.mod h1:wc6Hbh3K2TgCUSfBC/BOzabItujtHMESZeFk5ZhdxhQ= github.com/grafana/grafana-plugin-sdk-go v0.114.0/go.mod h1:D7x3ah+1d4phNXpbnOaxa/osSaZlwh9/ZUnGGzegRbk= -github.com/grafana/grafana-plugin-sdk-go v0.211.0 h1:hYtieOoYvsv/BcFbtspml4OzfuYrv1d14nESdf13qxQ= -github.com/grafana/grafana-plugin-sdk-go v0.211.0/go.mod h1:qsI4ktDf0lig74u8SLPJf9zRdVxWV/W4Wi+Ox6gifgs= github.com/grafana/grafana-plugin-sdk-go v0.212.0 h1:ohgMktFAasLTzAhKhcIzk81O60E29Za6ly02GhEqGIU= github.com/grafana/grafana-plugin-sdk-go v0.212.0/go.mod h1:qsI4ktDf0lig74u8SLPJf9zRdVxWV/W4Wi+Ox6gifgs= github.com/grafana/kindsys v0.0.0-20230508162304-452481b63482 h1:1YNoeIhii4UIIQpCPU+EXidnqf449d0C3ZntAEt4KSo= diff --git a/pkg/services/ngalert/api/tooling/definitions/alertmanager.go b/pkg/services/ngalert/api/tooling/definitions/alertmanager.go index 26b540e401..cc3b57713c 100644 --- a/pkg/services/ngalert/api/tooling/definitions/alertmanager.go +++ b/pkg/services/ngalert/api/tooling/definitions/alertmanager.go @@ -739,6 +739,8 @@ func (c *GettableApiAlertingConfig) GetReceivers() []*GettableApiReceiver { return c.Receivers } +func (c *GettableApiAlertingConfig) GetTimeIntervals() []config.TimeInterval { return c.TimeIntervals } + func (c *GettableApiAlertingConfig) GetMuteTimeIntervals() []config.MuteTimeInterval { return c.MuteTimeIntervals } @@ -803,6 +805,7 @@ type Config struct { Global *config.GlobalConfig `yaml:"global,omitempty" json:"global,omitempty"` Route *Route `yaml:"route,omitempty" json:"route,omitempty"` InhibitRules []config.InhibitRule `yaml:"inhibit_rules,omitempty" json:"inhibit_rules,omitempty"` + TimeIntervals []config.TimeInterval `yaml:"time_intervals,omitempty" json:"time_intervals,omitempty"` MuteTimeIntervals []config.MuteTimeInterval `yaml:"mute_time_intervals,omitempty" json:"mute_time_intervals,omitempty"` Templates []string `yaml:"templates" json:"templates"` } @@ -936,6 +939,15 @@ func (c *Config) UnmarshalJSON(b []byte) error { } tiNames := make(map[string]struct{}) + for _, ti := range c.TimeIntervals { + if ti.Name == "" { + return fmt.Errorf("missing name in time interval") + } + if _, ok := tiNames[ti.Name]; ok { + return fmt.Errorf("time interval %q is not unique", ti.Name) + } + tiNames[ti.Name] = struct{}{} + } for _, mt := range c.MuteTimeIntervals { if mt.Name == "" { return fmt.Errorf("missing name in mute time interval") @@ -976,6 +988,8 @@ func (c *PostableApiAlertingConfig) GetReceivers() []*PostableApiReceiver { return c.Receivers } +func (c *PostableApiAlertingConfig) GetTimeIntervals() []config.TimeInterval { return c.TimeIntervals } + func (c *PostableApiAlertingConfig) GetMuteTimeIntervals() []config.MuteTimeInterval { return c.MuteTimeIntervals } diff --git a/pkg/services/ngalert/api/tooling/definitions/alertmanager_test.go b/pkg/services/ngalert/api/tooling/definitions/alertmanager_test.go index c549eca710..e38506ca5d 100644 --- a/pkg/services/ngalert/api/tooling/definitions/alertmanager_test.go +++ b/pkg/services/ngalert/api/tooling/definitions/alertmanager_test.go @@ -485,7 +485,50 @@ func Test_ConfigUnmashaling(t *testing.T) { err error }{ { - desc: "empty mute time name should error", + desc: "missing time interval name should error", + err: errors.New("missing name in time interval"), + input: ` + { + "route": { + "receiver": "grafana-default-email" + }, + "time_intervals": [ + { + "name": "", + "time_intervals": [ + { + "times": [ + { + "start_time": "00:00", + "end_time": "12:00" + } + ] + } + ] + } + ], + "templates": null, + "receivers": [ + { + "name": "grafana-default-email", + "grafana_managed_receiver_configs": [ + { + "uid": "uxwfZvtnz", + "name": "email receiver", + "type": "email", + "disableResolveMessage": false, + "settings": { + "addresses": "" + }, + "secureFields": {} + } + ] + } + ] + } + `, + }, { + desc: "missing mute time interval name should error", err: errors.New("missing name in mute time interval"), input: ` { @@ -529,7 +572,64 @@ func Test_ConfigUnmashaling(t *testing.T) { `, }, { - desc: "not unique mute time names should error", + desc: "duplicate time interval names should error", + err: errors.New("time interval \"test1\" is not unique"), + input: ` + { + "route": { + "receiver": "grafana-default-email" + }, + "time_intervals": [ + { + "name": "test1", + "time_intervals": [ + { + "times": [ + { + "start_time": "00:00", + "end_time": "12:00" + } + ] + } + ] + }, + { + "name": "test1", + "time_intervals": [ + { + "times": [ + { + "start_time": "00:00", + "end_time": "12:00" + } + ] + } + ] + } + ], + "templates": null, + "receivers": [ + { + "name": "grafana-default-email", + "grafana_managed_receiver_configs": [ + { + "uid": "uxwfZvtnz", + "name": "email receiver", + "type": "email", + "disableResolveMessage": false, + "settings": { + "addresses": "" + }, + "secureFields": {} + } + ] + } + ] + } + `, + }, + { + desc: "duplicate mute time interval names should error", err: errors.New("mute time interval \"test1\" is not unique"), input: ` { @@ -585,6 +685,65 @@ func Test_ConfigUnmashaling(t *testing.T) { } `, }, + { + desc: "duplicate time and mute time interval names should error", + err: errors.New("mute time interval \"test1\" is not unique"), + input: ` + { + "route": { + "receiver": "grafana-default-email" + }, + "mute_time_intervals": [ + { + "name": "test1", + "time_intervals": [ + { + "times": [ + { + "start_time": "00:00", + "end_time": "12:00" + } + ] + } + ] + } + ], + "time_intervals": [ + { + "name": "test1", + "time_intervals": [ + { + "times": [ + { + "start_time": "00:00", + "end_time": "12:00" + } + ] + } + ] + } + ], + "templates": null, + "receivers": [ + { + "name": "grafana-default-email", + "grafana_managed_receiver_configs": [ + { + "uid": "uxwfZvtnz", + "name": "email receiver", + "type": "email", + "disableResolveMessage": false, + "settings": { + "addresses": "" + }, + "secureFields": {} + } + ] + } + ] + } + `, + }, { desc: "mute time intervals on root route should error", err: errors.New("root route must not have any mute time intervals"), diff --git a/pkg/services/ngalert/notifier/config.go b/pkg/services/ngalert/notifier/config.go index 86dfb40404..43b70760a2 100644 --- a/pkg/services/ngalert/notifier/config.go +++ b/pkg/services/ngalert/notifier/config.go @@ -111,6 +111,10 @@ func (a AlertingConfiguration) InhibitRules() []alertingNotify.InhibitRule { return a.alertmanagerConfig.InhibitRules } +func (a AlertingConfiguration) TimeIntervals() []alertingNotify.TimeInterval { + return a.alertmanagerConfig.TimeIntervals +} + func (a AlertingConfiguration) MuteTimeIntervals() []alertingNotify.MuteTimeInterval { return a.alertmanagerConfig.MuteTimeIntervals } diff --git a/pkg/services/ngalert/notifier/validation.go b/pkg/services/ngalert/notifier/validation.go index 1f2cad0b01..84964214a2 100644 --- a/pkg/services/ngalert/notifier/validation.go +++ b/pkg/services/ngalert/notifier/validation.go @@ -20,13 +20,14 @@ type NotificationSettingsValidator interface { // staticValidator is a NotificationSettingsValidator that uses static pre-fetched values for available receivers and mute timings. type staticValidator struct { - availableReceivers map[string]struct{} - availableMuteTimings map[string]struct{} + availableReceivers map[string]struct{} + availableTimeIntervals map[string]struct{} } // apiAlertingConfig contains the methods required to validate NotificationSettings and create autogen routes. type apiAlertingConfig[R receiver] interface { GetReceivers() []R + GetTimeIntervals() []config.TimeInterval GetMuteTimeIntervals() []config.MuteTimeInterval GetRoute() *definitions.Route } @@ -42,14 +43,17 @@ func NewNotificationSettingsValidator[R receiver](am apiAlertingConfig[R]) Notif availableReceivers[receiver.GetName()] = struct{}{} } - availableMuteTimings := make(map[string]struct{}) + availableTimeIntervals := make(map[string]struct{}) + for _, interval := range am.GetTimeIntervals() { + availableTimeIntervals[interval.Name] = struct{}{} + } for _, interval := range am.GetMuteTimeIntervals() { - availableMuteTimings[interval.Name] = struct{}{} + availableTimeIntervals[interval.Name] = struct{}{} } return staticValidator{ - availableReceivers: availableReceivers, - availableMuteTimings: availableMuteTimings, + availableReceivers: availableReceivers, + availableTimeIntervals: availableTimeIntervals, } } @@ -63,7 +67,7 @@ func (n staticValidator) Validate(settings models.NotificationSettings) error { errs = append(errs, fmt.Errorf("receiver '%s' does not exist", settings.Receiver)) } for _, interval := range settings.MuteTimeIntervals { - if _, ok := n.availableMuteTimings[interval]; !ok { + if _, ok := n.availableTimeIntervals[interval]; !ok { errs = append(errs, fmt.Errorf("mute time interval '%s' does not exist", interval)) } } diff --git a/pkg/tests/api/alerting/api_alertmanager_configuration_test.go b/pkg/tests/api/alerting/api_alertmanager_configuration_test.go index 7ca9c969c4..f4e0960493 100644 --- a/pkg/tests/api/alerting/api_alertmanager_configuration_test.go +++ b/pkg/tests/api/alerting/api_alertmanager_configuration_test.go @@ -12,6 +12,7 @@ import ( "github.com/prometheus/alertmanager/config" "github.com/prometheus/alertmanager/pkg/labels" + "github.com/prometheus/alertmanager/timeinterval" "github.com/prometheus/common/model" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -192,6 +193,69 @@ func TestIntegrationAlertmanagerConfiguration(t *testing.T) { }}, }, }, + }, { + name: "configuration with time intervals", + cfg: apimodels.PostableUserConfig{ + AlertmanagerConfig: apimodels.PostableApiAlertingConfig{ + Config: apimodels.Config{ + Route: &apimodels.Route{ + Receiver: "test", + Routes: []*apimodels.Route{{ + MuteTimeIntervals: []string{"weekends"}, + }}, + }, + TimeIntervals: []config.TimeInterval{{ + Name: "weekends", + TimeIntervals: []timeinterval.TimeInterval{{ + Weekdays: []timeinterval.WeekdayRange{{ + InclusiveRange: timeinterval.InclusiveRange{ + Begin: 1, + End: 5, + }, + }}, + }}, + }}, + }, + Receivers: []*apimodels.PostableApiReceiver{{ + Receiver: config.Receiver{ + Name: "test", + }, + }}, + }, + }, + }, { + // TODO: Mute time intervals is deprecated in Alertmanager and scheduled to be + // removed before version 1.0. Remove this test when support for mute time + // intervals is removed. + name: "configuration with mute time intervals", + cfg: apimodels.PostableUserConfig{ + AlertmanagerConfig: apimodels.PostableApiAlertingConfig{ + Config: apimodels.Config{ + Route: &apimodels.Route{ + Receiver: "test", + Routes: []*apimodels.Route{{ + MuteTimeIntervals: []string{"weekends"}, + }}, + }, + MuteTimeIntervals: []config.MuteTimeInterval{{ + Name: "weekends", + TimeIntervals: []timeinterval.TimeInterval{{ + Weekdays: []timeinterval.WeekdayRange{{ + InclusiveRange: timeinterval.InclusiveRange{ + Begin: 1, + End: 5, + }, + }}, + }}, + }}, + }, + Receivers: []*apimodels.PostableApiReceiver{{ + Receiver: config.Receiver{ + Name: "test", + }, + }}, + }, + }, }} for _, tc := range cases { From 18ec6fcdd326c5c4b0a9a998d1700a62d4605754 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 22 Feb 2024 18:26:01 +0200 Subject: [PATCH 0105/1406] Update dependency sass-loader to v14 (#83255) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- packages/grafana-prometheus/package.json | 2 +- packages/grafana-ui/package.json | 2 +- yarn.lock | 20 +++++++++++--------- 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index 7db6967868..58505eac03 100644 --- a/package.json +++ b/package.json @@ -203,7 +203,7 @@ "rimraf": "5.0.5", "rudder-sdk-js": "2.48.1", "sass": "1.70.0", - "sass-loader": "13.3.2", + "sass-loader": "14.1.1", "style-loader": "3.3.4", "stylelint": "15.11.0", "stylelint-config-prettier": "9.0.5", diff --git a/packages/grafana-prometheus/package.json b/packages/grafana-prometheus/package.json index f8c857df14..538c1456e4 100644 --- a/packages/grafana-prometheus/package.json +++ b/packages/grafana-prometheus/package.json @@ -135,7 +135,7 @@ "rollup-plugin-esbuild": "5.0.0", "rollup-plugin-node-externals": "^5.0.0", "sass": "1.70.0", - "sass-loader": "13.3.2", + "sass-loader": "14.1.1", "style-loader": "3.3.4", "testing-library-selector": "0.3.1", "ts-node": "10.9.2", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index a10d7c5ff2..e04b095af2 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -177,7 +177,7 @@ "rollup-plugin-dts": "^5.0.0", "rollup-plugin-esbuild": "5.0.0", "rollup-plugin-node-externals": "^5.0.0", - "sass-loader": "13.3.2", + "sass-loader": "14.1.1", "storybook": "7.4.5", "storybook-addon-turbo-build": "2.0.1", "storybook-dark-mode": "3.0.1", diff --git a/yarn.lock b/yarn.lock index 23e43ba0a5..445022dae2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4000,7 +4000,7 @@ __metadata: rollup-plugin-node-externals: "npm:^5.0.0" rxjs: "npm:7.8.1" sass: "npm:1.70.0" - sass-loader: "npm:13.3.2" + sass-loader: "npm:14.1.1" semver: "npm:7.6.0" style-loader: "npm:3.3.4" testing-library-selector: "npm:0.3.1" @@ -4281,7 +4281,7 @@ __metadata: rollup-plugin-esbuild: "npm:5.0.0" rollup-plugin-node-externals: "npm:^5.0.0" rxjs: "npm:7.8.1" - sass-loader: "npm:13.3.2" + sass-loader: "npm:14.1.1" slate: "npm:0.47.9" slate-plain-serializer: "npm:0.7.13" slate-react: "npm:0.22.10" @@ -18689,7 +18689,7 @@ __metadata: rudder-sdk-js: "npm:2.48.1" rxjs: "npm:7.8.1" sass: "npm:1.70.0" - sass-loader: "npm:13.3.2" + sass-loader: "npm:14.1.1" selecto: "npm:1.26.3" semver: "npm:7.6.0" slate: "npm:0.47.9" @@ -27897,19 +27897,19 @@ __metadata: languageName: node linkType: hard -"sass-loader@npm:13.3.2": - version: 13.3.2 - resolution: "sass-loader@npm:13.3.2" +"sass-loader@npm:14.1.1": + version: 14.1.1 + resolution: "sass-loader@npm:14.1.1" dependencies: neo-async: "npm:^2.6.2" peerDependencies: - fibers: ">= 3.1.0" + "@rspack/core": 0.x || 1.x node-sass: ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 sass: ^1.3.0 sass-embedded: "*" webpack: ^5.0.0 peerDependenciesMeta: - fibers: + "@rspack/core": optional: true node-sass: optional: true @@ -27917,7 +27917,9 @@ __metadata: optional: true sass-embedded: optional: true - checksum: 10/3486134c8813660772448bcd73e169fbe180fbaea0b825ec3e8fd1017de026e4e177e4b659a5b7d0f6c1b9f334ebe0134d91c60bd627d8ffff9b21018325a532 + webpack: + optional: true + checksum: 10/6cc0cb8143d04cb462c10efffbab86e9c4ea971bbdbc22d8c01f4ebf774fcf9b4fed775aaec6af23aeb7d440b37dce76cf6f2746ae36421d16ad4182f2220bc8 languageName: node linkType: hard From 1ef2e8d3668eb99942086d20afb42dbe0cea3123 Mon Sep 17 00:00:00 2001 From: Misi Date: Thu, 22 Feb 2024 17:33:27 +0100 Subject: [PATCH 0106/1406] Chore: Allow self-serve for ssoSettingsApi feature toggle (#83140) * Set AllowSelfServe to true ssoSettingsApi * Align ft registry validation with latest changes --- pkg/services/featuremgmt/registry.go | 11 ++++++----- pkg/services/featuremgmt/toggles_gen.json | 10 +++++++--- pkg/services/featuremgmt/toggles_gen_test.go | 4 ++-- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 4845193014..3af6aeb7e7 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -988,11 +988,12 @@ var ( Owner: grafanaSharingSquad, }, { - Name: "ssoSettingsApi", - Description: "Enables the SSO settings API and the OAuth configuration UIs in Grafana", - Stage: FeatureStagePublicPreview, - FrontendOnly: false, - Owner: identityAccessTeam, + Name: "ssoSettingsApi", + Description: "Enables the SSO settings API and the OAuth configuration UIs in Grafana", + Stage: FeatureStagePublicPreview, + AllowSelfServe: true, + FrontendOnly: false, + Owner: identityAccessTeam, }, { Name: "canvasPanelPanZoom", diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index b877d2e9af..35e90a0902 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -480,13 +480,17 @@ { "metadata": { "name": "ssoSettingsApi", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "resourceVersion": "1708503560010", + "creationTimestamp": "2024-02-16T18:36:28Z", + "annotations": { + "grafana.app/updatedTimestamp": "2024-02-21 08:19:20.010283 +0000 UTC" + } }, "spec": { "description": "Enables the SSO settings API and the OAuth configuration UIs in Grafana", "stage": "preview", - "codeowner": "@grafana/identity-access-team" + "codeowner": "@grafana/identity-access-team", + "allowSelfServe": true } }, { diff --git a/pkg/services/featuremgmt/toggles_gen_test.go b/pkg/services/featuremgmt/toggles_gen_test.go index 946a52e407..e1014d7906 100644 --- a/pkg/services/featuremgmt/toggles_gen_test.go +++ b/pkg/services/featuremgmt/toggles_gen_test.go @@ -49,8 +49,8 @@ func TestFeatureToggleFiles(t *testing.T) { if flag.Name != strings.TrimSpace(flag.Name) { t.Errorf("flag Name should not start/end with spaces. See: %s", flag.Name) } - if flag.AllowSelfServe && flag.Stage != FeatureStageGeneralAvailability { - t.Errorf("only allow self-serving GA toggles") + if flag.AllowSelfServe && !(flag.Stage == FeatureStageGeneralAvailability || flag.Stage == FeatureStagePublicPreview || flag.Stage == FeatureStageDeprecated) { + t.Errorf("only allow self-serving GA, PublicPreview and Deprecated toggles") } if flag.Owner == "" { t.Errorf("feature %s does not have an owner. please fill the FeatureFlag.Owner property", flag.Name) From a564c8c4398fe2a763712fa54e21885017151b45 Mon Sep 17 00:00:00 2001 From: George Robinson Date: Thu, 22 Feb 2024 16:57:20 +0000 Subject: [PATCH 0107/1406] Alerting: Keep order of time and mute time intervals consistent (#83257) --- .../api/tooling/definitions/alertmanager.go | 35 ++++++++++--------- .../tooling/definitions/alertmanager_test.go | 29 +++++++-------- pkg/services/ngalert/notifier/config.go | 8 ++--- pkg/services/ngalert/notifier/validation.go | 6 ++-- .../api_alertmanager_configuration_test.go | 14 ++++---- 5 files changed, 47 insertions(+), 45 deletions(-) diff --git a/pkg/services/ngalert/api/tooling/definitions/alertmanager.go b/pkg/services/ngalert/api/tooling/definitions/alertmanager.go index cc3b57713c..aa698d53f2 100644 --- a/pkg/services/ngalert/api/tooling/definitions/alertmanager.go +++ b/pkg/services/ngalert/api/tooling/definitions/alertmanager.go @@ -739,12 +739,12 @@ func (c *GettableApiAlertingConfig) GetReceivers() []*GettableApiReceiver { return c.Receivers } -func (c *GettableApiAlertingConfig) GetTimeIntervals() []config.TimeInterval { return c.TimeIntervals } - func (c *GettableApiAlertingConfig) GetMuteTimeIntervals() []config.MuteTimeInterval { return c.MuteTimeIntervals } +func (c *GettableApiAlertingConfig) GetTimeIntervals() []config.TimeInterval { return c.TimeIntervals } + func (c *GettableApiAlertingConfig) GetRoute() *Route { return c.Route } @@ -802,11 +802,12 @@ func (c *GettableApiAlertingConfig) validate() error { // Config is the top-level configuration for Alertmanager's config files. type Config struct { - Global *config.GlobalConfig `yaml:"global,omitempty" json:"global,omitempty"` - Route *Route `yaml:"route,omitempty" json:"route,omitempty"` - InhibitRules []config.InhibitRule `yaml:"inhibit_rules,omitempty" json:"inhibit_rules,omitempty"` - TimeIntervals []config.TimeInterval `yaml:"time_intervals,omitempty" json:"time_intervals,omitempty"` + Global *config.GlobalConfig `yaml:"global,omitempty" json:"global,omitempty"` + Route *Route `yaml:"route,omitempty" json:"route,omitempty"` + InhibitRules []config.InhibitRule `yaml:"inhibit_rules,omitempty" json:"inhibit_rules,omitempty"` + // MuteTimeIntervals is deprecated and will be removed before Alertmanager 1.0. MuteTimeIntervals []config.MuteTimeInterval `yaml:"mute_time_intervals,omitempty" json:"mute_time_intervals,omitempty"` + TimeIntervals []config.TimeInterval `yaml:"time_intervals,omitempty" json:"time_intervals,omitempty"` Templates []string `yaml:"templates" json:"templates"` } @@ -939,15 +940,6 @@ func (c *Config) UnmarshalJSON(b []byte) error { } tiNames := make(map[string]struct{}) - for _, ti := range c.TimeIntervals { - if ti.Name == "" { - return fmt.Errorf("missing name in time interval") - } - if _, ok := tiNames[ti.Name]; ok { - return fmt.Errorf("time interval %q is not unique", ti.Name) - } - tiNames[ti.Name] = struct{}{} - } for _, mt := range c.MuteTimeIntervals { if mt.Name == "" { return fmt.Errorf("missing name in mute time interval") @@ -957,6 +949,15 @@ func (c *Config) UnmarshalJSON(b []byte) error { } tiNames[mt.Name] = struct{}{} } + for _, ti := range c.TimeIntervals { + if ti.Name == "" { + return fmt.Errorf("missing name in time interval") + } + if _, ok := tiNames[ti.Name]; ok { + return fmt.Errorf("time interval %q is not unique", ti.Name) + } + tiNames[ti.Name] = struct{}{} + } return checkTimeInterval(c.Route, tiNames) } @@ -988,12 +989,12 @@ func (c *PostableApiAlertingConfig) GetReceivers() []*PostableApiReceiver { return c.Receivers } -func (c *PostableApiAlertingConfig) GetTimeIntervals() []config.TimeInterval { return c.TimeIntervals } - func (c *PostableApiAlertingConfig) GetMuteTimeIntervals() []config.MuteTimeInterval { return c.MuteTimeIntervals } +func (c *PostableApiAlertingConfig) GetTimeIntervals() []config.TimeInterval { return c.TimeIntervals } + func (c *PostableApiAlertingConfig) GetRoute() *Route { return c.Route } diff --git a/pkg/services/ngalert/api/tooling/definitions/alertmanager_test.go b/pkg/services/ngalert/api/tooling/definitions/alertmanager_test.go index e38506ca5d..4f0504e350 100644 --- a/pkg/services/ngalert/api/tooling/definitions/alertmanager_test.go +++ b/pkg/services/ngalert/api/tooling/definitions/alertmanager_test.go @@ -485,14 +485,14 @@ func Test_ConfigUnmashaling(t *testing.T) { err error }{ { - desc: "missing time interval name should error", - err: errors.New("missing name in time interval"), + desc: "missing mute time interval name should error", + err: errors.New("missing name in mute time interval"), input: ` { "route": { "receiver": "grafana-default-email" }, - "time_intervals": [ + "mute_time_intervals": [ { "name": "", "time_intervals": [ @@ -527,15 +527,16 @@ func Test_ConfigUnmashaling(t *testing.T) { ] } `, - }, { - desc: "missing mute time interval name should error", - err: errors.New("missing name in mute time interval"), + }, + { + desc: "missing time interval name should error", + err: errors.New("missing name in time interval"), input: ` { "route": { "receiver": "grafana-default-email" }, - "mute_time_intervals": [ + "time_intervals": [ { "name": "", "time_intervals": [ @@ -572,14 +573,14 @@ func Test_ConfigUnmashaling(t *testing.T) { `, }, { - desc: "duplicate time interval names should error", - err: errors.New("time interval \"test1\" is not unique"), + desc: "duplicate mute time interval names should error", + err: errors.New("mute time interval \"test1\" is not unique"), input: ` { "route": { "receiver": "grafana-default-email" }, - "time_intervals": [ + "mute_time_intervals": [ { "name": "test1", "time_intervals": [ @@ -629,14 +630,14 @@ func Test_ConfigUnmashaling(t *testing.T) { `, }, { - desc: "duplicate mute time interval names should error", - err: errors.New("mute time interval \"test1\" is not unique"), + desc: "duplicate time interval names should error", + err: errors.New("time interval \"test1\" is not unique"), input: ` { "route": { "receiver": "grafana-default-email" }, - "mute_time_intervals": [ + "time_intervals": [ { "name": "test1", "time_intervals": [ @@ -687,7 +688,7 @@ func Test_ConfigUnmashaling(t *testing.T) { }, { desc: "duplicate time and mute time interval names should error", - err: errors.New("mute time interval \"test1\" is not unique"), + err: errors.New("time interval \"test1\" is not unique"), input: ` { "route": { diff --git a/pkg/services/ngalert/notifier/config.go b/pkg/services/ngalert/notifier/config.go index 43b70760a2..0ea24acf64 100644 --- a/pkg/services/ngalert/notifier/config.go +++ b/pkg/services/ngalert/notifier/config.go @@ -111,14 +111,14 @@ func (a AlertingConfiguration) InhibitRules() []alertingNotify.InhibitRule { return a.alertmanagerConfig.InhibitRules } -func (a AlertingConfiguration) TimeIntervals() []alertingNotify.TimeInterval { - return a.alertmanagerConfig.TimeIntervals -} - func (a AlertingConfiguration) MuteTimeIntervals() []alertingNotify.MuteTimeInterval { return a.alertmanagerConfig.MuteTimeIntervals } +func (a AlertingConfiguration) TimeIntervals() []alertingNotify.TimeInterval { + return a.alertmanagerConfig.TimeIntervals +} + func (a AlertingConfiguration) Receivers() []*alertingNotify.APIReceiver { return a.receivers } diff --git a/pkg/services/ngalert/notifier/validation.go b/pkg/services/ngalert/notifier/validation.go index 84964214a2..48e0f20d2a 100644 --- a/pkg/services/ngalert/notifier/validation.go +++ b/pkg/services/ngalert/notifier/validation.go @@ -27,8 +27,8 @@ type staticValidator struct { // apiAlertingConfig contains the methods required to validate NotificationSettings and create autogen routes. type apiAlertingConfig[R receiver] interface { GetReceivers() []R - GetTimeIntervals() []config.TimeInterval GetMuteTimeIntervals() []config.MuteTimeInterval + GetTimeIntervals() []config.TimeInterval GetRoute() *definitions.Route } @@ -44,10 +44,10 @@ func NewNotificationSettingsValidator[R receiver](am apiAlertingConfig[R]) Notif } availableTimeIntervals := make(map[string]struct{}) - for _, interval := range am.GetTimeIntervals() { + for _, interval := range am.GetMuteTimeIntervals() { availableTimeIntervals[interval.Name] = struct{}{} } - for _, interval := range am.GetMuteTimeIntervals() { + for _, interval := range am.GetTimeIntervals() { availableTimeIntervals[interval.Name] = struct{}{} } diff --git a/pkg/tests/api/alerting/api_alertmanager_configuration_test.go b/pkg/tests/api/alerting/api_alertmanager_configuration_test.go index f4e0960493..52a2ad4694 100644 --- a/pkg/tests/api/alerting/api_alertmanager_configuration_test.go +++ b/pkg/tests/api/alerting/api_alertmanager_configuration_test.go @@ -194,7 +194,10 @@ func TestIntegrationAlertmanagerConfiguration(t *testing.T) { }, }, }, { - name: "configuration with time intervals", + // TODO: Mute time intervals is deprecated in Alertmanager and scheduled to be + // removed before version 1.0. Remove this test when support for mute time + // intervals is removed. + name: "configuration with mute time intervals", cfg: apimodels.PostableUserConfig{ AlertmanagerConfig: apimodels.PostableApiAlertingConfig{ Config: apimodels.Config{ @@ -204,7 +207,7 @@ func TestIntegrationAlertmanagerConfiguration(t *testing.T) { MuteTimeIntervals: []string{"weekends"}, }}, }, - TimeIntervals: []config.TimeInterval{{ + MuteTimeIntervals: []config.MuteTimeInterval{{ Name: "weekends", TimeIntervals: []timeinterval.TimeInterval{{ Weekdays: []timeinterval.WeekdayRange{{ @@ -224,10 +227,7 @@ func TestIntegrationAlertmanagerConfiguration(t *testing.T) { }, }, }, { - // TODO: Mute time intervals is deprecated in Alertmanager and scheduled to be - // removed before version 1.0. Remove this test when support for mute time - // intervals is removed. - name: "configuration with mute time intervals", + name: "configuration with time intervals", cfg: apimodels.PostableUserConfig{ AlertmanagerConfig: apimodels.PostableApiAlertingConfig{ Config: apimodels.Config{ @@ -237,7 +237,7 @@ func TestIntegrationAlertmanagerConfiguration(t *testing.T) { MuteTimeIntervals: []string{"weekends"}, }}, }, - MuteTimeIntervals: []config.MuteTimeInterval{{ + TimeIntervals: []config.TimeInterval{{ Name: "weekends", TimeIntervals: []timeinterval.TimeInterval{{ Weekdays: []timeinterval.WeekdayRange{{ From 28fa2849dfa0659add5c1224a1be2963631478c1 Mon Sep 17 00:00:00 2001 From: Haris Rozajac <58232930+harisrozajac@users.noreply.github.com> Date: Thu, 22 Feb 2024 10:34:21 -0700 Subject: [PATCH 0108/1406] Dashboard-Scene: View panel as table in edit mode (#83077) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * WIP: working functionality * betterer * Fully working: Alerts show up, toggling table view doesn't update viz type in options pane * betterer * improve * betterer * Refactoring a bit * wrong step * move data provider to vizPanel * Update * update * More refactorings * Fix InspectJsonTab tests (except 1); remove obsolete PanelControls * Fixed test * Update * minor fix --------- Co-authored-by: Torkel Ödegaard --- .../inspect/InspectJsonTab.test.tsx | 80 +++++++---- .../inspect/InspectJsonTab.tsx | 17 ++- .../PanelDataAlertingTab.test.tsx | 4 +- .../PanelDataTransformationsTab.test.tsx | 2 +- .../panel-edit/PanelEditControls.tsx | 33 +++++ .../panel-edit/PanelEditor.tsx | 21 +-- .../panel-edit/PanelEditorRenderer.tsx | 7 - .../panel-edit/ShareDataProvider.tsx | 35 +++++ .../panel-edit/VizPanelManager.test.tsx | 13 +- .../panel-edit/VizPanelManager.tsx | 126 ++++++++++++++---- .../scene/DashboardControls.tsx | 2 + .../features/dashboard-scene/utils/utils.ts | 10 +- 12 files changed, 251 insertions(+), 99 deletions(-) create mode 100644 public/app/features/dashboard-scene/panel-edit/PanelEditControls.tsx create mode 100644 public/app/features/dashboard-scene/panel-edit/ShareDataProvider.tsx diff --git a/public/app/features/dashboard-scene/inspect/InspectJsonTab.test.tsx b/public/app/features/dashboard-scene/inspect/InspectJsonTab.test.tsx index 0ba2851eae..f155cb206f 100644 --- a/public/app/features/dashboard-scene/inspect/InspectJsonTab.test.tsx +++ b/public/app/features/dashboard-scene/inspect/InspectJsonTab.test.tsx @@ -1,12 +1,21 @@ -import { FieldType, getDefaultTimeRange, LoadingState, standardTransformersRegistry, toDataFrame } from '@grafana/data'; +import { of } from 'rxjs'; + +import { + FieldType, + getDefaultTimeRange, + LoadingState, + PanelData, + standardTransformersRegistry, + toDataFrame, +} from '@grafana/data'; import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks'; -import { setPluginImportUtils } from '@grafana/runtime'; +import { setPluginImportUtils, setRunRequest } from '@grafana/runtime'; import { SceneCanvasText, - SceneDataNode, SceneDataTransformer, SceneGridItem, SceneGridLayout, + SceneQueryRunner, VizPanel, } from '@grafana/scenes'; import * as libpanels from 'app/features/library-panels/state/api'; @@ -34,8 +43,45 @@ jest.mock('@grafana/runtime', () => ({ getPluginLinkExtensions: jest.fn(() => ({ extensions: [], })), + getDataSourceSrv: () => { + return { + get: jest.fn().mockResolvedValue({ + getRef: () => ({ uid: 'ds1' }), + }), + getInstanceSettings: jest.fn().mockResolvedValue({ uid: 'ds1' }), + }; + }, })); +const runRequestMock = jest.fn().mockReturnValue( + of({ + state: LoadingState.Done, + timeRange: getDefaultTimeRange(), + series: [ + toDataFrame({ + fields: [{ name: 'value', type: FieldType.number, values: [1, 2, 3] }], + }), + ], + request: { + app: 'dashboard', + requestId: 'request-id', + dashboardUID: 'asd', + interval: '1s', + panelId: 1, + range: getDefaultTimeRange(), + targets: [], + timezone: 'utc', + intervalMs: 1000, + startTime: 1, + scopedVars: { + __sceneObject: { value: new SceneCanvasText({ text: 'asd' }) }, + }, + }, + }) +); + +setRunRequest(runRequestMock); + describe('InspectJsonTab', () => { it('Can show panel json', async () => { const { tab } = await buildTestScene(); @@ -121,31 +167,9 @@ function buildTestPanel() { }, }, ], - $data: new SceneDataNode({ - data: { - state: LoadingState.Done, - series: [ - toDataFrame({ - fields: [{ name: 'value', type: FieldType.number, values: [1, 2, 3] }], - }), - ], - timeRange: getDefaultTimeRange(), - request: { - app: 'dashboard', - requestId: 'request-id', - dashboardUID: 'asd', - interval: '1s', - panelId: 1, - range: getDefaultTimeRange(), - targets: [], - timezone: 'utc', - intervalMs: 1000, - startTime: 1, - scopedVars: { - __sceneObject: { value: new SceneCanvasText({ text: 'asd' }) }, - }, - }, - }, + $data: new SceneQueryRunner({ + datasource: { uid: 'abcdef' }, + queries: [{ refId: 'A' }], }), }), }); diff --git a/public/app/features/dashboard-scene/inspect/InspectJsonTab.tsx b/public/app/features/dashboard-scene/inspect/InspectJsonTab.tsx index f8abf99f08..74725d1d6c 100644 --- a/public/app/features/dashboard-scene/inspect/InspectJsonTab.tsx +++ b/public/app/features/dashboard-scene/inspect/InspectJsonTab.tsx @@ -26,6 +26,7 @@ import { InspectTab } from 'app/features/inspector/types'; import { getPrettyJSON } from 'app/features/inspector/utils/utils'; import { reportPanelInspectInteraction } from 'app/features/search/page/reporting'; +import { VizPanelManager } from '../panel-edit/VizPanelManager'; import { LibraryVizPanel } from '../scene/LibraryVizPanel'; import { PanelRepeaterGridItem } from '../scene/PanelRepeaterGridItem'; import { buildGridItemForPanel } from '../serialization/transformSaveModelToScene'; @@ -60,7 +61,7 @@ export class InspectJsonTab extends SceneObjectBase { public getOptions(): Array> { const panel = this.state.panelRef.resolve(); - const dataProvider = panel.state.$data; + const dataProvider = panel.state.$data ?? panel.parent?.state.$data; const options: Array> = [ { @@ -201,10 +202,14 @@ function getJsonText(show: ShowContent, panel: VizPanel): string { case 'panel-json': { reportPanelInspectInteraction(InspectTab.JSON, 'panelData'); - if (panel.parent instanceof SceneGridItem || panel.parent instanceof PanelRepeaterGridItem) { - objToStringify = gridItemToPanel(panel.parent); - } else if (panel.parent instanceof LibraryVizPanel) { + const parent = panel.parent!; + + if (parent instanceof SceneGridItem || parent instanceof PanelRepeaterGridItem) { + objToStringify = gridItemToPanel(parent); + } else if (parent instanceof LibraryVizPanel) { objToStringify = libraryPanelChildToLegacyRepresentation(panel); + } else if (parent instanceof VizPanelManager) { + objToStringify = parent.getPanelSaveModel(); } break; } @@ -246,18 +251,22 @@ function libraryPanelChildToLegacyRepresentation(panel: VizPanel<{}, {}>) { if (!(panel.parent instanceof LibraryVizPanel)) { throw 'Panel not child of LibraryVizPanel'; } + if (!(panel.parent.parent instanceof SceneGridItem)) { throw 'LibraryPanel not child of SceneGridItem'; } + const gridItem = panel.parent.parent; const libraryPanelObj = gridItemToPanel(gridItem); const panelObj = vizPanelToPanel(panel); + panelObj.gridPos = { x: gridItem.state.x || 0, y: gridItem.state.y || 0, h: gridItem.state.height || 0, w: gridItem.state.width || 0, }; + return { ...libraryPanelObj, ...panelObj }; } diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataAlertingTab.test.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataAlertingTab.test.tsx index d79fdaee32..1786c59882 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataAlertingTab.test.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataAlertingTab.test.tsx @@ -349,8 +349,8 @@ async function clickNewButton() { function createModel(dashboard: DashboardModel) { const scene = createDashboardSceneFromDashboardModel(dashboard); - const vizPanel = findVizPanelByKey(scene, getVizPanelKeyForPanelId(34)); - const model = new PanelDataAlertingTab(new VizPanelManager(vizPanel!.clone())); + const vizPanel = findVizPanelByKey(scene, getVizPanelKeyForPanelId(34))!; + const model = new PanelDataAlertingTab(VizPanelManager.createFor(vizPanel)); jest.spyOn(utils, 'getDashboardSceneFor').mockReturnValue(scene); return model; } diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataTransformationsTab.test.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataTransformationsTab.test.tsx index 010b1b3b91..2b9766c54d 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataTransformationsTab.test.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataTransformationsTab.test.tsx @@ -174,7 +174,7 @@ const setupVizPanelManger = (panelId: string) => { const scene = transformSaveModelToScene({ dashboard: testDashboard as unknown as DashboardDataDTO, meta: {} }); const panel = findVizPanelByKey(scene, panelId)!; - const vizPanelManager = new VizPanelManager(panel.clone()); + const vizPanelManager = VizPanelManager.createFor(panel); // The following happens on DahsboardScene activation. For the needs of this test this activation aint needed hence we hand-call it // @ts-expect-error diff --git a/public/app/features/dashboard-scene/panel-edit/PanelEditControls.tsx b/public/app/features/dashboard-scene/panel-edit/PanelEditControls.tsx new file mode 100644 index 0000000000..6a2478a0ac --- /dev/null +++ b/public/app/features/dashboard-scene/panel-edit/PanelEditControls.tsx @@ -0,0 +1,33 @@ +import React from 'react'; + +import { selectors } from '@grafana/e2e-selectors'; +import { config } from '@grafana/runtime'; +import { InlineSwitch } from '@grafana/ui'; + +import { PanelEditor } from './PanelEditor'; + +export interface Props { + panelEditor: PanelEditor; +} + +export function PanelEditControls({ panelEditor }: Props) { + const vizManager = panelEditor.state.vizManager; + const { panel, tableView } = vizManager.useState(); + const skipDataQuery = config.panels[panel.state.pluginId].skipDataQuery; + + return ( + <> + {!skipDataQuery && ( + vizManager.toggleTableView()} + aria-label="toggle-table-view" + data-testid={selectors.components.PanelEditor.toggleTableView} + /> + )} + + ); +} diff --git a/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx b/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx index 9184064160..3d0655a2d7 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx @@ -2,14 +2,9 @@ import * as H from 'history'; import { NavIndex } from '@grafana/data'; import { config, locationService } from '@grafana/runtime'; -import { SceneGridItem, SceneObject, SceneObjectBase, SceneObjectState, VizPanel } from '@grafana/scenes'; +import { SceneObjectBase, SceneObjectState, VizPanel } from '@grafana/scenes'; -import { - findVizPanelByKey, - getDashboardSceneFor, - getPanelIdForVizPanel, - getVizPanelKeyForPanelId, -} from '../utils/utils'; +import { getDashboardSceneFor, getPanelIdForVizPanel } from '../utils/utils'; import { PanelDataPane } from './PanelDataPane/PanelDataPane'; import { PanelEditorRenderer } from './PanelEditorRenderer'; @@ -17,7 +12,6 @@ import { PanelOptionsPane } from './PanelOptionsPane'; import { VizPanelManager } from './VizPanelManager'; export interface PanelEditorState extends SceneObjectState { - controls?: SceneObject[]; isDirty?: boolean; panelId: number; optionsPane: PanelOptionsPane; @@ -32,7 +26,6 @@ export class PanelEditor extends SceneObjectBase { public constructor(state: PanelEditorState) { super(state); - this.addActivationHandler(this._activationHandler.bind(this)); } @@ -90,25 +83,19 @@ export class PanelEditor extends SceneObjectBase { public commitChanges() { const dashboard = getDashboardSceneFor(this); - const sourcePanel = findVizPanelByKey(dashboard.state.body, getVizPanelKeyForPanelId(this.state.panelId)); if (!dashboard.state.isEditing) { dashboard.onEnterEditMode(); } - if (sourcePanel!.parent instanceof SceneGridItem) { - sourcePanel!.parent.setState({ body: this.state.vizManager.state.panel.clone() }); - } + this.state.vizManager.commitChanges(); } } export function buildPanelEditScene(panel: VizPanel): PanelEditor { - const panelClone = panel.clone(); - const vizPanelMgr = new VizPanelManager(panelClone); - return new PanelEditor({ panelId: getPanelIdForVizPanel(panel), optionsPane: new PanelOptionsPane({}), - vizManager: vizPanelMgr, + vizManager: VizPanelManager.createFor(panel), }); } diff --git a/public/app/features/dashboard-scene/panel-edit/PanelEditorRenderer.tsx b/public/app/features/dashboard-scene/panel-edit/PanelEditorRenderer.tsx index 2b81f59779..9850f78751 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelEditorRenderer.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelEditorRenderer.tsx @@ -127,13 +127,6 @@ function getStyles(theme: GrafanaTheme2) { minHeight: 0, gap: '8px', }), - controls: css({ - display: 'flex', - flexWrap: 'wrap', - alignItems: 'center', - gap: theme.spacing(1), - padding: theme.spacing(2, 0, 2, 2), - }), optionsPane: css({ flexDirection: 'column', borderLeft: `1px solid ${theme.colors.border.weak}`, diff --git a/public/app/features/dashboard-scene/panel-edit/ShareDataProvider.tsx b/public/app/features/dashboard-scene/panel-edit/ShareDataProvider.tsx new file mode 100644 index 0000000000..dcf9ca08a9 --- /dev/null +++ b/public/app/features/dashboard-scene/panel-edit/ShareDataProvider.tsx @@ -0,0 +1,35 @@ +import { SceneDataProvider, SceneDataState, SceneObjectBase } from '@grafana/scenes'; + +export class ShareDataProvider extends SceneObjectBase implements SceneDataProvider { + public constructor(private _source: SceneDataProvider) { + super(_source.state); + this.addActivationHandler(() => this.activationHandler()); + } + + private activationHandler() { + this._subs.add(this._source.subscribeToState((state) => this.setState({ data: state.data }))); + this.setState(this._source.state); + } + + public setContainerWidth(width: number) { + if (this.state.$data && this.state.$data.setContainerWidth) { + this.state.$data.setContainerWidth(width); + } + } + + public isDataReadyToDisplay() { + if (!this._source.isDataReadyToDisplay) { + return true; + } + + return this._source.isDataReadyToDisplay?.(); + } + + public cancelQuery() { + this._source.cancelQuery?.(); + } + + public getResultsStream() { + return this._source.getResultsStream!(); + } +} diff --git a/public/app/features/dashboard-scene/panel-edit/VizPanelManager.test.tsx b/public/app/features/dashboard-scene/panel-edit/VizPanelManager.test.tsx index b71340323b..a33670c822 100644 --- a/public/app/features/dashboard-scene/panel-edit/VizPanelManager.test.tsx +++ b/public/app/features/dashboard-scene/panel-edit/VizPanelManager.test.tsx @@ -2,7 +2,7 @@ import { map, of } from 'rxjs'; import { DataQueryRequest, DataSourceApi, DataSourceInstanceSettings, LoadingState, PanelData } from '@grafana/data'; import { locationService } from '@grafana/runtime'; -import { SceneDataTransformer, SceneQueryRunner, VizPanel } from '@grafana/scenes'; +import { SceneQueryRunner, VizPanel } from '@grafana/scenes'; import { DataQuery, DataSourceJsonData, DataSourceRef } from '@grafana/schema'; import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; import { InspectTab } from 'app/features/inspector/types'; @@ -140,7 +140,7 @@ jest.mock('@grafana/runtime', () => ({ })); describe('VizPanelManager', () => { - describe('changePluginType', () => { + describe('When changing plugin', () => { it('Should successfully change from one viz type to another', () => { const { vizPanelManager } = setupTest('panel-1'); expect(vizPanelManager.state.panel.state.pluginId).toBe('timeseries'); @@ -169,7 +169,7 @@ describe('VizPanelManager', () => { }, }); - const vizPanelManager = new VizPanelManager(vizPanel); + const vizPanelManager = VizPanelManager.createFor(vizPanel); expect(vizPanelManager.state.panel.state.fieldConfig.defaults.custom).toBe('Custom'); expect(vizPanelManager.state.panel.state.fieldConfig.overrides).toBe(overrides); @@ -193,7 +193,7 @@ describe('VizPanelManager', () => { fieldConfig: { defaults: { custom: 'Custom' }, overrides: [] }, }); - const vizPanelManager = new VizPanelManager(vizPanel); + const vizPanelManager = VizPanelManager.createFor(vizPanel); vizPanelManager.changePluginType('timeseries'); //@ts-ignore @@ -599,7 +599,6 @@ describe('VizPanelManager', () => { }, ]); - expect(vizPanelManager.panelData).toBeInstanceOf(SceneDataTransformer); expect(vizPanelManager.queryRunner.state.queries[0].panelId).toEqual(panelWithTransformations.id); // Changing dashboard query to a panel with queries only @@ -613,7 +612,6 @@ describe('VizPanelManager', () => { }, ]); - expect(vizPanelManager.panelData).toBeInstanceOf(SceneDataTransformer); expect(vizPanelManager.queryRunner.state.queries[0].panelId).toBe(panelWithQueriesOnly.id); }); }); @@ -624,8 +622,7 @@ const setupTest = (panelId: string) => { const scene = transformSaveModelToScene({ dashboard: testDashboard, meta: {} }); const panel = findVizPanelByKey(scene, panelId)!; - const vizPanelManager = new VizPanelManager(panel.clone()); - + const vizPanelManager = VizPanelManager.createFor(panel); // The following happens on DahsboardScene activation. For the needs of this test this activation aint needed hence we hand-call it // @ts-expect-error getDashboardSrv().setCurrent(new DashboardModelCompatibilityWrapper(scene)); diff --git a/public/app/features/dashboard-scene/panel-edit/VizPanelManager.tsx b/public/app/features/dashboard-scene/panel-edit/VizPanelManager.tsx index 6a87c2c64c..cc2919c4b4 100644 --- a/public/app/features/dashboard-scene/panel-edit/VizPanelManager.tsx +++ b/public/app/features/dashboard-scene/panel-edit/VizPanelManager.tsx @@ -21,10 +21,12 @@ import { DeepPartial, SceneQueryRunner, sceneGraph, - SceneDataProvider, SceneDataTransformer, + PanelBuilders, + SceneGridItem, + SceneObjectRef, } from '@grafana/scenes'; -import { DataQuery, DataTransformerConfig } from '@grafana/schema'; +import { DataQuery, DataTransformerConfig, Panel } from '@grafana/schema'; import { useStyles2 } from '@grafana/ui'; import { getPluginVersion } from 'app/features/dashboard/state/PanelModel'; import { getLastUsedDatasourceFromStorage } from 'app/features/dashboard/utils/dashboard'; @@ -34,12 +36,21 @@ import { GrafanaQuery } from 'app/plugins/datasource/grafana/types'; import { QueryGroupOptions } from 'app/types'; import { PanelTimeRange, PanelTimeRangeState } from '../scene/PanelTimeRange'; +import { gridItemToPanel } from '../serialization/transformSceneToSaveModel'; import { getDashboardSceneFor, getPanelIdForVizPanel, getQueryRunnerFor } from '../utils/utils'; interface VizPanelManagerState extends SceneObjectState { panel: VizPanel; + sourcePanel: SceneObjectRef; datasource?: DataSourceApi; dsSettings?: DataSourceInstanceSettings; + tableView?: VizPanel; +} + +export enum DisplayMode { + Fill = 0, + Fit = 1, + Exact = 2, } // VizPanelManager serves as an API to manipulate VizPanel state from the outside. It allows panel type, options and data manipulation. @@ -49,18 +60,29 @@ export class VizPanelManager extends SceneObjectBase { { options: DeepPartial<{}>; fieldConfig: FieldConfigSource> } | undefined > = {}; - public constructor(panel: VizPanel) { - super({ panel }); - + public constructor(state: VizPanelManagerState) { + super(state); this.addActivationHandler(() => this._onActivate()); } + /** + * Will clone the source panel and move the data provider to + * live on the VizPanelManager level instead of the VizPanel level + */ + public static createFor(sourcePanel: VizPanel) { + return new VizPanelManager({ + panel: sourcePanel.clone({ $data: undefined }), + $data: sourcePanel.state.$data?.clone(), + sourcePanel: sourcePanel.getRef(), + }); + } + private _onActivate() { this.loadDataSource(); } private async loadDataSource() { - const dataObj = this.state.panel.state.$data; + const dataObj = this.state.$data; if (!dataObj) { return; @@ -96,7 +118,7 @@ export class VizPanelManager extends SceneObjectBase { } } - public changePluginType(pluginType: string) { + public changePluginType(pluginId: string) { const { options: prevOptions, fieldConfig: prevFieldConfig, @@ -105,16 +127,19 @@ export class VizPanelManager extends SceneObjectBase { } = sceneUtils.cloneSceneObjectState(this.state.panel.state); // clear custom options - let newFieldConfig = { ...prevFieldConfig }; - newFieldConfig.defaults = { - ...newFieldConfig.defaults, - custom: {}, + let newFieldConfig: FieldConfigSource = { + defaults: { + ...prevFieldConfig.defaults, + custom: {}, + }, + overrides: filterFieldConfigOverrides(prevFieldConfig.overrides, isStandardFieldProp), }; - newFieldConfig.overrides = filterFieldConfigOverrides(newFieldConfig.overrides, isStandardFieldProp); this._cachedPluginOptions[prevPluginId] = { options: prevOptions, fieldConfig: prevFieldConfig }; - const cachedOptions = this._cachedPluginOptions[pluginType]?.options; - const cachedFieldConfig = this._cachedPluginOptions[pluginType]?.fieldConfig; + + const cachedOptions = this._cachedPluginOptions[pluginId]?.options; + const cachedFieldConfig = this._cachedPluginOptions[pluginId]?.fieldConfig; + if (cachedFieldConfig) { newFieldConfig = restoreCustomOverrideRules(newFieldConfig, cachedFieldConfig); } @@ -122,19 +147,19 @@ export class VizPanelManager extends SceneObjectBase { const newPanel = new VizPanel({ options: cachedOptions ?? {}, fieldConfig: newFieldConfig, - pluginId: pluginType, + pluginId: pluginId, ...restOfOldState, }); // When changing from non-data to data panel, we need to add a new data provider - if (!restOfOldState.$data && !config.panels[pluginType].skipDataQuery) { + if (!this.state.$data && !config.panels[pluginId].skipDataQuery) { let ds = getLastUsedDatasourceFromStorage(getDashboardSceneFor(this).state.uid!)?.datasourceUid; if (!ds) { ds = config.defaultDatasource; } - newPanel.setState({ + this.setState({ $data: new SceneDataTransformer({ $data: new SceneQueryRunner({ datasource: { @@ -153,8 +178,9 @@ export class VizPanelManager extends SceneObjectBase { options: newPanel.state.options, fieldConfig: newPanel.state.fieldConfig, id: 1, - type: pluginType, + type: pluginId, }; + const newOptions = newPlugin?.onPanelTypeChanged?.(panel, prevPluginId, prevOptions, prevFieldConfig); if (newOptions) { newPanel.onOptionsChange(newOptions, true); @@ -208,14 +234,17 @@ export class VizPanelManager extends SceneObjectBase { if (options.maxDataPoints !== dataObj.state.maxDataPoints) { dataObjStateUpdate.maxDataPoints = options.maxDataPoints ?? undefined; } + if (options.minInterval !== dataObj.state.minInterval && options.minInterval !== null) { dataObjStateUpdate.minInterval = options.minInterval; } + if (options.timeRange) { timeRangeObjStateUpdate.timeFrom = options.timeRange.from ?? undefined; timeRangeObjStateUpdate.timeShift = options.timeRange.shift ?? undefined; timeRangeObjStateUpdate.hideTimeOverride = options.timeRange.hide; } + if (timeRangeObj instanceof PanelTimeRange) { if (timeRangeObjStateUpdate.timeFrom !== undefined || timeRangeObjStateUpdate.timeShift !== undefined) { // update time override @@ -264,35 +293,76 @@ export class VizPanelManager extends SceneObjectBase { get queryRunner(): SceneQueryRunner { // Panel data object is always SceneQueryRunner wrapped in a SceneDataTransformer - const runner = getQueryRunnerFor(this.state.panel); + const runner = getQueryRunnerFor(this); if (!runner) { throw new Error('Query runner not found'); } + return runner; } get dataTransformer(): SceneDataTransformer { - const provider = this.state.panel.state.$data; + const provider = this.state.$data; if (!provider || !(provider instanceof SceneDataTransformer)) { throw new Error('Could not find SceneDataTransformer for panel'); } return provider; } - get panelData(): SceneDataProvider { - return this.state.panel.state.$data!; + public toggleTableView() { + if (this.state.tableView) { + this.setState({ tableView: undefined }); + return; + } + + this.setState({ + tableView: PanelBuilders.table() + .setTitle('') + .setOption('showTypeIcons', true) + .setOption('showHeader', true) + .build(), + }); + } + + public commitChanges() { + const sourcePanel = this.state.sourcePanel.resolve(); + + if (sourcePanel.parent instanceof SceneGridItem) { + sourcePanel.parent.setState({ + body: this.state.panel.clone({ + $data: this.state.$data?.clone(), + }), + }); + } + } + + /** + * Used from inspect json tab to view the current persisted model + */ + public getPanelSaveModel(): Panel | object { + const sourcePanel = this.state.sourcePanel.resolve(); + + if (sourcePanel.parent instanceof SceneGridItem) { + const parentClone = sourcePanel.parent.clone({ + body: this.state.panel.clone({ + $data: this.state.$data?.clone(), + }), + }); + + return gridItemToPanel(parentClone); + } + + return { error: 'Unsupported panel parent' }; } public static Component = ({ model }: SceneComponentProps) => { - const { panel } = model.useState(); + const { panel, tableView } = model.useState(); const styles = useStyles2(getStyles); - return ( -
- -
- ); + const panelToShow = tableView ?? panel; + + return
{}
; }; } diff --git a/public/app/features/dashboard-scene/scene/DashboardControls.tsx b/public/app/features/dashboard-scene/scene/DashboardControls.tsx index 2758c65bcf..55e5e370eb 100644 --- a/public/app/features/dashboard-scene/scene/DashboardControls.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardControls.tsx @@ -13,6 +13,7 @@ import { } from '@grafana/scenes'; import { Box, Stack, useStyles2 } from '@grafana/ui'; +import { PanelEditControls } from '../panel-edit/PanelEditControls'; import { getDashboardSceneFor } from '../utils/utils'; import { DashboardLinksControls } from './DashboardLinksControls'; @@ -51,6 +52,7 @@ function DashboardControlsRenderer({ model }: SceneComponentProps {!editPanel && } + {editPanel && } {!hideTimeControls && ( diff --git a/public/app/features/dashboard-scene/utils/utils.ts b/public/app/features/dashboard-scene/utils/utils.ts index d964c15ca1..286659e7bb 100644 --- a/public/app/features/dashboard-scene/utils/utils.ts +++ b/public/app/features/dashboard-scene/utils/utils.ts @@ -155,12 +155,14 @@ export function getQueryRunnerFor(sceneObject: SceneObject | undefined): SceneQu return undefined; } - if (sceneObject.state.$data instanceof SceneQueryRunner) { - return sceneObject.state.$data; + const dataProvider = sceneObject.state.$data ?? sceneObject.parent?.state.$data; + + if (dataProvider instanceof SceneQueryRunner) { + return dataProvider; } - if (sceneObject.state.$data instanceof SceneDataTransformer) { - return getQueryRunnerFor(sceneObject.state.$data); + if (dataProvider instanceof SceneDataTransformer) { + return getQueryRunnerFor(dataProvider); } return undefined; From d503107d7a7cb2f64f92bfb97c2b23ee6df9c35a Mon Sep 17 00:00:00 2001 From: Andreas Christou Date: Thu, 22 Feb 2024 18:59:06 +0000 Subject: [PATCH 0109/1406] Docker: Add workaround for building on `arm64` (#83242) Add workaround for building on arm64 --- Dockerfile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Dockerfile b/Dockerfile index 9f41c4c048..99d25ac1d5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -38,6 +38,9 @@ ARG GO_BUILD_TAGS="oss" ARG WIRE_TAGS="oss" ARG BINGO="true" +# This is required to allow building on arm64 due to https://github.com/golang/go/issues/22040 +RUN apk add --no-cache binutils-gold + # Install build dependencies RUN if grep -i -q alpine /etc/issue; then \ apk add --no-cache gcc g++ make git; \ From 086e60488fef9e76781e37b1d2af7815f28a104d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 22 Feb 2024 20:11:09 +0100 Subject: [PATCH 0110/1406] DataTrails: Remove the adhoc filters label (#83237) --- public/app/features/trails/DataTrail.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/public/app/features/trails/DataTrail.tsx b/public/app/features/trails/DataTrail.tsx index f3dba09403..69424e94b5 100644 --- a/public/app/features/trails/DataTrail.tsx +++ b/public/app/features/trails/DataTrail.tsx @@ -1,7 +1,7 @@ import { css } from '@emotion/css'; import React from 'react'; -import { AdHocVariableFilter, GrafanaTheme2 } from '@grafana/data'; +import { AdHocVariableFilter, GrafanaTheme2, VariableHide } from '@grafana/data'; import { locationService } from '@grafana/runtime'; import { AdHocFiltersVariable, @@ -211,6 +211,7 @@ function getVariableSet(initialDS?: string, metric?: string, initialFilters?: Ad new AdHocFiltersVariable({ name: VAR_FILTERS, datasource: trailDS, + hide: VariableHide.hideLabel, layout: 'vertical', filters: initialFilters ?? [], baseFilters: getBaseFiltersForMetric(metric), From 455bccea2a9932b84e5e6e66a7c4041cdc99611b Mon Sep 17 00:00:00 2001 From: Ezequiel Victorero Date: Thu, 22 Feb 2024 16:56:31 -0300 Subject: [PATCH 0111/1406] Scenes: Render old snapshots (#82277) --- .../PanelDataAlertingTab.test.tsx | 1 + .../transformSaveModelToScene.test.ts | 48 +++++++++++++++++++ .../transformSaveModelToScene.ts | 45 ++++++++++++++++- 3 files changed, 92 insertions(+), 2 deletions(-) diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataAlertingTab.test.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataAlertingTab.test.tsx index 1786c59882..a261687ea1 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataAlertingTab.test.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataAlertingTab.test.tsx @@ -159,6 +159,7 @@ const dashboard = { folderId: 1, folderTitle: 'super folder', }, + isSnapshot: () => false, } as unknown as DashboardModel; const panel = new PanelModel({ diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts index 2567575f3b..c43026dd29 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts @@ -49,6 +49,7 @@ import { NEW_LINK } from '../settings/links/utils'; import { getQueryRunnerFor } from '../utils/utils'; import { buildNewDashboardSaveModel } from './buildNewDashboardSaveModel'; +import { GRAFANA_DATASOURCE_REF } from './const'; import dashboard_to_load1 from './testfiles/dashboard_to_load1.json'; import repeatingRowsAndPanelsDashboardJson from './testfiles/repeating_rows_and_panels.json'; import { @@ -56,6 +57,7 @@ import { buildGridItemForPanel, createSceneVariableFromVariableModel, transformSaveModelToScene, + convertOldSnapshotToScenesSnapshot, } from './transformSaveModelToScene'; describe('transformSaveModelToScene', () => { @@ -1080,6 +1082,52 @@ describe('transformSaveModelToScene', () => { expect(dataLayers.state.layers[4].state.name).toBe('Alert States'); }); }); + + describe('when rendering a legacy snapshot as scene', () => { + it('should convert snapshotData to snapshot inside targets', () => { + const panel = createPanelSaveModel({ + title: 'test', + gridPos: { x: 1, y: 0, w: 12, h: 8 }, + // @ts-ignore + snapshotData: [ + { + fields: [ + { + name: 'Field 1', + type: 'time', + values: ['value1', 'value2'], + config: {}, + }, + { + name: 'Field 2', + type: 'number', + values: [1], + config: {}, + }, + ], + }, + ], + }) as Panel; + + const oldPanelModel = new PanelModel(panel); + convertOldSnapshotToScenesSnapshot(oldPanelModel); + + expect(oldPanelModel.snapshotData?.length).toStrictEqual(0); + expect(oldPanelModel.targets.length).toStrictEqual(1); + expect(oldPanelModel.datasource).toStrictEqual(GRAFANA_DATASOURCE_REF); + expect(oldPanelModel.targets[0].datasource).toStrictEqual(GRAFANA_DATASOURCE_REF); + expect(oldPanelModel.targets[0].queryType).toStrictEqual('snapshot'); + // @ts-ignore + expect(oldPanelModel.targets[0].snapshot.length).toBe(1); + // @ts-ignore + expect(oldPanelModel.targets[0].snapshot[0].data.values).toStrictEqual([['value1', 'value2'], [1]]); + // @ts-ignore + expect(oldPanelModel.targets[0].snapshot[0].schema.fields).toStrictEqual([ + { config: {}, name: 'Field 1', type: 'time' }, + { config: {}, name: 'Field 2', type: 'number' }, + ]); + }); + }); }); function buildGridItemForTest(saveModel: Partial): { gridItem: SceneGridItem; vizPanel: VizPanel } { diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts index 9b7e0c988c..611375cc10 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts @@ -1,4 +1,4 @@ -import { TypedVariableModel } from '@grafana/data'; +import { DataFrameDTO, DataFrameJSON, TypedVariableModel } from '@grafana/data'; import { config } from '@grafana/runtime'; import { VizPanel, @@ -55,6 +55,7 @@ import { } from '../utils/utils'; import { getAngularPanelMigrationHandler } from './angularMigration'; +import { GRAFANA_DATASOURCE_REF } from './const'; export interface DashboardLoaderState { dashboard?: DashboardScene; @@ -114,6 +115,11 @@ export function createSceneObjectsForPanels(oldPanels: PanelModel[]): SceneGridI panels.push(gridItem); } } else { + // when rendering a snapshot created with the legacy Dashboards convert data to new snapshot format to be compatible with Scenes + if (panel.snapshotData) { + convertOldSnapshotToScenesSnapshot(panel); + } + const panelObject = buildGridItemForPanel(panel); // when processing an expanded row, collect its panels @@ -371,7 +377,7 @@ export function createSceneVariableFromVariableModel(variable: TypedVariableMode } else if (variable.type === 'textbox') { return new TextBoxVariable({ ...commonProperties, - value: variable.query, + value: variable?.current?.value?.[0] ?? variable.query, skipUrlSync: variable.skipUrlSync, hide: variable.hide, }); @@ -528,3 +534,38 @@ function registerPanelInteractionsReporter(scene: DashboardScene) { } }); } + +const convertSnapshotData = (snapshotData: DataFrameDTO[]): DataFrameJSON[] => { + return snapshotData.map((data) => { + return { + data: { + values: data.fields.map((field) => field.values).filter((values): values is unknown[] => values !== undefined), + }, + schema: { + fields: data.fields.map((field) => ({ + name: field.name, + type: field.type, + config: field.config, + })), + }, + }; + }); +}; + +// override panel datasource and targets with snapshot data using the Grafana datasource +export const convertOldSnapshotToScenesSnapshot = (panel: PanelModel) => { + // only old snapshots created with old dashboards contains snapshotData + if (panel.snapshotData) { + panel.datasource = GRAFANA_DATASOURCE_REF; + panel.targets = [ + { + refId: panel.snapshotData[0]?.refId ?? '', + datasource: panel.datasource, + queryType: 'snapshot', + // @ts-ignore + snapshot: convertSnapshotData(panel.snapshotData), + }, + ]; + panel.snapshotData = []; + } +}; From 62163f8844dcb238f5736b0b45fe9bb3fdf56ee7 Mon Sep 17 00:00:00 2001 From: Kim Nylander <104772500+knylander-grafana@users.noreply.github.com> Date: Thu, 22 Feb 2024 17:22:00 -0500 Subject: [PATCH 0112/1406] [DOC] Tempo data source: fix broken link and clarify traces to profile (#83135) * Update Tempo data source to fix broken link * Correct profiles content * Move explanation table up * Chagnes from prettier * Resolve conflict --- .../tempo/configure-tempo-data-source.md | 5 +- .../datasources/tempo-traces-to-profiles.md | 52 +++++++++++-------- 2 files changed, 31 insertions(+), 26 deletions(-) diff --git a/docs/sources/datasources/tempo/configure-tempo-data-source.md b/docs/sources/datasources/tempo/configure-tempo-data-source.md index 66cbeaa713..8abe225e12 100644 --- a/docs/sources/datasources/tempo/configure-tempo-data-source.md +++ b/docs/sources/datasources/tempo/configure-tempo-data-source.md @@ -114,7 +114,7 @@ To use a simple configuration, follow these steps: ### Custom queries -To use custom queriess with the configuration, follow these steps: +To use custom queries with the configuration, follow these steps: 1. Select a metrics data source from the **Data source** drop-down. 1. Optional: Choose any tags to use in the query. If left blank, the default values of `cluster`, `hostname`, `namespace`, `pod`, `service.name` and `service.namespace` are used. @@ -281,9 +281,6 @@ datasources: [build-dashboards]: "/docs/grafana/ -> /docs/grafana//dashboards/build-dashboards" [build-dashboards]: "/docs/grafana-cloud/ -> /docs/grafana//dashboards/build-dashboards" -[configure-grafana-feature-toggles]: "/docs/grafana/ -> /docs/grafana//setup-grafana/configure-grafana#feature_toggles" -[configure-grafana-feature-toggles]: "/docs/grafana-cloud/ -> /docs/grafana//setup-grafana/configure-grafana#feature_toggles" - [data-source-management]: "/docs/grafana/ -> /docs/grafana//administration/data-source-management" [data-source-management]: "/docs/grafana-cloud/ -> /docs/grafana//administration/data-source-management" diff --git a/docs/sources/shared/datasources/tempo-traces-to-profiles.md b/docs/sources/shared/datasources/tempo-traces-to-profiles.md index fde1964f20..07e3c66784 100644 --- a/docs/sources/shared/datasources/tempo-traces-to-profiles.md +++ b/docs/sources/shared/datasources/tempo-traces-to-profiles.md @@ -42,12 +42,27 @@ To use trace to profiles, you must have a configured Grafana Pyroscope data sour This lets you see resource consumption in a flame graph visualization for each span without having to navigate away from the current view. Hover over a particular block in the flame graph to see more details about the resources being consumed. +## Configuration options + +The following table describes options for configuring your Trace to profiles settings: + +| Setting name | Description | +| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Data source** | Defines the target data source. You can currently select a Pyroscope \[profiling\] data source. | +| **Tags** | Defines the tags to use in the profile query. Default: `cluster`, `hostname`, `namespace`, `pod`, `service.name`, `service.namespace`. You can change the tag name for example to remove dots from the name if they are not allowed in the target data source. For example, map `http.status` to `http_status`. | +| **Profile type** | Defines the profile type that used in the query. | +| **Use custom query** | Toggles use of custom query with interpolation. | +| **Query** | Input to write custom query. Use variable interpolation to customize it with variables from span. | + ## Use a basic configuration To use a basic configuration, follow these steps: -1. Select a Pyroscope data source from the **Data source** drop-down. -1. Optional: Choose any tags to use in the query. If left blank, the default values of `service.name` and `service.namespace` are used. +1. In the left menu, select **Connections** > **Data sources**. +1. Select your configured Tempo data source from the **Data source** list. +1. Scroll down to the **Traces to profiles** section. +1. Select a Pyroscope data source in the **Data source** drop-down. +1. Optional: Add one or more tags to use in the query. If left blank, the default values of `service.name` and `service.namespace` are used. The tags you configure must be present in the spans attributes or resources for a trace-to-profiles span link to appear. @@ -58,33 +73,26 @@ To use a basic configuration, follow these steps: The profile type or app must be selected for the query to be valid. Grafana doesn't show any data if the profile type or app isn’t selected when a query runs. ![Traces to profile configuration options in the Tempo data source](/media/docs/tempo/profiles/Tempo-data-source-profiles-Settings.png) -1. Do not select **Use custom query**. 1. Select **Save and Test**. -If you have configured a Pyroscope data source and no profile data is available or the **Profiles for this span** button and the embedded flame graph is not visible, verify that the `pyroscope.profile.id` key-value pair exists in your span tags. +If you have configured a Pyroscope data source and no profile data is available or the **Profiles for this span** +button and the embedded flame graph isn't visible, verify that the `pyroscope.profile.id` key-value pair exists in your span tags. ## Configure a custom query To use a custom query with the configuration, follow these steps: -1. Select a Pyroscope data source from the **Data source** drop-down. -1. Optional: Choose any tags to use in the query. If left blank, the default values of `service.name` and `service.namespace` are used. +1. In the left menu, select **Connections** > **Data sources**. +1. Select a configured Tempo data source from the **Data source** list. +1. Scroll down to the **Traces to profiles** section. +1. Select a Pyroscope data source in the **Data source** drop-down. +1. Optional: Choose any tags to use in the query. If left blank, the default values of `service.name` and `service.namespace` are used. - These tags can be used in the custom query with `${__tags}` variable. This variable interpolates the mapped tags as list in an appropriate syntax for the data source. Only the tags that were present in the span are included; tags that aren't present are omitted. You can also configure a new name for the tag. This is useful in cases where the tag has dots in the name and the target data source doesn't allow using dots in labels. For example, you can remap `service.name` to `service_name`. If you don’t map any tags here, you can still use any tag in the query, for example: `method="${__span.tags.method}"`. You can learn more about custom query variables [here](/docs/grafana/latest/datasources/tempo/configure-tempo-data-source/#custom-query-variables). + These tags can be used in the custom query with `${__tags}` variable. This variable interpolates the mapped tags as list in an appropriate syntax for the data source. Only the tags that were present in the span are included; tags that aren't present are omitted. You can also configure a new name for the tag. This is useful in cases where the tag has dots in the name and the target data source doesn't allow using dots in labels. For example, you can remap `service.name` to `service_name`. If you don’t map any tags here, you can still use any tag in the query, for example: `method="${__span.tags.method}"`. You can learn more about custom query variables [here](/docs/grafana/latest/datasources/tempo/configure-tempo-data-source/#custom-query-variables). -1. Select one or more profile types to use in the query. Select the drop-down and choose options from the menu. -1. Switch on **Use custom query** to enter a custom query. -1. Specify a custom query to be used to query profile data. You can use various variables to make that query relevant for current span. The link is shown only if all the variables are interpolated with non-empty values to prevent creating an invalid query. You can interpolate the configured tags using the `$__tags` keyword. -1. Select **Save and Test**. - -## Configure trace to profiles - -The following table describes options for configuring your trace to profiles settings: +1. Select one or more profile types to use in the query. Select the drop-down and choose options from the menu. +1. Switch on **Use custom query** to enter a custom query. +1. Specify a custom query to be used to query profile data. You can use various variables to make that query relevant for current span. The link is shown only if all the variables are interpolated with non-empty values to prevent creating an invalid query. You can interpolate the configured tags using the `$__tags` keyword. +1. Select **Save and Test**. -| Setting name | Description | -| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Data source** | Defines the target data source. You can currently select a Pyroscope \[profiling\] data source. | -| **Tags** | Defines the tags to use in the profile query. Default: `cluster`, `hostname`, `namespace`, `pod`, `service.name`, `service.namespace`. You can change the tag name for example to remove dots from the name if they are not allowed in the target data source. For example, map `http.status` to `http_status`. | -| **Profile type** | Defines the profile type that will be used in the query. | -| **Use custom query** | Toggles use of custom query with interpolation. | -| **Query** | Input to write custom query. Use variable interpolation to customize it with variables from span. | + | From 49299ebd9cf8e37556f32043b7b91ca3b0e68868 Mon Sep 17 00:00:00 2001 From: Alvaro Huarte Date: Fri, 23 Feb 2024 05:20:56 +0100 Subject: [PATCH 0113/1406] Table Component: Improve text-wrapping behavior of cells (#82872) * Fix text-wrapping of cells in Tables * Set wordbreak on hover for long texts without spaces --- packages/grafana-ui/src/components/Table/styles.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/grafana-ui/src/components/Table/styles.ts b/packages/grafana-ui/src/components/Table/styles.ts index e37e697b28..ebd7d0e178 100644 --- a/packages/grafana-ui/src/components/Table/styles.ts +++ b/packages/grafana-ui/src/components/Table/styles.ts @@ -48,7 +48,11 @@ export function useTableStyles(theme: GrafanaTheme2, cellHeightOption: TableCell '&:hover': { overflow: overflowOnHover ? 'visible' : undefined, - width: overflowOnHover ? 'auto !important' : undefined, + width: overflowOnHover ? 'auto' : undefined, + height: overflowOnHover ? 'auto' : `${rowHeight - 1}px`, + minHeight: `${rowHeight - 1}px`, + wordBreak: overflowOnHover ? 'break-word' : undefined, + whiteSpace: overflowOnHover ? 'normal' : 'nowrap', boxShadow: overflowOnHover ? `0 0 2px ${theme.colors.primary.main}` : undefined, background: overflowOnHover ? background ?? theme.components.table.rowHoverBackground : undefined, zIndex: overflowOnHover ? 1 : undefined, From a4cc4179c87aafd8378d8737a84096f265a0c644 Mon Sep 17 00:00:00 2001 From: Kyle Cunningham Date: Thu, 22 Feb 2024 22:28:15 -0600 Subject: [PATCH 0114/1406] Table: Fix units showing in footer after reductions without units (#82081) * Add preservesUnits property to reducer registry items * Hide units when appropriate * Prettier * some code cleanup * Prevent error when no stat is selected. --------- Co-authored-by: nmarrs --- .../src/transformations/fieldReducer.ts | 42 +++++++++++++++++-- .../grafana-ui/src/components/Table/utils.ts | 21 ++++++++-- 2 files changed, 56 insertions(+), 7 deletions(-) diff --git a/packages/grafana-data/src/transformations/fieldReducer.ts b/packages/grafana-data/src/transformations/fieldReducer.ts index e43697d15c..b303654b89 100644 --- a/packages/grafana-data/src/transformations/fieldReducer.ts +++ b/packages/grafana-data/src/transformations/fieldReducer.ts @@ -41,6 +41,7 @@ export interface FieldReducerInfo extends RegistryItem { // Internal details emptyInputResult?: unknown; // typically null, but some things like 'count' & 'sum' should be zero standard: boolean; // The most common stats can all be calculated in a single pass + preservesUnits: boolean; // Whether this reducer preserves units, certain ones don't e.g. count, distinct count, etc, reduce?: FieldReducer; } @@ -141,6 +142,7 @@ export const fieldReducers = new Registry(() => [ standard: true, aliasIds: ['current'], reduce: calculateLastNotNull, + preservesUnits: true, }, { id: ReducerID.last, @@ -148,6 +150,7 @@ export const fieldReducers = new Registry(() => [ description: 'Last value', standard: true, reduce: calculateLast, + preservesUnits: true, }, { id: ReducerID.firstNotNull, @@ -155,17 +158,33 @@ export const fieldReducers = new Registry(() => [ description: 'First non-null value (also excludes NaNs)', standard: true, reduce: calculateFirstNotNull, + preservesUnits: true, + }, + { + id: ReducerID.first, + name: 'First', + description: 'First Value', + standard: true, + reduce: calculateFirst, + preservesUnits: true, + }, + { id: ReducerID.min, name: 'Min', description: 'Minimum Value', standard: true, preservesUnits: true }, + { id: ReducerID.max, name: 'Max', description: 'Maximum Value', standard: true, preservesUnits: true }, + { + id: ReducerID.mean, + name: 'Mean', + description: 'Average Value', + standard: true, + aliasIds: ['avg'], + preservesUnits: true, }, - { id: ReducerID.first, name: 'First', description: 'First Value', standard: true, reduce: calculateFirst }, - { id: ReducerID.min, name: 'Min', description: 'Minimum Value', standard: true }, - { id: ReducerID.max, name: 'Max', description: 'Maximum Value', standard: true }, - { id: ReducerID.mean, name: 'Mean', description: 'Average Value', standard: true, aliasIds: ['avg'] }, { id: ReducerID.variance, name: 'Variance', description: 'Variance of all values in a field', standard: false, reduce: calculateStdDev, + preservesUnits: true, }, { id: ReducerID.stdDev, @@ -173,6 +192,7 @@ export const fieldReducers = new Registry(() => [ description: 'Standard deviation of all values in a field', standard: false, reduce: calculateStdDev, + preservesUnits: true, }, { id: ReducerID.sum, @@ -181,6 +201,7 @@ export const fieldReducers = new Registry(() => [ emptyInputResult: 0, standard: true, aliasIds: ['total'], + preservesUnits: true, }, { id: ReducerID.count, @@ -188,36 +209,42 @@ export const fieldReducers = new Registry(() => [ description: 'Number of values in response', emptyInputResult: 0, standard: true, + preservesUnits: false, }, { id: ReducerID.range, name: 'Range', description: 'Difference between minimum and maximum values', standard: true, + preservesUnits: true, }, { id: ReducerID.delta, name: 'Delta', description: 'Cumulative change in value', standard: true, + preservesUnits: true, }, { id: ReducerID.step, name: 'Step', description: 'Minimum interval between values', standard: true, + preservesUnits: true, }, { id: ReducerID.diff, name: 'Difference', description: 'Difference between first and last values', standard: true, + preservesUnits: true, }, { id: ReducerID.logmin, name: 'Min (above zero)', description: 'Used for log min scale', standard: true, + preservesUnits: true, }, { id: ReducerID.allIsZero, @@ -225,6 +252,7 @@ export const fieldReducers = new Registry(() => [ description: 'All values are zero', emptyInputResult: false, standard: true, + preservesUnits: true, }, { id: ReducerID.allIsNull, @@ -232,6 +260,7 @@ export const fieldReducers = new Registry(() => [ description: 'All values are null', emptyInputResult: true, standard: true, + preservesUnits: false, }, { id: ReducerID.changeCount, @@ -239,6 +268,7 @@ export const fieldReducers = new Registry(() => [ description: 'Number of times the value changes', standard: false, reduce: calculateChangeCount, + preservesUnits: false, }, { id: ReducerID.distinctCount, @@ -246,12 +276,14 @@ export const fieldReducers = new Registry(() => [ description: 'Number of distinct values', standard: false, reduce: calculateDistinctCount, + preservesUnits: false, }, { id: ReducerID.diffperc, name: 'Difference percent', description: 'Percentage difference between first and last values', standard: true, + preservesUnits: false, }, { id: ReducerID.allValues, @@ -259,6 +291,7 @@ export const fieldReducers = new Registry(() => [ description: 'Returns an array with all values', standard: false, reduce: (field: Field) => ({ allValues: [...field.values] }), + preservesUnits: false, }, { id: ReducerID.uniqueValues, @@ -268,6 +301,7 @@ export const fieldReducers = new Registry(() => [ reduce: (field: Field) => ({ uniqueValues: [...new Set(field.values)], }), + preservesUnits: false, }, ]); diff --git a/packages/grafana-ui/src/components/Table/utils.ts b/packages/grafana-ui/src/components/Table/utils.ts index 4871f5a547..34a8efe5ae 100644 --- a/packages/grafana-ui/src/components/Table/utils.ts +++ b/packages/grafana-ui/src/components/Table/utils.ts @@ -385,10 +385,25 @@ export function getFooterItems( } function getFormattedValue(field: Field, reducer: string[], theme: GrafanaTheme2) { - const fmt = field.display ?? getDisplayProcessor({ field, theme }); + // If we don't have anything to return then we display nothing const calc = reducer[0]; - const v = reduceField({ field, reducers: reducer })[calc]; - return formattedValueToString(fmt(v)); + if (calc === undefined) { + return ''; + } + + // Calculate the reduction + const format = field.display ?? getDisplayProcessor({ field, theme }); + const fieldCalcValue = reduceField({ field, reducers: reducer })[calc]; + + // If the reducer preserves units then format the + // end value with the field display processor + const reducerInfo = fieldReducers.get(calc); + if (reducerInfo.preservesUnits) { + return formattedValueToString(format(fieldCalcValue)); + } + + // Otherwise we simply return the formatted string + return formattedValueToString({ text: fieldCalcValue }); } // This strips the raw vales from the `rows` object. From cc9ff3f8c9890b794493cca28b6b3a756f641a89 Mon Sep 17 00:00:00 2001 From: Konrad Lalik Date: Fri, 23 Feb 2024 09:19:16 +0100 Subject: [PATCH 0115/1406] Alerting: Add Loki ASH tests checking proper rendering of timeline chart component (#83235) Add Loki ASH tests checking proper rendering of timeline chart component --- .../state-history/LokiStateHistory.test.tsx | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/public/app/features/alerting/unified/components/rules/state-history/LokiStateHistory.test.tsx b/public/app/features/alerting/unified/components/rules/state-history/LokiStateHistory.test.tsx index e9ba271cde..3925cab0c3 100644 --- a/public/app/features/alerting/unified/components/rules/state-history/LokiStateHistory.test.tsx +++ b/public/app/features/alerting/unified/components/rules/state-history/LokiStateHistory.test.tsx @@ -3,7 +3,8 @@ import { render, waitFor } from '@testing-library/react'; import { http, HttpResponse } from 'msw'; import { setupServer } from 'msw/node'; import React from 'react'; -import { byRole, byText } from 'testing-library-selector'; +import { Props } from 'react-virtualized-auto-sizer'; +import { byRole, byTestId, byText } from 'testing-library-selector'; import { DataFrameJSON } from '@grafana/data'; import { setBackendSrv } from '@grafana/runtime'; @@ -15,6 +16,16 @@ import LokiStateHistory from './LokiStateHistory'; const server = setupServer(); +jest.mock('react-virtualized-auto-sizer', () => { + return ({ children }: Props) => + children({ + height: 600, + scaledHeight: 600, + scaledWidth: 1, + width: 1, + }); +}); + beforeAll(() => { setBackendSrv(backendSrv); server.listen({ onUnhandledRequest: 'error' }); @@ -80,6 +91,7 @@ const ui = { timestampViewer: byRole('list', { name: 'State history by timestamp' }), record: byRole('listitem'), noRecords: byText('No state transitions have occurred in the last 30 days'), + timelineChart: byTestId('uplot-main-div'), }; describe('LokiStateHistory', () => { @@ -96,6 +108,14 @@ describe('LokiStateHistory', () => { expect(timestampViewerElement).toHaveTextContent('/api/folders/:uid/'); }); + it('should render timeline chart', async () => { + render(, { wrapper: TestProvider }); + + await waitFor(() => expect(ui.loadingIndicator.query()).not.toBeInTheDocument()); + + expect(ui.timelineChart.get()).toBeInTheDocument(); + }); + it('should render no entries message when no records are returned', async () => { server.use( http.get('/api/v1/rules/history', () => From 217154f85d8a5fad853e98d6044a88612a6b8b53 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 23 Feb 2024 09:20:02 +0000 Subject: [PATCH 0116/1406] Update dependency stylelint-config-sass-guidelines to v11 (#83253) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 31 ++++++++++++++++--------------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index 58505eac03..b89b40701d 100644 --- a/package.json +++ b/package.json @@ -207,7 +207,7 @@ "style-loader": "3.3.4", "stylelint": "15.11.0", "stylelint-config-prettier": "9.0.5", - "stylelint-config-sass-guidelines": "10.0.0", + "stylelint-config-sass-guidelines": "11.0.0", "terser-webpack-plugin": "5.3.10", "testing-library-selector": "0.3.1", "tracelib": "1.0.1", diff --git a/yarn.lock b/yarn.lock index 445022dae2..7a297e7df4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18698,7 +18698,7 @@ __metadata: style-loader: "npm:3.3.4" stylelint: "npm:15.11.0" stylelint-config-prettier: "npm:9.0.5" - stylelint-config-sass-guidelines: "npm:10.0.0" + stylelint-config-sass-guidelines: "npm:11.0.0" symbol-observable: "npm:4.0.0" terser-webpack-plugin: "npm:5.3.10" testing-library-selector: "npm:0.3.1" @@ -25154,7 +25154,7 @@ __metadata: languageName: node linkType: hard -"postcss-scss@npm:4.0.9, postcss-scss@npm:^4.0.6": +"postcss-scss@npm:4.0.9, postcss-scss@npm:^4.0.9": version: 4.0.9 resolution: "postcss-scss@npm:4.0.9" peerDependencies: @@ -29331,30 +29331,31 @@ __metadata: languageName: node linkType: hard -"stylelint-config-sass-guidelines@npm:10.0.0": - version: 10.0.0 - resolution: "stylelint-config-sass-guidelines@npm:10.0.0" +"stylelint-config-sass-guidelines@npm:11.0.0": + version: 11.0.0 + resolution: "stylelint-config-sass-guidelines@npm:11.0.0" dependencies: - postcss-scss: "npm:^4.0.6" - stylelint-scss: "npm:^4.4.0" + postcss-scss: "npm:^4.0.9" + stylelint-scss: "npm:^6.0.0" peerDependencies: postcss: ^8.4.21 - stylelint: ^15.2.0 - checksum: 10/164ba5e519c35c8615193bddab61d50c72c2bb5d8ccbe706ced69403e3e14540a7379e5dad48353c1690caff783fb8559d375d6842d48f8b7385341e0e69eb3e + stylelint: ^16.1.0 + checksum: 10/0d55287e1a427ab3f598cfa29c27b07b803a7ad1d88239fc58a137cb50eac77616b48bf5b4ec408225c6442c912e83e63f706d8902fd17e6148d6392af0cb0cf languageName: node linkType: hard -"stylelint-scss@npm:^4.4.0": - version: 4.7.0 - resolution: "stylelint-scss@npm:4.7.0" +"stylelint-scss@npm:^6.0.0": + version: 6.1.0 + resolution: "stylelint-scss@npm:6.1.0" dependencies: + known-css-properties: "npm:^0.29.0" postcss-media-query-parser: "npm:^0.2.3" postcss-resolve-nested-selector: "npm:^0.1.1" - postcss-selector-parser: "npm:^6.0.11" + postcss-selector-parser: "npm:^6.0.15" postcss-value-parser: "npm:^4.2.0" peerDependencies: - stylelint: ^14.5.1 || ^15.0.0 - checksum: 10/6a49f1f19339c812adc1fc89bb30d0a79ab1a88082f8d18b9403893f06e4f646131d9d4f2788a2fe2847fe38ff6cf505de8a3f6358665e022f91903c7453f4c4 + stylelint: ^16.0.2 + checksum: 10/d1a37af38e6793f60ad3d836741ed624a7a07a56bea094750e72ed6e549243f966efcbe247940c6f1337328db700b1004f49e2119ff421235c979b2574995426 languageName: node linkType: hard From ad80518db07bcb5a0c3eb55e3015a25f529ddf6e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 23 Feb 2024 11:43:16 +0200 Subject: [PATCH 0117/1406] Update dependency webpack-dev-server to v5 (#83258) * Update dependency webpack-dev-server to v5 * update webpack.hot config (is this even used anymore?) --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Ashley Harrison --- package.json | 2 +- scripts/webpack/webpack.hot.js | 9 +- yarn.lock | 373 +++++++++++++++++++++------------ 3 files changed, 250 insertions(+), 134 deletions(-) diff --git a/package.json b/package.json index b89b40701d..10aca17d6b 100644 --- a/package.json +++ b/package.json @@ -217,7 +217,7 @@ "webpack": "5.90.2", "webpack-bundle-analyzer": "4.10.1", "webpack-cli": "5.1.4", - "webpack-dev-server": "4.15.1", + "webpack-dev-server": "5.0.2", "webpack-manifest-plugin": "5.0.0", "webpack-merge": "5.10.0", "yaml": "^2.0.0", diff --git a/scripts/webpack/webpack.hot.js b/scripts/webpack/webpack.hot.js index 7edd8db538..173f53f65c 100644 --- a/scripts/webpack/webpack.hot.js +++ b/scripts/webpack/webpack.hot.js @@ -30,9 +30,12 @@ module.exports = merge(common, { hot: true, open: false, port: 3333, - proxy: { - '!/public/build': 'http://localhost:3000', - }, + proxy: [ + { + context: '!/public/build', + target: 'http://localhost:3000', + }, + ], static: { publicPath: '/public/build/', }, diff --git a/yarn.lock b/yarn.lock index 7a297e7df4..336870d051 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8856,12 +8856,12 @@ __metadata: languageName: node linkType: hard -"@types/bonjour@npm:^3.5.9": - version: 3.5.10 - resolution: "@types/bonjour@npm:3.5.10" +"@types/bonjour@npm:^3.5.13": + version: 3.5.13 + resolution: "@types/bonjour@npm:3.5.13" dependencies: "@types/node": "npm:*" - checksum: 10/bfcadb042a41b124c4e3de4925e3be6d35b78f93f27c4535d5ff86980dc0f8bc407ed99b9b54528952dc62834d5a779392f7a12c2947dd19330eb05a6bcae15a + checksum: 10/e827570e097bd7d625a673c9c208af2d1a22fa3885c0a1646533cf24394c839c3e5f60ac1bc60c0ddcc69c0615078c9fb2c01b42596c7c582d895d974f2409ee languageName: node linkType: hard @@ -8888,13 +8888,13 @@ __metadata: languageName: node linkType: hard -"@types/connect-history-api-fallback@npm:^1.3.5": - version: 1.3.5 - resolution: "@types/connect-history-api-fallback@npm:1.3.5" +"@types/connect-history-api-fallback@npm:^1.5.4": + version: 1.5.4 + resolution: "@types/connect-history-api-fallback@npm:1.5.4" dependencies: "@types/express-serve-static-core": "npm:*" "@types/node": "npm:*" - checksum: 10/464d06e5ab00f113fa89978633d5eb00d225aeb4ebbadc07f6f3bc337aa7cbfcd74957b2a539d6d47f2e128e956a17819973ec7ae62ade2e16e367a6c38b8d3a + checksum: 10/e1dee43b8570ffac02d2d47a2b4ba80d3ca0dd1840632dafb221da199e59dbe3778d3d7303c9e23c6b401f37c076935a5bc2aeae1c4e5feaefe1c371fe2073fd languageName: node linkType: hard @@ -9353,15 +9353,15 @@ __metadata: languageName: node linkType: hard -"@types/express@npm:*, @types/express@npm:^4.17.13, @types/express@npm:^4.7.0": - version: 4.17.17 - resolution: "@types/express@npm:4.17.17" +"@types/express@npm:*, @types/express@npm:^4.17.21, @types/express@npm:^4.7.0": + version: 4.17.21 + resolution: "@types/express@npm:4.17.21" dependencies: "@types/body-parser": "npm:*" "@types/express-serve-static-core": "npm:^4.17.33" "@types/qs": "npm:*" "@types/serve-static": "npm:*" - checksum: 10/e2959a5fecdc53f8a524891a16e66dfc330ee0519e89c2579893179db686e10cfa6079a68e0fb8fd00eedbcaf3eabfd10916461939f3bc02ef671d848532c37e + checksum: 10/7a6d26cf6f43d3151caf4fec66ea11c9d23166e4f3102edfe45a94170654a54ea08cf3103d26b3928d7ebcc24162c90488e33986b7e3a5f8941225edd5eb18c7 languageName: node linkType: hard @@ -9471,6 +9471,13 @@ __metadata: languageName: node linkType: hard +"@types/http-errors@npm:*": + version: 2.0.4 + resolution: "@types/http-errors@npm:2.0.4" + checksum: 10/1f3d7c3b32c7524811a45690881736b3ef741bf9849ae03d32ad1ab7062608454b150a4e7f1351f83d26a418b2d65af9bdc06198f1c079d75578282884c4e8e3 + languageName: node + linkType: hard + "@types/http-proxy@npm:^1.17.8": version: 1.17.8 resolution: "@types/http-proxy@npm:1.17.8" @@ -9654,10 +9661,10 @@ __metadata: languageName: node linkType: hard -"@types/mime@npm:^1": - version: 1.3.2 - resolution: "@types/mime@npm:1.3.2" - checksum: 10/0493368244cced1a69cb791b485a260a422e6fcc857782e1178d1e6f219f1b161793e9f87f5fae1b219af0f50bee24fcbe733a18b4be8fdd07a38a8fb91146fd +"@types/mime@npm:*": + version: 3.0.4 + resolution: "@types/mime@npm:3.0.4" + checksum: 10/a6139c8e1f705ef2b064d072f6edc01f3c099023ad7c4fce2afc6c2bf0231888202adadbdb48643e8e20da0ce409481a49922e737eca52871b3dc08017455843 languageName: node linkType: hard @@ -9722,7 +9729,7 @@ __metadata: languageName: node linkType: hard -"@types/node-forge@npm:^1": +"@types/node-forge@npm:^1, @types/node-forge@npm:^1.3.0": version: 1.3.11 resolution: "@types/node-forge@npm:1.3.11" dependencies: @@ -10048,10 +10055,10 @@ __metadata: languageName: node linkType: hard -"@types/retry@npm:^0.12.0": - version: 0.12.1 - resolution: "@types/retry@npm:0.12.1" - checksum: 10/5f46b2556053655f78262bb33040dc58417c900457cc63ff37d6c35349814471453ef511af0cec76a540c601296cd2b22f64bab1ab649c0dacc0223765ba876c +"@types/retry@npm:0.12.2": + version: 0.12.2 + resolution: "@types/retry@npm:0.12.2" + checksum: 10/e5675035717b39ce4f42f339657cae9637cf0c0051cf54314a6a2c44d38d91f6544be9ddc0280587789b6afd056be5d99dbe3e9f4df68c286c36321579b1bf4a languageName: node linkType: hard @@ -10069,22 +10076,23 @@ __metadata: languageName: node linkType: hard -"@types/serve-index@npm:^1.9.1": - version: 1.9.1 - resolution: "@types/serve-index@npm:1.9.1" +"@types/serve-index@npm:^1.9.4": + version: 1.9.4 + resolution: "@types/serve-index@npm:1.9.4" dependencies: "@types/express": "npm:*" - checksum: 10/026f3995fb500f6df7c3fe5009e53bad6d739e20b84089f58ebfafb2f404bbbb6162bbe33f72d2f2af32d5b8d3799c8e179793f90d9ed5871fb8591190bb6056 + checksum: 10/72727c88d54da5b13275ebfb75dcdc4aa12417bbe9da1939e017c4c5f0c906fae843aa4e0fbfe360e7ee9df2f3d388c21abfc488f77ce58693fb57809f8ded92 languageName: node linkType: hard -"@types/serve-static@npm:*, @types/serve-static@npm:^1.13.10": - version: 1.13.10 - resolution: "@types/serve-static@npm:1.13.10" +"@types/serve-static@npm:*, @types/serve-static@npm:^1.15.5": + version: 1.15.5 + resolution: "@types/serve-static@npm:1.15.5" dependencies: - "@types/mime": "npm:^1" + "@types/http-errors": "npm:*" + "@types/mime": "npm:*" "@types/node": "npm:*" - checksum: 10/62b4e79cb049a5ed81789e2cdd8b91e289eb03b08130c249d74c8fd6d32840cffc6b50384c1ccd2ef0ecf306fe1188634fd9a8bce4339acd4bcc19ed16b2a0c3 + checksum: 10/49aa21c367fffe4588fc8c57ea48af0ea7cbadde7418bc53cde85d8bd57fd2a09a293970d9ea86e79f17a87f8adeb3e20da76aab38e1c4d1567931fa15c8af38 languageName: node linkType: hard @@ -10141,12 +10149,12 @@ __metadata: languageName: node linkType: hard -"@types/sockjs@npm:^0.3.33": - version: 0.3.33 - resolution: "@types/sockjs@npm:0.3.33" +"@types/sockjs@npm:^0.3.36": + version: 0.3.36 + resolution: "@types/sockjs@npm:0.3.36" dependencies: "@types/node": "npm:*" - checksum: 10/b9bbb2b5c5ead2fb884bb019f61a014e37410bddd295de28184e1b2e71ee6b04120c5ba7b9954617f0bdf962c13d06249ce65004490889c747c80d3f628ea842 + checksum: 10/b4b5381122465d80ea8b158537c00bc82317222d3fb31fd7229ff25b31fa89134abfbab969118da55622236bf3d8fee75759f3959908b5688991f492008f29bc languageName: node linkType: hard @@ -10275,12 +10283,12 @@ __metadata: languageName: node linkType: hard -"@types/ws@npm:^8.5.5": - version: 8.5.5 - resolution: "@types/ws@npm:8.5.5" +"@types/ws@npm:^8.5.10": + version: 8.5.10 + resolution: "@types/ws@npm:8.5.10" dependencies: "@types/node": "npm:*" - checksum: 10/b2d7da5bd469c2ff1ddcfba1da33a556dc02c539e727001e7dc7b4182935154143e96a101cc091686acefb4e115c8ee38111c6634934748b8dd2db0c851c50ab + checksum: 10/9b414dc5e0b6c6f1ea4b1635b3568c58707357f68076df9e7cd33194747b7d1716d5189c0dbdd68c8d2521b148e88184cf881bac7429eb0e5c989b001539ed31 languageName: node linkType: hard @@ -11720,13 +11728,6 @@ __metadata: languageName: node linkType: hard -"array-flatten@npm:^2.1.2": - version: 2.1.2 - resolution: "array-flatten@npm:2.1.2" - checksum: 10/e8988aac1fbfcdaae343d08c9a06a6fddd2c6141721eeeea45c3cf523bf4431d29a46602929455ed548c7a3e0769928cdc630405427297e7081bd118fdec9262 - languageName: node - linkType: hard - "array-ify@npm:^1.0.0": version: 1.0.0 resolution: "array-ify@npm:1.0.0" @@ -12462,15 +12463,13 @@ __metadata: languageName: node linkType: hard -"bonjour-service@npm:^1.0.11": - version: 1.0.11 - resolution: "bonjour-service@npm:1.0.11" +"bonjour-service@npm:^1.2.1": + version: 1.2.1 + resolution: "bonjour-service@npm:1.2.1" dependencies: - array-flatten: "npm:^2.1.2" - dns-equal: "npm:^1.0.0" fast-deep-equal: "npm:^3.1.3" - multicast-dns: "npm:^7.2.4" - checksum: 10/5cd903d1d6886463a557fddbf10d12a3204071eb8773fc267003ae7b474b91115c3a13fe8032b5fd08f9c77ef560a36cfe0b912244f3775f269293ecb5bff4f9 + multicast-dns: "npm:^7.2.5" + checksum: 10/8350d135ab8dd998a829136984d7f74bfc0667b162ab99ac98bae54d72ff7a6003c6fb7911739dfba7c56a113bd6ab06a4d4fe6719b18e66592c345663e7d923 languageName: node linkType: hard @@ -12686,6 +12685,15 @@ __metadata: languageName: node linkType: hard +"bundle-name@npm:^4.1.0": + version: 4.1.0 + resolution: "bundle-name@npm:4.1.0" + dependencies: + run-applescript: "npm:^7.0.0" + checksum: 10/1d966c8d2dbf4d9d394e53b724ac756c2414c45c01340b37743621f59cc565a435024b394ddcb62b9b335d1c9a31f4640eb648c3fec7f97ee74dc0694c9beb6c + languageName: node + linkType: hard + "byte-size@npm:8.1.1": version: 8.1.1 resolution: "byte-size@npm:8.1.1" @@ -13072,7 +13080,7 @@ __metadata: languageName: node linkType: hard -"chokidar@npm:3.5.3, chokidar@npm:>=3.0.0 <4.0.0, chokidar@npm:^3.3.1, chokidar@npm:^3.4.2, chokidar@npm:^3.5.3": +"chokidar@npm:3.5.3": version: 3.5.3 resolution: "chokidar@npm:3.5.3" dependencies: @@ -13091,6 +13099,25 @@ __metadata: languageName: node linkType: hard +"chokidar@npm:>=3.0.0 <4.0.0, chokidar@npm:^3.3.1, chokidar@npm:^3.4.2, chokidar@npm:^3.5.3, chokidar@npm:^3.6.0": + version: 3.6.0 + resolution: "chokidar@npm:3.6.0" + dependencies: + anymatch: "npm:~3.1.2" + braces: "npm:~3.0.2" + fsevents: "npm:~2.3.2" + glob-parent: "npm:~5.1.2" + is-binary-path: "npm:~2.1.0" + is-glob: "npm:~4.0.1" + normalize-path: "npm:~3.0.0" + readdirp: "npm:~3.6.0" + dependenciesMeta: + fsevents: + optional: true + checksum: 10/c327fb07704443f8d15f7b4a7ce93b2f0bc0e6cea07ec28a7570aa22cd51fcf0379df589403976ea956c369f25aa82d84561947e227cd925902e1751371658df + languageName: node + linkType: hard + "chownr@npm:^1.1.1": version: 1.1.4 resolution: "chownr@npm:1.1.4" @@ -15093,6 +15120,23 @@ __metadata: languageName: node linkType: hard +"default-browser-id@npm:^5.0.0": + version: 5.0.0 + resolution: "default-browser-id@npm:5.0.0" + checksum: 10/185bfaecec2c75fa423544af722a3469b20704c8d1942794a86e4364fe7d9e8e9f63241a5b769d61c8151993bc65833a5b959026fa1ccea343b3db0a33aa6deb + languageName: node + linkType: hard + +"default-browser@npm:^5.2.1": + version: 5.2.1 + resolution: "default-browser@npm:5.2.1" + dependencies: + bundle-name: "npm:^4.1.0" + default-browser-id: "npm:^5.0.0" + checksum: 10/afab7eff7b7f5f7a94d9114d1ec67273d3fbc539edf8c0f80019879d53aa71e867303c6f6d7cffeb10a6f3cfb59d4f963dba3f9c96830b4540cc7339a1bf9840 + languageName: node + linkType: hard + "default-gateway@npm:^6.0.3": version: 6.0.3 resolution: "default-gateway@npm:6.0.3" @@ -15129,6 +15173,13 @@ __metadata: languageName: node linkType: hard +"define-lazy-prop@npm:^3.0.0": + version: 3.0.0 + resolution: "define-lazy-prop@npm:3.0.0" + checksum: 10/f28421cf9ee86eecaf5f3b8fe875f13d7009c2625e97645bfff7a2a49aca678270b86c39f9c32939e5ca7ab96b551377ed4139558c795e076774287ad3af1aa4 + languageName: node + linkType: hard + "define-properties@npm:^1.1.3, define-properties@npm:^1.1.4, define-properties@npm:^1.2.0, define-properties@npm:^1.2.1": version: 1.2.1 resolution: "define-properties@npm:1.2.1" @@ -15359,13 +15410,6 @@ __metadata: languageName: node linkType: hard -"dns-equal@npm:^1.0.0": - version: 1.0.0 - resolution: "dns-equal@npm:1.0.0" - checksum: 10/c4f55af6f13536de39ebcfa15f504a5678d4fc2cf37b76fd41e73aa46dbd1fa596c9468c0c929aeb248ec443cb217fde949942c513312acf93c76cf783276617 - languageName: node - linkType: hard - "dns-packet@npm:^5.2.2": version: 5.4.0 resolution: "dns-packet@npm:5.4.0" @@ -18717,7 +18761,7 @@ __metadata: webpack-assets-manifest: "npm:^5.1.0" webpack-bundle-analyzer: "npm:4.10.1" webpack-cli: "npm:5.1.4" - webpack-dev-server: "npm:4.15.1" + webpack-dev-server: "npm:5.0.2" webpack-manifest-plugin: "npm:5.0.0" webpack-merge: "npm:5.10.0" whatwg-fetch: "npm:3.6.20" @@ -19112,10 +19156,10 @@ __metadata: languageName: node linkType: hard -"html-entities@npm:^2.1.0, html-entities@npm:^2.3.2": - version: 2.3.3 - resolution: "html-entities@npm:2.3.3" - checksum: 10/24f6b77ce234e263f3d44530de2356e67c313c8ba7e5f6e02c16dcea3a950711d8820afb320746d57b8dae61fde7aaaa7f60017b706fa4bce8624ba3c29ad316 +"html-entities@npm:^2.1.0, html-entities@npm:^2.4.0": + version: 2.4.0 + resolution: "html-entities@npm:2.4.0" + checksum: 10/646f2f19214bad751e060ceef4df98520654a1d0cd631b55d45504df2f0aaf8a14d8c0a5a4f92b353be298774d856157ac2d04a031d78889c9011892078ca157 languageName: node linkType: hard @@ -19807,10 +19851,10 @@ __metadata: languageName: node linkType: hard -"ipaddr.js@npm:^2.0.1": - version: 2.0.1 - resolution: "ipaddr.js@npm:2.0.1" - checksum: 10/b809f60af0473f1452480b05a2cec8270284290d18d2778df522d08e0b6d0db21b84f5bf4949190f3c728794d3eef36bfaeff14a1e1acf6045553f4532b119de +"ipaddr.js@npm:^2.1.0": + version: 2.1.0 + resolution: "ipaddr.js@npm:2.1.0" + checksum: 10/42c16d95cf451399707c2c46e605b88db1ea2b1477b25774b5a7ee96852b0bb1efdc01adbff01fedbe702ff246e1aca5c5e915a6f5a1f1485233a5f7c2eb73c2 languageName: node linkType: hard @@ -19985,6 +20029,15 @@ __metadata: languageName: node linkType: hard +"is-docker@npm:^3.0.0": + version: 3.0.0 + resolution: "is-docker@npm:3.0.0" + bin: + is-docker: cli.js + checksum: 10/b698118f04feb7eaf3338922bd79cba064ea54a1c3db6ec8c0c8d8ee7613e7e5854d802d3ef646812a8a3ace81182a085dfa0a71cc68b06f3fa794b9783b3c90 + languageName: node + linkType: hard + "is-extendable@npm:^1.0.0": version: 1.0.1 resolution: "is-extendable@npm:1.0.1" @@ -20086,6 +20139,17 @@ __metadata: languageName: node linkType: hard +"is-inside-container@npm:^1.0.0": + version: 1.0.0 + resolution: "is-inside-container@npm:1.0.0" + dependencies: + is-docker: "npm:^3.0.0" + bin: + is-inside-container: cli.js + checksum: 10/c50b75a2ab66ab3e8b92b3bc534e1ea72ca25766832c0623ac22d134116a98bcf012197d1caabe1d1c4bd5f84363d4aa5c36bb4b585fbcaf57be172cd10a1a03 + languageName: node + linkType: hard + "is-installed-globally@npm:~0.4.0": version: 0.4.0 resolution: "is-installed-globally@npm:0.4.0" @@ -20148,6 +20212,13 @@ __metadata: languageName: node linkType: hard +"is-network-error@npm:^1.0.0": + version: 1.0.1 + resolution: "is-network-error@npm:1.0.1" + checksum: 10/165d61500c4186c62db5a3a693d6bfa14ca40fe9b471ef4cd4f27b20ef6760880faf5386dc01ca9867531631782941fedaa94521d09959edf71f046e393c7b91 + languageName: node + linkType: hard + "is-node-process@npm:^1.2.0": version: 1.2.0 resolution: "is-node-process@npm:1.2.0" @@ -20407,6 +20478,15 @@ __metadata: languageName: node linkType: hard +"is-wsl@npm:^3.1.0": + version: 3.1.0 + resolution: "is-wsl@npm:3.1.0" + dependencies: + is-inside-container: "npm:^1.0.0" + checksum: 10/f9734c81f2f9cf9877c5db8356bfe1ff61680f1f4c1011e91278a9c0564b395ae796addb4bf33956871041476ec82c3e5260ed57b22ac91794d4ae70a1d2f0a9 + languageName: node + linkType: hard + "isarray@npm:0.0.1": version: 0.0.1 resolution: "isarray@npm:0.0.1" @@ -21645,13 +21725,13 @@ __metadata: languageName: node linkType: hard -"launch-editor@npm:^2.6.0": - version: 2.6.0 - resolution: "launch-editor@npm:2.6.0" +"launch-editor@npm:^2.6.1": + version: 2.6.1 + resolution: "launch-editor@npm:2.6.1" dependencies: picocolors: "npm:^1.0.0" - shell-quote: "npm:^1.7.3" - checksum: 10/48e4230643e8fdb5c14c11314706d58d9f3fbafe2606be3d6e37da1918ad8bfe39dd87875c726a1b59b9f4da99d87ec3e36d4c528464f0b820f9e91e5cb1c02d + shell-quote: "npm:^1.8.1" + checksum: 10/e06d193075ac09f7f8109f10cabe464a211bf7ed4cbe75f83348d6f67bf4d9f162f06e7a1ab3e1cd7fc250b5342c3b57080618aff2e646dc34248fe499227601 languageName: node linkType: hard @@ -22461,6 +22541,15 @@ __metadata: languageName: node linkType: hard +"memfs@npm:^4.6.0": + version: 4.7.7 + resolution: "memfs@npm:4.7.7" + dependencies: + tslib: "npm:^2.0.0" + checksum: 10/311633e5857c91f41021b43f00eda8d540fed2c2d9e02c780fe78de720cfb55d15ab2d5b5ce9f2576637589b82e84488f1b9ff503563e817ed65200ad24617fb + languageName: node + linkType: hard + "memoize-one@npm:6.0.0, memoize-one@npm:^6.0.0": version: 6.0.0 resolution: "memoize-one@npm:6.0.0" @@ -23190,15 +23279,15 @@ __metadata: languageName: node linkType: hard -"multicast-dns@npm:^7.2.4": - version: 7.2.4 - resolution: "multicast-dns@npm:7.2.4" +"multicast-dns@npm:^7.2.5": + version: 7.2.5 + resolution: "multicast-dns@npm:7.2.5" dependencies: dns-packet: "npm:^5.2.2" thunky: "npm:^1.0.2" bin: multicast-dns: cli.js - checksum: 10/e3a43c87e72b595a9ffac0486c808809af1b113728521cd4c5852f4c33c74383f2f9834d2e4a66b340301632f195cc1b2f07336b13dcd75c2ac5ebf95c933476 + checksum: 10/e9add8035fb7049ccbc87b1b069f05bb3b31e04fe057bf7d0116739d81295165afc2568291a4a962bee01a5074e475996816eed0f50c8110d652af5abb74f95a languageName: node linkType: hard @@ -24044,7 +24133,19 @@ __metadata: languageName: node linkType: hard -"open@npm:^8.0.4, open@npm:^8.0.9, open@npm:^8.4.0": +"open@npm:^10.0.3": + version: 10.0.3 + resolution: "open@npm:10.0.3" + dependencies: + default-browser: "npm:^5.2.1" + define-lazy-prop: "npm:^3.0.0" + is-inside-container: "npm:^1.0.0" + is-wsl: "npm:^3.1.0" + checksum: 10/4dc757ad1d3d63490822f991e9cbe3a7c05b7249fca2eaa571cb7d191e5cec88bc37e15d8ef4fd740d8989a288b661d8da253caa8d98e8c97430ddbbb0ae4ed1 + languageName: node + linkType: hard + +"open@npm:^8.0.4, open@npm:^8.4.0": version: 8.4.2 resolution: "open@npm:8.4.2" dependencies: @@ -24269,13 +24370,14 @@ __metadata: languageName: node linkType: hard -"p-retry@npm:^4.5.0": - version: 4.6.1 - resolution: "p-retry@npm:4.6.1" +"p-retry@npm:^6.2.0": + version: 6.2.0 + resolution: "p-retry@npm:6.2.0" dependencies: - "@types/retry": "npm:^0.12.0" + "@types/retry": "npm:0.12.2" + is-network-error: "npm:^1.0.0" retry: "npm:^0.13.1" - checksum: 10/e6d540413bb3d0b96e0db44f74a7af1dce41f5005e6e84d617960110b148348c86a3987be07797749e3ddd55817dd3a8ffd6eae3428758bc2994d987e48c3a70 + checksum: 10/1a5ac16828c96c03c354f78d643dfc7aa8f8b998e1b60e27533da2c75e5cabfb1c7f88ce312e813e09a80b056011fbb372d384132e9c92d27d052bd7c282a978 languageName: node linkType: hard @@ -27610,7 +27712,7 @@ __metadata: languageName: node linkType: hard -"rimraf@npm:5.0.5": +"rimraf@npm:5.0.5, rimraf@npm:^5.0.5": version: 5.0.5 resolution: "rimraf@npm:5.0.5" dependencies: @@ -27807,6 +27909,13 @@ __metadata: languageName: node linkType: hard +"run-applescript@npm:^7.0.0": + version: 7.0.0 + resolution: "run-applescript@npm:7.0.0" + checksum: 10/b02462454d8b182ad4117e5d4626e9e6782eb2072925c9fac582170b0627ae3c1ea92ee9b2df7daf84b5e9ffe14eb1cf5fb70bc44b15c8a0bfcdb47987e2410c + languageName: node + linkType: hard + "run-async@npm:^2.4.0": version: 2.4.1 resolution: "run-async@npm:2.4.1" @@ -28023,12 +28132,13 @@ __metadata: languageName: node linkType: hard -"selfsigned@npm:^2.1.1": - version: 2.1.1 - resolution: "selfsigned@npm:2.1.1" +"selfsigned@npm:^2.4.1": + version: 2.4.1 + resolution: "selfsigned@npm:2.4.1" dependencies: + "@types/node-forge": "npm:^1.3.0" node-forge: "npm:^1" - checksum: 10/6005206e0d005448274aceceaded5195b944f67a42b72d212a6169d2e5f4bdc87c15a3fe45732c544db8c7175702091aaf95403ad6632585294a6ec8cca63638 + checksum: 10/52536623f1cfdeb2f8b9198377f2ce7931c677ea69421238d1dc1ea2983bbe258e56c19e7d1af87035cad7270f19b7e996eaab1212e724d887722502f68e17f2 languageName: node linkType: hard @@ -28227,7 +28337,7 @@ __metadata: languageName: node linkType: hard -"shell-quote@npm:^1.7.3": +"shell-quote@npm:^1.8.1": version: 1.8.1 resolution: "shell-quote@npm:1.8.1" checksum: 10/af19ab5a1ec30cb4b2f91fd6df49a7442d5c4825a2e269b3712eded10eedd7f9efeaab96d57829880733fc55bcdd8e9b1d8589b4befb06667c731d08145e274d @@ -31316,27 +31426,30 @@ __metadata: languageName: node linkType: hard -"webpack-dev-middleware@npm:^5.3.1": - version: 5.3.1 - resolution: "webpack-dev-middleware@npm:5.3.1" +"webpack-dev-middleware@npm:^6.1.1": + version: 6.1.1 + resolution: "webpack-dev-middleware@npm:6.1.1" dependencies: colorette: "npm:^2.0.10" - memfs: "npm:^3.4.1" + memfs: "npm:^3.4.12" mime-types: "npm:^2.1.31" range-parser: "npm:^1.2.1" schema-utils: "npm:^4.0.0" peerDependencies: - webpack: ^4.0.0 || ^5.0.0 - checksum: 10/4b2609cc6573dfd1247fd6109b32269f32fa87703fefc50913acc3cd05d19d7c9595fe8dbc43ae927f54d58437dcd1a318f7db59def5072daba23890b855bd34 + webpack: ^5.0.0 + peerDependenciesMeta: + webpack: + optional: true + checksum: 10/b0637584f18b02174fd7fc2e6278efb8e2fb5308abe4ffe73658e59ff53a62c05686f161b06bd5c41d42611aa395b8c8f087d7ff8cf2304232c097a694a5b94e languageName: node linkType: hard -"webpack-dev-middleware@npm:^6.1.1": - version: 6.1.1 - resolution: "webpack-dev-middleware@npm:6.1.1" +"webpack-dev-middleware@npm:^7.0.0": + version: 7.0.0 + resolution: "webpack-dev-middleware@npm:7.0.0" dependencies: colorette: "npm:^2.0.10" - memfs: "npm:^3.4.12" + memfs: "npm:^4.6.0" mime-types: "npm:^2.1.31" range-parser: "npm:^1.2.1" schema-utils: "npm:^4.0.0" @@ -31345,46 +31458,46 @@ __metadata: peerDependenciesMeta: webpack: optional: true - checksum: 10/b0637584f18b02174fd7fc2e6278efb8e2fb5308abe4ffe73658e59ff53a62c05686f161b06bd5c41d42611aa395b8c8f087d7ff8cf2304232c097a694a5b94e + checksum: 10/e94902c35458c2431cd3332088cf33afe9236333e911605e6045392be11f521140600312c698263acf2a6c52e2092b8e8b3ea4b0ed34568df6cb7a79913aa1d3 languageName: node linkType: hard -"webpack-dev-server@npm:4.15.1": - version: 4.15.1 - resolution: "webpack-dev-server@npm:4.15.1" - dependencies: - "@types/bonjour": "npm:^3.5.9" - "@types/connect-history-api-fallback": "npm:^1.3.5" - "@types/express": "npm:^4.17.13" - "@types/serve-index": "npm:^1.9.1" - "@types/serve-static": "npm:^1.13.10" - "@types/sockjs": "npm:^0.3.33" - "@types/ws": "npm:^8.5.5" +"webpack-dev-server@npm:5.0.2": + version: 5.0.2 + resolution: "webpack-dev-server@npm:5.0.2" + dependencies: + "@types/bonjour": "npm:^3.5.13" + "@types/connect-history-api-fallback": "npm:^1.5.4" + "@types/express": "npm:^4.17.21" + "@types/serve-index": "npm:^1.9.4" + "@types/serve-static": "npm:^1.15.5" + "@types/sockjs": "npm:^0.3.36" + "@types/ws": "npm:^8.5.10" ansi-html-community: "npm:^0.0.8" - bonjour-service: "npm:^1.0.11" - chokidar: "npm:^3.5.3" + bonjour-service: "npm:^1.2.1" + chokidar: "npm:^3.6.0" colorette: "npm:^2.0.10" compression: "npm:^1.7.4" connect-history-api-fallback: "npm:^2.0.0" default-gateway: "npm:^6.0.3" express: "npm:^4.17.3" graceful-fs: "npm:^4.2.6" - html-entities: "npm:^2.3.2" + html-entities: "npm:^2.4.0" http-proxy-middleware: "npm:^2.0.3" - ipaddr.js: "npm:^2.0.1" - launch-editor: "npm:^2.6.0" - open: "npm:^8.0.9" - p-retry: "npm:^4.5.0" - rimraf: "npm:^3.0.2" - schema-utils: "npm:^4.0.0" - selfsigned: "npm:^2.1.1" + ipaddr.js: "npm:^2.1.0" + launch-editor: "npm:^2.6.1" + open: "npm:^10.0.3" + p-retry: "npm:^6.2.0" + rimraf: "npm:^5.0.5" + schema-utils: "npm:^4.2.0" + selfsigned: "npm:^2.4.1" serve-index: "npm:^1.9.1" sockjs: "npm:^0.3.24" spdy: "npm:^4.0.2" - webpack-dev-middleware: "npm:^5.3.1" - ws: "npm:^8.13.0" + webpack-dev-middleware: "npm:^7.0.0" + ws: "npm:^8.16.0" peerDependencies: - webpack: ^4.37.0 || ^5.0.0 + webpack: ^5.0.0 peerDependenciesMeta: webpack: optional: true @@ -31392,7 +31505,7 @@ __metadata: optional: true bin: webpack-dev-server: bin/webpack-dev-server.js - checksum: 10/fd6dfb6c71eb94696b21930ea4c2f25e95ba85fac1bbc15aa5d03af0a90712eba057901fa9131ed3e901665c95b2379208279aca61e9c48e7cda276c3caa95dd + checksum: 10/f47205b56a562c72083ad979fceb499dc60ef35a75a72b6fbcbccd258b6b304ab3a977877dc6ce68aa5fb90cee8ab9387e22ceb8e374a019e3d4ce77ad0c9493 languageName: node linkType: hard @@ -31877,9 +31990,9 @@ __metadata: languageName: node linkType: hard -"ws@npm:^8.13.0, ws@npm:^8.2.3, ws@npm:^8.9.0": - version: 8.13.0 - resolution: "ws@npm:8.13.0" +"ws@npm:^8.16.0, ws@npm:^8.2.3, ws@npm:^8.9.0": + version: 8.16.0 + resolution: "ws@npm:8.16.0" peerDependencies: bufferutil: ^4.0.1 utf-8-validate: ">=5.0.2" @@ -31888,7 +32001,7 @@ __metadata: optional: true utf-8-validate: optional: true - checksum: 10/1769532b6fdab9ff659f0b17810e7501831d34ecca23fd179ee64091dd93a51f42c59f6c7bb4c7a384b6c229aca8076fb312aa35626257c18081511ef62a161d + checksum: 10/7c511c59e979bd37b63c3aea4a8e4d4163204f00bd5633c053b05ed67835481995f61a523b0ad2b603566f9a89b34cb4965cb9fab9649fbfebd8f740cea57f17 languageName: node linkType: hard From d4bc0fe01855f54343378fc0efd6a754c10105b0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 23 Feb 2024 11:56:38 +0200 Subject: [PATCH 0118/1406] Update dependency stylelint to v16 (#83252) * Update dependency stylelint to v16 * remove stylelint-config-prettier since it's no longer necessary --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Ashley Harrison --- package.json | 3 +- stylelint.config.js | 5 +- yarn.lock | 280 +++++++++++++------------------------------- 3 files changed, 82 insertions(+), 206 deletions(-) diff --git a/package.json b/package.json index 10aca17d6b..6728f3302a 100644 --- a/package.json +++ b/package.json @@ -205,8 +205,7 @@ "sass": "1.70.0", "sass-loader": "14.1.1", "style-loader": "3.3.4", - "stylelint": "15.11.0", - "stylelint-config-prettier": "9.0.5", + "stylelint": "16.2.1", "stylelint-config-sass-guidelines": "11.0.0", "terser-webpack-plugin": "5.3.10", "testing-library-selector": "0.3.1", diff --git a/stylelint.config.js b/stylelint.config.js index 442ea0945a..e92d9fd3e0 100644 --- a/stylelint.config.js +++ b/stylelint.config.js @@ -1,9 +1,8 @@ module.exports = { - extends: ['stylelint-config-sass-guidelines', 'stylelint-config-prettier'], + extends: ['stylelint-config-sass-guidelines'], ignoreFiles: ['**/node_modules/**/*.scss'], rules: { 'at-rule-no-vendor-prefix': null, - 'color-hex-case': null, 'color-hex-length': null, 'color-named': null, 'declaration-block-no-duplicate-properties': [ @@ -21,11 +20,9 @@ module.exports = { 'border-right': [], 'border-top': [], }, - 'function-comma-space-after': null, 'function-url-quotes': null, 'length-zero-no-unit': null, 'max-nesting-depth': null, - 'number-no-trailing-zeros': null, 'property-no-vendor-prefix': null, 'rule-empty-line-before': null, 'scss/at-function-pattern': null, diff --git a/yarn.lock b/yarn.lock index 336870d051..800393ba18 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1899,38 +1899,38 @@ __metadata: languageName: node linkType: hard -"@csstools/css-parser-algorithms@npm:^2.3.1": - version: 2.3.2 - resolution: "@csstools/css-parser-algorithms@npm:2.3.2" +"@csstools/css-parser-algorithms@npm:^2.5.0": + version: 2.6.0 + resolution: "@csstools/css-parser-algorithms@npm:2.6.0" peerDependencies: - "@csstools/css-tokenizer": ^2.2.1 - checksum: 10/879798303bd30e3768dfcad2ee6e74170e8adc08a674ff1f1b4a9bedd3e51494f97dc35bec22e337d6ac0ad4af04b182c5520382972d1612ff0f4fa10855f98f + "@csstools/css-tokenizer": ^2.2.3 + checksum: 10/f8131e6c0e87879c9c6c608e809fba2e2a2e4e7f958c362f3b3e59f0856bbc1d95ba766782e708009af69ddc1470773ae59f0e1246ba8baa7a0c209a75b0e8b5 languageName: node linkType: hard -"@csstools/css-tokenizer@npm:^2.2.0": - version: 2.2.1 - resolution: "@csstools/css-tokenizer@npm:2.2.1" - checksum: 10/4bd013b1cd28008979bb2decece261d96d1387711ac417d871cef969ee13da272e87945f2e4ef5ddc49e27e8bb16829cbd08d2064e4e8363c86367e77181dfc9 +"@csstools/css-tokenizer@npm:^2.2.3": + version: 2.2.3 + resolution: "@csstools/css-tokenizer@npm:2.2.3" + checksum: 10/cf0c191cd6a9cdc0e85dd2a0472bd6c3ff394d31752dd6ee1e1bb21a35ad9fe116835ef9a804e7b8f3cf7845581bf01f975a6371200fe6f469b1971011459b52 languageName: node linkType: hard -"@csstools/media-query-list-parser@npm:^2.1.4": - version: 2.1.5 - resolution: "@csstools/media-query-list-parser@npm:2.1.5" +"@csstools/media-query-list-parser@npm:^2.1.7": + version: 2.1.8 + resolution: "@csstools/media-query-list-parser@npm:2.1.8" peerDependencies: - "@csstools/css-parser-algorithms": ^2.3.2 - "@csstools/css-tokenizer": ^2.2.1 - checksum: 10/8c5e60d5bbca001a8a4fed71d0495eb0a6f06eb596951ebfd33cb446a6753318b8ebf20ed9e3adf4ee4e0933fbe09cc1e13a48907d19a5327269e44b6e06f86d + "@csstools/css-parser-algorithms": ^2.6.0 + "@csstools/css-tokenizer": ^2.2.3 + checksum: 10/256e85390fa39b40103c68a7d731b2b1631020ed8b513bf19d8bc17a998d7a16fc165dcb11e34ce197f16ed78f0cfad3ed7c454b3deedd9850f0e304d7387f37 languageName: node linkType: hard -"@csstools/selector-specificity@npm:^3.0.0": - version: 3.0.0 - resolution: "@csstools/selector-specificity@npm:3.0.0" +"@csstools/selector-specificity@npm:^3.0.1": + version: 3.0.2 + resolution: "@csstools/selector-specificity@npm:3.0.2" peerDependencies: postcss-selector-parser: ^6.0.13 - checksum: 10/4a2dfe69998a499155d9dab4c2a0e7ae7594d8db98bb8a487d2d5347c0c501655051eb5eacad3fe323c86b0ba8212fe092c27fc883621e6ac2a27662edfc3528 + checksum: 10/af3cc9282b600170b7de0fed2106830ab353359bd11f66cf71259419c9bddf8f0773c3a6e513cd9f66fd7e4920a1786a7c288723cbb3ae207974c1e7de26293e languageName: node linkType: hard @@ -9682,7 +9682,7 @@ __metadata: languageName: node linkType: hard -"@types/minimist@npm:^1.2.0, @types/minimist@npm:^1.2.2": +"@types/minimist@npm:^1.2.0": version: 1.2.2 resolution: "@types/minimist@npm:1.2.2" checksum: 10/b8da83c66eb4aac0440e64674b19564d9d86c80ae273144db9681e5eeff66f238ade9515f5006ffbfa955ceff8b89ad2bd8ec577d7caee74ba101431fb07045d @@ -12862,18 +12862,6 @@ __metadata: languageName: node linkType: hard -"camelcase-keys@npm:^7.0.0": - version: 7.0.2 - resolution: "camelcase-keys@npm:7.0.2" - dependencies: - camelcase: "npm:^6.3.0" - map-obj: "npm:^4.1.0" - quick-lru: "npm:^5.1.1" - type-fest: "npm:^1.2.1" - checksum: 10/6f92d969b7fa97456ffc35fe93f0a42d0d0a00fbd94bfc6cac07c84da86e6acfb89fdf04151460d47c583d2dd38a3e9406f980efe9a3d2e143cdfe46a7343083 - languageName: node - linkType: hard - "camelcase@npm:^5.3.1": version: 5.3.1 resolution: "camelcase@npm:5.3.1" @@ -12881,7 +12869,7 @@ __metadata: languageName: node linkType: hard -"camelcase@npm:^6.0.0, camelcase@npm:^6.2.0, camelcase@npm:^6.3.0": +"camelcase@npm:^6.0.0, camelcase@npm:^6.2.0": version: 6.3.0 resolution: "camelcase@npm:6.3.0" checksum: 10/8c96818a9076434998511251dcb2761a94817ea17dbdc37f47ac080bd088fc62c7369429a19e2178b993497132c8cbcf5cc1f44ba963e76782ba469c0474938d @@ -15024,13 +15012,6 @@ __metadata: languageName: node linkType: hard -"decamelize@npm:^5.0.0": - version: 5.0.1 - resolution: "decamelize@npm:5.0.1" - checksum: 10/643e88804c538a334fae303ae1da8b30193b81dad8689643b35e6ab8ab60a3b03492cab6096d8163bd41fd384d969485f0634c000f80af502aa7f4047258d5b4 - languageName: node - linkType: hard - "decimal.js@npm:^10.4.1": version: 10.4.2 resolution: "decimal.js@npm:10.4.2" @@ -17166,7 +17147,7 @@ __metadata: languageName: node linkType: hard -"fast-glob@npm:^3.0.3, fast-glob@npm:^3.2.11, fast-glob@npm:^3.2.9, fast-glob@npm:^3.3.1, fast-glob@npm:^3.3.2": +"fast-glob@npm:^3.0.3, fast-glob@npm:^3.2.11, fast-glob@npm:^3.2.9, fast-glob@npm:^3.3.2": version: 3.3.2 resolution: "fast-glob@npm:3.3.2" dependencies: @@ -17326,12 +17307,12 @@ __metadata: languageName: node linkType: hard -"file-entry-cache@npm:^7.0.0": - version: 7.0.1 - resolution: "file-entry-cache@npm:7.0.1" +"file-entry-cache@npm:^8.0.0": + version: 8.0.0 + resolution: "file-entry-cache@npm:8.0.0" dependencies: - flat-cache: "npm:^3.1.1" - checksum: 10/52f83f8b880b1d69c0943645a8bf124a129f0381980cfbd9b4606f8f99f4798e7fdc4f470b69d95ca87187da51de908249a205ea61614ee2eeb10ea090c8eba6 + flat-cache: "npm:^4.0.0" + checksum: 10/afe55c4de4e0d226a23c1eae62a7219aafb390859122608a89fa4df6addf55c7fd3f1a2da6f5b41e7cdff496e4cf28bbd215d53eab5c817afa96d2b40c81bfb0 languageName: node linkType: hard @@ -17488,7 +17469,7 @@ __metadata: languageName: node linkType: hard -"flat-cache@npm:^3.0.4, flat-cache@npm:^3.1.1": +"flat-cache@npm:^3.0.4": version: 3.1.1 resolution: "flat-cache@npm:3.1.1" dependencies: @@ -17499,6 +17480,17 @@ __metadata: languageName: node linkType: hard +"flat-cache@npm:^4.0.0": + version: 4.0.0 + resolution: "flat-cache@npm:4.0.0" + dependencies: + flatted: "npm:^3.2.9" + keyv: "npm:^4.5.4" + rimraf: "npm:^5.0.5" + checksum: 10/344c60d397fab339b86b317d5c32dedeb31142b72160d7e17e0fc218c0a5f0aa09a48441ec8f5638e18c723c1d923a3d2a2eb922ae58656963306b42d2f47aec + languageName: node + linkType: hard + "flat@npm:^5.0.2": version: 5.0.2 resolution: "flat@npm:5.0.2" @@ -18740,8 +18732,7 @@ __metadata: slate-plain-serializer: "npm:0.7.13" slate-react: "npm:0.22.10" style-loader: "npm:3.3.4" - stylelint: "npm:15.11.0" - stylelint-config-prettier: "npm:9.0.5" + stylelint: "npm:16.2.1" stylelint-config-sass-guidelines: "npm:11.0.0" symbol-observable: "npm:4.0.0" terser-webpack-plugin: "npm:5.3.10" @@ -19603,10 +19594,10 @@ __metadata: languageName: node linkType: hard -"ignore@npm:^5.0.4, ignore@npm:^5.1.1, ignore@npm:^5.2.0, ignore@npm:^5.2.4": - version: 5.2.4 - resolution: "ignore@npm:5.2.4" - checksum: 10/4f7caf5d2005da21a382d4bd1d2aa741a3bed51de185c8562dd7f899a81a620ac4fd0619b06f7029a38ae79e4e4c134399db3bd0192c703c3ef54bb82df3086c +"ignore@npm:^5.0.4, ignore@npm:^5.1.1, ignore@npm:^5.2.0, ignore@npm:^5.2.4, ignore@npm:^5.3.0": + version: 5.3.1 + resolution: "ignore@npm:5.3.1" + checksum: 10/0a884c2fbc8c316f0b9f92beaf84464253b73230a4d4d286697be45fca081199191ca33e1c2e82d9e5f851f5e9a48a78e25a35c951e7eb41e59f150db3530065 languageName: node linkType: hard @@ -19648,13 +19639,6 @@ __metadata: languageName: node linkType: hard -"import-lazy@npm:^4.0.0": - version: 4.0.0 - resolution: "import-lazy@npm:4.0.0" - checksum: 10/943309cc8eb01ada12700448c288b0384f77a1bc33c7e00fa4cb223c665f467a13ce9aaceb8d2e4cf586b07c1d2828040263dcc069873ce63cfc2ac6fd087971 - languageName: node - linkType: hard - "import-local@npm:3.1.0, import-local@npm:^3.0.2": version: 3.1.0 resolution: "import-local@npm:3.1.0" @@ -19681,13 +19665,6 @@ __metadata: languageName: node linkType: hard -"indent-string@npm:^5.0.0": - version: 5.0.0 - resolution: "indent-string@npm:5.0.0" - checksum: 10/e466c27b6373440e6d84fbc19e750219ce25865cb82d578e41a6053d727e5520dc5725217d6eb1cc76005a1bb1696a0f106d84ce7ebda3033b963a38583fb3b3 - languageName: node - linkType: hard - "infer-owner@npm:^1.0.4": version: 1.0.4 resolution: "infer-owner@npm:1.0.4" @@ -21679,7 +21656,7 @@ __metadata: languageName: node linkType: hard -"keyv@npm:^4.5.3": +"keyv@npm:^4.5.3, keyv@npm:^4.5.4": version: 4.5.4 resolution: "keyv@npm:4.5.4" dependencies: @@ -22423,7 +22400,7 @@ __metadata: languageName: node linkType: hard -"map-obj@npm:^4.0.0, map-obj@npm:^4.1.0": +"map-obj@npm:^4.0.0": version: 4.3.0 resolution: "map-obj@npm:4.3.0" checksum: 10/fbc554934d1a27a1910e842bc87b177b1a556609dd803747c85ece420692380827c6ae94a95cce4407c054fa0964be3bf8226f7f2cb2e9eeee432c7c1985684e @@ -22590,23 +22567,10 @@ __metadata: languageName: node linkType: hard -"meow@npm:^10.1.5": - version: 10.1.5 - resolution: "meow@npm:10.1.5" - dependencies: - "@types/minimist": "npm:^1.2.2" - camelcase-keys: "npm:^7.0.0" - decamelize: "npm:^5.0.0" - decamelize-keys: "npm:^1.1.0" - hard-rejection: "npm:^2.1.0" - minimist-options: "npm:4.1.0" - normalize-package-data: "npm:^3.0.2" - read-pkg-up: "npm:^8.0.0" - redent: "npm:^4.0.0" - trim-newlines: "npm:^4.0.2" - type-fest: "npm:^1.2.2" - yargs-parser: "npm:^20.2.9" - checksum: 10/4d6d4c233b9405bace4fd6c60db0b5806d7186a047852ddce0748e56a57c75d4fef3ab2603a480bd74595e4e8e3a47b932d737397a62e043da1d3187f1240ff4 +"meow@npm:^13.1.0": + version: 13.2.0 + resolution: "meow@npm:13.2.0" + checksum: 10/4eff5bc921fed0b8a471ad79069d741a0210036d717547d0c7f36fdaf84ef7a3036225f38b6a53830d84dc9cbf8b944b097fde62381b8b5b215119e735ce1063 languageName: node linkType: hard @@ -22715,7 +22679,7 @@ __metadata: languageName: node linkType: hard -"min-indent@npm:^1.0.0, min-indent@npm:^1.0.1": +"min-indent@npm:^1.0.0": version: 1.0.1 resolution: "min-indent@npm:1.0.1" checksum: 10/bfc6dd03c5eaf623a4963ebd94d087f6f4bbbfd8c41329a7f09706b0cb66969c4ddd336abeb587bc44bc6f08e13bf90f0b374f9d71f9f01e04adc2cd6f083ef1 @@ -23579,7 +23543,7 @@ __metadata: languageName: node linkType: hard -"normalize-package-data@npm:^3.0.0, normalize-package-data@npm:^3.0.2, normalize-package-data@npm:^3.0.3": +"normalize-package-data@npm:^3.0.0, normalize-package-data@npm:^3.0.3": version: 3.0.3 resolution: "normalize-package-data@npm:3.0.3" dependencies: @@ -25247,12 +25211,12 @@ __metadata: languageName: node linkType: hard -"postcss-safe-parser@npm:^6.0.0": - version: 6.0.0 - resolution: "postcss-safe-parser@npm:6.0.0" +"postcss-safe-parser@npm:^7.0.0": + version: 7.0.0 + resolution: "postcss-safe-parser@npm:7.0.0" peerDependencies: - postcss: ^8.3.3 - checksum: 10/06c733eaad83a3954367e7ee02ddfe3796e7a44d4299ccf9239f40964a4daac153c7d77613f32964b5a86c0c6c2f6167738f31d578b73b17cb69d0c4446f0ebe + postcss: ^8.4.31 + checksum: 10/dba4d782393e6f07339c24bdb8b41166e483d5e7b8f34174c35c64065aef36aadef94b53e0501d7a630d42f51bbd824671e8fb1c2b417333b08b71c9b0066c76 languageName: node linkType: hard @@ -25265,7 +25229,7 @@ __metadata: languageName: node linkType: hard -"postcss-selector-parser@npm:^6.0.11, postcss-selector-parser@npm:^6.0.13, postcss-selector-parser@npm:^6.0.15, postcss-selector-parser@npm:^6.0.2, postcss-selector-parser@npm:^6.0.4": +"postcss-selector-parser@npm:^6.0.11, postcss-selector-parser@npm:^6.0.15, postcss-selector-parser@npm:^6.0.2, postcss-selector-parser@npm:^6.0.4": version: 6.0.15 resolution: "postcss-selector-parser@npm:6.0.15" dependencies: @@ -25305,7 +25269,7 @@ __metadata: languageName: node linkType: hard -"postcss@npm:8.4.35, postcss@npm:^8.4.28, postcss@npm:^8.4.33": +"postcss@npm:8.4.35, postcss@npm:^8.4.33": version: 8.4.35 resolution: "postcss@npm:8.4.35" dependencies: @@ -25757,13 +25721,6 @@ __metadata: languageName: node linkType: hard -"quick-lru@npm:^5.1.1": - version: 5.1.1 - resolution: "quick-lru@npm:5.1.1" - checksum: 10/a516faa25574be7947969883e6068dbe4aa19e8ef8e8e0fd96cddd6d36485e9106d85c0041a27153286b0770b381328f4072aa40d3b18a19f5f7d2b78b94b5ed - languageName: node - linkType: hard - "quick-lru@npm:^6.1.1": version: 6.1.1 resolution: "quick-lru@npm:6.1.1" @@ -27099,17 +27056,6 @@ __metadata: languageName: node linkType: hard -"read-pkg-up@npm:^8.0.0": - version: 8.0.0 - resolution: "read-pkg-up@npm:8.0.0" - dependencies: - find-up: "npm:^5.0.0" - read-pkg: "npm:^6.0.0" - type-fest: "npm:^1.0.1" - checksum: 10/fe4c80401656b40b408884457fffb5a8015c03b1018cfd8e48f8d82a5e9023e24963603aeb2755608d964593e046c15b34d29b07d35af9c7aa478be81805209c - languageName: node - linkType: hard - "read-pkg@npm:^3.0.0": version: 3.0.0 resolution: "read-pkg@npm:3.0.0" @@ -27133,18 +27079,6 @@ __metadata: languageName: node linkType: hard -"read-pkg@npm:^6.0.0": - version: 6.0.0 - resolution: "read-pkg@npm:6.0.0" - dependencies: - "@types/normalize-package-data": "npm:^2.4.0" - normalize-package-data: "npm:^3.0.2" - parse-json: "npm:^5.2.0" - type-fest: "npm:^1.0.1" - checksum: 10/0cebdff381128e923815c643074a87011070e5fc352bee575d327d6485da3317fab6d802a7b03deeb0be7be8d3ad1640397b3d5d2f044452caf4e8d1736bf94f - languageName: node - linkType: hard - "read@npm:^2.0.0": version: 2.1.0 resolution: "read@npm:2.1.0" @@ -27245,16 +27179,6 @@ __metadata: languageName: node linkType: hard -"redent@npm:^4.0.0": - version: 4.0.0 - resolution: "redent@npm:4.0.0" - dependencies: - indent-string: "npm:^5.0.0" - strip-indent: "npm:^4.0.0" - checksum: 10/6944e7b1d8f3fd28c2515f5c605b9f7f0ea0f4edddf41890bbbdd4d9ee35abb7540c3b278f03ff827bd278bb6ff4a5bd8692ca406b748c5c1c3ce7355e9fbf8f - languageName: node - linkType: hard - "redux-mock-store@npm:1.5.4": version: 1.5.4 resolution: "redux-mock-store@npm:1.5.4" @@ -29333,12 +29257,12 @@ __metadata: languageName: node linkType: hard -"strip-ansi@npm:^7.0.1": - version: 7.0.1 - resolution: "strip-ansi@npm:7.0.1" +"strip-ansi@npm:^7.0.1, strip-ansi@npm:^7.1.0": + version: 7.1.0 + resolution: "strip-ansi@npm:7.1.0" dependencies: ansi-regex: "npm:^6.0.1" - checksum: 10/07b3142f515d673e05d2da1ae07bba1eb2ba3b588135a38dea598ca11913b6e9487a9f2c9bed4c74cd31e554012b4503d9fb7e6034c7324973854feea2319110 + checksum: 10/475f53e9c44375d6e72807284024ac5d668ee1d06010740dec0b9744f2ddf47de8d7151f80e5f6190fc8f384e802fdf9504b76a7e9020c9faee7103623338be2 languageName: node linkType: hard @@ -29372,15 +29296,6 @@ __metadata: languageName: node linkType: hard -"strip-indent@npm:^4.0.0": - version: 4.0.0 - resolution: "strip-indent@npm:4.0.0" - dependencies: - min-indent: "npm:^1.0.1" - checksum: 10/06cbcd93da721c46bc13caeb1c00af93a9b18146a1c95927672d2decab6a25ad83662772417cea9317a2507fb143253ecc23c4415b64f5828cef9b638a744598 - languageName: node - linkType: hard - "strip-json-comments@npm:3.1.1, strip-json-comments@npm:^3.0.1, strip-json-comments@npm:^3.1.1": version: 3.1.1 resolution: "strip-json-comments@npm:3.1.1" @@ -29410,13 +29325,6 @@ __metadata: languageName: node linkType: hard -"style-search@npm:^0.1.0": - version: 0.1.0 - resolution: "style-search@npm:0.1.0" - checksum: 10/841049768c863737389558fafffa0b765f553bde041b7997c4cd54606b64b0d139936e2efee74dc1ce59fcde78aaa88484d9894838c31d5c98c1ccace312a59b - languageName: node - linkType: hard - "stylehacks@npm:^6.0.2": version: 6.0.2 resolution: "stylehacks@npm:6.0.2" @@ -29429,18 +29337,6 @@ __metadata: languageName: node linkType: hard -"stylelint-config-prettier@npm:9.0.5": - version: 9.0.5 - resolution: "stylelint-config-prettier@npm:9.0.5" - peerDependencies: - stylelint: ">= 11.x < 15" - bin: - stylelint-config-prettier: bin/check.js - stylelint-config-prettier-check: bin/check.js - checksum: 10/f00665801f65093269987eea0f6c0e1f4a479c76917da887577bf6063757575f5b752aa923d2677f87d32a14d528569ee0a869452dc41b0c3b61b5dcbec037f0 - languageName: node - linkType: hard - "stylelint-config-sass-guidelines@npm:11.0.0": version: 11.0.0 resolution: "stylelint-config-sass-guidelines@npm:11.0.0" @@ -29469,53 +29365,51 @@ __metadata: languageName: node linkType: hard -"stylelint@npm:15.11.0": - version: 15.11.0 - resolution: "stylelint@npm:15.11.0" +"stylelint@npm:16.2.1": + version: 16.2.1 + resolution: "stylelint@npm:16.2.1" dependencies: - "@csstools/css-parser-algorithms": "npm:^2.3.1" - "@csstools/css-tokenizer": "npm:^2.2.0" - "@csstools/media-query-list-parser": "npm:^2.1.4" - "@csstools/selector-specificity": "npm:^3.0.0" + "@csstools/css-parser-algorithms": "npm:^2.5.0" + "@csstools/css-tokenizer": "npm:^2.2.3" + "@csstools/media-query-list-parser": "npm:^2.1.7" + "@csstools/selector-specificity": "npm:^3.0.1" balanced-match: "npm:^2.0.0" colord: "npm:^2.9.3" - cosmiconfig: "npm:^8.2.0" + cosmiconfig: "npm:^9.0.0" css-functions-list: "npm:^3.2.1" css-tree: "npm:^2.3.1" debug: "npm:^4.3.4" - fast-glob: "npm:^3.3.1" + fast-glob: "npm:^3.3.2" fastest-levenshtein: "npm:^1.0.16" - file-entry-cache: "npm:^7.0.0" + file-entry-cache: "npm:^8.0.0" global-modules: "npm:^2.0.0" globby: "npm:^11.1.0" globjoin: "npm:^0.1.4" html-tags: "npm:^3.3.1" - ignore: "npm:^5.2.4" - import-lazy: "npm:^4.0.0" + ignore: "npm:^5.3.0" imurmurhash: "npm:^0.1.4" is-plain-object: "npm:^5.0.0" known-css-properties: "npm:^0.29.0" mathml-tag-names: "npm:^2.1.3" - meow: "npm:^10.1.5" + meow: "npm:^13.1.0" micromatch: "npm:^4.0.5" normalize-path: "npm:^3.0.0" picocolors: "npm:^1.0.0" - postcss: "npm:^8.4.28" + postcss: "npm:^8.4.33" postcss-resolve-nested-selector: "npm:^0.1.1" - postcss-safe-parser: "npm:^6.0.0" - postcss-selector-parser: "npm:^6.0.13" + postcss-safe-parser: "npm:^7.0.0" + postcss-selector-parser: "npm:^6.0.15" postcss-value-parser: "npm:^4.2.0" resolve-from: "npm:^5.0.0" string-width: "npm:^4.2.3" - strip-ansi: "npm:^6.0.1" - style-search: "npm:^0.1.0" + strip-ansi: "npm:^7.1.0" supports-hyperlinks: "npm:^3.0.0" svg-tags: "npm:^1.0.0" table: "npm:^6.8.1" write-file-atomic: "npm:^5.0.1" bin: stylelint: bin/stylelint.mjs - checksum: 10/34b9242b8a009642f8a9a50319c9a6c94b745a8605890df99830fc4d4847031e59719e68df12eed897fd486724fbfb1d240a8f267bb8b4440152a4dbfc3765f5 + checksum: 10/e2f8a5d273788239f4c5bd5c4d9a9737ec8062ebd6d247a51466bfce434e5da28e6346cfcf00a1b9f45738070a65d90675624288a9afe8346f7c1027821005a1 languageName: node linkType: hard @@ -30143,13 +30037,6 @@ __metadata: languageName: node linkType: hard -"trim-newlines@npm:^4.0.2": - version: 4.1.1 - resolution: "trim-newlines@npm:4.1.1" - checksum: 10/5b09f8e329e8f33c1111ef26906332ba7ba7248cde3e26fc054bb3d69f2858bf5feedca9559c572ff91f33e52977c28e0d41c387df6a02a633cbb8c2d8238627 - languageName: node - linkType: hard - "true-case-path@npm:^1.0.3": version: 1.0.3 resolution: "true-case-path@npm:1.0.3" @@ -30490,13 +30377,6 @@ __metadata: languageName: node linkType: hard -"type-fest@npm:^1.0.1, type-fest@npm:^1.2.1, type-fest@npm:^1.2.2": - version: 1.4.0 - resolution: "type-fest@npm:1.4.0" - checksum: 10/89875c247564601c2650bacad5ff80b859007fbdb6c9e43713ae3ffa3f584552eea60f33711dd762e16496a1ab4debd409822627be14097d9a17e39c49db591a - languageName: node - linkType: hard - "type-fest@npm:^2.19.0, type-fest@npm:~2.19": version: 2.19.0 resolution: "type-fest@npm:2.19.0" From fa37d8467f300854f16a77a6ef0cc03f9554fe5d Mon Sep 17 00:00:00 2001 From: Ben Donnelly Date: Fri, 23 Feb 2024 10:14:54 +0000 Subject: [PATCH 0119/1406] =?UTF-8?q?fix(build):=20make=20python=20and=20g?= =?UTF-8?q?cc=20are=20needed=20for=20some=20yarn=20dependencies=E2=80=A6?= =?UTF-8?q?=20(#83228)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix(build): make python and gcc are needed for some yarn dependencies to build --- Dockerfile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 99d25ac1d5..57c5bbacf9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,7 +20,9 @@ COPY packages packages COPY plugins-bundled plugins-bundled COPY public public -RUN yarn install --immutable +RUN apk add --no-cache make build-base python3 + +RUN yarn install --immutable --inline-builds COPY tsconfig.json .eslintrc .editorconfig .browserslistrc .prettierrc.js ./ COPY public public From c63456612eeff8538772dce8a83dc0d56bdfb1ea Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Fri, 23 Feb 2024 10:45:04 +0000 Subject: [PATCH 0120/1406] Chore: replace `react-popper` with `floating-ui` in time pickers (#82640) * replace react-popper with floating-ui in RelativeTimeRangePicker * replace react-popper with floating-ui in DateTimePicker --- .../DateTimePicker/DateTimePicker.tsx | 27 ++++++++--- .../RelativeTimeRangePicker.tsx | 46 +++++++++++++------ 2 files changed, 53 insertions(+), 20 deletions(-) diff --git a/packages/grafana-ui/src/components/DateTimePickers/DateTimePicker/DateTimePicker.tsx b/packages/grafana-ui/src/components/DateTimePickers/DateTimePicker/DateTimePicker.tsx index e0e7f45a11..4dfb5180d4 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/DateTimePicker/DateTimePicker.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/DateTimePicker/DateTimePicker.tsx @@ -1,10 +1,10 @@ import { css, cx } from '@emotion/css'; +import { autoUpdate, flip, shift, useFloating } from '@floating-ui/react'; import { useDialog } from '@react-aria/dialog'; import { FocusScope } from '@react-aria/focus'; import { useOverlay } from '@react-aria/overlays'; import React, { FormEvent, ReactNode, useCallback, useEffect, useRef, useState } from 'react'; import Calendar from 'react-calendar'; -import { usePopper } from 'react-popper'; import { useMedia } from 'react-use'; import { dateTimeFormat, DateTime, dateTime, GrafanaTheme2, isDateTime } from '@grafana/data'; @@ -76,11 +76,24 @@ export const DateTimePicker = ({ const isFullscreen = useMedia(`(min-width: ${theme.breakpoints.values.lg}px)`); const styles = useStyles2(getStyles); - const [markerElement, setMarkerElement] = useState(); - const [selectorElement, setSelectorElement] = useState(); + // the order of middleware is important! + // see https://floating-ui.com/docs/arrow#order + const middleware = [ + flip({ + // see https://floating-ui.com/docs/flip#combining-with-shift + crossAxis: false, + boundary: document.body, + }), + shift(), + ]; - const popper = usePopper(markerElement, selectorElement, { + const { refs, floatingStyles } = useFloating({ + open: isOpen, placement: 'bottom-start', + onOpenChange: setOpen, + middleware, + whileElementsMounted: autoUpdate, + strategy: 'fixed', }); const onApply = useCallback( @@ -107,7 +120,7 @@ export const DateTimePicker = ({ isFullscreen={isFullscreen} onOpen={onOpen} label={label} - ref={setMarkerElement} + ref={refs.setReference} showSeconds={showSeconds} /> {isOpen ? ( @@ -122,8 +135,8 @@ export const DateTimePicker = ({ onClose={() => setOpen(false)} maxDate={maxDate} minDate={minDate} - ref={setSelectorElement} - style={popper.styles.popper} + ref={refs.setFloating} + style={floatingStyles} showSeconds={showSeconds} disabledHours={disabledHours} disabledMinutes={disabledMinutes} diff --git a/packages/grafana-ui/src/components/DateTimePickers/RelativeTimeRangePicker/RelativeTimeRangePicker.tsx b/packages/grafana-ui/src/components/DateTimePickers/RelativeTimeRangePicker/RelativeTimeRangePicker.tsx index ec797abbb7..b7f5b76bda 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/RelativeTimeRangePicker/RelativeTimeRangePicker.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/RelativeTimeRangePicker/RelativeTimeRangePicker.tsx @@ -1,9 +1,9 @@ import { css, cx } from '@emotion/css'; +import { autoUpdate, flip, shift, useClick, useDismiss, useFloating, useInteractions } from '@floating-ui/react'; import { useDialog } from '@react-aria/dialog'; import { FocusScope } from '@react-aria/focus'; import { useOverlay } from '@react-aria/overlays'; import React, { FormEvent, useCallback, useRef, useState } from 'react'; -import { usePopper } from 'react-popper'; import { RelativeTimeRange, GrafanaTheme2, TimeOption } from '@grafana/data'; @@ -59,12 +59,31 @@ export function RelativeTimeRangePicker(props: RelativeTimeRangePickerProps) { ); const { dialogProps } = useDialog({}, ref); - const [markerElement, setMarkerElement] = useState(null); - const [selectorElement, setSelectorElement] = useState(null); - const popper = usePopper(markerElement, selectorElement, { - placement: 'auto-start', + // the order of middleware is important! + // see https://floating-ui.com/docs/arrow#order + const middleware = [ + flip({ + // see https://floating-ui.com/docs/flip#combining-with-shift + crossAxis: false, + boundary: document.body, + }), + shift(), + ]; + + const { context, refs, floatingStyles } = useFloating({ + open: isOpen, + placement: 'bottom-start', + onOpenChange: setIsOpen, + middleware, + whileElementsMounted: autoUpdate, + strategy: 'fixed', }); + const click = useClick(context); + const dismiss = useDismiss(context); + + const { getReferenceProps, getFloatingProps } = useInteractions([dismiss, click]); + const styles = useStyles2(getStyles(from.validation.errorMessage, to.validation.errorMessage)); const onChangeTimeOption = (option: TimeOption) => { @@ -109,8 +128,14 @@ export function RelativeTimeRangePicker(props: RelativeTimeRangePickerProps) { }; return ( -
- + + + ); +}; diff --git a/public/app/features/admin/migrate-to-cloud/EmptyState/EmptyState.tsx b/public/app/features/admin/migrate-to-cloud/EmptyState/EmptyState.tsx new file mode 100644 index 0000000000..09c01b4009 --- /dev/null +++ b/public/app/features/admin/migrate-to-cloud/EmptyState/EmptyState.tsx @@ -0,0 +1,37 @@ +import { css } from '@emotion/css'; +import React from 'react'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { Grid, Stack, useStyles2 } from '@grafana/ui'; + +import { CallToAction } from './CallToAction'; +import { InfoPaneLeft } from './InfoPaneLeft'; +import { InfoPaneRight } from './InfoPaneRight'; + +export const EmptyState = () => { + const styles = useStyles2(getStyles); + + return ( +
+ + + + + + + +
+ ); +}; + +const getStyles = (theme: GrafanaTheme2) => ({ + container: css({ + maxWidth: theme.breakpoints.values.xl, + }), +}); diff --git a/public/app/features/admin/migrate-to-cloud/EmptyState/InfoItem.tsx b/public/app/features/admin/migrate-to-cloud/EmptyState/InfoItem.tsx new file mode 100644 index 0000000000..5c107d8bf8 --- /dev/null +++ b/public/app/features/admin/migrate-to-cloud/EmptyState/InfoItem.tsx @@ -0,0 +1,26 @@ +import React, { ReactNode } from 'react'; + +import { Stack, Text, TextLink } from '@grafana/ui'; + +interface Props { + children: NonNullable; + title: string; + linkTitle?: string; + linkHref?: string; +} + +export const InfoItem = ({ children, title, linkHref, linkTitle }: Props) => { + return ( + + {title} + + {children} + + {linkHref && ( + + {linkTitle ?? linkHref} + + )} + + ); +}; diff --git a/public/app/features/admin/migrate-to-cloud/EmptyState/InfoPaneLeft.tsx b/public/app/features/admin/migrate-to-cloud/EmptyState/InfoPaneLeft.tsx new file mode 100644 index 0000000000..91eceb990e --- /dev/null +++ b/public/app/features/admin/migrate-to-cloud/EmptyState/InfoPaneLeft.tsx @@ -0,0 +1,47 @@ +import React from 'react'; + +import { Box, Stack } from '@grafana/ui'; +import { t, Trans } from 'app/core/internationalization'; + +import { InfoItem } from './InfoItem'; + +export const InfoPaneLeft = () => { + return ( + + + + + Grafana cloud is a fully managed cloud-hosted observability platform ideal for cloud native environments. + It's everything you love about Grafana without the overhead of maintaining, upgrading, and supporting + an installation. + + + + + In addition to the convenience of managed hosting, Grafana Cloud includes many cloud-exclusive features like + SLOs, incident management, machine learning, and powerful observability integrations. + + + + + Grafana Labs is committed to maintaining the highest standards of data privacy and security. By implementing + industry-standard security technologies and procedures, we help protect our customers' data from + unauthorized access, use, or disclosure. + + + + + ); +}; diff --git a/public/app/features/admin/migrate-to-cloud/EmptyState/InfoPaneRight.tsx b/public/app/features/admin/migrate-to-cloud/EmptyState/InfoPaneRight.tsx new file mode 100644 index 0000000000..48cbb1acd8 --- /dev/null +++ b/public/app/features/admin/migrate-to-cloud/EmptyState/InfoPaneRight.tsx @@ -0,0 +1,45 @@ +import React from 'react'; + +import { Box, Stack } from '@grafana/ui'; +import { t, Trans } from 'app/core/internationalization'; + +import { InfoItem } from './InfoItem'; + +export const InfoPaneRight = () => { + return ( + + + + + Exposing your data sources to the internet can raise security concerns. Private data source connect (PDC) + allows Grafana Cloud to access your existing data sources over a secure network tunnel. + + + + + Grafana Cloud has a generous free plan and a 14 day unlimited usage trial. After your trial expires, + you'll be billed based on usage over the free plan limits. + + + + + Once you connect this installation to a cloud stack, you'll be able to upload data sources and + dashboards. + + + + + ); +}; diff --git a/public/app/features/admin/migrate-to-cloud/MigrateToCloud.tsx b/public/app/features/admin/migrate-to-cloud/MigrateToCloud.tsx index f3d09ecc0a..a30d26fd97 100644 --- a/public/app/features/admin/migrate-to-cloud/MigrateToCloud.tsx +++ b/public/app/features/admin/migrate-to-cloud/MigrateToCloud.tsx @@ -1,19 +1,13 @@ import React from 'react'; -import { Stack, Text } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; -import { useGetStatusQuery } from './api'; +import { EmptyState } from './EmptyState/EmptyState'; export default function MigrateToCloud() { - const { data } = useGetStatusQuery(); - return ( - - TODO -
{JSON.stringify(data)}
-
+
); } diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 2edc000e59..1e66599fe4 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -691,6 +691,42 @@ "new-to-question": "New to Grafana?" } }, + "migrate-to-cloud": { + "can-i-move": { + "body": "Once you connect this installation to a cloud stack, you'll be able to upload data sources and dashboards.", + "link-title": "Learn about migrating other settings", + "title": "Can I move this installation to Grafana Cloud?" + }, + "cta": { + "button": "Migrate this instance to Cloud", + "header": "Let us manage your Grafana stack" + }, + "is-it-secure": { + "body": "Grafana Labs is committed to maintaining the highest standards of data privacy and security. By implementing industry-standard security technologies and procedures, we help protect our customers' data from unauthorized access, use, or disclosure.", + "link-title": "Grafana Labs Trust Center", + "title": "Is it secure?" + }, + "pdc": { + "body": "Exposing your data sources to the internet can raise security concerns. Private data source connect (PDC) allows Grafana Cloud to access your existing data sources over a secure network tunnel.", + "link-title": "Learn about PDC", + "title": "Not all my data sources are on the public internet" + }, + "pricing": { + "body": "Grafana Cloud has a generous free plan and a 14 day unlimited usage trial. After your trial expires, you'll be billed based on usage over the free plan limits.", + "link-title": "Grafana Cloud pricing", + "title": "How much does it cost?" + }, + "what-is-cloud": { + "body": "Grafana cloud is a fully managed cloud-hosted observability platform ideal for cloud native environments. It's everything you love about Grafana without the overhead of maintaining, upgrading, and supporting an installation.", + "link-title": "Learn about cloud features", + "title": "What is Grafana Cloud?" + }, + "why-host": { + "body": "In addition to the convenience of managed hosting, Grafana Cloud includes many cloud-exclusive features like SLOs, incident management, machine learning, and powerful observability integrations.", + "link-title": "More questions? Talk to an expert", + "title": "Why host with Grafana?" + } + }, "nav": { "add-new-connections": { "title": "Add new connection" @@ -877,7 +913,7 @@ "subtitle": "Manage folder dashboards and permissions" }, "migrate-to-cloud": { - "subtitle": "Copy data sources, dashboards, and alerts from this installation to a cloud stack", + "subtitle": "Copy configuration from your self-managed installation to a cloud stack", "title": "Migrate to Grafana Cloud" }, "monitoring": { diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index 9d894e7f4d..6cd2c16ee4 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -691,6 +691,42 @@ "new-to-question": "Ńęŵ ŧő Ğřäƒäʼnä?" } }, + "migrate-to-cloud": { + "can-i-move": { + "body": "Øʼnčę yőū čőʼnʼnęčŧ ŧĥįş įʼnşŧäľľäŧįőʼn ŧő ä čľőūđ şŧäčĸ, yőū'ľľ þę äþľę ŧő ūpľőäđ đäŧä şőūřčęş äʼnđ đäşĥþőäřđş.", + "link-title": "Ŀęäřʼn äþőūŧ mįģřäŧįʼnģ őŧĥęř şęŧŧįʼnģş", + "title": "Cäʼn Ĩ mővę ŧĥįş įʼnşŧäľľäŧįőʼn ŧő Ğřäƒäʼnä Cľőūđ?" + }, + "cta": { + "button": "Mįģřäŧę ŧĥįş įʼnşŧäʼnčę ŧő Cľőūđ", + "header": "Ŀęŧ ūş mäʼnäģę yőūř Ğřäƒäʼnä şŧäčĸ" + }, + "is-it-secure": { + "body": "Ğřäƒäʼnä Ŀäþş įş čőmmįŧŧęđ ŧő mäįʼnŧäįʼnįʼnģ ŧĥę ĥįģĥęşŧ şŧäʼnđäřđş őƒ đäŧä přįväčy äʼnđ şęčūřįŧy. ßy įmpľęmęʼnŧįʼnģ įʼnđūşŧřy-şŧäʼnđäřđ şęčūřįŧy ŧęčĥʼnőľőģįęş äʼnđ přőčęđūřęş, ŵę ĥęľp přőŧęčŧ őūř čūşŧőmęřş' đäŧä ƒřőm ūʼnäūŧĥőřįžęđ äččęşş, ūşę, őř đįşčľőşūřę.", + "link-title": "Ğřäƒäʼnä Ŀäþş Ŧřūşŧ Cęʼnŧęř", + "title": "Ĩş įŧ şęčūřę?" + }, + "pdc": { + "body": "Ēχpőşįʼnģ yőūř đäŧä şőūřčęş ŧő ŧĥę įʼnŧęřʼnęŧ čäʼn řäįşę şęčūřįŧy čőʼnčęřʼnş. Přįväŧę đäŧä şőūřčę čőʼnʼnęčŧ (PĐC) äľľőŵş Ğřäƒäʼnä Cľőūđ ŧő äččęşş yőūř ęχįşŧįʼnģ đäŧä şőūřčęş ővęř ä şęčūřę ʼnęŧŵőřĸ ŧūʼnʼnęľ.", + "link-title": "Ŀęäřʼn äþőūŧ PĐC", + "title": "Ńőŧ äľľ my đäŧä şőūřčęş äřę őʼn ŧĥę pūþľįč įʼnŧęřʼnęŧ" + }, + "pricing": { + "body": "Ğřäƒäʼnä Cľőūđ ĥäş ä ģęʼnęřőūş ƒřęę pľäʼn äʼnđ ä 14 đäy ūʼnľįmįŧęđ ūşäģę ŧřįäľ. Ńŧęř yőūř ŧřįäľ ęχpįřęş, yőū'ľľ þę þįľľęđ þäşęđ őʼn ūşäģę ővęř ŧĥę ƒřęę pľäʼn ľįmįŧş.", + "link-title": "Ğřäƒäʼnä Cľőūđ přįčįʼnģ", + "title": "Ħőŵ mūčĥ đőęş įŧ čőşŧ?" + }, + "what-is-cloud": { + "body": "Ğřäƒäʼnä čľőūđ įş ä ƒūľľy mäʼnäģęđ čľőūđ-ĥőşŧęđ őþşęřväþįľįŧy pľäŧƒőřm įđęäľ ƒőř čľőūđ ʼnäŧįvę ęʼnvįřőʼnmęʼnŧş. Ĩŧ'ş ęvęřyŧĥįʼnģ yőū ľővę äþőūŧ Ğřäƒäʼnä ŵįŧĥőūŧ ŧĥę ővęřĥęäđ őƒ mäįʼnŧäįʼnįʼnģ, ūpģřäđįʼnģ, äʼnđ şūppőřŧįʼnģ äʼn įʼnşŧäľľäŧįőʼn.", + "link-title": "Ŀęäřʼn äþőūŧ čľőūđ ƒęäŧūřęş", + "title": "Ŵĥäŧ įş Ğřäƒäʼnä Cľőūđ?" + }, + "why-host": { + "body": "Ĩʼn äđđįŧįőʼn ŧő ŧĥę čőʼnvęʼnįęʼnčę őƒ mäʼnäģęđ ĥőşŧįʼnģ, Ğřäƒäʼnä Cľőūđ įʼnčľūđęş mäʼny čľőūđ-ęχčľūşįvę ƒęäŧūřęş ľįĸę ŜĿØş, įʼnčįđęʼnŧ mäʼnäģęmęʼnŧ, mäčĥįʼnę ľęäřʼnįʼnģ, äʼnđ pőŵęřƒūľ őþşęřväþįľįŧy įʼnŧęģřäŧįőʼnş.", + "link-title": "Mőřę qūęşŧįőʼnş? Ŧäľĸ ŧő äʼn ęχpęřŧ", + "title": "Ŵĥy ĥőşŧ ŵįŧĥ Ğřäƒäʼnä?" + } + }, "nav": { "add-new-connections": { "title": "Åđđ ʼnęŵ čőʼnʼnęčŧįőʼn" @@ -877,7 +913,7 @@ "subtitle": "Mäʼnäģę ƒőľđęř đäşĥþőäřđş äʼnđ pęřmįşşįőʼnş" }, "migrate-to-cloud": { - "subtitle": "Cőpy đäŧä şőūřčęş, đäşĥþőäřđş, äʼnđ äľęřŧş ƒřőm ŧĥįş įʼnşŧäľľäŧįőʼn ŧő ä čľőūđ şŧäčĸ", + "subtitle": "Cőpy čőʼnƒįģūřäŧįőʼn ƒřőm yőūř şęľƒ-mäʼnäģęđ įʼnşŧäľľäŧįőʼn ŧő ä čľőūđ şŧäčĸ", "title": "Mįģřäŧę ŧő Ğřäƒäʼnä Cľőūđ" }, "monitoring": { From a0353b237af45c8242457a9ba97ee607076877a5 Mon Sep 17 00:00:00 2001 From: George Robinson Date: Fri, 23 Feb 2024 11:25:43 +0000 Subject: [PATCH 0122/1406] Alerting: Update swagger specs (#83260) --- pkg/services/ngalert/api/tooling/api.json | 556 ++++++++++++++++-- .../api/tooling/definitions/cortex-ruler.go | 2 +- pkg/services/ngalert/api/tooling/post.json | 201 +++++-- pkg/services/ngalert/api/tooling/spec.json | 201 +++++-- public/api-merged.json | 191 ++++-- public/openapi3.json | 191 ++++-- 6 files changed, 1077 insertions(+), 265 deletions(-) diff --git a/pkg/services/ngalert/api/tooling/api.json b/pkg/services/ngalert/api/tooling/api.json index 1ea9621515..4024e91414 100644 --- a/pkg/services/ngalert/api/tooling/api.json +++ b/pkg/services/ngalert/api/tooling/api.json @@ -225,6 +225,9 @@ ], "type": "string" }, + "notification_settings": { + "$ref": "#/definitions/AlertRuleNotificationSettingsExport" + }, "panelId": { "format": "int64", "type": "integer" @@ -294,6 +297,90 @@ }, "type": "object" }, + "AlertRuleNotificationSettings": { + "properties": { + "group_by": { + "default": [ + "alertname", + "grafana_folder" + ], + "description": "Override the labels by which incoming alerts are grouped together. For example, multiple alerts coming in for\ncluster=A and alertname=LatencyHigh would be batched into a single group. To aggregate by all possible labels\nuse the special value '...' as the sole label name.\nThis effectively disables aggregation entirely, passing through all alerts as-is. This is unlikely to be what\nyou want, unless you have a very low alert volume or your upstream notification system performs its own grouping.\nMust include 'alertname' and 'grafana_folder' if not using '...'.", + "example": [ + "alertname", + "grafana_folder", + "cluster" + ], + "items": { + "type": "string" + }, + "type": "array" + }, + "group_interval": { + "description": "Override how long to wait before sending a notification about new alerts that are added to a group of alerts for\nwhich an initial notification has already been sent. (Usually ~5m or more.)", + "example": "5m", + "type": "string" + }, + "group_wait": { + "description": "Override how long to initially wait to send a notification for a group of alerts. Allows to wait for an\ninhibiting alert to arrive or collect more initial alerts for the same group. (Usually ~0s to few minutes.)", + "example": "30s", + "type": "string" + }, + "mute_time_intervals": { + "description": "Override the times when notifications should be muted. These must match the name of a mute time interval defined\nin the alertmanager configuration mute_time_intervals section. When muted it will not send any notifications, but\notherwise acts normally.", + "example": [ + "maintenance" + ], + "items": { + "type": "string" + }, + "type": "array" + }, + "receiver": { + "description": "Name of the receiver to send notifications to.", + "example": "grafana-default-email", + "type": "string" + }, + "repeat_interval": { + "description": "Override how long to wait before sending a notification again if it has already been sent successfully for an\nalert. (Usually ~3h or more).\nNote that this parameter is implicitly bound by Alertmanager's `--data.retention` configuration flag.\nNotifications will be resent after either repeat_interval or the data retention period have passed, whichever\noccurs first. `repeat_interval` should not be less than `group_interval`.", + "example": "4h", + "type": "string" + } + }, + "required": [ + "receiver" + ], + "type": "object" + }, + "AlertRuleNotificationSettingsExport": { + "properties": { + "group_by": { + "items": { + "type": "string" + }, + "type": "array" + }, + "group_interval": { + "type": "string" + }, + "group_wait": { + "type": "string" + }, + "mute_time_intervals": { + "items": { + "type": "string" + }, + "type": "array" + }, + "receiver": { + "type": "string" + }, + "repeat_interval": { + "type": "string" + } + }, + "title": "AlertRuleNotificationSettingsExport is the provisioned export of models.NotificationSettings.", + "type": "object" + }, "AlertRuleUpgrade": { "properties": { "sendsTo": { @@ -311,6 +398,9 @@ }, "type": "object" }, + "AlertStateType": { + "type": "string" + }, "AlertingFileExport": { "properties": { "apiVersion": { @@ -438,6 +528,80 @@ }, "type": "object" }, + "Annotation": { + "properties": { + "alertId": { + "format": "int64", + "type": "integer" + }, + "alertName": { + "type": "string" + }, + "avatarUrl": { + "type": "string" + }, + "created": { + "format": "int64", + "type": "integer" + }, + "dashboardId": { + "format": "int64", + "type": "integer" + }, + "dashboardUID": { + "type": "string" + }, + "data": { + "$ref": "#/definitions/Json" + }, + "email": { + "type": "string" + }, + "id": { + "format": "int64", + "type": "integer" + }, + "login": { + "type": "string" + }, + "newState": { + "type": "string" + }, + "panelId": { + "format": "int64", + "type": "integer" + }, + "prevState": { + "type": "string" + }, + "tags": { + "items": { + "type": "string" + }, + "type": "array" + }, + "text": { + "type": "string" + }, + "time": { + "format": "int64", + "type": "integer" + }, + "timeEnd": { + "format": "int64", + "type": "integer" + }, + "updated": { + "format": "int64", + "type": "integer" + }, + "userId": { + "format": "int64", + "type": "integer" + } + }, + "type": "object" + }, "ApiRuleNode": { "properties": { "alert": { @@ -574,6 +738,7 @@ "type": "array" }, "mute_time_intervals": { + "description": "MuteTimeIntervals is deprecated and will be removed before Alertmanager 1.0.", "items": { "$ref": "#/definitions/MuteTimeInterval" }, @@ -587,6 +752,12 @@ "type": "string" }, "type": "array" + }, + "time_intervals": { + "items": { + "$ref": "#/definitions/TimeInterval" + }, + "type": "array" } }, "title": "Config is the top-level configuration for Alertmanager's config files.", @@ -645,12 +816,75 @@ }, "type": "array" }, + "CookieType": { + "type": "string" + }, "CounterResetHint": { "description": "or alternatively that we are dealing with a gauge histogram, where counter resets do not apply.", "format": "uint8", "title": "CounterResetHint contains the known information about a counter reset,", "type": "integer" }, + "CreateLibraryElementCommand": { + "description": "CreateLibraryElementCommand is the command for adding a LibraryElement", + "properties": { + "folderId": { + "description": "ID of the folder where the library element is stored.\n\nDeprecated: use FolderUID instead", + "format": "int64", + "type": "integer" + }, + "folderUid": { + "description": "UID of the folder where the library element is stored.", + "type": "string" + }, + "kind": { + "description": "Kind of element to create, Use 1 for library panels or 2 for c.\nDescription:\n1 - library panels\n2 - library variables", + "enum": [ + 1, + 2 + ], + "format": "int64", + "type": "integer" + }, + "model": { + "description": "The JSON model for the library element.", + "type": "object" + }, + "name": { + "description": "Name of the library element.", + "type": "string" + }, + "uid": { + "type": "string" + } + }, + "type": "object" + }, + "DashboardACLUpdateItem": { + "properties": { + "permission": { + "$ref": "#/definitions/PermissionType" + }, + "role": { + "enum": [ + "None", + "Viewer", + "Editor", + "Admin" + ], + "type": "string" + }, + "teamId": { + "format": "int64", + "type": "integer" + }, + "userId": { + "format": "int64", + "type": "integer" + } + }, + "type": "object" + }, "DashboardUpgrade": { "properties": { "dashboardId": { @@ -752,6 +986,9 @@ }, "webhook_url": { "$ref": "#/definitions/SecretURL" + }, + "webhook_url_file": { + "type": "string" } }, "title": "DiscordConfig configures notifications via Discord.", @@ -1327,6 +1564,7 @@ "type": "object" }, "mute_time_intervals": { + "description": "MuteTimeIntervals is deprecated and will be removed before Alertmanager 1.0.", "items": { "$ref": "#/definitions/MuteTimeInterval" }, @@ -1347,6 +1585,12 @@ "type": "string" }, "type": "array" + }, + "time_intervals": { + "items": { + "$ref": "#/definitions/TimeInterval" + }, + "type": "array" } }, "type": "object" @@ -1560,6 +1804,9 @@ ], "type": "string" }, + "notification_settings": { + "$ref": "#/definitions/AlertRuleNotificationSettings" + }, "orgId": { "format": "int64", "type": "integer" @@ -2028,6 +2275,9 @@ "send_resolved": { "type": "boolean" }, + "summary": { + "type": "string" + }, "text": { "type": "string" }, @@ -2036,6 +2286,9 @@ }, "webhook_url": { "$ref": "#/definitions/SecretURL" + }, + "webhook_url_file": { + "type": "string" } }, "type": "object" @@ -2074,6 +2327,48 @@ }, "type": "array" }, + "MetricRequest": { + "properties": { + "debug": { + "type": "boolean" + }, + "from": { + "description": "From Start time in epoch timestamps in milliseconds or relative using Grafana time units.", + "example": "now-1h", + "type": "string" + }, + "queries": { + "description": "queries.refId – Specifies an identifier of the query. Is optional and default to “A”.\nqueries.datasourceId – Specifies the data source to be queried. Each query in the request must have an unique datasourceId.\nqueries.maxDataPoints - Species maximum amount of data points that dashboard panel can render. Is optional and default to 100.\nqueries.intervalMs - Specifies the time interval in milliseconds of time series. Is optional and defaults to 1000.", + "example": [ + { + "datasource": { + "uid": "PD8C576611E62080A" + }, + "format": "table", + "intervalMs": 86400000, + "maxDataPoints": 1092, + "rawSql": "SELECT 1 as valueOne, 2 as valueTwo", + "refId": "A" + } + ], + "items": { + "$ref": "#/definitions/Json" + }, + "type": "array" + }, + "to": { + "description": "To End time in epoch timestamps in milliseconds or relative using Grafana time units.", + "example": "now", + "type": "string" + } + }, + "required": [ + "from", + "to", + "queries" + ], + "type": "object" + }, "MultiStatus": { "type": "object" }, @@ -2125,6 +2420,24 @@ }, "type": "object" }, + "NewApiKeyResult": { + "properties": { + "id": { + "example": 1, + "format": "int64", + "type": "integer" + }, + "key": { + "example": "glsa_yscW25imSKJIuav8zF37RZmnbiDvB05G_fcaaf58a", + "type": "string" + }, + "name": { + "example": "grafana", + "type": "string" + } + }, + "type": "object" + }, "NotFound": { "type": "object" }, @@ -2528,27 +2841,76 @@ }, "type": "object" }, - "PermissionDenied": { - "type": "object" - }, - "Point": { - "description": "If H is not nil, then this is a histogram point and only (T, H) is valid.\nIf H is nil, then only (T, V) is valid.", + "PatchPrefsCmd": { "properties": { - "H": { - "$ref": "#/definitions/FloatHistogram" + "cookies": { + "items": { + "$ref": "#/definitions/CookieType" + }, + "type": "array" }, - "T": { + "homeDashboardId": { + "default": 0, + "description": "The numerical :id of a favorited dashboard", "format": "int64", "type": "integer" }, - "V": { - "format": "double", - "type": "number" + "homeDashboardUID": { + "type": "string" + }, + "language": { + "type": "string" + }, + "queryHistory": { + "$ref": "#/definitions/QueryHistoryPreference" + }, + "theme": { + "enum": [ + "light", + "dark" + ], + "type": "string" + }, + "timezone": { + "enum": [ + "utc", + "browser" + ], + "type": "string" + }, + "weekStart": { + "type": "string" } }, - "title": "Point represents a single data point for a given timestamp.", "type": "object" }, + "Permission": { + "properties": { + "action": { + "type": "string" + }, + "created": { + "format": "date-time", + "type": "string" + }, + "scope": { + "type": "string" + }, + "updated": { + "format": "date-time", + "type": "string" + } + }, + "title": "Permission is the model for access control permissions.", + "type": "object" + }, + "PermissionDenied": { + "type": "object" + }, + "PermissionType": { + "format": "int64", + "type": "integer" + }, "PostableApiAlertingConfig": { "properties": { "global": { @@ -2561,6 +2923,7 @@ "type": "array" }, "mute_time_intervals": { + "description": "MuteTimeIntervals is deprecated and will be removed before Alertmanager 1.0.", "items": { "$ref": "#/definitions/MuteTimeInterval" }, @@ -2581,6 +2944,12 @@ "type": "string" }, "type": "array" + }, + "time_intervals": { + "items": { + "$ref": "#/definitions/TimeInterval" + }, + "type": "array" } }, "type": "object" @@ -2803,6 +3172,9 @@ ], "type": "string" }, + "notification_settings": { + "$ref": "#/definitions/AlertRuleNotificationSettings" + }, "title": { "type": "string" }, @@ -2979,6 +3351,9 @@ ], "type": "string" }, + "notification_settings": { + "$ref": "#/definitions/AlertRuleNotificationSettings" + }, "orgID": { "format": "int64", "type": "integer" @@ -3124,6 +3499,14 @@ }, "type": "object" }, + "QueryHistoryPreference": { + "properties": { + "homeTab": { + "type": "string" + } + }, + "type": "object" + }, "QueryStat": { "description": "The embedded FieldConfig's display name must be set.\nIt corresponds to the QueryResultMetaStat on the frontend (https://github.com/grafana/grafana/blob/master/packages/grafana-data/src/types/data.ts#L53).", "properties": { @@ -3358,6 +3741,53 @@ "title": "Responses is a map of RefIDs (Unique Query ID) to DataResponses.", "type": "object" }, + "RoleDTO": { + "properties": { + "created": { + "format": "date-time", + "type": "string" + }, + "delegatable": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "displayName": { + "type": "string" + }, + "global": { + "type": "boolean" + }, + "group": { + "type": "string" + }, + "hidden": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "permissions": { + "items": { + "$ref": "#/definitions/Permission" + }, + "type": "array" + }, + "uid": { + "type": "string" + }, + "updated": { + "format": "date-time", + "type": "string" + }, + "version": { + "format": "int64", + "type": "integer" + } + }, + "type": "object" + }, "Route": { "description": "A Route is a node that contains definitions of how to handle alerts. This is modified\nfrom the upstream alertmanager in that it adds the ObjectMatchers property.", "properties": { @@ -3659,7 +4089,12 @@ "type": "object" }, "Sample": { + "description": "Sample is a single sample belonging to a metric. It represents either a float\nsample or a histogram sample. If H is nil, it is a float sample. Otherwise,\nit is a histogram sample.", "properties": { + "F": { + "format": "double", + "type": "number" + }, "H": { "$ref": "#/definitions/FloatHistogram" }, @@ -3669,13 +4104,8 @@ "T": { "format": "int64", "type": "integer" - }, - "V": { - "format": "double", - "type": "number" } }, - "title": "Sample is a single sample belonging to a metric.", "type": "object" }, "Secret": { @@ -4168,42 +4598,18 @@ "type": "string" }, "TimeInterval": { - "description": "TimeInterval describes intervals of time. ContainsTime will tell you if a golang time is contained\nwithin the interval.", "properties": { - "days_of_month": { - "items": { - "type": "string" - }, - "type": "array" - }, - "location": { + "name": { "type": "string" }, - "months": { - "items": { - "type": "string" - }, - "type": "array" - }, - "times": { - "items": { - "$ref": "#/definitions/TimeRange" - }, - "type": "array" - }, - "weekdays": { - "items": { - "type": "string" - }, - "type": "array" - }, - "years": { + "time_intervals": { "items": { - "type": "string" + "$ref": "#/definitions/TimeInterval" }, "type": "array" } }, + "title": "TimeInterval represents a named set of time intervals for which a route should be muted.", "type": "object" }, "TimeIntervalItem": { @@ -4308,6 +4714,61 @@ "title": "URL is a custom URL type that allows validation at configuration load time.", "type": "object" }, + "UpdateDashboardACLCommand": { + "properties": { + "items": { + "items": { + "$ref": "#/definitions/DashboardACLUpdateItem" + }, + "type": "array" + } + }, + "type": "object" + }, + "UpdatePrefsCmd": { + "properties": { + "cookies": { + "items": { + "$ref": "#/definitions/CookieType" + }, + "type": "array" + }, + "homeDashboardId": { + "default": 0, + "description": "The numerical :id of a favorited dashboard", + "format": "int64", + "type": "integer" + }, + "homeDashboardUID": { + "type": "string" + }, + "language": { + "type": "string" + }, + "queryHistory": { + "$ref": "#/definitions/QueryHistoryPreference" + }, + "theme": { + "enum": [ + "light", + "dark", + "system" + ], + "type": "string" + }, + "timezone": { + "enum": [ + "utc", + "browser" + ], + "type": "string" + }, + "weekStart": { + "type": "string" + } + }, + "type": "object" + }, "UpdateRuleGroupResponse": { "properties": { "created": { @@ -4358,7 +4819,7 @@ "type": "array" }, "Vector": { - "description": "Vector is basically only an alias for model.Samples, but the\ncontract is that in a Vector, all Samples have the same timestamp.", + "description": "Vector is basically only an alias for []Sample, but the contract is that\nin a Vector, all Samples have the same timestamp.", "items": { "$ref": "#/definitions/Sample" }, @@ -4748,14 +5209,12 @@ "type": "object" }, "gettableSilences": { - "description": "GettableSilences gettable silences", "items": { "$ref": "#/definitions/gettableSilence" }, "type": "array" }, "integration": { - "description": "Integration integration", "properties": { "lastNotifyAttempt": { "description": "A timestamp indicating the last attempt to deliver a notification regardless of the outcome.\nFormat: date-time", @@ -4936,6 +5395,7 @@ "type": "object" }, "receiver": { + "description": "Receiver receiver", "properties": { "active": { "description": "active", diff --git a/pkg/services/ngalert/api/tooling/definitions/cortex-ruler.go b/pkg/services/ngalert/api/tooling/definitions/cortex-ruler.go index 21e091d9cd..6227be78e4 100644 --- a/pkg/services/ngalert/api/tooling/definitions/cortex-ruler.go +++ b/pkg/services/ngalert/api/tooling/definitions/cortex-ruler.go @@ -406,7 +406,7 @@ const ( ErrorErrState ExecutionErrorState = "Error" ) -// swagger: model +// swagger:model type AlertRuleNotificationSettings struct { // Name of the receiver to send notifications to. // required: true diff --git a/pkg/services/ngalert/api/tooling/post.json b/pkg/services/ngalert/api/tooling/post.json index 0f28aa4f8b..115965f0d9 100644 --- a/pkg/services/ngalert/api/tooling/post.json +++ b/pkg/services/ngalert/api/tooling/post.json @@ -225,6 +225,9 @@ ], "type": "string" }, + "notification_settings": { + "$ref": "#/definitions/AlertRuleNotificationSettingsExport" + }, "panelId": { "format": "int64", "type": "integer" @@ -294,6 +297,90 @@ }, "type": "object" }, + "AlertRuleNotificationSettings": { + "properties": { + "group_by": { + "default": [ + "alertname", + "grafana_folder" + ], + "description": "Override the labels by which incoming alerts are grouped together. For example, multiple alerts coming in for\ncluster=A and alertname=LatencyHigh would be batched into a single group. To aggregate by all possible labels\nuse the special value '...' as the sole label name.\nThis effectively disables aggregation entirely, passing through all alerts as-is. This is unlikely to be what\nyou want, unless you have a very low alert volume or your upstream notification system performs its own grouping.\nMust include 'alertname' and 'grafana_folder' if not using '...'.", + "example": [ + "alertname", + "grafana_folder", + "cluster" + ], + "items": { + "type": "string" + }, + "type": "array" + }, + "group_interval": { + "description": "Override how long to wait before sending a notification about new alerts that are added to a group of alerts for\nwhich an initial notification has already been sent. (Usually ~5m or more.)", + "example": "5m", + "type": "string" + }, + "group_wait": { + "description": "Override how long to initially wait to send a notification for a group of alerts. Allows to wait for an\ninhibiting alert to arrive or collect more initial alerts for the same group. (Usually ~0s to few minutes.)", + "example": "30s", + "type": "string" + }, + "mute_time_intervals": { + "description": "Override the times when notifications should be muted. These must match the name of a mute time interval defined\nin the alertmanager configuration mute_time_intervals section. When muted it will not send any notifications, but\notherwise acts normally.", + "example": [ + "maintenance" + ], + "items": { + "type": "string" + }, + "type": "array" + }, + "receiver": { + "description": "Name of the receiver to send notifications to.", + "example": "grafana-default-email", + "type": "string" + }, + "repeat_interval": { + "description": "Override how long to wait before sending a notification again if it has already been sent successfully for an\nalert. (Usually ~3h or more).\nNote that this parameter is implicitly bound by Alertmanager's `--data.retention` configuration flag.\nNotifications will be resent after either repeat_interval or the data retention period have passed, whichever\noccurs first. `repeat_interval` should not be less than `group_interval`.", + "example": "4h", + "type": "string" + } + }, + "required": [ + "receiver" + ], + "type": "object" + }, + "AlertRuleNotificationSettingsExport": { + "properties": { + "group_by": { + "items": { + "type": "string" + }, + "type": "array" + }, + "group_interval": { + "type": "string" + }, + "group_wait": { + "type": "string" + }, + "mute_time_intervals": { + "items": { + "type": "string" + }, + "type": "array" + }, + "receiver": { + "type": "string" + }, + "repeat_interval": { + "type": "string" + } + }, + "title": "AlertRuleNotificationSettingsExport is the provisioned export of models.NotificationSettings.", + "type": "object" + }, "AlertRuleUpgrade": { "properties": { "sendsTo": { @@ -574,6 +661,7 @@ "type": "array" }, "mute_time_intervals": { + "description": "MuteTimeIntervals is deprecated and will be removed before Alertmanager 1.0.", "items": { "$ref": "#/definitions/MuteTimeInterval" }, @@ -587,6 +675,12 @@ "type": "string" }, "type": "array" + }, + "time_intervals": { + "items": { + "$ref": "#/definitions/TimeInterval" + }, + "type": "array" } }, "title": "Config is the top-level configuration for Alertmanager's config files.", @@ -752,6 +846,9 @@ }, "webhook_url": { "$ref": "#/definitions/SecretURL" + }, + "webhook_url_file": { + "type": "string" } }, "title": "DiscordConfig configures notifications via Discord.", @@ -1327,6 +1424,7 @@ "type": "object" }, "mute_time_intervals": { + "description": "MuteTimeIntervals is deprecated and will be removed before Alertmanager 1.0.", "items": { "$ref": "#/definitions/MuteTimeInterval" }, @@ -1347,6 +1445,12 @@ "type": "string" }, "type": "array" + }, + "time_intervals": { + "items": { + "$ref": "#/definitions/TimeInterval" + }, + "type": "array" } }, "type": "object" @@ -1560,6 +1664,9 @@ ], "type": "string" }, + "notification_settings": { + "$ref": "#/definitions/AlertRuleNotificationSettings" + }, "orgId": { "format": "int64", "type": "integer" @@ -2028,6 +2135,9 @@ "send_resolved": { "type": "boolean" }, + "summary": { + "type": "string" + }, "text": { "type": "string" }, @@ -2036,6 +2146,9 @@ }, "webhook_url": { "$ref": "#/definitions/SecretURL" + }, + "webhook_url_file": { + "type": "string" } }, "type": "object" @@ -2531,24 +2644,6 @@ "PermissionDenied": { "type": "object" }, - "Point": { - "description": "If H is not nil, then this is a histogram point and only (T, H) is valid.\nIf H is nil, then only (T, V) is valid.", - "properties": { - "H": { - "$ref": "#/definitions/FloatHistogram" - }, - "T": { - "format": "int64", - "type": "integer" - }, - "V": { - "format": "double", - "type": "number" - } - }, - "title": "Point represents a single data point for a given timestamp.", - "type": "object" - }, "PostableApiAlertingConfig": { "properties": { "global": { @@ -2561,6 +2656,7 @@ "type": "array" }, "mute_time_intervals": { + "description": "MuteTimeIntervals is deprecated and will be removed before Alertmanager 1.0.", "items": { "$ref": "#/definitions/MuteTimeInterval" }, @@ -2581,6 +2677,12 @@ "type": "string" }, "type": "array" + }, + "time_intervals": { + "items": { + "$ref": "#/definitions/TimeInterval" + }, + "type": "array" } }, "type": "object" @@ -2803,6 +2905,9 @@ ], "type": "string" }, + "notification_settings": { + "$ref": "#/definitions/AlertRuleNotificationSettings" + }, "title": { "type": "string" }, @@ -2979,6 +3084,9 @@ ], "type": "string" }, + "notification_settings": { + "$ref": "#/definitions/AlertRuleNotificationSettings" + }, "orgID": { "format": "int64", "type": "integer" @@ -3659,7 +3767,12 @@ "type": "object" }, "Sample": { + "description": "Sample is a single sample belonging to a metric. It represents either a float\nsample or a histogram sample. If H is nil, it is a float sample. Otherwise,\nit is a histogram sample.", "properties": { + "F": { + "format": "double", + "type": "number" + }, "H": { "$ref": "#/definitions/FloatHistogram" }, @@ -3669,13 +3782,8 @@ "T": { "format": "int64", "type": "integer" - }, - "V": { - "format": "double", - "type": "number" } }, - "title": "Sample is a single sample belonging to a metric.", "type": "object" }, "Secret": { @@ -4168,42 +4276,18 @@ "type": "string" }, "TimeInterval": { - "description": "TimeInterval describes intervals of time. ContainsTime will tell you if a golang time is contained\nwithin the interval.", "properties": { - "days_of_month": { - "items": { - "type": "string" - }, - "type": "array" - }, - "location": { + "name": { "type": "string" }, - "months": { - "items": { - "type": "string" - }, - "type": "array" - }, - "times": { - "items": { - "$ref": "#/definitions/TimeRange" - }, - "type": "array" - }, - "weekdays": { - "items": { - "type": "string" - }, - "type": "array" - }, - "years": { + "time_intervals": { "items": { - "type": "string" + "$ref": "#/definitions/TimeInterval" }, "type": "array" } }, + "title": "TimeInterval represents a named set of time intervals for which a route should be muted.", "type": "object" }, "TimeIntervalItem": { @@ -4270,7 +4354,6 @@ "type": "object" }, "URL": { - "description": "The general form represented is:\n\n[scheme:][//[userinfo@]host][/]path[?query][#fragment]\n\nURLs that do not start with a slash after the scheme are interpreted as:\n\nscheme:opaque[?query][#fragment]\n\nNote that the Path field is stored in decoded form: /%47%6f%2f becomes /Go/.\nA consequence is that it is impossible to tell which slashes in the Path were\nslashes in the raw URL and which were %2f. This distinction is rarely important,\nbut when it is, the code should use the EscapedPath method, which preserves\nthe original encoding of Path.\n\nThe RawPath field is an optional field which is only set when the default\nencoding of Path is different from the escaped path. See the EscapedPath method\nfor more details.\n\nURL's String method uses the EscapedPath method to obtain the path.", "properties": { "ForceQuery": { "type": "boolean" @@ -4306,7 +4389,7 @@ "$ref": "#/definitions/Userinfo" } }, - "title": "A URL represents a parsed URL (technically, a URI reference).", + "title": "URL is a custom URL type that allows validation at configuration load time.", "type": "object" }, "UpdateRuleGroupResponse": { @@ -4359,7 +4442,7 @@ "type": "array" }, "Vector": { - "description": "Vector is basically only an alias for model.Samples, but the\ncontract is that in a Vector, all Samples have the same timestamp.", + "description": "Vector is basically only an alias for []Sample, but the contract is that\nin a Vector, all Samples have the same timestamp.", "items": { "$ref": "#/definitions/Sample" }, @@ -4512,6 +4595,7 @@ "type": "object" }, "alertGroup": { + "description": "AlertGroup alert group", "properties": { "alerts": { "description": "alerts", @@ -4696,6 +4780,7 @@ "type": "object" }, "gettableAlerts": { + "description": "GettableAlerts gettable alerts", "items": { "$ref": "#/definitions/gettableAlert" }, @@ -4750,6 +4835,7 @@ "type": "object" }, "gettableSilences": { + "description": "GettableSilences gettable silences", "items": { "$ref": "#/definitions/gettableSilence" }, @@ -4900,6 +4986,7 @@ "type": "array" }, "postableSilence": { + "description": "PostableSilence postable silence", "properties": { "comment": { "description": "comment", @@ -7113,6 +7200,12 @@ "200": { "$ref": "#/responses/GetReceiverResponse" }, + "403": { + "description": "PermissionDenied", + "schema": { + "$ref": "#/definitions/PermissionDenied" + } + }, "404": { "description": "NotFound", "schema": { diff --git a/pkg/services/ngalert/api/tooling/spec.json b/pkg/services/ngalert/api/tooling/spec.json index 9b208a3ea1..3177242b6b 100644 --- a/pkg/services/ngalert/api/tooling/spec.json +++ b/pkg/services/ngalert/api/tooling/spec.json @@ -2078,6 +2078,12 @@ "200": { "$ref": "#/responses/GetReceiverResponse" }, + "403": { + "description": "PermissionDenied", + "schema": { + "$ref": "#/definitions/PermissionDenied" + } + }, "404": { "description": "NotFound", "schema": { @@ -3783,6 +3789,9 @@ "OK" ] }, + "notification_settings": { + "$ref": "#/definitions/AlertRuleNotificationSettingsExport" + }, "panelId": { "type": "integer", "format": "int64" @@ -3850,6 +3859,90 @@ } } }, + "AlertRuleNotificationSettings": { + "type": "object", + "required": [ + "receiver" + ], + "properties": { + "group_by": { + "description": "Override the labels by which incoming alerts are grouped together. For example, multiple alerts coming in for\ncluster=A and alertname=LatencyHigh would be batched into a single group. To aggregate by all possible labels\nuse the special value '...' as the sole label name.\nThis effectively disables aggregation entirely, passing through all alerts as-is. This is unlikely to be what\nyou want, unless you have a very low alert volume or your upstream notification system performs its own grouping.\nMust include 'alertname' and 'grafana_folder' if not using '...'.", + "type": "array", + "default": [ + "alertname", + "grafana_folder" + ], + "items": { + "type": "string" + }, + "example": [ + "alertname", + "grafana_folder", + "cluster" + ] + }, + "group_interval": { + "description": "Override how long to wait before sending a notification about new alerts that are added to a group of alerts for\nwhich an initial notification has already been sent. (Usually ~5m or more.)", + "type": "string", + "example": "5m" + }, + "group_wait": { + "description": "Override how long to initially wait to send a notification for a group of alerts. Allows to wait for an\ninhibiting alert to arrive or collect more initial alerts for the same group. (Usually ~0s to few minutes.)", + "type": "string", + "example": "30s" + }, + "mute_time_intervals": { + "description": "Override the times when notifications should be muted. These must match the name of a mute time interval defined\nin the alertmanager configuration mute_time_intervals section. When muted it will not send any notifications, but\notherwise acts normally.", + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "maintenance" + ] + }, + "receiver": { + "description": "Name of the receiver to send notifications to.", + "type": "string", + "example": "grafana-default-email" + }, + "repeat_interval": { + "description": "Override how long to wait before sending a notification again if it has already been sent successfully for an\nalert. (Usually ~3h or more).\nNote that this parameter is implicitly bound by Alertmanager's `--data.retention` configuration flag.\nNotifications will be resent after either repeat_interval or the data retention period have passed, whichever\noccurs first. `repeat_interval` should not be less than `group_interval`.", + "type": "string", + "example": "4h" + } + } + }, + "AlertRuleNotificationSettingsExport": { + "type": "object", + "title": "AlertRuleNotificationSettingsExport is the provisioned export of models.NotificationSettings.", + "properties": { + "group_by": { + "type": "array", + "items": { + "type": "string" + } + }, + "group_interval": { + "type": "string" + }, + "group_wait": { + "type": "string" + }, + "mute_time_intervals": { + "type": "array", + "items": { + "type": "string" + } + }, + "receiver": { + "type": "string" + }, + "repeat_interval": { + "type": "string" + } + } + }, "AlertRuleUpgrade": { "type": "object", "properties": { @@ -4132,6 +4225,7 @@ } }, "mute_time_intervals": { + "description": "MuteTimeIntervals is deprecated and will be removed before Alertmanager 1.0.", "type": "array", "items": { "$ref": "#/definitions/MuteTimeInterval" @@ -4145,6 +4239,12 @@ "items": { "type": "string" } + }, + "time_intervals": { + "type": "array", + "items": { + "$ref": "#/definitions/TimeInterval" + } } } }, @@ -4310,6 +4410,9 @@ }, "webhook_url": { "$ref": "#/definitions/SecretURL" + }, + "webhook_url_file": { + "type": "string" } } }, @@ -4887,6 +4990,7 @@ } }, "mute_time_intervals": { + "description": "MuteTimeIntervals is deprecated and will be removed before Alertmanager 1.0.", "type": "array", "items": { "$ref": "#/definitions/MuteTimeInterval" @@ -4907,6 +5011,12 @@ "items": { "type": "string" } + }, + "time_intervals": { + "type": "array", + "items": { + "$ref": "#/definitions/TimeInterval" + } } } }, @@ -5120,6 +5230,9 @@ "OK" ] }, + "notification_settings": { + "$ref": "#/definitions/AlertRuleNotificationSettings" + }, "orgId": { "type": "integer", "format": "int64" @@ -5588,6 +5701,9 @@ "send_resolved": { "type": "boolean" }, + "summary": { + "type": "string" + }, "text": { "type": "string" }, @@ -5596,6 +5712,9 @@ }, "webhook_url": { "$ref": "#/definitions/SecretURL" + }, + "webhook_url_file": { + "type": "string" } } }, @@ -6091,24 +6210,6 @@ "PermissionDenied": { "type": "object" }, - "Point": { - "description": "If H is not nil, then this is a histogram point and only (T, H) is valid.\nIf H is nil, then only (T, V) is valid.", - "type": "object", - "title": "Point represents a single data point for a given timestamp.", - "properties": { - "H": { - "$ref": "#/definitions/FloatHistogram" - }, - "T": { - "type": "integer", - "format": "int64" - }, - "V": { - "type": "number", - "format": "double" - } - } - }, "PostableApiAlertingConfig": { "type": "object", "properties": { @@ -6122,6 +6223,7 @@ } }, "mute_time_intervals": { + "description": "MuteTimeIntervals is deprecated and will be removed before Alertmanager 1.0.", "type": "array", "items": { "$ref": "#/definitions/MuteTimeInterval" @@ -6142,6 +6244,12 @@ "items": { "type": "string" } + }, + "time_intervals": { + "type": "array", + "items": { + "$ref": "#/definitions/TimeInterval" + } } } }, @@ -6364,6 +6472,9 @@ "OK" ] }, + "notification_settings": { + "$ref": "#/definitions/AlertRuleNotificationSettings" + }, "title": { "type": "string" }, @@ -6551,6 +6662,9 @@ "OK" ] }, + "notification_settings": { + "$ref": "#/definitions/AlertRuleNotificationSettings" + }, "orgID": { "type": "integer", "format": "int64" @@ -7219,9 +7333,13 @@ } }, "Sample": { + "description": "Sample is a single sample belonging to a metric. It represents either a float\nsample or a histogram sample. If H is nil, it is a float sample. Otherwise,\nit is a histogram sample.", "type": "object", - "title": "Sample is a single sample belonging to a metric.", "properties": { + "F": { + "type": "number", + "format": "double" + }, "H": { "$ref": "#/definitions/FloatHistogram" }, @@ -7231,10 +7349,6 @@ "T": { "type": "integer", "format": "int64" - }, - "V": { - "type": "number", - "format": "double" } } }, @@ -7728,40 +7842,16 @@ "type": "string" }, "TimeInterval": { - "description": "TimeInterval describes intervals of time. ContainsTime will tell you if a golang time is contained\nwithin the interval.", "type": "object", + "title": "TimeInterval represents a named set of time intervals for which a route should be muted.", "properties": { - "days_of_month": { - "type": "array", - "items": { - "type": "string" - } - }, - "location": { + "name": { "type": "string" }, - "months": { - "type": "array", - "items": { - "type": "string" - } - }, - "times": { - "type": "array", - "items": { - "$ref": "#/definitions/TimeRange" - } - }, - "weekdays": { - "type": "array", - "items": { - "type": "string" - } - }, - "years": { + "time_intervals": { "type": "array", "items": { - "type": "string" + "$ref": "#/definitions/TimeInterval" } } } @@ -7830,9 +7920,8 @@ } }, "URL": { - "description": "The general form represented is:\n\n[scheme:][//[userinfo@]host][/]path[?query][#fragment]\n\nURLs that do not start with a slash after the scheme are interpreted as:\n\nscheme:opaque[?query][#fragment]\n\nNote that the Path field is stored in decoded form: /%47%6f%2f becomes /Go/.\nA consequence is that it is impossible to tell which slashes in the Path were\nslashes in the raw URL and which were %2f. This distinction is rarely important,\nbut when it is, the code should use the EscapedPath method, which preserves\nthe original encoding of Path.\n\nThe RawPath field is an optional field which is only set when the default\nencoding of Path is different from the escaped path. See the EscapedPath method\nfor more details.\n\nURL's String method uses the EscapedPath method to obtain the path.", "type": "object", - "title": "A URL represents a parsed URL (technically, a URI reference).", + "title": "URL is a custom URL type that allows validation at configuration load time.", "properties": { "ForceQuery": { "type": "boolean" @@ -7919,7 +8008,7 @@ } }, "Vector": { - "description": "Vector is basically only an alias for model.Samples, but the\ncontract is that in a Vector, all Samples have the same timestamp.", + "description": "Vector is basically only an alias for []Sample, but the contract is that\nin a Vector, all Samples have the same timestamp.", "type": "array", "items": { "$ref": "#/definitions/Sample" @@ -8072,6 +8161,7 @@ } }, "alertGroup": { + "description": "AlertGroup alert group", "type": "object", "required": [ "alerts", @@ -8259,6 +8349,7 @@ "$ref": "#/definitions/gettableAlert" }, "gettableAlerts": { + "description": "GettableAlerts gettable alerts", "type": "array", "items": { "$ref": "#/definitions/gettableAlert" @@ -8315,6 +8406,7 @@ "$ref": "#/definitions/gettableSilence" }, "gettableSilences": { + "description": "GettableSilences gettable silences", "type": "array", "items": { "$ref": "#/definitions/gettableSilence" @@ -8467,6 +8559,7 @@ } }, "postableSilence": { + "description": "PostableSilence postable silence", "type": "object", "required": [ "comment", diff --git a/public/api-merged.json b/public/api-merged.json index 725bd8a0d9..c528864407 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -12261,6 +12261,9 @@ "OK" ] }, + "notification_settings": { + "$ref": "#/definitions/AlertRuleNotificationSettingsExport" + }, "panelId": { "type": "integer", "format": "int64" @@ -12328,6 +12331,90 @@ } } }, + "AlertRuleNotificationSettings": { + "type": "object", + "required": [ + "receiver" + ], + "properties": { + "group_by": { + "description": "Override the labels by which incoming alerts are grouped together. For example, multiple alerts coming in for\ncluster=A and alertname=LatencyHigh would be batched into a single group. To aggregate by all possible labels\nuse the special value '...' as the sole label name.\nThis effectively disables aggregation entirely, passing through all alerts as-is. This is unlikely to be what\nyou want, unless you have a very low alert volume or your upstream notification system performs its own grouping.\nMust include 'alertname' and 'grafana_folder' if not using '...'.", + "type": "array", + "default": [ + "alertname", + "grafana_folder" + ], + "items": { + "type": "string" + }, + "example": [ + "alertname", + "grafana_folder", + "cluster" + ] + }, + "group_interval": { + "description": "Override how long to wait before sending a notification about new alerts that are added to a group of alerts for\nwhich an initial notification has already been sent. (Usually ~5m or more.)", + "type": "string", + "example": "5m" + }, + "group_wait": { + "description": "Override how long to initially wait to send a notification for a group of alerts. Allows to wait for an\ninhibiting alert to arrive or collect more initial alerts for the same group. (Usually ~0s to few minutes.)", + "type": "string", + "example": "30s" + }, + "mute_time_intervals": { + "description": "Override the times when notifications should be muted. These must match the name of a mute time interval defined\nin the alertmanager configuration mute_time_intervals section. When muted it will not send any notifications, but\notherwise acts normally.", + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "maintenance" + ] + }, + "receiver": { + "description": "Name of the receiver to send notifications to.", + "type": "string", + "example": "grafana-default-email" + }, + "repeat_interval": { + "description": "Override how long to wait before sending a notification again if it has already been sent successfully for an\nalert. (Usually ~3h or more).\nNote that this parameter is implicitly bound by Alertmanager's `--data.retention` configuration flag.\nNotifications will be resent after either repeat_interval or the data retention period have passed, whichever\noccurs first. `repeat_interval` should not be less than `group_interval`.", + "type": "string", + "example": "4h" + } + } + }, + "AlertRuleNotificationSettingsExport": { + "type": "object", + "title": "AlertRuleNotificationSettingsExport is the provisioned export of models.NotificationSettings.", + "properties": { + "group_by": { + "type": "array", + "items": { + "type": "string" + } + }, + "group_interval": { + "type": "string" + }, + "group_wait": { + "type": "string" + }, + "mute_time_intervals": { + "type": "array", + "items": { + "type": "string" + } + }, + "receiver": { + "type": "string" + }, + "repeat_interval": { + "type": "string" + } + } + }, "AlertRuleUpgrade": { "type": "object", "properties": { @@ -13243,6 +13330,7 @@ } }, "mute_time_intervals": { + "description": "MuteTimeIntervals is deprecated and will be removed before Alertmanager 1.0.", "type": "array", "items": { "$ref": "#/definitions/MuteTimeInterval" @@ -13256,6 +13344,12 @@ "items": { "type": "string" } + }, + "time_intervals": { + "type": "array", + "items": { + "$ref": "#/definitions/TimeInterval" + } } } }, @@ -14442,6 +14536,9 @@ }, "webhook_url": { "$ref": "#/definitions/SecretURL" + }, + "webhook_url_file": { + "type": "string" } } }, @@ -15242,6 +15339,7 @@ } }, "mute_time_intervals": { + "description": "MuteTimeIntervals is deprecated and will be removed before Alertmanager 1.0.", "type": "array", "items": { "$ref": "#/definitions/MuteTimeInterval" @@ -15262,6 +15360,12 @@ "items": { "type": "string" } + }, + "time_intervals": { + "type": "array", + "items": { + "$ref": "#/definitions/TimeInterval" + } } } }, @@ -15475,6 +15579,9 @@ "OK" ] }, + "notification_settings": { + "$ref": "#/definitions/AlertRuleNotificationSettings" + }, "orgId": { "type": "integer", "format": "int64" @@ -16440,6 +16547,9 @@ "send_resolved": { "type": "boolean" }, + "summary": { + "type": "string" + }, "text": { "type": "string" }, @@ -16448,6 +16558,9 @@ }, "webhook_url": { "$ref": "#/definitions/SecretURL" + }, + "webhook_url_file": { + "type": "string" } } }, @@ -17492,24 +17605,6 @@ "$ref": "#/definitions/Playlist" } }, - "Point": { - "description": "If H is not nil, then this is a histogram point and only (T, H) is valid.\nIf H is nil, then only (T, V) is valid.", - "type": "object", - "title": "Point represents a single data point for a given timestamp.", - "properties": { - "H": { - "$ref": "#/definitions/FloatHistogram" - }, - "T": { - "type": "integer", - "format": "int64" - }, - "V": { - "type": "number", - "format": "double" - } - } - }, "PostAnnotationsCmd": { "type": "object", "required": [ @@ -17578,6 +17673,7 @@ } }, "mute_time_intervals": { + "description": "MuteTimeIntervals is deprecated and will be removed before Alertmanager 1.0.", "type": "array", "items": { "$ref": "#/definitions/MuteTimeInterval" @@ -17598,6 +17694,12 @@ "items": { "type": "string" } + }, + "time_intervals": { + "type": "array", + "items": { + "$ref": "#/definitions/TimeInterval" + } } } }, @@ -17820,6 +17922,9 @@ "OK" ] }, + "notification_settings": { + "$ref": "#/definitions/AlertRuleNotificationSettings" + }, "title": { "type": "string" }, @@ -18053,6 +18158,9 @@ "OK" ] }, + "notification_settings": { + "$ref": "#/definitions/AlertRuleNotificationSettings" + }, "orgID": { "type": "integer", "format": "int64" @@ -19372,9 +19480,13 @@ } }, "Sample": { + "description": "Sample is a single sample belonging to a metric. It represents either a float\nsample or a histogram sample. If H is nil, it is a float sample. Otherwise,\nit is a histogram sample.", "type": "object", - "title": "Sample is a single sample belonging to a metric.", "properties": { + "F": { + "type": "number", + "format": "double" + }, "H": { "$ref": "#/definitions/FloatHistogram" }, @@ -19384,10 +19496,6 @@ "T": { "type": "integer", "format": "int64" - }, - "V": { - "type": "number", - "format": "double" } } }, @@ -20501,40 +20609,16 @@ "type": "string" }, "TimeInterval": { - "description": "TimeInterval describes intervals of time. ContainsTime will tell you if a golang time is contained\nwithin the interval.", "type": "object", + "title": "TimeInterval represents a named set of time intervals for which a route should be muted.", "properties": { - "days_of_month": { - "type": "array", - "items": { - "type": "string" - } - }, - "location": { + "name": { "type": "string" }, - "months": { - "type": "array", - "items": { - "type": "string" - } - }, - "times": { - "type": "array", - "items": { - "$ref": "#/definitions/TimeRange" - } - }, - "weekdays": { - "type": "array", - "items": { - "type": "string" - } - }, - "years": { + "time_intervals": { "type": "array", "items": { - "type": "string" + "$ref": "#/definitions/TimeInterval" } } } @@ -21520,7 +21604,7 @@ } }, "Vector": { - "description": "Vector is basically only an alias for model.Samples, but the\ncontract is that in a Vector, all Samples have the same timestamp.", + "description": "Vector is basically only an alias for []Sample, but the contract is that\nin a Vector, all Samples have the same timestamp.", "type": "array", "items": { "$ref": "#/definitions/Sample" @@ -21938,14 +22022,12 @@ } }, "gettableSilences": { - "description": "GettableSilences gettable silences", "type": "array", "items": { "$ref": "#/definitions/gettableSilence" } }, "integration": { - "description": "Integration integration", "type": "object", "required": [ "name", @@ -22154,6 +22236,7 @@ } }, "receiver": { + "description": "Receiver receiver", "type": "object", "required": [ "active", diff --git a/public/openapi3.json b/public/openapi3.json index bfa8084a60..718c294c00 100644 --- a/public/openapi3.json +++ b/public/openapi3.json @@ -2807,6 +2807,9 @@ ], "type": "string" }, + "notification_settings": { + "$ref": "#/components/schemas/AlertRuleNotificationSettingsExport" + }, "panelId": { "format": "int64", "type": "integer" @@ -2876,6 +2879,90 @@ }, "type": "object" }, + "AlertRuleNotificationSettings": { + "properties": { + "group_by": { + "default": [ + "alertname", + "grafana_folder" + ], + "description": "Override the labels by which incoming alerts are grouped together. For example, multiple alerts coming in for\ncluster=A and alertname=LatencyHigh would be batched into a single group. To aggregate by all possible labels\nuse the special value '...' as the sole label name.\nThis effectively disables aggregation entirely, passing through all alerts as-is. This is unlikely to be what\nyou want, unless you have a very low alert volume or your upstream notification system performs its own grouping.\nMust include 'alertname' and 'grafana_folder' if not using '...'.", + "example": [ + "alertname", + "grafana_folder", + "cluster" + ], + "items": { + "type": "string" + }, + "type": "array" + }, + "group_interval": { + "description": "Override how long to wait before sending a notification about new alerts that are added to a group of alerts for\nwhich an initial notification has already been sent. (Usually ~5m or more.)", + "example": "5m", + "type": "string" + }, + "group_wait": { + "description": "Override how long to initially wait to send a notification for a group of alerts. Allows to wait for an\ninhibiting alert to arrive or collect more initial alerts for the same group. (Usually ~0s to few minutes.)", + "example": "30s", + "type": "string" + }, + "mute_time_intervals": { + "description": "Override the times when notifications should be muted. These must match the name of a mute time interval defined\nin the alertmanager configuration mute_time_intervals section. When muted it will not send any notifications, but\notherwise acts normally.", + "example": [ + "maintenance" + ], + "items": { + "type": "string" + }, + "type": "array" + }, + "receiver": { + "description": "Name of the receiver to send notifications to.", + "example": "grafana-default-email", + "type": "string" + }, + "repeat_interval": { + "description": "Override how long to wait before sending a notification again if it has already been sent successfully for an\nalert. (Usually ~3h or more).\nNote that this parameter is implicitly bound by Alertmanager's `--data.retention` configuration flag.\nNotifications will be resent after either repeat_interval or the data retention period have passed, whichever\noccurs first. `repeat_interval` should not be less than `group_interval`.", + "example": "4h", + "type": "string" + } + }, + "required": [ + "receiver" + ], + "type": "object" + }, + "AlertRuleNotificationSettingsExport": { + "properties": { + "group_by": { + "items": { + "type": "string" + }, + "type": "array" + }, + "group_interval": { + "type": "string" + }, + "group_wait": { + "type": "string" + }, + "mute_time_intervals": { + "items": { + "type": "string" + }, + "type": "array" + }, + "receiver": { + "type": "string" + }, + "repeat_interval": { + "type": "string" + } + }, + "title": "AlertRuleNotificationSettingsExport is the provisioned export of models.NotificationSettings.", + "type": "object" + }, "AlertRuleUpgrade": { "properties": { "sendsTo": { @@ -3789,6 +3876,7 @@ "type": "array" }, "mute_time_intervals": { + "description": "MuteTimeIntervals is deprecated and will be removed before Alertmanager 1.0.", "items": { "$ref": "#/components/schemas/MuteTimeInterval" }, @@ -3802,6 +3890,12 @@ "type": "string" }, "type": "array" + }, + "time_intervals": { + "items": { + "$ref": "#/components/schemas/TimeInterval" + }, + "type": "array" } }, "title": "Config is the top-level configuration for Alertmanager's config files.", @@ -4988,6 +5082,9 @@ }, "webhook_url": { "$ref": "#/components/schemas/SecretURL" + }, + "webhook_url_file": { + "type": "string" } }, "title": "DiscordConfig configures notifications via Discord.", @@ -5789,6 +5886,7 @@ "type": "object" }, "mute_time_intervals": { + "description": "MuteTimeIntervals is deprecated and will be removed before Alertmanager 1.0.", "items": { "$ref": "#/components/schemas/MuteTimeInterval" }, @@ -5809,6 +5907,12 @@ "type": "string" }, "type": "array" + }, + "time_intervals": { + "items": { + "$ref": "#/components/schemas/TimeInterval" + }, + "type": "array" } }, "type": "object" @@ -6022,6 +6126,9 @@ ], "type": "string" }, + "notification_settings": { + "$ref": "#/components/schemas/AlertRuleNotificationSettings" + }, "orgId": { "format": "int64", "type": "integer" @@ -6987,6 +7094,9 @@ "send_resolved": { "type": "boolean" }, + "summary": { + "type": "string" + }, "text": { "type": "string" }, @@ -6995,6 +7105,9 @@ }, "webhook_url": { "$ref": "#/components/schemas/SecretURL" + }, + "webhook_url_file": { + "type": "string" } }, "type": "object" @@ -8040,24 +8153,6 @@ }, "type": "array" }, - "Point": { - "description": "If H is not nil, then this is a histogram point and only (T, H) is valid.\nIf H is nil, then only (T, V) is valid.", - "properties": { - "H": { - "$ref": "#/components/schemas/FloatHistogram" - }, - "T": { - "format": "int64", - "type": "integer" - }, - "V": { - "format": "double", - "type": "number" - } - }, - "title": "Point represents a single data point for a given timestamp.", - "type": "object" - }, "PostAnnotationsCmd": { "properties": { "dashboardId": { @@ -8125,6 +8220,7 @@ "type": "array" }, "mute_time_intervals": { + "description": "MuteTimeIntervals is deprecated and will be removed before Alertmanager 1.0.", "items": { "$ref": "#/components/schemas/MuteTimeInterval" }, @@ -8145,6 +8241,12 @@ "type": "string" }, "type": "array" + }, + "time_intervals": { + "items": { + "$ref": "#/components/schemas/TimeInterval" + }, + "type": "array" } }, "type": "object" @@ -8367,6 +8469,9 @@ ], "type": "string" }, + "notification_settings": { + "$ref": "#/components/schemas/AlertRuleNotificationSettings" + }, "title": { "type": "string" }, @@ -8589,6 +8694,9 @@ ], "type": "string" }, + "notification_settings": { + "$ref": "#/components/schemas/AlertRuleNotificationSettings" + }, "orgID": { "format": "int64", "type": "integer" @@ -9920,7 +10028,12 @@ "type": "object" }, "Sample": { + "description": "Sample is a single sample belonging to a metric. It represents either a float\nsample or a histogram sample. If H is nil, it is a float sample. Otherwise,\nit is a histogram sample.", "properties": { + "F": { + "format": "double", + "type": "number" + }, "H": { "$ref": "#/components/schemas/FloatHistogram" }, @@ -9930,13 +10043,8 @@ "T": { "format": "int64", "type": "integer" - }, - "V": { - "format": "double", - "type": "number" } }, - "title": "Sample is a single sample belonging to a metric.", "type": "object" }, "SaveDashboardCommand": { @@ -11048,42 +11156,18 @@ "type": "string" }, "TimeInterval": { - "description": "TimeInterval describes intervals of time. ContainsTime will tell you if a golang time is contained\nwithin the interval.", "properties": { - "days_of_month": { - "items": { - "type": "string" - }, - "type": "array" - }, - "location": { + "name": { "type": "string" }, - "months": { - "items": { - "type": "string" - }, - "type": "array" - }, - "times": { - "items": { - "$ref": "#/components/schemas/TimeRange" - }, - "type": "array" - }, - "weekdays": { - "items": { - "type": "string" - }, - "type": "array" - }, - "years": { + "time_intervals": { "items": { - "type": "string" + "$ref": "#/components/schemas/TimeInterval" }, "type": "array" } }, + "title": "TimeInterval represents a named set of time intervals for which a route should be muted.", "type": "object" }, "TimeIntervalItem": { @@ -12067,7 +12151,7 @@ "type": "array" }, "Vector": { - "description": "Vector is basically only an alias for model.Samples, but the\ncontract is that in a Vector, all Samples have the same timestamp.", + "description": "Vector is basically only an alias for []Sample, but the contract is that\nin a Vector, all Samples have the same timestamp.", "items": { "$ref": "#/components/schemas/Sample" }, @@ -12485,14 +12569,12 @@ "type": "object" }, "gettableSilences": { - "description": "GettableSilences gettable silences", "items": { "$ref": "#/components/schemas/gettableSilence" }, "type": "array" }, "integration": { - "description": "Integration integration", "properties": { "lastNotifyAttempt": { "description": "A timestamp indicating the last attempt to deliver a notification regardless of the outcome.\nFormat: date-time", @@ -12701,6 +12783,7 @@ "type": "object" }, "receiver": { + "description": "Receiver receiver", "properties": { "active": { "description": "active", From b25667223c77f8acdb6225d9454525f4416b1b72 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Fri, 23 Feb 2024 11:38:36 +0000 Subject: [PATCH 0123/1406] Box: add `direction` prop to `Box` (#83296) add "direction" prop to Box --- .../src/components/Layout/Box/Box.story.tsx | 1 + .../src/components/Layout/Box/Box.tsx | 9 ++- .../EmptyState/CallToAction.tsx | 18 +++-- .../EmptyState/InfoPaneLeft.tsx | 70 +++++++++---------- .../EmptyState/InfoPaneRight.tsx | 66 +++++++++-------- 5 files changed, 83 insertions(+), 81 deletions(-) diff --git a/packages/grafana-ui/src/components/Layout/Box/Box.story.tsx b/packages/grafana-ui/src/components/Layout/Box/Box.story.tsx index 203f9f1310..b564a3d247 100644 --- a/packages/grafana-ui/src/components/Layout/Box/Box.story.tsx +++ b/packages/grafana-ui/src/components/Layout/Box/Box.story.tsx @@ -73,6 +73,7 @@ Basic.argTypes = { paddingBottom: SpacingTokenControl, paddingLeft: SpacingTokenControl, paddingRight: SpacingTokenControl, + direction: { control: 'select', options: ['row', 'row-reverse', 'column', 'column-reverse'] }, display: { control: 'select', options: ['flex', 'block', 'inline', 'none'] }, backgroundColor: { control: 'select', options: backgroundOptions }, borderStyle: { control: 'select', options: borderStyleOptions }, diff --git a/packages/grafana-ui/src/components/Layout/Box/Box.tsx b/packages/grafana-ui/src/components/Layout/Box/Box.tsx index 754f088955..3f4a36c695 100644 --- a/packages/grafana-ui/src/components/Layout/Box/Box.tsx +++ b/packages/grafana-ui/src/components/Layout/Box/Box.tsx @@ -4,7 +4,7 @@ import React, { ElementType, forwardRef, PropsWithChildren } from 'react'; import { GrafanaTheme2, ThemeSpacingTokens, ThemeShape, ThemeShadows } from '@grafana/data'; import { useStyles2 } from '../../../themes'; -import { AlignItems, FlexProps, JustifyContent } from '../types'; +import { AlignItems, Direction, FlexProps, JustifyContent } from '../types'; import { ResponsiveProp, getResponsiveStyle } from '../utils/responsiveness'; type Display = 'flex' | 'block' | 'inline' | 'inline-block' | 'none'; @@ -54,6 +54,7 @@ interface BoxProps extends FlexProps, Omit, 'c // Flex Props alignItems?: ResponsiveProp; + direction?: ResponsiveProp; justifyContent?: ResponsiveProp; gap?: ResponsiveProp; @@ -91,6 +92,7 @@ export const Box = forwardRef>((props, borderColor, borderStyle, borderRadius, + direction, justifyContent, alignItems, boxShadow, @@ -123,6 +125,7 @@ export const Box = forwardRef>((props, borderColor, borderStyle, borderRadius, + direction, justifyContent, alignItems, boxShadow, @@ -188,6 +191,7 @@ const getStyles = ( borderColor: BoxProps['borderColor'], borderStyle: BoxProps['borderStyle'], borderRadius: BoxProps['borderRadius'], + direction: BoxProps['direction'], justifyContent: BoxProps['justifyContent'], alignItems: BoxProps['alignItems'], boxShadow: BoxProps['boxShadow'], @@ -247,6 +251,9 @@ const getStyles = ( getResponsiveStyle(theme, backgroundColor, (val) => ({ backgroundColor: customBackgroundColor(val, theme), })), + getResponsiveStyle(theme, direction, (val) => ({ + flexDirection: val, + })), getResponsiveStyle(theme, grow, (val) => ({ flexGrow: val, })), diff --git a/public/app/features/admin/migrate-to-cloud/EmptyState/CallToAction.tsx b/public/app/features/admin/migrate-to-cloud/EmptyState/CallToAction.tsx index b44796c8c1..75ac923613 100644 --- a/public/app/features/admin/migrate-to-cloud/EmptyState/CallToAction.tsx +++ b/public/app/features/admin/migrate-to-cloud/EmptyState/CallToAction.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { Box, Button, Stack, Text } from '@grafana/ui'; +import { Box, Button, Text } from '@grafana/ui'; import { Trans } from 'app/core/internationalization'; export const CallToAction = () => { @@ -9,15 +9,13 @@ export const CallToAction = () => { }; return ( - - - - Let us manage your Grafana stack - - - + + + Let us manage your Grafana stack + + ); }; diff --git a/public/app/features/admin/migrate-to-cloud/EmptyState/InfoPaneLeft.tsx b/public/app/features/admin/migrate-to-cloud/EmptyState/InfoPaneLeft.tsx index 91eceb990e..babc542e3e 100644 --- a/public/app/features/admin/migrate-to-cloud/EmptyState/InfoPaneLeft.tsx +++ b/public/app/features/admin/migrate-to-cloud/EmptyState/InfoPaneLeft.tsx @@ -1,47 +1,45 @@ import React from 'react'; -import { Box, Stack } from '@grafana/ui'; +import { Box } from '@grafana/ui'; import { t, Trans } from 'app/core/internationalization'; import { InfoItem } from './InfoItem'; export const InfoPaneLeft = () => { return ( - - - - - Grafana cloud is a fully managed cloud-hosted observability platform ideal for cloud native environments. - It's everything you love about Grafana without the overhead of maintaining, upgrading, and supporting - an installation. - - - - - In addition to the convenience of managed hosting, Grafana Cloud includes many cloud-exclusive features like - SLOs, incident management, machine learning, and powerful observability integrations. - - - - - Grafana Labs is committed to maintaining the highest standards of data privacy and security. By implementing - industry-standard security technologies and procedures, we help protect our customers' data from - unauthorized access, use, or disclosure. - - - + + + + Grafana cloud is a fully managed cloud-hosted observability platform ideal for cloud native environments. + It's everything you love about Grafana without the overhead of maintaining, upgrading, and supporting an + installation. + + + + + In addition to the convenience of managed hosting, Grafana Cloud includes many cloud-exclusive features like + SLOs, incident management, machine learning, and powerful observability integrations. + + + + + Grafana Labs is committed to maintaining the highest standards of data privacy and security. By implementing + industry-standard security technologies and procedures, we help protect our customers' data from + unauthorized access, use, or disclosure. + + ); }; diff --git a/public/app/features/admin/migrate-to-cloud/EmptyState/InfoPaneRight.tsx b/public/app/features/admin/migrate-to-cloud/EmptyState/InfoPaneRight.tsx index 48cbb1acd8..56fccbefaa 100644 --- a/public/app/features/admin/migrate-to-cloud/EmptyState/InfoPaneRight.tsx +++ b/public/app/features/admin/migrate-to-cloud/EmptyState/InfoPaneRight.tsx @@ -1,45 +1,43 @@ import React from 'react'; -import { Box, Stack } from '@grafana/ui'; +import { Box } from '@grafana/ui'; import { t, Trans } from 'app/core/internationalization'; import { InfoItem } from './InfoItem'; export const InfoPaneRight = () => { return ( - - - - - Exposing your data sources to the internet can raise security concerns. Private data source connect (PDC) - allows Grafana Cloud to access your existing data sources over a secure network tunnel. - - - - - Grafana Cloud has a generous free plan and a 14 day unlimited usage trial. After your trial expires, - you'll be billed based on usage over the free plan limits. - - - - - Once you connect this installation to a cloud stack, you'll be able to upload data sources and - dashboards. - - - + + + + Exposing your data sources to the internet can raise security concerns. Private data source connect (PDC) + allows Grafana Cloud to access your existing data sources over a secure network tunnel. + + + + + Grafana Cloud has a generous free plan and a 14 day unlimited usage trial. After your trial expires, + you'll be billed based on usage over the free plan limits. + + + + + Once you connect this installation to a cloud stack, you'll be able to upload data sources and + dashboards. + + ); }; From 3e456127cb4539587abb46676457df1abc3b767f Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Fri, 23 Feb 2024 12:39:30 +0100 Subject: [PATCH 0124/1406] E2E: Add plugin-e2e scenario verification tests (#79969) * add playwright test and plugin-e2e * run tests in ci * add ds config tests * add panel edit tests * add annotation test * add variable edit page tests * add explore page tests * add panel plugin tests * add readme * remove comments * fix broken test * remove user.json * remove newline in starlark * fix lint issue * ignore failure of playwright tests * update code owners * add detailed error messages in every expect * update message frame * fix link * upload report to gcp * echo url * add playwright developer guide * bump plugin-e2e * add custom provisioning dir * update plugin-e2e * remove not used imports * fix typo * minor fixes * use latest version of plugin-e2e * fix broken link * use latest plugin-e2e * add feature toggle scenario verification tests * bump version * use auth file from package * fix type error * add panel data assertions * rename parent dir and bump version * fix codeowners * reset files * remove not used file * update plugin-e2e * separate tests per role * pass prov dir * skip using provisioning fixture * wip * fix permission test * move to e2e dir * fix path to readme * post comment with report url * format starlark * post comment with report url * post comment with report url * fix token * make test fail * fix exit code * bump version * bump to latest plugin-e2e * revert reporting message * remove comments * readding report comment * change exit code * format starlark * force test to fail * add new step that posts comment * fix link * use latest playwright image * fix failing test * format starlark * remove unused fixture Co-authored-by: Marcus Andersson --------- Co-authored-by: Marcus Andersson --- .drone.yml | 112 +- .github/CODEOWNERS | 2 + .gitignore | 6 + contribute/developer-guide.md | 34 +- e2e/plugin-e2e/plugin-e2e-api-tests/README.md | 8 + .../as-admin-user/annotationEditPage.spec.ts | 15 + .../datasourceConfigPage.spec.ts | 35 + .../as-admin-user/explorePage.spec.ts | 15 + .../as-admin-user/featureToggles.spec.ts | 17 + .../as-admin-user/panelDataAssertion.spec.ts | 110 + .../as-admin-user/panelEditPage.spec.ts | 85 + .../as-admin-user/variableEditPage.spec.ts | 16 + .../as-viewer-user/permissions.spec.ts | 15 + e2e/plugin-e2e/plugin-e2e-api-tests/errors.ts | 4 + .../plugin-e2e-api-tests/mocks/queries.ts | 77 + .../plugin-e2e-api-tests/mocks/resources.ts | 19 + package.json | 5 + playwright.config.ts | 65 + scripts/drone/pipelines/build.star | 6 + scripts/drone/steps/lib.star | 78 + scripts/drone/utils/images.star | 1 + yarn.lock | 1834 +++++++++++++++-- 22 files changed, 2347 insertions(+), 212 deletions(-) create mode 100644 e2e/plugin-e2e/plugin-e2e-api-tests/README.md create mode 100644 e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/annotationEditPage.spec.ts create mode 100644 e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/datasourceConfigPage.spec.ts create mode 100644 e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/explorePage.spec.ts create mode 100644 e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/featureToggles.spec.ts create mode 100644 e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelDataAssertion.spec.ts create mode 100644 e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelEditPage.spec.ts create mode 100644 e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/variableEditPage.spec.ts create mode 100644 e2e/plugin-e2e/plugin-e2e-api-tests/as-viewer-user/permissions.spec.ts create mode 100644 e2e/plugin-e2e/plugin-e2e-api-tests/errors.ts create mode 100644 e2e/plugin-e2e/plugin-e2e-api-tests/mocks/queries.ts create mode 100644 e2e/plugin-e2e/plugin-e2e-api-tests/mocks/resources.ts create mode 100644 playwright.config.ts diff --git a/.drone.yml b/.drone.yml index 1e4a58497c..77c10b1237 100644 --- a/.drone.yml +++ b/.drone.yml @@ -653,6 +653,60 @@ steps: - e2e/cloud-plugins-suite/azure-monitor.spec.ts repo: - grafana/grafana +- commands: + - sleep 10s + - yarn e2e:playwright + depends_on: + - grafana-server + environment: + HOST: grafana-server + PORT: "3001" + PROV_DIR: /grafana/scripts/grafana-server/tmp/conf/provisioning + failure: ignore + image: mcr.microsoft.com/playwright:v1.41.2-jammy + name: playwright-plugin-e2e +- commands: + - apt-get update + - apt-get install -yq zip + - printenv GCP_GRAFANA_UPLOAD_ARTIFACTS_KEY > /tmp/gcpkey_upload_artifacts.json + - gcloud auth activate-service-account --key-file=/tmp/gcpkey_upload_artifacts.json + - gsutil cp -r ./playwright-report/. gs://releng-pipeline-artifacts-dev/${DRONE_BUILD_NUMBER}/playwright-report + - export E2E_PLAYWRIGHT_REPORT_URL=https://storage.googleapis.com/releng-pipeline-artifacts-dev/${DRONE_BUILD_NUMBER}/playwright-report/index.html + - "echo \"E2E Playwright report uploaded to: \n $${E2E_PLAYWRIGHT_REPORT_URL}\"" + depends_on: + - playwright-plugin-e2e + environment: + GCP_GRAFANA_UPLOAD_ARTIFACTS_KEY: + from_secret: gcp_upload_artifacts_key + failure: ignore + image: google/cloud-sdk:431.0.0 + name: playwright-e2e-report-upload + when: + status: + - success + - failure +- commands: + - if [ ! -d ./playwright-report/trace ]; then echo 'all tests passed'; exit 0; fi + - export E2E_PLAYWRIGHT_REPORT_URL=https://storage.googleapis.com/releng-pipeline-artifacts-dev/${DRONE_BUILD_NUMBER}/playwright-report/index.html + - 'curl -L -X POST https://api.github.com/repos/grafana/grafana/issues/${DRONE_PULL_REQUEST}/comments + -H "Accept: application/vnd.github+json" -H "Authorization: Bearer $${GITHUB_TOKEN}" + -H "X-GitHub-Api-Version: 2022-11-28" -d "{\"body\":\"❌ Failed to run Playwright + plugin e2e tests.

Click [here]($${E2E_PLAYWRIGHT_REPORT_URL}) to + browse the Playwright report and trace viewer.
For information on how to + run Playwright tests locally, refer to the [Developer guide](https://github.com/grafana/grafana/blob/main/contribute/developer-guide.md#to-run-the-playwright-tests). + \"}"' + depends_on: + - playwright-e2e-report-upload + environment: + GITHUB_TOKEN: + from_secret: github_token + failure: ignore + image: byrnedo/alpine-curl:0.1.8 + name: playwright-e2e-report-post-link + when: + status: + - success + - failure - commands: - if [ -z `find ./e2e -type f -name *spec.ts.mp4` ]; then echo 'missing videos'; false; fi @@ -1929,6 +1983,60 @@ steps: - e2e/cloud-plugins-suite/azure-monitor.spec.ts repo: - grafana/grafana +- commands: + - sleep 10s + - yarn e2e:playwright + depends_on: + - grafana-server + environment: + HOST: grafana-server + PORT: "3001" + PROV_DIR: /grafana/scripts/grafana-server/tmp/conf/provisioning + failure: ignore + image: mcr.microsoft.com/playwright:v1.41.2-jammy + name: playwright-plugin-e2e +- commands: + - apt-get update + - apt-get install -yq zip + - printenv GCP_GRAFANA_UPLOAD_ARTIFACTS_KEY > /tmp/gcpkey_upload_artifacts.json + - gcloud auth activate-service-account --key-file=/tmp/gcpkey_upload_artifacts.json + - gsutil cp -r ./playwright-report/. gs://releng-pipeline-artifacts-dev/${DRONE_BUILD_NUMBER}/playwright-report + - export E2E_PLAYWRIGHT_REPORT_URL=https://storage.googleapis.com/releng-pipeline-artifacts-dev/${DRONE_BUILD_NUMBER}/playwright-report/index.html + - "echo \"E2E Playwright report uploaded to: \n $${E2E_PLAYWRIGHT_REPORT_URL}\"" + depends_on: + - playwright-plugin-e2e + environment: + GCP_GRAFANA_UPLOAD_ARTIFACTS_KEY: + from_secret: gcp_upload_artifacts_key + failure: ignore + image: google/cloud-sdk:431.0.0 + name: playwright-e2e-report-upload + when: + status: + - success + - failure +- commands: + - if [ ! -d ./playwright-report/trace ]; then echo 'all tests passed'; exit 0; fi + - export E2E_PLAYWRIGHT_REPORT_URL=https://storage.googleapis.com/releng-pipeline-artifacts-dev/${DRONE_BUILD_NUMBER}/playwright-report/index.html + - 'curl -L -X POST https://api.github.com/repos/grafana/grafana/issues/${DRONE_PULL_REQUEST}/comments + -H "Accept: application/vnd.github+json" -H "Authorization: Bearer $${GITHUB_TOKEN}" + -H "X-GitHub-Api-Version: 2022-11-28" -d "{\"body\":\"❌ Failed to run Playwright + plugin e2e tests.

Click [here]($${E2E_PLAYWRIGHT_REPORT_URL}) to + browse the Playwright report and trace viewer.
For information on how to + run Playwright tests locally, refer to the [Developer guide](https://github.com/grafana/grafana/blob/main/contribute/developer-guide.md#to-run-the-playwright-tests). + \"}"' + depends_on: + - playwright-e2e-report-upload + environment: + GITHUB_TOKEN: + from_secret: github_token + failure: ignore + image: byrnedo/alpine-curl:0.1.8 + name: playwright-e2e-report-post-link + when: + status: + - success + - failure - commands: - if [ -z `find ./e2e -type f -name *spec.ts.mp4` ]; then echo 'missing videos'; false; fi @@ -4539,6 +4647,7 @@ steps: - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM cypress/included:13.1.0 - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM jwilder/dockerize:0.6.1 - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM koalaman/shellcheck:stable + - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM mcr.microsoft.com/playwright:v1.41.2-jammy depends_on: - authenticate-gcr image: aquasec/trivy:0.21.0 @@ -4573,6 +4682,7 @@ steps: - trivy --exit-code 1 --severity HIGH,CRITICAL cypress/included:13.1.0 - trivy --exit-code 1 --severity HIGH,CRITICAL jwilder/dockerize:0.6.1 - trivy --exit-code 1 --severity HIGH,CRITICAL koalaman/shellcheck:stable + - trivy --exit-code 1 --severity HIGH,CRITICAL mcr.microsoft.com/playwright:v1.41.2-jammy depends_on: - authenticate-gcr environment: @@ -4804,6 +4914,6 @@ kind: secret name: gcr_credentials --- kind: signature -hmac: 043eca32327fd48d9b479c3a51549b30f3825553c55df6220581def9bd22f513 +hmac: bce7b2ac72349019ec0c2ffbdca13c0591110ee53a3bfc72261df0ed05ca025a ... diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index a9aabc597e..aadbebd05b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -306,6 +306,7 @@ /public/app/core/internationalization/ @grafana/grafana-frontend-platform /e2e/ @grafana/grafana-frontend-platform /e2e/cloud-plugins-suite/ @grafana/partner-datasources +/e2e/plugin-e2e/plugin-e2e-api-tests/ @grafana/plugins-platform-frontend /packages/ @grafana/grafana-frontend-platform @grafana/plugins-platform-frontend /packages/grafana-e2e-selectors/ @grafana/grafana-frontend-platform /packages/grafana-e2e/ @grafana/grafana-frontend-platform @@ -365,6 +366,7 @@ /.husky/pre-commit @grafana/frontend-ops /cypress.config.js @grafana/grafana-frontend-platform /.levignore.js @grafana/plugins-platform-frontend +playwright.config.ts @grafana/plugins-platform-frontend # public folder diff --git a/.gitignore b/.gitignore index 8127687253..42f88c4425 100644 --- a/.gitignore +++ b/.gitignore @@ -171,6 +171,11 @@ compilation-stats.json /e2e/build_results.zip /e2e/extensions /e2e/extensions-suite +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ +/playwright/.auth/ # grafana server /scripts/grafana-server/server.log @@ -209,3 +214,4 @@ public/app/plugins/**/dist/ # Ignore transpiled JavaScript resulting from the generate-transformations.ts script. /public/app/features/transformers/docs/*.js /scripts/docs/generate-transformations.js + diff --git a/contribute/developer-guide.md b/contribute/developer-guide.md index 52923577d1..8ff02847bc 100644 --- a/contribute/developer-guide.md +++ b/contribute/developer-guide.md @@ -171,9 +171,11 @@ make test-go-integration-postgres ### Run end-to-end tests -The end to end tests in Grafana use [Cypress](https://www.cypress.io/) to run automated scripts in a headless Chromium browser. Read more about our [e2e framework](/contribute/style-guides/e2e.md). +The end to end tests in Grafana use [Cypress](https://www.cypress.io/) and [Playwright](https://playwright.dev/) to run automated scripts in a browser. Read more about our Cypress [e2e framework](/contribute/style-guides/e2e.md). -To run the tests: +#### Running Cypress tests + +To run all tests in a headless Chromium browser. ``` yarn e2e @@ -197,6 +199,34 @@ To choose a single test to follow in the browser as it runs, use `yarn e2e:dev` yarn e2e:dev ``` +#### To run the Playwright tests: + +**Note:** If you're using VS Code as your development editor, it's recommended to install the [Playwright test extension](https://marketplace.visualstudio.com/items?itemName=ms-playwright.playwright). It allows you to run, debug and generate Playwright tests from within the editor. For more information about the extension and how to install it, refer to the [Playwright documentation](https://playwright.dev/docs/getting-started-vscode). + +Each version of Playwright needs specific versions of browser binaries to operate. You will need to use the Playwright CLI to install these browsers. + +``` +yarn playwright install chromium +``` + +To run all tests in a headless Chromium browser and display results in the terminal. + +``` +yarn e2e:playwright +``` + +For a better developer experience, open the Playwright UI where you can easily walk through each step of the test and visually see what was happening before, during and after each step. + +``` +yarn e2e:playwright:ui +``` + +To open the HTML reporter for the last test run session. + +``` +yarn e2e:playwright:report +``` + ## Configure Grafana for development The default configuration, `defaults.ini`, is located in the `conf` directory. diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/README.md b/e2e/plugin-e2e/plugin-e2e-api-tests/README.md new file mode 100644 index 0000000000..477a6c384d --- /dev/null +++ b/e2e/plugin-e2e/plugin-e2e-api-tests/README.md @@ -0,0 +1,8 @@ +# @grafana/plugin-e2e API tests + +The purpose of the E2E tests in this directory is not to test the plugins per se - it's to verify that the fixtures, models and expect matchers provided by the [`@grafana/plugin-e2e`](https://github.com/grafana/plugin-tools/tree/main/packages/plugin-e2e) package are compatible with the latest version of Grafana. If you find that any of these tests are failing, it's probably due to one of the following reasons: + +- you have changed a value of a selector defined in @grafana/e2e-selector +- you have made structural changes to the UI + +For information on how to address this, follow the instructions in the [contributing guidelines](https://github.com/grafana/plugin-tools/blob/main/packages/plugin-e2e/CONTRIBUTING.md#how-to-fix-broken-test-scenarios-after-changes-in-grafana) for the @grafana/plugin-e2e package in the plugin-tools repository. diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/annotationEditPage.spec.ts b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/annotationEditPage.spec.ts new file mode 100644 index 0000000000..a23ceb8494 --- /dev/null +++ b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/annotationEditPage.spec.ts @@ -0,0 +1,15 @@ +import { expect, test } from '@grafana/plugin-e2e'; + +import { formatExpectError } from '../errors'; +import { successfulAnnotationQuery } from '../mocks/queries'; + +test('annotation query data with mocked response', async ({ annotationEditPage, page }) => { + annotationEditPage.mockQueryDataResponse(successfulAnnotationQuery); + await annotationEditPage.datasource.set('gdev-testdata'); + await page.getByLabel('Scenario').last().fill('CSV Content'); + await page.keyboard.press('Tab'); + await expect( + annotationEditPage.runQuery(), + formatExpectError('Expected annotation query to execute successfully') + ).toBeOK(); +}); diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/datasourceConfigPage.spec.ts b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/datasourceConfigPage.spec.ts new file mode 100644 index 0000000000..0176ddc268 --- /dev/null +++ b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/datasourceConfigPage.spec.ts @@ -0,0 +1,35 @@ +import { expect, test } from '@grafana/plugin-e2e'; + +import { formatExpectError } from '../errors'; + +test.describe('test createDataSourceConfigPage fixture, saveAndTest and toBeOK matcher', () => { + test('invalid credentials should return an error', async ({ createDataSourceConfigPage, page }) => { + const configPage = await createDataSourceConfigPage({ type: 'prometheus' }); + await page.getByPlaceholder('http://localhost:9090').fill('http://localhost:9090'); + await expect( + configPage.saveAndTest(), + formatExpectError('Expected save data source config to fail when Prometheus server is not running') + ).not.toBeOK(); + }); + + test('valid credentials should return a 200 status code', async ({ createDataSourceConfigPage, page }) => { + const configPage = await createDataSourceConfigPage({ type: 'prometheus' }); + configPage.mockHealthCheckResponse({ status: 200 }); + await page.getByPlaceholder('http://localhost:9090').fill('http://localhost:9090'); + await expect( + configPage.saveAndTest(), + formatExpectError('Expected data source config to be successfully saved') + ).toBeOK(); + }); +}); + +test.describe('test data source with frontend only health check', () => { + test('valid credentials should display a success alert on the page', async ({ createDataSourceConfigPage }) => { + const configPage = await createDataSourceConfigPage({ type: 'testdata' }); + await configPage.saveAndTest({ skipWaitForResponse: true }); + await expect( + configPage, + formatExpectError('Expected data source config to display success alert after save') + ).toHaveAlert('success', { hasNotText: 'Datasource updated' }); + }); +}); diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/explorePage.spec.ts b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/explorePage.spec.ts new file mode 100644 index 0000000000..d8780318f5 --- /dev/null +++ b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/explorePage.spec.ts @@ -0,0 +1,15 @@ +import { expect, test } from '@grafana/plugin-e2e'; + +import { formatExpectError } from '../errors'; + +test('query data response should be OK when query is valid', async ({ explorePage }) => { + await explorePage.datasource.set('gdev-testdata'); + await expect(explorePage.runQuery(), formatExpectError('Expected Explore query to execute successfully')).toBeOK(); +}); + +test('query data response should not be OK when query is invalid', async ({ explorePage }) => { + await explorePage.datasource.set('gdev-testdata'); + const queryEditorRow = await explorePage.getQueryEditorRow('A'); + await queryEditorRow.getByLabel('Labels').fill('invalid-label-format'); + await expect(explorePage.runQuery(), formatExpectError('Expected Explore query to fail')).not.toBeOK(); +}); diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/featureToggles.spec.ts b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/featureToggles.spec.ts new file mode 100644 index 0000000000..48ded217e4 --- /dev/null +++ b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/featureToggles.spec.ts @@ -0,0 +1,17 @@ +import { expect, test } from '@grafana/plugin-e2e'; + +const TRUTHY_CUSTOM_TOGGLE = 'custom_toggle1'; +const FALSY_CUSTOM_TOGGLE = 'custom_toggle2'; + +// override the feature toggles defined in playwright.config.ts only for tests in this file +test.use({ + featureToggles: { + [TRUTHY_CUSTOM_TOGGLE]: true, + [FALSY_CUSTOM_TOGGLE]: false, + }, +}); + +test('should set and check feature toggles correctly', async ({ isFeatureToggleEnabled }) => { + expect(await isFeatureToggleEnabled(TRUTHY_CUSTOM_TOGGLE)).toBeTruthy(); + expect(await isFeatureToggleEnabled(FALSY_CUSTOM_TOGGLE)).toBeFalsy(); +}); diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelDataAssertion.spec.ts b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelDataAssertion.spec.ts new file mode 100644 index 0000000000..b343baa8b4 --- /dev/null +++ b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelDataAssertion.spec.ts @@ -0,0 +1,110 @@ +import { expect, test, PanelEditPage, DashboardPage } from '@grafana/plugin-e2e'; + +import { formatExpectError } from '../errors'; +import { successfulDataQuery } from '../mocks/queries'; + +const REACT_TABLE_DASHBOARD = { uid: 'U_bZIMRMk' }; + +test.describe('panel edit page', () => { + test('table panel data assertions with provisioned dashboard', async ({ + page, + selectors, + grafanaVersion, + request, + }) => { + const panelEditPage = new PanelEditPage( + { page, selectors, grafanaVersion, request }, + { dashboard: REACT_TABLE_DASHBOARD, id: '4' } + ); + await panelEditPage.goto(); + await expect( + panelEditPage.panel.locator, + formatExpectError('Could not locate panel in panel edit page') + ).toBeVisible(); + await expect( + panelEditPage.panel.fieldNames, + formatExpectError('Could not locate header elements in table panel') + ).toContainText(['Field', 'Max', 'Mean', 'Last']); + }); + + test('table panel data assertions', async ({ panelEditPage }) => { + await panelEditPage.mockQueryDataResponse(successfulDataQuery, 200); + await panelEditPage.datasource.set('gdev-testdata'); + await panelEditPage.setVisualization('Table'); + await panelEditPage.refreshPanel(); + await expect( + panelEditPage.panel.locator, + formatExpectError('Could not locate panel in panel edit page') + ).toBeVisible(); + await expect( + panelEditPage.panel.fieldNames, + formatExpectError('Could not locate header elements in table panel') + ).toContainText(['col1', 'col2']); + await expect(panelEditPage.panel.data, formatExpectError('Could not locate headers in table panel')).toContainText([ + 'val1', + 'val2', + 'val3', + 'val4', + ]); + }); + + test('timeseries panel - table view assertions', async ({ panelEditPage }) => { + await panelEditPage.mockQueryDataResponse(successfulDataQuery, 200); + await panelEditPage.datasource.set('gdev-testdata'); + await panelEditPage.setVisualization('Time series'); + await panelEditPage.refreshPanel(); + await panelEditPage.toggleTableView(); + await expect( + panelEditPage.panel.locator, + formatExpectError('Could not locate panel in panel edit page') + ).toBeVisible(); + await expect( + panelEditPage.panel.fieldNames, + formatExpectError('Could not locate header elements in table panel') + ).toContainText(['col1', 'col2']); + await expect( + panelEditPage.panel.data, + formatExpectError('Could not locate data elements in table panel') + ).toContainText(['val1', 'val2', 'val3', 'val4']); + }); +}); + +test.describe('dashboard page', () => { + test('getting panel by title', async ({ page, selectors, grafanaVersion, request }) => { + const dashboardPage = new DashboardPage({ page, selectors, grafanaVersion, request }, REACT_TABLE_DASHBOARD); + await dashboardPage.goto(); + const panel = await dashboardPage.getPanelByTitle('Colored background'); + await expect(panel.fieldNames).toContainText(['Field', 'Max', 'Mean', 'Last']); + }); + + test('getting panel by id', async ({ page, selectors, grafanaVersion, request }) => { + const dashboardPage = new DashboardPage({ page, selectors, grafanaVersion, request }, REACT_TABLE_DASHBOARD); + await dashboardPage.goto(); + const panel = await dashboardPage.getPanelById('4'); + await expect(panel.fieldNames, formatExpectError('Could not locate header elements in table panel')).toContainText([ + 'Field', + 'Max', + 'Mean', + 'Last', + ]); + }); +}); + +test.describe('explore page', () => { + test('table panel', async ({ explorePage }) => { + const url = + 'left=%7B"datasource":"grafana","queries":%5B%7B"queryType":"randomWalk","refId":"A","datasource":%7B"type":"datasource","uid":"grafana"%7D%7D%5D,"range":%7B"from":"1547161200000","to":"1576364400000"%7D%7D&orgId=1'; + await explorePage.goto({ + queryParams: new URLSearchParams(url), + }); + await expect( + explorePage.timeSeriesPanel.locator, + formatExpectError('Could not locate time series panel in explore page') + ).toBeVisible(); + await expect( + explorePage.tablePanel.locator, + formatExpectError('Could not locate table panel in explore page') + ).toBeVisible(); + await expect(explorePage.tablePanel.fieldNames).toContainText(['time', 'A-series']); + }); +}); diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelEditPage.spec.ts b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelEditPage.spec.ts new file mode 100644 index 0000000000..a44827b0de --- /dev/null +++ b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelEditPage.spec.ts @@ -0,0 +1,85 @@ +import { expect, test } from '@grafana/plugin-e2e'; + +import { formatExpectError } from '../errors'; +import { successfulDataQuery } from '../mocks/queries'; +import { scenarios } from '../mocks/resources'; + +const PANEL_TITLE = 'Table panel E2E test'; +const TABLE_VIZ_NAME = 'Table'; +const STANDARD_OTIONS_CATEGORY = 'Standard options'; +const DISPLAY_NAME_LABEL = 'Display name'; + +test.describe('query editor query data', () => { + test('query data response should be OK when query is valid', async ({ panelEditPage }) => { + await panelEditPage.datasource.set('gdev-testdata'); + await expect( + panelEditPage.refreshPanel(), + formatExpectError('Expected panel query to execute successfully') + ).toBeOK(); + }); + + test('query data response should not be OK and panel error should be displayed when query is invalid', async ({ + panelEditPage, + }) => { + await panelEditPage.datasource.set('gdev-testdata'); + const queryEditorRow = await panelEditPage.getQueryEditorRow('A'); + await queryEditorRow.getByLabel('Labels').fill('invalid-label-format'); + await expect(panelEditPage.refreshPanel(), formatExpectError('Expected panel query to fail')).not.toBeOK(); + await expect( + panelEditPage.panel.getErrorIcon(), + formatExpectError('Expected panel error to be displayed after query execution') + ).toBeVisible(); + }); +}); + +test.describe('query editor with mocked responses', () => { + test('and resource `scenarios` is mocked', async ({ panelEditPage, selectors }) => { + await panelEditPage.mockResourceResponse('scenarios', scenarios); + await panelEditPage.datasource.set('gdev-testdata'); + const queryEditorRow = await panelEditPage.getQueryEditorRow('A'); + await queryEditorRow.getByLabel('Scenario').last().click(); + await expect( + panelEditPage.getByTestIdOrAriaLabel(selectors.components.Select.option), + formatExpectError('Expected certain select options to be displayed after clicking on the select input') + ).toHaveText(scenarios.map((s) => s.name)); + }); + + test('mocked query data response', async ({ panelEditPage, selectors }) => { + await panelEditPage.mockQueryDataResponse(successfulDataQuery, 200); + await panelEditPage.datasource.set('gdev-testdata'); + await panelEditPage.setVisualization(TABLE_VIZ_NAME); + await panelEditPage.refreshPanel(); + await expect( + panelEditPage.panel.getErrorIcon(), + formatExpectError('Did not expect panel error to be displayed after query execution') + ).not.toBeVisible(); + await expect( + panelEditPage.getByTestIdOrAriaLabel(selectors.components.Panels.Visualization.Table.body), + formatExpectError('Expected certain select options to be displayed after clicking on the select input') + ).toHaveText('val1val2val3val4'); + }); +}); + +test.describe('edit panel plugin settings', () => { + test('change viz to table panel, set panel title and collapse section', async ({ + panelEditPage, + selectors, + page, + }) => { + await panelEditPage.setVisualization(TABLE_VIZ_NAME); + await expect( + panelEditPage.getByTestIdOrAriaLabel(selectors.components.PanelEditor.toggleVizPicker), + formatExpectError('Expected panel visualization to be set to table') + ).toHaveText(TABLE_VIZ_NAME); + await panelEditPage.setPanelTitle(PANEL_TITLE); + await expect( + panelEditPage.getByTestIdOrAriaLabel(selectors.components.Panels.Panel.title(PANEL_TITLE)), + formatExpectError('Expected panel title to be updated') + ).toBeVisible(); + await panelEditPage.collapseSection(STANDARD_OTIONS_CATEGORY); + await expect( + page.getByText(DISPLAY_NAME_LABEL), + formatExpectError('Expected section to be collapsed') + ).toBeVisible(); + }); +}); diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/variableEditPage.spec.ts b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/variableEditPage.spec.ts new file mode 100644 index 0000000000..f76a5bc83a --- /dev/null +++ b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/variableEditPage.spec.ts @@ -0,0 +1,16 @@ +import { expect, test } from '@grafana/plugin-e2e'; + +import { formatExpectError } from '../errors'; +import { prometheusLabels } from '../mocks/resources'; + +test('variable query with mocked response', async ({ variableEditPage, page }) => { + variableEditPage.mockResourceResponse('api/v1/labels?*', prometheusLabels); + await variableEditPage.datasource.set('gdev-prometheus'); + await variableEditPage.getByTestIdOrAriaLabel('Query type').fill('Label names'); + await page.keyboard.press('Tab'); + await variableEditPage.runQuery(); + await expect( + variableEditPage, + formatExpectError('Expected variable edit page to display certain label names after query execution') + ).toDisplayPreviews(prometheusLabels.data); +}); diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/as-viewer-user/permissions.spec.ts b/e2e/plugin-e2e/plugin-e2e-api-tests/as-viewer-user/permissions.spec.ts new file mode 100644 index 0000000000..6346151f2c --- /dev/null +++ b/e2e/plugin-e2e/plugin-e2e-api-tests/as-viewer-user/permissions.spec.ts @@ -0,0 +1,15 @@ +import { expect, test } from '@grafana/plugin-e2e'; + +test('should redirect to start page when permissions to navigate to page is missing', async ({ page }) => { + await page.goto('/'); + const homePageTitle = await page.title(); + await page.goto('/datasources', { waitUntil: 'networkidle' }); + expect(await page.title()).toEqual(homePageTitle); +}); + +test('current user should have viewer role', async ({ page, request }) => { + await page.goto('/'); + const response = await request.get('/api/user/orgs'); + await expect(response).toBeOK(); + await expect(await response.json()).toContainEqual(expect.objectContaining({ role: 'Viewer' })); +}); diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/errors.ts b/e2e/plugin-e2e/plugin-e2e-api-tests/errors.ts new file mode 100644 index 0000000000..db7f985806 --- /dev/null +++ b/e2e/plugin-e2e/plugin-e2e-api-tests/errors.ts @@ -0,0 +1,4 @@ +export const formatExpectError = (message: string) => { + return `Error while verifying @grafana/plugin-e2e scenarios: ${message}. + See https://github.com/grafana/grafana/blob/main/plugin-e2e/plugin-e2e-api-tests/README.md for more information.`; +}; diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/mocks/queries.ts b/e2e/plugin-e2e/plugin-e2e-api-tests/mocks/queries.ts new file mode 100644 index 0000000000..16d2448192 --- /dev/null +++ b/e2e/plugin-e2e/plugin-e2e-api-tests/mocks/queries.ts @@ -0,0 +1,77 @@ +export const successfulDataQuery = { + results: { + A: { + status: 200, + frames: [ + { + schema: { + refId: 'A', + fields: [ + { + name: 'col1', + type: 'string', + typeInfo: { + frame: 'string', + nullable: true, + }, + }, + { + name: 'col2', + type: 'string', + typeInfo: { + frame: 'string', + nullable: true, + }, + }, + ], + }, + data: { + values: [ + ['val1', 'val3'], + ['val2', 'val4'], + ], + }, + }, + ], + }, + }, +}; + +export const successfulAnnotationQuery = { + results: { + Anno: { + status: 200, + frames: [ + { + schema: { + refId: 'Anno', + fields: [ + { + name: 'time', + type: 'time', + typeInfo: { + frame: 'time.Time', + nullable: true, + }, + }, + { + name: 'col2', + type: 'string', + typeInfo: { + frame: 'string', + nullable: true, + }, + }, + ], + }, + data: { + values: [ + [1702973084093, 1702973084099], + ['val1', 'val2'], + ], + }, + }, + ], + }, + }, +}; diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/mocks/resources.ts b/e2e/plugin-e2e/plugin-e2e-api-tests/mocks/resources.ts new file mode 100644 index 0000000000..658a74921e --- /dev/null +++ b/e2e/plugin-e2e/plugin-e2e-api-tests/mocks/resources.ts @@ -0,0 +1,19 @@ +export const scenarios = [ + { + description: '', + id: 'annotations', + name: 'Annotations', + stringInput: '', + }, + { + description: '', + id: 'arrow', + name: 'Load Apache Arrow Data', + stringInput: '', + }, +]; + +export const prometheusLabels = { + status: 'success', + data: ['__name__', 'action', 'active', 'address'], +}; diff --git a/package.json b/package.json index 6728f3302a..fe345505fc 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,9 @@ "e2e:enterprise": "./e2e/start-and-run-suite enterprise", "e2e:enterprise:dev": "./e2e/start-and-run-suite enterprise dev", "e2e:enterprise:debug": "./e2e/start-and-run-suite enterprise debug", + "e2e:playwright": "yarn playwright test", + "e2e:playwright:ui": "yarn playwright test --ui", + "e2e:playwright:report": "yarn playwright show-report", "test": "jest --notify --watch", "test:coverage": "jest --coverage", "test:coverage:changes": "jest --coverage --changedSince=origin/main", @@ -73,7 +76,9 @@ "@emotion/eslint-plugin": "11.11.0", "@grafana/eslint-config": "7.0.0", "@grafana/eslint-plugin": "link:./packages/grafana-eslint-rules", + "@grafana/plugin-e2e": "0.18.0", "@grafana/tsconfig": "^1.3.0-rc1", + "@playwright/test": "^1.41.2", "@pmmmwh/react-refresh-webpack-plugin": "0.5.11", "@react-types/button": "3.9.2", "@react-types/menu": "3.9.7", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000000..c653a8b608 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,65 @@ +import { defineConfig, devices } from '@playwright/test'; +import path, { dirname } from 'path'; + +import { PluginOptions } from '@grafana/plugin-e2e'; + +const testDirRoot = 'e2e/plugin-e2e/plugin-e2e-api-tests/'; + +export default defineConfig({ + fullyParallel: true, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + reporter: 'html', + use: { + baseURL: `http://${process.env.HOST || 'localhost'}:${process.env.PORT || 3000}`, + trace: 'on-first-retry', + httpCredentials: { + username: 'admin', + password: 'admin', + }, + provisioningRootDir: path.join(process.cwd(), process.env.PROV_DIR ?? 'conf/provisioning'), + }, + projects: [ + // Login to Grafana with admin user and store the cookie on disk for use in other tests + { + name: 'authenticate', + testDir: `${dirname(require.resolve('@grafana/plugin-e2e'))}/auth`, + testMatch: [/.*\.js/], + }, + // Login to Grafana with new user with viewer role and store the cookie on disk for use in other tests + { + name: 'createUserAndAuthenticate', + testDir: `${dirname(require.resolve('@grafana/plugin-e2e'))}/auth`, + testMatch: [/.*\.js/], + use: { + user: { + user: 'viewer', + password: 'password', + role: 'Viewer', + }, + }, + }, + // Run all tests in parallel using user with admin role + { + name: 'admin', + testDir: path.join(testDirRoot, '/as-admin-user'), + use: { + ...devices['Desktop Chrome'], + storageState: 'playwright/.auth/admin.json', + }, + dependencies: ['authenticate'], + }, + // Run all tests in parallel using user with viewer role + { + name: 'viewer', + testDir: path.join(testDirRoot, '/as-viewer-user'), + use: { + ...devices['Desktop Chrome'], + storageState: 'playwright/.auth/viewer.json', + }, + dependencies: ['createUserAndAuthenticate'], + }, + ], +}); diff --git a/scripts/drone/pipelines/build.star b/scripts/drone/pipelines/build.star index e12319f1fa..395ae441ca 100644 --- a/scripts/drone/pipelines/build.star +++ b/scripts/drone/pipelines/build.star @@ -13,6 +13,9 @@ load( "frontend_metrics_step", "grafana_server_step", "identify_runner_step", + "playwright_e2e_report_post_link", + "playwright_e2e_report_upload", + "playwright_e2e_tests_step", "publish_images_step", "release_canary_npm_packages_step", "store_storybook_step", @@ -100,6 +103,9 @@ def build_e2e(trigger, ver_mode): cloud = "azure", trigger = trigger_oss, ), + playwright_e2e_tests_step(), + playwright_e2e_report_upload(), + playwright_e2e_report_post_link(), e2e_tests_artifacts(), build_storybook_step(ver_mode = ver_mode), test_a11y_frontend_step(ver_mode = ver_mode), diff --git a/scripts/drone/steps/lib.star b/scripts/drone/steps/lib.star index f94f0ee4f3..df3f40f2ea 100644 --- a/scripts/drone/steps/lib.star +++ b/scripts/drone/steps/lib.star @@ -360,6 +360,65 @@ def e2e_tests_artifacts(): ], } +def playwright_e2e_report_upload(): + return { + "name": "playwright-e2e-report-upload", + "image": images["cloudsdk"], + "depends_on": [ + "playwright-plugin-e2e", + ], + "failure": "ignore", + "when": { + "status": [ + "success", + "failure", + ], + }, + "environment": { + "GCP_GRAFANA_UPLOAD_ARTIFACTS_KEY": from_secret(gcp_upload_artifacts_key), + }, + "commands": [ + "apt-get update", + "apt-get install -yq zip", + "printenv GCP_GRAFANA_UPLOAD_ARTIFACTS_KEY > /tmp/gcpkey_upload_artifacts.json", + "gcloud auth activate-service-account --key-file=/tmp/gcpkey_upload_artifacts.json", + "gsutil cp -r ./playwright-report/. gs://releng-pipeline-artifacts-dev/${DRONE_BUILD_NUMBER}/playwright-report", + "export E2E_PLAYWRIGHT_REPORT_URL=https://storage.googleapis.com/releng-pipeline-artifacts-dev/${DRONE_BUILD_NUMBER}/playwright-report/index.html", + 'echo "E2E Playwright report uploaded to: \n $${E2E_PLAYWRIGHT_REPORT_URL}"', + ], + } + +def playwright_e2e_report_post_link(): + return { + "name": "playwright-e2e-report-post-link", + "image": images["curl"], + "depends_on": [ + "playwright-e2e-report-upload", + ], + "failure": "ignore", + "when": { + "status": [ + "success", + "failure", + ], + }, + "environment": { + "GITHUB_TOKEN": from_secret("github_token"), + }, + "commands": [ + # if the trace doesn't folder exists, it means that there are no failed tests. + "if [ ! -d ./playwright-report/trace ]; then echo 'all tests passed'; exit 0; fi", + # if it exists, we will post a comment on the PR with the link to the report + "export E2E_PLAYWRIGHT_REPORT_URL=https://storage.googleapis.com/releng-pipeline-artifacts-dev/${DRONE_BUILD_NUMBER}/playwright-report/index.html", + "curl -L " + + "-X POST https://api.github.com/repos/grafana/grafana/issues/${DRONE_PULL_REQUEST}/comments " + + '-H "Accept: application/vnd.github+json" ' + + '-H "Authorization: Bearer $${GITHUB_TOKEN}" ' + + '-H "X-GitHub-Api-Version: 2022-11-28" -d ' + + '"{\\"body\\":\\"❌ Failed to run Playwright plugin e2e tests.

Click [here]($${E2E_PLAYWRIGHT_REPORT_URL}) to browse the Playwright report and trace viewer.
For information on how to run Playwright tests locally, refer to the [Developer guide](https://github.com/grafana/grafana/blob/main/contribute/developer-guide.md#to-run-the-playwright-tests). \\"}"', + ], + } + def upload_cdn_step(ver_mode, trigger = None): """Uploads CDN assets using the Grafana build tool. @@ -786,6 +845,25 @@ def cloud_plugins_e2e_tests_step(suite, cloud, trigger = None): step = dict(step, when = when) return step +def playwright_e2e_tests_step(): + return { + "environment": { + "PORT": "3001", + "HOST": "grafana-server", + "PROV_DIR": "/grafana/scripts/grafana-server/tmp/conf/provisioning", + }, + "name": "playwright-plugin-e2e", + "image": images["playwright"], + "failure": "ignore", + "depends_on": [ + "grafana-server", + ], + "commands": [ + "sleep 10s", # it seems sometimes that grafana-server is not actually ready when the step starts, so waiting for a few seconds before running the tests + "yarn e2e:playwright", + ], + } + def build_docs_website_step(): return { "name": "build-docs-website", diff --git a/scripts/drone/utils/images.star b/scripts/drone/utils/images.star index 59068896df..343ea0c755 100644 --- a/scripts/drone/utils/images.star +++ b/scripts/drone/utils/images.star @@ -33,4 +33,5 @@ images = { "cypress": "cypress/included:13.1.0", "dockerize": "jwilder/dockerize:0.6.1", "shellcheck": "koalaman/shellcheck:stable", + "playwright": "mcr.microsoft.com/playwright:v1.41.2-jammy", } diff --git a/yarn.lock b/yarn.lock index 800393ba18..e96abdc275 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12,6 +12,13 @@ __metadata: languageName: node linkType: hard +"@adobe/css-tools@npm:^4.3.0": + version: 4.3.1 + resolution: "@adobe/css-tools@npm:4.3.1" + checksum: 10/039a42ffdd41ecf3abcaf09c9fef0ffd634ccbe81c04002fc989e74564eba99bb19169a8f48dadf6442aa2c5c9f0925a7b27ec5c36a1ed1a3515fe77d6930996 + languageName: node + linkType: hard + "@adobe/css-tools@npm:^4.3.2": version: 4.3.2 resolution: "@adobe/css-tools@npm:4.3.2" @@ -103,7 +110,19 @@ __metadata: languageName: node linkType: hard -"@babel/generator@npm:^7.12.11, @babel/generator@npm:^7.22.9, @babel/generator@npm:^7.23.0, @babel/generator@npm:^7.23.6, @babel/generator@npm:^7.7.2": +"@babel/generator@npm:^7.12.11, @babel/generator@npm:^7.22.9, @babel/generator@npm:^7.23.0, @babel/generator@npm:^7.7.2": + version: 7.23.0 + resolution: "@babel/generator@npm:7.23.0" + dependencies: + "@babel/types": "npm:^7.23.0" + "@jridgewell/gen-mapping": "npm:^0.3.2" + "@jridgewell/trace-mapping": "npm:^0.3.17" + jsesc: "npm:^2.5.1" + checksum: 10/bd1598bd356756065d90ce26968dd464ac2b915c67623f6f071fb487da5f9eb454031a380e20e7c9a7ce5c4a49d23be6cb9efde404952b0b3f3c0c3a9b73d68a + languageName: node + linkType: hard + +"@babel/generator@npm:^7.23.6": version: 7.23.6 resolution: "@babel/generator@npm:7.23.6" dependencies: @@ -165,7 +184,20 @@ __metadata: languageName: node linkType: hard -"@babel/helper-create-regexp-features-plugin@npm:^7.18.6, @babel/helper-create-regexp-features-plugin@npm:^7.22.15, @babel/helper-create-regexp-features-plugin@npm:^7.22.5": +"@babel/helper-create-regexp-features-plugin@npm:^7.18.6, @babel/helper-create-regexp-features-plugin@npm:^7.22.5": + version: 7.22.9 + resolution: "@babel/helper-create-regexp-features-plugin@npm:7.22.9" + dependencies: + "@babel/helper-annotate-as-pure": "npm:^7.22.5" + regexpu-core: "npm:^5.3.1" + semver: "npm:^6.3.1" + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 10/6f3475a7661bc34527201c07eeeec3077c8adab0ed74bff728dc479da6c74bb393b6121ddf590ef1671f3f508fab3c7792a5ada65672665d84db4556daebd210 + languageName: node + linkType: hard + +"@babel/helper-create-regexp-features-plugin@npm:^7.22.15": version: 7.22.15 resolution: "@babel/helper-create-regexp-features-plugin@npm:7.22.15" dependencies: @@ -336,6 +368,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-string-parser@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/helper-string-parser@npm:7.22.5" + checksum: 10/7f275a7f1a9504da06afc33441e219796352a4a3d0288a961bc14d1e30e06833a71621b33c3e60ee3ac1ff3c502d55e392bcbc0665f6f9d2629809696fab7cdd + languageName: node + linkType: hard + "@babel/helper-string-parser@npm:^7.23.4": version: 7.23.4 resolution: "@babel/helper-string-parser@npm:7.23.4" @@ -399,6 +438,15 @@ __metadata: languageName: node linkType: hard +"@babel/parser@npm:^7.22.15": + version: 7.23.6 + resolution: "@babel/parser@npm:7.23.6" + bin: + parser: ./bin/babel-parser.js + checksum: 10/6be3a63d3c9d07b035b5a79c022327cb7e16cbd530140ecb731f19a650c794c315a72c699a22413ebeafaff14aa8f53435111898d59e01a393d741b85629fa7d + languageName: node + linkType: hard + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:^7.22.15, @babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:^7.23.3": version: 7.23.3 resolution: "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:7.23.3" @@ -714,13 +762,13 @@ __metadata: linkType: hard "@babel/plugin-syntax-typescript@npm:^7.22.5, @babel/plugin-syntax-typescript@npm:^7.7.2": - version: 7.22.5 - resolution: "@babel/plugin-syntax-typescript@npm:7.22.5" + version: 7.23.3 + resolution: "@babel/plugin-syntax-typescript@npm:7.23.3" dependencies: "@babel/helper-plugin-utils": "npm:^7.22.5" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/8ab7718fbb026d64da93681a57797d60326097fd7cb930380c8bffd9eb101689e90142c760a14b51e8e69c88a73ba3da956cb4520a3b0c65743aee5c71ef360a + checksum: 10/abfad3a19290d258b028e285a1f34c9b8a0cbe46ef79eafed4ed7ffce11b5d0720b5e536c82f91cbd8442cde35a3dd8e861fa70366d87ff06fdc0d4756e30876 languageName: node linkType: hard @@ -1202,6 +1250,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-react-display-name@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/plugin-transform-react-display-name@npm:7.22.5" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/a12bfd1e4e93055efca3ace3c34722571bda59d9740dca364d225d9c6e3ca874f134694d21715c42cc63d79efd46db9665bd4a022998767f9245f1e29d5d204d + languageName: node + linkType: hard + "@babel/plugin-transform-react-display-name@npm:^7.23.3": version: 7.23.3 resolution: "@babel/plugin-transform-react-display-name@npm:7.23.3" @@ -1239,6 +1298,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-react-pure-annotations@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/plugin-transform-react-pure-annotations@npm:7.22.5" + dependencies: + "@babel/helper-annotate-as-pure": "npm:^7.22.5" + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/092021c4f404e267002099ec20b3f12dd730cb90b0d83c5feed3dc00dbe43b9c42c795a18e7c6c7d7bddea20c7dd56221b146aec81b37f2e7eb5137331c61120 + languageName: node + linkType: hard + "@babel/plugin-transform-react-pure-annotations@npm:^7.23.3": version: 7.23.3 resolution: "@babel/plugin-transform-react-pure-annotations@npm:7.23.3" @@ -1607,7 +1678,7 @@ __metadata: languageName: node linkType: hard -"@babel/preset-react@npm:7.23.3, @babel/preset-react@npm:^7.22.5": +"@babel/preset-react@npm:7.23.3": version: 7.23.3 resolution: "@babel/preset-react@npm:7.23.3" dependencies: @@ -1623,6 +1694,22 @@ __metadata: languageName: node linkType: hard +"@babel/preset-react@npm:^7.22.5": + version: 7.22.15 + resolution: "@babel/preset-react@npm:7.22.15" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-validator-option": "npm:^7.22.15" + "@babel/plugin-transform-react-display-name": "npm:^7.22.5" + "@babel/plugin-transform-react-jsx": "npm:^7.22.15" + "@babel/plugin-transform-react-jsx-development": "npm:^7.22.5" + "@babel/plugin-transform-react-pure-annotations": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/f9296e45346c3b6ab8296952edde5f1774cc9fdbdbefbc76047278fc3e889d3e15740f038ce017aca562d89f32fcbb6c11783d464fc6ae3066433178fa58513c + languageName: node + linkType: hard + "@babel/preset-typescript@npm:^7.13.0": version: 7.23.2 resolution: "@babel/preset-typescript@npm:7.23.2" @@ -1679,7 +1766,27 @@ __metadata: languageName: node linkType: hard -"@babel/template@npm:^7.22.15, @babel/template@npm:^7.22.5, @babel/template@npm:^7.23.9, @babel/template@npm:^7.3.3": +"@babel/runtime@npm:^7.19.4": + version: 7.23.8 + resolution: "@babel/runtime@npm:7.23.8" + dependencies: + regenerator-runtime: "npm:^0.14.0" + checksum: 10/ec8f1967a36164da6cac868533ffdff97badd76d23d7d820cc84f0818864accef972f22f9c6a710185db1e3810e353fc18c3da721e5bb3ee8bc61bdbabce03ff + languageName: node + linkType: hard + +"@babel/template@npm:^7.22.15, @babel/template@npm:^7.3.3": + version: 7.22.15 + resolution: "@babel/template@npm:7.22.15" + dependencies: + "@babel/code-frame": "npm:^7.22.13" + "@babel/parser": "npm:^7.22.15" + "@babel/types": "npm:^7.22.15" + checksum: 10/21e768e4eed4d1da2ce5d30aa51db0f4d6d8700bc1821fec6292587df7bba2fe1a96451230de8c64b989740731888ebf1141138bfffb14cacccf4d05c66ad93f + languageName: node + linkType: hard + +"@babel/template@npm:^7.22.5, @babel/template@npm:^7.23.9": version: 7.23.9 resolution: "@babel/template@npm:7.23.9" dependencies: @@ -1708,7 +1815,29 @@ __metadata: languageName: node linkType: hard -"@babel/types@npm:^7.0.0, @babel/types@npm:^7.2.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.22.15, @babel/types@npm:^7.22.19, @babel/types@npm:^7.22.5, @babel/types@npm:^7.23.0, @babel/types@npm:^7.23.6, @babel/types@npm:^7.23.9, @babel/types@npm:^7.3.0, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3": +"@babel/types@npm:^7.0.0, @babel/types@npm:^7.2.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.22.15, @babel/types@npm:^7.22.19, @babel/types@npm:^7.22.5, @babel/types@npm:^7.23.0, @babel/types@npm:^7.3.0, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3": + version: 7.23.0 + resolution: "@babel/types@npm:7.23.0" + dependencies: + "@babel/helper-string-parser": "npm:^7.22.5" + "@babel/helper-validator-identifier": "npm:^7.22.20" + to-fast-properties: "npm:^2.0.0" + checksum: 10/ca5b896a26c91c5672254725c4c892a35567d2122afc47bd5331d1611a7f9230c19fc9ef591a5a6f80bf0d80737e104a9ac205c96447c74bee01d4319db58001 + languageName: node + linkType: hard + +"@babel/types@npm:^7.23.6": + version: 7.23.6 + resolution: "@babel/types@npm:7.23.6" + dependencies: + "@babel/helper-string-parser": "npm:^7.23.4" + "@babel/helper-validator-identifier": "npm:^7.22.20" + to-fast-properties: "npm:^2.0.0" + checksum: 10/07e70bb94d30b0231396b5e9a7726e6d9227a0a62e0a6830c0bd3232f33b024092e3d5a7d1b096a65bbf2bb43a9ab4c721bf618e115bfbb87b454fa060f88cbf + languageName: node + linkType: hard + +"@babel/types@npm:^7.23.9": version: 7.23.9 resolution: "@babel/types@npm:7.23.9" dependencies: @@ -3596,14 +3725,14 @@ __metadata: languageName: unknown linkType: soft -"@grafana/e2e-selectors@npm:10.3.3": - version: 10.3.3 - resolution: "@grafana/e2e-selectors@npm:10.3.3" +"@grafana/e2e-selectors@npm:10.0.2": + version: 10.0.2 + resolution: "@grafana/e2e-selectors@npm:10.0.2" dependencies: "@grafana/tsconfig": "npm:^1.2.0-rc1" - tslib: "npm:2.6.0" - typescript: "npm:5.2.2" - checksum: 10/11fcbf80d61d30a1ab5a99a6c24c5044c187bf6bb52c5d0a1c99b46ed6b28ea5865ff0b9fdfc66c22a744ba5fe9ea2f5030256d952f3b76302cc8cb8ffc01a73 + tslib: "npm:2.5.0" + typescript: "npm:4.8.4" + checksum: 10/8f2ea80ed8408801243b0ea10d504af39f361b6daab98fdc1f1461fc41593e5a026cb055e65a2437bf2f8e8e71150884c5f1173c302c01854e82b8ee17918500 languageName: node linkType: hard @@ -3759,7 +3888,7 @@ __metadata: languageName: node linkType: hard -"@grafana/faro-web-sdk@npm:1.3.8, @grafana/faro-web-sdk@npm:^1.3.6": +"@grafana/faro-web-sdk@npm:1.3.8": version: 1.3.8 resolution: "@grafana/faro-web-sdk@npm:1.3.8" dependencies: @@ -3770,6 +3899,17 @@ __metadata: languageName: node linkType: hard +"@grafana/faro-web-sdk@npm:^1.3.6": + version: 1.3.6 + resolution: "@grafana/faro-web-sdk@npm:1.3.6" + dependencies: + "@grafana/faro-core": "npm:^1.3.6" + ua-parser-js: "npm:^1.0.32" + web-vitals: "npm:^3.1.1" + checksum: 10/08a80e5b0b527a4955e803984d53f53fac6dd090b17a219853222090445e15601971f4b469648c59d0075106f6e8f4ddcabae1b0d3010f80a6d900d825656998 + languageName: node + linkType: hard + "@grafana/flamegraph@workspace:*, @grafana/flamegraph@workspace:packages/grafana-flamegraph": version: 0.0.0-use.local resolution: "@grafana/flamegraph@workspace:packages/grafana-flamegraph" @@ -3903,6 +4043,19 @@ __metadata: languageName: unknown linkType: soft +"@grafana/plugin-e2e@npm:0.18.0": + version: 0.18.0 + resolution: "@grafana/plugin-e2e@npm:0.18.0" + dependencies: + semver: "npm:^7.5.4" + uuid: "npm:^9.0.1" + yaml: "npm:^2.3.4" + peerDependencies: + "@playwright/test": ^1.41.2 + checksum: 10/bb34d74dd802ed7dd80db2de2b8051e8f961a9e0510b3555ee8be6e2c5cc46e3751de1235c4184f7ebbe00cbaa0c280e9013fab5ba167d3f8a8639191b5174ea + languageName: node + linkType: hard + "@grafana/prometheus@workspace:*, @grafana/prometheus@workspace:packages/grafana-prometheus": version: 0.0.0-use.local resolution: "@grafana/prometheus@workspace:packages/grafana-prometheus" @@ -4063,22 +4216,20 @@ __metadata: linkType: soft "@grafana/scenes@npm:^3.5.0": - version: 3.6.1 - resolution: "@grafana/scenes@npm:3.6.1" + version: 3.5.0 + resolution: "@grafana/scenes@npm:3.5.0" dependencies: - "@grafana/e2e-selectors": "npm:10.3.3" + "@grafana/e2e-selectors": "npm:10.0.2" react-grid-layout: "npm:1.3.4" react-use: "npm:17.4.0" react-virtualized-auto-sizer: "npm:1.0.7" uuid: "npm:^9.0.0" peerDependencies: - "@grafana/data": ^10.0.3 - "@grafana/runtime": ^10.0.3 - "@grafana/schema": ^10.0.3 - "@grafana/ui": ^10.0.3 - react: ^18.0.0 - react-dom: ^18.0.0 - checksum: 10/b993f8ad23217fcbadbacc68455c94f7e149cad5d683731f7e7aad531f9d1e81f4399514732ddb607c6e70ce3ec8aeb0a9a070778b4c1d6912fa954df2f308fd + "@grafana/data": 10.0.3 + "@grafana/runtime": 10.0.3 + "@grafana/schema": 10.0.3 + "@grafana/ui": 10.0.3 + checksum: 10/eac51e8bc4327fea39242e1580e4bc174aff984268dcf7b022dfeab5cd53eed63b72d92048e42e24c467ddf55130dbe555f4d4b0bb0a743ace5294ec90978ca5 languageName: node linkType: hard @@ -4452,6 +4603,20 @@ __metadata: languageName: node linkType: hard +"@jest/console@npm:^29.6.4": + version: 29.6.4 + resolution: "@jest/console@npm:29.6.4" + dependencies: + "@jest/types": "npm:^29.6.3" + "@types/node": "npm:*" + chalk: "npm:^4.0.0" + jest-message-util: "npm:^29.6.3" + jest-util: "npm:^29.6.3" + slash: "npm:^3.0.0" + checksum: 10/c482f87a43bb2c48da79c53ad4157542a67c6c90972a8615b852f889da73fd4bcd2a2b85d41b1c867e3df7e7f55e83b4ac91f226511d5645bc20f6498f114c0d + languageName: node + linkType: hard + "@jest/console@npm:^29.7.0": version: 29.7.0 resolution: "@jest/console@npm:29.7.0" @@ -4507,6 +4672,47 @@ __metadata: languageName: node linkType: hard +"@jest/core@npm:^29.6.4": + version: 29.6.4 + resolution: "@jest/core@npm:29.6.4" + dependencies: + "@jest/console": "npm:^29.6.4" + "@jest/reporters": "npm:^29.6.4" + "@jest/test-result": "npm:^29.6.4" + "@jest/transform": "npm:^29.6.4" + "@jest/types": "npm:^29.6.3" + "@types/node": "npm:*" + ansi-escapes: "npm:^4.2.1" + chalk: "npm:^4.0.0" + ci-info: "npm:^3.2.0" + exit: "npm:^0.1.2" + graceful-fs: "npm:^4.2.9" + jest-changed-files: "npm:^29.6.3" + jest-config: "npm:^29.6.4" + jest-haste-map: "npm:^29.6.4" + jest-message-util: "npm:^29.6.3" + jest-regex-util: "npm:^29.6.3" + jest-resolve: "npm:^29.6.4" + jest-resolve-dependencies: "npm:^29.6.4" + jest-runner: "npm:^29.6.4" + jest-runtime: "npm:^29.6.4" + jest-snapshot: "npm:^29.6.4" + jest-util: "npm:^29.6.3" + jest-validate: "npm:^29.6.3" + jest-watcher: "npm:^29.6.4" + micromatch: "npm:^4.0.4" + pretty-format: "npm:^29.6.3" + slash: "npm:^3.0.0" + strip-ansi: "npm:^6.0.0" + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + checksum: 10/a1e47946d715a1735f89fdf9fcf6e99278cef691ac893db4c13e283f753a5c62cec47d68d4f3d7fe823aac0fc603257740f630591e8029846e83f451456a4716 + languageName: node + linkType: hard + "@jest/environment@npm:^29.3.1, @jest/environment@npm:^29.7.0": version: 29.7.0 resolution: "@jest/environment@npm:29.7.0" @@ -4519,6 +4725,27 @@ __metadata: languageName: node linkType: hard +"@jest/environment@npm:^29.6.4": + version: 29.6.4 + resolution: "@jest/environment@npm:29.6.4" + dependencies: + "@jest/fake-timers": "npm:^29.6.4" + "@jest/types": "npm:^29.6.3" + "@types/node": "npm:*" + jest-mock: "npm:^29.6.3" + checksum: 10/8c31ed7f3992f450b994684a2a92d2a023e5e182773e13ea7c627edd25598279d5d1a958bea970e3868bc4d45e20881ddcf9bf9af9425f87d38959141f42693d + languageName: node + linkType: hard + +"@jest/expect-utils@npm:^29.6.4": + version: 29.6.4 + resolution: "@jest/expect-utils@npm:29.6.4" + dependencies: + jest-get-type: "npm:^29.6.3" + checksum: 10/47f17bb3262175600130c698fdaaa680ec7f4612bfdb3f4f9f03e0252c341f31135ae854246f5548453634deef949533aa35b3638cfa776ce5596fd4bd8f1c6e + languageName: node + linkType: hard + "@jest/expect-utils@npm:^29.7.0": version: 29.7.0 resolution: "@jest/expect-utils@npm:29.7.0" @@ -4528,6 +4755,16 @@ __metadata: languageName: node linkType: hard +"@jest/expect@npm:^29.6.4": + version: 29.6.4 + resolution: "@jest/expect@npm:29.6.4" + dependencies: + expect: "npm:^29.6.4" + jest-snapshot: "npm:^29.6.4" + checksum: 10/e6564697562885e2b96294740a507e82e76b9f0af83f6101e2979a8650ec14055452e0ea68c154f92ef93ef83b258f1a45aa39df4e0f472e5ff5c4468de4170f + languageName: node + linkType: hard + "@jest/expect@npm:^29.7.0": version: 29.7.0 resolution: "@jest/expect@npm:29.7.0" @@ -4552,6 +4789,32 @@ __metadata: languageName: node linkType: hard +"@jest/fake-timers@npm:^29.6.4": + version: 29.6.4 + resolution: "@jest/fake-timers@npm:29.6.4" + dependencies: + "@jest/types": "npm:^29.6.3" + "@sinonjs/fake-timers": "npm:^10.0.2" + "@types/node": "npm:*" + jest-message-util: "npm:^29.6.3" + jest-mock: "npm:^29.6.3" + jest-util: "npm:^29.6.3" + checksum: 10/87df3df08111adb51a425edcbd6a454ec5bf1fec582946d04e68616d669fadad64fefee3ddb05b3c91984e47e718917fac907015095c55c2acd58e4f863b0523 + languageName: node + linkType: hard + +"@jest/globals@npm:^29.6.4": + version: 29.6.4 + resolution: "@jest/globals@npm:29.6.4" + dependencies: + "@jest/environment": "npm:^29.6.4" + "@jest/expect": "npm:^29.6.4" + "@jest/types": "npm:^29.6.3" + jest-mock: "npm:^29.6.3" + checksum: 10/a41b18871a248151264668a38b13cb305f03db112bfd89ec44e858af0e79066e0b03d6b68c8baf1ec6c578be6fdb87519389c83438608b91471d17a5724858e0 + languageName: node + linkType: hard + "@jest/globals@npm:^29.7.0": version: 29.7.0 resolution: "@jest/globals@npm:29.7.0" @@ -4564,6 +4827,43 @@ __metadata: languageName: node linkType: hard +"@jest/reporters@npm:^29.6.4": + version: 29.6.4 + resolution: "@jest/reporters@npm:29.6.4" + dependencies: + "@bcoe/v8-coverage": "npm:^0.2.3" + "@jest/console": "npm:^29.6.4" + "@jest/test-result": "npm:^29.6.4" + "@jest/transform": "npm:^29.6.4" + "@jest/types": "npm:^29.6.3" + "@jridgewell/trace-mapping": "npm:^0.3.18" + "@types/node": "npm:*" + chalk: "npm:^4.0.0" + collect-v8-coverage: "npm:^1.0.0" + exit: "npm:^0.1.2" + glob: "npm:^7.1.3" + graceful-fs: "npm:^4.2.9" + istanbul-lib-coverage: "npm:^3.0.0" + istanbul-lib-instrument: "npm:^6.0.0" + istanbul-lib-report: "npm:^3.0.0" + istanbul-lib-source-maps: "npm:^4.0.0" + istanbul-reports: "npm:^3.1.3" + jest-message-util: "npm:^29.6.3" + jest-util: "npm:^29.6.3" + jest-worker: "npm:^29.6.4" + slash: "npm:^3.0.0" + string-length: "npm:^4.0.1" + strip-ansi: "npm:^6.0.0" + v8-to-istanbul: "npm:^9.0.1" + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + checksum: 10/ba6f6d9f621ef80cc8f2fae001f2c9b9e9186f2b570d386b2cfaf298a391332cf65121f72b700e037c9fb7813af4230f821f5f2e0c71c2bd3d301bfa2aa9b935 + languageName: node + linkType: hard + "@jest/reporters@npm:^29.7.0": version: 29.7.0 resolution: "@jest/reporters@npm:29.7.0" @@ -4621,6 +4921,18 @@ __metadata: languageName: node linkType: hard +"@jest/test-result@npm:^29.6.4": + version: 29.6.4 + resolution: "@jest/test-result@npm:29.6.4" + dependencies: + "@jest/console": "npm:^29.6.4" + "@jest/types": "npm:^29.6.3" + "@types/istanbul-lib-coverage": "npm:^2.0.0" + collect-v8-coverage: "npm:^1.0.0" + checksum: 10/d0dd2beaa4e9f3e7cebec44de1ab09a1020e77e4742c018f91bf281d49b824875a6d8f86408fdde195f53dd1ba8b7943b2843a9f002c5b0286f8933c21e65527 + languageName: node + linkType: hard + "@jest/test-result@npm:^29.7.0": version: 29.7.0 resolution: "@jest/test-result@npm:29.7.0" @@ -4633,6 +4945,18 @@ __metadata: languageName: node linkType: hard +"@jest/test-sequencer@npm:^29.6.4": + version: 29.6.4 + resolution: "@jest/test-sequencer@npm:29.6.4" + dependencies: + "@jest/test-result": "npm:^29.6.4" + graceful-fs: "npm:^4.2.9" + jest-haste-map: "npm:^29.6.4" + slash: "npm:^3.0.0" + checksum: 10/f0f09dc5c1cc190ae8944531072b598746c725c81d2810f47d4459a2d3113b3658469ff4e59cd028b221e4f2d015e0b968940e9ae08d800b2e8911590fbc184e + languageName: node + linkType: hard + "@jest/test-sequencer@npm:^29.7.0": version: 29.7.0 resolution: "@jest/test-sequencer@npm:29.7.0" @@ -4668,6 +4992,29 @@ __metadata: languageName: node linkType: hard +"@jest/transform@npm:^29.6.4": + version: 29.6.4 + resolution: "@jest/transform@npm:29.6.4" + dependencies: + "@babel/core": "npm:^7.11.6" + "@jest/types": "npm:^29.6.3" + "@jridgewell/trace-mapping": "npm:^0.3.18" + babel-plugin-istanbul: "npm:^6.1.1" + chalk: "npm:^4.0.0" + convert-source-map: "npm:^2.0.0" + fast-json-stable-stringify: "npm:^2.1.0" + graceful-fs: "npm:^4.2.9" + jest-haste-map: "npm:^29.6.4" + jest-regex-util: "npm:^29.6.3" + jest-util: "npm:^29.6.3" + micromatch: "npm:^4.0.4" + pirates: "npm:^4.0.4" + slash: "npm:^3.0.0" + write-file-atomic: "npm:^4.0.2" + checksum: 10/cba6b5a59de89b4446c0c0dc6ca08ea52fb1308caeaa72c70a232a1b81d178cad1a2fcce4e25db95d7623c03c34a244bdfe6769cc6ac71b8c3fcd2838bab583e + languageName: node + linkType: hard + "@jest/types@npm:^26.6.2": version: 26.6.2 resolution: "@jest/types@npm:26.6.2" @@ -4716,6 +5063,13 @@ __metadata: languageName: node linkType: hard +"@jridgewell/resolve-uri@npm:3.1.0": + version: 3.1.0 + resolution: "@jridgewell/resolve-uri@npm:3.1.0" + checksum: 10/320ceb37af56953757b28e5b90c34556157676d41e3d0a3ff88769274d62373582bb0f0276a4f2d29c3f4fdd55b82b8be5731f52d391ad2ecae9b321ee1c742d + languageName: node + linkType: hard + "@jridgewell/resolve-uri@npm:^3.0.3, @jridgewell/resolve-uri@npm:^3.1.0": version: 3.1.1 resolution: "@jridgewell/resolve-uri@npm:3.1.1" @@ -4730,6 +5084,16 @@ __metadata: 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": "npm:^0.3.0" + "@jridgewell/trace-mapping": "npm:^0.3.9" + checksum: 10/1aaa42075bac32a551708025da0c07b11c11fb05ccd10fb70df2cb0db88773338ab0f33f175d9865379cb855bb3b1cda478367747a1087309fda40a7b9214bfa + languageName: node + linkType: hard + "@jridgewell/source-map@npm:^0.3.3": version: 0.3.5 resolution: "@jridgewell/source-map@npm:0.3.5" @@ -4740,6 +5104,13 @@ __metadata: languageName: node linkType: hard +"@jridgewell/sourcemap-codec@npm:1.4.14": + version: 1.4.14 + resolution: "@jridgewell/sourcemap-codec@npm:1.4.14" + checksum: 10/26e768fae6045481a983e48aa23d8fcd23af5da70ebd74b0649000e815e7fbb01ea2bc088c9176b3fffeb9bec02184e58f46125ef3320b30eaa1f4094cfefa38 + languageName: node + linkType: hard + "@jridgewell/sourcemap-codec@npm:^1.4.10, @jridgewell/sourcemap-codec@npm:^1.4.14, @jridgewell/sourcemap-codec@npm:^1.4.15": version: 1.4.15 resolution: "@jridgewell/sourcemap-codec@npm:1.4.15" @@ -4757,7 +5128,27 @@ __metadata: languageName: node linkType: hard -"@jridgewell/trace-mapping@npm:^0.3.12, @jridgewell/trace-mapping@npm:^0.3.17, @jridgewell/trace-mapping@npm:^0.3.18, @jridgewell/trace-mapping@npm:^0.3.20, @jridgewell/trace-mapping@npm:^0.3.21, @jridgewell/trace-mapping@npm:^0.3.9": +"@jridgewell/trace-mapping@npm:^0.3.12, @jridgewell/trace-mapping@npm:^0.3.17, @jridgewell/trace-mapping@npm:^0.3.18, @jridgewell/trace-mapping@npm:^0.3.9": + version: 0.3.18 + resolution: "@jridgewell/trace-mapping@npm:0.3.18" + dependencies: + "@jridgewell/resolve-uri": "npm:3.1.0" + "@jridgewell/sourcemap-codec": "npm:1.4.14" + checksum: 10/f4fabdddf82398a797bcdbb51c574cd69b383db041a6cae1a6a91478681d6aab340c01af655cfd8c6e01cde97f63436a1445f08297cdd33587621cf05ffa0d55 + languageName: node + linkType: hard + +"@jridgewell/trace-mapping@npm:^0.3.20": + version: 0.3.21 + resolution: "@jridgewell/trace-mapping@npm:0.3.21" + dependencies: + "@jridgewell/resolve-uri": "npm:^3.1.0" + "@jridgewell/sourcemap-codec": "npm:^1.4.14" + checksum: 10/925dda0620887e5a24f11b5a3a106f4e8b1a66155b49be6ceee61432174df33a17c243d8a89b2cd79ccebd281d817878759236a2fc42c47325ae9f73dfbfb90d + languageName: node + linkType: hard + +"@jridgewell/trace-mapping@npm:^0.3.21": version: 0.3.22 resolution: "@jridgewell/trace-mapping@npm:0.3.22" dependencies: @@ -5972,6 +6363,17 @@ __metadata: languageName: node linkType: hard +"@playwright/test@npm:^1.41.2": + version: 1.41.2 + resolution: "@playwright/test@npm:1.41.2" + dependencies: + playwright: "npm:1.41.2" + bin: + playwright: cli.js + checksum: 10/e87405987fa024f75acc223c47fcb2da0a66b2fa0cd9a583ca5b02aac12be353d0c262bf6a22b9bc40550c86c8b7629e70cd27f508ec370d9c92bb72f74581e7 + languageName: node + linkType: hard + "@pmmmwh/react-refresh-webpack-plugin@npm:0.5.11, @pmmmwh/react-refresh-webpack-plugin@npm:^0.5.5": version: 0.5.11 resolution: "@pmmmwh/react-refresh-webpack-plugin@npm:0.5.11" @@ -6980,10 +7382,17 @@ __metadata: languageName: node linkType: hard -"@remix-run/router@npm:1.14.2, @remix-run/router@npm:^1.5.0": - version: 1.14.2 - resolution: "@remix-run/router@npm:1.14.2" - checksum: 10/422844e88b985f1e287301b302c6cf8169c9eea792f80d40464f97b25393bb2e697228ebd7a7b61444d5a51c5873c4a637aad20acde5886a5caf62e833c5ceee +"@remix-run/router@npm:1.5.0": + version: 1.5.0 + resolution: "@remix-run/router@npm:1.5.0" + checksum: 10/3da27d64519df94919020f4b42aaa016f26891331be98468cbc807bc4d2cfb401d7e47d4f88a4a3d777fc3af23d162c668357a8e5d2c5947acdbca7b691bc325 + languageName: node + linkType: hard + +"@remix-run/router@npm:^1.5.0": + version: 1.11.0 + resolution: "@remix-run/router@npm:1.11.0" + checksum: 10/629ec578b9dfd3c5cb5de64a0798dd7846ec5ba0351aa66f42b1c65efb43da8f30366be59b825303648965b0df55b638c110949b24ef94fd62e98117fdfb0c0f languageName: node linkType: hard @@ -8350,6 +8759,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-darwin-arm64@npm:1.3.90": + version: 1.3.90 + resolution: "@swc/core-darwin-arm64@npm:1.3.90" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + "@swc/core-darwin-arm64@npm:1.4.0": version: 1.4.0 resolution: "@swc/core-darwin-arm64@npm:1.4.0" @@ -8364,6 +8780,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-darwin-x64@npm:1.3.90": + version: 1.3.90 + resolution: "@swc/core-darwin-x64@npm:1.3.90" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + "@swc/core-darwin-x64@npm:1.4.0": version: 1.4.0 resolution: "@swc/core-darwin-x64@npm:1.4.0" @@ -8378,6 +8801,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-linux-arm-gnueabihf@npm:1.3.90": + version: 1.3.90 + resolution: "@swc/core-linux-arm-gnueabihf@npm:1.3.90" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + "@swc/core-linux-arm-gnueabihf@npm:1.4.0": version: 1.4.0 resolution: "@swc/core-linux-arm-gnueabihf@npm:1.4.0" @@ -8392,6 +8822,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-linux-arm64-gnu@npm:1.3.90": + version: 1.3.90 + resolution: "@swc/core-linux-arm64-gnu@npm:1.3.90" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + "@swc/core-linux-arm64-gnu@npm:1.4.0": version: 1.4.0 resolution: "@swc/core-linux-arm64-gnu@npm:1.4.0" @@ -8406,6 +8843,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-linux-arm64-musl@npm:1.3.90": + version: 1.3.90 + resolution: "@swc/core-linux-arm64-musl@npm:1.3.90" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + "@swc/core-linux-arm64-musl@npm:1.4.0": version: 1.4.0 resolution: "@swc/core-linux-arm64-musl@npm:1.4.0" @@ -8420,6 +8864,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-linux-x64-gnu@npm:1.3.90": + version: 1.3.90 + resolution: "@swc/core-linux-x64-gnu@npm:1.3.90" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + "@swc/core-linux-x64-gnu@npm:1.4.0": version: 1.4.0 resolution: "@swc/core-linux-x64-gnu@npm:1.4.0" @@ -8434,6 +8885,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-linux-x64-musl@npm:1.3.90": + version: 1.3.90 + resolution: "@swc/core-linux-x64-musl@npm:1.3.90" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + "@swc/core-linux-x64-musl@npm:1.4.0": version: 1.4.0 resolution: "@swc/core-linux-x64-musl@npm:1.4.0" @@ -8448,6 +8906,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-win32-arm64-msvc@npm:1.3.90": + version: 1.3.90 + resolution: "@swc/core-win32-arm64-msvc@npm:1.3.90" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + "@swc/core-win32-arm64-msvc@npm:1.4.0": version: 1.4.0 resolution: "@swc/core-win32-arm64-msvc@npm:1.4.0" @@ -8462,6 +8927,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-win32-ia32-msvc@npm:1.3.90": + version: 1.3.90 + resolution: "@swc/core-win32-ia32-msvc@npm:1.3.90" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + "@swc/core-win32-ia32-msvc@npm:1.4.0": version: 1.4.0 resolution: "@swc/core-win32-ia32-msvc@npm:1.4.0" @@ -8476,6 +8948,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-win32-x64-msvc@npm:1.3.90": + version: 1.3.90 + resolution: "@swc/core-win32-x64-msvc@npm:1.3.90" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@swc/core-win32-x64-msvc@npm:1.4.0": version: 1.4.0 resolution: "@swc/core-win32-x64-msvc@npm:1.4.0" @@ -8536,7 +9015,7 @@ __metadata: languageName: node linkType: hard -"@swc/core@npm:1.4.1, @swc/core@npm:^1.3.49": +"@swc/core@npm:1.4.1": version: 1.4.1 resolution: "@swc/core@npm:1.4.1" dependencies: @@ -8582,6 +9061,52 @@ __metadata: languageName: node linkType: hard +"@swc/core@npm:^1.3.49": + version: 1.3.90 + resolution: "@swc/core@npm:1.3.90" + dependencies: + "@swc/core-darwin-arm64": "npm:1.3.90" + "@swc/core-darwin-x64": "npm:1.3.90" + "@swc/core-linux-arm-gnueabihf": "npm:1.3.90" + "@swc/core-linux-arm64-gnu": "npm:1.3.90" + "@swc/core-linux-arm64-musl": "npm:1.3.90" + "@swc/core-linux-x64-gnu": "npm:1.3.90" + "@swc/core-linux-x64-musl": "npm:1.3.90" + "@swc/core-win32-arm64-msvc": "npm:1.3.90" + "@swc/core-win32-ia32-msvc": "npm:1.3.90" + "@swc/core-win32-x64-msvc": "npm:1.3.90" + "@swc/counter": "npm:^0.1.1" + "@swc/types": "npm:^0.1.5" + peerDependencies: + "@swc/helpers": ^0.5.0 + dependenciesMeta: + "@swc/core-darwin-arm64": + optional: true + "@swc/core-darwin-x64": + optional: true + "@swc/core-linux-arm-gnueabihf": + optional: true + "@swc/core-linux-arm64-gnu": + optional: true + "@swc/core-linux-arm64-musl": + optional: true + "@swc/core-linux-x64-gnu": + optional: true + "@swc/core-linux-x64-musl": + optional: true + "@swc/core-win32-arm64-msvc": + optional: true + "@swc/core-win32-ia32-msvc": + optional: true + "@swc/core-win32-x64-msvc": + optional: true + peerDependenciesMeta: + "@swc/helpers": + optional: true + checksum: 10/214af37af77b968203d495745a86db985734527f4696243bda5fb9ce868830d70e7a2cdbb268da2ee994d9fcedded25073d7b709fa09b75e96f9ba7d13a63da0 + languageName: node + linkType: hard + "@swc/counter@npm:^0.1.1, @swc/counter@npm:^0.1.2, @swc/counter@npm:^0.1.3": version: 0.1.3 resolution: "@swc/counter@npm:0.1.3" @@ -8589,7 +9114,7 @@ __metadata: languageName: node linkType: hard -"@swc/helpers@npm:0.5.6, @swc/helpers@npm:^0.5.0": +"@swc/helpers@npm:0.5.6": version: 0.5.6 resolution: "@swc/helpers@npm:0.5.6" dependencies: @@ -8598,6 +9123,15 @@ __metadata: languageName: node linkType: hard +"@swc/helpers@npm:^0.5.0": + version: 0.5.1 + resolution: "@swc/helpers@npm:0.5.1" + dependencies: + tslib: "npm:^2.4.0" + checksum: 10/4954c4d2dd97bf965e863a10ffa44c3fdaf7653f2fa9ef1a6cf7ffffd67f3f832216588f9751afd75fdeaea60c4688c75c01e2405119c448f1a109c9a7958c54 + languageName: node + linkType: hard + "@swc/types@npm:^0.1.5": version: 0.1.5 resolution: "@swc/types@npm:0.1.5" @@ -8621,7 +9155,7 @@ __metadata: languageName: node linkType: hard -"@testing-library/jest-dom@npm:6.4.2, @testing-library/jest-dom@npm:^6.1.2": +"@testing-library/jest-dom@npm:6.4.2": version: 6.4.2 resolution: "@testing-library/jest-dom@npm:6.4.2" dependencies: @@ -8654,6 +9188,36 @@ __metadata: languageName: node linkType: hard +"@testing-library/jest-dom@npm:^6.1.2": + version: 6.1.2 + resolution: "@testing-library/jest-dom@npm:6.1.2" + dependencies: + "@adobe/css-tools": "npm:^4.3.0" + "@babel/runtime": "npm:^7.9.2" + aria-query: "npm:^5.0.0" + chalk: "npm:^3.0.0" + css.escape: "npm:^1.5.1" + dom-accessibility-api: "npm:^0.5.6" + lodash: "npm:^4.17.15" + redent: "npm:^3.0.0" + peerDependencies: + "@jest/globals": ">= 28" + "@types/jest": ">= 28" + jest: ">= 28" + vitest: ">= 0.32" + peerDependenciesMeta: + "@jest/globals": + optional: true + "@types/jest": + optional: true + jest: + optional: true + vitest: + optional: true + checksum: 10/36e27f9011dd60de8936d3edb2a81753ef7a370480019e571c6e85b935b5fa2aba47244104badf6d6b636fd170fdee5c0fdd92fb3630957d4a6f11d302b57398 + languageName: node + linkType: hard + "@testing-library/react-hooks@npm:^8.0.1": version: 8.0.1 resolution: "@testing-library/react-hooks@npm:8.0.1" @@ -8866,9 +9430,9 @@ __metadata: linkType: hard "@types/chance@npm:^1.1.3": - version: 1.1.6 - resolution: "@types/chance@npm:1.1.6" - checksum: 10/f4366f1b3144d143af3e6f0fad2ed1db7b9bdfa7d82d40944e9619d57fe7e6b60e8c1452f47a8ededa6b2188932879518628ecd9aac81c40384ded39c26338ba + version: 1.1.3 + resolution: "@types/chance@npm:1.1.3" + checksum: 10/5fffaa9f1a54690cb1236aeda31fdc833e3c5e6049c1e70e6d94c6b41d8f590088bf61a8a45934d1ea9cfde001ec90857363bd35b1775b523098d51da2e444bc languageName: node linkType: hard @@ -8882,9 +9446,9 @@ __metadata: linkType: hard "@types/common-tags@npm:^1.8.0": - version: 1.8.4 - resolution: "@types/common-tags@npm:1.8.4" - checksum: 10/40c95a2f6388beb1cdeed3c9986ac0d6a3a551fce706e3e364a00ded48ab624b06b1ac8b94679bb2da9653e5eb3e450bad26873f5189993a5d8e8bdace74cbb2 + version: 1.8.1 + resolution: "@types/common-tags@npm:1.8.1" + checksum: 10/0800298cca3be151b26cedbb8d69d686140f98fd68decb4069c29611ccb7c0572d1e8ffca55e5a6dd7615a15c897337fe1c983d36d92a4a50b5664a2b03ed01b languageName: node linkType: hard @@ -9026,9 +9590,9 @@ __metadata: linkType: hard "@types/d3-force@npm:*, @types/d3-force@npm:^3.0.0": - version: 3.0.9 - resolution: "@types/d3-force@npm:3.0.9" - checksum: 10/e7f2260b9d57d0623f24b2876a10f6e4f7b9690035a5d9e776513b5f9261d0d1a8c436b0b977445f1753686b88ce1cecf3dd4538907da833c6ddc03cfbf45936 + version: 3.0.4 + resolution: "@types/d3-force@npm:3.0.4" + checksum: 10/99d7513a71ec35e6dd1285b1acadeadd9f104055e0ae1f7e8a0aee599566e5c83e1f7afdad2f50f0e83b90af1d171d3b81edc3397c081f88856791dad0aa00c1 languageName: node linkType: hard @@ -9258,9 +9822,9 @@ __metadata: linkType: hard "@types/diff@npm:^5": - version: 5.0.9 - resolution: "@types/diff@npm:5.0.9" - checksum: 10/6924740cb67a49771ea3753ee9b15c676860a6227b2bf0200ed9cef4111ff0f59fec8c51c1170bd30a8c7370b32673b308a9cd2da28525130f842194a822ef42 + version: 5.0.5 + resolution: "@types/diff@npm:5.0.5" + checksum: 10/d3b2f90dcc511a2034ec0eb76f24b36ae8dca93e629742d72af01a9b9e38ec627207ca93e8601608698c6b6126a73247b340e6ef9c285e88eb0cca380a67ac5d languageName: node linkType: hard @@ -9454,7 +10018,7 @@ __metadata: languageName: node linkType: hard -"@types/hoist-non-react-statics@npm:3.3.5, @types/hoist-non-react-statics@npm:^3.3.0, @types/hoist-non-react-statics@npm:^3.3.1": +"@types/hoist-non-react-statics@npm:3.3.5, @types/hoist-non-react-statics@npm:^3.3.0": version: 3.3.5 resolution: "@types/hoist-non-react-statics@npm:3.3.5" dependencies: @@ -9464,6 +10028,16 @@ __metadata: languageName: node linkType: hard +"@types/hoist-non-react-statics@npm:^3.3.1": + version: 3.3.4 + resolution: "@types/hoist-non-react-statics@npm:3.3.4" + dependencies: + "@types/react": "npm:*" + hoist-non-react-statics: "npm:^3.3.0" + checksum: 10/dee430941a9ea16b7f665ecafa9b134066a49d13ae497fc051cf5d41b3aead394ab1a8179c3c98c9a3584f80aed16fab82dd7979c7dcddfbb5f74a132575d362 + languageName: node + linkType: hard + "@types/html-minifier-terser@npm:^6.0.0": version: 6.0.0 resolution: "@types/html-minifier-terser@npm:6.0.0" @@ -9519,13 +10093,13 @@ __metadata: languageName: node linkType: hard -"@types/jest@npm:*, @types/jest@npm:29.5.12, @types/jest@npm:^29.5.4": - version: 29.5.12 - resolution: "@types/jest@npm:29.5.12" +"@types/jest@npm:*, @types/jest@npm:^29.5.4": + version: 29.5.4 + resolution: "@types/jest@npm:29.5.4" dependencies: expect: "npm:^29.0.0" pretty-format: "npm:^29.0.0" - checksum: 10/312e8dcf92cdd5a5847d6426f0940829bca6fe6b5a917248f3d7f7ef5d85c9ce78ef05e47d2bbabc40d41a930e0e36db2d443d2610a9e3db9062da2d5c904211 + checksum: 10/c56081b958c06f4f3a30f7beabf4e94e70db96a4b41b8a73549fea7f9bf0a8c124ab3998ea4e6d040d1b8c95cfbe0b8d4a607da4bdea03c9e116f92c147df193 languageName: node linkType: hard @@ -9539,6 +10113,16 @@ __metadata: languageName: node linkType: hard +"@types/jest@npm:29.5.12": + version: 29.5.12 + resolution: "@types/jest@npm:29.5.12" + dependencies: + expect: "npm:^29.0.0" + pretty-format: "npm:^29.0.0" + checksum: 10/312e8dcf92cdd5a5847d6426f0940829bca6fe6b5a917248f3d7f7ef5d85c9ce78ef05e47d2bbabc40d41a930e0e36db2d443d2610a9e3db9062da2d5c904211 + languageName: node + linkType: hard + "@types/jquery@npm:3.5.29": version: 3.5.29 resolution: "@types/jquery@npm:3.5.29" @@ -9563,9 +10147,9 @@ __metadata: linkType: hard "@types/js-yaml@npm:^4.0.5": - version: 4.0.9 - resolution: "@types/js-yaml@npm:4.0.9" - checksum: 10/a0ce595db8a987904badd21fc50f9f444cb73069f4b95a76cc222e0a17b3ff180669059c763ec314bc4c3ce284379177a9da80e83c5f650c6c1310cafbfaa8e6 + version: 4.0.5 + resolution: "@types/js-yaml@npm:4.0.5" + checksum: 10/6fff5f47d97070f1a01022517ce4bd81a0cfac7cd30f9dbc7222dc5f8db4bfe5f5c8cba3f4b02bdbd6f31f691050db97395b33c8df66d1e7c4f66096b41a3df6 languageName: node linkType: hard @@ -9580,7 +10164,14 @@ __metadata: languageName: node linkType: hard -"@types/json-schema@npm:*, @types/json-schema@npm:^7.0.12, @types/json-schema@npm:^7.0.8, @types/json-schema@npm:^7.0.9": +"@types/json-schema@npm:*, @types/json-schema@npm:^7.0.8, @types/json-schema@npm:^7.0.9": + version: 7.0.9 + resolution: "@types/json-schema@npm:7.0.9" + checksum: 10/7ceb41e396240aa69ae15c02ffbb6548ea2bb2f845a7378c711c7c908a9a8438a0330f3135f1ccb6e82e334b9e2ec5b94fb57a1435f2b15362d38e9d5109e5ea + languageName: node + linkType: hard + +"@types/json-schema@npm:^7.0.12": version: 7.0.15 resolution: "@types/json-schema@npm:7.0.15" checksum: 10/1a3c3e06236e4c4aab89499c428d585527ce50c24fe8259e8b3926d3df4cfbbbcf306cfc73ddfb66cbafc973116efd15967020b0f738f63e09e64c7d260519e7 @@ -9625,18 +10216,18 @@ __metadata: linkType: hard "@types/logfmt@npm:^1.2.3": - version: 1.2.6 - resolution: "@types/logfmt@npm:1.2.6" + version: 1.2.3 + resolution: "@types/logfmt@npm:1.2.3" dependencies: "@types/node": "npm:*" - checksum: 10/ac69ee5c99e074bf3ad31d27f877402b84be59e2c200fc4ecfbf295244505a2b6408db1c377c96f90d0444a18fd253d34f0f0810c162e73f6e82c327022c3008 + checksum: 10/d5872ab0432c687dc95a4c3a1c21c8eca24553415ef6a34f6cbbe0eefc4b7b8fb8b2af80df4a53fcf7cc7b212569df568bed1b17f7c2a976c4416f4a67b285de languageName: node linkType: hard "@types/lucene@npm:^2": - version: 2.1.7 - resolution: "@types/lucene@npm:2.1.7" - checksum: 10/38bf8521a071697612f3629839f5d55558ae40ebba1486300f35b862d1b403758a0c5e9a8370bc1baae11d1779e6b1dae28c82f528ec8064dcee33dca008c910 + version: 2.1.4 + resolution: "@types/lucene@npm:2.1.4" + checksum: 10/418057a390752b36745428887ef527121740d54137a2b2da9f10388d2e9d1fe13d1d04b9b2605101bdd99a38ae357d1d5b08f6302f2eca7cead4e28f30ec964d languageName: node linkType: hard @@ -9738,12 +10329,12 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:*, @types/node@npm:20.11.19, @types/node@npm:>=13.7.0, @types/node@npm:^20.11.16": - version: 20.11.19 - resolution: "@types/node@npm:20.11.19" +"@types/node@npm:*, @types/node@npm:>=13.7.0": + version: 20.8.10 + resolution: "@types/node@npm:20.8.10" dependencies: undici-types: "npm:~5.26.4" - checksum: 10/c7f4705d6c84aa21679ad180c33c13ca9567f650e66e14bcee77c7c43d14619c7cd3b4d7b2458947143030b7b1930180efa6d12d999b45366abff9fed7a17472 + checksum: 10/8930039077c8ad74de74c724909412bea8110c3f8892bcef8dda3e9629073bed65632ee755f94b252bcdae8ca71cf83e89a4a440a105e2b1b7c9797b43483049 languageName: node linkType: hard @@ -9754,6 +10345,15 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:20.11.19": + version: 20.11.19 + resolution: "@types/node@npm:20.11.19" + dependencies: + undici-types: "npm:~5.26.4" + checksum: 10/c7f4705d6c84aa21679ad180c33c13ca9567f650e66e14bcee77c7c43d14619c7cd3b4d7b2458947143030b7b1930180efa6d12d999b45366abff9fed7a17472 + languageName: node + linkType: hard + "@types/node@npm:^14.14.31": version: 14.18.36 resolution: "@types/node@npm:14.18.36" @@ -9768,6 +10368,15 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:^20.11.16": + version: 20.11.18 + resolution: "@types/node@npm:20.11.18" + dependencies: + undici-types: "npm:~5.26.4" + checksum: 10/eeaa55032e6702867e96d7b6f98df1d60af09d37ab72f2b905b349ec7e458dfb9c4d9cfc562962f5a51b156a968eea773d8025688f88b735944c81e3ac0e3b7f + languageName: node + linkType: hard + "@types/normalize-package-data@npm:^2.4.0": version: 2.4.1 resolution: "@types/normalize-package-data@npm:2.4.1" @@ -9884,7 +10493,16 @@ __metadata: languageName: node linkType: hard -"@types/react-dom@npm:*, @types/react-dom@npm:18.2.19, @types/react-dom@npm:^18.0.0": +"@types/react-dom@npm:*, @types/react-dom@npm:^18.0.0": + version: 18.2.7 + resolution: "@types/react-dom@npm:18.2.7" + dependencies: + "@types/react": "npm:*" + checksum: 10/9b70ef66cbe2d2898ea37eb79ee3697e0e4ad3d950e769a601f79be94097d43b8ef45b98a0b29528203c7d731c81666f637b2b7032deeced99214b4bc0662614 + languageName: node + linkType: hard + +"@types/react-dom@npm:18.2.19": version: 18.2.19 resolution: "@types/react-dom@npm:18.2.19" dependencies: @@ -9971,7 +10589,7 @@ __metadata: languageName: node linkType: hard -"@types/react-transition-group@npm:4.4.10, @types/react-transition-group@npm:^4.4.0": +"@types/react-transition-group@npm:4.4.10": version: 4.4.10 resolution: "@types/react-transition-group@npm:4.4.10" dependencies: @@ -9980,6 +10598,15 @@ __metadata: languageName: node linkType: hard +"@types/react-transition-group@npm:^4.4.0": + version: 4.4.6 + resolution: "@types/react-transition-group@npm:4.4.6" + dependencies: + "@types/react": "npm:*" + checksum: 10/eb4a14df7ad283be56d44c4bd4351136bd50dfedf6958299fbbc571d6871fad17a373b5b9a6d44adac27154d1f2059225a26c4fee79053349a4d52eb89277787 + languageName: node + linkType: hard + "@types/react-virtualized-auto-sizer@npm:1.0.4": version: 1.0.4 resolution: "@types/react-virtualized-auto-sizer@npm:1.0.4" @@ -9990,12 +10617,12 @@ __metadata: linkType: hard "@types/react-window-infinite-loader@npm:^1": - version: 1.0.9 - resolution: "@types/react-window-infinite-loader@npm:1.0.9" + version: 1.0.6 + resolution: "@types/react-window-infinite-loader@npm:1.0.6" dependencies: "@types/react": "npm:*" "@types/react-window": "npm:*" - checksum: 10/9f2c27f24bfa726ceaef6612a4adbda745f3455c877193f68dfa48591274c670a6df4fa6870785cff5f948e289ceb9a247fb7cbf67e3cd555ab16d11866fd63f + checksum: 10/d4648dfb44614e4f0137d7b77eb1868b0c5252f451a78edfc4520e508157ce7687d4b7d9efd6df8f01e72e0d92224338b8c8d934220f32a3081b528599a25829 languageName: node linkType: hard @@ -10069,13 +10696,20 @@ __metadata: languageName: node linkType: hard -"@types/semver@npm:7.5.7, @types/semver@npm:^7.3.12, @types/semver@npm:^7.3.4, @types/semver@npm:^7.5.0": +"@types/semver@npm:7.5.7": version: 7.5.7 resolution: "@types/semver@npm:7.5.7" checksum: 10/535d88ec577fe59e38211881f79a1e2ba391e9e1516f8fff74e7196a5ba54315bace9c67a4616c334c830c89027d70a9f473a4ceb634526086a9da39180f2f9a languageName: node linkType: hard +"@types/semver@npm:^7.3.12, @types/semver@npm:^7.3.4, @types/semver@npm:^7.5.0": + version: 7.5.6 + resolution: "@types/semver@npm:7.5.6" + checksum: 10/e77282b17f74354e17e771c0035cccb54b94cc53d0433fa7e9ba9d23fd5d7edcd14b6c8b7327d58bbd89e83b1c5eda71dfe408e06b929007e2b89586e9b63459 + languageName: node + linkType: hard + "@types/serve-index@npm:^1.9.4": version: 1.9.4 resolution: "@types/serve-index@npm:1.9.4" @@ -10412,13 +11046,13 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/scope-manager@npm:5.62.0": - version: 5.62.0 - resolution: "@typescript-eslint/scope-manager@npm:5.62.0" +"@typescript-eslint/scope-manager@npm:5.59.9": + version: 5.59.9 + resolution: "@typescript-eslint/scope-manager@npm:5.59.9" dependencies: - "@typescript-eslint/types": "npm:5.62.0" - "@typescript-eslint/visitor-keys": "npm:5.62.0" - checksum: 10/e827770baa202223bc0387e2fd24f630690809e460435b7dc9af336c77322290a770d62bd5284260fa881c86074d6a9fd6c97b07382520b115f6786b8ed499da + "@typescript-eslint/types": "npm:5.59.9" + "@typescript-eslint/visitor-keys": "npm:5.59.9" + checksum: 10/83b538212fc422cd6a26eee49deab60a29fa6d8bbd0dffca6daa02318959c76ddf1dc00db9ce0236258f26c1f726be78a25d2f6c5603233f591716d6299480e5 languageName: node linkType: hard @@ -10476,10 +11110,10 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/types@npm:5.62.0": - version: 5.62.0 - resolution: "@typescript-eslint/types@npm:5.62.0" - checksum: 10/24e8443177be84823242d6729d56af2c4b47bfc664dd411a1d730506abf2150d6c31bdefbbc6d97c8f91043e3a50e0c698239dcb145b79bb6b0c34469aaf6c45 +"@typescript-eslint/types@npm:5.59.9": + version: 5.59.9 + resolution: "@typescript-eslint/types@npm:5.59.9" + checksum: 10/49226e5384ac801db245fe668b4bd7610a11c5ade9c05ee93767fd188462c4d25755b8592f21210cc9856fae3c5566d4811ed0f7fefe30e48e5823e71ab4623e languageName: node linkType: hard @@ -10497,12 +11131,12 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/typescript-estree@npm:5.62.0": - version: 5.62.0 - resolution: "@typescript-eslint/typescript-estree@npm:5.62.0" +"@typescript-eslint/typescript-estree@npm:5.59.9": + version: 5.59.9 + resolution: "@typescript-eslint/typescript-estree@npm:5.59.9" dependencies: - "@typescript-eslint/types": "npm:5.62.0" - "@typescript-eslint/visitor-keys": "npm:5.62.0" + "@typescript-eslint/types": "npm:5.59.9" + "@typescript-eslint/visitor-keys": "npm:5.59.9" debug: "npm:^4.3.4" globby: "npm:^11.1.0" is-glob: "npm:^4.0.3" @@ -10511,7 +11145,7 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 10/06c975eb5f44b43bd19fadc2e1023c50cf87038fe4c0dd989d4331c67b3ff509b17fa60a3251896668ab4d7322bdc56162a9926971218d2e1a1874d2bef9a52e + checksum: 10/79cf330815244f2ab12762df9296812c20f3ff859f14dc997a79ce09eabd7c8d0d190ed00fcdf380288a2b4035ca40c9f0002dc9c6c2875885ad3b94c2eab58b languageName: node linkType: hard @@ -10588,30 +11222,30 @@ __metadata: linkType: hard "@typescript-eslint/utils@npm:^5.10.0": - version: 5.62.0 - resolution: "@typescript-eslint/utils@npm:5.62.0" + version: 5.59.9 + resolution: "@typescript-eslint/utils@npm:5.59.9" dependencies: "@eslint-community/eslint-utils": "npm:^4.2.0" "@types/json-schema": "npm:^7.0.9" "@types/semver": "npm:^7.3.12" - "@typescript-eslint/scope-manager": "npm:5.62.0" - "@typescript-eslint/types": "npm:5.62.0" - "@typescript-eslint/typescript-estree": "npm:5.62.0" + "@typescript-eslint/scope-manager": "npm:5.59.9" + "@typescript-eslint/types": "npm:5.59.9" + "@typescript-eslint/typescript-estree": "npm:5.59.9" eslint-scope: "npm:^5.1.1" semver: "npm:^7.3.7" peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - checksum: 10/15ef13e43998a082b15f85db979f8d3ceb1f9ce4467b8016c267b1738d5e7cdb12aa90faf4b4e6dd6486c236cf9d33c463200465cf25ff997dbc0f12358550a1 + checksum: 10/e48429d9dd83d7ae1b95c64b35af790e36cd8c1b2b9b63b2f69b5f804bb58a12918396f2f0540afd413673e1e0d22399a2cd2e2ad6534e50af2990a04e8ca7c4 languageName: node linkType: hard -"@typescript-eslint/visitor-keys@npm:5.62.0": - version: 5.62.0 - resolution: "@typescript-eslint/visitor-keys@npm:5.62.0" +"@typescript-eslint/visitor-keys@npm:5.59.9": + version: 5.59.9 + resolution: "@typescript-eslint/visitor-keys@npm:5.59.9" dependencies: - "@typescript-eslint/types": "npm:5.62.0" + "@typescript-eslint/types": "npm:5.59.9" eslint-visitor-keys: "npm:^3.3.0" - checksum: 10/dc613ab7569df9bbe0b2ca677635eb91839dfb2ca2c6fa47870a5da4f160db0b436f7ec0764362e756d4164e9445d49d5eb1ff0b87f4c058946ae9d8c92eb388 + checksum: 10/85761ef0be6910cb4de841b3cd8f39a734f5373ed92f808365882ef357f0a33ad6f75c4b3bf0b408f0399781ac5d14f12033de3e4b53a46b61015444b05854c0 languageName: node linkType: hard @@ -11332,6 +11966,15 @@ __metadata: languageName: node linkType: hard +"acorn@npm:^8.5.0": + version: 8.10.0 + resolution: "acorn@npm:8.10.0" + bin: + acorn: bin/acorn + checksum: 10/522310c20fdc3c271caed3caf0f06c51d61cb42267279566edd1d58e83dbc12eebdafaab666a0f0be1b7ad04af9c6bc2a6f478690a9e6391c3c8b165ada917dd + languageName: node + linkType: hard + "add-dom-event-listener@npm:^1.1.0": version: 1.1.0 resolution: "add-dom-event-listener@npm:1.1.0" @@ -11579,7 +12222,17 @@ __metadata: languageName: node linkType: hard -"anymatch@npm:^3.0.3, anymatch@npm:^3.1.3, anymatch@npm:~3.1.2": +"anymatch@npm:^3.0.3, anymatch@npm:~3.1.2": + version: 3.1.2 + resolution: "anymatch@npm:3.1.2" + dependencies: + normalize-path: "npm:^3.0.0" + picomatch: "npm:^2.0.4" + checksum: 10/985163db2292fac9e5a1e072bf99f1b5baccf196e4de25a0b0b81865ebddeb3b3eb4480734ef0a2ac8c002845396b91aa89121f5b84f93981a4658164a9ec6e9 + languageName: node + linkType: hard + +"anymatch@npm:^3.1.3": version: 3.1.3 resolution: "anymatch@npm:3.1.3" dependencies: @@ -11762,20 +12415,7 @@ __metadata: languageName: node linkType: hard -"array.prototype.findlastindex@npm:^1.2.3": - version: 1.2.3 - resolution: "array.prototype.findlastindex@npm:1.2.3" - dependencies: - call-bind: "npm:^1.0.2" - define-properties: "npm:^1.2.0" - es-abstract: "npm:^1.22.1" - es-shim-unscopables: "npm:^1.0.0" - get-intrinsic: "npm:^1.2.1" - checksum: 10/063cbab8eeac3aa01f3e980eecb9a8c5d87723032b49f7f814ecc6d75c33c03c17e3f43a458127a62e16303cab412f95d6ad9dc7e0ae6d9dc27a9bb76c24df7a - languageName: node - linkType: hard - -"array.prototype.flat@npm:^1.3.1, array.prototype.flat@npm:^1.3.2": +"array.prototype.flat@npm:^1.3.1": version: 1.3.2 resolution: "array.prototype.flat@npm:1.3.2" dependencies: @@ -12018,13 +12658,20 @@ __metadata: languageName: node linkType: hard -"axe-core@npm:=4.7.0, axe-core@npm:^4.2.0": +"axe-core@npm:=4.7.0": version: 4.7.0 resolution: "axe-core@npm:4.7.0" checksum: 10/615c0f7722c3c9fcf353dbd70b00e2ceae234d4c17cbc839dd85c01d16797c4e4da45f8d27c6118e9e6b033fb06efd196106e13651a1b2f3a10e0f11c7b2f660 languageName: node linkType: hard +"axe-core@npm:^4.2.0": + version: 4.6.3 + resolution: "axe-core@npm:4.6.3" + checksum: 10/280f6a7067129875380f733ae84093ce29c4b8cfe36e1a8ff46bd5d2bcd57d093f11b00223ddf5fef98ca147e0e6568ddd0ada9415cf8ae15d379224bf3cbb51 + languageName: node + linkType: hard + "axios@npm:^1.6.0": version: 1.6.7 resolution: "axios@npm:1.6.7" @@ -12071,6 +12718,23 @@ __metadata: languageName: node linkType: hard +"babel-jest@npm:^29.6.4": + version: 29.6.4 + resolution: "babel-jest@npm:29.6.4" + dependencies: + "@jest/transform": "npm:^29.6.4" + "@types/babel__core": "npm:^7.1.14" + babel-plugin-istanbul: "npm:^6.1.1" + babel-preset-jest: "npm:^29.6.3" + chalk: "npm:^4.0.0" + graceful-fs: "npm:^4.2.9" + slash: "npm:^3.0.0" + peerDependencies: + "@babel/core": ^7.8.0 + checksum: 10/1e26438368719336d3cb6144b68155f4837154b38d917180b9d0a2344e17dacb59b1213e593005fa7f63041052ad0e38cd1fb1de1c6b54f7d353f617a2ad20cf + languageName: node + linkType: hard + "babel-loader@npm:9.1.3, babel-loader@npm:^9.0.0": version: 9.1.3 resolution: "babel-loader@npm:9.1.3" @@ -12587,7 +13251,21 @@ __metadata: languageName: node linkType: hard -"browserslist@npm:^4.0.0, browserslist@npm:^4.14.5, browserslist@npm:^4.21.10, browserslist@npm:^4.21.4, browserslist@npm:^4.22.2": +"browserslist@npm:^4.0.0, browserslist@npm:^4.14.5, browserslist@npm:^4.21.4, browserslist@npm:^4.22.2": + version: 4.22.2 + resolution: "browserslist@npm:4.22.2" + dependencies: + caniuse-lite: "npm:^1.0.30001565" + electron-to-chromium: "npm:^1.4.601" + node-releases: "npm:^2.0.14" + update-browserslist-db: "npm:^1.0.13" + bin: + browserslist: cli.js + checksum: 10/e3590793db7f66ad3a50817e7b7f195ce61e029bd7187200244db664bfbe0ac832f784e4f6b9c958aef8ea4abe001ae7880b7522682df521f4bc0a5b67660b5e + languageName: node + linkType: hard + +"browserslist@npm:^4.21.10": version: 4.22.3 resolution: "browserslist@npm:4.22.3" dependencies: @@ -12895,6 +13573,13 @@ __metadata: languageName: node linkType: hard +"caniuse-lite@npm:^1.0.30001565": + version: 1.0.30001579 + resolution: "caniuse-lite@npm:1.0.30001579" + checksum: 10/2cd0c02e5d66b09888743ad2b624dbde697ace5c76b55bfd6065ea033f6abea8ac3f5d3c9299c042f91b396e2141b49bc61f5e17086dc9ba3a866cc6790134c0 + languageName: node + linkType: hard + "canvas-hypertxt@npm:^1.0.3": version: 1.0.3 resolution: "canvas-hypertxt@npm:1.0.3" @@ -13877,7 +14562,7 @@ __metadata: languageName: node linkType: hard -"core-js@npm:3.36.0, core-js@npm:^3.6.0, core-js@npm:^3.8.3": +"core-js@npm:3.36.0": version: 3.36.0 resolution: "core-js@npm:3.36.0" checksum: 10/896326c6391c1607dc645293c214cd31c6c535d4a77a88b15fc29e787199f9b06dc15986ddfbc798335bf7a7afd1e92152c94aa5a974790a7f97a98121774302 @@ -13891,6 +14576,13 @@ __metadata: languageName: node linkType: hard +"core-js@npm:^3.6.0, core-js@npm:^3.8.3": + version: 3.35.1 + resolution: "core-js@npm:3.35.1" + checksum: 10/5d31f22eb05cf66bd1a2088a04b7106faa5d0b91c1ffa5d72c5203e4974c31bd7e11969297f540a806c00c74c23991eaad5639592df8b5dbe4412fff3c075cd5 + languageName: node + linkType: hard + "core-util-is@npm:1.0.2": version: 1.0.2 resolution: "core-util-is@npm:1.0.2" @@ -14124,7 +14816,7 @@ __metadata: languageName: node linkType: hard -"css-loader@npm:6.10.0, css-loader@npm:^6.7.1": +"css-loader@npm:6.10.0": version: 6.10.0 resolution: "css-loader@npm:6.10.0" dependencies: @@ -14148,6 +14840,24 @@ __metadata: languageName: node linkType: hard +"css-loader@npm:^6.7.1": + version: 6.9.1 + resolution: "css-loader@npm:6.9.1" + dependencies: + icss-utils: "npm:^5.1.0" + postcss: "npm:^8.4.33" + postcss-modules-extract-imports: "npm:^3.0.0" + postcss-modules-local-by-default: "npm:^4.0.4" + postcss-modules-scope: "npm:^3.1.1" + postcss-modules-values: "npm:^4.0.0" + postcss-value-parser: "npm:^4.2.0" + semver: "npm:^7.5.4" + peerDependencies: + webpack: ^5.0.0 + checksum: 10/6f897406188ed7f6db03daab0602ed86df1e967b48a048ab72d0ee223e59ab9e13c5235481b12deb79e12aadf0be43bc3bdee71e1dc1e875e4bcd91c05b464af + languageName: node + linkType: hard + "css-minimizer-webpack-plugin@npm:6.0.0": version: 6.0.0 resolution: "css-minimizer-webpack-plugin@npm:6.0.0" @@ -15084,13 +15794,20 @@ __metadata: languageName: node linkType: hard -"deepmerge@npm:^4.0, deepmerge@npm:^4.2.2": +"deepmerge@npm:^4.0": version: 4.3.1 resolution: "deepmerge@npm:4.3.1" checksum: 10/058d9e1b0ff1a154468bf3837aea436abcfea1ba1d165ddaaf48ca93765fdd01a30d33c36173da8fbbed951dd0a267602bc782fe288b0fc4b7e1e7091afc4529 languageName: node linkType: hard +"deepmerge@npm:^4.2.2": + version: 4.2.2 + resolution: "deepmerge@npm:4.2.2" + checksum: 10/0e58ed14f530d08f9b996cfc3a41b0801691620235bc5e1883260e3ed1c1b4a1dfb59f865770e45d5dfb1d7ee108c4fc10c2f85e822989d4123490ea90be2545 + languageName: node + linkType: hard + "default-browser-id@npm:3.0.0": version: 3.0.0 resolution: "default-browser-id@npm:3.0.0" @@ -15418,7 +16135,7 @@ __metadata: languageName: node linkType: hard -"dom-accessibility-api@npm:^0.5.9": +"dom-accessibility-api@npm:^0.5.6, dom-accessibility-api@npm:^0.5.9": version: 0.5.10 resolution: "dom-accessibility-api@npm:0.5.10" checksum: 10/3ce183680c598392f89ec13fd04f495f95890c09d3da45909123ff549a10621ca21eee0258f929e4ed16a2cc73255d649174402b5fb7cd790983aa33b5a6fa3f @@ -15662,6 +16379,13 @@ __metadata: languageName: node linkType: hard +"electron-to-chromium@npm:^1.4.601": + version: 1.4.625 + resolution: "electron-to-chromium@npm:1.4.625" + checksum: 10/610a4eaabf6a064d8f6d4dfa25c55a3940f09a3b25edc8a271821d1b270bb28c4c9f19225d81bfc59deaa12c1f8f0144f3b4510631c6b6b47e0b6216737e216a + languageName: node + linkType: hard + "electron-to-chromium@npm:^1.4.648": version: 1.4.648 resolution: "electron-to-chromium@npm:1.4.648" @@ -16436,18 +17160,18 @@ __metadata: languageName: node linkType: hard -"eslint-import-resolver-node@npm:^0.3.9": - version: 0.3.9 - resolution: "eslint-import-resolver-node@npm:0.3.9" +"eslint-import-resolver-node@npm:^0.3.7": + version: 0.3.7 + resolution: "eslint-import-resolver-node@npm:0.3.7" dependencies: debug: "npm:^3.2.7" - is-core-module: "npm:^2.13.0" - resolve: "npm:^1.22.4" - checksum: 10/d52e08e1d96cf630957272e4f2644dcfb531e49dcfd1edd2e07e43369eb2ec7a7d4423d417beee613201206ff2efa4eb9a582b5825ee28802fc7c71fcd53ca83 + is-core-module: "npm:^2.11.0" + resolve: "npm:^1.22.1" + checksum: 10/31c6dfbd3457d1e6170ac2326b7ba9c323ff1ea68e3fcc5309f234bd1cefed050ee9b35e458b5eaed91323ab0d29bb2eddb41a1720ba7ca09bbacb00a0339d64 languageName: node linkType: hard -"eslint-module-utils@npm:^2.8.0": +"eslint-module-utils@npm:^2.7.4": version: 2.8.0 resolution: "eslint-module-utils@npm:2.8.0" dependencies: @@ -16460,29 +17184,27 @@ __metadata: linkType: hard "eslint-plugin-import@npm:^2.26.0": - version: 2.29.1 - resolution: "eslint-plugin-import@npm:2.29.1" + version: 2.27.5 + resolution: "eslint-plugin-import@npm:2.27.5" dependencies: - array-includes: "npm:^3.1.7" - array.prototype.findlastindex: "npm:^1.2.3" - array.prototype.flat: "npm:^1.3.2" - array.prototype.flatmap: "npm:^1.3.2" + array-includes: "npm:^3.1.6" + array.prototype.flat: "npm:^1.3.1" + array.prototype.flatmap: "npm:^1.3.1" debug: "npm:^3.2.7" doctrine: "npm:^2.1.0" - eslint-import-resolver-node: "npm:^0.3.9" - eslint-module-utils: "npm:^2.8.0" - hasown: "npm:^2.0.0" - is-core-module: "npm:^2.13.1" + eslint-import-resolver-node: "npm:^0.3.7" + eslint-module-utils: "npm:^2.7.4" + has: "npm:^1.0.3" + is-core-module: "npm:^2.11.0" is-glob: "npm:^4.0.3" minimatch: "npm:^3.1.2" - object.fromentries: "npm:^2.0.7" - object.groupby: "npm:^1.0.1" - object.values: "npm:^1.1.7" - semver: "npm:^6.3.1" - tsconfig-paths: "npm:^3.15.0" + object.values: "npm:^1.1.6" + resolve: "npm:^1.22.1" + semver: "npm:^6.3.0" + tsconfig-paths: "npm:^3.14.1" peerDependencies: eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 - checksum: 10/5865f05c38552145423c535326ec9a7113ab2305c7614c8b896ff905cfabc859c8805cac21e979c9f6f742afa333e6f62f812eabf891a7e8f5f0b853a32593c1 + checksum: 10/b8ab9521bd47acdad959309cbb5635069cebd0f1dfd14b5f6ad24f609dfda82c604b029c7366cafce1d359845300957ec246587cd5e4b237a0378118a9d3dfa7 languageName: node linkType: hard @@ -17002,6 +17724,19 @@ __metadata: languageName: node linkType: hard +"expect@npm:^29.6.4": + version: 29.6.4 + resolution: "expect@npm:29.6.4" + dependencies: + "@jest/expect-utils": "npm:^29.6.4" + jest-get-type: "npm:^29.6.3" + jest-matcher-utils: "npm:^29.6.4" + jest-message-util: "npm:^29.6.3" + jest-util: "npm:^29.6.3" + checksum: 10/1e9224ce01de2bcd861b5a2b9409cc316c4f298beaa2c4ffb8a907a593e15ddff905506676f2b1f20d31fb1c0919a4527310b37b6d93f2ba4c4f77bf9881a90e + languageName: node + linkType: hard + "exponential-backoff@npm:^3.1.1": version: 3.1.1 resolution: "exponential-backoff@npm:3.1.1" @@ -17140,6 +17875,13 @@ __metadata: languageName: node linkType: hard +"fast-fifo@npm:^1.0.0": + version: 1.1.0 + resolution: "fast-fifo@npm:1.1.0" + checksum: 10/895f4c9873a4d5059dfa244aa0dde2b22ee563fd673d85b638869715f92244f9d6469bc0873bcb40554d28c51cbc7590045718462cfda1da503b1c6985815209 + languageName: node + linkType: hard + "fast-fifo@npm:^1.1.0": version: 1.3.2 resolution: "fast-fifo@npm:1.3.2" @@ -17147,7 +17889,33 @@ __metadata: languageName: node linkType: hard -"fast-glob@npm:^3.0.3, fast-glob@npm:^3.2.11, fast-glob@npm:^3.2.9, fast-glob@npm:^3.3.2": +"fast-glob@npm:^3.0.3": + version: 3.3.1 + resolution: "fast-glob@npm:3.3.1" + dependencies: + "@nodelib/fs.stat": "npm:^2.0.2" + "@nodelib/fs.walk": "npm:^1.2.3" + glob-parent: "npm:^5.1.2" + merge2: "npm:^1.3.0" + micromatch: "npm:^4.0.4" + checksum: 10/51bcd15472879dfe51d4b01c5b70bbc7652724d39cdd082ba11276dbd7d84db0f6b33757e1938af8b2768a4bf485d9be0c89153beae24ee8331d6dcc7550379f + languageName: node + linkType: hard + +"fast-glob@npm:^3.2.11, fast-glob@npm:^3.2.9": + version: 3.3.0 + resolution: "fast-glob@npm:3.3.0" + dependencies: + "@nodelib/fs.stat": "npm:^2.0.2" + "@nodelib/fs.walk": "npm:^1.2.3" + glob-parent: "npm:^5.1.2" + merge2: "npm:^1.3.0" + micromatch: "npm:^4.0.4" + checksum: 10/4cd74914f13eab48dd1a0d16051aa102c13d30ea8a79c991563ea3111a37ff6d888518964291d52d723e7ad2a946149ce9f13d27ad9a07a1e4e1aefb4717ed29 + languageName: node + linkType: hard + +"fast-glob@npm:^3.3.2": version: 3.3.2 resolution: "fast-glob@npm:3.3.2" dependencies: @@ -17230,7 +17998,7 @@ __metadata: languageName: node linkType: hard -"fastq@npm:^1.13.0, fastq@npm:^1.6.0": +"fastq@npm:^1.13.0": version: 1.17.1 resolution: "fastq@npm:1.17.1" dependencies: @@ -17239,6 +18007,15 @@ __metadata: languageName: node linkType: hard +"fastq@npm:^1.6.0": + version: 1.13.0 + resolution: "fastq@npm:1.13.0" + dependencies: + reusify: "npm:^1.0.4" + checksum: 10/0902cb9b81accf34e5542612c8a1df6c6ea47674f85bcc9cdc38795a28b53e4a096f751cfcf4fb25d2ea42fee5447499ba6cf5af5d0209297e1d1fd4dd551bb6 + languageName: node + linkType: hard + "fault@npm:^1.0.0": version: 1.0.4 resolution: "fault@npm:1.0.4" @@ -17794,7 +18571,7 @@ __metadata: languageName: node linkType: hard -"fsevents@npm:^2.3.2, fsevents@npm:~2.3.2": +"fsevents@npm:2.3.2, fsevents@npm:^2.3.2, fsevents@npm:~2.3.2": version: 2.3.2 resolution: "fsevents@npm:2.3.2" dependencies: @@ -17804,7 +18581,7 @@ __metadata: languageName: node linkType: hard -"fsevents@patch:fsevents@npm%3A^2.3.2#optional!builtin, fsevents@patch:fsevents@npm%3A~2.3.2#optional!builtin": +"fsevents@patch:fsevents@npm%3A2.3.2#optional!builtin, fsevents@patch:fsevents@npm%3A^2.3.2#optional!builtin, fsevents@patch:fsevents@npm%3A~2.3.2#optional!builtin": version: 2.3.2 resolution: "fsevents@patch:fsevents@npm%3A2.3.2#optional!builtin::version=2.3.2&hash=df0bf1" dependencies: @@ -18406,13 +19183,20 @@ __metadata: languageName: node linkType: hard -"graceful-fs@npm:4.2.11, graceful-fs@npm:^4.1.11, graceful-fs@npm:^4.1.15, graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.10, graceful-fs@npm:^4.2.11, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.8, graceful-fs@npm:^4.2.9": +"graceful-fs@npm:4.2.11, graceful-fs@npm:^4.2.10, graceful-fs@npm:^4.2.11, graceful-fs@npm:^4.2.8": version: 4.2.11 resolution: "graceful-fs@npm:4.2.11" checksum: 10/bf152d0ed1dc159239db1ba1f74fdbc40cb02f626770dcd5815c427ce0688c2635a06ed69af364396da4636d0408fcf7d4afdf7881724c3307e46aff30ca49e2 languageName: node linkType: hard +"graceful-fs@npm:^4.1.11, graceful-fs@npm:^4.1.15, graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.9": + version: 4.2.10 + resolution: "graceful-fs@npm:4.2.10" + checksum: 10/0c83c52b62c68a944dcfb9d66b0f9f10f7d6e3d081e8067b9bfdc9e5f3a8896584d576036f82915773189eec1eba599397fc620e75c03c0610fb3d67c6713c1a + languageName: node + linkType: hard + "grafana@workspace:.": version: 0.0.0-use.local resolution: "grafana@workspace:." @@ -18449,6 +19233,7 @@ __metadata: "@grafana/lezer-logql": "npm:0.2.3" "@grafana/monaco-logql": "npm:^0.0.7" "@grafana/o11y-ds-frontend": "workspace:*" + "@grafana/plugin-e2e": "npm:0.18.0" "@grafana/prometheus": "workspace:*" "@grafana/runtime": "workspace:*" "@grafana/scenes": "npm:^3.5.0" @@ -18469,6 +19254,7 @@ __metadata: "@opentelemetry/api": "npm:1.7.0" "@opentelemetry/exporter-collector": "npm:0.25.0" "@opentelemetry/semantic-conventions": "npm:1.21.0" + "@playwright/test": "npm:^1.41.2" "@pmmmwh/react-refresh-webpack-plugin": "npm:0.5.11" "@popperjs/core": "npm:2.11.8" "@prometheus-io/lezer-promql": "npm:^0.37.0-rc.1" @@ -19491,11 +20277,11 @@ __metadata: linkType: hard "i18next-browser-languagedetector@npm:^7.0.2": - version: 7.2.0 - resolution: "i18next-browser-languagedetector@npm:7.2.0" + version: 7.0.2 + resolution: "i18next-browser-languagedetector@npm:7.0.2" dependencies: - "@babel/runtime": "npm:^7.23.2" - checksum: 10/5117b4961e0f32818f0d4587e81767d38c3a8e27305f1734fff2b07fe8c256161e2cdbd453b766b3c097055813fe89c43bce68b1d8f765b5b7f694d9852fe703 + "@babel/runtime": "npm:^7.19.4" + checksum: 10/9f07be9d94e4df342f0eb2aab1437534db0832edb9b20b0504ae6afda0db0294cacb0d11d723fd39f522c47a3c9ba91b8e834a8c0d7f4ec2261a1e37dcd63b61 languageName: node linkType: hard @@ -19639,7 +20425,7 @@ __metadata: languageName: node linkType: hard -"import-local@npm:3.1.0, import-local@npm:^3.0.2": +"import-local@npm:3.1.0": version: 3.1.0 resolution: "import-local@npm:3.1.0" dependencies: @@ -19651,6 +20437,18 @@ __metadata: languageName: node linkType: hard +"import-local@npm:^3.0.2": + version: 3.0.3 + resolution: "import-local@npm:3.0.3" + dependencies: + pkg-dir: "npm:^4.2.0" + resolve-cwd: "npm:^3.0.0" + bin: + import-local-fixture: fixtures/cli.js + checksum: 10/38ae57d35e7fd5f63b55895050c798d4dd590e4e2337e9ffa882fb3ea7a7716f3162c7300e382e0a733ca5d07b389fadff652c00fa7b072d5cb6ea34ca06b179 + languageName: node + linkType: hard + "imurmurhash@npm:^0.1.4": version: 0.1.4 resolution: "imurmurhash@npm:0.1.4" @@ -19958,7 +20756,7 @@ __metadata: languageName: node linkType: hard -"is-core-module@npm:^2.13.0, is-core-module@npm:^2.13.1, is-core-module@npm:^2.5.0, is-core-module@npm:^2.8.1, is-core-module@npm:^2.9.0": +"is-core-module@npm:^2.11.0, is-core-module@npm:^2.13.0, is-core-module@npm:^2.5.0, is-core-module@npm:^2.8.1, is-core-module@npm:^2.9.0": version: 2.13.1 resolution: "is-core-module@npm:2.13.1" dependencies: @@ -20383,7 +21181,20 @@ __metadata: languageName: node linkType: hard -"is-typed-array@npm:^1.1.10, is-typed-array@npm:^1.1.12, is-typed-array@npm:^1.1.3, is-typed-array@npm:^1.1.9": +"is-typed-array@npm:^1.1.10, is-typed-array@npm:^1.1.3, is-typed-array@npm:^1.1.9": + version: 1.1.10 + resolution: "is-typed-array@npm:1.1.10" + dependencies: + available-typed-arrays: "npm:^1.0.5" + call-bind: "npm:^1.0.2" + for-each: "npm:^0.3.3" + gopd: "npm:^1.0.1" + has-tostringtag: "npm:^1.0.0" + checksum: 10/2392b2473bbc994f5c30d6848e32bab3cab6c80b795aaec3020baf5419ff7df38fc11b3a043eb56d50f842394c578dbb204a7a29398099f895cf111c5b27f327 + languageName: node + linkType: hard + +"is-typed-array@npm:^1.1.12": version: 1.1.12 resolution: "is-typed-array@npm:1.1.12" dependencies: @@ -20635,6 +21446,17 @@ __metadata: languageName: node linkType: hard +"jest-changed-files@npm:^29.6.3": + version: 29.6.3 + resolution: "jest-changed-files@npm:29.6.3" + dependencies: + execa: "npm:^5.0.0" + jest-util: "npm:^29.6.3" + p-limit: "npm:^3.1.0" + checksum: 10/ffcd0add3351c54ee0eb3ed88e2352ecf46d9118daed99ab0b73cb25502848a19db3ff3027f8b6ac1a168e158bc87d2d30adbd6c88119fad19f5f87357896f82 + languageName: node + linkType: hard + "jest-changed-files@npm:^29.7.0": version: 29.7.0 resolution: "jest-changed-files@npm:29.7.0" @@ -20646,6 +21468,34 @@ __metadata: languageName: node linkType: hard +"jest-circus@npm:^29.6.4": + version: 29.6.4 + resolution: "jest-circus@npm:29.6.4" + dependencies: + "@jest/environment": "npm:^29.6.4" + "@jest/expect": "npm:^29.6.4" + "@jest/test-result": "npm:^29.6.4" + "@jest/types": "npm:^29.6.3" + "@types/node": "npm:*" + chalk: "npm:^4.0.0" + co: "npm:^4.6.0" + dedent: "npm:^1.0.0" + is-generator-fn: "npm:^2.0.0" + jest-each: "npm:^29.6.3" + jest-matcher-utils: "npm:^29.6.4" + jest-message-util: "npm:^29.6.3" + jest-runtime: "npm:^29.6.4" + jest-snapshot: "npm:^29.6.4" + jest-util: "npm:^29.6.3" + p-limit: "npm:^3.1.0" + pretty-format: "npm:^29.6.3" + pure-rand: "npm:^6.0.0" + slash: "npm:^3.0.0" + stack-utils: "npm:^2.0.3" + checksum: 10/70dd56c1dec25a7499df85d942f27549ce257430b36597e984dbb3dac630a6707133e50a67285cbcf5564aace700e54748c6b7290136b188363678d0af8db17a + languageName: node + linkType: hard + "jest-circus@npm:^29.7.0": version: 29.7.0 resolution: "jest-circus@npm:29.7.0" @@ -20700,6 +21550,71 @@ __metadata: languageName: node linkType: hard +"jest-cli@npm:^29.6.4": + version: 29.6.4 + resolution: "jest-cli@npm:29.6.4" + dependencies: + "@jest/core": "npm:^29.6.4" + "@jest/test-result": "npm:^29.6.4" + "@jest/types": "npm:^29.6.3" + chalk: "npm:^4.0.0" + exit: "npm:^0.1.2" + graceful-fs: "npm:^4.2.9" + import-local: "npm:^3.0.2" + jest-config: "npm:^29.6.4" + jest-util: "npm:^29.6.3" + jest-validate: "npm:^29.6.3" + prompts: "npm:^2.0.1" + yargs: "npm:^17.3.1" + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + bin: + jest: bin/jest.js + checksum: 10/24b6ca6c8409433a8da621d91a7637ff67e606c0d6c18b606bbbc1a0c7f7e819b935ec1bc6c45c8e6892faf9f073c409f058cdf0ac454f9379ddb826175d40cf + languageName: node + linkType: hard + +"jest-config@npm:^29.6.4": + version: 29.6.4 + resolution: "jest-config@npm:29.6.4" + dependencies: + "@babel/core": "npm:^7.11.6" + "@jest/test-sequencer": "npm:^29.6.4" + "@jest/types": "npm:^29.6.3" + babel-jest: "npm:^29.6.4" + chalk: "npm:^4.0.0" + ci-info: "npm:^3.2.0" + deepmerge: "npm:^4.2.2" + glob: "npm:^7.1.3" + graceful-fs: "npm:^4.2.9" + jest-circus: "npm:^29.6.4" + jest-environment-node: "npm:^29.6.4" + jest-get-type: "npm:^29.6.3" + jest-regex-util: "npm:^29.6.3" + jest-resolve: "npm:^29.6.4" + jest-runner: "npm:^29.6.4" + jest-util: "npm:^29.6.3" + jest-validate: "npm:^29.6.3" + micromatch: "npm:^4.0.4" + parse-json: "npm:^5.2.0" + pretty-format: "npm:^29.6.3" + slash: "npm:^3.0.0" + strip-json-comments: "npm:^3.1.1" + peerDependencies: + "@types/node": "*" + ts-node: ">=9.0.0" + peerDependenciesMeta: + "@types/node": + optional: true + ts-node: + optional: true + checksum: 10/5d9019f046684bf45e87c01996197ccfb96936bca67242f59fdf40d9bd76622c97b5b57bbb257591e12edb941285662dac106f8f327910b26c822adda7057c6d + languageName: node + linkType: hard + "jest-config@npm:^29.7.0": version: 29.7.0 resolution: "jest-config@npm:29.7.0" @@ -20781,6 +21696,27 @@ __metadata: languageName: node linkType: hard +"jest-diff@npm:^29.6.4": + version: 29.6.4 + resolution: "jest-diff@npm:29.6.4" + dependencies: + chalk: "npm:^4.0.0" + diff-sequences: "npm:^29.6.3" + jest-get-type: "npm:^29.6.3" + pretty-format: "npm:^29.6.3" + checksum: 10/b1720b78d1de8e6efaf74425df57e749008049b7c2f8a60af73667fd886653bbc7ee69a452076073ad4b2e3d9d1cd6599bb9dc00a8fb69f02b9075423aafee3c + languageName: node + linkType: hard + +"jest-docblock@npm:^29.6.3": + version: 29.6.3 + resolution: "jest-docblock@npm:29.6.3" + dependencies: + detect-newline: "npm:^3.0.0" + checksum: 10/fa9d8d344f093659beb741e2efa22db663ef8c441200b74707da7749a8799a0022824166d7fdd972e773f7c4fab01227f1847e3315fa63584f539b7b78b285eb + languageName: node + linkType: hard + "jest-docblock@npm:^29.7.0": version: 29.7.0 resolution: "jest-docblock@npm:29.7.0" @@ -20790,6 +21726,19 @@ __metadata: languageName: node linkType: hard +"jest-each@npm:^29.6.3": + version: 29.6.3 + resolution: "jest-each@npm:29.6.3" + dependencies: + "@jest/types": "npm:^29.6.3" + chalk: "npm:^4.0.0" + jest-get-type: "npm:^29.6.3" + jest-util: "npm:^29.6.3" + pretty-format: "npm:^29.6.3" + checksum: 10/e08c01ffba3c6254b6555b749eb7b2e55ca4f153d4e1d8ac15b7dcec4d5c06b150b9f9a272cf3cd622b3b2c495c2d5ee656165b9a93c1c06dcb1a82f13fa95e0 + languageName: node + linkType: hard + "jest-each@npm:^29.7.0": version: 29.7.0 resolution: "jest-each@npm:29.7.0" @@ -20845,6 +21794,20 @@ __metadata: languageName: node linkType: hard +"jest-environment-node@npm:^29.6.4": + version: 29.6.4 + resolution: "jest-environment-node@npm:29.6.4" + dependencies: + "@jest/environment": "npm:^29.6.4" + "@jest/fake-timers": "npm:^29.6.4" + "@jest/types": "npm:^29.6.3" + "@types/node": "npm:*" + jest-mock: "npm:^29.6.3" + jest-util: "npm:^29.6.3" + checksum: 10/7c7bb39a35eaff23eb553c5b7cae776c0622737e64bcd6b558b745012da1366d5f7e2de0a6f659c39f3869783c0b92a5a3dc64649638dbf8f3208c82832b6321 + languageName: node + linkType: hard + "jest-environment-node@npm:^29.7.0": version: 29.7.0 resolution: "jest-environment-node@npm:29.7.0" @@ -20887,6 +21850,29 @@ __metadata: languageName: node linkType: hard +"jest-haste-map@npm:^29.6.4": + version: 29.6.4 + resolution: "jest-haste-map@npm:29.6.4" + dependencies: + "@jest/types": "npm:^29.6.3" + "@types/graceful-fs": "npm:^4.1.3" + "@types/node": "npm:*" + anymatch: "npm:^3.0.3" + fb-watchman: "npm:^2.0.0" + fsevents: "npm:^2.3.2" + graceful-fs: "npm:^4.2.9" + jest-regex-util: "npm:^29.6.3" + jest-util: "npm:^29.6.3" + jest-worker: "npm:^29.6.4" + micromatch: "npm:^4.0.4" + walker: "npm:^1.0.8" + dependenciesMeta: + fsevents: + optional: true + checksum: 10/5fb2dc9cd028b6d02a8d4dcaf81b790b60f9034a20b6a1deca9a4ad529741362cd1035020f3d94f6c160103b1ea3d46771f40a3330ab4d8d3c073d515e11b810 + languageName: node + linkType: hard + "jest-haste-map@npm:^29.7.0": version: 29.7.0 resolution: "jest-haste-map@npm:29.7.0" @@ -20922,6 +21908,16 @@ __metadata: languageName: node linkType: hard +"jest-leak-detector@npm:^29.6.3": + version: 29.6.3 + resolution: "jest-leak-detector@npm:29.6.3" + dependencies: + jest-get-type: "npm:^29.6.3" + pretty-format: "npm:^29.6.3" + checksum: 10/27548fcfc7602fe1b88f8600185e35ffff71751f3631e52bbfdfc72776f5a13a430185cf02fc632b41320a74f99ae90e40ce101c8887509f0f919608a7175129 + languageName: node + linkType: hard + "jest-leak-detector@npm:^29.7.0": version: 29.7.0 resolution: "jest-leak-detector@npm:29.7.0" @@ -20944,6 +21940,35 @@ __metadata: languageName: node linkType: hard +"jest-matcher-utils@npm:^29.6.4": + version: 29.6.4 + resolution: "jest-matcher-utils@npm:29.6.4" + dependencies: + chalk: "npm:^4.0.0" + jest-diff: "npm:^29.6.4" + jest-get-type: "npm:^29.6.3" + pretty-format: "npm:^29.6.3" + checksum: 10/de306e3592d316ff9725b8e2595c6a4bb9c05b1f296b3e73aef5cf945a4b4799dbfc3fc080e74f4e6259b65123a70b2dc3595db5cfcbaaa30ed3d37ec59551a0 + languageName: node + linkType: hard + +"jest-message-util@npm:^29.6.3": + version: 29.6.3 + resolution: "jest-message-util@npm:29.6.3" + dependencies: + "@babel/code-frame": "npm:^7.12.13" + "@jest/types": "npm:^29.6.3" + "@types/stack-utils": "npm:^2.0.0" + chalk: "npm:^4.0.0" + graceful-fs: "npm:^4.2.9" + micromatch: "npm:^4.0.4" + pretty-format: "npm:^29.6.3" + slash: "npm:^3.0.0" + stack-utils: "npm:^2.0.3" + checksum: 10/fe659a92a32e6f9c3fdb9b07792a2a362b3d091334eb230b12524ffb5023457ea39d7fc412187e4f245dbe394fd012591878a2b5932eaedd7e82d5c9b416035c + languageName: node + linkType: hard + "jest-message-util@npm:^29.7.0": version: 29.7.0 resolution: "jest-message-util@npm:29.7.0" @@ -20972,6 +21997,17 @@ __metadata: languageName: node linkType: hard +"jest-mock@npm:^29.6.3": + version: 29.6.3 + resolution: "jest-mock@npm:29.6.3" + dependencies: + "@jest/types": "npm:^29.6.3" + "@types/node": "npm:*" + jest-util: "npm:^29.6.3" + checksum: 10/9da22e0edfb77b2ed2d4204f305b41a17c2133c0cb638ee81e875115ae6e01adf57681a0ab8f7c2b7e0cde7dcd916d62c7b5d28176c342bb80bbfcea8a168729 + languageName: node + linkType: hard + "jest-pnp-resolver@npm:^1.2.2": version: 1.2.2 resolution: "jest-pnp-resolver@npm:1.2.2" @@ -20991,6 +22027,16 @@ __metadata: languageName: node linkType: hard +"jest-resolve-dependencies@npm:^29.6.4": + version: 29.6.4 + resolution: "jest-resolve-dependencies@npm:29.6.4" + dependencies: + jest-regex-util: "npm:^29.6.3" + jest-snapshot: "npm:^29.6.4" + checksum: 10/8a1616558e78213bc4c7c445e7188776f7d4940e3858684e0404c485bbc23e1e10093bf59640fbc4b88141f361f0c01e760eb2c79bf088ad1896971941869158 + languageName: node + linkType: hard + "jest-resolve-dependencies@npm:^29.7.0": version: 29.7.0 resolution: "jest-resolve-dependencies@npm:29.7.0" @@ -21001,6 +22047,23 @@ __metadata: languageName: node linkType: hard +"jest-resolve@npm:^29.6.4": + version: 29.6.4 + resolution: "jest-resolve@npm:29.6.4" + dependencies: + chalk: "npm:^4.0.0" + graceful-fs: "npm:^4.2.9" + jest-haste-map: "npm:^29.6.4" + jest-pnp-resolver: "npm:^1.2.2" + jest-util: "npm:^29.6.3" + jest-validate: "npm:^29.6.3" + resolve: "npm:^1.20.0" + resolve.exports: "npm:^2.0.0" + slash: "npm:^3.0.0" + checksum: 10/63fcd739106363a8928a96131fd0fd7dfa8b052b70d0c3a3f1099f33666b2d9086880f01e714e46b1044f7d107caaae81fd1547bd2c149d6b7a06453d5cc14b3 + languageName: node + linkType: hard + "jest-resolve@npm:^29.7.0": version: 29.7.0 resolution: "jest-resolve@npm:29.7.0" @@ -21018,6 +22081,35 @@ __metadata: languageName: node linkType: hard +"jest-runner@npm:^29.6.4": + version: 29.6.4 + resolution: "jest-runner@npm:29.6.4" + dependencies: + "@jest/console": "npm:^29.6.4" + "@jest/environment": "npm:^29.6.4" + "@jest/test-result": "npm:^29.6.4" + "@jest/transform": "npm:^29.6.4" + "@jest/types": "npm:^29.6.3" + "@types/node": "npm:*" + chalk: "npm:^4.0.0" + emittery: "npm:^0.13.1" + graceful-fs: "npm:^4.2.9" + jest-docblock: "npm:^29.6.3" + jest-environment-node: "npm:^29.6.4" + jest-haste-map: "npm:^29.6.4" + jest-leak-detector: "npm:^29.6.3" + jest-message-util: "npm:^29.6.3" + jest-resolve: "npm:^29.6.4" + jest-runtime: "npm:^29.6.4" + jest-util: "npm:^29.6.3" + jest-watcher: "npm:^29.6.4" + jest-worker: "npm:^29.6.4" + p-limit: "npm:^3.1.0" + source-map-support: "npm:0.5.13" + checksum: 10/b51345e07b78b546dd192f734d1e5324f41fe5aceafba6a8238b3f38f3bcafa433ee9a5bdf41f853bed4279b044ce0621be75b33b840f4a9745b6c958220a21e + languageName: node + linkType: hard + "jest-runner@npm:^29.7.0": version: 29.7.0 resolution: "jest-runner@npm:29.7.0" @@ -21047,6 +22139,36 @@ __metadata: languageName: node linkType: hard +"jest-runtime@npm:^29.6.4": + version: 29.6.4 + resolution: "jest-runtime@npm:29.6.4" + dependencies: + "@jest/environment": "npm:^29.6.4" + "@jest/fake-timers": "npm:^29.6.4" + "@jest/globals": "npm:^29.6.4" + "@jest/source-map": "npm:^29.6.3" + "@jest/test-result": "npm:^29.6.4" + "@jest/transform": "npm:^29.6.4" + "@jest/types": "npm:^29.6.3" + "@types/node": "npm:*" + chalk: "npm:^4.0.0" + cjs-module-lexer: "npm:^1.0.0" + collect-v8-coverage: "npm:^1.0.0" + glob: "npm:^7.1.3" + graceful-fs: "npm:^4.2.9" + jest-haste-map: "npm:^29.6.4" + jest-message-util: "npm:^29.6.3" + jest-mock: "npm:^29.6.3" + jest-regex-util: "npm:^29.6.3" + jest-resolve: "npm:^29.6.4" + jest-snapshot: "npm:^29.6.4" + jest-util: "npm:^29.6.3" + slash: "npm:^3.0.0" + strip-bom: "npm:^4.0.0" + checksum: 10/e70669b5a439f5a26156b972462471fbac997e6779fe7421df000f5fe8f1f11abe0c7cb664edd1cf3849c3f2d8329dc7e8495ce89251352a6cabff9153dcf99a + languageName: node + linkType: hard + "jest-runtime@npm:^29.7.0": version: 29.7.0 resolution: "jest-runtime@npm:29.7.0" @@ -21077,6 +22199,34 @@ __metadata: languageName: node linkType: hard +"jest-snapshot@npm:^29.6.4": + version: 29.6.4 + resolution: "jest-snapshot@npm:29.6.4" + dependencies: + "@babel/core": "npm:^7.11.6" + "@babel/generator": "npm:^7.7.2" + "@babel/plugin-syntax-jsx": "npm:^7.7.2" + "@babel/plugin-syntax-typescript": "npm:^7.7.2" + "@babel/types": "npm:^7.3.3" + "@jest/expect-utils": "npm:^29.6.4" + "@jest/transform": "npm:^29.6.4" + "@jest/types": "npm:^29.6.3" + babel-preset-current-node-syntax: "npm:^1.0.0" + chalk: "npm:^4.0.0" + expect: "npm:^29.6.4" + graceful-fs: "npm:^4.2.9" + jest-diff: "npm:^29.6.4" + jest-get-type: "npm:^29.6.3" + jest-matcher-utils: "npm:^29.6.4" + jest-message-util: "npm:^29.6.3" + jest-util: "npm:^29.6.3" + natural-compare: "npm:^1.4.0" + pretty-format: "npm:^29.6.3" + semver: "npm:^7.5.3" + checksum: 10/998476c7ffef43cfe97ef9b786c6c6489440b0042537b0c1ad8b4366de73fc6b1ec16d148ef294b24b835d045c186b2543217e6906dd495b4d20112e85007168 + languageName: node + linkType: hard + "jest-snapshot@npm:^29.7.0": version: 29.7.0 resolution: "jest-snapshot@npm:29.7.0" @@ -21119,6 +22269,34 @@ __metadata: languageName: node linkType: hard +"jest-util@npm:^29.6.3": + version: 29.6.3 + resolution: "jest-util@npm:29.6.3" + dependencies: + "@jest/types": "npm:^29.6.3" + "@types/node": "npm:*" + chalk: "npm:^4.0.0" + ci-info: "npm:^3.2.0" + graceful-fs: "npm:^4.2.9" + picomatch: "npm:^2.2.3" + checksum: 10/455af2b5e064213b33b837a18ddd3d31878aee31ad40bbd599de2a4977f860a797e491cb94894e38bbd352cb7b31d41448b7ec3b346408613015411cd88ed57f + languageName: node + linkType: hard + +"jest-validate@npm:^29.6.3": + version: 29.6.3 + resolution: "jest-validate@npm:29.6.3" + dependencies: + "@jest/types": "npm:^29.6.3" + camelcase: "npm:^6.2.0" + chalk: "npm:^4.0.0" + jest-get-type: "npm:^29.6.3" + leven: "npm:^3.1.0" + pretty-format: "npm:^29.6.3" + checksum: 10/4e0a3ef5a2c181f10dcd979ae62188166effd52d7aec7916deaa29b75b29bdf4e01b77375246c7c16032d95cb7364ceae69c5d146e4348ccdf8a3a43d1c6862c + languageName: node + linkType: hard + "jest-validate@npm:^29.7.0": version: 29.7.0 resolution: "jest-validate@npm:29.7.0" @@ -21133,6 +22311,22 @@ __metadata: languageName: node linkType: hard +"jest-watcher@npm:^29.6.4": + version: 29.6.4 + resolution: "jest-watcher@npm:29.6.4" + dependencies: + "@jest/test-result": "npm:^29.6.4" + "@jest/types": "npm:^29.6.3" + "@types/node": "npm:*" + ansi-escapes: "npm:^4.2.1" + chalk: "npm:^4.0.0" + emittery: "npm:^0.13.1" + jest-util: "npm:^29.6.3" + string-length: "npm:^4.0.1" + checksum: 10/01c008b2f3e76b024f9e9dd242ba5b172634a6b9bc201d7f27cb82563071f06ae87e3ae8db50336034264a98d20a45459068f089597c956a96959109527498c4 + languageName: node + linkType: hard + "jest-watcher@npm:^29.7.0": version: 29.7.0 resolution: "jest-watcher@npm:29.7.0" @@ -21183,6 +22377,18 @@ __metadata: languageName: node linkType: hard +"jest-worker@npm:^29.6.4": + version: 29.6.4 + resolution: "jest-worker@npm:29.6.4" + dependencies: + "@types/node": "npm:*" + jest-util: "npm:^29.6.3" + merge-stream: "npm:^2.0.0" + supports-color: "npm:^8.0.0" + checksum: 10/52b2f238f21ff20dd4cb74ab85c1b32ba8ce5709355ae2f6d3e908a06ac0828658ab9d94562d752833d0e469f35517ceba9b68ca6b0d27fa1057115f065864ce + languageName: node + linkType: hard + "jest@npm:29.3.1": version: 29.3.1 resolution: "jest@npm:29.3.1" @@ -21202,7 +22408,7 @@ __metadata: languageName: node linkType: hard -"jest@npm:29.7.0, jest@npm:^29.6.4": +"jest@npm:29.7.0": version: 29.7.0 resolution: "jest@npm:29.7.0" dependencies: @@ -21221,6 +22427,25 @@ __metadata: languageName: node linkType: hard +"jest@npm:^29.6.4": + version: 29.6.4 + resolution: "jest@npm:29.6.4" + dependencies: + "@jest/core": "npm:^29.6.4" + "@jest/types": "npm:^29.6.3" + import-local: "npm:^3.0.2" + jest-cli: "npm:^29.6.4" + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + bin: + jest: bin/jest.js + checksum: 10/d747e293bd63f583e7978ac0693ab7a019812fa44b9bf3b3fe20e75e8a343bcd8251d292326d73151dc0b8a2b5a974d878b3aa9ffb146dfa7980553f64a35b43 + languageName: node + linkType: hard + "jiti@npm:^1.20.0": version: 1.21.0 resolution: "jiti@npm:1.21.0" @@ -21488,7 +22713,7 @@ __metadata: languageName: node linkType: hard -"json5@npm:^1.0.2": +"json5@npm:^1.0.1": version: 1.0.2 resolution: "json5@npm:1.0.2" dependencies: @@ -22171,14 +23396,14 @@ __metadata: linkType: hard "logfmt@npm:^1.3.2": - version: 1.4.0 - resolution: "logfmt@npm:1.4.0" + version: 1.3.2 + resolution: "logfmt@npm:1.3.2" dependencies: split: "npm:0.2.x" through: "npm:2.3.x" bin: - logfmt: bin/logfmt - checksum: 10/4576cc77faa5596c62bdbb4aec9efeba8e6758495b395a48ab2c7ee49e0673c85c2498ed792740b21607e011c4d94e4fc7449034ba7ba67f8a9ae14a2fb1e801 + logfmt: ./bin/logfmt + checksum: 10/08a4d4467cc8e066f05394a966ea103fa8785da3e22fb82a502e62cc0edc3c8679405bb8bbdd93c859da7defffe1d7feeeb47a59da11cdd76e48bf9374430cdd languageName: node linkType: hard @@ -22745,7 +23970,7 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:9.0.3, minimatch@npm:^9.0.0, minimatch@npm:^9.0.1, minimatch@npm:^9.0.3": +"minimatch@npm:9.0.3, minimatch@npm:^9.0.0, minimatch@npm:^9.0.3": version: 9.0.3 resolution: "minimatch@npm:9.0.3" dependencies: @@ -22772,6 +23997,15 @@ __metadata: languageName: node linkType: hard +"minimatch@npm:^9.0.1": + version: 9.0.1 + resolution: "minimatch@npm:9.0.1" + dependencies: + brace-expansion: "npm:^2.0.1" + checksum: 10/b4e98f4dc740dcf33999a99af23ae6e5e1c47632f296dc95cb649a282150f92378d41434bf64af4ea2e5975255a757d031c3bf014bad9214544ac57d97f3ba63 + languageName: node + linkType: hard + "minimist-options@npm:4.1.0": version: 4.1.0 resolution: "minimist-options@npm:4.1.0" @@ -23327,11 +24561,11 @@ __metadata: linkType: hard "nanoid@npm:^5.0.4": - version: 5.0.5 - resolution: "nanoid@npm:5.0.5" + version: 5.0.4 + resolution: "nanoid@npm:5.0.4" bin: nanoid: bin/nanoid.js - checksum: 10/94d40a2ed50f894c585f06b142ce40c939bdb10611f202e2d755c69e6d914974bcddc559993465155cd86b703b19f91c99759d8ede2109ae8820ca75cb662b96 + checksum: 10/cf09cca3774f3147100948f7478f75f4c9ee97a4af65c328dd9abbd83b12f8bb35cf9f89a21c330f3b759d667a4cd0140ed84aa5fdd522c61e0d341aeaa7fb6f languageName: node linkType: hard @@ -23974,18 +25208,6 @@ __metadata: languageName: node linkType: hard -"object.groupby@npm:^1.0.1": - version: 1.0.1 - resolution: "object.groupby@npm:1.0.1" - dependencies: - call-bind: "npm:^1.0.2" - define-properties: "npm:^1.2.0" - es-abstract: "npm:^1.22.1" - get-intrinsic: "npm:^1.2.1" - checksum: 10/b7123d91403f95d63978513b23a6079c30f503311f64035fafc863c291c787f287b58df3b21ef002ce1d0b820958c9009dd5a8ab696e0eca325639d345e41524 - languageName: node - linkType: hard - "object.hasown@npm:^1.1.2": version: 1.1.2 resolution: "object.hasown@npm:1.1.2" @@ -24005,7 +25227,7 @@ __metadata: languageName: node linkType: hard -"object.values@npm:^1.1.6, object.values@npm:^1.1.7": +"object.values@npm:^1.1.6": version: 1.1.7 resolution: "object.values@npm:1.1.7" dependencies: @@ -24792,6 +26014,30 @@ __metadata: languageName: node linkType: hard +"playwright-core@npm:1.41.2": + version: 1.41.2 + resolution: "playwright-core@npm:1.41.2" + bin: + playwright-core: cli.js + checksum: 10/77ff881ebb9cc0654edd00c5ff202f5f61aee7a5318e1f12a82e30a3636de21e8b5982fae6138e5bb90115ae509c15a640cf85b10b3e2daebb2bb286da54fd4c + languageName: node + linkType: hard + +"playwright@npm:1.41.2": + version: 1.41.2 + resolution: "playwright@npm:1.41.2" + dependencies: + fsevents: "npm:2.3.2" + playwright-core: "npm:1.41.2" + dependenciesMeta: + fsevents: + optional: true + bin: + playwright: cli.js + checksum: 10/272399f622dc2df90fbef147b9b1cfab5d7a78cc364bdfa98d2bf08faa9894346f58629fe4fef41b108ca2cb203b3970d7886b7f392cb0399c75b521478e2920 + languageName: node + linkType: hard + "pluralize@npm:8.0.0, pluralize@npm:^8.0.0": version: 8.0.0 resolution: "pluralize@npm:8.0.0" @@ -25373,6 +26619,17 @@ __metadata: languageName: node linkType: hard +"pretty-format@npm:^29.6.3": + version: 29.6.3 + resolution: "pretty-format@npm:29.6.3" + dependencies: + "@jest/schemas": "npm:^29.6.3" + ansi-styles: "npm:^5.0.0" + react-is: "npm:^18.0.0" + checksum: 10/4a17a0953b3e2d334e628dc9ff11cfad988e6adb00c074bf9d10f3eb1919ad56b30d987148ac0ce1d0317ad392cd78b39a74b6cbac4e66af609f6127ad3aaaf0 + languageName: node + linkType: hard + "pretty-hrtime@npm:^1.0.3": version: 1.0.3 resolution: "pretty-hrtime@npm:1.0.3" @@ -25698,7 +26955,7 @@ __metadata: languageName: node linkType: hard -"queue-tick@npm:^1.0.1": +"queue-tick@npm:^1.0.0, queue-tick@npm:^1.0.1": version: 1.0.1 resolution: "queue-tick@npm:1.0.1" checksum: 10/f447926c513b64a857906f017a3b350f7d11277e3c8d2a21a42b7998fa1a613d7a829091e12d142bb668905c8f68d8103416c7197856efb0c72fa835b8e254b5 @@ -26312,11 +27569,11 @@ __metadata: linkType: hard "react-hook-form@npm:^7.49.2": - version: 7.50.1 - resolution: "react-hook-form@npm:7.50.1" + version: 7.49.2 + resolution: "react-hook-form@npm:7.49.2" peerDependencies: react: ^16.8.0 || ^17 || ^18 - checksum: 10/54a9daa2143c601a9867e96a2159a0bbe98707b5bbeb5953bfdf3342d2f04bfcaa6169907ed167c5c1f3a044630860d4f43685f4ac4e15b9cd892d1b00d54dde + checksum: 10/7895d65b8458c42d46eb338803bb0fd1aab42fc69ecf80b47846eace9493a10cac5b05c9b744a5f9f1f7969a3e2703fc2118cdab97e49a7798a72d09f106383f languageName: node linkType: hard @@ -26622,16 +27879,16 @@ __metadata: linkType: hard "react-router-dom-v5-compat@npm:^6.10.0": - version: 6.21.3 - resolution: "react-router-dom-v5-compat@npm:6.21.3" + version: 6.10.0 + resolution: "react-router-dom-v5-compat@npm:6.10.0" dependencies: history: "npm:^5.3.0" - react-router: "npm:6.21.3" + react-router: "npm:6.10.0" peerDependencies: react: ">=16.8" react-dom: ">=16.8" react-router-dom: 4 || 5 - checksum: 10/ed40e3e241a8c2a85aa0a95b52b77b9b9667e3f1a48a8a60e0b33e3c615c3b490b6c3a645ea92efc5783251111d039a5c809b779e1a93b9da23de6e97e4e1bd0 + checksum: 10/b86edb22640e25687a843fd46c22b58e147316ee957653e684d9f46a7e71da113a761a2f74c60d42f5d4222e497f825e541a1f4d8872b26edb4b623fb7c87928 languageName: node linkType: hard @@ -26672,14 +27929,14 @@ __metadata: languageName: node linkType: hard -"react-router@npm:6.21.3": - version: 6.21.3 - resolution: "react-router@npm:6.21.3" +"react-router@npm:6.10.0": + version: 6.10.0 + resolution: "react-router@npm:6.10.0" dependencies: - "@remix-run/router": "npm:1.14.2" + "@remix-run/router": "npm:1.5.0" peerDependencies: react: ">=16.8" - checksum: 10/3d5107cfdb440519d84e6ad6d95454e3bf41ec97677b95f7b2a7f281f8ddf191b765cf1b599ead951f3cd33ed4429f140590d74a01cfdf835dc2f812023a978a + checksum: 10/3c78db213d2c67c7ae06125b296889ebf3407963268ef23319312b4d7bf455ecfaa59164be73d6b4e19fb2ef6c2771d7dfe764d5a91cbdbb7c8e84c95aca99cc languageName: node linkType: hard @@ -26896,7 +28153,7 @@ __metadata: languageName: node linkType: hard -"react-use@npm:17.5.0, react-use@npm:^17.4.2": +"react-use@npm:17.5.0": version: 17.5.0 resolution: "react-use@npm:17.5.0" dependencies: @@ -26921,6 +28178,31 @@ __metadata: languageName: node linkType: hard +"react-use@npm:^17.4.2": + version: 17.4.2 + resolution: "react-use@npm:17.4.2" + dependencies: + "@types/js-cookie": "npm:^2.2.6" + "@xobotyi/scrollbar-width": "npm:^1.9.5" + copy-to-clipboard: "npm:^3.3.1" + fast-deep-equal: "npm:^3.1.3" + fast-shallow-equal: "npm:^1.0.0" + js-cookie: "npm:^2.2.1" + nano-css: "npm:^5.6.1" + react-universal-interface: "npm:^0.6.2" + resize-observer-polyfill: "npm:^1.5.1" + screenfull: "npm:^5.1.0" + set-harmonic-interval: "npm:^1.0.1" + throttle-debounce: "npm:^3.0.1" + ts-easing: "npm:^0.2.0" + tslib: "npm:^2.1.0" + peerDependencies: + react: "*" + react-dom: "*" + checksum: 10/56d2da474d949d22eb34ff3ffccf5526986d51ed68a8f4e64f4b79bdcff3f0ea55d322c104e3fc0819b08b8765e8eb3fa47d8b506e9d61ff1fdc7bd1374c17d6 + languageName: node + linkType: hard + "react-virtual@npm:2.10.4, react-virtual@npm:^2.8.2": version: 2.10.4 resolution: "react-virtual@npm:2.10.4" @@ -26976,12 +28258,12 @@ __metadata: linkType: hard "react-zoom-pan-pinch@npm:^3.3.0": - version: 3.4.2 - resolution: "react-zoom-pan-pinch@npm:3.4.2" + version: 3.3.0 + resolution: "react-zoom-pan-pinch@npm:3.3.0" peerDependencies: react: "*" react-dom: "*" - checksum: 10/3014c26523d69eb6a10fb1862374e4fcfb8bcf14614b0a872ca178473634427753d5c7ed6f80233f973236fcd232dd5eec70365c6eb68703e435a9a74b75bf3f + checksum: 10/56e102f603dc5d0dbcff2effe403a1f46b718bde0bbad395fc990db0ef48f25fefb6969fec465948e90471a8e8e702cddaaa93742f77075cb1f4f32b10c1ab43 languageName: node linkType: hard @@ -27528,7 +28810,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.22.1, resolve@npm:^1.22.4": +"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.22.1": version: 1.22.8 resolution: "resolve@npm:1.22.8" dependencies: @@ -27554,7 +28836,7 @@ __metadata: languageName: node linkType: hard -"resolve@patch:resolve@npm%3A^1.10.0#optional!builtin, resolve@patch:resolve@npm%3A^1.12.0#optional!builtin, resolve@patch:resolve@npm%3A^1.14.2#optional!builtin, resolve@patch:resolve@npm%3A^1.19.0#optional!builtin, resolve@patch:resolve@npm%3A^1.20.0#optional!builtin, resolve@patch:resolve@npm%3A^1.22.1#optional!builtin, resolve@patch:resolve@npm%3A^1.22.4#optional!builtin": +"resolve@patch:resolve@npm%3A^1.10.0#optional!builtin, resolve@patch:resolve@npm%3A^1.12.0#optional!builtin, resolve@patch:resolve@npm%3A^1.14.2#optional!builtin, resolve@patch:resolve@npm%3A^1.19.0#optional!builtin, resolve@patch:resolve@npm%3A^1.20.0#optional!builtin, resolve@patch:resolve@npm%3A^1.22.1#optional!builtin": version: 1.22.8 resolution: "resolve@patch:resolve@npm%3A1.22.8#optional!builtin::version=1.22.8&hash=c3c19d" dependencies: @@ -29082,7 +30364,7 @@ __metadata: languageName: node linkType: hard -"streamx@npm:^2.12.0, streamx@npm:^2.12.5, streamx@npm:^2.13.2, streamx@npm:^2.14.0": +"streamx@npm:^2.12.0, streamx@npm:^2.13.2, streamx@npm:^2.14.0": version: 2.15.7 resolution: "streamx@npm:2.15.7" dependencies: @@ -29092,6 +30374,16 @@ __metadata: languageName: node linkType: hard +"streamx@npm:^2.12.5": + version: 2.12.5 + resolution: "streamx@npm:2.12.5" + dependencies: + fast-fifo: "npm:^1.0.0" + queue-tick: "npm:^1.0.0" + checksum: 10/daa5789ca31101684d9266f7ea77294908bd3e55607805ac1657f0cef1ee0a1966bc3988d2ec12c5f68a718d481147fa3ace2525486a1e39ca7155c598917cd1 + languageName: node + linkType: hard + "strict-event-emitter@npm:^0.2.4": version: 0.2.8 resolution: "strict-event-emitter@npm:0.2.8" @@ -29707,7 +30999,7 @@ __metadata: languageName: node linkType: hard -"terser-webpack-plugin@npm:5.3.10, terser-webpack-plugin@npm:^5.1.3, terser-webpack-plugin@npm:^5.3.1, terser-webpack-plugin@npm:^5.3.10, terser-webpack-plugin@npm:^5.3.7": +"terser-webpack-plugin@npm:5.3.10, terser-webpack-plugin@npm:^5.3.10": version: 5.3.10 resolution: "terser-webpack-plugin@npm:5.3.10" dependencies: @@ -29729,6 +31021,28 @@ __metadata: languageName: node linkType: hard +"terser-webpack-plugin@npm:^5.1.3, terser-webpack-plugin@npm:^5.3.1, terser-webpack-plugin@npm:^5.3.7": + version: 5.3.9 + resolution: "terser-webpack-plugin@npm:5.3.9" + dependencies: + "@jridgewell/trace-mapping": "npm:^0.3.17" + jest-worker: "npm:^27.4.5" + schema-utils: "npm:^3.1.1" + serialize-javascript: "npm:^6.0.1" + terser: "npm:^5.16.8" + peerDependencies: + webpack: ^5.1.0 + peerDependenciesMeta: + "@swc/core": + optional: true + esbuild: + optional: true + uglify-js: + optional: true + checksum: 10/339737a407e034b7a9d4a66e31d84d81c10433e41b8eae2ca776f0e47c2048879be482a9aa08e8c27565a2a949bc68f6e07f451bf4d9aa347dd61b3d000f5353 + languageName: node + linkType: hard + "terser@npm:^5.0.0, terser@npm:^5.15.1, terser@npm:^5.26.0, terser@npm:^5.7.2": version: 5.27.0 resolution: "terser@npm:5.27.0" @@ -29743,6 +31057,20 @@ __metadata: languageName: node linkType: hard +"terser@npm:^5.16.8": + version: 5.17.2 + resolution: "terser@npm:5.17.2" + dependencies: + "@jridgewell/source-map": "npm:^0.3.2" + acorn: "npm:^8.5.0" + commander: "npm:^2.20.0" + source-map-support: "npm:~0.5.20" + bin: + terser: bin/terser + checksum: 10/6df529586a4913657547dd8bfe2b5a59704b7acbe4e49ac938a16f829a62226f98dafb19c88b7af66b245ea281ee5dbeec33a41349ac3c035855417b06ebd646 + languageName: node + linkType: hard + "test-exclude@npm:^6.0.0": version: 6.0.0 resolution: "test-exclude@npm:6.0.0" @@ -30198,15 +31526,15 @@ __metadata: languageName: node linkType: hard -"tsconfig-paths@npm:^3.15.0": - version: 3.15.0 - resolution: "tsconfig-paths@npm:3.15.0" +"tsconfig-paths@npm:^3.14.1": + version: 3.14.1 + resolution: "tsconfig-paths@npm:3.14.1" dependencies: "@types/json5": "npm:^0.0.29" - json5: "npm:^1.0.2" + json5: "npm:^1.0.1" minimist: "npm:^1.2.6" strip-bom: "npm:^3.0.0" - checksum: 10/2041beaedc6c271fc3bedd12e0da0cc553e65d030d4ff26044b771fac5752d0460944c0b5e680f670c2868c95c664a256cec960ae528888db6ded83524e33a14 + checksum: 10/51be8bd8f90e49d2f8b3f61f544557e631dd5cee35e247dd316be27d723c9e99de9ce59eb39395ca20f1e43aedfc1fef0272ba25acb0a0e0e9a38cffd692256d languageName: node linkType: hard @@ -30469,6 +31797,16 @@ __metadata: 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: 10/f985d8dd6ae815753d61cb81e434f3a4a5796ac52e423370fca6ad11bcd188df4013d82e3ba3b88c9746745b9341390ba68f862dc9d30bac6465e0699f2a795b + languageName: node + linkType: hard + "typescript@npm:5.2.2": version: 5.2.2 resolution: "typescript@npm:5.2.2" @@ -30489,6 +31827,16 @@ __metadata: languageName: node linkType: hard +"typescript@patch:typescript@npm%3A4.8.4#optional!builtin": + version: 4.8.4 + resolution: "typescript@patch:typescript@npm%3A4.8.4#optional!builtin::version=4.8.4&hash=1a91c8" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 10/5d81fd8cf5152091a0c0b84ebc868de8433583072a340c4899e0fc7ad6a80314b880a1466868c9a6a1f640c3d1f2fe7f41f8c541b99d78c8b414263dfa27eba3 + languageName: node + linkType: hard + "typescript@patch:typescript@npm%3A5.2.2#optional!builtin": version: 5.2.2 resolution: "typescript@patch:typescript@npm%3A5.2.2#optional!builtin::version=5.2.2&hash=f3b441" @@ -30924,7 +32272,7 @@ __metadata: languageName: node linkType: hard -"uuid@npm:9.0.1, uuid@npm:^9.0.0": +"uuid@npm:9.0.1, uuid@npm:^9.0.0, uuid@npm:^9.0.1": version: 9.0.1 resolution: "uuid@npm:9.0.1" bin: @@ -31412,7 +32760,7 @@ __metadata: languageName: node linkType: hard -"webpack-merge@npm:5.10.0, webpack-merge@npm:^5.7.3": +"webpack-merge@npm:5.10.0": version: 5.10.0 resolution: "webpack-merge@npm:5.10.0" dependencies: @@ -31423,6 +32771,16 @@ __metadata: languageName: node linkType: hard +"webpack-merge@npm:^5.7.3": + version: 5.9.0 + resolution: "webpack-merge@npm:5.9.0" + dependencies: + clone-deep: "npm:^4.0.1" + wildcard: "npm:^2.0.0" + checksum: 10/d23dd1f0bad0b9821bf58443d2d29097d65cd9353046c2d8a6d7b57877ec19cf64be57cc7ef2a371a15cf9264fe6eaf8dea4015dc87487e664ffab2a28329d56 + languageName: node + linkType: hard + "webpack-sources@npm:^1.4.3": version: 1.4.3 resolution: "webpack-sources@npm:1.4.3" @@ -31457,9 +32815,9 @@ __metadata: languageName: node linkType: hard -"webpack@npm:5, webpack@npm:5.90.2, webpack@npm:^5": - version: 5.90.2 - resolution: "webpack@npm:5.90.2" +"webpack@npm:5, webpack@npm:^5": + version: 5.90.1 + resolution: "webpack@npm:5.90.1" dependencies: "@types/eslint-scope": "npm:^3.7.3" "@types/estree": "npm:^1.0.5" @@ -31490,7 +32848,7 @@ __metadata: optional: true bin: webpack: bin/webpack.js - checksum: 10/4eaeed1255c9c7738921c4ce4facdb3b78dbfcb3441496942f6d160a41fbcebd24fb2c6dbb64739b357c5ff78e5a298f6c82eca482438b95130a3ba4e16d084a + checksum: 10/6ad23518123f1742238177920cefa61152d981f986adac5901236845c86ba9bb375a3ba75e188925c856c3d2a76a2ba119e95b8a608a51424968389041089075 languageName: node linkType: hard @@ -31568,6 +32926,43 @@ __metadata: languageName: node linkType: hard +"webpack@npm:5.90.2": + version: 5.90.2 + resolution: "webpack@npm:5.90.2" + dependencies: + "@types/eslint-scope": "npm:^3.7.3" + "@types/estree": "npm:^1.0.5" + "@webassemblyjs/ast": "npm:^1.11.5" + "@webassemblyjs/wasm-edit": "npm:^1.11.5" + "@webassemblyjs/wasm-parser": "npm:^1.11.5" + acorn: "npm:^8.7.1" + acorn-import-assertions: "npm:^1.9.0" + browserslist: "npm:^4.21.10" + chrome-trace-event: "npm:^1.0.2" + enhanced-resolve: "npm:^5.15.0" + es-module-lexer: "npm:^1.2.1" + eslint-scope: "npm:5.1.1" + events: "npm:^3.2.0" + glob-to-regexp: "npm:^0.4.1" + graceful-fs: "npm:^4.2.9" + json-parse-even-better-errors: "npm:^2.3.1" + loader-runner: "npm:^4.2.0" + mime-types: "npm:^2.1.27" + neo-async: "npm:^2.6.2" + schema-utils: "npm:^3.2.0" + tapable: "npm:^2.1.1" + terser-webpack-plugin: "npm:^5.3.10" + watchpack: "npm:^2.4.0" + webpack-sources: "npm:^3.2.3" + peerDependenciesMeta: + webpack-cli: + optional: true + bin: + webpack: bin/webpack.js + checksum: 10/4eaeed1255c9c7738921c4ce4facdb3b78dbfcb3441496942f6d160a41fbcebd24fb2c6dbb64739b357c5ff78e5a298f6c82eca482438b95130a3ba4e16d084a + languageName: node + linkType: hard + "websocket-driver@npm:>=0.5.1, websocket-driver@npm:^0.7.4": version: 0.7.4 resolution: "websocket-driver@npm:0.7.4" @@ -31674,7 +33069,7 @@ __metadata: languageName: node linkType: hard -"which-typed-array@npm:^1.1.11, which-typed-array@npm:^1.1.2, which-typed-array@npm:^1.1.9": +"which-typed-array@npm:^1.1.11": version: 1.1.11 resolution: "which-typed-array@npm:1.1.11" dependencies: @@ -31687,6 +33082,20 @@ __metadata: languageName: node linkType: hard +"which-typed-array@npm:^1.1.2, which-typed-array@npm:^1.1.9": + version: 1.1.9 + resolution: "which-typed-array@npm:1.1.9" + dependencies: + available-typed-arrays: "npm:^1.0.5" + call-bind: "npm:^1.0.2" + for-each: "npm:^0.3.3" + gopd: "npm:^1.0.1" + has-tostringtag: "npm:^1.0.0" + is-typed-array: "npm:^1.1.10" + checksum: 10/90ef760a09dcffc479138a6bc77fd2933a81a41d531f4886ae212f6edb54a0645a43a6c24de2c096aea910430035ac56b3d22a06f3d64e5163fa178d0f24e08e + languageName: node + linkType: hard + "which@npm:^1.3.1": version: 1.3.1 resolution: "which@npm:1.3.1" @@ -31979,6 +33388,13 @@ __metadata: linkType: hard "yaml@npm:^2.0.0": + version: 2.3.1 + resolution: "yaml@npm:2.3.1" + checksum: 10/66501d597e43766eb94dc175d28ec8b2c63087d6a78783e59b4218eee32b9172740f9f27d54b7bc0ca8af61422f7134929f9974faeaac99d583787e793852fd2 + languageName: node + linkType: hard + +"yaml@npm:^2.3.4": version: 2.3.4 resolution: "yaml@npm:2.3.4" checksum: 10/f8207ce43065a22268a2806ea6a0fa3974c6fde92b4b2fa0082357e487bc333e85dc518910007e7ac001b532c7c84bd3eccb6c7757e94182b564028b0008f44b From 7730a38474bbdb622b26cbc7f74acec6fbda4ed1 Mon Sep 17 00:00:00 2001 From: Andreas Christou Date: Fri, 23 Feb 2024 13:11:04 +0000 Subject: [PATCH 0125/1406] CI: Remove inline-builds flag (#83306) Remove inline-builds --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 57c5bbacf9..27f52d1757 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,7 +22,7 @@ COPY public public RUN apk add --no-cache make build-base python3 -RUN yarn install --immutable --inline-builds +RUN yarn install --immutable COPY tsconfig.json .eslintrc .editorconfig .browserslistrc .prettierrc.js ./ COPY public public From 43b186a52ed3bb9a6a2e3fe6d26588387a001c66 Mon Sep 17 00:00:00 2001 From: Daniel Reimhult <118008016+raymalt@users.noreply.github.com> Date: Fri, 23 Feb 2024 14:24:12 +0100 Subject: [PATCH 0126/1406] Unit: Add SI prefix for empty unit (#79897) * Unit: Add SI prefix for empty unit * Units: Change name from SI prefix to SI short --- packages/grafana-data/src/valueFormats/categories.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/grafana-data/src/valueFormats/categories.ts b/packages/grafana-data/src/valueFormats/categories.ts index 8343845662..79ef525e24 100644 --- a/packages/grafana-data/src/valueFormats/categories.ts +++ b/packages/grafana-data/src/valueFormats/categories.ts @@ -45,6 +45,7 @@ export const getCategories = (): ValueFormatCategory[] => [ id: 'short', fn: scaledUnits(1000, ['', ' K', ' Mil', ' Bil', ' Tri', ' Quadr', ' Quint', ' Sext', ' Sept']), }, + { name: 'SI short', id: 'sishort', fn: SIPrefix('') }, { name: 'Percent (0-100)', id: 'percent', fn: toPercent }, { name: 'Percent (0.0-1.0)', id: 'percentunit', fn: toPercentUnit }, { name: 'Humidity (%H)', id: 'humidity', fn: toFixedUnit('%H') }, From 49d3cb29eb3c0caac8e2cd549919430146be799d Mon Sep 17 00:00:00 2001 From: Todd Treece <360020+toddtreece@users.noreply.github.com> Date: Fri, 23 Feb 2024 08:54:24 -0500 Subject: [PATCH 0127/1406] Chore: Add go workspace (#83191) --------- Co-authored-by: ismail simsek --- .drone.yml | 30 +- .github/CODEOWNERS | 2 + go.mod | 7 - go.sum | 6 + go.work | 10 + go.work.sum | 862 +++++++++++++++++++++++++++++++++++ pkg/util/xorm/go.mod | 17 +- pkg/util/xorm/go.sum | 22 +- pkg/util/xorm/xorm_test.go | 17 + scripts/drone/steps/lib.star | 6 +- 10 files changed, 943 insertions(+), 36 deletions(-) create mode 100644 go.work create mode 100644 go.work.sum create mode 100644 pkg/util/xorm/xorm_test.go diff --git a/.drone.yml b/.drone.yml index 77c10b1237..d632880afc 100644 --- a/.drone.yml +++ b/.drone.yml @@ -343,7 +343,8 @@ steps: name: wire-install - commands: - apk add --update build-base shared-mime-info shared-mime-info-lang - - go test -tags requires_buildifer -short -covermode=atomic -timeout=5m ./pkg/... + - go list -f '{{.Dir}}/...' -m | xargs go test -tags requires_buildifer -short -covermode=atomic + -timeout=5m depends_on: - wire-install image: golang:1.21.6-alpine @@ -1018,7 +1019,8 @@ steps: - commands: - apk add --update build-base - go clean -testcache - - go test -run IntegrationRedis -covermode=atomic -timeout=2m ./pkg/... + - go list -f '{{.Dir}}/...' -m | xargs go test -run IntegrationRedis -covermode=atomic + -timeout=2m depends_on: - wire-install - wait-for-redis @@ -1033,7 +1035,8 @@ steps: - commands: - apk add --update build-base - go clean -testcache - - go test -run IntegrationMemcached -covermode=atomic -timeout=2m ./pkg/... + - go list -f '{{.Dir}}/...' -m | xargs go test -run IntegrationMemcached -covermode=atomic + -timeout=2m depends_on: - wire-install - wait-for-memcached @@ -1701,7 +1704,8 @@ steps: name: wire-install - commands: - apk add --update build-base shared-mime-info shared-mime-info-lang - - go test -tags requires_buildifer -short -covermode=atomic -timeout=5m ./pkg/... + - go list -f '{{.Dir}}/...' -m | xargs go test -tags requires_buildifer -short -covermode=atomic + -timeout=5m depends_on: - wire-install image: golang:1.21.6-alpine @@ -2446,7 +2450,8 @@ steps: - commands: - apk add --update build-base - go clean -testcache - - go test -run IntegrationRedis -covermode=atomic -timeout=2m ./pkg/... + - go list -f '{{.Dir}}/...' -m | xargs go test -run IntegrationRedis -covermode=atomic + -timeout=2m depends_on: - wire-install - wait-for-redis @@ -2461,7 +2466,8 @@ steps: - commands: - apk add --update build-base - go clean -testcache - - go test -run IntegrationMemcached -covermode=atomic -timeout=2m ./pkg/... + - go list -f '{{.Dir}}/...' -m | xargs go test -run IntegrationMemcached -covermode=atomic + -timeout=2m depends_on: - wire-install - wait-for-memcached @@ -3204,7 +3210,8 @@ steps: name: wire-install - commands: - apk add --update build-base shared-mime-info shared-mime-info-lang - - go test -tags requires_buildifer -short -covermode=atomic -timeout=5m ./pkg/... + - go list -f '{{.Dir}}/...' -m | xargs go test -tags requires_buildifer -short -covermode=atomic + -timeout=5m depends_on: - wire-install image: golang:1.21.6-alpine @@ -3626,7 +3633,8 @@ steps: name: wire-install - commands: - apk add --update build-base shared-mime-info shared-mime-info-lang - - go test -tags requires_buildifer -short -covermode=atomic -timeout=5m ./pkg/... + - go list -f '{{.Dir}}/...' -m | xargs go test -tags requires_buildifer -short -covermode=atomic + -timeout=5m depends_on: - wire-install image: golang:1.21.6-alpine @@ -4233,7 +4241,8 @@ steps: - commands: - apk add --update build-base - go clean -testcache - - go test -run IntegrationRedis -covermode=atomic -timeout=2m ./pkg/... + - go list -f '{{.Dir}}/...' -m | xargs go test -run IntegrationRedis -covermode=atomic + -timeout=2m depends_on: - wire-install - wait-for-redis @@ -4248,7 +4257,8 @@ steps: - commands: - apk add --update build-base - go clean -testcache - - go test -run IntegrationMemcached -covermode=atomic -timeout=2m ./pkg/... + - go list -f '{{.Dir}}/...' -m | xargs go test -run IntegrationMemcached -covermode=atomic + -timeout=2m depends_on: - wire-install - wait-for-memcached diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index aadbebd05b..250033509e 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -57,6 +57,8 @@ # Backend code /go.mod @grafana/backend-platform /go.sum @grafana/backend-platform +/go.work @grafana/grafana-app-platform-squad +/go.work.sum @grafana/grafana-app-platform-squad /.bingo/ @grafana/backend-platform /pkg/README.md @grafana/backend-platform /pkg/ruleguard.rules.go @grafana/backend-platform diff --git a/go.mod b/go.mod index 7720d65450..6e11a415ab 100644 --- a/go.mod +++ b/go.mod @@ -508,13 +508,6 @@ replace github.com/crewjam/saml => github.com/grafana/saml v0.4.15-0.20231025143 // No harm to Thema because it's only a dependency in its main package. replace github.com/hashicorp/go-hclog => github.com/hashicorp/go-hclog v0.16.1 -// This is a patched v0.8.2 intended to fix session.Find (and others) silently ignoring SQLITE_BUSY errors. This could -// happen, for example, during a read when the sqlite db is under heavy write load. -// This patch cherry picks compatible fixes from upstream xorm PR#1998 and can be reverted on upgrade to xorm v1.2.0+. -// This has also been patched to support the azuresql driver that is a thin wrapper for the mssql driver with azure authentication support. -//replace xorm.io/xorm => github.com/grafana/xorm v0.8.3-0.20230627081928-d04aa38aa209 -replace xorm.io/xorm => ./pkg/util/xorm - // 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.20240208102907-e82436ce63e6 diff --git a/go.sum b/go.sum index 23d3e67d15..621e66ce90 100644 --- a/go.sum +++ b/go.sum @@ -1650,6 +1650,8 @@ github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9/go.mod h1:GgB8SF9nRG github.com/deepmap/oapi-codegen v1.12.4 h1:pPmn6qI9MuOtCz82WY2Xaw46EQjgvxednXXrP7g5Q2s= github.com/deepmap/oapi-codegen v1.12.4/go.mod h1:3lgHGMu6myQ2vqbbTXH2H1o4eXFTGnFiDaOaKKl5yas= github.com/denisenkom/go-mssqldb v0.0.0-20190515213511-eb9f6a1743f3/go.mod h1:zAg7JM8CkOJ43xKXIj7eRO9kmWm/TW578qo+oDO6tuM= +github.com/denisenkom/go-mssqldb v0.0.0-20190707035753-2be1aa521ff4/go.mod h1:zAg7JM8CkOJ43xKXIj7eRO9kmWm/TW578qo+oDO6tuM= +github.com/denisenkom/go-mssqldb v0.12.0 h1:VtrkII767ttSPNRfFekePK3sctr+joXgO58stqQbtUA= github.com/denisenkom/go-mssqldb v0.12.0/go.mod h1:iiK0YP1ZeepvmBQk/QpLEhhTNJgfzrpArPY/aFvc9yU= github.com/dennwc/varint v1.0.0 h1:kGNFFSSw8ToIy3obO/kKr8U9GZYUAxQEVuix4zfDWzE= github.com/dennwc/varint v1.0.0/go.mod h1:hnItb35rvZvJrbTALZtY/iQfDs48JKRG1RPpgziApxA= @@ -3673,6 +3675,7 @@ github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= +github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs= github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b/go.mod h1:T3BPAOm2cqquPa0MKWeNkmOM5RQsRhkrwMWonFMN7fE= go.elastic.co/apm v1.8.0/go.mod h1:tCw6CkOJgkWnzEthFN9HUP1uL3Gjc/Ur6m7gRPLaoH0= @@ -5132,5 +5135,8 @@ sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= xorm.io/builder v0.3.6 h1:ha28mQ2M+TFx96Hxo+iq6tQgnkC9IZkM6D8w9sKHHF8= xorm.io/builder v0.3.6/go.mod h1:LEFAPISnRzG+zxaxj2vPicRwz67BdhFreKg8yv8/TgU= +xorm.io/core v0.7.2/go.mod h1:jJfd0UAEzZ4t87nbQYtVjmqpIODugN6PD2D9E+dJvdM= xorm.io/core v0.7.3 h1:W8ws1PlrnkS1CZU1YWaYLMQcQilwAmQXU0BJDJon+H0= xorm.io/core v0.7.3/go.mod h1:jJfd0UAEzZ4t87nbQYtVjmqpIODugN6PD2D9E+dJvdM= +xorm.io/xorm v0.8.2 h1:nbg1AyWn7iLrwp0Dqg8IrYOBkBYYJ85ry9bvZLVl4Ok= +xorm.io/xorm v0.8.2/go.mod h1:ZkJLEYLoVyg7amJK/5r779bHyzs2AU8f8VMiP6BM7uY= diff --git a/go.work b/go.work new file mode 100644 index 0000000000..1c986619a4 --- /dev/null +++ b/go.work @@ -0,0 +1,10 @@ +go 1.21.0 + +use ( + . + ./pkg/util/xorm +) + +// when we release xorm we would like to release it like github.com/grafana/grafana/pkg/util/xorm +// but we don't want to change all the imports. so we use replace to handle this situation +replace xorm.io/xorm => ./pkg/util/xorm diff --git a/go.work.sum b/go.work.sum new file mode 100644 index 0000000000..de3ead0b03 --- /dev/null +++ b/go.work.sum @@ -0,0 +1,862 @@ +bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898 h1:SC+c6A1qTFstO9qmB86mPV2IpYme/2ZoEQ0hrP+wo+Q= +buf.build/gen/go/grpc-ecosystem/grpc-gateway/bufbuild/connect-go v1.4.1-20221127060915-a1ecdc58eccd.1 h1:vp9EaPFSb75qe/793x58yE5fY1IJ/gdxb/kcDUzavtI= +buf.build/gen/go/grpc-ecosystem/grpc-gateway/bufbuild/connect-go v1.4.1-20221127060915-a1ecdc58eccd.1/go.mod h1:YDq2B5X5BChU0lxAG5MxHpDb8mx1fv9OGtF2mwOe7hY= +buf.build/gen/go/grpc-ecosystem/grpc-gateway/protocolbuffers/go v1.28.1-20221127060915-a1ecdc58eccd.4 h1:z3Xc9n8yZ5k/Xr4ZTuff76TAYP20dWy7ZBV4cGIpbkM= +cloud.google.com/go/accessapproval v1.7.4 h1:ZvLvJ952zK8pFHINjpMBY5k7LTAp/6pBf50RDMRgBUI= +cloud.google.com/go/accesscontextmanager v1.8.4 h1:Yo4g2XrBETBCqyWIibN3NHNPQKUfQqti0lI+70rubeE= +cloud.google.com/go/aiplatform v1.57.0 h1:WcZ6wDf/1qBWatmGM9Z+2BTiNjQQX54k2BekHUj93DQ= +cloud.google.com/go/aiplatform v1.57.0/go.mod h1:pwZMGvqe0JRkI1GWSZCtnAfrR4K1bv65IHILGA//VEU= +cloud.google.com/go/analytics v0.21.6 h1:fnV7B8lqyEYxCU0LKk+vUL7mTlqRAq4uFlIthIdr/iA= +cloud.google.com/go/apigateway v1.6.4 h1:VVIxCtVerchHienSlaGzV6XJGtEM9828Erzyr3miUGs= +cloud.google.com/go/apigeeconnect v1.6.4 h1:jSoGITWKgAj/ssVogNE9SdsTqcXnryPzsulENSRlusI= +cloud.google.com/go/apigeeregistry v0.8.2 h1:DSaD1iiqvELag+lV4VnnqUUFd8GXELu01tKVdWZrviE= +cloud.google.com/go/apikeys v0.6.0 h1:B9CdHFZTFjVti89tmyXXrO+7vSNo2jvZuHG8zD5trdQ= +cloud.google.com/go/appengine v1.8.4 h1:Qub3fqR7iA1daJWdzjp/Q0Jz0fUG0JbMc7Ui4E9IX/E= +cloud.google.com/go/area120 v0.8.4 h1:YnSO8m02pOIo6AEOgiOoUDVbw4pf+bg2KLHi4rky320= +cloud.google.com/go/artifactregistry v1.14.6 h1:/hQaadYytMdA5zBh+RciIrXZQBWK4vN7EUsrQHG+/t8= +cloud.google.com/go/asset v1.15.3 h1:uI8Bdm81s0esVWbWrTHcjFDFKNOa9aB7rI1vud1hO84= +cloud.google.com/go/assuredworkloads v1.11.4 h1:FsLSkmYYeNuzDm8L4YPfLWV+lQaUrJmH5OuD37t1k20= +cloud.google.com/go/automl v1.13.4 h1:i9tOKXX+1gE7+rHpWKjiuPfGBVIYoWvLNIGpWgPtF58= +cloud.google.com/go/baremetalsolution v1.2.3 h1:oQiFYYCe0vwp7J8ZmF6siVKEumWtiPFJMJcGuyDVRUk= +cloud.google.com/go/batch v1.7.0 h1:AxuSPoL2fWn/rUyvWeNCNd0V2WCr+iHRCU9QO1PUmpY= +cloud.google.com/go/batch v1.7.0/go.mod h1:J64gD4vsNSA2O5TtDB5AAux3nJ9iV8U3ilg3JDBYejU= +cloud.google.com/go/beyondcorp v1.0.3 h1:VXf9SnrnSmj2BF2cHkoTHvOUp8gjsz1KJFOMW7czdsY= +cloud.google.com/go/bigquery v1.57.1 h1:FiULdbbzUxWD0Y4ZGPSVCDLvqRSyCIO6zKV7E2nf5uA= +cloud.google.com/go/billing v1.18.0 h1:GvKy4xLy1zF1XPbwP5NJb2HjRxhnhxjjXxvyZ1S/IAo= +cloud.google.com/go/billing v1.18.0/go.mod h1:5DOYQStCxquGprqfuid/7haD7th74kyMBHkjO/OvDtk= +cloud.google.com/go/binaryauthorization v1.8.0 h1:PHS89lcFayWIEe0/s2jTBiEOtqghCxzc7y7bRNlifBs= +cloud.google.com/go/binaryauthorization v1.8.0/go.mod h1:VQ/nUGRKhrStlGr+8GMS8f6/vznYLkdK5vaKfdCIpvU= +cloud.google.com/go/certificatemanager v1.7.4 h1:5YMQ3Q+dqGpwUZ9X5sipsOQ1fLPsxod9HNq0+nrqc6I= +cloud.google.com/go/channel v1.17.3 h1:Rd4+fBrjiN6tZ4TR8R/38elkyEkz6oogGDr7jDyjmMY= +cloud.google.com/go/cloudbuild v1.15.0 h1:9IHfEMWdCklJ1cwouoiQrnxmP0q3pH7JUt8Hqx4Qbck= +cloud.google.com/go/clouddms v1.7.3 h1:xe/wJKz55VO1+L891a1EG9lVUgfHr9Ju/I3xh1nwF84= +cloud.google.com/go/cloudtasks v1.12.4 h1:5xXuFfAjg0Z5Wb81j2GAbB3e0bwroCeSF+5jBn/L650= +cloud.google.com/go/contactcenterinsights v1.12.1 h1:EiGBeejtDDtr3JXt9W7xlhXyZ+REB5k2tBgVPVtmNb0= +cloud.google.com/go/contactcenterinsights v1.12.1/go.mod h1:HHX5wrz5LHVAwfI2smIotQG9x8Qd6gYilaHcLLLmNis= +cloud.google.com/go/container v1.29.0 h1:jIltU529R2zBFvP8rhiG1mgeTcnT27KhU0H/1d6SQRg= +cloud.google.com/go/container v1.29.0/go.mod h1:b1A1gJeTBXVLQ6GGw9/9M4FG94BEGsqJ5+t4d/3N7O4= +cloud.google.com/go/containeranalysis v0.11.3 h1:5rhYLX+3a01drpREqBZVXR9YmWH45RnML++8NsCtuD8= +cloud.google.com/go/datacatalog v1.19.0 h1:rbYNmHwvAOOwnW2FPXYkaK3Mf1MmGqRzK0mMiIEyLdo= +cloud.google.com/go/dataflow v0.9.4 h1:7VmCNWcPJBS/srN2QnStTB6nu4Eb5TMcpkmtaPVhRt4= +cloud.google.com/go/dataform v0.9.1 h1:jV+EsDamGX6cE127+QAcCR/lergVeeZdEQ6DdrxW3sQ= +cloud.google.com/go/datafusion v1.7.4 h1:Q90alBEYlMi66zL5gMSGQHfbZLB55mOAg03DhwTTfsk= +cloud.google.com/go/datalabeling v0.8.4 h1:zrq4uMmunf2KFDl/7dS6iCDBBAxBnKVDyw6+ajz3yu0= +cloud.google.com/go/dataplex v1.13.0 h1:ACVOuxwe7gP0SqEso9SLyXbcZNk5l8hjcTX+XLntI5s= +cloud.google.com/go/dataplex v1.13.0/go.mod h1:mHJYQQ2VEJHsyoC0OdNyy988DvEbPhqFs5OOLffLX0c= +cloud.google.com/go/dataproc v1.12.0 h1:W47qHL3W4BPkAIbk4SWmIERwsWBaNnWm0P2sdx3YgGU= +cloud.google.com/go/dataproc/v2 v2.3.0 h1:tTVP9tTxmc8fixxOd/8s6Q6Pz/+yzn7r7XdZHretQH0= +cloud.google.com/go/dataqna v0.8.4 h1:NJnu1kAPamZDs/if3bJ3+Wb6tjADHKL83NUWsaIp2zg= +cloud.google.com/go/datastore v1.15.0 h1:0P9WcsQeTWjuD1H14JIY7XQscIPQ4Laje8ti96IC5vg= +cloud.google.com/go/datastream v1.10.3 h1:Z2sKPIB7bT2kMW5Uhxy44ZgdJzxzE5uKjavoW+EuHEE= +cloud.google.com/go/deploy v1.16.0 h1:5OVjzm8MPC5kP+Ywbs0mdE0O7AXvAUXksSyHAyMFyMg= +cloud.google.com/go/deploy v1.16.0/go.mod h1:e5XOUI5D+YGldyLNZ21wbp9S8otJbBE4i88PtO9x/2g= +cloud.google.com/go/dialogflow v1.47.0 h1:tLCWad8HZhlyUNfDzDP5m+oH6h/1Uvw/ei7B9AnsWMk= +cloud.google.com/go/dialogflow v1.47.0/go.mod h1:mHly4vU7cPXVweuB5R0zsYKPMzy240aQdAu06SqBbAQ= +cloud.google.com/go/dlp v1.11.1 h1:OFlXedmPP/5//X1hBEeq3D9kUVm9fb6ywYANlpv/EsQ= +cloud.google.com/go/documentai v1.23.6 h1:0/S3AhS23+0qaFe3tkgMmS3STxgDgmE1jg4TvaDOZ9g= +cloud.google.com/go/documentai v1.23.6/go.mod h1:ghzBsyVTiVdkfKaUCum/9bGBEyBjDO4GfooEcYKhN+g= +cloud.google.com/go/domains v0.9.4 h1:ua4GvsDztZ5F3xqjeLKVRDeOvJshf5QFgWGg1CKti3A= +cloud.google.com/go/edgecontainer v1.1.4 h1:Szy3Q/N6bqgQGyxqjI+6xJZbmvPvnFHp3UZr95DKcQ0= +cloud.google.com/go/errorreporting v0.3.0 h1:kj1XEWMu8P0qlLhm3FwcaFsUvXChV/OraZwA70trRR0= +cloud.google.com/go/essentialcontacts v1.6.5 h1:S2if6wkjR4JCEAfDtIiYtD+sTz/oXjh2NUG4cgT1y/Q= +cloud.google.com/go/eventarc v1.13.3 h1:+pFmO4eu4dOVipSaFBLkmqrRYG94Xl/TQZFOeohkuqU= +cloud.google.com/go/filestore v1.8.0 h1:/+wUEGwk3x3Kxomi2cP5dsR8+SIXxo7M0THDjreFSYo= +cloud.google.com/go/firestore v1.14.0 h1:8aLcKnMPoldYU3YHgu4t2exrKhLQkqaXAGqT0ljrFVw= +cloud.google.com/go/functions v1.15.4 h1:ZjdiV3MyumRM6++1Ixu6N0VV9LAGlCX4AhW6Yjr1t+U= +cloud.google.com/go/gaming v1.10.1 h1:5qZmZEWzMf8GEFgm9NeC3bjFRpt7x4S6U7oLbxaf7N8= +cloud.google.com/go/gkebackup v1.3.4 h1:KhnOrr9A1tXYIYeXKqCKbCI8TL2ZNGiD3dm+d7BDUBg= +cloud.google.com/go/gkeconnect v0.8.4 h1:1JLpZl31YhQDQeJ98tK6QiwTpgHFYRJwpntggpQQWis= +cloud.google.com/go/gkehub v0.14.4 h1:J5tYUtb3r0cl2mM7+YHvV32eL+uZQ7lONyUZnPikCEo= +cloud.google.com/go/gkemulticloud v1.0.3 h1:NmJsNX9uQ2CT78957xnjXZb26TDIMvv+d5W2vVUt0Pg= +cloud.google.com/go/grafeas v0.3.0 h1:oyTL/KjiUeBs9eYLw/40cpSZglUC+0F7X4iu/8t7NWs= +cloud.google.com/go/gsuiteaddons v1.6.4 h1:uuw2Xd37yHftViSI8J2hUcCS8S7SH3ZWH09sUDLW30Q= +cloud.google.com/go/iap v1.9.3 h1:M4vDbQ4TLXdaljXVZSwW7XtxpwXUUarY2lIs66m0aCM= +cloud.google.com/go/ids v1.4.4 h1:VuFqv2ctf/A7AyKlNxVvlHTzjrEvumWaZflUzBPz/M4= +cloud.google.com/go/iot v1.7.4 h1:m1WljtkZnvLTIRYW1YTOv5A6H1yKgLHR6nU7O8yf27w= +cloud.google.com/go/language v1.12.2 h1:zg9uq2yS9PGIOdc0Kz/l+zMtOlxKWonZjjo5w5YPG2A= +cloud.google.com/go/lifesciences v0.9.4 h1:rZEI/UxcxVKEzyoRS/kdJ1VoolNItRWjNN0Uk9tfexg= +cloud.google.com/go/logging v1.8.1 h1:26skQWPeYhvIasWKm48+Eq7oUqdcdbwsCVwz5Ys0FvU= +cloud.google.com/go/longrunning v0.5.4 h1:w8xEcbZodnA2BbW6sVirkkoC+1gP8wS57EUUgGS0GVg= +cloud.google.com/go/managedidentities v1.6.4 h1:SF/u1IJduMqQQdJA4MDyivlIQ4SrV5qAawkr/ZEREkY= +cloud.google.com/go/maps v1.6.2 h1:WxxLo//b60nNFESefLgaBQevu8QGUmRV3+noOjCfIHs= +cloud.google.com/go/maps v1.6.2/go.mod h1:4+buOHhYXFBp58Zj/K+Lc1rCmJssxxF4pJ5CJnhdz18= +cloud.google.com/go/mediatranslation v0.8.4 h1:VRCQfZB4s6jN0CSy7+cO3m4ewNwgVnaePanVCQh/9Z4= +cloud.google.com/go/memcache v1.10.4 h1:cdex/ayDd294XBj2cGeMe6Y+H1JvhN8y78B9UW7pxuQ= +cloud.google.com/go/metastore v1.13.3 h1:94l/Yxg9oBZjin2bzI79oK05feYefieDq0o5fjLSkC8= +cloud.google.com/go/monitoring v1.16.3 h1:mf2SN9qSoBtIgiMA4R/y4VADPWZA7VCNJA079qLaZQ8= +cloud.google.com/go/networkconnectivity v1.14.3 h1:e9lUkCe2BexsqsUc2bjV8+gFBpQa54J+/F3qKVtW+wA= +cloud.google.com/go/networkmanagement v1.9.3 h1:HsQk4FNKJUX04k3OI6gUsoveiHMGvDRqlaFM2xGyvqU= +cloud.google.com/go/networksecurity v0.9.4 h1:947tNIPnj1bMGTIEBo3fc4QrrFKS5hh0bFVsHmFm4Vo= +cloud.google.com/go/notebooks v1.11.2 h1:eTOTfNL1yM6L/PCtquJwjWg7ZZGR0URFaFgbs8kllbM= +cloud.google.com/go/optimization v1.6.2 h1:iFsoexcp13cGT3k/Hv8PA5aK+FP7FnbhwDO9llnruas= +cloud.google.com/go/orchestration v1.8.4 h1:kgwZ2f6qMMYIVBtUGGoU8yjYWwMTHDanLwM/CQCFaoQ= +cloud.google.com/go/orgpolicy v1.11.4 h1:RWuXQDr9GDYhjmrredQJC7aY7cbyqP9ZuLbq5GJGves= +cloud.google.com/go/osconfig v1.12.4 h1:OrRCIYEAbrbXdhm13/JINn9pQchvTTIzgmOCA7uJw8I= +cloud.google.com/go/oslogin v1.12.2 h1:NP/KgsD9+0r9hmHC5wKye0vJXVwdciv219DtYKYjgqE= +cloud.google.com/go/phishingprotection v0.8.4 h1:sPLUQkHq6b4AL0czSJZ0jd6vL55GSTHz2B3Md+TCZI0= +cloud.google.com/go/policytroubleshooter v1.10.2 h1:sq+ScLP83d7GJy9+wpwYJVnY+q6xNTXwOdRIuYjvHT4= +cloud.google.com/go/privatecatalog v0.9.4 h1:Vo10IpWKbNvc/z/QZPVXgCiwfjpWoZ/wbgful4Uh/4E= +cloud.google.com/go/pubsub v1.33.0 h1:6SPCPvWav64tj0sVX/+npCBKhUi/UjJehy9op/V3p2g= +cloud.google.com/go/pubsublite v1.8.1 h1:pX+idpWMIH30/K7c0epN6V703xpIcMXWRjKJsz0tYGY= +cloud.google.com/go/recaptchaenterprise v1.3.1 h1:u6EznTGzIdsyOsvm+Xkw0aSuKFXQlyjGE9a4exk6iNQ= +cloud.google.com/go/recaptchaenterprise/v2 v2.9.0 h1:Zrd4LvT9PaW91X/Z13H0i5RKEv9suCLuk8zp+bfOpN4= +cloud.google.com/go/recaptchaenterprise/v2 v2.9.0/go.mod h1:Dak54rw6lC2gBY8FBznpOCAR58wKf+R+ZSJRoeJok4w= +cloud.google.com/go/recommendationengine v0.8.4 h1:JRiwe4hvu3auuh2hujiTc2qNgPPfVp+Q8KOpsXlEzKQ= +cloud.google.com/go/recommender v1.11.3 h1:VndmgyS/J3+izR8V8BHa7HV/uun8//ivQ3k5eVKKyyM= +cloud.google.com/go/redis v1.14.1 h1:J9cEHxG9YLmA9o4jTSvWt/RuVEn6MTrPlYSCRHujxDQ= +cloud.google.com/go/resourcemanager v1.9.4 h1:JwZ7Ggle54XQ/FVYSBrMLOQIKoIT/uer8mmNvNLK51k= +cloud.google.com/go/resourcesettings v1.6.4 h1:yTIL2CsZswmMfFyx2Ic77oLVzfBFoWBYgpkgiSPnC4Y= +cloud.google.com/go/retail v1.14.4 h1:geqdX1FNqqL2p0ADXjPpw8lq986iv5GrVcieTYafuJQ= +cloud.google.com/go/run v1.3.3 h1:qdfZteAm+vgzN1iXzILo3nJFQbzziudkJrvd9wCf3FQ= +cloud.google.com/go/scheduler v1.10.5 h1:eMEettHlFhG5pXsoHouIM5nRT+k+zU4+GUvRtnxhuVI= +cloud.google.com/go/secretmanager v1.11.4 h1:krnX9qpG2kR2fJ+u+uNyNo+ACVhplIAS4Pu7u+4gd+k= +cloud.google.com/go/security v1.15.4 h1:sdnh4Islb1ljaNhpIXlIPgb3eYj70QWgPVDKOUYvzJc= +cloud.google.com/go/securitycenter v1.24.3 h1:crdn2Z2rFIy8WffmmhdlX3CwZJusqCiShtnrGFRwpeE= +cloud.google.com/go/securitycenter v1.24.3/go.mod h1:l1XejOngggzqwr4Fa2Cn+iWZGf+aBLTXtB/vXjy5vXM= +cloud.google.com/go/servicecontrol v1.11.1 h1:d0uV7Qegtfaa7Z2ClDzr9HJmnbJW7jn0WhZ7wOX6hLE= +cloud.google.com/go/servicedirectory v1.11.3 h1:5niCMfkw+jifmFtbBrtRedbXkJm3fubSR/KHbxSJZVM= +cloud.google.com/go/servicemanagement v1.8.0 h1:fopAQI/IAzlxnVeiKn/8WiV6zKndjFkvi+gzu+NjywY= +cloud.google.com/go/serviceusage v1.6.0 h1:rXyq+0+RSIm3HFypctp7WoXxIA563rn206CfMWdqXX4= +cloud.google.com/go/shell v1.7.4 h1:nurhlJcSVFZneoRZgkBEHumTYf/kFJptCK2eBUq/88M= +cloud.google.com/go/spanner v1.53.1 h1:xNmE0SXMSxNBuk7lRZ5G/S+A49X91zkSTt7Jn5Ptlvw= +cloud.google.com/go/spanner v1.53.1/go.mod h1:liG4iCeLqm5L3fFLU5whFITqP0e0orsAW1uUSrd4rws= +cloud.google.com/go/speech v1.21.0 h1:qkxNao58oF8ghAHE1Eghen7XepawYEN5zuZXYWaUTA4= +cloud.google.com/go/storagetransfer v1.10.3 h1:YM1dnj5gLjfL6aDldO2s4GeU8JoAvH1xyIwXre63KmI= +cloud.google.com/go/talent v1.6.5 h1:LnRJhhYkODDBoTwf6BeYkiJHFw9k+1mAFNyArwZUZAs= +cloud.google.com/go/texttospeech v1.7.4 h1:ahrzTgr7uAbvebuhkBAAVU6kRwVD0HWsmDsvMhtad5Q= +cloud.google.com/go/tpu v1.6.4 h1:XIEH5c0WeYGaVy9H+UueiTaf3NI6XNdB4/v6TFQJxtE= +cloud.google.com/go/trace v1.10.4 h1:2qOAuAzNezwW3QN+t41BtkDJOG42HywL73q8x/f6fnM= +cloud.google.com/go/translate v1.9.3 h1:t5WXTqlrk8VVJu/i3WrYQACjzYJiff5szARHiyqqPzI= +cloud.google.com/go/video v1.20.3 h1:Xrpbm2S9UFQ1pZEeJt9Vqm5t2T/z9y/M3rNXhFoo8Is= +cloud.google.com/go/videointelligence v1.11.4 h1:YS4j7lY0zxYyneTFXjBJUj2r4CFe/UoIi/PJG0Zt/Rg= +cloud.google.com/go/vision v1.2.0 h1:/CsSTkbmO9HC8iQpxbK8ATms3OQaX3YQUeTMGCxlaK4= +cloud.google.com/go/vision/v2 v2.7.5 h1:T/ujUghvEaTb+YnFY/jiYwVAkMbIC8EieK0CJo6B4vg= +cloud.google.com/go/vmmigration v1.7.4 h1:qPNdab4aGgtaRX+51jCOtJxlJp6P26qua4o1xxUDjpc= +cloud.google.com/go/vmwareengine v1.0.3 h1:WY526PqM6QNmFHSqe2sRfK6gRpzWjmL98UFkql2+JDM= +cloud.google.com/go/vpcaccess v1.7.4 h1:zbs3V+9ux45KYq8lxxn/wgXole6SlBHHKKyZhNJoS+8= +cloud.google.com/go/webrisk v1.9.4 h1:iceR3k0BCRZgf2D/NiKviVMFfuNC9LmeNLtxUFRB/wI= +cloud.google.com/go/websecurityscanner v1.6.4 h1:5Gp7h5j7jywxLUp6NTpjNPkgZb3ngl0tUSw6ICWvtJQ= +cloud.google.com/go/workflows v1.12.3 h1:qocsqETmLAl34mSa01hKZjcqAvt699gaoFbooGGMvaM= +contrib.go.opencensus.io/exporter/aws v0.0.0-20200617204711-c478e41e60e9 h1:yxE46rQA0QaqPGqN2UnwXvgCrRqtjR1CsGSWVTRjvv4= +contrib.go.opencensus.io/exporter/prometheus v0.4.2 h1:sqfsYl5GIY/L570iT+l93ehxaWJs2/OwXtiWwew3oAg= +contrib.go.opencensus.io/exporter/prometheus v0.4.2/go.mod h1:dvEHbiKmgvbr5pjaF9fpw1KeYcjrnC1J8B+JKjsZyRQ= +contrib.go.opencensus.io/exporter/stackdriver v0.13.10 h1:a9+GZPUe+ONKUwULjlEOucMMG0qfSCCenlji0Nhqbys= +contrib.go.opencensus.io/integrations/ocsql v0.1.7 h1:G3k7C0/W44zcqkpRSFyjU9f6HZkbwIrL//qqnlqWZ60= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9 h1:VpgP7xuJadIUuKccphEpTJnWhS2jkQyMt6Y7pJCD7fY= +docker.io/go-docker v1.0.0 h1:VdXS/aNYQxyA9wdLD5z8Q8Ro688/hG8HzKxYVEVbE6s= +docker.io/go-docker v1.0.0/go.mod h1:7tiAn5a0LFmjbPDbyTPOaTTOuG1ZRNXdPA6RvKY+fpY= +filippo.io/edwards25519 v1.0.0 h1:0wAIcmJUqRdI8IJ/3eGi5/HwXZWPujYXXlkrQogz0Ek= +filippo.io/edwards25519 v1.0.0/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= +gioui.org v0.0.0-20210308172011-57750fc8a0a6 h1:K72hopUosKG3ntOPNG4OzzbuhxGuVf06fa2la1/H/Ho= +git.sr.ht/~sbinet/gg v0.3.1 h1:LNhjNn8DerC8f9DHLz6lS0YYul/b602DUxDgGkd/Aik= +github.com/99designs/basicauth-go v0.0.0-20160802081356-2a93ba0f464d h1:j6oB/WPCigdOkxtuPl1VSIiLpy7Mdsu6phQffbF19Ng= +github.com/99designs/httpsignatures-go v0.0.0-20170731043157-88528bf4ca7e h1:rl2Aq4ZODqTDkeSqQBy+fzpZPamacO1Srp8zq7jf2Sc= +github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9 h1:HD8gA2tkByhMAwYaFAX9w2l7vxvBQ5NMoxDrkhqhtn4= +github.com/Azure/azure-amqp-common-go/v3 v3.2.2 h1:CJpxNAGxP7UBhDusRUoaOn0uOorQyAYhQYLnNgkRhlY= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal v1.1.2 h1:mLY+pNLjCUeKhgnAJWAKhEUQM+RJQo2H1fuGSw1Ky1E= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.0.0 h1:ECsQtyERDVz3NP3kvDOTLvbQhqWp/x9EsGKtb4ogUr8= +github.com/Azure/azure-service-bus-go v0.11.5 h1:EVMicXGNrSX+rHRCBgm/TRQ4VUZ1m3yAYM/AB2R/SOs= +github.com/Azure/go-amqp v0.16.4 h1:/1oIXrq5zwXLHaoYDliJyiFjJSpJZMWGgtMX9e0/Z30= +github.com/Azure/go-autorest/autorest/azure/auth v0.5.11 h1:P6bYXFoao05z5uhOQzbC3Qd8JqF3jUoocoTeIxkp2cA= +github.com/Azure/go-autorest/autorest/azure/auth v0.5.11/go.mod h1:84w/uV8E37feW2NCJ08uT9VBfjfUHpgLVnG2InYD6cg= +github.com/Azure/go-autorest/autorest/azure/cli v0.4.5 h1:0W/yGmFdTIT77fvdlGZ0LMISoLHFJ7Tx4U0yeB+uFs4= +github.com/Azure/go-autorest/autorest/azure/cli v0.4.5/go.mod h1:ADQAXrkgm7acgWVUNamOgh8YNrv4p27l3Wc55oVfpzg= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 h1:1BDTz0u9nC3//pOCMdNH+CiXJVYJh5UQNCOBG7jbELc= +github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 h1:sR+/8Yb4slttB4vD+b9btVEnWgL3Q00OBTzVT8B9C0c= +github.com/CloudyKit/jet/v3 v3.0.0 h1:1PwO5w5VCtlUUl+KTOBsTGZlhjWkcybsGaAau52tOy8= +github.com/DATA-DOG/go-sqlmock v1.3.3 h1:CWUqKXe0s8A2z6qCgkP4Kru7wC11YoAnoupUKFDnH08= +github.com/DataDog/datadog-go v4.0.0+incompatible h1:Dq8Dr+4sV1gBO1sHDWdW+4G+PdsA+YSJOK925MxrrCY= +github.com/GoogleCloudPlatform/cloudsql-proxy v1.29.0 h1:YNu23BtH0PKF+fg3ykSorCp6jSTjcEtfnYLzbmcjVRA= +github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c h1:RGWPOewvKIROun94nF7v2cua9qP+thov/7M50KEoeSU= +github.com/Joker/hpp v1.0.0 h1:65+iuJYdRXv/XyN62C1uEmmOx3432rNG/rKlX6V7Kkc= +github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible h1:1G1pk05UrOh0NlF1oeaaix1x8XzrfjIDK47TY0Zehcw= +github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= +github.com/OneOfOne/xxhash v1.2.6 h1:U68crOE3y3MPttCMQGywZOLrTeF5HHJ3/vDBCJn9/bA= +github.com/OneOfOne/xxhash v1.2.6/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q= +github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= +github.com/RaveNoX/go-jsoncommentstrip v1.0.0 h1:t527LHHE3HmiHrq74QMpNPZpGCIJzTx+apLkMKt4HC0= +github.com/RoaringBitmap/gocroaring v0.4.0 h1:5nufXUgWpBEUNEJXw7926YAA58ZAQRpWPrQV1xCoSjc= +github.com/RoaringBitmap/real-roaring-datasets v0.0.0-20190726190000-eb7c87156f76 h1:ZYlhPbqQFU+AHfgtCdHGDTtRW1a8geZyiE8c6Q+Sl1s= +github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398 h1:WDC6ySpJzbxGWFh4aMxFFC28wwGp5pEuoTtvA4q/qQ4= +github.com/Shopify/sarama v1.38.1 h1:lqqPUPQZ7zPqYlWpTh+LQ9bhYNu2xJL6k1SJN4WVe2A= +github.com/Shopify/sarama v1.38.1/go.mod h1:iwv9a67Ha8VNa+TifujYoWGxWnu2kNVAQdSdZ4X2o5g= +github.com/Shopify/toxiproxy v2.1.4+incompatible h1:TKdv8HiTLgE5wdJuEML90aBgNWsokNbMijUGhmcoBJc= +github.com/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrdtl/UvroE= +github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5 h1:rFw4nCn9iMW+Vajsk51NtYIcwSTkXr+JGrMd36kTDJw= +github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= +github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9 h1:7kQgkwGRoLzC9K0oyXdJo7nve/bynv/KwUsxbiTlzAM= +github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19 h1:iXUgAaqDcIUGbRoy2TdeofRG/j1zpGRSEmNK05T+bi8= +github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b h1:slYM766cy2nI3BwyRiyQj/Ud48djTMtMebDqepE95rw= +github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY= +github.com/alecthomas/kong v0.2.11 h1:RKeJXXWfg9N47RYfMm0+igkxBCTF4bzbneAxaqid0c4= +github.com/alecthomas/kong v0.2.11/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE= +github.com/alecthomas/participle/v2 v2.1.0 h1:z7dElHRrOEEq45F2TG5cbQihMtNTv8vwldytDj7Wrz4= +github.com/alecthomas/participle/v2 v2.1.0/go.mod h1:Y1+hAs8DHPmc3YUFzqllV+eSQ9ljPTk0ZkPMtEdAx2c= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= +github.com/alicebob/miniredis v2.5.0+incompatible h1:yBHoLpsyjupjz3NL3MhKMVkR41j82Yjf3KFv7ApYzUI= +github.com/alicebob/miniredis v2.5.0+incompatible/go.mod h1:8HZjEj4yU0dwhYHky+DxYx+6BMjkBbe5ONFIF1MXffk= +github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg= +github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40 h1:q4dksr6ICHXqG5hm0ZW5IHyeEJXoIJSOZeBLmWPNeIQ= +github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40/go.mod h1:Q7yQnSMnLvcXlZ8RV+jwz/6y1rQTqbX6C82SndT52Zs= +github.com/apache/arrow/go/v10 v10.0.1 h1:n9dERvixoC/1JjDmBcs9FPaEryoANa2sCgVFo6ez9cI= +github.com/apache/arrow/go/v11 v11.0.0 h1:hqauxvFQxww+0mEU/2XHG6LT7eZternCZq+A5Yly2uM= +github.com/apache/arrow/go/v12 v12.0.0 h1:xtZE63VWl7qLdB0JObIXvvhGjoVNrQ9ciIHG2OK5cmc= +github.com/apache/arrow/go/v13 v13.0.0 h1:kELrvDQuKZo8csdWYqBQfyi431x6Zs/YJTEgUuSVcWk= +github.com/apache/arrow/go/v13 v13.0.0/go.mod h1:W69eByFNO0ZR30q1/7Sr9d83zcVZmF2MiP3fFYAWJOc= +github.com/apache/thrift v0.18.1 h1:lNhK/1nqjbwbiOPDBPFJVKxgDEGSepKuTh6OLiXW8kg= +github.com/apache/thrift v0.18.1/go.mod h1:rdQn/dCcDKEWjjylUeueum4vQEjG2v8v2PqriUnbr+I= +github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3 h1:ZSTrOEhiM5J5RFxEaFvMZVEAM1KvT1YzbEOwB2EAGjA= +github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e h1:QEF07wC0T1rKkctt1RINW/+RMTVmiwxETico2l3gxJA= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6 h1:G1bPvciwNyF7IUmKXNt9Ak3m6u9DE1rF+RmtIkBpVdA= +github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a h1:pv34s756C4pEXnjgPfGYgdhg/ZdajGhyOvzx8k+23nw= +github.com/aws/aws-lambda-go v1.13.3 h1:SuCy7H3NLyp+1Mrfp+m80jcbi9KYWAs9/BXwppwRDzY= +github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.8.1 h1:w/fPGB0t5rWwA43mux4e9ozFSH5zF1moQemlA131PWc= +github.com/aws/aws-sdk-go-v2/service/kms v1.16.3 h1:nUP29LA4GZZPihNSo5ZcF4Rl73u+bN5IBRnrQA0jFK4= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.4 h1:EmIEXOjAdXtxa2OGM1VAajZV/i06Q8qd4kBpJd9/p1k= +github.com/aws/aws-sdk-go-v2/service/sns v1.17.4 h1:7TdmoJJBwLFyakXjfrGztejwY5Ie1JEto7YFfznCmAw= +github.com/aws/aws-sdk-go-v2/service/sqs v1.18.3 h1:uHjK81fESbGy2Y9lspub1+C6VN5W2UXTDo2A/Pm4G0U= +github.com/aws/aws-sdk-go-v2/service/ssm v1.24.1 h1:zc1YLcknvxdW/i1MuJKmEnFB2TNkOfguuQaGRvJXPng= +github.com/aws/aws-xray-sdk-go v0.9.4 h1:3mtFCrgFR5IefmWFV5pscHp9TTyOWuqaIKJIY0d1Y4g= +github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible h1:Ppm0npCCsmuR9oQaBtRuZcmILVE74aXE+AmrJj8L2ns= +github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= +github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 h1:mXoPYz/Ul5HYEDvkta6I8/rnYM5gSdSV2tJ6XbZuEtY= +github.com/bmatcuk/doublestar/v2 v2.0.3 h1:D6SI8MzWzXXBXZFS87cFL6s/n307lEU+thM2SUnge3g= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= +github.com/boombuler/barcode v1.0.1 h1:NDBbPmhS+EqABEs5Kg3n/5ZNjy73Pz7SIV+KCeqyXcs= +github.com/bwesterb/go-ristretto v1.2.3 h1:1w53tCkGhCQ5djbat3+MH0BAQ5Kfgbt56UZQ/JMzngw= +github.com/casbin/casbin/v2 v2.37.0 h1:/poEwPSovi4bTOcP752/CsTQiRz2xycyVKFG7GUhbDw= +github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= +github.com/cenkalti/backoff/v3 v3.0.0 h1:ske+9nBpD9qZsTBoF41nW5L+AIuFBKMeze18XQ3eG1c= +github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g= +github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= +github.com/chromedp/chromedp v0.9.2 h1:dKtNz4kApb06KuSXoTQIyUC2TrA0fhGDwNZf3bcgfKw= +github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic= +github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= +github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= +github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= +github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575 h1:kHaBemcxl8o/pQ5VM1c8PVE1PubbNx3mjUr09OqWGCs= +github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible h1:C29Ae4G5GtYyYMm1aztcyj/J5ckgJm2zwdDajFbx1NY= +github.com/circonus-labs/circonusllhist v0.1.3 h1:TJH+oke8D16535+jHExHj4nQvzlZrj7ug5D7I/orNUA= +github.com/clbanning/mxj v1.8.4 h1:HuhwZtbyvyOw+3Z1AowPkU87JkJUSv751ELWaiTpj8I= +github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec h1:EdRZT3IeKQmfCSrgo8SZ8V3MEnskuJP0wCYNpe+aiXo= +github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI= +github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe h1:QQ3GSy+MqSHxm/d8nCtnAiZdYFd45cYZPs8vOOIYKfk= +github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= +github.com/cockroachdb/cockroach-go v0.0.0-20200312223839-f565e4789405 h1:i1XXyBMAGL7NqogtoS6NHQ/IJwCbG0R725hAhEhldOI= +github.com/cockroachdb/datadriven v1.0.2 h1:H9MtNqVoVhvd9nCBwOyDjUEdZCREqbIdCJD93PBm/jA= +github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd h1:qMd81Ts1T2OTKmB4acZcyKaMtRnY5Y44NuXGX2GFJ1w= +github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0 h1:sDMmm+q/3+BukdIpxwO365v/Rbspp2Nt5XntgQRXq8Q= +github.com/codegangsta/negroni v1.0.0 h1:+aYywywx4bnKXWvoWtRfJ91vC59NbEhEY03sZjQhbVY= +github.com/containerd/containerd v1.6.8 h1:h4dOFDwzHmqFEP754PgfgTeVXFnLiRc6kiqC7tplDJs= +github.com/containerd/containerd v1.6.8/go.mod h1:By6p5KqPK0/7/CgO/A6t/Gz+CUYUu2zf1hUaaymVXB0= +github.com/containerd/continuity v0.0.0-20200107194136-26c1120b8d41 h1:kIFnQBO7rQ0XkMe6xEwbybYHBEaWmh/f++laI6Emt7M= +github.com/coreos/bbolt v1.3.2 h1:wZwiHHUieZCquLkDL0B8UhzreNWsPHooDAG3q34zk0s= +github.com/coreos/etcd v3.3.10+incompatible h1:jFneRYjIvLMLhDLCzuTuU4rSJUjRplcJQ7pD7MnhC04= +github.com/coreos/go-etcd v2.0.0+incompatible h1:bXhRBIXoTm9BYHS3gE0TtQuyNZyeEMux2sDi4oo5YOo= +github.com/coreos/go-oidc v2.2.1+incompatible h1:mh48q/BqXqgjVHpy2ZY7WnWAbenxRjsz9N1i1YxjHAk= +github.com/coreos/go-oidc v2.2.1+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= +github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f h1:JOrtw2xFKzlg+cbHpyrpLDmnN1HqhBfnX7WDiW7eG2c= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f h1:lBNOc5arjvs8E5mO2tbpBpLoyyu8B6e44T7hJy6potg= +github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk= +github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= +github.com/crewjam/httperr v0.2.0 h1:b2BfXR8U3AlIHwNeFFvZ+BV1LFvKLlzMjzaTnZMybNo= +github.com/crewjam/httperr v0.2.0/go.mod h1:Jlz+Sg/XqBQhyMjdDiC+GNNRzZTD7x39Gu3pglZ5oH4= +github.com/cristalhq/hedgedhttp v0.9.1 h1:g68L9cf8uUyQKQJwciD0A1Vgbsz+QgCjuB1I8FAsCDs= +github.com/cristalhq/hedgedhttp v0.9.1/go.mod h1:XkqWU6qVMutbhW68NnzjWrGtH8NUx1UfYqGYtHVKIsI= +github.com/cucumber/godog v0.8.1 h1:lVb+X41I4YDreE+ibZ50bdXmySxgRviYFgKY6Aw4XE8= +github.com/cznic/b v0.0.0-20180115125044-35e9bbe41f07 h1:UHFGPvSxX4C4YBApSPvmUfL8tTvWLj2ryqvT9K4Jcuk= +github.com/cznic/fileutil v0.0.0-20180108211300-6a051e75936f h1:7uSNgsgcarNk4oiN/nNkO0J7KAjlsF5Yv5Gf/tFdHas= +github.com/cznic/golex v0.0.0-20170803123110-4ab7c5e190e4 h1:CVAqftqbj+exlab+8KJQrE+kNIVlQfJt58j4GxCMF1s= +github.com/cznic/internal v0.0.0-20180608152220-f44710a21d00 h1:FHpbUtp2K8X53/b4aFNj4my5n+i3x+CQCZWNuHWH/+E= +github.com/cznic/lldb v1.1.0 h1:AIA+ham6TSJ+XkMe8imQ/g8KPzMUVWAwqUQQdtuMsHs= +github.com/cznic/mathutil v0.0.0-20180504122225-ca4c9f2c1369 h1:XNT/Zf5l++1Pyg08/HV04ppB0gKxAqtZQBRYiYrUuYk= +github.com/cznic/ql v1.2.0 h1:lcKp95ZtdF0XkWhGnVIXGF8dVD2X+ClS08tglKtf+ak= +github.com/cznic/sortutil v0.0.0-20150617083342-4c7342852e65 h1:hxuZop6tSoOi0sxFzoGGYdRqNrPubyaIf9KoBG9tPiE= +github.com/cznic/strutil v0.0.0-20171016134553-529a34b1c186 h1:0rkFMAbn5KBKNpJyHQ6Prb95vIKanmAe62KxsrN+sqA= +github.com/cznic/zappy v0.0.0-20160723133515-2533cb5b45cc h1:YKKpTb2BrXN2GYyGaygIdis1vXbE7SSAG9axGWIMClg= +github.com/dave/astrid v0.0.0-20170323122508-8c2895878b14 h1:YI1gOOdmMk3xodBao7fehcvoZsEeOyy/cfhlpCSPgM4= +github.com/dave/brenda v1.1.0 h1:Sl1LlwXnbw7xMhq3y2x11McFu43AjDcwkllxxgZ3EZw= +github.com/dave/courtney v0.3.0 h1:8aR1os2ImdIQf3Zj4oro+lD/L4Srb5VwGefqZ/jzz7U= +github.com/dave/gopackages v0.0.0-20170318123100-46e7023ec56e h1:l99YKCdrK4Lvb/zTupt0GMPfNbncAGf8Cv/t1sYLOg0= +github.com/dave/kerr v0.0.0-20170318121727-bc25dd6abe8e h1:xURkGi4RydhyaYR6PzcyHTueQudxY4LgxN1oYEPJHa0= +github.com/dave/patsy v0.0.0-20210517141501-957256f50cba h1:1o36L4EKbZzazMk8iGC4kXpVnZ6TPxR2mZ9qVKjNNAs= +github.com/dave/rebecca v0.9.1 h1:jxVfdOxRirbXL28vXMvUvJ1in3djwkVKXCq339qhBL0= +github.com/dchest/uniuri v1.2.0 h1:koIcOUdrTIivZgSLhHQvKgqdWZq5d7KdMEWF1Ud6+5g= +github.com/dchest/uniuri v1.2.0/go.mod h1:fSzm4SLHzNZvWLvWJew423PhAzkpNQYq+uNLq4kxhkY= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= +github.com/denisenkom/go-mssqldb v0.0.0-20190515213511-eb9f6a1743f3/go.mod h1:zAg7JM8CkOJ43xKXIj7eRO9kmWm/TW578qo+oDO6tuM= +github.com/denisenkom/go-mssqldb v0.12.0 h1:VtrkII767ttSPNRfFekePK3sctr+joXgO58stqQbtUA= +github.com/denisenkom/go-mssqldb v0.12.0/go.mod h1:iiK0YP1ZeepvmBQk/QpLEhhTNJgfzrpArPY/aFvc9yU= +github.com/devigned/tab v0.1.1 h1:3mD6Kb1mUOYeLpJvTVSDwSg5ZsfSxfvxGRTxRsJsITA= +github.com/dgraph-io/badger v1.6.0 h1:DshxFxZWXUcO0xX476VJC07Xsr6ZCBVRHKZ93Oh7Evo= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954 h1:RMLoZVzv4GliuWafOuPuQDKSm1SJph7uCRnnS61JAn4= +github.com/dhui/dktest v0.3.0 h1:kwX5a7EkLcjo7VpsPQSYJcKGbXBXdjI9FGjuUj1jn6I= +github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U= +github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 h1:bWDMxwH3px2JBh6AyO7hdCn/PkvCZXii8TGj7sbtEbQ= +github.com/drone/drone-runtime v1.1.0 h1:IsKbwiLY6+ViNBzX0F8PERJVZZcEJm9rgxEh3uZP5IE= +github.com/drone/drone-runtime v1.1.0/go.mod h1:+osgwGADc/nyl40J0fdsf8Z09bgcBZXvXXnLOY48zYs= +github.com/drone/drone-yaml v1.2.3 h1:SWzLmzr8ARhbtw1WsVDENa8WFY2Pi9l0FVMfafVUWz8= +github.com/drone/drone-yaml v1.2.3/go.mod h1:QsqliFK8nG04AHFN9tTn9XJomRBQHD4wcejWW1uz/10= +github.com/drone/funcmap v0.0.0-20211123105308-29742f68a7d1 h1:E8hjIYiEyI+1S2XZSLpMkqT9V8+YMljFNBWrFpuVM3A= +github.com/drone/funcmap v0.0.0-20211123105308-29742f68a7d1/go.mod h1:Hph0/pT6ZxbujnE1Z6/08p5I0XXuOsppqF6NQlGOK0E= +github.com/drone/signal v1.0.0 h1:NrnM2M/4yAuU/tXs6RP1a1ZfxnaHwYkd0kJurA1p6uI= +github.com/eapache/go-resiliency v1.3.0 h1:RRL0nge+cWGlxXbUzJ7yMcq6w2XBEr19dCN6HECGaT0= +github.com/eapache/go-resiliency v1.3.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6iZYSs1ZI+iQho= +github.com/eapache/go-xerial-snappy v0.0.0-20230111030713-bf00bc1b83b6 h1:8yY/I9ndfrgrXUbOGObLHKBR4Fl3nZXwM2c7OYTT8hM= +github.com/eapache/go-xerial-snappy v0.0.0-20230111030713-bf00bc1b83b6/go.mod h1:YvSRo5mw33fLEx1+DlK6L2VV43tJt5Eyel9n9XBcR+0= +github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc= +github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385 h1:clC1lXBpe2kTj2VHdaIu9ajZQe4kcEY9j0NsnDDBZ3o= +github.com/elastic/go-sysinfo v1.1.1 h1:ZVlaLDyhVkDfjwPGU55CQRCRolNpc7P0BbyhhQZQmMI= +github.com/elastic/go-windows v1.0.0 h1:qLURgZFkkrYyTTkvYpsZIgf83AUsdIHfvlJaqaZ7aSY= +github.com/etcd-io/bbolt v1.3.3 h1:gSJmxrs37LgTqR/oyJBWok6k6SvXEUerFTbltIhXkBM= +github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb h1:IT4JYU7k4ikYg1SCxNI1/Tieq/NFvh6dzLdgi7eu0tM= +github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb/go.mod h1:bH6Xx7IW64qjjJq8M2u4dxNaBiDfKK+z/3eGDpXEQhc= +github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072 h1:DddqAaWDpywytcG8w/qoQ5sAN8X12d3Z3koB0C3Rxsc= +github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= +github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8= +github.com/form3tech-oss/jwt-go v3.2.2+incompatible h1:TcekIExNqud5crz4xD2pavyTgWiPvpYe4Xau31I0PRk= +github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= +github.com/franela/goblin v0.0.0-20210519012713-85d372ac71e2 h1:cZqz+yOJ/R64LcKjNQOdARott/jP7BnUQ9Ah7KaZCvw= +github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8 h1:a9ENSRDFBUPkJ5lCgVZh26+ZbGyoVJG7yb5SSzF5H54= +github.com/fsouza/fake-gcs-server v1.7.0 h1:Un0BXUXrRWYSmYyC1Rqm2e2WJfTPyDy/HGMz31emTi8= +github.com/gavv/httpexpect v2.0.0+incompatible h1:1X9kcRshkSKEjNJJxX9Y9mQ5BRfbxU5kORdjhlA1yX8= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8= +github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= +github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8 h1:DujepqpGd1hyOd7aW59XpK7Qymp8iy83xq74fLr21is= +github.com/go-bindata/go-bindata v3.1.1+incompatible h1:tR4f0e4VTO7LK6B2YWyAoVEzG9ByG1wrXB4TL9+jiYg= +github.com/go-check/check v0.0.0-20180628173108-788fd7840127 h1:0gkP6mzaMqkmpcJYCFOLkIBwI7xFExG03bbkOkCvUPI= +github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8= +github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-fonts/dejavu v0.1.0 h1:JSajPXURYqpr+Cu8U9bt8K+XcACIHWqWrvWCKyeFmVQ= +github.com/go-fonts/latin-modern v0.2.0 h1:5/Tv1Ek/QCr20C6ZOz15vw3g7GELYL98KWr8Hgo+3vk= +github.com/go-fonts/liberation v0.2.0 h1:jAkAWJP4S+OsrPLZM4/eC9iW7CtHy+HBXrEwZXWo5VM= +github.com/go-fonts/stix v0.1.0 h1:UlZlgrvvmT/58o573ot7NFw0vZasZ5I6bcIft/oMdgg= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1 h1:QbL/5oDUmRBzO9/Z7Seo6zf912W/a6Sr4Eu0G/3Jho0= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4 h1:WtGNWLvXpe6ZudgnXrq0barxBImvnnJoMEhXAzcbM0I= +github.com/go-ini/ini v1.25.4 h1:Mujh4R/dH6YL8bxuISne3xX2+qcQ9p0IxKAP6ExWoUo= +github.com/go-kit/kit v0.12.0 h1:e4o3o3IsBfAKQh5Qbbiqyfu97Ku7jrO/JbohvztANh4= +github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81 h1:6zl3BbBhdnMkpSj2YY30qV3gDcVBGtFgVsV3+/i+mKQ= +github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab h1:xveKWz2iaueeTaUgdetzel+U7exyigDYBryyVfV/rZk= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-pdf/fpdf v0.6.0 h1:MlgtGIfsdMEEQJr2le6b/HNr1ZlQwxyWr77r2aj2U/8= +github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= +github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= +github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= +github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= +github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= +github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJWXmqUsHwfTRRkQ= +github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU= +github.com/gobuffalo/attrs v0.1.0 h1:LY6/rbhPD/hfa+AfviaMXBmZBGb0fGHF12yg7f3dPQA= +github.com/gobuffalo/buffalo v0.13.0 h1:Fyn55HJULJpFPMUNx9lrPK31qvr37+bpNGFbpAOauGI= +github.com/gobuffalo/buffalo-plugins v1.15.0 h1:71I2OqFmlP4p3N9Vl0maq+IPHKuTEUevoeFkLHjgF3k= +github.com/gobuffalo/buffalo-pop v1.0.5 h1:8aXdBlo9MEKFGHyl489+28Jw7Ud59Th1U+5Ayu1wNL0= +github.com/gobuffalo/depgen v0.1.0 h1:31atYa/UW9V5q8vMJ+W6wd64OaaTHUrCUXER358zLM4= +github.com/gobuffalo/envy v1.9.0 h1:eZR0DuEgVLfeIb1zIKt3bT4YovIMf9O9LXQeCZLXpqE= +github.com/gobuffalo/events v1.4.1 h1:OLJIun6wRx4DOW19XoL/AoyjuJltqeOBFH3q8cDvNb8= +github.com/gobuffalo/fizz v1.10.0 h1:I8vad0PnmR+CLjSnZ5L5jlhBm4S88UIGOoZZL3/3e24= +github.com/gobuffalo/flect v0.2.1 h1:GPoRjEN0QObosV4XwuoWvSd5uSiL0N3e91/xqyY4crQ= +github.com/gobuffalo/genny v0.6.0 h1:d7c6d66ZrTHHty01hDX1/TcTWvAJQxRZl885KWX5kHY= +github.com/gobuffalo/genny/v2 v2.0.5 h1:IH0EHcvwKT0MdASzptvkz/ViYBQELTklq1/l8Ot3Q5E= +github.com/gobuffalo/gitgen v0.0.0-20190315122116-cc086187d211 h1:mSVZ4vj4khv+oThUfS+SQU3UuFIZ5Zo6UNcvK8E8Mz8= +github.com/gobuffalo/github_flavored_markdown v1.1.0 h1:8Zzj4fTRl/OP2R7sGerzSf6g2nEJnaBEJe7UAOiEvbQ= +github.com/gobuffalo/gogen v0.2.0 h1:Xx7NCe+/y++eII2aWAFZ09/81MhDCsZwvMzIFJoQRnU= +github.com/gobuffalo/helpers v0.6.1 h1:LLcL4BsiyDQYtMRUUpyFdBFvFXQ6hNYOpwrcYeilVWM= +github.com/gobuffalo/here v0.6.0 h1:hYrd0a6gDmWxBM4TnrGw8mQg24iSVoIkHEk7FodQcBI= +github.com/gobuffalo/httptest v1.0.2 h1:LWp2khlgA697h4BIYWW2aRxvB93jMnBrbakQ/r2KLzs= +github.com/gobuffalo/licenser v1.1.0 h1:xAHoWgZ8vbRkxC8a+SBVL7Y7atJBRZfXM9H9MmPXx1k= +github.com/gobuffalo/logger v1.0.3 h1:YaXOTHNPCvkqqA7w05A4v0k2tCdpr+sgFlgINbQ6gqc= +github.com/gobuffalo/makr v1.1.5 h1:lOlpv2iz0dNa4qse0ZYQgbtT+ybwVxWEAcOZbcPmeYc= +github.com/gobuffalo/mapi v1.2.1 h1:TyfbtQaW7GvS4DXdF1KQOSGrW6L0uiFmGDz+JgEIMbM= +github.com/gobuffalo/meta v0.3.0 h1:F0BFeZuQ1UmsHAVBgsnzolheLmv11t2GQ+53OFOP7lk= +github.com/gobuffalo/mw-basicauth v1.0.3 h1:bCqDBHnByenQitOtFdEtMvlWVgPwODrfZ+nVkgGoJZ8= +github.com/gobuffalo/mw-contenttype v0.0.0-20180802152300-74f5a47f4d56 h1:SUFp8EbFjlKXkvqstoxPWx3nVPV3BSKZTswQNTZFaik= +github.com/gobuffalo/mw-csrf v0.0.0-20180802151833-446ff26e108b h1:A13B4mhcFQcjPJ1GFBrh61B4Qo87fZa82FfTt9LX/QU= +github.com/gobuffalo/mw-forcessl v0.0.0-20180802152810-73921ae7a130 h1:v94+IGhlBro0Lz1gOR3lrdAVSZ0mJF2NxsdppKd7FnI= +github.com/gobuffalo/mw-i18n v0.0.0-20180802152014-e3060b7e13d6 h1:pZhsgF8RXEngHdibuRNOXNk1pL0K9rFa5HOcvURNTQ4= +github.com/gobuffalo/mw-paramlogger v0.0.0-20181005191442-d6ee392ec72e h1:TsmUXyHjj5ReuN1AJjEVukf72J6AfRTF2CfTEaqVLT8= +github.com/gobuffalo/mw-tokenauth v0.0.0-20181001105134-8545f626c189 h1:nhPzONHNGlXZIMFfKm6cWpRSq5oTanRK1qBtfCPBFyE= +github.com/gobuffalo/nulls v0.3.0 h1:yfOsQarm6pD7Crg/VnpI9Odh5nBlO+eDeKRiHYZOsTA= +github.com/gobuffalo/packd v1.0.0 h1:6ERZvJHfe24rfFmA9OaoKBdC7+c9sydrytMg8SdFGBM= +github.com/gobuffalo/packr v1.22.0 h1:/YVd/GRGsu0QuoCJtlcWSVllobs4q3Xvx3nqxTvPyN0= +github.com/gobuffalo/packr/v2 v2.8.0 h1:IULGd15bQL59ijXLxEvA5wlMxsmx/ZkQv9T282zNVIY= +github.com/gobuffalo/plush v3.8.3+incompatible h1:kzvUTnFPhwyfPEsx7U7LI05/IIslZVGnAlMA1heWub8= +github.com/gobuffalo/plush/v4 v4.0.0 h1:ZHdmfr2R7DQ77XzWZK2PGKJOXm9NRy21EZ6Rw7FhuNw= +github.com/gobuffalo/plushgen v0.1.2 h1:s4yAgNdfNMyMQ7o+Is4f1VlH2L1tKosT+m7BF28C8H4= +github.com/gobuffalo/pop v4.13.1+incompatible h1:AhbqPxNOBN/DBb2DBaiBqzOXIBQXxEYzngHHJ+ytP4g= +github.com/gobuffalo/pop/v5 v5.3.1 h1:dJbBPy6e0G0VRjn28md3fk16wpYIBv5iYVQWd0eqmkQ= +github.com/gobuffalo/release v1.7.0 h1:5+6HdlnRQ2anNSOm3GyMvmiCjSIXg3dcJhNA7Eh99dQ= +github.com/gobuffalo/shoulders v1.0.4 h1:Hw7wjvyasJJo+bDsebhpnnizHCxfxQ3C4mmLjEzcdXY= +github.com/gobuffalo/syncx v0.1.0 h1://CNTQ/+VFQizkW24DrBtTBvj8c2+chz5Y7kbboQ2qk= +github.com/gobuffalo/tags v2.1.7+incompatible h1:GUxxh34f9SI4U0Pj3ZqvopO9SlzuqSf+g4ZGSPSszt4= +github.com/gobuffalo/tags/v3 v3.1.0 h1:mzdCYooN2VsLRr8KIAdEZ1lh1Py7JSMsiEGCGata2AQ= +github.com/gobuffalo/uuid v2.0.5+incompatible h1:c5uWRuEnYggYCrT9AJm0U2v1QTG7OVDAvxhj8tIV5Gc= +github.com/gobuffalo/validate v2.0.4+incompatible h1:ZTxozrIw8qQ5nfhShmc4izjYPTsPhfdXTdhXOd5OS9o= +github.com/gobuffalo/validate/v3 v3.2.0 h1:Zrpkz2kuZ4rGXLaO3IHVlwX512/cUWRvNjw46Cjhz2Q= +github.com/gobuffalo/x v0.0.0-20181007152206-913e47c59ca7 h1:N0iqtKwkicU8M2rLirTDJxdwuL8I2/8MjMlEayaNSgE= +github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= +github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= +github.com/gobwas/ws v1.2.1 h1:F2aeBZrm2NDsc7vbovKrWSogd4wvfAxg0FQ89/iqOTk= +github.com/goccy/go-yaml v1.11.0 h1:n7Z+zx8S9f9KgzG6KtQKf+kwqXZlLNR2F6018Dgau54= +github.com/goccy/go-yaml v1.11.0/go.mod h1:H+mJrWtjPTJAHvRbV09MCK9xYwODM+wRTVFFTWckfng= +github.com/gocql/gocql v0.0.0-20190301043612-f6df8288f9b4 h1:vF83LI8tAakwEwvWZtrIEx7pOySacl2TOxx6eXk4ePo= +github.com/godbus/dbus/v5 v5.0.4 h1:9349emZab16e7zQvpmsbtjc18ykshndd8y2PG3sgJbA= +github.com/gofrs/uuid/v3 v3.1.2 h1:V3IBv1oU82x6YIr5txe3azVHgmOKYdyKQTowm9moBlY= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= +github.com/golang/gddo v0.0.0-20190904175337-72a348e765d2 h1:xisWqjiKEff2B0KfFYGpCqc3M3zdTz+OHQHRc09FeYk= +github.com/gomodule/redigo v1.8.9 h1:Sl3u+2BI/kk+VEatbj0scLdrFhjPmbxOc1myhDP41ws= +github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE= +github.com/google/go-jsonnet v0.18.0 h1:/6pTy6g+Jh1a1I2UMoAODkqELFiVIdOxbNwv0DDzoOg= +github.com/google/go-jsonnet v0.18.0/go.mod h1:C3fTzyVJDslXdiTqw/bTFk7vSGyCtH3MGRbDfvEwGd0= +github.com/google/go-pkcs11 v0.2.1-0.20230907215043-c6f79328ddf9 h1:OF1IPgv+F4NmqmJ98KTjdN97Vs1JxDPB3vbmYzV2dpk= +github.com/google/go-replayers/grpcreplay v1.1.0 h1:S5+I3zYyZ+GQz68OfbURDdt/+cSMqCK1wrvNx7WBzTE= +github.com/google/go-replayers/httpreplay v1.1.1 h1:H91sIMlt1NZzN7R+/ASswyouLJfW0WLW7fhyUFvDEkY= +github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/google/subcommands v1.0.1 h1:/eqq+otEXm5vhfBrbREPCSVQbvofip6kIz+mX5TUH7k= +github.com/googleapis/go-type-adapters v1.0.0 h1:9XdMn+d/G57qq1s8dNc5IesGCXHf6V2HZ2JwRxfA2tA= +github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8 h1:tlyzajkF3030q6M8SvmJSemC9DTHL/xaMa18b65+JM4= +github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= +github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= +github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= +github.com/gorilla/pat v0.0.0-20180118222023-199c85a7f6d1 h1:LqbZZ9sNMWVjeXS4NN5oVvhMjDyLhmA1LG86oSo+IqY= +github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= +github.com/gorilla/sessions v1.1.3 h1:uXoZdcdA5XdXF3QzuSlheVRUvjl+1rKY7zBXL68L9RU= +github.com/gotestyourself/gotestyourself v2.2.0+incompatible h1:AQwinXlbQR2HvPjQZOmDhRqsv5mZf+Jb1RnSLxcqZcI= +github.com/grafana/e2e v0.1.1-0.20221018202458-cffd2bb71c7b h1:Ha+kSIoTutf4ytlVw/SaEclDUloYx0+FXDKJWKhNbE4= +github.com/grafana/e2e v0.1.1-0.20221018202458-cffd2bb71c7b/go.mod h1:3UsooRp7yW5/NJQBlXcTsAHOoykEhNUYXkQ3r6ehEEY= +github.com/grafana/gomemcache v0.0.0-20231023152154-6947259a0586 h1:/of8Z8taCPftShATouOrBVy6GaTTjgQd/VfNiZp/VXQ= +github.com/grafana/gomemcache v0.0.0-20231023152154-6947259a0586/go.mod h1:PGk3RjYHpxMM8HFPhKKo+vve3DdlPUELZLSDEFehPuU= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM= +github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 h1:MJG/KsmcqMwFAkh8mTnAwhyKoB+sTAnY4CACC110tbU= +github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645/go.mod h1:6iZfnjpejD4L/4DwD7NryNaJyCQdzwWwH2MWhCA90Kw= +github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8= +github.com/hamba/avro/v2 v2.17.2 h1:6PKpEWzJfNnvBgn7m2/8WYaDOUASxfDU+Jyb4ojDgFY= +github.com/hamba/avro/v2 v2.17.2/go.mod h1:Q9YK+qxAhtVrNqOhwlZTATLgLA8qxG2vtvkhK8fJ7Jo= +github.com/hanwen/go-fuse v1.0.0 h1:GxS9Zrn6c35/BnfiVsZVWmsG803xwE7eVRDvcf/BEVc= +github.com/hanwen/go-fuse/v2 v2.1.0 h1:+32ffteETaLYClUj0a3aHjZ1hOPxxaNEHiZiujuDaek= +github.com/hashicorp/consul/sdk v0.15.0 h1:2qK9nDrr4tiJKRoxPGhm6B7xJjLVIQqkjiab2M4aKjU= +github.com/hashicorp/go-syslog v1.0.0 h1:KaodqZuhUoZereWVIYmpUgZysurB1kBLX2j0MwMrUAE= +github.com/hashicorp/go.net v0.0.1 h1:sNCoNyDEvN1xa+X0baata4RdcpKwcMS6DH+xwfqPgjw= +github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= +github.com/hashicorp/mdns v1.0.4 h1:sY0CMhFmjIPDMlTB+HfymFHCaYLhgifZ0QhjaYKD/UQ= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= +github.com/hudl/fargo v1.4.0 h1:ZDDILMbB37UlAVLlWcJ2Iz1XuahZZTDZfdCKeclfq2s= +github.com/hydrogen18/memlistener v0.0.0-20200120041712-dcc25e7acd91 h1:KyZDvZ/GGn+r+Y3DKZ7UOQ/TP4xV6HNkrwiVMB1GnNY= +github.com/iancoleman/strcase v0.2.0 h1:05I4QRnGpI0m37iZQRuskXh+w77mr6Z41lwQzuHLwW0= +github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab h1:BA4a7pe6ZTd9F8kXETBoijjFJ/ntaa//1wiH9BZu4zU= +github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk= +github.com/influxdata/influxdb v1.7.6 h1:8mQ7A/V+3noMGCt/P9pD09ISaiz9XvgCk303UYA3gcs= +github.com/influxdata/influxdb1-client v0.0.0-20200827194710-b269163b24ab h1:HqW4xhhynfjrtEiiSGcQUd6vrK23iMam1FO8rI7mwig= +github.com/inhies/go-bytesize v0.0.0-20201103132853-d0aed0d254f8 h1:RrGCja4Grfz7QM2hw+SUZIYlbHoqBfbvzlWRT3seXB8= +github.com/iris-contrib/blackfriday v2.0.0+incompatible h1:o5sHQHHm0ToHUlAJSTjW9UWicjJSDDauOOQ2AHuIVp4= +github.com/iris-contrib/go.uuid v2.0.0+incompatible h1:XZubAYg61/JwnJNbZilGjf3b3pB80+OQg2qf6c8BfWE= +github.com/iris-contrib/jade v1.1.3 h1:p7J/50I0cjo0wq/VWVCDFd8taPJbuFC+bq23SniRFX0= +github.com/iris-contrib/pongo2 v0.0.1 h1:zGP7pW51oi5eQZMIlGA3I+FHY9/HOQWDB+572yin0to= +github.com/iris-contrib/schema v0.0.1 h1:10g/WnoRR+U+XXHWKBHeNy/+tZmM2kcAVGLOsz+yaDA= +github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0= +github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= +github.com/jackc/fake v0.0.0-20150926172116-812a484cc733 h1:vr3AYkKovP8uR8AvSGGUK1IDqRa5lAAvEkZG1LKaCRc= +github.com/jackc/pgconn v1.11.0 h1:HiHArx4yFbwl91X3qqIHtUFoiIfLNJXCQRsnzkiwwaQ= +github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= +github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A= +github.com/jackc/pgproto3/v2 v2.2.0 h1:r7JypeP2D3onoQTCxWdTpCtJ4D+qpKr0TxvoyMhZ5ns= +github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg= +github.com/jackc/pgtype v1.10.0 h1:ILnBWrRMSXGczYvmkYD6PsYyVFUNLTnIUJHHDLmqk38= +github.com/jackc/pgx v3.6.2+incompatible h1:2zP5OD7kiyR3xzRYMhOcXVvkDZsImVXfj+yIyTQf3/o= +github.com/jackc/pgx/v4 v4.15.0 h1:B7dTkXsdILD3MF987WGGCcg+tvLW6bZJdEcqVFeU//w= +github.com/jackc/puddle v1.2.1 h1:gI8os0wpRXFd4FiAY2dWiqRK037tjj3t7rKFeO4X5iw= +github.com/jackspirou/syscerts v0.0.0-20160531025014-b68f5469dff1 h1:9Xm8CKtMZIXgcopfdWk/qZ1rt0HjMgfMR9nxxSeK6vk= +github.com/jackspirou/syscerts v0.0.0-20160531025014-b68f5469dff1/go.mod h1:zuHl3Hh+e9P6gmBPvcqR1HjkaWHC/csgyskg6IaFKFo= +github.com/jaegertracing/jaeger v1.41.0 h1:vVNky8dP46M2RjGaZ7qRENqylW+tBFay3h57N16Ip7M= +github.com/jaegertracing/jaeger v1.41.0/go.mod h1:SIkAT75iVmA9U+mESGYuMH6UQv6V9Qy4qxo0lwfCQAc= +github.com/jandelgado/gcov2lcov v1.0.4-0.20210120124023-b83752c6dc08 h1:vn5CHED3UxZKIneSxETU9SXXGgsILP8hZHlx+M0u1BQ= +github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc= +github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= +github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= +github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= +github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= +github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg= +github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= +github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= +github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= +github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8= +github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= +github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= +github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= +github.com/jedib0t/go-pretty/v6 v6.2.4 h1:wdaj2KHD2W+mz8JgJ/Q6L/T5dB7kyqEFI16eLq7GEmk= +github.com/jedib0t/go-pretty/v6 v6.2.4/go.mod h1:+nE9fyyHGil+PuISTCrp7avEdo6bqoMwqZnuiK2r2a0= +github.com/jhump/gopoet v0.1.0 h1:gYjOPnzHd2nzB37xYQZxj4EIQNpBrBskRqQQ3q4ZgSg= +github.com/jhump/goprotoc v0.5.0 h1:Y1UgUX+txUznfqcGdDef8ZOVlyQvnV0pKWZH08RmZuo= +github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 h1:rp+c0RAYOWj8l6qbCUTSiRLG/iKnW3K3/QfPPuSsBt4= +github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= +github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o= +github.com/jsternberg/zap-logfmt v1.2.0 h1:1v+PK4/B48cy8cfQbxL4FmmNZrjnIMr2BsnyEmXqv2o= +github.com/jsternberg/zap-logfmt v1.2.0/go.mod h1:kz+1CUmCutPWABnNkOu9hOHKdT2q3TDYCcsFy9hpqb0= +github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d h1:c93kUJDtVAXFEhsCh5jSxyOJmFHuzcihnslQiX8Urwo= +github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= +github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5 h1:PJr+ZMXIecYc1Ey2zucXdR73SMBtgjPgwa31099IMv0= +github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 h1:uC1QfSlInpQF+M0ao65imhwqKnz3Q2z/d8PWZRMQvDM= +github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA= +github.com/karrick/godirwalk v1.16.1 h1:DynhcF+bztK8gooS0+NDJFrdNZjJ3gzVzC545UNA9iw= +github.com/kataras/golog v0.0.10 h1:vRDRUmwacco/pmBAm8geLn8rHEdc+9Z4NAr5Sh7TG/4= +github.com/kataras/iris/v12 v12.1.8 h1:O3gJasjm7ZxpxwTH8tApZsvf274scSGQAUpNe47c37U= +github.com/kataras/neffos v0.0.14 h1:pdJaTvUG3NQfeMbbVCI8JT2T5goPldyyfUB2PJfh1Bs= +github.com/kataras/pio v0.0.2 h1:6NAi+uPJ/Zuid6mrAKlgpbI11/zK/lV4B2rxWaJN98Y= +github.com/kataras/sitemap v0.0.5 h1:4HCONX5RLgVy6G4RkYOV3vKNcma9p236LdGOipJsaFE= +github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= +github.com/kisielk/errcheck v1.5.0 h1:e8esj/e4R+SAOwFwN+n3zr0nYeCyeweozKfO23MvHzY= +github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg= +github.com/klauspost/asmfmt v1.3.2 h1:4Ri7ox3EwapiOjCki+hw14RyKk201CN4rzyCJRFLpK4= +github.com/klauspost/cpuid v1.2.1 h1:vJi+O/nMdFt0vqm8NZBI6wzALWdA2X+egi0ogNyrC/w= +github.com/knadh/koanf v1.5.0 h1:q2TSd/3Pyc/5yP9ldIrSdIz26MCcyNQzW0pEAugLPNs= +github.com/knadh/koanf v1.5.0/go.mod h1:Hgyjp4y8v44hpZtPzs7JZfRAW5AhN7KfZcwv1RYggDs= +github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= +github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY= +github.com/kr/pty v1.1.8 h1:AkaSdXYQOWeaO3neb8EM634ahkXXe3jYbVh/F9lq+GI= +github.com/kshvakov/clickhouse v1.3.5 h1:PDTYk9VYgbjPAWry3AoDREeMgOVUFij6bh6IjlloHL0= +github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo= +github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= +github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= +github.com/lestrrat-go/backoff/v2 v2.0.8 h1:oNb5E5isby2kiro9AgdHLv5N5tint1AnDVVf2E2un5A= +github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y= +github.com/lestrrat-go/blackmagic v1.0.0 h1:XzdxDbuQTz0RZZEmdU7cnQxUtFUzgCSPq8RCz4BxIi4= +github.com/lestrrat-go/blackmagic v1.0.0/go.mod h1:TNgH//0vYSs8VXDCfkZLgIrVTTXQELZffUV0tz3MtdQ= +github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= +github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= +github.com/lestrrat-go/iter v1.0.1 h1:q8faalr2dY6o8bV45uwrxq12bRa1ezKrB6oM9FUgN4A= +github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc= +github.com/lestrrat-go/jwx v1.2.25 h1:tAx93jN2SdPvFn08fHNAhqFJazn5mBBOB8Zli0g0otA= +github.com/lestrrat-go/jwx v1.2.25/go.mod h1:zoNuZymNl5lgdcu6P7K6ie2QRll5HVfF4xwxBBK1NxY= +github.com/lestrrat-go/option v1.0.0 h1:WqAWL8kh8VcSoD6xjSH34/1m8yxluXQbDeKNfvFeEO4= +github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743 h1:143Bb8f8DuGWck/xpNUOckBVYfFbBTnLevfRZ1aVVqo= +github.com/lightstep/lightstep-tracer-go v0.18.1 h1:vi1F1IQ8N7hNWytK9DpJsUfQhGuNSc19z330K6vl4zk= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/luna-duclos/instrumentedsql v1.1.3 h1:t7mvC0z1jUt5A0UQ6I/0H31ryymuQRnJcWCiqV3lSAA= +github.com/lyft/protoc-gen-star v0.6.1 h1:erE0rdztuaDq3bpGifD95wfoPrSZc95nGA6tbiNYh6M= +github.com/lyft/protoc-gen-star/v2 v2.0.3 h1:/3+/2sWyXeMLzKd1bX+ixWKgEMsULrIivpDsuaF441o= +github.com/lyft/protoc-gen-validate v0.0.13 h1:KNt/RhmQTOLr7Aj8PsJ7mTronaFyx80mRTT9qF261dA= +github.com/markbates/deplist v1.1.3 h1:/OcV27jxF6aLU+rVGF1RQdT2n+qMlKXeQF6yA0cBQ4k= +github.com/markbates/errx v1.1.0 h1:QDFeR+UP95dO12JgW+tgi2UVfo0V8YBHiUIOaeBPiEI= +github.com/markbates/going v1.0.2 h1:uNQHDDfMRNOUmuxDbPbvatyw4wr4UOSUZkGkdkcip1o= +github.com/markbates/grift v1.0.4 h1:JjTyhlgPtgEnyHNvVn5lk21zWQbWD3cGE0YdyvvbZYg= +github.com/markbates/hmax v1.0.0 h1:yo2N0gBoCnUMKhV/VRLHomT6Y9wUm+oQQENuWJqCdlM= +github.com/markbates/inflect v1.0.4 h1:5fh1gzTFhfae06u3hzHYO9xe3l3v3nW5Pwt3naLTP5g= +github.com/markbates/oncer v1.0.0 h1:E83IaVAHygyndzPimgUYJjbshhDTALZyXxvk9FOlQRY= +github.com/markbates/pkger v0.17.1 h1:/MKEtWqtc0mZvu9OinB9UzVN9iYCwLWuyUv4Bw+PCno= +github.com/markbates/refresh v1.4.10 h1:6EZ/vvVpWiam8OTIhrhfV9cVJR/NvScvcCiqosbTkbA= +github.com/markbates/safe v1.0.1 h1:yjZkbvRM6IzKj9tlu/zMJLS0n/V351OZWRnF3QfaUxI= +github.com/markbates/sigtx v1.0.0 h1:y/xtkBvNPRjD4KeEplf4w9rJVSc23/xl+jXYGowTwy0= +github.com/markbates/willie v1.0.9 h1:394PpHImWjScL9X2VRCDXJAcc77sHsSr3w3sOnL/DVc= +github.com/matryer/moq v0.2.7 h1:RtpiPUM8L7ZSCbSwK+QcZH/E9tgqAkFjKQxsRs25b4w= +github.com/matryer/moq v0.2.7/go.mod h1:kITsx543GOENm48TUAQyJ9+SAvFSr7iGQXPoth/VUBk= +github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= +github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g= +github.com/mediocregopher/radix/v3 v3.4.2 h1:galbPBjIwmyREgwGCfQEN4X8lxbJnKBYurgz+VfcStA= +github.com/microcosm-cc/bluemonday v1.0.2 h1:5lPfLTTAvAbtS0VqT+94yOtFnGfUWYyx0+iToC3Os3s= +github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 h1:AMFGa4R4MiIpspGNG7Z948v4n35fFGB3RR3G/ry4FWs= +github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 h1:+n/aFZefKZp7spd8DFdX7uMikMLXX4oubIzJF4kv/wI= +github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g= +github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= +github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= +github.com/minio/minio-go/v7 v7.0.52 h1:8XhG36F6oKQUDDSuz6dY3rioMzovKjW40W6ANuN0Dps= +github.com/minio/minio-go/v7 v7.0.52/go.mod h1:IbbodHyjUAguneyucUaahv+VMNs/EOTV9du7A7/Z3HU= +github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= +github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= +github.com/mitchellh/cli v1.1.5 h1:OxRIeJXpAMztws/XHlN2vu6imG5Dpq+j61AzAX5fLng= +github.com/mitchellh/gox v0.4.0 h1:lfGJxY7ToLJQjHHwi0EX6uYBdK78egf954SQl13PQJc= +github.com/mitchellh/iochan v1.0.0 h1:C+X3KsSTLFVBr/tK1eYN/vs4rJcvsiLU338UhYPJWeY= +github.com/mithrandie/readline-csvq v1.2.1 h1:4cfeYeVSrqKEWi/1t7CjyhFD2yS6fm+l+oe+WyoSNlI= +github.com/mithrandie/readline-csvq v1.2.1/go.mod h1:ydD9Eyp3/wn8KPSNbKmMZe4RQQauCuxi26yEo4N40dk= +github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5 h1:8Q0qkMVC/MmWkpIdlvZgcv2o2jrlF6zqVOh7W5YHdMA= +github.com/monoculum/formam v0.0.0-20180901015400-4e68be1d79ba h1:FEJJhVHSH+Kyxa5qNe/7dprlZbFcj2TG51OWIouhwls= +github.com/montanaflynn/stats v0.7.0 h1:r3y12KyNxj/Sb/iOE46ws+3mS1+MZca1wlHQFPsY/JU= +github.com/mostynb/go-grpc-compression v1.1.17 h1:N9t6taOJN3mNTTi0wDf4e3lp/G/ON1TP67Pn0vTUA9I= +github.com/mostynb/go-grpc-compression v1.1.17/go.mod h1:FUSBr0QjKqQgoDG/e0yiqlR6aqyXC39+g/hFLDfSsEY= +github.com/moul/http2curl v1.0.0 h1:dRMWoAtb+ePxMlLkrCbAqh4TlPHXvoGUSQ323/9Zahs= +github.com/nakagami/firebirdsql v0.0.0-20190310045651-3c02a58cfed8 h1:P48LjvUQpTReR3TQRbxSeSBsMXzfK0uol7eRcr7VBYQ= +github.com/natessilva/dag v0.0.0-20180124060714-7194b8dcc5c4 h1:dnMxwus89s86tI8rcGVp2HwZzlz7c5o92VOy7dSckBQ= +github.com/nats-io/jwt v1.2.2 h1:w3GMTO969dFg+UOKTmmyuu7IGdusK+7Ytlt//OYH/uU= +github.com/nats-io/jwt/v2 v2.0.3 h1:i/O6cmIsjpcQyWDYNcq2JyZ3/VTF8SJ4JWluI5OhpvI= +github.com/nats-io/nats-server/v2 v2.5.0 h1:wsnVaaXH9VRSg+A2MVg5Q727/CqxnmPLGFQ3YZYKTQg= +github.com/nats-io/nats.go v1.12.1 h1:+0ndxwUPz3CmQ2vjbXdkC1fo3FdiOQDim4gl3Mge8Qo= +github.com/nats-io/nkeys v0.3.0 h1:cgM5tL53EvYRU+2YLXIK0G2mJtK12Ft9oeooSZMA2G8= +github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= +github.com/nicksnyder/go-i18n v1.10.0 h1:5AzlPKvXBH4qBzmZ09Ua9Gipyruv6uApMcrNZdo96+Q= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/oklog/oklog v0.3.2 h1:wVfs8F+in6nTBMkA7CbRw+zZMIB7nNM825cM1wuzoTk= +github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU= +github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= +github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88= +github.com/open-telemetry/opentelemetry-collector-contrib/exporter/jaegerexporter v0.74.0 h1:0dve/IbuHfQOnlIBQQwpCxIeMp7uig9DQVuvisWPDRs= +github.com/open-telemetry/opentelemetry-collector-contrib/exporter/jaegerexporter v0.74.0/go.mod h1:bIeSj+SaZdP3CE9Xae+zurdQC6DXX0tPP6NAEVmgtt4= +github.com/open-telemetry/opentelemetry-collector-contrib/exporter/kafkaexporter v0.74.0 h1:MrVOfBTNBe4n/daZjV4yvHZRR0Jg/MOCl/mNwymHwDM= +github.com/open-telemetry/opentelemetry-collector-contrib/exporter/kafkaexporter v0.74.0/go.mod h1:v4H2ATSrKfOTbQnmjCxpvuOjrO/GUURAgey9RzrPsuQ= +github.com/open-telemetry/opentelemetry-collector-contrib/exporter/zipkinexporter v0.74.0 h1:8Kk5g5PKQBUV3idjJy1NWVLLReEzjnB8C1lFgQxZ0TI= +github.com/open-telemetry/opentelemetry-collector-contrib/exporter/zipkinexporter v0.74.0/go.mod h1:UtVfxZGhPU2OvDh7H8o67VKWG9qHAHRNkhmZUWqCvME= +github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal v0.74.0 h1:vU5ZebauzCuYNXFlQaWaYnOfjoOAnS+Sc8+oNWoHkbM= +github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal v0.74.0/go.mod h1:TEu3TnUv1TuyHtjllrUDQ/ImpyD+GrkDejZv4hxl3G8= +github.com/open-telemetry/opentelemetry-collector-contrib/internal/sharedcomponent v0.74.0 h1:COFBWXiWnhRs9x1oYJbDg5cyiNAozp8sycriD9+1/7E= +github.com/open-telemetry/opentelemetry-collector-contrib/internal/sharedcomponent v0.74.0/go.mod h1:cAKlYKU+/8mk6ETOnD+EAi5gpXZjDrGweAB9YTYrv/g= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/jaeger v0.74.0 h1:ww1pPXfAM0WHsymQnsN+s4B9DgwQC+GyoBq0t27JV/k= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/jaeger v0.74.0/go.mod h1:OpEw7tyCg+iG1ywEgZ03qe5sP/8fhYdtWCMoqA8JCug= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/opencensus v0.74.0 h1:0Fh6OjlUB9HlnX90/gGiyyFvnmNBv6inj7bSaVqQ7UQ= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/opencensus v0.74.0/go.mod h1:13ekplz1UmvK99Vz2VjSBWPYqoRBEax5LPmA1tFHnhA= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/zipkin v0.74.0 h1:A5xoBaMHX1WzLfvlqK6NBXq4XIbuSVJIpec5r6PDE7U= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/zipkin v0.74.0/go.mod h1:TJT7HkhFPrJic30Vk4seF/eRk8sa0VQ442Xq/qd+DLY= +github.com/open-telemetry/opentelemetry-collector-contrib/receiver/jaegerreceiver v0.74.0 h1:pWNSPCKD+V4rC+MnZj8uErEbcsYUpEqU3InNYyafAPY= +github.com/open-telemetry/opentelemetry-collector-contrib/receiver/jaegerreceiver v0.74.0/go.mod h1:0lXcDf6LUbtDxZZO3zDbRzMuL7gL1Q0FPOR8/3IBwaQ= +github.com/open-telemetry/opentelemetry-collector-contrib/receiver/kafkareceiver v0.74.0 h1:NWd9+rQTd6pELLf3copo7CEuNgKp90kgyhPozpwax2U= +github.com/open-telemetry/opentelemetry-collector-contrib/receiver/kafkareceiver v0.74.0/go.mod h1:anSbwGOousKpnNAVMNP5YieA4KOFuEzHkvya0vvtsaI= +github.com/open-telemetry/opentelemetry-collector-contrib/receiver/opencensusreceiver v0.74.0 h1:Law7+BImq8DIBsdniSX8Iy2/GH5CRHpT1gsRaC9ZT8A= +github.com/open-telemetry/opentelemetry-collector-contrib/receiver/opencensusreceiver v0.74.0/go.mod h1:uiW3V9EX8A5DOoxqDLuSh++ewHr+owtonCSiqMcpy3w= +github.com/open-telemetry/opentelemetry-collector-contrib/receiver/zipkinreceiver v0.74.0 h1:2uysjsaqkf9STFeJN/M6i/sSYEN5pZJ94Qd2/Hg1pKE= +github.com/open-telemetry/opentelemetry-collector-contrib/receiver/zipkinreceiver v0.74.0/go.mod h1:qoGuayD7cAtshnKosIQHd6dobcn6/sqgUn0v/Cg2UB8= +github.com/opencontainers/runc v1.0.0-rc9 h1:/k06BMULKF5hidyoZymkoDCzdJzltZpz/UU4LguQVtc= +github.com/opentracing-contrib/go-grpc v0.0.0-20210225150812-73cb765af46e h1:4cPxUYdgaGzZIT5/j0IfqOrrXmq6bG8AwvwisMXpdrg= +github.com/opentracing-contrib/go-grpc v0.0.0-20210225150812-73cb765af46e/go.mod h1:DYR5Eij8rJl8h7gblRrOZ8g0kW1umSpKqYIBTgeDtLo= +github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492 h1:lM6RxxfUMrYL/f8bWEUqdXrANWtrL7Nndbm9iFN0DlU= +github.com/opentracing/basictracer-go v1.0.0 h1:YyUAhaEfjoWXclZVJ9sGoNct7j4TVk7lZWlQw5UXuoo= +github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5 h1:ZCnq+JUrvXcDVhX/xRolRBZifmabN1HcS1wrPSvxhrU= +github.com/openzipkin/zipkin-go v0.4.1 h1:kNd/ST2yLLWhaWrkgchya40TJabe8Hioj9udfPcEO5A= +github.com/openzipkin/zipkin-go v0.4.1/go.mod h1:qY0VqDSN1pOBN94dBc6w2GJlWLiovAyg7Qt6/I9HecM= +github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw= +github.com/ory/analytics-go/v4 v4.0.0 h1:KQ2P00j9dbj4lDC/Albw/zn/scK67041RhqeW5ptsbw= +github.com/ory/dockertest v3.3.5+incompatible h1:iLLK6SQwIhcbrG783Dghaaa3WPzGc+4Emza6EbVUUGA= +github.com/ory/dockertest/v3 v3.6.3 h1:L8JWiGgR+fnj90AEOkTFIEp4j5uWAK72P3IUsYgn2cs= +github.com/ory/gojsonreference v0.0.0-20190720135523-6b606c2d8ee8 h1:e2S2FmxqSbhFyVNP24HncpRY+X1qAZmtE3nZ0gJKR4Q= +github.com/ory/gojsonschema v1.1.1-0.20190919112458-f254ca73d5e9 h1:LDIG2Mnha10nFZuVXv3GIBqhQ1+JLwRXPcP4Ykx5VOY= +github.com/ory/herodot v0.9.2 h1:/54FEEMCJNUJKIYRTioOS/0dxdzc9yNtI8/DRVF6KfY= +github.com/ory/jsonschema/v3 v3.0.1 h1:xzV7w2rt/Qn+jvh71joIXNKKOCqqNyTlaIxdxU0IQJc= +github.com/pact-foundation/pact-go v1.0.4 h1:OYkFijGHoZAYbOIb1LWXrwKQbMMRUv1oQ89blD2Mh2Q= +github.com/parnurzeal/gorequest v0.2.15 h1:oPjDCsF5IkD4gUk6vIgsxYNaSgvAnIh1EJeROn3HdJU= +github.com/pelletier/go-toml/v2 v2.0.5 h1:ipoSadvV8oGUjnUbMub59IDPPwfxF694nG/jwbMiyQg= +github.com/performancecopilot/speed v3.0.0+incompatible h1:2WnRzIquHa5QxaJKShDkLM+sc0JPuwhXzK8OYOyt3Vg= +github.com/performancecopilot/speed/v4 v4.0.0 h1:VxEDCmdkfbQYDlcr/GC9YoN9PQ6p8ulk9xVsepYy9ZY= +github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= +github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc= +github.com/philhofer/fwd v1.0.0 h1:UbZqGr5Y38ApvM/V/jEljVxwocdweyH+vmYvRPBnbqQ= +github.com/phpdave11/gofpdf v1.4.2 h1:KPKiIbfwbvC/wOncwhrpRdXVj2CZTCFlw4wnoyjtHfQ= +github.com/phpdave11/gofpdi v1.0.13 h1:o61duiW8M9sMlkVXWlvP92sZJtGKENvW3VExs6dZukQ= +github.com/pierrec/lz4 v2.0.5+incompatible h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM8aXeqhl0I= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgcC4zIxODThtZNPirFr42+A= +github.com/pkg/profile v1.2.1 h1:F++O52m40owAmADcojzM+9gyjmMOY/T4oYJkgFDH8RE= +github.com/pkg/sftp v1.13.1 h1:I2qBYMChEhIjOgazfJmV3/mZM256btk6wkCDRmW7JYs= +github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/pquerna/cachecontrol v0.1.0 h1:yJMy84ti9h/+OEWa752kBTKv4XC30OtVVHYv/8cTqKc= +github.com/pquerna/cachecontrol v0.1.0/go.mod h1:NrUG3Z7Rdu85UNR3vm7SOsl1nFIeSiQnrHV5K9mBcUI= +github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= +github.com/prometheus/common/assets v0.2.0 h1:0P5OrzoHrYBOSM1OigWL3mY8ZvV2N4zIE/5AahrSrfM= +github.com/prometheus/statsd_exporter v0.22.7 h1:7Pji/i2GuhK6Lu7DHrtTkFmNBCudCPT1pX2CziuyQR0= +github.com/prometheus/statsd_exporter v0.22.7/go.mod h1:N/TevpjkIh9ccs6nuzY3jQn9dFqnUakOjnEuMPJJJnI= +github.com/prometheus/tsdb v0.7.1 h1:YZcsG11NqnK4czYLrWd9mpEuAJIHVQLwdrleYfszMAA= +github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= +github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/rhnvrm/simples3 v0.5.0 h1:X+WX0hqoKScdoJAw/G3GArfZ6Ygsn8q+6MdocTMKXOw= +github.com/rogpeppe/fastuuid v1.2.0 h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi2s= +github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4 h1:BN/Nyn2nWMoqGRA7G7paDNDqTXE30mXGqzzybrfo05w= +github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.15.0 h1:uPRuwkWF4J6fGsJ2R0Gn2jB1EQiav9k3S6CSdygQJXY= +github.com/rubenv/sql-migrate v0.0.0-20190212093014-1007f53448d7 h1:ID2fzWzRFJcF/xf/8eLN9GW5CXb6NQnKfC+ksTwMNpY= +github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww= +github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245 h1:K1Xf3bKttbF+koVGaX5xngRIZ5bVjbmPnaxE/dR08uY= +github.com/ryanuber/columnize v2.1.2+incompatible h1:C89EOx/XBWwIXl8wm8OPJBd7kPF25UfsK2X7Ph/zCAk= +github.com/sagikazarmark/crypt v0.6.0 h1:REOEXCs/NFY/1jOCEouMuT4zEniE5YoXbvpC5X/TLF8= +github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da h1:p3Vo3i64TCLY7gIfzeQaUJ+kppEO5WQG3cL8iE8tGHU= +github.com/santhosh-tekuri/jsonschema v1.2.4 h1:hNhW8e7t+H1vgY+1QeEQpveR6D4+OwKPXCfD2aieJis= +github.com/santhosh-tekuri/jsonschema/v2 v2.1.0 h1:7KOtBzox6l1PbyZCuQfo923yIBpoMtGCDOD78P9lv9g= +github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= +github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiyyjYS17cCYRqP13/SHk= +github.com/seatgeek/logrus-gelf-formatter v0.0.0-20210219220335-367fa274be2c h1:jwWrlqKHQeSRjTskQaHBtCOWbaMsd54NBAnofYbEHGs= +github.com/segmentio/analytics-go v3.1.0+incompatible h1:IyiOfUgQFVHvsykKKbdI7ZsH374uv3/DfZUo9+G0Z80= +github.com/segmentio/backo-go v0.0.0-20200129164019-23eae7c10bd3 h1:ZuhckGJ10ulaKkdvJtiAqsLTiPrLaXSdnVgXJKJkTxE= +github.com/segmentio/conf v1.2.0 h1:5OT9+6OyVHLsFLsiJa/2KlqiA1m7mpdUBlkB/qYTMts= +github.com/segmentio/fasthash v0.0.0-20180216231524-a72b379d632e h1:uO75wNGioszjmIzcY/tvdDYKRLVvzggtAmmJkn9j4GQ= +github.com/segmentio/fasthash v0.0.0-20180216231524-a72b379d632e/go.mod h1:tm/wZFQ8e24NYaBGIlnO2WGCAi67re4HHuOm0sftE/M= +github.com/segmentio/go-snakecase v1.1.0 h1:ZJO4SNKKV0MjGOv0LHnixxN5FYv1JKBnVXEuBpwcbQI= +github.com/segmentio/objconv v1.0.1 h1:QjfLzwriJj40JibCV3MGSEiAoXixbp4ybhwfTB8RXOM= +github.com/segmentio/parquet-go v0.0.0-20230427215636-d483faba23a5 h1:7CWCjaHrXSUCHrRhIARMGDVKdB82tnPAQMmANeflKOw= +github.com/segmentio/parquet-go v0.0.0-20230427215636-d483faba23a5/go.mod h1:+J0xQnJjm8DuQUHBO7t57EnmPbstT6+b45+p3DC9k1Q= +github.com/sercand/kuberesolver/v4 v4.0.0 h1:frL7laPDG/lFm5n98ODmWnn+cvPpzlkf3LhzuPhcHP4= +github.com/sercand/kuberesolver/v4 v4.0.0/go.mod h1:F4RGyuRmMAjeXHKL+w4P7AwUnPceEAPAhxUgXZjKgvM= +github.com/sercand/kuberesolver/v5 v5.1.1 h1:CYH+d67G0sGBj7q5wLK61yzqJJ8gLLC8aeprPTHb6yY= +github.com/sercand/kuberesolver/v5 v5.1.1/go.mod h1:Fs1KbKhVRnB2aDWN12NjKCB+RgYMWZJ294T3BtmVCpQ= +github.com/serenize/snaker v0.0.0-20171204205717-a683aaf2d516 h1:ofR1ZdrNSkiWcMsRrubK9tb2/SlZVWttAfqUjJi6QYc= +github.com/shirou/gopsutil/v3 v3.23.2 h1:PAWSuiAszn7IhPMBtXsbSCafej7PqUOvY6YywlQUExU= +github.com/shirou/gopsutil/v3 v3.23.2/go.mod h1:gv0aQw33GLo3pG8SiWKiQrbDzbRY1K80RyZJ7V4Th1M= +github.com/shoenig/test v0.6.6 h1:Oe8TPH9wAbv++YPNDKJWUnI8Q4PPWCx3UbOfH+FxiMU= +github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e h1:MZM7FHLqUHYI0Y/mQAt3d2aYa0SiNms/hFqC9qJYolM= +github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041 h1:llrF3Fs4018ePo4+G/HV/uQUqEI1HMDjCeOf2V6puPc= +github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b h1:vYEG87HxbU6dXj5npkeulCS96Dtz5xg3jcfCgpcvbIw= +github.com/shurcooL/highlight_go v0.0.0-20170515013102-78fb10f4a5f8 h1:xLQlo0Ghg8zBaQi+tjpK+z/WLjbg/BhAWP9pYgqo/LQ= +github.com/shurcooL/octicon v0.0.0-20180602230221-c42b0e3b24d9 h1:j3cAp1j8k/tSLaCcDiXIpVJ8FzSJ9g1eeOAPRJYM75k= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/sony/gobreaker v0.4.1 h1:oMnRNZXX5j85zso6xCPRNPtmAycat+WcoKbklScLDgQ= +github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d h1:yKm7XZV6j9Ev6lojP2XaIshpT4ymkqhMeSghO5Ps00E= +github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e h1:qpG93cPwA5f7s/ZPBJnGOYQNK/vKsaDaseuKT5Asee8= +github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/viper v1.14.0 h1:Rg7d3Lo706X9tHsJMUjdiwMpHB7W8WnSVOssIY+JElU= +github.com/spf13/viper v1.14.0/go.mod h1:WT//axPky3FdvXHzGw33dNdXXXfFQqmEalje+egj8As= +github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad h1:fiWzISvDn0Csy5H0iwgAuJGQTUpVfEMJJd4nRFXogbc= +github.com/sqs/goreturns v0.0.0-20181028201513-538ac6014518 h1:iD+PFTQwKEmbwSdwfvP5ld2WEI/g7qbdhmHJ2ASfYGs= +github.com/square/go-jose/v3 v3.0.0-20200630053402-0a67ce9b0693 h1:wD1IWQwAhdWclCwaf6DdzgCAe9Bfz1M+4AHRd7N786Y= +github.com/streadway/amqp v1.0.0 h1:kuuDrUJFZL1QYL9hUNuCxNObNzB0bV/ZG5jV3RWAQgo= +github.com/streadway/handy v0.0.0-20200128134331-0f66f006fb2e h1:mOtuXaRAbVZsxAHVdPR3IjfmN8T1h2iczJLynhLybf8= +github.com/substrait-io/substrait-go v0.4.2 h1:buDnjsb3qAqTaNbOR7VKmNgXf4lYQxWEcnSGUWBtmN8= +github.com/substrait-io/substrait-go v0.4.2/go.mod h1:qhpnLmrcvAnlZsUyPXZRqldiHapPTXC3t7xFgDi3aQg= +github.com/tidwall/gjson v1.14.2 h1:6BBkirS0rAHjumnjHF6qgy5d2YAJ1TLIaFE2lzfOLqo= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +github.com/tinylib/msgp v1.1.2 h1:gWmO7n0Ys2RBEb7GPYB9Ujq8Mk5p2U08lRnmMcGy6BQ= +github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM= +github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI= +github.com/tklauser/numcpus v0.6.0 h1:kebhY2Qt+3U6RNK7UqpYNA+tJ23IBEGKkB7JQBfDYms= +github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ5UVIcaL4= +github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926 h1:G3dpKMzFDjgEh2q1Z7zUUtKa8ViPtH+ocF0bE0g00O8= +github.com/uber-go/atomic v1.4.0 h1:yOuPqEq4ovnhEjpHmfFwsqBXDYbQeT6Nb0bwD6XnD5o= +github.com/uber-go/atomic v1.4.0/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex1PFV8g= +github.com/unrolled/secure v0.0.0-20181005190816-ff9db2ff917f h1:ltz/eIXkYWdMCZbu3Rb+bUmWVTm5AqM0QM8o0uKir4U= +github.com/urfave/negroni v1.0.0 h1:kIimOitoypq34K7TG7DUaJ9kq/N4Ofuwi1sjz0KipXc= +github.com/valyala/fasthttp v1.6.0 h1:uWF8lgKmeaIewWVPwi4GRq2P6+R46IgYZdxWtM+GtEY= +github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a h1:0R4NLDRDZX6JcmhJgXi5E4b8Wg84ihbmUKp/GvSPEzc= +github.com/vinzenz/yaml v0.0.0-20170920082545-91409cdd725d h1:3wDi6J5APMqaHBVPuVd7RmHD2gRTfqbdcVSpCNoUWtk= +github.com/vinzenz/yaml v0.0.0-20170920082545-91409cdd725d/go.mod h1:mb5taDqMnJiZNRQ3+02W2IFG+oEz1+dTuCXkp4jpkfo= +github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= +github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +github.com/weaveworks/common v0.0.0-20230511094633-334485600903 h1:ph7R2CS/0o1gBzpzK/CioUKJVsXNVXfDGR8FZ9rMZIw= +github.com/weaveworks/common v0.0.0-20230511094633-334485600903/go.mod h1:rgbeLfJUtEr+G74cwFPR1k/4N0kDeaeSv/qhUNE4hm8= +github.com/weaveworks/promrus v1.2.0 h1:jOLf6pe6/vss4qGHjXmGz4oDJQA+AOCqEL3FvvZGz7M= +github.com/weaveworks/promrus v1.2.0/go.mod h1:SaE82+OJ91yqjrE1rsvBWVzNZKcHYFtMUyS1+Ogs/KA= +github.com/willf/bitset v1.1.11 h1:N7Z7E9UvjW+sGsEl7k/SJrvY2reP1A07MrGuCjIOjRE= +github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI= +github.com/willf/bloom v2.0.3+incompatible h1:QDacWdqcAUI1MPOwIQZRy9kOR7yxfyEmxX8Wdm2/JPA= +github.com/willf/bloom v2.0.3+incompatible/go.mod h1:MmAltL9pDMNTrvUkxdg0k0q5I0suxmuwp3KbyrZLOZ8= +github.com/xanzy/go-gitlab v0.15.0 h1:rWtwKTgEnXyNUGrOArN7yyc3THRkpYcKXIXia9abywQ= +github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= +github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= +github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= +github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c h1:u40Z8hqBAAQyv+vATcGgV0YCnDjqSL7/q/JyPhhJSPk= +github.com/xdg/stringprep v1.0.0 h1:d9X0esnoa3dFsV0FG35rAT0RIhYFlPq7MiP+DW89La0= +github.com/xhit/go-str2duration v1.2.0 h1:BcV5u025cITWxEQKGWr1URRzrcXtu7uk8+luz3Yuhwc= +github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77 h1:ESFSdwYZvkeru3RtdrYueztKhOBCSAAzS4Gf+k0tEow= +github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c h1:3lbZUMbMiGUW/LMkfsEABsc5zNT9+b1CvsJx47JzJ8g= +github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA= +github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= +github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg= +github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b h1:FosyBZYxY34Wul7O/MSKey3txpPYyCqVO5ZyceuQJEI= +github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8= +github.com/zenazn/goji v1.0.1 h1:4lbD8Mx2h7IvloP7r2C0D6ltZP6Ufip8Hn0wmSK5LR8= +github.com/zenazn/goji v1.0.1/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= +github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs= +gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b h1:7gd+rd8P3bqcn/96gOZa3F5dpJr/vEiDQYlNb/y2uNs= +go.elastic.co/apm v1.8.0 h1:AWEKpHwRal0yCMd4K8Oxy1HAa7xid+xq1yy+XjgoVU0= +go.elastic.co/apm/module/apmhttp v1.8.0 h1:5AJPefWJzWDLX/47XIDfaloGiYWkkOQEULvlrI6Ieaw= +go.elastic.co/apm/module/apmot v1.8.0 h1:7r8b5RGDN5gAUG7FoegzJ24+jFSZF7FvY2ODODaKFYk= +go.elastic.co/fastjson v1.0.0 h1:ooXV/ABvf+tBul26jcVViPT3sBir0PvXgibYB1IQQzg= +go.opentelemetry.io/collector v0.74.0 h1:0s2DKWczGj/pLTsXGb1P+Je7dyuGx9Is4/Dri1+cS7g= +go.opentelemetry.io/collector v0.74.0/go.mod h1:7NjZAvkhQ6E+NLN4EAH2hw3Nssi+F14t7mV7lMNXCto= +go.opentelemetry.io/collector/component v0.74.0 h1:W32ILPgbA5LO+m9Se61hbbtiLM6FYusNM36K5/CCOi0= +go.opentelemetry.io/collector/component v0.74.0/go.mod h1:zHbWqbdmnHeIZAuO3s1Fo/kWPC2oKuolIhlPmL4bzyo= +go.opentelemetry.io/collector/confmap v0.74.0 h1:tl4fSHC/MXZiEvsZhDhd03TgzvArOe69Qn020sZsTfQ= +go.opentelemetry.io/collector/confmap v0.74.0/go.mod h1:NvUhMS2v8rniLvDAnvGjYOt0qBohk6TIibb1NuyVB1Q= +go.opentelemetry.io/collector/consumer v0.74.0 h1:+kjT/ixG+4SVSHg7u9mQe0+LNDc6PuG8Wn2hoL/yGYk= +go.opentelemetry.io/collector/consumer v0.74.0/go.mod h1:MuGqt8/OKVAOjrh5WHr1TR2qwHizy64ZP2uNSr+XpvI= +go.opentelemetry.io/collector/exporter v0.74.0 h1:VZxDuVz9kJM/Yten3xA/abJwLJNkxLThiao6E1ULW7c= +go.opentelemetry.io/collector/exporter v0.74.0/go.mod h1:kw5YoorpKqEpZZ/a5ODSoYFK1mszzcKBNORd32S8Z7c= +go.opentelemetry.io/collector/exporter/otlpexporter v0.74.0 h1:YKvTeYcBrJwbcXNy65fJ/xytUSMurpYn/KkJD0x+DAY= +go.opentelemetry.io/collector/exporter/otlpexporter v0.74.0/go.mod h1:cRbvsnpSxzySoTSnXbOGPQZu9KHlEyKkTeE21f9Q1p4= +go.opentelemetry.io/collector/featuregate v1.0.0 h1:5MGqe2v5zxaoo73BUOvUTunftX5J8RGrbFsC2Ha7N3g= +go.opentelemetry.io/collector/receiver v0.74.0 h1:jlgBFa0iByvn8VuX27UxtqiPiZE8ejmU5lb1nSptWD8= +go.opentelemetry.io/collector/receiver v0.74.0/go.mod h1:SQkyATvoZCJefNkI2jnrR63SOdrmDLYCnQqXJ7ACqn0= +go.opentelemetry.io/collector/receiver/otlpreceiver v0.74.0 h1:e/X/W0z2Jtpy3Yd3CXkmEm9vSpKq/P3pKUrEVMUFBRw= +go.opentelemetry.io/collector/receiver/otlpreceiver v0.74.0/go.mod h1:9X9/RYFxJIaK0JLlRZ0PpmQSSlYpY+r4KsTOj2jWj14= +go.opentelemetry.io/collector/semconv v0.90.1 h1:2fkQZbefQBbIcNb9Rk1mRcWlFZgQOk7CpST1e1BK8eg= +go.opentelemetry.io/contrib v0.18.0 h1:uqBh0brileIvG6luvBjdxzoFL8lxDGuhxJWsvK3BveI= +go.opentelemetry.io/contrib/propagators/b3 v1.15.0 h1:bMaonPyFcAvZ4EVzkUNkfnUHP5Zi63CIDlA3dRsEg8Q= +go.opentelemetry.io/contrib/propagators/b3 v1.15.0/go.mod h1:VjU0g2v6HSQ+NwfifambSLAeBgevjIcqmceaKWEzl0c= +go.opentelemetry.io/otel/bridge/opencensus v0.37.0 h1:ieH3gw7b1eg90ARsFAlAsX5LKVZgnCYfaDwRrK6xLHU= +go.opentelemetry.io/otel/bridge/opencensus v0.37.0/go.mod h1:ddiK+1PE68l/Xk04BGTh9Y6WIcxcLrmcVxVlS0w5WZ0= +go.opentelemetry.io/otel/bridge/opentracing v1.10.0 h1:WzAVGovpC1s7KD5g4taU6BWYZP3QGSDVTlbRu9fIHw8= +go.opentelemetry.io/otel/bridge/opentracing v1.10.0/go.mod h1:J7GLR/uxxqMAzZptsH0pjte3Ep4GacTCrbGBoDuHBqk= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0 h1:digkEZCJWobwBqMwC0cwCq8/wkkRy/OowZg5OArWZrM= +go.opentelemetry.io/otel/exporters/prometheus v0.37.0 h1:NQc0epfL0xItsmGgSXgfbH2C1fq2VLXkZoDFsfRNHpc= +go.opentelemetry.io/otel/exporters/prometheus v0.37.0/go.mod h1:hB8qWjsStK36t50/R0V2ULFb4u95X/Q6zupXLgvjTh8= +go.opentelemetry.io/otel/oteltest v0.18.0 h1:FbKDFm/LnQDOHuGjED+fy3s5YMVg0z019GJ9Er66hYo= +go.opentelemetry.io/otel/sdk/metric v0.39.0 h1:Kun8i1eYf48kHH83RucG93ffz0zGV1sh46FAScOTuDI= +go.opentelemetry.io/otel/sdk/metric v0.39.0/go.mod h1:piDIRgjcK7u0HCL5pCA4e74qpK/jk3NiUoAHATVAmiI= +go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8= +go.uber.org/mock v0.2.0 h1:TaP3xedm7JaAgScZO7tlvlKrqT0p7I6OsdGB5YNSMDU= +go.uber.org/mock v0.2.0/go.mod h1:J0y0rp9L3xiff1+ZBfKxlC1fz2+aO16tw0tsDOixfuM= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= +golang.org/x/image v0.0.0-20220302094943-723b81ca9867 h1:TcHcE0vrmgzNH1v3ppjcMGbhG5+9fMuvOmUYwNEF4q4= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028 h1:4+4C/Iv2U4fMZBiMCc98MG1In4gJY5YRhtpDNeDeHWs= +gonum.org/v1/netlib v0.0.0-20191229114700-bbb4dff026f8 h1:kHY67jAKYewKUCz9YdNDa7iLAJ2WfNmoHzCCX4KnA8w= +gonum.org/v1/plot v0.10.1 h1:dnifSs43YJuNMDzB7v8wV64O4ABBHReuAVAoBxqBqS4= +google.golang.org/genproto/googleapis/bytestream v0.0.0-20231120223509-83a465c0220f h1:hL+1ptbhFoeL1HcROQ8OGXaqH0jYRRibgWQWco0/Ugc= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0 h1:M1YKkFIboKNieVO5DLUEVzQfGwJD30Nv2jfUgzb5UcE= +google.golang.org/grpc/examples v0.0.0-20210304020650-930c79186c99 h1:qA8rMbz1wQ4DOFfM2ouD29DG9aHWBm6ZOy9BGxiUMmY= +gopkg.in/DataDog/dd-trace-go.v1 v1.27.0 h1:WGVt9dwn9vNeWZVdDYzjGQbEW8CghAkJlrC8w80jFVY= +gopkg.in/airbrake/gobrake.v2 v2.0.9 h1:7z2uVWwn7oVeeugY1DtlPAy5H+KYgB1KeKTnqjNatLo= +gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= +gopkg.in/cheggaaa/pb.v1 v1.0.25 h1:Ev7yu1/f6+d+b3pi5vPdRPc6nNtP1umSfcWiEfRqv6I= +gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= +gopkg.in/gcfg.v1 v1.2.3 h1:m8OOJ4ccYHnx2f4gQwpno8nAX5OGOh7RLaaz0pj3Ogs= +gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2 h1:OAj3g0cR6Dx/R07QgQe8wkA9RNjB2u4i700xBkIT4e0= +gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= +gopkg.in/go-playground/mold.v2 v2.2.0 h1:Y4IYB4/HYQfuq43zaKh6vs9cVelLE9qbqe2fkyfCTWQ= +gopkg.in/go-playground/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ= +gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE= +gopkg.in/gorp.v1 v1.7.2 h1:j3DWlAyGVv8whO7AcIWznQ2Yj7yJkn34B8s63GViAAw= +gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec h1:RlWgLqCMMIYYEVcAR5MDsuHlVkaIPDAF+5Dehzg8L5A= +gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce h1:xcEWjVhvbDy+nHP67nPDDpbYrY+ILlfndk4bRioVHaU= +gopkg.in/resty.v1 v1.12.0 h1:CuXP0Pjfw9rOuY6EP+UvtNvt5DSqHpIxILZKT/quCZI= +gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg= +gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98= +gopkg.in/telebot.v3 v3.2.1 h1:3I4LohaAyJBiivGmkfB+CiVu7QFOWkuZ4+KHgO/G3rs= +gopkg.in/validator.v2 v2.0.0-20180514200540-135c24b11c19 h1:WB265cn5OpO+hK3pikC9hpP1zI/KTwmyMFKloW9eOVc= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +honnef.co/go/tools v0.1.3 h1:qTakTkI6ni6LFD5sBwwsdSO+AQqbSIxOauHTTQKZ/7o= +howett.net/plist v0.0.0-20181124034731-591f970eefbb h1:jhnBjNi9UFpfpl8YZhA9CrOqpnJdvzuiHsl/dnxl11M= +k8s.io/apiextensions-apiserver v0.26.2 h1:/yTG2B9jGY2Q70iGskMf41qTLhL9XeNN2KhI0uDgwko= +k8s.io/apiextensions-apiserver v0.26.2/go.mod h1:Y7UPgch8nph8mGCuVk0SK83LnS8Esf3n6fUBgew8SH8= +k8s.io/gengo v0.0.0-20230829151522-9cce18d56c01 h1:pWEwq4Asjm4vjW7vcsmijwBhOr1/shsbSYiWXmNGlks= +k8s.io/gengo v0.0.0-20230829151522-9cce18d56c01/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= +k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= +modernc.org/cc v1.0.0 h1:nPibNuDEx6tvYrUAtvDTTw98rx5juGsa5zuDnKwEEQQ= +modernc.org/golex v1.0.0 h1:wWpDlbK8ejRfSyi0frMyhilD3JBvtcx2AdGDnU+JtsE= +modernc.org/xc v1.0.0 h1:7ccXrupWZIS3twbUGrtKmHS2DXY6xegFua+6O3xgAFU= +nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g= +rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE= +rsc.io/pdf v0.1.1 h1:k1MczvYDUvJBe93bYd7wrZLLUEcLZAuF824/I4e5Xr4= +rsc.io/quote/v3 v3.1.0 h1:9JKUTTIUgS6kzR9mK1YuGKv6Nl+DijDNIc0ghT58FaY= +rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4= +sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0 h1:ucqkfpjg9WzSUubAO62csmucvxl4/JeW3F4I4909XkM= diff --git a/pkg/util/xorm/go.mod b/pkg/util/xorm/go.mod index 80a175b05a..78f85ed97f 100644 --- a/pkg/util/xorm/go.mod +++ b/pkg/util/xorm/go.mod @@ -1,13 +1,20 @@ -module xorm.io/xorm +module github.com/grafana/grafana/pkg/util/xorm -go 1.20 +go 1.21 require ( + github.com/mattn/go-sqlite3 v1.14.19 + github.com/stretchr/testify v1.8.4 xorm.io/builder v0.3.6 - xorm.io/core v0.7.2 + xorm.io/core v0.7.3 ) require ( - github.com/davecgh/go-spew v1.1.1 // indirect - gopkg.in/yaml.v2 v2.2.8 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/go-sql-driver/mysql v1.7.1 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/rogpeppe/go-internal v1.11.0 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/pkg/util/xorm/go.sum b/pkg/util/xorm/go.sum index d9708f043a..0295fff331 100644 --- a/pkg/util/xorm/go.sum +++ b/pkg/util/xorm/go.sum @@ -1,28 +1,28 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= github.com/go-xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:9wScpmSP5A3Bk8V3XHWUcJmYTh+ZnlHVyc+A4oZYS3Y= github.com/go-xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:56xuuqnHyryaerycW3BfssRdxQstACi0Epw/yC5E2xM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -google.golang.org/appengine v1.6.0 h1:Tfd7cKwKbFRsI8RMAD3oqqw7JPFRrvFlOsfbgVkjOOw= google.golang.org/appengine v1.6.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= xorm.io/builder v0.3.6 h1:ha28mQ2M+TFx96Hxo+iq6tQgnkC9IZkM6D8w9sKHHF8= xorm.io/builder v0.3.6/go.mod h1:LEFAPISnRzG+zxaxj2vPicRwz67BdhFreKg8yv8/TgU= -xorm.io/core v0.7.2 h1:mEO22A2Z7a3fPaZMk6gKL/jMD80iiyNwRrX5HOv3XLw= -xorm.io/core v0.7.2/go.mod h1:jJfd0UAEzZ4t87nbQYtVjmqpIODugN6PD2D9E+dJvdM= +xorm.io/core v0.7.3 h1:W8ws1PlrnkS1CZU1YWaYLMQcQilwAmQXU0BJDJon+H0= diff --git a/pkg/util/xorm/xorm_test.go b/pkg/util/xorm/xorm_test.go new file mode 100644 index 0000000000..988528edb6 --- /dev/null +++ b/pkg/util/xorm/xorm_test.go @@ -0,0 +1,17 @@ +package xorm + +import ( + "testing" + + _ "github.com/mattn/go-sqlite3" + "github.com/stretchr/testify/require" +) + +func TestNewEngine(t *testing.T) { + t.Run("successfully create a new engine", func(t *testing.T) { + eng, err := NewEngine("sqlite3", "./test.db") + require.NoError(t, err) + require.NotNil(t, eng) + require.Equal(t, "sqlite3", eng.DriverName()) + }) +} diff --git a/scripts/drone/steps/lib.star b/scripts/drone/steps/lib.star index df3f40f2ea..9f24661913 100644 --- a/scripts/drone/steps/lib.star +++ b/scripts/drone/steps/lib.star @@ -565,7 +565,7 @@ def test_backend_step(): # shared-mime-info and shared-mime-info-lang is used for exactly 1 test for the # mime.TypeByExtension function. "apk add --update build-base shared-mime-info shared-mime-info-lang", - "go test -tags requires_buildifer -short -covermode=atomic -timeout=5m ./pkg/...", + "go list -f '{{.Dir}}/...' -m | xargs go test -tags requires_buildifer -short -covermode=atomic -timeout=5m", ], } @@ -1036,7 +1036,7 @@ def mysql_integration_tests_steps(hostname, version): def redis_integration_tests_steps(): cmds = [ "go clean -testcache", - "go test -run IntegrationRedis -covermode=atomic -timeout=2m ./pkg/...", + "go list -f '{{.Dir}}/...' -m | xargs go test -run IntegrationRedis -covermode=atomic -timeout=2m", ] environment = { @@ -1061,7 +1061,7 @@ def remote_alertmanager_integration_tests_steps(): def memcached_integration_tests_steps(): cmds = [ "go clean -testcache", - "go test -run IntegrationMemcached -covermode=atomic -timeout=2m ./pkg/...", + "go list -f '{{.Dir}}/...' -m | xargs go test -run IntegrationMemcached -covermode=atomic -timeout=2m", ] environment = { From 1631e4130393a2ccea8159ac99e8ebf1d0a726a0 Mon Sep 17 00:00:00 2001 From: Joey <90795735+joey-grafana@users.noreply.github.com> Date: Fri, 23 Feb 2024 14:03:35 +0000 Subject: [PATCH 0128/1406] Tempo: Add template variable interpolation for filters (#83213) * Interpolate template variables in filters * Add tests --- .../datasource/tempo/datasource.test.ts | 29 ++++++++++++-- .../plugins/datasource/tempo/datasource.ts | 39 ++++++++++++++----- 2 files changed, 55 insertions(+), 13 deletions(-) diff --git a/public/app/plugins/datasource/tempo/datasource.test.ts b/public/app/plugins/datasource/tempo/datasource.test.ts index ac8dc49f37..0ed1d22114 100644 --- a/public/app/plugins/datasource/tempo/datasource.test.ts +++ b/public/app/plugins/datasource/tempo/datasource.test.ts @@ -93,7 +93,24 @@ describe('Tempo data source', () => { minDuration: '$interpolationVar', maxDuration: '$interpolationVar', serviceMapQuery, - filters: [], + filters: [ + { + id: 'service-name', + operator: '=', + scope: TraceqlSearchScope.Resource, + tag: 'service.name', + value: '$interpolationVarWithPipe', + valueType: 'string', + }, + { + id: 'tagId', + operator: '=', + scope: TraceqlSearchScope.Span, + tag: '$interpolationVar', + value: '$interpolationVar', + valueType: 'string', + }, + ], }; } let templateSrv: TemplateSrv; @@ -110,7 +127,7 @@ describe('Tempo data source', () => { templateSrv = initTemplateSrv([{ name: 'templateVariable1' }, { name: 'templateVariable2' }], expectedValues); }); - it('when traceId query for dashboard->explore', async () => { + it('when moving from dashboard to explore', async () => { const expectedValues = { interpolationVar: 'interpolationText', interpolationText: 'interpolationText', @@ -129,9 +146,12 @@ describe('Tempo data source', () => { expect(queries[0].minDuration).toBe(text); expect(queries[0].maxDuration).toBe(text); expect(queries[0].serviceMapQuery).toBe(text); + expect(queries[0].filters[0].value).toBe(textWithPipe); + expect(queries[0].filters[1].value).toBe(text); + expect(queries[0].filters[1].tag).toBe(text); }); - it('when traceId query for template variable', async () => { + it('when applying template variables', async () => { const scopedText = 'scopedInterpolationText'; const ds = new TempoDatasource(defaultSettings, templateSrv); const resp = ds.applyTemplateVariables(getQuery(), { @@ -144,6 +164,9 @@ describe('Tempo data source', () => { expect(resp.search).toBe(scopedText); expect(resp.minDuration).toBe(scopedText); expect(resp.maxDuration).toBe(scopedText); + expect(resp.filters[0].value).toBe(textWithPipe); + expect(resp.filters[1].value).toBe(scopedText); + expect(resp.filters[1].tag).toBe(scopedText); }); it('when serviceMapQuery is an array', async () => { diff --git a/public/app/plugins/datasource/tempo/datasource.ts b/public/app/plugins/datasource/tempo/datasource.ts index 398fd25f18..13d82d9abf 100644 --- a/public/app/plugins/datasource/tempo/datasource.ts +++ b/public/app/plugins/datasource/tempo/datasource.ts @@ -377,9 +377,12 @@ export class TempoDatasource extends DataSourceWithBackend this.hasGroupBy(t)); - if (groupBy) { - subQueries.push(this.handleMetricsSummary(groupBy, generateQueryFromFilters(groupBy.filters), options)); + const target = targets.traceqlSearch.find((t) => this.hasGroupBy(t)); + if (target) { + const appliedQuery = this.applyVariables(target, options.scopedVars); + subQueries.push( + this.handleMetricsSummary(appliedQuery, generateQueryFromFilters(appliedQuery.filters), options) + ); } } @@ -387,25 +390,23 @@ export class TempoDatasource extends DataSourceWithBackend !this.hasGroupBy(t)) : targets.traceqlSearch; if (traceqlSearchTargets.length > 0) { - const queryValueFromFilters = generateQueryFromFilters(traceqlSearchTargets[0].filters); - - // We want to support template variables also in Search for consistency with other data sources - const queryValue = this.templateSrv.replace(queryValueFromFilters, options.scopedVars); + const appliedQuery = this.applyVariables(traceqlSearchTargets[0], options.scopedVars); + const queryValueFromFilters = generateQueryFromFilters(appliedQuery.filters); reportInteraction('grafana_traces_traceql_search_queried', { datasourceType: 'tempo', app: options.app ?? '', grafana_version: config.buildInfo.version, - query: queryValue ?? '', + query: queryValueFromFilters ?? '', streaming: config.featureToggles.traceQLStreaming, }); if (config.featureToggles.traceQLStreaming && this.isFeatureAvailable(FeatureName.streaming)) { - subQueries.push(this.handleStreamingSearch(options, traceqlSearchTargets, queryValue)); + subQueries.push(this.handleStreamingSearch(options, traceqlSearchTargets, queryValueFromFilters)); } else { subQueries.push( this._request('/api/search', { - q: queryValue, + q: queryValueFromFilters, limit: options.targets[0].limit ?? DEFAULT_LIMIT, spss: options.targets[0].spss ?? DEFAULT_SPSS, start: options.range.from.unix(), @@ -522,6 +523,24 @@ export class TempoDatasource extends DataSourceWithBackend { + const updatedFilter = { + ...filter, + tag: this.templateSrv.replace(filter.tag ?? '', scopedVars), + }; + + if (filter.value) { + updatedFilter.value = + typeof filter.value === 'string' + ? this.templateSrv.replace(filter.value ?? '', scopedVars, VariableFormatID.Pipe) + : filter.value.map((v) => this.templateSrv.replace(v ?? '', scopedVars, VariableFormatID.Pipe)); + } + + return updatedFilter; + }); + } + return { ...expandedQuery, query: this.templateSrv.replace(query.query ?? '', scopedVars, VariableFormatID.Pipe), From 604e02be1520612578a46eefa96e7853d57be8a4 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Fri, 23 Feb 2024 14:25:44 +0000 Subject: [PATCH 0129/1406] Grid: Add `alignItems` prop (#83314) add alignItems props to Grid --- .../src/components/Layout/Grid/Grid.story.tsx | 14 ++++++++++++-- .../grafana-ui/src/components/Layout/Grid/Grid.tsx | 12 +++++++++--- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/packages/grafana-ui/src/components/Layout/Grid/Grid.story.tsx b/packages/grafana-ui/src/components/Layout/Grid/Grid.story.tsx index 2c73d9d0f4..d4dadc083a 100644 --- a/packages/grafana-ui/src/components/Layout/Grid/Grid.story.tsx +++ b/packages/grafana-ui/src/components/Layout/Grid/Grid.story.tsx @@ -6,6 +6,10 @@ import { useTheme2 } from '../../../themes'; import { Grid } from './Grid'; import mdx from './Grid.mdx'; +const dimensions = Array.from({ length: 9 }).map(() => ({ + minHeight: `${Math.random() * 100 + 100}px`, +})); + const meta: Meta = { title: 'General/Layout/Grid', component: Grid, @@ -22,15 +26,21 @@ const meta: Meta = { export const ColumnsNumber: StoryFn = (args) => { const theme = useTheme2(); return ( - + {Array.from({ length: 9 }).map((_, i) => ( -
+
N# {i}
))} ); }; +ColumnsNumber.argTypes = { + alignItems: { + control: 'select', + options: ['stretch', 'flex-start', 'flex-end', 'center', 'baseline', 'start', 'end', 'self-start', 'self-end'], + }, +}; ColumnsNumber.args = { columns: 3, }; diff --git a/packages/grafana-ui/src/components/Layout/Grid/Grid.tsx b/packages/grafana-ui/src/components/Layout/Grid/Grid.tsx index 0a452686b8..4225bb6074 100644 --- a/packages/grafana-ui/src/components/Layout/Grid/Grid.tsx +++ b/packages/grafana-ui/src/components/Layout/Grid/Grid.tsx @@ -4,12 +4,14 @@ import React, { forwardRef, HTMLAttributes } from 'react'; import { GrafanaTheme2, ThemeSpacingTokens } from '@grafana/data'; import { useStyles2 } from '../../../themes'; +import { AlignItems } from '../types'; import { getResponsiveStyle, ResponsiveProp } from '../utils/responsiveness'; interface GridPropsBase extends Omit, 'className' | 'style'> { children: NonNullable; /** Specifies the gutters between columns and rows. It is overwritten when a column or row gap has a value. */ gap?: ResponsiveProp; + alignItems?: ResponsiveProp; } interface PropsWithColumns extends GridPropsBase { @@ -30,8 +32,8 @@ interface PropsWithMinColumnWidth extends GridPropsBase { type GridProps = PropsWithColumns | PropsWithMinColumnWidth; export const Grid = forwardRef((props, ref) => { - const { children, gap, columns, minColumnWidth, ...rest } = props; - const styles = useStyles2(getGridStyles, gap, columns, minColumnWidth); + const { alignItems, children, gap, columns, minColumnWidth, ...rest } = props; + const styles = useStyles2(getGridStyles, gap, columns, minColumnWidth, alignItems); return (
@@ -46,7 +48,8 @@ const getGridStyles = ( theme: GrafanaTheme2, gap: GridProps['gap'], columns: GridProps['columns'], - minColumnWidth: GridProps['minColumnWidth'] + minColumnWidth: GridProps['minColumnWidth'], + alignItems: GridProps['alignItems'] ) => { return { grid: css([ @@ -62,6 +65,9 @@ const getGridStyles = ( getResponsiveStyle(theme, columns, (val) => ({ gridTemplateColumns: `repeat(${val}, 1fr)`, })), + getResponsiveStyle(theme, alignItems, (val) => ({ + alignItems: val, + })), ]), }; }; From 83c01f9711ffc8be87facaddae4e7c553b44b65d Mon Sep 17 00:00:00 2001 From: Darren Janeczek <38694490+darrenjaneczek@users.noreply.github.com> Date: Fri, 23 Feb 2024 10:00:10 -0500 Subject: [PATCH 0130/1406] datatrails: detect if current trail state is bookmarked (#83283) * fix: detect if current trail state is bookmarked --- public/app/features/trails/DataTrail.tsx | 2 + public/app/features/trails/MetricScene.tsx | 13 ++----- .../trails/TrailStore/TrailStore.test.ts | 31 ++++++++++++++- .../features/trails/TrailStore/TrailStore.ts | 33 ++++++++++++++++ .../trails/TrailStore/useBookmarkState.ts | 39 +++++++++++++++++++ 5 files changed, 108 insertions(+), 10 deletions(-) create mode 100644 public/app/features/trails/TrailStore/useBookmarkState.ts diff --git a/public/app/features/trails/DataTrail.tsx b/public/app/features/trails/DataTrail.tsx index 69424e94b5..0392dc8712 100644 --- a/public/app/features/trails/DataTrail.tsx +++ b/public/app/features/trails/DataTrail.tsx @@ -86,6 +86,8 @@ export class DataTrail extends SceneObjectBase { const newStepWasAppended = newNumberOfSteps > oldNumberOfSteps; if (newStepWasAppended) { + // In order for the `useBookmarkState` to re-evaluate after a new step was made: + this.forceRender(); // Do nothing because the state is already up to date -- it created a new step! return; } diff --git a/public/app/features/trails/MetricScene.tsx b/public/app/features/trails/MetricScene.tsx index 7362d6c883..25bf96b2fc 100644 --- a/public/app/features/trails/MetricScene.tsx +++ b/public/app/features/trails/MetricScene.tsx @@ -1,5 +1,5 @@ import { css } from '@emotion/css'; -import React, { useState } from 'react'; +import React from 'react'; import { DashboardCursorSync, GrafanaTheme2 } from '@grafana/data'; import { @@ -26,7 +26,7 @@ import { getAutoQueriesForMetric } from './AutomaticMetricQueries/AutoQueryEngin import { AutoVizPanel } from './AutomaticMetricQueries/AutoVizPanel'; import { AutoQueryDef, AutoQueryInfo } from './AutomaticMetricQueries/types'; import { ShareTrailButton } from './ShareTrailButton'; -import { getTrailStore } from './TrailStore/TrailStore'; +import { useBookmarkState } from './TrailStore/useBookmarkState'; import { ActionViewDefinition, ActionViewType, @@ -151,14 +151,9 @@ export class MetricActionBar extends SceneObjectBase { const metricScene = sceneGraph.getAncestor(model, MetricScene); const styles = useStyles2(getStyles); const trail = getTrailFor(model); - const [isBookmarked, setBookmarked] = useState(false); + const [isBookmarked, toggleBookmark] = useBookmarkState(trail); const { actionView } = metricScene.useState(); - const onBookmarkTrail = () => { - getTrailStore().addBookmark(trail); - setBookmarked(!isBookmarked); - }; - return (
@@ -180,7 +175,7 @@ export class MetricActionBar extends SceneObjectBase { ) } tooltip={'Bookmark'} - onClick={onBookmarkTrail} + onClick={toggleBookmark} /> {trail.state.embedded && ( diff --git a/public/app/features/trails/TrailStore/TrailStore.test.ts b/public/app/features/trails/TrailStore/TrailStore.test.ts index 38ca5149c0..2aa5d1671e 100644 --- a/public/app/features/trails/TrailStore/TrailStore.test.ts +++ b/public/app/features/trails/TrailStore/TrailStore.test.ts @@ -167,7 +167,7 @@ describe('TrailStore', () => { }); }); describe('Initialize store with one bookmark trail', () => { - beforeAll(() => { + beforeEach(() => { localStorage.clear(); localStorage.setItem( BOOKMARKED_TRAILS_KEY, @@ -225,6 +225,35 @@ describe('TrailStore', () => { expect(store.recent.length).toBe(1); }); + it('should be able to obtain index of bookmark', () => { + const trail = store.bookmarks[0].resolve(); + const index = store.getBookmarkIndex(trail); + expect(index).toBe(0); + }); + + it('index should be undefined for removed bookmarks', () => { + const trail = store.bookmarks[0].resolve(); + store.removeBookmark(0); + const index = store.getBookmarkIndex(trail); + expect(index).toBe(undefined); + }); + + it('index should be undefined for a trail that has changed since it was bookmarked', () => { + const trail = store.bookmarks[0].resolve(); + trail.setState({ metric: 'something_completely_different' }); + const index = store.getBookmarkIndex(trail); + expect(index).toBe(undefined); + }); + + it('should be able to obtain index of a bookmark for a trail that changed back to bookmarked state', () => { + const trail = store.bookmarks[0].resolve(); + const bookmarkedMetric = trail.state.metric; + trail.setState({ metric: 'something_completely_different' }); + trail.setState({ metric: bookmarkedMetric }); + const index = store.getBookmarkIndex(trail); + expect(index).toBe(0); + }); + it('should remove a bookmark', () => { expect(store.bookmarks.length).toBe(1); store.removeBookmark(0); diff --git a/public/app/features/trails/TrailStore/TrailStore.ts b/public/app/features/trails/TrailStore/TrailStore.ts index 9c81ec5016..e11a786c35 100644 --- a/public/app/features/trails/TrailStore/TrailStore.ts +++ b/public/app/features/trails/TrailStore/TrailStore.ts @@ -100,6 +100,7 @@ export class TrailStore { load() { this._recent = this._loadFromStorage(RECENT_TRAILS_KEY); this._bookmarks = this._loadFromStorage(BOOKMARKED_TRAILS_KEY); + this._refreshBookmarkIndexMap(); } setRecentTrail(trail: DataTrail) { @@ -125,15 +126,47 @@ export class TrailStore { addBookmark(trail: DataTrail) { this._bookmarks.unshift(trail.getRef()); + this._refreshBookmarkIndexMap(); this._save(); } removeBookmark(index: number) { if (index < this._bookmarks.length) { this._bookmarks.splice(index, 1); + this._refreshBookmarkIndexMap(); this._save(); } } + + getBookmarkIndex(trail: DataTrail) { + const bookmarkKey = getBookmarkKey(trail); + const bookmarkIndex = this._bookmarkIndexMap.get(bookmarkKey); + return bookmarkIndex; + } + + private _bookmarkIndexMap = new Map(); + + private _refreshBookmarkIndexMap() { + this._bookmarkIndexMap.clear(); + this._bookmarks.forEach((bookmarked, index) => { + const trail = bookmarked.resolve(); + const key = getBookmarkKey(trail); + // If there are duplicate bookmarks, the latest index will be kept + this._bookmarkIndexMap.set(key, index); + }); + } +} + +function getBookmarkKey(trail: DataTrail) { + const urlState = getUrlSyncManager().getUrlState(trail); + // Not part of state + delete urlState.actionView; + // Populate defaults + if (urlState['var-groupby'] === '') { + urlState['var-groupby'] = '$__all'; + } + const key = JSON.stringify(urlState); + return key; } let store: TrailStore | undefined; diff --git a/public/app/features/trails/TrailStore/useBookmarkState.ts b/public/app/features/trails/TrailStore/useBookmarkState.ts new file mode 100644 index 0000000000..18336b0148 --- /dev/null +++ b/public/app/features/trails/TrailStore/useBookmarkState.ts @@ -0,0 +1,39 @@ +import { useState } from 'react'; + +import { DataTrail } from '../DataTrail'; + +import { getTrailStore } from './TrailStore'; + +export function useBookmarkState(trail: DataTrail) { + // Note that trail object may stay the same, but the state used by `getBookmarkIndex` result may + // differ for each re-render of this hook + const getBookmarkIndex = () => getTrailStore().getBookmarkIndex(trail); + + const indexOnRender = getBookmarkIndex(); + + const [bookmarkIndex, setBookmarkIndex] = useState(indexOnRender); + + // Check if index changed and force a re-render + if (indexOnRender !== bookmarkIndex) { + setBookmarkIndex(indexOnRender); + } + + const isBookmarked = bookmarkIndex != null; + + const toggleBookmark = () => { + if (isBookmarked) { + let indexToRemove = getBookmarkIndex(); + while (indexToRemove != null) { + // This loop will remove all indices that have an equivalent bookmark key + getTrailStore().removeBookmark(indexToRemove); + indexToRemove = getBookmarkIndex(); + } + } else { + getTrailStore().addBookmark(trail); + } + setBookmarkIndex(getBookmarkIndex()); + }; + + const result: [typeof isBookmarked, typeof toggleBookmark] = [isBookmarked, toggleBookmark]; + return result; +} From 49e18a3e7a698fcc7b5ec2b2034824bde05f3a42 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Fri, 23 Feb 2024 15:00:24 +0000 Subject: [PATCH 0131/1406] Chore: replace `react-popper` with `floating-ui` in `Popover` (#82922) * replace react-popper with floating-ui in Popover * update HoverCard * fix unit tests * mock useTransitionStyles to ensure consistent unit test results --- .betterer.results | 7 - .../components/ColorPicker/ColorPicker.tsx | 17 -- .../src/components/Table/FilterPopup.tsx | 1 - .../src/components/Table/Table.test.tsx | 8 + .../src/components/Tooltip/Popover.tsx | 170 +++++++++--------- .../alerting/unified/components/HoverCard.tsx | 76 +++----- 6 files changed, 117 insertions(+), 162 deletions(-) diff --git a/.betterer.results b/.betterer.results index f04979e6be..d305a5130a 100644 --- a/.betterer.results +++ b/.betterer.results @@ -1662,13 +1662,6 @@ exports[`better eslint`] = { "public/app/features/alerting/unified/components/GrafanaAlertmanagerDeliveryWarning.tsx:5381": [ [0, 0, 0, "Styles should be written using objects.", "0"] ], - "public/app/features/alerting/unified/components/HoverCard.tsx:5381": [ - [0, 0, 0, "Styles should be written using objects.", "0"], - [0, 0, 0, "Styles should be written using objects.", "1"], - [0, 0, 0, "Styles should be written using objects.", "2"], - [0, 0, 0, "Styles should be written using objects.", "3"], - [0, 0, 0, "Styles should be written using objects.", "4"] - ], "public/app/features/alerting/unified/components/Label.tsx:5381": [ [0, 0, 0, "Styles should be written using objects.", "0"], [0, 0, 0, "Styles should be written using objects.", "1"], diff --git a/packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx b/packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx index 65912a39d6..57fc7cfe6c 100644 --- a/packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx @@ -92,23 +92,6 @@ const getStyles = stylesFactory((theme: GrafanaTheme2) => { color: theme.colors.text.primary, maxWidth: '400px', fontSize: theme.typography.size.sm, - // !important because these styles are also provided to popper via .popper classes from Tooltip component - // hope to get rid of those soon - padding: '15px !important', - '& [data-placement^="top"]': { - paddingLeft: '0 !important', - paddingRight: '0 !important', - }, - '& [data-placement^="bottom"]': { - paddingLeft: '0 !important', - paddingRight: '0 !important', - }, - '& [data-placement^="left"]': { - paddingTop: '0 !important', - }, - '& [data-placement^="right"]': { - paddingTop: '0 !important', - }, }), }; }); diff --git a/packages/grafana-ui/src/components/Table/FilterPopup.tsx b/packages/grafana-ui/src/components/Table/FilterPopup.tsx index efa1e848e0..422dfe7a4f 100644 --- a/packages/grafana-ui/src/components/Table/FilterPopup.tsx +++ b/packages/grafana-ui/src/components/Table/FilterPopup.tsx @@ -108,7 +108,6 @@ const getStyles = (theme: GrafanaTheme2) => ({ backgroundColor: theme.colors.background.primary, border: `1px solid ${theme.colors.border.weak}`, padding: theme.spacing(2), - margin: theme.spacing(1, 0), boxShadow: theme.shadows.z3, borderRadius: theme.shape.radius.default, }), diff --git a/packages/grafana-ui/src/components/Table/Table.test.tsx b/packages/grafana-ui/src/components/Table/Table.test.tsx index 5d855d25a4..e6ed28d2a8 100644 --- a/packages/grafana-ui/src/components/Table/Table.test.tsx +++ b/packages/grafana-ui/src/components/Table/Table.test.tsx @@ -7,6 +7,14 @@ import { applyFieldOverrides, createTheme, DataFrame, FieldType, toDataFrame } f import { Table } from './Table'; import { Props } from './types'; +// mock transition styles to ensure consistent behaviour in unit tests +jest.mock('@floating-ui/react', () => ({ + ...jest.requireActual('@floating-ui/react'), + useTransitionStyles: () => ({ + styles: {}, + }), +})); + function getDefaultDataFrame(): DataFrame { const dataFrame = toDataFrame({ name: 'A', diff --git a/packages/grafana-ui/src/components/Tooltip/Popover.tsx b/packages/grafana-ui/src/components/Tooltip/Popover.tsx index cb760b3cf5..81527f3cde 100644 --- a/packages/grafana-ui/src/components/Tooltip/Popover.tsx +++ b/packages/grafana-ui/src/components/Tooltip/Popover.tsx @@ -1,96 +1,102 @@ -import { Placement, VirtualElement } from '@popperjs/core'; -import React, { PureComponent } from 'react'; -import { Manager, Popper as ReactPopper, PopperArrowProps } from 'react-popper'; -import Transition from 'react-transition-group/Transition'; +import { + FloatingArrow, + arrow, + autoUpdate, + flip, + offset, + shift, + useFloating, + useTransitionStyles, +} from '@floating-ui/react'; +import React, { useLayoutEffect, useRef } from 'react'; +import { useTheme2 } from '../../themes'; +import { getPlacement } from '../../utils/tooltipUtils'; import { Portal } from '../Portal/Portal'; -import { PopoverContent } from './types'; - -const defaultTransitionStyles = { - transitionProperty: 'opacity', - transitionDuration: '200ms', - transitionTimingFunction: 'linear', - opacity: 0, -}; - -const transitionStyles: { [key: string]: object } = { - exited: { opacity: 0 }, - entering: { opacity: 0 }, - entered: { opacity: 1, transitionDelay: '0s' }, - exiting: { opacity: 0, transitionDelay: '500ms' }, -}; - -export type RenderPopperArrowFn = (props: { arrowProps: PopperArrowProps; placement: string }) => JSX.Element; +import { PopoverContent, TooltipPlacement } from './types'; interface Props extends Omit, 'content'> { show: boolean; - placement?: Placement; + placement?: TooltipPlacement; content: PopoverContent; - referenceElement: HTMLElement | VirtualElement; + referenceElement: HTMLElement; wrapperClassName?: string; - renderArrow?: RenderPopperArrowFn; + renderArrow?: boolean; } -class Popover extends PureComponent { - render() { - const { content, show, placement, className, wrapperClassName, renderArrow, referenceElement, ...rest } = - this.props; +export function Popover({ + content, + show, + placement, + className, + wrapperClassName, + referenceElement, + renderArrow, + ...rest +}: Props) { + const theme = useTheme2(); + const arrowRef = useRef(null); - return ( - - - {(transitionState) => { - return ( - - - {({ ref, style, placement, arrowProps, update }) => { - return ( -
-
- {typeof content === 'string' && content} - {React.isValidElement(content) && React.cloneElement(content)} - {typeof content === 'function' && - content({ - updatePopperPosition: update, - })} - {renderArrow && - renderArrow({ - arrowProps, - placement, - })} -
-
- ); - }} -
-
- ); - }} -
-
+ // the order of middleware is important! + // `arrow` should almost always be at the end + // see https://floating-ui.com/docs/arrow#order + const middleware = [ + offset(8), + flip({ + fallbackAxisSideDirection: 'end', + // see https://floating-ui.com/docs/flip#combining-with-shift + crossAxis: false, + boundary: document.body, + }), + shift(), + ]; + + if (renderArrow) { + middleware.push( + arrow({ + element: arrowRef, + }) ); } -} -export { Popover }; + const { context, refs, floatingStyles } = useFloating({ + open: show, + placement: getPlacement(placement), + middleware, + whileElementsMounted: autoUpdate, + strategy: 'fixed', + }); + + useLayoutEffect(() => { + refs.setReference(referenceElement); + }, [referenceElement, refs]); + + const { styles: placementStyles } = useTransitionStyles(context, { + initial: () => ({ + opacity: 0, + }), + duration: theme.transitions.duration.enteringScreen, + }); + + return show ? ( + +
+
+ {renderArrow && } + {typeof content === 'string' && content} + {React.isValidElement(content) && React.cloneElement(content)} + {typeof content === 'function' && content({})} +
+
+
+ ) : undefined; +} diff --git a/public/app/features/alerting/unified/components/HoverCard.tsx b/public/app/features/alerting/unified/components/HoverCard.tsx index 5d592b1113..62edfd1c79 100644 --- a/public/app/features/alerting/unified/components/HoverCard.tsx +++ b/public/app/features/alerting/unified/components/HoverCard.tsx @@ -53,17 +53,13 @@ export const HoverCard = ({
- : () => <> - } + renderArrow={arrow} /> )} @@ -82,55 +78,25 @@ export const HoverCard = ({ }; const getStyles = (theme: GrafanaTheme2) => ({ - popover: (offset: number) => css` - border-radius: ${theme.shape.radius.default}; - box-shadow: ${theme.shadows.z3}; - background: ${theme.colors.background.primary}; - border: 1px solid ${theme.colors.border.medium}; - - margin-bottom: ${theme.spacing(offset)}; - `, + popover: css({ + borderRadius: theme.shape.radius.default, + boxShadow: theme.shadows.z3, + background: theme.colors.background.primary, + border: `1px solid ${theme.colors.border.medium}`, + }), card: { - body: css` - padding: ${theme.spacing(1)}; - `, - header: css` - padding: ${theme.spacing(1)}; - background: ${theme.colors.background.secondary}; - border-bottom: solid 1px ${theme.colors.border.medium}; - `, - footer: css` - padding: ${theme.spacing(0.5)} ${theme.spacing(1)}; - background: ${theme.colors.background.secondary}; - border-top: solid 1px ${theme.colors.border.medium}; - `, - }, - // TODO currently only works with bottom placement - arrow: (placement: string) => { - const ARROW_SIZE = '9px'; - - return css` - width: 0; - height: 0; - - border-left: ${ARROW_SIZE} solid transparent; - border-right: ${ARROW_SIZE} solid transparent; - /* using hex colors here because the border colors use alpha transparency */ - border-top: ${ARROW_SIZE} solid ${theme.isLight ? '#d2d3d4' : '#2d3037'}; - - &:after { - content: ''; - position: absolute; - - border: ${ARROW_SIZE} solid ${theme.colors.background.primary}; - border-bottom: 0; - border-left-color: transparent; - border-right-color: transparent; - - margin-top: 1px; - bottom: 1px; - left: -${ARROW_SIZE}; - } - `; + body: css({ + padding: theme.spacing(1), + }), + header: css({ + padding: theme.spacing(1), + background: theme.colors.background.secondary, + borderBottom: `solid 1px ${theme.colors.border.medium}`, + }), + footer: css({ + padding: theme.spacing(0.5, 1), + background: theme.colors.background.secondary, + borderTop: `solid 1px ${theme.colors.border.medium}`, + }), }, }); From ea8b3267e55f8bba95769bc7adf9b6d427c7ab86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 23 Feb 2024 16:22:34 +0100 Subject: [PATCH 0132/1406] DataTrails: Sticky controls (#83286) * DataTrails: Sticky controls * Update --- public/app/features/trails/DataTrail.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/public/app/features/trails/DataTrail.tsx b/public/app/features/trails/DataTrail.tsx index 0392dc8712..b229a525f8 100644 --- a/public/app/features/trails/DataTrail.tsx +++ b/public/app/features/trails/DataTrail.tsx @@ -235,13 +235,17 @@ function getStyles(theme: GrafanaTheme2) { flexGrow: 1, display: 'flex', flexDirection: 'column', - gap: theme.spacing(1), }), controls: css({ display: 'flex', gap: theme.spacing(1), + padding: theme.spacing(1, 0), alignItems: 'flex-end', flexWrap: 'wrap', + position: 'sticky', + background: theme.isDark ? theme.colors.background.canvas : theme.colors.background.primary, + zIndex: theme.zIndex.activePanel + 1, + top: 0, }), }; } From bbe9c8661a75d514f4a76fe43dc2f2f34413b1d2 Mon Sep 17 00:00:00 2001 From: Kristina Date: Fri, 23 Feb 2024 09:44:21 -0600 Subject: [PATCH 0133/1406] Explore: Use rich history local storage for autocomplete (#81386) * move autocomplete logic * Tests * Write to correct history * Remove historyUpdatedAction and related code * add helpful comments * Add option to mute all errors/warnings for autocomplete * Add back in legacy local storage query history for transition period * Move params to an object for easier use and defaults * Do not make time filter required * fix tests * change deprecation version and add issue number --- .../core/history/RichHistoryLocalStorage.ts | 14 ++- .../history/richHistoryStorageProvider.ts | 6 + public/app/core/utils/explore.test.ts | 23 ---- public/app/core/utils/explore.ts | 32 ----- public/app/core/utils/richHistory.test.ts | 32 +++-- public/app/core/utils/richHistory.ts | 38 ++++-- public/app/core/utils/richHistoryTypes.ts | 4 +- .../RichHistory/RichHistoryQueriesTab.tsx | 10 +- .../app/features/explore/state/explorePane.ts | 2 - public/app/features/explore/state/history.ts | 59 ++++----- .../app/features/explore/state/query.test.ts | 25 ++++ public/app/features/explore/state/query.ts | 24 ++-- .../app/features/explore/state/utils.test.ts | 115 +++++++++++++++++- public/app/features/explore/state/utils.ts | 66 ++++++++-- 14 files changed, 295 insertions(+), 155 deletions(-) diff --git a/public/app/core/history/RichHistoryLocalStorage.ts b/public/app/core/history/RichHistoryLocalStorage.ts index 737246ca30..ab84d420d4 100644 --- a/public/app/core/history/RichHistoryLocalStorage.ts +++ b/public/app/core/history/RichHistoryLocalStorage.ts @@ -37,10 +37,16 @@ export default class RichHistoryLocalStorage implements RichHistoryStorage { const allQueries = getRichHistoryDTOs().map(fromDTO); const queries = filters.starred ? allQueries.filter((q) => q.starred === true) : allQueries; - const richHistory = filterAndSortQueries(queries, filters.sortOrder, filters.datasourceFilters, filters.search, [ - filters.from, - filters.to, - ]); + const timeFilter: [number, number] | undefined = + filters.from && filters.to ? [filters.from, filters.to] : undefined; + + const richHistory = filterAndSortQueries( + queries, + filters.sortOrder, + filters.datasourceFilters, + filters.search, + timeFilter + ); return { richHistory, total: richHistory.length }; } diff --git a/public/app/core/history/richHistoryStorageProvider.ts b/public/app/core/history/richHistoryStorageProvider.ts index 44337ffbb0..84defd0f00 100644 --- a/public/app/core/history/richHistoryStorageProvider.ts +++ b/public/app/core/history/richHistoryStorageProvider.ts @@ -10,10 +10,16 @@ import RichHistoryStorage from './RichHistoryStorage'; const richHistoryLocalStorage = new RichHistoryLocalStorage(); const richHistoryRemoteStorage = new RichHistoryRemoteStorage(); +// for query history operations export const getRichHistoryStorage = (): RichHistoryStorage => { return config.queryHistoryEnabled ? richHistoryRemoteStorage : richHistoryLocalStorage; }; +// for autocomplete read and write operations +export const getLocalRichHistoryStorage = (): RichHistoryStorage => { + return richHistoryLocalStorage; +}; + interface RichHistorySupportedFeatures { availableFilters: SortOrder[]; lastUsedDataSourcesAvailable: boolean; diff --git a/public/app/core/utils/explore.test.ts b/public/app/core/utils/explore.test.ts index 07a840f7a3..658f3eefd0 100644 --- a/public/app/core/utils/explore.test.ts +++ b/public/app/core/utils/explore.test.ts @@ -2,7 +2,6 @@ import { DataSourceApi, dateTime, ExploreUrlState, LogsSortOrder } from '@grafan import { serializeStateToUrlParam } from '@grafana/data/src/utils/url'; import { DataQuery } from '@grafana/schema'; import { RefreshPicker } from '@grafana/ui'; -import store from 'app/core/store'; import { DEFAULT_RANGE } from 'app/features/explore/state/utils'; import { DatasourceSrvMock, MockDataSourceApi } from '../../../test/mocks/datasource_srv'; @@ -11,7 +10,6 @@ import { buildQueryTransaction, hasNonEmptyQuery, refreshIntervalToSortOrder, - updateHistory, getExploreUrl, GetExploreUrlArguments, getTimeRange, @@ -151,27 +149,6 @@ describe('getExploreUrl', () => { }); }); -describe('updateHistory()', () => { - const datasourceId = 'myDatasource'; - const key = `grafana.explore.history.${datasourceId}`; - - beforeEach(() => { - store.delete(key); - expect(store.exists(key)).toBeFalsy(); - }); - - test('should save history item to localStorage', () => { - const expected = [ - { - query: { refId: '1', expr: 'metric' }, - }, - ]; - expect(updateHistory([], datasourceId, [{ refId: '1', expr: 'metric' }])).toMatchObject(expected); - expect(store.exists(key)).toBeTruthy(); - expect(store.getObject(key)).toMatchObject(expected); - }); -}); - describe('hasNonEmptyQuery', () => { test('should return true if one query is non-empty', () => { expect(hasNonEmptyQuery([{ refId: '1', key: '2', context: 'explore', expr: 'foo' }])).toBeTruthy(); diff --git a/public/app/core/utils/explore.ts b/public/app/core/utils/explore.ts index 7852a73a26..501a03d57d 100644 --- a/public/app/core/utils/explore.ts +++ b/public/app/core/utils/explore.ts @@ -10,7 +10,6 @@ import { DataSourceApi, DataSourceRef, DefaultTimeZone, - HistoryItem, IntervalValues, LogsDedupStrategy, LogsSortOrder, @@ -37,8 +36,6 @@ export const DEFAULT_UI_STATE = { export const ID_ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz'; const nanoid = customAlphabet(ID_ALPHABET, 3); -const MAX_HISTORY_ITEMS = 100; - const LAST_USED_DATASOURCE_KEY = 'grafana.explore.datasource'; const lastUsedDatasourceKeyForOrgId = (orgId: number) => `${LAST_USED_DATASOURCE_KEY}.${orgId}`; export const getLastUsedDatasourceUID = (orgId: number) => @@ -276,35 +273,6 @@ export function hasNonEmptyQuery(queries: TQuery[]): b ); } -/** - * Update the query history. Side-effect: store history in local storage - */ -export function updateHistory( - history: Array>, - datasourceId: string, - queries: T[] -): Array> { - const ts = Date.now(); - let updatedHistory = history; - queries.forEach((query) => { - updatedHistory = [{ query, ts }, ...updatedHistory]; - }); - - if (updatedHistory.length > MAX_HISTORY_ITEMS) { - updatedHistory = updatedHistory.slice(0, MAX_HISTORY_ITEMS); - } - - // Combine all queries of a datasource type into one history - const historyKey = `grafana.explore.history.${datasourceId}`; - try { - store.setObject(historyKey, updatedHistory); - return updatedHistory; - } catch (error) { - console.error(error); - return history; - } -} - export const getQueryKeys = (queries: DataQuery[]): string[] => { const queryKeys = queries.reduce((newQueryKeys, query, index) => { const primaryKey = query.datasource?.uid || query.key; diff --git a/public/app/core/utils/richHistory.test.ts b/public/app/core/utils/richHistory.test.ts index 4e87f38e74..0c6a95b981 100644 --- a/public/app/core/utils/richHistory.test.ts +++ b/public/app/core/utils/richHistory.test.ts @@ -109,15 +109,13 @@ describe('richHistory', () => { it('should append query to query history', async () => { Date.now = jest.fn(() => 2); - const { limitExceeded, richHistoryStorageFull } = await addToRichHistory( - mock.testDatasourceUid, - mock.testDatasourceName, - mock.testQueries, - mock.testStarred, - mock.testComment, - true, - true - ); + const { limitExceeded, richHistoryStorageFull } = await addToRichHistory({ + localOverride: false, + datasource: { uid: mock.testDatasourceUid, name: mock.testDatasourceName }, + queries: mock.testQueries, + starred: mock.testStarred, + comment: mock.testComment, + }); expect(limitExceeded).toBeFalsy(); expect(richHistoryStorageFull).toBeFalsy(); expect(richHistoryStorageMock.addToRichHistory).toBeCalledWith({ @@ -142,15 +140,13 @@ describe('richHistory', () => { }); }); - const { richHistoryStorageFull, limitExceeded } = await addToRichHistory( - mock.testDatasourceUid, - mock.testDatasourceName, - mock.testQueries, - mock.testStarred, - mock.testComment, - true, - true - ); + const { richHistoryStorageFull, limitExceeded } = await addToRichHistory({ + localOverride: false, + datasource: { uid: mock.testDatasourceUid, name: mock.testDatasourceName }, + queries: mock.testQueries, + starred: mock.testStarred, + comment: mock.testComment, + }); expect(richHistoryStorageFull).toBeFalsy(); expect(limitExceeded).toBeTruthy(); }); diff --git a/public/app/core/utils/richHistory.ts b/public/app/core/utils/richHistory.ts index 459e62b56d..ad95f62569 100644 --- a/public/app/core/utils/richHistory.ts +++ b/public/app/core/utils/richHistory.ts @@ -15,7 +15,7 @@ import { RichHistoryStorageWarning, RichHistoryStorageWarningDetails, } from '../history/RichHistoryStorage'; -import { getRichHistoryStorage } from '../history/richHistoryStorageProvider'; +import { getLocalRichHistoryStorage, getRichHistoryStorage } from '../history/richHistoryStorageProvider'; import { RichHistorySearchFilters, RichHistorySettings, SortOrder } from './richHistoryTypes'; @@ -26,15 +26,27 @@ export { RichHistorySearchFilters, RichHistorySettings, SortOrder }; * Side-effect: store history in local storage */ +type addToRichHistoryParams = { + localOverride: boolean; + datasource: { uid: string; name?: string }; + queries: DataQuery[]; + starred: boolean; + comment?: string; + showNotif?: { + quotaExceededError?: boolean; + limitExceededWarning?: boolean; + otherErrors?: boolean; + }; +}; + export async function addToRichHistory( - datasourceUid: string, - datasourceName: string | null, - queries: DataQuery[], - starred: boolean, - comment: string | null, - showQuotaExceededError: boolean, - showLimitExceededWarning: boolean + params: addToRichHistoryParams ): Promise<{ richHistoryStorageFull?: boolean; limitExceeded?: boolean }> { + const { queries, localOverride, datasource, starred, comment, showNotif } = params; + // default showing of errors to true + const showQuotaExceededError = showNotif?.quotaExceededError ?? true; + const showLimitExceededWarning = showNotif?.limitExceededWarning ?? true; + const showOtherErrors = showNotif?.otherErrors ?? true; /* Save only queries, that are not falsy (e.g. empty object, null, ...) */ const newQueriesToSave: DataQuery[] = queries && queries.filter((query) => notEmptyQuery(query)); @@ -44,9 +56,11 @@ export async function addToRichHistory( let warning: RichHistoryStorageWarningDetails | undefined; try { - const result = await getRichHistoryStorage().addToRichHistory({ - datasourceUid: datasourceUid, - datasourceName: datasourceName ?? '', + // for autocomplete we want to ensure writing to local storage + const storage = localOverride ? getLocalRichHistoryStorage() : getRichHistoryStorage(); + const result = await storage.addToRichHistory({ + datasourceUid: datasource.uid, + datasourceName: datasource.name ?? '', queries: newQueriesToSave, starred, comment: comment ?? '', @@ -57,7 +71,7 @@ export async function addToRichHistory( if (error.name === RichHistoryServiceError.StorageFull) { richHistoryStorageFull = true; showQuotaExceededError && dispatch(notifyApp(createErrorNotification(error.message))); - } else if (error.name !== RichHistoryServiceError.DuplicatedEntry) { + } else if (showOtherErrors && error.name !== RichHistoryServiceError.DuplicatedEntry) { dispatch( notifyApp( createErrorNotification( diff --git a/public/app/core/utils/richHistoryTypes.ts b/public/app/core/utils/richHistoryTypes.ts index b311b82752..c49029c035 100644 --- a/public/app/core/utils/richHistoryTypes.ts +++ b/public/app/core/utils/richHistoryTypes.ts @@ -23,8 +23,8 @@ export type RichHistorySearchFilters = { sortOrder: SortOrder; /** Names of data sources (not uids) - used by local and remote storage **/ datasourceFilters: string[]; - from: number; - to: number; + from?: number; + to?: number; starred: boolean; page?: number; }; diff --git a/public/app/features/explore/RichHistory/RichHistoryQueriesTab.tsx b/public/app/features/explore/RichHistory/RichHistoryQueriesTab.tsx index e118d22635..7a7b5cc9d0 100644 --- a/public/app/features/explore/RichHistory/RichHistoryQueriesTab.tsx +++ b/public/app/features/explore/RichHistory/RichHistoryQueriesTab.tsx @@ -166,6 +166,10 @@ export function RichHistoryQueriesTab(props: RichHistoryQueriesTabProps) { const mappedQueriesToHeadings = mapQueriesToHeadings(queries, richHistorySearchFilters.sortOrder); const sortOrderOptions = getSortOrderOptions(); const partialResults = queries.length && queries.length !== totalQueries; + const timeFilter = [ + richHistorySearchFilters.from || 0, + richHistorySearchFilters.to || richHistorySettings.retentionPeriod, + ]; return (
@@ -174,13 +178,13 @@ export function RichHistoryQueriesTab(props: RichHistoryQueriesTabProps) {
Filter history
-
{mapNumbertoTimeInSlider(richHistorySearchFilters.from)}
+
{mapNumbertoTimeInSlider(timeFilter[0])}
-
{mapNumbertoTimeInSlider(richHistorySearchFilters.to)}
+
{mapNumbertoTimeInSlider(timeFilter[1])}
diff --git a/public/app/features/explore/state/explorePane.ts b/public/app/features/explore/state/explorePane.ts index c426aaf690..e6feeda299 100644 --- a/public/app/features/explore/state/explorePane.ts +++ b/public/app/features/explore/state/explorePane.ts @@ -20,7 +20,6 @@ import { createAsyncThunk, ThunkResult } from 'app/types'; import { ExploreItemState } from 'app/types/explore'; import { datasourceReducer } from './datasource'; -import { historyReducer } from './history'; import { richHistorySearchFiltersUpdatedAction, richHistoryUpdatedAction } from './main'; import { queryReducer, runQueries } from './query'; import { timeReducer, updateTime } from './time'; @@ -214,7 +213,6 @@ export const paneReducer = (state: ExploreItemState = makeExplorePaneState(), ac state = queryReducer(state, action); state = datasourceReducer(state, action); state = timeReducer(state, action); - state = historyReducer(state, action); if (richHistoryUpdatedAction.match(action)) { const { richHistory, total } = action.payload.richHistoryResults; diff --git a/public/app/features/explore/state/history.ts b/public/app/features/explore/state/history.ts index 8a02945ceb..fa8348fd8d 100644 --- a/public/app/features/explore/state/history.ts +++ b/public/app/features/explore/state/history.ts @@ -1,6 +1,3 @@ -import { AnyAction, createAction } from '@reduxjs/toolkit'; - -import { HistoryItem } from '@grafana/data'; import { DataQuery } from '@grafana/schema'; import { addToRichHistory, @@ -26,16 +23,6 @@ import { } from './main'; import { selectPanesEntries } from './selectors'; -// -// Actions and Payloads -// - -export interface HistoryUpdatedPayload { - exploreId: string; - history: HistoryItem[]; -} -export const historyUpdatedAction = createAction('explore/historyUpdated'); - // // Action creators // @@ -74,25 +61,33 @@ const forEachExplorePane = (state: ExploreState, callback: (item: ExploreItemSta }; export const addHistoryItem = ( + localOverride: boolean, datasourceUid: string, datasourceName: string, - queries: DataQuery[] + queries: DataQuery[], + hideAllErrorsAndWarnings: boolean ): ThunkResult => { return async (dispatch, getState) => { - const { richHistoryStorageFull, limitExceeded } = await addToRichHistory( - datasourceUid, - datasourceName, + const showNotif = hideAllErrorsAndWarnings + ? { quotaExceededError: false, limitExceededWarning: false, otherErrors: false } + : { + quotaExceededError: !getState().explore.richHistoryStorageFull, + limitExceededWarning: !getState().explore.richHistoryLimitExceededWarningShown, + }; + const { richHistoryStorageFull, limitExceeded } = await addToRichHistory({ + localOverride, + datasource: { uid: datasourceUid, name: datasourceName }, queries, - false, - '', - !getState().explore.richHistoryStorageFull, - !getState().explore.richHistoryLimitExceededWarningShown - ); - if (richHistoryStorageFull) { - dispatch(richHistoryStorageFullAction()); - } - if (limitExceeded) { - dispatch(richHistoryLimitExceededAction()); + starred: false, + showNotif, + }); + if (!hideAllErrorsAndWarnings) { + if (richHistoryStorageFull) { + dispatch(richHistoryStorageFullAction()); + } + if (limitExceeded) { + dispatch(richHistoryLimitExceededAction()); + } } }; }; @@ -199,13 +194,3 @@ export const updateHistorySearchFilters = (exploreId: string, filters: RichHisto } }; }; - -export const historyReducer = (state: ExploreItemState, action: AnyAction): ExploreItemState => { - if (historyUpdatedAction.match(action)) { - return { - ...state, - history: action.payload.history, - }; - } - return state; -}; diff --git a/public/app/features/explore/state/query.test.ts b/public/app/features/explore/state/query.test.ts index 18bd129cba..334270c8f6 100644 --- a/public/app/features/explore/state/query.test.ts +++ b/public/app/features/explore/state/query.test.ts @@ -16,10 +16,12 @@ import { SupplementaryQueryType, } from '@grafana/data'; import { DataQuery, DataSourceRef } from '@grafana/schema'; +import config from 'app/core/config'; import { queryLogsSample, queryLogsVolume } from 'app/features/logs/logsModel'; import { createAsyncThunk, ExploreItemState, StoreState, ThunkDispatch } from 'app/types'; import { reducerTester } from '../../../../test/core/redux/reducerTester'; +import * as richHistory from '../../../core/utils/richHistory'; import { configureStore } from '../../../store/configureStore'; import { setTimeSrv, TimeSrv } from '../../dashboard/services/TimeSrv'; import { makeLogs } from '../__mocks__/makeLogs'; @@ -155,6 +157,11 @@ describe('runQueries', () => { } as unknown as Partial); }; + beforeEach(() => { + config.queryHistoryEnabled = false; + jest.clearAllMocks(); + }); + it('should pass dataFrames to state even if there is error in response', async () => { const { dispatch, getState } = setupTests(); setupQueryResponse(getState()); @@ -202,6 +209,24 @@ describe('runQueries', () => { await dispatch(saveCorrelationsAction({ exploreId: 'left', correlations: [] })); expect(getState().explore.panes.left!.graphResult).toBeDefined(); }); + + it('should add history items to both local and remote storage with the flag enabled', async () => { + config.queryHistoryEnabled = true; + const { dispatch } = setupTests(); + jest.spyOn(richHistory, 'addToRichHistory'); + await dispatch(runQueries({ exploreId: 'left' })); + expect((richHistory.addToRichHistory as jest.Mock).mock.calls).toHaveLength(2); + expect((richHistory.addToRichHistory as jest.Mock).mock.calls[0][0].localOverride).toBeTruthy(); + expect((richHistory.addToRichHistory as jest.Mock).mock.calls[1][0].localOverride).toBeFalsy(); + }); + + it('should add history items to local storage only with the flag disabled', async () => { + const { dispatch } = setupTests(); + jest.spyOn(richHistory, 'addToRichHistory'); + await dispatch(runQueries({ exploreId: 'left' })); + expect((richHistory.addToRichHistory as jest.Mock).mock.calls).toHaveLength(1); + expect((richHistory.addToRichHistory as jest.Mock).mock.calls[0][0].localOverride).toBeTruthy(); + }); }); describe('running queries', () => { diff --git a/public/app/features/explore/state/query.ts b/public/app/features/explore/state/query.ts index b2b123fa22..320f1b4710 100644 --- a/public/app/features/explore/state/query.ts +++ b/public/app/features/explore/state/query.ts @@ -13,7 +13,6 @@ import { dateTimeForTimeZone, hasQueryExportSupport, hasQueryImportSupport, - HistoryItem, LoadingState, LogsVolumeType, PanelEvents, @@ -35,7 +34,6 @@ import { getTimeRange, hasNonEmptyQuery, stopQueryState, - updateHistory, } from 'app/core/utils/explore'; import { getShiftedTimeRange } from 'app/core/utils/timePicker'; import { getCorrelationsBySourceUIDs } from 'app/features/correlations/utils'; @@ -67,7 +65,7 @@ import { import { getCorrelations } from './correlations'; import { saveCorrelationsAction } from './explorePane'; -import { addHistoryItem, historyUpdatedAction, loadRichHistory } from './history'; +import { addHistoryItem, loadRichHistory } from './history'; import { changeCorrelationEditorDetails } from './main'; import { updateTime } from './time'; import { @@ -481,16 +479,18 @@ export function modifyQueries( async function handleHistory( dispatch: ThunkDispatch, state: ExploreState, - history: Array>, datasource: DataSourceApi, - queries: DataQuery[], - exploreId: string + queries: DataQuery[] ) { - const datasourceId = datasource.meta.id; - const nextHistory = updateHistory(history, datasourceId, queries); - dispatch(historyUpdatedAction({ exploreId, history: nextHistory })); - - dispatch(addHistoryItem(datasource.uid, datasource.name, queries)); + /* + Always write to local storage. If query history is enabled, we will use local storage for autocomplete only (and want to hide errors) + If query history is disabled, we will use local storage for query history as well, and will want to show errors + */ + dispatch(addHistoryItem(true, datasource.uid, datasource.name, queries, config.queryHistoryEnabled)); + if (config.queryHistoryEnabled) { + // write to remote if flag enabled + dispatch(addHistoryItem(false, datasource.uid, datasource.name, queries, false)); + } // Because filtering happens in the backend we cannot add a new entry without checking if it matches currently // used filters. Instead, we refresh the query history list. @@ -550,7 +550,7 @@ export const runQueries = createAsyncThunk( })); if (datasourceInstance != null) { - handleHistory(dispatch, getState().explore, exploreItemState.history, datasourceInstance, queries, exploreId); + handleHistory(dispatch, getState().explore, datasourceInstance, queries); } const cachedValue = getResultsFromCache(cache, absoluteRange); diff --git a/public/app/features/explore/state/utils.test.ts b/public/app/features/explore/state/utils.test.ts index e58c1fe4b4..e044c64728 100644 --- a/public/app/features/explore/state/utils.test.ts +++ b/public/app/features/explore/state/utils.test.ts @@ -1,6 +1,9 @@ import { dateTime } from '@grafana/data'; +import { getLocalRichHistoryStorage } from 'app/core/history/richHistoryStorageProvider'; import * as exploreUtils from 'app/core/utils/explore'; +import { loadAndInitDatasource, getRange, fromURLRange, MAX_HISTORY_AUTOCOMPLETE_ITEMS } from './utils'; + const dataSourceMock = { get: jest.fn(), }; @@ -8,7 +11,15 @@ jest.mock('app/features/plugins/datasource_srv', () => ({ getDatasourceSrv: jest.fn(() => dataSourceMock), })); -import { loadAndInitDatasource, getRange, fromURLRange } from './utils'; +const mockLocalDataStorage = { + getRichHistory: jest.fn(), +}; + +jest.mock('app/core/history/richHistoryStorageProvider', () => ({ + getLocalRichHistoryStorage: jest.fn(() => { + return mockLocalDataStorage; + }), +})); const DEFAULT_DATASOURCE = { uid: 'abc123', name: 'Default' }; const TEST_DATASOURCE = { uid: 'def789', name: 'Test' }; @@ -28,6 +39,7 @@ describe('loadAndInitDatasource', () => { setLastUsedDatasourceUIDSpy = jest.spyOn(exploreUtils, 'setLastUsedDatasourceUID'); dataSourceMock.get.mockRejectedValueOnce(new Error('Datasource not found')); dataSourceMock.get.mockResolvedValue(DEFAULT_DATASOURCE); + mockLocalDataStorage.getRichHistory.mockResolvedValue({ total: 0, richHistory: [] }); const { instance } = await loadAndInitDatasource(1, { uid: 'Unknown' }); @@ -41,14 +53,111 @@ describe('loadAndInitDatasource', () => { it('saves last loaded data source uid', async () => { setLastUsedDatasourceUIDSpy = jest.spyOn(exploreUtils, 'setLastUsedDatasourceUID'); dataSourceMock.get.mockResolvedValue(TEST_DATASOURCE); + mockLocalDataStorage.getRichHistory.mockResolvedValue({ + total: 0, + richHistory: [], + }); const { instance } = await loadAndInitDatasource(1, { uid: 'Test' }); - expect(dataSourceMock.get).toBeCalledTimes(1); - expect(dataSourceMock.get).toBeCalledWith({ uid: 'Test' }); + expect(dataSourceMock.get).toHaveBeenCalledTimes(1); + expect(dataSourceMock.get).toHaveBeenCalledWith({ uid: 'Test' }); + expect(getLocalRichHistoryStorage).toHaveBeenCalledTimes(1); + expect(instance).toMatchObject(TEST_DATASOURCE); expect(setLastUsedDatasourceUIDSpy).toBeCalledWith(1, TEST_DATASOURCE.uid); }); + + it('pulls history data and returns the history by query', async () => { + setLastUsedDatasourceUIDSpy = jest.spyOn(exploreUtils, 'setLastUsedDatasourceUID'); + dataSourceMock.get.mockResolvedValue(TEST_DATASOURCE); + mockLocalDataStorage.getRichHistory.mockResolvedValueOnce({ + total: 1, + richHistory: [ + { + id: '0', + createdAt: 0, + datasourceUid: 'Test', + datasourceName: 'Test', + starred: false, + comment: '', + queries: [{ refId: 'A' }, { refId: 'B' }], + }, + ], + }); + + const { history } = await loadAndInitDatasource(1, { uid: 'Test' }); + expect(getLocalRichHistoryStorage).toHaveBeenCalledTimes(1); + expect(history.length).toEqual(2); + }); + + it('pulls history data and returns the history by query with Mixed results', async () => { + setLastUsedDatasourceUIDSpy = jest.spyOn(exploreUtils, 'setLastUsedDatasourceUID'); + dataSourceMock.get.mockResolvedValue(TEST_DATASOURCE); + mockLocalDataStorage.getRichHistory.mockResolvedValueOnce({ + total: 1, + richHistory: [ + { + id: '0', + createdAt: 0, + datasourceUid: 'Test', + datasourceName: 'Test', + starred: false, + comment: '', + queries: [{ refId: 'A' }, { refId: 'B' }], + }, + ], + }); + + mockLocalDataStorage.getRichHistory.mockResolvedValueOnce({ + total: 1, + richHistory: [ + { + id: '0', + createdAt: 0, + datasourceUid: 'Mixed', + datasourceName: 'Mixed', + starred: false, + comment: '', + queries: [ + { refId: 'A', datasource: { uid: 'def789' } }, + { refId: 'B', datasource: { uid: 'def789' } }, + ], + }, + ], + }); + + const { history } = await loadAndInitDatasource(1, { uid: 'Test' }); + expect(getLocalRichHistoryStorage).toHaveBeenCalledTimes(1); + expect(history.length).toEqual(4); + }); + + it('pulls history data and returns only a max of MAX_HISTORY_AUTOCOMPLETE_ITEMS items', async () => { + const queryList = [...Array(MAX_HISTORY_AUTOCOMPLETE_ITEMS + 50).keys()].map((i) => { + return { refId: `ref-${i}` }; + }); + + setLastUsedDatasourceUIDSpy = jest.spyOn(exploreUtils, 'setLastUsedDatasourceUID'); + dataSourceMock.get.mockResolvedValue(TEST_DATASOURCE); + mockLocalDataStorage.getRichHistory.mockResolvedValueOnce({ + total: 1, + richHistory: [ + { + id: '0', + createdAt: 0, + datasourceUid: 'Test', + datasourceName: 'Test', + starred: false, + comment: '', + queries: queryList, + }, + ], + }); + + const { history } = await loadAndInitDatasource(1, { uid: 'Test' }); + expect(getLocalRichHistoryStorage).toHaveBeenCalledTimes(1); + expect(history.length).toEqual(MAX_HISTORY_AUTOCOMPLETE_ITEMS); + }); }); describe('getRange', () => { diff --git a/public/app/features/explore/state/utils.ts b/public/app/features/explore/state/utils.ts index 616ba220c8..0918049c03 100644 --- a/public/app/features/explore/state/utils.ts +++ b/public/app/features/explore/state/utils.ts @@ -21,16 +21,20 @@ import { URLRangeValue, } from '@grafana/data'; import { getDataSourceSrv } from '@grafana/runtime'; -import { DataQuery, DataSourceRef, TimeZone } from '@grafana/schema'; +import { DataQuery, DataSourceJsonData, DataSourceRef, TimeZone } from '@grafana/schema'; +import { getLocalRichHistoryStorage } from 'app/core/history/richHistoryStorageProvider'; +import { SortOrder } from 'app/core/utils/richHistory'; import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource'; import { ExplorePanelData, StoreState } from 'app/types'; -import { ExploreItemState } from 'app/types/explore'; +import { ExploreItemState, RichHistoryQuery } from 'app/types/explore'; import store from '../../../core/store'; import { setLastUsedDatasourceUID } from '../../../core/utils/explore'; import { getDatasourceSrv } from '../../plugins/datasource_srv'; import { loadSupplementaryQueries } from '../utils/supplementaryQueries'; +export const MAX_HISTORY_AUTOCOMPLETE_ITEMS = 100; + export const DEFAULT_RANGE = { from: 'now-1h', to: 'now', @@ -100,7 +104,7 @@ export async function loadAndInitDatasource( orgId: number, datasource: DataSourceRef | string ): Promise<{ history: HistoryItem[]; instance: DataSourceApi }> { - let instance; + let instance: DataSourceApi; try { // let datasource be a ref if we have the info, otherwise a name or uid will do for lookup instance = await getDatasourceSrv().get(datasource); @@ -119,12 +123,60 @@ export async function loadAndInitDatasource( } } - const historyKey = `grafana.explore.history.${instance.meta?.id}`; - const history = store.getObject(historyKey, []); - // Save last-used datasource + let history: HistoryItem[] = []; + + const localStorageHistory = getLocalRichHistoryStorage(); + + const historyResults = await localStorageHistory.getRichHistory({ + search: '', + sortOrder: SortOrder.Ascending, + datasourceFilters: [instance.name], + starred: false, + }); + + // first, fill autocomplete with query history for that datasource + if ((historyResults.total || 0) > 0) { + historyResults.richHistory.forEach((historyResult: RichHistoryQuery) => { + historyResult.queries.forEach((q) => { + history.push({ ts: parseInt(historyResult.id, 10), query: q }); + }); + }); + } + + if (history.length < MAX_HISTORY_AUTOCOMPLETE_ITEMS) { + // check the last 100 mixed history results seperately + const historyMixedResults = await localStorageHistory.getRichHistory({ + search: '', + sortOrder: SortOrder.Ascending, + datasourceFilters: [MIXED_DATASOURCE_NAME], + starred: false, + }); + if ((historyMixedResults.total || 0) > 0) { + // second, fill autocomplete with queries for that datasource used in Mixed scenarios + historyMixedResults.richHistory.forEach((historyResult: RichHistoryQuery) => { + historyResult.queries.forEach((q) => { + if (q?.datasource?.uid === instance.uid) { + history.push({ ts: parseInt(historyResult.id, 10), query: q }); + } + }); + }); + } + } + + // finally, add any legacy local storage history that might exist. To be removed in Grafana 12 #83309 + if (history.length < MAX_HISTORY_AUTOCOMPLETE_ITEMS) { + const historyKey = `grafana.explore.history.${instance.meta?.id}`; + history = [...history, ...store.getObject(historyKey, [])]; + } + + if (history.length > MAX_HISTORY_AUTOCOMPLETE_ITEMS) { + history.length = MAX_HISTORY_AUTOCOMPLETE_ITEMS; + } + + // Save last-used datasource setLastUsedDatasourceUID(orgId, instance.uid); - return { history, instance }; + return { history: history, instance }; } export function createCacheKey(absRange: AbsoluteTimeRange) { From b2601d71d5bf025280169698a875f9a28ca55feb Mon Sep 17 00:00:00 2001 From: Jo Date: Fri, 23 Feb 2024 16:53:37 +0100 Subject: [PATCH 0134/1406] IAM: Remove fully rolled out feature toggles (#83308) * remove anon stat ft * remove split scope flag * remove feature toggle from frontend --- .../feature-toggles/index.md | 1 - .../src/types/featureToggles.gen.ts | 2 -- pkg/services/featuremgmt/registry.go | 19 ------------------- pkg/services/featuremgmt/toggles_gen.csv | 2 -- pkg/services/featuremgmt/toggles_gen.go | 8 -------- pkg/services/featuremgmt/toggles_gen.json | 12 +++++++----- .../app/features/admin/ServerStats.test.tsx | 1 - public/app/features/admin/ServerStats.tsx | 2 +- public/app/features/admin/UserListPage.tsx | 2 +- 9 files changed, 9 insertions(+), 40 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 1b8c404a11..3825032dfd 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -54,7 +54,6 @@ Some features are enabled by default. You can disable these feature by setting t | `recoveryThreshold` | Enables feature recovery threshold (aka hysteresis) for threshold server-side expression | Yes | | `lokiStructuredMetadata` | Enables the loki data source to request structured metadata from the Loki server | Yes | | `logRowsPopoverMenu` | Enable filtering menu displayed when text of a log line is selected | Yes | -| `displayAnonymousStats` | Enables anonymous stats to be shown in the UI for Grafana | Yes | | `lokiQueryHints` | Enables query hints for Loki | Yes | | `alertingPreviewUpgrade` | Show Unified Alerting preview and upgrade page in legacy alerting | Yes | | `alertingQueryOptimization` | Optimizes eligible queries in order to reduce load on datasources | | diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 5f00693294..501ccfb269 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -105,7 +105,6 @@ export interface FeatureToggles { grafanaAPIServerEnsureKubectlAccess?: boolean; featureToggleAdminPage?: boolean; awsAsyncQueryCaching?: boolean; - splitScopes?: boolean; permissionsFilterRemoveSubquery?: boolean; prometheusConfigOverhaulAuth?: boolean; configurableSchedulerTick?: boolean; @@ -162,7 +161,6 @@ export interface FeatureToggles { pluginsSkipHostEnvVars?: boolean; tableSharedCrosshair?: boolean; regressionTransformation?: boolean; - displayAnonymousStats?: boolean; lokiQueryHints?: boolean; kubernetesFeatureToggles?: boolean; alertingPreviewUpgrade?: boolean; diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 3af6aeb7e7..9adfa778b3 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -649,16 +649,6 @@ var ( Expression: "true", // enabled by default Owner: awsDatasourcesSquad, }, - { - Name: "splitScopes", - Description: "Support faster dashboard and folder search by splitting permission scopes into parts", - Stage: FeatureStageDeprecated, - FrontendOnly: false, - Expression: "true", // enabled by default - Owner: identityAccessTeam, - RequiresRestart: true, - HideFromAdminPage: true, // This is internal work to speed up dashboard search, and is not ready for wider use - }, { Name: "permissionsFilterRemoveSubquery", Description: "Alternative permission filter implementation that does not use subqueries for fetching the dashboard folder", @@ -1068,15 +1058,6 @@ var ( FrontendOnly: true, Owner: grafanaDatavizSquad, }, - { - Name: "displayAnonymousStats", - Description: "Enables anonymous stats to be shown in the UI for Grafana", - Stage: FeatureStageGeneralAvailability, - FrontendOnly: true, - Owner: identityAccessTeam, - AllowSelfServe: false, - Expression: "true", // enabled by default - }, { // this is mainly used a a way to quickly disable query hints as a safe guard for our infrastructure Name: "lokiQueryHints", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index a9ebfc7ea8..9a978ec61d 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -86,7 +86,6 @@ grafanaAPIServerWithExperimentalAPIs,experimental,@grafana/grafana-app-platform- grafanaAPIServerEnsureKubectlAccess,experimental,@grafana/grafana-app-platform-squad,true,true,false featureToggleAdminPage,experimental,@grafana/grafana-operator-experience-squad,false,true,false awsAsyncQueryCaching,GA,@grafana/aws-datasources,false,false,false -splitScopes,deprecated,@grafana/identity-access-team,false,true,false permissionsFilterRemoveSubquery,experimental,@grafana/backend-platform,false,false,false prometheusConfigOverhaulAuth,GA,@grafana/observability-metrics,false,false,false configurableSchedulerTick,experimental,@grafana/alerting-squad,false,true,false @@ -143,7 +142,6 @@ logRowsPopoverMenu,GA,@grafana/observability-logs,false,false,true pluginsSkipHostEnvVars,experimental,@grafana/plugins-platform-backend,false,false,false tableSharedCrosshair,experimental,@grafana/dataviz-squad,false,false,true regressionTransformation,preview,@grafana/dataviz-squad,false,false,true -displayAnonymousStats,GA,@grafana/identity-access-team,false,false,true lokiQueryHints,GA,@grafana/observability-logs,false,false,true kubernetesFeatureToggles,experimental,@grafana/grafana-operator-experience-squad,false,false,true alertingPreviewUpgrade,GA,@grafana/alerting-squad,false,true,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 746eec1bd6..ada2955c68 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -355,10 +355,6 @@ const ( // Enable caching for async queries for Redshift and Athena. Requires that the datasource has caching and async query support enabled FlagAwsAsyncQueryCaching = "awsAsyncQueryCaching" - // FlagSplitScopes - // Support faster dashboard and folder search by splitting permission scopes into parts - FlagSplitScopes = "splitScopes" - // FlagPermissionsFilterRemoveSubquery // Alternative permission filter implementation that does not use subqueries for fetching the dashboard folder FlagPermissionsFilterRemoveSubquery = "permissionsFilterRemoveSubquery" @@ -583,10 +579,6 @@ const ( // Enables regression analysis transformation FlagRegressionTransformation = "regressionTransformation" - // FlagDisplayAnonymousStats - // Enables anonymous stats to be shown in the UI for Grafana - FlagDisplayAnonymousStats = "displayAnonymousStats" - // FlagLokiQueryHints // Enables query hints for Loki FlagLokiQueryHints = "lokiQueryHints" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 35e90a0902..e0bca47266 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -83,7 +83,7 @@ "name": "pluginsInstrumentationStatusSource", "resourceVersion": "1708108588074", "creationTimestamp": "2024-02-16T18:36:28Z", - "deletionTimestamp": "2024-02-19T14:18:02Z" + "deletionTimestamp": "2024-02-23T13:23:30Z" }, "spec": { "description": "Include a status source label for plugin request metrics and logs", @@ -510,7 +510,8 @@ "metadata": { "name": "displayAnonymousStats", "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "creationTimestamp": "2024-02-16T18:36:28Z", + "deletionTimestamp": "2024-02-23T13:23:30Z" }, "spec": { "description": "Enables anonymous stats to be shown in the UI for Grafana", @@ -1399,7 +1400,7 @@ "name": "traceToMetrics", "resourceVersion": "1708108588074", "creationTimestamp": "2024-02-16T18:36:28Z", - "deletionTimestamp": "2024-02-16T10:09:14Z" + "deletionTimestamp": "2024-02-23T13:23:30Z" }, "spec": { "description": "Enable trace to metrics links", @@ -1527,7 +1528,8 @@ "metadata": { "name": "splitScopes", "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "creationTimestamp": "2024-02-16T18:36:28Z", + "deletionTimestamp": "2024-02-23T13:23:30Z" }, "spec": { "description": "Support faster dashboard and folder search by splitting permission scopes into parts", @@ -2130,4 +2132,4 @@ } } ] -} +} \ No newline at end of file diff --git a/public/app/features/admin/ServerStats.test.tsx b/public/app/features/admin/ServerStats.test.tsx index b658800f12..2a88bcb7fe 100644 --- a/public/app/features/admin/ServerStats.test.tsx +++ b/public/app/features/admin/ServerStats.test.tsx @@ -51,7 +51,6 @@ describe('ServerStats', () => { }); it('Should render page with anonymous stats', async () => { - config.featureToggles.displayAnonymousStats = true; config.anonymousEnabled = true; config.anonymousDeviceLimit = 10; render(); diff --git a/public/app/features/admin/ServerStats.tsx b/public/app/features/admin/ServerStats.tsx index 4e94e8a6a4..8d36649545 100644 --- a/public/app/features/admin/ServerStats.tsx +++ b/public/app/features/admin/ServerStats.tsx @@ -100,7 +100,7 @@ export const ServerStats = () => { }; const getAnonymousStatsContent = (stats: ServerStat | null, config: GrafanaBootConfig) => { - if (!config.anonymousEnabled || !config.featureToggles.displayAnonymousStats || !stats?.activeDevices) { + if (!config.anonymousEnabled || !stats?.activeDevices) { return []; } if (!config.anonymousDeviceLimit) { diff --git a/public/app/features/admin/UserListPage.tsx b/public/app/features/admin/UserListPage.tsx index acf37d55c6..378740f95a 100644 --- a/public/app/features/admin/UserListPage.tsx +++ b/public/app/features/admin/UserListPage.tsx @@ -79,7 +79,7 @@ export default function UserListPage() { onChangeTab={() => setView(TabView.ORG)} data-testid={selectors.tabs.orgUsers} /> - {config.anonymousEnabled && config.featureToggles.displayAnonymousStats && ( + {config.anonymousEnabled && ( Date: Fri, 23 Feb 2024 16:03:23 +0000 Subject: [PATCH 0135/1406] RBAC: add kind, attribute and identifier to annotation permissions during the migration (#83299) add kind, attribute and identifier to annotation permissions during the migration --- .../accesscontrol/dashboard_permissions.go | 57 ++++++++++++------- 1 file changed, 37 insertions(+), 20 deletions(-) diff --git a/pkg/services/sqlstore/migrations/accesscontrol/dashboard_permissions.go b/pkg/services/sqlstore/migrations/accesscontrol/dashboard_permissions.go index 0198ded573..9a2aeda128 100644 --- a/pkg/services/sqlstore/migrations/accesscontrol/dashboard_permissions.go +++ b/pkg/services/sqlstore/migrations/accesscontrol/dashboard_permissions.go @@ -730,14 +730,22 @@ func (m *managedDashboardAnnotationActionsMigrator) Exec(sess *xorm.Session, mg for roleId, mappedPermissions := range mapped { for scope, roleActions := range mappedPermissions { + // Create a temporary permission to split the scope into kind, attribute and identifier + tempPerm := ac.Permission{ + Scope: scope, + } + kind, attribute, identifier := tempPerm.SplitScope() if roleActions[dashboards.ActionDashboardsRead] { if !roleActions[ac.ActionAnnotationsRead] { toAdd = append(toAdd, ac.Permission{ - RoleID: roleId, - Updated: now, - Created: now, - Scope: scope, - Action: ac.ActionAnnotationsRead, + RoleID: roleId, + Updated: now, + Created: now, + Scope: scope, + Action: ac.ActionAnnotationsRead, + Kind: kind, + Attribute: attribute, + Identifier: identifier, }) } } @@ -745,29 +753,38 @@ func (m *managedDashboardAnnotationActionsMigrator) Exec(sess *xorm.Session, mg if roleActions[dashboards.ActionDashboardsWrite] { if !roleActions[ac.ActionAnnotationsCreate] { toAdd = append(toAdd, ac.Permission{ - RoleID: roleId, - Updated: now, - Created: now, - Scope: scope, - Action: ac.ActionAnnotationsCreate, + RoleID: roleId, + Updated: now, + Created: now, + Scope: scope, + Action: ac.ActionAnnotationsCreate, + Kind: kind, + Attribute: attribute, + Identifier: identifier, }) } if !roleActions[ac.ActionAnnotationsDelete] { toAdd = append(toAdd, ac.Permission{ - RoleID: roleId, - Updated: now, - Created: now, - Scope: scope, - Action: ac.ActionAnnotationsDelete, + RoleID: roleId, + Updated: now, + Created: now, + Scope: scope, + Action: ac.ActionAnnotationsDelete, + Kind: kind, + Attribute: attribute, + Identifier: identifier, }) } if !roleActions[ac.ActionAnnotationsWrite] { toAdd = append(toAdd, ac.Permission{ - RoleID: roleId, - Updated: now, - Created: now, - Scope: scope, - Action: ac.ActionAnnotationsWrite, + RoleID: roleId, + Updated: now, + Created: now, + Scope: scope, + Action: ac.ActionAnnotationsWrite, + Kind: kind, + Attribute: attribute, + Identifier: identifier, }) } } From 19b1e71feefbae21bbe76a64fad9724ce876b9ed Mon Sep 17 00:00:00 2001 From: Ieva Date: Fri, 23 Feb 2024 16:13:21 +0000 Subject: [PATCH 0136/1406] IP range AC for data sources: compare the base of the URL only (#83305) * compare the base of the URL and ignore the path * change the logic to compare scheme and host explicitly * fix the test --- .../grafana_request_id_header_middleware.go | 15 ++++++++----- ...afana_request_id_header_middleware_test.go | 21 ++++++++++++------- pkg/setting/setting.go | 12 +++++++++-- 3 files changed, 33 insertions(+), 15 deletions(-) diff --git a/pkg/services/pluginsintegration/clientmiddleware/grafana_request_id_header_middleware.go b/pkg/services/pluginsintegration/clientmiddleware/grafana_request_id_header_middleware.go index f8282cd27e..7637c8b051 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/grafana_request_id_header_middleware.go +++ b/pkg/services/pluginsintegration/clientmiddleware/grafana_request_id_header_middleware.go @@ -5,7 +5,7 @@ import ( "crypto/hmac" "crypto/sha256" "encoding/hex" - "path" + "net/url" "github.com/google/uuid" @@ -49,17 +49,22 @@ func (m *HostedGrafanaACHeaderMiddleware) applyGrafanaRequestIDHeader(ctx contex } // Check if the request is for a datasource that is allowed to have the header - target := pCtx.DataSourceInstanceSettings.URL - + dsURL := pCtx.DataSourceInstanceSettings.URL + dsBaseURL, err := url.Parse(dsURL) + if err != nil { + m.log.Debug("Failed to parse data source URL", "error", err) + return + } foundMatch := false for _, allowedURL := range m.cfg.IPRangeACAllowedURLs { - if path.Clean(allowedURL) == path.Clean(target) { + // Only look at the scheme and host, ignore the path + if allowedURL.Host == dsBaseURL.Host && allowedURL.Scheme == dsBaseURL.Scheme { foundMatch = true break } } if !foundMatch { - m.log.Debug("Data source URL not among the allow-listed URLs", "url", target) + m.log.Debug("Data source URL not among the allow-listed URLs", "url", dsBaseURL.String()) return } diff --git a/pkg/services/pluginsintegration/clientmiddleware/grafana_request_id_header_middleware_test.go b/pkg/services/pluginsintegration/clientmiddleware/grafana_request_id_header_middleware_test.go index 8204c70d4a..3c389edea1 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/grafana_request_id_header_middleware_test.go +++ b/pkg/services/pluginsintegration/clientmiddleware/grafana_request_id_header_middleware_test.go @@ -6,6 +6,7 @@ import ( "crypto/sha256" "encoding/hex" "net/http" + "net/url" "testing" "github.com/stretchr/testify/require" @@ -22,7 +23,8 @@ import ( func Test_HostedGrafanaACHeaderMiddleware(t *testing.T) { t.Run("Should set Grafana request ID headers if the data source URL is in the allow list", func(t *testing.T) { cfg := setting.NewCfg() - cfg.IPRangeACAllowedURLs = []string{"https://logs.grafana.net"} + allowedURL := &url.URL{Scheme: "https", Host: "logs.grafana.net"} + cfg.IPRangeACAllowedURLs = []*url.URL{allowedURL} cfg.IPRangeACSecretKey = "secret" cdt := clienttest.NewClientDecoratorTest(t, clienttest.WithMiddlewares(NewHostedGrafanaACHeaderMiddleware(cfg))) @@ -63,7 +65,8 @@ func Test_HostedGrafanaACHeaderMiddleware(t *testing.T) { t.Run("Should not set Grafana request ID headers if the data source URL is not in the allow list", func(t *testing.T) { cfg := setting.NewCfg() - cfg.IPRangeACAllowedURLs = []string{"https://logs.grafana.net"} + allowedURL := &url.URL{Scheme: "https", Host: "logs.grafana.net"} + cfg.IPRangeACAllowedURLs = []*url.URL{allowedURL} cfg.IPRangeACSecretKey = "secret" cdt := clienttest.NewClientDecoratorTest(t, clienttest.WithMiddlewares(NewHostedGrafanaACHeaderMiddleware(cfg))) @@ -85,9 +88,10 @@ func Test_HostedGrafanaACHeaderMiddleware(t *testing.T) { require.Len(t, cdt.CallResourceReq.Headers[GrafanaSignedRequestID], 0) }) - t.Run("Should set Grafana request ID headers if a sanitized data source URL is in the allow list", func(t *testing.T) { + t.Run("Should set Grafana request ID headers if URL scheme and host match a URL from the allow list", func(t *testing.T) { cfg := setting.NewCfg() - cfg.IPRangeACAllowedURLs = []string{"https://logs.GRAFANA.net/"} + allowedURL := &url.URL{Scheme: "https", Host: "logs.grafana.net"} + cfg.IPRangeACAllowedURLs = []*url.URL{allowedURL} cfg.IPRangeACSecretKey = "secret" cdt := clienttest.NewClientDecoratorTest(t, clienttest.WithMiddlewares(NewHostedGrafanaACHeaderMiddleware(cfg))) @@ -99,19 +103,20 @@ func Test_HostedGrafanaACHeaderMiddleware(t *testing.T) { err := cdt.Decorator.CallResource(ctx, &backend.CallResourceRequest{ PluginContext: backend.PluginContext{ DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{ - URL: "https://logs.grafana.net/abc/../", + URL: "https://logs.grafana.net/abc/../some/path", }, }, }, nopCallResourceSender) require.NoError(t, err) - require.Len(t, cdt.CallResourceReq.Headers[GrafanaRequestID], 0) - require.Len(t, cdt.CallResourceReq.Headers[GrafanaSignedRequestID], 0) + require.Len(t, cdt.CallResourceReq.Headers[GrafanaRequestID], 1) + require.Len(t, cdt.CallResourceReq.Headers[GrafanaSignedRequestID], 1) }) t.Run("Should set Grafana internal request header if the request is internal (doesn't have X-Real-IP header set)", func(t *testing.T) { cfg := setting.NewCfg() - cfg.IPRangeACAllowedURLs = []string{"https://logs.grafana.net"} + allowedURL := &url.URL{Scheme: "https", Host: "logs.grafana.net"} + cfg.IPRangeACAllowedURLs = []*url.URL{allowedURL} cfg.IPRangeACSecretKey = "secret" cdt := clienttest.NewClientDecoratorTest(t, clienttest.WithMiddlewares(NewHostedGrafanaACHeaderMiddleware(cfg))) diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 95a28baafa..c00b5e5009 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -339,7 +339,7 @@ type Cfg struct { // IP range access control IPRangeACEnabled bool - IPRangeACAllowedURLs []string + IPRangeACAllowedURLs []*url.URL IPRangeACSecretKey string // SQL Data sources @@ -1949,7 +1949,15 @@ func (cfg *Cfg) readDataSourceSecuritySettings() { cfg.IPRangeACEnabled = datasources.Key("enabled").MustBool(false) cfg.IPRangeACSecretKey = datasources.Key("secret_key").MustString("") allowedURLString := datasources.Key("allow_list").MustString("") - cfg.IPRangeACAllowedURLs = util.SplitString(allowedURLString) + for _, urlString := range util.SplitString(allowedURLString) { + allowedURL, err := url.Parse(urlString) + if err != nil { + cfg.Logger.Error("Error parsing allowed URL for IP range access control", "error", err) + continue + } else { + cfg.IPRangeACAllowedURLs = append(cfg.IPRangeACAllowedURLs, allowedURL) + } + } } func (cfg *Cfg) readSqlDataSourceSettings() { From 1477e658ec0795d1a41bcd1da7c377125f05b72d Mon Sep 17 00:00:00 2001 From: Andreas Christou Date: Fri, 23 Feb 2024 16:15:28 +0000 Subject: [PATCH 0137/1406] CI: Add retry for yarn install (#83317) Add retry for yarn install --- .drone.yml | 24 ++++++++++++------------ scripts/drone/steps/lib.star | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.drone.yml b/.drone.yml index d632880afc..dcc5b951d6 100644 --- a/.drone.yml +++ b/.drone.yml @@ -124,7 +124,7 @@ steps: name: identify-runner - commands: - apk add --update g++ make python3 && ln -sf /usr/bin/python3 /usr/bin/python - - yarn install --immutable + - yarn install --immutable || yarn install --immutable depends_on: [] image: node:20.9.0-alpine name: yarn-install @@ -226,7 +226,7 @@ steps: name: identify-runner - commands: - apk add --update g++ make python3 && ln -sf /usr/bin/python3 /usr/bin/python - - yarn install --immutable + - yarn install --immutable || yarn install --immutable depends_on: [] image: node:20.9.0-alpine name: yarn-install @@ -539,7 +539,7 @@ steps: name: wire-install - commands: - apk add --update g++ make python3 && ln -sf /usr/bin/python3 /usr/bin/python - - yarn install --immutable + - yarn install --immutable || yarn install --immutable depends_on: [] image: node:20.9.0-alpine name: yarn-install @@ -1113,7 +1113,7 @@ steps: name: identify-runner - commands: - apk add --update g++ make python3 && ln -sf /usr/bin/python3 /usr/bin/python - - yarn install --immutable + - yarn install --immutable || yarn install --immutable depends_on: [] image: node:20.9.0-alpine name: yarn-install @@ -1473,7 +1473,7 @@ steps: name: identify-runner - commands: - apk add --update g++ make python3 && ln -sf /usr/bin/python3 /usr/bin/python - - yarn install --immutable + - yarn install --immutable || yarn install --immutable depends_on: [] image: node:20.9.0-alpine name: yarn-install @@ -1550,7 +1550,7 @@ steps: name: identify-runner - commands: - apk add --update g++ make python3 && ln -sf /usr/bin/python3 /usr/bin/python - - yarn install --immutable + - yarn install --immutable || yarn install --immutable depends_on: [] image: node:20.9.0-alpine name: yarn-install @@ -1609,7 +1609,7 @@ steps: name: identify-runner - commands: - apk add --update g++ make python3 && ln -sf /usr/bin/python3 /usr/bin/python - - yarn install --immutable + - yarn install --immutable || yarn install --immutable depends_on: [] image: node:20.9.0-alpine name: yarn-install @@ -1873,7 +1873,7 @@ steps: name: wire-install - commands: - apk add --update g++ make python3 && ln -sf /usr/bin/python3 /usr/bin/python - - yarn install --immutable + - yarn install --immutable || yarn install --immutable depends_on: [] image: node:20.9.0-alpine name: yarn-install @@ -2853,7 +2853,7 @@ steps: name: compile-build-cmd - commands: - apk add --update g++ make python3 && ln -sf /usr/bin/python3 /usr/bin/python - - yarn install --immutable + - yarn install --immutable || yarn install --immutable depends_on: [] image: node:20.9.0-alpine name: yarn-install @@ -3127,7 +3127,7 @@ steps: name: identify-runner - commands: - apk add --update g++ make python3 && ln -sf /usr/bin/python3 /usr/bin/python - - yarn install --immutable + - yarn install --immutable || yarn install --immutable depends_on: [] image: node:20.9.0-alpine name: yarn-install @@ -3552,7 +3552,7 @@ steps: name: identify-runner - commands: - apk add --update g++ make python3 && ln -sf /usr/bin/python3 /usr/bin/python - - yarn install --immutable + - yarn install --immutable || yarn install --immutable depends_on: [] image: node:20.9.0-alpine name: yarn-install @@ -4924,6 +4924,6 @@ kind: secret name: gcr_credentials --- kind: signature -hmac: bce7b2ac72349019ec0c2ffbdca13c0591110ee53a3bfc72261df0ed05ca025a +hmac: 0d5e600881f5a8f4294cafd07f450f92f4088e1cda7254d111a635bf01f07465 ... diff --git a/scripts/drone/steps/lib.star b/scripts/drone/steps/lib.star index 9f24661913..9a010c61f2 100644 --- a/scripts/drone/steps/lib.star +++ b/scripts/drone/steps/lib.star @@ -37,7 +37,7 @@ def yarn_install_step(): "commands": [ # Python is needed to build `esfx`, which is needed by `msagl` "apk add --update g++ make python3 && ln -sf /usr/bin/python3 /usr/bin/python", - "yarn install --immutable", + "yarn install --immutable || yarn install --immutable", ], "depends_on": [], } From 57114a4916a1d61e6649a4748fcd3ee5de3532f1 Mon Sep 17 00:00:00 2001 From: brendamuir <100768211+brendamuir@users.noreply.github.com> Date: Fri, 23 Feb 2024 17:52:19 +0100 Subject: [PATCH 0138/1406] Alerting docs: rework create alert rules definition and topic (#83220) * Alerting docs: rework create alert rules definition and topic * ran prettier * corrects note * adds configure notifications section * update branch * corrects links * parts of alert rule creation * Update docs/sources/alerting/configure-notifications/_index.md Co-authored-by: Pepe Cano <825430+ppcano@users.noreply.github.com> * moved section * updates recording rule steps * gets rid of configure integrations topic * deletes configure integrations topic * deletes links * ran prettier * Include contact point links * phase 2 sorting out manage notifications * manage notification changes * manage notification updates * finishing touches * fixes links * link fixes * more link fix * link fixes * ran prettier --------- Signed-off-by: Jack Baldry Co-authored-by: Pepe Cano <825430+ppcano@users.noreply.github.com> Co-authored-by: Jack Baldry --- .../sources/alerting/alerting-rules/_index.md | 52 +++++++-------- ...reate-mimir-loki-managed-recording-rule.md | 16 +++-- .../integrations/_index.md | 50 -------------- .../configure-notifications/_index.md | 26 ++++++++ .../create-notification-policy.md | 3 +- .../create-silence.md | 3 +- .../manage-contact-points/_index.md | 65 +++++++++++++++++++ .../integrations/configure-oncall.md | 4 +- .../integrations/pager-duty.md | 0 .../integrations/webhook-notifier.md | 0 .../mute-timings.md | 3 +- .../template-notifications/_index.md | 16 +++-- .../create-notification-templates.md | 2 + .../images-in-notifications.md | 14 ++-- .../template-notifications/reference.md | 2 + .../use-notification-templates.md | 10 +-- .../using-go-templating-language.md | 14 ++-- .../alerting/manage-notifications/_index.md | 41 +++--------- .../manage-contact-points.md | 34 ---------- docs/sources/alerting/monitor/_index.md | 2 +- .../setup-grafana/configure-grafana/_index.md | 2 +- 21 files changed, 171 insertions(+), 188 deletions(-) delete mode 100644 docs/sources/alerting/alerting-rules/manage-contact-points/integrations/_index.md create mode 100644 docs/sources/alerting/configure-notifications/_index.md rename docs/sources/alerting/{alerting-rules => configure-notifications}/create-notification-policy.md (99%) rename docs/sources/alerting/{manage-notifications => configure-notifications}/create-silence.md (98%) rename docs/sources/alerting/{alerting-rules => configure-notifications}/manage-contact-points/_index.md (51%) rename docs/sources/alerting/{alerting-rules => configure-notifications}/manage-contact-points/integrations/configure-oncall.md (95%) rename docs/sources/alerting/{alerting-rules => configure-notifications}/manage-contact-points/integrations/pager-duty.md (100%) rename docs/sources/alerting/{alerting-rules => configure-notifications}/manage-contact-points/integrations/webhook-notifier.md (100%) rename docs/sources/alerting/{manage-notifications => configure-notifications}/mute-timings.md (98%) rename docs/sources/alerting/{manage-notifications => configure-notifications}/template-notifications/_index.md (78%) rename docs/sources/alerting/{manage-notifications => configure-notifications}/template-notifications/create-notification-templates.md (98%) rename docs/sources/alerting/{manage-notifications => configure-notifications/template-notifications}/images-in-notifications.md (96%) rename docs/sources/alerting/{manage-notifications => configure-notifications}/template-notifications/reference.md (98%) rename docs/sources/alerting/{manage-notifications => configure-notifications}/template-notifications/use-notification-templates.md (72%) rename docs/sources/alerting/{manage-notifications => configure-notifications}/template-notifications/using-go-templating-language.md (93%) delete mode 100644 docs/sources/alerting/manage-notifications/manage-contact-points.md diff --git a/docs/sources/alerting/alerting-rules/_index.md b/docs/sources/alerting/alerting-rules/_index.md index 3063d36929..1d7c8722d9 100644 --- a/docs/sources/alerting/alerting-rules/_index.md +++ b/docs/sources/alerting/alerting-rules/_index.md @@ -5,54 +5,46 @@ aliases: - unified-alerting/alerting-rules/ - ./create-alerts/ canonical: https://grafana.com/docs/grafana/latest/alerting/alerting-rules/ -description: Configure the features and integrations you need to create and manage your alerts +description: Create and manage alert rules labels: products: - cloud - enterprise - oss -menuTitle: Configure -title: Configure Alerting +menuTitle: Create and manage alert rules +title: Create and manage alert rules weight: 120 --- -# Configure Alerting +# Create and manage alert rules -Configure the features and integrations that you need to create and manage your alerts. +An alert rule consists of one or more queries and expressions that select the data you want to measure. It also contains a condition, which is the threshold that an alert rule must meet or exceed in order to fire. -**Configure alert rules** +Create, manage, view, and adjust alert rules to alert on your metrics data or log entries from multiple data sources — no matter where your data is stored. -[Configure Grafana-managed alert rules][create-grafana-managed-rule]. +The main parts of alert rule creation are: -[Configure data source-managed alert rules][create-mimir-loki-managed-rule] +1. Select your data source +1. Query your data +1. Normalize your data +1. Set your threshold -**Configure recording rules** +**Query, expressions, and alert condition** -_Recording rules are only available for compatible Prometheus or Loki data sources._ +What are you monitoring? How are you measuring it? -For more information, see [Configure recording rules][create-mimir-loki-managed-recording-rule]. +{{< admonition type="note" >}} +Expressions can only be used for Grafana-managed alert rules. +{{< /admonition >}} -**Configure contact points** +**Evaluation** -For information on how to configure contact points, see [Configure contact points][manage-contact-points]. +How do you want your alert to be evaluated? -**Configure notification policies** +**Labels and notifications** -For information on how to configure notification policies, see [Configure notification policies][create-notification-policy]. +How do you want to route your alert? What kind of additional labels could you add to annotate your alert rules and ease searching? -{{% docs/reference %}} -[create-mimir-loki-managed-rule]: "/docs/grafana/ -> /docs/grafana//alerting/alerting-rules/create-mimir-loki-managed-rule" -[create-mimir-loki-managed-rule]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/alerting-rules/create-mimir-loki-managed-rule" +**Annotations** -[create-mimir-loki-managed-recording-rule]: "/docs/grafana/ -> /docs/grafana//alerting/alerting-rules/create-mimir-loki-managed-recording-rule" -[create-mimir-loki-managed-recording-rule]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/alerting-rules/create-mimir-loki-managed-recording-rule" - -[create-grafana-managed-rule]: "/docs/grafana/ -> /docs/grafana//alerting/alerting-rules/create-grafana-managed-rule" -[create-grafana-managed-rule]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/alerting-rules/create-grafana-managed-rule" - -[manage-contact-points]: "/docs/grafana/ -> /docs/grafana//alerting/alerting-rules/manage-contact-points" -[manage-contact-points]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/alerting-rules/manage-contact-points" - -[create-notification-policy]: "/docs/grafana/ -> /docs/grafana//alerting/alerting-rules/create-notification-policy" -[create-notification-policy]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/alerting-rules/create-notification-policy" -{{% /docs/reference %}} +Do you want to add more context on the alert in your notification messages, for example, what caused the alert to fire? Which server did it happen on? diff --git a/docs/sources/alerting/alerting-rules/create-mimir-loki-managed-recording-rule.md b/docs/sources/alerting/alerting-rules/create-mimir-loki-managed-recording-rule.md index 08fbbbc492..637cbf3ca4 100644 --- a/docs/sources/alerting/alerting-rules/create-mimir-loki-managed-recording-rule.md +++ b/docs/sources/alerting/alerting-rules/create-mimir-loki-managed-recording-rule.md @@ -3,7 +3,7 @@ aliases: - ../unified-alerting/alerting-rules/create-cortex-loki-managed-recording-rule/ - ../unified-alerting/alerting-rules/create-mimir-loki-managed-recording-rule/ canonical: https://grafana.com/docs/grafana/latest/alerting/alerting-rules/create-mimir-loki-managed-recording-rule/ -description: Configure recording rules for an external Grafana Mimir or Loki instance +description: Create recording rules for an external Grafana Mimir or Loki instance keywords: - grafana - alerting @@ -16,13 +16,14 @@ labels: - cloud - enterprise - oss -title: Configure recording rules +title: Create recording rules weight: 300 --- -# Configure recording rules +# Create recording rules -You can create and manage recording rules for an external Grafana Mimir or Loki instance. Recording rules calculate frequently needed expressions or computationally expensive expressions in advance and save the result as a new set of time series. Querying this new time series is faster, especially for dashboards since they query the same expression every time the dashboards refresh. +You can create and manage recording rules for an external Grafana Mimir or Loki instance. +Recording rules calculate frequently needed expressions or computationally expensive expressions in advance and save the result as a new set of time series. Querying this new time series is faster, especially for dashboards since they query the same expression every time the dashboards refresh. **Note:** @@ -48,13 +49,14 @@ To create recording rules, follow these steps. 1. Click **Alerts & IRM** -> **Alerting** -> **Alert rules**. -1. Click **New recording rule**. +1. Select **Rule type** -> **Recording**. +1. Click **+New recording rule**. -1. Set rule name. +1. Enter recording rule name. The recording rule name must be a Prometheus metric name and contain no whitespace. -1. Define query. +1. Define recording rule. - Select your Loki or Prometheus data source. - Enter a query. 1. Add namespace and group. diff --git a/docs/sources/alerting/alerting-rules/manage-contact-points/integrations/_index.md b/docs/sources/alerting/alerting-rules/manage-contact-points/integrations/_index.md deleted file mode 100644 index dfd1efd3c6..0000000000 --- a/docs/sources/alerting/alerting-rules/manage-contact-points/integrations/_index.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -aliases: - - alerting/manage-notifications/manage-contact-points/configure-integrations/ -canonical: https://grafana.com/docs/grafana/latest/alerting/alerting-rules/manage-contact-points/integrations/ -description: Configure contact point integrations to select your preferred communication channels for receiving notifications of firing alerts. -keywords: - - Grafana - - alerting - - guide - - notifications - - integrations - - contact points -labels: - products: - - cloud - - enterprise - - oss -title: Configure contact point integrations -weight: 100 ---- - -# Configure contact point integrations - -Configure contact point integrations in Grafana to select your preferred communication channel for receiving notifications when your alert rules are firing. Each integration has its own configuration options and setup process. In most cases, this involves providing an API key or a Webhook URL. - -Once configured, you can use integrations as part of your contact points to receive notifications whenever your alert changes its state. In this section, we'll cover the basic steps to configure your integrations, so you can start receiving real-time alerts and stay on top of your monitoring data. - -## List of supported integrations - -| Name | Type | -| ----------------------- | ------------------------- | -| DingDing | `dingding` | -| Discord | `discord` | -| Email | `email` | -| Google Chat | `googlechat` | -| Hipchat | `hipchat` | -| Kafka | `kafka` | -| Line | `line` | -| Microsoft Teams | `teams` | -| Opsgenie | `opsgenie` | -| Pagerduty | `pagerduty` | -| Prometheus Alertmanager | `prometheus-alertmanager` | -| Pushover | `pushover` | -| Sensu | `sensu` | -| Sensu Go | `sensugo` | -| Slack | `slack` | -| Telegram | `telegram` | -| Threema | `threema` | -| VictorOps | `victorops` | -| Webhook | `webhook` | diff --git a/docs/sources/alerting/configure-notifications/_index.md b/docs/sources/alerting/configure-notifications/_index.md new file mode 100644 index 0000000000..f709efed8f --- /dev/null +++ b/docs/sources/alerting/configure-notifications/_index.md @@ -0,0 +1,26 @@ +--- +canonical: https://grafana.com/docs/grafana/latest/alerting/configure-notifications +description: Configure how, when, and where to send your alert notifications +keywords: + - grafana + - alert + - notifications +labels: + products: + - cloud + - enterprise + - oss +menuTitle: Configure notifications +title: Configure notifications +weight: 125 +--- + +# Configure notifications + +Choose how, when, and where to send your alert notifications. + +As a first step, define your contact points; where to send your alert notifications to. A contact point is a set of one or more integrations that are used to deliver notifications. + +Next, create a notification policy which is a set of rules for where, when and how your alerts are routed to contact points. In a notification policy, you define where to send your alert notifications by choosing one of the contact points you created. + +Optionally, you can add notification templates to contact points for reuse and consistent messaging in your notifications. diff --git a/docs/sources/alerting/alerting-rules/create-notification-policy.md b/docs/sources/alerting/configure-notifications/create-notification-policy.md similarity index 99% rename from docs/sources/alerting/alerting-rules/create-notification-policy.md rename to docs/sources/alerting/configure-notifications/create-notification-policy.md index 3df48d66ae..72cfcc9f70 100644 --- a/docs/sources/alerting/alerting-rules/create-notification-policy.md +++ b/docs/sources/alerting/configure-notifications/create-notification-policy.md @@ -3,6 +3,7 @@ aliases: - ../notifications/ - ../old-alerting/notifications/ - ../unified-alerting/notifications/ + - ./alerting-rules/ canonical: https://grafana.com/docs/grafana/latest/alerting/alerting-rules/create-notification-policy/ description: Configure notification policies to determine how alerts are routed to contact points keywords: @@ -17,7 +18,7 @@ labels: - enterprise - oss title: Configure notification policies -weight: 420 +weight: 430 --- # Configure notification policies diff --git a/docs/sources/alerting/manage-notifications/create-silence.md b/docs/sources/alerting/configure-notifications/create-silence.md similarity index 98% rename from docs/sources/alerting/manage-notifications/create-silence.md rename to docs/sources/alerting/configure-notifications/create-silence.md index 3b0d544360..9d361832b4 100644 --- a/docs/sources/alerting/manage-notifications/create-silence.md +++ b/docs/sources/alerting/configure-notifications/create-silence.md @@ -6,6 +6,7 @@ aliases: - ../silences/remove-silence/ - ../unified-alerting/silences/ - ../silences/ + - ./alerting/manage-notifications/create-silence/ canonical: https://grafana.com/docs/grafana/latest/alerting/manage-notifications/create-silence/ description: Create silences to stop notifications from getting created for a specified window of time keywords: @@ -19,7 +20,7 @@ labels: - enterprise - oss title: Manage silences -weight: 410 +weight: 440 --- # Manage silences diff --git a/docs/sources/alerting/alerting-rules/manage-contact-points/_index.md b/docs/sources/alerting/configure-notifications/manage-contact-points/_index.md similarity index 51% rename from docs/sources/alerting/alerting-rules/manage-contact-points/_index.md rename to docs/sources/alerting/configure-notifications/manage-contact-points/_index.md index 186c487914..1f7fa7c184 100644 --- a/docs/sources/alerting/alerting-rules/manage-contact-points/_index.md +++ b/docs/sources/alerting/configure-notifications/manage-contact-points/_index.md @@ -7,6 +7,10 @@ aliases: - ../contact-points/test-contact-point/ # /docs/grafana//alerting/contact-points/test-contact-point/ - ../manage-notifications/manage-contact-points/ # /docs/grafana//alerting/manage-notifications/manage-contact-points/ - create-contact-point/ # /docs/grafana//alerting/alerting-rules/create-contact-point/ + - ./alerting-rules/ + - ./alerting-rules/manage-notifications/manage-contact-points/ + - alerting/manage-notifications/manage-contact-points/configure-integrations/ + - ./alerting-rules/manage-contact-points/ canonical: https://grafana.com/docs/grafana/latest/alerting/alerting-rules/manage-contact-points/ description: Configure contact points to define how your contacts are notified when an alert rule fires keywords: @@ -28,6 +32,8 @@ weight: 410 Use contact points to define how your contacts are notified when an alert rule fires. You can add, edit, delete, and test a contact point. +Configure contact point integrations in Grafana to select your preferred communication channel for receiving notifications when your alert rules are firing. + ## Add a contact point Complete the following steps to add a contact point. @@ -75,3 +81,62 @@ Complete the following steps to test a contact point. 1. Click **Test** to open the contact point testing modal. 1. Choose whether to send a predefined test notification or choose custom to add your own custom annotations and labels to include in the notification. 1. Click **Send test notification** to fire the alert. + +## Manage contact points + +The Contact points list view lists all existing contact points and notification templates. + +On the **Contact Points** tab, you can: + +- Search for name and type of contact points and integrations +- View all existing contact points and integrations +- View how many notification policies each contact point is being used for and navigate directly to the linked notification policies +- View the status of notification deliveries +- Export individual contact points or all contact points in JSON, YAML, or Terraform format +- Delete contact points that are not in use by a notification policy + +On the **Notification templates** tab, you can: + +- View, edit, copy or delete existing notification templates + +## Configure contact point integrations + +Each contact point integration has its own configuration options and setup process. In most cases, this involves providing an API key or a Webhook URL. + +Once configured, you can use integrations as part of your contact points to receive notifications whenever your alert changes its state. In this section, we'll cover the basic steps to configure your integrations, so you can start receiving real-time alerts and stay on top of your monitoring data. + +## List of supported integrations + +| Name | Type | +| ------------------------ | ------------------------- | +| DingDing | `dingding` | +| Discord | `discord` | +| Email | `email` | +| Google Chat | `googlechat` | +| [Grafana Oncall][oncall] | `oncall` | +| Hipchat | `hipchat` | +| Kafka | `kafka` | +| Line | `line` | +| Microsoft Teams | `teams` | +| Opsgenie | `opsgenie` | +| [Pagerduty][pagerduty] | `pagerduty` | +| Prometheus Alertmanager | `prometheus-alertmanager` | +| Pushover | `pushover` | +| Sensu | `sensu` | +| Sensu Go | `sensugo` | +| Slack | `slack` | +| Telegram | `telegram` | +| Threema | `threema` | +| VictorOps | `victorops` | +| [Webhook][webhook] | `webhook` | + +{{% docs/reference %}} +[pagerduty]: "/docs/grafana/ -> /docs/grafana//alerting/configure-notifications/manage-contact-points/integrations/pager-duty" +[pagerduty]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/manage-contact-points/integrations/pager-duty" + +[oncall]: "/docs/grafana/ -> /docs/grafana//alerting/configure-notifications/manage-contact-points/integrations/configure-oncall" +[oncall]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/manage-contact-points/integrations/configure-oncall" + +[webhook]: "/docs/grafana/ -> /docs/grafana//alerting/configure-notifications/manage-contact-points/integrations/webhook-notifier" +[webhook]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/manage-contact-points/integrations/webhook-notifier" +{{% /docs/reference %}} diff --git a/docs/sources/alerting/alerting-rules/manage-contact-points/integrations/configure-oncall.md b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-oncall.md similarity index 95% rename from docs/sources/alerting/alerting-rules/manage-contact-points/integrations/configure-oncall.md rename to docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-oncall.md index f71e505303..bf3fa657a0 100644 --- a/docs/sources/alerting/alerting-rules/manage-contact-points/integrations/configure-oncall.md +++ b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-oncall.md @@ -63,8 +63,8 @@ To set up the Grafana OnCall integration using the Grafana Alerting application, This redirects you to the Grafana OnCall integration page in the Grafana OnCall application. From there, you can add [routes and escalation chains][escalation-chain]. {{% docs/reference %}} -[create-notification-policy]: "/docs/grafana/ -> /docs/grafana//alerting/alerting-rules/create-notification-policy" -[create-notification-policy]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/alerting-rules/create-notification-policy" +[create-notification-policy]: "/docs/grafana/ -> /docs/grafana//alerting/configure-notifications/create-notification-policy" +[create-notification-policy]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/create-notification-policy" [oncall-integration]: "/docs/grafana/ -> /docs/oncall/latest/integrations/grafana-alerting" [oncall-integration]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/oncall/integrations/grafana-alerting" diff --git a/docs/sources/alerting/alerting-rules/manage-contact-points/integrations/pager-duty.md b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/pager-duty.md similarity index 100% rename from docs/sources/alerting/alerting-rules/manage-contact-points/integrations/pager-duty.md rename to docs/sources/alerting/configure-notifications/manage-contact-points/integrations/pager-duty.md diff --git a/docs/sources/alerting/alerting-rules/manage-contact-points/integrations/webhook-notifier.md b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/webhook-notifier.md similarity index 100% rename from docs/sources/alerting/alerting-rules/manage-contact-points/integrations/webhook-notifier.md rename to docs/sources/alerting/configure-notifications/manage-contact-points/integrations/webhook-notifier.md diff --git a/docs/sources/alerting/manage-notifications/mute-timings.md b/docs/sources/alerting/configure-notifications/mute-timings.md similarity index 98% rename from docs/sources/alerting/manage-notifications/mute-timings.md rename to docs/sources/alerting/configure-notifications/mute-timings.md index e3337f7877..7e6d20154f 100644 --- a/docs/sources/alerting/manage-notifications/mute-timings.md +++ b/docs/sources/alerting/configure-notifications/mute-timings.md @@ -2,6 +2,7 @@ aliases: - ../notifications/mute-timings/ - ../unified-alerting/notifications/mute-timings/ + - ./alerting/manage-notifications/mute-timings/ canonical: https://grafana.com/docs/grafana/latest/alerting/manage-notifications/mute-timings/ description: Create mute timings to prevent alerts from firing during a specific and reoccurring period of time keywords: @@ -17,7 +18,7 @@ labels: - enterprise - oss title: Create mute timings -weight: 420 +weight: 450 --- # Create mute timings diff --git a/docs/sources/alerting/manage-notifications/template-notifications/_index.md b/docs/sources/alerting/configure-notifications/template-notifications/_index.md similarity index 78% rename from docs/sources/alerting/manage-notifications/template-notifications/_index.md rename to docs/sources/alerting/configure-notifications/template-notifications/_index.md index 1a5507f577..00ac7e8ace 100644 --- a/docs/sources/alerting/manage-notifications/template-notifications/_index.md +++ b/docs/sources/alerting/configure-notifications/template-notifications/_index.md @@ -1,4 +1,6 @@ --- +aliases: + - ./alerting-rules/manage-notifications/manage-contact-points/template-notifications/ canonical: https://grafana.com/docs/grafana/latest/alerting/manage-notifications/template-notifications/ description: Customize your notifications using notification templates keywords: @@ -12,7 +14,7 @@ labels: - enterprise - oss title: Customize notifications -weight: 400 +weight: 420 --- # Customize notifications @@ -52,12 +54,12 @@ Use notification templates to send notifications to your contact points. Data that is available when writing templates. {{% docs/reference %}} -[reference]: "/docs/grafana/ -> /docs/grafana//alerting/manage-notifications/template-notifications/reference" -[reference]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/manage-notifications/template-notifications/reference" +[reference]: "/docs/grafana/ -> /docs/grafana//alerting/configure-notifications/template-notifications/reference" +[reference]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/template-notifications/reference" -[use-notification-templates]: "/docs/grafana/ -> /docs/grafana//alerting/manage-notifications/template-notifications/use-notification-templates" -[use-notification-templates]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/manage-notifications/template-notifications/use-notification-templates" +[use-notification-templates]: "/docs/grafana/ -> /docs/grafana//alerting/configure-notifications/template-notifications/use-notification-templates" +[use-notification-templates]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/template-notifications/use-notification-templates" -[using-go-templating-language]: "/docs/grafana/ -> /docs/grafana//alerting/manage-notifications/template-notifications/using-go-templating-language" -[using-go-templating-language]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/manage-notifications/template-notifications/using-go-templating-language" +[using-go-templating-language]: "/docs/grafana/ -> /docs/grafana//alerting/configure-notifications/template-notifications/using-go-templating-language" +[using-go-templating-language]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/template-notifications/using-go-templating-language" {{% /docs/reference %}} diff --git a/docs/sources/alerting/manage-notifications/template-notifications/create-notification-templates.md b/docs/sources/alerting/configure-notifications/template-notifications/create-notification-templates.md similarity index 98% rename from docs/sources/alerting/manage-notifications/template-notifications/create-notification-templates.md rename to docs/sources/alerting/configure-notifications/template-notifications/create-notification-templates.md index 947556e4e2..d0b333c14d 100644 --- a/docs/sources/alerting/manage-notifications/template-notifications/create-notification-templates.md +++ b/docs/sources/alerting/configure-notifications/template-notifications/create-notification-templates.md @@ -1,4 +1,6 @@ --- +aliases: + - ./alerting-rules/manage-notifications/manage-contact-points/template-notifications/create-notification-templates/ canonical: https://grafana.com/docs/grafana/latest/alerting/manage-notifications/template-notifications/create-notification-templates/ description: Create notification templates to sent to your contact points keywords: diff --git a/docs/sources/alerting/manage-notifications/images-in-notifications.md b/docs/sources/alerting/configure-notifications/template-notifications/images-in-notifications.md similarity index 96% rename from docs/sources/alerting/manage-notifications/images-in-notifications.md rename to docs/sources/alerting/configure-notifications/template-notifications/images-in-notifications.md index 89807f1c8b..ce7806e087 100644 --- a/docs/sources/alerting/manage-notifications/images-in-notifications.md +++ b/docs/sources/alerting/configure-notifications/template-notifications/images-in-notifications.md @@ -1,4 +1,6 @@ --- +aliases: + - ./alerting-rules/manage-notifications/manage-contact-points/template-notifications/images-in-notifications/ canonical: https://grafana.com/docs/grafana/latest/alerting/manage-notifications/images-in-notifications/ description: Use images in notifications to help users better understand why alerts are firing or have been resolved keywords: @@ -12,7 +14,7 @@ labels: - enterprise - oss title: Use images in notifications -weight: 405 +weight: 500 --- # Use images in notifications @@ -33,7 +35,7 @@ Refer to the table at the end of this page for a list of contact points and thei ## Requirements -1. To use images in notifications, Grafana must be set up to use [image rendering][image-rendering]. You can either install the image rendering plugin or run it as a remote rendering service. +1. To use images in notifications, Grafana must be set up to use 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][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. @@ -69,8 +71,6 @@ If screenshots should be uploaded to cloud storage then `upload_external_image_s # will be persisted to disk for up to temp_data_lifetime. upload_external_image_storage = false -For more information on image rendering, refer to [image rendering][image-rendering]. - Restart Grafana for the changes to take effect. ## Advanced configuration @@ -137,9 +137,3 @@ For example, if a screenshot could not be taken within the expected time (10 sec - `grafana_screenshot_successes_total` - `grafana_screenshot_upload_failures_total` - `grafana_screenshot_upload_successes_total` - -{{% docs/reference %}} -[image-rendering]: "/docs/ -> /docs/grafana//setup-grafana/image-rendering" - -[paths]: "/docs/ -> /docs/grafana//setup-grafana/configure-grafana#paths" -{{% /docs/reference %}} diff --git a/docs/sources/alerting/manage-notifications/template-notifications/reference.md b/docs/sources/alerting/configure-notifications/template-notifications/reference.md similarity index 98% rename from docs/sources/alerting/manage-notifications/template-notifications/reference.md rename to docs/sources/alerting/configure-notifications/template-notifications/reference.md index 16c26acc6d..dc0dee7f18 100644 --- a/docs/sources/alerting/manage-notifications/template-notifications/reference.md +++ b/docs/sources/alerting/configure-notifications/template-notifications/reference.md @@ -1,4 +1,6 @@ --- +aliases: + - ./alerting-rules/manage-notifications/manage-contact-points/template-notifications/reference/ canonical: https://grafana.com/docs/grafana/latest/alerting/manage-notifications/template-notifications/reference/ description: Learn about templating notifications options keywords: diff --git a/docs/sources/alerting/manage-notifications/template-notifications/use-notification-templates.md b/docs/sources/alerting/configure-notifications/template-notifications/use-notification-templates.md similarity index 72% rename from docs/sources/alerting/manage-notifications/template-notifications/use-notification-templates.md rename to docs/sources/alerting/configure-notifications/template-notifications/use-notification-templates.md index a2a341e02c..24ae3a2b56 100644 --- a/docs/sources/alerting/manage-notifications/template-notifications/use-notification-templates.md +++ b/docs/sources/alerting/configure-notifications/template-notifications/use-notification-templates.md @@ -1,4 +1,6 @@ --- +aliases: + - ./alerting-rules/manage-notifications/manage-contact-points/template-notifications/use-notification-templates/ canonical: https://grafana.com/docs/grafana/latest/alerting/manage-notifications/template-notifications/use-notification-templates/ description: Use notification templates in contact points to customize your notifications keywords: @@ -35,9 +37,9 @@ In the Contact points tab, you can see a list of your contact points. 1. Click **Save contact point**. {{% docs/reference %}} -[create-notification-templates]: "/docs/grafana/ -> /docs/grafana//alerting/manage-notifications/template-notifications/create-notification-templates" -[create-notification-templates]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/manage-notifications/template-notifications/create-notification-templates" +[create-notification-templates]: "/docs/grafana/ -> /docs/grafana//alerting/configure-notifications/template-notifications/create-notification-templates" +[create-notification-templates]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/template-notifications/create-notification-templates" -[using-go-templating-language]: "/docs/grafana/ -> /docs/grafana//alerting/manage-notifications/template-notifications/using-go-templating-language" -[using-go-templating-language]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/manage-notifications/template-notifications/using-go-templating-language" +[using-go-templating-language]: "/docs/grafana/ -> /docs/grafana//alerting/configure-notifications/template-notifications/using-go-templating-language" +[using-go-templating-language]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/template-notifications/using-go-templating-language" {{% /docs/reference %}} diff --git a/docs/sources/alerting/manage-notifications/template-notifications/using-go-templating-language.md b/docs/sources/alerting/configure-notifications/template-notifications/using-go-templating-language.md similarity index 93% rename from docs/sources/alerting/manage-notifications/template-notifications/using-go-templating-language.md rename to docs/sources/alerting/configure-notifications/template-notifications/using-go-templating-language.md index 50115d3f23..9831bc08b2 100644 --- a/docs/sources/alerting/manage-notifications/template-notifications/using-go-templating-language.md +++ b/docs/sources/alerting/configure-notifications/template-notifications/using-go-templating-language.md @@ -1,4 +1,6 @@ --- +aliases: + - ./alerting-rules/manage-notifications/manage-contact-points/template-notifications/using-go-templating-language/ canonical: https://grafana.com/docs/grafana/latest/alerting/manage-notifications/template-notifications/using-go-templating-language/ description: Use Go's templating language to create your own notification templates keywords: @@ -280,12 +282,12 @@ grafana_folder = "Test alerts" ``` {{% docs/reference %}} -[create-notification-templates]: "/docs/grafana/ -> /docs/grafana//alerting/manage-notifications/template-notifications/create-notification-templates" -[create-notification-templates]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/manage-notifications/template-notifications/create-notification-templates" +[create-notification-templates]: "/docs/grafana/ -> /docs/grafana//alerting/configure-notifications/template-notifications/create-notification-templates" +[create-notification-templates]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/template-notifications/create-notification-templates" -[extendeddata]: "/docs/grafana/ -> /docs/grafana//alerting/manage-notifications/template-notifications/reference#extendeddata" -[extendeddata]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/manage-notifications/template-notifications/reference#extendeddata" +[extendeddata]: "/docs/grafana/ -> /docs/grafana//alerting/configure-notifications/template-notifications/reference#extendeddata" +[extendeddata]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/template-notifications/reference#extendeddata" -[reference]: "/docs/grafana/ -> /docs/grafana//alerting/manage-notifications/template-notifications/reference" -[reference]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/manage-notifications/template-notifications/reference" +[reference]: "/docs/grafana/ -> /docs/grafana//alerting/configure-notifications/template-notifications/reference" +[reference]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/template-notifications/reference" {{% /docs/reference %}} diff --git a/docs/sources/alerting/manage-notifications/_index.md b/docs/sources/alerting/manage-notifications/_index.md index 1ea9bc2492..5080cb24d2 100644 --- a/docs/sources/alerting/manage-notifications/_index.md +++ b/docs/sources/alerting/manage-notifications/_index.md @@ -1,47 +1,22 @@ --- canonical: https://grafana.com/docs/grafana/latest/alerting/manage-notifications/ -description: Manage your alerts by creating silences, mute timings, and more +description: Detect and respond for day-to-day triage and analysis of what’s going on and action you need to take keywords: - grafana - - alert - - notifications + - detect + - respont labels: products: - cloud - enterprise - oss -menuTitle: Manage -title: Manage your alerts +menuTitle: Detect and respond +title: Detect and respond weight: 130 --- -# Manage your alerts +# Detect and respond -Once you have set up your alert rules, contact points, and notification policies, you can use Grafana Alerting to: +Use Grafana Alerting to track and generate alerts and send notifications, providing an efficient way for engineers to monitor, respond, and triage issues within their services. -[Create silences][create-silence] - -[Create mute timings][mute-timings] - -[Declare incidents from firing alerts][declare-incident-from-firing-alert] - -[View the state and health of alert rules][view-state-health] - -[View and filter alert rules][view-alert-rules] - -{{% docs/reference %}} -[create-silence]: "/docs/grafana/ -> /docs/grafana//alerting/manage-notifications/create-silence" -[create-silence]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/manage-notifications/create-silence" - -[declare-incident-from-firing-alert]: "/docs/grafana/ -> /docs/grafana//alerting/manage-notifications/declare-incident-from-alert" -[declare-incident-from-firing-alert]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/manage-notifications/declare-incident-from-alert" - -[mute-timings]: "/docs/grafana/ -> /docs/grafana//alerting/manage-notifications/mute-timings" -[mute-timings]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/manage-notifications/mute-timings" - -[view-alert-rules]: "/docs/grafana/ -> /docs/grafana//alerting/manage-notifications/view-alert-rules" -[view-alert-rules]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/manage-notifications/view-alert-rules" - -[view-state-health]: "/docs/grafana/ -> /docs/grafana//alerting/manage-notifications/view-state-health" -[view-state-health]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/manage-notifications/view-state-health" -{{% /docs/reference %}} +Alerts and alert notifications provide a lot of value as key indicators to issues during the triage process, providing engineers with the information they need to understand what is going on in their system or service. diff --git a/docs/sources/alerting/manage-notifications/manage-contact-points.md b/docs/sources/alerting/manage-notifications/manage-contact-points.md deleted file mode 100644 index bbfd98e4b1..0000000000 --- a/docs/sources/alerting/manage-notifications/manage-contact-points.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -canonical: https://grafana.com/docs/grafana/latest/alerting/manage-notifications/manage-contact-points/ -description: View, edit, copy, or delete your contact points and notification templates -keywords: - - grafana - - alerting - - contact points - - search - - export -labels: - products: - - cloud - - enterprise - - oss -title: Manage contact points -weight: 410 ---- - -# Manage contact points - -The Contact points list view lists all existing contact points and notification templates. - -On the **Contact Points** tab, you can: - -- Search for name and type of contact points and integrations -- View all existing contact points and integrations -- View how many notification policies each contact point is being used for and navigate directly to the linked notification policies -- View the status of notification deliveries -- Export individual contact points or all contact points in JSON, YAML, or Terraform format -- Delete contact points that are not in use by a notification policy - -On the **Notification templates** tab, you can: - -- View, edit, copy or delete existing notification templates diff --git a/docs/sources/alerting/monitor/_index.md b/docs/sources/alerting/monitor/_index.md index 577910d569..6769d67c94 100644 --- a/docs/sources/alerting/monitor/_index.md +++ b/docs/sources/alerting/monitor/_index.md @@ -12,7 +12,7 @@ labels: products: - enterprise - oss -menuTitle: Monitor +menuTitle: Monitor alerting title: Meta monitoring weight: 140 --- diff --git a/docs/sources/setup-grafana/configure-grafana/_index.md b/docs/sources/setup-grafana/configure-grafana/_index.md index bf00cb8c98..b89462f9c5 100644 --- a/docs/sources/setup-grafana/configure-grafana/_index.md +++ b/docs/sources/setup-grafana/configure-grafana/_index.md @@ -1626,7 +1626,7 @@ The interval string is a possibly signed sequence of decimal numbers, followed b ## [unified_alerting.screenshots] -For more information about screenshots, refer to [Images in notifications]({{< relref "../../alerting/manage-notifications/images-in-notifications" >}}). +For more information about screenshots, refer to [Images in notifications]({{< relref "../../alerting/configure-notifications/template-notifications/images-in-notifications" >}}). ### capture From 92fa868a77e91ab517d7ff81ef683a89319a1342 Mon Sep 17 00:00:00 2001 From: Kristina Date: Fri, 23 Feb 2024 10:55:44 -0600 Subject: [PATCH 0139/1406] remove oss from security config docs (#82936) --- .../configure-security/configure-request-security.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/sources/setup-grafana/configure-security/configure-request-security.md b/docs/sources/setup-grafana/configure-security/configure-request-security.md index 9a2fb05cb9..437786ed07 100644 --- a/docs/sources/setup-grafana/configure-security/configure-request-security.md +++ b/docs/sources/setup-grafana/configure-security/configure-request-security.md @@ -8,7 +8,6 @@ labels: products: - cloud - enterprise - - oss title: Configure request security weight: 1100 --- From a97562906c350e434ca07019ab1f5d2a7e2d646d Mon Sep 17 00:00:00 2001 From: Jev Forsberg <46619047+baldm0mma@users.noreply.github.com> Date: Fri, 23 Feb 2024 10:00:24 -0700 Subject: [PATCH 0140/1406] Table: Add ability for Table to render Standard Options "No value" value when DataFrames or field values are empty (#82948) * baldm0mma/no_value_message/ add fieldConfig to Table props * baldm0mma/no_value_message/ add noValuesDisplayText to table props * baldm0mma/no_value_message/ add fieldConfig to tablePanel * baldm0mma/no_value_message/ add tests * baldm0mma/no_value_message/ update test values * baldm0mma/no_value_message/ update args in tests * baldm0mma/no_value_message/ update with NO_DATA_TEXT const --- .../src/components/Table/Table.test.tsx | 158 +++++++++++------- .../grafana-ui/src/components/Table/Table.tsx | 5 +- .../grafana-ui/src/components/Table/types.ts | 3 +- public/app/plugins/panel/table/TablePanel.tsx | 1 + 4 files changed, 109 insertions(+), 58 deletions(-) diff --git a/packages/grafana-ui/src/components/Table/Table.test.tsx b/packages/grafana-ui/src/components/Table/Table.test.tsx index e6ed28d2a8..1f8c73c142 100644 --- a/packages/grafana-ui/src/components/Table/Table.test.tsx +++ b/packages/grafana-ui/src/components/Table/Table.test.tsx @@ -15,57 +15,66 @@ jest.mock('@floating-ui/react', () => ({ }), })); -function getDefaultDataFrame(): DataFrame { - const dataFrame = toDataFrame({ - name: 'A', - fields: [ - { - name: 'time', - type: FieldType.time, - values: [1609459200000, 1609470000000, 1609462800000, 1609466400000], - config: { - custom: { - filterable: false, - }, +const dataFrameData = { + name: 'A', + fields: [ + { + name: 'time', + type: FieldType.time, + values: [1609459200000, 1609470000000, 1609462800000, 1609466400000], + config: { + custom: { + filterable: false, }, }, - { - name: 'temperature', - type: FieldType.number, - values: [10, NaN, 11, 12], - config: { - custom: { - filterable: false, - }, - links: [ - { - targetBlank: true, - title: 'Value link', - url: '${__value.text}', - }, - ], + }, + { + name: 'temperature', + type: FieldType.number, + values: [10, NaN, 11, 12], + config: { + custom: { + filterable: false, }, - }, - { - name: 'img', - type: FieldType.string, - values: ['data:image/png;base64,1', 'data:image/png;base64,2', 'data:image/png;base64,3'], - config: { - custom: { - filterable: false, - displayMode: 'image', + links: [ + { + targetBlank: true, + title: 'Value link', + url: '${__value.text}', }, - links: [ - { - targetBlank: true, - title: 'Image link', - url: '${__value.text}', - }, - ], + ], + }, + }, + { + name: 'img', + type: FieldType.string, + values: ['data:image/png;base64,1', 'data:image/png;base64,2', 'data:image/png;base64,3'], + config: { + custom: { + filterable: false, + displayMode: 'image', }, + links: [ + { + targetBlank: true, + title: 'Image link', + url: '${__value.text}', + }, + ], }, - ], - }); + }, + ], +}; + +const fullDataFrame = toDataFrame(dataFrameData); + +const emptyValuesDataFrame = toDataFrame({ + ...dataFrameData, + // Remove all values + fields: dataFrameData.fields.map((field) => ({ ...field, values: [] })), +}); + +function getDataFrame(dataFrame: DataFrame): DataFrame { return applyOverrides(dataFrame); } @@ -76,7 +85,7 @@ function applyOverrides(dataFrame: DataFrame) { defaults: {}, overrides: [], }, - replaceVariables: (value, vars, format) => { + replaceVariables: (value, vars, _format) => { return vars && value === '${__value.text}' ? '${__value.text} interpolation' : value; }, timeZone: 'utc', @@ -91,7 +100,7 @@ function getTestContext(propOverrides: Partial = {}) { const onColumnResize = jest.fn(); const props: Props = { ariaLabel: 'aria-label', - data: getDefaultDataFrame(), + data: getDataFrame(fullDataFrame), height: 600, width: 800, onSortByChange, @@ -136,16 +145,53 @@ function getRowsData(rows: HTMLElement[]): Object[] { } describe('Table', () => { - describe('when mounted without data', () => { - it('then no data to show should be displayed', () => { - getTestContext({ data: toDataFrame([]) }); - expect(getTable()).toBeInTheDocument(); - expect(screen.queryByRole('row')).not.toBeInTheDocument(); - expect(screen.getByText(/No data/i)).toBeInTheDocument(); + describe('when mounted with EMPTY data', () => { + describe('and Standard Options `No value` value is NOT set', () => { + it('the default `no data` message should be displayed', () => { + getTestContext({ data: toDataFrame([]) }); + expect(getTable()).toBeInTheDocument(); + expect(screen.queryByRole('row')).not.toBeInTheDocument(); + expect(screen.getByText(/No data/i)).toBeInTheDocument(); + }); + }); + + describe('and Standard Options `No value` value IS set', () => { + it('the `No value` Standard Options message should be displayed', () => { + const noValuesDisplayText = 'All healthy'; + getTestContext({ + data: toDataFrame([]), + fieldConfig: { defaults: { noValue: noValuesDisplayText }, overrides: [] }, + }); + expect(getTable()).toBeInTheDocument(); + expect(screen.queryByRole('row')).not.toBeInTheDocument(); + expect(screen.getByText(noValuesDisplayText)).toBeInTheDocument(); + }); }); }); describe('when mounted with data', () => { + describe('but empty values', () => { + describe('and Standard Options `No value` value is NOT set', () => { + it('the default `no data` message should be displayed', () => { + getTestContext({ data: getDataFrame(emptyValuesDataFrame) }); + expect(getTable()).toBeInTheDocument(); + expect(screen.getByText(/No data/i)).toBeInTheDocument(); + }); + }); + + describe('and Standard Options `No value` value IS set', () => { + it('the `No value` Standard Options message should be displayed', () => { + const noValuesDisplayText = 'All healthy'; + getTestContext({ + data: getDataFrame(emptyValuesDataFrame), + fieldConfig: { defaults: { noValue: noValuesDisplayText }, overrides: [] }, + }); + expect(getTable()).toBeInTheDocument(); + expect(screen.getByText(noValuesDisplayText)).toBeInTheDocument(); + }); + }); + }); + it('then correct rows should be rendered', () => { getTestContext(); expect(getTable()).toBeInTheDocument(); @@ -351,7 +397,7 @@ describe('Table', () => { const onColumnResize = jest.fn(); const props: Props = { ariaLabel: 'aria-label', - data: getDefaultDataFrame(), + data: getDataFrame(fullDataFrame), height: 600, width: 800, onSortByChange, @@ -493,7 +539,7 @@ describe('Table', () => { const onColumnResize = jest.fn(); const props: Props = { ariaLabel: 'aria-label', - data: getDefaultDataFrame(), + data: getDataFrame(fullDataFrame), height: 600, width: 800, onSortByChange, @@ -549,7 +595,7 @@ describe('Table', () => { }) ); - const defaultFrame = getDefaultDataFrame(); + const defaultFrame = getDataFrame(fullDataFrame); getTestContext({ data: applyOverrides({ diff --git a/packages/grafana-ui/src/components/Table/Table.tsx b/packages/grafana-ui/src/components/Table/Table.tsx index 774856991c..b418401bc9 100644 --- a/packages/grafana-ui/src/components/Table/Table.tsx +++ b/packages/grafana-ui/src/components/Table/Table.tsx @@ -29,6 +29,7 @@ import { getColumns, sortCaseInsensitive, sortNumber, getFooterItems, createFoot const COLUMN_MIN_WIDTH = 150; const FOOTER_ROW_HEIGHT = 36; +const NO_DATA_TEXT = 'No data'; export const Table = memo((props: Props) => { const { @@ -49,6 +50,7 @@ export const Table = memo((props: Props) => { timeRange, enableSharedCrosshair = false, initialRowIndex = undefined, + fieldConfig, } = props; const listRef = useRef(null); @@ -58,6 +60,7 @@ export const Table = memo((props: Props) => { const tableStyles = useTableStyles(theme, cellHeight); const headerHeight = noHeader ? 0 : tableStyles.rowHeight; const [footerItems, setFooterItems] = useState(footerValues); + const noValuesDisplayText = fieldConfig?.defaults?.noValue ?? NO_DATA_TEXT; const footerHeight = useMemo(() => { const EXTENDED_ROW_HEIGHT = FOOTER_ROW_HEIGHT; @@ -324,7 +327,7 @@ export const Table = memo((props: Props) => {
) : (
- No data + {noValuesDisplayText}
)} {footerItems && ( diff --git a/packages/grafana-ui/src/components/Table/types.ts b/packages/grafana-ui/src/components/Table/types.ts index 62a6d1471e..b3d1f9f3be 100644 --- a/packages/grafana-ui/src/components/Table/types.ts +++ b/packages/grafana-ui/src/components/Table/types.ts @@ -2,7 +2,7 @@ import { Property } from 'csstype'; import { FC } from 'react'; import { CellProps, Column, Row, TableState, UseExpandedRowProps } from 'react-table'; -import { DataFrame, Field, KeyValue, SelectableValue, TimeRange } from '@grafana/data'; +import { DataFrame, Field, KeyValue, SelectableValue, TimeRange, FieldConfigSource } from '@grafana/data'; import * as schema from '@grafana/schema'; import { TableStyles } from './styles'; @@ -99,6 +99,7 @@ export interface Props { enableSharedCrosshair?: boolean; // The index of the field value that the table will initialize scrolled to initialRowIndex?: number; + fieldConfig?: FieldConfigSource; } /** diff --git a/public/app/plugins/panel/table/TablePanel.tsx b/public/app/plugins/panel/table/TablePanel.tsx index 15df95afde..e3d9d9f774 100644 --- a/public/app/plugins/panel/table/TablePanel.tsx +++ b/public/app/plugins/panel/table/TablePanel.tsx @@ -63,6 +63,7 @@ export function TablePanel(props: Props) { cellHeight={options.cellHeight} timeRange={timeRange} enableSharedCrosshair={config.featureToggles.tableSharedCrosshair && enableSharedCrosshair} + fieldConfig={fieldConfig} /> ); From b3c0f69576d8e2328c72937dec9fbd43c5e95e5a Mon Sep 17 00:00:00 2001 From: Darren Janeczek <38694490+darrenjaneczek@users.noreply.github.com> Date: Fri, 23 Feb 2024 12:03:10 -0500 Subject: [PATCH 0141/1406] style: datatrails: use Tag on cards (#83198) * style: datatrails: use Tag on cards --- public/app/features/trails/DataTrailCard.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/public/app/features/trails/DataTrailCard.tsx b/public/app/features/trails/DataTrailCard.tsx index fa3f2b185a..a0e35dc870 100644 --- a/public/app/features/trails/DataTrailCard.tsx +++ b/public/app/features/trails/DataTrailCard.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { dateTimeFormat, GrafanaTheme2 } from '@grafana/data'; import { AdHocFiltersVariable, sceneGraph } from '@grafana/scenes'; -import { Badge, Card, IconButton, Stack, useStyles2 } from '@grafana/ui'; +import { Card, IconButton, Stack, Tag, useStyles2 } from '@grafana/ui'; import { DataTrail } from './DataTrail'; import { VAR_FILTERS } from './shared'; @@ -26,13 +26,15 @@ export function DataTrailCard({ trail, onSelect, onDelete }: Props) { const filters = filtersVariable.state.filters; const dsValue = getDataSource(trail); + const onClick = () => onSelect(trail); + return ( - onSelect(trail)} className={styles.card}> + {getMetricName(trail.state.metric)}
{filters.map((f) => ( - + ))}
From 18d1ced069afa5bc7189596a619dcac9004f6c5b Mon Sep 17 00:00:00 2001 From: Darren Janeczek <38694490+darrenjaneczek@users.noreply.github.com> Date: Fri, 23 Feb 2024 12:03:40 -0500 Subject: [PATCH 0142/1406] style: datatrails: indicate prometheus data source (#83199) * style: indicate prometheus data source --- public/app/features/trails/DataTrail.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/public/app/features/trails/DataTrail.tsx b/public/app/features/trails/DataTrail.tsx index b229a525f8..a7a284c958 100644 --- a/public/app/features/trails/DataTrail.tsx +++ b/public/app/features/trails/DataTrail.tsx @@ -29,7 +29,7 @@ import { MetricScene } from './MetricScene'; import { MetricSelectScene } from './MetricSelectScene'; import { MetricsHeader } from './MetricsHeader'; import { getTrailStore } from './TrailStore/TrailStore'; -import { LOGS_METRIC, MetricSelectedEvent, trailDS, VAR_DATASOURCE, VAR_FILTERS } from './shared'; +import { MetricSelectedEvent, trailDS, VAR_DATASOURCE, VAR_FILTERS } from './shared'; import { getUrlForTrail } from './utils'; export interface DataTrailState extends SceneObjectState { @@ -206,9 +206,9 @@ function getVariableSet(initialDS?: string, metric?: string, initialFilters?: Ad variables: [ new DataSourceVariable({ name: VAR_DATASOURCE, - label: 'Data source', + label: 'Prometheus data source', value: initialDS, - pluginId: metric === LOGS_METRIC ? 'loki' : 'prometheus', + pluginId: 'prometheus', }), new AdHocFiltersVariable({ name: VAR_FILTERS, From 2b4f1087712e82d17db8ce29adf4a40f5b76e0fc Mon Sep 17 00:00:00 2001 From: Jack Baldry Date: Fri, 23 Feb 2024 17:30:08 +0000 Subject: [PATCH 0143/1406] Revert "Alerting docs: rework create alert rules definition and topic" (#83328) --- .../sources/alerting/alerting-rules/_index.md | 52 ++++++++------- ...reate-mimir-loki-managed-recording-rule.md | 16 ++--- .../create-notification-policy.md | 3 +- .../manage-contact-points/_index.md | 65 ------------------- .../integrations/_index.md | 50 ++++++++++++++ .../integrations/configure-oncall.md | 4 +- .../integrations/pager-duty.md | 0 .../integrations/webhook-notifier.md | 0 .../configure-notifications/_index.md | 26 -------- .../alerting/manage-notifications/_index.md | 41 +++++++++--- .../create-silence.md | 3 +- .../images-in-notifications.md | 14 ++-- .../manage-contact-points.md | 34 ++++++++++ .../mute-timings.md | 3 +- .../template-notifications/_index.md | 16 ++--- .../create-notification-templates.md | 2 - .../template-notifications/reference.md | 2 - .../use-notification-templates.md | 10 ++- .../using-go-templating-language.md | 14 ++-- docs/sources/alerting/monitor/_index.md | 2 +- .../setup-grafana/configure-grafana/_index.md | 2 +- 21 files changed, 188 insertions(+), 171 deletions(-) rename docs/sources/alerting/{configure-notifications => alerting-rules}/create-notification-policy.md (99%) rename docs/sources/alerting/{configure-notifications => alerting-rules}/manage-contact-points/_index.md (51%) create mode 100644 docs/sources/alerting/alerting-rules/manage-contact-points/integrations/_index.md rename docs/sources/alerting/{configure-notifications => alerting-rules}/manage-contact-points/integrations/configure-oncall.md (95%) rename docs/sources/alerting/{configure-notifications => alerting-rules}/manage-contact-points/integrations/pager-duty.md (100%) rename docs/sources/alerting/{configure-notifications => alerting-rules}/manage-contact-points/integrations/webhook-notifier.md (100%) delete mode 100644 docs/sources/alerting/configure-notifications/_index.md rename docs/sources/alerting/{configure-notifications => manage-notifications}/create-silence.md (98%) rename docs/sources/alerting/{configure-notifications/template-notifications => manage-notifications}/images-in-notifications.md (96%) create mode 100644 docs/sources/alerting/manage-notifications/manage-contact-points.md rename docs/sources/alerting/{configure-notifications => manage-notifications}/mute-timings.md (98%) rename docs/sources/alerting/{configure-notifications => manage-notifications}/template-notifications/_index.md (78%) rename docs/sources/alerting/{configure-notifications => manage-notifications}/template-notifications/create-notification-templates.md (98%) rename docs/sources/alerting/{configure-notifications => manage-notifications}/template-notifications/reference.md (98%) rename docs/sources/alerting/{configure-notifications => manage-notifications}/template-notifications/use-notification-templates.md (72%) rename docs/sources/alerting/{configure-notifications => manage-notifications}/template-notifications/using-go-templating-language.md (93%) diff --git a/docs/sources/alerting/alerting-rules/_index.md b/docs/sources/alerting/alerting-rules/_index.md index 1d7c8722d9..3063d36929 100644 --- a/docs/sources/alerting/alerting-rules/_index.md +++ b/docs/sources/alerting/alerting-rules/_index.md @@ -5,46 +5,54 @@ aliases: - unified-alerting/alerting-rules/ - ./create-alerts/ canonical: https://grafana.com/docs/grafana/latest/alerting/alerting-rules/ -description: Create and manage alert rules +description: Configure the features and integrations you need to create and manage your alerts labels: products: - cloud - enterprise - oss -menuTitle: Create and manage alert rules -title: Create and manage alert rules +menuTitle: Configure +title: Configure Alerting weight: 120 --- -# Create and manage alert rules +# Configure Alerting -An alert rule consists of one or more queries and expressions that select the data you want to measure. It also contains a condition, which is the threshold that an alert rule must meet or exceed in order to fire. +Configure the features and integrations that you need to create and manage your alerts. -Create, manage, view, and adjust alert rules to alert on your metrics data or log entries from multiple data sources — no matter where your data is stored. +**Configure alert rules** -The main parts of alert rule creation are: +[Configure Grafana-managed alert rules][create-grafana-managed-rule]. -1. Select your data source -1. Query your data -1. Normalize your data -1. Set your threshold +[Configure data source-managed alert rules][create-mimir-loki-managed-rule] -**Query, expressions, and alert condition** +**Configure recording rules** -What are you monitoring? How are you measuring it? +_Recording rules are only available for compatible Prometheus or Loki data sources._ -{{< admonition type="note" >}} -Expressions can only be used for Grafana-managed alert rules. -{{< /admonition >}} +For more information, see [Configure recording rules][create-mimir-loki-managed-recording-rule]. -**Evaluation** +**Configure contact points** -How do you want your alert to be evaluated? +For information on how to configure contact points, see [Configure contact points][manage-contact-points]. -**Labels and notifications** +**Configure notification policies** -How do you want to route your alert? What kind of additional labels could you add to annotate your alert rules and ease searching? +For information on how to configure notification policies, see [Configure notification policies][create-notification-policy]. -**Annotations** +{{% docs/reference %}} +[create-mimir-loki-managed-rule]: "/docs/grafana/ -> /docs/grafana//alerting/alerting-rules/create-mimir-loki-managed-rule" +[create-mimir-loki-managed-rule]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/alerting-rules/create-mimir-loki-managed-rule" -Do you want to add more context on the alert in your notification messages, for example, what caused the alert to fire? Which server did it happen on? +[create-mimir-loki-managed-recording-rule]: "/docs/grafana/ -> /docs/grafana//alerting/alerting-rules/create-mimir-loki-managed-recording-rule" +[create-mimir-loki-managed-recording-rule]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/alerting-rules/create-mimir-loki-managed-recording-rule" + +[create-grafana-managed-rule]: "/docs/grafana/ -> /docs/grafana//alerting/alerting-rules/create-grafana-managed-rule" +[create-grafana-managed-rule]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/alerting-rules/create-grafana-managed-rule" + +[manage-contact-points]: "/docs/grafana/ -> /docs/grafana//alerting/alerting-rules/manage-contact-points" +[manage-contact-points]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/alerting-rules/manage-contact-points" + +[create-notification-policy]: "/docs/grafana/ -> /docs/grafana//alerting/alerting-rules/create-notification-policy" +[create-notification-policy]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/alerting-rules/create-notification-policy" +{{% /docs/reference %}} diff --git a/docs/sources/alerting/alerting-rules/create-mimir-loki-managed-recording-rule.md b/docs/sources/alerting/alerting-rules/create-mimir-loki-managed-recording-rule.md index 637cbf3ca4..08fbbbc492 100644 --- a/docs/sources/alerting/alerting-rules/create-mimir-loki-managed-recording-rule.md +++ b/docs/sources/alerting/alerting-rules/create-mimir-loki-managed-recording-rule.md @@ -3,7 +3,7 @@ aliases: - ../unified-alerting/alerting-rules/create-cortex-loki-managed-recording-rule/ - ../unified-alerting/alerting-rules/create-mimir-loki-managed-recording-rule/ canonical: https://grafana.com/docs/grafana/latest/alerting/alerting-rules/create-mimir-loki-managed-recording-rule/ -description: Create recording rules for an external Grafana Mimir or Loki instance +description: Configure recording rules for an external Grafana Mimir or Loki instance keywords: - grafana - alerting @@ -16,14 +16,13 @@ labels: - cloud - enterprise - oss -title: Create recording rules +title: Configure recording rules weight: 300 --- -# Create recording rules +# Configure recording rules -You can create and manage recording rules for an external Grafana Mimir or Loki instance. -Recording rules calculate frequently needed expressions or computationally expensive expressions in advance and save the result as a new set of time series. Querying this new time series is faster, especially for dashboards since they query the same expression every time the dashboards refresh. +You can create and manage recording rules for an external Grafana Mimir or Loki instance. Recording rules calculate frequently needed expressions or computationally expensive expressions in advance and save the result as a new set of time series. Querying this new time series is faster, especially for dashboards since they query the same expression every time the dashboards refresh. **Note:** @@ -49,14 +48,13 @@ To create recording rules, follow these steps. 1. Click **Alerts & IRM** -> **Alerting** -> **Alert rules**. -1. Select **Rule type** -> **Recording**. -1. Click **+New recording rule**. +1. Click **New recording rule**. -1. Enter recording rule name. +1. Set rule name. The recording rule name must be a Prometheus metric name and contain no whitespace. -1. Define recording rule. +1. Define query. - Select your Loki or Prometheus data source. - Enter a query. 1. Add namespace and group. diff --git a/docs/sources/alerting/configure-notifications/create-notification-policy.md b/docs/sources/alerting/alerting-rules/create-notification-policy.md similarity index 99% rename from docs/sources/alerting/configure-notifications/create-notification-policy.md rename to docs/sources/alerting/alerting-rules/create-notification-policy.md index 72cfcc9f70..3df48d66ae 100644 --- a/docs/sources/alerting/configure-notifications/create-notification-policy.md +++ b/docs/sources/alerting/alerting-rules/create-notification-policy.md @@ -3,7 +3,6 @@ aliases: - ../notifications/ - ../old-alerting/notifications/ - ../unified-alerting/notifications/ - - ./alerting-rules/ canonical: https://grafana.com/docs/grafana/latest/alerting/alerting-rules/create-notification-policy/ description: Configure notification policies to determine how alerts are routed to contact points keywords: @@ -18,7 +17,7 @@ labels: - enterprise - oss title: Configure notification policies -weight: 430 +weight: 420 --- # Configure notification policies diff --git a/docs/sources/alerting/configure-notifications/manage-contact-points/_index.md b/docs/sources/alerting/alerting-rules/manage-contact-points/_index.md similarity index 51% rename from docs/sources/alerting/configure-notifications/manage-contact-points/_index.md rename to docs/sources/alerting/alerting-rules/manage-contact-points/_index.md index 1f7fa7c184..186c487914 100644 --- a/docs/sources/alerting/configure-notifications/manage-contact-points/_index.md +++ b/docs/sources/alerting/alerting-rules/manage-contact-points/_index.md @@ -7,10 +7,6 @@ aliases: - ../contact-points/test-contact-point/ # /docs/grafana//alerting/contact-points/test-contact-point/ - ../manage-notifications/manage-contact-points/ # /docs/grafana//alerting/manage-notifications/manage-contact-points/ - create-contact-point/ # /docs/grafana//alerting/alerting-rules/create-contact-point/ - - ./alerting-rules/ - - ./alerting-rules/manage-notifications/manage-contact-points/ - - alerting/manage-notifications/manage-contact-points/configure-integrations/ - - ./alerting-rules/manage-contact-points/ canonical: https://grafana.com/docs/grafana/latest/alerting/alerting-rules/manage-contact-points/ description: Configure contact points to define how your contacts are notified when an alert rule fires keywords: @@ -32,8 +28,6 @@ weight: 410 Use contact points to define how your contacts are notified when an alert rule fires. You can add, edit, delete, and test a contact point. -Configure contact point integrations in Grafana to select your preferred communication channel for receiving notifications when your alert rules are firing. - ## Add a contact point Complete the following steps to add a contact point. @@ -81,62 +75,3 @@ Complete the following steps to test a contact point. 1. Click **Test** to open the contact point testing modal. 1. Choose whether to send a predefined test notification or choose custom to add your own custom annotations and labels to include in the notification. 1. Click **Send test notification** to fire the alert. - -## Manage contact points - -The Contact points list view lists all existing contact points and notification templates. - -On the **Contact Points** tab, you can: - -- Search for name and type of contact points and integrations -- View all existing contact points and integrations -- View how many notification policies each contact point is being used for and navigate directly to the linked notification policies -- View the status of notification deliveries -- Export individual contact points or all contact points in JSON, YAML, or Terraform format -- Delete contact points that are not in use by a notification policy - -On the **Notification templates** tab, you can: - -- View, edit, copy or delete existing notification templates - -## Configure contact point integrations - -Each contact point integration has its own configuration options and setup process. In most cases, this involves providing an API key or a Webhook URL. - -Once configured, you can use integrations as part of your contact points to receive notifications whenever your alert changes its state. In this section, we'll cover the basic steps to configure your integrations, so you can start receiving real-time alerts and stay on top of your monitoring data. - -## List of supported integrations - -| Name | Type | -| ------------------------ | ------------------------- | -| DingDing | `dingding` | -| Discord | `discord` | -| Email | `email` | -| Google Chat | `googlechat` | -| [Grafana Oncall][oncall] | `oncall` | -| Hipchat | `hipchat` | -| Kafka | `kafka` | -| Line | `line` | -| Microsoft Teams | `teams` | -| Opsgenie | `opsgenie` | -| [Pagerduty][pagerduty] | `pagerduty` | -| Prometheus Alertmanager | `prometheus-alertmanager` | -| Pushover | `pushover` | -| Sensu | `sensu` | -| Sensu Go | `sensugo` | -| Slack | `slack` | -| Telegram | `telegram` | -| Threema | `threema` | -| VictorOps | `victorops` | -| [Webhook][webhook] | `webhook` | - -{{% docs/reference %}} -[pagerduty]: "/docs/grafana/ -> /docs/grafana//alerting/configure-notifications/manage-contact-points/integrations/pager-duty" -[pagerduty]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/manage-contact-points/integrations/pager-duty" - -[oncall]: "/docs/grafana/ -> /docs/grafana//alerting/configure-notifications/manage-contact-points/integrations/configure-oncall" -[oncall]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/manage-contact-points/integrations/configure-oncall" - -[webhook]: "/docs/grafana/ -> /docs/grafana//alerting/configure-notifications/manage-contact-points/integrations/webhook-notifier" -[webhook]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/manage-contact-points/integrations/webhook-notifier" -{{% /docs/reference %}} diff --git a/docs/sources/alerting/alerting-rules/manage-contact-points/integrations/_index.md b/docs/sources/alerting/alerting-rules/manage-contact-points/integrations/_index.md new file mode 100644 index 0000000000..dfd1efd3c6 --- /dev/null +++ b/docs/sources/alerting/alerting-rules/manage-contact-points/integrations/_index.md @@ -0,0 +1,50 @@ +--- +aliases: + - alerting/manage-notifications/manage-contact-points/configure-integrations/ +canonical: https://grafana.com/docs/grafana/latest/alerting/alerting-rules/manage-contact-points/integrations/ +description: Configure contact point integrations to select your preferred communication channels for receiving notifications of firing alerts. +keywords: + - Grafana + - alerting + - guide + - notifications + - integrations + - contact points +labels: + products: + - cloud + - enterprise + - oss +title: Configure contact point integrations +weight: 100 +--- + +# Configure contact point integrations + +Configure contact point integrations in Grafana to select your preferred communication channel for receiving notifications when your alert rules are firing. Each integration has its own configuration options and setup process. In most cases, this involves providing an API key or a Webhook URL. + +Once configured, you can use integrations as part of your contact points to receive notifications whenever your alert changes its state. In this section, we'll cover the basic steps to configure your integrations, so you can start receiving real-time alerts and stay on top of your monitoring data. + +## List of supported integrations + +| Name | Type | +| ----------------------- | ------------------------- | +| DingDing | `dingding` | +| Discord | `discord` | +| Email | `email` | +| Google Chat | `googlechat` | +| Hipchat | `hipchat` | +| Kafka | `kafka` | +| Line | `line` | +| Microsoft Teams | `teams` | +| Opsgenie | `opsgenie` | +| Pagerduty | `pagerduty` | +| Prometheus Alertmanager | `prometheus-alertmanager` | +| Pushover | `pushover` | +| Sensu | `sensu` | +| Sensu Go | `sensugo` | +| Slack | `slack` | +| Telegram | `telegram` | +| Threema | `threema` | +| VictorOps | `victorops` | +| Webhook | `webhook` | diff --git a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-oncall.md b/docs/sources/alerting/alerting-rules/manage-contact-points/integrations/configure-oncall.md similarity index 95% rename from docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-oncall.md rename to docs/sources/alerting/alerting-rules/manage-contact-points/integrations/configure-oncall.md index bf3fa657a0..f71e505303 100644 --- a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-oncall.md +++ b/docs/sources/alerting/alerting-rules/manage-contact-points/integrations/configure-oncall.md @@ -63,8 +63,8 @@ To set up the Grafana OnCall integration using the Grafana Alerting application, This redirects you to the Grafana OnCall integration page in the Grafana OnCall application. From there, you can add [routes and escalation chains][escalation-chain]. {{% docs/reference %}} -[create-notification-policy]: "/docs/grafana/ -> /docs/grafana//alerting/configure-notifications/create-notification-policy" -[create-notification-policy]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/create-notification-policy" +[create-notification-policy]: "/docs/grafana/ -> /docs/grafana//alerting/alerting-rules/create-notification-policy" +[create-notification-policy]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/alerting-rules/create-notification-policy" [oncall-integration]: "/docs/grafana/ -> /docs/oncall/latest/integrations/grafana-alerting" [oncall-integration]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/oncall/integrations/grafana-alerting" diff --git a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/pager-duty.md b/docs/sources/alerting/alerting-rules/manage-contact-points/integrations/pager-duty.md similarity index 100% rename from docs/sources/alerting/configure-notifications/manage-contact-points/integrations/pager-duty.md rename to docs/sources/alerting/alerting-rules/manage-contact-points/integrations/pager-duty.md diff --git a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/webhook-notifier.md b/docs/sources/alerting/alerting-rules/manage-contact-points/integrations/webhook-notifier.md similarity index 100% rename from docs/sources/alerting/configure-notifications/manage-contact-points/integrations/webhook-notifier.md rename to docs/sources/alerting/alerting-rules/manage-contact-points/integrations/webhook-notifier.md diff --git a/docs/sources/alerting/configure-notifications/_index.md b/docs/sources/alerting/configure-notifications/_index.md deleted file mode 100644 index f709efed8f..0000000000 --- a/docs/sources/alerting/configure-notifications/_index.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -canonical: https://grafana.com/docs/grafana/latest/alerting/configure-notifications -description: Configure how, when, and where to send your alert notifications -keywords: - - grafana - - alert - - notifications -labels: - products: - - cloud - - enterprise - - oss -menuTitle: Configure notifications -title: Configure notifications -weight: 125 ---- - -# Configure notifications - -Choose how, when, and where to send your alert notifications. - -As a first step, define your contact points; where to send your alert notifications to. A contact point is a set of one or more integrations that are used to deliver notifications. - -Next, create a notification policy which is a set of rules for where, when and how your alerts are routed to contact points. In a notification policy, you define where to send your alert notifications by choosing one of the contact points you created. - -Optionally, you can add notification templates to contact points for reuse and consistent messaging in your notifications. diff --git a/docs/sources/alerting/manage-notifications/_index.md b/docs/sources/alerting/manage-notifications/_index.md index 5080cb24d2..1ea9bc2492 100644 --- a/docs/sources/alerting/manage-notifications/_index.md +++ b/docs/sources/alerting/manage-notifications/_index.md @@ -1,22 +1,47 @@ --- canonical: https://grafana.com/docs/grafana/latest/alerting/manage-notifications/ -description: Detect and respond for day-to-day triage and analysis of what’s going on and action you need to take +description: Manage your alerts by creating silences, mute timings, and more keywords: - grafana - - detect - - respont + - alert + - notifications labels: products: - cloud - enterprise - oss -menuTitle: Detect and respond -title: Detect and respond +menuTitle: Manage +title: Manage your alerts weight: 130 --- -# Detect and respond +# Manage your alerts -Use Grafana Alerting to track and generate alerts and send notifications, providing an efficient way for engineers to monitor, respond, and triage issues within their services. +Once you have set up your alert rules, contact points, and notification policies, you can use Grafana Alerting to: -Alerts and alert notifications provide a lot of value as key indicators to issues during the triage process, providing engineers with the information they need to understand what is going on in their system or service. +[Create silences][create-silence] + +[Create mute timings][mute-timings] + +[Declare incidents from firing alerts][declare-incident-from-firing-alert] + +[View the state and health of alert rules][view-state-health] + +[View and filter alert rules][view-alert-rules] + +{{% docs/reference %}} +[create-silence]: "/docs/grafana/ -> /docs/grafana//alerting/manage-notifications/create-silence" +[create-silence]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/manage-notifications/create-silence" + +[declare-incident-from-firing-alert]: "/docs/grafana/ -> /docs/grafana//alerting/manage-notifications/declare-incident-from-alert" +[declare-incident-from-firing-alert]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/manage-notifications/declare-incident-from-alert" + +[mute-timings]: "/docs/grafana/ -> /docs/grafana//alerting/manage-notifications/mute-timings" +[mute-timings]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/manage-notifications/mute-timings" + +[view-alert-rules]: "/docs/grafana/ -> /docs/grafana//alerting/manage-notifications/view-alert-rules" +[view-alert-rules]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/manage-notifications/view-alert-rules" + +[view-state-health]: "/docs/grafana/ -> /docs/grafana//alerting/manage-notifications/view-state-health" +[view-state-health]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/manage-notifications/view-state-health" +{{% /docs/reference %}} diff --git a/docs/sources/alerting/configure-notifications/create-silence.md b/docs/sources/alerting/manage-notifications/create-silence.md similarity index 98% rename from docs/sources/alerting/configure-notifications/create-silence.md rename to docs/sources/alerting/manage-notifications/create-silence.md index 9d361832b4..3b0d544360 100644 --- a/docs/sources/alerting/configure-notifications/create-silence.md +++ b/docs/sources/alerting/manage-notifications/create-silence.md @@ -6,7 +6,6 @@ aliases: - ../silences/remove-silence/ - ../unified-alerting/silences/ - ../silences/ - - ./alerting/manage-notifications/create-silence/ canonical: https://grafana.com/docs/grafana/latest/alerting/manage-notifications/create-silence/ description: Create silences to stop notifications from getting created for a specified window of time keywords: @@ -20,7 +19,7 @@ labels: - enterprise - oss title: Manage silences -weight: 440 +weight: 410 --- # Manage silences diff --git a/docs/sources/alerting/configure-notifications/template-notifications/images-in-notifications.md b/docs/sources/alerting/manage-notifications/images-in-notifications.md similarity index 96% rename from docs/sources/alerting/configure-notifications/template-notifications/images-in-notifications.md rename to docs/sources/alerting/manage-notifications/images-in-notifications.md index ce7806e087..89807f1c8b 100644 --- a/docs/sources/alerting/configure-notifications/template-notifications/images-in-notifications.md +++ b/docs/sources/alerting/manage-notifications/images-in-notifications.md @@ -1,6 +1,4 @@ --- -aliases: - - ./alerting-rules/manage-notifications/manage-contact-points/template-notifications/images-in-notifications/ canonical: https://grafana.com/docs/grafana/latest/alerting/manage-notifications/images-in-notifications/ description: Use images in notifications to help users better understand why alerts are firing or have been resolved keywords: @@ -14,7 +12,7 @@ labels: - enterprise - oss title: Use images in notifications -weight: 500 +weight: 405 --- # Use images in notifications @@ -35,7 +33,7 @@ Refer to the table at the end of this page for a list of contact points and thei ## Requirements -1. To use images in notifications, Grafana must be set up to use image rendering. You can either install the image rendering plugin or run it as a remote rendering service. +1. To use images in notifications, Grafana must be set up to use [image rendering][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][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. @@ -71,6 +69,8 @@ If screenshots should be uploaded to cloud storage then `upload_external_image_s # will be persisted to disk for up to temp_data_lifetime. upload_external_image_storage = false +For more information on image rendering, refer to [image rendering][image-rendering]. + Restart Grafana for the changes to take effect. ## Advanced configuration @@ -137,3 +137,9 @@ For example, if a screenshot could not be taken within the expected time (10 sec - `grafana_screenshot_successes_total` - `grafana_screenshot_upload_failures_total` - `grafana_screenshot_upload_successes_total` + +{{% docs/reference %}} +[image-rendering]: "/docs/ -> /docs/grafana//setup-grafana/image-rendering" + +[paths]: "/docs/ -> /docs/grafana//setup-grafana/configure-grafana#paths" +{{% /docs/reference %}} diff --git a/docs/sources/alerting/manage-notifications/manage-contact-points.md b/docs/sources/alerting/manage-notifications/manage-contact-points.md new file mode 100644 index 0000000000..bbfd98e4b1 --- /dev/null +++ b/docs/sources/alerting/manage-notifications/manage-contact-points.md @@ -0,0 +1,34 @@ +--- +canonical: https://grafana.com/docs/grafana/latest/alerting/manage-notifications/manage-contact-points/ +description: View, edit, copy, or delete your contact points and notification templates +keywords: + - grafana + - alerting + - contact points + - search + - export +labels: + products: + - cloud + - enterprise + - oss +title: Manage contact points +weight: 410 +--- + +# Manage contact points + +The Contact points list view lists all existing contact points and notification templates. + +On the **Contact Points** tab, you can: + +- Search for name and type of contact points and integrations +- View all existing contact points and integrations +- View how many notification policies each contact point is being used for and navigate directly to the linked notification policies +- View the status of notification deliveries +- Export individual contact points or all contact points in JSON, YAML, or Terraform format +- Delete contact points that are not in use by a notification policy + +On the **Notification templates** tab, you can: + +- View, edit, copy or delete existing notification templates diff --git a/docs/sources/alerting/configure-notifications/mute-timings.md b/docs/sources/alerting/manage-notifications/mute-timings.md similarity index 98% rename from docs/sources/alerting/configure-notifications/mute-timings.md rename to docs/sources/alerting/manage-notifications/mute-timings.md index 7e6d20154f..e3337f7877 100644 --- a/docs/sources/alerting/configure-notifications/mute-timings.md +++ b/docs/sources/alerting/manage-notifications/mute-timings.md @@ -2,7 +2,6 @@ aliases: - ../notifications/mute-timings/ - ../unified-alerting/notifications/mute-timings/ - - ./alerting/manage-notifications/mute-timings/ canonical: https://grafana.com/docs/grafana/latest/alerting/manage-notifications/mute-timings/ description: Create mute timings to prevent alerts from firing during a specific and reoccurring period of time keywords: @@ -18,7 +17,7 @@ labels: - enterprise - oss title: Create mute timings -weight: 450 +weight: 420 --- # Create mute timings diff --git a/docs/sources/alerting/configure-notifications/template-notifications/_index.md b/docs/sources/alerting/manage-notifications/template-notifications/_index.md similarity index 78% rename from docs/sources/alerting/configure-notifications/template-notifications/_index.md rename to docs/sources/alerting/manage-notifications/template-notifications/_index.md index 00ac7e8ace..1a5507f577 100644 --- a/docs/sources/alerting/configure-notifications/template-notifications/_index.md +++ b/docs/sources/alerting/manage-notifications/template-notifications/_index.md @@ -1,6 +1,4 @@ --- -aliases: - - ./alerting-rules/manage-notifications/manage-contact-points/template-notifications/ canonical: https://grafana.com/docs/grafana/latest/alerting/manage-notifications/template-notifications/ description: Customize your notifications using notification templates keywords: @@ -14,7 +12,7 @@ labels: - enterprise - oss title: Customize notifications -weight: 420 +weight: 400 --- # Customize notifications @@ -54,12 +52,12 @@ Use notification templates to send notifications to your contact points. Data that is available when writing templates. {{% docs/reference %}} -[reference]: "/docs/grafana/ -> /docs/grafana//alerting/configure-notifications/template-notifications/reference" -[reference]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/template-notifications/reference" +[reference]: "/docs/grafana/ -> /docs/grafana//alerting/manage-notifications/template-notifications/reference" +[reference]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/manage-notifications/template-notifications/reference" -[use-notification-templates]: "/docs/grafana/ -> /docs/grafana//alerting/configure-notifications/template-notifications/use-notification-templates" -[use-notification-templates]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/template-notifications/use-notification-templates" +[use-notification-templates]: "/docs/grafana/ -> /docs/grafana//alerting/manage-notifications/template-notifications/use-notification-templates" +[use-notification-templates]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/manage-notifications/template-notifications/use-notification-templates" -[using-go-templating-language]: "/docs/grafana/ -> /docs/grafana//alerting/configure-notifications/template-notifications/using-go-templating-language" -[using-go-templating-language]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/template-notifications/using-go-templating-language" +[using-go-templating-language]: "/docs/grafana/ -> /docs/grafana//alerting/manage-notifications/template-notifications/using-go-templating-language" +[using-go-templating-language]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/manage-notifications/template-notifications/using-go-templating-language" {{% /docs/reference %}} diff --git a/docs/sources/alerting/configure-notifications/template-notifications/create-notification-templates.md b/docs/sources/alerting/manage-notifications/template-notifications/create-notification-templates.md similarity index 98% rename from docs/sources/alerting/configure-notifications/template-notifications/create-notification-templates.md rename to docs/sources/alerting/manage-notifications/template-notifications/create-notification-templates.md index d0b333c14d..947556e4e2 100644 --- a/docs/sources/alerting/configure-notifications/template-notifications/create-notification-templates.md +++ b/docs/sources/alerting/manage-notifications/template-notifications/create-notification-templates.md @@ -1,6 +1,4 @@ --- -aliases: - - ./alerting-rules/manage-notifications/manage-contact-points/template-notifications/create-notification-templates/ canonical: https://grafana.com/docs/grafana/latest/alerting/manage-notifications/template-notifications/create-notification-templates/ description: Create notification templates to sent to your contact points keywords: diff --git a/docs/sources/alerting/configure-notifications/template-notifications/reference.md b/docs/sources/alerting/manage-notifications/template-notifications/reference.md similarity index 98% rename from docs/sources/alerting/configure-notifications/template-notifications/reference.md rename to docs/sources/alerting/manage-notifications/template-notifications/reference.md index dc0dee7f18..16c26acc6d 100644 --- a/docs/sources/alerting/configure-notifications/template-notifications/reference.md +++ b/docs/sources/alerting/manage-notifications/template-notifications/reference.md @@ -1,6 +1,4 @@ --- -aliases: - - ./alerting-rules/manage-notifications/manage-contact-points/template-notifications/reference/ canonical: https://grafana.com/docs/grafana/latest/alerting/manage-notifications/template-notifications/reference/ description: Learn about templating notifications options keywords: diff --git a/docs/sources/alerting/configure-notifications/template-notifications/use-notification-templates.md b/docs/sources/alerting/manage-notifications/template-notifications/use-notification-templates.md similarity index 72% rename from docs/sources/alerting/configure-notifications/template-notifications/use-notification-templates.md rename to docs/sources/alerting/manage-notifications/template-notifications/use-notification-templates.md index 24ae3a2b56..a2a341e02c 100644 --- a/docs/sources/alerting/configure-notifications/template-notifications/use-notification-templates.md +++ b/docs/sources/alerting/manage-notifications/template-notifications/use-notification-templates.md @@ -1,6 +1,4 @@ --- -aliases: - - ./alerting-rules/manage-notifications/manage-contact-points/template-notifications/use-notification-templates/ canonical: https://grafana.com/docs/grafana/latest/alerting/manage-notifications/template-notifications/use-notification-templates/ description: Use notification templates in contact points to customize your notifications keywords: @@ -37,9 +35,9 @@ In the Contact points tab, you can see a list of your contact points. 1. Click **Save contact point**. {{% docs/reference %}} -[create-notification-templates]: "/docs/grafana/ -> /docs/grafana//alerting/configure-notifications/template-notifications/create-notification-templates" -[create-notification-templates]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/template-notifications/create-notification-templates" +[create-notification-templates]: "/docs/grafana/ -> /docs/grafana//alerting/manage-notifications/template-notifications/create-notification-templates" +[create-notification-templates]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/manage-notifications/template-notifications/create-notification-templates" -[using-go-templating-language]: "/docs/grafana/ -> /docs/grafana//alerting/configure-notifications/template-notifications/using-go-templating-language" -[using-go-templating-language]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/template-notifications/using-go-templating-language" +[using-go-templating-language]: "/docs/grafana/ -> /docs/grafana//alerting/manage-notifications/template-notifications/using-go-templating-language" +[using-go-templating-language]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/manage-notifications/template-notifications/using-go-templating-language" {{% /docs/reference %}} diff --git a/docs/sources/alerting/configure-notifications/template-notifications/using-go-templating-language.md b/docs/sources/alerting/manage-notifications/template-notifications/using-go-templating-language.md similarity index 93% rename from docs/sources/alerting/configure-notifications/template-notifications/using-go-templating-language.md rename to docs/sources/alerting/manage-notifications/template-notifications/using-go-templating-language.md index 9831bc08b2..50115d3f23 100644 --- a/docs/sources/alerting/configure-notifications/template-notifications/using-go-templating-language.md +++ b/docs/sources/alerting/manage-notifications/template-notifications/using-go-templating-language.md @@ -1,6 +1,4 @@ --- -aliases: - - ./alerting-rules/manage-notifications/manage-contact-points/template-notifications/using-go-templating-language/ canonical: https://grafana.com/docs/grafana/latest/alerting/manage-notifications/template-notifications/using-go-templating-language/ description: Use Go's templating language to create your own notification templates keywords: @@ -282,12 +280,12 @@ grafana_folder = "Test alerts" ``` {{% docs/reference %}} -[create-notification-templates]: "/docs/grafana/ -> /docs/grafana//alerting/configure-notifications/template-notifications/create-notification-templates" -[create-notification-templates]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/template-notifications/create-notification-templates" +[create-notification-templates]: "/docs/grafana/ -> /docs/grafana//alerting/manage-notifications/template-notifications/create-notification-templates" +[create-notification-templates]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/manage-notifications/template-notifications/create-notification-templates" -[extendeddata]: "/docs/grafana/ -> /docs/grafana//alerting/configure-notifications/template-notifications/reference#extendeddata" -[extendeddata]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/template-notifications/reference#extendeddata" +[extendeddata]: "/docs/grafana/ -> /docs/grafana//alerting/manage-notifications/template-notifications/reference#extendeddata" +[extendeddata]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/manage-notifications/template-notifications/reference#extendeddata" -[reference]: "/docs/grafana/ -> /docs/grafana//alerting/configure-notifications/template-notifications/reference" -[reference]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/template-notifications/reference" +[reference]: "/docs/grafana/ -> /docs/grafana//alerting/manage-notifications/template-notifications/reference" +[reference]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/manage-notifications/template-notifications/reference" {{% /docs/reference %}} diff --git a/docs/sources/alerting/monitor/_index.md b/docs/sources/alerting/monitor/_index.md index 6769d67c94..577910d569 100644 --- a/docs/sources/alerting/monitor/_index.md +++ b/docs/sources/alerting/monitor/_index.md @@ -12,7 +12,7 @@ labels: products: - enterprise - oss -menuTitle: Monitor alerting +menuTitle: Monitor title: Meta monitoring weight: 140 --- diff --git a/docs/sources/setup-grafana/configure-grafana/_index.md b/docs/sources/setup-grafana/configure-grafana/_index.md index b89462f9c5..bf00cb8c98 100644 --- a/docs/sources/setup-grafana/configure-grafana/_index.md +++ b/docs/sources/setup-grafana/configure-grafana/_index.md @@ -1626,7 +1626,7 @@ The interval string is a possibly signed sequence of decimal numbers, followed b ## [unified_alerting.screenshots] -For more information about screenshots, refer to [Images in notifications]({{< relref "../../alerting/configure-notifications/template-notifications/images-in-notifications" >}}). +For more information about screenshots, refer to [Images in notifications]({{< relref "../../alerting/manage-notifications/images-in-notifications" >}}). ### capture From 23963a1f34dab9e1169a9adf9bed8f06bef8fb86 Mon Sep 17 00:00:00 2001 From: Todd Treece <360020+toddtreece@users.noreply.github.com> Date: Fri, 23 Feb 2024 12:48:26 -0500 Subject: [PATCH 0144/1406] Chore: Update wire to v0.6.0 using bingo (#83323) --- .bingo/Variables.mk | 15 +++++---------- .bingo/variables.env | 4 ++-- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/.bingo/Variables.mk b/.bingo/Variables.mk index 2c9eb12423..363c8b2cd3 100644 --- a/.bingo/Variables.mk +++ b/.bingo/Variables.mk @@ -1,13 +1,8 @@ -# Auto generated binary variables helper managed by https://github.com/bwplotka/bingo v0.8. DO NOT EDIT. +# Auto generated binary variables helper managed by https://github.com/bwplotka/bingo v0.9. DO NOT EDIT. # All tools are designed to be build inside $GOBIN. BINGO_DIR := $(dir $(lastword $(MAKEFILE_LIST))) GOPATH ?= $(shell go env GOPATH) -ifeq ($(OS),Windows_NT) - PATHSEP := $(if $(COMSPEC),;,:) - GOBIN ?= $(firstword $(subst $(PATHSEP), ,$(subst \,/,${GOPATH})))/bin -else - GOBIN ?= $(firstword $(subst :, ,${GOPATH}))/bin -endif +GOBIN ?= $(firstword $(subst :, ,${GOPATH}))/bin GO ?= $(shell which go) # Below generated variables ensure that every time a tool under each variable is invoked, the correct version @@ -64,9 +59,9 @@ $(SWAGGER): $(BINGO_DIR)/swagger.mod @echo "(re)installing $(GOBIN)/swagger-v0.30.2" @cd $(BINGO_DIR) && GOWORK=off $(GO) build -mod=mod -modfile=swagger.mod -o=$(GOBIN)/swagger-v0.30.2 "github.com/go-swagger/go-swagger/cmd/swagger" -WIRE := $(GOBIN)/wire-v0.5.0 +WIRE := $(GOBIN)/wire-v0.6.0 $(WIRE): $(BINGO_DIR)/wire.mod @# Install binary/ries using Go 1.14+ build command. This is using bwplotka/bingo-controlled, separate go module with pinned dependencies. - @echo "(re)installing $(GOBIN)/wire-v0.5.0" - @cd $(BINGO_DIR) && GOWORK=off $(GO) build -mod=mod -modfile=wire.mod -o=$(GOBIN)/wire-v0.5.0 "github.com/google/wire/cmd/wire" + @echo "(re)installing $(GOBIN)/wire-v0.6.0" + @cd $(BINGO_DIR) && GOWORK=off $(GO) build -mod=mod -modfile=wire.mod -o=$(GOBIN)/wire-v0.6.0 "github.com/google/wire/cmd/wire" diff --git a/.bingo/variables.env b/.bingo/variables.env index cf844231b9..dc64e5f430 100644 --- a/.bingo/variables.env +++ b/.bingo/variables.env @@ -1,4 +1,4 @@ -# Auto generated binary variables helper managed by https://github.com/bwplotka/bingo v0.8. DO NOT EDIT. +# Auto generated binary variables helper managed by https://github.com/bwplotka/bingo v0.9. DO NOT EDIT. # All tools are designed to be build inside $GOBIN. # Those variables will work only until 'bingo get' was invoked, or if tools were installed via Makefile's Variables.mk. GOBIN=${GOBIN:=$(go env GOBIN)} @@ -22,5 +22,5 @@ LEFTHOOK="${GOBIN}/lefthook-v1.4.8" SWAGGER="${GOBIN}/swagger-v0.30.2" -WIRE="${GOBIN}/wire-v0.5.0" +WIRE="${GOBIN}/wire-v0.6.0" From 8895b43c8562799dd1f6e2252896e0427f4582b1 Mon Sep 17 00:00:00 2001 From: Tania <10127682+undef1nd@users.noreply.github.com> Date: Fri, 23 Feb 2024 20:06:43 +0100 Subject: [PATCH 0145/1406] Chore: Remove docs and kinds report generators (#83277) * Chore: Remove codegen for docs * Remove kindsysreport --- .github/CODEOWNERS | 1 - .prettierignore | 6 - Makefile | 1 - docs/sources/developers/kinds/_index.md | 36 - .../developers/kinds/composable/_index.md | 12 - .../alertgroups/panelcfg/schema-reference.md | 33 - .../panelcfg/schema-reference.md | 40 - .../dataquery/schema-reference.md | 24 - .../barchart/panelcfg/schema-reference.md | 183 -- .../bargauge/panelcfg/schema-reference.md | 83 - .../candlestick/panelcfg/schema-reference.md | 277 -- .../canvas/panelcfg/schema-reference.md | 160 -- .../cloudwatch/dataquery/schema-reference.md | 24 - .../panelcfg/schema-reference.md | 41 - .../datagrid/panelcfg/schema-reference.md | 31 - .../debug/panelcfg/schema-reference.md | 42 - .../dataquery/schema-reference.md | 283 -- .../gauge/panelcfg/schema-reference.md | 80 - .../geomap/panelcfg/schema-reference.md | 97 - .../dataquery/schema-reference.md | 24 - .../dataquery/schema-reference.md | 33 - .../heatmap/panelcfg/schema-reference.md | 250 -- .../histogram/panelcfg/schema-reference.md | 148 -- .../logs/panelcfg/schema-reference.md | 39 - .../loki/dataquery/schema-reference.md | 36 - .../news/panelcfg/schema-reference.md | 32 - .../nodegraph/panelcfg/schema-reference.md | 57 - .../parca/dataquery/schema-reference.md | 30 - .../piechart/panelcfg/schema-reference.md | 154 -- .../prometheus/dataquery/schema-reference.md | 43 - .../stat/panelcfg/schema-reference.md | 81 - .../panelcfg/schema-reference.md | 119 - .../panelcfg/schema-reference.md | 118 - .../table/panelcfg/schema-reference.md | 340 --- .../tempo/dataquery/schema-reference.md | 24 - .../testdata/dataquery/schema-reference.md | 119 - .../text/panelcfg/schema-reference.md | 46 - .../timeseries/panelcfg/schema-reference.md | 233 -- .../trend/panelcfg/schema-reference.md | 225 -- .../xychart/panelcfg/schema-reference.md | 242 -- docs/sources/developers/kinds/core/_index.md | 14 - .../core/accesspolicy/schema-reference.md | 131 - .../kinds/core/dashboard/schema-reference.md | 677 ----- .../kinds/core/folder/schema-reference.md | 111 - .../core/librarypanel/schema-reference.md | 142 - .../core/preferences/schema-reference.md | 144 - .../core/publicdashboard/schema-reference.md | 111 - .../kinds/core/role/schema-reference.md | 110 - .../core/rolebinding/schema-reference.md | 136 - .../core/serviceaccount/schema-reference.md | 114 - .../kinds/core/team/schema-reference.md | 107 - docs/sources/developers/kinds/maturity.md | 298 --- kinds/gen.go | 1 - pkg/codegen/jenny_docs.go | 793 ------ pkg/codegen/tmpl/docs.tmpl | 21 - pkg/kindsysreport/attributes.go | 74 - pkg/kindsysreport/codegen/report.go | 391 --- pkg/kindsysreport/codegen/report.json | 2338 ----------------- pkg/kindsysreport/codeowners.go | 61 - public/app/plugins/gen.go | 3 - 60 files changed, 9624 deletions(-) delete mode 100644 docs/sources/developers/kinds/_index.md delete mode 100644 docs/sources/developers/kinds/composable/_index.md delete mode 100644 docs/sources/developers/kinds/composable/alertgroups/panelcfg/schema-reference.md delete mode 100644 docs/sources/developers/kinds/composable/annotationslist/panelcfg/schema-reference.md delete mode 100644 docs/sources/developers/kinds/composable/azuremonitor/dataquery/schema-reference.md delete mode 100644 docs/sources/developers/kinds/composable/barchart/panelcfg/schema-reference.md delete mode 100644 docs/sources/developers/kinds/composable/bargauge/panelcfg/schema-reference.md delete mode 100644 docs/sources/developers/kinds/composable/candlestick/panelcfg/schema-reference.md delete mode 100644 docs/sources/developers/kinds/composable/canvas/panelcfg/schema-reference.md delete mode 100644 docs/sources/developers/kinds/composable/cloudwatch/dataquery/schema-reference.md delete mode 100644 docs/sources/developers/kinds/composable/dashboardlist/panelcfg/schema-reference.md delete mode 100644 docs/sources/developers/kinds/composable/datagrid/panelcfg/schema-reference.md delete mode 100644 docs/sources/developers/kinds/composable/debug/panelcfg/schema-reference.md delete mode 100644 docs/sources/developers/kinds/composable/elasticsearch/dataquery/schema-reference.md delete mode 100644 docs/sources/developers/kinds/composable/gauge/panelcfg/schema-reference.md delete mode 100644 docs/sources/developers/kinds/composable/geomap/panelcfg/schema-reference.md delete mode 100644 docs/sources/developers/kinds/composable/googlecloudmonitoring/dataquery/schema-reference.md delete mode 100644 docs/sources/developers/kinds/composable/grafanapyroscope/dataquery/schema-reference.md delete mode 100644 docs/sources/developers/kinds/composable/heatmap/panelcfg/schema-reference.md delete mode 100644 docs/sources/developers/kinds/composable/histogram/panelcfg/schema-reference.md delete mode 100644 docs/sources/developers/kinds/composable/logs/panelcfg/schema-reference.md delete mode 100644 docs/sources/developers/kinds/composable/loki/dataquery/schema-reference.md delete mode 100644 docs/sources/developers/kinds/composable/news/panelcfg/schema-reference.md delete mode 100644 docs/sources/developers/kinds/composable/nodegraph/panelcfg/schema-reference.md delete mode 100644 docs/sources/developers/kinds/composable/parca/dataquery/schema-reference.md delete mode 100644 docs/sources/developers/kinds/composable/piechart/panelcfg/schema-reference.md delete mode 100644 docs/sources/developers/kinds/composable/prometheus/dataquery/schema-reference.md delete mode 100644 docs/sources/developers/kinds/composable/stat/panelcfg/schema-reference.md delete mode 100644 docs/sources/developers/kinds/composable/statetimeline/panelcfg/schema-reference.md delete mode 100644 docs/sources/developers/kinds/composable/statushistory/panelcfg/schema-reference.md delete mode 100644 docs/sources/developers/kinds/composable/table/panelcfg/schema-reference.md delete mode 100644 docs/sources/developers/kinds/composable/tempo/dataquery/schema-reference.md delete mode 100644 docs/sources/developers/kinds/composable/testdata/dataquery/schema-reference.md delete mode 100644 docs/sources/developers/kinds/composable/text/panelcfg/schema-reference.md delete mode 100644 docs/sources/developers/kinds/composable/timeseries/panelcfg/schema-reference.md delete mode 100644 docs/sources/developers/kinds/composable/trend/panelcfg/schema-reference.md delete mode 100644 docs/sources/developers/kinds/composable/xychart/panelcfg/schema-reference.md delete mode 100644 docs/sources/developers/kinds/core/_index.md delete mode 100644 docs/sources/developers/kinds/core/accesspolicy/schema-reference.md delete mode 100644 docs/sources/developers/kinds/core/dashboard/schema-reference.md delete mode 100644 docs/sources/developers/kinds/core/folder/schema-reference.md delete mode 100644 docs/sources/developers/kinds/core/librarypanel/schema-reference.md delete mode 100644 docs/sources/developers/kinds/core/preferences/schema-reference.md delete mode 100644 docs/sources/developers/kinds/core/publicdashboard/schema-reference.md delete mode 100644 docs/sources/developers/kinds/core/role/schema-reference.md delete mode 100644 docs/sources/developers/kinds/core/rolebinding/schema-reference.md delete mode 100644 docs/sources/developers/kinds/core/serviceaccount/schema-reference.md delete mode 100644 docs/sources/developers/kinds/core/team/schema-reference.md delete mode 100644 docs/sources/developers/kinds/maturity.md delete mode 100644 pkg/codegen/jenny_docs.go delete mode 100644 pkg/codegen/tmpl/docs.tmpl delete mode 100644 pkg/kindsysreport/attributes.go delete mode 100644 pkg/kindsysreport/codegen/report.go delete mode 100644 pkg/kindsysreport/codegen/report.json delete mode 100644 pkg/kindsysreport/codeowners.go diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 250033509e..1141c2058b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -271,7 +271,6 @@ /pkg/services/store/ @grafana/grafana-app-platform-squad /pkg/infra/filestorage/ @grafana/grafana-app-platform-squad /pkg/modules/ @grafana/grafana-app-platform-squad -/pkg/kindsysreport/ @grafana/grafana-app-platform-squad /pkg/services/grpcserver/ @grafana/grafana-app-platform-squad /pkg/generated @grafana/grafana-app-platform-squad diff --git a/.prettierignore b/.prettierignore index 524e01209c..160926fd71 100644 --- a/.prettierignore +++ b/.prettierignore @@ -32,11 +32,5 @@ public/api-merged.json public/api-enterprise-spec.json public/openapi3.json -# Generated Kinds report -kinds/report.json - -# Generated schema docs -docs/sources/developers/kinds/ - # Crowdin files public/locales/**/*.json diff --git a/Makefile b/Makefile index cd49a42305..3a0645364e 100644 --- a/Makefile +++ b/Makefile @@ -106,7 +106,6 @@ gen-cue: ## Do all CUE/Thema code generation go generate ./pkg/plugins/plugindef go generate ./kinds/gen.go go generate ./public/app/plugins/gen.go - go generate ./pkg/kindsysreport/codegen/report.go gen-go: $(WIRE) @echo "generate go files" diff --git a/docs/sources/developers/kinds/_index.md b/docs/sources/developers/kinds/_index.md deleted file mode 100644 index de9fa0e3fb..0000000000 --- a/docs/sources/developers/kinds/_index.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -_build: - list: false -labels: - products: - - enterprise - - oss -title: Grafana schema -weight: 200 ---- - -# Grafana schema - -> Grafana’s schemas, kind system and related code generation are in active development. - -Grafana is moving to a schema-centric model of development, where schemas are the single source of truth that specify -the shape of objects - for example, dashboards, datasources, users - in the frontend, backend, and plugin code. -Eventually, all of Grafana’s object types will be schematized within the “Kind System.” Kinds, their schemas, the Kind -system rules, and associated code generators will collectively provide a clear, consistent foundation for Grafana’s -APIs, documentation, persistent storage, clients, as-code tooling, and so forth. - -It’s exciting to imagine the possibilities that a crisp, consistent development workflow will enable - this is why -companies build [developer platforms](https://internaldeveloperplatform.org/)! At the same time, it’s also -overwhelming - any schema system that can meet Grafana’s complex requirements will necessarily have a lot of moving -parts. Additionally, we must account for having Grafana continue to work as we make the transition - a prerequisite -for every large-scale refactoring. - -In the Grafana ecosystem, there are three basic Kind categories and associated schema categories: - -- [Core Kinds]({{< relref "core/" >}}) -- Custom Kinds -- [Composable Kinds]({{< relref "composable/" >}}) - -The schema authoring workflow for each varies, as does the path to maturity. -[Grafana Kinds - From Zero to Maturity]({{< relref "maturity/" >}}) contains general reference material applicable to -all Kind-writing, and links to the guides for each category of Kind. diff --git a/docs/sources/developers/kinds/composable/_index.md b/docs/sources/developers/kinds/composable/_index.md deleted file mode 100644 index 14e40cd6a3..0000000000 --- a/docs/sources/developers/kinds/composable/_index.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -labels: - products: - - enterprise - - oss -title: Composable kinds -weight: 200 ---- - -# Grafana composable kinds - -{{< section >}} diff --git a/docs/sources/developers/kinds/composable/alertgroups/panelcfg/schema-reference.md b/docs/sources/developers/kinds/composable/alertgroups/panelcfg/schema-reference.md deleted file mode 100644 index 8d69846cf2..0000000000 --- a/docs/sources/developers/kinds/composable/alertgroups/panelcfg/schema-reference.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -keywords: - - grafana - - schema -labels: - products: - - cloud - - enterprise - - oss -title: AlertGroupsPanelCfg kind ---- -> Both documentation generation and kinds schemas are in active development and subject to change without prior notice. - -## AlertGroupsPanelCfg - -#### Maturity: [merged](../../../maturity/#merged) -#### Version: 0.0 - - - -| Property | Type | Required | Default | Description | -|-----------|--------------------|----------|---------|-------------| -| `Options` | [object](#options) | **Yes** | | | - -### Options - -| Property | Type | Required | Default | Description | -|----------------|---------|----------|---------|-------------------------------------------------------------| -| `alertmanager` | string | **Yes** | | Name of the alertmanager used as a source for alerts | -| `expandAll` | boolean | **Yes** | | Expand all alert groups by default | -| `labels` | string | **Yes** | | Comma-separated list of values used to filter alert results | - - diff --git a/docs/sources/developers/kinds/composable/annotationslist/panelcfg/schema-reference.md b/docs/sources/developers/kinds/composable/annotationslist/panelcfg/schema-reference.md deleted file mode 100644 index eb739f4509..0000000000 --- a/docs/sources/developers/kinds/composable/annotationslist/panelcfg/schema-reference.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -keywords: - - grafana - - schema -labels: - products: - - cloud - - enterprise - - oss -title: AnnotationsListPanelCfg kind ---- -> Both documentation generation and kinds schemas are in active development and subject to change without prior notice. - -## AnnotationsListPanelCfg - -#### Maturity: [experimental](../../../maturity/#experimental) -#### Version: 0.0 - - - -| Property | Type | Required | Default | Description | -|-----------|--------------------|----------|---------|-------------| -| `Options` | [object](#options) | **Yes** | | | - -### Options - -| Property | Type | Required | Default | Description | -|-------------------------|----------|----------|---------|-------------| -| `limit` | uint32 | **Yes** | `10` | | -| `navigateAfter` | string | **Yes** | `10m` | | -| `navigateBefore` | string | **Yes** | `10m` | | -| `navigateToPanel` | boolean | **Yes** | `true` | | -| `onlyFromThisDashboard` | boolean | **Yes** | `false` | | -| `onlyInTimeRange` | boolean | **Yes** | `false` | | -| `showTags` | boolean | **Yes** | `true` | | -| `showTime` | boolean | **Yes** | `true` | | -| `showUser` | boolean | **Yes** | `true` | | -| `tags` | string[] | **Yes** | | | - - diff --git a/docs/sources/developers/kinds/composable/azuremonitor/dataquery/schema-reference.md b/docs/sources/developers/kinds/composable/azuremonitor/dataquery/schema-reference.md deleted file mode 100644 index 7c8f824177..0000000000 --- a/docs/sources/developers/kinds/composable/azuremonitor/dataquery/schema-reference.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -keywords: - - grafana - - schema -labels: - products: - - cloud - - enterprise - - oss -title: AzureMonitorDataQuery kind ---- -> Both documentation generation and kinds schemas are in active development and subject to change without prior notice. - -## AzureMonitorDataQuery - -#### Maturity: [merged](../../../maturity/#merged) -#### Version: 0.0 - - - -| Property | Type | Required | Default | Description | -|----------|------|----------|---------|-------------| - - diff --git a/docs/sources/developers/kinds/composable/barchart/panelcfg/schema-reference.md b/docs/sources/developers/kinds/composable/barchart/panelcfg/schema-reference.md deleted file mode 100644 index 2a3e4d43c7..0000000000 --- a/docs/sources/developers/kinds/composable/barchart/panelcfg/schema-reference.md +++ /dev/null @@ -1,183 +0,0 @@ ---- -keywords: - - grafana - - schema -labels: - products: - - cloud - - enterprise - - oss -title: BarChartPanelCfg kind ---- -> Both documentation generation and kinds schemas are in active development and subject to change without prior notice. - -## BarChartPanelCfg - -#### Maturity: [experimental](../../../maturity/#experimental) -#### Version: 0.0 - - - -| Property | Type | Required | Default | Description | -|---------------|------------------------|----------|---------|-------------| -| `FieldConfig` | [object](#fieldconfig) | **Yes** | | | -| `Options` | [object](#options) | **Yes** | | | - -### FieldConfig - -It extends [AxisConfig](#axisconfig) and [HideableFieldConfig](#hideablefieldconfig). - -| Property | Type | Required | Default | Description | -|---------------------|-----------------------------------------------------------|----------|---------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `axisBorderShow` | boolean | No | | *(Inherited from [AxisConfig](#axisconfig))* | -| `axisCenteredZero` | boolean | No | | *(Inherited from [AxisConfig](#axisconfig))* | -| `axisColorMode` | string | No | | *(Inherited from [AxisConfig](#axisconfig))*
TODO docs
Possible values are: `text`, `series`. | -| `axisGridShow` | boolean | No | | *(Inherited from [AxisConfig](#axisconfig))* | -| `axisLabel` | string | No | | *(Inherited from [AxisConfig](#axisconfig))* | -| `axisPlacement` | string | No | | *(Inherited from [AxisConfig](#axisconfig))*
TODO docs
Possible values are: `auto`, `top`, `right`, `bottom`, `left`, `hidden`. | -| `axisSoftMax` | number | No | | *(Inherited from [AxisConfig](#axisconfig))* | -| `axisSoftMin` | number | No | | *(Inherited from [AxisConfig](#axisconfig))* | -| `axisWidth` | number | No | | *(Inherited from [AxisConfig](#axisconfig))* | -| `fillOpacity` | integer | No | `80` | Controls the fill opacity of the bars.
Constraint: `>=0 & <=100`. | -| `gradientMode` | string | No | | Set the mode of the gradient fill. Fill gradient is based on the line color. To change the color, use the standard color scheme field option.
Gradient appearance is influenced by the Fill opacity setting. | -| `hideFrom` | [HideSeriesConfig](#hideseriesconfig) | No | | *(Inherited from [HideableFieldConfig](#hideablefieldconfig))*
TODO docs | -| `lineWidth` | integer | No | `1` | Controls line width of the bars.
Constraint: `>=0 & <=10`. | -| `scaleDistribution` | [ScaleDistributionConfig](#scaledistributionconfig) | No | | *(Inherited from [AxisConfig](#axisconfig))*
TODO docs | -| `thresholdsStyle` | [GraphThresholdsStyleConfig](#graphthresholdsstyleconfig) | No | | TODO docs | - -### AxisConfig - -TODO docs - -| Property | Type | Required | Default | Description | -|---------------------|-----------------------------------------------------|----------|---------|----------------------------------------------------------------------------------------| -| `axisBorderShow` | boolean | No | | | -| `axisCenteredZero` | boolean | No | | | -| `axisColorMode` | string | No | | TODO docs
Possible values are: `text`, `series`. | -| `axisGridShow` | boolean | No | | | -| `axisLabel` | string | No | | | -| `axisPlacement` | string | No | | TODO docs
Possible values are: `auto`, `top`, `right`, `bottom`, `left`, `hidden`. | -| `axisSoftMax` | number | No | | | -| `axisSoftMin` | number | No | | | -| `axisWidth` | number | No | | | -| `scaleDistribution` | [ScaleDistributionConfig](#scaledistributionconfig) | No | | TODO docs | - -### ScaleDistributionConfig - -TODO docs - -| Property | Type | Required | Default | Description | -|-------------------|--------|----------|---------|--------------------------------------------------------------------------| -| `type` | string | **Yes** | | TODO docs
Possible values are: `linear`, `log`, `ordinal`, `symlog`. | -| `linearThreshold` | number | No | | | -| `log` | number | No | | | - -### GraphThresholdsStyleConfig - -TODO docs - -| Property | Type | Required | Default | Description | -|----------|--------|----------|---------|-----------------------------------------------------------------------------------------------------------| -| `mode` | string | **Yes** | | TODO docs
Possible values are: `off`, `line`, `dashed`, `area`, `line+area`, `dashed+area`, `series`. | - -### HideSeriesConfig - -TODO docs - -| Property | Type | Required | Default | Description | -|-----------|---------|----------|---------|-------------| -| `legend` | boolean | **Yes** | | | -| `tooltip` | boolean | **Yes** | | | -| `viz` | boolean | **Yes** | | | - -### HideableFieldConfig - -TODO docs - -| Property | Type | Required | Default | Description | -|------------|---------------------------------------|----------|---------|-------------| -| `hideFrom` | [HideSeriesConfig](#hideseriesconfig) | No | | TODO docs | - -### Options - -It extends [OptionsWithLegend](#optionswithlegend) and [OptionsWithTooltip](#optionswithtooltip) and [OptionsWithTextFormatting](#optionswithtextformatting). - -| Property | Type | Required | Default | Description | -|-----------------------|-------------------------------------------------|----------|---------|------------------------------------------------------------------------------------------------------------------| -| `barWidth` | number | **Yes** | `0.97` | Controls the width of bars. 1 = Max width, 0 = Min width.
Constraint: `>=0 & <=1`. | -| `fullHighlight` | boolean | **Yes** | `false` | Enables mode which highlights the entire bar area and shows tooltip when cursor
hovers over highlighted area | -| `groupWidth` | number | **Yes** | `0.7` | Controls the width of groups. 1 = max with, 0 = min width.
Constraint: `>=0 & <=1`. | -| `legend` | [VizLegendOptions](#vizlegendoptions) | **Yes** | | *(Inherited from [OptionsWithLegend](#optionswithlegend))*
TODO docs | -| `orientation` | string | **Yes** | | Controls the orientation of the bar chart, either vertical or horizontal. | -| `showValue` | string | **Yes** | | This controls whether values are shown on top or to the left of bars. | -| `stacking` | string | **Yes** | | Controls whether bars are stacked or not, either normally or in percent mode. | -| `tooltip` | [VizTooltipOptions](#viztooltipoptions) | **Yes** | | *(Inherited from [OptionsWithTooltip](#optionswithtooltip))*
TODO docs | -| `xTickLabelMaxLength` | integer | **Yes** | | Sets the max length that a label can have before it is truncated.
Constraint: `>=0 & <=2147483647`. | -| `xTickLabelRotation` | integer | **Yes** | `0` | Controls the rotation of the x axis labels.
Constraint: `>=-90 & <=90`. | -| `barRadius` | number | No | `0` | Controls the radius of each bar.
Constraint: `>=0 & <=0.5`. | -| `colorByField` | string | No | | Use the color value for a sibling field to color each bar value. | -| `text` | [VizTextDisplayOptions](#viztextdisplayoptions) | No | | *(Inherited from [OptionsWithTextFormatting](#optionswithtextformatting))*
TODO docs | -| `xField` | string | No | | Manually select which field from the dataset to represent the x field. | -| `xTickLabelSpacing` | int32 | No | `0` | Controls the spacing between x axis labels.
negative values indicate backwards skipping behavior | - -### OptionsWithLegend - -TODO docs - -| Property | Type | Required | Default | Description | -|----------|---------------------------------------|----------|---------|-------------| -| `legend` | [VizLegendOptions](#vizlegendoptions) | **Yes** | | TODO docs | - -### VizLegendOptions - -TODO docs - -| Property | Type | Required | Default | Description | -|---------------|----------|----------|---------|-----------------------------------------------------------------------------------------------------------------------------------------| -| `calcs` | string[] | **Yes** | | | -| `displayMode` | string | **Yes** | | TODO docs
Note: "hidden" needs to remain as an option for plugins compatibility
Possible values are: `list`, `table`, `hidden`. | -| `placement` | string | **Yes** | | TODO docs
Possible values are: `bottom`, `right`. | -| `showLegend` | boolean | **Yes** | | | -| `asTable` | boolean | No | | | -| `isVisible` | boolean | No | | | -| `sortBy` | string | No | | | -| `sortDesc` | boolean | No | | | -| `width` | number | No | | | - -### OptionsWithTextFormatting - -TODO docs - -| Property | Type | Required | Default | Description | -|----------|-------------------------------------------------|----------|---------|-------------| -| `text` | [VizTextDisplayOptions](#viztextdisplayoptions) | No | | TODO docs | - -### VizTextDisplayOptions - -TODO docs - -| Property | Type | Required | Default | Description | -|-------------|--------|----------|---------|--------------------------| -| `titleSize` | number | No | | Explicit title text size | -| `valueSize` | number | No | | Explicit value text size | - -### OptionsWithTooltip - -TODO docs - -| Property | Type | Required | Default | Description | -|-----------|-----------------------------------------|----------|---------|-------------| -| `tooltip` | [VizTooltipOptions](#viztooltipoptions) | **Yes** | | TODO docs | - -### VizTooltipOptions - -TODO docs - -| Property | Type | Required | Default | Description | -|-------------|--------|----------|---------|---------------------------------------------------------------| -| `mode` | string | **Yes** | | TODO docs
Possible values are: `single`, `multi`, `none`. | -| `sort` | string | **Yes** | | TODO docs
Possible values are: `asc`, `desc`, `none`. | -| `maxHeight` | number | No | | | -| `maxWidth` | number | No | | | - - diff --git a/docs/sources/developers/kinds/composable/bargauge/panelcfg/schema-reference.md b/docs/sources/developers/kinds/composable/bargauge/panelcfg/schema-reference.md deleted file mode 100644 index db7bc0e5c0..0000000000 --- a/docs/sources/developers/kinds/composable/bargauge/panelcfg/schema-reference.md +++ /dev/null @@ -1,83 +0,0 @@ ---- -keywords: - - grafana - - schema -labels: - products: - - cloud - - enterprise - - oss -title: BarGaugePanelCfg kind ---- -> Both documentation generation and kinds schemas are in active development and subject to change without prior notice. - -## BarGaugePanelCfg - -#### Maturity: [experimental](../../../maturity/#experimental) -#### Version: 0.0 - - - -| Property | Type | Required | Default | Description | -|-----------|--------------------|----------|---------|-------------| -| `Options` | [object](#options) | **Yes** | | | - -### Options - -It extends [SingleStatBaseOptions](#singlestatbaseoptions). - -| Property | Type | Required | Default | Description | -|-----------------|-------------------------------------------------|----------|---------|-----------------------------------------------------------------------------------------------------------------------------------------------| -| `displayMode` | string | **Yes** | | Enum expressing the possible display modes
for the bar gauge component of Grafana UI
Possible values are: `basic`, `lcd`, `gradient`. | -| `maxVizHeight` | uint32 | **Yes** | `300` | | -| `minVizHeight` | uint32 | **Yes** | `16` | | -| `minVizWidth` | uint32 | **Yes** | `8` | | -| `namePlacement` | string | **Yes** | | Allows for the bar gauge name to be placed explicitly
Possible values are: `auto`, `top`, `left`. | -| `showUnfilled` | boolean | **Yes** | `true` | | -| `sizing` | string | **Yes** | | Allows for the bar gauge size to be set explicitly
Possible values are: `auto`, `manual`. | -| `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 | - -### ReduceDataOptions - -TODO docs - -| Property | Type | Required | Default | Description | -|----------|----------|----------|---------|---------------------------------------------------------------| -| `calcs` | string[] | **Yes** | | When !values, pick one value for the whole field | -| `fields` | string | No | | Which fields to show. By default this is only numeric fields | -| `limit` | number | No | | if showing all values limit | -| `values` | boolean | No | | If true show each row value | - -### SingleStatBaseOptions - -TODO docs - -It extends [OptionsWithTextFormatting](#optionswithtextformatting). - -| Property | Type | Required | Default | Description | -|-----------------|-------------------------------------------------|----------|---------|------------------------------------------------------------------------------------------| -| `orientation` | string | **Yes** | | TODO docs
Possible values are: `auto`, `vertical`, `horizontal`. | -| `reduceOptions` | [ReduceDataOptions](#reducedataoptions) | **Yes** | | TODO docs | -| `text` | [VizTextDisplayOptions](#viztextdisplayoptions) | No | | *(Inherited from [OptionsWithTextFormatting](#optionswithtextformatting))*
TODO docs | - -### OptionsWithTextFormatting - -TODO docs - -| Property | Type | Required | Default | Description | -|----------|-------------------------------------------------|----------|---------|-------------| -| `text` | [VizTextDisplayOptions](#viztextdisplayoptions) | No | | TODO docs | - -### VizTextDisplayOptions - -TODO docs - -| Property | Type | Required | Default | Description | -|-------------|--------|----------|---------|--------------------------| -| `titleSize` | number | No | | Explicit title text size | -| `valueSize` | number | No | | Explicit value text size | - - diff --git a/docs/sources/developers/kinds/composable/candlestick/panelcfg/schema-reference.md b/docs/sources/developers/kinds/composable/candlestick/panelcfg/schema-reference.md deleted file mode 100644 index db0bbe5188..0000000000 --- a/docs/sources/developers/kinds/composable/candlestick/panelcfg/schema-reference.md +++ /dev/null @@ -1,277 +0,0 @@ ---- -keywords: - - grafana - - schema -labels: - products: - - cloud - - enterprise - - oss -title: CandlestickPanelCfg kind ---- -> Both documentation generation and kinds schemas are in active development and subject to change without prior notice. - -## CandlestickPanelCfg - -#### Maturity: [experimental](../../../maturity/#experimental) -#### Version: 0.0 - - - -| Property | Type | Required | Default | Description | -|-----------------------|---------------------------------------|----------|---------|-------------------------------------------------------------| -| `CandleStyle` | string | **Yes** | | Possible values are: `candles`, `ohlcbars`. | -| `CandlestickColors` | [object](#candlestickcolors) | **Yes** | | | -| `CandlestickFieldMap` | [object](#candlestickfieldmap) | **Yes** | | | -| `ColorStrategy` | string | **Yes** | | Possible values are: `open-close`, `close-close`. | -| `FieldConfig` | [GraphFieldConfig](#graphfieldconfig) | **Yes** | | TODO docs | -| `Options` | [object](#options) | **Yes** | | | -| `VizDisplayMode` | string | **Yes** | | Possible values are: `candles+volume`, `candles`, `volume`. | - -### CandlestickColors - -| Property | Type | Required | Default | Description | -|----------|--------|----------|---------|-------------| -| `down` | string | **Yes** | `red` | | -| `flat` | string | **Yes** | `gray` | | -| `up` | string | **Yes** | `green` | | - -### CandlestickFieldMap - -| Property | Type | Required | Default | Description | -|----------|--------|----------|---------|------------------------------------------------------------------------------| -| `close` | string | No | | Corresponds to the final (end) value of the given period | -| `high` | string | No | | Corresponds to the highest value of the given period | -| `low` | string | No | | Corresponds to the lowest value of the given period | -| `open` | string | No | | Corresponds to the starting value of the given period | -| `volume` | string | No | | Corresponds to the sample count in the given period. (e.g. number of trades) | - -### GraphFieldConfig - -TODO docs - -It extends [LineConfig](#lineconfig) and [FillConfig](#fillconfig) and [PointsConfig](#pointsconfig) and [AxisConfig](#axisconfig) and [BarConfig](#barconfig) and [StackableFieldConfig](#stackablefieldconfig) and [HideableFieldConfig](#hideablefieldconfig). - -| Property | Type | Required | Default | Description | -|---------------------|-----------------------------------------------------------|----------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `axisBorderShow` | boolean | No | | *(Inherited from [AxisConfig](#axisconfig))* | -| `axisCenteredZero` | boolean | No | | *(Inherited from [AxisConfig](#axisconfig))* | -| `axisColorMode` | string | No | | *(Inherited from [AxisConfig](#axisconfig))*
TODO docs
Possible values are: `text`, `series`. | -| `axisGridShow` | boolean | No | | *(Inherited from [AxisConfig](#axisconfig))* | -| `axisLabel` | string | No | | *(Inherited from [AxisConfig](#axisconfig))* | -| `axisPlacement` | string | No | | *(Inherited from [AxisConfig](#axisconfig))*
TODO docs
Possible values are: `auto`, `top`, `right`, `bottom`, `left`, `hidden`. | -| `axisSoftMax` | number | No | | *(Inherited from [AxisConfig](#axisconfig))* | -| `axisSoftMin` | number | No | | *(Inherited from [AxisConfig](#axisconfig))* | -| `axisWidth` | number | No | | *(Inherited from [AxisConfig](#axisconfig))* | -| `barAlignment` | integer | No | | *(Inherited from [BarConfig](#barconfig))*
TODO docs
Possible values are: `-1`, `0`, `1`. | -| `barMaxWidth` | number | No | | *(Inherited from [BarConfig](#barconfig))* | -| `barWidthFactor` | number | No | | *(Inherited from [BarConfig](#barconfig))* | -| `drawStyle` | string | No | | TODO docs
Possible values are: `line`, `bars`, `points`. | -| `fillBelowTo` | string | No | | *(Inherited from [FillConfig](#fillconfig))* | -| `fillColor` | string | No | | *(Inherited from [FillConfig](#fillconfig))* | -| `fillOpacity` | number | No | | *(Inherited from [FillConfig](#fillconfig))* | -| `gradientMode` | string | No | | TODO docs
Possible values are: `none`, `opacity`, `hue`, `scheme`. | -| `hideFrom` | [HideSeriesConfig](#hideseriesconfig) | No | | *(Inherited from [HideableFieldConfig](#hideablefieldconfig))*
TODO docs | -| `lineColor` | string | No | | *(Inherited from [LineConfig](#lineconfig))* | -| `lineInterpolation` | string | No | | *(Inherited from [LineConfig](#lineconfig))*
TODO docs
Possible values are: `linear`, `smooth`, `stepBefore`, `stepAfter`. | -| `lineStyle` | [LineStyle](#linestyle) | No | | *(Inherited from [LineConfig](#lineconfig))*
TODO docs | -| `lineWidth` | number | No | | *(Inherited from [LineConfig](#lineconfig))* | -| `pointColor` | string | No | | *(Inherited from [PointsConfig](#pointsconfig))* | -| `pointSize` | number | No | | *(Inherited from [PointsConfig](#pointsconfig))* | -| `pointSymbol` | string | No | | *(Inherited from [PointsConfig](#pointsconfig))* | -| `scaleDistribution` | [ScaleDistributionConfig](#scaledistributionconfig) | No | | *(Inherited from [AxisConfig](#axisconfig))*
TODO docs | -| `showPoints` | string | No | | *(Inherited from [PointsConfig](#pointsconfig))*
TODO docs
Possible values are: `auto`, `never`, `always`. | -| `spanNulls` | | No | | *(Inherited from [LineConfig](#lineconfig))*
Indicate if null values should be treated as gaps or connected.
When the value is a number, it represents the maximum delta in the
X axis that should be considered connected. For timeseries, this is milliseconds | -| `stacking` | [StackingConfig](#stackingconfig) | No | | *(Inherited from [StackableFieldConfig](#stackablefieldconfig))*
TODO docs | -| `thresholdsStyle` | [GraphThresholdsStyleConfig](#graphthresholdsstyleconfig) | No | | TODO docs | -| `transform` | string | No | | TODO docs
Possible values are: `constant`, `negative-Y`. | - -### AxisConfig - -TODO docs - -| Property | Type | Required | Default | Description | -|---------------------|-----------------------------------------------------|----------|---------|----------------------------------------------------------------------------------------| -| `axisBorderShow` | boolean | No | | | -| `axisCenteredZero` | boolean | No | | | -| `axisColorMode` | string | No | | TODO docs
Possible values are: `text`, `series`. | -| `axisGridShow` | boolean | No | | | -| `axisLabel` | string | No | | | -| `axisPlacement` | string | No | | TODO docs
Possible values are: `auto`, `top`, `right`, `bottom`, `left`, `hidden`. | -| `axisSoftMax` | number | No | | | -| `axisSoftMin` | number | No | | | -| `axisWidth` | number | No | | | -| `scaleDistribution` | [ScaleDistributionConfig](#scaledistributionconfig) | No | | TODO docs | - -### ScaleDistributionConfig - -TODO docs - -| Property | Type | Required | Default | Description | -|-------------------|--------|----------|---------|--------------------------------------------------------------------------| -| `type` | string | **Yes** | | TODO docs
Possible values are: `linear`, `log`, `ordinal`, `symlog`. | -| `linearThreshold` | number | No | | | -| `log` | number | No | | | - -### BarConfig - -TODO docs - -| Property | Type | Required | Default | Description | -|------------------|---------|----------|---------|----------------------------------------------------| -| `barAlignment` | integer | No | | TODO docs
Possible values are: `-1`, `0`, `1`. | -| `barMaxWidth` | number | No | | | -| `barWidthFactor` | number | No | | | - -### FillConfig - -TODO docs - -| Property | Type | Required | Default | Description | -|---------------|--------|----------|---------|-------------| -| `fillBelowTo` | string | No | | | -| `fillColor` | string | No | | | -| `fillOpacity` | number | No | | | - -### GraphThresholdsStyleConfig - -TODO docs - -| Property | Type | Required | Default | Description | -|----------|--------|----------|---------|-----------------------------------------------------------------------------------------------------------| -| `mode` | string | **Yes** | | TODO docs
Possible values are: `off`, `line`, `dashed`, `area`, `line+area`, `dashed+area`, `series`. | - -### HideSeriesConfig - -TODO docs - -| Property | Type | Required | Default | Description | -|-----------|---------|----------|---------|-------------| -| `legend` | boolean | **Yes** | | | -| `tooltip` | boolean | **Yes** | | | -| `viz` | boolean | **Yes** | | | - -### HideableFieldConfig - -TODO docs - -| Property | Type | Required | Default | Description | -|------------|---------------------------------------|----------|---------|-------------| -| `hideFrom` | [HideSeriesConfig](#hideseriesconfig) | No | | TODO docs | - -### LineConfig - -TODO docs - -| Property | Type | Required | Default | Description | -|---------------------|-------------------------|----------|---------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `lineColor` | string | No | | | -| `lineInterpolation` | string | No | | TODO docs
Possible values are: `linear`, `smooth`, `stepBefore`, `stepAfter`. | -| `lineStyle` | [LineStyle](#linestyle) | No | | TODO docs | -| `lineWidth` | number | No | | | -| `spanNulls` | | No | | Indicate if null values should be treated as gaps or connected.
When the value is a number, it represents the maximum delta in the
X axis that should be considered connected. For timeseries, this is milliseconds | - -### LineStyle - -TODO docs - -| Property | Type | Required | Default | Description | -|----------|----------|----------|---------|--------------------------------------------------------| -| `dash` | number[] | No | | | -| `fill` | string | No | | Possible values are: `solid`, `dash`, `dot`, `square`. | - -### PointsConfig - -TODO docs - -| Property | Type | Required | Default | Description | -|---------------|--------|----------|---------|---------------------------------------------------------------| -| `pointColor` | string | No | | | -| `pointSize` | number | No | | | -| `pointSymbol` | string | No | | | -| `showPoints` | string | No | | TODO docs
Possible values are: `auto`, `never`, `always`. | - -### StackableFieldConfig - -TODO docs - -| Property | Type | Required | Default | Description | -|------------|-----------------------------------|----------|---------|-------------| -| `stacking` | [StackingConfig](#stackingconfig) | No | | TODO docs | - -### StackingConfig - -TODO docs - -| Property | Type | Required | Default | Description | -|----------|--------|----------|---------|-----------------------------------------------------------------| -| `group` | string | No | | | -| `mode` | string | No | | TODO docs
Possible values are: `none`, `normal`, `percent`. | - -### Options - -It extends [OptionsWithLegend](#optionswithlegend) and [OptionsWithTooltip](#optionswithtooltip). - -| Property | Type | Required | Default | Description | -|--------------------|-----------------------------------------|----------|---------|----------------------------------------------------------------------------| -| `candleStyle` | string | **Yes** | | Sets the style of the candlesticks | -| `colorStrategy` | string | **Yes** | | Sets the color strategy for the candlesticks | -| `colors` | [CandlestickColors](#candlestickcolors) | **Yes** | | | -| `fields` | [object](#fields) | **Yes** | `map[]` | Map fields to appropriate dimension | -| `legend` | [VizLegendOptions](#vizlegendoptions) | **Yes** | | *(Inherited from [OptionsWithLegend](#optionswithlegend))*
TODO docs | -| `mode` | string | **Yes** | | Sets which dimensions are used for the visualization | -| `tooltip` | [VizTooltipOptions](#viztooltipoptions) | **Yes** | | *(Inherited from [OptionsWithTooltip](#optionswithtooltip))*
TODO docs | -| `includeAllFields` | boolean | No | `false` | When enabled, all fields will be sent to the graph | - -### OptionsWithLegend - -TODO docs - -| Property | Type | Required | Default | Description | -|----------|---------------------------------------|----------|---------|-------------| -| `legend` | [VizLegendOptions](#vizlegendoptions) | **Yes** | | TODO docs | - -### VizLegendOptions - -TODO docs - -| Property | Type | Required | Default | Description | -|---------------|----------|----------|---------|-----------------------------------------------------------------------------------------------------------------------------------------| -| `calcs` | string[] | **Yes** | | | -| `displayMode` | string | **Yes** | | TODO docs
Note: "hidden" needs to remain as an option for plugins compatibility
Possible values are: `list`, `table`, `hidden`. | -| `placement` | string | **Yes** | | TODO docs
Possible values are: `bottom`, `right`. | -| `showLegend` | boolean | **Yes** | | | -| `asTable` | boolean | No | | | -| `isVisible` | boolean | No | | | -| `sortBy` | string | No | | | -| `sortDesc` | boolean | No | | | -| `width` | number | No | | | - -### OptionsWithTooltip - -TODO docs - -| Property | Type | Required | Default | Description | -|-----------|-----------------------------------------|----------|---------|-------------| -| `tooltip` | [VizTooltipOptions](#viztooltipoptions) | **Yes** | | TODO docs | - -### VizTooltipOptions - -TODO docs - -| Property | Type | Required | Default | Description | -|-------------|--------|----------|---------|---------------------------------------------------------------| -| `mode` | string | **Yes** | | TODO docs
Possible values are: `single`, `multi`, `none`. | -| `sort` | string | **Yes** | | TODO docs
Possible values are: `asc`, `desc`, `none`. | -| `maxHeight` | number | No | | | -| `maxWidth` | number | No | | | - -### Fields - -Map fields to appropriate dimension - -| Property | Type | Required | Default | Description | -|----------|-----------------------------------|----------|---------|-------------| -| `object` | Possible types are: [](#), [](#). | | | - - diff --git a/docs/sources/developers/kinds/composable/canvas/panelcfg/schema-reference.md b/docs/sources/developers/kinds/composable/canvas/panelcfg/schema-reference.md deleted file mode 100644 index ca4647b16f..0000000000 --- a/docs/sources/developers/kinds/composable/canvas/panelcfg/schema-reference.md +++ /dev/null @@ -1,160 +0,0 @@ ---- -keywords: - - grafana - - schema -labels: - products: - - cloud - - enterprise - - oss -title: CanvasPanelCfg kind ---- -> Both documentation generation and kinds schemas are in active development and subject to change without prior notice. - -## CanvasPanelCfg - -#### Maturity: [experimental](../../../maturity/#experimental) -#### Version: 0.0 - - - -| Property | Type | Required | Default | Description | -|-------------------------|----------------------------------|----------|---------|-----------------------------------------------------------------------| -| `BackgroundConfig` | [object](#backgroundconfig) | **Yes** | | | -| `BackgroundImageSize` | string | **Yes** | | Possible values are: `original`, `contain`, `cover`, `fill`, `tile`. | -| `CanvasConnection` | [object](#canvasconnection) | **Yes** | | | -| `CanvasElementOptions` | [object](#canvaselementoptions) | **Yes** | | | -| `ConnectionCoordinates` | [object](#connectioncoordinates) | **Yes** | | | -| `ConnectionPath` | string | **Yes** | | Possible values are: `straight`. | -| `Constraint` | [object](#constraint) | **Yes** | | | -| `HorizontalConstraint` | string | **Yes** | | Possible values are: `left`, `right`, `leftright`, `center`, `scale`. | -| `HttpRequestMethod` | string | **Yes** | | Possible values are: `GET`, `POST`, `PUT`. | -| `LineConfig` | [object](#lineconfig) | **Yes** | | | -| `Options` | [object](#options) | **Yes** | | | -| `Placement` | [object](#placement) | **Yes** | | | -| `VerticalConstraint` | string | **Yes** | | Possible values are: `top`, `bottom`, `topbottom`, `center`, `scale`. | - -### BackgroundConfig - -| Property | Type | Required | Default | Description | -|----------|-----------------------------------------------------|----------|---------|----------------------------------------------------------------------| -| `color` | [ColorDimensionConfig](#colordimensionconfig) | No | | | -| `image` | [ResourceDimensionConfig](#resourcedimensionconfig) | No | | Links to a resource (image/svg path) | -| `size` | string | No | | Possible values are: `original`, `contain`, `cover`, `fill`, `tile`. | - -### ColorDimensionConfig - -It extends [BaseDimensionConfig](#basedimensionconfig). - -| Property | Type | Required | Default | Description | -|----------|--------|----------|---------|--------------------------------------------------------------------------------------------------------------| -| `field` | string | No | | *(Inherited from [BaseDimensionConfig](#basedimensionconfig))*
fixed: T -- will be added by each element | -| `fixed` | string | No | | | - -### BaseDimensionConfig - -| Property | Type | Required | Default | Description | -|----------|--------|----------|---------|-------------------------------------------| -| `field` | string | No | | fixed: T -- will be added by each element | - -### ResourceDimensionConfig - -Links to a resource (image/svg path) - -It extends [BaseDimensionConfig](#basedimensionconfig). - -| Property | Type | Required | Default | Description | -|----------|--------|----------|---------|--------------------------------------------------------------------------------------------------------------| -| `mode` | string | **Yes** | | Possible values are: `fixed`, `field`, `mapping`. | -| `field` | string | No | | *(Inherited from [BaseDimensionConfig](#basedimensionconfig))*
fixed: T -- will be added by each element | -| `fixed` | string | No | | | - -### CanvasConnection - -| Property | Type | Required | Default | Description | -|--------------|-------------------------------------------------|----------|---------|----------------------------------| -| `path` | string | **Yes** | | Possible values are: `straight`. | -| `source` | [ConnectionCoordinates](#connectioncoordinates) | **Yes** | | | -| `target` | [ConnectionCoordinates](#connectioncoordinates) | **Yes** | | | -| `color` | [ColorDimensionConfig](#colordimensionconfig) | No | | | -| `size` | [ScaleDimensionConfig](#scaledimensionconfig) | No | | | -| `targetName` | string | No | | | - -### ConnectionCoordinates - -| Property | Type | Required | Default | Description | -|----------|--------|----------|---------|-------------| -| `x` | number | **Yes** | | | -| `y` | number | **Yes** | | | - -### ScaleDimensionConfig - -It extends [BaseDimensionConfig](#basedimensionconfig). - -| Property | Type | Required | Default | Description | -|----------|--------|----------|---------|--------------------------------------------------------------------------------------------------------------| -| `max` | number | **Yes** | | | -| `min` | number | **Yes** | | | -| `field` | string | No | | *(Inherited from [BaseDimensionConfig](#basedimensionconfig))*
fixed: T -- will be added by each element | -| `fixed` | number | No | | | -| `mode` | string | No | | Possible values are: `linear`, `quad`. | - -### CanvasElementOptions - -| Property | Type | Required | Default | Description | -|---------------|-----------------------------------------|----------|---------|---------------------------------------------------------| -| `name` | string | **Yes** | | | -| `type` | string | **Yes** | | | -| `background` | [BackgroundConfig](#backgroundconfig) | No | | | -| `border` | [LineConfig](#lineconfig) | No | | | -| `config` | | No | | TODO: figure out how to define this (element config(s)) | -| `connections` | [CanvasConnection](#canvasconnection)[] | No | | | -| `constraint` | [Constraint](#constraint) | No | | | -| `placement` | [Placement](#placement) | No | | | - -### Constraint - -| Property | Type | Required | Default | Description | -|--------------|--------|----------|---------|-----------------------------------------------------------------------| -| `horizontal` | string | No | | Possible values are: `left`, `right`, `leftright`, `center`, `scale`. | -| `vertical` | string | No | | Possible values are: `top`, `bottom`, `topbottom`, `center`, `scale`. | - -### LineConfig - -| Property | Type | Required | Default | Description | -|----------|-----------------------------------------------|----------|---------|-------------| -| `color` | [ColorDimensionConfig](#colordimensionconfig) | No | | | -| `width` | number | No | | | - -### Placement - -| Property | Type | Required | Default | Description | -|----------|--------|----------|---------|-------------| -| `bottom` | number | No | | | -| `height` | number | No | | | -| `left` | number | No | | | -| `right` | number | No | | | -| `top` | number | No | | | -| `width` | number | No | | | - -### Options - -| Property | Type | Required | Default | Description | -|---------------------|-----------------|----------|---------|--------------------------------------------------------------------------------------------------------------------------------------| -| `inlineEditing` | boolean | **Yes** | `true` | Enable inline editing | -| `panZoom` | boolean | **Yes** | `true` | Enable pan and zoom | -| `root` | [object](#root) | **Yes** | | The root element of canvas (frame), where all canvas elements are nested
TODO: Figure out how to define a default value for this | -| `showAdvancedTypes` | boolean | **Yes** | `true` | Show all available element types | - -### Root - -The root element of canvas (frame), where all canvas elements are nested -TODO: Figure out how to define a default value for this - -| Property | Type | Required | Default | Description | -|------------|-------------------------------------------------|----------|---------|----------------------------------------------------------------| -| `elements` | [CanvasElementOptions](#canvaselementoptions)[] | **Yes** | | The list of canvas elements attached to the root element | -| `name` | string | **Yes** | | Name of the root element | -| `type` | string | **Yes** | | Type of root element (frame)
Possible values are: `frame`. | - - diff --git a/docs/sources/developers/kinds/composable/cloudwatch/dataquery/schema-reference.md b/docs/sources/developers/kinds/composable/cloudwatch/dataquery/schema-reference.md deleted file mode 100644 index 78cb0d0f47..0000000000 --- a/docs/sources/developers/kinds/composable/cloudwatch/dataquery/schema-reference.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -keywords: - - grafana - - schema -labels: - products: - - cloud - - enterprise - - oss -title: CloudWatchDataQuery kind ---- -> Both documentation generation and kinds schemas are in active development and subject to change without prior notice. - -## CloudWatchDataQuery - -#### Maturity: [experimental](../../../maturity/#experimental) -#### Version: 0.0 - - - -| Property | Type | Required | Default | Description | -|----------|------|----------|---------|-------------| - - diff --git a/docs/sources/developers/kinds/composable/dashboardlist/panelcfg/schema-reference.md b/docs/sources/developers/kinds/composable/dashboardlist/panelcfg/schema-reference.md deleted file mode 100644 index 732b4ea583..0000000000 --- a/docs/sources/developers/kinds/composable/dashboardlist/panelcfg/schema-reference.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -keywords: - - grafana - - schema -labels: - products: - - cloud - - enterprise - - oss -title: DashboardListPanelCfg kind ---- -> Both documentation generation and kinds schemas are in active development and subject to change without prior notice. - -## DashboardListPanelCfg - -#### Maturity: [experimental](../../../maturity/#experimental) -#### Version: 0.0 - - - -| Property | Type | Required | Default | Description | -|-----------|--------------------|----------|---------|-------------| -| `Options` | [object](#options) | **Yes** | | | - -### Options - -| Property | Type | Required | Default | Description | -|----------------------|----------|----------|---------|-----------------------------------------------------------------| -| `includeVars` | boolean | **Yes** | `false` | | -| `keepTime` | boolean | **Yes** | `false` | | -| `maxItems` | integer | **Yes** | `10` | | -| `query` | string | **Yes** | `` | | -| `showHeadings` | boolean | **Yes** | `true` | | -| `showRecentlyViewed` | boolean | **Yes** | `false` | | -| `showSearch` | boolean | **Yes** | `false` | | -| `showStarred` | boolean | **Yes** | `true` | | -| `tags` | string[] | **Yes** | | | -| `folderId` | integer | No | | folderId is deprecated, and migrated to folderUid on panel init | -| `folderUID` | string | No | | | - - diff --git a/docs/sources/developers/kinds/composable/datagrid/panelcfg/schema-reference.md b/docs/sources/developers/kinds/composable/datagrid/panelcfg/schema-reference.md deleted file mode 100644 index adc4c54aec..0000000000 --- a/docs/sources/developers/kinds/composable/datagrid/panelcfg/schema-reference.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -keywords: - - grafana - - schema -labels: - products: - - cloud - - enterprise - - oss -title: DatagridPanelCfg kind ---- -> Both documentation generation and kinds schemas are in active development and subject to change without prior notice. - -## DatagridPanelCfg - -#### Maturity: [experimental](../../../maturity/#experimental) -#### Version: 0.0 - - - -| Property | Type | Required | Default | Description | -|-----------|--------------------|----------|---------|-------------| -| `Options` | [object](#options) | **Yes** | | | - -### Options - -| Property | Type | Required | Default | Description | -|------------------|---------|----------|---------|-----------------------------------| -| `selectedSeries` | integer | **Yes** | `0` | Constraint: `>=0 & <=2147483647`. | - - diff --git a/docs/sources/developers/kinds/composable/debug/panelcfg/schema-reference.md b/docs/sources/developers/kinds/composable/debug/panelcfg/schema-reference.md deleted file mode 100644 index 9083244288..0000000000 --- a/docs/sources/developers/kinds/composable/debug/panelcfg/schema-reference.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -keywords: - - grafana - - schema -labels: - products: - - cloud - - enterprise - - oss -title: DebugPanelCfg kind ---- -> Both documentation generation and kinds schemas are in active development and subject to change without prior notice. - -## DebugPanelCfg - -#### Maturity: [experimental](../../../maturity/#experimental) -#### Version: 0.0 - - - -| Property | Type | Required | Default | Description | -|----------------|-------------------------|----------|---------|---------------------------------------------------------------------------| -| `DebugMode` | string | **Yes** | | Possible values are: `render`, `events`, `cursor`, `State`, `ThrowError`. | -| `Options` | [object](#options) | **Yes** | | | -| `UpdateConfig` | [object](#updateconfig) | **Yes** | | | - -### Options - -| Property | Type | Required | Default | Description | -|------------|-------------------------------|----------|---------|---------------------------------------------------------------------------| -| `mode` | string | **Yes** | | Possible values are: `render`, `events`, `cursor`, `State`, `ThrowError`. | -| `counters` | [UpdateConfig](#updateconfig) | No | | | - -### UpdateConfig - -| Property | Type | Required | Default | Description | -|-----------------|---------|----------|---------|-------------| -| `dataChanged` | boolean | **Yes** | | | -| `render` | boolean | **Yes** | | | -| `schemaChanged` | boolean | **Yes** | | | - - diff --git a/docs/sources/developers/kinds/composable/elasticsearch/dataquery/schema-reference.md b/docs/sources/developers/kinds/composable/elasticsearch/dataquery/schema-reference.md deleted file mode 100644 index 384fe82ee0..0000000000 --- a/docs/sources/developers/kinds/composable/elasticsearch/dataquery/schema-reference.md +++ /dev/null @@ -1,283 +0,0 @@ ---- -keywords: - - grafana - - schema -labels: - products: - - cloud - - enterprise - - oss -title: ElasticsearchDataQuery kind ---- -> Both documentation generation and kinds schemas are in active development and subject to change without prior notice. - -## ElasticsearchDataQuery - -#### Maturity: [experimental](../../../maturity/#experimental) -#### Version: 0.0 - - - -| Property | Type | Required | Default | Description | -|--------------|-------------------------------------------|----------|---------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `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. | -| `alias` | string | No | | Alias pattern | -| `bucketAggs` | [BucketAggregation](#bucketaggregation)[] | No | | List of bucket aggregations | -| `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)
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 | | Specify the query flavor
TODO make this required and give it a default | -| `query` | string | No | | Lucene query | -| `timeField` | string | No | | Name of time field | - -### BucketAggregation - -| Property | Type | Required | Default | Description | -|----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|---------|-------------| -| `object` | Possible types are: [DateHistogram](#datehistogram), [Histogram](#histogram), [Terms](#terms), [Filters](#filters), [GeoHashGrid](#geohashgrid), [Nested](#nested). | | | - -### DateHistogram - -It extends [BucketAggregationWithField](#bucketaggregationwithfield). - -| Property | Type | Required | Default | 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 | Default | 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 | Default | 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 | Default | 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 | Default | 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 | Default | 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 | Default | 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 | Default | 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))* | - -### MetricAggregation - -| Property | Type | Required | Default | Description | -|----------|------------------------------------------------------------------------------------------------------|----------|---------|-------------| -| `object` | Possible types are: [Count](#count), [PipelineMetricAggregation](#pipelinemetricaggregation), [](#). | | | - -### Count - -It extends [BaseMetricAggregation](#basemetricaggregation). - -| Property | Type | Required | Default | Description | -|----------|---------|----------|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `id` | string | **Yes** | | *(Inherited from [BaseMetricAggregation](#basemetricaggregation))* | -| `type` | string | **Yes** | | *(Inherited from [BaseMetricAggregation](#basemetricaggregation))*
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`. | -| `hide` | boolean | No | | *(Inherited from [BaseMetricAggregation](#basemetricaggregation))* | - -### BaseMetricAggregation - -| Property | Type | Required | Default | Description | -|----------|---------|----------|---------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `id` | string | **Yes** | | | -| `type` | string | **Yes** | | 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`. | -| `hide` | boolean | No | | | - -### PipelineMetricAggregation - -| Property | Type | Required | Default | Description | -|----------|-------------------------------------------------------------------------------------------------------------------------------------------------|----------|---------|-------------| -| `object` | Possible types are: [MovingAverage](#movingaverage), [Derivative](#derivative), [CumulativeSum](#cumulativesum), [BucketScript](#bucketscript). | | | - -### BucketScript - -It extends [PipelineMetricAggregationWithMultipleBucketPaths](#pipelinemetricaggregationwithmultiplebucketpaths). - -| Property | Type | Required | Default | Description | -|---------------------|-----------------------------------------|----------|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `type` | string | **Yes** | | *(Inherited from [PipelineMetricAggregationWithMultipleBucketPaths](#pipelinemetricaggregationwithmultiplebucketpaths))*
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`. | -| `hide` | boolean | No | | *(Inherited from [PipelineMetricAggregationWithMultipleBucketPaths](#pipelinemetricaggregationwithmultiplebucketpaths))* | -| `id` | string | No | | *(Inherited from [PipelineMetricAggregationWithMultipleBucketPaths](#pipelinemetricaggregationwithmultiplebucketpaths))* | -| `pipelineVariables` | [PipelineVariable](#pipelinevariable)[] | No | | *(Inherited from [PipelineMetricAggregationWithMultipleBucketPaths](#pipelinemetricaggregationwithmultiplebucketpaths))* | -| `settings` | [object](#settings) | No | | | - -### PipelineMetricAggregationWithMultipleBucketPaths - -It extends [BaseMetricAggregation](#basemetricaggregation). - -| Property | Type | Required | Default | Description | -|---------------------|-----------------------------------------|----------|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `id` | string | **Yes** | | *(Inherited from [BaseMetricAggregation](#basemetricaggregation))* | -| `type` | string | **Yes** | | *(Inherited from [BaseMetricAggregation](#basemetricaggregation))*
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`. | -| `hide` | boolean | No | | *(Inherited from [BaseMetricAggregation](#basemetricaggregation))* | -| `pipelineVariables` | [PipelineVariable](#pipelinevariable)[] | No | | | - -### PipelineVariable - -| Property | Type | Required | Default | Description | -|---------------|--------|----------|---------|-------------| -| `name` | string | **Yes** | | | -| `pipelineAgg` | string | **Yes** | | | - -### Settings - -| Property | Type | Required | Default | Description | -|----------|------|----------|---------|-------------| -| `script` | | No | | | - -### CumulativeSum - -It extends [BasePipelineMetricAggregation](#basepipelinemetricaggregation). - -| Property | Type | Required | Default | Description | -|---------------|---------------------|----------|---------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `type` | string | **Yes** | | *(Inherited from [BasePipelineMetricAggregation](#basepipelinemetricaggregation))*
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 [BasePipelineMetricAggregation](#basepipelinemetricaggregation))* | -| `hide` | boolean | No | | *(Inherited from [BasePipelineMetricAggregation](#basepipelinemetricaggregation))* | -| `id` | string | No | | *(Inherited from [BasePipelineMetricAggregation](#basepipelinemetricaggregation))* | -| `pipelineAgg` | string | No | | *(Inherited from [BasePipelineMetricAggregation](#basepipelinemetricaggregation))* | -| `settings` | [object](#settings) | No | | | - -### BasePipelineMetricAggregation - -It extends [MetricAggregationWithField](#metricaggregationwithfield). - -| Property | Type | Required | Default | 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 | | | - -### MetricAggregationWithField - -It extends [BaseMetricAggregation](#basemetricaggregation). - -| Property | Type | Required | Default | Description | -|----------|---------|----------|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `id` | string | **Yes** | | *(Inherited from [BaseMetricAggregation](#basemetricaggregation))* | -| `type` | string | **Yes** | | *(Inherited from [BaseMetricAggregation](#basemetricaggregation))*
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 | | | -| `hide` | boolean | No | | *(Inherited from [BaseMetricAggregation](#basemetricaggregation))* | - -### Settings - -| Property | Type | Required | Default | Description | -|----------|--------|----------|---------|-------------| -| `format` | string | No | | | - -### Derivative - -It extends [BasePipelineMetricAggregation](#basepipelinemetricaggregation). - -| Property | Type | Required | Default | Description | -|---------------|---------------------|----------|---------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `type` | string | **Yes** | | *(Inherited from [BasePipelineMetricAggregation](#basepipelinemetricaggregation))*
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 [BasePipelineMetricAggregation](#basepipelinemetricaggregation))* | -| `hide` | boolean | No | | *(Inherited from [BasePipelineMetricAggregation](#basepipelinemetricaggregation))* | -| `id` | string | No | | *(Inherited from [BasePipelineMetricAggregation](#basepipelinemetricaggregation))* | -| `pipelineAgg` | string | No | | *(Inherited from [BasePipelineMetricAggregation](#basepipelinemetricaggregation))* | -| `settings` | [object](#settings) | No | | | - -### Settings - -| Property | Type | Required | Default | Description | -|----------|--------|----------|---------|-------------| -| `unit` | string | No | | | - -### MovingAverage - -#MovingAverage's settings are overridden in types.ts - -It extends [BasePipelineMetricAggregation](#basepipelinemetricaggregation). - -| Property | Type | Required | Default | Description | -|---------------|---------------------|----------|---------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `type` | string | **Yes** | | *(Inherited from [BasePipelineMetricAggregation](#basepipelinemetricaggregation))*
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 [BasePipelineMetricAggregation](#basepipelinemetricaggregation))* | -| `hide` | boolean | No | | *(Inherited from [BasePipelineMetricAggregation](#basepipelinemetricaggregation))* | -| `id` | string | No | | *(Inherited from [BasePipelineMetricAggregation](#basepipelinemetricaggregation))* | -| `pipelineAgg` | string | No | | *(Inherited from [BasePipelineMetricAggregation](#basepipelinemetricaggregation))* | -| `settings` | [object](#settings) | No | | | - -### Settings - -| Property | Type | Required | Default | Description | -|----------|------|----------|---------|-------------| - -### Meta - -| Property | Type | Required | Default | Description | -|----------|------|----------|---------|-------------| - -### Settings - -| Property | Type | Required | Default | Description | -|-----------|----------|----------|---------|-------------| -| `metrics` | string[] | No | | | -| `orderBy` | string | No | | | -| `order` | string | No | | | - - diff --git a/docs/sources/developers/kinds/composable/gauge/panelcfg/schema-reference.md b/docs/sources/developers/kinds/composable/gauge/panelcfg/schema-reference.md deleted file mode 100644 index 5cc9eaa99c..0000000000 --- a/docs/sources/developers/kinds/composable/gauge/panelcfg/schema-reference.md +++ /dev/null @@ -1,80 +0,0 @@ ---- -keywords: - - grafana - - schema -labels: - products: - - cloud - - enterprise - - oss -title: GaugePanelCfg kind ---- -> Both documentation generation and kinds schemas are in active development and subject to change without prior notice. - -## GaugePanelCfg - -#### Maturity: [experimental](../../../maturity/#experimental) -#### Version: 0.0 - - - -| Property | Type | Required | Default | Description | -|-----------|--------------------|----------|---------|-------------| -| `Options` | [object](#options) | **Yes** | | | - -### Options - -It extends [SingleStatBaseOptions](#singlestatbaseoptions). - -| Property | Type | Required | Default | Description | -|------------------------|-------------------------------------------------|----------|---------|---------------------------------------------------------------------------------------------------------------------------------------------| -| `minVizHeight` | uint32 | **Yes** | `75` | | -| `minVizWidth` | uint32 | **Yes** | `75` | | -| `showThresholdLabels` | boolean | **Yes** | `false` | | -| `showThresholdMarkers` | boolean | **Yes** | `true` | | -| `sizing` | string | **Yes** | | Allows for the bar gauge size to be set explicitly
Possible values are: `auto`, `manual`. | -| `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 | - -### ReduceDataOptions - -TODO docs - -| Property | Type | Required | Default | Description | -|----------|----------|----------|---------|---------------------------------------------------------------| -| `calcs` | string[] | **Yes** | | When !values, pick one value for the whole field | -| `fields` | string | No | | Which fields to show. By default this is only numeric fields | -| `limit` | number | No | | if showing all values limit | -| `values` | boolean | No | | If true show each row value | - -### SingleStatBaseOptions - -TODO docs - -It extends [OptionsWithTextFormatting](#optionswithtextformatting). - -| Property | Type | Required | Default | Description | -|-----------------|-------------------------------------------------|----------|---------|------------------------------------------------------------------------------------------| -| `orientation` | string | **Yes** | | TODO docs
Possible values are: `auto`, `vertical`, `horizontal`. | -| `reduceOptions` | [ReduceDataOptions](#reducedataoptions) | **Yes** | | TODO docs | -| `text` | [VizTextDisplayOptions](#viztextdisplayoptions) | No | | *(Inherited from [OptionsWithTextFormatting](#optionswithtextformatting))*
TODO docs | - -### OptionsWithTextFormatting - -TODO docs - -| Property | Type | Required | Default | Description | -|----------|-------------------------------------------------|----------|---------|-------------| -| `text` | [VizTextDisplayOptions](#viztextdisplayoptions) | No | | TODO docs | - -### VizTextDisplayOptions - -TODO docs - -| Property | Type | Required | Default | Description | -|-------------|--------|----------|---------|--------------------------| -| `titleSize` | number | No | | Explicit title text size | -| `valueSize` | number | No | | Explicit value text size | - - diff --git a/docs/sources/developers/kinds/composable/geomap/panelcfg/schema-reference.md b/docs/sources/developers/kinds/composable/geomap/panelcfg/schema-reference.md deleted file mode 100644 index cfa0b3c913..0000000000 --- a/docs/sources/developers/kinds/composable/geomap/panelcfg/schema-reference.md +++ /dev/null @@ -1,97 +0,0 @@ ---- -keywords: - - grafana - - schema -labels: - products: - - cloud - - enterprise - - oss -title: GeomapPanelCfg kind ---- -> Both documentation generation and kinds schemas are in active development and subject to change without prior notice. - -## GeomapPanelCfg - -#### Maturity: [experimental](../../../maturity/#experimental) -#### Version: 0.0 - - - -| Property | Type | Required | Default | Description | -|-------------------|----------------------------|----------|---------|-----------------------------------------------| -| `ControlsOptions` | [object](#controlsoptions) | **Yes** | | | -| `MapCenterID` | string | **Yes** | | Possible values are: `zero`, `coords`, `fit`. | -| `MapViewConfig` | [object](#mapviewconfig) | **Yes** | | | -| `Options` | [object](#options) | **Yes** | | | -| `TooltipMode` | string | **Yes** | | Possible values are: `none`, `details`. | -| `TooltipOptions` | [object](#tooltipoptions) | **Yes** | | | - -### ControlsOptions - -| Property | Type | Required | Default | Description | -|-------------------|---------|----------|---------|--------------------------| -| `mouseWheelZoom` | boolean | No | | let the mouse wheel zoom | -| `showAttribution` | boolean | No | | Lower right | -| `showDebug` | boolean | No | | Show debug | -| `showMeasure` | boolean | No | | Show measure | -| `showScale` | boolean | No | | Scale options | -| `showZoom` | boolean | No | | Zoom (upper left) | - -### MapViewConfig - -| Property | Type | Required | Default | Description | -|-------------|---------|----------|---------|-------------| -| `id` | string | **Yes** | `zero` | | -| `allLayers` | boolean | No | `true` | | -| `lastOnly` | boolean | No | | | -| `lat` | int64 | No | `0` | | -| `layer` | string | No | | | -| `lon` | int64 | No | `0` | | -| `maxZoom` | integer | No | | | -| `minZoom` | integer | No | | | -| `padding` | integer | No | | | -| `shared` | boolean | No | | | -| `zoom` | int64 | No | `1` | | - -### Options - -| Property | Type | Required | Default | Description | -|------------|---------------------------------------|----------|---------|-------------| -| `basemap` | [MapLayerOptions](#maplayeroptions) | **Yes** | | | -| `controls` | [ControlsOptions](#controlsoptions) | **Yes** | | | -| `layers` | [MapLayerOptions](#maplayeroptions)[] | **Yes** | | | -| `tooltip` | [TooltipOptions](#tooltipoptions) | **Yes** | | | -| `view` | [MapViewConfig](#mapviewconfig) | **Yes** | | | - -### MapLayerOptions - -| Property | Type | Required | Default | Description | -|--------------|---------------------------------------------|----------|---------|----------------------------------------------------------------------------------------------------------------------------| -| `name` | string | **Yes** | | configured unique display name | -| `type` | string | **Yes** | | | -| `config` | | No | | Custom options depending on the type | -| `filterData` | | No | | Defines a frame MatcherConfig that may filter data for the given layer | -| `location` | [FrameGeometrySource](#framegeometrysource) | No | | | -| `opacity` | integer | No | | Common properties:
https://openlayers.org/en/latest/apidoc/module-ol_layer_Base-BaseLayer.html
Layer opacity (0-1) | -| `tooltip` | boolean | No | | Check tooltip (defaults to true) | - -### FrameGeometrySource - -| Property | Type | Required | Default | Description | -|-------------|--------|----------|---------|-------------------------------------------------------------| -| `mode` | string | **Yes** | | Possible values are: `auto`, `geohash`, `coords`, `lookup`. | -| `gazetteer` | string | No | | Path to Gazetteer | -| `geohash` | string | No | | Field mappings | -| `latitude` | string | No | | | -| `longitude` | string | No | | | -| `lookup` | string | No | | | -| `wkt` | string | No | | | - -### TooltipOptions - -| Property | Type | Required | Default | Description | -|----------|--------|----------|---------|-----------------------------------------| -| `mode` | string | **Yes** | | Possible values are: `none`, `details`. | - - diff --git a/docs/sources/developers/kinds/composable/googlecloudmonitoring/dataquery/schema-reference.md b/docs/sources/developers/kinds/composable/googlecloudmonitoring/dataquery/schema-reference.md deleted file mode 100644 index d8f28ff51d..0000000000 --- a/docs/sources/developers/kinds/composable/googlecloudmonitoring/dataquery/schema-reference.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -keywords: - - grafana - - schema -labels: - products: - - cloud - - enterprise - - oss -title: GoogleCloudMonitoringDataQuery kind ---- -> Both documentation generation and kinds schemas are in active development and subject to change without prior notice. - -## GoogleCloudMonitoringDataQuery - -#### Maturity: [merged](../../../maturity/#merged) -#### Version: 0.0 - - - -| Property | Type | Required | Default | Description | -|----------|------|----------|---------|-------------| - - diff --git a/docs/sources/developers/kinds/composable/grafanapyroscope/dataquery/schema-reference.md b/docs/sources/developers/kinds/composable/grafanapyroscope/dataquery/schema-reference.md deleted file mode 100644 index 3b7a95d56c..0000000000 --- a/docs/sources/developers/kinds/composable/grafanapyroscope/dataquery/schema-reference.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -keywords: - - grafana - - schema -labels: - products: - - cloud - - enterprise - - oss -title: GrafanaPyroscopeDataQuery kind ---- -> Both documentation generation and kinds schemas are in active development and subject to change without prior notice. - -## GrafanaPyroscopeDataQuery - -#### Maturity: [experimental](../../../maturity/#experimental) -#### Version: 0.0 - - - -| Property | Type | Required | Default | Description | -|-----------------|----------|----------|---------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `groupBy` | string[] | **Yes** | | Allows to group the results. | -| `labelSelector` | string | **Yes** | `{}` | Specifies the query label selectors. | -| `profileTypeId` | string | **Yes** | | Specifies the type of profile to query. | -| `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)
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) | -| `maxNodes` | integer | No | | Sets the maximum number of nodes in the flamegraph. | -| `queryType` | string | No | | Specify the query flavor
TODO make this required and give it a default | -| `spanSelector` | string[] | No | | Specifies the query span selectors. | - - diff --git a/docs/sources/developers/kinds/composable/heatmap/panelcfg/schema-reference.md b/docs/sources/developers/kinds/composable/heatmap/panelcfg/schema-reference.md deleted file mode 100644 index 3c704e1226..0000000000 --- a/docs/sources/developers/kinds/composable/heatmap/panelcfg/schema-reference.md +++ /dev/null @@ -1,250 +0,0 @@ ---- -keywords: - - grafana - - schema -labels: - products: - - cloud - - enterprise - - oss -title: HeatmapPanelCfg kind ---- -> Both documentation generation and kinds schemas are in active development and subject to change without prior notice. - -## HeatmapPanelCfg - -#### Maturity: [merged](../../../maturity/#merged) -#### Version: 0.0 - - - -| Property | Type | Required | Default | Description | -|-----------------------|--------------------------------|----------|---------|-------------------------------------------------------------------------------------------| -| `CellValues` | [object](#cellvalues) | **Yes** | | Controls cell value options | -| `ExemplarConfig` | [object](#exemplarconfig) | **Yes** | | Controls exemplar options | -| `FieldConfig` | [object](#fieldconfig) | **Yes** | | | -| `FilterValueRange` | [object](#filtervaluerange) | **Yes** | | Controls the value filter range | -| `HeatmapColorMode` | string | **Yes** | | Controls the color mode of the heatmap
Possible values are: `opacity`, `scheme`. | -| `HeatmapColorOptions` | [object](#heatmapcoloroptions) | **Yes** | | Controls various color options | -| `HeatmapColorScale` | string | **Yes** | | Controls the color scale of the heatmap
Possible values are: `linear`, `exponential`. | -| `HeatmapLegend` | [object](#heatmaplegend) | **Yes** | | Controls legend options | -| `HeatmapTooltip` | [object](#heatmaptooltip) | **Yes** | | Controls tooltip options | -| `Options` | [object](#options) | **Yes** | | | -| `RowsHeatmapOptions` | [object](#rowsheatmapoptions) | **Yes** | | Controls frame rows options | -| `YAxisConfig` | [object](#yaxisconfig) | **Yes** | | Configuration options for the yAxis | - -### CellValues - -Controls cell value options - -| Property | Type | Required | Default | Description | -|------------|--------|----------|---------|-------------------------------------------------| -| `decimals` | number | No | | Controls the number of decimals for cell values | -| `unit` | string | No | | Controls the cell value unit | - -### ExemplarConfig - -Controls exemplar options - -| Property | Type | Required | Default | Description | -|----------|--------|----------|---------|----------------------------------------| -| `color` | string | **Yes** | | Sets the color of the exemplar markers | - -### FieldConfig - -It extends [HideableFieldConfig](#hideablefieldconfig). - -| Property | Type | Required | Default | Description | -|---------------------|-----------------------------------------------------|----------|---------|------------------------------------------------------------------------------| -| `hideFrom` | [HideSeriesConfig](#hideseriesconfig) | No | | *(Inherited from [HideableFieldConfig](#hideablefieldconfig))*
TODO docs | -| `scaleDistribution` | [ScaleDistributionConfig](#scaledistributionconfig) | No | | TODO docs | - -### HideSeriesConfig - -TODO docs - -| Property | Type | Required | Default | Description | -|-----------|---------|----------|---------|-------------| -| `legend` | boolean | **Yes** | | | -| `tooltip` | boolean | **Yes** | | | -| `viz` | boolean | **Yes** | | | - -### HideableFieldConfig - -TODO docs - -| Property | Type | Required | Default | Description | -|------------|---------------------------------------|----------|---------|-------------| -| `hideFrom` | [HideSeriesConfig](#hideseriesconfig) | No | | TODO docs | - -### ScaleDistributionConfig - -TODO docs - -| Property | Type | Required | Default | Description | -|-------------------|--------|----------|---------|--------------------------------------------------------------------------| -| `type` | string | **Yes** | | TODO docs
Possible values are: `linear`, `log`, `ordinal`, `symlog`. | -| `linearThreshold` | number | No | | | -| `log` | number | No | | | - -### FilterValueRange - -Controls the value filter range - -| Property | Type | Required | Default | Description | -|----------|--------|----------|---------|--------------------------------------------------------------------------| -| `ge` | number | No | | Sets the filter range to values greater than or equal to the given value | -| `le` | number | No | | Sets the filter range to values less than or equal to the given value | - -### HeatmapColorOptions - -Controls various color options - -| Property | Type | Required | Default | Description | -|------------|---------|----------|---------|-------------------------------------------------------------------------------------------| -| `exponent` | number | **Yes** | | Controls the exponent when scale is set to exponential | -| `fill` | string | **Yes** | | Controls the color fill when in opacity mode | -| `reverse` | boolean | **Yes** | | Reverses the color scheme | -| `scheme` | string | **Yes** | | Controls the color scheme used | -| `steps` | integer | **Yes** | | Controls the number of color steps
Constraint: `>=2 & <=128`. | -| `max` | number | No | | Sets the maximum value for the color scale | -| `min` | number | No | | Sets the minimum value for the color scale | -| `mode` | string | No | | Controls the color mode of the heatmap
Possible values are: `opacity`, `scheme`. | -| `scale` | string | No | | Controls the color scale of the heatmap
Possible values are: `linear`, `exponential`. | - -### HeatmapLegend - -Controls legend options - -| Property | Type | Required | Default | Description | -|----------|---------|----------|---------|---------------------------------| -| `show` | boolean | **Yes** | | Controls if the legend is shown | - -### HeatmapTooltip - -Controls tooltip options - -| Property | Type | Required | Default | Description | -|------------------|---------|----------|---------|----------------------------------------------------------------| -| `mode` | string | **Yes** | | TODO docs
Possible values are: `single`, `multi`, `none`. | -| `maxHeight` | number | No | | | -| `maxWidth` | number | No | | | -| `showColorScale` | boolean | No | | Controls if the tooltip shows a color scale in header | -| `yHistogram` | boolean | No | | Controls if the tooltip shows a histogram of the y-axis values | - -### Options - -| Property | Type | Required | Default | Description | -|----------------|---------------------------------------------------------|----------|----------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `color` | [object](#color) | **Yes** | `map[exponent:0.5 fill:dark-orange reverse:false scheme:Oranges steps:64]` | Controls the color options | -| `exemplars` | [ExemplarConfig](#exemplarconfig) | **Yes** | | Controls exemplar options | -| `legend` | [HeatmapLegend](#heatmaplegend) | **Yes** | | Controls legend options | -| `showValue` | string | **Yes** | | | *{
layout: ui.HeatmapCellLayout & "auto" // TODO: fix after remove when https://github.com/grafana/cuetsy/issues/74 is fixed
}
Controls the display of the value in the cell | -| `tooltip` | [object](#tooltip) | **Yes** | `map[mode:single showColorScale:false yHistogram:false]` | Controls tooltip options | -| `yAxis` | [YAxisConfig](#yaxisconfig) | **Yes** | | Configuration options for the yAxis | -| `calculate` | boolean | No | `false` | Controls if the heatmap should be calculated from data | -| `calculation` | [HeatmapCalculationOptions](#heatmapcalculationoptions) | No | | | -| `cellGap` | integer | No | `1` | Controls gap between cells
Constraint: `>=0 & <=25`. | -| `cellRadius` | number | No | | Controls cell radius | -| `cellValues` | [object](#cellvalues) | No | `map[]` | Controls cell value unit | -| `filterValues` | [object](#filtervalues) | No | `map[le:1e-09]` | Filters values between a given range | -| `rowsFrame` | [RowsHeatmapOptions](#rowsheatmapoptions) | No | | Controls frame rows options | - -### HeatmapCalculationOptions - -| Property | Type | Required | Default | Description | -|------------|-------------------------------------------------------------------|----------|---------|-------------| -| `xBuckets` | [HeatmapCalculationBucketConfig](#heatmapcalculationbucketconfig) | No | | | -| `yBuckets` | [HeatmapCalculationBucketConfig](#heatmapcalculationbucketconfig) | No | | | - -### HeatmapCalculationBucketConfig - -| Property | Type | Required | Default | Description | -|----------|-----------------------------------------------------|----------|---------|----------------------------------------------------------| -| `mode` | string | No | | Possible values are: `size`, `count`. | -| `scale` | [ScaleDistributionConfig](#scaledistributionconfig) | No | | TODO docs | -| `value` | string | No | | The number of buckets to use for the axis in the heatmap | - -### RowsHeatmapOptions - -Controls frame rows options - -| Property | Type | Required | Default | Description | -|----------|--------|----------|---------|----------------------------------------------------------| -| `layout` | string | No | | Possible values are: `le`, `ge`, `unknown`, `auto`. | -| `value` | string | No | | Sets the name of the cell when not calculating from data | - -### YAxisConfig - -Configuration options for the yAxis - -It extends [AxisConfig](#axisconfig). - -| Property | Type | Required | Default | Description | -|---------------------|-----------------------------------------------------|----------|---------|-----------------------------------------------------------------------------------------------------------------------------------------| -| `axisBorderShow` | boolean | No | | *(Inherited from [AxisConfig](#axisconfig))* | -| `axisCenteredZero` | boolean | No | | *(Inherited from [AxisConfig](#axisconfig))* | -| `axisColorMode` | string | No | | *(Inherited from [AxisConfig](#axisconfig))*
TODO docs
Possible values are: `text`, `series`. | -| `axisGridShow` | boolean | No | | *(Inherited from [AxisConfig](#axisconfig))* | -| `axisLabel` | string | No | | *(Inherited from [AxisConfig](#axisconfig))* | -| `axisPlacement` | string | No | | *(Inherited from [AxisConfig](#axisconfig))*
TODO docs
Possible values are: `auto`, `top`, `right`, `bottom`, `left`, `hidden`. | -| `axisSoftMax` | number | No | | *(Inherited from [AxisConfig](#axisconfig))* | -| `axisSoftMin` | number | No | | *(Inherited from [AxisConfig](#axisconfig))* | -| `axisWidth` | number | No | | *(Inherited from [AxisConfig](#axisconfig))* | -| `decimals` | number | No | | Controls the number of decimals for yAxis values | -| `max` | number | No | | Sets the maximum value for the yAxis | -| `min` | number | No | | Sets the minimum value for the yAxis | -| `reverse` | boolean | No | | Reverses the yAxis | -| `scaleDistribution` | [ScaleDistributionConfig](#scaledistributionconfig) | No | | *(Inherited from [AxisConfig](#axisconfig))*
TODO docs | -| `unit` | string | No | | Sets the yAxis unit | - -### AxisConfig - -TODO docs - -| Property | Type | Required | Default | Description | -|---------------------|-----------------------------------------------------|----------|---------|----------------------------------------------------------------------------------------| -| `axisBorderShow` | boolean | No | | | -| `axisCenteredZero` | boolean | No | | | -| `axisColorMode` | string | No | | TODO docs
Possible values are: `text`, `series`. | -| `axisGridShow` | boolean | No | | | -| `axisLabel` | string | No | | | -| `axisPlacement` | string | No | | TODO docs
Possible values are: `auto`, `top`, `right`, `bottom`, `left`, `hidden`. | -| `axisSoftMax` | number | No | | | -| `axisSoftMin` | number | No | | | -| `axisWidth` | number | No | | | -| `scaleDistribution` | [ScaleDistributionConfig](#scaledistributionconfig) | No | | TODO docs | - -### CellValues - -Controls cell value unit - -| Property | Type | Required | Default | Description | -|----------|-----------------------------------|----------|---------|-------------| -| `object` | Possible types are: [](#), [](#). | | | - -### Color - -Controls the color options - -| Property | Type | Required | Default | Description | -|----------|-----------------------------------|----------|---------|-------------| -| `object` | Possible types are: [](#), [](#). | | | - -### FilterValues - -Filters values between a given range - -| Property | Type | Required | Default | Description | -|----------|-----------------------------------|----------|---------|-------------| -| `object` | Possible types are: [](#), [](#). | | | - -### Tooltip - -Controls tooltip options - -| Property | Type | Required | Default | Description | -|----------|-----------------------------------|----------|---------|-------------| -| `object` | Possible types are: [](#), [](#). | | | - - diff --git a/docs/sources/developers/kinds/composable/histogram/panelcfg/schema-reference.md b/docs/sources/developers/kinds/composable/histogram/panelcfg/schema-reference.md deleted file mode 100644 index d4408fbade..0000000000 --- a/docs/sources/developers/kinds/composable/histogram/panelcfg/schema-reference.md +++ /dev/null @@ -1,148 +0,0 @@ ---- -keywords: - - grafana - - schema -labels: - products: - - cloud - - enterprise - - oss -title: HistogramPanelCfg kind ---- -> Both documentation generation and kinds schemas are in active development and subject to change without prior notice. - -## HistogramPanelCfg - -#### Maturity: [experimental](../../../maturity/#experimental) -#### Version: 0.0 - - - -| Property | Type | Required | Default | Description | -|---------------|------------------------|----------|---------|-------------| -| `FieldConfig` | [object](#fieldconfig) | **Yes** | | | -| `Options` | [object](#options) | **Yes** | | | - -### FieldConfig - -It extends [AxisConfig](#axisconfig) and [HideableFieldConfig](#hideablefieldconfig). - -| Property | Type | Required | Default | Description | -|---------------------|-----------------------------------------------------|----------|---------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `axisBorderShow` | boolean | No | | *(Inherited from [AxisConfig](#axisconfig))* | -| `axisCenteredZero` | boolean | No | | *(Inherited from [AxisConfig](#axisconfig))* | -| `axisColorMode` | string | No | | *(Inherited from [AxisConfig](#axisconfig))*
TODO docs
Possible values are: `text`, `series`. | -| `axisGridShow` | boolean | No | | *(Inherited from [AxisConfig](#axisconfig))* | -| `axisLabel` | string | No | | *(Inherited from [AxisConfig](#axisconfig))* | -| `axisPlacement` | string | No | | *(Inherited from [AxisConfig](#axisconfig))*
TODO docs
Possible values are: `auto`, `top`, `right`, `bottom`, `left`, `hidden`. | -| `axisSoftMax` | number | No | | *(Inherited from [AxisConfig](#axisconfig))* | -| `axisSoftMin` | number | No | | *(Inherited from [AxisConfig](#axisconfig))* | -| `axisWidth` | number | No | | *(Inherited from [AxisConfig](#axisconfig))* | -| `fillOpacity` | integer | No | `80` | Controls the fill opacity of the bars.
Constraint: `>=0 & <=100`. | -| `gradientMode` | string | No | | Set the mode of the gradient fill. Fill gradient is based on the line color. To change the color, use the standard color scheme field option.
Gradient appearance is influenced by the Fill opacity setting. | -| `hideFrom` | [HideSeriesConfig](#hideseriesconfig) | No | | *(Inherited from [HideableFieldConfig](#hideablefieldconfig))*
TODO docs | -| `lineWidth` | integer | No | `1` | Controls line width of the bars.
Constraint: `>=0 & <=10`. | -| `scaleDistribution` | [ScaleDistributionConfig](#scaledistributionconfig) | No | | *(Inherited from [AxisConfig](#axisconfig))*
TODO docs | - -### AxisConfig - -TODO docs - -| Property | Type | Required | Default | Description | -|---------------------|-----------------------------------------------------|----------|---------|----------------------------------------------------------------------------------------| -| `axisBorderShow` | boolean | No | | | -| `axisCenteredZero` | boolean | No | | | -| `axisColorMode` | string | No | | TODO docs
Possible values are: `text`, `series`. | -| `axisGridShow` | boolean | No | | | -| `axisLabel` | string | No | | | -| `axisPlacement` | string | No | | TODO docs
Possible values are: `auto`, `top`, `right`, `bottom`, `left`, `hidden`. | -| `axisSoftMax` | number | No | | | -| `axisSoftMin` | number | No | | | -| `axisWidth` | number | No | | | -| `scaleDistribution` | [ScaleDistributionConfig](#scaledistributionconfig) | No | | TODO docs | - -### ScaleDistributionConfig - -TODO docs - -| Property | Type | Required | Default | Description | -|-------------------|--------|----------|---------|--------------------------------------------------------------------------| -| `type` | string | **Yes** | | TODO docs
Possible values are: `linear`, `log`, `ordinal`, `symlog`. | -| `linearThreshold` | number | No | | | -| `log` | number | No | | | - -### HideSeriesConfig - -TODO docs - -| Property | Type | Required | Default | Description | -|-----------|---------|----------|---------|-------------| -| `legend` | boolean | **Yes** | | | -| `tooltip` | boolean | **Yes** | | | -| `viz` | boolean | **Yes** | | | - -### HideableFieldConfig - -TODO docs - -| Property | Type | Required | Default | Description | -|------------|---------------------------------------|----------|---------|-------------| -| `hideFrom` | [HideSeriesConfig](#hideseriesconfig) | No | | TODO docs | - -### Options - -It extends [OptionsWithLegend](#optionswithlegend) and [OptionsWithTooltip](#optionswithtooltip). - -| Property | Type | Required | Default | Description | -|----------------|-----------------------------------------|----------|---------|-----------------------------------------------------------------------------------------------------------------------------------------| -| `legend` | [VizLegendOptions](#vizlegendoptions) | **Yes** | | *(Inherited from [OptionsWithLegend](#optionswithlegend))*
TODO docs | -| `tooltip` | [VizTooltipOptions](#viztooltipoptions) | **Yes** | | *(Inherited from [OptionsWithTooltip](#optionswithtooltip))*
TODO docs | -| `bucketCount` | integer | No | `30` | Bucket count (approx)
Constraint: `>0 & <=2147483647`. | -| `bucketOffset` | number | No | `0` | Offset buckets by this amount
Constraint: `>=-340282346638528859811704183484516925440 & <=340282346638528859811704183484516925440`. | -| `bucketSize` | integer | No | | Size of each bucket | -| `combine` | boolean | No | | Combines multiple series into a single histogram | - -### OptionsWithLegend - -TODO docs - -| Property | Type | Required | Default | Description | -|----------|---------------------------------------|----------|---------|-------------| -| `legend` | [VizLegendOptions](#vizlegendoptions) | **Yes** | | TODO docs | - -### VizLegendOptions - -TODO docs - -| Property | Type | Required | Default | Description | -|---------------|----------|----------|---------|-----------------------------------------------------------------------------------------------------------------------------------------| -| `calcs` | string[] | **Yes** | | | -| `displayMode` | string | **Yes** | | TODO docs
Note: "hidden" needs to remain as an option for plugins compatibility
Possible values are: `list`, `table`, `hidden`. | -| `placement` | string | **Yes** | | TODO docs
Possible values are: `bottom`, `right`. | -| `showLegend` | boolean | **Yes** | | | -| `asTable` | boolean | No | | | -| `isVisible` | boolean | No | | | -| `sortBy` | string | No | | | -| `sortDesc` | boolean | No | | | -| `width` | number | No | | | - -### OptionsWithTooltip - -TODO docs - -| Property | Type | Required | Default | Description | -|-----------|-----------------------------------------|----------|---------|-------------| -| `tooltip` | [VizTooltipOptions](#viztooltipoptions) | **Yes** | | TODO docs | - -### VizTooltipOptions - -TODO docs - -| Property | Type | Required | Default | Description | -|-------------|--------|----------|---------|---------------------------------------------------------------| -| `mode` | string | **Yes** | | TODO docs
Possible values are: `single`, `multi`, `none`. | -| `sort` | string | **Yes** | | TODO docs
Possible values are: `asc`, `desc`, `none`. | -| `maxHeight` | number | No | | | -| `maxWidth` | number | No | | | - - diff --git a/docs/sources/developers/kinds/composable/logs/panelcfg/schema-reference.md b/docs/sources/developers/kinds/composable/logs/panelcfg/schema-reference.md deleted file mode 100644 index 6a41deb49d..0000000000 --- a/docs/sources/developers/kinds/composable/logs/panelcfg/schema-reference.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -keywords: - - grafana - - schema -labels: - products: - - cloud - - enterprise - - oss -title: LogsPanelCfg kind ---- -> Both documentation generation and kinds schemas are in active development and subject to change without prior notice. - -## LogsPanelCfg - -#### Maturity: [experimental](../../../maturity/#experimental) -#### Version: 0.0 - - - -| Property | Type | Required | Default | Description | -|-----------|--------------------|----------|---------|-------------| -| `Options` | [object](#options) | **Yes** | | | - -### Options - -| Property | Type | Required | Default | Description | -|------------------------|---------|----------|---------|---------------------------------------------------------------| -| `dedupStrategy` | string | **Yes** | | Possible values are: `none`, `exact`, `numbers`, `signature`. | -| `enableLogDetails` | boolean | **Yes** | | | -| `prettifyLogMessage` | boolean | **Yes** | | | -| `showCommonLabels` | boolean | **Yes** | | | -| `showLabels` | boolean | **Yes** | | | -| `showLogContextToggle` | boolean | **Yes** | | | -| `showTime` | boolean | **Yes** | | | -| `sortOrder` | string | **Yes** | | Possible values are: `Descending`, `Ascending`. | -| `wrapLogMessage` | boolean | **Yes** | | | - - diff --git a/docs/sources/developers/kinds/composable/loki/dataquery/schema-reference.md b/docs/sources/developers/kinds/composable/loki/dataquery/schema-reference.md deleted file mode 100644 index d7cfba8b09..0000000000 --- a/docs/sources/developers/kinds/composable/loki/dataquery/schema-reference.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -keywords: - - grafana - - schema -labels: - products: - - cloud - - enterprise - - oss -title: LokiDataQuery kind ---- -> Both documentation generation and kinds schemas are in active development and subject to change without prior notice. - -## LokiDataQuery - -#### Maturity: [experimental](../../../maturity/#experimental) -#### Version: 0.0 - - - -| Property | Type | Required | Default | Description | -|----------------|---------|----------|---------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `expr` | string | **Yes** | | The LogQL query. | -| `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 | -| `editorMode` | string | No | | Possible values are: `code`, `builder`. | -| `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) | -| `instant` | boolean | No | | @deprecated, now use queryType. | -| `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 | | Specify the query flavor
TODO make this required and give it a default | -| `range` | boolean | No | | @deprecated, now use queryType. | -| `resolution` | integer | No | | @deprecated, now use step. | -| `step` | string | No | | Used to set step value for range queries. | - - diff --git a/docs/sources/developers/kinds/composable/news/panelcfg/schema-reference.md b/docs/sources/developers/kinds/composable/news/panelcfg/schema-reference.md deleted file mode 100644 index f4290acc9a..0000000000 --- a/docs/sources/developers/kinds/composable/news/panelcfg/schema-reference.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -keywords: - - grafana - - schema -labels: - products: - - cloud - - enterprise - - oss -title: NewsPanelCfg kind ---- -> Both documentation generation and kinds schemas are in active development and subject to change without prior notice. - -## NewsPanelCfg - -#### Maturity: [experimental](../../../maturity/#experimental) -#### Version: 0.0 - - - -| Property | Type | Required | Default | Description | -|-----------|--------------------|----------|---------|-------------| -| `Options` | [object](#options) | **Yes** | | | - -### Options - -| Property | Type | Required | Default | Description | -|-------------|---------|----------|---------|--------------------------------------------| -| `feedUrl` | string | No | | empty/missing will default to grafana blog | -| `showImage` | boolean | No | `true` | | - - diff --git a/docs/sources/developers/kinds/composable/nodegraph/panelcfg/schema-reference.md b/docs/sources/developers/kinds/composable/nodegraph/panelcfg/schema-reference.md deleted file mode 100644 index 1ac4a3f7a3..0000000000 --- a/docs/sources/developers/kinds/composable/nodegraph/panelcfg/schema-reference.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -keywords: - - grafana - - schema -labels: - products: - - cloud - - enterprise - - oss -title: NodeGraphPanelCfg kind ---- -> Both documentation generation and kinds schemas are in active development and subject to change without prior notice. - -## NodeGraphPanelCfg - -#### Maturity: [experimental](../../../maturity/#experimental) -#### Version: 0.0 - - - -| Property | Type | Required | Default | Description | -|---------------|------------------------|----------|---------|-------------| -| `ArcOption` | [object](#arcoption) | **Yes** | | | -| `EdgeOptions` | [object](#edgeoptions) | **Yes** | | | -| `NodeOptions` | [object](#nodeoptions) | **Yes** | | | -| `Options` | [object](#options) | **Yes** | | | - -### ArcOption - -| Property | Type | Required | Default | Description | -|----------|--------|----------|---------|-----------------------------------------------------------------------------------------------------| -| `color` | string | No | | The color of the arc. | -| `field` | string | No | | Field from which to get the value. Values should be less than 1, representing fraction of a circle. | - -### EdgeOptions - -| Property | Type | Required | Default | Description | -|---------------------|--------|----------|---------|-----------------------------------------------------------------------------| -| `mainStatUnit` | string | No | | Unit for the main stat to override what ever is set in the data frame. | -| `secondaryStatUnit` | string | No | | Unit for the secondary stat to override what ever is set in the data frame. | - -### NodeOptions - -| Property | Type | Required | Default | Description | -|---------------------|---------------------------|----------|---------|-----------------------------------------------------------------------------------------| -| `arcs` | [ArcOption](#arcoption)[] | No | | Define which fields are shown as part of the node arc (colored circle around the node). | -| `mainStatUnit` | string | No | | Unit for the main stat to override what ever is set in the data frame. | -| `secondaryStatUnit` | string | No | | Unit for the secondary stat to override what ever is set in the data frame. | - -### Options - -| Property | Type | Required | Default | Description | -|----------|-----------------------------|----------|---------|-------------| -| `edges` | [EdgeOptions](#edgeoptions) | No | | | -| `nodes` | [NodeOptions](#nodeoptions) | No | | | - - diff --git a/docs/sources/developers/kinds/composable/parca/dataquery/schema-reference.md b/docs/sources/developers/kinds/composable/parca/dataquery/schema-reference.md deleted file mode 100644 index baf0cc6be0..0000000000 --- a/docs/sources/developers/kinds/composable/parca/dataquery/schema-reference.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -keywords: - - grafana - - schema -labels: - products: - - cloud - - enterprise - - oss -title: ParcaDataQuery kind ---- -> Both documentation generation and kinds schemas are in active development and subject to change without prior notice. - -## ParcaDataQuery - -#### Maturity: [experimental](../../../maturity/#experimental) -#### Version: 0.0 - - - -| Property | Type | Required | Default | Description | -|-----------------|---------|----------|---------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `labelSelector` | string | **Yes** | `{}` | Specifies the query label selectors. | -| `profileTypeId` | string | **Yes** | | Specifies the type of profile to query. | -| `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)
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/piechart/panelcfg/schema-reference.md b/docs/sources/developers/kinds/composable/piechart/panelcfg/schema-reference.md deleted file mode 100644 index 89e7cf0bdf..0000000000 --- a/docs/sources/developers/kinds/composable/piechart/panelcfg/schema-reference.md +++ /dev/null @@ -1,154 +0,0 @@ ---- -keywords: - - grafana - - schema -labels: - products: - - cloud - - enterprise - - oss -title: PieChartPanelCfg kind ---- -> Both documentation generation and kinds schemas are in active development and subject to change without prior notice. - -## PieChartPanelCfg - -#### Maturity: [experimental](../../../maturity/#experimental) -#### Version: 0.0 - - - -| Property | Type | Required | Default | Description | -|-------------------------|---------------------------------------------|----------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `FieldConfig` | [HideableFieldConfig](#hideablefieldconfig) | **Yes** | | TODO docs | -| `Options` | [object](#options) | **Yes** | | | -| `PieChartLabels` | string | **Yes** | | Select labels to display on the pie chart.
- Name - The series or field name.
- Percent - The percentage of the whole.
- Value - The raw numerical value.
Possible values are: `name`, `value`, `percent`. | -| `PieChartLegendOptions` | [object](#piechartlegendoptions) | **Yes** | | | -| `PieChartLegendValues` | string | **Yes** | | Select values to display in the legend.
- Percent: The percentage of the whole.
- Value: The raw numerical value.
Possible values are: `value`, `percent`. | -| `PieChartType` | string | **Yes** | | Select the pie chart display style.
Possible values are: `pie`, `donut`. | - -### HideableFieldConfig - -TODO docs - -| Property | Type | Required | Default | Description | -|------------|---------------------------------------|----------|---------|-------------| -| `hideFrom` | [HideSeriesConfig](#hideseriesconfig) | No | | TODO docs | - -### HideSeriesConfig - -TODO docs - -| Property | Type | Required | Default | Description | -|-----------|---------|----------|---------|-------------| -| `legend` | boolean | **Yes** | | | -| `tooltip` | boolean | **Yes** | | | -| `viz` | boolean | **Yes** | | | - -### Options - -It extends [OptionsWithTooltip](#optionswithtooltip) and [SingleStatBaseOptions](#singlestatbaseoptions). - -| Property | Type | Required | Default | Description | -|-----------------|-------------------------------------------------|----------|---------|---------------------------------------------------------------------------------------------------------------------------------------------| -| `displayLabels` | string[] | **Yes** | | | -| `legend` | [PieChartLegendOptions](#piechartlegendoptions) | **Yes** | | | -| `pieType` | string | **Yes** | | Select the pie chart display style.
Possible values are: `pie`, `donut`. | -| `tooltip` | [VizTooltipOptions](#viztooltipoptions) | **Yes** | | *(Inherited from [OptionsWithTooltip](#optionswithtooltip))*
TODO docs | -| `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 | - -### OptionsWithTooltip - -TODO docs - -| Property | Type | Required | Default | Description | -|-----------|-----------------------------------------|----------|---------|-------------| -| `tooltip` | [VizTooltipOptions](#viztooltipoptions) | **Yes** | | TODO docs | - -### VizTooltipOptions - -TODO docs - -| Property | Type | Required | Default | Description | -|-------------|--------|----------|---------|---------------------------------------------------------------| -| `mode` | string | **Yes** | | TODO docs
Possible values are: `single`, `multi`, `none`. | -| `sort` | string | **Yes** | | TODO docs
Possible values are: `asc`, `desc`, `none`. | -| `maxHeight` | number | No | | | -| `maxWidth` | number | No | | | - -### PieChartLegendOptions - -It extends [VizLegendOptions](#vizlegendoptions). - -| Property | Type | Required | Default | Description | -|---------------|----------|----------|---------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `calcs` | string[] | **Yes** | | *(Inherited from [VizLegendOptions](#vizlegendoptions))* | -| `displayMode` | string | **Yes** | | *(Inherited from [VizLegendOptions](#vizlegendoptions))*
TODO docs
Note: "hidden" needs to remain as an option for plugins compatibility
Possible values are: `list`, `table`, `hidden`. | -| `placement` | string | **Yes** | | *(Inherited from [VizLegendOptions](#vizlegendoptions))*
TODO docs
Possible values are: `bottom`, `right`. | -| `showLegend` | boolean | **Yes** | | *(Inherited from [VizLegendOptions](#vizlegendoptions))* | -| `values` | string[] | **Yes** | | | -| `asTable` | boolean | No | | *(Inherited from [VizLegendOptions](#vizlegendoptions))* | -| `isVisible` | boolean | No | | *(Inherited from [VizLegendOptions](#vizlegendoptions))* | -| `sortBy` | string | No | | *(Inherited from [VizLegendOptions](#vizlegendoptions))* | -| `sortDesc` | boolean | No | | *(Inherited from [VizLegendOptions](#vizlegendoptions))* | -| `width` | number | No | | *(Inherited from [VizLegendOptions](#vizlegendoptions))* | - -### VizLegendOptions - -TODO docs - -| Property | Type | Required | Default | Description | -|---------------|----------|----------|---------|-----------------------------------------------------------------------------------------------------------------------------------------| -| `calcs` | string[] | **Yes** | | | -| `displayMode` | string | **Yes** | | TODO docs
Note: "hidden" needs to remain as an option for plugins compatibility
Possible values are: `list`, `table`, `hidden`. | -| `placement` | string | **Yes** | | TODO docs
Possible values are: `bottom`, `right`. | -| `showLegend` | boolean | **Yes** | | | -| `asTable` | boolean | No | | | -| `isVisible` | boolean | No | | | -| `sortBy` | string | No | | | -| `sortDesc` | boolean | No | | | -| `width` | number | No | | | - -### ReduceDataOptions - -TODO docs - -| Property | Type | Required | Default | Description | -|----------|----------|----------|---------|---------------------------------------------------------------| -| `calcs` | string[] | **Yes** | | When !values, pick one value for the whole field | -| `fields` | string | No | | Which fields to show. By default this is only numeric fields | -| `limit` | number | No | | if showing all values limit | -| `values` | boolean | No | | If true show each row value | - -### SingleStatBaseOptions - -TODO docs - -It extends [OptionsWithTextFormatting](#optionswithtextformatting). - -| Property | Type | Required | Default | Description | -|-----------------|-------------------------------------------------|----------|---------|------------------------------------------------------------------------------------------| -| `orientation` | string | **Yes** | | TODO docs
Possible values are: `auto`, `vertical`, `horizontal`. | -| `reduceOptions` | [ReduceDataOptions](#reducedataoptions) | **Yes** | | TODO docs | -| `text` | [VizTextDisplayOptions](#viztextdisplayoptions) | No | | *(Inherited from [OptionsWithTextFormatting](#optionswithtextformatting))*
TODO docs | - -### OptionsWithTextFormatting - -TODO docs - -| Property | Type | Required | Default | Description | -|----------|-------------------------------------------------|----------|---------|-------------| -| `text` | [VizTextDisplayOptions](#viztextdisplayoptions) | No | | TODO docs | - -### VizTextDisplayOptions - -TODO docs - -| Property | Type | Required | Default | Description | -|-------------|--------|----------|---------|--------------------------| -| `titleSize` | number | No | | Explicit title text size | -| `valueSize` | number | No | | Explicit value text size | - - diff --git a/docs/sources/developers/kinds/composable/prometheus/dataquery/schema-reference.md b/docs/sources/developers/kinds/composable/prometheus/dataquery/schema-reference.md deleted file mode 100644 index 028dea0ce1..0000000000 --- a/docs/sources/developers/kinds/composable/prometheus/dataquery/schema-reference.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -keywords: - - grafana - - schema -labels: - products: - - cloud - - enterprise - - oss -title: PrometheusDataQuery kind ---- -> Both documentation generation and kinds schemas are in active development and subject to change without prior notice. - -## PrometheusDataQuery - -#### Maturity: [experimental](../../../maturity/#experimental) -#### Version: 0.0 - - - -| Property | Type | Required | Default | Description | -|------------------|------------------|----------|---------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `expr` | string | **Yes** | | The actual expression/query that will be evaluated by Prometheus | -| `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 | -| `editorMode` | string | No | | Possible values are: `code`, `builder`. | -| `exemplar` | boolean | No | | Execute an additional query to identify interesting raw samples relevant for the given expr | -| `format` | string | No | | Possible values are: `time_series`, `table`, `heatmap`. | -| `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) | -| `instant` | boolean | No | | Returns only the latest value that Prometheus has scraped for the requested time series | -| `intervalFactor` | number | No | | @deprecated Used to specify how many times to divide max data points by. We use max data points under query options
See https://github.com/grafana/grafana/issues/48081 | -| `legendFormat` | string | No | | Series name override or template. Ex. {{hostname}} will be replaced with label value for hostname | -| `queryType` | string | No | | Specify the query flavor
TODO make this required and give it a default | -| `range` | boolean | No | | Returns a Range vector, comprised of a set of time series containing a range of data points over time for each time series | -| `scope` | [object](#scope) | No | | | - -### Scope - -| Property | Type | Required | Default | Description | -|------------|--------|----------|---------|-------------| -| `matchers` | string | **Yes** | | | - - diff --git a/docs/sources/developers/kinds/composable/stat/panelcfg/schema-reference.md b/docs/sources/developers/kinds/composable/stat/panelcfg/schema-reference.md deleted file mode 100644 index 51fac3c312..0000000000 --- a/docs/sources/developers/kinds/composable/stat/panelcfg/schema-reference.md +++ /dev/null @@ -1,81 +0,0 @@ ---- -keywords: - - grafana - - schema -labels: - products: - - cloud - - enterprise - - oss -title: StatPanelCfg kind ---- -> Both documentation generation and kinds schemas are in active development and subject to change without prior notice. - -## StatPanelCfg - -#### Maturity: [experimental](../../../maturity/#experimental) -#### Version: 0.0 - - - -| Property | Type | Required | Default | Description | -|-----------|--------------------|----------|---------|-------------| -| `Options` | [object](#options) | **Yes** | | | - -### Options - -It extends [SingleStatBaseOptions](#singlestatbaseoptions). - -| Property | Type | Required | Default | Description | -|---------------------|-------------------------------------------------|----------|---------|---------------------------------------------------------------------------------------------------------------------------------------------| -| `colorMode` | string | **Yes** | | TODO docs
Possible values are: `value`, `background`, `background_solid`, `none`. | -| `graphMode` | string | **Yes** | | TODO docs
Possible values are: `none`, `line`, `area`. | -| `justifyMode` | string | **Yes** | | TODO docs
Possible values are: `auto`, `center`. | -| `showPercentChange` | boolean | **Yes** | `false` | | -| `textMode` | string | **Yes** | | TODO docs
Possible values are: `auto`, `value`, `value_and_name`, `name`, `none`. | -| `wideLayout` | boolean | **Yes** | `true` | | -| `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 | - -### ReduceDataOptions - -TODO docs - -| Property | Type | Required | Default | Description | -|----------|----------|----------|---------|---------------------------------------------------------------| -| `calcs` | string[] | **Yes** | | When !values, pick one value for the whole field | -| `fields` | string | No | | Which fields to show. By default this is only numeric fields | -| `limit` | number | No | | if showing all values limit | -| `values` | boolean | No | | If true show each row value | - -### SingleStatBaseOptions - -TODO docs - -It extends [OptionsWithTextFormatting](#optionswithtextformatting). - -| Property | Type | Required | Default | Description | -|-----------------|-------------------------------------------------|----------|---------|------------------------------------------------------------------------------------------| -| `orientation` | string | **Yes** | | TODO docs
Possible values are: `auto`, `vertical`, `horizontal`. | -| `reduceOptions` | [ReduceDataOptions](#reducedataoptions) | **Yes** | | TODO docs | -| `text` | [VizTextDisplayOptions](#viztextdisplayoptions) | No | | *(Inherited from [OptionsWithTextFormatting](#optionswithtextformatting))*
TODO docs | - -### OptionsWithTextFormatting - -TODO docs - -| Property | Type | Required | Default | Description | -|----------|-------------------------------------------------|----------|---------|-------------| -| `text` | [VizTextDisplayOptions](#viztextdisplayoptions) | No | | TODO docs | - -### VizTextDisplayOptions - -TODO docs - -| Property | Type | Required | Default | Description | -|-------------|--------|----------|---------|--------------------------| -| `titleSize` | number | No | | Explicit title text size | -| `valueSize` | number | No | | Explicit value text size | - - diff --git a/docs/sources/developers/kinds/composable/statetimeline/panelcfg/schema-reference.md b/docs/sources/developers/kinds/composable/statetimeline/panelcfg/schema-reference.md deleted file mode 100644 index 3875023916..0000000000 --- a/docs/sources/developers/kinds/composable/statetimeline/panelcfg/schema-reference.md +++ /dev/null @@ -1,119 +0,0 @@ ---- -keywords: - - grafana - - schema -labels: - products: - - cloud - - enterprise - - oss -title: StateTimelinePanelCfg kind ---- -> Both documentation generation and kinds schemas are in active development and subject to change without prior notice. - -## StateTimelinePanelCfg - -#### Maturity: [experimental](../../../maturity/#experimental) -#### Version: 0.0 - - - -| Property | Type | Required | Default | Description | -|---------------|------------------------|----------|---------|-------------| -| `FieldConfig` | [object](#fieldconfig) | **Yes** | | | -| `Options` | [object](#options) | **Yes** | | | - -### FieldConfig - -It extends [HideableFieldConfig](#hideablefieldconfig). - -| Property | Type | Required | Default | Description | -|---------------|---------------------------------------|----------|---------|------------------------------------------------------------------------------| -| `fillOpacity` | integer | No | `70` | Constraint: `>=0 & <=100`. | -| `hideFrom` | [HideSeriesConfig](#hideseriesconfig) | No | | *(Inherited from [HideableFieldConfig](#hideablefieldconfig))*
TODO docs | -| `lineWidth` | integer | No | `0` | Constraint: `>=0 & <=10`. | - -### HideSeriesConfig - -TODO docs - -| Property | Type | Required | Default | Description | -|-----------|---------|----------|---------|-------------| -| `legend` | boolean | **Yes** | | | -| `tooltip` | boolean | **Yes** | | | -| `viz` | boolean | **Yes** | | | - -### HideableFieldConfig - -TODO docs - -| Property | Type | Required | Default | Description | -|------------|---------------------------------------|----------|---------|-------------| -| `hideFrom` | [HideSeriesConfig](#hideseriesconfig) | No | | TODO docs | - -### Options - -It extends [OptionsWithLegend](#optionswithlegend) and [OptionsWithTooltip](#optionswithtooltip) and [OptionsWithTimezones](#optionswithtimezones). - -| Property | Type | Required | Default | Description | -|---------------|-----------------------------------------|----------|---------|----------------------------------------------------------------------------| -| `legend` | [VizLegendOptions](#vizlegendoptions) | **Yes** | | *(Inherited from [OptionsWithLegend](#optionswithlegend))*
TODO docs | -| `rowHeight` | number | **Yes** | `0.9` | Controls the row height | -| `showValue` | string | **Yes** | | Show timeline values on chart | -| `tooltip` | [VizTooltipOptions](#viztooltipoptions) | **Yes** | | *(Inherited from [OptionsWithTooltip](#optionswithtooltip))*
TODO docs | -| `alignValue` | string | No | | Controls value alignment on the timelines | -| `mergeValues` | boolean | No | `true` | Merge equal consecutive values | -| `timezone` | string[] | No | | *(Inherited from [OptionsWithTimezones](#optionswithtimezones))* | - -### OptionsWithLegend - -TODO docs - -| Property | Type | Required | Default | Description | -|----------|---------------------------------------|----------|---------|-------------| -| `legend` | [VizLegendOptions](#vizlegendoptions) | **Yes** | | TODO docs | - -### VizLegendOptions - -TODO docs - -| Property | Type | Required | Default | Description | -|---------------|----------|----------|---------|-----------------------------------------------------------------------------------------------------------------------------------------| -| `calcs` | string[] | **Yes** | | | -| `displayMode` | string | **Yes** | | TODO docs
Note: "hidden" needs to remain as an option for plugins compatibility
Possible values are: `list`, `table`, `hidden`. | -| `placement` | string | **Yes** | | TODO docs
Possible values are: `bottom`, `right`. | -| `showLegend` | boolean | **Yes** | | | -| `asTable` | boolean | No | | | -| `isVisible` | boolean | No | | | -| `sortBy` | string | No | | | -| `sortDesc` | boolean | No | | | -| `width` | number | No | | | - -### OptionsWithTimezones - -TODO docs - -| Property | Type | Required | Default | Description | -|------------|----------|----------|---------|-------------| -| `timezone` | string[] | No | | | - -### OptionsWithTooltip - -TODO docs - -| Property | Type | Required | Default | Description | -|-----------|-----------------------------------------|----------|---------|-------------| -| `tooltip` | [VizTooltipOptions](#viztooltipoptions) | **Yes** | | TODO docs | - -### VizTooltipOptions - -TODO docs - -| Property | Type | Required | Default | Description | -|-------------|--------|----------|---------|---------------------------------------------------------------| -| `mode` | string | **Yes** | | TODO docs
Possible values are: `single`, `multi`, `none`. | -| `sort` | string | **Yes** | | TODO docs
Possible values are: `asc`, `desc`, `none`. | -| `maxHeight` | number | No | | | -| `maxWidth` | number | No | | | - - diff --git a/docs/sources/developers/kinds/composable/statushistory/panelcfg/schema-reference.md b/docs/sources/developers/kinds/composable/statushistory/panelcfg/schema-reference.md deleted file mode 100644 index 63d454b66a..0000000000 --- a/docs/sources/developers/kinds/composable/statushistory/panelcfg/schema-reference.md +++ /dev/null @@ -1,118 +0,0 @@ ---- -keywords: - - grafana - - schema -labels: - products: - - cloud - - enterprise - - oss -title: StatusHistoryPanelCfg kind ---- -> Both documentation generation and kinds schemas are in active development and subject to change without prior notice. - -## StatusHistoryPanelCfg - -#### Maturity: [experimental](../../../maturity/#experimental) -#### Version: 0.0 - - - -| Property | Type | Required | Default | Description | -|---------------|------------------------|----------|---------|-------------| -| `FieldConfig` | [object](#fieldconfig) | **Yes** | | | -| `Options` | [object](#options) | **Yes** | | | - -### FieldConfig - -It extends [HideableFieldConfig](#hideablefieldconfig). - -| Property | Type | Required | Default | Description | -|---------------|---------------------------------------|----------|---------|------------------------------------------------------------------------------| -| `fillOpacity` | integer | No | `70` | Constraint: `>=0 & <=100`. | -| `hideFrom` | [HideSeriesConfig](#hideseriesconfig) | No | | *(Inherited from [HideableFieldConfig](#hideablefieldconfig))*
TODO docs | -| `lineWidth` | integer | No | `1` | Constraint: `>=0 & <=10`. | - -### HideSeriesConfig - -TODO docs - -| Property | Type | Required | Default | Description | -|-----------|---------|----------|---------|-------------| -| `legend` | boolean | **Yes** | | | -| `tooltip` | boolean | **Yes** | | | -| `viz` | boolean | **Yes** | | | - -### HideableFieldConfig - -TODO docs - -| Property | Type | Required | Default | Description | -|------------|---------------------------------------|----------|---------|-------------| -| `hideFrom` | [HideSeriesConfig](#hideseriesconfig) | No | | TODO docs | - -### Options - -It extends [OptionsWithLegend](#optionswithlegend) and [OptionsWithTooltip](#optionswithtooltip) and [OptionsWithTimezones](#optionswithtimezones). - -| Property | Type | Required | Default | Description | -|-------------|-----------------------------------------|----------|---------|----------------------------------------------------------------------------| -| `legend` | [VizLegendOptions](#vizlegendoptions) | **Yes** | | *(Inherited from [OptionsWithLegend](#optionswithlegend))*
TODO docs | -| `rowHeight` | number | **Yes** | `0.9` | Set the height of the rows
Constraint: `>=0 & <=1`. | -| `showValue` | string | **Yes** | | Show values on the columns | -| `tooltip` | [VizTooltipOptions](#viztooltipoptions) | **Yes** | | *(Inherited from [OptionsWithTooltip](#optionswithtooltip))*
TODO docs | -| `colWidth` | number | No | `0.9` | Controls the column width | -| `timezone` | string[] | No | | *(Inherited from [OptionsWithTimezones](#optionswithtimezones))* | - -### OptionsWithLegend - -TODO docs - -| Property | Type | Required | Default | Description | -|----------|---------------------------------------|----------|---------|-------------| -| `legend` | [VizLegendOptions](#vizlegendoptions) | **Yes** | | TODO docs | - -### VizLegendOptions - -TODO docs - -| Property | Type | Required | Default | Description | -|---------------|----------|----------|---------|-----------------------------------------------------------------------------------------------------------------------------------------| -| `calcs` | string[] | **Yes** | | | -| `displayMode` | string | **Yes** | | TODO docs
Note: "hidden" needs to remain as an option for plugins compatibility
Possible values are: `list`, `table`, `hidden`. | -| `placement` | string | **Yes** | | TODO docs
Possible values are: `bottom`, `right`. | -| `showLegend` | boolean | **Yes** | | | -| `asTable` | boolean | No | | | -| `isVisible` | boolean | No | | | -| `sortBy` | string | No | | | -| `sortDesc` | boolean | No | | | -| `width` | number | No | | | - -### OptionsWithTimezones - -TODO docs - -| Property | Type | Required | Default | Description | -|------------|----------|----------|---------|-------------| -| `timezone` | string[] | No | | | - -### OptionsWithTooltip - -TODO docs - -| Property | Type | Required | Default | Description | -|-----------|-----------------------------------------|----------|---------|-------------| -| `tooltip` | [VizTooltipOptions](#viztooltipoptions) | **Yes** | | TODO docs | - -### VizTooltipOptions - -TODO docs - -| Property | Type | Required | Default | Description | -|-------------|--------|----------|---------|---------------------------------------------------------------| -| `mode` | string | **Yes** | | TODO docs
Possible values are: `single`, `multi`, `none`. | -| `sort` | string | **Yes** | | TODO docs
Possible values are: `asc`, `desc`, `none`. | -| `maxHeight` | number | No | | | -| `maxWidth` | number | No | | | - - diff --git a/docs/sources/developers/kinds/composable/table/panelcfg/schema-reference.md b/docs/sources/developers/kinds/composable/table/panelcfg/schema-reference.md deleted file mode 100644 index 802497a602..0000000000 --- a/docs/sources/developers/kinds/composable/table/panelcfg/schema-reference.md +++ /dev/null @@ -1,340 +0,0 @@ ---- -keywords: - - grafana - - schema -labels: - products: - - cloud - - enterprise - - oss -title: TablePanelCfg kind ---- -> Both documentation generation and kinds schemas are in active development and subject to change without prior notice. - -## TablePanelCfg - -#### Maturity: [experimental](../../../maturity/#experimental) -#### Version: 0.0 - - - -| Property | Type | Required | Default | Description | -|---------------|------------------------|----------|---------|-------------| -| `FieldConfig` | [object](#fieldconfig) | **Yes** | | | -| `Options` | [object](#options) | **Yes** | | | - -### FieldConfig - -| Property | Type | Required | Default | Description | -|---------------|---------------------------------------|----------|---------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `align` | string | **Yes** | | TODO -- should not be table specific!
TODO docs
Possible values are: `auto`, `left`, `right`, `center`. | -| `cellOptions` | [TableCellOptions](#tablecelloptions) | **Yes** | | Table cell options. Each cell has a display mode
and other potential options for that display. | -| `inspect` | boolean | **Yes** | `false` | | -| `displayMode` | string | No | | Internally, this is the "type" of cell that's being displayed
in the table such as colored text, JSON, gauge, etc.
The color-background-solid, gradient-gauge, and lcd-gauge
modes are deprecated in favor of new cell subOptions
Possible values are: `auto`, `color-text`, `color-background`, `color-background-solid`, `gradient-gauge`, `lcd-gauge`, `json-view`, `basic`, `image`, `gauge`, `sparkline`, `data-links`, `custom`. | -| `filterable` | boolean | No | | | -| `hidden` | boolean | No | | | -| `hideHeader` | boolean | No | | Hides any header for a column, useful for columns that show some static content or buttons. | -| `minWidth` | number | No | | | -| `width` | number | No | | | - -### TableCellOptions - -Table cell options. Each cell has a display mode -and other potential options for that display. - -| Property | Type | Required | Default | Description | -|----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|---------|-------------| -| `object` | Possible types are: [TableAutoCellOptions](#tableautocelloptions), [TableSparklineCellOptions](#tablesparklinecelloptions), [TableBarGaugeCellOptions](#tablebargaugecelloptions), [TableColoredBackgroundCellOptions](#tablecoloredbackgroundcelloptions), [TableColorTextCellOptions](#tablecolortextcelloptions), [TableImageCellOptions](#tableimagecelloptions), [TableDataLinksCellOptions](#tabledatalinkscelloptions), [TableJsonViewCellOptions](#tablejsonviewcelloptions). | | | - -### GraphThresholdsStyleConfig - -TODO docs - -| Property | Type | Required | Default | Description | -|----------|--------|----------|---------|-----------------------------------------------------------------------------------------------------------| -| `mode` | string | **Yes** | | TODO docs
Possible values are: `off`, `line`, `dashed`, `area`, `line+area`, `dashed+area`, `series`. | - -### HideSeriesConfig - -TODO docs - -| Property | Type | Required | Default | Description | -|-----------|---------|----------|---------|-------------| -| `legend` | boolean | **Yes** | | | -| `tooltip` | boolean | **Yes** | | | -| `viz` | boolean | **Yes** | | | - -### LineStyle - -TODO docs - -| Property | Type | Required | Default | Description | -|----------|----------|----------|---------|--------------------------------------------------------| -| `dash` | number[] | No | | | -| `fill` | string | No | | Possible values are: `solid`, `dash`, `dot`, `square`. | - -### ScaleDistributionConfig - -TODO docs - -| Property | Type | Required | Default | Description | -|-------------------|--------|----------|---------|--------------------------------------------------------------------------| -| `type` | string | **Yes** | | TODO docs
Possible values are: `linear`, `log`, `ordinal`, `symlog`. | -| `linearThreshold` | number | No | | | -| `log` | number | No | | | - -### StackingConfig - -TODO docs - -| Property | Type | Required | Default | Description | -|----------|--------|----------|---------|-----------------------------------------------------------------| -| `group` | string | No | | | -| `mode` | string | No | | TODO docs
Possible values are: `none`, `normal`, `percent`. | - -### TableAutoCellOptions - -Auto mode table cell options - -| Property | Type | Required | Default | Description | -|----------|--------|----------|---------|-------------| -| `type` | string | **Yes** | | | - -### TableBarGaugeCellOptions - -Gauge cell options - -| Property | Type | Required | Default | Description | -|--------------------|--------|----------|---------|-----------------------------------------------------------------------------------------------------------------------------------------------| -| `type` | string | **Yes** | | | -| `mode` | string | No | | Enum expressing the possible display modes
for the bar gauge component of Grafana UI
Possible values are: `basic`, `lcd`, `gradient`. | -| `valueDisplayMode` | string | No | | Allows for the table cell gauge display type to set the gauge mode.
Possible values are: `color`, `text`, `hidden`. | - -### TableColorTextCellOptions - -Colored text cell options - -| Property | Type | Required | Default | Description | -|----------|--------|----------|---------|-------------| -| `type` | string | **Yes** | | | - -### TableColoredBackgroundCellOptions - -Colored background cell options - -| Property | Type | Required | Default | Description | -|----------|--------|----------|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `type` | string | **Yes** | | | -| `mode` | string | No | | Display mode to the "Colored Background" display
mode for table cells. Either displays a solid color (basic mode)
or a gradient.
Possible values are: `basic`, `gradient`. | - -### TableDataLinksCellOptions - -Show data links in the cell - -| Property | Type | Required | Default | Description | -|----------|--------|----------|---------|-------------| -| `type` | string | **Yes** | | | - -### TableImageCellOptions - -Json view cell options - -| Property | Type | Required | Default | Description | -|----------|--------|----------|---------|-------------| -| `type` | string | **Yes** | | | - -### TableJsonViewCellOptions - -Json view cell options - -| Property | Type | Required | Default | Description | -|----------|--------|----------|---------|-------------| -| `type` | string | **Yes** | | | - -### TableSparklineCellOptions - -Sparkline cell options - -It extends [GraphFieldConfig](#graphfieldconfig). - -| Property | Type | Required | Default | Description | -|---------------------|-----------------------------------------------------------|----------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `type` | string | **Yes** | | | -| `axisBorderShow` | boolean | No | | *(Inherited from [GraphFieldConfig](#graphfieldconfig))* | -| `axisCenteredZero` | boolean | No | | *(Inherited from [GraphFieldConfig](#graphfieldconfig))* | -| `axisColorMode` | string | No | | *(Inherited from [GraphFieldConfig](#graphfieldconfig))*
TODO docs
Possible values are: `text`, `series`. | -| `axisGridShow` | boolean | No | | *(Inherited from [GraphFieldConfig](#graphfieldconfig))* | -| `axisLabel` | string | No | | *(Inherited from [GraphFieldConfig](#graphfieldconfig))* | -| `axisPlacement` | string | No | | *(Inherited from [GraphFieldConfig](#graphfieldconfig))*
TODO docs
Possible values are: `auto`, `top`, `right`, `bottom`, `left`, `hidden`. | -| `axisSoftMax` | number | No | | *(Inherited from [GraphFieldConfig](#graphfieldconfig))* | -| `axisSoftMin` | number | No | | *(Inherited from [GraphFieldConfig](#graphfieldconfig))* | -| `axisWidth` | number | No | | *(Inherited from [GraphFieldConfig](#graphfieldconfig))* | -| `barAlignment` | integer | No | | *(Inherited from [GraphFieldConfig](#graphfieldconfig))*
TODO docs
Possible values are: `-1`, `0`, `1`. | -| `barMaxWidth` | number | No | | *(Inherited from [GraphFieldConfig](#graphfieldconfig))* | -| `barWidthFactor` | number | No | | *(Inherited from [GraphFieldConfig](#graphfieldconfig))* | -| `drawStyle` | string | No | | *(Inherited from [GraphFieldConfig](#graphfieldconfig))*
TODO docs
Possible values are: `line`, `bars`, `points`. | -| `fillBelowTo` | string | No | | *(Inherited from [GraphFieldConfig](#graphfieldconfig))* | -| `fillColor` | string | No | | *(Inherited from [GraphFieldConfig](#graphfieldconfig))* | -| `fillOpacity` | number | No | | *(Inherited from [GraphFieldConfig](#graphfieldconfig))* | -| `gradientMode` | string | No | | *(Inherited from [GraphFieldConfig](#graphfieldconfig))*
TODO docs
Possible values are: `none`, `opacity`, `hue`, `scheme`. | -| `hideFrom` | [HideSeriesConfig](#hideseriesconfig) | No | | *(Inherited from [GraphFieldConfig](#graphfieldconfig))*
TODO docs | -| `hideValue` | boolean | No | | | -| `lineColor` | string | No | | *(Inherited from [GraphFieldConfig](#graphfieldconfig))* | -| `lineInterpolation` | string | No | | *(Inherited from [GraphFieldConfig](#graphfieldconfig))*
TODO docs
Possible values are: `linear`, `smooth`, `stepBefore`, `stepAfter`. | -| `lineStyle` | [LineStyle](#linestyle) | No | | *(Inherited from [GraphFieldConfig](#graphfieldconfig))*
TODO docs | -| `lineWidth` | number | No | | *(Inherited from [GraphFieldConfig](#graphfieldconfig))* | -| `pointColor` | string | No | | *(Inherited from [GraphFieldConfig](#graphfieldconfig))* | -| `pointSize` | number | No | | *(Inherited from [GraphFieldConfig](#graphfieldconfig))* | -| `pointSymbol` | string | No | | *(Inherited from [GraphFieldConfig](#graphfieldconfig))* | -| `scaleDistribution` | [ScaleDistributionConfig](#scaledistributionconfig) | No | | *(Inherited from [GraphFieldConfig](#graphfieldconfig))*
TODO docs | -| `showPoints` | string | No | | *(Inherited from [GraphFieldConfig](#graphfieldconfig))*
TODO docs
Possible values are: `auto`, `never`, `always`. | -| `spanNulls` | | No | | *(Inherited from [GraphFieldConfig](#graphfieldconfig))*
Indicate if null values should be treated as gaps or connected.
When the value is a number, it represents the maximum delta in the
X axis that should be considered connected. For timeseries, this is milliseconds | -| `stacking` | [StackingConfig](#stackingconfig) | No | | *(Inherited from [GraphFieldConfig](#graphfieldconfig))*
TODO docs | -| `thresholdsStyle` | [GraphThresholdsStyleConfig](#graphthresholdsstyleconfig) | No | | *(Inherited from [GraphFieldConfig](#graphfieldconfig))*
TODO docs | -| `transform` | string | No | | *(Inherited from [GraphFieldConfig](#graphfieldconfig))*
TODO docs
Possible values are: `constant`, `negative-Y`. | - -### GraphFieldConfig - -TODO docs - -It extends [LineConfig](#lineconfig) and [FillConfig](#fillconfig) and [PointsConfig](#pointsconfig) and [AxisConfig](#axisconfig) and [BarConfig](#barconfig) and [StackableFieldConfig](#stackablefieldconfig) and [HideableFieldConfig](#hideablefieldconfig). - -| Property | Type | Required | Default | Description | -|---------------------|-----------------------------------------------------------|----------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `axisBorderShow` | boolean | No | | *(Inherited from [AxisConfig](#axisconfig))* | -| `axisCenteredZero` | boolean | No | | *(Inherited from [AxisConfig](#axisconfig))* | -| `axisColorMode` | string | No | | *(Inherited from [AxisConfig](#axisconfig))*
TODO docs
Possible values are: `text`, `series`. | -| `axisGridShow` | boolean | No | | *(Inherited from [AxisConfig](#axisconfig))* | -| `axisLabel` | string | No | | *(Inherited from [AxisConfig](#axisconfig))* | -| `axisPlacement` | string | No | | *(Inherited from [AxisConfig](#axisconfig))*
TODO docs
Possible values are: `auto`, `top`, `right`, `bottom`, `left`, `hidden`. | -| `axisSoftMax` | number | No | | *(Inherited from [AxisConfig](#axisconfig))* | -| `axisSoftMin` | number | No | | *(Inherited from [AxisConfig](#axisconfig))* | -| `axisWidth` | number | No | | *(Inherited from [AxisConfig](#axisconfig))* | -| `barAlignment` | integer | No | | *(Inherited from [BarConfig](#barconfig))*
TODO docs
Possible values are: `-1`, `0`, `1`. | -| `barMaxWidth` | number | No | | *(Inherited from [BarConfig](#barconfig))* | -| `barWidthFactor` | number | No | | *(Inherited from [BarConfig](#barconfig))* | -| `drawStyle` | string | No | | TODO docs
Possible values are: `line`, `bars`, `points`. | -| `fillBelowTo` | string | No | | *(Inherited from [FillConfig](#fillconfig))* | -| `fillColor` | string | No | | *(Inherited from [FillConfig](#fillconfig))* | -| `fillOpacity` | number | No | | *(Inherited from [FillConfig](#fillconfig))* | -| `gradientMode` | string | No | | TODO docs
Possible values are: `none`, `opacity`, `hue`, `scheme`. | -| `hideFrom` | [HideSeriesConfig](#hideseriesconfig) | No | | *(Inherited from [HideableFieldConfig](#hideablefieldconfig))*
TODO docs | -| `lineColor` | string | No | | *(Inherited from [LineConfig](#lineconfig))* | -| `lineInterpolation` | string | No | | *(Inherited from [LineConfig](#lineconfig))*
TODO docs
Possible values are: `linear`, `smooth`, `stepBefore`, `stepAfter`. | -| `lineStyle` | [LineStyle](#linestyle) | No | | *(Inherited from [LineConfig](#lineconfig))*
TODO docs | -| `lineWidth` | number | No | | *(Inherited from [LineConfig](#lineconfig))* | -| `pointColor` | string | No | | *(Inherited from [PointsConfig](#pointsconfig))* | -| `pointSize` | number | No | | *(Inherited from [PointsConfig](#pointsconfig))* | -| `pointSymbol` | string | No | | *(Inherited from [PointsConfig](#pointsconfig))* | -| `scaleDistribution` | [ScaleDistributionConfig](#scaledistributionconfig) | No | | *(Inherited from [AxisConfig](#axisconfig))*
TODO docs | -| `showPoints` | string | No | | *(Inherited from [PointsConfig](#pointsconfig))*
TODO docs
Possible values are: `auto`, `never`, `always`. | -| `spanNulls` | | No | | *(Inherited from [LineConfig](#lineconfig))*
Indicate if null values should be treated as gaps or connected.
When the value is a number, it represents the maximum delta in the
X axis that should be considered connected. For timeseries, this is milliseconds | -| `stacking` | [StackingConfig](#stackingconfig) | No | | *(Inherited from [StackableFieldConfig](#stackablefieldconfig))*
TODO docs | -| `thresholdsStyle` | [GraphThresholdsStyleConfig](#graphthresholdsstyleconfig) | No | | TODO docs | -| `transform` | string | No | | TODO docs
Possible values are: `constant`, `negative-Y`. | - -### AxisConfig - -TODO docs - -| Property | Type | Required | Default | Description | -|---------------------|-----------------------------------------------------|----------|---------|----------------------------------------------------------------------------------------| -| `axisBorderShow` | boolean | No | | | -| `axisCenteredZero` | boolean | No | | | -| `axisColorMode` | string | No | | TODO docs
Possible values are: `text`, `series`. | -| `axisGridShow` | boolean | No | | | -| `axisLabel` | string | No | | | -| `axisPlacement` | string | No | | TODO docs
Possible values are: `auto`, `top`, `right`, `bottom`, `left`, `hidden`. | -| `axisSoftMax` | number | No | | | -| `axisSoftMin` | number | No | | | -| `axisWidth` | number | No | | | -| `scaleDistribution` | [ScaleDistributionConfig](#scaledistributionconfig) | No | | TODO docs | - -### BarConfig - -TODO docs - -| Property | Type | Required | Default | Description | -|------------------|---------|----------|---------|----------------------------------------------------| -| `barAlignment` | integer | No | | TODO docs
Possible values are: `-1`, `0`, `1`. | -| `barMaxWidth` | number | No | | | -| `barWidthFactor` | number | No | | | - -### FillConfig - -TODO docs - -| Property | Type | Required | Default | Description | -|---------------|--------|----------|---------|-------------| -| `fillBelowTo` | string | No | | | -| `fillColor` | string | No | | | -| `fillOpacity` | number | No | | | - -### HideableFieldConfig - -TODO docs - -| Property | Type | Required | Default | Description | -|------------|---------------------------------------|----------|---------|-------------| -| `hideFrom` | [HideSeriesConfig](#hideseriesconfig) | No | | TODO docs | - -### LineConfig - -TODO docs - -| Property | Type | Required | Default | Description | -|---------------------|-------------------------|----------|---------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `lineColor` | string | No | | | -| `lineInterpolation` | string | No | | TODO docs
Possible values are: `linear`, `smooth`, `stepBefore`, `stepAfter`. | -| `lineStyle` | [LineStyle](#linestyle) | No | | TODO docs | -| `lineWidth` | number | No | | | -| `spanNulls` | | No | | Indicate if null values should be treated as gaps or connected.
When the value is a number, it represents the maximum delta in the
X axis that should be considered connected. For timeseries, this is milliseconds | - -### PointsConfig - -TODO docs - -| Property | Type | Required | Default | Description | -|---------------|--------|----------|---------|---------------------------------------------------------------| -| `pointColor` | string | No | | | -| `pointSize` | number | No | | | -| `pointSymbol` | string | No | | | -| `showPoints` | string | No | | TODO docs
Possible values are: `auto`, `never`, `always`. | - -### StackableFieldConfig - -TODO docs - -| Property | Type | Required | Default | Description | -|------------|-----------------------------------|----------|---------|-------------| -| `stacking` | [StackingConfig](#stackingconfig) | No | | TODO docs | - -### Options - -| Property | Type | Required | Default | Description | -|-----------------|---------------------------------------------------|----------|----------------------------------------------|--------------------------------------------------------------------| -| `frameIndex` | number | **Yes** | `0` | Represents the index of the selected frame | -| `showHeader` | boolean | **Yes** | `true` | Controls whether the panel should show the header | -| `cellHeight` | string | No | | Controls the height of the rows | -| `footer` | [object](#footer) | No | `map[countRows:false reducer:[] show:false]` | Controls footer options | -| `showTypeIcons` | boolean | No | `false` | Controls whether the header should show icons for the column types | -| `sortBy` | [TableSortByFieldState](#tablesortbyfieldstate)[] | No | | Used to control row sorting | - -### TableSortByFieldState - -Sort by field state - -| Property | Type | Required | Default | Description | -|---------------|---------|----------|---------|-----------------------------------------------| -| `displayName` | string | **Yes** | | Sets the display name of the field to sort by | -| `desc` | boolean | No | | Flag used to indicate descending sort order | - -### Footer - -Controls footer options - -| Property | Type | Required | Default | Description | -|----------|-----------------------------------|----------|---------|-------------| -| `object` | Possible types are: [](#), [](#). | | | - - diff --git a/docs/sources/developers/kinds/composable/tempo/dataquery/schema-reference.md b/docs/sources/developers/kinds/composable/tempo/dataquery/schema-reference.md deleted file mode 100644 index 1b44c7cc98..0000000000 --- a/docs/sources/developers/kinds/composable/tempo/dataquery/schema-reference.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -keywords: - - grafana - - schema -labels: - products: - - cloud - - enterprise - - oss -title: TempoDataQuery kind ---- -> Both documentation generation and kinds schemas are in active development and subject to change without prior notice. - -## TempoDataQuery - -#### Maturity: [experimental](../../../maturity/#experimental) -#### Version: 0.0 - - - -| Property | Type | Required | Default | Description | -|----------|------|----------|---------|-------------| - - diff --git a/docs/sources/developers/kinds/composable/testdata/dataquery/schema-reference.md b/docs/sources/developers/kinds/composable/testdata/dataquery/schema-reference.md deleted file mode 100644 index bc5da9a3aa..0000000000 --- a/docs/sources/developers/kinds/composable/testdata/dataquery/schema-reference.md +++ /dev/null @@ -1,119 +0,0 @@ ---- -keywords: - - grafana - - schema -labels: - products: - - cloud - - enterprise - - oss -title: TestDataDataQuery kind ---- -> Both documentation generation and kinds schemas are in active development and subject to change without prior notice. - -## TestDataDataQuery - -#### Maturity: [experimental](../../../maturity/#experimental) -#### Version: 0.0 - - - -| Property | Type | Required | Default | Description | -|-------------------|-------------------------------------|----------|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `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. | -| `alias` | string | No | | | -| `channel` | string | No | | | -| `csvContent` | string | No | | | -| `csvFileName` | string | No | | | -| `csvWave` | [CSVWave](#csvwave)[] | No | | | -| `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 | -| `dropPercent` | number | No | | Drop percentage (the chance we will lose a point 0-100) | -| `errorType` | string | No | | Possible values are: `server_panic`, `frontend_exception`, `frontend_observable`. | -| `flamegraphDiff` | boolean | No | | | -| `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) | -| `labels` | string | No | | | -| `levelColumn` | boolean | No | | | -| `lines` | integer | No | | | -| `nodes` | [NodesQuery](#nodesquery) | No | | | -| `points` | array[] | No | | | -| `pulseWave` | [PulseWaveQuery](#pulsewavequery) | No | | | -| `queryType` | string | No | | Specify the query flavor
TODO make this required and give it a default | -| `rawFrameContent` | string | No | | | -| `scenarioId` | string | No | | Possible values are: `random_walk`, `slow_query`, `random_walk_with_error`, `random_walk_table`, `exponential_heatmap_bucket_data`, `linear_heatmap_bucket_data`, `no_data_points`, `datapoints_outside_range`, `csv_metric_values`, `predictable_pulse`, `predictable_csv_wave`, `streaming_client`, `simulation`, `usa`, `live`, `grafana_api`, `arrow`, `annotations`, `table_static`, `server_error_500`, `logs`, `node_graph`, `flame_graph`, `raw_frame`, `csv_file`, `csv_content`, `trace`, `manual_entry`, `variables-query`. | -| `seriesCount` | integer | No | | | -| `sim` | [SimulationQuery](#simulationquery) | No | | | -| `spanCount` | integer | No | | | -| `stream` | [StreamingQuery](#streamingquery) | No | | | -| `stringInput` | string | No | | | -| `usa` | [USAQuery](#usaquery) | No | | | - -### CSVWave - -| Property | Type | Required | Default | Description | -|-------------|---------|----------|---------|-------------| -| `labels` | string | No | | | -| `name` | string | No | | | -| `timeStep` | integer | No | | | -| `valuesCSV` | string | No | | | - -### NodesQuery - -| Property | Type | Required | Default | Description | -|----------|---------|----------|---------|-------------------------------------------------------------------------------------| -| `count` | integer | No | | | -| `seed` | integer | No | | | -| `type` | string | No | | Possible values are: `random`, `response_small`, `response_medium`, `random edges`. | - -### PulseWaveQuery - -| Property | Type | Required | Default | Description | -|------------|---------|----------|---------|-------------| -| `offCount` | integer | No | | | -| `offValue` | number | No | | | -| `onCount` | integer | No | | | -| `onValue` | number | No | | | -| `timeStep` | integer | No | | | - -### SimulationQuery - -| Property | Type | Required | Default | Description | -|----------|-------------------|----------|---------|-------------| -| `key` | [object](#key) | **Yes** | | | -| `config` | [object](#config) | No | | | -| `last` | boolean | No | | | -| `stream` | boolean | No | | | - -### Config - -| Property | Type | Required | Default | Description | -|----------|------|----------|---------|-------------| - -### Key - -| Property | Type | Required | Default | Description | -|----------|--------|----------|---------|-------------| -| `tick` | number | **Yes** | | | -| `type` | string | **Yes** | | | -| `uid` | string | No | | | - -### StreamingQuery - -| Property | Type | Required | Default | Description | -|----------|---------|----------|---------|-----------------------------------------------------------| -| `noise` | integer | **Yes** | | | -| `speed` | integer | **Yes** | | | -| `spread` | integer | **Yes** | | | -| `type` | string | **Yes** | | Possible values are: `signal`, `logs`, `fetch`, `traces`. | -| `bands` | integer | No | | | -| `url` | string | No | | | - -### USAQuery - -| Property | Type | Required | Default | Description | -|----------|----------|----------|---------|-------------| -| `fields` | string[] | No | | | -| `mode` | string | No | | | -| `period` | string | No | | | -| `states` | string[] | No | | | - - diff --git a/docs/sources/developers/kinds/composable/text/panelcfg/schema-reference.md b/docs/sources/developers/kinds/composable/text/panelcfg/schema-reference.md deleted file mode 100644 index 8b566c2a2a..0000000000 --- a/docs/sources/developers/kinds/composable/text/panelcfg/schema-reference.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -keywords: - - grafana - - schema -labels: - products: - - cloud - - enterprise - - oss -title: TextPanelCfg kind ---- -> Both documentation generation and kinds schemas are in active development and subject to change without prior notice. - -## TextPanelCfg - -#### Maturity: [experimental](../../../maturity/#experimental) -#### Version: 0.0 - - - -| Property | Type | Required | Default | Description | -|----------------|------------------------|----------|-------------|---------------------------------------------------------------------------------------------------------| -| `CodeLanguage` | string | **Yes** | `plaintext` | Possible values are: `plaintext`, `yaml`, `xml`, `typescript`, `sql`, `go`, `markdown`, `html`, `json`. | -| `CodeOptions` | [object](#codeoptions) | **Yes** | | | -| `Options` | [object](#options) | **Yes** | | | -| `TextMode` | string | **Yes** | | Possible values are: `html`, `markdown`, `code`. | - -### CodeOptions - -| Property | Type | Required | Default | Description | -|-------------------|---------|----------|-------------|---------------------------------------------------------------------------------------------------------| -| `language` | string | **Yes** | `plaintext` | Possible values are: `plaintext`, `yaml`, `xml`, `typescript`, `sql`, `go`, `markdown`, `html`, `json`. | -| `showLineNumbers` | boolean | **Yes** | `false` | | -| `showMiniMap` | boolean | **Yes** | `false` | | - -### Options - -| Property | Type | Required | Default | Description | -|-----------|-----------------------------|----------|--------------------------------------------------------------------------------|--------------------------------------------------| -| `content` | string | **Yes** | `# Title | | -| | | | | | -| | | | For markdown syntax help: [commonmark.org/help](https://commonmark.org/help/)` | | -| `mode` | string | **Yes** | | Possible values are: `html`, `markdown`, `code`. | -| `code` | [CodeOptions](#codeoptions) | No | | | - - diff --git a/docs/sources/developers/kinds/composable/timeseries/panelcfg/schema-reference.md b/docs/sources/developers/kinds/composable/timeseries/panelcfg/schema-reference.md deleted file mode 100644 index 4f1c350d7c..0000000000 --- a/docs/sources/developers/kinds/composable/timeseries/panelcfg/schema-reference.md +++ /dev/null @@ -1,233 +0,0 @@ ---- -keywords: - - grafana - - schema -labels: - products: - - cloud - - enterprise - - oss -title: TimeSeriesPanelCfg kind ---- -> Both documentation generation and kinds schemas are in active development and subject to change without prior notice. - -## TimeSeriesPanelCfg - -#### Maturity: [merged](../../../maturity/#merged) -#### Version: 0.0 - - - -| Property | Type | Required | Default | Description | -|---------------|---------------------------------------|----------|---------|-------------| -| `FieldConfig` | [GraphFieldConfig](#graphfieldconfig) | **Yes** | | TODO docs | -| `Options` | [object](#options) | **Yes** | | | - -### GraphFieldConfig - -TODO docs - -It extends [LineConfig](#lineconfig) and [FillConfig](#fillconfig) and [PointsConfig](#pointsconfig) and [AxisConfig](#axisconfig) and [BarConfig](#barconfig) and [StackableFieldConfig](#stackablefieldconfig) and [HideableFieldConfig](#hideablefieldconfig). - -| Property | Type | Required | Default | Description | -|---------------------|-----------------------------------------------------------|----------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `axisBorderShow` | boolean | No | | *(Inherited from [AxisConfig](#axisconfig))* | -| `axisCenteredZero` | boolean | No | | *(Inherited from [AxisConfig](#axisconfig))* | -| `axisColorMode` | string | No | | *(Inherited from [AxisConfig](#axisconfig))*
TODO docs
Possible values are: `text`, `series`. | -| `axisGridShow` | boolean | No | | *(Inherited from [AxisConfig](#axisconfig))* | -| `axisLabel` | string | No | | *(Inherited from [AxisConfig](#axisconfig))* | -| `axisPlacement` | string | No | | *(Inherited from [AxisConfig](#axisconfig))*
TODO docs
Possible values are: `auto`, `top`, `right`, `bottom`, `left`, `hidden`. | -| `axisSoftMax` | number | No | | *(Inherited from [AxisConfig](#axisconfig))* | -| `axisSoftMin` | number | No | | *(Inherited from [AxisConfig](#axisconfig))* | -| `axisWidth` | number | No | | *(Inherited from [AxisConfig](#axisconfig))* | -| `barAlignment` | integer | No | | *(Inherited from [BarConfig](#barconfig))*
TODO docs
Possible values are: `-1`, `0`, `1`. | -| `barMaxWidth` | number | No | | *(Inherited from [BarConfig](#barconfig))* | -| `barWidthFactor` | number | No | | *(Inherited from [BarConfig](#barconfig))* | -| `drawStyle` | string | No | | TODO docs
Possible values are: `line`, `bars`, `points`. | -| `fillBelowTo` | string | No | | *(Inherited from [FillConfig](#fillconfig))* | -| `fillColor` | string | No | | *(Inherited from [FillConfig](#fillconfig))* | -| `fillOpacity` | number | No | | *(Inherited from [FillConfig](#fillconfig))* | -| `gradientMode` | string | No | | TODO docs
Possible values are: `none`, `opacity`, `hue`, `scheme`. | -| `hideFrom` | [HideSeriesConfig](#hideseriesconfig) | No | | *(Inherited from [HideableFieldConfig](#hideablefieldconfig))*
TODO docs | -| `lineColor` | string | No | | *(Inherited from [LineConfig](#lineconfig))* | -| `lineInterpolation` | string | No | | *(Inherited from [LineConfig](#lineconfig))*
TODO docs
Possible values are: `linear`, `smooth`, `stepBefore`, `stepAfter`. | -| `lineStyle` | [LineStyle](#linestyle) | No | | *(Inherited from [LineConfig](#lineconfig))*
TODO docs | -| `lineWidth` | number | No | | *(Inherited from [LineConfig](#lineconfig))* | -| `pointColor` | string | No | | *(Inherited from [PointsConfig](#pointsconfig))* | -| `pointSize` | number | No | | *(Inherited from [PointsConfig](#pointsconfig))* | -| `pointSymbol` | string | No | | *(Inherited from [PointsConfig](#pointsconfig))* | -| `scaleDistribution` | [ScaleDistributionConfig](#scaledistributionconfig) | No | | *(Inherited from [AxisConfig](#axisconfig))*
TODO docs | -| `showPoints` | string | No | | *(Inherited from [PointsConfig](#pointsconfig))*
TODO docs
Possible values are: `auto`, `never`, `always`. | -| `spanNulls` | | No | | *(Inherited from [LineConfig](#lineconfig))*
Indicate if null values should be treated as gaps or connected.
When the value is a number, it represents the maximum delta in the
X axis that should be considered connected. For timeseries, this is milliseconds | -| `stacking` | [StackingConfig](#stackingconfig) | No | | *(Inherited from [StackableFieldConfig](#stackablefieldconfig))*
TODO docs | -| `thresholdsStyle` | [GraphThresholdsStyleConfig](#graphthresholdsstyleconfig) | No | | TODO docs | -| `transform` | string | No | | TODO docs
Possible values are: `constant`, `negative-Y`. | - -### AxisConfig - -TODO docs - -| Property | Type | Required | Default | Description | -|---------------------|-----------------------------------------------------|----------|---------|----------------------------------------------------------------------------------------| -| `axisBorderShow` | boolean | No | | | -| `axisCenteredZero` | boolean | No | | | -| `axisColorMode` | string | No | | TODO docs
Possible values are: `text`, `series`. | -| `axisGridShow` | boolean | No | | | -| `axisLabel` | string | No | | | -| `axisPlacement` | string | No | | TODO docs
Possible values are: `auto`, `top`, `right`, `bottom`, `left`, `hidden`. | -| `axisSoftMax` | number | No | | | -| `axisSoftMin` | number | No | | | -| `axisWidth` | number | No | | | -| `scaleDistribution` | [ScaleDistributionConfig](#scaledistributionconfig) | No | | TODO docs | - -### ScaleDistributionConfig - -TODO docs - -| Property | Type | Required | Default | Description | -|-------------------|--------|----------|---------|--------------------------------------------------------------------------| -| `type` | string | **Yes** | | TODO docs
Possible values are: `linear`, `log`, `ordinal`, `symlog`. | -| `linearThreshold` | number | No | | | -| `log` | number | No | | | - -### BarConfig - -TODO docs - -| Property | Type | Required | Default | Description | -|------------------|---------|----------|---------|----------------------------------------------------| -| `barAlignment` | integer | No | | TODO docs
Possible values are: `-1`, `0`, `1`. | -| `barMaxWidth` | number | No | | | -| `barWidthFactor` | number | No | | | - -### FillConfig - -TODO docs - -| Property | Type | Required | Default | Description | -|---------------|--------|----------|---------|-------------| -| `fillBelowTo` | string | No | | | -| `fillColor` | string | No | | | -| `fillOpacity` | number | No | | | - -### GraphThresholdsStyleConfig - -TODO docs - -| Property | Type | Required | Default | Description | -|----------|--------|----------|---------|-----------------------------------------------------------------------------------------------------------| -| `mode` | string | **Yes** | | TODO docs
Possible values are: `off`, `line`, `dashed`, `area`, `line+area`, `dashed+area`, `series`. | - -### HideSeriesConfig - -TODO docs - -| Property | Type | Required | Default | Description | -|-----------|---------|----------|---------|-------------| -| `legend` | boolean | **Yes** | | | -| `tooltip` | boolean | **Yes** | | | -| `viz` | boolean | **Yes** | | | - -### HideableFieldConfig - -TODO docs - -| Property | Type | Required | Default | Description | -|------------|---------------------------------------|----------|---------|-------------| -| `hideFrom` | [HideSeriesConfig](#hideseriesconfig) | No | | TODO docs | - -### LineConfig - -TODO docs - -| Property | Type | Required | Default | Description | -|---------------------|-------------------------|----------|---------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `lineColor` | string | No | | | -| `lineInterpolation` | string | No | | TODO docs
Possible values are: `linear`, `smooth`, `stepBefore`, `stepAfter`. | -| `lineStyle` | [LineStyle](#linestyle) | No | | TODO docs | -| `lineWidth` | number | No | | | -| `spanNulls` | | No | | Indicate if null values should be treated as gaps or connected.
When the value is a number, it represents the maximum delta in the
X axis that should be considered connected. For timeseries, this is milliseconds | - -### LineStyle - -TODO docs - -| Property | Type | Required | Default | Description | -|----------|----------|----------|---------|--------------------------------------------------------| -| `dash` | number[] | No | | | -| `fill` | string | No | | Possible values are: `solid`, `dash`, `dot`, `square`. | - -### PointsConfig - -TODO docs - -| Property | Type | Required | Default | Description | -|---------------|--------|----------|---------|---------------------------------------------------------------| -| `pointColor` | string | No | | | -| `pointSize` | number | No | | | -| `pointSymbol` | string | No | | | -| `showPoints` | string | No | | TODO docs
Possible values are: `auto`, `never`, `always`. | - -### StackableFieldConfig - -TODO docs - -| Property | Type | Required | Default | Description | -|------------|-----------------------------------|----------|---------|-------------| -| `stacking` | [StackingConfig](#stackingconfig) | No | | TODO docs | - -### StackingConfig - -TODO docs - -| Property | Type | Required | Default | Description | -|----------|--------|----------|---------|-----------------------------------------------------------------| -| `group` | string | No | | | -| `mode` | string | No | | TODO docs
Possible values are: `none`, `normal`, `percent`. | - -### Options - -It extends [OptionsWithTimezones](#optionswithtimezones). - -| Property | Type | Required | Default | Description | -|------------|-----------------------------------------|----------|---------|------------------------------------------------------------------| -| `legend` | [VizLegendOptions](#vizlegendoptions) | **Yes** | | TODO docs | -| `tooltip` | [VizTooltipOptions](#viztooltipoptions) | **Yes** | | TODO docs | -| `timezone` | string[] | No | | *(Inherited from [OptionsWithTimezones](#optionswithtimezones))* | - -### OptionsWithTimezones - -TODO docs - -| Property | Type | Required | Default | Description | -|------------|----------|----------|---------|-------------| -| `timezone` | string[] | No | | | - -### VizLegendOptions - -TODO docs - -| Property | Type | Required | Default | Description | -|---------------|----------|----------|---------|-----------------------------------------------------------------------------------------------------------------------------------------| -| `calcs` | string[] | **Yes** | | | -| `displayMode` | string | **Yes** | | TODO docs
Note: "hidden" needs to remain as an option for plugins compatibility
Possible values are: `list`, `table`, `hidden`. | -| `placement` | string | **Yes** | | TODO docs
Possible values are: `bottom`, `right`. | -| `showLegend` | boolean | **Yes** | | | -| `asTable` | boolean | No | | | -| `isVisible` | boolean | No | | | -| `sortBy` | string | No | | | -| `sortDesc` | boolean | No | | | -| `width` | number | No | | | - -### VizTooltipOptions - -TODO docs - -| Property | Type | Required | Default | Description | -|-------------|--------|----------|---------|---------------------------------------------------------------| -| `mode` | string | **Yes** | | TODO docs
Possible values are: `single`, `multi`, `none`. | -| `sort` | string | **Yes** | | TODO docs
Possible values are: `asc`, `desc`, `none`. | -| `maxHeight` | number | No | | | -| `maxWidth` | number | No | | | - - diff --git a/docs/sources/developers/kinds/composable/trend/panelcfg/schema-reference.md b/docs/sources/developers/kinds/composable/trend/panelcfg/schema-reference.md deleted file mode 100644 index 9bf489e4c0..0000000000 --- a/docs/sources/developers/kinds/composable/trend/panelcfg/schema-reference.md +++ /dev/null @@ -1,225 +0,0 @@ ---- -keywords: - - grafana - - schema -labels: - products: - - cloud - - enterprise - - oss -title: TrendPanelCfg kind ---- -> Both documentation generation and kinds schemas are in active development and subject to change without prior notice. - -## TrendPanelCfg - -#### Maturity: [merged](../../../maturity/#merged) -#### Version: 0.0 - - - -| Property | Type | Required | Default | Description | -|---------------|---------------------------------------|----------|---------|----------------------------------------------------------------------| -| `FieldConfig` | [GraphFieldConfig](#graphfieldconfig) | **Yes** | | TODO docs | -| `Options` | [object](#options) | **Yes** | | Identical to timeseries... except it does not have timezone settings | - -### GraphFieldConfig - -TODO docs - -It extends [LineConfig](#lineconfig) and [FillConfig](#fillconfig) and [PointsConfig](#pointsconfig) and [AxisConfig](#axisconfig) and [BarConfig](#barconfig) and [StackableFieldConfig](#stackablefieldconfig) and [HideableFieldConfig](#hideablefieldconfig). - -| Property | Type | Required | Default | Description | -|---------------------|-----------------------------------------------------------|----------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `axisBorderShow` | boolean | No | | *(Inherited from [AxisConfig](#axisconfig))* | -| `axisCenteredZero` | boolean | No | | *(Inherited from [AxisConfig](#axisconfig))* | -| `axisColorMode` | string | No | | *(Inherited from [AxisConfig](#axisconfig))*
TODO docs
Possible values are: `text`, `series`. | -| `axisGridShow` | boolean | No | | *(Inherited from [AxisConfig](#axisconfig))* | -| `axisLabel` | string | No | | *(Inherited from [AxisConfig](#axisconfig))* | -| `axisPlacement` | string | No | | *(Inherited from [AxisConfig](#axisconfig))*
TODO docs
Possible values are: `auto`, `top`, `right`, `bottom`, `left`, `hidden`. | -| `axisSoftMax` | number | No | | *(Inherited from [AxisConfig](#axisconfig))* | -| `axisSoftMin` | number | No | | *(Inherited from [AxisConfig](#axisconfig))* | -| `axisWidth` | number | No | | *(Inherited from [AxisConfig](#axisconfig))* | -| `barAlignment` | integer | No | | *(Inherited from [BarConfig](#barconfig))*
TODO docs
Possible values are: `-1`, `0`, `1`. | -| `barMaxWidth` | number | No | | *(Inherited from [BarConfig](#barconfig))* | -| `barWidthFactor` | number | No | | *(Inherited from [BarConfig](#barconfig))* | -| `drawStyle` | string | No | | TODO docs
Possible values are: `line`, `bars`, `points`. | -| `fillBelowTo` | string | No | | *(Inherited from [FillConfig](#fillconfig))* | -| `fillColor` | string | No | | *(Inherited from [FillConfig](#fillconfig))* | -| `fillOpacity` | number | No | | *(Inherited from [FillConfig](#fillconfig))* | -| `gradientMode` | string | No | | TODO docs
Possible values are: `none`, `opacity`, `hue`, `scheme`. | -| `hideFrom` | [HideSeriesConfig](#hideseriesconfig) | No | | *(Inherited from [HideableFieldConfig](#hideablefieldconfig))*
TODO docs | -| `lineColor` | string | No | | *(Inherited from [LineConfig](#lineconfig))* | -| `lineInterpolation` | string | No | | *(Inherited from [LineConfig](#lineconfig))*
TODO docs
Possible values are: `linear`, `smooth`, `stepBefore`, `stepAfter`. | -| `lineStyle` | [LineStyle](#linestyle) | No | | *(Inherited from [LineConfig](#lineconfig))*
TODO docs | -| `lineWidth` | number | No | | *(Inherited from [LineConfig](#lineconfig))* | -| `pointColor` | string | No | | *(Inherited from [PointsConfig](#pointsconfig))* | -| `pointSize` | number | No | | *(Inherited from [PointsConfig](#pointsconfig))* | -| `pointSymbol` | string | No | | *(Inherited from [PointsConfig](#pointsconfig))* | -| `scaleDistribution` | [ScaleDistributionConfig](#scaledistributionconfig) | No | | *(Inherited from [AxisConfig](#axisconfig))*
TODO docs | -| `showPoints` | string | No | | *(Inherited from [PointsConfig](#pointsconfig))*
TODO docs
Possible values are: `auto`, `never`, `always`. | -| `spanNulls` | | No | | *(Inherited from [LineConfig](#lineconfig))*
Indicate if null values should be treated as gaps or connected.
When the value is a number, it represents the maximum delta in the
X axis that should be considered connected. For timeseries, this is milliseconds | -| `stacking` | [StackingConfig](#stackingconfig) | No | | *(Inherited from [StackableFieldConfig](#stackablefieldconfig))*
TODO docs | -| `thresholdsStyle` | [GraphThresholdsStyleConfig](#graphthresholdsstyleconfig) | No | | TODO docs | -| `transform` | string | No | | TODO docs
Possible values are: `constant`, `negative-Y`. | - -### AxisConfig - -TODO docs - -| Property | Type | Required | Default | Description | -|---------------------|-----------------------------------------------------|----------|---------|----------------------------------------------------------------------------------------| -| `axisBorderShow` | boolean | No | | | -| `axisCenteredZero` | boolean | No | | | -| `axisColorMode` | string | No | | TODO docs
Possible values are: `text`, `series`. | -| `axisGridShow` | boolean | No | | | -| `axisLabel` | string | No | | | -| `axisPlacement` | string | No | | TODO docs
Possible values are: `auto`, `top`, `right`, `bottom`, `left`, `hidden`. | -| `axisSoftMax` | number | No | | | -| `axisSoftMin` | number | No | | | -| `axisWidth` | number | No | | | -| `scaleDistribution` | [ScaleDistributionConfig](#scaledistributionconfig) | No | | TODO docs | - -### ScaleDistributionConfig - -TODO docs - -| Property | Type | Required | Default | Description | -|-------------------|--------|----------|---------|--------------------------------------------------------------------------| -| `type` | string | **Yes** | | TODO docs
Possible values are: `linear`, `log`, `ordinal`, `symlog`. | -| `linearThreshold` | number | No | | | -| `log` | number | No | | | - -### BarConfig - -TODO docs - -| Property | Type | Required | Default | Description | -|------------------|---------|----------|---------|----------------------------------------------------| -| `barAlignment` | integer | No | | TODO docs
Possible values are: `-1`, `0`, `1`. | -| `barMaxWidth` | number | No | | | -| `barWidthFactor` | number | No | | | - -### FillConfig - -TODO docs - -| Property | Type | Required | Default | Description | -|---------------|--------|----------|---------|-------------| -| `fillBelowTo` | string | No | | | -| `fillColor` | string | No | | | -| `fillOpacity` | number | No | | | - -### GraphThresholdsStyleConfig - -TODO docs - -| Property | Type | Required | Default | Description | -|----------|--------|----------|---------|-----------------------------------------------------------------------------------------------------------| -| `mode` | string | **Yes** | | TODO docs
Possible values are: `off`, `line`, `dashed`, `area`, `line+area`, `dashed+area`, `series`. | - -### HideSeriesConfig - -TODO docs - -| Property | Type | Required | Default | Description | -|-----------|---------|----------|---------|-------------| -| `legend` | boolean | **Yes** | | | -| `tooltip` | boolean | **Yes** | | | -| `viz` | boolean | **Yes** | | | - -### HideableFieldConfig - -TODO docs - -| Property | Type | Required | Default | Description | -|------------|---------------------------------------|----------|---------|-------------| -| `hideFrom` | [HideSeriesConfig](#hideseriesconfig) | No | | TODO docs | - -### LineConfig - -TODO docs - -| Property | Type | Required | Default | Description | -|---------------------|-------------------------|----------|---------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `lineColor` | string | No | | | -| `lineInterpolation` | string | No | | TODO docs
Possible values are: `linear`, `smooth`, `stepBefore`, `stepAfter`. | -| `lineStyle` | [LineStyle](#linestyle) | No | | TODO docs | -| `lineWidth` | number | No | | | -| `spanNulls` | | No | | Indicate if null values should be treated as gaps or connected.
When the value is a number, it represents the maximum delta in the
X axis that should be considered connected. For timeseries, this is milliseconds | - -### LineStyle - -TODO docs - -| Property | Type | Required | Default | Description | -|----------|----------|----------|---------|--------------------------------------------------------| -| `dash` | number[] | No | | | -| `fill` | string | No | | Possible values are: `solid`, `dash`, `dot`, `square`. | - -### PointsConfig - -TODO docs - -| Property | Type | Required | Default | Description | -|---------------|--------|----------|---------|---------------------------------------------------------------| -| `pointColor` | string | No | | | -| `pointSize` | number | No | | | -| `pointSymbol` | string | No | | | -| `showPoints` | string | No | | TODO docs
Possible values are: `auto`, `never`, `always`. | - -### StackableFieldConfig - -TODO docs - -| Property | Type | Required | Default | Description | -|------------|-----------------------------------|----------|---------|-------------| -| `stacking` | [StackingConfig](#stackingconfig) | No | | TODO docs | - -### StackingConfig - -TODO docs - -| Property | Type | Required | Default | Description | -|----------|--------|----------|---------|-----------------------------------------------------------------| -| `group` | string | No | | | -| `mode` | string | No | | TODO docs
Possible values are: `none`, `normal`, `percent`. | - -### Options - -Identical to timeseries... except it does not have timezone settings - -| Property | Type | Required | Default | Description | -|-----------|-----------------------------------------|----------|---------|-------------------------------------------------------| -| `legend` | [VizLegendOptions](#vizlegendoptions) | **Yes** | | TODO docs | -| `tooltip` | [VizTooltipOptions](#viztooltipoptions) | **Yes** | | TODO docs | -| `xField` | string | No | | Name of the x field to use (defaults to first number) | - -### VizLegendOptions - -TODO docs - -| Property | Type | Required | Default | Description | -|---------------|----------|----------|---------|-----------------------------------------------------------------------------------------------------------------------------------------| -| `calcs` | string[] | **Yes** | | | -| `displayMode` | string | **Yes** | | TODO docs
Note: "hidden" needs to remain as an option for plugins compatibility
Possible values are: `list`, `table`, `hidden`. | -| `placement` | string | **Yes** | | TODO docs
Possible values are: `bottom`, `right`. | -| `showLegend` | boolean | **Yes** | | | -| `asTable` | boolean | No | | | -| `isVisible` | boolean | No | | | -| `sortBy` | string | No | | | -| `sortDesc` | boolean | No | | | -| `width` | number | No | | | - -### VizTooltipOptions - -TODO docs - -| Property | Type | Required | Default | Description | -|-------------|--------|----------|---------|---------------------------------------------------------------| -| `mode` | string | **Yes** | | TODO docs
Possible values are: `single`, `multi`, `none`. | -| `sort` | string | **Yes** | | TODO docs
Possible values are: `asc`, `desc`, `none`. | -| `maxHeight` | number | No | | | -| `maxWidth` | number | No | | | - - diff --git a/docs/sources/developers/kinds/composable/xychart/panelcfg/schema-reference.md b/docs/sources/developers/kinds/composable/xychart/panelcfg/schema-reference.md deleted file mode 100644 index 4b7cb0ca76..0000000000 --- a/docs/sources/developers/kinds/composable/xychart/panelcfg/schema-reference.md +++ /dev/null @@ -1,242 +0,0 @@ ---- -keywords: - - grafana - - schema -labels: - products: - - cloud - - enterprise - - oss -title: XYChartPanelCfg kind ---- -> Both documentation generation and kinds schemas are in active development and subject to change without prior notice. - -## XYChartPanelCfg - -#### Maturity: [experimental](../../../maturity/#experimental) -#### Version: 0.0 - - - -| Property | Type | Required | Default | Description | -|-----------------------|--------------------------------|----------|---------|----------------------------------------------------------------------| -| `FieldConfig` | [object](#fieldconfig) | **Yes** | | | -| `Options` | [object](#options) | **Yes** | | | -| `ScatterSeriesConfig` | [object](#scatterseriesconfig) | **Yes** | | | -| `ScatterShow` | string | **Yes** | | Possible values are: `points`, `lines`, `points+lines`. | -| `SeriesMapping` | string | **Yes** | | Auto is "table" in the UI
Possible values are: `auto`, `manual`. | -| `XYDimensionConfig` | [object](#xydimensionconfig) | **Yes** | | Configuration for the Table/Auto mode | - -### FieldConfig - -It extends [HideableFieldConfig](#hideablefieldconfig) and [AxisConfig](#axisconfig). - -| Property | Type | Required | Default | Description | -|---------------------|-----------------------------------------------------|----------|---------|-----------------------------------------------------------------------------------------------------------------------------------------| -| `axisBorderShow` | boolean | No | | *(Inherited from [AxisConfig](#axisconfig))* | -| `axisCenteredZero` | boolean | No | | *(Inherited from [AxisConfig](#axisconfig))* | -| `axisColorMode` | string | No | | *(Inherited from [AxisConfig](#axisconfig))*
TODO docs
Possible values are: `text`, `series`. | -| `axisGridShow` | boolean | No | | *(Inherited from [AxisConfig](#axisconfig))* | -| `axisLabel` | string | No | | *(Inherited from [AxisConfig](#axisconfig))* | -| `axisPlacement` | string | No | | *(Inherited from [AxisConfig](#axisconfig))*
TODO docs
Possible values are: `auto`, `top`, `right`, `bottom`, `left`, `hidden`. | -| `axisSoftMax` | number | No | | *(Inherited from [AxisConfig](#axisconfig))* | -| `axisSoftMin` | number | No | | *(Inherited from [AxisConfig](#axisconfig))* | -| `axisWidth` | number | No | | *(Inherited from [AxisConfig](#axisconfig))* | -| `hideFrom` | [HideSeriesConfig](#hideseriesconfig) | No | | *(Inherited from [HideableFieldConfig](#hideablefieldconfig))*
TODO docs | -| `labelValue` | [TextDimensionConfig](#textdimensionconfig) | No | | | -| `label` | string | No | | TODO docs
Possible values are: `auto`, `never`, `always`. | -| `lineColor` | [ColorDimensionConfig](#colordimensionconfig) | No | | | -| `lineStyle` | [LineStyle](#linestyle) | No | | TODO docs | -| `lineWidth` | integer | No | | Constraint: `>=0 & <=2147483647`. | -| `pointColor` | [ColorDimensionConfig](#colordimensionconfig) | No | | | -| `pointSize` | [ScaleDimensionConfig](#scaledimensionconfig) | No | | | -| `scaleDistribution` | [ScaleDistributionConfig](#scaledistributionconfig) | No | | *(Inherited from [AxisConfig](#axisconfig))*
TODO docs | -| `show` | string | No | | Possible values are: `points`, `lines`, `points+lines`. | - -### AxisConfig - -TODO docs - -| Property | Type | Required | Default | Description | -|---------------------|-----------------------------------------------------|----------|---------|----------------------------------------------------------------------------------------| -| `axisBorderShow` | boolean | No | | | -| `axisCenteredZero` | boolean | No | | | -| `axisColorMode` | string | No | | TODO docs
Possible values are: `text`, `series`. | -| `axisGridShow` | boolean | No | | | -| `axisLabel` | string | No | | | -| `axisPlacement` | string | No | | TODO docs
Possible values are: `auto`, `top`, `right`, `bottom`, `left`, `hidden`. | -| `axisSoftMax` | number | No | | | -| `axisSoftMin` | number | No | | | -| `axisWidth` | number | No | | | -| `scaleDistribution` | [ScaleDistributionConfig](#scaledistributionconfig) | No | | TODO docs | - -### ScaleDistributionConfig - -TODO docs - -| Property | Type | Required | Default | Description | -|-------------------|--------|----------|---------|--------------------------------------------------------------------------| -| `type` | string | **Yes** | | TODO docs
Possible values are: `linear`, `log`, `ordinal`, `symlog`. | -| `linearThreshold` | number | No | | | -| `log` | number | No | | | - -### ColorDimensionConfig - -It extends [BaseDimensionConfig](#basedimensionconfig). - -| Property | Type | Required | Default | Description | -|----------|--------|----------|---------|--------------------------------------------------------------------------------------------------------------| -| `field` | string | No | | *(Inherited from [BaseDimensionConfig](#basedimensionconfig))*
fixed: T -- will be added by each element | -| `fixed` | string | No | | | - -### BaseDimensionConfig - -| Property | Type | Required | Default | Description | -|----------|--------|----------|---------|-------------------------------------------| -| `field` | string | No | | fixed: T -- will be added by each element | - -### HideSeriesConfig - -TODO docs - -| Property | Type | Required | Default | Description | -|-----------|---------|----------|---------|-------------| -| `legend` | boolean | **Yes** | | | -| `tooltip` | boolean | **Yes** | | | -| `viz` | boolean | **Yes** | | | - -### HideableFieldConfig - -TODO docs - -| Property | Type | Required | Default | Description | -|------------|---------------------------------------|----------|---------|-------------| -| `hideFrom` | [HideSeriesConfig](#hideseriesconfig) | No | | TODO docs | - -### LineStyle - -TODO docs - -| Property | Type | Required | Default | Description | -|----------|----------|----------|---------|--------------------------------------------------------| -| `dash` | number[] | No | | | -| `fill` | string | No | | Possible values are: `solid`, `dash`, `dot`, `square`. | - -### ScaleDimensionConfig - -It extends [BaseDimensionConfig](#basedimensionconfig). - -| Property | Type | Required | Default | Description | -|----------|--------|----------|---------|--------------------------------------------------------------------------------------------------------------| -| `max` | number | **Yes** | | | -| `min` | number | **Yes** | | | -| `field` | string | No | | *(Inherited from [BaseDimensionConfig](#basedimensionconfig))*
fixed: T -- will be added by each element | -| `fixed` | number | No | | | -| `mode` | string | No | | Possible values are: `linear`, `quad`. | - -### TextDimensionConfig - -It extends [BaseDimensionConfig](#basedimensionconfig). - -| Property | Type | Required | Default | Description | -|----------|--------|----------|---------|--------------------------------------------------------------------------------------------------------------| -| `mode` | string | **Yes** | | Possible values are: `fixed`, `field`, `template`. | -| `field` | string | No | | *(Inherited from [BaseDimensionConfig](#basedimensionconfig))*
fixed: T -- will be added by each element | -| `fixed` | string | No | | | - -### Options - -It extends [OptionsWithLegend](#optionswithlegend) and [OptionsWithTooltip](#optionswithtooltip). - -| Property | Type | Required | Default | Description | -|-----------------|-----------------------------------------------|----------|---------|----------------------------------------------------------------------------| -| `dims` | [XYDimensionConfig](#xydimensionconfig) | **Yes** | | Configuration for the Table/Auto mode | -| `legend` | [VizLegendOptions](#vizlegendoptions) | **Yes** | | *(Inherited from [OptionsWithLegend](#optionswithlegend))*
TODO docs | -| `series` | [ScatterSeriesConfig](#scatterseriesconfig)[] | **Yes** | | Manual Mode | -| `tooltip` | [VizTooltipOptions](#viztooltipoptions) | **Yes** | | *(Inherited from [OptionsWithTooltip](#optionswithtooltip))*
TODO docs | -| `seriesMapping` | string | No | | Auto is "table" in the UI
Possible values are: `auto`, `manual`. | - -### OptionsWithLegend - -TODO docs - -| Property | Type | Required | Default | Description | -|----------|---------------------------------------|----------|---------|-------------| -| `legend` | [VizLegendOptions](#vizlegendoptions) | **Yes** | | TODO docs | - -### VizLegendOptions - -TODO docs - -| Property | Type | Required | Default | Description | -|---------------|----------|----------|---------|-----------------------------------------------------------------------------------------------------------------------------------------| -| `calcs` | string[] | **Yes** | | | -| `displayMode` | string | **Yes** | | TODO docs
Note: "hidden" needs to remain as an option for plugins compatibility
Possible values are: `list`, `table`, `hidden`. | -| `placement` | string | **Yes** | | TODO docs
Possible values are: `bottom`, `right`. | -| `showLegend` | boolean | **Yes** | | | -| `asTable` | boolean | No | | | -| `isVisible` | boolean | No | | | -| `sortBy` | string | No | | | -| `sortDesc` | boolean | No | | | -| `width` | number | No | | | - -### OptionsWithTooltip - -TODO docs - -| Property | Type | Required | Default | Description | -|-----------|-----------------------------------------|----------|---------|-------------| -| `tooltip` | [VizTooltipOptions](#viztooltipoptions) | **Yes** | | TODO docs | - -### VizTooltipOptions - -TODO docs - -| Property | Type | Required | Default | Description | -|-------------|--------|----------|---------|---------------------------------------------------------------| -| `mode` | string | **Yes** | | TODO docs
Possible values are: `single`, `multi`, `none`. | -| `sort` | string | **Yes** | | TODO docs
Possible values are: `asc`, `desc`, `none`. | -| `maxHeight` | number | No | | | -| `maxWidth` | number | No | | | - -### ScatterSeriesConfig - -It extends [FieldConfig](#fieldconfig). - -| Property | Type | Required | Default | Description | -|---------------------|-----------------------------------------------------|----------|---------|-------------------------------------------------------------------------------------------------------------------------------------------| -| `axisBorderShow` | boolean | No | | *(Inherited from [FieldConfig](#fieldconfig))* | -| `axisCenteredZero` | boolean | No | | *(Inherited from [FieldConfig](#fieldconfig))* | -| `axisColorMode` | string | No | | *(Inherited from [FieldConfig](#fieldconfig))*
TODO docs
Possible values are: `text`, `series`. | -| `axisGridShow` | boolean | No | | *(Inherited from [FieldConfig](#fieldconfig))* | -| `axisLabel` | string | No | | *(Inherited from [FieldConfig](#fieldconfig))* | -| `axisPlacement` | string | No | | *(Inherited from [FieldConfig](#fieldconfig))*
TODO docs
Possible values are: `auto`, `top`, `right`, `bottom`, `left`, `hidden`. | -| `axisSoftMax` | number | No | | *(Inherited from [FieldConfig](#fieldconfig))* | -| `axisSoftMin` | number | No | | *(Inherited from [FieldConfig](#fieldconfig))* | -| `axisWidth` | number | No | | *(Inherited from [FieldConfig](#fieldconfig))* | -| `frame` | number | No | | | -| `hideFrom` | [HideSeriesConfig](#hideseriesconfig) | No | | *(Inherited from [FieldConfig](#fieldconfig))*
TODO docs | -| `labelValue` | [TextDimensionConfig](#textdimensionconfig) | No | | *(Inherited from [FieldConfig](#fieldconfig))* | -| `label` | string | No | | *(Inherited from [FieldConfig](#fieldconfig))*
TODO docs
Possible values are: `auto`, `never`, `always`. | -| `lineColor` | [ColorDimensionConfig](#colordimensionconfig) | No | | *(Inherited from [FieldConfig](#fieldconfig))* | -| `lineStyle` | [LineStyle](#linestyle) | No | | *(Inherited from [FieldConfig](#fieldconfig))*
TODO docs | -| `lineWidth` | integer | No | | *(Inherited from [FieldConfig](#fieldconfig))*
Constraint: `>=0 & <=2147483647`. | -| `name` | string | No | | | -| `pointColor` | [ColorDimensionConfig](#colordimensionconfig) | No | | *(Inherited from [FieldConfig](#fieldconfig))* | -| `pointSize` | [ScaleDimensionConfig](#scaledimensionconfig) | No | | *(Inherited from [FieldConfig](#fieldconfig))* | -| `scaleDistribution` | [ScaleDistributionConfig](#scaledistributionconfig) | No | | *(Inherited from [FieldConfig](#fieldconfig))*
TODO docs | -| `show` | string | No | | *(Inherited from [FieldConfig](#fieldconfig))*
Possible values are: `points`, `lines`, `points+lines`. | -| `x` | string | No | | | -| `y` | string | No | | | - -### XYDimensionConfig - -Configuration for the Table/Auto mode - -| Property | Type | Required | Default | Description | -|-----------|----------|----------|---------|-----------------------------------| -| `frame` | integer | **Yes** | | Constraint: `>=0 & <=2147483647`. | -| `exclude` | string[] | No | | | -| `x` | string | No | | | - - diff --git a/docs/sources/developers/kinds/core/_index.md b/docs/sources/developers/kinds/core/_index.md deleted file mode 100644 index f791015d5b..0000000000 --- a/docs/sources/developers/kinds/core/_index.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -labels: - products: - - enterprise - - oss -title: Core kinds -weight: 200 ---- - -# Grafana core kinds - -Kinds that define Grafana’s core schematized object types - dashboards, datasources, users, etc. - -{{< section >}} diff --git a/docs/sources/developers/kinds/core/accesspolicy/schema-reference.md b/docs/sources/developers/kinds/core/accesspolicy/schema-reference.md deleted file mode 100644 index 0df9b57ef0..0000000000 --- a/docs/sources/developers/kinds/core/accesspolicy/schema-reference.md +++ /dev/null @@ -1,131 +0,0 @@ ---- -keywords: - - grafana - - schema -labels: - products: - - cloud - - enterprise - - oss -title: AccessPolicy kind ---- -> Both documentation generation and kinds schemas are in active development and subject to change without prior notice. - -## AccessPolicy - -#### Maturity: [merged](../../../maturity/#merged) -#### Version: 0.0 - -Access rules for a scope+role. NOTE there is a unique constraint on role+scope - -| Property | Type | Required | Default | Description | -|------------|---------------------|----------|---------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `metadata` | [object](#metadata) | **Yes** | | metadata contains embedded CommonMetadata and can be extended with custom string fields
TODO: use CommonMetadata instead of redefining here; currently needs to be defined here
without external reference as using the CommonMetadata reference breaks thema codegen. | -| `spec` | [object](#spec) | **Yes** | | | -| `status` | [object](#status) | **Yes** | | | - -### Metadata - -metadata contains embedded CommonMetadata and can be extended with custom string fields -TODO: use CommonMetadata instead of redefining here; currently needs to be defined here -without external reference as using the CommonMetadata reference breaks thema codegen. - -It extends [_kubeObjectMetadata](#_kubeobjectmetadata). - -| Property | Type | Required | Default | Description | -|---------------------|------------------------|----------|---------|-----------------------------------------------------------------------------------------------------------------------------------------| -| `createdBy` | string | **Yes** | | | -| `creationTimestamp` | string | **Yes** | | *(Inherited from [_kubeObjectMetadata](#_kubeobjectmetadata))* | -| `extraFields` | [object](#extrafields) | **Yes** | | extraFields is reserved for any fields that are pulled from the API server metadata but do not have concrete fields in the CUE metadata | -| `finalizers` | string[] | **Yes** | | *(Inherited from [_kubeObjectMetadata](#_kubeobjectmetadata))* | -| `labels` | map[string]string | **Yes** | | *(Inherited from [_kubeObjectMetadata](#_kubeobjectmetadata))* | -| `resourceVersion` | string | **Yes** | | *(Inherited from [_kubeObjectMetadata](#_kubeobjectmetadata))* | -| `uid` | string | **Yes** | | *(Inherited from [_kubeObjectMetadata](#_kubeobjectmetadata))* | -| `updateTimestamp` | string | **Yes** | | | -| `updatedBy` | string | **Yes** | | | -| `deletionTimestamp` | string | No | | *(Inherited from [_kubeObjectMetadata](#_kubeobjectmetadata))* | - -### _kubeObjectMetadata - -_kubeObjectMetadata is metadata found in a kubernetes object's metadata field. -It is not exhaustive and only includes fields which may be relevant to a kind's implementation, -As it is also intended to be generic enough to function with any API Server. - -| Property | Type | Required | Default | Description | -|---------------------|-------------------|----------|---------|-------------| -| `creationTimestamp` | string | **Yes** | | | -| `finalizers` | string[] | **Yes** | | | -| `labels` | map[string]string | **Yes** | | | -| `resourceVersion` | string | **Yes** | | | -| `uid` | string | **Yes** | | | -| `deletionTimestamp` | string | No | | | - -### ExtraFields - -extraFields is reserved for any fields that are pulled from the API server metadata but do not have concrete fields in the CUE metadata - -| Property | Type | Required | Default | Description | -|----------|------|----------|---------|-------------| - -### Spec - -| Property | Type | Required | Default | Description | -|----------|-----------------------------|----------|---------|--------------------------------------------------------------------------------------------------------------------------------| -| `role` | [RoleRef](#roleref) | **Yes** | | | -| `rules` | [AccessRule](#accessrule)[] | **Yes** | | The set of rules to apply. Note that * is required to modify
access policy rules, and that "none" will reject all actions | -| `scope` | [ResourceRef](#resourceref) | **Yes** | | | - -### AccessRule - -| Property | Type | Required | Default | Description | -|----------|--------|----------|---------|-----------------------------------------------------------------------------------------------------------------------------------------| -| `kind` | string | **Yes** | | The kind this rule applies to (dashboards, alert, etc) | -| `verb` | string | **Yes** | | READ, WRITE, CREATE, DELETE, ...
should move to k8s style verbs like: "get", "list", "watch", "create", "update", "patch", "delete" | -| `target` | string | No | | Specific sub-elements like "alert.rules" or "dashboard.permissions"???? | - -### ResourceRef - -| Property | Type | Required | Default | Description | -|----------|--------|----------|---------|-------------| -| `kind` | string | **Yes** | | | -| `name` | string | **Yes** | | | - -### RoleRef - -| Property | Type | Required | Default | Description | -|----------|--------|----------|---------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `kind` | string | **Yes** | | Policies can apply to roles, teams, or users
Applying policies to individual users is supported, but discouraged
Possible values are: `Role`, `BuiltinRole`, `Team`, `User`. | -| `name` | string | **Yes** | | | -| `xname` | string | **Yes** | | | - -### Status - -| Property | Type | Required | Default | Description | -|--------------------|------------------------------------------------------------|----------|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `additionalFields` | [object](#additionalfields) | No | | additionalFields is reserved for future use | -| `operatorStates` | map[string][status.#OperatorState](#status.#operatorstate) | No | | operatorStates is a map of operator ID to operator state evaluations.
Any operator which consumes this kind SHOULD add its state evaluation information to this field. | - -### AdditionalFields - -additionalFields is reserved for future use - -| Property | Type | Required | Default | Description | -|----------|------|----------|---------|-------------| - -### Status.#OperatorState - -| Property | Type | Required | Default | Description | -|--------------------|--------------------|----------|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `lastEvaluation` | string | **Yes** | | lastEvaluation is the ResourceVersion last evaluated | -| `state` | string | **Yes** | | state describes the state of the lastEvaluation.
It is limited to three possible states for machine evaluation.
Possible values are: `success`, `in_progress`, `failed`. | -| `descriptiveState` | string | No | | descriptiveState is an optional more descriptive state field which has no requirements on format | -| `details` | [object](#details) | No | | details contains any extra information that is operator-specific | - -### Details - -details contains any extra information that is operator-specific - -| Property | Type | Required | Default | Description | -|----------|------|----------|---------|-------------| - - diff --git a/docs/sources/developers/kinds/core/dashboard/schema-reference.md b/docs/sources/developers/kinds/core/dashboard/schema-reference.md deleted file mode 100644 index 0bbf32a8c2..0000000000 --- a/docs/sources/developers/kinds/core/dashboard/schema-reference.md +++ /dev/null @@ -1,677 +0,0 @@ ---- -keywords: - - grafana - - schema -labels: - products: - - cloud - - enterprise - - oss -title: Dashboard kind ---- -> Both documentation generation and kinds schemas are in active development and subject to change without prior notice. - -## Dashboard - -#### Maturity: [experimental](../../../maturity/#experimental) -#### Version: 0.0 - -A Grafana dashboard. - -| Property | Type | Required | Default | Description | -|------------|---------------------|----------|---------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `metadata` | [object](#metadata) | **Yes** | | metadata contains embedded CommonMetadata and can be extended with custom string fields
TODO: use CommonMetadata instead of redefining here; currently needs to be defined here
without external reference as using the CommonMetadata reference breaks thema codegen. | -| `spec` | [object](#spec) | **Yes** | | | -| `status` | [object](#status) | **Yes** | | | - -### Metadata - -metadata contains embedded CommonMetadata and can be extended with custom string fields -TODO: use CommonMetadata instead of redefining here; currently needs to be defined here -without external reference as using the CommonMetadata reference breaks thema codegen. - -It extends [_kubeObjectMetadata](#_kubeobjectmetadata). - -| Property | Type | Required | Default | Description | -|---------------------|------------------------|----------|---------|-----------------------------------------------------------------------------------------------------------------------------------------| -| `createdBy` | string | **Yes** | | | -| `creationTimestamp` | string | **Yes** | | *(Inherited from [_kubeObjectMetadata](#_kubeobjectmetadata))* | -| `extraFields` | [object](#extrafields) | **Yes** | | extraFields is reserved for any fields that are pulled from the API server metadata but do not have concrete fields in the CUE metadata | -| `finalizers` | string[] | **Yes** | | *(Inherited from [_kubeObjectMetadata](#_kubeobjectmetadata))* | -| `labels` | map[string]string | **Yes** | | *(Inherited from [_kubeObjectMetadata](#_kubeobjectmetadata))* | -| `resourceVersion` | string | **Yes** | | *(Inherited from [_kubeObjectMetadata](#_kubeobjectmetadata))* | -| `uid` | string | **Yes** | | *(Inherited from [_kubeObjectMetadata](#_kubeobjectmetadata))* | -| `updateTimestamp` | string | **Yes** | | | -| `updatedBy` | string | **Yes** | | | -| `deletionTimestamp` | string | No | | *(Inherited from [_kubeObjectMetadata](#_kubeobjectmetadata))* | - -### _kubeObjectMetadata - -_kubeObjectMetadata is metadata found in a kubernetes object's metadata field. -It is not exhaustive and only includes fields which may be relevant to a kind's implementation, -As it is also intended to be generic enough to function with any API Server. - -| Property | Type | Required | Default | Description | -|---------------------|-------------------|----------|---------|-------------| -| `creationTimestamp` | string | **Yes** | | | -| `finalizers` | string[] | **Yes** | | | -| `labels` | map[string]string | **Yes** | | | -| `resourceVersion` | string | **Yes** | | | -| `uid` | string | **Yes** | | | -| `deletionTimestamp` | string | No | | | - -### ExtraFields - -extraFields is reserved for any fields that are pulled from the API server metadata but do not have concrete fields in the CUE metadata - -| Property | Type | Required | Default | Description | -|----------|------|----------|---------|-------------| - -### Spec - -| Property | Type | Required | Default | Description | -|------------------------|---------------------------------------------|----------|-----------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `schemaVersion` | uint16 | **Yes** | `36` | Version of the JSON schema, incremented each time a Grafana update brings
changes to said schema. | -| `annotations` | [AnnotationContainer](#annotationcontainer) | No | | Contains the list of annotations that are associated with the dashboard.
Annotations are used to overlay event markers and overlay event tags on graphs.
Grafana comes with a native annotation store and the ability to add annotation events directly from the graph panel or via the HTTP API.
See https://grafana.com/docs/grafana/latest/dashboards/build-dashboards/annotate-visualizations/ | -| `description` | string | No | | Description of dashboard. | -| `editable` | boolean | No | `true` | Whether a dashboard is editable or not. | -| `fiscalYearStartMonth` | integer | No | `0` | The month that the fiscal year starts on. 0 = January, 11 = December
Constraint: `>=0 & <12`. | -| `gnetId` | string | No | | ID of a dashboard imported from the https://grafana.com/grafana/dashboards/ portal | -| `graphTooltip` | integer | No | `0` | 0 for no shared crosshair or tooltip (default).
1 for shared crosshair.
2 for shared crosshair AND shared tooltip.
Possible values are: `0`, `1`, `2`. | -| `id` | integer or null | No | | Unique numeric identifier for the dashboard.
`id` is internal to a specific Grafana instance. `uid` should be used to identify a dashboard across Grafana instances. | -| `links` | [DashboardLink](#dashboardlink)[] | No | | Links with references to other dashboards or external websites. | -| `liveNow` | boolean | No | | When set to true, the dashboard will redraw panels at an interval matching the pixel width.
This will keep data "moving left" regardless of the query refresh rate. This setting helps
avoid dashboards presenting stale live data | -| `panels` | [object](#panels)[] | No | | List of dashboard panels | -| `refresh` | string | No | | Refresh rate of dashboard. Represented via interval string, e.g. "5s", "1m", "1h", "1d". | -| `revision` | integer | No | | 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. | -| `snapshot` | [Snapshot](#snapshot) | No | | A dashboard snapshot shares an interactive dashboard publicly.
It is a read-only version of a dashboard, and is not editable.
It is possible to create a snapshot of a snapshot.
Grafana strips away all sensitive information from the dashboard.
Sensitive information stripped: queries (metric, template,annotation) and panel links. | -| `tags` | string[] | No | | Tags associated with dashboard. | -| `templating` | [object](#templating) | No | | Configured template variables | -| `time` | [object](#time) | No | | Time range for dashboard.
Accepted values are relative time strings like {from: 'now-6h', to: 'now'} or absolute time strings like {from: '2020-07-10T08:00:00.000Z', to: '2020-07-10T14:00:00.000Z'}. | -| `timepicker` | [TimePickerConfig](#timepickerconfig) | No | | Time picker configuration
It defines the default config for the time picker and the refresh picker for the specific dashboard. | -| `timezone` | string | No | `browser` | Timezone of dashboard. Accepted values are IANA TZDB zone ID or "browser" or "utc". | -| `title` | string | No | | Title of dashboard. | -| `uid` | string | No | | Unique dashboard identifier that can be generated by anyone. string (8-40) | -| `version` | uint32 | No | | Version of the dashboard, incremented each time the dashboard is updated. | -| `weekStart` | string | No | | Day when the week starts. Expressed by the name of the day in lowercase, e.g. "monday". | - -### AnnotationContainer - -Contains the list of annotations that are associated with the dashboard. -Annotations are used to overlay event markers and overlay event tags on graphs. -Grafana comes with a native annotation store and the ability to add annotation events directly from the graph panel or via the HTTP API. -See https://grafana.com/docs/grafana/latest/dashboards/build-dashboards/annotate-visualizations/ - -| Property | Type | Required | Default | Description | -|----------|---------------------------------------|----------|---------|---------------------| -| `list` | [AnnotationQuery](#annotationquery)[] | No | | List of annotations | - -### AnnotationQuery - -TODO docs -FROM: AnnotationQuery in grafana-data/src/types/annotations.ts - -| Property | Type | Required | Default | Description | -|--------------|-------------------------------------------------|----------|---------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `datasource` | [DataSourceRef](#datasourceref) | **Yes** | | Ref to a DataSource instance | -| `enable` | boolean | **Yes** | `true` | When enabled the annotation query is issued with every dashboard refresh | -| `iconColor` | string | **Yes** | | Color to use for the annotation event markers | -| `name` | string | **Yes** | | Name of annotation. | -| `builtIn` | number | No | `0` | Set to 1 for the standard annotation query all dashboards have by default. | -| `filter` | [AnnotationPanelFilter](#annotationpanelfilter) | No | | | -| `hide` | boolean | No | `false` | Annotation queries can be toggled on or off at the top of the dashboard.
When hide is true, the toggle is not shown in the dashboard. | -| `target` | [AnnotationTarget](#annotationtarget) | No | | TODO: this should be a regular DataQuery that depends on the selected dashboard
these match the properties of the "grafana" datasouce that is default in most dashboards | -| `type` | string | No | | TODO -- this should not exist here, it is based on the --grafana-- datasource | - -### AnnotationPanelFilter - -| Property | Type | Required | Default | Description | -|-----------|-----------|----------|---------|-----------------------------------------------------| -| `ids` | integer[] | **Yes** | | Panel IDs that should be included or excluded | -| `exclude` | boolean | No | `false` | Should the specified panels be included or excluded | - -### AnnotationTarget - -TODO: this should be a regular DataQuery that depends on the selected dashboard -these match the properties of the "grafana" datasouce that is default in most dashboards - -| Property | Type | Required | Default | Description | -|------------|----------|----------|---------|-------------------------------------------------------------------------------------------------------------------| -| `limit` | integer | **Yes** | | Only required/valid for the grafana datasource...
but code+tests is already depending on it so hard to change | -| `matchAny` | boolean | **Yes** | | Only required/valid for the grafana datasource...
but code+tests is already depending on it so hard to change | -| `tags` | string[] | **Yes** | | Only required/valid for the grafana datasource...
but code+tests is already depending on it so hard to change | -| `type` | string | **Yes** | | Only required/valid for the grafana datasource...
but code+tests is already depending on it so hard to change | - -### DataSourceRef - -Ref to a DataSource instance - -| Property | Type | Required | Default | Description | -|----------|--------|----------|---------|------------------------------| -| `type` | string | No | | The plugin type-id | -| `uid` | string | No | | Specific datasource instance | - -### DashboardLink - -Links with references to other dashboards or external resources - -| Property | Type | Required | Default | Description | -|---------------|----------|----------|---------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `asDropdown` | boolean | **Yes** | `false` | If true, all dashboards links will be displayed in a dropdown. If false, all dashboards links will be displayed side by side. Only valid if the type is dashboards | -| `icon` | string | **Yes** | | Icon name to be displayed with the link | -| `includeVars` | boolean | **Yes** | `false` | If true, includes current template variables values in the link as query params | -| `keepTime` | boolean | **Yes** | `false` | If true, includes current time range in the link as query params | -| `tags` | string[] | **Yes** | | List of tags to limit the linked dashboards. If empty, all dashboards will be displayed. Only valid if the type is dashboards | -| `targetBlank` | boolean | **Yes** | `false` | If true, the link will be opened in a new tab | -| `title` | string | **Yes** | | Title to display with the link | -| `tooltip` | string | **Yes** | | Tooltip to display when the user hovers their mouse over it | -| `type` | string | **Yes** | | Dashboard Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource)
Possible values are: `link`, `dashboards`. | -| `url` | string | No | | Link URL. Only required/valid if the type is link | - -### Snapshot - -A dashboard snapshot shares an interactive dashboard publicly. -It is a read-only version of a dashboard, and is not editable. -It is possible to create a snapshot of a snapshot. -Grafana strips away all sensitive information from the dashboard. -Sensitive information stripped: queries (metric, template,annotation) and panel links. - -| Property | Type | Required | Default | Description | -|---------------|---------|----------|---------|--------------------------------------------------------------------------------| -| `created` | string | **Yes** | | Time when the snapshot was created | -| `expires` | string | **Yes** | | Time when the snapshot expires, default is never to expire | -| `externalUrl` | string | **Yes** | | external url, if snapshot was shared in external grafana instance | -| `external` | boolean | **Yes** | | Is the snapshot saved in an external grafana instance | -| `id` | uint32 | **Yes** | | Unique identifier of the snapshot | -| `key` | string | **Yes** | | Optional, defined the unique key of the snapshot, required if external is true | -| `name` | string | **Yes** | | Optional, name of the snapshot | -| `orgId` | uint32 | **Yes** | | org id of the snapshot | -| `originalUrl` | string | **Yes** | | original url, url of the dashboard that was snapshotted | -| `updated` | string | **Yes** | | last time when the snapshot was updated | -| `userId` | uint32 | **Yes** | | user id of the snapshot creator | -| `url` | string | No | | url of the snapshot, if snapshot was shared internally | - -### TimePickerConfig - -Time picker configuration -It defines the default config for the time picker and the refresh picker for the specific dashboard. - -| Property | Type | Required | Default | Description | -|---------------------|----------|----------|---------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------| -| `hidden` | boolean | No | `false` | Whether timepicker is visible or not. | -| `nowDelay` | string | No | | Override the now time by entering a time delay. Use this option to accommodate known delays in data aggregation to avoid null values. | -| `refresh_intervals` | string[] | No | `[5s 10s 30s 1m 5m 15m 30m 1h 2h 1d]` | Interval options available in the refresh picker dropdown. | -| `time_options` | string[] | No | `[5m 15m 1h 6h 12h 24h 2d 7d 30d]` | Selectable options available in the time picker dropdown. Has no effect on provisioned dashboard. | - -### Panels - -| Property | Type | Required | Default | Description | -|----------|---------------------------------------------------|----------|---------|-------------| -| `object` | Possible types are: [](#), [RowPanel](#rowpanel). | | | - -### DataTransformerConfig - -Transformations allow to manipulate data returned by a query before the system applies a visualization. -Using transformations you can: rename fields, join time series data, perform mathematical operations across queries, -use the output of one transformation as the input to another transformation, etc. - -| Property | Type | Required | Default | Description | -|------------|---------------------------------|----------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `id` | string | **Yes** | | Unique identifier of transformer | -| `options` | | **Yes** | | Options to be passed to the transformer
Valid options depend on the transformer id | -| `disabled` | boolean | No | | Disabled transformations are skipped | -| `filter` | [MatcherConfig](#matcherconfig) | No | | Matcher is a predicate configuration. Based on the config a set of field(s) or values is filtered in order to apply override / transformation.
It comes with in id ( to resolve implementation from registry) and a configuration that’s specific to a particular matcher type. | -| `topic` | string | No | | Where to pull DataFrames from as input to transformation
Possible values are: `series`, `annotations`, `alertStates`. | - -### MatcherConfig - -Matcher is a predicate configuration. Based on the config a set of field(s) or values is filtered in order to apply override / transformation. -It comes with in id ( to resolve implementation from registry) and a configuration that’s specific to a particular matcher type. - -| Property | Type | Required | Default | Description | -|-----------|--------|----------|---------|--------------------------------------------------------------------------------| -| `id` | string | **Yes** | `` | The matcher id. This is used to find the matcher implementation from registry. | -| `options` | | No | | The matcher options. This is specific to the matcher implementation. | - -### FieldConfigSource - -The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results. -Each column within this structure is called a field. A field can represent a single time series or table column. -Field options allow you to change how the data is displayed in your visualizations. - -| Property | Type | Required | Default | Description | -|-------------|-----------------------------|----------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `defaults` | [FieldConfig](#fieldconfig) | **Yes** | | The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results.
Each column within this structure is called a field. A field can represent a single time series or table column.
Field options allow you to change how the data is displayed in your visualizations. | -| `overrides` | [object](#overrides)[] | **Yes** | | Overrides are the options applied to specific fields overriding the defaults. | - -### FieldConfig - -The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results. -Each column within this structure is called a field. A field can represent a single time series or table column. -Field options allow you to change how the data is displayed in your visualizations. - -| Property | Type | Required | Default | Description | -|---------------------|---------------------------------------|----------|---------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `color` | [FieldColor](#fieldcolor) | No | | Map a field to a color. | -| `custom` | [object](#custom) | No | | custom is specified by the FieldConfig field
in panel plugin schemas. | -| `decimals` | number | No | | Specify the number of decimals Grafana includes in the rendered value.
If you leave this field blank, Grafana automatically truncates the number of decimals based on the value.
For example 1.1234 will display as 1.12 and 100.456 will display as 100.
To display all decimals, set the unit to `String`. | -| `description` | string | No | | Human readable field metadata | -| `displayNameFromDS` | string | No | | This can be used by data sources that return and explicit naming structure for values and labels
When this property is configured, this value is used rather than the default naming strategy. | -| `displayName` | string | No | | The display value for this field. This supports template variables blank is auto | -| `filterable` | boolean | No | | True if data source field supports ad-hoc filters | -| `links` | | No | | The behavior when clicking on a result | -| `mappings` | [ValueMapping](#valuemapping)[] | No | | Convert input values into a display string | -| `max` | number | No | | The maximum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields. | -| `min` | number | No | | The minimum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields. | -| `noValue` | string | No | | Alternative to empty string | -| `path` | string | No | | An explicit path to the field in the datasource. When the frame meta includes a path,
This will default to `${frame.meta.path}/${field.name}

When defined, this value can be used as an identifier within the datasource scope, and
may be used to update the results | -| `thresholds` | [ThresholdsConfig](#thresholdsconfig) | No | | Thresholds configuration for the panel | -| `unit` | string | No | | Unit a field should use. The unit you select is applied to all fields except time.
You can use the units ID availables in Grafana or a custom unit.
Available units in Grafana: https://github.com/grafana/grafana/blob/main/packages/grafana-data/src/valueFormats/categories.ts
As custom unit, you can use the following formats:
`suffix:` for custom unit that should go after value.
`prefix:` for custom unit that should go before value.
`time:` For custom date time formats type for example `time:YYYY-MM-DD`.
`si:` for custom SI units. For example: `si: mF`. This one is a bit more advanced as you can specify both a unit and the source data scale. So if your source data is represented as milli (thousands of) something prefix the unit with that SI scale character.
`count:` for a custom count unit.
`currency:` for custom a currency unit. | -| `writeable` | boolean | No | | True if data source can write a value to the path. Auth/authz are supported separately | - -### FieldColor - -Map a field to a color. - -| Property | Type | Required | Default | Description | -|--------------|--------|----------|---------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `mode` | string | **Yes** | | Color mode for a field. You can specify a single color, or select a continuous (gradient) color schemes, based on a value.
Continuous color interpolates a color using the percentage of a value relative to min and max.
Accepted values are:
`thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold
`palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations
`palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations
`continuous-GrYlRd`: ontinuous Green-Yellow-Red palette mode
`continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode
`continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode
`continuous-YlRd`: Continuous Yellow-Red palette mode
`continuous-BlPu`: Continuous Blue-Purple palette mode
`continuous-YlBl`: Continuous Yellow-Blue palette mode
`continuous-blues`: Continuous Blue palette mode
`continuous-reds`: Continuous Red palette mode
`continuous-greens`: Continuous Green palette mode
`continuous-purples`: Continuous Purple palette mode
`shades`: Shades of a single color. Specify a single color, useful in an override rule.
`fixed`: Fixed color mode. Specify a single color, useful in an override rule.
Possible values are: `thresholds`, `palette-classic`, `palette-classic-by-name`, `continuous-GrYlRd`, `continuous-RdYlGr`, `continuous-BlYlRd`, `continuous-YlRd`, `continuous-BlPu`, `continuous-YlBl`, `continuous-blues`, `continuous-reds`, `continuous-greens`, `continuous-purples`, `fixed`, `shades`. | -| `fixedColor` | string | No | | The fixed color value for fixed or shades color modes. | -| `seriesBy` | string | No | | Defines how to assign a series color from "by value" color schemes. For example for an aggregated data points like a timeseries, the color can be assigned by the min, max or last value.
Possible values are: `min`, `max`, `last`. | - -### ThresholdsConfig - -Thresholds configuration for the panel - -| Property | Type | Required | Default | Description | -|----------|---------------------------|----------|---------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `mode` | string | **Yes** | | Thresholds can either be `absolute` (specific number) or `percentage` (relative to min or max, it will be values between 0 and 1).
Possible values are: `absolute`, `percentage`. | -| `steps` | [Threshold](#threshold)[] | **Yes** | | Must be sorted by 'value', first value is always -Infinity | - -### Threshold - -User-defined value for a metric that triggers visual changes in a panel when this value is met or exceeded -They are used to conditionally style and color visualizations based on query results , and can be applied to most visualizations. - -| Property | Type | Required | Default | Description | -|----------|----------------|----------|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `color` | string | **Yes** | | Color represents the color of the visual change that will occur in the dashboard when the threshold value is met or exceeded. | -| `value` | number or null | **Yes** | | Value represents a specified metric for the threshold, which triggers a visual change in the dashboard when this value is met or exceeded.
Nulls currently appear here when serializing -Infinity to JSON. | - -### ValueMapping - -Allow to transform the visual representation of specific data values in a visualization, irrespective of their original units - -| Property | Type | Required | Default | Description | -|----------|-------------------------------------------------------------------------------------------------------------------------------|----------|---------|-------------| -| `object` | Possible types are: [ValueMap](#valuemap), [RangeMap](#rangemap), [RegexMap](#regexmap), [SpecialValueMap](#specialvaluemap). | | | - -### RangeMap - -Maps numerical ranges to a display text and color. -For example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number. - -| Property | Type | Required | Default | Description | -|-----------|--------------------|----------|---------|-----------------------------------------------------------------------------------| -| `options` | [object](#options) | **Yes** | | Range to match against and the result to apply when the value is within the range | -| `type` | string | **Yes** | | | - -### Options - -Range to match against and the result to apply when the value is within the range - -| Property | Type | Required | Default | Description | -|----------|-------------------------------------------|----------|---------|-----------------------------------------------------------------------| -| `from` | number or null | **Yes** | | Min value of the range. It can be null which means -Infinity | -| `result` | [ValueMappingResult](#valuemappingresult) | **Yes** | | Result used as replacement with text and color when the value matches | -| `to` | number or null | **Yes** | | Max value of the range. It can be null which means +Infinity | - -### ValueMappingResult - -Result used as replacement with text and color when the value matches - -| Property | Type | Required | Default | Description | -|----------|---------|----------|---------|-----------------------------------------------------------------------| -| `color` | string | No | | Text to use when the value matches | -| `icon` | string | No | | Icon to display when the value matches. Only specific visualizations. | -| `index` | integer | No | | Position in the mapping array. Only used internally. | -| `text` | string | No | | Text to display when the value matches | - -### RegexMap - -Maps regular expressions to replacement text and a color. -For example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain. - -| Property | Type | Required | Default | Description | -|-----------|--------------------|----------|---------|----------------------------------------------------------------------------------------------| -| `options` | [object](#options) | **Yes** | | Regular expression to match against and the result to apply when the value matches the regex | -| `type` | string | **Yes** | | | - -### Options - -Regular expression to match against and the result to apply when the value matches the regex - -| Property | Type | Required | Default | Description | -|-----------|-------------------------------------------|----------|---------|-----------------------------------------------------------------------| -| `pattern` | string | **Yes** | | Regular expression to match against | -| `result` | [ValueMappingResult](#valuemappingresult) | **Yes** | | Result used as replacement with text and color when the value matches | - -### SpecialValueMap - -Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color. -See SpecialValueMatch to see the list of special values. -For example, you can configure a special value mapping so that null values appear as N/A. - -| Property | Type | Required | Default | Description | -|-----------|--------------------|----------|---------|-------------| -| `options` | [object](#options) | **Yes** | | | -| `type` | string | **Yes** | | | - -### Options - -| Property | Type | Required | Default | Description | -|----------|-------------------------------------------|----------|---------|--------------------------------------------------------------------------------------------------------------------------------------| -| `match` | string | **Yes** | | Special value types supported by the `SpecialValueMap`
Possible values are: `true`, `false`, `null`, `nan`, `null+nan`, `empty`. | -| `result` | [ValueMappingResult](#valuemappingresult) | **Yes** | | Result used as replacement with text and color when the value matches | - -### ValueMap - -Maps text values to a color or different display text and color. -For example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number. - -| Property | Type | Required | Default | Description | -|-----------|------------------------------------------------------|----------|---------|---------------------------------------------------------------------------------------------------------------| -| `options` | map[string][ValueMappingResult](#valuemappingresult) | **Yes** | | Map with : ValueMappingResult. For example: { "10": { text: "Perfection!", color: "green" } } | -| `type` | string | **Yes** | | | - -### Custom - -custom is specified by the FieldConfig field -in panel plugin schemas. - -| Property | Type | Required | Default | Description | -|----------|------|----------|---------|-------------| - -### Overrides - -| Property | Type | Required | Default | Description | -|--------------|---------------------------------------------|----------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `matcher` | [MatcherConfig](#matcherconfig) | **Yes** | | Matcher is a predicate configuration. Based on the config a set of field(s) or values is filtered in order to apply override / transformation.
It comes with in id ( to resolve implementation from registry) and a configuration that’s specific to a particular matcher type. | -| `properties` | [DynamicConfigValue](#dynamicconfigvalue)[] | **Yes** | | | - -### DynamicConfigValue - -| Property | Type | Required | Default | Description | -|----------|--------|----------|---------|-------------| -| `id` | string | **Yes** | `` | | -| `value` | | No | | | - -### GridPos - -Position and dimensions of a panel in the grid - -| Property | Type | Required | Default | Description | -|----------|---------|----------|---------|-------------------------------------------------------------------------------------------------------------------| -| `h` | uint32 | **Yes** | `9` | Panel height. The height is the number of rows from the top edge of the panel. | -| `w` | integer | **Yes** | `12` | Panel width. The width is the number of columns from the left edge of the panel.
Constraint: `>0 & <=24`. | -| `x` | integer | **Yes** | `0` | Panel x. The x coordinate is the number of columns from the left edge of the grid
Constraint: `>=0 & <24`. | -| `y` | uint32 | **Yes** | `0` | Panel y. The y coordinate is the number of rows from the top edge of the grid | -| `static` | boolean | No | | Whether the panel is fixed within the grid. If true, the panel will not be affected by other panels' interactions | - -### LibraryPanelRef - -A library panel is a reusable panel that you can use in any dashboard. -When you make a change to a library panel, that change propagates to all instances of where the panel is used. -Library panels streamline reuse of panels across multiple dashboards. - -| Property | Type | Required | Default | Description | -|----------|--------|----------|---------|--------------------| -| `name` | string | **Yes** | | Library panel name | -| `uid` | string | **Yes** | | Library panel uid | - -### Panel - -Dashboard panels are the basic visualization building blocks. - -| Property | Type | Required | Default | Description | -|--------------------|---------------------------------------------------|----------|---------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `type` | string | **Yes** | | The panel plugin type id. This is used to find the plugin to display the panel.
Constraint: `length >=1`. | -| `cacheTimeout` | string | No | | Sets panel queries cache timeout. | -| `datasource` | [DataSourceRef](#datasourceref) | No | | Ref to a DataSource instance | -| `description` | string | No | | Panel description. | -| `fieldConfig` | [FieldConfigSource](#fieldconfigsource) | No | | The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results.
Each column within this structure is called a field. A field can represent a single time series or table column.
Field options allow you to change how the data is displayed in your visualizations. | -| `gridPos` | [GridPos](#gridpos) | No | | Position and dimensions of a panel in the grid | -| `hideTimeOverride` | boolean | No | | Controls if the timeFrom or timeShift overrides are shown in the panel header | -| `id` | uint32 | No | | Unique identifier of the panel. Generated by Grafana when creating a new panel. It must be unique within a dashboard, but not globally. | -| `interval` | string | No | | The min time interval setting defines a lower limit for the $__interval and $__interval_ms variables.
This value must be formatted as a number followed by a valid time
identifier like: "40s", "3d", etc.
See: https://grafana.com/docs/grafana/latest/panels-visualizations/query-transform-data/#query-options | -| `libraryPanel` | [LibraryPanelRef](#librarypanelref) | No | | A library panel is a reusable panel that you can use in any dashboard.
When you make a change to a library panel, that change propagates to all instances of where the panel is used.
Library panels streamline reuse of panels across multiple dashboards. | -| `links` | [DashboardLink](#dashboardlink)[] | No | | Panel links. | -| `maxDataPoints` | number | No | | The maximum number of data points that the panel queries are retrieving. | -| `maxPerRow` | number | No | | Option for repeated panels that controls max items per row
Only relevant for horizontally repeated panels | -| `options` | [object](#options) | No | | It depends on the panel plugin. They are specified by the Options field in panel plugin schemas. | -| `pluginVersion` | string | No | | The version of the plugin that is used for this panel. This is used to find the plugin to display the panel and to migrate old panel configs. | -| `queryCachingTTL` | number | No | | Overrides the data source configured time-to-live for a query cache item in milliseconds | -| `repeatDirection` | string | No | `h` | Direction to repeat in if 'repeat' is set.
`h` for horizontal, `v` for vertical.
Possible values are: `h`, `v`. | -| `repeat` | string | No | | Name of template variable to repeat for. | -| `targets` | [Target](#target)[] | No | | Depends on the panel plugin. See the plugin documentation for details. | -| `timeFrom` | string | No | | Overrides the relative time range for individual panels,
which causes them to be different than what is selected in
the dashboard time picker in the top-right corner of the dashboard. You can use this to show metrics from different
time periods or days on the same dashboard.
The value is formatted as time operation like: `now-5m` (Last 5 minutes), `now/d` (the day so far),
`now-5d/d`(Last 5 days), `now/w` (This week so far), `now-2y/y` (Last 2 years).
Note: Panel time overrides have no effect when the dashboard’s time range is absolute.
See: https://grafana.com/docs/grafana/latest/panels-visualizations/query-transform-data/#query-options | -| `timeShift` | string | No | | Overrides the time range for individual panels by shifting its start and end relative to the time picker.
For example, you can shift the time range for the panel to be two hours earlier than the dashboard time picker setting `2h`.
Note: Panel time overrides have no effect when the dashboard’s time range is absolute.
See: https://grafana.com/docs/grafana/latest/panels-visualizations/query-transform-data/#query-options | -| `title` | string | No | | Panel title. | -| `transformations` | [DataTransformerConfig](#datatransformerconfig)[] | No | | List of transformations that are applied to the panel data before rendering.
When there are multiple transformations, Grafana applies them in the order they are listed.
Each transformation creates a result set that then passes on to the next transformation in the processing pipeline. | -| `transparent` | boolean | No | `false` | Whether to display the panel without a background. | - -### Target - -Schema for panel targets is specified by datasource -plugins. We use a placeholder definition, which the Go -schema loader either left open/as-is with the Base -variant of the Dashboard and Panel families, or filled -with types derived from plugins in the Instance variant. -When working directly from CUE, importers can extend this -type directly to achieve the same effect. - -| Property | Type | Required | Default | Description | -|----------|------|----------|---------|-------------| - -### Options - -It depends on the panel plugin. They are specified by the Options field in panel plugin schemas. - -| Property | Type | Required | Default | Description | -|----------|------|----------|---------|-------------| - -### RowPanel - -Row panel - -| Property | Type | Required | Default | Description | -|--------------|---------------------------------|----------|---------|-----------------------------------------------------------------------------------------------------------------------------------------| -| `collapsed` | boolean | **Yes** | `false` | Whether this row should be collapsed or not. | -| `id` | uint32 | **Yes** | | Unique identifier of the panel. Generated by Grafana when creating a new panel. It must be unique within a dashboard, but not globally. | -| `panels` | [Panel](#panel)[] | **Yes** | | List of panels in the row | -| `type` | string | **Yes** | | The panel type
Possible values are: `row`. | -| `datasource` | [DataSourceRef](#datasourceref) | No | | Ref to a DataSource instance | -| `gridPos` | [GridPos](#gridpos) | No | | Position and dimensions of a panel in the grid | -| `repeat` | string | No | | Name of template variable to repeat for. | -| `title` | string | No | | Row title | - -### Panel - -Dashboard panels are the basic visualization building blocks. - -| Property | Type | Required | Default | Description | -|--------------------|---------------------------------------------------|----------|---------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `type` | string | **Yes** | | The panel plugin type id. This is used to find the plugin to display the panel.
Constraint: `length >=1`. | -| `cacheTimeout` | string | No | | Sets panel queries cache timeout. | -| `datasource` | [DataSourceRef](#datasourceref) | No | | Ref to a DataSource instance | -| `description` | string | No | | Panel description. | -| `fieldConfig` | [FieldConfigSource](#fieldconfigsource) | No | | The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results.
Each column within this structure is called a field. A field can represent a single time series or table column.
Field options allow you to change how the data is displayed in your visualizations. | -| `gridPos` | [GridPos](#gridpos) | No | | Position and dimensions of a panel in the grid | -| `hideTimeOverride` | boolean | No | | Controls if the timeFrom or timeShift overrides are shown in the panel header | -| `id` | uint32 | No | | Unique identifier of the panel. Generated by Grafana when creating a new panel. It must be unique within a dashboard, but not globally. | -| `interval` | string | No | | The min time interval setting defines a lower limit for the $__interval and $__interval_ms variables.
This value must be formatted as a number followed by a valid time
identifier like: "40s", "3d", etc.
See: https://grafana.com/docs/grafana/latest/panels-visualizations/query-transform-data/#query-options | -| `libraryPanel` | [LibraryPanelRef](#librarypanelref) | No | | A library panel is a reusable panel that you can use in any dashboard.
When you make a change to a library panel, that change propagates to all instances of where the panel is used.
Library panels streamline reuse of panels across multiple dashboards. | -| `links` | [DashboardLink](#dashboardlink)[] | No | | Panel links. | -| `maxDataPoints` | number | No | | The maximum number of data points that the panel queries are retrieving. | -| `maxPerRow` | number | No | | Option for repeated panels that controls max items per row
Only relevant for horizontally repeated panels | -| `options` | [options](#options) | No | | It depends on the panel plugin. They are specified by the Options field in panel plugin schemas. | -| `pluginVersion` | string | No | | The version of the plugin that is used for this panel. This is used to find the plugin to display the panel and to migrate old panel configs. | -| `queryCachingTTL` | number | No | | Overrides the data source configured time-to-live for a query cache item in milliseconds | -| `repeatDirection` | string | No | `h` | Direction to repeat in if 'repeat' is set.
`h` for horizontal, `v` for vertical.
Possible values are: `h`, `v`. | -| `repeat` | string | No | | Name of template variable to repeat for. | -| `targets` | [Target](#target)[] | No | | Depends on the panel plugin. See the plugin documentation for details. | -| `timeFrom` | string | No | | Overrides the relative time range for individual panels,
which causes them to be different than what is selected in
the dashboard time picker in the top-right corner of the dashboard. You can use this to show metrics from different
time periods or days on the same dashboard.
The value is formatted as time operation like: `now-5m` (Last 5 minutes), `now/d` (the day so far),
`now-5d/d`(Last 5 days), `now/w` (This week so far), `now-2y/y` (Last 2 years).
Note: Panel time overrides have no effect when the dashboard’s time range is absolute.
See: https://grafana.com/docs/grafana/latest/panels-visualizations/query-transform-data/#query-options | -| `timeShift` | string | No | | Overrides the time range for individual panels by shifting its start and end relative to the time picker.
For example, you can shift the time range for the panel to be two hours earlier than the dashboard time picker setting `2h`.
Note: Panel time overrides have no effect when the dashboard’s time range is absolute.
See: https://grafana.com/docs/grafana/latest/panels-visualizations/query-transform-data/#query-options | -| `title` | string | No | | Panel title. | -| `transformations` | [DataTransformerConfig](#datatransformerconfig)[] | No | | List of transformations that are applied to the panel data before rendering.
When there are multiple transformations, Grafana applies them in the order they are listed.
Each transformation creates a result set that then passes on to the next transformation in the processing pipeline. | -| `transparent` | boolean | No | `false` | Whether to display the panel without a background. | - -### FieldConfigSource - -The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results. -Each column within this structure is called a field. A field can represent a single time series or table column. -Field options allow you to change how the data is displayed in your visualizations. - -| Property | Type | Required | Default | Description | -|-------------|-----------------------------|----------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `defaults` | [FieldConfig](#fieldconfig) | **Yes** | | The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results.
Each column within this structure is called a field. A field can represent a single time series or table column.
Field options allow you to change how the data is displayed in your visualizations. | -| `overrides` | [overrides](#overrides)[] | **Yes** | | Overrides are the options applied to specific fields overriding the defaults. | - -### FieldConfig - -The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results. -Each column within this structure is called a field. A field can represent a single time series or table column. -Field options allow you to change how the data is displayed in your visualizations. - -| Property | Type | Required | Default | Description | -|---------------------|---------------------------------------|----------|---------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `color` | [FieldColor](#fieldcolor) | No | | Map a field to a color. | -| `custom` | [custom](#custom) | No | | custom is specified by the FieldConfig field
in panel plugin schemas. | -| `decimals` | number | No | | Specify the number of decimals Grafana includes in the rendered value.
If you leave this field blank, Grafana automatically truncates the number of decimals based on the value.
For example 1.1234 will display as 1.12 and 100.456 will display as 100.
To display all decimals, set the unit to `String`. | -| `description` | string | No | | Human readable field metadata | -| `displayNameFromDS` | string | No | | This can be used by data sources that return and explicit naming structure for values and labels
When this property is configured, this value is used rather than the default naming strategy. | -| `displayName` | string | No | | The display value for this field. This supports template variables blank is auto | -| `filterable` | boolean | No | | True if data source field supports ad-hoc filters | -| `links` | | No | | The behavior when clicking on a result | -| `mappings` | [ValueMapping](#valuemapping)[] | No | | Convert input values into a display string | -| `max` | number | No | | The maximum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields. | -| `min` | number | No | | The minimum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields. | -| `noValue` | string | No | | Alternative to empty string | -| `path` | string | No | | An explicit path to the field in the datasource. When the frame meta includes a path,
This will default to `${frame.meta.path}/${field.name}

When defined, this value can be used as an identifier within the datasource scope, and
may be used to update the results | -| `thresholds` | [ThresholdsConfig](#thresholdsconfig) | No | | Thresholds configuration for the panel | -| `unit` | string | No | | Unit a field should use. The unit you select is applied to all fields except time.
You can use the units ID availables in Grafana or a custom unit.
Available units in Grafana: https://github.com/grafana/grafana/blob/main/packages/grafana-data/src/valueFormats/categories.ts
As custom unit, you can use the following formats:
`suffix:` for custom unit that should go after value.
`prefix:` for custom unit that should go before value.
`time:` For custom date time formats type for example `time:YYYY-MM-DD`.
`si:` for custom SI units. For example: `si: mF`. This one is a bit more advanced as you can specify both a unit and the source data scale. So if your source data is represented as milli (thousands of) something prefix the unit with that SI scale character.
`count:` for a custom count unit.
`currency:` for custom a currency unit. | -| `writeable` | boolean | No | | True if data source can write a value to the path. Auth/authz are supported separately | - -### RangeMap - -Maps numerical ranges to a display text and color. -For example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number. - -| Property | Type | Required | Default | Description | -|-----------|---------------------|----------|---------|-----------------------------------------------------------------------------------| -| `options` | [options](#options) | **Yes** | | Range to match against and the result to apply when the value is within the range | -| `type` | string | **Yes** | | | - -### RegexMap - -Maps regular expressions to replacement text and a color. -For example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain. - -| Property | Type | Required | Default | Description | -|-----------|---------------------|----------|---------|----------------------------------------------------------------------------------------------| -| `options` | [options](#options) | **Yes** | | Regular expression to match against and the result to apply when the value matches the regex | -| `type` | string | **Yes** | | | - -### SpecialValueMap - -Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color. -See SpecialValueMatch to see the list of special values. -For example, you can configure a special value mapping so that null values appear as N/A. - -| Property | Type | Required | Default | Description | -|-----------|---------------------|----------|---------|-------------| -| `options` | [options](#options) | **Yes** | | | -| `type` | string | **Yes** | | | - -### Templating - -Configured template variables - -| Property | Type | Required | Default | Description | -|----------|-----------------------------------|----------|---------|----------------------------------------------------------------------------------------------| -| `list` | [VariableModel](#variablemodel)[] | No | | List of configured template variables with their saved values along with some other metadata | - -### VariableModel - -A variable is a placeholder for a value. You can use variables in metric queries and in panel titles. - -| Property | Type | Required | Default | Description | -|---------------|-------------------------------------|----------|---------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `name` | string | **Yes** | | Name of variable | -| `type` | string | **Yes** | | Dashboard variable type
`query`: Query-generated list of values such as metric names, server names, sensor IDs, data centers, and so on.
`adhoc`: Key/value filters that are automatically added to all metric queries for a data source (Prometheus, Loki, InfluxDB, and Elasticsearch only).
`constant`: Define a hidden constant.
`datasource`: Quickly change the data source for an entire dashboard.
`interval`: Interval variables represent time spans.
`textbox`: Display a free text input field with an optional default value.
`custom`: Define the variable options manually using a comma-separated list.
`system`: Variables defined by Grafana. See: https://grafana.com/docs/grafana/latest/dashboards/variables/add-template-variables/#global-variables
Possible values are: `query`, `adhoc`, `groupby`, `constant`, `datasource`, `interval`, `textbox`, `custom`, `system`. | -| `allValue` | string | No | | Custom all value | -| `current` | [VariableOption](#variableoption) | No | | Option to be selected in a variable. | -| `datasource` | [DataSourceRef](#datasourceref) | No | | Ref to a DataSource instance | -| `description` | string | No | | Description of variable. It can be defined but `null`. | -| `hide` | integer | No | | Determine if the variable shows on dashboard
Accepted values are 0 (show label and value), 1 (show value only), 2 (show nothing).
Possible values are: `0`, `1`, `2`. | -| `includeAll` | boolean | No | `false` | Whether all value option is available or not | -| `label` | string | No | | Optional display name | -| `multi` | boolean | No | `false` | Whether multiple values can be selected or not from variable value list | -| `options` | [VariableOption](#variableoption)[] | No | | Options that can be selected for a variable. | -| `query` | | No | | Query used to fetch values for a variable | -| `refresh` | integer | No | | Options to config when to refresh a variable
`0`: Never refresh the variable
`1`: Queries the data source every time the dashboard loads.
`2`: Queries the data source when the dashboard time range changes.
Possible values are: `0`, `1`, `2`. | -| `regex` | string | No | | Optional field, if you want to extract part of a series name or metric node segment.
Named capture groups can be used to separate the display text and value. | -| `skipUrlSync` | boolean | No | `false` | Whether the variable value should be managed by URL query params or not | -| `sort` | integer | No | | Sort variable options
Accepted values are:
`0`: No sorting
`1`: Alphabetical ASC
`2`: Alphabetical DESC
`3`: Numerical ASC
`4`: Numerical DESC
`5`: Alphabetical Case Insensitive ASC
`6`: Alphabetical Case Insensitive DESC
`7`: Natural ASC
`8`: Natural DESC
Possible values are: `0`, `1`, `2`, `3`, `4`, `5`, `6`, `7`, `8`. | - -### VariableOption - -Option to be selected in a variable. - -| Property | Type | Required | Default | Description | -|------------|---------|----------|---------|---------------------------------------| -| `text` | | **Yes** | | Text to be displayed for the option | -| `value` | | **Yes** | | Value of the option | -| `selected` | boolean | No | | Whether the option is selected or not | - -### Time - -Time range for dashboard. -Accepted values are relative time strings like {from: 'now-6h', to: 'now'} or absolute time strings like {from: '2020-07-10T08:00:00.000Z', to: '2020-07-10T14:00:00.000Z'}. - -| Property | Type | Required | Default | Description | -|----------|--------|----------|----------|-------------| -| `from` | string | **Yes** | `now-6h` | | -| `to` | string | **Yes** | `now` | | - -### Status - -| Property | Type | Required | Default | Description | -|--------------------|------------------------------------------------------------|----------|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `additionalFields` | [object](#additionalfields) | No | | additionalFields is reserved for future use | -| `operatorStates` | map[string][status.#OperatorState](#status.#operatorstate) | No | | operatorStates is a map of operator ID to operator state evaluations.
Any operator which consumes this kind SHOULD add its state evaluation information to this field. | - -### AdditionalFields - -additionalFields is reserved for future use - -| Property | Type | Required | Default | Description | -|----------|------|----------|---------|-------------| - -### Status.#OperatorState - -| Property | Type | Required | Default | Description | -|--------------------|--------------------|----------|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `lastEvaluation` | string | **Yes** | | lastEvaluation is the ResourceVersion last evaluated | -| `state` | string | **Yes** | | state describes the state of the lastEvaluation.
It is limited to three possible states for machine evaluation.
Possible values are: `success`, `in_progress`, `failed`. | -| `descriptiveState` | string | No | | descriptiveState is an optional more descriptive state field which has no requirements on format | -| `details` | [object](#details) | No | | details contains any extra information that is operator-specific | - -### Details - -details contains any extra information that is operator-specific - -| Property | Type | Required | Default | Description | -|----------|------|----------|---------|-------------| - - diff --git a/docs/sources/developers/kinds/core/folder/schema-reference.md b/docs/sources/developers/kinds/core/folder/schema-reference.md deleted file mode 100644 index cbc8f41c41..0000000000 --- a/docs/sources/developers/kinds/core/folder/schema-reference.md +++ /dev/null @@ -1,111 +0,0 @@ ---- -keywords: - - grafana - - schema -labels: - products: - - cloud - - enterprise - - oss -title: Folder kind ---- -> Both documentation generation and kinds schemas are in active development and subject to change without prior notice. - -## Folder - -#### Maturity: [merged](../../../maturity/#merged) -#### Version: 0.0 - -A folder is a collection of resources that are grouped together and can share permissions. - -| Property | Type | Required | Default | Description | -|------------|---------------------|----------|---------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `metadata` | [object](#metadata) | **Yes** | | metadata contains embedded CommonMetadata and can be extended with custom string fields
TODO: use CommonMetadata instead of redefining here; currently needs to be defined here
without external reference as using the CommonMetadata reference breaks thema codegen. | -| `spec` | [object](#spec) | **Yes** | | TODO:
common metadata will soon support setting the parent folder in the metadata | -| `status` | [object](#status) | **Yes** | | | - -### Metadata - -metadata contains embedded CommonMetadata and can be extended with custom string fields -TODO: use CommonMetadata instead of redefining here; currently needs to be defined here -without external reference as using the CommonMetadata reference breaks thema codegen. - -It extends [_kubeObjectMetadata](#_kubeobjectmetadata). - -| Property | Type | Required | Default | Description | -|---------------------|------------------------|----------|---------|-----------------------------------------------------------------------------------------------------------------------------------------| -| `createdBy` | string | **Yes** | | | -| `creationTimestamp` | string | **Yes** | | *(Inherited from [_kubeObjectMetadata](#_kubeobjectmetadata))* | -| `extraFields` | [object](#extrafields) | **Yes** | | extraFields is reserved for any fields that are pulled from the API server metadata but do not have concrete fields in the CUE metadata | -| `finalizers` | string[] | **Yes** | | *(Inherited from [_kubeObjectMetadata](#_kubeobjectmetadata))* | -| `labels` | map[string]string | **Yes** | | *(Inherited from [_kubeObjectMetadata](#_kubeobjectmetadata))* | -| `resourceVersion` | string | **Yes** | | *(Inherited from [_kubeObjectMetadata](#_kubeobjectmetadata))* | -| `uid` | string | **Yes** | | *(Inherited from [_kubeObjectMetadata](#_kubeobjectmetadata))* | -| `updateTimestamp` | string | **Yes** | | | -| `updatedBy` | string | **Yes** | | | -| `deletionTimestamp` | string | No | | *(Inherited from [_kubeObjectMetadata](#_kubeobjectmetadata))* | - -### _kubeObjectMetadata - -_kubeObjectMetadata is metadata found in a kubernetes object's metadata field. -It is not exhaustive and only includes fields which may be relevant to a kind's implementation, -As it is also intended to be generic enough to function with any API Server. - -| Property | Type | Required | Default | Description | -|---------------------|-------------------|----------|---------|-------------| -| `creationTimestamp` | string | **Yes** | | | -| `finalizers` | string[] | **Yes** | | | -| `labels` | map[string]string | **Yes** | | | -| `resourceVersion` | string | **Yes** | | | -| `uid` | string | **Yes** | | | -| `deletionTimestamp` | string | No | | | - -### ExtraFields - -extraFields is reserved for any fields that are pulled from the API server metadata but do not have concrete fields in the CUE metadata - -| Property | Type | Required | Default | Description | -|----------|------|----------|---------|-------------| - -### Spec - -TODO: -common metadata will soon support setting the parent folder in the metadata - -| Property | Type | Required | Default | Description | -|---------------|--------|----------|---------|--------------------------------------| -| `title` | string | **Yes** | | Folder title | -| `uid` | string | **Yes** | | Unique folder id. (will be k8s name) | -| `description` | string | No | | Description of the folder. | - -### Status - -| Property | Type | Required | Default | Description | -|--------------------|------------------------------------------------------------|----------|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `additionalFields` | [object](#additionalfields) | No | | additionalFields is reserved for future use | -| `operatorStates` | map[string][status.#OperatorState](#status.#operatorstate) | No | | operatorStates is a map of operator ID to operator state evaluations.
Any operator which consumes this kind SHOULD add its state evaluation information to this field. | - -### AdditionalFields - -additionalFields is reserved for future use - -| Property | Type | Required | Default | Description | -|----------|------|----------|---------|-------------| - -### Status.#OperatorState - -| Property | Type | Required | Default | Description | -|--------------------|--------------------|----------|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `lastEvaluation` | string | **Yes** | | lastEvaluation is the ResourceVersion last evaluated | -| `state` | string | **Yes** | | state describes the state of the lastEvaluation.
It is limited to three possible states for machine evaluation.
Possible values are: `success`, `in_progress`, `failed`. | -| `descriptiveState` | string | No | | descriptiveState is an optional more descriptive state field which has no requirements on format | -| `details` | [object](#details) | No | | details contains any extra information that is operator-specific | - -### Details - -details contains any extra information that is operator-specific - -| Property | Type | Required | Default | Description | -|----------|------|----------|---------|-------------| - - diff --git a/docs/sources/developers/kinds/core/librarypanel/schema-reference.md b/docs/sources/developers/kinds/core/librarypanel/schema-reference.md deleted file mode 100644 index eaab5f3544..0000000000 --- a/docs/sources/developers/kinds/core/librarypanel/schema-reference.md +++ /dev/null @@ -1,142 +0,0 @@ ---- -keywords: - - grafana - - schema -labels: - products: - - cloud - - enterprise - - oss -title: LibraryPanel kind ---- -> Both documentation generation and kinds schemas are in active development and subject to change without prior notice. - -## LibraryPanel - -#### Maturity: [experimental](../../../maturity/#experimental) -#### Version: 0.0 - -A standalone panel - -| Property | Type | Required | Default | Description | -|------------|---------------------|----------|---------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `metadata` | [object](#metadata) | **Yes** | | metadata contains embedded CommonMetadata and can be extended with custom string fields
TODO: use CommonMetadata instead of redefining here; currently needs to be defined here
without external reference as using the CommonMetadata reference breaks thema codegen. | -| `spec` | [object](#spec) | **Yes** | | | -| `status` | [object](#status) | **Yes** | | | - -### Metadata - -metadata contains embedded CommonMetadata and can be extended with custom string fields -TODO: use CommonMetadata instead of redefining here; currently needs to be defined here -without external reference as using the CommonMetadata reference breaks thema codegen. - -It extends [_kubeObjectMetadata](#_kubeobjectmetadata). - -| Property | Type | Required | Default | Description | -|---------------------|------------------------|----------|---------|-----------------------------------------------------------------------------------------------------------------------------------------| -| `createdBy` | string | **Yes** | | | -| `creationTimestamp` | string | **Yes** | | *(Inherited from [_kubeObjectMetadata](#_kubeobjectmetadata))* | -| `extraFields` | [object](#extrafields) | **Yes** | | extraFields is reserved for any fields that are pulled from the API server metadata but do not have concrete fields in the CUE metadata | -| `finalizers` | string[] | **Yes** | | *(Inherited from [_kubeObjectMetadata](#_kubeobjectmetadata))* | -| `labels` | map[string]string | **Yes** | | *(Inherited from [_kubeObjectMetadata](#_kubeobjectmetadata))* | -| `resourceVersion` | string | **Yes** | | *(Inherited from [_kubeObjectMetadata](#_kubeobjectmetadata))* | -| `uid` | string | **Yes** | | *(Inherited from [_kubeObjectMetadata](#_kubeobjectmetadata))* | -| `updateTimestamp` | string | **Yes** | | | -| `updatedBy` | string | **Yes** | | | -| `deletionTimestamp` | string | No | | *(Inherited from [_kubeObjectMetadata](#_kubeobjectmetadata))* | - -### _kubeObjectMetadata - -_kubeObjectMetadata is metadata found in a kubernetes object's metadata field. -It is not exhaustive and only includes fields which may be relevant to a kind's implementation, -As it is also intended to be generic enough to function with any API Server. - -| Property | Type | Required | Default | Description | -|---------------------|-------------------|----------|---------|-------------| -| `creationTimestamp` | string | **Yes** | | | -| `finalizers` | string[] | **Yes** | | | -| `labels` | map[string]string | **Yes** | | | -| `resourceVersion` | string | **Yes** | | | -| `uid` | string | **Yes** | | | -| `deletionTimestamp` | string | No | | | - -### ExtraFields - -extraFields is reserved for any fields that are pulled from the API server metadata but do not have concrete fields in the CUE metadata - -| Property | Type | Required | Default | Description | -|----------|------|----------|---------|-------------| - -### Spec - -| Property | Type | Required | Default | Description | -|-----------------|-------------------------------------------------|----------|---------|--------------------------------------------------------------------------------------------------------------------------------------| -| `model` | [object](#model) | **Yes** | | TODO: should be the same panel schema defined in dashboard
Typescript: Omit; | -| `name` | string | **Yes** | | Panel name (also saved in the model)
Constraint: `length >=1`. | -| `type` | string | **Yes** | | The panel type (from inside the model)
Constraint: `length >=1`. | -| `uid` | string | **Yes** | | Library element UID | -| `version` | integer | **Yes** | | panel version, incremented each time the dashboard is updated. | -| `description` | string | No | | Panel description | -| `folderUid` | string | No | | Folder UID | -| `meta` | [LibraryElementDTOMeta](#libraryelementdtometa) | No | | | -| `schemaVersion` | uint16 | No | | Dashboard version when this was saved (zero if unknown) | - -### LibraryElementDTOMeta - -| Property | Type | Required | Default | Description | -|-----------------------|---------------------------------------------------------|----------|---------|-------------| -| `connectedDashboards` | integer | **Yes** | | | -| `createdBy` | [LibraryElementDTOMetaUser](#libraryelementdtometauser) | **Yes** | | | -| `created` | string | **Yes** | | | -| `folderName` | string | **Yes** | | | -| `folderUid` | string | **Yes** | | | -| `updatedBy` | [LibraryElementDTOMetaUser](#libraryelementdtometauser) | **Yes** | | | -| `updated` | string | **Yes** | | | - -### LibraryElementDTOMetaUser - -| Property | Type | Required | Default | Description | -|-------------|---------|----------|---------|-------------| -| `avatarUrl` | string | **Yes** | | | -| `id` | integer | **Yes** | | | -| `name` | string | **Yes** | | | - -### Model - -TODO: should be the same panel schema defined in dashboard -Typescript: Omit; - -| Property | Type | Required | Default | Description | -|----------|------|----------|---------|-------------| - -### Status - -| Property | Type | Required | Default | Description | -|--------------------|------------------------------------------------------------|----------|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `additionalFields` | [object](#additionalfields) | No | | additionalFields is reserved for future use | -| `operatorStates` | map[string][status.#OperatorState](#status.#operatorstate) | No | | operatorStates is a map of operator ID to operator state evaluations.
Any operator which consumes this kind SHOULD add its state evaluation information to this field. | - -### AdditionalFields - -additionalFields is reserved for future use - -| Property | Type | Required | Default | Description | -|----------|------|----------|---------|-------------| - -### Status.#OperatorState - -| Property | Type | Required | Default | Description | -|--------------------|--------------------|----------|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `lastEvaluation` | string | **Yes** | | lastEvaluation is the ResourceVersion last evaluated | -| `state` | string | **Yes** | | state describes the state of the lastEvaluation.
It is limited to three possible states for machine evaluation.
Possible values are: `success`, `in_progress`, `failed`. | -| `descriptiveState` | string | No | | descriptiveState is an optional more descriptive state field which has no requirements on format | -| `details` | [object](#details) | No | | details contains any extra information that is operator-specific | - -### Details - -details contains any extra information that is operator-specific - -| Property | Type | Required | Default | Description | -|----------|------|----------|---------|-------------| - - diff --git a/docs/sources/developers/kinds/core/preferences/schema-reference.md b/docs/sources/developers/kinds/core/preferences/schema-reference.md deleted file mode 100644 index e5ac35187c..0000000000 --- a/docs/sources/developers/kinds/core/preferences/schema-reference.md +++ /dev/null @@ -1,144 +0,0 @@ ---- -keywords: - - grafana - - schema -labels: - products: - - cloud - - enterprise - - oss -title: Preferences kind ---- -> Both documentation generation and kinds schemas are in active development and subject to change without prior notice. - -## Preferences - -#### Maturity: [merged](../../../maturity/#merged) -#### Version: 0.0 - -The user or team frontend preferences - -| Property | Type | Required | Default | Description | -|------------|---------------------|----------|---------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `metadata` | [object](#metadata) | **Yes** | | metadata contains embedded CommonMetadata and can be extended with custom string fields
TODO: use CommonMetadata instead of redefining here; currently needs to be defined here
without external reference as using the CommonMetadata reference breaks thema codegen. | -| `spec` | [object](#spec) | **Yes** | | Spec defines user, team or org Grafana preferences
swagger:model Preferences | -| `status` | [object](#status) | **Yes** | | | - -### Metadata - -metadata contains embedded CommonMetadata and can be extended with custom string fields -TODO: use CommonMetadata instead of redefining here; currently needs to be defined here -without external reference as using the CommonMetadata reference breaks thema codegen. - -It extends [_kubeObjectMetadata](#_kubeobjectmetadata). - -| Property | Type | Required | Default | Description | -|---------------------|------------------------|----------|---------|-----------------------------------------------------------------------------------------------------------------------------------------| -| `createdBy` | string | **Yes** | | | -| `creationTimestamp` | string | **Yes** | | *(Inherited from [_kubeObjectMetadata](#_kubeobjectmetadata))* | -| `extraFields` | [object](#extrafields) | **Yes** | | extraFields is reserved for any fields that are pulled from the API server metadata but do not have concrete fields in the CUE metadata | -| `finalizers` | string[] | **Yes** | | *(Inherited from [_kubeObjectMetadata](#_kubeobjectmetadata))* | -| `labels` | map[string]string | **Yes** | | *(Inherited from [_kubeObjectMetadata](#_kubeobjectmetadata))* | -| `resourceVersion` | string | **Yes** | | *(Inherited from [_kubeObjectMetadata](#_kubeobjectmetadata))* | -| `uid` | string | **Yes** | | *(Inherited from [_kubeObjectMetadata](#_kubeobjectmetadata))* | -| `updateTimestamp` | string | **Yes** | | | -| `updatedBy` | string | **Yes** | | | -| `deletionTimestamp` | string | No | | *(Inherited from [_kubeObjectMetadata](#_kubeobjectmetadata))* | - -### _kubeObjectMetadata - -_kubeObjectMetadata is metadata found in a kubernetes object's metadata field. -It is not exhaustive and only includes fields which may be relevant to a kind's implementation, -As it is also intended to be generic enough to function with any API Server. - -| Property | Type | Required | Default | Description | -|---------------------|-------------------|----------|---------|-------------| -| `creationTimestamp` | string | **Yes** | | | -| `finalizers` | string[] | **Yes** | | | -| `labels` | map[string]string | **Yes** | | | -| `resourceVersion` | string | **Yes** | | | -| `uid` | string | **Yes** | | | -| `deletionTimestamp` | string | No | | | - -### ExtraFields - -extraFields is reserved for any fields that are pulled from the API server metadata but do not have concrete fields in the CUE metadata - -| Property | Type | Required | Default | Description | -|----------|------|----------|---------|-------------| - -### Spec - -Spec defines user, team or org Grafana preferences -swagger:model Preferences - -| Property | Type | Required | Default | Description | -|---------------------|---------------------------------------------------|----------|---------|---------------------------------------------------------------------------------| -| `cookiePreferences` | [CookiePreferences](#cookiepreferences) | No | | | -| `homeDashboardUID` | string | No | | UID for the home dashboard | -| `language` | string | No | | Selected language (beta) | -| `queryHistory` | [QueryHistoryPreference](#queryhistorypreference) | No | | | -| `theme` | string | No | | light, dark, empty is default | -| `timezone` | string | No | | The timezone selection
TODO: this should use the timezone defined in common | -| `weekStart` | string | No | | day of the week (sunday, monday, etc) | - -### CookiePreferences - -| Property | Type | Required | Default | Description | -|---------------|------------------------|----------|---------|-------------| -| `analytics` | [object](#analytics) | No | | | -| `functional` | [object](#functional) | No | | | -| `performance` | [object](#performance) | No | | | - -### Analytics - -| Property | Type | Required | Default | Description | -|----------|------|----------|---------|-------------| - -### Functional - -| Property | Type | Required | Default | Description | -|----------|------|----------|---------|-------------| - -### Performance - -| Property | Type | Required | Default | Description | -|----------|------|----------|---------|-------------| - -### QueryHistoryPreference - -| Property | Type | Required | Default | Description | -|-----------|--------|----------|---------|---------------------------------------------| -| `homeTab` | string | No | | one of: '' | 'query' | 'starred'; | - -### Status - -| Property | Type | Required | Default | Description | -|--------------------|------------------------------------------------------------|----------|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `additionalFields` | [object](#additionalfields) | No | | additionalFields is reserved for future use | -| `operatorStates` | map[string][status.#OperatorState](#status.#operatorstate) | No | | operatorStates is a map of operator ID to operator state evaluations.
Any operator which consumes this kind SHOULD add its state evaluation information to this field. | - -### AdditionalFields - -additionalFields is reserved for future use - -| Property | Type | Required | Default | Description | -|----------|------|----------|---------|-------------| - -### Status.#OperatorState - -| Property | Type | Required | Default | Description | -|--------------------|--------------------|----------|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `lastEvaluation` | string | **Yes** | | lastEvaluation is the ResourceVersion last evaluated | -| `state` | string | **Yes** | | state describes the state of the lastEvaluation.
It is limited to three possible states for machine evaluation.
Possible values are: `success`, `in_progress`, `failed`. | -| `descriptiveState` | string | No | | descriptiveState is an optional more descriptive state field which has no requirements on format | -| `details` | [object](#details) | No | | details contains any extra information that is operator-specific | - -### Details - -details contains any extra information that is operator-specific - -| Property | Type | Required | Default | Description | -|----------|------|----------|---------|-------------| - - diff --git a/docs/sources/developers/kinds/core/publicdashboard/schema-reference.md b/docs/sources/developers/kinds/core/publicdashboard/schema-reference.md deleted file mode 100644 index 3f6f3ab223..0000000000 --- a/docs/sources/developers/kinds/core/publicdashboard/schema-reference.md +++ /dev/null @@ -1,111 +0,0 @@ ---- -keywords: - - grafana - - schema -labels: - products: - - cloud - - enterprise - - oss -title: PublicDashboard kind ---- -> Both documentation generation and kinds schemas are in active development and subject to change without prior notice. - -## PublicDashboard - -#### Maturity: [merged](../../../maturity/#merged) -#### Version: 0.0 - -Public dashboard configuration - -| Property | Type | Required | Default | Description | -|------------|---------------------|----------|---------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `metadata` | [object](#metadata) | **Yes** | | metadata contains embedded CommonMetadata and can be extended with custom string fields
TODO: use CommonMetadata instead of redefining here; currently needs to be defined here
without external reference as using the CommonMetadata reference breaks thema codegen. | -| `spec` | [object](#spec) | **Yes** | | | -| `status` | [object](#status) | **Yes** | | | - -### Metadata - -metadata contains embedded CommonMetadata and can be extended with custom string fields -TODO: use CommonMetadata instead of redefining here; currently needs to be defined here -without external reference as using the CommonMetadata reference breaks thema codegen. - -It extends [_kubeObjectMetadata](#_kubeobjectmetadata). - -| Property | Type | Required | Default | Description | -|---------------------|------------------------|----------|---------|-----------------------------------------------------------------------------------------------------------------------------------------| -| `createdBy` | string | **Yes** | | | -| `creationTimestamp` | string | **Yes** | | *(Inherited from [_kubeObjectMetadata](#_kubeobjectmetadata))* | -| `extraFields` | [object](#extrafields) | **Yes** | | extraFields is reserved for any fields that are pulled from the API server metadata but do not have concrete fields in the CUE metadata | -| `finalizers` | string[] | **Yes** | | *(Inherited from [_kubeObjectMetadata](#_kubeobjectmetadata))* | -| `labels` | map[string]string | **Yes** | | *(Inherited from [_kubeObjectMetadata](#_kubeobjectmetadata))* | -| `resourceVersion` | string | **Yes** | | *(Inherited from [_kubeObjectMetadata](#_kubeobjectmetadata))* | -| `uid` | string | **Yes** | | *(Inherited from [_kubeObjectMetadata](#_kubeobjectmetadata))* | -| `updateTimestamp` | string | **Yes** | | | -| `updatedBy` | string | **Yes** | | | -| `deletionTimestamp` | string | No | | *(Inherited from [_kubeObjectMetadata](#_kubeobjectmetadata))* | - -### _kubeObjectMetadata - -_kubeObjectMetadata is metadata found in a kubernetes object's metadata field. -It is not exhaustive and only includes fields which may be relevant to a kind's implementation, -As it is also intended to be generic enough to function with any API Server. - -| Property | Type | Required | Default | Description | -|---------------------|-------------------|----------|---------|-------------| -| `creationTimestamp` | string | **Yes** | | | -| `finalizers` | string[] | **Yes** | | | -| `labels` | map[string]string | **Yes** | | | -| `resourceVersion` | string | **Yes** | | | -| `uid` | string | **Yes** | | | -| `deletionTimestamp` | string | No | | | - -### ExtraFields - -extraFields is reserved for any fields that are pulled from the API server metadata but do not have concrete fields in the CUE metadata - -| Property | Type | Required | Default | Description | -|----------|------|----------|---------|-------------| - -### Spec - -| Property | Type | Required | Default | Description | -|------------------------|---------|----------|---------|-----------------------------------------------------------------| -| `annotationsEnabled` | boolean | **Yes** | | Flag that indicates if annotations are enabled | -| `dashboardUid` | string | **Yes** | | Dashboard unique identifier referenced by this public dashboard | -| `isEnabled` | boolean | **Yes** | | Flag that indicates if the public dashboard is enabled | -| `timeSelectionEnabled` | boolean | **Yes** | | Flag that indicates if the time range picker is enabled | -| `uid` | string | **Yes** | | Unique public dashboard identifier | -| `accessToken` | string | No | | Unique public access token | - -### Status - -| Property | Type | Required | Default | Description | -|--------------------|------------------------------------------------------------|----------|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `additionalFields` | [object](#additionalfields) | No | | additionalFields is reserved for future use | -| `operatorStates` | map[string][status.#OperatorState](#status.#operatorstate) | No | | operatorStates is a map of operator ID to operator state evaluations.
Any operator which consumes this kind SHOULD add its state evaluation information to this field. | - -### AdditionalFields - -additionalFields is reserved for future use - -| Property | Type | Required | Default | Description | -|----------|------|----------|---------|-------------| - -### Status.#OperatorState - -| Property | Type | Required | Default | Description | -|--------------------|--------------------|----------|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `lastEvaluation` | string | **Yes** | | lastEvaluation is the ResourceVersion last evaluated | -| `state` | string | **Yes** | | state describes the state of the lastEvaluation.
It is limited to three possible states for machine evaluation.
Possible values are: `success`, `in_progress`, `failed`. | -| `descriptiveState` | string | No | | descriptiveState is an optional more descriptive state field which has no requirements on format | -| `details` | [object](#details) | No | | details contains any extra information that is operator-specific | - -### Details - -details contains any extra information that is operator-specific - -| Property | Type | Required | Default | Description | -|----------|------|----------|---------|-------------| - - diff --git a/docs/sources/developers/kinds/core/role/schema-reference.md b/docs/sources/developers/kinds/core/role/schema-reference.md deleted file mode 100644 index b5d96f7947..0000000000 --- a/docs/sources/developers/kinds/core/role/schema-reference.md +++ /dev/null @@ -1,110 +0,0 @@ ---- -keywords: - - grafana - - schema -labels: - products: - - cloud - - enterprise - - oss -title: Role kind ---- -> Both documentation generation and kinds schemas are in active development and subject to change without prior notice. - -## Role - -#### Maturity: [merged](../../../maturity/#merged) -#### Version: 0.0 - -Roles represent a set of users+teams that should share similar access - -| Property | Type | Required | Default | Description | -|------------|---------------------|----------|---------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `metadata` | [object](#metadata) | **Yes** | | metadata contains embedded CommonMetadata and can be extended with custom string fields
TODO: use CommonMetadata instead of redefining here; currently needs to be defined here
without external reference as using the CommonMetadata reference breaks thema codegen. | -| `spec` | [object](#spec) | **Yes** | | | -| `status` | [object](#status) | **Yes** | | | - -### Metadata - -metadata contains embedded CommonMetadata and can be extended with custom string fields -TODO: use CommonMetadata instead of redefining here; currently needs to be defined here -without external reference as using the CommonMetadata reference breaks thema codegen. - -It extends [_kubeObjectMetadata](#_kubeobjectmetadata). - -| Property | Type | Required | Default | Description | -|---------------------|------------------------|----------|---------|-----------------------------------------------------------------------------------------------------------------------------------------| -| `createdBy` | string | **Yes** | | | -| `creationTimestamp` | string | **Yes** | | *(Inherited from [_kubeObjectMetadata](#_kubeobjectmetadata))* | -| `extraFields` | [object](#extrafields) | **Yes** | | extraFields is reserved for any fields that are pulled from the API server metadata but do not have concrete fields in the CUE metadata | -| `finalizers` | string[] | **Yes** | | *(Inherited from [_kubeObjectMetadata](#_kubeobjectmetadata))* | -| `labels` | map[string]string | **Yes** | | *(Inherited from [_kubeObjectMetadata](#_kubeobjectmetadata))* | -| `resourceVersion` | string | **Yes** | | *(Inherited from [_kubeObjectMetadata](#_kubeobjectmetadata))* | -| `uid` | string | **Yes** | | *(Inherited from [_kubeObjectMetadata](#_kubeobjectmetadata))* | -| `updateTimestamp` | string | **Yes** | | | -| `updatedBy` | string | **Yes** | | | -| `deletionTimestamp` | string | No | | *(Inherited from [_kubeObjectMetadata](#_kubeobjectmetadata))* | - -### _kubeObjectMetadata - -_kubeObjectMetadata is metadata found in a kubernetes object's metadata field. -It is not exhaustive and only includes fields which may be relevant to a kind's implementation, -As it is also intended to be generic enough to function with any API Server. - -| Property | Type | Required | Default | Description | -|---------------------|-------------------|----------|---------|-------------| -| `creationTimestamp` | string | **Yes** | | | -| `finalizers` | string[] | **Yes** | | | -| `labels` | map[string]string | **Yes** | | | -| `resourceVersion` | string | **Yes** | | | -| `uid` | string | **Yes** | | | -| `deletionTimestamp` | string | No | | | - -### ExtraFields - -extraFields is reserved for any fields that are pulled from the API server metadata but do not have concrete fields in the CUE metadata - -| Property | Type | Required | Default | Description | -|----------|------|----------|---------|-------------| - -### Spec - -| Property | Type | Required | Default | Description | -|---------------|---------|----------|---------|-----------------------------------------------------------| -| `hidden` | boolean | **Yes** | | Do not show this role | -| `name` | string | **Yes** | | The role identifier `managed:builtins:editor:permissions` | -| `description` | string | No | | Role description | -| `displayName` | string | No | | Optional display | -| `groupName` | string | No | | Name of the team. | - -### Status - -| Property | Type | Required | Default | Description | -|--------------------|------------------------------------------------------------|----------|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `additionalFields` | [object](#additionalfields) | No | | additionalFields is reserved for future use | -| `operatorStates` | map[string][status.#OperatorState](#status.#operatorstate) | No | | operatorStates is a map of operator ID to operator state evaluations.
Any operator which consumes this kind SHOULD add its state evaluation information to this field. | - -### AdditionalFields - -additionalFields is reserved for future use - -| Property | Type | Required | Default | Description | -|----------|------|----------|---------|-------------| - -### Status.#OperatorState - -| Property | Type | Required | Default | Description | -|--------------------|--------------------|----------|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `lastEvaluation` | string | **Yes** | | lastEvaluation is the ResourceVersion last evaluated | -| `state` | string | **Yes** | | state describes the state of the lastEvaluation.
It is limited to three possible states for machine evaluation.
Possible values are: `success`, `in_progress`, `failed`. | -| `descriptiveState` | string | No | | descriptiveState is an optional more descriptive state field which has no requirements on format | -| `details` | [object](#details) | No | | details contains any extra information that is operator-specific | - -### Details - -details contains any extra information that is operator-specific - -| Property | Type | Required | Default | Description | -|----------|------|----------|---------|-------------| - - diff --git a/docs/sources/developers/kinds/core/rolebinding/schema-reference.md b/docs/sources/developers/kinds/core/rolebinding/schema-reference.md deleted file mode 100644 index 370a2a82cc..0000000000 --- a/docs/sources/developers/kinds/core/rolebinding/schema-reference.md +++ /dev/null @@ -1,136 +0,0 @@ ---- -keywords: - - grafana - - schema -labels: - products: - - cloud - - enterprise - - oss -title: RoleBinding kind ---- -> Both documentation generation and kinds schemas are in active development and subject to change without prior notice. - -## RoleBinding - -#### Maturity: [merged](../../../maturity/#merged) -#### Version: 0.0 - -Role bindings links a user|team to a configured role - -| Property | Type | Required | Default | Description | -|------------|---------------------|----------|---------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `metadata` | [object](#metadata) | **Yes** | | metadata contains embedded CommonMetadata and can be extended with custom string fields
TODO: use CommonMetadata instead of redefining here; currently needs to be defined here
without external reference as using the CommonMetadata reference breaks thema codegen. | -| `spec` | [object](#spec) | **Yes** | | | -| `status` | [object](#status) | **Yes** | | | - -### Metadata - -metadata contains embedded CommonMetadata and can be extended with custom string fields -TODO: use CommonMetadata instead of redefining here; currently needs to be defined here -without external reference as using the CommonMetadata reference breaks thema codegen. - -It extends [_kubeObjectMetadata](#_kubeobjectmetadata). - -| Property | Type | Required | Default | Description | -|---------------------|------------------------|----------|---------|-----------------------------------------------------------------------------------------------------------------------------------------| -| `createdBy` | string | **Yes** | | | -| `creationTimestamp` | string | **Yes** | | *(Inherited from [_kubeObjectMetadata](#_kubeobjectmetadata))* | -| `extraFields` | [object](#extrafields) | **Yes** | | extraFields is reserved for any fields that are pulled from the API server metadata but do not have concrete fields in the CUE metadata | -| `finalizers` | string[] | **Yes** | | *(Inherited from [_kubeObjectMetadata](#_kubeobjectmetadata))* | -| `labels` | map[string]string | **Yes** | | *(Inherited from [_kubeObjectMetadata](#_kubeobjectmetadata))* | -| `resourceVersion` | string | **Yes** | | *(Inherited from [_kubeObjectMetadata](#_kubeobjectmetadata))* | -| `uid` | string | **Yes** | | *(Inherited from [_kubeObjectMetadata](#_kubeobjectmetadata))* | -| `updateTimestamp` | string | **Yes** | | | -| `updatedBy` | string | **Yes** | | | -| `deletionTimestamp` | string | No | | *(Inherited from [_kubeObjectMetadata](#_kubeobjectmetadata))* | - -### _kubeObjectMetadata - -_kubeObjectMetadata is metadata found in a kubernetes object's metadata field. -It is not exhaustive and only includes fields which may be relevant to a kind's implementation, -As it is also intended to be generic enough to function with any API Server. - -| Property | Type | Required | Default | Description | -|---------------------|-------------------|----------|---------|-------------| -| `creationTimestamp` | string | **Yes** | | | -| `finalizers` | string[] | **Yes** | | | -| `labels` | map[string]string | **Yes** | | | -| `resourceVersion` | string | **Yes** | | | -| `uid` | string | **Yes** | | | -| `deletionTimestamp` | string | No | | | - -### ExtraFields - -extraFields is reserved for any fields that are pulled from the API server metadata but do not have concrete fields in the CUE metadata - -| Property | Type | Required | Default | Description | -|----------|------|----------|---------|-------------| - -### Spec - -| Property | Type | Required | Default | Description | -|-----------|-------------------------------------------|----------|---------|----------------------------| -| `role` | [object](#role) | **Yes** | | The role we are discussing | -| `subject` | [RoleBindingSubject](#rolebindingsubject) | **Yes** | | | - -### RoleBindingSubject - -| Property | Type | Required | Default | Description | -|----------|--------|----------|---------|--------------------------------------| -| `kind` | string | **Yes** | | Possible values are: `Team`, `User`. | -| `name` | string | **Yes** | | The team/user identifier name | - -### Role - -The role we are discussing - -| Property | Type | Required | Default | Description | -|----------|-----------------------------------------------------------------------------------------|----------|---------|-------------| -| `object` | Possible types are: [BuiltinRoleRef](#builtinroleref), [CustomRoleRef](#customroleref). | | | - -### BuiltinRoleRef - -| Property | Type | Required | Default | Description | -|----------|--------|----------|---------|---------------------------------------------------| -| `kind` | string | **Yes** | | Possible values are: `BuiltinRole`. | -| `name` | string | **Yes** | | Possible values are: `viewer`, `editor`, `admin`. | - -### CustomRoleRef - -| Property | Type | Required | Default | Description | -|----------|--------|----------|---------|------------------------------| -| `kind` | string | **Yes** | | Possible values are: `Role`. | -| `name` | string | **Yes** | | | - -### Status - -| Property | Type | Required | Default | Description | -|--------------------|------------------------------------------------------------|----------|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `additionalFields` | [object](#additionalfields) | No | | additionalFields is reserved for future use | -| `operatorStates` | map[string][status.#OperatorState](#status.#operatorstate) | No | | operatorStates is a map of operator ID to operator state evaluations.
Any operator which consumes this kind SHOULD add its state evaluation information to this field. | - -### AdditionalFields - -additionalFields is reserved for future use - -| Property | Type | Required | Default | Description | -|----------|------|----------|---------|-------------| - -### Status.#OperatorState - -| Property | Type | Required | Default | Description | -|--------------------|--------------------|----------|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `lastEvaluation` | string | **Yes** | | lastEvaluation is the ResourceVersion last evaluated | -| `state` | string | **Yes** | | state describes the state of the lastEvaluation.
It is limited to three possible states for machine evaluation.
Possible values are: `success`, `in_progress`, `failed`. | -| `descriptiveState` | string | No | | descriptiveState is an optional more descriptive state field which has no requirements on format | -| `details` | [object](#details) | No | | details contains any extra information that is operator-specific | - -### Details - -details contains any extra information that is operator-specific - -| Property | Type | Required | Default | Description | -|----------|------|----------|---------|-------------| - - diff --git a/docs/sources/developers/kinds/core/serviceaccount/schema-reference.md b/docs/sources/developers/kinds/core/serviceaccount/schema-reference.md deleted file mode 100644 index 4c609ec6bb..0000000000 --- a/docs/sources/developers/kinds/core/serviceaccount/schema-reference.md +++ /dev/null @@ -1,114 +0,0 @@ ---- -keywords: -- grafana -- schema -labels: - products: - - enterprise - - oss -title: ServiceAccount kind ---- - -> Both documentation generation and kinds schemas are in active development and subject to change without prior notice. - -## ServiceAccount - -#### Maturity: [merged](../../../maturity/#merged) - -#### Version: 0.0 - -system account - -| Property | Type | Required | Default | Description | -| ---------- | ------------------- | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `metadata` | [object](#metadata) | **Yes** | | metadata contains embedded CommonMetadata and can be extended with custom string fields
TODO: use CommonMetadata instead of redefining here; currently needs to be defined here
without external reference as using the CommonMetadata reference breaks thema codegen. | -| `spec` | [object](#spec) | **Yes** | | | -| `status` | [object](#status) | **Yes** | | | - -### Metadata - -metadata contains embedded CommonMetadata and can be extended with custom string fields -TODO: use CommonMetadata instead of redefining here; currently needs to be defined here -without external reference as using the CommonMetadata reference breaks thema codegen. - -It extends [\_kubeObjectMetadata](#_kubeobjectmetadata). - -| Property | Type | Required | Default | Description | -| ------------------- | ---------------------- | -------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------- | -| `createdBy` | string | **Yes** | | | -| `creationTimestamp` | string | **Yes** | | _(Inherited from [\_kubeObjectMetadata](#_kubeobjectmetadata))_ | -| `extraFields` | [object](#extrafields) | **Yes** | | extraFields is reserved for any fields that are pulled from the API server metadata but do not have concrete fields in the CUE metadata | -| `finalizers` | string[] | **Yes** | | _(Inherited from [\_kubeObjectMetadata](#_kubeobjectmetadata))_ | -| `labels` | map[string]string | **Yes** | | _(Inherited from [\_kubeObjectMetadata](#_kubeobjectmetadata))_ | -| `resourceVersion` | string | **Yes** | | _(Inherited from [\_kubeObjectMetadata](#_kubeobjectmetadata))_ | -| `uid` | string | **Yes** | | _(Inherited from [\_kubeObjectMetadata](#_kubeobjectmetadata))_ | -| `updateTimestamp` | string | **Yes** | | | -| `updatedBy` | string | **Yes** | | | -| `deletionTimestamp` | string | No | | _(Inherited from [\_kubeObjectMetadata](#_kubeobjectmetadata))_ | - -### \_kubeObjectMetadata - -\_kubeObjectMetadata is metadata found in a kubernetes object's metadata field. -It is not exhaustive and only includes fields which may be relevant to a kind's implementation, -As it is also intended to be generic enough to function with any API Server. - -| Property | Type | Required | Default | Description | -| ------------------- | ----------------- | -------- | ------- | ----------- | -| `creationTimestamp` | string | **Yes** | | | -| `finalizers` | string[] | **Yes** | | | -| `labels` | map[string]string | **Yes** | | | -| `resourceVersion` | string | **Yes** | | | -| `uid` | string | **Yes** | | | -| `deletionTimestamp` | string | No | | | - -### ExtraFields - -extraFields is reserved for any fields that are pulled from the API server metadata but do not have concrete fields in the CUE metadata - -| Property | Type | Required | Default | Description | -| -------- | ---- | -------- | ------- | ----------- | - -### Spec - -| Property | Type | Required | Default | Description | -| --------------- | ------------------ | -------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------- | -| `avatarUrl` | string | **Yes** | | AvatarUrl is the service account's avatar URL. It allows the frontend to display a picture in front
of the service account. | -| `id` | integer | **Yes** | | ID is the unique identifier of the service account in the database. | -| `isDisabled` | boolean | **Yes** | | IsDisabled indicates if the service account is disabled. | -| `login` | string | **Yes** | | Login of the service account. | -| `name` | string | **Yes** | | Name of the service account. | -| `orgId` | integer | **Yes** | | OrgId is the ID of an organisation the service account belongs to. | -| `role` | string | **Yes** | | OrgRole is a Grafana Organization Role which can be 'Viewer', 'Editor', 'Admin'.
Possible values are: `Admin`, `Editor`, `Viewer`. | -| `tokens` | integer | **Yes** | | Tokens is the number of active tokens for the service account.
Tokens are used to authenticate the service account against Grafana. | -| `accessControl` | map[string]boolean | No | | AccessControl metadata associated with a given resource. | -| `teams` | string[] | No | | Teams is a list of teams the service account belongs to. | - -### Status - -| Property | Type | Required | Default | Description | -| ------------------ | ---------------------------------------------------------- | -------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `additionalFields` | [object](#additionalfields) | No | | additionalFields is reserved for future use | -| `operatorStates` | map[string][status.#OperatorState](#status.#operatorstate) | No | | operatorStates is a map of operator ID to operator state evaluations.
Any operator which consumes this kind SHOULD add its state evaluation information to this field. | - -### AdditionalFields - -additionalFields is reserved for future use - -| Property | Type | Required | Default | Description | -| -------- | ---- | -------- | ------- | ----------- | - -### Status.#OperatorState - -| Property | Type | Required | Default | Description | -| ------------------ | ------------------ | -------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `lastEvaluation` | string | **Yes** | | lastEvaluation is the ResourceVersion last evaluated | -| `state` | string | **Yes** | | state describes the state of the lastEvaluation.
It is limited to three possible states for machine evaluation.
Possible values are: `success`, `in_progress`, `failed`. | -| `descriptiveState` | string | No | | descriptiveState is an optional more descriptive state field which has no requirements on format | -| `details` | [object](#details) | No | | details contains any extra information that is operator-specific | - -### Details - -details contains any extra information that is operator-specific - -| Property | Type | Required | Default | Description | -| -------- | ---- | -------- | ------- | ----------- | diff --git a/docs/sources/developers/kinds/core/team/schema-reference.md b/docs/sources/developers/kinds/core/team/schema-reference.md deleted file mode 100644 index 6f759abf31..0000000000 --- a/docs/sources/developers/kinds/core/team/schema-reference.md +++ /dev/null @@ -1,107 +0,0 @@ ---- -keywords: - - grafana - - schema -labels: - products: - - cloud - - enterprise - - oss -title: Team kind ---- -> Both documentation generation and kinds schemas are in active development and subject to change without prior notice. - -## Team - -#### Maturity: [merged](../../../maturity/#merged) -#### Version: 0.0 - -A team is a named grouping of Grafana users to which access control rules may be assigned. - -| Property | Type | Required | Default | Description | -|------------|---------------------|----------|---------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `metadata` | [object](#metadata) | **Yes** | | metadata contains embedded CommonMetadata and can be extended with custom string fields
TODO: use CommonMetadata instead of redefining here; currently needs to be defined here
without external reference as using the CommonMetadata reference breaks thema codegen. | -| `spec` | [object](#spec) | **Yes** | | | -| `status` | [object](#status) | **Yes** | | | - -### Metadata - -metadata contains embedded CommonMetadata and can be extended with custom string fields -TODO: use CommonMetadata instead of redefining here; currently needs to be defined here -without external reference as using the CommonMetadata reference breaks thema codegen. - -It extends [_kubeObjectMetadata](#_kubeobjectmetadata). - -| Property | Type | Required | Default | Description | -|---------------------|------------------------|----------|---------|-----------------------------------------------------------------------------------------------------------------------------------------| -| `createdBy` | string | **Yes** | | | -| `creationTimestamp` | string | **Yes** | | *(Inherited from [_kubeObjectMetadata](#_kubeobjectmetadata))* | -| `extraFields` | [object](#extrafields) | **Yes** | | extraFields is reserved for any fields that are pulled from the API server metadata but do not have concrete fields in the CUE metadata | -| `finalizers` | string[] | **Yes** | | *(Inherited from [_kubeObjectMetadata](#_kubeobjectmetadata))* | -| `labels` | map[string]string | **Yes** | | *(Inherited from [_kubeObjectMetadata](#_kubeobjectmetadata))* | -| `resourceVersion` | string | **Yes** | | *(Inherited from [_kubeObjectMetadata](#_kubeobjectmetadata))* | -| `uid` | string | **Yes** | | *(Inherited from [_kubeObjectMetadata](#_kubeobjectmetadata))* | -| `updateTimestamp` | string | **Yes** | | | -| `updatedBy` | string | **Yes** | | | -| `deletionTimestamp` | string | No | | *(Inherited from [_kubeObjectMetadata](#_kubeobjectmetadata))* | - -### _kubeObjectMetadata - -_kubeObjectMetadata is metadata found in a kubernetes object's metadata field. -It is not exhaustive and only includes fields which may be relevant to a kind's implementation, -As it is also intended to be generic enough to function with any API Server. - -| Property | Type | Required | Default | Description | -|---------------------|-------------------|----------|---------|-------------| -| `creationTimestamp` | string | **Yes** | | | -| `finalizers` | string[] | **Yes** | | | -| `labels` | map[string]string | **Yes** | | | -| `resourceVersion` | string | **Yes** | | | -| `uid` | string | **Yes** | | | -| `deletionTimestamp` | string | No | | | - -### ExtraFields - -extraFields is reserved for any fields that are pulled from the API server metadata but do not have concrete fields in the CUE metadata - -| Property | Type | Required | Default | Description | -|----------|------|----------|---------|-------------| - -### Spec - -| Property | Type | Required | Default | Description | -|----------|--------|----------|---------|--------------------| -| `name` | string | **Yes** | | Name of the team. | -| `email` | string | No | | Email of the team. | - -### Status - -| Property | Type | Required | Default | Description | -|--------------------|------------------------------------------------------------|----------|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `additionalFields` | [object](#additionalfields) | No | | additionalFields is reserved for future use | -| `operatorStates` | map[string][status.#OperatorState](#status.#operatorstate) | No | | operatorStates is a map of operator ID to operator state evaluations.
Any operator which consumes this kind SHOULD add its state evaluation information to this field. | - -### AdditionalFields - -additionalFields is reserved for future use - -| Property | Type | Required | Default | Description | -|----------|------|----------|---------|-------------| - -### Status.#OperatorState - -| Property | Type | Required | Default | Description | -|--------------------|--------------------|----------|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `lastEvaluation` | string | **Yes** | | lastEvaluation is the ResourceVersion last evaluated | -| `state` | string | **Yes** | | state describes the state of the lastEvaluation.
It is limited to three possible states for machine evaluation.
Possible values are: `success`, `in_progress`, `failed`. | -| `descriptiveState` | string | No | | descriptiveState is an optional more descriptive state field which has no requirements on format | -| `details` | [object](#details) | No | | details contains any extra information that is operator-specific | - -### Details - -details contains any extra information that is operator-specific - -| Property | Type | Required | Default | Description | -|----------|------|----------|---------|-------------| - - diff --git a/docs/sources/developers/kinds/maturity.md b/docs/sources/developers/kinds/maturity.md deleted file mode 100644 index e9a6264f2f..0000000000 --- a/docs/sources/developers/kinds/maturity.md +++ /dev/null @@ -1,298 +0,0 @@ ---- -keywords: -- grafana -- schema -- maturity -labels: - products: - - enterprise - - oss -title: Grafana Kinds - From Zero to Maturity -weight: 300 ---- - -# Grafana Kinds - From Zero to Maturity - -> Grafana’s schema, Kind, and related codegen systems are under intense development. - -Fear of unknown impacts leads to defensive coding, slow PRs, circular arguments, and an overall hesitance to engage. -That friction alone is sufficient to sink a large-scale project. This guide seeks to counteract this friction by -defining an end goal for all schemas: “mature.” This is the word we’re using to refer to the commonsense notion of “this -software reached 1.0.” - -In general, 1.0/mature suggests: “we’ve thought about this thing, done the necessary experimenting, know what it is, and -feel confident about presenting it to the world.” In the context of schemas intended to act as a single source of truth -driving many use cases, we can intuitively phrase maturity as: - -- The schema follows general best practices (e.g. good comments, follows field type rules), and the team owning the - schema believes that the fields described in the schema are accurate. -- Automation propagates the schema as source of truth to every relevant - [domain](https://docs.google.com/document/d/13Rv395_T8WTLBgdL-2rbXKu0fx_TW-Q9yz9x6oBjm6g/edit#heading=h.67pop2k2f8fq) - (for example: types in frontend, backend, as-code; plugins SDK; docs; APIs and storage; search indexing) - -This intuitive definition gets us pointed in the right direction. But we can’t just jump straight there - we have to -approach it methodically. To that end, this doc outlines four (ok five, but really, four) basic maturity milestones that -we expect Kinds and their schemas to progress through: - -- _(Planned - Put a Kind name on the official TODO list)_ -- **Merged** - Get an initial schema written down. Not final. Not perfect. -- **Experimental** - Kind schemas are the source of truth for basic working code. -- **Stable** - Kind schemas are the source of truth for all target domains. -- **Mature** - The operational transition path for the Kind is battle-tested and reliable. - -These milestones have functional definitions, tied to code and enforced in CI. A Kind having reached a particular -milestone corresponds to properties of the code that are enforced in CI; advancing to the next milestone likely has a -direct impact on code generation and runtime behavior. - -Finally, the above definitions imply that maturity for _individual Kinds/schemas_ depends on _the Kind system_ being -mature, as well. This is by design: **Grafana Labs does not intend to publicize any single schema as mature until -[certain schema system milestones are met](https://github.com/orgs/grafana/projects/133/views/8).** - -## Schema Maturity Milestones - -Maturity milestones are a linear progression. Each milestone implies that the conditions of its predecessors continue to -be met. - -Reaching a particular milestone implies that the properties of all prior milestones are still met. - -### (Milestone 0 - Planned) {#planned} - -| **Goal** | Put a Kind name on the official TODO list: [Kind Schematization Progress Tracker](https://docs.google.com/spreadsheets/d/1DL6nZHyX42X013QraWYbKsMmHozLrtXDj8teLKvwYMY/edit#gid=0) | -| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Reached when** | The planned Kind is listed in the relevant sheet of the progress tracker with a link to track / be able to see when exactly it is planned and who is responsible for doing it | -| **Common hurdles** | Existing definitions may not correspond clearly to an object boundary - e.g. playlists are currently in denormalized SQL tables playlist and playlist_item | -| **Public-facing guarantees** | None | -| **customer-facing stage** | None | - -### Milestone 1 - Merged {#merged} - -| **Goal** | Get an initial schema written down. Not final. Not perfect. | -| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Reached when** | A PR introducing the initial version of a schema has been merged. | -| **Common hurdles** | Getting comfortable with Thema and CUE
Figuring out where all the existing definitions of the Kind are
Knowing whether it’s safe to omit possibly-crufty fields from the existing definitions when writing the schema | -| **Public-facing guarantees** | None | -| **User-facing stage** | None | - -### Milestone 2 - Experimental {#experimental} - -| **Goal** | Schemas are the source of truth for basic working code. | -| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Reached when** | Go and TypeScript types generated from schema are used in all relevant production code, having replaced handwritten type definitions (if any). | -| **Common hurdles** | Compromises on field definitions that seemed fine to reach “committed” start to feel unacceptable
Ergonomics of generated code may start to bite
Aligning with the look and feel of related schemas | -| **Public-facing guarantees** | Kinds are available for as-code usage in [grok](https://github.com/grafana/grok), and in tools downstream of grok, following all of grok’s standard patterns. | -| **Stage comms** | Internal users:- Start using the schema and give feedback internally to help move to the next stage.External users:- Align with the [experimental](https://docs.google.com/document/d/1lqp0hALax2PT7jSObsX52EbQmIDFnLFMqIbBrJ4EYCE/edit#heading=h.ehl5iy7pcjvq) stage in the release definition document.  - Experimental schemas will be discoverable, and from a customer PoV should never be used in production, but they can be explored and we are more than happy to receive feedback | - -## Schema-writing guidelines - -### Avoid anonymous nested structs - -**_Always name your sub-objects._** - -In CUE, nesting structs is like nesting objects in JSON, and just as easy: - -```json -one: { - two: { - three: { - } -} -``` - -While these can be accurately represented in other languages, they aren’t especially friendly to work with: - -```typescript -// TypeScript -export interface One { - two: { - three: string; - }; -} -``` - -```go -// Go -type One struct { - Two struct { - Three string `json:"three"` - } `json:"two"` -} -``` - -Instead, within your schema, prefer to make root-level definitions with the appropriate attributes: - -```cue -// Cue -one: { - two: #Two - #Two: { - three: string - } @cuetsy(kind="interface") -} -``` - -```Typescript -// TypeScript -export interface Two { - three: string; -} -export interface One { - two: Two; -} -``` - -```Go -// Go -type One struct { - Two Two `json:"two"` -} -type Two struct { - Three string `json:"three"` -} -``` - -### Use precise numeric types - -**_Use precise numeric types like `float64` or `uint32`. Never use `number`._** - -Never use `number` for a numeric type in a schema. - -Instead, use a specific, sized type like `int64` or `float32`. This makes your intent precisely clear. -TypeScript will still represent these fields with `number`, but other languages (e.g. Go, Protobuf) can be more precise. - -Unlike in Go, int and uint are not your friends. These correspond to `math/big` types. Use a sized type, -like `uint32` or `int32`, unless the use case specifically requires a huge numeric space. - -### No explicit `null` - -**_Do not use `null` as a type in any schema._** - -This one is tricky to think about, and requires some background. - -Historically, Grafana’s dashboard JSON has often contained fields with the explicit value `null`. -This was problematic, because explicit `null` introduces an ambiguity: is a JSON field being present -with value null meaningfully different from the field being absent? That is, should a program behave differently -if it encounters a null vs. an absent field? - -In almost all cases, the answer is “no.” Thus, the ambiguity: if both explicit null and absence are _accepted_ -by a system, it pushes responsibility onto anyone writing code in that system to decide, case-by-case, -whether the two are _intended to be meaningfully different_, and therefore whether behavior should be different. - -CUE does have a `null` type, and only accepts data containing `nulls` as valid if the schema explicitly allows a `null`. -That means, by default, using CUE for schemas removes the possibility of ambiguity in code that receives data validated -by those schemas, even if the language they’re writing in still allows for ambiguity. (Javascript does, Go doesn’t.) - -As a schema author, this means you’re being unambiguous by default - no `nulls`. That’s good! The only question is -whether it’s worth explicitly allowing a `null` for some particular case: - -```Cue -someField: int32 | null -``` - -The _only_ time this _may_ be a good idea is if your field needs to be able to represent a value -that is not otherwise acceptable within the value space - for example, if `someField` needs to be able to contain -[Infinity](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/POSITIVE_INFINITY). -When such values are serialized to null by default, it can be convenient to accept null in the schema - but even then, -explicit null is unlikely to be the best way to represent such values, because it is so subtle and falsey. - -**Above all, DO NOT accept `null` in a schema simply because current behavior sometimes unintentionally produces a `null`.** -Schematization is an opportunity to get rid of this ambiguity. Fix the accidental null-producing behavior, instead. - -### Issues - -- If a schema has a "kind" field and its set as enum, it generates a Kind alias that conflicts with the generated - Kind struct. -- Byte fields are existing in Go but not in TS, so the generator fails. -- **omitempty** is useful when we return things like json.RawMessage (alias of []byte) because Postgres saves this - information as `nil`, when MySQL and SQLite save it as `{}`. If we found it in the rest of the cases, it isn't necessary - to set `?` in the field in the schema. - -## Schema Attributes - -Grafana’s schema system relies on [CUE attributes](https://cuelang.org/docs/references/spec/#attributes)declared on -properties within schemas to control some aspects of code generation behavior. -In a schema, an attribute is the whole of `@cuetsy(kind=”type”)`: - -```Cue -field: string @cuetsy(kind="type") -``` - -CUE attributes are purely informational - they cannot influence CUE evaluation behavior, including the types being -expressed in a Thema schema. - -CUE attributes have three parts. In `@cuetsy(kind=”type”)`, those are: - -- name - `@cuetsy` -- arg - `kind` -- argval - `“type”` - -Any given attribute may consist of `{name}`, `{name,arg}`, or `{name,arg,argval}`. These three levels form a tree -(meaning of any argval is specific to its arg, which is specific to its name). The following documentation represents -this tree using a header hierarchy. - -### @cuetsy - -These attributes control the behavior of the [cuetsy code generator](https://github.com/grafana/cuetsy), which converts -CUE to TypeScript. We include only the kind arg here for brevity; cuetsy’s README has the canonical documentation on all -supported args and argvals, and their intended usage. - -Notes: - -- Only top-level fields in a Thema schema are scanned for `@cuetsy` attributes. -- Grafana’s code generators hardcode that an interface (`@cuetsy(kind=”interface”)`) is generated to represent the root - schema object, unless it is known to be a [grouped lineage](https://docs.google.com/document/d/13Rv395_T8WTLBgdL-2rbXKu0fx_TW-Q9yz9x6oBjm6g/edit#heading=h.vx7stzpxtw4t). - -#### kind - -Indicates the kind of TypeScript symbol that should be generated for that schema field. - -#### interface - -Generate the schema field as a TS interface. Field must be struct-kinded. - -#### enum - -Generate the schema field as a TS enum. Field must be either int-kinded (numeric enums) or string-kinded (string enums). - -#### type - -Generate the schema field as a TS type alias. - -### @grafana - -These attributes control code generation behaviors that are specific to Grafana core. Some may also be supported -in plugin code generators. - -#### TSVeneer - -Applying a TSVeneer arg to a field in a schema indicates that the schema author wants to enrich the generated type -(for example by adding generic type parameters), so code generation should expect a handwritten -[veneer](https://docs.google.com/document/d/13Rv395_T8WTLBgdL-2rbXKu0fx_TW-Q9yz9x6oBjm6g/edit#heading=h.bmtjq0bb1yxp). - -TSVeneer requires at least one argval, each of which impacts TypeScript code generation in its own way. -Multiple argvals may be given, separated by `|`. - -A TSVeneer arg has no effect if it is applied to a field that is not exported as a standalone TypeScript type -(which usually means a CUE field that also has an `@cuetsy(kind=)` attribute). - -#### type - -A handwritten veneer is needed to refine the raw generated TypeScript type, for example by adding generics. -See [the dashboard types veneer](https://github.com/grafana/grafana/blob/5f93e67419e9587363d1fc1e6f1f4a8044eb54d0/packages/grafana-schema/src/veneer/dashboard.types.ts) -for an example, and [some](https://github.com/grafana/grafana/blob/5f93e67419e9587363d1fc1e6f1f4a8044eb54d0/kinds/dashboard/dashboard_kind.cue#L12) -[corresponding](https://github.com/grafana/grafana/blob/5f93e67419e9587363d1fc1e6f1f4a8044eb54d0/kinds/dashboard/dashboard_kind.cue#L143) -CUE attributes. - -### @grafanamaturity - -These attributes are used to support iterative development of a schema towards maturity. - -Grafana code generators and CI enforce that schemas marked as mature MUST NOT have any `@grafanamaturity` attributes. - -#### NeedsExpertReview - -Indicates that a non-expert on that schema wrote the field, and was not fully confident in its type and/or docs. - -Primarily useful on very large schemas, like the dashboard schema, for getting _something_ written down for a given -field that at least makes validation tests pass, but making clear that the field isn’t necessarily properly correct. - -No argval is accepted. (Use a `//` comment to say more about the attention that’s needed.) diff --git a/kinds/gen.go b/kinds/gen.go index 00225af2b3..b126b1a32b 100644 --- a/kinds/gen.go +++ b/kinds/gen.go @@ -47,7 +47,6 @@ func main() { true, // forcing group so that we ignore the top level resource (for now) codegen.TSResourceJenny{}), codegen.TSVeneerIndexJenny(filepath.Join("packages", "grafana-schema", "src")), - codegen.DocsJenny(filepath.Join("docs", "sources", "developers", "kinds", "core")), ) header := codegen.SlashHeaderMapper("kinds/gen.go") diff --git a/pkg/codegen/jenny_docs.go b/pkg/codegen/jenny_docs.go deleted file mode 100644 index 1883faff00..0000000000 --- a/pkg/codegen/jenny_docs.go +++ /dev/null @@ -1,793 +0,0 @@ -package codegen - -import ( - "bytes" - "encoding/json" - "errors" - "fmt" - "io" - "path" - "path/filepath" - "reflect" - "sort" - "strings" - "text/template" - - "cuelang.org/go/cue/cuecontext" - "github.com/grafana/codejen" - "github.com/grafana/kindsys" - "github.com/grafana/thema/encoding/jsonschema" - "github.com/olekukonko/tablewriter" - "github.com/xeipuuv/gojsonpointer" - - "github.com/grafana/grafana/pkg/components/simplejson" -) - -func DocsJenny(docsPath string) OneToOne { - return docsJenny{ - docsPath: docsPath, - } -} - -type docsJenny struct { - docsPath string -} - -func (j docsJenny) JennyName() string { - return "DocsJenny" -} - -func (j docsJenny) Generate(kind kindsys.Kind) (*codejen.File, error) { - // TODO remove this once codejen catches nils https://github.com/grafana/codejen/issues/5 - if kind == nil { - return nil, nil - } - - f, err := jsonschema.GenerateSchema(kind.Lineage().Latest()) - if err != nil { - return nil, fmt.Errorf("failed to generate json representation for the schema: %v", err) - } - b, err := cuecontext.New().BuildFile(f).MarshalJSON() - if err != nil { - return nil, fmt.Errorf("failed to marshal schema value to json: %v", err) - } - - // We don't need entire json obj, only the value of components.schemas path - var obj struct { - Info struct { - Title string - } - Components struct { - Schemas json.RawMessage - } - } - dec := json.NewDecoder(bytes.NewReader(b)) - dec.UseNumber() - err = dec.Decode(&obj) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal schema json: %v", err) - } - - // fixes the references between the types within a json after making components.schema. the root of the json - kindJsonStr := strings.Replace(string(obj.Components.Schemas), "#/components/schemas/", "#/", -1) - - kindProps := kind.Props().Common() - data := templateData{ - KindName: kindProps.Name, - KindVersion: kind.Lineage().Latest().Version().String(), - KindMaturity: fmt.Sprintf("[%s](../../../maturity/#%[1]s)", kindProps.Maturity), - KindDescription: kindProps.Description, - Markdown: "{{ .Markdown }}", - } - - tmpl, err := makeTemplate(data, "docs.tmpl") - if err != nil { - return nil, err - } - - doc, err := jsonToMarkdown([]byte(kindJsonStr), string(tmpl), obj.Info.Title) - if err != nil { - return nil, fmt.Errorf("failed to build markdown for kind %s: %v", kindProps.Name, err) - } - - return codejen.NewFile(filepath.Join(j.docsPath, strings.ToLower(kindProps.Name), "schema-reference.md"), doc, j), nil -} - -// makeTemplate pre-populates the template with the kind metadata -func makeTemplate(data templateData, tmpl string) ([]byte, error) { - buf := new(bytes.Buffer) - if err := tmpls.Lookup(tmpl).Execute(buf, data); err != nil { - return []byte{}, fmt.Errorf("failed to populate docs template with the kind metadata") - } - return buf.Bytes(), nil -} - -type templateData struct { - KindName string - KindVersion string - KindMaturity string - KindDescription string - Markdown string -} - -// -------------------- JSON to Markdown conversion -------------------- -// Copied from https://github.com/marcusolsson/json-schema-docs and slightly changed to fit the DocsJenny -type constraints struct { - Pattern string `json:"pattern"` - Maximum json.Number `json:"maximum"` - ExclusiveMinimum bool `json:"exclusiveMinimum"` - Minimum json.Number `json:"minimum"` - ExclusiveMaximum bool `json:"exclusiveMaximum"` - MinLength uint `json:"minLength"` - MaxLength uint `json:"maxLength"` -} - -type schema struct { - constraints - ID string `json:"$id,omitempty"` - Ref string `json:"$ref,omitempty"` - Schema string `json:"$schema,omitempty"` - Title string `json:"title,omitempty"` - Description string `json:"description,omitempty"` - Required []string `json:"required,omitempty"` - Type PropertyTypes `json:"type,omitempty"` - Properties map[string]*schema `json:"properties,omitempty"` - Items *schema `json:"items,omitempty"` - Definitions map[string]*schema `json:"definitions,omitempty"` - Enum []Any `json:"enum"` - Default any `json:"default"` - AllOf []*schema `json:"allOf"` - OneOf []*schema `json:"oneOf"` - AdditionalProperties *schema `json:"additionalProperties"` - extends []string `json:"-"` - inheritedFrom string `json:"-"` -} - -func renderMapType(props *schema) string { - if props == nil { - return "" - } - - if props.Type.HasType(PropertyTypeObject) { - name, anchor := propNameAndAnchor(props.Title, props.Title) - return fmt.Sprintf("[%s](#%s)", name, anchor) - } - - if props.AdditionalProperties != nil { - return "map[string]" + renderMapType(props.AdditionalProperties) - } - - if props.Items != nil { - return "[]" + renderMapType(props.Items) - } - - var types []string - for _, t := range props.Type { - types = append(types, string(t)) - } - return strings.Join(types, ", ") -} - -func jsonToMarkdown(jsonData []byte, tpl string, kindName string) ([]byte, error) { - sch, err := newSchema(jsonData, kindName) - if err != nil { - return []byte{}, err - } - - t, err := template.New("markdown").Parse(tpl) - if err != nil { - return []byte{}, err - } - - buf := new(bytes.Buffer) - err = t.Execute(buf, sch) - if err != nil { - return []byte{}, err - } - - return buf.Bytes(), nil -} - -func newSchema(b []byte, kindName string) (*schema, error) { - var data map[string]*schema - if err := json.Unmarshal(b, &data); err != nil { - return nil, err - } - - // Needed for resolving in-schema references. - root, err := simplejson.NewJson(b) - if err != nil { - return nil, err - } - - return resolveSchema(data[kindName], root) -} - -// resolveSchema recursively resolves schemas. -func resolveSchema(schem *schema, root *simplejson.Json) (*schema, error) { - for _, prop := range schem.Properties { - if prop.Ref != "" { - tmp, err := resolveReference(prop.Ref, root) - if err != nil { - return nil, err - } - *prop = *tmp - } - foo, err := resolveSchema(prop, root) - if err != nil { - return nil, err - } - *prop = *foo - } - - if schem.Items != nil { - if schem.Items.Ref != "" { - tmp, err := resolveReference(schem.Items.Ref, root) - if err != nil { - return nil, err - } - *schem.Items = *tmp - } - foo, err := resolveSchema(schem.Items, root) - if err != nil { - return nil, err - } - *schem.Items = *foo - } - - if len(schem.AllOf) > 0 { - for idx, child := range schem.AllOf { - tmp, err := resolveSubSchema(schem, child, root) - if err != nil { - return nil, err - } - schem.AllOf[idx] = tmp - - if len(tmp.Title) > 0 { - schem.extends = append(schem.extends, tmp.Title) - } - } - } - - if len(schem.OneOf) > 0 { - for idx, child := range schem.OneOf { - tmp, err := resolveSubSchema(schem, child, root) - if err != nil { - return nil, err - } - schem.OneOf[idx] = tmp - } - } - - if schem.AdditionalProperties != nil { - if schem.AdditionalProperties.Ref != "" { - tmp, err := resolveReference(schem.AdditionalProperties.Ref, root) - if err != nil { - return nil, err - } - *schem.AdditionalProperties = *tmp - } - foo, err := resolveSchema(schem.AdditionalProperties, root) - if err != nil { - return nil, err - } - *schem.AdditionalProperties = *foo - } - - return schem, nil -} - -func resolveSubSchema(parent, child *schema, root *simplejson.Json) (*schema, error) { - if child.Ref != "" { - tmp, err := resolveReference(child.Ref, root) - if err != nil { - return nil, err - } - *child = *tmp - } - - if len(child.Required) > 0 { - parent.Required = append(parent.Required, child.Required...) - } - - child, err := resolveSchema(child, root) - if err != nil { - return nil, err - } - - if parent.Properties == nil { - parent.Properties = make(map[string]*schema) - } - - for k, v := range child.Properties { - prop := *v - prop.inheritedFrom = child.Title - parent.Properties[k] = &prop - } - - return child, err -} - -// resolveReference loads a schema from a $ref. -// If ref contains a hashtag (#), the part after represents a in-schema reference. -func resolveReference(ref string, root *simplejson.Json) (*schema, error) { - i := strings.Index(ref, "#") - - if i != 0 { - return nil, fmt.Errorf("not in-schema reference: %s", ref) - } - return resolveInSchemaReference(ref[i+1:], root) -} - -func resolveInSchemaReference(ref string, root *simplejson.Json) (*schema, error) { - // in-schema reference - pointer, err := gojsonpointer.NewJsonPointer(ref) - if err != nil { - return nil, err - } - - v, _, err := pointer.Get(root.MustMap()) - if err != nil { - return nil, err - } - - var sch schema - b, err := json.Marshal(v) - if err != nil { - return nil, err - } - - if err := json.Unmarshal(b, &sch); err != nil { - return nil, err - } - - // Set the ref name as title - sch.Title = path.Base(ref) - - return &sch, nil -} - -type mdSection struct { - title string - extends string - description string - rows [][]string -} - -func (md mdSection) write(w io.Writer) { - if md.title != "" { - fmt.Fprintf(w, "### %s\n", strings.Title(md.title)) - fmt.Fprintln(w) - } - - if md.description != "" { - fmt.Fprintln(w, md.description) - fmt.Fprintln(w) - } - - if md.extends != "" { - fmt.Fprintln(w, md.extends) - fmt.Fprintln(w) - } - - table := tablewriter.NewWriter(w) - table.SetHeader([]string{"Property", "Type", "Required", "Default", "Description"}) - table.SetBorders(tablewriter.Border{Left: true, Top: false, Right: true, Bottom: false}) - table.SetCenterSeparator("|") - table.SetAutoFormatHeaders(false) - table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) - table.SetAutoWrapText(false) - table.AppendBulk(md.rows) - table.Render() - fmt.Fprintln(w) -} - -// Markdown returns the Markdown representation of the schema. -// -// The level argument can be used to offset the heading levels. This can be -// useful if you want to add the schema under a subheading. -func (s *schema) Markdown() string { - buf := new(bytes.Buffer) - - for _, v := range s.sections() { - v.write(buf) - } - - return buf.String() -} - -func (s *schema) sections() []mdSection { - md := mdSection{} - - if s.AdditionalProperties == nil { - md.title = s.Title - } - md.description = s.Description - - if len(s.extends) > 0 { - md.extends = makeExtends(s.extends) - } - md.rows = makeRows(s) - - sections := []mdSection{md} - for _, sch := range findDefinitions(s) { - for _, ss := range sch.sections() { - if !contains(sections, ss) { - sections = append(sections, ss) - } - } - } - - return sections -} - -func contains(sl []mdSection, elem mdSection) bool { - for _, s := range sl { - if reflect.DeepEqual(s, elem) { - return true - } - } - return false -} - -func makeExtends(from []string) string { - fromLinks := make([]string, 0, len(from)) - for _, f := range from { - fromLinks = append(fromLinks, fmt.Sprintf("[%s](#%s)", f, strings.ToLower(f))) - } - - return fmt.Sprintf("It extends %s.", strings.Join(fromLinks, " and ")) -} - -func findDefinitions(s *schema) []*schema { - // Gather all properties of object type so that we can generate the - // properties for them recursively. - var objs []*schema - - definition := func(k string, p *schema) { - if p.Type.HasType(PropertyTypeObject) && p.AdditionalProperties == nil { - // Use the identifier as the title. - if len(p.Title) == 0 { - p.Title = k - } - objs = append(objs, p) - } - - // If the property is an array of objects, use the name of the array - // property as the title. - if p.Type.HasType(PropertyTypeArray) { - if p.Items != nil { - if p.Items.Type.HasType(PropertyTypeObject) { - if len(p.Items.Title) == 0 { - p.Items.Title = k - } - objs = append(objs, p.Items) - } - } - } - } - - for k, p := range s.Properties { - // If a property has AdditionalProperties, then it's a map - if p.AdditionalProperties != nil { - definition(k, p.AdditionalProperties) - } - - definition(k, p) - } - - // This code could probably be unified with the one above - for _, child := range s.AllOf { - if child.Type.HasType(PropertyTypeObject) { - objs = append(objs, child) - } - - if child.Type.HasType(PropertyTypeArray) { - if child.Items != nil { - if child.Items.Type.HasType(PropertyTypeObject) { - objs = append(objs, child.Items) - } - } - } - } - - for _, child := range s.OneOf { - if child.Type.HasType(PropertyTypeObject) { - objs = append(objs, child) - } - - if child.Type.HasType(PropertyTypeArray) { - if child.Items != nil { - if child.Items.Type.HasType(PropertyTypeObject) { - objs = append(objs, child.Items) - } - } - } - } - - // Sort the object schemas. - sort.Slice(objs, func(i, j int) bool { - return objs[i].Title < objs[j].Title - }) - - return objs -} - -func makeRows(s *schema) [][]string { - // Buffer all property rows so that we can sort them before printing them. - rows := make([][]string, 0, len(s.Properties)) - - var typeStr string - if len(s.OneOf) > 0 { - typeStr = enumStr(s) - rows = append(rows, []string{"`object`", typeStr, "", ""}) - return rows - } - - for key, p := range s.Properties { - alias := propTypeAlias(p) - - if alias != "" { - typeStr = alias - } else { - typeStr = propTypeStr(key, p) - } - - // Emphasize required properties. - var required string - if in(s.Required, key) { - required = "**Yes**" - } else { - required = "No" - } - - var desc string - if p.inheritedFrom != "" { - desc = fmt.Sprintf("*(Inherited from [%s](#%s))*", p.inheritedFrom, strings.ToLower(p.inheritedFrom)) - } - - if p.Description != "" { - desc += "\n" + p.Description - } - - if len(p.Enum) > 0 { - vals := make([]string, 0, len(p.Enum)) - for _, e := range p.Enum { - vals = append(vals, e.String()) - } - desc += "\nPossible values are: `" + strings.Join(vals, "`, `") + "`." - } - - var defaultValue string - if p.Default != nil { - defaultValue = fmt.Sprintf("`%v`", p.Default) - } - - // Render a constraint only if it's not a type alias https://cuelang.org/docs/references/spec/#predeclared-identifiers - if alias == "" { - desc += constraintDescr(p) - } - rows = append(rows, []string{fmt.Sprintf("`%s`", key), typeStr, required, defaultValue, formatForTable(desc)}) - } - - // Sort by the required column, then by the name column. - sort.Slice(rows, func(i, j int) bool { - if rows[i][2] < rows[j][2] { - return true - } - if rows[i][2] > rows[j][2] { - return false - } - return rows[i][0] < rows[j][0] - }) - return rows -} - -func propTypeAlias(prop *schema) string { - if prop.Minimum == "" || prop.Maximum == "" { - return "" - } - - min := prop.Minimum - max := prop.Maximum - - switch { - case min == "0" && max == "255": - return "uint8" - case min == "0" && max == "65535": - return "uint16" - case min == "0" && max == "4294967295": - return "uint32" - case min == "0" && max == "18446744073709551615": - return "uint64" - case min == "-128" && max == "127": - return "int8" - case min == "-32768" && max == "32767": - return "int16" - case min == "-2147483648" && max == "2147483647": - return "int32" - case min == "-9223372036854775808" && max == "9223372036854775807": - return "int64" - default: - return "" - } -} - -func constraintDescr(prop *schema) string { - if prop.Minimum != "" && prop.Maximum != "" { - var left, right string - if prop.ExclusiveMinimum { - left = ">" + prop.Minimum.String() - } else { - left = ">=" + prop.Minimum.String() - } - - if prop.ExclusiveMaximum { - right = "<" + prop.Maximum.String() - } else { - right = "<=" + prop.Maximum.String() - } - return fmt.Sprintf("\nConstraint: `%s & %s`.", left, right) - } - - if prop.MinLength > 0 { - left := fmt.Sprintf(">=%v", prop.MinLength) - right := "" - - if prop.MaxLength > 0 { - right = fmt.Sprintf(" && <=%v", prop.MaxLength) - } - return fmt.Sprintf("\nConstraint: `length %s`.", left+right) - } - - if prop.Pattern != "" { - return fmt.Sprintf("\nConstraint: must match `%s`.", prop.Pattern) - } - - return "" -} - -func enumStr(propValue *schema) string { - var vals []string - for _, v := range propValue.OneOf { - vals = append(vals, fmt.Sprintf("[%s](#%s)", v.Title, strings.ToLower(v.Title))) - } - return "Possible types are: " + strings.Join(vals, ", ") + "." -} - -func propTypeStr(propName string, propValue *schema) string { - // If the property has AdditionalProperties, it is most likely a map type - if propValue.AdditionalProperties != nil { - mapValue := renderMapType(propValue.AdditionalProperties) - return "map[string]" + mapValue - } - - propType := make([]string, 0, len(propValue.Type)) - // Generate relative links for objects and arrays of objects. - for _, pt := range propValue.Type { - switch pt { - case PropertyTypeObject: - name, anchor := propNameAndAnchor(propName, propValue.Title) - propType = append(propType, fmt.Sprintf("[%s](#%s)", name, anchor)) - case PropertyTypeArray: - if propValue.Items != nil { - for _, pi := range propValue.Items.Type { - if pi == PropertyTypeObject { - name, anchor := propNameAndAnchor(propName, propValue.Items.Title) - propType = append(propType, fmt.Sprintf("[%s](#%s)[]", name, anchor)) - } else { - propType = append(propType, fmt.Sprintf("%s[]", pi)) - } - } - } else { - propType = append(propType, string(pt)) - } - default: - propType = append(propType, string(pt)) - } - } - - if len(propType) == 0 { - return "" - } - - if len(propType) == 1 { - return propType[0] - } - - if len(propType) == 2 { - return strings.Join(propType, " or ") - } - - return fmt.Sprintf("%s, or %s", strings.Join(propType[:len(propType)-1], ", "), propType[len(propType)-1]) -} - -func propNameAndAnchor(prop, title string) (string, string) { - if len(title) > 0 { - return title, strings.ToLower(title) - } - return string(PropertyTypeObject), strings.ToLower(prop) -} - -// in returns true if a string slice contains a specific string. -func in(strs []string, str string) bool { - for _, s := range strs { - if s == str { - return true - } - } - return false -} - -// formatForTable returns string usable in a Markdown table. -// It trims white spaces, replaces new lines and pipe characters. -func formatForTable(in string) string { - s := strings.TrimSpace(in) - s = strings.ReplaceAll(s, "\n", "
") - s = strings.ReplaceAll(s, "|", "|") - return s -} - -type PropertyTypes []PropertyType - -func (pts *PropertyTypes) HasType(pt PropertyType) bool { - for _, t := range *pts { - if t == pt { - return true - } - } - return false -} - -func (pts *PropertyTypes) UnmarshalJSON(data []byte) error { - var value any - if err := json.Unmarshal(data, &value); err != nil { - return err - } - - switch val := value.(type) { - case string: - *pts = []PropertyType{PropertyType(val)} - return nil - case []any: - var pt []PropertyType - for _, t := range val { - s, ok := t.(string) - if !ok { - return errors.New("unsupported property type") - } - pt = append(pt, PropertyType(s)) - } - *pts = pt - default: - return errors.New("unsupported property type") - } - - return nil -} - -type PropertyType string - -const ( - PropertyTypeString PropertyType = "string" - PropertyTypeNumber PropertyType = "number" - PropertyTypeBoolean PropertyType = "boolean" - PropertyTypeObject PropertyType = "object" - PropertyTypeArray PropertyType = "array" - PropertyTypeNull PropertyType = "null" -) - -type Any struct { - value any -} - -func (u *Any) UnmarshalJSON(data []byte) error { - if err := json.Unmarshal(data, &u.value); err != nil { - return err - } - return nil -} - -func (u *Any) String() string { - return fmt.Sprintf("%v", u.value) -} diff --git a/pkg/codegen/tmpl/docs.tmpl b/pkg/codegen/tmpl/docs.tmpl deleted file mode 100644 index 4fa8df8002..0000000000 --- a/pkg/codegen/tmpl/docs.tmpl +++ /dev/null @@ -1,21 +0,0 @@ ---- -keywords: - - grafana - - schema -labels: - products: - - cloud - - enterprise - - oss -title: {{ .KindName }} kind ---- -> Both documentation generation and kinds schemas are in active development and subject to change without prior notice. - -## {{ .KindName }} - -#### Maturity: {{ .KindMaturity }} -#### Version: {{ .KindVersion }} - -{{ .KindDescription }} - -{{ .Markdown }} diff --git a/pkg/kindsysreport/attributes.go b/pkg/kindsysreport/attributes.go deleted file mode 100644 index c51a4b3df7..0000000000 --- a/pkg/kindsysreport/attributes.go +++ /dev/null @@ -1,74 +0,0 @@ -package kindsysreport - -import ( - "cuelang.org/go/cue" -) - -type AttributeWalker struct { - seen map[cue.Value]bool - count map[string]int -} - -func (w *AttributeWalker) Count(sch cue.Value, attrs ...string) map[string]int { - w.seen = make(map[cue.Value]bool) - w.count = make(map[string]int) - - for _, attr := range attrs { - w.count[attr] = 0 - } - - w.walk(cue.MakePath(), sch) - return w.count -} - -func (w *AttributeWalker) walk(p cue.Path, v cue.Value) { - if w.seen[v] { - return - } - - w.seen[v] = true - - for attr := range w.count { - if found := v.Attribute(attr); found.Err() == nil { - w.count[attr]++ - } - } - - // nolint: exhaustive - switch v.Kind() { - case cue.StructKind: - // If current cue.Value is a reference to another - // definition, we don't want to traverse its fields - // individually, because we'll do so for the actual def. - if v != cue.Dereference(v) { - return - } - - iter, err := v.Fields(cue.All()) - if err != nil { - panic(err) - } - - for iter.Next() { - w.walk(appendPath(p, iter.Selector()), iter.Value()) - } - if lv := v.LookupPath(cue.MakePath(cue.AnyString)); lv.Exists() { - w.walk(appendPath(p, cue.AnyString), lv) - } - case cue.ListKind: - list, err := v.List() - if err != nil { - panic(err) - } - for i := 0; list.Next(); i++ { - w.walk(appendPath(p, cue.Index(i)), list.Value()) - } - if lv := v.LookupPath(cue.MakePath(cue.AnyIndex)); lv.Exists() { - w.walk(appendPath(p, cue.AnyString), lv) - } - } -} - -func appendPath(p cue.Path, sel cue.Selector) cue.Path { - return cue.MakePath(append(p.Selectors(), sel)...) -} diff --git a/pkg/kindsysreport/codegen/report.go b/pkg/kindsysreport/codegen/report.go deleted file mode 100644 index 433c5d43c6..0000000000 --- a/pkg/kindsysreport/codegen/report.go +++ /dev/null @@ -1,391 +0,0 @@ -//go:build ignore -// +build ignore - -//go:generate go run report.go - -package main - -import ( - "context" - "encoding/json" - "fmt" - "net/url" - "os" - "path/filepath" - "reflect" - "sort" - "strings" - - "cuelang.org/go/cue" - "github.com/grafana/codejen" - "github.com/grafana/kindsys" - "github.com/grafana/thema" - - "github.com/grafana/grafana/pkg/kindsysreport" - "github.com/grafana/grafana/pkg/plugins/pfs/corelist" - "github.com/grafana/grafana/pkg/plugins/plugindef" - "github.com/grafana/grafana/pkg/registry/corekind" -) - -const ( - // Program's output - reportFileName = "report.json" - - // External references - repoBaseURL = "https://github.com/grafana/grafana/tree/main" - docsBaseURL = "https://grafana.com/docs/grafana/next/developers/kinds" - - // Local references - coreTSPath = "packages/grafana-schema/src/raw/%s/%s/%s_types.gen.ts" - coreGoPath = "pkg/kinds/%s" - coreCUEPath = "kinds/%s/%s_kind.cue" - - composableTSPath = "public/app/plugins/%s/%s/%s.gen.ts" - composableGoPath = "pkg/tsdb/%s/kinds/%s/types_%s_gen.go" - composableCUEPath = "public/app/plugins/%s/%s/%s.cue" -) - -func main() { - report := buildKindStateReport() - reportJSON := elsedie(json.MarshalIndent(report, "", " "))("error generating json output") - - file := codejen.NewFile(reportFileName, reportJSON, reportJenny{}) - filesystem := elsedie(file.ToFS())("error building in-memory file system") - - if _, set := os.LookupEnv("CODEGEN_VERIFY"); set { - if err := filesystem.Verify(context.Background(), ""); err != nil { - die(fmt.Errorf("generated code is out of sync with inputs:\n%s\nrun `make gen-cue` to regenerate", err)) - } - } else if err := filesystem.Write(context.Background(), ""); err != nil { - die(fmt.Errorf("error while writing generated code to disk:\n%s", err)) - } -} - -// static list of planned core kinds so that we can inject ones that -// haven't been started on yet as "planned" -var plannedCoreKinds = []string{ - "Dashboard", - "Playlist", - "Team", - "User", - "Folder", - "DataSource", - "APIKey", - "ServiceAccount", - "Thumb", - "Query", - "QueryHistory", -} - -type KindLinks struct { - Schema string - Go string - Ts string - Docs string -} - -type Kind struct { - kindsys.SomeKindProperties - Category string - Links KindLinks - GrafanaMaturityCount int - CodeOwners []string -} - -// MarshalJSON is overwritten to marshal -// kindsys.SomeKindProperties at root level. -func (k Kind) MarshalJSON() ([]byte, error) { - b, err := json.Marshal(k.SomeKindProperties) - if err != nil { - return nil, err - } - - var m map[string]any - if err = json.Unmarshal(b, &m); err != nil { - return nil, err - } - - m["category"] = k.Category - m["grafanaMaturityCount"] = k.GrafanaMaturityCount - - if len(k.CodeOwners) == 0 { - m["codeowners"] = []string{} - } else { - m["codeowners"] = k.CodeOwners - } - - m["links"] = map[string]string{} - for _, ref := range []string{"Schema", "Go", "Ts", "Docs"} { - refVal := reflect.ValueOf(k.Links).FieldByName(ref).String() - if len(refVal) > 0 { - m["links"].(map[string]string)[toCamelCase(ref)] = refVal - } else { - m["links"].(map[string]string)[toCamelCase(ref)] = "n/a" - } - } - - return json.Marshal(m) -} - -type KindStateReport struct { - Kinds map[string]Kind `json:"kinds"` - Dimensions map[string]Dimension `json:"dimensions"` -} - -func (r *KindStateReport) add(k Kind) { - kName := k.Common().MachineName - - r.Kinds[kName] = k - r.Dimensions["maturity"][k.Common().Maturity.String()].add(kName) - r.Dimensions["category"][k.Category].add(kName) -} - -type Dimension map[string]*DimensionValue - -type DimensionValue struct { - Name string `json:"name"` - Items []string `json:"items"` - Count int `json:"count"` -} - -func (dv *DimensionValue) add(s string) { - dv.Count++ - dv.Items = append(dv.Items, s) -} - -// emptyKindStateReport is used to ensure certain -// dimension values are present (even if empty) in -// the final report. -func emptyKindStateReport() *KindStateReport { - return &KindStateReport{ - Kinds: make(map[string]Kind), - Dimensions: map[string]Dimension{ - "maturity": { - "planned": emptyDimensionValue("planned"), - "merged": emptyDimensionValue("merged"), - "experimental": emptyDimensionValue("experimental"), - "stable": emptyDimensionValue("stable"), - "mature": emptyDimensionValue("mature"), - }, - "category": { - "core": emptyDimensionValue("core"), - "composable": emptyDimensionValue("composable"), - }, - }, - } -} - -func emptyDimensionValue(name string) *DimensionValue { - return &DimensionValue{ - Name: name, - Items: make([]string, 0), - Count: 0, - } -} - -func buildKindStateReport() *KindStateReport { - r := emptyKindStateReport() - b := corekind.NewBase(nil) - - groot := filepath.Join(elsedie(os.Getwd())("cannot get cwd"), "..", "..", "..") - of := elsedie(kindsysreport.NewCodeOwnersFinder(groot))("cannot parse .github/codeowners") - - seen := make(map[string]bool) - for _, k := range b.All() { - seen[k.Props().Common().Name] = true - lin := k.Lineage() - links := buildCoreLinks(lin, k.Def().Properties) - r.add(Kind{ - SomeKindProperties: k.Props(), - Category: "core", - Links: links, - GrafanaMaturityCount: grafanaMaturityAttrCount(lin.Latest().Underlying()), - CodeOwners: findCodeOwners(of, links), - }) - } - - for _, kn := range plannedCoreKinds { - if seen[kn] { - continue - } - - r.add(Kind{ - SomeKindProperties: kindsys.CoreProperties{ - CommonProperties: kindsys.CommonProperties{ - Name: kn, - PluralName: kn + "s", - MachineName: machinize(kn), - PluralMachineName: machinize(kn) + "s", - Maturity: "planned", - }, - }, - Category: "core", - }) - } - - all := kindsys.SchemaInterfaces(nil) - for _, pp := range corelist.New(nil) { - for _, si := range all { - if ck, has := pp.ComposableKinds[si.Name()]; has { - links := buildComposableLinks(pp.Properties, ck.Def().Properties) - r.add(Kind{ - SomeKindProperties: ck.Props(), - Category: "composable", - Links: links, - GrafanaMaturityCount: grafanaMaturityAttrCount(ck.Lineage().Latest().Underlying()), - CodeOwners: findCodeOwners(of, links), - }) - } else if may := si.Should(string(pp.Properties.Type)); may { - n := plugindef.DerivePascalName(pp.Properties) + si.Name() - ck := kindsys.ComposableProperties{ - SchemaInterface: si.Name(), - CommonProperties: kindsys.CommonProperties{ - Name: n, - PluralName: n + "s", - MachineName: machinize(n), - PluralMachineName: machinize(n) + "s", - LineageIsGroup: si.IsGroup(), - Maturity: "planned", - }, - } - r.add(Kind{ - SomeKindProperties: ck, - Category: "composable", - }) - } - } - } - - for _, d := range r.Dimensions { - for _, dv := range d { - sort.Strings(dv.Items) - } - } - - return r -} - -func buildCoreLinks(lin thema.Lineage, cp kindsys.CoreProperties) KindLinks { - const category = "core" - vpath := fmt.Sprintf("v%v", lin.Latest().Version()[0]) - if cp.Maturity.Less(kindsys.MaturityStable) { - vpath = "x" - } - - return KindLinks{ - Schema: elsedie(url.JoinPath(repoBaseURL, fmt.Sprintf(coreCUEPath, cp.MachineName, cp.MachineName)))("cannot build schema link"), - Go: elsedie(url.JoinPath(repoBaseURL, fmt.Sprintf(coreGoPath, cp.MachineName)))("cannot build go link"), - Ts: elsedie(url.JoinPath(repoBaseURL, fmt.Sprintf(coreTSPath, cp.MachineName, vpath, cp.MachineName)))("cannot build ts link"), - Docs: elsedie(url.JoinPath(docsBaseURL, category, cp.MachineName, "schema-reference"))("cannot build docs link"), - } -} - -// used to map names for those plugins that aren't following -// naming conventions, like 'annonlist' which comes from "Annotations list". -var irregularPluginNames = map[string]string{ - // Panel - "alertgroups": "alertGroups", - "annotationslist": "annolist", - "dashboardlist": "dashlist", - "nodegraph": "nodeGraph", - "statetimeline": "state-timeline", - "statushistory": "status-history", - "tableold": "table-old", - // Datasource - "googlecloudmonitoring": "cloud-monitoring", - "azuremonitor": "grafana-azure-monitor-datasource", - "microsoftsqlserver": "mssql", - "postgresql": "postgres", - "testdata": "grafana-testdata-datasource", -} - -func buildComposableLinks(pp plugindef.PluginDef, cp kindsys.ComposableProperties) KindLinks { - const category = "composable" - schemaInterface := strings.ToLower(cp.SchemaInterface) - - pName := strings.Replace(cp.MachineName, schemaInterface, "", 1) - if irr, ok := irregularPluginNames[pName]; ok { - pName = irr - } - - var goLink string - if pp.Backend != nil && *pp.Backend { - goLink = elsedie(url.JoinPath(repoBaseURL, fmt.Sprintf(composableGoPath, pName, schemaInterface, schemaInterface)))("cannot build go link") - } - - return KindLinks{ - Schema: elsedie(url.JoinPath(repoBaseURL, fmt.Sprintf(composableCUEPath, string(pp.Type), pName, schemaInterface)))("cannot build schema link"), - Go: goLink, - Ts: elsedie(url.JoinPath(repoBaseURL, fmt.Sprintf(composableTSPath, string(pp.Type), pName, schemaInterface)))("cannot build ts link"), - Docs: elsedie(url.JoinPath(docsBaseURL, category, cp.MachineName, "schema-reference"))("cannot build docs link"), - } -} - -func grafanaMaturityAttrCount(sch cue.Value) int { - const attr = "grafanamaturity" - aw := new(kindsysreport.AttributeWalker) - return aw.Count(sch, attr)[attr] -} - -func findCodeOwners(of kindsysreport.CodeOwnersFinder, links KindLinks) []string { - owners := elsedie(of.FindFor([]string{ - toLocalPath(links.Schema), - toLocalPath(links.Go), - toLocalPath(links.Ts), - }...))("cannot find code owners") - - sort.Strings(owners) - return owners -} - -func machinize(s string) string { - return strings.Map(func(r rune) rune { - switch { - case r >= 'a' && r <= 'z': - fallthrough - case r >= '0' && r <= '9': - fallthrough - case r == '_': - return r - case r >= 'A' && r <= 'Z': - return r + 32 - case r == '-': - return '_' - default: - return -1 - } - }, s) -} - -func toCamelCase(s string) string { - return strings.ToLower(string(s[0])) + s[1:] -} - -func toLocalPath(s string) string { - return strings.Replace(s, repoBaseURL+"/", "", 1) -} - -type reportJenny struct{} - -func (reportJenny) JennyName() string { - return "ReportJenny" -} - -func elsedie[T any](t T, err error) func(msg string) T { - if err != nil { - return func(msg string) T { - fmt.Fprintf(os.Stderr, "%s: %s\n", msg, err) - os.Exit(1) - return t - } - } - - return func(msg string) T { - return t - } -} - -func die(err error) { - fmt.Fprint(os.Stderr, err, "\n") - os.Exit(1) -} diff --git a/pkg/kindsysreport/codegen/report.json b/pkg/kindsysreport/codegen/report.json deleted file mode 100644 index a44e34a947..0000000000 --- a/pkg/kindsysreport/codegen/report.json +++ /dev/null @@ -1,2338 +0,0 @@ -{ - "kinds": { - "accesspolicy": { - "category": "core", - "codeowners": [ - "grafana/grafana-as-code", - "grafana/grafana-frontend-platform", - "grafana/plugins-platform-frontend" - ], - "crd": { - "dummySchema": false, - "group": "accesspolicy.core.grafana.com", - "scope": "Namespaced" - }, - "currentVersion": [ - 0, - 0 - ], - "description": "Access rules for a scope+role. NOTE there is a unique constraint on role+scope", - "grafanaMaturityCount": 0, - "lineageIsGroup": false, - "links": { - "docs": "https://grafana.com/docs/grafana/next/developers/kinds/core/accesspolicy/schema-reference", - "go": "https://github.com/grafana/grafana/tree/main/pkg/kinds/accesspolicy", - "schema": "https://github.com/grafana/grafana/tree/main/kinds/accesspolicy/accesspolicy_kind.cue", - "ts": "https://github.com/grafana/grafana/tree/main/packages/grafana-schema/src/raw/accesspolicy/x/accesspolicy_types.gen.ts" - }, - "machineName": "accesspolicy", - "maturity": "merged", - "name": "AccessPolicy", - "pluralMachineName": "accesspolicies", - "pluralName": "AccessPolicies" - }, - "alertgroupspanelcfg": { - "category": "composable", - "codeowners": [ - "grafana/alerting-frontend" - ], - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": true, - "links": { - "docs": "https://grafana.com/docs/grafana/next/developers/kinds/composable/alertgroupspanelcfg/schema-reference", - "go": "n/a", - "schema": "https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/alertGroups/panelcfg.cue", - "ts": "https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/alertGroups/panelcfg.gen.ts" - }, - "machineName": "alertgroupspanelcfg", - "maturity": "merged", - "name": "AlertGroupsPanelCfg", - "pluralMachineName": "alertgroupspanelcfgs", - "pluralName": "AlertGroupsPanelCfgs", - "schemaInterface": "PanelCfg" - }, - "alertlistpanelcfg": { - "category": "composable", - "codeowners": [], - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": true, - "links": { - "docs": "n/a", - "go": "n/a", - "schema": "n/a", - "ts": "n/a" - }, - "machineName": "alertlistpanelcfg", - "maturity": "planned", - "name": "AlertListPanelCfg", - "pluralMachineName": "alertlistpanelcfgs", - "pluralName": "AlertListPanelCfgs", - "schemaInterface": "PanelCfg" - }, - "alertmanagerdataquery": { - "category": "composable", - "codeowners": [], - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": false, - "links": { - "docs": "n/a", - "go": "n/a", - "schema": "n/a", - "ts": "n/a" - }, - "machineName": "alertmanagerdataquery", - "maturity": "planned", - "name": "AlertmanagerDataQuery", - "pluralMachineName": "alertmanagerdataquerys", - "pluralName": "AlertmanagerDataQuerys", - "schemaInterface": "DataQuery" - }, - "alertmanagerdatasourcecfg": { - "category": "composable", - "codeowners": [], - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": true, - "links": { - "docs": "n/a", - "go": "n/a", - "schema": "n/a", - "ts": "n/a" - }, - "machineName": "alertmanagerdatasourcecfg", - "maturity": "planned", - "name": "AlertmanagerDataSourceCfg", - "pluralMachineName": "alertmanagerdatasourcecfgs", - "pluralName": "AlertmanagerDataSourceCfgs", - "schemaInterface": "DataSourceCfg" - }, - "annotationslistpanelcfg": { - "category": "composable", - "codeowners": [ - "grafana/grafana-frontend-platform" - ], - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": true, - "links": { - "docs": "https://grafana.com/docs/grafana/next/developers/kinds/composable/annotationslistpanelcfg/schema-reference", - "go": "n/a", - "schema": "https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/annolist/panelcfg.cue", - "ts": "https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/annolist/panelcfg.gen.ts" - }, - "machineName": "annotationslistpanelcfg", - "maturity": "experimental", - "name": "AnnotationsListPanelCfg", - "pluralMachineName": "annotationslistpanelcfgs", - "pluralName": "AnnotationsListPanelCfgs", - "schemaInterface": "PanelCfg" - }, - "apikey": { - "category": "core", - "codeowners": [], - "crd": { - "dummySchema": false, - "group": "", - "scope": "" - }, - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": false, - "links": { - "docs": "n/a", - "go": "n/a", - "schema": "n/a", - "ts": "n/a" - }, - "machineName": "apikey", - "maturity": "planned", - "name": "APIKey", - "pluralMachineName": "apikeys", - "pluralName": "APIKeys" - }, - "azuremonitordataquery": { - "category": "composable", - "codeowners": [], - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": false, - "links": { - "docs": "https://grafana.com/docs/grafana/next/developers/kinds/composable/azuremonitordataquery/schema-reference", - "go": "https://github.com/grafana/grafana/tree/main/pkg/tsdb/grafana-azure-monitor-datasource/kinds/dataquery/types_dataquery_gen.go", - "schema": "https://github.com/grafana/grafana/tree/main/public/app/plugins/datasource/grafana-azure-monitor-datasource/dataquery.cue", - "ts": "https://github.com/grafana/grafana/tree/main/public/app/plugins/datasource/grafana-azure-monitor-datasource/dataquery.gen.ts" - }, - "machineName": "azuremonitordataquery", - "maturity": "merged", - "name": "AzureMonitorDataQuery", - "pluralMachineName": "azuremonitordataquerys", - "pluralName": "AzureMonitorDataQuerys", - "schemaInterface": "DataQuery" - }, - "azuremonitordatasourcecfg": { - "category": "composable", - "codeowners": [], - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": true, - "links": { - "docs": "n/a", - "go": "n/a", - "schema": "n/a", - "ts": "n/a" - }, - "machineName": "azuremonitordatasourcecfg", - "maturity": "planned", - "name": "AzureMonitorDataSourceCfg", - "pluralMachineName": "azuremonitordatasourcecfgs", - "pluralName": "AzureMonitorDataSourceCfgs", - "schemaInterface": "DataSourceCfg" - }, - "barchartpanelcfg": { - "category": "composable", - "codeowners": [ - "grafana/dataviz-squad" - ], - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": true, - "links": { - "docs": "https://grafana.com/docs/grafana/next/developers/kinds/composable/barchartpanelcfg/schema-reference", - "go": "n/a", - "schema": "https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/barchart/panelcfg.cue", - "ts": "https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/barchart/panelcfg.gen.ts" - }, - "machineName": "barchartpanelcfg", - "maturity": "experimental", - "name": "BarChartPanelCfg", - "pluralMachineName": "barchartpanelcfgs", - "pluralName": "BarChartPanelCfgs", - "schemaInterface": "PanelCfg" - }, - "bargaugepanelcfg": { - "category": "composable", - "codeowners": [ - "grafana/dataviz-squad" - ], - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": true, - "links": { - "docs": "https://grafana.com/docs/grafana/next/developers/kinds/composable/bargaugepanelcfg/schema-reference", - "go": "n/a", - "schema": "https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/bargauge/panelcfg.cue", - "ts": "https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/bargauge/panelcfg.gen.ts" - }, - "machineName": "bargaugepanelcfg", - "maturity": "experimental", - "name": "BarGaugePanelCfg", - "pluralMachineName": "bargaugepanelcfgs", - "pluralName": "BarGaugePanelCfgs", - "schemaInterface": "PanelCfg" - }, - "candlestickpanelcfg": { - "category": "composable", - "codeowners": [ - "grafana/dataviz-squad" - ], - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": true, - "links": { - "docs": "https://grafana.com/docs/grafana/next/developers/kinds/composable/candlestickpanelcfg/schema-reference", - "go": "n/a", - "schema": "https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/candlestick/panelcfg.cue", - "ts": "https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/candlestick/panelcfg.gen.ts" - }, - "machineName": "candlestickpanelcfg", - "maturity": "experimental", - "name": "CandlestickPanelCfg", - "pluralMachineName": "candlestickpanelcfgs", - "pluralName": "CandlestickPanelCfgs", - "schemaInterface": "PanelCfg" - }, - "canvaspanelcfg": { - "category": "composable", - "codeowners": [ - "grafana/dataviz-squad" - ], - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": true, - "links": { - "docs": "https://grafana.com/docs/grafana/next/developers/kinds/composable/canvaspanelcfg/schema-reference", - "go": "n/a", - "schema": "https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/canvas/panelcfg.cue", - "ts": "https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/canvas/panelcfg.gen.ts" - }, - "machineName": "canvaspanelcfg", - "maturity": "experimental", - "name": "CanvasPanelCfg", - "pluralMachineName": "canvaspanelcfgs", - "pluralName": "CanvasPanelCfgs", - "schemaInterface": "PanelCfg" - }, - "cloudwatchdataquery": { - "category": "composable", - "codeowners": [ - "grafana/aws-datasources" - ], - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": false, - "links": { - "docs": "https://grafana.com/docs/grafana/next/developers/kinds/composable/cloudwatchdataquery/schema-reference", - "go": "https://github.com/grafana/grafana/tree/main/pkg/tsdb/cloudwatch/kinds/dataquery/types_dataquery_gen.go", - "schema": "https://github.com/grafana/grafana/tree/main/public/app/plugins/datasource/cloudwatch/dataquery.cue", - "ts": "https://github.com/grafana/grafana/tree/main/public/app/plugins/datasource/cloudwatch/dataquery.gen.ts" - }, - "machineName": "cloudwatchdataquery", - "maturity": "experimental", - "name": "CloudWatchDataQuery", - "pluralMachineName": "cloudwatchdataquerys", - "pluralName": "CloudWatchDataQuerys", - "schemaInterface": "DataQuery" - }, - "cloudwatchdatasourcecfg": { - "category": "composable", - "codeowners": [], - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": true, - "links": { - "docs": "n/a", - "go": "n/a", - "schema": "n/a", - "ts": "n/a" - }, - "machineName": "cloudwatchdatasourcecfg", - "maturity": "planned", - "name": "CloudWatchDataSourceCfg", - "pluralMachineName": "cloudwatchdatasourcecfgs", - "pluralName": "CloudWatchDataSourceCfgs", - "schemaInterface": "DataSourceCfg" - }, - "dashboard": { - "category": "core", - "codeowners": [ - "grafana/grafana-as-code", - "grafana/grafana-frontend-platform", - "grafana/plugins-platform-frontend" - ], - "crd": { - "dummySchema": true, - "group": "dashboard.core.grafana.com", - "scope": "Namespaced" - }, - "currentVersion": [ - 0, - 0 - ], - "description": "A Grafana dashboard.", - "grafanaMaturityCount": 105, - "lineageIsGroup": false, - "links": { - "docs": "https://grafana.com/docs/grafana/next/developers/kinds/core/dashboard/schema-reference", - "go": "https://github.com/grafana/grafana/tree/main/pkg/kinds/dashboard", - "schema": "https://github.com/grafana/grafana/tree/main/kinds/dashboard/dashboard_kind.cue", - "ts": "https://github.com/grafana/grafana/tree/main/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts" - }, - "machineName": "dashboard", - "maturity": "experimental", - "name": "Dashboard", - "pluralMachineName": "dashboards", - "pluralName": "Dashboards" - }, - "dashboarddataquery": { - "category": "composable", - "codeowners": [], - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": false, - "links": { - "docs": "n/a", - "go": "n/a", - "schema": "n/a", - "ts": "n/a" - }, - "machineName": "dashboarddataquery", - "maturity": "planned", - "name": "DashboardDataQuery", - "pluralMachineName": "dashboarddataquerys", - "pluralName": "DashboardDataQuerys", - "schemaInterface": "DataQuery" - }, - "dashboarddatasourcecfg": { - "category": "composable", - "codeowners": [], - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": true, - "links": { - "docs": "n/a", - "go": "n/a", - "schema": "n/a", - "ts": "n/a" - }, - "machineName": "dashboarddatasourcecfg", - "maturity": "planned", - "name": "DashboardDataSourceCfg", - "pluralMachineName": "dashboarddatasourcecfgs", - "pluralName": "DashboardDataSourceCfgs", - "schemaInterface": "DataSourceCfg" - }, - "dashboardlistpanelcfg": { - "category": "composable", - "codeowners": [ - "grafana/grafana-frontend-platform" - ], - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": true, - "links": { - "docs": "https://grafana.com/docs/grafana/next/developers/kinds/composable/dashboardlistpanelcfg/schema-reference", - "go": "n/a", - "schema": "https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/dashlist/panelcfg.cue", - "ts": "https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/dashlist/panelcfg.gen.ts" - }, - "machineName": "dashboardlistpanelcfg", - "maturity": "experimental", - "name": "DashboardListPanelCfg", - "pluralMachineName": "dashboardlistpanelcfgs", - "pluralName": "DashboardListPanelCfgs", - "schemaInterface": "PanelCfg" - }, - "datagridpanelcfg": { - "category": "composable", - "codeowners": [ - "grafana/dataviz-squad" - ], - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": true, - "links": { - "docs": "https://grafana.com/docs/grafana/next/developers/kinds/composable/datagridpanelcfg/schema-reference", - "go": "n/a", - "schema": "https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/datagrid/panelcfg.cue", - "ts": "https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/datagrid/panelcfg.gen.ts" - }, - "machineName": "datagridpanelcfg", - "maturity": "experimental", - "name": "DatagridPanelCfg", - "pluralMachineName": "datagridpanelcfgs", - "pluralName": "DatagridPanelCfgs", - "schemaInterface": "PanelCfg" - }, - "datasource": { - "category": "core", - "codeowners": [], - "crd": { - "dummySchema": false, - "group": "", - "scope": "" - }, - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": false, - "links": { - "docs": "n/a", - "go": "n/a", - "schema": "n/a", - "ts": "n/a" - }, - "machineName": "datasource", - "maturity": "planned", - "name": "DataSource", - "pluralMachineName": "datasources", - "pluralName": "DataSources" - }, - "debugpanelcfg": { - "category": "composable", - "codeowners": [ - "ryantxu" - ], - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": true, - "links": { - "docs": "https://grafana.com/docs/grafana/next/developers/kinds/composable/debugpanelcfg/schema-reference", - "go": "n/a", - "schema": "https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/debug/panelcfg.cue", - "ts": "https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/debug/panelcfg.gen.ts" - }, - "machineName": "debugpanelcfg", - "maturity": "experimental", - "name": "DebugPanelCfg", - "pluralMachineName": "debugpanelcfgs", - "pluralName": "DebugPanelCfgs", - "schemaInterface": "PanelCfg" - }, - "elasticsearchdataquery": { - "category": "composable", - "codeowners": [ - "grafana/observability-logs" - ], - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": false, - "links": { - "docs": "https://grafana.com/docs/grafana/next/developers/kinds/composable/elasticsearchdataquery/schema-reference", - "go": "https://github.com/grafana/grafana/tree/main/pkg/tsdb/elasticsearch/kinds/dataquery/types_dataquery_gen.go", - "schema": "https://github.com/grafana/grafana/tree/main/public/app/plugins/datasource/elasticsearch/dataquery.cue", - "ts": "https://github.com/grafana/grafana/tree/main/public/app/plugins/datasource/elasticsearch/dataquery.gen.ts" - }, - "machineName": "elasticsearchdataquery", - "maturity": "experimental", - "name": "ElasticsearchDataQuery", - "pluralMachineName": "elasticsearchdataquerys", - "pluralName": "ElasticsearchDataQuerys", - "schemaInterface": "DataQuery" - }, - "elasticsearchdatasourcecfg": { - "category": "composable", - "codeowners": [], - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": true, - "links": { - "docs": "n/a", - "go": "n/a", - "schema": "n/a", - "ts": "n/a" - }, - "machineName": "elasticsearchdatasourcecfg", - "maturity": "planned", - "name": "ElasticsearchDataSourceCfg", - "pluralMachineName": "elasticsearchdatasourcecfgs", - "pluralName": "ElasticsearchDataSourceCfgs", - "schemaInterface": "DataSourceCfg" - }, - "flamegraphpanelcfg": { - "category": "composable", - "codeowners": [], - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": true, - "links": { - "docs": "n/a", - "go": "n/a", - "schema": "n/a", - "ts": "n/a" - }, - "machineName": "flamegraphpanelcfg", - "maturity": "planned", - "name": "FlameGraphPanelCfg", - "pluralMachineName": "flamegraphpanelcfgs", - "pluralName": "FlameGraphPanelCfgs", - "schemaInterface": "PanelCfg" - }, - "folder": { - "category": "core", - "codeowners": [], - "crd": { - "dummySchema": false, - "group": "", - "scope": "" - }, - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": false, - "links": { - "docs": "n/a", - "go": "n/a", - "schema": "n/a", - "ts": "n/a" - }, - "machineName": "folder", - "maturity": "planned", - "name": "Folder", - "pluralMachineName": "folders", - "pluralName": "Folders" - }, - "gaugepanelcfg": { - "category": "composable", - "codeowners": [ - "grafana/dataviz-squad" - ], - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": true, - "links": { - "docs": "https://grafana.com/docs/grafana/next/developers/kinds/composable/gaugepanelcfg/schema-reference", - "go": "n/a", - "schema": "https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/gauge/panelcfg.cue", - "ts": "https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/gauge/panelcfg.gen.ts" - }, - "machineName": "gaugepanelcfg", - "maturity": "experimental", - "name": "GaugePanelCfg", - "pluralMachineName": "gaugepanelcfgs", - "pluralName": "GaugePanelCfgs", - "schemaInterface": "PanelCfg" - }, - "geomappanelcfg": { - "category": "composable", - "codeowners": [ - "grafana/dataviz-squad" - ], - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": true, - "links": { - "docs": "https://grafana.com/docs/grafana/next/developers/kinds/composable/geomappanelcfg/schema-reference", - "go": "n/a", - "schema": "https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/geomap/panelcfg.cue", - "ts": "https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/geomap/panelcfg.gen.ts" - }, - "machineName": "geomappanelcfg", - "maturity": "experimental", - "name": "GeomapPanelCfg", - "pluralMachineName": "geomappanelcfgs", - "pluralName": "GeomapPanelCfgs", - "schemaInterface": "PanelCfg" - }, - "gettingstartedpanelcfg": { - "category": "composable", - "codeowners": [], - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": true, - "links": { - "docs": "n/a", - "go": "n/a", - "schema": "n/a", - "ts": "n/a" - }, - "machineName": "gettingstartedpanelcfg", - "maturity": "planned", - "name": "GettingStartedPanelCfg", - "pluralMachineName": "gettingstartedpanelcfgs", - "pluralName": "GettingStartedPanelCfgs", - "schemaInterface": "PanelCfg" - }, - "googlecloudmonitoringdataquery": { - "category": "composable", - "codeowners": [ - "grafana/partner-datasources" - ], - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": false, - "links": { - "docs": "https://grafana.com/docs/grafana/next/developers/kinds/composable/googlecloudmonitoringdataquery/schema-reference", - "go": "https://github.com/grafana/grafana/tree/main/pkg/tsdb/cloud-monitoring/kinds/dataquery/types_dataquery_gen.go", - "schema": "https://github.com/grafana/grafana/tree/main/public/app/plugins/datasource/cloud-monitoring/dataquery.cue", - "ts": "https://github.com/grafana/grafana/tree/main/public/app/plugins/datasource/cloud-monitoring/dataquery.gen.ts" - }, - "machineName": "googlecloudmonitoringdataquery", - "maturity": "merged", - "name": "GoogleCloudMonitoringDataQuery", - "pluralMachineName": "googlecloudmonitoringdataquerys", - "pluralName": "GoogleCloudMonitoringDataQuerys", - "schemaInterface": "DataQuery" - }, - "googlecloudmonitoringdatasourcecfg": { - "category": "composable", - "codeowners": [], - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": true, - "links": { - "docs": "n/a", - "go": "n/a", - "schema": "n/a", - "ts": "n/a" - }, - "machineName": "googlecloudmonitoringdatasourcecfg", - "maturity": "planned", - "name": "GoogleCloudMonitoringDataSourceCfg", - "pluralMachineName": "googlecloudmonitoringdatasourcecfgs", - "pluralName": "GoogleCloudMonitoringDataSourceCfgs", - "schemaInterface": "DataSourceCfg" - }, - "grafanadataquery": { - "category": "composable", - "codeowners": [], - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": false, - "links": { - "docs": "n/a", - "go": "n/a", - "schema": "n/a", - "ts": "n/a" - }, - "machineName": "grafanadataquery", - "maturity": "planned", - "name": "GrafanaDataQuery", - "pluralMachineName": "grafanadataquerys", - "pluralName": "GrafanaDataQuerys", - "schemaInterface": "DataQuery" - }, - "grafanadatasourcecfg": { - "category": "composable", - "codeowners": [], - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": true, - "links": { - "docs": "n/a", - "go": "n/a", - "schema": "n/a", - "ts": "n/a" - }, - "machineName": "grafanadatasourcecfg", - "maturity": "planned", - "name": "GrafanaDataSourceCfg", - "pluralMachineName": "grafanadatasourcecfgs", - "pluralName": "GrafanaDataSourceCfgs", - "schemaInterface": "DataSourceCfg" - }, - "grafanapyroscopedataquery": { - "category": "composable", - "codeowners": [], - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": false, - "links": { - "docs": "https://grafana.com/docs/grafana/next/developers/kinds/composable/grafanapyroscopedataquery/schema-reference", - "go": "https://github.com/grafana/grafana/tree/main/pkg/tsdb/grafanapyroscope/kinds/dataquery/types_dataquery_gen.go", - "schema": "https://github.com/grafana/grafana/tree/main/public/app/plugins/datasource/grafanapyroscope/dataquery.cue", - "ts": "https://github.com/grafana/grafana/tree/main/public/app/plugins/datasource/grafanapyroscope/dataquery.gen.ts" - }, - "machineName": "grafanapyroscopedataquery", - "maturity": "experimental", - "name": "GrafanaPyroscopeDataQuery", - "pluralMachineName": "grafanapyroscopedataquerys", - "pluralName": "GrafanaPyroscopeDataQuerys", - "schemaInterface": "DataQuery" - }, - "grafanapyroscopedatasourcecfg": { - "category": "composable", - "codeowners": [], - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": true, - "links": { - "docs": "n/a", - "go": "n/a", - "schema": "n/a", - "ts": "n/a" - }, - "machineName": "grafanapyroscopedatasourcecfg", - "maturity": "planned", - "name": "GrafanaPyroscopeDataSourceCfg", - "pluralMachineName": "grafanapyroscopedatasourcecfgs", - "pluralName": "GrafanaPyroscopeDataSourceCfgs", - "schemaInterface": "DataSourceCfg" - }, - "graphitedataquery": { - "category": "composable", - "codeowners": [], - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": false, - "links": { - "docs": "n/a", - "go": "n/a", - "schema": "n/a", - "ts": "n/a" - }, - "machineName": "graphitedataquery", - "maturity": "planned", - "name": "GraphiteDataQuery", - "pluralMachineName": "graphitedataquerys", - "pluralName": "GraphiteDataQuerys", - "schemaInterface": "DataQuery" - }, - "graphitedatasourcecfg": { - "category": "composable", - "codeowners": [], - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": true, - "links": { - "docs": "n/a", - "go": "n/a", - "schema": "n/a", - "ts": "n/a" - }, - "machineName": "graphitedatasourcecfg", - "maturity": "planned", - "name": "GraphiteDataSourceCfg", - "pluralMachineName": "graphitedatasourcecfgs", - "pluralName": "GraphiteDataSourceCfgs", - "schemaInterface": "DataSourceCfg" - }, - "grapholdpanelcfg": { - "category": "composable", - "codeowners": [], - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": true, - "links": { - "docs": "n/a", - "go": "n/a", - "schema": "n/a", - "ts": "n/a" - }, - "machineName": "grapholdpanelcfg", - "maturity": "planned", - "name": "GraphOldPanelCfg", - "pluralMachineName": "grapholdpanelcfgs", - "pluralName": "GraphOldPanelCfgs", - "schemaInterface": "PanelCfg" - }, - "heatmappanelcfg": { - "category": "composable", - "codeowners": [ - "grafana/dataviz-squad" - ], - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": true, - "links": { - "docs": "https://grafana.com/docs/grafana/next/developers/kinds/composable/heatmappanelcfg/schema-reference", - "go": "n/a", - "schema": "https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/heatmap/panelcfg.cue", - "ts": "https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/heatmap/panelcfg.gen.ts" - }, - "machineName": "heatmappanelcfg", - "maturity": "merged", - "name": "HeatmapPanelCfg", - "pluralMachineName": "heatmappanelcfgs", - "pluralName": "HeatmapPanelCfgs", - "schemaInterface": "PanelCfg" - }, - "histogrampanelcfg": { - "category": "composable", - "codeowners": [ - "grafana/dataviz-squad" - ], - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": true, - "links": { - "docs": "https://grafana.com/docs/grafana/next/developers/kinds/composable/histogrampanelcfg/schema-reference", - "go": "n/a", - "schema": "https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/histogram/panelcfg.cue", - "ts": "https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/histogram/panelcfg.gen.ts" - }, - "machineName": "histogrampanelcfg", - "maturity": "experimental", - "name": "HistogramPanelCfg", - "pluralMachineName": "histogrampanelcfgs", - "pluralName": "HistogramPanelCfgs", - "schemaInterface": "PanelCfg" - }, - "jaegerdataquery": { - "category": "composable", - "codeowners": [], - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": false, - "links": { - "docs": "n/a", - "go": "n/a", - "schema": "n/a", - "ts": "n/a" - }, - "machineName": "jaegerdataquery", - "maturity": "planned", - "name": "JaegerDataQuery", - "pluralMachineName": "jaegerdataquerys", - "pluralName": "JaegerDataQuerys", - "schemaInterface": "DataQuery" - }, - "jaegerdatasourcecfg": { - "category": "composable", - "codeowners": [], - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": true, - "links": { - "docs": "n/a", - "go": "n/a", - "schema": "n/a", - "ts": "n/a" - }, - "machineName": "jaegerdatasourcecfg", - "maturity": "planned", - "name": "JaegerDataSourceCfg", - "pluralMachineName": "jaegerdatasourcecfgs", - "pluralName": "JaegerDataSourceCfgs", - "schemaInterface": "DataSourceCfg" - }, - "librarypanel": { - "category": "core", - "codeowners": [ - "grafana/grafana-as-code", - "grafana/grafana-frontend-platform", - "grafana/plugins-platform-frontend" - ], - "crd": { - "dummySchema": false, - "group": "librarypanel.core.grafana.com", - "scope": "Namespaced" - }, - "currentVersion": [ - 0, - 0 - ], - "description": "A standalone panel", - "grafanaMaturityCount": 19, - "lineageIsGroup": false, - "links": { - "docs": "https://grafana.com/docs/grafana/next/developers/kinds/core/librarypanel/schema-reference", - "go": "https://github.com/grafana/grafana/tree/main/pkg/kinds/librarypanel", - "schema": "https://github.com/grafana/grafana/tree/main/kinds/librarypanel/librarypanel_kind.cue", - "ts": "https://github.com/grafana/grafana/tree/main/packages/grafana-schema/src/raw/librarypanel/x/librarypanel_types.gen.ts" - }, - "machineName": "librarypanel", - "maturity": "experimental", - "name": "LibraryPanel", - "pluralMachineName": "librarypanels", - "pluralName": "LibraryPanels" - }, - "livepanelcfg": { - "category": "composable", - "codeowners": [], - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": true, - "links": { - "docs": "n/a", - "go": "n/a", - "schema": "n/a", - "ts": "n/a" - }, - "machineName": "livepanelcfg", - "maturity": "planned", - "name": "LivePanelCfg", - "pluralMachineName": "livepanelcfgs", - "pluralName": "LivePanelCfgs", - "schemaInterface": "PanelCfg" - }, - "logspanelcfg": { - "category": "composable", - "codeowners": [ - "grafana/observability-logs" - ], - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": true, - "links": { - "docs": "https://grafana.com/docs/grafana/next/developers/kinds/composable/logspanelcfg/schema-reference", - "go": "n/a", - "schema": "https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/logs/panelcfg.cue", - "ts": "https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/logs/panelcfg.gen.ts" - }, - "machineName": "logspanelcfg", - "maturity": "experimental", - "name": "LogsPanelCfg", - "pluralMachineName": "logspanelcfgs", - "pluralName": "LogsPanelCfgs", - "schemaInterface": "PanelCfg" - }, - "lokidataquery": { - "category": "composable", - "codeowners": [ - "grafana/observability-logs" - ], - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": false, - "links": { - "docs": "https://grafana.com/docs/grafana/next/developers/kinds/composable/lokidataquery/schema-reference", - "go": "https://github.com/grafana/grafana/tree/main/pkg/tsdb/loki/kinds/dataquery/types_dataquery_gen.go", - "schema": "https://github.com/grafana/grafana/tree/main/public/app/plugins/datasource/loki/dataquery.cue", - "ts": "https://github.com/grafana/grafana/tree/main/public/app/plugins/datasource/loki/dataquery.gen.ts" - }, - "machineName": "lokidataquery", - "maturity": "experimental", - "name": "LokiDataQuery", - "pluralMachineName": "lokidataquerys", - "pluralName": "LokiDataQuerys", - "schemaInterface": "DataQuery" - }, - "lokidatasourcecfg": { - "category": "composable", - "codeowners": [], - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": true, - "links": { - "docs": "n/a", - "go": "n/a", - "schema": "n/a", - "ts": "n/a" - }, - "machineName": "lokidatasourcecfg", - "maturity": "planned", - "name": "LokiDataSourceCfg", - "pluralMachineName": "lokidatasourcecfgs", - "pluralName": "LokiDataSourceCfgs", - "schemaInterface": "DataSourceCfg" - }, - "microsoftsqlserverdataquery": { - "category": "composable", - "codeowners": [], - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": false, - "links": { - "docs": "n/a", - "go": "n/a", - "schema": "n/a", - "ts": "n/a" - }, - "machineName": "microsoftsqlserverdataquery", - "maturity": "planned", - "name": "MicrosoftSQLServerDataQuery", - "pluralMachineName": "microsoftsqlserverdataquerys", - "pluralName": "MicrosoftSQLServerDataQuerys", - "schemaInterface": "DataQuery" - }, - "microsoftsqlserverdatasourcecfg": { - "category": "composable", - "codeowners": [], - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": true, - "links": { - "docs": "n/a", - "go": "n/a", - "schema": "n/a", - "ts": "n/a" - }, - "machineName": "microsoftsqlserverdatasourcecfg", - "maturity": "planned", - "name": "MicrosoftSQLServerDataSourceCfg", - "pluralMachineName": "microsoftsqlserverdatasourcecfgs", - "pluralName": "MicrosoftSQLServerDataSourceCfgs", - "schemaInterface": "DataSourceCfg" - }, - "mysqldataquery": { - "category": "composable", - "codeowners": [], - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": false, - "links": { - "docs": "n/a", - "go": "n/a", - "schema": "n/a", - "ts": "n/a" - }, - "machineName": "mysqldataquery", - "maturity": "planned", - "name": "MySQLDataQuery", - "pluralMachineName": "mysqldataquerys", - "pluralName": "MySQLDataQuerys", - "schemaInterface": "DataQuery" - }, - "mysqldatasourcecfg": { - "category": "composable", - "codeowners": [], - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": true, - "links": { - "docs": "n/a", - "go": "n/a", - "schema": "n/a", - "ts": "n/a" - }, - "machineName": "mysqldatasourcecfg", - "maturity": "planned", - "name": "MySQLDataSourceCfg", - "pluralMachineName": "mysqldatasourcecfgs", - "pluralName": "MySQLDataSourceCfgs", - "schemaInterface": "DataSourceCfg" - }, - "newspanelcfg": { - "category": "composable", - "codeowners": [ - "grafana/grafana-frontend-platform" - ], - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": true, - "links": { - "docs": "https://grafana.com/docs/grafana/next/developers/kinds/composable/newspanelcfg/schema-reference", - "go": "n/a", - "schema": "https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/news/panelcfg.cue", - "ts": "https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/news/panelcfg.gen.ts" - }, - "machineName": "newspanelcfg", - "maturity": "experimental", - "name": "NewsPanelCfg", - "pluralMachineName": "newspanelcfgs", - "pluralName": "NewsPanelCfgs", - "schemaInterface": "PanelCfg" - }, - "nodegraphpanelcfg": { - "category": "composable", - "codeowners": [ - "grafana/app-o11y-visualizations", - "grafana/observability-traces-and-profiling" - ], - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": true, - "links": { - "docs": "https://grafana.com/docs/grafana/next/developers/kinds/composable/nodegraphpanelcfg/schema-reference", - "go": "n/a", - "schema": "https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/nodeGraph/panelcfg.cue", - "ts": "https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/nodeGraph/panelcfg.gen.ts" - }, - "machineName": "nodegraphpanelcfg", - "maturity": "experimental", - "name": "NodeGraphPanelCfg", - "pluralMachineName": "nodegraphpanelcfgs", - "pluralName": "NodeGraphPanelCfgs", - "schemaInterface": "PanelCfg" - }, - "parcadataquery": { - "category": "composable", - "codeowners": [ - "grafana/observability-traces-and-profiling" - ], - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": false, - "links": { - "docs": "https://grafana.com/docs/grafana/next/developers/kinds/composable/parcadataquery/schema-reference", - "go": "https://github.com/grafana/grafana/tree/main/pkg/tsdb/parca/kinds/dataquery/types_dataquery_gen.go", - "schema": "https://github.com/grafana/grafana/tree/main/public/app/plugins/datasource/parca/dataquery.cue", - "ts": "https://github.com/grafana/grafana/tree/main/public/app/plugins/datasource/parca/dataquery.gen.ts" - }, - "machineName": "parcadataquery", - "maturity": "experimental", - "name": "ParcaDataQuery", - "pluralMachineName": "parcadataquerys", - "pluralName": "ParcaDataQuerys", - "schemaInterface": "DataQuery" - }, - "parcadatasourcecfg": { - "category": "composable", - "codeowners": [], - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": true, - "links": { - "docs": "n/a", - "go": "n/a", - "schema": "n/a", - "ts": "n/a" - }, - "machineName": "parcadatasourcecfg", - "maturity": "planned", - "name": "ParcaDataSourceCfg", - "pluralMachineName": "parcadatasourcecfgs", - "pluralName": "ParcaDataSourceCfgs", - "schemaInterface": "DataSourceCfg" - }, - "piechartpanelcfg": { - "category": "composable", - "codeowners": [ - "grafana/dataviz-squad" - ], - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": true, - "links": { - "docs": "https://grafana.com/docs/grafana/next/developers/kinds/composable/piechartpanelcfg/schema-reference", - "go": "n/a", - "schema": "https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/piechart/panelcfg.cue", - "ts": "https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/piechart/panelcfg.gen.ts" - }, - "machineName": "piechartpanelcfg", - "maturity": "experimental", - "name": "PieChartPanelCfg", - "pluralMachineName": "piechartpanelcfgs", - "pluralName": "PieChartPanelCfgs", - "schemaInterface": "PanelCfg" - }, - "playlist": { - "category": "core", - "codeowners": [], - "crd": { - "dummySchema": false, - "group": "", - "scope": "" - }, - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": false, - "links": { - "docs": "n/a", - "go": "n/a", - "schema": "n/a", - "ts": "n/a" - }, - "machineName": "playlist", - "maturity": "planned", - "name": "Playlist", - "pluralMachineName": "playlists", - "pluralName": "Playlists" - }, - "postgresqldataquery": { - "category": "composable", - "codeowners": [], - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": false, - "links": { - "docs": "n/a", - "go": "n/a", - "schema": "n/a", - "ts": "n/a" - }, - "machineName": "postgresqldataquery", - "maturity": "planned", - "name": "PostgreSQLDataQuery", - "pluralMachineName": "postgresqldataquerys", - "pluralName": "PostgreSQLDataQuerys", - "schemaInterface": "DataQuery" - }, - "postgresqldatasourcecfg": { - "category": "composable", - "codeowners": [], - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": true, - "links": { - "docs": "n/a", - "go": "n/a", - "schema": "n/a", - "ts": "n/a" - }, - "machineName": "postgresqldatasourcecfg", - "maturity": "planned", - "name": "PostgreSQLDataSourceCfg", - "pluralMachineName": "postgresqldatasourcecfgs", - "pluralName": "PostgreSQLDataSourceCfgs", - "schemaInterface": "DataSourceCfg" - }, - "preferences": { - "category": "core", - "codeowners": [ - "grafana/grafana-as-code", - "grafana/grafana-frontend-platform", - "grafana/plugins-platform-frontend" - ], - "crd": { - "dummySchema": false, - "group": "preferences.core.grafana.com", - "scope": "Namespaced" - }, - "currentVersion": [ - 0, - 0 - ], - "description": "The user or team frontend preferences", - "grafanaMaturityCount": 0, - "lineageIsGroup": false, - "links": { - "docs": "https://grafana.com/docs/grafana/next/developers/kinds/core/preferences/schema-reference", - "go": "https://github.com/grafana/grafana/tree/main/pkg/kinds/preferences", - "schema": "https://github.com/grafana/grafana/tree/main/kinds/preferences/preferences_kind.cue", - "ts": "https://github.com/grafana/grafana/tree/main/packages/grafana-schema/src/raw/preferences/x/preferences_types.gen.ts" - }, - "machineName": "preferences", - "maturity": "merged", - "name": "Preferences", - "pluralMachineName": "preferences", - "pluralName": "Preferences" - }, - "prometheusdataquery": { - "category": "composable", - "codeowners": [ - "grafana/observability-metrics" - ], - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": false, - "links": { - "docs": "https://grafana.com/docs/grafana/next/developers/kinds/composable/prometheusdataquery/schema-reference", - "go": "https://github.com/grafana/grafana/tree/main/pkg/tsdb/prometheus/kinds/dataquery/types_dataquery_gen.go", - "schema": "https://github.com/grafana/grafana/tree/main/public/app/plugins/datasource/prometheus/dataquery.cue", - "ts": "https://github.com/grafana/grafana/tree/main/public/app/plugins/datasource/prometheus/dataquery.gen.ts" - }, - "machineName": "prometheusdataquery", - "maturity": "experimental", - "name": "PrometheusDataQuery", - "pluralMachineName": "prometheusdataquerys", - "pluralName": "PrometheusDataQuerys", - "schemaInterface": "DataQuery" - }, - "prometheusdatasourcecfg": { - "category": "composable", - "codeowners": [], - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": true, - "links": { - "docs": "n/a", - "go": "n/a", - "schema": "n/a", - "ts": "n/a" - }, - "machineName": "prometheusdatasourcecfg", - "maturity": "planned", - "name": "PrometheusDataSourceCfg", - "pluralMachineName": "prometheusdatasourcecfgs", - "pluralName": "PrometheusDataSourceCfgs", - "schemaInterface": "DataSourceCfg" - }, - "publicdashboard": { - "category": "core", - "codeowners": [ - "grafana/grafana-as-code", - "grafana/grafana-frontend-platform", - "grafana/plugins-platform-frontend" - ], - "crd": { - "dummySchema": false, - "group": "publicdashboard.core.grafana.com", - "scope": "Namespaced" - }, - "currentVersion": [ - 0, - 0 - ], - "description": "Public dashboard configuration", - "grafanaMaturityCount": 0, - "lineageIsGroup": false, - "links": { - "docs": "https://grafana.com/docs/grafana/next/developers/kinds/core/publicdashboard/schema-reference", - "go": "https://github.com/grafana/grafana/tree/main/pkg/kinds/publicdashboard", - "schema": "https://github.com/grafana/grafana/tree/main/kinds/publicdashboard/publicdashboard_kind.cue", - "ts": "https://github.com/grafana/grafana/tree/main/packages/grafana-schema/src/raw/publicdashboard/x/publicdashboard_types.gen.ts" - }, - "machineName": "publicdashboard", - "maturity": "merged", - "name": "PublicDashboard", - "pluralMachineName": "publicdashboards", - "pluralName": "PublicDashboards" - }, - "query": { - "category": "core", - "codeowners": [], - "crd": { - "dummySchema": false, - "group": "", - "scope": "" - }, - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": false, - "links": { - "docs": "n/a", - "go": "n/a", - "schema": "n/a", - "ts": "n/a" - }, - "machineName": "query", - "maturity": "planned", - "name": "Query", - "pluralMachineName": "querys", - "pluralName": "Querys" - }, - "queryhistory": { - "category": "core", - "codeowners": [], - "crd": { - "dummySchema": false, - "group": "", - "scope": "" - }, - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": false, - "links": { - "docs": "n/a", - "go": "n/a", - "schema": "n/a", - "ts": "n/a" - }, - "machineName": "queryhistory", - "maturity": "planned", - "name": "QueryHistory", - "pluralMachineName": "queryhistorys", - "pluralName": "QueryHistorys" - }, - "role": { - "category": "core", - "codeowners": [ - "grafana/grafana-as-code", - "grafana/grafana-frontend-platform", - "grafana/plugins-platform-frontend" - ], - "crd": { - "dummySchema": false, - "group": "role.core.grafana.com", - "scope": "Namespaced" - }, - "currentVersion": [ - 0, - 0 - ], - "description": "Roles represent a set of users+teams that should share similar access", - "grafanaMaturityCount": 0, - "lineageIsGroup": false, - "links": { - "docs": "https://grafana.com/docs/grafana/next/developers/kinds/core/role/schema-reference", - "go": "https://github.com/grafana/grafana/tree/main/pkg/kinds/role", - "schema": "https://github.com/grafana/grafana/tree/main/kinds/role/role_kind.cue", - "ts": "https://github.com/grafana/grafana/tree/main/packages/grafana-schema/src/raw/role/x/role_types.gen.ts" - }, - "machineName": "role", - "maturity": "merged", - "name": "Role", - "pluralMachineName": "roles", - "pluralName": "Roles" - }, - "rolebinding": { - "category": "core", - "codeowners": [ - "grafana/grafana-as-code", - "grafana/grafana-frontend-platform", - "grafana/plugins-platform-frontend" - ], - "crd": { - "dummySchema": false, - "group": "rolebinding.core.grafana.com", - "scope": "Namespaced" - }, - "currentVersion": [ - 0, - 0 - ], - "description": "Role bindings links a user|team to a configured role", - "grafanaMaturityCount": 0, - "lineageIsGroup": false, - "links": { - "docs": "https://grafana.com/docs/grafana/next/developers/kinds/core/rolebinding/schema-reference", - "go": "https://github.com/grafana/grafana/tree/main/pkg/kinds/rolebinding", - "schema": "https://github.com/grafana/grafana/tree/main/kinds/rolebinding/rolebinding_kind.cue", - "ts": "https://github.com/grafana/grafana/tree/main/packages/grafana-schema/src/raw/rolebinding/x/rolebinding_types.gen.ts" - }, - "machineName": "rolebinding", - "maturity": "merged", - "name": "RoleBinding", - "pluralMachineName": "rolebindings", - "pluralName": "RoleBindings" - }, - "serviceaccount": { - "category": "core", - "codeowners": [], - "crd": { - "dummySchema": false, - "group": "", - "scope": "" - }, - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": false, - "links": { - "docs": "n/a", - "go": "n/a", - "schema": "n/a", - "ts": "n/a" - }, - "machineName": "serviceaccount", - "maturity": "planned", - "name": "ServiceAccount", - "pluralMachineName": "serviceaccounts", - "pluralName": "ServiceAccounts" - }, - "statetimelinepanelcfg": { - "category": "composable", - "codeowners": [ - "grafana/dataviz-squad" - ], - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": true, - "links": { - "docs": "https://grafana.com/docs/grafana/next/developers/kinds/composable/statetimelinepanelcfg/schema-reference", - "go": "n/a", - "schema": "https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/state-timeline/panelcfg.cue", - "ts": "https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/state-timeline/panelcfg.gen.ts" - }, - "machineName": "statetimelinepanelcfg", - "maturity": "experimental", - "name": "StateTimelinePanelCfg", - "pluralMachineName": "statetimelinepanelcfgs", - "pluralName": "StateTimelinePanelCfgs", - "schemaInterface": "PanelCfg" - }, - "statpanelcfg": { - "category": "composable", - "codeowners": [ - "grafana/dataviz-squad" - ], - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": true, - "links": { - "docs": "https://grafana.com/docs/grafana/next/developers/kinds/composable/statpanelcfg/schema-reference", - "go": "n/a", - "schema": "https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/stat/panelcfg.cue", - "ts": "https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/stat/panelcfg.gen.ts" - }, - "machineName": "statpanelcfg", - "maturity": "experimental", - "name": "StatPanelCfg", - "pluralMachineName": "statpanelcfgs", - "pluralName": "StatPanelCfgs", - "schemaInterface": "PanelCfg" - }, - "statushistorypanelcfg": { - "category": "composable", - "codeowners": [ - "grafana/dataviz-squad" - ], - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": true, - "links": { - "docs": "https://grafana.com/docs/grafana/next/developers/kinds/composable/statushistorypanelcfg/schema-reference", - "go": "n/a", - "schema": "https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/status-history/panelcfg.cue", - "ts": "https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/status-history/panelcfg.gen.ts" - }, - "machineName": "statushistorypanelcfg", - "maturity": "experimental", - "name": "StatusHistoryPanelCfg", - "pluralMachineName": "statushistorypanelcfgs", - "pluralName": "StatusHistoryPanelCfgs", - "schemaInterface": "PanelCfg" - }, - "tableoldpanelcfg": { - "category": "composable", - "codeowners": [], - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": true, - "links": { - "docs": "n/a", - "go": "n/a", - "schema": "n/a", - "ts": "n/a" - }, - "machineName": "tableoldpanelcfg", - "maturity": "planned", - "name": "TableOldPanelCfg", - "pluralMachineName": "tableoldpanelcfgs", - "pluralName": "TableOldPanelCfgs", - "schemaInterface": "PanelCfg" - }, - "tablepanelcfg": { - "category": "composable", - "codeowners": [ - "grafana/dataviz-squad" - ], - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": true, - "links": { - "docs": "https://grafana.com/docs/grafana/next/developers/kinds/composable/tablepanelcfg/schema-reference", - "go": "n/a", - "schema": "https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/table/panelcfg.cue", - "ts": "https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/table/panelcfg.gen.ts" - }, - "machineName": "tablepanelcfg", - "maturity": "experimental", - "name": "TablePanelCfg", - "pluralMachineName": "tablepanelcfgs", - "pluralName": "TablePanelCfgs", - "schemaInterface": "PanelCfg" - }, - "team": { - "category": "core", - "codeowners": [ - "grafana/grafana-as-code", - "grafana/grafana-frontend-platform", - "grafana/plugins-platform-frontend" - ], - "crd": { - "dummySchema": false, - "group": "team.core.grafana.com", - "scope": "Namespaced" - }, - "currentVersion": [ - 0, - 0 - ], - "description": "A team is a named grouping of Grafana users to which access control rules may be assigned.", - "grafanaMaturityCount": 0, - "lineageIsGroup": false, - "links": { - "docs": "https://grafana.com/docs/grafana/next/developers/kinds/core/team/schema-reference", - "go": "https://github.com/grafana/grafana/tree/main/pkg/kinds/team", - "schema": "https://github.com/grafana/grafana/tree/main/kinds/team/team_kind.cue", - "ts": "https://github.com/grafana/grafana/tree/main/packages/grafana-schema/src/raw/team/x/team_types.gen.ts" - }, - "machineName": "team", - "maturity": "merged", - "name": "Team", - "pluralMachineName": "teams", - "pluralName": "Teams" - }, - "tempodataquery": { - "category": "composable", - "codeowners": [ - "grafana/observability-traces-and-profiling" - ], - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": false, - "links": { - "docs": "https://grafana.com/docs/grafana/next/developers/kinds/composable/tempodataquery/schema-reference", - "go": "https://github.com/grafana/grafana/tree/main/pkg/tsdb/tempo/kinds/dataquery/types_dataquery_gen.go", - "schema": "https://github.com/grafana/grafana/tree/main/public/app/plugins/datasource/tempo/dataquery.cue", - "ts": "https://github.com/grafana/grafana/tree/main/public/app/plugins/datasource/tempo/dataquery.gen.ts" - }, - "machineName": "tempodataquery", - "maturity": "experimental", - "name": "TempoDataQuery", - "pluralMachineName": "tempodataquerys", - "pluralName": "TempoDataQuerys", - "schemaInterface": "DataQuery" - }, - "tempodatasourcecfg": { - "category": "composable", - "codeowners": [], - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": true, - "links": { - "docs": "n/a", - "go": "n/a", - "schema": "n/a", - "ts": "n/a" - }, - "machineName": "tempodatasourcecfg", - "maturity": "planned", - "name": "TempoDataSourceCfg", - "pluralMachineName": "tempodatasourcecfgs", - "pluralName": "TempoDataSourceCfgs", - "schemaInterface": "DataSourceCfg" - }, - "testdatadataquery": { - "category": "composable", - "codeowners": [ - "grafana/plugins-platform-backend", - "grafana/plugins-platform-frontend" - ], - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": false, - "links": { - "docs": "https://grafana.com/docs/grafana/next/developers/kinds/composable/testdatadataquery/schema-reference", - "go": "https://github.com/grafana/grafana/tree/main/pkg/tsdb/grafana-testdata-datasource/kinds/dataquery/types_dataquery_gen.go", - "schema": "https://github.com/grafana/grafana/tree/main/public/app/plugins/datasource/grafana-testdata-datasource/dataquery.cue", - "ts": "https://github.com/grafana/grafana/tree/main/public/app/plugins/datasource/grafana-testdata-datasource/dataquery.gen.ts" - }, - "machineName": "testdatadataquery", - "maturity": "experimental", - "name": "TestDataDataQuery", - "pluralMachineName": "testdatadataquerys", - "pluralName": "TestDataDataQuerys", - "schemaInterface": "DataQuery" - }, - "testdatadatasourcecfg": { - "category": "composable", - "codeowners": [], - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": true, - "links": { - "docs": "n/a", - "go": "n/a", - "schema": "n/a", - "ts": "n/a" - }, - "machineName": "testdatadatasourcecfg", - "maturity": "planned", - "name": "TestDataDataSourceCfg", - "pluralMachineName": "testdatadatasourcecfgs", - "pluralName": "TestDataDataSourceCfgs", - "schemaInterface": "DataSourceCfg" - }, - "textpanelcfg": { - "category": "composable", - "codeowners": [ - "grafana/grafana-frontend-platform" - ], - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": true, - "links": { - "docs": "https://grafana.com/docs/grafana/next/developers/kinds/composable/textpanelcfg/schema-reference", - "go": "n/a", - "schema": "https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/text/panelcfg.cue", - "ts": "https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/text/panelcfg.gen.ts" - }, - "machineName": "textpanelcfg", - "maturity": "experimental", - "name": "TextPanelCfg", - "pluralMachineName": "textpanelcfgs", - "pluralName": "TextPanelCfgs", - "schemaInterface": "PanelCfg" - }, - "thumb": { - "category": "core", - "codeowners": [], - "crd": { - "dummySchema": false, - "group": "", - "scope": "" - }, - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": false, - "links": { - "docs": "n/a", - "go": "n/a", - "schema": "n/a", - "ts": "n/a" - }, - "machineName": "thumb", - "maturity": "planned", - "name": "Thumb", - "pluralMachineName": "thumbs", - "pluralName": "Thumbs" - }, - "timeseriespanelcfg": { - "category": "composable", - "codeowners": [ - "grafana/dataviz-squad" - ], - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": true, - "links": { - "docs": "https://grafana.com/docs/grafana/next/developers/kinds/composable/timeseriespanelcfg/schema-reference", - "go": "n/a", - "schema": "https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/timeseries/panelcfg.cue", - "ts": "https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/timeseries/panelcfg.gen.ts" - }, - "machineName": "timeseriespanelcfg", - "maturity": "merged", - "name": "TimeSeriesPanelCfg", - "pluralMachineName": "timeseriespanelcfgs", - "pluralName": "TimeSeriesPanelCfgs", - "schemaInterface": "PanelCfg" - }, - "tracespanelcfg": { - "category": "composable", - "codeowners": [], - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": true, - "links": { - "docs": "n/a", - "go": "n/a", - "schema": "n/a", - "ts": "n/a" - }, - "machineName": "tracespanelcfg", - "maturity": "planned", - "name": "TracesPanelCfg", - "pluralMachineName": "tracespanelcfgs", - "pluralName": "TracesPanelCfgs", - "schemaInterface": "PanelCfg" - }, - "trendpanelcfg": { - "category": "composable", - "codeowners": [ - "grafana/dataviz-squad" - ], - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": true, - "links": { - "docs": "https://grafana.com/docs/grafana/next/developers/kinds/composable/trendpanelcfg/schema-reference", - "go": "n/a", - "schema": "https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/trend/panelcfg.cue", - "ts": "https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/trend/panelcfg.gen.ts" - }, - "machineName": "trendpanelcfg", - "maturity": "merged", - "name": "TrendPanelCfg", - "pluralMachineName": "trendpanelcfgs", - "pluralName": "TrendPanelCfgs", - "schemaInterface": "PanelCfg" - }, - "user": { - "category": "core", - "codeowners": [], - "crd": { - "dummySchema": false, - "group": "", - "scope": "" - }, - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": false, - "links": { - "docs": "n/a", - "go": "n/a", - "schema": "n/a", - "ts": "n/a" - }, - "machineName": "user", - "maturity": "planned", - "name": "User", - "pluralMachineName": "users", - "pluralName": "Users" - }, - "welcomepanelcfg": { - "category": "composable", - "codeowners": [], - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": true, - "links": { - "docs": "n/a", - "go": "n/a", - "schema": "n/a", - "ts": "n/a" - }, - "machineName": "welcomepanelcfg", - "maturity": "planned", - "name": "WelcomePanelCfg", - "pluralMachineName": "welcomepanelcfgs", - "pluralName": "WelcomePanelCfgs", - "schemaInterface": "PanelCfg" - }, - "xychartpanelcfg": { - "category": "composable", - "codeowners": [ - "grafana/dataviz-squad" - ], - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": true, - "links": { - "docs": "https://grafana.com/docs/grafana/next/developers/kinds/composable/xychartpanelcfg/schema-reference", - "go": "n/a", - "schema": "https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/xychart/panelcfg.cue", - "ts": "https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/xychart/panelcfg.gen.ts" - }, - "machineName": "xychartpanelcfg", - "maturity": "experimental", - "name": "XYChartPanelCfg", - "pluralMachineName": "xychartpanelcfgs", - "pluralName": "XYChartPanelCfgs", - "schemaInterface": "PanelCfg" - }, - "zipkindataquery": { - "category": "composable", - "codeowners": [], - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": false, - "links": { - "docs": "n/a", - "go": "n/a", - "schema": "n/a", - "ts": "n/a" - }, - "machineName": "zipkindataquery", - "maturity": "planned", - "name": "ZipkinDataQuery", - "pluralMachineName": "zipkindataquerys", - "pluralName": "ZipkinDataQuerys", - "schemaInterface": "DataQuery" - }, - "zipkindatasourcecfg": { - "category": "composable", - "codeowners": [], - "currentVersion": [ - 0, - 0 - ], - "grafanaMaturityCount": 0, - "lineageIsGroup": true, - "links": { - "docs": "n/a", - "go": "n/a", - "schema": "n/a", - "ts": "n/a" - }, - "machineName": "zipkindatasourcecfg", - "maturity": "planned", - "name": "ZipkinDataSourceCfg", - "pluralMachineName": "zipkindatasourcecfgs", - "pluralName": "ZipkinDataSourceCfgs", - "schemaInterface": "DataSourceCfg" - } - }, - "dimensions": { - "category": { - "composable": { - "name": "composable", - "items": [ - "alertgroupspanelcfg", - "alertlistpanelcfg", - "alertmanagerdataquery", - "alertmanagerdatasourcecfg", - "annotationslistpanelcfg", - "azuremonitordataquery", - "azuremonitordatasourcecfg", - "barchartpanelcfg", - "bargaugepanelcfg", - "candlestickpanelcfg", - "canvaspanelcfg", - "cloudwatchdataquery", - "cloudwatchdatasourcecfg", - "dashboarddataquery", - "dashboarddatasourcecfg", - "dashboardlistpanelcfg", - "datagridpanelcfg", - "debugpanelcfg", - "elasticsearchdataquery", - "elasticsearchdatasourcecfg", - "flamegraphpanelcfg", - "gaugepanelcfg", - "geomappanelcfg", - "gettingstartedpanelcfg", - "googlecloudmonitoringdataquery", - "googlecloudmonitoringdatasourcecfg", - "grafanadataquery", - "grafanadatasourcecfg", - "grafanapyroscopedataquery", - "grafanapyroscopedatasourcecfg", - "graphitedataquery", - "graphitedatasourcecfg", - "grapholdpanelcfg", - "heatmappanelcfg", - "histogrampanelcfg", - "jaegerdataquery", - "jaegerdatasourcecfg", - "livepanelcfg", - "logspanelcfg", - "lokidataquery", - "lokidatasourcecfg", - "microsoftsqlserverdataquery", - "microsoftsqlserverdatasourcecfg", - "mysqldataquery", - "mysqldatasourcecfg", - "newspanelcfg", - "nodegraphpanelcfg", - "parcadataquery", - "parcadatasourcecfg", - "piechartpanelcfg", - "postgresqldataquery", - "postgresqldatasourcecfg", - "prometheusdataquery", - "prometheusdatasourcecfg", - "statetimelinepanelcfg", - "statpanelcfg", - "statushistorypanelcfg", - "tableoldpanelcfg", - "tablepanelcfg", - "tempodataquery", - "tempodatasourcecfg", - "testdatadataquery", - "testdatadatasourcecfg", - "textpanelcfg", - "timeseriespanelcfg", - "tracespanelcfg", - "trendpanelcfg", - "welcomepanelcfg", - "xychartpanelcfg", - "zipkindataquery", - "zipkindatasourcecfg" - ], - "count": 71 - }, - "core": { - "name": "core", - "items": [ - "accesspolicy", - "apikey", - "dashboard", - "datasource", - "folder", - "librarypanel", - "playlist", - "preferences", - "publicdashboard", - "query", - "queryhistory", - "role", - "rolebinding", - "serviceaccount", - "team", - "thumb", - "user" - ], - "count": 17 - } - }, - "maturity": { - "experimental": { - "name": "experimental", - "items": [ - "annotationslistpanelcfg", - "barchartpanelcfg", - "bargaugepanelcfg", - "candlestickpanelcfg", - "canvaspanelcfg", - "cloudwatchdataquery", - "dashboard", - "dashboardlistpanelcfg", - "datagridpanelcfg", - "debugpanelcfg", - "elasticsearchdataquery", - "gaugepanelcfg", - "geomappanelcfg", - "grafanapyroscopedataquery", - "histogrampanelcfg", - "librarypanel", - "logspanelcfg", - "lokidataquery", - "newspanelcfg", - "nodegraphpanelcfg", - "parcadataquery", - "piechartpanelcfg", - "prometheusdataquery", - "statetimelinepanelcfg", - "statpanelcfg", - "statushistorypanelcfg", - "tablepanelcfg", - "tempodataquery", - "testdatadataquery", - "textpanelcfg", - "xychartpanelcfg" - ], - "count": 31 - }, - "mature": { - "name": "mature", - "items": [], - "count": 0 - }, - "merged": { - "name": "merged", - "items": [ - "accesspolicy", - "alertgroupspanelcfg", - "azuremonitordataquery", - "googlecloudmonitoringdataquery", - "heatmappanelcfg", - "preferences", - "publicdashboard", - "role", - "rolebinding", - "team", - "timeseriespanelcfg", - "trendpanelcfg" - ], - "count": 12 - }, - "planned": { - "name": "planned", - "items": [ - "alertlistpanelcfg", - "alertmanagerdataquery", - "alertmanagerdatasourcecfg", - "apikey", - "azuremonitordatasourcecfg", - "cloudwatchdatasourcecfg", - "dashboarddataquery", - "dashboarddatasourcecfg", - "datasource", - "elasticsearchdatasourcecfg", - "flamegraphpanelcfg", - "folder", - "gettingstartedpanelcfg", - "googlecloudmonitoringdatasourcecfg", - "grafanadataquery", - "grafanadatasourcecfg", - "grafanapyroscopedatasourcecfg", - "graphitedataquery", - "graphitedatasourcecfg", - "grapholdpanelcfg", - "jaegerdataquery", - "jaegerdatasourcecfg", - "livepanelcfg", - "lokidatasourcecfg", - "microsoftsqlserverdataquery", - "microsoftsqlserverdatasourcecfg", - "mysqldataquery", - "mysqldatasourcecfg", - "parcadatasourcecfg", - "playlist", - "postgresqldataquery", - "postgresqldatasourcecfg", - "prometheusdatasourcecfg", - "query", - "queryhistory", - "serviceaccount", - "tableoldpanelcfg", - "tempodatasourcecfg", - "testdatadatasourcecfg", - "thumb", - "tracespanelcfg", - "user", - "welcomepanelcfg", - "zipkindataquery", - "zipkindatasourcecfg" - ], - "count": 45 - }, - "stable": { - "name": "stable", - "items": [], - "count": 0 - } - } - } -} \ No newline at end of file diff --git a/pkg/kindsysreport/codeowners.go b/pkg/kindsysreport/codeowners.go deleted file mode 100644 index 5c1a3edcc7..0000000000 --- a/pkg/kindsysreport/codeowners.go +++ /dev/null @@ -1,61 +0,0 @@ -package kindsysreport - -import ( - "os" - "path/filepath" - - "github.com/hmarr/codeowners" -) - -type CodeOwnersFinder struct { - ruleset codeowners.Ruleset -} - -func NewCodeOwnersFinder(groot string) (CodeOwnersFinder, error) { - //nolint:gosec - file, err := os.Open(filepath.Join(groot, ".github", "CODEOWNERS")) - if err != nil { - return CodeOwnersFinder{}, err - } - - ruleset, err := codeowners.ParseFile(file) - if err != nil { - return CodeOwnersFinder{}, err - } - - return CodeOwnersFinder{ - ruleset: ruleset, - }, nil -} - -func (f CodeOwnersFinder) FindFor(pp ...string) ([]string, error) { - if len(f.ruleset) == 0 { - return nil, nil - } - - // Set, to avoid duplicates - m := make(map[string]struct{}) - - for _, p := range pp { - r, err := f.ruleset.Match(p) - if err != nil { - return nil, err - } - - // No rule found for path p - if r == nil { - continue - } - - for _, o := range r.Owners { - m[o.Value] = struct{}{} - } - } - - result := make([]string, 0, len(m)) - for k := range m { - result = append(result, k) - } - - return result, nil -} diff --git a/public/app/plugins/gen.go b/public/app/plugins/gen.go index faf6037d8e..558e503245 100644 --- a/public/app/plugins/gen.go +++ b/public/app/plugins/gen.go @@ -51,9 +51,6 @@ func main() { codegen.PluginTreeListJenny(), codegen.PluginGoTypesJenny("pkg/tsdb"), codegen.PluginTSTypesJenny("public/app/plugins", adaptToPipeline(corecodegen.TSTypesJenny{})), - kind2pd(rt, corecodegen.DocsJenny( - filepath.Join("docs", "sources", "developers", "kinds", "composable"), - )), codegen.PluginTSEachMajor(rt), ) From 2146f6ce1f622f0b03bc88720010d3ce1f58293d Mon Sep 17 00:00:00 2001 From: Todd Treece <360020+toddtreece@users.noreply.github.com> Date: Fri, 23 Feb 2024 14:10:51 -0500 Subject: [PATCH 0146/1406] Chore: Update drone signature (#83334) --- .drone.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.drone.yml b/.drone.yml index dcc5b951d6..284bf0d15d 100644 --- a/.drone.yml +++ b/.drone.yml @@ -4924,6 +4924,6 @@ kind: secret name: gcr_credentials --- kind: signature -hmac: 0d5e600881f5a8f4294cafd07f450f92f4088e1cda7254d111a635bf01f07465 +hmac: b588ab1704559f537b65d7fe2cb45c4308274943a2be023805eb491cc9dc7302 ... From f4b432841b8849c6cd88466808619fef1c47d2b3 Mon Sep 17 00:00:00 2001 From: Todd Treece <360020+toddtreece@users.noreply.github.com> Date: Fri, 23 Feb 2024 14:28:30 -0500 Subject: [PATCH 0147/1406] Chore: Add env var check to `make drone` (#83337) --- Makefile | 1 + scripts/drone/env-var-check.sh | 12 ++++++++++++ 2 files changed, 13 insertions(+) create mode 100755 scripts/drone/env-var-check.sh diff --git a/Makefile b/Makefile index 3a0645364e..79fd4a84ea 100644 --- a/Makefile +++ b/Makefile @@ -318,6 +318,7 @@ gen-ts: # Use this make target to regenerate the configuration YAML files when # you modify starlark files. drone: $(DRONE) + bash scripts/drone/env-var-check.sh $(DRONE) starlark --format $(DRONE) lint .drone.yml --trusted $(DRONE) --server https://drone.grafana.net sign --save grafana/grafana diff --git a/scripts/drone/env-var-check.sh b/scripts/drone/env-var-check.sh new file mode 100755 index 0000000000..adfbe88f4c --- /dev/null +++ b/scripts/drone/env-var-check.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +# ensure DRONE_SERVER and DRONE_TOKEN env variables are set +if [ -z "$DRONE_SERVER" ]; then + echo "DRONE_SERVER environment variable is not set." + exit 1 +fi + +if [ -z "$DRONE_TOKEN" ]; then + echo "DRONE_TOKEN environment variable is not set." + exit 1 +fi \ No newline at end of file From e5a26a3f7c21d0e9c50b678be5e5431cf0cc0cfa Mon Sep 17 00:00:00 2001 From: Todd Treece <360020+toddtreece@users.noreply.github.com> Date: Fri, 23 Feb 2024 15:15:43 -0500 Subject: [PATCH 0148/1406] K8s: Add apimachinery and apiserver packages (#83190) --- .github/CODEOWNERS | 2 + go.mod | 18 +- go.sum | 23 +- go.work | 2 + go.work.sum | 848 +----------------- pkg/apimachinery/apis/common/v0alpha1/doc.go | 5 + .../apis/common/v0alpha1/resource.go | 0 .../apis/common/v0alpha1/types.go | 0 .../apis/common/v0alpha1/unstructured.go | 0 .../common/v0alpha1/zz_generated.defaults.go | 0 .../common/v0alpha1/zz_generated.openapi.go | 110 +-- ...enerated.openapi_violation_exceptions.list | 2 +- pkg/apimachinery/go.mod | 37 + pkg/apimachinery/go.sum | 104 +++ pkg/apis/common/v0alpha1/doc.go | 5 - pkg/apis/dashboard/v0alpha1/register.go | 2 +- pkg/apis/dashboard/v0alpha1/types.go | 2 +- .../dashboardsnapshot/v0alpha1/register.go | 2 +- pkg/apis/dashboardsnapshot/v0alpha1/types.go | 2 +- pkg/apis/datasource/v0alpha1/register.go | 2 +- pkg/apis/datasource/v0alpha1/types.go | 2 +- pkg/apis/example/v0alpha1/register.go | 2 +- pkg/apis/example/v0alpha1/types.go | 2 +- pkg/apis/featuretoggle/v0alpha1/register.go | 2 +- pkg/apis/featuretoggle/v0alpha1/types.go | 2 +- .../v0alpha1/zz_generated.deepcopy.go | 2 +- pkg/apis/folder/v0alpha1/register.go | 2 +- pkg/apis/peakq/v0alpha1/register.go | 2 +- pkg/apis/playlist/v0alpha1/register.go | 2 +- pkg/apis/query/v0alpha1/register.go | 2 +- pkg/apis/query/v0alpha1/template/types.go | 2 +- pkg/apis/scope/v0alpha1/register.go | 2 +- pkg/apis/service/v0alpha1/register.go | 2 +- .../apiserver/builder/common.go | 0 .../apiserver/builder/helper.go | 20 +- .../apiserver/builder/openapi.go | 7 +- .../apiserver/builder/request_handler.go | 0 .../responsewriter/responsewriter.go | 0 .../responsewriter/responsewriter_test.go | 3 +- pkg/apiserver/go.mod | 109 +++ pkg/apiserver/go.sum | 262 ++++++ .../apiserver/registry/generic/strategy.go | 0 .../apiserver/rest/dualwriter.go | 7 +- .../apiserver/storage/file/file.go | 0 .../apiserver/storage/file/restoptions.go | 0 .../apiserver/storage/file/util.go | 0 .../apiserver/storage/file/watchset.go | 0 pkg/cmd/grafana/apiserver/server.go | 13 +- pkg/registry/apis/dashboard/legacy_storage.go | 2 +- pkg/registry/apis/dashboard/register.go | 6 +- pkg/registry/apis/dashboard/sub_versions.go | 2 +- .../apis/dashboard/summary_storage.go | 2 +- .../apis/dashboardsnapshot/exporter.go | 2 +- .../apis/dashboardsnapshot/register.go | 2 +- .../apis/dashboardsnapshot/sub_body.go | 2 +- pkg/registry/apis/datasource/connections.go | 2 +- pkg/registry/apis/datasource/querier.go | 2 +- pkg/registry/apis/datasource/register.go | 4 +- pkg/registry/apis/example/dummy_storage.go | 4 +- pkg/registry/apis/example/register.go | 2 +- pkg/registry/apis/example/storage.go | 2 +- pkg/registry/apis/featuretoggle/current.go | 2 +- pkg/registry/apis/featuretoggle/features.go | 2 +- pkg/registry/apis/featuretoggle/register.go | 2 +- pkg/registry/apis/featuretoggle/toggles.go | 2 +- pkg/registry/apis/folders/register.go | 4 +- pkg/registry/apis/folders/storage.go | 4 +- pkg/registry/apis/peakq/register.go | 2 +- pkg/registry/apis/peakq/storage.go | 4 +- pkg/registry/apis/playlist/register.go | 4 +- pkg/registry/apis/playlist/storage.go | 4 +- pkg/registry/apis/query/plugins.go | 2 +- pkg/registry/apis/query/register.go | 2 +- pkg/registry/apis/scope/register.go | 2 +- pkg/registry/apis/scope/storage.go | 4 +- pkg/registry/apis/service/register.go | 2 +- pkg/registry/apis/service/storage.go | 4 +- pkg/services/apiserver/options/aggregator.go | 2 +- pkg/services/apiserver/service.go | 16 +- pkg/services/apiserver/standalone/factory.go | 2 +- pkg/services/apiserver/wireset.go | 2 +- .../database/database_test.go | 2 +- pkg/services/dashboardsnapshots/service.go | 2 +- .../service/service_test.go | 2 +- 84 files changed, 701 insertions(+), 1020 deletions(-) create mode 100644 pkg/apimachinery/apis/common/v0alpha1/doc.go rename pkg/{ => apimachinery}/apis/common/v0alpha1/resource.go (100%) rename pkg/{ => apimachinery}/apis/common/v0alpha1/types.go (100%) rename pkg/{ => apimachinery}/apis/common/v0alpha1/unstructured.go (100%) rename pkg/{ => apimachinery}/apis/common/v0alpha1/zz_generated.defaults.go (100%) rename pkg/{ => apimachinery}/apis/common/v0alpha1/zz_generated.openapi.go (96%) rename pkg/{ => apimachinery}/apis/common/v0alpha1/zz_generated.openapi_violation_exceptions.list (98%) create mode 100644 pkg/apimachinery/go.mod create mode 100644 pkg/apimachinery/go.sum delete mode 100644 pkg/apis/common/v0alpha1/doc.go rename pkg/{services => }/apiserver/builder/common.go (100%) rename pkg/{services => }/apiserver/builder/helper.go (90%) rename pkg/{services => }/apiserver/builder/openapi.go (92%) rename pkg/{services => }/apiserver/builder/request_handler.go (100%) rename pkg/{services => }/apiserver/endpoints/responsewriter/responsewriter.go (100%) rename pkg/{services => }/apiserver/endpoints/responsewriter/responsewriter_test.go (97%) create mode 100644 pkg/apiserver/go.mod create mode 100644 pkg/apiserver/go.sum rename pkg/{services => }/apiserver/registry/generic/strategy.go (100%) rename pkg/{services => }/apiserver/rest/dualwriter.go (97%) rename pkg/{services => }/apiserver/storage/file/file.go (100%) rename pkg/{services => }/apiserver/storage/file/restoptions.go (100%) rename pkg/{services => }/apiserver/storage/file/util.go (100%) rename pkg/{services => }/apiserver/storage/file/watchset.go (100%) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 1141c2058b..b41cff4f24 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -99,6 +99,8 @@ /pkg/mocks/ @grafana/backend-platform /pkg/models/ @grafana/backend-platform /pkg/server/ @grafana/backend-platform +/pkg/apiserver @grafana/grafana-app-platform-squad +/pkg/apimachinery @grafana/grafana-app-platform-squad /pkg/services/annotations/ @grafana/backend-platform /pkg/services/apikey/ @grafana/identity-access-team /pkg/services/cleanup/ @grafana/backend-platform diff --git a/go.mod b/go.mod index 6e11a415ab..cea0701592 100644 --- a/go.mod +++ b/go.mod @@ -275,14 +275,14 @@ require ( require ( github.com/spf13/cobra v1.8.0 // @grafana/grafana-app-platform-squad go.opentelemetry.io/otel v1.22.0 // @grafana/backend-platform - k8s.io/api v0.29.0 // @grafana/grafana-app-platform-squad - k8s.io/apimachinery v0.29.0 // @grafana/grafana-app-platform-squad - k8s.io/apiserver v0.29.0 // @grafana/grafana-app-platform-squad - k8s.io/client-go v0.29.0 // @grafana/grafana-app-platform-squad - k8s.io/component-base v0.29.0 // @grafana/grafana-app-platform-squad - k8s.io/klog/v2 v2.110.1 // @grafana/grafana-app-platform-squad + k8s.io/api v0.29.2 // @grafana/grafana-app-platform-squad + k8s.io/apimachinery v0.29.2 // @grafana/grafana-app-platform-squad + k8s.io/apiserver v0.29.2 // @grafana/grafana-app-platform-squad + k8s.io/client-go v0.29.2 // @grafana/grafana-app-platform-squad + k8s.io/component-base v0.29.2 // @grafana/grafana-app-platform-squad + k8s.io/klog/v2 v2.120.1 // @grafana/grafana-app-platform-squad k8s.io/kube-aggregator v0.29.0 // @grafana/grafana-app-platform-squad - k8s.io/kube-openapi v0.0.0-20231214164306-ab13479f8bf8 // @grafana/grafana-app-platform-squad + k8s.io/kube-openapi v0.0.0-20240220201932-37d671a357a5 // @grafana/grafana-app-platform-squad ) require github.com/grafana/gofpdf v0.0.0-20231002120153-857cc45be447 // @grafana/sharing-squad @@ -347,7 +347,7 @@ require ( github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-immutable-radix v1.3.1 // indirect github.com/hashicorp/go-retryablehttp v0.7.4 // indirect - github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // @grafana/alerting-squad-backend github.com/hashicorp/hcl v1.0.0 // indirect github.com/hashicorp/memberlist v0.5.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect @@ -409,7 +409,7 @@ require ( gopkg.in/fsnotify/fsnotify.v1 v1.4.7 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect - k8s.io/kms v0.29.0 // indirect + k8s.io/kms v0.29.2 // indirect lukechampine.com/uint128 v1.3.0 // indirect modernc.org/cc/v3 v3.40.0 // indirect modernc.org/ccgo/v3 v3.16.13 // indirect diff --git a/go.sum b/go.sum index 621e66ce90..98dd5431b4 100644 --- a/go.sum +++ b/go.sum @@ -5006,35 +5006,28 @@ honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9 honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0= k8s.io/api v0.28.4/go.mod h1:axWTGrY88s/5YE+JSt4uUi6NMM+gur1en2REMR7IRj0= -k8s.io/api v0.29.0 h1:NiCdQMY1QOp1H8lfRyeEf8eOwV6+0xA6XEE44ohDX2A= -k8s.io/api v0.29.0/go.mod h1:sdVmXoz2Bo/cb77Pxi71IPTSErEW32xa4aXwKH7gfBA= +k8s.io/api v0.29.2 h1:hBC7B9+MU+ptchxEqTNW2DkUosJpp1P+Wn6YncZ474A= k8s.io/apimachinery v0.28.4/go.mod h1:wI37ncBvfAoswfq626yPTe6Bz1c22L7uaJ8dho83mgg= -k8s.io/apimachinery v0.29.0 h1:+ACVktwyicPz0oc6MTMLwa2Pw3ouLAfAon1wPLtG48o= -k8s.io/apimachinery v0.29.0/go.mod h1:eVBxQ/cwiJxH58eK/jd/vAk4mrxmVlnpBH5J2GbMeis= -k8s.io/apiserver v0.29.0 h1:Y1xEMjJkP+BIi0GSEv1BBrf1jLU9UPfAnnGGbbDdp7o= -k8s.io/apiserver v0.29.0/go.mod h1:31n78PsRKPmfpee7/l9NYEv67u6hOL6AfcE761HapDM= +k8s.io/apimachinery v0.29.2 h1:EWGpfJ856oj11C52NRCHuU7rFDwxev48z+6DSlGNsV8= +k8s.io/apiserver v0.29.2 h1:+Z9S0dSNr+CjnVXQePG8TcBWHr3Q7BmAr7NraHvsMiQ= k8s.io/client-go v0.28.4/go.mod h1:0VDZFpgoZfelyP5Wqu0/r/TRYcLYuJ2U1KEeoaPa1N4= -k8s.io/client-go v0.29.0 h1:KmlDtFcrdUzOYrBhXHgKw5ycWzc3ryPX5mQe0SkG3y8= -k8s.io/client-go v0.29.0/go.mod h1:yLkXH4HKMAywcrD82KMSmfYg2DlE8mepPR4JGSo5n38= +k8s.io/client-go v0.29.2 h1:FEg85el1TeZp+/vYJM7hkDlSTFZ+c5nnK44DJ4FyoRg= k8s.io/code-generator v0.29.1 h1:8ba8BdtSmAVHgAMpzThb/fuyQeTRtN7NtN7VjMcDLew= k8s.io/code-generator v0.29.1/go.mod h1:FwFi3C9jCrmbPjekhaCYcYG1n07CYiW1+PAPCockaos= -k8s.io/component-base v0.29.0 h1:T7rjd5wvLnPBV1vC4zWd/iWRbV8Mdxs+nGaoaFzGw3s= -k8s.io/component-base v0.29.0/go.mod h1:sADonFTQ9Zc9yFLghpDpmNXEdHyQmFIGbiuZbqAXQ1M= +k8s.io/component-base v0.29.2 h1:lpiLyuvPA9yV1aQwGLENYyK7n/8t6l3nn3zAtFTJYe8= k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= k8s.io/klog/v2 v2.80.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= -k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0= k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo= -k8s.io/kms v0.29.0 h1:KJ1zaZt74CgvgV3NR7tnURJ/mJOKC5X3nwon/WdwgxI= -k8s.io/kms v0.29.0/go.mod h1:mB0f9HLxRXeXUfHfn1A7rpwOlzXI1gIWu86z6buNoYA= +k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= +k8s.io/kms v0.29.2 h1:MDsbp98gSlEQs7K7dqLKNNTwKFQRYYvO4UOlBOjNy6Y= k8s.io/kube-aggregator v0.29.0 h1:N4fmtePxOZ+bwiK1RhVEztOU+gkoVkvterHgpwAuiTw= k8s.io/kube-aggregator v0.29.0/go.mod h1:bjatII63ORkFg5yUFP2qm2OC49R0wwxZhRVIyJ4Z4X0= k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9/go.mod h1:wZK2AVp1uHCp4VamDVgBP2COHZjqD1T68Rf0CM3YjSM= -k8s.io/kube-openapi v0.0.0-20231214164306-ab13479f8bf8 h1:yHNkNuLjht7iq95pO9QmbjOWCguvn8mDe3lT78nqPkw= -k8s.io/kube-openapi v0.0.0-20231214164306-ab13479f8bf8/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA= +k8s.io/kube-openapi v0.0.0-20240220201932-37d671a357a5 h1:QSpdNrZ9uRlV0VkqLvVO0Rqg8ioKi3oSw7O5P7pJV8M= k8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= k8s.io/utils v0.0.0-20230406110748-d93618cff8a2/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= k8s.io/utils v0.0.0-20230711102312-30195339c3c7/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= diff --git a/go.work b/go.work index 1c986619a4..33dd16d7ea 100644 --- a/go.work +++ b/go.work @@ -2,6 +2,8 @@ go 1.21.0 use ( . + ./pkg/apimachinery + ./pkg/apiserver ./pkg/util/xorm ) diff --git a/go.work.sum b/go.work.sum index de3ead0b03..35c0c2b8ee 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1,862 +1,18 @@ -bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898 h1:SC+c6A1qTFstO9qmB86mPV2IpYme/2ZoEQ0hrP+wo+Q= -buf.build/gen/go/grpc-ecosystem/grpc-gateway/bufbuild/connect-go v1.4.1-20221127060915-a1ecdc58eccd.1 h1:vp9EaPFSb75qe/793x58yE5fY1IJ/gdxb/kcDUzavtI= -buf.build/gen/go/grpc-ecosystem/grpc-gateway/bufbuild/connect-go v1.4.1-20221127060915-a1ecdc58eccd.1/go.mod h1:YDq2B5X5BChU0lxAG5MxHpDb8mx1fv9OGtF2mwOe7hY= -buf.build/gen/go/grpc-ecosystem/grpc-gateway/protocolbuffers/go v1.28.1-20221127060915-a1ecdc58eccd.4 h1:z3Xc9n8yZ5k/Xr4ZTuff76TAYP20dWy7ZBV4cGIpbkM= -cloud.google.com/go/accessapproval v1.7.4 h1:ZvLvJ952zK8pFHINjpMBY5k7LTAp/6pBf50RDMRgBUI= -cloud.google.com/go/accesscontextmanager v1.8.4 h1:Yo4g2XrBETBCqyWIibN3NHNPQKUfQqti0lI+70rubeE= -cloud.google.com/go/aiplatform v1.57.0 h1:WcZ6wDf/1qBWatmGM9Z+2BTiNjQQX54k2BekHUj93DQ= -cloud.google.com/go/aiplatform v1.57.0/go.mod h1:pwZMGvqe0JRkI1GWSZCtnAfrR4K1bv65IHILGA//VEU= -cloud.google.com/go/analytics v0.21.6 h1:fnV7B8lqyEYxCU0LKk+vUL7mTlqRAq4uFlIthIdr/iA= -cloud.google.com/go/apigateway v1.6.4 h1:VVIxCtVerchHienSlaGzV6XJGtEM9828Erzyr3miUGs= -cloud.google.com/go/apigeeconnect v1.6.4 h1:jSoGITWKgAj/ssVogNE9SdsTqcXnryPzsulENSRlusI= -cloud.google.com/go/apigeeregistry v0.8.2 h1:DSaD1iiqvELag+lV4VnnqUUFd8GXELu01tKVdWZrviE= -cloud.google.com/go/apikeys v0.6.0 h1:B9CdHFZTFjVti89tmyXXrO+7vSNo2jvZuHG8zD5trdQ= -cloud.google.com/go/appengine v1.8.4 h1:Qub3fqR7iA1daJWdzjp/Q0Jz0fUG0JbMc7Ui4E9IX/E= -cloud.google.com/go/area120 v0.8.4 h1:YnSO8m02pOIo6AEOgiOoUDVbw4pf+bg2KLHi4rky320= -cloud.google.com/go/artifactregistry v1.14.6 h1:/hQaadYytMdA5zBh+RciIrXZQBWK4vN7EUsrQHG+/t8= -cloud.google.com/go/asset v1.15.3 h1:uI8Bdm81s0esVWbWrTHcjFDFKNOa9aB7rI1vud1hO84= -cloud.google.com/go/assuredworkloads v1.11.4 h1:FsLSkmYYeNuzDm8L4YPfLWV+lQaUrJmH5OuD37t1k20= -cloud.google.com/go/automl v1.13.4 h1:i9tOKXX+1gE7+rHpWKjiuPfGBVIYoWvLNIGpWgPtF58= -cloud.google.com/go/baremetalsolution v1.2.3 h1:oQiFYYCe0vwp7J8ZmF6siVKEumWtiPFJMJcGuyDVRUk= -cloud.google.com/go/batch v1.7.0 h1:AxuSPoL2fWn/rUyvWeNCNd0V2WCr+iHRCU9QO1PUmpY= -cloud.google.com/go/batch v1.7.0/go.mod h1:J64gD4vsNSA2O5TtDB5AAux3nJ9iV8U3ilg3JDBYejU= -cloud.google.com/go/beyondcorp v1.0.3 h1:VXf9SnrnSmj2BF2cHkoTHvOUp8gjsz1KJFOMW7czdsY= -cloud.google.com/go/bigquery v1.57.1 h1:FiULdbbzUxWD0Y4ZGPSVCDLvqRSyCIO6zKV7E2nf5uA= -cloud.google.com/go/billing v1.18.0 h1:GvKy4xLy1zF1XPbwP5NJb2HjRxhnhxjjXxvyZ1S/IAo= -cloud.google.com/go/billing v1.18.0/go.mod h1:5DOYQStCxquGprqfuid/7haD7th74kyMBHkjO/OvDtk= -cloud.google.com/go/binaryauthorization v1.8.0 h1:PHS89lcFayWIEe0/s2jTBiEOtqghCxzc7y7bRNlifBs= -cloud.google.com/go/binaryauthorization v1.8.0/go.mod h1:VQ/nUGRKhrStlGr+8GMS8f6/vznYLkdK5vaKfdCIpvU= -cloud.google.com/go/certificatemanager v1.7.4 h1:5YMQ3Q+dqGpwUZ9X5sipsOQ1fLPsxod9HNq0+nrqc6I= -cloud.google.com/go/channel v1.17.3 h1:Rd4+fBrjiN6tZ4TR8R/38elkyEkz6oogGDr7jDyjmMY= -cloud.google.com/go/cloudbuild v1.15.0 h1:9IHfEMWdCklJ1cwouoiQrnxmP0q3pH7JUt8Hqx4Qbck= -cloud.google.com/go/clouddms v1.7.3 h1:xe/wJKz55VO1+L891a1EG9lVUgfHr9Ju/I3xh1nwF84= -cloud.google.com/go/cloudtasks v1.12.4 h1:5xXuFfAjg0Z5Wb81j2GAbB3e0bwroCeSF+5jBn/L650= -cloud.google.com/go/contactcenterinsights v1.12.1 h1:EiGBeejtDDtr3JXt9W7xlhXyZ+REB5k2tBgVPVtmNb0= -cloud.google.com/go/contactcenterinsights v1.12.1/go.mod h1:HHX5wrz5LHVAwfI2smIotQG9x8Qd6gYilaHcLLLmNis= -cloud.google.com/go/container v1.29.0 h1:jIltU529R2zBFvP8rhiG1mgeTcnT27KhU0H/1d6SQRg= -cloud.google.com/go/container v1.29.0/go.mod h1:b1A1gJeTBXVLQ6GGw9/9M4FG94BEGsqJ5+t4d/3N7O4= -cloud.google.com/go/containeranalysis v0.11.3 h1:5rhYLX+3a01drpREqBZVXR9YmWH45RnML++8NsCtuD8= -cloud.google.com/go/datacatalog v1.19.0 h1:rbYNmHwvAOOwnW2FPXYkaK3Mf1MmGqRzK0mMiIEyLdo= -cloud.google.com/go/dataflow v0.9.4 h1:7VmCNWcPJBS/srN2QnStTB6nu4Eb5TMcpkmtaPVhRt4= -cloud.google.com/go/dataform v0.9.1 h1:jV+EsDamGX6cE127+QAcCR/lergVeeZdEQ6DdrxW3sQ= -cloud.google.com/go/datafusion v1.7.4 h1:Q90alBEYlMi66zL5gMSGQHfbZLB55mOAg03DhwTTfsk= -cloud.google.com/go/datalabeling v0.8.4 h1:zrq4uMmunf2KFDl/7dS6iCDBBAxBnKVDyw6+ajz3yu0= -cloud.google.com/go/dataplex v1.13.0 h1:ACVOuxwe7gP0SqEso9SLyXbcZNk5l8hjcTX+XLntI5s= -cloud.google.com/go/dataplex v1.13.0/go.mod h1:mHJYQQ2VEJHsyoC0OdNyy988DvEbPhqFs5OOLffLX0c= -cloud.google.com/go/dataproc v1.12.0 h1:W47qHL3W4BPkAIbk4SWmIERwsWBaNnWm0P2sdx3YgGU= -cloud.google.com/go/dataproc/v2 v2.3.0 h1:tTVP9tTxmc8fixxOd/8s6Q6Pz/+yzn7r7XdZHretQH0= -cloud.google.com/go/dataqna v0.8.4 h1:NJnu1kAPamZDs/if3bJ3+Wb6tjADHKL83NUWsaIp2zg= -cloud.google.com/go/datastore v1.15.0 h1:0P9WcsQeTWjuD1H14JIY7XQscIPQ4Laje8ti96IC5vg= -cloud.google.com/go/datastream v1.10.3 h1:Z2sKPIB7bT2kMW5Uhxy44ZgdJzxzE5uKjavoW+EuHEE= -cloud.google.com/go/deploy v1.16.0 h1:5OVjzm8MPC5kP+Ywbs0mdE0O7AXvAUXksSyHAyMFyMg= -cloud.google.com/go/deploy v1.16.0/go.mod h1:e5XOUI5D+YGldyLNZ21wbp9S8otJbBE4i88PtO9x/2g= -cloud.google.com/go/dialogflow v1.47.0 h1:tLCWad8HZhlyUNfDzDP5m+oH6h/1Uvw/ei7B9AnsWMk= -cloud.google.com/go/dialogflow v1.47.0/go.mod h1:mHly4vU7cPXVweuB5R0zsYKPMzy240aQdAu06SqBbAQ= -cloud.google.com/go/dlp v1.11.1 h1:OFlXedmPP/5//X1hBEeq3D9kUVm9fb6ywYANlpv/EsQ= -cloud.google.com/go/documentai v1.23.6 h1:0/S3AhS23+0qaFe3tkgMmS3STxgDgmE1jg4TvaDOZ9g= -cloud.google.com/go/documentai v1.23.6/go.mod h1:ghzBsyVTiVdkfKaUCum/9bGBEyBjDO4GfooEcYKhN+g= -cloud.google.com/go/domains v0.9.4 h1:ua4GvsDztZ5F3xqjeLKVRDeOvJshf5QFgWGg1CKti3A= -cloud.google.com/go/edgecontainer v1.1.4 h1:Szy3Q/N6bqgQGyxqjI+6xJZbmvPvnFHp3UZr95DKcQ0= -cloud.google.com/go/errorreporting v0.3.0 h1:kj1XEWMu8P0qlLhm3FwcaFsUvXChV/OraZwA70trRR0= -cloud.google.com/go/essentialcontacts v1.6.5 h1:S2if6wkjR4JCEAfDtIiYtD+sTz/oXjh2NUG4cgT1y/Q= -cloud.google.com/go/eventarc v1.13.3 h1:+pFmO4eu4dOVipSaFBLkmqrRYG94Xl/TQZFOeohkuqU= -cloud.google.com/go/filestore v1.8.0 h1:/+wUEGwk3x3Kxomi2cP5dsR8+SIXxo7M0THDjreFSYo= -cloud.google.com/go/firestore v1.14.0 h1:8aLcKnMPoldYU3YHgu4t2exrKhLQkqaXAGqT0ljrFVw= -cloud.google.com/go/functions v1.15.4 h1:ZjdiV3MyumRM6++1Ixu6N0VV9LAGlCX4AhW6Yjr1t+U= -cloud.google.com/go/gaming v1.10.1 h1:5qZmZEWzMf8GEFgm9NeC3bjFRpt7x4S6U7oLbxaf7N8= -cloud.google.com/go/gkebackup v1.3.4 h1:KhnOrr9A1tXYIYeXKqCKbCI8TL2ZNGiD3dm+d7BDUBg= -cloud.google.com/go/gkeconnect v0.8.4 h1:1JLpZl31YhQDQeJ98tK6QiwTpgHFYRJwpntggpQQWis= -cloud.google.com/go/gkehub v0.14.4 h1:J5tYUtb3r0cl2mM7+YHvV32eL+uZQ7lONyUZnPikCEo= -cloud.google.com/go/gkemulticloud v1.0.3 h1:NmJsNX9uQ2CT78957xnjXZb26TDIMvv+d5W2vVUt0Pg= -cloud.google.com/go/grafeas v0.3.0 h1:oyTL/KjiUeBs9eYLw/40cpSZglUC+0F7X4iu/8t7NWs= -cloud.google.com/go/gsuiteaddons v1.6.4 h1:uuw2Xd37yHftViSI8J2hUcCS8S7SH3ZWH09sUDLW30Q= -cloud.google.com/go/iap v1.9.3 h1:M4vDbQ4TLXdaljXVZSwW7XtxpwXUUarY2lIs66m0aCM= -cloud.google.com/go/ids v1.4.4 h1:VuFqv2ctf/A7AyKlNxVvlHTzjrEvumWaZflUzBPz/M4= -cloud.google.com/go/iot v1.7.4 h1:m1WljtkZnvLTIRYW1YTOv5A6H1yKgLHR6nU7O8yf27w= -cloud.google.com/go/language v1.12.2 h1:zg9uq2yS9PGIOdc0Kz/l+zMtOlxKWonZjjo5w5YPG2A= -cloud.google.com/go/lifesciences v0.9.4 h1:rZEI/UxcxVKEzyoRS/kdJ1VoolNItRWjNN0Uk9tfexg= -cloud.google.com/go/logging v1.8.1 h1:26skQWPeYhvIasWKm48+Eq7oUqdcdbwsCVwz5Ys0FvU= -cloud.google.com/go/longrunning v0.5.4 h1:w8xEcbZodnA2BbW6sVirkkoC+1gP8wS57EUUgGS0GVg= -cloud.google.com/go/managedidentities v1.6.4 h1:SF/u1IJduMqQQdJA4MDyivlIQ4SrV5qAawkr/ZEREkY= -cloud.google.com/go/maps v1.6.2 h1:WxxLo//b60nNFESefLgaBQevu8QGUmRV3+noOjCfIHs= -cloud.google.com/go/maps v1.6.2/go.mod h1:4+buOHhYXFBp58Zj/K+Lc1rCmJssxxF4pJ5CJnhdz18= -cloud.google.com/go/mediatranslation v0.8.4 h1:VRCQfZB4s6jN0CSy7+cO3m4ewNwgVnaePanVCQh/9Z4= -cloud.google.com/go/memcache v1.10.4 h1:cdex/ayDd294XBj2cGeMe6Y+H1JvhN8y78B9UW7pxuQ= -cloud.google.com/go/metastore v1.13.3 h1:94l/Yxg9oBZjin2bzI79oK05feYefieDq0o5fjLSkC8= -cloud.google.com/go/monitoring v1.16.3 h1:mf2SN9qSoBtIgiMA4R/y4VADPWZA7VCNJA079qLaZQ8= -cloud.google.com/go/networkconnectivity v1.14.3 h1:e9lUkCe2BexsqsUc2bjV8+gFBpQa54J+/F3qKVtW+wA= -cloud.google.com/go/networkmanagement v1.9.3 h1:HsQk4FNKJUX04k3OI6gUsoveiHMGvDRqlaFM2xGyvqU= -cloud.google.com/go/networksecurity v0.9.4 h1:947tNIPnj1bMGTIEBo3fc4QrrFKS5hh0bFVsHmFm4Vo= -cloud.google.com/go/notebooks v1.11.2 h1:eTOTfNL1yM6L/PCtquJwjWg7ZZGR0URFaFgbs8kllbM= -cloud.google.com/go/optimization v1.6.2 h1:iFsoexcp13cGT3k/Hv8PA5aK+FP7FnbhwDO9llnruas= -cloud.google.com/go/orchestration v1.8.4 h1:kgwZ2f6qMMYIVBtUGGoU8yjYWwMTHDanLwM/CQCFaoQ= -cloud.google.com/go/orgpolicy v1.11.4 h1:RWuXQDr9GDYhjmrredQJC7aY7cbyqP9ZuLbq5GJGves= -cloud.google.com/go/osconfig v1.12.4 h1:OrRCIYEAbrbXdhm13/JINn9pQchvTTIzgmOCA7uJw8I= -cloud.google.com/go/oslogin v1.12.2 h1:NP/KgsD9+0r9hmHC5wKye0vJXVwdciv219DtYKYjgqE= -cloud.google.com/go/phishingprotection v0.8.4 h1:sPLUQkHq6b4AL0czSJZ0jd6vL55GSTHz2B3Md+TCZI0= -cloud.google.com/go/policytroubleshooter v1.10.2 h1:sq+ScLP83d7GJy9+wpwYJVnY+q6xNTXwOdRIuYjvHT4= -cloud.google.com/go/privatecatalog v0.9.4 h1:Vo10IpWKbNvc/z/QZPVXgCiwfjpWoZ/wbgful4Uh/4E= -cloud.google.com/go/pubsub v1.33.0 h1:6SPCPvWav64tj0sVX/+npCBKhUi/UjJehy9op/V3p2g= -cloud.google.com/go/pubsublite v1.8.1 h1:pX+idpWMIH30/K7c0epN6V703xpIcMXWRjKJsz0tYGY= -cloud.google.com/go/recaptchaenterprise v1.3.1 h1:u6EznTGzIdsyOsvm+Xkw0aSuKFXQlyjGE9a4exk6iNQ= -cloud.google.com/go/recaptchaenterprise/v2 v2.9.0 h1:Zrd4LvT9PaW91X/Z13H0i5RKEv9suCLuk8zp+bfOpN4= -cloud.google.com/go/recaptchaenterprise/v2 v2.9.0/go.mod h1:Dak54rw6lC2gBY8FBznpOCAR58wKf+R+ZSJRoeJok4w= -cloud.google.com/go/recommendationengine v0.8.4 h1:JRiwe4hvu3auuh2hujiTc2qNgPPfVp+Q8KOpsXlEzKQ= -cloud.google.com/go/recommender v1.11.3 h1:VndmgyS/J3+izR8V8BHa7HV/uun8//ivQ3k5eVKKyyM= -cloud.google.com/go/redis v1.14.1 h1:J9cEHxG9YLmA9o4jTSvWt/RuVEn6MTrPlYSCRHujxDQ= -cloud.google.com/go/resourcemanager v1.9.4 h1:JwZ7Ggle54XQ/FVYSBrMLOQIKoIT/uer8mmNvNLK51k= -cloud.google.com/go/resourcesettings v1.6.4 h1:yTIL2CsZswmMfFyx2Ic77oLVzfBFoWBYgpkgiSPnC4Y= -cloud.google.com/go/retail v1.14.4 h1:geqdX1FNqqL2p0ADXjPpw8lq986iv5GrVcieTYafuJQ= -cloud.google.com/go/run v1.3.3 h1:qdfZteAm+vgzN1iXzILo3nJFQbzziudkJrvd9wCf3FQ= -cloud.google.com/go/scheduler v1.10.5 h1:eMEettHlFhG5pXsoHouIM5nRT+k+zU4+GUvRtnxhuVI= -cloud.google.com/go/secretmanager v1.11.4 h1:krnX9qpG2kR2fJ+u+uNyNo+ACVhplIAS4Pu7u+4gd+k= -cloud.google.com/go/security v1.15.4 h1:sdnh4Islb1ljaNhpIXlIPgb3eYj70QWgPVDKOUYvzJc= -cloud.google.com/go/securitycenter v1.24.3 h1:crdn2Z2rFIy8WffmmhdlX3CwZJusqCiShtnrGFRwpeE= -cloud.google.com/go/securitycenter v1.24.3/go.mod h1:l1XejOngggzqwr4Fa2Cn+iWZGf+aBLTXtB/vXjy5vXM= -cloud.google.com/go/servicecontrol v1.11.1 h1:d0uV7Qegtfaa7Z2ClDzr9HJmnbJW7jn0WhZ7wOX6hLE= -cloud.google.com/go/servicedirectory v1.11.3 h1:5niCMfkw+jifmFtbBrtRedbXkJm3fubSR/KHbxSJZVM= -cloud.google.com/go/servicemanagement v1.8.0 h1:fopAQI/IAzlxnVeiKn/8WiV6zKndjFkvi+gzu+NjywY= -cloud.google.com/go/serviceusage v1.6.0 h1:rXyq+0+RSIm3HFypctp7WoXxIA563rn206CfMWdqXX4= -cloud.google.com/go/shell v1.7.4 h1:nurhlJcSVFZneoRZgkBEHumTYf/kFJptCK2eBUq/88M= -cloud.google.com/go/spanner v1.53.1 h1:xNmE0SXMSxNBuk7lRZ5G/S+A49X91zkSTt7Jn5Ptlvw= -cloud.google.com/go/spanner v1.53.1/go.mod h1:liG4iCeLqm5L3fFLU5whFITqP0e0orsAW1uUSrd4rws= -cloud.google.com/go/speech v1.21.0 h1:qkxNao58oF8ghAHE1Eghen7XepawYEN5zuZXYWaUTA4= -cloud.google.com/go/storagetransfer v1.10.3 h1:YM1dnj5gLjfL6aDldO2s4GeU8JoAvH1xyIwXre63KmI= -cloud.google.com/go/talent v1.6.5 h1:LnRJhhYkODDBoTwf6BeYkiJHFw9k+1mAFNyArwZUZAs= -cloud.google.com/go/texttospeech v1.7.4 h1:ahrzTgr7uAbvebuhkBAAVU6kRwVD0HWsmDsvMhtad5Q= -cloud.google.com/go/tpu v1.6.4 h1:XIEH5c0WeYGaVy9H+UueiTaf3NI6XNdB4/v6TFQJxtE= -cloud.google.com/go/trace v1.10.4 h1:2qOAuAzNezwW3QN+t41BtkDJOG42HywL73q8x/f6fnM= -cloud.google.com/go/translate v1.9.3 h1:t5WXTqlrk8VVJu/i3WrYQACjzYJiff5szARHiyqqPzI= -cloud.google.com/go/video v1.20.3 h1:Xrpbm2S9UFQ1pZEeJt9Vqm5t2T/z9y/M3rNXhFoo8Is= -cloud.google.com/go/videointelligence v1.11.4 h1:YS4j7lY0zxYyneTFXjBJUj2r4CFe/UoIi/PJG0Zt/Rg= -cloud.google.com/go/vision v1.2.0 h1:/CsSTkbmO9HC8iQpxbK8ATms3OQaX3YQUeTMGCxlaK4= -cloud.google.com/go/vision/v2 v2.7.5 h1:T/ujUghvEaTb+YnFY/jiYwVAkMbIC8EieK0CJo6B4vg= -cloud.google.com/go/vmmigration v1.7.4 h1:qPNdab4aGgtaRX+51jCOtJxlJp6P26qua4o1xxUDjpc= -cloud.google.com/go/vmwareengine v1.0.3 h1:WY526PqM6QNmFHSqe2sRfK6gRpzWjmL98UFkql2+JDM= -cloud.google.com/go/vpcaccess v1.7.4 h1:zbs3V+9ux45KYq8lxxn/wgXole6SlBHHKKyZhNJoS+8= -cloud.google.com/go/webrisk v1.9.4 h1:iceR3k0BCRZgf2D/NiKviVMFfuNC9LmeNLtxUFRB/wI= -cloud.google.com/go/websecurityscanner v1.6.4 h1:5Gp7h5j7jywxLUp6NTpjNPkgZb3ngl0tUSw6ICWvtJQ= -cloud.google.com/go/workflows v1.12.3 h1:qocsqETmLAl34mSa01hKZjcqAvt699gaoFbooGGMvaM= -contrib.go.opencensus.io/exporter/aws v0.0.0-20200617204711-c478e41e60e9 h1:yxE46rQA0QaqPGqN2UnwXvgCrRqtjR1CsGSWVTRjvv4= -contrib.go.opencensus.io/exporter/prometheus v0.4.2 h1:sqfsYl5GIY/L570iT+l93ehxaWJs2/OwXtiWwew3oAg= -contrib.go.opencensus.io/exporter/prometheus v0.4.2/go.mod h1:dvEHbiKmgvbr5pjaF9fpw1KeYcjrnC1J8B+JKjsZyRQ= -contrib.go.opencensus.io/exporter/stackdriver v0.13.10 h1:a9+GZPUe+ONKUwULjlEOucMMG0qfSCCenlji0Nhqbys= -contrib.go.opencensus.io/integrations/ocsql v0.1.7 h1:G3k7C0/W44zcqkpRSFyjU9f6HZkbwIrL//qqnlqWZ60= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9 h1:VpgP7xuJadIUuKccphEpTJnWhS2jkQyMt6Y7pJCD7fY= -docker.io/go-docker v1.0.0 h1:VdXS/aNYQxyA9wdLD5z8Q8Ro688/hG8HzKxYVEVbE6s= -docker.io/go-docker v1.0.0/go.mod h1:7tiAn5a0LFmjbPDbyTPOaTTOuG1ZRNXdPA6RvKY+fpY= -filippo.io/edwards25519 v1.0.0 h1:0wAIcmJUqRdI8IJ/3eGi5/HwXZWPujYXXlkrQogz0Ek= -filippo.io/edwards25519 v1.0.0/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= -gioui.org v0.0.0-20210308172011-57750fc8a0a6 h1:K72hopUosKG3ntOPNG4OzzbuhxGuVf06fa2la1/H/Ho= -git.sr.ht/~sbinet/gg v0.3.1 h1:LNhjNn8DerC8f9DHLz6lS0YYul/b602DUxDgGkd/Aik= -github.com/99designs/basicauth-go v0.0.0-20160802081356-2a93ba0f464d h1:j6oB/WPCigdOkxtuPl1VSIiLpy7Mdsu6phQffbF19Ng= -github.com/99designs/httpsignatures-go v0.0.0-20170731043157-88528bf4ca7e h1:rl2Aq4ZODqTDkeSqQBy+fzpZPamacO1Srp8zq7jf2Sc= -github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9 h1:HD8gA2tkByhMAwYaFAX9w2l7vxvBQ5NMoxDrkhqhtn4= -github.com/Azure/azure-amqp-common-go/v3 v3.2.2 h1:CJpxNAGxP7UBhDusRUoaOn0uOorQyAYhQYLnNgkRhlY= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal v1.1.2 h1:mLY+pNLjCUeKhgnAJWAKhEUQM+RJQo2H1fuGSw1Ky1E= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.0.0 h1:ECsQtyERDVz3NP3kvDOTLvbQhqWp/x9EsGKtb4ogUr8= -github.com/Azure/azure-service-bus-go v0.11.5 h1:EVMicXGNrSX+rHRCBgm/TRQ4VUZ1m3yAYM/AB2R/SOs= -github.com/Azure/go-amqp v0.16.4 h1:/1oIXrq5zwXLHaoYDliJyiFjJSpJZMWGgtMX9e0/Z30= -github.com/Azure/go-autorest/autorest/azure/auth v0.5.11 h1:P6bYXFoao05z5uhOQzbC3Qd8JqF3jUoocoTeIxkp2cA= -github.com/Azure/go-autorest/autorest/azure/auth v0.5.11/go.mod h1:84w/uV8E37feW2NCJ08uT9VBfjfUHpgLVnG2InYD6cg= -github.com/Azure/go-autorest/autorest/azure/cli v0.4.5 h1:0W/yGmFdTIT77fvdlGZ0LMISoLHFJ7Tx4U0yeB+uFs4= -github.com/Azure/go-autorest/autorest/azure/cli v0.4.5/go.mod h1:ADQAXrkgm7acgWVUNamOgh8YNrv4p27l3Wc55oVfpzg= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 h1:1BDTz0u9nC3//pOCMdNH+CiXJVYJh5UQNCOBG7jbELc= -github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 h1:sR+/8Yb4slttB4vD+b9btVEnWgL3Q00OBTzVT8B9C0c= -github.com/CloudyKit/jet/v3 v3.0.0 h1:1PwO5w5VCtlUUl+KTOBsTGZlhjWkcybsGaAau52tOy8= -github.com/DATA-DOG/go-sqlmock v1.3.3 h1:CWUqKXe0s8A2z6qCgkP4Kru7wC11YoAnoupUKFDnH08= -github.com/DataDog/datadog-go v4.0.0+incompatible h1:Dq8Dr+4sV1gBO1sHDWdW+4G+PdsA+YSJOK925MxrrCY= -github.com/GoogleCloudPlatform/cloudsql-proxy v1.29.0 h1:YNu23BtH0PKF+fg3ykSorCp6jSTjcEtfnYLzbmcjVRA= -github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c h1:RGWPOewvKIROun94nF7v2cua9qP+thov/7M50KEoeSU= -github.com/Joker/hpp v1.0.0 h1:65+iuJYdRXv/XyN62C1uEmmOx3432rNG/rKlX6V7Kkc= -github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible h1:1G1pk05UrOh0NlF1oeaaix1x8XzrfjIDK47TY0Zehcw= -github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= -github.com/OneOfOne/xxhash v1.2.6 h1:U68crOE3y3MPttCMQGywZOLrTeF5HHJ3/vDBCJn9/bA= -github.com/OneOfOne/xxhash v1.2.6/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q= -github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= -github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= -github.com/RaveNoX/go-jsoncommentstrip v1.0.0 h1:t527LHHE3HmiHrq74QMpNPZpGCIJzTx+apLkMKt4HC0= -github.com/RoaringBitmap/gocroaring v0.4.0 h1:5nufXUgWpBEUNEJXw7926YAA58ZAQRpWPrQV1xCoSjc= -github.com/RoaringBitmap/real-roaring-datasets v0.0.0-20190726190000-eb7c87156f76 h1:ZYlhPbqQFU+AHfgtCdHGDTtRW1a8geZyiE8c6Q+Sl1s= -github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398 h1:WDC6ySpJzbxGWFh4aMxFFC28wwGp5pEuoTtvA4q/qQ4= -github.com/Shopify/sarama v1.38.1 h1:lqqPUPQZ7zPqYlWpTh+LQ9bhYNu2xJL6k1SJN4WVe2A= -github.com/Shopify/sarama v1.38.1/go.mod h1:iwv9a67Ha8VNa+TifujYoWGxWnu2kNVAQdSdZ4X2o5g= -github.com/Shopify/toxiproxy v2.1.4+incompatible h1:TKdv8HiTLgE5wdJuEML90aBgNWsokNbMijUGhmcoBJc= -github.com/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrdtl/UvroE= -github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5 h1:rFw4nCn9iMW+Vajsk51NtYIcwSTkXr+JGrMd36kTDJw= -github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= -github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9 h1:7kQgkwGRoLzC9K0oyXdJo7nve/bynv/KwUsxbiTlzAM= -github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19 h1:iXUgAaqDcIUGbRoy2TdeofRG/j1zpGRSEmNK05T+bi8= -github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b h1:slYM766cy2nI3BwyRiyQj/Ud48djTMtMebDqepE95rw= -github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY= -github.com/alecthomas/kong v0.2.11 h1:RKeJXXWfg9N47RYfMm0+igkxBCTF4bzbneAxaqid0c4= -github.com/alecthomas/kong v0.2.11/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE= -github.com/alecthomas/participle/v2 v2.1.0 h1:z7dElHRrOEEq45F2TG5cbQihMtNTv8vwldytDj7Wrz4= -github.com/alecthomas/participle/v2 v2.1.0/go.mod h1:Y1+hAs8DHPmc3YUFzqllV+eSQ9ljPTk0ZkPMtEdAx2c= -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= github.com/alicebob/miniredis v2.5.0+incompatible h1:yBHoLpsyjupjz3NL3MhKMVkR41j82Yjf3KFv7ApYzUI= -github.com/alicebob/miniredis v2.5.0+incompatible/go.mod h1:8HZjEj4yU0dwhYHky+DxYx+6BMjkBbe5ONFIF1MXffk= -github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg= -github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40 h1:q4dksr6ICHXqG5hm0ZW5IHyeEJXoIJSOZeBLmWPNeIQ= -github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40/go.mod h1:Q7yQnSMnLvcXlZ8RV+jwz/6y1rQTqbX6C82SndT52Zs= -github.com/apache/arrow/go/v10 v10.0.1 h1:n9dERvixoC/1JjDmBcs9FPaEryoANa2sCgVFo6ez9cI= -github.com/apache/arrow/go/v11 v11.0.0 h1:hqauxvFQxww+0mEU/2XHG6LT7eZternCZq+A5Yly2uM= -github.com/apache/arrow/go/v12 v12.0.0 h1:xtZE63VWl7qLdB0JObIXvvhGjoVNrQ9ciIHG2OK5cmc= -github.com/apache/arrow/go/v13 v13.0.0 h1:kELrvDQuKZo8csdWYqBQfyi431x6Zs/YJTEgUuSVcWk= -github.com/apache/arrow/go/v13 v13.0.0/go.mod h1:W69eByFNO0ZR30q1/7Sr9d83zcVZmF2MiP3fFYAWJOc= -github.com/apache/thrift v0.18.1 h1:lNhK/1nqjbwbiOPDBPFJVKxgDEGSepKuTh6OLiXW8kg= -github.com/apache/thrift v0.18.1/go.mod h1:rdQn/dCcDKEWjjylUeueum4vQEjG2v8v2PqriUnbr+I= -github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3 h1:ZSTrOEhiM5J5RFxEaFvMZVEAM1KvT1YzbEOwB2EAGjA= -github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM= -github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e h1:QEF07wC0T1rKkctt1RINW/+RMTVmiwxETico2l3gxJA= -github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6 h1:G1bPvciwNyF7IUmKXNt9Ak3m6u9DE1rF+RmtIkBpVdA= -github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a h1:pv34s756C4pEXnjgPfGYgdhg/ZdajGhyOvzx8k+23nw= -github.com/aws/aws-lambda-go v1.13.3 h1:SuCy7H3NLyp+1Mrfp+m80jcbi9KYWAs9/BXwppwRDzY= -github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.8.1 h1:w/fPGB0t5rWwA43mux4e9ozFSH5zF1moQemlA131PWc= -github.com/aws/aws-sdk-go-v2/service/kms v1.16.3 h1:nUP29LA4GZZPihNSo5ZcF4Rl73u+bN5IBRnrQA0jFK4= -github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.4 h1:EmIEXOjAdXtxa2OGM1VAajZV/i06Q8qd4kBpJd9/p1k= -github.com/aws/aws-sdk-go-v2/service/sns v1.17.4 h1:7TdmoJJBwLFyakXjfrGztejwY5Ie1JEto7YFfznCmAw= -github.com/aws/aws-sdk-go-v2/service/sqs v1.18.3 h1:uHjK81fESbGy2Y9lspub1+C6VN5W2UXTDo2A/Pm4G0U= -github.com/aws/aws-sdk-go-v2/service/ssm v1.24.1 h1:zc1YLcknvxdW/i1MuJKmEnFB2TNkOfguuQaGRvJXPng= -github.com/aws/aws-xray-sdk-go v0.9.4 h1:3mtFCrgFR5IefmWFV5pscHp9TTyOWuqaIKJIY0d1Y4g= -github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible h1:Ppm0npCCsmuR9oQaBtRuZcmILVE74aXE+AmrJj8L2ns= -github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= -github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 h1:mXoPYz/Ul5HYEDvkta6I8/rnYM5gSdSV2tJ6XbZuEtY= -github.com/bmatcuk/doublestar/v2 v2.0.3 h1:D6SI8MzWzXXBXZFS87cFL6s/n307lEU+thM2SUnge3g= -github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= -github.com/boombuler/barcode v1.0.1 h1:NDBbPmhS+EqABEs5Kg3n/5ZNjy73Pz7SIV+KCeqyXcs= -github.com/bwesterb/go-ristretto v1.2.3 h1:1w53tCkGhCQ5djbat3+MH0BAQ5Kfgbt56UZQ/JMzngw= -github.com/casbin/casbin/v2 v2.37.0 h1:/poEwPSovi4bTOcP752/CsTQiRz2xycyVKFG7GUhbDw= github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= -github.com/cenkalti/backoff/v3 v3.0.0 h1:ske+9nBpD9qZsTBoF41nW5L+AIuFBKMeze18XQ3eG1c= -github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= -github.com/chromedp/chromedp v0.9.2 h1:dKtNz4kApb06KuSXoTQIyUC2TrA0fhGDwNZf3bcgfKw= -github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic= -github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= -github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= -github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= -github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575 h1:kHaBemcxl8o/pQ5VM1c8PVE1PubbNx3mjUr09OqWGCs= -github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible h1:C29Ae4G5GtYyYMm1aztcyj/J5ckgJm2zwdDajFbx1NY= -github.com/circonus-labs/circonusllhist v0.1.3 h1:TJH+oke8D16535+jHExHj4nQvzlZrj7ug5D7I/orNUA= -github.com/clbanning/mxj v1.8.4 h1:HuhwZtbyvyOw+3Z1AowPkU87JkJUSv751ELWaiTpj8I= -github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec h1:EdRZT3IeKQmfCSrgo8SZ8V3MEnskuJP0wCYNpe+aiXo= -github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI= -github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe h1:QQ3GSy+MqSHxm/d8nCtnAiZdYFd45cYZPs8vOOIYKfk= github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= -github.com/cockroachdb/cockroach-go v0.0.0-20200312223839-f565e4789405 h1:i1XXyBMAGL7NqogtoS6NHQ/IJwCbG0R725hAhEhldOI= -github.com/cockroachdb/datadriven v1.0.2 h1:H9MtNqVoVhvd9nCBwOyDjUEdZCREqbIdCJD93PBm/jA= -github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd h1:qMd81Ts1T2OTKmB4acZcyKaMtRnY5Y44NuXGX2GFJ1w= -github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0 h1:sDMmm+q/3+BukdIpxwO365v/Rbspp2Nt5XntgQRXq8Q= -github.com/codegangsta/negroni v1.0.0 h1:+aYywywx4bnKXWvoWtRfJ91vC59NbEhEY03sZjQhbVY= -github.com/containerd/containerd v1.6.8 h1:h4dOFDwzHmqFEP754PgfgTeVXFnLiRc6kiqC7tplDJs= -github.com/containerd/containerd v1.6.8/go.mod h1:By6p5KqPK0/7/CgO/A6t/Gz+CUYUu2zf1hUaaymVXB0= -github.com/containerd/continuity v0.0.0-20200107194136-26c1120b8d41 h1:kIFnQBO7rQ0XkMe6xEwbybYHBEaWmh/f++laI6Emt7M= -github.com/coreos/bbolt v1.3.2 h1:wZwiHHUieZCquLkDL0B8UhzreNWsPHooDAG3q34zk0s= -github.com/coreos/etcd v3.3.10+incompatible h1:jFneRYjIvLMLhDLCzuTuU4rSJUjRplcJQ7pD7MnhC04= -github.com/coreos/go-etcd v2.0.0+incompatible h1:bXhRBIXoTm9BYHS3gE0TtQuyNZyeEMux2sDi4oo5YOo= -github.com/coreos/go-oidc v2.2.1+incompatible h1:mh48q/BqXqgjVHpy2ZY7WnWAbenxRjsz9N1i1YxjHAk= -github.com/coreos/go-oidc v2.2.1+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f h1:JOrtw2xFKzlg+cbHpyrpLDmnN1HqhBfnX7WDiW7eG2c= -github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f h1:lBNOc5arjvs8E5mO2tbpBpLoyyu8B6e44T7hJy6potg= github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk= -github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= -github.com/crewjam/httperr v0.2.0 h1:b2BfXR8U3AlIHwNeFFvZ+BV1LFvKLlzMjzaTnZMybNo= -github.com/crewjam/httperr v0.2.0/go.mod h1:Jlz+Sg/XqBQhyMjdDiC+GNNRzZTD7x39Gu3pglZ5oH4= -github.com/cristalhq/hedgedhttp v0.9.1 h1:g68L9cf8uUyQKQJwciD0A1Vgbsz+QgCjuB1I8FAsCDs= -github.com/cristalhq/hedgedhttp v0.9.1/go.mod h1:XkqWU6qVMutbhW68NnzjWrGtH8NUx1UfYqGYtHVKIsI= -github.com/cucumber/godog v0.8.1 h1:lVb+X41I4YDreE+ibZ50bdXmySxgRviYFgKY6Aw4XE8= -github.com/cznic/b v0.0.0-20180115125044-35e9bbe41f07 h1:UHFGPvSxX4C4YBApSPvmUfL8tTvWLj2ryqvT9K4Jcuk= -github.com/cznic/fileutil v0.0.0-20180108211300-6a051e75936f h1:7uSNgsgcarNk4oiN/nNkO0J7KAjlsF5Yv5Gf/tFdHas= -github.com/cznic/golex v0.0.0-20170803123110-4ab7c5e190e4 h1:CVAqftqbj+exlab+8KJQrE+kNIVlQfJt58j4GxCMF1s= -github.com/cznic/internal v0.0.0-20180608152220-f44710a21d00 h1:FHpbUtp2K8X53/b4aFNj4my5n+i3x+CQCZWNuHWH/+E= -github.com/cznic/lldb v1.1.0 h1:AIA+ham6TSJ+XkMe8imQ/g8KPzMUVWAwqUQQdtuMsHs= -github.com/cznic/mathutil v0.0.0-20180504122225-ca4c9f2c1369 h1:XNT/Zf5l++1Pyg08/HV04ppB0gKxAqtZQBRYiYrUuYk= -github.com/cznic/ql v1.2.0 h1:lcKp95ZtdF0XkWhGnVIXGF8dVD2X+ClS08tglKtf+ak= -github.com/cznic/sortutil v0.0.0-20150617083342-4c7342852e65 h1:hxuZop6tSoOi0sxFzoGGYdRqNrPubyaIf9KoBG9tPiE= -github.com/cznic/strutil v0.0.0-20171016134553-529a34b1c186 h1:0rkFMAbn5KBKNpJyHQ6Prb95vIKanmAe62KxsrN+sqA= -github.com/cznic/zappy v0.0.0-20160723133515-2533cb5b45cc h1:YKKpTb2BrXN2GYyGaygIdis1vXbE7SSAG9axGWIMClg= -github.com/dave/astrid v0.0.0-20170323122508-8c2895878b14 h1:YI1gOOdmMk3xodBao7fehcvoZsEeOyy/cfhlpCSPgM4= -github.com/dave/brenda v1.1.0 h1:Sl1LlwXnbw7xMhq3y2x11McFu43AjDcwkllxxgZ3EZw= -github.com/dave/courtney v0.3.0 h1:8aR1os2ImdIQf3Zj4oro+lD/L4Srb5VwGefqZ/jzz7U= -github.com/dave/gopackages v0.0.0-20170318123100-46e7023ec56e h1:l99YKCdrK4Lvb/zTupt0GMPfNbncAGf8Cv/t1sYLOg0= -github.com/dave/kerr v0.0.0-20170318121727-bc25dd6abe8e h1:xURkGi4RydhyaYR6PzcyHTueQudxY4LgxN1oYEPJHa0= -github.com/dave/patsy v0.0.0-20210517141501-957256f50cba h1:1o36L4EKbZzazMk8iGC4kXpVnZ6TPxR2mZ9qVKjNNAs= -github.com/dave/rebecca v0.9.1 h1:jxVfdOxRirbXL28vXMvUvJ1in3djwkVKXCq339qhBL0= -github.com/dchest/uniuri v1.2.0 h1:koIcOUdrTIivZgSLhHQvKgqdWZq5d7KdMEWF1Ud6+5g= -github.com/dchest/uniuri v1.2.0/go.mod h1:fSzm4SLHzNZvWLvWJew423PhAzkpNQYq+uNLq4kxhkY= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= github.com/denisenkom/go-mssqldb v0.0.0-20190515213511-eb9f6a1743f3/go.mod h1:zAg7JM8CkOJ43xKXIj7eRO9kmWm/TW578qo+oDO6tuM= -github.com/denisenkom/go-mssqldb v0.12.0 h1:VtrkII767ttSPNRfFekePK3sctr+joXgO58stqQbtUA= github.com/denisenkom/go-mssqldb v0.12.0/go.mod h1:iiK0YP1ZeepvmBQk/QpLEhhTNJgfzrpArPY/aFvc9yU= -github.com/devigned/tab v0.1.1 h1:3mD6Kb1mUOYeLpJvTVSDwSg5ZsfSxfvxGRTxRsJsITA= -github.com/dgraph-io/badger v1.6.0 h1:DshxFxZWXUcO0xX476VJC07Xsr6ZCBVRHKZ93Oh7Evo= -github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= -github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954 h1:RMLoZVzv4GliuWafOuPuQDKSm1SJph7uCRnnS61JAn4= -github.com/dhui/dktest v0.3.0 h1:kwX5a7EkLcjo7VpsPQSYJcKGbXBXdjI9FGjuUj1jn6I= -github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U= -github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 h1:bWDMxwH3px2JBh6AyO7hdCn/PkvCZXii8TGj7sbtEbQ= -github.com/drone/drone-runtime v1.1.0 h1:IsKbwiLY6+ViNBzX0F8PERJVZZcEJm9rgxEh3uZP5IE= -github.com/drone/drone-runtime v1.1.0/go.mod h1:+osgwGADc/nyl40J0fdsf8Z09bgcBZXvXXnLOY48zYs= -github.com/drone/drone-yaml v1.2.3 h1:SWzLmzr8ARhbtw1WsVDENa8WFY2Pi9l0FVMfafVUWz8= -github.com/drone/drone-yaml v1.2.3/go.mod h1:QsqliFK8nG04AHFN9tTn9XJomRBQHD4wcejWW1uz/10= -github.com/drone/funcmap v0.0.0-20211123105308-29742f68a7d1 h1:E8hjIYiEyI+1S2XZSLpMkqT9V8+YMljFNBWrFpuVM3A= -github.com/drone/funcmap v0.0.0-20211123105308-29742f68a7d1/go.mod h1:Hph0/pT6ZxbujnE1Z6/08p5I0XXuOsppqF6NQlGOK0E= -github.com/drone/signal v1.0.0 h1:NrnM2M/4yAuU/tXs6RP1a1ZfxnaHwYkd0kJurA1p6uI= -github.com/eapache/go-resiliency v1.3.0 h1:RRL0nge+cWGlxXbUzJ7yMcq6w2XBEr19dCN6HECGaT0= -github.com/eapache/go-resiliency v1.3.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6iZYSs1ZI+iQho= -github.com/eapache/go-xerial-snappy v0.0.0-20230111030713-bf00bc1b83b6 h1:8yY/I9ndfrgrXUbOGObLHKBR4Fl3nZXwM2c7OYTT8hM= -github.com/eapache/go-xerial-snappy v0.0.0-20230111030713-bf00bc1b83b6/go.mod h1:YvSRo5mw33fLEx1+DlK6L2VV43tJt5Eyel9n9XBcR+0= -github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc= -github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385 h1:clC1lXBpe2kTj2VHdaIu9ajZQe4kcEY9j0NsnDDBZ3o= -github.com/elastic/go-sysinfo v1.1.1 h1:ZVlaLDyhVkDfjwPGU55CQRCRolNpc7P0BbyhhQZQmMI= -github.com/elastic/go-windows v1.0.0 h1:qLURgZFkkrYyTTkvYpsZIgf83AUsdIHfvlJaqaZ7aSY= -github.com/etcd-io/bbolt v1.3.3 h1:gSJmxrs37LgTqR/oyJBWok6k6SvXEUerFTbltIhXkBM= -github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb h1:IT4JYU7k4ikYg1SCxNI1/Tieq/NFvh6dzLdgi7eu0tM= -github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb/go.mod h1:bH6Xx7IW64qjjJq8M2u4dxNaBiDfKK+z/3eGDpXEQhc= -github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072 h1:DddqAaWDpywytcG8w/qoQ5sAN8X12d3Z3koB0C3Rxsc= -github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= -github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8= -github.com/form3tech-oss/jwt-go v3.2.2+incompatible h1:TcekIExNqud5crz4xD2pavyTgWiPvpYe4Xau31I0PRk= -github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= -github.com/franela/goblin v0.0.0-20210519012713-85d372ac71e2 h1:cZqz+yOJ/R64LcKjNQOdARott/jP7BnUQ9Ah7KaZCvw= -github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8 h1:a9ENSRDFBUPkJ5lCgVZh26+ZbGyoVJG7yb5SSzF5H54= -github.com/fsouza/fake-gcs-server v1.7.0 h1:Un0BXUXrRWYSmYyC1Rqm2e2WJfTPyDy/HGMz31emTi8= -github.com/gavv/httpexpect v2.0.0+incompatible h1:1X9kcRshkSKEjNJJxX9Y9mQ5BRfbxU5kORdjhlA1yX8= -github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= -github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8= -github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= -github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8 h1:DujepqpGd1hyOd7aW59XpK7Qymp8iy83xq74fLr21is= -github.com/go-bindata/go-bindata v3.1.1+incompatible h1:tR4f0e4VTO7LK6B2YWyAoVEzG9ByG1wrXB4TL9+jiYg= -github.com/go-check/check v0.0.0-20180628173108-788fd7840127 h1:0gkP6mzaMqkmpcJYCFOLkIBwI7xFExG03bbkOkCvUPI= -github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8= -github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= -github.com/go-fonts/dejavu v0.1.0 h1:JSajPXURYqpr+Cu8U9bt8K+XcACIHWqWrvWCKyeFmVQ= -github.com/go-fonts/latin-modern v0.2.0 h1:5/Tv1Ek/QCr20C6ZOz15vw3g7GELYL98KWr8Hgo+3vk= -github.com/go-fonts/liberation v0.2.0 h1:jAkAWJP4S+OsrPLZM4/eC9iW7CtHy+HBXrEwZXWo5VM= -github.com/go-fonts/stix v0.1.0 h1:UlZlgrvvmT/58o573ot7NFw0vZasZ5I6bcIft/oMdgg= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1 h1:QbL/5oDUmRBzO9/Z7Seo6zf912W/a6Sr4Eu0G/3Jho0= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4 h1:WtGNWLvXpe6ZudgnXrq0barxBImvnnJoMEhXAzcbM0I= -github.com/go-ini/ini v1.25.4 h1:Mujh4R/dH6YL8bxuISne3xX2+qcQ9p0IxKAP6ExWoUo= -github.com/go-kit/kit v0.12.0 h1:e4o3o3IsBfAKQh5Qbbiqyfu97Ku7jrO/JbohvztANh4= -github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81 h1:6zl3BbBhdnMkpSj2YY30qV3gDcVBGtFgVsV3+/i+mKQ= -github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab h1:xveKWz2iaueeTaUgdetzel+U7exyigDYBryyVfV/rZk= -github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= -github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= -github.com/go-pdf/fpdf v0.6.0 h1:MlgtGIfsdMEEQJr2le6b/HNr1ZlQwxyWr77r2aj2U/8= -github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= -github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= -github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= -github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= -github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= -github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJWXmqUsHwfTRRkQ= -github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU= -github.com/gobuffalo/attrs v0.1.0 h1:LY6/rbhPD/hfa+AfviaMXBmZBGb0fGHF12yg7f3dPQA= -github.com/gobuffalo/buffalo v0.13.0 h1:Fyn55HJULJpFPMUNx9lrPK31qvr37+bpNGFbpAOauGI= -github.com/gobuffalo/buffalo-plugins v1.15.0 h1:71I2OqFmlP4p3N9Vl0maq+IPHKuTEUevoeFkLHjgF3k= -github.com/gobuffalo/buffalo-pop v1.0.5 h1:8aXdBlo9MEKFGHyl489+28Jw7Ud59Th1U+5Ayu1wNL0= -github.com/gobuffalo/depgen v0.1.0 h1:31atYa/UW9V5q8vMJ+W6wd64OaaTHUrCUXER358zLM4= -github.com/gobuffalo/envy v1.9.0 h1:eZR0DuEgVLfeIb1zIKt3bT4YovIMf9O9LXQeCZLXpqE= -github.com/gobuffalo/events v1.4.1 h1:OLJIun6wRx4DOW19XoL/AoyjuJltqeOBFH3q8cDvNb8= -github.com/gobuffalo/fizz v1.10.0 h1:I8vad0PnmR+CLjSnZ5L5jlhBm4S88UIGOoZZL3/3e24= -github.com/gobuffalo/flect v0.2.1 h1:GPoRjEN0QObosV4XwuoWvSd5uSiL0N3e91/xqyY4crQ= -github.com/gobuffalo/genny v0.6.0 h1:d7c6d66ZrTHHty01hDX1/TcTWvAJQxRZl885KWX5kHY= -github.com/gobuffalo/genny/v2 v2.0.5 h1:IH0EHcvwKT0MdASzptvkz/ViYBQELTklq1/l8Ot3Q5E= -github.com/gobuffalo/gitgen v0.0.0-20190315122116-cc086187d211 h1:mSVZ4vj4khv+oThUfS+SQU3UuFIZ5Zo6UNcvK8E8Mz8= -github.com/gobuffalo/github_flavored_markdown v1.1.0 h1:8Zzj4fTRl/OP2R7sGerzSf6g2nEJnaBEJe7UAOiEvbQ= -github.com/gobuffalo/gogen v0.2.0 h1:Xx7NCe+/y++eII2aWAFZ09/81MhDCsZwvMzIFJoQRnU= -github.com/gobuffalo/helpers v0.6.1 h1:LLcL4BsiyDQYtMRUUpyFdBFvFXQ6hNYOpwrcYeilVWM= -github.com/gobuffalo/here v0.6.0 h1:hYrd0a6gDmWxBM4TnrGw8mQg24iSVoIkHEk7FodQcBI= -github.com/gobuffalo/httptest v1.0.2 h1:LWp2khlgA697h4BIYWW2aRxvB93jMnBrbakQ/r2KLzs= -github.com/gobuffalo/licenser v1.1.0 h1:xAHoWgZ8vbRkxC8a+SBVL7Y7atJBRZfXM9H9MmPXx1k= -github.com/gobuffalo/logger v1.0.3 h1:YaXOTHNPCvkqqA7w05A4v0k2tCdpr+sgFlgINbQ6gqc= -github.com/gobuffalo/makr v1.1.5 h1:lOlpv2iz0dNa4qse0ZYQgbtT+ybwVxWEAcOZbcPmeYc= -github.com/gobuffalo/mapi v1.2.1 h1:TyfbtQaW7GvS4DXdF1KQOSGrW6L0uiFmGDz+JgEIMbM= -github.com/gobuffalo/meta v0.3.0 h1:F0BFeZuQ1UmsHAVBgsnzolheLmv11t2GQ+53OFOP7lk= -github.com/gobuffalo/mw-basicauth v1.0.3 h1:bCqDBHnByenQitOtFdEtMvlWVgPwODrfZ+nVkgGoJZ8= -github.com/gobuffalo/mw-contenttype v0.0.0-20180802152300-74f5a47f4d56 h1:SUFp8EbFjlKXkvqstoxPWx3nVPV3BSKZTswQNTZFaik= -github.com/gobuffalo/mw-csrf v0.0.0-20180802151833-446ff26e108b h1:A13B4mhcFQcjPJ1GFBrh61B4Qo87fZa82FfTt9LX/QU= -github.com/gobuffalo/mw-forcessl v0.0.0-20180802152810-73921ae7a130 h1:v94+IGhlBro0Lz1gOR3lrdAVSZ0mJF2NxsdppKd7FnI= -github.com/gobuffalo/mw-i18n v0.0.0-20180802152014-e3060b7e13d6 h1:pZhsgF8RXEngHdibuRNOXNk1pL0K9rFa5HOcvURNTQ4= -github.com/gobuffalo/mw-paramlogger v0.0.0-20181005191442-d6ee392ec72e h1:TsmUXyHjj5ReuN1AJjEVukf72J6AfRTF2CfTEaqVLT8= -github.com/gobuffalo/mw-tokenauth v0.0.0-20181001105134-8545f626c189 h1:nhPzONHNGlXZIMFfKm6cWpRSq5oTanRK1qBtfCPBFyE= -github.com/gobuffalo/nulls v0.3.0 h1:yfOsQarm6pD7Crg/VnpI9Odh5nBlO+eDeKRiHYZOsTA= -github.com/gobuffalo/packd v1.0.0 h1:6ERZvJHfe24rfFmA9OaoKBdC7+c9sydrytMg8SdFGBM= -github.com/gobuffalo/packr v1.22.0 h1:/YVd/GRGsu0QuoCJtlcWSVllobs4q3Xvx3nqxTvPyN0= -github.com/gobuffalo/packr/v2 v2.8.0 h1:IULGd15bQL59ijXLxEvA5wlMxsmx/ZkQv9T282zNVIY= -github.com/gobuffalo/plush v3.8.3+incompatible h1:kzvUTnFPhwyfPEsx7U7LI05/IIslZVGnAlMA1heWub8= -github.com/gobuffalo/plush/v4 v4.0.0 h1:ZHdmfr2R7DQ77XzWZK2PGKJOXm9NRy21EZ6Rw7FhuNw= -github.com/gobuffalo/plushgen v0.1.2 h1:s4yAgNdfNMyMQ7o+Is4f1VlH2L1tKosT+m7BF28C8H4= -github.com/gobuffalo/pop v4.13.1+incompatible h1:AhbqPxNOBN/DBb2DBaiBqzOXIBQXxEYzngHHJ+ytP4g= -github.com/gobuffalo/pop/v5 v5.3.1 h1:dJbBPy6e0G0VRjn28md3fk16wpYIBv5iYVQWd0eqmkQ= -github.com/gobuffalo/release v1.7.0 h1:5+6HdlnRQ2anNSOm3GyMvmiCjSIXg3dcJhNA7Eh99dQ= -github.com/gobuffalo/shoulders v1.0.4 h1:Hw7wjvyasJJo+bDsebhpnnizHCxfxQ3C4mmLjEzcdXY= -github.com/gobuffalo/syncx v0.1.0 h1://CNTQ/+VFQizkW24DrBtTBvj8c2+chz5Y7kbboQ2qk= -github.com/gobuffalo/tags v2.1.7+incompatible h1:GUxxh34f9SI4U0Pj3ZqvopO9SlzuqSf+g4ZGSPSszt4= -github.com/gobuffalo/tags/v3 v3.1.0 h1:mzdCYooN2VsLRr8KIAdEZ1lh1Py7JSMsiEGCGata2AQ= -github.com/gobuffalo/uuid v2.0.5+incompatible h1:c5uWRuEnYggYCrT9AJm0U2v1QTG7OVDAvxhj8tIV5Gc= -github.com/gobuffalo/validate v2.0.4+incompatible h1:ZTxozrIw8qQ5nfhShmc4izjYPTsPhfdXTdhXOd5OS9o= -github.com/gobuffalo/validate/v3 v3.2.0 h1:Zrpkz2kuZ4rGXLaO3IHVlwX512/cUWRvNjw46Cjhz2Q= -github.com/gobuffalo/x v0.0.0-20181007152206-913e47c59ca7 h1:N0iqtKwkicU8M2rLirTDJxdwuL8I2/8MjMlEayaNSgE= -github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= -github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= -github.com/gobwas/ws v1.2.1 h1:F2aeBZrm2NDsc7vbovKrWSogd4wvfAxg0FQ89/iqOTk= -github.com/goccy/go-yaml v1.11.0 h1:n7Z+zx8S9f9KgzG6KtQKf+kwqXZlLNR2F6018Dgau54= -github.com/goccy/go-yaml v1.11.0/go.mod h1:H+mJrWtjPTJAHvRbV09MCK9xYwODM+wRTVFFTWckfng= -github.com/gocql/gocql v0.0.0-20190301043612-f6df8288f9b4 h1:vF83LI8tAakwEwvWZtrIEx7pOySacl2TOxx6eXk4ePo= -github.com/godbus/dbus/v5 v5.0.4 h1:9349emZab16e7zQvpmsbtjc18ykshndd8y2PG3sgJbA= -github.com/gofrs/uuid/v3 v3.1.2 h1:V3IBv1oU82x6YIr5txe3azVHgmOKYdyKQTowm9moBlY= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= -github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= -github.com/golang/gddo v0.0.0-20190904175337-72a348e765d2 h1:xisWqjiKEff2B0KfFYGpCqc3M3zdTz+OHQHRc09FeYk= -github.com/gomodule/redigo v1.8.9 h1:Sl3u+2BI/kk+VEatbj0scLdrFhjPmbxOc1myhDP41ws= -github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE= -github.com/google/go-jsonnet v0.18.0 h1:/6pTy6g+Jh1a1I2UMoAODkqELFiVIdOxbNwv0DDzoOg= -github.com/google/go-jsonnet v0.18.0/go.mod h1:C3fTzyVJDslXdiTqw/bTFk7vSGyCtH3MGRbDfvEwGd0= -github.com/google/go-pkcs11 v0.2.1-0.20230907215043-c6f79328ddf9 h1:OF1IPgv+F4NmqmJ98KTjdN97Vs1JxDPB3vbmYzV2dpk= -github.com/google/go-replayers/grpcreplay v1.1.0 h1:S5+I3zYyZ+GQz68OfbURDdt/+cSMqCK1wrvNx7WBzTE= -github.com/google/go-replayers/httpreplay v1.1.1 h1:H91sIMlt1NZzN7R+/ASswyouLJfW0WLW7fhyUFvDEkY= -github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA= -github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= -github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= -github.com/google/subcommands v1.0.1 h1:/eqq+otEXm5vhfBrbREPCSVQbvofip6kIz+mX5TUH7k= -github.com/googleapis/go-type-adapters v1.0.0 h1:9XdMn+d/G57qq1s8dNc5IesGCXHf6V2HZ2JwRxfA2tA= -github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8 h1:tlyzajkF3030q6M8SvmJSemC9DTHL/xaMa18b65+JM4= -github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= -github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= -github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= -github.com/gorilla/pat v0.0.0-20180118222023-199c85a7f6d1 h1:LqbZZ9sNMWVjeXS4NN5oVvhMjDyLhmA1LG86oSo+IqY= -github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= -github.com/gorilla/sessions v1.1.3 h1:uXoZdcdA5XdXF3QzuSlheVRUvjl+1rKY7zBXL68L9RU= -github.com/gotestyourself/gotestyourself v2.2.0+incompatible h1:AQwinXlbQR2HvPjQZOmDhRqsv5mZf+Jb1RnSLxcqZcI= -github.com/grafana/e2e v0.1.1-0.20221018202458-cffd2bb71c7b h1:Ha+kSIoTutf4ytlVw/SaEclDUloYx0+FXDKJWKhNbE4= -github.com/grafana/e2e v0.1.1-0.20221018202458-cffd2bb71c7b/go.mod h1:3UsooRp7yW5/NJQBlXcTsAHOoykEhNUYXkQ3r6ehEEY= -github.com/grafana/gomemcache v0.0.0-20231023152154-6947259a0586 h1:/of8Z8taCPftShATouOrBVy6GaTTjgQd/VfNiZp/VXQ= -github.com/grafana/gomemcache v0.0.0-20231023152154-6947259a0586/go.mod h1:PGk3RjYHpxMM8HFPhKKo+vve3DdlPUELZLSDEFehPuU= -github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM= -github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 h1:MJG/KsmcqMwFAkh8mTnAwhyKoB+sTAnY4CACC110tbU= -github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645/go.mod h1:6iZfnjpejD4L/4DwD7NryNaJyCQdzwWwH2MWhCA90Kw= -github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8= -github.com/hamba/avro/v2 v2.17.2 h1:6PKpEWzJfNnvBgn7m2/8WYaDOUASxfDU+Jyb4ojDgFY= -github.com/hamba/avro/v2 v2.17.2/go.mod h1:Q9YK+qxAhtVrNqOhwlZTATLgLA8qxG2vtvkhK8fJ7Jo= -github.com/hanwen/go-fuse v1.0.0 h1:GxS9Zrn6c35/BnfiVsZVWmsG803xwE7eVRDvcf/BEVc= -github.com/hanwen/go-fuse/v2 v2.1.0 h1:+32ffteETaLYClUj0a3aHjZ1hOPxxaNEHiZiujuDaek= -github.com/hashicorp/consul/sdk v0.15.0 h1:2qK9nDrr4tiJKRoxPGhm6B7xJjLVIQqkjiab2M4aKjU= -github.com/hashicorp/go-syslog v1.0.0 h1:KaodqZuhUoZereWVIYmpUgZysurB1kBLX2j0MwMrUAE= -github.com/hashicorp/go.net v0.0.1 h1:sNCoNyDEvN1xa+X0baata4RdcpKwcMS6DH+xwfqPgjw= -github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= -github.com/hashicorp/mdns v1.0.4 h1:sY0CMhFmjIPDMlTB+HfymFHCaYLhgifZ0QhjaYKD/UQ= -github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= -github.com/hudl/fargo v1.4.0 h1:ZDDILMbB37UlAVLlWcJ2Iz1XuahZZTDZfdCKeclfq2s= -github.com/hydrogen18/memlistener v0.0.0-20200120041712-dcc25e7acd91 h1:KyZDvZ/GGn+r+Y3DKZ7UOQ/TP4xV6HNkrwiVMB1GnNY= -github.com/iancoleman/strcase v0.2.0 h1:05I4QRnGpI0m37iZQRuskXh+w77mr6Z41lwQzuHLwW0= -github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab h1:BA4a7pe6ZTd9F8kXETBoijjFJ/ntaa//1wiH9BZu4zU= -github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk= -github.com/influxdata/influxdb v1.7.6 h1:8mQ7A/V+3noMGCt/P9pD09ISaiz9XvgCk303UYA3gcs= -github.com/influxdata/influxdb1-client v0.0.0-20200827194710-b269163b24ab h1:HqW4xhhynfjrtEiiSGcQUd6vrK23iMam1FO8rI7mwig= -github.com/inhies/go-bytesize v0.0.0-20201103132853-d0aed0d254f8 h1:RrGCja4Grfz7QM2hw+SUZIYlbHoqBfbvzlWRT3seXB8= -github.com/iris-contrib/blackfriday v2.0.0+incompatible h1:o5sHQHHm0ToHUlAJSTjW9UWicjJSDDauOOQ2AHuIVp4= -github.com/iris-contrib/go.uuid v2.0.0+incompatible h1:XZubAYg61/JwnJNbZilGjf3b3pB80+OQg2qf6c8BfWE= -github.com/iris-contrib/jade v1.1.3 h1:p7J/50I0cjo0wq/VWVCDFd8taPJbuFC+bq23SniRFX0= -github.com/iris-contrib/pongo2 v0.0.1 h1:zGP7pW51oi5eQZMIlGA3I+FHY9/HOQWDB+572yin0to= -github.com/iris-contrib/schema v0.0.1 h1:10g/WnoRR+U+XXHWKBHeNy/+tZmM2kcAVGLOsz+yaDA= -github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0= -github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= -github.com/jackc/fake v0.0.0-20150926172116-812a484cc733 h1:vr3AYkKovP8uR8AvSGGUK1IDqRa5lAAvEkZG1LKaCRc= -github.com/jackc/pgconn v1.11.0 h1:HiHArx4yFbwl91X3qqIHtUFoiIfLNJXCQRsnzkiwwaQ= -github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= -github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc= -github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= -github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A= -github.com/jackc/pgproto3/v2 v2.2.0 h1:r7JypeP2D3onoQTCxWdTpCtJ4D+qpKr0TxvoyMhZ5ns= -github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg= -github.com/jackc/pgtype v1.10.0 h1:ILnBWrRMSXGczYvmkYD6PsYyVFUNLTnIUJHHDLmqk38= -github.com/jackc/pgx v3.6.2+incompatible h1:2zP5OD7kiyR3xzRYMhOcXVvkDZsImVXfj+yIyTQf3/o= -github.com/jackc/pgx/v4 v4.15.0 h1:B7dTkXsdILD3MF987WGGCcg+tvLW6bZJdEcqVFeU//w= -github.com/jackc/puddle v1.2.1 h1:gI8os0wpRXFd4FiAY2dWiqRK037tjj3t7rKFeO4X5iw= -github.com/jackspirou/syscerts v0.0.0-20160531025014-b68f5469dff1 h1:9Xm8CKtMZIXgcopfdWk/qZ1rt0HjMgfMR9nxxSeK6vk= -github.com/jackspirou/syscerts v0.0.0-20160531025014-b68f5469dff1/go.mod h1:zuHl3Hh+e9P6gmBPvcqR1HjkaWHC/csgyskg6IaFKFo= -github.com/jaegertracing/jaeger v1.41.0 h1:vVNky8dP46M2RjGaZ7qRENqylW+tBFay3h57N16Ip7M= -github.com/jaegertracing/jaeger v1.41.0/go.mod h1:SIkAT75iVmA9U+mESGYuMH6UQv6V9Qy4qxo0lwfCQAc= -github.com/jandelgado/gcov2lcov v1.0.4-0.20210120124023-b83752c6dc08 h1:vn5CHED3UxZKIneSxETU9SXXGgsILP8hZHlx+M0u1BQ= -github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc= -github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= -github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= -github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= -github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= -github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg= -github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= -github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= -github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= -github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8= -github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= -github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= -github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= -github.com/jedib0t/go-pretty/v6 v6.2.4 h1:wdaj2KHD2W+mz8JgJ/Q6L/T5dB7kyqEFI16eLq7GEmk= -github.com/jedib0t/go-pretty/v6 v6.2.4/go.mod h1:+nE9fyyHGil+PuISTCrp7avEdo6bqoMwqZnuiK2r2a0= -github.com/jhump/gopoet v0.1.0 h1:gYjOPnzHd2nzB37xYQZxj4EIQNpBrBskRqQQ3q4ZgSg= -github.com/jhump/goprotoc v0.5.0 h1:Y1UgUX+txUznfqcGdDef8ZOVlyQvnV0pKWZH08RmZuo= -github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 h1:rp+c0RAYOWj8l6qbCUTSiRLG/iKnW3K3/QfPPuSsBt4= -github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= -github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o= -github.com/jsternberg/zap-logfmt v1.2.0 h1:1v+PK4/B48cy8cfQbxL4FmmNZrjnIMr2BsnyEmXqv2o= -github.com/jsternberg/zap-logfmt v1.2.0/go.mod h1:kz+1CUmCutPWABnNkOu9hOHKdT2q3TDYCcsFy9hpqb0= -github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d h1:c93kUJDtVAXFEhsCh5jSxyOJmFHuzcihnslQiX8Urwo= -github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= -github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5 h1:PJr+ZMXIecYc1Ey2zucXdR73SMBtgjPgwa31099IMv0= -github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 h1:uC1QfSlInpQF+M0ao65imhwqKnz3Q2z/d8PWZRMQvDM= -github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA= -github.com/karrick/godirwalk v1.16.1 h1:DynhcF+bztK8gooS0+NDJFrdNZjJ3gzVzC545UNA9iw= -github.com/kataras/golog v0.0.10 h1:vRDRUmwacco/pmBAm8geLn8rHEdc+9Z4NAr5Sh7TG/4= -github.com/kataras/iris/v12 v12.1.8 h1:O3gJasjm7ZxpxwTH8tApZsvf274scSGQAUpNe47c37U= -github.com/kataras/neffos v0.0.14 h1:pdJaTvUG3NQfeMbbVCI8JT2T5goPldyyfUB2PJfh1Bs= -github.com/kataras/pio v0.0.2 h1:6NAi+uPJ/Zuid6mrAKlgpbI11/zK/lV4B2rxWaJN98Y= -github.com/kataras/sitemap v0.0.5 h1:4HCONX5RLgVy6G4RkYOV3vKNcma9p236LdGOipJsaFE= -github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= -github.com/kisielk/errcheck v1.5.0 h1:e8esj/e4R+SAOwFwN+n3zr0nYeCyeweozKfO23MvHzY= -github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg= -github.com/klauspost/asmfmt v1.3.2 h1:4Ri7ox3EwapiOjCki+hw14RyKk201CN4rzyCJRFLpK4= github.com/klauspost/cpuid v1.2.1 h1:vJi+O/nMdFt0vqm8NZBI6wzALWdA2X+egi0ogNyrC/w= -github.com/knadh/koanf v1.5.0 h1:q2TSd/3Pyc/5yP9ldIrSdIz26MCcyNQzW0pEAugLPNs= -github.com/knadh/koanf v1.5.0/go.mod h1:Hgyjp4y8v44hpZtPzs7JZfRAW5AhN7KfZcwv1RYggDs= -github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= -github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY= -github.com/kr/pty v1.1.8 h1:AkaSdXYQOWeaO3neb8EM634ahkXXe3jYbVh/F9lq+GI= -github.com/kshvakov/clickhouse v1.3.5 h1:PDTYk9VYgbjPAWry3AoDREeMgOVUFij6bh6IjlloHL0= -github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo= -github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= -github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= -github.com/lestrrat-go/backoff/v2 v2.0.8 h1:oNb5E5isby2kiro9AgdHLv5N5tint1AnDVVf2E2un5A= -github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y= -github.com/lestrrat-go/blackmagic v1.0.0 h1:XzdxDbuQTz0RZZEmdU7cnQxUtFUzgCSPq8RCz4BxIi4= -github.com/lestrrat-go/blackmagic v1.0.0/go.mod h1:TNgH//0vYSs8VXDCfkZLgIrVTTXQELZffUV0tz3MtdQ= -github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= -github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= -github.com/lestrrat-go/iter v1.0.1 h1:q8faalr2dY6o8bV45uwrxq12bRa1ezKrB6oM9FUgN4A= -github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc= -github.com/lestrrat-go/jwx v1.2.25 h1:tAx93jN2SdPvFn08fHNAhqFJazn5mBBOB8Zli0g0otA= -github.com/lestrrat-go/jwx v1.2.25/go.mod h1:zoNuZymNl5lgdcu6P7K6ie2QRll5HVfF4xwxBBK1NxY= -github.com/lestrrat-go/option v1.0.0 h1:WqAWL8kh8VcSoD6xjSH34/1m8yxluXQbDeKNfvFeEO4= -github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= -github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743 h1:143Bb8f8DuGWck/xpNUOckBVYfFbBTnLevfRZ1aVVqo= -github.com/lightstep/lightstep-tracer-go v0.18.1 h1:vi1F1IQ8N7hNWytK9DpJsUfQhGuNSc19z330K6vl4zk= -github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= -github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= -github.com/luna-duclos/instrumentedsql v1.1.3 h1:t7mvC0z1jUt5A0UQ6I/0H31ryymuQRnJcWCiqV3lSAA= -github.com/lyft/protoc-gen-star v0.6.1 h1:erE0rdztuaDq3bpGifD95wfoPrSZc95nGA6tbiNYh6M= -github.com/lyft/protoc-gen-star/v2 v2.0.3 h1:/3+/2sWyXeMLzKd1bX+ixWKgEMsULrIivpDsuaF441o= -github.com/lyft/protoc-gen-validate v0.0.13 h1:KNt/RhmQTOLr7Aj8PsJ7mTronaFyx80mRTT9qF261dA= -github.com/markbates/deplist v1.1.3 h1:/OcV27jxF6aLU+rVGF1RQdT2n+qMlKXeQF6yA0cBQ4k= -github.com/markbates/errx v1.1.0 h1:QDFeR+UP95dO12JgW+tgi2UVfo0V8YBHiUIOaeBPiEI= -github.com/markbates/going v1.0.2 h1:uNQHDDfMRNOUmuxDbPbvatyw4wr4UOSUZkGkdkcip1o= -github.com/markbates/grift v1.0.4 h1:JjTyhlgPtgEnyHNvVn5lk21zWQbWD3cGE0YdyvvbZYg= -github.com/markbates/hmax v1.0.0 h1:yo2N0gBoCnUMKhV/VRLHomT6Y9wUm+oQQENuWJqCdlM= -github.com/markbates/inflect v1.0.4 h1:5fh1gzTFhfae06u3hzHYO9xe3l3v3nW5Pwt3naLTP5g= -github.com/markbates/oncer v1.0.0 h1:E83IaVAHygyndzPimgUYJjbshhDTALZyXxvk9FOlQRY= -github.com/markbates/pkger v0.17.1 h1:/MKEtWqtc0mZvu9OinB9UzVN9iYCwLWuyUv4Bw+PCno= -github.com/markbates/refresh v1.4.10 h1:6EZ/vvVpWiam8OTIhrhfV9cVJR/NvScvcCiqosbTkbA= -github.com/markbates/safe v1.0.1 h1:yjZkbvRM6IzKj9tlu/zMJLS0n/V351OZWRnF3QfaUxI= -github.com/markbates/sigtx v1.0.0 h1:y/xtkBvNPRjD4KeEplf4w9rJVSc23/xl+jXYGowTwy0= -github.com/markbates/willie v1.0.9 h1:394PpHImWjScL9X2VRCDXJAcc77sHsSr3w3sOnL/DVc= -github.com/matryer/moq v0.2.7 h1:RtpiPUM8L7ZSCbSwK+QcZH/E9tgqAkFjKQxsRs25b4w= -github.com/matryer/moq v0.2.7/go.mod h1:kITsx543GOENm48TUAQyJ9+SAvFSr7iGQXPoth/VUBk= -github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= -github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g= -github.com/mediocregopher/radix/v3 v3.4.2 h1:galbPBjIwmyREgwGCfQEN4X8lxbJnKBYurgz+VfcStA= -github.com/microcosm-cc/bluemonday v1.0.2 h1:5lPfLTTAvAbtS0VqT+94yOtFnGfUWYyx0+iToC3Os3s= -github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 h1:AMFGa4R4MiIpspGNG7Z948v4n35fFGB3RR3G/ry4FWs= -github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 h1:+n/aFZefKZp7spd8DFdX7uMikMLXX4oubIzJF4kv/wI= -github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g= -github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= -github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= -github.com/minio/minio-go/v7 v7.0.52 h1:8XhG36F6oKQUDDSuz6dY3rioMzovKjW40W6ANuN0Dps= -github.com/minio/minio-go/v7 v7.0.52/go.mod h1:IbbodHyjUAguneyucUaahv+VMNs/EOTV9du7A7/Z3HU= -github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= -github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= -github.com/mitchellh/cli v1.1.5 h1:OxRIeJXpAMztws/XHlN2vu6imG5Dpq+j61AzAX5fLng= -github.com/mitchellh/gox v0.4.0 h1:lfGJxY7ToLJQjHHwi0EX6uYBdK78egf954SQl13PQJc= -github.com/mitchellh/iochan v1.0.0 h1:C+X3KsSTLFVBr/tK1eYN/vs4rJcvsiLU338UhYPJWeY= -github.com/mithrandie/readline-csvq v1.2.1 h1:4cfeYeVSrqKEWi/1t7CjyhFD2yS6fm+l+oe+WyoSNlI= -github.com/mithrandie/readline-csvq v1.2.1/go.mod h1:ydD9Eyp3/wn8KPSNbKmMZe4RQQauCuxi26yEo4N40dk= -github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5 h1:8Q0qkMVC/MmWkpIdlvZgcv2o2jrlF6zqVOh7W5YHdMA= -github.com/monoculum/formam v0.0.0-20180901015400-4e68be1d79ba h1:FEJJhVHSH+Kyxa5qNe/7dprlZbFcj2TG51OWIouhwls= -github.com/montanaflynn/stats v0.7.0 h1:r3y12KyNxj/Sb/iOE46ws+3mS1+MZca1wlHQFPsY/JU= -github.com/mostynb/go-grpc-compression v1.1.17 h1:N9t6taOJN3mNTTi0wDf4e3lp/G/ON1TP67Pn0vTUA9I= -github.com/mostynb/go-grpc-compression v1.1.17/go.mod h1:FUSBr0QjKqQgoDG/e0yiqlR6aqyXC39+g/hFLDfSsEY= -github.com/moul/http2curl v1.0.0 h1:dRMWoAtb+ePxMlLkrCbAqh4TlPHXvoGUSQ323/9Zahs= -github.com/nakagami/firebirdsql v0.0.0-20190310045651-3c02a58cfed8 h1:P48LjvUQpTReR3TQRbxSeSBsMXzfK0uol7eRcr7VBYQ= -github.com/natessilva/dag v0.0.0-20180124060714-7194b8dcc5c4 h1:dnMxwus89s86tI8rcGVp2HwZzlz7c5o92VOy7dSckBQ= -github.com/nats-io/jwt v1.2.2 h1:w3GMTO969dFg+UOKTmmyuu7IGdusK+7Ytlt//OYH/uU= -github.com/nats-io/jwt/v2 v2.0.3 h1:i/O6cmIsjpcQyWDYNcq2JyZ3/VTF8SJ4JWluI5OhpvI= -github.com/nats-io/nats-server/v2 v2.5.0 h1:wsnVaaXH9VRSg+A2MVg5Q727/CqxnmPLGFQ3YZYKTQg= -github.com/nats-io/nats.go v1.12.1 h1:+0ndxwUPz3CmQ2vjbXdkC1fo3FdiOQDim4gl3Mge8Qo= -github.com/nats-io/nkeys v0.3.0 h1:cgM5tL53EvYRU+2YLXIK0G2mJtK12Ft9oeooSZMA2G8= -github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= -github.com/nicksnyder/go-i18n v1.10.0 h1:5AzlPKvXBH4qBzmZ09Ua9Gipyruv6uApMcrNZdo96+Q= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= -github.com/oklog/oklog v0.3.2 h1:wVfs8F+in6nTBMkA7CbRw+zZMIB7nNM825cM1wuzoTk= -github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU= -github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= -github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88= -github.com/open-telemetry/opentelemetry-collector-contrib/exporter/jaegerexporter v0.74.0 h1:0dve/IbuHfQOnlIBQQwpCxIeMp7uig9DQVuvisWPDRs= -github.com/open-telemetry/opentelemetry-collector-contrib/exporter/jaegerexporter v0.74.0/go.mod h1:bIeSj+SaZdP3CE9Xae+zurdQC6DXX0tPP6NAEVmgtt4= -github.com/open-telemetry/opentelemetry-collector-contrib/exporter/kafkaexporter v0.74.0 h1:MrVOfBTNBe4n/daZjV4yvHZRR0Jg/MOCl/mNwymHwDM= -github.com/open-telemetry/opentelemetry-collector-contrib/exporter/kafkaexporter v0.74.0/go.mod h1:v4H2ATSrKfOTbQnmjCxpvuOjrO/GUURAgey9RzrPsuQ= -github.com/open-telemetry/opentelemetry-collector-contrib/exporter/zipkinexporter v0.74.0 h1:8Kk5g5PKQBUV3idjJy1NWVLLReEzjnB8C1lFgQxZ0TI= -github.com/open-telemetry/opentelemetry-collector-contrib/exporter/zipkinexporter v0.74.0/go.mod h1:UtVfxZGhPU2OvDh7H8o67VKWG9qHAHRNkhmZUWqCvME= -github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal v0.74.0 h1:vU5ZebauzCuYNXFlQaWaYnOfjoOAnS+Sc8+oNWoHkbM= -github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal v0.74.0/go.mod h1:TEu3TnUv1TuyHtjllrUDQ/ImpyD+GrkDejZv4hxl3G8= -github.com/open-telemetry/opentelemetry-collector-contrib/internal/sharedcomponent v0.74.0 h1:COFBWXiWnhRs9x1oYJbDg5cyiNAozp8sycriD9+1/7E= -github.com/open-telemetry/opentelemetry-collector-contrib/internal/sharedcomponent v0.74.0/go.mod h1:cAKlYKU+/8mk6ETOnD+EAi5gpXZjDrGweAB9YTYrv/g= -github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/jaeger v0.74.0 h1:ww1pPXfAM0WHsymQnsN+s4B9DgwQC+GyoBq0t27JV/k= -github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/jaeger v0.74.0/go.mod h1:OpEw7tyCg+iG1ywEgZ03qe5sP/8fhYdtWCMoqA8JCug= -github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/opencensus v0.74.0 h1:0Fh6OjlUB9HlnX90/gGiyyFvnmNBv6inj7bSaVqQ7UQ= -github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/opencensus v0.74.0/go.mod h1:13ekplz1UmvK99Vz2VjSBWPYqoRBEax5LPmA1tFHnhA= -github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/zipkin v0.74.0 h1:A5xoBaMHX1WzLfvlqK6NBXq4XIbuSVJIpec5r6PDE7U= -github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/zipkin v0.74.0/go.mod h1:TJT7HkhFPrJic30Vk4seF/eRk8sa0VQ442Xq/qd+DLY= -github.com/open-telemetry/opentelemetry-collector-contrib/receiver/jaegerreceiver v0.74.0 h1:pWNSPCKD+V4rC+MnZj8uErEbcsYUpEqU3InNYyafAPY= -github.com/open-telemetry/opentelemetry-collector-contrib/receiver/jaegerreceiver v0.74.0/go.mod h1:0lXcDf6LUbtDxZZO3zDbRzMuL7gL1Q0FPOR8/3IBwaQ= -github.com/open-telemetry/opentelemetry-collector-contrib/receiver/kafkareceiver v0.74.0 h1:NWd9+rQTd6pELLf3copo7CEuNgKp90kgyhPozpwax2U= -github.com/open-telemetry/opentelemetry-collector-contrib/receiver/kafkareceiver v0.74.0/go.mod h1:anSbwGOousKpnNAVMNP5YieA4KOFuEzHkvya0vvtsaI= -github.com/open-telemetry/opentelemetry-collector-contrib/receiver/opencensusreceiver v0.74.0 h1:Law7+BImq8DIBsdniSX8Iy2/GH5CRHpT1gsRaC9ZT8A= -github.com/open-telemetry/opentelemetry-collector-contrib/receiver/opencensusreceiver v0.74.0/go.mod h1:uiW3V9EX8A5DOoxqDLuSh++ewHr+owtonCSiqMcpy3w= -github.com/open-telemetry/opentelemetry-collector-contrib/receiver/zipkinreceiver v0.74.0 h1:2uysjsaqkf9STFeJN/M6i/sSYEN5pZJ94Qd2/Hg1pKE= -github.com/open-telemetry/opentelemetry-collector-contrib/receiver/zipkinreceiver v0.74.0/go.mod h1:qoGuayD7cAtshnKosIQHd6dobcn6/sqgUn0v/Cg2UB8= -github.com/opencontainers/runc v1.0.0-rc9 h1:/k06BMULKF5hidyoZymkoDCzdJzltZpz/UU4LguQVtc= -github.com/opentracing-contrib/go-grpc v0.0.0-20210225150812-73cb765af46e h1:4cPxUYdgaGzZIT5/j0IfqOrrXmq6bG8AwvwisMXpdrg= -github.com/opentracing-contrib/go-grpc v0.0.0-20210225150812-73cb765af46e/go.mod h1:DYR5Eij8rJl8h7gblRrOZ8g0kW1umSpKqYIBTgeDtLo= -github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492 h1:lM6RxxfUMrYL/f8bWEUqdXrANWtrL7Nndbm9iFN0DlU= -github.com/opentracing/basictracer-go v1.0.0 h1:YyUAhaEfjoWXclZVJ9sGoNct7j4TVk7lZWlQw5UXuoo= -github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5 h1:ZCnq+JUrvXcDVhX/xRolRBZifmabN1HcS1wrPSvxhrU= -github.com/openzipkin/zipkin-go v0.4.1 h1:kNd/ST2yLLWhaWrkgchya40TJabe8Hioj9udfPcEO5A= -github.com/openzipkin/zipkin-go v0.4.1/go.mod h1:qY0VqDSN1pOBN94dBc6w2GJlWLiovAyg7Qt6/I9HecM= -github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw= -github.com/ory/analytics-go/v4 v4.0.0 h1:KQ2P00j9dbj4lDC/Albw/zn/scK67041RhqeW5ptsbw= -github.com/ory/dockertest v3.3.5+incompatible h1:iLLK6SQwIhcbrG783Dghaaa3WPzGc+4Emza6EbVUUGA= -github.com/ory/dockertest/v3 v3.6.3 h1:L8JWiGgR+fnj90AEOkTFIEp4j5uWAK72P3IUsYgn2cs= -github.com/ory/gojsonreference v0.0.0-20190720135523-6b606c2d8ee8 h1:e2S2FmxqSbhFyVNP24HncpRY+X1qAZmtE3nZ0gJKR4Q= -github.com/ory/gojsonschema v1.1.1-0.20190919112458-f254ca73d5e9 h1:LDIG2Mnha10nFZuVXv3GIBqhQ1+JLwRXPcP4Ykx5VOY= -github.com/ory/herodot v0.9.2 h1:/54FEEMCJNUJKIYRTioOS/0dxdzc9yNtI8/DRVF6KfY= -github.com/ory/jsonschema/v3 v3.0.1 h1:xzV7w2rt/Qn+jvh71joIXNKKOCqqNyTlaIxdxU0IQJc= -github.com/pact-foundation/pact-go v1.0.4 h1:OYkFijGHoZAYbOIb1LWXrwKQbMMRUv1oQ89blD2Mh2Q= -github.com/parnurzeal/gorequest v0.2.15 h1:oPjDCsF5IkD4gUk6vIgsxYNaSgvAnIh1EJeROn3HdJU= -github.com/pelletier/go-toml/v2 v2.0.5 h1:ipoSadvV8oGUjnUbMub59IDPPwfxF694nG/jwbMiyQg= -github.com/performancecopilot/speed v3.0.0+incompatible h1:2WnRzIquHa5QxaJKShDkLM+sc0JPuwhXzK8OYOyt3Vg= -github.com/performancecopilot/speed/v4 v4.0.0 h1:VxEDCmdkfbQYDlcr/GC9YoN9PQ6p8ulk9xVsepYy9ZY= -github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= -github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc= -github.com/philhofer/fwd v1.0.0 h1:UbZqGr5Y38ApvM/V/jEljVxwocdweyH+vmYvRPBnbqQ= -github.com/phpdave11/gofpdf v1.4.2 h1:KPKiIbfwbvC/wOncwhrpRdXVj2CZTCFlw4wnoyjtHfQ= -github.com/phpdave11/gofpdi v1.0.13 h1:o61duiW8M9sMlkVXWlvP92sZJtGKENvW3VExs6dZukQ= github.com/pierrec/lz4 v2.0.5+incompatible h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM8aXeqhl0I= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgcC4zIxODThtZNPirFr42+A= -github.com/pkg/profile v1.2.1 h1:F++O52m40owAmADcojzM+9gyjmMOY/T4oYJkgFDH8RE= -github.com/pkg/sftp v1.13.1 h1:I2qBYMChEhIjOgazfJmV3/mZM256btk6wkCDRmW7JYs= -github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo= -github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= -github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= -github.com/pquerna/cachecontrol v0.1.0 h1:yJMy84ti9h/+OEWa752kBTKv4XC30OtVVHYv/8cTqKc= -github.com/pquerna/cachecontrol v0.1.0/go.mod h1:NrUG3Z7Rdu85UNR3vm7SOsl1nFIeSiQnrHV5K9mBcUI= -github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= -github.com/prometheus/common/assets v0.2.0 h1:0P5OrzoHrYBOSM1OigWL3mY8ZvV2N4zIE/5AahrSrfM= -github.com/prometheus/statsd_exporter v0.22.7 h1:7Pji/i2GuhK6Lu7DHrtTkFmNBCudCPT1pX2CziuyQR0= -github.com/prometheus/statsd_exporter v0.22.7/go.mod h1:N/TevpjkIh9ccs6nuzY3jQn9dFqnUakOjnEuMPJJJnI= -github.com/prometheus/tsdb v0.7.1 h1:YZcsG11NqnK4czYLrWd9mpEuAJIHVQLwdrleYfszMAA= -github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= -github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= -github.com/rhnvrm/simples3 v0.5.0 h1:X+WX0hqoKScdoJAw/G3GArfZ6Ygsn8q+6MdocTMKXOw= -github.com/rogpeppe/fastuuid v1.2.0 h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi2s= -github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4 h1:BN/Nyn2nWMoqGRA7G7paDNDqTXE30mXGqzzybrfo05w= -github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= -github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= -github.com/rs/zerolog v1.15.0 h1:uPRuwkWF4J6fGsJ2R0Gn2jB1EQiav9k3S6CSdygQJXY= -github.com/rubenv/sql-migrate v0.0.0-20190212093014-1007f53448d7 h1:ID2fzWzRFJcF/xf/8eLN9GW5CXb6NQnKfC+ksTwMNpY= github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww= -github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245 h1:K1Xf3bKttbF+koVGaX5xngRIZ5bVjbmPnaxE/dR08uY= -github.com/ryanuber/columnize v2.1.2+incompatible h1:C89EOx/XBWwIXl8wm8OPJBd7kPF25UfsK2X7Ph/zCAk= -github.com/sagikazarmark/crypt v0.6.0 h1:REOEXCs/NFY/1jOCEouMuT4zEniE5YoXbvpC5X/TLF8= -github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da h1:p3Vo3i64TCLY7gIfzeQaUJ+kppEO5WQG3cL8iE8tGHU= -github.com/santhosh-tekuri/jsonschema v1.2.4 h1:hNhW8e7t+H1vgY+1QeEQpveR6D4+OwKPXCfD2aieJis= -github.com/santhosh-tekuri/jsonschema/v2 v2.1.0 h1:7KOtBzox6l1PbyZCuQfo923yIBpoMtGCDOD78P9lv9g= -github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= -github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiyyjYS17cCYRqP13/SHk= -github.com/seatgeek/logrus-gelf-formatter v0.0.0-20210219220335-367fa274be2c h1:jwWrlqKHQeSRjTskQaHBtCOWbaMsd54NBAnofYbEHGs= -github.com/segmentio/analytics-go v3.1.0+incompatible h1:IyiOfUgQFVHvsykKKbdI7ZsH374uv3/DfZUo9+G0Z80= -github.com/segmentio/backo-go v0.0.0-20200129164019-23eae7c10bd3 h1:ZuhckGJ10ulaKkdvJtiAqsLTiPrLaXSdnVgXJKJkTxE= -github.com/segmentio/conf v1.2.0 h1:5OT9+6OyVHLsFLsiJa/2KlqiA1m7mpdUBlkB/qYTMts= -github.com/segmentio/fasthash v0.0.0-20180216231524-a72b379d632e h1:uO75wNGioszjmIzcY/tvdDYKRLVvzggtAmmJkn9j4GQ= -github.com/segmentio/fasthash v0.0.0-20180216231524-a72b379d632e/go.mod h1:tm/wZFQ8e24NYaBGIlnO2WGCAi67re4HHuOm0sftE/M= -github.com/segmentio/go-snakecase v1.1.0 h1:ZJO4SNKKV0MjGOv0LHnixxN5FYv1JKBnVXEuBpwcbQI= -github.com/segmentio/objconv v1.0.1 h1:QjfLzwriJj40JibCV3MGSEiAoXixbp4ybhwfTB8RXOM= -github.com/segmentio/parquet-go v0.0.0-20230427215636-d483faba23a5 h1:7CWCjaHrXSUCHrRhIARMGDVKdB82tnPAQMmANeflKOw= -github.com/segmentio/parquet-go v0.0.0-20230427215636-d483faba23a5/go.mod h1:+J0xQnJjm8DuQUHBO7t57EnmPbstT6+b45+p3DC9k1Q= -github.com/sercand/kuberesolver/v4 v4.0.0 h1:frL7laPDG/lFm5n98ODmWnn+cvPpzlkf3LhzuPhcHP4= -github.com/sercand/kuberesolver/v4 v4.0.0/go.mod h1:F4RGyuRmMAjeXHKL+w4P7AwUnPceEAPAhxUgXZjKgvM= -github.com/sercand/kuberesolver/v5 v5.1.1 h1:CYH+d67G0sGBj7q5wLK61yzqJJ8gLLC8aeprPTHb6yY= -github.com/sercand/kuberesolver/v5 v5.1.1/go.mod h1:Fs1KbKhVRnB2aDWN12NjKCB+RgYMWZJ294T3BtmVCpQ= -github.com/serenize/snaker v0.0.0-20171204205717-a683aaf2d516 h1:ofR1ZdrNSkiWcMsRrubK9tb2/SlZVWttAfqUjJi6QYc= -github.com/shirou/gopsutil/v3 v3.23.2 h1:PAWSuiAszn7IhPMBtXsbSCafej7PqUOvY6YywlQUExU= -github.com/shirou/gopsutil/v3 v3.23.2/go.mod h1:gv0aQw33GLo3pG8SiWKiQrbDzbRY1K80RyZJ7V4Th1M= -github.com/shoenig/test v0.6.6 h1:Oe8TPH9wAbv++YPNDKJWUnI8Q4PPWCx3UbOfH+FxiMU= -github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e h1:MZM7FHLqUHYI0Y/mQAt3d2aYa0SiNms/hFqC9qJYolM= -github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041 h1:llrF3Fs4018ePo4+G/HV/uQUqEI1HMDjCeOf2V6puPc= -github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b h1:vYEG87HxbU6dXj5npkeulCS96Dtz5xg3jcfCgpcvbIw= -github.com/shurcooL/highlight_go v0.0.0-20170515013102-78fb10f4a5f8 h1:xLQlo0Ghg8zBaQi+tjpK+z/WLjbg/BhAWP9pYgqo/LQ= -github.com/shurcooL/octicon v0.0.0-20180602230221-c42b0e3b24d9 h1:j3cAp1j8k/tSLaCcDiXIpVJ8FzSJ9g1eeOAPRJYM75k= -github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= -github.com/sony/gobreaker v0.4.1 h1:oMnRNZXX5j85zso6xCPRNPtmAycat+WcoKbklScLDgQ= -github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d h1:yKm7XZV6j9Ev6lojP2XaIshpT4ymkqhMeSghO5Ps00E= -github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e h1:qpG93cPwA5f7s/ZPBJnGOYQNK/vKsaDaseuKT5Asee8= -github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= -github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/viper v1.14.0 h1:Rg7d3Lo706X9tHsJMUjdiwMpHB7W8WnSVOssIY+JElU= -github.com/spf13/viper v1.14.0/go.mod h1:WT//axPky3FdvXHzGw33dNdXXXfFQqmEalje+egj8As= -github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad h1:fiWzISvDn0Csy5H0iwgAuJGQTUpVfEMJJd4nRFXogbc= -github.com/sqs/goreturns v0.0.0-20181028201513-538ac6014518 h1:iD+PFTQwKEmbwSdwfvP5ld2WEI/g7qbdhmHJ2ASfYGs= -github.com/square/go-jose/v3 v3.0.0-20200630053402-0a67ce9b0693 h1:wD1IWQwAhdWclCwaf6DdzgCAe9Bfz1M+4AHRd7N786Y= -github.com/streadway/amqp v1.0.0 h1:kuuDrUJFZL1QYL9hUNuCxNObNzB0bV/ZG5jV3RWAQgo= -github.com/streadway/handy v0.0.0-20200128134331-0f66f006fb2e h1:mOtuXaRAbVZsxAHVdPR3IjfmN8T1h2iczJLynhLybf8= -github.com/substrait-io/substrait-go v0.4.2 h1:buDnjsb3qAqTaNbOR7VKmNgXf4lYQxWEcnSGUWBtmN8= -github.com/substrait-io/substrait-go v0.4.2/go.mod h1:qhpnLmrcvAnlZsUyPXZRqldiHapPTXC3t7xFgDi3aQg= -github.com/tidwall/gjson v1.14.2 h1:6BBkirS0rAHjumnjHF6qgy5d2YAJ1TLIaFE2lzfOLqo= -github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= -github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= -github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= -github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= -github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= -github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= -github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= -github.com/tinylib/msgp v1.1.2 h1:gWmO7n0Ys2RBEb7GPYB9Ujq8Mk5p2U08lRnmMcGy6BQ= -github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM= -github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI= -github.com/tklauser/numcpus v0.6.0 h1:kebhY2Qt+3U6RNK7UqpYNA+tJ23IBEGKkB7JQBfDYms= -github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ5UVIcaL4= -github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926 h1:G3dpKMzFDjgEh2q1Z7zUUtKa8ViPtH+ocF0bE0g00O8= -github.com/uber-go/atomic v1.4.0 h1:yOuPqEq4ovnhEjpHmfFwsqBXDYbQeT6Nb0bwD6XnD5o= -github.com/uber-go/atomic v1.4.0/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex1PFV8g= -github.com/unrolled/secure v0.0.0-20181005190816-ff9db2ff917f h1:ltz/eIXkYWdMCZbu3Rb+bUmWVTm5AqM0QM8o0uKir4U= -github.com/urfave/negroni v1.0.0 h1:kIimOitoypq34K7TG7DUaJ9kq/N4Ofuwi1sjz0KipXc= -github.com/valyala/fasthttp v1.6.0 h1:uWF8lgKmeaIewWVPwi4GRq2P6+R46IgYZdxWtM+GtEY= -github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a h1:0R4NLDRDZX6JcmhJgXi5E4b8Wg84ihbmUKp/GvSPEzc= -github.com/vinzenz/yaml v0.0.0-20170920082545-91409cdd725d h1:3wDi6J5APMqaHBVPuVd7RmHD2gRTfqbdcVSpCNoUWtk= -github.com/vinzenz/yaml v0.0.0-20170920082545-91409cdd725d/go.mod h1:mb5taDqMnJiZNRQ3+02W2IFG+oEz1+dTuCXkp4jpkfo= -github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= -github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= -github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= -github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= -github.com/weaveworks/common v0.0.0-20230511094633-334485600903 h1:ph7R2CS/0o1gBzpzK/CioUKJVsXNVXfDGR8FZ9rMZIw= -github.com/weaveworks/common v0.0.0-20230511094633-334485600903/go.mod h1:rgbeLfJUtEr+G74cwFPR1k/4N0kDeaeSv/qhUNE4hm8= -github.com/weaveworks/promrus v1.2.0 h1:jOLf6pe6/vss4qGHjXmGz4oDJQA+AOCqEL3FvvZGz7M= -github.com/weaveworks/promrus v1.2.0/go.mod h1:SaE82+OJ91yqjrE1rsvBWVzNZKcHYFtMUyS1+Ogs/KA= -github.com/willf/bitset v1.1.11 h1:N7Z7E9UvjW+sGsEl7k/SJrvY2reP1A07MrGuCjIOjRE= -github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI= -github.com/willf/bloom v2.0.3+incompatible h1:QDacWdqcAUI1MPOwIQZRy9kOR7yxfyEmxX8Wdm2/JPA= -github.com/willf/bloom v2.0.3+incompatible/go.mod h1:MmAltL9pDMNTrvUkxdg0k0q5I0suxmuwp3KbyrZLOZ8= -github.com/xanzy/go-gitlab v0.15.0 h1:rWtwKTgEnXyNUGrOArN7yyc3THRkpYcKXIXia9abywQ= -github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= -github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= -github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= -github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c h1:u40Z8hqBAAQyv+vATcGgV0YCnDjqSL7/q/JyPhhJSPk= -github.com/xdg/stringprep v1.0.0 h1:d9X0esnoa3dFsV0FG35rAT0RIhYFlPq7MiP+DW89La0= -github.com/xhit/go-str2duration v1.2.0 h1:BcV5u025cITWxEQKGWr1URRzrcXtu7uk8+luz3Yuhwc= -github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= -github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77 h1:ESFSdwYZvkeru3RtdrYueztKhOBCSAAzS4Gf+k0tEow= -github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c h1:3lbZUMbMiGUW/LMkfsEABsc5zNT9+b1CvsJx47JzJ8g= -github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY= -github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA= -github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= -github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg= -github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b h1:FosyBZYxY34Wul7O/MSKey3txpPYyCqVO5ZyceuQJEI= -github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8= -github.com/zenazn/goji v1.0.1 h1:4lbD8Mx2h7IvloP7r2C0D6ltZP6Ufip8Hn0wmSK5LR8= -github.com/zenazn/goji v1.0.1/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs= -gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b h1:7gd+rd8P3bqcn/96gOZa3F5dpJr/vEiDQYlNb/y2uNs= -go.elastic.co/apm v1.8.0 h1:AWEKpHwRal0yCMd4K8Oxy1HAa7xid+xq1yy+XjgoVU0= -go.elastic.co/apm/module/apmhttp v1.8.0 h1:5AJPefWJzWDLX/47XIDfaloGiYWkkOQEULvlrI6Ieaw= -go.elastic.co/apm/module/apmot v1.8.0 h1:7r8b5RGDN5gAUG7FoegzJ24+jFSZF7FvY2ODODaKFYk= -go.elastic.co/fastjson v1.0.0 h1:ooXV/ABvf+tBul26jcVViPT3sBir0PvXgibYB1IQQzg= go.opentelemetry.io/collector v0.74.0 h1:0s2DKWczGj/pLTsXGb1P+Je7dyuGx9Is4/Dri1+cS7g= -go.opentelemetry.io/collector v0.74.0/go.mod h1:7NjZAvkhQ6E+NLN4EAH2hw3Nssi+F14t7mV7lMNXCto= -go.opentelemetry.io/collector/component v0.74.0 h1:W32ILPgbA5LO+m9Se61hbbtiLM6FYusNM36K5/CCOi0= -go.opentelemetry.io/collector/component v0.74.0/go.mod h1:zHbWqbdmnHeIZAuO3s1Fo/kWPC2oKuolIhlPmL4bzyo= -go.opentelemetry.io/collector/confmap v0.74.0 h1:tl4fSHC/MXZiEvsZhDhd03TgzvArOe69Qn020sZsTfQ= -go.opentelemetry.io/collector/confmap v0.74.0/go.mod h1:NvUhMS2v8rniLvDAnvGjYOt0qBohk6TIibb1NuyVB1Q= -go.opentelemetry.io/collector/consumer v0.74.0 h1:+kjT/ixG+4SVSHg7u9mQe0+LNDc6PuG8Wn2hoL/yGYk= -go.opentelemetry.io/collector/consumer v0.74.0/go.mod h1:MuGqt8/OKVAOjrh5WHr1TR2qwHizy64ZP2uNSr+XpvI= -go.opentelemetry.io/collector/exporter v0.74.0 h1:VZxDuVz9kJM/Yten3xA/abJwLJNkxLThiao6E1ULW7c= -go.opentelemetry.io/collector/exporter v0.74.0/go.mod h1:kw5YoorpKqEpZZ/a5ODSoYFK1mszzcKBNORd32S8Z7c= -go.opentelemetry.io/collector/exporter/otlpexporter v0.74.0 h1:YKvTeYcBrJwbcXNy65fJ/xytUSMurpYn/KkJD0x+DAY= -go.opentelemetry.io/collector/exporter/otlpexporter v0.74.0/go.mod h1:cRbvsnpSxzySoTSnXbOGPQZu9KHlEyKkTeE21f9Q1p4= -go.opentelemetry.io/collector/featuregate v1.0.0 h1:5MGqe2v5zxaoo73BUOvUTunftX5J8RGrbFsC2Ha7N3g= -go.opentelemetry.io/collector/receiver v0.74.0 h1:jlgBFa0iByvn8VuX27UxtqiPiZE8ejmU5lb1nSptWD8= -go.opentelemetry.io/collector/receiver v0.74.0/go.mod h1:SQkyATvoZCJefNkI2jnrR63SOdrmDLYCnQqXJ7ACqn0= -go.opentelemetry.io/collector/receiver/otlpreceiver v0.74.0 h1:e/X/W0z2Jtpy3Yd3CXkmEm9vSpKq/P3pKUrEVMUFBRw= -go.opentelemetry.io/collector/receiver/otlpreceiver v0.74.0/go.mod h1:9X9/RYFxJIaK0JLlRZ0PpmQSSlYpY+r4KsTOj2jWj14= -go.opentelemetry.io/collector/semconv v0.90.1 h1:2fkQZbefQBbIcNb9Rk1mRcWlFZgQOk7CpST1e1BK8eg= go.opentelemetry.io/contrib v0.18.0 h1:uqBh0brileIvG6luvBjdxzoFL8lxDGuhxJWsvK3BveI= -go.opentelemetry.io/contrib/propagators/b3 v1.15.0 h1:bMaonPyFcAvZ4EVzkUNkfnUHP5Zi63CIDlA3dRsEg8Q= -go.opentelemetry.io/contrib/propagators/b3 v1.15.0/go.mod h1:VjU0g2v6HSQ+NwfifambSLAeBgevjIcqmceaKWEzl0c= -go.opentelemetry.io/otel/bridge/opencensus v0.37.0 h1:ieH3gw7b1eg90ARsFAlAsX5LKVZgnCYfaDwRrK6xLHU= -go.opentelemetry.io/otel/bridge/opencensus v0.37.0/go.mod h1:ddiK+1PE68l/Xk04BGTh9Y6WIcxcLrmcVxVlS0w5WZ0= -go.opentelemetry.io/otel/bridge/opentracing v1.10.0 h1:WzAVGovpC1s7KD5g4taU6BWYZP3QGSDVTlbRu9fIHw8= -go.opentelemetry.io/otel/bridge/opentracing v1.10.0/go.mod h1:J7GLR/uxxqMAzZptsH0pjte3Ep4GacTCrbGBoDuHBqk= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0 h1:digkEZCJWobwBqMwC0cwCq8/wkkRy/OowZg5OArWZrM= -go.opentelemetry.io/otel/exporters/prometheus v0.37.0 h1:NQc0epfL0xItsmGgSXgfbH2C1fq2VLXkZoDFsfRNHpc= -go.opentelemetry.io/otel/exporters/prometheus v0.37.0/go.mod h1:hB8qWjsStK36t50/R0V2ULFb4u95X/Q6zupXLgvjTh8= -go.opentelemetry.io/otel/oteltest v0.18.0 h1:FbKDFm/LnQDOHuGjED+fy3s5YMVg0z019GJ9Er66hYo= -go.opentelemetry.io/otel/sdk/metric v0.39.0 h1:Kun8i1eYf48kHH83RucG93ffz0zGV1sh46FAScOTuDI= -go.opentelemetry.io/otel/sdk/metric v0.39.0/go.mod h1:piDIRgjcK7u0HCL5pCA4e74qpK/jk3NiUoAHATVAmiI= -go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8= -go.uber.org/mock v0.2.0 h1:TaP3xedm7JaAgScZO7tlvlKrqT0p7I6OsdGB5YNSMDU= -go.uber.org/mock v0.2.0/go.mod h1:J0y0rp9L3xiff1+ZBfKxlC1fz2+aO16tw0tsDOixfuM= -go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= -golang.org/x/image v0.0.0-20220302094943-723b81ca9867 h1:TcHcE0vrmgzNH1v3ppjcMGbhG5+9fMuvOmUYwNEF4q4= -golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028 h1:4+4C/Iv2U4fMZBiMCc98MG1In4gJY5YRhtpDNeDeHWs= -gonum.org/v1/netlib v0.0.0-20191229114700-bbb4dff026f8 h1:kHY67jAKYewKUCz9YdNDa7iLAJ2WfNmoHzCCX4KnA8w= -gonum.org/v1/plot v0.10.1 h1:dnifSs43YJuNMDzB7v8wV64O4ABBHReuAVAoBxqBqS4= -google.golang.org/genproto/googleapis/bytestream v0.0.0-20231120223509-83a465c0220f h1:hL+1ptbhFoeL1HcROQ8OGXaqH0jYRRibgWQWco0/Ugc= -google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0 h1:M1YKkFIboKNieVO5DLUEVzQfGwJD30Nv2jfUgzb5UcE= -google.golang.org/grpc/examples v0.0.0-20210304020650-930c79186c99 h1:qA8rMbz1wQ4DOFfM2ouD29DG9aHWBm6ZOy9BGxiUMmY= -gopkg.in/DataDog/dd-trace-go.v1 v1.27.0 h1:WGVt9dwn9vNeWZVdDYzjGQbEW8CghAkJlrC8w80jFVY= -gopkg.in/airbrake/gobrake.v2 v2.0.9 h1:7z2uVWwn7oVeeugY1DtlPAy5H+KYgB1KeKTnqjNatLo= -gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= -gopkg.in/cheggaaa/pb.v1 v1.0.25 h1:Ev7yu1/f6+d+b3pi5vPdRPc6nNtP1umSfcWiEfRqv6I= -gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8= -gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= -gopkg.in/gcfg.v1 v1.2.3 h1:m8OOJ4ccYHnx2f4gQwpno8nAX5OGOh7RLaaz0pj3Ogs= -gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2 h1:OAj3g0cR6Dx/R07QgQe8wkA9RNjB2u4i700xBkIT4e0= -gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= -gopkg.in/go-playground/mold.v2 v2.2.0 h1:Y4IYB4/HYQfuq43zaKh6vs9cVelLE9qbqe2fkyfCTWQ= -gopkg.in/go-playground/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ= -gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE= -gopkg.in/gorp.v1 v1.7.2 h1:j3DWlAyGVv8whO7AcIWznQ2Yj7yJkn34B8s63GViAAw= -gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec h1:RlWgLqCMMIYYEVcAR5MDsuHlVkaIPDAF+5Dehzg8L5A= -gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce h1:xcEWjVhvbDy+nHP67nPDDpbYrY+ILlfndk4bRioVHaU= -gopkg.in/resty.v1 v1.12.0 h1:CuXP0Pjfw9rOuY6EP+UvtNvt5DSqHpIxILZKT/quCZI= -gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg= -gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98= -gopkg.in/telebot.v3 v3.2.1 h1:3I4LohaAyJBiivGmkfB+CiVu7QFOWkuZ4+KHgO/G3rs= -gopkg.in/validator.v2 v2.0.0-20180514200540-135c24b11c19 h1:WB265cn5OpO+hK3pikC9hpP1zI/KTwmyMFKloW9eOVc= -gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= -honnef.co/go/tools v0.1.3 h1:qTakTkI6ni6LFD5sBwwsdSO+AQqbSIxOauHTTQKZ/7o= -howett.net/plist v0.0.0-20181124034731-591f970eefbb h1:jhnBjNi9UFpfpl8YZhA9CrOqpnJdvzuiHsl/dnxl11M= -k8s.io/apiextensions-apiserver v0.26.2 h1:/yTG2B9jGY2Q70iGskMf41qTLhL9XeNN2KhI0uDgwko= -k8s.io/apiextensions-apiserver v0.26.2/go.mod h1:Y7UPgch8nph8mGCuVk0SK83LnS8Esf3n6fUBgew8SH8= -k8s.io/gengo v0.0.0-20230829151522-9cce18d56c01 h1:pWEwq4Asjm4vjW7vcsmijwBhOr1/shsbSYiWXmNGlks= -k8s.io/gengo v0.0.0-20230829151522-9cce18d56c01/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= -modernc.org/cc v1.0.0 h1:nPibNuDEx6tvYrUAtvDTTw98rx5juGsa5zuDnKwEEQQ= -modernc.org/golex v1.0.0 h1:wWpDlbK8ejRfSyi0frMyhilD3JBvtcx2AdGDnU+JtsE= -modernc.org/xc v1.0.0 h1:7ccXrupWZIS3twbUGrtKmHS2DXY6xegFua+6O3xgAFU= -nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g= -rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE= -rsc.io/pdf v0.1.1 h1:k1MczvYDUvJBe93bYd7wrZLLUEcLZAuF824/I4e5Xr4= -rsc.io/quote/v3 v3.1.0 h1:9JKUTTIUgS6kzR9mK1YuGKv6Nl+DijDNIc0ghT58FaY= -rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4= -sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0 h1:ucqkfpjg9WzSUubAO62csmucvxl4/JeW3F4I4909XkM= +k8s.io/kms v0.29.2 h1:MDsbp98gSlEQs7K7dqLKNNTwKFQRYYvO4UOlBOjNy6Y= +k8s.io/kms v0.29.2/go.mod h1:s/9RC4sYRZ/6Tn6yhNjbfJuZdb8LzlXhdlBnKizeFDo= diff --git a/pkg/apimachinery/apis/common/v0alpha1/doc.go b/pkg/apimachinery/apis/common/v0alpha1/doc.go new file mode 100644 index 0000000000..8dbffd26f9 --- /dev/null +++ b/pkg/apimachinery/apis/common/v0alpha1/doc.go @@ -0,0 +1,5 @@ +// +k8s:openapi-gen=true +// +k8s:defaulter-gen=TypeMeta +// +groupName=common.grafana.app + +package v0alpha1 // import "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" diff --git a/pkg/apis/common/v0alpha1/resource.go b/pkg/apimachinery/apis/common/v0alpha1/resource.go similarity index 100% rename from pkg/apis/common/v0alpha1/resource.go rename to pkg/apimachinery/apis/common/v0alpha1/resource.go diff --git a/pkg/apis/common/v0alpha1/types.go b/pkg/apimachinery/apis/common/v0alpha1/types.go similarity index 100% rename from pkg/apis/common/v0alpha1/types.go rename to pkg/apimachinery/apis/common/v0alpha1/types.go diff --git a/pkg/apis/common/v0alpha1/unstructured.go b/pkg/apimachinery/apis/common/v0alpha1/unstructured.go similarity index 100% rename from pkg/apis/common/v0alpha1/unstructured.go rename to pkg/apimachinery/apis/common/v0alpha1/unstructured.go diff --git a/pkg/apis/common/v0alpha1/zz_generated.defaults.go b/pkg/apimachinery/apis/common/v0alpha1/zz_generated.defaults.go similarity index 100% rename from pkg/apis/common/v0alpha1/zz_generated.defaults.go rename to pkg/apimachinery/apis/common/v0alpha1/zz_generated.defaults.go diff --git a/pkg/apis/common/v0alpha1/zz_generated.openapi.go b/pkg/apimachinery/apis/common/v0alpha1/zz_generated.openapi.go similarity index 96% rename from pkg/apis/common/v0alpha1/zz_generated.openapi.go rename to pkg/apimachinery/apis/common/v0alpha1/zz_generated.openapi.go index cd4217bd8b..2dd8493f32 100644 --- a/pkg/apis/common/v0alpha1/zz_generated.openapi.go +++ b/pkg/apimachinery/apis/common/v0alpha1/zz_generated.openapi.go @@ -17,64 +17,64 @@ import ( func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { return map[string]common.OpenAPIDefinition{ - "github.com/grafana/grafana/pkg/apis/common/v0alpha1.ObjectReference": schema_pkg_apis_common_v0alpha1_ObjectReference(ref), - "github.com/grafana/grafana/pkg/apis/common/v0alpha1.Unstructured": Unstructured{}.OpenAPIDefinition(), - "k8s.io/apimachinery/pkg/apis/meta/v1.APIGroup": schema_pkg_apis_meta_v1_APIGroup(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.APIGroupList": schema_pkg_apis_meta_v1_APIGroupList(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.APIResource": schema_pkg_apis_meta_v1_APIResource(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.APIResourceList": schema_pkg_apis_meta_v1_APIResourceList(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.APIVersions": schema_pkg_apis_meta_v1_APIVersions(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.ApplyOptions": schema_pkg_apis_meta_v1_ApplyOptions(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.Condition": schema_pkg_apis_meta_v1_Condition(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.CreateOptions": schema_pkg_apis_meta_v1_CreateOptions(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.DeleteOptions": schema_pkg_apis_meta_v1_DeleteOptions(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.Duration": schema_pkg_apis_meta_v1_Duration(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.FieldsV1": schema_pkg_apis_meta_v1_FieldsV1(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.GetOptions": schema_pkg_apis_meta_v1_GetOptions(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.GroupKind": schema_pkg_apis_meta_v1_GroupKind(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.GroupResource": schema_pkg_apis_meta_v1_GroupResource(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.GroupVersion": schema_pkg_apis_meta_v1_GroupVersion(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.GroupVersionForDiscovery": schema_pkg_apis_meta_v1_GroupVersionForDiscovery(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.GroupVersionKind": schema_pkg_apis_meta_v1_GroupVersionKind(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.GroupVersionResource": schema_pkg_apis_meta_v1_GroupVersionResource(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.InternalEvent": schema_pkg_apis_meta_v1_InternalEvent(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.LabelSelector": schema_pkg_apis_meta_v1_LabelSelector(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.LabelSelectorRequirement": schema_pkg_apis_meta_v1_LabelSelectorRequirement(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.List": schema_pkg_apis_meta_v1_List(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta": schema_pkg_apis_meta_v1_ListMeta(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.ListOptions": schema_pkg_apis_meta_v1_ListOptions(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.ManagedFieldsEntry": schema_pkg_apis_meta_v1_ManagedFieldsEntry(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.MicroTime": schema_pkg_apis_meta_v1_MicroTime(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta": schema_pkg_apis_meta_v1_ObjectMeta(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.OwnerReference": schema_pkg_apis_meta_v1_OwnerReference(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.PartialObjectMetadata": schema_pkg_apis_meta_v1_PartialObjectMetadata(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.PartialObjectMetadataList": schema_pkg_apis_meta_v1_PartialObjectMetadataList(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.Patch": schema_pkg_apis_meta_v1_Patch(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.PatchOptions": schema_pkg_apis_meta_v1_PatchOptions(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.Preconditions": schema_pkg_apis_meta_v1_Preconditions(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.RootPaths": schema_pkg_apis_meta_v1_RootPaths(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.ServerAddressByClientCIDR": schema_pkg_apis_meta_v1_ServerAddressByClientCIDR(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.Status": schema_pkg_apis_meta_v1_Status(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.StatusCause": schema_pkg_apis_meta_v1_StatusCause(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.StatusDetails": schema_pkg_apis_meta_v1_StatusDetails(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.Table": schema_pkg_apis_meta_v1_Table(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.TableColumnDefinition": schema_pkg_apis_meta_v1_TableColumnDefinition(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.TableOptions": schema_pkg_apis_meta_v1_TableOptions(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.TableRow": schema_pkg_apis_meta_v1_TableRow(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.TableRowCondition": schema_pkg_apis_meta_v1_TableRowCondition(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.Time": schema_pkg_apis_meta_v1_Time(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.Timestamp": schema_pkg_apis_meta_v1_Timestamp(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.TypeMeta": schema_pkg_apis_meta_v1_TypeMeta(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.UpdateOptions": schema_pkg_apis_meta_v1_UpdateOptions(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.WatchEvent": schema_pkg_apis_meta_v1_WatchEvent(ref), - "k8s.io/apimachinery/pkg/runtime.RawExtension": schema_k8sio_apimachinery_pkg_runtime_RawExtension(ref), - "k8s.io/apimachinery/pkg/runtime.TypeMeta": schema_k8sio_apimachinery_pkg_runtime_TypeMeta(ref), - "k8s.io/apimachinery/pkg/runtime.Unknown": schema_k8sio_apimachinery_pkg_runtime_Unknown(ref), - "k8s.io/apimachinery/pkg/version.Info": schema_k8sio_apimachinery_pkg_version_Info(ref), + "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.ObjectReference": schema_apimachinery_apis_common_v0alpha1_ObjectReference(ref), + "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.Unstructured": Unstructured{}.OpenAPIDefinition(), + "k8s.io/apimachinery/pkg/apis/meta/v1.APIGroup": schema_pkg_apis_meta_v1_APIGroup(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.APIGroupList": schema_pkg_apis_meta_v1_APIGroupList(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.APIResource": schema_pkg_apis_meta_v1_APIResource(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.APIResourceList": schema_pkg_apis_meta_v1_APIResourceList(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.APIVersions": schema_pkg_apis_meta_v1_APIVersions(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.ApplyOptions": schema_pkg_apis_meta_v1_ApplyOptions(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.Condition": schema_pkg_apis_meta_v1_Condition(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.CreateOptions": schema_pkg_apis_meta_v1_CreateOptions(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.DeleteOptions": schema_pkg_apis_meta_v1_DeleteOptions(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.Duration": schema_pkg_apis_meta_v1_Duration(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.FieldsV1": schema_pkg_apis_meta_v1_FieldsV1(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.GetOptions": schema_pkg_apis_meta_v1_GetOptions(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.GroupKind": schema_pkg_apis_meta_v1_GroupKind(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.GroupResource": schema_pkg_apis_meta_v1_GroupResource(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.GroupVersion": schema_pkg_apis_meta_v1_GroupVersion(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.GroupVersionForDiscovery": schema_pkg_apis_meta_v1_GroupVersionForDiscovery(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.GroupVersionKind": schema_pkg_apis_meta_v1_GroupVersionKind(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.GroupVersionResource": schema_pkg_apis_meta_v1_GroupVersionResource(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.InternalEvent": schema_pkg_apis_meta_v1_InternalEvent(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.LabelSelector": schema_pkg_apis_meta_v1_LabelSelector(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.LabelSelectorRequirement": schema_pkg_apis_meta_v1_LabelSelectorRequirement(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.List": schema_pkg_apis_meta_v1_List(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta": schema_pkg_apis_meta_v1_ListMeta(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.ListOptions": schema_pkg_apis_meta_v1_ListOptions(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.ManagedFieldsEntry": schema_pkg_apis_meta_v1_ManagedFieldsEntry(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.MicroTime": schema_pkg_apis_meta_v1_MicroTime(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta": schema_pkg_apis_meta_v1_ObjectMeta(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.OwnerReference": schema_pkg_apis_meta_v1_OwnerReference(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.PartialObjectMetadata": schema_pkg_apis_meta_v1_PartialObjectMetadata(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.PartialObjectMetadataList": schema_pkg_apis_meta_v1_PartialObjectMetadataList(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.Patch": schema_pkg_apis_meta_v1_Patch(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.PatchOptions": schema_pkg_apis_meta_v1_PatchOptions(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.Preconditions": schema_pkg_apis_meta_v1_Preconditions(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.RootPaths": schema_pkg_apis_meta_v1_RootPaths(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.ServerAddressByClientCIDR": schema_pkg_apis_meta_v1_ServerAddressByClientCIDR(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.Status": schema_pkg_apis_meta_v1_Status(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.StatusCause": schema_pkg_apis_meta_v1_StatusCause(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.StatusDetails": schema_pkg_apis_meta_v1_StatusDetails(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.Table": schema_pkg_apis_meta_v1_Table(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.TableColumnDefinition": schema_pkg_apis_meta_v1_TableColumnDefinition(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.TableOptions": schema_pkg_apis_meta_v1_TableOptions(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.TableRow": schema_pkg_apis_meta_v1_TableRow(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.TableRowCondition": schema_pkg_apis_meta_v1_TableRowCondition(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.Time": schema_pkg_apis_meta_v1_Time(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.Timestamp": schema_pkg_apis_meta_v1_Timestamp(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.TypeMeta": schema_pkg_apis_meta_v1_TypeMeta(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.UpdateOptions": schema_pkg_apis_meta_v1_UpdateOptions(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.WatchEvent": schema_pkg_apis_meta_v1_WatchEvent(ref), + "k8s.io/apimachinery/pkg/runtime.RawExtension": schema_k8sio_apimachinery_pkg_runtime_RawExtension(ref), + "k8s.io/apimachinery/pkg/runtime.TypeMeta": schema_k8sio_apimachinery_pkg_runtime_TypeMeta(ref), + "k8s.io/apimachinery/pkg/runtime.Unknown": schema_k8sio_apimachinery_pkg_runtime_Unknown(ref), + "k8s.io/apimachinery/pkg/version.Info": schema_k8sio_apimachinery_pkg_version_Info(ref), } } -func schema_pkg_apis_common_v0alpha1_ObjectReference(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_apimachinery_apis_common_v0alpha1_ObjectReference(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ diff --git a/pkg/apis/common/v0alpha1/zz_generated.openapi_violation_exceptions.list b/pkg/apimachinery/apis/common/v0alpha1/zz_generated.openapi_violation_exceptions.list similarity index 98% rename from pkg/apis/common/v0alpha1/zz_generated.openapi_violation_exceptions.list rename to pkg/apimachinery/apis/common/v0alpha1/zz_generated.openapi_violation_exceptions.list index b1c3962be8..3061580736 100644 --- a/pkg/apis/common/v0alpha1/zz_generated.openapi_violation_exceptions.list +++ b/pkg/apimachinery/apis/common/v0alpha1/zz_generated.openapi_violation_exceptions.list @@ -25,7 +25,7 @@ API rule violation: list_type_missing,k8s.io/apimachinery/pkg/apis/meta/v1,Table API rule violation: list_type_missing,k8s.io/apimachinery/pkg/apis/meta/v1,UpdateOptions,DryRun API rule violation: list_type_missing,k8s.io/apimachinery/pkg/runtime,RawExtension,Raw API rule violation: list_type_missing,k8s.io/apimachinery/pkg/runtime,Unknown,Raw -API rule violation: names_match,github.com/grafana/grafana/pkg/apis/common/v0alpha1,Unstructured,Object +API rule violation: names_match,github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1,Unstructured,Object API rule violation: names_match,k8s.io/apimachinery/pkg/apis/meta/v1,APIResourceList,APIResources API rule violation: names_match,k8s.io/apimachinery/pkg/apis/meta/v1,Duration,Duration API rule violation: names_match,k8s.io/apimachinery/pkg/apis/meta/v1,InternalEvent,Object diff --git a/pkg/apimachinery/go.mod b/pkg/apimachinery/go.mod new file mode 100644 index 0000000000..8baa078c4b --- /dev/null +++ b/pkg/apimachinery/go.mod @@ -0,0 +1,37 @@ +module github.com/grafana/grafana/pkg/apimachinery + +go 1.21.0 + +require ( + k8s.io/apimachinery v0.29.2 + k8s.io/kube-openapi v0.0.0-20240220201932-37d671a357a5 +) + +require ( + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/go-logr/logr v1.4.1 // indirect + github.com/go-openapi/jsonpointer v0.20.2 // indirect + github.com/go-openapi/jsonreference v0.20.4 // indirect + github.com/go-openapi/swag v0.22.9 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + golang.org/x/net v0.20.0 // indirect + golang.org/x/text v0.14.0 // indirect + google.golang.org/protobuf v1.32.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/klog/v2 v2.120.1 // indirect + k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect +) diff --git a/pkg/apimachinery/go.sum b/pkg/apimachinery/go.sum new file mode 100644 index 0000000000..8b6fe021c1 --- /dev/null +++ b/pkg/apimachinery/go.sum @@ -0,0 +1,104 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-openapi/jsonpointer v0.20.2 h1:mQc3nmndL8ZBzStEo3JYF8wzmeWffDH4VbXz58sAx6Q= +github.com/go-openapi/jsonreference v0.20.4 h1:bKlDxQxQJgwpUSgOENiMPzCTBVuc7vTdXSSgNeAhojU= +github.com/go-openapi/swag v0.22.9 h1:XX2DssF+mQKM2DHsbgZK74y/zj4mo9I99+89xUmuZCE= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/apimachinery v0.29.2 h1:EWGpfJ856oj11C52NRCHuU7rFDwxev48z+6DSlGNsV8= +k8s.io/apimachinery v0.29.2/go.mod h1:6HVkd1FwxIagpYrHSwJlQqZI3G9LfYWRPAkUvLnXTKU= +k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= +k8s.io/kube-openapi v0.0.0-20240220201932-37d671a357a5 h1:QSpdNrZ9uRlV0VkqLvVO0Rqg8ioKi3oSw7O5P7pJV8M= +k8s.io/kube-openapi v0.0.0-20240220201932-37d671a357a5/go.mod h1:Pa1PvrP7ACSkuX6I7KYomY6cmMA0Tx86waBhDUgoKPw= +k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= +k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/pkg/apis/common/v0alpha1/doc.go b/pkg/apis/common/v0alpha1/doc.go deleted file mode 100644 index add503dd6c..0000000000 --- a/pkg/apis/common/v0alpha1/doc.go +++ /dev/null @@ -1,5 +0,0 @@ -// +k8s:openapi-gen=true -// +k8s:defaulter-gen=TypeMeta -// +groupName=common.grafana.app - -package v0alpha1 // import "github.com/grafana/grafana/pkg/apis/common/v0alpha1" diff --git a/pkg/apis/dashboard/v0alpha1/register.go b/pkg/apis/dashboard/v0alpha1/register.go index a6490488d4..1bb1bf2235 100644 --- a/pkg/apis/dashboard/v0alpha1/register.go +++ b/pkg/apis/dashboard/v0alpha1/register.go @@ -4,7 +4,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" - common "github.com/grafana/grafana/pkg/apis/common/v0alpha1" + common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" ) const ( diff --git a/pkg/apis/dashboard/v0alpha1/types.go b/pkg/apis/dashboard/v0alpha1/types.go index b8cd8c284e..349f15c7b5 100644 --- a/pkg/apis/dashboard/v0alpha1/types.go +++ b/pkg/apis/dashboard/v0alpha1/types.go @@ -3,7 +3,7 @@ package v0alpha1 import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - common "github.com/grafana/grafana/pkg/apis/common/v0alpha1" + common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" ) // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object diff --git a/pkg/apis/dashboardsnapshot/v0alpha1/register.go b/pkg/apis/dashboardsnapshot/v0alpha1/register.go index e8022d55bf..57209ad7b9 100644 --- a/pkg/apis/dashboardsnapshot/v0alpha1/register.go +++ b/pkg/apis/dashboardsnapshot/v0alpha1/register.go @@ -4,7 +4,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" - common "github.com/grafana/grafana/pkg/apis/common/v0alpha1" + common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" ) const ( diff --git a/pkg/apis/dashboardsnapshot/v0alpha1/types.go b/pkg/apis/dashboardsnapshot/v0alpha1/types.go index 6a0785bded..8e3aa7520f 100644 --- a/pkg/apis/dashboardsnapshot/v0alpha1/types.go +++ b/pkg/apis/dashboardsnapshot/v0alpha1/types.go @@ -3,7 +3,7 @@ package v0alpha1 import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - common "github.com/grafana/grafana/pkg/apis/common/v0alpha1" + common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" ) // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object diff --git a/pkg/apis/datasource/v0alpha1/register.go b/pkg/apis/datasource/v0alpha1/register.go index 801512af9d..7eae1938ef 100644 --- a/pkg/apis/datasource/v0alpha1/register.go +++ b/pkg/apis/datasource/v0alpha1/register.go @@ -3,7 +3,7 @@ package v0alpha1 import ( "k8s.io/apimachinery/pkg/runtime" - common "github.com/grafana/grafana/pkg/apis/common/v0alpha1" + common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" ) const ( diff --git a/pkg/apis/datasource/v0alpha1/types.go b/pkg/apis/datasource/v0alpha1/types.go index b60f548863..7f72cd32da 100644 --- a/pkg/apis/datasource/v0alpha1/types.go +++ b/pkg/apis/datasource/v0alpha1/types.go @@ -3,7 +3,7 @@ package v0alpha1 import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - common "github.com/grafana/grafana/pkg/apis/common/v0alpha1" + common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" ) // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object diff --git a/pkg/apis/example/v0alpha1/register.go b/pkg/apis/example/v0alpha1/register.go index 5f8f3aa37f..e254cad1d3 100644 --- a/pkg/apis/example/v0alpha1/register.go +++ b/pkg/apis/example/v0alpha1/register.go @@ -4,7 +4,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" - common "github.com/grafana/grafana/pkg/apis/common/v0alpha1" + common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" ) const ( diff --git a/pkg/apis/example/v0alpha1/types.go b/pkg/apis/example/v0alpha1/types.go index c623542cfd..a8fef3ffe6 100644 --- a/pkg/apis/example/v0alpha1/types.go +++ b/pkg/apis/example/v0alpha1/types.go @@ -3,7 +3,7 @@ package v0alpha1 import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - common "github.com/grafana/grafana/pkg/apis/common/v0alpha1" + common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" ) // Mirrors the info exposed in "github.com/grafana/grafana/pkg/setting" diff --git a/pkg/apis/featuretoggle/v0alpha1/register.go b/pkg/apis/featuretoggle/v0alpha1/register.go index e670bd30dd..b1e7d06238 100644 --- a/pkg/apis/featuretoggle/v0alpha1/register.go +++ b/pkg/apis/featuretoggle/v0alpha1/register.go @@ -4,7 +4,7 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" - common "github.com/grafana/grafana/pkg/apis/common/v0alpha1" + common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" ) const ( diff --git a/pkg/apis/featuretoggle/v0alpha1/types.go b/pkg/apis/featuretoggle/v0alpha1/types.go index e10c7f2b08..223ffd6096 100644 --- a/pkg/apis/featuretoggle/v0alpha1/types.go +++ b/pkg/apis/featuretoggle/v0alpha1/types.go @@ -3,7 +3,7 @@ package v0alpha1 import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - common "github.com/grafana/grafana/pkg/apis/common/v0alpha1" + common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" ) // Feature represents a feature in development and information about that feature diff --git a/pkg/apis/featuretoggle/v0alpha1/zz_generated.deepcopy.go b/pkg/apis/featuretoggle/v0alpha1/zz_generated.deepcopy.go index c902f5dd61..6b06fab2b6 100644 --- a/pkg/apis/featuretoggle/v0alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/featuretoggle/v0alpha1/zz_generated.deepcopy.go @@ -8,7 +8,7 @@ package v0alpha1 import ( - commonv0alpha1 "github.com/grafana/grafana/pkg/apis/common/v0alpha1" + commonv0alpha1 "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" runtime "k8s.io/apimachinery/pkg/runtime" ) diff --git a/pkg/apis/folder/v0alpha1/register.go b/pkg/apis/folder/v0alpha1/register.go index cd57f96e03..010054ad15 100644 --- a/pkg/apis/folder/v0alpha1/register.go +++ b/pkg/apis/folder/v0alpha1/register.go @@ -4,7 +4,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" - common "github.com/grafana/grafana/pkg/apis/common/v0alpha1" + common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" ) const ( diff --git a/pkg/apis/peakq/v0alpha1/register.go b/pkg/apis/peakq/v0alpha1/register.go index 214840fbad..b58288d353 100644 --- a/pkg/apis/peakq/v0alpha1/register.go +++ b/pkg/apis/peakq/v0alpha1/register.go @@ -5,7 +5,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" - common "github.com/grafana/grafana/pkg/apis/common/v0alpha1" + common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" ) const ( diff --git a/pkg/apis/playlist/v0alpha1/register.go b/pkg/apis/playlist/v0alpha1/register.go index f375b4e8d7..c1886b2d57 100644 --- a/pkg/apis/playlist/v0alpha1/register.go +++ b/pkg/apis/playlist/v0alpha1/register.go @@ -4,7 +4,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" - common "github.com/grafana/grafana/pkg/apis/common/v0alpha1" + common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" ) const ( diff --git a/pkg/apis/query/v0alpha1/register.go b/pkg/apis/query/v0alpha1/register.go index b728ad7b43..a64293f9be 100644 --- a/pkg/apis/query/v0alpha1/register.go +++ b/pkg/apis/query/v0alpha1/register.go @@ -4,7 +4,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" - common "github.com/grafana/grafana/pkg/apis/common/v0alpha1" + common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" ) const ( diff --git a/pkg/apis/query/v0alpha1/template/types.go b/pkg/apis/query/v0alpha1/template/types.go index 56fc48e213..4785176495 100644 --- a/pkg/apis/query/v0alpha1/template/types.go +++ b/pkg/apis/query/v0alpha1/template/types.go @@ -3,7 +3,7 @@ package template import ( "github.com/grafana/grafana-plugin-sdk-go/data" - common "github.com/grafana/grafana/pkg/apis/common/v0alpha1" + common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" query "github.com/grafana/grafana/pkg/apis/query/v0alpha1" ) diff --git a/pkg/apis/scope/v0alpha1/register.go b/pkg/apis/scope/v0alpha1/register.go index d8a6284856..81dc1535fc 100644 --- a/pkg/apis/scope/v0alpha1/register.go +++ b/pkg/apis/scope/v0alpha1/register.go @@ -5,7 +5,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" - common "github.com/grafana/grafana/pkg/apis/common/v0alpha1" + common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" ) const ( diff --git a/pkg/apis/service/v0alpha1/register.go b/pkg/apis/service/v0alpha1/register.go index 70b1a3d1d6..530b1914fe 100644 --- a/pkg/apis/service/v0alpha1/register.go +++ b/pkg/apis/service/v0alpha1/register.go @@ -5,7 +5,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" - common "github.com/grafana/grafana/pkg/apis/common/v0alpha1" + common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" ) const ( diff --git a/pkg/services/apiserver/builder/common.go b/pkg/apiserver/builder/common.go similarity index 100% rename from pkg/services/apiserver/builder/common.go rename to pkg/apiserver/builder/common.go diff --git a/pkg/services/apiserver/builder/helper.go b/pkg/apiserver/builder/helper.go similarity index 90% rename from pkg/services/apiserver/builder/helper.go rename to pkg/apiserver/builder/helper.go index f698775236..136dba3269 100644 --- a/pkg/services/apiserver/builder/helper.go +++ b/pkg/apiserver/builder/helper.go @@ -20,14 +20,16 @@ import ( "k8s.io/apiserver/pkg/util/openapi" k8sscheme "k8s.io/client-go/kubernetes/scheme" "k8s.io/kube-openapi/pkg/common" - - "github.com/grafana/grafana/pkg/setting" ) func SetupConfig( scheme *runtime.Scheme, serverConfig *genericapiserver.RecommendedConfig, builders []APIGroupBuilder, + buildTimestamp int64, + buildVersion string, + buildCommit string, + buildBranch string, ) error { defsGetter := GetOpenAPIDefinitions(builders) serverConfig.OpenAPIConfig = genericapiserver.DefaultOpenAPIConfig( @@ -39,7 +41,7 @@ func SetupConfig( openapinamer.NewDefinitionNamer(scheme, k8sscheme.Scheme)) // Add the custom routes to service discovery - serverConfig.OpenAPIV3Config.PostProcessSpec = getOpenAPIPostProcessor(builders) + serverConfig.OpenAPIV3Config.PostProcessSpec = getOpenAPIPostProcessor(buildVersion, builders) serverConfig.OpenAPIV3Config.GetOperationIDAndTagsFromRoute = func(r common.Route) (string, []string, error) { tags := []string{} prop, ok := r.Metadata()["x-kubernetes-group-version-kind"] @@ -53,8 +55,8 @@ func SetupConfig( } // Set the swagger build versions - serverConfig.OpenAPIConfig.Info.Version = setting.BuildVersion - serverConfig.OpenAPIV3Config.Info.Version = setting.BuildVersion + serverConfig.OpenAPIConfig.Info.Version = buildVersion + serverConfig.OpenAPIV3Config.Info.Version = buildVersion serverConfig.SkipOpenAPIInstallation = false serverConfig.BuildHandlerChainFunc = func(delegateHandler http.Handler, c *genericapiserver.Config) http.Handler { @@ -75,16 +77,16 @@ func SetupConfig( if err != nil { return err } - before, after, _ := strings.Cut(setting.BuildVersion, ".") + before, after, _ := strings.Cut(buildVersion, ".") serverConfig.Version = &version.Info{ Major: before, Minor: after, GoVersion: goruntime.Version(), Platform: fmt.Sprintf("%s/%s", goruntime.GOOS, goruntime.GOARCH), Compiler: goruntime.Compiler, - GitTreeState: setting.BuildBranch, - GitCommit: setting.BuildCommit, - BuildDate: time.Unix(setting.BuildStamp, 0).UTC().Format(time.DateTime), + GitTreeState: buildBranch, + GitCommit: buildCommit, + BuildDate: time.Unix(buildTimestamp, 0).UTC().Format(time.DateTime), GitVersion: k8sVersion, } return nil diff --git a/pkg/services/apiserver/builder/openapi.go b/pkg/apiserver/builder/openapi.go similarity index 92% rename from pkg/services/apiserver/builder/openapi.go rename to pkg/apiserver/builder/openapi.go index ad8d288bfc..2cc16d9aae 100644 --- a/pkg/services/apiserver/builder/openapi.go +++ b/pkg/apiserver/builder/openapi.go @@ -8,8 +8,7 @@ import ( "k8s.io/kube-openapi/pkg/spec3" spec "k8s.io/kube-openapi/pkg/validation/spec" - "github.com/grafana/grafana/pkg/apis/common/v0alpha1" - "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" ) // This should eventually live in grafana-app-sdk @@ -30,7 +29,7 @@ func GetOpenAPIDefinitions(builders []APIGroupBuilder) common.GetOpenAPIDefiniti // Modify the the OpenAPI spec to include the additional routes. // Currently this requires: https://github.com/kubernetes/kube-openapi/pull/420 // In future k8s release, the hook will use Config3 rather than the same hook for both v2 and v3 -func getOpenAPIPostProcessor(builders []APIGroupBuilder) func(*spec3.OpenAPI) (*spec3.OpenAPI, error) { +func getOpenAPIPostProcessor(version string, builders []APIGroupBuilder) func(*spec3.OpenAPI) (*spec3.OpenAPI, error) { return func(s *spec3.OpenAPI) (*spec3.OpenAPI, error) { if s.Paths == nil { return s, nil @@ -45,7 +44,7 @@ func getOpenAPIPostProcessor(builders []APIGroupBuilder) func(*spec3.OpenAPI) (* Info: &spec.Info{ InfoProps: spec.InfoProps{ Title: gv.String(), - Version: setting.BuildVersion, + Version: version, }, }, Components: s.Components, diff --git a/pkg/services/apiserver/builder/request_handler.go b/pkg/apiserver/builder/request_handler.go similarity index 100% rename from pkg/services/apiserver/builder/request_handler.go rename to pkg/apiserver/builder/request_handler.go diff --git a/pkg/services/apiserver/endpoints/responsewriter/responsewriter.go b/pkg/apiserver/endpoints/responsewriter/responsewriter.go similarity index 100% rename from pkg/services/apiserver/endpoints/responsewriter/responsewriter.go rename to pkg/apiserver/endpoints/responsewriter/responsewriter.go diff --git a/pkg/services/apiserver/endpoints/responsewriter/responsewriter_test.go b/pkg/apiserver/endpoints/responsewriter/responsewriter_test.go similarity index 97% rename from pkg/services/apiserver/endpoints/responsewriter/responsewriter_test.go rename to pkg/apiserver/endpoints/responsewriter/responsewriter_test.go index a2c0c5eace..4c759ac648 100644 --- a/pkg/services/apiserver/endpoints/responsewriter/responsewriter_test.go +++ b/pkg/apiserver/endpoints/responsewriter/responsewriter_test.go @@ -7,8 +7,9 @@ import ( "testing" "time" - grafanaresponsewriter "github.com/grafana/grafana/pkg/services/apiserver/endpoints/responsewriter" "github.com/stretchr/testify/require" + + grafanaresponsewriter "github.com/grafana/grafana/pkg/apiserver/endpoints/responsewriter" ) func TestResponseAdapter(t *testing.T) { diff --git a/pkg/apiserver/go.mod b/pkg/apiserver/go.mod new file mode 100644 index 0000000000..a977562cac --- /dev/null +++ b/pkg/apiserver/go.mod @@ -0,0 +1,109 @@ +module github.com/grafana/grafana/pkg/apiserver + +go 1.21.0 + +require ( + github.com/bwmarrin/snowflake v0.3.0 + github.com/gorilla/mux v1.8.0 + github.com/stretchr/testify v1.8.4 + golang.org/x/mod v0.14.0 + k8s.io/apimachinery v0.29.2 + k8s.io/apiserver v0.29.2 + k8s.io/client-go v0.29.2 + k8s.io/klog/v2 v2.120.1 + k8s.io/kube-openapi v0.0.0-20240220201932-37d671a357a5 +) + +require ( + github.com/NYTimes/gziphandler v1.1.1 // indirect + github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df // indirect + github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/blang/semver/v4 v4.0.0 // indirect + github.com/cenkalti/backoff/v4 v4.2.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/coreos/go-semver v0.3.1 // indirect + github.com/coreos/go-systemd/v22 v22.5.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/evanphx/json-patch v5.6.0+incompatible // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-openapi/jsonpointer v0.20.2 // indirect + github.com/go-openapi/jsonreference v0.20.4 // indirect + github.com/go-openapi/swag v0.22.9 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/google/btree v1.1.2 // indirect + github.com/google/cel-go v0.17.7 // indirect + github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/google/pprof v0.0.0-20231205033806-a5a03c77bf08 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect + github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.1 // indirect + github.com/imdario/mergo v0.3.16 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jonboulle/clockwork v0.4.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/client_golang v1.18.0 // indirect + github.com/prometheus/client_model v0.5.0 // indirect + github.com/prometheus/common v0.46.0 // indirect + github.com/prometheus/procfs v0.12.0 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/spf13/cobra v1.8.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/stoewer/go-strcase v1.3.0 // indirect + go.etcd.io/etcd/api/v3 v3.5.10 // indirect + go.etcd.io/etcd/client/pkg/v3 v3.5.10 // indirect + go.etcd.io/etcd/client/v3 v3.5.10 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.47.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // indirect + go.opentelemetry.io/otel v1.22.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 // indirect + go.opentelemetry.io/otel/metric v1.22.0 // indirect + go.opentelemetry.io/otel/sdk v1.22.0 // indirect + go.opentelemetry.io/otel/trace v1.22.0 // indirect + go.opentelemetry.io/proto/otlp v1.0.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.26.0 // indirect + golang.org/x/crypto v0.18.0 // indirect + golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb // indirect + golang.org/x/net v0.20.0 // indirect + golang.org/x/oauth2 v0.16.0 // indirect + golang.org/x/sync v0.6.0 // indirect + golang.org/x/sys v0.16.0 // indirect + golang.org/x/term v0.16.0 // indirect + golang.org/x/text v0.14.0 // indirect + golang.org/x/time v0.5.0 // indirect + golang.org/x/tools v0.17.0 // indirect + google.golang.org/appengine v1.6.8 // indirect + google.golang.org/genproto v0.0.0-20231212172506-995d672761c0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20231212172506-995d672761c0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20231212172506-995d672761c0 // indirect + google.golang.org/grpc v1.60.1 // indirect + google.golang.org/protobuf v1.32.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/api v0.29.2 // indirect + k8s.io/component-base v0.29.2 // indirect + k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect + sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.28.0 // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect + sigs.k8s.io/yaml v1.3.0 // indirect +) diff --git a/pkg/apiserver/go.sum b/pkg/apiserver/go.sum new file mode 100644 index 0000000000..d9ce32d70d --- /dev/null +++ b/pkg/apiserver/go.sum @@ -0,0 +1,262 @@ +cloud.google.com/go v0.111.0 h1:YHLKNupSD1KqjDbQ3+LVdQ81h/UJbJyZG203cEfnQgM= +cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk= +cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= +cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= +github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I= +github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df h1:7RFfzj4SSt6nnvCPbCqijJi1nWCd+TqAT3bYCStRC18= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0= +github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4 h1:/inchEIKaYC1Akx+H+gqO04wryn5h75LSazbRlnya1k= +github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= +github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= +github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= +github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/envoyproxy/protoc-gen-validate v1.0.2 h1:QkIBuU5k+x7/QXPvPPnWXWlCdaBFApVqftFV6k087DA= +github.com/envoyproxy/protoc-gen-validate v1.0.2/go.mod h1:GpiZQP3dDbg4JouG/NNS7QWXpgx6x8QiMKdmN72jogE= +github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-logr/zapr v1.2.3 h1:a9vnzlIBPQBBkeaR9IuMUfmVOrQlkoC4YfPoFkX3T7A= +github.com/go-openapi/jsonpointer v0.20.2 h1:mQc3nmndL8ZBzStEo3JYF8wzmeWffDH4VbXz58sAx6Q= +github.com/go-openapi/jsonreference v0.20.4 h1:bKlDxQxQJgwpUSgOENiMPzCTBVuc7vTdXSSgNeAhojU= +github.com/go-openapi/swag v0.22.9 h1:XX2DssF+mQKM2DHsbgZK74y/zj4mo9I99+89xUmuZCE= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= +github.com/google/cel-go v0.17.7 h1:6ebJFzu1xO2n7TLtN+UBqShGBhlD85bhvglh5DpcfqQ= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20231205033806-a5a03c77bf08 h1:PxlBVtIFHR/mtWk2i0gTEdCz+jBnqiuHNSki0epDbVs= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340 h1:uGoIog/wiQHI9GAxXO5TJbT0wWKH3O9HhOJW1F9c3fY= +github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.1 h1:6UKoz5ujsI55KNpsJH3UwCq3T8kKbZwNZBNPuTTje8U= +github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4= +github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o= +github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg= +github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk= +github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= +github.com/prometheus/common v0.46.0 h1:doXzt5ybi1HBKpsZOL0sSkaNHJJqkyfEWZGGqqScV0Y= +github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js= +github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 h1:6fotK7otjonDflCTK0BCfls4SPy3NcCVb5dqqmbRknE= +github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75/go.mod h1:KO6IkyS8Y3j8OdNO85qEYBsRPuteD+YciPomcXdrMnk= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.etcd.io/bbolt v1.3.8 h1:xs88BrvEv273UsB79e0hcVrlUWmS0a8upikMFhSyAtA= +go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= +go.etcd.io/etcd/api/v3 v3.5.10 h1:szRajuUUbLyppkhs9K6BRtjY37l66XQQmw7oZRANE4k= +go.etcd.io/etcd/api/v3 v3.5.10/go.mod h1:TidfmT4Uycad3NM/o25fG3J07odo4GBB9hoxaodFCtI= +go.etcd.io/etcd/client/pkg/v3 v3.5.10 h1:kfYIdQftBnbAq8pUWFXfpuuxFSKzlmM5cSn76JByiT0= +go.etcd.io/etcd/client/pkg/v3 v3.5.10/go.mod h1:DYivfIviIuQ8+/lCq4vcxuseg2P2XbHygkKwFo9fc8U= +go.etcd.io/etcd/client/v2 v2.305.10 h1:MrmRktzv/XF8CvtQt+P6wLUlURaNpSDJHFZhe//2QE4= +go.etcd.io/etcd/client/v2 v2.305.10/go.mod h1:m3CKZi69HzilhVqtPDcjhSGp+kA1OmbNn0qamH80xjA= +go.etcd.io/etcd/client/v3 v3.5.10 h1:W9TXNZ+oB3MCd/8UjxHTWK5J9Nquw9fQBLJd5ne5/Ao= +go.etcd.io/etcd/client/v3 v3.5.10/go.mod h1:RVeBnDz2PUEZqTpgqwAtUd8nAPf5kjyFyND7P1VkOKc= +go.etcd.io/etcd/pkg/v3 v3.5.10 h1:WPR8K0e9kWl1gAhB5A7gEa5ZBTNkT9NdNWrR8Qpo1CM= +go.etcd.io/etcd/pkg/v3 v3.5.10/go.mod h1:TKTuCKKcF1zxmfKWDkfz5qqYaE3JncKKZPFf8c1nFUs= +go.etcd.io/etcd/raft/v3 v3.5.10 h1:cgNAYe7xrsrn/5kXMSaH8kM/Ky8mAdMqGOxyYwpP0LA= +go.etcd.io/etcd/raft/v3 v3.5.10/go.mod h1:odD6kr8XQXTy9oQnyMPBOr0TVe+gT0neQhElQ6jbGRc= +go.etcd.io/etcd/server/v3 v3.5.10 h1:4NOGyOwD5sUZ22PiWYKmfxqoeh72z6EhYjNosKGLmZg= +go.etcd.io/etcd/server/v3 v3.5.10/go.mod h1:gBplPHfs6YI0L+RpGkTQO7buDbHv5HJGG/Bst0/zIPo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.47.0 h1:UNQQKPfTDe1J81ViolILjTKPr9WetKW6uei2hFgJmFs= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24= +go.opentelemetry.io/otel v1.22.0 h1:xS7Ku+7yTFvDfDraDIJVpw7XPyuHlB9MCiqqX5mcJ6Y= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 h1:cl5P5/GIfFh4t6xyruOgJP5QiA1pw4fYYdv6nc6CBWw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 h1:tIqheXEFWAZ7O8A7m+J0aPTmpJN3YQ7qetUAdkkkKpk= +go.opentelemetry.io/otel/metric v1.22.0 h1:lypMQnGyJYeuYPhOM/bgjbFM6WE44W1/T45er4d8Hhg= +go.opentelemetry.io/otel/sdk v1.22.0 h1:6coWHw9xw7EfClIC/+O31R8IY3/+EiRFHevmHafB2Gw= +go.opentelemetry.io/otel/trace v1.22.0 h1:Hg6pPujv0XG9QaVbGOBVHunyuLcCC3jN7WEhPx83XD0= +go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= +go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= +golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb h1:c0vyKkb6yr3KR7jEfJaOSv4lG7xPkbN6r52aJz1d8a8= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= +golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/genproto v0.0.0-20231212172506-995d672761c0 h1:YJ5pD9rF8o9Qtta0Cmy9rdBwkSjrTCT6XTiUQVOtIos= +google.golang.org/genproto/googleapis/api v0.0.0-20231212172506-995d672761c0 h1:s1w3X6gQxwrLEpxnLd/qXTVLgQE2yXwaOaoa6IlY/+o= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231212172506-995d672761c0 h1:/jFB8jK5R3Sq3i/lmeZO0cATSzFfZaJq1J2Euan3XKU= +google.golang.org/grpc v1.60.1 h1:26+wFr+cNqSGFcOXcabYC0lUVJVRa2Sb2ortSK7VrEU= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.29.2 h1:hBC7B9+MU+ptchxEqTNW2DkUosJpp1P+Wn6YncZ474A= +k8s.io/api v0.29.2/go.mod h1:sdIaaKuU7P44aoyyLlikSLayT6Vb7bvJNCX105xZXY0= +k8s.io/apimachinery v0.29.2 h1:EWGpfJ856oj11C52NRCHuU7rFDwxev48z+6DSlGNsV8= +k8s.io/apimachinery v0.29.2/go.mod h1:6HVkd1FwxIagpYrHSwJlQqZI3G9LfYWRPAkUvLnXTKU= +k8s.io/apiserver v0.29.2 h1:+Z9S0dSNr+CjnVXQePG8TcBWHr3Q7BmAr7NraHvsMiQ= +k8s.io/apiserver v0.29.2/go.mod h1:B0LieKVoyU7ykQvPFm7XSdIHaCHSzCzQWPFa5bqbeMQ= +k8s.io/client-go v0.29.2 h1:FEg85el1TeZp+/vYJM7hkDlSTFZ+c5nnK44DJ4FyoRg= +k8s.io/client-go v0.29.2/go.mod h1:knlvFZE58VpqbQpJNbCbctTVXcd35mMyAAwBdpt4jrA= +k8s.io/component-base v0.29.2 h1:lpiLyuvPA9yV1aQwGLENYyK7n/8t6l3nn3zAtFTJYe8= +k8s.io/component-base v0.29.2/go.mod h1:BfB3SLrefbZXiBfbM+2H1dlat21Uewg/5qtKOl8degM= +k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= +k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20240220201932-37d671a357a5 h1:QSpdNrZ9uRlV0VkqLvVO0Rqg8ioKi3oSw7O5P7pJV8M= +k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= +k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.28.0 h1:TgtAeesdhpm2SGwkQasmbeqDo8th5wOBA5h/AjTKA4I= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.28.0/go.mod h1:VHVDI/KrK4fjnV61bE2g3sA7tiETLn8sooImelsCx3Y= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/pkg/services/apiserver/registry/generic/strategy.go b/pkg/apiserver/registry/generic/strategy.go similarity index 100% rename from pkg/services/apiserver/registry/generic/strategy.go rename to pkg/apiserver/registry/generic/strategy.go diff --git a/pkg/services/apiserver/rest/dualwriter.go b/pkg/apiserver/rest/dualwriter.go similarity index 97% rename from pkg/services/apiserver/rest/dualwriter.go rename to pkg/apiserver/rest/dualwriter.go index 7200172249..81dcca18bf 100644 --- a/pkg/services/apiserver/rest/dualwriter.go +++ b/pkg/apiserver/rest/dualwriter.go @@ -8,8 +8,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apiserver/pkg/registry/rest" - - "github.com/grafana/grafana/pkg/infra/log" + "k8s.io/klog/v2" ) var ( @@ -64,7 +63,6 @@ type LegacyStorage interface { type DualWriter struct { Storage legacy LegacyStorage - log log.Logger } // NewDualWriter returns a new DualWriter. @@ -72,7 +70,6 @@ func NewDualWriter(legacy LegacyStorage, storage Storage) *DualWriter { return &DualWriter{ Storage: storage, legacy: legacy, - log: log.New("grafana-apiserver.dualwriter"), } } @@ -93,7 +90,7 @@ func (d *DualWriter) Create(ctx context.Context, obj runtime.Object, createValid rsp, err := d.Storage.Create(ctx, created, createValidation, options) if err != nil { - d.log.Error("unable to create object in duplicate storage", "error", err) + klog.Error("unable to create object in duplicate storage", "error", err) } return rsp, err } diff --git a/pkg/services/apiserver/storage/file/file.go b/pkg/apiserver/storage/file/file.go similarity index 100% rename from pkg/services/apiserver/storage/file/file.go rename to pkg/apiserver/storage/file/file.go diff --git a/pkg/services/apiserver/storage/file/restoptions.go b/pkg/apiserver/storage/file/restoptions.go similarity index 100% rename from pkg/services/apiserver/storage/file/restoptions.go rename to pkg/apiserver/storage/file/restoptions.go diff --git a/pkg/services/apiserver/storage/file/util.go b/pkg/apiserver/storage/file/util.go similarity index 100% rename from pkg/services/apiserver/storage/file/util.go rename to pkg/apiserver/storage/file/util.go diff --git a/pkg/services/apiserver/storage/file/watchset.go b/pkg/apiserver/storage/file/watchset.go similarity index 100% rename from pkg/services/apiserver/storage/file/watchset.go rename to pkg/apiserver/storage/file/watchset.go diff --git a/pkg/cmd/grafana/apiserver/server.go b/pkg/cmd/grafana/apiserver/server.go index 19a9236db6..246123d029 100644 --- a/pkg/cmd/grafana/apiserver/server.go +++ b/pkg/cmd/grafana/apiserver/server.go @@ -13,10 +13,11 @@ import ( "k8s.io/client-go/tools/clientcmd" netutils "k8s.io/utils/net" + "github.com/grafana/grafana/pkg/apiserver/builder" grafanaAPIServer "github.com/grafana/grafana/pkg/services/apiserver" - "github.com/grafana/grafana/pkg/services/apiserver/builder" "github.com/grafana/grafana/pkg/services/apiserver/standalone" "github.com/grafana/grafana/pkg/services/apiserver/utils" + "github.com/grafana/grafana/pkg/setting" ) const ( @@ -151,7 +152,15 @@ func (o *APIServerOptions) Config() (*genericapiserver.RecommendedConfig, error) serverConfig.DisabledPostStartHooks = serverConfig.DisabledPostStartHooks.Insert("priority-and-fairness-config-consumer") // Add OpenAPI specs for each group+version - err := builder.SetupConfig(grafanaAPIServer.Scheme, serverConfig, o.builders) + err := builder.SetupConfig( + grafanaAPIServer.Scheme, + serverConfig, + o.builders, + setting.BuildStamp, + setting.BuildVersion, + setting.BuildCommit, + setting.BuildBranch, + ) return serverConfig, err } diff --git a/pkg/registry/apis/dashboard/legacy_storage.go b/pkg/registry/apis/dashboard/legacy_storage.go index e5a3a5ede6..20e5193b24 100644 --- a/pkg/registry/apis/dashboard/legacy_storage.go +++ b/pkg/registry/apis/dashboard/legacy_storage.go @@ -11,7 +11,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apiserver/pkg/registry/rest" - common "github.com/grafana/grafana/pkg/apis/common/v0alpha1" + common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1" "github.com/grafana/grafana/pkg/registry/apis/dashboard/access" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" diff --git a/pkg/registry/apis/dashboard/register.go b/pkg/registry/apis/dashboard/register.go index e6434240b1..643155dd28 100644 --- a/pkg/registry/apis/dashboard/register.go +++ b/pkg/registry/apis/dashboard/register.go @@ -15,14 +15,14 @@ import ( common "k8s.io/kube-openapi/pkg/common" "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1" + "github.com/grafana/grafana/pkg/apiserver/builder" + grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic" + grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/registry/apis/dashboard/access" "github.com/grafana/grafana/pkg/services/accesscontrol" - "github.com/grafana/grafana/pkg/services/apiserver/builder" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" - grafanaregistry "github.com/grafana/grafana/pkg/services/apiserver/registry/generic" - grafanarest "github.com/grafana/grafana/pkg/services/apiserver/rest" "github.com/grafana/grafana/pkg/services/apiserver/utils" "github.com/grafana/grafana/pkg/services/dashboards" dashver "github.com/grafana/grafana/pkg/services/dashboardversion" diff --git a/pkg/registry/apis/dashboard/sub_versions.go b/pkg/registry/apis/dashboard/sub_versions.go index a38f9947d8..858d3e6780 100644 --- a/pkg/registry/apis/dashboard/sub_versions.go +++ b/pkg/registry/apis/dashboard/sub_versions.go @@ -11,7 +11,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apiserver/pkg/registry/rest" - common "github.com/grafana/grafana/pkg/apis/common/v0alpha1" + common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" dashver "github.com/grafana/grafana/pkg/services/dashboardversion" diff --git a/pkg/registry/apis/dashboard/summary_storage.go b/pkg/registry/apis/dashboard/summary_storage.go index 17311be137..9f8966e012 100644 --- a/pkg/registry/apis/dashboard/summary_storage.go +++ b/pkg/registry/apis/dashboard/summary_storage.go @@ -8,7 +8,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apiserver/pkg/registry/rest" - common "github.com/grafana/grafana/pkg/apis/common/v0alpha1" + common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" "github.com/grafana/grafana/pkg/registry/apis/dashboard/access" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" ) diff --git a/pkg/registry/apis/dashboardsnapshot/exporter.go b/pkg/registry/apis/dashboardsnapshot/exporter.go index fdf8caab9e..18dbd203ea 100644 --- a/pkg/registry/apis/dashboardsnapshot/exporter.go +++ b/pkg/registry/apis/dashboardsnapshot/exporter.go @@ -10,8 +10,8 @@ import ( "gocloud.dev/blob" "k8s.io/kube-openapi/pkg/spec3" + "github.com/grafana/grafana/pkg/apiserver/builder" "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/services/apiserver/builder" "github.com/grafana/grafana/pkg/services/dashboardsnapshots" ) diff --git a/pkg/registry/apis/dashboardsnapshot/register.go b/pkg/registry/apis/dashboardsnapshot/register.go index 98a42c684b..0034bb2b9e 100644 --- a/pkg/registry/apis/dashboardsnapshot/register.go +++ b/pkg/registry/apis/dashboardsnapshot/register.go @@ -21,10 +21,10 @@ import ( "k8s.io/kube-openapi/pkg/validation/spec" dashboardsnapshot "github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1" + "github.com/grafana/grafana/pkg/apiserver/builder" "github.com/grafana/grafana/pkg/infra/appcontext" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/services/apiserver/builder" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/apiserver/utils" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" diff --git a/pkg/registry/apis/dashboardsnapshot/sub_body.go b/pkg/registry/apis/dashboardsnapshot/sub_body.go index 45614b5e63..9d0a7349e4 100644 --- a/pkg/registry/apis/dashboardsnapshot/sub_body.go +++ b/pkg/registry/apis/dashboardsnapshot/sub_body.go @@ -7,7 +7,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apiserver/pkg/registry/rest" - common "github.com/grafana/grafana/pkg/apis/common/v0alpha1" + common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" dashboardsnapshot "github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/dashboardsnapshots" diff --git a/pkg/registry/apis/datasource/connections.go b/pkg/registry/apis/datasource/connections.go index c6d0708d31..3e701f59d5 100644 --- a/pkg/registry/apis/datasource/connections.go +++ b/pkg/registry/apis/datasource/connections.go @@ -8,7 +8,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apiserver/pkg/registry/rest" - common "github.com/grafana/grafana/pkg/apis/common/v0alpha1" + common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" ) var ( diff --git a/pkg/registry/apis/datasource/querier.go b/pkg/registry/apis/datasource/querier.go index 9d5b9ff470..5811dbb837 100644 --- a/pkg/registry/apis/datasource/querier.go +++ b/pkg/registry/apis/datasource/querier.go @@ -6,7 +6,7 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - common "github.com/grafana/grafana/pkg/apis/common/v0alpha1" + common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1" "github.com/grafana/grafana/pkg/infra/appcontext" "github.com/grafana/grafana/pkg/plugins" diff --git a/pkg/registry/apis/datasource/register.go b/pkg/registry/apis/datasource/register.go index b16e9987d4..9cc158188b 100644 --- a/pkg/registry/apis/datasource/register.go +++ b/pkg/registry/apis/datasource/register.go @@ -18,12 +18,12 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" - common "github.com/grafana/grafana/pkg/apis/common/v0alpha1" + common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1" query "github.com/grafana/grafana/pkg/apis/query/v0alpha1" + "github.com/grafana/grafana/pkg/apiserver/builder" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/accesscontrol" - "github.com/grafana/grafana/pkg/services/apiserver/builder" "github.com/grafana/grafana/pkg/services/apiserver/utils" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" diff --git a/pkg/registry/apis/example/dummy_storage.go b/pkg/registry/apis/example/dummy_storage.go index 38184338a6..574a87dd52 100644 --- a/pkg/registry/apis/example/dummy_storage.go +++ b/pkg/registry/apis/example/dummy_storage.go @@ -12,10 +12,10 @@ import ( genericregistry "k8s.io/apiserver/pkg/registry/generic/registry" "k8s.io/apiserver/pkg/registry/rest" - common "github.com/grafana/grafana/pkg/apis/common/v0alpha1" + common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" example "github.com/grafana/grafana/pkg/apis/example/v0alpha1" + grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic" grafanarequest "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" - grafanaregistry "github.com/grafana/grafana/pkg/services/apiserver/registry/generic" ) var ( diff --git a/pkg/registry/apis/example/register.go b/pkg/registry/apis/example/register.go index 69a4add5d5..c18d5d4c4e 100644 --- a/pkg/registry/apis/example/register.go +++ b/pkg/registry/apis/example/register.go @@ -21,8 +21,8 @@ import ( "k8s.io/kube-openapi/pkg/validation/spec" example "github.com/grafana/grafana/pkg/apis/example/v0alpha1" + "github.com/grafana/grafana/pkg/apiserver/builder" "github.com/grafana/grafana/pkg/infra/appcontext" - "github.com/grafana/grafana/pkg/services/apiserver/builder" "github.com/grafana/grafana/pkg/services/featuremgmt" ) diff --git a/pkg/registry/apis/example/storage.go b/pkg/registry/apis/example/storage.go index e2e198fa0b..4e2c185883 100644 --- a/pkg/registry/apis/example/storage.go +++ b/pkg/registry/apis/example/storage.go @@ -12,7 +12,7 @@ import ( "k8s.io/apiserver/pkg/registry/rest" example "github.com/grafana/grafana/pkg/apis/example/v0alpha1" - grafanaregistry "github.com/grafana/grafana/pkg/services/apiserver/registry/generic" + grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic" "github.com/grafana/grafana/pkg/setting" ) diff --git a/pkg/registry/apis/featuretoggle/current.go b/pkg/registry/apis/featuretoggle/current.go index aa8204fc4a..97d59a76a1 100644 --- a/pkg/registry/apis/featuretoggle/current.go +++ b/pkg/registry/apis/featuretoggle/current.go @@ -11,7 +11,7 @@ import ( v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - common "github.com/grafana/grafana/pkg/apis/common/v0alpha1" + common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" "github.com/grafana/grafana/pkg/apis/featuretoggle/v0alpha1" "github.com/grafana/grafana/pkg/cmd/grafana-cli/logger" "github.com/grafana/grafana/pkg/infra/appcontext" diff --git a/pkg/registry/apis/featuretoggle/features.go b/pkg/registry/apis/featuretoggle/features.go index 37d8352a0c..6902d8bb3c 100644 --- a/pkg/registry/apis/featuretoggle/features.go +++ b/pkg/registry/apis/featuretoggle/features.go @@ -10,7 +10,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apiserver/pkg/registry/rest" - common "github.com/grafana/grafana/pkg/apis/common/v0alpha1" + common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" "github.com/grafana/grafana/pkg/apis/featuretoggle/v0alpha1" "github.com/grafana/grafana/pkg/services/apiserver/utils" "github.com/grafana/grafana/pkg/services/featuremgmt" diff --git a/pkg/registry/apis/featuretoggle/register.go b/pkg/registry/apis/featuretoggle/register.go index 205f3a25e0..e93b396d55 100644 --- a/pkg/registry/apis/featuretoggle/register.go +++ b/pkg/registry/apis/featuretoggle/register.go @@ -14,8 +14,8 @@ import ( "k8s.io/kube-openapi/pkg/validation/spec" "github.com/grafana/grafana/pkg/apis/featuretoggle/v0alpha1" + "github.com/grafana/grafana/pkg/apiserver/builder" "github.com/grafana/grafana/pkg/services/accesscontrol" - "github.com/grafana/grafana/pkg/services/apiserver/builder" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/setting" ) diff --git a/pkg/registry/apis/featuretoggle/toggles.go b/pkg/registry/apis/featuretoggle/toggles.go index 81f5c092a3..9c920c5a0b 100644 --- a/pkg/registry/apis/featuretoggle/toggles.go +++ b/pkg/registry/apis/featuretoggle/toggles.go @@ -9,7 +9,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apiserver/pkg/registry/rest" - common "github.com/grafana/grafana/pkg/apis/common/v0alpha1" + common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" "github.com/grafana/grafana/pkg/apis/featuretoggle/v0alpha1" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/featuremgmt" diff --git a/pkg/registry/apis/folders/register.go b/pkg/registry/apis/folders/register.go index 5147d80164..5c2b48f8c3 100644 --- a/pkg/registry/apis/folders/register.go +++ b/pkg/registry/apis/folders/register.go @@ -15,11 +15,11 @@ import ( common "k8s.io/kube-openapi/pkg/common" "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" + "github.com/grafana/grafana/pkg/apiserver/builder" + grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/infra/appcontext" "github.com/grafana/grafana/pkg/services/accesscontrol" - "github.com/grafana/grafana/pkg/services/apiserver/builder" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" - grafanarest "github.com/grafana/grafana/pkg/services/apiserver/rest" "github.com/grafana/grafana/pkg/services/apiserver/utils" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/featuremgmt" diff --git a/pkg/registry/apis/folders/storage.go b/pkg/registry/apis/folders/storage.go index dc90e9ff79..1466a703f5 100644 --- a/pkg/registry/apis/folders/storage.go +++ b/pkg/registry/apis/folders/storage.go @@ -6,8 +6,8 @@ import ( genericregistry "k8s.io/apiserver/pkg/registry/generic/registry" "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" - grafanaregistry "github.com/grafana/grafana/pkg/services/apiserver/registry/generic" - grafanarest "github.com/grafana/grafana/pkg/services/apiserver/rest" + grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic" + grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" ) var _ grafanarest.Storage = (*storage)(nil) diff --git a/pkg/registry/apis/peakq/register.go b/pkg/registry/apis/peakq/register.go index 3be6d11d03..076b3b427a 100644 --- a/pkg/registry/apis/peakq/register.go +++ b/pkg/registry/apis/peakq/register.go @@ -14,7 +14,7 @@ import ( "k8s.io/kube-openapi/pkg/validation/spec" peakq "github.com/grafana/grafana/pkg/apis/peakq/v0alpha1" - "github.com/grafana/grafana/pkg/services/apiserver/builder" + "github.com/grafana/grafana/pkg/apiserver/builder" "github.com/grafana/grafana/pkg/services/featuremgmt" ) diff --git a/pkg/registry/apis/peakq/storage.go b/pkg/registry/apis/peakq/storage.go index d65bdbc771..c3bd23aac3 100644 --- a/pkg/registry/apis/peakq/storage.go +++ b/pkg/registry/apis/peakq/storage.go @@ -10,8 +10,8 @@ import ( genericregistry "k8s.io/apiserver/pkg/registry/generic/registry" peakq "github.com/grafana/grafana/pkg/apis/peakq/v0alpha1" - grafanaregistry "github.com/grafana/grafana/pkg/services/apiserver/registry/generic" - grafanarest "github.com/grafana/grafana/pkg/services/apiserver/rest" + grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic" + grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/services/apiserver/utils" ) diff --git a/pkg/registry/apis/playlist/register.go b/pkg/registry/apis/playlist/register.go index 857ad5bca4..59b10b2cca 100644 --- a/pkg/registry/apis/playlist/register.go +++ b/pkg/registry/apis/playlist/register.go @@ -15,9 +15,9 @@ import ( common "k8s.io/kube-openapi/pkg/common" playlist "github.com/grafana/grafana/pkg/apis/playlist/v0alpha1" - "github.com/grafana/grafana/pkg/services/apiserver/builder" + "github.com/grafana/grafana/pkg/apiserver/builder" + grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" - grafanarest "github.com/grafana/grafana/pkg/services/apiserver/rest" "github.com/grafana/grafana/pkg/services/apiserver/utils" playlistsvc "github.com/grafana/grafana/pkg/services/playlist" "github.com/grafana/grafana/pkg/setting" diff --git a/pkg/registry/apis/playlist/storage.go b/pkg/registry/apis/playlist/storage.go index eb7512c717..0c537e3127 100644 --- a/pkg/registry/apis/playlist/storage.go +++ b/pkg/registry/apis/playlist/storage.go @@ -6,8 +6,8 @@ import ( genericregistry "k8s.io/apiserver/pkg/registry/generic/registry" playlist "github.com/grafana/grafana/pkg/apis/playlist/v0alpha1" - grafanaregistry "github.com/grafana/grafana/pkg/services/apiserver/registry/generic" - grafanarest "github.com/grafana/grafana/pkg/services/apiserver/rest" + grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic" + grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" ) var _ grafanarest.Storage = (*storage)(nil) diff --git a/pkg/registry/apis/query/plugins.go b/pkg/registry/apis/query/plugins.go index 786ba1a5c6..1836602d4d 100644 --- a/pkg/registry/apis/query/plugins.go +++ b/pkg/registry/apis/query/plugins.go @@ -8,7 +8,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apiserver/pkg/registry/rest" - common "github.com/grafana/grafana/pkg/apis/common/v0alpha1" + common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" example "github.com/grafana/grafana/pkg/apis/example/v0alpha1" query "github.com/grafana/grafana/pkg/apis/query/v0alpha1" ) diff --git a/pkg/registry/apis/query/register.go b/pkg/registry/apis/query/register.go index e1f3fa02cd..e4e218e7ef 100644 --- a/pkg/registry/apis/query/register.go +++ b/pkg/registry/apis/query/register.go @@ -17,11 +17,11 @@ import ( "k8s.io/kube-openapi/pkg/validation/spec" "github.com/grafana/grafana/pkg/apis/query/v0alpha1" + "github.com/grafana/grafana/pkg/apiserver/builder" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/registry/apis/query/runner" "github.com/grafana/grafana/pkg/services/accesscontrol" - "github.com/grafana/grafana/pkg/services/apiserver/builder" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext" diff --git a/pkg/registry/apis/scope/register.go b/pkg/registry/apis/scope/register.go index 3de7605c67..a2095ea570 100644 --- a/pkg/registry/apis/scope/register.go +++ b/pkg/registry/apis/scope/register.go @@ -12,7 +12,7 @@ import ( "k8s.io/kube-openapi/pkg/common" scope "github.com/grafana/grafana/pkg/apis/scope/v0alpha1" - "github.com/grafana/grafana/pkg/services/apiserver/builder" + "github.com/grafana/grafana/pkg/apiserver/builder" "github.com/grafana/grafana/pkg/services/featuremgmt" ) diff --git a/pkg/registry/apis/scope/storage.go b/pkg/registry/apis/scope/storage.go index c0083c946f..c01f93a409 100644 --- a/pkg/registry/apis/scope/storage.go +++ b/pkg/registry/apis/scope/storage.go @@ -10,8 +10,8 @@ import ( genericregistry "k8s.io/apiserver/pkg/registry/generic/registry" scope "github.com/grafana/grafana/pkg/apis/scope/v0alpha1" - grafanaregistry "github.com/grafana/grafana/pkg/services/apiserver/registry/generic" - grafanarest "github.com/grafana/grafana/pkg/services/apiserver/rest" + grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic" + grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/services/apiserver/utils" ) diff --git a/pkg/registry/apis/service/register.go b/pkg/registry/apis/service/register.go index 2d55353e2b..2032b8ea0e 100644 --- a/pkg/registry/apis/service/register.go +++ b/pkg/registry/apis/service/register.go @@ -12,7 +12,7 @@ import ( "k8s.io/kube-openapi/pkg/common" service "github.com/grafana/grafana/pkg/apis/service/v0alpha1" - "github.com/grafana/grafana/pkg/services/apiserver/builder" + "github.com/grafana/grafana/pkg/apiserver/builder" "github.com/grafana/grafana/pkg/services/featuremgmt" ) diff --git a/pkg/registry/apis/service/storage.go b/pkg/registry/apis/service/storage.go index bf0d8551b8..a21023996d 100644 --- a/pkg/registry/apis/service/storage.go +++ b/pkg/registry/apis/service/storage.go @@ -10,8 +10,8 @@ import ( genericregistry "k8s.io/apiserver/pkg/registry/generic/registry" service "github.com/grafana/grafana/pkg/apis/service/v0alpha1" - grafanaregistry "github.com/grafana/grafana/pkg/services/apiserver/registry/generic" - grafanarest "github.com/grafana/grafana/pkg/services/apiserver/rest" + grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic" + grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/services/apiserver/utils" ) diff --git a/pkg/services/apiserver/options/aggregator.go b/pkg/services/apiserver/options/aggregator.go index 33c210ae53..2ba0736fb0 100644 --- a/pkg/services/apiserver/options/aggregator.go +++ b/pkg/services/apiserver/options/aggregator.go @@ -18,7 +18,7 @@ import ( "k8s.io/kube-openapi/pkg/common" servicev0alpha1 "github.com/grafana/grafana/pkg/apis/service/v0alpha1" - filestorage "github.com/grafana/grafana/pkg/services/apiserver/storage/file" + filestorage "github.com/grafana/grafana/pkg/apiserver/storage/file" ) // AggregatorServerOptions contains the state for the aggregator apiserver diff --git a/pkg/services/apiserver/service.go b/pkg/services/apiserver/service.go index aaa5f4ad37..c050072249 100644 --- a/pkg/services/apiserver/service.go +++ b/pkg/services/apiserver/service.go @@ -19,6 +19,9 @@ import ( "k8s.io/client-go/tools/clientcmd" "github.com/grafana/grafana/pkg/api/routing" + "github.com/grafana/grafana/pkg/apiserver/builder" + grafanaresponsewriter "github.com/grafana/grafana/pkg/apiserver/endpoints/responsewriter" + filestorage "github.com/grafana/grafana/pkg/apiserver/storage/file" "github.com/grafana/grafana/pkg/infra/appcontext" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/tracing" @@ -28,11 +31,8 @@ import ( "github.com/grafana/grafana/pkg/services/apiserver/aggregator" "github.com/grafana/grafana/pkg/services/apiserver/auth/authenticator" "github.com/grafana/grafana/pkg/services/apiserver/auth/authorizer" - "github.com/grafana/grafana/pkg/services/apiserver/builder" - grafanaresponsewriter "github.com/grafana/grafana/pkg/services/apiserver/endpoints/responsewriter" grafanaapiserveroptions "github.com/grafana/grafana/pkg/services/apiserver/options" entitystorage "github.com/grafana/grafana/pkg/services/apiserver/storage/entity" - filestorage "github.com/grafana/grafana/pkg/services/apiserver/storage/file" "github.com/grafana/grafana/pkg/services/apiserver/utils" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -280,7 +280,15 @@ func (s *service) start(ctx context.Context) error { } // Add OpenAPI specs for each group+version - err := builder.SetupConfig(Scheme, serverConfig, builders) + err := builder.SetupConfig( + Scheme, + serverConfig, + builders, + s.cfg.BuildStamp, + s.cfg.BuildVersion, + s.cfg.BuildCommit, + s.cfg.BuildBranch, + ) if err != nil { return err } diff --git a/pkg/services/apiserver/standalone/factory.go b/pkg/services/apiserver/standalone/factory.go index b9a7f4455c..9c13bf4bd0 100644 --- a/pkg/services/apiserver/standalone/factory.go +++ b/pkg/services/apiserver/standalone/factory.go @@ -9,6 +9,7 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1" + "github.com/grafana/grafana/pkg/apiserver/builder" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/registry/apis/datasource" "github.com/grafana/grafana/pkg/registry/apis/example" @@ -16,7 +17,6 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/query" "github.com/grafana/grafana/pkg/registry/apis/query/runner" "github.com/grafana/grafana/pkg/services/accesscontrol/actest" - "github.com/grafana/grafana/pkg/services/apiserver/builder" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/apiserver/options" "github.com/grafana/grafana/pkg/services/featuremgmt" diff --git a/pkg/services/apiserver/wireset.go b/pkg/services/apiserver/wireset.go index f7163f1ad6..f8e840e424 100644 --- a/pkg/services/apiserver/wireset.go +++ b/pkg/services/apiserver/wireset.go @@ -3,7 +3,7 @@ package apiserver import ( "github.com/google/wire" - "github.com/grafana/grafana/pkg/services/apiserver/builder" + "github.com/grafana/grafana/pkg/apiserver/builder" ) var WireSet = wire.NewSet( diff --git a/pkg/services/dashboardsnapshots/database/database_test.go b/pkg/services/dashboardsnapshots/database/database_test.go index 3a2b732ae4..45e53d3ef6 100644 --- a/pkg/services/dashboardsnapshots/database/database_test.go +++ b/pkg/services/dashboardsnapshots/database/database_test.go @@ -8,7 +8,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - common "github.com/grafana/grafana/pkg/apis/common/v0alpha1" + common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" dashboardsnapshot "github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db" diff --git a/pkg/services/dashboardsnapshots/service.go b/pkg/services/dashboardsnapshots/service.go index 07bcea602c..07ae3423c6 100644 --- a/pkg/services/dashboardsnapshots/service.go +++ b/pkg/services/dashboardsnapshots/service.go @@ -8,7 +8,7 @@ import ( "net/http" "time" - common "github.com/grafana/grafana/pkg/apis/common/v0alpha1" + common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" dashboardsnapshot "github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" diff --git a/pkg/services/dashboardsnapshots/service/service_test.go b/pkg/services/dashboardsnapshots/service/service_test.go index 70c924e164..f71de4d176 100644 --- a/pkg/services/dashboardsnapshots/service/service_test.go +++ b/pkg/services/dashboardsnapshots/service/service_test.go @@ -7,7 +7,7 @@ import ( "github.com/stretchr/testify/require" - common "github.com/grafana/grafana/pkg/apis/common/v0alpha1" + common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" dashboardsnapshot "github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/services/dashboardsnapshots" From dfeb33fe5594195f039ef83884002e4773a6b60e Mon Sep 17 00:00:00 2001 From: Isabel <76437239+imatwawana@users.noreply.github.com> Date: Fri, 23 Feb 2024 16:51:00 -0500 Subject: [PATCH 0149/1406] Docs: restructure Configure field overrides (#81833) * Removed view and delete overrides sections * Added examples heading and moved examples down one heading level * Added override rules section and removed rule definitions from task * Added supported visualizations section and table and docs ref links * Docs: edit Configure field overrides (#81834) * Formatted note * Added missing content and general edits * Updated screenshots and examples and general edits * Fix small formatting issues * Fixed links * Uploaded images to admin, updated image links, and removed local images * Swapped figure shortcode for simple Markdown --- .../configure-overrides/index.md | 187 ++++++++++++++---- 1 file changed, 145 insertions(+), 42 deletions(-) diff --git a/docs/sources/panels-visualizations/configure-overrides/index.md b/docs/sources/panels-visualizations/configure-overrides/index.md index 6f875238ee..64472b8110 100644 --- a/docs/sources/panels-visualizations/configure-overrides/index.md +++ b/docs/sources/panels-visualizations/configure-overrides/index.md @@ -21,13 +21,61 @@ weight: 110 # Configure field overrides -Overrides allow you to customize visualization settings for specific fields or series. This is accomplished by adding an override rule that targets a particular set of fields and that can each define multiple options. +Overrides allow you to customize visualization settings for specific fields or series. When you add an override rule, it targets a particular set of fields and lets you define multiple options for how that field is displayed. -For example, you set the unit for all fields that include the text 'bytes' by adding an override using the `Fields with name matching regex` matcher and then add the Unit option to the override rule. +For example, you can override the default unit measurement for all fields that include the text "bytes" by adding an override using the **Fields with name matching regex** matcher and then the **Standard options > Unit** setting to the override rule: -## Example 1: Format temperature +![Field with unit override](/media/docs/grafana/panels-visualizations/screenshot-unit-override-v10.3.png) -Let’s assume that our result set is a data frame that consists of two fields: time and temperature. +After you've set them, your overrides appear in both the **All** and **Overrides** tabs of the panel editor pane: + +![All and Overrides tabs of panel editor pane](/media/docs/grafana/panels-visualizations/screenshot-all-overrides-tabs-v11.png) + +## Supported visualizations + +You can configure field overrides for the following visualizations: + +| | | | +| -------------------------- | ---------------------- | -------------------------------- | +| [Bar chart][bar chart] | [Geomap][geomap] | [State timeline][state timeline] | +| [Bar gauge][bar gauge] | [Heatmap][heatmap] | [Status history][status history] | +| [Candlestick][candlestick] | [Histogram][histogram] | [Table][table] | +| [Canvas][canvas] | [Pie chart][pie chart] | [Time series][time series] | +| [Gauge][gauge] | [Stat][stat] | [Trend][trend] | + + + +## Override rules + +You can choose from five types of override rules, which are described in the following sections. + +### Fields with name + +Select a field from the list of all available fields. Properties you add to this type of rule are only applied to this single field. + +### Fields with name matching regex + +Specify fields to override with a regular expression. Properties you add to this type of rule are applied to all fields where the field name matches the regular expression. This override doesn't rename the field; to do this, use the [Rename by regex transformation][]. + +### Fields with type + +Select fields by type, such as string, numeric, or time. Properties you add to this type of rule are applied to all fields that match the selected type. + +### Fields returned by query + +Select all fields returned by a specific query, such as A, B, or C. Properties you add to this type of rule are applied to all fields returned by the selected query. + +### Fields with values + +Select all fields returned by your defined reducer condition, such as **Min**, **Max**, **Count**, **Total**. Properties you add to this type of rule are applied to all fields returned by the selected condition. + +## Examples + +The following examples demonstrate how you can use override rules to alter the display of fields in visualizations. + +### Example 1: Format temperature + +The following result set is a data frame that consists of two fields: time and temperature. | time | temperature | | :-----------------: | :---------: | @@ -35,7 +83,14 @@ Let’s assume that our result set is a data frame that consists of two fields: | 2020-01-02 03:05:00 | 47.0 | | 2020-01-02 03:06:00 | 48.0 | -Each field (column) of this structure can have field options applied that alter the way its values are displayed. This means that you can, for example, set the Unit to Temperature > Celsius, resulting in the following table: +You can apply field options to each field (column) of this structure to alter the way its values are displayed. For example, you can set the following override rule: + +- Rule: **Fields with type** +- Field: temperature +- Override property: **Standard options > Unit** + - Selection: **Temperature > Celsius** + +This results in the following table: | time | temperature | | :-----------------: | :---------: | @@ -43,7 +98,7 @@ Each field (column) of this structure can have field options applied that alter | 2020-01-02 03:05:00 | 47.0 °C | | 2020-01-02 03:06:00 | 48.0 °C | -In addition, the decimal place is not required, so we can remove it. You can change the Decimals from `auto` to zero (`0`), resulting in the following table: +In addition, the decimal place isn't required, so you can remove it by adding another override property that changes the **Standard options > Decimals** setting from **auto** to `0`. That results in the following table: | time | temperature | | :-----------------: | :---------: | @@ -51,9 +106,9 @@ In addition, the decimal place is not required, so we can remove it. You can cha | 2020-01-02 03:05:00 | 47 °C | | 2020-01-02 03:06:00 | 48 °C | -## Example 2: Format temperature and humidity +### Example 2: Format temperature and humidity -Let’s assume that our result set is a data frame that consists of four fields: time, high temp, low temp, and humidity. +The following result set is a data frame that consists of four fields: time, high temp, low temp, and humidity. | time | high temp | low temp | humidity | | ------------------- | --------- | -------- | -------- | @@ -61,7 +116,16 @@ Let’s assume that our result set is a data frame that consists of four fields: | 2020-01-02 03:05:00 | 47.0 | 34.0 | 68 | | 2020-01-02 03:06:00 | 48.0 | 31.0 | 68 | -Let's add the Celsius unit and get rid of the decimal place. This results in the following table: +Use the following override rule and properties to add the **Celsius** unit option and remove the decimal place: + +- Rule: **Fields with type** +- Field: temperature +- Override property: **Standard options > Unit** + - Selection: **Temperature > Celsius** +- Override property: **Standard options > Decimals** + -Change setting from **auto** to `0` + +This results in the following table: | time | high temp | low temp | humidity | | ------------------- | --------- | -------- | -------- | @@ -69,7 +133,7 @@ Let's add the Celsius unit and get rid of the decimal place. This results in the | 2020-01-02 03:05:00 | 47 °C | 34 °C | 68 °C | | 2020-01-02 03:06:00 | 48 °C | 31 °C | 68 °C | -The temperature fields look good, but the humidity must now be changed. We can fix this by applying a field option override to the humidity field and change the unit to Misc > percent (0-100). +The temperature fields are displaying correctly, but the humidity has incorrect units. You can fix this by applying a **Misc > Percent (0-100)** override to the humidity field. This results in the following table: | time | high temp | low temp | humidity | | ------------------- | --------- | -------- | -------- | @@ -79,47 +143,86 @@ The temperature fields look good, but the humidity must now be changed. We can f ## Add a field override -A field override rule can customize the visualization settings for a specific field or series. - -1. Edit the panel to which you want to add an override. -1. In the panel options side pane, click **Add field override** at the bottom of the pane. - -1. Select which fields an override rule will be applied to: - - **Fields with name:** Select a field from the list of all available fields. Properties you add to a rule with this selector are only applied to this single field. - - **Fields with name matching regex:** Specify fields to override with a regular expression. Properties you add to a rule with this selector are applied to all fields where the field name match the regex. This override doesn't rename the field; to do this, use the [Rename by regex transformation]({{< relref "../query-transform-data/transform-data/#rename-by-regex" >}}). - - **Fields with type:** Select fields by type, such as string, numeric, and so on. Properties you add to a rule with this selector are applied to all fields that match the selected type. - - **Fields returned by query:** Select all fields returned by a specific query, such as A, B, or C. Properties you add to a rule with this selector are applied to all fields returned by the selected query. +To add a field override, follow these steps: + +1. Navigate to the panel to which you want to add the data link. +1. Hover over any part of the panel to display the menu icon in the upper-right corner. +1. Click the menu icon and select **Edit** to open the panel editor. +1. At the bottom of the panel editor pane, click **Add field override**. +1. Select the fields to which the override will be applied: + - **Fields with name** + - **Fields with name matching regex** + - **Fields with type** + - **Fields returned by query** + - **Fields with values** 1. Click **Add override property**. 1. Select the field option that you want to apply. -1. Enter options by adding values in the fields. To return options to default values, delete the white text in the fields. -1. Continue to add overrides to this field by clicking **Add override property**, or you can click **Add override** and select a different field to add overrides to. -1. When finished, click **Save** to save all panel edits to the dashboard. +1. Continue to add overrides to this field by clicking **Add override property**. +1. Add as many overrides as you need. +1. When you're finished, click **Save** to save all panel edits to the dashboard. -## Delete a field override +## Edit a field override -Delete a field override when you no longer need it. When you delete an override, the appearance of value defaults to its original format. This change impacts dashboards and dashboard users that rely on an affected panel. +To edit a field override, follow these steps: -1. Edit the panel that contains the override you want to delete. -1. In panel options side pane, scroll down until you see the overrides. -1. Click the override you want to delete and then click the associated trash icon. +1. Navigate to the panel to which you want to add the data link. +1. Hover over any part of the panel to display the menu icon in the upper-right corner. +1. Click the menu icon and select **Edit** to open the panel editor. +1. In the panel editor pane, click the **Overrides** tab. +1. Locate the override you want to change. +1. Perform any of the following tasks: + - Edit settings on existing overrides or field selection parameters. + - Delete existing override properties by clicking the **X** next to the property. + - Delete an override entirely by clicking the trash icon at the top-right corner. -## View field overrides +The changes you make take effect immediately. -You can view field overrides in the panel display options. +{{% docs/reference %}} +[bar chart]: "/docs/grafana/ -> /docs/grafana//panels-visualizations/visualizations/bar-chart" +[bar chart]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/panels-visualizations/visualizations/bar-chart" -1. Edit the panel that contains the overrides you want to view. -1. In panel options side pane, scroll down until you see the overrides. +[bar gauge]: "/docs/grafana/ -> /docs/grafana//panels-visualizations/visualizations/bar-gauge" +[bar gauge]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/panels-visualizations/visualizations/bar-gauge" -> The override settings that appear on the **All** tab are the same as the settings that appear on the **Overrides** tab. +[candlestick]: "/docs/grafana/ -> /docs/grafana//panels-visualizations/visualizations/candlestick" +[candlestick]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/panels-visualizations/visualizations/candlestick" -## Edit a field override +[canvas]: "/docs/grafana/ -> /docs/grafana//panels-visualizations/visualizations/canvas" +[canvas]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/panels-visualizations/visualizations/canvas" -Edit a field override when you want to make changes to an override setting. The change you make takes effect immediately. +[gauge]: "/docs/grafana/ -> /docs/grafana//panels-visualizations/visualizations/gauge" +[gauge]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/panels-visualizations/visualizations/gauge" -1. Edit the panel that contains the overrides you want to edit. -1. In panel options side pane, scroll down until you see the overrides. -1. Locate the override that you want to change. -1. Perform any of the following: - - Edit settings on existing overrides or field selection parameters. - - Delete existing override properties by clicking the **X** next to the property. - - Add an override properties by clicking **Add override property**. +[geomap]: "/docs/grafana/ -> /docs/grafana//panels-visualizations/visualizations/geomap" +[geomap]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/panels-visualizations/visualizations/geomap" + +[heatmap]: "/docs/grafana/ -> /docs/grafana//panels-visualizations/visualizations/heatmap" +[heatmap]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/panels-visualizations/visualizations/heatmap" + +[histogram]: "/docs/grafana/ -> /docs/grafana//panels-visualizations/visualizations/histogram" +[histogram]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/panels-visualizations/visualizations/histogram" + +[pie chart]: "/docs/grafana/ -> /docs/grafana//panels-visualizations/visualizations/pie-chart" +[pie chart]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/panels-visualizations/visualizations/pie-chart" + +[stat]: "/docs/grafana/ -> /docs/grafana//panels-visualizations/visualizations/stat" +[stat]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/panels-visualizations/visualizations/stat" + +[state timeline]: "/docs/grafana/ -> /docs/grafana//panels-visualizations/visualizations/state-timeline" +[state timeline]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/panels-visualizations/visualizations/state-timeline" + +[status history]: "/docs/grafana/ -> /docs/grafana//panels-visualizations/visualizations/status-history" +[status history]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/panels-visualizations/visualizations/status-history" + +[table]: "/docs/grafana/ -> /docs/grafana//panels-visualizations/visualizations/table" +[table]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/panels-visualizations/visualizations/table" + +[time series]: "/docs/grafana/ -> /docs/grafana//panels-visualizations/visualizations/time-series" +[time series]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/panels-visualizations/visualizations/time-series" + +[trend]: "/docs/grafana/ -> /docs/grafana//panels-visualizations/visualizations/trend" +[trend]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/panels-visualizations/visualizations/trend" + +[Rename by regex transformation]: "/docs/grafana/ -> /docs/grafana//panels-visualizations/query-transform-data/transform-data#rename-by-regex" +[Rename by regex transformation]: "/docs/grafana-cloud -> /docs/grafana-cloud/visualizations/panels-visualizations/query-transform-data/transform-data#rename-by-regex" +{{% /docs/reference %}} From 240480ac9bfe90b3bcae2404f3b77c4814a656a2 Mon Sep 17 00:00:00 2001 From: ismail simsek Date: Fri, 23 Feb 2024 22:57:12 +0100 Subject: [PATCH 0150/1406] Chore: Add a key prop to warning component to prevent "should have a unique key prop" error (#83340) add a key prop to prevent "should have a unique key prop" warning/error on the browser console --- public/app/features/query/components/QueryEditorRow.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/public/app/features/query/components/QueryEditorRow.tsx b/public/app/features/query/components/QueryEditorRow.tsx index dbf12944e5..9f3489dedb 100644 --- a/public/app/features/query/components/QueryEditorRow.tsx +++ b/public/app/features/query/components/QueryEditorRow.tsx @@ -405,6 +405,7 @@ export class QueryEditorRow extends PureComponent Date: Fri, 23 Feb 2024 15:03:35 -0700 Subject: [PATCH 0151/1406] Dashboard-Scene: Show empty state after removing last panel (#83114) * Show empty state after removing last panel * betterer * Refactor isEmpty state update in DashboardScene.tsx * don't need viewPanelScene check * track isEmpty through a behavior * Fix test * Add test for empty state * minor fix * Refactor isEmpty check * Don't use const * clean up --- .../pages/DashboardScenePage.test.tsx | 7 +++++++ .../dashboard-scene/scene/DashboardScene.tsx | 15 +-------------- .../scene/DashboardSceneRenderer.tsx | 14 ++++++-------- .../dashboard-scene/scene/PanelMenuBehavior.tsx | 4 +--- .../transformSaveModelToScene.test.ts | 2 +- .../serialization/transformSaveModelToScene.ts | 17 +++++++++++++++++ 6 files changed, 33 insertions(+), 26 deletions(-) diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePage.test.tsx b/public/app/features/dashboard-scene/pages/DashboardScenePage.test.tsx index 72107998d7..3033bd6390 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePage.test.tsx +++ b/public/app/features/dashboard-scene/pages/DashboardScenePage.test.tsx @@ -194,6 +194,13 @@ describe('DashboardScenePage', () => { expect(screen.queryByTitle('Panel A')).not.toBeInTheDocument(); expect(await screen.findByTitle('Panel B')).toBeInTheDocument(); }); + + it('Shows empty state when dashboard is empty', async () => { + loadDashboardMock.mockResolvedValue({ dashboard: { panels: [] }, meta: {} }); + setup(); + + expect(await screen.findByText('Start your new dashboard by adding a visualization')).toBeInTheDocument(); + }); }); interface VizOptions { diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index 412448b1f5..a8b6471329 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -94,6 +94,7 @@ export interface DashboardSceneState extends SceneObjectState { editPanel?: PanelEditor; /** Scene object that handles the current drawer or modal */ overlay?: SceneObject; + isEmpty?: boolean; } export class DashboardScene extends SceneObjectBase { @@ -522,20 +523,6 @@ export class DashboardScene extends SceneObjectBase { locationService.partial({ editview: 'settings' }); }; - public isEmpty = (): boolean => { - const { body, viewPanelScene } = this.state; - - if (!!viewPanelScene) { - return !!viewPanelScene.state.body; - } - - if (body instanceof SceneFlexLayout || body instanceof SceneGridLayout) { - return body.state.children.length === 0; - } - - throw new Error('Invalid body type'); - }; - /** * Called by the SceneQueryRunner to privide contextural parameters (tracking) props for the request */ diff --git a/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx b/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx index cd58d311fd..17915583be 100644 --- a/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx @@ -14,7 +14,7 @@ import { DashboardScene } from './DashboardScene'; import { NavToolbarActions } from './NavToolbarActions'; export function DashboardSceneRenderer({ model }: SceneComponentProps) { - const { controls, overlay, editview, editPanel } = model.useState(); + const { controls, overlay, editview, editPanel, isEmpty } = model.useState(); const styles = useStyles2(getStyles); const location = useLocation(); const navIndex = useSelector((state) => state.navIndex); @@ -34,12 +34,9 @@ export function DashboardSceneRenderer({ model }: SceneComponentProps; const withPanels = ( - <> - {controls && } -
- -
- +
+ +
); return ( @@ -49,7 +46,8 @@ export function DashboardSceneRenderer({ model }: SceneComponentProps
- {model.isEmpty() ? emptyState : withPanels} + {controls && } + {isEmpty ? emptyState : withPanels}
)} diff --git a/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx b/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx index c3b105fa18..f5b823d5e9 100644 --- a/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx +++ b/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx @@ -417,9 +417,7 @@ export function removePanel(dashboard: DashboardScene, panel: VizPanel, ask: boo const layout = dashboard.state.body; if (layout instanceof SceneGridLayout || SceneFlexLayout) { - layout.setState({ - children: panels, - }); + layout.setState({ children: panels }); } } diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts index c43026dd29..3992895e62 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts @@ -140,7 +140,7 @@ describe('transformSaveModelToScene', () => { const scene = createDashboardSceneFromDashboardModel(oldModel); - expect(scene.state.$behaviors).toHaveLength(5); + expect(scene.state.$behaviors).toHaveLength(6); expect(scene.state.$behaviors![0]).toBeInstanceOf(behaviors.CursorSync); expect((scene.state.$behaviors![0] as behaviors.CursorSync).state.sync).toEqual(DashboardCursorSync.Crosshair); }); diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts index 611375cc10..47d8994371 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts @@ -28,6 +28,7 @@ import { UserActionEvent, GroupByVariable, AdHocFiltersVariable, + SceneFlexLayout, } from '@grafana/scenes'; import { DashboardModel, PanelModel } from 'app/features/dashboard/state'; import { trackDashboardLoaded } from 'app/features/dashboard/utils/tracking'; @@ -266,6 +267,7 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel) registerDashboardMacro, registerDashboardSceneTracking(oldModel), registerPanelInteractionsReporter, + trackIfIsEmpty, ], $data: layers.length > 0 @@ -535,6 +537,21 @@ function registerPanelInteractionsReporter(scene: DashboardScene) { }); } +export function trackIfIsEmpty(parent: DashboardScene) { + updateIsEmpty(parent); + + parent.state.body.subscribeToState(() => { + updateIsEmpty(parent); + }); +} + +function updateIsEmpty(parent: DashboardScene) { + const { body } = parent.state; + if (body instanceof SceneFlexLayout || body instanceof SceneGridLayout) { + parent.setState({ isEmpty: body.state.children.length === 0 }); + } +} + const convertSnapshotData = (snapshotData: DataFrameDTO[]): DataFrameJSON[] => { return snapshotData.map((data) => { return { From 6e6b9a62a20ec644dcb19b968395c61296551eef Mon Sep 17 00:00:00 2001 From: Leon Sorokin Date: Fri, 23 Feb 2024 16:18:24 -0600 Subject: [PATCH 0152/1406] VizTooltips: Use global portal (#81986) Co-authored-by: Adela Almasan --- .../uPlot/plugins/TooltipPlugin2.tsx | 229 +++++++++--------- .../src/options/builder/tooltip.tsx | 5 +- .../core/components/TimelineChart/timeline.ts | 30 +-- .../panel/candlestick/CandlestickPanel.tsx | 25 +- .../plugins/panel/heatmap/HeatmapPanel.tsx | 17 +- public/app/plugins/panel/heatmap/module.tsx | 5 +- .../state-timeline/StateTimelinePanel.tsx | 21 +- .../status-history/StatusHistoryPanel.tsx | 21 +- .../panel/timeseries/TimeSeriesPanel.tsx | 21 +- .../timeseries/plugins/AnnotationsPlugin2.tsx | 17 +- .../annotations2/AnnotationEditor2.tsx | 28 ++- .../annotations2/AnnotationMarker2.tsx | 159 +++++------- .../annotations2/AnnotationTooltip2.tsx | 38 +-- 13 files changed, 305 insertions(+), 311 deletions(-) diff --git a/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx b/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx index 3b755bc379..f2ce44ecdc 100644 --- a/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx +++ b/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx @@ -1,12 +1,12 @@ import { css, cx } from '@emotion/css'; -import React, { useLayoutEffect, useRef, useReducer, CSSProperties, useContext, useEffect } from 'react'; +import React, { useLayoutEffect, useRef, useReducer, CSSProperties } from 'react'; import { createPortal } from 'react-dom'; import uPlot from 'uplot'; import { GrafanaTheme2 } from '@grafana/data'; import { useStyles2 } from '../../../themes'; -import { LayoutItemContext } from '../../Layout/LayoutItemContext'; +import { getPortalContainer } from '../../Portal/Portal'; import { UPlotConfigBuilder } from '../config/UPlotConfigBuilder'; import { CloseButton } from './CloseButton'; @@ -29,6 +29,8 @@ interface TooltipPlugin2Props { config: UPlotConfigBuilder; hoverMode: TooltipHoverMode; + syncTooltip?: () => boolean; + // x only queryZoom?: (range: { from: number; to: number }) => void; // y-only, via shiftKey @@ -80,14 +82,16 @@ function mergeState(prevState: TooltipContainerState, nextState: Partial {}, -}; +function initState(): TooltipContainerState { + return { + style: { transform: '', pointerEvents: 'none' }, + isHovering: false, + isPinned: false, + contents: null, + plot: null, + dismiss: () => {}, + }; +} // min px width that triggers zoom const MIN_ZOOM_DIST = 5; @@ -105,13 +109,16 @@ export const TooltipPlugin2 = ({ queryZoom, maxWidth, maxHeight, + syncTooltip = () => false, }: TooltipPlugin2Props) => { const domRef = useRef(null); + const portalRoot = useRef(null); - const [{ plot, isHovering, isPinned, contents, style, dismiss }, setState] = useReducer(mergeState, INITIAL_STATE); + if (portalRoot.current == null) { + portalRoot.current = getPortalContainer(); + } - const { boostZIndex } = useContext(LayoutItemContext); - useEffect(() => (isPinned ? boostZIndex() : undefined), [isPinned]); + const [{ plot, isHovering, isPinned, contents, style, dismiss }, setState] = useReducer(mergeState, null, initState); const sizeRef = useRef(); @@ -150,20 +157,19 @@ export const TooltipPlugin2 = ({ let _isPinned = isPinned; let _style = style; + let plotVisible = false; + const updateHovering = () => { - _isHovering = closestSeriesIdx != null || (hoverMode === TooltipHoverMode.xAll && _someSeriesIdx); + if (viaSync) { + _isHovering = plotVisible && _someSeriesIdx && syncTooltip(); + } else { + _isHovering = closestSeriesIdx != null || (hoverMode === TooltipHoverMode.xAll && _someSeriesIdx); + } }; let offsetX = 0; let offsetY = 0; - let containRect = { - lft: 0, - top: 0, - rgt: screen.width, - btm: screen.height, - }; - let selectedRange: TimeRange2 | null = null; let seriesIdxs: Array = plot?.cursor.idxs!.slice()!; let closestSeriesIdx: number | null = null; @@ -231,7 +237,6 @@ export const TooltipPlugin2 = ({ setState(state); selectedRange = null; - viaSync = false; }; const dismiss = () => { @@ -293,58 +298,11 @@ export const TooltipPlugin2 = ({ } } }); - - const haltAncestorId = 'pageContent'; - const scrollbarWidth = 16; - - // if we're in a container that can clip the tooltip, we should try to stay within that rather than window edges - u.over.addEventListener( - 'mouseenter', - () => { - // clamp to viewport bounds - let htmlEl = document.documentElement; - let winWid = htmlEl.clientWidth - scrollbarWidth; - let winHgt = htmlEl.clientHeight - scrollbarWidth; - - let lft = 0, - top = 0, - rgt = winWid, - btm = winHgt; - - // find nearest scrollable container where overflow is not visible, (stop at #pageContent) - let par: HTMLElement | null = u.root; - - while (par != null && par.id !== haltAncestorId) { - let style = getComputedStyle(par); - let overflowX = style.getPropertyValue('overflow-x'); - let overflowY = style.getPropertyValue('overflow-y'); - - if (overflowX !== 'visible' || overflowY !== 'visible') { - let rect = par.getBoundingClientRect(); - lft = Math.max(rect.x, lft); - top = Math.max(rect.y, top); - rgt = Math.min(lft + rect.width, rgt); - btm = Math.min(top + rect.height, btm); - break; - } - - par = par.parentElement; - } - - containRect.lft = lft; - containRect.top = top; - containRect.rgt = rgt; - containRect.btm = btm; - }, - { capture: true } - ); }); config.addHook('setSelect', (u) => { - let e = u.cursor!.event; - - if (e != null && (clientZoom || queryZoom != null)) { - if (maybeZoomAction(e)) { + if (!viaSync && (clientZoom || queryZoom != null)) { + if (maybeZoomAction(u.cursor!.event)) { if (clientZoom && yDrag) { if (u.select.height >= MIN_ZOOM_DIST) { for (let key in u.scales!) { @@ -427,6 +385,8 @@ export const TooltipPlugin2 = ({ // TODO: we only need this for multi/all mode? config.addHook('setSeries', (u, seriesIdx) => { closestSeriesIdx = seriesIdx; + + viaSync = u.cursor.event == null; updateHovering(); scheduleRender(); }); @@ -436,78 +396,107 @@ export const TooltipPlugin2 = ({ seriesIdxs = _plot?.cursor!.idxs!.slice()!; _someSeriesIdx = seriesIdxs.some((v, i) => i > 0 && v != null); + viaSync = u.cursor.event == null; updateHovering(); scheduleRender(); }); + const scrollbarWidth = 16; + let winWid = 0; + let winHgt = 0; + + const updateWinSize = () => { + _isHovering && !_isPinned && dismiss(); + + winWid = window.innerWidth - scrollbarWidth; + winHgt = window.innerHeight - scrollbarWidth; + }; + + const updatePlotVisible = () => { + plotVisible = + _plot!.rect.bottom <= winHgt && _plot!.rect.top >= 0 && _plot!.rect.left >= 0 && _plot!.rect.right <= winWid; + }; + + updateWinSize(); + config.addHook('ready', updatePlotVisible); + // fires on mousemoves config.addHook('setCursor', (u) => { - let { left = -10, top = -10, event } = u.cursor; + viaSync = u.cursor.event == null; + + if (!_isHovering) { + return; + } + + let { left = -10, top = -10 } = u.cursor; if (left >= 0 || top >= 0) { - viaSync = event == null; + let clientX = u.rect.left + left; + let clientY = u.rect.top + top; let transform = ''; - // this means it's a synthetic event from uPlot's sync - if (viaSync) { - // TODO: smarter positioning here to avoid viewport clipping? - transform = `translateX(${left}px) translateY(${u.rect.height / 2}px) translateY(-50%)`; - } else { - let { width, height } = sizeRef.current!; - - width += TOOLTIP_OFFSET; - height += TOOLTIP_OFFSET; + let { width, height } = sizeRef.current!; - let clientX = u.rect.left + left; - let clientY = u.rect.top + top; + width += TOOLTIP_OFFSET; + height += TOOLTIP_OFFSET; - if (offsetY !== 0) { - if (clientY + height < containRect.btm || clientY - height < 0) { - offsetY = 0; - } else if (offsetY !== -height) { - offsetY = -height; - } - } else { - if (clientY + height > containRect.btm && clientY - height >= 0) { - offsetY = -height; - } + if (offsetY !== 0) { + if (clientY + height < winHgt || clientY - height < 0) { + offsetY = 0; + } else if (offsetY !== -height) { + offsetY = -height; } + } else { + if (clientY + height > winHgt && clientY - height >= 0) { + offsetY = -height; + } + } - if (offsetX !== 0) { - if (clientX + width < containRect.rgt || clientX - width < 0) { - offsetX = 0; - } else if (offsetX !== -width) { - offsetX = -width; - } - } else { - if (clientX + width > containRect.rgt && clientX - width >= 0) { - offsetX = -width; - } + if (offsetX !== 0) { + if (clientX + width < winWid || clientX - width < 0) { + offsetX = 0; + } else if (offsetX !== -width) { + offsetX = -width; } + } else { + if (clientX + width > winWid && clientX - width >= 0) { + offsetX = -width; + } + } - const shiftX = left + (offsetX === 0 ? TOOLTIP_OFFSET : -TOOLTIP_OFFSET); - const shiftY = top + (offsetY === 0 ? TOOLTIP_OFFSET : -TOOLTIP_OFFSET); + const shiftX = clientX + (offsetX === 0 ? TOOLTIP_OFFSET : -TOOLTIP_OFFSET); + const shiftY = clientY + (offsetY === 0 ? TOOLTIP_OFFSET : -TOOLTIP_OFFSET); - const reflectX = offsetX === 0 ? '' : 'translateX(-100%)'; - const reflectY = offsetY === 0 ? '' : 'translateY(-100%)'; + const reflectX = offsetX === 0 ? '' : 'translateX(-100%)'; + const reflectY = offsetY === 0 ? '' : 'translateY(-100%)'; - // TODO: to a transition only when switching sides - // transition: transform 100ms; + // TODO: to a transition only when switching sides + // transition: transform 100ms; - transform = `translateX(${shiftX}px) ${reflectX} translateY(${shiftY}px) ${reflectY}`; - } + transform = `translateX(${shiftX}px) ${reflectX} translateY(${shiftY}px) ${reflectY}`; - if (_isHovering) { - if (domRef.current != null) { - domRef.current.style.transform = transform; - } else { - _style.transform = transform; - scheduleRender(); - } + if (domRef.current != null) { + domRef.current.style.transform = transform; + } else { + _style.transform = transform; + scheduleRender(); } } }); + + const onscroll = () => { + updatePlotVisible(); + _isHovering && !_isPinned && dismiss(); + }; + + window.addEventListener('resize', updateWinSize); + window.addEventListener('scroll', onscroll, true); + + return () => { + window.removeEventListener('resize', updateWinSize); + window.removeEventListener('scroll', onscroll, true); + }; }, [config]); useLayoutEffect(() => { @@ -524,7 +513,7 @@ export const TooltipPlugin2 = ({ {isPinned && } {contents}
, - plot.over + portalRoot.current ); } diff --git a/packages/grafana-ui/src/options/builder/tooltip.tsx b/packages/grafana-ui/src/options/builder/tooltip.tsx index a5091b48b5..0ff12becc3 100644 --- a/packages/grafana-ui/src/options/builder/tooltip.tsx +++ b/packages/grafana-ui/src/options/builder/tooltip.tsx @@ -66,15 +66,16 @@ export function addTooltipOptions( settings: { integer: true, }, - showIf: (options: T) => options.tooltip?.mode !== TooltipDisplayMode.None, + showIf: (options: T) => false, // options.tooltip?.mode !== TooltipDisplayMode.None, }) .addNumberInput({ path: 'tooltip.maxHeight', name: 'Max height', category, + defaultValue: 600, settings: { integer: true, }, - showIf: (options: T) => options.tooltip?.mode !== TooltipDisplayMode.None, + showIf: (options: T) => false, //options.tooltip?.mode !== TooltipDisplayMode.None, }); } diff --git a/public/app/core/components/TimelineChart/timeline.ts b/public/app/core/components/TimelineChart/timeline.ts index e5fcb3555c..5afd52afbd 100644 --- a/public/app/core/components/TimelineChart/timeline.ts +++ b/public/app/core/components/TimelineChart/timeline.ts @@ -5,7 +5,7 @@ import { alpha } from '@grafana/data/src/themes/colorManipulator'; import { TimelineValueAlignment, VisibilityMode } from '@grafana/schema'; import { FIXED_UNIT } from '@grafana/ui'; import { distribute, SPACE_BETWEEN } from 'app/plugins/panel/barchart/distribute'; -import { pointWithin, Quadtree, Rect } from 'app/plugins/panel/barchart/quadtree'; +import { Quadtree, Rect } from 'app/plugins/panel/barchart/quadtree'; import { FieldConfig as StateTimeLineFieldConfig } from 'app/plugins/panel/state-timeline/panelcfg.gen'; import { FieldConfig as StatusHistoryFieldConfig } from 'app/plugins/panel/status-history/panelcfg.gen'; @@ -384,7 +384,7 @@ export function getConfig(opts: TimelineCoreOptions) { }); }; - function setHovered(cx: number, cy: number, cys: number[]) { + function setHovered(cx: number, cy: number, viaSync = false) { hovered.fill(null); hoveredAtCursor = null; @@ -392,19 +392,21 @@ export function getConfig(opts: TimelineCoreOptions) { return; } - for (let i = 0; i < cys.length; i++) { - let cy2 = cys[i]; - - qt.get(cx, cy2, 1, 1, (o) => { - if (pointWithin(cx, cy2, o.x, o.y, o.x + o.w, o.y + o.h)) { + // first gets all items in all quads intersected by a 1px wide by 10k high rect at the x cursor position and 0 y position. + // (we use 10k instead of plot area height for simplicity and not having to pass around the uPlot instance) + qt.get(cx, 0, uPlot.pxRatio, 1e4, (o) => { + // filter only rects that intersect along x dir + if (cx >= o.x && cx <= o.x + o.w) { + // if also intersect along y dir, set both "direct hovered" and "one-of hovered" + if (cy >= o.y && cy <= o.y + o.h) { + hovered[o.sidx] = hoveredAtCursor = o; + } + // else only set "one-of hovered" (no "direct hovered") in multi mode or when synced + else if (hoverMulti || viaSync) { hovered[o.sidx] = o; - - if (Math.abs(cy - cy2) <= o.h / 2) { - hoveredAtCursor = o; - } } - }); - } + } + }); } const cursor: uPlot.Cursor = { @@ -426,7 +428,7 @@ export function getConfig(opts: TimelineCoreOptions) { let prevHovered = hoveredAtCursor; - setHovered(cx, cy, hoverMulti ? yMids : [cy]); + setHovered(cx, cy, u.cursor.event == null); if (hoveredAtCursor != null) { if (hoveredAtCursor !== prevHovered) { diff --git a/public/app/plugins/panel/candlestick/CandlestickPanel.tsx b/public/app/plugins/panel/candlestick/CandlestickPanel.tsx index 2e1d6ab208..fa71996e3b 100644 --- a/public/app/plugins/panel/candlestick/CandlestickPanel.tsx +++ b/public/app/plugins/panel/candlestick/CandlestickPanel.tsx @@ -1,12 +1,12 @@ // this file is pretty much a copy-paste of TimeSeriesPanel.tsx :( // with some extra renderers passed to the component -import React, { useMemo, useState } from 'react'; +import React, { useMemo, useState, useCallback } from 'react'; import uPlot from 'uplot'; -import { DashboardCursorSync, Field, getDisplayProcessor, getLinksSupplier, PanelProps } from '@grafana/data'; +import { Field, getDisplayProcessor, getLinksSupplier, PanelProps } from '@grafana/data'; import { PanelDataErrorView } from '@grafana/runtime'; -import { TooltipDisplayMode } from '@grafana/schema'; +import { DashboardCursorSync, TooltipDisplayMode } from '@grafana/schema'; import { TooltipPlugin, TooltipPlugin2, UPlotConfigBuilder, usePanelContext, useTheme2, ZoomPlugin } from '@grafana/ui'; import { AxisProps } from '@grafana/ui/src/components/uPlot/config/UPlotAxisBuilder'; import { ScaleProps } from '@grafana/ui/src/components/uPlot/config/UPlotScaleBuilder'; @@ -46,6 +46,13 @@ export const CandlestickPanel = ({ const theme = useTheme2(); + // TODO: we should just re-init when this changes, and have this be a static setting + const syncTooltip = useCallback( + () => sync?.() === DashboardCursorSync.Tooltip, + // eslint-disable-next-line react-hooks/exhaustive-deps + [] + ); + const info = useMemo(() => { return prepareCandlestickFields(data.series, options, theme, timeRange); }, [data.series, options, theme, timeRange]); @@ -233,9 +240,8 @@ export const CandlestickPanel = ({ } } - const enableAnnotationCreation = Boolean(canAddAnnotations && canAddAnnotations()); - const showNewVizTooltips = - config.featureToggles.newVizTooltips && (sync == null || sync() !== DashboardCursorSync.Tooltip); + const enableAnnotationCreation = Boolean(canAddAnnotations?.()); + const showNewVizTooltips = Boolean(config.featureToggles.newVizTooltips); return ( { - if (viaSync) { - return null; - } - if (enableAnnotationCreation && timeRange2 != null) { setNewAnnotationRange(timeRange2); dismiss(); @@ -296,7 +299,7 @@ export const CandlestickPanel = ({ seriesFrame={alignedDataFrame} dataIdxs={dataIdxs} seriesIdx={seriesIdx} - mode={options.tooltip.mode} + mode={viaSync ? TooltipDisplayMode.Multi : options.tooltip.mode} sortOrder={options.tooltip.sort} isPinned={isPinned} annotate={enableAnnotationCreation ? annotate : undefined} diff --git a/public/app/plugins/panel/heatmap/HeatmapPanel.tsx b/public/app/plugins/panel/heatmap/HeatmapPanel.tsx index a9f02d7f6f..018ddca15c 100644 --- a/public/app/plugins/panel/heatmap/HeatmapPanel.tsx +++ b/public/app/plugins/panel/heatmap/HeatmapPanel.tsx @@ -60,6 +60,13 @@ export const HeatmapPanel = ({ const styles = useStyles2(getStyles); const { sync, canAddAnnotations } = usePanelContext(); + // TODO: we should just re-init when this changes, and have this be a static setting + const syncTooltip = useCallback( + () => sync?.() === DashboardCursorSync.Tooltip, + // eslint-disable-next-line react-hooks/exhaustive-deps + [] + ); + // temp range set for adding new annotation set by TooltipPlugin2, consumed by AnnotationPlugin2 const [newAnnotationRange, setNewAnnotationRange] = useState(null); @@ -159,8 +166,7 @@ export const HeatmapPanel = ({ // ugh const dataRef = useRef(info); dataRef.current = info; - const showNewVizTooltips = - config.featureToggles.newVizTooltips && (sync == null || sync() !== DashboardCursorSync.Tooltip); + const showNewVizTooltips = Boolean(config.featureToggles.newVizTooltips); const builder = useMemo(() => { const scaleConfig: ScaleDistributionConfig = dataRef.current?.heatmap?.fields[1].config?.custom?.scaleDistribution; @@ -243,11 +249,8 @@ export const HeatmapPanel = ({ config={builder} hoverMode={TooltipHoverMode.xyOne} queryZoom={onChangeTimeRange} + syncTooltip={syncTooltip} render={(u, dataIdxs, seriesIdx, isPinned, dismiss, timeRange2, viaSync) => { - if (viaSync) { - return null; - } - if (enableAnnotationCreation && timeRange2 != null) { setNewAnnotationRange(timeRange2); dismiss(); @@ -263,7 +266,7 @@ export const HeatmapPanel = ({ return ( (HeatmapPanel) settings: { integer: true, }, - showIf: (options) => config.featureToggles.newVizTooltips && options.tooltip?.mode !== TooltipDisplayMode.None, + showIf: (options) => false, // config.featureToggles.newVizTooltips && options.tooltip?.mode !== TooltipDisplayMode.None, }); builder.addNumberInput({ path: 'tooltip.maxHeight', name: 'Max height', category, + defaultValue: 600, settings: { integer: true, }, - showIf: (options) => config.featureToggles.newVizTooltips && options.tooltip?.mode !== TooltipDisplayMode.None, + showIf: (options) => false, // config.featureToggles.newVizTooltips && options.tooltip?.mode !== TooltipDisplayMode.None, }); category = ['Legend']; diff --git a/public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx b/public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx index cf3539fd4d..0cdc5c0ec9 100644 --- a/public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx +++ b/public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx @@ -52,6 +52,13 @@ export const StateTimelinePanel = ({ }: TimelinePanelProps) => { const theme = useTheme2(); + // TODO: we should just re-init when this changes, and have this be a static setting + const syncTooltip = useCallback( + () => sync?.() === DashboardCursorSync.Tooltip, + // eslint-disable-next-line react-hooks/exhaustive-deps + [] + ); + const oldConfig = useRef(undefined); const isToolTipOpen = useRef(false); @@ -163,8 +170,7 @@ export const StateTimelinePanel = ({ } } const enableAnnotationCreation = Boolean(canAddAnnotations && canAddAnnotations()); - const showNewVizTooltips = - config.featureToggles.newVizTooltips && (sync == null || sync() !== DashboardCursorSync.Tooltip); + const showNewVizTooltips = Boolean(config.featureToggles.newVizTooltips); return ( { - if (viaSync) { - return null; - } - if (enableAnnotationCreation && timeRange2 != null) { setNewAnnotationRange(timeRange2); dismiss(); @@ -228,7 +233,7 @@ export const StateTimelinePanel = ({ seriesFrame={alignedFrame} dataIdxs={dataIdxs} seriesIdx={seriesIdx} - mode={options.tooltip.mode} + mode={viaSync ? TooltipDisplayMode.Multi : options.tooltip.mode} sortOrder={options.tooltip.sort} isPinned={isPinned} timeRange={timeRange} diff --git a/public/app/plugins/panel/status-history/StatusHistoryPanel.tsx b/public/app/plugins/panel/status-history/StatusHistoryPanel.tsx index 241e0f6889..29192589c1 100644 --- a/public/app/plugins/panel/status-history/StatusHistoryPanel.tsx +++ b/public/app/plugins/panel/status-history/StatusHistoryPanel.tsx @@ -49,6 +49,13 @@ export const StatusHistoryPanel = ({ }: TimelinePanelProps) => { const theme = useTheme2(); + // TODO: we should just re-init when this changes, and have this be a static setting + const syncTooltip = useCallback( + () => sync?.() === DashboardCursorSync.Tooltip, + // eslint-disable-next-line react-hooks/exhaustive-deps + [] + ); + const oldConfig = useRef(undefined); const isToolTipOpen = useRef(false); @@ -192,8 +199,7 @@ export const StatusHistoryPanel = ({ ); } - const showNewVizTooltips = - config.featureToggles.newVizTooltips && (sync == null || sync() !== DashboardCursorSync.Tooltip); + const showNewVizTooltips = Boolean(config.featureToggles.newVizTooltips); return ( { - if (viaSync) { - return null; - } - if (enableAnnotationCreation && timeRange2 != null) { setNewAnnotationRange(timeRange2); dismiss(); @@ -256,7 +261,7 @@ export const StatusHistoryPanel = ({ seriesFrame={alignedFrame} dataIdxs={dataIdxs} seriesIdx={seriesIdx} - mode={options.tooltip.mode} + mode={viaSync ? TooltipDisplayMode.Multi : options.tooltip.mode} sortOrder={options.tooltip.sort} isPinned={isPinned} timeRange={timeRange} diff --git a/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx b/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx index 630701a436..50ee36677b 100644 --- a/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx +++ b/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, useState } from 'react'; +import React, { useMemo, useState, useCallback } from 'react'; import { PanelProps, DataFrameType, DashboardCursorSync } from '@grafana/data'; import { PanelDataErrorView } from '@grafana/runtime'; @@ -51,11 +51,17 @@ export const TimeSeriesPanel = ({ }, [frames, id]); const enableAnnotationCreation = Boolean(canAddAnnotations && canAddAnnotations()); - const showNewVizTooltips = - config.featureToggles.newVizTooltips && (sync == null || sync() !== DashboardCursorSync.Tooltip); + const showNewVizTooltips = Boolean(config.featureToggles.newVizTooltips); // temp range set for adding new annotation set by TooltipPlugin2, consumed by AnnotationPlugin2 const [newAnnotationRange, setNewAnnotationRange] = useState(null); + // TODO: we should just re-init when this changes, and have this be a static setting + const syncTooltip = useCallback( + () => sync?.() === DashboardCursorSync.Tooltip, + // eslint-disable-next-line react-hooks/exhaustive-deps + [] + ); + if (!frames || suggestions) { return ( - {!showNewVizTooltips && } + {options.tooltip.mode === TooltipDisplayMode.None || ( <> {showNewVizTooltips ? ( @@ -113,11 +119,8 @@ export const TimeSeriesPanel = ({ } queryZoom={onChangeTimeRange} clientZoom={true} + syncTooltip={syncTooltip} render={(u, dataIdxs, seriesIdx, isPinned = false, dismiss, timeRange2, viaSync) => { - if (viaSync) { - return null; - } - if (enableAnnotationCreation && timeRange2 != null) { setNewAnnotationRange(timeRange2); dismiss(); @@ -138,7 +141,7 @@ export const TimeSeriesPanel = ({ seriesFrame={alignedDataFrame} dataIdxs={dataIdxs} seriesIdx={seriesIdx} - mode={options.tooltip.mode} + mode={viaSync ? TooltipDisplayMode.Multi : options.tooltip.mode} sortOrder={options.tooltip.sort} isPinned={isPinned} annotate={enableAnnotationCreation ? annotate : undefined} diff --git a/public/app/plugins/panel/timeseries/plugins/AnnotationsPlugin2.tsx b/public/app/plugins/panel/timeseries/plugins/AnnotationsPlugin2.tsx index c1e616aeb2..8c0313d1ca 100644 --- a/public/app/plugins/panel/timeseries/plugins/AnnotationsPlugin2.tsx +++ b/public/app/plugins/panel/timeseries/plugins/AnnotationsPlugin2.tsx @@ -4,9 +4,9 @@ import { createPortal } from 'react-dom'; import tinycolor from 'tinycolor2'; import uPlot from 'uplot'; -import { arrayToDataFrame, colorManipulator, DataFrame, DataTopic, GrafanaTheme2 } from '@grafana/data'; +import { arrayToDataFrame, colorManipulator, DataFrame, DataTopic } from '@grafana/data'; import { TimeZone } from '@grafana/schema'; -import { DEFAULT_ANNOTATION_COLOR, UPlotConfigBuilder, useStyles2, useTheme2 } from '@grafana/ui'; +import { DEFAULT_ANNOTATION_COLOR, getPortalContainer, UPlotConfigBuilder, useStyles2, useTheme2 } from '@grafana/ui'; import { AnnotationMarker2 } from './annotations2/AnnotationMarker2'; @@ -65,6 +65,8 @@ export const AnnotationsPlugin2 = ({ }: AnnotationsPluginProps) => { const [plot, setPlot] = useState(); + const [portalRoot] = useState(() => getPortalContainer()); + const styles = useStyles2(getStyles); const getColorByName = useTheme2().visualization.getColorByName; @@ -221,9 +223,10 @@ export const AnnotationsPlugin2 = ({ annoVals={vals} className={className} style={style} - timezone={timeZone} + timeZone={timeZone} key={`${frameIdx}:${i}`} exitWipEdit={isWip ? exitWipEdit : null} + portalRoot={portalRoot} /> ); } @@ -238,14 +241,14 @@ export const AnnotationsPlugin2 = ({ return null; }; -const getStyles = (theme: GrafanaTheme2) => ({ +const getStyles = () => ({ annoMarker: css({ position: 'absolute', width: 0, height: 0, - borderLeft: '6px solid transparent', - borderRight: '6px solid transparent', - borderBottomWidth: '6px', + borderLeft: '5px solid transparent', + borderRight: '5px solid transparent', + borderBottomWidth: '5px', borderBottomStyle: 'solid', transform: 'translateX(-50%)', cursor: 'pointer', diff --git a/public/app/plugins/panel/timeseries/plugins/annotations2/AnnotationEditor2.tsx b/public/app/plugins/panel/timeseries/plugins/annotations2/AnnotationEditor2.tsx index 6f56a9a1a1..1e01344b46 100644 --- a/public/app/plugins/panel/timeseries/plugins/annotations2/AnnotationEditor2.tsx +++ b/public/app/plugins/panel/timeseries/plugins/annotations2/AnnotationEditor2.tsx @@ -1,8 +1,8 @@ import { css } from '@emotion/css'; -import React, { useContext, useEffect } from 'react'; -import { useAsyncFn } from 'react-use'; +import React, { useContext, useEffect, useRef } from 'react'; +import { useAsyncFn, useClickAway } from 'react-use'; -import { AnnotationEventUIModel, GrafanaTheme2 } from '@grafana/data'; +import { AnnotationEventUIModel, GrafanaTheme2, dateTimeFormat, systemDateFormats } from '@grafana/data'; import { Button, Field, @@ -20,7 +20,7 @@ import { getAnnotationTags } from 'app/features/annotations/api'; interface Props { annoVals: Record; annoIdx: number; - timeFormatter: (v: number) => string; + timeZone: string; dismiss: () => void; } @@ -29,25 +29,35 @@ interface AnnotationEditFormDTO { tags: string[]; } -export const AnnotationEditor2 = ({ annoVals, annoIdx, dismiss, timeFormatter, ...otherProps }: Props) => { +export const AnnotationEditor2 = ({ annoVals, annoIdx, dismiss, timeZone, ...otherProps }: Props) => { const styles = useStyles2(getStyles); - const panelContext = usePanelContext(); + const { onAnnotationCreate, onAnnotationUpdate } = usePanelContext(); + + const clickAwayRef = useRef(null); + + useClickAway(clickAwayRef, dismiss); const layoutCtx = useContext(LayoutItemContext); useEffect(() => layoutCtx.boostZIndex(), [layoutCtx]); const [createAnnotationState, createAnnotation] = useAsyncFn(async (event: AnnotationEventUIModel) => { - const result = await panelContext.onAnnotationCreate!(event); + const result = await onAnnotationCreate!(event); dismiss(); return result; }); const [updateAnnotationState, updateAnnotation] = useAsyncFn(async (event: AnnotationEventUIModel) => { - const result = await panelContext.onAnnotationUpdate!(event); + const result = await onAnnotationUpdate!(event); dismiss(); return result; }); + const timeFormatter = (value: number) => + dateTimeFormat(value, { + format: systemDateFormats.fullDate, + timeZone, + }); + const isUpdatingAnnotation = annoVals.id?.[annoIdx] != null; const isRegionAnnotation = annoVals.isRegion?.[annoIdx]; const operation = isUpdatingAnnotation ? updateAnnotation : createAnnotation; @@ -68,7 +78,7 @@ export const AnnotationEditor2 = ({ annoVals, annoIdx, dismiss, timeFormatter, . // Annotation editor return ( -
+
{isUpdatingAnnotation ? 'Edit annotation' : 'Add annotation'}
diff --git a/public/app/plugins/panel/timeseries/plugins/annotations2/AnnotationMarker2.tsx b/public/app/plugins/panel/timeseries/plugins/annotations2/AnnotationMarker2.tsx index e0af375355..db27a162b9 100644 --- a/public/app/plugins/panel/timeseries/plugins/annotations2/AnnotationMarker2.tsx +++ b/public/app/plugins/panel/timeseries/plugins/annotations2/AnnotationMarker2.tsx @@ -1,10 +1,12 @@ import { css } from '@emotion/css'; -import React, { useCallback, useLayoutEffect, useRef, useState } from 'react'; -import useClickAway from 'react-use/lib/useClickAway'; +import { flip, shift, autoUpdate } from '@floating-ui/dom'; +import { useFloating } from '@floating-ui/react'; +import React, { useState } from 'react'; +import { createPortal } from 'react-dom'; -import { dateTimeFormat, GrafanaTheme2, systemDateFormats } from '@grafana/data'; +import { GrafanaTheme2 } from '@grafana/data'; import { TimeZone } from '@grafana/schema'; -import { usePanelContext, useStyles2 } from '@grafana/ui'; +import { useStyles2 } from '@grafana/ui'; import { AnnotationEditor2 } from './AnnotationEditor2'; import { AnnotationTooltip2 } from './AnnotationTooltip2'; @@ -14,131 +16,94 @@ interface AnnoBoxProps { annoIdx: number; style: React.CSSProperties | null; className: string; - timezone: TimeZone; + timeZone: TimeZone; exitWipEdit?: null | (() => void); + portalRoot: HTMLElement; } const STATE_DEFAULT = 0; const STATE_EDITING = 1; const STATE_HOVERED = 2; -export const AnnotationMarker2 = ({ annoVals, annoIdx, className, style, exitWipEdit, timezone }: AnnoBoxProps) => { - const { canEditAnnotations, canDeleteAnnotations, ...panelCtx } = usePanelContext(); - +export const AnnotationMarker2 = ({ + annoVals, + annoIdx, + className, + style, + exitWipEdit, + timeZone, + portalRoot, +}: AnnoBoxProps) => { const styles = useStyles2(getStyles); - const [state, setState] = useState(STATE_DEFAULT); - - const clickAwayRef = useRef(null); - - useClickAway(clickAwayRef, () => { - if (state === STATE_EDITING) { - setIsEditingWrap(false); - } + const [state, setState] = useState(exitWipEdit != null ? STATE_EDITING : STATE_DEFAULT); + const { refs, floatingStyles } = useFloating({ + open: true, + placement: 'bottom', + middleware: [ + flip({ + fallbackAxisSideDirection: 'end', + // see https://floating-ui.com/docs/flip#combining-with-shift + crossAxis: false, + boundary: document.body, + }), + shift(), + ], + whileElementsMounted: autoUpdate, + strategy: 'fixed', }); - const domRef = React.createRef(); - - // similar to TooltipPlugin2, when editing annotation (pinned), it should boost z-index - const setIsEditingWrap = useCallback( - (isEditing: boolean) => { - setState(isEditing ? STATE_EDITING : STATE_DEFAULT); - if (!isEditing && exitWipEdit != null) { - exitWipEdit(); - } - }, - [exitWipEdit] - ); - - const onAnnotationEdit = useCallback(() => { - setIsEditingWrap(true); - }, [setIsEditingWrap]); - - const onAnnotationDelete = useCallback(() => { - if (panelCtx.onAnnotationDelete) { - panelCtx.onAnnotationDelete(annoVals.id?.[annoIdx]); - } - }, [annoIdx, annoVals.id, panelCtx]); - - const timeFormatter = useCallback( - (value: number) => { - return dateTimeFormat(value, { - format: systemDateFormats.fullDate, - timeZone: timezone, - }); - }, - [timezone] - ); - - useLayoutEffect( - () => { - if (exitWipEdit != null) { - setIsEditingWrap(true); - } - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [] - ); - - const renderAnnotationTooltip = useCallback(() => { - let dashboardUID = annoVals.dashboardUID?.[annoIdx]; - - return ( + const contents = + state === STATE_HOVERED ? ( setState(STATE_EDITING)} /> - ); - }, [ - timeFormatter, - onAnnotationEdit, - onAnnotationDelete, - canEditAnnotations, - annoVals, - annoIdx, - canDeleteAnnotations, - ]); - - const renderAnnotationEditor = useCallback(() => { - return ( + ) : state === STATE_EDITING ? ( setIsEditingWrap(false)} - timeFormatter={timeFormatter} annoIdx={annoIdx} annoVals={annoVals} + timeZone={timeZone} + dismiss={() => { + exitWipEdit?.(); + setState(STATE_DEFAULT); + }} /> - ); - }, [annoIdx, annoVals, timeFormatter, setIsEditingWrap]); + ) : null; return (
state !== STATE_EDITING && setState(STATE_HOVERED)} onMouseLeave={() => state !== STATE_EDITING && setState(STATE_DEFAULT)} > -
- {state === STATE_HOVERED && renderAnnotationTooltip()} - {state === STATE_EDITING && renderAnnotationEditor()} -
+ {contents && + createPortal( +
+ {contents} +
, + portalRoot + )}
); }; const getStyles = (theme: GrafanaTheme2) => ({ - annoInfo: css({ - background: theme.colors.background.secondary, - minWidth: '300px', - // maxWidth: '400px', + // NOTE: shares much with TooltipPlugin2 + annoBox: css({ + top: 0, + left: 0, + zIndex: theme.zIndex.tooltip, + borderRadius: theme.shape.radius.default, position: 'absolute', - top: '5px', - left: '50%', - transform: 'translateX(-50%)', + background: theme.colors.background.primary, + border: `1px solid ${theme.colors.border.weak}`, + boxShadow: theme.shadows.z2, + userSelect: 'text', + minWidth: '300px', }), }); diff --git a/public/app/plugins/panel/timeseries/plugins/annotations2/AnnotationTooltip2.tsx b/public/app/plugins/panel/timeseries/plugins/annotations2/AnnotationTooltip2.tsx index 1e53aafb99..0b72deb99f 100644 --- a/public/app/plugins/panel/timeseries/plugins/annotations2/AnnotationTooltip2.tsx +++ b/public/app/plugins/panel/timeseries/plugins/annotations2/AnnotationTooltip2.tsx @@ -1,34 +1,39 @@ import { css } from '@emotion/css'; import React, { useContext, useEffect } from 'react'; -import { GrafanaTheme2, textUtil } from '@grafana/data'; -import { HorizontalGroup, IconButton, LayoutItemContext, Tag, useStyles2 } from '@grafana/ui'; +import { GrafanaTheme2, dateTimeFormat, systemDateFormats, textUtil } from '@grafana/data'; +import { HorizontalGroup, IconButton, LayoutItemContext, Tag, usePanelContext, useStyles2 } from '@grafana/ui'; import alertDef from 'app/features/alerting/state/alertDef'; interface Props { annoVals: Record; annoIdx: number; - timeFormatter: (v: number) => string; - canEdit: boolean; - canDelete: boolean; + timeZone: string; onEdit: () => void; - onDelete: () => void; } -export const AnnotationTooltip2 = ({ - annoVals, - annoIdx, - timeFormatter, - canEdit, - canDelete, - onEdit, - onDelete, -}: Props) => { +const retFalse = () => false; + +export const AnnotationTooltip2 = ({ annoVals, annoIdx, timeZone, onEdit }: Props) => { + const annoId = annoVals.id?.[annoIdx]; + const styles = useStyles2(getStyles); + const { canEditAnnotations = retFalse, canDeleteAnnotations = retFalse, onAnnotationDelete } = usePanelContext(); + + const dashboardUID = annoVals.dashboardUID?.[annoIdx]; + const canEdit = canEditAnnotations(dashboardUID); + const canDelete = canDeleteAnnotations(dashboardUID) && onAnnotationDelete != null; + const layoutCtx = useContext(LayoutItemContext); useEffect(() => layoutCtx.boostZIndex(), [layoutCtx]); + const timeFormatter = (value: number) => + dateTimeFormat(value, { + format: systemDateFormats.fullDate, + timeZone, + }); + let time = timeFormatter(annoVals.time[annoIdx]); let text = annoVals.text[annoIdx]; @@ -75,9 +80,8 @@ export const AnnotationTooltip2 = ({ onAnnotationDelete(annoId)} tooltip="Delete" - disabled={!annoVals.id?.[annoIdx]} /> )}
From 110028706a160af8c66672d06554f616d660d82c Mon Sep 17 00:00:00 2001 From: Todd Treece <360020+toddtreece@users.noreply.github.com> Date: Fri, 23 Feb 2024 19:36:23 -0500 Subject: [PATCH 0153/1406] K8s: Update codegen to support new packages (#83347) --------- Co-authored-by: Charandas Batra --- go.work.sum | 178 +++++++++++++++++- hack/openapi-codegen.sh | 7 +- hack/update-codegen.sh | 96 ++++++---- .../v0alpha1/zz_generated.openapi.go | 4 +- .../v0alpha1/zz_generated.openapi.go | 8 +- .../v0alpha1/zz_generated.openapi.go | 4 +- .../example/v0alpha1/zz_generated.openapi.go | 4 +- .../v0alpha1/zz_generated.openapi.go | 4 +- .../query/v0alpha1/zz_generated.openapi.go | 4 +- 9 files changed, 250 insertions(+), 59 deletions(-) diff --git a/go.work.sum b/go.work.sum index 35c0c2b8ee..5ab4d7f0dc 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1,18 +1,188 @@ +buf.build/gen/go/grpc-ecosystem/grpc-gateway/bufbuild/connect-go v1.4.1-20221127060915-a1ecdc58eccd.1/go.mod h1:YDq2B5X5BChU0lxAG5MxHpDb8mx1fv9OGtF2mwOe7hY= +cloud.google.com/go/aiplatform v1.57.0/go.mod h1:pwZMGvqe0JRkI1GWSZCtnAfrR4K1bv65IHILGA//VEU= +cloud.google.com/go/batch v1.7.0/go.mod h1:J64gD4vsNSA2O5TtDB5AAux3nJ9iV8U3ilg3JDBYejU= +cloud.google.com/go/billing v1.18.0/go.mod h1:5DOYQStCxquGprqfuid/7haD7th74kyMBHkjO/OvDtk= +cloud.google.com/go/binaryauthorization v1.8.0/go.mod h1:VQ/nUGRKhrStlGr+8GMS8f6/vznYLkdK5vaKfdCIpvU= +cloud.google.com/go/contactcenterinsights v1.12.1/go.mod h1:HHX5wrz5LHVAwfI2smIotQG9x8Qd6gYilaHcLLLmNis= +cloud.google.com/go/container v1.29.0/go.mod h1:b1A1gJeTBXVLQ6GGw9/9M4FG94BEGsqJ5+t4d/3N7O4= +cloud.google.com/go/dataplex v1.13.0/go.mod h1:mHJYQQ2VEJHsyoC0OdNyy988DvEbPhqFs5OOLffLX0c= +cloud.google.com/go/deploy v1.16.0/go.mod h1:e5XOUI5D+YGldyLNZ21wbp9S8otJbBE4i88PtO9x/2g= +cloud.google.com/go/dialogflow v1.47.0/go.mod h1:mHly4vU7cPXVweuB5R0zsYKPMzy240aQdAu06SqBbAQ= +cloud.google.com/go/documentai v1.23.6/go.mod h1:ghzBsyVTiVdkfKaUCum/9bGBEyBjDO4GfooEcYKhN+g= +cloud.google.com/go/maps v1.6.2/go.mod h1:4+buOHhYXFBp58Zj/K+Lc1rCmJssxxF4pJ5CJnhdz18= +cloud.google.com/go/recaptchaenterprise/v2 v2.9.0/go.mod h1:Dak54rw6lC2gBY8FBznpOCAR58wKf+R+ZSJRoeJok4w= +cloud.google.com/go/securitycenter v1.24.3/go.mod h1:l1XejOngggzqwr4Fa2Cn+iWZGf+aBLTXtB/vXjy5vXM= +cloud.google.com/go/spanner v1.53.1/go.mod h1:liG4iCeLqm5L3fFLU5whFITqP0e0orsAW1uUSrd4rws= +contrib.go.opencensus.io/exporter/prometheus v0.4.2/go.mod h1:dvEHbiKmgvbr5pjaF9fpw1KeYcjrnC1J8B+JKjsZyRQ= +docker.io/go-docker v1.0.0/go.mod h1:7tiAn5a0LFmjbPDbyTPOaTTOuG1ZRNXdPA6RvKY+fpY= +filippo.io/edwards25519 v1.0.0/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= +github.com/Azure/go-autorest/autorest/azure/auth v0.5.11/go.mod h1:84w/uV8E37feW2NCJ08uT9VBfjfUHpgLVnG2InYD6cg= +github.com/Azure/go-autorest/autorest/azure/cli v0.4.5/go.mod h1:ADQAXrkgm7acgWVUNamOgh8YNrv4p27l3Wc55oVfpzg= +github.com/OneOfOne/xxhash v1.2.6/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q= +github.com/Shopify/sarama v1.38.1/go.mod h1:iwv9a67Ha8VNa+TifujYoWGxWnu2kNVAQdSdZ4X2o5g= +github.com/alecthomas/kong v0.2.11/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE= +github.com/alecthomas/participle/v2 v2.1.0/go.mod h1:Y1+hAs8DHPmc3YUFzqllV+eSQ9ljPTk0ZkPMtEdAx2c= github.com/alicebob/miniredis v2.5.0+incompatible h1:yBHoLpsyjupjz3NL3MhKMVkR41j82Yjf3KFv7ApYzUI= +github.com/alicebob/miniredis v2.5.0+incompatible/go.mod h1:8HZjEj4yU0dwhYHky+DxYx+6BMjkBbe5ONFIF1MXffk= +github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40/go.mod h1:Q7yQnSMnLvcXlZ8RV+jwz/6y1rQTqbX6C82SndT52Zs= +github.com/apache/arrow/go/v13 v13.0.0/go.mod h1:W69eByFNO0ZR30q1/7Sr9d83zcVZmF2MiP3fFYAWJOc= +github.com/apache/thrift v0.18.1/go.mod h1:rdQn/dCcDKEWjjylUeueum4vQEjG2v8v2PqriUnbr+I= +github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM= github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= +github.com/containerd/containerd v1.6.8/go.mod h1:By6p5KqPK0/7/CgO/A6t/Gz+CUYUu2zf1hUaaymVXB0= +github.com/coreos/go-oidc v2.2.1+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f h1:JOrtw2xFKzlg+cbHpyrpLDmnN1HqhBfnX7WDiW7eG2c= github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk= -github.com/denisenkom/go-mssqldb v0.0.0-20190515213511-eb9f6a1743f3/go.mod h1:zAg7JM8CkOJ43xKXIj7eRO9kmWm/TW578qo+oDO6tuM= -github.com/denisenkom/go-mssqldb v0.12.0/go.mod h1:iiK0YP1ZeepvmBQk/QpLEhhTNJgfzrpArPY/aFvc9yU= +github.com/crewjam/httperr v0.2.0/go.mod h1:Jlz+Sg/XqBQhyMjdDiC+GNNRzZTD7x39Gu3pglZ5oH4= +github.com/cristalhq/hedgedhttp v0.9.1/go.mod h1:XkqWU6qVMutbhW68NnzjWrGtH8NUx1UfYqGYtHVKIsI= +github.com/dchest/uniuri v1.2.0/go.mod h1:fSzm4SLHzNZvWLvWJew423PhAzkpNQYq+uNLq4kxhkY= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= +github.com/drone/drone-runtime v1.1.0/go.mod h1:+osgwGADc/nyl40J0fdsf8Z09bgcBZXvXXnLOY48zYs= +github.com/drone/drone-yaml v1.2.3/go.mod h1:QsqliFK8nG04AHFN9tTn9XJomRBQHD4wcejWW1uz/10= +github.com/drone/funcmap v0.0.0-20211123105308-29742f68a7d1/go.mod h1:Hph0/pT6ZxbujnE1Z6/08p5I0XXuOsppqF6NQlGOK0E= +github.com/eapache/go-resiliency v1.3.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6iZYSs1ZI+iQho= +github.com/eapache/go-xerial-snappy v0.0.0-20230111030713-bf00bc1b83b6/go.mod h1:YvSRo5mw33fLEx1+DlK6L2VV43tJt5Eyel9n9XBcR+0= +github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb/go.mod h1:bH6Xx7IW64qjjJq8M2u4dxNaBiDfKK+z/3eGDpXEQhc= +github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= +github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= +github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= +github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU= +github.com/goccy/go-yaml v1.11.0/go.mod h1:H+mJrWtjPTJAHvRbV09MCK9xYwODM+wRTVFFTWckfng= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE= +github.com/google/go-jsonnet v0.18.0/go.mod h1:C3fTzyVJDslXdiTqw/bTFk7vSGyCtH3MGRbDfvEwGd0= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= +github.com/grafana/e2e v0.1.1-0.20221018202458-cffd2bb71c7b/go.mod h1:3UsooRp7yW5/NJQBlXcTsAHOoykEhNUYXkQ3r6ehEEY= +github.com/grafana/gomemcache v0.0.0-20231023152154-6947259a0586/go.mod h1:PGk3RjYHpxMM8HFPhKKo+vve3DdlPUELZLSDEFehPuU= +github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645/go.mod h1:6iZfnjpejD4L/4DwD7NryNaJyCQdzwWwH2MWhCA90Kw= +github.com/hamba/avro/v2 v2.17.2/go.mod h1:Q9YK+qxAhtVrNqOhwlZTATLgLA8qxG2vtvkhK8fJ7Jo= +github.com/jackspirou/syscerts v0.0.0-20160531025014-b68f5469dff1/go.mod h1:zuHl3Hh+e9P6gmBPvcqR1HjkaWHC/csgyskg6IaFKFo= +github.com/jaegertracing/jaeger v1.41.0/go.mod h1:SIkAT75iVmA9U+mESGYuMH6UQv6V9Qy4qxo0lwfCQAc= +github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= +github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= +github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= +github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= +github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= +github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= +github.com/jedib0t/go-pretty/v6 v6.2.4/go.mod h1:+nE9fyyHGil+PuISTCrp7avEdo6bqoMwqZnuiK2r2a0= +github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/jsternberg/zap-logfmt v1.2.0/go.mod h1:kz+1CUmCutPWABnNkOu9hOHKdT2q3TDYCcsFy9hpqb0= github.com/klauspost/cpuid v1.2.1 h1:vJi+O/nMdFt0vqm8NZBI6wzALWdA2X+egi0ogNyrC/w= +github.com/knadh/koanf v1.5.0/go.mod h1:Hgyjp4y8v44hpZtPzs7JZfRAW5AhN7KfZcwv1RYggDs= +github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= +github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y= +github.com/lestrrat-go/blackmagic v1.0.0/go.mod h1:TNgH//0vYSs8VXDCfkZLgIrVTTXQELZffUV0tz3MtdQ= +github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= +github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc= +github.com/lestrrat-go/jwx v1.2.25/go.mod h1:zoNuZymNl5lgdcu6P7K6ie2QRll5HVfF4xwxBBK1NxY= +github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/matryer/moq v0.2.7/go.mod h1:kITsx543GOENm48TUAQyJ9+SAvFSr7iGQXPoth/VUBk= +github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= +github.com/minio/minio-go/v7 v7.0.52/go.mod h1:IbbodHyjUAguneyucUaahv+VMNs/EOTV9du7A7/Z3HU= +github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= +github.com/mithrandie/readline-csvq v1.2.1/go.mod h1:ydD9Eyp3/wn8KPSNbKmMZe4RQQauCuxi26yEo4N40dk= +github.com/mostynb/go-grpc-compression v1.1.17/go.mod h1:FUSBr0QjKqQgoDG/e0yiqlR6aqyXC39+g/hFLDfSsEY= +github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= +github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= +github.com/open-telemetry/opentelemetry-collector-contrib/exporter/jaegerexporter v0.74.0/go.mod h1:bIeSj+SaZdP3CE9Xae+zurdQC6DXX0tPP6NAEVmgtt4= +github.com/open-telemetry/opentelemetry-collector-contrib/exporter/kafkaexporter v0.74.0/go.mod h1:v4H2ATSrKfOTbQnmjCxpvuOjrO/GUURAgey9RzrPsuQ= +github.com/open-telemetry/opentelemetry-collector-contrib/exporter/zipkinexporter v0.74.0/go.mod h1:UtVfxZGhPU2OvDh7H8o67VKWG9qHAHRNkhmZUWqCvME= +github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal v0.74.0/go.mod h1:TEu3TnUv1TuyHtjllrUDQ/ImpyD+GrkDejZv4hxl3G8= +github.com/open-telemetry/opentelemetry-collector-contrib/internal/sharedcomponent v0.74.0/go.mod h1:cAKlYKU+/8mk6ETOnD+EAi5gpXZjDrGweAB9YTYrv/g= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/jaeger v0.74.0/go.mod h1:OpEw7tyCg+iG1ywEgZ03qe5sP/8fhYdtWCMoqA8JCug= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/opencensus v0.74.0/go.mod h1:13ekplz1UmvK99Vz2VjSBWPYqoRBEax5LPmA1tFHnhA= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/zipkin v0.74.0/go.mod h1:TJT7HkhFPrJic30Vk4seF/eRk8sa0VQ442Xq/qd+DLY= +github.com/open-telemetry/opentelemetry-collector-contrib/receiver/jaegerreceiver v0.74.0/go.mod h1:0lXcDf6LUbtDxZZO3zDbRzMuL7gL1Q0FPOR8/3IBwaQ= +github.com/open-telemetry/opentelemetry-collector-contrib/receiver/kafkareceiver v0.74.0/go.mod h1:anSbwGOousKpnNAVMNP5YieA4KOFuEzHkvya0vvtsaI= +github.com/open-telemetry/opentelemetry-collector-contrib/receiver/opencensusreceiver v0.74.0/go.mod h1:uiW3V9EX8A5DOoxqDLuSh++ewHr+owtonCSiqMcpy3w= +github.com/open-telemetry/opentelemetry-collector-contrib/receiver/zipkinreceiver v0.74.0/go.mod h1:qoGuayD7cAtshnKosIQHd6dobcn6/sqgUn0v/Cg2UB8= +github.com/opentracing-contrib/go-grpc v0.0.0-20210225150812-73cb765af46e/go.mod h1:DYR5Eij8rJl8h7gblRrOZ8g0kW1umSpKqYIBTgeDtLo= +github.com/openzipkin/zipkin-go v0.4.1/go.mod h1:qY0VqDSN1pOBN94dBc6w2GJlWLiovAyg7Qt6/I9HecM= github.com/pierrec/lz4 v2.0.5+incompatible h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM8aXeqhl0I= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/pquerna/cachecontrol v0.1.0/go.mod h1:NrUG3Z7Rdu85UNR3vm7SOsl1nFIeSiQnrHV5K9mBcUI= +github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc= +github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= +github.com/prometheus/statsd_exporter v0.22.7/go.mod h1:N/TevpjkIh9ccs6nuzY3jQn9dFqnUakOjnEuMPJJJnI= +github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww= -github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs= +github.com/segmentio/fasthash v0.0.0-20180216231524-a72b379d632e/go.mod h1:tm/wZFQ8e24NYaBGIlnO2WGCAi67re4HHuOm0sftE/M= +github.com/segmentio/parquet-go v0.0.0-20230427215636-d483faba23a5/go.mod h1:+J0xQnJjm8DuQUHBO7t57EnmPbstT6+b45+p3DC9k1Q= +github.com/sercand/kuberesolver/v4 v4.0.0/go.mod h1:F4RGyuRmMAjeXHKL+w4P7AwUnPceEAPAhxUgXZjKgvM= +github.com/sercand/kuberesolver/v5 v5.1.1/go.mod h1:Fs1KbKhVRnB2aDWN12NjKCB+RgYMWZJ294T3BtmVCpQ= +github.com/shirou/gopsutil/v3 v3.23.2/go.mod h1:gv0aQw33GLo3pG8SiWKiQrbDzbRY1K80RyZJ7V4Th1M= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/viper v1.14.0/go.mod h1:WT//axPky3FdvXHzGw33dNdXXXfFQqmEalje+egj8As= +github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= +github.com/substrait-io/substrait-go v0.4.2/go.mod h1:qhpnLmrcvAnlZsUyPXZRqldiHapPTXC3t7xFgDi3aQg= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI= +github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ5UVIcaL4= +github.com/uber-go/atomic v1.4.0/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex1PFV8g= +github.com/vinzenz/yaml v0.0.0-20170920082545-91409cdd725d/go.mod h1:mb5taDqMnJiZNRQ3+02W2IFG+oEz1+dTuCXkp4jpkfo= +github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +github.com/weaveworks/common v0.0.0-20230511094633-334485600903/go.mod h1:rgbeLfJUtEr+G74cwFPR1k/4N0kDeaeSv/qhUNE4hm8= +github.com/weaveworks/promrus v1.2.0/go.mod h1:SaE82+OJ91yqjrE1rsvBWVzNZKcHYFtMUyS1+Ogs/KA= +github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI= +github.com/willf/bloom v2.0.3+incompatible/go.mod h1:MmAltL9pDMNTrvUkxdg0k0q5I0suxmuwp3KbyrZLOZ8= +github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8= +github.com/zenazn/goji v1.0.1/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= go.opentelemetry.io/collector v0.74.0 h1:0s2DKWczGj/pLTsXGb1P+Je7dyuGx9Is4/Dri1+cS7g= +go.opentelemetry.io/collector v0.74.0/go.mod h1:7NjZAvkhQ6E+NLN4EAH2hw3Nssi+F14t7mV7lMNXCto= +go.opentelemetry.io/collector/component v0.74.0/go.mod h1:zHbWqbdmnHeIZAuO3s1Fo/kWPC2oKuolIhlPmL4bzyo= +go.opentelemetry.io/collector/confmap v0.74.0/go.mod h1:NvUhMS2v8rniLvDAnvGjYOt0qBohk6TIibb1NuyVB1Q= +go.opentelemetry.io/collector/consumer v0.74.0/go.mod h1:MuGqt8/OKVAOjrh5WHr1TR2qwHizy64ZP2uNSr+XpvI= +go.opentelemetry.io/collector/exporter v0.74.0/go.mod h1:kw5YoorpKqEpZZ/a5ODSoYFK1mszzcKBNORd32S8Z7c= +go.opentelemetry.io/collector/exporter/otlpexporter v0.74.0/go.mod h1:cRbvsnpSxzySoTSnXbOGPQZu9KHlEyKkTeE21f9Q1p4= +go.opentelemetry.io/collector/receiver v0.74.0/go.mod h1:SQkyATvoZCJefNkI2jnrR63SOdrmDLYCnQqXJ7ACqn0= +go.opentelemetry.io/collector/receiver/otlpreceiver v0.74.0/go.mod h1:9X9/RYFxJIaK0JLlRZ0PpmQSSlYpY+r4KsTOj2jWj14= go.opentelemetry.io/contrib v0.18.0 h1:uqBh0brileIvG6luvBjdxzoFL8lxDGuhxJWsvK3BveI= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.42.0/go.mod h1:5z+/ZWJQKXa9YT34fQNx5K8Hd1EoIhvtUygUQPqEOgQ= +go.opentelemetry.io/contrib/propagators/b3 v1.15.0/go.mod h1:VjU0g2v6HSQ+NwfifambSLAeBgevjIcqmceaKWEzl0c= +go.opentelemetry.io/otel/bridge/opencensus v0.37.0/go.mod h1:ddiK+1PE68l/Xk04BGTh9Y6WIcxcLrmcVxVlS0w5WZ0= +go.opentelemetry.io/otel/bridge/opentracing v1.10.0/go.mod h1:J7GLR/uxxqMAzZptsH0pjte3Ep4GacTCrbGBoDuHBqk= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0/go.mod h1:0+KuTDyKL4gjKCF75pHOX4wuzYDUZYfAQdSu43o+Z2I= +go.opentelemetry.io/otel/exporters/prometheus v0.37.0/go.mod h1:hB8qWjsStK36t50/R0V2ULFb4u95X/Q6zupXLgvjTh8= +go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A= +go.opentelemetry.io/otel/sdk/metric v0.39.0/go.mod h1:piDIRgjcK7u0HCL5pCA4e74qpK/jk3NiUoAHATVAmiI= +go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= +go.uber.org/mock v0.2.0/go.mod h1:J0y0rp9L3xiff1+ZBfKxlC1fz2+aO16tw0tsDOixfuM= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= +golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA= +golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20211123203042-d83791d6bcd9/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/tools v0.12.0/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM= +golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= +google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3/go.mod h1:5RBcpGRxr25RbDzY5w+dmaqpSEvl8Gwl1x2CICf60ic= +google.golang.org/genproto/googleapis/api v0.0.0-20231211222908-989df2bf70f3/go.mod h1:k2dtGpRrbsSyKcNPKKI5sstZkrNCZwpU/ns96JoHbGg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231211222908-989df2bf70f3/go.mod h1:eJVxU6o+4G1PSczBr85xmyvSNYAKvAYgkub40YGomFM= +google.golang.org/grpc v1.60.0/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM= +gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98= +k8s.io/api v0.29.0/go.mod h1:sdVmXoz2Bo/cb77Pxi71IPTSErEW32xa4aXwKH7gfBA= +k8s.io/apiextensions-apiserver v0.26.2/go.mod h1:Y7UPgch8nph8mGCuVk0SK83LnS8Esf3n6fUBgew8SH8= +k8s.io/apimachinery v0.29.0/go.mod h1:eVBxQ/cwiJxH58eK/jd/vAk4mrxmVlnpBH5J2GbMeis= +k8s.io/apiserver v0.29.0/go.mod h1:31n78PsRKPmfpee7/l9NYEv67u6hOL6AfcE761HapDM= +k8s.io/client-go v0.29.0/go.mod h1:yLkXH4HKMAywcrD82KMSmfYg2DlE8mepPR4JGSo5n38= +k8s.io/component-base v0.29.0/go.mod h1:sADonFTQ9Zc9yFLghpDpmNXEdHyQmFIGbiuZbqAXQ1M= +k8s.io/gengo v0.0.0-20230829151522-9cce18d56c01/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= -k8s.io/kms v0.29.2 h1:MDsbp98gSlEQs7K7dqLKNNTwKFQRYYvO4UOlBOjNy6Y= +k8s.io/kms v0.29.0/go.mod h1:mB0f9HLxRXeXUfHfn1A7rpwOlzXI1gIWu86z6buNoYA= k8s.io/kms v0.29.2/go.mod h1:s/9RC4sYRZ/6Tn6yhNjbfJuZdb8LzlXhdlBnKizeFDo= +k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA= +k8s.io/kube-openapi v0.0.0-20231214164306-ab13479f8bf8/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA= diff --git a/hack/openapi-codegen.sh b/hack/openapi-codegen.sh index efcf678e6b..070966375d 100644 --- a/hack/openapi-codegen.sh +++ b/hack/openapi-codegen.sh @@ -115,7 +115,6 @@ function grafana::codegen::gen_openapi() { local input_pkgs=() while read -r dir; do - echo ${dir} pkg="$(cd "${dir}" && GO111MODULE=on go list -find .)" input_pkgs+=("${pkg}") done < <( @@ -127,6 +126,8 @@ function grafana::codegen::gen_openapi() { | LC_ALL=C sort -u ) + + local new_report="" if [ "${#input_pkgs[@]}" != 0 ]; then echo "Generating openapi code for ${#input_pkgs[@]} targets" @@ -139,7 +140,6 @@ function grafana::codegen::gen_openapi() { inputs+=("--input-dirs" "$arg") done - local new_report new_report="${root}/${report}.tmp" if [ -n "${update_report}" ]; then new_report="${root}/${report}" @@ -157,6 +157,9 @@ function grafana::codegen::gen_openapi() { fi touch "${root}/${report}" # in case it doesn't exist yet + if [[ -z "${new_report}" ]]; then + return 0 + fi if ! diff -u "${root}/${report}" "${new_report}"; then echo -e "ERROR:" echo -e "\tAPI rule check failed for ${root}/${report}: new reported violations" diff --git a/hack/update-codegen.sh b/hack/update-codegen.sh index 37c8439ea9..de30331ed0 100755 --- a/hack/update-codegen.sh +++ b/hack/update-codegen.sh @@ -10,7 +10,7 @@ set -o nounset set -o pipefail SCRIPT_ROOT=$(dirname "${BASH_SOURCE[0]}")/.. -CODEGEN_PKG=${CODEGEN_PKG:-$(cd "${SCRIPT_ROOT}"; ls -d -1 ./vendor/k8s.io/code-generator 2>/dev/null || echo $GOPATH/pkg/mod/k8s.io/code-generator@v0.29.1)} +CODEGEN_PKG=${CODEGEN_PKG:-$(cd "${SCRIPT_ROOT}"; ls -d -1 ./vendor/k8s.io/code-generator 2>/dev/null || echo $(go env GOPATH)/pkg/mod/k8s.io/code-generator@v0.29.1)} OUTDIR="${HOME}/go/src" OPENAPI_VIOLATION_EXCEPTIONS_FILENAME="zz_generated.openapi_violation_exceptions.list" @@ -18,52 +18,70 @@ OPENAPI_VIOLATION_EXCEPTIONS_FILENAME="zz_generated.openapi_violation_exceptions source "${CODEGEN_PKG}/kube_codegen.sh" source "$(dirname "${BASH_SOURCE[0]}")/openapi-codegen.sh" +selected_pkg="${1-}" -for api_pkg in $(ls ./pkg/apis); do - if [[ "${1-}" != "" && ${api_pkg} != $1 ]]; then - continue - fi - include_common_input_dirs=$([[ ${api_pkg} == "common" ]] && echo "true" || echo "false") - for pkg_version in $(ls ./pkg/apis/${api_pkg}); do - echo "API: ${api_pkg}/${pkg_version}" - echo "-------------------------------------------" +grafana::codegen:run() { + local generate_root=$1 + local skipped="true" + for api_pkg in $(grafana:codegen:lsdirs ./${generate_root}/apis); do + echo "Generating code for ${generate_root}/apis/${api_pkg}..." + echo "=============================================" + if [[ "${selected_pkg}" != "" && ${api_pkg} != $selected_pkg ]]; then + continue + fi + skipped="false" + include_common_input_dirs=$([[ ${api_pkg} == "common" ]] && echo "true" || echo "false") kube::codegen::gen_helpers \ - --input-pkg-root github.com/grafana/grafana/pkg/apis/${api_pkg}/${pkg_version} \ + --input-pkg-root github.com/grafana/grafana/${generate_root}/apis/${api_pkg} \ --output-base "${OUTDIR}" \ --boilerplate "${SCRIPT_ROOT}/hack/boilerplate.go.txt" + for pkg_version in $(grafana:codegen:lsdirs ./${generate_root}/apis/${api_pkg}); do + grafana::codegen::gen_openapi \ + --input-pkg-single github.com/grafana/grafana/${generate_root}/apis/${api_pkg}/${pkg_version} \ + --output-base "${OUTDIR}" \ + --report-filename "${OPENAPI_VIOLATION_EXCEPTIONS_FILENAME}" \ + --update-report \ + --boilerplate "${SCRIPT_ROOT}/hack/boilerplate.go.txt" \ + --include-common-input-dirs ${include_common_input_dirs} - echo "Generating openapi package for ${api_pkg}, version=${pkg_version} ..." + violations_file="${OUTDIR}/github.com/grafana/grafana/${generate_root}/apis/${api_pkg}/${pkg_version}/${OPENAPI_VIOLATION_EXCEPTIONS_FILENAME}" + # delete violation exceptions file, if empty + if ! grep -q . "${violations_file}"; then + echo "Deleting ${violations_file} since it is empty" + rm ${violations_file} + fi - grafana::codegen::gen_openapi \ - --input-pkg-single github.com/grafana/grafana/pkg/apis/${api_pkg}/${pkg_version} \ - --output-base "${OUTDIR}" \ - --report-filename "${OPENAPI_VIOLATION_EXCEPTIONS_FILENAME}" \ - --update-report \ - --boilerplate "${SCRIPT_ROOT}/hack/boilerplate.go.txt" \ - --include-common-input-dirs ${include_common_input_dirs} - - violations_file="${OUTDIR}/github.com/grafana/grafana/pkg/apis/${api_pkg}/${pkg_version}/${OPENAPI_VIOLATION_EXCEPTIONS_FILENAME}" - # delete violation exceptions file, if empty - if ! grep -q . "${violations_file}"; then - echo "Deleting ${violations_file} since it is empty" - rm ${violations_file} - fi - - echo "" + echo "" + done done -done - -echo "Generating client code..." -echo "---------------------------" - -kube::codegen::gen_client \ - --with-watch \ - --with-applyconfig \ - --input-pkg-root github.com/grafana/grafana/pkg/apis \ - --output-pkg-root github.com/grafana/grafana/pkg/generated \ - --output-base "${OUTDIR}" \ - --boilerplate "${SCRIPT_ROOT}/hack/boilerplate.go.txt" + + if [[ "${skipped}" == "true" ]]; then + echo "no apis matching ${selected_pkg}. skipping..." + echo + return 0 + fi + + echo "Generating client code..." + echo "-------------------------" + + kube::codegen::gen_client \ + --with-watch \ + --with-applyconfig \ + --input-pkg-root github.com/grafana/grafana/${generate_root}/apis \ + --output-pkg-root github.com/grafana/grafana/${generate_root}/generated \ + --output-base "${OUTDIR}" \ + --boilerplate "${SCRIPT_ROOT}/hack/boilerplate.go.txt" + + echo "" +} + +grafana:codegen:lsdirs() { + ls -d $1/*/ | xargs basename -a +} + +grafana::codegen:run pkg +grafana::codegen:run pkg/apimachinery echo "done." diff --git a/pkg/apis/dashboard/v0alpha1/zz_generated.openapi.go b/pkg/apis/dashboard/v0alpha1/zz_generated.openapi.go index d701d525d0..c4767b898c 100644 --- a/pkg/apis/dashboard/v0alpha1/zz_generated.openapi.go +++ b/pkg/apis/dashboard/v0alpha1/zz_generated.openapi.go @@ -121,7 +121,7 @@ func schema_pkg_apis_dashboard_v0alpha1_Dashboard(ref common.ReferenceCallback) "spec": { SchemaProps: spec.SchemaProps{ Description: "The dashboard body (unstructured for now)", - Ref: ref("github.com/grafana/grafana/pkg/apis/common/v0alpha1.Unstructured"), + Ref: ref("github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.Unstructured"), }, }, }, @@ -129,7 +129,7 @@ func schema_pkg_apis_dashboard_v0alpha1_Dashboard(ref common.ReferenceCallback) }, }, Dependencies: []string{ - "github.com/grafana/grafana/pkg/apis/common/v0alpha1.Unstructured", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.Unstructured", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, } } diff --git a/pkg/apis/dashboardsnapshot/v0alpha1/zz_generated.openapi.go b/pkg/apis/dashboardsnapshot/v0alpha1/zz_generated.openapi.go index f5824a7d78..7566695cfa 100644 --- a/pkg/apis/dashboardsnapshot/v0alpha1/zz_generated.openapi.go +++ b/pkg/apis/dashboardsnapshot/v0alpha1/zz_generated.openapi.go @@ -61,7 +61,7 @@ func schema_pkg_apis_dashboardsnapshot_v0alpha1_DashboardCreateCommand(ref commo "dashboard": { SchemaProps: spec.SchemaProps{ Description: "The complete dashboard model. required:true", - Ref: ref("github.com/grafana/grafana/pkg/apis/common/v0alpha1.Unstructured"), + Ref: ref("github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.Unstructured"), }, }, "expires": { @@ -85,7 +85,7 @@ func schema_pkg_apis_dashboardsnapshot_v0alpha1_DashboardCreateCommand(ref commo }, }, Dependencies: []string{ - "github.com/grafana/grafana/pkg/apis/common/v0alpha1.Unstructured"}, + "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.Unstructured"}, } } @@ -325,7 +325,7 @@ func schema_pkg_apis_dashboardsnapshot_v0alpha1_FullDashboardSnapshot(ref common "dashboard": { SchemaProps: spec.SchemaProps{ Description: "The raw dashboard (unstructured for now)", - Ref: ref("github.com/grafana/grafana/pkg/apis/common/v0alpha1.Unstructured"), + Ref: ref("github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.Unstructured"), }, }, }, @@ -333,7 +333,7 @@ func schema_pkg_apis_dashboardsnapshot_v0alpha1_FullDashboardSnapshot(ref common }, }, Dependencies: []string{ - "github.com/grafana/grafana/pkg/apis/common/v0alpha1.Unstructured", "github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1.SnapshotInfo", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.Unstructured", "github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1.SnapshotInfo", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, } } diff --git a/pkg/apis/datasource/v0alpha1/zz_generated.openapi.go b/pkg/apis/datasource/v0alpha1/zz_generated.openapi.go index 6635a5af92..1450dc9847 100644 --- a/pkg/apis/datasource/v0alpha1/zz_generated.openapi.go +++ b/pkg/apis/datasource/v0alpha1/zz_generated.openapi.go @@ -163,13 +163,13 @@ func schema_pkg_apis_datasource_v0alpha1_HealthCheckResult(ref common.ReferenceC "details": { SchemaProps: spec.SchemaProps{ Description: "Spec depends on the the plugin", - Ref: ref("github.com/grafana/grafana/pkg/apis/common/v0alpha1.Unstructured"), + Ref: ref("github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.Unstructured"), }, }, }, }, }, Dependencies: []string{ - "github.com/grafana/grafana/pkg/apis/common/v0alpha1.Unstructured"}, + "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.Unstructured"}, } } diff --git a/pkg/apis/example/v0alpha1/zz_generated.openapi.go b/pkg/apis/example/v0alpha1/zz_generated.openapi.go index 10f31c70d6..8c441c5506 100644 --- a/pkg/apis/example/v0alpha1/zz_generated.openapi.go +++ b/pkg/apis/example/v0alpha1/zz_generated.openapi.go @@ -51,14 +51,14 @@ func schema_pkg_apis_example_v0alpha1_DummyResource(ref common.ReferenceCallback }, "spec": { SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/grafana/grafana/pkg/apis/common/v0alpha1.Unstructured"), + Ref: ref("github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.Unstructured"), }, }, }, }, }, Dependencies: []string{ - "github.com/grafana/grafana/pkg/apis/common/v0alpha1.Unstructured", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.Unstructured", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, } } diff --git a/pkg/apis/featuretoggle/v0alpha1/zz_generated.openapi.go b/pkg/apis/featuretoggle/v0alpha1/zz_generated.openapi.go index 784d4201be..a7817ad424 100644 --- a/pkg/apis/featuretoggle/v0alpha1/zz_generated.openapi.go +++ b/pkg/apis/featuretoggle/v0alpha1/zz_generated.openapi.go @@ -418,7 +418,7 @@ func schema_pkg_apis_featuretoggle_v0alpha1_ToggleStatus(ref common.ReferenceCal "source": { SchemaProps: spec.SchemaProps{ Description: "Where was the value configured eg: startup | tenant|org | user | browser missing means default", - Ref: ref("github.com/grafana/grafana/pkg/apis/common/v0alpha1.ObjectReference"), + Ref: ref("github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.ObjectReference"), }, }, "warning": { @@ -433,6 +433,6 @@ func schema_pkg_apis_featuretoggle_v0alpha1_ToggleStatus(ref common.ReferenceCal }, }, Dependencies: []string{ - "github.com/grafana/grafana/pkg/apis/common/v0alpha1.ObjectReference"}, + "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.ObjectReference"}, } } diff --git a/pkg/apis/query/v0alpha1/zz_generated.openapi.go b/pkg/apis/query/v0alpha1/zz_generated.openapi.go index 48619ed2dc..755fb534ce 100644 --- a/pkg/apis/query/v0alpha1/zz_generated.openapi.go +++ b/pkg/apis/query/v0alpha1/zz_generated.openapi.go @@ -536,7 +536,7 @@ func schema_apis_query_v0alpha1_template_TemplateVariable(ref common.ReferenceCa "valueListDefinition": { SchemaProps: spec.SchemaProps{ Description: "ValueListDefinition is the object definition used by the FE to get a list of possible values to select for render.", - Ref: ref("github.com/grafana/grafana/pkg/apis/common/v0alpha1.Unstructured"), + Ref: ref("github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.Unstructured"), }, }, }, @@ -544,7 +544,7 @@ func schema_apis_query_v0alpha1_template_TemplateVariable(ref common.ReferenceCa }, }, Dependencies: []string{ - "github.com/grafana/grafana/pkg/apis/common/v0alpha1.Unstructured"}, + "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.Unstructured"}, } } From 11ff4e7b8c2520b9b1022b800255289b553ec460 Mon Sep 17 00:00:00 2001 From: Darren Janeczek <38694490+darrenjaneczek@users.noreply.github.com> Date: Sun, 25 Feb 2024 12:54:10 -0500 Subject: [PATCH 0154/1406] fix: datatrails: start step is "metric select" after reinitializing from URL (#83179) fix: datatrails: start step is "metric select" If restoring from a saved data trail, or URL, the selection of metric becomes a second step. The first step is always without a metric, and displays the metric selection scene. --- public/app/features/trails/DataTrail.tsx | 2 +- .../app/features/trails/DataTrailsHistory.tsx | 17 ++++++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/public/app/features/trails/DataTrail.tsx b/public/app/features/trails/DataTrail.tsx index a7a284c958..68f044923e 100644 --- a/public/app/features/trails/DataTrail.tsx +++ b/public/app/features/trails/DataTrail.tsx @@ -193,7 +193,7 @@ export class DataTrail extends SceneObjectBase { }; } -function getTopSceneFor(metric?: string) { +export function getTopSceneFor(metric?: string) { if (metric) { return new MetricScene({ metric: metric }); } else { diff --git a/public/app/features/trails/DataTrailsHistory.tsx b/public/app/features/trails/DataTrailsHistory.tsx index f5def4ad5b..c562b85cd3 100644 --- a/public/app/features/trails/DataTrailsHistory.tsx +++ b/public/app/features/trails/DataTrailsHistory.tsx @@ -13,7 +13,7 @@ import { } from '@grafana/scenes'; import { useStyles2, Tooltip, Stack } from '@grafana/ui'; -import { DataTrail, DataTrailState } from './DataTrail'; +import { DataTrail, DataTrailState, getTopSceneFor } from './DataTrail'; import { VAR_FILTERS } from './shared'; import { getTrailFor, isSceneTimeRangeState } from './utils'; @@ -44,7 +44,22 @@ export class DataTrailHistory extends SceneObjectBase { const trail = getTrailFor(this); if (this.state.steps.length === 0) { + // We always want to ensure in initial 'start' step this.addTrailStep(trail, 'start'); + + if (trail.state.metric) { + // But if our current trail has a metric, we want to remove it and the topScene, + // so that the "start" step always displays a metric select screen. + + // So we remove the metric and update the topscene for the "start" step + const { metric, ...startState } = trail.state; + startState.topScene = getTopSceneFor(undefined); + this.state.steps[0].trailState = startState; + + // But must add a secondary step to represent the selection of the metric + // for this restored trail state + this.addTrailStep(trail, 'metric'); + } } trail.subscribeToState((newState, oldState) => { From af4382e4c28cf3667b5fc898cb2aed66489b4fda Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Mon, 26 Feb 2024 09:21:29 +0100 Subject: [PATCH 0155/1406] plugin-e2e: Fix flaky test (#83370) fix mock issue --- .../as-admin-user/panelEditPage.spec.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelEditPage.spec.ts b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelEditPage.spec.ts index a44827b0de..5f611f7e14 100644 --- a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelEditPage.spec.ts +++ b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelEditPage.spec.ts @@ -1,4 +1,4 @@ -import { expect, test } from '@grafana/plugin-e2e'; +import { DashboardPage, expect, test } from '@grafana/plugin-e2e'; import { formatExpectError } from '../errors'; import { successfulDataQuery } from '../mocks/queries'; @@ -33,8 +33,11 @@ test.describe('query editor query data', () => { }); test.describe('query editor with mocked responses', () => { - test('and resource `scenarios` is mocked', async ({ panelEditPage, selectors }) => { - await panelEditPage.mockResourceResponse('scenarios', scenarios); + test('and resource `scenarios` is mocked', async ({ page, selectors, grafanaVersion, request }) => { + const dashboardPage = new DashboardPage({ page, selectors, grafanaVersion, request }); + await dashboardPage.goto(); + await dashboardPage.mockResourceResponse('scenarios', scenarios); + const panelEditPage = await dashboardPage.addPanel(); await panelEditPage.datasource.set('gdev-testdata'); const queryEditorRow = await panelEditPage.getQueryEditorRow('A'); await queryEditorRow.getByLabel('Scenario').last().click(); From 1899afccb5ac62004d2fc1ccfc2ddf42018c7e1a Mon Sep 17 00:00:00 2001 From: Joey <90795735+joey-grafana@users.noreply.github.com> Date: Mon, 26 Feb 2024 08:59:13 +0000 Subject: [PATCH 0156/1406] Tempo: Add support for ad-hoc filters (#83290) * Add support for ad-hoc filters * Add tests * Update types --- .../datasource/tempo/datasource.test.ts | 53 +++++++++++++++++++ .../plugins/datasource/tempo/datasource.ts | 18 +++++++ 2 files changed, 71 insertions(+) diff --git a/public/app/plugins/datasource/tempo/datasource.test.ts b/public/app/plugins/datasource/tempo/datasource.test.ts index 0ed1d22114..de5416de26 100644 --- a/public/app/plugins/datasource/tempo/datasource.test.ts +++ b/public/app/plugins/datasource/tempo/datasource.test.ts @@ -1159,6 +1159,59 @@ describe('label values', () => { }); }); +describe('should provide functionality for ad-hoc filters', () => { + let datasource: TempoDatasource; + + beforeEach(() => { + datasource = createTempoDatasource(); + jest.spyOn(datasource, 'metadataRequest').mockImplementation( + createMetadataRequest({ + data: { + scopes: [{ name: 'span', tags: ['label1', 'label2'] }], + tagValues: [ + { + type: 'value1', + value: 'value1', + label: 'value1', + }, + { + type: 'value2', + value: 'value2', + label: 'value2', + }, + ], + }, + }) + ); + }); + + it('for getTagKeys', async () => { + const response = await datasource.getTagKeys(); + expect(response).toEqual([{ text: 'span.label1' }, { text: 'span.label2' }]); + }); + + it('for getTagValues', async () => { + const now = dateTime('2021-04-20T15:55:00Z'); + const options = { + key: 'span.label1', + filters: [], + timeRange: { + from: now, + to: now, + raw: { + from: 'now-15m', + to: 'now', + }, + }, + }; + const response = await datasource.getTagValues(options); + expect(response).toEqual([ + { text: { type: 'value1', value: 'value1', label: 'value1' } }, + { text: { type: 'value2', value: 'value2', label: 'value2' } }, + ]); + }); +}); + const prometheusMock = (): DataSourceApi => { return { query: jest.fn(() => diff --git a/public/app/plugins/datasource/tempo/datasource.ts b/public/app/plugins/datasource/tempo/datasource.ts index 13d82d9abf..d2ada1f496 100644 --- a/public/app/plugins/datasource/tempo/datasource.ts +++ b/public/app/plugins/datasource/tempo/datasource.ts @@ -11,6 +11,7 @@ import { DataQueryResponse, DataQueryResponseData, DataSourceApi, + DataSourceGetTagValuesOptions, DataSourceInstanceSettings, dateTime, FieldType, @@ -211,6 +212,23 @@ export class TempoDatasource extends DataSourceWithBackend> { + await this.languageProvider.fetchTags(); + const tags = this.languageProvider.tagsV2 || []; + return tags + .map(({ name, tags }) => + tags.filter((tag) => tag !== undefined).map((t) => (name !== 'intrinsic' ? `${name}.${t}` : `${t}`)) + ) + .flat() + .map((tag) => ({ text: tag })); + } + + // Allows to retrieve the list of tag values for ad-hoc filters + getTagValues(options: DataSourceGetTagValuesOptions): Promise> { + return this.labelValuesQuery(options.key.replace(/^(resource|span)\./, '')); + } + init = async () => { const response = await lastValueFrom( this._request('/api/status/buildinfo').pipe( From dea0a0f6c80a3ee8808d5193f5054be8245e6fe5 Mon Sep 17 00:00:00 2001 From: Selene Date: Mon, 26 Feb 2024 10:18:19 +0100 Subject: [PATCH 0157/1406] Kinds: Generate k8 resources without use kindys/thema (#83310) Generate k8 resources reading cue file directly instead of use thema/kindsys binding --- kinds/gen.go | 54 +++++++- pkg/codegen/jenny_go_resources.go | 128 ----------------- pkg/codegen/jenny_k8_resources.go | 130 ++++++++++++++++++ pkg/codegen/jenny_tsveneerindex.go | 2 + pkg/codegen/tmpl.go | 15 +- pkg/codegen/tmpl/core_metadata.tmpl | 33 +++++ pkg/codegen/tmpl/core_resource.tmpl | 5 +- pkg/codegen/tmpl/core_status.tmpl | 65 +++++++++ pkg/kinds/accesspolicy/accesspolicy_gen.go | 2 +- .../accesspolicy/accesspolicy_metadata_gen.go | 2 +- .../accesspolicy/accesspolicy_status_gen.go | 2 +- pkg/kinds/dashboard/dashboard_gen.go | 2 +- pkg/kinds/dashboard/dashboard_metadata_gen.go | 2 +- pkg/kinds/dashboard/dashboard_status_gen.go | 2 +- pkg/kinds/librarypanel/librarypanel_gen.go | 2 +- .../librarypanel/librarypanel_metadata_gen.go | 2 +- .../librarypanel/librarypanel_status_gen.go | 2 +- pkg/kinds/preferences/preferences_gen.go | 2 +- .../preferences/preferences_metadata_gen.go | 2 +- .../preferences/preferences_status_gen.go | 2 +- .../publicdashboard/publicdashboard_gen.go | 2 +- .../publicdashboard_metadata_gen.go | 2 +- .../publicdashboard_status_gen.go | 2 +- pkg/kinds/role/role_gen.go | 2 +- pkg/kinds/role/role_metadata_gen.go | 2 +- pkg/kinds/role/role_status_gen.go | 2 +- pkg/kinds/rolebinding/rolebinding_gen.go | 2 +- .../rolebinding/rolebinding_metadata_gen.go | 2 +- .../rolebinding/rolebinding_status_gen.go | 2 +- pkg/kinds/team/team_gen.go | 2 +- pkg/kinds/team/team_metadata_gen.go | 2 +- pkg/kinds/team/team_status_gen.go | 2 +- 32 files changed, 320 insertions(+), 160 deletions(-) delete mode 100644 pkg/codegen/jenny_go_resources.go create mode 100644 pkg/codegen/jenny_k8_resources.go create mode 100644 pkg/codegen/tmpl/core_metadata.tmpl create mode 100644 pkg/codegen/tmpl/core_status.tmpl diff --git a/kinds/gen.go b/kinds/gen.go index b126b1a32b..57ee9f830c 100644 --- a/kinds/gen.go +++ b/kinds/gen.go @@ -15,6 +15,7 @@ import ( "sort" "strings" + "cuelang.org/go/cue" "cuelang.org/go/cue/errors" "github.com/grafana/codejen" "github.com/grafana/cuetsy" @@ -38,8 +39,6 @@ func main() { // All the jennies that comprise the core kinds generator pipeline coreKindsGen.Append( - &codegen.ResourceGoTypesJenny{}, - &codegen.SubresourceGoTypesJenny{}, codegen.CoreKindJenny(cuectx.GoCoreKindParentPath, nil), codegen.BaseCoreRegistryJenny(filepath.Join("pkg", "registry", "corekind"), cuectx.GoCoreKindParentPath), codegen.LatestMajorsOrXJenny( @@ -95,6 +94,16 @@ func main() { die(err) } + // Merging k8 resources + k8Resources, err := genK8Resources(kinddirs) + if err != nil { + die(err) + } + + if err = jfs.Merge(k8Resources); err != nil { + die(err) + } + if _, set := os.LookupEnv("CODEGEN_VERIFY"); set { if err = jfs.Verify(context.Background(), groot); err != nil { die(fmt.Errorf("generated code is out of sync with inputs:\n%s\nrun `make gen-cue` to regenerate", err)) @@ -181,3 +190,44 @@ func die(err error) { fmt.Fprint(os.Stderr, err, "\n") os.Exit(1) } + +func genK8Resources(dirs []os.DirEntry) (*codejen.FS, error) { + jenny := codejen.JennyListWithNamer[[]cue.Value](func(_ []cue.Value) string { + return "K8Resources" + }) + + jenny.Append(&codegen.K8ResourcesJenny{}) + + header := codegen.SlashHeaderMapper("kinds/gen.go") + jenny.AddPostprocessors(header) + + return jenny.GenerateFS(loadCueFiles(dirs)) +} + +func loadCueFiles(dirs []os.DirEntry) []cue.Value { + ctx := cuectx.GrafanaCUEContext() + values := make([]cue.Value, 0) + for _, dir := range dirs { + if !dir.IsDir() { + continue + } + + entries, err := os.ReadDir(dir.Name()) + if err != nil { + fmt.Fprintf(os.Stderr, "error opening %s directory: %s", dir, err) + os.Exit(1) + } + + // It's assuming that we only have one file in each folder + entry := filepath.Join(dir.Name(), entries[0].Name()) + cueFile, err := os.ReadFile(entry) + if err != nil { + fmt.Fprintf(os.Stderr, "unable to open %s/%s file: %s", dir, entries[0].Name(), err) + os.Exit(1) + } + + values = append(values, ctx.CompileBytes(cueFile)) + } + + return values +} diff --git a/pkg/codegen/jenny_go_resources.go b/pkg/codegen/jenny_go_resources.go deleted file mode 100644 index 72a3dfa5f6..0000000000 --- a/pkg/codegen/jenny_go_resources.go +++ /dev/null @@ -1,128 +0,0 @@ -package codegen - -import ( - "bytes" - "fmt" - "go/format" - "strings" - - "cuelang.org/go/cue" - "github.com/dave/dst/dstutil" - "github.com/grafana/codejen" - "github.com/grafana/kindsys" - "github.com/grafana/thema/encoding/gocode" - "github.com/grafana/thema/encoding/openapi" -) - -var schPath = cue.MakePath(cue.Hid("_#schema", "github.com/grafana/thema")) - -type ResourceGoTypesJenny struct { - ApplyFuncs []dstutil.ApplyFunc - ExpandReferences bool -} - -func (*ResourceGoTypesJenny) JennyName() string { - return "GoTypesJenny" -} - -func (ag *ResourceGoTypesJenny) Generate(kind kindsys.Kind) (*codejen.File, error) { - comm := kind.Props().Common() - sfg := SchemaForGen{ - Name: comm.Name, - Schema: kind.Lineage().Latest(), - IsGroup: comm.LineageIsGroup, - } - sch := sfg.Schema - - iter, err := sch.Underlying().LookupPath(schPath).Fields() - if err != nil { - return nil, err - } - - var subr []string - for iter.Next() { - subr = append(subr, typeNameFromKey(iter.Selector().String())) - } - - buf := new(bytes.Buffer) - mname := kind.Props().Common().MachineName - if err := tmpls.Lookup("core_resource.tmpl").Execute(buf, tvars_resource{ - PackageName: mname, - KindName: kind.Props().Common().Name, - Version: strings.Replace(sfg.Schema.Version().String(), ".", "-", -1), - SubresourceNames: subr, - }); err != nil { - return nil, fmt.Errorf("failed executing core resource template: %w", err) - } - - if err != nil { - return nil, err - } - - content, err := format.Source(buf.Bytes()) - if err != nil { - return nil, err - } - - return codejen.NewFile(fmt.Sprintf("pkg/kinds/%s/%s_gen.go", mname, mname), content, ag), nil -} - -type SubresourceGoTypesJenny struct { - ApplyFuncs []dstutil.ApplyFunc - ExpandReferences bool -} - -func (*SubresourceGoTypesJenny) JennyName() string { - return "GoResourceTypes" -} - -func (g *SubresourceGoTypesJenny) Generate(kind kindsys.Kind) (codejen.Files, error) { - comm := kind.Props().Common() - sfg := SchemaForGen{ - Name: comm.Name, - Schema: kind.Lineage().Latest(), - IsGroup: comm.LineageIsGroup, - } - sch := sfg.Schema - - // Iterate through all top-level fields and make go types for them - // (this should consist of "spec" and arbitrary subresources) - i, err := sch.Underlying().LookupPath(schPath).Fields() - if err != nil { - return nil, err - } - files := make(codejen.Files, 0) - for i.Next() { - str := i.Selector().String() - - b, err := gocode.GenerateTypesOpenAPI(sch, &gocode.TypeConfigOpenAPI{ - // TODO will need to account for sanitizing e.g. dashes here at some point - Config: &openapi.Config{ - Group: false, // TODO: better - RootName: typeNameFromKey(str), - Subpath: cue.MakePath(cue.Str(str)), - }, - PackageName: sfg.Schema.Lineage().Name(), - ApplyFuncs: append(g.ApplyFuncs, PrefixDropper(sfg.Name)), - }) - if err != nil { - return nil, err - } - - name := sfg.Schema.Lineage().Name() - files = append(files, codejen.File{ - RelativePath: fmt.Sprintf("pkg/kinds/%s/%s_%s_gen.go", name, name, strings.ToLower(str)), - Data: b, - From: []codejen.NamedJenny{g}, - }) - } - - return files, nil -} - -func typeNameFromKey(key string) string { - if len(key) > 0 { - return strings.ToUpper(key[:1]) + key[1:] - } - return strings.ToUpper(key) -} diff --git a/pkg/codegen/jenny_k8_resources.go b/pkg/codegen/jenny_k8_resources.go new file mode 100644 index 0000000000..56ca123af7 --- /dev/null +++ b/pkg/codegen/jenny_k8_resources.go @@ -0,0 +1,130 @@ +package codegen + +import ( + "bytes" + "fmt" + "go/format" + "strings" + + "cuelang.org/go/cue" + "github.com/grafana/codejen" +) + +// K8ResourcesJenny generates resource, metadata and status for each file. +type K8ResourcesJenny struct { +} + +func (jenny *K8ResourcesJenny) JennyName() string { + return "K8ResourcesJenny" +} + +func (jenny *K8ResourcesJenny) Generate(cueFiles []cue.Value) (codejen.Files, error) { + files := make(codejen.Files, 0) + for _, val := range cueFiles { + pkg, err := getPackageName(val) + if err != nil { + return nil, err + } + + resource, err := jenny.genResource(pkg, val) + if err != nil { + return nil, err + } + + metadata, err := jenny.genMetadata(pkg) + if err != nil { + return nil, err + } + + status, err := jenny.genStatus(pkg) + if err != nil { + return nil, err + } + + files = append(files, resource) + files = append(files, metadata) + files = append(files, status) + } + + return files, nil +} + +func (jenny *K8ResourcesJenny) genResource(pkg string, val cue.Value) (codejen.File, error) { + version, err := getVersion(val) + if err != nil { + return codejen.File{}, err + } + + pkgName := strings.ToLower(pkg) + + buf := new(bytes.Buffer) + if err := tmpls.Lookup("core_resource.tmpl").Execute(buf, tvars_resource{ + PackageName: pkgName, + KindName: pkg, + Version: version, + }); err != nil { + return codejen.File{}, fmt.Errorf("failed executing core resource template: %w", err) + } + + content, err := format.Source(buf.Bytes()) + if err != nil { + return codejen.File{}, err + } + + return *codejen.NewFile(fmt.Sprintf("pkg/kinds/%s/%s_gen.go", pkgName, pkgName), content, jenny), nil +} + +func (jenny *K8ResourcesJenny) genMetadata(pkg string) (codejen.File, error) { + pkg = strings.ToLower(pkg) + + buf := new(bytes.Buffer) + if err := tmpls.Lookup("core_metadata.tmpl").Execute(buf, tvars_metadata{ + PackageName: pkg, + }); err != nil { + return codejen.File{}, fmt.Errorf("failed executing core resource template: %w", err) + } + + return *codejen.NewFile(fmt.Sprintf("pkg/kinds/%s/%s_metadata_gen.go", pkg, pkg), buf.Bytes(), jenny), nil +} + +func (jenny *K8ResourcesJenny) genStatus(pkg string) (codejen.File, error) { + pkg = strings.ToLower(pkg) + + buf := new(bytes.Buffer) + if err := tmpls.Lookup("core_status.tmpl").Execute(buf, tvars_status{ + PackageName: pkg, + }); err != nil { + return codejen.File{}, fmt.Errorf("failed executing core resource template: %w", err) + } + + return *codejen.NewFile(fmt.Sprintf("pkg/kinds/%s/%s_status_gen.go", pkg, pkg), buf.Bytes(), jenny), nil +} + +func getPackageName(val cue.Value) (string, error) { + name := val.LookupPath(cue.ParsePath("name")) + pkg, err := name.String() + if err != nil { + return "", fmt.Errorf("file doesn't have name field set: %s", err) + } + return pkg, nil +} + +func getVersion(val cue.Value) (string, error) { + val = val.LookupPath(cue.ParsePath("lineage.schemas[0].version")) + versionValues, err := val.List() + if err != nil { + return "", fmt.Errorf("missing version in schema: %s", err) + } + + version := make([]int64, 0) + for versionValues.Next() { + v, err := versionValues.Value().Int64() + if err != nil { + return "", fmt.Errorf("version should be a list of two elements: %s", err) + } + + version = append(version, v) + } + + return fmt.Sprintf("%d-%d", version[0], version[1]), nil +} diff --git a/pkg/codegen/jenny_tsveneerindex.go b/pkg/codegen/jenny_tsveneerindex.go index 875301ec81..96160bdfdd 100644 --- a/pkg/codegen/jenny_tsveneerindex.go +++ b/pkg/codegen/jenny_tsveneerindex.go @@ -18,6 +18,8 @@ import ( "github.com/grafana/thema/encoding/typescript" ) +var schPath = cue.MakePath(cue.Hid("_#schema", "github.com/grafana/thema")) + // TSVeneerIndexJenny generates an index.gen.ts file with references to all // generated TS types. Elements with the attribute @grafana(TSVeneer="type") are // exported from a handwritten file, rather than the raw generated types. diff --git a/pkg/codegen/tmpl.go b/pkg/codegen/tmpl.go index 0abfdd4511..d329e990f6 100644 --- a/pkg/codegen/tmpl.go +++ b/pkg/codegen/tmpl.go @@ -39,9 +39,16 @@ type ( Kinds []kindsys.Core } tvars_resource struct { - PackageName string - KindName string - Version string - SubresourceNames []string + PackageName string + KindName string + Version string + } + + tvars_metadata struct { + PackageName string + } + + tvars_status struct { + PackageName string } ) diff --git a/pkg/codegen/tmpl/core_metadata.tmpl b/pkg/codegen/tmpl/core_metadata.tmpl new file mode 100644 index 0000000000..11bf70f342 --- /dev/null +++ b/pkg/codegen/tmpl/core_metadata.tmpl @@ -0,0 +1,33 @@ +package {{ .PackageName }} + +import ( + "time" +) + +// Metadata defines model for Metadata. +type Metadata struct { + CreatedBy string `json:"createdBy"` + CreationTimestamp time.Time `json:"creationTimestamp"` + DeletionTimestamp *time.Time `json:"deletionTimestamp,omitempty"` + + // extraFields is reserved for any fields that are pulled from the API server metadata but do not have concrete fields in the CUE metadata + ExtraFields map[string]any `json:"extraFields"` + Finalizers []string `json:"finalizers"` + Labels map[string]string `json:"labels"` + ResourceVersion string `json:"resourceVersion"` + Uid string `json:"uid"` + UpdateTimestamp time.Time `json:"updateTimestamp"` + UpdatedBy string `json:"updatedBy"` +} + +// _kubeObjectMetadata is metadata found in a kubernetes object's metadata field. +// It is not exhaustive and only includes fields which may be relevant to a kind's implementation, +// As it is also intended to be generic enough to function with any API Server. +type KubeObjectMetadata struct { + CreationTimestamp time.Time `json:"creationTimestamp"` + DeletionTimestamp *time.Time `json:"deletionTimestamp,omitempty"` + Finalizers []string `json:"finalizers"` + Labels map[string]string `json:"labels"` + ResourceVersion string `json:"resourceVersion"` + Uid string `json:"uid"` +} diff --git a/pkg/codegen/tmpl/core_resource.tmpl b/pkg/codegen/tmpl/core_resource.tmpl index 8c4f6cb583..d929505e4a 100644 --- a/pkg/codegen/tmpl/core_resource.tmpl +++ b/pkg/codegen/tmpl/core_resource.tmpl @@ -28,6 +28,7 @@ func NewK8sResource(name string, s *Spec) K8sResource { // Resource is the wire representation of {{ .KindName }}. // It currently will soon be merged into the k8s flavor (TODO be better) type Resource struct { - {{- range .SubresourceNames }} - {{ . }} {{ . }} `json:"{{ . | ToLower }}"`{{end}} + Metadata Metadata `json:"metadata"` + Spec Spec `json:"spec"` + Status Status `json:"status"` } diff --git a/pkg/codegen/tmpl/core_status.tmpl b/pkg/codegen/tmpl/core_status.tmpl new file mode 100644 index 0000000000..da2ceb99c8 --- /dev/null +++ b/pkg/codegen/tmpl/core_status.tmpl @@ -0,0 +1,65 @@ +package {{ .PackageName }} + +// Defines values for OperatorStateState. +const ( + OperatorStateStateFailed OperatorStateState = "failed" + OperatorStateStateInProgress OperatorStateState = "in_progress" + OperatorStateStateSuccess OperatorStateState = "success" +) + +// Defines values for StatusOperatorStateState. +const ( + StatusOperatorStateStateFailed StatusOperatorStateState = "failed" + StatusOperatorStateStateInProgress StatusOperatorStateState = "in_progress" + StatusOperatorStateStateSuccess StatusOperatorStateState = "success" +) + +// OperatorState defines model for OperatorState. +type OperatorState struct { + // descriptiveState is an optional more descriptive state field which has no requirements on format + DescriptiveState *string `json:"descriptiveState,omitempty"` + + // details contains any extra information that is operator-specific + Details map[string]any `json:"details,omitempty"` + + // lastEvaluation is the ResourceVersion last evaluated + LastEvaluation string `json:"lastEvaluation"` + + // state describes the state of the lastEvaluation. + // It is limited to three possible states for machine evaluation. + State OperatorStateState `json:"state"` +} + +// OperatorStateState state describes the state of the lastEvaluation. +// It is limited to three possible states for machine evaluation. +type OperatorStateState string + +// Status defines model for Status. +type Status struct { + // additionalFields is reserved for future use + AdditionalFields map[string]any `json:"additionalFields,omitempty"` + + // operatorStates is a map of operator ID to operator state evaluations. + // Any operator which consumes this kind SHOULD add its state evaluation information to this field. + OperatorStates map[string]StatusOperatorState `json:"operatorStates,omitempty"` +} + +// StatusOperatorState defines model for status.#OperatorState. +type StatusOperatorState struct { + // descriptiveState is an optional more descriptive state field which has no requirements on format + DescriptiveState *string `json:"descriptiveState,omitempty"` + + // details contains any extra information that is operator-specific + Details map[string]any `json:"details,omitempty"` + + // lastEvaluation is the ResourceVersion last evaluated + LastEvaluation string `json:"lastEvaluation"` + + // state describes the state of the lastEvaluation. + // It is limited to three possible states for machine evaluation. + State StatusOperatorStateState `json:"state"` +} + +// StatusOperatorStateState state describes the state of the lastEvaluation. +// It is limited to three possible states for machine evaluation. +type StatusOperatorStateState string diff --git a/pkg/kinds/accesspolicy/accesspolicy_gen.go b/pkg/kinds/accesspolicy/accesspolicy_gen.go index 90e97fd931..a528ab8ea3 100644 --- a/pkg/kinds/accesspolicy/accesspolicy_gen.go +++ b/pkg/kinds/accesspolicy/accesspolicy_gen.go @@ -3,7 +3,7 @@ // Generated by: // kinds/gen.go // Using jennies: -// GoTypesJenny +// K8ResourcesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/pkg/kinds/accesspolicy/accesspolicy_metadata_gen.go b/pkg/kinds/accesspolicy/accesspolicy_metadata_gen.go index 46bcec2632..689f54c57d 100644 --- a/pkg/kinds/accesspolicy/accesspolicy_metadata_gen.go +++ b/pkg/kinds/accesspolicy/accesspolicy_metadata_gen.go @@ -3,7 +3,7 @@ // Generated by: // kinds/gen.go // Using jennies: -// GoResourceTypes +// K8ResourcesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/pkg/kinds/accesspolicy/accesspolicy_status_gen.go b/pkg/kinds/accesspolicy/accesspolicy_status_gen.go index 8f4e90abab..5101e41774 100644 --- a/pkg/kinds/accesspolicy/accesspolicy_status_gen.go +++ b/pkg/kinds/accesspolicy/accesspolicy_status_gen.go @@ -3,7 +3,7 @@ // Generated by: // kinds/gen.go // Using jennies: -// GoResourceTypes +// K8ResourcesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/pkg/kinds/dashboard/dashboard_gen.go b/pkg/kinds/dashboard/dashboard_gen.go index 183f56b8ff..28ac37b0b7 100644 --- a/pkg/kinds/dashboard/dashboard_gen.go +++ b/pkg/kinds/dashboard/dashboard_gen.go @@ -3,7 +3,7 @@ // Generated by: // kinds/gen.go // Using jennies: -// GoTypesJenny +// K8ResourcesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/pkg/kinds/dashboard/dashboard_metadata_gen.go b/pkg/kinds/dashboard/dashboard_metadata_gen.go index c628f6037d..67047c56cc 100644 --- a/pkg/kinds/dashboard/dashboard_metadata_gen.go +++ b/pkg/kinds/dashboard/dashboard_metadata_gen.go @@ -3,7 +3,7 @@ // Generated by: // kinds/gen.go // Using jennies: -// GoResourceTypes +// K8ResourcesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/pkg/kinds/dashboard/dashboard_status_gen.go b/pkg/kinds/dashboard/dashboard_status_gen.go index 49a6d55b91..82c114dff2 100644 --- a/pkg/kinds/dashboard/dashboard_status_gen.go +++ b/pkg/kinds/dashboard/dashboard_status_gen.go @@ -3,7 +3,7 @@ // Generated by: // kinds/gen.go // Using jennies: -// GoResourceTypes +// K8ResourcesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/pkg/kinds/librarypanel/librarypanel_gen.go b/pkg/kinds/librarypanel/librarypanel_gen.go index e13b6addf9..ea25be0ad4 100644 --- a/pkg/kinds/librarypanel/librarypanel_gen.go +++ b/pkg/kinds/librarypanel/librarypanel_gen.go @@ -3,7 +3,7 @@ // Generated by: // kinds/gen.go // Using jennies: -// GoTypesJenny +// K8ResourcesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/pkg/kinds/librarypanel/librarypanel_metadata_gen.go b/pkg/kinds/librarypanel/librarypanel_metadata_gen.go index 49a44f5710..e899f28b1f 100644 --- a/pkg/kinds/librarypanel/librarypanel_metadata_gen.go +++ b/pkg/kinds/librarypanel/librarypanel_metadata_gen.go @@ -3,7 +3,7 @@ // Generated by: // kinds/gen.go // Using jennies: -// GoResourceTypes +// K8ResourcesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/pkg/kinds/librarypanel/librarypanel_status_gen.go b/pkg/kinds/librarypanel/librarypanel_status_gen.go index 8b04861837..69072c08df 100644 --- a/pkg/kinds/librarypanel/librarypanel_status_gen.go +++ b/pkg/kinds/librarypanel/librarypanel_status_gen.go @@ -3,7 +3,7 @@ // Generated by: // kinds/gen.go // Using jennies: -// GoResourceTypes +// K8ResourcesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/pkg/kinds/preferences/preferences_gen.go b/pkg/kinds/preferences/preferences_gen.go index 9240e81efa..4f6861b821 100644 --- a/pkg/kinds/preferences/preferences_gen.go +++ b/pkg/kinds/preferences/preferences_gen.go @@ -3,7 +3,7 @@ // Generated by: // kinds/gen.go // Using jennies: -// GoTypesJenny +// K8ResourcesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/pkg/kinds/preferences/preferences_metadata_gen.go b/pkg/kinds/preferences/preferences_metadata_gen.go index bd33d5d53a..dacb200beb 100644 --- a/pkg/kinds/preferences/preferences_metadata_gen.go +++ b/pkg/kinds/preferences/preferences_metadata_gen.go @@ -3,7 +3,7 @@ // Generated by: // kinds/gen.go // Using jennies: -// GoResourceTypes +// K8ResourcesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/pkg/kinds/preferences/preferences_status_gen.go b/pkg/kinds/preferences/preferences_status_gen.go index e9a7553380..53fbb8a07c 100644 --- a/pkg/kinds/preferences/preferences_status_gen.go +++ b/pkg/kinds/preferences/preferences_status_gen.go @@ -3,7 +3,7 @@ // Generated by: // kinds/gen.go // Using jennies: -// GoResourceTypes +// K8ResourcesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/pkg/kinds/publicdashboard/publicdashboard_gen.go b/pkg/kinds/publicdashboard/publicdashboard_gen.go index c9e106313f..68239e6469 100644 --- a/pkg/kinds/publicdashboard/publicdashboard_gen.go +++ b/pkg/kinds/publicdashboard/publicdashboard_gen.go @@ -3,7 +3,7 @@ // Generated by: // kinds/gen.go // Using jennies: -// GoTypesJenny +// K8ResourcesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/pkg/kinds/publicdashboard/publicdashboard_metadata_gen.go b/pkg/kinds/publicdashboard/publicdashboard_metadata_gen.go index 0f0b0a878a..c5b84bf270 100644 --- a/pkg/kinds/publicdashboard/publicdashboard_metadata_gen.go +++ b/pkg/kinds/publicdashboard/publicdashboard_metadata_gen.go @@ -3,7 +3,7 @@ // Generated by: // kinds/gen.go // Using jennies: -// GoResourceTypes +// K8ResourcesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/pkg/kinds/publicdashboard/publicdashboard_status_gen.go b/pkg/kinds/publicdashboard/publicdashboard_status_gen.go index 100e343138..95de4433cf 100644 --- a/pkg/kinds/publicdashboard/publicdashboard_status_gen.go +++ b/pkg/kinds/publicdashboard/publicdashboard_status_gen.go @@ -3,7 +3,7 @@ // Generated by: // kinds/gen.go // Using jennies: -// GoResourceTypes +// K8ResourcesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/pkg/kinds/role/role_gen.go b/pkg/kinds/role/role_gen.go index 8b1bfb9947..c054e8c177 100644 --- a/pkg/kinds/role/role_gen.go +++ b/pkg/kinds/role/role_gen.go @@ -3,7 +3,7 @@ // Generated by: // kinds/gen.go // Using jennies: -// GoTypesJenny +// K8ResourcesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/pkg/kinds/role/role_metadata_gen.go b/pkg/kinds/role/role_metadata_gen.go index b64111b964..21bd45d336 100644 --- a/pkg/kinds/role/role_metadata_gen.go +++ b/pkg/kinds/role/role_metadata_gen.go @@ -3,7 +3,7 @@ // Generated by: // kinds/gen.go // Using jennies: -// GoResourceTypes +// K8ResourcesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/pkg/kinds/role/role_status_gen.go b/pkg/kinds/role/role_status_gen.go index f5088b2c2d..ff9f44bdc5 100644 --- a/pkg/kinds/role/role_status_gen.go +++ b/pkg/kinds/role/role_status_gen.go @@ -3,7 +3,7 @@ // Generated by: // kinds/gen.go // Using jennies: -// GoResourceTypes +// K8ResourcesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/pkg/kinds/rolebinding/rolebinding_gen.go b/pkg/kinds/rolebinding/rolebinding_gen.go index e12f62c1cd..216bd3a952 100644 --- a/pkg/kinds/rolebinding/rolebinding_gen.go +++ b/pkg/kinds/rolebinding/rolebinding_gen.go @@ -3,7 +3,7 @@ // Generated by: // kinds/gen.go // Using jennies: -// GoTypesJenny +// K8ResourcesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/pkg/kinds/rolebinding/rolebinding_metadata_gen.go b/pkg/kinds/rolebinding/rolebinding_metadata_gen.go index 841dba6762..2c2f4b2834 100644 --- a/pkg/kinds/rolebinding/rolebinding_metadata_gen.go +++ b/pkg/kinds/rolebinding/rolebinding_metadata_gen.go @@ -3,7 +3,7 @@ // Generated by: // kinds/gen.go // Using jennies: -// GoResourceTypes +// K8ResourcesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/pkg/kinds/rolebinding/rolebinding_status_gen.go b/pkg/kinds/rolebinding/rolebinding_status_gen.go index 1843070259..1b4552df63 100644 --- a/pkg/kinds/rolebinding/rolebinding_status_gen.go +++ b/pkg/kinds/rolebinding/rolebinding_status_gen.go @@ -3,7 +3,7 @@ // Generated by: // kinds/gen.go // Using jennies: -// GoResourceTypes +// K8ResourcesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/pkg/kinds/team/team_gen.go b/pkg/kinds/team/team_gen.go index c4be50f713..155f91d0a5 100644 --- a/pkg/kinds/team/team_gen.go +++ b/pkg/kinds/team/team_gen.go @@ -3,7 +3,7 @@ // Generated by: // kinds/gen.go // Using jennies: -// GoTypesJenny +// K8ResourcesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/pkg/kinds/team/team_metadata_gen.go b/pkg/kinds/team/team_metadata_gen.go index d4acb2f00d..2709fc4374 100644 --- a/pkg/kinds/team/team_metadata_gen.go +++ b/pkg/kinds/team/team_metadata_gen.go @@ -3,7 +3,7 @@ // Generated by: // kinds/gen.go // Using jennies: -// GoResourceTypes +// K8ResourcesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/pkg/kinds/team/team_status_gen.go b/pkg/kinds/team/team_status_gen.go index 5983d8f260..d9e9c6535f 100644 --- a/pkg/kinds/team/team_status_gen.go +++ b/pkg/kinds/team/team_status_gen.go @@ -3,7 +3,7 @@ // Generated by: // kinds/gen.go // Using jennies: -// GoResourceTypes +// K8ResourcesJenny // // Run 'make gen-cue' from repository root to regenerate. From d5adcf350a3801bdb93a931ab3983699af98fe55 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Mon, 26 Feb 2024 10:10:35 +0000 Subject: [PATCH 0158/1406] E2C: Add cloud landing page (#83316) * restructure and create cloud empty state * use new Grid prop and run i18n:extract --- .../admin/migrate-to-cloud/MigrateToCloud.tsx | 2 +- .../cloud/EmptyState/EmptyState.tsx | 34 +++++++++ .../cloud/EmptyState/InfoPane.tsx | 75 +++++++++++++++++++ .../cloud/EmptyState/MigrationTokenPane.tsx | 30 ++++++++ .../{ => onprem}/EmptyState/CallToAction.tsx | 0 .../{ => onprem}/EmptyState/EmptyState.tsx | 1 + .../{ => onprem}/EmptyState/InfoPaneLeft.tsx | 4 +- .../{ => onprem}/EmptyState/InfoPaneRight.tsx | 4 +- .../{EmptyState => shared}/InfoItem.tsx | 4 +- public/locales/en-US/grafana.json | 22 ++++++ public/locales/pseudo-LOCALE/grafana.json | 22 ++++++ 11 files changed, 190 insertions(+), 8 deletions(-) create mode 100644 public/app/features/admin/migrate-to-cloud/cloud/EmptyState/EmptyState.tsx create mode 100644 public/app/features/admin/migrate-to-cloud/cloud/EmptyState/InfoPane.tsx create mode 100644 public/app/features/admin/migrate-to-cloud/cloud/EmptyState/MigrationTokenPane.tsx rename public/app/features/admin/migrate-to-cloud/{ => onprem}/EmptyState/CallToAction.tsx (100%) rename public/app/features/admin/migrate-to-cloud/{ => onprem}/EmptyState/EmptyState.tsx (96%) rename public/app/features/admin/migrate-to-cloud/{ => onprem}/EmptyState/InfoPaneLeft.tsx (92%) rename public/app/features/admin/migrate-to-cloud/{ => onprem}/EmptyState/InfoPaneRight.tsx (91%) rename public/app/features/admin/migrate-to-cloud/{EmptyState => shared}/InfoItem.tsx (87%) diff --git a/public/app/features/admin/migrate-to-cloud/MigrateToCloud.tsx b/public/app/features/admin/migrate-to-cloud/MigrateToCloud.tsx index a30d26fd97..3545be33a2 100644 --- a/public/app/features/admin/migrate-to-cloud/MigrateToCloud.tsx +++ b/public/app/features/admin/migrate-to-cloud/MigrateToCloud.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { Page } from 'app/core/components/Page/Page'; -import { EmptyState } from './EmptyState/EmptyState'; +import { EmptyState } from './onprem/EmptyState/EmptyState'; export default function MigrateToCloud() { return ( diff --git a/public/app/features/admin/migrate-to-cloud/cloud/EmptyState/EmptyState.tsx b/public/app/features/admin/migrate-to-cloud/cloud/EmptyState/EmptyState.tsx new file mode 100644 index 0000000000..9ffcc83999 --- /dev/null +++ b/public/app/features/admin/migrate-to-cloud/cloud/EmptyState/EmptyState.tsx @@ -0,0 +1,34 @@ +import { css } from '@emotion/css'; +import React from 'react'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { Grid, useStyles2 } from '@grafana/ui'; + +import { InfoPane } from './InfoPane'; +import { MigrationTokenPane } from './MigrationTokenPane'; + +export const EmptyState = () => { + const styles = useStyles2(getStyles); + + return ( +
+ + + + +
+ ); +}; + +const getStyles = (theme: GrafanaTheme2) => ({ + container: css({ + maxWidth: theme.breakpoints.values.xl, + }), +}); diff --git a/public/app/features/admin/migrate-to-cloud/cloud/EmptyState/InfoPane.tsx b/public/app/features/admin/migrate-to-cloud/cloud/EmptyState/InfoPane.tsx new file mode 100644 index 0000000000..27b30134b4 --- /dev/null +++ b/public/app/features/admin/migrate-to-cloud/cloud/EmptyState/InfoPane.tsx @@ -0,0 +1,75 @@ +import { css } from '@emotion/css'; +import React from 'react'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { Box, Stack, TextLink, useStyles2 } from '@grafana/ui'; +import { t, Trans } from 'app/core/internationalization'; + +import { InfoItem } from '../../shared/InfoItem'; + +export const InfoPane = () => { + const styles = useStyles2(getStyles); + + return ( + + + + Some configuration from your self-managed Grafana instance can be automatically copied to this cloud stack. + + + + + + The migration process must be started from your self-managed Grafana instance. + +
    +
  1. + + Log in to your self-managed instance and navigate to Administration, General, Migrate to Grafana Cloud. + +
  2. +
  3. + + Select "Migrate this instance to Cloud". + +
  4. +
  5. + + You'll be prompted for a migration token. Generate one from this screen. + +
  6. +
  7. + + In your self-managed instance, select "Upload everything" to upload data sources and + dashboards to this cloud stack. + +
  8. +
  9. + + If some of your data sources will not work over the public internet, you’ll need to install Private Data + Source Connect in your self-managed environment. + +
  10. +
+
+
+ + {t('migrate-to-cloud.get-started.configure-pdc-link', 'Configure PDC for this stack')} + +
+ ); +}; + +const getStyles = (theme: GrafanaTheme2) => ({ + list: css({ + padding: 'revert', + }), +}); diff --git a/public/app/features/admin/migrate-to-cloud/cloud/EmptyState/MigrationTokenPane.tsx b/public/app/features/admin/migrate-to-cloud/cloud/EmptyState/MigrationTokenPane.tsx new file mode 100644 index 0000000000..192a54c923 --- /dev/null +++ b/public/app/features/admin/migrate-to-cloud/cloud/EmptyState/MigrationTokenPane.tsx @@ -0,0 +1,30 @@ +import React from 'react'; + +import { Box, Button, Text } from '@grafana/ui'; +import { t, Trans } from 'app/core/internationalization'; + +import { InfoItem } from '../../shared/InfoItem'; + +export const MigrationTokenPane = () => { + const onGenerateToken = () => { + console.log('TODO: generate token!'); + }; + const tokenStatus = 'TODO'; + + return ( + + + + Your self-managed Grafana instance will require a special authentication token to securely connect to this + cloud stack. + + + + Current status: {{ tokenStatus }} + + + + ); +}; diff --git a/public/app/features/admin/migrate-to-cloud/EmptyState/CallToAction.tsx b/public/app/features/admin/migrate-to-cloud/onprem/EmptyState/CallToAction.tsx similarity index 100% rename from public/app/features/admin/migrate-to-cloud/EmptyState/CallToAction.tsx rename to public/app/features/admin/migrate-to-cloud/onprem/EmptyState/CallToAction.tsx diff --git a/public/app/features/admin/migrate-to-cloud/EmptyState/EmptyState.tsx b/public/app/features/admin/migrate-to-cloud/onprem/EmptyState/EmptyState.tsx similarity index 96% rename from public/app/features/admin/migrate-to-cloud/EmptyState/EmptyState.tsx rename to public/app/features/admin/migrate-to-cloud/onprem/EmptyState/EmptyState.tsx index 09c01b4009..02183bbc95 100644 --- a/public/app/features/admin/migrate-to-cloud/EmptyState/EmptyState.tsx +++ b/public/app/features/admin/migrate-to-cloud/onprem/EmptyState/EmptyState.tsx @@ -16,6 +16,7 @@ export const EmptyState = () => { { return ( - + { return ( - + { return ( {title} - - {children} - + {children} {linkHref && ( {linkTitle ?? linkHref} diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 1e66599fe4..bfb1935321 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -701,11 +701,33 @@ "button": "Migrate this instance to Cloud", "header": "Let us manage your Grafana stack" }, + "get-started": { + "body": "The migration process must be started from your self-managed Grafana instance.", + "configure-pdc-link": "Configure PDC for this stack", + "link-title": "Learn more about Private Data Source Connect", + "step-1": "Log in to your self-managed instance and navigate to Administration, General, Migrate to Grafana Cloud.", + "step-2": "Select \"Migrate this instance to Cloud\".", + "step-3": "You'll be prompted for a migration token. Generate one from this screen.", + "step-4": "In your self-managed instance, select \"Upload everything\" to upload data sources and dashboards to this cloud stack.", + "step-5": "If some of your data sources will not work over the public internet, you’ll need to install Private Data Source Connect in your self-managed environment.", + "title": "How to get started" + }, "is-it-secure": { "body": "Grafana Labs is committed to maintaining the highest standards of data privacy and security. By implementing industry-standard security technologies and procedures, we help protect our customers' data from unauthorized access, use, or disclosure.", "link-title": "Grafana Labs Trust Center", "title": "Is it secure?" }, + "migrate-to-this-stack": { + "body": "Some configuration from your self-managed Grafana instance can be automatically copied to this cloud stack.", + "link-title": "View the full migration guide", + "title": "Migrate configuration to this stack" + }, + "migration-token": { + "body": "Your self-managed Grafana instance will require a special authentication token to securely connect to this cloud stack.", + "generate-button": "Generate a migration token", + "status": "Current status: {{tokenStatus}}", + "title": "Migration token" + }, "pdc": { "body": "Exposing your data sources to the internet can raise security concerns. Private data source connect (PDC) allows Grafana Cloud to access your existing data sources over a secure network tunnel.", "link-title": "Learn about PDC", diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index 6cd2c16ee4..2608eb4fef 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -701,11 +701,33 @@ "button": "Mįģřäŧę ŧĥįş įʼnşŧäʼnčę ŧő Cľőūđ", "header": "Ŀęŧ ūş mäʼnäģę yőūř Ğřäƒäʼnä şŧäčĸ" }, + "get-started": { + "body": "Ŧĥę mįģřäŧįőʼn přőčęşş mūşŧ þę şŧäřŧęđ ƒřőm yőūř şęľƒ-mäʼnäģęđ Ğřäƒäʼnä įʼnşŧäʼnčę.", + "configure-pdc-link": "Cőʼnƒįģūřę PĐC ƒőř ŧĥįş şŧäčĸ", + "link-title": "Ŀęäřʼn mőřę äþőūŧ Přįväŧę Đäŧä Ŝőūřčę Cőʼnʼnęčŧ", + "step-1": "Ŀőģ įʼn ŧő yőūř şęľƒ-mäʼnäģęđ įʼnşŧäʼnčę äʼnđ ʼnävįģäŧę ŧő Åđmįʼnįşŧřäŧįőʼn, Ğęʼnęřäľ, Mįģřäŧę ŧő Ğřäƒäʼnä Cľőūđ.", + "step-2": "Ŝęľęčŧ \"Mįģřäŧę ŧĥįş įʼnşŧäʼnčę ŧő Cľőūđ\".", + "step-3": "Ÿőū'ľľ þę přőmpŧęđ ƒőř ä mįģřäŧįőʼn ŧőĸęʼn. Ğęʼnęřäŧę őʼnę ƒřőm ŧĥįş şčřęęʼn.", + "step-4": "Ĩʼn yőūř şęľƒ-mäʼnäģęđ įʼnşŧäʼnčę, şęľęčŧ \"Ůpľőäđ ęvęřyŧĥįʼnģ\" ŧő ūpľőäđ đäŧä şőūřčęş äʼnđ đäşĥþőäřđş ŧő ŧĥįş čľőūđ şŧäčĸ.", + "step-5": "Ĩƒ şőmę őƒ yőūř đäŧä şőūřčęş ŵįľľ ʼnőŧ ŵőřĸ ővęř ŧĥę pūþľįč įʼnŧęřʼnęŧ, yőū’ľľ ʼnęęđ ŧő įʼnşŧäľľ Přįväŧę Đäŧä Ŝőūřčę Cőʼnʼnęčŧ įʼn yőūř şęľƒ-mäʼnäģęđ ęʼnvįřőʼnmęʼnŧ.", + "title": "Ħőŵ ŧő ģęŧ şŧäřŧęđ" + }, "is-it-secure": { "body": "Ğřäƒäʼnä Ŀäþş įş čőmmįŧŧęđ ŧő mäįʼnŧäįʼnįʼnģ ŧĥę ĥįģĥęşŧ şŧäʼnđäřđş őƒ đäŧä přįväčy äʼnđ şęčūřįŧy. ßy įmpľęmęʼnŧįʼnģ įʼnđūşŧřy-şŧäʼnđäřđ şęčūřįŧy ŧęčĥʼnőľőģįęş äʼnđ přőčęđūřęş, ŵę ĥęľp přőŧęčŧ őūř čūşŧőmęřş' đäŧä ƒřőm ūʼnäūŧĥőřįžęđ äččęşş, ūşę, őř đįşčľőşūřę.", "link-title": "Ğřäƒäʼnä Ŀäþş Ŧřūşŧ Cęʼnŧęř", "title": "Ĩş įŧ şęčūřę?" }, + "migrate-to-this-stack": { + "body": "Ŝőmę čőʼnƒįģūřäŧįőʼn ƒřőm yőūř şęľƒ-mäʼnäģęđ Ğřäƒäʼnä įʼnşŧäʼnčę čäʼn þę äūŧőmäŧįčäľľy čőpįęđ ŧő ŧĥįş čľőūđ şŧäčĸ.", + "link-title": "Vįęŵ ŧĥę ƒūľľ mįģřäŧįőʼn ģūįđę", + "title": "Mįģřäŧę čőʼnƒįģūřäŧįőʼn ŧő ŧĥįş şŧäčĸ" + }, + "migration-token": { + "body": "Ÿőūř şęľƒ-mäʼnäģęđ Ğřäƒäʼnä įʼnşŧäʼnčę ŵįľľ řęqūįřę ä şpęčįäľ äūŧĥęʼnŧįčäŧįőʼn ŧőĸęʼn ŧő şęčūřęľy čőʼnʼnęčŧ ŧő ŧĥįş čľőūđ şŧäčĸ.", + "generate-button": "Ğęʼnęřäŧę ä mįģřäŧįőʼn ŧőĸęʼn", + "status": "Cūřřęʼnŧ şŧäŧūş: {{tokenStatus}}", + "title": "Mįģřäŧįőʼn ŧőĸęʼn" + }, "pdc": { "body": "Ēχpőşįʼnģ yőūř đäŧä şőūřčęş ŧő ŧĥę įʼnŧęřʼnęŧ čäʼn řäįşę şęčūřįŧy čőʼnčęřʼnş. Přįväŧę đäŧä şőūřčę čőʼnʼnęčŧ (PĐC) äľľőŵş Ğřäƒäʼnä Cľőūđ ŧő äččęşş yőūř ęχįşŧįʼnģ đäŧä şőūřčęş ővęř ä şęčūřę ʼnęŧŵőřĸ ŧūʼnʼnęľ.", "link-title": "Ŀęäřʼn äþőūŧ PĐC", From ae00b4fb532fa34332682a6edeefefc48131b78d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 26 Feb 2024 12:24:33 +0200 Subject: [PATCH 0159/1406] Update dependency @grafana/aws-sdk to v0.3.2 (#83374) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 1500 +++----------------------------------------------- 2 files changed, 67 insertions(+), 1435 deletions(-) diff --git a/package.json b/package.json index fe345505fc..3428edf991 100644 --- a/package.json +++ b/package.json @@ -241,7 +241,7 @@ "@grafana-plugins/stackdriver": "workspace:*", "@grafana-plugins/tempo": "workspace:*", "@grafana-plugins/zipkin": "workspace:*", - "@grafana/aws-sdk": "0.3.1", + "@grafana/aws-sdk": "0.3.2", "@grafana/data": "workspace:*", "@grafana/e2e-selectors": "workspace:*", "@grafana/experimental": "1.7.10", diff --git a/yarn.lock b/yarn.lock index e96abdc275..38d8921047 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12,13 +12,6 @@ __metadata: languageName: node linkType: hard -"@adobe/css-tools@npm:^4.3.0": - version: 4.3.1 - resolution: "@adobe/css-tools@npm:4.3.1" - checksum: 10/039a42ffdd41ecf3abcaf09c9fef0ffd634ccbe81c04002fc989e74564eba99bb19169a8f48dadf6442aa2c5c9f0925a7b27ec5c36a1ed1a3515fe77d6930996 - languageName: node - linkType: hard - "@adobe/css-tools@npm:^4.3.2": version: 4.3.2 resolution: "@adobe/css-tools@npm:4.3.2" @@ -110,19 +103,7 @@ __metadata: languageName: node linkType: hard -"@babel/generator@npm:^7.12.11, @babel/generator@npm:^7.22.9, @babel/generator@npm:^7.23.0, @babel/generator@npm:^7.7.2": - version: 7.23.0 - resolution: "@babel/generator@npm:7.23.0" - dependencies: - "@babel/types": "npm:^7.23.0" - "@jridgewell/gen-mapping": "npm:^0.3.2" - "@jridgewell/trace-mapping": "npm:^0.3.17" - jsesc: "npm:^2.5.1" - checksum: 10/bd1598bd356756065d90ce26968dd464ac2b915c67623f6f071fb487da5f9eb454031a380e20e7c9a7ce5c4a49d23be6cb9efde404952b0b3f3c0c3a9b73d68a - languageName: node - linkType: hard - -"@babel/generator@npm:^7.23.6": +"@babel/generator@npm:^7.12.11, @babel/generator@npm:^7.22.9, @babel/generator@npm:^7.23.0, @babel/generator@npm:^7.23.6, @babel/generator@npm:^7.7.2": version: 7.23.6 resolution: "@babel/generator@npm:7.23.6" dependencies: @@ -184,20 +165,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-create-regexp-features-plugin@npm:^7.18.6, @babel/helper-create-regexp-features-plugin@npm:^7.22.5": - version: 7.22.9 - resolution: "@babel/helper-create-regexp-features-plugin@npm:7.22.9" - dependencies: - "@babel/helper-annotate-as-pure": "npm:^7.22.5" - regexpu-core: "npm:^5.3.1" - semver: "npm:^6.3.1" - peerDependencies: - "@babel/core": ^7.0.0 - checksum: 10/6f3475a7661bc34527201c07eeeec3077c8adab0ed74bff728dc479da6c74bb393b6121ddf590ef1671f3f508fab3c7792a5ada65672665d84db4556daebd210 - languageName: node - linkType: hard - -"@babel/helper-create-regexp-features-plugin@npm:^7.22.15": +"@babel/helper-create-regexp-features-plugin@npm:^7.18.6, @babel/helper-create-regexp-features-plugin@npm:^7.22.15, @babel/helper-create-regexp-features-plugin@npm:^7.22.5": version: 7.22.15 resolution: "@babel/helper-create-regexp-features-plugin@npm:7.22.15" dependencies: @@ -368,13 +336,6 @@ __metadata: languageName: node linkType: hard -"@babel/helper-string-parser@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/helper-string-parser@npm:7.22.5" - checksum: 10/7f275a7f1a9504da06afc33441e219796352a4a3d0288a961bc14d1e30e06833a71621b33c3e60ee3ac1ff3c502d55e392bcbc0665f6f9d2629809696fab7cdd - languageName: node - linkType: hard - "@babel/helper-string-parser@npm:^7.23.4": version: 7.23.4 resolution: "@babel/helper-string-parser@npm:7.23.4" @@ -438,15 +399,6 @@ __metadata: languageName: node linkType: hard -"@babel/parser@npm:^7.22.15": - version: 7.23.6 - resolution: "@babel/parser@npm:7.23.6" - bin: - parser: ./bin/babel-parser.js - checksum: 10/6be3a63d3c9d07b035b5a79c022327cb7e16cbd530140ecb731f19a650c794c315a72c699a22413ebeafaff14aa8f53435111898d59e01a393d741b85629fa7d - languageName: node - linkType: hard - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:^7.22.15, @babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:^7.23.3": version: 7.23.3 resolution: "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:7.23.3" @@ -1250,17 +1202,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-react-display-name@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/plugin-transform-react-display-name@npm:7.22.5" - dependencies: - "@babel/helper-plugin-utils": "npm:^7.22.5" - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 10/a12bfd1e4e93055efca3ace3c34722571bda59d9740dca364d225d9c6e3ca874f134694d21715c42cc63d79efd46db9665bd4a022998767f9245f1e29d5d204d - languageName: node - linkType: hard - "@babel/plugin-transform-react-display-name@npm:^7.23.3": version: 7.23.3 resolution: "@babel/plugin-transform-react-display-name@npm:7.23.3" @@ -1298,18 +1239,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-react-pure-annotations@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/plugin-transform-react-pure-annotations@npm:7.22.5" - dependencies: - "@babel/helper-annotate-as-pure": "npm:^7.22.5" - "@babel/helper-plugin-utils": "npm:^7.22.5" - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 10/092021c4f404e267002099ec20b3f12dd730cb90b0d83c5feed3dc00dbe43b9c42c795a18e7c6c7d7bddea20c7dd56221b146aec81b37f2e7eb5137331c61120 - languageName: node - linkType: hard - "@babel/plugin-transform-react-pure-annotations@npm:^7.23.3": version: 7.23.3 resolution: "@babel/plugin-transform-react-pure-annotations@npm:7.23.3" @@ -1678,7 +1607,7 @@ __metadata: languageName: node linkType: hard -"@babel/preset-react@npm:7.23.3": +"@babel/preset-react@npm:7.23.3, @babel/preset-react@npm:^7.22.5": version: 7.23.3 resolution: "@babel/preset-react@npm:7.23.3" dependencies: @@ -1694,22 +1623,6 @@ __metadata: languageName: node linkType: hard -"@babel/preset-react@npm:^7.22.5": - version: 7.22.15 - resolution: "@babel/preset-react@npm:7.22.15" - dependencies: - "@babel/helper-plugin-utils": "npm:^7.22.5" - "@babel/helper-validator-option": "npm:^7.22.15" - "@babel/plugin-transform-react-display-name": "npm:^7.22.5" - "@babel/plugin-transform-react-jsx": "npm:^7.22.15" - "@babel/plugin-transform-react-jsx-development": "npm:^7.22.5" - "@babel/plugin-transform-react-pure-annotations": "npm:^7.22.5" - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 10/f9296e45346c3b6ab8296952edde5f1774cc9fdbdbefbc76047278fc3e889d3e15740f038ce017aca562d89f32fcbb6c11783d464fc6ae3066433178fa58513c - languageName: node - linkType: hard - "@babel/preset-typescript@npm:^7.13.0": version: 7.23.2 resolution: "@babel/preset-typescript@npm:7.23.2" @@ -1757,7 +1670,7 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:7.23.9, @babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.11.1, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.14.0, @babel/runtime@npm:^7.14.5, @babel/runtime@npm:^7.15.4, @babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.18.0, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.20.0, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.7.2, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2": +"@babel/runtime@npm:7.23.9, @babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.11.1, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.14.0, @babel/runtime@npm:^7.14.5, @babel/runtime@npm:^7.15.4, @babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.18.0, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.19.4, @babel/runtime@npm:^7.20.0, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.7.2, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2": version: 7.23.9 resolution: "@babel/runtime@npm:7.23.9" dependencies: @@ -1766,27 +1679,7 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.19.4": - version: 7.23.8 - resolution: "@babel/runtime@npm:7.23.8" - dependencies: - regenerator-runtime: "npm:^0.14.0" - checksum: 10/ec8f1967a36164da6cac868533ffdff97badd76d23d7d820cc84f0818864accef972f22f9c6a710185db1e3810e353fc18c3da721e5bb3ee8bc61bdbabce03ff - languageName: node - linkType: hard - -"@babel/template@npm:^7.22.15, @babel/template@npm:^7.3.3": - version: 7.22.15 - resolution: "@babel/template@npm:7.22.15" - dependencies: - "@babel/code-frame": "npm:^7.22.13" - "@babel/parser": "npm:^7.22.15" - "@babel/types": "npm:^7.22.15" - checksum: 10/21e768e4eed4d1da2ce5d30aa51db0f4d6d8700bc1821fec6292587df7bba2fe1a96451230de8c64b989740731888ebf1141138bfffb14cacccf4d05c66ad93f - languageName: node - linkType: hard - -"@babel/template@npm:^7.22.5, @babel/template@npm:^7.23.9": +"@babel/template@npm:^7.22.15, @babel/template@npm:^7.22.5, @babel/template@npm:^7.23.9, @babel/template@npm:^7.3.3": version: 7.23.9 resolution: "@babel/template@npm:7.23.9" dependencies: @@ -1815,29 +1708,7 @@ __metadata: languageName: node linkType: hard -"@babel/types@npm:^7.0.0, @babel/types@npm:^7.2.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.22.15, @babel/types@npm:^7.22.19, @babel/types@npm:^7.22.5, @babel/types@npm:^7.23.0, @babel/types@npm:^7.3.0, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3": - version: 7.23.0 - resolution: "@babel/types@npm:7.23.0" - dependencies: - "@babel/helper-string-parser": "npm:^7.22.5" - "@babel/helper-validator-identifier": "npm:^7.22.20" - to-fast-properties: "npm:^2.0.0" - checksum: 10/ca5b896a26c91c5672254725c4c892a35567d2122afc47bd5331d1611a7f9230c19fc9ef591a5a6f80bf0d80737e104a9ac205c96447c74bee01d4319db58001 - languageName: node - linkType: hard - -"@babel/types@npm:^7.23.6": - version: 7.23.6 - resolution: "@babel/types@npm:7.23.6" - dependencies: - "@babel/helper-string-parser": "npm:^7.23.4" - "@babel/helper-validator-identifier": "npm:^7.22.20" - to-fast-properties: "npm:^2.0.0" - checksum: 10/07e70bb94d30b0231396b5e9a7726e6d9227a0a62e0a6830c0bd3232f33b024092e3d5a7d1b096a65bbf2bb43a9ab4c721bf618e115bfbb87b454fa060f88cbf - languageName: node - linkType: hard - -"@babel/types@npm:^7.23.9": +"@babel/types@npm:^7.0.0, @babel/types@npm:^7.2.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.22.15, @babel/types@npm:^7.22.19, @babel/types@npm:^7.22.5, @babel/types@npm:^7.23.0, @babel/types@npm:^7.23.6, @babel/types@npm:^7.23.9, @babel/types@npm:^7.3.0, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3": version: 7.23.9 resolution: "@babel/types@npm:7.23.9" dependencies: @@ -3650,13 +3521,13 @@ __metadata: languageName: node linkType: hard -"@grafana/aws-sdk@npm:0.3.1": - version: 0.3.1 - resolution: "@grafana/aws-sdk@npm:0.3.1" +"@grafana/aws-sdk@npm:0.3.2": + version: 0.3.2 + resolution: "@grafana/aws-sdk@npm:0.3.2" dependencies: "@grafana/async-query-data": "npm:0.1.4" "@grafana/experimental": "npm:1.7.0" - checksum: 10/89b42fa6351b78ce9760fb07ebfad37d45aa3327858ad4e88acc1699ccc9d6541ba0232ba8331d6b0a6b61a1d6b134e19ae5e91b2e90353a97958291c15bc9ef + checksum: 10/e0023948df9691c66873efd1d8f2ccde829aa7b7c520e01c7458dfe21dfdd46fd5638be006e97a34c39247b19fa9b61759cfb05e0d9b54fc9ab5f368cbae241b languageName: node linkType: hard @@ -3888,7 +3759,7 @@ __metadata: languageName: node linkType: hard -"@grafana/faro-web-sdk@npm:1.3.8": +"@grafana/faro-web-sdk@npm:1.3.8, @grafana/faro-web-sdk@npm:^1.3.6": version: 1.3.8 resolution: "@grafana/faro-web-sdk@npm:1.3.8" dependencies: @@ -3899,17 +3770,6 @@ __metadata: languageName: node linkType: hard -"@grafana/faro-web-sdk@npm:^1.3.6": - version: 1.3.6 - resolution: "@grafana/faro-web-sdk@npm:1.3.6" - dependencies: - "@grafana/faro-core": "npm:^1.3.6" - ua-parser-js: "npm:^1.0.32" - web-vitals: "npm:^3.1.1" - checksum: 10/08a80e5b0b527a4955e803984d53f53fac6dd090b17a219853222090445e15601971f4b469648c59d0075106f6e8f4ddcabae1b0d3010f80a6d900d825656998 - languageName: node - linkType: hard - "@grafana/flamegraph@workspace:*, @grafana/flamegraph@workspace:packages/grafana-flamegraph": version: 0.0.0-use.local resolution: "@grafana/flamegraph@workspace:packages/grafana-flamegraph" @@ -4603,20 +4463,6 @@ __metadata: languageName: node linkType: hard -"@jest/console@npm:^29.6.4": - version: 29.6.4 - resolution: "@jest/console@npm:29.6.4" - dependencies: - "@jest/types": "npm:^29.6.3" - "@types/node": "npm:*" - chalk: "npm:^4.0.0" - jest-message-util: "npm:^29.6.3" - jest-util: "npm:^29.6.3" - slash: "npm:^3.0.0" - checksum: 10/c482f87a43bb2c48da79c53ad4157542a67c6c90972a8615b852f889da73fd4bcd2a2b85d41b1c867e3df7e7f55e83b4ac91f226511d5645bc20f6498f114c0d - languageName: node - linkType: hard - "@jest/console@npm:^29.7.0": version: 29.7.0 resolution: "@jest/console@npm:29.7.0" @@ -4672,47 +4518,6 @@ __metadata: languageName: node linkType: hard -"@jest/core@npm:^29.6.4": - version: 29.6.4 - resolution: "@jest/core@npm:29.6.4" - dependencies: - "@jest/console": "npm:^29.6.4" - "@jest/reporters": "npm:^29.6.4" - "@jest/test-result": "npm:^29.6.4" - "@jest/transform": "npm:^29.6.4" - "@jest/types": "npm:^29.6.3" - "@types/node": "npm:*" - ansi-escapes: "npm:^4.2.1" - chalk: "npm:^4.0.0" - ci-info: "npm:^3.2.0" - exit: "npm:^0.1.2" - graceful-fs: "npm:^4.2.9" - jest-changed-files: "npm:^29.6.3" - jest-config: "npm:^29.6.4" - jest-haste-map: "npm:^29.6.4" - jest-message-util: "npm:^29.6.3" - jest-regex-util: "npm:^29.6.3" - jest-resolve: "npm:^29.6.4" - jest-resolve-dependencies: "npm:^29.6.4" - jest-runner: "npm:^29.6.4" - jest-runtime: "npm:^29.6.4" - jest-snapshot: "npm:^29.6.4" - jest-util: "npm:^29.6.3" - jest-validate: "npm:^29.6.3" - jest-watcher: "npm:^29.6.4" - micromatch: "npm:^4.0.4" - pretty-format: "npm:^29.6.3" - slash: "npm:^3.0.0" - strip-ansi: "npm:^6.0.0" - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true - checksum: 10/a1e47946d715a1735f89fdf9fcf6e99278cef691ac893db4c13e283f753a5c62cec47d68d4f3d7fe823aac0fc603257740f630591e8029846e83f451456a4716 - languageName: node - linkType: hard - "@jest/environment@npm:^29.3.1, @jest/environment@npm:^29.7.0": version: 29.7.0 resolution: "@jest/environment@npm:29.7.0" @@ -4725,27 +4530,6 @@ __metadata: languageName: node linkType: hard -"@jest/environment@npm:^29.6.4": - version: 29.6.4 - resolution: "@jest/environment@npm:29.6.4" - dependencies: - "@jest/fake-timers": "npm:^29.6.4" - "@jest/types": "npm:^29.6.3" - "@types/node": "npm:*" - jest-mock: "npm:^29.6.3" - checksum: 10/8c31ed7f3992f450b994684a2a92d2a023e5e182773e13ea7c627edd25598279d5d1a958bea970e3868bc4d45e20881ddcf9bf9af9425f87d38959141f42693d - languageName: node - linkType: hard - -"@jest/expect-utils@npm:^29.6.4": - version: 29.6.4 - resolution: "@jest/expect-utils@npm:29.6.4" - dependencies: - jest-get-type: "npm:^29.6.3" - checksum: 10/47f17bb3262175600130c698fdaaa680ec7f4612bfdb3f4f9f03e0252c341f31135ae854246f5548453634deef949533aa35b3638cfa776ce5596fd4bd8f1c6e - languageName: node - linkType: hard - "@jest/expect-utils@npm:^29.7.0": version: 29.7.0 resolution: "@jest/expect-utils@npm:29.7.0" @@ -4755,16 +4539,6 @@ __metadata: languageName: node linkType: hard -"@jest/expect@npm:^29.6.4": - version: 29.6.4 - resolution: "@jest/expect@npm:29.6.4" - dependencies: - expect: "npm:^29.6.4" - jest-snapshot: "npm:^29.6.4" - checksum: 10/e6564697562885e2b96294740a507e82e76b9f0af83f6101e2979a8650ec14055452e0ea68c154f92ef93ef83b258f1a45aa39df4e0f472e5ff5c4468de4170f - languageName: node - linkType: hard - "@jest/expect@npm:^29.7.0": version: 29.7.0 resolution: "@jest/expect@npm:29.7.0" @@ -4789,32 +4563,6 @@ __metadata: languageName: node linkType: hard -"@jest/fake-timers@npm:^29.6.4": - version: 29.6.4 - resolution: "@jest/fake-timers@npm:29.6.4" - dependencies: - "@jest/types": "npm:^29.6.3" - "@sinonjs/fake-timers": "npm:^10.0.2" - "@types/node": "npm:*" - jest-message-util: "npm:^29.6.3" - jest-mock: "npm:^29.6.3" - jest-util: "npm:^29.6.3" - checksum: 10/87df3df08111adb51a425edcbd6a454ec5bf1fec582946d04e68616d669fadad64fefee3ddb05b3c91984e47e718917fac907015095c55c2acd58e4f863b0523 - languageName: node - linkType: hard - -"@jest/globals@npm:^29.6.4": - version: 29.6.4 - resolution: "@jest/globals@npm:29.6.4" - dependencies: - "@jest/environment": "npm:^29.6.4" - "@jest/expect": "npm:^29.6.4" - "@jest/types": "npm:^29.6.3" - jest-mock: "npm:^29.6.3" - checksum: 10/a41b18871a248151264668a38b13cb305f03db112bfd89ec44e858af0e79066e0b03d6b68c8baf1ec6c578be6fdb87519389c83438608b91471d17a5724858e0 - languageName: node - linkType: hard - "@jest/globals@npm:^29.7.0": version: 29.7.0 resolution: "@jest/globals@npm:29.7.0" @@ -4827,43 +4575,6 @@ __metadata: languageName: node linkType: hard -"@jest/reporters@npm:^29.6.4": - version: 29.6.4 - resolution: "@jest/reporters@npm:29.6.4" - dependencies: - "@bcoe/v8-coverage": "npm:^0.2.3" - "@jest/console": "npm:^29.6.4" - "@jest/test-result": "npm:^29.6.4" - "@jest/transform": "npm:^29.6.4" - "@jest/types": "npm:^29.6.3" - "@jridgewell/trace-mapping": "npm:^0.3.18" - "@types/node": "npm:*" - chalk: "npm:^4.0.0" - collect-v8-coverage: "npm:^1.0.0" - exit: "npm:^0.1.2" - glob: "npm:^7.1.3" - graceful-fs: "npm:^4.2.9" - istanbul-lib-coverage: "npm:^3.0.0" - istanbul-lib-instrument: "npm:^6.0.0" - istanbul-lib-report: "npm:^3.0.0" - istanbul-lib-source-maps: "npm:^4.0.0" - istanbul-reports: "npm:^3.1.3" - jest-message-util: "npm:^29.6.3" - jest-util: "npm:^29.6.3" - jest-worker: "npm:^29.6.4" - slash: "npm:^3.0.0" - string-length: "npm:^4.0.1" - strip-ansi: "npm:^6.0.0" - v8-to-istanbul: "npm:^9.0.1" - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true - checksum: 10/ba6f6d9f621ef80cc8f2fae001f2c9b9e9186f2b570d386b2cfaf298a391332cf65121f72b700e037c9fb7813af4230f821f5f2e0c71c2bd3d301bfa2aa9b935 - languageName: node - linkType: hard - "@jest/reporters@npm:^29.7.0": version: 29.7.0 resolution: "@jest/reporters@npm:29.7.0" @@ -4921,18 +4632,6 @@ __metadata: languageName: node linkType: hard -"@jest/test-result@npm:^29.6.4": - version: 29.6.4 - resolution: "@jest/test-result@npm:29.6.4" - dependencies: - "@jest/console": "npm:^29.6.4" - "@jest/types": "npm:^29.6.3" - "@types/istanbul-lib-coverage": "npm:^2.0.0" - collect-v8-coverage: "npm:^1.0.0" - checksum: 10/d0dd2beaa4e9f3e7cebec44de1ab09a1020e77e4742c018f91bf281d49b824875a6d8f86408fdde195f53dd1ba8b7943b2843a9f002c5b0286f8933c21e65527 - languageName: node - linkType: hard - "@jest/test-result@npm:^29.7.0": version: 29.7.0 resolution: "@jest/test-result@npm:29.7.0" @@ -4945,18 +4644,6 @@ __metadata: languageName: node linkType: hard -"@jest/test-sequencer@npm:^29.6.4": - version: 29.6.4 - resolution: "@jest/test-sequencer@npm:29.6.4" - dependencies: - "@jest/test-result": "npm:^29.6.4" - graceful-fs: "npm:^4.2.9" - jest-haste-map: "npm:^29.6.4" - slash: "npm:^3.0.0" - checksum: 10/f0f09dc5c1cc190ae8944531072b598746c725c81d2810f47d4459a2d3113b3658469ff4e59cd028b221e4f2d015e0b968940e9ae08d800b2e8911590fbc184e - languageName: node - linkType: hard - "@jest/test-sequencer@npm:^29.7.0": version: 29.7.0 resolution: "@jest/test-sequencer@npm:29.7.0" @@ -4992,29 +4679,6 @@ __metadata: languageName: node linkType: hard -"@jest/transform@npm:^29.6.4": - version: 29.6.4 - resolution: "@jest/transform@npm:29.6.4" - dependencies: - "@babel/core": "npm:^7.11.6" - "@jest/types": "npm:^29.6.3" - "@jridgewell/trace-mapping": "npm:^0.3.18" - babel-plugin-istanbul: "npm:^6.1.1" - chalk: "npm:^4.0.0" - convert-source-map: "npm:^2.0.0" - fast-json-stable-stringify: "npm:^2.1.0" - graceful-fs: "npm:^4.2.9" - jest-haste-map: "npm:^29.6.4" - jest-regex-util: "npm:^29.6.3" - jest-util: "npm:^29.6.3" - micromatch: "npm:^4.0.4" - pirates: "npm:^4.0.4" - slash: "npm:^3.0.0" - write-file-atomic: "npm:^4.0.2" - checksum: 10/cba6b5a59de89b4446c0c0dc6ca08ea52fb1308caeaa72c70a232a1b81d178cad1a2fcce4e25db95d7623c03c34a244bdfe6769cc6ac71b8c3fcd2838bab583e - languageName: node - linkType: hard - "@jest/types@npm:^26.6.2": version: 26.6.2 resolution: "@jest/types@npm:26.6.2" @@ -5063,13 +4727,6 @@ __metadata: languageName: node linkType: hard -"@jridgewell/resolve-uri@npm:3.1.0": - version: 3.1.0 - resolution: "@jridgewell/resolve-uri@npm:3.1.0" - checksum: 10/320ceb37af56953757b28e5b90c34556157676d41e3d0a3ff88769274d62373582bb0f0276a4f2d29c3f4fdd55b82b8be5731f52d391ad2ecae9b321ee1c742d - languageName: node - linkType: hard - "@jridgewell/resolve-uri@npm:^3.0.3, @jridgewell/resolve-uri@npm:^3.1.0": version: 3.1.1 resolution: "@jridgewell/resolve-uri@npm:3.1.1" @@ -5084,16 +4741,6 @@ __metadata: 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": "npm:^0.3.0" - "@jridgewell/trace-mapping": "npm:^0.3.9" - checksum: 10/1aaa42075bac32a551708025da0c07b11c11fb05ccd10fb70df2cb0db88773338ab0f33f175d9865379cb855bb3b1cda478367747a1087309fda40a7b9214bfa - languageName: node - linkType: hard - "@jridgewell/source-map@npm:^0.3.3": version: 0.3.5 resolution: "@jridgewell/source-map@npm:0.3.5" @@ -5104,13 +4751,6 @@ __metadata: languageName: node linkType: hard -"@jridgewell/sourcemap-codec@npm:1.4.14": - version: 1.4.14 - resolution: "@jridgewell/sourcemap-codec@npm:1.4.14" - checksum: 10/26e768fae6045481a983e48aa23d8fcd23af5da70ebd74b0649000e815e7fbb01ea2bc088c9176b3fffeb9bec02184e58f46125ef3320b30eaa1f4094cfefa38 - languageName: node - linkType: hard - "@jridgewell/sourcemap-codec@npm:^1.4.10, @jridgewell/sourcemap-codec@npm:^1.4.14, @jridgewell/sourcemap-codec@npm:^1.4.15": version: 1.4.15 resolution: "@jridgewell/sourcemap-codec@npm:1.4.15" @@ -5128,27 +4768,7 @@ __metadata: languageName: node linkType: hard -"@jridgewell/trace-mapping@npm:^0.3.12, @jridgewell/trace-mapping@npm:^0.3.17, @jridgewell/trace-mapping@npm:^0.3.18, @jridgewell/trace-mapping@npm:^0.3.9": - version: 0.3.18 - resolution: "@jridgewell/trace-mapping@npm:0.3.18" - dependencies: - "@jridgewell/resolve-uri": "npm:3.1.0" - "@jridgewell/sourcemap-codec": "npm:1.4.14" - checksum: 10/f4fabdddf82398a797bcdbb51c574cd69b383db041a6cae1a6a91478681d6aab340c01af655cfd8c6e01cde97f63436a1445f08297cdd33587621cf05ffa0d55 - languageName: node - linkType: hard - -"@jridgewell/trace-mapping@npm:^0.3.20": - version: 0.3.21 - resolution: "@jridgewell/trace-mapping@npm:0.3.21" - dependencies: - "@jridgewell/resolve-uri": "npm:^3.1.0" - "@jridgewell/sourcemap-codec": "npm:^1.4.14" - checksum: 10/925dda0620887e5a24f11b5a3a106f4e8b1a66155b49be6ceee61432174df33a17c243d8a89b2cd79ccebd281d817878759236a2fc42c47325ae9f73dfbfb90d - languageName: node - linkType: hard - -"@jridgewell/trace-mapping@npm:^0.3.21": +"@jridgewell/trace-mapping@npm:^0.3.12, @jridgewell/trace-mapping@npm:^0.3.17, @jridgewell/trace-mapping@npm:^0.3.18, @jridgewell/trace-mapping@npm:^0.3.20, @jridgewell/trace-mapping@npm:^0.3.21, @jridgewell/trace-mapping@npm:^0.3.9": version: 0.3.22 resolution: "@jridgewell/trace-mapping@npm:0.3.22" dependencies: @@ -8759,13 +8379,6 @@ __metadata: languageName: node linkType: hard -"@swc/core-darwin-arm64@npm:1.3.90": - version: 1.3.90 - resolution: "@swc/core-darwin-arm64@npm:1.3.90" - conditions: os=darwin & cpu=arm64 - languageName: node - linkType: hard - "@swc/core-darwin-arm64@npm:1.4.0": version: 1.4.0 resolution: "@swc/core-darwin-arm64@npm:1.4.0" @@ -8780,13 +8393,6 @@ __metadata: languageName: node linkType: hard -"@swc/core-darwin-x64@npm:1.3.90": - version: 1.3.90 - resolution: "@swc/core-darwin-x64@npm:1.3.90" - conditions: os=darwin & cpu=x64 - languageName: node - linkType: hard - "@swc/core-darwin-x64@npm:1.4.0": version: 1.4.0 resolution: "@swc/core-darwin-x64@npm:1.4.0" @@ -8801,13 +8407,6 @@ __metadata: languageName: node linkType: hard -"@swc/core-linux-arm-gnueabihf@npm:1.3.90": - version: 1.3.90 - resolution: "@swc/core-linux-arm-gnueabihf@npm:1.3.90" - conditions: os=linux & cpu=arm - languageName: node - linkType: hard - "@swc/core-linux-arm-gnueabihf@npm:1.4.0": version: 1.4.0 resolution: "@swc/core-linux-arm-gnueabihf@npm:1.4.0" @@ -8822,13 +8421,6 @@ __metadata: languageName: node linkType: hard -"@swc/core-linux-arm64-gnu@npm:1.3.90": - version: 1.3.90 - resolution: "@swc/core-linux-arm64-gnu@npm:1.3.90" - conditions: os=linux & cpu=arm64 & libc=glibc - languageName: node - linkType: hard - "@swc/core-linux-arm64-gnu@npm:1.4.0": version: 1.4.0 resolution: "@swc/core-linux-arm64-gnu@npm:1.4.0" @@ -8843,13 +8435,6 @@ __metadata: languageName: node linkType: hard -"@swc/core-linux-arm64-musl@npm:1.3.90": - version: 1.3.90 - resolution: "@swc/core-linux-arm64-musl@npm:1.3.90" - conditions: os=linux & cpu=arm64 & libc=musl - languageName: node - linkType: hard - "@swc/core-linux-arm64-musl@npm:1.4.0": version: 1.4.0 resolution: "@swc/core-linux-arm64-musl@npm:1.4.0" @@ -8864,13 +8449,6 @@ __metadata: languageName: node linkType: hard -"@swc/core-linux-x64-gnu@npm:1.3.90": - version: 1.3.90 - resolution: "@swc/core-linux-x64-gnu@npm:1.3.90" - conditions: os=linux & cpu=x64 & libc=glibc - languageName: node - linkType: hard - "@swc/core-linux-x64-gnu@npm:1.4.0": version: 1.4.0 resolution: "@swc/core-linux-x64-gnu@npm:1.4.0" @@ -8885,13 +8463,6 @@ __metadata: languageName: node linkType: hard -"@swc/core-linux-x64-musl@npm:1.3.90": - version: 1.3.90 - resolution: "@swc/core-linux-x64-musl@npm:1.3.90" - conditions: os=linux & cpu=x64 & libc=musl - languageName: node - linkType: hard - "@swc/core-linux-x64-musl@npm:1.4.0": version: 1.4.0 resolution: "@swc/core-linux-x64-musl@npm:1.4.0" @@ -8906,13 +8477,6 @@ __metadata: languageName: node linkType: hard -"@swc/core-win32-arm64-msvc@npm:1.3.90": - version: 1.3.90 - resolution: "@swc/core-win32-arm64-msvc@npm:1.3.90" - conditions: os=win32 & cpu=arm64 - languageName: node - linkType: hard - "@swc/core-win32-arm64-msvc@npm:1.4.0": version: 1.4.0 resolution: "@swc/core-win32-arm64-msvc@npm:1.4.0" @@ -8927,13 +8491,6 @@ __metadata: languageName: node linkType: hard -"@swc/core-win32-ia32-msvc@npm:1.3.90": - version: 1.3.90 - resolution: "@swc/core-win32-ia32-msvc@npm:1.3.90" - conditions: os=win32 & cpu=ia32 - languageName: node - linkType: hard - "@swc/core-win32-ia32-msvc@npm:1.4.0": version: 1.4.0 resolution: "@swc/core-win32-ia32-msvc@npm:1.4.0" @@ -8948,13 +8505,6 @@ __metadata: languageName: node linkType: hard -"@swc/core-win32-x64-msvc@npm:1.3.90": - version: 1.3.90 - resolution: "@swc/core-win32-x64-msvc@npm:1.3.90" - conditions: os=win32 & cpu=x64 - languageName: node - linkType: hard - "@swc/core-win32-x64-msvc@npm:1.4.0": version: 1.4.0 resolution: "@swc/core-win32-x64-msvc@npm:1.4.0" @@ -9015,7 +8565,7 @@ __metadata: languageName: node linkType: hard -"@swc/core@npm:1.4.1": +"@swc/core@npm:1.4.1, @swc/core@npm:^1.3.49": version: 1.4.1 resolution: "@swc/core@npm:1.4.1" dependencies: @@ -9061,52 +8611,6 @@ __metadata: languageName: node linkType: hard -"@swc/core@npm:^1.3.49": - version: 1.3.90 - resolution: "@swc/core@npm:1.3.90" - dependencies: - "@swc/core-darwin-arm64": "npm:1.3.90" - "@swc/core-darwin-x64": "npm:1.3.90" - "@swc/core-linux-arm-gnueabihf": "npm:1.3.90" - "@swc/core-linux-arm64-gnu": "npm:1.3.90" - "@swc/core-linux-arm64-musl": "npm:1.3.90" - "@swc/core-linux-x64-gnu": "npm:1.3.90" - "@swc/core-linux-x64-musl": "npm:1.3.90" - "@swc/core-win32-arm64-msvc": "npm:1.3.90" - "@swc/core-win32-ia32-msvc": "npm:1.3.90" - "@swc/core-win32-x64-msvc": "npm:1.3.90" - "@swc/counter": "npm:^0.1.1" - "@swc/types": "npm:^0.1.5" - peerDependencies: - "@swc/helpers": ^0.5.0 - dependenciesMeta: - "@swc/core-darwin-arm64": - optional: true - "@swc/core-darwin-x64": - optional: true - "@swc/core-linux-arm-gnueabihf": - optional: true - "@swc/core-linux-arm64-gnu": - optional: true - "@swc/core-linux-arm64-musl": - optional: true - "@swc/core-linux-x64-gnu": - optional: true - "@swc/core-linux-x64-musl": - optional: true - "@swc/core-win32-arm64-msvc": - optional: true - "@swc/core-win32-ia32-msvc": - optional: true - "@swc/core-win32-x64-msvc": - optional: true - peerDependenciesMeta: - "@swc/helpers": - optional: true - checksum: 10/214af37af77b968203d495745a86db985734527f4696243bda5fb9ce868830d70e7a2cdbb268da2ee994d9fcedded25073d7b709fa09b75e96f9ba7d13a63da0 - languageName: node - linkType: hard - "@swc/counter@npm:^0.1.1, @swc/counter@npm:^0.1.2, @swc/counter@npm:^0.1.3": version: 0.1.3 resolution: "@swc/counter@npm:0.1.3" @@ -9114,7 +8618,7 @@ __metadata: languageName: node linkType: hard -"@swc/helpers@npm:0.5.6": +"@swc/helpers@npm:0.5.6, @swc/helpers@npm:^0.5.0": version: 0.5.6 resolution: "@swc/helpers@npm:0.5.6" dependencies: @@ -9123,15 +8627,6 @@ __metadata: languageName: node linkType: hard -"@swc/helpers@npm:^0.5.0": - version: 0.5.1 - resolution: "@swc/helpers@npm:0.5.1" - dependencies: - tslib: "npm:^2.4.0" - checksum: 10/4954c4d2dd97bf965e863a10ffa44c3fdaf7653f2fa9ef1a6cf7ffffd67f3f832216588f9751afd75fdeaea60c4688c75c01e2405119c448f1a109c9a7958c54 - languageName: node - linkType: hard - "@swc/types@npm:^0.1.5": version: 0.1.5 resolution: "@swc/types@npm:0.1.5" @@ -9155,7 +8650,7 @@ __metadata: languageName: node linkType: hard -"@testing-library/jest-dom@npm:6.4.2": +"@testing-library/jest-dom@npm:6.4.2, @testing-library/jest-dom@npm:^6.1.2": version: 6.4.2 resolution: "@testing-library/jest-dom@npm:6.4.2" dependencies: @@ -9188,36 +8683,6 @@ __metadata: languageName: node linkType: hard -"@testing-library/jest-dom@npm:^6.1.2": - version: 6.1.2 - resolution: "@testing-library/jest-dom@npm:6.1.2" - dependencies: - "@adobe/css-tools": "npm:^4.3.0" - "@babel/runtime": "npm:^7.9.2" - aria-query: "npm:^5.0.0" - chalk: "npm:^3.0.0" - css.escape: "npm:^1.5.1" - dom-accessibility-api: "npm:^0.5.6" - lodash: "npm:^4.17.15" - redent: "npm:^3.0.0" - peerDependencies: - "@jest/globals": ">= 28" - "@types/jest": ">= 28" - jest: ">= 28" - vitest: ">= 0.32" - peerDependenciesMeta: - "@jest/globals": - optional: true - "@types/jest": - optional: true - jest: - optional: true - vitest: - optional: true - checksum: 10/36e27f9011dd60de8936d3edb2a81753ef7a370480019e571c6e85b935b5fa2aba47244104badf6d6b636fd170fdee5c0fdd92fb3630957d4a6f11d302b57398 - languageName: node - linkType: hard - "@testing-library/react-hooks@npm:^8.0.1": version: 8.0.1 resolution: "@testing-library/react-hooks@npm:8.0.1" @@ -10018,7 +9483,7 @@ __metadata: languageName: node linkType: hard -"@types/hoist-non-react-statics@npm:3.3.5, @types/hoist-non-react-statics@npm:^3.3.0": +"@types/hoist-non-react-statics@npm:3.3.5, @types/hoist-non-react-statics@npm:^3.3.0, @types/hoist-non-react-statics@npm:^3.3.1": version: 3.3.5 resolution: "@types/hoist-non-react-statics@npm:3.3.5" dependencies: @@ -10028,16 +9493,6 @@ __metadata: languageName: node linkType: hard -"@types/hoist-non-react-statics@npm:^3.3.1": - version: 3.3.4 - resolution: "@types/hoist-non-react-statics@npm:3.3.4" - dependencies: - "@types/react": "npm:*" - hoist-non-react-statics: "npm:^3.3.0" - checksum: 10/dee430941a9ea16b7f665ecafa9b134066a49d13ae497fc051cf5d41b3aead394ab1a8179c3c98c9a3584f80aed16fab82dd7979c7dcddfbb5f74a132575d362 - languageName: node - linkType: hard - "@types/html-minifier-terser@npm:^6.0.0": version: 6.0.0 resolution: "@types/html-minifier-terser@npm:6.0.0" @@ -10093,13 +9548,13 @@ __metadata: languageName: node linkType: hard -"@types/jest@npm:*, @types/jest@npm:^29.5.4": - version: 29.5.4 - resolution: "@types/jest@npm:29.5.4" +"@types/jest@npm:*, @types/jest@npm:29.5.12, @types/jest@npm:^29.5.4": + version: 29.5.12 + resolution: "@types/jest@npm:29.5.12" dependencies: expect: "npm:^29.0.0" pretty-format: "npm:^29.0.0" - checksum: 10/c56081b958c06f4f3a30f7beabf4e94e70db96a4b41b8a73549fea7f9bf0a8c124ab3998ea4e6d040d1b8c95cfbe0b8d4a607da4bdea03c9e116f92c147df193 + checksum: 10/312e8dcf92cdd5a5847d6426f0940829bca6fe6b5a917248f3d7f7ef5d85c9ce78ef05e47d2bbabc40d41a930e0e36db2d443d2610a9e3db9062da2d5c904211 languageName: node linkType: hard @@ -10113,16 +9568,6 @@ __metadata: languageName: node linkType: hard -"@types/jest@npm:29.5.12": - version: 29.5.12 - resolution: "@types/jest@npm:29.5.12" - dependencies: - expect: "npm:^29.0.0" - pretty-format: "npm:^29.0.0" - checksum: 10/312e8dcf92cdd5a5847d6426f0940829bca6fe6b5a917248f3d7f7ef5d85c9ce78ef05e47d2bbabc40d41a930e0e36db2d443d2610a9e3db9062da2d5c904211 - languageName: node - linkType: hard - "@types/jquery@npm:3.5.29": version: 3.5.29 resolution: "@types/jquery@npm:3.5.29" @@ -10164,14 +9609,7 @@ __metadata: 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.9 - resolution: "@types/json-schema@npm:7.0.9" - checksum: 10/7ceb41e396240aa69ae15c02ffbb6548ea2bb2f845a7378c711c7c908a9a8438a0330f3135f1ccb6e82e334b9e2ec5b94fb57a1435f2b15362d38e9d5109e5ea - languageName: node - linkType: hard - -"@types/json-schema@npm:^7.0.12": +"@types/json-schema@npm:*, @types/json-schema@npm:^7.0.12, @types/json-schema@npm:^7.0.8, @types/json-schema@npm:^7.0.9": version: 7.0.15 resolution: "@types/json-schema@npm:7.0.15" checksum: 10/1a3c3e06236e4c4aab89499c428d585527ce50c24fe8259e8b3926d3df4cfbbbcf306cfc73ddfb66cbafc973116efd15967020b0f738f63e09e64c7d260519e7 @@ -10329,12 +9767,12 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:*, @types/node@npm:>=13.7.0": - version: 20.8.10 - resolution: "@types/node@npm:20.8.10" +"@types/node@npm:*, @types/node@npm:20.11.19, @types/node@npm:>=13.7.0, @types/node@npm:^20.11.16": + version: 20.11.19 + resolution: "@types/node@npm:20.11.19" dependencies: undici-types: "npm:~5.26.4" - checksum: 10/8930039077c8ad74de74c724909412bea8110c3f8892bcef8dda3e9629073bed65632ee755f94b252bcdae8ca71cf83e89a4a440a105e2b1b7c9797b43483049 + checksum: 10/c7f4705d6c84aa21679ad180c33c13ca9567f650e66e14bcee77c7c43d14619c7cd3b4d7b2458947143030b7b1930180efa6d12d999b45366abff9fed7a17472 languageName: node linkType: hard @@ -10345,15 +9783,6 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:20.11.19": - version: 20.11.19 - resolution: "@types/node@npm:20.11.19" - dependencies: - undici-types: "npm:~5.26.4" - checksum: 10/c7f4705d6c84aa21679ad180c33c13ca9567f650e66e14bcee77c7c43d14619c7cd3b4d7b2458947143030b7b1930180efa6d12d999b45366abff9fed7a17472 - languageName: node - linkType: hard - "@types/node@npm:^14.14.31": version: 14.18.36 resolution: "@types/node@npm:14.18.36" @@ -10368,15 +9797,6 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:^20.11.16": - version: 20.11.18 - resolution: "@types/node@npm:20.11.18" - dependencies: - undici-types: "npm:~5.26.4" - checksum: 10/eeaa55032e6702867e96d7b6f98df1d60af09d37ab72f2b905b349ec7e458dfb9c4d9cfc562962f5a51b156a968eea773d8025688f88b735944c81e3ac0e3b7f - languageName: node - linkType: hard - "@types/normalize-package-data@npm:^2.4.0": version: 2.4.1 resolution: "@types/normalize-package-data@npm:2.4.1" @@ -10493,16 +9913,7 @@ __metadata: languageName: node linkType: hard -"@types/react-dom@npm:*, @types/react-dom@npm:^18.0.0": - version: 18.2.7 - resolution: "@types/react-dom@npm:18.2.7" - dependencies: - "@types/react": "npm:*" - checksum: 10/9b70ef66cbe2d2898ea37eb79ee3697e0e4ad3d950e769a601f79be94097d43b8ef45b98a0b29528203c7d731c81666f637b2b7032deeced99214b4bc0662614 - languageName: node - linkType: hard - -"@types/react-dom@npm:18.2.19": +"@types/react-dom@npm:*, @types/react-dom@npm:18.2.19, @types/react-dom@npm:^18.0.0": version: 18.2.19 resolution: "@types/react-dom@npm:18.2.19" dependencies: @@ -10589,7 +10000,7 @@ __metadata: languageName: node linkType: hard -"@types/react-transition-group@npm:4.4.10": +"@types/react-transition-group@npm:4.4.10, @types/react-transition-group@npm:^4.4.0": version: 4.4.10 resolution: "@types/react-transition-group@npm:4.4.10" dependencies: @@ -10598,15 +10009,6 @@ __metadata: languageName: node linkType: hard -"@types/react-transition-group@npm:^4.4.0": - version: 4.4.6 - resolution: "@types/react-transition-group@npm:4.4.6" - dependencies: - "@types/react": "npm:*" - checksum: 10/eb4a14df7ad283be56d44c4bd4351136bd50dfedf6958299fbbc571d6871fad17a373b5b9a6d44adac27154d1f2059225a26c4fee79053349a4d52eb89277787 - languageName: node - linkType: hard - "@types/react-virtualized-auto-sizer@npm:1.0.4": version: 1.0.4 resolution: "@types/react-virtualized-auto-sizer@npm:1.0.4" @@ -10696,17 +10098,10 @@ __metadata: languageName: node linkType: hard -"@types/semver@npm:7.5.7": - version: 7.5.7 - resolution: "@types/semver@npm:7.5.7" - checksum: 10/535d88ec577fe59e38211881f79a1e2ba391e9e1516f8fff74e7196a5ba54315bace9c67a4616c334c830c89027d70a9f473a4ceb634526086a9da39180f2f9a - languageName: node - linkType: hard - -"@types/semver@npm:^7.3.12, @types/semver@npm:^7.3.4, @types/semver@npm:^7.5.0": - version: 7.5.6 - resolution: "@types/semver@npm:7.5.6" - checksum: 10/e77282b17f74354e17e771c0035cccb54b94cc53d0433fa7e9ba9d23fd5d7edcd14b6c8b7327d58bbd89e83b1c5eda71dfe408e06b929007e2b89586e9b63459 +"@types/semver@npm:7.5.7, @types/semver@npm:^7.3.12, @types/semver@npm:^7.3.4, @types/semver@npm:^7.5.0": + version: 7.5.7 + resolution: "@types/semver@npm:7.5.7" + checksum: 10/535d88ec577fe59e38211881f79a1e2ba391e9e1516f8fff74e7196a5ba54315bace9c67a4616c334c830c89027d70a9f473a4ceb634526086a9da39180f2f9a languageName: node linkType: hard @@ -11966,15 +11361,6 @@ __metadata: languageName: node linkType: hard -"acorn@npm:^8.5.0": - version: 8.10.0 - resolution: "acorn@npm:8.10.0" - bin: - acorn: bin/acorn - checksum: 10/522310c20fdc3c271caed3caf0f06c51d61cb42267279566edd1d58e83dbc12eebdafaab666a0f0be1b7ad04af9c6bc2a6f478690a9e6391c3c8b165ada917dd - languageName: node - linkType: hard - "add-dom-event-listener@npm:^1.1.0": version: 1.1.0 resolution: "add-dom-event-listener@npm:1.1.0" @@ -12222,17 +11608,7 @@ __metadata: languageName: node linkType: hard -"anymatch@npm:^3.0.3, anymatch@npm:~3.1.2": - version: 3.1.2 - resolution: "anymatch@npm:3.1.2" - dependencies: - normalize-path: "npm:^3.0.0" - picomatch: "npm:^2.0.4" - checksum: 10/985163db2292fac9e5a1e072bf99f1b5baccf196e4de25a0b0b81865ebddeb3b3eb4480734ef0a2ac8c002845396b91aa89121f5b84f93981a4658164a9ec6e9 - languageName: node - linkType: hard - -"anymatch@npm:^3.1.3": +"anymatch@npm:^3.0.3, anymatch@npm:^3.1.3, anymatch@npm:~3.1.2": version: 3.1.3 resolution: "anymatch@npm:3.1.3" dependencies: @@ -12658,20 +12034,13 @@ __metadata: languageName: node linkType: hard -"axe-core@npm:=4.7.0": +"axe-core@npm:=4.7.0, axe-core@npm:^4.2.0": version: 4.7.0 resolution: "axe-core@npm:4.7.0" checksum: 10/615c0f7722c3c9fcf353dbd70b00e2ceae234d4c17cbc839dd85c01d16797c4e4da45f8d27c6118e9e6b033fb06efd196106e13651a1b2f3a10e0f11c7b2f660 languageName: node linkType: hard -"axe-core@npm:^4.2.0": - version: 4.6.3 - resolution: "axe-core@npm:4.6.3" - checksum: 10/280f6a7067129875380f733ae84093ce29c4b8cfe36e1a8ff46bd5d2bcd57d093f11b00223ddf5fef98ca147e0e6568ddd0ada9415cf8ae15d379224bf3cbb51 - languageName: node - linkType: hard - "axios@npm:^1.6.0": version: 1.6.7 resolution: "axios@npm:1.6.7" @@ -12718,23 +12087,6 @@ __metadata: languageName: node linkType: hard -"babel-jest@npm:^29.6.4": - version: 29.6.4 - resolution: "babel-jest@npm:29.6.4" - dependencies: - "@jest/transform": "npm:^29.6.4" - "@types/babel__core": "npm:^7.1.14" - babel-plugin-istanbul: "npm:^6.1.1" - babel-preset-jest: "npm:^29.6.3" - chalk: "npm:^4.0.0" - graceful-fs: "npm:^4.2.9" - slash: "npm:^3.0.0" - peerDependencies: - "@babel/core": ^7.8.0 - checksum: 10/1e26438368719336d3cb6144b68155f4837154b38d917180b9d0a2344e17dacb59b1213e593005fa7f63041052ad0e38cd1fb1de1c6b54f7d353f617a2ad20cf - languageName: node - linkType: hard - "babel-loader@npm:9.1.3, babel-loader@npm:^9.0.0": version: 9.1.3 resolution: "babel-loader@npm:9.1.3" @@ -13251,21 +12603,7 @@ __metadata: languageName: node linkType: hard -"browserslist@npm:^4.0.0, browserslist@npm:^4.14.5, browserslist@npm:^4.21.4, browserslist@npm:^4.22.2": - version: 4.22.2 - resolution: "browserslist@npm:4.22.2" - dependencies: - caniuse-lite: "npm:^1.0.30001565" - electron-to-chromium: "npm:^1.4.601" - node-releases: "npm:^2.0.14" - update-browserslist-db: "npm:^1.0.13" - bin: - browserslist: cli.js - checksum: 10/e3590793db7f66ad3a50817e7b7f195ce61e029bd7187200244db664bfbe0ac832f784e4f6b9c958aef8ea4abe001ae7880b7522682df521f4bc0a5b67660b5e - languageName: node - linkType: hard - -"browserslist@npm:^4.21.10": +"browserslist@npm:^4.0.0, browserslist@npm:^4.14.5, browserslist@npm:^4.21.10, browserslist@npm:^4.21.4, browserslist@npm:^4.22.2": version: 4.22.3 resolution: "browserslist@npm:4.22.3" dependencies: @@ -13573,13 +12911,6 @@ __metadata: languageName: node linkType: hard -"caniuse-lite@npm:^1.0.30001565": - version: 1.0.30001579 - resolution: "caniuse-lite@npm:1.0.30001579" - checksum: 10/2cd0c02e5d66b09888743ad2b624dbde697ace5c76b55bfd6065ea033f6abea8ac3f5d3c9299c042f91b396e2141b49bc61f5e17086dc9ba3a866cc6790134c0 - languageName: node - linkType: hard - "canvas-hypertxt@npm:^1.0.3": version: 1.0.3 resolution: "canvas-hypertxt@npm:1.0.3" @@ -14562,7 +13893,7 @@ __metadata: languageName: node linkType: hard -"core-js@npm:3.36.0": +"core-js@npm:3.36.0, core-js@npm:^3.6.0, core-js@npm:^3.8.3": version: 3.36.0 resolution: "core-js@npm:3.36.0" checksum: 10/896326c6391c1607dc645293c214cd31c6c535d4a77a88b15fc29e787199f9b06dc15986ddfbc798335bf7a7afd1e92152c94aa5a974790a7f97a98121774302 @@ -14576,13 +13907,6 @@ __metadata: languageName: node linkType: hard -"core-js@npm:^3.6.0, core-js@npm:^3.8.3": - version: 3.35.1 - resolution: "core-js@npm:3.35.1" - checksum: 10/5d31f22eb05cf66bd1a2088a04b7106faa5d0b91c1ffa5d72c5203e4974c31bd7e11969297f540a806c00c74c23991eaad5639592df8b5dbe4412fff3c075cd5 - languageName: node - linkType: hard - "core-util-is@npm:1.0.2": version: 1.0.2 resolution: "core-util-is@npm:1.0.2" @@ -14816,7 +14140,7 @@ __metadata: languageName: node linkType: hard -"css-loader@npm:6.10.0": +"css-loader@npm:6.10.0, css-loader@npm:^6.7.1": version: 6.10.0 resolution: "css-loader@npm:6.10.0" dependencies: @@ -14840,24 +14164,6 @@ __metadata: languageName: node linkType: hard -"css-loader@npm:^6.7.1": - version: 6.9.1 - resolution: "css-loader@npm:6.9.1" - dependencies: - icss-utils: "npm:^5.1.0" - postcss: "npm:^8.4.33" - postcss-modules-extract-imports: "npm:^3.0.0" - postcss-modules-local-by-default: "npm:^4.0.4" - postcss-modules-scope: "npm:^3.1.1" - postcss-modules-values: "npm:^4.0.0" - postcss-value-parser: "npm:^4.2.0" - semver: "npm:^7.5.4" - peerDependencies: - webpack: ^5.0.0 - checksum: 10/6f897406188ed7f6db03daab0602ed86df1e967b48a048ab72d0ee223e59ab9e13c5235481b12deb79e12aadf0be43bc3bdee71e1dc1e875e4bcd91c05b464af - languageName: node - linkType: hard - "css-minimizer-webpack-plugin@npm:6.0.0": version: 6.0.0 resolution: "css-minimizer-webpack-plugin@npm:6.0.0" @@ -15794,20 +15100,13 @@ __metadata: languageName: node linkType: hard -"deepmerge@npm:^4.0": +"deepmerge@npm:^4.0, deepmerge@npm:^4.2.2": version: 4.3.1 resolution: "deepmerge@npm:4.3.1" checksum: 10/058d9e1b0ff1a154468bf3837aea436abcfea1ba1d165ddaaf48ca93765fdd01a30d33c36173da8fbbed951dd0a267602bc782fe288b0fc4b7e1e7091afc4529 languageName: node linkType: hard -"deepmerge@npm:^4.2.2": - version: 4.2.2 - resolution: "deepmerge@npm:4.2.2" - checksum: 10/0e58ed14f530d08f9b996cfc3a41b0801691620235bc5e1883260e3ed1c1b4a1dfb59f865770e45d5dfb1d7ee108c4fc10c2f85e822989d4123490ea90be2545 - languageName: node - linkType: hard - "default-browser-id@npm:3.0.0": version: 3.0.0 resolution: "default-browser-id@npm:3.0.0" @@ -16135,7 +15434,7 @@ __metadata: languageName: node linkType: hard -"dom-accessibility-api@npm:^0.5.6, dom-accessibility-api@npm:^0.5.9": +"dom-accessibility-api@npm:^0.5.9": version: 0.5.10 resolution: "dom-accessibility-api@npm:0.5.10" checksum: 10/3ce183680c598392f89ec13fd04f495f95890c09d3da45909123ff549a10621ca21eee0258f929e4ed16a2cc73255d649174402b5fb7cd790983aa33b5a6fa3f @@ -16379,13 +15678,6 @@ __metadata: languageName: node linkType: hard -"electron-to-chromium@npm:^1.4.601": - version: 1.4.625 - resolution: "electron-to-chromium@npm:1.4.625" - checksum: 10/610a4eaabf6a064d8f6d4dfa25c55a3940f09a3b25edc8a271821d1b270bb28c4c9f19225d81bfc59deaa12c1f8f0144f3b4510631c6b6b47e0b6216737e216a - languageName: node - linkType: hard - "electron-to-chromium@npm:^1.4.648": version: 1.4.648 resolution: "electron-to-chromium@npm:1.4.648" @@ -17724,19 +17016,6 @@ __metadata: languageName: node linkType: hard -"expect@npm:^29.6.4": - version: 29.6.4 - resolution: "expect@npm:29.6.4" - dependencies: - "@jest/expect-utils": "npm:^29.6.4" - jest-get-type: "npm:^29.6.3" - jest-matcher-utils: "npm:^29.6.4" - jest-message-util: "npm:^29.6.3" - jest-util: "npm:^29.6.3" - checksum: 10/1e9224ce01de2bcd861b5a2b9409cc316c4f298beaa2c4ffb8a907a593e15ddff905506676f2b1f20d31fb1c0919a4527310b37b6d93f2ba4c4f77bf9881a90e - languageName: node - linkType: hard - "exponential-backoff@npm:^3.1.1": version: 3.1.1 resolution: "exponential-backoff@npm:3.1.1" @@ -17875,13 +17154,6 @@ __metadata: languageName: node linkType: hard -"fast-fifo@npm:^1.0.0": - version: 1.1.0 - resolution: "fast-fifo@npm:1.1.0" - checksum: 10/895f4c9873a4d5059dfa244aa0dde2b22ee563fd673d85b638869715f92244f9d6469bc0873bcb40554d28c51cbc7590045718462cfda1da503b1c6985815209 - languageName: node - linkType: hard - "fast-fifo@npm:^1.1.0": version: 1.3.2 resolution: "fast-fifo@npm:1.3.2" @@ -17889,33 +17161,7 @@ __metadata: languageName: node linkType: hard -"fast-glob@npm:^3.0.3": - version: 3.3.1 - resolution: "fast-glob@npm:3.3.1" - dependencies: - "@nodelib/fs.stat": "npm:^2.0.2" - "@nodelib/fs.walk": "npm:^1.2.3" - glob-parent: "npm:^5.1.2" - merge2: "npm:^1.3.0" - micromatch: "npm:^4.0.4" - checksum: 10/51bcd15472879dfe51d4b01c5b70bbc7652724d39cdd082ba11276dbd7d84db0f6b33757e1938af8b2768a4bf485d9be0c89153beae24ee8331d6dcc7550379f - languageName: node - linkType: hard - -"fast-glob@npm:^3.2.11, fast-glob@npm:^3.2.9": - version: 3.3.0 - resolution: "fast-glob@npm:3.3.0" - dependencies: - "@nodelib/fs.stat": "npm:^2.0.2" - "@nodelib/fs.walk": "npm:^1.2.3" - glob-parent: "npm:^5.1.2" - merge2: "npm:^1.3.0" - micromatch: "npm:^4.0.4" - checksum: 10/4cd74914f13eab48dd1a0d16051aa102c13d30ea8a79c991563ea3111a37ff6d888518964291d52d723e7ad2a946149ce9f13d27ad9a07a1e4e1aefb4717ed29 - languageName: node - linkType: hard - -"fast-glob@npm:^3.3.2": +"fast-glob@npm:^3.0.3, fast-glob@npm:^3.2.11, fast-glob@npm:^3.2.9, fast-glob@npm:^3.3.2": version: 3.3.2 resolution: "fast-glob@npm:3.3.2" dependencies: @@ -17998,7 +17244,7 @@ __metadata: languageName: node linkType: hard -"fastq@npm:^1.13.0": +"fastq@npm:^1.13.0, fastq@npm:^1.6.0": version: 1.17.1 resolution: "fastq@npm:1.17.1" dependencies: @@ -18007,15 +17253,6 @@ __metadata: languageName: node linkType: hard -"fastq@npm:^1.6.0": - version: 1.13.0 - resolution: "fastq@npm:1.13.0" - dependencies: - reusify: "npm:^1.0.4" - checksum: 10/0902cb9b81accf34e5542612c8a1df6c6ea47674f85bcc9cdc38795a28b53e4a096f751cfcf4fb25d2ea42fee5447499ba6cf5af5d0209297e1d1fd4dd551bb6 - languageName: node - linkType: hard - "fault@npm:^1.0.0": version: 1.0.4 resolution: "fault@npm:1.0.4" @@ -19183,20 +18420,13 @@ __metadata: languageName: node linkType: hard -"graceful-fs@npm:4.2.11, graceful-fs@npm:^4.2.10, graceful-fs@npm:^4.2.11, graceful-fs@npm:^4.2.8": +"graceful-fs@npm:4.2.11, graceful-fs@npm:^4.1.11, graceful-fs@npm:^4.1.15, graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.10, graceful-fs@npm:^4.2.11, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.8, graceful-fs@npm:^4.2.9": version: 4.2.11 resolution: "graceful-fs@npm:4.2.11" checksum: 10/bf152d0ed1dc159239db1ba1f74fdbc40cb02f626770dcd5815c427ce0688c2635a06ed69af364396da4636d0408fcf7d4afdf7881724c3307e46aff30ca49e2 languageName: node linkType: hard -"graceful-fs@npm:^4.1.11, graceful-fs@npm:^4.1.15, graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.9": - version: 4.2.10 - resolution: "graceful-fs@npm:4.2.10" - checksum: 10/0c83c52b62c68a944dcfb9d66b0f9f10f7d6e3d081e8067b9bfdc9e5f3a8896584d576036f82915773189eec1eba599397fc620e75c03c0610fb3d67c6713c1a - languageName: node - linkType: hard - "grafana@workspace:.": version: 0.0.0-use.local resolution: "grafana@workspace:." @@ -19220,7 +18450,7 @@ __metadata: "@grafana-plugins/stackdriver": "workspace:*" "@grafana-plugins/tempo": "workspace:*" "@grafana-plugins/zipkin": "workspace:*" - "@grafana/aws-sdk": "npm:0.3.1" + "@grafana/aws-sdk": "npm:0.3.2" "@grafana/data": "workspace:*" "@grafana/e2e-selectors": "workspace:*" "@grafana/eslint-config": "npm:7.0.0" @@ -20425,7 +19655,7 @@ __metadata: languageName: node linkType: hard -"import-local@npm:3.1.0": +"import-local@npm:3.1.0, import-local@npm:^3.0.2": version: 3.1.0 resolution: "import-local@npm:3.1.0" dependencies: @@ -20437,18 +19667,6 @@ __metadata: languageName: node linkType: hard -"import-local@npm:^3.0.2": - version: 3.0.3 - resolution: "import-local@npm:3.0.3" - dependencies: - pkg-dir: "npm:^4.2.0" - resolve-cwd: "npm:^3.0.0" - bin: - import-local-fixture: fixtures/cli.js - checksum: 10/38ae57d35e7fd5f63b55895050c798d4dd590e4e2337e9ffa882fb3ea7a7716f3162c7300e382e0a733ca5d07b389fadff652c00fa7b072d5cb6ea34ca06b179 - languageName: node - linkType: hard - "imurmurhash@npm:^0.1.4": version: 0.1.4 resolution: "imurmurhash@npm:0.1.4" @@ -21181,20 +20399,7 @@ __metadata: languageName: node linkType: hard -"is-typed-array@npm:^1.1.10, is-typed-array@npm:^1.1.3, is-typed-array@npm:^1.1.9": - version: 1.1.10 - resolution: "is-typed-array@npm:1.1.10" - dependencies: - available-typed-arrays: "npm:^1.0.5" - call-bind: "npm:^1.0.2" - for-each: "npm:^0.3.3" - gopd: "npm:^1.0.1" - has-tostringtag: "npm:^1.0.0" - checksum: 10/2392b2473bbc994f5c30d6848e32bab3cab6c80b795aaec3020baf5419ff7df38fc11b3a043eb56d50f842394c578dbb204a7a29398099f895cf111c5b27f327 - languageName: node - linkType: hard - -"is-typed-array@npm:^1.1.12": +"is-typed-array@npm:^1.1.10, is-typed-array@npm:^1.1.12, is-typed-array@npm:^1.1.3, is-typed-array@npm:^1.1.9": version: 1.1.12 resolution: "is-typed-array@npm:1.1.12" dependencies: @@ -21446,17 +20651,6 @@ __metadata: languageName: node linkType: hard -"jest-changed-files@npm:^29.6.3": - version: 29.6.3 - resolution: "jest-changed-files@npm:29.6.3" - dependencies: - execa: "npm:^5.0.0" - jest-util: "npm:^29.6.3" - p-limit: "npm:^3.1.0" - checksum: 10/ffcd0add3351c54ee0eb3ed88e2352ecf46d9118daed99ab0b73cb25502848a19db3ff3027f8b6ac1a168e158bc87d2d30adbd6c88119fad19f5f87357896f82 - languageName: node - linkType: hard - "jest-changed-files@npm:^29.7.0": version: 29.7.0 resolution: "jest-changed-files@npm:29.7.0" @@ -21468,34 +20662,6 @@ __metadata: languageName: node linkType: hard -"jest-circus@npm:^29.6.4": - version: 29.6.4 - resolution: "jest-circus@npm:29.6.4" - dependencies: - "@jest/environment": "npm:^29.6.4" - "@jest/expect": "npm:^29.6.4" - "@jest/test-result": "npm:^29.6.4" - "@jest/types": "npm:^29.6.3" - "@types/node": "npm:*" - chalk: "npm:^4.0.0" - co: "npm:^4.6.0" - dedent: "npm:^1.0.0" - is-generator-fn: "npm:^2.0.0" - jest-each: "npm:^29.6.3" - jest-matcher-utils: "npm:^29.6.4" - jest-message-util: "npm:^29.6.3" - jest-runtime: "npm:^29.6.4" - jest-snapshot: "npm:^29.6.4" - jest-util: "npm:^29.6.3" - p-limit: "npm:^3.1.0" - pretty-format: "npm:^29.6.3" - pure-rand: "npm:^6.0.0" - slash: "npm:^3.0.0" - stack-utils: "npm:^2.0.3" - checksum: 10/70dd56c1dec25a7499df85d942f27549ce257430b36597e984dbb3dac630a6707133e50a67285cbcf5564aace700e54748c6b7290136b188363678d0af8db17a - languageName: node - linkType: hard - "jest-circus@npm:^29.7.0": version: 29.7.0 resolution: "jest-circus@npm:29.7.0" @@ -21550,71 +20716,6 @@ __metadata: languageName: node linkType: hard -"jest-cli@npm:^29.6.4": - version: 29.6.4 - resolution: "jest-cli@npm:29.6.4" - dependencies: - "@jest/core": "npm:^29.6.4" - "@jest/test-result": "npm:^29.6.4" - "@jest/types": "npm:^29.6.3" - chalk: "npm:^4.0.0" - exit: "npm:^0.1.2" - graceful-fs: "npm:^4.2.9" - import-local: "npm:^3.0.2" - jest-config: "npm:^29.6.4" - jest-util: "npm:^29.6.3" - jest-validate: "npm:^29.6.3" - prompts: "npm:^2.0.1" - yargs: "npm:^17.3.1" - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true - bin: - jest: bin/jest.js - checksum: 10/24b6ca6c8409433a8da621d91a7637ff67e606c0d6c18b606bbbc1a0c7f7e819b935ec1bc6c45c8e6892faf9f073c409f058cdf0ac454f9379ddb826175d40cf - languageName: node - linkType: hard - -"jest-config@npm:^29.6.4": - version: 29.6.4 - resolution: "jest-config@npm:29.6.4" - dependencies: - "@babel/core": "npm:^7.11.6" - "@jest/test-sequencer": "npm:^29.6.4" - "@jest/types": "npm:^29.6.3" - babel-jest: "npm:^29.6.4" - chalk: "npm:^4.0.0" - ci-info: "npm:^3.2.0" - deepmerge: "npm:^4.2.2" - glob: "npm:^7.1.3" - graceful-fs: "npm:^4.2.9" - jest-circus: "npm:^29.6.4" - jest-environment-node: "npm:^29.6.4" - jest-get-type: "npm:^29.6.3" - jest-regex-util: "npm:^29.6.3" - jest-resolve: "npm:^29.6.4" - jest-runner: "npm:^29.6.4" - jest-util: "npm:^29.6.3" - jest-validate: "npm:^29.6.3" - micromatch: "npm:^4.0.4" - parse-json: "npm:^5.2.0" - pretty-format: "npm:^29.6.3" - slash: "npm:^3.0.0" - strip-json-comments: "npm:^3.1.1" - peerDependencies: - "@types/node": "*" - ts-node: ">=9.0.0" - peerDependenciesMeta: - "@types/node": - optional: true - ts-node: - optional: true - checksum: 10/5d9019f046684bf45e87c01996197ccfb96936bca67242f59fdf40d9bd76622c97b5b57bbb257591e12edb941285662dac106f8f327910b26c822adda7057c6d - languageName: node - linkType: hard - "jest-config@npm:^29.7.0": version: 29.7.0 resolution: "jest-config@npm:29.7.0" @@ -21696,27 +20797,6 @@ __metadata: languageName: node linkType: hard -"jest-diff@npm:^29.6.4": - version: 29.6.4 - resolution: "jest-diff@npm:29.6.4" - dependencies: - chalk: "npm:^4.0.0" - diff-sequences: "npm:^29.6.3" - jest-get-type: "npm:^29.6.3" - pretty-format: "npm:^29.6.3" - checksum: 10/b1720b78d1de8e6efaf74425df57e749008049b7c2f8a60af73667fd886653bbc7ee69a452076073ad4b2e3d9d1cd6599bb9dc00a8fb69f02b9075423aafee3c - languageName: node - linkType: hard - -"jest-docblock@npm:^29.6.3": - version: 29.6.3 - resolution: "jest-docblock@npm:29.6.3" - dependencies: - detect-newline: "npm:^3.0.0" - checksum: 10/fa9d8d344f093659beb741e2efa22db663ef8c441200b74707da7749a8799a0022824166d7fdd972e773f7c4fab01227f1847e3315fa63584f539b7b78b285eb - languageName: node - linkType: hard - "jest-docblock@npm:^29.7.0": version: 29.7.0 resolution: "jest-docblock@npm:29.7.0" @@ -21726,19 +20806,6 @@ __metadata: languageName: node linkType: hard -"jest-each@npm:^29.6.3": - version: 29.6.3 - resolution: "jest-each@npm:29.6.3" - dependencies: - "@jest/types": "npm:^29.6.3" - chalk: "npm:^4.0.0" - jest-get-type: "npm:^29.6.3" - jest-util: "npm:^29.6.3" - pretty-format: "npm:^29.6.3" - checksum: 10/e08c01ffba3c6254b6555b749eb7b2e55ca4f153d4e1d8ac15b7dcec4d5c06b150b9f9a272cf3cd622b3b2c495c2d5ee656165b9a93c1c06dcb1a82f13fa95e0 - languageName: node - linkType: hard - "jest-each@npm:^29.7.0": version: 29.7.0 resolution: "jest-each@npm:29.7.0" @@ -21794,20 +20861,6 @@ __metadata: languageName: node linkType: hard -"jest-environment-node@npm:^29.6.4": - version: 29.6.4 - resolution: "jest-environment-node@npm:29.6.4" - dependencies: - "@jest/environment": "npm:^29.6.4" - "@jest/fake-timers": "npm:^29.6.4" - "@jest/types": "npm:^29.6.3" - "@types/node": "npm:*" - jest-mock: "npm:^29.6.3" - jest-util: "npm:^29.6.3" - checksum: 10/7c7bb39a35eaff23eb553c5b7cae776c0622737e64bcd6b558b745012da1366d5f7e2de0a6f659c39f3869783c0b92a5a3dc64649638dbf8f3208c82832b6321 - languageName: node - linkType: hard - "jest-environment-node@npm:^29.7.0": version: 29.7.0 resolution: "jest-environment-node@npm:29.7.0" @@ -21850,29 +20903,6 @@ __metadata: languageName: node linkType: hard -"jest-haste-map@npm:^29.6.4": - version: 29.6.4 - resolution: "jest-haste-map@npm:29.6.4" - dependencies: - "@jest/types": "npm:^29.6.3" - "@types/graceful-fs": "npm:^4.1.3" - "@types/node": "npm:*" - anymatch: "npm:^3.0.3" - fb-watchman: "npm:^2.0.0" - fsevents: "npm:^2.3.2" - graceful-fs: "npm:^4.2.9" - jest-regex-util: "npm:^29.6.3" - jest-util: "npm:^29.6.3" - jest-worker: "npm:^29.6.4" - micromatch: "npm:^4.0.4" - walker: "npm:^1.0.8" - dependenciesMeta: - fsevents: - optional: true - checksum: 10/5fb2dc9cd028b6d02a8d4dcaf81b790b60f9034a20b6a1deca9a4ad529741362cd1035020f3d94f6c160103b1ea3d46771f40a3330ab4d8d3c073d515e11b810 - languageName: node - linkType: hard - "jest-haste-map@npm:^29.7.0": version: 29.7.0 resolution: "jest-haste-map@npm:29.7.0" @@ -21908,64 +20938,25 @@ __metadata: languageName: node linkType: hard -"jest-leak-detector@npm:^29.6.3": - version: 29.6.3 - resolution: "jest-leak-detector@npm:29.6.3" - dependencies: - jest-get-type: "npm:^29.6.3" - pretty-format: "npm:^29.6.3" - checksum: 10/27548fcfc7602fe1b88f8600185e35ffff71751f3631e52bbfdfc72776f5a13a430185cf02fc632b41320a74f99ae90e40ce101c8887509f0f919608a7175129 - languageName: node - linkType: hard - "jest-leak-detector@npm:^29.7.0": version: 29.7.0 resolution: "jest-leak-detector@npm:29.7.0" dependencies: jest-get-type: "npm:^29.6.3" - pretty-format: "npm:^29.7.0" - checksum: 10/e3950e3ddd71e1d0c22924c51a300a1c2db6cf69ec1e51f95ccf424bcc070f78664813bef7aed4b16b96dfbdeea53fe358f8aeaaea84346ae15c3735758f1605 - languageName: node - linkType: hard - -"jest-matcher-utils@npm:29.7.0, jest-matcher-utils@npm:^29.7.0": - version: 29.7.0 - resolution: "jest-matcher-utils@npm:29.7.0" - dependencies: - chalk: "npm:^4.0.0" - jest-diff: "npm:^29.7.0" - jest-get-type: "npm:^29.6.3" - pretty-format: "npm:^29.7.0" - checksum: 10/981904a494299cf1e3baed352f8a3bd8b50a8c13a662c509b6a53c31461f94ea3bfeffa9d5efcfeb248e384e318c87de7e3baa6af0f79674e987482aa189af40 - languageName: node - linkType: hard - -"jest-matcher-utils@npm:^29.6.4": - version: 29.6.4 - resolution: "jest-matcher-utils@npm:29.6.4" - dependencies: - chalk: "npm:^4.0.0" - jest-diff: "npm:^29.6.4" - jest-get-type: "npm:^29.6.3" - pretty-format: "npm:^29.6.3" - checksum: 10/de306e3592d316ff9725b8e2595c6a4bb9c05b1f296b3e73aef5cf945a4b4799dbfc3fc080e74f4e6259b65123a70b2dc3595db5cfcbaaa30ed3d37ec59551a0 + pretty-format: "npm:^29.7.0" + checksum: 10/e3950e3ddd71e1d0c22924c51a300a1c2db6cf69ec1e51f95ccf424bcc070f78664813bef7aed4b16b96dfbdeea53fe358f8aeaaea84346ae15c3735758f1605 languageName: node linkType: hard -"jest-message-util@npm:^29.6.3": - version: 29.6.3 - resolution: "jest-message-util@npm:29.6.3" +"jest-matcher-utils@npm:29.7.0, jest-matcher-utils@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-matcher-utils@npm:29.7.0" dependencies: - "@babel/code-frame": "npm:^7.12.13" - "@jest/types": "npm:^29.6.3" - "@types/stack-utils": "npm:^2.0.0" chalk: "npm:^4.0.0" - graceful-fs: "npm:^4.2.9" - micromatch: "npm:^4.0.4" - pretty-format: "npm:^29.6.3" - slash: "npm:^3.0.0" - stack-utils: "npm:^2.0.3" - checksum: 10/fe659a92a32e6f9c3fdb9b07792a2a362b3d091334eb230b12524ffb5023457ea39d7fc412187e4f245dbe394fd012591878a2b5932eaedd7e82d5c9b416035c + jest-diff: "npm:^29.7.0" + jest-get-type: "npm:^29.6.3" + pretty-format: "npm:^29.7.0" + checksum: 10/981904a494299cf1e3baed352f8a3bd8b50a8c13a662c509b6a53c31461f94ea3bfeffa9d5efcfeb248e384e318c87de7e3baa6af0f79674e987482aa189af40 languageName: node linkType: hard @@ -21997,17 +20988,6 @@ __metadata: languageName: node linkType: hard -"jest-mock@npm:^29.6.3": - version: 29.6.3 - resolution: "jest-mock@npm:29.6.3" - dependencies: - "@jest/types": "npm:^29.6.3" - "@types/node": "npm:*" - jest-util: "npm:^29.6.3" - checksum: 10/9da22e0edfb77b2ed2d4204f305b41a17c2133c0cb638ee81e875115ae6e01adf57681a0ab8f7c2b7e0cde7dcd916d62c7b5d28176c342bb80bbfcea8a168729 - languageName: node - linkType: hard - "jest-pnp-resolver@npm:^1.2.2": version: 1.2.2 resolution: "jest-pnp-resolver@npm:1.2.2" @@ -22027,16 +21007,6 @@ __metadata: languageName: node linkType: hard -"jest-resolve-dependencies@npm:^29.6.4": - version: 29.6.4 - resolution: "jest-resolve-dependencies@npm:29.6.4" - dependencies: - jest-regex-util: "npm:^29.6.3" - jest-snapshot: "npm:^29.6.4" - checksum: 10/8a1616558e78213bc4c7c445e7188776f7d4940e3858684e0404c485bbc23e1e10093bf59640fbc4b88141f361f0c01e760eb2c79bf088ad1896971941869158 - languageName: node - linkType: hard - "jest-resolve-dependencies@npm:^29.7.0": version: 29.7.0 resolution: "jest-resolve-dependencies@npm:29.7.0" @@ -22047,23 +21017,6 @@ __metadata: languageName: node linkType: hard -"jest-resolve@npm:^29.6.4": - version: 29.6.4 - resolution: "jest-resolve@npm:29.6.4" - dependencies: - chalk: "npm:^4.0.0" - graceful-fs: "npm:^4.2.9" - jest-haste-map: "npm:^29.6.4" - jest-pnp-resolver: "npm:^1.2.2" - jest-util: "npm:^29.6.3" - jest-validate: "npm:^29.6.3" - resolve: "npm:^1.20.0" - resolve.exports: "npm:^2.0.0" - slash: "npm:^3.0.0" - checksum: 10/63fcd739106363a8928a96131fd0fd7dfa8b052b70d0c3a3f1099f33666b2d9086880f01e714e46b1044f7d107caaae81fd1547bd2c149d6b7a06453d5cc14b3 - languageName: node - linkType: hard - "jest-resolve@npm:^29.7.0": version: 29.7.0 resolution: "jest-resolve@npm:29.7.0" @@ -22081,35 +21034,6 @@ __metadata: languageName: node linkType: hard -"jest-runner@npm:^29.6.4": - version: 29.6.4 - resolution: "jest-runner@npm:29.6.4" - dependencies: - "@jest/console": "npm:^29.6.4" - "@jest/environment": "npm:^29.6.4" - "@jest/test-result": "npm:^29.6.4" - "@jest/transform": "npm:^29.6.4" - "@jest/types": "npm:^29.6.3" - "@types/node": "npm:*" - chalk: "npm:^4.0.0" - emittery: "npm:^0.13.1" - graceful-fs: "npm:^4.2.9" - jest-docblock: "npm:^29.6.3" - jest-environment-node: "npm:^29.6.4" - jest-haste-map: "npm:^29.6.4" - jest-leak-detector: "npm:^29.6.3" - jest-message-util: "npm:^29.6.3" - jest-resolve: "npm:^29.6.4" - jest-runtime: "npm:^29.6.4" - jest-util: "npm:^29.6.3" - jest-watcher: "npm:^29.6.4" - jest-worker: "npm:^29.6.4" - p-limit: "npm:^3.1.0" - source-map-support: "npm:0.5.13" - checksum: 10/b51345e07b78b546dd192f734d1e5324f41fe5aceafba6a8238b3f38f3bcafa433ee9a5bdf41f853bed4279b044ce0621be75b33b840f4a9745b6c958220a21e - languageName: node - linkType: hard - "jest-runner@npm:^29.7.0": version: 29.7.0 resolution: "jest-runner@npm:29.7.0" @@ -22139,36 +21063,6 @@ __metadata: languageName: node linkType: hard -"jest-runtime@npm:^29.6.4": - version: 29.6.4 - resolution: "jest-runtime@npm:29.6.4" - dependencies: - "@jest/environment": "npm:^29.6.4" - "@jest/fake-timers": "npm:^29.6.4" - "@jest/globals": "npm:^29.6.4" - "@jest/source-map": "npm:^29.6.3" - "@jest/test-result": "npm:^29.6.4" - "@jest/transform": "npm:^29.6.4" - "@jest/types": "npm:^29.6.3" - "@types/node": "npm:*" - chalk: "npm:^4.0.0" - cjs-module-lexer: "npm:^1.0.0" - collect-v8-coverage: "npm:^1.0.0" - glob: "npm:^7.1.3" - graceful-fs: "npm:^4.2.9" - jest-haste-map: "npm:^29.6.4" - jest-message-util: "npm:^29.6.3" - jest-mock: "npm:^29.6.3" - jest-regex-util: "npm:^29.6.3" - jest-resolve: "npm:^29.6.4" - jest-snapshot: "npm:^29.6.4" - jest-util: "npm:^29.6.3" - slash: "npm:^3.0.0" - strip-bom: "npm:^4.0.0" - checksum: 10/e70669b5a439f5a26156b972462471fbac997e6779fe7421df000f5fe8f1f11abe0c7cb664edd1cf3849c3f2d8329dc7e8495ce89251352a6cabff9153dcf99a - languageName: node - linkType: hard - "jest-runtime@npm:^29.7.0": version: 29.7.0 resolution: "jest-runtime@npm:29.7.0" @@ -22199,34 +21093,6 @@ __metadata: languageName: node linkType: hard -"jest-snapshot@npm:^29.6.4": - version: 29.6.4 - resolution: "jest-snapshot@npm:29.6.4" - dependencies: - "@babel/core": "npm:^7.11.6" - "@babel/generator": "npm:^7.7.2" - "@babel/plugin-syntax-jsx": "npm:^7.7.2" - "@babel/plugin-syntax-typescript": "npm:^7.7.2" - "@babel/types": "npm:^7.3.3" - "@jest/expect-utils": "npm:^29.6.4" - "@jest/transform": "npm:^29.6.4" - "@jest/types": "npm:^29.6.3" - babel-preset-current-node-syntax: "npm:^1.0.0" - chalk: "npm:^4.0.0" - expect: "npm:^29.6.4" - graceful-fs: "npm:^4.2.9" - jest-diff: "npm:^29.6.4" - jest-get-type: "npm:^29.6.3" - jest-matcher-utils: "npm:^29.6.4" - jest-message-util: "npm:^29.6.3" - jest-util: "npm:^29.6.3" - natural-compare: "npm:^1.4.0" - pretty-format: "npm:^29.6.3" - semver: "npm:^7.5.3" - checksum: 10/998476c7ffef43cfe97ef9b786c6c6489440b0042537b0c1ad8b4366de73fc6b1ec16d148ef294b24b835d045c186b2543217e6906dd495b4d20112e85007168 - languageName: node - linkType: hard - "jest-snapshot@npm:^29.7.0": version: 29.7.0 resolution: "jest-snapshot@npm:29.7.0" @@ -22269,34 +21135,6 @@ __metadata: languageName: node linkType: hard -"jest-util@npm:^29.6.3": - version: 29.6.3 - resolution: "jest-util@npm:29.6.3" - dependencies: - "@jest/types": "npm:^29.6.3" - "@types/node": "npm:*" - chalk: "npm:^4.0.0" - ci-info: "npm:^3.2.0" - graceful-fs: "npm:^4.2.9" - picomatch: "npm:^2.2.3" - checksum: 10/455af2b5e064213b33b837a18ddd3d31878aee31ad40bbd599de2a4977f860a797e491cb94894e38bbd352cb7b31d41448b7ec3b346408613015411cd88ed57f - languageName: node - linkType: hard - -"jest-validate@npm:^29.6.3": - version: 29.6.3 - resolution: "jest-validate@npm:29.6.3" - dependencies: - "@jest/types": "npm:^29.6.3" - camelcase: "npm:^6.2.0" - chalk: "npm:^4.0.0" - jest-get-type: "npm:^29.6.3" - leven: "npm:^3.1.0" - pretty-format: "npm:^29.6.3" - checksum: 10/4e0a3ef5a2c181f10dcd979ae62188166effd52d7aec7916deaa29b75b29bdf4e01b77375246c7c16032d95cb7364ceae69c5d146e4348ccdf8a3a43d1c6862c - languageName: node - linkType: hard - "jest-validate@npm:^29.7.0": version: 29.7.0 resolution: "jest-validate@npm:29.7.0" @@ -22311,22 +21149,6 @@ __metadata: languageName: node linkType: hard -"jest-watcher@npm:^29.6.4": - version: 29.6.4 - resolution: "jest-watcher@npm:29.6.4" - dependencies: - "@jest/test-result": "npm:^29.6.4" - "@jest/types": "npm:^29.6.3" - "@types/node": "npm:*" - ansi-escapes: "npm:^4.2.1" - chalk: "npm:^4.0.0" - emittery: "npm:^0.13.1" - jest-util: "npm:^29.6.3" - string-length: "npm:^4.0.1" - checksum: 10/01c008b2f3e76b024f9e9dd242ba5b172634a6b9bc201d7f27cb82563071f06ae87e3ae8db50336034264a98d20a45459068f089597c956a96959109527498c4 - languageName: node - linkType: hard - "jest-watcher@npm:^29.7.0": version: 29.7.0 resolution: "jest-watcher@npm:29.7.0" @@ -22377,18 +21199,6 @@ __metadata: languageName: node linkType: hard -"jest-worker@npm:^29.6.4": - version: 29.6.4 - resolution: "jest-worker@npm:29.6.4" - dependencies: - "@types/node": "npm:*" - jest-util: "npm:^29.6.3" - merge-stream: "npm:^2.0.0" - supports-color: "npm:^8.0.0" - checksum: 10/52b2f238f21ff20dd4cb74ab85c1b32ba8ce5709355ae2f6d3e908a06ac0828658ab9d94562d752833d0e469f35517ceba9b68ca6b0d27fa1057115f065864ce - languageName: node - linkType: hard - "jest@npm:29.3.1": version: 29.3.1 resolution: "jest@npm:29.3.1" @@ -22408,7 +21218,7 @@ __metadata: languageName: node linkType: hard -"jest@npm:29.7.0": +"jest@npm:29.7.0, jest@npm:^29.6.4": version: 29.7.0 resolution: "jest@npm:29.7.0" dependencies: @@ -22427,25 +21237,6 @@ __metadata: languageName: node linkType: hard -"jest@npm:^29.6.4": - version: 29.6.4 - resolution: "jest@npm:29.6.4" - dependencies: - "@jest/core": "npm:^29.6.4" - "@jest/types": "npm:^29.6.3" - import-local: "npm:^3.0.2" - jest-cli: "npm:^29.6.4" - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true - bin: - jest: bin/jest.js - checksum: 10/d747e293bd63f583e7978ac0693ab7a019812fa44b9bf3b3fe20e75e8a343bcd8251d292326d73151dc0b8a2b5a974d878b3aa9ffb146dfa7980553f64a35b43 - languageName: node - linkType: hard - "jiti@npm:^1.20.0": version: 1.21.0 resolution: "jiti@npm:1.21.0" @@ -23970,7 +22761,7 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:9.0.3, minimatch@npm:^9.0.0, minimatch@npm:^9.0.3": +"minimatch@npm:9.0.3, minimatch@npm:^9.0.0, minimatch@npm:^9.0.1, minimatch@npm:^9.0.3": version: 9.0.3 resolution: "minimatch@npm:9.0.3" dependencies: @@ -23997,15 +22788,6 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^9.0.1": - version: 9.0.1 - resolution: "minimatch@npm:9.0.1" - dependencies: - brace-expansion: "npm:^2.0.1" - checksum: 10/b4e98f4dc740dcf33999a99af23ae6e5e1c47632f296dc95cb649a282150f92378d41434bf64af4ea2e5975255a757d031c3bf014bad9214544ac57d97f3ba63 - languageName: node - linkType: hard - "minimist-options@npm:4.1.0": version: 4.1.0 resolution: "minimist-options@npm:4.1.0" @@ -26619,17 +25401,6 @@ __metadata: languageName: node linkType: hard -"pretty-format@npm:^29.6.3": - version: 29.6.3 - resolution: "pretty-format@npm:29.6.3" - dependencies: - "@jest/schemas": "npm:^29.6.3" - ansi-styles: "npm:^5.0.0" - react-is: "npm:^18.0.0" - checksum: 10/4a17a0953b3e2d334e628dc9ff11cfad988e6adb00c074bf9d10f3eb1919ad56b30d987148ac0ce1d0317ad392cd78b39a74b6cbac4e66af609f6127ad3aaaf0 - languageName: node - linkType: hard - "pretty-hrtime@npm:^1.0.3": version: 1.0.3 resolution: "pretty-hrtime@npm:1.0.3" @@ -26955,7 +25726,7 @@ __metadata: languageName: node linkType: hard -"queue-tick@npm:^1.0.0, queue-tick@npm:^1.0.1": +"queue-tick@npm:^1.0.1": version: 1.0.1 resolution: "queue-tick@npm:1.0.1" checksum: 10/f447926c513b64a857906f017a3b350f7d11277e3c8d2a21a42b7998fa1a613d7a829091e12d142bb668905c8f68d8103416c7197856efb0c72fa835b8e254b5 @@ -28153,7 +26924,7 @@ __metadata: languageName: node linkType: hard -"react-use@npm:17.5.0": +"react-use@npm:17.5.0, react-use@npm:^17.4.2": version: 17.5.0 resolution: "react-use@npm:17.5.0" dependencies: @@ -28178,31 +26949,6 @@ __metadata: languageName: node linkType: hard -"react-use@npm:^17.4.2": - version: 17.4.2 - resolution: "react-use@npm:17.4.2" - dependencies: - "@types/js-cookie": "npm:^2.2.6" - "@xobotyi/scrollbar-width": "npm:^1.9.5" - copy-to-clipboard: "npm:^3.3.1" - fast-deep-equal: "npm:^3.1.3" - fast-shallow-equal: "npm:^1.0.0" - js-cookie: "npm:^2.2.1" - nano-css: "npm:^5.6.1" - react-universal-interface: "npm:^0.6.2" - resize-observer-polyfill: "npm:^1.5.1" - screenfull: "npm:^5.1.0" - set-harmonic-interval: "npm:^1.0.1" - throttle-debounce: "npm:^3.0.1" - ts-easing: "npm:^0.2.0" - tslib: "npm:^2.1.0" - peerDependencies: - react: "*" - react-dom: "*" - checksum: 10/56d2da474d949d22eb34ff3ffccf5526986d51ed68a8f4e64f4b79bdcff3f0ea55d322c104e3fc0819b08b8765e8eb3fa47d8b506e9d61ff1fdc7bd1374c17d6 - languageName: node - linkType: hard - "react-virtual@npm:2.10.4, react-virtual@npm:^2.8.2": version: 2.10.4 resolution: "react-virtual@npm:2.10.4" @@ -30364,7 +29110,7 @@ __metadata: languageName: node linkType: hard -"streamx@npm:^2.12.0, streamx@npm:^2.13.2, streamx@npm:^2.14.0": +"streamx@npm:^2.12.0, streamx@npm:^2.12.5, streamx@npm:^2.13.2, streamx@npm:^2.14.0": version: 2.15.7 resolution: "streamx@npm:2.15.7" dependencies: @@ -30374,16 +29120,6 @@ __metadata: languageName: node linkType: hard -"streamx@npm:^2.12.5": - version: 2.12.5 - resolution: "streamx@npm:2.12.5" - dependencies: - fast-fifo: "npm:^1.0.0" - queue-tick: "npm:^1.0.0" - checksum: 10/daa5789ca31101684d9266f7ea77294908bd3e55607805ac1657f0cef1ee0a1966bc3988d2ec12c5f68a718d481147fa3ace2525486a1e39ca7155c598917cd1 - languageName: node - linkType: hard - "strict-event-emitter@npm:^0.2.4": version: 0.2.8 resolution: "strict-event-emitter@npm:0.2.8" @@ -30999,7 +29735,7 @@ __metadata: languageName: node linkType: hard -"terser-webpack-plugin@npm:5.3.10, terser-webpack-plugin@npm:^5.3.10": +"terser-webpack-plugin@npm:5.3.10, terser-webpack-plugin@npm:^5.1.3, terser-webpack-plugin@npm:^5.3.1, terser-webpack-plugin@npm:^5.3.10, terser-webpack-plugin@npm:^5.3.7": version: 5.3.10 resolution: "terser-webpack-plugin@npm:5.3.10" dependencies: @@ -31021,28 +29757,6 @@ __metadata: languageName: node linkType: hard -"terser-webpack-plugin@npm:^5.1.3, terser-webpack-plugin@npm:^5.3.1, terser-webpack-plugin@npm:^5.3.7": - version: 5.3.9 - resolution: "terser-webpack-plugin@npm:5.3.9" - dependencies: - "@jridgewell/trace-mapping": "npm:^0.3.17" - jest-worker: "npm:^27.4.5" - schema-utils: "npm:^3.1.1" - serialize-javascript: "npm:^6.0.1" - terser: "npm:^5.16.8" - peerDependencies: - webpack: ^5.1.0 - peerDependenciesMeta: - "@swc/core": - optional: true - esbuild: - optional: true - uglify-js: - optional: true - checksum: 10/339737a407e034b7a9d4a66e31d84d81c10433e41b8eae2ca776f0e47c2048879be482a9aa08e8c27565a2a949bc68f6e07f451bf4d9aa347dd61b3d000f5353 - languageName: node - linkType: hard - "terser@npm:^5.0.0, terser@npm:^5.15.1, terser@npm:^5.26.0, terser@npm:^5.7.2": version: 5.27.0 resolution: "terser@npm:5.27.0" @@ -31057,20 +29771,6 @@ __metadata: languageName: node linkType: hard -"terser@npm:^5.16.8": - version: 5.17.2 - resolution: "terser@npm:5.17.2" - dependencies: - "@jridgewell/source-map": "npm:^0.3.2" - acorn: "npm:^8.5.0" - commander: "npm:^2.20.0" - source-map-support: "npm:~0.5.20" - bin: - terser: bin/terser - checksum: 10/6df529586a4913657547dd8bfe2b5a59704b7acbe4e49ac938a16f829a62226f98dafb19c88b7af66b245ea281ee5dbeec33a41349ac3c035855417b06ebd646 - languageName: node - linkType: hard - "test-exclude@npm:^6.0.0": version: 6.0.0 resolution: "test-exclude@npm:6.0.0" @@ -32760,7 +31460,7 @@ __metadata: languageName: node linkType: hard -"webpack-merge@npm:5.10.0": +"webpack-merge@npm:5.10.0, webpack-merge@npm:^5.7.3": version: 5.10.0 resolution: "webpack-merge@npm:5.10.0" dependencies: @@ -32771,16 +31471,6 @@ __metadata: languageName: node linkType: hard -"webpack-merge@npm:^5.7.3": - version: 5.9.0 - resolution: "webpack-merge@npm:5.9.0" - dependencies: - clone-deep: "npm:^4.0.1" - wildcard: "npm:^2.0.0" - checksum: 10/d23dd1f0bad0b9821bf58443d2d29097d65cd9353046c2d8a6d7b57877ec19cf64be57cc7ef2a371a15cf9264fe6eaf8dea4015dc87487e664ffab2a28329d56 - languageName: node - linkType: hard - "webpack-sources@npm:^1.4.3": version: 1.4.3 resolution: "webpack-sources@npm:1.4.3" @@ -32815,9 +31505,9 @@ __metadata: languageName: node linkType: hard -"webpack@npm:5, webpack@npm:^5": - version: 5.90.1 - resolution: "webpack@npm:5.90.1" +"webpack@npm:5, webpack@npm:5.90.2, webpack@npm:^5": + version: 5.90.2 + resolution: "webpack@npm:5.90.2" dependencies: "@types/eslint-scope": "npm:^3.7.3" "@types/estree": "npm:^1.0.5" @@ -32848,7 +31538,7 @@ __metadata: optional: true bin: webpack: bin/webpack.js - checksum: 10/6ad23518123f1742238177920cefa61152d981f986adac5901236845c86ba9bb375a3ba75e188925c856c3d2a76a2ba119e95b8a608a51424968389041089075 + checksum: 10/4eaeed1255c9c7738921c4ce4facdb3b78dbfcb3441496942f6d160a41fbcebd24fb2c6dbb64739b357c5ff78e5a298f6c82eca482438b95130a3ba4e16d084a languageName: node linkType: hard @@ -32926,43 +31616,6 @@ __metadata: languageName: node linkType: hard -"webpack@npm:5.90.2": - version: 5.90.2 - resolution: "webpack@npm:5.90.2" - dependencies: - "@types/eslint-scope": "npm:^3.7.3" - "@types/estree": "npm:^1.0.5" - "@webassemblyjs/ast": "npm:^1.11.5" - "@webassemblyjs/wasm-edit": "npm:^1.11.5" - "@webassemblyjs/wasm-parser": "npm:^1.11.5" - acorn: "npm:^8.7.1" - acorn-import-assertions: "npm:^1.9.0" - browserslist: "npm:^4.21.10" - chrome-trace-event: "npm:^1.0.2" - enhanced-resolve: "npm:^5.15.0" - es-module-lexer: "npm:^1.2.1" - eslint-scope: "npm:5.1.1" - events: "npm:^3.2.0" - glob-to-regexp: "npm:^0.4.1" - graceful-fs: "npm:^4.2.9" - json-parse-even-better-errors: "npm:^2.3.1" - loader-runner: "npm:^4.2.0" - mime-types: "npm:^2.1.27" - neo-async: "npm:^2.6.2" - schema-utils: "npm:^3.2.0" - tapable: "npm:^2.1.1" - terser-webpack-plugin: "npm:^5.3.10" - watchpack: "npm:^2.4.0" - webpack-sources: "npm:^3.2.3" - peerDependenciesMeta: - webpack-cli: - optional: true - bin: - webpack: bin/webpack.js - checksum: 10/4eaeed1255c9c7738921c4ce4facdb3b78dbfcb3441496942f6d160a41fbcebd24fb2c6dbb64739b357c5ff78e5a298f6c82eca482438b95130a3ba4e16d084a - languageName: node - linkType: hard - "websocket-driver@npm:>=0.5.1, websocket-driver@npm:^0.7.4": version: 0.7.4 resolution: "websocket-driver@npm:0.7.4" @@ -33069,7 +31722,7 @@ __metadata: languageName: node linkType: hard -"which-typed-array@npm:^1.1.11": +"which-typed-array@npm:^1.1.11, which-typed-array@npm:^1.1.2, which-typed-array@npm:^1.1.9": version: 1.1.11 resolution: "which-typed-array@npm:1.1.11" dependencies: @@ -33082,20 +31735,6 @@ __metadata: languageName: node linkType: hard -"which-typed-array@npm:^1.1.2, which-typed-array@npm:^1.1.9": - version: 1.1.9 - resolution: "which-typed-array@npm:1.1.9" - dependencies: - available-typed-arrays: "npm:^1.0.5" - call-bind: "npm:^1.0.2" - for-each: "npm:^0.3.3" - gopd: "npm:^1.0.1" - has-tostringtag: "npm:^1.0.0" - is-typed-array: "npm:^1.1.10" - checksum: 10/90ef760a09dcffc479138a6bc77fd2933a81a41d531f4886ae212f6edb54a0645a43a6c24de2c096aea910430035ac56b3d22a06f3d64e5163fa178d0f24e08e - languageName: node - linkType: hard - "which@npm:^1.3.1": version: 1.3.1 resolution: "which@npm:1.3.1" @@ -33387,14 +32026,7 @@ __metadata: languageName: node linkType: hard -"yaml@npm:^2.0.0": - version: 2.3.1 - resolution: "yaml@npm:2.3.1" - checksum: 10/66501d597e43766eb94dc175d28ec8b2c63087d6a78783e59b4218eee32b9172740f9f27d54b7bc0ca8af61422f7134929f9974faeaac99d583787e793852fd2 - languageName: node - linkType: hard - -"yaml@npm:^2.3.4": +"yaml@npm:^2.0.0, yaml@npm:^2.3.4": version: 2.3.4 resolution: "yaml@npm:2.3.4" checksum: 10/f8207ce43065a22268a2806ea6a0fa3974c6fde92b4b2fa0082357e487bc333e85dc518910007e7ac001b532c7c84bd3eccb6c7757e94182b564028b0008f44b From d0679f0993f15d2f6a67f7563bb3d8ee33588413 Mon Sep 17 00:00:00 2001 From: Serge Zaitsev Date: Mon, 26 Feb 2024 11:27:22 +0100 Subject: [PATCH 0160/1406] Chore: Add support bundle for folders (#83360) * add support bundle for folders * fix ProvideService in tests * add a test for collector --- pkg/api/dashboard_test.go | 3 +- pkg/api/folder_bench_test.go | 3 +- .../annotationsimpl/annotations_test.go | 3 +- .../database/database_folder_test.go | 2 +- .../dashboards/database/database_test.go | 5 +- pkg/services/folder/folderimpl/folder.go | 70 ++++++++++++++++++- pkg/services/folder/folderimpl/folder_test.go | 68 +++++++++++++++++- pkg/services/folder/folderimpl/sqlstore.go | 7 +- .../libraryelements/libraryelements_test.go | 4 +- .../librarypanels/librarypanels_test.go | 4 +- .../ngalert/migration/store/testing.go | 3 +- pkg/services/ngalert/testutil/testutil.go | 3 +- .../sqlstore/permissions/dashboard_test.go | 3 +- .../permissions/dashboards_bench_test.go | 3 +- 14 files changed, 163 insertions(+), 18 deletions(-) diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index 4f759ae100..bede8c3cfd 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -56,6 +56,7 @@ import ( publicdashboardModels "github.com/grafana/grafana/pkg/services/publicdashboards/models" "github.com/grafana/grafana/pkg/services/quota/quotatest" "github.com/grafana/grafana/pkg/services/star/startest" + "github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest" "github.com/grafana/grafana/pkg/services/tag/tagimpl" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user/usertest" @@ -829,7 +830,7 @@ func getDashboardShouldReturn200WithConfig(t *testing.T, sc *scenarioContext, pr dashboardPermissions := accesscontrolmock.NewMockedPermissionsService() folderSvc := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), - cfg, dashboardStore, folderStore, db.InitTestDB(t), features, nil) + cfg, dashboardStore, folderStore, db.InitTestDB(t), features, supportbundlestest.NewFakeBundleService(), nil) if dashboardService == nil { dashboardService, err = service.ProvideDashboardServiceImpl( diff --git a/pkg/api/folder_bench_test.go b/pkg/api/folder_bench_test.go index 17bb27e0dd..1363d7b4c9 100644 --- a/pkg/api/folder_bench_test.go +++ b/pkg/api/folder_bench_test.go @@ -41,6 +41,7 @@ import ( "github.com/grafana/grafana/pkg/services/star" "github.com/grafana/grafana/pkg/services/star/startest" "github.com/grafana/grafana/pkg/services/supportbundles/bundleregistry" + "github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest" "github.com/grafana/grafana/pkg/services/tag/tagimpl" "github.com/grafana/grafana/pkg/services/team" "github.com/grafana/grafana/pkg/services/team/teamimpl" @@ -453,7 +454,7 @@ func setupServer(b testing.TB, sc benchScenario, features featuremgmt.FeatureTog folderStore := folderimpl.ProvideDashboardFolderStore(sc.db) ac := acimpl.ProvideAccessControl(sc.cfg) - folderServiceWithFlagOn := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), sc.cfg, dashStore, folderStore, sc.db, features, nil) + folderServiceWithFlagOn := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), sc.cfg, dashStore, folderStore, sc.db, features, supportbundlestest.NewFakeBundleService(), nil) cfg := setting.NewCfg() folderPermissions, err := ossaccesscontrol.ProvideFolderPermissions( diff --git a/pkg/services/annotations/annotationsimpl/annotations_test.go b/pkg/services/annotations/annotationsimpl/annotations_test.go index ef407f09a8..a3053d5d12 100644 --- a/pkg/services/annotations/annotationsimpl/annotations_test.go +++ b/pkg/services/annotations/annotationsimpl/annotations_test.go @@ -26,6 +26,7 @@ import ( "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/quota/quotatest" "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest" "github.com/grafana/grafana/pkg/services/tag/tagimpl" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" @@ -226,7 +227,7 @@ func TestIntegrationAnnotationListingWithInheritedRBAC(t *testing.T) { }) ac := acimpl.ProvideAccessControl(sql.Cfg) - folderSvc := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), sql.Cfg, dashStore, folderimpl.ProvideDashboardFolderStore(sql), sql, features, nil) + folderSvc := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), sql.Cfg, dashStore, folderimpl.ProvideDashboardFolderStore(sql), sql, features, supportbundlestest.NewFakeBundleService(), nil) cfg := setting.NewCfg() cfg.AnnotationMaximumTagsLength = 60 diff --git a/pkg/services/dashboards/database/database_folder_test.go b/pkg/services/dashboards/database/database_folder_test.go index a2d8bf90de..250f926d57 100644 --- a/pkg/services/dashboards/database/database_folder_test.go +++ b/pkg/services/dashboards/database/database_folder_test.go @@ -295,7 +295,7 @@ func TestIntegrationDashboardInheritedFolderRBAC(t *testing.T) { guardian.New = origNewGuardian }) - folderSvc := folderimpl.ProvideService(mock.New(), bus.ProvideBus(tracing.InitializeTracerForTest()), sqlStore.Cfg, dashboardWriteStore, folderimpl.ProvideDashboardFolderStore(sqlStore), sqlStore, features, nil) + folderSvc := folderimpl.ProvideService(mock.New(), bus.ProvideBus(tracing.InitializeTracerForTest()), sqlStore.Cfg, dashboardWriteStore, folderimpl.ProvideDashboardFolderStore(sqlStore), sqlStore, features, supportbundlestest.NewFakeBundleService(), nil) parentUID := "" for i := 0; ; i++ { diff --git a/pkg/services/dashboards/database/database_test.go b/pkg/services/dashboards/database/database_test.go index c1f6c44f6c..2bf380956f 100644 --- a/pkg/services/dashboards/database/database_test.go +++ b/pkg/services/dashboards/database/database_test.go @@ -26,6 +26,7 @@ import ( "github.com/grafana/grafana/pkg/services/search/model" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/sqlstore/searchstore" + "github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest" "github.com/grafana/grafana/pkg/services/tag/tagimpl" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" @@ -712,7 +713,7 @@ func TestIntegrationFindDashboardsByTitle(t *testing.T) { ac := acimpl.ProvideAccessControl(sqlStore.Cfg) folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore) - folderServiceWithFlagOn := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), sqlStore.Cfg, dashboardStore, folderStore, sqlStore, features, nil) + folderServiceWithFlagOn := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), sqlStore.Cfg, dashboardStore, folderStore, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil) user := &user.SignedInUser{ OrgID: 1, @@ -829,7 +830,7 @@ func TestIntegrationFindDashboardsByFolder(t *testing.T) { ac := acimpl.ProvideAccessControl(sqlStore.Cfg) folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore) - folderServiceWithFlagOn := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), sqlStore.Cfg, dashboardStore, folderStore, sqlStore, features, nil) + folderServiceWithFlagOn := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), sqlStore.Cfg, dashboardStore, folderStore, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil) user := &user.SignedInUser{ OrgID: 1, diff --git a/pkg/services/folder/folderimpl/folder.go b/pkg/services/folder/folderimpl/folder.go index 385bf99226..cc85369810 100644 --- a/pkg/services/folder/folderimpl/folder.go +++ b/pkg/services/folder/folderimpl/folder.go @@ -2,6 +2,7 @@ package folderimpl import ( "context" + "encoding/json" "errors" "fmt" "runtime" @@ -27,6 +28,8 @@ import ( "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/sqlstore/migrator" "github.com/grafana/grafana/pkg/services/store/entity" + "github.com/grafana/grafana/pkg/services/supportbundles" + "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" ) @@ -40,7 +43,6 @@ type Service struct { dashboardFolderStore folder.FolderStore features featuremgmt.FeatureToggles accessControl accesscontrol.AccessControl - // bus is currently used to publish event in case of title change bus bus.Bus @@ -57,6 +59,7 @@ func ProvideService( folderStore folder.FolderStore, db db.DB, // DB for the (new) nested folder store features featuremgmt.FeatureToggles, + supportBundles supportbundles.Service, r prometheus.Registerer, ) folder.Service { store := ProvideStore(db, cfg) @@ -75,6 +78,8 @@ func ProvideService( } srv.DBMigration(db) + supportBundles.RegisterSupportItemCollector(srv.supportBundleCollector()) + ac.RegisterScopeAttributeResolver(dashboards.NewFolderNameScopeResolver(folderStore, srv)) ac.RegisterScopeAttributeResolver(dashboards.NewFolderIDScopeResolver(folderStore, srv)) ac.RegisterScopeAttributeResolver(dashboards.NewFolderUIDScopeResolver(srv)) @@ -1181,3 +1186,66 @@ func (s *Service) RegisterService(r folder.RegistryService) error { return nil } + +func (s *Service) supportBundleCollector() supportbundles.Collector { + collector := supportbundles.Collector{ + UID: "folder-stats", + DisplayName: "Folder information", + Description: "Folder information for the Grafana instance", + IncludedByDefault: false, + Default: true, + Fn: func(ctx context.Context) (*supportbundles.SupportItem, error) { + s.log.Info("Generating folder support bundle") + folders, err := s.GetFolders(ctx, folder.GetFoldersQuery{ + OrgID: 0, + SignedInUser: &user.SignedInUser{ + Login: "sa-supportbundle", + OrgRole: "Admin", + IsGrafanaAdmin: true, + IsServiceAccount: true, + Permissions: map[int64]map[string][]string{accesscontrol.GlobalOrgID: {dashboards.ActionFoldersRead: {dashboards.ScopeFoldersAll}}}, + }, + }) + if err != nil { + return nil, err + } + return s.supportItemFromFolders(folders) + }, + } + return collector +} + +func (s *Service) supportItemFromFolders(folders []*folder.Folder) (*supportbundles.SupportItem, error) { + stats := struct { + Total int `json:"total"` // how many folders? + Depths map[int]int `json:"depths"` // how deep they are? + Children map[int]int `json:"children"` // how many child folders they have? + Folders []*folder.Folder `json:"folders"` // what are they? + }{Total: len(folders), Folders: folders, Children: map[int]int{}, Depths: map[int]int{}} + + // Build parent-child mapping + parents := map[string]string{} + children := map[string][]string{} + for _, f := range folders { + parents[f.UID] = f.ParentUID + children[f.ParentUID] = append(children[f.ParentUID], f.UID) + } + // Find depths of each folder + for _, f := range folders { + depth := 0 + for uid := f.UID; uid != ""; uid = parents[uid] { + depth++ + } + stats.Depths[depth] += 1 + stats.Children[len(children[f.UID])] += 1 + } + + b, err := json.MarshalIndent(stats, "", " ") + if err != nil { + return nil, err + } + return &supportbundles.SupportItem{ + Filename: "folders.json", + FileBytes: b, + }, nil +} diff --git a/pkg/services/folder/folderimpl/folder_test.go b/pkg/services/folder/folderimpl/folder_test.go index 2599259f8b..e23fe64f20 100644 --- a/pkg/services/folder/folderimpl/folder_test.go +++ b/pkg/services/folder/folderimpl/folder_test.go @@ -2,6 +2,7 @@ package folderimpl import ( "context" + "encoding/json" "errors" "fmt" "math/rand" @@ -40,6 +41,7 @@ import ( "github.com/grafana/grafana/pkg/services/quota/quotatest" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/store/entity" + "github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest" "github.com/grafana/grafana/pkg/services/tag/tagimpl" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" @@ -57,7 +59,7 @@ func TestIntegrationProvideFolderService(t *testing.T) { cfg := setting.NewCfg() ac := acmock.New() db := sqlstore.InitTestDB(t) - ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), cfg, nil, nil, db, &featuremgmt.FeatureManager{}, nil) + ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), cfg, nil, nil, db, &featuremgmt.FeatureManager{}, supportbundlestest.NewFakeBundleService(), nil) require.Len(t, ac.Calls.RegisterAttributeScopeResolver, 3) }) @@ -1798,6 +1800,70 @@ func TestFolderServiceGetFolders(t *testing.T) { }) } +func TestSupportBundle(t *testing.T) { + f := func(uid, parent string) *folder.Folder { return &folder.Folder{UID: uid, ParentUID: parent} } + for _, tc := range []struct { + Folders []*folder.Folder + ExpectedTotal int + ExpectedDepths map[int]int + ExpectedChildren map[int]int + }{ + // Empty folder list + { + Folders: []*folder.Folder{}, + ExpectedTotal: 0, + ExpectedDepths: map[int]int{}, + ExpectedChildren: map[int]int{}, + }, + // Single folder + { + Folders: []*folder.Folder{f("a", "")}, + ExpectedTotal: 1, + ExpectedDepths: map[int]int{1: 1}, + ExpectedChildren: map[int]int{0: 1}, + }, + // Flat folders + { + Folders: []*folder.Folder{f("a", ""), f("b", ""), f("c", "")}, + ExpectedTotal: 3, + ExpectedDepths: map[int]int{1: 3}, + ExpectedChildren: map[int]int{0: 3}, + }, + // Nested folders + { + Folders: []*folder.Folder{f("a", ""), f("ab", "a"), f("ac", "a"), f("x", ""), f("xy", "x"), f("xyz", "xy")}, + ExpectedTotal: 6, + ExpectedDepths: map[int]int{1: 2, 2: 3, 3: 1}, + ExpectedChildren: map[int]int{0: 3, 1: 2, 2: 1}, + }, + } { + svc := &Service{} + supportItem, err := svc.supportItemFromFolders(tc.Folders) + if err != nil { + t.Fatal(err) + } + + stats := struct { + Total int `json:"total"` + Depths map[int]int `json:"depths"` + Children map[int]int `json:"children"` + }{} + if err := json.Unmarshal(supportItem.FileBytes, &stats); err != nil { + t.Fatal(err) + } + + if stats.Total != tc.ExpectedTotal { + t.Error("Total mismatch", stats, tc) + } + if fmt.Sprint(stats.Depths) != fmt.Sprint(tc.ExpectedDepths) { + t.Error("Depths mismatch", stats, tc.ExpectedDepths) + } + if fmt.Sprint(stats.Children) != fmt.Sprint(tc.ExpectedChildren) { + t.Error("Depths mismatch", stats, tc.ExpectedChildren) + } + } +} + func CreateSubtreeInStore(t *testing.T, store store, service *Service, depth int, prefix string, cmd folder.CreateFolderCommand) []*folder.Folder { t.Helper() diff --git a/pkg/services/folder/folderimpl/sqlstore.go b/pkg/services/folder/folderimpl/sqlstore.go index 71aa645586..e283c05a95 100644 --- a/pkg/services/folder/folderimpl/sqlstore.go +++ b/pkg/services/folder/folderimpl/sqlstore.go @@ -464,8 +464,11 @@ func (ss *sqlStore) GetFolders(ctx context.Context, q getFoldersQuery) ([]*folde s.WriteString(getFullpathJoinsSQL()) } // covered by UQE_folder_org_id_uid - s.WriteString(` WHERE f0.org_id=?`) - args := []any{q.OrgID} + args := []any{} + if q.OrgID > 0 { + s.WriteString(` WHERE f0.org_id=?`) + args = []any{q.OrgID} + } if len(partialUIDs) > 0 { s.WriteString(` AND f0.uid IN (?` + strings.Repeat(", ?", len(partialUIDs)-1) + `)`) for _, uid := range partialUIDs { diff --git a/pkg/services/libraryelements/libraryelements_test.go b/pkg/services/libraryelements/libraryelements_test.go index 861c68a011..1bb0b8abbc 100644 --- a/pkg/services/libraryelements/libraryelements_test.go +++ b/pkg/services/libraryelements/libraryelements_test.go @@ -327,7 +327,7 @@ func createFolder(t *testing.T, sc scenarioContext, title string) *folder.Folder require.NoError(t, err) folderStore := folderimpl.ProvideDashboardFolderStore(sc.sqlStore) - s := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), cfg, dashboardStore, folderStore, sc.sqlStore, features, nil) + s := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), cfg, dashboardStore, folderStore, sc.sqlStore, features, supportbundlestest.NewFakeBundleService(), nil) t.Logf("Creating folder with title and UID %q", title) ctx := appcontext.WithUser(context.Background(), &sc.user) folder, err := s.Create(ctx, &folder.CreateFolderCommand{ @@ -460,7 +460,7 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo Cfg: sqlStore.Cfg, features: featuremgmt.WithFeatures(), SQLStore: sqlStore, - folderService: folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), sqlStore.Cfg, dashboardStore, folderStore, sqlStore, features, nil), + folderService: folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), sqlStore.Cfg, dashboardStore, folderStore, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil), } // deliberate difference between signed in user and user in db to make it crystal clear diff --git a/pkg/services/librarypanels/librarypanels_test.go b/pkg/services/librarypanels/librarypanels_test.go index 9c57b3f1d4..dae0821acf 100644 --- a/pkg/services/librarypanels/librarypanels_test.go +++ b/pkg/services/librarypanels/librarypanels_test.go @@ -759,7 +759,7 @@ func createFolder(t *testing.T, sc scenarioContext, title string) *folder.Folder dashboardStore, err := database.ProvideDashboardStore(sc.sqlStore, cfg, features, tagimpl.ProvideService(sc.sqlStore), quotaService) require.NoError(t, err) folderStore := folderimpl.ProvideDashboardFolderStore(sc.sqlStore) - s := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), cfg, dashboardStore, folderStore, sc.sqlStore, features, nil) + s := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), cfg, dashboardStore, folderStore, sc.sqlStore, features, supportbundlestest.NewFakeBundleService(), nil) t.Logf("Creating folder with title and UID %q", title) ctx := appcontext.WithUser(context.Background(), sc.user) @@ -841,7 +841,7 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo dashboardStore, err := database.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore), quotaService) require.NoError(t, err) features := featuremgmt.WithFeatures() - folderService := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), cfg, dashboardStore, folderStore, sqlStore, features, nil) + folderService := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), cfg, dashboardStore, folderStore, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil) elementService := libraryelements.ProvideService(cfg, sqlStore, routing.NewRouteRegister(), folderService, featuremgmt.WithFeatures(), ac) service := LibraryPanelService{ diff --git a/pkg/services/ngalert/migration/store/testing.go b/pkg/services/ngalert/migration/store/testing.go index 1c00676e79..6eafec7a38 100644 --- a/pkg/services/ngalert/migration/store/testing.go +++ b/pkg/services/ngalert/migration/store/testing.go @@ -30,6 +30,7 @@ import ( "github.com/grafana/grafana/pkg/services/quota/quotatest" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/supportbundles/bundleregistry" + "github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest" "github.com/grafana/grafana/pkg/services/tag/tagimpl" "github.com/grafana/grafana/pkg/services/team/teamimpl" "github.com/grafana/grafana/pkg/services/user/userimpl" @@ -69,7 +70,7 @@ func NewTestMigrationStore(t testing.TB, sqlStore *sqlstore.SQLStore, cfg *setti dashboardStore, err := database.ProvideDashboardStore(sqlStore, sqlStore.Cfg, features, tagimpl.ProvideService(sqlStore), quotaService) require.NoError(t, err) - folderService := folderimpl.ProvideService(ac, bus, cfg, dashboardStore, folderStore, sqlStore, features, nil) + folderService := folderimpl.ProvideService(ac, bus, cfg, dashboardStore, folderStore, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil) err = folderService.RegisterService(alertingStore) require.NoError(t, err) diff --git a/pkg/services/ngalert/testutil/testutil.go b/pkg/services/ngalert/testutil/testutil.go index 455aca4e0e..e86166a5eb 100644 --- a/pkg/services/ngalert/testutil/testutil.go +++ b/pkg/services/ngalert/testutil/testutil.go @@ -20,13 +20,14 @@ import ( "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/quota/quotatest" "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest" "github.com/grafana/grafana/pkg/services/tag/tagimpl" "github.com/grafana/grafana/pkg/setting" ) func SetupFolderService(tb testing.TB, cfg *setting.Cfg, db db.DB, dashboardStore dashboards.Store, folderStore *folderimpl.DashboardFolderStoreImpl, bus *bus.InProcBus, features featuremgmt.FeatureToggles, ac accesscontrol.AccessControl) folder.Service { tb.Helper() - return folderimpl.ProvideService(ac, bus, cfg, dashboardStore, folderStore, db, features, nil) + return folderimpl.ProvideService(ac, bus, cfg, dashboardStore, folderStore, db, features, supportbundlestest.NewFakeBundleService(), nil) } func SetupDashboardService(tb testing.TB, sqlStore *sqlstore.SQLStore, fs *folderimpl.DashboardFolderStoreImpl, cfg *setting.Cfg) (*dashboardservice.DashboardServiceImpl, dashboards.Store) { diff --git a/pkg/services/sqlstore/permissions/dashboard_test.go b/pkg/services/sqlstore/permissions/dashboard_test.go index 48613ab14c..00a77a7964 100644 --- a/pkg/services/sqlstore/permissions/dashboard_test.go +++ b/pkg/services/sqlstore/permissions/dashboard_test.go @@ -29,6 +29,7 @@ import ( "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/sqlstore/permissions" "github.com/grafana/grafana/pkg/services/sqlstore/searchstore" + "github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest" "github.com/grafana/grafana/pkg/services/tag/tagimpl" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/tests/testsuite" @@ -702,7 +703,7 @@ func setupNestedTest(t *testing.T, usr *user.SignedInUser, perms []accesscontrol dashStore, err := database.ProvideDashboardStore(db, db.Cfg, features, tagimpl.ProvideService(db), quotatest.New(false, nil)) require.NoError(t, err) - folderSvc := folderimpl.ProvideService(mock.New(), bus.ProvideBus(tracing.InitializeTracerForTest()), db.Cfg, dashStore, folderimpl.ProvideDashboardFolderStore(db), db, features, nil) + folderSvc := folderimpl.ProvideService(mock.New(), bus.ProvideBus(tracing.InitializeTracerForTest()), db.Cfg, dashStore, folderimpl.ProvideDashboardFolderStore(db), db, features, supportbundlestest.NewFakeBundleService(), nil) // create parent folder parent, err := folderSvc.Create(context.Background(), &folder.CreateFolderCommand{ diff --git a/pkg/services/sqlstore/permissions/dashboards_bench_test.go b/pkg/services/sqlstore/permissions/dashboards_bench_test.go index 28083e5cdd..ca804da246 100644 --- a/pkg/services/sqlstore/permissions/dashboards_bench_test.go +++ b/pkg/services/sqlstore/permissions/dashboards_bench_test.go @@ -27,6 +27,7 @@ import ( "github.com/grafana/grafana/pkg/services/quota/quotatest" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/sqlstore/permissions" + "github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest" "github.com/grafana/grafana/pkg/services/tag/tagimpl" "github.com/grafana/grafana/pkg/services/user" ) @@ -83,7 +84,7 @@ func setupBenchMark(b *testing.B, usr user.SignedInUser, features featuremgmt.Fe dashboardWriteStore, err := database.ProvideDashboardStore(store, store.Cfg, features, tagimpl.ProvideService(store), quotaService) require.NoError(b, err) - folderSvc := folderimpl.ProvideService(mock.New(), bus.ProvideBus(tracing.InitializeTracerForTest()), store.Cfg, dashboardWriteStore, folderimpl.ProvideDashboardFolderStore(store), store, features, nil) + folderSvc := folderimpl.ProvideService(mock.New(), bus.ProvideBus(tracing.InitializeTracerForTest()), store.Cfg, dashboardWriteStore, folderimpl.ProvideDashboardFolderStore(store), store, features, supportbundlestest.NewFakeBundleService(), nil) origNewGuardian := guardian.New guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanViewValue: true, CanSaveValue: true}) From 80d6bf6da0b2f51f0197f9ae6eda7e7cc89c2d9d Mon Sep 17 00:00:00 2001 From: Gabriel MABILLE Date: Mon, 26 Feb 2024 11:29:09 +0100 Subject: [PATCH 0161/1406] AuthN: Remove embedded oauth server (#83146) * AuthN: Remove embedded oauth server * Restore main * go mod tidy * Fix problem * Remove permission intersection * Fix test and lint * Fix TestData test * Revert to origin/main * Update go.mod * Update go.mod * Update go.sum --- .../developers/plugins/plugin.schema.json | 26 - .../feature-toggles/index.md | 1 - go.mod | 25 +- go.sum | 704 ----------------- .../src/types/featureToggles.gen.ts | 1 - .../oauth-external-registration/plugin.json | 37 - pkg/plugins/pfs/pfs_test.go | 3 - pkg/plugins/plugindef/plugindef.cue | 14 - pkg/plugins/plugindef/plugindef_types_gen.go | 14 - pkg/server/wire.go | 4 - pkg/services/accesscontrol/accesscontrol.go | 104 --- .../accesscontrol/accesscontrol_test.go | 208 ----- pkg/services/accesscontrol/acimpl/service.go | 4 +- .../accesscontrol/acimpl/service_test.go | 4 +- pkg/services/accesscontrol/models.go | 5 +- pkg/services/authn/authnimpl/service.go | 10 +- pkg/services/authn/clients/basic.go | 5 - pkg/services/authn/clients/basic_test.go | 6 - pkg/services/authn/clients/ext_jwt.go | 9 +- pkg/services/authn/clients/ext_jwt_test.go | 36 +- pkg/services/extsvcauth/models.go | 15 - .../extsvcauth/oauthserver/api/api.go | 37 - pkg/services/extsvcauth/oauthserver/errors.go | 25 - .../oauthserver/external_service.go | 153 ---- .../oauthserver/external_service_test.go | 213 ----- pkg/services/extsvcauth/oauthserver/models.go | 58 -- .../oauthserver/oasimpl/aggregate_store.go | 162 ---- .../oasimpl/aggregate_store_test.go | 119 --- .../oauthserver/oasimpl/introspection.go | 21 - .../extsvcauth/oauthserver/oasimpl/service.go | 500 ------------ .../oauthserver/oasimpl/service_test.go | 625 --------------- .../extsvcauth/oauthserver/oasimpl/session.go | 16 - .../extsvcauth/oauthserver/oasimpl/token.go | 353 --------- .../oauthserver/oasimpl/token_test.go | 745 ------------------ .../extsvcauth/oauthserver/oastest/fakes.go | 38 - .../oauthserver/oastest/store_mock.go | 191 ----- .../extsvcauth/oauthserver/store/database.go | 252 ------ .../oauthserver/store/database_test.go | 490 ------------ .../extsvcauth/oauthserver/utils/utils.go | 35 - .../oauthserver/utils/utils_test.go | 82 -- pkg/services/extsvcauth/registry/service.go | 38 +- .../extsvcauth/registry/service_test.go | 63 +- pkg/services/featuremgmt/registry.go | 7 - pkg/services/featuremgmt/toggles_gen.csv | 1 - pkg/services/featuremgmt/toggles_gen.go | 4 - pkg/services/featuremgmt/toggles_gen.json | 3 +- .../pluginsintegration/loader/loader_test.go | 109 +-- .../serviceregistration.go | 27 +- pkg/services/serviceaccounts/api/api.go | 2 +- .../serviceaccounts/extsvcaccounts/service.go | 12 +- pkg/services/serviceaccounts/proxy/service.go | 2 +- .../sqlstore/migrations/migrations.go | 4 - .../migrations/oauthserver/migrations.go | 52 -- .../ServiceAccountsListPage.tsx | 2 +- scripts/modowners/README.md | 1 - 55 files changed, 46 insertions(+), 5631 deletions(-) delete mode 100644 pkg/plugins/manager/testdata/oauth-external-registration/plugin.json delete mode 100644 pkg/services/extsvcauth/oauthserver/api/api.go delete mode 100644 pkg/services/extsvcauth/oauthserver/errors.go delete mode 100644 pkg/services/extsvcauth/oauthserver/external_service.go delete mode 100644 pkg/services/extsvcauth/oauthserver/external_service_test.go delete mode 100644 pkg/services/extsvcauth/oauthserver/models.go delete mode 100644 pkg/services/extsvcauth/oauthserver/oasimpl/aggregate_store.go delete mode 100644 pkg/services/extsvcauth/oauthserver/oasimpl/aggregate_store_test.go delete mode 100644 pkg/services/extsvcauth/oauthserver/oasimpl/introspection.go delete mode 100644 pkg/services/extsvcauth/oauthserver/oasimpl/service.go delete mode 100644 pkg/services/extsvcauth/oauthserver/oasimpl/service_test.go delete mode 100644 pkg/services/extsvcauth/oauthserver/oasimpl/session.go delete mode 100644 pkg/services/extsvcauth/oauthserver/oasimpl/token.go delete mode 100644 pkg/services/extsvcauth/oauthserver/oasimpl/token_test.go delete mode 100644 pkg/services/extsvcauth/oauthserver/oastest/fakes.go delete mode 100644 pkg/services/extsvcauth/oauthserver/oastest/store_mock.go delete mode 100644 pkg/services/extsvcauth/oauthserver/store/database.go delete mode 100644 pkg/services/extsvcauth/oauthserver/store/database_test.go delete mode 100644 pkg/services/extsvcauth/oauthserver/utils/utils.go delete mode 100644 pkg/services/extsvcauth/oauthserver/utils/utils_test.go delete mode 100644 pkg/services/sqlstore/migrations/oauthserver/migrations.go diff --git a/docs/sources/developers/plugins/plugin.schema.json b/docs/sources/developers/plugins/plugin.schema.json index 57f14a4f35..4947f96e0f 100644 --- a/docs/sources/developers/plugins/plugin.schema.json +++ b/docs/sources/developers/plugins/plugin.schema.json @@ -505,32 +505,6 @@ } } } - }, - "impersonation": { - "type": "object", - "description": "Impersonation describes the permissions that the plugin will be restricted to when acting on behalf of the user.", - "properties": { - "groups": { - "type": "boolean", - "description": "Groups allows the service to list the impersonated user's teams." - }, - "permissions": { - "type": "array", - "description": "Permissions are the permissions that the plugin needs when impersonating a user. The intersection of this set with the impersonated user's permission guarantees that the client will not gain more privileges than the impersonated user has.", - "items": { - "type": "object", - "additionalProperties": false, - "properties": { - "action": { - "type": "string" - }, - "scope": { - "type": "string" - } - } - } - } - } } } }, 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 3825032dfd..0c10036bdd 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -185,7 +185,6 @@ The following toggles require explicitly setting Grafana's [app mode]({{< relref | Feature toggle name | Description | | -------------------------------------- | -------------------------------------------------------------- | | `unifiedStorage` | SQL-based k8s storage | -| `externalServiceAuth` | Starts an OAuth2 authentication provider for external services | | `grafanaAPIServerWithExperimentalAPIs` | Register experimental APIs with the k8s API server | | `grafanaAPIServerEnsureKubectlAccess` | Start an additional https handler and write kubectl options | | `kubernetesQueryServiceRewrite` | Rewrite requests targeting /ds/query to the query service | diff --git a/go.mod b/go.mod index cea0701592..48d2484a67 100644 --- a/go.mod +++ b/go.mod @@ -121,7 +121,7 @@ require ( gopkg.in/mail.v2 v2.3.1 // @grafana/backend-platform gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // @grafana/alerting-squad-backend - xorm.io/builder v0.3.6 // indirect; @grafana/backend-platform + xorm.io/builder v0.3.6 // @grafana/backend-platform xorm.io/core v0.7.3 // @grafana/backend-platform xorm.io/xorm v0.8.2 // @grafana/alerting-squad-backend ) @@ -173,7 +173,7 @@ require ( github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-msgpack v0.5.5 // indirect - github.com/hashicorp/go-multierror v1.1.1 // indirect; @grafana/alerting-squad + github.com/hashicorp/go-multierror v1.1.1 // @grafana/alerting-squad github.com/hashicorp/go-sockaddr v1.0.6 // indirect github.com/hashicorp/golang-lru v0.6.0 // indirect github.com/hashicorp/yamux v0.1.1 // indirect @@ -263,12 +263,10 @@ require ( github.com/grafana/tempo v1.5.1-0.20230524121406-1dc1bfe7085b // @grafana/observability-traces-and-profiling github.com/grafana/thema v0.0.0-20230712153715-375c1b45f3ed // @grafana/grafana-as-code github.com/microsoft/go-mssqldb v1.6.1-0.20240214161942-b65008136246 // @grafana/grafana-bi-squad - github.com/ory/fosite v0.44.1-0.20230317114349-45a6785cc54f // @grafana/grafana-authnz-team github.com/redis/go-redis/v9 v9.0.2 // @grafana/alerting-squad-backend github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // @grafana/grafana-as-code go.opentelemetry.io/contrib/samplers/jaegerremote v0.16.0 // @grafana/backend-platform golang.org/x/mod v0.14.0 // @grafana/backend-platform - gopkg.in/square/go-jose.v2 v2.6.0 // @grafana/grafana-authnz-team k8s.io/utils v0.0.0-20230726121419-3b25d923346b // @grafana/partner-datasources ) @@ -315,11 +313,7 @@ require ( github.com/cockroachdb/redact v1.1.3 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect - github.com/cristalhq/jwt/v4 v4.0.2 // indirect - github.com/dave/jennifer v1.5.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/dgraph-io/ristretto v0.1.1 // indirect - github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect github.com/docker/distribution v2.8.2+incompatible // indirect github.com/docker/go-connections v0.4.0 // indirect github.com/drone-runners/drone-runner-docker v1.8.2 // indirect @@ -327,7 +321,6 @@ require ( github.com/drone/envsubst v1.0.3 // indirect github.com/drone/runner-go v1.12.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect - github.com/ecordell/optgen v0.0.6 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/evanphx/json-patch v5.6.0+incompatible // indirect github.com/felixge/httpsnoop v1.0.4 // indirect @@ -344,11 +337,8 @@ require ( github.com/google/s2a-go v0.1.7 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect github.com/grafana/regexp v0.0.0-20221123153739-15dc172cd2db // indirect - github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-immutable-radix v1.3.1 // indirect - github.com/hashicorp/go-retryablehttp v0.7.4 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // @grafana/alerting-squad-backend - github.com/hashicorp/hcl v1.0.0 // indirect github.com/hashicorp/memberlist v0.5.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/invopop/yaml v0.2.0 // indirect @@ -356,10 +346,8 @@ require ( github.com/klauspost/cpuid/v2 v2.2.5 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect - github.com/magiconair/properties v1.8.6 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-ieproxy v0.0.3 // indirect - github.com/mattn/goveralls v0.0.6 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/mapstructure v1.5.0 //@grafana/grafana-authnz-team github.com/mitchellh/reflectwalk v1.0.2 // indirect @@ -368,12 +356,6 @@ require ( github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.0.3-0.20220512140940-7b36cea86235 // indirect github.com/opentracing-contrib/go-stdlib v1.0.0 // indirect - github.com/ory/go-acc v0.2.6 // indirect - github.com/ory/go-convenience v0.1.0 // indirect - github.com/ory/viper v1.7.5 // indirect - github.com/ory/x v0.0.214 // indirect - github.com/pborman/uuid v1.2.0 // indirect - github.com/pelletier/go-toml v1.9.5 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/redis/rueidis v1.0.16 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect @@ -382,12 +364,9 @@ require ( github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/segmentio/asm v1.2.0 // indirect github.com/shopspring/decimal v1.2.0 // indirect - github.com/spf13/afero v1.9.2 // indirect github.com/spf13/cast v1.5.0 // indirect - github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // @grafana-app-platform-squad github.com/stoewer/go-strcase v1.3.0 // indirect - github.com/subosito/gotenv v1.4.1 // indirect github.com/unknwon/bra v0.0.0-20200517080246-1e3013ecaff8 // indirect github.com/unknwon/com v1.0.1 // indirect github.com/unknwon/log v0.0.0-20150304194804-e617c87089d3 // indirect diff --git a/go.sum b/go.sum index 98dd5431b4..9474ee87d9 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,3 @@ -bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8= buf.build/gen/go/grpc-ecosystem/grpc-gateway/protocolbuffers/go v1.28.1-20221127060915-a1ecdc58eccd.4/go.mod h1:92ejKVTiuvnKoAtRlpJpIxKfloI935DDqhs0NCRx+KM= buf.build/gen/go/parca-dev/parca/bufbuild/connect-go v1.4.1-20221222094228-8b1d3d0f62e6.1 h1:wQ75SnlaD0X30PnrmA+07A/5fnQWrAHy1mzv+CPB5Oo= buf.build/gen/go/parca-dev/parca/bufbuild/connect-go v1.4.1-20221222094228-8b1d3d0f62e6.1/go.mod h1:VYzBTKhjl92cl3sv+xznQcJHCezU7qnI0FhBAUb4n8c= @@ -8,7 +7,6 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.37.4/go.mod h1:NHPJ89PdicEuT9hdPXMROBD91xc5uRDxsMtSB16k7hw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.41.0/go.mod h1:OauMR7DV8fzvZIl2qg6rkaIhD/vmgk4iwEw/h6ercmg= cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= @@ -1283,9 +1281,7 @@ github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3 github.com/CloudyKit/jet/v3 v3.0.0/go.mod h1:HKQPgSJmdK8hdoAbKUUWajkHyHo4RaU5rMdUywE7VMo= github.com/Code-Hex/go-generics-cache v1.3.1 h1:i8rLwyhoyhaerr7JpjtYjJZUcCbWOdiYO3fZXLiEC4g= github.com/Code-Hex/go-generics-cache v1.3.1/go.mod h1:qxcC9kRVrct9rHeiYpFWSoW1vxyillCVzX13KZG8dl4= -github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= -github.com/DataDog/datadog-go v4.0.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/FZambia/eagle v0.1.0 h1:9gyX6x+xjoIfglgyPTcYm7dvY7FJ93us1QY5De4CyXA= github.com/FZambia/eagle v0.1.0/go.mod h1:YjGSPVkQTNcVLfzEUQJNgW9ScPR0K4u/Ky0yeFa4oDA= github.com/GoogleCloudPlatform/cloudsql-proxy v1.29.0/go.mod h1:spvB9eLJH9dutlbPSRmHvSXXHOwGRyeXh1jVdquA2G8= @@ -1297,17 +1293,14 @@ github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKz github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= -github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= -github.com/Masterminds/semver/v3 v3.0.3/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Masterminds/sprig/v3 v3.2.1/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk= github.com/Masterminds/sprig/v3 v3.2.2 h1:17jRggJu518dr3QaafizSXOjKYp94wKfABxUmyxvxX8= github.com/Masterminds/sprig/v3 v3.2.2/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk= github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= -github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= @@ -1317,7 +1310,6 @@ github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8 github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 h1:kkhsdkhsCvIsutKu5zLMgWtgh9YxGCNAw8Ad8hjwfYg= github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= -github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= @@ -1335,7 +1327,6 @@ github.com/VividCortex/mysqlerr v0.0.0-20170204212430-6c6b55f8796f/go.mod h1:f3H github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8= github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= -github.com/ajg/form v0.0.0-20160822230020-523a5da1a92f/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY= github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk= @@ -1388,17 +1379,13 @@ github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgI github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= -github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= -github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= -github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= github.com/aws/aws-sdk-go v1.15.27/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0= github.com/aws/aws-sdk-go v1.17.7/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= -github.com/aws/aws-sdk-go v1.23.19/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.37.0/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= github.com/aws/aws-sdk-go v1.38.35/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= @@ -1447,7 +1434,6 @@ github.com/aws/aws-sdk-go-v2/service/sso v1.11.3 h1:frW4ikGcxfAEDfmQqWgMLp+F1n4n github.com/aws/aws-sdk-go-v2/service/sso v1.11.3/go.mod h1:7UQ/e69kU7LDPtY40OyoHYgRmgfGM4mgsLYtcObdveU= github.com/aws/aws-sdk-go-v2/service/sts v1.16.3 h1:cJGRyzCSVwZC7zZZ1xbx9m32UnrKydRYhOvcD1NYP9Q= github.com/aws/aws-sdk-go-v2/service/sts v1.16.3/go.mod h1:bfBj0iVmsUyUg4weDB4NxktD9rDGeKSVWnjTnwbx9b8= -github.com/aws/aws-xray-sdk-go v0.9.4/go.mod h1:XtMKdBQfpVut+tJEwI7+dJFRxxRdxHDyVNp2tHXRq04= github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= github.com/aws/smithy-go v1.11.2 h1:eG/N+CcUMAvsdffgMvjMKwfyDzIkjM6pfxMJ8Mzc6mE= github.com/aws/smithy-go v1.11.2/go.mod h1:3xHYmszWVx2c0kIwQeEVf9uSm4fYZt67FBJnwub1bgM= @@ -1492,7 +1478,6 @@ github.com/blugelabs/ice v1.0.0 h1:um7wf9e6jbkTVCrOyQq3tKK43fBMOvLUYxbj3Qtc4eo= github.com/blugelabs/ice v1.0.0/go.mod h1:gNfFPk5zM+yxJROhthxhVQYjpBO9amuxWXJQ2Lo+IbQ= github.com/bmatcuk/doublestar v1.1.1 h1:YroD6BJCZBYx06yYFEWvUuKVWQn3vLLQAVmDmvTSaiQ= github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= -github.com/bmatcuk/doublestar/v2 v2.0.3/go.mod h1:QMmcs3H2AUQICWhfzLXz+IYln8lRQmTZRptLie8RgRw= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= @@ -1515,9 +1500,7 @@ github.com/caio/go-tdigest v3.1.0+incompatible h1:uoVMJ3Q5lXmVLCCqaMGHLBWnbGoN6L github.com/caio/go-tdigest v3.1.0+incompatible/go.mod h1:sHQM/ubZStBUmF1WbB8FAm8q9GjDajLC5T7ydxE3JHI= github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= github.com/casbin/casbin/v2 v2.37.0/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg= -github.com/cenkalti/backoff v2.1.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= -github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= @@ -1545,7 +1528,6 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= -github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575/go.mod h1:9d6lWj8KzO/fd/NrVaLscBKmPigpZpn5YawRPw+e3Yo= github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/clbanning/mxj v1.8.4/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5PVGJng= @@ -1574,8 +1556,6 @@ github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMe github.com/cockroachdb/apd/v2 v2.0.2 h1:weh8u7Cneje73dDh+2tEVLUvyBc89iwepWCD8b8034E= github.com/cockroachdb/apd/v2 v2.0.2/go.mod h1:DDxRlzC2lo3/vSlmSoS7JkqbbrARPuFOGr0B9pvN3Gw= github.com/cockroachdb/cockroach-go v0.0.0-20181001143604-e0a95dfd547c/go.mod h1:XGLbWH/ujMcbPbhZq52Nv6UrCghb1yGn//133kEsvDk= -github.com/cockroachdb/cockroach-go v0.0.0-20190925194419-606b3d062051/go.mod h1:XGLbWH/ujMcbPbhZq52Nv6UrCghb1yGn//133kEsvDk= -github.com/cockroachdb/cockroach-go v0.0.0-20200312223839-f565e4789405/go.mod h1:XGLbWH/ujMcbPbhZq52Nv6UrCghb1yGn//133kEsvDk= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= github.com/cockroachdb/datadriven v1.0.2/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU= github.com/cockroachdb/errors v1.9.1 h1:yFVvsI0VxmRShfawbt/laCIDy/mtTqqnvoNgiy5bEV8= @@ -1586,14 +1566,8 @@ github.com/cockroachdb/redact v1.1.3 h1:AKZds10rFSIj7qADf0g46UixK8NNLwWTNdCIGS5w github.com/cockroachdb/redact v1.1.3/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM= -github.com/codegangsta/negroni v1.0.0/go.mod h1:v0y3T5G7Y1UlFfyxFn/QLRU4a2EuNau2iZY63YTKWo0= github.com/containerd/containerd v1.2.7/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= github.com/containerd/containerd v1.3.4/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.4.3/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/continuity v0.0.0-20181203112020-004b46473808/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= -github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= -github.com/containerd/continuity v0.0.0-20200107194136-26c1120b8d41/go.mod h1:Dq467ZllaHgAtVp4p1xUQWBrFXR9s/wyoTpG8zOJGkY= -github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= @@ -1607,19 +1581,14 @@ github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/cristalhq/jwt/v4 v4.0.2 h1:g/AD3h0VicDamtlM70GWGElp8kssQEv+5wYd7L9WOhU= -github.com/cristalhq/jwt/v4 v4.0.2/go.mod h1:HnYraSNKDRag1DZP92rYHyrjyQHnVEHPNqesmzs+miQ= -github.com/cucumber/godog v0.8.1/go.mod h1:vSh3r/lM+psC1BPXvdkSEuNjmXfpVqrMGYAElF6hxnA= github.com/cznic/b v0.0.0-20180115125044-35e9bbe41f07/go.mod h1:URriBxXwVq5ijiJ12C7iIZqlA69nTlI+LgI6/pwftG8= github.com/cznic/fileutil v0.0.0-20180108211300-6a051e75936f/go.mod h1:8S58EK26zhXSxzv7NQFpnliaOQsmDUxvoQO3rt154Vg= github.com/cznic/golex v0.0.0-20170803123110-4ab7c5e190e4/go.mod h1:+bmmJDNmKlhWNG+gwWCkaBoTy39Fs+bzRxVBzoTQbIc= @@ -1630,18 +1599,10 @@ github.com/cznic/ql v1.2.0/go.mod h1:FbpzhyZrqr0PVlK6ury+PoW3T0ODUV22OeWIxcaOrSE github.com/cznic/sortutil v0.0.0-20150617083342-4c7342852e65/go.mod h1:q2w6Bg5jeox1B+QkJ6Wp/+Vn0G/bo3f1uY7Fn3vivIQ= github.com/cznic/strutil v0.0.0-20171016134553-529a34b1c186/go.mod h1:AHHPPPXTw0h6pVabbcbyGRK1DckRn7r/STdZEeIDzZc= github.com/cznic/zappy v0.0.0-20160723133515-2533cb5b45cc/go.mod h1:Y1SNZ4dRUOKXshKUbwUapqNncRrho4mkjQebgEHZLj8= -github.com/dave/astrid v0.0.0-20170323122508-8c2895878b14/go.mod h1:Sth2QfxfATb/nW4EsrSi2KyJmbcniZ8TgTaji17D6ms= -github.com/dave/brenda v1.1.0/go.mod h1:4wCUr6gSlu5/1Tk7akE5X7UorwiQ8Rij0SKH3/BGMOM= -github.com/dave/courtney v0.3.0/go.mod h1:BAv3hA06AYfNUjfjQr+5gc6vxeBVOupLqrColj+QSD8= github.com/dave/dst v0.27.2 h1:4Y5VFTkhGLC1oddtNwuxxe36pnyLxMFXT51FOzH8Ekc= github.com/dave/dst v0.27.2/go.mod h1:jHh6EOibnHgcUW3WjKHisiooEkYwqpHLBSX1iOBhEyc= -github.com/dave/gopackages v0.0.0-20170318123100-46e7023ec56e/go.mod h1:i00+b/gKdIDIxuLDFob7ustLAVqhsZRk2qVZrArELGQ= -github.com/dave/jennifer v1.4.0/go.mod h1:fIb+770HOpJ2fmN9EPPKOqm1vMGhB+TwXKMZhrIygKg= github.com/dave/jennifer v1.5.0 h1:HmgPN93bVDpkQyYbqhCHj5QlgvUkvEOzMyEvKLgCRrg= github.com/dave/jennifer v1.5.0/go.mod h1:4MnyiFIlZS3l5tSDn8VnzE6ffAhYBMB2SZntBsZGUok= -github.com/dave/kerr v0.0.0-20170318121727-bc25dd6abe8e/go.mod h1:qZqlPyPvfsDJt+3wHJ1EvSXDuVjFTK0j2p/ca+gtsb8= -github.com/dave/patsy v0.0.0-20210517141501-957256f50cba/go.mod h1:qfR88CgEGLoiqDaE+xxDCi5QA5v4vUoW0UCX2Nd5Tlc= -github.com/dave/rebecca v0.9.1/go.mod h1:N6XYdMD/OKw3lkF3ywh8Z6wPGuwNFDNtWYEMFWEmXBA= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -1657,20 +1618,13 @@ github.com/dennwc/varint v1.0.0 h1:kGNFFSSw8ToIy3obO/kKr8U9GZYUAxQEVuix4zfDWzE= github.com/dennwc/varint v1.0.0/go.mod h1:hnItb35rvZvJrbTALZtY/iQfDs48JKRG1RPpgziApxA= github.com/devigned/tab v0.1.1/go.mod h1:XG9mPq0dFghrYvoBF3xdRrJzSTX1b7IQrvaL9mzjeJY= github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4= -github.com/dgraph-io/ristretto v0.0.1/go.mod h1:T40EBc7CJke8TkpiYfGGKAeFjSaxuFXhuXRyumBd6RE= -github.com/dgraph-io/ristretto v0.0.2/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= -github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8= -github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= -github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y= -github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dgryski/go-metro v0.0.0-20180109044635-280f6062b5bc/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw= github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 h1:y7y0Oa6UawqTFPCDw9JG6pdKt4F9pAhHv0B7FMGaGD0= github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= -github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dhui/dktest v0.3.0/go.mod h1:cyzIUfGsBEbZ6BT7tnXqAShHSXCZhSNmFl70sZ7c1yc= github.com/digitalocean/godo v1.106.0 h1:m5iErwl3xHovGFlawd50n54ntgXHt1BLsvU6BXsVxEU= github.com/digitalocean/godo v1.106.0/go.mod h1:R6EmmWI8CT1+fCtjWY9UCB+L5uufuZH13wk3YhxycCs= @@ -1706,23 +1660,17 @@ github.com/drone/runner-go v1.12.0 h1:zUjDj9ylsJ4n4Mvy4znddq/Z4EBzcUXzTltpzokKtg github.com/drone/runner-go v1.12.0/go.mod h1:vu4pPPYDoeN6vdYQAY01GGGsAIW4aLganJNaa8Fx8zE= github.com/drone/signal v1.0.0/go.mod h1:S8t92eFT0g4WUgEc/LxG+LCuiskpMNsG0ajAMGnyZpc= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/dustin/go-humanize v0.0.0-20180713052910-9f541cc9db5d/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= -github.com/ecordell/optgen v0.0.6 h1:aSknPe6ZUBrjwHGp2+6XfmfCGYGD6W0ZDfCmmsrS7s4= -github.com/ecordell/optgen v0.0.6/go.mod h1:bAPkLVWcBlTX5EkXW0UTPRj3+yjq2I6VLgH8OasuQEM= github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= github.com/edsrzf/mmap-go v1.1.0 h1:6EUwBLQ/Mcr1EYLE4Tn1VdW1A4ckqCQWZBw8Hr0kjpQ= github.com/edsrzf/mmap-go v1.1.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q= github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM= -github.com/elastic/go-sysinfo v1.1.1/go.mod h1:i1ZYdU10oLNfRzq4vq62BEwD2fH8KaWh6eh0ikPT9F0= -github.com/elastic/go-windows v1.0.0/go.mod h1:TsU0Nrp7/y3+VwE82FoZF8gC/XFg/Elz6CcloAxnPgU= -github.com/elazarl/goproxy v0.0.0-20181003060214-f58a169a71a5/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/elazarl/goproxy v0.0.0-20230731152917-f99041a5c027 h1:1L0aalTpPz7YlMxETKpmQoWMBkeiuorElZIXoNmgiPE= github.com/elazarl/goproxy v0.0.0-20230731152917-f99041a5c027/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8= @@ -1771,7 +1719,6 @@ github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYF github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= -github.com/fatih/structs v1.0.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= @@ -1809,11 +1756,8 @@ github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM= github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= github.com/gin-gonic/gin v1.7.3/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= -github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= -github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A= github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= -github.com/go-bindata/go-bindata v3.1.1+incompatible/go.mod h1:xK8Dsgwmeed+BBsSy2XTopBn/8uK2HWuGSnA11C3Joo= github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= @@ -1861,34 +1805,18 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre github.com/go-logr/zapr v1.2.3 h1:a9vnzlIBPQBBkeaR9IuMUfmVOrQlkoC4YfPoFkX3T7A= github.com/go-logr/zapr v1.2.3/go.mod h1:eIauM6P8qSvTw5o2ez6UEAfGjQKrxQTl5EoK+Qa2oG4= github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8= -github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI= -github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= -github.com/go-openapi/analysis v0.18.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= -github.com/go-openapi/analysis v0.19.2/go.mod h1:3P1osvZa9jKjb8ed2TPng3f0i/UY9snX6gxi44djMjk= -github.com/go-openapi/analysis v0.19.4/go.mod h1:3P1osvZa9jKjb8ed2TPng3f0i/UY9snX6gxi44djMjk= -github.com/go-openapi/analysis v0.19.5/go.mod h1:hkEAkxagaIvIP7VTn8ygJNkd4kAYON2rCu0v0ObL0AU= -github.com/go-openapi/analysis v0.19.10/go.mod h1:qmhS3VNFxBlquFJ0RGoDtylO9y4pgTAUNE9AEEMdlJQ= github.com/go-openapi/analysis v0.21.2/go.mod h1:HZwRk4RRisyG8vx2Oe6aqeSQcoxRp47Xkp3+K6q+LdY= github.com/go-openapi/analysis v0.21.4/go.mod h1:4zQ35W4neeZTqh3ol0rv/O8JBbka9QyAgQRPp9y3pfo= github.com/go-openapi/analysis v0.21.5/go.mod h1:25YcZosX9Lwz2wBsrFrrsL8bmjjXdlyP6zsr2AMy29M= github.com/go-openapi/analysis v0.22.0/go.mod h1:acDnkkCI2QxIo8sSIPgmp1wUlRohV7vfGtAIVae73b0= github.com/go-openapi/analysis v0.22.2 h1:ZBmNoP2h5omLKr/srIC9bfqrUGzT6g6gNv03HE9Vpj0= github.com/go-openapi/analysis v0.22.2/go.mod h1:pDF4UbZsQTo/oNuRfAWWd4dAh4yuYf//LYorPTjrpvo= -github.com/go-openapi/errors v0.17.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= -github.com/go-openapi/errors v0.18.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= -github.com/go-openapi/errors v0.19.2/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94= -github.com/go-openapi/errors v0.19.3/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94= -github.com/go-openapi/errors v0.19.6/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= github.com/go-openapi/errors v0.19.8/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= github.com/go-openapi/errors v0.19.9/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= -github.com/go-openapi/errors v0.20.0/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= github.com/go-openapi/errors v0.20.2/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= github.com/go-openapi/errors v0.20.4/go.mod h1:Z3FlZ4I8jEGxjUK+bugx3on2mIAk4txuAOhlsB1FSgk= github.com/go-openapi/errors v0.21.0 h1:FhChC/duCnfoLj1gZ0BgaBmzhJC2SL/sJr8a2vAobSY= github.com/go-openapi/errors v0.21.0/go.mod h1:jxNTMUxRCKj65yb/okJGEtahVd7uvWnuWfj53bse4ho= -github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= -github.com/go-openapi/jsonpointer v0.18.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= -github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= @@ -1896,10 +1824,6 @@ github.com/go-openapi/jsonpointer v0.20.0/go.mod h1:6PGzBjjIIumbLYysB73Klnms1mwn github.com/go-openapi/jsonpointer v0.20.1/go.mod h1:bHen+N0u1KEO3YlmqOjTT9Adn1RfD91Ar825/PuiRVs= github.com/go-openapi/jsonpointer v0.20.2 h1:mQc3nmndL8ZBzStEo3JYF8wzmeWffDH4VbXz58sAx6Q= github.com/go-openapi/jsonpointer v0.20.2/go.mod h1:bHen+N0u1KEO3YlmqOjTT9Adn1RfD91Ar825/PuiRVs= -github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= -github.com/go-openapi/jsonreference v0.18.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= -github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= -github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= github.com/go-openapi/jsonreference v0.20.1/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= @@ -1907,30 +1831,13 @@ github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En github.com/go-openapi/jsonreference v0.20.3/go.mod h1:FviDZ46i9ivh810gqzFLl5NttD5q3tSlMLqLr6okedM= github.com/go-openapi/jsonreference v0.20.4 h1:bKlDxQxQJgwpUSgOENiMPzCTBVuc7vTdXSSgNeAhojU= github.com/go-openapi/jsonreference v0.20.4/go.mod h1:5pZJyJP2MnYCpoeoMAql78cCHauHj0V9Lhc506VOpw4= -github.com/go-openapi/loads v0.17.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= -github.com/go-openapi/loads v0.18.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= -github.com/go-openapi/loads v0.19.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= -github.com/go-openapi/loads v0.19.2/go.mod h1:QAskZPMX5V0C2gvfkGZzJlINuP7Hx/4+ix5jWFxsNPs= -github.com/go-openapi/loads v0.19.3/go.mod h1:YVfqhUCdahYwR3f3iiwQLhicVRvLlU/WO5WPaZvcvSI= -github.com/go-openapi/loads v0.19.5/go.mod h1:dswLCAdonkRufe/gSUC3gN8nTSaB9uaS2es0x5/IbjY= github.com/go-openapi/loads v0.21.1/go.mod h1:/DtAMXXneXFjbQMGEtbamCZb+4x7eGwkvZCvBmwUG+g= github.com/go-openapi/loads v0.21.2/go.mod h1:Jq58Os6SSGz0rzh62ptiu8Z31I+OTHqmULx5e/gJbNw= github.com/go-openapi/loads v0.21.3/go.mod h1:Y3aMR24iHbKHppOj91nQ/SHc0cuPbAr4ndY4a02xydc= github.com/go-openapi/loads v0.21.5 h1:jDzF4dSoHw6ZFADCGltDb2lE4F6De7aWSpe+IcsRzT0= github.com/go-openapi/loads v0.21.5/go.mod h1:PxTsnFBoBe+z89riT+wYt3prmSBP6GDAQh2l9H1Flz8= -github.com/go-openapi/runtime v0.0.0-20180920151709-4f900dc2ade9/go.mod h1:6v9a6LTXWQCdL8k1AO3cvqx5OtZY/Y9wKTgaoP6YRfA= -github.com/go-openapi/runtime v0.19.0/go.mod h1:OwNfisksmmaZse4+gpV3Ne9AyMOlP1lt4sK4FXt0O64= -github.com/go-openapi/runtime v0.19.4/go.mod h1:X277bwSUBxVlCYR3r7xgZZGKVvBd/29gLDlFGtJ8NL4= -github.com/go-openapi/runtime v0.19.15/go.mod h1:dhGWCTKRXlAfGnQG0ONViOZpjfg0m2gUt9nTQPQZuoo= -github.com/go-openapi/runtime v0.19.26/go.mod h1:BvrQtn6iVb2QmiVXRsFAm6ZCAZBpbVKFfN6QWCp582M= github.com/go-openapi/runtime v0.27.1 h1:ae53yaOoh+fx/X5Eaq8cRmavHgDma65XPZuvBqvJYto= github.com/go-openapi/runtime v0.27.1/go.mod h1:fijeJEiEclyS8BRurYE1DE5TLb9/KZl6eAdbzjsrlLU= -github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= -github.com/go-openapi/spec v0.18.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= -github.com/go-openapi/spec v0.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcsZsD5zxMY= -github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= -github.com/go-openapi/spec v0.19.6/go.mod h1:Hm2Jr4jv8G1ciIAo+frC/Ft+rR2kQDh8JHKHb3gWUSk= -github.com/go-openapi/spec v0.19.8/go.mod h1:Hm2Jr4jv8G1ciIAo+frC/Ft+rR2kQDh8JHKHb3gWUSk= github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= github.com/go-openapi/spec v0.20.9/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= @@ -1938,13 +1845,6 @@ github.com/go-openapi/spec v0.20.12/go.mod h1:iSCgnBcwbMW9SfzJb8iYynXvcY6C/QFrI7 github.com/go-openapi/spec v0.20.13/go.mod h1:8EOhTpBoFiask8rrgwbLC3zmJfz4zsCUueRuPM6GNkw= github.com/go-openapi/spec v0.20.14 h1:7CBlRnw+mtjFGlPDRZmAMnq35cRzI91xj03HVyUi/Do= github.com/go-openapi/spec v0.20.14/go.mod h1:8EOhTpBoFiask8rrgwbLC3zmJfz4zsCUueRuPM6GNkw= -github.com/go-openapi/strfmt v0.17.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= -github.com/go-openapi/strfmt v0.18.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= -github.com/go-openapi/strfmt v0.19.0/go.mod h1:+uW+93UVvGGq2qGaZxdDeJqSAqBqBdl+ZPMF/cC8nDY= -github.com/go-openapi/strfmt v0.19.2/go.mod h1:0yX7dbo8mKIvc3XSKp7MNfxw4JytCfCD6+bY1AVL9LU= -github.com/go-openapi/strfmt v0.19.3/go.mod h1:0yX7dbo8mKIvc3XSKp7MNfxw4JytCfCD6+bY1AVL9LU= -github.com/go-openapi/strfmt v0.19.4/go.mod h1:eftuHTlB/dI8Uq8JJOyRlieZf+WkkxUuk0dgdHXr2Qk= -github.com/go-openapi/strfmt v0.19.5/go.mod h1:eftuHTlB/dI8Uq8JJOyRlieZf+WkkxUuk0dgdHXr2Qk= github.com/go-openapi/strfmt v0.21.0/go.mod h1:ZRQ409bWMj+SOgXofQAGTIo2Ebu72Gs+WaRADcS5iNg= github.com/go-openapi/strfmt v0.21.1/go.mod h1:I/XVKeLc5+MM5oPNN7P6urMOpuLXEcNrCX/rPGuWb0k= github.com/go-openapi/strfmt v0.21.3/go.mod h1:k+RzNO0Da+k3FrrynSNN8F7n/peCmQQqbbXjtDfvmGg= @@ -1952,12 +1852,7 @@ github.com/go-openapi/strfmt v0.21.9/go.mod h1:0k3v301mglEaZRJdDDGSlN6Npq4VMVU69 github.com/go-openapi/strfmt v0.21.10/go.mod h1:vNDMwbilnl7xKiO/Ve/8H8Bb2JIInBnH+lqiw6QWgis= github.com/go-openapi/strfmt v0.22.0 h1:Ew9PnEYc246TwrEspvBdDHS4BVKXy/AOVsfqGDgAcaI= github.com/go-openapi/strfmt v0.22.0/go.mod h1:HzJ9kokGIju3/K6ap8jL+OlGAbjpSv27135Yr9OivU4= -github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= -github.com/go-openapi/swag v0.18.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= -github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-openapi/swag v0.19.7/go.mod h1:ao+8BpOPyKdpQz3AOJfbeEVpLmWAvlT1IfTe5McPyhY= -github.com/go-openapi/swag v0.19.9/go.mod h1:ao+8BpOPyKdpQz3AOJfbeEVpLmWAvlT1IfTe5McPyhY= github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-openapi/swag v0.21.1/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= @@ -1966,10 +1861,6 @@ github.com/go-openapi/swag v0.22.5/go.mod h1:Gl91UqO+btAM0plGGxHqJcQZ1ZTy6jbmrid github.com/go-openapi/swag v0.22.6/go.mod h1:Gl91UqO+btAM0plGGxHqJcQZ1ZTy6jbmridBTsDy8A0= github.com/go-openapi/swag v0.22.9 h1:XX2DssF+mQKM2DHsbgZK74y/zj4mo9I99+89xUmuZCE= github.com/go-openapi/swag v0.22.9/go.mod h1:3/OXnFfnMAwBD099SwYRk7GD3xOrr1iL7d/XNLXVVwE= -github.com/go-openapi/validate v0.18.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4= -github.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2KDnRCRMUi7GTA= -github.com/go-openapi/validate v0.19.3/go.mod h1:90Vh6jjkTn+OT1Eefm0ZixWNFjhtOH7vS9k0lo6zwJo= -github.com/go-openapi/validate v0.19.10/go.mod h1:RKEZTUWDkxKQxN2jDT7ZnZi2bhZlbNMAuKvKB+IaGx8= github.com/go-openapi/validate v0.22.1/go.mod h1:rjnrwK57VJ7A8xqfpAOEKRH8yQSGUriMu5/zuPSQ1hg= github.com/go-openapi/validate v0.22.4/go.mod h1:qm6O8ZIcPVdSY5219468Jv7kBdGvkiZLPOmqnqTUZ2A= github.com/go-openapi/validate v0.23.0 h1:2l7PJLzCis4YUGEoW6eoQw3WhyM65WSIcjX6SQnlfDw= @@ -1990,7 +1881,6 @@ github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyL github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= -github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= @@ -2008,253 +1898,29 @@ github.com/go-zookeeper/zk v1.0.2/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL github.com/go-zookeeper/zk v1.0.3 h1:7M2kwOsc//9VeeFiPtf+uSJlVpU66x9Ba5+8XK7/TDg= github.com/go-zookeeper/zk v1.0.3/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL+UX1Qcw= github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0= -github.com/gobuffalo/attrs v0.1.0/go.mod h1:fmNpaWyHM0tRm8gCZWKx8yY9fvaNLo2PyzBNSrBZ5Hw= -github.com/gobuffalo/buffalo v0.12.8-0.20181004233540-fac9bb505aa8/go.mod h1:sLyT7/dceRXJUxSsE813JTQtA3Eb1vjxWfo/N//vXIY= -github.com/gobuffalo/buffalo v0.13.0/go.mod h1:Mjn1Ba9wpIbpbrD+lIDMy99pQ0H0LiddMIIDGse7qT4= -github.com/gobuffalo/buffalo-plugins v1.0.2/go.mod h1:pOp/uF7X3IShFHyobahTkTLZaeUXwb0GrUTb9ngJWTs= -github.com/gobuffalo/buffalo-plugins v1.0.4/go.mod h1:pWS1vjtQ6uD17MVFWf7i3zfThrEKWlI5+PYLw/NaDB4= -github.com/gobuffalo/buffalo-plugins v1.4.3/go.mod h1:uCzTY0woez4nDMdQjkcOYKanngeUVRO2HZi7ezmAjWY= -github.com/gobuffalo/buffalo-plugins v1.5.1/go.mod h1:jbmwSZK5+PiAP9cC09VQOrGMZFCa/P0UMlIS3O12r5w= -github.com/gobuffalo/buffalo-plugins v1.6.4/go.mod h1:/+N1aophkA2jZ1ifB2O3Y9yGwu6gKOVMtUmJnbg+OZI= -github.com/gobuffalo/buffalo-plugins v1.6.5/go.mod h1:0HVkbgrVs/MnPZ/FOseDMVanCTm2RNcdM0PuXcL1NNI= -github.com/gobuffalo/buffalo-plugins v1.6.7/go.mod h1:ZGZRkzz2PiKWHs0z7QsPBOTo2EpcGRArMEym6ghKYgk= -github.com/gobuffalo/buffalo-plugins v1.6.9/go.mod h1:yYlYTrPdMCz+6/+UaXg5Jm4gN3xhsvsQ2ygVatZV5vw= -github.com/gobuffalo/buffalo-plugins v1.6.11/go.mod h1:eAA6xJIL8OuynJZ8amXjRmHND6YiusVAaJdHDN1Lu8Q= -github.com/gobuffalo/buffalo-plugins v1.8.2/go.mod h1:9te6/VjEQ7pKp7lXlDIMqzxgGpjlKoAcAANdCgoR960= -github.com/gobuffalo/buffalo-plugins v1.8.3/go.mod h1:IAWq6vjZJVXebIq2qGTLOdlXzmpyTZ5iJG5b59fza5U= -github.com/gobuffalo/buffalo-plugins v1.9.4/go.mod h1:grCV6DGsQlVzQwk6XdgcL3ZPgLm9BVxlBmXPMF8oBHI= -github.com/gobuffalo/buffalo-plugins v1.10.0/go.mod h1:4osg8d9s60txLuGwXnqH+RCjPHj9K466cDFRl3PErHI= -github.com/gobuffalo/buffalo-plugins v1.11.0/go.mod h1:rtIvAYRjYibgmWhnjKmo7OadtnxuMG5ZQLr25ozAzjg= -github.com/gobuffalo/buffalo-plugins v1.15.0/go.mod h1:BqSx01nwgKUQr/MArXzFkSD0QvdJidiky1OKgyfgrK8= -github.com/gobuffalo/buffalo-pop v1.0.5/go.mod h1:Fw/LfFDnSmB/vvQXPvcXEjzP98Tc+AudyNWUBWKCwQ8= github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY= github.com/gobuffalo/depgen v0.1.0/go.mod h1:+ifsuy7fhi15RWncXQQKjWS9JPkdah5sZvtHc2RXGlg= -github.com/gobuffalo/envy v1.6.4/go.mod h1:Abh+Jfw475/NWtYMEt+hnJWRiC8INKWibIMyNt1w2Mc= -github.com/gobuffalo/envy v1.6.5/go.mod h1:N+GkhhZ/93bGZc6ZKhJLP6+m+tCNPKwgSpH9kaifseQ= -github.com/gobuffalo/envy v1.6.6/go.mod h1:N+GkhhZ/93bGZc6ZKhJLP6+m+tCNPKwgSpH9kaifseQ= -github.com/gobuffalo/envy v1.6.7/go.mod h1:N+GkhhZ/93bGZc6ZKhJLP6+m+tCNPKwgSpH9kaifseQ= -github.com/gobuffalo/envy v1.6.8/go.mod h1:N+GkhhZ/93bGZc6ZKhJLP6+m+tCNPKwgSpH9kaifseQ= -github.com/gobuffalo/envy v1.6.9/go.mod h1:N+GkhhZ/93bGZc6ZKhJLP6+m+tCNPKwgSpH9kaifseQ= -github.com/gobuffalo/envy v1.6.10/go.mod h1:X0CFllQjTV5ogsnUrg+Oks2yTI+PU2dGYBJOEI2D1Uo= -github.com/gobuffalo/envy v1.6.11/go.mod h1:Fiq52W7nrHGDggFPhn2ZCcHw4u/rqXkqo+i7FB6EAcg= -github.com/gobuffalo/envy v1.6.12/go.mod h1:qJNrJhKkZpEW0glh5xP2syQHH5kgdmgsKss2Kk8PTP0= github.com/gobuffalo/envy v1.6.15/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= -github.com/gobuffalo/envy v1.7.1/go.mod h1:FurDp9+EDPE4aIUS3ZLyD+7/9fpx7YRt/ukY6jIHf0w= -github.com/gobuffalo/envy v1.8.1/go.mod h1:FurDp9+EDPE4aIUS3ZLyD+7/9fpx7YRt/ukY6jIHf0w= -github.com/gobuffalo/envy v1.9.0/go.mod h1:FurDp9+EDPE4aIUS3ZLyD+7/9fpx7YRt/ukY6jIHf0w= -github.com/gobuffalo/events v1.0.3/go.mod h1:Txo8WmqScapa7zimEQIwgiJBvMECMe9gJjsKNPN3uZw= -github.com/gobuffalo/events v1.0.7/go.mod h1:z8txf6H9jWhQ5Scr7YPLWg/cgXBRj8Q4uYI+rsVCCSQ= -github.com/gobuffalo/events v1.0.8/go.mod h1:A5KyqT1sA+3GJiBE4QKZibse9mtOcI9nw8gGrDdqYGs= -github.com/gobuffalo/events v1.1.3/go.mod h1:9yPGWYv11GENtzrIRApwQRMYSbUgCsZ1w6R503fCfrk= -github.com/gobuffalo/events v1.1.4/go.mod h1:09/YRRgZHEOts5Isov+g9X2xajxdvOAcUuAHIX/O//A= -github.com/gobuffalo/events v1.1.5/go.mod h1:3YUSzgHfYctSjEjLCWbkXP6djH2M+MLaVRzb4ymbAK0= -github.com/gobuffalo/events v1.1.7/go.mod h1:6fGqxH2ing5XMb3EYRq9LEkVlyPGs4oO/eLzh+S8CxY= -github.com/gobuffalo/events v1.1.8/go.mod h1:UFy+W6X6VbCWS8k2iT81HYX65dMtiuVycMy04cplt/8= -github.com/gobuffalo/events v1.1.9/go.mod h1:/0nf8lMtP5TkgNbzYxR6Bl4GzBy5s5TebgNTdRfRbPM= -github.com/gobuffalo/events v1.3.1/go.mod h1:9JOkQVoyRtailYVE/JJ2ZQ/6i4gTjM5t2HsZK4C1cSA= -github.com/gobuffalo/events v1.4.1/go.mod h1:SjXgWKpeSuvQDvGhgMz5IXx3Czu+IbL+XPLR41NvVQY= -github.com/gobuffalo/fizz v1.0.12/go.mod h1:C0sltPxpYK8Ftvf64kbsQa2yiCZY4RZviurNxXdAKwc= -github.com/gobuffalo/fizz v1.9.8/go.mod h1:w1FEn1yKNVCc49KnADGyYGRPH7jFON3ak4Bj1yUudHo= -github.com/gobuffalo/fizz v1.10.0/go.mod h1:J2XGPO0AfJ1zKw7+2BA+6FEGAkyEsdCOLvN93WCT2WI= -github.com/gobuffalo/flect v0.0.0-20180907193754-dc14d8acaf9f/go.mod h1:rCiQgmAE4axgBNl3jZWzS5rETRYTGOsrixTRaCPzNdA= -github.com/gobuffalo/flect v0.0.0-20181002182613-4571df4b1daf/go.mod h1:rCiQgmAE4axgBNl3jZWzS5rETRYTGOsrixTRaCPzNdA= -github.com/gobuffalo/flect v0.0.0-20181007231023-ae7ed6bfe683/go.mod h1:rCiQgmAE4axgBNl3jZWzS5rETRYTGOsrixTRaCPzNdA= -github.com/gobuffalo/flect v0.0.0-20181018182602-fd24a256709f/go.mod h1:rCiQgmAE4axgBNl3jZWzS5rETRYTGOsrixTRaCPzNdA= -github.com/gobuffalo/flect v0.0.0-20181019110701-3d6f0b585514/go.mod h1:rCiQgmAE4axgBNl3jZWzS5rETRYTGOsrixTRaCPzNdA= -github.com/gobuffalo/flect v0.0.0-20181024204909-8f6be1a8c6c2/go.mod h1:rCiQgmAE4axgBNl3jZWzS5rETRYTGOsrixTRaCPzNdA= -github.com/gobuffalo/flect v0.0.0-20181104133451-1f6e9779237a/go.mod h1:rCiQgmAE4axgBNl3jZWzS5rETRYTGOsrixTRaCPzNdA= -github.com/gobuffalo/flect v0.0.0-20181114183036-47375f6d8328/go.mod h1:0HvNbHdfh+WOvDSIASqJOSxTOWSxCCUF++k/Y53v9rI= -github.com/gobuffalo/flect v0.0.0-20181210151238-24a2b68e0316/go.mod h1:en58vff74S9b99Eg42Dr+/9yPu437QjlNsO/hBYPuOk= -github.com/gobuffalo/flect v0.0.0-20190104192022-4af577e09bf2/go.mod h1:en58vff74S9b99Eg42Dr+/9yPu437QjlNsO/hBYPuOk= -github.com/gobuffalo/flect v0.0.0-20190117212819-a62e61d96794/go.mod h1:397QT6v05LkZkn07oJXXT6y9FCfwC8Pug0WA2/2mE9k= github.com/gobuffalo/flect v0.1.0/go.mod h1:d2ehjJqGOH/Kjqcoz+F7jHTBbmDb38yXA598Hb50EGs= github.com/gobuffalo/flect v0.1.1/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= github.com/gobuffalo/flect v0.1.3/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= -github.com/gobuffalo/flect v0.1.5/go.mod h1:W3K3X9ksuZfir8f/LrfVtWmCDQFfayuylOJ7sz/Fj80= -github.com/gobuffalo/flect v0.2.0/go.mod h1:W3K3X9ksuZfir8f/LrfVtWmCDQFfayuylOJ7sz/Fj80= -github.com/gobuffalo/flect v0.2.1/go.mod h1:vmkQwuZYhN5Pc4ljYQZzP+1sq+NEkK+lh20jmEmX3jc= -github.com/gobuffalo/genny v0.0.0-20180924032338-7af3a40f2252/go.mod h1:tUTQOogrr7tAQnhajMSH6rv1BVev34H2sa1xNHMy94g= -github.com/gobuffalo/genny v0.0.0-20181003150629-3786a0744c5d/go.mod h1:WAd8HmjMVrnkAZbmfgH5dLBUchsZfqzp/WS5sQz+uTM= -github.com/gobuffalo/genny v0.0.0-20181005145118-318a41a134cc/go.mod h1:WAd8HmjMVrnkAZbmfgH5dLBUchsZfqzp/WS5sQz+uTM= -github.com/gobuffalo/genny v0.0.0-20181007153042-b8de7d566757/go.mod h1:+oG5Ljrw04czAHbPXREwaFojJbpUvcIy4DiOnbEJFTA= -github.com/gobuffalo/genny v0.0.0-20181012161047-33e5f43d83a6/go.mod h1:+oG5Ljrw04czAHbPXREwaFojJbpUvcIy4DiOnbEJFTA= -github.com/gobuffalo/genny v0.0.0-20181017160347-90a774534246/go.mod h1:+oG5Ljrw04czAHbPXREwaFojJbpUvcIy4DiOnbEJFTA= -github.com/gobuffalo/genny v0.0.0-20181024195656-51392254bf53/go.mod h1:o9GEH5gn5sCKLVB5rHFC4tq40rQ3VRUzmx6WwmaqISE= -github.com/gobuffalo/genny v0.0.0-20181025145300-af3f81d526b8/go.mod h1:uZ1fFYvdcP8mu0B/Ynarf6dsGvp7QFIpk/QACUuFUVI= -github.com/gobuffalo/genny v0.0.0-20181027191429-94d6cfb5c7fc/go.mod h1:x7SkrQQBx204Y+O9EwRXeszLJDTaWN0GnEasxgLrQTA= -github.com/gobuffalo/genny v0.0.0-20181027195209-3887b7171c4f/go.mod h1:JbKx8HSWICu5zyqWOa0dVV1pbbXOHusrSzQUprW6g+w= -github.com/gobuffalo/genny v0.0.0-20181106193839-7dcb0924caf1/go.mod h1:x61yHxvbDCgQ/7cOAbJCacZQuHgB0KMSzoYcw5debjU= -github.com/gobuffalo/genny v0.0.0-20181107223128-f18346459dbe/go.mod h1:utQD3aKKEsdb03oR+Vi/6ztQb1j7pO10N3OBoowRcSU= -github.com/gobuffalo/genny v0.0.0-20181114215459-0a4decd77f5d/go.mod h1:kN2KZ8VgXF9VIIOj/GM0Eo7YK+un4Q3tTreKOf0q1ng= -github.com/gobuffalo/genny v0.0.0-20181119162812-e8ff4adce8bb/go.mod h1:BA9htSe4bZwBDJLe8CUkoqkypq3hn3+CkoHqVOW718E= -github.com/gobuffalo/genny v0.0.0-20181127225641-2d959acc795b/go.mod h1:l54xLXNkteX/PdZ+HlgPk1qtcrgeOr3XUBBPDbH+7CQ= -github.com/gobuffalo/genny v0.0.0-20181128191930-77e34f71ba2a/go.mod h1:FW/D9p7cEEOqxYA71/hnrkOWm62JZ5ZNxcNIVJEaWBU= -github.com/gobuffalo/genny v0.0.0-20181203165245-fda8bcce96b1/go.mod h1:wpNSANu9UErftfiaAlz1pDZclrYzLtO5lALifODyjuM= -github.com/gobuffalo/genny v0.0.0-20181203201232-849d2c9534ea/go.mod h1:wpNSANu9UErftfiaAlz1pDZclrYzLtO5lALifODyjuM= -github.com/gobuffalo/genny v0.0.0-20181206121324-d6fb8a0dbe36/go.mod h1:wpNSANu9UErftfiaAlz1pDZclrYzLtO5lALifODyjuM= -github.com/gobuffalo/genny v0.0.0-20181207164119-84844398a37d/go.mod h1:y0ysCHGGQf2T3vOhCrGHheYN54Y/REj0ayd0Suf4C/8= -github.com/gobuffalo/genny v0.0.0-20181211165820-e26c8466f14d/go.mod h1:sHnK+ZSU4e2feXP3PA29ouij6PUEiN+RCwECjCTB3yM= -github.com/gobuffalo/genny v0.0.0-20190104222617-a71664fc38e7/go.mod h1:QPsQ1FnhEsiU8f+O0qKWXz2RE4TiDqLVChWkBuh1WaY= -github.com/gobuffalo/genny v0.0.0-20190112155932-f31a84fcacf5/go.mod h1:CIaHCrSIuJ4il6ka3Hub4DR4adDrGoXGEEt2FbBxoIo= github.com/gobuffalo/genny v0.0.0-20190329151137-27723ad26ef9/go.mod h1:rWs4Z12d1Zbf19rlsn0nurr75KqhYp52EAGGxTbBhNk= github.com/gobuffalo/genny v0.0.0-20190403191548-3ca520ef0d9e/go.mod h1:80lIj3kVJWwOrXWWMRzzdhW3DsrdjILVil/SFKBzF28= github.com/gobuffalo/genny v0.1.0/go.mod h1:XidbUqzak3lHdS//TPu2OgiFB+51Ur5f7CSnXZ/JDvo= github.com/gobuffalo/genny v0.1.1/go.mod h1:5TExbEyY48pfunL4QSXxlDOmdsD44RRq4mVZ0Ex28Xk= -github.com/gobuffalo/genny v0.2.0/go.mod h1:rWs4Z12d1Zbf19rlsn0nurr75KqhYp52EAGGxTbBhNk= -github.com/gobuffalo/genny v0.3.0/go.mod h1:ywJ2CoXrTZj7rbS8HTbzv7uybnLKlsNSBhEQ+yFI3E8= -github.com/gobuffalo/genny v0.6.0/go.mod h1:Vigx9VDiNscYpa/LwrURqGXLSIbzTfapt9+K6gF1kTA= -github.com/gobuffalo/genny/v2 v2.0.5/go.mod h1:kRkJuAw9mdI37AiEYjV4Dl+TgkBDYf8HZVjLkqe5eBg= github.com/gobuffalo/gitgen v0.0.0-20190315122116-cc086187d211/go.mod h1:vEHJk/E9DmhejeLeNt7UVvlSGv3ziL+djtTr3yyzcOw= -github.com/gobuffalo/github_flavored_markdown v1.0.4/go.mod h1:uRowCdK+q8d/RF0Kt3/DSalaIXbb0De/dmTqMQdkQ4I= -github.com/gobuffalo/github_flavored_markdown v1.0.5/go.mod h1:U0643QShPF+OF2tJvYNiYDLDGDuQmJZXsf/bHOJPsMY= -github.com/gobuffalo/github_flavored_markdown v1.0.7/go.mod h1:w93Pd9Lz6LvyQXEG6DktTPHkOtCbr+arAD5mkwMzXLI= -github.com/gobuffalo/github_flavored_markdown v1.1.0/go.mod h1:TSpTKWcRTI0+v7W3x8dkSKMLJSUpuVitlptCkpeY8ic= github.com/gobuffalo/gogen v0.0.0-20190315121717-8f38393713f5/go.mod h1:V9QVDIxsgKNZs6L2IYiGR8datgMhB577vzTDqypH360= github.com/gobuffalo/gogen v0.1.0/go.mod h1:8NTelM5qd8RZ15VjQTFkAW6qOMx5wBbW4dSCS3BY8gg= github.com/gobuffalo/gogen v0.1.1/go.mod h1:y8iBtmHmGc4qa3urIyo1shvOD8JftTtfcKi+71xfDNE= -github.com/gobuffalo/gogen v0.2.0/go.mod h1:V9QVDIxsgKNZs6L2IYiGR8datgMhB577vzTDqypH360= -github.com/gobuffalo/helpers v0.2.2/go.mod h1:xYbzUdCUpVzLwLnqV8HIjT6hmG0Cs7YIBCJkNM597jw= -github.com/gobuffalo/helpers v0.2.4/go.mod h1:NX7v27yxPDOPTgUFYmJ5ow37EbxdoLraucOGvMNawyk= -github.com/gobuffalo/helpers v0.5.0/go.mod h1:stpgxJ2C7T99NLyAxGUnYMM2zAtBk5NKQR0SIbd05j4= -github.com/gobuffalo/helpers v0.6.0/go.mod h1:pncVrer7x/KRvnL5aJABLAuT/RhKRR9klL6dkUOhyv8= -github.com/gobuffalo/helpers v0.6.1/go.mod h1:wInbDi0vTJKZBviURTLRMFLE4+nF2uRuuL2fnlYo7w4= -github.com/gobuffalo/here v0.6.0/go.mod h1:wAG085dHOYqUpf+Ap+WOdrPTp5IYcDAs/x7PLa8Y5fM= -github.com/gobuffalo/httptest v1.0.2/go.mod h1:7T1IbSrg60ankme0aDLVnEY0h056g9M1/ZvpVThtB7E= -github.com/gobuffalo/licenser v0.0.0-20180924033006-eae28e638a42/go.mod h1:Ubo90Np8gpsSZqNScZZkVXXAo5DGhTb+WYFIjlnog8w= -github.com/gobuffalo/licenser v0.0.0-20181025145548-437d89de4f75/go.mod h1:x3lEpYxkRG/XtGCUNkio+6RZ/dlOvLzTI9M1auIwFcw= -github.com/gobuffalo/licenser v0.0.0-20181027200154-58051a75da95/go.mod h1:BzhaaxGd1tq1+OLKObzgdCV9kqVhbTulxOpYbvMQWS0= -github.com/gobuffalo/licenser v0.0.0-20181109171355-91a2a7aac9a7/go.mod h1:m+Ygox92pi9bdg+gVaycvqE8RVSjZp7mWw75+K5NPHk= -github.com/gobuffalo/licenser v0.0.0-20181128165715-cc7305f8abed/go.mod h1:oU9F9UCE+AzI/MueCKZamsezGOOHfSirltllOVeRTAE= -github.com/gobuffalo/licenser v0.0.0-20181203160806-fe900bbede07/go.mod h1:ph6VDNvOzt1CdfaWC+9XwcBnlSTBz2j49PBwum6RFaU= -github.com/gobuffalo/licenser v0.0.0-20181211173111-f8a311c51159/go.mod h1:ve/Ue99DRuvnTaLq2zKa6F4KtHiYf7W046tDjuGYPfM= -github.com/gobuffalo/licenser v1.1.0/go.mod h1:ZVWE6uKUE3rGf7sedUHWVjNWrEgxaUQLVFL+pQiWpfY= -github.com/gobuffalo/logger v0.0.0-20181022175615-46cfb361fc27/go.mod h1:8sQkgyhWipz1mIctHF4jTxmJh1Vxhp7mP8IqbljgJZo= -github.com/gobuffalo/logger v0.0.0-20181027144941-73d08d2bb969/go.mod h1:7uGg2duHKpWnN4+YmyKBdLXfhopkAdVM6H3nKbyFbz8= -github.com/gobuffalo/logger v0.0.0-20181027193913-9cf4dd0efe46/go.mod h1:7uGg2duHKpWnN4+YmyKBdLXfhopkAdVM6H3nKbyFbz8= -github.com/gobuffalo/logger v0.0.0-20181109185836-3feeab578c17/go.mod h1:oNErH0xLe+utO+OW8ptXMSA5DkiSEDW1u3zGIt8F9Ew= -github.com/gobuffalo/logger v0.0.0-20181117211126-8e9b89b7c264/go.mod h1:5etB91IE0uBlw9k756fVKZJdS+7M7ejVhmpXXiSFj0I= -github.com/gobuffalo/logger v0.0.0-20181127160119-5b956e21995c/go.mod h1:+HxKANrR9VGw9yN3aOAppJKvhO05ctDi63w4mDnKv2U= github.com/gobuffalo/logger v0.0.0-20190315122211-86e12af44bc2/go.mod h1:QdxcLw541hSGtBnhUc4gaNIXRjiDppFGaDqzbrBd3v8= -github.com/gobuffalo/logger v1.0.0/go.mod h1:2zbswyIUa45I+c+FLXuWl9zSWEiVuthsk8ze5s8JvPs= -github.com/gobuffalo/logger v1.0.1/go.mod h1:2zbswyIUa45I+c+FLXuWl9zSWEiVuthsk8ze5s8JvPs= -github.com/gobuffalo/logger v1.0.3/go.mod h1:SoeejUwldiS7ZsyCBphOGURmWdwUFXs0J7TCjEhjKxM= -github.com/gobuffalo/makr v1.1.5/go.mod h1:Y+o0btAH1kYAMDJW/TX3+oAXEu0bmSLLoC9mIFxtzOw= -github.com/gobuffalo/mapi v1.0.0/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= github.com/gobuffalo/mapi v1.0.1/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= github.com/gobuffalo/mapi v1.0.2/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= -github.com/gobuffalo/mapi v1.1.0/go.mod h1:pqQ1XAqvpy/JYtRwoieNps2yU8MFiMxBUpAm2FBtQ50= -github.com/gobuffalo/mapi v1.2.1/go.mod h1:giGJ2AUESRepOFYAzWpq8Gf/s/QDryxoEHisQtFN3cY= -github.com/gobuffalo/meta v0.0.0-20181018155829-df62557efcd3/go.mod h1:XTTOhwMNryif3x9LkTTBO/Llrveezd71u3quLd0u7CM= -github.com/gobuffalo/meta v0.0.0-20181018192820-8c6cef77dab3/go.mod h1:E94EPzx9NERGCY69UWlcj6Hipf2uK/vnfrF4QD0plVE= -github.com/gobuffalo/meta v0.0.0-20181025145500-3a985a084b0a/go.mod h1:YDAKBud2FP7NZdruCSlmTmDOZbVSa6bpK7LJ/A/nlKg= -github.com/gobuffalo/meta v0.0.0-20181114191255-b130ebedd2f7/go.mod h1:K6cRZ29ozr4Btvsqkjvg5nDFTLOgTqf03KA70Ks0ypE= -github.com/gobuffalo/meta v0.0.0-20181127070345-0d7e59dd540b/go.mod h1:RLO7tMvE0IAKAM8wny1aN12pvEKn7EtkBLkUZR00Qf8= -github.com/gobuffalo/meta v0.0.0-20190120163247-50bbb1fa260d/go.mod h1:KKsH44nIK2gA8p0PJmRT9GvWJUdphkDUA8AJEvFWiqM= -github.com/gobuffalo/meta v0.0.0-20190329152330-e161e8a93e3b/go.mod h1:mCRSy5F47tjK8yaIDcJad4oe9fXxY5gLrx3Xx2spK+0= -github.com/gobuffalo/meta v0.3.0/go.mod h1:cpr6mrUX5H/B4wEP86Gdq568TK4+dKUD8oRPl698RUw= -github.com/gobuffalo/mw-basicauth v1.0.3/go.mod h1:dg7+ilMZOKnQFHDefUzUHufNyTswVUviCBgF244C1+0= -github.com/gobuffalo/mw-contenttype v0.0.0-20180802152300-74f5a47f4d56/go.mod h1:7EvcmzBbeCvFtQm5GqF9ys6QnCxz2UM1x0moiWLq1No= -github.com/gobuffalo/mw-csrf v0.0.0-20180802151833-446ff26e108b/go.mod h1:sbGtb8DmDZuDUQoxjr8hG1ZbLtZboD9xsn6p77ppcHo= -github.com/gobuffalo/mw-forcessl v0.0.0-20180802152810-73921ae7a130/go.mod h1:JvNHRj7bYNAMUr/5XMkZaDcw3jZhUZpsmzhd//FFWmQ= -github.com/gobuffalo/mw-i18n v0.0.0-20180802152014-e3060b7e13d6/go.mod h1:91AQfukc52A6hdfIfkxzyr+kpVYDodgAeT5cjX1UIj4= -github.com/gobuffalo/mw-paramlogger v0.0.0-20181005191442-d6ee392ec72e/go.mod h1:6OJr6VwSzgJMqWMj7TYmRUqzNe2LXu/W1rRW4MAz/ME= -github.com/gobuffalo/mw-tokenauth v0.0.0-20181001105134-8545f626c189/go.mod h1:UqBF00IfKvd39ni5+yI5MLMjAf4gX7cDKN/26zDOD6c= -github.com/gobuffalo/nulls v0.2.0/go.mod h1:w4q8RoSCEt87Q0K0sRIZWYeIxkxog5mh3eN3C/n+dUc= -github.com/gobuffalo/nulls v0.3.0/go.mod h1:UP49vd/k+bcaz6m0cHMyuk8oQ7XgLnkfxeiVoPAvBSs= -github.com/gobuffalo/packd v0.0.0-20181027182251-01ad393492c8/go.mod h1:SmdBdhj6uhOsg1Ui4SFAyrhuc7U4VCildosO5IDJ3lc= -github.com/gobuffalo/packd v0.0.0-20181027190505-aafc0d02c411/go.mod h1:SmdBdhj6uhOsg1Ui4SFAyrhuc7U4VCildosO5IDJ3lc= -github.com/gobuffalo/packd v0.0.0-20181027194105-7ae579e6d213/go.mod h1:SmdBdhj6uhOsg1Ui4SFAyrhuc7U4VCildosO5IDJ3lc= -github.com/gobuffalo/packd v0.0.0-20181031195726-c82734870264/go.mod h1:Yf2toFaISlyQrr5TfO3h6DB9pl9mZRmyvBGQb/aQ/pI= -github.com/gobuffalo/packd v0.0.0-20181104210303-d376b15f8e96/go.mod h1:Yf2toFaISlyQrr5TfO3h6DB9pl9mZRmyvBGQb/aQ/pI= -github.com/gobuffalo/packd v0.0.0-20181111195323-b2e760a5f0ff/go.mod h1:Yf2toFaISlyQrr5TfO3h6DB9pl9mZRmyvBGQb/aQ/pI= -github.com/gobuffalo/packd v0.0.0-20181114190715-f25c5d2471d7/go.mod h1:Yf2toFaISlyQrr5TfO3h6DB9pl9mZRmyvBGQb/aQ/pI= -github.com/gobuffalo/packd v0.0.0-20181124090624-311c6248e5fb/go.mod h1:Foenia9ZvITEvG05ab6XpiD5EfBHPL8A6hush8SJ0o8= -github.com/gobuffalo/packd v0.0.0-20181207120301-c49825f8f6f4/go.mod h1:LYc0TGKFBBFTRC9dg2pcRcMqGCTMD7T2BIMP7OBuQAA= -github.com/gobuffalo/packd v0.0.0-20181212173646-eca3b8fd6687/go.mod h1:LYc0TGKFBBFTRC9dg2pcRcMqGCTMD7T2BIMP7OBuQAA= github.com/gobuffalo/packd v0.0.0-20190315124812-a385830c7fc0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= github.com/gobuffalo/packd v0.1.0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= -github.com/gobuffalo/packd v0.2.0/go.mod h1:k2CkHP3bjbqL2GwxwhxUy1DgnlbW644hkLC9iIUvZwY= -github.com/gobuffalo/packd v0.3.0/go.mod h1:zC7QkmNkYVGKPw4tHpBQ+ml7W/3tIebgeo1b36chA3Q= -github.com/gobuffalo/packd v1.0.0/go.mod h1:6VTc4htmJRFB7u1m/4LeMTWjFoYrUiBkU9Fdec9hrhI= -github.com/gobuffalo/packr v1.13.7/go.mod h1:KkinLIn/n6+3tVXMwg6KkNvWwVsrRAz4ph+jgpk3Z24= -github.com/gobuffalo/packr v1.15.0/go.mod h1:t5gXzEhIviQwVlNx/+3SfS07GS+cZ2hn76WLzPp6MGI= -github.com/gobuffalo/packr v1.15.1/go.mod h1:IeqicJ7jm8182yrVmNbM6PR4g79SjN9tZLH8KduZZwE= -github.com/gobuffalo/packr v1.19.0/go.mod h1:MstrNkfCQhd5o+Ct4IJ0skWlxN8emOq8DsoT1G98VIU= -github.com/gobuffalo/packr v1.20.0/go.mod h1:JDytk1t2gP+my1ig7iI4NcVaXr886+N0ecUga6884zw= -github.com/gobuffalo/packr v1.21.0/go.mod h1:H00jGfj1qFKxscFJSw8wcL4hpQtPe1PfU2wa6sg/SR0= -github.com/gobuffalo/packr v1.22.0/go.mod h1:Qr3Wtxr3+HuQEwWqlLnNW4t1oTvK+7Gc/Rnoi/lDFvA= -github.com/gobuffalo/packr/v2 v2.0.0-rc.8/go.mod h1:y60QCdzwuMwO2R49fdQhsjCPv7tLQFR0ayzxxla9zes= -github.com/gobuffalo/packr/v2 v2.0.0-rc.9/go.mod h1:fQqADRfZpEsgkc7c/K7aMew3n4aF1Kji7+lIZeR98Fc= -github.com/gobuffalo/packr/v2 v2.0.0-rc.10/go.mod h1:4CWWn4I5T3v4c1OsJ55HbHlUEKNWMITG5iIkdr4Px4w= -github.com/gobuffalo/packr/v2 v2.0.0-rc.11/go.mod h1:JoieH/3h3U4UmatmV93QmqyPUdf4wVM9HELaHEu+3fk= -github.com/gobuffalo/packr/v2 v2.0.0-rc.12/go.mod h1:FV1zZTsVFi1DSCboO36Xgs4pzCZBjB/tDV9Cz/lSaR8= -github.com/gobuffalo/packr/v2 v2.0.0-rc.13/go.mod h1:2Mp7GhBFMdJlOK8vGfl7SYtfMP3+5roE39ejlfjw0rA= -github.com/gobuffalo/packr/v2 v2.0.0-rc.14/go.mod h1:06otbrNvDKO1eNQ3b8hst+1010UooI2MFg+B2Ze4MV8= -github.com/gobuffalo/packr/v2 v2.0.0-rc.15/go.mod h1:IMe7H2nJvcKXSF90y4X1rjYIRlNMJYCxEhssBXNZwWs= github.com/gobuffalo/packr/v2 v2.0.9/go.mod h1:emmyGweYTm6Kdper+iywB6YK5YzuKchGtJQZ0Odn4pQ= github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0= -github.com/gobuffalo/packr/v2 v2.4.0/go.mod h1:ra341gygw9/61nSjAbfwcwh8IrYL4WmR4IsPkPBhQiY= -github.com/gobuffalo/packr/v2 v2.5.2/go.mod h1:sgEE1xNZ6G0FNN5xn9pevVu4nywaxHvgup67xisti08= -github.com/gobuffalo/packr/v2 v2.7.1/go.mod h1:qYEvAazPaVxy7Y7KR0W8qYEE+RymX74kETFqjFoFlOc= -github.com/gobuffalo/packr/v2 v2.8.0/go.mod h1:PDk2k3vGevNE3SwVyVRgQCCXETC9SaONCNSXT1Q8M1g= -github.com/gobuffalo/plush v3.7.16+incompatible/go.mod h1:rQ4zdtUUyZNqULlc6bqd5scsPfLKfT0+TGMChgduDvI= -github.com/gobuffalo/plush v3.7.20+incompatible/go.mod h1:rQ4zdtUUyZNqULlc6bqd5scsPfLKfT0+TGMChgduDvI= -github.com/gobuffalo/plush v3.7.21+incompatible/go.mod h1:rQ4zdtUUyZNqULlc6bqd5scsPfLKfT0+TGMChgduDvI= -github.com/gobuffalo/plush v3.7.22+incompatible/go.mod h1:rQ4zdtUUyZNqULlc6bqd5scsPfLKfT0+TGMChgduDvI= -github.com/gobuffalo/plush v3.7.23+incompatible/go.mod h1:rQ4zdtUUyZNqULlc6bqd5scsPfLKfT0+TGMChgduDvI= -github.com/gobuffalo/plush v3.7.30+incompatible/go.mod h1:rQ4zdtUUyZNqULlc6bqd5scsPfLKfT0+TGMChgduDvI= -github.com/gobuffalo/plush v3.7.31+incompatible/go.mod h1:rQ4zdtUUyZNqULlc6bqd5scsPfLKfT0+TGMChgduDvI= -github.com/gobuffalo/plush v3.7.32+incompatible/go.mod h1:rQ4zdtUUyZNqULlc6bqd5scsPfLKfT0+TGMChgduDvI= -github.com/gobuffalo/plush v3.8.2+incompatible/go.mod h1:rQ4zdtUUyZNqULlc6bqd5scsPfLKfT0+TGMChgduDvI= -github.com/gobuffalo/plush v3.8.3+incompatible/go.mod h1:rQ4zdtUUyZNqULlc6bqd5scsPfLKfT0+TGMChgduDvI= -github.com/gobuffalo/plush/v4 v4.0.0/go.mod h1:ErFS3UxKqEb8fpFJT7lYErfN/Nw6vHGiDMTjxpk5bQ0= -github.com/gobuffalo/plushgen v0.0.0-20181128164830-d29dcb966cb2/go.mod h1:r9QwptTFnuvSaSRjpSp4S2/4e2D3tJhARYbvEBcKSb4= -github.com/gobuffalo/plushgen v0.0.0-20181203163832-9fc4964505c2/go.mod h1:opEdT33AA2HdrIwK1aibqnTJDVVKXC02Bar/GT1YRVs= -github.com/gobuffalo/plushgen v0.0.0-20181207152837-eedb135bd51b/go.mod h1:Lcw7HQbEVm09sAQrCLzIxuhFbB3nAgp4c55E+UlynR0= -github.com/gobuffalo/plushgen v0.0.0-20190104222512-177cd2b872b3/go.mod h1:tYxCozi8X62bpZyKXYHw1ncx2ZtT2nFvG42kuLwYjoc= -github.com/gobuffalo/plushgen v0.1.2/go.mod h1:3U71v6HWZpVER1nInTXeAwdoRNsRd4W8aeIa1Lyp+Bk= -github.com/gobuffalo/pop v4.8.2+incompatible/go.mod h1:DwBz3SD5SsHpTZiTubcsFWcVDpJWGsxjVjMPnkiThWg= -github.com/gobuffalo/pop v4.8.3+incompatible/go.mod h1:DwBz3SD5SsHpTZiTubcsFWcVDpJWGsxjVjMPnkiThWg= -github.com/gobuffalo/pop v4.8.4+incompatible/go.mod h1:DwBz3SD5SsHpTZiTubcsFWcVDpJWGsxjVjMPnkiThWg= -github.com/gobuffalo/pop v4.13.1+incompatible/go.mod h1:DwBz3SD5SsHpTZiTubcsFWcVDpJWGsxjVjMPnkiThWg= -github.com/gobuffalo/pop/v5 v5.0.11/go.mod h1:mZJHJbA3cy2V18abXYuVop2ldEJ8UZ2DK6qOekC5u5g= -github.com/gobuffalo/pop/v5 v5.3.1/go.mod h1:vcEDhh6cJ3WVENqJDFt/6z7zNb7lLnlN8vj3n5G9rYA= -github.com/gobuffalo/release v1.0.35/go.mod h1:VtHFAKs61vO3wboCec5xr9JPTjYyWYcvaM3lclkc4x4= -github.com/gobuffalo/release v1.0.38/go.mod h1:VtHFAKs61vO3wboCec5xr9JPTjYyWYcvaM3lclkc4x4= -github.com/gobuffalo/release v1.0.42/go.mod h1:RPs7EtafH4oylgetOJpGP0yCZZUiO4vqHfTHJjSdpug= -github.com/gobuffalo/release v1.0.52/go.mod h1:RPs7EtafH4oylgetOJpGP0yCZZUiO4vqHfTHJjSdpug= -github.com/gobuffalo/release v1.0.53/go.mod h1:FdF257nd8rqhNaqtDWFGhxdJ/Ig4J7VcS3KL7n/a+aA= -github.com/gobuffalo/release v1.0.54/go.mod h1:Pe5/RxRa/BE8whDpGfRqSI7D1a0evGK1T4JDm339tJc= -github.com/gobuffalo/release v1.0.61/go.mod h1:mfIO38ujUNVDlBziIYqXquYfBF+8FDHUjKZgYC1Hj24= -github.com/gobuffalo/release v1.0.72/go.mod h1:NP5NXgg/IX3M5XmHmWR99D687/3Dt9qZtTK/Lbwc1hU= -github.com/gobuffalo/release v1.1.1/go.mod h1:Sluak1Xd6kcp6snkluR1jeXAogdJZpFFRzTYRs/2uwg= -github.com/gobuffalo/release v1.1.3/go.mod h1:CuXc5/m+4zuq8idoDt1l4va0AXAn/OSs08uHOfMVr8E= -github.com/gobuffalo/release v1.1.6/go.mod h1:18naWa3kBsqO0cItXZNJuefCKOENpbbUIqRL1g+p6z0= -github.com/gobuffalo/release v1.7.0/go.mod h1:xH2NjAueVSY89XgC4qx24ojEQ4zQ9XCGVs5eXwJTkEs= -github.com/gobuffalo/shoulders v1.0.1/go.mod h1:V33CcVmaQ4gRUmHKwq1fiTXuf8Gp/qjQBUL5tHPmvbA= -github.com/gobuffalo/shoulders v1.0.4/go.mod h1:LqMcHhKRuBPMAYElqOe3POHiZ1x7Ry0BE8ZZ84Bx+k4= -github.com/gobuffalo/syncx v0.0.0-20181120191700-98333ab04150/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw= -github.com/gobuffalo/syncx v0.0.0-20181120194010-558ac7de985f/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw= github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw= -github.com/gobuffalo/syncx v0.1.0/go.mod h1:Mg/s+5pv7IgxEp6sA+NFpqS4o2x+R9dQNwbwT0iuOGQ= -github.com/gobuffalo/tags v2.0.11+incompatible/go.mod h1:9XmhOkyaB7UzvuY4UoZO4s67q8/xRMVJEaakauVQYeY= -github.com/gobuffalo/tags v2.0.14+incompatible/go.mod h1:9XmhOkyaB7UzvuY4UoZO4s67q8/xRMVJEaakauVQYeY= -github.com/gobuffalo/tags v2.0.15+incompatible/go.mod h1:9XmhOkyaB7UzvuY4UoZO4s67q8/xRMVJEaakauVQYeY= -github.com/gobuffalo/tags v2.1.0+incompatible/go.mod h1:9XmhOkyaB7UzvuY4UoZO4s67q8/xRMVJEaakauVQYeY= -github.com/gobuffalo/tags v2.1.7+incompatible/go.mod h1:9XmhOkyaB7UzvuY4UoZO4s67q8/xRMVJEaakauVQYeY= -github.com/gobuffalo/tags/v3 v3.0.2/go.mod h1:ZQeN6TCTiwAFnS0dNcbDtSgZDwNKSpqajvVtt6mlYpA= -github.com/gobuffalo/tags/v3 v3.1.0/go.mod h1:ZQeN6TCTiwAFnS0dNcbDtSgZDwNKSpqajvVtt6mlYpA= -github.com/gobuffalo/uuid v2.0.3+incompatible/go.mod h1:ErhIzkRhm0FtRuiE/PeORqcw4cVi1RtSpnwYrxuvkfE= -github.com/gobuffalo/uuid v2.0.4+incompatible/go.mod h1:ErhIzkRhm0FtRuiE/PeORqcw4cVi1RtSpnwYrxuvkfE= -github.com/gobuffalo/uuid v2.0.5+incompatible/go.mod h1:ErhIzkRhm0FtRuiE/PeORqcw4cVi1RtSpnwYrxuvkfE= -github.com/gobuffalo/validate v2.0.3+incompatible/go.mod h1:N+EtDe0J8252BgfzQUChBgfd6L93m9weay53EWFVsMM= -github.com/gobuffalo/validate v2.0.4+incompatible/go.mod h1:N+EtDe0J8252BgfzQUChBgfd6L93m9weay53EWFVsMM= -github.com/gobuffalo/validate/v3 v3.0.0/go.mod h1:HFpjq+AIiA2RHoQnQVTFKF/ZpUPXwyw82LgyDPxQ9r0= -github.com/gobuffalo/validate/v3 v3.1.0/go.mod h1:HFpjq+AIiA2RHoQnQVTFKF/ZpUPXwyw82LgyDPxQ9r0= -github.com/gobuffalo/validate/v3 v3.2.0/go.mod h1:PrhDOdDHxtN8KUgMvF3TDL0r1YZXV4sQnyFX/EmeETY= -github.com/gobuffalo/x v0.0.0-20181003152136-452098b06085/go.mod h1:WevpGD+5YOreDJznWevcn8NTmQEW5STSBgIkpkjzqXc= -github.com/gobuffalo/x v0.0.0-20181007152206-913e47c59ca7/go.mod h1:9rDPXaB3kXdKWzMc4odGQQdG2e2DIEmANy5aSJ9yesY= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= @@ -2269,12 +1935,9 @@ github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MG github.com/goccy/go-yaml v1.9.5/go.mod h1:U/jl18uSupI5rdI2jmuCswEA2htH9eXfferR3KfscvA= github.com/gocql/gocql v0.0.0-20190301043612-f6df8288f9b4/go.mod h1:4Fw1eo5iaEhDUs8XyuhSVCVy52Jq3L+/3GJgYkwc+/0= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gofrs/uuid v3.1.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= -github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= -github.com/gofrs/uuid/v3 v3.1.2/go.mod h1:xPwMqoocQ1L5G6pXX5BcE7N5jlzn2o19oqAKxwZW/kI= github.com/gogo/googleapis v0.0.0-20180223154316-0cd9801be74a/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= github.com/gogo/googleapis v1.4.1 h1:1Yx4Myt7BxzvUr5ldGSbwYiZG6t9wGBZ+8/fX3Wvtq0= @@ -2308,15 +1971,12 @@ github.com/golang-sql/sqlexp v0.0.0-20170517235910-f1bb20e5a188/go.mod h1:vXjM/+ github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= -github.com/golang/gddo v0.0.0-20180828051604-96d2a289f41e/go.mod h1:xEhNfoBDX1hzLm2Nf80qUvZ2sVwoMZ8d6IE2SrsQfh4= -github.com/golang/gddo v0.0.0-20190904175337-72a348e765d2/go.mod h1:xEhNfoBDX1hzLm2Nf80qUvZ2sVwoMZ8d6IE2SrsQfh4= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP3NQ= github.com/golang/glog v1.1.2 h1:DVjP2PbBOzHyzA+dn3WhHIq4NdVu3Q+pvivFICf/7fo= github.com/golang/glog v1.1.2/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -2332,7 +1992,6 @@ github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71 github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= -github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -2395,7 +2054,6 @@ github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4r github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-github/v45 v45.2.0 h1:5oRLszbrkvxDDqBCNj2hjDZMKmvexaZ1xw/FCD+K3FI= github.com/google/go-github/v45 v45.2.0/go.mod h1:FObaZJEDSTa/WGCzZ2Z3eoCDXWJKMenWWTrd8jrta28= -github.com/google/go-jsonnet v0.16.0/go.mod h1:sOcuej3UW1vpPTZOr8L7RQimqai1a57bt5j22LzGZCw= github.com/google/go-pkcs11 v0.2.0/go.mod h1:6eQoGcuNJpa7jnd5pMGdkSaQpNDYvPlXWMcjXXThLlY= github.com/google/go-pkcs11 v0.2.1-0.20230907215043-c6f79328ddf9/go.mod h1:6eQoGcuNJpa7jnd5pMGdkSaQpNDYvPlXWMcjXXThLlY= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= @@ -2442,7 +2100,6 @@ github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.1.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -2483,30 +2140,21 @@ github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+ github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gophercloud/gophercloud v1.8.0 h1:TM3Jawprb2NrdOnvcHhWJalmKmAmOGgfZElM/3oBYCk= github.com/gophercloud/gophercloud v1.8.0/go.mod h1:aAVqcocTSXh2vYFZ1JTvx4EQmfgzxRcNupUfxZbBNDM= -github.com/gopherjs/gopherjs v0.0.0-20181004151105-1babbf986f6f/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg= github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/gorilla/mux v1.7.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= -github.com/gorilla/pat v0.0.0-20180118222023-199c85a7f6d1/go.mod h1:YeAe0gNeiNT5hoiZRI4yiOky6jVdNvfO2N6Kav/HmxY= -github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= -github.com/gorilla/sessions v1.1.2/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w= -github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w= github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= -github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gotestyourself/gotestyourself v1.3.0/go.mod h1:zZKM6oeNM8k+FRljX1mnzVYeS8wiGgQyvST1/GafPbY= -github.com/gotestyourself/gotestyourself v2.2.0+incompatible/go.mod h1:zZKM6oeNM8k+FRljX1mnzVYeS8wiGgQyvST1/GafPbY= github.com/grafana/alerting v0.0.0-20240222104113-abfafef9a7d2 h1:fmUMdtP7ditGgJFdXCwVxDrKnondHNNe0TkhN5YaIAI= github.com/grafana/alerting v0.0.0-20240222104113-abfafef9a7d2/go.mod h1:brTFeACal/cSZAR8XO/4LPKs7rzNfS86okl6QjSP1eY= github.com/grafana/codejen v0.0.3 h1:tAWxoTUuhgmEqxJPOLtJoxlPBbMULFwKFOcRsPRPXDw= @@ -2554,7 +2202,6 @@ github.com/grafana/tempo v1.5.1-0.20230524121406-1dc1bfe7085b/go.mod h1:UK7kTP5l github.com/grafana/thema v0.0.0-20230712153715-375c1b45f3ed h1:TMtHc+B0SSNw2in6Ro1dAiBYSPRp4NzKgndFDfupt18= github.com/grafana/thema v0.0.0-20230712153715-375c1b45f3ed/go.mod h1:3zLJnssFRPCnebCBRlq53t5LgYv9P1mbj0XMozZMTww= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= -github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= @@ -2562,7 +2209,6 @@ github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vb github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340 h1:uGoIog/wiQHI9GAxXO5TJbT0wWKH3O9HhOJW1F9c3fY= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340/go.mod h1:3bDW6wMZJB7tiONtC/1Xpicra6Wp5GgbTbQWCbI5fkc= -github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= @@ -2607,7 +2253,6 @@ github.com/hashicorp/go-plugin v1.2.2/go.mod h1:F9eH4LrE/ZsRdbwhfjs9k9HoDUwAHnYt github.com/hashicorp/go-plugin v1.6.0 h1:wgd4KxHJTVGGqWBq4QPB1i5BZNEx9BR8+OFmHDmTk8A= github.com/hashicorp/go-plugin v1.6.0/go.mod h1:lBS5MtSSBZk0SHc66KACcjjlU6WzEVP/8pwz68aMkCI= github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= -github.com/hashicorp/go-retryablehttp v0.6.8/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= github.com/hashicorp/go-retryablehttp v0.7.1/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= github.com/hashicorp/go-retryablehttp v0.7.4 h1:ZQgVdpTdAL7WpMIwLzCfbalOcSUdkDZnpUv3/+BxzFA= github.com/hashicorp/go-retryablehttp v0.7.4/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= @@ -2635,7 +2280,6 @@ github.com/hashicorp/golang-lru v0.6.0 h1:uL2shRDx7RTrOrTCUZEGP/wJUFiUI8QT6E7z5o github.com/hashicorp/golang-lru v0.6.0/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= -github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/hcl/v2 v2.17.0 h1:z1XvSUyXd1HP10U4lrLg5e0JMVz6CPaJvAgxM0KNZVY= github.com/hashicorp/hcl/v2 v2.17.0/go.mod h1:gJyW2PTShkJqQBKpAmPO3yxMxIuoXkOF2TpqXzrQyx4= @@ -2692,7 +2336,6 @@ github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod github.com/influxdata/influxdb1-client v0.0.0-20200827194710-b269163b24ab/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= github.com/influxdata/line-protocol v0.0.0-20210311194329-9aa0e372d097 h1:vilfsDSy7TDxedi9gyBkMvAirat/oRcL0lFdJBf6tdM= github.com/influxdata/line-protocol v0.0.0-20210311194329-9aa0e372d097/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo= -github.com/inhies/go-bytesize v0.0.0-20201103132853-d0aed0d254f8/go.mod h1:KrtyD5PFj++GKkFS/7/RRrfnRhAMGQwy75GLCHWrCNs= github.com/invopop/yaml v0.2.0 h1:7zky/qH+O0DwAyoobXUqvVBwgBFRxKoQ/3FjcVpjTMY= github.com/invopop/yaml v0.2.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q= github.com/ionos-cloud/sdk-go/v6 v6.1.10 h1:3815Q2Hw/wc4cJ8wD7bwfsmDsdfIEp80B7BQMj0YP2w= @@ -2709,9 +2352,6 @@ github.com/jackc/fake v0.0.0-20150926172116-812a484cc733/go.mod h1:WrMFNQdiFJ80s github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= -github.com/jackc/pgconn v1.3.2/go.mod h1:LvCquS3HbBKwgl7KbX9KyqEIumJAbm1UMcTvGaIf3bM= -github.com/jackc/pgconn v1.5.0/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI= -github.com/jackc/pgconn v1.6.0/go.mod h1:yeseQo4xhQbgyJs2c87RAXOH2i624N0Fh1KSPJya7qo= github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= @@ -2726,37 +2366,26 @@ github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= -github.com/jackc/pgproto3/v2 v2.0.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgproto3/v2 v2.0.2/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgproto3/v2 v2.2.0/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgservicefile v0.0.0-20200307190119-3430c5407db8/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= -github.com/jackc/pgtype v1.2.0/go.mod h1:5m2OfMh1wTK7x+Fk952IDmI4nw3nPrvtQdM0ZT4WpC0= -github.com/jackc/pgtype v1.3.0/go.mod h1:b0JqxHvPmljG+HQ5IsvQ0yqeSi4nGcDTVjFoiLDb0Ik= github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= github.com/jackc/pgtype v1.10.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= github.com/jackc/pgx v3.2.0+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I= -github.com/jackc/pgx v3.6.2+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I= github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= -github.com/jackc/pgx/v4 v4.4.1/go.mod h1:6iSW+JznC0YT+SgBn7rNxoEBsBgSmnC5FwyCekOGUiE= -github.com/jackc/pgx/v4 v4.6.0/go.mod h1:vPh43ZzxijXUVJ+t/EmXBtFmbFVO72cuneCT9oAlxAg= github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= github.com/jackc/pgx/v4 v4.15.0/go.mod h1:D/zyOyXiaM1TmVWnOM18p0xdDtdakRBa0RsVGI3U3bw= github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.2.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jandelgado/gcov2lcov v1.0.4-0.20210120124023-b83752c6dc08/go.mod h1:NnSxK6TMlg1oGDBfGelGbjgorT5/L3cchlbtgFYZSss= github.com/jarcoal/httpmock v1.3.0/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= -github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc= github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= github.com/jhump/gopoet v0.0.0-20190322174617-17282ff210b3/go.mod h1:me9yfT6IJSlOL3FCfrg+L6yzUEZ+5jW6WHt4Sk+UPUI= @@ -2772,12 +2401,8 @@ github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9Y github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= -github.com/jmoiron/sqlx v0.0.0-20180614180643-0dae4fefe7c0/go.mod h1:IiEW3SEiiErVyFdH8NTuWjSifiEQKUoyK3LNqr2kCHU= -github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= -github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901/go.mod h1:Z86h9688Y0wesXCyonoVr47MasHilkuLMqGhRZ4Hpak= -github.com/joho/godotenv v1.2.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= @@ -2807,16 +2432,8 @@ github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+ github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= -github.com/karrick/godirwalk v1.7.5/go.mod h1:2c9FRhkDxdIbgkOnCEvnSWs71Bhugbl46shStcFDJ34= -github.com/karrick/godirwalk v1.7.7/go.mod h1:2c9FRhkDxdIbgkOnCEvnSWs71Bhugbl46shStcFDJ34= -github.com/karrick/godirwalk v1.7.8/go.mod h1:2c9FRhkDxdIbgkOnCEvnSWs71Bhugbl46shStcFDJ34= github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4= github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= -github.com/karrick/godirwalk v1.10.9/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= -github.com/karrick/godirwalk v1.10.12/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= -github.com/karrick/godirwalk v1.15.3/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk= -github.com/karrick/godirwalk v1.15.5/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk= -github.com/karrick/godirwalk v1.16.1/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk= github.com/kataras/golog v0.0.10/go.mod h1:yJ8YKCmyL+nWjERB90Qwn+bdyBZsaQwU3bTVFgkFIp8= github.com/kataras/iris/v12 v12.1.8/go.mod h1:LMYy4VlP67TQ3Zgriz8RE2h2kMZV2SgMYbq3UhfoFmE= github.com/kataras/neffos v0.0.14/go.mod h1:8lqADm8PnbeFfL7CLXh1WHw53dG27MC3pgi2R1rmoTE= @@ -2826,12 +2443,10 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNU github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= -github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE= github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= -github.com/klauspost/compress v1.9.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.9.7/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= @@ -2845,10 +2460,8 @@ github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgo github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= -github.com/knadh/koanf v0.14.1-0.20201201075439-e0853799f9ec/go.mod h1:H5mEFsTeWizwFXHKtsITL5ipsLTuAMQoGuQpp+1JL9U= github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b h1:udzkj9S/zlT5X367kqJis0QP7YMxobob6zhzq6Yre00= github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b/go.mod h1:pcaDhQK0/NJZEvtCO0qQPPropqV0sJOJ6YW7X+9kRwM= -github.com/konsorten/go-windows-terminal-sequences v0.0.0-20180402223658-b729f2633dfe/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -2861,8 +2474,6 @@ github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NB github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -2881,11 +2492,9 @@ github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1 github.com/leesper/go_rng v0.0.0-20190531154944-a612b043e353 h1:X/79QL0b4YJVO5+OsPH9rF2u428CIrGL/jLmPsoOQQ4= github.com/leesper/go_rng v0.0.0-20190531154944-a612b043e353/go.mod h1:N0SVk0uhy+E1PZ3C9ctsPRlvOPAFPkCNlcPBDkt0N3U= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= -github.com/lib/pq v0.0.0-20180327071824-d34b9ff171c2/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= @@ -2896,9 +2505,6 @@ github.com/linkedin/goavro/v2 v2.10.0 h1:eTBIRoInBM88gITGXYtUSqqxLTFXfOsJBiX8ZMW github.com/linkedin/goavro/v2 v2.10.0/go.mod h1:UgQUb2N/pmueQYH9bfqFioWxzYCZXSfF8Jw03O5sjqA= github.com/linode/linodego v1.25.0 h1:zYMz0lTasD503jBu3tSRhzEmXHQN1zptCw5o71ibyyU= github.com/linode/linodego v1.25.0/go.mod h1:BMZI0pMM/YGjBis7pIXDPbcgYfCZLH0/UvzqtsGtG1c= -github.com/luna-duclos/instrumentedsql v0.0.0-20181127104832-b7d587d28109/go.mod h1:PWUIzhtavmOR965zfawVsHXbEuU1G29BPZ/CB3C7jXk= -github.com/luna-duclos/instrumentedsql v1.1.2/go.mod h1:4LGbEqDnopzNAiyxPPDXhLspyunZxgPTMJBKtC6U0BQ= -github.com/luna-duclos/instrumentedsql v1.1.3/go.mod h1:9J1njvFds+zN7y85EDhN9XNQLANWwZt2ULeIC8yMNYs= github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= github.com/lyft/protoc-gen-star v0.6.1/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= github.com/lyft/protoc-gen-star/v2 v2.0.1/go.mod h1:RcCdONR2ScXaYnQC5tUzxzlpA3WVYF7/opLeUgcQs/o= @@ -2910,39 +2516,14 @@ github.com/magefile/mage v1.11.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXq github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg= github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= -github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.7.1/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/markbates/deplist v1.0.4/go.mod h1:gRRbPbbuA8TmMiRvaOzUlRfzfjeCCBqX2A6arxN01MM= -github.com/markbates/deplist v1.0.5/go.mod h1:gRRbPbbuA8TmMiRvaOzUlRfzfjeCCBqX2A6arxN01MM= -github.com/markbates/deplist v1.1.3/go.mod h1:BF7ioVzAJYEtzQN/os4rt8H8Ti3h0T7EoN+7eyALktE= -github.com/markbates/errx v1.1.0/go.mod h1:PLa46Oex9KNbVDZhKel8v1OT7hD5JZ2eI7AHhA0wswc= -github.com/markbates/going v1.0.2/go.mod h1:UWCk3zm0UKefHZ7l8BNqi26UyiEMniznk8naLdTcy6c= -github.com/markbates/grift v1.0.4/go.mod h1:wbmtW74veyx+cgfwFhlnnMWqhoz55rnHR47oMXzsyVs= -github.com/markbates/hmax v1.0.0/go.mod h1:cOkR9dktiESxIMu+65oc/r/bdY4bE8zZw3OLhLx0X2c= -github.com/markbates/inflect v1.0.0/go.mod h1:oTeZL2KHA7CUX6X+fovmK9OvIOFuqu0TwdQrZjLTh88= -github.com/markbates/inflect v1.0.1/go.mod h1:uv3UVNBe5qBIfCm8O8Q+DW+S1EopeyINj+Ikhc7rnCk= -github.com/markbates/inflect v1.0.3/go.mod h1:1fR9+pO2KHEO9ZRtto13gDwwZaAKstQzferVeWqbgNs= -github.com/markbates/inflect v1.0.4/go.mod h1:1fR9+pO2KHEO9ZRtto13gDwwZaAKstQzferVeWqbgNs= -github.com/markbates/oncer v0.0.0-20180924031910-e862a676800b/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE= -github.com/markbates/oncer v0.0.0-20180924034138-723ad0170a46/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE= -github.com/markbates/oncer v0.0.0-20181014194634-05fccaae8fc4/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE= github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE= -github.com/markbates/oncer v1.0.0/go.mod h1:Z59JA581E9GP6w96jai+TGqafHPW+cPfRxz2aSZ0mcI= -github.com/markbates/pkger v0.17.1/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI= -github.com/markbates/refresh v1.4.10/go.mod h1:NDPHvotuZmTmesXxr95C9bjlw1/0frJwtME2dzcVKhc= -github.com/markbates/safe v1.0.0/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= -github.com/markbates/sigtx v1.0.0/go.mod h1:QF1Hv6Ic6Ca6W+T+DL0Y/ypborFKyvUY9HmuCD4VeTc= -github.com/markbates/willie v1.0.9/go.mod h1:fsrFVWl91+gXpx/6dv715j7i11fYPfZ9ZGfH0DQzY7w= github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= github.com/mattermost/xml-roundtrip-validator v0.1.0 h1:RXbVD2UAl7A7nOTR4u7E3ILa4IbtvKBHw64LDsmu9hU= @@ -2981,24 +2562,19 @@ github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzp github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= -github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI= github.com/mattn/go-sqlite3 v1.14.19/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= -github.com/mattn/goveralls v0.0.6 h1:cr8Y0VMo/MnEZBjxNN/vh6G90SZ7IMb6lms1dzMoO+Y= -github.com/mattn/goveralls v0.0.6/go.mod h1:h8b4ow6FxSPMQHF6o2ve3qsclnffZjYTNEKmLesRwqw= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= github.com/maxatome/go-testdeep v1.12.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM= github.com/mediocregopher/radix/v3 v3.4.2/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8= -github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc= github.com/microsoft/go-mssqldb v1.6.1-0.20240214161942-b65008136246 h1:KT4vTYcHqj5C5hMK5kSpyAk7MnFqfHVWLL4VqMq66S8= github.com/microsoft/go-mssqldb v1.6.1-0.20240214161942-b65008136246/go.mod h1:00mDtPbeQCRGC1HwOOR5K/gr30P1NcEG0vx6Kbv2aJU= @@ -3031,10 +2607,7 @@ github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTS github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.0.0/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.2.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/mapstructure v1.3.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.4.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= @@ -3058,7 +2631,6 @@ github.com/moby/moby v23.0.4+incompatible h1:A/pe8vi9KIKhNbzR0G3wW4ACKDsMgXILBve github.com/moby/moby v23.0.4+incompatible/go.mod h1:fDXVQ6+S340veQPv35CzDahGBmHsiclFwfEygB/TWMc= github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8= github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= -github.com/moby/term v0.0.0-20200915141129-7f0af18e79f2/go.mod h1:TjQg8pa4iejrUrjiz0MCtMV38jdMNW4doKSiBrEvCQQ= github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw= github.com/moby/term v0.0.0-20221205130635-1aeaba878587 h1:HfkjXDfhgVaN5rmueG8cL8KKeFNecRCXFhaJ2qZ5SKA= github.com/moby/term v0.0.0-20221205130635-1aeaba878587/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= @@ -3072,14 +2644,12 @@ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjY github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= -github.com/monoculum/formam v0.0.0-20180901015400-4e68be1d79ba/go.mod h1:RKgILGEJq24YyJ2ban8EO0RUVSJlF1pGsEvoLEACr/Q= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/montanaflynn/stats v0.6.6/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= -github.com/moul/http2curl v0.0.0-20170919181001-9ac6cf4d929b/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= github.com/mpvl/unique v0.0.0-20150818121801-cbe035fff7de h1:D5x39vF5KCwKQaw+OC9ZPiLVHXz3UFw2+psEX+gYcto= github.com/mpvl/unique v0.0.0-20150818121801-cbe035fff7de/go.mod h1:kJun4WP5gFuHZgRjZUWWuH1DTxCtxbHDOIJsudS8jzY= @@ -3108,7 +2678,6 @@ github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxzi github.com/nats-io/nkeys v0.2.0/go.mod h1:XdZpAbhgyyODYqjTawOnIOI7VlbKSarI9Gfy1tqEu/s= github.com/nats-io/nkeys v0.3.0/go.mod h1:gvUNGjVcM2IPr5rCsRsC6Wb3Hr2CQAm08dsxtV6A5y4= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= -github.com/nicksnyder/go-i18n v1.10.0/go.mod h1:HrK7VCrbOvQoUAQ7Vpy7i87N7JZZZ7R2xBGjv0j365Q= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= @@ -3119,17 +2688,11 @@ github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= -github.com/oleiade/reflections v1.0.0/go.mod h1:RbATFBbKYkVdqmSFtx13Bb/tVhR0lgOBXunWTZKeL4w= -github.com/oleiade/reflections v1.0.1 h1:D1XO3LVEYroYskEsoSiGItp9RUxG6jWnCVvrqH0HHQM= -github.com/oleiade/reflections v1.0.1/go.mod h1:rdFxbxq4QXVZWj0F+e9jqjDkc7dbp97vkRixKo2JR60= github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.9.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.16.2/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E= @@ -3150,12 +2713,7 @@ github.com/onsi/ginkgo/v2 v2.9.2/go.mod h1:WHcJJG2dIlcCqVfBAwUCrJxSPFb6v4azBwgxe github.com/onsi/ginkgo/v2 v2.9.4/go.mod h1:gCQYp2Q+kSoIj7ykSVb9nskRSsR6PUj4AiLywzIhbKM= github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4= github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o= -github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= -github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/onsi/gomega v1.6.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY= @@ -3181,8 +2739,6 @@ github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zM github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/opencontainers/image-spec v1.0.3-0.20220512140940-7b36cea86235 h1:DxS3bbeUSCpMQr3mTez5PIDrS+yBeBsoDsftOhqB1Fg= github.com/opencontainers/image-spec v1.0.3-0.20220512140940-7b36cea86235/go.mod h1:K/JAU0m27RFhDRX4PcFdIKntROP6y5Ed6O91aZYDQfs= -github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= -github.com/opencontainers/runc v1.0.0-rc9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= github.com/opentracing-contrib/go-stdlib v1.0.0 h1:TBS7YuVotp8myLon4Pv7BtCBzOTo1DeZCld0Z63mW2w= github.com/opentracing-contrib/go-stdlib v1.0.0/go.mod h1:qtI1ogk+2JhVPIXVc6q+NHziSmy2W5GbdQZFUHADCBU= @@ -3197,52 +2753,17 @@ github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnh github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= github.com/openzipkin/zipkin-go v0.2.5/go.mod h1:KpXfKdgRDnnhsxw4pNIH9Md5lyFqKUa4YDFlwRYAMyE= github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= -github.com/ory/analytics-go/v4 v4.0.0/go.mod h1:FMx9cLRD9xN+XevPvZ5FDMfignpmcqPP6FUKnJ9/MmE= -github.com/ory/dockertest v3.3.5+incompatible/go.mod h1:1vX4m9wsvi00u5bseYwXaSnhNrne+V0E6LAcBILJdPs= -github.com/ory/dockertest/v3 v3.5.4/go.mod h1:J8ZUbNB2FOhm1cFZW9xBpDsODqsSWcyYgtJYVPcnF70= -github.com/ory/dockertest/v3 v3.6.3/go.mod h1:EFLcVUOl8qCwp9NyDAcCDtq/QviLtYswW/VbWzUnTNE= -github.com/ory/fosite v0.29.0/go.mod h1:0atSZmXO7CAcs6NPMI/Qtot8tmZYj04Nddoold4S2h0= -github.com/ory/fosite v0.44.1-0.20230317114349-45a6785cc54f h1:OFA3y3TJ2qsBXCBMXUNvTzHNBS8/kXdk4cHpJGzBKO4= -github.com/ory/fosite v0.44.1-0.20230317114349-45a6785cc54f/go.mod h1:N0WZtyPBAuXedTpwzbKl4tSYU8wpjlMQoxnKcL2m8dU= -github.com/ory/go-acc v0.0.0-20181118080137-ddc355013f90/go.mod h1:sxnvPCxChFuSmTJGj8FdMupeq1BezCiEpDjTUXQ4hf4= -github.com/ory/go-acc v0.2.6 h1:YfI+L9dxI7QCtWn2RbawqO0vXhiThdXu/RgizJBbaq0= -github.com/ory/go-acc v0.2.6/go.mod h1:4Kb/UnPcT8qRAk3IAxta+hvVapdxTLWtrr7bFLlEgpw= -github.com/ory/go-convenience v0.1.0 h1:zouLKfF2GoSGnJwGq+PE/nJAE6dj2Zj5QlTgmMTsTS8= -github.com/ory/go-convenience v0.1.0/go.mod h1:uEY/a60PL5c12nYz4V5cHY03IBmwIAEm8TWB0yn9KNs= -github.com/ory/gojsonreference v0.0.0-20190720135523-6b606c2d8ee8/go.mod h1:wsH1C4nIeeQClDtD5AH7kF1uTS6zWyqfjVDTmB0Em7A= -github.com/ory/gojsonschema v1.1.1-0.20190919112458-f254ca73d5e9/go.mod h1:BNZpdJgB74KOLSsWFvzw6roXg1I6O51WO8roMmW+T7Y= -github.com/ory/herodot v0.6.2/go.mod h1:3BOneqcyBsVybCPAJoi92KN2BpJHcmDqAMcAAaJiJow= -github.com/ory/herodot v0.7.0/go.mod h1:YXKOfAXYdQojDP5sD8m0ajowq3+QXNdtxA+QiUXBwn0= -github.com/ory/herodot v0.8.3/go.mod h1:rvLjxOAlU5omtmgjCfazQX2N82EpMfl3BytBWc1jjsk= -github.com/ory/herodot v0.9.2/go.mod h1:Da2HXR8mpwPbPrH+Gv9qV8mM5gI3v+PoJ69BA4l2RAk= -github.com/ory/jsonschema/v3 v3.0.1/go.mod h1:jgLHekkFk0uiGdEWGleC+tOm6JSSP8cbf17PnBuGXlw= -github.com/ory/viper v1.5.6/go.mod h1:TYmpFpKLxjQwvT4f0QPpkOn4sDXU1kDgAwJpgLYiQ28= -github.com/ory/viper v1.7.4/go.mod h1:T6sodNZKNGPpashUOk7EtXz2isovz8oCd57GNVkkNmE= -github.com/ory/viper v1.7.5 h1:+xVdq7SU3e1vNaCsk/ixsfxE4zylk1TJUiJrY647jUE= -github.com/ory/viper v1.7.5/go.mod h1:ypOuyJmEUb3oENywQZRgeAMwqgOyDqwboO1tj3DjTaM= -github.com/ory/x v0.0.84/go.mod h1:RXLPBG7B+hAViONVg0sHwK+U/ie1Y/NeXrq1JcARfoE= -github.com/ory/x v0.0.93/go.mod h1:lfcTaGXpTZs7IEQAW00r9EtTCOxD//SiP5uWtNiz31g= -github.com/ory/x v0.0.110/go.mod h1:DJfkE3GdakhshNhw4zlKoRaL/ozg/lcTahA9OCih2BE= -github.com/ory/x v0.0.127/go.mod h1:FwUujfFuCj5d+xgLn4fGMYPnzriR5bdAIulFXMtnK0M= -github.com/ory/x v0.0.214 h1:nz5ijvm5MVhYxWsQSuUrW1hj9F5QLZvPn/nLo5s06T4= -github.com/ory/x v0.0.214/go.mod h1:aRl57gzyD4GF0HQCekovXhv0xTZgAgiht3o8eVhsm9Q= github.com/ovh/go-ovh v1.4.3 h1:Gs3V823zwTFpzgGLZNI6ILS4rmxZgJwJCz54Er9LwD0= github.com/ovh/go-ovh v1.4.3/go.mod h1:AkPXVtgwB6xlKblMjRKJJmjRp+ogrE7fz2lVgcQY8SY= github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= -github.com/parnurzeal/gorequest v0.2.15/go.mod h1:3Kh2QUMJoqw3icWAecsyzkpY7UzRfDhbRdTjtNwNiUE= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= -github.com/pborman/uuid v1.2.0 h1:J7Q5mO4ysT1dv8hyrUGHb9+ooztCXu1D8MY8DZYsu3g= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= -github.com/pelletier/go-toml v1.4.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo= -github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys= github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= -github.com/pelletier/go-toml v1.8.0/go.mod h1:D6yutnOGMveHEPV7VQOuvI/gXY61bv+9bAOTRnLElKs= -github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas= github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= @@ -3250,8 +2771,6 @@ github.com/performancecopilot/speed/v4 v4.0.0/go.mod h1:qxrSyuDGrTOWfV+uKRFhfxw6 github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= -github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= -github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY= github.com/phpdave11/gofpdi v1.0.7/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= @@ -3270,7 +2789,6 @@ github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmd github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -3286,7 +2804,6 @@ github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM= github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= -github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= @@ -3311,10 +2828,8 @@ github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJ github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= -github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= @@ -3340,10 +2855,7 @@ github.com/prometheus/exporter-toolkit v0.11.0/go.mod h1:BVnENhnNecpwoTLiABx7mrP github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20190425082905-87a4384529e0/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= @@ -3355,7 +2867,6 @@ github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/prometheus/prometheus v0.49.0 h1:i0CEhreJo3ZcZNeK7ulISinCac0MgL0krVOGgNmfFRY= github.com/prometheus/prometheus v0.49.0/go.mod h1:aDogiyqmv3aBIWDb5z5Sdcxuuf2BOfiJwOIm9JGpMnI= -github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/protocolbuffers/txtpbfmt v0.0.0-20220428173112-74888fd59c2b h1:zd/2RNzIRkoGGMjE+YIsZ85CnDIz672JK2F3Zl4vux4= github.com/protocolbuffers/txtpbfmt v0.0.0-20220428173112-74888fd59c2b/go.mod h1:KjY0wibdYKc4DYkerHSbguaf3JeIPGhNJBp2BNiFH78= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= @@ -3363,11 +2874,9 @@ github.com/redis/go-redis/v9 v9.0.2 h1:BA426Zqe/7r56kCcvxYLWe1mkaz71LKF77GwgFzSx github.com/redis/go-redis/v9 v9.0.2/go.mod h1:/xDTe9EF1LM61hek62Poq2nzQSGj0xSrEtEHbBQevps= github.com/redis/rueidis v1.0.16 h1:ieB3AqZe9GcuTWZL8PFu1Mfn+pfqjBZAJEZh7zOcwSI= github.com/redis/rueidis v1.0.16/go.mod h1:8B+r5wdnjwK3lTFml5VtxjzGOQAC+5UmujoD12pDrEo= -github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= -github.com/rhnvrm/simples3 v0.5.0/go.mod h1:Y+3vYm2V7Y4VijFoJHHTrja6OgPrJ2cBti8dPGkC3sA= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.3.4 h1:3Z3Eu6FGHZWSfNKJTOUiPatWwfc7DzJRU04jFUqJODw= github.com/rivo/uniseg v0.3.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= @@ -3376,13 +2885,9 @@ github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzG github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc= -github.com/rogpeppe/go-internal v1.0.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.3.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.4.0/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.5.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= @@ -3390,13 +2895,11 @@ github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/f github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= -github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/rs/cors v1.10.1 h1:L0uuZVXIKlI1SShY2nhFfo44TYvDPQ1w4oFkUJNfhyo= github.com/rs/cors v1.10.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= -github.com/rubenv/sql-migrate v0.0.0-20190212093014-1007f53448d7/go.mod h1:WS0rl9eEliYI8DPnr3TOwz4439pay+qNgzJoVya/DmY= github.com/russellhaering/goxmldsig v1.4.0 h1:8UcDh/xGyQiyrW+Fq5t8f+l2DLB1+zlhYzkPUJ7Qhys= github.com/russellhaering/goxmldsig v1.4.0/go.mod h1:gM4MDENBQf7M+V824SGfyIUVFWydB7n0KkEubVJl+Tw= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= @@ -3411,58 +2914,34 @@ github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFo github.com/ryanuber/columnize v2.1.2+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sagikazarmark/crypt v0.6.0/go.mod h1:U8+INwJo3nBv1m6A/8OBXAq7Jnpspk5AxSgDyEQcea8= github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= -github.com/santhosh-tekuri/jsonschema v1.2.4/go.mod h1:TEAUOeZSmIxTTuHatJzrvARHiuO9LYd+cIxzgEHCQI4= -github.com/santhosh-tekuri/jsonschema/v2 v2.1.0/go.mod h1:yzJzKUGV4RbWqWIBBP4wSOBqavX5saE02yirLS0OTyg= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/scaleway/scaleway-sdk-go v1.0.0-beta.21 h1:yWfiTPwYxB0l5fGMhl/G+liULugVIHD9AU77iNLrURQ= github.com/scaleway/scaleway-sdk-go v1.0.0-beta.21/go.mod h1:fCa7OJZ/9DRTnOKmxvT6pn+LPWUptQAmHF/SBJUGEcg= github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= -github.com/seatgeek/logrus-gelf-formatter v0.0.0-20210219220335-367fa274be2c/go.mod h1:/THDZYi7F/BsVEcYzYPqdcWFQ+1C2InkawTKfLOAnzg= -github.com/segmentio/analytics-go v3.0.1+incompatible/go.mod h1:C7CYBtQWk4vRk2RyLu0qOcbHJ18E3F1HV2C/8JvKN48= -github.com/segmentio/analytics-go v3.1.0+incompatible/go.mod h1:C7CYBtQWk4vRk2RyLu0qOcbHJ18E3F1HV2C/8JvKN48= github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg= github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= -github.com/segmentio/backo-go v0.0.0-20160424052352-204274ad699c/go.mod h1:kJ9mm9YmoWSkk+oQ+5Cj8DEoRCX2JT6As4kEtIIOp1M= -github.com/segmentio/backo-go v0.0.0-20200129164019-23eae7c10bd3/go.mod h1:9/Rh6yILuLysoQnZ2oNooD2g7aBnvM7r/fNVxRNWfBc= -github.com/segmentio/conf v1.2.0/go.mod h1:Y3B9O/PqqWqjyxyWWseyj/quPEtMu1zDp/kVbSWWaB0= github.com/segmentio/encoding v0.3.6 h1:E6lVLyDPseWEulBmCmAKPanDd3jiyGDo5gMcugCRwZQ= github.com/segmentio/encoding v0.3.6/go.mod h1:n0JeuIqEQrQoPDGsjo8UNd1iA0U8d8+oHAA4E3G3OxM= -github.com/segmentio/go-snakecase v1.1.0/go.mod h1:jk1miR5MS7Na32PZUykG89Arm+1BUSYhuGR6b7+hJto= -github.com/segmentio/objconv v1.0.1/go.mod h1:auayaH5k3137Cl4SoXTgrzQcuQDmvuVtZgS0fb1Ahys= -github.com/serenize/snaker v0.0.0-20171204205717-a683aaf2d516/go.mod h1:Yow6lPLSAXx2ifx470yD/nUe22Dv5vBvxK/UK9UUTVs= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= -github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/shoenig/test v0.6.6/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= -github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= -github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= -github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU= -github.com/shurcooL/highlight_go v0.0.0-20170515013102-78fb10f4a5f8/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag= github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c h1:aqg5Vm5dwtvL+YgDpBcK1ITf3o96N/K7/wsRXQnUTEs= github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c/go.mod h1:owqhoLW1qZoYLZzLnBw+QkPP9WZnjlSWihhxAJC1+/M= -github.com/shurcooL/octicon v0.0.0-20180602230221-c42b0e3b24d9/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ= -github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546 h1:pXY9qYc/MP5zdvqWEUH6SjNiu7VhSjuVFTFiTcphaLU= github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw= -github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= -github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= -github.com/sirupsen/logrus v1.1.0/go.mod h1:zrgwTnHtNr00buQ1vSptGe8m1f/BbgsPukg8qsT7A+A= -github.com/sirupsen/logrus v1.1.1/go.mod h1:zrgwTnHtNr00buQ1vSptGe8m1f/BbgsPukg8qsT7A+A= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= @@ -3471,7 +2950,6 @@ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 h1:Jpy1PXuP99tXNrhbq2BaPz9B+jNAvH1JPQQpG/9GCXY= github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= @@ -3479,50 +2957,32 @@ github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4k github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js= github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= -github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= -github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/afero v1.2.0/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= -github.com/spf13/afero v1.9.2 h1:j49Hj62F0n+DaZ1dDCvhABaPNSGNkt32oRFxI33IEMw= github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= -github.com/spf13/cast v1.2.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.3.2-0.20200723214538-8d17101741c8/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= -github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= -github.com/spf13/cobra v0.0.6/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= -github.com/spf13/cobra v0.0.7/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= -github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= -github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.2.1/go.mod h1:P4AexN0a+C9tGAnUFNwDMYYZv3pjFuvmeiMyKRaNVlI= -github.com/spf13/viper v1.3.1/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= -github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= github.com/spf13/viper v1.13.0/go.mod h1:Icm2xNL3/8uyh/wFuB1jI7TiTNKp8632Nwegu+zgdYw= github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= github.com/spyzhov/ajson v0.9.0 h1:tF46gJGOenYVj+k9K1U1XpCxVWhmiyY5PsVCAs1+OJ0= github.com/spyzhov/ajson v0.9.0/go.mod h1:a6oSw0MMb7Z5aD2tPoPO+jq11ETKgXUr2XktHdT8Wt8= -github.com/sqs/goreturns v0.0.0-20181028201513-538ac6014518/go.mod h1:CKI4AZ4XmGV240rTHfO0hfE83S6/a3/Q1siZJ/vXf7A= -github.com/square/go-jose/v3 v3.0.0-20200630053402-0a67ce9b0693/go.mod h1:6hSY48PjDm4UObWmGLyJE9DxYVKTgR9kbCspXXJEhcU= github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= @@ -3552,38 +3012,19 @@ github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/subosito/gotenv v1.1.1/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= -github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= -github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs= github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf h1:Z2X3Os7oRzpdJ75iPqWZc0HeJWFYNCvKsfpQwFpRNTA= github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf/go.mod h1:M8agBzgqHIhgj7wEn9/0hJUZcrvt9VY+Ln+S1I5Mha0= -github.com/tidwall/gjson v1.3.2/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls= -github.com/tidwall/gjson v1.6.8/go.mod h1:zeFuBCIqD4sN/gmqBzZ4j7Jd6UcA2Fc56x7QFsv+8fI= -github.com/tidwall/gjson v1.7.1/go.mod h1:5/xDoumyyDNerp2U36lyolv46b3uF/9Bu6OfyQ9GImk= -github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E= -github.com/tidwall/match v1.0.3/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v0.0.0-20180105212114-65a9db5fad51/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= -github.com/tidwall/pretty v1.0.2/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= -github.com/tidwall/pretty v1.1.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= -github.com/tidwall/sjson v1.0.4/go.mod h1:bURseu1nuBkFpIES5cz6zBtjmYeOQmEESshn7VpF15Y= -github.com/tidwall/sjson v1.1.5/go.mod h1:VuJzsZnTowhSxWdOgsAnb886i4AjEyTkk7tNtsL7EYE= -github.com/tinylib/msgp v1.1.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 h1:6fotK7otjonDflCTK0BCfls4SPy3NcCVb5dqqmbRknE= github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75/go.mod h1:KO6IkyS8Y3j8OdNO85qEYBsRPuteD+YciPomcXdrMnk= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/ua-parser/uap-go v0.0.0-20211112212520-00c877edfe0f h1:A+MmlgpvrHLeUP8dkBVn4Pnf5Bp5Yk2OALm7SEJLLE8= github.com/ua-parser/uap-go v0.0.0-20211112212520-00c877edfe0f/go.mod h1:OBcG9bn7sHtXgarhUEb3OfCnNsgtGnkVf41ilSZ3K3E= -github.com/uber-go/atomic v1.3.2/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex1PFV8g= -github.com/uber/jaeger-client-go v2.15.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= -github.com/uber/jaeger-client-go v2.22.1+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o= github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= -github.com/uber/jaeger-lib v1.5.0/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= -github.com/uber/jaeger-lib v2.2.0+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg= github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= @@ -3599,8 +3040,6 @@ github.com/unknwon/com v1.0.1 h1:3d1LTxD+Lnf3soQiD4Cp/0BRB+Rsa/+RTvz8GMMzIXs= github.com/unknwon/com v1.0.1/go.mod h1:tOOxU81rwgoCLoOVVPHb6T/wt8HZygqH5id+GNnlCXM= github.com/unknwon/log v0.0.0-20150304194804-e617c87089d3 h1:4EYQaWAatQokdji3zqZloVIW/Ke1RQjYw2zHULyrHJg= github.com/unknwon/log v0.0.0-20150304194804-e617c87089d3/go.mod h1:1xEUf2abjfP92w2GZTV+GgaRxXErwRXcClbUwrNJffU= -github.com/unrolled/secure v0.0.0-20180918153822-f340ee86eb8b/go.mod h1:mnPT77IAdsi/kV7+Es7y+pXALeV3h7G6dQF6mNYjcLA= -github.com/unrolled/secure v0.0.0-20181005190816-ff9db2ff917f/go.mod h1:mnPT77IAdsi/kV7+Es7y+pXALeV3h7G6dQF6mNYjcLA= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli v1.22.14 h1:ebbhrRiGK2i4naQJr+1Xj92HXZCrK7MsyTS/ob3HnAk= @@ -3631,7 +3070,6 @@ github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6 github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I= -github.com/xdg/stringprep v0.0.0-20180714160509-73f8eece6fdc/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y= github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= @@ -3648,7 +3086,6 @@ github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= -github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c/go.mod h1:UrdRz5enIKZ63MEE3IF9l2/ebyx59GyGgPi+tICQdmM= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= github.com/yalue/merged_fs v1.2.2 h1:vXHTpJBluJryju7BBpytr3PDIkzsPMpiEknxVGPhN/I= github.com/yalue/merged_fs v1.2.2/go.mod h1:WqqchfVYQyclV2tnR7wtRhBddzBvLVR83Cjw9BKQw0M= @@ -3678,11 +3115,6 @@ github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxt github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs= github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b/go.mod h1:T3BPAOm2cqquPa0MKWeNkmOM5RQsRhkrwMWonFMN7fE= -go.elastic.co/apm v1.8.0/go.mod h1:tCw6CkOJgkWnzEthFN9HUP1uL3Gjc/Ur6m7gRPLaoH0= -go.elastic.co/apm/module/apmhttp v1.8.0/go.mod h1:9LPFlEON51/lRbnWDfqAWErihIiAFDUMfMV27YjoWQ8= -go.elastic.co/apm/module/apmot v1.8.0/go.mod h1:Q5Xzabte8G/fkvDjr1jlDuOSUt9hkVWNZEHh6ZNaTjI= -go.elastic.co/fastjson v1.0.0/go.mod h1:PmeUOMMtLHQr9ZS9J9owrAVg0FkaZDRZJEFTTGHtchs= -go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.8 h1:xs88BrvEv273UsB79e0hcVrlUWmS0a8upikMFhSyAtA= go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= @@ -3710,11 +3142,7 @@ go.etcd.io/etcd/raft/v3 v3.5.10 h1:cgNAYe7xrsrn/5kXMSaH8kM/Ky8mAdMqGOxyYwpP0LA= go.etcd.io/etcd/raft/v3 v3.5.10/go.mod h1:odD6kr8XQXTy9oQnyMPBOr0TVe+gT0neQhElQ6jbGRc= go.etcd.io/etcd/server/v3 v3.5.10 h1:4NOGyOwD5sUZ22PiWYKmfxqoeh72z6EhYjNosKGLmZg= go.etcd.io/etcd/server/v3 v3.5.10/go.mod h1:gBplPHfs6YI0L+RpGkTQO7buDbHv5HJGG/Bst0/zIPo= -go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= go.mongodb.org/mongo-driver v1.1.0/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= -go.mongodb.org/mongo-driver v1.1.1/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= -go.mongodb.org/mongo-driver v1.3.0/go.mod h1:MSWZXKOynuguX+JSvwP8i+58jYCXxbia8HS3gZBapIE= -go.mongodb.org/mongo-driver v1.3.4/go.mod h1:MSWZXKOynuguX+JSvwP8i+58jYCXxbia8HS3gZBapIE= go.mongodb.org/mongo-driver v1.7.3/go.mod h1:NqaYOwnXWr5Pm7AOpO5QFxKJ503nbMse/R79oO62zWg= go.mongodb.org/mongo-driver v1.7.5/go.mod h1:VXEWRZ6URJIkUq2SCAyapmhH0ZLRBP+FT4xhp5Zvxng= go.mongodb.org/mongo-driver v1.10.0/go.mod h1:wsihk0Kdgv8Kqu1Anit4sfK+22vSFbUrAVEYRhCXrA8= @@ -3726,7 +3154,6 @@ go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.1/go.mod h1:Ap50jQcDJrx6rB6VgeeFPtuPIf3wMRvRfrfYDO6+BmA= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= @@ -3739,10 +3166,8 @@ go.opentelemetry.io/collector/pdata v1.0.0/go.mod h1:TsDFgs4JLNG7t6x9D8kGswXUz4m go.opentelemetry.io/collector/pdata v1.0.1 h1:dGX2h7maA6zHbl5D3AsMnF1c3Nn+3EUftbVCLzeyNvA= go.opentelemetry.io/collector/pdata v1.0.1/go.mod h1:jutXeu0QOXYY8wcZ/hege+YAnSBP3+jpTqYU1+JTI5Y= go.opentelemetry.io/collector/semconv v0.90.1/go.mod h1:j/8THcqVxFna1FpvA2zYIsUperEtOaRaqoLYIN4doWw= -go.opentelemetry.io/contrib v0.18.0/go.mod h1:G/EtFaa6qaN7+LxqfIAT3GiZa7Wv5DTBUzl5H4LY0Kc= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.47.0 h1:UNQQKPfTDe1J81ViolILjTKPr9WetKW6uei2hFgJmFs= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.47.0/go.mod h1:r9vWsPS/3AQItv3OSlEJ/E4mbrhUbbw18meOjArPtKQ= -go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.18.0/go.mod h1:iK1G0FgHurSJ/aYLg5LpnPI0pqdanM73S3dhyDp0Lk4= go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.46.1 h1:gbhw/u49SS3gkPWiYweQNJGm/uJN5GkI/FrosxSHT7A= go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.46.1/go.mod h1:GnOaBaFQ2we3b9AGWJpsBa7v1S5RlQzlC3O7dRMxZhM= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24= @@ -3762,7 +3187,6 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0/go.mod h go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0/go.mod h1:/OpE/y70qVkndM0TrxT4KBoN3RsFZP0QaofcfYrj76I= go.opentelemetry.io/otel/metric v1.22.0 h1:lypMQnGyJYeuYPhOM/bgjbFM6WE44W1/T45er4d8Hhg= go.opentelemetry.io/otel/metric v1.22.0/go.mod h1:evJGjVpZv0mQ5QBRJoBF64yMuOf4xCWdXjK8pzFvliY= -go.opentelemetry.io/otel/oteltest v0.18.0/go.mod h1:NyierCU3/G8DLTva7KRzGii2fdxdR89zXKH1bNWY7Bo= go.opentelemetry.io/otel/sdk v1.17.0/go.mod h1:U87sE0f5vQB7hwUoW98pW5Rz4ZDuCFBZFNUBlSgmDFQ= go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= go.opentelemetry.io/otel/sdk v1.22.0 h1:6coWHw9xw7EfClIC/+O31R8IY3/+EiRFHevmHafB2Gw= @@ -3779,7 +3203,6 @@ go.starlark.net v0.0.0-20230525235612-a134d8f9ddca/go.mod h1:jxU+3+j+71eXOW14274 go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= -go.uber.org/atomic v1.5.1/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= @@ -3811,48 +3234,26 @@ go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= gocloud.dev v0.25.0 h1:Y7vDq8xj7SyM848KXf32Krda2e6jQ4CLh/mTeCSqXtk= gocloud.dev v0.25.0/go.mod h1:7HegHVCYZrMiU3IE1qtnzf/vRrDwLYnRNR3EhWX8x9Y= -golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20180830192347-182538f80094/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20180910181607-0e37d006457b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181001203147-e3636079e1a4/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181009213950-7c1a557ab941/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181015023909-0c41d7ab0a0e/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181024171144-74cb1d3d52f4/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181025113841-85e1b3f9139a/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181025213731-e84da0312774/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181106171534-e4dc69e5b2fd/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181112202954-3d3f9f413869/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181127143415-eb0de9b17e85/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190102171810-8d7daa0c54b3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190103213133-ff983b9c42bc/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190617133340-57b3e21c3d56/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191122220453-ac88ee75c92c/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200117160349-530e935923ad/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200320181102-891825fb96df/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -3894,7 +3295,6 @@ golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190312203227-4b39c73a6495/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3/go.mod h1:NOZ3BPKG0ec/BKJQgnvsSFpcKLM5xXVWnvZS97DWHgE= @@ -3962,33 +3362,22 @@ golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180816102801-aaf60122140d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180921000356-2f5d2388922f/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180926154720-4dfa2610cdf3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181017193950-04a2e542c03f/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181108082009-03003ca0c849/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181207154023-610586996380/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190320064053-1272bf9dcd53/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190424112056-4829fb13d2c6/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -3997,12 +3386,10 @@ golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191003171128-d98b1b443823/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191112182307-2180aed22343/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200219183655-46282727080f/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -4012,7 +3399,6 @@ golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= @@ -4076,7 +3462,6 @@ golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20181003184128-c57b0facaced/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -4142,37 +3527,22 @@ golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20180816055513-1c9583448a9c/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180831094639-fa5fdf94c789/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180906133057-8cf3aee42992/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180921163948-d47a0f339242/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180927150500-dad3d9fb7b6e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181005133103-4497e2df6f9e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181011152604-fa43e7bc11ba/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181022134430-8a28ead16f52/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181024145615-5cd93ef61a7c/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181025063200-d989b31c8746/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181026064943-731415f00dce/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181106135930-3a76605856fd/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181206074257-70b957f3b65e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181221143128-b4a75ba826a6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190102155601-82a175fd1598/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190116161447-11f53e031339/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190130150945-aca44879d564/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190321052220-f7bb7a8bee54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -4180,10 +3550,8 @@ golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190426135247-a129542de9ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -4196,9 +3564,7 @@ golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191020152052-9984515f0562/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191025021431-6c3a3bfe00ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191105231009-c1f44814a5cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191112214154-59a1497f0cea/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -4207,7 +3573,6 @@ golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200121082415-34d275377bf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -4220,12 +3585,10 @@ golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200828194041-157a740278f4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -4295,7 +3658,6 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -4374,31 +3736,8 @@ golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181003024731-2f84ea8ef872/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181006002542-f60d9635b16a/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181008205924-a2b3f7f249e9/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181013182035-5e66757b835f/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181017214349-06f26fdaaa28/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181024171208-a2dc47679d30/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181026183834-f60e5f99f081/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181105230042-78dc5bac0cac/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181107215632-34b416bd17b3/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181114190951-94339b83286c/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181119130350-139d099f6620/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181127195227-b4e97c0ed882/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181127232545-e782529d0ddd/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181203210056-e5f3ab76ea4b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181205224935-3576414c54a4/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181206194817-bcd4e47d0288/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181207183836-8bc39b988060/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181212172921-837e80568c09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181221001348-537d06c36207/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190102213336-ca9055ed7d04/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190104182027-498d95493402/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190111214448-fc1d57b08d7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190118193359-16909d206f00/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= @@ -4417,20 +3756,14 @@ golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBn golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190613204242-ed0dc450797f/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190617190820-da514acc4774/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190624190245-7f2218787638/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190711191110-9a621aea19f8/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191004055002-72853e10c5a3/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -4441,31 +3774,25 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191224055732-dd894d0a8a40/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117220505-0cba7a3a9ee9/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200203215610-ab391d50b528/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200308013534-11ec41452d41/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200522201501-cb1345f3a375/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200717024301-6ddee64345a6/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= @@ -4485,7 +3812,6 @@ golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.8-0.20211029000441-d6a9af8af023/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= -golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= @@ -4514,7 +3840,6 @@ golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNq golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= -gonum.org/v1/gonum v0.6.2/go.mod h1:9mxDZsDKxgMAuccQkewq682L+0eCu4dCN2yonUJTCLU= gonum.org/v1/gonum v0.7.0/go.mod h1:L02bwd0sqlsvRv41G7wGWFCsVNZFv/k1xzGIxeANHGM= gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0= @@ -4522,9 +3847,7 @@ gonum.org/v1/gonum v0.11.0/go.mod h1:fSG4YDCxxUZQJ7rKsQrj0gMOg00Il0Z96/qMA4bVQhA gonum.org/v1/gonum v0.12.0 h1:xKuo6hzt+gMav00meVPUlXwSdoEJP46BR+wdxQEFK2o= gonum.org/v1/gonum v0.12.0/go.mod h1:73TDxJfAAHeA8Mk9mf8NlIppyhQNo5GLTcYeqgo2lvY= gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= -gonum.org/v1/netlib v0.0.0-20191229114700-bbb4dff026f8/go.mod h1:2IgXn/sJaRbePPBA1wRj8OE+QLvVaH0q8SK6TSTKlnk= gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= -gonum.org/v1/plot v0.0.0-20200111075622-4abb28f724d5/go.mod h1:+HbaZVpsa73UwN7kXGCECULRHovLRJjH+t5cFPgxErs= gonum.org/v1/plot v0.9.0/go.mod h1:3Pcqqmp6RHvJI72kgb8fThyUnav364FOsdDo2aGW5lY= gonum.org/v1/plot v0.10.1/go.mod h1:VZW5OlhkL1mysU9vaqNHnsy86inf6Ot+jB3r+BczCEo= google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= @@ -4625,8 +3948,6 @@ google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRn google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= -google.golang.org/genproto v0.0.0-20190626174449-989357319d63/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= -google.golang.org/genproto v0.0.0-20190708153700-3bdd9d9f5532/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= @@ -4652,7 +3973,6 @@ google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEY google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200806141610-86f49bd18e98/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200911024640-645f7a48b24f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= @@ -4848,7 +4168,6 @@ google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLD google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= @@ -4901,7 +4220,6 @@ google.golang.org/grpc v1.60.1 h1:26+wFr+cNqSGFcOXcabYC0lUVJVRa2Sb2ortSK7VrEU= google.golang.org/grpc v1.60.1/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM= google.golang.org/grpc/cmd/protoc-gen-go-grpc v0.0.0-20200910201057-6591123024b3/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= -google.golang.org/grpc/examples v0.0.0-20210304020650-930c79186c99/go.mod h1:Ly7ZA/ARzg8fnPU9TyZIxoz33sEUuWX7txiqs8lPTgE= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -4922,8 +4240,6 @@ google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= -gopkg.in/DataDog/dd-trace-go.v1 v1.27.0/go.mod h1:Sp1lku8WJMvNV0kjDI4Ni/T7J/U3BO5ct5kEaoVU8+I= -gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= @@ -4939,37 +4255,24 @@ gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMy gopkg.in/fsnotify/fsnotify.v1 v1.4.7 h1:XNNYLJHt73EyYiCZi6+xjupS9CpvmiDgjPTAjrBlQbo= gopkg.in/fsnotify/fsnotify.v1 v1.4.7/go.mod h1:Fyux9zXlo4rWoMSIzpn9fDAYjalPqJ/K1qJ27s+7ltE= gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= -gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= -gopkg.in/go-playground/mold.v2 v2.2.0/go.mod h1:XMyyRsGtakkDPbxXbrA5VODo6bUXyvoDjLd5l3T0XoA= gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= -gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw= -gopkg.in/gorp.v1 v1.7.2/go.mod h1:Wo3h+DBQZIxATwftsglhdD/62zRFPhGhTiu5jUJmCaw= gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/ini.v1 v1.55.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.66.6/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/mail.v2 v2.0.0-20180731213649-a0242b2233b4/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw= gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk= gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw= gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= -gopkg.in/square/go-jose.v2 v2.1.9/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= -gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= -gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= -gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/telebot.v3 v3.2.1/go.mod h1:GJKwwWqp9nSkIVN51eRKU78aB5f5OnQuWdwiIZfPbko= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/validator.v2 v2.0.0-20180514200540-135c24b11c19/go.mod h1:o4V0GXN9/CAmCsvJ0oXYZvrZOe7syiDZSN1GWGZTGzc= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -4977,7 +4280,6 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= @@ -5004,7 +4306,6 @@ honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= -howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0= k8s.io/api v0.28.4/go.mod h1:axWTGrY88s/5YE+JSt4uUi6NMM+gur1en2REMR7IRj0= k8s.io/api v0.29.2 h1:hBC7B9+MU+ptchxEqTNW2DkUosJpp1P+Wn6YncZ474A= k8s.io/apimachinery v0.28.4/go.mod h1:wI37ncBvfAoswfq626yPTe6Bz1c22L7uaJ8dho83mgg= @@ -5037,7 +4338,6 @@ lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= lukechampine.com/uint128 v1.3.0 h1:cDdUVfRwDUDovz610ABgFD17nXD4/uDgVHl2sC3+sbo= lukechampine.com/uint128 v1.3.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= -modernc.org/cc v1.0.0/go.mod h1:1Sk4//wdnYJiUIxnW8ddKpaOJCF37yAdqYnkxUpaYxw= modernc.org/cc/v3 v3.36.0/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= modernc.org/cc/v3 v3.36.2/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= modernc.org/cc/v3 v3.36.3/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= @@ -5056,7 +4356,6 @@ modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw= modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY= modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk= modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ= -modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk= modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM= modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= modernc.org/libc v0.0.0-20220428101251-2d5f3daf273b/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA= @@ -5073,7 +4372,6 @@ modernc.org/libc v1.21.4/go.mod h1:przBsL5RDOZajTVslkugzLBj1evTue36jEomFQOoYuI= modernc.org/libc v1.22.2/go.mod h1:uvQavJ1pZ0hIoC/jfqNoMLURIMhKzINIWypNM17puug= modernc.org/libc v1.22.4 h1:wymSbZb0AlrjdAVX3cjreCHTPCpPARbQXNz6BHPzdwQ= modernc.org/libc v1.22.4/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY= -modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k= modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= @@ -5092,7 +4390,6 @@ modernc.org/sqlite v1.18.1/go.mod h1:6ho+Gow7oX5V+OiOQ6Tr4xeqbx13UZ6t+Fw9IRUG4d4 modernc.org/sqlite v1.18.2/go.mod h1:kvrTLEWgxUcHa2GfHBQtanR1H9ht3hTJNtKpzH9k1u0= modernc.org/sqlite v1.21.2 h1:ixuUG0QS413Vfzyx6FWx6PYTmHaOegTY+hjzhn7L+a0= modernc.org/sqlite v1.21.2/go.mod h1:cxbLkB5WS32DnQqeH4h4o1B0eMr8W/y8/RGuxQ3JsC0= -modernc.org/strutil v1.1.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs= modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw= modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY= modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= @@ -5104,7 +4401,6 @@ modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= -modernc.org/xc v1.0.0/go.mod h1:mRNCo0bvLjGhHO9WsyuKVU4q0ceiDDDoEeWDJHrNx8I= modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8= modernc.org/z v1.7.0 h1:xkDw/KepgEjeizO2sNco+hqYkU12taxQFqPEmgm1GWE= modernc.org/z v1.7.0/go.mod h1:hVdgNMh8ggTuRG1rGU8x+xGRFfiQUIAw0ZqlPy8+HyQ= diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 501ccfb269..48f8b5b5f9 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -77,7 +77,6 @@ export interface FeatureToggles { alertStateHistoryLokiOnly?: boolean; unifiedRequestLog?: boolean; renderAuthJWT?: boolean; - externalServiceAuth?: boolean; refactorVariablesTimeRange?: boolean; enableElasticsearchBackendQuerying?: boolean; faroDatasourceSelector?: boolean; diff --git a/pkg/plugins/manager/testdata/oauth-external-registration/plugin.json b/pkg/plugins/manager/testdata/oauth-external-registration/plugin.json deleted file mode 100644 index 1f51c5ef91..0000000000 --- a/pkg/plugins/manager/testdata/oauth-external-registration/plugin.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "id": "grafana-test-datasource", - "type": "datasource", - "name": "Test", - "backend": true, - "executable": "gpx_test_datasource", - "info": { - "author": { - "name": "Grafana Labs", - "url": "https://grafana.com" - }, - "logos": { - "large": "img/ds.svg", - "small": "img/ds.svg" - }, - "screenshots": [], - "updated": "2023-08-03", - "version": "1.0.0" - }, - "iam": { - "impersonation": { - "groups" : true, - "permissions" : [ - { - "action": "read", - "scope": "datasource" - } - ] - }, - "permissions" : [ - { - "action": "read", - "scope": "datasource" - } - ] - } -} diff --git a/pkg/plugins/pfs/pfs_test.go b/pkg/plugins/pfs/pfs_test.go index e98fd66b83..8980e59345 100644 --- a/pkg/plugins/pfs/pfs_test.go +++ b/pkg/plugins/pfs/pfs_test.go @@ -142,9 +142,6 @@ func TestParsePluginTestdata(t *testing.T) { "external-registration": { rootid: "grafana-test-datasource", }, - "oauth-external-registration": { - rootid: "grafana-test-datasource", - }, } staticRootPath, err := filepath.Abs(filepath.Join("..", "manager", "testdata")) diff --git a/pkg/plugins/plugindef/plugindef.cue b/pkg/plugins/plugindef/plugindef.cue index 40ab7c2575..89255479fc 100644 --- a/pkg/plugins/plugindef/plugindef.cue +++ b/pkg/plugins/plugindef/plugindef.cue @@ -422,20 +422,6 @@ schemas: [{ #IAM: { // Permissions are the permissions that the external service needs its associated service account to have. permissions?: [...#Permission] - - // Impersonation describes the permissions that the external service will have on behalf of the user - // This is only available with the OAuth2 Server - impersonation?: #Impersonation - } - - #Impersonation: { - // Groups allows the service to list the impersonated user's teams. - // Defaults to true. - groups?: bool - // Permissions are the permissions that the external service needs when impersonating a user. - // The intersection of this set with the impersonated user's permission guarantees that the client will not - // gain more privileges than the impersonated user has. - permissions?: [...#Permission] } } }] diff --git a/pkg/plugins/plugindef/plugindef_types_gen.go b/pkg/plugins/plugindef/plugindef_types_gen.go index d4ec3ded09..1b5cb5e152 100644 --- a/pkg/plugins/plugindef/plugindef_types_gen.go +++ b/pkg/plugins/plugindef/plugindef_types_gen.go @@ -132,24 +132,10 @@ type Header struct { // IAM allows the plugin to get a service account with tailored permissions and a token // (or to use the client_credentials grant if the token provider is the OAuth2 Server) type IAM struct { - Impersonation *Impersonation `json:"impersonation,omitempty"` - // Permissions are the permissions that the external service needs its associated service account to have. Permissions []Permission `json:"permissions,omitempty"` } -// Impersonation defines model for Impersonation. -type Impersonation struct { - // Groups allows the service to list the impersonated user's teams. - // Defaults to true. - Groups *bool `json:"groups,omitempty"` - - // Permissions are the permissions that the external service needs when impersonating a user. - // The intersection of this set with the impersonated user's permission guarantees that the client will not - // gain more privileges than the impersonated user has. - Permissions []Permission `json:"permissions,omitempty"` -} - // A resource to be included in a plugin. type Include struct { // RBAC action the user must have to access the route diff --git a/pkg/server/wire.go b/pkg/server/wire.go index adf79d115c..09862887f5 100644 --- a/pkg/server/wire.go +++ b/pkg/server/wire.go @@ -67,8 +67,6 @@ import ( "github.com/grafana/grafana/pkg/services/encryption" encryptionservice "github.com/grafana/grafana/pkg/services/encryption/service" "github.com/grafana/grafana/pkg/services/extsvcauth" - "github.com/grafana/grafana/pkg/services/extsvcauth/oauthserver" - "github.com/grafana/grafana/pkg/services/extsvcauth/oauthserver/oasimpl" extsvcreg "github.com/grafana/grafana/pkg/services/extsvcauth/registry" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" @@ -372,8 +370,6 @@ var wireBasicSet = wire.NewSet( supportbundlesimpl.ProvideService, extsvcaccounts.ProvideExtSvcAccountsService, wire.Bind(new(serviceaccounts.ExtSvcAccountsService), new(*extsvcaccounts.ExtSvcAccountsService)), - oasimpl.ProvideService, - wire.Bind(new(oauthserver.OAuth2Server), new(*oasimpl.OAuth2ServiceImpl)), extsvcreg.ProvideExtSvcRegistry, wire.Bind(new(extsvcauth.ExternalServiceRegistry), new(*extsvcreg.Registry)), anonstore.ProvideAnonDBStore, diff --git a/pkg/services/accesscontrol/accesscontrol.go b/pkg/services/accesscontrol/accesscontrol.go index a4d61ed99c..a7ced89eb6 100644 --- a/pkg/services/accesscontrol/accesscontrol.go +++ b/pkg/services/accesscontrol/accesscontrol.go @@ -292,110 +292,6 @@ func Reduce(ps []Permission) map[string][]string { return reduced } -// intersectScopes computes the minimal list of scopes common to two slices. -func intersectScopes(s1, s2 []string) []string { - if len(s1) == 0 || len(s2) == 0 { - return []string{} - } - - // helpers - splitScopes := func(s []string) (map[string]bool, map[string]bool) { - scopes := make(map[string]bool) - wildcards := make(map[string]bool) - for _, s := range s { - if isWildcard(s) { - wildcards[s] = true - } else { - scopes[s] = true - } - } - return scopes, wildcards - } - includes := func(wildcardsSet map[string]bool, scope string) bool { - for wildcard := range wildcardsSet { - if wildcard == "*" || strings.HasPrefix(scope, wildcard[:len(wildcard)-1]) { - return true - } - } - return false - } - - res := make([]string, 0) - - // split input into scopes and wildcards - s1Scopes, s1Wildcards := splitScopes(s1) - s2Scopes, s2Wildcards := splitScopes(s2) - - // intersect wildcards - wildcards := make(map[string]bool) - for s := range s1Wildcards { - // if s1 wildcard is included in s2 wildcards - // then it is included in the intersection - if includes(s2Wildcards, s) { - wildcards[s] = true - continue - } - } - for s := range s2Wildcards { - // if s2 wildcard is included in s1 wildcards - // then it is included in the intersection - if includes(s1Wildcards, s) { - wildcards[s] = true - } - } - - // intersect scopes - scopes := make(map[string]bool) - for s := range s1Scopes { - // if s1 scope is included in s2 wilcards or s2 scopes - // then it is included in the intersection - if includes(s2Wildcards, s) || s2Scopes[s] { - scopes[s] = true - } - } - for s := range s2Scopes { - // if s2 scope is included in s1 wilcards - // then it is included in the intersection - if includes(s1Wildcards, s) { - scopes[s] = true - } - } - - // merge wildcards and scopes - for w := range wildcards { - res = append(res, w) - } - for s := range scopes { - res = append(res, s) - } - - return res -} - -// Intersect returns the intersection of two slices of permissions, grouping scopes by action. -func Intersect(p1, p2 []Permission) map[string][]string { - if len(p1) == 0 || len(p2) == 0 { - return map[string][]string{} - } - - res := make(map[string][]string) - p1m := Reduce(p1) - p2m := Reduce(p2) - - // Loop over the smallest map - if len(p1m) > len(p2m) { - p1m, p2m = p2m, p1m - } - - for a1, s1 := range p1m { - if s2, ok := p2m[a1]; ok { - res[a1] = intersectScopes(s1, s2) - } - } - - return res -} - func ValidateScope(scope string) bool { prefix, last := scope[:len(scope)-1], scope[len(scope)-1] // verify that last char is either ':' or '/' if last character of scope is '*' diff --git a/pkg/services/accesscontrol/accesscontrol_test.go b/pkg/services/accesscontrol/accesscontrol_test.go index 1b13136cb7..b39ca3b8a8 100644 --- a/pkg/services/accesscontrol/accesscontrol_test.go +++ b/pkg/services/accesscontrol/accesscontrol_test.go @@ -1,7 +1,6 @@ package accesscontrol import ( - "fmt" "testing" "github.com/stretchr/testify/require" @@ -126,210 +125,3 @@ func TestReduce(t *testing.T) { }) } } - -func TestIntersect(t *testing.T) { - tests := []struct { - name string - p1 []Permission - p2 []Permission - want map[string][]string - }{ - { - name: "no permission", - p1: []Permission{}, - p2: []Permission{}, - want: map[string][]string{}, - }, - { - name: "no intersection", - p1: []Permission{{Action: "orgs:read"}}, - p2: []Permission{{Action: "orgs:write"}}, - want: map[string][]string{}, - }, - { - name: "intersection no scopes", - p1: []Permission{{Action: "orgs:read"}}, - p2: []Permission{{Action: "orgs:read"}}, - want: map[string][]string{"orgs:read": {}}, - }, - { - name: "unbalanced intersection", - p1: []Permission{{Action: "teams:read", Scope: "teams:id:1"}}, - p2: []Permission{{Action: "teams:read"}}, - want: map[string][]string{"teams:read": {}}, - }, - { - name: "intersection", - p1: []Permission{ - {Action: "teams:read", Scope: "teams:id:1"}, - {Action: "teams:read", Scope: "teams:id:2"}, - {Action: "teams:write", Scope: "teams:id:1"}, - }, - p2: []Permission{ - {Action: "teams:read", Scope: "teams:id:1"}, - {Action: "teams:read", Scope: "teams:id:3"}, - {Action: "teams:write", Scope: "teams:id:1"}, - }, - want: map[string][]string{ - "teams:read": {"teams:id:1"}, - "teams:write": {"teams:id:1"}, - }, - }, - { - name: "intersection with wildcards", - p1: []Permission{ - {Action: "teams:read", Scope: "teams:id:1"}, - {Action: "teams:read", Scope: "teams:id:2"}, - {Action: "teams:write", Scope: "teams:id:1"}, - }, - p2: []Permission{ - {Action: "teams:read", Scope: "*"}, - {Action: "teams:write", Scope: "*"}, - }, - want: map[string][]string{ - "teams:read": {"teams:id:1", "teams:id:2"}, - "teams:write": {"teams:id:1"}, - }, - }, - { - name: "intersection with wildcards on both sides", - p1: []Permission{ - {Action: "dashboards:read", Scope: "dashboards:uid:1"}, - {Action: "dashboards:read", Scope: "folders:uid:1"}, - {Action: "dashboards:read", Scope: "dashboards:uid:*"}, - {Action: "folders:read", Scope: "folders:uid:1"}, - }, - p2: []Permission{ - {Action: "dashboards:read", Scope: "folders:uid:*"}, - {Action: "dashboards:read", Scope: "dashboards:uid:*"}, - {Action: "folders:read", Scope: "folders:uid:*"}, - }, - want: map[string][]string{ - "dashboards:read": {"dashboards:uid:*", "folders:uid:1"}, - "folders:read": {"folders:uid:1"}, - }, - }, - { - name: "intersection with wildcards of different sizes", - p1: []Permission{ - {Action: "dashboards:read", Scope: "folders:uid:1"}, - {Action: "dashboards:read", Scope: "dashboards:*"}, - {Action: "folders:read", Scope: "folders:*"}, - {Action: "teams:read", Scope: "teams:id:1"}, - }, - p2: []Permission{ - {Action: "dashboards:read", Scope: "folders:uid:*"}, - {Action: "dashboards:read", Scope: "dashboards:uid:*"}, - {Action: "folders:read", Scope: "folders:uid:*"}, - {Action: "teams:read", Scope: "*"}, - }, - want: map[string][]string{ - "dashboards:read": {"dashboards:uid:*", "folders:uid:1"}, - "folders:read": {"folders:uid:*"}, - "teams:read": {"teams:id:1"}, - }, - }, - } - check := func(t *testing.T, want map[string][]string, p1, p2 []Permission) { - intersect := Intersect(p1, p2) - for action, scopes := range intersect { - want, ok := want[action] - require.True(t, ok) - require.ElementsMatch(t, scopes, want, fmt.Sprintf("scopes for %v differs from expected", action)) - } - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Intersect is commutative - check(t, tt.want, tt.p1, tt.p2) - check(t, tt.want, tt.p2, tt.p1) - }) - } -} - -func Test_intersectScopes(t *testing.T) { - tests := []struct { - name string - s1 []string - s2 []string - want []string - }{ - { - name: "no values", - s1: []string{}, - s2: []string{}, - want: []string{}, - }, - { - name: "no values on one side", - s1: []string{}, - s2: []string{"teams:id:1"}, - want: []string{}, - }, - { - name: "empty values on one side", - s1: []string{""}, - s2: []string{"team:id:1"}, - want: []string{}, - }, - { - name: "no intersection", - s1: []string{"teams:id:1"}, - s2: []string{"teams:id:2"}, - want: []string{}, - }, - { - name: "intersection", - s1: []string{"teams:id:1"}, - s2: []string{"teams:id:1"}, - want: []string{"teams:id:1"}, - }, - { - name: "intersection with wildcard", - s1: []string{"teams:id:1", "teams:id:2"}, - s2: []string{"teams:id:*"}, - want: []string{"teams:id:1", "teams:id:2"}, - }, - { - name: "intersection of wildcards", - s1: []string{"teams:id:*"}, - s2: []string{"teams:id:*"}, - want: []string{"teams:id:*"}, - }, - { - name: "intersection with a bigger wildcards", - s1: []string{"teams:id:*"}, - s2: []string{"teams:*"}, - want: []string{"teams:id:*"}, - }, - { - name: "intersection of different wildcards with a bigger one", - s1: []string{"dashboards:uid:*", "folders:uid:*"}, - s2: []string{"*"}, - want: []string{"dashboards:uid:*", "folders:uid:*"}, - }, - { - name: "intersection with wildcards and scopes on both sides", - s1: []string{"dashboards:uid:*", "folders:uid:1"}, - s2: []string{"folders:uid:*", "dashboards:uid:1"}, - want: []string{"dashboards:uid:1", "folders:uid:1"}, - }, - { - name: "intersection of non reduced list of scopes", - s1: []string{"dashboards:uid:*", "dashboards:*", "dashboards:uid:1"}, - s2: []string{"dashboards:uid:*", "dashboards:*", "dashboards:uid:2"}, - want: []string{"dashboards:uid:*", "dashboards:*", "dashboards:uid:1", "dashboards:uid:2"}, - }, - } - check := func(t *testing.T, want []string, s1, s2 []string) { - intersect := intersectScopes(s1, s2) - require.ElementsMatch(t, want, intersect) - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Intersect is commutative - check(t, tt.want, tt.s1, tt.s2) - check(t, tt.want, tt.s2, tt.s1) - }) - } -} diff --git a/pkg/services/accesscontrol/acimpl/service.go b/pkg/services/accesscontrol/acimpl/service.go index 004e700554..017e8b5599 100644 --- a/pkg/services/accesscontrol/acimpl/service.go +++ b/pkg/services/accesscontrol/acimpl/service.go @@ -427,7 +427,7 @@ func PermissionMatchesSearchOptions(permission accesscontrol.Permission, searchO } func (s *Service) SaveExternalServiceRole(ctx context.Context, cmd accesscontrol.SaveExternalServiceRoleCommand) error { - if !(s.features.IsEnabled(ctx, featuremgmt.FlagExternalServiceAuth) || s.features.IsEnabled(ctx, featuremgmt.FlagExternalServiceAccounts)) { + if !s.features.IsEnabled(ctx, featuremgmt.FlagExternalServiceAccounts) { s.log.Debug("Registering an external service role is behind a feature flag, enable it to use this feature.") return nil } @@ -440,7 +440,7 @@ func (s *Service) SaveExternalServiceRole(ctx context.Context, cmd accesscontrol } func (s *Service) DeleteExternalServiceRole(ctx context.Context, externalServiceID string) error { - if !(s.features.IsEnabled(ctx, featuremgmt.FlagExternalServiceAuth) || s.features.IsEnabled(ctx, featuremgmt.FlagExternalServiceAccounts)) { + if !s.features.IsEnabled(ctx, featuremgmt.FlagExternalServiceAccounts) { s.log.Debug("Deleting an external service role is behind a feature flag, enable it to use this feature.") return nil } diff --git a/pkg/services/accesscontrol/acimpl/service_test.go b/pkg/services/accesscontrol/acimpl/service_test.go index 053448e48f..a36ed4cdfe 100644 --- a/pkg/services/accesscontrol/acimpl/service_test.go +++ b/pkg/services/accesscontrol/acimpl/service_test.go @@ -869,7 +869,7 @@ func TestService_SaveExternalServiceRole(t *testing.T) { t.Run(tt.name, func(t *testing.T) { ctx := context.Background() ac := setupTestEnv(t) - ac.features = featuremgmt.WithFeatures(featuremgmt.FlagExternalServiceAuth, featuremgmt.FlagExternalServiceAccounts) + ac.features = featuremgmt.WithFeatures(featuremgmt.FlagExternalServiceAccounts) for _, r := range tt.runs { err := ac.SaveExternalServiceRole(ctx, r.cmd) if r.wantErr { @@ -915,7 +915,7 @@ func TestService_DeleteExternalServiceRole(t *testing.T) { t.Run(tt.name, func(t *testing.T) { ctx := context.Background() ac := setupTestEnv(t) - ac.features = featuremgmt.WithFeatures(featuremgmt.FlagExternalServiceAuth, featuremgmt.FlagExternalServiceAccounts) + ac.features = featuremgmt.WithFeatures(featuremgmt.FlagExternalServiceAccounts) if tt.initCmd != nil { err := ac.SaveExternalServiceRole(ctx, *tt.initCmd) diff --git a/pkg/services/accesscontrol/models.go b/pkg/services/accesscontrol/models.go index 9b85f1159f..12bdbfcef0 100644 --- a/pkg/services/accesscontrol/models.go +++ b/pkg/services/accesscontrol/models.go @@ -341,9 +341,8 @@ const ( ActionAPIKeyDelete = "apikeys:delete" // Users actions - ActionUsersRead = "users:read" - ActionUsersWrite = "users:write" - ActionUsersImpersonate = "users:impersonate" + ActionUsersRead = "users:read" + ActionUsersWrite = "users:write" // We can ignore gosec G101 since this does not contain any credentials. // nolint:gosec diff --git a/pkg/services/authn/authnimpl/service.go b/pkg/services/authn/authnimpl/service.go index b436d098cf..cb7ef5f9f3 100644 --- a/pkg/services/authn/authnimpl/service.go +++ b/pkg/services/authn/authnimpl/service.go @@ -24,7 +24,6 @@ import ( "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/authn/authnimpl/sync" "github.com/grafana/grafana/pkg/services/authn/clients" - "github.com/grafana/grafana/pkg/services/extsvcauth/oauthserver" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/ldap/service" "github.com/grafana/grafana/pkg/services/login" @@ -72,7 +71,7 @@ func ProvideService( features *featuremgmt.FeatureManager, oauthTokenService oauthtoken.OAuthTokenService, socialService social.Service, cache *remotecache.RemoteCache, ldapService service.LDAP, registerer prometheus.Registerer, - signingKeysService signingkeys.Service, oauthServer oauthserver.OAuth2Server, + signingKeysService signingkeys.Service, settingsProviderService setting.Provider, ) *Service { s := &Service{ @@ -136,9 +135,10 @@ func ProvideService( s.RegisterClient(clients.ProvideJWT(jwtService, cfg)) } - if s.cfg.ExtendedJWTAuthEnabled && features.IsEnabledGlobally(featuremgmt.FlagExternalServiceAuth) { - s.RegisterClient(clients.ProvideExtendedJWT(userService, cfg, signingKeysService, oauthServer)) - } + // FIXME (gamab): Commenting that out for now as we want to re-use the client for external service auth + // if s.cfg.ExtendedJWTAuthEnabled && features.IsEnabledGlobally(featuremgmt.FlagExternalServiceAuth) { + // s.RegisterClient(clients.ProvideExtendedJWT(userService, cfg, signingKeysService, oauthServer)) + // } for name := range socialService.GetOAuthProviders() { clientName := authn.ClientWithPrefix(name) diff --git a/pkg/services/authn/clients/basic.go b/pkg/services/authn/clients/basic.go index bda9f3c1ed..d0c7050f5b 100644 --- a/pkg/services/authn/clients/basic.go +++ b/pkg/services/authn/clients/basic.go @@ -2,7 +2,6 @@ package clients import ( "context" - "strings" "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/util/errutil" @@ -43,10 +42,6 @@ func (c *Basic) Test(ctx context.Context, r *authn.Request) bool { if r.HTTPRequest == nil { return false } - // The OAuth2 introspection endpoint uses basic auth but is handled by the oauthserver package. - if strings.EqualFold(r.HTTPRequest.RequestURI, "/oauth2/introspect") { - return false - } return looksLikeBasicAuthRequest(r) } diff --git a/pkg/services/authn/clients/basic_test.go b/pkg/services/authn/clients/basic_test.go index 93fbd7d416..fbf2a96a2d 100644 --- a/pkg/services/authn/clients/basic_test.go +++ b/pkg/services/authn/clients/basic_test.go @@ -85,12 +85,6 @@ func TestBasic_Test(t *testing.T) { HTTPRequest: &http.Request{Header: map[string][]string{authorizationHeaderName: {"something"}}}, }, }, - { - desc: "should fail when the URL ends with /oauth2/introspect", - req: &authn.Request{ - HTTPRequest: &http.Request{Header: map[string][]string{authorizationHeaderName: {encodeBasicAuth("user", "password")}}, RequestURI: "/oauth2/introspect"}, - }, - }, } for _, tt := range tests { diff --git a/pkg/services/authn/clients/ext_jwt.go b/pkg/services/authn/clients/ext_jwt.go index 01d8a524bb..1fd3ef8b35 100644 --- a/pkg/services/authn/clients/ext_jwt.go +++ b/pkg/services/authn/clients/ext_jwt.go @@ -14,7 +14,6 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/authn" - "github.com/grafana/grafana/pkg/services/extsvcauth/oauthserver" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/signingkeys" "github.com/grafana/grafana/pkg/services/user" @@ -33,13 +32,12 @@ const ( rfc9068MediaType = "application/at+jwt" ) -func ProvideExtendedJWT(userService user.Service, cfg *setting.Cfg, signingKeys signingkeys.Service, oauthServer oauthserver.OAuth2Server) *ExtendedJWT { +func ProvideExtendedJWT(userService user.Service, cfg *setting.Cfg, signingKeys signingkeys.Service) *ExtendedJWT { return &ExtendedJWT{ cfg: cfg, log: log.New(authn.ClientExtendedJWT), userService: userService, signingKeys: signingKeys, - oauthServer: oauthServer, } } @@ -48,7 +46,6 @@ type ExtendedJWT struct { log log.Logger userService user.Service signingKeys signingkeys.Service - oauthServer oauthserver.OAuth2Server } type ExtendedJWTClaims struct { @@ -222,10 +219,6 @@ func (s *ExtendedJWT) validateClientIdClaim(ctx context.Context, claims Extended return fmt.Errorf("missing 'client_id' claim") } - if _, err := s.oauthServer.GetExternalService(ctx, claims.ClientID); err != nil { - return fmt.Errorf("invalid 'client_id' claim: %s", claims.ClientID) - } - return nil } diff --git a/pkg/services/authn/clients/ext_jwt_test.go b/pkg/services/authn/clients/ext_jwt_test.go index 5325baf3e3..64ff1c5095 100644 --- a/pkg/services/authn/clients/ext_jwt_test.go +++ b/pkg/services/authn/clients/ext_jwt_test.go @@ -17,8 +17,6 @@ import ( "github.com/grafana/grafana/pkg/models/roletype" "github.com/grafana/grafana/pkg/services/authn" - "github.com/grafana/grafana/pkg/services/extsvcauth/oauthserver" - "github.com/grafana/grafana/pkg/services/extsvcauth/oauthserver/oastest" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/signingkeys" "github.com/grafana/grafana/pkg/services/signingkeys/signingkeystest" @@ -268,27 +266,6 @@ func TestExtendedJWT_Authenticate(t *testing.T) { want: nil, wantErr: true, }, - { - name: "should return error when the client was not found", - payload: ExtendedJWTClaims{ - Claims: jwt.Claims{ - Issuer: "http://localhost:3000", - Subject: "user:id:2", - Audience: jwt.Audience{"http://localhost:3000"}, - ID: "1234567890", - Expiry: jwt.NewNumericDate(time.Date(2023, 5, 3, 0, 0, 0, 0, time.UTC)), - IssuedAt: jwt.NewNumericDate(time.Date(2023, 5, 2, 0, 0, 0, 0, time.UTC)), - }, - ClientID: "unknown-client-id", - Scopes: []string{"profile", "groups"}, - }, - initTestEnv: func(env *testEnv) { - env.oauthSvc.ExpectedErr = oauthserver.ErrClientNotFoundFn("unknown-client-id") - }, - orgID: 1, - want: nil, - wantErr: true, - }, } for _, tc := range testCases { @@ -521,21 +498,18 @@ func setupTestCtx(t *testing.T, cfg *setting.Cfg) *testEnv { } userSvc := &usertest.FakeUserService{} - oauthSvc := &oastest.FakeService{} - extJwtClient := ProvideExtendedJWT(userSvc, cfg, signingKeysSvc, oauthSvc) + extJwtClient := ProvideExtendedJWT(userSvc, cfg, signingKeysSvc) return &testEnv{ - oauthSvc: oauthSvc, - userSvc: userSvc, - s: extJwtClient, + userSvc: userSvc, + s: extJwtClient, } } type testEnv struct { - oauthSvc *oastest.FakeService - userSvc *usertest.FakeUserService - s *ExtendedJWT + userSvc *usertest.FakeUserService + s *ExtendedJWT } func generateToken(payload ExtendedJWTClaims, signingKey any, alg jose.SignatureAlgorithm) string { diff --git a/pkg/services/extsvcauth/models.go b/pkg/services/extsvcauth/models.go index 717eb42f67..c2a71dde45 100644 --- a/pkg/services/extsvcauth/models.go +++ b/pkg/services/extsvcauth/models.go @@ -7,7 +7,6 @@ import ( ) const ( - OAuth2Server AuthProvider = "OAuth2Server" ServiceAccounts AuthProvider = "ServiceAccounts" // TmpOrgID is the orgID we use while global service accounts are not supported. @@ -40,23 +39,9 @@ type SelfCfg struct { Permissions []accesscontrol.Permission } -type ImpersonationCfg struct { - // Enabled allows the service to request access tokens to impersonate users - Enabled bool - // Groups allows the service to list the impersonated user's teams - Groups bool - // Permissions are the permissions that the external service needs when impersonating a user. - // The intersection of this set with the impersonated user's permission guarantees that the client will not - // gain more privileges than the impersonated user has and vice versa. - Permissions []accesscontrol.Permission -} - // ExternalServiceRegistration represents the registration form to save new client. type ExternalServiceRegistration struct { Name string - // Impersonation access configuration - // (this is not available on all auth providers) - Impersonation ImpersonationCfg // Self access configuration Self SelfCfg // Auth Provider that the client will use to connect to Grafana diff --git a/pkg/services/extsvcauth/oauthserver/api/api.go b/pkg/services/extsvcauth/oauthserver/api/api.go deleted file mode 100644 index 921a01cb1d..0000000000 --- a/pkg/services/extsvcauth/oauthserver/api/api.go +++ /dev/null @@ -1,37 +0,0 @@ -package api - -import ( - "github.com/grafana/grafana/pkg/api/routing" - contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" - "github.com/grafana/grafana/pkg/services/extsvcauth/oauthserver" -) - -type api struct { - router routing.RouteRegister - oauthServer oauthserver.OAuth2Server -} - -func NewAPI( - router routing.RouteRegister, - oauthServer oauthserver.OAuth2Server, -) *api { - return &api{ - router: router, - oauthServer: oauthServer, - } -} - -func (a *api) RegisterAPIEndpoints() { - a.router.Group("/oauth2", func(oauthRouter routing.RouteRegister) { - oauthRouter.Post("/introspect", a.handleIntrospectionRequest) - oauthRouter.Post("/token", a.handleTokenRequest) - }) -} - -func (a *api) handleTokenRequest(c *contextmodel.ReqContext) { - a.oauthServer.HandleTokenRequest(c.Resp, c.Req) -} - -func (a *api) handleIntrospectionRequest(c *contextmodel.ReqContext) { - a.oauthServer.HandleIntrospectionRequest(c.Resp, c.Req) -} diff --git a/pkg/services/extsvcauth/oauthserver/errors.go b/pkg/services/extsvcauth/oauthserver/errors.go deleted file mode 100644 index 942245b362..0000000000 --- a/pkg/services/extsvcauth/oauthserver/errors.go +++ /dev/null @@ -1,25 +0,0 @@ -package oauthserver - -import ( - "github.com/grafana/grafana/pkg/util/errutil" -) - -var ( - ErrClientNotFoundMessageID = "oauthserver.client-not-found" -) - -var ( - ErrClientRequiredID = errutil.BadRequest( - "oauthserver.required-client-id", - errutil.WithPublicMessage("client ID is required")).Errorf("Client ID is required") - ErrClientRequiredName = errutil.BadRequest( - "oauthserver.required-client-name", - errutil.WithPublicMessage("client name is required")).Errorf("Client name is required") - ErrClientNotFound = errutil.NotFound( - ErrClientNotFoundMessageID, - errutil.WithPublicMessage("Requested client has not been found")) -) - -func ErrClientNotFoundFn(clientID string) error { - return ErrClientNotFound.Errorf("client '%s' not found", clientID) -} diff --git a/pkg/services/extsvcauth/oauthserver/external_service.go b/pkg/services/extsvcauth/oauthserver/external_service.go deleted file mode 100644 index bff2bf9346..0000000000 --- a/pkg/services/extsvcauth/oauthserver/external_service.go +++ /dev/null @@ -1,153 +0,0 @@ -package oauthserver - -import ( - "context" - "strconv" - "strings" - - "github.com/ory/fosite" - - ac "github.com/grafana/grafana/pkg/services/accesscontrol" - "github.com/grafana/grafana/pkg/services/extsvcauth" - "github.com/grafana/grafana/pkg/services/user" -) - -type OAuthExternalService struct { - ID int64 `xorm:"id pk autoincr"` - Name string `xorm:"name"` - ClientID string `xorm:"client_id"` - Secret string `xorm:"secret"` - RedirectURI string `xorm:"redirect_uri"` // Not used yet (code flow) - GrantTypes string `xorm:"grant_types"` // CSV value - Audiences string `xorm:"audiences"` // CSV value - PublicPem []byte `xorm:"public_pem"` - ServiceAccountID int64 `xorm:"service_account_id"` - // SelfPermissions are the registered service account permissions (registered and managed permissions) - SelfPermissions []ac.Permission - // ImpersonatePermissions is the restriction set of permissions while impersonating - ImpersonatePermissions []ac.Permission - - // SignedInUser refers to the current Service Account identity/user - SignedInUser *user.SignedInUser - Scopes []string - ImpersonateScopes []string -} - -// ToExternalService converts the ExternalService (used internally by the oauthserver) to extsvcauth.ExternalService (used outside the package) -// If object must contain Key pairs, pass them as parameters, otherwise only the client PublicPem will be added. -func (c *OAuthExternalService) ToExternalService(keys *extsvcauth.KeyResult) *extsvcauth.ExternalService { - c2 := &extsvcauth.ExternalService{ - ID: c.ClientID, - Name: c.Name, - Secret: c.Secret, - OAuthExtra: &extsvcauth.OAuthExtra{ - GrantTypes: c.GrantTypes, - Audiences: c.Audiences, - RedirectURI: c.RedirectURI, - KeyResult: keys, - }, - } - - // Fallback to only display the public pem - if keys == nil && len(c.PublicPem) > 0 { - c2.OAuthExtra.KeyResult = &extsvcauth.KeyResult{PublicPem: string(c.PublicPem)} - } - - return c2 -} - -func (c *OAuthExternalService) LogID() string { - return "{name: " + c.Name + ", clientID: " + c.ClientID + "}" -} - -// GetID returns the client ID. -func (c *OAuthExternalService) GetID() string { return c.ClientID } - -// GetHashedSecret returns the hashed secret as it is stored in the store. -func (c *OAuthExternalService) GetHashedSecret() []byte { - // Hashed version is stored in the secret field - return []byte(c.Secret) -} - -// GetRedirectURIs returns the client's allowed redirect URIs. -func (c *OAuthExternalService) GetRedirectURIs() []string { - return []string{c.RedirectURI} -} - -// GetGrantTypes returns the client's allowed grant types. -func (c *OAuthExternalService) GetGrantTypes() fosite.Arguments { - return strings.Split(c.GrantTypes, ",") -} - -// GetResponseTypes returns the client's allowed response types. -// All allowed combinations of response types have to be listed, each combination having -// response types of the combination separated by a space. -func (c *OAuthExternalService) GetResponseTypes() fosite.Arguments { - return fosite.Arguments{"code"} -} - -// GetScopes returns the scopes this client is allowed to request on its own behalf. -func (c *OAuthExternalService) GetScopes() fosite.Arguments { - if c.Scopes != nil { - return c.Scopes - } - - ret := []string{"profile", "email", "groups", "entitlements"} - if c.SignedInUser != nil && c.SignedInUser.Permissions != nil { - perms := c.SignedInUser.Permissions[TmpOrgID] - for action := range perms { - // Add all actions that the plugin is allowed to request - ret = append(ret, action) - } - } - - c.Scopes = ret - return ret -} - -// GetScopes returns the scopes this client is allowed to request on a specific user. -func (c *OAuthExternalService) GetScopesOnUser(ctx context.Context, accessControl ac.AccessControl, userID int64) []string { - ev := ac.EvalPermission(ac.ActionUsersImpersonate, ac.Scope("users", "id", strconv.FormatInt(userID, 10))) - hasAccess, errAccess := accessControl.Evaluate(ctx, c.SignedInUser, ev) - if errAccess != nil || !hasAccess { - return nil - } - - if c.ImpersonateScopes != nil { - return c.ImpersonateScopes - } - - ret := []string{} - if c.ImpersonatePermissions != nil { - perms := c.ImpersonatePermissions - for i := range perms { - if perms[i].Action == ac.ActionUsersRead && perms[i].Scope == ScopeGlobalUsersSelf { - ret = append(ret, "profile", "email", ac.ActionUsersRead) - continue - } - if perms[i].Action == ac.ActionUsersPermissionsRead && perms[i].Scope == ScopeUsersSelf { - ret = append(ret, "entitlements", ac.ActionUsersPermissionsRead) - continue - } - if perms[i].Action == ac.ActionTeamsRead && perms[i].Scope == ScopeTeamsSelf { - ret = append(ret, "groups", ac.ActionTeamsRead) - continue - } - // Add all actions that the plugin is allowed to request - ret = append(ret, perms[i].Action) - } - } - - c.ImpersonateScopes = ret - return ret -} - -// IsPublic returns true, if this client is marked as public. -func (c *OAuthExternalService) IsPublic() bool { - return false -} - -// GetAudience returns the allowed audience(s) for this client. -func (c *OAuthExternalService) GetAudience() fosite.Arguments { - return strings.Split(c.Audiences, ",") -} diff --git a/pkg/services/extsvcauth/oauthserver/external_service_test.go b/pkg/services/extsvcauth/oauthserver/external_service_test.go deleted file mode 100644 index 67e4011738..0000000000 --- a/pkg/services/extsvcauth/oauthserver/external_service_test.go +++ /dev/null @@ -1,213 +0,0 @@ -package oauthserver - -import ( - "context" - "testing" - - "github.com/stretchr/testify/require" - - ac "github.com/grafana/grafana/pkg/services/accesscontrol" - "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" - "github.com/grafana/grafana/pkg/services/dashboards" - "github.com/grafana/grafana/pkg/services/user" - "github.com/grafana/grafana/pkg/setting" -) - -func setupTestEnv(t *testing.T) *OAuthExternalService { - t.Helper() - - client := &OAuthExternalService{ - Name: "my-ext-service", - ClientID: "RANDOMID", - Secret: "RANDOMSECRET", - GrantTypes: "client_credentials,urn:ietf:params:oauth:grant-type:jwt-bearer", - ServiceAccountID: 2, - SelfPermissions: []ac.Permission{ - {Action: ac.ActionUsersImpersonate, Scope: ac.ScopeUsersAll}, - }, - SignedInUser: &user.SignedInUser{ - UserID: 2, - OrgID: 1, - }, - } - return client -} - -func TestExternalService_GetScopesOnUser(t *testing.T) { - testCases := []struct { - name string - impersonatePermissions []ac.Permission - initTestEnv func(*OAuthExternalService) - expectedScopes []string - }{ - { - name: "should return nil when the service account has no impersonate permissions", - expectedScopes: nil, - }, - { - name: "should return the 'profile', 'email' and associated RBAC action", - initTestEnv: func(c *OAuthExternalService) { - c.SignedInUser.Permissions = map[int64]map[string][]string{ - 1: { - ac.ActionUsersImpersonate: {ac.ScopeUsersAll}, - }, - } - c.ImpersonatePermissions = []ac.Permission{ - {Action: ac.ActionUsersRead, Scope: ScopeGlobalUsersSelf}, - } - }, - expectedScopes: []string{"profile", "email", ac.ActionUsersRead}, - }, - { - name: "should return 'entitlements' and associated RBAC action scopes", - initTestEnv: func(c *OAuthExternalService) { - c.SignedInUser.Permissions = map[int64]map[string][]string{ - 1: { - ac.ActionUsersImpersonate: {ac.ScopeUsersAll}, - }, - } - c.ImpersonatePermissions = []ac.Permission{ - {Action: ac.ActionUsersPermissionsRead, Scope: ScopeUsersSelf}, - } - }, - expectedScopes: []string{"entitlements", ac.ActionUsersPermissionsRead}, - }, - { - name: "should return 'groups' and associated RBAC action scopes", - initTestEnv: func(c *OAuthExternalService) { - c.SignedInUser.Permissions = map[int64]map[string][]string{ - 1: { - ac.ActionUsersImpersonate: {ac.ScopeUsersAll}, - }, - } - c.ImpersonatePermissions = []ac.Permission{ - {Action: ac.ActionTeamsRead, Scope: ScopeTeamsSelf}, - } - }, - expectedScopes: []string{"groups", ac.ActionTeamsRead}, - }, - { - name: "should return all scopes", - initTestEnv: func(c *OAuthExternalService) { - c.SignedInUser.Permissions = map[int64]map[string][]string{ - 1: { - ac.ActionUsersImpersonate: {ac.ScopeUsersAll}, - }, - } - c.ImpersonatePermissions = []ac.Permission{ - {Action: ac.ActionUsersRead, Scope: ScopeGlobalUsersSelf}, - {Action: ac.ActionUsersPermissionsRead, Scope: ScopeUsersSelf}, - {Action: ac.ActionTeamsRead, Scope: ScopeTeamsSelf}, - {Action: dashboards.ActionDashboardsRead, Scope: dashboards.ScopeDashboardsAll}, - } - }, - expectedScopes: []string{"profile", "email", ac.ActionUsersRead, - "entitlements", ac.ActionUsersPermissionsRead, - "groups", ac.ActionTeamsRead, - "dashboards:read"}, - }, - { - name: "should return stored scopes when the client's impersonate scopes has already been set", - initTestEnv: func(c *OAuthExternalService) { - c.SignedInUser.Permissions = map[int64]map[string][]string{ - 1: { - ac.ActionUsersImpersonate: {ac.ScopeUsersAll}, - }, - } - c.ImpersonateScopes = []string{"dashboard:create", "profile", "email", "entitlements", "groups"} - }, - expectedScopes: []string{"profile", "email", "entitlements", "groups", "dashboard:create"}, - }, - } - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - c := setupTestEnv(t) - if tc.initTestEnv != nil { - tc.initTestEnv(c) - } - scopes := c.GetScopesOnUser(context.Background(), acimpl.ProvideAccessControl(setting.NewCfg()), 3) - require.ElementsMatch(t, tc.expectedScopes, scopes) - }) - } -} - -func TestExternalService_GetScopes(t *testing.T) { - testCases := []struct { - name string - impersonatePermissions []ac.Permission - initTestEnv func(*OAuthExternalService) - expectedScopes []string - }{ - { - name: "should return default scopes when the signed in user is nil", - initTestEnv: func(c *OAuthExternalService) { - c.SignedInUser = nil - }, - expectedScopes: []string{"profile", "email", "entitlements", "groups"}, - }, - { - name: "should return default scopes when the signed in user has no permissions", - initTestEnv: func(c *OAuthExternalService) { - c.SignedInUser.Permissions = map[int64]map[string][]string{} - }, - expectedScopes: []string{"profile", "email", "entitlements", "groups"}, - }, - { - name: "should return additional scopes from signed in user's permissions", - initTestEnv: func(c *OAuthExternalService) { - c.SignedInUser.Permissions = map[int64]map[string][]string{ - 1: { - dashboards.ActionDashboardsRead: {dashboards.ScopeDashboardsAll}, - }, - } - }, - expectedScopes: []string{"profile", "email", "entitlements", "groups", "dashboards:read"}, - }, - { - name: "should return stored scopes when the client's scopes has already been set", - initTestEnv: func(c *OAuthExternalService) { - c.Scopes = []string{"profile", "email", "entitlements", "groups"} - }, - expectedScopes: []string{"profile", "email", "entitlements", "groups"}, - }, - } - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - c := setupTestEnv(t) - if tc.initTestEnv != nil { - tc.initTestEnv(c) - } - scopes := c.GetScopes() - require.ElementsMatch(t, tc.expectedScopes, scopes) - }) - } -} - -func TestExternalService_ToDTO(t *testing.T) { - client := &OAuthExternalService{ - ID: 1, - Name: "my-ext-service", - ClientID: "test", - Secret: "testsecret", - RedirectURI: "http://localhost:3000", - GrantTypes: "client_credentials,urn:ietf:params:oauth:grant-type:jwt-bearer", - Audiences: "https://example.org,https://second.example.org", - PublicPem: []byte("pem_encoded_public_key"), - } - - dto := client.ToExternalService(nil) - - require.Equal(t, client.ClientID, dto.ID) - require.Equal(t, client.Name, dto.Name) - require.Equal(t, client.Secret, dto.Secret) - - require.NotNil(t, dto.OAuthExtra) - - require.Equal(t, client.RedirectURI, dto.OAuthExtra.RedirectURI) - require.Equal(t, client.GrantTypes, dto.OAuthExtra.GrantTypes) - require.Equal(t, client.Audiences, dto.OAuthExtra.Audiences) - require.Equal(t, client.PublicPem, []byte(dto.OAuthExtra.KeyResult.PublicPem)) - require.Empty(t, dto.OAuthExtra.KeyResult.PrivatePem) - require.Empty(t, dto.OAuthExtra.KeyResult.URL) - require.False(t, dto.OAuthExtra.KeyResult.Generated) -} diff --git a/pkg/services/extsvcauth/oauthserver/models.go b/pkg/services/extsvcauth/oauthserver/models.go deleted file mode 100644 index 70ba02ed31..0000000000 --- a/pkg/services/extsvcauth/oauthserver/models.go +++ /dev/null @@ -1,58 +0,0 @@ -package oauthserver - -import ( - "context" - "net/http" - - "github.com/grafana/grafana/pkg/services/extsvcauth" - "gopkg.in/square/go-jose.v2" -) - -const ( - // TmpOrgID is the orgID we use while global service accounts are not supported. - TmpOrgID int64 = 1 - // NoServiceAccountID is the ID we use for client that have no service account associated. - NoServiceAccountID int64 = 0 - - // List of scopes used to identify the impersonated user. - ScopeUsersSelf = "users:self" - ScopeGlobalUsersSelf = "global.users:self" - ScopeTeamsSelf = "teams:self" - - // Supported encryptions - RS256 = "RS256" - ES256 = "ES256" -) - -// OAuth2Server represents a service in charge of managing OAuth2 clients -// and handling OAuth2 requests (token, introspection). -type OAuth2Server interface { - // SaveExternalService creates or updates an external service in the database, it generates client_id and secrets and - // it ensures that the associated service account has the correct permissions. - SaveExternalService(ctx context.Context, cmd *extsvcauth.ExternalServiceRegistration) (*extsvcauth.ExternalService, error) - // GetExternalService retrieves an external service from store by client_id. It populates the SelfPermissions and - // SignedInUser from the associated service account. - GetExternalService(ctx context.Context, id string) (*OAuthExternalService, error) - // RemoveExternalService removes an external service and its associated resources from the store. - RemoveExternalService(ctx context.Context, name string) error - - // HandleTokenRequest handles the client's OAuth2 query to obtain an access_token by presenting its authorization - // grant (ex: client_credentials, jwtbearer). - HandleTokenRequest(rw http.ResponseWriter, req *http.Request) - // HandleIntrospectionRequest handles the OAuth2 query to determine the active state of an OAuth 2.0 token and - // to determine meta-information about this token. - HandleIntrospectionRequest(rw http.ResponseWriter, req *http.Request) -} - -//go:generate mockery --name Store --structname MockStore --outpkg oastest --filename store_mock.go --output ./oastest/ - -type Store interface { - DeleteExternalService(ctx context.Context, id string) error - GetExternalService(ctx context.Context, id string) (*OAuthExternalService, error) - GetExternalServiceNames(ctx context.Context) ([]string, error) - GetExternalServiceByName(ctx context.Context, name string) (*OAuthExternalService, error) - GetExternalServicePublicKey(ctx context.Context, clientID string) (*jose.JSONWebKey, error) - RegisterExternalService(ctx context.Context, client *OAuthExternalService) error - SaveExternalService(ctx context.Context, client *OAuthExternalService) error - UpdateExternalServiceGrantTypes(ctx context.Context, clientID, grantTypes string) error -} diff --git a/pkg/services/extsvcauth/oauthserver/oasimpl/aggregate_store.go b/pkg/services/extsvcauth/oauthserver/oasimpl/aggregate_store.go deleted file mode 100644 index bd2bb90371..0000000000 --- a/pkg/services/extsvcauth/oauthserver/oasimpl/aggregate_store.go +++ /dev/null @@ -1,162 +0,0 @@ -package oasimpl - -import ( - "context" - "time" - - "github.com/ory/fosite" - "github.com/ory/fosite/handler/oauth2" - "github.com/ory/fosite/handler/rfc7523" - "gopkg.in/square/go-jose.v2" - - "github.com/grafana/grafana/pkg/services/extsvcauth/oauthserver/utils" -) - -var _ fosite.ClientManager = &OAuth2ServiceImpl{} -var _ oauth2.AuthorizeCodeStorage = &OAuth2ServiceImpl{} -var _ oauth2.AccessTokenStorage = &OAuth2ServiceImpl{} -var _ oauth2.RefreshTokenStorage = &OAuth2ServiceImpl{} -var _ rfc7523.RFC7523KeyStorage = &OAuth2ServiceImpl{} -var _ oauth2.TokenRevocationStorage = &OAuth2ServiceImpl{} - -// GetClient loads the client by its ID or returns an error -// if the client does not exist or another error occurred. -func (s *OAuth2ServiceImpl) GetClient(ctx context.Context, id string) (fosite.Client, error) { - return s.GetExternalService(ctx, id) -} - -// ClientAssertionJWTValid returns an error if the JTI is -// known or the DB check failed and nil if the JTI is not known. -func (s *OAuth2ServiceImpl) ClientAssertionJWTValid(ctx context.Context, jti string) error { - return s.memstore.ClientAssertionJWTValid(ctx, jti) -} - -// SetClientAssertionJWT marks a JTI as known for the given -// expiry time. Before inserting the new JTI, it will clean -// up any existing JTIs that have expired as those tokens can -// not be replayed due to the expiry. -func (s *OAuth2ServiceImpl) SetClientAssertionJWT(ctx context.Context, jti string, exp time.Time) error { - return s.memstore.SetClientAssertionJWT(ctx, jti, exp) -} - -// GetAuthorizeCodeSession stores the authorization request for a given authorization code. -func (s *OAuth2ServiceImpl) CreateAuthorizeCodeSession(ctx context.Context, code string, request fosite.Requester) (err error) { - return s.memstore.CreateAuthorizeCodeSession(ctx, code, request) -} - -// GetAuthorizeCodeSession hydrates the session based on the given code and returns the authorization request. -// If the authorization code has been invalidated with `InvalidateAuthorizeCodeSession`, this -// method should return the ErrInvalidatedAuthorizeCode error. -// -// Make sure to also return the fosite.Requester value when returning the fosite.ErrInvalidatedAuthorizeCode error! -func (s *OAuth2ServiceImpl) GetAuthorizeCodeSession(ctx context.Context, code string, session fosite.Session) (request fosite.Requester, err error) { - return s.memstore.GetAuthorizeCodeSession(ctx, code, session) -} - -// InvalidateAuthorizeCodeSession is called when an authorize code is being used. The state of the authorization -// code should be set to invalid and consecutive requests to GetAuthorizeCodeSession should return the -// ErrInvalidatedAuthorizeCode error. -func (s *OAuth2ServiceImpl) InvalidateAuthorizeCodeSession(ctx context.Context, code string) (err error) { - return s.memstore.InvalidateAuthorizeCodeSession(ctx, code) -} - -func (s *OAuth2ServiceImpl) CreateAccessTokenSession(ctx context.Context, signature string, request fosite.Requester) (err error) { - return s.memstore.CreateAccessTokenSession(ctx, signature, request) -} - -func (s *OAuth2ServiceImpl) GetAccessTokenSession(ctx context.Context, signature string, session fosite.Session) (request fosite.Requester, err error) { - return s.memstore.GetAccessTokenSession(ctx, signature, session) -} - -func (s *OAuth2ServiceImpl) DeleteAccessTokenSession(ctx context.Context, signature string) (err error) { - return s.memstore.DeleteAccessTokenSession(ctx, signature) -} - -func (s *OAuth2ServiceImpl) CreateRefreshTokenSession(ctx context.Context, signature string, request fosite.Requester) (err error) { - return s.memstore.CreateRefreshTokenSession(ctx, signature, request) -} - -func (s *OAuth2ServiceImpl) GetRefreshTokenSession(ctx context.Context, signature string, session fosite.Session) (request fosite.Requester, err error) { - return s.memstore.GetRefreshTokenSession(ctx, signature, session) -} - -func (s *OAuth2ServiceImpl) DeleteRefreshTokenSession(ctx context.Context, signature string) (err error) { - return s.memstore.DeleteRefreshTokenSession(ctx, signature) -} - -// RevokeRefreshToken revokes a refresh token as specified in: -// https://tools.ietf.org/html/rfc7009#section-2.1 -// If the particular -// token is a refresh token and the authorization server supports the -// revocation of access tokens, then the authorization server SHOULD -// also invalidate all access tokens based on the same authorization -// grant (see Implementation Note). -func (s *OAuth2ServiceImpl) RevokeRefreshToken(ctx context.Context, requestID string) error { - return s.memstore.RevokeRefreshToken(ctx, requestID) -} - -// RevokeRefreshTokenMaybeGracePeriod revokes a refresh token as specified in: -// https://tools.ietf.org/html/rfc7009#section-2.1 -// If the particular -// token is a refresh token and the authorization server supports the -// revocation of access tokens, then the authorization server SHOULD -// also invalidate all access tokens based on the same authorization -// grant (see Implementation Note). -// -// If the Refresh Token grace period is greater than zero in configuration the token -// will have its expiration time set as UTCNow + GracePeriod. -func (s *OAuth2ServiceImpl) RevokeRefreshTokenMaybeGracePeriod(ctx context.Context, requestID string, signature string) error { - return s.memstore.RevokeRefreshTokenMaybeGracePeriod(ctx, requestID, signature) -} - -// RevokeAccessToken revokes an access token as specified in: -// https://tools.ietf.org/html/rfc7009#section-2.1 -// If the token passed to the request -// is an access token, the server MAY revoke the respective refresh -// token as well. -func (s *OAuth2ServiceImpl) RevokeAccessToken(ctx context.Context, requestID string) error { - return s.memstore.RevokeAccessToken(ctx, requestID) -} - -// GetPublicKey returns public key, issued by 'issuer', and assigned for subject. Public key is used to check -// signature of jwt assertion in authorization grants. -func (s *OAuth2ServiceImpl) GetPublicKey(ctx context.Context, issuer string, subject string, kid string) (*jose.JSONWebKey, error) { - return s.sqlstore.GetExternalServicePublicKey(ctx, issuer) -} - -// GetPublicKeys returns public key, set issued by 'issuer', and assigned for subject. -func (s *OAuth2ServiceImpl) GetPublicKeys(ctx context.Context, issuer string, subject string) (*jose.JSONWebKeySet, error) { - jwk, err := s.sqlstore.GetExternalServicePublicKey(ctx, issuer) - if err != nil { - return nil, err - } - return &jose.JSONWebKeySet{ - Keys: []jose.JSONWebKey{*jwk}, - }, nil -} - -// GetPublicKeyScopes returns assigned scope for assertion, identified by public key, issued by 'issuer'. -func (s *OAuth2ServiceImpl) GetPublicKeyScopes(ctx context.Context, issuer string, subject string, kid string) ([]string, error) { - client, err := s.GetExternalService(ctx, issuer) - if err != nil { - return nil, err - } - userID, err := utils.ParseUserIDFromSubject(subject) - if err != nil { - return nil, err - } - return client.GetScopesOnUser(ctx, s.accessControl, userID), nil -} - -// IsJWTUsed returns true, if JWT is not known yet or it can not be considered valid, because it must be already -// expired. -func (s *OAuth2ServiceImpl) IsJWTUsed(ctx context.Context, jti string) (bool, error) { - return s.memstore.IsJWTUsed(ctx, jti) -} - -// MarkJWTUsedForTime marks JWT as used for a time passed in exp parameter. This helps ensure that JWTs are not -// replayed by maintaining the set of used "jti" values for the length of time for which the JWT would be -// considered valid based on the applicable "exp" instant. (https://tools.ietf.org/html/rfc7523#section-3) -func (s *OAuth2ServiceImpl) MarkJWTUsedForTime(ctx context.Context, jti string, exp time.Time) error { - return s.memstore.MarkJWTUsedForTime(ctx, jti, exp) -} diff --git a/pkg/services/extsvcauth/oauthserver/oasimpl/aggregate_store_test.go b/pkg/services/extsvcauth/oauthserver/oasimpl/aggregate_store_test.go deleted file mode 100644 index 1501239cce..0000000000 --- a/pkg/services/extsvcauth/oauthserver/oasimpl/aggregate_store_test.go +++ /dev/null @@ -1,119 +0,0 @@ -package oasimpl - -import ( - "context" - "testing" - "time" - - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - - ac "github.com/grafana/grafana/pkg/services/accesscontrol" - "github.com/grafana/grafana/pkg/services/extsvcauth/oauthserver" - "github.com/grafana/grafana/pkg/services/user" -) - -var cachedExternalService = func() *oauthserver.OAuthExternalService { - return &oauthserver.OAuthExternalService{ - Name: "my-ext-service", - ClientID: "RANDOMID", - Secret: "RANDOMSECRET", - GrantTypes: "client_credentials", - PublicPem: []byte("-----BEGIN PUBLIC KEY-----"), - ServiceAccountID: 1, - SelfPermissions: []ac.Permission{{Action: "users:impersonate", Scope: "users:*"}}, - SignedInUser: &user.SignedInUser{ - UserID: 2, - OrgID: 1, - Permissions: map[int64]map[string][]string{ - 1: { - "users:impersonate": {"users:*"}, - }, - }, - }, - } -} - -func TestOAuth2ServiceImpl_GetPublicKeyScopes(t *testing.T) { - testCases := []struct { - name string - initTestEnv func(*TestEnv) - impersonatePermissions []ac.Permission - userID string - expectedScopes []string - wantErr bool - }{ - { - name: "should error out when GetExternalService returns error", - initTestEnv: func(env *TestEnv) { - env.OAuthStore.On("GetExternalService", mock.Anything, mock.Anything).Return(nil, oauthserver.ErrClientNotFoundFn("my-ext-service")) - }, - wantErr: true, - }, - { - name: "should error out when the user id cannot be parsed", - initTestEnv: func(env *TestEnv) { - env.S.cache.Set("my-ext-service", *cachedExternalService(), time.Minute) - }, - userID: "user:3", - wantErr: true, - }, - { - name: "should return no scope when the external service is not allowed to impersonate the user", - initTestEnv: func(env *TestEnv) { - client := cachedExternalService() - client.SignedInUser.Permissions = map[int64]map[string][]string{} - env.S.cache.Set("my-ext-service", *client, time.Minute) - }, - userID: "user:id:3", - expectedScopes: nil, - wantErr: false, - }, - { - name: "should return no scope when the external service has an no impersonate permission", - initTestEnv: func(env *TestEnv) { - client := cachedExternalService() - client.ImpersonatePermissions = []ac.Permission{} - env.S.cache.Set("my-ext-service", *client, time.Minute) - }, - userID: "user:id:3", - expectedScopes: []string{}, - wantErr: false, - }, - { - name: "should return the scopes when the external service has impersonate permissions", - initTestEnv: func(env *TestEnv) { - env.S.cache.Set("my-ext-service", *cachedExternalService(), time.Minute) - client := cachedExternalService() - client.ImpersonatePermissions = []ac.Permission{ - {Action: ac.ActionUsersImpersonate, Scope: ac.ScopeUsersAll}, - {Action: ac.ActionUsersRead, Scope: oauthserver.ScopeGlobalUsersSelf}, - {Action: ac.ActionUsersPermissionsRead, Scope: oauthserver.ScopeUsersSelf}, - {Action: ac.ActionTeamsRead, Scope: oauthserver.ScopeTeamsSelf}} - env.S.cache.Set("my-ext-service", *client, time.Minute) - }, - userID: "user:id:3", - expectedScopes: []string{"users:impersonate", - "profile", "email", ac.ActionUsersRead, - "entitlements", ac.ActionUsersPermissionsRead, - "groups", ac.ActionTeamsRead}, - wantErr: false, - }, - } - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - env := setupTestEnv(t) - if tc.initTestEnv != nil { - tc.initTestEnv(env) - } - - scopes, err := env.S.GetPublicKeyScopes(context.Background(), "my-ext-service", tc.userID, "") - if tc.wantErr { - require.Error(t, err) - return - } - - require.ElementsMatch(t, tc.expectedScopes, scopes) - }) - } -} diff --git a/pkg/services/extsvcauth/oauthserver/oasimpl/introspection.go b/pkg/services/extsvcauth/oauthserver/oasimpl/introspection.go deleted file mode 100644 index 3182efbf3a..0000000000 --- a/pkg/services/extsvcauth/oauthserver/oasimpl/introspection.go +++ /dev/null @@ -1,21 +0,0 @@ -package oasimpl - -import ( - "log" - "net/http" -) - -// HandleIntrospectionRequest handles the OAuth2 query to determine the active state of an OAuth 2.0 token and -// to determine meta-information about this token -func (s *OAuth2ServiceImpl) HandleIntrospectionRequest(rw http.ResponseWriter, req *http.Request) { - ctx := req.Context() - currentOAuthSessionData := NewAuthSession() - ir, err := s.oauthProvider.NewIntrospectionRequest(ctx, req, currentOAuthSessionData) - if err != nil { - log.Printf("Error occurred in NewIntrospectionRequest: %+v", err) - s.oauthProvider.WriteIntrospectionError(ctx, rw, err) - return - } - - s.oauthProvider.WriteIntrospectionResponse(ctx, rw, ir) -} diff --git a/pkg/services/extsvcauth/oauthserver/oasimpl/service.go b/pkg/services/extsvcauth/oauthserver/oasimpl/service.go deleted file mode 100644 index 8c5a90248d..0000000000 --- a/pkg/services/extsvcauth/oauthserver/oasimpl/service.go +++ /dev/null @@ -1,500 +0,0 @@ -package oasimpl - -import ( - "context" - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rand" - "crypto/rsa" - "crypto/x509" - "encoding/base64" - "encoding/pem" - "errors" - "fmt" - "strings" - "time" - - "github.com/go-jose/go-jose/v3" - "github.com/ory/fosite" - "github.com/ory/fosite/compose" - "github.com/ory/fosite/storage" - "github.com/ory/fosite/token/jwt" - "golang.org/x/crypto/bcrypt" - - "github.com/grafana/grafana/pkg/api/routing" - "github.com/grafana/grafana/pkg/bus" - "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/infra/localcache" - "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/infra/slugify" - ac "github.com/grafana/grafana/pkg/services/accesscontrol" - "github.com/grafana/grafana/pkg/services/extsvcauth" - "github.com/grafana/grafana/pkg/services/extsvcauth/oauthserver" - "github.com/grafana/grafana/pkg/services/extsvcauth/oauthserver/api" - "github.com/grafana/grafana/pkg/services/extsvcauth/oauthserver/store" - "github.com/grafana/grafana/pkg/services/extsvcauth/oauthserver/utils" - "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings" - "github.com/grafana/grafana/pkg/services/serviceaccounts" - "github.com/grafana/grafana/pkg/services/signingkeys" - "github.com/grafana/grafana/pkg/services/team" - "github.com/grafana/grafana/pkg/services/user" - "github.com/grafana/grafana/pkg/setting" -) - -const ( - cacheExpirationTime = 5 * time.Minute - cacheCleanupInterval = 5 * time.Minute -) - -type OAuth2ServiceImpl struct { - cache *localcache.CacheService - memstore *storage.MemoryStore - cfg *setting.Cfg - sqlstore oauthserver.Store - oauthProvider fosite.OAuth2Provider - logger log.Logger - accessControl ac.AccessControl - acService ac.Service - saService serviceaccounts.ExtSvcAccountsService - userService user.Service - teamService team.Service - publicKey any -} - -func ProvideService(router routing.RouteRegister, bus bus.Bus, db db.DB, cfg *setting.Cfg, - extSvcAccSvc serviceaccounts.ExtSvcAccountsService, accessControl ac.AccessControl, acSvc ac.Service, userSvc user.Service, - teamSvc team.Service, keySvc signingkeys.Service, fmgmt *featuremgmt.FeatureManager) (*OAuth2ServiceImpl, error) { - if !fmgmt.IsEnabledGlobally(featuremgmt.FlagExternalServiceAuth) { - return nil, nil - } - config := &fosite.Config{ - AccessTokenLifespan: cfg.OAuth2ServerAccessTokenLifespan, - TokenURL: fmt.Sprintf("%voauth2/token", cfg.AppURL), - AccessTokenIssuer: cfg.AppURL, - IDTokenIssuer: cfg.AppURL, - ScopeStrategy: fosite.WildcardScopeStrategy, - } - - s := &OAuth2ServiceImpl{ - cache: localcache.New(cacheExpirationTime, cacheCleanupInterval), - cfg: cfg, - accessControl: accessControl, - acService: acSvc, - memstore: storage.NewMemoryStore(), - sqlstore: store.NewStore(db), - logger: log.New("oauthserver"), - userService: userSvc, - saService: extSvcAccSvc, - teamService: teamSvc, - } - - api := api.NewAPI(router, s) - api.RegisterAPIEndpoints() - - bus.AddEventListener(s.handlePluginStateChanged) - - s.oauthProvider = newProvider(config, s, keySvc) - - return s, nil -} - -func newProvider(config *fosite.Config, storage any, signingKeyService signingkeys.Service) fosite.OAuth2Provider { - keyGetter := func(ctx context.Context) (any, error) { - _, key, err := signingKeyService.GetOrCreatePrivateKey(ctx, signingkeys.ServerPrivateKeyID, jose.ES256) - return key, err - } - return compose.Compose( - config, - storage, - &compose.CommonStrategy{ - CoreStrategy: compose.NewOAuth2JWTStrategy(keyGetter, compose.NewOAuth2HMACStrategy(config), config), - Signer: &jwt.DefaultSigner{GetPrivateKey: keyGetter}, - }, - compose.OAuth2ClientCredentialsGrantFactory, - compose.RFC7523AssertionGrantFactory, - - compose.OAuth2TokenIntrospectionFactory, - compose.OAuth2TokenRevocationFactory, - ) -} - -// HasExternalService returns whether an external service has been saved with that name. -func (s *OAuth2ServiceImpl) HasExternalService(ctx context.Context, name string) (bool, error) { - client, errRetrieve := s.sqlstore.GetExternalServiceByName(ctx, name) - if errRetrieve != nil && !errors.Is(errRetrieve, oauthserver.ErrClientNotFound) { - return false, errRetrieve - } - - return client != nil, nil -} - -// GetExternalService retrieves an external service from store by client_id. It populates the SelfPermissions and -// SignedInUser from the associated service account. -// For performance reason, the service uses caching. -func (s *OAuth2ServiceImpl) GetExternalService(ctx context.Context, id string) (*oauthserver.OAuthExternalService, error) { - entry, ok := s.cache.Get(id) - if ok { - client, ok := entry.(oauthserver.OAuthExternalService) - if ok { - s.logger.Debug("GetExternalService: cache hit", "id", id) - return &client, nil - } - } - - client, err := s.sqlstore.GetExternalService(ctx, id) - if err != nil { - return nil, err - } - - if err := s.setClientUser(ctx, client); err != nil { - return nil, err - } - - s.cache.Set(id, *client, cacheExpirationTime) - return client, nil -} - -// setClientUser sets the SignedInUser and SelfPermissions fields of the client -func (s *OAuth2ServiceImpl) setClientUser(ctx context.Context, client *oauthserver.OAuthExternalService) error { - if client.ServiceAccountID == oauthserver.NoServiceAccountID { - s.logger.Debug("GetExternalService: service has no service account, hence no permission", "client_id", client.ClientID, "name", client.Name) - - // Create a signed in user with no role and no permission - client.SignedInUser = &user.SignedInUser{ - UserID: oauthserver.NoServiceAccountID, - OrgID: oauthserver.TmpOrgID, - Name: client.Name, - Permissions: map[int64]map[string][]string{oauthserver.TmpOrgID: {}}, - } - return nil - } - - s.logger.Debug("GetExternalService: fetch permissions", "client_id", client.ClientID) - sa, err := s.saService.RetrieveExtSvcAccount(ctx, oauthserver.TmpOrgID, client.ServiceAccountID) - if err != nil { - s.logger.Error("GetExternalService: error fetching service account", "id", client.ClientID, "error", err) - return err - } - client.SignedInUser = &user.SignedInUser{ - UserID: sa.ID, - OrgID: oauthserver.TmpOrgID, - OrgRole: sa.Role, - Login: sa.Login, - Name: sa.Name, - Permissions: map[int64]map[string][]string{}, - } - client.SelfPermissions, err = s.acService.GetUserPermissions(ctx, client.SignedInUser, ac.Options{}) - if err != nil { - s.logger.Error("GetExternalService: error fetching permissions", "client_id", client.ClientID, "error", err) - return err - } - client.SignedInUser.Permissions[oauthserver.TmpOrgID] = ac.GroupScopesByAction(client.SelfPermissions) - return nil -} - -// GetExternalServiceNames get the names of External Service in store -func (s *OAuth2ServiceImpl) GetExternalServiceNames(ctx context.Context) ([]string, error) { - s.logger.Debug("Get external service names from store") - res, err := s.sqlstore.GetExternalServiceNames(ctx) - if err != nil { - s.logger.Error("Could not fetch clients from store", "error", err.Error()) - return nil, err - } - return res, nil -} - -func (s *OAuth2ServiceImpl) RemoveExternalService(ctx context.Context, name string) error { - s.logger.Info("Remove external service", "service", name) - - client, err := s.sqlstore.GetExternalServiceByName(ctx, name) - if err != nil { - if errors.Is(err, oauthserver.ErrClientNotFound) { - s.logger.Debug("No external service linked to this name", "name", name) - return nil - } - s.logger.Error("Error fetching external service", "name", name, "error", err.Error()) - return err - } - - // Since we will delete the service, clear cache entry - s.cache.Delete(client.ClientID) - - // Delete the OAuth client info in store - if err := s.sqlstore.DeleteExternalService(ctx, client.ClientID); err != nil { - s.logger.Error("Error deleting external service", "name", name, "error", err.Error()) - return err - } - s.logger.Debug("Deleted external service", "name", name, "client_id", client.ClientID) - - // Remove the associated service account - return s.saService.RemoveExtSvcAccount(ctx, oauthserver.TmpOrgID, slugify.Slugify(name)) -} - -// SaveExternalService creates or updates an external service in the database, it generates client_id and secrets and -// it ensures that the associated service account has the correct permissions. -// Database consistency is not guaranteed, consider changing this in the future. -func (s *OAuth2ServiceImpl) SaveExternalService(ctx context.Context, registration *extsvcauth.ExternalServiceRegistration) (*extsvcauth.ExternalService, error) { - if registration == nil { - s.logger.Warn("RegisterExternalService called without registration") - return nil, nil - } - slug := registration.Name - s.logger.Info("Registering external service", "external service", slug) - - // Check if the client already exists in store - client, errFetchExtSvc := s.sqlstore.GetExternalServiceByName(ctx, slug) - if errFetchExtSvc != nil && !errors.Is(errFetchExtSvc, oauthserver.ErrClientNotFound) { - s.logger.Error("Error fetching service", "external service", slug, "error", errFetchExtSvc) - return nil, errFetchExtSvc - } - // Otherwise, create a new client - if client == nil { - s.logger.Debug("External service does not yet exist", "external service", slug) - client = &oauthserver.OAuthExternalService{ - Name: slug, - ServiceAccountID: oauthserver.NoServiceAccountID, - Audiences: s.cfg.AppURL, - } - } - - // Parse registration form to compute required permissions for the client - client.SelfPermissions, client.ImpersonatePermissions = s.handleRegistrationPermissions(registration) - - if registration.OAuthProviderCfg == nil { - return nil, errors.New("missing oauth provider configuration") - } - - if registration.OAuthProviderCfg.RedirectURI != nil { - client.RedirectURI = *registration.OAuthProviderCfg.RedirectURI - } - - var errGenCred error - client.ClientID, client.Secret, errGenCred = s.genCredentials() - if errGenCred != nil { - s.logger.Error("Error generating credentials", "client", client.LogID(), "error", errGenCred) - return nil, errGenCred - } - - grantTypes := s.computeGrantTypes(registration.Self.Enabled, registration.Impersonation.Enabled) - client.GrantTypes = strings.Join(grantTypes, ",") - - // Handle key options - s.logger.Debug("Handle key options") - keys, err := s.handleKeyOptions(ctx, registration.OAuthProviderCfg.Key) - if err != nil { - s.logger.Error("Error handling key options", "client", client.LogID(), "error", err) - return nil, err - } - if keys != nil { - client.PublicPem = []byte(keys.PublicPem) - } - dto := client.ToExternalService(keys) - - hashedSecret, err := bcrypt.GenerateFromPassword([]byte(client.Secret), bcrypt.DefaultCost) - if err != nil { - s.logger.Error("Error hashing secret", "client", client.LogID(), "error", err) - return nil, err - } - client.Secret = string(hashedSecret) - - s.logger.Debug("Save service account") - saID, errSaveServiceAccount := s.saService.ManageExtSvcAccount(ctx, &serviceaccounts.ManageExtSvcAccountCmd{ - ExtSvcSlug: slugify.Slugify(client.Name), - Enabled: registration.Self.Enabled, - OrgID: oauthserver.TmpOrgID, - Permissions: client.SelfPermissions, - }) - if errSaveServiceAccount != nil { - return nil, errSaveServiceAccount - } - client.ServiceAccountID = saID - - err = s.sqlstore.SaveExternalService(ctx, client) - if err != nil { - s.logger.Error("Error saving external service", "client", client.LogID(), "error", err) - return nil, err - } - s.logger.Debug("Registered", "client", client.LogID()) - return dto, nil -} - -// randString generates a a cryptographically secure random string of n bytes -func (s *OAuth2ServiceImpl) randString(n int) (string, error) { - res := make([]byte, n) - if _, err := rand.Read(res); err != nil { - return "", err - } - return base64.RawURLEncoding.EncodeToString(res), nil -} - -func (s *OAuth2ServiceImpl) genCredentials() (string, string, error) { - id, err := s.randString(20) - if err != nil { - return "", "", err - } - // client_secret must be at least 32 bytes long - secret, err := s.randString(32) - if err != nil { - return "", "", err - } - return id, secret, err -} - -func (s *OAuth2ServiceImpl) computeGrantTypes(selfAccessEnabled, impersonationEnabled bool) []string { - grantTypes := []string{} - - if selfAccessEnabled { - grantTypes = append(grantTypes, string(fosite.GrantTypeClientCredentials)) - } - - if impersonationEnabled { - grantTypes = append(grantTypes, string(fosite.GrantTypeJWTBearer)) - } - - return grantTypes -} - -func (s *OAuth2ServiceImpl) handleKeyOptions(ctx context.Context, keyOption *extsvcauth.KeyOption) (*extsvcauth.KeyResult, error) { - if keyOption == nil { - return nil, fmt.Errorf("keyOption is nil") - } - - var publicPem, privatePem string - - if keyOption.Generate { - switch s.cfg.OAuth2ServerGeneratedKeyTypeForClient { - case "RSA": - privateKey, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - return nil, err - } - publicPem = string(pem.EncodeToMemory(&pem.Block{ - Type: "RSA PUBLIC KEY", - Bytes: x509.MarshalPKCS1PublicKey(&privateKey.PublicKey), - })) - privatePem = string(pem.EncodeToMemory(&pem.Block{ - Type: "RSA PRIVATE KEY", - Bytes: x509.MarshalPKCS1PrivateKey(privateKey), - })) - s.logger.Debug("RSA key has been generated") - default: // default to ECDSA - privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - if err != nil { - return nil, err - } - publicDer, err := x509.MarshalPKIXPublicKey(&privateKey.PublicKey) - if err != nil { - return nil, err - } - - privateDer, err := x509.MarshalPKCS8PrivateKey(privateKey) - if err != nil { - return nil, err - } - - publicPem = string(pem.EncodeToMemory(&pem.Block{ - Type: "PUBLIC KEY", - Bytes: publicDer, - })) - privatePem = string(pem.EncodeToMemory(&pem.Block{ - Type: "PRIVATE KEY", - Bytes: privateDer, - })) - s.logger.Debug("ECDSA key has been generated") - } - - return &extsvcauth.KeyResult{ - PrivatePem: privatePem, - PublicPem: publicPem, - Generated: true, - }, nil - } - - // TODO MVP allow specifying a URL to get the public key - // if registration.Key.URL != "" { - // return &oauthserver.KeyResult{ - // URL: registration.Key.URL, - // }, nil - // } - - if keyOption.PublicPEM != "" { - pemEncoded, err := base64.StdEncoding.DecodeString(keyOption.PublicPEM) - if err != nil { - s.logger.Error("Cannot decode base64 encoded PEM string", "error", err) - } - _, err = utils.ParsePublicKeyPem(pemEncoded) - if err != nil { - s.logger.Error("Cannot parse PEM encoded string", "error", err) - return nil, err - } - return &extsvcauth.KeyResult{ - PublicPem: string(pemEncoded), - }, nil - } - - return nil, fmt.Errorf("at least one key option must be specified") -} - -// handleRegistrationPermissions parses the registration form to retrieve requested permissions and adds default -// permissions when impersonation is requested -func (*OAuth2ServiceImpl) handleRegistrationPermissions(registration *extsvcauth.ExternalServiceRegistration) ([]ac.Permission, []ac.Permission) { - selfPermissions := registration.Self.Permissions - impersonatePermissions := []ac.Permission{} - - if len(registration.Impersonation.Permissions) > 0 { - requiredForToken := []ac.Permission{ - {Action: ac.ActionUsersRead, Scope: oauthserver.ScopeGlobalUsersSelf}, - {Action: ac.ActionUsersPermissionsRead, Scope: oauthserver.ScopeUsersSelf}, - } - if registration.Impersonation.Groups { - requiredForToken = append(requiredForToken, ac.Permission{Action: ac.ActionTeamsRead, Scope: oauthserver.ScopeTeamsSelf}) - } - impersonatePermissions = append(requiredForToken, registration.Impersonation.Permissions...) - selfPermissions = append(selfPermissions, ac.Permission{Action: ac.ActionUsersImpersonate, Scope: ac.ScopeUsersAll}) - } - return selfPermissions, impersonatePermissions -} - -// handlePluginStateChanged reset the client authorized grant_types according to the plugin state -func (s *OAuth2ServiceImpl) handlePluginStateChanged(ctx context.Context, event *pluginsettings.PluginStateChangedEvent) error { - s.logger.Debug("Plugin state changed", "pluginId", event.PluginId, "enabled", event.Enabled) - - if event.OrgId != extsvcauth.TmpOrgID { - s.logger.Debug("External Service not tied to this organization", "OrgId", event.OrgId) - return nil - } - - // Retrieve client associated to the plugin - client, err := s.sqlstore.GetExternalServiceByName(ctx, event.PluginId) - if err != nil { - if errors.Is(err, oauthserver.ErrClientNotFound) { - s.logger.Debug("No external service linked to this plugin", "pluginId", event.PluginId) - return nil - } - s.logger.Error("Error fetching service", "pluginId", event.PluginId, "error", err.Error()) - return err - } - - // Since we will change the grants, clear cache entry - s.cache.Delete(client.ClientID) - - if !event.Enabled { - // Plugin is disabled => remove all grant_types - return s.sqlstore.UpdateExternalServiceGrantTypes(ctx, client.ClientID, "") - } - - if err := s.setClientUser(ctx, client); err != nil { - return err - } - - // The plugin has self permissions (not only impersonate) - canOnlyImpersonate := len(client.SelfPermissions) == 1 && (client.SelfPermissions[0].Action == ac.ActionUsersImpersonate) - selfEnabled := len(client.SelfPermissions) > 0 && !canOnlyImpersonate - // The plugin declared impersonate permissions - impersonateEnabled := len(client.ImpersonatePermissions) > 0 - - grantTypes := s.computeGrantTypes(selfEnabled, impersonateEnabled) - - return s.sqlstore.UpdateExternalServiceGrantTypes(ctx, client.ClientID, strings.Join(grantTypes, ",")) -} diff --git a/pkg/services/extsvcauth/oauthserver/oasimpl/service_test.go b/pkg/services/extsvcauth/oauthserver/oasimpl/service_test.go deleted file mode 100644 index 42a77a24e1..0000000000 --- a/pkg/services/extsvcauth/oauthserver/oasimpl/service_test.go +++ /dev/null @@ -1,625 +0,0 @@ -package oasimpl - -import ( - "context" - "crypto/rand" - "crypto/rsa" - "encoding/base64" - "fmt" - "slices" - "testing" - "time" - - "github.com/ory/fosite" - "github.com/ory/fosite/storage" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - - "github.com/grafana/grafana/pkg/infra/localcache" - "github.com/grafana/grafana/pkg/infra/log" - ac "github.com/grafana/grafana/pkg/services/accesscontrol" - "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" - "github.com/grafana/grafana/pkg/services/accesscontrol/actest" - "github.com/grafana/grafana/pkg/services/extsvcauth" - "github.com/grafana/grafana/pkg/services/extsvcauth/oauthserver" - "github.com/grafana/grafana/pkg/services/extsvcauth/oauthserver/oastest" - "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/services/org" - "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings" - sa "github.com/grafana/grafana/pkg/services/serviceaccounts" - saTests "github.com/grafana/grafana/pkg/services/serviceaccounts/tests" - "github.com/grafana/grafana/pkg/services/signingkeys/signingkeystest" - "github.com/grafana/grafana/pkg/services/team/teamtest" - "github.com/grafana/grafana/pkg/services/user" - "github.com/grafana/grafana/pkg/services/user/usertest" - "github.com/grafana/grafana/pkg/setting" -) - -const ( - AppURL = "https://oauth.test/" - TokenURL = AppURL + "oauth2/token" -) - -var ( - pk, _ = rsa.GenerateKey(rand.Reader, 4096) - Client1Key, _ = rsa.GenerateKey(rand.Reader, 4096) -) - -type TestEnv struct { - S *OAuth2ServiceImpl - Cfg *setting.Cfg - AcStore *actest.MockStore - OAuthStore *oastest.MockStore - UserService *usertest.FakeUserService - TeamService *teamtest.FakeService - SAService *saTests.MockExtSvcAccountsService -} - -func setupTestEnv(t *testing.T) *TestEnv { - t.Helper() - - cfg := setting.NewCfg() - cfg.AppURL = AppURL - - config := &fosite.Config{ - AccessTokenLifespan: time.Hour, - TokenURL: TokenURL, - AccessTokenIssuer: AppURL, - IDTokenIssuer: AppURL, - ScopeStrategy: fosite.WildcardScopeStrategy, - } - - fmgt := featuremgmt.WithFeatures(featuremgmt.FlagExternalServiceAuth) - - env := &TestEnv{ - Cfg: cfg, - AcStore: &actest.MockStore{}, - OAuthStore: &oastest.MockStore{}, - UserService: usertest.NewUserServiceFake(), - TeamService: teamtest.NewFakeService(), - SAService: saTests.NewMockExtSvcAccountsService(t), - } - env.S = &OAuth2ServiceImpl{ - cache: localcache.New(cacheExpirationTime, cacheCleanupInterval), - cfg: cfg, - accessControl: acimpl.ProvideAccessControl(cfg), - acService: acimpl.ProvideOSSService(cfg, env.AcStore, localcache.New(0, 0), fmgt), - memstore: storage.NewMemoryStore(), - sqlstore: env.OAuthStore, - logger: log.New("oauthserver.test"), - userService: env.UserService, - saService: env.SAService, - teamService: env.TeamService, - publicKey: &pk.PublicKey, - } - - env.S.oauthProvider = newProvider(config, env.S, &signingkeystest.FakeSigningKeysService{ - ExpectedSinger: pk, - ExpectedKeyID: "default", - ExpectedError: nil, - }) - - return env -} - -func TestOAuth2ServiceImpl_SaveExternalService(t *testing.T) { - const serviceName = "my-ext-service" - - tests := []struct { - name string - init func(*TestEnv) - cmd *extsvcauth.ExternalServiceRegistration - mockChecks func(*testing.T, *TestEnv) - wantErr bool - }{ - { - name: "should create a new client without permissions", - init: func(env *TestEnv) { - // No client at the beginning - env.OAuthStore.On("GetExternalServiceByName", mock.Anything, mock.Anything).Return(nil, oauthserver.ErrClientNotFoundFn(serviceName)) - env.OAuthStore.On("SaveExternalService", mock.Anything, mock.Anything).Return(nil) - - // Return a service account ID - env.SAService.On("ManageExtSvcAccount", mock.Anything, mock.Anything).Return(int64(0), nil) - }, - cmd: &extsvcauth.ExternalServiceRegistration{ - Name: serviceName, - OAuthProviderCfg: &extsvcauth.OAuthProviderCfg{Key: &extsvcauth.KeyOption{Generate: true}}, - }, - mockChecks: func(t *testing.T, env *TestEnv) { - env.OAuthStore.AssertCalled(t, "GetExternalServiceByName", mock.Anything, mock.MatchedBy(func(name string) bool { - return name == serviceName - })) - env.OAuthStore.AssertCalled(t, "SaveExternalService", mock.Anything, mock.MatchedBy(func(client *oauthserver.OAuthExternalService) bool { - return client.Name == serviceName && client.ClientID != "" && client.Secret != "" && - len(client.GrantTypes) == 0 && len(client.PublicPem) > 0 && client.ServiceAccountID == 0 && - len(client.ImpersonatePermissions) == 0 - })) - }, - }, - { - name: "should allow client credentials grant with correct permissions", - init: func(env *TestEnv) { - // No client at the beginning - env.OAuthStore.On("GetExternalServiceByName", mock.Anything, mock.Anything).Return(nil, oauthserver.ErrClientNotFoundFn(serviceName)) - env.OAuthStore.On("SaveExternalService", mock.Anything, mock.Anything).Return(nil) - - // Return a service account ID - env.SAService.On("ManageExtSvcAccount", mock.Anything, mock.Anything).Return(int64(10), nil) - }, - cmd: &extsvcauth.ExternalServiceRegistration{ - Name: serviceName, - Self: extsvcauth.SelfCfg{ - Enabled: true, - Permissions: []ac.Permission{{Action: ac.ActionUsersRead, Scope: ac.ScopeUsersAll}}, - }, - OAuthProviderCfg: &extsvcauth.OAuthProviderCfg{Key: &extsvcauth.KeyOption{Generate: true}}, - }, - mockChecks: func(t *testing.T, env *TestEnv) { - env.OAuthStore.AssertCalled(t, "GetExternalServiceByName", mock.Anything, mock.MatchedBy(func(name string) bool { - return name == serviceName - })) - env.OAuthStore.AssertCalled(t, "SaveExternalService", mock.Anything, mock.MatchedBy(func(client *oauthserver.OAuthExternalService) bool { - return client.Name == serviceName && len(client.ClientID) > 0 && len(client.Secret) > 0 && - client.GrantTypes == string(fosite.GrantTypeClientCredentials) && - len(client.PublicPem) > 0 && client.ServiceAccountID == 10 && - len(client.ImpersonatePermissions) == 0 && - len(client.SelfPermissions) > 0 - })) - // Check that despite no credential_grants the service account still has a permission to impersonate users - env.SAService.AssertCalled(t, "ManageExtSvcAccount", mock.Anything, - mock.MatchedBy(func(cmd *sa.ManageExtSvcAccountCmd) bool { - return len(cmd.Permissions) == 1 && cmd.Permissions[0] == ac.Permission{Action: ac.ActionUsersRead, Scope: ac.ScopeUsersAll} - })) - }, - }, - { - name: "should allow jwt bearer grant and set default permissions", - init: func(env *TestEnv) { - // No client at the beginning - env.OAuthStore.On("GetExternalServiceByName", mock.Anything, mock.Anything).Return(nil, oauthserver.ErrClientNotFoundFn(serviceName)) - env.OAuthStore.On("SaveExternalService", mock.Anything, mock.Anything).Return(nil) - // The service account needs to be created with a permission to impersonate users - env.SAService.On("ManageExtSvcAccount", mock.Anything, mock.Anything).Return(int64(10), nil) - }, - cmd: &extsvcauth.ExternalServiceRegistration{ - Name: serviceName, - OAuthProviderCfg: &extsvcauth.OAuthProviderCfg{Key: &extsvcauth.KeyOption{Generate: true}}, - Impersonation: extsvcauth.ImpersonationCfg{ - Enabled: true, - Groups: true, - Permissions: []ac.Permission{{Action: "dashboards:read", Scope: "dashboards:*"}}, - }, - }, - mockChecks: func(t *testing.T, env *TestEnv) { - // Check that the external service impersonate permissions contains the default permissions required to populate the access token - env.OAuthStore.AssertCalled(t, "SaveExternalService", mock.Anything, mock.MatchedBy(func(client *oauthserver.OAuthExternalService) bool { - impPerm := client.ImpersonatePermissions - return slices.Contains(impPerm, ac.Permission{Action: "dashboards:read", Scope: "dashboards:*"}) && - slices.Contains(impPerm, ac.Permission{Action: ac.ActionUsersRead, Scope: oauthserver.ScopeGlobalUsersSelf}) && - slices.Contains(impPerm, ac.Permission{Action: ac.ActionUsersPermissionsRead, Scope: oauthserver.ScopeUsersSelf}) && - slices.Contains(impPerm, ac.Permission{Action: ac.ActionTeamsRead, Scope: oauthserver.ScopeTeamsSelf}) - })) - // Check that despite no credential_grants the service account still has a permission to impersonate users - env.SAService.AssertCalled(t, "ManageExtSvcAccount", mock.Anything, - mock.MatchedBy(func(cmd *sa.ManageExtSvcAccountCmd) bool { - return len(cmd.Permissions) == 1 && cmd.Permissions[0] == ac.Permission{Action: ac.ActionUsersImpersonate, Scope: ac.ScopeUsersAll} - })) - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - env := setupTestEnv(t) - if tt.init != nil { - tt.init(env) - } - - dto, err := env.S.SaveExternalService(context.Background(), tt.cmd) - if tt.wantErr { - require.Error(t, err) - return - } - require.NoError(t, err) - - // Check that we generated client ID and secret - require.NotEmpty(t, dto.ID) - require.NotEmpty(t, dto.Secret) - - // Check that we have generated keys and that we correctly return them - if tt.cmd.OAuthProviderCfg.Key != nil && tt.cmd.OAuthProviderCfg.Key.Generate { - require.NotNil(t, dto.OAuthExtra.KeyResult) - require.True(t, dto.OAuthExtra.KeyResult.Generated) - require.NotEmpty(t, dto.OAuthExtra.KeyResult.PublicPem) - require.NotEmpty(t, dto.OAuthExtra.KeyResult.PrivatePem) - } - - // Check that we computed grant types and created or updated the service account - if tt.cmd.Self.Enabled { - require.NotNil(t, dto.OAuthExtra.GrantTypes) - require.Contains(t, dto.OAuthExtra.GrantTypes, fosite.GrantTypeClientCredentials, "grant types should contain client_credentials") - } else { - require.NotContains(t, dto.OAuthExtra.GrantTypes, fosite.GrantTypeClientCredentials, "grant types should not contain client_credentials") - } - // Check that we updated grant types - if tt.cmd.Impersonation.Enabled { - require.NotNil(t, dto.OAuthExtra.GrantTypes) - require.Contains(t, dto.OAuthExtra.GrantTypes, fosite.GrantTypeJWTBearer, "grant types should contain JWT Bearer grant") - } else { - require.NotContains(t, dto.OAuthExtra.GrantTypes, fosite.GrantTypeJWTBearer, "grant types should not contain JWT Bearer grant") - } - - // Check that mocks were called as expected - env.OAuthStore.AssertExpectations(t) - env.SAService.AssertExpectations(t) - env.AcStore.AssertExpectations(t) - - // Additional checks performed - if tt.mockChecks != nil { - tt.mockChecks(t, env) - } - }) - } -} - -func TestOAuth2ServiceImpl_GetExternalService(t *testing.T) { - const serviceName = "my-ext-service" - - dummyClient := func() *oauthserver.OAuthExternalService { - return &oauthserver.OAuthExternalService{ - Name: serviceName, - ClientID: "RANDOMID", - Secret: "RANDOMSECRET", - GrantTypes: "client_credentials", - PublicPem: []byte("-----BEGIN PUBLIC KEY-----"), - ServiceAccountID: 1, - } - } - cachedClient := &oauthserver.OAuthExternalService{ - Name: serviceName, - ClientID: "RANDOMID", - Secret: "RANDOMSECRET", - GrantTypes: "client_credentials", - PublicPem: []byte("-----BEGIN PUBLIC KEY-----"), - ServiceAccountID: 1, - SelfPermissions: []ac.Permission{{Action: "users:impersonate", Scope: "users:*"}}, - SignedInUser: &user.SignedInUser{ - UserID: 1, - Permissions: map[int64]map[string][]string{ - 1: { - "users:impersonate": {"users:*"}, - }, - }, - }, - } - testCases := []struct { - name string - init func(*TestEnv) - mockChecks func(*testing.T, *TestEnv) - wantPerm []ac.Permission - wantErr bool - }{ - { - name: "should hit the cache", - init: func(env *TestEnv) { - env.S.cache.Set(serviceName, *cachedClient, time.Minute) - }, - mockChecks: func(t *testing.T, env *TestEnv) { - env.OAuthStore.AssertNotCalled(t, "GetExternalService", mock.Anything, mock.Anything) - }, - wantPerm: []ac.Permission{{Action: "users:impersonate", Scope: "users:*"}}, - }, - { - name: "should return error when the client was not found", - init: func(env *TestEnv) { - env.OAuthStore.On("GetExternalService", mock.Anything, mock.Anything).Return(nil, oauthserver.ErrClientNotFoundFn(serviceName)) - }, - wantErr: true, - }, - { - name: "should return error when the service account was not found", - init: func(env *TestEnv) { - env.OAuthStore.On("GetExternalService", mock.Anything, mock.Anything).Return(dummyClient(), nil) - env.SAService.On("RetrieveExtSvcAccount", mock.Anything, int64(1), int64(1)).Return(&sa.ExtSvcAccount{}, sa.ErrServiceAccountNotFound) - }, - mockChecks: func(t *testing.T, env *TestEnv) { - env.OAuthStore.AssertCalled(t, "GetExternalService", mock.Anything, mock.Anything) - env.SAService.AssertCalled(t, "RetrieveExtSvcAccount", mock.Anything, 1, 1) - }, - wantErr: true, - }, - { - name: "should return error when the service account has no permissions", - init: func(env *TestEnv) { - env.OAuthStore.On("GetExternalService", mock.Anything, mock.Anything).Return(dummyClient(), nil) - env.SAService.On("RetrieveExtSvcAccount", mock.Anything, int64(1), int64(1)).Return(&sa.ExtSvcAccount{}, nil) - env.AcStore.On("GetUserPermissions", mock.Anything, mock.Anything).Return(nil, fmt.Errorf("some error")) - }, - mockChecks: func(t *testing.T, env *TestEnv) { - env.OAuthStore.AssertCalled(t, "GetExternalService", mock.Anything, mock.Anything) - env.SAService.AssertCalled(t, "RetrieveExtSvcAccount", mock.Anything, 1, 1) - }, - wantErr: true, - }, - { - name: "should return correctly", - init: func(env *TestEnv) { - env.OAuthStore.On("GetExternalService", mock.Anything, mock.Anything).Return(dummyClient(), nil) - env.SAService.On("RetrieveExtSvcAccount", mock.Anything, int64(1), int64(1)).Return(&sa.ExtSvcAccount{ID: 1}, nil) - env.AcStore.On("GetUserPermissions", mock.Anything, mock.Anything).Return([]ac.Permission{{Action: ac.ActionUsersImpersonate, Scope: ac.ScopeUsersAll}}, nil) - }, - mockChecks: func(t *testing.T, env *TestEnv) { - env.OAuthStore.AssertCalled(t, "GetExternalService", mock.Anything, mock.Anything) - env.SAService.AssertCalled(t, "RetrieveExtSvcAccount", mock.Anything, int64(1), int64(1)) - }, - wantPerm: []ac.Permission{{Action: "users:impersonate", Scope: "users:*"}}, - }, - { - name: "should return correctly when the client has no service account", - init: func(env *TestEnv) { - client := &oauthserver.OAuthExternalService{ - Name: serviceName, - ClientID: "RANDOMID", - Secret: "RANDOMSECRET", - GrantTypes: "client_credentials", - PublicPem: []byte("-----BEGIN PUBLIC KEY-----"), - ServiceAccountID: oauthserver.NoServiceAccountID, - } - env.OAuthStore.On("GetExternalService", mock.Anything, mock.Anything).Return(client, nil) - }, - mockChecks: func(t *testing.T, env *TestEnv) { - env.OAuthStore.AssertCalled(t, "GetExternalService", mock.Anything, mock.Anything) - }, - wantPerm: []ac.Permission{}, - }, - } - for _, tt := range testCases { - t.Run(tt.name, func(t *testing.T) { - env := setupTestEnv(t) - if tt.init != nil { - tt.init(env) - } - - client, err := env.S.GetExternalService(context.Background(), serviceName) - if tt.wantErr { - require.Error(t, err) - return - } - require.NoError(t, err) - - if tt.mockChecks != nil { - tt.mockChecks(t, env) - } - - require.Equal(t, serviceName, client.Name) - require.ElementsMatch(t, client.SelfPermissions, tt.wantPerm) - assertArrayInMap(t, client.SignedInUser.Permissions[1], ac.GroupScopesByAction(tt.wantPerm)) - - env.OAuthStore.AssertExpectations(t) - env.SAService.AssertExpectations(t) - }) - } -} - -func assertArrayInMap[K comparable, V string](t *testing.T, m1 map[K][]V, m2 map[K][]V) { - for k, v := range m1 { - require.Contains(t, m2, k) - require.ElementsMatch(t, v, m2[k]) - } -} - -func TestOAuth2ServiceImpl_RemoveExternalService(t *testing.T) { - const serviceName = "my-ext-service" - const clientID = "RANDOMID" - - dummyClient := &oauthserver.OAuthExternalService{ - Name: serviceName, - ClientID: clientID, - ServiceAccountID: 1, - } - - testCases := []struct { - name string - init func(*TestEnv) - }{ - { - name: "should do nothing on not found", - init: func(env *TestEnv) { - env.OAuthStore.On("GetExternalServiceByName", mock.Anything, serviceName).Return(nil, oauthserver.ErrClientNotFoundFn(serviceName)) - }, - }, - { - name: "should remove the external service and its associated service account", - init: func(env *TestEnv) { - env.OAuthStore.On("GetExternalServiceByName", mock.Anything, serviceName).Return(dummyClient, nil) - env.OAuthStore.On("DeleteExternalService", mock.Anything, clientID).Return(nil) - env.SAService.On("RemoveExtSvcAccount", mock.Anything, oauthserver.TmpOrgID, serviceName).Return(nil) - }, - }, - } - for _, tt := range testCases { - t.Run(tt.name, func(t *testing.T) { - env := setupTestEnv(t) - if tt.init != nil { - tt.init(env) - } - - err := env.S.RemoveExternalService(context.Background(), serviceName) - require.NoError(t, err) - - env.OAuthStore.AssertExpectations(t) - env.SAService.AssertExpectations(t) - }) - } -} - -func TestTestOAuth2ServiceImpl_handleKeyOptions(t *testing.T) { - testCases := []struct { - name string - keyOption *extsvcauth.KeyOption - expectedResult *extsvcauth.KeyResult - wantErr bool - }{ - { - name: "should return error when the key option is nil", - wantErr: true, - }, - { - name: "should return error when the key option is empty", - keyOption: &extsvcauth.KeyOption{}, - wantErr: true, - }, - { - name: "should return successfully when PublicPEM is specified", - keyOption: &extsvcauth.KeyOption{ - PublicPEM: base64.StdEncoding.EncodeToString([]byte(`-----BEGIN PUBLIC KEY----- -MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEbsGtoGJTopAIbhqy49/vyCJuDot+ -mgGaC8vUIigFQVsVB+v/HZ4yG1Rcvysig+tyNk1dZQpozpFc2dGmzHlGhw== ------END PUBLIC KEY-----`)), - }, - wantErr: false, - expectedResult: &extsvcauth.KeyResult{ - PublicPem: `-----BEGIN PUBLIC KEY----- -MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEbsGtoGJTopAIbhqy49/vyCJuDot+ -mgGaC8vUIigFQVsVB+v/HZ4yG1Rcvysig+tyNk1dZQpozpFc2dGmzHlGhw== ------END PUBLIC KEY-----`, - Generated: false, - PrivatePem: "", - URL: "", - }, - }, - } - env := setupTestEnv(t) - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - result, err := env.S.handleKeyOptions(context.Background(), tc.keyOption) - if tc.wantErr { - require.Error(t, err) - return - } - require.NoError(t, err) - require.Equal(t, tc.expectedResult, result) - }) - } - - t.Run("should generate an ECDSA key pair (default) when generate key option is specified", func(t *testing.T) { - result, err := env.S.handleKeyOptions(context.Background(), &extsvcauth.KeyOption{Generate: true}) - - require.NoError(t, err) - require.NotNil(t, result.PrivatePem) - require.NotNil(t, result.PublicPem) - require.True(t, result.Generated) - }) - - t.Run("should generate an RSA key pair when generate key option is specified", func(t *testing.T) { - env.S.cfg.OAuth2ServerGeneratedKeyTypeForClient = "RSA" - result, err := env.S.handleKeyOptions(context.Background(), &extsvcauth.KeyOption{Generate: true}) - - require.NoError(t, err) - require.NotNil(t, result.PrivatePem) - require.NotNil(t, result.PublicPem) - require.True(t, result.Generated) - }) -} - -func TestOAuth2ServiceImpl_handlePluginStateChanged(t *testing.T) { - pluginID := "my-app" - clientID := "RANDOMID" - impersonatePermission := []ac.Permission{{Action: ac.ActionUsersImpersonate, Scope: ac.ScopeUsersAll}} - selfPermission := append(impersonatePermission, ac.Permission{Action: ac.ActionUsersRead, Scope: ac.ScopeUsersAll}) - saID := int64(101) - client := &oauthserver.OAuthExternalService{ - ID: 11, - Name: pluginID, - ClientID: clientID, - Secret: "SECRET", - ServiceAccountID: saID, - } - clientWithImpersonate := &oauthserver.OAuthExternalService{ - ID: 11, - Name: pluginID, - ClientID: clientID, - Secret: "SECRET", - ImpersonatePermissions: []ac.Permission{ - {Action: ac.ActionUsersRead, Scope: ac.ScopeUsersAll}, - }, - ServiceAccountID: saID, - } - extSvcAcc := &sa.ExtSvcAccount{ - ID: saID, - Login: "sa-my-app", - Name: pluginID, - OrgID: extsvcauth.TmpOrgID, - IsDisabled: false, - Role: org.RoleNone, - } - - tests := []struct { - name string - init func(*TestEnv) - cmd *pluginsettings.PluginStateChangedEvent - }{ - { - name: "should do nothing with not found", - init: func(te *TestEnv) { - te.OAuthStore.On("GetExternalServiceByName", mock.Anything, "unknown").Return(nil, oauthserver.ErrClientNotFoundFn("unknown")) - }, - cmd: &pluginsettings.PluginStateChangedEvent{PluginId: "unknown", OrgId: 1, Enabled: false}, - }, - { - name: "should remove grants", - init: func(te *TestEnv) { - te.OAuthStore.On("GetExternalServiceByName", mock.Anything, pluginID).Return(clientWithImpersonate, nil) - te.OAuthStore.On("UpdateExternalServiceGrantTypes", mock.Anything, clientID, "").Return(nil) - }, - cmd: &pluginsettings.PluginStateChangedEvent{PluginId: pluginID, OrgId: 1, Enabled: false}, - }, - { - name: "should set both grants", - init: func(te *TestEnv) { - te.OAuthStore.On("GetExternalServiceByName", mock.Anything, mock.Anything).Return(clientWithImpersonate, nil) - te.SAService.On("RetrieveExtSvcAccount", mock.Anything, extsvcauth.TmpOrgID, saID).Return(extSvcAcc, nil) - te.AcStore.On("GetUserPermissions", mock.Anything, mock.Anything, mock.Anything).Return(selfPermission, nil) - te.OAuthStore.On("UpdateExternalServiceGrantTypes", mock.Anything, clientID, - string(fosite.GrantTypeClientCredentials)+","+string(fosite.GrantTypeJWTBearer)).Return(nil) - }, - cmd: &pluginsettings.PluginStateChangedEvent{PluginId: pluginID, OrgId: 1, Enabled: true}, - }, - { - name: "should set impersonate grant", - init: func(te *TestEnv) { - te.OAuthStore.On("GetExternalServiceByName", mock.Anything, mock.Anything).Return(clientWithImpersonate, nil) - te.SAService.On("RetrieveExtSvcAccount", mock.Anything, extsvcauth.TmpOrgID, saID).Return(extSvcAcc, nil) - te.AcStore.On("GetUserPermissions", mock.Anything, mock.Anything, mock.Anything).Return(impersonatePermission, nil) - te.OAuthStore.On("UpdateExternalServiceGrantTypes", mock.Anything, clientID, string(fosite.GrantTypeJWTBearer)).Return(nil) - }, - cmd: &pluginsettings.PluginStateChangedEvent{PluginId: pluginID, OrgId: 1, Enabled: true}, - }, - { - name: "should set client_credentials grant", - init: func(te *TestEnv) { - te.OAuthStore.On("GetExternalServiceByName", mock.Anything, mock.Anything).Return(client, nil) - te.SAService.On("RetrieveExtSvcAccount", mock.Anything, extsvcauth.TmpOrgID, saID).Return(extSvcAcc, nil) - te.AcStore.On("GetUserPermissions", mock.Anything, mock.Anything, mock.Anything).Return(selfPermission, nil) - te.OAuthStore.On("UpdateExternalServiceGrantTypes", mock.Anything, clientID, string(fosite.GrantTypeClientCredentials)).Return(nil) - }, - cmd: &pluginsettings.PluginStateChangedEvent{PluginId: pluginID, OrgId: 1, Enabled: true}, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - env := setupTestEnv(t) - if tt.init != nil { - tt.init(env) - } - - err := env.S.handlePluginStateChanged(context.Background(), tt.cmd) - require.NoError(t, err) - - // Check that mocks were called as expected - env.OAuthStore.AssertExpectations(t) - env.SAService.AssertExpectations(t) - env.AcStore.AssertExpectations(t) - }) - } -} diff --git a/pkg/services/extsvcauth/oauthserver/oasimpl/session.go b/pkg/services/extsvcauth/oauthserver/oasimpl/session.go deleted file mode 100644 index 6d184c8c6d..0000000000 --- a/pkg/services/extsvcauth/oauthserver/oasimpl/session.go +++ /dev/null @@ -1,16 +0,0 @@ -package oasimpl - -import ( - "github.com/ory/fosite/handler/oauth2" - "github.com/ory/fosite/token/jwt" -) - -func NewAuthSession() *oauth2.JWTSession { - sess := &oauth2.JWTSession{ - JWTClaims: new(jwt.JWTClaims), - JWTHeader: new(jwt.Headers), - } - // Our tokens will follow the RFC9068 - sess.JWTHeader.Add("typ", "at+jwt") - return sess -} diff --git a/pkg/services/extsvcauth/oauthserver/oasimpl/token.go b/pkg/services/extsvcauth/oauthserver/oasimpl/token.go deleted file mode 100644 index d36f6b4ce0..0000000000 --- a/pkg/services/extsvcauth/oauthserver/oasimpl/token.go +++ /dev/null @@ -1,353 +0,0 @@ -package oasimpl - -import ( - "context" - "errors" - "fmt" - "net/http" - "strconv" - - "github.com/ory/fosite" - "github.com/ory/fosite/handler/oauth2" - - ac "github.com/grafana/grafana/pkg/services/accesscontrol" - "github.com/grafana/grafana/pkg/services/auth/identity" - "github.com/grafana/grafana/pkg/services/extsvcauth/oauthserver" - "github.com/grafana/grafana/pkg/services/extsvcauth/oauthserver/utils" - "github.com/grafana/grafana/pkg/services/team" - "github.com/grafana/grafana/pkg/services/user" -) - -// HandleTokenRequest handles the client's OAuth2 query to obtain an access_token by presenting its authorization -// grant (ex: client_credentials, jwtbearer) -func (s *OAuth2ServiceImpl) HandleTokenRequest(rw http.ResponseWriter, req *http.Request) { - // This context will be passed to all methods. - ctx := req.Context() - - // Create an empty session object which will be passed to the request handlers - oauthSession := NewAuthSession() - - // This will create an access request object and iterate through the registered TokenEndpointHandlers to validate the request. - accessRequest, err := s.oauthProvider.NewAccessRequest(ctx, req, oauthSession) - if err != nil { - s.writeAccessError(ctx, rw, accessRequest, err) - return - } - - client, err := s.GetExternalService(ctx, accessRequest.GetClient().GetID()) - if err != nil || client == nil { - s.oauthProvider.WriteAccessError(ctx, rw, accessRequest, &fosite.RFC6749Error{ - DescriptionField: "Could not find the requested subject.", - ErrorField: "not_found", - CodeField: http.StatusBadRequest, - }) - return - } - oauthSession.JWTClaims.Add("client_id", client.ClientID) - - errClientCred := s.handleClientCredentials(ctx, accessRequest, oauthSession, client) - if errClientCred != nil { - s.writeAccessError(ctx, rw, accessRequest, errClientCred) - return - } - - errJWTBearer := s.handleJWTBearer(ctx, accessRequest, oauthSession, client) - if errJWTBearer != nil { - s.writeAccessError(ctx, rw, accessRequest, errJWTBearer) - return - } - - // All tokens we generate in this service should target Grafana's API. - accessRequest.GrantAudience(s.cfg.AppURL) - - // Prepare response, fosite handlers will populate the token. - response, err := s.oauthProvider.NewAccessResponse(ctx, accessRequest) - if err != nil { - s.writeAccessError(ctx, rw, accessRequest, err) - return - } - s.oauthProvider.WriteAccessResponse(ctx, rw, accessRequest, response) -} - -// writeAccessError logs the error then uses fosite to write the error back to the user. -func (s *OAuth2ServiceImpl) writeAccessError(ctx context.Context, rw http.ResponseWriter, accessRequest fosite.AccessRequester, err error) { - var fositeErr *fosite.RFC6749Error - if errors.As(err, &fositeErr) { - s.logger.Error("Description", fositeErr.DescriptionField, "hint", fositeErr.HintField, "error", fositeErr.ErrorField) - } else { - s.logger.Error("Error", err) - } - s.oauthProvider.WriteAccessError(ctx, rw, accessRequest, err) -} - -// splitOAuthScopes sort scopes that are generic (profile, email, groups, entitlements) from scopes -// that are RBAC actions (used to further restrict the entitlements embedded in the access_token) -func splitOAuthScopes(requestedScopes fosite.Arguments) (map[string]bool, map[string]bool) { - actionsFilter := map[string]bool{} - claimsFilter := map[string]bool{} - for _, scope := range requestedScopes { - switch scope { - case "profile", "email", "groups", "entitlements": - claimsFilter[scope] = true - default: - actionsFilter[scope] = true - } - } - return actionsFilter, claimsFilter -} - -// handleJWTBearer populates the "impersonation" access_token generated by fosite to match the rfc9068 specifications (entitlements, groups). -// It ensures that the user can be impersonated, that the generated token audiences only contain Grafana's AppURL (and token endpoint) -// and that entitlements solely contain the user's permissions that the client is allowed to have. -func (s *OAuth2ServiceImpl) handleJWTBearer(ctx context.Context, accessRequest fosite.AccessRequester, oauthSession *oauth2.JWTSession, client *oauthserver.OAuthExternalService) error { - if !accessRequest.GetGrantTypes().ExactOne(string(fosite.GrantTypeJWTBearer)) { - return nil - } - - userID, err := utils.ParseUserIDFromSubject(oauthSession.Subject) - if err != nil { - return &fosite.RFC6749Error{ - DescriptionField: "Could not find the requested subject.", - ErrorField: "not_found", - CodeField: http.StatusBadRequest, - } - } - - // Check audiences list only contains the AppURL and the token endpoint - for _, aud := range accessRequest.GetGrantedAudience() { - if aud != fmt.Sprintf("%voauth2/token", s.cfg.AppURL) && aud != s.cfg.AppURL { - return &fosite.RFC6749Error{ - DescriptionField: "Client is not allowed to target this Audience.", - HintField: "The audience must be the AppURL or the token endpoint.", - ErrorField: "invalid_request", - CodeField: http.StatusForbidden, - } - } - } - - // If the client was not allowed to impersonate the user we would not have reached this point given allowed scopes would have been empty - // But just in case we check again - ev := ac.EvalPermission(ac.ActionUsersImpersonate, ac.Scope("users", "id", strconv.FormatInt(userID, 10))) - hasAccess, errAccess := s.accessControl.Evaluate(ctx, client.SignedInUser, ev) - if errAccess != nil || !hasAccess { - return &fosite.RFC6749Error{ - DescriptionField: "Client is not allowed to impersonate subject.", - ErrorField: "restricted_access", - CodeField: http.StatusForbidden, - } - } - - // Populate claims' suject from the session subject - oauthSession.JWTClaims.Subject = oauthSession.Subject - - // Get the user - query := user.GetUserByIDQuery{ID: userID} - dbUser, err := s.userService.GetByID(ctx, &query) - if err != nil { - if errors.Is(err, user.ErrUserNotFound) { - return &fosite.RFC6749Error{ - DescriptionField: "Could not find the requested subject.", - ErrorField: "not_found", - CodeField: http.StatusBadRequest, - } - } - return &fosite.RFC6749Error{ - DescriptionField: "The request subject could not be processed.", - ErrorField: "server_error", - CodeField: http.StatusInternalServerError, - } - } - oauthSession.Username = dbUser.Login - - // Split scopes into actions and claims - actionsFilter, claimsFilter := splitOAuthScopes(accessRequest.GetGrantedScopes()) - - teams := []*team.TeamDTO{} - // Fetch teams if the groups scope is requested or if we need to populate it in the entitlements - if claimsFilter["groups"] || - (claimsFilter["entitlements"] && (len(actionsFilter) == 0 || actionsFilter["teams:read"])) { - var errGetTeams error - teams, errGetTeams = s.teamService.GetTeamsByUser(ctx, &team.GetTeamsByUserQuery{ - OrgID: oauthserver.TmpOrgID, - UserID: dbUser.ID, - // Fetch teams without restriction on permissions - SignedInUser: &user.SignedInUser{ - OrgID: oauthserver.TmpOrgID, - Permissions: map[int64]map[string][]string{ - oauthserver.TmpOrgID: { - ac.ActionTeamsRead: {ac.ScopeTeamsAll}, - }, - }, - }, - }) - if errGetTeams != nil { - return &fosite.RFC6749Error{ - DescriptionField: "The teams scope could not be processed.", - ErrorField: "server_error", - CodeField: http.StatusInternalServerError, - } - } - } - if claimsFilter["profile"] { - oauthSession.JWTClaims.Add("name", dbUser.Name) - oauthSession.JWTClaims.Add("login", dbUser.Login) - oauthSession.JWTClaims.Add("updated_at", dbUser.Updated.Unix()) - } - if claimsFilter["email"] { - oauthSession.JWTClaims.Add("email", dbUser.Email) - } - if claimsFilter["groups"] { - teamNames := make([]string, 0, len(teams)) - for _, team := range teams { - teamNames = append(teamNames, team.Name) - } - oauthSession.JWTClaims.Add("groups", teamNames) - } - - if claimsFilter["entitlements"] { - // Get the user permissions (apply the actions filter) - permissions, errGetPermission := s.filteredUserPermissions(ctx, userID, actionsFilter) - if errGetPermission != nil { - return errGetPermission - } - - // Compute the impersonated permissions (apply the actions filter, replace the scope self with the user id) - impPerms := s.filteredImpersonatePermissions(client.ImpersonatePermissions, userID, teams, actionsFilter) - - // Intersect the permissions with the client permissions - intesect := ac.Intersect(permissions, impPerms) - - oauthSession.JWTClaims.Add("entitlements", intesect) - } - - return nil -} - -// filteredUserPermissions gets the user permissions and applies the actions filter -func (s *OAuth2ServiceImpl) filteredUserPermissions(ctx context.Context, userID int64, actionsFilter map[string]bool) ([]ac.Permission, error) { - permissions, err := s.acService.SearchUserPermissions(ctx, oauthserver.TmpOrgID, - ac.SearchOptions{NamespacedID: fmt.Sprintf("%s:%d", identity.NamespaceUser, userID)}) - if err != nil { - return nil, &fosite.RFC6749Error{ - DescriptionField: "The permissions scope could not be processed.", - ErrorField: "server_error", - CodeField: http.StatusInternalServerError, - } - } - - // Apply the actions filter - if len(actionsFilter) > 0 { - filtered := []ac.Permission{} - for i := range permissions { - if actionsFilter[permissions[i].Action] { - filtered = append(filtered, permissions[i]) - } - } - permissions = filtered - } - return permissions, nil -} - -// filteredImpersonatePermissions computes the impersonated permissions. -// It applies the actions filter and replaces the "self RBAC scopes" (~ scope templates) by the correct user id/team id. -func (*OAuth2ServiceImpl) filteredImpersonatePermissions(impersonatePermissions []ac.Permission, userID int64, teams []*team.TeamDTO, actionsFilter map[string]bool) []ac.Permission { - // Compute the impersonated permissions - impPerms := impersonatePermissions - // Apply the actions filter - if len(actionsFilter) > 0 { - filtered := []ac.Permission{} - for i := range impPerms { - if actionsFilter[impPerms[i].Action] { - filtered = append(filtered, impPerms[i]) - } - } - impPerms = filtered - } - - // Replace the scope self with the user id - correctScopes := []ac.Permission{} - for i := range impPerms { - switch impPerms[i].Scope { - case oauthserver.ScopeGlobalUsersSelf: - correctScopes = append(correctScopes, ac.Permission{ - Action: impPerms[i].Action, - Scope: ac.Scope("global.users", "id", strconv.FormatInt(userID, 10)), - }) - case oauthserver.ScopeUsersSelf: - correctScopes = append(correctScopes, ac.Permission{ - Action: impPerms[i].Action, - Scope: ac.Scope("users", "id", strconv.FormatInt(userID, 10)), - }) - case oauthserver.ScopeTeamsSelf: - for t := range teams { - correctScopes = append(correctScopes, ac.Permission{ - Action: impPerms[i].Action, - Scope: ac.Scope("teams", "id", strconv.FormatInt(teams[t].ID, 10)), - }) - } - default: - correctScopes = append(correctScopes, impPerms[i]) - } - continue - } - return correctScopes -} - -// handleClientCredentials populates the client's access_token generated by fosite to match the rfc9068 specifications (entitlements, groups) -func (s *OAuth2ServiceImpl) handleClientCredentials(ctx context.Context, accessRequest fosite.AccessRequester, oauthSession *oauth2.JWTSession, client *oauthserver.OAuthExternalService) error { - if !accessRequest.GetGrantTypes().ExactOne("client_credentials") { - return nil - } - // Set the subject to the service account associated to the client - oauthSession.JWTClaims.Subject = fmt.Sprintf("user:id:%d", client.ServiceAccountID) - - sa := client.SignedInUser - if sa == nil { - return &fosite.RFC6749Error{ - DescriptionField: "Could not find the service account of the client", - ErrorField: "not_found", - CodeField: http.StatusNotFound, - } - } - oauthSession.Username = sa.Login - - // For client credentials, scopes are not marked as granted by fosite but the request would have been rejected - // already if the client was not allowed to request them - for _, scope := range accessRequest.GetRequestedScopes() { - accessRequest.GrantScope(scope) - } - - // Split scopes into actions and claims - actionsFilter, claimsFilter := splitOAuthScopes(accessRequest.GetGrantedScopes()) - - if claimsFilter["profile"] { - oauthSession.JWTClaims.Add("name", sa.Name) - oauthSession.JWTClaims.Add("login", sa.Login) - } - if claimsFilter["email"] { - s.logger.Debug("Service accounts have no emails") - } - if claimsFilter["groups"] { - s.logger.Debug("Service accounts have no groups") - } - if claimsFilter["entitlements"] { - s.logger.Debug("Processing client entitlements") - if sa.Permissions != nil && sa.Permissions[oauthserver.TmpOrgID] != nil { - perms := sa.Permissions[oauthserver.TmpOrgID] - if len(actionsFilter) > 0 { - filtered := map[string][]string{} - for action := range actionsFilter { - if _, ok := perms[action]; ok { - filtered[action] = perms[action] - } - } - perms = filtered - } - oauthSession.JWTClaims.Add("entitlements", perms) - } else { - s.logger.Debug("Client has no permissions") - } - } - - return nil -} diff --git a/pkg/services/extsvcauth/oauthserver/oasimpl/token_test.go b/pkg/services/extsvcauth/oauthserver/oasimpl/token_test.go deleted file mode 100644 index ac49dfddb5..0000000000 --- a/pkg/services/extsvcauth/oauthserver/oasimpl/token_test.go +++ /dev/null @@ -1,745 +0,0 @@ -package oasimpl - -import ( - "context" - "crypto/rsa" - "encoding/json" - "net/http" - "net/http/httptest" - "net/url" - "strings" - "testing" - "time" - - "github.com/google/uuid" - "github.com/ory/fosite" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - "golang.org/x/crypto/bcrypt" - "golang.org/x/exp/maps" - "gopkg.in/square/go-jose.v2" - "gopkg.in/square/go-jose.v2/jwt" - - "github.com/grafana/grafana/pkg/models/roletype" - ac "github.com/grafana/grafana/pkg/services/accesscontrol" - "github.com/grafana/grafana/pkg/services/extsvcauth/oauthserver" - sa "github.com/grafana/grafana/pkg/services/serviceaccounts" - "github.com/grafana/grafana/pkg/services/team" - "github.com/grafana/grafana/pkg/services/user" -) - -func TestOAuth2ServiceImpl_handleClientCredentials(t *testing.T) { - client1 := &oauthserver.OAuthExternalService{ - Name: "testapp", - ClientID: "RANDOMID", - GrantTypes: string(fosite.GrantTypeClientCredentials), - ServiceAccountID: 2, - SignedInUser: &user.SignedInUser{ - UserID: 2, - Name: "Test App", - Login: "testapp", - OrgRole: roletype.RoleViewer, - Permissions: map[int64]map[string][]string{ - oauthserver.TmpOrgID: { - "dashboards:read": {"dashboards:*", "folders:*"}, - "dashboards:write": {"dashboards:uid:1"}, - }, - }, - }, - } - - tests := []struct { - name string - scopes []string - client *oauthserver.OAuthExternalService - expectedClaims map[string]any - wantErr bool - }{ - { - name: "no claim without client_credentials grant type", - client: &oauthserver.OAuthExternalService{ - Name: "testapp", - ClientID: "RANDOMID", - GrantTypes: string(fosite.GrantTypeJWTBearer), - ServiceAccountID: 2, - SignedInUser: &user.SignedInUser{}, - }, - }, - { - name: "no claims without scopes", - client: client1, - }, - { - name: "profile claims", - client: client1, - scopes: []string{"profile"}, - expectedClaims: map[string]any{"name": "Test App", "login": "testapp"}, - }, - { - name: "email claims should be empty", - client: client1, - scopes: []string{"email"}, - }, - { - name: "groups claims should be empty", - client: client1, - scopes: []string{"groups"}, - }, - { - name: "entitlements claims", - client: client1, - scopes: []string{"entitlements"}, - expectedClaims: map[string]any{"entitlements": map[string][]string{ - "dashboards:read": {"dashboards:*", "folders:*"}, - "dashboards:write": {"dashboards:uid:1"}, - }}, - }, - { - name: "scoped entitlements claims", - client: client1, - scopes: []string{"entitlements", "dashboards:write"}, - expectedClaims: map[string]any{"entitlements": map[string][]string{ - "dashboards:write": {"dashboards:uid:1"}, - }}, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ctx := context.Background() - env := setupTestEnv(t) - session := &fosite.DefaultSession{} - requester := fosite.NewAccessRequest(session) - requester.GrantTypes = fosite.Arguments(strings.Split(tt.client.GrantTypes, ",")) - requester.RequestedScope = fosite.Arguments(tt.scopes) - sessionData := NewAuthSession() - err := env.S.handleClientCredentials(ctx, requester, sessionData, tt.client) - if tt.wantErr { - require.Error(t, err) - return - } - require.NoError(t, err) - - if tt.expectedClaims == nil { - require.Empty(t, sessionData.JWTClaims.Extra) - return - } - require.Len(t, sessionData.JWTClaims.Extra, len(tt.expectedClaims)) - for claimsKey, claimsValue := range tt.expectedClaims { - switch expected := claimsValue.(type) { - case []string: - require.ElementsMatch(t, claimsValue, sessionData.JWTClaims.Extra[claimsKey]) - case map[string][]string: - actual, ok := sessionData.JWTClaims.Extra[claimsKey].(map[string][]string) - require.True(t, ok, "expected map[string][]string") - - require.ElementsMatch(t, maps.Keys(expected), maps.Keys(actual)) - for expKey, expValue := range expected { - require.ElementsMatch(t, expValue, actual[expKey]) - } - default: - require.Equal(t, claimsValue, sessionData.JWTClaims.Extra[claimsKey]) - } - } - }) - } -} - -func TestOAuth2ServiceImpl_handleJWTBearer(t *testing.T) { - now := time.Now() - client1 := &oauthserver.OAuthExternalService{ - Name: "testapp", - ClientID: "RANDOMID", - GrantTypes: string(fosite.GrantTypeJWTBearer), - ServiceAccountID: 2, - SignedInUser: &user.SignedInUser{ - UserID: 2, - OrgID: oauthserver.TmpOrgID, - Name: "Test App", - Login: "testapp", - OrgRole: roletype.RoleViewer, - Permissions: map[int64]map[string][]string{ - oauthserver.TmpOrgID: { - "users:impersonate": {"users:*"}, - }, - }, - }, - } - user56 := &user.User{ - ID: 56, - Email: "user56@example.org", - Login: "user56", - Name: "User 56", - Updated: now, - } - teams := []*team.TeamDTO{ - {ID: 1, Name: "Team 1", OrgID: 1}, - {ID: 2, Name: "Team 2", OrgID: 1}, - } - client1WithPerm := func(perms []ac.Permission) *oauthserver.OAuthExternalService { - client := *client1 - client.ImpersonatePermissions = perms - return &client - } - - tests := []struct { - name string - initEnv func(*TestEnv) - scopes []string - client *oauthserver.OAuthExternalService - subject string - expectedClaims map[string]any - wantErr bool - }{ - { - name: "no claim without jwtbearer grant type", - client: &oauthserver.OAuthExternalService{ - Name: "testapp", - ClientID: "RANDOMID", - GrantTypes: string(fosite.GrantTypeClientCredentials), - ServiceAccountID: 2, - }, - }, - { - name: "err invalid subject", - client: client1, - subject: "invalid_subject", - wantErr: true, - }, - { - name: "err client is not allowed to impersonate", - client: &oauthserver.OAuthExternalService{ - Name: "testapp", - ClientID: "RANDOMID", - GrantTypes: string(fosite.GrantTypeJWTBearer), - ServiceAccountID: 2, - SignedInUser: &user.SignedInUser{ - UserID: 2, - Name: "Test App", - Login: "testapp", - OrgRole: roletype.RoleViewer, - Permissions: map[int64]map[string][]string{oauthserver.TmpOrgID: {}}, - }, - }, - subject: "user:id:56", - wantErr: true, - }, - { - name: "err subject not found", - initEnv: func(env *TestEnv) { - env.UserService.ExpectedError = user.ErrUserNotFound - }, - client: client1, - subject: "user:id:56", - wantErr: true, - }, - { - name: "no claim without scope", - initEnv: func(env *TestEnv) { - env.UserService.ExpectedUser = user56 - }, - client: client1, - subject: "user:id:56", - }, - { - name: "profile claims", - initEnv: func(env *TestEnv) { - env.UserService.ExpectedUser = user56 - }, - client: client1, - subject: "user:id:56", - scopes: []string{"profile"}, - expectedClaims: map[string]any{ - "name": "User 56", - "login": "user56", - "updated_at": now.Unix(), - }, - }, - { - name: "email claim", - initEnv: func(env *TestEnv) { - env.UserService.ExpectedUser = user56 - }, - client: client1, - subject: "user:id:56", - scopes: []string{"email"}, - expectedClaims: map[string]any{ - "email": "user56@example.org", - }, - }, - { - name: "groups claim", - initEnv: func(env *TestEnv) { - env.UserService.ExpectedUser = user56 - env.TeamService.ExpectedTeamsByUser = teams - }, - client: client1, - subject: "user:id:56", - scopes: []string{"groups"}, - expectedClaims: map[string]any{ - "groups": []string{"Team 1", "Team 2"}, - }, - }, - { - name: "no entitlement without permission intersection", - initEnv: func(env *TestEnv) { - env.UserService.ExpectedUser = user56 - env.AcStore.On("GetUsersBasicRoles", mock.Anything, mock.Anything, mock.Anything).Return(map[int64][]string{ - 56: {"Viewer"}}, nil) - env.AcStore.On("SearchUsersPermissions", mock.Anything, mock.Anything, mock.Anything).Return(map[int64][]ac.Permission{ - 56: {{Action: "dashboards:read", Scope: "dashboards:uid:1"}}, - }, nil) - }, - client: client1WithPerm([]ac.Permission{ - {Action: "datasources:read", Scope: "datasources:*"}, - }), - subject: "user:id:56", - expectedClaims: map[string]any{ - "entitlements": map[string][]string{}, - }, - scopes: []string{"entitlements"}, - }, - { - name: "entitlements contains only the intersection of permissions", - initEnv: func(env *TestEnv) { - env.UserService.ExpectedUser = user56 - env.AcStore.On("GetUsersBasicRoles", mock.Anything, mock.Anything, mock.Anything).Return(map[int64][]string{ - 56: {"Viewer"}}, nil) - env.AcStore.On("SearchUsersPermissions", mock.Anything, mock.Anything, mock.Anything).Return(map[int64][]ac.Permission{ - 56: { - {Action: "dashboards:read", Scope: "dashboards:uid:1"}, - {Action: "datasources:read", Scope: "datasources:uid:1"}, - }, - }, nil) - }, - client: client1WithPerm([]ac.Permission{ - {Action: "datasources:read", Scope: "datasources:*"}, - }), - subject: "user:id:56", - expectedClaims: map[string]any{ - "entitlements": map[string][]string{ - "datasources:read": {"datasources:uid:1"}, - }, - }, - scopes: []string{"entitlements"}, - }, - { - name: "entitlements have correctly translated users:self permissions", - initEnv: func(env *TestEnv) { - env.UserService.ExpectedUser = user56 - env.AcStore.On("GetUsersBasicRoles", mock.Anything, mock.Anything, mock.Anything).Return(map[int64][]string{ - 56: {"Viewer"}}, nil) - env.AcStore.On("SearchUsersPermissions", mock.Anything, mock.Anything, mock.Anything).Return(map[int64][]ac.Permission{ - 56: { - {Action: "users:read", Scope: "global.users:id:*"}, - {Action: "users.permissions:read", Scope: "users:id:*"}, - }}, nil) - }, - client: client1WithPerm([]ac.Permission{ - {Action: "users:read", Scope: "global.users:self"}, - {Action: "users.permissions:read", Scope: "users:self"}, - }), - subject: "user:id:56", - expectedClaims: map[string]any{ - "entitlements": map[string][]string{ - "users:read": {"global.users:id:56"}, - "users.permissions:read": {"users:id:56"}, - }, - }, - scopes: []string{"entitlements"}, - }, - { - name: "entitlements have correctly translated teams:self permissions", - initEnv: func(env *TestEnv) { - env.UserService.ExpectedUser = user56 - env.TeamService.ExpectedTeamsByUser = teams - env.AcStore.On("GetUsersBasicRoles", mock.Anything, mock.Anything, mock.Anything).Return(map[int64][]string{ - 56: {"Viewer"}}, nil) - env.AcStore.On("SearchUsersPermissions", mock.Anything, mock.Anything, mock.Anything).Return(map[int64][]ac.Permission{ - 56: {{Action: "teams:read", Scope: "teams:*"}}}, nil) - }, - client: client1WithPerm([]ac.Permission{ - {Action: "teams:read", Scope: "teams:self"}, - }), - subject: "user:id:56", - expectedClaims: map[string]any{ - "entitlements": map[string][]string{"teams:read": {"teams:id:1", "teams:id:2"}}, - }, - scopes: []string{"entitlements"}, - }, - { - name: "entitlements are correctly filtered based on scopes", - initEnv: func(env *TestEnv) { - env.UserService.ExpectedUser = user56 - env.TeamService.ExpectedTeamsByUser = teams - env.AcStore.On("GetUsersBasicRoles", mock.Anything, mock.Anything, mock.Anything).Return(map[int64][]string{ - 56: {"Viewer"}}, nil) - env.AcStore.On("SearchUsersPermissions", mock.Anything, mock.Anything, mock.Anything).Return(map[int64][]ac.Permission{ - 56: { - {Action: "users:read", Scope: "global.users:id:*"}, - {Action: "datasources:read", Scope: "datasources:uid:1"}, - }}, nil) - }, - client: client1WithPerm([]ac.Permission{ - {Action: "users:read", Scope: "global.users:*"}, - {Action: "datasources:read", Scope: "datasources:*"}, - }), - subject: "user:id:56", - expectedClaims: map[string]any{ - "entitlements": map[string][]string{"users:read": {"global.users:id:*"}}, - }, - scopes: []string{"entitlements", "users:read"}, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ctx := context.Background() - env := setupTestEnv(t) - session := &fosite.DefaultSession{} - requester := fosite.NewAccessRequest(session) - requester.GrantTypes = fosite.Arguments(strings.Split(tt.client.GrantTypes, ",")) - requester.RequestedScope = fosite.Arguments(tt.scopes) - requester.GrantedScope = fosite.Arguments(tt.scopes) - sessionData := NewAuthSession() - sessionData.Subject = tt.subject - - if tt.initEnv != nil { - tt.initEnv(env) - } - err := env.S.handleJWTBearer(ctx, requester, sessionData, tt.client) - if tt.wantErr { - require.Error(t, err) - return - } - require.NoError(t, err) - - if tt.expectedClaims == nil { - require.Empty(t, sessionData.JWTClaims.Extra) - return - } - require.Len(t, sessionData.JWTClaims.Extra, len(tt.expectedClaims)) - - for claimsKey, claimsValue := range tt.expectedClaims { - switch expected := claimsValue.(type) { - case []string: - require.ElementsMatch(t, claimsValue, sessionData.JWTClaims.Extra[claimsKey]) - case map[string][]string: - actual, ok := sessionData.JWTClaims.Extra[claimsKey].(map[string][]string) - require.True(t, ok, "expected map[string][]string") - - require.ElementsMatch(t, maps.Keys(expected), maps.Keys(actual)) - for expKey, expValue := range expected { - require.ElementsMatch(t, expValue, actual[expKey]) - } - default: - require.Equal(t, claimsValue, sessionData.JWTClaims.Extra[claimsKey]) - } - } - - env.AcStore.AssertExpectations(t) - }) - } -} - -type tokenResponse struct { - AccessToken string `json:"access_token"` - ExpiresIn int `json:"expires_in"` - Scope string `json:"scope"` - TokenType string `json:"token_type"` -} - -type claims struct { - jwt.Claims - ClientID string `json:"client_id"` - Groups []string `json:"groups"` - Email string `json:"email"` - Name string `json:"name"` - Login string `json:"login"` - Scopes []string `json:"scope"` - Entitlements map[string][]string `json:"entitlements"` -} - -func TestOAuth2ServiceImpl_HandleTokenRequest(t *testing.T) { - tests := []struct { - name string - tweakTestClient func(*oauthserver.OAuthExternalService) - reqParams url.Values - wantCode int - wantScope []string - wantClaims *claims - }{ - { - name: "should allow client credentials grant", - reqParams: url.Values{ - "grant_type": {string(fosite.GrantTypeClientCredentials)}, - "client_id": {"CLIENT1ID"}, - "client_secret": {"CLIENT1SECRET"}, - "scope": {"profile email groups entitlements"}, - "audience": {AppURL}, - }, - wantCode: http.StatusOK, - wantScope: []string{"profile", "email", "groups", "entitlements"}, - wantClaims: &claims{ - Claims: jwt.Claims{ - Subject: "user:id:2", // From client1.ServiceAccountID - Issuer: AppURL, // From env.S.Config.Issuer - Audience: jwt.Audience{AppURL}, - }, - ClientID: "CLIENT1ID", - Name: "client-1", - Login: "client-1", - Entitlements: map[string][]string{ - "users:impersonate": {"users:*"}, - }, - }, - }, - { - name: "should allow jwt-bearer grant", - reqParams: url.Values{ - "grant_type": {string(fosite.GrantTypeJWTBearer)}, - "client_id": {"CLIENT1ID"}, - "client_secret": {"CLIENT1SECRET"}, - "assertion": { - genAssertion(t, Client1Key, "CLIENT1ID", "user:id:56", TokenURL, AppURL), - }, - "scope": {"profile email groups entitlements"}, - }, - wantCode: http.StatusOK, - wantScope: []string{"profile", "email", "groups", "entitlements"}, - wantClaims: &claims{ - Claims: jwt.Claims{ - Subject: "user:id:56", // To match the assertion - Issuer: AppURL, // From env.S.Config.Issuer - Audience: jwt.Audience{TokenURL, AppURL}, - }, - ClientID: "CLIENT1ID", - Email: "user56@example.org", - Name: "User 56", - Login: "user56", - Groups: []string{"Team 1", "Team 2"}, - Entitlements: map[string][]string{ - "dashboards:read": {"folders:uid:UID1"}, - "folders:read": {"folders:uid:UID1"}, - "users:read": {"global.users:id:56"}, - }, - }, - }, - { - name: "should deny jwt-bearer grant with wrong audience", - reqParams: url.Values{ - "grant_type": {string(fosite.GrantTypeJWTBearer)}, - "client_id": {"CLIENT1ID"}, - "client_secret": {"CLIENT1SECRET"}, - "assertion": { - genAssertion(t, Client1Key, "CLIENT1ID", "user:id:56", TokenURL, "invalid audience"), - }, - "scope": {"profile email groups entitlements"}, - }, - wantCode: http.StatusForbidden, - }, - { - name: "should deny jwt-bearer grant for clients without the grant", - reqParams: url.Values{ - "grant_type": {string(fosite.GrantTypeJWTBearer)}, - "client_id": {"CLIENT1ID"}, - "client_secret": {"CLIENT1SECRET"}, - "assertion": { - genAssertion(t, Client1Key, "CLIENT1ID", "user:id:56", TokenURL, AppURL), - }, - "scope": {"profile email groups entitlements"}, - }, - tweakTestClient: func(es *oauthserver.OAuthExternalService) { - es.GrantTypes = string(fosite.GrantTypeClientCredentials) - }, - wantCode: http.StatusBadRequest, - }, - { - name: "should deny client_credentials grant for clients without the grant", - reqParams: url.Values{ - "grant_type": {string(fosite.GrantTypeClientCredentials)}, - "client_id": {"CLIENT1ID"}, - "client_secret": {"CLIENT1SECRET"}, - "scope": {"profile email groups entitlements"}, - "audience": {AppURL}, - }, - tweakTestClient: func(es *oauthserver.OAuthExternalService) { - es.GrantTypes = string(fosite.GrantTypeJWTBearer) - }, - wantCode: http.StatusBadRequest, - }, - { - name: "should deny client_credentials grant with wrong secret", - reqParams: url.Values{ - "grant_type": {string(fosite.GrantTypeClientCredentials)}, - "client_id": {"CLIENT1ID"}, - "client_secret": {"WRONG_SECRET"}, - "scope": {"profile email groups entitlements"}, - "audience": {AppURL}, - }, - tweakTestClient: func(es *oauthserver.OAuthExternalService) { - es.GrantTypes = string(fosite.GrantTypeClientCredentials) - }, - wantCode: http.StatusUnauthorized, - }, - { - name: "should deny jwt-bearer grant with wrong secret", - reqParams: url.Values{ - "grant_type": {string(fosite.GrantTypeJWTBearer)}, - "client_id": {"CLIENT1ID"}, - "client_secret": {"WRONG_SECRET"}, - "assertion": { - genAssertion(t, Client1Key, "CLIENT1ID", "user:id:56", TokenURL, AppURL), - }, - "scope": {"profile email groups entitlements"}, - }, - tweakTestClient: func(es *oauthserver.OAuthExternalService) { - es.GrantTypes = string(fosite.GrantTypeJWTBearer) - }, - wantCode: http.StatusUnauthorized, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - env := setupTestEnv(t) - setupHandleTokenRequestEnv(t, env, tt.tweakTestClient) - - req := httptest.NewRequest("POST", "/oauth2/token", strings.NewReader(tt.reqParams.Encode())) - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - - resp := httptest.NewRecorder() - - env.S.HandleTokenRequest(resp, req) - - require.Equal(t, tt.wantCode, resp.Code, resp.Body.String()) - if tt.wantCode != http.StatusOK { - return - } - - body := resp.Body.Bytes() - require.NotEmpty(t, body) - - var tokenResp tokenResponse - require.NoError(t, json.Unmarshal(body, &tokenResp)) - - // Check token response - require.NotEmpty(t, tokenResp.Scope) - require.ElementsMatch(t, tt.wantScope, strings.Split(tokenResp.Scope, " ")) - require.Positive(t, tokenResp.ExpiresIn) - require.Equal(t, "bearer", tokenResp.TokenType) - require.NotEmpty(t, tokenResp.AccessToken) - - // Check access token - parsedToken, err := jwt.ParseSigned(tokenResp.AccessToken) - require.NoError(t, err) - require.Len(t, parsedToken.Headers, 1) - typeHeader := parsedToken.Headers[0].ExtraHeaders["typ"] - require.Equal(t, "at+jwt", strings.ToLower(typeHeader.(string))) - require.Equal(t, "RS256", parsedToken.Headers[0].Algorithm) - // Check access token claims - var claims claims - require.NoError(t, parsedToken.Claims(pk.Public(), &claims)) - // Check times and remove them - require.Positive(t, claims.IssuedAt.Time()) - require.Positive(t, claims.Expiry.Time()) - claims.IssuedAt = jwt.NewNumericDate(time.Time{}) - claims.Expiry = jwt.NewNumericDate(time.Time{}) - // Check the ID and remove it - require.NotEmpty(t, claims.ID) - claims.ID = "" - // Compare the rest - require.Equal(t, tt.wantClaims, &claims) - }) - } -} - -func genAssertion(t *testing.T, signKey *rsa.PrivateKey, clientID, sub string, audience ...string) string { - key := jose.SigningKey{Algorithm: jose.RS256, Key: signKey} - assertion := jwt.Claims{ - ID: uuid.New().String(), - Issuer: clientID, - Subject: sub, - Audience: audience, - Expiry: jwt.NewNumericDate(time.Now().Add(time.Hour)), - IssuedAt: jwt.NewNumericDate(time.Now()), - } - - var signerOpts = jose.SignerOptions{} - signerOpts.WithType("JWT") - rsaSigner, errSigner := jose.NewSigner(key, &signerOpts) - require.NoError(t, errSigner) - builder := jwt.Signed(rsaSigner) - rawJWT, errSign := builder.Claims(assertion).CompactSerialize() - require.NoError(t, errSign) - return rawJWT -} - -// setupHandleTokenRequestEnv creates a client and a user and sets all Mocks call for the handleTokenRequest test cases -func setupHandleTokenRequestEnv(t *testing.T, env *TestEnv, opt func(*oauthserver.OAuthExternalService)) { - now := time.Now() - hashedSecret, err := bcrypt.GenerateFromPassword([]byte("CLIENT1SECRET"), bcrypt.DefaultCost) - require.NoError(t, err) - client1 := &oauthserver.OAuthExternalService{ - Name: "client-1", - ClientID: "CLIENT1ID", - Secret: string(hashedSecret), - GrantTypes: string(fosite.GrantTypeClientCredentials + "," + fosite.GrantTypeJWTBearer), - ServiceAccountID: 2, - ImpersonatePermissions: []ac.Permission{ - {Action: "users:read", Scope: oauthserver.ScopeGlobalUsersSelf}, - {Action: "users.permissions:read", Scope: oauthserver.ScopeUsersSelf}, - {Action: "teams:read", Scope: oauthserver.ScopeTeamsSelf}, - - {Action: "folders:read", Scope: "folders:*"}, - {Action: "dashboards:read", Scope: "folders:*"}, - {Action: "dashboards:read", Scope: "dashboards:*"}, - }, - SelfPermissions: []ac.Permission{ - {Action: "users:impersonate", Scope: "users:*"}, - }, - Audiences: AppURL, - } - - // Apply any option the test case might need - if opt != nil { - opt(client1) - } - - sa1 := &sa.ExtSvcAccount{ - ID: client1.ServiceAccountID, - Name: client1.Name, - Login: client1.Name, - OrgID: oauthserver.TmpOrgID, - IsDisabled: false, - Role: roletype.RoleNone, - } - - user56 := &user.User{ - ID: 56, - Email: "user56@example.org", - Login: "user56", - Name: "User 56", - Updated: now, - } - user56Permissions := []ac.Permission{ - {Action: "users:read", Scope: "global.users:id:56"}, - {Action: "folders:read", Scope: "folders:uid:UID1"}, - {Action: "dashboards:read", Scope: "folders:uid:UID1"}, - {Action: "datasources:read", Scope: "datasources:uid:DS_UID2"}, // This one should be ignored when impersonating - } - user56Teams := []*team.TeamDTO{ - {ID: 1, Name: "Team 1", OrgID: 1}, - {ID: 2, Name: "Team 2", OrgID: 1}, - } - - // To retrieve the Client, its publicKey and its permissions - env.OAuthStore.On("GetExternalService", mock.Anything, client1.ClientID).Return(client1, nil) - env.OAuthStore.On("GetExternalServicePublicKey", mock.Anything, client1.ClientID).Return(&jose.JSONWebKey{Key: Client1Key.Public(), Algorithm: "RS256"}, nil) - env.SAService.On("RetrieveExtSvcAccount", mock.Anything, oauthserver.TmpOrgID, client1.ServiceAccountID).Return(sa1, nil) - env.AcStore.On("GetUserPermissions", mock.Anything, mock.Anything).Return(client1.SelfPermissions, nil) - // To retrieve the user to impersonate, its permissions and its teams - env.AcStore.On("SearchUsersPermissions", mock.Anything, mock.Anything, mock.Anything).Return(map[int64][]ac.Permission{ - user56.ID: user56Permissions}, nil) - env.AcStore.On("GetUsersBasicRoles", mock.Anything, mock.Anything, mock.Anything).Return(map[int64][]string{ - user56.ID: {"Viewer"}}, nil) - env.TeamService.ExpectedTeamsByUser = user56Teams - env.UserService.ExpectedUser = user56 -} diff --git a/pkg/services/extsvcauth/oauthserver/oastest/fakes.go b/pkg/services/extsvcauth/oauthserver/oastest/fakes.go deleted file mode 100644 index 35a80fdab3..0000000000 --- a/pkg/services/extsvcauth/oauthserver/oastest/fakes.go +++ /dev/null @@ -1,38 +0,0 @@ -package oastest - -import ( - "context" - "net/http" - - "github.com/grafana/grafana/pkg/services/extsvcauth" - "github.com/grafana/grafana/pkg/services/extsvcauth/oauthserver" - "gopkg.in/square/go-jose.v2" -) - -type FakeService struct { - ExpectedClient *oauthserver.OAuthExternalService - ExpectedKey *jose.JSONWebKey - ExpectedErr error -} - -var _ oauthserver.OAuth2Server = &FakeService{} - -func (s *FakeService) SaveExternalService(ctx context.Context, cmd *extsvcauth.ExternalServiceRegistration) (*extsvcauth.ExternalService, error) { - return s.ExpectedClient.ToExternalService(nil), s.ExpectedErr -} - -func (s *FakeService) GetExternalService(ctx context.Context, id string) (*oauthserver.OAuthExternalService, error) { - return s.ExpectedClient, s.ExpectedErr -} - -func (s *FakeService) GetExternalServiceNames(ctx context.Context) ([]string, error) { - return nil, nil -} - -func (s *FakeService) RemoveExternalService(ctx context.Context, name string) error { - return s.ExpectedErr -} - -func (s *FakeService) HandleTokenRequest(rw http.ResponseWriter, req *http.Request) {} - -func (s *FakeService) HandleIntrospectionRequest(rw http.ResponseWriter, req *http.Request) {} diff --git a/pkg/services/extsvcauth/oauthserver/oastest/store_mock.go b/pkg/services/extsvcauth/oauthserver/oastest/store_mock.go deleted file mode 100644 index 68d6aeab5c..0000000000 --- a/pkg/services/extsvcauth/oauthserver/oastest/store_mock.go +++ /dev/null @@ -1,191 +0,0 @@ -// Code generated by mockery v2.35.2. DO NOT EDIT. - -package oastest - -import ( - context "context" - - mock "github.com/stretchr/testify/mock" - jose "gopkg.in/square/go-jose.v2" - - oauthserver "github.com/grafana/grafana/pkg/services/extsvcauth/oauthserver" -) - -// MockStore is an autogenerated mock type for the Store type -type MockStore struct { - mock.Mock -} - -// DeleteExternalService provides a mock function with given fields: ctx, id -func (_m *MockStore) DeleteExternalService(ctx context.Context, id string) error { - ret := _m.Called(ctx, id) - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { - r0 = rf(ctx, id) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// GetExternalService provides a mock function with given fields: ctx, id -func (_m *MockStore) GetExternalService(ctx context.Context, id string) (*oauthserver.OAuthExternalService, error) { - ret := _m.Called(ctx, id) - - var r0 *oauthserver.OAuthExternalService - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string) (*oauthserver.OAuthExternalService, error)); ok { - return rf(ctx, id) - } - if rf, ok := ret.Get(0).(func(context.Context, string) *oauthserver.OAuthExternalService); ok { - r0 = rf(ctx, id) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*oauthserver.OAuthExternalService) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { - r1 = rf(ctx, id) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// GetExternalServiceByName provides a mock function with given fields: ctx, name -func (_m *MockStore) GetExternalServiceByName(ctx context.Context, name string) (*oauthserver.OAuthExternalService, error) { - ret := _m.Called(ctx, name) - - var r0 *oauthserver.OAuthExternalService - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string) (*oauthserver.OAuthExternalService, error)); ok { - return rf(ctx, name) - } - if rf, ok := ret.Get(0).(func(context.Context, string) *oauthserver.OAuthExternalService); ok { - r0 = rf(ctx, name) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*oauthserver.OAuthExternalService) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { - r1 = rf(ctx, name) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// GetExternalServiceNames provides a mock function with given fields: ctx -func (_m *MockStore) GetExternalServiceNames(ctx context.Context) ([]string, error) { - ret := _m.Called(ctx) - - var r0 []string - var r1 error - if rf, ok := ret.Get(0).(func(context.Context) ([]string, error)); ok { - return rf(ctx) - } - if rf, ok := ret.Get(0).(func(context.Context) []string); ok { - r0 = rf(ctx) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]string) - } - } - - if rf, ok := ret.Get(1).(func(context.Context) error); ok { - r1 = rf(ctx) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// GetExternalServicePublicKey provides a mock function with given fields: ctx, clientID -func (_m *MockStore) GetExternalServicePublicKey(ctx context.Context, clientID string) (*jose.JSONWebKey, error) { - ret := _m.Called(ctx, clientID) - - var r0 *jose.JSONWebKey - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string) (*jose.JSONWebKey, error)); ok { - return rf(ctx, clientID) - } - if rf, ok := ret.Get(0).(func(context.Context, string) *jose.JSONWebKey); ok { - r0 = rf(ctx, clientID) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*jose.JSONWebKey) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { - r1 = rf(ctx, clientID) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// RegisterExternalService provides a mock function with given fields: ctx, client -func (_m *MockStore) RegisterExternalService(ctx context.Context, client *oauthserver.OAuthExternalService) error { - ret := _m.Called(ctx, client) - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *oauthserver.OAuthExternalService) error); ok { - r0 = rf(ctx, client) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// SaveExternalService provides a mock function with given fields: ctx, client -func (_m *MockStore) SaveExternalService(ctx context.Context, client *oauthserver.OAuthExternalService) error { - ret := _m.Called(ctx, client) - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *oauthserver.OAuthExternalService) error); ok { - r0 = rf(ctx, client) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// UpdateExternalServiceGrantTypes provides a mock function with given fields: ctx, clientID, grantTypes -func (_m *MockStore) UpdateExternalServiceGrantTypes(ctx context.Context, clientID string, grantTypes string) error { - ret := _m.Called(ctx, clientID, grantTypes) - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok { - r0 = rf(ctx, clientID, grantTypes) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// NewMockStore creates a new instance of MockStore. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewMockStore(t interface { - mock.TestingT - Cleanup(func()) -}) *MockStore { - mock := &MockStore{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/pkg/services/extsvcauth/oauthserver/store/database.go b/pkg/services/extsvcauth/oauthserver/store/database.go deleted file mode 100644 index bd0978b098..0000000000 --- a/pkg/services/extsvcauth/oauthserver/store/database.go +++ /dev/null @@ -1,252 +0,0 @@ -package store - -import ( - "context" - "crypto/ecdsa" - "crypto/rsa" - "errors" - - "gopkg.in/square/go-jose.v2" - - "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/services/extsvcauth/oauthserver" - "github.com/grafana/grafana/pkg/services/extsvcauth/oauthserver/utils" -) - -type store struct { - db db.DB -} - -func NewStore(db db.DB) oauthserver.Store { - return &store{db: db} -} - -func createImpersonatePermissions(sess *db.Session, client *oauthserver.OAuthExternalService) error { - if len(client.ImpersonatePermissions) == 0 { - return nil - } - - insertPermQuery := make([]any, 1, len(client.ImpersonatePermissions)*3+1) - insertPermStmt := `INSERT INTO oauth_impersonate_permission (client_id, action, scope) VALUES ` - for _, perm := range client.ImpersonatePermissions { - insertPermStmt += "(?, ?, ?)," - insertPermQuery = append(insertPermQuery, client.ClientID, perm.Action, perm.Scope) - } - insertPermQuery[0] = insertPermStmt[:len(insertPermStmt)-1] - _, err := sess.Exec(insertPermQuery...) - return err -} - -func registerExternalService(sess *db.Session, client *oauthserver.OAuthExternalService) error { - insertQuery := []any{ - `INSERT INTO oauth_client (name, client_id, secret, grant_types, audiences, service_account_id, public_pem, redirect_uri) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, - client.Name, - client.ClientID, - client.Secret, - client.GrantTypes, - client.Audiences, - client.ServiceAccountID, - client.PublicPem, - client.RedirectURI, - } - if _, err := sess.Exec(insertQuery...); err != nil { - return err - } - - return createImpersonatePermissions(sess, client) -} - -func (s *store) RegisterExternalService(ctx context.Context, client *oauthserver.OAuthExternalService) error { - return s.db.WithTransactionalDbSession(ctx, func(sess *db.Session) error { - return registerExternalService(sess, client) - }) -} - -func recreateImpersonatePermissions(sess *db.Session, client *oauthserver.OAuthExternalService, prevClientID string) error { - deletePermQuery := `DELETE FROM oauth_impersonate_permission WHERE client_id = ?` - if _, errDelPerm := sess.Exec(deletePermQuery, prevClientID); errDelPerm != nil { - return errDelPerm - } - - if len(client.ImpersonatePermissions) == 0 { - return nil - } - - return createImpersonatePermissions(sess, client) -} - -func updateExternalService(sess *db.Session, client *oauthserver.OAuthExternalService, prevClientID string) error { - updateQuery := []any{ - `UPDATE oauth_client SET client_id = ?, secret = ?, grant_types = ?, audiences = ?, service_account_id = ?, public_pem = ?, redirect_uri = ? WHERE name = ?`, - client.ClientID, - client.Secret, - client.GrantTypes, - client.Audiences, - client.ServiceAccountID, - client.PublicPem, - client.RedirectURI, - client.Name, - } - if _, err := sess.Exec(updateQuery...); err != nil { - return err - } - - return recreateImpersonatePermissions(sess, client, prevClientID) -} - -func (s *store) SaveExternalService(ctx context.Context, client *oauthserver.OAuthExternalService) error { - if client.Name == "" { - return oauthserver.ErrClientRequiredName - } - return s.db.WithTransactionalDbSession(ctx, func(sess *db.Session) error { - previous, errFetchExtSvc := getExternalServiceByName(sess, client.Name) - if errFetchExtSvc != nil && !errors.Is(errFetchExtSvc, oauthserver.ErrClientNotFound) { - return errFetchExtSvc - } - if previous == nil { - return registerExternalService(sess, client) - } - return updateExternalService(sess, client, previous.ClientID) - }) -} - -func (s *store) GetExternalService(ctx context.Context, id string) (*oauthserver.OAuthExternalService, error) { - res := &oauthserver.OAuthExternalService{} - if id == "" { - return nil, oauthserver.ErrClientRequiredID - } - - err := s.db.WithTransactionalDbSession(ctx, func(sess *db.Session) error { - getClientQuery := `SELECT - id, name, client_id, secret, grant_types, audiences, service_account_id, public_pem, redirect_uri - FROM oauth_client - WHERE client_id = ?` - found, err := sess.SQL(getClientQuery, id).Get(res) - if err != nil { - return err - } - if !found { - res = nil - return oauthserver.ErrClientNotFoundFn(id) - } - - impersonatePermQuery := `SELECT action, scope FROM oauth_impersonate_permission WHERE client_id = ?` - return sess.SQL(impersonatePermQuery, id).Find(&res.ImpersonatePermissions) - }) - - return res, err -} - -// GetPublicKey returns public key, issued by 'issuer', and assigned for subject. Public key is used to check -// signature of jwt assertion in authorization grants. -func (s *store) GetExternalServicePublicKey(ctx context.Context, clientID string) (*jose.JSONWebKey, error) { - res := &oauthserver.OAuthExternalService{} - if clientID == "" { - return nil, oauthserver.ErrClientRequiredID - } - - if err := s.db.WithTransactionalDbSession(ctx, func(sess *db.Session) error { - getKeyQuery := `SELECT public_pem FROM oauth_client WHERE client_id = ?` - found, err := sess.SQL(getKeyQuery, clientID).Get(res) - if err != nil { - return err - } - if !found { - return oauthserver.ErrClientNotFoundFn(clientID) - } - return nil - }); err != nil { - return nil, err - } - - key, errParseKey := utils.ParsePublicKeyPem(res.PublicPem) - if errParseKey != nil { - return nil, errParseKey - } - - var alg string - switch key.(type) { - case *rsa.PublicKey: - alg = oauthserver.RS256 - case *ecdsa.PublicKey: - alg = oauthserver.ES256 - } - - return &jose.JSONWebKey{ - Algorithm: alg, - Key: key, - }, nil -} - -func (s *store) GetExternalServiceByName(ctx context.Context, name string) (*oauthserver.OAuthExternalService, error) { - res := &oauthserver.OAuthExternalService{} - if name == "" { - return nil, oauthserver.ErrClientRequiredName - } - - err := s.db.WithTransactionalDbSession(ctx, func(sess *db.Session) error { - var errGetByName error - res, errGetByName = getExternalServiceByName(sess, name) - return errGetByName - }) - - return res, err -} - -func getExternalServiceByName(sess *db.Session, name string) (*oauthserver.OAuthExternalService, error) { - res := &oauthserver.OAuthExternalService{} - getClientQuery := `SELECT - id, name, client_id, secret, grant_types, audiences, service_account_id, public_pem, redirect_uri - FROM oauth_client - WHERE name = ?` - found, err := sess.SQL(getClientQuery, name).Get(res) - if err != nil { - return nil, err - } - if !found { - return nil, oauthserver.ErrClientNotFoundFn(name) - } - - impersonatePermQuery := `SELECT action, scope FROM oauth_impersonate_permission WHERE client_id = ?` - errPerm := sess.SQL(impersonatePermQuery, res.ClientID).Find(&res.ImpersonatePermissions) - - return res, errPerm -} - -// FIXME: If we ever do a search method remove that method -func (s *store) GetExternalServiceNames(ctx context.Context) ([]string, error) { - res := []string{} - - err := s.db.WithTransactionalDbSession(ctx, func(sess *db.Session) error { - return sess.SQL(`SELECT name FROM oauth_client`).Find(&res) - }) - - return res, err -} - -func (s *store) UpdateExternalServiceGrantTypes(ctx context.Context, clientID, grantTypes string) error { - if clientID == "" { - return oauthserver.ErrClientRequiredID - } - - return s.db.WithTransactionalDbSession(ctx, func(sess *db.Session) error { - query := `UPDATE oauth_client SET grant_types = ? WHERE client_id = ?` - _, err := sess.Exec(query, grantTypes, clientID) - return err - }) -} - -func (s *store) DeleteExternalService(ctx context.Context, id string) error { - if id == "" { - return oauthserver.ErrClientRequiredID - } - - return s.db.WithTransactionalDbSession(ctx, func(sess *db.Session) error { - if _, err := sess.Exec(`DELETE FROM oauth_client WHERE client_id = ?`, id); err != nil { - return err - } - - _, err := sess.Exec(`DELETE FROM oauth_impersonate_permission WHERE client_id = ?`, id) - return err - }) -} diff --git a/pkg/services/extsvcauth/oauthserver/store/database_test.go b/pkg/services/extsvcauth/oauthserver/store/database_test.go deleted file mode 100644 index ae03a1c6f8..0000000000 --- a/pkg/services/extsvcauth/oauthserver/store/database_test.go +++ /dev/null @@ -1,490 +0,0 @@ -package store - -import ( - "context" - "errors" - "testing" - - "github.com/go-jose/go-jose/v3" - "github.com/stretchr/testify/require" - - "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/services/accesscontrol" - "github.com/grafana/grafana/pkg/services/extsvcauth/oauthserver" - "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/tests/testsuite" -) - -func TestMain(m *testing.M) { - testsuite.Run(m) -} - -func TestStore_RegisterAndGetClient(t *testing.T) { - s := &store{db: db.InitTestDB(t, db.InitTestDBOpt{FeatureFlags: []string{featuremgmt.FlagExternalServiceAuth}})} - tests := []struct { - name string - client oauthserver.OAuthExternalService - wantErr bool - }{ - { - name: "register and get", - client: oauthserver.OAuthExternalService{ - Name: "The Worst App Ever", - ClientID: "ANonRandomClientID", - Secret: "ICouldKeepSecrets", - GrantTypes: "clients_credentials", - PublicPem: []byte(`------BEGIN FAKE PUBLIC KEY----- -VGhpcyBJcyBOb3QgQW4gUlNBIEtleS4gVGhpcyBJcyBOb3QgQW4gUlNBIEtleS4gVGhpcyBJcyBO -b3QgQW4gUlNBIEtleS4gVGhpcyBJcyBOb3QgQW4gUlNBIEtleS4gVGhpcyBJcyBOb3QgQW4gUlNB -IEtleS4gVGhpcyBJcyBOb3QgQW4gUlNBIEtleS4gVGhpcyBJcyBOb3QgQW4gUlNBIEtleS4gVGhp -cyBJcyBOb3QgQW4gUlNBIEtleS4gVGhpcyBJcyBOb3QgQW4gUlNBIEtleS4gVGhpcyBJcyBOb3Qg -QW4gUlNBIEtleS4gVGhpcyBJcyBOb3QgQW4gUlNBIEtleS4gVGhpcyBJcyBOb3QgQW4gUlNBIEtl -eS4gVGhpcyBJcyBOb3QgQW4gUlNBIEtleS4gVGhpcyBJcyBOb3QgQW4gUlNBIEtleS4gVGhpcyBJ -cyBOb3QgQW4gUlNBIEtleS4gVGhpcyBJcyBOb3QgQW4gUlNBIEtleS4gVGhpcyBJcyBOb3QgQW4g -UlNBIEtleS4gVGhpcyBJcyBOb3QgQW4gUlNBIEtleS4gVGhpcyBJcyBOb3QgQW4gUlNBIEtleS4g -VGhpcyBJcyBOb3QgQW4gUlNBIEtleS4gVGhpcyBJcyBOb3QgQW4gUlNBIEtleS4gVGhpcyBJcyBO -b3QgQW4gUlNBIEtleS4gVGhpcyBJcyBOb3QgQW4gUlNBIEtleS4gVGhpcyBJcyBOb3QgQW4gUlNB -IEtleS4gVGhpcyBJcyBOb3QgQW4gUlNBIEtleS4gVGhpcyBJcyBOb3QgQW4gUlNBIEtleS4gVGhp -cyBJcyBOb3QgQW4gUlNBIEtleS4gVGhpcyBJcyBOb3QgQW4gUlNBIEtleS4gVGhpcyBJcyBOb3Qg -QW4gUlNBIEtleS4gVGhpcyBJcyBOb3QgQW4gUlNBIEtleS4gVGhpcyBJcyBOb3QgQW4gUlNBIEtl -eS4gVGhpcyBJcyBOb3QgQW4gUlNBIEtleS4gVGhpcyBJcyBOb3QgQW4gUlNBIEtleS4gVGhpcyBJ -cyBOb3QgQW4gUlNBIEtleS4gVGhpcyBJcyBOb3QgQW4gUlNBIEtleS4gVGhpcyBJcyBOb3QgQW4g -UlNBIEtleS4gVGhpcyBJcyBOb3QgQW4gUlNBIEtleS4gVGhpcyBJcyBOb3QgQW4gUlNBIEtleS4g -VGhpcyBJcyBOb3QgQW4gUlNBIEtleS4gVGhpcyBJcyBOb3QgQW4gUlNBIEtleS4gVGhpcyBJcyBO -b3QgQW4gUlNBIEtleS4gVGhpcyBJcyBOb3QgQW4gUlNBIEtleS4gVGhpcyBJcyBOb3QgQW4gUlNB -IEtleS4gVGhpcyBJcyBOb3QgQW4gUlNBIEtleS4gVGhpcyBJcyBOb3QgQW4gUlNBIEtleS4gVGhp -cyBJcyBOb3QgQW4gUlNBIEtleS4gVGhpcyBJcyBOb3QgQW4gUlNBIEtleS4uLi4gSXQgSXMgSnVz -dCBBIFJlZ3VsYXIgQmFzZTY0IEVuY29kZWQgU3RyaW5nLi4uCg== -------END FAKE PUBLIC KEY-----`), - ServiceAccountID: 2, - SelfPermissions: nil, - ImpersonatePermissions: nil, - RedirectURI: "/whereto", - }, - wantErr: false, - }, - { - name: "register with impersonate permissions and get", - client: oauthserver.OAuthExternalService{ - Name: "The Best App Ever", - ClientID: "AnAlmostRandomClientID", - Secret: "ICannotKeepSecrets", - GrantTypes: "clients_credentials", - PublicPem: []byte(`test`), - ServiceAccountID: 2, - SelfPermissions: nil, - ImpersonatePermissions: []accesscontrol.Permission{ - {Action: "dashboards:create", Scope: "folders:*"}, - {Action: "dashboards:read", Scope: "folders:*"}, - {Action: "dashboards:read", Scope: "dashboards:*"}, - {Action: "dashboards:write", Scope: "folders:*"}, - {Action: "dashboards:write", Scope: "dashboards:*"}, - }, - RedirectURI: "/whereto", - }, - wantErr: false, - }, - { - name: "register with audiences and get", - client: oauthserver.OAuthExternalService{ - Name: "The Most Normal App Ever", - ClientID: "AnAlmostRandomClientIDAgain", - Secret: "ICanKeepSecretsEventually", - GrantTypes: "clients_credentials", - PublicPem: []byte(`test`), - ServiceAccountID: 2, - SelfPermissions: nil, - Audiences: "https://oauth.test/,https://sub.oauth.test/", - RedirectURI: "/whereto", - }, - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ctx := context.Background() - err := s.RegisterExternalService(ctx, &tt.client) - if tt.wantErr { - require.Error(t, err) - return - } - require.NoError(t, err) - - // Compare results - compareClientToStored(t, s, &tt.client) - }) - } -} - -func TestStore_SaveExternalService(t *testing.T) { - client1 := oauthserver.OAuthExternalService{ - Name: "my-external-service", - ClientID: "ClientID", - Secret: "Secret", - GrantTypes: "client_credentials", - PublicPem: []byte("test"), - ServiceAccountID: 2, - ImpersonatePermissions: []accesscontrol.Permission{}, - RedirectURI: "/whereto", - } - client1WithPerm := client1 - client1WithPerm.ImpersonatePermissions = []accesscontrol.Permission{ - {Action: "dashboards:read", Scope: "folders:*"}, - {Action: "dashboards:read", Scope: "dashboards:*"}, - } - client1WithNewSecrets := client1 - client1WithNewSecrets.ClientID = "NewClientID" - client1WithNewSecrets.Secret = "NewSecret" - client1WithNewSecrets.PublicPem = []byte("newtest") - - client1WithAud := client1 - client1WithAud.Audiences = "https://oauth.test/,https://sub.oauth.test/" - - tests := []struct { - name string - runs []oauthserver.OAuthExternalService - wantErr bool - }{ - { - name: "error no name", - runs: []oauthserver.OAuthExternalService{{}}, - wantErr: true, - }, - { - name: "simple register", - runs: []oauthserver.OAuthExternalService{client1}, - wantErr: false, - }, - { - name: "no update", - runs: []oauthserver.OAuthExternalService{client1, client1}, - wantErr: false, - }, - { - name: "add permissions", - runs: []oauthserver.OAuthExternalService{client1, client1WithPerm}, - wantErr: false, - }, - { - name: "remove permissions", - runs: []oauthserver.OAuthExternalService{client1WithPerm, client1}, - wantErr: false, - }, - { - name: "update id and secrets", - runs: []oauthserver.OAuthExternalService{client1, client1WithNewSecrets}, - wantErr: false, - }, - { - name: "update audience", - runs: []oauthserver.OAuthExternalService{client1, client1WithAud}, - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - s := &store{db: db.InitTestDB(t, db.InitTestDBOpt{FeatureFlags: []string{featuremgmt.FlagExternalServiceAuth}})} - for i := range tt.runs { - err := s.SaveExternalService(context.Background(), &tt.runs[i]) - if tt.wantErr { - require.Error(t, err) - return - } - require.NoError(t, err) - - compareClientToStored(t, s, &tt.runs[i]) - } - }) - } -} - -func TestStore_GetExternalServiceByName(t *testing.T) { - client1 := oauthserver.OAuthExternalService{ - Name: "my-external-service", - ClientID: "ClientID", - Secret: "Secret", - GrantTypes: "client_credentials", - PublicPem: []byte("test"), - ServiceAccountID: 2, - ImpersonatePermissions: []accesscontrol.Permission{}, - RedirectURI: "/whereto", - } - client2 := oauthserver.OAuthExternalService{ - Name: "my-external-service-2", - ClientID: "ClientID2", - Secret: "Secret2", - GrantTypes: "client_credentials,urn:ietf:params:grant-type:jwt-bearer", - PublicPem: []byte("test2"), - ServiceAccountID: 3, - Audiences: "https://oauth.test/,https://sub.oauth.test/", - ImpersonatePermissions: []accesscontrol.Permission{ - {Action: "dashboards:read", Scope: "folders:*"}, - {Action: "dashboards:read", Scope: "dashboards:*"}, - }, - RedirectURI: "/whereto", - } - s := &store{db: db.InitTestDB(t, db.InitTestDBOpt{FeatureFlags: []string{featuremgmt.FlagExternalServiceAuth}})} - require.NoError(t, s.SaveExternalService(context.Background(), &client1)) - require.NoError(t, s.SaveExternalService(context.Background(), &client2)) - - tests := []struct { - name string - search string - want *oauthserver.OAuthExternalService - wantErr bool - }{ - { - name: "no name provided", - search: "", - want: nil, - wantErr: true, - }, - { - name: "not found", - search: "unknown-external-service", - want: nil, - wantErr: true, - }, - { - name: "search client 1 by name", - search: "my-external-service", - want: &client1, - wantErr: false, - }, - { - name: "search client 2 by name", - search: "my-external-service-2", - want: &client2, - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - stored, err := s.GetExternalServiceByName(context.Background(), tt.search) - if tt.wantErr { - require.Error(t, err) - return - } - require.NoError(t, err) - - compareClients(t, stored, tt.want) - }) - } -} - -func TestStore_GetExternalServicePublicKey(t *testing.T) { - clientID := "ClientID" - createClient := func(clientID string, publicPem string) *oauthserver.OAuthExternalService { - return &oauthserver.OAuthExternalService{ - Name: "my-external-service", - ClientID: clientID, - Secret: "Secret", - GrantTypes: "client_credentials", - PublicPem: []byte(publicPem), - ServiceAccountID: 2, - ImpersonatePermissions: []accesscontrol.Permission{}, - RedirectURI: "/whereto", - } - } - - testCases := []struct { - name string - client *oauthserver.OAuthExternalService - clientID string - want *jose.JSONWebKey - wantKeyType string - wantErr bool - }{ - { - name: "should return an error when clientID is empty", - clientID: "", - client: createClient(clientID, ""), - want: nil, - wantErr: true, - }, - { - name: "should return an error when the client was not found", - clientID: "random", - client: createClient(clientID, ""), - want: nil, - wantErr: true, - }, - { - name: "should return an error when PublicPem is not valid", - clientID: clientID, - client: createClient(clientID, ""), - want: nil, - wantErr: true, - }, - { - name: "should return the JSON Web Key ES256", - clientID: clientID, - client: createClient(clientID, `-----BEGIN PUBLIC KEY----- -MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEbsGtoGJTopAIbhqy49/vyCJuDot+ -mgGaC8vUIigFQVsVB+v/HZ4yG1Rcvysig+tyNk1dZQpozpFc2dGmzHlGhw== ------END PUBLIC KEY-----`), - wantKeyType: oauthserver.ES256, - wantErr: false, - }, - { - name: "should return the JSON Web Key RS256", - clientID: clientID, - client: createClient(clientID, `-----BEGIN RSA PUBLIC KEY----- -MIIBCgKCAQEAxkly/cHvsxd6EcShGUlFAB5lIMlIbGRocCVWbIM26f6pnGr+gCNv -s365DQdQ/jUjF8bSEQM+EtjGlv2Y7Jm7dQROpPzX/1M+53Us/Gl138UtAEgL5ZKe -SKN5J/f9Nx4wkgb99v2Bt0nz6xv+kSJwgR0o8zi8shDR5n7a5mTdlQe2NOixzWlT -vnpp6Tm+IE+XyXXcrCr01I9Rf+dKuYOPSJ1K3PDgFmmGvsLcjRCCK9EftfY0keU+ -IP+sh8ewNxc6KcaLBXm3Tadb1c/HyuMi6FyYw7s9m8tyAvI1CMBAcXqLIEaRgNrc -vuO8AU0bVoUmYMKhozkcCYHudkeS08hEjQIDAQAB ------END RSA PUBLIC KEY-----`), - wantKeyType: oauthserver.RS256, - wantErr: false, - }, - } - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - s := &store{db: db.InitTestDB(t, db.InitTestDBOpt{FeatureFlags: []string{featuremgmt.FlagExternalServiceAuth}})} - require.NoError(t, s.SaveExternalService(context.Background(), tc.client)) - - webKey, err := s.GetExternalServicePublicKey(context.Background(), tc.clientID) - if tc.wantErr { - require.Error(t, err) - return - } - require.NoError(t, err) - - require.Equal(t, tc.wantKeyType, webKey.Algorithm) - }) - } -} - -func TestStore_RemoveExternalService(t *testing.T) { - ctx := context.Background() - client1 := oauthserver.OAuthExternalService{ - Name: "my-external-service", - ClientID: "ClientID", - ImpersonatePermissions: []accesscontrol.Permission{}, - } - client2 := oauthserver.OAuthExternalService{ - Name: "my-external-service-2", - ClientID: "ClientID2", - ImpersonatePermissions: []accesscontrol.Permission{ - {Action: "dashboards:read", Scope: "folders:*"}, - {Action: "dashboards:read", Scope: "dashboards:*"}, - }, - } - - // Init store - s := &store{db: db.InitTestDB(t, db.InitTestDBOpt{FeatureFlags: []string{featuremgmt.FlagExternalServiceAuth}})} - require.NoError(t, s.SaveExternalService(context.Background(), &client1)) - require.NoError(t, s.SaveExternalService(context.Background(), &client2)) - - // Check presence of clients in store - getState := func(t *testing.T) map[string]bool { - client, err := s.GetExternalService(ctx, "ClientID") - if err != nil && !errors.Is(err, oauthserver.ErrClientNotFound) { - require.Fail(t, "error fetching client") - } - - client2, err := s.GetExternalService(ctx, "ClientID2") - if err != nil && !errors.Is(err, oauthserver.ErrClientNotFound) { - require.Fail(t, "error fetching client") - } - - return map[string]bool{ - "ClientID": client != nil, - "ClientID2": client2 != nil, - } - } - - tests := []struct { - name string - id string - state map[string]bool - wantErr bool - }{ - { - name: "no id provided", - state: map[string]bool{"ClientID": true, "ClientID2": true}, - wantErr: true, - }, - { - name: "not found", - id: "ClientID3", - state: map[string]bool{"ClientID": true, "ClientID2": true}, - wantErr: false, - }, - { - name: "remove client 2", - id: "ClientID2", - state: map[string]bool{"ClientID": true, "ClientID2": false}, - wantErr: false, - }, - { - name: "remove client 1", - id: "ClientID", - state: map[string]bool{"ClientID": false, "ClientID2": false}, - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := s.DeleteExternalService(ctx, tt.id) - if tt.wantErr { - require.Error(t, err) - } else { - require.NoError(t, err) - } - require.EqualValues(t, tt.state, getState(t)) - }) - } -} - -func Test_store_GetExternalServiceNames(t *testing.T) { - ctx := context.Background() - client1 := oauthserver.OAuthExternalService{ - Name: "my-external-service", - ClientID: "ClientID", - ImpersonatePermissions: []accesscontrol.Permission{}, - } - client2 := oauthserver.OAuthExternalService{ - Name: "my-external-service-2", - ClientID: "ClientID2", - ImpersonatePermissions: []accesscontrol.Permission{ - {Action: "dashboards:read", Scope: "folders:*"}, - {Action: "dashboards:read", Scope: "dashboards:*"}, - }, - } - - // Init store - s := &store{db: db.InitTestDB(t, db.InitTestDBOpt{FeatureFlags: []string{featuremgmt.FlagExternalServiceAuth}})} - require.NoError(t, s.SaveExternalService(context.Background(), &client1)) - require.NoError(t, s.SaveExternalService(context.Background(), &client2)) - - got, err := s.GetExternalServiceNames(ctx) - require.NoError(t, err) - require.ElementsMatch(t, []string{"my-external-service", "my-external-service-2"}, got) -} - -func compareClientToStored(t *testing.T, s *store, wanted *oauthserver.OAuthExternalService) { - ctx := context.Background() - stored, err := s.GetExternalService(ctx, wanted.ClientID) - require.NoError(t, err) - require.NotNil(t, stored) - - compareClients(t, stored, wanted) -} - -func compareClients(t *testing.T, stored *oauthserver.OAuthExternalService, wanted *oauthserver.OAuthExternalService) { - // Reset ID so we can compare - require.NotZero(t, stored.ID) - stored.ID = 0 - - // Compare permissions separately - wantedPerms := wanted.ImpersonatePermissions - storedPerms := stored.ImpersonatePermissions - wanted.ImpersonatePermissions = nil - stored.ImpersonatePermissions = nil - require.EqualValues(t, *wanted, *stored) - require.ElementsMatch(t, wantedPerms, storedPerms) -} diff --git a/pkg/services/extsvcauth/oauthserver/utils/utils.go b/pkg/services/extsvcauth/oauthserver/utils/utils.go deleted file mode 100644 index 83f79d1973..0000000000 --- a/pkg/services/extsvcauth/oauthserver/utils/utils.go +++ /dev/null @@ -1,35 +0,0 @@ -package utils - -import ( - "crypto/x509" - "encoding/pem" - "errors" - "fmt" - "strconv" - "strings" - - "github.com/grafana/grafana/pkg/services/authn" -) - -// ParseUserIDFromSubject parses the user ID from format "user:id:". -func ParseUserIDFromSubject(subject string) (int64, error) { - trimmed := strings.TrimPrefix(subject, fmt.Sprintf("%s:id:", authn.NamespaceUser)) - return strconv.ParseInt(trimmed, 10, 64) -} - -// ParsePublicKeyPem parses the public key from the PEM encoded public key. -func ParsePublicKeyPem(publicPem []byte) (any, error) { - block, _ := pem.Decode(publicPem) - if block == nil { - return nil, errors.New("could not decode PEM block") - } - - switch block.Type { - case "PUBLIC KEY": - return x509.ParsePKIXPublicKey(block.Bytes) - case "RSA PUBLIC KEY": - return x509.ParsePKCS1PublicKey(block.Bytes) - default: - return nil, fmt.Errorf("unknown key type %q", block.Type) - } -} diff --git a/pkg/services/extsvcauth/oauthserver/utils/utils_test.go b/pkg/services/extsvcauth/oauthserver/utils/utils_test.go deleted file mode 100644 index 8da34460e6..0000000000 --- a/pkg/services/extsvcauth/oauthserver/utils/utils_test.go +++ /dev/null @@ -1,82 +0,0 @@ -package utils - -import ( - "testing" - - "github.com/stretchr/testify/require" -) - -func TestParsePublicKeyPem(t *testing.T) { - testCases := []struct { - name string - publicKeyPem string - wantErr bool - }{ - { - name: "should return error when the public key pem is empty", - publicKeyPem: "", - wantErr: true, - }, - { - name: "should return error when the public key pem is invalid", - publicKeyPem: `-----BEGIN RSA PRIVATE KEY----- -MIIEowIBAAKCAQEAxP72NEnQF3o3eFFMtFqyloW9oLhTydxXS2dA2NolMvXewO77 -UvJw54wkOdrJrJO2BIw+XBrrb+13+koRUnwa2DNsh+SWG0PEe/31mt0zJrCmNM37 -iJYIu3KZR2aRlierVY5gyrIniBIZ9blQspI6SRY9xmo6Wdh0VCRnsCV5sMlaqerI -snLpYOjGtMmL0rFuW2jKrAzpbq7L99IDgPbiH7tluaQkGIxoc29S4wjwg0NgQONT -GkfJEeXQIkxOHNM5WGb8mvjX4U0jMdXvC4WUWcS+KpcIV7ee8uEs2xDz++N4HYAS -ty37sY8wwW22QUW9I7XlSC4rsC88Ft5ar8yLsQIDAQABAoIBAAQ1yTv+mFmKGYGj -JiskFZVBNDdpPRQvNvfj8+c2iU08ozc3HEyuZQKT1InefsknCoCwIRyNkDrPBc2F -8/cR8y5r8e25EUqxoPM/7xXxVIinBZRTEyU9BKCB71vu6Z1eiWs9jNzEIDNopKCj -ZmG8nY2Gkckp58eYCEtskEE72c0RBPg8ZTBdc1cLqbNVUjkLvR5e98ruDz6b+wyH -FnztZ0k48zM047Ior69OwFRBg+S7d6cgMMmcq4X2pg3xgQMs0Se/4+pmvBf9JPSB -kl3qpVAkzM1IFdrmpFtBzeaqYNj3uU6Bm7NxEiqjAoeDxO231ziSdzIPtXIy5eRl -9WMZCqkCgYEA1ZOaT77aa54zgjAwjNB2Poo3yoUtYJz+yNCR0CPM4MzCas3PR4XI -WUXo+RNofWvRJF88aAVX7+J0UTnRr25rN12NDbo3aZhX2YNDGBe3hgB/FOAI5UAh -9SaU070PFeGzqlu/xWdx5GFk/kiNUNLX/X4xgUGPTiwY4LQeI9lffzkCgYEA7CA7 -VHaNPGVsaNKMJVjrZeYOxNBsrH99IEgaP76DC+EVR2JYVzrNxmN6ZlRxD4CRTcyd -oquTFoFFw26gJIJAYF8MtusOD3PArnpdCRSoELezYdtVhS0yx8TSHGVC9MWSSt7O -IdjzEFpA99HPkYFjXUiWXjfCTK7ofI0RXC6a+DkCgYEAoQb8nYuEGwfYRhwXPtQd -kuGbVvI6WFGGN9opVgjn+8Xl/6jU01QmzkhLcyAS9B1KPmYfoT4GIzNWB7fURLS3 -2bKLGwJ/rPnTooe5Gn0nPb06E38mtdI4yCEirNIqgZD+aT9rw2ZPFKXqA16oTXvq -pZFzucS4S3Qr/Z9P6i+GNOECgYBkvPuS9WEcO0kdD3arGFyVhKkYXrN+hIWlmB1a -xLS0BLtHUTXPQU85LIez0KLLslZLkthN5lVCbLSOxEueR9OfSe3qvC2ref7icWHv -1dg+CaGGRkUeJEJd6CKb6re+Jexb9OKMnjpU56yADgs4ULNLwQQl/jPu81BMkwKt -CVUkQQKBgFvbuUmYtP3aqV/Kt036Q6aB6Xwg29u2XFTe4BgW7c55teebtVmGA/zc -GMwRsF4rWCcScmHGcSKlW9L6S6OxmkYjDDRhimKyHgoiQ9tawWag2XCeOlyJ+hkc -/qwwKxScuFIi2xwT+aAmR70Xk11qXTft+DaEcHdxOOZD8gA0Gxr3 ------END RSA PRIVATE KEY-----`, - wantErr: true, - }, - { - name: "should parse the public key if it is in PKCS1 format", - publicKeyPem: `-----BEGIN RSA PUBLIC KEY----- -MIIBCgKCAQEAy06MeS06Ea7zGKfOM8kosxuUBMNhrWKWMvW4Jq1IXG+lyTfann2+ -kI1rKeWAQ9YbxNzLynahoKN47EQ6mqM1Yj5v9iKWtSvCMKHWBuqrG5ksaEQaAVsA -PDg8aOQrI1MSW9Hoc1CummcWX+HKNPVwIzG3sCboENFzEG8GrJgoNHZgmyOYEMMD -2WCdfY0I9Dm0/uuNMAcyMuVhRhOtT3j91zCXvDju2+M2EejApMkV9r7FqGmNH5Hw -8u43nWXnWc4UYXEItE8nPxuqsZia2mdB5MSIdKu8a7ytFcQ+tiK6vempnxHZytEL -6NDX8DLydHbEsLUn6hc76ODVkr/wRiuYdQIDAQAB ------END RSA PUBLIC KEY-----`, - wantErr: false, - }, - { - name: "should parse the public key if it is in PKIX/X.509 format", - publicKeyPem: `-----BEGIN PUBLIC KEY----- -MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEbsGtoGJTopAIbhqy49/vyCJuDot+ -mgGaC8vUIigFQVsVB+v/HZ4yG1Rcvysig+tyNk1dZQpozpFc2dGmzHlGhw== ------END PUBLIC KEY-----`, - wantErr: false, - }, - } - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - _, err := ParsePublicKeyPem([]byte(tc.publicKeyPem)) - if tc.wantErr { - require.Error(t, err) - } else { - require.NoError(t, err) - } - }) - } -} diff --git a/pkg/services/extsvcauth/registry/service.go b/pkg/services/extsvcauth/registry/service.go index 7348f607c6..9a7ec4a8cc 100644 --- a/pkg/services/extsvcauth/registry/service.go +++ b/pkg/services/extsvcauth/registry/service.go @@ -1,5 +1,7 @@ package registry +// FIXME (gamab): we can eventually remove this package + import ( "context" "sync" @@ -9,7 +11,6 @@ import ( "github.com/grafana/grafana/pkg/infra/serverlock" "github.com/grafana/grafana/pkg/infra/slugify" "github.com/grafana/grafana/pkg/services/extsvcauth" - "github.com/grafana/grafana/pkg/services/extsvcauth/oauthserver/oasimpl" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/serviceaccounts/extsvcaccounts" ) @@ -29,21 +30,20 @@ type serverLocker interface { type Registry struct { features featuremgmt.FeatureToggles logger log.Logger - oauthReg extsvcauth.ExternalServiceRegistry saReg extsvcauth.ExternalServiceRegistry + // FIXME (gamab): we can remove this field and use the saReg.GetExternalServiceNames directly extSvcProviders map[string]extsvcauth.AuthProvider lock sync.Mutex serverLock serverLocker } -func ProvideExtSvcRegistry(oauthServer *oasimpl.OAuth2ServiceImpl, saSvc *extsvcaccounts.ExtSvcAccountsService, serverLock *serverlock.ServerLockService, features featuremgmt.FeatureToggles) *Registry { +func ProvideExtSvcRegistry(saSvc *extsvcaccounts.ExtSvcAccountsService, serverLock *serverlock.ServerLockService, features featuremgmt.FeatureToggles) *Registry { return &Registry{ extSvcProviders: map[string]extsvcauth.AuthProvider{}, features: features, lock: sync.Mutex{}, logger: log.New("extsvcauth.registry"), - oauthReg: oauthServer, saReg: saSvc, serverLock: serverLock, } @@ -70,11 +70,6 @@ func (r *Registry) CleanUpOrphanedExternalServices(ctx context.Context) error { errCleanUp = err return } - case extsvcauth.OAuth2Server: - if err := r.oauthReg.RemoveExternalService(ctx, name); err != nil { - errCleanUp = err - return - } } } } @@ -121,13 +116,6 @@ func (r *Registry) RemoveExternalService(ctx context.Context, name string) error } r.logger.Debug("Routing External Service removal to the External Service Account service", "service", name) return r.saReg.RemoveExternalService(ctx, name) - case extsvcauth.OAuth2Server: - if !r.features.IsEnabled(ctx, featuremgmt.FlagExternalServiceAuth) { - r.logger.Debug("Skipping External Service removal, flag disabled", "service", name, "flag", featuremgmt.FlagExternalServiceAccounts) - return nil - } - r.logger.Debug("Routing External Service removal to the OAuth2Server", "service", name) - return r.oauthReg.RemoveExternalService(ctx, name) default: return extsvcauth.ErrUnknownProvider.Errorf("unknown provider '%v'", provider) } @@ -157,13 +145,6 @@ func (r *Registry) SaveExternalService(ctx context.Context, cmd *extsvcauth.Exte } r.logger.Debug("Routing the External Service registration to the External Service Account service", "service", cmd.Name) extSvc, errSave = r.saReg.SaveExternalService(ctx, cmd) - case extsvcauth.OAuth2Server: - if !r.features.IsEnabled(ctx, featuremgmt.FlagExternalServiceAuth) { - r.logger.Warn("Skipping External Service authentication, flag disabled", "service", cmd.Name, "flag", featuremgmt.FlagExternalServiceAuth) - return - } - r.logger.Debug("Routing the External Service registration to the OAuth2Server", "service", cmd.Name) - extSvc, errSave = r.oauthReg.SaveExternalService(ctx, cmd) default: errSave = extsvcauth.ErrUnknownProvider.Errorf("unknown provider '%v'", cmd.AuthProvider) } @@ -187,16 +168,7 @@ func (r *Registry) retrieveExtSvcProviders(ctx context.Context) (map[string]exts extsvcs[names[i]] = extsvcauth.ServiceAccounts } } - // Important to run this second as the OAuth server uses External Service Accounts as well. - if r.features.IsEnabled(ctx, featuremgmt.FlagExternalServiceAuth) { - names, err := r.oauthReg.GetExternalServiceNames(ctx) - if err != nil { - return nil, err - } - for i := range names { - extsvcs[names[i]] = extsvcauth.OAuth2Server - } - } + return extsvcs, nil } diff --git a/pkg/services/extsvcauth/registry/service_test.go b/pkg/services/extsvcauth/registry/service_test.go index f43f0b327f..40c046f4ce 100644 --- a/pkg/services/extsvcauth/registry/service_test.go +++ b/pkg/services/extsvcauth/registry/service_test.go @@ -14,9 +14,8 @@ import ( ) type TestEnv struct { - r *Registry - oauthReg *tests.ExternalServiceRegistryMock - saReg *tests.ExternalServiceRegistryMock + r *Registry + saReg *tests.ExternalServiceRegistryMock } // Never lock in tests @@ -29,12 +28,10 @@ func (f *fakeServerLock) LockExecuteAndReleaseWithRetries(ctx context.Context, a func setupTestEnv(t *testing.T) *TestEnv { env := TestEnv{} - env.oauthReg = tests.NewExternalServiceRegistryMock(t) env.saReg = tests.NewExternalServiceRegistryMock(t) env.r = &Registry{ - features: featuremgmt.WithFeatures(featuremgmt.FlagExternalServiceAuth, featuremgmt.FlagExternalServiceAccounts), + features: featuremgmt.WithFeatures(featuremgmt.FlagExternalServiceAccounts), logger: log.New("extsvcauth.registry.test"), - oauthReg: env.oauthReg, saReg: env.saReg, extSvcProviders: map[string]extsvcauth.AuthProvider{}, serverLock: &fakeServerLock{}, @@ -51,39 +48,24 @@ func TestRegistry_CleanUpOrphanedExternalServices(t *testing.T) { name: "should not clean up when every service registered", init: func(te *TestEnv) { // Have registered two services one requested a service account, the other requested to be an oauth client - te.r.extSvcProviders = map[string]extsvcauth.AuthProvider{"sa-svc": extsvcauth.ServiceAccounts, "oauth-svc": extsvcauth.OAuth2Server} + te.r.extSvcProviders = map[string]extsvcauth.AuthProvider{"sa-svc": extsvcauth.ServiceAccounts} - te.oauthReg.On("GetExternalServiceNames", mock.Anything).Return([]string{"oauth-svc"}, nil) // Also return the external service account attached to the OAuth Server - te.saReg.On("GetExternalServiceNames", mock.Anything).Return([]string{"sa-svc", "oauth-svc"}, nil) + te.saReg.On("GetExternalServiceNames", mock.Anything).Return([]string{"sa-svc"}, nil) }, }, { name: "should clean up an orphaned service account", init: func(te *TestEnv) { // Have registered two services one requested a service account, the other requested to be an oauth client - te.r.extSvcProviders = map[string]extsvcauth.AuthProvider{"sa-svc": extsvcauth.ServiceAccounts, "oauth-svc": extsvcauth.OAuth2Server} + te.r.extSvcProviders = map[string]extsvcauth.AuthProvider{"sa-svc": extsvcauth.ServiceAccounts} - te.oauthReg.On("GetExternalServiceNames", mock.Anything).Return([]string{"oauth-svc"}, nil) // Also return the external service account attached to the OAuth Server - te.saReg.On("GetExternalServiceNames", mock.Anything).Return([]string{"sa-svc", "orphaned-sa-svc", "oauth-svc"}, nil) + te.saReg.On("GetExternalServiceNames", mock.Anything).Return([]string{"sa-svc", "orphaned-sa-svc"}, nil) te.saReg.On("RemoveExternalService", mock.Anything, "orphaned-sa-svc").Return(nil) }, }, - { - name: "should clean up an orphaned OAuth Client", - init: func(te *TestEnv) { - // Have registered two services one requested a service account, the other requested to be an oauth client - te.r.extSvcProviders = map[string]extsvcauth.AuthProvider{"sa-svc": extsvcauth.ServiceAccounts, "oauth-svc": extsvcauth.OAuth2Server} - - te.oauthReg.On("GetExternalServiceNames", mock.Anything).Return([]string{"oauth-svc", "orphaned-oauth-svc"}, nil) - // Also return the external service account attached to the OAuth Server - te.saReg.On("GetExternalServiceNames", mock.Anything).Return([]string{"sa-svc", "orphaned-oauth-svc", "oauth-svc"}, nil) - - te.oauthReg.On("RemoveExternalService", mock.Anything, "orphaned-oauth-svc").Return(nil) - }, - }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -93,37 +75,6 @@ func TestRegistry_CleanUpOrphanedExternalServices(t *testing.T) { err := env.r.CleanUpOrphanedExternalServices(context.Background()) require.NoError(t, err) - env.oauthReg.AssertExpectations(t) - env.saReg.AssertExpectations(t) - }) - } -} - -func TestRegistry_GetExternalServiceNames(t *testing.T) { - tests := []struct { - name string - init func(*TestEnv) - want []string - }{ - { - name: "should deduplicate names", - init: func(te *TestEnv) { - te.saReg.On("GetExternalServiceNames", mock.Anything).Return([]string{"sa-svc", "oauth-svc"}, nil) - te.oauthReg.On("GetExternalServiceNames", mock.Anything).Return([]string{"oauth-svc"}, nil) - }, - want: []string{"sa-svc", "oauth-svc"}, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - env := setupTestEnv(t) - tt.init(env) - - names, err := env.r.GetExternalServiceNames(context.Background()) - require.NoError(t, err) - require.ElementsMatch(t, tt.want, names) - - env.oauthReg.AssertExpectations(t) env.saReg.AssertExpectations(t) }) } diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 9adfa778b3..6d768fbfec 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -443,13 +443,6 @@ var ( Owner: grafanaAsCodeSquad, HideFromAdminPage: true, }, - { - Name: "externalServiceAuth", - Description: "Starts an OAuth2 authentication provider for external services", - Stage: FeatureStageExperimental, - RequiresDevMode: true, - Owner: identityAccessTeam, - }, { Name: "refactorVariablesTimeRange", Description: "Refactor time range variables flow to reduce number of API calls made when query variables are chained", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 9a978ec61d..bbf88310ec 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -58,7 +58,6 @@ alertStateHistoryLokiPrimary,experimental,@grafana/alerting-squad,false,false,fa alertStateHistoryLokiOnly,experimental,@grafana/alerting-squad,false,false,false unifiedRequestLog,experimental,@grafana/backend-platform,false,false,false renderAuthJWT,preview,@grafana/grafana-as-code,false,false,false -externalServiceAuth,experimental,@grafana/identity-access-team,true,false,false refactorVariablesTimeRange,preview,@grafana/dashboards-squad,false,false,false enableElasticsearchBackendQuerying,GA,@grafana/observability-logs,false,false,false faroDatasourceSelector,preview,@grafana/app-o11y,false,false,true diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index ada2955c68..17fa7f7610 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -243,10 +243,6 @@ const ( // Uses JWT-based auth for rendering instead of relying on remote cache FlagRenderAuthJWT = "renderAuthJWT" - // FlagExternalServiceAuth - // Starts an OAuth2 authentication provider for external services - FlagExternalServiceAuth = "externalServiceAuth" - // FlagRefactorVariablesTimeRange // Refactor time range variables flow to reduce number of API calls made when query variables are chained FlagRefactorVariablesTimeRange = "refactorVariablesTimeRange" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index e0bca47266..23a5c2204a 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -1569,7 +1569,8 @@ "metadata": { "name": "externalServiceAuth", "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "creationTimestamp": "2024-02-16T18:36:28Z", + "deletionTimestamp": "2024-02-21T10:10:41Z" }, "spec": { "description": "Starts an OAuth2 authentication provider for external services", diff --git a/pkg/services/pluginsintegration/loader/loader_test.go b/pkg/services/pluginsintegration/loader/loader_test.go index 1b3b75b53b..9d33476df4 100644 --- a/pkg/services/pluginsintegration/loader/loader_test.go +++ b/pkg/services/pluginsintegration/loader/loader_test.go @@ -508,116 +508,11 @@ func TestLoader_Load(t *testing.T) { } func TestLoader_Load_ExternalRegistration(t *testing.T) { - boolPtr := func(b bool) *bool { return &b } stringPtr := func(s string) *string { return &s } - t.Run("Load a plugin with oauth client registration", func(t *testing.T) { - cfg := &config.Cfg{ - Features: featuremgmt.WithFeatures(featuremgmt.FlagExternalServiceAuth), - PluginsAllowUnsigned: []string{"grafana-test-datasource"}, - AWSAssumeRoleEnabled: true, - } - pluginPaths := []string{filepath.Join(testDataDir(t), "oauth-external-registration")} - expected := []*plugins.Plugin{ - {JSONData: plugins.JSONData{ - ID: "grafana-test-datasource", - Type: plugins.TypeDataSource, - Name: "Test", - Backend: true, - Executable: "gpx_test_datasource", - Info: plugins.Info{ - Author: plugins.InfoLink{ - Name: "Grafana Labs", - URL: "https://grafana.com", - }, - Version: "1.0.0", - Logos: plugins.Logos{ - Small: "public/plugins/grafana-test-datasource/img/ds.svg", - Large: "public/plugins/grafana-test-datasource/img/ds.svg", - }, - Updated: "2023-08-03", - Screenshots: []plugins.Screenshots{}, - }, - Dependencies: plugins.Dependencies{ - GrafanaVersion: "*", - Plugins: []plugins.Dependency{}, - }, - IAM: &plugindef.IAM{ - Impersonation: &plugindef.Impersonation{ - Groups: boolPtr(true), - Permissions: []plugindef.Permission{ - { - Action: "read", - Scope: stringPtr("datasource"), - }, - }, - }, - Permissions: []plugindef.Permission{ - { - Action: "read", - Scope: stringPtr("datasource"), - }, - }, - }, - }, - FS: mustNewStaticFSForTests(t, pluginPaths[0]), - Class: plugins.ClassExternal, - Signature: plugins.SignatureStatusUnsigned, - Module: "public/plugins/grafana-test-datasource/module.js", - BaseURL: "public/plugins/grafana-test-datasource", - ExternalService: &auth.ExternalService{ - ClientID: "client-id", - ClientSecret: "secretz", - PrivateKey: "priv@t3", - }, - }, - } - - backendFactoryProvider := fakes.NewFakeBackendProcessProvider() - backendFactoryProvider.BackendFactoryFunc = func(ctx context.Context, plugin *plugins.Plugin) backendplugin.PluginFactoryFunc { - return func(pluginID string, logger log.Logger, env func() []string) (backendplugin.Plugin, error) { - require.Equal(t, "grafana-test-datasource", pluginID) - require.Equal(t, []string{ - "GF_VERSION=", "GF_EDITION=", "GF_ENTERPRISE_LICENSE_PATH=", - "GF_ENTERPRISE_APP_URL=", "GF_ENTERPRISE_LICENSE_TEXT=", "GF_APP_URL=", - "GF_PLUGIN_APP_CLIENT_ID=client-id", "GF_PLUGIN_APP_CLIENT_SECRET=secretz", - "GF_PLUGIN_APP_PRIVATE_KEY=priv@t3", "GF_INSTANCE_FEATURE_TOGGLES_ENABLE=externalServiceAuth", - }, env()) - return &fakes.FakeBackendPlugin{}, nil - } - } - - l := newLoaderWithOpts(t, cfg, loaderDepOpts{ - authServiceRegistry: &fakes.FakeAuthService{ - Result: &auth.ExternalService{ - ClientID: "client-id", - ClientSecret: "secretz", - PrivateKey: "priv@t3", - }, - }, - backendFactoryProvider: backendFactoryProvider, - }) - got, err := l.Load(context.Background(), &fakes.FakePluginSource{ - PluginClassFunc: func(ctx context.Context) plugins.Class { - return plugins.ClassExternal - }, - PluginURIsFunc: func(ctx context.Context) []string { - return pluginPaths - }, - DefaultSignatureFunc: func(ctx context.Context) (plugins.Signature, bool) { - return plugins.Signature{}, false - }, - }) - - require.NoError(t, err) - if !cmp.Equal(got, expected, compareOpts...) { - t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, expected, compareOpts...)) - } - }) - t.Run("Load a plugin with service account registration", func(t *testing.T) { cfg := &config.Cfg{ - Features: featuremgmt.WithFeatures(featuremgmt.FlagExternalServiceAuth), + Features: featuremgmt.WithFeatures(featuremgmt.FlagExternalServiceAccounts), PluginsAllowUnsigned: []string{"grafana-test-datasource"}, AWSAssumeRoleEnabled: true, } @@ -676,7 +571,7 @@ func TestLoader_Load_ExternalRegistration(t *testing.T) { "GF_VERSION=", "GF_EDITION=", "GF_ENTERPRISE_LICENSE_PATH=", "GF_ENTERPRISE_APP_URL=", "GF_ENTERPRISE_LICENSE_TEXT=", "GF_APP_URL=", "GF_PLUGIN_APP_CLIENT_ID=client-id", "GF_PLUGIN_APP_CLIENT_SECRET=secretz", - "GF_INSTANCE_FEATURE_TOGGLES_ENABLE=externalServiceAuth", + "GF_INSTANCE_FEATURE_TOGGLES_ENABLE=externalServiceAccounts", }, env()) return &fakes.FakeBackendPlugin{}, nil } diff --git a/pkg/services/pluginsintegration/serviceregistration/serviceregistration.go b/pkg/services/pluginsintegration/serviceregistration/serviceregistration.go index b57408a170..08b1661500 100644 --- a/pkg/services/pluginsintegration/serviceregistration/serviceregistration.go +++ b/pkg/services/pluginsintegration/serviceregistration/serviceregistration.go @@ -23,7 +23,7 @@ type Service struct { func ProvideService(cfg *config.Cfg, reg extsvcauth.ExternalServiceRegistry, settingsSvc pluginsettings.Service) *Service { s := &Service{ - featureEnabled: cfg.Features.IsEnabledGlobally(featuremgmt.FlagExternalServiceAuth) || cfg.Features.IsEnabledGlobally(featuremgmt.FlagExternalServiceAccounts), + featureEnabled: cfg.Features.IsEnabledGlobally(featuremgmt.FlagExternalServiceAccounts), log: log.New("plugins.external.registration"), reg: reg, settingsSvc: settingsSvc, @@ -58,18 +58,6 @@ func (s *Service) RegisterExternalService(ctx context.Context, pluginID string, enabled = (settings != nil) && settings.Enabled } - - impersonation := extsvcauth.ImpersonationCfg{} - if svc.Impersonation != nil { - impersonation.Permissions = toAccessControlPermissions(svc.Impersonation.Permissions) - impersonation.Enabled = enabled - if svc.Impersonation.Groups != nil { - impersonation.Groups = *svc.Impersonation.Groups - } else { - impersonation.Groups = true - } - } - self := extsvcauth.SelfCfg{} self.Enabled = enabled if len(svc.Permissions) > 0 { @@ -77,16 +65,9 @@ func (s *Service) RegisterExternalService(ctx context.Context, pluginID string, } registration := &extsvcauth.ExternalServiceRegistration{ - Name: pluginID, - Impersonation: impersonation, - Self: self, - } - - // Default authProvider now is ServiceAccounts - registration.AuthProvider = extsvcauth.ServiceAccounts - if svc.Impersonation != nil { - registration.AuthProvider = extsvcauth.OAuth2Server - registration.OAuthProviderCfg = &extsvcauth.OAuthProviderCfg{Key: &extsvcauth.KeyOption{Generate: true}} + Name: pluginID, + Self: self, + AuthProvider: extsvcauth.ServiceAccounts, } extSvc, err := s.reg.SaveExternalService(ctx, registration) diff --git a/pkg/services/serviceaccounts/api/api.go b/pkg/services/serviceaccounts/api/api.go index 8d52d7c8d3..e76a5c29ca 100644 --- a/pkg/services/serviceaccounts/api/api.go +++ b/pkg/services/serviceaccounts/api/api.go @@ -48,7 +48,7 @@ func NewServiceAccountsAPI( RouterRegister: routerRegister, log: log.New("serviceaccounts.api"), permissionService: permissionService, - isExternalSAEnabled: features.IsEnabledGlobally(featuremgmt.FlagExternalServiceAccounts) || features.IsEnabledGlobally(featuremgmt.FlagExternalServiceAuth), + isExternalSAEnabled: features.IsEnabledGlobally(featuremgmt.FlagExternalServiceAccounts), } } diff --git a/pkg/services/serviceaccounts/extsvcaccounts/service.go b/pkg/services/serviceaccounts/extsvcaccounts/service.go index 8b71b6e140..3422a0105b 100644 --- a/pkg/services/serviceaccounts/extsvcaccounts/service.go +++ b/pkg/services/serviceaccounts/extsvcaccounts/service.go @@ -45,7 +45,7 @@ func ProvideExtSvcAccountsService(acSvc ac.Service, bus bus.Bus, db db.DB, featu tracer: tracer, } - if features.IsEnabledGlobally(featuremgmt.FlagExternalServiceAccounts) || features.IsEnabledGlobally(featuremgmt.FlagExternalServiceAuth) { + if features.IsEnabledGlobally(featuremgmt.FlagExternalServiceAccounts) { // Register the metrics esa.metrics = newMetrics(reg, saSvc, logger) @@ -133,7 +133,7 @@ func (esa *ExtSvcAccountsService) GetExternalServiceNames(ctx context.Context) ( // SaveExternalService creates, updates or delete a service account (and its token) with the requested permissions. func (esa *ExtSvcAccountsService) SaveExternalService(ctx context.Context, cmd *extsvcauth.ExternalServiceRegistration) (*extsvcauth.ExternalService, error) { // This is double proofing, we should never reach here anyway the flags have already been checked. - if !esa.features.IsEnabled(ctx, featuremgmt.FlagExternalServiceAccounts) && !esa.features.IsEnabled(ctx, featuremgmt.FlagExternalServiceAuth) { + if !esa.features.IsEnabled(ctx, featuremgmt.FlagExternalServiceAccounts) { esa.logger.Warn("This feature is behind a feature flag, please set it if you want to save external services") return nil, nil } @@ -148,10 +148,6 @@ func (esa *ExtSvcAccountsService) SaveExternalService(ctx context.Context, cmd * slug := slugify.Slugify(cmd.Name) - if cmd.Impersonation.Enabled { - esa.logger.Warn("Impersonation setup skipped. It is not possible to impersonate with a service account token.", "service", slug) - } - saID, err := esa.ManageExtSvcAccount(ctx, &sa.ManageExtSvcAccountCmd{ ExtSvcSlug: slug, Enabled: cmd.Self.Enabled, @@ -181,7 +177,7 @@ func (esa *ExtSvcAccountsService) SaveExternalService(ctx context.Context, cmd * func (esa *ExtSvcAccountsService) RemoveExternalService(ctx context.Context, name string) error { // This is double proofing, we should never reach here anyway the flags have already been checked. - if !esa.features.IsEnabled(ctx, featuremgmt.FlagExternalServiceAccounts) && !esa.features.IsEnabled(ctx, featuremgmt.FlagExternalServiceAuth) { + if !esa.features.IsEnabled(ctx, featuremgmt.FlagExternalServiceAccounts) { esa.logger.Warn("This feature is behind a feature flag, please set it if you want to save external services") return nil } @@ -220,7 +216,7 @@ func (esa *ExtSvcAccountsService) RemoveExtSvcAccount(ctx context.Context, orgID // ManageExtSvcAccount creates, updates or deletes the service account associated with an external service func (esa *ExtSvcAccountsService) ManageExtSvcAccount(ctx context.Context, cmd *sa.ManageExtSvcAccountCmd) (int64, error) { // This is double proofing, we should never reach here anyway the flags have already been checked. - if !esa.features.IsEnabled(ctx, featuremgmt.FlagExternalServiceAccounts) && !esa.features.IsEnabled(ctx, featuremgmt.FlagExternalServiceAuth) { + if !esa.features.IsEnabled(ctx, featuremgmt.FlagExternalServiceAccounts) { esa.logger.Warn("This feature is behind a feature flag, please set it if you want to save external services") return 0, nil } diff --git a/pkg/services/serviceaccounts/proxy/service.go b/pkg/services/serviceaccounts/proxy/service.go index beabbd3d29..66d18da945 100644 --- a/pkg/services/serviceaccounts/proxy/service.go +++ b/pkg/services/serviceaccounts/proxy/service.go @@ -38,7 +38,7 @@ func ProvideServiceAccountsProxy( s := &ServiceAccountsProxy{ log: log.New("serviceaccounts.proxy"), proxiedService: proxiedService, - isProxyEnabled: features.IsEnabledGlobally(featuremgmt.FlagExternalServiceAccounts) || features.IsEnabledGlobally(featuremgmt.FlagExternalServiceAuth), + isProxyEnabled: features.IsEnabledGlobally(featuremgmt.FlagExternalServiceAccounts), } serviceaccountsAPI := api.NewServiceAccountsAPI(cfg, s, ac, accesscontrolService, routeRegister, permissionService, features) diff --git a/pkg/services/sqlstore/migrations/migrations.go b/pkg/services/sqlstore/migrations/migrations.go index 95eef328ce..6cbb2f1b69 100644 --- a/pkg/services/sqlstore/migrations/migrations.go +++ b/pkg/services/sqlstore/migrations/migrations.go @@ -5,7 +5,6 @@ import ( "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/sqlstore/migrations/accesscontrol" "github.com/grafana/grafana/pkg/services/sqlstore/migrations/anonservice" - "github.com/grafana/grafana/pkg/services/sqlstore/migrations/oauthserver" "github.com/grafana/grafana/pkg/services/sqlstore/migrations/signingkeys" "github.com/grafana/grafana/pkg/services/sqlstore/migrations/ssosettings" "github.com/grafana/grafana/pkg/services/sqlstore/migrations/ualert" @@ -95,9 +94,6 @@ func (oss *OSSMigrations) AddMigration(mg *Migrator) { AddExternalAlertmanagerToDatasourceMigration(mg) addFolderMigrations(mg) - if oss.features != nil && oss.features.IsEnabledGlobally(featuremgmt.FlagExternalServiceAuth) { - oauthserver.AddMigration(mg) - } anonservice.AddMigration(mg) signingkeys.AddMigration(mg) diff --git a/pkg/services/sqlstore/migrations/oauthserver/migrations.go b/pkg/services/sqlstore/migrations/oauthserver/migrations.go deleted file mode 100644 index b47590e7b2..0000000000 --- a/pkg/services/sqlstore/migrations/oauthserver/migrations.go +++ /dev/null @@ -1,52 +0,0 @@ -package oauthserver - -import "github.com/grafana/grafana/pkg/services/sqlstore/migrator" - -func AddMigration(mg *migrator.Migrator) { - impersonatePermissionsTable := migrator.Table{ - Name: "oauth_impersonate_permission", - Columns: []*migrator.Column{ - {Name: "id", Type: migrator.DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, - {Name: "client_id", Type: migrator.DB_Varchar, Length: 190, Nullable: false}, - {Name: "action", Type: migrator.DB_Varchar, Length: 190, Nullable: false}, - {Name: "scope", Type: migrator.DB_Varchar, Length: 190, Nullable: true}, - }, - Indices: []*migrator.Index{ - {Cols: []string{"client_id", "action", "scope"}, Type: migrator.UniqueIndex}, - }, - } - - clientTable := migrator.Table{ - Name: "oauth_client", - Columns: []*migrator.Column{ - {Name: "id", Type: migrator.DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, - {Name: "name", Type: migrator.DB_Varchar, Length: 190, Nullable: true}, - {Name: "client_id", Type: migrator.DB_Varchar, Length: 190, Nullable: false}, - {Name: "secret", Type: migrator.DB_Varchar, Length: 190, Nullable: false}, - {Name: "grant_types", Type: migrator.DB_Text, Nullable: true}, - {Name: "audiences", Type: migrator.DB_Varchar, Length: 190, Nullable: true}, - {Name: "service_account_id", Type: migrator.DB_BigInt, Nullable: true}, - {Name: "public_pem", Type: migrator.DB_Text, Nullable: true}, - {Name: "redirect_uri", Type: migrator.DB_Varchar, Length: 190, Nullable: true}, - }, - Indices: []*migrator.Index{ - {Cols: []string{"client_id"}, Type: migrator.UniqueIndex}, - {Cols: []string{"client_id", "service_account_id"}, Type: migrator.UniqueIndex}, - {Cols: []string{"name"}, Type: migrator.UniqueIndex}, - }, - } - - // Impersonate Permission - mg.AddMigration("create impersonate permissions table", migrator.NewAddTableMigration(impersonatePermissionsTable)) - - //------- indexes ------------------ - mg.AddMigration("add unique index client_id action scope", migrator.NewAddIndexMigration(impersonatePermissionsTable, impersonatePermissionsTable.Indices[0])) - - // Client - mg.AddMigration("create client table", migrator.NewAddTableMigration(clientTable)) - - //------- indexes ------------------ - mg.AddMigration("add unique index client_id", migrator.NewAddIndexMigration(clientTable, clientTable.Indices[0])) - mg.AddMigration("add unique index client_id service_account_id", migrator.NewAddIndexMigration(clientTable, clientTable.Indices[1])) - mg.AddMigration("add unique index name", migrator.NewAddIndexMigration(clientTable, clientTable.Indices[2])) -} diff --git a/public/app/features/serviceaccounts/ServiceAccountsListPage.tsx b/public/app/features/serviceaccounts/ServiceAccountsListPage.tsx index 46737866d5..38096370c7 100644 --- a/public/app/features/serviceaccounts/ServiceAccountsListPage.tsx +++ b/public/app/features/serviceaccounts/ServiceAccountsListPage.tsx @@ -62,7 +62,7 @@ const availableFilters = [ { label: 'Disabled', value: ServiceAccountStateFilter.Disabled }, ]; -if (config.featureToggles.externalServiceAccounts || config.featureToggles.externalServiceAuth) { +if (config.featureToggles.externalServiceAccounts) { availableFilters.push({ label: 'Managed', value: ServiceAccountStateFilter.External }); } diff --git a/scripts/modowners/README.md b/scripts/modowners/README.md index 6edd78b067..06640dab31 100644 --- a/scripts/modowners/README.md +++ b/scripts/modowners/README.md @@ -79,7 +79,6 @@ golang.org/x/oauth2@v0.8.0 github.com/drone/drone-cli@v1.6.1 github.com/google/go-github/v45@v45.2.0 github.com/Masterminds/semver/v3@v3.1.1 -github.com/ory/fosite@v0.44.1-0.20230317114349-45a6785cc54f gopkg.in/square/go-jose.v2@v2.6.0 filippo.io/age@v1.1.1 github.com/docker/docker@v23.0.4+incompatible From 2fa4ac2a739dbaa228a6928ebe2ee2580710d7aa Mon Sep 17 00:00:00 2001 From: Fabrizio <135109076+fabrizio-grafana@users.noreply.github.com> Date: Mon, 26 Feb 2024 11:39:26 +0100 Subject: [PATCH 0162/1406] Tempo: Remove duplicated code (#81476) --- .betterer.results | 6 - .../src/TemporaryAlert.tsx | 14 +- .../tempo/NativeSearch/NativeSearch.tsx | 18 +- .../NativeSearch/TagsField/TagsField.tsx | 78 ++++----- .../SearchTraceQLEditor/SearchField.test.tsx | 5 +- .../tempo/SearchTraceQLEditor/SearchField.tsx | 154 ++++++++++-------- .../SearchTraceQLEditor/TagsInput.test.tsx | 6 +- .../tempo/SearchTraceQLEditor/TagsInput.tsx | 2 +- .../TraceQLSearch.test.tsx | 2 +- .../SearchTraceQLEditor/TraceQLSearch.tsx | 11 +- .../datasource/tempo/ServiceGraphSection.tsx | 2 +- .../actions/appNotification.ts | 128 --------------- .../_importedDependencies/actions/index.ts | 5 - .../actions/types/appNotifications.ts | 36 ---- .../actions/types/index.ts | 1 - .../AdHocFilter/types.ts} | 0 .../core/appNotification.ts | 46 ------ .../_importedDependencies/core/errors.ts | 21 --- .../tempo/_importedDependencies/store.ts | 27 --- .../app/plugins/datasource/tempo/package.json | 2 - .../tempo/traceql/TraceQLEditor.tsx | 12 +- .../tempo/traceql/autocomplete.test.ts | 2 +- .../datasource/tempo/traceql/autocomplete.ts | 15 +- yarn.lock | 2 - 24 files changed, 173 insertions(+), 422 deletions(-) delete mode 100644 public/app/plugins/datasource/tempo/_importedDependencies/actions/appNotification.ts delete mode 100644 public/app/plugins/datasource/tempo/_importedDependencies/actions/index.ts delete mode 100644 public/app/plugins/datasource/tempo/_importedDependencies/actions/types/appNotifications.ts delete mode 100644 public/app/plugins/datasource/tempo/_importedDependencies/actions/types/index.ts rename public/app/plugins/datasource/tempo/_importedDependencies/{types/index.ts => components/AdHocFilter/types.ts} (100%) delete mode 100644 public/app/plugins/datasource/tempo/_importedDependencies/core/appNotification.ts delete mode 100644 public/app/plugins/datasource/tempo/_importedDependencies/core/errors.ts delete mode 100644 public/app/plugins/datasource/tempo/_importedDependencies/store.ts diff --git a/.betterer.results b/.betterer.results index d305a5130a..7436903ef0 100644 --- a/.betterer.results +++ b/.betterer.results @@ -5638,12 +5638,6 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "1"], [0, 0, 0, "Unexpected any. Specify a different type.", "2"] ], - "public/app/plugins/datasource/tempo/_importedDependencies/store.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Do not use any type assertions.", "2"], - [0, 0, 0, "Unexpected any. Specify a different type.", "3"] - ], "public/app/plugins/datasource/tempo/_importedDependencies/test/helpers/createFetchResponse.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Do not use any type assertions.", "1"] diff --git a/packages/grafana-o11y-ds-frontend/src/TemporaryAlert.tsx b/packages/grafana-o11y-ds-frontend/src/TemporaryAlert.tsx index cabc3e3c49..8a7003a576 100644 --- a/packages/grafana-o11y-ds-frontend/src/TemporaryAlert.tsx +++ b/packages/grafana-o11y-ds-frontend/src/TemporaryAlert.tsx @@ -58,5 +58,17 @@ export const TemporaryAlert = (props: AlertProps) => { } }, [props.severity, props.text]); - return <>{visible && }; + return ( + <> + {visible && ( + setVisible(false)} + severity={props.severity} + title={props.text} + /> + )} + + ); }; diff --git a/public/app/plugins/datasource/tempo/NativeSearch/NativeSearch.tsx b/public/app/plugins/datasource/tempo/NativeSearch/NativeSearch.tsx index ae78da4318..1653cc04de 100644 --- a/public/app/plugins/datasource/tempo/NativeSearch/NativeSearch.tsx +++ b/public/app/plugins/datasource/tempo/NativeSearch/NativeSearch.tsx @@ -2,12 +2,10 @@ import { css } from '@emotion/css'; import React, { useCallback, useState, useEffect, useMemo } from 'react'; import { GrafanaTheme2, isValidGoDuration, SelectableValue, toOption } from '@grafana/data'; +import { TemporaryAlert } from '@grafana/o11y-ds-frontend'; import { FetchError, getTemplateSrv, isFetchError, TemplateSrv } from '@grafana/runtime'; import { InlineFieldRow, InlineField, Input, Alert, useStyles2, fuzzyMatch, Select } from '@grafana/ui'; -import { notifyApp } from '../_importedDependencies/actions/appNotification'; -import { createErrorNotification } from '../_importedDependencies/core/appNotification'; -import { dispatch } from '../_importedDependencies/store'; import { DEFAULT_LIMIT, TempoDatasource } from '../datasource'; import TempoLanguageProvider from '../language_provider'; import { TempoQuery } from '../types'; @@ -26,6 +24,7 @@ const durationPlaceholder = 'e.g. 1.2s, 100ms'; const NativeSearch = ({ datasource, query, onChange, onBlur, onRunQuery }: Props) => { const styles = useStyles2(getStyles); + const [alertText, setAlertText] = useState(); const languageProvider = useMemo(() => new TempoLanguageProvider(datasource), [datasource]); const [serviceOptions, setServiceOptions] = useState>>(); const [spanOptions, setSpanOptions] = useState>>(); @@ -47,19 +46,21 @@ const NativeSearch = ({ datasource, query, onChange, onBlur, onRunQuery }: Props try { const options = await languageProvider.getOptionsV1(lpName); const filteredOptions = options.filter((item) => (item.value ? fuzzyMatch(item.value, query).found : false)); + setAlertText(undefined); + setError(null); return filteredOptions; } catch (error) { if (isFetchError(error) && error?.status === 404) { setError(error); } else if (error instanceof Error) { - dispatch(notifyApp(createErrorNotification('Error', error))); + setAlertText(`Error: ${error.message}`); } return []; } finally { setIsLoading((prevValue) => ({ ...prevValue, [name]: false })); } }, - [languageProvider] + [languageProvider, setAlertText] ); useEffect(() => { @@ -74,17 +75,19 @@ const NativeSearch = ({ datasource, query, onChange, onBlur, onRunQuery }: Props spans.push(toOption(query.spanName)); } setSpanOptions(spans); + setAlertText(undefined); + setError(null); } catch (error) { // Display message if Tempo is connected but search 404's if (isFetchError(error) && error?.status === 404) { setError(error); } else if (error instanceof Error) { - dispatch(notifyApp(createErrorNotification('Error', error))); + setAlertText(`Error: ${error.message}`); } } }; fetchOptions(); - }, [languageProvider, loadOptions, query.serviceName, query.spanName]); + }, [languageProvider, loadOptions, query.serviceName, query.spanName, setAlertText]); const onKeyDown = (keyEvent: React.KeyboardEvent) => { if (keyEvent.key === 'Enter' && (keyEvent.shiftKey || keyEvent.ctrlKey)) { @@ -255,6 +258,7 @@ const NativeSearch = ({ datasource, query, onChange, onBlur, onRunQuery }: Props configure it in the datasource settings. ) : null} + {alertText && } ); }; diff --git a/public/app/plugins/datasource/tempo/NativeSearch/TagsField/TagsField.tsx b/public/app/plugins/datasource/tempo/NativeSearch/TagsField/TagsField.tsx index 3869c3fcb3..9acc12b289 100644 --- a/public/app/plugins/datasource/tempo/NativeSearch/TagsField/TagsField.tsx +++ b/public/app/plugins/datasource/tempo/NativeSearch/TagsField/TagsField.tsx @@ -1,12 +1,10 @@ import { css } from '@emotion/css'; -import React, { useEffect, useRef } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; +import { TemporaryAlert } from '@grafana/o11y-ds-frontend'; import { CodeEditor, Monaco, monacoTypes, useTheme2 } from '@grafana/ui'; -import { notifyApp } from '../../_importedDependencies/actions/appNotification'; -import { createErrorNotification } from '../../_importedDependencies/core/appNotification'; -import { dispatch } from '../../_importedDependencies/store'; import { TempoDatasource } from '../../datasource'; import { CompletionProvider } from './autocomplete'; @@ -21,40 +19,44 @@ interface Props { } export function TagsField(props: Props) { + const [alertText, setAlertText] = useState(); const { onChange, onBlur, placeholder } = props; - const setupAutocompleteFn = useAutocomplete(props.datasource); + const setupAutocompleteFn = useAutocomplete(props.datasource, setAlertText); const theme = useTheme2(); const styles = getStyles(theme, placeholder); return ( - { - setupAutocompleteFn(editor, monaco); - setupPlaceholder(editor, monaco, styles); - setupAutoSize(editor); - }} - /> + <> + { + setupAutocompleteFn(editor, monaco); + setupPlaceholder(editor, monaco, styles); + setupAutoSize(editor); + }} + /> + {alertText && } + ); } @@ -103,9 +105,10 @@ function setupAutoSize(editor: monacoTypes.editor.IStandaloneCodeEditor) { /** * Hook that returns function that will set up monaco autocomplete for the label selector - * @param datasource + * @param datasource the Tempo datasource instance + * @param setAlertText setter for the alert text */ -function useAutocomplete(datasource: TempoDatasource) { +function useAutocomplete(datasource: TempoDatasource, setAlertText: (text?: string) => void) { // We need the provider ref so we can pass it the label/values data later. This is because we run the call for the // values here but there is additional setup needed for the provider later on. We could run the getSeries() in the // returned function but that is run after the monaco is mounted so would delay the request a bit when it does not @@ -118,14 +121,15 @@ function useAutocomplete(datasource: TempoDatasource) { const fetchTags = async () => { try { await datasource.languageProvider.start(); + setAlertText(undefined); } catch (error) { if (error instanceof Error) { - dispatch(notifyApp(createErrorNotification('Error', error))); + setAlertText(`Error: ${error.message}`); } } }; fetchTags(); - }, [datasource]); + }, [datasource, setAlertText]); const autocompleteDisposeFun = useRef<(() => void) | null>(null); useEffect(() => { diff --git a/public/app/plugins/datasource/tempo/SearchTraceQLEditor/SearchField.test.tsx b/public/app/plugins/datasource/tempo/SearchTraceQLEditor/SearchField.test.tsx index e81d0208ca..42a4935371 100644 --- a/public/app/plugins/datasource/tempo/SearchTraceQLEditor/SearchField.test.tsx +++ b/public/app/plugins/datasource/tempo/SearchTraceQLEditor/SearchField.test.tsx @@ -3,7 +3,6 @@ import userEvent from '@testing-library/user-event'; import React from 'react'; import { LanguageProvider } from '@grafana/data'; -import { FetchError } from '@grafana/runtime'; import { TraceqlFilter, TraceqlSearchScope } from '../dataquery.gen'; import { TempoDatasource } from '../datasource'; @@ -290,9 +289,7 @@ const renderSearchField = ( datasource={datasource} updateFilter={updateFilter} filter={filter} - setError={function (error: FetchError): void { - throw error; - }} + setError={() => {}} tags={tags || []} hideTag={hideTag} query={'{}'} diff --git a/public/app/plugins/datasource/tempo/SearchTraceQLEditor/SearchField.tsx b/public/app/plugins/datasource/tempo/SearchTraceQLEditor/SearchField.tsx index 68cc5a2676..eaaeda3222 100644 --- a/public/app/plugins/datasource/tempo/SearchTraceQLEditor/SearchField.tsx +++ b/public/app/plugins/datasource/tempo/SearchTraceQLEditor/SearchField.tsx @@ -4,12 +4,10 @@ import React, { useState, useEffect, useMemo } from 'react'; import useAsync from 'react-use/lib/useAsync'; import { SelectableValue } from '@grafana/data'; +import { TemporaryAlert } from '@grafana/o11y-ds-frontend'; import { FetchError, getTemplateSrv, isFetchError } from '@grafana/runtime'; import { Select, HorizontalGroup, useStyles2 } from '@grafana/ui'; -import { notifyApp } from '../_importedDependencies/actions/appNotification'; -import { createErrorNotification } from '../_importedDependencies/core/appNotification'; -import { dispatch } from '../_importedDependencies/store'; import { TraceqlFilter, TraceqlSearchScope } from '../dataquery.gen'; import { TempoDatasource } from '../datasource'; import { operators as allOperators, stringOperators, numberOperators, keywordOperators } from '../traceql/traceql'; @@ -26,7 +24,8 @@ interface Props { filter: TraceqlFilter; datasource: TempoDatasource; updateFilter: (f: TraceqlFilter) => void; - setError: (error: FetchError) => void; + deleteFilter?: (f: TraceqlFilter) => void; + setError: (error: FetchError | null) => void; isTagsLoading?: boolean; tags: string[]; hideScope?: boolean; @@ -51,6 +50,7 @@ const SearchField = ({ allowCustomValue = true, }: Props) => { const styles = useStyles2(getStyles); + const [alertText, setAlertText] = useState(); const scopedTag = useMemo(() => filterScopedTag(filter), [filter]); // We automatically change the operator to the regex op when users select 2 or more values // However, they expect this to be automatically rolled back to the previous operator once @@ -60,13 +60,16 @@ const SearchField = ({ const updateOptions = async () => { try { - return filter.tag ? await datasource.languageProvider.getOptionsV2(scopedTag, query) : []; + const result = filter.tag ? await datasource.languageProvider.getOptionsV2(scopedTag, query) : []; + setAlertText(undefined); + setError(null); + return result; } catch (error) { // Display message if Tempo is connected but search 404's if (isFetchError(error) && error?.status === 404) { setError(error); } else if (error instanceof Error) { - dispatch(notifyApp(createErrorNotification('Error', error))); + setAlertText(`Error: ${error.message}`); } } return []; @@ -135,78 +138,85 @@ const SearchField = ({ }; return ( - - {!hideScope && ( + <> + + {!hideScope && ( + ({ + label: t, + value: t, + })) + )} + value={filter.tag} + onChange={(v) => { + updateFilter({ ...filter, tag: v?.value, value: [] }); + }} + placeholder="Select tag" + isClearable + aria-label={`select ${filter.id} tag`} + allowCustomValue={true} + /> + )} ({ - label: t, - value: t, - })) - )} - value={filter.tag} - onChange={(v) => { - updateFilter({ ...filter, tag: v?.value, value: [] }); - }} - placeholder="Select tag" - isClearable - aria-label={`select ${filter.id} tag`} + isClearable={false} + aria-label={`select ${filter.id} operator`} allowCustomValue={true} + width={8} /> - )} - { - if (Array.isArray(val)) { - updateFilter({ ...filter, value: val.map((v) => v.value), valueType: val[0]?.type || uniqueOptionType }); - } else { - updateFilter({ ...filter, value: val?.value, valueType: val?.type || uniqueOptionType }); - } - }} - placeholder="Select value" - isClearable={true} - aria-label={`select ${filter.id} value`} - allowCustomValue={allowCustomValue} - isMulti={isMulti} - allowCreateWhileLoading - /> - )} - + {!hideValue && ( + s.value === scope)} onChange={onScopeChanged} /> +
-
+ {scope && (
- - s.value === namespace) ?? + (namespace ? { label: namespace, value: namespace } : undefined) + } + onChange={onNamespaceChanged} + allowCustomValue={true} + backspaceRemovesValue={true} + isClearable={true} + />
+ )} - {scope && ( -
- - -
- )} -
- - ); - } + {scope && namespace && ( +
+ + onPanelConfigChange('maxPerRow', value.value)} - // /> - // ); - // }, - // }) - // ) - // ); + return ( + panelManager.setState({ repeatDirection: value })} + /> + ); + }, + }) + ) + .addItem( + new OptionsPaneItemDescriptor({ + title: 'Max per row', + showIf: () => Boolean(panelManager.state.repeat && panelManager.state.repeatDirection === 'h'), + render: function renderOption() { + const maxPerRowOptions = [2, 3, 4, 6, 8, 12].map((value) => ({ label: value.toString(), value })); + return ( + ; }; + +interface Props2 { + panelManager: VizPanelManager; + id?: string; + onChange: (name?: string) => void; +} + +export const RepeatRowSelect2 = ({ panelManager, id, onChange }: Props2) => { + const { panel, repeat } = panelManager.useState(); + const sceneVars = useMemo(() => sceneGraph.getVariables(panel), [panel]); + const variables = sceneVars.useState().variables; + + const variableOptions = useMemo(() => { + const options: Array> = variables.map((item) => ({ + label: item.state.name, + value: item.state.name, + })); + + if (options.length === 0) { + options.unshift({ + label: 'No template variables found', + value: null, + }); + } + + options.unshift({ + label: 'Disable repeating', + value: null, + }); + + return options; + }, [variables]); + + const onSelectChange = useCallback((option: SelectableValue) => onChange(option.value!), [onChange]); + + return ` -- There is a lot of overlap between `RuleActionButtons` and `RuleDetailsActionButtons`. As these components contain a lot of logic it would be nice to extract that logic into hoooks +- There is a lot of overlap between `RuleActionButtons` and `RuleDetailsActionButtons`. As these components contain a lot of logic it would be nice to extract that logic into hooks - Create a shared timings form that can be used in both `EditDefaultPolicyForm.tsx` and `EditNotificationPolicyForm.tsx` ## Testing diff --git a/public/app/features/plugins/sandbox/README.md b/public/app/features/plugins/sandbox/README.md index d2fb03e068..cff6aa0236 100644 --- a/public/app/features/plugins/sandbox/README.md +++ b/public/app/features/plugins/sandbox/README.md @@ -22,7 +22,7 @@ When a plugin is marked for loading, grafana decides if it should load it in the - If a plugin is marked to load in a sandbox, first the source code is downloaded with `fetch`, then pre-processed to adjust sourceMaps and CDNs and finally evaluated inside a new near-membrane virtual environment. In either case, Grafana receives a pluginExport object that later uses to initialize plugins. For Grafana's core, this -pluginExport is idential in functionality and properties regardless of the loading method. +pluginExport is identical in functionality and properties regardless of the loading method. # Plugin execution @@ -64,7 +64,7 @@ those plugins that use web workers. Performance is still under tests when this w Distortions is the mechanism to intercept calls from the child realm code to JS APIS and DOM APIs. e.g: `Array.map` or `document.getElement`. -Distortions allow to replace the function that will execute inside the child realm wnen the function is invoked. +Distortions allow to replace the function that will execute inside the child realm when the function is invoked. Distortions also allow to intercept the exchange of objects between the child realm and the incubator realm, we can, for example, inspect all DOM elements access and generally speaking all objects that go to the child realm. From 9709ac8b84114043a954b3ee15444c61ebad6208 Mon Sep 17 00:00:00 2001 From: Misi Date: Mon, 26 Feb 2024 20:59:49 +0100 Subject: [PATCH 0195/1406] Auth: Revert provider list change (#83435) * Load auth.grafananet as the last provider * skip test --- pkg/services/ssosettings/strategies/oauth_strategy.go | 2 +- pkg/services/ssosettings/strategies/oauth_strategy_test.go | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/services/ssosettings/strategies/oauth_strategy.go b/pkg/services/ssosettings/strategies/oauth_strategy.go index 22e3b2e4ba..215d0a5cf0 100644 --- a/pkg/services/ssosettings/strategies/oauth_strategy.go +++ b/pkg/services/ssosettings/strategies/oauth_strategy.go @@ -48,7 +48,7 @@ func (s *OAuthStrategy) GetProviderConfig(_ context.Context, provider string) (m } func (s *OAuthStrategy) loadAllSettings() { - allProviders := append([]string{social.GrafanaNetProviderName}, ssosettings.AllOAuthProviders...) + allProviders := append(ssosettings.AllOAuthProviders, social.GrafanaNetProviderName) for _, provider := range allProviders { settings := s.loadSettingsForProvider(provider) if provider == social.GrafanaNetProviderName { diff --git a/pkg/services/ssosettings/strategies/oauth_strategy_test.go b/pkg/services/ssosettings/strategies/oauth_strategy_test.go index 172d143bb8..c5e4cc28be 100644 --- a/pkg/services/ssosettings/strategies/oauth_strategy_test.go +++ b/pkg/services/ssosettings/strategies/oauth_strategy_test.go @@ -166,6 +166,7 @@ func TestGetProviderConfig_ExtraFields(t *testing.T) { }) t.Run("grafana_com", func(t *testing.T) { + t.Skip("Skipping to revert an issue.") result, err := strategy.GetProviderConfig(context.Background(), "grafana_com") require.NoError(t, err) From 58b0323bbb9dd100d3265e1010b4aa341dbd12cb Mon Sep 17 00:00:00 2001 From: Fabrizio <135109076+fabrizio-grafana@users.noreply.github.com> Date: Mon, 26 Feb 2024 21:03:13 +0100 Subject: [PATCH 0196/1406] Fix linting of docs file (#83441) --- .../azure-monitor/query-editor/index.md | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/sources/datasources/azure-monitor/query-editor/index.md b/docs/sources/datasources/azure-monitor/query-editor/index.md index ef584f4625..da4ab7cc9e 100644 --- a/docs/sources/datasources/azure-monitor/query-editor/index.md +++ b/docs/sources/datasources/azure-monitor/query-editor/index.md @@ -86,17 +86,17 @@ For example: - `Blob Type: {{ blobtype }}` becomes `Blob Type: PageBlob`, `Blob Type: BlockBlob` - `{{ resourcegroup }} - {{ resourcename }}` becomes `production - web_server` -| Alias pattern | Description | -| ----------------------------- | ------------------------------------------------------------------------------------------------------ | -| `{{ subscriptionid }}` | Replaced with the subscription ID. | -| `{{ subscription }}` | Replaced with the subscription name. | -| `{{ resourcegroup }}` | Replaced with the the resource group. | -| `{{ namespace }}` | Replaced with the resource type or namespace, such as `Microsoft.Compute/virtualMachines`. | -| `{{ resourcename }}` | Replaced with the resource name. | -| `{{ metric }}` | Replaced with the metric name, such as "Percentage CPU". | +| Alias pattern | Description | +| ------------------------------ | ------------------------------------------------------------------------------------------------------ | +| `{{ subscriptionid }}` | Replaced with the subscription ID. | +| `{{ subscription }}` | Replaced with the subscription name. | +| `{{ resourcegroup }}` | Replaced with the the resource group. | +| `{{ namespace }}` | Replaced with the resource type or namespace, such as `Microsoft.Compute/virtualMachines`. | +| `{{ resourcename }}` | Replaced with the resource name. | +| `{{ metric }}` | Replaced with the metric name, such as "Percentage CPU". | | _`{{ arbitraryDimensionID }}`_ | Replaced with the value of the specified dimension. For example, `{{ blobtype }}` becomes `BlockBlob`. | -| `{{ dimensionname }}` | _(Legacy for backward compatibility)_ Replaced with the name of the first dimension. | -| `{{ dimensionvalue }}` | _(Legacy for backward compatibility)_ Replaced with the value of the first dimension. | +| `{{ dimensionname }}` | _(Legacy for backward compatibility)_ Replaced with the name of the first dimension. | +| `{{ dimensionvalue }}` | _(Legacy for backward compatibility)_ Replaced with the value of the first dimension. | ### Filter using dimensions From 0bfe9db668736ab9950f477562fe4fe0edd24649 Mon Sep 17 00:00:00 2001 From: Isabella Siu Date: Mon, 26 Feb 2024 15:59:54 -0500 Subject: [PATCH 0197/1406] CloudWatch: Move SessionCache onto the instance (#83278) --- pkg/tsdb/cloudwatch/annotation_query_test.go | 4 +- pkg/tsdb/cloudwatch/cloudwatch.go | 76 ++++++++++--------- .../cloudwatch/cloudwatch_integration_test.go | 16 ++-- pkg/tsdb/cloudwatch/cloudwatch_test.go | 49 +++++++----- pkg/tsdb/cloudwatch/log_actions_test.go | 52 ++++++------- pkg/tsdb/cloudwatch/log_sync_query_test.go | 42 +++++----- .../metric_data_input_builder_test.go | 2 +- .../metric_data_query_builder_test.go | 22 +++--- pkg/tsdb/cloudwatch/metric_find_query_test.go | 12 +-- pkg/tsdb/cloudwatch/test_utils.go | 3 +- pkg/tsdb/cloudwatch/time_series_query_test.go | 36 +++++---- 11 files changed, 164 insertions(+), 150 deletions(-) diff --git a/pkg/tsdb/cloudwatch/annotation_query_test.go b/pkg/tsdb/cloudwatch/annotation_query_test.go index 2aeab655b7..6779c82a89 100644 --- a/pkg/tsdb/cloudwatch/annotation_query_test.go +++ b/pkg/tsdb/cloudwatch/annotation_query_test.go @@ -30,7 +30,7 @@ func TestQuery_AnnotationQuery(t *testing.T) { client = fakeCWAnnotationsClient{describeAlarmsForMetricOutput: &cloudwatch.DescribeAlarmsForMetricOutput{}} im := defaultTestInstanceManager() - executor := newExecutor(im, &fakeSessionCache{}, log.NewNullLogger()) + executor := newExecutor(im, log.NewNullLogger()) _, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{ PluginContext: backend.PluginContext{ DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}, @@ -62,7 +62,7 @@ func TestQuery_AnnotationQuery(t *testing.T) { client = fakeCWAnnotationsClient{describeAlarmsOutput: &cloudwatch.DescribeAlarmsOutput{}} im := defaultTestInstanceManager() - executor := newExecutor(im, &fakeSessionCache{}, log.NewNullLogger()) + executor := newExecutor(im, log.NewNullLogger()) _, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{ PluginContext: backend.PluginContext{ DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}, diff --git a/pkg/tsdb/cloudwatch/cloudwatch.go b/pkg/tsdb/cloudwatch/cloudwatch.go index 2d89697eb4..ddff020b69 100644 --- a/pkg/tsdb/cloudwatch/cloudwatch.go +++ b/pkg/tsdb/cloudwatch/cloudwatch.go @@ -47,6 +47,7 @@ type DataQueryJson struct { type DataSource struct { Settings models.CloudWatchSettings HTTPClient *http.Client + sessions SessionCache tagValueCache *cache.Cache ProxyOpts *proxy.Options } @@ -66,7 +67,6 @@ func ProvideService(httpClientProvider *httpclient.Provider) *CloudWatchService executor := newExecutor( datasource.NewInstanceManager(NewInstanceSettings(httpClientProvider)), - awsds.NewSessionCache(), logger, ) @@ -83,11 +83,10 @@ type SessionCache interface { GetSession(c awsds.SessionConfig) (*session.Session, error) } -func newExecutor(im instancemgmt.InstanceManager, sessions SessionCache, logger log.Logger) *cloudWatchExecutor { +func newExecutor(im instancemgmt.InstanceManager, logger log.Logger) *cloudWatchExecutor { e := &cloudWatchExecutor{ - im: im, - sessions: sessions, - logger: logger, + im: im, + logger: logger, } e.resourceHandler = httpadapter.New(e.newResourceMux()) @@ -115,6 +114,7 @@ func NewInstanceSettings(httpClientProvider *httpclient.Provider) datasource.Ins Settings: instanceSettings, HTTPClient: httpClient, tagValueCache: cache.New(tagValueCacheExpiration, tagValueCacheExpiration*5), + sessions: awsds.NewSessionCache(), // this is used to build a custom dialer when secure socks proxy is enabled ProxyOpts: opts.ProxyOptions, }, nil @@ -123,9 +123,8 @@ func NewInstanceSettings(httpClientProvider *httpclient.Provider) datasource.Ins // cloudWatchExecutor executes CloudWatch requests type cloudWatchExecutor struct { - im instancemgmt.InstanceManager - sessions SessionCache - logger log.Logger + im instancemgmt.InstanceManager + logger log.Logger resourceHandler backend.CallResourceHandler } @@ -160,7 +159,7 @@ func (e *cloudWatchExecutor) getRequestContext(ctx context.Context, pluginCtx ba return models.RequestContext{}, err } - sess, err := e.newSession(ctx, pluginCtx, r) + sess, err := instance.newSession(r) if err != nil { return models.RequestContext{}, err } @@ -247,12 +246,12 @@ func (e *cloudWatchExecutor) checkHealthMetrics(ctx context.Context, pluginCtx b MetricName: &metric, } - session, err := e.newSession(ctx, pluginCtx, defaultRegion) + instance, err := e.getInstance(ctx, pluginCtx) if err != nil { return err } - instance, err := e.getInstance(ctx, pluginCtx) + session, err := instance.newSession(defaultRegion) if err != nil { return err } @@ -263,7 +262,7 @@ func (e *cloudWatchExecutor) checkHealthMetrics(ctx context.Context, pluginCtx b } func (e *cloudWatchExecutor) checkHealthLogs(ctx context.Context, pluginCtx backend.PluginContext) error { - session, err := e.newSession(ctx, pluginCtx, defaultRegion) + session, err := e.newSessionFromContext(ctx, pluginCtx, defaultRegion) if err != nil { return err } @@ -272,42 +271,36 @@ func (e *cloudWatchExecutor) checkHealthLogs(ctx context.Context, pluginCtx back return err } -func (e *cloudWatchExecutor) newSession(ctx context.Context, pluginCtx backend.PluginContext, region string) (*session.Session, error) { - instance, err := e.getInstance(ctx, pluginCtx) - if err != nil { - return nil, err - } - +func (ds *DataSource) newSession(region string) (*session.Session, error) { if region == defaultRegion { - if len(instance.Settings.Region) == 0 { + if len(ds.Settings.Region) == 0 { return nil, models.ErrMissingRegion } - region = instance.Settings.Region + region = ds.Settings.Region } - - sess, err := e.sessions.GetSession(awsds.SessionConfig{ + sess, err := ds.sessions.GetSession(awsds.SessionConfig{ // https://github.com/grafana/grafana/issues/46365 // HTTPClient: instance.HTTPClient, Settings: awsds.AWSDatasourceSettings{ - Profile: instance.Settings.Profile, + Profile: ds.Settings.Profile, Region: region, - AuthType: instance.Settings.AuthType, - AssumeRoleARN: instance.Settings.AssumeRoleARN, - ExternalID: instance.Settings.ExternalID, - Endpoint: instance.Settings.Endpoint, - DefaultRegion: instance.Settings.Region, - AccessKey: instance.Settings.AccessKey, - SecretKey: instance.Settings.SecretKey, + AuthType: ds.Settings.AuthType, + AssumeRoleARN: ds.Settings.AssumeRoleARN, + ExternalID: ds.Settings.ExternalID, + Endpoint: ds.Settings.Endpoint, + DefaultRegion: ds.Settings.Region, + AccessKey: ds.Settings.AccessKey, + SecretKey: ds.Settings.SecretKey, }, UserAgentName: aws.String("Cloudwatch"), - AuthSettings: &instance.Settings.GrafanaSettings, + AuthSettings: &ds.Settings.GrafanaSettings, }) if err != nil { return nil, err } // work around until https://github.com/grafana/grafana/issues/39089 is implemented - if instance.Settings.GrafanaSettings.SecureSocksDSProxyEnabled && instance.Settings.SecureSocksProxyEnabled { + if ds.Settings.GrafanaSettings.SecureSocksDSProxyEnabled && ds.Settings.SecureSocksProxyEnabled { // only update the transport to try to avoid the issue mentioned here https://github.com/grafana/grafana/issues/46365 // also, 'sess' is cached and reused, so the first time it might have the transport not set, the following uses it will if sess.Config.HTTPClient.Transport == nil { @@ -320,7 +313,7 @@ func (e *cloudWatchExecutor) newSession(ctx context.Context, pluginCtx backend.P } sess.Config.HTTPClient.Transport = defTransport.Clone() } - err = proxy.New(instance.ProxyOpts).ConfigureSecureSocksHTTPProxy(sess.Config.HTTPClient.Transport.(*http.Transport)) + err = proxy.New(ds.ProxyOpts).ConfigureSecureSocksHTTPProxy(sess.Config.HTTPClient.Transport.(*http.Transport)) if err != nil { return nil, fmt.Errorf("error configuring Secure Socks proxy for Transport: %w", err) } @@ -328,6 +321,15 @@ func (e *cloudWatchExecutor) newSession(ctx context.Context, pluginCtx backend.P return sess, nil } +func (e *cloudWatchExecutor) newSessionFromContext(ctx context.Context, pluginCtx backend.PluginContext, region string) (*session.Session, error) { + instance, err := e.getInstance(ctx, pluginCtx) + if err != nil { + return nil, err + } + + return instance.newSession(region) +} + func (e *cloudWatchExecutor) getInstance(ctx context.Context, pluginCtx backend.PluginContext) (*DataSource, error) { i, err := e.im.Get(ctx, pluginCtx) if err != nil { @@ -339,7 +341,7 @@ func (e *cloudWatchExecutor) getInstance(ctx context.Context, pluginCtx backend. } func (e *cloudWatchExecutor) getCWClient(ctx context.Context, pluginCtx backend.PluginContext, region string) (cloudwatchiface.CloudWatchAPI, error) { - sess, err := e.newSession(ctx, pluginCtx, region) + sess, err := e.newSessionFromContext(ctx, pluginCtx, region) if err != nil { return nil, err } @@ -347,7 +349,7 @@ func (e *cloudWatchExecutor) getCWClient(ctx context.Context, pluginCtx backend. } func (e *cloudWatchExecutor) getCWLogsClient(ctx context.Context, pluginCtx backend.PluginContext, region string) (cloudwatchlogsiface.CloudWatchLogsAPI, error) { - sess, err := e.newSession(ctx, pluginCtx, region) + sess, err := e.newSessionFromContext(ctx, pluginCtx, region) if err != nil { return nil, err } @@ -358,7 +360,7 @@ func (e *cloudWatchExecutor) getCWLogsClient(ctx context.Context, pluginCtx back } func (e *cloudWatchExecutor) getEC2Client(ctx context.Context, pluginCtx backend.PluginContext, region string) (models.EC2APIProvider, error) { - sess, err := e.newSession(ctx, pluginCtx, region) + sess, err := e.newSessionFromContext(ctx, pluginCtx, region) if err != nil { return nil, err } @@ -368,7 +370,7 @@ func (e *cloudWatchExecutor) getEC2Client(ctx context.Context, pluginCtx backend func (e *cloudWatchExecutor) getRGTAClient(ctx context.Context, pluginCtx backend.PluginContext, region string) (resourcegroupstaggingapiiface.ResourceGroupsTaggingAPIAPI, error) { - sess, err := e.newSession(ctx, pluginCtx, region) + sess, err := e.newSessionFromContext(ctx, pluginCtx, region) if err != nil { return nil, err } diff --git a/pkg/tsdb/cloudwatch/cloudwatch_integration_test.go b/pkg/tsdb/cloudwatch/cloudwatch_integration_test.go index 20eae1ac11..e9ce1632e2 100644 --- a/pkg/tsdb/cloudwatch/cloudwatch_integration_test.go +++ b/pkg/tsdb/cloudwatch/cloudwatch_integration_test.go @@ -68,7 +68,7 @@ func Test_CloudWatch_CallResource_Integration_Test(t *testing.T) { {MetricName: aws.String("Test_MetricName8"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName4"), Value: aws.String("Value1")}}}, {MetricName: aws.String("Test_MetricName9"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName1"), Value: aws.String("Value2")}}}, }, MetricsPerPage: 100} - executor := newExecutor(im, &fakeSessionCache{}, log.NewNullLogger()) + executor := newExecutor(im, log.NewNullLogger()) req := &backend.CallResourceRequest{ Method: "GET", @@ -104,7 +104,7 @@ func Test_CloudWatch_CallResource_Integration_Test(t *testing.T) { {MetricName: aws.String("Test_MetricName8"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName4")}}}, {MetricName: aws.String("Test_MetricName9"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName1")}}}, }, MetricsPerPage: 2} - executor := newExecutor(im, &fakeSessionCache{}, log.NewNullLogger()) + executor := newExecutor(im, log.NewNullLogger()) req := &backend.CallResourceRequest{ Method: "GET", @@ -129,7 +129,7 @@ func Test_CloudWatch_CallResource_Integration_Test(t *testing.T) { t.Run("Should handle standard dimension key query and return hard coded keys", func(t *testing.T) { im := defaultTestInstanceManager() api = mocks.FakeMetricsAPI{} - executor := newExecutor(im, &fakeSessionCache{}, log.NewNullLogger()) + executor := newExecutor(im, log.NewNullLogger()) req := &backend.CallResourceRequest{ Method: "GET", @@ -154,7 +154,7 @@ func Test_CloudWatch_CallResource_Integration_Test(t *testing.T) { t.Run("Should handle custom namespace dimension key query and return hard coded keys", func(t *testing.T) { im := defaultTestInstanceManager() api = mocks.FakeMetricsAPI{} - executor := newExecutor(im, &fakeSessionCache{}, log.NewNullLogger()) + executor := newExecutor(im, log.NewNullLogger()) req := &backend.CallResourceRequest{ Method: "GET", @@ -190,7 +190,7 @@ func Test_CloudWatch_CallResource_Integration_Test(t *testing.T) { {MetricName: aws.String("Test_MetricName8"), Namespace: aws.String("AWS/EC2"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName4")}}}, {MetricName: aws.String("Test_MetricName9"), Namespace: aws.String("AWS/EC2"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName1")}}}, }, MetricsPerPage: 2} - executor := newExecutor(im, &fakeSessionCache{}, log.NewNullLogger()) + executor := newExecutor(im, log.NewNullLogger()) req := &backend.CallResourceRequest{ Method: "GET", @@ -227,7 +227,7 @@ func Test_CloudWatch_CallResource_Integration_Test(t *testing.T) { }, }, }, nil) - executor := newExecutor(im, &fakeSessionCache{}, log.NewNullLogger()) + executor := newExecutor(im, log.NewNullLogger()) req := &backend.CallResourceRequest{ Method: "GET", @@ -249,7 +249,7 @@ func Test_CloudWatch_CallResource_Integration_Test(t *testing.T) { t.Run("Should handle region requests and return regions from the api", func(t *testing.T) { im := defaultTestInstanceManager() - executor := newExecutor(im, &fakeSessionCache{}, log.NewNullLogger()) + executor := newExecutor(im, log.NewNullLogger()) req := &backend.CallResourceRequest{ Method: "GET", Path: `/regions`, @@ -275,7 +275,7 @@ func Test_CloudWatch_CallResource_Integration_Test(t *testing.T) { }}, nil }) - executor := newExecutor(imWithoutDefaultRegion, &fakeSessionCache{}, log.NewNullLogger()) + executor := newExecutor(imWithoutDefaultRegion, log.NewNullLogger()) req := &backend.CallResourceRequest{ Method: "GET", Path: `/regions`, diff --git a/pkg/tsdb/cloudwatch/cloudwatch_test.go b/pkg/tsdb/cloudwatch/cloudwatch_test.go index a989c7ada4..c2287c279c 100644 --- a/pkg/tsdb/cloudwatch/cloudwatch_test.go +++ b/pkg/tsdb/cloudwatch/cloudwatch_test.go @@ -135,7 +135,7 @@ func Test_CheckHealth(t *testing.T) { t.Run("successfully query metrics and logs", func(t *testing.T) { client = fakeCheckHealthClient{} - executor := newExecutor(im, &fakeSessionCache{}, log.NewNullLogger()) + executor := newExecutor(im, log.NewNullLogger()) resp, err := executor.CheckHealth(context.Background(), &backend.CheckHealthRequest{ PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}}, @@ -154,7 +154,7 @@ func Test_CheckHealth(t *testing.T) { return nil, fmt.Errorf("some logs query error") }} - executor := newExecutor(im, &fakeSessionCache{}, log.NewNullLogger()) + executor := newExecutor(im, log.NewNullLogger()) resp, err := executor.CheckHealth(context.Background(), &backend.CheckHealthRequest{ PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}}, @@ -173,7 +173,7 @@ func Test_CheckHealth(t *testing.T) { return fmt.Errorf("some list metrics error") }} - executor := newExecutor(im, &fakeSessionCache{}, log.NewNullLogger()) + executor := newExecutor(im, log.NewNullLogger()) resp, err := executor.CheckHealth(context.Background(), &backend.CheckHealthRequest{ PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}}, @@ -188,10 +188,16 @@ func Test_CheckHealth(t *testing.T) { t.Run("fail to get clients", func(t *testing.T) { client = fakeCheckHealthClient{} + im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { + return DataSource{ + Settings: models.CloudWatchSettings{AWSDatasourceSettings: awsds.AWSDatasourceSettings{Region: "us-east-1"}}, + sessions: &fakeSessionCache{getSession: func(c awsds.SessionConfig) (*session.Session, error) { + return nil, fmt.Errorf("some sessions error") + }}, + }, nil + }) - executor := newExecutor(im, &fakeSessionCache{getSession: func(c awsds.SessionConfig) (*session.Session, error) { - return nil, fmt.Errorf("some sessions error") - }}, log.NewNullLogger()) + executor := newExecutor(im, log.NewNullLogger()) resp, err := executor.CheckHealth(context.Background(), &backend.CheckHealthRequest{ PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}}, @@ -216,22 +222,25 @@ func TestNewSession_passes_authSettings(t *testing.T) { SecureSocksDSProxyEnabled: true, } im := datasource.NewInstanceManager((func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { - return DataSource{Settings: models.CloudWatchSettings{ - AWSDatasourceSettings: awsds.AWSDatasourceSettings{ - Region: "us-east-1", + return DataSource{ + Settings: models.CloudWatchSettings{ + AWSDatasourceSettings: awsds.AWSDatasourceSettings{ + Region: "us-east-1", + }, + GrafanaSettings: expectedSettings, }, - GrafanaSettings: expectedSettings, - }}, nil - })) - executor := newExecutor(im, &fakeSessionCache{getSession: func(c awsds.SessionConfig) (*session.Session, error) { - assert.NotNil(t, c.AuthSettings) - assert.Equal(t, expectedSettings, *c.AuthSettings) - return &session.Session{ - Config: &aws.Config{}, + sessions: &fakeSessionCache{getSession: func(c awsds.SessionConfig) (*session.Session, error) { + assert.NotNil(t, c.AuthSettings) + assert.Equal(t, expectedSettings, *c.AuthSettings) + return &session.Session{ + Config: &aws.Config{}, + }, nil + }}, }, nil - }}, log.NewNullLogger()) + })) + executor := newExecutor(im, log.NewNullLogger()) - _, err := executor.newSession(context.Background(), + _, err := executor.newSessionFromContext(context.Background(), backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}}, "us-east-1") require.NoError(t, err) } @@ -275,7 +284,7 @@ func TestQuery_ResourceRequest_DescribeLogGroups_with_CrossAccountQuerying(t *te }, } - executor := newExecutor(im, &fakeSessionCache{}, log.NewNullLogger()) + executor := newExecutor(im, log.NewNullLogger()) err := executor.CallResource(contextWithFeaturesEnabled(features.FlagCloudWatchCrossAccountQuerying), req, sender) assert.NoError(t, err) diff --git a/pkg/tsdb/cloudwatch/log_actions_test.go b/pkg/tsdb/cloudwatch/log_actions_test.go index 1155c9090f..f0d784c60f 100644 --- a/pkg/tsdb/cloudwatch/log_actions_test.go +++ b/pkg/tsdb/cloudwatch/log_actions_test.go @@ -88,10 +88,10 @@ func TestQuery_handleGetLogEvents_passes_nil_start_and_end_times_to_GetLogEvents cli = fakeCWLogsClient{} im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { - return DataSource{Settings: models.CloudWatchSettings{}}, nil + return DataSource{Settings: models.CloudWatchSettings{}, sessions: &fakeSessionCache{}}, nil }) - executor := newExecutor(im, &fakeSessionCache{}, log.NewNullLogger()) + executor := newExecutor(im, log.NewNullLogger()) _, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{ PluginContext: backend.PluginContext{ DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}, @@ -122,9 +122,9 @@ func TestQuery_GetLogEvents_returns_response_from_GetLogEvents_to_data_frame_fie return cli } im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { - return DataSource{Settings: models.CloudWatchSettings{}}, nil + return DataSource{Settings: models.CloudWatchSettings{}, sessions: &fakeSessionCache{}}, nil }) - executor := newExecutor(im, &fakeSessionCache{}, log.NewNullLogger()) + executor := newExecutor(im, log.NewNullLogger()) cli = &mocks.MockLogEvents{} cli.On("GetLogEventsWithContext", mock.Anything, mock.Anything, mock.Anything).Return(&cloudwatchlogs.GetLogEventsOutput{ @@ -206,10 +206,10 @@ func TestQuery_StartQuery(t *testing.T) { AWSDatasourceSettings: awsds.AWSDatasourceSettings{ Region: "us-east-2", }, - }}, nil + }, sessions: &fakeSessionCache{}}, nil }) - executor := newExecutor(im, &fakeSessionCache{}, log.NewNullLogger()) + executor := newExecutor(im, log.NewNullLogger()) _, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{ PluginContext: backend.PluginContext{ DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}, @@ -263,10 +263,10 @@ func TestQuery_StartQuery(t *testing.T) { AWSDatasourceSettings: awsds.AWSDatasourceSettings{ Region: "us-east-2", }, - }}, nil + }, sessions: &fakeSessionCache{}}, nil }) - executor := newExecutor(im, &fakeSessionCache{}, log.NewNullLogger()) + executor := newExecutor(im, log.NewNullLogger()) resp, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{ PluginContext: backend.PluginContext{ DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}, @@ -321,9 +321,9 @@ func Test_executeStartQuery(t *testing.T) { t.Run("successfully parses information from JSON to StartQueryWithContext", func(t *testing.T) { cli = fakeCWLogsClient{} im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { - return DataSource{Settings: models.CloudWatchSettings{}}, nil + return DataSource{Settings: models.CloudWatchSettings{}, sessions: &fakeSessionCache{}}, nil }) - executor := newExecutor(im, &fakeSessionCache{}, log.NewNullLogger()) + executor := newExecutor(im, log.NewNullLogger()) _, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{ PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}}, @@ -357,9 +357,9 @@ func Test_executeStartQuery(t *testing.T) { t.Run("does not populate StartQueryInput.limit when no limit provided", func(t *testing.T) { cli = fakeCWLogsClient{} im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { - return DataSource{Settings: models.CloudWatchSettings{}}, nil + return DataSource{Settings: models.CloudWatchSettings{}, sessions: &fakeSessionCache{}}, nil }) - executor := newExecutor(im, &fakeSessionCache{}, log.NewNullLogger()) + executor := newExecutor(im, log.NewNullLogger()) _, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{ PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}}, @@ -383,9 +383,9 @@ func Test_executeStartQuery(t *testing.T) { t.Run("attaches logGroupIdentifiers if the crossAccount feature is enabled", func(t *testing.T) { cli = fakeCWLogsClient{} im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { - return DataSource{Settings: models.CloudWatchSettings{}}, nil + return DataSource{Settings: models.CloudWatchSettings{}, sessions: &fakeSessionCache{}}, nil }) - executor := newExecutor(im, &fakeSessionCache{}, log.NewNullLogger()) + executor := newExecutor(im, log.NewNullLogger()) _, err := executor.QueryData(contextWithFeaturesEnabled(features.FlagCloudWatchCrossAccountQuerying), &backend.QueryDataRequest{ PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}}, @@ -419,9 +419,9 @@ func Test_executeStartQuery(t *testing.T) { t.Run("attaches logGroupIdentifiers if the crossAccount feature is enabled and strips out trailing *", func(t *testing.T) { cli = fakeCWLogsClient{} im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { - return DataSource{Settings: models.CloudWatchSettings{}}, nil + return DataSource{Settings: models.CloudWatchSettings{}, sessions: &fakeSessionCache{}}, nil }) - executor := newExecutor(im, &fakeSessionCache{}, log.NewNullLogger()) + executor := newExecutor(im, log.NewNullLogger()) _, err := executor.QueryData(contextWithFeaturesEnabled(features.FlagCloudWatchCrossAccountQuerying), &backend.QueryDataRequest{ PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}}, @@ -455,9 +455,9 @@ func Test_executeStartQuery(t *testing.T) { t.Run("uses LogGroupNames if the cross account feature flag is not enabled, and log group names is present", func(t *testing.T) { cli = fakeCWLogsClient{} im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { - return DataSource{Settings: models.CloudWatchSettings{}}, nil + return DataSource{Settings: models.CloudWatchSettings{}, sessions: &fakeSessionCache{}}, nil }) - executor := newExecutor(im, &fakeSessionCache{}, log.NewNullLogger()) + executor := newExecutor(im, log.NewNullLogger()) _, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{ PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}}, Queries: []backend.DataQuery{ @@ -490,9 +490,9 @@ func Test_executeStartQuery(t *testing.T) { t.Run("ignores logGroups if feature flag is disabled even if logGroupNames is not present", func(t *testing.T) { cli = fakeCWLogsClient{} im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { - return DataSource{Settings: models.CloudWatchSettings{}}, nil + return DataSource{Settings: models.CloudWatchSettings{}, sessions: &fakeSessionCache{}}, nil }) - executor := newExecutor(im, &fakeSessionCache{}, log.NewNullLogger()) + executor := newExecutor(im, log.NewNullLogger()) _, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{ PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}}, Queries: []backend.DataQuery{ @@ -524,9 +524,9 @@ func Test_executeStartQuery(t *testing.T) { t.Run("it always uses logGroups when feature flag is enabled and ignores log group names", func(t *testing.T) { cli = fakeCWLogsClient{} im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { - return DataSource{Settings: models.CloudWatchSettings{}}, nil + return DataSource{Settings: models.CloudWatchSettings{}, sessions: &fakeSessionCache{}}, nil }) - executor := newExecutor(im, &fakeSessionCache{}, log.NewNullLogger()) + executor := newExecutor(im, log.NewNullLogger()) _, err := executor.QueryData(contextWithFeaturesEnabled(features.FlagCloudWatchCrossAccountQuerying), &backend.QueryDataRequest{ PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}}, Queries: []backend.DataQuery{ @@ -589,7 +589,7 @@ func TestQuery_StopQuery(t *testing.T) { } im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { - return DataSource{Settings: models.CloudWatchSettings{}}, nil + return DataSource{Settings: models.CloudWatchSettings{}, sessions: &fakeSessionCache{}}, nil }) timeRange := backend.TimeRange{ @@ -597,7 +597,7 @@ func TestQuery_StopQuery(t *testing.T) { To: time.Unix(1584700643, 0), } - executor := newExecutor(im, &fakeSessionCache{}, log.NewNullLogger()) + executor := newExecutor(im, log.NewNullLogger()) resp, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{ PluginContext: backend.PluginContext{ DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}, @@ -684,10 +684,10 @@ func TestQuery_GetQueryResults(t *testing.T) { } im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { - return DataSource{Settings: models.CloudWatchSettings{}}, nil + return DataSource{Settings: models.CloudWatchSettings{}, sessions: &fakeSessionCache{}}, nil }) - executor := newExecutor(im, &fakeSessionCache{}, log.NewNullLogger()) + executor := newExecutor(im, log.NewNullLogger()) resp, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{ PluginContext: backend.PluginContext{ DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}, diff --git a/pkg/tsdb/cloudwatch/log_sync_query_test.go b/pkg/tsdb/cloudwatch/log_sync_query_test.go index 112b4746b6..f1bed59ccf 100644 --- a/pkg/tsdb/cloudwatch/log_sync_query_test.go +++ b/pkg/tsdb/cloudwatch/log_sync_query_test.go @@ -37,11 +37,11 @@ func Test_executeSyncLogQuery(t *testing.T) { t.Run("getCWLogsClient is called with region from input JSON", func(t *testing.T) { cli = fakeCWLogsClient{queryResults: cloudwatchlogs.GetQueryResultsOutput{Status: aws.String("Complete")}} + sess := fakeSessionCache{} im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { - return DataSource{Settings: models.CloudWatchSettings{}}, nil + return DataSource{Settings: models.CloudWatchSettings{}, sessions: &sess}, nil }) - sess := fakeSessionCache{} - executor := newExecutor(im, &sess, log.NewNullLogger()) + executor := newExecutor(im, log.NewNullLogger()) _, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{ Headers: map[string]string{headerFromAlert: "some value"}, @@ -63,12 +63,12 @@ func Test_executeSyncLogQuery(t *testing.T) { t.Run("getCWLogsClient is called with region from instance manager when region is default", func(t *testing.T) { cli = fakeCWLogsClient{queryResults: cloudwatchlogs.GetQueryResultsOutput{Status: aws.String("Complete")}} + sess := fakeSessionCache{} im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { - return DataSource{Settings: models.CloudWatchSettings{AWSDatasourceSettings: awsds.AWSDatasourceSettings{Region: "instance manager's region"}}}, nil + return DataSource{Settings: models.CloudWatchSettings{AWSDatasourceSettings: awsds.AWSDatasourceSettings{Region: "instance manager's region"}}, sessions: &sess}, nil }) - sess := fakeSessionCache{} - executor := newExecutor(im, &sess, log.NewNullLogger()) + executor := newExecutor(im, log.NewNullLogger()) _, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{ Headers: map[string]string{headerFromAlert: "some value"}, PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}}, @@ -121,11 +121,10 @@ func Test_executeSyncLogQuery(t *testing.T) { syncCalled = false cli = fakeCWLogsClient{queryResults: cloudwatchlogs.GetQueryResultsOutput{Status: aws.String("Complete")}} im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { - return DataSource{Settings: models.CloudWatchSettings{AWSDatasourceSettings: awsds.AWSDatasourceSettings{Region: "instance manager's region"}}}, nil + return DataSource{Settings: models.CloudWatchSettings{AWSDatasourceSettings: awsds.AWSDatasourceSettings{Region: "instance manager's region"}}, sessions: &fakeSessionCache{}}, nil }) - sess := fakeSessionCache{} - executor := newExecutor(im, &sess, log.NewNullLogger()) + executor := newExecutor(im, log.NewNullLogger()) _, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{ Headers: tc.headers, PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}}, @@ -164,11 +163,10 @@ func Test_executeSyncLogQuery(t *testing.T) { cli = fakeCWLogsClient{queryResults: cloudwatchlogs.GetQueryResultsOutput{Status: aws.String("Complete")}} im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { - return DataSource{Settings: models.CloudWatchSettings{AWSDatasourceSettings: awsds.AWSDatasourceSettings{Region: "instance manager's region"}}}, nil + return DataSource{Settings: models.CloudWatchSettings{AWSDatasourceSettings: awsds.AWSDatasourceSettings{Region: "instance manager's region"}}, sessions: &fakeSessionCache{}}, nil }) - sess := fakeSessionCache{} - executor := newExecutor(im, &sess, log.NewNullLogger()) + executor := newExecutor(im, log.NewNullLogger()) _, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{ PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}}, Queries: []backend.DataQuery{ @@ -205,9 +203,9 @@ func Test_executeSyncLogQuery_handles_RefId_from_input_queries(t *testing.T) { }, nil) cli.On("GetQueryResultsWithContext", mock.Anything, mock.Anything, mock.Anything).Return(&cloudwatchlogs.GetQueryResultsOutput{Status: aws.String("Complete")}, nil) im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { - return DataSource{Settings: models.CloudWatchSettings{}}, nil + return DataSource{Settings: models.CloudWatchSettings{}, sessions: &fakeSessionCache{}}, nil }) - executor := newExecutor(im, &fakeSessionCache{}, log.NewNullLogger()) + executor := newExecutor(im, log.NewNullLogger()) res, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{ Headers: map[string]string{headerFromAlert: "some value"}, @@ -234,9 +232,9 @@ func Test_executeSyncLogQuery_handles_RefId_from_input_queries(t *testing.T) { }, nil) cli.On("GetQueryResultsWithContext", mock.Anything, mock.Anything, mock.Anything).Return(&cloudwatchlogs.GetQueryResultsOutput{Status: aws.String("Complete")}, nil) im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { - return DataSource{Settings: models.CloudWatchSettings{}}, nil + return DataSource{Settings: models.CloudWatchSettings{}, sessions: &fakeSessionCache{}}, nil }) - executor := newExecutor(im, &fakeSessionCache{}, log.NewNullLogger()) + executor := newExecutor(im, log.NewNullLogger()) res, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{ Headers: map[string]string{headerFromAlert: "some value"}, @@ -303,9 +301,9 @@ func Test_executeSyncLogQuery_handles_RefId_from_input_queries(t *testing.T) { Status: aws.String("Complete")}, nil) im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { - return DataSource{Settings: models.CloudWatchSettings{}}, nil + return DataSource{Settings: models.CloudWatchSettings{}, sessions: &fakeSessionCache{}}, nil }) - executor := newExecutor(im, &fakeSessionCache{}, log.NewNullLogger()) + executor := newExecutor(im, log.NewNullLogger()) res, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{ Headers: map[string]string{headerFromAlert: "some value"}, @@ -348,10 +346,10 @@ func Test_executeSyncLogQuery_handles_RefId_from_input_queries(t *testing.T) { }, nil) cli.On("GetQueryResultsWithContext", mock.Anything, mock.Anything, mock.Anything).Return(&cloudwatchlogs.GetQueryResultsOutput{Status: aws.String("Running")}, nil) im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { - return DataSource{Settings: models.CloudWatchSettings{LogsTimeout: models.Duration{Duration: time.Millisecond}}}, nil + return DataSource{Settings: models.CloudWatchSettings{LogsTimeout: models.Duration{Duration: time.Millisecond}}, sessions: &fakeSessionCache{}}, nil }) - executor := newExecutor(im, &fakeSessionCache{}, log.NewNullLogger()) + executor := newExecutor(im, log.NewNullLogger()) _, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{ Headers: map[string]string{headerFromAlert: "some value"}, @@ -381,9 +379,9 @@ func Test_executeSyncLogQuery_handles_RefId_from_input_queries(t *testing.T) { &fakeAWSError{code: "foo", message: "bar"}, ) im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { - return DataSource{Settings: models.CloudWatchSettings{}}, nil + return DataSource{Settings: models.CloudWatchSettings{}, sessions: &fakeSessionCache{}}, nil }) - executor := newExecutor(im, &fakeSessionCache{}, log.NewNullLogger()) + executor := newExecutor(im, log.NewNullLogger()) res, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{ Headers: map[string]string{headerFromAlert: "some value"}, diff --git a/pkg/tsdb/cloudwatch/metric_data_input_builder_test.go b/pkg/tsdb/cloudwatch/metric_data_input_builder_test.go index eaa35e3931..e891f996d2 100644 --- a/pkg/tsdb/cloudwatch/metric_data_input_builder_test.go +++ b/pkg/tsdb/cloudwatch/metric_data_input_builder_test.go @@ -27,7 +27,7 @@ func TestMetricDataInputBuilder(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - executor := newExecutor(nil, &fakeSessionCache{}, log.NewNullLogger()) + executor := newExecutor(nil, log.NewNullLogger()) query := getBaseQuery() query.TimezoneUTCOffset = tc.timezoneUTCOffset diff --git a/pkg/tsdb/cloudwatch/metric_data_query_builder_test.go b/pkg/tsdb/cloudwatch/metric_data_query_builder_test.go index e033481a30..b956a88a45 100644 --- a/pkg/tsdb/cloudwatch/metric_data_query_builder_test.go +++ b/pkg/tsdb/cloudwatch/metric_data_query_builder_test.go @@ -13,7 +13,7 @@ import ( func TestMetricDataQueryBuilder(t *testing.T) { t.Run("buildMetricDataQuery", func(t *testing.T) { t.Run("should use metric stat", func(t *testing.T) { - executor := newExecutor(nil, &fakeSessionCache{}, log.NewNullLogger()) + executor := newExecutor(nil, log.NewNullLogger()) query := getBaseQuery() query.MetricEditorMode = models.MetricEditorModeBuilder query.MetricQueryType = models.MetricQueryTypeSearch @@ -25,7 +25,7 @@ func TestMetricDataQueryBuilder(t *testing.T) { }) t.Run("should pass AccountId in metric stat query", func(t *testing.T) { - executor := newExecutor(nil, &fakeSessionCache{}, log.NewNullLogger()) + executor := newExecutor(nil, log.NewNullLogger()) query := getBaseQuery() query.MetricEditorMode = models.MetricEditorModeBuilder query.MetricQueryType = models.MetricQueryTypeSearch @@ -36,7 +36,7 @@ func TestMetricDataQueryBuilder(t *testing.T) { }) t.Run("should leave AccountId in metric stat query", func(t *testing.T) { - executor := newExecutor(nil, &fakeSessionCache{}, log.NewNullLogger()) + executor := newExecutor(nil, log.NewNullLogger()) query := getBaseQuery() query.MetricEditorMode = models.MetricEditorModeBuilder query.MetricQueryType = models.MetricQueryTypeSearch @@ -46,7 +46,7 @@ func TestMetricDataQueryBuilder(t *testing.T) { }) t.Run("should use custom built expression", func(t *testing.T) { - executor := newExecutor(nil, &fakeSessionCache{}, log.NewNullLogger()) + executor := newExecutor(nil, log.NewNullLogger()) query := getBaseQuery() query.MetricEditorMode = models.MetricEditorModeBuilder query.MetricQueryType = models.MetricQueryTypeSearch @@ -58,7 +58,7 @@ func TestMetricDataQueryBuilder(t *testing.T) { }) t.Run("should use sql expression", func(t *testing.T) { - executor := newExecutor(nil, &fakeSessionCache{}, log.NewNullLogger()) + executor := newExecutor(nil, log.NewNullLogger()) query := getBaseQuery() query.MetricEditorMode = models.MetricEditorModeRaw query.MetricQueryType = models.MetricQueryTypeQuery @@ -70,7 +70,7 @@ func TestMetricDataQueryBuilder(t *testing.T) { }) t.Run("should use user defined math expression", func(t *testing.T) { - executor := newExecutor(nil, &fakeSessionCache{}, log.NewNullLogger()) + executor := newExecutor(nil, log.NewNullLogger()) query := getBaseQuery() query.MetricEditorMode = models.MetricEditorModeRaw query.MetricQueryType = models.MetricQueryTypeSearch @@ -82,7 +82,7 @@ func TestMetricDataQueryBuilder(t *testing.T) { }) t.Run("should set period in user defined expression", func(t *testing.T) { - executor := newExecutor(nil, &fakeSessionCache{}, log.NewNullLogger()) + executor := newExecutor(nil, log.NewNullLogger()) query := getBaseQuery() query.MetricEditorMode = models.MetricEditorModeRaw query.MetricQueryType = models.MetricQueryTypeSearch @@ -96,7 +96,7 @@ func TestMetricDataQueryBuilder(t *testing.T) { }) t.Run("should set label", func(t *testing.T) { - executor := newExecutor(nil, &fakeSessionCache{}, log.NewNullLogger()) + executor := newExecutor(nil, log.NewNullLogger()) query := getBaseQuery() query.Label = "some label" @@ -108,7 +108,7 @@ func TestMetricDataQueryBuilder(t *testing.T) { }) t.Run("should not set label for empty string query label", func(t *testing.T) { - executor := newExecutor(nil, &fakeSessionCache{}, log.NewNullLogger()) + executor := newExecutor(nil, log.NewNullLogger()) query := getBaseQuery() query.Label = "" @@ -119,7 +119,7 @@ func TestMetricDataQueryBuilder(t *testing.T) { }) t.Run(`should not specify accountId when it is "all"`, func(t *testing.T) { - executor := newExecutor(nil, &fakeSessionCache{}, log.NewNullLogger()) + executor := newExecutor(nil, log.NewNullLogger()) query := &models.CloudWatchQuery{ Namespace: "AWS/EC2", MetricName: "CPUUtilization", @@ -137,7 +137,7 @@ func TestMetricDataQueryBuilder(t *testing.T) { }) t.Run("should set accountId when it is specified", func(t *testing.T) { - executor := newExecutor(nil, &fakeSessionCache{}, log.NewNullLogger()) + executor := newExecutor(nil, log.NewNullLogger()) query := &models.CloudWatchQuery{ Namespace: "AWS/EC2", MetricName: "CPUUtilization", diff --git a/pkg/tsdb/cloudwatch/metric_find_query_test.go b/pkg/tsdb/cloudwatch/metric_find_query_test.go index f8c73ac8f5..400c304162 100644 --- a/pkg/tsdb/cloudwatch/metric_find_query_test.go +++ b/pkg/tsdb/cloudwatch/metric_find_query_test.go @@ -53,7 +53,7 @@ func TestQuery_InstanceAttributes(t *testing.T) { } im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { - return DataSource{Settings: models.CloudWatchSettings{}}, nil + return DataSource{Settings: models.CloudWatchSettings{}, sessions: &fakeSessionCache{}}, nil }) filterMap := map[string][]string{ @@ -62,7 +62,7 @@ func TestQuery_InstanceAttributes(t *testing.T) { filterJson, err := json.Marshal(filterMap) require.NoError(t, err) - executor := newExecutor(im, &fakeSessionCache{}, log.NewNullLogger()) + executor := newExecutor(im, log.NewNullLogger()) resp, err := executor.handleGetEc2InstanceAttribute( context.Background(), backend.PluginContext{ @@ -137,10 +137,10 @@ func TestQuery_EBSVolumeIDs(t *testing.T) { } im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { - return DataSource{Settings: models.CloudWatchSettings{}}, nil + return DataSource{Settings: models.CloudWatchSettings{}, sessions: &fakeSessionCache{}}, nil }) - executor := newExecutor(im, &fakeSessionCache{}, log.NewNullLogger()) + executor := newExecutor(im, log.NewNullLogger()) resp, err := executor.handleGetEbsVolumeIds( context.Background(), backend.PluginContext{ @@ -198,7 +198,7 @@ func TestQuery_ResourceARNs(t *testing.T) { } im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { - return DataSource{Settings: models.CloudWatchSettings{}}, nil + return DataSource{Settings: models.CloudWatchSettings{}, sessions: &fakeSessionCache{}}, nil }) tagMap := map[string][]string{ @@ -207,7 +207,7 @@ func TestQuery_ResourceARNs(t *testing.T) { tagJson, err := json.Marshal(tagMap) require.NoError(t, err) - executor := newExecutor(im, &fakeSessionCache{}, log.NewNullLogger()) + executor := newExecutor(im, log.NewNullLogger()) resp, err := executor.handleGetResourceArns( context.Background(), backend.PluginContext{ diff --git a/pkg/tsdb/cloudwatch/test_utils.go b/pkg/tsdb/cloudwatch/test_utils.go index 0d2683b19d..26f7b8d315 100644 --- a/pkg/tsdb/cloudwatch/test_utils.go +++ b/pkg/tsdb/cloudwatch/test_utils.go @@ -209,7 +209,8 @@ func testInstanceManager(pageLimit int) instancemgmt.InstanceManager { Region: "us-east-1", }, GrafanaSettings: awsds.AuthSettings{ListMetricsPageLimit: pageLimit}, - }}, nil + }, + sessions: &fakeSessionCache{}}, nil })) } diff --git a/pkg/tsdb/cloudwatch/time_series_query_test.go b/pkg/tsdb/cloudwatch/time_series_query_test.go index 9945211aef..d1a1d13b47 100644 --- a/pkg/tsdb/cloudwatch/time_series_query_test.go +++ b/pkg/tsdb/cloudwatch/time_series_query_test.go @@ -27,7 +27,7 @@ import ( ) func TestTimeSeriesQuery(t *testing.T) { - executor := newExecutor(nil, &fakeSessionCache{}, log.NewNullLogger()) + executor := newExecutor(defaultTestInstanceManager(), log.NewNullLogger()) now := time.Now() origNewCWClient := NewCWClient @@ -53,7 +53,7 @@ func TestTimeSeriesQuery(t *testing.T) { im := defaultTestInstanceManager() - executor := newExecutor(im, &fakeSessionCache{}, log.NewNullLogger()) + executor := newExecutor(im, log.NewNullLogger()) resp, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{ PluginContext: backend.PluginContext{ DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}, @@ -147,19 +147,18 @@ func Test_executeTimeSeriesQuery_getCWClient_is_called_once_per_region_and_GetMe return &mockMetricClient } - im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { - return DataSource{Settings: models.CloudWatchSettings{}}, nil - }) - t.Run("Queries with the same region should call GetSession with that region 1 time and call GetMetricDataWithContext 1 time", func(t *testing.T) { mockSessionCache := &mockSessionCache{} mockSessionCache.On("GetSession", mock.MatchedBy( func(config awsds.SessionConfig) bool { return config.Settings.Region == "us-east-1" })). // region from queries is asserted here Return(&session.Session{Config: &aws.Config{}}, nil).Once() + im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { + return DataSource{Settings: models.CloudWatchSettings{}, sessions: mockSessionCache}, nil + }) mockMetricClient = mocks.MetricsAPI{} mockMetricClient.On("GetMetricDataWithContext", mock.Anything, mock.Anything, mock.Anything).Return(nil, nil) - executor := newExecutor(im, mockSessionCache, log.NewNullLogger()) + executor := newExecutor(im, log.NewNullLogger()) _, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{ PluginContext: backend.PluginContext{ DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}, @@ -207,10 +206,15 @@ func Test_executeTimeSeriesQuery_getCWClient_is_called_once_per_region_and_GetMe sessionCache.On("GetSession", mock.MatchedBy( func(config awsds.SessionConfig) bool { return config.Settings.Region == "us-east-2" })). Return(&session.Session{Config: &aws.Config{}}, nil, nil).Once() + + im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { + return DataSource{Settings: models.CloudWatchSettings{}, sessions: sessionCache}, nil + }) + mockMetricClient = mocks.MetricsAPI{} mockMetricClient.On("GetMetricDataWithContext", mock.Anything, mock.Anything, mock.Anything).Return(nil, nil) - executor := newExecutor(im, sessionCache, log.NewNullLogger()) + executor := newExecutor(im, log.NewNullLogger()) _, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{ PluginContext: backend.PluginContext{ DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}, @@ -337,13 +341,13 @@ func Test_QueryData_timeSeriesQuery_GetMetricDataWithContext(t *testing.T) { } im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { - return DataSource{Settings: models.CloudWatchSettings{}}, nil + return DataSource{Settings: models.CloudWatchSettings{}, sessions: &fakeSessionCache{}}, nil }) t.Run("passes query label as GetMetricData label", func(t *testing.T) { api = mocks.MetricsAPI{} api.On("GetMetricDataWithContext", mock.Anything, mock.Anything, mock.Anything).Return(&cloudwatch.GetMetricDataOutput{}, nil) - executor := newExecutor(im, &fakeSessionCache{}, log.NewNullLogger()) + executor := newExecutor(im, log.NewNullLogger()) query := newTestQuery(t, queryParameters{ Label: aws.String("${PROP('Period')} some words ${PROP('Dim.InstanceId')}"), }) @@ -382,7 +386,7 @@ func Test_QueryData_timeSeriesQuery_GetMetricDataWithContext(t *testing.T) { t.Run(name, func(t *testing.T) { api = mocks.MetricsAPI{} api.On("GetMetricDataWithContext", mock.Anything, mock.Anything, mock.Anything).Return(&cloudwatch.GetMetricDataOutput{}, nil) - executor := newExecutor(im, &fakeSessionCache{}, log.NewNullLogger()) + executor := newExecutor(im, log.NewNullLogger()) _, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{ PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}}, @@ -433,7 +437,7 @@ func Test_QueryData_response_data_frame_name_is_always_response_label(t *testing }}, nil) im := defaultTestInstanceManager() - executor := newExecutor(im, &fakeSessionCache{}, log.NewNullLogger()) + executor := newExecutor(im, log.NewNullLogger()) t.Run("where user defines search expression", func(t *testing.T) { query := newTestQuery(t, queryParameters{ @@ -590,7 +594,7 @@ func TestTimeSeriesQuery_CrossAccountQuerying(t *testing.T) { t.Run("should call GetMetricDataInput with AccountId nil when no AccountId is provided", func(t *testing.T) { api = mocks.MetricsAPI{} api.On("GetMetricDataWithContext", mock.Anything, mock.Anything, mock.Anything).Return(&cloudwatch.GetMetricDataOutput{}, nil) - executor := newExecutor(im, &fakeSessionCache{}, log.NewNullLogger()) + executor := newExecutor(im, log.NewNullLogger()) _, err := executor.QueryData(contextWithFeaturesEnabled(features.FlagCloudWatchCrossAccountQuerying), &backend.QueryDataRequest{ PluginContext: backend.PluginContext{ @@ -631,7 +635,7 @@ func TestTimeSeriesQuery_CrossAccountQuerying(t *testing.T) { t.Run("should call GetMetricDataInput with AccountId nil when feature flag is false", func(t *testing.T) { api = mocks.MetricsAPI{} api.On("GetMetricDataWithContext", mock.Anything, mock.Anything, mock.Anything).Return(&cloudwatch.GetMetricDataOutput{}, nil) - executor := newExecutor(im, &fakeSessionCache{}, log.NewNullLogger()) + executor := newExecutor(im, log.NewNullLogger()) _, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{ PluginContext: backend.PluginContext{ DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}, @@ -672,7 +676,7 @@ func TestTimeSeriesQuery_CrossAccountQuerying(t *testing.T) { t.Run("should call GetMetricDataInput with AccountId in a MetricStat query", func(t *testing.T) { api = mocks.MetricsAPI{} api.On("GetMetricDataWithContext", mock.Anything, mock.Anything, mock.Anything).Return(&cloudwatch.GetMetricDataOutput{}, nil) - executor := newExecutor(im, &fakeSessionCache{}, log.NewNullLogger()) + executor := newExecutor(im, log.NewNullLogger()) _, err := executor.QueryData(contextWithFeaturesEnabled(features.FlagCloudWatchCrossAccountQuerying), &backend.QueryDataRequest{ PluginContext: backend.PluginContext{ DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}, @@ -713,7 +717,7 @@ func TestTimeSeriesQuery_CrossAccountQuerying(t *testing.T) { t.Run("should GetMetricDataInput with AccountId in an inferred search expression query", func(t *testing.T) { api = mocks.MetricsAPI{} api.On("GetMetricDataWithContext", mock.Anything, mock.Anything, mock.Anything).Return(&cloudwatch.GetMetricDataOutput{}, nil) - executor := newExecutor(im, &fakeSessionCache{}, log.NewNullLogger()) + executor := newExecutor(im, log.NewNullLogger()) _, err := executor.QueryData(contextWithFeaturesEnabled(features.FlagCloudWatchCrossAccountQuerying), &backend.QueryDataRequest{ PluginContext: backend.PluginContext{ DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}, From 0848e7dd69272e97a1b88ba5e6a61d4e5ea7f978 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 26 Feb 2024 19:23:51 +0000 Subject: [PATCH 0198/1406] Update dependency @types/react to v18.2.59 --- package.json | 2 +- packages/grafana-data/package.json | 2 +- packages/grafana-flamegraph/package.json | 2 +- .../grafana-o11y-ds-frontend/package.json | 2 +- packages/grafana-prometheus/package.json | 2 +- packages/grafana-runtime/package.json | 2 +- packages/grafana-sql/package.json | 2 +- packages/grafana-ui/package.json | 2 +- .../datasource/azuremonitor/package.json | 2 +- .../datasource/cloud-monitoring/package.json | 2 +- .../grafana-pyroscope-datasource/package.json | 2 +- .../grafana-testdata-datasource/package.json | 2 +- .../app/plugins/datasource/parca/package.json | 2 +- .../app/plugins/datasource/tempo/package.json | 2 +- .../plugins/datasource/zipkin/package.json | 2 +- yarn.lock | 38 +++++++++---------- 16 files changed, 34 insertions(+), 34 deletions(-) diff --git a/package.json b/package.json index 9946684861..0cfa75b5e2 100644 --- a/package.json +++ b/package.json @@ -123,7 +123,7 @@ "@types/papaparse": "5.3.14", "@types/pluralize": "^0.0.33", "@types/prismjs": "1.26.3", - "@types/react": "18.2.55", + "@types/react": "18.2.59", "@types/react-beautiful-dnd": "13.1.8", "@types/react-dom": "18.2.19", "@types/react-grid-layout": "1.3.5", diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index 883764535e..838aa5a604 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -78,7 +78,7 @@ "@types/marked": "5.0.2", "@types/node": "20.11.20", "@types/papaparse": "5.3.14", - "@types/react": "18.2.55", + "@types/react": "18.2.59", "@types/react-dom": "18.2.19", "@types/testing-library__jest-dom": "5.14.9", "@types/tinycolor2": "1.4.6", diff --git a/packages/grafana-flamegraph/package.json b/packages/grafana-flamegraph/package.json index c5c692db8f..d1a03bea97 100644 --- a/packages/grafana-flamegraph/package.json +++ b/packages/grafana-flamegraph/package.json @@ -67,7 +67,7 @@ "@types/d3": "^7", "@types/jest": "^29.5.4", "@types/lodash": "4.14.202", - "@types/react": "18.2.55", + "@types/react": "18.2.59", "@types/react-virtualized-auto-sizer": "1.0.4", "@types/tinycolor2": "1.4.6", "babel-jest": "29.7.0", diff --git a/packages/grafana-o11y-ds-frontend/package.json b/packages/grafana-o11y-ds-frontend/package.json index 2ffab0e1a7..a8980f072d 100644 --- a/packages/grafana-o11y-ds-frontend/package.json +++ b/packages/grafana-o11y-ds-frontend/package.json @@ -34,7 +34,7 @@ "@testing-library/react": "14.2.1", "@testing-library/user-event": "14.5.2", "@types/jest": "^29.5.4", - "@types/react": "18.2.55", + "@types/react": "18.2.59", "@types/systemjs": "6.13.5", "@types/testing-library__jest-dom": "5.14.9", "jest": "^29.6.4", diff --git a/packages/grafana-prometheus/package.json b/packages/grafana-prometheus/package.json index 4c7d821ac1..50a214459f 100644 --- a/packages/grafana-prometheus/package.json +++ b/packages/grafana-prometheus/package.json @@ -97,7 +97,7 @@ "@types/node": "20.11.20", "@types/pluralize": "^0.0.33", "@types/prismjs": "1.26.3", - "@types/react": "18.2.55", + "@types/react": "18.2.59", "@types/react-beautiful-dnd": "13.1.8", "@types/react-dom": "18.2.19", "@types/react-highlight-words": "0.16.7", diff --git a/packages/grafana-runtime/package.json b/packages/grafana-runtime/package.json index 84d6f4d680..ebc543cd48 100644 --- a/packages/grafana-runtime/package.json +++ b/packages/grafana-runtime/package.json @@ -60,7 +60,7 @@ "@types/history": "4.7.11", "@types/jest": "29.5.12", "@types/lodash": "4.14.202", - "@types/react": "18.2.55", + "@types/react": "18.2.59", "@types/react-dom": "18.2.19", "@types/systemjs": "6.13.5", "esbuild": "0.18.12", diff --git a/packages/grafana-sql/package.json b/packages/grafana-sql/package.json index 6fc45eba7f..6c94fac6ac 100644 --- a/packages/grafana-sql/package.json +++ b/packages/grafana-sql/package.json @@ -39,7 +39,7 @@ "@testing-library/react-hooks": "^8.0.1", "@testing-library/user-event": "14.5.2", "@types/jest": "^29.5.4", - "@types/react": "18.2.55", + "@types/react": "18.2.59", "@types/systemjs": "6.13.5", "@types/testing-library__jest-dom": "5.14.9", "jest": "^29.6.4", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index c7b46f6b52..3118e85513 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -142,7 +142,7 @@ "@types/mock-raf": "1.0.6", "@types/node": "20.11.20", "@types/prismjs": "1.26.3", - "@types/react": "18.2.55", + "@types/react": "18.2.59", "@types/react-beautiful-dnd": "13.1.8", "@types/react-calendar": "3.9.0", "@types/react-color": "3.0.11", diff --git a/public/app/plugins/datasource/azuremonitor/package.json b/public/app/plugins/datasource/azuremonitor/package.json index 167026946a..8339305882 100644 --- a/public/app/plugins/datasource/azuremonitor/package.json +++ b/public/app/plugins/datasource/azuremonitor/package.json @@ -31,7 +31,7 @@ "@types/lodash": "4.14.202", "@types/node": "20.11.20", "@types/prismjs": "1.26.3", - "@types/react": "18.2.55", + "@types/react": "18.2.59", "@types/testing-library__jest-dom": "5.14.9", "react-select-event": "5.5.1", "ts-node": "10.9.2", diff --git a/public/app/plugins/datasource/cloud-monitoring/package.json b/public/app/plugins/datasource/cloud-monitoring/package.json index 443d0c488c..68ec97c884 100644 --- a/public/app/plugins/datasource/cloud-monitoring/package.json +++ b/public/app/plugins/datasource/cloud-monitoring/package.json @@ -34,7 +34,7 @@ "@types/lodash": "4.14.202", "@types/node": "20.11.20", "@types/prismjs": "1.26.3", - "@types/react": "18.2.55", + "@types/react": "18.2.59", "@types/react-test-renderer": "18.0.7", "@types/testing-library__jest-dom": "5.14.9", "react-select-event": "5.5.1", diff --git a/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json b/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json index 3ce77fc8e3..04a37d9866 100644 --- a/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json +++ b/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json @@ -27,7 +27,7 @@ "@types/jest": "29.5.12", "@types/lodash": "4.14.202", "@types/prismjs": "1.26.3", - "@types/react": "18.2.55", + "@types/react": "18.2.59", "@types/react-dom": "18.2.19", "@types/testing-library__jest-dom": "5.14.9", "css-loader": "6.10.0", diff --git a/public/app/plugins/datasource/grafana-testdata-datasource/package.json b/public/app/plugins/datasource/grafana-testdata-datasource/package.json index 19061828d2..5e4a71003e 100644 --- a/public/app/plugins/datasource/grafana-testdata-datasource/package.json +++ b/public/app/plugins/datasource/grafana-testdata-datasource/package.json @@ -28,7 +28,7 @@ "@types/jest": "29.5.12", "@types/lodash": "4.14.202", "@types/node": "20.11.20", - "@types/react": "18.2.55", + "@types/react": "18.2.59", "@types/testing-library__jest-dom": "5.14.9", "@types/uuid": "9.0.8", "ts-node": "10.9.2", diff --git a/public/app/plugins/datasource/parca/package.json b/public/app/plugins/datasource/parca/package.json index a1874cf6c0..f5b02b031b 100644 --- a/public/app/plugins/datasource/parca/package.json +++ b/public/app/plugins/datasource/parca/package.json @@ -21,7 +21,7 @@ "@testing-library/react": "14.2.1", "@testing-library/user-event": "14.5.2", "@types/lodash": "4.14.202", - "@types/react": "18.2.55", + "@types/react": "18.2.59", "ts-node": "10.9.2", "webpack": "5.90.2" }, diff --git a/public/app/plugins/datasource/tempo/package.json b/public/app/plugins/datasource/tempo/package.json index 74492416ca..14ed7e4f36 100644 --- a/public/app/plugins/datasource/tempo/package.json +++ b/public/app/plugins/datasource/tempo/package.json @@ -46,7 +46,7 @@ "@types/lodash": "4.14.202", "@types/node": "20.11.20", "@types/prismjs": "1.26.3", - "@types/react": "18.2.55", + "@types/react": "18.2.59", "@types/react-dom": "18.2.19", "@types/semver": "7.5.7", "@types/uuid": "9.0.8", diff --git a/public/app/plugins/datasource/zipkin/package.json b/public/app/plugins/datasource/zipkin/package.json index 65f01284a5..efe00b1e93 100644 --- a/public/app/plugins/datasource/zipkin/package.json +++ b/public/app/plugins/datasource/zipkin/package.json @@ -22,7 +22,7 @@ "@testing-library/react": "14.2.1", "@types/jest": "29.5.12", "@types/lodash": "4.14.202", - "@types/react": "18.2.55", + "@types/react": "18.2.59", "ts-node": "10.9.2", "webpack": "5.90.2" }, diff --git a/yarn.lock b/yarn.lock index f23af54f7a..6b59cc316e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3233,7 +3233,7 @@ __metadata: "@types/lodash": "npm:4.14.202" "@types/node": "npm:20.11.20" "@types/prismjs": "npm:1.26.3" - "@types/react": "npm:18.2.55" + "@types/react": "npm:18.2.59" "@types/testing-library__jest-dom": "npm:5.14.9" fast-deep-equal: "npm:^3.1.3" i18next: "npm:^23.0.0" @@ -3270,7 +3270,7 @@ __metadata: "@types/jest": "npm:29.5.12" "@types/lodash": "npm:4.14.202" "@types/prismjs": "npm:1.26.3" - "@types/react": "npm:18.2.55" + "@types/react": "npm:18.2.59" "@types/react-dom": "npm:18.2.19" "@types/testing-library__jest-dom": "npm:5.14.9" css-loader: "npm:6.10.0" @@ -3311,7 +3311,7 @@ __metadata: "@types/jest": "npm:29.5.12" "@types/lodash": "npm:4.14.202" "@types/node": "npm:20.11.20" - "@types/react": "npm:18.2.55" + "@types/react": "npm:18.2.59" "@types/testing-library__jest-dom": "npm:5.14.9" "@types/uuid": "npm:9.0.8" d3-random: "npm:^3.0.1" @@ -3365,7 +3365,7 @@ __metadata: "@testing-library/react": "npm:14.2.1" "@testing-library/user-event": "npm:14.5.2" "@types/lodash": "npm:4.14.202" - "@types/react": "npm:18.2.55" + "@types/react": "npm:18.2.59" lodash: "npm:4.17.21" monaco-editor: "npm:0.34.0" react: "npm:18.2.0" @@ -3400,7 +3400,7 @@ __metadata: "@types/lodash": "npm:4.14.202" "@types/node": "npm:20.11.20" "@types/prismjs": "npm:1.26.3" - "@types/react": "npm:18.2.55" + "@types/react": "npm:18.2.59" "@types/react-test-renderer": "npm:18.0.7" "@types/testing-library__jest-dom": "npm:5.14.9" debounce-promise: "npm:3.1.2" @@ -3452,7 +3452,7 @@ __metadata: "@types/lodash": "npm:4.14.202" "@types/node": "npm:20.11.20" "@types/prismjs": "npm:1.26.3" - "@types/react": "npm:18.2.55" + "@types/react": "npm:18.2.59" "@types/react-dom": "npm:18.2.19" "@types/semver": "npm:7.5.7" "@types/uuid": "npm:9.0.8" @@ -3497,7 +3497,7 @@ __metadata: "@testing-library/react": "npm:14.2.1" "@types/jest": "npm:29.5.12" "@types/lodash": "npm:4.14.202" - "@types/react": "npm:18.2.55" + "@types/react": "npm:18.2.59" lodash: "npm:4.17.21" react: "npm:18.2.0" react-use: "npm:17.5.0" @@ -3552,7 +3552,7 @@ __metadata: "@types/marked": "npm:5.0.2" "@types/node": "npm:20.11.20" "@types/papaparse": "npm:5.3.14" - "@types/react": "npm:18.2.55" + "@types/react": "npm:18.2.59" "@types/react-dom": "npm:18.2.19" "@types/string-hash": "npm:1.1.3" "@types/testing-library__jest-dom": "npm:5.14.9" @@ -3787,7 +3787,7 @@ __metadata: "@types/d3": "npm:^7" "@types/jest": "npm:^29.5.4" "@types/lodash": "npm:4.14.202" - "@types/react": "npm:18.2.55" + "@types/react": "npm:18.2.59" "@types/react-virtualized-auto-sizer": "npm:1.0.4" "@types/tinycolor2": "npm:1.4.6" babel-jest: "npm:29.7.0" @@ -3868,7 +3868,7 @@ __metadata: "@testing-library/react": "npm:14.2.1" "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:^29.5.4" - "@types/react": "npm:18.2.55" + "@types/react": "npm:18.2.59" "@types/systemjs": "npm:6.13.5" "@types/testing-library__jest-dom": "npm:5.14.9" jest: "npm:^29.6.4" @@ -3954,7 +3954,7 @@ __metadata: "@types/node": "npm:20.11.20" "@types/pluralize": "npm:^0.0.33" "@types/prismjs": "npm:1.26.3" - "@types/react": "npm:18.2.55" + "@types/react": "npm:18.2.59" "@types/react-beautiful-dnd": "npm:13.1.8" "@types/react-dom": "npm:18.2.19" "@types/react-highlight-words": "npm:0.16.7" @@ -4047,7 +4047,7 @@ __metadata: "@types/history": "npm:4.7.11" "@types/jest": "npm:29.5.12" "@types/lodash": "npm:4.14.202" - "@types/react": "npm:18.2.55" + "@types/react": "npm:18.2.59" "@types/react-dom": "npm:18.2.19" "@types/systemjs": "npm:6.13.5" esbuild: "npm:0.18.12" @@ -4128,7 +4128,7 @@ __metadata: "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:^29.5.4" "@types/lodash": "npm:4.14.202" - "@types/react": "npm:18.2.55" + "@types/react": "npm:18.2.59" "@types/react-virtualized-auto-sizer": "npm:1.0.4" "@types/systemjs": "npm:6.13.5" "@types/testing-library__jest-dom": "npm:5.14.9" @@ -4215,7 +4215,7 @@ __metadata: "@types/mock-raf": "npm:1.0.6" "@types/node": "npm:20.11.20" "@types/prismjs": "npm:1.26.3" - "@types/react": "npm:18.2.55" + "@types/react": "npm:18.2.59" "@types/react-beautiful-dnd": "npm:13.1.8" "@types/react-calendar": "npm:3.9.0" "@types/react-color": "npm:3.0.11" @@ -9910,14 +9910,14 @@ __metadata: languageName: node linkType: hard -"@types/react@npm:*, @types/react@npm:18.2.55, @types/react@npm:>=16": - version: 18.2.55 - resolution: "@types/react@npm:18.2.55" +"@types/react@npm:*, @types/react@npm:18.2.59, @types/react@npm:>=16": + version: 18.2.59 + resolution: "@types/react@npm:18.2.59" dependencies: "@types/prop-types": "npm:*" "@types/scheduler": "npm:*" csstype: "npm:^3.0.2" - checksum: 10/bf8fe19e73575489e63c0726355f164157cd69e75f2a862436ad2c0586e732cb953a7255a6bc73145e8f9506ee7a723f9a569ca9a39c53984e5b12b84e1c718a + checksum: 10/d065b998bc99ac61ff30011072a55e52eaa51ad20a84bc5893fae5e5de80d2e0a61217772a3ab79eb19d2f8d8ae3bd8451db5e0a77044f279618a1fc3446def6 languageName: node linkType: hard @@ -18413,7 +18413,7 @@ __metadata: "@types/papaparse": "npm:5.3.14" "@types/pluralize": "npm:^0.0.33" "@types/prismjs": "npm:1.26.3" - "@types/react": "npm:18.2.55" + "@types/react": "npm:18.2.59" "@types/react-beautiful-dnd": "npm:13.1.8" "@types/react-dom": "npm:18.2.19" "@types/react-grid-layout": "npm:1.3.5" From f8dc40df5249b90cdfd3005479570a25e8ad3177 Mon Sep 17 00:00:00 2001 From: Todd Treece <360020+toddtreece@users.noreply.github.com> Date: Mon, 26 Feb 2024 16:07:27 -0500 Subject: [PATCH 0199/1406] Chore: Point to xorm fork in go.mod (#83436) --- go.mod | 3 +++ go.sum | 9 ++------- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index 54fa1f6b1d..0c8c0e11ae 100644 --- a/go.mod +++ b/go.mod @@ -494,3 +494,6 @@ replace github.com/hashicorp/go-hclog => github.com/hashicorp/go-hclog v0.16.1 replace github.com/prometheus/alertmanager => github.com/grafana/prometheus-alertmanager v0.25.1-0.20240208102907-e82436ce63e6 exclude github.com/mattn/go-sqlite3 v2.0.3+incompatible + +// Use our fork xorm. go.work currently overrides this and points to the local ./pkg/util/xorm directory. +replace xorm.io/xorm => github.com/grafana/grafana/pkg/util/xorm v0.0.1 diff --git a/go.sum b/go.sum index a71dbc4562..988032666b 100644 --- a/go.sum +++ b/go.sum @@ -1611,8 +1611,6 @@ github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9/go.mod h1:GgB8SF9nRG github.com/deepmap/oapi-codegen v1.12.4 h1:pPmn6qI9MuOtCz82WY2Xaw46EQjgvxednXXrP7g5Q2s= github.com/deepmap/oapi-codegen v1.12.4/go.mod h1:3lgHGMu6myQ2vqbbTXH2H1o4eXFTGnFiDaOaKKl5yas= github.com/denisenkom/go-mssqldb v0.0.0-20190515213511-eb9f6a1743f3/go.mod h1:zAg7JM8CkOJ43xKXIj7eRO9kmWm/TW578qo+oDO6tuM= -github.com/denisenkom/go-mssqldb v0.0.0-20190707035753-2be1aa521ff4/go.mod h1:zAg7JM8CkOJ43xKXIj7eRO9kmWm/TW578qo+oDO6tuM= -github.com/denisenkom/go-mssqldb v0.12.0 h1:VtrkII767ttSPNRfFekePK3sctr+joXgO58stqQbtUA= github.com/denisenkom/go-mssqldb v0.12.0/go.mod h1:iiK0YP1ZeepvmBQk/QpLEhhTNJgfzrpArPY/aFvc9yU= github.com/dennwc/varint v1.0.0 h1:kGNFFSSw8ToIy3obO/kKr8U9GZYUAxQEVuix4zfDWzE= github.com/dennwc/varint v1.0.0/go.mod h1:hnItb35rvZvJrbTALZtY/iQfDs48JKRG1RPpgziApxA= @@ -2186,6 +2184,8 @@ github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240226124929-648abdbd0ea4 h github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240226124929-648abdbd0ea4/go.mod h1:vrRQJuNprTWqwm6JPxHf3BoTJhvO15QMEjQ7Q/YUOnI= github.com/grafana/grafana/pkg/apiserver v0.0.0-20240226124929-648abdbd0ea4 h1:tIbI5zgos92vwJ8lV3zwHwuxkV03GR3FGLkFW9V5LxY= github.com/grafana/grafana/pkg/apiserver v0.0.0-20240226124929-648abdbd0ea4/go.mod h1:vpYI6DHvFO595rpQGooUjcyicjt9rOevldDdW79peV0= +github.com/grafana/grafana/pkg/util/xorm v0.0.1 h1:72QZjxWIWpSeOF8ob4aMV058kfgZyeetkAB8dmeti2o= +github.com/grafana/grafana/pkg/util/xorm v0.0.1/go.mod h1:eNfbB9f2jM8o9RfwqwjY8SYm5tvowJ8Ly+iE4P9rXII= github.com/grafana/kindsys v0.0.0-20230508162304-452481b63482 h1:1YNoeIhii4UIIQpCPU+EXidnqf449d0C3ZntAEt4KSo= github.com/grafana/kindsys v0.0.0-20230508162304-452481b63482/go.mod h1:GNcfpy5+SY6RVbNGQW264gC0r336Dm+0zgQ5vt6+M8Y= github.com/grafana/prometheus-alertmanager v0.25.1-0.20240208102907-e82436ce63e6 h1:CBm0rwLCPDyarg9/bHJ50rBLYmyMDoyCWpgRMITZhdA= @@ -3111,8 +3111,6 @@ github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= -github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs= -github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b/go.mod h1:T3BPAOm2cqquPa0MKWeNkmOM5RQsRhkrwMWonFMN7fE= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.8 h1:xs88BrvEv273UsB79e0hcVrlUWmS0a8upikMFhSyAtA= @@ -4431,8 +4429,5 @@ sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= xorm.io/builder v0.3.6 h1:ha28mQ2M+TFx96Hxo+iq6tQgnkC9IZkM6D8w9sKHHF8= xorm.io/builder v0.3.6/go.mod h1:LEFAPISnRzG+zxaxj2vPicRwz67BdhFreKg8yv8/TgU= -xorm.io/core v0.7.2/go.mod h1:jJfd0UAEzZ4t87nbQYtVjmqpIODugN6PD2D9E+dJvdM= xorm.io/core v0.7.3 h1:W8ws1PlrnkS1CZU1YWaYLMQcQilwAmQXU0BJDJon+H0= xorm.io/core v0.7.3/go.mod h1:jJfd0UAEzZ4t87nbQYtVjmqpIODugN6PD2D9E+dJvdM= -xorm.io/xorm v0.8.2 h1:nbg1AyWn7iLrwp0Dqg8IrYOBkBYYJ85ry9bvZLVl4Ok= -xorm.io/xorm v0.8.2/go.mod h1:ZkJLEYLoVyg7amJK/5r779bHyzs2AU8f8VMiP6BM7uY= From 44639e1063bf5d61ca972d202d4690037c216e8c Mon Sep 17 00:00:00 2001 From: Isabella Siu Date: Mon, 26 Feb 2024 16:18:22 -0500 Subject: [PATCH 0200/1406] CloudWatch: Fetch externalId from settings instead of env (#83332) --- pkg/tsdb/cloudwatch/routes/external_id.go | 9 +++++--- .../cloudwatch/routes/external_id_test.go | 22 ++++++++++++++++--- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/pkg/tsdb/cloudwatch/routes/external_id.go b/pkg/tsdb/cloudwatch/routes/external_id.go index 8be23aeccf..5361cbdda7 100644 --- a/pkg/tsdb/cloudwatch/routes/external_id.go +++ b/pkg/tsdb/cloudwatch/routes/external_id.go @@ -5,9 +5,7 @@ import ( "encoding/json" "net/http" "net/url" - "os" - "github.com/grafana/grafana-aws-sdk/pkg/awsds" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana/pkg/tsdb/cloudwatch/models" ) @@ -17,8 +15,13 @@ type ExternalIdResponse struct { } func ExternalIdHandler(ctx context.Context, pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, parameters url.Values) ([]byte, *models.HttpError) { + reqCtx, err := reqCtxFactory(ctx, pluginCtx, "default") + if err != nil { + return nil, models.NewHttpError("error in ExternalIdHandler", http.StatusInternalServerError, err) + } + response := ExternalIdResponse{ - ExternalId: os.Getenv(awsds.GrafanaAssumeRoleExternalIdKeyName), + ExternalId: reqCtx.Settings.GrafanaSettings.ExternalID, } jsonResponse, err := json.Marshal(response) if err != nil { diff --git a/pkg/tsdb/cloudwatch/routes/external_id_test.go b/pkg/tsdb/cloudwatch/routes/external_id_test.go index b69df5f717..116378e09c 100644 --- a/pkg/tsdb/cloudwatch/routes/external_id_test.go +++ b/pkg/tsdb/cloudwatch/routes/external_id_test.go @@ -1,19 +1,30 @@ package routes import ( + "context" "net/http" "net/http/httptest" "testing" + "github.com/grafana/grafana-aws-sdk/pkg/awsds" + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana/pkg/tsdb/cloudwatch/models" "github.com/stretchr/testify/assert" ) func Test_external_id_route(t *testing.T) { - t.Run("successfully returns an external id from the env", func(t *testing.T) { + t.Run("successfully returns an external id from the instance", func(t *testing.T) { t.Setenv("AWS_AUTH_EXTERNAL_ID", "mock-external-id") rr := httptest.NewRecorder() - handler := http.HandlerFunc(ResourceRequestMiddleware(ExternalIdHandler, logger, nil)) + factoryFunc := func(_ context.Context, _ backend.PluginContext, region string) (reqCtx models.RequestContext, err error) { + return models.RequestContext{ + Settings: models.CloudWatchSettings{ + GrafanaSettings: awsds.AuthSettings{ExternalID: "mock-external-id"}, + }, + }, nil + } + handler := http.HandlerFunc(ResourceRequestMiddleware(ExternalIdHandler, logger, factoryFunc)) req := httptest.NewRequest("GET", "/external-id", nil) handler.ServeHTTP(rr, req) @@ -25,7 +36,12 @@ func Test_external_id_route(t *testing.T) { t.Run("returns an empty string if there is no external id", func(t *testing.T) { rr := httptest.NewRecorder() - handler := http.HandlerFunc(ResourceRequestMiddleware(ExternalIdHandler, logger, nil)) + factoryFunc := func(_ context.Context, _ backend.PluginContext, region string) (reqCtx models.RequestContext, err error) { + return models.RequestContext{ + Settings: models.CloudWatchSettings{}, + }, nil + } + handler := http.HandlerFunc(ResourceRequestMiddleware(ExternalIdHandler, logger, factoryFunc)) req := httptest.NewRequest("GET", "/external-id", nil) handler.ServeHTTP(rr, req) From 6097ce5b61d9a064c3f5dbb5c34af6603e46e47a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 26 Feb 2024 21:03:44 +0000 Subject: [PATCH 0201/1406] Update dependency @types/react-color to v3.0.12 --- packages/grafana-ui/package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 3118e85513..7150449e08 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -145,7 +145,7 @@ "@types/react": "18.2.59", "@types/react-beautiful-dnd": "13.1.8", "@types/react-calendar": "3.9.0", - "@types/react-color": "3.0.11", + "@types/react-color": "3.0.12", "@types/react-dom": "18.2.19", "@types/react-highlight-words": "0.16.7", "@types/react-router-dom": "5.3.3", diff --git a/yarn.lock b/yarn.lock index 6b59cc316e..9a01e017ff 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4218,7 +4218,7 @@ __metadata: "@types/react": "npm:18.2.59" "@types/react-beautiful-dnd": "npm:13.1.8" "@types/react-calendar": "npm:3.9.0" - "@types/react-color": "npm:3.0.11" + "@types/react-color": "npm:3.0.12" "@types/react-dom": "npm:18.2.19" "@types/react-highlight-words": "npm:0.16.7" "@types/react-router-dom": "npm:5.3.3" @@ -9776,13 +9776,13 @@ __metadata: languageName: node linkType: hard -"@types/react-color@npm:3.0.11": - version: 3.0.11 - resolution: "@types/react-color@npm:3.0.11" +"@types/react-color@npm:3.0.12": + version: 3.0.12 + resolution: "@types/react-color@npm:3.0.12" dependencies: "@types/react": "npm:*" "@types/reactcss": "npm:*" - checksum: 10/c683e7222da449fc68953710bd8e95c9748d12c223f89d78891878f813a4560bca18663ee431658e47aa41cfd7f36b2ab1e61f04007e9e8acd4878b75236b653 + checksum: 10/d8ed71d297d026faded787588720ee5e158edfa45ee23c5e517d8bb1441ccaa25f8a0c88069fd63829f7263bbbdc78a7ae2df5957061847a2d2c6351c46c13dd languageName: node linkType: hard From bdeff840687d542d106e724f90960c43d2e69482 Mon Sep 17 00:00:00 2001 From: Isabella Siu Date: Mon, 26 Feb 2024 16:19:45 -0500 Subject: [PATCH 0202/1406] CloudWatch: Refactor "getDimensionValuesForWildcards" (#83335) --- .../get_dimension_values_for_wildcards.go | 9 ++------- .../get_dimension_values_for_wildcards_test.go | 14 +++++++------- pkg/tsdb/cloudwatch/time_series_query.go | 2 +- 3 files changed, 10 insertions(+), 15 deletions(-) diff --git a/pkg/tsdb/cloudwatch/get_dimension_values_for_wildcards.go b/pkg/tsdb/cloudwatch/get_dimension_values_for_wildcards.go index 79c3caecd0..60ba597aa4 100644 --- a/pkg/tsdb/cloudwatch/get_dimension_values_for_wildcards.go +++ b/pkg/tsdb/cloudwatch/get_dimension_values_for_wildcards.go @@ -14,13 +14,8 @@ import ( // getDimensionValues gets the actual dimension values for dimensions with a wildcard func (e *cloudWatchExecutor) getDimensionValuesForWildcards(ctx context.Context, pluginCtx backend.PluginContext, region string, - client models.CloudWatchMetricsAPIProvider, origQueries []*models.CloudWatchQuery, tagValueCache *cache.Cache) ([]*models.CloudWatchQuery, error) { - instance, err := e.getInstance(ctx, pluginCtx) - if err != nil { - return nil, err - } - - metricsClient := clients.NewMetricsClient(client, instance.Settings.GrafanaSettings.ListMetricsPageLimit) + client models.CloudWatchMetricsAPIProvider, origQueries []*models.CloudWatchQuery, tagValueCache *cache.Cache, listMetricsPageLimit int) ([]*models.CloudWatchQuery, error) { + metricsClient := clients.NewMetricsClient(client, listMetricsPageLimit) service := services.NewListMetricsService(metricsClient) // create copies of the original query. All the fields besides Dimensions are primitives queries := copyQueries(origQueries) diff --git a/pkg/tsdb/cloudwatch/get_dimension_values_for_wildcards_test.go b/pkg/tsdb/cloudwatch/get_dimension_values_for_wildcards_test.go index 68514ccc68..3dbd5021e5 100644 --- a/pkg/tsdb/cloudwatch/get_dimension_values_for_wildcards_test.go +++ b/pkg/tsdb/cloudwatch/get_dimension_values_for_wildcards_test.go @@ -27,7 +27,7 @@ func TestGetDimensionValuesForWildcards(t *testing.T) { query := getBaseQuery() query.MetricName = "Test_MetricName1" query.Dimensions = map[string][]string{"Test_DimensionName1": {"Value1"}} - queries, err := executor.getDimensionValuesForWildcards(ctx, pluginCtx, "us-east-1", nil, []*models.CloudWatchQuery{query}, tagValueCache) + queries, err := executor.getDimensionValuesForWildcards(ctx, pluginCtx, "us-east-1", nil, []*models.CloudWatchQuery{query}, tagValueCache, 50) assert.Nil(t, err) assert.Len(t, queries, 1) assert.NotNil(t, queries[0].Dimensions["Test_DimensionName1"], 1) @@ -38,7 +38,7 @@ func TestGetDimensionValuesForWildcards(t *testing.T) { query := getBaseQuery() query.MetricName = "Test_MetricName1" query.Dimensions = map[string][]string{"Test_DimensionName1": {"*"}} - queries, err := executor.getDimensionValuesForWildcards(ctx, pluginCtx, "us-east-1", nil, []*models.CloudWatchQuery{query}, tagValueCache) + queries, err := executor.getDimensionValuesForWildcards(ctx, pluginCtx, "us-east-1", nil, []*models.CloudWatchQuery{query}, tagValueCache, 50) assert.Nil(t, err) assert.Len(t, queries, 1) assert.NotNil(t, queries[0].Dimensions["Test_DimensionName1"]) @@ -57,7 +57,7 @@ func TestGetDimensionValuesForWildcards(t *testing.T) { {MetricName: utils.Pointer("Test_MetricName4"), Dimensions: []*cloudwatch.Dimension{{Name: utils.Pointer("Test_DimensionName1"), Value: utils.Pointer("Value2")}}}, }} api.On("ListMetricsPagesWithContext").Return(nil) - queries, err := executor.getDimensionValuesForWildcards(ctx, pluginCtx, "us-east-1", api, []*models.CloudWatchQuery{query}, tagValueCache) + queries, err := executor.getDimensionValuesForWildcards(ctx, pluginCtx, "us-east-1", api, []*models.CloudWatchQuery{query}, tagValueCache, 50) assert.Nil(t, err) assert.Len(t, queries, 1) assert.Equal(t, map[string][]string{"Test_DimensionName1": {"Value1", "Value2", "Value3", "Value4"}}, queries[0].Dimensions) @@ -73,13 +73,13 @@ func TestGetDimensionValuesForWildcards(t *testing.T) { {MetricName: utils.Pointer("Test_MetricName"), Dimensions: []*cloudwatch.Dimension{{Name: utils.Pointer("Test_DimensionName"), Value: utils.Pointer("Value")}}}, }} api.On("ListMetricsPagesWithContext").Return(nil) - _, err := executor.getDimensionValuesForWildcards(ctx, pluginCtx, "us-east-1", api, []*models.CloudWatchQuery{query}, tagValueCache) + _, err := executor.getDimensionValuesForWildcards(ctx, pluginCtx, "us-east-1", api, []*models.CloudWatchQuery{query}, tagValueCache, 50) assert.Nil(t, err) // make sure the original query wasn't altered assert.Equal(t, map[string][]string{"Test_DimensionName": {"*"}}, query.Dimensions) //setting the api to nil confirms that it's using the cached value - queries, err := executor.getDimensionValuesForWildcards(ctx, pluginCtx, "us-east-1", nil, []*models.CloudWatchQuery{query}, tagValueCache) + queries, err := executor.getDimensionValuesForWildcards(ctx, pluginCtx, "us-east-1", nil, []*models.CloudWatchQuery{query}, tagValueCache, 50) assert.Nil(t, err) assert.Len(t, queries, 1) assert.Equal(t, map[string][]string{"Test_DimensionName": {"Value"}}, queries[0].Dimensions) @@ -93,7 +93,7 @@ func TestGetDimensionValuesForWildcards(t *testing.T) { query.MatchExact = false api := &mocks.MetricsAPI{Metrics: []*cloudwatch.Metric{}} api.On("ListMetricsPagesWithContext").Return(nil) - queries, err := executor.getDimensionValuesForWildcards(ctx, pluginCtx, "us-east-1", api, []*models.CloudWatchQuery{query}, tagValueCache) + queries, err := executor.getDimensionValuesForWildcards(ctx, pluginCtx, "us-east-1", api, []*models.CloudWatchQuery{query}, tagValueCache, 50) assert.Nil(t, err) assert.Len(t, queries, 1) // assert that the values was set to an empty array @@ -104,7 +104,7 @@ func TestGetDimensionValuesForWildcards(t *testing.T) { {MetricName: utils.Pointer("Test_MetricName"), Dimensions: []*cloudwatch.Dimension{{Name: utils.Pointer("Test_DimensionName2"), Value: utils.Pointer("Value")}}}, } api.On("ListMetricsPagesWithContext").Return(nil) - queries, err = executor.getDimensionValuesForWildcards(ctx, pluginCtx, "us-east-1", api, []*models.CloudWatchQuery{query}, tagValueCache) + queries, err = executor.getDimensionValuesForWildcards(ctx, pluginCtx, "us-east-1", api, []*models.CloudWatchQuery{query}, tagValueCache, 50) assert.Nil(t, err) assert.Len(t, queries, 1) assert.Equal(t, map[string][]string{"Test_DimensionName2": {"Value"}}, queries[0].Dimensions) diff --git a/pkg/tsdb/cloudwatch/time_series_query.go b/pkg/tsdb/cloudwatch/time_series_query.go index eafb9b11a8..c392694d61 100644 --- a/pkg/tsdb/cloudwatch/time_series_query.go +++ b/pkg/tsdb/cloudwatch/time_series_query.go @@ -97,7 +97,7 @@ func (e *cloudWatchExecutor) executeTimeSeriesQuery(ctx context.Context, req *ba } if features.IsEnabled(ctx, features.FlagCloudWatchWildCardDimensionValues) { - requestQueries, err = e.getDimensionValuesForWildcards(ctx, req.PluginContext, region, client, requestQueries, instance.tagValueCache) + requestQueries, err = e.getDimensionValuesForWildcards(ctx, req.PluginContext, region, client, requestQueries, instance.tagValueCache, instance.Settings.GrafanaSettings.ListMetricsPageLimit) if err != nil { return err } From 6a11bee6af91a7ff84068c7b7e672438af5ba83a Mon Sep 17 00:00:00 2001 From: Yuri Tseretyan Date: Mon, 26 Feb 2024 17:04:27 -0500 Subject: [PATCH 0203/1406] Alerting: Deprecate max_annotations_to_keep and max_annotation_age in [alerting] configuration section (#83266) * introduce new config section [unified_alerting.state_history.annotations] and deprecate settings in [alerting] Co-authored-by: brendamuir <100768211+brendamuir@users.noreply.github.com> --- conf/defaults.ini | 13 ++++++++++ conf/sample.ini | 13 ++++++++++ .../setup-grafana/configure-grafana/_index.md | 26 +++++++++++++++++-- pkg/setting/setting.go | 14 ++++++++-- 4 files changed, 62 insertions(+), 4 deletions(-) diff --git a/conf/defaults.ini b/conf/defaults.ini index 4457cee9db..f21b0ea8ee 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -1299,6 +1299,17 @@ loki_basic_auth_password = # ex. # mylabelkey = mylabelvalue +[unified_alerting.state_history.annotations] +# Controls retention of annotations automatically created while evaluating alert rules. +# Alert state history backend must be configured to be annotations (see setting [unified_alerting.state_history].backend). + +# Configures how long alert annotations are stored for. Default is 0, which keeps them forever. +# This setting should be expressed as a duration. Ex 6h (hours), 10d (days), 2w (weeks), 1M (month). +max_age = + +# Configures max number of alert annotations that Grafana stores. Default value is 0, which keeps all alert annotations. +max_annotations_to_keep = + [unified_alerting.upgrade] # If set to true when upgrading from legacy alerting to Unified Alerting, grafana will first delete all existing # Unified Alerting resources, thus re-upgrading all organizations from scratch. If false or unset, organizations that @@ -1360,9 +1371,11 @@ min_interval_seconds = 1 # Configures for how long alert annotations are stored. Default is 0, which keeps them forever. # This setting should be expressed as an duration. Ex 6h (hours), 10d (days), 2w (weeks), 1M (month). +# Deprecated, use [annotations.alerting].max_age instead max_annotation_age = # Configures max number of alert annotations that Grafana stores. Default value is 0, which keeps all alert annotations. +# Deprecated, use [annotations.alerting].max_annotations_to_keep instead max_annotations_to_keep = #################################### Annotations ######################### diff --git a/conf/sample.ini b/conf/sample.ini index ad4f8569fd..e153c17c2a 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -1194,6 +1194,17 @@ # Any number of label key-value-pairs can be provided. ; mylabelkey = mylabelvalue +[unified_alerting.state_history.annotations] +# This section controls retention of annotations automatically created while evaluating alert rules +# when alerting state history backend is configured to be annotations (a setting [unified_alerting.state_history].backend + +# Configures for how long alert annotations are stored. Default is 0, which keeps them forever. +# This setting should be expressed as an duration. Ex 6h (hours), 10d (days), 2w (weeks), 1M (month). +max_age = + +# Configures max number of alert annotations that Grafana stores. Default value is 0, which keeps all alert annotations. +max_annotations_to_keep = + [unified_alerting.upgrade] # If set to true when upgrading from legacy alerting to Unified Alerting, grafana will first delete all existing # Unified Alerting resources, thus re-upgrading all organizations from scratch. If false or unset, organizations that @@ -1233,9 +1244,11 @@ # Configures for how long alert annotations are stored. Default is 0, which keeps them forever. # This setting should be expressed as a duration. Examples: 6h (hours), 10d (days), 2w (weeks), 1M (month). +# Deprecated, use [annotations.alerting].max_age instead ;max_annotation_age = # Configures max number of alert annotations that Grafana stores. Default value is 0, which keeps all alert annotations. +# Deprecated, use [annotations.alerting].max_annotations_to_keep instead ;max_annotations_to_keep = #################################### Annotations ######################### diff --git a/docs/sources/setup-grafana/configure-grafana/_index.md b/docs/sources/setup-grafana/configure-grafana/_index.md index 30a55bf5ab..a7ddce0e8c 100644 --- a/docs/sources/setup-grafana/configure-grafana/_index.md +++ b/docs/sources/setup-grafana/configure-grafana/_index.md @@ -1654,6 +1654,20 @@ For example: `disabled_labels=grafana_folder`
+## [unified_alerting.state_history.annotations] + +This section controls retention of annotations automatically created while evaluating alert rules when alerting state history backend is configured to be annotations (see setting [unified_alerting.state_history].backend) + +### max_age + +Configures for how long alert annotations are stored. Default is 0, which keeps them forever. This setting should be expressed as an duration. Ex 6h (hours), 10d (days), 2w (weeks), 1M (month). + +### max_annotations_to_keep + +Configures max number of alert annotations that Grafana stores. Default value is 0, which keeps all alert annotations. + +
+ ## [unified_alerting.upgrade] For more information about upgrading to Grafana Alerting, refer to [Upgrade Alerting](/docs/grafana/next/alerting/set-up/migrating-alerts/). @@ -1713,12 +1727,20 @@ Sets the minimum interval between rule evaluations. Default value is `1`. > **Note.** This setting has precedence over each individual rule frequency. If a rule frequency is lower than this value, then this value is enforced. -### max_annotation_age = +### max_annotation_age + +{{% admonition type="note" %}} +This option is deprecated - See `max_age` option in [unified_alerting.state_history.annotations]({{< relref "#unified_alertingstate_historyannotations" >}}) instead. +{{% /admonition %}} Configures for how long alert annotations are stored. Default is 0, which keeps them forever. This setting should be expressed as a duration. Examples: 6h (hours), 10d (days), 2w (weeks), 1M (month). -### max_annotations_to_keep = +### max_annotations_to_keep + +{{% admonition type="note" %}} +This option is deprecated - See `max_annotations_to_keep` option in [unified_alerting.state_history.annotations]({{< relref "#unified_alertingstate_historyannotations" >}}) instead. +{{% /admonition %}} Configures max number of alert annotations that Grafana stores. Default value is 0, which keeps all alert annotations. diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index b4d946569e..9a25a70bad 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -702,7 +702,6 @@ func (cfg *Cfg) readAnnotationSettings() error { dashboardAnnotation := cfg.Raw.Section("annotations.dashboard") apiIAnnotation := cfg.Raw.Section("annotations.api") - alertingSection := cfg.Raw.Section("alerting") var newAnnotationCleanupSettings = func(section *ini.Section, maxAgeField string) AnnotationCleanupSettings { maxAge, err := gtime.ParseDuration(section.Key(maxAgeField).MustString("")) @@ -716,7 +715,18 @@ func (cfg *Cfg) readAnnotationSettings() error { } } - cfg.AlertingAnnotationCleanupSetting = newAnnotationCleanupSettings(alertingSection, "max_annotation_age") + alertingAnnotations := cfg.Raw.Section("unified_alerting.state_history.annotations") + if alertingAnnotations.Key("max_age").Value() == "" && section.Key("max_annotations_to_keep").Value() == "" { + alertingSection := cfg.Raw.Section("alerting") + cleanup := newAnnotationCleanupSettings(alertingSection, "max_annotation_age") + if cleanup.MaxCount > 0 || cleanup.MaxAge > 0 { + cfg.Logger.Warn("settings 'max_annotations_to_keep' and 'max_annotation_age' in section [alerting] are deprecated. Please use settings 'max_annotations_to_keep' and 'max_age' in section [unified_alerting.state_history.annotations]") + } + cfg.AlertingAnnotationCleanupSetting = cleanup + } else { + cfg.AlertingAnnotationCleanupSetting = newAnnotationCleanupSettings(alertingAnnotations, "max_age") + } + cfg.DashboardAnnotationCleanupSettings = newAnnotationCleanupSettings(dashboardAnnotation, "max_age") cfg.APIAnnotationCleanupSettings = newAnnotationCleanupSettings(apiIAnnotation, "max_age") From f2a21f383190d9e48e9aafc66a1aefbf507bb1bd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 27 Feb 2024 00:55:27 +0200 Subject: [PATCH 0204/1406] Update dependency @types/react-window-infinite-loader to v1.0.9 (#83445) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 9a01e017ff..3f8adaa023 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9892,12 +9892,12 @@ __metadata: linkType: hard "@types/react-window-infinite-loader@npm:^1": - version: 1.0.6 - resolution: "@types/react-window-infinite-loader@npm:1.0.6" + version: 1.0.9 + resolution: "@types/react-window-infinite-loader@npm:1.0.9" dependencies: "@types/react": "npm:*" "@types/react-window": "npm:*" - checksum: 10/d4648dfb44614e4f0137d7b77eb1868b0c5252f451a78edfc4520e508157ce7687d4b7d9efd6df8f01e72e0d92224338b8c8d934220f32a3081b528599a25829 + checksum: 10/9f2c27f24bfa726ceaef6612a4adbda745f3455c877193f68dfa48591274c670a6df4fa6870785cff5f948e289ceb9a247fb7cbf67e3cd555ab16d11866fd63f languageName: node linkType: hard From 77f9d29291733ec0e6d55a4a65b915c2f309080c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 26 Feb 2024 22:58:09 +0000 Subject: [PATCH 0205/1406] Update dependency @types/semver to v7.5.8 --- package.json | 2 +- packages/grafana-prometheus/package.json | 2 +- public/app/plugins/datasource/tempo/package.json | 2 +- yarn.lock | 14 +++++++------- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 0cfa75b5e2..cc2c3b79b2 100644 --- a/package.json +++ b/package.json @@ -137,7 +137,7 @@ "@types/react-window": "1.8.8", "@types/react-window-infinite-loader": "^1", "@types/redux-mock-store": "1.0.6", - "@types/semver": "7.5.7", + "@types/semver": "7.5.8", "@types/slate": "0.47.11", "@types/slate-plain-serializer": "0.7.5", "@types/slate-react": "0.22.9", diff --git a/packages/grafana-prometheus/package.json b/packages/grafana-prometheus/package.json index 50a214459f..1a62bcd7ff 100644 --- a/packages/grafana-prometheus/package.json +++ b/packages/grafana-prometheus/package.json @@ -102,7 +102,7 @@ "@types/react-dom": "18.2.19", "@types/react-highlight-words": "0.16.7", "@types/react-window": "1.8.8", - "@types/semver": "7.5.7", + "@types/semver": "7.5.8", "@types/testing-library__jest-dom": "5.14.9", "@types/uuid": "9.0.8", "@typescript-eslint/eslint-plugin": "6.21.0", diff --git a/public/app/plugins/datasource/tempo/package.json b/public/app/plugins/datasource/tempo/package.json index 14ed7e4f36..a3b7fa7c6d 100644 --- a/public/app/plugins/datasource/tempo/package.json +++ b/public/app/plugins/datasource/tempo/package.json @@ -48,7 +48,7 @@ "@types/prismjs": "1.26.3", "@types/react": "18.2.59", "@types/react-dom": "18.2.19", - "@types/semver": "7.5.7", + "@types/semver": "7.5.8", "@types/uuid": "9.0.8", "glob": "10.3.10", "react-select-event": "5.5.1", diff --git a/yarn.lock b/yarn.lock index 3f8adaa023..2b3cb32593 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3454,7 +3454,7 @@ __metadata: "@types/prismjs": "npm:1.26.3" "@types/react": "npm:18.2.59" "@types/react-dom": "npm:18.2.19" - "@types/semver": "npm:7.5.7" + "@types/semver": "npm:7.5.8" "@types/uuid": "npm:9.0.8" buffer: "npm:6.0.3" events: "npm:3.3.0" @@ -3959,7 +3959,7 @@ __metadata: "@types/react-dom": "npm:18.2.19" "@types/react-highlight-words": "npm:0.16.7" "@types/react-window": "npm:1.8.8" - "@types/semver": "npm:7.5.7" + "@types/semver": "npm:7.5.8" "@types/testing-library__jest-dom": "npm:5.14.9" "@types/uuid": "npm:9.0.8" "@typescript-eslint/eslint-plugin": "npm:6.21.0" @@ -9971,10 +9971,10 @@ __metadata: languageName: node linkType: hard -"@types/semver@npm:7.5.7, @types/semver@npm:^7.3.12, @types/semver@npm:^7.3.4, @types/semver@npm:^7.5.0": - version: 7.5.7 - resolution: "@types/semver@npm:7.5.7" - checksum: 10/535d88ec577fe59e38211881f79a1e2ba391e9e1516f8fff74e7196a5ba54315bace9c67a4616c334c830c89027d70a9f473a4ceb634526086a9da39180f2f9a +"@types/semver@npm:7.5.8, @types/semver@npm:^7.3.12, @types/semver@npm:^7.3.4, @types/semver@npm:^7.5.0": + version: 7.5.8 + resolution: "@types/semver@npm:7.5.8" + checksum: 10/3496808818ddb36deabfe4974fd343a78101fa242c4690044ccdc3b95dcf8785b494f5d628f2f47f38a702f8db9c53c67f47d7818f2be1b79f2efb09692e1178 languageName: node linkType: hard @@ -18428,7 +18428,7 @@ __metadata: "@types/react-window": "npm:1.8.8" "@types/react-window-infinite-loader": "npm:^1" "@types/redux-mock-store": "npm:1.0.6" - "@types/semver": "npm:7.5.7" + "@types/semver": "npm:7.5.8" "@types/slate": "npm:0.47.11" "@types/slate-plain-serializer": "npm:0.7.5" "@types/slate-react": "npm:0.22.9" From d4a932ae33c3e590c6bca7a0e1fcedbc0364d409 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 26 Feb 2024 23:36:04 +0000 Subject: [PATCH 0206/1406] Update dependency dompurify to v3.0.9 --- yarn.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/yarn.lock b/yarn.lock index 2b3cb32593..97728e69e0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15415,16 +15415,16 @@ __metadata: linkType: hard "dompurify@npm:^2.2.0": - version: 2.4.5 - resolution: "dompurify@npm:2.4.5" - checksum: 10/d764c2ff126b3749dad35bc34eed40f51141d7dfd620e938c92f08d68c32beeb259d06abadeee91f6e2a8c8737ce670e2124ac9a257ba3bcdc666598cebcde01 + version: 2.4.7 + resolution: "dompurify@npm:2.4.7" + checksum: 10/bf223b4608204b0f4ded4cad2e7711b9afbe4dc9646f645601463629484a6ccc83906571d24340c0df7776a147ceb6d42cc36697e514aa72c865662977164784 languageName: node linkType: hard "dompurify@npm:^3.0.0": - version: 3.0.8 - resolution: "dompurify@npm:3.0.8" - checksum: 10/671fa18bd4bcb1a6ff2e59ecf919f807615b551e7add8834b27751d4e0f3d754a67725482d1efdd259317cadcaaccb72a8afc3aba829ac59730e760041591a1a + version: 3.0.9 + resolution: "dompurify@npm:3.0.9" + checksum: 10/cfb8ed92672e7ddfa43a9ce5bfcd4b3c91287454402672da930b0ecfc8c86d0d2133116607e6c7c77a07ddd8c6baec6d11fa07d9fddebd8701572e3cace2ecea languageName: node linkType: hard From 4db30754a691a56fb539192e48f3ce4f9f70b289 Mon Sep 17 00:00:00 2001 From: Leon Sorokin Date: Mon, 26 Feb 2024 18:22:36 -0600 Subject: [PATCH 0207/1406] Graph (old): Migrate right y axis to TimeSeries via custom.axisPlacement (#83452) --- .../panel/timeseries/__snapshots__/migrations.test.ts.snap | 4 ++++ public/app/plugins/panel/timeseries/migrations.ts | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/public/app/plugins/panel/timeseries/__snapshots__/migrations.test.ts.snap b/public/app/plugins/panel/timeseries/__snapshots__/migrations.test.ts.snap index 8c4bec6a30..272f0ec96d 100644 --- a/public/app/plugins/panel/timeseries/__snapshots__/migrations.test.ts.snap +++ b/public/app/plugins/panel/timeseries/__snapshots__/migrations.test.ts.snap @@ -656,6 +656,10 @@ exports[`Graph Migrations twoYAxis 1`] = ` "id": "max", "value": 25, }, + { + "id": "custom.axisPlacement", + "value": "right", + }, { "id": "custom.axisLabel", "value": "Y222", diff --git a/public/app/plugins/panel/timeseries/migrations.ts b/public/app/plugins/panel/timeseries/migrations.ts index 18c72cb83b..7acc221794 100644 --- a/public/app/plugins/panel/timeseries/migrations.ts +++ b/public/app/plugins/panel/timeseries/migrations.ts @@ -658,6 +658,11 @@ function fillY2DynamicValues( } } + props.push({ + id: `custom.axisPlacement`, + value: AxisPlacement.Right, + }); + // Add any custom property const y1G = y1.custom ?? {}; const y2G = y2.custom ?? {}; From 99271441fb9a8a56c839e03302a784dbf6762720 Mon Sep 17 00:00:00 2001 From: Adela Almasan <88068998+adela-almasan@users.noreply.github.com> Date: Mon, 26 Feb 2024 18:35:45 -0600 Subject: [PATCH 0208/1406] Legend: Allow item copy in Table mode (#83319) --- .../grafana-ui/src/components/VizLegend/VizLegendTableItem.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/grafana-ui/src/components/VizLegend/VizLegendTableItem.tsx b/packages/grafana-ui/src/components/VizLegend/VizLegendTableItem.tsx index 41321b6152..5a83f4908f 100644 --- a/packages/grafana-ui/src/components/VizLegend/VizLegendTableItem.tsx +++ b/packages/grafana-ui/src/components/VizLegend/VizLegendTableItem.tsx @@ -126,6 +126,7 @@ const getStyles = (theme: GrafanaTheme2) => { maxWidth: '600px', textOverflow: 'ellipsis', overflow: 'hidden', + userSelect: 'text', }), labelDisabled: css({ label: 'LegendLabelDisabled', From 2ed8201f256831217e270e7753bb234f8b9d78dd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 27 Feb 2024 00:07:30 +0000 Subject: [PATCH 0209/1406] Update dependency nanoid to v5.0.6 --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 97728e69e0..66e90effd7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -23216,11 +23216,11 @@ __metadata: linkType: hard "nanoid@npm:^5.0.4": - version: 5.0.4 - resolution: "nanoid@npm:5.0.4" + version: 5.0.6 + resolution: "nanoid@npm:5.0.6" bin: nanoid: bin/nanoid.js - checksum: 10/cf09cca3774f3147100948f7478f75f4c9ee97a4af65c328dd9abbd83b12f8bb35cf9f89a21c330f3b759d667a4cd0140ed84aa5fdd522c61e0d341aeaa7fb6f + checksum: 10/cd5d3eebd3b148b68b4b0238d94b1d8b4d955cc1a74b8e5217c1daecaed584d4b3701f41ce0f5e909ba4cd214592aff41fb53ac1955d77ea85d58df936726f29 languageName: node linkType: hard From 2c3596854f21e20d5cc12eea4f2c78462c6221fb Mon Sep 17 00:00:00 2001 From: Leon Sorokin Date: Mon, 26 Feb 2024 19:18:40 -0600 Subject: [PATCH 0210/1406] BarChart: TooltipPlugin2 (#80920) Co-authored-by: Adela Almasan --- .../src/components/VizTooltip/utils.ts | 6 ++- .../plugins/panel/barchart/BarChartPanel.tsx | 37 +++++++++++++++++- .../barchart/__snapshots__/utils.test.ts.snap | 24 ++++++++---- public/app/plugins/panel/barchart/bars.ts | 38 +++++++++++++------ public/app/plugins/panel/barchart/module.tsx | 1 + public/app/plugins/panel/barchart/quadtree.ts | 16 +++----- public/app/plugins/panel/barchart/utils.ts | 19 +++++++++- public/app/plugins/panel/timeseries/utils.ts | 2 +- 8 files changed, 107 insertions(+), 36 deletions(-) diff --git a/packages/grafana-ui/src/components/VizTooltip/utils.ts b/packages/grafana-ui/src/components/VizTooltip/utils.ts index 24d81a57c4..7bbc0795b6 100644 --- a/packages/grafana-ui/src/components/VizTooltip/utils.ts +++ b/packages/grafana-ui/src/components/VizTooltip/utils.ts @@ -118,8 +118,12 @@ export const getContentItems = ( const v = fields[i].values[dataIdx]; - // no value -> zero? + if (v == null && field.config.noValue == null) { + continue; + } + const display = field.display!(v); // super expensive :( + // sort NaN and non-numeric to bottom (regardless of sort order) const numeric = !Number.isNaN(display.numeric) ? display.numeric diff --git a/public/app/plugins/panel/barchart/BarChartPanel.tsx b/public/app/plugins/panel/barchart/BarChartPanel.tsx index 27d7a366e1..b5d2b1ad4b 100644 --- a/public/app/plugins/panel/barchart/BarChartPanel.tsx +++ b/public/app/plugins/panel/barchart/BarChartPanel.tsx @@ -12,7 +12,7 @@ import { TimeRange, VizOrientation, } from '@grafana/data'; -import { PanelDataErrorView } from '@grafana/runtime'; +import { PanelDataErrorView, config } from '@grafana/runtime'; import { SortOrder } from '@grafana/schema'; import { GraphGradientMode, @@ -28,13 +28,17 @@ import { VizLayout, VizLegend, VizTooltipContainer, + TooltipPlugin2, } from '@grafana/ui'; import { HoverEvent, addTooltipSupport } from '@grafana/ui/src/components/uPlot/config/addTooltipSupport'; +import { TooltipHoverMode } from '@grafana/ui/src/components/uPlot/plugins/TooltipPlugin2'; import { CloseButton } from 'app/core/components/CloseButton/CloseButton'; import { GraphNG, GraphNGProps, PropDiffFn } from 'app/core/components/GraphNG/GraphNG'; import { getFieldLegendItem } from 'app/core/components/TimelineChart/utils'; import { DataHoverView } from 'app/features/visualization/data-hover/DataHoverView'; +import { TimeSeriesTooltip } from '../timeseries/TimeSeriesTooltip'; + import { Options } from './panelcfg.gen'; import { prepareBarChartDisplayValues, preparePlotConfigBuilder } from './utils'; @@ -302,9 +306,12 @@ export const BarChartPanel = ({ data, options, fieldConfig, width, height, timeZ fillOpacity, allFrames: info.viz, fullHighlight, + hoverMulti: tooltip.mode === TooltipDisplayMode.Multi, }); }; + const showNewVizTooltips = Boolean(config.featureToggles.newVizTooltips); + return ( {(config) => { - if (oldConfig.current !== config) { + if (showNewVizTooltips && options.tooltip.mode !== TooltipDisplayMode.None) { + return ( + { + return ( + + ); + }} + maxWidth={options.tooltip.maxWidth} + maxHeight={options.tooltip.maxHeight} + /> + ); + } + + if (!showNewVizTooltips && oldConfig.current !== config) { oldConfig.current = addTooltipSupport({ config, onUPlotClick, diff --git a/public/app/plugins/panel/barchart/__snapshots__/utils.test.ts.snap b/public/app/plugins/panel/barchart/__snapshots__/utils.test.ts.snap index e8513d91a0..460a05d35c 100644 --- a/public/app/plugins/panel/barchart/__snapshots__/utils.test.ts.snap +++ b/public/app/plugins/panel/barchart/__snapshots__/utils.test.ts.snap @@ -68,7 +68,8 @@ exports[`BarChart utils preparePlotConfigBuilder orientation 1`] = ` "y": false, }, "focus": { - "prox": 30, + "dist": [Function], + "prox": 1000, }, "points": { "bbox": [Function], @@ -223,7 +224,8 @@ exports[`BarChart utils preparePlotConfigBuilder orientation 2`] = ` "y": false, }, "focus": { - "prox": 30, + "dist": [Function], + "prox": 1000, }, "points": { "bbox": [Function], @@ -378,7 +380,8 @@ exports[`BarChart utils preparePlotConfigBuilder orientation 3`] = ` "y": false, }, "focus": { - "prox": 30, + "dist": [Function], + "prox": 1000, }, "points": { "bbox": [Function], @@ -533,7 +536,8 @@ exports[`BarChart utils preparePlotConfigBuilder stacking 1`] = ` "y": false, }, "focus": { - "prox": 30, + "dist": [Function], + "prox": 1000, }, "points": { "bbox": [Function], @@ -688,7 +692,8 @@ exports[`BarChart utils preparePlotConfigBuilder stacking 2`] = ` "y": false, }, "focus": { - "prox": 30, + "dist": [Function], + "prox": 1000, }, "points": { "bbox": [Function], @@ -843,7 +848,8 @@ exports[`BarChart utils preparePlotConfigBuilder stacking 3`] = ` "y": false, }, "focus": { - "prox": 30, + "dist": [Function], + "prox": 1000, }, "points": { "bbox": [Function], @@ -998,7 +1004,8 @@ exports[`BarChart utils preparePlotConfigBuilder value visibility 1`] = ` "y": false, }, "focus": { - "prox": 30, + "dist": [Function], + "prox": 1000, }, "points": { "bbox": [Function], @@ -1153,7 +1160,8 @@ exports[`BarChart utils preparePlotConfigBuilder value visibility 2`] = ` "y": false, }, "focus": { - "prox": 30, + "dist": [Function], + "prox": 1000, }, "points": { "bbox": [Function], diff --git a/public/app/plugins/panel/barchart/bars.ts b/public/app/plugins/panel/barchart/bars.ts index 9cc562781f..541d5c3f30 100644 --- a/public/app/plugins/panel/barchart/bars.ts +++ b/public/app/plugins/panel/barchart/bars.ts @@ -15,7 +15,7 @@ import { formatTime } from '@grafana/ui/src/components/uPlot/config/UPlotAxisBui import { StackingGroup, preparePlotData2 } from '@grafana/ui/src/components/uPlot/utils'; import { distribute, SPACE_BETWEEN } from './distribute'; -import { intersects, pointWithin, Quadtree, Rect } from './quadtree'; +import { findRects, intersects, pointWithin, Quadtree, Rect } from './quadtree'; const groupDistr = SPACE_BETWEEN; const barDistr = SPACE_BETWEEN; @@ -56,6 +56,7 @@ export interface BarsOptions { text?: VizTextDisplayOptions; onHover?: (seriesIdx: number, valueIdx: number) => void; onLeave?: (seriesIdx: number, valueIdx: number) => void; + hoverMulti?: boolean; legend?: VizLegendOptions; xSpacing?: number; xTimeAuto?: boolean; @@ -128,6 +129,7 @@ export function getConfig(opts: BarsOptions, theme: GrafanaTheme2) { fillOpacity = 1, showValue, xSpacing = 0, + hoverMulti = false, } = opts; const isXHorizontal = xOri === ScaleOrientation.Horizontal; const hasAutoValueSize = !Boolean(opts.text?.valueSize); @@ -141,6 +143,8 @@ export function getConfig(opts: BarsOptions, theme: GrafanaTheme2) { } let qt: Quadtree; + const numSeries = 30; // !! + const hovered: Array = Array(numSeries).fill(null); let hRect: Rect | null; // for distr: 2 scales, the splits array should contain indices into data[0] rather than values @@ -324,7 +328,7 @@ export function getConfig(opts: BarsOptions, theme: GrafanaTheme2) { let barRect = { x: lft, y: top, w: wid, h: hgt, sidx: seriesIdx, didx: dataIdx }; - if (opts.fullHighlight) { + if (!isStacked && opts.fullHighlight) { if (opts.xOri === ScaleOrientation.Horizontal) { barRect.y = 0; barRect.h = u.bbox.height; @@ -443,8 +447,6 @@ export function getConfig(opts: BarsOptions, theme: GrafanaTheme2) { }); const init = (u: uPlot) => { - let over = u.over; - over.style.overflow = 'hidden'; u.root.querySelectorAll('.u-cursor-pt').forEach((el) => { el.style.borderRadius = '0'; @@ -462,7 +464,8 @@ export function getConfig(opts: BarsOptions, theme: GrafanaTheme2) { y: false, }, dataIdx: (u, seriesIdx) => { - if (seriesIdx === 1) { + if (seriesIdx === 0) { + hovered.fill(null); hRect = null; let cx = u.cursor.left! * uPlot.pxRatio; @@ -470,26 +473,37 @@ export function getConfig(opts: BarsOptions, theme: GrafanaTheme2) { qt.get(cx, cy, 1, 1, (o) => { if (pointWithin(cx, cy, o.x, o.y, o.x + o.w, o.y + o.h)) { - hRect = o; + hRect = hovered[0] = o; + hovered[hRect.sidx] = hRect; + + hoverMulti && + findRects(qt, undefined, hRect.didx).forEach((r) => { + hovered[r.sidx] = r; + }); } }); } - return hRect && seriesIdx === hRect.sidx ? hRect.didx : null; + return hovered[seriesIdx]?.didx; }, points: { fill: 'rgba(255,255,255,0.4)', bbox: (u, seriesIdx) => { - let isHovered = hRect && seriesIdx === hRect.sidx; + let hRect2 = hovered[seriesIdx]; + let isHovered = hRect2 != null; return { - left: isHovered ? hRect!.x / uPlot.pxRatio : -10, - top: isHovered ? hRect!.y / uPlot.pxRatio : -10, - width: isHovered ? hRect!.w / uPlot.pxRatio : 0, - height: isHovered ? hRect!.h / uPlot.pxRatio : 0, + left: isHovered ? hRect2!.x / uPlot.pxRatio : -10, + top: isHovered ? hRect2!.y / uPlot.pxRatio : -10, + width: isHovered ? hRect2!.w / uPlot.pxRatio : 0, + height: isHovered ? hRect2!.h / uPlot.pxRatio : 0, }; }, }, + focus: { + prox: 1e3, + dist: (u, seriesIdx) => (hRect?.sidx === seriesIdx ? 0 : Infinity), + }, }; // Build bars diff --git a/public/app/plugins/panel/barchart/module.tsx b/public/app/plugins/panel/barchart/module.tsx index eb61ddc0c5..21e23f6f60 100644 --- a/public/app/plugins/panel/barchart/module.tsx +++ b/public/app/plugins/panel/barchart/module.tsx @@ -225,6 +225,7 @@ export const plugin = new PanelPlugin(BarChartPanel) path: 'fullHighlight', name: 'Highlight full area on hover', defaultValue: defaultOptions.fullHighlight, + showIf: (c) => c.stacking === StackingMode.None, }); builder.addFieldNamePicker({ diff --git a/public/app/plugins/panel/barchart/quadtree.ts b/public/app/plugins/panel/barchart/quadtree.ts index be6be6b4d7..26850a9a8e 100644 --- a/public/app/plugins/panel/barchart/quadtree.ts +++ b/public/app/plugins/panel/barchart/quadtree.ts @@ -14,24 +14,20 @@ export function pointWithin(px: number, py: number, rlft: number, rtop: number, /** * @internal */ -export function findRect(qt: Quadtree, sidx: number, didx: number): Rect | undefined { - let out: Rect | undefined; +export function findRects(qt: Quadtree, sidx?: number, didx?: number) { + let rects: Rect[] = []; if (qt.o.length) { - out = qt.o.find((rect) => rect.sidx === sidx && rect.didx === didx); + rects.push(...qt.o.filter((rect) => (sidx == null || rect.sidx === sidx) && (didx == null || rect.didx === didx))); } - if (out == null && qt.q) { + if (qt.q) { for (let i = 0; i < qt.q.length; i++) { - out = findRect(qt.q[i], sidx, didx); - - if (out) { - break; - } + rects.push(...findRects(qt.q[i], sidx, didx)); } } - return out; + return rects; } /** diff --git a/public/app/plugins/panel/barchart/utils.ts b/public/app/plugins/panel/barchart/utils.ts index 62f342ce08..e170ead77b 100644 --- a/public/app/plugins/panel/barchart/utils.ts +++ b/public/app/plugins/panel/barchart/utils.ts @@ -17,6 +17,7 @@ import { getFieldDisplayName, } from '@grafana/data'; import { maybeSortFrame } from '@grafana/data/src/transformations/transformers/joinDataFrames'; +import { config as runtimeConfig } from '@grafana/runtime'; import { AxisColorMode, AxisPlacement, @@ -33,6 +34,8 @@ import { AxisProps } from '@grafana/ui/src/components/uPlot/config/UPlotAxisBuil import { getStackingGroups } from '@grafana/ui/src/components/uPlot/utils'; import { findField } from 'app/features/dimensions'; +import { setClassicPaletteIdxs } from '../timeseries/utils'; + import { BarsOptions, getConfig } from './bars'; import { FieldConfig, Options, defaultFieldConfig } from './panelcfg.gen'; import { BarChartDisplayValues, BarChartDisplayWarning } from './types'; @@ -60,6 +63,7 @@ export interface BarChartOptionsEX extends Options { getColor?: (seriesIdx: number, valueIdx: number, value: unknown) => string | null; timeZone?: TimeZone; fillOpacity?: number; + hoverMulti?: boolean; } export const preparePlotConfigBuilder: UPlotConfigPrepFn = ({ @@ -82,6 +86,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn = ({ legend, timeZone, fullHighlight, + hoverMulti, }) => { const builder = new UPlotConfigBuilder(); @@ -122,6 +127,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn = ({ xTimeAuto: frame.fields[0]?.type === FieldType.time && !frame.fields[0].config.unit?.startsWith('time:'), negY: frame.fields.map((f) => f.config.custom?.transform === GraphTransform.NegativeY), fullHighlight, + hoverMulti, }; const config = getConfig(opts, theme); @@ -132,7 +138,8 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn = ({ builder.addHook('drawClear', config.drawClear); builder.addHook('draw', config.draw); - builder.setTooltipInterpolator(config.interpolateTooltip); + const showNewVizTooltips = Boolean(runtimeConfig.featureToggles.newVizTooltips); + !showNewVizTooltips && builder.setTooltipInterpolator(config.interpolateTooltip); if (xTickLabelRotation !== 0) { // these are the amount of space we already have available between plot edge and first label @@ -389,7 +396,8 @@ export function prepareBarChartDisplayValues( series[0], series[0].fields.findIndex((f) => f.type === FieldType.time) ) - : outerJoinDataFrames({ frames: series }); + : outerJoinDataFrames({ frames: series, keepDisplayNames: true }); + if (!frame) { return { warn: 'Unable to join data' }; } @@ -478,6 +486,13 @@ export function prepareBarChartDisplayValues( }; } + // if both string and time fields exist, remove unused leftover time field + if (frame.fields[0].type === FieldType.time && frame.fields[0] !== firstField) { + frame.fields.shift(); + } + + setClassicPaletteIdxs([frame], theme, 0); + if (!fields.length) { return { warn: 'No numeric fields found', diff --git a/public/app/plugins/panel/timeseries/utils.ts b/public/app/plugins/panel/timeseries/utils.ts index 73d74ec00c..a7d24f820e 100644 --- a/public/app/plugins/panel/timeseries/utils.ts +++ b/public/app/plugins/panel/timeseries/utils.ts @@ -242,7 +242,7 @@ const matchEnumColorToSeriesColor = (frames: DataFrame[], theme: GrafanaTheme2) } }; -const setClassicPaletteIdxs = (frames: DataFrame[], theme: GrafanaTheme2, skipFieldIdx?: number) => { +export const setClassicPaletteIdxs = (frames: DataFrame[], theme: GrafanaTheme2, skipFieldIdx?: number) => { let seriesIndex = 0; frames.forEach((frame) => { frame.fields.forEach((field, fieldIdx) => { From 93fef224ae5d28100a47b37338c9ff18a52d0dc9 Mon Sep 17 00:00:00 2001 From: Leon Sorokin Date: Mon, 26 Feb 2024 19:41:39 -0600 Subject: [PATCH 0211/1406] TimeSeries: Don't re-init chart with bars style on data updates (#83355) --- .../transformers/joinDataFrames.ts | 17 +++++++---- .../app/core/components/GraphNG/utils.test.ts | 2 -- public/app/core/components/GraphNG/utils.ts | 28 ++++++++----------- 3 files changed, 23 insertions(+), 24 deletions(-) diff --git a/packages/grafana-data/src/transformations/transformers/joinDataFrames.ts b/packages/grafana-data/src/transformations/transformers/joinDataFrames.ts index 121a039181..a03edb78f0 100644 --- a/packages/grafana-data/src/transformations/transformers/joinDataFrames.ts +++ b/packages/grafana-data/src/transformations/transformers/joinDataFrames.ts @@ -61,6 +61,11 @@ export interface JoinOptions { */ keepDisplayNames?: boolean; + /** + * @internal -- Optionally specify how to treat null values + */ + nullMode?: (field: Field) => JoinNullMode; + /** * @internal -- Optionally specify a join mode (outer or inner) */ @@ -95,6 +100,9 @@ export function joinDataFrames(options: JoinOptions): DataFrame | undefined { return; } + const nullMode = + options.nullMode ?? ((field: Field) => (field.config.custom?.spanNulls === true ? NULL_REMOVE : NULL_EXPAND)); + if (options.frames.length === 1) { let frame = options.frames[0]; let frameCopy = frame; @@ -186,8 +194,7 @@ export function joinDataFrames(options: JoinOptions): DataFrame | undefined { } // Support the standard graph span nulls field config - let spanNulls = field.config.custom?.spanNulls; - nullModesFrame.push(spanNulls === true ? NULL_REMOVE : spanNulls === -1 ? NULL_RETAIN : NULL_EXPAND); + nullModesFrame.push(nullMode(field)); let labels = field.labels ?? {}; let name = field.name; @@ -374,9 +381,9 @@ export type AlignedData = | [xValues: number[] | TypedArray, ...yValues: Array | TypedArray>]; // nullModes -const NULL_REMOVE = 0; // nulls are converted to undefined (e.g. for spanGaps: true) -const NULL_RETAIN = 1; // nulls are retained, with alignment artifacts set to undefined (default) -const NULL_EXPAND = 2; // nulls are expanded to include any adjacent alignment artifacts +export const NULL_REMOVE = 0; // nulls are converted to undefined (e.g. for spanGaps: true) +export const NULL_RETAIN = 1; // nulls are retained, with alignment artifacts set to undefined (default) +export const NULL_EXPAND = 2; // nulls are expanded to include any adjacent alignment artifacts type JoinNullMode = number; // NULL_IGNORE | NULL_RETAIN | NULL_EXPAND; diff --git a/public/app/core/components/GraphNG/utils.test.ts b/public/app/core/components/GraphNG/utils.test.ts index 9675cd7ca5..485384ab2a 100644 --- a/public/app/core/components/GraphNG/utils.test.ts +++ b/public/app/core/components/GraphNG/utils.test.ts @@ -353,7 +353,6 @@ describe('GraphNG utils', () => { "config": { "custom": { "drawStyle": "bars", - "spanNulls": -1, }, }, "labels": { @@ -386,7 +385,6 @@ describe('GraphNG utils', () => { "config": { "custom": { "drawStyle": "bars", - "spanNulls": -1, }, }, "labels": { diff --git a/public/app/core/components/GraphNG/utils.ts b/public/app/core/components/GraphNG/utils.ts index 64cd07e1b6..dc49d3abe3 100644 --- a/public/app/core/components/GraphNG/utils.ts +++ b/public/app/core/components/GraphNG/utils.ts @@ -1,4 +1,5 @@ import { DataFrame, Field, FieldType, outerJoinDataFrames, TimeRange } from '@grafana/data'; +import { NULL_EXPAND, NULL_REMOVE, NULL_RETAIN } from '@grafana/data/src/transformations/transformers/joinDataFrames'; import { applyNullInsertThreshold } from '@grafana/data/src/transformations/transformers/nulls/nullInsertThreshold'; import { nullToUndefThreshold } from '@grafana/data/src/transformations/transformers/nulls/nullToUndefThreshold'; import { GraphDrawStyle } from '@grafana/schema'; @@ -68,23 +69,10 @@ export function preparePlotFrame(frames: DataFrame[], dimFields: XYFieldMatchers } }); - let numBarSeries = 0; - - frames.forEach((frame) => { - frame.fields.forEach((f) => { - if (isVisibleBarField(f)) { - // prevent minesweeper-expansion of nulls (gaps) when joining bars - // since bar width is determined from the minimum distance between non-undefined values - // (this strategy will still retain any original pre-join nulls, though) - f.config.custom = { - ...f.config.custom, - spanNulls: -1, - }; - - numBarSeries++; - } - }); - }); + let numBarSeries = frames.reduce( + (acc, frame) => acc + frame.fields.reduce((acc, field) => acc + (isVisibleBarField(field) ? 1 : 0), 0), + 0 + ); // to make bar widths of all series uniform (equal to narrowest bar series), find smallest distance between x points let minXDelta = Infinity; @@ -115,6 +103,12 @@ export function preparePlotFrame(frames: DataFrame[], dimFields: XYFieldMatchers // https://github.com/grafana/grafana/pull/31121 // https://github.com/grafana/grafana/pull/71806 keepDisplayNames: true, + + // prevent minesweeper-expansion of nulls (gaps) when joining bars + // since bar width is determined from the minimum distance between non-undefined values + // (this strategy will still retain any original pre-join nulls, though) + nullMode: (field) => + isVisibleBarField(field) ? NULL_RETAIN : field.config.custom?.spanNulls === true ? NULL_REMOVE : NULL_EXPAND, }); if (alignedFrame) { From 2540842c95da9b5fd98f7c42de4834c1d9089f42 Mon Sep 17 00:00:00 2001 From: Adela Almasan <88068998+adela-almasan@users.noreply.github.com> Date: Mon, 26 Feb 2024 21:25:26 -0600 Subject: [PATCH 0212/1406] Histogram: Replace null values (#83195) --- .../transformers/histogram.test.ts | 90 +++++++++++++++++++ .../transformations/transformers/histogram.ts | 23 ++++- .../transformers/nulls/nullToValue.ts | 32 ++++--- 3 files changed, 129 insertions(+), 16 deletions(-) diff --git a/packages/grafana-data/src/transformations/transformers/histogram.test.ts b/packages/grafana-data/src/transformations/transformers/histogram.test.ts index 64a952d219..7bbd03397a 100644 --- a/packages/grafana-data/src/transformations/transformers/histogram.test.ts +++ b/packages/grafana-data/src/transformations/transformers/histogram.test.ts @@ -22,6 +22,14 @@ describe('histogram frames frames', () => { fields: [{ name: 'C', type: FieldType.number, values: [5, 6, 7, 8, 9] }], }); + const series3 = toDataFrame({ + fields: [{ name: 'D', type: FieldType.number, values: [1, 2, 3, null, null] }], + }); + + const series4 = toDataFrame({ + fields: [{ name: 'E', type: FieldType.number, values: [4, 5, null, 6, null], config: { noValue: '0' } }], + }); + const out = histogramFieldsToFrame(buildHistogram([series1, series2])!); expect( out.fields.map((f) => ({ @@ -188,5 +196,87 @@ describe('histogram frames frames', () => { }, ] `); + + // NULLs filtering test + const out3 = histogramFieldsToFrame(buildHistogram([series3])!); + expect( + out3.fields.map((f) => ({ + name: f.name, + values: f.values, + })) + ).toMatchInlineSnapshot(` + [ + { + "name": "xMin", + "values": [ + 1, + 2, + 3, + ], + }, + { + "name": "xMax", + "values": [ + 2, + 3, + 4, + ], + }, + { + "name": "D", + "values": [ + 1, + 1, + 1, + ], + }, + ] + `); + + // noValue nulls test + const out4 = histogramFieldsToFrame(buildHistogram([series4])!); + expect( + out4.fields.map((f) => ({ + name: f.name, + values: f.values, + config: f.config, + })) + ).toMatchInlineSnapshot(` + [ + { + "config": {}, + "name": "xMin", + "values": [ + 0, + 4, + 5, + 6, + ], + }, + { + "config": {}, + "name": "xMax", + "values": [ + 1, + 5, + 6, + 7, + ], + }, + { + "config": { + "noValue": "0", + "unit": undefined, + }, + "name": "E", + "values": [ + 2, + 1, + 1, + 1, + ], + }, + ] + `); }); }); diff --git a/packages/grafana-data/src/transformations/transformers/histogram.ts b/packages/grafana-data/src/transformations/transformers/histogram.ts index 3f21dcdd9c..1f333048e8 100644 --- a/packages/grafana-data/src/transformations/transformers/histogram.ts +++ b/packages/grafana-data/src/transformations/transformers/histogram.ts @@ -8,6 +8,7 @@ import { roundDecimals } from '../../utils'; import { DataTransformerID } from './ids'; import { AlignedData, join } from './joinDataFrames'; +import { nullToValueField } from './nulls/nullToValue'; import { transformationsVariableSupport } from './utils'; /** @@ -334,6 +335,26 @@ export function buildHistogram(frames: DataFrame[], options?: HistogramTransform let bucketCount = options?.bucketCount ?? DEFAULT_BUCKET_COUNT; let bucketOffset = options?.bucketOffset ?? 0; + // replace or filter nulls from numeric fields + frames = frames.map((frame) => { + return { + ...frame, + fields: frame.fields.map((field) => { + if (field.type === FieldType.number) { + const noValue = Number(field.config.noValue); + + if (!Number.isNaN(noValue)) { + field = nullToValueField(field, noValue); + } else { + field = { ...field, values: field.values.filter((v) => v != null) }; + } + } + + return field; + }), + }; + }); + // if bucket size is auto, try to calc from all numeric fields if (!bucketSize || bucketSize < 0) { let allValues: number[] = []; @@ -347,8 +368,6 @@ export function buildHistogram(frames: DataFrame[], options?: HistogramTransform } } - allValues = allValues.filter((v) => v != null); - allValues.sort((a, b) => a - b); let smallestDelta = Infinity; diff --git a/packages/grafana-data/src/transformations/transformers/nulls/nullToValue.ts b/packages/grafana-data/src/transformations/transformers/nulls/nullToValue.ts index 4c3ba5a81e..a2d2eeb88c 100644 --- a/packages/grafana-data/src/transformations/transformers/nulls/nullToValue.ts +++ b/packages/grafana-data/src/transformations/transformers/nulls/nullToValue.ts @@ -1,27 +1,31 @@ -import { DataFrame } from '../../../types'; +import { DataFrame, Field } from '../../../types'; export function nullToValue(frame: DataFrame) { return { ...frame, fields: frame.fields.map((field) => { - const noValue = +field.config?.noValue!; + const noValue = Number(field.config.noValue); if (!Number.isNaN(noValue)) { - const transformedVals = field.values.slice(); - - for (let i = 0; i < transformedVals.length; i++) { - if (transformedVals[i] === null) { - transformedVals[i] = noValue; - } - } - - return { - ...field, - values: transformedVals, - }; + return nullToValueField(field, noValue); } else { return field; } }), }; } + +export function nullToValueField(field: Field, noValue: number) { + const transformedVals = field.values.slice(); + + for (let i = 0; i < transformedVals.length; i++) { + if (transformedVals[i] === null) { + transformedVals[i] = noValue; + } + } + + return { + ...field, + values: transformedVals, + }; +} From faaf4dc1e33ec47ba226d675b89ddfed747ffe88 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Tue, 27 Feb 2024 09:54:06 +0000 Subject: [PATCH 0213/1406] E2C: Implement cloud auth flow (#83409) * implement cloud auth * move logic into MigrationTokenPane folder * update PDC link * add missed translations --- .../features/admin/migrate-to-cloud/api.ts | 49 ++++++++++++- .../admin/migrate-to-cloud/cloud/InfoPane.tsx | 2 +- .../cloud/MigrationTokenPane.tsx | 30 -------- .../DeleteMigrationTokenModal.tsx | 43 +++++++++++ .../MigrationTokenModal.tsx | 45 ++++++++++++ .../MigrationTokenPane/MigrationTokenPane.tsx | 72 +++++++++++++++++++ .../cloud/MigrationTokenPane/TokenStatus.tsx | 24 +++++++ .../admin/migrate-to-cloud/cloud/Page.tsx | 2 +- public/app/store/configureStore.ts | 4 +- public/locales/en-US/grafana.json | 19 ++++- public/locales/pseudo-LOCALE/grafana.json | 19 ++++- 11 files changed, 273 insertions(+), 36 deletions(-) delete mode 100644 public/app/features/admin/migrate-to-cloud/cloud/MigrationTokenPane.tsx create mode 100644 public/app/features/admin/migrate-to-cloud/cloud/MigrationTokenPane/DeleteMigrationTokenModal.tsx create mode 100644 public/app/features/admin/migrate-to-cloud/cloud/MigrationTokenPane/MigrationTokenModal.tsx create mode 100644 public/app/features/admin/migrate-to-cloud/cloud/MigrationTokenPane/MigrationTokenPane.tsx create mode 100644 public/app/features/admin/migrate-to-cloud/cloud/MigrationTokenPane/TokenStatus.tsx diff --git a/public/app/features/admin/migrate-to-cloud/api.ts b/public/app/features/admin/migrate-to-cloud/api.ts index d1972ce85c..b144fce904 100644 --- a/public/app/features/admin/migrate-to-cloud/api.ts +++ b/public/app/features/admin/migrate-to-cloud/api.ts @@ -31,7 +31,17 @@ interface MigrateToCloudStatusDTO { enabled: boolean; } +interface CreateMigrationTokenResponseDTO { + token: string; +} + +// TODO remove these mock properties/functions +const MOCK_DELAY_MS = 1000; +const MOCK_TOKEN = 'TODO_thisWillBeABigLongToken'; +let HAS_MIGRATION_TOKEN = false; + export const migrateToCloudAPI = createApi({ + tagTypes: ['migrationToken'], reducerPath: 'migrateToCloudAPI', baseQuery: createBackendSrvBaseQuery({ baseURL: '/api' }), endpoints: (builder) => ({ @@ -39,7 +49,44 @@ export const migrateToCloudAPI = createApi({ getStatus: builder.query({ queryFn: () => ({ data: { enabled: false } }), }), + createMigrationToken: builder.mutation({ + invalidatesTags: ['migrationToken'], + queryFn: async () => { + return new Promise((resolve) => { + setTimeout(() => { + HAS_MIGRATION_TOKEN = true; + resolve({ data: { token: MOCK_TOKEN } }); + }, MOCK_DELAY_MS); + }); + }, + }), + deleteMigrationToken: builder.mutation({ + invalidatesTags: ['migrationToken'], + queryFn: async () => { + return new Promise((resolve) => { + setTimeout(() => { + HAS_MIGRATION_TOKEN = false; + resolve({ data: undefined }); + }, MOCK_DELAY_MS); + }); + }, + }), + hasMigrationToken: builder.query({ + providesTags: ['migrationToken'], + queryFn: async () => { + return new Promise((resolve) => { + setTimeout(() => { + resolve({ data: HAS_MIGRATION_TOKEN }); + }, MOCK_DELAY_MS); + }); + }, + }), }), }); -export const { useGetStatusQuery } = migrateToCloudAPI; +export const { + useGetStatusQuery, + useCreateMigrationTokenMutation, + useDeleteMigrationTokenMutation, + useHasMigrationTokenQuery, +} = migrateToCloudAPI; diff --git a/public/app/features/admin/migrate-to-cloud/cloud/InfoPane.tsx b/public/app/features/admin/migrate-to-cloud/cloud/InfoPane.tsx index 929daaa642..a15b521286 100644 --- a/public/app/features/admin/migrate-to-cloud/cloud/InfoPane.tsx +++ b/public/app/features/admin/migrate-to-cloud/cloud/InfoPane.tsx @@ -61,7 +61,7 @@ export const InfoPane = () => { - + {t('migrate-to-cloud.get-started.configure-pdc-link', 'Configure PDC for this stack')} diff --git a/public/app/features/admin/migrate-to-cloud/cloud/MigrationTokenPane.tsx b/public/app/features/admin/migrate-to-cloud/cloud/MigrationTokenPane.tsx deleted file mode 100644 index d70f219d1c..0000000000 --- a/public/app/features/admin/migrate-to-cloud/cloud/MigrationTokenPane.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import React from 'react'; - -import { Box, Button, Text } from '@grafana/ui'; -import { t, Trans } from 'app/core/internationalization'; - -import { InfoItem } from '../shared/InfoItem'; - -export const MigrationTokenPane = () => { - const onGenerateToken = () => { - console.log('TODO: generate token!'); - }; - const tokenStatus = 'TODO'; - - return ( - - - - Your self-managed Grafana instance will require a special authentication token to securely connect to this - cloud stack. - - - - Current status: {{ tokenStatus }} - - - - ); -}; diff --git a/public/app/features/admin/migrate-to-cloud/cloud/MigrationTokenPane/DeleteMigrationTokenModal.tsx b/public/app/features/admin/migrate-to-cloud/cloud/MigrationTokenPane/DeleteMigrationTokenModal.tsx new file mode 100644 index 0000000000..6d1f3ddc8e --- /dev/null +++ b/public/app/features/admin/migrate-to-cloud/cloud/MigrationTokenPane/DeleteMigrationTokenModal.tsx @@ -0,0 +1,43 @@ +import React, { useState } from 'react'; + +import { Modal, Button } from '@grafana/ui'; +import { Trans, t } from 'app/core/internationalization'; + +interface Props { + hideModal: () => void; + onConfirm: () => Promise<{ data: void } | { error: unknown }>; +} + +export const DeleteMigrationTokenModal = ({ hideModal, onConfirm }: Props) => { + const [isDeleting, setIsDeleting] = useState(false); + + const onConfirmDelete = async () => { + setIsDeleting(true); + await onConfirm(); + setIsDeleting(false); + hideModal(); + }; + + return ( + + + If you've already used this token with a self-managed installation, that installation will no longer be + able to upload content. + + + + + + + ); +}; diff --git a/public/app/features/admin/migrate-to-cloud/cloud/MigrationTokenPane/MigrationTokenModal.tsx b/public/app/features/admin/migrate-to-cloud/cloud/MigrationTokenPane/MigrationTokenModal.tsx new file mode 100644 index 0000000000..70a61593e0 --- /dev/null +++ b/public/app/features/admin/migrate-to-cloud/cloud/MigrationTokenPane/MigrationTokenModal.tsx @@ -0,0 +1,45 @@ +import React, { useId } from 'react'; + +import { Modal, Button, Input, Stack, ClipboardButton, Field } from '@grafana/ui'; +import { Trans, t } from 'app/core/internationalization'; + +interface Props { + hideModal: () => void; + migrationToken: string; +} + +export const MigrationTokenModal = ({ hideModal, migrationToken }: Props) => { + const inputId = useId(); + + return ( + + + + + migrationToken}> + Copy to clipboard + + + + + + migrationToken} onClipboardCopy={hideModal}> + Copy to clipboard and close + + + + ); +}; diff --git a/public/app/features/admin/migrate-to-cloud/cloud/MigrationTokenPane/MigrationTokenPane.tsx b/public/app/features/admin/migrate-to-cloud/cloud/MigrationTokenPane/MigrationTokenPane.tsx new file mode 100644 index 0000000000..884eaf46bc --- /dev/null +++ b/public/app/features/admin/migrate-to-cloud/cloud/MigrationTokenPane/MigrationTokenPane.tsx @@ -0,0 +1,72 @@ +import React from 'react'; + +import { Box, Button, ModalsController, Text } from '@grafana/ui'; +import { t, Trans } from 'app/core/internationalization'; + +import { useCreateMigrationTokenMutation, useDeleteMigrationTokenMutation, useHasMigrationTokenQuery } from '../../api'; +import { InfoItem } from '../../shared/InfoItem'; + +import { DeleteMigrationTokenModal } from './DeleteMigrationTokenModal'; +import { MigrationTokenModal } from './MigrationTokenModal'; +import { TokenStatus } from './TokenStatus'; + +export const MigrationTokenPane = () => { + const { data: hasToken, isFetching } = useHasMigrationTokenQuery(); + const [createToken, createTokenResponse] = useCreateMigrationTokenMutation(); + const [deleteToken, deleteTokenResponse] = useDeleteMigrationTokenMutation(); + + return ( + + {({ showModal, hideModal }) => ( + + + + Your self-managed Grafana instance will require a special authentication token to securely connect to this + cloud stack. + + + + + Current status:{' '} + + + + {hasToken ? ( + + ) : ( + + )} + + )} + + ); +}; diff --git a/public/app/features/admin/migrate-to-cloud/cloud/MigrationTokenPane/TokenStatus.tsx b/public/app/features/admin/migrate-to-cloud/cloud/MigrationTokenPane/TokenStatus.tsx new file mode 100644 index 0000000000..a907d06dfe --- /dev/null +++ b/public/app/features/admin/migrate-to-cloud/cloud/MigrationTokenPane/TokenStatus.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import Skeleton from 'react-loading-skeleton'; + +import { Text } from '@grafana/ui'; +import { Trans } from 'app/core/internationalization'; + +interface Props { + hasToken: boolean; + isFetching: boolean; +} + +export const TokenStatus = ({ hasToken, isFetching }: Props) => { + if (isFetching) { + return ; + } + + return hasToken ? ( + + Token created and active + + ) : ( + No active token + ); +}; diff --git a/public/app/features/admin/migrate-to-cloud/cloud/Page.tsx b/public/app/features/admin/migrate-to-cloud/cloud/Page.tsx index 765e8cdb84..4ae3676ae6 100644 --- a/public/app/features/admin/migrate-to-cloud/cloud/Page.tsx +++ b/public/app/features/admin/migrate-to-cloud/cloud/Page.tsx @@ -5,7 +5,7 @@ import { GrafanaTheme2 } from '@grafana/data'; import { Grid, useStyles2 } from '@grafana/ui'; import { InfoPane } from './InfoPane'; -import { MigrationTokenPane } from './MigrationTokenPane'; +import { MigrationTokenPane } from './MigrationTokenPane/MigrationTokenPane'; export const Page = () => { const styles = useStyles2(getStyles); diff --git a/public/app/store/configureStore.ts b/public/app/store/configureStore.ts index a7a462bf3a..4794d46d7e 100644 --- a/public/app/store/configureStore.ts +++ b/public/app/store/configureStore.ts @@ -1,6 +1,7 @@ import { configureStore as reduxConfigureStore, createListenerMiddleware } from '@reduxjs/toolkit'; import { setupListeners } from '@reduxjs/toolkit/query'; +import { migrateToCloudAPI } from 'app/features/admin/migrate-to-cloud/api'; import { browseDashboardsAPI } from 'app/features/browse-dashboards/api/browseDashboardsAPI'; import { publicDashboardApi } from 'app/features/dashboard/api/publicDashboardApi'; import { StoreState } from 'app/types/store'; @@ -28,7 +29,8 @@ export function configureStore(initialState?: Partial) { listenerMiddleware.middleware, alertingApi.middleware, publicDashboardApi.middleware, - browseDashboardsAPI.middleware + browseDashboardsAPI.middleware, + migrateToCloudAPI.middleware ), devTools: process.env.NODE_ENV !== 'production', preloadedState: { diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index bfb1935321..e93f39c9c3 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -724,8 +724,21 @@ }, "migration-token": { "body": "Your self-managed Grafana instance will require a special authentication token to securely connect to this cloud stack.", + "delete-button": "Delete this migration token", + "delete-modal-body": "If you've already used this token with a self-managed installation, that installation will no longer be able to upload content.", + "delete-modal-cancel": "Cancel", + "delete-modal-confirm": "Delete", + "delete-modal-deleting": "Deleting...", + "delete-modal-title": "Delete migration token", "generate-button": "Generate a migration token", - "status": "Current status: {{tokenStatus}}", + "generate-button-loading": "Generating a migration token...", + "modal-close": "Close", + "modal-copy-and-close": "Copy to clipboard and close", + "modal-copy-button": "Copy to clipboard", + "modal-field-description": "Copy the token now as you will not be able to see it again. Losing a token requires creating a new one.", + "modal-field-label": "Token", + "modal-title": "Migration token created", + "status": "Current status: <2>", "title": "Migration token" }, "pdc": { @@ -738,6 +751,10 @@ "link-title": "Grafana Cloud pricing", "title": "How much does it cost?" }, + "token-status": { + "active": "Token created and active", + "no-active": "No active token" + }, "what-is-cloud": { "body": "Grafana cloud is a fully managed cloud-hosted observability platform ideal for cloud native environments. It's everything you love about Grafana without the overhead of maintaining, upgrading, and supporting an installation.", "link-title": "Learn about cloud features", diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index 2608eb4fef..a41468f885 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -724,8 +724,21 @@ }, "migration-token": { "body": "Ÿőūř şęľƒ-mäʼnäģęđ Ğřäƒäʼnä įʼnşŧäʼnčę ŵįľľ řęqūįřę ä şpęčįäľ äūŧĥęʼnŧįčäŧįőʼn ŧőĸęʼn ŧő şęčūřęľy čőʼnʼnęčŧ ŧő ŧĥįş čľőūđ şŧäčĸ.", + "delete-button": "Đęľęŧę ŧĥįş mįģřäŧįőʼn ŧőĸęʼn", + "delete-modal-body": "Ĩƒ yőū'vę äľřęäđy ūşęđ ŧĥįş ŧőĸęʼn ŵįŧĥ ä şęľƒ-mäʼnäģęđ įʼnşŧäľľäŧįőʼn, ŧĥäŧ įʼnşŧäľľäŧįőʼn ŵįľľ ʼnő ľőʼnģęř þę äþľę ŧő ūpľőäđ čőʼnŧęʼnŧ.", + "delete-modal-cancel": "Cäʼnčęľ", + "delete-modal-confirm": "Đęľęŧę", + "delete-modal-deleting": "Đęľęŧįʼnģ...", + "delete-modal-title": "Đęľęŧę mįģřäŧįőʼn ŧőĸęʼn", "generate-button": "Ğęʼnęřäŧę ä mįģřäŧįőʼn ŧőĸęʼn", - "status": "Cūřřęʼnŧ şŧäŧūş: {{tokenStatus}}", + "generate-button-loading": "Ğęʼnęřäŧįʼnģ ä mįģřäŧįőʼn ŧőĸęʼn...", + "modal-close": "Cľőşę", + "modal-copy-and-close": "Cőpy ŧő čľįpþőäřđ äʼnđ čľőşę", + "modal-copy-button": "Cőpy ŧő čľįpþőäřđ", + "modal-field-description": "Cőpy ŧĥę ŧőĸęʼn ʼnőŵ äş yőū ŵįľľ ʼnőŧ þę äþľę ŧő şęę įŧ äģäįʼn. Ŀőşįʼnģ ä ŧőĸęʼn řęqūįřęş čřęäŧįʼnģ ä ʼnęŵ őʼnę.", + "modal-field-label": "Ŧőĸęʼn", + "modal-title": "Mįģřäŧįőʼn ŧőĸęʼn čřęäŧęđ", + "status": "Cūřřęʼnŧ şŧäŧūş: <2>", "title": "Mįģřäŧįőʼn ŧőĸęʼn" }, "pdc": { @@ -738,6 +751,10 @@ "link-title": "Ğřäƒäʼnä Cľőūđ přįčįʼnģ", "title": "Ħőŵ mūčĥ đőęş įŧ čőşŧ?" }, + "token-status": { + "active": "Ŧőĸęʼn čřęäŧęđ äʼnđ äčŧįvę", + "no-active": "Ńő äčŧįvę ŧőĸęʼn" + }, "what-is-cloud": { "body": "Ğřäƒäʼnä čľőūđ įş ä ƒūľľy mäʼnäģęđ čľőūđ-ĥőşŧęđ őþşęřväþįľįŧy pľäŧƒőřm įđęäľ ƒőř čľőūđ ʼnäŧįvę ęʼnvįřőʼnmęʼnŧş. Ĩŧ'ş ęvęřyŧĥįʼnģ yőū ľővę äþőūŧ Ğřäƒäʼnä ŵįŧĥőūŧ ŧĥę ővęřĥęäđ őƒ mäįʼnŧäįʼnįʼnģ, ūpģřäđįʼnģ, äʼnđ şūppőřŧįʼnģ äʼn įʼnşŧäľľäŧįőʼn.", "link-title": "Ŀęäřʼn äþőūŧ čľőūđ ƒęäŧūřęş", From 81eded16aa9b1b3662786ec3c96852fa96cbc8fc Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 27 Feb 2024 00:38:15 +0000 Subject: [PATCH 0214/1406] Update dependency react-virtualized-auto-sizer to v1.0.23 --- package.json | 2 +- packages/grafana-flamegraph/package.json | 2 +- packages/grafana-sql/package.json | 2 +- yarn.lock | 14 +++++++------- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index cc2c3b79b2..28c977ddb1 100644 --- a/package.json +++ b/package.json @@ -389,7 +389,7 @@ "react-transition-group": "4.4.5", "react-use": "17.5.0", "react-virtual": "2.10.4", - "react-virtualized-auto-sizer": "1.0.22", + "react-virtualized-auto-sizer": "1.0.23", "react-window": "1.8.10", "react-window-infinite-loader": "1.0.9", "react-zoom-pan-pinch": "^3.3.0", diff --git a/packages/grafana-flamegraph/package.json b/packages/grafana-flamegraph/package.json index d1a03bea97..e5d83979ca 100644 --- a/packages/grafana-flamegraph/package.json +++ b/packages/grafana-flamegraph/package.json @@ -51,7 +51,7 @@ "lodash": "4.17.21", "react": "18.2.0", "react-use": "17.5.0", - "react-virtualized-auto-sizer": "1.0.22", + "react-virtualized-auto-sizer": "1.0.23", "tinycolor2": "1.6.0", "tslib": "2.6.2" }, diff --git a/packages/grafana-sql/package.json b/packages/grafana-sql/package.json index 6c94fac6ac..66286a7795 100644 --- a/packages/grafana-sql/package.json +++ b/packages/grafana-sql/package.json @@ -26,7 +26,7 @@ "immutable": "4.3.5", "lodash": "4.17.21", "react-use": "17.5.0", - "react-virtualized-auto-sizer": "1.0.22", + "react-virtualized-auto-sizer": "1.0.23", "rxjs": "7.8.1", "sql-formatter-plus": "^1.3.6", "tslib": "2.6.2", diff --git a/yarn.lock b/yarn.lock index 66e90effd7..27a40c09e1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3797,7 +3797,7 @@ __metadata: lodash: "npm:4.17.21" react: "npm:18.2.0" react-use: "npm:17.5.0" - react-virtualized-auto-sizer: "npm:1.0.22" + react-virtualized-auto-sizer: "npm:1.0.23" rollup: "npm:2.79.1" rollup-plugin-dts: "npm:^5.0.0" rollup-plugin-esbuild: "npm:5.0.0" @@ -4138,7 +4138,7 @@ __metadata: lodash: "npm:4.17.21" react: "npm:18.2.0" react-use: "npm:17.5.0" - react-virtualized-auto-sizer: "npm:1.0.22" + react-virtualized-auto-sizer: "npm:1.0.23" rxjs: "npm:7.8.1" sql-formatter-plus: "npm:^1.3.6" ts-jest: "npm:29.1.2" @@ -18601,7 +18601,7 @@ __metadata: react-transition-group: "npm:4.4.5" react-use: "npm:17.5.0" react-virtual: "npm:2.10.4" - react-virtualized-auto-sizer: "npm:1.0.22" + react-virtualized-auto-sizer: "npm:1.0.23" react-window: "npm:1.8.10" react-window-infinite-loader: "npm:1.0.9" react-zoom-pan-pinch: "npm:^3.3.0" @@ -26833,13 +26833,13 @@ __metadata: languageName: node linkType: hard -"react-virtualized-auto-sizer@npm:1.0.22": - version: 1.0.22 - resolution: "react-virtualized-auto-sizer@npm:1.0.22" +"react-virtualized-auto-sizer@npm:1.0.23": + version: 1.0.23 + resolution: "react-virtualized-auto-sizer@npm:1.0.23" peerDependencies: react: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 react-dom: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 - checksum: 10/87c808dea49fae6361d01a411f7cdcc87681551578e733594916c745a725303b5b3c88283117cf2fd569627df0758bbac62a59bd7629ba85e59aed01d5c421d6 + checksum: 10/a9b4ca0b64aaf27f9f3d214770a4a8fcaeb3d17383508d690420149e889088e1334a9dd54e69d6cb4ab891071ce0344d5605f028b941b39a331d491861eddfc6 languageName: node linkType: hard From cc3b088b6c45e4f60afe760d5ec5b7ce6a55169e Mon Sep 17 00:00:00 2001 From: Jo Date: Tue, 27 Feb 2024 11:10:54 +0100 Subject: [PATCH 0215/1406] Teams: Fix missing context in team service (#83327) fix missing context in team service --- .../commands/conflict_user_command_test.go | 2 +- .../accesscontrol/database/database_test.go | 4 +- .../resourcepermissions/store_bench_test.go | 2 +- pkg/services/team/team.go | 2 +- pkg/services/team/teamimpl/store.go | 6 +-- pkg/services/team/teamimpl/store_test.go | 40 +++++++++---------- pkg/services/team/teamimpl/team.go | 4 +- pkg/services/team/teamtest/team.go | 2 +- 8 files changed, 31 insertions(+), 31 deletions(-) diff --git a/pkg/cmd/grafana-cli/commands/conflict_user_command_test.go b/pkg/cmd/grafana-cli/commands/conflict_user_command_test.go index 3a2e034ad8..0da42a55cb 100644 --- a/pkg/cmd/grafana-cli/commands/conflict_user_command_test.go +++ b/pkg/cmd/grafana-cli/commands/conflict_user_command_test.go @@ -641,7 +641,7 @@ func TestIntegrationMergeUser(t *testing.T) { userWithUpperCase, err := usrSvc.Create(context.Background(), &dupUserEmailcmd) require.NoError(t, err) // this is the user we want to update to another team - err = teamSvc.AddTeamMember(userWithUpperCase.ID, testOrgID, team1.ID, false, 0) + err = teamSvc.AddTeamMember(context.Background(), userWithUpperCase.ID, testOrgID, team1.ID, false, 0) require.NoError(t, err) // get users diff --git a/pkg/services/accesscontrol/database/database_test.go b/pkg/services/accesscontrol/database/database_test.go index cbfbd43bb4..a929cd6cb3 100644 --- a/pkg/services/accesscontrol/database/database_test.go +++ b/pkg/services/accesscontrol/database/database_test.go @@ -255,7 +255,7 @@ func createUserAndTeam(t *testing.T, userSrv user.Service, teamSvc team.Service, team, err := teamSvc.CreateTeam("team", "", orgID) require.NoError(t, err) - err = teamSvc.AddTeamMember(user.ID, orgID, team.ID, false, dashboardaccess.PERMISSION_VIEW) + err = teamSvc.AddTeamMember(context.Background(), user.ID, orgID, team.ID, false, dashboardaccess.PERMISSION_VIEW) require.NoError(t, err) return user, team @@ -303,7 +303,7 @@ func createUsersAndTeams(t *testing.T, svcs helperServices, orgID int64, users [ team, err := svcs.teamSvc.CreateTeam(fmt.Sprintf("team%v", i+1), "", orgID) require.NoError(t, err) - err = svcs.teamSvc.AddTeamMember(user.ID, orgID, team.ID, false, dashboardaccess.PERMISSION_VIEW) + err = svcs.teamSvc.AddTeamMember(context.Background(), user.ID, orgID, team.ID, false, dashboardaccess.PERMISSION_VIEW) require.NoError(t, err) err = svcs.orgSvc.UpdateOrgUser(context.Background(), diff --git a/pkg/services/accesscontrol/resourcepermissions/store_bench_test.go b/pkg/services/accesscontrol/resourcepermissions/store_bench_test.go index 887d199328..f85a457573 100644 --- a/pkg/services/accesscontrol/resourcepermissions/store_bench_test.go +++ b/pkg/services/accesscontrol/resourcepermissions/store_bench_test.go @@ -170,7 +170,7 @@ func generateTeamsAndUsers(b *testing.B, db *sqlstore.SQLStore, users int) ([]in globalUserId++ userIds = append(userIds, userId) - err = teamSvc.AddTeamMember(userId, 1, teamId, false, 1) + err = teamSvc.AddTeamMember(context.Background(), userId, 1, teamId, false, 1) require.NoError(b, err) } } diff --git a/pkg/services/team/team.go b/pkg/services/team/team.go index 74c51c5547..5f1bca4024 100644 --- a/pkg/services/team/team.go +++ b/pkg/services/team/team.go @@ -14,7 +14,7 @@ type Service interface { GetTeamByID(ctx context.Context, query *GetTeamByIDQuery) (*TeamDTO, error) GetTeamsByUser(ctx context.Context, query *GetTeamsByUserQuery) ([]*TeamDTO, error) GetTeamIDsByUser(ctx context.Context, query *GetTeamIDsByUserQuery) ([]int64, error) - AddTeamMember(userID, orgID, teamID int64, isExternal bool, permission dashboardaccess.PermissionType) error + AddTeamMember(ctx context.Context, userID, orgID, teamID int64, isExternal bool, permission dashboardaccess.PermissionType) error UpdateTeamMember(ctx context.Context, cmd *UpdateTeamMemberCommand) error IsTeamMember(orgId int64, teamId int64, userId int64) (bool, error) RemoveTeamMember(ctx context.Context, cmd *RemoveTeamMemberCommand) error diff --git a/pkg/services/team/teamimpl/store.go b/pkg/services/team/teamimpl/store.go index 1a4a3e9bb3..915dcf41d5 100644 --- a/pkg/services/team/teamimpl/store.go +++ b/pkg/services/team/teamimpl/store.go @@ -26,7 +26,7 @@ type store interface { GetByUser(ctx context.Context, query *team.GetTeamsByUserQuery) ([]*team.TeamDTO, error) GetIDsByUser(ctx context.Context, query *team.GetTeamIDsByUserQuery) ([]int64, error) RemoveUsersMemberships(ctx context.Context, userID int64) error - AddMember(userID, orgID, teamID int64, isExternal bool, permission dashboardaccess.PermissionType) error + AddMember(ctx context.Context, userID, orgID, teamID int64, isExternal bool, permission dashboardaccess.PermissionType) error UpdateMember(ctx context.Context, cmd *team.UpdateTeamMemberCommand) error IsMember(orgId int64, teamId int64, userId int64) (bool, error) RemoveMember(ctx context.Context, cmd *team.RemoveTeamMemberCommand) error @@ -355,8 +355,8 @@ WHERE tm.user_id=? AND tm.org_id=?;`, query.UserID, query.OrgID).Find(&queryResu } // AddTeamMember adds a user to a team -func (ss *xormStore) AddMember(userID, orgID, teamID int64, isExternal bool, permission dashboardaccess.PermissionType) error { - return ss.db.WithTransactionalDbSession(context.Background(), func(sess *db.Session) error { +func (ss *xormStore) AddMember(ctx context.Context, userID, orgID, teamID int64, isExternal bool, permission dashboardaccess.PermissionType) error { + return ss.db.WithTransactionalDbSession(ctx, func(sess *db.Session) error { if isMember, err := isTeamMember(sess, orgID, teamID, userID); err != nil { return err } else if isMember { diff --git a/pkg/services/team/teamimpl/store_test.go b/pkg/services/team/teamimpl/store_test.go index 5a3b09cd91..b78aefa50a 100644 --- a/pkg/services/team/teamimpl/store_test.go +++ b/pkg/services/team/teamimpl/store_test.go @@ -92,9 +92,9 @@ func TestIntegrationTeamCommandsAndQueries(t *testing.T) { require.Equal(t, team1.OrgID, testOrgID) require.EqualValues(t, team1.MemberCount, 0) - err = teamSvc.AddTeamMember(userIds[0], testOrgID, team1.ID, false, 0) + err = teamSvc.AddTeamMember(context.Background(), userIds[0], testOrgID, team1.ID, false, 0) require.NoError(t, err) - err = teamSvc.AddTeamMember(userIds[1], testOrgID, team1.ID, true, 0) + err = teamSvc.AddTeamMember(context.Background(), userIds[1], testOrgID, team1.ID, true, 0) require.NoError(t, err) q1 := &team.GetTeamMembersQuery{OrgID: testOrgID, TeamID: team1.ID, SignedInUser: testUser} @@ -152,7 +152,7 @@ func TestIntegrationTeamCommandsAndQueries(t *testing.T) { team1 := teamQueryResult.Teams[0] - err = teamSvc.AddTeamMember(userId, testOrgID, team1.ID, true, 0) + err = teamSvc.AddTeamMember(context.Background(), userId, testOrgID, team1.ID, true, 0) require.NoError(t, err) memberQuery := &team.GetTeamMembersQuery{OrgID: testOrgID, TeamID: team1.ID, External: true, SignedInUser: testUser} @@ -168,7 +168,7 @@ func TestIntegrationTeamCommandsAndQueries(t *testing.T) { t.Run("Should be able to update users in a team", func(t *testing.T) { userId := userIds[0] - err = teamSvc.AddTeamMember(userId, testOrgID, team1.ID, false, 0) + err = teamSvc.AddTeamMember(context.Background(), userId, testOrgID, team1.ID, false, 0) require.NoError(t, err) qBeforeUpdate := &team.GetTeamMembersQuery{OrgID: testOrgID, TeamID: team1.ID, SignedInUser: testUser} @@ -195,7 +195,7 @@ func TestIntegrationTeamCommandsAndQueries(t *testing.T) { sqlStore = db.InitTestDB(t) setup() userID := userIds[0] - err = teamSvc.AddTeamMember(userID, testOrgID, team1.ID, false, 0) + err = teamSvc.AddTeamMember(context.Background(), userID, testOrgID, team1.ID, false, 0) require.NoError(t, err) qBeforeUpdate := &team.GetTeamMembersQuery{OrgID: testOrgID, TeamID: team1.ID, SignedInUser: testUser} @@ -251,7 +251,7 @@ func TestIntegrationTeamCommandsAndQueries(t *testing.T) { require.NoError(t, err) // Add a team member - err = teamSvc.AddTeamMember(userIds[0], testOrgID, team2.ID, false, 0) + err = teamSvc.AddTeamMember(context.Background(), userIds[0], testOrgID, team2.ID, false, 0) require.NoError(t, err) defer func() { err := teamSvc.RemoveTeamMember(context.Background(), @@ -304,7 +304,7 @@ func TestIntegrationTeamCommandsAndQueries(t *testing.T) { sqlStore = db.InitTestDB(t) setup() groupId := team2.ID - err := teamSvc.AddTeamMember(userIds[0], testOrgID, groupId, false, 0) + err := teamSvc.AddTeamMember(context.Background(), userIds[0], testOrgID, groupId, false, 0) require.NoError(t, err) query := &team.GetTeamsByUserQuery{ @@ -323,7 +323,7 @@ func TestIntegrationTeamCommandsAndQueries(t *testing.T) { }) t.Run("Should be able to remove users from a group", func(t *testing.T) { - err = teamSvc.AddTeamMember(userIds[0], testOrgID, team1.ID, false, 0) + err = teamSvc.AddTeamMember(context.Background(), userIds[0], testOrgID, team1.ID, false, 0) require.NoError(t, err) err = teamSvc.RemoveTeamMember(context.Background(), &team.RemoveTeamMemberCommand{OrgID: testOrgID, TeamID: team1.ID, UserID: userIds[0]}) @@ -336,7 +336,7 @@ func TestIntegrationTeamCommandsAndQueries(t *testing.T) { }) t.Run("Should have empty teams", func(t *testing.T) { - err = teamSvc.AddTeamMember(userIds[0], testOrgID, team1.ID, false, dashboardaccess.PERMISSION_ADMIN) + err = teamSvc.AddTeamMember(context.Background(), userIds[0], testOrgID, team1.ID, false, dashboardaccess.PERMISSION_ADMIN) require.NoError(t, err) t.Run("A user should be able to remove the admin permission for the last admin", func(t *testing.T) { @@ -353,10 +353,10 @@ func TestIntegrationTeamCommandsAndQueries(t *testing.T) { sqlStore = db.InitTestDB(t) setup() - err = teamSvc.AddTeamMember(userIds[0], testOrgID, team1.ID, false, dashboardaccess.PERMISSION_ADMIN) + err = teamSvc.AddTeamMember(context.Background(), userIds[0], testOrgID, team1.ID, false, dashboardaccess.PERMISSION_ADMIN) require.NoError(t, err) - err = teamSvc.AddTeamMember(userIds[1], testOrgID, team1.ID, false, dashboardaccess.PERMISSION_ADMIN) + err = teamSvc.AddTeamMember(context.Background(), userIds[1], testOrgID, team1.ID, false, dashboardaccess.PERMISSION_ADMIN) require.NoError(t, err) err = teamSvc.UpdateTeamMember(context.Background(), &team.UpdateTeamMemberCommand{OrgID: testOrgID, TeamID: team1.ID, UserID: userIds[0], Permission: 0}) require.NoError(t, err) @@ -379,11 +379,11 @@ func TestIntegrationTeamCommandsAndQueries(t *testing.T) { hiddenUsers := map[string]struct{}{"loginuser0": {}, "loginuser1": {}} teamId := team1.ID - err = teamSvc.AddTeamMember(userIds[0], testOrgID, teamId, false, 0) + err = teamSvc.AddTeamMember(context.Background(), userIds[0], testOrgID, teamId, false, 0) require.NoError(t, err) - err = teamSvc.AddTeamMember(userIds[1], testOrgID, teamId, false, 0) + err = teamSvc.AddTeamMember(context.Background(), userIds[1], testOrgID, teamId, false, 0) require.NoError(t, err) - err = teamSvc.AddTeamMember(userIds[2], testOrgID, teamId, false, 0) + err = teamSvc.AddTeamMember(context.Background(), userIds[2], testOrgID, teamId, false, 0) require.NoError(t, err) searchQuery := &team.SearchTeamsQuery{OrgID: testOrgID, Page: 1, Limit: 10, SignedInUser: signedInUser, HiddenUsers: hiddenUsers} @@ -418,11 +418,11 @@ func TestIntegrationTeamCommandsAndQueries(t *testing.T) { groupId := team2.ID // add service account to team - err = teamSvc.AddTeamMember(serviceAccount.ID, testOrgID, groupId, false, 0) + err = teamSvc.AddTeamMember(context.Background(), serviceAccount.ID, testOrgID, groupId, false, 0) require.NoError(t, err) // add user to team - err = teamSvc.AddTeamMember(userIds[0], testOrgID, groupId, false, 0) + err = teamSvc.AddTeamMember(context.Background(), userIds[0], testOrgID, groupId, false, 0) require.NoError(t, err) teamMembersQuery := &team.GetTeamMembersQuery{ @@ -550,13 +550,13 @@ func TestIntegrationSQLStore_GetTeamMembers_ACFilter(t *testing.T) { userIds[i] = user.ID } - errAddMember := teamSvc.AddTeamMember(userIds[0], testOrgID, team1.ID, false, 0) + errAddMember := teamSvc.AddTeamMember(context.Background(), userIds[0], testOrgID, team1.ID, false, 0) require.NoError(t, errAddMember) - errAddMember = teamSvc.AddTeamMember(userIds[1], testOrgID, team1.ID, false, 0) + errAddMember = teamSvc.AddTeamMember(context.Background(), userIds[1], testOrgID, team1.ID, false, 0) require.NoError(t, errAddMember) - errAddMember = teamSvc.AddTeamMember(userIds[2], testOrgID, team2.ID, false, 0) + errAddMember = teamSvc.AddTeamMember(context.Background(), userIds[2], testOrgID, team2.ID, false, 0) require.NoError(t, errAddMember) - errAddMember = teamSvc.AddTeamMember(userIds[3], testOrgID, team2.ID, false, 0) + errAddMember = teamSvc.AddTeamMember(context.Background(), userIds[3], testOrgID, team2.ID, false, 0) require.NoError(t, errAddMember) } diff --git a/pkg/services/team/teamimpl/team.go b/pkg/services/team/teamimpl/team.go index 1952788440..11cef2ab58 100644 --- a/pkg/services/team/teamimpl/team.go +++ b/pkg/services/team/teamimpl/team.go @@ -50,8 +50,8 @@ func (s *Service) GetTeamIDsByUser(ctx context.Context, query *team.GetTeamIDsBy return s.store.GetIDsByUser(ctx, query) } -func (s *Service) AddTeamMember(userID, orgID, teamID int64, isExternal bool, permission dashboardaccess.PermissionType) error { - return s.store.AddMember(userID, orgID, teamID, isExternal, permission) +func (s *Service) AddTeamMember(ctx context.Context, userID, orgID, teamID int64, isExternal bool, permission dashboardaccess.PermissionType) error { + return s.store.AddMember(ctx, userID, orgID, teamID, isExternal, permission) } func (s *Service) UpdateTeamMember(ctx context.Context, cmd *team.UpdateTeamMemberCommand) error { diff --git a/pkg/services/team/teamtest/team.go b/pkg/services/team/teamtest/team.go index 0f97e9077b..2c6fccdf33 100644 --- a/pkg/services/team/teamtest/team.go +++ b/pkg/services/team/teamtest/team.go @@ -45,7 +45,7 @@ func (s *FakeService) GetTeamsByUser(ctx context.Context, query *team.GetTeamsBy return s.ExpectedTeamsByUser, s.ExpectedError } -func (s *FakeService) AddTeamMember(userID, orgID, teamID int64, isExternal bool, permission dashboardaccess.PermissionType) error { +func (s *FakeService) AddTeamMember(ctx context.Context, userID, orgID, teamID int64, isExternal bool, permission dashboardaccess.PermissionType) error { return s.ExpectedError } From 801107892bd35b9594d1fd92f68d262b9849f286 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 27 Feb 2024 09:57:52 +0000 Subject: [PATCH 0216/1406] Update dependency @types/react to v18.2.60 --- package.json | 2 +- packages/grafana-data/package.json | 2 +- packages/grafana-flamegraph/package.json | 2 +- .../grafana-o11y-ds-frontend/package.json | 2 +- packages/grafana-prometheus/package.json | 2 +- packages/grafana-runtime/package.json | 2 +- packages/grafana-sql/package.json | 2 +- packages/grafana-ui/package.json | 2 +- .../datasource/azuremonitor/package.json | 2 +- .../datasource/cloud-monitoring/package.json | 2 +- .../grafana-pyroscope-datasource/package.json | 2 +- .../grafana-testdata-datasource/package.json | 2 +- .../app/plugins/datasource/parca/package.json | 2 +- .../app/plugins/datasource/tempo/package.json | 2 +- .../plugins/datasource/zipkin/package.json | 2 +- yarn.lock | 38 +++++++++---------- 16 files changed, 34 insertions(+), 34 deletions(-) diff --git a/package.json b/package.json index 28c977ddb1..4befb20c88 100644 --- a/package.json +++ b/package.json @@ -123,7 +123,7 @@ "@types/papaparse": "5.3.14", "@types/pluralize": "^0.0.33", "@types/prismjs": "1.26.3", - "@types/react": "18.2.59", + "@types/react": "18.2.60", "@types/react-beautiful-dnd": "13.1.8", "@types/react-dom": "18.2.19", "@types/react-grid-layout": "1.3.5", diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index 838aa5a604..c06a4a0589 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -78,7 +78,7 @@ "@types/marked": "5.0.2", "@types/node": "20.11.20", "@types/papaparse": "5.3.14", - "@types/react": "18.2.59", + "@types/react": "18.2.60", "@types/react-dom": "18.2.19", "@types/testing-library__jest-dom": "5.14.9", "@types/tinycolor2": "1.4.6", diff --git a/packages/grafana-flamegraph/package.json b/packages/grafana-flamegraph/package.json index e5d83979ca..1391a71641 100644 --- a/packages/grafana-flamegraph/package.json +++ b/packages/grafana-flamegraph/package.json @@ -67,7 +67,7 @@ "@types/d3": "^7", "@types/jest": "^29.5.4", "@types/lodash": "4.14.202", - "@types/react": "18.2.59", + "@types/react": "18.2.60", "@types/react-virtualized-auto-sizer": "1.0.4", "@types/tinycolor2": "1.4.6", "babel-jest": "29.7.0", diff --git a/packages/grafana-o11y-ds-frontend/package.json b/packages/grafana-o11y-ds-frontend/package.json index a8980f072d..517f8feda9 100644 --- a/packages/grafana-o11y-ds-frontend/package.json +++ b/packages/grafana-o11y-ds-frontend/package.json @@ -34,7 +34,7 @@ "@testing-library/react": "14.2.1", "@testing-library/user-event": "14.5.2", "@types/jest": "^29.5.4", - "@types/react": "18.2.59", + "@types/react": "18.2.60", "@types/systemjs": "6.13.5", "@types/testing-library__jest-dom": "5.14.9", "jest": "^29.6.4", diff --git a/packages/grafana-prometheus/package.json b/packages/grafana-prometheus/package.json index 1a62bcd7ff..ec7eba97a8 100644 --- a/packages/grafana-prometheus/package.json +++ b/packages/grafana-prometheus/package.json @@ -97,7 +97,7 @@ "@types/node": "20.11.20", "@types/pluralize": "^0.0.33", "@types/prismjs": "1.26.3", - "@types/react": "18.2.59", + "@types/react": "18.2.60", "@types/react-beautiful-dnd": "13.1.8", "@types/react-dom": "18.2.19", "@types/react-highlight-words": "0.16.7", diff --git a/packages/grafana-runtime/package.json b/packages/grafana-runtime/package.json index ebc543cd48..2dc1bd6b41 100644 --- a/packages/grafana-runtime/package.json +++ b/packages/grafana-runtime/package.json @@ -60,7 +60,7 @@ "@types/history": "4.7.11", "@types/jest": "29.5.12", "@types/lodash": "4.14.202", - "@types/react": "18.2.59", + "@types/react": "18.2.60", "@types/react-dom": "18.2.19", "@types/systemjs": "6.13.5", "esbuild": "0.18.12", diff --git a/packages/grafana-sql/package.json b/packages/grafana-sql/package.json index 66286a7795..d4e531e5ce 100644 --- a/packages/grafana-sql/package.json +++ b/packages/grafana-sql/package.json @@ -39,7 +39,7 @@ "@testing-library/react-hooks": "^8.0.1", "@testing-library/user-event": "14.5.2", "@types/jest": "^29.5.4", - "@types/react": "18.2.59", + "@types/react": "18.2.60", "@types/systemjs": "6.13.5", "@types/testing-library__jest-dom": "5.14.9", "jest": "^29.6.4", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 7150449e08..9bd5200f2f 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -142,7 +142,7 @@ "@types/mock-raf": "1.0.6", "@types/node": "20.11.20", "@types/prismjs": "1.26.3", - "@types/react": "18.2.59", + "@types/react": "18.2.60", "@types/react-beautiful-dnd": "13.1.8", "@types/react-calendar": "3.9.0", "@types/react-color": "3.0.12", diff --git a/public/app/plugins/datasource/azuremonitor/package.json b/public/app/plugins/datasource/azuremonitor/package.json index 8339305882..f400937ef6 100644 --- a/public/app/plugins/datasource/azuremonitor/package.json +++ b/public/app/plugins/datasource/azuremonitor/package.json @@ -31,7 +31,7 @@ "@types/lodash": "4.14.202", "@types/node": "20.11.20", "@types/prismjs": "1.26.3", - "@types/react": "18.2.59", + "@types/react": "18.2.60", "@types/testing-library__jest-dom": "5.14.9", "react-select-event": "5.5.1", "ts-node": "10.9.2", diff --git a/public/app/plugins/datasource/cloud-monitoring/package.json b/public/app/plugins/datasource/cloud-monitoring/package.json index 68ec97c884..38376e2cc2 100644 --- a/public/app/plugins/datasource/cloud-monitoring/package.json +++ b/public/app/plugins/datasource/cloud-monitoring/package.json @@ -34,7 +34,7 @@ "@types/lodash": "4.14.202", "@types/node": "20.11.20", "@types/prismjs": "1.26.3", - "@types/react": "18.2.59", + "@types/react": "18.2.60", "@types/react-test-renderer": "18.0.7", "@types/testing-library__jest-dom": "5.14.9", "react-select-event": "5.5.1", diff --git a/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json b/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json index 04a37d9866..ee899f9a01 100644 --- a/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json +++ b/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json @@ -27,7 +27,7 @@ "@types/jest": "29.5.12", "@types/lodash": "4.14.202", "@types/prismjs": "1.26.3", - "@types/react": "18.2.59", + "@types/react": "18.2.60", "@types/react-dom": "18.2.19", "@types/testing-library__jest-dom": "5.14.9", "css-loader": "6.10.0", diff --git a/public/app/plugins/datasource/grafana-testdata-datasource/package.json b/public/app/plugins/datasource/grafana-testdata-datasource/package.json index 5e4a71003e..392461e3ed 100644 --- a/public/app/plugins/datasource/grafana-testdata-datasource/package.json +++ b/public/app/plugins/datasource/grafana-testdata-datasource/package.json @@ -28,7 +28,7 @@ "@types/jest": "29.5.12", "@types/lodash": "4.14.202", "@types/node": "20.11.20", - "@types/react": "18.2.59", + "@types/react": "18.2.60", "@types/testing-library__jest-dom": "5.14.9", "@types/uuid": "9.0.8", "ts-node": "10.9.2", diff --git a/public/app/plugins/datasource/parca/package.json b/public/app/plugins/datasource/parca/package.json index f5b02b031b..1e5e9d03c7 100644 --- a/public/app/plugins/datasource/parca/package.json +++ b/public/app/plugins/datasource/parca/package.json @@ -21,7 +21,7 @@ "@testing-library/react": "14.2.1", "@testing-library/user-event": "14.5.2", "@types/lodash": "4.14.202", - "@types/react": "18.2.59", + "@types/react": "18.2.60", "ts-node": "10.9.2", "webpack": "5.90.2" }, diff --git a/public/app/plugins/datasource/tempo/package.json b/public/app/plugins/datasource/tempo/package.json index a3b7fa7c6d..67e0f117f7 100644 --- a/public/app/plugins/datasource/tempo/package.json +++ b/public/app/plugins/datasource/tempo/package.json @@ -46,7 +46,7 @@ "@types/lodash": "4.14.202", "@types/node": "20.11.20", "@types/prismjs": "1.26.3", - "@types/react": "18.2.59", + "@types/react": "18.2.60", "@types/react-dom": "18.2.19", "@types/semver": "7.5.8", "@types/uuid": "9.0.8", diff --git a/public/app/plugins/datasource/zipkin/package.json b/public/app/plugins/datasource/zipkin/package.json index efe00b1e93..3a1f70b19a 100644 --- a/public/app/plugins/datasource/zipkin/package.json +++ b/public/app/plugins/datasource/zipkin/package.json @@ -22,7 +22,7 @@ "@testing-library/react": "14.2.1", "@types/jest": "29.5.12", "@types/lodash": "4.14.202", - "@types/react": "18.2.59", + "@types/react": "18.2.60", "ts-node": "10.9.2", "webpack": "5.90.2" }, diff --git a/yarn.lock b/yarn.lock index 27a40c09e1..d11cccb905 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3233,7 +3233,7 @@ __metadata: "@types/lodash": "npm:4.14.202" "@types/node": "npm:20.11.20" "@types/prismjs": "npm:1.26.3" - "@types/react": "npm:18.2.59" + "@types/react": "npm:18.2.60" "@types/testing-library__jest-dom": "npm:5.14.9" fast-deep-equal: "npm:^3.1.3" i18next: "npm:^23.0.0" @@ -3270,7 +3270,7 @@ __metadata: "@types/jest": "npm:29.5.12" "@types/lodash": "npm:4.14.202" "@types/prismjs": "npm:1.26.3" - "@types/react": "npm:18.2.59" + "@types/react": "npm:18.2.60" "@types/react-dom": "npm:18.2.19" "@types/testing-library__jest-dom": "npm:5.14.9" css-loader: "npm:6.10.0" @@ -3311,7 +3311,7 @@ __metadata: "@types/jest": "npm:29.5.12" "@types/lodash": "npm:4.14.202" "@types/node": "npm:20.11.20" - "@types/react": "npm:18.2.59" + "@types/react": "npm:18.2.60" "@types/testing-library__jest-dom": "npm:5.14.9" "@types/uuid": "npm:9.0.8" d3-random: "npm:^3.0.1" @@ -3365,7 +3365,7 @@ __metadata: "@testing-library/react": "npm:14.2.1" "@testing-library/user-event": "npm:14.5.2" "@types/lodash": "npm:4.14.202" - "@types/react": "npm:18.2.59" + "@types/react": "npm:18.2.60" lodash: "npm:4.17.21" monaco-editor: "npm:0.34.0" react: "npm:18.2.0" @@ -3400,7 +3400,7 @@ __metadata: "@types/lodash": "npm:4.14.202" "@types/node": "npm:20.11.20" "@types/prismjs": "npm:1.26.3" - "@types/react": "npm:18.2.59" + "@types/react": "npm:18.2.60" "@types/react-test-renderer": "npm:18.0.7" "@types/testing-library__jest-dom": "npm:5.14.9" debounce-promise: "npm:3.1.2" @@ -3452,7 +3452,7 @@ __metadata: "@types/lodash": "npm:4.14.202" "@types/node": "npm:20.11.20" "@types/prismjs": "npm:1.26.3" - "@types/react": "npm:18.2.59" + "@types/react": "npm:18.2.60" "@types/react-dom": "npm:18.2.19" "@types/semver": "npm:7.5.8" "@types/uuid": "npm:9.0.8" @@ -3497,7 +3497,7 @@ __metadata: "@testing-library/react": "npm:14.2.1" "@types/jest": "npm:29.5.12" "@types/lodash": "npm:4.14.202" - "@types/react": "npm:18.2.59" + "@types/react": "npm:18.2.60" lodash: "npm:4.17.21" react: "npm:18.2.0" react-use: "npm:17.5.0" @@ -3552,7 +3552,7 @@ __metadata: "@types/marked": "npm:5.0.2" "@types/node": "npm:20.11.20" "@types/papaparse": "npm:5.3.14" - "@types/react": "npm:18.2.59" + "@types/react": "npm:18.2.60" "@types/react-dom": "npm:18.2.19" "@types/string-hash": "npm:1.1.3" "@types/testing-library__jest-dom": "npm:5.14.9" @@ -3787,7 +3787,7 @@ __metadata: "@types/d3": "npm:^7" "@types/jest": "npm:^29.5.4" "@types/lodash": "npm:4.14.202" - "@types/react": "npm:18.2.59" + "@types/react": "npm:18.2.60" "@types/react-virtualized-auto-sizer": "npm:1.0.4" "@types/tinycolor2": "npm:1.4.6" babel-jest: "npm:29.7.0" @@ -3868,7 +3868,7 @@ __metadata: "@testing-library/react": "npm:14.2.1" "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:^29.5.4" - "@types/react": "npm:18.2.59" + "@types/react": "npm:18.2.60" "@types/systemjs": "npm:6.13.5" "@types/testing-library__jest-dom": "npm:5.14.9" jest: "npm:^29.6.4" @@ -3954,7 +3954,7 @@ __metadata: "@types/node": "npm:20.11.20" "@types/pluralize": "npm:^0.0.33" "@types/prismjs": "npm:1.26.3" - "@types/react": "npm:18.2.59" + "@types/react": "npm:18.2.60" "@types/react-beautiful-dnd": "npm:13.1.8" "@types/react-dom": "npm:18.2.19" "@types/react-highlight-words": "npm:0.16.7" @@ -4047,7 +4047,7 @@ __metadata: "@types/history": "npm:4.7.11" "@types/jest": "npm:29.5.12" "@types/lodash": "npm:4.14.202" - "@types/react": "npm:18.2.59" + "@types/react": "npm:18.2.60" "@types/react-dom": "npm:18.2.19" "@types/systemjs": "npm:6.13.5" esbuild: "npm:0.18.12" @@ -4128,7 +4128,7 @@ __metadata: "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:^29.5.4" "@types/lodash": "npm:4.14.202" - "@types/react": "npm:18.2.59" + "@types/react": "npm:18.2.60" "@types/react-virtualized-auto-sizer": "npm:1.0.4" "@types/systemjs": "npm:6.13.5" "@types/testing-library__jest-dom": "npm:5.14.9" @@ -4215,7 +4215,7 @@ __metadata: "@types/mock-raf": "npm:1.0.6" "@types/node": "npm:20.11.20" "@types/prismjs": "npm:1.26.3" - "@types/react": "npm:18.2.59" + "@types/react": "npm:18.2.60" "@types/react-beautiful-dnd": "npm:13.1.8" "@types/react-calendar": "npm:3.9.0" "@types/react-color": "npm:3.0.12" @@ -9910,14 +9910,14 @@ __metadata: languageName: node linkType: hard -"@types/react@npm:*, @types/react@npm:18.2.59, @types/react@npm:>=16": - version: 18.2.59 - resolution: "@types/react@npm:18.2.59" +"@types/react@npm:*, @types/react@npm:18.2.60, @types/react@npm:>=16": + version: 18.2.60 + resolution: "@types/react@npm:18.2.60" dependencies: "@types/prop-types": "npm:*" "@types/scheduler": "npm:*" csstype: "npm:^3.0.2" - checksum: 10/d065b998bc99ac61ff30011072a55e52eaa51ad20a84bc5893fae5e5de80d2e0a61217772a3ab79eb19d2f8d8ae3bd8451db5e0a77044f279618a1fc3446def6 + checksum: 10/5f2f6091623f13375a5bbc7e5c222cd212b5d6366ead737b76c853f6f52b314db24af5ae3f688d2d49814c668c216858a75433f145311839d8989d46bb3cbecf languageName: node linkType: hard @@ -18413,7 +18413,7 @@ __metadata: "@types/papaparse": "npm:5.3.14" "@types/pluralize": "npm:^0.0.33" "@types/prismjs": "npm:1.26.3" - "@types/react": "npm:18.2.59" + "@types/react": "npm:18.2.60" "@types/react-beautiful-dnd": "npm:13.1.8" "@types/react-dom": "npm:18.2.19" "@types/react-grid-layout": "npm:1.3.5" From 4d0fca443cb47c203262b5f5a0540b06b9983b55 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 27 Feb 2024 10:25:32 +0000 Subject: [PATCH 0217/1406] I18n: Download translations from Crowdin (#83390) New Crowdin translations by GitHub Action Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- public/locales/de-DE/grafana.json | 75 +++++++++++++++++++++++++++++ public/locales/es-ES/grafana.json | 75 +++++++++++++++++++++++++++++ public/locales/fr-FR/grafana.json | 75 +++++++++++++++++++++++++++++ public/locales/zh-Hans/grafana.json | 75 +++++++++++++++++++++++++++++ 4 files changed, 300 insertions(+) diff --git a/public/locales/de-DE/grafana.json b/public/locales/de-DE/grafana.json index 4fe5020886..be84f20067 100644 --- a/public/locales/de-DE/grafana.json +++ b/public/locales/de-DE/grafana.json @@ -691,6 +691,81 @@ "new-to-question": "" } }, + "migrate-to-cloud": { + "can-i-move": { + "body": "", + "link-title": "", + "title": "" + }, + "cta": { + "button": "", + "header": "" + }, + "get-started": { + "body": "", + "configure-pdc-link": "", + "link-title": "", + "step-1": "", + "step-2": "", + "step-3": "", + "step-4": "", + "step-5": "", + "title": "" + }, + "is-it-secure": { + "body": "", + "link-title": "", + "title": "" + }, + "migrate-to-this-stack": { + "body": "", + "link-title": "", + "title": "" + }, + "migration-token": { + "body": "", + "delete-button": "", + "delete-modal-body": "", + "delete-modal-cancel": "", + "delete-modal-confirm": "", + "delete-modal-deleting": "", + "delete-modal-title": "", + "generate-button": "", + "generate-button-loading": "", + "modal-close": "", + "modal-copy-and-close": "", + "modal-copy-button": "", + "modal-field-description": "", + "modal-field-label": "", + "modal-title": "", + "status": "", + "title": "" + }, + "pdc": { + "body": "", + "link-title": "", + "title": "" + }, + "pricing": { + "body": "", + "link-title": "", + "title": "" + }, + "token-status": { + "active": "", + "no-active": "" + }, + "what-is-cloud": { + "body": "", + "link-title": "", + "title": "" + }, + "why-host": { + "body": "", + "link-title": "", + "title": "" + } + }, "nav": { "add-new-connections": { "title": "Neue Verbindung hinzufügen" diff --git a/public/locales/es-ES/grafana.json b/public/locales/es-ES/grafana.json index 678a9c7a20..232f18c072 100644 --- a/public/locales/es-ES/grafana.json +++ b/public/locales/es-ES/grafana.json @@ -691,6 +691,81 @@ "new-to-question": "" } }, + "migrate-to-cloud": { + "can-i-move": { + "body": "", + "link-title": "", + "title": "" + }, + "cta": { + "button": "", + "header": "" + }, + "get-started": { + "body": "", + "configure-pdc-link": "", + "link-title": "", + "step-1": "", + "step-2": "", + "step-3": "", + "step-4": "", + "step-5": "", + "title": "" + }, + "is-it-secure": { + "body": "", + "link-title": "", + "title": "" + }, + "migrate-to-this-stack": { + "body": "", + "link-title": "", + "title": "" + }, + "migration-token": { + "body": "", + "delete-button": "", + "delete-modal-body": "", + "delete-modal-cancel": "", + "delete-modal-confirm": "", + "delete-modal-deleting": "", + "delete-modal-title": "", + "generate-button": "", + "generate-button-loading": "", + "modal-close": "", + "modal-copy-and-close": "", + "modal-copy-button": "", + "modal-field-description": "", + "modal-field-label": "", + "modal-title": "", + "status": "", + "title": "" + }, + "pdc": { + "body": "", + "link-title": "", + "title": "" + }, + "pricing": { + "body": "", + "link-title": "", + "title": "" + }, + "token-status": { + "active": "", + "no-active": "" + }, + "what-is-cloud": { + "body": "", + "link-title": "", + "title": "" + }, + "why-host": { + "body": "", + "link-title": "", + "title": "" + } + }, "nav": { "add-new-connections": { "title": "Añadir nueva conexión" diff --git a/public/locales/fr-FR/grafana.json b/public/locales/fr-FR/grafana.json index 5a8a02ea6f..7e0283a931 100644 --- a/public/locales/fr-FR/grafana.json +++ b/public/locales/fr-FR/grafana.json @@ -691,6 +691,81 @@ "new-to-question": "" } }, + "migrate-to-cloud": { + "can-i-move": { + "body": "", + "link-title": "", + "title": "" + }, + "cta": { + "button": "", + "header": "" + }, + "get-started": { + "body": "", + "configure-pdc-link": "", + "link-title": "", + "step-1": "", + "step-2": "", + "step-3": "", + "step-4": "", + "step-5": "", + "title": "" + }, + "is-it-secure": { + "body": "", + "link-title": "", + "title": "" + }, + "migrate-to-this-stack": { + "body": "", + "link-title": "", + "title": "" + }, + "migration-token": { + "body": "", + "delete-button": "", + "delete-modal-body": "", + "delete-modal-cancel": "", + "delete-modal-confirm": "", + "delete-modal-deleting": "", + "delete-modal-title": "", + "generate-button": "", + "generate-button-loading": "", + "modal-close": "", + "modal-copy-and-close": "", + "modal-copy-button": "", + "modal-field-description": "", + "modal-field-label": "", + "modal-title": "", + "status": "", + "title": "" + }, + "pdc": { + "body": "", + "link-title": "", + "title": "" + }, + "pricing": { + "body": "", + "link-title": "", + "title": "" + }, + "token-status": { + "active": "", + "no-active": "" + }, + "what-is-cloud": { + "body": "", + "link-title": "", + "title": "" + }, + "why-host": { + "body": "", + "link-title": "", + "title": "" + } + }, "nav": { "add-new-connections": { "title": "Ajouter une nouvelle connexion" diff --git a/public/locales/zh-Hans/grafana.json b/public/locales/zh-Hans/grafana.json index 94bca5f93a..b9dc3b97a8 100644 --- a/public/locales/zh-Hans/grafana.json +++ b/public/locales/zh-Hans/grafana.json @@ -685,6 +685,81 @@ "new-to-question": "" } }, + "migrate-to-cloud": { + "can-i-move": { + "body": "", + "link-title": "", + "title": "" + }, + "cta": { + "button": "", + "header": "" + }, + "get-started": { + "body": "", + "configure-pdc-link": "", + "link-title": "", + "step-1": "", + "step-2": "", + "step-3": "", + "step-4": "", + "step-5": "", + "title": "" + }, + "is-it-secure": { + "body": "", + "link-title": "", + "title": "" + }, + "migrate-to-this-stack": { + "body": "", + "link-title": "", + "title": "" + }, + "migration-token": { + "body": "", + "delete-button": "", + "delete-modal-body": "", + "delete-modal-cancel": "", + "delete-modal-confirm": "", + "delete-modal-deleting": "", + "delete-modal-title": "", + "generate-button": "", + "generate-button-loading": "", + "modal-close": "", + "modal-copy-and-close": "", + "modal-copy-button": "", + "modal-field-description": "", + "modal-field-label": "", + "modal-title": "", + "status": "", + "title": "" + }, + "pdc": { + "body": "", + "link-title": "", + "title": "" + }, + "pricing": { + "body": "", + "link-title": "", + "title": "" + }, + "token-status": { + "active": "", + "no-active": "" + }, + "what-is-cloud": { + "body": "", + "link-title": "", + "title": "" + }, + "why-host": { + "body": "", + "link-title": "", + "title": "" + } + }, "nav": { "add-new-connections": { "title": "添加新连接" From ff28c042453a346d5de8747442b29cadae8cdee1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 27 Feb 2024 10:23:55 +0000 Subject: [PATCH 0218/1406] Update dependency ol-ext to v4.0.15 --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 4befb20c88..622eb3c675 100644 --- a/package.json +++ b/package.json @@ -351,7 +351,7 @@ "nanoid": "^5.0.4", "node-forge": "^1.3.1", "ol": "7.4.0", - "ol-ext": "4.0.14", + "ol-ext": "4.0.15", "papaparse": "5.4.1", "pluralize": "^8.0.0", "prismjs": "1.29.0", diff --git a/yarn.lock b/yarn.lock index d11cccb905..6536e4769c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18554,7 +18554,7 @@ __metadata: node-forge: "npm:^1.3.1" node-notifier: "npm:10.0.1" ol: "npm:7.4.0" - ol-ext: "npm:4.0.14" + ol-ext: "npm:4.0.15" papaparse: "npm:5.4.1" pluralize: "npm:^8.0.0" postcss: "npm:8.4.35" @@ -23907,12 +23907,12 @@ __metadata: languageName: node linkType: hard -"ol-ext@npm:4.0.14": - version: 4.0.14 - resolution: "ol-ext@npm:4.0.14" +"ol-ext@npm:4.0.15": + version: 4.0.15 + resolution: "ol-ext@npm:4.0.15" peerDependencies: ol: ">= 5.3.0" - checksum: 10/e25668577b66766d3d1d475f36cedfaaab9333e62196e1c48ac5f6a0d7e5b97f6b063a52661dd159e3e43694fa668d948987791d81fb34879c9aa9a5769363a1 + checksum: 10/d9b313c3e4ed3d61ab4ad35b0be1fdf1456a2f253a581a2f9c28859ba0f22d8d86378ade6d8a18b4eb65c9916d9a277db2a1fde73aec4cbb816abe251d38e68a languageName: node linkType: hard From 45f8e7f8cf97f5dd3939a2989def547e2bf97944 Mon Sep 17 00:00:00 2001 From: brendamuir <100768211+brendamuir@users.noreply.github.com> Date: Tue, 27 Feb 2024 11:56:11 +0100 Subject: [PATCH 0219/1406] Revert "Revert "Alerting docs: rework create alert rules definition and topic"" (#83372) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Revert "Revert "Alerting docs: rework create alert rules definition and topic…" This reverts commit 2b4f1087712e82d17db8ce29adf4a40f5b76e0fc. * updates aliases * fixes after testing aliases * more alias updates * test silence alias * fix alias for mute timings * attempt alias fix * ran prettier * fixes more aliases * quick title update * fixes alias * Update docs/sources/alerting/configure-notifications/manage-contact-points/_index.md Co-authored-by: Jack Baldry * Update docs/sources/alerting/configure-notifications/manage-contact-points/_index.md Co-authored-by: Jack Baldry * Update docs/sources/alerting/configure-notifications/manage-contact-points/_index.md Co-authored-by: Jack Baldry * Update docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-oncall.md Co-authored-by: Jack Baldry * Update docs/sources/alerting/configure-notifications/template-notifications/reference.md Co-authored-by: Jack Baldry * Update docs/sources/alerting/configure-notifications/template-notifications/use-notification-templates.md Co-authored-by: Jack Baldry * Update docs/sources/alerting/configure-notifications/template-notifications/using-go-templating-language.md Co-authored-by: Jack Baldry * Update docs/sources/alerting/configure-notifications/create-silence.md Co-authored-by: Jack Baldry * Update docs/sources/alerting/configure-notifications/manage-contact-points/integrations/webhook-notifier.md Co-authored-by: Jack Baldry * Update docs/sources/alerting/configure-notifications/mute-timings.md Co-authored-by: Jack Baldry * Update docs/sources/alerting/configure-notifications/template-notifications/_index.md Co-authored-by: Jack Baldry * Update docs/sources/alerting/manage-notifications/_index.md Co-authored-by: Jack Baldry * Update docs/sources/alerting/configure-notifications/create-notification-policy.md Co-authored-by: Jack Baldry * Update docs/sources/alerting/configure-notifications/manage-contact-points/_index.md Co-authored-by: Jack Baldry * Update docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-oncall.md Co-authored-by: Jack Baldry * Update docs/sources/alerting/configure-notifications/manage-contact-points/integrations/pager-duty.md Co-authored-by: Jack Baldry * Update docs/sources/alerting/configure-notifications/manage-contact-points/integrations/webhook-notifier.md Co-authored-by: Jack Baldry * Update docs/sources/alerting/configure-notifications/mute-timings.md Co-authored-by: Jack Baldry * Update docs/sources/alerting/configure-notifications/template-notifications/_index.md Co-authored-by: Jack Baldry * fix silence aliases * fix canonical * Update docs/sources/alerting/configure-notifications/template-notifications/create-notification-templates.md Co-authored-by: Jack Baldry * Update docs/sources/alerting/configure-notifications/create-notification-policy.md Co-authored-by: Jack Baldry --------- Co-authored-by: Jack Baldry --- docs/sources/alerting/_index.md | 5 +- .../sources/alerting/alerting-rules/_index.md | 52 +++---- ...reate-mimir-loki-managed-recording-rule.md | 16 +- .../manage-contact-points/_index.md | 77 ---------- .../integrations/_index.md | 50 ------ .../configure-notifications/_index.md | 26 ++++ .../create-notification-policy.md | 11 +- .../create-silence.md | 17 ++- .../manage-contact-points/_index.md | 142 ++++++++++++++++++ .../integrations/configure-oncall.md | 9 +- .../integrations/pager-duty.md | 4 +- .../integrations/webhook-notifier.md | 9 +- .../mute-timings.md | 9 +- .../template-notifications/_index.md | 18 ++- .../create-notification-templates.md | 4 +- .../images-in-notifications.md | 16 +- .../template-notifications/reference.md | 4 +- .../use-notification-templates.md | 12 +- .../using-go-templating-language.md | 16 +- .../alerting/manage-notifications/_index.md | 41 +---- .../manage-contact-points.md | 34 ----- docs/sources/alerting/monitor/_index.md | 2 +- .../setup-grafana/configure-grafana/_index.md | 2 +- 23 files changed, 284 insertions(+), 292 deletions(-) delete mode 100644 docs/sources/alerting/alerting-rules/manage-contact-points/_index.md delete mode 100644 docs/sources/alerting/alerting-rules/manage-contact-points/integrations/_index.md create mode 100644 docs/sources/alerting/configure-notifications/_index.md rename docs/sources/alerting/{alerting-rules => configure-notifications}/create-notification-policy.md (93%) rename docs/sources/alerting/{manage-notifications => configure-notifications}/create-silence.md (80%) create mode 100644 docs/sources/alerting/configure-notifications/manage-contact-points/_index.md rename docs/sources/alerting/{alerting-rules => configure-notifications}/manage-contact-points/integrations/configure-oncall.md (83%) rename docs/sources/alerting/{alerting-rules => configure-notifications}/manage-contact-points/integrations/pager-duty.md (80%) rename docs/sources/alerting/{alerting-rules => configure-notifications}/manage-contact-points/integrations/webhook-notifier.md (90%) rename docs/sources/alerting/{manage-notifications => configure-notifications}/mute-timings.md (91%) rename docs/sources/alerting/{manage-notifications => configure-notifications}/template-notifications/_index.md (74%) rename docs/sources/alerting/{manage-notifications => configure-notifications}/template-notifications/create-notification-templates.md (96%) rename docs/sources/alerting/{manage-notifications => configure-notifications/template-notifications}/images-in-notifications.md (95%) rename docs/sources/alerting/{manage-notifications => configure-notifications}/template-notifications/reference.md (95%) rename docs/sources/alerting/{manage-notifications => configure-notifications}/template-notifications/use-notification-templates.md (63%) rename docs/sources/alerting/{manage-notifications => configure-notifications}/template-notifications/using-go-templating-language.md (91%) delete mode 100644 docs/sources/alerting/manage-notifications/manage-contact-points.md diff --git a/docs/sources/alerting/_index.md b/docs/sources/alerting/_index.md index 6d0c0f1a62..3feee0d5f1 100644 --- a/docs/sources/alerting/_index.md +++ b/docs/sources/alerting/_index.md @@ -10,11 +10,12 @@ labels: - cloud - enterprise - oss -title: Alerting +menuTitle: Alerting +title: Grafana Alerting weight: 114 --- -# Alerting +# Grafana Alerting Grafana Alerting allows you to learn about problems in your systems moments after they occur. diff --git a/docs/sources/alerting/alerting-rules/_index.md b/docs/sources/alerting/alerting-rules/_index.md index 3063d36929..1d7c8722d9 100644 --- a/docs/sources/alerting/alerting-rules/_index.md +++ b/docs/sources/alerting/alerting-rules/_index.md @@ -5,54 +5,46 @@ aliases: - unified-alerting/alerting-rules/ - ./create-alerts/ canonical: https://grafana.com/docs/grafana/latest/alerting/alerting-rules/ -description: Configure the features and integrations you need to create and manage your alerts +description: Create and manage alert rules labels: products: - cloud - enterprise - oss -menuTitle: Configure -title: Configure Alerting +menuTitle: Create and manage alert rules +title: Create and manage alert rules weight: 120 --- -# Configure Alerting +# Create and manage alert rules -Configure the features and integrations that you need to create and manage your alerts. +An alert rule consists of one or more queries and expressions that select the data you want to measure. It also contains a condition, which is the threshold that an alert rule must meet or exceed in order to fire. -**Configure alert rules** +Create, manage, view, and adjust alert rules to alert on your metrics data or log entries from multiple data sources — no matter where your data is stored. -[Configure Grafana-managed alert rules][create-grafana-managed-rule]. +The main parts of alert rule creation are: -[Configure data source-managed alert rules][create-mimir-loki-managed-rule] +1. Select your data source +1. Query your data +1. Normalize your data +1. Set your threshold -**Configure recording rules** +**Query, expressions, and alert condition** -_Recording rules are only available for compatible Prometheus or Loki data sources._ +What are you monitoring? How are you measuring it? -For more information, see [Configure recording rules][create-mimir-loki-managed-recording-rule]. +{{< admonition type="note" >}} +Expressions can only be used for Grafana-managed alert rules. +{{< /admonition >}} -**Configure contact points** +**Evaluation** -For information on how to configure contact points, see [Configure contact points][manage-contact-points]. +How do you want your alert to be evaluated? -**Configure notification policies** +**Labels and notifications** -For information on how to configure notification policies, see [Configure notification policies][create-notification-policy]. +How do you want to route your alert? What kind of additional labels could you add to annotate your alert rules and ease searching? -{{% docs/reference %}} -[create-mimir-loki-managed-rule]: "/docs/grafana/ -> /docs/grafana//alerting/alerting-rules/create-mimir-loki-managed-rule" -[create-mimir-loki-managed-rule]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/alerting-rules/create-mimir-loki-managed-rule" +**Annotations** -[create-mimir-loki-managed-recording-rule]: "/docs/grafana/ -> /docs/grafana//alerting/alerting-rules/create-mimir-loki-managed-recording-rule" -[create-mimir-loki-managed-recording-rule]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/alerting-rules/create-mimir-loki-managed-recording-rule" - -[create-grafana-managed-rule]: "/docs/grafana/ -> /docs/grafana//alerting/alerting-rules/create-grafana-managed-rule" -[create-grafana-managed-rule]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/alerting-rules/create-grafana-managed-rule" - -[manage-contact-points]: "/docs/grafana/ -> /docs/grafana//alerting/alerting-rules/manage-contact-points" -[manage-contact-points]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/alerting-rules/manage-contact-points" - -[create-notification-policy]: "/docs/grafana/ -> /docs/grafana//alerting/alerting-rules/create-notification-policy" -[create-notification-policy]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/alerting-rules/create-notification-policy" -{{% /docs/reference %}} +Do you want to add more context on the alert in your notification messages, for example, what caused the alert to fire? Which server did it happen on? diff --git a/docs/sources/alerting/alerting-rules/create-mimir-loki-managed-recording-rule.md b/docs/sources/alerting/alerting-rules/create-mimir-loki-managed-recording-rule.md index 08fbbbc492..637cbf3ca4 100644 --- a/docs/sources/alerting/alerting-rules/create-mimir-loki-managed-recording-rule.md +++ b/docs/sources/alerting/alerting-rules/create-mimir-loki-managed-recording-rule.md @@ -3,7 +3,7 @@ aliases: - ../unified-alerting/alerting-rules/create-cortex-loki-managed-recording-rule/ - ../unified-alerting/alerting-rules/create-mimir-loki-managed-recording-rule/ canonical: https://grafana.com/docs/grafana/latest/alerting/alerting-rules/create-mimir-loki-managed-recording-rule/ -description: Configure recording rules for an external Grafana Mimir or Loki instance +description: Create recording rules for an external Grafana Mimir or Loki instance keywords: - grafana - alerting @@ -16,13 +16,14 @@ labels: - cloud - enterprise - oss -title: Configure recording rules +title: Create recording rules weight: 300 --- -# Configure recording rules +# Create recording rules -You can create and manage recording rules for an external Grafana Mimir or Loki instance. Recording rules calculate frequently needed expressions or computationally expensive expressions in advance and save the result as a new set of time series. Querying this new time series is faster, especially for dashboards since they query the same expression every time the dashboards refresh. +You can create and manage recording rules for an external Grafana Mimir or Loki instance. +Recording rules calculate frequently needed expressions or computationally expensive expressions in advance and save the result as a new set of time series. Querying this new time series is faster, especially for dashboards since they query the same expression every time the dashboards refresh. **Note:** @@ -48,13 +49,14 @@ To create recording rules, follow these steps. 1. Click **Alerts & IRM** -> **Alerting** -> **Alert rules**. -1. Click **New recording rule**. +1. Select **Rule type** -> **Recording**. +1. Click **+New recording rule**. -1. Set rule name. +1. Enter recording rule name. The recording rule name must be a Prometheus metric name and contain no whitespace. -1. Define query. +1. Define recording rule. - Select your Loki or Prometheus data source. - Enter a query. 1. Add namespace and group. diff --git a/docs/sources/alerting/alerting-rules/manage-contact-points/_index.md b/docs/sources/alerting/alerting-rules/manage-contact-points/_index.md deleted file mode 100644 index 186c487914..0000000000 --- a/docs/sources/alerting/alerting-rules/manage-contact-points/_index.md +++ /dev/null @@ -1,77 +0,0 @@ ---- -aliases: - - ../contact-points/ # /docs/grafana//alerting/contact-points/ - - ../contact-points/create-contact-point/ # /docs/grafana//alerting/contact-points/create-contact-point/ - - ../contact-points/delete-contact-point/ # /docs/grafana//alerting/contact-points/delete-contact-point/ - - ../contact-points/edit-contact-point/ # /docs/grafana//alerting/contact-points/edit-contact-point/ - - ../contact-points/test-contact-point/ # /docs/grafana//alerting/contact-points/test-contact-point/ - - ../manage-notifications/manage-contact-points/ # /docs/grafana//alerting/manage-notifications/manage-contact-points/ - - create-contact-point/ # /docs/grafana//alerting/alerting-rules/create-contact-point/ -canonical: https://grafana.com/docs/grafana/latest/alerting/alerting-rules/manage-contact-points/ -description: Configure contact points to define how your contacts are notified when an alert rule fires -keywords: - - grafana - - alerting - - guide - - contact point - - templating -labels: - products: - - cloud - - enterprise - - oss -title: Configure contact points -weight: 410 ---- - -# Configure contact points - -Use contact points to define how your contacts are notified when an alert rule fires. You can add, edit, delete, and test a contact point. - -## Add a contact point - -Complete the following steps to add a contact point. - -1. In the left-side menu, click **Alerts & IRM** and then **Alerting**. -1. Click **Contact points**. -1. From the **Choose Alertmanager** dropdown, select an Alertmanager. By default, **Grafana Alertmanager** is selected. -1. On the **Contact Points** tab, click **+ Add contact point**. -1. Enter a descriptive name for the contact point. -1. From **Integration**, select a type and fill out mandatory fields. For example, if you choose email, enter the email addresses. Or if you choose Slack, enter the Slack channel(s) and users who should be contacted. -1. Some contact point integrations, like email or webhook, have optional settings. In **Optional settings**, specify additional settings for the selected contact point integration. -1. In Notification settings, optionally select **Disable resolved message** if you do not want to be notified when an alert resolves. -1. To add another contact point integration, click **Add contact point integration** and repeat steps 6 through 8. -1. Save your changes. - -## Edit a contact point - -Complete the following steps to edit a contact point. - -1. In the left-side menu, click **Alerts & IRM** and then **Alerting**. -1. Click **Contact points** to view a list of existing contact points. -1. On the **Contact Points** tab, find the contact point you want to edit, and then click **Edit**. -1. Update the contact point and save your changes. - -## Delete a contact point - -Complete the following steps to delete a contact point. - -1. In the left-side menu, click **Alerts & IRM** and then **Alerting**. -1. Click **Contact points** to view a list of existing contact points. -1. On the **Contact Points** tab, find the contact point you want to delete, and then click **More** -> **Delete**. -1. In the confirmation dialog, click **Yes, delete**. - -{{% admonition type="note" %}} -You cannot delete contact points that are in use by a notification policy. Either delete the notification policy or update it to use another contact point. -{{% /admonition %}} - -## Test a contact point - -Complete the following steps to test a contact point. - -1. In the left-side menu, click **Alerts & IRM** and then **Alerting**. -1. Click **Contact points** to view a list of existing contact points. -1. On the **Contact Points** tab, find the contact point you want to test, then click **Edit**. You can also create a new contact point if needed. -1. Click **Test** to open the contact point testing modal. -1. Choose whether to send a predefined test notification or choose custom to add your own custom annotations and labels to include in the notification. -1. Click **Send test notification** to fire the alert. diff --git a/docs/sources/alerting/alerting-rules/manage-contact-points/integrations/_index.md b/docs/sources/alerting/alerting-rules/manage-contact-points/integrations/_index.md deleted file mode 100644 index dfd1efd3c6..0000000000 --- a/docs/sources/alerting/alerting-rules/manage-contact-points/integrations/_index.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -aliases: - - alerting/manage-notifications/manage-contact-points/configure-integrations/ -canonical: https://grafana.com/docs/grafana/latest/alerting/alerting-rules/manage-contact-points/integrations/ -description: Configure contact point integrations to select your preferred communication channels for receiving notifications of firing alerts. -keywords: - - Grafana - - alerting - - guide - - notifications - - integrations - - contact points -labels: - products: - - cloud - - enterprise - - oss -title: Configure contact point integrations -weight: 100 ---- - -# Configure contact point integrations - -Configure contact point integrations in Grafana to select your preferred communication channel for receiving notifications when your alert rules are firing. Each integration has its own configuration options and setup process. In most cases, this involves providing an API key or a Webhook URL. - -Once configured, you can use integrations as part of your contact points to receive notifications whenever your alert changes its state. In this section, we'll cover the basic steps to configure your integrations, so you can start receiving real-time alerts and stay on top of your monitoring data. - -## List of supported integrations - -| Name | Type | -| ----------------------- | ------------------------- | -| DingDing | `dingding` | -| Discord | `discord` | -| Email | `email` | -| Google Chat | `googlechat` | -| Hipchat | `hipchat` | -| Kafka | `kafka` | -| Line | `line` | -| Microsoft Teams | `teams` | -| Opsgenie | `opsgenie` | -| Pagerduty | `pagerduty` | -| Prometheus Alertmanager | `prometheus-alertmanager` | -| Pushover | `pushover` | -| Sensu | `sensu` | -| Sensu Go | `sensugo` | -| Slack | `slack` | -| Telegram | `telegram` | -| Threema | `threema` | -| VictorOps | `victorops` | -| Webhook | `webhook` | diff --git a/docs/sources/alerting/configure-notifications/_index.md b/docs/sources/alerting/configure-notifications/_index.md new file mode 100644 index 0000000000..f709efed8f --- /dev/null +++ b/docs/sources/alerting/configure-notifications/_index.md @@ -0,0 +1,26 @@ +--- +canonical: https://grafana.com/docs/grafana/latest/alerting/configure-notifications +description: Configure how, when, and where to send your alert notifications +keywords: + - grafana + - alert + - notifications +labels: + products: + - cloud + - enterprise + - oss +menuTitle: Configure notifications +title: Configure notifications +weight: 125 +--- + +# Configure notifications + +Choose how, when, and where to send your alert notifications. + +As a first step, define your contact points; where to send your alert notifications to. A contact point is a set of one or more integrations that are used to deliver notifications. + +Next, create a notification policy which is a set of rules for where, when and how your alerts are routed to contact points. In a notification policy, you define where to send your alert notifications by choosing one of the contact points you created. + +Optionally, you can add notification templates to contact points for reuse and consistent messaging in your notifications. diff --git a/docs/sources/alerting/alerting-rules/create-notification-policy.md b/docs/sources/alerting/configure-notifications/create-notification-policy.md similarity index 93% rename from docs/sources/alerting/alerting-rules/create-notification-policy.md rename to docs/sources/alerting/configure-notifications/create-notification-policy.md index 3df48d66ae..55d0e33192 100644 --- a/docs/sources/alerting/alerting-rules/create-notification-policy.md +++ b/docs/sources/alerting/configure-notifications/create-notification-policy.md @@ -1,9 +1,10 @@ --- aliases: - - ../notifications/ - - ../old-alerting/notifications/ - - ../unified-alerting/notifications/ -canonical: https://grafana.com/docs/grafana/latest/alerting/alerting-rules/create-notification-policy/ + - ../notifications/ # /docs/grafana/latest/alerting/notifications/ + - ../old-alerting/notifications/ # /docs/grafana/latest/alerting/old-alerting/notifications/ + - ../unified-alerting/notifications/ # /docs/grafana/latest/alerting/unified-alerting/notifications/ + - ../alerting-rules/create-notification-policy/ # /docs/grafana/latest/alerting/alerting-rules/create-notification-policy/ +canonical: https://grafana.com/docs/grafana/latest/alerting/configure-notifications/create-notification-policy/ description: Configure notification policies to determine how alerts are routed to contact points keywords: - grafana @@ -17,7 +18,7 @@ labels: - enterprise - oss title: Configure notification policies -weight: 420 +weight: 430 --- # Configure notification policies diff --git a/docs/sources/alerting/manage-notifications/create-silence.md b/docs/sources/alerting/configure-notifications/create-silence.md similarity index 80% rename from docs/sources/alerting/manage-notifications/create-silence.md rename to docs/sources/alerting/configure-notifications/create-silence.md index 3b0d544360..874cd40df5 100644 --- a/docs/sources/alerting/manage-notifications/create-silence.md +++ b/docs/sources/alerting/configure-notifications/create-silence.md @@ -1,12 +1,13 @@ --- aliases: - - ../silences/create-silence/ - - ../silences/edit-silence/ - - ../silences/linking-to-silence-form/ - - ../silences/remove-silence/ - - ../unified-alerting/silences/ - - ../silences/ -canonical: https://grafana.com/docs/grafana/latest/alerting/manage-notifications/create-silence/ + - ../silences/create-silence/ # /docs/grafana/latest/alerting/silences/create-silence/ + - ../silences/edit-silence/ # /docs/grafana/latest/alerting/silences/edit-silence/ + - ../silences/linking-to-silence-form/ # /docs/grafana/latest/alerting/silences/linking-to-silence-form/ + - ../silences/remove-silence/ # /docs/grafana/latest/alerting/silences/remove-silence/ + - ../unified-alerting/silences/ # /docs/grafana/latest/alerting/unified-alerting/silences/ + - ../silences/ # /docs/grafana/latest/alerting/silences/ + - ../manage-notifications/create-silence/ # /docs/grafana/latest/alerting/manage-notifications/create-silence/ +canonical: https://grafana.com/docs/grafana/latest/alerting/configure-notifications/create-silence/ description: Create silences to stop notifications from getting created for a specified window of time keywords: - grafana @@ -19,7 +20,7 @@ labels: - enterprise - oss title: Manage silences -weight: 410 +weight: 440 --- # Manage silences diff --git a/docs/sources/alerting/configure-notifications/manage-contact-points/_index.md b/docs/sources/alerting/configure-notifications/manage-contact-points/_index.md new file mode 100644 index 0000000000..17071b8b13 --- /dev/null +++ b/docs/sources/alerting/configure-notifications/manage-contact-points/_index.md @@ -0,0 +1,142 @@ +--- +aliases: + - ../contact-points/ # /docs/grafana//alerting/contact-points/ + - ../contact-points/create-contact-point/ # /docs/grafana//alerting/contact-points/create-contact-point/ + - ../contact-points/delete-contact-point/ # /docs/grafana//alerting/contact-points/delete-contact-point/ + - ../contact-points/edit-contact-point/ # /docs/grafana//alerting/contact-points/edit-contact-point/ + - ../contact-points/test-contact-point/ # /docs/grafana//alerting/contact-points/test-contact-point/ + - ../manage-notifications/manage-contact-points/ # /docs/grafana//alerting/manage-notifications/manage-contact-points/ + - ../alerting-rules/create-contact-point/ # /docs/grafana//alerting/alerting-rules/create-contact-point/ + - ../alerting-rules/manage-contact-points/ # /docs/grafana/latest/alerting/alerting-rules/manage-contact-points/ + - ../alerting-rules/create-notification-policy/ # /docs/grafana/latest/alerting/alerting-rules/create-notification-policy/ + - ../alerting-rules/manage-contact-points/integrations/ # /docs/grafana/latest/alerting/alerting-rules/manage-contact-points/integrations/ + - ../manage-notifications/manage-contact-points/ # /docs/grafana/latest/alerting/manage-notifications/manage-contact-points/ +canonical: https://grafana.com/docs/grafana/latest/alerting/configure-notifications/manage-contact-points/ +description: Configure contact points to define how your contacts are notified when an alert rule fires +keywords: + - grafana + - alerting + - guide + - contact point + - templating +labels: + products: + - cloud + - enterprise + - oss +title: Configure contact points +weight: 410 +--- + +# Configure contact points + +Use contact points to define how your contacts are notified when an alert rule fires. You can add, edit, delete, and test a contact point. + +Configure contact point integrations in Grafana to select your preferred communication channel for receiving notifications when your alert rules are firing. + +## Add a contact point + +Complete the following steps to add a contact point. + +1. In the left-side menu, click **Alerts & IRM** and then **Alerting**. +1. Click **Contact points**. +1. From the **Choose Alertmanager** dropdown, select an Alertmanager. By default, **Grafana Alertmanager** is selected. +1. On the **Contact Points** tab, click **+ Add contact point**. +1. Enter a descriptive name for the contact point. +1. From **Integration**, select a type and fill out mandatory fields. For example, if you choose email, enter the email addresses. Or if you choose Slack, enter the Slack channel(s) and users who should be contacted. +1. Some contact point integrations, like email or webhook, have optional settings. In **Optional settings**, specify additional settings for the selected contact point integration. +1. In Notification settings, optionally select **Disable resolved message** if you do not want to be notified when an alert resolves. +1. To add another contact point integration, click **Add contact point integration** and repeat steps 6 through 8. +1. Save your changes. + +## Edit a contact point + +Complete the following steps to edit a contact point. + +1. In the left-side menu, click **Alerts & IRM** and then **Alerting**. +1. Click **Contact points** to view a list of existing contact points. +1. On the **Contact Points** tab, find the contact point you want to edit, and then click **Edit**. +1. Update the contact point and save your changes. + +## Delete a contact point + +Complete the following steps to delete a contact point. + +1. In the left-side menu, click **Alerts & IRM** and then **Alerting**. +1. Click **Contact points** to view a list of existing contact points. +1. On the **Contact Points** tab, find the contact point you want to delete, and then click **More** -> **Delete**. +1. In the confirmation dialog, click **Yes, delete**. + +{{% admonition type="note" %}} +You cannot delete contact points that are in use by a notification policy. Either delete the notification policy or update it to use another contact point. +{{% /admonition %}} + +## Test a contact point + +Complete the following steps to test a contact point. + +1. In the left-side menu, click **Alerts & IRM** and then **Alerting**. +1. Click **Contact points** to view a list of existing contact points. +1. On the **Contact Points** tab, find the contact point you want to test, then click **Edit**. You can also create a new contact point if needed. +1. Click **Test** to open the contact point testing modal. +1. Choose whether to send a predefined test notification or choose custom to add your own custom annotations and labels to include in the notification. +1. Click **Send test notification** to fire the alert. + +## Manage contact points + +The Contact points list view lists all existing contact points and notification templates. + +On the **Contact Points** tab, you can: + +- Search for name and type of contact points and integrations +- View all existing contact points and integrations +- View how many notification policies each contact point is being used for and navigate directly to the linked notification policies +- View the status of notification deliveries +- Export individual contact points or all contact points in JSON, YAML, or Terraform format +- Delete contact points that are not in use by a notification policy + +On the **Notification templates** tab, you can: + +- View, edit, copy or delete existing notification templates + +## Configure contact point integrations + +Each contact point integration has its own configuration options and setup process. In most cases, this involves providing an API key or a Webhook URL. + +Once configured, you can use integrations as part of your contact points to receive notifications whenever your alert changes its state. In this section, we'll cover the basic steps to configure your integrations, so you can start receiving real-time alerts and stay on top of your monitoring data. + +## List of supported integrations + +| Name | Type | +| ------------------------ | ------------------------- | +| DingDing | `dingding` | +| Discord | `discord` | +| Email | `email` | +| Google Chat | `googlechat` | +| [Grafana Oncall][oncall] | `oncall` | +| Hipchat | `hipchat` | +| Kafka | `kafka` | +| Line | `line` | +| Microsoft Teams | `teams` | +| Opsgenie | `opsgenie` | +| [Pagerduty][pagerduty] | `pagerduty` | +| Prometheus Alertmanager | `prometheus-alertmanager` | +| Pushover | `pushover` | +| Sensu | `sensu` | +| Sensu Go | `sensugo` | +| Slack | `slack` | +| Telegram | `telegram` | +| Threema | `threema` | +| VictorOps | `victorops` | +| [Webhook][webhook] | `webhook` | + +{{% docs/reference %}} +[pagerduty]: "/docs/grafana/ -> /docs/grafana//alerting/configure-notifications/manage-contact-points/integrations/pager-duty" +[pagerduty]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/manage-contact-points/integrations/pager-duty" + +[oncall]: "/docs/grafana/ -> /docs/grafana//alerting/configure-notifications/manage-contact-points/integrations/configure-oncall" +[oncall]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/manage-contact-points/integrations/configure-oncall" + +[webhook]: "/docs/grafana/ -> /docs/grafana//alerting/configure-notifications/manage-contact-points/integrations/webhook-notifier" +[webhook]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/manage-contact-points/integrations/webhook-notifier" +{{% /docs/reference %}} diff --git a/docs/sources/alerting/alerting-rules/manage-contact-points/integrations/configure-oncall.md b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-oncall.md similarity index 83% rename from docs/sources/alerting/alerting-rules/manage-contact-points/integrations/configure-oncall.md rename to docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-oncall.md index f71e505303..f1561fc25d 100644 --- a/docs/sources/alerting/alerting-rules/manage-contact-points/integrations/configure-oncall.md +++ b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-oncall.md @@ -1,5 +1,8 @@ --- -canonical: https://grafana.com/docs/grafana/latest/alerting/alerting-rules/manage-contact-points/integrations/configure-oncall/ +aliases: + - ../../../alerting-rules/manage-contact-points/configure-oncall/ # /docs/grafana/latest/alerting/alerting-rules/manage-contact-points/configure-oncall/ + - ../../../alerting-rules/manage-contact-points/integrations/configure-oncall/ # /docs/grafana/latest/alerting/alerting-rules/manage-contact-points/integrations/configure-oncall/ +canonical: https://grafana.com/docs/grafana/latest/alerting/configure-notifications/manage-contact-points/integrations/configure-oncall/ description: Configure the Alerting - Grafana OnCall integration to connect alerts generated by Grafana Alerting with Grafana OnCall keywords: - grafana @@ -63,8 +66,8 @@ To set up the Grafana OnCall integration using the Grafana Alerting application, This redirects you to the Grafana OnCall integration page in the Grafana OnCall application. From there, you can add [routes and escalation chains][escalation-chain]. {{% docs/reference %}} -[create-notification-policy]: "/docs/grafana/ -> /docs/grafana//alerting/alerting-rules/create-notification-policy" -[create-notification-policy]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/alerting-rules/create-notification-policy" +[create-notification-policy]: "/docs/grafana/ -> /docs/grafana//alerting/configure-notifications/create-notification-policy" +[create-notification-policy]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/create-notification-policy" [oncall-integration]: "/docs/grafana/ -> /docs/oncall/latest/integrations/grafana-alerting" [oncall-integration]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/oncall/integrations/grafana-alerting" diff --git a/docs/sources/alerting/alerting-rules/manage-contact-points/integrations/pager-duty.md b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/pager-duty.md similarity index 80% rename from docs/sources/alerting/alerting-rules/manage-contact-points/integrations/pager-duty.md rename to docs/sources/alerting/configure-notifications/manage-contact-points/integrations/pager-duty.md index da79c613eb..d3826bfb2e 100644 --- a/docs/sources/alerting/alerting-rules/manage-contact-points/integrations/pager-duty.md +++ b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/pager-duty.md @@ -1,5 +1,7 @@ --- -canonical: https://grafana.com/docs/grafana/latest/alerting/alerting-rules/manage-contact-points/integrations/pager-duty/ +aliases: + - ../../../alerting-rules/manage-contact-points/integrations/pager-duty/ # /docs/grafana/latest/alerting/alerting-rules/manage-contact-points/integrations/pager-duty/ +canonical: https://grafana.com/docs/grafana/latest/alerting/configure-notifications/manage-contact-points/integrations/pager-duty/ description: Configure the PagerDuty integration for Alerting keywords: - grafana diff --git a/docs/sources/alerting/alerting-rules/manage-contact-points/integrations/webhook-notifier.md b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/webhook-notifier.md similarity index 90% rename from docs/sources/alerting/alerting-rules/manage-contact-points/integrations/webhook-notifier.md rename to docs/sources/alerting/configure-notifications/manage-contact-points/integrations/webhook-notifier.md index 77004bc920..7e11d78576 100644 --- a/docs/sources/alerting/alerting-rules/manage-contact-points/integrations/webhook-notifier.md +++ b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/webhook-notifier.md @@ -1,9 +1,12 @@ --- aliases: - - ../contact-points/notifiers/webhook-notifier/ - - ../fundamentals/contact-points/webhook-notifier/ + - ../../../fundamentals/contact-points/notifiers/webhook-notifier/ # /docs/grafana/latest/alerting/fundamentals/contact-points/notifiers/webhook-notifier/ + - ../../../fundamentals/contact-points/webhook-notifier/ # /docs/grafana/latest/alerting/fundamentals/contact-points/webhook-notifier/ + - ../../../manage-notifications/manage-contact-points/webhook-notifier/ # /docs/grafana/latest/alerting/manage-notifications/manage-contact-points/webhook-notifier/ - alerting/manage-notifications/manage-contact-points/webhook-notifier/ -canonical: https://grafana.com/docs/grafana/latest/alerting/alerting-rules/manage-contact-points/integrations/webhook-notifier/ + - ../../../alerting-rules/manage-contact-points/integrations/webhook-notifier/ # /docs/grafana/latest/alerting/alerting-rules/manage-contact-points/integrations/webhook-notifier/ + +canonical: https://grafana.com/docs/grafana/latest/alerting/configure-notifications/manage-contact-points/integrations/webhook-notifier/ description: Configure the webhook notifier integration for Alerting keywords: - grafana diff --git a/docs/sources/alerting/manage-notifications/mute-timings.md b/docs/sources/alerting/configure-notifications/mute-timings.md similarity index 91% rename from docs/sources/alerting/manage-notifications/mute-timings.md rename to docs/sources/alerting/configure-notifications/mute-timings.md index e3337f7877..2c4d9a52dc 100644 --- a/docs/sources/alerting/manage-notifications/mute-timings.md +++ b/docs/sources/alerting/configure-notifications/mute-timings.md @@ -1,8 +1,9 @@ --- aliases: - - ../notifications/mute-timings/ - - ../unified-alerting/notifications/mute-timings/ -canonical: https://grafana.com/docs/grafana/latest/alerting/manage-notifications/mute-timings/ + - ../notifications/mute-timings/ # /docs/grafana/latest/alerting/notifications/mute-timings/ + - ../unified-alerting/notifications/mute-timings/ # /docs/grafana/latest/alerting/unified-alerting/notifications/mute-timings/ + - ../manage-notifications/mute-timings/ # /docs/grafana/latest/alerting/manage-notifications/mute-timings/ +canonical: /docs/grafana/latest/alerting/configure-notifications/mute-timings/ description: Create mute timings to prevent alerts from firing during a specific and reoccurring period of time keywords: - grafana @@ -17,7 +18,7 @@ labels: - enterprise - oss title: Create mute timings -weight: 420 +weight: 450 --- # Create mute timings diff --git a/docs/sources/alerting/manage-notifications/template-notifications/_index.md b/docs/sources/alerting/configure-notifications/template-notifications/_index.md similarity index 74% rename from docs/sources/alerting/manage-notifications/template-notifications/_index.md rename to docs/sources/alerting/configure-notifications/template-notifications/_index.md index 1a5507f577..c096b044ca 100644 --- a/docs/sources/alerting/manage-notifications/template-notifications/_index.md +++ b/docs/sources/alerting/configure-notifications/template-notifications/_index.md @@ -1,5 +1,7 @@ --- -canonical: https://grafana.com/docs/grafana/latest/alerting/manage-notifications/template-notifications/ +aliases: + - ../manage-notifications/template-notifications/ # /docs/grafana/latest/alerting/manage-notifications/template-notifications/ +canonical: https://grafana.com/docs/grafana/latest/alerting/configure-notifications/template-notifications/ description: Customize your notifications using notification templates keywords: - grafana @@ -12,7 +14,7 @@ labels: - enterprise - oss title: Customize notifications -weight: 400 +weight: 420 --- # Customize notifications @@ -52,12 +54,12 @@ Use notification templates to send notifications to your contact points. Data that is available when writing templates. {{% docs/reference %}} -[reference]: "/docs/grafana/ -> /docs/grafana//alerting/manage-notifications/template-notifications/reference" -[reference]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/manage-notifications/template-notifications/reference" +[reference]: "/docs/grafana/ -> /docs/grafana//alerting/configure-notifications/template-notifications/reference" +[reference]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/template-notifications/reference" -[use-notification-templates]: "/docs/grafana/ -> /docs/grafana//alerting/manage-notifications/template-notifications/use-notification-templates" -[use-notification-templates]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/manage-notifications/template-notifications/use-notification-templates" +[use-notification-templates]: "/docs/grafana/ -> /docs/grafana//alerting/configure-notifications/template-notifications/use-notification-templates" +[use-notification-templates]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/template-notifications/use-notification-templates" -[using-go-templating-language]: "/docs/grafana/ -> /docs/grafana//alerting/manage-notifications/template-notifications/using-go-templating-language" -[using-go-templating-language]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/manage-notifications/template-notifications/using-go-templating-language" +[using-go-templating-language]: "/docs/grafana/ -> /docs/grafana//alerting/configure-notifications/template-notifications/using-go-templating-language" +[using-go-templating-language]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/template-notifications/using-go-templating-language" {{% /docs/reference %}} diff --git a/docs/sources/alerting/manage-notifications/template-notifications/create-notification-templates.md b/docs/sources/alerting/configure-notifications/template-notifications/create-notification-templates.md similarity index 96% rename from docs/sources/alerting/manage-notifications/template-notifications/create-notification-templates.md rename to docs/sources/alerting/configure-notifications/template-notifications/create-notification-templates.md index 947556e4e2..7efefb3237 100644 --- a/docs/sources/alerting/manage-notifications/template-notifications/create-notification-templates.md +++ b/docs/sources/alerting/configure-notifications/template-notifications/create-notification-templates.md @@ -1,5 +1,7 @@ --- -canonical: https://grafana.com/docs/grafana/latest/alerting/manage-notifications/template-notifications/create-notification-templates/ +aliases: + - ../../manage-notifications/template-notifications/create-notification-templates/ # /docs/grafana/latest/alerting/manage-notifications/template-notifications/create-notification-templates/ +canonical: https://grafana.com/docs/grafana/latest/alerting/configure-notifications/template-notifications/create-notification-templates/ description: Create notification templates to sent to your contact points keywords: - grafana diff --git a/docs/sources/alerting/manage-notifications/images-in-notifications.md b/docs/sources/alerting/configure-notifications/template-notifications/images-in-notifications.md similarity index 95% rename from docs/sources/alerting/manage-notifications/images-in-notifications.md rename to docs/sources/alerting/configure-notifications/template-notifications/images-in-notifications.md index 89807f1c8b..ade25b1e21 100644 --- a/docs/sources/alerting/manage-notifications/images-in-notifications.md +++ b/docs/sources/alerting/configure-notifications/template-notifications/images-in-notifications.md @@ -1,5 +1,7 @@ --- -canonical: https://grafana.com/docs/grafana/latest/alerting/manage-notifications/images-in-notifications/ +aliases: + - ../manage-notifications/images-in-notifications/ # /docs/grafana/latest/alerting/manage-notifications/images-in-notifications/ +canonical: https://grafana.com/docs/grafana/latest/alerting/configure-notifications/template-notifications/images-in-notifications/ description: Use images in notifications to help users better understand why alerts are firing or have been resolved keywords: - grafana @@ -12,7 +14,7 @@ labels: - enterprise - oss title: Use images in notifications -weight: 405 +weight: 500 --- # Use images in notifications @@ -33,7 +35,7 @@ Refer to the table at the end of this page for a list of contact points and thei ## Requirements -1. To use images in notifications, Grafana must be set up to use [image rendering][image-rendering]. You can either install the image rendering plugin or run it as a remote rendering service. +1. To use images in notifications, Grafana must be set up to use 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][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. @@ -69,8 +71,6 @@ If screenshots should be uploaded to cloud storage then `upload_external_image_s # will be persisted to disk for up to temp_data_lifetime. upload_external_image_storage = false -For more information on image rendering, refer to [image rendering][image-rendering]. - Restart Grafana for the changes to take effect. ## Advanced configuration @@ -137,9 +137,3 @@ For example, if a screenshot could not be taken within the expected time (10 sec - `grafana_screenshot_successes_total` - `grafana_screenshot_upload_failures_total` - `grafana_screenshot_upload_successes_total` - -{{% docs/reference %}} -[image-rendering]: "/docs/ -> /docs/grafana//setup-grafana/image-rendering" - -[paths]: "/docs/ -> /docs/grafana//setup-grafana/configure-grafana#paths" -{{% /docs/reference %}} diff --git a/docs/sources/alerting/manage-notifications/template-notifications/reference.md b/docs/sources/alerting/configure-notifications/template-notifications/reference.md similarity index 95% rename from docs/sources/alerting/manage-notifications/template-notifications/reference.md rename to docs/sources/alerting/configure-notifications/template-notifications/reference.md index 16c26acc6d..650dc9becc 100644 --- a/docs/sources/alerting/manage-notifications/template-notifications/reference.md +++ b/docs/sources/alerting/configure-notifications/template-notifications/reference.md @@ -1,5 +1,7 @@ --- -canonical: https://grafana.com/docs/grafana/latest/alerting/manage-notifications/template-notifications/reference/ +aliases: + - ../../manage-notifications/template-notifications/reference/ # /docs/grafana/latest/alerting/manage-notifications/template-notifications/reference/ +canonical: https://grafana.com/docs/grafana/latest/alerting/configure-notifications/template-notifications/reference/ description: Learn about templating notifications options keywords: - grafana diff --git a/docs/sources/alerting/manage-notifications/template-notifications/use-notification-templates.md b/docs/sources/alerting/configure-notifications/template-notifications/use-notification-templates.md similarity index 63% rename from docs/sources/alerting/manage-notifications/template-notifications/use-notification-templates.md rename to docs/sources/alerting/configure-notifications/template-notifications/use-notification-templates.md index a2a341e02c..aad06d1b11 100644 --- a/docs/sources/alerting/manage-notifications/template-notifications/use-notification-templates.md +++ b/docs/sources/alerting/configure-notifications/template-notifications/use-notification-templates.md @@ -1,5 +1,7 @@ --- -canonical: https://grafana.com/docs/grafana/latest/alerting/manage-notifications/template-notifications/use-notification-templates/ +aliases: + - ../../manage-notifications/template-notifications/use-notification-templates/ # /docs/grafana/latest/alerting/manage-notifications/template-notifications/use-notification-templates/ +canonical: https://grafana.com/docs/grafana/latest/alerting/configure-notifications/template-notifications/use-notification-templates/ description: Use notification templates in contact points to customize your notifications keywords: - grafana @@ -35,9 +37,9 @@ In the Contact points tab, you can see a list of your contact points. 1. Click **Save contact point**. {{% docs/reference %}} -[create-notification-templates]: "/docs/grafana/ -> /docs/grafana//alerting/manage-notifications/template-notifications/create-notification-templates" -[create-notification-templates]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/manage-notifications/template-notifications/create-notification-templates" +[create-notification-templates]: "/docs/grafana/ -> /docs/grafana//alerting/configure-notifications/template-notifications/create-notification-templates" +[create-notification-templates]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/template-notifications/create-notification-templates" -[using-go-templating-language]: "/docs/grafana/ -> /docs/grafana//alerting/manage-notifications/template-notifications/using-go-templating-language" -[using-go-templating-language]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/manage-notifications/template-notifications/using-go-templating-language" +[using-go-templating-language]: "/docs/grafana/ -> /docs/grafana//alerting/configure-notifications/template-notifications/using-go-templating-language" +[using-go-templating-language]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/template-notifications/using-go-templating-language" {{% /docs/reference %}} diff --git a/docs/sources/alerting/manage-notifications/template-notifications/using-go-templating-language.md b/docs/sources/alerting/configure-notifications/template-notifications/using-go-templating-language.md similarity index 91% rename from docs/sources/alerting/manage-notifications/template-notifications/using-go-templating-language.md rename to docs/sources/alerting/configure-notifications/template-notifications/using-go-templating-language.md index 50115d3f23..dbf69d0a69 100644 --- a/docs/sources/alerting/manage-notifications/template-notifications/using-go-templating-language.md +++ b/docs/sources/alerting/configure-notifications/template-notifications/using-go-templating-language.md @@ -1,5 +1,7 @@ --- -canonical: https://grafana.com/docs/grafana/latest/alerting/manage-notifications/template-notifications/using-go-templating-language/ +aliases: + - ../../manage-notifications/template-notifications/using-go-templating-language/ # /docs/grafana/latest/alerting/manage-notifications/template-notifications/using-go-templating-language/ +canonical: https://grafana.com/docs/grafana/latest/alerting/configure-notifications/template-notifications/using-go-templating-language/ description: Use Go's templating language to create your own notification templates keywords: - grafana @@ -280,12 +282,12 @@ grafana_folder = "Test alerts" ``` {{% docs/reference %}} -[create-notification-templates]: "/docs/grafana/ -> /docs/grafana//alerting/manage-notifications/template-notifications/create-notification-templates" -[create-notification-templates]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/manage-notifications/template-notifications/create-notification-templates" +[create-notification-templates]: "/docs/grafana/ -> /docs/grafana//alerting/configure-notifications/template-notifications/create-notification-templates" +[create-notification-templates]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/template-notifications/create-notification-templates" -[extendeddata]: "/docs/grafana/ -> /docs/grafana//alerting/manage-notifications/template-notifications/reference#extendeddata" -[extendeddata]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/manage-notifications/template-notifications/reference#extendeddata" +[extendeddata]: "/docs/grafana/ -> /docs/grafana//alerting/configure-notifications/template-notifications/reference#extendeddata" +[extendeddata]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/template-notifications/reference#extendeddata" -[reference]: "/docs/grafana/ -> /docs/grafana//alerting/manage-notifications/template-notifications/reference" -[reference]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/manage-notifications/template-notifications/reference" +[reference]: "/docs/grafana/ -> /docs/grafana//alerting/configure-notifications/template-notifications/reference" +[reference]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/template-notifications/reference" {{% /docs/reference %}} diff --git a/docs/sources/alerting/manage-notifications/_index.md b/docs/sources/alerting/manage-notifications/_index.md index 1ea9bc2492..9adb839afc 100644 --- a/docs/sources/alerting/manage-notifications/_index.md +++ b/docs/sources/alerting/manage-notifications/_index.md @@ -1,47 +1,22 @@ --- canonical: https://grafana.com/docs/grafana/latest/alerting/manage-notifications/ -description: Manage your alerts by creating silences, mute timings, and more +description: Detect and respond for day-to-day triage and analysis of what’s going on and action you need to take keywords: - grafana - - alert - - notifications + - detect + - respond labels: products: - cloud - enterprise - oss -menuTitle: Manage -title: Manage your alerts +menuTitle: Detect and respond +title: Detect and respond weight: 130 --- -# Manage your alerts +# Detect and respond -Once you have set up your alert rules, contact points, and notification policies, you can use Grafana Alerting to: +Use Grafana Alerting to track and generate alerts and send notifications, providing an efficient way for engineers to monitor, respond, and triage issues within their services. -[Create silences][create-silence] - -[Create mute timings][mute-timings] - -[Declare incidents from firing alerts][declare-incident-from-firing-alert] - -[View the state and health of alert rules][view-state-health] - -[View and filter alert rules][view-alert-rules] - -{{% docs/reference %}} -[create-silence]: "/docs/grafana/ -> /docs/grafana//alerting/manage-notifications/create-silence" -[create-silence]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/manage-notifications/create-silence" - -[declare-incident-from-firing-alert]: "/docs/grafana/ -> /docs/grafana//alerting/manage-notifications/declare-incident-from-alert" -[declare-incident-from-firing-alert]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/manage-notifications/declare-incident-from-alert" - -[mute-timings]: "/docs/grafana/ -> /docs/grafana//alerting/manage-notifications/mute-timings" -[mute-timings]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/manage-notifications/mute-timings" - -[view-alert-rules]: "/docs/grafana/ -> /docs/grafana//alerting/manage-notifications/view-alert-rules" -[view-alert-rules]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/manage-notifications/view-alert-rules" - -[view-state-health]: "/docs/grafana/ -> /docs/grafana//alerting/manage-notifications/view-state-health" -[view-state-health]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/manage-notifications/view-state-health" -{{% /docs/reference %}} +Alerts and alert notifications provide a lot of value as key indicators to issues during the triage process, providing engineers with the information they need to understand what is going on in their system or service. diff --git a/docs/sources/alerting/manage-notifications/manage-contact-points.md b/docs/sources/alerting/manage-notifications/manage-contact-points.md deleted file mode 100644 index bbfd98e4b1..0000000000 --- a/docs/sources/alerting/manage-notifications/manage-contact-points.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -canonical: https://grafana.com/docs/grafana/latest/alerting/manage-notifications/manage-contact-points/ -description: View, edit, copy, or delete your contact points and notification templates -keywords: - - grafana - - alerting - - contact points - - search - - export -labels: - products: - - cloud - - enterprise - - oss -title: Manage contact points -weight: 410 ---- - -# Manage contact points - -The Contact points list view lists all existing contact points and notification templates. - -On the **Contact Points** tab, you can: - -- Search for name and type of contact points and integrations -- View all existing contact points and integrations -- View how many notification policies each contact point is being used for and navigate directly to the linked notification policies -- View the status of notification deliveries -- Export individual contact points or all contact points in JSON, YAML, or Terraform format -- Delete contact points that are not in use by a notification policy - -On the **Notification templates** tab, you can: - -- View, edit, copy or delete existing notification templates diff --git a/docs/sources/alerting/monitor/_index.md b/docs/sources/alerting/monitor/_index.md index 577910d569..6769d67c94 100644 --- a/docs/sources/alerting/monitor/_index.md +++ b/docs/sources/alerting/monitor/_index.md @@ -12,7 +12,7 @@ labels: products: - enterprise - oss -menuTitle: Monitor +menuTitle: Monitor alerting title: Meta monitoring weight: 140 --- diff --git a/docs/sources/setup-grafana/configure-grafana/_index.md b/docs/sources/setup-grafana/configure-grafana/_index.md index a7ddce0e8c..6d1627b8d3 100644 --- a/docs/sources/setup-grafana/configure-grafana/_index.md +++ b/docs/sources/setup-grafana/configure-grafana/_index.md @@ -1626,7 +1626,7 @@ The interval string is a possibly signed sequence of decimal numbers, followed b ## [unified_alerting.screenshots] -For more information about screenshots, refer to [Images in notifications]({{< relref "../../alerting/manage-notifications/images-in-notifications" >}}). +For more information about screenshots, refer to [Images in notifications]({{< relref "../../alerting/configure-notifications/template-notifications/images-in-notifications" >}}). ### capture From ffae7d111c905cbd79d48a6a2a866148b79ab856 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 27 Feb 2024 10:43:07 +0000 Subject: [PATCH 0220/1406] Update dependency rudder-sdk-js to v2.48.2 --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 622eb3c675..4d922f7fd7 100644 --- a/package.json +++ b/package.json @@ -206,7 +206,7 @@ "react-test-renderer": "18.2.0", "redux-mock-store": "1.5.4", "rimraf": "5.0.5", - "rudder-sdk-js": "2.48.1", + "rudder-sdk-js": "2.48.2", "sass": "1.70.0", "sass-loader": "14.1.1", "style-loader": "3.3.4", diff --git a/yarn.lock b/yarn.lock index 6536e4769c..e668ab9304 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18611,7 +18611,7 @@ __metadata: regenerator-runtime: "npm:0.14.1" reselect: "npm:4.1.8" rimraf: "npm:5.0.5" - rudder-sdk-js: "npm:2.48.1" + rudder-sdk-js: "npm:2.48.2" rxjs: "npm:7.8.1" sass: "npm:1.70.0" sass-loader: "npm:14.1.1" @@ -27720,9 +27720,9 @@ __metadata: languageName: node linkType: hard -"rudder-sdk-js@npm:2.48.1": - version: 2.48.1 - resolution: "rudder-sdk-js@npm:2.48.1" +"rudder-sdk-js@npm:2.48.2": + version: 2.48.2 + resolution: "rudder-sdk-js@npm:2.48.2" dependencies: "@lukeed/uuid": "npm:2.0.1" "@segment/localstorage-retry": "npm:1.3.0" @@ -27730,7 +27730,7 @@ __metadata: get-value: "npm:3.0.1" msw: "npm:1.3.2" ramda: "npm:0.29.1" - checksum: 10/0febca9dc854d1ee6ad020000aff4f8712f0efb2882e367d8f2c403731b9e25da79d74af2e62035784644ff3fb8fca4bf705ad2209c81751fad24352d679e171 + checksum: 10/56f9ac8a838a21a9b86b0161e69e6520388779cdcb108447d7bdb06111d61c6b44c29a536d9c08b6714745c40f7fdd4ce1d201eda4089f5230fc63fc7b9b5dd9 languageName: node linkType: hard From 8d9921a5bad0df535ee7b4cd5f70d43a44da0cde Mon Sep 17 00:00:00 2001 From: Gabriel MABILLE Date: Tue, 27 Feb 2024 12:21:26 +0100 Subject: [PATCH 0221/1406] RBAC: Fix delete team permissions on team delete (#83442) * RBAC: Remove team permissions on delete * Remove unecessary deletes from store function * Nit on mock * Add test to the database * Nit on comment * Add another test to check that other permissions remain --- pkg/services/accesscontrol/accesscontrol.go | 4 ++ pkg/services/accesscontrol/acimpl/service.go | 4 ++ pkg/services/accesscontrol/actest/fake.go | 8 +++ .../accesscontrol/actest/store_mock.go | 18 +++++ .../accesscontrol/database/database.go | 55 ++++++++++++++ .../accesscontrol/database/database_test.go | 71 +++++++++++++++++++ pkg/services/accesscontrol/mock/mock.go | 11 +++ pkg/services/team/teamapi/team.go | 6 ++ pkg/services/team/teamimpl/store.go | 6 +- 9 files changed, 178 insertions(+), 5 deletions(-) diff --git a/pkg/services/accesscontrol/accesscontrol.go b/pkg/services/accesscontrol/accesscontrol.go index a7ced89eb6..8098cdc140 100644 --- a/pkg/services/accesscontrol/accesscontrol.go +++ b/pkg/services/accesscontrol/accesscontrol.go @@ -35,6 +35,9 @@ type Service interface { // DeleteUserPermissions removes all permissions user has in org and all permission to that user // If orgID is set to 0 remove permissions from all orgs DeleteUserPermissions(ctx context.Context, orgID, userID int64) error + // DeleteTeamPermissions removes all role assignments and permissions granted to a team + // and removes permissions scoped to the team. + DeleteTeamPermissions(ctx context.Context, orgID, teamID int64) error // DeclareFixedRoles allows the caller to declare, to the service, fixed roles and their // assignments to organization roles ("Viewer", "Editor", "Admin") or "Grafana Admin" DeclareFixedRoles(registrations ...RoleRegistration) error @@ -52,6 +55,7 @@ type Store interface { SearchUsersPermissions(ctx context.Context, orgID int64, options SearchOptions) (map[int64][]Permission, error) GetUsersBasicRoles(ctx context.Context, userFilter []int64, orgID int64) (map[int64][]string, error) DeleteUserPermissions(ctx context.Context, orgID, userID int64) error + DeleteTeamPermissions(ctx context.Context, orgID, teamID int64) error SaveExternalServiceRole(ctx context.Context, cmd SaveExternalServiceRoleCommand) error DeleteExternalServiceRole(ctx context.Context, externalServiceID string) error } diff --git a/pkg/services/accesscontrol/acimpl/service.go b/pkg/services/accesscontrol/acimpl/service.go index 017e8b5599..529d17d6ea 100644 --- a/pkg/services/accesscontrol/acimpl/service.go +++ b/pkg/services/accesscontrol/acimpl/service.go @@ -166,6 +166,10 @@ func (s *Service) DeleteUserPermissions(ctx context.Context, orgID int64, userID return s.store.DeleteUserPermissions(ctx, orgID, userID) } +func (s *Service) DeleteTeamPermissions(ctx context.Context, orgID int64, teamID int64) error { + return s.store.DeleteTeamPermissions(ctx, orgID, teamID) +} + // DeclareFixedRoles allow the caller to declare, to the service, fixed roles and their assignments // to organization roles ("Viewer", "Editor", "Admin") or "Grafana Admin" func (s *Service) DeclareFixedRoles(registrations ...accesscontrol.RoleRegistration) error { diff --git a/pkg/services/accesscontrol/actest/fake.go b/pkg/services/accesscontrol/actest/fake.go index fa788b96f3..12e4330fee 100644 --- a/pkg/services/accesscontrol/actest/fake.go +++ b/pkg/services/accesscontrol/actest/fake.go @@ -41,6 +41,10 @@ func (f FakeService) DeleteUserPermissions(ctx context.Context, orgID, userID in return f.ExpectedErr } +func (f FakeService) DeleteTeamPermissions(ctx context.Context, orgID, teamID int64) error { + return f.ExpectedErr +} + func (f FakeService) DeclareFixedRoles(registrations ...accesscontrol.RoleRegistration) error { return f.ExpectedErr } @@ -94,6 +98,10 @@ func (f FakeStore) DeleteUserPermissions(ctx context.Context, orgID, userID int6 return f.ExpectedErr } +func (f FakeStore) DeleteTeamPermissions(ctx context.Context, orgID, teamID int64) error { + return f.ExpectedErr +} + func (f FakeStore) SaveExternalServiceRole(ctx context.Context, cmd accesscontrol.SaveExternalServiceRoleCommand) error { return f.ExpectedErr } diff --git a/pkg/services/accesscontrol/actest/store_mock.go b/pkg/services/accesscontrol/actest/store_mock.go index 12ef1560a8..70509a8842 100644 --- a/pkg/services/accesscontrol/actest/store_mock.go +++ b/pkg/services/accesscontrol/actest/store_mock.go @@ -51,6 +51,24 @@ func (_m *MockStore) DeleteUserPermissions(ctx context.Context, orgID int64, use return r0 } +// DeleteTeamPermissions provides a mock function with given fields: ctx, orgID, teamID +func (_m *MockStore) DeleteTeamPermissions(ctx context.Context, orgID int64, teamID int64) error { + ret := _m.Called(ctx, orgID, teamID) + + if len(ret) == 0 { + panic("no return value specified for DeleteTeamPermissions") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, int64, int64) error); ok { + r0 = rf(ctx, orgID, teamID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // GetUserPermissions provides a mock function with given fields: ctx, query func (_m *MockStore) GetUserPermissions(ctx context.Context, query accesscontrol.GetUserPermissionsQuery) ([]accesscontrol.Permission, error) { ret := _m.Called(ctx, query) diff --git a/pkg/services/accesscontrol/database/database.go b/pkg/services/accesscontrol/database/database.go index b914f2c3ff..6e61421ee2 100644 --- a/pkg/services/accesscontrol/database/database.go +++ b/pkg/services/accesscontrol/database/database.go @@ -241,3 +241,58 @@ func (s *AccessControlStore) DeleteUserPermissions(ctx context.Context, orgID, u }) return err } + +func (s *AccessControlStore) DeleteTeamPermissions(ctx context.Context, orgID, teamID int64) error { + err := s.sql.WithDbSession(ctx, func(sess *db.Session) error { + roleDeleteQuery := "DELETE FROM team_role WHERE team_id = ? AND org_id = ?" + roleDeleteParams := []any{roleDeleteQuery, teamID, orgID} + + // Delete team role assignments + if _, err := sess.Exec(roleDeleteParams...); err != nil { + return err + } + + // Delete permissions that are scoped to the team + if _, err := sess.Exec("DELETE FROM permission WHERE scope = ?", accesscontrol.Scope("teams", "id", strconv.FormatInt(teamID, 10))); err != nil { + return err + } + + // Delete the team managed role + roleQuery := "SELECT id FROM role WHERE name = ? AND org_id = ?" + roleParams := []any{accesscontrol.ManagedTeamRoleName(teamID), orgID} + + var roleIDs []int64 + if err := sess.SQL(roleQuery, roleParams...).Find(&roleIDs); err != nil { + return err + } + + if len(roleIDs) == 0 { + return nil + } + + permissionDeleteQuery := "DELETE FROM permission WHERE role_id IN(? " + strings.Repeat(",?", len(roleIDs)-1) + ")" + permissionDeleteParams := make([]any, 0, len(roleIDs)+1) + permissionDeleteParams = append(permissionDeleteParams, permissionDeleteQuery) + for _, id := range roleIDs { + permissionDeleteParams = append(permissionDeleteParams, id) + } + + // Delete managed team permissions + if _, err := sess.Exec(permissionDeleteParams...); err != nil { + return err + } + + managedRoleDeleteQuery := "DELETE FROM role WHERE id IN(? " + strings.Repeat(",?", len(roleIDs)-1) + ")" + managedRoleDeleteParams := []any{managedRoleDeleteQuery} + for _, id := range roleIDs { + managedRoleDeleteParams = append(managedRoleDeleteParams, id) + } + // Delete managed team role + if _, err := sess.Exec(managedRoleDeleteParams...); err != nil { + return err + } + + return nil + }) + return err +} diff --git a/pkg/services/accesscontrol/database/database_test.go b/pkg/services/accesscontrol/database/database_test.go index a929cd6cb3..4e4f19b236 100644 --- a/pkg/services/accesscontrol/database/database_test.go +++ b/pkg/services/accesscontrol/database/database_test.go @@ -243,6 +243,77 @@ func TestAccessControlStore_DeleteUserPermissions(t *testing.T) { }) } +func TestAccessControlStore_DeleteTeamPermissions(t *testing.T) { + t.Run("expect permissions related to team to be deleted", func(t *testing.T) { + store, permissionsStore, sql, teamSvc, _ := setupTestEnv(t) + user, team := createUserAndTeam(t, sql, teamSvc, 1) + + // grant permission to the team + _, err := permissionsStore.SetTeamResourcePermission(context.Background(), 1, team.ID, rs.SetResourcePermissionCommand{ + Actions: []string{"dashboards:write"}, + Resource: "dashboards", + ResourceAttribute: "uid", + ResourceID: "xxYYzz", + }, nil) + require.NoError(t, err) + + // generate permissions scoped to the team + _, err = permissionsStore.SetUserResourcePermission(context.Background(), 1, accesscontrol.User{ID: user.ID}, rs.SetResourcePermissionCommand{ + Actions: []string{"team:read"}, + Resource: "teams", + ResourceAttribute: "id", + ResourceID: fmt.Sprintf("%d", team.ID), + }, nil) + require.NoError(t, err) + + err = store.DeleteTeamPermissions(context.Background(), 1, team.ID) + require.NoError(t, err) + + permissions, err := store.GetUserPermissions(context.Background(), accesscontrol.GetUserPermissionsQuery{ + OrgID: 1, + UserID: user.ID, + Roles: []string{"Admin"}, + TeamIDs: []int64{team.ID}, + }) + require.NoError(t, err) + assert.Len(t, permissions, 0) + }) + t.Run("expect permissions not related to team to be kept", func(t *testing.T) { + store, permissionsStore, sql, teamSvc, _ := setupTestEnv(t) + user, team := createUserAndTeam(t, sql, teamSvc, 1) + + // grant permission to the team + _, err := permissionsStore.SetTeamResourcePermission(context.Background(), 1, team.ID, rs.SetResourcePermissionCommand{ + Actions: []string{"dashboards:write"}, + Resource: "dashboards", + ResourceAttribute: "uid", + ResourceID: "xxYYzz", + }, nil) + require.NoError(t, err) + + // generate permissions scoped to another team + _, err = permissionsStore.SetUserResourcePermission(context.Background(), 1, accesscontrol.User{ID: user.ID}, rs.SetResourcePermissionCommand{ + Actions: []string{"team:read"}, + Resource: "teams", + ResourceAttribute: "id", + ResourceID: fmt.Sprintf("%d", team.ID+1), + }, nil) + require.NoError(t, err) + + err = store.DeleteTeamPermissions(context.Background(), 1, team.ID) + require.NoError(t, err) + + permissions, err := store.GetUserPermissions(context.Background(), accesscontrol.GetUserPermissionsQuery{ + OrgID: 1, + UserID: user.ID, + Roles: []string{"Admin"}, + TeamIDs: []int64{team.ID}, + }) + require.NoError(t, err) + assert.Len(t, permissions, 1) + }) +} + func createUserAndTeam(t *testing.T, userSrv user.Service, teamSvc team.Service, orgID int64) (*user.User, team.Team) { t.Helper() diff --git a/pkg/services/accesscontrol/mock/mock.go b/pkg/services/accesscontrol/mock/mock.go index a0bcd2f46e..ef66d74c30 100644 --- a/pkg/services/accesscontrol/mock/mock.go +++ b/pkg/services/accesscontrol/mock/mock.go @@ -28,6 +28,7 @@ type Calls struct { RegisterFixedRoles []interface{} RegisterAttributeScopeResolver []interface{} DeleteUserPermissions []interface{} + DeleteTeamPermissions []interface{} SearchUsersPermissions []interface{} SearchUserPermissions []interface{} SaveExternalServiceRole []interface{} @@ -53,6 +54,7 @@ type Mock struct { RegisterFixedRolesFunc func() error RegisterScopeAttributeResolverFunc func(string, accesscontrol.ScopeAttributeResolver) DeleteUserPermissionsFunc func(context.Context, int64) error + DeleteTeamPermissionsFunc func(context.Context, int64) error SearchUsersPermissionsFunc func(context.Context, identity.Requester, int64, accesscontrol.SearchOptions) (map[int64][]accesscontrol.Permission, error) SearchUserPermissionsFunc func(ctx context.Context, orgID int64, searchOptions accesscontrol.SearchOptions) ([]accesscontrol.Permission, error) SaveExternalServiceRoleFunc func(ctx context.Context, cmd accesscontrol.SaveExternalServiceRoleCommand) error @@ -199,6 +201,15 @@ func (m *Mock) DeleteUserPermissions(ctx context.Context, orgID, userID int64) e return nil } +func (m *Mock) DeleteTeamPermissions(ctx context.Context, orgID, teamID int64) error { + m.Calls.DeleteTeamPermissions = append(m.Calls.DeleteTeamPermissions, []interface{}{ctx, orgID, teamID}) + // Use override if provided + if m.DeleteTeamPermissionsFunc != nil { + return m.DeleteTeamPermissionsFunc(ctx, teamID) + } + return nil +} + // SearchUsersPermissions returns all users' permissions filtered by an action prefix func (m *Mock) SearchUsersPermissions(ctx context.Context, usr identity.Requester, options accesscontrol.SearchOptions) (map[int64][]accesscontrol.Permission, error) { user := usr.(*user.SignedInUser) diff --git a/pkg/services/team/teamapi/team.go b/pkg/services/team/teamapi/team.go index 7a807a803b..70fcc9ae8e 100644 --- a/pkg/services/team/teamapi/team.go +++ b/pkg/services/team/teamapi/team.go @@ -127,6 +127,12 @@ func (tapi *TeamAPI) deleteTeamByID(c *contextmodel.ReqContext) response.Respons } return response.Error(http.StatusInternalServerError, "Failed to delete Team", err) } + + // Clear associated team assignments, managed role and permissions + if err := tapi.ac.DeleteTeamPermissions(c.Req.Context(), orgID, teamID); err != nil { + return response.Error(http.StatusInternalServerError, "Failed to delete Team permissions", err) + } + return response.Success("Team deleted") } diff --git a/pkg/services/team/teamimpl/store.go b/pkg/services/team/teamimpl/store.go index 915dcf41d5..f8a85618d9 100644 --- a/pkg/services/team/teamimpl/store.go +++ b/pkg/services/team/teamimpl/store.go @@ -143,7 +143,6 @@ func (ss *xormStore) Delete(ctx context.Context, cmd *team.DeleteTeamCommand) er "DELETE FROM team_member WHERE org_id=? and team_id = ?", "DELETE FROM team WHERE org_id=? and id = ?", "DELETE FROM dashboard_acl WHERE org_id=? and team_id = ?", - "DELETE FROM team_role WHERE org_id=? and team_id = ?", } deletes = append(deletes, ss.deletes...) @@ -154,10 +153,7 @@ func (ss *xormStore) Delete(ctx context.Context, cmd *team.DeleteTeamCommand) er return err } } - - _, err := sess.Exec("DELETE FROM permission WHERE scope=?", ac.Scope("teams", "id", fmt.Sprint(cmd.ID))) - - return err + return nil }) } From d04c579fc23c1c06b5c117c452b7cd7bd9cc8023 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 27 Feb 2024 11:19:57 +0000 Subject: [PATCH 0222/1406] Update dependency webpack to v5.90.3 --- package.json | 2 +- packages/grafana-plugin-configs/package.json | 2 +- packages/grafana-prometheus/package.json | 2 +- packages/grafana-ui/package.json | 2 +- .../datasource/azuremonitor/package.json | 2 +- .../datasource/cloud-monitoring/package.json | 2 +- .../grafana-pyroscope-datasource/package.json | 2 +- .../grafana-testdata-datasource/package.json | 2 +- .../app/plugins/datasource/parca/package.json | 2 +- .../app/plugins/datasource/tempo/package.json | 2 +- .../plugins/datasource/zipkin/package.json | 2 +- yarn.lock | 30 +++++++++---------- 12 files changed, 26 insertions(+), 26 deletions(-) diff --git a/package.json b/package.json index 4d922f7fd7..683a2d9f15 100644 --- a/package.json +++ b/package.json @@ -218,7 +218,7 @@ "ts-jest": "29.1.2", "ts-node": "10.9.2", "typescript": "5.3.3", - "webpack": "5.90.2", + "webpack": "5.90.3", "webpack-bundle-analyzer": "4.10.1", "webpack-cli": "5.1.4", "webpack-dev-server": "5.0.2", diff --git a/packages/grafana-plugin-configs/package.json b/packages/grafana-plugin-configs/package.json index 25745892f5..a4caf4e875 100644 --- a/packages/grafana-plugin-configs/package.json +++ b/packages/grafana-plugin-configs/package.json @@ -14,7 +14,7 @@ "glob": "10.3.10", "replace-in-file-webpack-plugin": "1.0.6", "swc-loader": "0.2.6", - "webpack": "5.90.2" + "webpack": "5.90.3" }, "packageManager": "yarn@4.1.0" } diff --git a/packages/grafana-prometheus/package.json b/packages/grafana-prometheus/package.json index ec7eba97a8..380f12ab45 100644 --- a/packages/grafana-prometheus/package.json +++ b/packages/grafana-prometheus/package.json @@ -140,7 +140,7 @@ "testing-library-selector": "0.3.1", "ts-node": "10.9.2", "typescript": "5.3.3", - "webpack": "5.90.2", + "webpack": "5.90.3", "webpack-cli": "5.1.4" }, "peerDependencies": { diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 9bd5200f2f..60116069b1 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -183,7 +183,7 @@ "storybook-dark-mode": "3.0.1", "style-loader": "3.3.4", "typescript": "5.3.3", - "webpack": "5.90.2" + "webpack": "5.90.3" }, "peerDependencies": { "react": "^17.0.0 || ^18.0.0", diff --git a/public/app/plugins/datasource/azuremonitor/package.json b/public/app/plugins/datasource/azuremonitor/package.json index f400937ef6..fce5e1152b 100644 --- a/public/app/plugins/datasource/azuremonitor/package.json +++ b/public/app/plugins/datasource/azuremonitor/package.json @@ -36,7 +36,7 @@ "react-select-event": "5.5.1", "ts-node": "10.9.2", "typescript": "5.3.3", - "webpack": "5.90.2" + "webpack": "5.90.3" }, "peerDependencies": { "@grafana/runtime": "*" diff --git a/public/app/plugins/datasource/cloud-monitoring/package.json b/public/app/plugins/datasource/cloud-monitoring/package.json index 38376e2cc2..48135a9846 100644 --- a/public/app/plugins/datasource/cloud-monitoring/package.json +++ b/public/app/plugins/datasource/cloud-monitoring/package.json @@ -41,7 +41,7 @@ "react-test-renderer": "18.2.0", "ts-node": "10.9.2", "typescript": "5.3.3", - "webpack": "5.90.2" + "webpack": "5.90.3" }, "peerDependencies": { "@grafana/runtime": "*" diff --git a/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json b/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json index ee899f9a01..2a682b4ffc 100644 --- a/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json +++ b/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json @@ -35,7 +35,7 @@ "style-loader": "3.3.4", "ts-node": "10.9.2", "typescript": "5.3.3", - "webpack": "5.90.2" + "webpack": "5.90.3" }, "peerDependencies": { "@grafana/runtime": "*" diff --git a/public/app/plugins/datasource/grafana-testdata-datasource/package.json b/public/app/plugins/datasource/grafana-testdata-datasource/package.json index 392461e3ed..4688d79003 100644 --- a/public/app/plugins/datasource/grafana-testdata-datasource/package.json +++ b/public/app/plugins/datasource/grafana-testdata-datasource/package.json @@ -32,7 +32,7 @@ "@types/testing-library__jest-dom": "5.14.9", "@types/uuid": "9.0.8", "ts-node": "10.9.2", - "webpack": "5.90.2" + "webpack": "5.90.3" }, "peerDependencies": { "@grafana/runtime": "*" diff --git a/public/app/plugins/datasource/parca/package.json b/public/app/plugins/datasource/parca/package.json index 1e5e9d03c7..5065411a48 100644 --- a/public/app/plugins/datasource/parca/package.json +++ b/public/app/plugins/datasource/parca/package.json @@ -23,7 +23,7 @@ "@types/lodash": "4.14.202", "@types/react": "18.2.60", "ts-node": "10.9.2", - "webpack": "5.90.2" + "webpack": "5.90.3" }, "peerDependencies": { "@grafana/runtime": "*" diff --git a/public/app/plugins/datasource/tempo/package.json b/public/app/plugins/datasource/tempo/package.json index 67e0f117f7..87c078516c 100644 --- a/public/app/plugins/datasource/tempo/package.json +++ b/public/app/plugins/datasource/tempo/package.json @@ -54,7 +54,7 @@ "react-select-event": "5.5.1", "ts-node": "10.9.2", "typescript": "5.3.3", - "webpack": "5.90.2" + "webpack": "5.90.3" }, "peerDependencies": { "@grafana/runtime": "*" diff --git a/public/app/plugins/datasource/zipkin/package.json b/public/app/plugins/datasource/zipkin/package.json index 3a1f70b19a..fae2903608 100644 --- a/public/app/plugins/datasource/zipkin/package.json +++ b/public/app/plugins/datasource/zipkin/package.json @@ -24,7 +24,7 @@ "@types/lodash": "4.14.202", "@types/react": "18.2.60", "ts-node": "10.9.2", - "webpack": "5.90.2" + "webpack": "5.90.3" }, "peerDependencies": { "@grafana/runtime": "*" diff --git a/yarn.lock b/yarn.lock index e668ab9304..3928b3b251 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3248,7 +3248,7 @@ __metadata: ts-node: "npm:10.9.2" tslib: "npm:2.6.2" typescript: "npm:5.3.3" - webpack: "npm:5.90.2" + webpack: "npm:5.90.3" peerDependencies: "@grafana/runtime": "*" languageName: unknown @@ -3287,7 +3287,7 @@ __metadata: ts-node: "npm:10.9.2" tslib: "npm:2.6.2" typescript: "npm:5.3.3" - webpack: "npm:5.90.2" + webpack: "npm:5.90.3" peerDependencies: "@grafana/runtime": "*" languageName: unknown @@ -3323,7 +3323,7 @@ __metadata: ts-node: "npm:10.9.2" tslib: "npm:2.6.2" uuid: "npm:9.0.1" - webpack: "npm:5.90.2" + webpack: "npm:5.90.3" peerDependencies: "@grafana/runtime": "*" languageName: unknown @@ -3373,7 +3373,7 @@ __metadata: rxjs: "npm:7.8.1" ts-node: "npm:10.9.2" tslib: "npm:2.6.2" - webpack: "npm:5.90.2" + webpack: "npm:5.90.3" peerDependencies: "@grafana/runtime": "*" languageName: unknown @@ -3418,7 +3418,7 @@ __metadata: ts-node: "npm:10.9.2" tslib: "npm:2.6.2" typescript: "npm:5.3.3" - webpack: "npm:5.90.2" + webpack: "npm:5.90.3" peerDependencies: "@grafana/runtime": "*" languageName: unknown @@ -3476,7 +3476,7 @@ __metadata: tslib: "npm:2.6.2" typescript: "npm:5.3.3" uuid: "npm:9.0.1" - webpack: "npm:5.90.2" + webpack: "npm:5.90.3" peerDependencies: "@grafana/runtime": "*" languageName: unknown @@ -3504,7 +3504,7 @@ __metadata: rxjs: "npm:7.8.1" ts-node: "npm:10.9.2" tslib: "npm:2.6.2" - webpack: "npm:5.90.2" + webpack: "npm:5.90.3" peerDependencies: "@grafana/runtime": "*" languageName: unknown @@ -3897,7 +3897,7 @@ __metadata: replace-in-file-webpack-plugin: "npm:1.0.6" swc-loader: "npm:0.2.6" tslib: "npm:2.6.2" - webpack: "npm:5.90.2" + webpack: "npm:5.90.3" languageName: unknown linkType: soft @@ -4019,7 +4019,7 @@ __metadata: tslib: "npm:2.6.2" typescript: "npm:5.3.3" uuid: "npm:9.0.1" - webpack: "npm:5.90.2" + webpack: "npm:5.90.3" webpack-cli: "npm:5.1.4" whatwg-fetch: "npm:3.6.20" peerDependencies: @@ -4303,7 +4303,7 @@ __metadata: typescript: "npm:5.3.3" uplot: "npm:1.6.30" uuid: "npm:9.0.1" - webpack: "npm:5.90.2" + webpack: "npm:5.90.3" peerDependencies: react: ^17.0.0 || ^18.0.0 react-dom: ^17.0.0 || ^18.0.0 @@ -18637,7 +18637,7 @@ __metadata: uplot: "npm:1.6.30" uuid: "npm:9.0.1" visjs-network: "npm:4.25.0" - webpack: "npm:5.90.2" + webpack: "npm:5.90.3" webpack-assets-manifest: "npm:^5.1.0" webpack-bundle-analyzer: "npm:4.10.1" webpack-cli: "npm:5.1.4" @@ -31378,9 +31378,9 @@ __metadata: languageName: node linkType: hard -"webpack@npm:5, webpack@npm:5.90.2, webpack@npm:^5": - version: 5.90.2 - resolution: "webpack@npm:5.90.2" +"webpack@npm:5, webpack@npm:5.90.3, webpack@npm:^5": + version: 5.90.3 + resolution: "webpack@npm:5.90.3" dependencies: "@types/eslint-scope": "npm:^3.7.3" "@types/estree": "npm:^1.0.5" @@ -31411,7 +31411,7 @@ __metadata: optional: true bin: webpack: bin/webpack.js - checksum: 10/4eaeed1255c9c7738921c4ce4facdb3b78dbfcb3441496942f6d160a41fbcebd24fb2c6dbb64739b357c5ff78e5a298f6c82eca482438b95130a3ba4e16d084a + checksum: 10/48c9696eca950bfa7c943a24b8235fdf0575acd73a8eb1661f8189d3d1f431362f3a0e158e2941a7e4f0852ea6e32d7d4e89283149247e4389a8aad0fe6c247e languageName: node linkType: hard From 5edd96ae77d01d194a97ec8e4a42190fa7a829f3 Mon Sep 17 00:00:00 2001 From: Will Browne Date: Tue, 27 Feb 2024 12:38:02 +0100 Subject: [PATCH 0223/1406] Plugins: Refactor plugin config into separate env var and request scoped services (#83261) * seperate services for env + req * merge with main * fix tests * undo changes to golden file * fix linter * remove unused fields * split out new config struct * provide config * undo go mod changes * more renaming * fix tests * undo bra.toml changes * update go.work.sum * undo changes * trigger * apply PR feedback --- go.work.sum | 585 ++++++++++++++- pkg/api/frontendsettings_test.go | 2 +- pkg/api/metrics_test.go | 10 +- pkg/api/plugin_resource_test.go | 5 +- pkg/api/plugins_test.go | 2 +- pkg/expr/dataplane_test.go | 5 +- pkg/expr/service_test.go | 7 +- pkg/plugins/config/config.go | 98 +-- pkg/plugins/envvars/envvars.go | 333 +-------- pkg/plugins/manager/client/client.go | 4 +- pkg/plugins/manager/client/client_test.go | 16 +- pkg/plugins/manager/fakes/fakes.go | 15 + pkg/plugins/manager/installer.go | 2 +- .../manager/loader/assetpath/assetpath.go | 6 +- .../loader/assetpath/assetpath_test.go | 4 +- pkg/plugins/manager/loader/finder/local.go | 2 +- pkg/plugins/manager/loader/loader_test.go | 26 +- .../manager/pipeline/bootstrap/bootstrap.go | 2 +- .../manager/pipeline/bootstrap/steps.go | 4 +- .../manager/pipeline/bootstrap/steps_test.go | 6 +- .../manager/pipeline/discovery/discovery.go | 2 +- .../manager/pipeline/discovery/steps.go | 2 +- .../pipeline/initialization/initialization.go | 4 +- .../manager/pipeline/initialization/steps.go | 2 +- .../pipeline/initialization/steps_test.go | 8 +- .../pipeline/termination/termination.go | 4 +- .../manager/pipeline/validation/steps.go | 8 +- .../manager/pipeline/validation/validation.go | 4 +- pkg/plugins/manager/signature/authorizer.go | 6 +- pkg/plugins/manager/signature/manifest.go | 8 +- .../manager/signature/manifest_test.go | 16 +- pkg/plugins/pluginscdn/pluginscdn.go | 4 +- pkg/plugins/pluginscdn/pluginscdn_test.go | 4 +- pkg/plugins/repo/service.go | 2 +- .../angulardetectorsprovider/dynamic.go | 2 +- .../angulardetectorsprovider/dynamic_test.go | 2 +- .../angularinspector/angularinspector.go | 2 +- .../angularinspector/angularinspector_test.go | 4 +- .../pluginsintegration/config/config.go | 90 --- .../pluginsintegration/loader/loader_test.go | 81 +- .../pluginsintegration/pipeline/pipeline.go | 16 +- .../pluginsintegration/pipeline/steps.go | 14 +- .../pluginsintegration/pipeline/steps_test.go | 4 +- .../pluginsintegration/pluginconfig/config.go | 144 ++++ .../{config => pluginconfig}/config_test.go | 4 +- .../pluginconfig/envvars.go | 178 +++++ .../pluginconfig}/envvars_test.go | 696 +++++------------- .../pluginsintegration/pluginconfig/fakes.go | 16 + .../pluginconfig/request.go | 151 ++++ .../pluginconfig/request_test.go | 398 ++++++++++ .../{config => pluginconfig}/tracing.go | 2 +- .../{config => pluginconfig}/tracing_test.go | 2 +- .../plugincontext/plugincontext.go | 43 +- .../plugincontext/plugincontext_test.go | 4 +- .../pluginsintegration/pluginsintegration.go | 14 +- .../pluginsintegration/renderer/renderer.go | 29 +- .../renderer/renderer_test.go | 6 +- .../serviceregistration.go | 2 +- .../pluginsintegration/test_helper.go | 10 +- .../publicdashboards/api/common_test.go | 6 +- pkg/services/query/query_test.go | 5 +- pkg/tsdb/legacydata/service/service_test.go | 5 +- 62 files changed, 1932 insertions(+), 1206 deletions(-) delete mode 100644 pkg/services/pluginsintegration/config/config.go create mode 100644 pkg/services/pluginsintegration/pluginconfig/config.go rename pkg/services/pluginsintegration/{config => pluginconfig}/config_test.go (97%) create mode 100644 pkg/services/pluginsintegration/pluginconfig/envvars.go rename pkg/{plugins/envvars => services/pluginsintegration/pluginconfig}/envvars_test.go (52%) create mode 100644 pkg/services/pluginsintegration/pluginconfig/fakes.go create mode 100644 pkg/services/pluginsintegration/pluginconfig/request.go create mode 100644 pkg/services/pluginsintegration/pluginconfig/request_test.go rename pkg/services/pluginsintegration/{config => pluginconfig}/tracing.go (97%) rename pkg/services/pluginsintegration/{config => pluginconfig}/tracing_test.go (98%) diff --git a/go.work.sum b/go.work.sum index 5fb59d51c9..57235ef9f2 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1,189 +1,772 @@ +buf.build/gen/go/grpc-ecosystem/grpc-gateway/bufbuild/connect-go v1.4.1-20221127060915-a1ecdc58eccd.1 h1:vp9EaPFSb75qe/793x58yE5fY1IJ/gdxb/kcDUzavtI= buf.build/gen/go/grpc-ecosystem/grpc-gateway/bufbuild/connect-go v1.4.1-20221127060915-a1ecdc58eccd.1/go.mod h1:YDq2B5X5BChU0lxAG5MxHpDb8mx1fv9OGtF2mwOe7hY= +buf.build/gen/go/grpc-ecosystem/grpc-gateway/protocolbuffers/go v1.28.1-20221127060915-a1ecdc58eccd.4 h1:z3Xc9n8yZ5k/Xr4ZTuff76TAYP20dWy7ZBV4cGIpbkM= +cloud.google.com/go/accessapproval v1.7.4 h1:ZvLvJ952zK8pFHINjpMBY5k7LTAp/6pBf50RDMRgBUI= +cloud.google.com/go/accesscontextmanager v1.8.4 h1:Yo4g2XrBETBCqyWIibN3NHNPQKUfQqti0lI+70rubeE= +cloud.google.com/go/aiplatform v1.57.0 h1:WcZ6wDf/1qBWatmGM9Z+2BTiNjQQX54k2BekHUj93DQ= cloud.google.com/go/aiplatform v1.57.0/go.mod h1:pwZMGvqe0JRkI1GWSZCtnAfrR4K1bv65IHILGA//VEU= +cloud.google.com/go/analytics v0.21.6 h1:fnV7B8lqyEYxCU0LKk+vUL7mTlqRAq4uFlIthIdr/iA= +cloud.google.com/go/apigateway v1.6.4 h1:VVIxCtVerchHienSlaGzV6XJGtEM9828Erzyr3miUGs= +cloud.google.com/go/apigeeconnect v1.6.4 h1:jSoGITWKgAj/ssVogNE9SdsTqcXnryPzsulENSRlusI= +cloud.google.com/go/apigeeregistry v0.8.2 h1:DSaD1iiqvELag+lV4VnnqUUFd8GXELu01tKVdWZrviE= +cloud.google.com/go/apikeys v0.6.0 h1:B9CdHFZTFjVti89tmyXXrO+7vSNo2jvZuHG8zD5trdQ= +cloud.google.com/go/appengine v1.8.4 h1:Qub3fqR7iA1daJWdzjp/Q0Jz0fUG0JbMc7Ui4E9IX/E= +cloud.google.com/go/area120 v0.8.4 h1:YnSO8m02pOIo6AEOgiOoUDVbw4pf+bg2KLHi4rky320= +cloud.google.com/go/artifactregistry v1.14.6 h1:/hQaadYytMdA5zBh+RciIrXZQBWK4vN7EUsrQHG+/t8= +cloud.google.com/go/asset v1.15.3 h1:uI8Bdm81s0esVWbWrTHcjFDFKNOa9aB7rI1vud1hO84= +cloud.google.com/go/assuredworkloads v1.11.4 h1:FsLSkmYYeNuzDm8L4YPfLWV+lQaUrJmH5OuD37t1k20= +cloud.google.com/go/automl v1.13.4 h1:i9tOKXX+1gE7+rHpWKjiuPfGBVIYoWvLNIGpWgPtF58= +cloud.google.com/go/baremetalsolution v1.2.3 h1:oQiFYYCe0vwp7J8ZmF6siVKEumWtiPFJMJcGuyDVRUk= +cloud.google.com/go/batch v1.7.0 h1:AxuSPoL2fWn/rUyvWeNCNd0V2WCr+iHRCU9QO1PUmpY= cloud.google.com/go/batch v1.7.0/go.mod h1:J64gD4vsNSA2O5TtDB5AAux3nJ9iV8U3ilg3JDBYejU= +cloud.google.com/go/beyondcorp v1.0.3 h1:VXf9SnrnSmj2BF2cHkoTHvOUp8gjsz1KJFOMW7czdsY= +cloud.google.com/go/bigquery v1.57.1 h1:FiULdbbzUxWD0Y4ZGPSVCDLvqRSyCIO6zKV7E2nf5uA= +cloud.google.com/go/billing v1.18.0 h1:GvKy4xLy1zF1XPbwP5NJb2HjRxhnhxjjXxvyZ1S/IAo= cloud.google.com/go/billing v1.18.0/go.mod h1:5DOYQStCxquGprqfuid/7haD7th74kyMBHkjO/OvDtk= +cloud.google.com/go/binaryauthorization v1.8.0 h1:PHS89lcFayWIEe0/s2jTBiEOtqghCxzc7y7bRNlifBs= cloud.google.com/go/binaryauthorization v1.8.0/go.mod h1:VQ/nUGRKhrStlGr+8GMS8f6/vznYLkdK5vaKfdCIpvU= +cloud.google.com/go/certificatemanager v1.7.4 h1:5YMQ3Q+dqGpwUZ9X5sipsOQ1fLPsxod9HNq0+nrqc6I= +cloud.google.com/go/channel v1.17.3 h1:Rd4+fBrjiN6tZ4TR8R/38elkyEkz6oogGDr7jDyjmMY= +cloud.google.com/go/cloudbuild v1.15.0 h1:9IHfEMWdCklJ1cwouoiQrnxmP0q3pH7JUt8Hqx4Qbck= +cloud.google.com/go/clouddms v1.7.3 h1:xe/wJKz55VO1+L891a1EG9lVUgfHr9Ju/I3xh1nwF84= +cloud.google.com/go/cloudtasks v1.12.4 h1:5xXuFfAjg0Z5Wb81j2GAbB3e0bwroCeSF+5jBn/L650= +cloud.google.com/go/contactcenterinsights v1.12.1 h1:EiGBeejtDDtr3JXt9W7xlhXyZ+REB5k2tBgVPVtmNb0= cloud.google.com/go/contactcenterinsights v1.12.1/go.mod h1:HHX5wrz5LHVAwfI2smIotQG9x8Qd6gYilaHcLLLmNis= +cloud.google.com/go/container v1.29.0 h1:jIltU529R2zBFvP8rhiG1mgeTcnT27KhU0H/1d6SQRg= cloud.google.com/go/container v1.29.0/go.mod h1:b1A1gJeTBXVLQ6GGw9/9M4FG94BEGsqJ5+t4d/3N7O4= +cloud.google.com/go/containeranalysis v0.11.3 h1:5rhYLX+3a01drpREqBZVXR9YmWH45RnML++8NsCtuD8= +cloud.google.com/go/datacatalog v1.19.0 h1:rbYNmHwvAOOwnW2FPXYkaK3Mf1MmGqRzK0mMiIEyLdo= +cloud.google.com/go/dataflow v0.9.4 h1:7VmCNWcPJBS/srN2QnStTB6nu4Eb5TMcpkmtaPVhRt4= +cloud.google.com/go/dataform v0.9.1 h1:jV+EsDamGX6cE127+QAcCR/lergVeeZdEQ6DdrxW3sQ= +cloud.google.com/go/datafusion v1.7.4 h1:Q90alBEYlMi66zL5gMSGQHfbZLB55mOAg03DhwTTfsk= +cloud.google.com/go/datalabeling v0.8.4 h1:zrq4uMmunf2KFDl/7dS6iCDBBAxBnKVDyw6+ajz3yu0= +cloud.google.com/go/dataplex v1.13.0 h1:ACVOuxwe7gP0SqEso9SLyXbcZNk5l8hjcTX+XLntI5s= cloud.google.com/go/dataplex v1.13.0/go.mod h1:mHJYQQ2VEJHsyoC0OdNyy988DvEbPhqFs5OOLffLX0c= +cloud.google.com/go/dataproc v1.12.0 h1:W47qHL3W4BPkAIbk4SWmIERwsWBaNnWm0P2sdx3YgGU= +cloud.google.com/go/dataproc/v2 v2.3.0 h1:tTVP9tTxmc8fixxOd/8s6Q6Pz/+yzn7r7XdZHretQH0= +cloud.google.com/go/dataqna v0.8.4 h1:NJnu1kAPamZDs/if3bJ3+Wb6tjADHKL83NUWsaIp2zg= +cloud.google.com/go/datastore v1.15.0 h1:0P9WcsQeTWjuD1H14JIY7XQscIPQ4Laje8ti96IC5vg= +cloud.google.com/go/datastream v1.10.3 h1:Z2sKPIB7bT2kMW5Uhxy44ZgdJzxzE5uKjavoW+EuHEE= +cloud.google.com/go/deploy v1.16.0 h1:5OVjzm8MPC5kP+Ywbs0mdE0O7AXvAUXksSyHAyMFyMg= cloud.google.com/go/deploy v1.16.0/go.mod h1:e5XOUI5D+YGldyLNZ21wbp9S8otJbBE4i88PtO9x/2g= +cloud.google.com/go/dialogflow v1.47.0 h1:tLCWad8HZhlyUNfDzDP5m+oH6h/1Uvw/ei7B9AnsWMk= cloud.google.com/go/dialogflow v1.47.0/go.mod h1:mHly4vU7cPXVweuB5R0zsYKPMzy240aQdAu06SqBbAQ= +cloud.google.com/go/dlp v1.11.1 h1:OFlXedmPP/5//X1hBEeq3D9kUVm9fb6ywYANlpv/EsQ= +cloud.google.com/go/documentai v1.23.6 h1:0/S3AhS23+0qaFe3tkgMmS3STxgDgmE1jg4TvaDOZ9g= cloud.google.com/go/documentai v1.23.6/go.mod h1:ghzBsyVTiVdkfKaUCum/9bGBEyBjDO4GfooEcYKhN+g= +cloud.google.com/go/domains v0.9.4 h1:ua4GvsDztZ5F3xqjeLKVRDeOvJshf5QFgWGg1CKti3A= +cloud.google.com/go/edgecontainer v1.1.4 h1:Szy3Q/N6bqgQGyxqjI+6xJZbmvPvnFHp3UZr95DKcQ0= +cloud.google.com/go/errorreporting v0.3.0 h1:kj1XEWMu8P0qlLhm3FwcaFsUvXChV/OraZwA70trRR0= +cloud.google.com/go/essentialcontacts v1.6.5 h1:S2if6wkjR4JCEAfDtIiYtD+sTz/oXjh2NUG4cgT1y/Q= +cloud.google.com/go/eventarc v1.13.3 h1:+pFmO4eu4dOVipSaFBLkmqrRYG94Xl/TQZFOeohkuqU= +cloud.google.com/go/filestore v1.8.0 h1:/+wUEGwk3x3Kxomi2cP5dsR8+SIXxo7M0THDjreFSYo= +cloud.google.com/go/firestore v1.14.0 h1:8aLcKnMPoldYU3YHgu4t2exrKhLQkqaXAGqT0ljrFVw= +cloud.google.com/go/functions v1.15.4 h1:ZjdiV3MyumRM6++1Ixu6N0VV9LAGlCX4AhW6Yjr1t+U= +cloud.google.com/go/gaming v1.10.1 h1:5qZmZEWzMf8GEFgm9NeC3bjFRpt7x4S6U7oLbxaf7N8= +cloud.google.com/go/gkebackup v1.3.4 h1:KhnOrr9A1tXYIYeXKqCKbCI8TL2ZNGiD3dm+d7BDUBg= +cloud.google.com/go/gkeconnect v0.8.4 h1:1JLpZl31YhQDQeJ98tK6QiwTpgHFYRJwpntggpQQWis= +cloud.google.com/go/gkehub v0.14.4 h1:J5tYUtb3r0cl2mM7+YHvV32eL+uZQ7lONyUZnPikCEo= +cloud.google.com/go/gkemulticloud v1.0.3 h1:NmJsNX9uQ2CT78957xnjXZb26TDIMvv+d5W2vVUt0Pg= +cloud.google.com/go/grafeas v0.3.0 h1:oyTL/KjiUeBs9eYLw/40cpSZglUC+0F7X4iu/8t7NWs= +cloud.google.com/go/gsuiteaddons v1.6.4 h1:uuw2Xd37yHftViSI8J2hUcCS8S7SH3ZWH09sUDLW30Q= +cloud.google.com/go/iap v1.9.3 h1:M4vDbQ4TLXdaljXVZSwW7XtxpwXUUarY2lIs66m0aCM= +cloud.google.com/go/ids v1.4.4 h1:VuFqv2ctf/A7AyKlNxVvlHTzjrEvumWaZflUzBPz/M4= +cloud.google.com/go/iot v1.7.4 h1:m1WljtkZnvLTIRYW1YTOv5A6H1yKgLHR6nU7O8yf27w= +cloud.google.com/go/language v1.12.2 h1:zg9uq2yS9PGIOdc0Kz/l+zMtOlxKWonZjjo5w5YPG2A= +cloud.google.com/go/lifesciences v0.9.4 h1:rZEI/UxcxVKEzyoRS/kdJ1VoolNItRWjNN0Uk9tfexg= +cloud.google.com/go/logging v1.8.1 h1:26skQWPeYhvIasWKm48+Eq7oUqdcdbwsCVwz5Ys0FvU= +cloud.google.com/go/longrunning v0.5.4 h1:w8xEcbZodnA2BbW6sVirkkoC+1gP8wS57EUUgGS0GVg= +cloud.google.com/go/managedidentities v1.6.4 h1:SF/u1IJduMqQQdJA4MDyivlIQ4SrV5qAawkr/ZEREkY= +cloud.google.com/go/maps v1.6.2 h1:WxxLo//b60nNFESefLgaBQevu8QGUmRV3+noOjCfIHs= cloud.google.com/go/maps v1.6.2/go.mod h1:4+buOHhYXFBp58Zj/K+Lc1rCmJssxxF4pJ5CJnhdz18= +cloud.google.com/go/mediatranslation v0.8.4 h1:VRCQfZB4s6jN0CSy7+cO3m4ewNwgVnaePanVCQh/9Z4= +cloud.google.com/go/memcache v1.10.4 h1:cdex/ayDd294XBj2cGeMe6Y+H1JvhN8y78B9UW7pxuQ= +cloud.google.com/go/metastore v1.13.3 h1:94l/Yxg9oBZjin2bzI79oK05feYefieDq0o5fjLSkC8= +cloud.google.com/go/monitoring v1.16.3 h1:mf2SN9qSoBtIgiMA4R/y4VADPWZA7VCNJA079qLaZQ8= +cloud.google.com/go/networkconnectivity v1.14.3 h1:e9lUkCe2BexsqsUc2bjV8+gFBpQa54J+/F3qKVtW+wA= +cloud.google.com/go/networkmanagement v1.9.3 h1:HsQk4FNKJUX04k3OI6gUsoveiHMGvDRqlaFM2xGyvqU= +cloud.google.com/go/networksecurity v0.9.4 h1:947tNIPnj1bMGTIEBo3fc4QrrFKS5hh0bFVsHmFm4Vo= +cloud.google.com/go/notebooks v1.11.2 h1:eTOTfNL1yM6L/PCtquJwjWg7ZZGR0URFaFgbs8kllbM= +cloud.google.com/go/optimization v1.6.2 h1:iFsoexcp13cGT3k/Hv8PA5aK+FP7FnbhwDO9llnruas= +cloud.google.com/go/orchestration v1.8.4 h1:kgwZ2f6qMMYIVBtUGGoU8yjYWwMTHDanLwM/CQCFaoQ= +cloud.google.com/go/orgpolicy v1.11.4 h1:RWuXQDr9GDYhjmrredQJC7aY7cbyqP9ZuLbq5GJGves= +cloud.google.com/go/osconfig v1.12.4 h1:OrRCIYEAbrbXdhm13/JINn9pQchvTTIzgmOCA7uJw8I= +cloud.google.com/go/oslogin v1.12.2 h1:NP/KgsD9+0r9hmHC5wKye0vJXVwdciv219DtYKYjgqE= +cloud.google.com/go/phishingprotection v0.8.4 h1:sPLUQkHq6b4AL0czSJZ0jd6vL55GSTHz2B3Md+TCZI0= +cloud.google.com/go/policytroubleshooter v1.10.2 h1:sq+ScLP83d7GJy9+wpwYJVnY+q6xNTXwOdRIuYjvHT4= +cloud.google.com/go/privatecatalog v0.9.4 h1:Vo10IpWKbNvc/z/QZPVXgCiwfjpWoZ/wbgful4Uh/4E= +cloud.google.com/go/pubsub v1.33.0 h1:6SPCPvWav64tj0sVX/+npCBKhUi/UjJehy9op/V3p2g= +cloud.google.com/go/pubsublite v1.8.1 h1:pX+idpWMIH30/K7c0epN6V703xpIcMXWRjKJsz0tYGY= +cloud.google.com/go/recaptchaenterprise v1.3.1 h1:u6EznTGzIdsyOsvm+Xkw0aSuKFXQlyjGE9a4exk6iNQ= +cloud.google.com/go/recaptchaenterprise/v2 v2.9.0 h1:Zrd4LvT9PaW91X/Z13H0i5RKEv9suCLuk8zp+bfOpN4= cloud.google.com/go/recaptchaenterprise/v2 v2.9.0/go.mod h1:Dak54rw6lC2gBY8FBznpOCAR58wKf+R+ZSJRoeJok4w= +cloud.google.com/go/recommendationengine v0.8.4 h1:JRiwe4hvu3auuh2hujiTc2qNgPPfVp+Q8KOpsXlEzKQ= +cloud.google.com/go/recommender v1.11.3 h1:VndmgyS/J3+izR8V8BHa7HV/uun8//ivQ3k5eVKKyyM= +cloud.google.com/go/redis v1.14.1 h1:J9cEHxG9YLmA9o4jTSvWt/RuVEn6MTrPlYSCRHujxDQ= +cloud.google.com/go/resourcemanager v1.9.4 h1:JwZ7Ggle54XQ/FVYSBrMLOQIKoIT/uer8mmNvNLK51k= +cloud.google.com/go/resourcesettings v1.6.4 h1:yTIL2CsZswmMfFyx2Ic77oLVzfBFoWBYgpkgiSPnC4Y= +cloud.google.com/go/retail v1.14.4 h1:geqdX1FNqqL2p0ADXjPpw8lq986iv5GrVcieTYafuJQ= +cloud.google.com/go/run v1.3.3 h1:qdfZteAm+vgzN1iXzILo3nJFQbzziudkJrvd9wCf3FQ= +cloud.google.com/go/scheduler v1.10.5 h1:eMEettHlFhG5pXsoHouIM5nRT+k+zU4+GUvRtnxhuVI= +cloud.google.com/go/secretmanager v1.11.4 h1:krnX9qpG2kR2fJ+u+uNyNo+ACVhplIAS4Pu7u+4gd+k= +cloud.google.com/go/security v1.15.4 h1:sdnh4Islb1ljaNhpIXlIPgb3eYj70QWgPVDKOUYvzJc= +cloud.google.com/go/securitycenter v1.24.3 h1:crdn2Z2rFIy8WffmmhdlX3CwZJusqCiShtnrGFRwpeE= cloud.google.com/go/securitycenter v1.24.3/go.mod h1:l1XejOngggzqwr4Fa2Cn+iWZGf+aBLTXtB/vXjy5vXM= +cloud.google.com/go/servicecontrol v1.11.1 h1:d0uV7Qegtfaa7Z2ClDzr9HJmnbJW7jn0WhZ7wOX6hLE= +cloud.google.com/go/servicedirectory v1.11.3 h1:5niCMfkw+jifmFtbBrtRedbXkJm3fubSR/KHbxSJZVM= +cloud.google.com/go/servicemanagement v1.8.0 h1:fopAQI/IAzlxnVeiKn/8WiV6zKndjFkvi+gzu+NjywY= +cloud.google.com/go/serviceusage v1.6.0 h1:rXyq+0+RSIm3HFypctp7WoXxIA563rn206CfMWdqXX4= +cloud.google.com/go/shell v1.7.4 h1:nurhlJcSVFZneoRZgkBEHumTYf/kFJptCK2eBUq/88M= +cloud.google.com/go/spanner v1.53.1 h1:xNmE0SXMSxNBuk7lRZ5G/S+A49X91zkSTt7Jn5Ptlvw= cloud.google.com/go/spanner v1.53.1/go.mod h1:liG4iCeLqm5L3fFLU5whFITqP0e0orsAW1uUSrd4rws= +cloud.google.com/go/speech v1.21.0 h1:qkxNao58oF8ghAHE1Eghen7XepawYEN5zuZXYWaUTA4= +cloud.google.com/go/storagetransfer v1.10.3 h1:YM1dnj5gLjfL6aDldO2s4GeU8JoAvH1xyIwXre63KmI= +cloud.google.com/go/talent v1.6.5 h1:LnRJhhYkODDBoTwf6BeYkiJHFw9k+1mAFNyArwZUZAs= +cloud.google.com/go/texttospeech v1.7.4 h1:ahrzTgr7uAbvebuhkBAAVU6kRwVD0HWsmDsvMhtad5Q= +cloud.google.com/go/tpu v1.6.4 h1:XIEH5c0WeYGaVy9H+UueiTaf3NI6XNdB4/v6TFQJxtE= +cloud.google.com/go/trace v1.10.4 h1:2qOAuAzNezwW3QN+t41BtkDJOG42HywL73q8x/f6fnM= +cloud.google.com/go/translate v1.9.3 h1:t5WXTqlrk8VVJu/i3WrYQACjzYJiff5szARHiyqqPzI= +cloud.google.com/go/video v1.20.3 h1:Xrpbm2S9UFQ1pZEeJt9Vqm5t2T/z9y/M3rNXhFoo8Is= +cloud.google.com/go/videointelligence v1.11.4 h1:YS4j7lY0zxYyneTFXjBJUj2r4CFe/UoIi/PJG0Zt/Rg= +cloud.google.com/go/vision v1.2.0 h1:/CsSTkbmO9HC8iQpxbK8ATms3OQaX3YQUeTMGCxlaK4= +cloud.google.com/go/vision/v2 v2.7.5 h1:T/ujUghvEaTb+YnFY/jiYwVAkMbIC8EieK0CJo6B4vg= +cloud.google.com/go/vmmigration v1.7.4 h1:qPNdab4aGgtaRX+51jCOtJxlJp6P26qua4o1xxUDjpc= +cloud.google.com/go/vmwareengine v1.0.3 h1:WY526PqM6QNmFHSqe2sRfK6gRpzWjmL98UFkql2+JDM= +cloud.google.com/go/vpcaccess v1.7.4 h1:zbs3V+9ux45KYq8lxxn/wgXole6SlBHHKKyZhNJoS+8= +cloud.google.com/go/webrisk v1.9.4 h1:iceR3k0BCRZgf2D/NiKviVMFfuNC9LmeNLtxUFRB/wI= +cloud.google.com/go/websecurityscanner v1.6.4 h1:5Gp7h5j7jywxLUp6NTpjNPkgZb3ngl0tUSw6ICWvtJQ= +cloud.google.com/go/workflows v1.12.3 h1:qocsqETmLAl34mSa01hKZjcqAvt699gaoFbooGGMvaM= +contrib.go.opencensus.io/exporter/aws v0.0.0-20200617204711-c478e41e60e9 h1:yxE46rQA0QaqPGqN2UnwXvgCrRqtjR1CsGSWVTRjvv4= +contrib.go.opencensus.io/exporter/prometheus v0.4.2 h1:sqfsYl5GIY/L570iT+l93ehxaWJs2/OwXtiWwew3oAg= contrib.go.opencensus.io/exporter/prometheus v0.4.2/go.mod h1:dvEHbiKmgvbr5pjaF9fpw1KeYcjrnC1J8B+JKjsZyRQ= +contrib.go.opencensus.io/exporter/stackdriver v0.13.10 h1:a9+GZPUe+ONKUwULjlEOucMMG0qfSCCenlji0Nhqbys= +contrib.go.opencensus.io/integrations/ocsql v0.1.7 h1:G3k7C0/W44zcqkpRSFyjU9f6HZkbwIrL//qqnlqWZ60= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9 h1:VpgP7xuJadIUuKccphEpTJnWhS2jkQyMt6Y7pJCD7fY= +docker.io/go-docker v1.0.0 h1:VdXS/aNYQxyA9wdLD5z8Q8Ro688/hG8HzKxYVEVbE6s= docker.io/go-docker v1.0.0/go.mod h1:7tiAn5a0LFmjbPDbyTPOaTTOuG1ZRNXdPA6RvKY+fpY= +filippo.io/edwards25519 v1.0.0 h1:0wAIcmJUqRdI8IJ/3eGi5/HwXZWPujYXXlkrQogz0Ek= filippo.io/edwards25519 v1.0.0/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= +gioui.org v0.0.0-20210308172011-57750fc8a0a6 h1:K72hopUosKG3ntOPNG4OzzbuhxGuVf06fa2la1/H/Ho= +git.sr.ht/~sbinet/gg v0.3.1 h1:LNhjNn8DerC8f9DHLz6lS0YYul/b602DUxDgGkd/Aik= +github.com/99designs/basicauth-go v0.0.0-20160802081356-2a93ba0f464d h1:j6oB/WPCigdOkxtuPl1VSIiLpy7Mdsu6phQffbF19Ng= +github.com/99designs/httpsignatures-go v0.0.0-20170731043157-88528bf4ca7e h1:rl2Aq4ZODqTDkeSqQBy+fzpZPamacO1Srp8zq7jf2Sc= +github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9 h1:HD8gA2tkByhMAwYaFAX9w2l7vxvBQ5NMoxDrkhqhtn4= +github.com/Azure/azure-amqp-common-go/v3 v3.2.2 h1:CJpxNAGxP7UBhDusRUoaOn0uOorQyAYhQYLnNgkRhlY= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal v1.1.2 h1:mLY+pNLjCUeKhgnAJWAKhEUQM+RJQo2H1fuGSw1Ky1E= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.0.0 h1:ECsQtyERDVz3NP3kvDOTLvbQhqWp/x9EsGKtb4ogUr8= +github.com/Azure/azure-service-bus-go v0.11.5 h1:EVMicXGNrSX+rHRCBgm/TRQ4VUZ1m3yAYM/AB2R/SOs= +github.com/Azure/go-amqp v0.16.4 h1:/1oIXrq5zwXLHaoYDliJyiFjJSpJZMWGgtMX9e0/Z30= +github.com/Azure/go-autorest/autorest/azure/auth v0.5.11 h1:P6bYXFoao05z5uhOQzbC3Qd8JqF3jUoocoTeIxkp2cA= github.com/Azure/go-autorest/autorest/azure/auth v0.5.11/go.mod h1:84w/uV8E37feW2NCJ08uT9VBfjfUHpgLVnG2InYD6cg= +github.com/Azure/go-autorest/autorest/azure/cli v0.4.5 h1:0W/yGmFdTIT77fvdlGZ0LMISoLHFJ7Tx4U0yeB+uFs4= github.com/Azure/go-autorest/autorest/azure/cli v0.4.5/go.mod h1:ADQAXrkgm7acgWVUNamOgh8YNrv4p27l3Wc55oVfpzg= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 h1:1BDTz0u9nC3//pOCMdNH+CiXJVYJh5UQNCOBG7jbELc= +github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 h1:sR+/8Yb4slttB4vD+b9btVEnWgL3Q00OBTzVT8B9C0c= +github.com/CloudyKit/jet/v3 v3.0.0 h1:1PwO5w5VCtlUUl+KTOBsTGZlhjWkcybsGaAau52tOy8= +github.com/DataDog/datadog-go v3.2.0+incompatible h1:qSG2N4FghB1He/r2mFrWKCaL7dXCilEuNEeAn20fdD4= +github.com/GoogleCloudPlatform/cloudsql-proxy v1.29.0 h1:YNu23BtH0PKF+fg3ykSorCp6jSTjcEtfnYLzbmcjVRA= +github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c h1:RGWPOewvKIROun94nF7v2cua9qP+thov/7M50KEoeSU= +github.com/Joker/hpp v1.0.0 h1:65+iuJYdRXv/XyN62C1uEmmOx3432rNG/rKlX6V7Kkc= +github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible h1:1G1pk05UrOh0NlF1oeaaix1x8XzrfjIDK47TY0Zehcw= +github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= +github.com/OneOfOne/xxhash v1.2.6 h1:U68crOE3y3MPttCMQGywZOLrTeF5HHJ3/vDBCJn9/bA= github.com/OneOfOne/xxhash v1.2.6/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q= +github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= +github.com/RaveNoX/go-jsoncommentstrip v1.0.0 h1:t527LHHE3HmiHrq74QMpNPZpGCIJzTx+apLkMKt4HC0= +github.com/RoaringBitmap/gocroaring v0.4.0 h1:5nufXUgWpBEUNEJXw7926YAA58ZAQRpWPrQV1xCoSjc= +github.com/RoaringBitmap/real-roaring-datasets v0.0.0-20190726190000-eb7c87156f76 h1:ZYlhPbqQFU+AHfgtCdHGDTtRW1a8geZyiE8c6Q+Sl1s= +github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398 h1:WDC6ySpJzbxGWFh4aMxFFC28wwGp5pEuoTtvA4q/qQ4= +github.com/Shopify/sarama v1.38.1 h1:lqqPUPQZ7zPqYlWpTh+LQ9bhYNu2xJL6k1SJN4WVe2A= github.com/Shopify/sarama v1.38.1/go.mod h1:iwv9a67Ha8VNa+TifujYoWGxWnu2kNVAQdSdZ4X2o5g= +github.com/Shopify/toxiproxy v2.1.4+incompatible h1:TKdv8HiTLgE5wdJuEML90aBgNWsokNbMijUGhmcoBJc= +github.com/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrdtl/UvroE= +github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5 h1:rFw4nCn9iMW+Vajsk51NtYIcwSTkXr+JGrMd36kTDJw= +github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= +github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9 h1:7kQgkwGRoLzC9K0oyXdJo7nve/bynv/KwUsxbiTlzAM= +github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19 h1:iXUgAaqDcIUGbRoy2TdeofRG/j1zpGRSEmNK05T+bi8= +github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b h1:slYM766cy2nI3BwyRiyQj/Ud48djTMtMebDqepE95rw= +github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY= +github.com/alecthomas/kong v0.2.11 h1:RKeJXXWfg9N47RYfMm0+igkxBCTF4bzbneAxaqid0c4= github.com/alecthomas/kong v0.2.11/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE= +github.com/alecthomas/participle/v2 v2.1.0 h1:z7dElHRrOEEq45F2TG5cbQihMtNTv8vwldytDj7Wrz4= github.com/alecthomas/participle/v2 v2.1.0/go.mod h1:Y1+hAs8DHPmc3YUFzqllV+eSQ9ljPTk0ZkPMtEdAx2c= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= github.com/alicebob/miniredis v2.5.0+incompatible h1:yBHoLpsyjupjz3NL3MhKMVkR41j82Yjf3KFv7ApYzUI= github.com/alicebob/miniredis v2.5.0+incompatible/go.mod h1:8HZjEj4yU0dwhYHky+DxYx+6BMjkBbe5ONFIF1MXffk= +github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg= +github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40 h1:q4dksr6ICHXqG5hm0ZW5IHyeEJXoIJSOZeBLmWPNeIQ= github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40/go.mod h1:Q7yQnSMnLvcXlZ8RV+jwz/6y1rQTqbX6C82SndT52Zs= +github.com/apache/arrow/go/v10 v10.0.1 h1:n9dERvixoC/1JjDmBcs9FPaEryoANa2sCgVFo6ez9cI= +github.com/apache/arrow/go/v11 v11.0.0 h1:hqauxvFQxww+0mEU/2XHG6LT7eZternCZq+A5Yly2uM= +github.com/apache/arrow/go/v12 v12.0.0 h1:xtZE63VWl7qLdB0JObIXvvhGjoVNrQ9ciIHG2OK5cmc= +github.com/apache/arrow/go/v13 v13.0.0 h1:kELrvDQuKZo8csdWYqBQfyi431x6Zs/YJTEgUuSVcWk= github.com/apache/arrow/go/v13 v13.0.0/go.mod h1:W69eByFNO0ZR30q1/7Sr9d83zcVZmF2MiP3fFYAWJOc= +github.com/apache/thrift v0.18.1 h1:lNhK/1nqjbwbiOPDBPFJVKxgDEGSepKuTh6OLiXW8kg= github.com/apache/thrift v0.18.1/go.mod h1:rdQn/dCcDKEWjjylUeueum4vQEjG2v8v2PqriUnbr+I= +github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3 h1:ZSTrOEhiM5J5RFxEaFvMZVEAM1KvT1YzbEOwB2EAGjA= github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e h1:QEF07wC0T1rKkctt1RINW/+RMTVmiwxETico2l3gxJA= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6 h1:G1bPvciwNyF7IUmKXNt9Ak3m6u9DE1rF+RmtIkBpVdA= +github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a h1:pv34s756C4pEXnjgPfGYgdhg/ZdajGhyOvzx8k+23nw= +github.com/aws/aws-lambda-go v1.13.3 h1:SuCy7H3NLyp+1Mrfp+m80jcbi9KYWAs9/BXwppwRDzY= +github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.8.1 h1:w/fPGB0t5rWwA43mux4e9ozFSH5zF1moQemlA131PWc= +github.com/aws/aws-sdk-go-v2/service/kms v1.16.3 h1:nUP29LA4GZZPihNSo5ZcF4Rl73u+bN5IBRnrQA0jFK4= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.4 h1:EmIEXOjAdXtxa2OGM1VAajZV/i06Q8qd4kBpJd9/p1k= +github.com/aws/aws-sdk-go-v2/service/sns v1.17.4 h1:7TdmoJJBwLFyakXjfrGztejwY5Ie1JEto7YFfznCmAw= +github.com/aws/aws-sdk-go-v2/service/sqs v1.18.3 h1:uHjK81fESbGy2Y9lspub1+C6VN5W2UXTDo2A/Pm4G0U= +github.com/aws/aws-sdk-go-v2/service/ssm v1.24.1 h1:zc1YLcknvxdW/i1MuJKmEnFB2TNkOfguuQaGRvJXPng= +github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible h1:Ppm0npCCsmuR9oQaBtRuZcmILVE74aXE+AmrJj8L2ns= +github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= +github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 h1:mXoPYz/Ul5HYEDvkta6I8/rnYM5gSdSV2tJ6XbZuEtY= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= +github.com/boombuler/barcode v1.0.1 h1:NDBbPmhS+EqABEs5Kg3n/5ZNjy73Pz7SIV+KCeqyXcs= +github.com/bwesterb/go-ristretto v1.2.3 h1:1w53tCkGhCQ5djbat3+MH0BAQ5Kfgbt56UZQ/JMzngw= +github.com/casbin/casbin/v2 v2.37.0 h1:/poEwPSovi4bTOcP752/CsTQiRz2xycyVKFG7GUhbDw= github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= +github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= +github.com/chromedp/chromedp v0.9.2 h1:dKtNz4kApb06KuSXoTQIyUC2TrA0fhGDwNZf3bcgfKw= +github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic= +github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= +github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= +github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= +github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible h1:C29Ae4G5GtYyYMm1aztcyj/J5ckgJm2zwdDajFbx1NY= +github.com/circonus-labs/circonusllhist v0.1.3 h1:TJH+oke8D16535+jHExHj4nQvzlZrj7ug5D7I/orNUA= +github.com/clbanning/mxj v1.8.4 h1:HuhwZtbyvyOw+3Z1AowPkU87JkJUSv751ELWaiTpj8I= +github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec h1:EdRZT3IeKQmfCSrgo8SZ8V3MEnskuJP0wCYNpe+aiXo= +github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI= +github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe h1:QQ3GSy+MqSHxm/d8nCtnAiZdYFd45cYZPs8vOOIYKfk= github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= +github.com/cockroachdb/cockroach-go v0.0.0-20181001143604-e0a95dfd547c h1:2zRrJWIt/f9c9HhNHAgrRgq0San5gRRUJTBXLkchal0= +github.com/cockroachdb/datadriven v1.0.2 h1:H9MtNqVoVhvd9nCBwOyDjUEdZCREqbIdCJD93PBm/jA= +github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd h1:qMd81Ts1T2OTKmB4acZcyKaMtRnY5Y44NuXGX2GFJ1w= +github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0 h1:sDMmm+q/3+BukdIpxwO365v/Rbspp2Nt5XntgQRXq8Q= +github.com/containerd/containerd v1.6.8 h1:h4dOFDwzHmqFEP754PgfgTeVXFnLiRc6kiqC7tplDJs= github.com/containerd/containerd v1.6.8/go.mod h1:By6p5KqPK0/7/CgO/A6t/Gz+CUYUu2zf1hUaaymVXB0= +github.com/coreos/etcd v3.3.10+incompatible h1:jFneRYjIvLMLhDLCzuTuU4rSJUjRplcJQ7pD7MnhC04= +github.com/coreos/go-etcd v2.0.0+incompatible h1:bXhRBIXoTm9BYHS3gE0TtQuyNZyeEMux2sDi4oo5YOo= +github.com/coreos/go-oidc v2.2.1+incompatible h1:mh48q/BqXqgjVHpy2ZY7WnWAbenxRjsz9N1i1YxjHAk= github.com/coreos/go-oidc v2.2.1+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f h1:JOrtw2xFKzlg+cbHpyrpLDmnN1HqhBfnX7WDiW7eG2c= +github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf h1:CAKfRE2YtTUIjjh1bkBtyYFaUT/WmOqsJjgtihT0vMI= github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk= +github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= +github.com/crewjam/httperr v0.2.0 h1:b2BfXR8U3AlIHwNeFFvZ+BV1LFvKLlzMjzaTnZMybNo= github.com/crewjam/httperr v0.2.0/go.mod h1:Jlz+Sg/XqBQhyMjdDiC+GNNRzZTD7x39Gu3pglZ5oH4= +github.com/cristalhq/hedgedhttp v0.9.1 h1:g68L9cf8uUyQKQJwciD0A1Vgbsz+QgCjuB1I8FAsCDs= github.com/cristalhq/hedgedhttp v0.9.1/go.mod h1:XkqWU6qVMutbhW68NnzjWrGtH8NUx1UfYqGYtHVKIsI= +github.com/cznic/b v0.0.0-20180115125044-35e9bbe41f07 h1:UHFGPvSxX4C4YBApSPvmUfL8tTvWLj2ryqvT9K4Jcuk= +github.com/cznic/fileutil v0.0.0-20180108211300-6a051e75936f h1:7uSNgsgcarNk4oiN/nNkO0J7KAjlsF5Yv5Gf/tFdHas= +github.com/cznic/golex v0.0.0-20170803123110-4ab7c5e190e4 h1:CVAqftqbj+exlab+8KJQrE+kNIVlQfJt58j4GxCMF1s= +github.com/cznic/internal v0.0.0-20180608152220-f44710a21d00 h1:FHpbUtp2K8X53/b4aFNj4my5n+i3x+CQCZWNuHWH/+E= +github.com/cznic/lldb v1.1.0 h1:AIA+ham6TSJ+XkMe8imQ/g8KPzMUVWAwqUQQdtuMsHs= +github.com/cznic/mathutil v0.0.0-20180504122225-ca4c9f2c1369 h1:XNT/Zf5l++1Pyg08/HV04ppB0gKxAqtZQBRYiYrUuYk= +github.com/cznic/ql v1.2.0 h1:lcKp95ZtdF0XkWhGnVIXGF8dVD2X+ClS08tglKtf+ak= +github.com/cznic/sortutil v0.0.0-20150617083342-4c7342852e65 h1:hxuZop6tSoOi0sxFzoGGYdRqNrPubyaIf9KoBG9tPiE= +github.com/cznic/strutil v0.0.0-20171016134553-529a34b1c186 h1:0rkFMAbn5KBKNpJyHQ6Prb95vIKanmAe62KxsrN+sqA= +github.com/cznic/zappy v0.0.0-20160723133515-2533cb5b45cc h1:YKKpTb2BrXN2GYyGaygIdis1vXbE7SSAG9axGWIMClg= +github.com/dchest/uniuri v1.2.0 h1:koIcOUdrTIivZgSLhHQvKgqdWZq5d7KdMEWF1Ud6+5g= github.com/dchest/uniuri v1.2.0/go.mod h1:fSzm4SLHzNZvWLvWJew423PhAzkpNQYq+uNLq4kxhkY= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= +github.com/denisenkom/go-mssqldb v0.12.0 h1:VtrkII767ttSPNRfFekePK3sctr+joXgO58stqQbtUA= +github.com/devigned/tab v0.1.1 h1:3mD6Kb1mUOYeLpJvTVSDwSg5ZsfSxfvxGRTxRsJsITA= +github.com/dgraph-io/badger v1.6.0 h1:DshxFxZWXUcO0xX476VJC07Xsr6ZCBVRHKZ93Oh7Evo= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= +github.com/dhui/dktest v0.3.0 h1:kwX5a7EkLcjo7VpsPQSYJcKGbXBXdjI9FGjuUj1jn6I= +github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U= +github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 h1:bWDMxwH3px2JBh6AyO7hdCn/PkvCZXii8TGj7sbtEbQ= +github.com/drone/drone-runtime v1.1.0 h1:IsKbwiLY6+ViNBzX0F8PERJVZZcEJm9rgxEh3uZP5IE= github.com/drone/drone-runtime v1.1.0/go.mod h1:+osgwGADc/nyl40J0fdsf8Z09bgcBZXvXXnLOY48zYs= +github.com/drone/drone-yaml v1.2.3 h1:SWzLmzr8ARhbtw1WsVDENa8WFY2Pi9l0FVMfafVUWz8= github.com/drone/drone-yaml v1.2.3/go.mod h1:QsqliFK8nG04AHFN9tTn9XJomRBQHD4wcejWW1uz/10= +github.com/drone/funcmap v0.0.0-20211123105308-29742f68a7d1 h1:E8hjIYiEyI+1S2XZSLpMkqT9V8+YMljFNBWrFpuVM3A= github.com/drone/funcmap v0.0.0-20211123105308-29742f68a7d1/go.mod h1:Hph0/pT6ZxbujnE1Z6/08p5I0XXuOsppqF6NQlGOK0E= +github.com/drone/signal v1.0.0 h1:NrnM2M/4yAuU/tXs6RP1a1ZfxnaHwYkd0kJurA1p6uI= +github.com/eapache/go-resiliency v1.3.0 h1:RRL0nge+cWGlxXbUzJ7yMcq6w2XBEr19dCN6HECGaT0= github.com/eapache/go-resiliency v1.3.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6iZYSs1ZI+iQho= +github.com/eapache/go-xerial-snappy v0.0.0-20230111030713-bf00bc1b83b6 h1:8yY/I9ndfrgrXUbOGObLHKBR4Fl3nZXwM2c7OYTT8hM= github.com/eapache/go-xerial-snappy v0.0.0-20230111030713-bf00bc1b83b6/go.mod h1:YvSRo5mw33fLEx1+DlK6L2VV43tJt5Eyel9n9XBcR+0= +github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc= +github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385 h1:clC1lXBpe2kTj2VHdaIu9ajZQe4kcEY9j0NsnDDBZ3o= +github.com/etcd-io/bbolt v1.3.3 h1:gSJmxrs37LgTqR/oyJBWok6k6SvXEUerFTbltIhXkBM= +github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb h1:IT4JYU7k4ikYg1SCxNI1/Tieq/NFvh6dzLdgi7eu0tM= github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb/go.mod h1:bH6Xx7IW64qjjJq8M2u4dxNaBiDfKK+z/3eGDpXEQhc= +github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072 h1:DddqAaWDpywytcG8w/qoQ5sAN8X12d3Z3koB0C3Rxsc= +github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= +github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8= +github.com/form3tech-oss/jwt-go v3.2.2+incompatible h1:TcekIExNqud5crz4xD2pavyTgWiPvpYe4Xau31I0PRk= +github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= +github.com/franela/goblin v0.0.0-20210519012713-85d372ac71e2 h1:cZqz+yOJ/R64LcKjNQOdARott/jP7BnUQ9Ah7KaZCvw= +github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8 h1:a9ENSRDFBUPkJ5lCgVZh26+ZbGyoVJG7yb5SSzF5H54= +github.com/fsouza/fake-gcs-server v1.7.0 h1:Un0BXUXrRWYSmYyC1Rqm2e2WJfTPyDy/HGMz31emTi8= +github.com/gavv/httpexpect v2.0.0+incompatible h1:1X9kcRshkSKEjNJJxX9Y9mQ5BRfbxU5kORdjhlA1yX8= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8= github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= +github.com/go-check/check v0.0.0-20180628173108-788fd7840127 h1:0gkP6mzaMqkmpcJYCFOLkIBwI7xFExG03bbkOkCvUPI= +github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8= github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-fonts/dejavu v0.1.0 h1:JSajPXURYqpr+Cu8U9bt8K+XcACIHWqWrvWCKyeFmVQ= +github.com/go-fonts/latin-modern v0.2.0 h1:5/Tv1Ek/QCr20C6ZOz15vw3g7GELYL98KWr8Hgo+3vk= +github.com/go-fonts/liberation v0.2.0 h1:jAkAWJP4S+OsrPLZM4/eC9iW7CtHy+HBXrEwZXWo5VM= +github.com/go-fonts/stix v0.1.0 h1:UlZlgrvvmT/58o573ot7NFw0vZasZ5I6bcIft/oMdgg= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1 h1:QbL/5oDUmRBzO9/Z7Seo6zf912W/a6Sr4Eu0G/3Jho0= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4 h1:WtGNWLvXpe6ZudgnXrq0barxBImvnnJoMEhXAzcbM0I= +github.com/go-ini/ini v1.25.4 h1:Mujh4R/dH6YL8bxuISne3xX2+qcQ9p0IxKAP6ExWoUo= +github.com/go-kit/kit v0.12.0 h1:e4o3o3IsBfAKQh5Qbbiqyfu97Ku7jrO/JbohvztANh4= +github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81 h1:6zl3BbBhdnMkpSj2YY30qV3gDcVBGtFgVsV3+/i+mKQ= +github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab h1:xveKWz2iaueeTaUgdetzel+U7exyigDYBryyVfV/rZk= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-pdf/fpdf v0.6.0 h1:MlgtGIfsdMEEQJr2le6b/HNr1ZlQwxyWr77r2aj2U/8= +github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= +github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= +github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= +github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJWXmqUsHwfTRRkQ= github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU= +github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd h1:hSkbZ9XSyjyBirMeqSqUrK+9HboWrweVlzRNqoBi2d4= +github.com/gobuffalo/depgen v0.1.0 h1:31atYa/UW9V5q8vMJ+W6wd64OaaTHUrCUXER358zLM4= +github.com/gobuffalo/envy v1.7.0 h1:GlXgaiBkmrYMHco6t4j7SacKO4XUjvh5pwXh0f4uxXU= +github.com/gobuffalo/flect v0.1.3 h1:3GQ53z7E3o00C/yy7Ko8VXqQXoJGLkrTQCLTF1EjoXU= +github.com/gobuffalo/genny v0.1.1 h1:iQ0D6SpNXIxu52WESsD+KoQ7af2e3nCfnSBoSF/hKe0= +github.com/gobuffalo/gitgen v0.0.0-20190315122116-cc086187d211 h1:mSVZ4vj4khv+oThUfS+SQU3UuFIZ5Zo6UNcvK8E8Mz8= +github.com/gobuffalo/gogen v0.1.1 h1:dLg+zb+uOyd/mKeQUYIbwbNmfRsr9hd/WtYWepmayhI= +github.com/gobuffalo/logger v0.0.0-20190315122211-86e12af44bc2 h1:8thhT+kUJMTMy3HlX4+y9Da+BNJck+p109tqqKp7WDs= +github.com/gobuffalo/mapi v1.0.2 h1:fq9WcL1BYrm36SzK6+aAnZ8hcp+SrmnDyAxhNx8dvJk= +github.com/gobuffalo/packd v0.1.0 h1:4sGKOD8yaYJ+dek1FDkwcxCHA40M4kfKgFHx8N2kwbU= +github.com/gobuffalo/packr/v2 v2.2.0 h1:Ir9W9XIm9j7bhhkKE9cokvtTl1vBm62A/fene/ZCj6A= +github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754 h1:tpom+2CJmpzAWj5/VEHync2rJGi+epHNIeRSWjzGA+4= +github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= +github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= +github.com/gobwas/ws v1.2.1 h1:F2aeBZrm2NDsc7vbovKrWSogd4wvfAxg0FQ89/iqOTk= +github.com/goccy/go-yaml v1.11.0 h1:n7Z+zx8S9f9KgzG6KtQKf+kwqXZlLNR2F6018Dgau54= github.com/goccy/go-yaml v1.11.0/go.mod h1:H+mJrWtjPTJAHvRbV09MCK9xYwODM+wRTVFFTWckfng= +github.com/gocql/gocql v0.0.0-20190301043612-f6df8288f9b4 h1:vF83LI8tAakwEwvWZtrIEx7pOySacl2TOxx6eXk4ePo= +github.com/godbus/dbus/v5 v5.0.4 h1:9349emZab16e7zQvpmsbtjc18ykshndd8y2PG3sgJbA= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= +github.com/gomodule/redigo v1.8.9 h1:Sl3u+2BI/kk+VEatbj0scLdrFhjPmbxOc1myhDP41ws= github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE= +github.com/google/go-jsonnet v0.18.0 h1:/6pTy6g+Jh1a1I2UMoAODkqELFiVIdOxbNwv0DDzoOg= github.com/google/go-jsonnet v0.18.0/go.mod h1:C3fTzyVJDslXdiTqw/bTFk7vSGyCtH3MGRbDfvEwGd0= +github.com/google/go-pkcs11 v0.2.1-0.20230907215043-c6f79328ddf9 h1:OF1IPgv+F4NmqmJ98KTjdN97Vs1JxDPB3vbmYzV2dpk= +github.com/google/go-replayers/grpcreplay v1.1.0 h1:S5+I3zYyZ+GQz68OfbURDdt/+cSMqCK1wrvNx7WBzTE= +github.com/google/go-replayers/httpreplay v1.1.1 h1:H91sIMlt1NZzN7R+/ASswyouLJfW0WLW7fhyUFvDEkY= +github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/google/subcommands v1.0.1 h1:/eqq+otEXm5vhfBrbREPCSVQbvofip6kIz+mX5TUH7k= +github.com/googleapis/go-type-adapters v1.0.0 h1:9XdMn+d/G57qq1s8dNc5IesGCXHf6V2HZ2JwRxfA2tA= +github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8 h1:tlyzajkF3030q6M8SvmJSemC9DTHL/xaMa18b65+JM4= +github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= +github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= +github.com/grafana/e2e v0.1.1-0.20221018202458-cffd2bb71c7b h1:Ha+kSIoTutf4ytlVw/SaEclDUloYx0+FXDKJWKhNbE4= github.com/grafana/e2e v0.1.1-0.20221018202458-cffd2bb71c7b/go.mod h1:3UsooRp7yW5/NJQBlXcTsAHOoykEhNUYXkQ3r6ehEEY= +github.com/grafana/gomemcache v0.0.0-20231023152154-6947259a0586 h1:/of8Z8taCPftShATouOrBVy6GaTTjgQd/VfNiZp/VXQ= github.com/grafana/gomemcache v0.0.0-20231023152154-6947259a0586/go.mod h1:PGk3RjYHpxMM8HFPhKKo+vve3DdlPUELZLSDEFehPuU= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM= +github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 h1:MJG/KsmcqMwFAkh8mTnAwhyKoB+sTAnY4CACC110tbU= github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645/go.mod h1:6iZfnjpejD4L/4DwD7NryNaJyCQdzwWwH2MWhCA90Kw= +github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8= +github.com/hamba/avro/v2 v2.17.2 h1:6PKpEWzJfNnvBgn7m2/8WYaDOUASxfDU+Jyb4ojDgFY= github.com/hamba/avro/v2 v2.17.2/go.mod h1:Q9YK+qxAhtVrNqOhwlZTATLgLA8qxG2vtvkhK8fJ7Jo= +github.com/hanwen/go-fuse v1.0.0 h1:GxS9Zrn6c35/BnfiVsZVWmsG803xwE7eVRDvcf/BEVc= +github.com/hanwen/go-fuse/v2 v2.1.0 h1:+32ffteETaLYClUj0a3aHjZ1hOPxxaNEHiZiujuDaek= +github.com/hashicorp/consul/sdk v0.15.0 h1:2qK9nDrr4tiJKRoxPGhm6B7xJjLVIQqkjiab2M4aKjU= +github.com/hashicorp/go-syslog v1.0.0 h1:KaodqZuhUoZereWVIYmpUgZysurB1kBLX2j0MwMrUAE= +github.com/hashicorp/go.net v0.0.1 h1:sNCoNyDEvN1xa+X0baata4RdcpKwcMS6DH+xwfqPgjw= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= +github.com/hashicorp/mdns v1.0.4 h1:sY0CMhFmjIPDMlTB+HfymFHCaYLhgifZ0QhjaYKD/UQ= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= +github.com/hudl/fargo v1.4.0 h1:ZDDILMbB37UlAVLlWcJ2Iz1XuahZZTDZfdCKeclfq2s= +github.com/hydrogen18/memlistener v0.0.0-20200120041712-dcc25e7acd91 h1:KyZDvZ/GGn+r+Y3DKZ7UOQ/TP4xV6HNkrwiVMB1GnNY= +github.com/iancoleman/strcase v0.2.0 h1:05I4QRnGpI0m37iZQRuskXh+w77mr6Z41lwQzuHLwW0= +github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab h1:BA4a7pe6ZTd9F8kXETBoijjFJ/ntaa//1wiH9BZu4zU= +github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk= +github.com/influxdata/influxdb v1.7.6 h1:8mQ7A/V+3noMGCt/P9pD09ISaiz9XvgCk303UYA3gcs= +github.com/influxdata/influxdb1-client v0.0.0-20200827194710-b269163b24ab h1:HqW4xhhynfjrtEiiSGcQUd6vrK23iMam1FO8rI7mwig= +github.com/iris-contrib/blackfriday v2.0.0+incompatible h1:o5sHQHHm0ToHUlAJSTjW9UWicjJSDDauOOQ2AHuIVp4= +github.com/iris-contrib/go.uuid v2.0.0+incompatible h1:XZubAYg61/JwnJNbZilGjf3b3pB80+OQg2qf6c8BfWE= +github.com/iris-contrib/jade v1.1.3 h1:p7J/50I0cjo0wq/VWVCDFd8taPJbuFC+bq23SniRFX0= +github.com/iris-contrib/pongo2 v0.0.1 h1:zGP7pW51oi5eQZMIlGA3I+FHY9/HOQWDB+572yin0to= +github.com/iris-contrib/schema v0.0.1 h1:10g/WnoRR+U+XXHWKBHeNy/+tZmM2kcAVGLOsz+yaDA= +github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0= +github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= +github.com/jackc/fake v0.0.0-20150926172116-812a484cc733 h1:vr3AYkKovP8uR8AvSGGUK1IDqRa5lAAvEkZG1LKaCRc= +github.com/jackc/pgconn v1.11.0 h1:HiHArx4yFbwl91X3qqIHtUFoiIfLNJXCQRsnzkiwwaQ= +github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= +github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A= +github.com/jackc/pgproto3/v2 v2.2.0 h1:r7JypeP2D3onoQTCxWdTpCtJ4D+qpKr0TxvoyMhZ5ns= +github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg= +github.com/jackc/pgtype v1.10.0 h1:ILnBWrRMSXGczYvmkYD6PsYyVFUNLTnIUJHHDLmqk38= +github.com/jackc/pgx v3.2.0+incompatible h1:0Vihzu20St42/UDsvZGdNE6jak7oi/UOeMzwMPHkgFY= +github.com/jackc/pgx/v4 v4.15.0 h1:B7dTkXsdILD3MF987WGGCcg+tvLW6bZJdEcqVFeU//w= +github.com/jackc/puddle v1.2.1 h1:gI8os0wpRXFd4FiAY2dWiqRK037tjj3t7rKFeO4X5iw= +github.com/jackspirou/syscerts v0.0.0-20160531025014-b68f5469dff1 h1:9Xm8CKtMZIXgcopfdWk/qZ1rt0HjMgfMR9nxxSeK6vk= github.com/jackspirou/syscerts v0.0.0-20160531025014-b68f5469dff1/go.mod h1:zuHl3Hh+e9P6gmBPvcqR1HjkaWHC/csgyskg6IaFKFo= +github.com/jaegertracing/jaeger v1.41.0 h1:vVNky8dP46M2RjGaZ7qRENqylW+tBFay3h57N16Ip7M= github.com/jaegertracing/jaeger v1.41.0/go.mod h1:SIkAT75iVmA9U+mESGYuMH6UQv6V9Qy4qxo0lwfCQAc= +github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc= +github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= +github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= +github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg= github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= +github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= +github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8= github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= +github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= +github.com/jedib0t/go-pretty/v6 v6.2.4 h1:wdaj2KHD2W+mz8JgJ/Q6L/T5dB7kyqEFI16eLq7GEmk= github.com/jedib0t/go-pretty/v6 v6.2.4/go.mod h1:+nE9fyyHGil+PuISTCrp7avEdo6bqoMwqZnuiK2r2a0= +github.com/jhump/gopoet v0.1.0 h1:gYjOPnzHd2nzB37xYQZxj4EIQNpBrBskRqQQ3q4ZgSg= +github.com/jhump/goprotoc v0.5.0 h1:Y1UgUX+txUznfqcGdDef8ZOVlyQvnV0pKWZH08RmZuo= +github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o= +github.com/jsternberg/zap-logfmt v1.2.0 h1:1v+PK4/B48cy8cfQbxL4FmmNZrjnIMr2BsnyEmXqv2o= github.com/jsternberg/zap-logfmt v1.2.0/go.mod h1:kz+1CUmCutPWABnNkOu9hOHKdT2q3TDYCcsFy9hpqb0= +github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d h1:c93kUJDtVAXFEhsCh5jSxyOJmFHuzcihnslQiX8Urwo= +github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= +github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5 h1:PJr+ZMXIecYc1Ey2zucXdR73SMBtgjPgwa31099IMv0= +github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 h1:uC1QfSlInpQF+M0ao65imhwqKnz3Q2z/d8PWZRMQvDM= +github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA= +github.com/karrick/godirwalk v1.10.3 h1:lOpSw2vJP0y5eLBW906QwKsUK/fe/QDyoqM5rnnuPDY= +github.com/kataras/golog v0.0.10 h1:vRDRUmwacco/pmBAm8geLn8rHEdc+9Z4NAr5Sh7TG/4= +github.com/kataras/iris/v12 v12.1.8 h1:O3gJasjm7ZxpxwTH8tApZsvf274scSGQAUpNe47c37U= +github.com/kataras/neffos v0.0.14 h1:pdJaTvUG3NQfeMbbVCI8JT2T5goPldyyfUB2PJfh1Bs= +github.com/kataras/pio v0.0.2 h1:6NAi+uPJ/Zuid6mrAKlgpbI11/zK/lV4B2rxWaJN98Y= +github.com/kataras/sitemap v0.0.5 h1:4HCONX5RLgVy6G4RkYOV3vKNcma9p236LdGOipJsaFE= +github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= +github.com/kisielk/errcheck v1.5.0 h1:e8esj/e4R+SAOwFwN+n3zr0nYeCyeweozKfO23MvHzY= +github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg= +github.com/klauspost/asmfmt v1.3.2 h1:4Ri7ox3EwapiOjCki+hw14RyKk201CN4rzyCJRFLpK4= github.com/klauspost/cpuid v1.2.1 h1:vJi+O/nMdFt0vqm8NZBI6wzALWdA2X+egi0ogNyrC/w= +github.com/knadh/koanf v1.5.0 h1:q2TSd/3Pyc/5yP9ldIrSdIz26MCcyNQzW0pEAugLPNs= github.com/knadh/koanf v1.5.0/go.mod h1:Hgyjp4y8v44hpZtPzs7JZfRAW5AhN7KfZcwv1RYggDs= +github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= +github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY= +github.com/kr/pty v1.1.8 h1:AkaSdXYQOWeaO3neb8EM634ahkXXe3jYbVh/F9lq+GI= +github.com/kshvakov/clickhouse v1.3.5 h1:PDTYk9VYgbjPAWry3AoDREeMgOVUFij6bh6IjlloHL0= +github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo= +github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= +github.com/lestrrat-go/backoff/v2 v2.0.8 h1:oNb5E5isby2kiro9AgdHLv5N5tint1AnDVVf2E2un5A= github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y= +github.com/lestrrat-go/blackmagic v1.0.0 h1:XzdxDbuQTz0RZZEmdU7cnQxUtFUzgCSPq8RCz4BxIi4= github.com/lestrrat-go/blackmagic v1.0.0/go.mod h1:TNgH//0vYSs8VXDCfkZLgIrVTTXQELZffUV0tz3MtdQ= +github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= +github.com/lestrrat-go/iter v1.0.1 h1:q8faalr2dY6o8bV45uwrxq12bRa1ezKrB6oM9FUgN4A= github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc= +github.com/lestrrat-go/jwx v1.2.25 h1:tAx93jN2SdPvFn08fHNAhqFJazn5mBBOB8Zli0g0otA= github.com/lestrrat-go/jwx v1.2.25/go.mod h1:zoNuZymNl5lgdcu6P7K6ie2QRll5HVfF4xwxBBK1NxY= +github.com/lestrrat-go/option v1.0.0 h1:WqAWL8kh8VcSoD6xjSH34/1m8yxluXQbDeKNfvFeEO4= github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743 h1:143Bb8f8DuGWck/xpNUOckBVYfFbBTnLevfRZ1aVVqo= +github.com/lightstep/lightstep-tracer-go v0.18.1 h1:vi1F1IQ8N7hNWytK9DpJsUfQhGuNSc19z330K6vl4zk= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/lyft/protoc-gen-star v0.6.1 h1:erE0rdztuaDq3bpGifD95wfoPrSZc95nGA6tbiNYh6M= +github.com/lyft/protoc-gen-star/v2 v2.0.3 h1:/3+/2sWyXeMLzKd1bX+ixWKgEMsULrIivpDsuaF441o= +github.com/lyft/protoc-gen-validate v0.0.13 h1:KNt/RhmQTOLr7Aj8PsJ7mTronaFyx80mRTT9qF261dA= +github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= +github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2 h1:JgVTCPf0uBVcUSWpyXmGpgOc62nK5HWUBKAGc3Qqa5k= +github.com/markbates/safe v1.0.1 h1:yjZkbvRM6IzKj9tlu/zMJLS0n/V351OZWRnF3QfaUxI= +github.com/matryer/moq v0.2.7 h1:RtpiPUM8L7ZSCbSwK+QcZH/E9tgqAkFjKQxsRs25b4w= github.com/matryer/moq v0.2.7/go.mod h1:kITsx543GOENm48TUAQyJ9+SAvFSr7iGQXPoth/VUBk= +github.com/mattn/goveralls v0.0.2 h1:7eJB6EqsPhRVxvwEXGnqdO2sJI0PTsrWoTMXEk9/OQc= +github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= +github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g= +github.com/mediocregopher/radix/v3 v3.4.2 h1:galbPBjIwmyREgwGCfQEN4X8lxbJnKBYurgz+VfcStA= +github.com/microcosm-cc/bluemonday v1.0.2 h1:5lPfLTTAvAbtS0VqT+94yOtFnGfUWYyx0+iToC3Os3s= +github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 h1:AMFGa4R4MiIpspGNG7Z948v4n35fFGB3RR3G/ry4FWs= +github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 h1:+n/aFZefKZp7spd8DFdX7uMikMLXX4oubIzJF4kv/wI= +github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g= +github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= +github.com/minio/minio-go/v7 v7.0.52 h1:8XhG36F6oKQUDDSuz6dY3rioMzovKjW40W6ANuN0Dps= github.com/minio/minio-go/v7 v7.0.52/go.mod h1:IbbodHyjUAguneyucUaahv+VMNs/EOTV9du7A7/Z3HU= +github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= +github.com/mitchellh/cli v1.1.5 h1:OxRIeJXpAMztws/XHlN2vu6imG5Dpq+j61AzAX5fLng= +github.com/mitchellh/gox v0.4.0 h1:lfGJxY7ToLJQjHHwi0EX6uYBdK78egf954SQl13PQJc= +github.com/mitchellh/iochan v1.0.0 h1:C+X3KsSTLFVBr/tK1eYN/vs4rJcvsiLU338UhYPJWeY= +github.com/mithrandie/readline-csvq v1.2.1 h1:4cfeYeVSrqKEWi/1t7CjyhFD2yS6fm+l+oe+WyoSNlI= github.com/mithrandie/readline-csvq v1.2.1/go.mod h1:ydD9Eyp3/wn8KPSNbKmMZe4RQQauCuxi26yEo4N40dk= +github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5 h1:8Q0qkMVC/MmWkpIdlvZgcv2o2jrlF6zqVOh7W5YHdMA= +github.com/montanaflynn/stats v0.7.0 h1:r3y12KyNxj/Sb/iOE46ws+3mS1+MZca1wlHQFPsY/JU= +github.com/mostynb/go-grpc-compression v1.1.17 h1:N9t6taOJN3mNTTi0wDf4e3lp/G/ON1TP67Pn0vTUA9I= github.com/mostynb/go-grpc-compression v1.1.17/go.mod h1:FUSBr0QjKqQgoDG/e0yiqlR6aqyXC39+g/hFLDfSsEY= +github.com/moul/http2curl v1.0.0 h1:dRMWoAtb+ePxMlLkrCbAqh4TlPHXvoGUSQ323/9Zahs= +github.com/nakagami/firebirdsql v0.0.0-20190310045651-3c02a58cfed8 h1:P48LjvUQpTReR3TQRbxSeSBsMXzfK0uol7eRcr7VBYQ= +github.com/natessilva/dag v0.0.0-20180124060714-7194b8dcc5c4 h1:dnMxwus89s86tI8rcGVp2HwZzlz7c5o92VOy7dSckBQ= +github.com/nats-io/jwt v1.2.2 h1:w3GMTO969dFg+UOKTmmyuu7IGdusK+7Ytlt//OYH/uU= +github.com/nats-io/jwt/v2 v2.0.3 h1:i/O6cmIsjpcQyWDYNcq2JyZ3/VTF8SJ4JWluI5OhpvI= +github.com/nats-io/nats-server/v2 v2.5.0 h1:wsnVaaXH9VRSg+A2MVg5Q727/CqxnmPLGFQ3YZYKTQg= +github.com/nats-io/nats.go v1.12.1 h1:+0ndxwUPz3CmQ2vjbXdkC1fo3FdiOQDim4gl3Mge8Qo= +github.com/nats-io/nkeys v0.3.0 h1:cgM5tL53EvYRU+2YLXIK0G2mJtK12Ft9oeooSZMA2G8= +github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/oklog/oklog v0.3.2 h1:wVfs8F+in6nTBMkA7CbRw+zZMIB7nNM825cM1wuzoTk= +github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU= github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= +github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88= +github.com/open-telemetry/opentelemetry-collector-contrib/exporter/jaegerexporter v0.74.0 h1:0dve/IbuHfQOnlIBQQwpCxIeMp7uig9DQVuvisWPDRs= github.com/open-telemetry/opentelemetry-collector-contrib/exporter/jaegerexporter v0.74.0/go.mod h1:bIeSj+SaZdP3CE9Xae+zurdQC6DXX0tPP6NAEVmgtt4= +github.com/open-telemetry/opentelemetry-collector-contrib/exporter/kafkaexporter v0.74.0 h1:MrVOfBTNBe4n/daZjV4yvHZRR0Jg/MOCl/mNwymHwDM= github.com/open-telemetry/opentelemetry-collector-contrib/exporter/kafkaexporter v0.74.0/go.mod h1:v4H2ATSrKfOTbQnmjCxpvuOjrO/GUURAgey9RzrPsuQ= +github.com/open-telemetry/opentelemetry-collector-contrib/exporter/zipkinexporter v0.74.0 h1:8Kk5g5PKQBUV3idjJy1NWVLLReEzjnB8C1lFgQxZ0TI= github.com/open-telemetry/opentelemetry-collector-contrib/exporter/zipkinexporter v0.74.0/go.mod h1:UtVfxZGhPU2OvDh7H8o67VKWG9qHAHRNkhmZUWqCvME= +github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal v0.74.0 h1:vU5ZebauzCuYNXFlQaWaYnOfjoOAnS+Sc8+oNWoHkbM= github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal v0.74.0/go.mod h1:TEu3TnUv1TuyHtjllrUDQ/ImpyD+GrkDejZv4hxl3G8= +github.com/open-telemetry/opentelemetry-collector-contrib/internal/sharedcomponent v0.74.0 h1:COFBWXiWnhRs9x1oYJbDg5cyiNAozp8sycriD9+1/7E= github.com/open-telemetry/opentelemetry-collector-contrib/internal/sharedcomponent v0.74.0/go.mod h1:cAKlYKU+/8mk6ETOnD+EAi5gpXZjDrGweAB9YTYrv/g= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/jaeger v0.74.0 h1:ww1pPXfAM0WHsymQnsN+s4B9DgwQC+GyoBq0t27JV/k= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/jaeger v0.74.0/go.mod h1:OpEw7tyCg+iG1ywEgZ03qe5sP/8fhYdtWCMoqA8JCug= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/opencensus v0.74.0 h1:0Fh6OjlUB9HlnX90/gGiyyFvnmNBv6inj7bSaVqQ7UQ= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/opencensus v0.74.0/go.mod h1:13ekplz1UmvK99Vz2VjSBWPYqoRBEax5LPmA1tFHnhA= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/zipkin v0.74.0 h1:A5xoBaMHX1WzLfvlqK6NBXq4XIbuSVJIpec5r6PDE7U= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/zipkin v0.74.0/go.mod h1:TJT7HkhFPrJic30Vk4seF/eRk8sa0VQ442Xq/qd+DLY= +github.com/open-telemetry/opentelemetry-collector-contrib/receiver/jaegerreceiver v0.74.0 h1:pWNSPCKD+V4rC+MnZj8uErEbcsYUpEqU3InNYyafAPY= github.com/open-telemetry/opentelemetry-collector-contrib/receiver/jaegerreceiver v0.74.0/go.mod h1:0lXcDf6LUbtDxZZO3zDbRzMuL7gL1Q0FPOR8/3IBwaQ= +github.com/open-telemetry/opentelemetry-collector-contrib/receiver/kafkareceiver v0.74.0 h1:NWd9+rQTd6pELLf3copo7CEuNgKp90kgyhPozpwax2U= github.com/open-telemetry/opentelemetry-collector-contrib/receiver/kafkareceiver v0.74.0/go.mod h1:anSbwGOousKpnNAVMNP5YieA4KOFuEzHkvya0vvtsaI= +github.com/open-telemetry/opentelemetry-collector-contrib/receiver/opencensusreceiver v0.74.0 h1:Law7+BImq8DIBsdniSX8Iy2/GH5CRHpT1gsRaC9ZT8A= github.com/open-telemetry/opentelemetry-collector-contrib/receiver/opencensusreceiver v0.74.0/go.mod h1:uiW3V9EX8A5DOoxqDLuSh++ewHr+owtonCSiqMcpy3w= +github.com/open-telemetry/opentelemetry-collector-contrib/receiver/zipkinreceiver v0.74.0 h1:2uysjsaqkf9STFeJN/M6i/sSYEN5pZJ94Qd2/Hg1pKE= github.com/open-telemetry/opentelemetry-collector-contrib/receiver/zipkinreceiver v0.74.0/go.mod h1:qoGuayD7cAtshnKosIQHd6dobcn6/sqgUn0v/Cg2UB8= +github.com/opentracing-contrib/go-grpc v0.0.0-20210225150812-73cb765af46e h1:4cPxUYdgaGzZIT5/j0IfqOrrXmq6bG8AwvwisMXpdrg= github.com/opentracing-contrib/go-grpc v0.0.0-20210225150812-73cb765af46e/go.mod h1:DYR5Eij8rJl8h7gblRrOZ8g0kW1umSpKqYIBTgeDtLo= +github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492 h1:lM6RxxfUMrYL/f8bWEUqdXrANWtrL7Nndbm9iFN0DlU= +github.com/opentracing/basictracer-go v1.0.0 h1:YyUAhaEfjoWXclZVJ9sGoNct7j4TVk7lZWlQw5UXuoo= +github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5 h1:ZCnq+JUrvXcDVhX/xRolRBZifmabN1HcS1wrPSvxhrU= +github.com/openzipkin/zipkin-go v0.4.1 h1:kNd/ST2yLLWhaWrkgchya40TJabe8Hioj9udfPcEO5A= github.com/openzipkin/zipkin-go v0.4.1/go.mod h1:qY0VqDSN1pOBN94dBc6w2GJlWLiovAyg7Qt6/I9HecM= +github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw= +github.com/pact-foundation/pact-go v1.0.4 h1:OYkFijGHoZAYbOIb1LWXrwKQbMMRUv1oQ89blD2Mh2Q= +github.com/pborman/uuid v1.2.0 h1:J7Q5mO4ysT1dv8hyrUGHb9+ooztCXu1D8MY8DZYsu3g= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml/v2 v2.0.5 h1:ipoSadvV8oGUjnUbMub59IDPPwfxF694nG/jwbMiyQg= +github.com/performancecopilot/speed v3.0.0+incompatible h1:2WnRzIquHa5QxaJKShDkLM+sc0JPuwhXzK8OYOyt3Vg= +github.com/performancecopilot/speed/v4 v4.0.0 h1:VxEDCmdkfbQYDlcr/GC9YoN9PQ6p8ulk9xVsepYy9ZY= +github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= +github.com/phpdave11/gofpdf v1.4.2 h1:KPKiIbfwbvC/wOncwhrpRdXVj2CZTCFlw4wnoyjtHfQ= +github.com/phpdave11/gofpdi v1.0.13 h1:o61duiW8M9sMlkVXWlvP92sZJtGKENvW3VExs6dZukQ= github.com/pierrec/lz4 v2.0.5+incompatible h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM8aXeqhl0I= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgcC4zIxODThtZNPirFr42+A= +github.com/pkg/profile v1.2.1 h1:F++O52m40owAmADcojzM+9gyjmMOY/T4oYJkgFDH8RE= +github.com/pkg/sftp v1.13.1 h1:I2qBYMChEhIjOgazfJmV3/mZM256btk6wkCDRmW7JYs= +github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/pquerna/cachecontrol v0.1.0 h1:yJMy84ti9h/+OEWa752kBTKv4XC30OtVVHYv/8cTqKc= github.com/pquerna/cachecontrol v0.1.0/go.mod h1:NrUG3Z7Rdu85UNR3vm7SOsl1nFIeSiQnrHV5K9mBcUI= +github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc= +github.com/prometheus/common/assets v0.2.0 h1:0P5OrzoHrYBOSM1OigWL3mY8ZvV2N4zIE/5AahrSrfM= github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= +github.com/prometheus/statsd_exporter v0.22.7 h1:7Pji/i2GuhK6Lu7DHrtTkFmNBCudCPT1pX2CziuyQR0= github.com/prometheus/statsd_exporter v0.22.7/go.mod h1:N/TevpjkIh9ccs6nuzY3jQn9dFqnUakOjnEuMPJJJnI= +github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/rogpeppe/fastuuid v1.2.0 h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi2s= +github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4 h1:BN/Nyn2nWMoqGRA7G7paDNDqTXE30mXGqzzybrfo05w= +github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.15.0 h1:uPRuwkWF4J6fGsJ2R0Gn2jB1EQiav9k3S6CSdygQJXY= github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww= +github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245 h1:K1Xf3bKttbF+koVGaX5xngRIZ5bVjbmPnaxE/dR08uY= +github.com/ryanuber/columnize v2.1.2+incompatible h1:C89EOx/XBWwIXl8wm8OPJBd7kPF25UfsK2X7Ph/zCAk= +github.com/sagikazarmark/crypt v0.6.0 h1:REOEXCs/NFY/1jOCEouMuT4zEniE5YoXbvpC5X/TLF8= +github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da h1:p3Vo3i64TCLY7gIfzeQaUJ+kppEO5WQG3cL8iE8tGHU= +github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= +github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiyyjYS17cCYRqP13/SHk= +github.com/segmentio/fasthash v0.0.0-20180216231524-a72b379d632e h1:uO75wNGioszjmIzcY/tvdDYKRLVvzggtAmmJkn9j4GQ= github.com/segmentio/fasthash v0.0.0-20180216231524-a72b379d632e/go.mod h1:tm/wZFQ8e24NYaBGIlnO2WGCAi67re4HHuOm0sftE/M= +github.com/segmentio/parquet-go v0.0.0-20230427215636-d483faba23a5 h1:7CWCjaHrXSUCHrRhIARMGDVKdB82tnPAQMmANeflKOw= github.com/segmentio/parquet-go v0.0.0-20230427215636-d483faba23a5/go.mod h1:+J0xQnJjm8DuQUHBO7t57EnmPbstT6+b45+p3DC9k1Q= +github.com/sercand/kuberesolver/v4 v4.0.0 h1:frL7laPDG/lFm5n98ODmWnn+cvPpzlkf3LhzuPhcHP4= github.com/sercand/kuberesolver/v4 v4.0.0/go.mod h1:F4RGyuRmMAjeXHKL+w4P7AwUnPceEAPAhxUgXZjKgvM= +github.com/sercand/kuberesolver/v5 v5.1.1 h1:CYH+d67G0sGBj7q5wLK61yzqJJ8gLLC8aeprPTHb6yY= github.com/sercand/kuberesolver/v5 v5.1.1/go.mod h1:Fs1KbKhVRnB2aDWN12NjKCB+RgYMWZJ294T3BtmVCpQ= +github.com/shirou/gopsutil/v3 v3.23.2 h1:PAWSuiAszn7IhPMBtXsbSCafej7PqUOvY6YywlQUExU= github.com/shirou/gopsutil/v3 v3.23.2/go.mod h1:gv0aQw33GLo3pG8SiWKiQrbDzbRY1K80RyZJ7V4Th1M= +github.com/shoenig/test v0.6.6 h1:Oe8TPH9wAbv++YPNDKJWUnI8Q4PPWCx3UbOfH+FxiMU= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/sony/gobreaker v0.4.1 h1:oMnRNZXX5j85zso6xCPRNPtmAycat+WcoKbklScLDgQ= +github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.9.2 h1:j49Hj62F0n+DaZ1dDCvhABaPNSGNkt32oRFxI33IEMw= github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= +github.com/spf13/viper v1.14.0 h1:Rg7d3Lo706X9tHsJMUjdiwMpHB7W8WnSVOssIY+JElU= github.com/spf13/viper v1.14.0/go.mod h1:WT//axPky3FdvXHzGw33dNdXXXfFQqmEalje+egj8As= +github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad h1:fiWzISvDn0Csy5H0iwgAuJGQTUpVfEMJJd4nRFXogbc= github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= +github.com/streadway/amqp v1.0.0 h1:kuuDrUJFZL1QYL9hUNuCxNObNzB0bV/ZG5jV3RWAQgo= +github.com/streadway/handy v0.0.0-20200128134331-0f66f006fb2e h1:mOtuXaRAbVZsxAHVdPR3IjfmN8T1h2iczJLynhLybf8= +github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs= +github.com/substrait-io/substrait-go v0.4.2 h1:buDnjsb3qAqTaNbOR7VKmNgXf4lYQxWEcnSGUWBtmN8= github.com/substrait-io/substrait-go v0.4.2/go.mod h1:qhpnLmrcvAnlZsUyPXZRqldiHapPTXC3t7xFgDi3aQg= +github.com/tidwall/gjson v1.14.2 h1:6BBkirS0rAHjumnjHF6qgy5d2YAJ1TLIaFE2lzfOLqo= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM= github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI= +github.com/tklauser/numcpus v0.6.0 h1:kebhY2Qt+3U6RNK7UqpYNA+tJ23IBEGKkB7JQBfDYms= github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ5UVIcaL4= +github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926 h1:G3dpKMzFDjgEh2q1Z7zUUtKa8ViPtH+ocF0bE0g00O8= +github.com/uber-go/atomic v1.4.0 h1:yOuPqEq4ovnhEjpHmfFwsqBXDYbQeT6Nb0bwD6XnD5o= github.com/uber-go/atomic v1.4.0/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex1PFV8g= +github.com/urfave/negroni v1.0.0 h1:kIimOitoypq34K7TG7DUaJ9kq/N4Ofuwi1sjz0KipXc= +github.com/valyala/fasthttp v1.6.0 h1:uWF8lgKmeaIewWVPwi4GRq2P6+R46IgYZdxWtM+GtEY= +github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a h1:0R4NLDRDZX6JcmhJgXi5E4b8Wg84ihbmUKp/GvSPEzc= +github.com/vinzenz/yaml v0.0.0-20170920082545-91409cdd725d h1:3wDi6J5APMqaHBVPuVd7RmHD2gRTfqbdcVSpCNoUWtk= github.com/vinzenz/yaml v0.0.0-20170920082545-91409cdd725d/go.mod h1:mb5taDqMnJiZNRQ3+02W2IFG+oEz1+dTuCXkp4jpkfo= +github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +github.com/weaveworks/common v0.0.0-20230511094633-334485600903 h1:ph7R2CS/0o1gBzpzK/CioUKJVsXNVXfDGR8FZ9rMZIw= github.com/weaveworks/common v0.0.0-20230511094633-334485600903/go.mod h1:rgbeLfJUtEr+G74cwFPR1k/4N0kDeaeSv/qhUNE4hm8= +github.com/weaveworks/promrus v1.2.0 h1:jOLf6pe6/vss4qGHjXmGz4oDJQA+AOCqEL3FvvZGz7M= github.com/weaveworks/promrus v1.2.0/go.mod h1:SaE82+OJ91yqjrE1rsvBWVzNZKcHYFtMUyS1+Ogs/KA= +github.com/willf/bitset v1.1.11 h1:N7Z7E9UvjW+sGsEl7k/SJrvY2reP1A07MrGuCjIOjRE= github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI= +github.com/willf/bloom v2.0.3+incompatible h1:QDacWdqcAUI1MPOwIQZRy9kOR7yxfyEmxX8Wdm2/JPA= github.com/willf/bloom v2.0.3+incompatible/go.mod h1:MmAltL9pDMNTrvUkxdg0k0q5I0suxmuwp3KbyrZLOZ8= +github.com/xanzy/go-gitlab v0.15.0 h1:rWtwKTgEnXyNUGrOArN7yyc3THRkpYcKXIXia9abywQ= +github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= +github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= +github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= +github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c h1:u40Z8hqBAAQyv+vATcGgV0YCnDjqSL7/q/JyPhhJSPk= +github.com/xdg/stringprep v1.0.0 h1:d9X0esnoa3dFsV0FG35rAT0RIhYFlPq7MiP+DW89La0= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xhit/go-str2duration v1.2.0 h1:BcV5u025cITWxEQKGWr1URRzrcXtu7uk8+luz3Yuhwc= +github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77 h1:ESFSdwYZvkeru3RtdrYueztKhOBCSAAzS4Gf+k0tEow= +github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA= +github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= +github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg= github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b h1:FosyBZYxY34Wul7O/MSKey3txpPYyCqVO5ZyceuQJEI= github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8= +github.com/zenazn/goji v1.0.1 h1:4lbD8Mx2h7IvloP7r2C0D6ltZP6Ufip8Hn0wmSK5LR8= github.com/zenazn/goji v1.0.1/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= +gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b h1:7gd+rd8P3bqcn/96gOZa3F5dpJr/vEiDQYlNb/y2uNs= go.opentelemetry.io/collector v0.74.0 h1:0s2DKWczGj/pLTsXGb1P+Je7dyuGx9Is4/Dri1+cS7g= go.opentelemetry.io/collector v0.74.0/go.mod h1:7NjZAvkhQ6E+NLN4EAH2hw3Nssi+F14t7mV7lMNXCto= +go.opentelemetry.io/collector/component v0.74.0 h1:W32ILPgbA5LO+m9Se61hbbtiLM6FYusNM36K5/CCOi0= go.opentelemetry.io/collector/component v0.74.0/go.mod h1:zHbWqbdmnHeIZAuO3s1Fo/kWPC2oKuolIhlPmL4bzyo= +go.opentelemetry.io/collector/confmap v0.74.0 h1:tl4fSHC/MXZiEvsZhDhd03TgzvArOe69Qn020sZsTfQ= go.opentelemetry.io/collector/confmap v0.74.0/go.mod h1:NvUhMS2v8rniLvDAnvGjYOt0qBohk6TIibb1NuyVB1Q= +go.opentelemetry.io/collector/consumer v0.74.0 h1:+kjT/ixG+4SVSHg7u9mQe0+LNDc6PuG8Wn2hoL/yGYk= go.opentelemetry.io/collector/consumer v0.74.0/go.mod h1:MuGqt8/OKVAOjrh5WHr1TR2qwHizy64ZP2uNSr+XpvI= +go.opentelemetry.io/collector/exporter v0.74.0 h1:VZxDuVz9kJM/Yten3xA/abJwLJNkxLThiao6E1ULW7c= go.opentelemetry.io/collector/exporter v0.74.0/go.mod h1:kw5YoorpKqEpZZ/a5ODSoYFK1mszzcKBNORd32S8Z7c= +go.opentelemetry.io/collector/exporter/otlpexporter v0.74.0 h1:YKvTeYcBrJwbcXNy65fJ/xytUSMurpYn/KkJD0x+DAY= go.opentelemetry.io/collector/exporter/otlpexporter v0.74.0/go.mod h1:cRbvsnpSxzySoTSnXbOGPQZu9KHlEyKkTeE21f9Q1p4= +go.opentelemetry.io/collector/featuregate v1.0.0 h1:5MGqe2v5zxaoo73BUOvUTunftX5J8RGrbFsC2Ha7N3g= +go.opentelemetry.io/collector/receiver v0.74.0 h1:jlgBFa0iByvn8VuX27UxtqiPiZE8ejmU5lb1nSptWD8= go.opentelemetry.io/collector/receiver v0.74.0/go.mod h1:SQkyATvoZCJefNkI2jnrR63SOdrmDLYCnQqXJ7ACqn0= +go.opentelemetry.io/collector/receiver/otlpreceiver v0.74.0 h1:e/X/W0z2Jtpy3Yd3CXkmEm9vSpKq/P3pKUrEVMUFBRw= go.opentelemetry.io/collector/receiver/otlpreceiver v0.74.0/go.mod h1:9X9/RYFxJIaK0JLlRZ0PpmQSSlYpY+r4KsTOj2jWj14= +go.opentelemetry.io/collector/semconv v0.90.1 h1:2fkQZbefQBbIcNb9Rk1mRcWlFZgQOk7CpST1e1BK8eg= go.opentelemetry.io/contrib v0.18.0 h1:uqBh0brileIvG6luvBjdxzoFL8lxDGuhxJWsvK3BveI= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.42.0/go.mod h1:5z+/ZWJQKXa9YT34fQNx5K8Hd1EoIhvtUygUQPqEOgQ= +go.opentelemetry.io/contrib/propagators/b3 v1.15.0 h1:bMaonPyFcAvZ4EVzkUNkfnUHP5Zi63CIDlA3dRsEg8Q= go.opentelemetry.io/contrib/propagators/b3 v1.15.0/go.mod h1:VjU0g2v6HSQ+NwfifambSLAeBgevjIcqmceaKWEzl0c= +go.opentelemetry.io/otel/bridge/opencensus v0.37.0 h1:ieH3gw7b1eg90ARsFAlAsX5LKVZgnCYfaDwRrK6xLHU= go.opentelemetry.io/otel/bridge/opencensus v0.37.0/go.mod h1:ddiK+1PE68l/Xk04BGTh9Y6WIcxcLrmcVxVlS0w5WZ0= +go.opentelemetry.io/otel/bridge/opentracing v1.10.0 h1:WzAVGovpC1s7KD5g4taU6BWYZP3QGSDVTlbRu9fIHw8= go.opentelemetry.io/otel/bridge/opentracing v1.10.0/go.mod h1:J7GLR/uxxqMAzZptsH0pjte3Ep4GacTCrbGBoDuHBqk= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0/go.mod h1:0+KuTDyKL4gjKCF75pHOX4wuzYDUZYfAQdSu43o+Z2I= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0 h1:digkEZCJWobwBqMwC0cwCq8/wkkRy/OowZg5OArWZrM= +go.opentelemetry.io/otel/exporters/prometheus v0.37.0 h1:NQc0epfL0xItsmGgSXgfbH2C1fq2VLXkZoDFsfRNHpc= go.opentelemetry.io/otel/exporters/prometheus v0.37.0/go.mod h1:hB8qWjsStK36t50/R0V2ULFb4u95X/Q6zupXLgvjTh8= go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A= +go.opentelemetry.io/otel/sdk/metric v0.39.0 h1:Kun8i1eYf48kHH83RucG93ffz0zGV1sh46FAScOTuDI= go.opentelemetry.io/otel/sdk/metric v0.39.0/go.mod h1:piDIRgjcK7u0HCL5pCA4e74qpK/jk3NiUoAHATVAmiI= go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8= go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= +go.uber.org/mock v0.2.0 h1:TaP3xedm7JaAgScZO7tlvlKrqT0p7I6OsdGB5YNSMDU= go.uber.org/mock v0.2.0/go.mod h1:J0y0rp9L3xiff1+ZBfKxlC1fz2+aO16tw0tsDOixfuM= go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA= +golang.org/x/image v0.0.0-20220302094943-723b81ca9867 h1:TcHcE0vrmgzNH1v3ppjcMGbhG5+9fMuvOmUYwNEF4q4= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028 h1:4+4C/Iv2U4fMZBiMCc98MG1In4gJY5YRhtpDNeDeHWs= golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20211123203042-d83791d6bcd9/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/tools v0.12.0/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM= golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= +gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0 h1:OE9mWmgKkjJyEmDAAtGMPjXu+YNeGvK9VTSHY6+Qihc= +gonum.org/v1/plot v0.10.1 h1:dnifSs43YJuNMDzB7v8wV64O4ABBHReuAVAoBxqBqS4= google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3/go.mod h1:5RBcpGRxr25RbDzY5w+dmaqpSEvl8Gwl1x2CICf60ic= google.golang.org/genproto/googleapis/api v0.0.0-20231211222908-989df2bf70f3/go.mod h1:k2dtGpRrbsSyKcNPKKI5sstZkrNCZwpU/ns96JoHbGg= +google.golang.org/genproto/googleapis/bytestream v0.0.0-20231120223509-83a465c0220f h1:hL+1ptbhFoeL1HcROQ8OGXaqH0jYRRibgWQWco0/Ugc= google.golang.org/genproto/googleapis/rpc v0.0.0-20231211222908-989df2bf70f3/go.mod h1:eJVxU6o+4G1PSczBr85xmyvSNYAKvAYgkub40YGomFM= google.golang.org/grpc v1.60.0/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0 h1:M1YKkFIboKNieVO5DLUEVzQfGwJD30Nv2jfUgzb5UcE= +gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= +gopkg.in/cheggaaa/pb.v1 v1.0.25 h1:Ev7yu1/f6+d+b3pi5vPdRPc6nNtP1umSfcWiEfRqv6I= +gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= +gopkg.in/gcfg.v1 v1.2.3 h1:m8OOJ4ccYHnx2f4gQwpno8nAX5OGOh7RLaaz0pj3Ogs= +gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= +gopkg.in/go-playground/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ= +gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec h1:RlWgLqCMMIYYEVcAR5MDsuHlVkaIPDAF+5Dehzg8L5A= +gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce h1:xcEWjVhvbDy+nHP67nPDDpbYrY+ILlfndk4bRioVHaU= +gopkg.in/resty.v1 v1.12.0 h1:CuXP0Pjfw9rOuY6EP+UvtNvt5DSqHpIxILZKT/quCZI= +gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= +gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg= gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98= +gopkg.in/telebot.v3 v3.2.1 h1:3I4LohaAyJBiivGmkfB+CiVu7QFOWkuZ4+KHgO/G3rs= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +honnef.co/go/tools v0.1.3 h1:qTakTkI6ni6LFD5sBwwsdSO+AQqbSIxOauHTTQKZ/7o= k8s.io/api v0.29.0/go.mod h1:sdVmXoz2Bo/cb77Pxi71IPTSErEW32xa4aXwKH7gfBA= +k8s.io/apiextensions-apiserver v0.26.2 h1:/yTG2B9jGY2Q70iGskMf41qTLhL9XeNN2KhI0uDgwko= k8s.io/apiextensions-apiserver v0.26.2/go.mod h1:Y7UPgch8nph8mGCuVk0SK83LnS8Esf3n6fUBgew8SH8= k8s.io/apimachinery v0.29.0/go.mod h1:eVBxQ/cwiJxH58eK/jd/vAk4mrxmVlnpBH5J2GbMeis= k8s.io/apiserver v0.29.0/go.mod h1:31n78PsRKPmfpee7/l9NYEv67u6hOL6AfcE761HapDM= k8s.io/client-go v0.29.0/go.mod h1:yLkXH4HKMAywcrD82KMSmfYg2DlE8mepPR4JGSo5n38= k8s.io/component-base v0.29.0/go.mod h1:sADonFTQ9Zc9yFLghpDpmNXEdHyQmFIGbiuZbqAXQ1M= +k8s.io/gengo v0.0.0-20230829151522-9cce18d56c01 h1:pWEwq4Asjm4vjW7vcsmijwBhOr1/shsbSYiWXmNGlks= k8s.io/gengo v0.0.0-20230829151522-9cce18d56c01/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= k8s.io/kms v0.29.0/go.mod h1:mB0f9HLxRXeXUfHfn1A7rpwOlzXI1gIWu86z6buNoYA= -k8s.io/kms v0.29.2/go.mod h1:s/9RC4sYRZ/6Tn6yhNjbfJuZdb8LzlXhdlBnKizeFDo= k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA= k8s.io/kube-openapi v0.0.0-20231214164306-ab13479f8bf8/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA= +nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g= +rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE= +rsc.io/pdf v0.1.1 h1:k1MczvYDUvJBe93bYd7wrZLLUEcLZAuF824/I4e5Xr4= +rsc.io/quote/v3 v3.1.0 h1:9JKUTTIUgS6kzR9mK1YuGKv6Nl+DijDNIc0ghT58FaY= +rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4= +sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0 h1:ucqkfpjg9WzSUubAO62csmucvxl4/JeW3F4I4909XkM= diff --git a/pkg/api/frontendsettings_test.go b/pkg/api/frontendsettings_test.go index faaf564c4d..d0fbcb0c14 100644 --- a/pkg/api/frontendsettings_test.go +++ b/pkg/api/frontendsettings_test.go @@ -71,7 +71,7 @@ func setupTestEnvironment(t *testing.T, cfg *setting.Cfg, features featuremgmt.F grafanaUpdateChecker: &updatechecker.GrafanaService{}, AccessControl: accesscontrolmock.New(), PluginSettings: pluginsSettings, - pluginsCDNService: pluginscdn.ProvideService(&config.Cfg{ + pluginsCDNService: pluginscdn.ProvideService(&config.PluginManagementCfg{ PluginsCDNURLTemplate: cfg.PluginsCDNURLTemplate, PluginSettings: cfg.PluginSettings, }), diff --git a/pkg/api/metrics_test.go b/pkg/api/metrics_test.go index 370596435c..992d14305a 100644 --- a/pkg/api/metrics_test.go +++ b/pkg/api/metrics_test.go @@ -20,11 +20,11 @@ import ( "github.com/grafana/grafana/pkg/plugins/backendplugin" "github.com/grafana/grafana/pkg/plugins/config" pluginClient "github.com/grafana/grafana/pkg/plugins/manager/client" - pluginFakes "github.com/grafana/grafana/pkg/plugins/manager/fakes" "github.com/grafana/grafana/pkg/plugins/manager/registry" "github.com/grafana/grafana/pkg/services/datasources" fakeDatasources "github.com/grafana/grafana/pkg/services/datasources/fakes" "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginconfig" "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" @@ -78,7 +78,7 @@ func TestAPIEndpoint_Metrics_QueryMetricsV2(t *testing.T) { }, }, }, &fakeDatasources.FakeCacheService{}, &fakeDatasources.FakeDataSourceService{}, - pluginSettings.ProvideService(dbtest.NewFakeDB(), secretstest.NewFakeSecretsService()), pluginFakes.NewFakeLicensingService(), &config.Cfg{}), + pluginSettings.ProvideService(dbtest.NewFakeDB(), secretstest.NewFakeSecretsService()), pluginconfig.NewFakePluginRequestConfigProvider()), ) serverFeatureEnabled := SetupAPITestServer(t, func(hs *HTTPServer) { hs.queryDataService = qds @@ -125,7 +125,7 @@ func TestAPIEndpoint_Metrics_PluginDecryptionFailure(t *testing.T) { }, }, &fakeDatasources.FakeCacheService{}, - ds, pluginSettings.ProvideService(db, secretstest.NewFakeSecretsService()), pluginFakes.NewFakeLicensingService(), &config.Cfg{}, + ds, pluginSettings.ProvideService(db, secretstest.NewFakeSecretsService()), pluginconfig.NewFakePluginRequestConfigProvider(), ) qds := query.ProvideService( cfg, @@ -297,13 +297,13 @@ func TestDataSourceQueryError(t *testing.T) { &fakeDatasources.FakeCacheService{}, nil, &fakePluginRequestValidator{}, - pluginClient.ProvideService(r, &config.Cfg{}), + pluginClient.ProvideService(r, &config.PluginManagementCfg{}), plugincontext.ProvideService(cfg, localcache.ProvideService(), &pluginstore.FakePluginStore{ PluginList: []pluginstore.Plugin{pluginstore.ToGrafanaDTO(p)}, }, &fakeDatasources.FakeCacheService{}, ds, pluginSettings.ProvideService(dbtest.NewFakeDB(), - secretstest.NewFakeSecretsService()), pluginFakes.NewFakeLicensingService(), &config.Cfg{}), + secretstest.NewFakeSecretsService()), pluginconfig.NewFakePluginRequestConfigProvider()), ) hs.QuotaService = quotatest.New(false, nil) }) diff --git a/pkg/api/plugin_resource_test.go b/pkg/api/plugin_resource_test.go index 9fb4532019..e2ea99902e 100644 --- a/pkg/api/plugin_resource_test.go +++ b/pkg/api/plugin_resource_test.go @@ -21,7 +21,6 @@ import ( "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/backendplugin/coreplugin" - "github.com/grafana/grafana/pkg/plugins/config" pluginClient "github.com/grafana/grafana/pkg/plugins/manager/client" "github.com/grafana/grafana/pkg/plugins/manager/fakes" "github.com/grafana/grafana/pkg/services/accesscontrol" @@ -31,6 +30,7 @@ import ( "github.com/grafana/grafana/pkg/services/oauthtoken/oauthtokentest" "github.com/grafana/grafana/pkg/services/pluginsintegration" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginaccesscontrol" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginconfig" "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" @@ -49,7 +49,6 @@ func TestCallResource(t *testing.T) { cfg := setting.NewCfg() cfg.StaticRootPath = staticRootPath cfg.Azure = &azsettings.AzureSettings{} - pCfg := config.Cfg{} coreRegistry := coreplugin.ProvideCoreRegistry(tracing.InitializeTracerForTest(), nil, &cloudwatch.CloudWatchService{}, nil, nil, nil, nil, nil, nil, nil, nil, testdatasource.ProvideService(), nil, nil, nil, nil, nil, nil) @@ -57,7 +56,7 @@ func TestCallResource(t *testing.T) { testCtx := pluginsintegration.CreateIntegrationTestCtx(t, cfg, coreRegistry) pcp := plugincontext.ProvideService(cfg, localcache.ProvideService(), testCtx.PluginStore, &datasources.FakeCacheService{}, - &datasources.FakeDataSourceService{}, pluginSettings.ProvideService(db.InitTestDB(t), fakeSecrets.NewFakeSecretsService()), nil, &pCfg) + &datasources.FakeDataSourceService{}, pluginSettings.ProvideService(db.InitTestDB(t), fakeSecrets.NewFakeSecretsService()), pluginconfig.NewFakePluginRequestConfigProvider()) srv := SetupAPITestServer(t, func(hs *HTTPServer) { hs.Cfg = cfg diff --git a/pkg/api/plugins_test.go b/pkg/api/plugins_test.go index 96fb1078a1..4d6841bc28 100644 --- a/pkg/api/plugins_test.go +++ b/pkg/api/plugins_test.go @@ -513,7 +513,7 @@ func pluginAssetScenario(t *testing.T, desc string, url string, urlPattern strin pluginStore: pluginstore.New(pluginRegistry, &fakes.FakeLoader{}), pluginFileStore: filestore.ProvideService(pluginRegistry), log: log.NewNopLogger(), - pluginsCDNService: pluginscdn.ProvideService(&config.Cfg{ + pluginsCDNService: pluginscdn.ProvideService(&config.PluginManagementCfg{ PluginsCDNURLTemplate: cfg.PluginsCDNURLTemplate, PluginSettings: cfg.PluginSettings, }), diff --git a/pkg/expr/dataplane_test.go b/pkg/expr/dataplane_test.go index c302064c1f..186f0add4e 100644 --- a/pkg/expr/dataplane_test.go +++ b/pkg/expr/dataplane_test.go @@ -14,11 +14,10 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/plugins" - "github.com/grafana/grafana/pkg/plugins/config" - pluginFakes "github.com/grafana/grafana/pkg/plugins/manager/fakes" "github.com/grafana/grafana/pkg/services/datasources" datafakes "github.com/grafana/grafana/pkg/services/datasources/fakes" "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginconfig" "github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" "github.com/grafana/grafana/pkg/services/user" @@ -62,7 +61,7 @@ func framesPassThroughService(t *testing.T, frames data.Frames) (data.Frames, er {JSONData: plugins.JSONData{ID: "test"}}, }}, &datafakes.FakeCacheService{}, &datafakes.FakeDataSourceService{}, - nil, pluginFakes.NewFakeLicensingService(), &config.Cfg{}), + nil, pluginconfig.NewFakePluginRequestConfigProvider()), tracer: tracing.InitializeTracerForTest(), metrics: newMetrics(nil), } diff --git a/pkg/expr/service_test.go b/pkg/expr/service_test.go index 7983be6951..3492b1121d 100644 --- a/pkg/expr/service_test.go +++ b/pkg/expr/service_test.go @@ -15,11 +15,10 @@ import ( "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/plugins" - "github.com/grafana/grafana/pkg/plugins/config" - "github.com/grafana/grafana/pkg/plugins/manager/fakes" "github.com/grafana/grafana/pkg/services/datasources" datafakes "github.com/grafana/grafana/pkg/services/datasources/fakes" "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginconfig" "github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" "github.com/grafana/grafana/pkg/services/user" @@ -42,7 +41,7 @@ func TestService(t *testing.T) { PluginList: []pluginstore.Plugin{ {JSONData: plugins.JSONData{ID: "test"}}, }, - }, &datafakes.FakeCacheService{}, &datafakes.FakeDataSourceService{}, nil, fakes.NewFakeLicensingService(), &config.Cfg{}) + }, &datafakes.FakeCacheService{}, &datafakes.FakeDataSourceService{}, nil, pluginconfig.NewFakePluginRequestConfigProvider()) s := Service{ cfg: setting.NewCfg(), @@ -128,7 +127,7 @@ func TestDSQueryError(t *testing.T) { PluginList: []pluginstore.Plugin{ {JSONData: plugins.JSONData{ID: "test"}}, }, - }, &datafakes.FakeCacheService{}, &datafakes.FakeDataSourceService{}, nil, nil, &config.Cfg{}) + }, &datafakes.FakeCacheService{}, &datafakes.FakeDataSourceService{}, nil, pluginconfig.NewFakePluginRequestConfigProvider()) s := Service{ cfg: setting.NewCfg(), diff --git a/pkg/plugins/config/config.go b/pkg/plugins/config/config.go index 3c5d8908ad..68019f3276 100644 --- a/pkg/plugins/config/config.go +++ b/pkg/plugins/config/config.go @@ -1,16 +1,13 @@ package config import ( - "github.com/grafana/grafana-azure-sdk-go/azsettings" - - "github.com/grafana/grafana/pkg/plugins/log" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/setting" ) -type Cfg struct { - log log.Logger - +// PluginManagementCfg is the configuration for the plugin management system. +// It includes settings which are used to configure different components of plugin management. +type PluginManagementCfg struct { DevMode bool PluginsPath string @@ -20,25 +17,6 @@ type Cfg struct { DisablePlugins []string ForwardHostEnvVars []string - // AWS Plugin Auth - AWSAllowedAuthProviders []string - AWSAssumeRoleEnabled bool - AWSExternalId string - AWSSessionDuration string - AWSListMetricsPageLimit string - AWSForwardSettingsPlugins []string - - // Azure Cloud settings - Azure *azsettings.AzureSettings - AzureAuthEnabled bool - - // Proxy Settings - ProxySettings setting.SecureSocksDSProxySettings - - BuildVersion string // TODO Remove - - LogDatasourceRequests bool - PluginsCDNURLTemplate string Tracing Tracing @@ -52,57 +30,27 @@ type Cfg struct { AngularSupportEnabled bool HideAngularDeprecation []string - - ConcurrentQueryCount int - - UserFacingDefaultError string - - DataProxyRowLimit int64 - - SQLDatasourceMaxOpenConnsDefault int - SQLDatasourceMaxIdleConnsDefault int - SQLDatasourceMaxConnLifetimeDefault int } -func NewCfg(devMode bool, pluginsPath string, pluginSettings setting.PluginSettings, pluginsAllowUnsigned []string, - awsAllowedAuthProviders []string, awsAssumeRoleEnabled bool, awsExternalId string, awsSessionDuration string, awsListMetricsPageLimit string, AWSForwardSettingsPlugins []string, azure *azsettings.AzureSettings, secureSocksDSProxy setting.SecureSocksDSProxySettings, - grafanaVersion string, logDatasourceRequests bool, pluginsCDNURLTemplate string, appURL string, appSubURL string, tracing Tracing, features featuremgmt.FeatureToggles, angularSupportEnabled bool, - grafanaComURL string, disablePlugins []string, hideAngularDeprecation []string, forwardHostEnvVars []string, concurrentQueryCount int, azureAuthEnabled bool, - userFacingDefaultError string, dataProxyRowLimit int64, - sqlDatasourceMaxOpenConnsDefault int, sqlDatasourceMaxIdleConnsDefault int, sqlDatasourceMaxConnLifetimeDefault int, -) *Cfg { - return &Cfg{ - log: log.New("plugin.cfg"), - PluginsPath: pluginsPath, - BuildVersion: grafanaVersion, - DevMode: devMode, - PluginSettings: pluginSettings, - PluginsAllowUnsigned: pluginsAllowUnsigned, - DisablePlugins: disablePlugins, - AWSAllowedAuthProviders: awsAllowedAuthProviders, - AWSAssumeRoleEnabled: awsAssumeRoleEnabled, - AWSExternalId: awsExternalId, - AWSSessionDuration: awsSessionDuration, - AWSListMetricsPageLimit: awsListMetricsPageLimit, - AWSForwardSettingsPlugins: AWSForwardSettingsPlugins, - Azure: azure, - ProxySettings: secureSocksDSProxy, - LogDatasourceRequests: logDatasourceRequests, - PluginsCDNURLTemplate: pluginsCDNURLTemplate, - Tracing: tracing, - GrafanaComURL: grafanaComURL, - GrafanaAppURL: appURL, - GrafanaAppSubURL: appSubURL, - Features: features, - AngularSupportEnabled: angularSupportEnabled, - HideAngularDeprecation: hideAngularDeprecation, - ForwardHostEnvVars: forwardHostEnvVars, - ConcurrentQueryCount: concurrentQueryCount, - AzureAuthEnabled: azureAuthEnabled, - UserFacingDefaultError: userFacingDefaultError, - DataProxyRowLimit: dataProxyRowLimit, - SQLDatasourceMaxOpenConnsDefault: sqlDatasourceMaxOpenConnsDefault, - SQLDatasourceMaxIdleConnsDefault: sqlDatasourceMaxIdleConnsDefault, - SQLDatasourceMaxConnLifetimeDefault: sqlDatasourceMaxConnLifetimeDefault, +// NewPluginManagementCfg returns a new PluginManagementCfg. +func NewPluginManagementCfg(devMode bool, pluginsPath string, pluginSettings setting.PluginSettings, pluginsAllowUnsigned []string, + pluginsCDNURLTemplate string, appURL string, appSubURL string, tracing Tracing, features featuremgmt.FeatureToggles, + angularSupportEnabled bool, grafanaComURL string, disablePlugins []string, hideAngularDeprecation []string, + forwardHostEnvVars []string) *PluginManagementCfg { + return &PluginManagementCfg{ + PluginsPath: pluginsPath, + DevMode: devMode, + PluginSettings: pluginSettings, + PluginsAllowUnsigned: pluginsAllowUnsigned, + DisablePlugins: disablePlugins, + PluginsCDNURLTemplate: pluginsCDNURLTemplate, + Tracing: tracing, + GrafanaComURL: grafanaComURL, + GrafanaAppURL: appURL, + GrafanaAppSubURL: appSubURL, + Features: features, + AngularSupportEnabled: angularSupportEnabled, + HideAngularDeprecation: hideAngularDeprecation, + ForwardHostEnvVars: forwardHostEnvVars, } } diff --git a/pkg/plugins/envvars/envvars.go b/pkg/plugins/envvars/envvars.go index 9913b1a9b7..442a350d51 100644 --- a/pkg/plugins/envvars/envvars.go +++ b/pkg/plugins/envvars/envvars.go @@ -2,32 +2,14 @@ package envvars import ( "context" - "fmt" "os" - "slices" - "sort" - "strconv" - "strings" - - "github.com/grafana/grafana-aws-sdk/pkg/awsds" - "github.com/grafana/grafana-azure-sdk-go/azsettings" - "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana-plugin-sdk-go/backend/proxy" - "github.com/grafana/grafana-plugin-sdk-go/experimental/featuretoggles" "github.com/grafana/grafana/pkg/plugins" - "github.com/grafana/grafana/pkg/plugins/auth" - "github.com/grafana/grafana/pkg/plugins/config" - "github.com/grafana/grafana/pkg/services/featuremgmt" -) - -const ( - customConfigPrefix = "GF_PLUGIN" ) -// allowedHostEnvVarNames is the list of environment variables that can be passed from Grafana's process to the +// permittedHostEnvVarNames is the list of environment variables that can be passed from Grafana's process to the // plugin's process -var allowedHostEnvVarNames = []string{ +var permittedHostEnvVarNames = []string{ // Env vars used by net/http (Go stdlib) for http/https proxy // https://github.com/golang/net/blob/fbaf41277f28102c36926d1368dafbe2b54b4c1d/http/httpproxy/proxy.go#L91-L93 "HTTP_PROXY", @@ -39,286 +21,25 @@ var allowedHostEnvVarNames = []string{ } type Provider interface { - Get(ctx context.Context, p *plugins.Plugin) []string + PluginEnvVars(ctx context.Context, p *plugins.Plugin) []string } -type Service struct { - cfg *config.Cfg - license plugins.Licensing -} +type Service struct{} -func NewProvider(cfg *config.Cfg, license plugins.Licensing) *Service { - return &Service{ - cfg: cfg, - license: license, - } +func DefaultProvider() *Service { + return &Service{} } -func (s *Service) Get(ctx context.Context, p *plugins.Plugin) []string { - hostEnv := []string{ - fmt.Sprintf("GF_VERSION=%s", s.cfg.BuildVersion), - } - - if s.license != nil { - hostEnv = append( - hostEnv, - fmt.Sprintf("GF_EDITION=%s", s.license.Edition()), - fmt.Sprintf("GF_ENTERPRISE_LICENSE_PATH=%s", s.license.Path()), - fmt.Sprintf("GF_ENTERPRISE_APP_URL=%s", s.license.AppURL()), - ) - hostEnv = append(hostEnv, s.license.Environment()...) - } - - if p.ExternalService != nil { - hostEnv = append( - hostEnv, - fmt.Sprintf("GF_APP_URL=%s", s.cfg.GrafanaAppURL), - fmt.Sprintf("GF_PLUGIN_APP_CLIENT_ID=%s", p.ExternalService.ClientID), - fmt.Sprintf("GF_PLUGIN_APP_CLIENT_SECRET=%s", p.ExternalService.ClientSecret), - ) - if p.ExternalService.PrivateKey != "" { - hostEnv = append(hostEnv, fmt.Sprintf("GF_PLUGIN_APP_PRIVATE_KEY=%s", p.ExternalService.PrivateKey)) - } - } - - hostEnv = append(hostEnv, s.featureToggleEnableVar(ctx)...) - hostEnv = append(hostEnv, s.awsEnvVars()...) - hostEnv = append(hostEnv, s.secureSocksProxyEnvVars()...) - hostEnv = append(hostEnv, azsettings.WriteToEnvStr(s.cfg.Azure)...) - hostEnv = append(hostEnv, s.tracingEnvVars(p)...) - - // If SkipHostEnvVars is enabled, get some allowed variables from the current process and pass - // them down to the plugin. If the flag is not set, do not add anything else because ALL env vars - // from the current process (os.Environ()) will be forwarded to the plugin's process by go-plugin - if p.SkipHostEnvVars { - hostEnv = append(hostEnv, s.allowedHostEnvVars()...) - } - - ev := getPluginSettings(p.ID, s.cfg).asEnvVar(customConfigPrefix, hostEnv...) - - return ev +func (s *Service) PluginEnvVars(_ context.Context, _ *plugins.Plugin) []string { + return PermittedHostEnvVars() } -// GetConfigMap returns a map of configuration that should be passed in a plugin request. -// -//nolint:gocyclo -func (s *Service) GetConfigMap(ctx context.Context, pluginID string, _ *auth.ExternalService) map[string]string { - m := make(map[string]string) - - if s.cfg.GrafanaAppURL != "" { - m[backend.AppURL] = s.cfg.GrafanaAppURL - } - if s.cfg.ConcurrentQueryCount != 0 { - m[backend.ConcurrentQueryCount] = strconv.Itoa(s.cfg.ConcurrentQueryCount) - } - - if s.cfg.UserFacingDefaultError != "" { - m[backend.UserFacingDefaultError] = s.cfg.UserFacingDefaultError - } - - if s.cfg.DataProxyRowLimit != 0 { - m[backend.SQLRowLimit] = strconv.FormatInt(s.cfg.DataProxyRowLimit, 10) - } - - m[backend.SQLMaxOpenConnsDefault] = strconv.Itoa(s.cfg.SQLDatasourceMaxOpenConnsDefault) - m[backend.SQLMaxIdleConnsDefault] = strconv.Itoa(s.cfg.SQLDatasourceMaxIdleConnsDefault) - m[backend.SQLMaxConnLifetimeSecondsDefault] = strconv.Itoa(s.cfg.SQLDatasourceMaxConnLifetimeDefault) - - // TODO add support via plugin SDK - // if externalService != nil { - // m[oauthtokenretriever.AppURL] = s.cfg.GrafanaAppURL - // m[oauthtokenretriever.AppClientID] = externalService.ClientID - // m[oauthtokenretriever.AppClientSecret] = externalService.ClientSecret - // m[oauthtokenretriever.AppPrivateKey] = externalService.PrivateKey - // } - - if s.cfg.Features != nil { - enabledFeatures := s.cfg.Features.GetEnabled(ctx) - if len(enabledFeatures) > 0 { - features := make([]string, 0, len(enabledFeatures)) - for feat := range enabledFeatures { - features = append(features, feat) - } - sort.Strings(features) - m[featuretoggles.EnabledFeatures] = strings.Join(features, ",") - } - } - - if slices.Contains[[]string, string](s.cfg.AWSForwardSettingsPlugins, pluginID) { - if !s.cfg.AWSAssumeRoleEnabled { - m[awsds.AssumeRoleEnabledEnvVarKeyName] = "false" - } - if len(s.cfg.AWSAllowedAuthProviders) > 0 { - m[awsds.AllowedAuthProvidersEnvVarKeyName] = strings.Join(s.cfg.AWSAllowedAuthProviders, ",") - } - if s.cfg.AWSExternalId != "" { - m[awsds.GrafanaAssumeRoleExternalIdKeyName] = s.cfg.AWSExternalId - } - if s.cfg.AWSSessionDuration != "" { - m[awsds.SessionDurationEnvVarKeyName] = s.cfg.AWSSessionDuration - } - if s.cfg.AWSListMetricsPageLimit != "" { - m[awsds.ListMetricsPageLimitKeyName] = s.cfg.AWSListMetricsPageLimit - } - } - - if s.cfg.ProxySettings.Enabled { - m[proxy.PluginSecureSocksProxyEnabled] = "true" - m[proxy.PluginSecureSocksProxyClientCert] = s.cfg.ProxySettings.ClientCert - m[proxy.PluginSecureSocksProxyClientKey] = s.cfg.ProxySettings.ClientKey - m[proxy.PluginSecureSocksProxyRootCACert] = s.cfg.ProxySettings.RootCA - m[proxy.PluginSecureSocksProxyProxyAddress] = s.cfg.ProxySettings.ProxyAddress - m[proxy.PluginSecureSocksProxyServerName] = s.cfg.ProxySettings.ServerName - m[proxy.PluginSecureSocksProxyAllowInsecure] = strconv.FormatBool(s.cfg.ProxySettings.AllowInsecure) - } - - // Settings here will be extracted by grafana-azure-sdk-go from the plugin context - if s.cfg.AzureAuthEnabled { - m[azsettings.AzureAuthEnabled] = strconv.FormatBool(s.cfg.AzureAuthEnabled) - } - azureSettings := s.cfg.Azure - if azureSettings != nil && slices.Contains[[]string, string](azureSettings.ForwardSettingsPlugins, pluginID) { - if azureSettings.Cloud != "" { - m[azsettings.AzureCloud] = azureSettings.Cloud - } - - if azureSettings.ManagedIdentityEnabled { - m[azsettings.ManagedIdentityEnabled] = "true" - - if azureSettings.ManagedIdentityClientId != "" { - m[azsettings.ManagedIdentityClientID] = azureSettings.ManagedIdentityClientId - } - } - - if azureSettings.UserIdentityEnabled { - m[azsettings.UserIdentityEnabled] = "true" - - if azureSettings.UserIdentityTokenEndpoint != nil { - if azureSettings.UserIdentityTokenEndpoint.TokenUrl != "" { - m[azsettings.UserIdentityTokenURL] = azureSettings.UserIdentityTokenEndpoint.TokenUrl - } - if azureSettings.UserIdentityTokenEndpoint.ClientId != "" { - m[azsettings.UserIdentityClientID] = azureSettings.UserIdentityTokenEndpoint.ClientId - } - if azureSettings.UserIdentityTokenEndpoint.ClientSecret != "" { - m[azsettings.UserIdentityClientSecret] = azureSettings.UserIdentityTokenEndpoint.ClientSecret - } - if azureSettings.UserIdentityTokenEndpoint.UsernameAssertion { - m[azsettings.UserIdentityAssertion] = "username" - } - } - } - - if azureSettings.WorkloadIdentityEnabled { - m[azsettings.WorkloadIdentityEnabled] = "true" - - if azureSettings.WorkloadIdentitySettings != nil { - if azureSettings.WorkloadIdentitySettings.ClientId != "" { - m[azsettings.WorkloadIdentityClientID] = azureSettings.WorkloadIdentitySettings.ClientId - } - if azureSettings.WorkloadIdentitySettings.TenantId != "" { - m[azsettings.WorkloadIdentityTenantID] = azureSettings.WorkloadIdentitySettings.TenantId - } - if azureSettings.WorkloadIdentitySettings.TokenFile != "" { - m[azsettings.WorkloadIdentityTokenFile] = azureSettings.WorkloadIdentitySettings.TokenFile - } - } - } - } - - // TODO add support via plugin SDK - // ps := getPluginSettings(pluginID, s.cfg) - // for k, v := range ps { - // m[fmt.Sprintf("%s_%s", customConfigPrefix, strings.ToUpper(k))] = v - // } - - return m -} - -func (s *Service) tracingEnvVars(plugin *plugins.Plugin) []string { - pluginTracingEnabled := s.cfg.Features != nil && s.cfg.Features.IsEnabledGlobally(featuremgmt.FlagEnablePluginsTracingByDefault) - if v, exists := s.cfg.PluginSettings[plugin.ID]["tracing"]; exists && !pluginTracingEnabled { - pluginTracingEnabled = v == "true" - } - if !s.cfg.Tracing.IsEnabled() || !pluginTracingEnabled { - return nil - } - - vars := []string{ - fmt.Sprintf("GF_INSTANCE_OTLP_ADDRESS=%s", s.cfg.Tracing.OpenTelemetry.Address), - fmt.Sprintf("GF_INSTANCE_OTLP_PROPAGATION=%s", s.cfg.Tracing.OpenTelemetry.Propagation), - - fmt.Sprintf("GF_INSTANCE_OTLP_SAMPLER_TYPE=%s", s.cfg.Tracing.OpenTelemetry.Sampler), - fmt.Sprintf("GF_INSTANCE_OTLP_SAMPLER_PARAM=%.6f", s.cfg.Tracing.OpenTelemetry.SamplerParam), - fmt.Sprintf("GF_INSTANCE_OTLP_SAMPLER_REMOTE_URL=%s", s.cfg.Tracing.OpenTelemetry.SamplerRemoteURL), - } - if plugin.Info.Version != "" { - vars = append(vars, fmt.Sprintf("GF_PLUGIN_VERSION=%s", plugin.Info.Version)) - } - return vars -} - -func (s *Service) featureToggleEnableVar(ctx context.Context) []string { - var variables []string // an array is used for consistency and keep the logic simpler for no features case - - if s.cfg.Features == nil { - return variables - } - - enabledFeatures := s.cfg.Features.GetEnabled(ctx) - if len(enabledFeatures) > 0 { - features := make([]string, 0, len(enabledFeatures)) - for feat := range enabledFeatures { - features = append(features, feat) - } - variables = append(variables, fmt.Sprintf("GF_INSTANCE_FEATURE_TOGGLES_ENABLE=%s", strings.Join(features, ","))) - } - - return variables -} - -func (s *Service) awsEnvVars() []string { - var variables []string - if !s.cfg.AWSAssumeRoleEnabled { - variables = append(variables, awsds.AssumeRoleEnabledEnvVarKeyName+"=false") - } - if len(s.cfg.AWSAllowedAuthProviders) > 0 { - variables = append(variables, awsds.AllowedAuthProvidersEnvVarKeyName+"="+strings.Join(s.cfg.AWSAllowedAuthProviders, ",")) - } - if s.cfg.AWSExternalId != "" { - variables = append(variables, awsds.GrafanaAssumeRoleExternalIdKeyName+"="+s.cfg.AWSExternalId) - } - if s.cfg.AWSSessionDuration != "" { - variables = append(variables, awsds.SessionDurationEnvVarKeyName+"="+s.cfg.AWSSessionDuration) - } - if s.cfg.AWSListMetricsPageLimit != "" { - variables = append(variables, awsds.ListMetricsPageLimitKeyName+"="+s.cfg.AWSListMetricsPageLimit) - } - - return variables -} - -func (s *Service) secureSocksProxyEnvVars() []string { - if s.cfg.ProxySettings.Enabled { - return []string{ - proxy.PluginSecureSocksProxyClientCert + "=" + s.cfg.ProxySettings.ClientCert, - proxy.PluginSecureSocksProxyClientKey + "=" + s.cfg.ProxySettings.ClientKey, - proxy.PluginSecureSocksProxyRootCACert + "=" + s.cfg.ProxySettings.RootCA, - proxy.PluginSecureSocksProxyProxyAddress + "=" + s.cfg.ProxySettings.ProxyAddress, - proxy.PluginSecureSocksProxyServerName + "=" + s.cfg.ProxySettings.ServerName, - proxy.PluginSecureSocksProxyEnabled + "=" + strconv.FormatBool(s.cfg.ProxySettings.Enabled), - proxy.PluginSecureSocksProxyAllowInsecure + "=" + strconv.FormatBool(s.cfg.ProxySettings.AllowInsecure), - } - } - return nil -} - -// allowedHostEnvVars returns the variables that can be passed from Grafana's process +// PermittedHostEnvVars returns the variables that can be passed from Grafana's process // (current process, also known as: "host") to the plugin process. -// A string in format "k=v" is returned for each variable in allowedHostEnvVarNames, if it's set. -func (s *Service) allowedHostEnvVars() []string { +// A string in format "k=v" is returned for each variable in PermittedHostEnvVarNames, if it's set. +func PermittedHostEnvVars() []string { var r []string - for _, envVarName := range allowedHostEnvVarNames { + for _, envVarName := range PermittedHostEnvVarNames() { if envVarValue, ok := os.LookupEnv(envVarName); ok { r = append(r, envVarName+"="+envVarValue) } @@ -326,32 +47,6 @@ func (s *Service) allowedHostEnvVars() []string { return r } -type pluginSettings map[string]string - -func getPluginSettings(pluginID string, cfg *config.Cfg) pluginSettings { - ps := pluginSettings{} - for k, v := range cfg.PluginSettings[pluginID] { - if k == "path" || strings.ToLower(k) == "id" { - continue - } - ps[k] = v - } - - return ps -} - -func (ps pluginSettings) asEnvVar(prefix string, hostEnv ...string) []string { - env := make([]string, 0, len(ps)) - for k, v := range ps { - key := fmt.Sprintf("%s_%s", prefix, strings.ToUpper(k)) - if value := os.Getenv(key); value != "" { - v = value - } - - env = append(env, fmt.Sprintf("%s=%s", key, v)) - } - - env = append(env, hostEnv...) - - return env +func PermittedHostEnvVarNames() []string { + return permittedHostEnvVarNames } diff --git a/pkg/plugins/manager/client/client.go b/pkg/plugins/manager/client/client.go index 3565d97ef9..a055882ecd 100644 --- a/pkg/plugins/manager/client/client.go +++ b/pkg/plugins/manager/client/client.go @@ -29,10 +29,10 @@ var ( type Service struct { pluginRegistry registry.Service - cfg *config.Cfg + cfg *config.PluginManagementCfg } -func ProvideService(pluginRegistry registry.Service, cfg *config.Cfg) *Service { +func ProvideService(pluginRegistry registry.Service, cfg *config.PluginManagementCfg) *Service { return &Service{ pluginRegistry: pluginRegistry, cfg: cfg, diff --git a/pkg/plugins/manager/client/client_test.go b/pkg/plugins/manager/client/client_test.go index 7c32f97526..c6fe6b19d1 100644 --- a/pkg/plugins/manager/client/client_test.go +++ b/pkg/plugins/manager/client/client_test.go @@ -18,7 +18,7 @@ import ( func TestQueryData(t *testing.T) { t.Run("Empty registry should return not registered error", func(t *testing.T) { registry := fakes.NewFakePluginRegistry() - client := ProvideService(registry, &config.Cfg{}) + client := ProvideService(registry, &config.PluginManagementCfg{}) _, err := client.QueryData(context.Background(), &backend.QueryDataRequest{}) require.Error(t, err) require.ErrorIs(t, err, plugins.ErrPluginNotRegistered) @@ -63,7 +63,7 @@ func TestQueryData(t *testing.T) { err := registry.Add(context.Background(), p) require.NoError(t, err) - client := ProvideService(registry, &config.Cfg{}) + client := ProvideService(registry, &config.PluginManagementCfg{}) _, err = client.QueryData(context.Background(), &backend.QueryDataRequest{ PluginContext: backend.PluginContext{ PluginID: "grafana", @@ -79,7 +79,7 @@ func TestQueryData(t *testing.T) { func TestCheckHealth(t *testing.T) { t.Run("empty plugin registry should return plugin not registered error", func(t *testing.T) { registry := fakes.NewFakePluginRegistry() - client := ProvideService(registry, &config.Cfg{}) + client := ProvideService(registry, &config.PluginManagementCfg{}) _, err := client.CheckHealth(context.Background(), &backend.CheckHealthRequest{}) require.Error(t, err) require.ErrorIs(t, err, plugins.ErrPluginNotRegistered) @@ -125,7 +125,7 @@ func TestCheckHealth(t *testing.T) { err := registry.Add(context.Background(), p) require.NoError(t, err) - client := ProvideService(registry, &config.Cfg{}) + client := ProvideService(registry, &config.PluginManagementCfg{}) _, err = client.CheckHealth(context.Background(), &backend.CheckHealthRequest{ PluginContext: backend.PluginContext{ PluginID: "grafana", @@ -189,7 +189,7 @@ func TestCallResource(t *testing.T) { err := registry.Add(context.Background(), p) require.NoError(t, err) - client := ProvideService(registry, &config.Cfg{}) + client := ProvideService(registry, &config.PluginManagementCfg{}) err = client.CallResource(context.Background(), req, sender) require.NoError(t, err) @@ -252,7 +252,7 @@ func TestCallResource(t *testing.T) { err := registry.Add(context.Background(), p) require.NoError(t, err) - client := ProvideService(registry, &config.Cfg{}) + client := ProvideService(registry, &config.PluginManagementCfg{}) err = client.CallResource(context.Background(), req, sender) require.NoError(t, err) @@ -298,7 +298,7 @@ func TestCallResource(t *testing.T) { err := registry.Add(context.Background(), p) require.NoError(t, err) - client := ProvideService(registry, &config.Cfg{}) + client := ProvideService(registry, &config.PluginManagementCfg{}) err = client.CallResource(context.Background(), req, sender) require.NoError(t, err) @@ -366,7 +366,7 @@ func TestCallResource(t *testing.T) { err := registry.Add(context.Background(), p) require.NoError(t, err) - client := ProvideService(registry, &config.Cfg{}) + client := ProvideService(registry, &config.PluginManagementCfg{}) err = client.CallResource(context.Background(), req, sender) require.NoError(t, err) diff --git a/pkg/plugins/manager/fakes/fakes.go b/pkg/plugins/manager/fakes/fakes.go index 51f40db583..17e39fe86e 100644 --- a/pkg/plugins/manager/fakes/fakes.go +++ b/pkg/plugins/manager/fakes/fakes.go @@ -255,6 +255,21 @@ func (s *FakePluginStorage) Extract(ctx context.Context, pluginID string, dirNam return &storage.ExtractedPluginArchive{}, nil } +type FakePluginEnvProvider struct { + PluginEnvVarsFunc func(ctx context.Context, plugin *plugins.Plugin) []string +} + +func NewFakePluginEnvProvider() *FakePluginEnvProvider { + return &FakePluginEnvProvider{} +} + +func (p *FakePluginEnvProvider) PluginEnvVars(ctx context.Context, plugin *plugins.Plugin) []string { + if p.PluginEnvVarsFunc != nil { + return p.PluginEnvVarsFunc(ctx, plugin) + } + return []string{} +} + type FakeProcessManager struct { StartFunc func(_ context.Context, p *plugins.Plugin) error StopFunc func(_ context.Context, p *plugins.Plugin) error diff --git a/pkg/plugins/manager/installer.go b/pkg/plugins/manager/installer.go index 4463220a88..02aafee673 100644 --- a/pkg/plugins/manager/installer.go +++ b/pkg/plugins/manager/installer.go @@ -28,7 +28,7 @@ type PluginInstaller struct { serviceRegistry auth.ExternalServiceRegistry } -func ProvideInstaller(cfg *config.Cfg, pluginRegistry registry.Service, pluginLoader loader.Service, +func ProvideInstaller(cfg *config.PluginManagementCfg, pluginRegistry registry.Service, pluginLoader loader.Service, pluginRepo repo.Service, serviceRegistry auth.ExternalServiceRegistry) *PluginInstaller { return New(pluginRegistry, pluginLoader, pluginRepo, storage.FileSystem(log.NewPrettyLogger("installer.fs"), cfg.PluginsPath), storage.SimpleDirNameGeneratorFunc, serviceRegistry) diff --git a/pkg/plugins/manager/loader/assetpath/assetpath.go b/pkg/plugins/manager/loader/assetpath/assetpath.go index bc235c3846..c741a7865f 100644 --- a/pkg/plugins/manager/loader/assetpath/assetpath.go +++ b/pkg/plugins/manager/loader/assetpath/assetpath.go @@ -18,10 +18,10 @@ import ( // on the plugins CDN, and it will switch to the correct implementation depending on the plugin and the config. type Service struct { cdn *pluginscdn.Service - cfg *config.Cfg + cfg *config.PluginManagementCfg } -func ProvideService(cfg *config.Cfg, cdn *pluginscdn.Service) *Service { +func ProvideService(cfg *config.PluginManagementCfg, cdn *pluginscdn.Service) *Service { return &Service{cfg: cfg, cdn: cdn} } @@ -39,7 +39,7 @@ func NewPluginInfo(pluginJSON plugins.JSONData, class plugins.Class, fs plugins. } } -func DefaultService(cfg *config.Cfg) *Service { +func DefaultService(cfg *config.PluginManagementCfg) *Service { return &Service{cfg: cfg, cdn: pluginscdn.ProvideService(cfg)} } diff --git a/pkg/plugins/manager/loader/assetpath/assetpath_test.go b/pkg/plugins/manager/loader/assetpath/assetpath_test.go index be0303f273..2f9b3d26cb 100644 --- a/pkg/plugins/manager/loader/assetpath/assetpath_test.go +++ b/pkg/plugins/manager/loader/assetpath/assetpath_test.go @@ -36,7 +36,7 @@ func TestService(t *testing.T) { }, } { t.Run(tc.name, func(t *testing.T) { - cfg := &config.Cfg{ + cfg := &config.PluginManagementCfg{ PluginsCDNURLTemplate: tc.cdnBaseURL, PluginSettings: map[string]map[string]string{ "one": {"cdn": "true"}, @@ -142,7 +142,7 @@ func TestService(t *testing.T) { appSubURL: "/grafana/", }, } { - cfg := &config.Cfg{GrafanaAppSubURL: tc.appSubURL} + cfg := &config.PluginManagementCfg{GrafanaAppSubURL: tc.appSubURL} svc := ProvideService(cfg, pluginscdn.ProvideService(cfg)) dir := "/plugins/test-datasource" diff --git a/pkg/plugins/manager/loader/finder/local.go b/pkg/plugins/manager/loader/finder/local.go index 75c4a7e9a7..0a9cb076d4 100644 --- a/pkg/plugins/manager/loader/finder/local.go +++ b/pkg/plugins/manager/loader/finder/local.go @@ -37,7 +37,7 @@ func NewLocalFinder(devMode bool, features featuremgmt.FeatureToggles) *Local { } } -func ProvideLocalFinder(cfg *config.Cfg) *Local { +func ProvideLocalFinder(cfg *config.PluginManagementCfg) *Local { return NewLocalFinder(cfg.DevMode, cfg.Features) } diff --git a/pkg/plugins/manager/loader/loader_test.go b/pkg/plugins/manager/loader/loader_test.go index 77360a7fe2..6eb1322648 100644 --- a/pkg/plugins/manager/loader/loader_test.go +++ b/pkg/plugins/manager/loader/loader_test.go @@ -61,14 +61,14 @@ func TestLoader_Load(t *testing.T) { tests := []struct { name string class plugins.Class - cfg *config.Cfg + cfg *config.PluginManagementCfg pluginPaths []string want []*plugins.Plugin }{ { name: "Load a Core plugin", class: plugins.ClassCore, - cfg: &config.Cfg{Features: featuremgmt.WithFeatures()}, + cfg: &config.PluginManagementCfg{Features: featuremgmt.WithFeatures()}, pluginPaths: []string{filepath.Join(corePluginDir, "app/plugins/datasource/cloudwatch")}, want: []*plugins.Plugin{ { @@ -117,7 +117,7 @@ func TestLoader_Load(t *testing.T) { { name: "Load a Bundled plugin", class: plugins.ClassBundled, - cfg: &config.Cfg{Features: featuremgmt.WithFeatures()}, + cfg: &config.PluginManagementCfg{Features: featuremgmt.WithFeatures()}, pluginPaths: []string{"../testdata/valid-v2-signature"}, want: []*plugins.Plugin{ { @@ -158,7 +158,7 @@ func TestLoader_Load(t *testing.T) { { name: "Load plugin with symbolic links", class: plugins.ClassExternal, - cfg: &config.Cfg{Features: featuremgmt.WithFeatures()}, + cfg: &config.PluginManagementCfg{Features: featuremgmt.WithFeatures()}, pluginPaths: []string{"../testdata/symbolic-plugin-dirs"}, want: []*plugins.Plugin{ { @@ -237,7 +237,7 @@ func TestLoader_Load(t *testing.T) { { name: "Load an unsigned plugin (development)", class: plugins.ClassExternal, - cfg: &config.Cfg{ + cfg: &config.PluginManagementCfg{ DevMode: true, Features: featuremgmt.WithFeatures(), }, @@ -277,14 +277,14 @@ func TestLoader_Load(t *testing.T) { { name: "Load an unsigned plugin (production)", class: plugins.ClassExternal, - cfg: &config.Cfg{Features: featuremgmt.WithFeatures()}, + cfg: &config.PluginManagementCfg{Features: featuremgmt.WithFeatures()}, pluginPaths: []string{"../testdata/unsigned-datasource"}, want: []*plugins.Plugin{}, }, { name: "Load an unsigned plugin using PluginsAllowUnsigned config (production)", class: plugins.ClassExternal, - cfg: &config.Cfg{ + cfg: &config.PluginManagementCfg{ PluginsAllowUnsigned: []string{"test-datasource"}, Features: featuremgmt.WithFeatures(), }, @@ -324,14 +324,14 @@ func TestLoader_Load(t *testing.T) { { name: "Load a plugin with v1 manifest should return signatureInvalid", class: plugins.ClassExternal, - cfg: &config.Cfg{Features: featuremgmt.WithFeatures()}, + cfg: &config.PluginManagementCfg{Features: featuremgmt.WithFeatures()}, pluginPaths: []string{"../testdata/lacking-files"}, want: []*plugins.Plugin{}, }, { name: "Load a plugin with v1 manifest using PluginsAllowUnsigned config (production) should return signatureInvali", class: plugins.ClassExternal, - cfg: &config.Cfg{ + cfg: &config.PluginManagementCfg{ PluginsAllowUnsigned: []string{"test-datasource"}, Features: featuremgmt.WithFeatures(), }, @@ -341,7 +341,7 @@ func TestLoader_Load(t *testing.T) { { name: "Load a plugin with manifest which has a file not found in plugin folder", class: plugins.ClassExternal, - cfg: &config.Cfg{ + cfg: &config.PluginManagementCfg{ PluginsAllowUnsigned: []string{"test-datasource"}, Features: featuremgmt.WithFeatures(), }, @@ -351,7 +351,7 @@ func TestLoader_Load(t *testing.T) { { name: "Load a plugin with file which is missing from the manifest", class: plugins.ClassExternal, - cfg: &config.Cfg{ + cfg: &config.PluginManagementCfg{ PluginsAllowUnsigned: []string{"test-datasource"}, Features: featuremgmt.WithFeatures(), }, @@ -361,7 +361,7 @@ func TestLoader_Load(t *testing.T) { { name: "Load an app with includes", class: plugins.ClassExternal, - cfg: &config.Cfg{ + cfg: &config.PluginManagementCfg{ PluginsAllowUnsigned: []string{"test-app"}, Features: featuremgmt.WithFeatures(), }, @@ -413,7 +413,7 @@ func TestLoader_Load(t *testing.T) { { name: "Load a plugin with app sub url set", class: plugins.ClassExternal, - cfg: &config.Cfg{ + cfg: &config.PluginManagementCfg{ DevMode: true, GrafanaAppSubURL: "grafana", Features: featuremgmt.WithFeatures(), diff --git a/pkg/plugins/manager/pipeline/bootstrap/bootstrap.go b/pkg/plugins/manager/pipeline/bootstrap/bootstrap.go index 19300c4e24..df9e16692b 100644 --- a/pkg/plugins/manager/pipeline/bootstrap/bootstrap.go +++ b/pkg/plugins/manager/pipeline/bootstrap/bootstrap.go @@ -42,7 +42,7 @@ type Opts struct { } // New returns a new Bootstrap stage. -func New(cfg *config.Cfg, opts Opts) *Bootstrap { +func New(cfg *config.PluginManagementCfg, opts Opts) *Bootstrap { if opts.ConstructFunc == nil { opts.ConstructFunc = DefaultConstructFunc(signature.DefaultCalculator(cfg), assetpath.DefaultService(cfg)) } diff --git a/pkg/plugins/manager/pipeline/bootstrap/steps.go b/pkg/plugins/manager/pipeline/bootstrap/steps.go index ab77106b74..dffb91df5d 100644 --- a/pkg/plugins/manager/pipeline/bootstrap/steps.go +++ b/pkg/plugins/manager/pipeline/bootstrap/steps.go @@ -29,7 +29,7 @@ func DefaultConstructFunc(signatureCalculator plugins.SignatureCalculator, asset } // DefaultDecorateFuncs are the default DecorateFuncs used for the Decorate step of the Bootstrap stage. -func DefaultDecorateFuncs(cfg *config.Cfg) []DecorateFunc { +func DefaultDecorateFuncs(cfg *config.PluginManagementCfg) []DecorateFunc { return []DecorateFunc{ AppDefaultNavURLDecorateFunc, TemplateDecorateFunc, @@ -161,7 +161,7 @@ func configureAppChildPlugin(parent *plugins.Plugin, child *plugins.Plugin) { // SkipHostEnvVarsDecorateFunc returns a DecorateFunc that configures the SkipHostEnvVars field of the plugin. // It will be set to true if the FlagPluginsSkipHostEnvVars feature flag is set, and the plugin is not present in the // ForwardHostEnvVars plugin ids list. -func SkipHostEnvVarsDecorateFunc(cfg *config.Cfg) DecorateFunc { +func SkipHostEnvVarsDecorateFunc(cfg *config.PluginManagementCfg) DecorateFunc { return func(_ context.Context, p *plugins.Plugin) (*plugins.Plugin, error) { p.SkipHostEnvVars = cfg.Features.IsEnabledGlobally(featuremgmt.FlagPluginsSkipHostEnvVars) && !slices.Contains(cfg.ForwardHostEnvVars, p.ID) diff --git a/pkg/plugins/manager/pipeline/bootstrap/steps_test.go b/pkg/plugins/manager/pipeline/bootstrap/steps_test.go index d64ab36a2b..2a5b15d976 100644 --- a/pkg/plugins/manager/pipeline/bootstrap/steps_test.go +++ b/pkg/plugins/manager/pipeline/bootstrap/steps_test.go @@ -144,7 +144,7 @@ func TestSkipEnvVarsDecorateFunc(t *testing.T) { const pluginID = "plugin-id" t.Run("feature flag is not present", func(t *testing.T) { - f := SkipHostEnvVarsDecorateFunc(&config.Cfg{Features: featuremgmt.WithFeatures()}) + f := SkipHostEnvVarsDecorateFunc(&config.PluginManagementCfg{Features: featuremgmt.WithFeatures()}) p, err := f(context.Background(), &plugins.Plugin{JSONData: plugins.JSONData{ID: pluginID}}) require.NoError(t, err) require.False(t, p.SkipHostEnvVars) @@ -152,7 +152,7 @@ func TestSkipEnvVarsDecorateFunc(t *testing.T) { t.Run("feature flag is present", func(t *testing.T) { t.Run("no plugin settings should set SkipHostEnvVars to true", func(t *testing.T) { - f := SkipHostEnvVarsDecorateFunc(&config.Cfg{ + f := SkipHostEnvVarsDecorateFunc(&config.PluginManagementCfg{ Features: featuremgmt.WithFeatures(featuremgmt.FlagPluginsSkipHostEnvVars), }) p, err := f(context.Background(), &plugins.Plugin{JSONData: plugins.JSONData{ID: pluginID}}) @@ -188,7 +188,7 @@ func TestSkipEnvVarsDecorateFunc(t *testing.T) { }, } { t.Run(tc.name, func(t *testing.T) { - f := SkipHostEnvVarsDecorateFunc(&config.Cfg{ + f := SkipHostEnvVarsDecorateFunc(&config.PluginManagementCfg{ Features: featuremgmt.WithFeatures(featuremgmt.FlagPluginsSkipHostEnvVars), ForwardHostEnvVars: tc.forwardHostEnvVars, }) diff --git a/pkg/plugins/manager/pipeline/discovery/discovery.go b/pkg/plugins/manager/pipeline/discovery/discovery.go index 8d7c2b4f71..f05f2582e2 100644 --- a/pkg/plugins/manager/pipeline/discovery/discovery.go +++ b/pkg/plugins/manager/pipeline/discovery/discovery.go @@ -40,7 +40,7 @@ type Opts struct { } // New returns a new Discovery stage. -func New(cfg *config.Cfg, opts Opts) *Discovery { +func New(cfg *config.PluginManagementCfg, opts Opts) *Discovery { if opts.FindFunc == nil { opts.FindFunc = DefaultFindFunc(cfg) } diff --git a/pkg/plugins/manager/pipeline/discovery/steps.go b/pkg/plugins/manager/pipeline/discovery/steps.go index bff2c41856..0256b216b2 100644 --- a/pkg/plugins/manager/pipeline/discovery/steps.go +++ b/pkg/plugins/manager/pipeline/discovery/steps.go @@ -11,7 +11,7 @@ import ( // DefaultFindFunc is the default function used for the Find step of the Discovery stage. It will scan the local // filesystem for plugins. -func DefaultFindFunc(cfg *config.Cfg) FindFunc { +func DefaultFindFunc(cfg *config.PluginManagementCfg) FindFunc { return finder.NewLocalFinder(cfg.DevMode, cfg.Features).Find } diff --git a/pkg/plugins/manager/pipeline/initialization/initialization.go b/pkg/plugins/manager/pipeline/initialization/initialization.go index f4ef2df857..03b7d9375b 100644 --- a/pkg/plugins/manager/pipeline/initialization/initialization.go +++ b/pkg/plugins/manager/pipeline/initialization/initialization.go @@ -17,7 +17,7 @@ type Initializer interface { type InitializeFunc func(ctx context.Context, p *plugins.Plugin) (*plugins.Plugin, error) type Initialize struct { - cfg *config.Cfg + cfg *config.PluginManagementCfg initializeSteps []InitializeFunc log log.Logger } @@ -27,7 +27,7 @@ type Opts struct { } // New returns a new Initialization stage. -func New(cfg *config.Cfg, opts Opts) *Initialize { +func New(cfg *config.PluginManagementCfg, opts Opts) *Initialize { if opts.InitializeFuncs == nil { opts.InitializeFuncs = []InitializeFunc{} } diff --git a/pkg/plugins/manager/pipeline/initialization/steps.go b/pkg/plugins/manager/pipeline/initialization/steps.go index db06e6f72e..c27cf66760 100644 --- a/pkg/plugins/manager/pipeline/initialization/steps.go +++ b/pkg/plugins/manager/pipeline/initialization/steps.go @@ -47,7 +47,7 @@ func (b *BackendClientInit) Initialize(ctx context.Context, p *plugins.Plugin) ( } // this will ensure that the env variables are calculated every time a plugin is started - envFunc := func() []string { return b.envVarProvider.Get(ctx, p) } + envFunc := func() []string { return b.envVarProvider.PluginEnvVars(ctx, p) } if backendClient, err := backendFactory(p.ID, p.Logger(), envFunc); err != nil { return nil, err diff --git a/pkg/plugins/manager/pipeline/initialization/steps_test.go b/pkg/plugins/manager/pipeline/initialization/steps_test.go index fa387ec6f8..b5e43195e7 100644 --- a/pkg/plugins/manager/pipeline/initialization/steps_test.go +++ b/pkg/plugins/manager/pipeline/initialization/steps_test.go @@ -121,12 +121,12 @@ func (f *fakeBackendProvider) BackendFactory(_ context.Context, _ *plugins.Plugi } type fakeEnvVarsProvider struct { - GetFunc func(ctx context.Context, p *plugins.Plugin) []string + PluginEnvVarsFunc func(ctx context.Context, p *plugins.Plugin) []string } -func (f *fakeEnvVarsProvider) Get(ctx context.Context, p *plugins.Plugin) []string { - if f.GetFunc != nil { - return f.GetFunc(ctx, p) +func (f *fakeEnvVarsProvider) PluginEnvVars(ctx context.Context, p *plugins.Plugin) []string { + if f.PluginEnvVarsFunc != nil { + return f.PluginEnvVars(ctx, p) } return nil } diff --git a/pkg/plugins/manager/pipeline/termination/termination.go b/pkg/plugins/manager/pipeline/termination/termination.go index 85c31de910..66e76d8a54 100644 --- a/pkg/plugins/manager/pipeline/termination/termination.go +++ b/pkg/plugins/manager/pipeline/termination/termination.go @@ -17,7 +17,7 @@ type Terminator interface { type TerminateFunc func(ctx context.Context, p *plugins.Plugin) error type Terminate struct { - cfg *config.Cfg + cfg *config.PluginManagementCfg terminateSteps []TerminateFunc log log.Logger } @@ -27,7 +27,7 @@ type Opts struct { } // New returns a new Termination stage. -func New(cfg *config.Cfg, opts Opts) (*Terminate, error) { +func New(cfg *config.PluginManagementCfg, opts Opts) (*Terminate, error) { if opts.TerminateFuncs == nil { opts.TerminateFuncs = []TerminateFunc{} } diff --git a/pkg/plugins/manager/pipeline/validation/steps.go b/pkg/plugins/manager/pipeline/validation/steps.go index 5efe0b4bb2..20dee23f78 100644 --- a/pkg/plugins/manager/pipeline/validation/steps.go +++ b/pkg/plugins/manager/pipeline/validation/steps.go @@ -14,7 +14,7 @@ import ( ) // DefaultValidateFuncs are the default ValidateFunc used for the Validate step of the Validation stage. -func DefaultValidateFuncs(cfg *config.Cfg) []ValidateFunc { +func DefaultValidateFuncs(cfg *config.PluginManagementCfg) []ValidateFunc { return []ValidateFunc{ SignatureValidationStep(signature.NewValidator(signature.NewUnsignedAuthorizer(cfg))), ModuleJSValidationStep(), @@ -74,16 +74,16 @@ func (v *ModuleJSValidator) Validate(_ context.Context, p *plugins.Plugin) error } type AngularDetector struct { - cfg *config.Cfg + cfg *config.PluginManagementCfg angularInspector angularinspector.Inspector log log.Logger } -func AngularDetectionStep(cfg *config.Cfg, angularInspector angularinspector.Inspector) ValidateFunc { +func AngularDetectionStep(cfg *config.PluginManagementCfg, angularInspector angularinspector.Inspector) ValidateFunc { return newAngularDetector(cfg, angularInspector).Validate } -func newAngularDetector(cfg *config.Cfg, angularInspector angularinspector.Inspector) *AngularDetector { +func newAngularDetector(cfg *config.PluginManagementCfg, angularInspector angularinspector.Inspector) *AngularDetector { return &AngularDetector{ cfg: cfg, angularInspector: angularInspector, diff --git a/pkg/plugins/manager/pipeline/validation/validation.go b/pkg/plugins/manager/pipeline/validation/validation.go index 05c48f59f1..6c3ab23dc1 100644 --- a/pkg/plugins/manager/pipeline/validation/validation.go +++ b/pkg/plugins/manager/pipeline/validation/validation.go @@ -17,7 +17,7 @@ type Validator interface { type ValidateFunc func(ctx context.Context, p *plugins.Plugin) error type Validate struct { - cfg *config.Cfg + cfg *config.PluginManagementCfg validateSteps []ValidateFunc log log.Logger } @@ -27,7 +27,7 @@ type Opts struct { } // New returns a new Validation stage. -func New(cfg *config.Cfg, opts Opts) *Validate { +func New(cfg *config.PluginManagementCfg, opts Opts) *Validate { if opts.ValidateFuncs == nil { opts.ValidateFuncs = DefaultValidateFuncs(cfg) } diff --git a/pkg/plugins/manager/signature/authorizer.go b/pkg/plugins/manager/signature/authorizer.go index a96aa6f16b..5dc0f1845f 100644 --- a/pkg/plugins/manager/signature/authorizer.go +++ b/pkg/plugins/manager/signature/authorizer.go @@ -5,18 +5,18 @@ import ( "github.com/grafana/grafana/pkg/plugins/config" ) -func ProvideOSSAuthorizer(cfg *config.Cfg) *UnsignedPluginAuthorizer { +func ProvideOSSAuthorizer(cfg *config.PluginManagementCfg) *UnsignedPluginAuthorizer { return NewUnsignedAuthorizer(cfg) } -func NewUnsignedAuthorizer(cfg *config.Cfg) *UnsignedPluginAuthorizer { +func NewUnsignedAuthorizer(cfg *config.PluginManagementCfg) *UnsignedPluginAuthorizer { return &UnsignedPluginAuthorizer{ cfg: cfg, } } type UnsignedPluginAuthorizer struct { - cfg *config.Cfg + cfg *config.PluginManagementCfg } func (u *UnsignedPluginAuthorizer) CanLoadPlugin(p *plugins.Plugin) bool { diff --git a/pkg/plugins/manager/signature/manifest.go b/pkg/plugins/manager/signature/manifest.go index 5313d9c7e3..f2c64917f3 100644 --- a/pkg/plugins/manager/signature/manifest.go +++ b/pkg/plugins/manager/signature/manifest.go @@ -59,17 +59,17 @@ func (m *PluginManifest) isV2() bool { type Signature struct { kr plugins.KeyRetriever - cfg *config.Cfg + cfg *config.PluginManagementCfg log log.Logger } var _ plugins.SignatureCalculator = &Signature{} -func ProvideService(cfg *config.Cfg, kr plugins.KeyRetriever) *Signature { +func ProvideService(cfg *config.PluginManagementCfg, kr plugins.KeyRetriever) *Signature { return NewCalculator(cfg, kr) } -func NewCalculator(cfg *config.Cfg, kr plugins.KeyRetriever) *Signature { +func NewCalculator(cfg *config.PluginManagementCfg, kr plugins.KeyRetriever) *Signature { return &Signature{ kr: kr, cfg: cfg, @@ -77,7 +77,7 @@ func NewCalculator(cfg *config.Cfg, kr plugins.KeyRetriever) *Signature { } } -func DefaultCalculator(cfg *config.Cfg) *Signature { +func DefaultCalculator(cfg *config.PluginManagementCfg) *Signature { return &Signature{ kr: statickey.New(), cfg: cfg, diff --git a/pkg/plugins/manager/signature/manifest_test.go b/pkg/plugins/manager/signature/manifest_test.go index 6f950e54dd..7242578588 100644 --- a/pkg/plugins/manager/signature/manifest_test.go +++ b/pkg/plugins/manager/signature/manifest_test.go @@ -52,7 +52,7 @@ NR7DnB0CCQHO+4FlSPtXFTzNepoc+CytQyDAeOLMLmf2Tqhk2YShk+G/YlVX -----END PGP SIGNATURE-----` t.Run("valid manifest", func(t *testing.T) { - s := ProvideService(&config.Cfg{}, statickey.New()) + s := ProvideService(&config.PluginManagementCfg{}, statickey.New()) manifest, err := s.readPluginManifest(context.Background(), []byte(txt)) require.NoError(t, err) @@ -69,7 +69,7 @@ NR7DnB0CCQHO+4FlSPtXFTzNepoc+CytQyDAeOLMLmf2Tqhk2YShk+G/YlVX t.Run("invalid manifest", func(t *testing.T) { modified := strings.ReplaceAll(txt, "README.md", "xxxxxxxxxx") - s := ProvideService(&config.Cfg{}, statickey.New()) + s := ProvideService(&config.PluginManagementCfg{}, statickey.New()) _, err := s.readPluginManifest(context.Background(), []byte(modified)) require.Error(t, err) }) @@ -107,7 +107,7 @@ khdr/tZ1PDgRxMqB/u+Vtbpl0xSxgblnrDOYMSI= -----END PGP SIGNATURE-----` t.Run("valid manifest", func(t *testing.T) { - s := ProvideService(&config.Cfg{}, statickey.New()) + s := ProvideService(&config.PluginManagementCfg{}, statickey.New()) manifest, err := s.readPluginManifest(context.Background(), []byte(txt)) require.NoError(t, err) @@ -155,7 +155,7 @@ func TestCalculate(t *testing.T) { for _, tc := range tcs { basePath := filepath.Join(parentDir, "testdata/non-pvt-with-root-url/plugin") - s := ProvideService(&config.Cfg{GrafanaAppURL: tc.appURL}, statickey.New()) + s := ProvideService(&config.PluginManagementCfg{GrafanaAppURL: tc.appURL}, statickey.New()) sig, err := s.Calculate(context.Background(), &fakes.FakePluginSource{ PluginClassFunc: func(ctx context.Context) plugins.Class { return plugins.ClassExternal @@ -183,7 +183,7 @@ func TestCalculate(t *testing.T) { basePath := "../testdata/renderer-added-file/plugin" runningWindows = true - s := ProvideService(&config.Cfg{}, statickey.New()) + s := ProvideService(&config.PluginManagementCfg{}, statickey.New()) sig, err := s.Calculate(context.Background(), &fakes.FakePluginSource{ PluginClassFunc: func(ctx context.Context) plugins.Class { return plugins.ClassExternal @@ -247,7 +247,7 @@ func TestCalculate(t *testing.T) { toSlash = tc.platform.toSlashFunc() fromSlash = tc.platform.fromSlashFunc() - s := ProvideService(&config.Cfg{}, statickey.New()) + s := ProvideService(&config.PluginManagementCfg{}, statickey.New()) pfs, err := tc.fsFactory() require.NoError(t, err) pfs, err = newPathSeparatorOverrideFS(string(tc.platform.separator), pfs) @@ -715,7 +715,7 @@ func Test_validateManifest(t *testing.T) { } for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { - s := ProvideService(&config.Cfg{}, statickey.New()) + s := ProvideService(&config.PluginManagementCfg{}, statickey.New()) err := s.validateManifest(context.Background(), *tc.manifest, nil) require.Errorf(t, err, tc.expectedErr) }) @@ -811,7 +811,7 @@ pHo= } func Test_VerifyRevokedKey(t *testing.T) { - s := ProvideService(&config.Cfg{}, &revokedKeyProvider{}) + s := ProvideService(&config.PluginManagementCfg{}, &revokedKeyProvider{}) m := createV2Manifest(t) txt := `-----BEGIN PGP SIGNED MESSAGE----- Hash: SHA512 diff --git a/pkg/plugins/pluginscdn/pluginscdn.go b/pkg/plugins/pluginscdn/pluginscdn.go index 5f93f48259..44445d449f 100644 --- a/pkg/plugins/pluginscdn/pluginscdn.go +++ b/pkg/plugins/pluginscdn/pluginscdn.go @@ -16,10 +16,10 @@ var ErrPluginNotCDN = errors.New("plugin is not a cdn plugin") // Service provides methods for the plugins CDN. type Service struct { - cfg *config.Cfg + cfg *config.PluginManagementCfg } -func ProvideService(cfg *config.Cfg) *Service { +func ProvideService(cfg *config.PluginManagementCfg) *Service { return &Service{cfg: cfg} } diff --git a/pkg/plugins/pluginscdn/pluginscdn_test.go b/pkg/plugins/pluginscdn/pluginscdn_test.go index 08cae194aa..f18285b0d6 100644 --- a/pkg/plugins/pluginscdn/pluginscdn_test.go +++ b/pkg/plugins/pluginscdn/pluginscdn_test.go @@ -8,7 +8,7 @@ import ( ) func TestService(t *testing.T) { - svc := ProvideService(&config.Cfg{ + svc := ProvideService(&config.PluginManagementCfg{ PluginsCDNURLTemplate: "https://cdn.example.com", PluginSettings: map[string]map[string]string{ "one": {"cdn": "true"}, @@ -40,7 +40,7 @@ func TestService(t *testing.T) { }, } { t.Run(c.name, func(t *testing.T) { - u, err := ProvideService(&config.Cfg{PluginsCDNURLTemplate: c.cfgURL}).BaseURL() + u, err := ProvideService(&config.PluginManagementCfg{PluginsCDNURLTemplate: c.cfgURL}).BaseURL() require.NoError(t, err) require.Equal(t, c.expBaseURL, u) }) diff --git a/pkg/plugins/repo/service.go b/pkg/plugins/repo/service.go index 8b1a7a1654..b55d2d582f 100644 --- a/pkg/plugins/repo/service.go +++ b/pkg/plugins/repo/service.go @@ -21,7 +21,7 @@ type Manager struct { log log.PrettyLogger } -func ProvideService(cfg *config.Cfg) (*Manager, error) { +func ProvideService(cfg *config.PluginManagementCfg) (*Manager, error) { baseURL, err := url.JoinPath(cfg.GrafanaComURL, "/api/plugins") if err != nil { return nil, err diff --git a/pkg/services/pluginsintegration/angulardetectorsprovider/dynamic.go b/pkg/services/pluginsintegration/angulardetectorsprovider/dynamic.go index a2b326df0f..3e5229d2e3 100644 --- a/pkg/services/pluginsintegration/angulardetectorsprovider/dynamic.go +++ b/pkg/services/pluginsintegration/angulardetectorsprovider/dynamic.go @@ -47,7 +47,7 @@ type Dynamic struct { mux sync.RWMutex } -func ProvideDynamic(cfg *config.Cfg, store angularpatternsstore.Service, features featuremgmt.FeatureToggles) (*Dynamic, error) { +func ProvideDynamic(cfg *config.PluginManagementCfg, store angularpatternsstore.Service, features featuremgmt.FeatureToggles) (*Dynamic, error) { d := &Dynamic{ log: log.New("plugin.angulardetectorsprovider.dynamic"), features: features, diff --git a/pkg/services/pluginsintegration/angulardetectorsprovider/dynamic_test.go b/pkg/services/pluginsintegration/angulardetectorsprovider/dynamic_test.go index 9ffbfc409a..c3fa42273b 100644 --- a/pkg/services/pluginsintegration/angulardetectorsprovider/dynamic_test.go +++ b/pkg/services/pluginsintegration/angulardetectorsprovider/dynamic_test.go @@ -574,7 +574,7 @@ func provideDynamic(t *testing.T, gcomURL string, opts ...provideDynamicOpts) *D opt.store = angularpatternsstore.ProvideService(kvstore.NewFakeKVStore()) } d, err := ProvideDynamic( - &config.Cfg{GrafanaComURL: gcomURL}, + &config.PluginManagementCfg{GrafanaComURL: gcomURL}, opt.store, featuremgmt.WithFeatures(featuremgmt.FlagPluginsDynamicAngularDetectionPatterns), ) diff --git a/pkg/services/pluginsintegration/angularinspector/angularinspector.go b/pkg/services/pluginsintegration/angularinspector/angularinspector.go index 9eb8b20ab3..b42fa54869 100644 --- a/pkg/services/pluginsintegration/angularinspector/angularinspector.go +++ b/pkg/services/pluginsintegration/angularinspector/angularinspector.go @@ -12,7 +12,7 @@ type Service struct { angularinspector.Inspector } -func ProvideService(cfg *config.Cfg, dynamic *angulardetectorsprovider.Dynamic) (*Service, error) { +func ProvideService(cfg *config.PluginManagementCfg, dynamic *angulardetectorsprovider.Dynamic) (*Service, error) { var detectorsProvider angulardetector.DetectorsProvider var err error static := angularinspector.NewDefaultStaticDetectorsProvider() diff --git a/pkg/services/pluginsintegration/angularinspector/angularinspector_test.go b/pkg/services/pluginsintegration/angularinspector/angularinspector_test.go index de98c2f838..14b288995b 100644 --- a/pkg/services/pluginsintegration/angularinspector/angularinspector_test.go +++ b/pkg/services/pluginsintegration/angularinspector/angularinspector_test.go @@ -17,7 +17,7 @@ import ( func TestProvideService(t *testing.T) { t.Run("uses hardcoded inspector if feature flag is not present", func(t *testing.T) { - pCfg := &config.Cfg{Features: featuremgmt.WithFeatures()} + pCfg := &config.PluginManagementCfg{Features: featuremgmt.WithFeatures()} dynamic, err := angulardetectorsprovider.ProvideDynamic( pCfg, angularpatternsstore.ProvideService(kvstore.NewFakeKVStore()), @@ -33,7 +33,7 @@ func TestProvideService(t *testing.T) { }) t.Run("uses dynamic inspector with hardcoded fallback if feature flag is present", func(t *testing.T) { - pCfg := &config.Cfg{Features: featuremgmt.WithFeatures( + pCfg := &config.PluginManagementCfg{Features: featuremgmt.WithFeatures( featuremgmt.FlagPluginsDynamicAngularDetectionPatterns, )} dynamic, err := angulardetectorsprovider.ProvideDynamic( diff --git a/pkg/services/pluginsintegration/config/config.go b/pkg/services/pluginsintegration/config/config.go deleted file mode 100644 index 7c269dd31c..0000000000 --- a/pkg/services/pluginsintegration/config/config.go +++ /dev/null @@ -1,90 +0,0 @@ -package config - -import ( - "fmt" - "strings" - - pCfg "github.com/grafana/grafana/pkg/plugins/config" - "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/setting" - "github.com/grafana/grafana/pkg/util" -) - -func ProvideConfig(settingProvider setting.Provider, grafanaCfg *setting.Cfg, features featuremgmt.FeatureToggles) (*pCfg.Cfg, error) { - plugins := settingProvider.Section("plugins") - allowedUnsigned := grafanaCfg.PluginsAllowUnsigned - if len(plugins.KeyValue("allow_loading_unsigned_plugins").Value()) > 0 { - allowedUnsigned = strings.Split(plugins.KeyValue("allow_loading_unsigned_plugins").Value(), ",") - } - - // Get aws settings from settingProvider instead of grafanaCfg - aws := settingProvider.Section("aws") - allowedAuth := grafanaCfg.AWSAllowedAuthProviders - if len(aws.KeyValue("allowed_auth_providers").Value()) > 0 { - allowedAuth = util.SplitString(aws.KeyValue("allowed_auth_providers").Value()) - } - if len(allowedAuth) > 0 { - allowedUnsigned = strings.Split(settingProvider.KeyValue("plugins", "allow_loading_unsigned_plugins").Value(), ",") - } - awsForwardSettingsPlugins := grafanaCfg.AWSForwardSettingsPlugins - if len(aws.KeyValue("forward_settings_to_plugins").Value()) > 0 { - awsForwardSettingsPlugins = util.SplitString(aws.KeyValue("forward_settings_to_plugins").Value()) - } - - tracingCfg, err := newTracingCfg(grafanaCfg) - if err != nil { - return nil, fmt.Errorf("new opentelemetry cfg: %w", err) - } - - return pCfg.NewCfg( - settingProvider.KeyValue("", "app_mode").MustBool(grafanaCfg.Env == setting.Dev), - grafanaCfg.PluginsPath, - extractPluginSettings(settingProvider), - allowedUnsigned, - allowedAuth, - aws.KeyValue("assume_role_enabled").MustBool(grafanaCfg.AWSAssumeRoleEnabled), - aws.KeyValue("external_id").Value(), - aws.KeyValue("session_duration").Value(), - aws.KeyValue("list_metrics_page_limit").Value(), - awsForwardSettingsPlugins, - grafanaCfg.Azure, - grafanaCfg.SecureSocksDSProxy, - grafanaCfg.BuildVersion, - grafanaCfg.PluginLogBackendRequests, - grafanaCfg.PluginsCDNURLTemplate, - grafanaCfg.AppURL, - grafanaCfg.AppSubURL, - tracingCfg, - features, - grafanaCfg.AngularSupportEnabled, - grafanaCfg.GrafanaComURL, - grafanaCfg.DisablePlugins, - grafanaCfg.HideAngularDeprecation, - grafanaCfg.ForwardHostEnvVars, - grafanaCfg.ConcurrentQueryCount, - grafanaCfg.AzureAuthEnabled, - grafanaCfg.UserFacingDefaultError, - grafanaCfg.DataProxyRowLimit, - grafanaCfg.SqlDatasourceMaxOpenConnsDefault, - grafanaCfg.SqlDatasourceMaxIdleConnsDefault, - grafanaCfg.SqlDatasourceMaxConnLifetimeDefault, - ), nil -} - -func extractPluginSettings(settingProvider setting.Provider) setting.PluginSettings { - ps := setting.PluginSettings{} - for sectionName, sectionCopy := range settingProvider.Current() { - if !strings.HasPrefix(sectionName, "plugin.") { - continue - } - // Calling Current() returns a redacted version of section. We need to replace the map values with the unredacted values. - section := settingProvider.Section(sectionName) - for k := range sectionCopy { - sectionCopy[k] = section.KeyValue(k).MustString("") - } - pluginID := strings.Replace(sectionName, "plugin.", "", 1) - ps[pluginID] = sectionCopy - } - - return ps -} diff --git a/pkg/services/pluginsintegration/loader/loader_test.go b/pkg/services/pluginsintegration/loader/loader_test.go index 9d33476df4..0b8976f45c 100644 --- a/pkg/services/pluginsintegration/loader/loader_test.go +++ b/pkg/services/pluginsintegration/loader/loader_test.go @@ -60,7 +60,7 @@ func TestLoader_Load(t *testing.T) { tests := []struct { name string class plugins.Class - cfg *config.Cfg + cfg *config.PluginManagementCfg pluginPaths []string want []*plugins.Plugin pluginErrors map[string]*plugins.SignatureError @@ -68,7 +68,7 @@ func TestLoader_Load(t *testing.T) { { name: "Load a Core plugin", class: plugins.ClassCore, - cfg: &config.Cfg{Features: featuremgmt.WithFeatures()}, + cfg: &config.PluginManagementCfg{Features: featuremgmt.WithFeatures()}, pluginPaths: []string{filepath.Join(corePluginDir(t), "app/plugins/datasource/cloudwatch")}, want: []*plugins.Plugin{ { @@ -117,7 +117,7 @@ func TestLoader_Load(t *testing.T) { { name: "Load a Bundled plugin", class: plugins.ClassBundled, - cfg: &config.Cfg{Features: featuremgmt.WithFeatures()}, + cfg: &config.PluginManagementCfg{Features: featuremgmt.WithFeatures()}, pluginPaths: []string{filepath.Join(testDataDir(t), "valid-v2-signature")}, want: []*plugins.Plugin{ { @@ -158,7 +158,7 @@ func TestLoader_Load(t *testing.T) { { name: "Load plugin with symbolic links", class: plugins.ClassExternal, - cfg: &config.Cfg{Features: featuremgmt.WithFeatures()}, + cfg: &config.PluginManagementCfg{Features: featuremgmt.WithFeatures()}, pluginPaths: []string{filepath.Join(testDataDir(t), "symbolic-plugin-dirs")}, want: []*plugins.Plugin{ { @@ -237,7 +237,7 @@ func TestLoader_Load(t *testing.T) { { name: "Load an unsigned plugin (development)", class: plugins.ClassExternal, - cfg: &config.Cfg{ + cfg: &config.PluginManagementCfg{ DevMode: true, Features: featuremgmt.WithFeatures(), }, @@ -277,7 +277,7 @@ func TestLoader_Load(t *testing.T) { { name: "Load an unsigned plugin (production)", class: plugins.ClassExternal, - cfg: &config.Cfg{Features: featuremgmt.WithFeatures()}, + cfg: &config.PluginManagementCfg{Features: featuremgmt.WithFeatures()}, pluginPaths: []string{filepath.Join(testDataDir(t), "unsigned-datasource")}, want: []*plugins.Plugin{}, pluginErrors: map[string]*plugins.SignatureError{ @@ -290,7 +290,7 @@ func TestLoader_Load(t *testing.T) { { name: "Load an unsigned plugin using PluginsAllowUnsigned config (production)", class: plugins.ClassExternal, - cfg: &config.Cfg{ + cfg: &config.PluginManagementCfg{ PluginsAllowUnsigned: []string{"test-datasource"}, Features: featuremgmt.WithFeatures(), }, @@ -330,7 +330,7 @@ func TestLoader_Load(t *testing.T) { { name: "Load a plugin with v1 manifest should return signatureInvalid", class: plugins.ClassExternal, - cfg: &config.Cfg{Features: featuremgmt.WithFeatures()}, + cfg: &config.PluginManagementCfg{Features: featuremgmt.WithFeatures()}, pluginPaths: []string{filepath.Join(testDataDir(t), "lacking-files")}, want: []*plugins.Plugin{}, pluginErrors: map[string]*plugins.SignatureError{ @@ -343,7 +343,7 @@ func TestLoader_Load(t *testing.T) { { name: "Load a plugin with v1 manifest using PluginsAllowUnsigned config (production) should return signatureInvalid", class: plugins.ClassExternal, - cfg: &config.Cfg{ + cfg: &config.PluginManagementCfg{ PluginsAllowUnsigned: []string{"test-datasource"}, Features: featuremgmt.WithFeatures(), }, @@ -359,7 +359,7 @@ func TestLoader_Load(t *testing.T) { { name: "Load a plugin with manifest which has a file not found in plugin folder", class: plugins.ClassExternal, - cfg: &config.Cfg{ + cfg: &config.PluginManagementCfg{ PluginsAllowUnsigned: []string{"test-datasource"}, Features: featuremgmt.WithFeatures(), }, @@ -375,7 +375,7 @@ func TestLoader_Load(t *testing.T) { { name: "Load a plugin with file which is missing from the manifest", class: plugins.ClassExternal, - cfg: &config.Cfg{ + cfg: &config.PluginManagementCfg{ PluginsAllowUnsigned: []string{"test-datasource"}, Features: featuremgmt.WithFeatures(), }, @@ -391,7 +391,7 @@ func TestLoader_Load(t *testing.T) { { name: "Load an app with includes", class: plugins.ClassExternal, - cfg: &config.Cfg{ + cfg: &config.PluginManagementCfg{ PluginsAllowUnsigned: []string{"test-app"}, Features: featuremgmt.WithFeatures(), }, @@ -443,7 +443,7 @@ func TestLoader_Load(t *testing.T) { { name: "Load a plugin with app sub url set", class: plugins.ClassExternal, - cfg: &config.Cfg{ + cfg: &config.PluginManagementCfg{ DevMode: true, GrafanaAppSubURL: "grafana", Features: featuremgmt.WithFeatures(), @@ -511,10 +511,9 @@ func TestLoader_Load_ExternalRegistration(t *testing.T) { stringPtr := func(s string) *string { return &s } t.Run("Load a plugin with service account registration", func(t *testing.T) { - cfg := &config.Cfg{ + cfg := &config.PluginManagementCfg{ Features: featuremgmt.WithFeatures(featuremgmt.FlagExternalServiceAccounts), PluginsAllowUnsigned: []string{"grafana-test-datasource"}, - AWSAssumeRoleEnabled: true, } pluginPaths := []string{filepath.Join(testDataDir(t), "external-registration")} expected := []*plugins.Plugin{ @@ -567,12 +566,6 @@ func TestLoader_Load_ExternalRegistration(t *testing.T) { backendFactoryProvider.BackendFactoryFunc = func(ctx context.Context, plugin *plugins.Plugin) backendplugin.PluginFactoryFunc { return func(pluginID string, logger log.Logger, env func() []string) (backendplugin.Plugin, error) { require.Equal(t, "grafana-test-datasource", pluginID) - require.Equal(t, []string{ - "GF_VERSION=", "GF_EDITION=", "GF_ENTERPRISE_LICENSE_PATH=", - "GF_ENTERPRISE_APP_URL=", "GF_ENTERPRISE_LICENSE_TEXT=", "GF_APP_URL=", - "GF_PLUGIN_APP_CLIENT_ID=client-id", "GF_PLUGIN_APP_CLIENT_SECRET=secretz", - "GF_INSTANCE_FEATURE_TOGGLES_ENABLE=externalServiceAccounts", - }, env()) return &fakes.FakeBackendPlugin{}, nil } } @@ -607,7 +600,7 @@ func TestLoader_Load_ExternalRegistration(t *testing.T) { func TestLoader_Load_CustomSource(t *testing.T) { t.Run("Load a plugin", func(t *testing.T) { - cfg := &config.Cfg{ + cfg := &config.PluginManagementCfg{ PluginsCDNURLTemplate: "https://cdn.example.com", PluginSettings: setting.PluginSettings{ "grafana-worldmap-panel": {"cdn": "true"}, @@ -685,7 +678,7 @@ func TestLoader_Load_MultiplePlugins(t *testing.T) { t.Run("Load multiple", func(t *testing.T) { tests := []struct { name string - cfg *config.Cfg + cfg *config.PluginManagementCfg pluginPaths []string existingPlugins map[string]struct{} want []*plugins.Plugin @@ -693,7 +686,7 @@ func TestLoader_Load_MultiplePlugins(t *testing.T) { }{ { name: "Load multiple plugins (broken, valid, unsigned)", - cfg: &config.Cfg{ + cfg: &config.PluginManagementCfg{ GrafanaAppURL: "http://localhost:3000", Features: featuremgmt.WithFeatures(), }, @@ -783,14 +776,14 @@ func TestLoader_Load_MultiplePlugins(t *testing.T) { func TestLoader_Load_RBACReady(t *testing.T) { tests := []struct { name string - cfg *config.Cfg + cfg *config.PluginManagementCfg pluginPaths []string existingPlugins map[string]struct{} want []*plugins.Plugin }{ { name: "Load plugin defining one RBAC role", - cfg: &config.Cfg{ + cfg: &config.PluginManagementCfg{ GrafanaAppURL: "http://localhost:3000", Features: featuremgmt.WithFeatures(), }, @@ -911,7 +904,7 @@ func TestLoader_Load_Signature_RootURL(t *testing.T) { reg := fakes.NewFakePluginRegistry() procPrvdr := fakes.NewFakeBackendProcessProvider() procMgr := fakes.NewFakeProcessManager() - cfg := &config.Cfg{GrafanaAppURL: defaultAppURL, Features: featuremgmt.WithFeatures()} + cfg := &config.PluginManagementCfg{GrafanaAppURL: defaultAppURL, Features: featuremgmt.WithFeatures()} l := newLoader(t, cfg, reg, procMgr, procPrvdr, newFakeSignatureErrorTracker()) got, err := l.Load(context.Background(), &fakes.FakePluginSource{ PluginClassFunc: func(ctx context.Context) plugins.Class { @@ -988,7 +981,7 @@ func TestLoader_Load_DuplicatePlugins(t *testing.T) { reg := fakes.NewFakePluginRegistry() procPrvdr := fakes.NewFakeBackendProcessProvider() procMgr := fakes.NewFakeProcessManager() - cfg := &config.Cfg{Features: featuremgmt.WithFeatures()} + cfg := &config.PluginManagementCfg{Features: featuremgmt.WithFeatures()} l := newLoader(t, cfg, reg, procMgr, procPrvdr, newFakeSignatureErrorTracker()) got, err := l.Load(context.Background(), &fakes.FakePluginSource{ PluginClassFunc: func(ctx context.Context) plugins.Class { @@ -1078,7 +1071,7 @@ func TestLoader_Load_SkipUninitializedPlugins(t *testing.T) { } } procMgr := fakes.NewFakeProcessManager() - cfg := &config.Cfg{Features: featuremgmt.WithFeatures()} + cfg := &config.PluginManagementCfg{Features: featuremgmt.WithFeatures()} l := newLoader(t, cfg, reg, procMgr, procPrvdr, newFakeSignatureErrorTracker()) got, err := l.Load(context.Background(), &fakes.FakePluginSource{ PluginClassFunc: func(ctx context.Context) plugins.Class { @@ -1135,7 +1128,7 @@ func TestLoader_AngularClass(t *testing.T) { }, } // if angularDetected = true, it means that the detection has run - l := newLoaderWithOpts(t, &config.Cfg{AngularSupportEnabled: true, Features: featuremgmt.WithFeatures()}, loaderDepOpts{ + l := newLoaderWithOpts(t, &config.PluginManagementCfg{AngularSupportEnabled: true, Features: featuremgmt.WithFeatures()}, loaderDepOpts{ angularInspector: angularinspector.AlwaysAngularFakeInspector, }) p, err := l.Load(context.Background(), fakePluginSource) @@ -1161,10 +1154,10 @@ func TestLoader_Load_Angular(t *testing.T) { } for _, cfgTc := range []struct { name string - cfg *config.Cfg + cfg *config.PluginManagementCfg }{ - {name: "angular support enabled", cfg: &config.Cfg{AngularSupportEnabled: true, Features: featuremgmt.WithFeatures()}}, - {name: "angular support disabled", cfg: &config.Cfg{AngularSupportEnabled: false, Features: featuremgmt.WithFeatures()}}, + {name: "angular support enabled", cfg: &config.PluginManagementCfg{AngularSupportEnabled: true, Features: featuremgmt.WithFeatures()}}, + {name: "angular support disabled", cfg: &config.PluginManagementCfg{AngularSupportEnabled: false, Features: featuremgmt.WithFeatures()}}, } { t.Run(cfgTc.name, func(t *testing.T) { for _, tc := range []struct { @@ -1211,20 +1204,20 @@ func TestLoader_HideAngularDeprecation(t *testing.T) { } for _, tc := range []struct { name string - cfg *config.Cfg + cfg *config.PluginManagementCfg expHideAngularDeprecation bool }{ - {name: "with plugin id in HideAngularDeprecation list", cfg: &config.Cfg{ + {name: "with plugin id in HideAngularDeprecation list", cfg: &config.PluginManagementCfg{ AngularSupportEnabled: true, HideAngularDeprecation: []string{"one-app", "two-panel", "test-datasource", "three-datasource"}, Features: featuremgmt.WithFeatures(), }, expHideAngularDeprecation: true}, - {name: "without plugin id in HideAngularDeprecation list", cfg: &config.Cfg{ + {name: "without plugin id in HideAngularDeprecation list", cfg: &config.PluginManagementCfg{ AngularSupportEnabled: true, HideAngularDeprecation: []string{"one-app", "two-panel", "three-datasource"}, Features: featuremgmt.WithFeatures(), }, expHideAngularDeprecation: false}, - {name: "with empty HideAngularDeprecation", cfg: &config.Cfg{ + {name: "with empty HideAngularDeprecation", cfg: &config.PluginManagementCfg{ AngularSupportEnabled: true, HideAngularDeprecation: nil, Features: featuremgmt.WithFeatures(), @@ -1315,7 +1308,7 @@ func TestLoader_Load_NestedPlugins(t *testing.T) { procPrvdr := fakes.NewFakeBackendProcessProvider() procMgr := fakes.NewFakeProcessManager() reg := fakes.NewFakePluginRegistry() - cfg := &config.Cfg{Features: featuremgmt.WithFeatures()} + cfg := &config.PluginManagementCfg{Features: featuremgmt.WithFeatures()} l := newLoader(t, cfg, reg, procMgr, procPrvdr, newFakeSignatureErrorTracker()) got, err := l.Load(context.Background(), &fakes.FakePluginSource{ @@ -1492,7 +1485,7 @@ func TestLoader_Load_NestedPlugins(t *testing.T) { reg := fakes.NewFakePluginRegistry() procPrvdr := fakes.NewFakeBackendProcessProvider() procMgr := fakes.NewFakeProcessManager() - cfg := &config.Cfg{Features: featuremgmt.WithFeatures()} + cfg := &config.PluginManagementCfg{Features: featuremgmt.WithFeatures()} l := newLoader(t, cfg, reg, procMgr, procPrvdr, newFakeSignatureErrorTracker()) got, err := l.Load(context.Background(), &fakes.FakePluginSource{ PluginClassFunc: func(ctx context.Context) plugins.Class { @@ -1523,11 +1516,10 @@ type loaderDepOpts struct { backendFactoryProvider plugins.BackendFactoryProvider } -func newLoader(t *testing.T, cfg *config.Cfg, reg registry.Service, proc process.Manager, +func newLoader(t *testing.T, cfg *config.PluginManagementCfg, reg registry.Service, proc process.Manager, backendFactory plugins.BackendFactoryProvider, sigErrTracker pluginerrs.SignatureErrorTracker, ) *Loader { assets := assetpath.ProvideService(cfg, pluginscdn.ProvideService(cfg)) - lic := fakes.NewFakeLicensingService() angularInspector := angularinspector.NewStaticInspector() terminate, err := pipeline.ProvideTerminationStage(cfg, reg, proc) @@ -1537,13 +1529,12 @@ func newLoader(t *testing.T, cfg *config.Cfg, reg registry.Service, proc process finder.NewLocalFinder(false, featuremgmt.WithFeatures()), reg), pipeline.ProvideBootstrapStage(cfg, signature.DefaultCalculator(cfg), assets), pipeline.ProvideValidationStage(cfg, signature.NewValidator(signature.NewUnsignedAuthorizer(cfg)), angularInspector, sigErrTracker), - pipeline.ProvideInitializationStage(cfg, reg, lic, backendFactory, proc, &fakes.FakeAuthService{}, fakes.NewFakeRoleRegistry()), + pipeline.ProvideInitializationStage(cfg, reg, backendFactory, proc, &fakes.FakeAuthService{}, fakes.NewFakeRoleRegistry(), fakes.NewFakePluginEnvProvider()), terminate) } -func newLoaderWithOpts(t *testing.T, cfg *config.Cfg, opts loaderDepOpts) *Loader { +func newLoaderWithOpts(t *testing.T, cfg *config.PluginManagementCfg, opts loaderDepOpts) *Loader { assets := assetpath.ProvideService(cfg, pluginscdn.ProvideService(cfg)) - lic := fakes.NewFakeLicensingService() reg := fakes.NewFakePluginRegistry() proc := fakes.NewFakeProcessManager() @@ -1570,7 +1561,7 @@ func newLoaderWithOpts(t *testing.T, cfg *config.Cfg, opts loaderDepOpts) *Loade finder.NewLocalFinder(false, featuremgmt.WithFeatures()), reg), pipeline.ProvideBootstrapStage(cfg, signature.DefaultCalculator(cfg), assets), pipeline.ProvideValidationStage(cfg, signature.NewValidator(signature.NewUnsignedAuthorizer(cfg)), angularInspector, sigErrTracker), - pipeline.ProvideInitializationStage(cfg, reg, lic, backendFactoryProvider, proc, authServiceRegistry, fakes.NewFakeRoleRegistry()), + pipeline.ProvideInitializationStage(cfg, reg, backendFactoryProvider, proc, authServiceRegistry, fakes.NewFakeRoleRegistry(), fakes.NewFakePluginEnvProvider()), terminate) } diff --git a/pkg/services/pluginsintegration/pipeline/pipeline.go b/pkg/services/pluginsintegration/pipeline/pipeline.go index 6ed3cbe622..b2b59b79b4 100644 --- a/pkg/services/pluginsintegration/pipeline/pipeline.go +++ b/pkg/services/pluginsintegration/pipeline/pipeline.go @@ -21,7 +21,7 @@ import ( "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginerrs" ) -func ProvideDiscoveryStage(cfg *config.Cfg, pf finder.Finder, pr registry.Service) *discovery.Discovery { +func ProvideDiscoveryStage(cfg *config.PluginManagementCfg, pf finder.Finder, pr registry.Service) *discovery.Discovery { return discovery.New(cfg, discovery.Opts{ FindFunc: pf.Find, FindFilterFuncs: []discovery.FindFilterFunc{ @@ -41,14 +41,14 @@ func ProvideDiscoveryStage(cfg *config.Cfg, pf finder.Finder, pr registry.Servic }) } -func ProvideBootstrapStage(cfg *config.Cfg, sc plugins.SignatureCalculator, a *assetpath.Service) *bootstrap.Bootstrap { +func ProvideBootstrapStage(cfg *config.PluginManagementCfg, sc plugins.SignatureCalculator, a *assetpath.Service) *bootstrap.Bootstrap { return bootstrap.New(cfg, bootstrap.Opts{ ConstructFunc: bootstrap.DefaultConstructFunc(sc, a), DecorateFuncs: bootstrap.DefaultDecorateFuncs(cfg), }) } -func ProvideValidationStage(cfg *config.Cfg, sv signature.Validator, ai angularinspector.Inspector, +func ProvideValidationStage(cfg *config.PluginManagementCfg, sv signature.Validator, ai angularinspector.Inspector, et pluginerrs.SignatureErrorTracker) *validation.Validate { return validation.New(cfg, validation.Opts{ ValidateFuncs: []validation.ValidateFunc{ @@ -59,13 +59,13 @@ func ProvideValidationStage(cfg *config.Cfg, sv signature.Validator, ai angulari }) } -func ProvideInitializationStage(cfg *config.Cfg, pr registry.Service, l plugins.Licensing, - bp plugins.BackendFactoryProvider, pm process.Manager, externalServiceRegistry auth.ExternalServiceRegistry, - roleRegistry plugins.RoleRegistry) *initialization.Initialize { +func ProvideInitializationStage(cfg *config.PluginManagementCfg, pr registry.Service, bp plugins.BackendFactoryProvider, + pm process.Manager, externalServiceRegistry auth.ExternalServiceRegistry, + roleRegistry plugins.RoleRegistry, pluginEnvProvider envvars.Provider) *initialization.Initialize { return initialization.New(cfg, initialization.Opts{ InitializeFuncs: []initialization.InitializeFunc{ ExternalServiceRegistrationStep(cfg, externalServiceRegistry), - initialization.BackendClientInitStep(envvars.NewProvider(cfg, l), bp), + initialization.BackendClientInitStep(pluginEnvProvider, bp), initialization.PluginRegistrationStep(pr), initialization.BackendProcessStartStep(pm), RegisterPluginRolesStep(roleRegistry), @@ -74,7 +74,7 @@ func ProvideInitializationStage(cfg *config.Cfg, pr registry.Service, l plugins. }) } -func ProvideTerminationStage(cfg *config.Cfg, pr registry.Service, pm process.Manager) (*termination.Terminate, error) { +func ProvideTerminationStage(cfg *config.PluginManagementCfg, pr registry.Service, pm process.Manager) (*termination.Terminate, error) { return termination.New(cfg, termination.Opts{ TerminateFuncs: []termination.TerminateFunc{ termination.BackendProcessTerminatorStep(pm), diff --git a/pkg/services/pluginsintegration/pipeline/steps.go b/pkg/services/pluginsintegration/pipeline/steps.go index 2685a634c6..d5408d0224 100644 --- a/pkg/services/pluginsintegration/pipeline/steps.go +++ b/pkg/services/pluginsintegration/pipeline/steps.go @@ -21,17 +21,17 @@ import ( // ExternalServiceRegistration implements an InitializeFunc for registering external services. type ExternalServiceRegistration struct { - cfg *config.Cfg + cfg *config.PluginManagementCfg externalServiceRegistry auth.ExternalServiceRegistry log log.Logger } // ExternalServiceRegistrationStep returns an InitializeFunc for registering external services. -func ExternalServiceRegistrationStep(cfg *config.Cfg, externalServiceRegistry auth.ExternalServiceRegistry) initialization.InitializeFunc { +func ExternalServiceRegistrationStep(cfg *config.PluginManagementCfg, externalServiceRegistry auth.ExternalServiceRegistry) initialization.InitializeFunc { return newExternalServiceRegistration(cfg, externalServiceRegistry).Register } -func newExternalServiceRegistration(cfg *config.Cfg, serviceRegistry auth.ExternalServiceRegistry) *ExternalServiceRegistration { +func newExternalServiceRegistration(cfg *config.PluginManagementCfg, serviceRegistry auth.ExternalServiceRegistry) *ExternalServiceRegistration { return &ExternalServiceRegistration{ cfg: cfg, externalServiceRegistry: serviceRegistry, @@ -128,11 +128,11 @@ func (v *SignatureValidation) Validate(ctx context.Context, p *plugins.Plugin) e // DisablePlugins is a filter step that will filter out any configured plugins type DisablePlugins struct { log log.Logger - cfg *config.Cfg + cfg *config.PluginManagementCfg } // NewDisablePluginsStep returns a new DisablePlugins. -func NewDisablePluginsStep(cfg *config.Cfg) *DisablePlugins { +func NewDisablePluginsStep(cfg *config.PluginManagementCfg) *DisablePlugins { return &DisablePlugins{ cfg: cfg, log: log.New("plugins.disable"), @@ -164,11 +164,11 @@ func (c *DisablePlugins) Filter(bundles []*plugins.FoundBundle) ([]*plugins.Foun // AsExternal is a filter step that will skip loading a core plugin to use an external one. type AsExternal struct { log log.Logger - cfg *config.Cfg + cfg *config.PluginManagementCfg } // NewDisablePluginsStep returns a new DisablePlugins. -func NewAsExternalStep(cfg *config.Cfg) *AsExternal { +func NewAsExternalStep(cfg *config.PluginManagementCfg) *AsExternal { return &AsExternal{ cfg: cfg, log: log.New("plugins.asExternal"), diff --git a/pkg/services/pluginsintegration/pipeline/steps_test.go b/pkg/services/pluginsintegration/pipeline/steps_test.go index e3431d5ced..09ac6c41df 100644 --- a/pkg/services/pluginsintegration/pipeline/steps_test.go +++ b/pkg/services/pluginsintegration/pipeline/steps_test.go @@ -14,7 +14,7 @@ import ( ) func TestSkipPlugins(t *testing.T) { - cfg := &config.Cfg{ + cfg := &config.PluginManagementCfg{ DisablePlugins: []string{"plugin1", "plugin2"}, } s := NewDisablePluginsStep(cfg) @@ -68,7 +68,7 @@ func TestAsExternal(t *testing.T) { } t.Run("should skip a core plugin", func(t *testing.T) { - cfg := &config.Cfg{ + cfg := &config.PluginManagementCfg{ Features: featuremgmt.WithFeatures(featuremgmt.FlagExternalCorePlugins), PluginSettings: setting.PluginSettings{ "plugin1": map[string]string{ diff --git a/pkg/services/pluginsintegration/pluginconfig/config.go b/pkg/services/pluginsintegration/pluginconfig/config.go new file mode 100644 index 0000000000..0f2097341c --- /dev/null +++ b/pkg/services/pluginsintegration/pluginconfig/config.go @@ -0,0 +1,144 @@ +package pluginconfig + +import ( + "fmt" + "strings" + + "github.com/grafana/grafana/pkg/util" + + "github.com/grafana/grafana-azure-sdk-go/azsettings" + + "github.com/grafana/grafana/pkg/plugins/config" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/setting" +) + +// ProvidePluginManagementConfig returns a new config.PluginManagementCfg. +// It is used to provide configuration to Grafana's implementation of the plugin management system. +func ProvidePluginManagementConfig(cfg *setting.Cfg, settingProvider setting.Provider, features featuremgmt.FeatureToggles) (*config.PluginManagementCfg, error) { + plugins := settingProvider.Section("plugins") + allowedUnsigned := cfg.PluginsAllowUnsigned + if len(plugins.KeyValue("allow_loading_unsigned_plugins").Value()) > 0 { + allowedUnsigned = strings.Split(plugins.KeyValue("allow_loading_unsigned_plugins").Value(), ",") + } + + tracingCfg, err := newTracingCfg(cfg) + if err != nil { + return nil, fmt.Errorf("new opentelemetry cfg: %w", err) + } + + return config.NewPluginManagementCfg( + settingProvider.KeyValue("", "app_mode").MustBool(cfg.Env == setting.Dev), + cfg.PluginsPath, + extractPluginSettings(settingProvider), + allowedUnsigned, + cfg.PluginsCDNURLTemplate, + cfg.AppURL, + cfg.AppSubURL, + tracingCfg, + features, + cfg.AngularSupportEnabled, + cfg.GrafanaComURL, + cfg.DisablePlugins, + cfg.HideAngularDeprecation, + cfg.ForwardHostEnvVars, + ), nil +} + +// PluginInstanceCfg contains the configuration for a plugin instance. +// It is used to provide configuration to the plugin instance either via env vars or via each plugin request. +type PluginInstanceCfg struct { + GrafanaAppURL string + Features featuremgmt.FeatureToggles + + Tracing config.Tracing + + PluginSettings setting.PluginSettings + + AWSAllowedAuthProviders []string + AWSAssumeRoleEnabled bool + AWSExternalId string + AWSSessionDuration string + AWSListMetricsPageLimit string + AWSForwardSettingsPlugins []string + + Azure *azsettings.AzureSettings + AzureAuthEnabled bool + + ProxySettings setting.SecureSocksDSProxySettings + + GrafanaVersion string + + ConcurrentQueryCount int + + UserFacingDefaultError string + + DataProxyRowLimit int64 + + SQLDatasourceMaxOpenConnsDefault int + SQLDatasourceMaxIdleConnsDefault int + SQLDatasourceMaxConnLifetimeDefault int +} + +// ProvidePluginInstanceConfig returns a new PluginInstanceCfg. +func ProvidePluginInstanceConfig(cfg *setting.Cfg, settingProvider setting.Provider, features featuremgmt.FeatureToggles) (*PluginInstanceCfg, error) { + aws := settingProvider.Section("aws") + allowedAuth := cfg.AWSAllowedAuthProviders + if len(aws.KeyValue("allowed_auth_providers").Value()) > 0 { + allowedAuth = util.SplitString(aws.KeyValue("allowed_auth_providers").Value()) + } + awsForwardSettingsPlugins := cfg.AWSForwardSettingsPlugins + if len(aws.KeyValue("forward_settings_to_plugins").Value()) > 0 { + awsForwardSettingsPlugins = util.SplitString(aws.KeyValue("forward_settings_to_plugins").Value()) + } + + tracingCfg, err := newTracingCfg(cfg) + if err != nil { + return nil, fmt.Errorf("new opentelemetry cfg: %w", err) + } + + if cfg.Azure == nil { + cfg.Azure = &azsettings.AzureSettings{} + } + + return &PluginInstanceCfg{ + GrafanaAppURL: cfg.AppURL, + Features: features, + Tracing: tracingCfg, + PluginSettings: extractPluginSettings(settingProvider), + AWSAllowedAuthProviders: allowedAuth, + AWSAssumeRoleEnabled: aws.KeyValue("assume_role_enabled").MustBool(cfg.AWSAssumeRoleEnabled), + AWSExternalId: aws.KeyValue("external_id").Value(), + AWSSessionDuration: aws.KeyValue("session_duration").Value(), + AWSListMetricsPageLimit: aws.KeyValue("list_metrics_page_limit").Value(), + AWSForwardSettingsPlugins: awsForwardSettingsPlugins, + Azure: cfg.Azure, + AzureAuthEnabled: cfg.Azure.AzureAuthEnabled, + ProxySettings: cfg.SecureSocksDSProxy, + GrafanaVersion: cfg.BuildVersion, + ConcurrentQueryCount: cfg.ConcurrentQueryCount, + UserFacingDefaultError: cfg.UserFacingDefaultError, + DataProxyRowLimit: cfg.DataProxyRowLimit, + SQLDatasourceMaxOpenConnsDefault: cfg.SqlDatasourceMaxOpenConnsDefault, + SQLDatasourceMaxIdleConnsDefault: cfg.SqlDatasourceMaxIdleConnsDefault, + SQLDatasourceMaxConnLifetimeDefault: cfg.SqlDatasourceMaxConnLifetimeDefault, + }, nil +} + +func extractPluginSettings(settingProvider setting.Provider) setting.PluginSettings { + ps := setting.PluginSettings{} + for sectionName, sectionCopy := range settingProvider.Current() { + if !strings.HasPrefix(sectionName, "plugin.") { + continue + } + // Calling Current() returns a redacted version of section. We need to replace the map values with the unredacted values. + section := settingProvider.Section(sectionName) + for k := range sectionCopy { + sectionCopy[k] = section.KeyValue(k).MustString("") + } + pluginID := strings.Replace(sectionName, "plugin.", "", 1) + ps[pluginID] = sectionCopy + } + + return ps +} diff --git a/pkg/services/pluginsintegration/config/config_test.go b/pkg/services/pluginsintegration/pluginconfig/config_test.go similarity index 97% rename from pkg/services/pluginsintegration/config/config_test.go rename to pkg/services/pluginsintegration/pluginconfig/config_test.go index 3ee6eddf2c..e5bebbc93a 100644 --- a/pkg/services/pluginsintegration/config/config_test.go +++ b/pkg/services/pluginsintegration/pluginconfig/config_test.go @@ -1,4 +1,4 @@ -package config +package pluginconfig import ( "testing" @@ -18,7 +18,7 @@ func TestPluginSettings(t *testing.T) { [plugin.test-datasource] foo = 5m bar = something - + [plugin.secret-plugin] secret_key = secret normal_key = not a secret`)) diff --git a/pkg/services/pluginsintegration/pluginconfig/envvars.go b/pkg/services/pluginsintegration/pluginconfig/envvars.go new file mode 100644 index 0000000000..bb8c2a0360 --- /dev/null +++ b/pkg/services/pluginsintegration/pluginconfig/envvars.go @@ -0,0 +1,178 @@ +package pluginconfig + +import ( + "context" + "fmt" + "os" + "slices" + "strconv" + "strings" + + "github.com/grafana/grafana-aws-sdk/pkg/awsds" + "github.com/grafana/grafana-azure-sdk-go/azsettings" + "github.com/grafana/grafana-plugin-sdk-go/backend/proxy" + + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/plugins/envvars" + "github.com/grafana/grafana/pkg/services/featuremgmt" +) + +var _ envvars.Provider = (*EnvVarsProvider)(nil) + +type EnvVarsProvider struct { + cfg *PluginInstanceCfg + license plugins.Licensing +} + +func NewEnvVarsProvider(cfg *PluginInstanceCfg, license plugins.Licensing) *EnvVarsProvider { + return &EnvVarsProvider{ + cfg: cfg, + license: license, + } +} + +func (p *EnvVarsProvider) PluginEnvVars(ctx context.Context, plugin *plugins.Plugin) []string { + hostEnv := []string{ + envVar("GF_VERSION", p.cfg.GrafanaVersion), + } + + if p.license != nil { + hostEnv = append( + hostEnv, + envVar("GF_EDITION", p.license.Edition()), + envVar("GF_ENTERPRISE_LICENSE_PATH", p.license.Path()), + envVar("GF_ENTERPRISE_APP_URL", p.license.AppURL()), + ) + hostEnv = append(hostEnv, p.license.Environment()...) + } + + if plugin.ExternalService != nil { + hostEnv = append( + hostEnv, + envVar("GF_APP_URL", p.cfg.GrafanaAppURL), + envVar("GF_PLUGIN_APP_CLIENT_ID", plugin.ExternalService.ClientID), + envVar("GF_PLUGIN_APP_CLIENT_SECRET", plugin.ExternalService.ClientSecret), + ) + if plugin.ExternalService.PrivateKey != "" { + hostEnv = append(hostEnv, envVar("GF_PLUGIN_APP_PRIVATE_KEY", plugin.ExternalService.PrivateKey)) + } + } + + hostEnv = append(hostEnv, p.featureToggleEnableVars(ctx)...) + hostEnv = append(hostEnv, p.awsEnvVars(plugin.PluginID())...) + hostEnv = append(hostEnv, p.secureSocksProxyEnvVars()...) + hostEnv = append(hostEnv, azsettings.WriteToEnvStr(p.cfg.Azure)...) + hostEnv = append(hostEnv, p.tracingEnvVars(plugin)...) + hostEnv = append(hostEnv, p.pluginSettingsEnvVars(plugin.PluginID())...) + + // If SkipHostEnvVars is enabled, get some allowed variables from the current process and pass + // them down to the plugin. If the flag is not set, do not add anything else because ALL env vars + // from the current process (os.Environ()) will be forwarded to the plugin's process by go-plugin + if plugin.SkipHostEnvVars { + hostEnv = append(hostEnv, envvars.PermittedHostEnvVars()...) + } + + return hostEnv +} + +func (p *EnvVarsProvider) featureToggleEnableVars(ctx context.Context) []string { + var variables []string // an array is used for consistency and keep the logic simpler for no features case + + if p.cfg.Features == nil { + return variables + } + + enabledFeatures := p.cfg.Features.GetEnabled(ctx) + if len(enabledFeatures) > 0 { + features := make([]string, 0, len(enabledFeatures)) + for feat := range enabledFeatures { + features = append(features, feat) + } + variables = append(variables, envVar("GF_INSTANCE_FEATURE_TOGGLES_ENABLE", strings.Join(features, ","))) + } + + return variables +} + +func (p *EnvVarsProvider) awsEnvVars(pluginID string) []string { + if !slices.Contains[[]string, string](p.cfg.AWSForwardSettingsPlugins, pluginID) { + return []string{} + } + + var variables []string + if !p.cfg.AWSAssumeRoleEnabled { + variables = append(variables, envVar(awsds.AssumeRoleEnabledEnvVarKeyName, "false")) + } + if len(p.cfg.AWSAllowedAuthProviders) > 0 { + variables = append(variables, envVar(awsds.AllowedAuthProvidersEnvVarKeyName, strings.Join(p.cfg.AWSAllowedAuthProviders, ","))) + } + if p.cfg.AWSExternalId != "" { + variables = append(variables, envVar(awsds.GrafanaAssumeRoleExternalIdKeyName, p.cfg.AWSExternalId)) + } + if p.cfg.AWSSessionDuration != "" { + variables = append(variables, envVar(awsds.SessionDurationEnvVarKeyName, p.cfg.AWSSessionDuration)) + } + if p.cfg.AWSListMetricsPageLimit != "" { + variables = append(variables, envVar(awsds.ListMetricsPageLimitKeyName, p.cfg.AWSListMetricsPageLimit)) + } + + return variables +} + +func (p *EnvVarsProvider) secureSocksProxyEnvVars() []string { + if p.cfg.ProxySettings.Enabled { + return []string{ + envVar(proxy.PluginSecureSocksProxyClientCert, p.cfg.ProxySettings.ClientCert), + envVar(proxy.PluginSecureSocksProxyClientKey, p.cfg.ProxySettings.ClientKey), + envVar(proxy.PluginSecureSocksProxyRootCACert, p.cfg.ProxySettings.RootCA), + envVar(proxy.PluginSecureSocksProxyProxyAddress, p.cfg.ProxySettings.ProxyAddress), + envVar(proxy.PluginSecureSocksProxyServerName, p.cfg.ProxySettings.ServerName), + envVar(proxy.PluginSecureSocksProxyEnabled, strconv.FormatBool(p.cfg.ProxySettings.Enabled)), + envVar(proxy.PluginSecureSocksProxyAllowInsecure, strconv.FormatBool(p.cfg.ProxySettings.AllowInsecure)), + } + } + return nil +} + +func (p *EnvVarsProvider) tracingEnvVars(plugin *plugins.Plugin) []string { + pluginTracingEnabled := p.cfg.Features.IsEnabledGlobally(featuremgmt.FlagEnablePluginsTracingByDefault) + if v, exists := p.cfg.PluginSettings[plugin.ID]["tracing"]; exists && !pluginTracingEnabled { + pluginTracingEnabled = v == "true" + } + + if !p.cfg.Tracing.IsEnabled() || !pluginTracingEnabled { + return nil + } + + vars := []string{ + envVar("GF_INSTANCE_OTLP_ADDRESS", p.cfg.Tracing.OpenTelemetry.Address), + envVar("GF_INSTANCE_OTLP_PROPAGATION", p.cfg.Tracing.OpenTelemetry.Propagation), + envVar("GF_INSTANCE_OTLP_SAMPLER_TYPE", p.cfg.Tracing.OpenTelemetry.Sampler), + fmt.Sprintf("GF_INSTANCE_OTLP_SAMPLER_PARAM=%.6f", p.cfg.Tracing.OpenTelemetry.SamplerParam), + envVar("GF_INSTANCE_OTLP_SAMPLER_REMOTE_URL", p.cfg.Tracing.OpenTelemetry.SamplerRemoteURL), + } + if plugin.Info.Version != "" { + vars = append(vars, fmt.Sprintf("GF_PLUGIN_VERSION=%s", plugin.Info.Version)) + } + return vars +} + +func (p *EnvVarsProvider) pluginSettingsEnvVars(pluginID string) []string { + const customConfigPrefix = "GF_PLUGIN" + var env []string + for k, v := range p.cfg.PluginSettings[pluginID] { + if k == "path" || strings.ToLower(k) == "id" { + continue + } + key := fmt.Sprintf("%s_%s", customConfigPrefix, strings.ToUpper(k)) + if value := os.Getenv(key); value != "" { + v = value + } + env = append(env, fmt.Sprintf("%s=%s", key, v)) + } + return env +} + +func envVar(key, value string) string { + return fmt.Sprintf("%s=%s", key, value) +} diff --git a/pkg/plugins/envvars/envvars_test.go b/pkg/services/pluginsintegration/pluginconfig/envvars_test.go similarity index 52% rename from pkg/plugins/envvars/envvars_test.go rename to pkg/services/pluginsintegration/pluginconfig/envvars_test.go index ed775d0ff7..a3bf9025df 100644 --- a/pkg/plugins/envvars/envvars_test.go +++ b/pkg/services/pluginsintegration/pluginconfig/envvars_test.go @@ -1,10 +1,12 @@ -package envvars +package pluginconfig import ( "context" "strings" "testing" + "gopkg.in/ini.v1" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -13,13 +15,14 @@ import ( "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/auth" "github.com/grafana/grafana/pkg/plugins/config" + "github.com/grafana/grafana/pkg/plugins/envvars" "github.com/grafana/grafana/pkg/plugins/manager/fakes" "github.com/grafana/grafana/pkg/plugins/plugindef" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/setting" ) -func TestInitializer_envVars(t *testing.T) { +func TestPluginEnvVarsProvider_PluginEnvVars(t *testing.T) { t.Run("backend datasource with license", func(t *testing.T) { p := &plugins.Plugin{ JSONData: plugins.JSONData{ @@ -34,27 +37,29 @@ func TestInitializer_envVars(t *testing.T) { LicenseAppURL: "https://myorg.com/", } - envVarsProvider := NewProvider(&config.Cfg{ + cfg := &PluginInstanceCfg{ PluginSettings: map[string]map[string]string{ "test": { "custom_env_var": "customVal", }, }, AWSAssumeRoleEnabled: true, - }, licensing) + Features: featuremgmt.WithFeatures(), + } - envVars := envVarsProvider.Get(context.Background(), p) + provider := NewEnvVarsProvider(cfg, licensing) + envVars := provider.PluginEnvVars(context.Background(), p) 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_APP_URL=https://myorg.com/", envVars[4]) - assert.Equal(t, "GF_ENTERPRISE_LICENSE_TEXT=token", envVars[5]) + assert.Equal(t, "GF_VERSION=", envVars[0]) + assert.Equal(t, "GF_EDITION=test", envVars[1]) + assert.Equal(t, "GF_ENTERPRISE_LICENSE_PATH=/path/to/ent/license", envVars[2]) + assert.Equal(t, "GF_ENTERPRISE_APP_URL=https://myorg.com/", envVars[3]) + assert.Equal(t, "GF_ENTERPRISE_LICENSE_TEXT=token", envVars[4]) + assert.Equal(t, "GF_PLUGIN_CUSTOM_ENV_VAR=customVal", envVars[5]) }) } -func TestInitializer_skipHostEnvVars(t *testing.T) { +func TestPluginEnvVarsProvider_skipHostEnvVars(t *testing.T) { const ( envVarName = "HTTP_PROXY" envVarValue = "lorem ipsum" @@ -69,8 +74,12 @@ func TestInitializer_skipHostEnvVars(t *testing.T) { } t.Run("without FlagPluginsSkipHostEnvVars should not populate host env vars", func(t *testing.T) { - envVarsProvider := NewProvider(&config.Cfg{Features: featuremgmt.WithFeatures()}, nil) - envVars := envVarsProvider.Get(context.Background(), p) + cfg := setting.NewCfg() + pCfg, err := ProvidePluginInstanceConfig(cfg, setting.ProvideProvider(cfg), featuremgmt.WithFeatures()) + require.NoError(t, err) + + provider := NewEnvVarsProvider(pCfg, nil) + envVars := provider.PluginEnvVars(context.Background(), p) // We want to test that the envvars.Provider does not add any of the host env vars. // When starting the plugin via go-plugin, ALL host env vars will be added by go-plugin, @@ -80,21 +89,22 @@ func TestInitializer_skipHostEnvVars(t *testing.T) { }) t.Run("with SkipHostEnvVars = true", func(t *testing.T) { - p := &plugins.Plugin{ - JSONData: plugins.JSONData{ID: "test"}, - SkipHostEnvVars: true, - } - envVarsProvider := NewProvider(&config.Cfg{}, nil) + p.SkipHostEnvVars = true + + cfg := setting.NewCfg() + pCfg, err := ProvidePluginInstanceConfig(cfg, setting.ProvideProvider(cfg), featuremgmt.WithFeatures()) + require.NoError(t, err) + provider := NewEnvVarsProvider(pCfg, nil) t.Run("should populate allowed host env vars", func(t *testing.T) { // Set all allowed variables - for _, ev := range allowedHostEnvVarNames { + for _, ev := range envvars.PermittedHostEnvVarNames() { t.Setenv(ev, envVarValue) } - envVars := envVarsProvider.Get(context.Background(), p) + envVars := provider.PluginEnvVars(context.Background(), p) // Test against each variable - for _, expEvName := range allowedHostEnvVarNames { + for _, expEvName := range envvars.PermittedHostEnvVarNames() { gotEvValue, ok := getEnvVarWithExists(envVars, expEvName) require.True(t, ok, "host env var should be present") require.Equal(t, envVarValue, gotEvValue) @@ -103,20 +113,20 @@ func TestInitializer_skipHostEnvVars(t *testing.T) { t.Run("should not populate host env vars that aren't allowed", func(t *testing.T) { // Set all allowed variables - for _, ev := range allowedHostEnvVarNames { + for _, ev := range envvars.PermittedHostEnvVarNames() { t.Setenv(ev, envVarValue) } // ...and an extra one, which should not leak const superSecretEnvVariableName = "SUPER_SECRET_VALUE" t.Setenv(superSecretEnvVariableName, "01189998819991197253") - envVars := envVarsProvider.Get(context.Background(), p) + envVars := provider.PluginEnvVars(context.Background(), p) // Super secret should not leak _, ok := getEnvVarWithExists(envVars, superSecretEnvVariableName) require.False(t, ok, "super secret env var should not be leaked") // Everything else should be present - for _, expEvName := range allowedHostEnvVarNames { + for _, expEvName := range envvars.PermittedHostEnvVarNames() { var gotEvValue string gotEvValue, ok = getEnvVarWithExists(envVars, expEvName) require.True(t, ok, "host env var should be present") @@ -126,7 +136,7 @@ func TestInitializer_skipHostEnvVars(t *testing.T) { }) } -func TestInitializer_tracingEnvironmentVariables(t *testing.T) { +func TestPluginEnvVarsProvider_tracingEnvironmentVariables(t *testing.T) { const pluginID = "plugin_id" defaultPlugin := &plugins.Plugin{ @@ -197,54 +207,63 @@ func TestInitializer_tracingEnvironmentVariables(t *testing.T) { for _, tc := range []struct { name string - cfg *config.Cfg + cfg *PluginInstanceCfg plugin *plugins.Plugin exp func(t *testing.T, envVars []string) }{ { name: "otel not configured", - cfg: &config.Cfg{ - Tracing: config.Tracing{}, + cfg: &PluginInstanceCfg{ AWSAssumeRoleEnabled: false, + Tracing: config.Tracing{}, + Features: featuremgmt.WithFeatures(), }, plugin: defaultPlugin, exp: expNoTracing, }, { name: "otel not configured but plugin-tracing enabled", - cfg: &config.Cfg{ - Tracing: config.Tracing{}, + cfg: &PluginInstanceCfg{ PluginSettings: map[string]map[string]string{pluginID: {"tracing": "true"}}, + Tracing: config.Tracing{}, + Features: featuremgmt.WithFeatures(), }, plugin: defaultPlugin, exp: expNoTracing, }, { name: "otlp no propagation plugin enabled", - cfg: &config.Cfg{ - Tracing: config.Tracing{ - OpenTelemetry: defaultOTelCfg, - }, + cfg: &PluginInstanceCfg{ PluginSettings: map[string]map[string]string{ pluginID: {"tracing": "true"}, }, + Tracing: config.Tracing{ + OpenTelemetry: defaultOTelCfg, + }, + Features: featuremgmt.WithFeatures(), }, + plugin: defaultPlugin, exp: expDefaultOtlp, }, { name: "otlp no propagation disabled by default", - cfg: &config.Cfg{ + cfg: &PluginInstanceCfg{ Tracing: config.Tracing{ OpenTelemetry: defaultOTelCfg, }, + Features: featuremgmt.WithFeatures(), }, plugin: defaultPlugin, exp: expNoTracing, }, { name: "otlp propagation plugin enabled", - cfg: &config.Cfg{ + cfg: &PluginInstanceCfg{ + PluginSettings: map[string]map[string]string{ + pluginID: {"tracing": "true"}, + }, + AWSAssumeRoleEnabled: true, Tracing: config.Tracing{ OpenTelemetry: config.OpenTelemetryCfg{ Address: "127.0.0.1:4317", @@ -256,27 +275,28 @@ func TestInitializer_tracingEnvironmentVariables(t *testing.T) { SamplerRemoteURL: "", }, }, - PluginSettings: map[string]map[string]string{ - pluginID: {"tracing": "true"}, - }, - AWSAssumeRoleEnabled: true, + Features: featuremgmt.WithFeatures(), }, plugin: defaultPlugin, exp: func(t *testing.T, envVars []string) { assert.Len(t, envVars, 8) - assert.Equal(t, "GF_PLUGIN_TRACING=true", envVars[0]) - assert.Equal(t, "GF_VERSION=", envVars[1]) - assert.Equal(t, "GF_INSTANCE_OTLP_ADDRESS=127.0.0.1:4317", envVars[2]) - assert.Equal(t, "GF_INSTANCE_OTLP_PROPAGATION=w3c", envVars[3]) - assert.Equal(t, "GF_INSTANCE_OTLP_SAMPLER_TYPE=", envVars[4]) - assert.Equal(t, "GF_INSTANCE_OTLP_SAMPLER_PARAM=1.000000", envVars[5]) - assert.Equal(t, "GF_INSTANCE_OTLP_SAMPLER_REMOTE_URL=", envVars[6]) - assert.Equal(t, "GF_PLUGIN_VERSION=1.0.0", envVars[7]) + assert.Equal(t, "GF_VERSION=", envVars[0]) + assert.Equal(t, "GF_INSTANCE_OTLP_ADDRESS=127.0.0.1:4317", envVars[1]) + assert.Equal(t, "GF_INSTANCE_OTLP_PROPAGATION=w3c", envVars[2]) + assert.Equal(t, "GF_INSTANCE_OTLP_SAMPLER_TYPE=", envVars[3]) + assert.Equal(t, "GF_INSTANCE_OTLP_SAMPLER_PARAM=1.000000", envVars[4]) + assert.Equal(t, "GF_INSTANCE_OTLP_SAMPLER_REMOTE_URL=", envVars[5]) + assert.Equal(t, "GF_PLUGIN_VERSION=1.0.0", envVars[6]) + assert.Equal(t, "GF_PLUGIN_TRACING=true", envVars[7]) }, }, { name: "otlp enabled composite propagation", - cfg: &config.Cfg{ + cfg: &PluginInstanceCfg{ + PluginSettings: map[string]map[string]string{ + pluginID: {"tracing": "true"}, + }, + AWSAssumeRoleEnabled: true, Tracing: config.Tracing{ OpenTelemetry: config.OpenTelemetryCfg{ Address: "127.0.0.1:4317", @@ -288,137 +308,142 @@ func TestInitializer_tracingEnvironmentVariables(t *testing.T) { SamplerRemoteURL: "", }, }, - PluginSettings: map[string]map[string]string{ - pluginID: {"tracing": "true"}, - }, - AWSAssumeRoleEnabled: true, + Features: featuremgmt.WithFeatures(), }, plugin: defaultPlugin, exp: func(t *testing.T, envVars []string) { assert.Len(t, envVars, 8) - assert.Equal(t, "GF_PLUGIN_TRACING=true", envVars[0]) - assert.Equal(t, "GF_VERSION=", envVars[1]) - assert.Equal(t, "GF_INSTANCE_OTLP_ADDRESS=127.0.0.1:4317", envVars[2]) - assert.Equal(t, "GF_INSTANCE_OTLP_PROPAGATION=w3c,jaeger", envVars[3]) - assert.Equal(t, "GF_INSTANCE_OTLP_SAMPLER_TYPE=", envVars[4]) - assert.Equal(t, "GF_INSTANCE_OTLP_SAMPLER_PARAM=1.000000", envVars[5]) - assert.Equal(t, "GF_INSTANCE_OTLP_SAMPLER_REMOTE_URL=", envVars[6]) - assert.Equal(t, "GF_PLUGIN_VERSION=1.0.0", envVars[7]) + assert.Equal(t, "GF_VERSION=", envVars[0]) + assert.Equal(t, "GF_INSTANCE_OTLP_ADDRESS=127.0.0.1:4317", envVars[1]) + assert.Equal(t, "GF_INSTANCE_OTLP_PROPAGATION=w3c,jaeger", envVars[2]) + assert.Equal(t, "GF_INSTANCE_OTLP_SAMPLER_TYPE=", envVars[3]) + assert.Equal(t, "GF_INSTANCE_OTLP_SAMPLER_PARAM=1.000000", envVars[4]) + assert.Equal(t, "GF_INSTANCE_OTLP_SAMPLER_REMOTE_URL=", envVars[5]) + assert.Equal(t, "GF_PLUGIN_VERSION=1.0.0", envVars[6]) + assert.Equal(t, "GF_PLUGIN_TRACING=true", envVars[7]) }, }, { name: "otlp no propagation disabled by default", - cfg: &config.Cfg{ + cfg: &PluginInstanceCfg{ Tracing: config.Tracing{ OpenTelemetry: config.OpenTelemetryCfg{ Address: "127.0.0.1:4317", Propagation: "w3c", }, }, + Features: featuremgmt.WithFeatures(), }, plugin: defaultPlugin, exp: expNoTracing, }, { name: "disabled on plugin", - cfg: &config.Cfg{ - Tracing: config.Tracing{ - OpenTelemetry: defaultOTelCfg, - }, + cfg: &PluginInstanceCfg{ PluginSettings: setting.PluginSettings{ pluginID: map[string]string{"tracing": "false"}, }, + Tracing: config.Tracing{ + OpenTelemetry: defaultOTelCfg, + }, + Features: featuremgmt.WithFeatures(), }, plugin: defaultPlugin, exp: expNoTracing, }, { name: "disabled on plugin with other plugin settings", - cfg: &config.Cfg{ - Tracing: config.Tracing{ - OpenTelemetry: defaultOTelCfg, - }, + cfg: &PluginInstanceCfg{ PluginSettings: map[string]map[string]string{ pluginID: {"some_other_option": "true"}, }, AWSAssumeRoleEnabled: true, + Tracing: config.Tracing{ + OpenTelemetry: defaultOTelCfg, + }, + Features: featuremgmt.WithFeatures(), }, plugin: defaultPlugin, exp: expNoTracing, }, { name: "enabled on plugin with other plugin settings", - cfg: &config.Cfg{ - Tracing: config.Tracing{ - OpenTelemetry: defaultOTelCfg, - }, + cfg: &PluginInstanceCfg{ PluginSettings: map[string]map[string]string{ pluginID: {"some_other_option": "true", "tracing": "true"}, }, + Tracing: config.Tracing{ + OpenTelemetry: defaultOTelCfg, + }, + Features: featuremgmt.WithFeatures(), }, plugin: defaultPlugin, exp: expDefaultOtlp, }, { name: `enabled on plugin with no "tracing" plugin setting but with enablePluginsTracingByDefault feature flag`, - cfg: &config.Cfg{ + cfg: &PluginInstanceCfg{ + PluginSettings: map[string]map[string]string{pluginID: {}}, Tracing: config.Tracing{ OpenTelemetry: defaultOTelCfg, }, - PluginSettings: map[string]map[string]string{pluginID: {}}, - Features: featuremgmt.WithFeatures(featuremgmt.FlagEnablePluginsTracingByDefault), + Features: featuremgmt.WithFeatures(featuremgmt.FlagEnablePluginsTracingByDefault), }, plugin: defaultPlugin, exp: expDefaultOtlp, }, { name: `enabled on plugin with plugin setting "tracing=false" but with enablePluginsTracingByDefault feature flag`, - cfg: &config.Cfg{ + cfg: &PluginInstanceCfg{ + PluginSettings: map[string]map[string]string{pluginID: {"tracing": "false"}}, Tracing: config.Tracing{ OpenTelemetry: defaultOTelCfg, }, - PluginSettings: map[string]map[string]string{pluginID: {"tracing": "false"}}, - Features: featuremgmt.WithFeatures(featuremgmt.FlagEnablePluginsTracingByDefault), + Features: featuremgmt.WithFeatures(featuremgmt.FlagEnablePluginsTracingByDefault), }, plugin: defaultPlugin, exp: expDefaultOtlp, }, { name: "GF_PLUGIN_VERSION is not present if tracing is disabled", - cfg: &config.Cfg{ + cfg: &PluginInstanceCfg{ + PluginSettings: map[string]map[string]string{pluginID: {"tracing": "true"}}, Tracing: config.Tracing{ OpenTelemetry: config.OpenTelemetryCfg{}, }, - PluginSettings: map[string]map[string]string{pluginID: {"tracing": "true"}}, + Features: featuremgmt.WithFeatures(), }, plugin: defaultPlugin, exp: expGfPluginVersionNotPresent, }, { name: "GF_PLUGIN_VERSION is present if tracing is enabled and plugin has version", - cfg: &config.Cfg{ + cfg: &PluginInstanceCfg{ + PluginSettings: map[string]map[string]string{pluginID: {"tracing": "true"}}, Tracing: config.Tracing{ OpenTelemetry: defaultOTelCfg, }, - PluginSettings: map[string]map[string]string{pluginID: {"tracing": "true"}}, + Features: featuremgmt.WithFeatures(), }, plugin: defaultPlugin, exp: expGfPluginVersionPresent, }, { name: "GF_PLUGIN_VERSION is not present if tracing is enabled but plugin doesn't have a version", - cfg: &config.Cfg{ + cfg: &PluginInstanceCfg{ + PluginSettings: map[string]map[string]string{pluginID: {"tracing": "true"}}, Tracing: config.Tracing{ OpenTelemetry: config.OpenTelemetryCfg{}, }, - PluginSettings: map[string]map[string]string{pluginID: {"tracing": "true"}}, + Features: featuremgmt.WithFeatures(), }, plugin: pluginWithoutVersion, exp: expGfPluginVersionNotPresent, }, { name: "no sampling (neversample)", - cfg: &config.Cfg{ + cfg: &PluginInstanceCfg{ + PluginSettings: map[string]map[string]string{pluginID: {"tracing": "true"}}, Tracing: config.Tracing{ OpenTelemetry: config.OpenTelemetryCfg{ Address: "127.0.0.1:4317", @@ -428,7 +453,7 @@ func TestInitializer_tracingEnvironmentVariables(t *testing.T) { SamplerRemoteURL: "", }, }, - PluginSettings: map[string]map[string]string{pluginID: {"tracing": "true"}}, + Features: featuremgmt.WithFeatures(), }, plugin: defaultPlugin, exp: func(t *testing.T, envVars []string) { @@ -439,7 +464,8 @@ func TestInitializer_tracingEnvironmentVariables(t *testing.T) { }, { name: "empty sampler with param", - cfg: &config.Cfg{ + cfg: &PluginInstanceCfg{ + PluginSettings: map[string]map[string]string{pluginID: {"tracing": "true"}}, Tracing: config.Tracing{ OpenTelemetry: config.OpenTelemetryCfg{ Address: "127.0.0.1:4317", @@ -449,7 +475,7 @@ func TestInitializer_tracingEnvironmentVariables(t *testing.T) { SamplerRemoteURL: "", }, }, - PluginSettings: map[string]map[string]string{pluginID: {"tracing": "true"}}, + Features: featuremgmt.WithFeatures(), }, plugin: defaultPlugin, exp: func(t *testing.T, envVars []string) { @@ -460,7 +486,8 @@ func TestInitializer_tracingEnvironmentVariables(t *testing.T) { }, { name: "const sampler with param", - cfg: &config.Cfg{ + cfg: &PluginInstanceCfg{ + PluginSettings: map[string]map[string]string{pluginID: {"tracing": "true"}}, Tracing: config.Tracing{ OpenTelemetry: config.OpenTelemetryCfg{ Address: "127.0.0.1:4317", @@ -470,7 +497,7 @@ func TestInitializer_tracingEnvironmentVariables(t *testing.T) { SamplerRemoteURL: "", }, }, - PluginSettings: map[string]map[string]string{pluginID: {"tracing": "true"}}, + Features: featuremgmt.WithFeatures(), }, plugin: defaultPlugin, exp: func(t *testing.T, envVars []string) { @@ -481,7 +508,8 @@ func TestInitializer_tracingEnvironmentVariables(t *testing.T) { }, { name: "rateLimiting sampler", - cfg: &config.Cfg{ + cfg: &PluginInstanceCfg{ + PluginSettings: map[string]map[string]string{pluginID: {"tracing": "true"}}, Tracing: config.Tracing{ OpenTelemetry: config.OpenTelemetryCfg{ Address: "127.0.0.1:4317", @@ -491,7 +519,7 @@ func TestInitializer_tracingEnvironmentVariables(t *testing.T) { SamplerRemoteURL: "", }, }, - PluginSettings: map[string]map[string]string{pluginID: {"tracing": "true"}}, + Features: featuremgmt.WithFeatures(), }, plugin: defaultPlugin, exp: func(t *testing.T, envVars []string) { @@ -502,7 +530,9 @@ func TestInitializer_tracingEnvironmentVariables(t *testing.T) { }, { name: "remote sampler", - cfg: &config.Cfg{ + cfg: &PluginInstanceCfg{ + PluginSettings: map[string]map[string]string{pluginID: {"tracing": "true"}}, + Features: featuremgmt.WithFeatures(), Tracing: config.Tracing{ OpenTelemetry: config.OpenTelemetryCfg{ Address: "127.0.0.1:4317", @@ -512,7 +542,6 @@ func TestInitializer_tracingEnvironmentVariables(t *testing.T) { SamplerRemoteURL: "127.0.0.1:10001", }, }, - PluginSettings: map[string]map[string]string{pluginID: {"tracing": "true"}}, }, plugin: defaultPlugin, exp: func(t *testing.T, envVars []string) { @@ -523,8 +552,8 @@ func TestInitializer_tracingEnvironmentVariables(t *testing.T) { }, } { t.Run(tc.name, func(t *testing.T) { - envVarsProvider := NewProvider(tc.cfg, nil) - envVars := envVarsProvider.Get(context.Background(), tc.plugin) + p := NewEnvVarsProvider(tc.cfg, nil) + envVars := p.PluginEnvVars(context.Background(), tc.plugin) tc.exp(t, envVars) }) } @@ -556,7 +585,7 @@ func getEnvVar(vars []string, wanted string) string { return v } -func TestInitializer_authEnvVars(t *testing.T) { +func TestPluginEnvVarsProvider_authEnvVars(t *testing.T) { t.Run("backend datasource with auth registration", func(t *testing.T) { p := &plugins.Plugin{ JSONData: plugins.JSONData{ @@ -570,10 +599,16 @@ func TestInitializer_authEnvVars(t *testing.T) { }, } - envVarsProvider := NewProvider(&config.Cfg{ - GrafanaAppURL: "https://myorg.com/", - }, nil) - envVars := envVarsProvider.Get(context.Background(), p) + cfg := &setting.Cfg{ + Raw: ini.Empty(), + AppURL: "https://myorg.com/", + } + + pCfg, err := ProvidePluginInstanceConfig(cfg, setting.ProvideProvider(cfg), featuremgmt.WithFeatures()) + require.NoError(t, err) + + provider := NewEnvVarsProvider(pCfg, nil) + envVars := provider.PluginEnvVars(context.Background(), p) assert.Equal(t, "GF_VERSION=", envVars[0]) assert.Equal(t, "GF_APP_URL=https://myorg.com/", envVars[1]) assert.Equal(t, "GF_PLUGIN_APP_CLIENT_ID=clientID", envVars[2]) @@ -582,22 +617,52 @@ func TestInitializer_authEnvVars(t *testing.T) { }) } -func TestInitalizer_awsEnvVars(t *testing.T) { +func TestPluginEnvVarsProvider_awsEnvVars(t *testing.T) { t.Run("backend datasource with aws settings", func(t *testing.T) { - p := &plugins.Plugin{} - envVarsProvider := NewProvider(&config.Cfg{ - AWSAssumeRoleEnabled: false, - AWSAllowedAuthProviders: []string{"grafana_assume_role", "keys"}, - AWSExternalId: "mock_external_id", - AWSSessionDuration: "10m", - AWSListMetricsPageLimit: "100", - }, nil) - envVars := envVarsProvider.Get(context.Background(), p) - assert.ElementsMatch(t, []string{"GF_VERSION=", "AWS_AUTH_AssumeRoleEnabled=false", "AWS_AUTH_AllowedAuthProviders=grafana_assume_role,keys", "AWS_AUTH_EXTERNAL_ID=mock_external_id", "AWS_AUTH_SESSION_DURATION=10m", "AWS_CW_LIST_METRICS_PAGE_LIMIT=100"}, envVars) + tcs := []struct { + name string + pluginID string + forwardToPlugins []string + expected []string + }{ + { + name: "Will generate AWS env vars for plugin as long as is in the forwardToPlugins list", + forwardToPlugins: []string{"foobar-datasource", "cloudwatch", "prometheus"}, + pluginID: "cloudwatch", + expected: []string{"GF_VERSION=", "AWS_AUTH_AssumeRoleEnabled=false", "AWS_AUTH_AllowedAuthProviders=grafana_assume_role,keys", "AWS_AUTH_EXTERNAL_ID=mock_external_id", "AWS_AUTH_SESSION_DURATION=10m", "AWS_CW_LIST_METRICS_PAGE_LIMIT=100"}, + }, + { + name: "Will not generate AWS env vars for plugin as long as is in not the forwardToPlugins list", + forwardToPlugins: []string{"cloudwatch", "foobar-datasource"}, + pluginID: "prometheus", + expected: []string{"GF_VERSION="}, + }, + } + + for _, tc := range tcs { + p := &plugins.Plugin{ + JSONData: plugins.JSONData{ + ID: tc.pluginID, + }, + } + cfg := &PluginInstanceCfg{ + AWSAssumeRoleEnabled: false, + AWSAllowedAuthProviders: []string{"grafana_assume_role", "keys"}, + AWSExternalId: "mock_external_id", + AWSSessionDuration: "10m", + AWSListMetricsPageLimit: "100", + AWSForwardSettingsPlugins: tc.forwardToPlugins, + Features: featuremgmt.WithFeatures(), + } + + provider := NewEnvVarsProvider(cfg, nil) + envVars := provider.PluginEnvVars(context.Background(), p) + assert.ElementsMatch(t, tc.expected, envVars) + } }) } -func TestInitializer_featureToggleEnvVar(t *testing.T) { +func TestPluginEnvVarsProvider_featureToggleEnvVar(t *testing.T) { t.Run("backend datasource with feature toggle", func(t *testing.T) { expectedFeatures := []string{"feat-1", "feat-2"} featuresLookup := map[string]bool{ @@ -605,13 +670,13 @@ func TestInitializer_featureToggleEnvVar(t *testing.T) { expectedFeatures[1]: true, } - p := &plugins.Plugin{} - envVarsProvider := NewProvider(&config.Cfg{ + cfg := &PluginInstanceCfg{ Features: featuremgmt.WithFeatures(expectedFeatures[0], true, expectedFeatures[1], true), - }, nil) - envVars := envVarsProvider.Get(context.Background(), p) + } - assert.Equal(t, 3, len(envVars)) + p := NewEnvVarsProvider(cfg, nil) + envVars := p.PluginEnvVars(context.Background(), &plugins.Plugin{}) + assert.Equal(t, 2, len(envVars)) toggleExpression := strings.Split(envVars[1], "=") assert.Equal(t, 2, len(toggleExpression)) @@ -631,10 +696,10 @@ func TestInitializer_featureToggleEnvVar(t *testing.T) { }) } -func TestInitalizer_azureEnvVars(t *testing.T) { +func TestPluginEnvVarsProvider_azureEnvVars(t *testing.T) { t.Run("backend datasource with azure settings", func(t *testing.T) { - p := &plugins.Plugin{} - envVarsProvider := NewProvider(&config.Cfg{ + cfg := &setting.Cfg{ + Raw: ini.Empty(), AWSAssumeRoleEnabled: true, Azure: &azsettings.AzureSettings{ Cloud: azsettings.AzurePublic, @@ -654,8 +719,13 @@ func TestInitalizer_azureEnvVars(t *testing.T) { UsernameAssertion: true, }, }, - }, nil) - envVars := envVarsProvider.Get(context.Background(), p) + } + + pCfg, err := ProvidePluginInstanceConfig(cfg, setting.ProvideProvider(cfg), featuremgmt.WithFeatures()) + require.NoError(t, err) + + provider := NewEnvVarsProvider(pCfg, nil) + envVars := provider.PluginEnvVars(context.Background(), &plugins.Plugin{}) assert.ElementsMatch(t, []string{"GF_VERSION=", "GFAZPL_AZURE_CLOUD=AzureCloud", "GFAZPL_MANAGED_IDENTITY_ENABLED=true", "GFAZPL_MANAGED_IDENTITY_CLIENT_ID=mock_managed_identity_client_id", "GFAZPL_WORKLOAD_IDENTITY_ENABLED=true", @@ -670,377 +740,3 @@ func TestInitalizer_azureEnvVars(t *testing.T) { }, envVars) }) } - -func TestService_GetConfigMap_Defaults(t *testing.T) { - s := &Service{ - cfg: &config.Cfg{}, - } - - require.Equal(t, map[string]string{ - "GF_SQL_MAX_OPEN_CONNS_DEFAULT": "0", - "GF_SQL_MAX_IDLE_CONNS_DEFAULT": "0", - "GF_SQL_MAX_CONN_LIFETIME_SECONDS_DEFAULT": "0", - }, s.GetConfigMap(context.Background(), "", nil)) -} - -func TestService_GetConfigMap(t *testing.T) { - tcs := []struct { - name string - cfg *config.Cfg - expected map[string]string - }{ - { - name: "Both features and proxy settings enabled", - cfg: &config.Cfg{ - Features: featuremgmt.WithFeatures("feat-2", "feat-500", "feat-1"), - ProxySettings: setting.SecureSocksDSProxySettings{ - Enabled: true, - ShowUI: true, - ClientCert: "c3rt", - ClientKey: "k3y", - RootCA: "ca", - ProxyAddress: "https://proxy.grafana.com", - ServerName: "secureProxy", - AllowInsecure: true, - }, - }, - expected: map[string]string{ - "GF_INSTANCE_FEATURE_TOGGLES_ENABLE": "feat-1,feat-2,feat-500", - "GF_SECURE_SOCKS_DATASOURCE_PROXY_SERVER_ENABLED": "true", - "GF_SECURE_SOCKS_DATASOURCE_PROXY_CLIENT_CERT": "c3rt", - "GF_SECURE_SOCKS_DATASOURCE_PROXY_CLIENT_KEY": "k3y", - "GF_SECURE_SOCKS_DATASOURCE_PROXY_ROOT_CA_CERT": "ca", - "GF_SECURE_SOCKS_DATASOURCE_PROXY_PROXY_ADDRESS": "https://proxy.grafana.com", - "GF_SECURE_SOCKS_DATASOURCE_PROXY_SERVER_NAME": "secureProxy", - "GF_SECURE_SOCKS_DATASOURCE_PROXY_ALLOW_INSECURE": "true", - }, - }, - { - name: "Features enabled but proxy settings disabled", - cfg: &config.Cfg{ - Features: featuremgmt.WithFeatures("feat-2", "feat-500", "feat-1"), - ProxySettings: setting.SecureSocksDSProxySettings{ - Enabled: false, - ShowUI: true, - ClientCert: "c3rt", - ClientKey: "k3y", - RootCA: "ca", - ProxyAddress: "https://proxy.grafana.com", - ServerName: "secureProxy", - }, - }, - expected: map[string]string{ - "GF_INSTANCE_FEATURE_TOGGLES_ENABLE": "feat-1,feat-2,feat-500", - }, - }, - { - name: "Both features and proxy settings disabled", - cfg: &config.Cfg{ - Features: featuremgmt.WithFeatures("feat-2", false), - ProxySettings: setting.SecureSocksDSProxySettings{ - Enabled: false, - ShowUI: true, - ClientCert: "c3rt", - ClientKey: "k3y", - RootCA: "ca", - ProxyAddress: "https://proxy.grafana.com", - ServerName: "secureProxy", - }, - }, - expected: map[string]string{}, - }, - { - name: "Both features and proxy settings empty", - cfg: &config.Cfg{ - Features: nil, - ProxySettings: setting.SecureSocksDSProxySettings{}, - }, - expected: map[string]string{}, - }, - } - for _, tc := range tcs { - t.Run(tc.name, func(t *testing.T) { - s := &Service{ - cfg: tc.cfg, - } - require.Subset(t, s.GetConfigMap(context.Background(), "", nil), tc.expected) - }) - } -} - -func TestService_GetConfigMap_featureToggles(t *testing.T) { - t.Run("Feature toggles list is deterministic", func(t *testing.T) { - tcs := []struct { - features featuremgmt.FeatureToggles - expectedConfig map[string]string - }{ - { - features: nil, - expectedConfig: map[string]string{}, - }, - { - features: featuremgmt.WithFeatures(), - expectedConfig: map[string]string{}, - }, - { - features: featuremgmt.WithFeatures("A", "B", "C"), - expectedConfig: map[string]string{"GF_INSTANCE_FEATURE_TOGGLES_ENABLE": "A,B,C"}, - }, - { - features: featuremgmt.WithFeatures("C", "B", "A"), - expectedConfig: map[string]string{"GF_INSTANCE_FEATURE_TOGGLES_ENABLE": "A,B,C"}, - }, - { - features: featuremgmt.WithFeatures("b", "a", "c", "d"), - expectedConfig: map[string]string{"GF_INSTANCE_FEATURE_TOGGLES_ENABLE": "a,b,c,d"}, - }, - } - - for _, tc := range tcs { - s := &Service{ - cfg: &config.Cfg{ - Features: tc.features, - }, - } - require.Subset(t, s.GetConfigMap(context.Background(), "", nil), tc.expectedConfig) - } - }) -} - -func TestService_GetConfigMap_appURL(t *testing.T) { - t.Run("Uses the configured app URL", func(t *testing.T) { - s := &Service{ - cfg: &config.Cfg{ - GrafanaAppURL: "https://myorg.com/", - }, - } - require.Subset(t, s.GetConfigMap(context.Background(), "", nil), map[string]string{"GF_APP_URL": "https://myorg.com/"}) - }) -} - -func TestService_GetConfigMap_SQL(t *testing.T) { - t.Run("Uses the configured values", func(t *testing.T) { - s := &Service{ - cfg: &config.Cfg{ - DataProxyRowLimit: 23, - SQLDatasourceMaxOpenConnsDefault: 24, - SQLDatasourceMaxIdleConnsDefault: 25, - SQLDatasourceMaxConnLifetimeDefault: 26, - }, - } - - require.Subset(t, s.GetConfigMap(context.Background(), "", nil), map[string]string{ - "GF_SQL_ROW_LIMIT": "23", - "GF_SQL_MAX_OPEN_CONNS_DEFAULT": "24", - "GF_SQL_MAX_IDLE_CONNS_DEFAULT": "25", - "GF_SQL_MAX_CONN_LIFETIME_SECONDS_DEFAULT": "26", - }) - }) - - t.Run("Uses the configured max-default-values, even when they are zero", func(t *testing.T) { - s := &Service{ - cfg: &config.Cfg{ - SQLDatasourceMaxOpenConnsDefault: 0, - SQLDatasourceMaxIdleConnsDefault: 0, - SQLDatasourceMaxConnLifetimeDefault: 0, - }, - } - - require.Equal(t, map[string]string{ - "GF_SQL_MAX_OPEN_CONNS_DEFAULT": "0", - "GF_SQL_MAX_IDLE_CONNS_DEFAULT": "0", - "GF_SQL_MAX_CONN_LIFETIME_SECONDS_DEFAULT": "0", - }, s.GetConfigMap(context.Background(), "", nil)) - }) -} - -func TestService_GetConfigMap_concurrentQueryCount(t *testing.T) { - t.Run("Uses the configured concurrent query count", func(t *testing.T) { - s := &Service{ - cfg: &config.Cfg{ - ConcurrentQueryCount: 42, - }, - } - require.Subset(t, s.GetConfigMap(context.Background(), "", nil), map[string]string{"GF_CONCURRENT_QUERY_COUNT": "42"}) - }) - - t.Run("Doesn't set the concurrent query count if it is not in the config", func(t *testing.T) { - s := &Service{ - cfg: &config.Cfg{}, - } - require.NotContains(t, s.GetConfigMap(context.Background(), "", nil), "GF_CONCURRENT_QUERY_COUNT") - }) - - t.Run("Doesn't set the concurrent query count if it is zero", func(t *testing.T) { - s := &Service{ - cfg: &config.Cfg{ - ConcurrentQueryCount: 0, - }, - } - require.NotContains(t, s.GetConfigMap(context.Background(), "", nil), "GF_CONCURRENT_QUERY_COUNT") - }) -} - -func TestService_GetConfigMap_azureAuthEnabled(t *testing.T) { - t.Run("Uses the configured azureAuthEnabled", func(t *testing.T) { - s := &Service{ - cfg: &config.Cfg{ - AzureAuthEnabled: true, - }, - } - require.Subset(t, s.GetConfigMap(context.Background(), "", nil), map[string]string{"GFAZPL_AZURE_AUTH_ENABLED": "true"}) - }) - - t.Run("Doesn't set the azureAuthEnabled if it is not in the config", func(t *testing.T) { - s := &Service{ - cfg: &config.Cfg{}, - } - require.NotContains(t, s.GetConfigMap(context.Background(), "", nil), "GFAZPL_AZURE_AUTH_ENABLED") - }) - - t.Run("Doesn't set the azureAuthEnabled if it is false", func(t *testing.T) { - s := &Service{ - cfg: &config.Cfg{ - AzureAuthEnabled: false, - }, - } - require.NotContains(t, s.GetConfigMap(context.Background(), "", nil), "GFAZPL_AZURE_AUTH_ENABLED") - }) -} - -func TestService_GetConfigMap_azure(t *testing.T) { - azSettings := &azsettings.AzureSettings{ - Cloud: azsettings.AzurePublic, - ManagedIdentityEnabled: true, - ManagedIdentityClientId: "mock_managed_identity_client_id", - WorkloadIdentityEnabled: true, - WorkloadIdentitySettings: &azsettings.WorkloadIdentitySettings{ - TenantId: "mock_workload_identity_tenant_id", - ClientId: "mock_workload_identity_client_id", - TokenFile: "mock_workload_identity_token_file", - }, - UserIdentityEnabled: true, - UserIdentityTokenEndpoint: &azsettings.TokenEndpointSettings{ - TokenUrl: "mock_user_identity_token_url", - ClientId: "mock_user_identity_client_id", - ClientSecret: "mock_user_identity_client_secret", - UsernameAssertion: true, - }, - ForwardSettingsPlugins: []string{"grafana-azure-monitor-datasource", "prometheus", "grafana-azure-data-explorer-datasource", "mssql"}, - } - - t.Run("uses the azure settings for an Azure plugin", func(t *testing.T) { - s := &Service{ - cfg: &config.Cfg{ - Azure: azSettings, - }, - } - require.Subset(t, s.GetConfigMap(context.Background(), "grafana-azure-monitor-datasource", nil), map[string]string{ - "GFAZPL_AZURE_CLOUD": "AzureCloud", "GFAZPL_MANAGED_IDENTITY_ENABLED": "true", - "GFAZPL_MANAGED_IDENTITY_CLIENT_ID": "mock_managed_identity_client_id", - "GFAZPL_WORKLOAD_IDENTITY_ENABLED": "true", - "GFAZPL_WORKLOAD_IDENTITY_TENANT_ID": "mock_workload_identity_tenant_id", - "GFAZPL_WORKLOAD_IDENTITY_CLIENT_ID": "mock_workload_identity_client_id", - "GFAZPL_WORKLOAD_IDENTITY_TOKEN_FILE": "mock_workload_identity_token_file", - "GFAZPL_USER_IDENTITY_ENABLED": "true", - "GFAZPL_USER_IDENTITY_TOKEN_URL": "mock_user_identity_token_url", - "GFAZPL_USER_IDENTITY_CLIENT_ID": "mock_user_identity_client_id", - "GFAZPL_USER_IDENTITY_CLIENT_SECRET": "mock_user_identity_client_secret", - "GFAZPL_USER_IDENTITY_ASSERTION": "username", - }) - }) - - t.Run("does not use the azure settings for a non-Azure plugin", func(t *testing.T) { - s := &Service{ - cfg: &config.Cfg{ - Azure: azSettings, - }, - } - - m := s.GetConfigMap(context.Background(), "", nil) - require.NotContains(t, m, "GFAZPL_AZURE_CLOUD") - require.NotContains(t, m, "GFAZPL_MANAGED_IDENTITY_ENABLED") - require.NotContains(t, m, "GFAZPL_MANAGED_IDENTITY_CLIENT_ID") - require.NotContains(t, m, "GFAZPL_WORKLOAD_IDENTITY_ENABLED") - require.NotContains(t, m, "GFAZPL_WORKLOAD_IDENTITY_TENANT_ID") - require.NotContains(t, m, "GFAZPL_WORKLOAD_IDENTITY_CLIENT_ID") - require.NotContains(t, m, "GFAZPL_WORKLOAD_IDENTITY_TOKEN_FILE") - require.NotContains(t, m, "GFAZPL_USER_IDENTITY_ENABLED") - require.NotContains(t, m, "GFAZPL_USER_IDENTITY_TOKEN_URL") - require.NotContains(t, m, "GFAZPL_USER_IDENTITY_CLIENT_ID") - require.NotContains(t, m, "GFAZPL_USER_IDENTITY_CLIENT_SECRET") - require.NotContains(t, m, "GFAZPL_USER_IDENTITY_ASSERTION") - }) - - t.Run("uses the azure settings for a non-Azure user-specified plugin", func(t *testing.T) { - azSettings.ForwardSettingsPlugins = append(azSettings.ForwardSettingsPlugins, "test-datasource") - s := &Service{ - cfg: &config.Cfg{ - Azure: azSettings, - }, - } - require.Subset(t, s.GetConfigMap(context.Background(), "test-datasource", nil), map[string]string{ - "GFAZPL_AZURE_CLOUD": "AzureCloud", "GFAZPL_MANAGED_IDENTITY_ENABLED": "true", - "GFAZPL_MANAGED_IDENTITY_CLIENT_ID": "mock_managed_identity_client_id", - "GFAZPL_WORKLOAD_IDENTITY_ENABLED": "true", - "GFAZPL_WORKLOAD_IDENTITY_TENANT_ID": "mock_workload_identity_tenant_id", - "GFAZPL_WORKLOAD_IDENTITY_CLIENT_ID": "mock_workload_identity_client_id", - "GFAZPL_WORKLOAD_IDENTITY_TOKEN_FILE": "mock_workload_identity_token_file", - "GFAZPL_USER_IDENTITY_ENABLED": "true", - "GFAZPL_USER_IDENTITY_TOKEN_URL": "mock_user_identity_token_url", - "GFAZPL_USER_IDENTITY_CLIENT_ID": "mock_user_identity_client_id", - "GFAZPL_USER_IDENTITY_CLIENT_SECRET": "mock_user_identity_client_secret", - "GFAZPL_USER_IDENTITY_ASSERTION": "username", - }) - }) -} - -func TestService_GetConfigMap_aws(t *testing.T) { - cfg := &config.Cfg{ - AWSAssumeRoleEnabled: false, - AWSAllowedAuthProviders: []string{"grafana_assume_role", "keys"}, - AWSExternalId: "mock_external_id", - AWSSessionDuration: "10m", - AWSListMetricsPageLimit: "100", - AWSForwardSettingsPlugins: []string{"cloudwatch", "prometheus", "elasticsearch"}, - } - - t.Run("uses the aws settings for an AWS plugin", func(t *testing.T) { - s := &Service{ - cfg: cfg, - } - require.Subset(t, s.GetConfigMap(context.Background(), "cloudwatch", nil), map[string]string{ - "AWS_AUTH_AssumeRoleEnabled": "false", - "AWS_AUTH_AllowedAuthProviders": "grafana_assume_role,keys", - "AWS_AUTH_EXTERNAL_ID": "mock_external_id", - "AWS_AUTH_SESSION_DURATION": "10m", - "AWS_CW_LIST_METRICS_PAGE_LIMIT": "100", - }) - }) - - t.Run("does not use the aws settings for a non-aws plugin", func(t *testing.T) { - s := &Service{ - cfg: cfg, - } - m := s.GetConfigMap(context.Background(), "", nil) - require.NotContains(t, m, "AWS_AUTH_AssumeRoleEnabled") - require.NotContains(t, m, "AWS_AUTH_AllowedAuthProviders") - require.NotContains(t, m, "AWS_AUTH_EXTERNAL_ID") - require.NotContains(t, m, "AWS_AUTH_SESSION_DURATION") - require.NotContains(t, m, "AWS_CW_LIST_METRICS_PAGE_LIMIT") - }) - - t.Run("uses the aws settings for a non-aws user-specified plugin", func(t *testing.T) { - cfg.AWSForwardSettingsPlugins = append(cfg.AWSForwardSettingsPlugins, "test-datasource") - s := &Service{ - cfg: cfg, - } - require.Subset(t, s.GetConfigMap(context.Background(), "test-datasource", nil), map[string]string{ - "AWS_AUTH_AssumeRoleEnabled": "false", - "AWS_AUTH_AllowedAuthProviders": "grafana_assume_role,keys", - "AWS_AUTH_EXTERNAL_ID": "mock_external_id", - "AWS_AUTH_SESSION_DURATION": "10m", - "AWS_CW_LIST_METRICS_PAGE_LIMIT": "100", - }) - }) -} diff --git a/pkg/services/pluginsintegration/pluginconfig/fakes.go b/pkg/services/pluginsintegration/pluginconfig/fakes.go new file mode 100644 index 0000000000..c58a844380 --- /dev/null +++ b/pkg/services/pluginsintegration/pluginconfig/fakes.go @@ -0,0 +1,16 @@ +package pluginconfig + +import "context" + +var _ PluginRequestConfigProvider = (*FakePluginRequestConfigProvider)(nil) + +type FakePluginRequestConfigProvider struct{} + +func NewFakePluginRequestConfigProvider() *FakePluginRequestConfigProvider { + return &FakePluginRequestConfigProvider{} +} + +// PluginRequestConfig returns a map of configuration that should be passed in a plugin request. +func (s *FakePluginRequestConfigProvider) PluginRequestConfig(ctx context.Context, pluginID string) map[string]string { + return map[string]string{} +} diff --git a/pkg/services/pluginsintegration/pluginconfig/request.go b/pkg/services/pluginsintegration/pluginconfig/request.go new file mode 100644 index 0000000000..92cdd8f1ef --- /dev/null +++ b/pkg/services/pluginsintegration/pluginconfig/request.go @@ -0,0 +1,151 @@ +package pluginconfig + +import ( + "context" + "slices" + "sort" + "strconv" + "strings" + + "github.com/grafana/grafana-aws-sdk/pkg/awsds" + "github.com/grafana/grafana-azure-sdk-go/azsettings" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/backend/proxy" + "github.com/grafana/grafana-plugin-sdk-go/experimental/featuretoggles" +) + +var _ PluginRequestConfigProvider = (*RequestConfigProvider)(nil) + +type PluginRequestConfigProvider interface { + PluginRequestConfig(ctx context.Context, pluginID string) map[string]string +} + +type RequestConfigProvider struct { + cfg *PluginInstanceCfg +} + +func NewRequestConfigProvider(cfg *PluginInstanceCfg) *RequestConfigProvider { + return &RequestConfigProvider{ + cfg: cfg, + } +} + +// PluginRequestConfig returns a map of configuration that should be passed in a plugin request. +// nolint:gocyclo +func (s *RequestConfigProvider) PluginRequestConfig(ctx context.Context, pluginID string) map[string]string { + m := make(map[string]string) + + if s.cfg.GrafanaAppURL != "" { + m[backend.AppURL] = s.cfg.GrafanaAppURL + } + if s.cfg.ConcurrentQueryCount != 0 { + m[backend.ConcurrentQueryCount] = strconv.Itoa(s.cfg.ConcurrentQueryCount) + } + + enabledFeatures := s.cfg.Features.GetEnabled(ctx) + if len(enabledFeatures) > 0 { + features := make([]string, 0, len(enabledFeatures)) + for feat := range enabledFeatures { + features = append(features, feat) + } + sort.Strings(features) + m[featuretoggles.EnabledFeatures] = strings.Join(features, ",") + } + + if slices.Contains[[]string, string](s.cfg.AWSForwardSettingsPlugins, pluginID) { + if !s.cfg.AWSAssumeRoleEnabled { + m[awsds.AssumeRoleEnabledEnvVarKeyName] = "false" + } + if len(s.cfg.AWSAllowedAuthProviders) > 0 { + m[awsds.AllowedAuthProvidersEnvVarKeyName] = strings.Join(s.cfg.AWSAllowedAuthProviders, ",") + } + if s.cfg.AWSExternalId != "" { + m[awsds.GrafanaAssumeRoleExternalIdKeyName] = s.cfg.AWSExternalId + } + if s.cfg.AWSSessionDuration != "" { + m[awsds.SessionDurationEnvVarKeyName] = s.cfg.AWSSessionDuration + } + if s.cfg.AWSListMetricsPageLimit != "" { + m[awsds.ListMetricsPageLimitKeyName] = s.cfg.AWSListMetricsPageLimit + } + } + + if s.cfg.ProxySettings.Enabled { + m[proxy.PluginSecureSocksProxyEnabled] = "true" + m[proxy.PluginSecureSocksProxyClientCert] = s.cfg.ProxySettings.ClientCert + m[proxy.PluginSecureSocksProxyClientKey] = s.cfg.ProxySettings.ClientKey + m[proxy.PluginSecureSocksProxyRootCACert] = s.cfg.ProxySettings.RootCA + m[proxy.PluginSecureSocksProxyProxyAddress] = s.cfg.ProxySettings.ProxyAddress + m[proxy.PluginSecureSocksProxyServerName] = s.cfg.ProxySettings.ServerName + m[proxy.PluginSecureSocksProxyAllowInsecure] = strconv.FormatBool(s.cfg.ProxySettings.AllowInsecure) + } + + // Settings here will be extracted by grafana-azure-sdk-go from the plugin context + if s.cfg.AzureAuthEnabled { + m[azsettings.AzureAuthEnabled] = strconv.FormatBool(s.cfg.AzureAuthEnabled) + } + azureSettings := s.cfg.Azure + if azureSettings != nil && slices.Contains[[]string, string](azureSettings.ForwardSettingsPlugins, pluginID) { + if azureSettings.Cloud != "" { + m[azsettings.AzureCloud] = azureSettings.Cloud + } + + if azureSettings.ManagedIdentityEnabled { + m[azsettings.ManagedIdentityEnabled] = "true" + + if azureSettings.ManagedIdentityClientId != "" { + m[azsettings.ManagedIdentityClientID] = azureSettings.ManagedIdentityClientId + } + } + + if azureSettings.UserIdentityEnabled { + m[azsettings.UserIdentityEnabled] = "true" + + if azureSettings.UserIdentityTokenEndpoint != nil { + if azureSettings.UserIdentityTokenEndpoint.TokenUrl != "" { + m[azsettings.UserIdentityTokenURL] = azureSettings.UserIdentityTokenEndpoint.TokenUrl + } + if azureSettings.UserIdentityTokenEndpoint.ClientId != "" { + m[azsettings.UserIdentityClientID] = azureSettings.UserIdentityTokenEndpoint.ClientId + } + if azureSettings.UserIdentityTokenEndpoint.ClientSecret != "" { + m[azsettings.UserIdentityClientSecret] = azureSettings.UserIdentityTokenEndpoint.ClientSecret + } + if azureSettings.UserIdentityTokenEndpoint.UsernameAssertion { + m[azsettings.UserIdentityAssertion] = "username" + } + } + } + + if azureSettings.WorkloadIdentityEnabled { + m[azsettings.WorkloadIdentityEnabled] = "true" + + if azureSettings.WorkloadIdentitySettings != nil { + if azureSettings.WorkloadIdentitySettings.ClientId != "" { + m[azsettings.WorkloadIdentityClientID] = azureSettings.WorkloadIdentitySettings.ClientId + } + if azureSettings.WorkloadIdentitySettings.TenantId != "" { + m[azsettings.WorkloadIdentityTenantID] = azureSettings.WorkloadIdentitySettings.TenantId + } + if azureSettings.WorkloadIdentitySettings.TokenFile != "" { + m[azsettings.WorkloadIdentityTokenFile] = azureSettings.WorkloadIdentitySettings.TokenFile + } + } + } + } + + if s.cfg.UserFacingDefaultError != "" { + m[backend.UserFacingDefaultError] = s.cfg.UserFacingDefaultError + } + + if s.cfg.DataProxyRowLimit != 0 { + m[backend.SQLRowLimit] = strconv.FormatInt(s.cfg.DataProxyRowLimit, 10) + } + + m[backend.SQLMaxOpenConnsDefault] = strconv.Itoa(s.cfg.SQLDatasourceMaxOpenConnsDefault) + m[backend.SQLMaxIdleConnsDefault] = strconv.Itoa(s.cfg.SQLDatasourceMaxIdleConnsDefault) + m[backend.SQLMaxConnLifetimeSecondsDefault] = strconv.Itoa(s.cfg.SQLDatasourceMaxConnLifetimeDefault) + + return m +} diff --git a/pkg/services/pluginsintegration/pluginconfig/request_test.go b/pkg/services/pluginsintegration/pluginconfig/request_test.go new file mode 100644 index 0000000000..409958b287 --- /dev/null +++ b/pkg/services/pluginsintegration/pluginconfig/request_test.go @@ -0,0 +1,398 @@ +package pluginconfig + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana-azure-sdk-go/azsettings" + + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/setting" +) + +func TestRequestConfigProvider_PluginRequestConfig_Defaults(t *testing.T) { + cfg := setting.NewCfg() + pCfg, err := ProvidePluginInstanceConfig(cfg, setting.ProvideProvider(cfg), featuremgmt.WithFeatures()) + require.NoError(t, err) + + p := NewRequestConfigProvider(pCfg) + require.Equal(t, map[string]string{ + "GF_SQL_MAX_OPEN_CONNS_DEFAULT": "0", + "GF_SQL_MAX_IDLE_CONNS_DEFAULT": "0", + "GF_SQL_MAX_CONN_LIFETIME_SECONDS_DEFAULT": "0", + }, p.PluginRequestConfig(context.Background(), "")) +} + +func TestRequestConfigProvider_PluginRequestConfig(t *testing.T) { + tcs := []struct { + name string + cfg *PluginInstanceCfg + expected map[string]string + }{ + { + name: "Both features and proxy settings enabled", + cfg: &PluginInstanceCfg{ + ProxySettings: setting.SecureSocksDSProxySettings{ + Enabled: true, + ShowUI: true, + ClientCert: "c3rt", + ClientKey: "k3y", + RootCA: "ca", + ProxyAddress: "https://proxy.grafana.com", + ServerName: "secureProxy", + AllowInsecure: true, + }, + Features: featuremgmt.WithFeatures("feat-2", "feat-500", "feat-1"), + }, + expected: map[string]string{ + "GF_INSTANCE_FEATURE_TOGGLES_ENABLE": "feat-1,feat-2,feat-500", + "GF_SECURE_SOCKS_DATASOURCE_PROXY_SERVER_ENABLED": "true", + "GF_SECURE_SOCKS_DATASOURCE_PROXY_CLIENT_CERT": "c3rt", + "GF_SECURE_SOCKS_DATASOURCE_PROXY_CLIENT_KEY": "k3y", + "GF_SECURE_SOCKS_DATASOURCE_PROXY_ROOT_CA_CERT": "ca", + "GF_SECURE_SOCKS_DATASOURCE_PROXY_PROXY_ADDRESS": "https://proxy.grafana.com", + "GF_SECURE_SOCKS_DATASOURCE_PROXY_SERVER_NAME": "secureProxy", + "GF_SECURE_SOCKS_DATASOURCE_PROXY_ALLOW_INSECURE": "true", + }, + }, + { + name: "Features enabled but proxy settings disabled", + cfg: &PluginInstanceCfg{ + ProxySettings: setting.SecureSocksDSProxySettings{ + Enabled: false, + ShowUI: true, + ClientCert: "c3rt", + ClientKey: "k3y", + RootCA: "ca", + ProxyAddress: "https://proxy.grafana.com", + ServerName: "secureProxy", + }, + Features: featuremgmt.WithFeatures("feat-2", "feat-500", "feat-1"), + }, + expected: map[string]string{ + "GF_INSTANCE_FEATURE_TOGGLES_ENABLE": "feat-1,feat-2,feat-500", + }, + }, + { + name: "Both features and proxy settings disabled", + cfg: &PluginInstanceCfg{ + ProxySettings: setting.SecureSocksDSProxySettings{ + Enabled: false, + ShowUI: true, + ClientCert: "c3rt", + ClientKey: "k3y", + RootCA: "ca", + ProxyAddress: "https://proxy.grafana.com", + ServerName: "secureProxy", + }, + Features: featuremgmt.WithFeatures("feat-2", false), + }, + expected: map[string]string{}, + }, + { + name: "Both features and proxy settings empty", + cfg: &PluginInstanceCfg{ + ProxySettings: setting.SecureSocksDSProxySettings{}, + Features: featuremgmt.WithFeatures(), + }, + expected: map[string]string{}, + }, + } + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + p := NewRequestConfigProvider(tc.cfg) + require.Subset(t, p.PluginRequestConfig(context.Background(), ""), tc.expected) + }) + } +} + +func TestRequestConfigProvider_PluginRequestConfig_featureToggles(t *testing.T) { + t.Run("Feature toggles list is deterministic", func(t *testing.T) { + tcs := []struct { + features featuremgmt.FeatureToggles + expectedConfig map[string]string + }{ + { + features: featuremgmt.WithFeatures(), + expectedConfig: map[string]string{}, + }, + { + features: featuremgmt.WithFeatures("A", "B", "C"), + expectedConfig: map[string]string{"GF_INSTANCE_FEATURE_TOGGLES_ENABLE": "A,B,C"}, + }, + { + features: featuremgmt.WithFeatures("C", "B", "A"), + expectedConfig: map[string]string{"GF_INSTANCE_FEATURE_TOGGLES_ENABLE": "A,B,C"}, + }, + { + features: featuremgmt.WithFeatures("b", "a", "c", "d"), + expectedConfig: map[string]string{"GF_INSTANCE_FEATURE_TOGGLES_ENABLE": "a,b,c,d"}, + }, + } + + for _, tc := range tcs { + cfg := setting.NewCfg() + pCfg, err := ProvidePluginInstanceConfig(cfg, setting.ProvideProvider(cfg), tc.features) + require.NoError(t, err) + + p := NewRequestConfigProvider(pCfg) + require.Subset(t, p.PluginRequestConfig(context.Background(), ""), tc.expectedConfig) + } + }) +} + +func TestRequestConfigProvider_PluginRequestConfig_appURL(t *testing.T) { + t.Run("Uses the configured app URL", func(t *testing.T) { + cfg := setting.NewCfg() + cfg.AppURL = "https://myorg.com/" + + pCfg, err := ProvidePluginInstanceConfig(cfg, setting.ProvideProvider(cfg), featuremgmt.WithFeatures()) + require.NoError(t, err) + + p := NewRequestConfigProvider(pCfg) + require.Subset(t, p.PluginRequestConfig(context.Background(), ""), map[string]string{"GF_APP_URL": "https://myorg.com/"}) + }) +} + +func TestRequestConfigProvider_PluginRequestConfig_SQL(t *testing.T) { + t.Run("Uses the configured values", func(t *testing.T) { + cfg := setting.NewCfg() + cfg.DataProxyRowLimit = 23 + cfg.SqlDatasourceMaxOpenConnsDefault = 24 + cfg.SqlDatasourceMaxIdleConnsDefault = 25 + cfg.SqlDatasourceMaxConnLifetimeDefault = 26 + + pCfg, err := ProvidePluginInstanceConfig(cfg, setting.ProvideProvider(cfg), featuremgmt.WithFeatures()) + require.NoError(t, err) + + p := NewRequestConfigProvider(pCfg) + require.Subset(t, p.PluginRequestConfig(context.Background(), ""), map[string]string{ + "GF_SQL_ROW_LIMIT": "23", + "GF_SQL_MAX_OPEN_CONNS_DEFAULT": "24", + "GF_SQL_MAX_IDLE_CONNS_DEFAULT": "25", + "GF_SQL_MAX_CONN_LIFETIME_SECONDS_DEFAULT": "26", + }) + }) + + t.Run("Uses the configured max-default-values, even when they are zero", func(t *testing.T) { + cfg := setting.NewCfg() + cfg.SqlDatasourceMaxOpenConnsDefault = 0 + cfg.SqlDatasourceMaxIdleConnsDefault = 0 + cfg.SqlDatasourceMaxConnLifetimeDefault = 0 + + pCfg, err := ProvidePluginInstanceConfig(cfg, setting.ProvideProvider(cfg), featuremgmt.WithFeatures()) + require.NoError(t, err) + + p := NewRequestConfigProvider(pCfg) + require.Equal(t, map[string]string{ + "GF_SQL_MAX_OPEN_CONNS_DEFAULT": "0", + "GF_SQL_MAX_IDLE_CONNS_DEFAULT": "0", + "GF_SQL_MAX_CONN_LIFETIME_SECONDS_DEFAULT": "0", + }, p.PluginRequestConfig(context.Background(), "")) + }) +} + +func TestRequestConfigProvider_PluginRequestConfig_concurrentQueryCount(t *testing.T) { + t.Run("Uses the configured concurrent query count", func(t *testing.T) { + cfg := setting.NewCfg() + cfg.ConcurrentQueryCount = 42 + + pCfg, err := ProvidePluginInstanceConfig(cfg, setting.ProvideProvider(cfg), featuremgmt.WithFeatures()) + require.NoError(t, err) + + p := NewRequestConfigProvider(pCfg) + require.Subset(t, p.PluginRequestConfig(context.Background(), ""), map[string]string{"GF_CONCURRENT_QUERY_COUNT": "42"}) + }) + + t.Run("Doesn't set the concurrent query count if it is not in the config", func(t *testing.T) { + cfg := setting.NewCfg() + pCfg, err := ProvidePluginInstanceConfig(cfg, setting.ProvideProvider(cfg), featuremgmt.WithFeatures()) + require.NoError(t, err) + + p := NewRequestConfigProvider(pCfg) + require.NotContains(t, p.PluginRequestConfig(context.Background(), ""), "GF_CONCURRENT_QUERY_COUNT") + }) + + t.Run("Doesn't set the concurrent query count if it is zero", func(t *testing.T) { + cfg := setting.NewCfg() + cfg.ConcurrentQueryCount = 0 + + pCfg, err := ProvidePluginInstanceConfig(cfg, setting.ProvideProvider(cfg), featuremgmt.WithFeatures()) + require.NoError(t, err) + + p := NewRequestConfigProvider(pCfg) + require.NotContains(t, p.PluginRequestConfig(context.Background(), ""), "GF_CONCURRENT_QUERY_COUNT") + }) +} + +func TestRequestConfigProvider_PluginRequestConfig_azureAuthEnabled(t *testing.T) { + t.Run("Uses the configured azureAuthEnabled", func(t *testing.T) { + cfg := &PluginInstanceCfg{ + AzureAuthEnabled: true, + Features: featuremgmt.WithFeatures(), + } + + p := NewRequestConfigProvider(cfg) + require.Subset(t, p.PluginRequestConfig(context.Background(), ""), map[string]string{"GFAZPL_AZURE_AUTH_ENABLED": "true"}) + }) + + t.Run("Doesn't set the azureAuthEnabled if it is not in the config", func(t *testing.T) { + cfg := &PluginInstanceCfg{ + Features: featuremgmt.WithFeatures(), + } + + p := NewRequestConfigProvider(cfg) + require.NotContains(t, p.PluginRequestConfig(context.Background(), ""), "GFAZPL_AZURE_AUTH_ENABLED") + }) + + t.Run("Doesn't set the azureAuthEnabled if it is false", func(t *testing.T) { + cfg := &PluginInstanceCfg{ + AzureAuthEnabled: false, + Features: featuremgmt.WithFeatures(), + } + + p := NewRequestConfigProvider(cfg) + require.NotContains(t, p.PluginRequestConfig(context.Background(), ""), "GFAZPL_AZURE_AUTH_ENABLED") + }) +} + +func TestRequestConfigProvider_PluginRequestConfig_azure(t *testing.T) { + azSettings := &azsettings.AzureSettings{ + Cloud: azsettings.AzurePublic, + ManagedIdentityEnabled: true, + ManagedIdentityClientId: "mock_managed_identity_client_id", + WorkloadIdentityEnabled: true, + WorkloadIdentitySettings: &azsettings.WorkloadIdentitySettings{ + TenantId: "mock_workload_identity_tenant_id", + ClientId: "mock_workload_identity_client_id", + TokenFile: "mock_workload_identity_token_file", + }, + UserIdentityEnabled: true, + UserIdentityTokenEndpoint: &azsettings.TokenEndpointSettings{ + TokenUrl: "mock_user_identity_token_url", + ClientId: "mock_user_identity_client_id", + ClientSecret: "mock_user_identity_client_secret", + UsernameAssertion: true, + }, + ForwardSettingsPlugins: []string{"grafana-azure-monitor-datasource", "prometheus", "grafana-azure-data-explorer-datasource", "mssql"}, + } + + t.Run("uses the azure settings for an Azure plugin", func(t *testing.T) { + cfg := setting.NewCfg() + cfg.Azure = azSettings + + pCfg, err := ProvidePluginInstanceConfig(cfg, setting.ProvideProvider(cfg), featuremgmt.WithFeatures()) + require.NoError(t, err) + + p := NewRequestConfigProvider(pCfg) + require.Subset(t, p.PluginRequestConfig(context.Background(), "grafana-azure-monitor-datasource"), map[string]string{ + "GFAZPL_AZURE_CLOUD": "AzureCloud", "GFAZPL_MANAGED_IDENTITY_ENABLED": "true", + "GFAZPL_MANAGED_IDENTITY_CLIENT_ID": "mock_managed_identity_client_id", + "GFAZPL_WORKLOAD_IDENTITY_ENABLED": "true", + "GFAZPL_WORKLOAD_IDENTITY_TENANT_ID": "mock_workload_identity_tenant_id", + "GFAZPL_WORKLOAD_IDENTITY_CLIENT_ID": "mock_workload_identity_client_id", + "GFAZPL_WORKLOAD_IDENTITY_TOKEN_FILE": "mock_workload_identity_token_file", + "GFAZPL_USER_IDENTITY_ENABLED": "true", + "GFAZPL_USER_IDENTITY_TOKEN_URL": "mock_user_identity_token_url", + "GFAZPL_USER_IDENTITY_CLIENT_ID": "mock_user_identity_client_id", + "GFAZPL_USER_IDENTITY_CLIENT_SECRET": "mock_user_identity_client_secret", + "GFAZPL_USER_IDENTITY_ASSERTION": "username", + }) + }) + + t.Run("does not use the azure settings for a non-Azure plugin", func(t *testing.T) { + cfg := setting.NewCfg() + cfg.Azure = azSettings + + pCfg, err := ProvidePluginInstanceConfig(cfg, setting.ProvideProvider(cfg), featuremgmt.WithFeatures()) + require.NoError(t, err) + + p := NewRequestConfigProvider(pCfg) + m := p.PluginRequestConfig(context.Background(), "") + require.NotContains(t, m, "GFAZPL_AZURE_CLOUD") + require.NotContains(t, m, "GFAZPL_MANAGED_IDENTITY_ENABLED") + require.NotContains(t, m, "GFAZPL_MANAGED_IDENTITY_CLIENT_ID") + require.NotContains(t, m, "GFAZPL_WORKLOAD_IDENTITY_ENABLED") + require.NotContains(t, m, "GFAZPL_WORKLOAD_IDENTITY_TENANT_ID") + require.NotContains(t, m, "GFAZPL_WORKLOAD_IDENTITY_CLIENT_ID") + require.NotContains(t, m, "GFAZPL_WORKLOAD_IDENTITY_TOKEN_FILE") + require.NotContains(t, m, "GFAZPL_USER_IDENTITY_ENABLED") + require.NotContains(t, m, "GFAZPL_USER_IDENTITY_TOKEN_URL") + require.NotContains(t, m, "GFAZPL_USER_IDENTITY_CLIENT_ID") + require.NotContains(t, m, "GFAZPL_USER_IDENTITY_CLIENT_SECRET") + require.NotContains(t, m, "GFAZPL_USER_IDENTITY_ASSERTION") + }) + + t.Run("uses the azure settings for a non-Azure user-specified plugin", func(t *testing.T) { + azSettings.ForwardSettingsPlugins = append(azSettings.ForwardSettingsPlugins, "test-datasource") + cfg := setting.NewCfg() + cfg.Azure = azSettings + + pCfg, err := ProvidePluginInstanceConfig(cfg, setting.ProvideProvider(cfg), featuremgmt.WithFeatures()) + require.NoError(t, err) + + p := NewRequestConfigProvider(pCfg) + require.Subset(t, p.PluginRequestConfig(context.Background(), "test-datasource"), map[string]string{ + "GFAZPL_AZURE_CLOUD": "AzureCloud", "GFAZPL_MANAGED_IDENTITY_ENABLED": "true", + "GFAZPL_MANAGED_IDENTITY_CLIENT_ID": "mock_managed_identity_client_id", + "GFAZPL_WORKLOAD_IDENTITY_ENABLED": "true", + "GFAZPL_WORKLOAD_IDENTITY_TENANT_ID": "mock_workload_identity_tenant_id", + "GFAZPL_WORKLOAD_IDENTITY_CLIENT_ID": "mock_workload_identity_client_id", + "GFAZPL_WORKLOAD_IDENTITY_TOKEN_FILE": "mock_workload_identity_token_file", + "GFAZPL_USER_IDENTITY_ENABLED": "true", + "GFAZPL_USER_IDENTITY_TOKEN_URL": "mock_user_identity_token_url", + "GFAZPL_USER_IDENTITY_CLIENT_ID": "mock_user_identity_client_id", + "GFAZPL_USER_IDENTITY_CLIENT_SECRET": "mock_user_identity_client_secret", + "GFAZPL_USER_IDENTITY_ASSERTION": "username", + }) + }) +} + +func TestRequestConfigProvider_PluginRequestConfig_aws(t *testing.T) { + cfg := &PluginInstanceCfg{ + Features: featuremgmt.WithFeatures(), + } + + cfg.AWSAssumeRoleEnabled = false + cfg.AWSAllowedAuthProviders = []string{"grafana_assume_role", "keys"} + cfg.AWSExternalId = "mock_external_id" + cfg.AWSSessionDuration = "10m" + cfg.AWSListMetricsPageLimit = "100" + cfg.AWSForwardSettingsPlugins = []string{"cloudwatch", "prometheus", "elasticsearch"} + + p := NewRequestConfigProvider(cfg) + + t.Run("uses the aws settings for an AWS plugin", func(t *testing.T) { + require.Subset(t, p.PluginRequestConfig(context.Background(), "cloudwatch"), map[string]string{ + "AWS_AUTH_AssumeRoleEnabled": "false", + "AWS_AUTH_AllowedAuthProviders": "grafana_assume_role,keys", + "AWS_AUTH_EXTERNAL_ID": "mock_external_id", + "AWS_AUTH_SESSION_DURATION": "10m", + "AWS_CW_LIST_METRICS_PAGE_LIMIT": "100", + }) + }) + + t.Run("does not use the aws settings for a non-aws plugin", func(t *testing.T) { + m := p.PluginRequestConfig(context.Background(), "") + require.NotContains(t, m, "AWS_AUTH_AssumeRoleEnabled") + require.NotContains(t, m, "AWS_AUTH_AllowedAuthProviders") + require.NotContains(t, m, "AWS_AUTH_EXTERNAL_ID") + require.NotContains(t, m, "AWS_AUTH_SESSION_DURATION") + require.NotContains(t, m, "AWS_CW_LIST_METRICS_PAGE_LIMIT") + }) + + t.Run("uses the aws settings for a non-aws user-specified plugin", func(t *testing.T) { + cfg.AWSForwardSettingsPlugins = append(cfg.AWSForwardSettingsPlugins, "test-datasource") + + p = NewRequestConfigProvider(cfg) + require.Subset(t, p.PluginRequestConfig(context.Background(), "test-datasource"), map[string]string{ + "AWS_AUTH_AssumeRoleEnabled": "false", + "AWS_AUTH_AllowedAuthProviders": "grafana_assume_role,keys", + "AWS_AUTH_EXTERNAL_ID": "mock_external_id", + "AWS_AUTH_SESSION_DURATION": "10m", + "AWS_CW_LIST_METRICS_PAGE_LIMIT": "100", + }) + }) +} diff --git a/pkg/services/pluginsintegration/config/tracing.go b/pkg/services/pluginsintegration/pluginconfig/tracing.go similarity index 97% rename from pkg/services/pluginsintegration/config/tracing.go rename to pkg/services/pluginsintegration/pluginconfig/tracing.go index 6dcc3b4abd..0eada776ce 100644 --- a/pkg/services/pluginsintegration/config/tracing.go +++ b/pkg/services/pluginsintegration/pluginconfig/tracing.go @@ -1,4 +1,4 @@ -package config +package pluginconfig import ( "fmt" diff --git a/pkg/services/pluginsintegration/config/tracing_test.go b/pkg/services/pluginsintegration/pluginconfig/tracing_test.go similarity index 98% rename from pkg/services/pluginsintegration/config/tracing_test.go rename to pkg/services/pluginsintegration/pluginconfig/tracing_test.go index 6f5367da02..b7ad8333ef 100644 --- a/pkg/services/pluginsintegration/config/tracing_test.go +++ b/pkg/services/pluginsintegration/pluginconfig/tracing_test.go @@ -1,4 +1,4 @@ -package config +package pluginconfig import ( "testing" diff --git a/pkg/services/pluginsintegration/plugincontext/plugincontext.go b/pkg/services/pluginsintegration/plugincontext/plugincontext.go index 3f950826b0..4d601b7b34 100644 --- a/pkg/services/pluginsintegration/plugincontext/plugincontext.go +++ b/pkg/services/pluginsintegration/plugincontext/plugincontext.go @@ -15,11 +15,10 @@ import ( "github.com/grafana/grafana/pkg/infra/localcache" "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/envvars" "github.com/grafana/grafana/pkg/services/auth/identity" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/pluginsintegration/adapters" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginconfig" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" "github.com/grafana/grafana/pkg/setting" @@ -32,28 +31,28 @@ const ( func ProvideService(cfg *setting.Cfg, cacheService *localcache.CacheService, pluginStore pluginstore.Store, dataSourceCache datasources.CacheService, dataSourceService datasources.DataSourceService, - pluginSettingsService pluginsettings.Service, licensing plugins.Licensing, pCfg *config.Cfg) *Provider { + pluginSettingsService pluginsettings.Service, pluginRequestConfigProvider pluginconfig.PluginRequestConfigProvider) *Provider { return &Provider{ - cfg: cfg, - cacheService: cacheService, - pluginStore: pluginStore, - dataSourceCache: dataSourceCache, - dataSourceService: dataSourceService, - pluginSettingsService: pluginSettingsService, - pluginEnvVars: envvars.NewProvider(pCfg, licensing), - logger: log.New("plugin.context"), + cfg: cfg, + cacheService: cacheService, + pluginStore: pluginStore, + dataSourceCache: dataSourceCache, + dataSourceService: dataSourceService, + pluginSettingsService: pluginSettingsService, + pluginRequestConfigProvider: pluginRequestConfigProvider, + logger: log.New("plugin.context"), } } type Provider struct { - cfg *setting.Cfg - pluginEnvVars *envvars.Service - cacheService *localcache.CacheService - pluginStore pluginstore.Store - dataSourceCache datasources.CacheService - dataSourceService datasources.DataSourceService - pluginSettingsService pluginsettings.Service - logger log.Logger + cfg *setting.Cfg + pluginRequestConfigProvider pluginconfig.PluginRequestConfigProvider + cacheService *localcache.CacheService + pluginStore pluginstore.Store + dataSourceCache datasources.CacheService + dataSourceService datasources.DataSourceService + pluginSettingsService pluginsettings.Service + logger log.Logger } // Get will retrieve plugin context by the provided pluginID and orgID. @@ -83,7 +82,7 @@ func (p *Provider) Get(ctx context.Context, pluginID string, user identity.Reque pCtx.AppInstanceSettings = appSettings } - settings := p.pluginEnvVars.GetConfigMap(ctx, pluginID, plugin.ExternalService) + settings := p.pluginRequestConfigProvider.PluginRequestConfig(ctx, pluginID) pCtx.GrafanaConfig = backend.NewGrafanaCfg(settings) ua, err := useragent.New(p.cfg.BuildVersion, runtime.GOOS, runtime.GOARCH) @@ -120,7 +119,7 @@ func (p *Provider) GetWithDataSource(ctx context.Context, pluginID string, user } pCtx.DataSourceInstanceSettings = datasourceSettings - settings := p.pluginEnvVars.GetConfigMap(ctx, pluginID, plugin.ExternalService) + settings := p.pluginRequestConfigProvider.PluginRequestConfig(ctx, pluginID) pCtx.GrafanaConfig = backend.NewGrafanaCfg(settings) ua, err := useragent.New(p.cfg.BuildVersion, runtime.GOOS, runtime.GOARCH) @@ -168,7 +167,7 @@ func (p *Provider) PluginContextForDataSource(ctx context.Context, datasourceSet pCtx.DataSourceInstanceSettings = datasourceSettings - settings := p.pluginEnvVars.GetConfigMap(ctx, pluginID, plugin.ExternalService) + settings := p.pluginRequestConfigProvider.PluginRequestConfig(ctx, pluginID) pCtx.GrafanaConfig = backend.NewGrafanaCfg(settings) ua, err := useragent.New(p.cfg.BuildVersion, runtime.GOOS, runtime.GOARCH) diff --git a/pkg/services/pluginsintegration/plugincontext/plugincontext_test.go b/pkg/services/pluginsintegration/plugincontext/plugincontext_test.go index 7622f3d29d..feec2e53db 100644 --- a/pkg/services/pluginsintegration/plugincontext/plugincontext_test.go +++ b/pkg/services/pluginsintegration/plugincontext/plugincontext_test.go @@ -10,11 +10,11 @@ import ( "github.com/grafana/grafana/pkg/infra/db/dbtest" "github.com/grafana/grafana/pkg/infra/localcache" "github.com/grafana/grafana/pkg/plugins" - "github.com/grafana/grafana/pkg/plugins/config" pluginFakes "github.com/grafana/grafana/pkg/plugins/manager/fakes" "github.com/grafana/grafana/pkg/plugins/manager/registry" "github.com/grafana/grafana/pkg/services/datasources" fakeDatasources "github.com/grafana/grafana/pkg/services/datasources/fakes" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginconfig" "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" @@ -43,7 +43,7 @@ func TestGet(t *testing.T) { db := &dbtest.FakeDB{ExpectedError: pluginsettings.ErrPluginSettingNotFound} pcp := plugincontext.ProvideService(cfg, localcache.ProvideService(), pluginstore.New(preg, &pluginFakes.FakeLoader{}), &fakeDatasources.FakeCacheService{}, - ds, pluginSettings.ProvideService(db, secretstest.NewFakeSecretsService()), pluginFakes.NewFakeLicensingService(), &config.Cfg{}, + ds, pluginSettings.ProvideService(db, secretstest.NewFakeSecretsService()), pluginconfig.NewFakePluginRequestConfigProvider(), ) identity := &user.SignedInUser{OrgID: int64(1), Login: "admin"} diff --git a/pkg/services/pluginsintegration/pluginsintegration.go b/pkg/services/pluginsintegration/pluginsintegration.go index 0a10913d60..143f91e60d 100644 --- a/pkg/services/pluginsintegration/pluginsintegration.go +++ b/pkg/services/pluginsintegration/pluginsintegration.go @@ -10,6 +10,7 @@ import ( "github.com/grafana/grafana/pkg/plugins/backendplugin/coreplugin" "github.com/grafana/grafana/pkg/plugins/backendplugin/provider" pCfg "github.com/grafana/grafana/pkg/plugins/config" + "github.com/grafana/grafana/pkg/plugins/envvars" "github.com/grafana/grafana/pkg/plugins/log" "github.com/grafana/grafana/pkg/plugins/manager/client" "github.com/grafana/grafana/pkg/plugins/manager/filestore" @@ -35,13 +36,13 @@ import ( "github.com/grafana/grafana/pkg/services/pluginsintegration/angularinspector" "github.com/grafana/grafana/pkg/services/pluginsintegration/angularpatternsstore" "github.com/grafana/grafana/pkg/services/pluginsintegration/clientmiddleware" - "github.com/grafana/grafana/pkg/services/pluginsintegration/config" "github.com/grafana/grafana/pkg/services/pluginsintegration/keyretriever" "github.com/grafana/grafana/pkg/services/pluginsintegration/keyretriever/dynamic" "github.com/grafana/grafana/pkg/services/pluginsintegration/keystore" "github.com/grafana/grafana/pkg/services/pluginsintegration/licensing" "github.com/grafana/grafana/pkg/services/pluginsintegration/loader" "github.com/grafana/grafana/pkg/services/pluginsintegration/pipeline" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginconfig" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginerrs" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginexternal" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings" @@ -55,7 +56,12 @@ import ( // WireSet provides a wire.ProviderSet of plugin providers. var WireSet = wire.NewSet( - config.ProvideConfig, + pluginconfig.ProvidePluginManagementConfig, + pluginconfig.ProvidePluginInstanceConfig, + pluginconfig.NewEnvVarsProvider, + wire.Bind(new(envvars.Provider), new(*pluginconfig.EnvVarsProvider)), + pluginconfig.NewRequestConfigProvider, + wire.Bind(new(pluginconfig.PluginRequestConfigProvider), new(*pluginconfig.RequestConfigProvider)), pluginstore.ProvideService, wire.Bind(new(pluginstore.Store), new(*pluginstore.Service)), wire.Bind(new(plugins.SecretsPluginManager), new(*pluginstore.Service)), @@ -130,7 +136,7 @@ var WireExtensionSet = wire.NewSet( ) func ProvideClientDecorator( - cfg *setting.Cfg, pCfg *pCfg.Cfg, + cfg *setting.Cfg, pCfg *pCfg.PluginManagementCfg, pluginRegistry registry.Service, oAuthTokenService oauthtoken.OAuthTokenService, tracer tracing.Tracer, @@ -142,7 +148,7 @@ func ProvideClientDecorator( } func NewClientDecorator( - cfg *setting.Cfg, pCfg *pCfg.Cfg, + cfg *setting.Cfg, pCfg *pCfg.PluginManagementCfg, pluginRegistry registry.Service, oAuthTokenService oauthtoken.OAuthTokenService, tracer tracing.Tracer, cachingService caching.CachingService, features *featuremgmt.FeatureManager, promRegisterer prometheus.Registerer, registry registry.Service, diff --git a/pkg/services/pluginsintegration/renderer/renderer.go b/pkg/services/pluginsintegration/renderer/renderer.go index fda142b539..57a276c6ce 100644 --- a/pkg/services/pluginsintegration/renderer/renderer.go +++ b/pkg/services/pluginsintegration/renderer/renderer.go @@ -8,7 +8,7 @@ import ( "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/backendplugin/pluginextensionv2" "github.com/grafana/grafana/pkg/plugins/backendplugin/provider" - "github.com/grafana/grafana/pkg/plugins/config" + pluginscfg "github.com/grafana/grafana/pkg/plugins/config" "github.com/grafana/grafana/pkg/plugins/envvars" "github.com/grafana/grafana/pkg/plugins/manager/loader" "github.com/grafana/grafana/pkg/plugins/manager/pipeline/bootstrap" @@ -21,10 +21,12 @@ import ( "github.com/grafana/grafana/pkg/plugins/manager/sources" "github.com/grafana/grafana/pkg/services/pluginsintegration/pipeline" "github.com/grafana/grafana/pkg/services/rendering" + "github.com/grafana/grafana/pkg/setting" ) -func ProvideService(cfg *config.Cfg, registry registry.Service, licensing plugins.Licensing) (*Manager, error) { - l, err := createLoader(cfg, registry, licensing) +func ProvideService(cfg *setting.Cfg, pCfg *pluginscfg.PluginManagementCfg, pluginEnvProvider envvars.Provider, registry registry.Service, + licensing plugins.Licensing) (*Manager, error) { + l, err := createLoader(cfg, pCfg, pluginEnvProvider, registry, licensing) if err != nil { return nil, err } @@ -33,14 +35,14 @@ func ProvideService(cfg *config.Cfg, registry registry.Service, licensing plugin } type Manager struct { - cfg *config.Cfg + cfg *setting.Cfg loader loader.Service log log.Logger renderer *Plugin } -func NewManager(cfg *config.Cfg, loader loader.Service) *Manager { +func NewManager(cfg *setting.Cfg, loader loader.Service) *Manager { return &Manager{ cfg: cfg, loader: loader, @@ -102,8 +104,9 @@ func (m *Manager) Renderer(ctx context.Context) (rendering.Plugin, bool) { return nil, false } -func createLoader(cfg *config.Cfg, pr registry.Service, l plugins.Licensing) (loader.Service, error) { - d := discovery.New(cfg, discovery.Opts{ +func createLoader(cfg *setting.Cfg, pCfg *pluginscfg.PluginManagementCfg, pluginEnvProvider envvars.Provider, + pr registry.Service, l plugins.Licensing) (loader.Service, error) { + d := discovery.New(pCfg, discovery.Opts{ FindFilterFuncs: []discovery.FindFilterFunc{ discovery.NewPermittedPluginTypesFilterStep([]plugins.Type{plugins.TypeRenderer}), func(ctx context.Context, class plugins.Class, bundles []*plugins.FoundBundle) ([]*plugins.FoundBundle, error) { @@ -111,21 +114,21 @@ func createLoader(cfg *config.Cfg, pr registry.Service, l plugins.Licensing) (lo }, }, }) - b := bootstrap.New(cfg, bootstrap.Opts{ + b := bootstrap.New(pCfg, bootstrap.Opts{ DecorateFuncs: []bootstrap.DecorateFunc{}, // no decoration required }) - v := validation.New(cfg, validation.Opts{ + v := validation.New(pCfg, validation.Opts{ ValidateFuncs: []validation.ValidateFunc{ - validation.SignatureValidationStep(signature.NewValidator(signature.NewUnsignedAuthorizer(cfg))), + validation.SignatureValidationStep(signature.NewValidator(signature.NewUnsignedAuthorizer(pCfg))), }, }) - i := initialization.New(cfg, initialization.Opts{ + i := initialization.New(pCfg, initialization.Opts{ InitializeFuncs: []initialization.InitializeFunc{ - initialization.BackendClientInitStep(envvars.NewProvider(cfg, l), provider.New(provider.RendererProvider)), + initialization.BackendClientInitStep(pluginEnvProvider, provider.New(provider.RendererProvider)), initialization.PluginRegistrationStep(pr), }, }) - t, err := termination.New(cfg, termination.Opts{ + t, err := termination.New(pCfg, termination.Opts{ TerminateFuncs: []termination.TerminateFunc{ termination.DeregisterStep(pr), }, diff --git a/pkg/services/pluginsintegration/renderer/renderer_test.go b/pkg/services/pluginsintegration/renderer/renderer_test.go index a2c8230bb7..0c5bdbfd90 100644 --- a/pkg/services/pluginsintegration/renderer/renderer_test.go +++ b/pkg/services/pluginsintegration/renderer/renderer_test.go @@ -9,8 +9,8 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/plugins" - "github.com/grafana/grafana/pkg/plugins/config" "github.com/grafana/grafana/pkg/plugins/manager/fakes" + "github.com/grafana/grafana/pkg/setting" ) func TestRenderer(t *testing.T) { @@ -33,7 +33,7 @@ func TestRenderer(t *testing.T) { return nil, nil }, } - cfg := &config.Cfg{ + cfg := &setting.Cfg{ PluginsPath: filepath.Join(testdataDir), } @@ -67,7 +67,7 @@ func TestRenderer(t *testing.T) { return nil, nil }, } - cfg := &config.Cfg{ + cfg := &setting.Cfg{ PluginsPath: filepath.Join(testdataDir), } diff --git a/pkg/services/pluginsintegration/serviceregistration/serviceregistration.go b/pkg/services/pluginsintegration/serviceregistration/serviceregistration.go index 08b1661500..e5b7c19d6f 100644 --- a/pkg/services/pluginsintegration/serviceregistration/serviceregistration.go +++ b/pkg/services/pluginsintegration/serviceregistration/serviceregistration.go @@ -21,7 +21,7 @@ type Service struct { settingsSvc pluginsettings.Service } -func ProvideService(cfg *config.Cfg, reg extsvcauth.ExternalServiceRegistry, settingsSvc pluginsettings.Service) *Service { +func ProvideService(cfg *config.PluginManagementCfg, reg extsvcauth.ExternalServiceRegistry, settingsSvc pluginsettings.Service) *Service { s := &Service{ featureEnabled: cfg.Features.IsEnabledGlobally(featuremgmt.FlagExternalServiceAccounts), log: log.New("plugins.external.registration"), diff --git a/pkg/services/pluginsintegration/test_helper.go b/pkg/services/pluginsintegration/test_helper.go index 600cdc976e..7d6670e430 100644 --- a/pkg/services/pluginsintegration/test_helper.go +++ b/pkg/services/pluginsintegration/test_helper.go @@ -28,8 +28,8 @@ import ( "github.com/grafana/grafana/pkg/plugins/manager/sources" "github.com/grafana/grafana/pkg/plugins/pluginscdn" "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/services/pluginsintegration/config" "github.com/grafana/grafana/pkg/services/pluginsintegration/pipeline" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginconfig" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginerrs" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" "github.com/grafana/grafana/pkg/setting" @@ -42,7 +42,7 @@ type IntegrationTestCtx struct { } func CreateIntegrationTestCtx(t *testing.T, cfg *setting.Cfg, coreRegistry *coreplugin.Registry) *IntegrationTestCtx { - pCfg, err := config.ProvideConfig(setting.ProvideProvider(cfg), cfg, featuremgmt.WithFeatures()) + pCfg, err := pluginconfig.ProvidePluginManagementConfig(cfg, setting.ProvideProvider(cfg), featuremgmt.WithFeatures()) require.NoError(t, err) cdn := pluginscdn.ProvideService(pCfg) @@ -54,7 +54,7 @@ func CreateIntegrationTestCtx(t *testing.T, cfg *setting.Cfg, coreRegistry *core disc := pipeline.ProvideDiscoveryStage(pCfg, finder.NewLocalFinder(true, pCfg.Features), reg) boot := pipeline.ProvideBootstrapStage(pCfg, signature.ProvideService(pCfg, statickey.New()), assetpath.ProvideService(pCfg, cdn)) valid := pipeline.ProvideValidationStage(pCfg, signature.NewValidator(signature.NewUnsignedAuthorizer(pCfg)), angularInspector, errTracker) - init := pipeline.ProvideInitializationStage(pCfg, reg, fakes.NewFakeLicensingService(), provider.ProvideService(coreRegistry), proc, &fakes.FakeAuthService{}, fakes.NewFakeRoleRegistry()) + init := pipeline.ProvideInitializationStage(pCfg, reg, provider.ProvideService(coreRegistry), proc, &fakes.FakeAuthService{}, fakes.NewFakeRoleRegistry(), nil) term, err := pipeline.ProvideTerminationStage(pCfg, reg, proc) require.NoError(t, err) @@ -84,7 +84,7 @@ type LoaderOpts struct { Initializer initialization.Initializer } -func CreateTestLoader(t *testing.T, cfg *pluginsCfg.Cfg, opts LoaderOpts) *loader.Loader { +func CreateTestLoader(t *testing.T, cfg *pluginsCfg.PluginManagementCfg, opts LoaderOpts) *loader.Loader { if opts.Discoverer == nil { opts.Discoverer = pipeline.ProvideDiscoveryStage(cfg, finder.NewLocalFinder(cfg.DevMode, cfg.Features), registry.ProvideService()) } @@ -100,7 +100,7 @@ func CreateTestLoader(t *testing.T, cfg *pluginsCfg.Cfg, opts LoaderOpts) *loade if opts.Initializer == nil { reg := registry.ProvideService() coreRegistry := coreplugin.NewRegistry(make(map[string]backendplugin.PluginFactoryFunc)) - opts.Initializer = pipeline.ProvideInitializationStage(cfg, reg, fakes.NewFakeLicensingService(), provider.ProvideService(coreRegistry), process.ProvideService(), &fakes.FakeAuthService{}, fakes.NewFakeRoleRegistry()) + opts.Initializer = pipeline.ProvideInitializationStage(cfg, reg, provider.ProvideService(coreRegistry), process.ProvideService(), &fakes.FakeAuthService{}, fakes.NewFakeRoleRegistry(), nil) } if opts.Terminator == nil { diff --git a/pkg/services/publicdashboards/api/common_test.go b/pkg/services/publicdashboards/api/common_test.go index 5e07e1ecff..e4ea82ea5d 100644 --- a/pkg/services/publicdashboards/api/common_test.go +++ b/pkg/services/publicdashboards/api/common_test.go @@ -16,8 +16,6 @@ import ( "github.com/grafana/grafana/pkg/infra/localcache" "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/manager/fakes" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" "github.com/grafana/grafana/pkg/services/contexthandler/ctxkey" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" @@ -27,6 +25,7 @@ import ( datasourceService "github.com/grafana/grafana/pkg/services/datasources/service" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/licensing/licensingtest" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginconfig" "github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext" pluginSettings "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings/service" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" @@ -150,8 +149,7 @@ func buildQueryDataService(t *testing.T, cs datasources.CacheService, fpc *fakeP }, }, }, &fakeDatasources.FakeCacheService{}, ds, - pluginSettings.ProvideService(store, fakeSecrets.NewFakeSecretsService()), fakes.NewFakeLicensingService(), - &config.Cfg{}) + pluginSettings.ProvideService(store, fakeSecrets.NewFakeSecretsService()), pluginconfig.NewFakePluginRequestConfigProvider()) return query.ProvideService( setting.NewCfg(), diff --git a/pkg/services/query/query_test.go b/pkg/services/query/query_test.go index c5786af6b3..486529466f 100644 --- a/pkg/services/query/query_test.go +++ b/pkg/services/query/query_test.go @@ -24,8 +24,6 @@ import ( "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/models/roletype" "github.com/grafana/grafana/pkg/plugins" - "github.com/grafana/grafana/pkg/plugins/config" - pluginFakes "github.com/grafana/grafana/pkg/plugins/manager/fakes" "github.com/grafana/grafana/pkg/services/auth/identity" "github.com/grafana/grafana/pkg/services/contexthandler" "github.com/grafana/grafana/pkg/services/contexthandler/ctxkey" @@ -33,6 +31,7 @@ import ( "github.com/grafana/grafana/pkg/services/datasources" fakeDatasources "github.com/grafana/grafana/pkg/services/datasources/fakes" "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginconfig" "github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext" pluginSettings "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings/service" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" @@ -482,7 +481,7 @@ func setup(t *testing.T) *testContext { {JSONData: plugins.JSONData{ID: "mysql"}}, }, }, &fakeDatasources.FakeCacheService{}, fakeDatasourceService, - pluginSettings.ProvideService(sqlStore, secretsService), pluginFakes.NewFakeLicensingService(), &config.Cfg{}, + pluginSettings.ProvideService(sqlStore, secretsService), pluginconfig.NewFakePluginRequestConfigProvider(), ) exprService := expr.ProvideService(&setting.Cfg{ExpressionsEnabled: true}, pc, pCtxProvider, &featuremgmt.FeatureManager{}, nil, tracing.InitializeTracerForTest()) diff --git a/pkg/tsdb/legacydata/service/service_test.go b/pkg/tsdb/legacydata/service/service_test.go index 0abac279e1..a38e7b43bb 100644 --- a/pkg/tsdb/legacydata/service/service_test.go +++ b/pkg/tsdb/legacydata/service/service_test.go @@ -12,13 +12,12 @@ import ( "github.com/grafana/grafana/pkg/infra/localcache" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/plugins" - "github.com/grafana/grafana/pkg/plugins/config" - pluginFakes "github.com/grafana/grafana/pkg/plugins/manager/fakes" acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/datasources/guardian" datasourceservice "github.com/grafana/grafana/pkg/services/datasources/service" "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginconfig" "github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext" pluginSettings "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings/service" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" @@ -54,7 +53,7 @@ func TestHandleRequest(t *testing.T) { pCtxProvider := plugincontext.ProvideService(sqlStore.Cfg, localcache.ProvideService(), &pluginstore.FakePluginStore{ PluginList: []pluginstore.Plugin{{JSONData: plugins.JSONData{ID: "test"}}}, - }, dsCache, dsService, pluginSettings.ProvideService(sqlStore, secretsService), pluginFakes.NewFakeLicensingService(), &config.Cfg{}) + }, dsCache, dsService, pluginSettings.ProvideService(sqlStore, secretsService), pluginconfig.NewFakePluginRequestConfigProvider()) s := ProvideService(client, nil, dsService, pCtxProvider) ds := &datasources.DataSource{ID: 12, Type: "test", JsonData: simplejson.New()} From d94db905c710e4bdc31a67e8a97427b8728872e3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 27 Feb 2024 13:52:12 +0200 Subject: [PATCH 0224/1406] Update dependency @grafana/scenes to v3.8.0 (#83502) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 50 ++++++++++++++++---------------------------------- 1 file changed, 16 insertions(+), 34 deletions(-) diff --git a/yarn.lock b/yarn.lock index 3928b3b251..c95e8dfead 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3594,14 +3594,14 @@ __metadata: languageName: unknown linkType: soft -"@grafana/e2e-selectors@npm:10.0.2": - version: 10.0.2 - resolution: "@grafana/e2e-selectors@npm:10.0.2" +"@grafana/e2e-selectors@npm:10.3.3": + version: 10.3.3 + resolution: "@grafana/e2e-selectors@npm:10.3.3" dependencies: "@grafana/tsconfig": "npm:^1.2.0-rc1" - tslib: "npm:2.5.0" - typescript: "npm:4.8.4" - checksum: 10/8f2ea80ed8408801243b0ea10d504af39f361b6daab98fdc1f1461fc41593e5a026cb055e65a2437bf2f8e8e71150884c5f1173c302c01854e82b8ee17918500 + tslib: "npm:2.6.0" + typescript: "npm:5.2.2" + checksum: 10/11fcbf80d61d30a1ab5a99a6c24c5044c187bf6bb52c5d0a1c99b46ed6b28ea5865ff0b9fdfc66c22a744ba5fe9ea2f5030256d952f3b76302cc8cb8ffc01a73 languageName: node linkType: hard @@ -4074,20 +4074,22 @@ __metadata: linkType: soft "@grafana/scenes@npm:^3.5.0": - version: 3.5.0 - resolution: "@grafana/scenes@npm:3.5.0" + version: 3.8.0 + resolution: "@grafana/scenes@npm:3.8.0" dependencies: - "@grafana/e2e-selectors": "npm:10.0.2" + "@grafana/e2e-selectors": "npm:10.3.3" react-grid-layout: "npm:1.3.4" react-use: "npm:17.4.0" react-virtualized-auto-sizer: "npm:1.0.7" uuid: "npm:^9.0.0" peerDependencies: - "@grafana/data": 10.0.3 - "@grafana/runtime": 10.0.3 - "@grafana/schema": 10.0.3 - "@grafana/ui": 10.0.3 - checksum: 10/eac51e8bc4327fea39242e1580e4bc174aff984268dcf7b022dfeab5cd53eed63b72d92048e42e24c467ddf55130dbe555f4d4b0bb0a743ace5294ec90978ca5 + "@grafana/data": ^10.0.3 + "@grafana/runtime": ^10.0.3 + "@grafana/schema": ^10.0.3 + "@grafana/ui": ^10.0.3 + react: ^18.0.0 + react-dom: ^18.0.0 + checksum: 10/711ab47e6689fb378bc6a49e5acd2fca7948001961dfb783b676b2f4ad9ece7f2e85a2c3a47aae1e9f105d647864bd568e2bcd1f6e29bd53028e4983cd36d4b1 languageName: node linkType: hard @@ -30370,16 +30372,6 @@ __metadata: 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: 10/f985d8dd6ae815753d61cb81e434f3a4a5796ac52e423370fca6ad11bcd188df4013d82e3ba3b88c9746745b9341390ba68f862dc9d30bac6465e0699f2a795b - languageName: node - linkType: hard - "typescript@npm:5.2.2": version: 5.2.2 resolution: "typescript@npm:5.2.2" @@ -30400,16 +30392,6 @@ __metadata: languageName: node linkType: hard -"typescript@patch:typescript@npm%3A4.8.4#optional!builtin": - version: 4.8.4 - resolution: "typescript@patch:typescript@npm%3A4.8.4#optional!builtin::version=4.8.4&hash=1a91c8" - bin: - tsc: bin/tsc - tsserver: bin/tsserver - checksum: 10/5d81fd8cf5152091a0c0b84ebc868de8433583072a340c4899e0fc7ad6a80314b880a1466868c9a6a1f640c3d1f2fe7f41f8c541b99d78c8b414263dfa27eba3 - languageName: node - linkType: hard - "typescript@patch:typescript@npm%3A5.2.2#optional!builtin": version: 5.2.2 resolution: "typescript@patch:typescript@npm%3A5.2.2#optional!builtin::version=5.2.2&hash=f3b441" From 92f53d3670c91de7bc02fca259d5a47fc2619480 Mon Sep 17 00:00:00 2001 From: Joey <90795735+joey-grafana@users.noreply.github.com> Date: Tue, 27 Feb 2024 12:28:17 +0000 Subject: [PATCH 0225/1406] Tracing: Add node graph panel suggestion (#83311) * Add node graph panel suggestion * Update logic * Add comment * Check for correct fields * Simplify logic * Also check for viz type * Remove extra logic on boolean --- .../features/panel/state/getAllSuggestions.ts | 1 + public/app/plugins/panel/nodeGraph/module.tsx | 75 ++++++++++--------- .../plugins/panel/nodeGraph/suggestions.ts | 71 ++++++++++++++++++ public/app/types/suggestions.ts | 1 + 4 files changed, 112 insertions(+), 36 deletions(-) create mode 100644 public/app/plugins/panel/nodeGraph/suggestions.ts diff --git a/public/app/features/panel/state/getAllSuggestions.ts b/public/app/features/panel/state/getAllSuggestions.ts index ee092d3b58..873e2166da 100644 --- a/public/app/features/panel/state/getAllSuggestions.ts +++ b/public/app/features/panel/state/getAllSuggestions.ts @@ -22,6 +22,7 @@ export const panelsToCheckFirst = [ 'candlestick', 'flamegraph', 'traces', + 'nodeGraph', ]; export async function getAllSuggestions(data?: PanelData, panel?: PanelModel): Promise { diff --git a/public/app/plugins/panel/nodeGraph/module.tsx b/public/app/plugins/panel/nodeGraph/module.tsx index 5d444e17e3..27c4d06a7f 100644 --- a/public/app/plugins/panel/nodeGraph/module.tsx +++ b/public/app/plugins/panel/nodeGraph/module.tsx @@ -2,41 +2,44 @@ import { PanelPlugin } from '@grafana/data'; import { NodeGraphPanel } from './NodeGraphPanel'; import { ArcOptionsEditor } from './editor/ArcOptionsEditor'; +import { NodeGraphSuggestionsSupplier } from './suggestions'; import { NodeGraphOptions } from './types'; -export const plugin = new PanelPlugin(NodeGraphPanel).setPanelOptions((builder, context) => { - builder.addNestedOptions({ - category: ['Nodes'], - path: 'nodes', - build: (builder) => { - builder.addUnitPicker({ - name: 'Main stat unit', - path: 'mainStatUnit', - }); - builder.addUnitPicker({ - name: 'Secondary stat unit', - path: 'secondaryStatUnit', - }); - builder.addCustomEditor({ - name: 'Arc sections', - path: 'arcs', - id: 'arcs', - editor: ArcOptionsEditor, - }); - }, - }); - builder.addNestedOptions({ - category: ['Edges'], - path: 'edges', - build: (builder) => { - builder.addUnitPicker({ - name: 'Main stat unit', - path: 'mainStatUnit', - }); - builder.addUnitPicker({ - name: 'Secondary stat unit', - path: 'secondaryStatUnit', - }); - }, - }); -}); +export const plugin = new PanelPlugin(NodeGraphPanel) + .setPanelOptions((builder, context) => { + builder.addNestedOptions({ + category: ['Nodes'], + path: 'nodes', + build: (builder) => { + builder.addUnitPicker({ + name: 'Main stat unit', + path: 'mainStatUnit', + }); + builder.addUnitPicker({ + name: 'Secondary stat unit', + path: 'secondaryStatUnit', + }); + builder.addCustomEditor({ + name: 'Arc sections', + path: 'arcs', + id: 'arcs', + editor: ArcOptionsEditor, + }); + }, + }); + builder.addNestedOptions({ + category: ['Edges'], + path: 'edges', + build: (builder) => { + builder.addUnitPicker({ + name: 'Main stat unit', + path: 'mainStatUnit', + }); + builder.addUnitPicker({ + name: 'Secondary stat unit', + path: 'secondaryStatUnit', + }); + }, + }); + }) + .setSuggestionsSupplier(new NodeGraphSuggestionsSupplier()); diff --git a/public/app/plugins/panel/nodeGraph/suggestions.ts b/public/app/plugins/panel/nodeGraph/suggestions.ts new file mode 100644 index 0000000000..eb4a4773ef --- /dev/null +++ b/public/app/plugins/panel/nodeGraph/suggestions.ts @@ -0,0 +1,71 @@ +import { DataFrame, FieldType, VisualizationSuggestionsBuilder, VisualizationSuggestionScore } from '@grafana/data'; +import { SuggestionName } from 'app/types/suggestions'; + +export class NodeGraphSuggestionsSupplier { + getListWithDefaults(builder: VisualizationSuggestionsBuilder) { + return builder.getListAppender<{}, {}>({ + name: SuggestionName.NodeGraph, + pluginId: 'nodeGraph', + }); + } + + hasCorrectFields(frames: DataFrame[]): boolean { + let hasNodesFrame = false; + let hasEdgesFrame = false; + + const nodeFields: Array<[string, FieldType]> = [ + ['id', FieldType.string], + ['title', FieldType.string], + ['mainstat', FieldType.number], + ]; + const edgeFields: Array<[string, FieldType]> = [ + ['id', FieldType.string], + ['source', FieldType.string], + ['target', FieldType.string], + ]; + + for (const frame of frames) { + if (this.checkFields(nodeFields, frame)) { + hasNodesFrame = true; + } + if (this.checkFields(edgeFields, frame)) { + hasEdgesFrame = true; + } + } + + return hasNodesFrame && hasEdgesFrame; + } + + checkFields(fields: Array<[string, FieldType]>, frame: DataFrame): boolean { + let hasCorrectFields = true; + + for (const field of fields) { + const [name, type] = field; + const frameField = frame.fields.find((f) => f.name === name); + if (!frameField || type !== frameField.type) { + hasCorrectFields = false; + break; + } + } + + return hasCorrectFields; + } + + getSuggestionsForData(builder: VisualizationSuggestionsBuilder) { + if (!builder.data) { + return; + } + + const hasCorrectFields = this.hasCorrectFields(builder.data.series); + const nodeGraphFrames = builder.data.series.filter( + (df) => df.meta && df.meta.preferredVisualisationType === 'nodeGraph' + ); + + if (hasCorrectFields || nodeGraphFrames.length === 2) { + this.getListWithDefaults(builder).append({ + name: SuggestionName.NodeGraph, + score: VisualizationSuggestionScore.Best, + }); + } + } +} diff --git a/public/app/types/suggestions.ts b/public/app/types/suggestions.ts index 5d6c813d34..2df8c82b9a 100644 --- a/public/app/types/suggestions.ts +++ b/public/app/types/suggestions.ts @@ -29,4 +29,5 @@ export enum SuggestionName { Logs = 'Logs', FlameGraph = 'Flame graph', Trace = 'Trace', + NodeGraph = 'Node graph', } From ac284def029eb2fec919e77ddbf515d8a3fafa99 Mon Sep 17 00:00:00 2001 From: Saturn IDC <38378045+fly3366@users.noreply.github.com> Date: Tue, 27 Feb 2024 20:29:58 +0800 Subject: [PATCH 0226/1406] CI: Fix missing vendor dependencies (#83464) --- Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Dockerfile b/Dockerfile index 27f52d1757..e44dbb4826 100644 --- a/Dockerfile +++ b/Dockerfile @@ -55,6 +55,8 @@ COPY .bingo .bingo # Include vendored dependencies COPY pkg/util/xorm/go.* pkg/util/xorm/ +COPY pkg/apiserver/go.* pkg/apiserver/ +COPY pkg/apimachinery/go.* pkg/apimachinery/ RUN go mod download RUN if [[ "$BINGO" = "true" ]]; then \ From db131f33123c5c704fd8d073e71d1cc216c95e04 Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Tue, 27 Feb 2024 12:52:21 +0000 Subject: [PATCH 0227/1406] E2C: Refactor api with dataWithMockDelay helper: (#83509) * E2C: Refactor api with dataWithMockDelay helper: * update codeowners * owner comment --- .github/CODEOWNERS | 4 +++ .../features/admin/migrate-to-cloud/api.ts | 35 +++++++++---------- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 8c5bc80e19..b35e441ffb 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -381,6 +381,10 @@ playwright.config.ts @grafana/plugins-platform-frontend /public/app/core/components/TimelineChart/ @grafana/dataviz-squad /public/app/features/all.ts @grafana/grafana-frontend-platform /public/app/features/admin/ @grafana/identity-access-team + +# Temp owners until Enterprise team takes over +/public/app/features/admin/migrate-to-cloud @grafana/grafana-frontend-platform + /public/app/features/auth-config/ @grafana/identity-access-team /public/app/features/annotations/ @grafana/dashboards-squad /public/app/features/api-keys/ @grafana/identity-access-team diff --git a/public/app/features/admin/migrate-to-cloud/api.ts b/public/app/features/admin/migrate-to-cloud/api.ts index b144fce904..405334ac9a 100644 --- a/public/app/features/admin/migrate-to-cloud/api.ts +++ b/public/app/features/admin/migrate-to-cloud/api.ts @@ -40,6 +40,14 @@ const MOCK_DELAY_MS = 1000; const MOCK_TOKEN = 'TODO_thisWillBeABigLongToken'; let HAS_MIGRATION_TOKEN = false; +function dataWithMockDelay(data: T): Promise<{ data: T }> { + return new Promise((resolve) => { + setTimeout(() => { + resolve({ data }); + }, MOCK_DELAY_MS); + }); +} + export const migrateToCloudAPI = createApi({ tagTypes: ['migrationToken'], reducerPath: 'migrateToCloudAPI', @@ -47,38 +55,29 @@ export const migrateToCloudAPI = createApi({ endpoints: (builder) => ({ // TODO :) getStatus: builder.query({ - queryFn: () => ({ data: { enabled: false } }), + queryFn: () => dataWithMockDelay({ enabled: true }), }), + createMigrationToken: builder.mutation({ invalidatesTags: ['migrationToken'], queryFn: async () => { - return new Promise((resolve) => { - setTimeout(() => { - HAS_MIGRATION_TOKEN = true; - resolve({ data: { token: MOCK_TOKEN } }); - }, MOCK_DELAY_MS); - }); + HAS_MIGRATION_TOKEN = true; + return dataWithMockDelay({ token: MOCK_TOKEN }); }, }), + deleteMigrationToken: builder.mutation({ invalidatesTags: ['migrationToken'], queryFn: async () => { - return new Promise((resolve) => { - setTimeout(() => { - HAS_MIGRATION_TOKEN = false; - resolve({ data: undefined }); - }, MOCK_DELAY_MS); - }); + HAS_MIGRATION_TOKEN = false; + return dataWithMockDelay(undefined); }, }), + hasMigrationToken: builder.query({ providesTags: ['migrationToken'], queryFn: async () => { - return new Promise((resolve) => { - setTimeout(() => { - resolve({ data: HAS_MIGRATION_TOKEN }); - }, MOCK_DELAY_MS); - }); + return dataWithMockDelay(HAS_MIGRATION_TOKEN); }, }), }), From 98efb2cf32c1d6e31c2b34daf9e1f62a58ce3e1d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 27 Feb 2024 12:52:47 +0000 Subject: [PATCH 0228/1406] Update dependency browserslist to v4.23.0 (#83505) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/yarn.lock b/yarn.lock index c95e8dfead..008864ed1c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12479,16 +12479,16 @@ __metadata: linkType: hard "browserslist@npm:^4.0.0, browserslist@npm:^4.14.5, browserslist@npm:^4.21.10, browserslist@npm:^4.21.4, browserslist@npm:^4.22.2": - version: 4.22.3 - resolution: "browserslist@npm:4.22.3" + version: 4.23.0 + resolution: "browserslist@npm:4.23.0" dependencies: - caniuse-lite: "npm:^1.0.30001580" - electron-to-chromium: "npm:^1.4.648" + caniuse-lite: "npm:^1.0.30001587" + electron-to-chromium: "npm:^1.4.668" node-releases: "npm:^2.0.14" update-browserslist-db: "npm:^1.0.13" bin: browserslist: cli.js - checksum: 10/d46a906c79dfe95d9702c020afbe5b7b4dbe2019b85432e7a020326adff27e63e3c0a52dc8d4e73247060bbe2c13f000714741903cf96a16baae9c216dc74c75 + checksum: 10/496c3862df74565dd942b4ae65f502c575cbeba1fa4a3894dad7aa3b16130dc3033bc502d8848147f7b625154a284708253d9598bcdbef5a1e34cf11dc7bad8e languageName: node linkType: hard @@ -12779,10 +12779,10 @@ __metadata: languageName: node linkType: hard -"caniuse-lite@npm:^1.0.0, caniuse-lite@npm:^1.0.30001578, caniuse-lite@npm:^1.0.30001580": - version: 1.0.30001581 - resolution: "caniuse-lite@npm:1.0.30001581" - checksum: 10/c2d049514e6af5e9a9b23646b7828191f4c2d3ef1ad999d3efe02683d56d0067d616e2eadb055fe5477f870b22e7252dc09834f95007c95f310d8eca30cfa912 +"caniuse-lite@npm:^1.0.0, caniuse-lite@npm:^1.0.30001578, caniuse-lite@npm:^1.0.30001587": + version: 1.0.30001591 + resolution: "caniuse-lite@npm:1.0.30001591" + checksum: 10/3891fad30a99b984a3a20570c0440d35dda933c79ea190cdb78a1f1743866506a4b41b4389b53a7c0351f2228125f9dc49308463f57e61503e5689b444add1a8 languageName: node linkType: hard @@ -15553,10 +15553,10 @@ __metadata: languageName: node linkType: hard -"electron-to-chromium@npm:^1.4.648": - version: 1.4.648 - resolution: "electron-to-chromium@npm:1.4.648" - checksum: 10/a18f06bafce9017ac7b587f76dac77063a0beb7dfcdf9d5971f72b322f56af6315e4fc3c59154a260a9188c168ac7632538797d57a8c53ab57025ace0c9441f2 +"electron-to-chromium@npm:^1.4.668": + version: 1.4.682 + resolution: "electron-to-chromium@npm:1.4.682" + checksum: 10/ae36e6e5c6c1d8e78fbc7f64baa51a8c58bd483e15bde8c65f5b2f62b490769b80644a389132095803bc553aef52581e7dc1d611f43ac64d6987100bcafd2aec languageName: node linkType: hard From 93f18404c0223ab6759cde4bfcff0fb719b43406 Mon Sep 17 00:00:00 2001 From: brendamuir <100768211+brendamuir@users.noreply.github.com> Date: Tue, 27 Feb 2024 14:00:09 +0100 Subject: [PATCH 0229/1406] Alerting docs: readjust titles (#83504) * Alerting docs: readjust titles * removes outdated videos * removes menutitle --- docs/sources/alerting/_index.md | 2 -- docs/sources/alerting/alerting-rules/_index.md | 7 +++---- .../alerting/alerting-rules/create-grafana-managed-rule.md | 2 -- docs/sources/alerting/monitor/_index.md | 2 +- 4 files changed, 4 insertions(+), 9 deletions(-) diff --git a/docs/sources/alerting/_index.md b/docs/sources/alerting/_index.md index 3feee0d5f1..204dde8121 100644 --- a/docs/sources/alerting/_index.md +++ b/docs/sources/alerting/_index.md @@ -27,8 +27,6 @@ Using Grafana Alerting, you create queries and expressions from multiple data so Grafana Alerting is available for Grafana OSS, Grafana Enterprise, or Grafana Cloud. With Mimir and Loki alert rules you can run alert expressions closer to your data and at massive scale, all managed by the Grafana UI you are already familiar with. -Watch this video to learn more about Grafana Alerting: {{< vimeo 720001629 >}} - _Refer to [Manage your alert rules][alerting-rules] for current instructions._ ## Key features and benefits diff --git a/docs/sources/alerting/alerting-rules/_index.md b/docs/sources/alerting/alerting-rules/_index.md index 1d7c8722d9..a0a1975723 100644 --- a/docs/sources/alerting/alerting-rules/_index.md +++ b/docs/sources/alerting/alerting-rules/_index.md @@ -5,18 +5,17 @@ aliases: - unified-alerting/alerting-rules/ - ./create-alerts/ canonical: https://grafana.com/docs/grafana/latest/alerting/alerting-rules/ -description: Create and manage alert rules +description: Configure alert rules labels: products: - cloud - enterprise - oss -menuTitle: Create and manage alert rules -title: Create and manage alert rules +title: Configure alert rules weight: 120 --- -# Create and manage alert rules +# Configure alert rules An alert rule consists of one or more queries and expressions that select the data you want to measure. It also contains a condition, which is the threshold that an alert rule must meet or exceed in order to fire. diff --git a/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md b/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md index 6a96c58f47..a26c9c2ca7 100644 --- a/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md +++ b/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md @@ -38,8 +38,6 @@ Grafana managed alert rules can only be edited or deleted by users with Edit per If you delete an alerting resource created in the UI, you can no longer retrieve it. To make a backup of your configuration and to be able to restore deleted alerting resources, create your alerting resources using file provisioning, Terraform, or the Alerting API. -Watch this video to learn more about creating alert rules: {{< vimeo 720001934 >}} - In the following sections, we’ll guide you through the process of creating your Grafana-managed alert rules. To create a Grafana-managed alert rule, use the in-product alert creation flow and follow these steps to help you. diff --git a/docs/sources/alerting/monitor/_index.md b/docs/sources/alerting/monitor/_index.md index 6769d67c94..577910d569 100644 --- a/docs/sources/alerting/monitor/_index.md +++ b/docs/sources/alerting/monitor/_index.md @@ -12,7 +12,7 @@ labels: products: - enterprise - oss -menuTitle: Monitor alerting +menuTitle: Monitor title: Meta monitoring weight: 140 --- From 12e0d5bef39c320e7c048e42c3b09e010314b1cb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 27 Feb 2024 13:43:13 +0000 Subject: [PATCH 0230/1406] Update dependency diff to v5.2.0 (#83511) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/yarn.lock b/yarn.lock index 008864ed1c..b9172423a5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9162,9 +9162,9 @@ __metadata: linkType: hard "@types/diff@npm:^5": - version: 5.0.5 - resolution: "@types/diff@npm:5.0.5" - checksum: 10/d3b2f90dcc511a2034ec0eb76f24b36ae8dca93e629742d72af01a9b9e38ec627207ca93e8601608698c6b6126a73247b340e6ef9c285e88eb0cca380a67ac5d + version: 5.0.9 + resolution: "@types/diff@npm:5.0.9" + checksum: 10/6924740cb67a49771ea3753ee9b15c676860a6227b2bf0200ed9cef4111ff0f59fec8c51c1170bd30a8c7370b32673b308a9cd2da28525130f842194a822ef42 languageName: node linkType: hard @@ -15251,9 +15251,9 @@ __metadata: linkType: hard "diff@npm:^5.1.0": - version: 5.1.0 - resolution: "diff@npm:5.1.0" - checksum: 10/f4557032a98b2967fe27b1a91dfcf8ebb6b9a24b1afe616b5c2312465100b861e9b8d4da374be535f2d6b967ce2f53826d7f6edc2a0d32b2ab55abc96acc2f9d + version: 5.2.0 + resolution: "diff@npm:5.2.0" + checksum: 10/01b7b440f83a997350a988e9d2f558366c0f90f15be19f4aa7f1bb3109a4e153dfc3b9fbf78e14ea725717017407eeaa2271e3896374a0181e8f52445740846d languageName: node linkType: hard From d6eefc73034a2db4101eb47131231179b6a10640 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 27 Feb 2024 13:59:24 +0000 Subject: [PATCH 0231/1406] Update dependency eslint to v8.57.0 (#83517) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 4 +- packages/grafana-eslint-rules/package.json | 2 +- packages/grafana-prometheus/package.json | 4 +- yarn.lock | 58 +++++++++++----------- 4 files changed, 34 insertions(+), 34 deletions(-) diff --git a/package.json b/package.json index 683a2d9f15..cd695b8d35 100644 --- a/package.json +++ b/package.json @@ -100,7 +100,7 @@ "@types/d3-scale-chromatic": "3.0.3", "@types/debounce-promise": "3.1.9", "@types/diff": "^5", - "@types/eslint": "8.56.2", + "@types/eslint": "8.56.3", "@types/eslint-scope": "^3.7.7", "@types/file-saver": "2.0.7", "@types/glob": "^8.0.0", @@ -164,7 +164,7 @@ "esbuild": "0.18.12", "esbuild-loader": "3.0.1", "esbuild-plugin-browserslist": "^0.11.0", - "eslint": "8.56.0", + "eslint": "8.57.0", "eslint-config-prettier": "9.1.0", "eslint-plugin-import": "^2.26.0", "eslint-plugin-jest": "27.8.0", diff --git a/packages/grafana-eslint-rules/package.json b/packages/grafana-eslint-rules/package.json index 42ccde7232..28cbed28e0 100644 --- a/packages/grafana-eslint-rules/package.json +++ b/packages/grafana-eslint-rules/package.json @@ -15,7 +15,7 @@ }, "devDependencies": { "@typescript-eslint/types": "^6.0.0", - "eslint": "8.56.0", + "eslint": "8.57.0", "tslib": "2.6.2" }, "private": true diff --git a/packages/grafana-prometheus/package.json b/packages/grafana-prometheus/package.json index 380f12ab45..9b01f15272 100644 --- a/packages/grafana-prometheus/package.json +++ b/packages/grafana-prometheus/package.json @@ -89,7 +89,7 @@ "@testing-library/user-event": "14.5.2", "@types/d3": "7.4.3", "@types/debounce-promise": "3.1.9", - "@types/eslint": "8.56.2", + "@types/eslint": "8.56.3", "@types/jest": "29.5.12", "@types/jquery": "3.5.29", "@types/lodash": "4.14.202", @@ -110,7 +110,7 @@ "copy-webpack-plugin": "12.0.2", "css-loader": "6.10.0", "esbuild": "0.18.12", - "eslint": "8.56.0", + "eslint": "8.57.0", "eslint-config-prettier": "9.1.0", "eslint-plugin-import": "^2.26.0", "eslint-plugin-jest": "27.8.0", diff --git a/yarn.lock b/yarn.lock index b9172423a5..5d85996201 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3066,10 +3066,10 @@ __metadata: languageName: node linkType: hard -"@eslint/js@npm:8.56.0": - version: 8.56.0 - resolution: "@eslint/js@npm:8.56.0" - checksum: 10/97a4b5ccf7e24f4d205a1fb0f21cdcd610348ecf685f6798a48dd41ba443f2c1eedd3050ff5a0b8f30b8cf6501ab512aa9b76e531db15e59c9ebaa41f3162e37 +"@eslint/js@npm:8.57.0": + version: 8.57.0 + resolution: "@eslint/js@npm:8.57.0" + checksum: 10/3c501ce8a997cf6cbbaf4ed358af5492875e3550c19b9621413b82caa9ae5382c584b0efa79835639e6e0ddaa568caf3499318e5bdab68643ef4199dce5eb0a0 languageName: node linkType: hard @@ -3697,7 +3697,7 @@ __metadata: dependencies: "@typescript-eslint/types": "npm:^6.0.0" "@typescript-eslint/utils": "npm:^6.0.0" - eslint: "npm:8.56.0" + eslint: "npm:8.57.0" tslib: "npm:2.6.2" languageName: unknown linkType: soft @@ -3946,7 +3946,7 @@ __metadata: "@testing-library/user-event": "npm:14.5.2" "@types/d3": "npm:7.4.3" "@types/debounce-promise": "npm:3.1.9" - "@types/eslint": "npm:8.56.2" + "@types/eslint": "npm:8.56.3" "@types/jest": "npm:29.5.12" "@types/jquery": "npm:3.5.29" "@types/lodash": "npm:4.14.202" @@ -3970,7 +3970,7 @@ __metadata: date-fns: "npm:3.3.1" debounce-promise: "npm:3.1.2" esbuild: "npm:0.18.12" - eslint: "npm:8.56.0" + eslint: "npm:8.57.0" eslint-config-prettier: "npm:9.1.0" eslint-plugin-import: "npm:^2.26.0" eslint-plugin-jest: "npm:27.8.0" @@ -4321,14 +4321,14 @@ __metadata: languageName: node linkType: hard -"@humanwhocodes/config-array@npm:^0.11.13": - version: 0.11.13 - resolution: "@humanwhocodes/config-array@npm:0.11.13" +"@humanwhocodes/config-array@npm:^0.11.13, @humanwhocodes/config-array@npm:^0.11.14": + version: 0.11.14 + resolution: "@humanwhocodes/config-array@npm:0.11.14" dependencies: - "@humanwhocodes/object-schema": "npm:^2.0.1" - debug: "npm:^4.1.1" + "@humanwhocodes/object-schema": "npm:^2.0.2" + debug: "npm:^4.3.1" minimatch: "npm:^3.0.5" - checksum: 10/9f655e1df7efa5a86822cd149ca5cef57240bb8ffd728f0c07cc682cc0a15c6bdce68425fbfd58f9b3e8b16f79b3fd8cb1e96b10c434c9a76f20b2a89f213272 + checksum: 10/3ffb24ecdfab64014a230e127118d50a1a04d11080cbb748bc21629393d100850496456bbcb4e8c438957fe0934430d731042f1264d6a167b62d32fc2863580a languageName: node linkType: hard @@ -4339,10 +4339,10 @@ __metadata: languageName: node linkType: hard -"@humanwhocodes/object-schema@npm:^2.0.1": - version: 2.0.1 - resolution: "@humanwhocodes/object-schema@npm:2.0.1" - checksum: 10/dbddfd0465aecf92ed845ec30d06dba3f7bb2496d544b33b53dac7abc40370c0e46b8787b268d24a366730d5eeb5336ac88967232072a183905ee4abf7df4dab +"@humanwhocodes/object-schema@npm:^2.0.2": + version: 2.0.2 + resolution: "@humanwhocodes/object-schema@npm:2.0.2" + checksum: 10/ef915e3e2f34652f3d383b28a9a99cfea476fa991482370889ab14aac8ecd2b38d47cc21932526c6d949da0daf4a4a6bf629d30f41b0caca25e146819cbfa70e languageName: node linkType: hard @@ -9215,13 +9215,13 @@ __metadata: languageName: node linkType: hard -"@types/eslint@npm:*, @types/eslint@npm:8.56.2, @types/eslint@npm:^8.37.0, @types/eslint@npm:^8.4.10": - version: 8.56.2 - resolution: "@types/eslint@npm:8.56.2" +"@types/eslint@npm:*, @types/eslint@npm:8.56.3, @types/eslint@npm:^8.37.0, @types/eslint@npm:^8.4.10": + version: 8.56.3 + resolution: "@types/eslint@npm:8.56.3" dependencies: "@types/estree": "npm:*" "@types/json-schema": "npm:*" - checksum: 10/9e4805e770ea90a561e1f69e5edce28b8f66e92e290705100e853c7c252cf87bef654168d0d47fc60c0effbe4517dd7a8d2fa6d3f04c7f831367d568009fd368 + checksum: 10/b5a006c24b5d3a2dba5acc12f21f96c960836beb08544cfedbbbd5b7770b6c951b41204d676b73d7d9065bef3435e5b4cb3796c57f66df21c12fd86018993a16 languageName: node linkType: hard @@ -16620,15 +16620,15 @@ __metadata: languageName: node linkType: hard -"eslint@npm:8.56.0": - version: 8.56.0 - resolution: "eslint@npm:8.56.0" +"eslint@npm:8.57.0": + version: 8.57.0 + resolution: "eslint@npm:8.57.0" dependencies: "@eslint-community/eslint-utils": "npm:^4.2.0" "@eslint-community/regexpp": "npm:^4.6.1" "@eslint/eslintrc": "npm:^2.1.4" - "@eslint/js": "npm:8.56.0" - "@humanwhocodes/config-array": "npm:^0.11.13" + "@eslint/js": "npm:8.57.0" + "@humanwhocodes/config-array": "npm:^0.11.14" "@humanwhocodes/module-importer": "npm:^1.0.1" "@nodelib/fs.walk": "npm:^1.2.8" "@ungap/structured-clone": "npm:^1.2.0" @@ -16664,7 +16664,7 @@ __metadata: text-table: "npm:^0.2.0" bin: eslint: bin/eslint.js - checksum: 10/ef6193c6e4cef20774b985a5cc2fd4bf6d3c4decd423117cbc4a0196617861745db291217ad3c537bc3a160650cca965bc818f55e1f3e446af1fcb293f9940a5 + checksum: 10/00496e218b23747a7a9817bf58b522276d0dc1f2e546dceb4eea49f9871574088f72f1f069a6b560ef537efa3a75261b8ef70e51ef19033da1cc4c86a755ef15 languageName: node linkType: hard @@ -18392,7 +18392,7 @@ __metadata: "@types/d3-scale-chromatic": "npm:3.0.3" "@types/debounce-promise": "npm:3.1.9" "@types/diff": "npm:^5" - "@types/eslint": "npm:8.56.2" + "@types/eslint": "npm:8.56.3" "@types/eslint-scope": "npm:^3.7.7" "@types/file-saver": "npm:2.0.7" "@types/glob": "npm:^8.0.0" @@ -18487,7 +18487,7 @@ __metadata: esbuild: "npm:0.18.12" esbuild-loader: "npm:3.0.1" esbuild-plugin-browserslist: "npm:^0.11.0" - eslint: "npm:8.56.0" + eslint: "npm:8.57.0" eslint-config-prettier: "npm:9.1.0" eslint-plugin-import: "npm:^2.26.0" eslint-plugin-jest: "npm:27.8.0" From a8574226bb9a551efe203252345a8befb166bb56 Mon Sep 17 00:00:00 2001 From: kay delaney <45561153+kaydelaney@users.noreply.github.com> Date: Tue, 27 Feb 2024 14:14:01 +0000 Subject: [PATCH 0232/1406] Dashboards: Fixes issue where panels would not refresh if time range updated while in panel view mode (#83418) --- .../dashboard/state/DashboardModel.ts | 34 ++++++++----------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/public/app/features/dashboard/state/DashboardModel.ts b/public/app/features/dashboard/state/DashboardModel.ts index e406dae520..9179f772ee 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 { private panelsAffectedByVariableChange: number[] | null; private appEventsSubscription: Subscription; private lastRefresh: number; - private timeRangeUpdatedDuringEdit = false; + private timeRangeUpdatedDuringEditOrView = false; private originalDashboard: Dashboard | null = null; // ------------------ @@ -426,8 +426,8 @@ export class DashboardModel implements TimeModel { this.events.publish(new TimeRangeUpdatedEvent(timeRange)); dispatch(onTimeRangeUpdated(this.uid, timeRange)); - if (this.panelInEdit) { - this.timeRangeUpdatedDuringEdit = true; + if (this.panelInEdit || this.panelInView) { + this.timeRangeUpdatedDuringEditOrView = true; } } @@ -469,7 +469,7 @@ export class DashboardModel implements TimeModel { initEditPanel(sourcePanel: PanelModel): PanelModel { getTimeSrv().stopAutoRefresh(); this.panelInEdit = sourcePanel.getEditClone(); - this.timeRangeUpdatedDuringEdit = false; + this.timeRangeUpdatedDuringEditOrView = false; return this.panelInEdit; } @@ -479,34 +479,30 @@ export class DashboardModel implements TimeModel { getTimeSrv().resumeAutoRefresh(); - if (this.panelsAffectedByVariableChange || this.timeRangeUpdatedDuringEdit) { - this.startRefresh({ - panelIds: this.panelsAffectedByVariableChange ?? [], - refreshAll: this.timeRangeUpdatedDuringEdit, - }); - this.panelsAffectedByVariableChange = null; - this.timeRangeUpdatedDuringEdit = false; - } + this.refreshIfPanelsAffectedByVariableChangeOrTimeRangeChanged(); } initViewPanel(panel: PanelModel) { this.panelInView = panel; + this.timeRangeUpdatedDuringEditOrView = false; panel.setIsViewing(true); } exitViewPanel(panel: PanelModel) { this.panelInView = undefined; panel.setIsViewing(false); - this.refreshIfPanelsAffectedByVariableChange(); + this.refreshIfPanelsAffectedByVariableChangeOrTimeRangeChanged(); } - private refreshIfPanelsAffectedByVariableChange() { - if (!this.panelsAffectedByVariableChange) { - return; + private refreshIfPanelsAffectedByVariableChangeOrTimeRangeChanged() { + if (this.panelsAffectedByVariableChange || this.timeRangeUpdatedDuringEditOrView) { + this.startRefresh({ + panelIds: this.panelsAffectedByVariableChange ?? [], + refreshAll: this.timeRangeUpdatedDuringEditOrView, + }); + this.panelsAffectedByVariableChange = null; + this.timeRangeUpdatedDuringEditOrView = false; } - - this.startRefresh({ panelIds: this.panelsAffectedByVariableChange, refreshAll: false }); - this.panelsAffectedByVariableChange = null; } private ensurePanelsHaveUniqueIds() { From e068804a9e4844283322105d5be0987bca9ba924 Mon Sep 17 00:00:00 2001 From: Giuseppe Guerra Date: Tue, 27 Feb 2024 15:14:23 +0100 Subject: [PATCH 0233/1406] Plugins: Angular deprecation: Fix AngularDeprecationNotice not being rendered on first page load (#83221) * Plugins: Angular deprecation: Wait for plugins to be inizialized before rendering AngularDeprecationNotice * use then * fix tests * mockCleanUpDashboardAndVariables.mockReset(); * Handle plugin not found * PR review feedback * Add comment * removed unnecessary return * PR review feedback * Use grafanaBootData * Removed comments * fix tests * Use config for hideDeprecation as well --- public/app/features/dashboard/state/DashboardModel.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/public/app/features/dashboard/state/DashboardModel.ts b/public/app/features/dashboard/state/DashboardModel.ts index 9179f772ee..f0afaba766 100644 --- a/public/app/features/dashboard/state/DashboardModel.ts +++ b/public/app/features/dashboard/state/DashboardModel.ts @@ -1338,7 +1338,9 @@ export class DashboardModel implements TimeModel { hasAngularPlugins(): boolean { return this.panels.some((panel) => { // Return false for plugins that are angular but have angular.hideDeprecation = false - const isAngularPanel = panel.isAngularPlugin() && !panel.plugin?.meta.angular?.hideDeprecation; + // We cannot use panel.plugin.isAngularPlugin() because panel.plugin may not be initialized at this stage. + const isAngularPanel = + config.panels[panel.type]?.angular?.detected && !config.panels[panel.type]?.angular?.hideDeprecation; let isAngularDs = false; if (panel.datasource?.uid) { isAngularDs = isAngularDatasourcePluginAndNotHidden(panel.datasource?.uid); From e5640248834e6ecc47c4611ecdfe4569d96fd861 Mon Sep 17 00:00:00 2001 From: Kristina Date: Tue, 27 Feb 2024 08:14:54 -0600 Subject: [PATCH 0234/1406] Docs: change discussion link to issue link (#83434) * change discussion link to issue link * run prettier --- docs/sources/alerting/set-up/performance-limitations/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/alerting/set-up/performance-limitations/index.md b/docs/sources/alerting/set-up/performance-limitations/index.md index fda55b9293..bf57d756ea 100644 --- a/docs/sources/alerting/set-up/performance-limitations/index.md +++ b/docs/sources/alerting/set-up/performance-limitations/index.md @@ -54,7 +54,7 @@ Grafana cannot be used to receive external alerts. You can only send alerts to t You have the option to send Grafana managed alerts to an external Alertmanager, you can find this option in the admin tab on the Alerting page. -For more information, refer to [this GitHub discussion](https://github.com/grafana/grafana/discussions/45773). +For more information, refer to [this GitHub issue](https://github.com/grafana/grafana/issues/73447). ## High load on database caused by a high number of alert instances From 6fab62739d9ca7b48c231fd546a19c8058805a40 Mon Sep 17 00:00:00 2001 From: ismail simsek Date: Tue, 27 Feb 2024 15:40:32 +0100 Subject: [PATCH 0235/1406] InfluxDB: Fix escaping template variable when it was used in parentheses (#83400) escape properly regardless of the parentheses --- .../plugins/datasource/influxdb/datasource.test.ts | 13 +++++++++++++ .../app/plugins/datasource/influxdb/datasource.ts | 4 ++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/datasource/influxdb/datasource.test.ts b/public/app/plugins/datasource/influxdb/datasource.test.ts index a5eb3beaba..5e20d38370 100644 --- a/public/app/plugins/datasource/influxdb/datasource.test.ts +++ b/public/app/plugins/datasource/influxdb/datasource.test.ts @@ -443,6 +443,19 @@ describe('InfluxDataSource Frontend Mode', () => { expect(result).toBe(expectation); }); + it('should return the escaped value if the value wrapped in regex 3', () => { + const value = ['env', 'env2', 'env3']; + const variableMock = queryBuilder() + .withId('tempVar') + .withName('tempVar') + .withMulti(false) + .withIncludeAll(true) + .build(); + const result = ds.interpolateQueryExpr(value, variableMock, 'select from /^($tempVar)$/'); + const expectation = `env|env2|env3`; + expect(result).toBe(expectation); + }); + it('should **not** return the escaped value if the value **is not** wrapped in regex', () => { const value = '/special/path'; const variableMock = queryBuilder().withId('tempVar').withName('tempVar').withMulti(false).build(); diff --git a/public/app/plugins/datasource/influxdb/datasource.ts b/public/app/plugins/datasource/influxdb/datasource.ts index daa4e18ae8..b3837a0987 100644 --- a/public/app/plugins/datasource/influxdb/datasource.ts +++ b/public/app/plugins/datasource/influxdb/datasource.ts @@ -316,8 +316,8 @@ export default class InfluxDatasource extends DataSourceWithBackend Date: Tue, 27 Feb 2024 16:22:24 +0100 Subject: [PATCH 0236/1406] Alerting docs: update Alerting Provisioning (#83376) * Minor updates to Provisioning Index page * Add instructions to export other alerting resources * Edit example provisioning a `template` via config file * Add `Resource` column to the `Export API endpoints` table * Sort the `export` endpoint on the table in `Alerting Provisioning HTTP API` * Minor updates for clarity to `Use configuration files to provision` docs * Add `More examples` in Terraform Provisioning docs * File provisioning: rename `Useful Links` section to `More examples` * Minor grammar change * Update docs/sources/alerting/set-up/provision-alerting-resources/_index.md Co-authored-by: brendamuir <100768211+brendamuir@users.noreply.github.com> * Update docs/sources/alerting/set-up/provision-alerting-resources/_index.md Co-authored-by: brendamuir <100768211+brendamuir@users.noreply.github.com> * Update docs/sources/alerting/set-up/provision-alerting-resources/export-alerting-resources/index.md Co-authored-by: brendamuir <100768211+brendamuir@users.noreply.github.com> * Update docs/sources/alerting/set-up/provision-alerting-resources/export-alerting-resources/index.md Co-authored-by: brendamuir <100768211+brendamuir@users.noreply.github.com> * Update docs/sources/alerting/set-up/provision-alerting-resources/export-alerting-resources/index.md Co-authored-by: brendamuir <100768211+brendamuir@users.noreply.github.com> * Update docs/sources/alerting/set-up/provision-alerting-resources/file-provisioning/index.md Co-authored-by: brendamuir <100768211+brendamuir@users.noreply.github.com> * Update docs/sources/alerting/set-up/provision-alerting-resources/file-provisioning/index.md Co-authored-by: brendamuir <100768211+brendamuir@users.noreply.github.com> * Update docs/sources/alerting/set-up/provision-alerting-resources/file-provisioning/index.md Co-authored-by: brendamuir <100768211+brendamuir@users.noreply.github.com> * Update docs/sources/alerting/set-up/provision-alerting-resources/file-provisioning/index.md Co-authored-by: brendamuir <100768211+brendamuir@users.noreply.github.com> * Update docs/sources/alerting/set-up/provision-alerting-resources/file-provisioning/index.md Co-authored-by: brendamuir <100768211+brendamuir@users.noreply.github.com> * Update docs/sources/alerting/set-up/provision-alerting-resources/file-provisioning/index.md Co-authored-by: brendamuir <100768211+brendamuir@users.noreply.github.com> * Update docs/sources/alerting/set-up/provision-alerting-resources/file-provisioning/index.md Co-authored-by: brendamuir <100768211+brendamuir@users.noreply.github.com> * Update docs/sources/alerting/set-up/provision-alerting-resources/file-provisioning/index.md Co-authored-by: brendamuir <100768211+brendamuir@users.noreply.github.com> * Update docs/sources/alerting/set-up/provision-alerting-resources/file-provisioning/index.md Co-authored-by: brendamuir <100768211+brendamuir@users.noreply.github.com> * Update docs/sources/alerting/set-up/provision-alerting-resources/file-provisioning/index.md Co-authored-by: brendamuir <100768211+brendamuir@users.noreply.github.com> * Update docs/sources/alerting/set-up/provision-alerting-resources/file-provisioning/index.md Co-authored-by: brendamuir <100768211+brendamuir@users.noreply.github.com> * Update docs/sources/alerting/set-up/provision-alerting-resources/file-provisioning/index.md Co-authored-by: brendamuir <100768211+brendamuir@users.noreply.github.com> * Update docs/sources/alerting/set-up/provision-alerting-resources/file-provisioning/index.md Co-authored-by: brendamuir <100768211+brendamuir@users.noreply.github.com> * Update docs/sources/alerting/set-up/provision-alerting-resources/file-provisioning/index.md Co-authored-by: brendamuir <100768211+brendamuir@users.noreply.github.com> * Update docs/sources/alerting/set-up/provision-alerting-resources/file-provisioning/index.md Co-authored-by: brendamuir <100768211+brendamuir@users.noreply.github.com> * Update docs/sources/alerting/set-up/provision-alerting-resources/file-provisioning/index.md Co-authored-by: brendamuir <100768211+brendamuir@users.noreply.github.com> * Update docs/sources/alerting/set-up/provision-alerting-resources/file-provisioning/index.md Co-authored-by: brendamuir <100768211+brendamuir@users.noreply.github.com> * Address requested changes to `Export` docs * export: Minor grammar change * Update docs/sources/alerting/set-up/provision-alerting-resources/export-alerting-resources/index.md Co-authored-by: brendamuir <100768211+brendamuir@users.noreply.github.com> * Fix `doc-validator` issue with relative link * Use patch fixed version of doc-validator that better supports docs/reference destinations Signed-off-by: Jack Baldry --------- Signed-off-by: Jack Baldry Co-authored-by: brendamuir <100768211+brendamuir@users.noreply.github.com> Co-authored-by: Jack Baldry --- .github/workflows/doc-validator.yml | 2 +- .../provision-alerting-resources/_index.md | 13 +- .../export-alerting-resources/index.md | 109 ++++++++++++--- .../file-provisioning/index.md | 132 ++++++++++-------- .../terraform-provisioning/index.md | 8 +- .../shared/alerts/alerting_provisioning.md | 10 +- 6 files changed, 174 insertions(+), 100 deletions(-) diff --git a/.github/workflows/doc-validator.yml b/.github/workflows/doc-validator.yml index e8241e41c6..620a8f84e7 100644 --- a/.github/workflows/doc-validator.yml +++ b/.github/workflows/doc-validator.yml @@ -7,7 +7,7 @@ jobs: doc-validator: runs-on: "ubuntu-latest" container: - image: "grafana/doc-validator:v4.1.0" + image: "grafana/doc-validator:v4.1.1" steps: - name: "Checkout code" uses: "actions/checkout@v4" diff --git a/docs/sources/alerting/set-up/provision-alerting-resources/_index.md b/docs/sources/alerting/set-up/provision-alerting-resources/_index.md index ff3b8270fd..6fa5e36332 100644 --- a/docs/sources/alerting/set-up/provision-alerting-resources/_index.md +++ b/docs/sources/alerting/set-up/provision-alerting-resources/_index.md @@ -33,7 +33,7 @@ Choose from the options below to import (or provision) your Grafana Alerting res 1. [Use configuration files to provision your alerting resources][alerting_file_provisioning], such as alert rules and contact points, through files on disk. {{< admonition type="note" >}} - File provisioning is not available in Grafana Cloud instances. + Provisioning with configuration files is not available in Grafana Cloud. {{< /admonition >}} 1. Use [Terraform to provision alerting resources][alerting_tf_provisioning]. @@ -42,14 +42,15 @@ Choose from the options below to import (or provision) your Grafana Alerting res {{< admonition type="note" >}} The JSON output from the majority of Alerting HTTP endpoints isn't compatible for provisioning via configuration files. - Instead, use the [Export Alerting endpoints](/docs/grafana//alerting/set-up/provision-alerting-resources/export-alerting-resources#export-api-endpoints) to return or download the alerting resources in provisioning format. + + If you need the alerting resources for file provisioning, use [Export Alerting endpoints](/docs/grafana//alerting/set-up/provision-alerting-resources/export-alerting-resources#export-api-endpoints) to return or download them in provisioning format. {{< /admonition >}} ## Export alerting resources -You can export both manually created and provisioned alerting resources. For more information, refer to [Export alerting resources][alerting_export]. +You can export both manually created and provisioned alerting resources. You can also edit and export an alert rule without applying the changes. -To modify imported alert rules, you can use the **Modify export** feature to edit and then export. +For detailed instructions on the various export options, refer to [Export alerting resources][alerting_export]. ## View provisioned alerting resources @@ -61,10 +62,6 @@ To view your provisioned resources in Grafana, complete the following steps. Provisioned resources are labeled **Provisioned**, so that it is clear that they were not created manually. -**Useful Links:** - -[Grafana provisioning][provisioning] - {{% docs/reference %}} [alerting_tf_provisioning]: "/docs/grafana/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/terraform-provisioning" [alerting_tf_provisioning]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/set-up/provision-alerting-resources/terraform-provisioning" diff --git a/docs/sources/alerting/set-up/provision-alerting-resources/export-alerting-resources/index.md b/docs/sources/alerting/set-up/provision-alerting-resources/export-alerting-resources/index.md index e57a82cb31..ce54ec866b 100644 --- a/docs/sources/alerting/set-up/provision-alerting-resources/export-alerting-resources/index.md +++ b/docs/sources/alerting/set-up/provision-alerting-resources/export-alerting-resources/index.md @@ -20,11 +20,13 @@ weight: 300 # Export alerting resources -Export your alerting resources, such as alert rules, contact points, and notification policies for provisioning, automatically importing single folders and single groups. +Export your alerting resources, such as alert rules, contact points, and notification policies for provisioning, automatically importing single folders and single groups. Use the [Grafana UI](#export-from-the-grafana-ui) or the [HTTP Alerting API](#http-alerting-api) to export these resources. + +## Export from the Grafana UI The export options listed below enable you to download resources in YAML, JSON, or Terraform format, facilitating their provisioning through [configuration files][alerting_file_provisioning] or [Terraform][alerting_tf_provisioning]. -## Export alert rules +### Export alert rules To export alert rules from the Grafana UI, complete the following steps. @@ -36,20 +38,16 @@ To export alert rules from the Grafana UI, complete the following steps. 1. Find the group you want to export and click the **Export rule group** icon. 1. Choose the format to export in. - The exported rule data appears in different formats - YAML, JSON, Terraform. + The exported alert rule data appears in different formats - YAML, JSON, Terraform. 1. Click **Copy Code** or **Download**. - a. Choose **Copy Code** to go to an existing file and paste in the code. - - b. Choose **Download** to download a file with the exported data. - -## Modify and export alert rules without saving changes - -Use the **Modify export** mode to edit and export an alert rule without updating it. +### Modify alert rule and export rule group without saving changes {{% admonition type="note" %}} This feature is for Grafana-managed alert rules only. It is available to Admin, Viewer, and Editor roles. {{% /admonition %}} +Use the **Modify export** mode to edit and export an alert rule without updating it. The exported data includes all alert rules within the same alert group. + To export a modified alert rule without saving the modifications, complete the following steps from the Grafana UI. 1. Click **Alerts & IRM** -> **Alert rules**. @@ -58,27 +56,87 @@ To export a modified alert rule without saving the modifications, complete the f 1. Click **Export**. 1. Choose the format to export in. - The exported rule data appears in different formats - YAML, JSON, Terraform. + The exported alert rule group appears in different formats - YAML, JSON, Terraform. 1. Click **Copy Code** or **Download**. - a. Choose **Copy Code** to go to an existing file and paste in the code. +### Export contact points - b. Choose **Download** to download a file with the exported data. +To export contact points from the Grafana UI, complete the following steps. + +1. Click **Alerts & IRM** -> **Contact points**. +1. Find the contact point you want to export and click **More** -> **Export**. +1. Choose the format to export in. + + The exported contact point appears in different formats - YAML, JSON, Terraform. + +1. Click **Copy Code** or **Download**. -## Export API endpoints +### Export templates -You can also use the **Alerting provisioning HTTP API** to export alerting resources in YAML or JSON formats for provisioning. +Grafana currently doesn't offer an Export UI for notification templates, unlike other Alerting resources presented in this documentation. -Note that most Alerting endpoints return a JSON format that is not compatible for provisioning via configuration files, except the ones listed below. +However, you can export it by manually copying the content template and title directly from the Grafana UI. + +1. Click **Alerts & IRM** -> **Contact points** -> **Notification templates** tab. +1. Find the template you want to export. +1. Copy the content and title. +1. Adjust it for the [file provisioning format][alerting_file_provisioning_template] or [Terraform resource][alerting_tf_provisioning_template]. + +### Export the notification policy tree + +All notification policies are provisioned through a single resource: the root of the notification policy tree. + +{{% admonition type="warning" %}} + +Since the policy tree is a single resource, provisioning it will overwrite a policy tree created through any other means. + +{{< /admonition >}} + +To export the notification policy tree from the Grafana UI, complete the following steps. + +1. Click **Alerts & IRM** -> **Notification policies**. +1. In the **Default notification policy** section, click **...** -> **Export**. +1. Choose the format to export in. -| Method | URI | Summary | -| ------ | ---------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | -| GET | /api/v1/provisioning/alert-rules/:uid/export | [Export an alert rule in provisioning file format.][export_rule] | -| GET | /api/v1/provisioning/folder/:folderUid/rule-groups/:group/export | [Export an alert rule group in provisioning file format.][export_rule_group] | -| GET | /api/v1/provisioning/alert-rules/export | [Export all alert rules in provisioning file format.][export_rules] | -| GET | /api/v1/provisioning/contact-points/export | [Export all contact points in provisioning file format.][export_contacts] | -| GET | /api/v1/provisioning/policies/export | [Export the notification policy tree in provisioning file format.][export_notifications] | + The exported contact point appears in different formats - YAML, JSON, Terraform. + +1. Click **Copy Code** or **Download**. + +### Export mute timings + +To export mute timings from the Grafana UI, complete the following steps. + +1. Click **Alerts & IRM** -> **Notification policies**, and then the **Mute timings** tab. +1. Find the mute timing you want to export and click **Export**. +1. Choose the format to export in. + + The exported contact point appears in different formats - YAML, JSON, Terraform. + +1. Click **Copy Code** or **Download**. + +## HTTP Alerting API + +You can use the [Alerting HTTP API][alerting_http_provisioning] to return existing alerting resources in JSON and import them to another Grafana instance using the same endpoint. For instance: + +| Resource | Method / URI | Summary | +| ----------- | ------------------------------------- | ------------------------ | +| Alert rules | GET /api/v1/provisioning/alert-rules | Get all alert rules. | +| Alert rules | POST /api/v1/provisioning/alert-rules | Create a new alert rule. | + +However, note these Alerting endpoints return a JSON format that is not compatible for provisioning through configuration files or Terraform, except the endpoints listed below. + +### Export API endpoints + +The **Alerting HTTP API** provides specific endpoints for exporting alerting resources in YAML or JSON formats, facilitating [provisioning via configuration files][alerting_file_provisioning]. Currently, Terraform format is not supported. + +| Resource | Method / URI | Summary | +| ------------------------ | -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | +| Alert rules | GET /api/v1/provisioning/alert-rules/export | [Export all alert rules in provisioning file format.][export_rules] | +| Alert rules | GET /api/v1/provisioning/folder/:folderUid/rule-groups/:group/export | [Export an alert rule group in provisioning file format.][export_rule_group] | +| Alert rules | GET /api/v1/provisioning/alert-rules/:uid/export | [Export an alert rule in provisioning file format.][export_rule] | +| Contact points | GET /api/v1/provisioning/contact-points/export | [Export all contact points in provisioning file format.][export_contacts] | +| Notification policy tree | GET /api/v1/provisioning/policies/export | [Export the notification policy tree in provisioning file format.][export_notifications] | These endpoints accept a `download` parameter to download a file containing the exported resources. @@ -88,11 +146,16 @@ These endpoints accept a `download` parameter to download a file containing the [alerting_tf_provisioning]: "/docs/grafana/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/terraform-provisioning" [alerting_tf_provisioning]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/set-up/provision-alerting-resources/terraform-provisioning" +[alerting_tf_provisioning_template]: "/docs/grafana/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/terraform-provisioning#import-contact-points-and-templates" +[alerting_tf_provisioning_template]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/set-up/provision-alerting-resources/terraform-provisioning#import-contact-points-and-templates" + [alerting_http_provisioning]: "/docs/grafana/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/http-api-provisioning" [alerting_http_provisioning]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/set-up/provision-alerting-resources/http-api-provisioning" [alerting_file_provisioning]: "/docs/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/file-provisioning" +[alerting_file_provisioning_template]: "/docs/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/file-provisioning/#import-templates" + [export_rule]: "/docs/grafana/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/http-api-provisioning/#span-idroute-get-alert-rule-exportspan-export-an-alert-rule-in-provisioning-file-format-_routegetalertruleexport_" [export_rule]: "/docs/grafana-cloud/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/http-api-provisioning/#span-idroute-get-alert-rule-exportspan-export-an-alert-rule-in-provisioning-file-format-_routegetalertruleexport_" diff --git a/docs/sources/alerting/set-up/provision-alerting-resources/file-provisioning/index.md b/docs/sources/alerting/set-up/provision-alerting-resources/file-provisioning/index.md index c07d922047..2e6cebd0b8 100644 --- a/docs/sources/alerting/set-up/provision-alerting-resources/file-provisioning/index.md +++ b/docs/sources/alerting/set-up/provision-alerting-resources/file-provisioning/index.md @@ -30,24 +30,26 @@ For a complete guide about how Grafana provisions resources, refer to the [Provi {{< admonition type="note" >}} +- Provisioning with configuration files is not available in Grafana Cloud. + - You cannot edit provisioned resources from files in Grafana. You can only change the resource properties by changing the provisioning file and restarting Grafana or carrying out a hot reload. This prevents changes being made to the resource that would be overwritten if a file is provisioned again or a hot reload is carried out. -- Importing takes place during the initial set up of your Grafana system, but you can re-run it at any time using the [Grafana Admin API](/docs/grafana//developers/http_api/admin#reload-provisioning-configurations). +- Provisioning using configuration files takes place during the initial set up of your Grafana system, but you can re-run it at any time using the [Grafana Admin API](/docs/grafana//developers/http_api/admin#reload-provisioning-configurations). - Importing an existing alerting resource results in a conflict. First, when present, remove the resources you plan to import. {{< /admonition >}} ## Import alert rules -Create or delete alert rules in your Grafana instance(s). +Create or delete alert rules using provisioning files in your Grafana instance(s). -1. Create alert rules in Grafana. +1. Find the alert rule group in Grafana. 1. [Export][alerting_export] and download a provisioning file for your alert rules. -1. Copy the contents into a YAML or JSON configuration file in the `provisioning/alerting` directory. +1. Copy the contents into a YAML or JSON configuration file and add it to the `provisioning/alerting` directory of the Grafana instance you want to import the alerting resources to. Example configuration files can be found below. -1. Add the file(s) to your GitOps workflow, so that they deploy alongside your Grafana instance(s). +1. Restart your Grafana instance (or reload the provisioned files using the Admin API). Here is an example of a configuration file for creating alert rules. @@ -138,15 +140,15 @@ deleteRules: ## Import contact points -Create or delete contact points in your Grafana instance(s). +Create or delete contact points using provisioning files in your Grafana instance(s). -1. Create a contact point in Grafana. +1. Find the contact point in Grafana. 1. [Export][alerting_export] and download a provisioning file for your contact point. -1. Copy the contents into a YAML or JSON configuration file in the `provisioning/alerting` directory. +1. Copy the contents into a YAML or JSON configuration file and add it to the `provisioning/alerting` directory of the Grafana instance you want to import the alerting resources to. Example configuration files can be found below. -1. Add the file(s) to your GitOps workflow, so that they deploy alongside your Grafana instance(s). +1. Restart your Grafana instance (or reload the provisioned files using the Admin API). Here is an example of a configuration file for creating contact points. @@ -569,9 +571,54 @@ settings: {{< /collapse >}} +## Import templates + +Create or delete templates using provisioning files in your Grafana instance(s). + +1. Find the notification template in Grafana. +1. [Export][alerting_export] a template by copying the template content and title. +1. Copy the contents into a YAML or JSON configuration file and add it to the `provisioning/alerting` directory of the Grafana instance you want to import the alerting resources to. + + Example configuration files can be found below. + +1. Restart your Grafana instance (or reload the provisioned files using the Admin API). + +Here is an example of a configuration file for creating templates. + +```yaml +# config file version +apiVersion: 1 + +# List of templates to import or update +templates: + # organization ID, default = 1 + - orgId: 1 + # name of the template, must be unique + name: my_first_template + # content of the the template + template: | + {{ define "my_first_template" }} + Custom notification message + {{ end }} +``` + +Here is an example of a configuration file for deleting templates. + +```yaml +# config file version +apiVersion: 1 + +# List of alert rule UIDs that should be deleted +deleteTemplates: + # organization ID, default = 1 + - orgId: 1 + # name of the template, must be unique + name: my_first_template +``` + ## Import notification policies -Create or reset the notification policy tree in your Grafana instance(s). +Create or reset the notification policy tree using provisioning files in your Grafana instance(s). In Grafana, the entire notification policy tree is considered a single, large resource. Add new specific policies as sub-policies under the root policy. Since specific policies may depend on each other, you cannot provision subsets of the policy tree; the entire tree must be defined in a single place. @@ -581,13 +628,13 @@ Since the policy tree is a single resource, provisioning it will overwrite a pol {{< /admonition >}} -1. Create a notification policy in Grafana. -1. [Export][alerting_export] and download a provisioning file for your notification policy. -1. Copy the contents into a YAML or JSON configuration file in the `provisioning/alerting` directory. +1. Find the notification policy tree in Grafana. +1. [Export][alerting_export] and download a provisioning file for your notification policy tree. +1. Copy the contents into a YAML or JSON configuration file and add it to the `provisioning/alerting` directory of the Grafana instance you want to import the alerting resources to. Example configuration files can be found below. -1. Add the file(s) to your GitOps workflow, so that they deploy alongside your Grafana instance(s). +1. Restart your Grafana instance (or reload the provisioned files using the Admin API). Here is an example of a configuration file for creating notification policies. @@ -663,55 +710,17 @@ resetPolicies: - 1 ``` -## Import templates - -Create or delete templates in your Grafana instance(s). - -1. Create a YAML or JSON configuration file. - - Example configuration files can be found below. - -1. Add the file(s) to your GitOps workflow, so that they deploy alongside your Grafana instance(s). - -Here is an example of a configuration file for creating templates. - -```yaml -# config file version -apiVersion: 1 - -# List of templates to import or update -templates: - # organization ID, default = 1 - - orgId: 1 - # name of the template, must be unique - name: my_first_template - # content of the the template - template: Alerting with a custom text template -``` - -Here is an example of a configuration file for deleting templates. - -```yaml -# config file version -apiVersion: 1 - -# List of alert rule UIDs that should be deleted -deleteTemplates: - # organization ID, default = 1 - - orgId: 1 - # name of the template, must be unique - name: my_first_template -``` - ## Import mute timings -Create or delete mute timings in your Grafana instance(s). +Create or delete mute timings via provisioning files using provisioning files in your Grafana instance(s). -1. Create a YAML or JSON configuration file. +1. Find the mute timing in Grafana. +1. [Export][alerting_export] and download a provisioning file for your mute timing. +1. Copy the contents into a YAML or JSON configuration file and add it to the `provisioning/alerting` directory of the Grafana instance you want to import the alerting resources to. Example configuration files can be found below. -1. Add the file(s) to your GitOps workflow, so that they deploy alongside your Grafana instance(s). +1. Restart your Grafana instance (or reload the provisioned files using the Admin API). Here is an example of a configuration file for creating mute timings. @@ -770,7 +779,7 @@ If you are a Kubernetes user, you can leverage file provisioning using Kubernete template: the content for my template ``` -1. Add the file(s) to your GitOps workflow, so that they deploy alongside your Grafana instance(s). +1. Restart your Grafana instance (or reload the provisioned files using the Admin API). ```yaml apiVersion: apps/v1 @@ -807,14 +816,17 @@ If you are a Kubernetes user, you can leverage file provisioning using Kubernete This eliminates the need for a persistent database to use Grafana Alerting in Kubernetes; all your provisioned resources appear after each restart or re-deployment. Grafana still requires a database for normal operation, you do not need to persist the contents of the database between restarts if all objects are provisioned using files. -**Useful Links:** +## More examples -[Grafana provisioning][provisioning] +- [Provision Grafana][provisioning] {{% docs/reference %}} + [alerting_export]: "/docs/grafana/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/export-alerting-resources" [alerting_export]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/set-up/provision-alerting-resources/export-alerting-resources" [provisioning]: "/docs/ -> /docs/grafana//administration/provisioning" +[reload-provisioning-configurations]: "/docs/ -> /docs/grafana//developers/http_api/admin#reload-provisioning-configurations" + {{% /docs/reference %}} diff --git a/docs/sources/alerting/set-up/provision-alerting-resources/terraform-provisioning/index.md b/docs/sources/alerting/set-up/provision-alerting-resources/terraform-provisioning/index.md index 936887d3e7..58a86af32e 100644 --- a/docs/sources/alerting/set-up/provision-alerting-resources/terraform-provisioning/index.md +++ b/docs/sources/alerting/set-up/provision-alerting-resources/terraform-provisioning/index.md @@ -23,7 +23,7 @@ weight: 200 Use Terraform’s Grafana Provider to manage your alerting resources and provision them into your Grafana system. Terraform provider support for Grafana Alerting makes it easy to create, manage, and maintain your entire Grafana Alerting stack as code. -Refer to [Grafana Provider](https://registry.terraform.io/providers/grafana/grafana/latest/docs) documentation for more examples and information on Terraform Alerting schemas. +Refer to [Grafana Terraform Provider](https://registry.terraform.io/providers/grafana/grafana/latest/docs) documentation for more examples and information on Terraform Alerting schemas. Complete the following tasks to create and manage your alerting resources using Terraform. @@ -369,11 +369,13 @@ resource "grafana_mute_timing" "mute_all" { } ``` -**Useful Links:** +## More examples -[Grafana Terraform Provider documentation](https://registry.terraform.io/providers/grafana/grafana/latest/docs) +- [Grafana Terraform Provider documentation](https://registry.terraform.io/providers/grafana/grafana/latest/docs) +- [Creating and managing a Grafana Cloud stack using Terraform](https://grafana.com/docs/grafana-cloud/developer-resources/infrastructure-as-code/terraform/terraform-cloud-stack) {{% docs/reference %}} + [alerting-rules]: "/docs/grafana/ -> /docs/grafana//alerting/alerting-rules" [alerting-rules]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/alerting-rules" diff --git a/docs/sources/shared/alerts/alerting_provisioning.md b/docs/sources/shared/alerts/alerting_provisioning.md index 7be0b2d010..c616cde9cc 100644 --- a/docs/sources/shared/alerts/alerting_provisioning.md +++ b/docs/sources/shared/alerts/alerting_provisioning.md @@ -36,14 +36,14 @@ For managing resources related to [data source-managed alerts]({{< relref "/docs | ------ | ---------------------------------------------------------------- | ----------------------------------------------------------------------- | --------------------------------------------------------------------- | | DELETE | /api/v1/provisioning/alert-rules/:uid | [route delete alert rule](#route-delete-alert-rule) | Delete a specific alert rule by UID. | | GET | /api/v1/provisioning/alert-rules/:uid | [route get alert rule](#route-get-alert-rule) | Get a specific alert rule by UID. | +| POST | /api/v1/provisioning/alert-rules | [route post alert rule](#route-post-alert-rule) | Create a new alert rule. | +| PUT | /api/v1/provisioning/alert-rules/:uid | [route put alert rule](#route-put-alert-rule) | Update an existing alert rule. | | GET | /api/v1/provisioning/alert-rules/:uid/export | [route get alert rule export](#route-get-alert-rule-export) | Export an alert rule in provisioning file format. | | GET | /api/v1/provisioning/folder/:folderUid/rule-groups/:group | [route get alert rule group](#route-get-alert-rule-group) | Get a rule group. | +| PUT | /api/v1/provisioning/folder/:folderUid/rule-groups/:group | [route put alert rule group](#route-put-alert-rule-group) | Update the interval of a rule group or modify the rules of the group. | | GET | /api/v1/provisioning/folder/:folderUid/rule-groups/:group/export | [route get alert rule group export](#route-get-alert-rule-group-export) | Export an alert rule group in provisioning file format. | | GET | /api/v1/provisioning/alert-rules | [route get alert rules](#route-get-alert-rules) | Get all the alert rules. | | GET | /api/v1/provisioning/alert-rules/export | [route get alert rules export](#route-get-alert-rules-export) | Export all alert rules in provisioning file format. | -| POST | /api/v1/provisioning/alert-rules | [route post alert rule](#route-post-alert-rule) | Create a new alert rule. | -| PUT | /api/v1/provisioning/alert-rules/:uid | [route put alert rule](#route-put-alert-rule) | Update an existing alert rule. | -| PUT | /api/v1/provisioning/folder/:folderUid/rule-groups/:group | [route put alert rule group](#route-put-alert-rule-group) | Update the interval of a rule group or modify the rules of the group. | #### Example alert rules template @@ -130,9 +130,9 @@ For managing resources related to [data source-managed alerts]({{< relref "/docs | ------ | ------------------------------------------ | ----------------------------------------------------------------- | ------------------------------------------------------ | | DELETE | /api/v1/provisioning/contact-points/:uid | [route delete contactpoints](#route-delete-contactpoints) | Delete a contact point. | | GET | /api/v1/provisioning/contact-points | [route get contactpoints](#route-get-contactpoints) | Get all the contact points. | -| GET | /api/v1/provisioning/contact-points/export | [route get contactpoints export](#route-get-contactpoints-export) | Export all contact points in provisioning file format. | | POST | /api/v1/provisioning/contact-points | [route post contactpoints](#route-post-contactpoints) | Create a contact point. | | PUT | /api/v1/provisioning/contact-points/:uid | [route put contactpoint](#route-put-contactpoint) | Update an existing contact point. | +| GET | /api/v1/provisioning/contact-points/export | [route get contactpoints export](#route-get-contactpoints-export) | Export all contact points in provisioning file format. | ### Notification policies @@ -140,8 +140,8 @@ For managing resources related to [data source-managed alerts]({{< relref "/docs | ------ | ------------------------------------ | ------------------------------------------------------------- | ---------------------------------------------------------------- | | DELETE | /api/v1/provisioning/policies | [route reset policy tree](#route-reset-policy-tree) | Clears the notification policy tree. | | GET | /api/v1/provisioning/policies | [route get policy tree](#route-get-policy-tree) | Get the notification policy tree. | -| GET | /api/v1/provisioning/policies/export | [route get policy tree export](#route-get-policy-tree-export) | Export the notification policy tree in provisioning file format. | | PUT | /api/v1/provisioning/policies | [route put policy tree](#route-put-policy-tree) | Sets the notification policy tree. | +| GET | /api/v1/provisioning/policies/export | [route get policy tree export](#route-get-policy-tree-export) | Export the notification policy tree in provisioning file format. | ### Mute timings From facb19fefb6159c167fb3f0d844476e229ede07b Mon Sep 17 00:00:00 2001 From: Adam Yeats <16296989+adamyeats@users.noreply.github.com> Date: Tue, 27 Feb 2024 16:46:29 +0100 Subject: [PATCH 0237/1406] AzureMonitor: Fix mishandled resources vs workspaces (#83184) --- .../azure-log-analytics-datasource.go | 11 +++++- .../azure-log-analytics-datasource_test.go | 36 +++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource.go b/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource.go index 35640d45d8..69e0886333 100644 --- a/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource.go +++ b/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource.go @@ -485,11 +485,19 @@ func (e *AzureLogAnalyticsDatasource) createRequest(ctx context.Context, queryUR } if len(query.Resources) > 1 && query.QueryType == dataquery.AzureQueryTypeAzureLogAnalytics && !query.AppInsightsQuery { - body["workspaces"] = query.Resources + str := strings.ToLower(query.Resources[0]) + + if strings.Contains(str, "microsoft.operationalinsights/workspaces") { + body["workspaces"] = query.Resources + } else { + body["resources"] = query.Resources + } } + if query.AppInsightsQuery { body["applications"] = query.Resources } + jsonValue, err := json.Marshal(body) if err != nil { return nil, fmt.Errorf("%v: %w", "failed to create request", err) @@ -499,6 +507,7 @@ func (e *AzureLogAnalyticsDatasource) createRequest(ctx context.Context, queryUR if err != nil { return nil, fmt.Errorf("%v: %w", "failed to create request", err) } + req.URL.Path = "/" req.Header.Set("Content-Type", "application/json") req.URL.Path = path.Join(req.URL.Path, query.URL) diff --git a/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource_test.go b/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource_test.go index 69a7edbf4b..56e56a545b 100644 --- a/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource_test.go +++ b/pkg/tsdb/azuremonitor/loganalytics/azure-log-analytics-datasource_test.go @@ -1553,6 +1553,42 @@ func TestLogAnalyticsCreateRequest(t *testing.T) { t.Errorf("Unexpected Body: %v", cmp.Diff(string(body), expectedBody)) } }) + + t.Run("correctly classifies resources as workspaces when matching criteria", func(t *testing.T) { + ds := AzureLogAnalyticsDatasource{} + req, err := ds.createRequest(ctx, url, &AzureLogAnalyticsQuery{ + Resources: []string{"/subscriptions/test-sub/resourceGroups/test-rg/providers/microsoft.operationalInsights/workSpaces/ws1", "microsoft.operationalInsights/workspaces/ws2"}, // Note different casings and partial paths + Query: "Perf", + QueryType: dataquery.AzureQueryTypeAzureLogAnalytics, + AppInsightsQuery: false, + DashboardTime: false, + }) + require.NoError(t, err) + expectedBody := `{"query":"Perf","workspaces":["/subscriptions/test-sub/resourceGroups/test-rg/providers/microsoft.operationalInsights/workSpaces/ws1","microsoft.operationalInsights/workspaces/ws2"]}` // Expecting resources to be classified as workspaces + body, err := io.ReadAll(req.Body) + require.NoError(t, err) + if !cmp.Equal(string(body), expectedBody) { + t.Errorf("Unexpected Body: %v", cmp.Diff(string(body), expectedBody)) + } + }) + + t.Run("correctly passes multiple resources not classified as workspaces", func(t *testing.T) { + ds := AzureLogAnalyticsDatasource{} + req, err := ds.createRequest(ctx, url, &AzureLogAnalyticsQuery{ + Resources: []string{"/subscriptions/test-sub/resourceGroups/test-rg/providers/SomeOtherService/serviceInstances/r1", "/subscriptions/test-sub/resourceGroups/test-rg/providers/SomeOtherService/serviceInstances/r2"}, + Query: "Perf", + QueryType: dataquery.AzureQueryTypeAzureLogAnalytics, + AppInsightsQuery: false, + DashboardTime: false, + }) + require.NoError(t, err) + expectedBody := `{"query":"Perf","resources":["/subscriptions/test-sub/resourceGroups/test-rg/providers/SomeOtherService/serviceInstances/r1","/subscriptions/test-sub/resourceGroups/test-rg/providers/SomeOtherService/serviceInstances/r2"]}` + body, err := io.ReadAll(req.Body) + require.NoError(t, err) + if !cmp.Equal(string(body), expectedBody) { + t.Errorf("Unexpected Body: %v", cmp.Diff(string(body), expectedBody)) + } + }) } func Test_executeQueryErrorWithDifferentLogAnalyticsCreds(t *testing.T) { From 30965d47a35b20a21cc07c627d2761ac96498fac Mon Sep 17 00:00:00 2001 From: Sergej-Vlasov <37613182+Sergej-Vlasov@users.noreply.github.com> Date: Tue, 27 Feb 2024 17:48:08 +0200 Subject: [PATCH 0238/1406] DashboardScene: Update query variable definition on query change (#83218) * update scenes query variable definition on query change * add variable query definition test --- .../editors/QueryVariableEditor.test.tsx | 40 +++++++++++++++++++ .../variables/editors/QueryVariableEditor.tsx | 10 ++++- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/public/app/features/dashboard-scene/settings/variables/editors/QueryVariableEditor.test.tsx b/public/app/features/dashboard-scene/settings/variables/editors/QueryVariableEditor.test.tsx index efb730fe90..6187525e22 100644 --- a/public/app/features/dashboard-scene/settings/variables/editors/QueryVariableEditor.test.tsx +++ b/public/app/features/dashboard-scene/settings/variables/editors/QueryVariableEditor.test.tsx @@ -213,6 +213,46 @@ describe('QueryVariableEditor', () => { expect(variable.state.sort).toBe(VariableSort.alphabeticalDesc); }); + it('should update the variable query definition when changing the query', async () => { + const { + variable, + renderer: { getByTestId }, + user, + } = await setup(); + const queryEditor = getByTestId( + selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsQueryInput + ); + + await user.type(queryEditor, '-new'); + await user.tab(); + + await waitFor(async () => { + await lastValueFrom(variable.validateAndUpdate()); + }); + + expect(variable.state.definition).toEqual('my-query-new'); + + await user.clear(queryEditor); + + await user.type(queryEditor, 'new definition'); + await user.tab(); + + await waitFor(async () => { + await lastValueFrom(variable.validateAndUpdate()); + }); + + expect(variable.state.definition).toEqual('new definition'); + + await user.clear(queryEditor); + await user.tab(); + + await waitFor(async () => { + await lastValueFrom(variable.validateAndUpdate()); + }); + + expect(variable.state.definition).toEqual(''); + }); + it('should update the variable state when changing the refresh', async () => { const { variable, diff --git a/public/app/features/dashboard-scene/settings/variables/editors/QueryVariableEditor.tsx b/public/app/features/dashboard-scene/settings/variables/editors/QueryVariableEditor.tsx index 259c69bcbd..2a164aa9f0 100644 --- a/public/app/features/dashboard-scene/settings/variables/editors/QueryVariableEditor.tsx +++ b/public/app/features/dashboard-scene/settings/variables/editors/QueryVariableEditor.tsx @@ -39,7 +39,15 @@ export function QueryVariableEditor({ variable, onRunQuery }: QueryVariableEdito variable.setState({ datasource }); }; const onQueryChange = (query: VariableQueryType) => { - variable.setState({ query }); + let definition: string; + if (typeof query === 'string') { + definition = query; + } else if (query.hasOwnProperty('query') && typeof query.query === 'string') { + definition = query.query; + } else { + definition = ''; + } + variable.setState({ query, definition }); onRunQuery(); }; From 0ad8c215aa642fad96572e4a290bc50e8b00286f Mon Sep 17 00:00:00 2001 From: Jack Westbrook Date: Tue, 27 Feb 2024 17:12:53 +0100 Subject: [PATCH 0239/1406] Build: Update plugin-config webpack config (#83256) build(plugin-configs): update webpack config to match latest create-plugin config --- packages/grafana-plugin-configs/webpack.config.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/grafana-plugin-configs/webpack.config.ts b/packages/grafana-plugin-configs/webpack.config.ts index 3b53e20553..72d62202c7 100644 --- a/packages/grafana-plugin-configs/webpack.config.ts +++ b/packages/grafana-plugin-configs/webpack.config.ts @@ -32,7 +32,7 @@ const config = async (env: Record): Promise => { buildDependencies: { config: [__filename], }, - cacheDirectory: path.resolve(__dirname, '../../.yarn/.cache/webpack', path.basename(process.cwd())), + cacheDirectory: path.resolve(__dirname, '../../node_modules/.cache/webpack', path.basename(process.cwd())), }, context: process.cwd(), @@ -144,6 +144,7 @@ const config = async (env: Record): Promise => { }, path: path.resolve(process.cwd(), DIST_DIR), publicPath: `public/plugins/${pluginJson.id}/`, + uniqueName: pluginJson.id, }, plugins: [ @@ -199,7 +200,7 @@ const config = async (env: Record): Promise => { lintDirtyModulesOnly: true, // don't lint on start, only lint changed files cacheLocation: path.resolve( __dirname, - '../../.yarn/.cache/eslint-webpack-plugin', + '../../node_modules/.cache/eslint-webpack-plugin', path.basename(process.cwd()), '.eslintcache' ), From fc29182f16b05258dc7934d10d080e2d254893ec Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Tue, 27 Feb 2024 16:34:00 +0000 Subject: [PATCH 0240/1406] Chore: Remove React 17 peer deps (#83524) remove react 17 peer dep --- packages/grafana-data/package.json | 4 ++-- packages/grafana-flamegraph/package.json | 4 ++-- .../grafana-o11y-ds-frontend/package.json | 4 ++-- packages/grafana-prometheus/package.json | 4 ++-- packages/grafana-runtime/package.json | 4 ++-- packages/grafana-ui/package.json | 4 ++-- yarn.lock | 24 +++++++++---------- 7 files changed, 24 insertions(+), 24 deletions(-) diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index c06a4a0589..740b037526 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -94,7 +94,7 @@ "typescript": "5.3.3" }, "peerDependencies": { - "react": "^17.0.0 || ^18.0.0", - "react-dom": "^17.0.0 || ^18.0.0" + "react": "^18.0.0", + "react-dom": "^18.0.0" } } diff --git a/packages/grafana-flamegraph/package.json b/packages/grafana-flamegraph/package.json index 1391a71641..18bea05581 100644 --- a/packages/grafana-flamegraph/package.json +++ b/packages/grafana-flamegraph/package.json @@ -82,7 +82,7 @@ "typescript": "5.3.3" }, "peerDependencies": { - "react": "^17.0.0 || ^18.0.0", - "react-dom": "^17.0.0 || ^18.0.0" + "react": "^18.0.0", + "react-dom": "^18.0.0" } } diff --git a/packages/grafana-o11y-ds-frontend/package.json b/packages/grafana-o11y-ds-frontend/package.json index 517f8feda9..531f0eabb3 100644 --- a/packages/grafana-o11y-ds-frontend/package.json +++ b/packages/grafana-o11y-ds-frontend/package.json @@ -44,7 +44,7 @@ "typescript": "5.3.3" }, "peerDependencies": { - "react": "^17.0.0 || ^18.0.0", - "react-dom": "^17.0.0 || ^18.0.0" + "react": "^18.0.0", + "react-dom": "^18.0.0" } } diff --git a/packages/grafana-prometheus/package.json b/packages/grafana-prometheus/package.json index 9b01f15272..28237b8f22 100644 --- a/packages/grafana-prometheus/package.json +++ b/packages/grafana-prometheus/package.json @@ -144,7 +144,7 @@ "webpack-cli": "5.1.4" }, "peerDependencies": { - "react": "^17.0.0 || ^18.0.0", - "react-dom": "^17.0.0 || ^18.0.0" + "react": "^18.0.0", + "react-dom": "^18.0.0" } } diff --git a/packages/grafana-runtime/package.json b/packages/grafana-runtime/package.json index 2dc1bd6b41..fc1e92938b 100644 --- a/packages/grafana-runtime/package.json +++ b/packages/grafana-runtime/package.json @@ -77,7 +77,7 @@ "typescript": "5.3.3" }, "peerDependencies": { - "react": "^17.0.0 || ^18.0.0", - "react-dom": "^17.0.0 || ^18.0.0" + "react": "^18.0.0", + "react-dom": "^18.0.0" } } diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 60116069b1..69436c7d08 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -186,7 +186,7 @@ "webpack": "5.90.3" }, "peerDependencies": { - "react": "^17.0.0 || ^18.0.0", - "react-dom": "^17.0.0 || ^18.0.0" + "react": "^18.0.0", + "react-dom": "^18.0.0" } } diff --git a/yarn.lock b/yarn.lock index 5d85996201..4a44becb24 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3589,8 +3589,8 @@ __metadata: uplot: "npm:1.6.30" xss: "npm:^1.0.14" peerDependencies: - react: ^17.0.0 || ^18.0.0 - react-dom: ^17.0.0 || ^18.0.0 + react: ^18.0.0 + react-dom: ^18.0.0 languageName: unknown linkType: soft @@ -3808,8 +3808,8 @@ __metadata: tslib: "npm:2.6.2" typescript: "npm:5.3.3" peerDependencies: - react: ^17.0.0 || ^18.0.0 - react-dom: ^17.0.0 || ^18.0.0 + react: ^18.0.0 + react-dom: ^18.0.0 languageName: unknown linkType: soft @@ -3880,8 +3880,8 @@ __metadata: tslib: "npm:2.6.2" typescript: "npm:5.3.3" peerDependencies: - react: ^17.0.0 || ^18.0.0 - react-dom: ^17.0.0 || ^18.0.0 + react: ^18.0.0 + react-dom: ^18.0.0 languageName: unknown linkType: soft @@ -4023,8 +4023,8 @@ __metadata: webpack-cli: "npm:5.1.4" whatwg-fetch: "npm:3.6.20" peerDependencies: - react: ^17.0.0 || ^18.0.0 - react-dom: ^17.0.0 || ^18.0.0 + react: ^18.0.0 + react-dom: ^18.0.0 languageName: unknown linkType: soft @@ -4068,8 +4068,8 @@ __metadata: tslib: "npm:2.6.2" typescript: "npm:5.3.3" peerDependencies: - react: ^17.0.0 || ^18.0.0 - react-dom: ^17.0.0 || ^18.0.0 + react: ^18.0.0 + react-dom: ^18.0.0 languageName: unknown linkType: soft @@ -4307,8 +4307,8 @@ __metadata: uuid: "npm:9.0.1" webpack: "npm:5.90.3" peerDependencies: - react: ^17.0.0 || ^18.0.0 - react-dom: ^17.0.0 || ^18.0.0 + react: ^18.0.0 + react-dom: ^18.0.0 languageName: unknown linkType: soft From a7fbe3c6dc88ed8190cb8559ada72b3112225324 Mon Sep 17 00:00:00 2001 From: linoman <2051016+linoman@users.noreply.github.com> Date: Tue, 27 Feb 2024 10:34:43 -0600 Subject: [PATCH 0241/1406] Password Policy: Update frontend facing messages (#83227) * update tests * update error messages --- pkg/services/user/password.go | 10 +++++----- pkg/services/user/password_test.go | 12 ++++++------ 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/pkg/services/user/password.go b/pkg/services/user/password.go index 915d42733a..70e0e131e5 100644 --- a/pkg/services/user/password.go +++ b/pkg/services/user/password.go @@ -8,8 +8,8 @@ import ( ) var ( - ErrPasswordTooShort = errutil.NewBase(errutil.StatusBadRequest, "password-policy-too-short", errutil.WithPublicMessage("New password is too short")) - ErrPasswordPolicyInfringe = errutil.NewBase(errutil.StatusBadRequest, "password-policy-infringe", errutil.WithPublicMessage("New password doesn't comply with the password policy")) + ErrPasswordTooShort = errutil.BadRequest("password.password-policy-too-short", errutil.WithPublicMessage("New password is too short")) + ErrPasswordPolicyInfringe = errutil.BadRequest("password.password-policy-infringe", errutil.WithPublicMessage("New password doesn't comply with the password policy")) MinPasswordLength = 12 ) @@ -33,12 +33,12 @@ func (p Password) Validate(config *setting.Cfg) error { func ValidatePassword(newPassword string, config *setting.Cfg) error { if !config.BasicAuthStrongPasswordPolicy { if len(newPassword) <= 4 { - return ErrPasswordTooShort + return ErrPasswordTooShort.Errorf("new password is too short") } return nil } if len(newPassword) < MinPasswordLength { - return ErrPasswordTooShort + return ErrPasswordPolicyInfringe.Errorf("new password is too short for the strong password policy") } hasUpperCase := false @@ -67,5 +67,5 @@ func ValidatePassword(newPassword string, config *setting.Cfg) error { return nil } } - return ErrPasswordPolicyInfringe + return ErrPasswordPolicyInfringe.Errorf("new password doesn't comply with the password policy") } diff --git a/pkg/services/user/password_test.go b/pkg/services/user/password_test.go index 8f8235fae1..1953d1a152 100644 --- a/pkg/services/user/password_test.go +++ b/pkg/services/user/password_test.go @@ -21,7 +21,7 @@ func TestPasswowrdService_ValidatePasswordHardcodePolicy(t *testing.T) { { name: "should return error when the password has less than 4 characters and strong password policy is disabled", passwordTest: NUMBER, - expectedError: ErrPasswordTooShort, + expectedError: ErrPasswordTooShort.Errorf("new password is too short"), strongPasswordPolicyEnabled: false, }, {name: "should not return error when the password has 4 characters and strong password policy is disabled", @@ -32,31 +32,31 @@ func TestPasswowrdService_ValidatePasswordHardcodePolicy(t *testing.T) { { name: "should return error when the password has less than 12 characters and strong password policy is enabled", passwordTest: NUMBER, - expectedError: ErrPasswordTooShort, + expectedError: ErrPasswordPolicyInfringe.Errorf("new password is too short for the strong password policy"), strongPasswordPolicyEnabled: true, }, { name: "should return error when the password is missing an uppercase character and strong password policy is enabled", passwordTest: LOWERCASE + NUMBER + SYMBOLS, - expectedError: ErrPasswordPolicyInfringe, + expectedError: ErrPasswordPolicyInfringe.Errorf("new password doesn't comply with the password policy"), strongPasswordPolicyEnabled: true, }, { name: "should return error when the password is missing a lowercase character and strong password policy is enabled", passwordTest: UPPERCASE + NUMBER + SYMBOLS, - expectedError: ErrPasswordPolicyInfringe, + expectedError: ErrPasswordPolicyInfringe.Errorf("new password doesn't comply with the password policy"), strongPasswordPolicyEnabled: true, }, { name: "should return error when the password is missing a number character and strong password policy is enabled", passwordTest: LOWERCASE + UPPERCASE + SYMBOLS, - expectedError: ErrPasswordPolicyInfringe, + expectedError: ErrPasswordPolicyInfringe.Errorf("new password doesn't comply with the password policy"), strongPasswordPolicyEnabled: true, }, { name: "should return error when the password is missing a symbol characters and strong password policy is enabled", passwordTest: LOWERCASE + UPPERCASE + NUMBER, - expectedError: ErrPasswordPolicyInfringe, + expectedError: ErrPasswordPolicyInfringe.Errorf("new password doesn't comply with the password policy"), strongPasswordPolicyEnabled: true, }, { From 96dfb385caf6dda32b76c684408c625369a34a52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=9D=80=EB=B9=88?= Date: Wed, 28 Feb 2024 01:39:51 +0900 Subject: [PATCH 0242/1406] Grafana: Replace magic number with a constant variable in response status (#80132) * Chore: Replace response status with const var * Apply suggestions from code review Co-authored-by: Sofia Papagiannaki <1632407+papagian@users.noreply.github.com> * Add net/http import --------- Co-authored-by: Sofia Papagiannaki <1632407+papagian@users.noreply.github.com> --- pkg/api/admin.go | 4 +- pkg/api/admin_provisioning.go | 11 +-- pkg/api/admin_users.go | 24 +++--- pkg/api/alerting.go | 82 +++++++++---------- pkg/api/annotations.go | 32 ++++---- pkg/api/apierrors/folder.go | 13 +-- pkg/api/apikey.go | 4 +- pkg/api/dashboard_permission.go | 4 +- pkg/api/dashboard_snapshot.go | 10 +-- pkg/api/datasources.go | 64 +++++++-------- pkg/api/folder.go | 2 +- pkg/api/folder_permission.go | 4 +- pkg/api/index.go | 8 +- pkg/api/login.go | 4 +- pkg/api/org_invite.go | 20 ++--- pkg/api/org_users.go | 14 ++-- pkg/api/playlist.go | 14 ++-- pkg/api/plugins.go | 4 +- pkg/api/render.go | 8 +- pkg/api/response/response.go | 2 +- pkg/api/routing/routing.go | 4 +- pkg/api/search.go | 6 +- pkg/api/signup.go | 28 +++---- pkg/api/user.go | 14 ++-- pkg/api/user_token.go | 22 ++--- pkg/middleware/auth.go | 2 +- pkg/middleware/recovery.go | 4 +- pkg/services/dashboardimport/api/api.go | 2 +- pkg/services/ldap/api/service.go | 4 +- pkg/services/libraryelements/api.go | 18 ++-- pkg/services/live/live.go | 2 +- pkg/services/ngalert/api/api_configuration.go | 6 +- pkg/services/ngalert/api/api_provisioning.go | 4 +- pkg/services/searchV2/http.go | 15 ++-- pkg/services/searchusers/searchusers.go | 4 +- pkg/services/store/http.go | 52 ++++++------ 36 files changed, 260 insertions(+), 255 deletions(-) diff --git a/pkg/api/admin.go b/pkg/api/admin.go index d298445eb8..da2d61c430 100644 --- a/pkg/api/admin.go +++ b/pkg/api/admin.go @@ -62,12 +62,12 @@ func (hs *HTTPServer) AdminGetVerboseSettings(c *contextmodel.ReqContext) respon func (hs *HTTPServer) AdminGetStats(c *contextmodel.ReqContext) response.Response { adminStats, err := hs.statsService.GetAdminStats(c.Req.Context(), &stats.GetAdminStatsQuery{}) if err != nil { - return response.Error(500, "Failed to get admin stats from database", err) + return response.Error(http.StatusInternalServerError, "Failed to get admin stats from database", err) } anonymousDeviceExpiration := 30 * 24 * time.Hour devicesCount, err := hs.anonService.CountDevices(c.Req.Context(), time.Now().Add(-anonymousDeviceExpiration), time.Now().Add(time.Minute)) if err != nil { - return response.Error(500, "Failed to get anon stats from database", err) + return response.Error(http.StatusInternalServerError, "Failed to get anon stats from database", err) } adminStats.AnonymousStats.ActiveDevices = devicesCount diff --git a/pkg/api/admin_provisioning.go b/pkg/api/admin_provisioning.go index 704a2e51b9..2eb4ea80d6 100644 --- a/pkg/api/admin_provisioning.go +++ b/pkg/api/admin_provisioning.go @@ -3,6 +3,7 @@ package api import ( "context" "errors" + "net/http" "github.com/grafana/grafana/pkg/api/response" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" @@ -26,7 +27,7 @@ import ( func (hs *HTTPServer) AdminProvisioningReloadDashboards(c *contextmodel.ReqContext) response.Response { err := hs.ProvisioningService.ProvisionDashboards(c.Req.Context()) if err != nil && !errors.Is(err, context.Canceled) { - return response.Error(500, "", err) + return response.Error(http.StatusInternalServerError, "", err) } return response.Success("Dashboards config reloaded") } @@ -49,7 +50,7 @@ func (hs *HTTPServer) AdminProvisioningReloadDashboards(c *contextmodel.ReqConte func (hs *HTTPServer) AdminProvisioningReloadDatasources(c *contextmodel.ReqContext) response.Response { err := hs.ProvisioningService.ProvisionDatasources(c.Req.Context()) if err != nil { - return response.Error(500, "", err) + return response.Error(http.StatusInternalServerError, "", err) } return response.Success("Datasources config reloaded") } @@ -72,7 +73,7 @@ func (hs *HTTPServer) AdminProvisioningReloadDatasources(c *contextmodel.ReqCont func (hs *HTTPServer) AdminProvisioningReloadPlugins(c *contextmodel.ReqContext) response.Response { err := hs.ProvisioningService.ProvisionPlugins(c.Req.Context()) if err != nil { - return response.Error(500, "Failed to reload plugins config", err) + return response.Error(http.StatusInternalServerError, "Failed to reload plugins config", err) } return response.Success("Plugins config reloaded") } @@ -95,7 +96,7 @@ func (hs *HTTPServer) AdminProvisioningReloadPlugins(c *contextmodel.ReqContext) func (hs *HTTPServer) AdminProvisioningReloadNotifications(c *contextmodel.ReqContext) response.Response { err := hs.ProvisioningService.ProvisionNotifications(c.Req.Context()) if err != nil { - return response.Error(500, "", err) + return response.Error(http.StatusInternalServerError, "", err) } return response.Success("Notifications config reloaded") } @@ -103,7 +104,7 @@ func (hs *HTTPServer) AdminProvisioningReloadNotifications(c *contextmodel.ReqCo func (hs *HTTPServer) AdminProvisioningReloadAlerting(c *contextmodel.ReqContext) response.Response { err := hs.ProvisioningService.ProvisionAlerting(c.Req.Context()) if err != nil { - return response.Error(500, "", err) + return response.Error(http.StatusInternalServerError, "", err) } return response.Success("Alerting config reloaded") } diff --git a/pkg/api/admin_users.go b/pkg/api/admin_users.go index 8561777426..53b733bcf7 100644 --- a/pkg/api/admin_users.go +++ b/pkg/api/admin_users.go @@ -196,10 +196,10 @@ func (hs *HTTPServer) AdminUpdateUserPermissions(c *contextmodel.ReqContext) res err = hs.userService.UpdatePermissions(c.Req.Context(), userID, form.IsGrafanaAdmin) if err != nil { if errors.Is(err, user.ErrLastGrafanaAdmin) { - return response.Error(400, user.ErrLastGrafanaAdmin.Error(), nil) + return response.Error(http.StatusBadRequest, user.ErrLastGrafanaAdmin.Error(), nil) } - return response.Error(500, "Failed to update user permissions", err) + return response.Error(http.StatusInternalServerError, "Failed to update user permissions", err) } return response.Success("User permissions updated") @@ -230,9 +230,9 @@ func (hs *HTTPServer) AdminDeleteUser(c *contextmodel.ReqContext) response.Respo if err := hs.userService.Delete(c.Req.Context(), &cmd); err != nil { if errors.Is(err, user.ErrUserNotFound) { - return response.Error(404, user.ErrUserNotFound.Error(), nil) + return response.Error(http.StatusNotFound, user.ErrUserNotFound.Error(), nil) } - return response.Error(500, "Failed to delete user", err) + return response.Error(http.StatusInternalServerError, "Failed to delete user", err) } g, ctx := errgroup.WithContext(c.Req.Context()) @@ -285,7 +285,7 @@ func (hs *HTTPServer) AdminDeleteUser(c *contextmodel.ReqContext) response.Respo return nil }) if err := g.Wait(); err != nil { - return response.Error(500, "Failed to delete user", err) + return response.Error(http.StatusInternalServerError, "Failed to delete user", err) } return response.Success("User deleted") @@ -315,20 +315,20 @@ func (hs *HTTPServer) AdminDisableUser(c *contextmodel.ReqContext) response.Resp // External users shouldn't be disabled from API authInfoQuery := &login.GetAuthInfoQuery{UserId: userID} if _, err := hs.authInfoService.GetAuthInfo(c.Req.Context(), authInfoQuery); !errors.Is(err, user.ErrUserNotFound) { - return response.Error(500, "Could not disable external user", nil) + return response.Error(http.StatusInternalServerError, "Could not disable external user", nil) } disableCmd := user.DisableUserCommand{UserID: userID, IsDisabled: true} if err := hs.userService.Disable(c.Req.Context(), &disableCmd); err != nil { if errors.Is(err, user.ErrUserNotFound) { - return response.Error(404, user.ErrUserNotFound.Error(), nil) + return response.Error(http.StatusNotFound, user.ErrUserNotFound.Error(), nil) } - return response.Error(500, "Failed to disable user", err) + return response.Error(http.StatusInternalServerError, "Failed to disable user", err) } err = hs.AuthTokenService.RevokeAllUserTokens(c.Req.Context(), userID) if err != nil { - return response.Error(500, "Failed to disable user", err) + return response.Error(http.StatusInternalServerError, "Failed to disable user", err) } return response.Success("User disabled") @@ -358,15 +358,15 @@ func (hs *HTTPServer) AdminEnableUser(c *contextmodel.ReqContext) response.Respo // External users shouldn't be disabled from API authInfoQuery := &login.GetAuthInfoQuery{UserId: userID} if _, err := hs.authInfoService.GetAuthInfo(c.Req.Context(), authInfoQuery); !errors.Is(err, user.ErrUserNotFound) { - return response.Error(500, "Could not enable external user", nil) + return response.Error(http.StatusInternalServerError, "Could not enable external user", nil) } disableCmd := user.DisableUserCommand{UserID: userID, IsDisabled: false} if err := hs.userService.Disable(c.Req.Context(), &disableCmd); err != nil { if errors.Is(err, user.ErrUserNotFound) { - return response.Error(404, user.ErrUserNotFound.Error(), nil) + return response.Error(http.StatusNotFound, user.ErrUserNotFound.Error(), nil) } - return response.Error(500, "Failed to enable user", err) + return response.Error(http.StatusInternalServerError, "Failed to enable user", err) } return response.Success("User enabled") diff --git a/pkg/api/alerting.go b/pkg/api/alerting.go index 3deb8f8447..8f6d8c4ac6 100644 --- a/pkg/api/alerting.go +++ b/pkg/api/alerting.go @@ -58,7 +58,7 @@ func (hs *HTTPServer) GetAlertStatesForDashboard(c *contextmodel.ReqContext) res dashboardID := c.QueryInt64("dashboardId") if dashboardID == 0 { - return response.Error(400, "Missing query parameter dashboardId", nil) + return response.Error(http.StatusBadRequest, "Missing query parameter dashboardId", nil) } query := alertmodels.GetAlertStatesForDashboardQuery{ @@ -68,7 +68,7 @@ func (hs *HTTPServer) GetAlertStatesForDashboard(c *contextmodel.ReqContext) res res, err := hs.AlertEngine.AlertStore.GetAlertStatesForDashboard(c.Req.Context(), &query) if err != nil { - return response.Error(500, "Failed to fetch alert states", err) + return response.Error(http.StatusInternalServerError, "Failed to fetch alert states", err) } return response.JSON(http.StatusOK, res) @@ -120,7 +120,7 @@ func (hs *HTTPServer) GetAlerts(c *contextmodel.ReqContext) response.Response { hits, err := hs.SearchService.SearchHandler(c.Req.Context(), &searchQuery) if err != nil { - return response.Error(500, "List alerts failed", err) + return response.Error(http.StatusInternalServerError, "List alerts failed", err) } for _, d := range hits { @@ -151,7 +151,7 @@ func (hs *HTTPServer) GetAlerts(c *contextmodel.ReqContext) response.Response { res, err := hs.AlertEngine.AlertStore.HandleAlertsQuery(c.Req.Context(), &query) if err != nil { - return response.Error(500, "List alerts failed", err) + return response.Error(http.StatusInternalServerError, "List alerts failed", err) } for _, alert := range res { @@ -177,19 +177,19 @@ func (hs *HTTPServer) AlertTest(c *contextmodel.ReqContext) response.Response { return response.Error(http.StatusBadRequest, "bad request data", err) } if _, idErr := dto.Dashboard.Get("id").Int64(); idErr != nil { - return response.Error(400, "The dashboard needs to be saved at least once before you can test an alert rule", nil) + return response.Error(http.StatusBadRequest, "The dashboard needs to be saved at least once before you can test an alert rule", nil) } res, err := hs.AlertEngine.AlertTest(c.SignedInUser.GetOrgID(), dto.Dashboard, dto.PanelId, c.SignedInUser) if err != nil { var validationErr alerting.ValidationError if errors.As(err, &validationErr) { - return response.Error(422, validationErr.Error(), nil) + return response.Error(http.StatusUnprocessableEntity, validationErr.Error(), nil) } if errors.Is(err, datasources.ErrDataSourceAccessDenied) { - return response.Error(403, "Access denied to datasource", err) + return response.Error(http.StatusForbidden, "Access denied to datasource", err) } - return response.Error(500, "Failed to test rule", err) + return response.Error(http.StatusInternalServerError, "Failed to test rule", err) } dtoRes := &dtos.AlertTestResult{ @@ -234,7 +234,7 @@ func (hs *HTTPServer) GetAlert(c *contextmodel.ReqContext) response.Response { res, err := hs.AlertEngine.AlertStore.GetAlertById(c.Req.Context(), &query) if err != nil { - return response.Error(500, "List alerts failed", err) + return response.Error(http.StatusInternalServerError, "List alerts failed", err) } return response.JSON(http.StatusOK, &res) @@ -266,7 +266,7 @@ func (hs *HTTPServer) GetAlertNotifiers() func(*contextmodel.ReqContext) respons func (hs *HTTPServer) GetAlertNotificationLookup(c *contextmodel.ReqContext) response.Response { alertNotifications, err := hs.getAlertNotificationsInternal(c) if err != nil { - return response.Error(500, "Failed to get alert notifications", err) + return response.Error(http.StatusInternalServerError, "Failed to get alert notifications", err) } result := make([]*dtos.AlertNotificationLookup, 0) @@ -292,7 +292,7 @@ func (hs *HTTPServer) GetAlertNotificationLookup(c *contextmodel.ReqContext) res func (hs *HTTPServer) GetAlertNotifications(c *contextmodel.ReqContext) response.Response { alertNotifications, err := hs.getAlertNotificationsInternal(c) if err != nil { - return response.Error(500, "Failed to get alert notifications", err) + return response.Error(http.StatusInternalServerError, "Failed to get alert notifications", err) } result := make([]*dtos.AlertNotification, 0) @@ -332,16 +332,16 @@ func (hs *HTTPServer) GetAlertNotificationByID(c *contextmodel.ReqContext) respo } if query.ID == 0 { - return response.Error(404, "Alert notification not found", nil) + return response.Error(http.StatusNotFound, "Alert notification not found", nil) } res, err := hs.AlertNotificationService.GetAlertNotifications(c.Req.Context(), query) if err != nil { - return response.Error(500, "Failed to get alert notifications", err) + return response.Error(http.StatusInternalServerError, "Failed to get alert notifications", err) } if res == nil { - return response.Error(404, "Alert notification not found", nil) + return response.Error(http.StatusNotFound, "Alert notification not found", nil) } return response.JSON(http.StatusOK, dtos.NewAlertNotification(res)) @@ -366,16 +366,16 @@ func (hs *HTTPServer) GetAlertNotificationByUID(c *contextmodel.ReqContext) resp } if query.UID == "" { - return response.Error(404, "Alert notification not found", nil) + return response.Error(http.StatusNotFound, "Alert notification not found", nil) } res, err := hs.AlertNotificationService.GetAlertNotificationsWithUid(c.Req.Context(), query) if err != nil { - return response.Error(500, "Failed to get alert notifications", err) + return response.Error(http.StatusInternalServerError, "Failed to get alert notifications", err) } if res == nil { - return response.Error(404, "Alert notification not found", nil) + return response.Error(http.StatusNotFound, "Alert notification not found", nil) } return response.JSON(http.StatusOK, dtos.NewAlertNotification(res)) @@ -403,13 +403,13 @@ func (hs *HTTPServer) CreateAlertNotification(c *contextmodel.ReqContext) respon res, err := hs.AlertNotificationService.CreateAlertNotificationCommand(c.Req.Context(), &cmd) if err != nil { if errors.Is(err, alertmodels.ErrAlertNotificationWithSameNameExists) || errors.Is(err, alertmodels.ErrAlertNotificationWithSameUIDExists) { - return response.Error(409, "Failed to create alert notification", err) + return response.Error(http.StatusConflict, "Failed to create alert notification", err) } var alertingErr alerting.ValidationError if errors.As(err, &alertingErr) { - return response.Error(400, err.Error(), err) + return response.Error(http.StatusBadRequest, err.Error(), err) } - return response.Error(500, "Failed to create alert notification", err) + return response.Error(http.StatusInternalServerError, "Failed to create alert notification", err) } return response.JSON(http.StatusOK, dtos.NewAlertNotification(res)) @@ -436,18 +436,18 @@ func (hs *HTTPServer) UpdateAlertNotification(c *contextmodel.ReqContext) respon err := hs.fillWithSecureSettingsData(c.Req.Context(), &cmd) if err != nil { - return response.Error(500, "Failed to update alert notification", err) + return response.Error(http.StatusInternalServerError, "Failed to update alert notification", err) } if _, err := hs.AlertNotificationService.UpdateAlertNotification(c.Req.Context(), &cmd); err != nil { if errors.Is(err, alertmodels.ErrAlertNotificationNotFound) { - return response.Error(404, err.Error(), err) + return response.Error(http.StatusNotFound, err.Error(), err) } var alertingErr alerting.ValidationError if errors.As(err, &alertingErr) { - return response.Error(400, err.Error(), err) + return response.Error(http.StatusBadRequest, err.Error(), err) } - return response.Error(500, "Failed to update alert notification", err) + return response.Error(http.StatusInternalServerError, "Failed to update alert notification", err) } query := alertmodels.GetAlertNotificationsQuery{ @@ -457,7 +457,7 @@ func (hs *HTTPServer) UpdateAlertNotification(c *contextmodel.ReqContext) respon res, err := hs.AlertNotificationService.GetAlertNotifications(c.Req.Context(), &query) if err != nil { - return response.Error(500, "Failed to get alert notification", err) + return response.Error(http.StatusInternalServerError, "Failed to get alert notification", err) } return response.JSON(http.StatusOK, dtos.NewAlertNotification(res)) @@ -485,14 +485,14 @@ func (hs *HTTPServer) UpdateAlertNotificationByUID(c *contextmodel.ReqContext) r err := hs.fillWithSecureSettingsDataByUID(c.Req.Context(), &cmd) if err != nil { - return response.Error(500, "Failed to update alert notification", err) + return response.Error(http.StatusInternalServerError, "Failed to update alert notification", err) } if _, err := hs.AlertNotificationService.UpdateAlertNotificationWithUid(c.Req.Context(), &cmd); err != nil { if errors.Is(err, alertmodels.ErrAlertNotificationNotFound) { - return response.Error(404, err.Error(), nil) + return response.Error(http.StatusNotFound, err.Error(), nil) } - return response.Error(500, "Failed to update alert notification", err) + return response.Error(http.StatusInternalServerError, "Failed to update alert notification", err) } query := alertmodels.GetAlertNotificationsWithUidQuery{ @@ -502,7 +502,7 @@ func (hs *HTTPServer) UpdateAlertNotificationByUID(c *contextmodel.ReqContext) r res, err := hs.AlertNotificationService.GetAlertNotificationsWithUid(c.Req.Context(), &query) if err != nil { - return response.Error(500, "Failed to get alert notification", err) + return response.Error(http.StatusInternalServerError, "Failed to get alert notification", err) } return response.JSON(http.StatusOK, dtos.NewAlertNotification(res)) @@ -591,9 +591,9 @@ func (hs *HTTPServer) DeleteAlertNotification(c *contextmodel.ReqContext) respon if err := hs.AlertNotificationService.DeleteAlertNotification(c.Req.Context(), &cmd); err != nil { if errors.Is(err, alertmodels.ErrAlertNotificationNotFound) { - return response.Error(404, err.Error(), nil) + return response.Error(http.StatusNotFound, err.Error(), nil) } - return response.Error(500, "Failed to delete alert notification", err) + return response.Error(http.StatusInternalServerError, "Failed to delete alert notification", err) } return response.Success("Notification deleted") @@ -619,9 +619,9 @@ func (hs *HTTPServer) DeleteAlertNotificationByUID(c *contextmodel.ReqContext) r if err := hs.AlertNotificationService.DeleteAlertNotificationWithUid(c.Req.Context(), &cmd); err != nil { if errors.Is(err, alertmodels.ErrAlertNotificationNotFound) { - return response.Error(404, err.Error(), nil) + return response.Error(http.StatusNotFound, err.Error(), nil) } - return response.Error(500, "Failed to delete alert notification", err) + return response.Error(http.StatusInternalServerError, "Failed to delete alert notification", err) } return response.JSON(http.StatusOK, util.DynMap{ @@ -659,14 +659,14 @@ func (hs *HTTPServer) NotificationTest(c *contextmodel.ReqContext) response.Resp if err := hs.AlertNotificationService.HandleNotificationTestCommand(c.Req.Context(), cmd); err != nil { if errors.Is(err, notifications.ErrSmtpNotEnabled) { - return response.Error(412, err.Error(), err) + return response.Error(http.StatusPreconditionFailed, err.Error(), err) } var alertingErr alerting.ValidationError if errors.As(err, &alertingErr) { - return response.Error(400, err.Error(), err) + return response.Error(http.StatusBadRequest, err.Error(), err) } - return response.Error(500, "Failed to send alert notifications", err) + return response.Error(http.StatusInternalServerError, "Failed to send alert notifications", err) } return response.Success("Test notification sent") @@ -704,7 +704,7 @@ func (hs *HTTPServer) PauseAlert(legacyAlertingEnabled *bool) func(c *contextmod query := alertmodels.GetAlertByIdQuery{ID: alertID} res, err := hs.AlertEngine.AlertStore.GetAlertById(c.Req.Context(), &query) if err != nil { - return response.Error(500, "Get Alert failed", err) + return response.Error(http.StatusInternalServerError, "Get Alert failed", err) } guardian, err := guardian.New(c.Req.Context(), res.DashboardID, c.SignedInUser.GetOrgID(), c.SignedInUser) @@ -713,10 +713,10 @@ func (hs *HTTPServer) PauseAlert(legacyAlertingEnabled *bool) func(c *contextmod } if canEdit, err := guardian.CanEdit(); err != nil || !canEdit { if err != nil { - return response.Error(500, "Error while checking permissions for Alert", err) + return response.Error(http.StatusInternalServerError, "Error while checking permissions for Alert", err) } - return response.Error(403, "Access denied to this dashboard and alert", nil) + return response.Error(http.StatusForbidden, "Access denied to this dashboard and alert", nil) } // Alert state validation @@ -737,7 +737,7 @@ func (hs *HTTPServer) PauseAlert(legacyAlertingEnabled *bool) func(c *contextmod } if err := hs.AlertEngine.AlertStore.PauseAlert(c.Req.Context(), &cmd); err != nil { - return response.Error(500, "", err) + return response.Error(http.StatusInternalServerError, "", err) } resp := alertmodels.AlertStateUnknown @@ -782,7 +782,7 @@ func (hs *HTTPServer) PauseAllAlerts(legacyAlertingEnabled *bool) func(c *contex } if err := hs.AlertEngine.AlertStore.PauseAllAlerts(c.Req.Context(), &updateCmd); err != nil { - return response.Error(500, "Failed to pause alerts", err) + return response.Error(http.StatusInternalServerError, "Failed to pause alerts", err) } resp := alertmodels.AlertStatePending diff --git a/pkg/api/annotations.go b/pkg/api/annotations.go index aae4d96ddb..717d8601ca 100644 --- a/pkg/api/annotations.go +++ b/pkg/api/annotations.go @@ -62,7 +62,7 @@ func (hs *HTTPServer) GetAnnotations(c *contextmodel.ReqContext) response.Respon items, err := hs.annotationsRepo.Find(c.Req.Context(), query) if err != nil { - return response.Error(500, "Failed to get annotations", err) + return response.Error(http.StatusInternalServerError, "Failed to get annotations", err) } // since there are several annotations per dashboard, we can cache dashboard uid @@ -138,7 +138,7 @@ func (hs *HTTPServer) PostAnnotation(c *contextmodel.ReqContext) response.Respon if cmd.Text == "" { err := &AnnotationError{"text field should not be empty"} - return response.Error(400, "Failed to save annotation", err) + return response.Error(http.StatusBadRequest, "Failed to save annotation", err) } userID, err := identity.UserIdentifier(c.SignedInUser.GetNamespacedID()) @@ -160,9 +160,9 @@ func (hs *HTTPServer) PostAnnotation(c *contextmodel.ReqContext) response.Respon if err := hs.annotationsRepo.Save(c.Req.Context(), &item); err != nil { if errors.Is(err, annotations.ErrTimerangeMissing) { - return response.Error(400, "Failed to save annotation", err) + return response.Error(http.StatusBadRequest, "Failed to save annotation", err) } - return response.ErrOrFallback(500, "Failed to save annotation", err) + return response.ErrOrFallback(http.StatusInternalServerError, "Failed to save annotation", err) } startID := item.ID @@ -200,7 +200,7 @@ func (hs *HTTPServer) PostGraphiteAnnotation(c *contextmodel.ReqContext) respons } if cmd.What == "" { err := &AnnotationError{"what field should not be empty"} - return response.Error(400, "Failed to save Graphite annotation", err) + return response.Error(http.StatusBadRequest, "Failed to save Graphite annotation", err) } text := formatGraphiteAnnotation(cmd.What, cmd.Data) @@ -220,12 +220,12 @@ func (hs *HTTPServer) PostGraphiteAnnotation(c *contextmodel.ReqContext) respons tagsArray = append(tagsArray, tagStr) } else { err := &AnnotationError{"tag should be a string"} - return response.Error(400, "Failed to save Graphite annotation", err) + return response.Error(http.StatusBadRequest, "Failed to save Graphite annotation", err) } } default: err := &AnnotationError{"unsupported tags format"} - return response.Error(400, "Failed to save Graphite annotation", err) + return response.Error(http.StatusBadRequest, "Failed to save Graphite annotation", err) } userID, err := identity.UserIdentifier(c.SignedInUser.GetNamespacedID()) @@ -242,7 +242,7 @@ func (hs *HTTPServer) PostGraphiteAnnotation(c *contextmodel.ReqContext) respons } if err := hs.annotationsRepo.Save(c.Req.Context(), &item); err != nil { - return response.ErrOrFallback(500, "Failed to save Graphite annotation", err) + return response.ErrOrFallback(http.StatusInternalServerError, "Failed to save Graphite annotation", err) } return response.JSON(http.StatusOK, util.DynMap{ @@ -307,7 +307,7 @@ func (hs *HTTPServer) UpdateAnnotation(c *contextmodel.ReqContext) response.Resp } if err := hs.annotationsRepo.Update(c.Req.Context(), &item); err != nil { - return response.ErrOrFallback(500, "Failed to update annotation", err) + return response.ErrOrFallback(http.StatusInternalServerError, "Failed to update annotation", err) } return response.Success("Annotation updated") @@ -386,7 +386,7 @@ func (hs *HTTPServer) PatchAnnotation(c *contextmodel.ReqContext) response.Respo } if err := hs.annotationsRepo.Update(c.Req.Context(), &existing); err != nil { - return response.ErrOrFallback(500, "Failed to update annotation", err) + return response.ErrOrFallback(http.StatusInternalServerError, "Failed to update annotation", err) } return response.Success("Annotation patched") @@ -459,7 +459,7 @@ func (hs *HTTPServer) MassDeleteAnnotations(c *contextmodel.ReqContext) response err = hs.annotationsRepo.Delete(c.Req.Context(), deleteParams) if err != nil { - return response.Error(500, "Failed to delete annotations", err) + return response.Error(http.StatusInternalServerError, "Failed to delete annotations", err) } return response.Success("Annotations deleted") @@ -488,7 +488,7 @@ func (hs *HTTPServer) GetAnnotationByID(c *contextmodel.ReqContext) response.Res annotation.AvatarURL = dtos.GetGravatarUrl(hs.Cfg, annotation.Email) } - return response.JSON(200, annotation) + return response.JSON(http.StatusOK, annotation) } // swagger:route DELETE /annotations/{annotation_id} annotations deleteAnnotationByID @@ -524,7 +524,7 @@ func (hs *HTTPServer) DeleteAnnotationByID(c *contextmodel.ReqContext) response. ID: annotationID, }) if err != nil { - return response.Error(500, "Failed to delete annotation", err) + return response.Error(http.StatusInternalServerError, "Failed to delete annotation", err) } return response.Success("Annotation deleted") @@ -560,11 +560,11 @@ func findAnnotationByID(ctx context.Context, repo annotations.Repository, annota items, err := repo.Find(ctx, query) if err != nil { - return nil, response.Error(500, "Failed to find annotation", err) + return nil, response.Error(http.StatusInternalServerError, "Failed to find annotation", err) } if len(items) == 0 { - return nil, response.Error(404, "Annotation not found", nil) + return nil, response.Error(http.StatusNotFound, "Annotation not found", nil) } return items[0], nil @@ -589,7 +589,7 @@ func (hs *HTTPServer) GetAnnotationTags(c *contextmodel.ReqContext) response.Res result, err := hs.annotationsRepo.FindTags(c.Req.Context(), query) if err != nil { - return response.Error(500, "Failed to find annotation tags", err) + return response.Error(http.StatusInternalServerError, "Failed to find annotation tags", err) } return response.JSON(http.StatusOK, annotations.GetAnnotationTagsResponse{Result: result}) diff --git a/pkg/api/apierrors/folder.go b/pkg/api/apierrors/folder.go index 1daedcf686..edad9a40a0 100644 --- a/pkg/api/apierrors/folder.go +++ b/pkg/api/apierrors/folder.go @@ -2,6 +2,7 @@ package apierrors import ( "errors" + "net/http" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/services/dashboards" @@ -19,25 +20,25 @@ func ToFolderErrorResponse(err error) response.Response { errors.Is(err, dashboards.ErrDashboardTypeMismatch) || errors.Is(err, dashboards.ErrDashboardInvalidUid) || errors.Is(err, dashboards.ErrDashboardUidTooLong) { - return response.Error(400, err.Error(), nil) + return response.Error(http.StatusBadRequest, err.Error(), nil) } if errors.Is(err, dashboards.ErrFolderAccessDenied) { - return response.Error(403, "Access denied", err) + return response.Error(http.StatusForbidden, "Access denied", err) } if errors.Is(err, dashboards.ErrFolderNotFound) { - return response.JSON(404, util.DynMap{"status": "not-found", "message": dashboards.ErrFolderNotFound.Error()}) + return response.JSON(http.StatusNotFound, util.DynMap{"status": "not-found", "message": dashboards.ErrFolderNotFound.Error()}) } if errors.Is(err, dashboards.ErrFolderSameNameExists) || errors.Is(err, dashboards.ErrFolderWithSameUIDExists) { - return response.Error(409, err.Error(), nil) + return response.Error(http.StatusConflict, err.Error(), nil) } if errors.Is(err, dashboards.ErrFolderVersionMismatch) { - return response.JSON(412, util.DynMap{"status": "version-mismatch", "message": dashboards.ErrFolderVersionMismatch.Error()}) + return response.JSON(http.StatusPreconditionFailed, util.DynMap{"status": "version-mismatch", "message": dashboards.ErrFolderVersionMismatch.Error()}) } - return response.ErrOrFallback(500, "Folder API error", err) + return response.ErrOrFallback(http.StatusInternalServerError, "Folder API error", err) } diff --git a/pkg/api/apikey.go b/pkg/api/apikey.go index 2ca740ce5b..fafaddecb6 100644 --- a/pkg/api/apikey.go +++ b/pkg/api/apikey.go @@ -92,9 +92,9 @@ func (hs *HTTPServer) DeleteAPIKey(c *contextmodel.ReqContext) response.Response if err != nil { var status int if errors.Is(err, apikey.ErrNotFound) { - status = 404 + status = http.StatusNotFound } else { - status = 500 + status = http.StatusInternalServerError } return response.Error(status, "Failed to delete API key", err) } diff --git a/pkg/api/dashboard_permission.go b/pkg/api/dashboard_permission.go index dcd3469c9e..1754ef752b 100644 --- a/pkg/api/dashboard_permission.go +++ b/pkg/api/dashboard_permission.go @@ -61,7 +61,7 @@ func (hs *HTTPServer) GetDashboardPermissionList(c *contextmodel.ReqContext) res acl, err := hs.getDashboardACL(c.Req.Context(), c.SignedInUser, dash) if err != nil { - return response.Error(500, "Failed to get dashboard permissions", err) + return response.Error(http.StatusInternalServerError, "Failed to get dashboard permissions", err) } filteredACLs := make([]*dashboards.DashboardACLInfoDTO, 0, len(acl)) @@ -124,7 +124,7 @@ func (hs *HTTPServer) UpdateDashboardPermissions(c *contextmodel.ReqContext) res return response.Error(http.StatusBadRequest, "bad request data", err) } if err := validatePermissionsUpdate(apiCmd); err != nil { - return response.Error(400, err.Error(), err) + return response.Error(http.StatusBadRequest, err.Error(), err) } dashUID := web.Params(c.Req)[":uid"] diff --git a/pkg/api/dashboard_snapshot.go b/pkg/api/dashboard_snapshot.go index f5fc19de81..561b4158d3 100644 --- a/pkg/api/dashboard_snapshot.go +++ b/pkg/api/dashboard_snapshot.go @@ -108,7 +108,7 @@ func (hs *HTTPServer) GetDashboardSnapshot(c *contextmodel.ReqContext) response. // expired snapshots should also be removed from db if snapshot.Expires.Before(time.Now()) { - return response.Error(404, "Dashboard snapshot not found", err) + return response.Error(http.StatusNotFound, "Dashboard snapshot not found", err) } dto := dtos.DashboardFullWithMeta{ @@ -146,15 +146,15 @@ func (hs *HTTPServer) DeleteDashboardSnapshotByDeleteKey(c *contextmodel.ReqCont key := web.Params(c.Req)[":deleteKey"] if len(key) == 0 { - return response.Error(404, "Snapshot not found", nil) + return response.Error(http.StatusNotFound, "Snapshot not found", nil) } err := dashboardsnapshots.DeleteWithKey(c.Req.Context(), key, hs.dashboardsnapshotsService) if err != nil { if errors.Is(err, dashboardsnapshots.ErrBaseNotFound) { - return response.Error(404, "Snapshot not found", err) + return response.Error(http.StatusNotFound, "Snapshot not found", err) } - return response.Error(500, "Failed to delete dashboard snapshot", err) + return response.Error(http.StatusInternalServerError, "Failed to delete dashboard snapshot", err) } return response.JSON(http.StatusOK, util.DynMap{ @@ -269,7 +269,7 @@ func (hs *HTTPServer) SearchDashboardSnapshots(c *contextmodel.ReqContext) respo searchQueryResult, err := hs.dashboardsnapshotsService.SearchDashboardSnapshots(c.Req.Context(), &searchQuery) if err != nil { - return response.Error(500, "Search failed", err) + return response.Error(http.StatusInternalServerError, "Search failed", err) } dto := make([]*dashboardsnapshots.DashboardSnapshotDTO, len(searchQueryResult)) diff --git a/pkg/api/datasources.go b/pkg/api/datasources.go index f19534364f..872e93dd55 100644 --- a/pkg/api/datasources.go +++ b/pkg/api/datasources.go @@ -51,12 +51,12 @@ func (hs *HTTPServer) GetDataSources(c *contextmodel.ReqContext) response.Respon dataSources, err := hs.DataSourcesService.GetDataSources(c.Req.Context(), &query) if err != nil { - return response.Error(500, "Failed to query datasources", err) + return response.Error(http.StatusInternalServerError, "Failed to query datasources", err) } filtered, err := hs.dsGuardian.New(c.SignedInUser.OrgID, c.SignedInUser).FilterDatasourcesByQueryPermissions(dataSources) if err != nil { - return response.Error(500, "Failed to query datasources", err) + return response.Error(http.StatusInternalServerError, "Failed to query datasources", err) } result := make(dtos.DataSourceList, 0) @@ -125,12 +125,12 @@ func (hs *HTTPServer) GetDataSourceById(c *contextmodel.ReqContext) response.Res dataSource, err := hs.DataSourcesService.GetDataSource(c.Req.Context(), &query) if err != nil { if errors.Is(err, datasources.ErrDataSourceNotFound) { - return response.Error(404, "Data source not found", nil) + return response.Error(http.StatusNotFound, "Data source not found", nil) } if errors.Is(err, datasources.ErrDataSourceIdentifierNotSet) { - return response.Error(400, "Datasource id is missing", nil) + return response.Error(http.StatusBadRequest, "Datasource id is missing", nil) } - return response.Error(500, "Failed to query datasources", err) + return response.Error(http.StatusInternalServerError, "Failed to query datasources", err) } dto := hs.convertModelToDtos(c.Req.Context(), dataSource) @@ -165,19 +165,19 @@ func (hs *HTTPServer) DeleteDataSourceById(c *contextmodel.ReqContext) response. } if id <= 0 { - return response.Error(400, "Missing valid datasource id", nil) + return response.Error(http.StatusBadRequest, "Missing valid datasource id", nil) } ds, err := hs.getRawDataSourceById(c.Req.Context(), id, c.SignedInUser.GetOrgID()) if err != nil { if errors.Is(err, datasources.ErrDataSourceNotFound) { - return response.Error(404, "Data source not found", nil) + return response.Error(http.StatusNotFound, "Data source not found", nil) } - return response.Error(400, "Failed to delete datasource", nil) + return response.Error(http.StatusBadRequest, "Failed to delete datasource", nil) } if ds.ReadOnly { - return response.Error(403, "Cannot delete read-only data source", nil) + return response.Error(http.StatusForbidden, "Cannot delete read-only data source", nil) } cmd := &datasources.DeleteDataSourceCommand{ID: id, OrgID: c.SignedInUser.GetOrgID(), Name: ds.Name} @@ -185,9 +185,9 @@ func (hs *HTTPServer) DeleteDataSourceById(c *contextmodel.ReqContext) response. err = hs.DataSourcesService.DeleteDataSource(c.Req.Context(), cmd) if err != nil { if errors.As(err, &secretsPluginError) { - return response.Error(500, "Failed to delete datasource: "+err.Error(), err) + return response.Error(http.StatusInternalServerError, "Failed to delete datasource: "+err.Error(), err) } - return response.Error(500, "Failed to delete datasource", err) + return response.Error(http.StatusInternalServerError, "Failed to delete datasource", err) } hs.Live.HandleDatasourceDelete(c.SignedInUser.GetOrgID(), ds.UID) @@ -244,19 +244,19 @@ func (hs *HTTPServer) DeleteDataSourceByUID(c *contextmodel.ReqContext) response uid := web.Params(c.Req)[":uid"] if uid == "" { - return response.Error(400, "Missing datasource uid", nil) + return response.Error(http.StatusBadRequest, "Missing datasource uid", nil) } ds, err := hs.getRawDataSourceByUID(c.Req.Context(), uid, c.SignedInUser.GetOrgID()) if err != nil { if errors.Is(err, datasources.ErrDataSourceNotFound) { - return response.Error(404, "Data source not found", nil) + return response.Error(http.StatusNotFound, "Data source not found", nil) } - return response.Error(400, "Failed to delete datasource", nil) + return response.Error(http.StatusBadRequest, "Failed to delete datasource", nil) } if ds.ReadOnly { - return response.Error(403, "Cannot delete read-only data source", nil) + return response.Error(http.StatusForbidden, "Cannot delete read-only data source", nil) } cmd := &datasources.DeleteDataSourceCommand{UID: uid, OrgID: c.SignedInUser.GetOrgID(), Name: ds.Name} @@ -264,9 +264,9 @@ func (hs *HTTPServer) DeleteDataSourceByUID(c *contextmodel.ReqContext) response err = hs.DataSourcesService.DeleteDataSource(c.Req.Context(), cmd) if err != nil { if errors.As(err, &secretsPluginError) { - return response.Error(500, "Failed to delete datasource: "+err.Error(), err) + return response.Error(http.StatusInternalServerError, "Failed to delete datasource: "+err.Error(), err) } - return response.Error(500, "Failed to delete datasource", err) + return response.Error(http.StatusInternalServerError, "Failed to delete datasource", err) } hs.Live.HandleDatasourceDelete(c.SignedInUser.GetOrgID(), ds.UID) @@ -294,29 +294,29 @@ func (hs *HTTPServer) DeleteDataSourceByName(c *contextmodel.ReqContext) respons name := web.Params(c.Req)[":name"] if name == "" { - return response.Error(400, "Missing valid datasource name", nil) + return response.Error(http.StatusBadRequest, "Missing valid datasource name", nil) } getCmd := &datasources.GetDataSourceQuery{Name: name, OrgID: c.SignedInUser.GetOrgID()} dataSource, err := hs.DataSourcesService.GetDataSource(c.Req.Context(), getCmd) if err != nil { if errors.Is(err, datasources.ErrDataSourceNotFound) { - return response.Error(404, "Data source not found", nil) + return response.Error(http.StatusNotFound, "Data source not found", nil) } - return response.Error(500, "Failed to delete datasource", err) + return response.Error(http.StatusInternalServerError, "Failed to delete datasource", err) } if dataSource.ReadOnly { - return response.Error(403, "Cannot delete read-only data source", nil) + return response.Error(http.StatusForbidden, "Cannot delete read-only data source", nil) } cmd := &datasources.DeleteDataSourceCommand{Name: name, OrgID: c.SignedInUser.GetOrgID()} err = hs.DataSourcesService.DeleteDataSource(c.Req.Context(), cmd) if err != nil { if errors.As(err, &secretsPluginError) { - return response.Error(500, "Failed to delete datasource: "+err.Error(), err) + return response.Error(http.StatusInternalServerError, "Failed to delete datasource: "+err.Error(), err) } - return response.Error(500, "Failed to delete datasource", err) + return response.Error(http.StatusInternalServerError, "Failed to delete datasource", err) } hs.Live.HandleDatasourceDelete(c.SignedInUser.GetOrgID(), dataSource.UID) @@ -528,9 +528,9 @@ func (hs *HTTPServer) UpdateDataSourceByID(c *contextmodel.ReqContext) response. ds, err := hs.getRawDataSourceById(c.Req.Context(), cmd.ID, cmd.OrgID) if err != nil { if errors.Is(err, datasources.ErrDataSourceNotFound) { - return response.Error(404, "Data source not found", nil) + return response.Error(http.StatusNotFound, "Data source not found", nil) } - return response.Error(500, "Failed to update datasource", err) + return response.Error(http.StatusInternalServerError, "Failed to update datasource", err) } // check if LBAC rules have been modified @@ -613,7 +613,7 @@ func checkTeamHTTPHeaderPermissions(hs *HTTPServer, c *contextmodel.ReqContext, func (hs *HTTPServer) updateDataSourceByID(c *contextmodel.ReqContext, ds *datasources.DataSource, cmd datasources.UpdateDataSourceCommand) response.Response { if ds.ReadOnly { - return response.Error(403, "Cannot update read-only data source", nil) + return response.Error(http.StatusForbidden, "Cannot update read-only data source", nil) } _, err := hs.DataSourcesService.UpdateDataSource(c.Req.Context(), &cmd) @@ -641,9 +641,9 @@ func (hs *HTTPServer) updateDataSourceByID(c *contextmodel.ReqContext, ds *datas dataSource, err := hs.DataSourcesService.GetDataSource(c.Req.Context(), &query) if err != nil { if errors.Is(err, datasources.ErrDataSourceNotFound) { - return response.Error(404, "Data source not found", nil) + return response.Error(http.StatusNotFound, "Data source not found", nil) } - return response.Error(500, "Failed to query datasource", err) + return response.Error(http.StatusInternalServerError, "Failed to query datasource", err) } datasourceDTO := hs.convertModelToDtos(c.Req.Context(), dataSource) @@ -704,9 +704,9 @@ func (hs *HTTPServer) GetDataSourceByName(c *contextmodel.ReqContext) response.R dataSource, err := hs.DataSourcesService.GetDataSource(c.Req.Context(), &query) if err != nil { if errors.Is(err, datasources.ErrDataSourceNotFound) { - return response.Error(404, "Data source not found", nil) + return response.Error(http.StatusNotFound, "Data source not found", nil) } - return response.Error(500, "Failed to query datasources", err) + return response.Error(http.StatusInternalServerError, "Failed to query datasources", err) } dto := hs.convertModelToDtos(c.Req.Context(), dataSource) @@ -732,9 +732,9 @@ func (hs *HTTPServer) GetDataSourceIdByName(c *contextmodel.ReqContext) response ds, err := hs.DataSourcesService.GetDataSource(c.Req.Context(), &query) if err != nil { if errors.Is(err, datasources.ErrDataSourceNotFound) { - return response.Error(404, "Data source not found", nil) + return response.Error(http.StatusNotFound, "Data source not found", nil) } - return response.Error(500, "Failed to query datasources", err) + return response.Error(http.StatusInternalServerError, "Failed to query datasources", err) } dtos := dtos.AnyId{ diff --git a/pkg/api/folder.go b/pkg/api/folder.go index b4822c035c..fbb63f4d98 100644 --- a/pkg/api/folder.go +++ b/pkg/api/folder.go @@ -298,7 +298,7 @@ func (hs *HTTPServer) DeleteFolder(c *contextmodel.ReqContext) response.Response err := hs.LibraryElementService.DeleteLibraryElementsInFolder(c.Req.Context(), c.SignedInUser, web.Params(c.Req)[":uid"]) if err != nil { if errors.Is(err, model.ErrFolderHasConnectedLibraryElements) { - return response.Error(403, "Folder could not be deleted because it contains library elements in use", err) + return response.Error(http.StatusForbidden, "Folder could not be deleted because it contains library elements in use", err) } return apierrors.ToFolderErrorResponse(err) } diff --git a/pkg/api/folder_permission.go b/pkg/api/folder_permission.go index 1b322cd3b0..8901eaa307 100644 --- a/pkg/api/folder_permission.go +++ b/pkg/api/folder_permission.go @@ -38,7 +38,7 @@ func (hs *HTTPServer) GetFolderPermissionList(c *contextmodel.ReqContext) respon acl, err := hs.getFolderACL(c.Req.Context(), c.SignedInUser, folder) if err != nil { - return response.Error(500, "Failed to get folder permissions", err) + return response.Error(http.StatusInternalServerError, "Failed to get folder permissions", err) } filteredACLs := make([]*dashboards.DashboardACLInfoDTO, 0, len(acl)) @@ -83,7 +83,7 @@ func (hs *HTTPServer) UpdateFolderPermissions(c *contextmodel.ReqContext) respon return response.Error(http.StatusBadRequest, "bad request data", err) } if err := validatePermissionsUpdate(apiCmd); err != nil { - return response.Error(400, err.Error(), err) + return response.Error(http.StatusBadRequest, err.Error(), err) } uid := web.Params(c.Req)[":uid"] diff --git a/pkg/api/index.go b/pkg/api/index.go index 4c52249cbb..2edc949bd5 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -255,7 +255,7 @@ func hashUserIdentifier(identifier string, secret string) string { func (hs *HTTPServer) Index(c *contextmodel.ReqContext) { data, err := hs.setIndexViewData(c) if err != nil { - c.Handle(hs.Cfg, 500, "Failed to get settings", err) + c.Handle(hs.Cfg, http.StatusInternalServerError, "Failed to get settings", err) return } c.HTML(http.StatusOK, "index", data) @@ -263,17 +263,17 @@ func (hs *HTTPServer) Index(c *contextmodel.ReqContext) { func (hs *HTTPServer) NotFoundHandler(c *contextmodel.ReqContext) { if c.IsApiRequest() { - c.JsonApiErr(404, "Not found", nil) + c.JsonApiErr(http.StatusNotFound, "Not found", nil) return } data, err := hs.setIndexViewData(c) if err != nil { - c.Handle(hs.Cfg, 500, "Failed to get settings", err) + c.Handle(hs.Cfg, http.StatusInternalServerError, "Failed to get settings", err) return } - c.HTML(404, "index", data) + c.HTML(http.StatusNotFound, "index", data) } func (hs *HTTPServer) getThemeForIndexData(themePrefId string, themeURLParam string) *pref.ThemeDTO { diff --git a/pkg/api/login.go b/pkg/api/login.go index 90b2d433a4..5443e95d56 100644 --- a/pkg/api/login.go +++ b/pkg/api/login.go @@ -96,7 +96,7 @@ func (hs *HTTPServer) LoginView(c *contextmodel.ReqContext) { viewData, err := setIndexViewData(hs, c) if err != nil { - c.Handle(hs.Cfg, 500, "Failed to get settings", err) + c.Handle(hs.Cfg, http.StatusInternalServerError, "Failed to get settings", err) return } @@ -196,7 +196,7 @@ func (hs *HTTPServer) LoginAPIPing(c *contextmodel.ReqContext) response.Response return response.JSON(http.StatusOK, util.DynMap{"message": "Logged in"}) } - return response.Error(401, "Unauthorized", nil) + return response.Error(http.StatusUnauthorized, "Unauthorized", nil) } func (hs *HTTPServer) LoginPost(c *contextmodel.ReqContext) response.Response { diff --git a/pkg/api/org_invite.go b/pkg/api/org_invite.go index af21963d7f..a8a64781ce 100644 --- a/pkg/api/org_invite.go +++ b/pkg/api/org_invite.go @@ -216,14 +216,14 @@ func (hs *HTTPServer) GetInviteInfoByCode(c *contextmodel.ReqContext) response.R queryResult, err := hs.tempUserService.GetTempUserByCode(c.Req.Context(), &query) if err != nil { if errors.Is(err, tempuser.ErrTempUserNotFound) { - return response.Error(404, "Invite not found", nil) + return response.Error(http.StatusNotFound, "Invite not found", nil) } - return response.Error(500, "Failed to get invite", err) + return response.Error(http.StatusInternalServerError, "Failed to get invite", err) } invite := queryResult if invite.Status != tempuser.TmpUserInvitePending { - return response.Error(404, "Invite not found", nil) + return response.Error(http.StatusNotFound, "Invite not found", nil) } return response.JSON(http.StatusOK, dtos.InviteInfo{ @@ -285,17 +285,17 @@ func (hs *HTTPServer) CompleteInvite(c *contextmodel.ReqContext) response.Respon usr, err := hs.userService.Create(c.Req.Context(), &cmd) if err != nil { if errors.Is(err, user.ErrUserAlreadyExists) { - return response.Error(412, fmt.Sprintf("User with email '%s' or username '%s' already exists", completeInvite.Email, completeInvite.Username), err) + return response.Error(http.StatusPreconditionFailed, fmt.Sprintf("User with email '%s' or username '%s' already exists", completeInvite.Email, completeInvite.Username), err) } - return response.Error(500, "failed to create user", err) + return response.Error(http.StatusInternalServerError, "failed to create user", err) } if err := hs.bus.Publish(c.Req.Context(), &events.SignUpCompleted{ Name: usr.NameOrFallback(), Email: usr.Email, }); err != nil { - return response.Error(500, "failed to publish event", err) + return response.Error(http.StatusInternalServerError, "failed to publish event", err) } if ok, rsp := hs.applyUserInvite(c.Req.Context(), usr, invite, true); !ok { @@ -304,7 +304,7 @@ func (hs *HTTPServer) CompleteInvite(c *contextmodel.ReqContext) response.Respon err = hs.loginUserWithUser(usr, c) if err != nil { - return response.Error(500, "failed to accept invite", err) + return response.Error(http.StatusInternalServerError, "failed to accept invite", err) } metrics.MApiUserSignUpCompleted.Inc() @@ -320,7 +320,7 @@ func (hs *HTTPServer) updateTempUserStatus(ctx context.Context, code string, sta // update temp user status updateTmpUserCmd := tempuser.UpdateTempUserStatusCommand{Code: code, Status: status} if err := hs.tempUserService.UpdateTempUserStatus(ctx, &updateTmpUserCmd); err != nil { - return false, response.Error(500, "Failed to update invite status", err) + return false, response.Error(http.StatusInternalServerError, "Failed to update invite status", err) } return true, nil @@ -331,7 +331,7 @@ func (hs *HTTPServer) applyUserInvite(ctx context.Context, usr *user.User, invit addOrgUserCmd := org.AddOrgUserCommand{OrgID: invite.OrgID, UserID: usr.ID, Role: invite.Role} if err := hs.orgService.AddOrgUser(ctx, &addOrgUserCmd); err != nil { if !errors.Is(err, org.ErrOrgUserAlreadyAdded) { - return false, response.Error(500, "Error while trying to create org user", err) + return false, response.Error(http.StatusInternalServerError, "Error while trying to create org user", err) } } @@ -343,7 +343,7 @@ func (hs *HTTPServer) applyUserInvite(ctx context.Context, usr *user.User, invit if setActive { // set org to active if err := hs.userService.SetUsingOrg(ctx, &user.SetUsingOrgCommand{OrgID: invite.OrgID, UserID: usr.ID}); err != nil { - return false, response.Error(500, "Failed to set org as active", err) + return false, response.Error(http.StatusInternalServerError, "Failed to set org as active", err) } } diff --git a/pkg/api/org_users.go b/pkg/api/org_users.go index 2181539199..dec12f2ae2 100644 --- a/pkg/api/org_users.go +++ b/pkg/api/org_users.go @@ -124,7 +124,7 @@ func (hs *HTTPServer) GetOrgUsersForCurrentOrg(c *contextmodel.ReqContext) respo }) if err != nil { - return response.Error(500, "Failed to get users for current organization", err) + return response.Error(http.StatusInternalServerError, "Failed to get users for current organization", err) } return response.JSON(http.StatusOK, result.OrgUsers) @@ -154,7 +154,7 @@ func (hs *HTTPServer) GetOrgUsersForCurrentOrgLookup(c *contextmodel.ReqContext) }) if err != nil { - return response.Error(500, "Failed to get users for current organization", err) + return response.Error(http.StatusInternalServerError, "Failed to get users for current organization", err) } result := make([]*dtos.UserLookupDTO, 0) @@ -199,7 +199,7 @@ func (hs *HTTPServer) GetOrgUsers(c *contextmodel.ReqContext) response.Response }) if err != nil { - return response.Error(500, "Failed to get users for organization", err) + return response.Error(http.StatusInternalServerError, "Failed to get users for organization", err) } return response.JSON(http.StatusOK, result.OrgUsers) @@ -251,7 +251,7 @@ func (hs *HTTPServer) SearchOrgUsers(c *contextmodel.ReqContext) response.Respon }) if err != nil { - return response.Error(500, "Failed to get users for organization", err) + return response.Error(http.StatusInternalServerError, "Failed to get users for organization", err) } return response.JSON(http.StatusOK, result) @@ -286,7 +286,7 @@ func (hs *HTTPServer) SearchOrgUsersWithPaging(c *contextmodel.ReqContext) respo result, err := hs.searchOrgUsersHelper(c, query) if err != nil { - return response.Error(500, "Failed to get users for current organization", err) + return response.Error(http.StatusInternalServerError, "Failed to get users for current organization", err) } return response.JSON(http.StatusOK, result) @@ -501,9 +501,9 @@ func (hs *HTTPServer) RemoveOrgUser(c *contextmodel.ReqContext) response.Respons func (hs *HTTPServer) removeOrgUserHelper(ctx context.Context, cmd *org.RemoveOrgUserCommand) response.Response { if err := hs.orgService.RemoveOrgUser(ctx, cmd); err != nil { if errors.Is(err, org.ErrLastOrgAdmin) { - return response.Error(400, "Cannot remove last organization admin", nil) + return response.Error(http.StatusBadRequest, "Cannot remove last organization admin", nil) } - return response.Error(500, "Failed to remove user from organization", err) + return response.Error(http.StatusInternalServerError, "Failed to remove user from organization", err) } if cmd.UserWasDeleted { diff --git a/pkg/api/playlist.go b/pkg/api/playlist.go index 9f8b2acb85..40daad0e6f 100644 --- a/pkg/api/playlist.go +++ b/pkg/api/playlist.go @@ -92,7 +92,7 @@ func (hs *HTTPServer) SearchPlaylists(c *contextmodel.ReqContext) response.Respo playlists, err := hs.playlistService.Search(c.Req.Context(), &searchQuery) if err != nil { - return response.Error(500, "Search failed", err) + return response.Error(http.StatusInternalServerError, "Search failed", err) } return response.JSON(http.StatusOK, playlists) @@ -114,7 +114,7 @@ func (hs *HTTPServer) GetPlaylist(c *contextmodel.ReqContext) response.Response dto, err := hs.playlistService.Get(c.Req.Context(), &cmd) if err != nil { - return response.Error(500, "Playlist not found", err) + return response.Error(http.StatusInternalServerError, "Playlist not found", err) } return response.JSON(http.StatusOK, dto) @@ -136,7 +136,7 @@ func (hs *HTTPServer) GetPlaylistItems(c *contextmodel.ReqContext) response.Resp dto, err := hs.playlistService.Get(c.Req.Context(), &cmd) if err != nil { - return response.Error(500, "Playlist not found", err) + return response.Error(http.StatusInternalServerError, "Playlist not found", err) } return response.JSON(http.StatusOK, dto.Items) @@ -157,7 +157,7 @@ func (hs *HTTPServer) DeletePlaylist(c *contextmodel.ReqContext) response.Respon cmd := playlist.DeletePlaylistCommand{UID: uid, OrgId: c.SignedInUser.GetOrgID()} if err := hs.playlistService.Delete(c.Req.Context(), &cmd); err != nil { - return response.Error(500, "Failed to delete playlist", err) + return response.Error(http.StatusInternalServerError, "Failed to delete playlist", err) } return response.JSON(http.StatusOK, "") @@ -182,7 +182,7 @@ func (hs *HTTPServer) CreatePlaylist(c *contextmodel.ReqContext) response.Respon p, err := hs.playlistService.Create(c.Req.Context(), &cmd) if err != nil { - return response.Error(500, "Failed to create playlist", err) + return response.Error(http.StatusInternalServerError, "Failed to create playlist", err) } return response.JSON(http.StatusOK, p) @@ -208,7 +208,7 @@ func (hs *HTTPServer) UpdatePlaylist(c *contextmodel.ReqContext) response.Respon _, err := hs.playlistService.Update(c.Req.Context(), &cmd) if err != nil { - return response.Error(500, "Failed to save playlist", err) + return response.Error(http.StatusInternalServerError, "Failed to save playlist", err) } dto, err := hs.playlistService.Get(c.Req.Context(), &playlist.GetPlaylistByUidQuery{ @@ -216,7 +216,7 @@ func (hs *HTTPServer) UpdatePlaylist(c *contextmodel.ReqContext) response.Respon OrgId: c.SignedInUser.GetOrgID(), }) if err != nil { - return response.Error(500, "Failed to load playlist", err) + return response.Error(http.StatusInternalServerError, "Failed to load playlist", err) } return response.JSON(http.StatusOK, dto) } diff --git a/pkg/api/plugins.go b/pkg/api/plugins.go index c8e9d1b48a..55a610891e 100644 --- a/pkg/api/plugins.go +++ b/pkg/api/plugins.go @@ -247,7 +247,7 @@ func (hs *HTTPServer) UpdatePluginSetting(c *contextmodel.ReqContext) response.R pluginID := web.Params(c.Req)[":pluginId"] if _, exists := hs.pluginStore.Plugin(c.Req.Context(), pluginID); !exists { - return response.Error(404, "Plugin not installed", nil) + return response.Error(http.StatusNotFound, "Plugin not installed", nil) } cmd.OrgId = c.SignedInUser.GetOrgID() @@ -262,7 +262,7 @@ func (hs *HTTPServer) UpdatePluginSetting(c *contextmodel.ReqContext) response.R OrgID: cmd.OrgId, EncryptedSecureJSONData: cmd.EncryptedSecureJsonData, }); err != nil { - return response.Error(500, "Failed to update plugin setting", err) + return response.Error(http.StatusInternalServerError, "Failed to update plugin setting", err) } hs.pluginContextProvider.InvalidateSettingsCache(c.Req.Context(), pluginID) diff --git a/pkg/api/render.go b/pkg/api/render.go index 0855022b51..289d4d988c 100644 --- a/pkg/api/render.go +++ b/pkg/api/render.go @@ -18,7 +18,7 @@ import ( func (hs *HTTPServer) RenderToPng(c *contextmodel.ReqContext) { queryReader, err := util.NewURLQueryReader(c.Req.URL) if err != nil { - c.Handle(hs.Cfg, 400, "Render parameters error", err) + c.Handle(hs.Cfg, http.StatusBadRequest, "Render parameters error", err) return } @@ -36,7 +36,7 @@ func (hs *HTTPServer) RenderToPng(c *contextmodel.ReqContext) { timeout, err := strconv.Atoi(queryReader.Get("timeout", "60")) if err != nil { - c.Handle(hs.Cfg, 400, "Render parameters error", fmt.Errorf("cannot parse timeout as int: %s", err)) + c.Handle(hs.Cfg, http.StatusBadRequest, "Render parameters error", fmt.Errorf("cannot parse timeout as int: %s", err)) return } @@ -79,11 +79,11 @@ func (hs *HTTPServer) RenderToPng(c *contextmodel.ReqContext) { }, nil) if err != nil { if errors.Is(err, rendering.ErrTimeout) { - c.Handle(hs.Cfg, 500, err.Error(), err) + c.Handle(hs.Cfg, http.StatusInternalServerError, err.Error(), err) return } - c.Handle(hs.Cfg, 500, "Rendering failed.", err) + c.Handle(hs.Cfg, http.StatusInternalServerError, "Rendering failed.", err) return } diff --git a/pkg/api/response/response.go b/pkg/api/response/response.go index a037c8ca60..a1d9d24325 100644 --- a/pkg/api/response/response.go +++ b/pkg/api/response/response.go @@ -316,7 +316,7 @@ func Respond(status int, body any) *NormalResponse { default: var err error if b, err = json.Marshal(body); err != nil { - return Error(500, "body json marshal", err) + return Error(http.StatusInternalServerError, "body json marshal", err) } } diff --git a/pkg/api/routing/routing.go b/pkg/api/routing/routing.go index c5f5ec0975..8d6dcc03c0 100644 --- a/pkg/api/routing/routing.go +++ b/pkg/api/routing/routing.go @@ -1,6 +1,8 @@ package routing import ( + "net/http" + "github.com/grafana/grafana/pkg/api/response" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/web" @@ -8,7 +10,7 @@ import ( var ( ServerError = func(err error) response.Response { - return response.Error(500, "Server error", err) + return response.Error(http.StatusInternalServerError, "Server error", err) } ) diff --git a/pkg/api/search.go b/pkg/api/search.go index 87f48c4c8c..19c606f6c2 100644 --- a/pkg/api/search.go +++ b/pkg/api/search.go @@ -31,7 +31,7 @@ func (hs *HTTPServer) Search(c *contextmodel.ReqContext) response.Response { permission := dashboardaccess.PERMISSION_VIEW if limit > 5000 { - return response.Error(422, "Limit is above maximum allowed (5000), use page parameter to access hits beyond limit", nil) + return response.Error(http.StatusUnprocessableEntity, "Limit is above maximum allowed (5000), use page parameter to access hits beyond limit", nil) } if c.Query("permission") == "Edit" { @@ -67,7 +67,7 @@ func (hs *HTTPServer) Search(c *contextmodel.ReqContext) response.Response { bothFolderIds := len(folderIDs) > 0 && len(folderUIDs) > 0 if bothDashboardIds || bothFolderIds { - return response.Error(400, "search supports UIDs or IDs, not both", nil) + return response.Error(http.StatusBadRequest, "search supports UIDs or IDs, not both", nil) } searchQuery := search.Query{ @@ -89,7 +89,7 @@ func (hs *HTTPServer) Search(c *contextmodel.ReqContext) response.Response { hits, err := hs.SearchService.SearchHandler(c.Req.Context(), &searchQuery) if err != nil { - return response.Error(500, "Search failed", err) + return response.Error(http.StatusInternalServerError, "Search failed", err) } defer c.TimeRequest(metrics.MApiDashboardSearch) diff --git a/pkg/api/signup.go b/pkg/api/signup.go index ed5a5e048e..935817aaed 100644 --- a/pkg/api/signup.go +++ b/pkg/api/signup.go @@ -34,7 +34,7 @@ func (hs *HTTPServer) SignUp(c *contextmodel.ReqContext) response.Response { return response.Error(http.StatusBadRequest, "bad request data", err) } if !hs.Cfg.AllowUserSignUp { - return response.Error(401, "User signup is disabled", nil) + return response.Error(http.StatusUnauthorized, "User signup is disabled", nil) } form.Email, err = ValidateAndNormalizeEmail(form.Email) @@ -45,7 +45,7 @@ func (hs *HTTPServer) SignUp(c *contextmodel.ReqContext) response.Response { existing := user.GetUserByLoginQuery{LoginOrEmail: form.Email} _, err = hs.userService.GetByLogin(c.Req.Context(), &existing) if err == nil { - return response.Error(422, "User with same email address already exists", nil) + return response.Error(http.StatusUnprocessableEntity, "User with same email address already exists", nil) } userID, errID := identity.UserIdentifier(c.SignedInUser.GetNamespacedID()) @@ -60,19 +60,19 @@ func (hs *HTTPServer) SignUp(c *contextmodel.ReqContext) response.Response { cmd.InvitedByUserID = userID cmd.Code, err = util.GetRandomString(20) if err != nil { - return response.Error(500, "Failed to generate random string", err) + return response.Error(http.StatusInternalServerError, "Failed to generate random string", err) } cmd.RemoteAddr = c.RemoteAddr() if _, err := hs.tempUserService.CreateTempUser(c.Req.Context(), &cmd); err != nil { - return response.Error(500, "Failed to create signup", err) + return response.Error(http.StatusInternalServerError, "Failed to create signup", err) } if err := hs.bus.Publish(c.Req.Context(), &events.SignUpStarted{ Email: form.Email, Code: cmd.Code, }); err != nil { - return response.Error(500, "Failed to publish event", err) + return response.Error(http.StatusInternalServerError, "Failed to publish event", err) } metrics.MApiUserSignUpStarted.Inc() @@ -86,7 +86,7 @@ func (hs *HTTPServer) SignUpStep2(c *contextmodel.ReqContext) response.Response return response.Error(http.StatusBadRequest, "bad request data", err) } if !hs.Cfg.AllowUserSignUp { - return response.Error(401, "User signup is disabled", nil) + return response.Error(http.StatusUnauthorized, "User signup is disabled", nil) } form.Email = strings.TrimSpace(form.Email) @@ -111,10 +111,10 @@ func (hs *HTTPServer) SignUpStep2(c *contextmodel.ReqContext) response.Response usr, err := hs.userService.Create(c.Req.Context(), &createUserCmd) if err != nil { if errors.Is(err, user.ErrUserAlreadyExists) { - return response.Error(401, "User with same email address already exists", nil) + return response.Error(http.StatusUnauthorized, "User with same email address already exists", nil) } - return response.Error(500, "Failed to create user", err) + return response.Error(http.StatusInternalServerError, "Failed to create user", err) } // publish signup event @@ -122,7 +122,7 @@ func (hs *HTTPServer) SignUpStep2(c *contextmodel.ReqContext) response.Response Email: usr.Email, Name: usr.NameOrFallback(), }); err != nil { - return response.Error(500, "Failed to publish event", err) + return response.Error(http.StatusInternalServerError, "Failed to publish event", err) } // mark temp user as completed @@ -134,7 +134,7 @@ func (hs *HTTPServer) SignUpStep2(c *contextmodel.ReqContext) response.Response invitesQuery := tempuser.GetTempUsersQuery{Email: form.Email, Status: tempuser.TmpUserInvitePending} invitesQueryResult, err := hs.tempUserService.GetTempUsersQuery(c.Req.Context(), &invitesQuery) if err != nil { - return response.Error(500, "Failed to query database for invites", err) + return response.Error(http.StatusInternalServerError, "Failed to query database for invites", err) } apiResponse := util.DynMap{"message": "User sign up completed successfully", "code": "redirect-to-landing-page"} @@ -147,7 +147,7 @@ func (hs *HTTPServer) SignUpStep2(c *contextmodel.ReqContext) response.Response err = hs.loginUserWithUser(usr, c) if err != nil { - return response.Error(500, "failed to login user", err) + return response.Error(http.StatusInternalServerError, "failed to login user", err) } metrics.MApiUserSignUpCompleted.Inc() @@ -161,14 +161,14 @@ func (hs *HTTPServer) verifyUserSignUpEmail(ctx context.Context, email string, c queryResult, err := hs.tempUserService.GetTempUserByCode(ctx, &query) if err != nil { if errors.Is(err, tempuser.ErrTempUserNotFound) { - return false, response.Error(404, "Invalid email verification code", nil) + return false, response.Error(http.StatusNotFound, "Invalid email verification code", nil) } - return false, response.Error(500, "Failed to read temp user", err) + return false, response.Error(http.StatusInternalServerError, "Failed to read temp user", err) } tempUser := queryResult if tempUser.Email != email { - return false, response.Error(404, "Email verification code does not match email", nil) + return false, response.Error(http.StatusNotFound, "Email verification code does not match email", nil) } return true, nil diff --git a/pkg/api/user.go b/pkg/api/user.go index 73e30ac0a8..a00b378e6f 100644 --- a/pkg/api/user.go +++ b/pkg/api/user.go @@ -66,9 +66,9 @@ func (hs *HTTPServer) getUserUserProfile(c *contextmodel.ReqContext, userID int6 userProfile, err := hs.userService.GetProfile(c.Req.Context(), &query) if err != nil { if errors.Is(err, user.ErrUserNotFound) { - return response.Error(404, user.ErrUserNotFound.Error(), nil) + return response.Error(http.StatusNotFound, user.ErrUserNotFound.Error(), nil) } - return response.Error(500, "Failed to get user", err) + return response.Error(http.StatusInternalServerError, "Failed to get user", err) } getAuthQuery := login.GetAuthInfoQuery{UserId: userID} @@ -104,9 +104,9 @@ func (hs *HTTPServer) GetUserByLoginOrEmail(c *contextmodel.ReqContext) response usr, err := hs.userService.GetByLogin(c.Req.Context(), &query) if err != nil { if errors.Is(err, user.ErrUserNotFound) { - return response.Error(404, user.ErrUserNotFound.Error(), nil) + return response.Error(http.StatusNotFound, user.ErrUserNotFound.Error(), nil) } - return response.Error(500, "Failed to get user", err) + return response.Error(http.StatusInternalServerError, "Failed to get user", err) } result := user.UserProfileDTO{ ID: usr.ID, @@ -203,13 +203,13 @@ func (hs *HTTPServer) UpdateUserActiveOrg(c *contextmodel.ReqContext) response.R } if !hs.validateUsingOrg(c.Req.Context(), userID, orgID) { - return response.Error(401, "Not a valid organization", nil) + return response.Error(http.StatusUnauthorized, "Not a valid organization", nil) } cmd := user.SetUsingOrgCommand{UserID: userID, OrgID: orgID} if err := hs.userService.SetUsingOrg(c.Req.Context(), &cmd); err != nil { - return response.Error(500, "Failed to change active organization", err) + return response.Error(http.StatusInternalServerError, "Failed to change active organization", err) } return response.Success("Active organization changed") @@ -721,7 +721,7 @@ func (hs *HTTPServer) ClearHelpFlags(c *contextmodel.ReqContext) response.Respon } if err := hs.userService.SetUserHelpFlag(c.Req.Context(), &cmd); err != nil { - return response.Error(500, "Failed to update help flag", err) + return response.Error(http.StatusInternalServerError, "Failed to update help flag", err) } return response.JSON(http.StatusOK, &util.DynMap{"message": "Help flag set", "helpFlags1": cmd.HelpFlags1}) diff --git a/pkg/api/user_token.go b/pkg/api/user_token.go index 3bb187c32f..0662abcf5d 100644 --- a/pkg/api/user_token.go +++ b/pkg/api/user_token.go @@ -149,14 +149,14 @@ func (hs *HTTPServer) logoutUserFromAllDevicesInternal(ctx context.Context, user _, err := hs.userService.GetByID(ctx, &userQuery) if err != nil { if errors.Is(err, user.ErrUserNotFound) { - return response.Error(404, "User not found", err) + return response.Error(http.StatusNotFound, "User not found", err) } - return response.Error(500, "Could not read user from database", err) + return response.Error(http.StatusInternalServerError, "Could not read user from database", err) } err = hs.AuthTokenService.RevokeAllUserTokens(ctx, userID) if err != nil { - return response.Error(500, "Failed to logout user", err) + return response.Error(http.StatusInternalServerError, "Failed to logout user", err) } return response.JSON(http.StatusOK, util.DynMap{ @@ -181,7 +181,7 @@ func (hs *HTTPServer) getUserAuthTokensInternal(c *contextmodel.ReqContext, user tokens, err := hs.AuthTokenService.GetUserTokens(c.Req.Context(), userID) if err != nil { - return response.Error(500, "Failed to get user auth tokens", err) + return response.Error(http.StatusInternalServerError, "Failed to get user auth tokens", err) } result := []*dtos.UserToken{} @@ -241,29 +241,29 @@ func (hs *HTTPServer) revokeUserAuthTokenInternal(c *contextmodel.ReqContext, us _, err := hs.userService.GetByID(c.Req.Context(), &userQuery) if err != nil { if errors.Is(err, user.ErrUserNotFound) { - return response.Error(404, "User not found", err) + return response.Error(http.StatusNotFound, "User not found", err) } - return response.Error(500, "Failed to get user", err) + return response.Error(http.StatusInternalServerError, "Failed to get user", err) } token, err := hs.AuthTokenService.GetUserToken(c.Req.Context(), userID, cmd.AuthTokenId) if err != nil { if errors.Is(err, auth.ErrUserTokenNotFound) { - return response.Error(404, "User auth token not found", err) + return response.Error(http.StatusNotFound, "User auth token not found", err) } - return response.Error(500, "Failed to get user auth token", err) + return response.Error(http.StatusInternalServerError, "Failed to get user auth token", err) } if c.UserToken != nil && c.UserToken.Id == token.Id { - return response.Error(400, "Cannot revoke active user auth token", nil) + return response.Error(http.StatusBadRequest, "Cannot revoke active user auth token", nil) } err = hs.AuthTokenService.RevokeToken(c.Req.Context(), token, false) if err != nil { if errors.Is(err, auth.ErrUserTokenNotFound) { - return response.Error(404, "User auth token not found", err) + return response.Error(http.StatusNotFound, "User auth token not found", err) } - return response.Error(500, "Failed to revoke user auth token", err) + return response.Error(http.StatusInternalServerError, "Failed to revoke user auth token", err) } return response.JSON(http.StatusOK, util.DynMap{ diff --git a/pkg/middleware/auth.go b/pkg/middleware/auth.go index b56c473bb6..ef726a08c2 100644 --- a/pkg/middleware/auth.go +++ b/pkg/middleware/auth.go @@ -56,7 +56,7 @@ func notAuthorized(c *contextmodel.ReqContext) { func tokenRevoked(c *contextmodel.ReqContext, err *auth.TokenRevokedError) { if c.IsApiRequest() { - c.JSON(401, map[string]any{ + c.JSON(http.StatusUnauthorized, map[string]any{ "message": "Token revoked", "error": map[string]any{ "id": "ERR_TOKEN_REVOKED", diff --git a/pkg/middleware/recovery.go b/pkg/middleware/recovery.go index 59127e1502..01e07ee8e3 100644 --- a/pkg/middleware/recovery.go +++ b/pkg/middleware/recovery.go @@ -170,9 +170,9 @@ func Recovery(cfg *setting.Cfg, license licensing.Licensing) web.Middleware { resp["error"] = data.Title } - ctx.JSON(500, resp) + ctx.JSON(http.StatusInternalServerError, resp) } else { - ctx.HTML(500, cfg.ErrTemplateName, data) + ctx.HTML(http.StatusInternalServerError, cfg.ErrTemplateName, data) } } }() diff --git a/pkg/services/dashboardimport/api/api.go b/pkg/services/dashboardimport/api/api.go index f225f95e24..8134b190de 100644 --- a/pkg/services/dashboardimport/api/api.go +++ b/pkg/services/dashboardimport/api/api.go @@ -73,7 +73,7 @@ func (api *ImportDashboardAPI) ImportDashboard(c *contextmodel.ReqContext) respo } if limitReached { - return response.Error(403, "Quota reached", nil) + return response.Error(http.StatusForbidden, "Quota reached", nil) } req.User = c.SignedInUser diff --git a/pkg/services/ldap/api/service.go b/pkg/services/ldap/api/service.go index 2c288c60fb..2d5de2a1da 100644 --- a/pkg/services/ldap/api/service.go +++ b/pkg/services/ldap/api/service.go @@ -197,10 +197,10 @@ func (s *Service) PostSyncUserWithLDAP(c *contextmodel.ReqContext) response.Resp authModuleQuery := &login.GetAuthInfoQuery{UserId: usr.ID, AuthModule: login.LDAPAuthModule} if _, err := s.authInfoService.GetAuthInfo(c.Req.Context(), authModuleQuery); err != nil { // validate the userId comes from LDAP if errors.Is(err, user.ErrUserNotFound) { - return response.Error(404, user.ErrUserNotFound.Error(), nil) + return response.Error(http.StatusNotFound, user.ErrUserNotFound.Error(), nil) } - return response.Error(500, "Failed to get user", err) + return response.Error(http.StatusInternalServerError, "Failed to get user", err) } userInfo, _, err := ldapClient.User(usr.Login) diff --git a/pkg/services/libraryelements/api.go b/pkg/services/libraryelements/api.go index 179af4913c..970fd31212 100644 --- a/pkg/services/libraryelements/api.go +++ b/pkg/services/libraryelements/api.go @@ -318,31 +318,31 @@ func (l *LibraryElementService) filterLibraryPanelsByPermission(c *contextmodel. func toLibraryElementError(err error, message string) response.Response { if errors.Is(err, model.ErrLibraryElementAlreadyExists) { - return response.Error(400, model.ErrLibraryElementAlreadyExists.Error(), err) + return response.Error(http.StatusBadRequest, model.ErrLibraryElementAlreadyExists.Error(), err) } if errors.Is(err, model.ErrLibraryElementNotFound) { - return response.Error(404, model.ErrLibraryElementNotFound.Error(), err) + return response.Error(http.StatusNotFound, model.ErrLibraryElementNotFound.Error(), err) } if errors.Is(err, model.ErrLibraryElementDashboardNotFound) { - return response.Error(404, model.ErrLibraryElementDashboardNotFound.Error(), err) + return response.Error(http.StatusNotFound, model.ErrLibraryElementDashboardNotFound.Error(), err) } if errors.Is(err, model.ErrLibraryElementVersionMismatch) { - return response.Error(412, model.ErrLibraryElementVersionMismatch.Error(), err) + return response.Error(http.StatusPreconditionFailed, model.ErrLibraryElementVersionMismatch.Error(), err) } if errors.Is(err, dashboards.ErrFolderNotFound) { - return response.Error(404, dashboards.ErrFolderNotFound.Error(), err) + return response.Error(http.StatusNotFound, dashboards.ErrFolderNotFound.Error(), err) } if errors.Is(err, dashboards.ErrFolderAccessDenied) { - return response.Error(403, dashboards.ErrFolderAccessDenied.Error(), err) + return response.Error(http.StatusForbidden, dashboards.ErrFolderAccessDenied.Error(), err) } if errors.Is(err, model.ErrLibraryElementHasConnections) { - return response.Error(403, model.ErrLibraryElementHasConnections.Error(), err) + return response.Error(http.StatusForbidden, model.ErrLibraryElementHasConnections.Error(), err) } if errors.Is(err, model.ErrLibraryElementInvalidUID) { - return response.Error(400, model.ErrLibraryElementInvalidUID.Error(), err) + return response.Error(http.StatusBadRequest, model.ErrLibraryElementInvalidUID.Error(), err) } if errors.Is(err, model.ErrLibraryElementUIDTooLong) { - return response.Error(400, model.ErrLibraryElementUIDTooLong.Error(), err) + return response.Error(http.StatusBadRequest, model.ErrLibraryElementUIDTooLong.Error(), err) } return response.ErrOrFallback(http.StatusInternalServerError, message, err) } diff --git a/pkg/services/live/live.go b/pkg/services/live/live.go index d81f7a9996..f580caf6a3 100644 --- a/pkg/services/live/live.go +++ b/pkg/services/live/live.go @@ -1043,7 +1043,7 @@ func (g *GrafanaLive) HandleInfoHTTP(ctx *contextmodel.ReqContext) response.Resp "active": g.GrafanaScope.Dashboards.HasGitOpsObserver(ctx.SignedInUser.GetOrgID()), }) } - return response.JSONStreaming(404, util.DynMap{ + return response.JSONStreaming(http.StatusNotFound, util.DynMap{ "message": "Info is not supported for this channel", }) } diff --git a/pkg/services/ngalert/api/api_configuration.go b/pkg/services/ngalert/api/api_configuration.go index dd30998825..e1c4fd3376 100644 --- a/pkg/services/ngalert/api/api_configuration.go +++ b/pkg/services/ngalert/api/api_configuration.go @@ -72,16 +72,16 @@ func (srv ConfigSrv) RoutePostNGalertConfig(c *contextmodel.ReqContext, body api sendAlertsTo, err := ngmodels.StringToAlertmanagersChoice(string(body.AlertmanagersChoice)) if err != nil { - return response.Error(400, "Invalid alertmanager choice specified", err) + return response.Error(http.StatusBadRequest, "Invalid alertmanager choice specified", err) } externalAlertmanagers, err := srv.externalAlertmanagers(c.Req.Context(), c.SignedInUser.GetOrgID()) if err != nil { - return response.Error(500, "Couldn't fetch the external Alertmanagers from datasources", err) + return response.Error(http.StatusInternalServerError, "Couldn't fetch the external Alertmanagers from datasources", err) } if sendAlertsTo == ngmodels.ExternalAlertmanagers && len(externalAlertmanagers) < 1 { - return response.Error(400, "At least one Alertmanager must be provided or configured as a datasource that handles alerts to choose this option", nil) + return response.Error(http.StatusBadRequest, "At least one Alertmanager must be provided or configured as a datasource that handles alerts to choose this option", nil) } cfg := &ngmodels.AdminConfiguration{ diff --git a/pkg/services/ngalert/api/api_provisioning.go b/pkg/services/ngalert/api/api_provisioning.go index 1246fef993..2bba2976ff 100644 --- a/pkg/services/ngalert/api/api_provisioning.go +++ b/pkg/services/ngalert/api/api_provisioning.go @@ -604,11 +604,11 @@ func exportHcl(download bool, body definitions.AlertingFileExport) response.Resp return nil } if err := convertToResources(); err != nil { - return response.Error(500, "failed to convert to HCL resources", err) + return response.Error(http.StatusInternalServerError, "failed to convert to HCL resources", err) } hclBody, err := hcl.Encode(resources...) if err != nil { - return response.Error(500, "body hcl encode", err) + return response.Error(http.StatusInternalServerError, "body hcl encode", err) } resp := response.Respond(http.StatusOK, hclBody) if download { diff --git a/pkg/services/searchV2/http.go b/pkg/services/searchV2/http.go index 8751da74ed..0b58e13469 100644 --- a/pkg/services/searchV2/http.go +++ b/pkg/services/searchV2/http.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "io" + "net/http" "github.com/prometheus/client_golang/prometheus" @@ -39,7 +40,7 @@ func (s *searchHTTPService) doQuery(c *contextmodel.ReqContext) response.Respons "reason": searchReadinessCheckResp.Reason, }).Inc() - return response.JSON(200, &backend.DataResponse{ + return response.JSON(http.StatusOK, &backend.DataResponse{ Frames: []*data.Frame{{ Name: "Loading", }}, @@ -49,30 +50,30 @@ func (s *searchHTTPService) doQuery(c *contextmodel.ReqContext) response.Respons body, err := io.ReadAll(c.Req.Body) if err != nil { - return response.Error(500, "error reading bytes", err) + return response.Error(http.StatusInternalServerError, "error reading bytes", err) } query := &DashboardQuery{} err = json.Unmarshal(body, query) if err != nil { - return response.Error(400, "error parsing body", err) + return response.Error(http.StatusBadRequest, "error parsing body", err) } resp := s.search.doDashboardQuery(c.Req.Context(), c.SignedInUser, c.SignedInUser.GetOrgID(), *query) if resp.Error != nil { - return response.Error(500, "error handling search request", resp.Error) + return response.Error(http.StatusInternalServerError, "error handling search request", resp.Error) } if len(resp.Frames) == 0 { msg := "invalid search response" - return response.Error(500, msg, errors.New(msg)) + return response.Error(http.StatusInternalServerError, msg, errors.New(msg)) } bytes, err := resp.MarshalJSON() if err != nil { - return response.Error(500, "error marshalling response", err) + return response.Error(http.StatusInternalServerError, "error marshalling response", err) } - return response.JSON(200, bytes) + return response.JSON(http.StatusOK, bytes) } diff --git a/pkg/services/searchusers/searchusers.go b/pkg/services/searchusers/searchusers.go index 2389de178d..b912fbecc9 100644 --- a/pkg/services/searchusers/searchusers.go +++ b/pkg/services/searchusers/searchusers.go @@ -46,7 +46,7 @@ func ProvideUsersService(cfg *setting.Cfg, searchUserFilter user.SearchUserFilte func (s *OSSService) SearchUsers(c *contextmodel.ReqContext) response.Response { result, err := s.SearchUser(c) if err != nil { - return response.ErrOrFallback(500, "Failed to fetch users", err) + return response.ErrOrFallback(http.StatusInternalServerError, "Failed to fetch users", err) } return response.JSON(http.StatusOK, result.Users) @@ -65,7 +65,7 @@ func (s *OSSService) SearchUsers(c *contextmodel.ReqContext) response.Response { func (s *OSSService) SearchUsersWithPaging(c *contextmodel.ReqContext) response.Response { result, err := s.SearchUser(c) if err != nil { - return response.ErrOrFallback(500, "Failed to fetch users", err) + return response.ErrOrFallback(http.StatusInternalServerError, "Failed to fetch users", err) } return response.JSON(http.StatusOK, result) diff --git a/pkg/services/store/http.go b/pkg/services/store/http.go index ad748a4a07..432e4d872b 100644 --- a/pkg/services/store/http.go +++ b/pkg/services/store/http.go @@ -68,7 +68,7 @@ func (s *standardStorageService) doWrite(c *contextmodel.ReqContext) response.Re if err != nil { return response.Error(http.StatusBadRequest, "save error", err) } - return response.JSON(200, rsp) + return response.JSON(http.StatusOK, rsp) } func (s *standardStorageService) doUpload(c *contextmodel.ReqContext) response.Response { @@ -85,7 +85,7 @@ func (s *standardStorageService) doUpload(c *contextmodel.ReqContext) response.R if err := c.Req.ParseMultipartForm(MAX_UPLOAD_SIZE); err != nil { rsp.Message = fmt.Sprintf("Please limit file uploaded under %s", util.ByteCountSI(MAX_UPLOAD_SIZE)) rsp.Error = true - return response.JSON(400, rsp) + return response.JSON(http.StatusBadRequest, rsp) } message := getMultipartFormValue(c.Req, "message") overwriteExistingFile := getMultipartFormValue(c.Req, "overwriteExistingFile") != "false" // must explicitly overwrite @@ -99,7 +99,7 @@ func (s *standardStorageService) doUpload(c *contextmodel.ReqContext) response.R if path == "" && folder == "" { rsp.Message = "please specify the upload folder or full path" rsp.Error = true - return response.JSON(400, rsp) + return response.JSON(http.StatusBadRequest, rsp) } for _, fileHeader := range fileHeaders { @@ -107,15 +107,15 @@ func (s *standardStorageService) doUpload(c *contextmodel.ReqContext) response.R // open each file to copy contents file, err := fileHeader.Open() if err != nil { - return response.Error(500, "Internal Server Error", err) + return response.Error(http.StatusInternalServerError, "Internal Server Error", err) } err = file.Close() if err != nil { - return response.Error(500, "Internal Server Error", err) + return response.Error(http.StatusInternalServerError, "Internal Server Error", err) } data, err := io.ReadAll(file) if err != nil { - return response.Error(500, "Internal Server Error", err) + return response.Error(http.StatusInternalServerError, "Internal Server Error", err) } if path == "" { @@ -147,7 +147,7 @@ func (s *standardStorageService) doUpload(c *contextmodel.ReqContext) response.R } } - return response.JSON(200, rsp) + return response.JSON(http.StatusOK, rsp) } func getMultipartFormValue(req *http.Request, key string) string { @@ -163,11 +163,11 @@ func (s *standardStorageService) read(c *contextmodel.ReqContext) response.Respo scope, path := getPathAndScope(c) file, err := s.Read(c.Req.Context(), c.SignedInUser, scope+"/"+path) if err != nil { - return response.Error(400, "cannot call read", err) + return response.Error(http.StatusBadRequest, "cannot call read", err) } if file == nil || file.Contents == nil { - return response.Error(404, "file does not exist", err) + return response.Error(http.StatusNotFound, "file does not exist", err) } // set the correct content type for svg @@ -181,9 +181,9 @@ func (s *standardStorageService) getOptions(c *contextmodel.ReqContext) response scope, path := getPathAndScope(c) opts, err := s.getWorkflowOptions(c.Req.Context(), c.SignedInUser, scope+"/"+path) if err != nil { - return response.Error(400, err.Error(), err) + return response.Error(http.StatusBadRequest, err.Error(), err) } - return response.JSON(200, opts) + return response.JSON(http.StatusOK, opts) } func (s *standardStorageService) doDelete(c *contextmodel.ReqContext) response.Response { @@ -192,9 +192,9 @@ func (s *standardStorageService) doDelete(c *contextmodel.ReqContext) response.R err := s.Delete(c.Req.Context(), c.SignedInUser, scope+"/"+path) if err != nil { - return response.Error(400, "failed to delete the file: "+err.Error(), err) + return response.Error(http.StatusBadRequest, "failed to delete the file: "+err.Error(), err) } - return response.JSON(200, map[string]any{ + return response.JSON(http.StatusOK, map[string]any{ "message": "Removed file from storage", "success": true, "path": path, @@ -204,26 +204,26 @@ func (s *standardStorageService) doDelete(c *contextmodel.ReqContext) response.R func (s *standardStorageService) doDeleteFolder(c *contextmodel.ReqContext) response.Response { body, err := io.ReadAll(c.Req.Body) if err != nil { - return response.Error(500, "error reading bytes", err) + return response.Error(http.StatusInternalServerError, "error reading bytes", err) } cmd := &DeleteFolderCmd{} err = json.Unmarshal(body, cmd) if err != nil { - return response.Error(400, "error parsing body", err) + return response.Error(http.StatusBadRequest, "error parsing body", err) } if cmd.Path == "" { - return response.Error(400, "empty path", err) + return response.Error(http.StatusBadRequest, "empty path", err) } // full path is api/storage/delete/upload/example.jpg, but we only want the part after upload _, path := getPathAndScope(c) if err := s.DeleteFolder(c.Req.Context(), c.SignedInUser, cmd); err != nil { - return response.Error(400, "failed to delete the folder: "+err.Error(), err) + return response.Error(http.StatusBadRequest, "failed to delete the folder: "+err.Error(), err) } - return response.JSON(200, map[string]any{ + return response.JSON(http.StatusOK, map[string]any{ "message": "Removed folder from storage", "success": true, "path": path, @@ -233,24 +233,24 @@ func (s *standardStorageService) doDeleteFolder(c *contextmodel.ReqContext) resp func (s *standardStorageService) doCreateFolder(c *contextmodel.ReqContext) response.Response { body, err := io.ReadAll(c.Req.Body) if err != nil { - return response.Error(500, "error reading bytes", err) + return response.Error(http.StatusInternalServerError, "error reading bytes", err) } cmd := &CreateFolderCmd{} err = json.Unmarshal(body, cmd) if err != nil { - return response.Error(400, "error parsing body", err) + return response.Error(http.StatusBadRequest, "error parsing body", err) } if cmd.Path == "" { - return response.Error(400, "empty path", err) + return response.Error(http.StatusBadRequest, "empty path", err) } if err := s.CreateFolder(c.Req.Context(), c.SignedInUser, cmd); err != nil { - return response.Error(400, "failed to create the folder: "+err.Error(), err) + return response.Error(http.StatusBadRequest, "failed to create the folder: "+err.Error(), err) } - return response.JSON(200, map[string]any{ + return response.JSON(http.StatusOK, map[string]any{ "message": "Folder created", "success": true, "path": cmd.Path, @@ -263,10 +263,10 @@ func (s *standardStorageService) list(c *contextmodel.ReqContext) response.Respo // maxFiles of 0 will result in default behaviour from wrapper frame, err := s.List(c.Req.Context(), c.SignedInUser, path, 0) if err != nil { - return response.Error(400, "error reading path", err) + return response.Error(http.StatusBadRequest, "error reading path", err) } if frame == nil { - return response.Error(404, "not found", nil) + return response.Error(http.StatusNotFound, "not found", nil) } return response.JSONStreaming(http.StatusOK, frame) } @@ -281,5 +281,5 @@ func (s *standardStorageService) getConfig(c *contextmodel.ReqContext) response. for _, s := range storages { roots = append(roots, s.Meta()) } - return response.JSON(200, roots) + return response.JSON(http.StatusOK, roots) } From 19743a7fef3aa8e0e2fc25e8b1eefb6ca603afc7 Mon Sep 17 00:00:00 2001 From: Oscar Kilhed Date: Tue, 27 Feb 2024 17:56:29 +0100 Subject: [PATCH 0243/1406] Dashboard scenes: Change library panel to set dashboard key to panel instead of library panel. (#83420) * change library panel so that the dashboard key is attached to the panel instead of the library panel * make sure the save model gets the id from the panel and not the library panel * do not reload everything when the library panel is already loaded * Fix merge issue * Clean up --- .../inspect/InspectJsonTab.test.tsx | 2 +- .../scene/DashboardSceneUrlSync.ts | 41 ++++++++++++++++++- .../dashboard-scene/scene/LibraryVizPanel.tsx | 15 +++++-- .../transformSaveModelToScene.ts | 2 +- .../transformSceneToSaveModel.ts | 5 ++- .../features/dashboard-scene/utils/utils.ts | 5 +++ 6 files changed, 61 insertions(+), 9 deletions(-) diff --git a/public/app/features/dashboard-scene/inspect/InspectJsonTab.test.tsx b/public/app/features/dashboard-scene/inspect/InspectJsonTab.test.tsx index f155cb206f..264e92daab 100644 --- a/public/app/features/dashboard-scene/inspect/InspectJsonTab.test.tsx +++ b/public/app/features/dashboard-scene/inspect/InspectJsonTab.test.tsx @@ -216,7 +216,7 @@ async function buildTestSceneWithLibraryPanel() { name: 'LibraryPanel A', title: 'LibraryPanel A title', uid: '111', - key: 'panel-22', + panelKey: 'panel-22', model: panel, version: 1, }; diff --git a/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts b/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts index 5b922de6bc..f818fd2787 100644 --- a/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts +++ b/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts @@ -2,15 +2,22 @@ import { Unsubscribable } from 'rxjs'; import { AppEvents } from '@grafana/data'; import { locationService } from '@grafana/runtime'; -import { SceneObjectBase, SceneObjectState, SceneObjectUrlSyncHandler, SceneObjectUrlValues } from '@grafana/scenes'; +import { + SceneObjectBase, + SceneObjectState, + SceneObjectUrlSyncHandler, + SceneObjectUrlValues, + VizPanel, +} from '@grafana/scenes'; import appEvents from 'app/core/app_events'; import { PanelInspectDrawer } from '../inspect/PanelInspectDrawer'; import { buildPanelEditScene } from '../panel-edit/PanelEditor'; import { createDashboardEditViewFor } from '../settings/utils'; -import { findVizPanelByKey, getDashboardSceneFor, isPanelClone } from '../utils/utils'; +import { findVizPanelByKey, getDashboardSceneFor, isLibraryPanelChild, isPanelClone } from '../utils/utils'; import { DashboardScene, DashboardSceneState } from './DashboardScene'; +import { LibraryVizPanel } from './LibraryVizPanel'; import { ViewPanelScene } from './ViewPanelScene'; import { DashboardRepeatsProcessedEvent } from './types'; @@ -71,6 +78,7 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler { // Handle view panel state if (typeof values.viewPanel === 'string') { const panel = findVizPanelByKey(this._scene, values.viewPanel); + if (!panel) { // // If we are trying to view a repeat clone that can't be found it might be that the repeats have not been processed yet if (isPanelClone(values.viewPanel)) { @@ -83,6 +91,11 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler { return; } + if (isLibraryPanelChild(panel)) { + this._handleLibraryPanel(panel, (p) => this._buildLibraryPanelViewScene(p)); + return; + } + update.viewPanelScene = new ViewPanelScene({ panelRef: panel.getRef() }); } else if (viewPanelScene && values.viewPanel === null) { update.viewPanelScene = undefined; @@ -99,6 +112,11 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler { if (!isEditing) { this._scene.onEnterEditMode(); } + if (isLibraryPanelChild(panel)) { + this._handleLibraryPanel(panel, (p) => { + this._scene.setState({ editPanel: buildPanelEditScene(p) }); + }); + } update.editPanel = buildPanelEditScene(panel); } else if (editPanel && values.editPanel === null) { update.editPanel = undefined; @@ -109,6 +127,25 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler { } } + private _buildLibraryPanelViewScene(vizPanel: VizPanel) { + this._scene.setState({ viewPanelScene: new ViewPanelScene({ panelRef: vizPanel.getRef() }) }); + } + + private _handleLibraryPanel(vizPanel: VizPanel, cb: (p: VizPanel) => void): void { + if (!(vizPanel.parent instanceof LibraryVizPanel)) { + throw new Error('Panel is not a child of a LibraryVizPanel'); + } + const libraryPanel = vizPanel.parent; + if (libraryPanel.state.isLoaded) { + cb(vizPanel); + } else { + libraryPanel.subscribeToState((n) => { + cb(n.panel!); + }); + libraryPanel.activate(); + } + } + private _handleViewRepeatClone(viewPanel: string) { if (!this._eventSub) { this._eventSub = this._scene.subscribeToEvent(DashboardRepeatsProcessedEvent, () => { diff --git a/public/app/features/dashboard-scene/scene/LibraryVizPanel.tsx b/public/app/features/dashboard-scene/scene/LibraryVizPanel.tsx index 61beed8fff..b1c2be5d6f 100644 --- a/public/app/features/dashboard-scene/scene/LibraryVizPanel.tsx +++ b/public/app/features/dashboard-scene/scene/LibraryVizPanel.tsx @@ -16,6 +16,8 @@ interface LibraryVizPanelState extends SceneObjectState { uid: string; name: string; panel?: VizPanel; + isLoaded?: boolean; + panelKey: string; _loadedVersion?: number; } @@ -24,7 +26,8 @@ export class LibraryVizPanel extends SceneObjectBase { constructor(state: LibraryVizPanelState) { super({ - panel: state.panel ?? getLoadingPanel(state.title), + panel: state.panel ?? getLoadingPanel(state.title, state.panelKey), + isLoaded: state.isLoaded ?? false, ...state, }); @@ -32,7 +35,9 @@ export class LibraryVizPanel extends SceneObjectBase { } private _onActivate = () => { - this.loadLibraryPanelFromPanelModel(); + if (!this.state.isLoaded) { + this.loadLibraryPanelFromPanelModel(); + } }; private async loadLibraryPanelFromPanelModel() { @@ -49,6 +54,7 @@ export class LibraryVizPanel extends SceneObjectBase { const panel = new VizPanel({ title: this.state.title, + key: this.state.panelKey, options: libPanelModel.options ?? {}, fieldConfig: libPanelModel.fieldConfig, pluginId: libPanelModel.type, @@ -66,7 +72,7 @@ export class LibraryVizPanel extends SceneObjectBase { ], }); - this.setState({ panel, _loadedVersion: libPanel.version }); + this.setState({ panel, _loadedVersion: libPanel.version, isLoaded: true }); } catch (err) { vizPanel.setState({ _pluginLoadError: 'Unable to load library panel: ' + this.state.uid, @@ -75,8 +81,9 @@ export class LibraryVizPanel extends SceneObjectBase { } } -function getLoadingPanel(title: string) { +function getLoadingPanel(title: string, panelKey: string) { return new VizPanel({ + key: panelKey, title, menu: new VizPanelMenu({ $behaviors: [panelMenuBehavior], diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts index b1bf8d691c..096f378a01 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts @@ -408,7 +408,7 @@ export function buildGridItemForLibPanel(panel: PanelModel) { title: panel.title, uid: panel.libraryPanel.uid, name: panel.libraryPanel.name, - key: getVizPanelKeyForPanelId(panel.id), + panelKey: getVizPanelKeyForPanelId(panel.id), }); return new SceneGridItem({ diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts index d452f99539..d0f61b9f2c 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts @@ -153,8 +153,11 @@ export function gridItemToPanel(gridItem: SceneGridItemLike, isSnapshot = false) w = gridItem.state.width ?? 0; h = gridItem.state.height ?? 0; + if (!gridItem.state.body.state.panel) { + throw new Error('Library panel has no panel'); + } return { - id: getPanelIdForVizPanel(gridItem.state.body), + id: getPanelIdForVizPanel(gridItem.state.body.state.panel), title: gridItem.state.body.state.title, gridPos: { x, y, w, h }, libraryPanel: { diff --git a/public/app/features/dashboard-scene/utils/utils.ts b/public/app/features/dashboard-scene/utils/utils.ts index b34f5103b9..4ddf171e05 100644 --- a/public/app/features/dashboard-scene/utils/utils.ts +++ b/public/app/features/dashboard-scene/utils/utils.ts @@ -15,6 +15,7 @@ import { import { initialIntervalVariableModelState } from 'app/features/variables/interval/reducer'; import { DashboardScene } from '../scene/DashboardScene'; +import { LibraryVizPanel } from '../scene/LibraryVizPanel'; import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks'; import { panelMenuBehavior } from '../scene/PanelMenuBehavior'; @@ -259,3 +260,7 @@ export function getDefaultVizPanel(dashboard: DashboardScene): VizPanel { }), }); } + +export function isLibraryPanelChild(vizPanel: VizPanel) { + return vizPanel.parent instanceof LibraryVizPanel; +} From a30617f8bd9a1930c8d8362137a0d89310b7cf91 Mon Sep 17 00:00:00 2001 From: Jev Forsberg <46619047+baldm0mma@users.noreply.github.com> Date: Tue, 27 Feb 2024 10:41:55 -0700 Subject: [PATCH 0244/1406] Transformations: Add transformation builder tests and refactor (#83285) * baldm0mma/script_tests/ update jest.config paths * baldm0mma/script_tests/ refactor makefile content * baldm0mma/script_tests/ refactor file write commands * baldm0mma/script_tests/ create test module * baldm0mma/script_tests/ add to codeowners * baldm0mma/script_tests/ recenter content * baldm0mma/script_tests/ create buildMarkdownContent and update whitespace * baldm0mma/script_tests/ run build script * baldm0mma/script_tests/ update tests to remove make dep and node dep * baldm0mma/script_tests/ update cntent * baldm0mma/script_tests/ update makefile * baldm0mma/script_tests/ update buildMarkdownContent * baldm0mma/script_tests/ remove unused imports * baldm0mma/script_tests/ update test annotation * baldm0mma/script_tests/ prettier * baldm0mma/script_tests/ update tests * baldm0mma/script_tests/ update content * baldm0mma/script_tests/ update annos * baldm0mma/script_tests/ remove unused vars * baldm0mma/script_tests/ remove unused imports * baldm0mma/script_tests/ update annos * baldm0mma/script_tests/ update paths * Update scripts/docs/generate-transformations.test.ts Co-authored-by: Jack Baldry * baldm0mma/script_tests/ update comment --------- Co-authored-by: Jack Baldry --- .github/CODEOWNERS | 2 +- docs/Makefile | 3 +- jest.config.js | 2 +- .../app/features/transformers/docs/content.ts | 1732 ++++++++--------- scripts/docs/generate-transformations.test.ts | 57 + scripts/docs/generate-transformations.ts | 52 +- 6 files changed, 962 insertions(+), 886 deletions(-) create mode 100644 scripts/docs/generate-transformations.test.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index b35e441ffb..42cb5d9276 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -530,7 +530,7 @@ playwright.config.ts @grafana/plugins-platform-frontend /scripts/generate-icon-bundle.js @grafana/plugins-platform-frontend @grafana/grafana-frontend-platform /scripts/levitate-parse-json-report.js @grafana/plugins-platform-frontend -/scripts/docs/generate-transformations.ts @grafana/dataviz-squad +/scripts/docs/generate-transformations* @grafana/dataviz-squad /scripts/webpack/ @grafana/frontend-ops /scripts/generate-a11y-report.sh @grafana/grafana-frontend-platform .pa11yci.conf.js @grafana/grafana-frontend-platform diff --git a/docs/Makefile b/docs/Makefile index 38997200b0..36b7e4f41b 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -9,7 +9,6 @@ include docs.mk .PHONY: sources/panels-visualizations/query-transform-data/transform-data/index.md sources/panels-visualizations/query-transform-data/transform-data/index.md: ## Generate the Transform Data page source. -sources/panels-visualizations/query-transform-data/transform-data/index.md: cd $(CURDIR)/.. && npx tsc ./scripts/docs/generate-transformations.ts && \ - node ./scripts/docs/generate-transformations.js > $(CURDIR)/$@ && \ + node -e "require('./scripts/docs/generate-transformations').buildMarkdownContent()" && \ npx prettier -w $(CURDIR)/$@ diff --git a/jest.config.js b/jest.config.js index 14b6e1084f..cc797aa32a 100644 --- a/jest.config.js +++ b/jest.config.js @@ -30,7 +30,7 @@ module.exports = { `/node_modules/(?!${esModules})`, // exclude es modules to prevent TS complaining ], moduleDirectories: ['public', 'node_modules'], - roots: ['/public/app', '/public/test', '/packages'], + roots: ['/public/app', '/public/test', '/packages', '/scripts'], testRegex: '(\\.|/)(test)\\.(jsx?|tsx?)$', moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'], setupFiles: ['jest-canvas-mock', './public/test/jest-setup.ts'], diff --git a/public/app/features/transformers/docs/content.ts b/public/app/features/transformers/docs/content.ts index 138bf005a0..f83e8cef41 100644 --- a/public/app/features/transformers/docs/content.ts +++ b/public/app/features/transformers/docs/content.ts @@ -39,44 +39,44 @@ export const transformationDocsContent: TransformationDocsContentType = { */ getHelperDocs: function (imageRenderType: ImageRenderType = ImageRenderType.ShortcodeFigure) { return ` - Use this transformation to add a new field calculated from two other fields. Each transformation allows you to add one new field. - - - **Mode** - Select a mode: - - **Reduce row** - Apply selected calculation on each row of selected fields independently. - - **Binary operation** - Apply basic binary operations (for example, sum or multiply) on values in a single row from two selected fields. - - **Unary operation** - Apply basic unary operations on values in a single row from a selected field. The available operations are: - - **Absolute value (abs)** - Returns the absolute value of a given expression. It represents its distance from zero as a positive number. - - **Natural exponential (exp)** - Returns _e_ raised to the power of a given expression. - - **Natural logarithm (ln)** - Returns the natural logarithm of a given expression. - - **Floor (floor)** - Returns the largest integer less than or equal to a given expression. - - **Ceiling (ceil)** - Returns the smallest integer greater than or equal to a given expression. - - **Cumulative functions** - Apply functions on the current row and all preceding rows. - - **Total** - Calculates the cumulative total up to and including the current row. - - **Mean** - Calculates the mean up to and including the current row. - - **Window functions** - Apply window functions. The window can either be **trailing** or **centered**. - With a trailing window the current row will be the last row in the window. - With a centered window the window will be centered on the current row. - For even window sizes, the window will be centered between the current row, and the previous row. - - **Mean** - Calculates the moving mean or running average. - - **Stddev** - Calculates the moving standard deviation. - - **Variance** - Calculates the moving variance. - - **Row index** - Insert a field with the row index. - - **Field name** - Select the names of fields you want to use in the calculation for the new field. - - **Calculation** - If you select **Reduce row** mode, then the **Calculation** field appears. Click in the field to see a list of calculation choices you can use to create the new field. For information about available calculations, refer to [Calculation types][]. - - **Operation** - If you select **Binary operation** or **Unary operation** mode, then the **Operation** fields appear. These fields allow you to apply basic math operations on values in a single row from selected fields. You can also use numerical values for binary operations. - - **As percentile** - If you select **Row index** mode, then the **As percentile** switch appears. This switch allows you to transform the row index as a percentage of the total number of rows. - - **Alias** - (Optional) Enter the name of your new field. If you leave this blank, then the field will be named to match the calculation. - - **Replace all fields** - (Optional) Select this option if you want to hide all other fields and display only your calculated field in the visualization. - - > **Note:** **Cumulative functions** and **Window functions** modes are currently in public preview. Grafana Labs offers limited support, and breaking changes might occur prior to the feature being made generally available. Enable the \`addFieldFromCalculationStatFunctions\` feature toggle in Grafana to use this feature. Contact Grafana Support to enable this feature in Grafana Cloud. - - In the example below, we added two fields together and named them Sum. +Use this transformation to add a new field calculated from two other fields. Each transformation allows you to add one new field. + +- **Mode** - Select a mode: + - **Reduce row** - Apply selected calculation on each row of selected fields independently. + - **Binary operation** - Apply basic binary operations (for example, sum or multiply) on values in a single row from two selected fields. + - **Unary operation** - Apply basic unary operations on values in a single row from a selected field. The available operations are: + - **Absolute value (abs)** - Returns the absolute value of a given expression. It represents its distance from zero as a positive number. + - **Natural exponential (exp)** - Returns _e_ raised to the power of a given expression. + - **Natural logarithm (ln)** - Returns the natural logarithm of a given expression. + - **Floor (floor)** - Returns the largest integer less than or equal to a given expression. + - **Ceiling (ceil)** - Returns the smallest integer greater than or equal to a given expression. + - **Cumulative functions** - Apply functions on the current row and all preceding rows. + - **Total** - Calculates the cumulative total up to and including the current row. + - **Mean** - Calculates the mean up to and including the current row. + - **Window functions** - Apply window functions. The window can either be **trailing** or **centered**. + With a trailing window the current row will be the last row in the window. + With a centered window the window will be centered on the current row. + For even window sizes, the window will be centered between the current row, and the previous row. + - **Mean** - Calculates the moving mean or running average. + - **Stddev** - Calculates the moving standard deviation. + - **Variance** - Calculates the moving variance. + - **Row index** - Insert a field with the row index. +- **Field name** - Select the names of fields you want to use in the calculation for the new field. +- **Calculation** - If you select **Reduce row** mode, then the **Calculation** field appears. Click in the field to see a list of calculation choices you can use to create the new field. For information about available calculations, refer to [Calculation types][]. +- **Operation** - If you select **Binary operation** or **Unary operation** mode, then the **Operation** fields appear. These fields allow you to apply basic math operations on values in a single row from selected fields. You can also use numerical values for binary operations. +- **As percentile** - If you select **Row index** mode, then the **As percentile** switch appears. This switch allows you to transform the row index as a percentage of the total number of rows. +- **Alias** - (Optional) Enter the name of your new field. If you leave this blank, then the field will be named to match the calculation. +- **Replace all fields** - (Optional) Select this option if you want to hide all other fields and display only your calculated field in the visualization. + +> **Note:** **Cumulative functions** and **Window functions** modes are currently in public preview. Grafana Labs offers limited support, and breaking changes might occur prior to the feature being made generally available. Enable the \`addFieldFromCalculationStatFunctions\` feature toggle in Grafana to use this feature. Contact Grafana Support to enable this feature in Grafana Cloud. + +In the example below, we added two fields together and named them Sum. - ${buildImageContent( - '/static/img/docs/transformations/add-field-from-calc-stat-example-7-0.png', - imageRenderType, - 'A stat visualization including one field called Sum' - )} +${buildImageContent( + '/static/img/docs/transformations/add-field-from-calc-stat-example-7-0.png', + imageRenderType, + 'A stat visualization including one field called Sum' +)} `; }, links: [ @@ -90,31 +90,31 @@ export const transformationDocsContent: TransformationDocsContentType = { name: 'Concatenate fields', getHelperDocs: function () { return ` - Use this transformation to combine all fields from all frames into one result. - - For example, if you have separate queries retrieving temperature and uptime data (Query A) and air quality index and error information (Query B), applying the concatenate transformation yields a consolidated data frame with all relevant information in one view. +Use this transformation to combine all fields from all frames into one result. - Consider the following: +For example, if you have separate queries retrieving temperature and uptime data (Query A) and air quality index and error information (Query B), applying the concatenate transformation yields a consolidated data frame with all relevant information in one view. - **Query A:** +Consider the following: - | Temp | Uptime | - | ----- | --------- | - | 15.4 | 1230233 | +**Query A:** - **Query B:** +| Temp | Uptime | +| ----- | ------- | +| 15.4 | 1230233 | - | AQI | Errors | - | ----- | ------ | - | 3.2 | 5 | +**Query B:** - After you concatenate the fields, the data frame would be: +| AQI | Errors | +| ----- | ------ | +| 3.2 | 5 | - | Temp | Uptime | AQI | Errors | - | ----- | -------- | ----- | ------ | - | 15.4 | 1230233 | 3.2 | 5 | +After you concatenate the fields, the data frame would be: - This transformation simplifies the process of merging data from different sources, providing a comprehensive view for analysis and visualization. +| Temp | Uptime | AQI | Errors | +| ---- | ------- | --- | ------ | +| 15.4 | 1230233 | 3.2 | 5 | + +This transformation simplifies the process of merging data from different sources, providing a comprehensive view for analysis and visualization. `; }, }, @@ -122,64 +122,64 @@ export const transformationDocsContent: TransformationDocsContentType = { name: 'Config from query results', getHelperDocs: function () { return ` - Use this transformation to select a query and extract standard options, such as **Min**, **Max**, **Unit**, and **Thresholds**, and apply them to other query results. This feature enables dynamic visualization configuration based on the data returned by a specific query. +Use this transformation to select a query and extract standard options, such as **Min**, **Max**, **Unit**, and **Thresholds**, and apply them to other query results. This feature enables dynamic visualization configuration based on the data returned by a specific query. - #### Options +#### Options - - **Config query** - Select the query that returns the data you want to use as configuration. - - **Apply to** - Select the fields or series to which the configuration should be applied. - - **Apply to options** - Specify a field type or use a field name regex, depending on your selection in **Apply to**. +- **Config query** - Select the query that returns the data you want to use as configuration. +- **Apply to** - Select the fields or series to which the configuration should be applied. +- **Apply to options** - Specify a field type or use a field name regex, depending on your selection in **Apply to**. - #### Field mapping table +#### Field mapping table - Below the configuration options, you'll find the field mapping table. This table lists all fields found in the data returned by the config query, along with **Use as** and **Select** options. It provides control over mapping fields to config properties, and for multiple rows, it allows you to choose which value to select. +Below the configuration options, you'll find the field mapping table. This table lists all fields found in the data returned by the config query, along with **Use as** and **Select** options. It provides control over mapping fields to config properties, and for multiple rows, it allows you to choose which value to select. - #### Example +#### Example - Input[0] (From query: A, name: ServerA) +Input[0] (From query: A, name: ServerA) - | Time | Value | - | ------------- | ----- | - | 1626178119127 | 10 | - | 1626178119129 | 30 | +| Time | Value | +| ------------- | ----- | +| 1626178119127 | 10 | +| 1626178119129 | 30 | - Input[1] (From query: B) +Input[1] (From query: B) - | Time | Value | - | ------------- | ----- | - | 1626178119127 | 100 | - | 1626178119129 | 100 | +| Time | Value | +| ------------- | ----- | +| 1626178119127 | 100 | +| 1626178119129 | 100 | - Output (Same as Input[0] but now with config on the Value field) +Output (Same as Input[0] but now with config on the Value field) - | Time | Value (config: Max=100) | - | ------------- | ----------------------- | - | 1626178119127 | 10 | - | 1626178119129 | 30 | +| Time | Value (config: Max=100) | +| ------------- | ----------------------- | +| 1626178119127 | 10 | +| 1626178119129 | 30 | - Each row in the source data becomes a separate field. Each field now has a maximum configuration option set. Options such as **Min**, **Max**, **Unit**, and **Thresholds** are part of the field configuration. If set, they are used by the visualization instead of any options manually configured in the panel editor options pane. +Each row in the source data becomes a separate field. Each field now has a maximum configuration option set. Options such as **Min**, **Max**, **Unit**, and **Thresholds** are part of the field configuration. If set, they are used by the visualization instead of any options manually configured in the panel editor options pane. - #### Value mappings +#### Value mappings - You can also transform a query result into value mappings. With this option, every row in the configuration query result defines a single value mapping row. See the following example. +You can also transform a query result into value mappings. With this option, every row in the configuration query result defines a single value mapping row. See the following example. - Config query result: +Config query result: - | Value | Text | Color | - | ----- | ------ | ----- | - | L | Low | blue | - | M | Medium | green | - | H | High | red | +| Value | Text | Color | +| ----- | ------ | ----- | +| L | Low | blue | +| M | Medium | green | +| H | High | red | - In the field mapping specify: +In the field mapping specify: - | Field | Use as | Select | - | ----- | ----------------------- | ---------- | - | Value | Value mappings / Value | All values | - | Text | Value mappings / Text | All values | - | Color | Value mappings / Ciolor | All values | +| Field | Use as | Select | +| ----- | ----------------------- | ---------- | +| Value | Value mappings / Value | All values | +| Text | Value mappings / Text | All values | +| Color | Value mappings / Ciolor | All values | - Grafana builds value mappings from your query result and applies them to the real data query results. You should see values being mapped and colored according to the config query results. +Grafana builds value mappings from your query result and applies them to the real data query results. You should see values being mapped and colored according to the config query results. `; }, }, @@ -187,44 +187,44 @@ export const transformationDocsContent: TransformationDocsContentType = { name: 'Convert field type', getHelperDocs: function () { return ` - Use this transformation to modify the field type of a specified field. - - This transformation has the following options: - - - **Field** - Select from available fields - - **as** - Select the FieldType to convert to - - **Numeric** - attempts to make the values numbers - - **String** - will make the values strings - - **Time** - attempts to parse the values as time - - Will show an option to specify a DateFormat as input by a string like yyyy-mm-dd or DD MM YYYY hh:mm:ss - - **Boolean** - will make the values booleans - - **Enum** - will make the values enums - - Will show a table to manage the enums - - **Other** - attempts to parse the values as JSON - - For example, consider the following query that could be modified by selecting the time field as Time and specifying Date Format as YYYY. - - #### Sample Query - - | Time | Mark | Value | - |------------|-----------|-------| - | 2017-07-01 | above | 25 | - | 2018-08-02 | below | 22 | - | 2019-09-02 | below | 29 | - | 2020-10-04 | above | 22 | - - The result: - - #### Transformed Query - - | Time | Mark | Value | - |---------------------|-----------|-------| - | 2017-01-01 00:00:00 | above | 25 | - | 2018-01-01 00:00:00 | below | 22 | - | 2019-01-01 00:00:00 | below | 29 | - | 2020-01-01 00:00:00 | above | 22 | - - This transformation allows you to flexibly adapt your data types, ensuring compatibility and consistency in your visualizations. +Use this transformation to modify the field type of a specified field. + +This transformation has the following options: + +- **Field** - Select from available fields +- **as** - Select the FieldType to convert to + - **Numeric** - attempts to make the values numbers + - **String** - will make the values strings + - **Time** - attempts to parse the values as time + - Will show an option to specify a DateFormat as input by a string like yyyy-mm-dd or DD MM YYYY hh:mm:ss + - **Boolean** - will make the values booleans + - **Enum** - will make the values enums + - Will show a table to manage the enums + - **Other** - attempts to parse the values as JSON + +For example, consider the following query that could be modified by selecting the time field as Time and specifying Date Format as YYYY. + +#### Sample Query + +| Time | Mark | Value | +|------------|-------|-------| +| 2017-07-01 | above | 25 | +| 2018-08-02 | below | 22 | +| 2019-09-02 | below | 29 | +| 2020-10-04 | above | 22 | + +The result: + +#### Transformed Query + +| Time | Mark | Value | +|---------------------|-------|-------| +| 2017-01-01 00:00:00 | above | 25 | +| 2018-01-01 00:00:00 | below | 22 | +| 2019-01-01 00:00:00 | below | 29 | +| 2020-01-01 00:00:00 | above | 22 | + +This transformation allows you to flexibly adapt your data types, ensuring compatibility and consistency in your visualizations. `; }, }, @@ -232,46 +232,46 @@ export const transformationDocsContent: TransformationDocsContentType = { name: 'Extract fields', getHelperDocs: function () { return ` - Use this transformation to select a source of data and extract content from it in different formats. This transformation has the following fields: +Use this transformation to select a source of data and extract content from it in different formats. This transformation has the following fields: - - **Source** - Select the field for the source of data. - - **Format** - Choose one of the following: - - **JSON** - Parse JSON content from the source. - - **Key+value pairs** - Parse content in the format 'a=b' or 'c:d' from the source. - - **Auto** - Discover fields automatically. - - **Replace All Fields** - (Optional) Select this option to hide all other fields and display only your calculated field in the visualization. - - **Keep Time** - (Optional) Available only if **Replace All Fields** is true. Keeps the time field in the output. +- **Source** - Select the field for the source of data. +- **Format** - Choose one of the following: + - **JSON** - Parse JSON content from the source. + - **Key+value pairs** - Parse content in the format 'a=b' or 'c:d' from the source. + - **Auto** - Discover fields automatically. +- **Replace All Fields** - (Optional) Select this option to hide all other fields and display only your calculated field in the visualization. +- **Keep Time** - (Optional) Available only if **Replace All Fields** is true. Keeps the time field in the output. - Consider the following dataset: +Consider the following dataset: - #### Dataset Example +#### Dataset Example - | Timestamp | json_data | - |-------------------|-----------| - | 1636678740000000000 | {"value": 1} | - | 1636678680000000000 | {"value": 5} | - | 1636678620000000000 | {"value": 12} | +| Timestamp | json_data | +|---------------------|---------------| +| 1636678740000000000 | {"value": 1} | +| 1636678680000000000 | {"value": 5} | +| 1636678620000000000 | {"value": 12} | - You could prepare the data to be used by a [Time series panel][] with this configuration: +You could prepare the data to be used by a [Time series panel][] with this configuration: - - Source: json_data - - Format: JSON - - Field: value - - Alias: my_value - - Replace all fields: true - - Keep time: true +- Source: json_data +- Format: JSON + - Field: value + - Alias: my_value +- Replace all fields: true +- Keep time: true - This will generate the following output: +This will generate the following output: - #### Transformed Data +#### Transformed Data - | Timestamp | my_value | - |-------------------|----------| - | 1636678740000000000 | 1 | - | 1636678680000000000 | 5 | - | 1636678620000000000 | 12 | +| Timestamp | my_value | +|---------------------|----------| +| 1636678740000000000 | 1 | +| 1636678680000000000 | 5 | +| 1636678620000000000 | 12 | - This transformation allows you to extract and format data in various ways. You can customize the extraction format based on your specific data needs. +This transformation allows you to extract and format data in various ways. You can customize the extraction format based on your specific data needs. `; }, links: [ @@ -285,45 +285,45 @@ export const transformationDocsContent: TransformationDocsContentType = { name: 'Lookup fields from resource', getHelperDocs: function () { return ` - Use this transformation to enrich a field value by looking up additional fields from an external source. +Use this transformation to enrich a field value by looking up additional fields from an external source. - This transformation has the following fields: +This transformation has the following fields: - - **Field** - Select a text field from your dataset. - - **Lookup** - Choose from **Countries**, **USA States**, and **Airports**. +- **Field** - Select a text field from your dataset. +- **Lookup** - Choose from **Countries**, **USA States**, and **Airports**. - This transformation currently supports spatial data. +This transformation currently supports spatial data. - For example, if you have this data: +For example, if you have this data: - #### Dataset Example +#### Dataset Example - | Location | Values | - |-----------|--------| - | AL | 0 | - | AK | 10 | - | Arizona | 5 | - | Arkansas | 1 | - | Somewhere | 5 | +| Location | Values | +|-----------|--------| +| AL | 0 | +| AK | 10 | +| Arizona | 5 | +| Arkansas | 1 | +| Somewhere | 5 | - With this configuration: +With this configuration: - - Field: location - - Lookup: USA States +- Field: location +- Lookup: USA States - You'll get the following output: +You'll get the following output: - #### Transformed Data +#### Transformed Data - | Location | ID | Name | Lng | Lat | Values | - |-----------|----|-----------|------------|------------|--------| - | AL | AL | Alabama | -80.891064 | 12.448457 | 0 | - | AK | AK | Arkansas | -100.891064| 24.448457 | 10 | - | Arizona | | | | | 5 | - | Arkansas | | | | | 1 | - | Somewhere | | | | | 5 | +| Location | ID | Name | Lng | Lat | Values | +|-----------|----|----------|-------------|-----------|--------| +| AL | AL | Alabama | -80.891064 | 12.448457 | 0 | +| AK | AK | Arkansas | -100.891064 | 24.448457 | 10 | +| Arizona | | | | | 5 | +| Arkansas | | | | | 1 | +| Somewhere | | | | | 5 | - This transformation lets you augment your data by fetching additional information from external sources, providing a more comprehensive dataset for analysis and visualization. +This transformation lets you augment your data by fetching additional information from external sources, providing a more comprehensive dataset for analysis and visualization. `; }, }, @@ -331,19 +331,19 @@ export const transformationDocsContent: TransformationDocsContentType = { name: 'Filter data by query refId', getHelperDocs: function (imageRenderType: ImageRenderType = ImageRenderType.ShortcodeFigure) { return ` - Use this transformation to hide one or more queries in panels that have multiple queries. +Use this transformation to hide one or more queries in panels that have multiple queries. - Grafana displays the query identification letters in dark gray text. Click a query identifier to toggle filtering. If the query letter is white, then the results are displayed. If the query letter is dark, then the results are hidden. +Grafana displays the query identification letters in dark gray text. Click a query identifier to toggle filtering. If the query letter is white, then the results are displayed. If the query letter is dark, then the results are hidden. - > **Note:** This transformation is not available for Graphite because this data source does not support correlating returned data with queries. +> **Note:** This transformation is not available for Graphite because this data source does not support correlating returned data with queries. - In the example below, the panel has three queries (A, B, C). We removed the B query from the visualization. +In the example below, the panel has three queries (A, B, C). We removed the B query from the visualization. - ${buildImageContent( - '/static/img/docs/transformations/filter-by-query-stat-example-7-0.png', - imageRenderType, - 'A stat visualization with results from two queries, A and C' - )} +${buildImageContent( + '/static/img/docs/transformations/filter-by-query-stat-example-7-0.png', + imageRenderType, + 'A stat visualization with results from two queries, A and C' +)} `; }, }, @@ -351,70 +351,70 @@ export const transformationDocsContent: TransformationDocsContentType = { name: 'Filter data by values', getHelperDocs: function () { return ` - Use this transformation to selectively filter data points directly within your visualization. This transformation provides options to include or exclude data based on one or more conditions applied to a selected field. +Use this transformation to selectively filter data points directly within your visualization. This transformation provides options to include or exclude data based on one or more conditions applied to a selected field. - This transformation is very useful if your data source does not natively filter by values. You might also use this to narrow values to display if you are using a shared query. +This transformation is very useful if your data source does not natively filter by values. You might also use this to narrow values to display if you are using a shared query. - The available conditions for all fields are: +The available conditions for all fields are: - - **Regex** - Match a regex expression. - - **Is Null** - Match if the value is null. - - **Is Not Null** - Match if the value is not null. - - **Equal** - Match if the value is equal to the specified value. - - **Different** - Match if the value is different than the specified value. +- **Regex** - Match a regex expression. +- **Is Null** - Match if the value is null. +- **Is Not Null** - Match if the value is not null. +- **Equal** - Match if the value is equal to the specified value. +- **Different** - Match if the value is different than the specified value. - The available conditions for number fields are: +The available conditions for number fields are: - - **Greater** - Match if the value is greater than the specified value. - - **Lower** - Match if the value is lower than the specified value. - - **Greater or equal** - Match if the value is greater or equal. - - **Lower or equal** - Match if the value is lower or equal. - - **Range** - Match a range between a specified minimum and maximum, min and max included. +- **Greater** - Match if the value is greater than the specified value. +- **Lower** - Match if the value is lower than the specified value. +- **Greater or equal** - Match if the value is greater or equal. +- **Lower or equal** - Match if the value is lower or equal. +- **Range** - Match a range between a specified minimum and maximum, min and max included. - Consider the following dataset: +Consider the following dataset: - #### Dataset Example +#### Dataset Example - | Time | Temperature | Altitude | - |---------------------|-------------|----------| - | 2020-07-07 11:34:23 | 32 | 101 | - | 2020-07-07 11:34:22 | 28 | 125 | - | 2020-07-07 11:34:21 | 26 | 110 | - | 2020-07-07 11:34:20 | 23 | 98 | - | 2020-07-07 10:32:24 | 31 | 95 | - | 2020-07-07 10:31:22 | 20 | 85 | - | 2020-07-07 09:30:57 | 19 | 101 | +| Time | Temperature | Altitude | +|---------------------|-------------|----------| +| 2020-07-07 11:34:23 | 32 | 101 | +| 2020-07-07 11:34:22 | 28 | 125 | +| 2020-07-07 11:34:21 | 26 | 110 | +| 2020-07-07 11:34:20 | 23 | 98 | +| 2020-07-07 10:32:24 | 31 | 95 | +| 2020-07-07 10:31:22 | 20 | 85 | +| 2020-07-07 09:30:57 | 19 | 101 | - If you **Include** the data points that have a temperature below 30°C, the configuration will look as follows: +If you **Include** the data points that have a temperature below 30°C, the configuration will look as follows: - - Filter Type: 'Include' - - Condition: Rows where 'Temperature' matches 'Lower Than' '30' +- Filter Type: 'Include' +- Condition: Rows where 'Temperature' matches 'Lower Than' '30' - And you will get the following result, where only the temperatures below 30°C are included: +And you will get the following result, where only the temperatures below 30°C are included: - #### Transformed Data +#### Transformed Data - | Time | Temperature | Altitude | - |---------------------|-------------|----------| - | 2020-07-07 11:34:22 | 28 | 125 | - | 2020-07-07 11:34:21 | 26 | 110 | - | 2020-07-07 11:34:20 | 23 | 98 | - | 2020-07-07 10:31:22 | 20 | 85 | - | 2020-07-07 09:30:57 | 19 | 101 | +| Time | Temperature | Altitude | +|---------------------|-------------|----------| +| 2020-07-07 11:34:22 | 28 | 125 | +| 2020-07-07 11:34:21 | 26 | 110 | +| 2020-07-07 11:34:20 | 23 | 98 | +| 2020-07-07 10:31:22 | 20 | 85 | +| 2020-07-07 09:30:57 | 19 | 101 | - You can add more than one condition to the filter. For example, you might want to include the data only if the altitude is greater than 100. To do so, add that condition to the following configuration: +You can add more than one condition to the filter. For example, you might want to include the data only if the altitude is greater than 100. To do so, add that condition to the following configuration: - - Filter type: 'Include' rows that 'Match All' conditions - - Condition 1: Rows where 'Temperature' matches 'Lower' than '30' - - Condition 2: Rows where 'Altitude' matches 'Greater' than '100' +- Filter type: 'Include' rows that 'Match All' conditions +- Condition 1: Rows where 'Temperature' matches 'Lower' than '30' +- Condition 2: Rows where 'Altitude' matches 'Greater' than '100' - When you have more than one condition, you can choose if you want the action (include/exclude) to be applied on rows that **Match all** conditions or **Match any** of the conditions you added. +When you have more than one condition, you can choose if you want the action (include/exclude) to be applied on rows that **Match all** conditions or **Match any** of the conditions you added. - In the example above, we chose **Match all** because we wanted to include the rows that have a temperature lower than 30°C *AND* an altitude higher than 100. If we wanted to include the rows that have a temperature lower than 30°C *OR* an altitude higher than 100 instead, then we would select **Match any**. This would include the first row in the original data, which has a temperature of 32°C (does not match the first condition) but an altitude of 101 (which matches the second condition), so it is included. +In the example above, we chose **Match all** because we wanted to include the rows that have a temperature lower than 30°C *AND* an altitude higher than 100. If we wanted to include the rows that have a temperature lower than 30°C *OR* an altitude higher than 100 instead, then we would select **Match any**. This would include the first row in the original data, which has a temperature of 32°C (does not match the first condition) but an altitude of 101 (which matches the second condition), so it is included. - Conditions that are invalid or incompletely configured are ignored. +Conditions that are invalid or incompletely configured are ignored. - This versatile data filtering transformation lets you to selectively include or exclude data points based on specific conditions. Customize the criteria to tailor your data presentation to meet your unique analytical needs. +This versatile data filtering transformation lets you to selectively include or exclude data points based on specific conditions. Customize the criteria to tailor your data presentation to meet your unique analytical needs. `; }, }, @@ -422,63 +422,63 @@ export const transformationDocsContent: TransformationDocsContentType = { name: 'Filter fields by name', getHelperDocs: function (imageRenderType: ImageRenderType = ImageRenderType.ShortcodeFigure) { return ` - Use this transformation to selectively remove parts of your query results. There are three ways to filter field names: - - - [Using a regular expression](#use-a-regular-expression) - - [Manually selecting included fields](#manually-select-included-fields) - - [Using a dashboard variable](#use-a-dashboard-variable) - - #### Use a regular expression - - When you filter using a regular expression, field names that match the regular expression are included. - - For example, from the input data: +Use this transformation to selectively remove parts of your query results. There are three ways to filter field names: - | Time | dev-eu-west | dev-eu-north | prod-eu-west | prod-eu-north | - | ------------------- | ----------- | ------------ | ------------ | ------------- | - | 2023-03-04 23:56:23 | 23.5 | 24.5 | 22.2 | 20.2 | - | 2023-03-04 23:56:23 | 23.6 | 24.4 | 22.1 | 20.1 | - - The result from using the regular expression 'prod.*' would be: - - | Time | prod-eu-west | prod-eu-north | - | ------------------- | ------------ | ------------- | - | 2023-03-04 23:56:23 | 22.2 | 20.2 | - | 2023-03-04 23:56:23 | 22.1 | 20.1 | - - The regular expression can include an interpolated dashboard variable by using the \${${'variableName'}} syntax. - - #### Manually select included fields - - Click and uncheck the field names to remove them from the result. Fields that are matched by the regular expression are still included, even if they're unchecked. - - #### Use a dashboard variable - - Enable 'From variable' to let you select a dashboard variable that's used to include fields. By setting up a [dashboard variable][] with multiple choices, the same fields can be displayed across multiple visualizations. +- [Using a regular expression](#use-a-regular-expression) +- [Manually selecting included fields](#manually-select-included-fields) +- [Using a dashboard variable](#use-a-dashboard-variable) - ${buildImageContent( - '/static/img/docs/transformations/filter-name-table-before-7-0.png', - imageRenderType, - 'A table visualization with time, value, Min, and Max columns' - )} +#### Use a regular expression - Here's the table after we applied the transformation to remove the Min field. +When you filter using a regular expression, field names that match the regular expression are included. - ${buildImageContent( - '/static/img/docs/transformations/filter-name-table-after-7-0.png', - imageRenderType, - 'A table visualization with time, value, and Max columns' - )} +For example, from the input data: - Here is the same query using a Stat visualization. +| Time | dev-eu-west | dev-eu-north | prod-eu-west | prod-eu-north | +| ------------------- | ----------- | ------------ | ------------ | ------------- | +| 2023-03-04 23:56:23 | 23.5 | 24.5 | 22.2 | 20.2 | +| 2023-03-04 23:56:23 | 23.6 | 24.4 | 22.1 | 20.1 | - ${buildImageContent( - '/static/img/docs/transformations/filter-name-stat-after-7-0.png', - imageRenderType, - 'A stat visualization with value and Max fields' - )} +The result from using the regular expression 'prod.*' would be: - This transformation provides flexibility in tailoring your query results to focus on the specific fields you need for effective analysis and visualization. +| Time | prod-eu-west | prod-eu-north | +| ------------------- | ------------ | ------------- | +| 2023-03-04 23:56:23 | 22.2 | 20.2 | +| 2023-03-04 23:56:23 | 22.1 | 20.1 | + +The regular expression can include an interpolated dashboard variable by using the \${${'variableName'}} syntax. + +#### Manually select included fields + +Click and uncheck the field names to remove them from the result. Fields that are matched by the regular expression are still included, even if they're unchecked. + +#### Use a dashboard variable + +Enable 'From variable' to let you select a dashboard variable that's used to include fields. By setting up a [dashboard variable][] with multiple choices, the same fields can be displayed across multiple visualizations. + +${buildImageContent( + '/static/img/docs/transformations/filter-name-table-before-7-0.png', + imageRenderType, + 'A table visualization with time, value, Min, and Max columns' +)} + +Here's the table after we applied the transformation to remove the Min field. + +${buildImageContent( + '/static/img/docs/transformations/filter-name-table-after-7-0.png', + imageRenderType, + 'A table visualization with time, value, and Max columns' +)} + +Here is the same query using a Stat visualization. + +${buildImageContent( + '/static/img/docs/transformations/filter-name-stat-after-7-0.png', + imageRenderType, + 'A stat visualization with value and Max fields' +)} + +This transformation provides flexibility in tailoring your query results to focus on the specific fields you need for effective analysis and visualization. `; }, }, @@ -486,49 +486,49 @@ export const transformationDocsContent: TransformationDocsContentType = { name: 'Format string', getHelperDocs: function () { return ` - Use this transformation to customize the output of a string field. This transformation has the following fields: - - - **Upper case** - Formats the entire string in uppercase characters. - - **Lower case** - Formats the entire string in lowercase characters. - - **Sentence case** - Formats the first character of the string in uppercase. - - **Title case** - Formats the first character of each word in the string in uppercase. - - **Pascal case** - Formats the first character of each word in the string in uppercase and doesn't include spaces between words. - - **Camel case** - Formats the first character of each word in the string in uppercase, except the first word, and doesn't include spaces between words. - - **Snake case** - Formats all characters in the string in lowercase and uses underscores instead of spaces between words. - - **Kebab case** - Formats all characters in the string in lowercase and uses dashes instead of spaces between words. - - **Trim** - Removes all leading and trailing spaces from the string. - - **Substring** - Returns a substring of the string, using the specified start and end positions. - - This transformation provides a convenient way to standardize and tailor the presentation of string data for better visualization and analysis. - - > **Note:** This transformation is currently in public preview. Grafana Labs offers limited support, and breaking changes might occur prior to the feature being made generally available. Enable the \`formatString\` feature toggle in Grafana to use this feature. Contact Grafana Support to enable this feature in Grafana Cloud.`; +Use this transformation to customize the output of a string field. This transformation has the following fields: + +- **Upper case** - Formats the entire string in uppercase characters. +- **Lower case** - Formats the entire string in lowercase characters. +- **Sentence case** - Formats the first character of the string in uppercase. +- **Title case** - Formats the first character of each word in the string in uppercase. +- **Pascal case** - Formats the first character of each word in the string in uppercase and doesn't include spaces between words. +- **Camel case** - Formats the first character of each word in the string in uppercase, except the first word, and doesn't include spaces between words. +- **Snake case** - Formats all characters in the string in lowercase and uses underscores instead of spaces between words. +- **Kebab case** - Formats all characters in the string in lowercase and uses dashes instead of spaces between words. +- **Trim** - Removes all leading and trailing spaces from the string. +- **Substring** - Returns a substring of the string, using the specified start and end positions. + +This transformation provides a convenient way to standardize and tailor the presentation of string data for better visualization and analysis. + +> **Note:** This transformation is currently in public preview. Grafana Labs offers limited support, and breaking changes might occur prior to the feature being made generally available. Enable the \`formatString\` feature toggle in Grafana to use this feature. Contact Grafana Support to enable this feature in Grafana Cloud.`; }, }, formatTime: { name: 'Format time', getHelperDocs: function () { return ` - Use this transformation to customize the output of a time field. Output can be formatted using [Moment.js format strings](https://momentjs.com/docs/#/displaying/). For example, if you want to display only the year of a time field, the format string 'YYYY' can be used to show the calendar year (for example, 1999 or 2012). +Use this transformation to customize the output of a time field. Output can be formatted using [Moment.js format strings](https://momentjs.com/docs/#/displaying/). For example, if you want to display only the year of a time field, the format string 'YYYY' can be used to show the calendar year (for example, 1999 or 2012). - **Before Transformation:** +**Before Transformation:** - | Timestamp | Event | - | ------------------- | -------------- | - | 1636678740000000000 | System Start | - | 1636678680000000000 | User Login | - | 1636678620000000000 | Data Updated | +| Timestamp | Event | +| ------------------- | ------------ | +| 1636678740000000000 | System Start | +| 1636678680000000000 | User Login | +| 1636678620000000000 | Data Updated | - **After applying 'YYYY-MM-DD HH:mm:ss':** +**After applying 'YYYY-MM-DD HH:mm:ss':** - | Timestamp | Event | - | ------------------- | -------------- | - | 2021-11-12 14:25:40 | System Start | - | 2021-11-12 14:24:40 | User Login | - | 2021-11-12 14:23:40 | Data Updated | +| Timestamp | Event | +| ------------------- | ------------ | +| 2021-11-12 14:25:40 | System Start | +| 2021-11-12 14:24:40 | User Login | +| 2021-11-12 14:23:40 | Data Updated | - This transformation lets you tailor the time representation in your visualizations, providing flexibility and precision in displaying temporal data. +This transformation lets you tailor the time representation in your visualizations, providing flexibility and precision in displaying temporal data. - > **Note:** This transformation is available in Grafana 10.1+ as an alpha feature. +> **Note:** This transformation is available in Grafana 10.1+ as an alpha feature. `; }, }, @@ -536,61 +536,61 @@ export const transformationDocsContent: TransformationDocsContentType = { name: 'Group by', getHelperDocs: function () { return ` - Use this transformation to group the data by a specified field (column) value and process calculations on each group. Click to see a list of calculation choices. For information about available calculations, refer to [Calculation types][]. - - Here's an example of original data. - - | Time | Server ID | CPU Temperature | Server Status | - | ------------------- | --------- | --------------- | ------------- | - | 2020-07-07 11:34:20 | server 1 | 80 | Shutdown | - | 2020-07-07 11:34:20 | server 3 | 62 | OK | - | 2020-07-07 10:32:20 | server 2 | 90 | Overload | - | 2020-07-07 10:31:22 | server 3 | 55 | OK | - | 2020-07-07 09:30:57 | server 3 | 62 | Rebooting | - | 2020-07-07 09:30:05 | server 2 | 88 | OK | - | 2020-07-07 09:28:06 | server 1 | 80 | OK | - | 2020-07-07 09:25:05 | server 2 | 88 | OK | - | 2020-07-07 09:23:07 | server 1 | 86 | OK | - - This transformation goes in two steps. First you specify one or multiple fields to group the data by. This will group all the same values of those fields together, as if you sorted them. For instance if we group by the Server ID field, then it would group the data this way: - - | Time | Server ID | CPU Temperature | Server Status | - | ------------------- | -------------- | --------------- | ------------- | - | 2020-07-07 11:34:20 | **server 1** | 80 | Shutdown | - | 2020-07-07 09:28:06 | **server 1** | 80 | OK | - | 2020-07-07 09:23:07 | **server 1** | 86 | OK | - | 2020-07-07 10:32:20 | server 2 | 90 | Overload | - | 2020-07-07 09:30:05 | server 2 | 88 | OK | - | 2020-07-07 09:25:05 | server 2 | 88 | OK | - | 2020-07-07 11:34:20 | **_server 3_** | 62 | OK | - | 2020-07-07 10:31:22 | **_server 3_** | 55 | OK | - | 2020-07-07 09:30:57 | **_server 3_** | 62 | Rebooting | - - All rows with the same value of Server ID are grouped together. - - After choosing which field you want to group your data by, you can add various calculations on the other fields, and apply the calculation to each group of rows. For instance, we could want to calculate the average CPU temperature for each of those servers. So we can add the _mean_ calculation applied on the CPU Temperature field to get the following: - - | Server ID | CPU Temperature (mean) | - | --------- | ---------------------- | - | server 1 | 82 | - | server 2 | 88.6 | - | server 3 | 59.6 | - - And we can add more than one calculation. For instance: - - - For field Time, we can calculate the _Last_ value, to know when the last data point was received for each server - - For field Server Status, we can calculate the _Last_ value to know what is the last state value for each server - - For field Temperature, we can also calculate the _Last_ value to know what is the latest monitored temperature for each server - - We would then get: - - | Server ID | CPU Temperature (mean) | CPU Temperature (last) | Time (last) | Server Status (last) | - | --------- | ---------------------- | ---------------------- | ------------------- | -------------------- | - | server 1 | 82 | 80 | 2020-07-07 11:34:20 | Shutdown | - | server 2 | 88.6 | 90 | 2020-07-07 10:32:20 | Overload | - | server 3 | 59.6 | 62 | 2020-07-07 11:34:20 | OK | - - This transformation allows you to extract essential information from your time series and present it conveniently. +Use this transformation to group the data by a specified field (column) value and process calculations on each group. Click to see a list of calculation choices. For information about available calculations, refer to [Calculation types][]. + +Here's an example of original data. + +| Time | Server ID | CPU Temperature | Server Status | +| ------------------- | --------- | --------------- | ------------- | +| 2020-07-07 11:34:20 | server 1 | 80 | Shutdown | +| 2020-07-07 11:34:20 | server 3 | 62 | OK | +| 2020-07-07 10:32:20 | server 2 | 90 | Overload | +| 2020-07-07 10:31:22 | server 3 | 55 | OK | +| 2020-07-07 09:30:57 | server 3 | 62 | Rebooting | +| 2020-07-07 09:30:05 | server 2 | 88 | OK | +| 2020-07-07 09:28:06 | server 1 | 80 | OK | +| 2020-07-07 09:25:05 | server 2 | 88 | OK | +| 2020-07-07 09:23:07 | server 1 | 86 | OK | + +This transformation goes in two steps. First you specify one or multiple fields to group the data by. This will group all the same values of those fields together, as if you sorted them. For instance if we group by the Server ID field, then it would group the data this way: + +| Time | Server ID | CPU Temperature | Server Status | +| ------------------- | -------------- | --------------- | ------------- | +| 2020-07-07 11:34:20 | **server 1** | 80 | Shutdown | +| 2020-07-07 09:28:06 | **server 1** | 80 | OK | +| 2020-07-07 09:23:07 | **server 1** | 86 | OK | +| 2020-07-07 10:32:20 | server 2 | 90 | Overload | +| 2020-07-07 09:30:05 | server 2 | 88 | OK | +| 2020-07-07 09:25:05 | server 2 | 88 | OK | +| 2020-07-07 11:34:20 | **_server 3_** | 62 | OK | +| 2020-07-07 10:31:22 | **_server 3_** | 55 | OK | +| 2020-07-07 09:30:57 | **_server 3_** | 62 | Rebooting | + +All rows with the same value of Server ID are grouped together. + +After choosing which field you want to group your data by, you can add various calculations on the other fields, and apply the calculation to each group of rows. For instance, we could want to calculate the average CPU temperature for each of those servers. So we can add the _mean_ calculation applied on the CPU Temperature field to get the following: + +| Server ID | CPU Temperature (mean) | +| --------- | ---------------------- | +| server 1 | 82 | +| server 2 | 88.6 | +| server 3 | 59.6 | + +And we can add more than one calculation. For instance: + +- For field Time, we can calculate the _Last_ value, to know when the last data point was received for each server +- For field Server Status, we can calculate the _Last_ value to know what is the last state value for each server +- For field Temperature, we can also calculate the _Last_ value to know what is the latest monitored temperature for each server + +We would then get: + +| Server ID | CPU Temperature (mean) | CPU Temperature (last) | Time (last) | Server Status (last) | +| --------- | ---------------------- | ---------------------- | ------------------- | -------------------- | +| server 1 | 82 | 80 | 2020-07-07 11:34:20 | Shutdown | +| server 2 | 88.6 | 90 | 2020-07-07 10:32:20 | Overload | +| server 3 | 59.6 | 62 | 2020-07-07 11:34:20 | OK | + +This transformation allows you to extract essential information from your time series and present it conveniently. `; }, links: [ @@ -604,27 +604,27 @@ export const transformationDocsContent: TransformationDocsContentType = { name: 'Grouping to matrix', getHelperDocs: function () { return ` - Use this transformation to combine three fields—which are used as input for the **Column**, **Row**, and **Cell value** fields from the query output—and generate a matrix. The matrix is calculated as follows: +Use this transformation to combine three fields—which are used as input for the **Column**, **Row**, and **Cell value** fields from the query output—and generate a matrix. The matrix is calculated as follows: - **Original data** +**Original data** - | Server ID | CPU Temperature | Server Status | - | --------- | --------------- | ------------- | - | server 1 | 82 | OK | - | server 2 | 88.6 | OK | - | server 3 | 59.6 | Shutdown | +| Server ID | CPU Temperature | Server Status | +| --------- | --------------- | ------------- | +| server 1 | 82 | OK | +| server 2 | 88.6 | OK | +| server 3 | 59.6 | Shutdown | - We can generate a matrix using the values of 'Server Status' as column names, the 'Server ID' values as row names, and the 'CPU Temperature' as content of each cell. The content of each cell will appear for the existing column ('Server Status') and row combination ('Server ID'). For the rest of the cells, you can select which value to display between: **Null**, **True**, **False**, or **Empty**. +We can generate a matrix using the values of 'Server Status' as column names, the 'Server ID' values as row names, and the 'CPU Temperature' as content of each cell. The content of each cell will appear for the existing column ('Server Status') and row combination ('Server ID'). For the rest of the cells, you can select which value to display between: **Null**, **True**, **False**, or **Empty**. - **Output** +**Output** - | Server ID\Server Status | OK | Shutdown | - | ----------------------- | ---- | -------- | - | server 1 | 82 | | - | server 2 | 88.6 | | - | server 3 | | 59.6 | +| Server ID\Server Status | OK | Shutdown | +| ----------------------- | ---- | -------- | +| server 1 | 82 | | +| server 2 | 88.6 | | +| server 3 | | 59.6 | - Use this transformation to construct a matrix by specifying fields from your query results. The matrix output reflects the relationships between the unique values in these fields. This helps you present complex relationships in a clear and structured matrix format. +Use this transformation to construct a matrix by specifying fields from your query results. The matrix output reflects the relationships between the unique values in these fields. This helps you present complex relationships in a clear and structured matrix format. `; }, }, @@ -632,34 +632,34 @@ export const transformationDocsContent: TransformationDocsContentType = { name: 'Create heatmap', getHelperDocs: function () { return ` - Use this transformation to prepare histogram data for visualizing trends over time. Similar to the heatmap visualization, this transformation converts histogram metrics into temporal buckets. +Use this transformation to prepare histogram data for visualizing trends over time. Similar to the heatmap visualization, this transformation converts histogram metrics into temporal buckets. - #### X Bucket +#### X Bucket - This setting determines how the x-axis is split into buckets. +This setting determines how the x-axis is split into buckets. - - **Size** - Specify a time interval in the input field. For example, a time range of '1h' creates cells one hour wide on the x-axis. - - **Count** - For non-time-related series, use this option to define the number of elements in a bucket. +- **Size** - Specify a time interval in the input field. For example, a time range of '1h' creates cells one hour wide on the x-axis. +- **Count** - For non-time-related series, use this option to define the number of elements in a bucket. - #### Y Bucket +#### Y Bucket - This setting determines how the y-axis is split into buckets. +This setting determines how the y-axis is split into buckets. - - **Linear** - - **Logarithmic** - Choose between log base 2 or log base 10. - - **Symlog** - Uses a symmetrical logarithmic scale. Choose between log base 2 or log base 10, allowing for negative values. +- **Linear** +- **Logarithmic** - Choose between log base 2 or log base 10. +- **Symlog** - Uses a symmetrical logarithmic scale. Choose between log base 2 or log base 10, allowing for negative values. - Assume you have the following dataset: +Assume you have the following dataset: - | Timestamp | Value | - |-------------------- |-------| - | 2023-01-01 12:00:00 | 5 | - | 2023-01-01 12:15:00 | 10 | - | 2023-01-01 12:30:00 | 15 | - | 2023-01-01 12:45:00 | 8 | +| Timestamp | Value | +|-------------------- |-------| +| 2023-01-01 12:00:00 | 5 | +| 2023-01-01 12:15:00 | 10 | +| 2023-01-01 12:30:00 | 15 | +| 2023-01-01 12:45:00 | 8 | - - With X Bucket set to 'Size: 15m' and Y Bucket as 'Linear', the histogram organizes values into time intervals of 15 minutes on the x-axis and linearly on the y-axis. - - For X Bucket as 'Count: 2' and Y Bucket as 'Logarithmic (base 10)', the histogram groups values into buckets of two on the x-axis and use a logarithmic scale on the y-axis. +- With X Bucket set to 'Size: 15m' and Y Bucket as 'Linear', the histogram organizes values into time intervals of 15 minutes on the x-axis and linearly on the y-axis. +- For X Bucket as 'Count: 2' and Y Bucket as 'Logarithmic (base 10)', the histogram groups values into buckets of two on the x-axis and use a logarithmic scale on the y-axis. `; }, }, @@ -667,49 +667,49 @@ export const transformationDocsContent: TransformationDocsContentType = { name: 'Histogram', getHelperDocs: function () { return ` - Use this transformation to generate a histogram based on input data, allowing you to visualize the distribution of values. - - - **Bucket size** - The range between the lowest and highest items in a bucket (xMin to xMax). - - **Bucket offset** - The offset for non-zero-based buckets. - - **Combine series** - Create a unified histogram using all available series. - - **Original data** - - Series 1: - - | A | B | C | - | --- | --- | --- | - | 1 | 3 | 5 | - | 2 | 4 | 6 | - | 3 | 5 | 7 | - | 4 | 6 | 8 | - | 5 | 7 | 9 | - - Series 2: - - | C | - | --- | - | 5 | - | 6 | - | 7 | - | 8 | - | 9 | - - **Output** - - | xMin | xMax | A | B | C | C | - | ---- | ---- | --- | --- | --- | --- | - | 1 | 2 | 1 | 0 | 0 | 0 | - | 2 | 3 | 1 | 0 | 0 | 0 | - | 3 | 4 | 1 | 1 | 0 | 0 | - | 4 | 5 | 1 | 1 | 0 | 0 | - | 5 | 6 | 1 | 1 | 1 | 1 | - | 6 | 7 | 0 | 1 | 1 | 1 | - | 7 | 8 | 0 | 1 | 1 | 1 | - | 8 | 9 | 0 | 0 | 1 | 1 | - | 9 | 10 | 0 | 0 | 1 | 1 | - - Visualize the distribution of values using the generated histogram, providing insights into the data's spread and density. +Use this transformation to generate a histogram based on input data, allowing you to visualize the distribution of values. + +- **Bucket size** - The range between the lowest and highest items in a bucket (xMin to xMax). +- **Bucket offset** - The offset for non-zero-based buckets. +- **Combine series** - Create a unified histogram using all available series. + +**Original data** + +Series 1: + +| A | B | C | +| - | - | - | +| 1 | 3 | 5 | +| 2 | 4 | 6 | +| 3 | 5 | 7 | +| 4 | 6 | 8 | +| 5 | 7 | 9 | + +Series 2: + +| C | +| - | +| 5 | +| 6 | +| 7 | +| 8 | +| 9 | + +**Output** + +| xMin | xMax | A | B | C | C | +| ---- | ---- | --| --| --| --| +| 1 | 2 | 1 | 0 | 0 | 0 | +| 2 | 3 | 1 | 0 | 0 | 0 | +| 3 | 4 | 1 | 1 | 0 | 0 | +| 4 | 5 | 1 | 1 | 0 | 0 | +| 5 | 6 | 1 | 1 | 1 | 1 | +| 6 | 7 | 0 | 1 | 1 | 1 | +| 7 | 8 | 0 | 1 | 1 | 1 | +| 8 | 9 | 0 | 0 | 1 | 1 | +| 9 | 10 | 0 | 0 | 1 | 1 | + +Visualize the distribution of values using the generated histogram, providing insights into the data's spread and density. `; }, }, @@ -717,89 +717,89 @@ export const transformationDocsContent: TransformationDocsContentType = { name: 'Join by field', getHelperDocs: function (imageRenderType: ImageRenderType = ImageRenderType.ShortcodeFigure) { return ` - Use this transformation to merge multiple results into a single table, enabling the consolidation of data from different queries. +Use this transformation to merge multiple results into a single table, enabling the consolidation of data from different queries. - This is especially useful for converting multiple time series results into a single wide table with a shared time field. +This is especially useful for converting multiple time series results into a single wide table with a shared time field. - #### Inner join +#### Inner join - An inner join merges data from multiple tables where all tables share the same value from the selected field. This type of join excludes data where values do not match in every result. +An inner join merges data from multiple tables where all tables share the same value from the selected field. This type of join excludes data where values do not match in every result. - Use this transformation to combine the results from multiple queries (combining on a passed join field or the first time column) into one result, and drop rows where a successful join cannot occur. +Use this transformation to combine the results from multiple queries (combining on a passed join field or the first time column) into one result, and drop rows where a successful join cannot occur. - In the following example, two queries return table data. It is visualized as two separate tables before applying the inner join transformation. +In the following example, two queries return table data. It is visualized as two separate tables before applying the inner join transformation. - **Query A:** +**Query A:** - | Time | Job | Uptime | - | ------------------- | ------- | --------- | - | 2020-07-07 11:34:20 | node | 25260122 | - | 2020-07-07 11:24:20 | postgre | 123001233 | - | 2020-07-07 11:14:20 | postgre | 345001233 | +| Time | Job | Uptime | +| ------------------- | ------- | --------- | +| 2020-07-07 11:34:20 | node | 25260122 | +| 2020-07-07 11:24:20 | postgre | 123001233 | +| 2020-07-07 11:14:20 | postgre | 345001233 | - **Query B:** +**Query B:** - | Time | Server | Errors | - | ------------------- | -------- | ------ | - | 2020-07-07 11:34:20 | server 1 | 15 | - | 2020-07-07 11:24:20 | server 2 | 5 | - | 2020-07-07 11:04:20 | server 3 | 10 | +| Time | Server | Errors | +| ------------------- | -------- | ------ | +| 2020-07-07 11:34:20 | server 1 | 15 | +| 2020-07-07 11:24:20 | server 2 | 5 | +| 2020-07-07 11:04:20 | server 3 | 10 | - The result after applying the inner join transformation looks like the following: +The result after applying the inner join transformation looks like the following: - | Time | Job | Uptime | Server | Errors | - | ------------------- | ------- | --------- | -------- | ------ | - | 2020-07-07 11:34:20 | node | 25260122 | server 1 | 15 | - | 2020-07-07 11:24:20 | postgre | 123001233 | server 2 | 5 | +| Time | Job | Uptime | Server | Errors | +| ------------------- | ------- | --------- | -------- | ------ | +| 2020-07-07 11:34:20 | node | 25260122 | server 1 | 15 | +| 2020-07-07 11:24:20 | postgre | 123001233 | server 2 | 5 | - #### Outer join +#### Outer join - An outer join includes all data from an inner join and rows where values do not match in every input. While the inner join joins Query A and Query B on the time field, the outer join includes all rows that don't match on the time field. +An outer join includes all data from an inner join and rows where values do not match in every input. While the inner join joins Query A and Query B on the time field, the outer join includes all rows that don't match on the time field. - In the following example, two queries return table data. It is visualized as two tables before applying the outer join transformation. +In the following example, two queries return table data. It is visualized as two tables before applying the outer join transformation. - **Query A:** +**Query A:** - | Time | Job | Uptime | - | ------------------- | ------- | --------- | - | 2020-07-07 11:34:20 | node | 25260122 | - | 2020-07-07 11:24:20 | postgre | 123001233 | - | 2020-07-07 11:14:20 | postgre | 345001233 | +| Time | Job | Uptime | +| ------------------- | ------- | --------- | +| 2020-07-07 11:34:20 | node | 25260122 | +| 2020-07-07 11:24:20 | postgre | 123001233 | +| 2020-07-07 11:14:20 | postgre | 345001233 | - **Query B:** +**Query B:** - | Time | Server | Errors | - | ------------------- | -------- | ------ | - | 2020-07-07 11:34:20 | server 1 | 15 | - | 2020-07-07 11:24:20 | server 2 | 5 | - | 2020-07-07 11:04:20 | server 3 | 10 | +| Time | Server | Errors | +| ------------------- | -------- | ------ | +| 2020-07-07 11:34:20 | server 1 | 15 | +| 2020-07-07 11:24:20 | server 2 | 5 | +| 2020-07-07 11:04:20 | server 3 | 10 | - The result after applying the outer join transformation looks like the following: +The result after applying the outer join transformation looks like the following: - | Time | Job | Uptime | Server | Errors | - | ------------------- | ------- | --------- | -------- | ------ | - | 2020-07-07 11:04:20 | | | server 3 | 10 | - | 2020-07-07 11:14:20 | postgre | 345001233 | | | - | 2020-07-07 11:34:20 | node | 25260122 | server 1 | 15 | - | 2020-07-07 11:24:20 | postgre | 123001233 | server 2 | 5 | +| Time | Job | Uptime | Server | Errors | +| ------------------- | ------- | --------- | -------- | ------ | +| 2020-07-07 11:04:20 | | | server 3 | 10 | +| 2020-07-07 11:14:20 | postgre | 345001233 | | | +| 2020-07-07 11:34:20 | node | 25260122 | server 1 | 15 | +| 2020-07-07 11:24:20 | postgre | 123001233 | server 2 | 5 | - In the following example, a template query displays time series data from multiple servers in a table visualization. The results of only one query can be viewed at a time. +In the following example, a template query displays time series data from multiple servers in a table visualization. The results of only one query can be viewed at a time. - ${buildImageContent( - '/static/img/docs/transformations/join-fields-before-7-0.png', - imageRenderType, - 'A table visualization showing results for one server' - )} +${buildImageContent( + '/static/img/docs/transformations/join-fields-before-7-0.png', + imageRenderType, + 'A table visualization showing results for one server' +)} - I applied a transformation to join the query results using the time field. Now I can run calculations, combine, and organize the results in this new table. +I applied a transformation to join the query results using the time field. Now I can run calculations, combine, and organize the results in this new table. - ${buildImageContent( - '/static/img/docs/transformations/join-fields-after-7-0.png', - imageRenderType, - 'A table visualization showing results for multiple servers' - )} +${buildImageContent( + '/static/img/docs/transformations/join-fields-after-7-0.png', + imageRenderType, + 'A table visualization showing results for multiple servers' +)} - Combine and analyze data from various queries with table joining for a comprehensive view of your information. +Combine and analyze data from various queries with table joining for a comprehensive view of your information. `; }, }, @@ -807,52 +807,52 @@ export const transformationDocsContent: TransformationDocsContentType = { name: 'Join by labels', getHelperDocs: function () { return ` - Use this transformation to join multiple results into a single table. - - This is especially useful for converting multiple time series results into a single wide table with a shared **Label** field. +Use this transformation to join multiple results into a single table. - - **Join** - Select the label to join by between the labels available or common across all time series. - - **Value** - The name for the output result. +This is especially useful for converting multiple time series results into a single wide table with a shared **Label** field. - #### Example +- **Join** - Select the label to join by between the labels available or common across all time series. +- **Value** - The name for the output result. - ##### Input +#### Example - series1{what="Temp", cluster="A", job="J1"} +##### Input - | Time | Value | - | ---- | ----- | - | 1 | 10 | - | 2 | 200 | +series1{what="Temp", cluster="A", job="J1"} - series2{what="Temp", cluster="B", job="J1"} +| Time | Value | +| ---- | ----- | +| 1 | 10 | +| 2 | 200 | - | Time | Value | - | ---- | ----- | - | 1 | 10 | - | 2 | 200 | +series2{what="Temp", cluster="B", job="J1"} - series3{what="Speed", cluster="B", job="J1"} +| Time | Value | +| ---- | ----- | +| 1 | 10 | +| 2 | 200 | - | Time | Value | - | ---- | ----- | - | 22 | 22 | - | 28 | 77 | +series3{what="Speed", cluster="B", job="J1"} - ##### Config +| Time | Value | +| ---- | ----- | +| 22 | 22 | +| 28 | 77 | - value: "what" +##### Config - ##### Output +value: "what" - | cluster | job | Temp | Speed | - | ------- | --- | ---- | ----- | - | A | J1 | 10 | | - | A | J1 | 200 | | - | B | J1 | 10 | 22 | - | B | J1 | 200 | 77 | +##### Output - Combine and organize time series data effectively with this transformation for comprehensive insights. +| cluster | job | Temp | Speed | +| ------- | --- | ---- | ----- | +| A | J1 | 10 | | +| A | J1 | 200 | | +| B | J1 | 10 | 22 | +| B | J1 | 200 | 77 | + +Combine and organize time series data effectively with this transformation for comprehensive insights. `; }, }, @@ -860,67 +860,67 @@ export const transformationDocsContent: TransformationDocsContentType = { name: 'Labels to fields', getHelperDocs: function () { return ` - Use this transformation to convert time series results with labels or tags into a table, including each label's keys and values in the result. Display labels as either columns or row values for enhanced data visualization. +Use this transformation to convert time series results with labels or tags into a table, including each label's keys and values in the result. Display labels as either columns or row values for enhanced data visualization. - Given a query result of two time series: +Given a query result of two time series: - - Series 1: labels Server=Server A, Datacenter=EU - - Series 2: labels Server=Server B, Datacenter=EU +- Series 1: labels Server=Server A, Datacenter=EU +- Series 2: labels Server=Server B, Datacenter=EU - In "Columns" mode, the result looks like this: +In "Columns" mode, the result looks like this: - | Time | Server | Datacenter | Value | - | ------------------- | -------- | ---------- | ----- | - | 2020-07-07 11:34:20 | Server A | EU | 1 | - | 2020-07-07 11:34:20 | Server B | EU | 2 | +| Time | Server | Datacenter | Value | +| ------------------- | -------- | ---------- | ----- | +| 2020-07-07 11:34:20 | Server A | EU | 1 | +| 2020-07-07 11:34:20 | Server B | EU | 2 | - In "Rows" mode, the result has a table for each series and show each label value like this: +In "Rows" mode, the result has a table for each series and show each label value like this: - | label | value | - | ---------- | -------- | - | Server | Server A | - | Datacenter | EU | +| label | value | +| ---------- | -------- | +| Server | Server A | +| Datacenter | EU | - | label | value | - | ---------- | -------- | - | Server | Server B | - | Datacenter | EU | +| label | value | +| ---------- | -------- | +| Server | Server B | +| Datacenter | EU | - #### Value field name +#### Value field name - If you selected Server as the **Value field name**, then you would get one field for every value of the Server label. +If you selected Server as the **Value field name**, then you would get one field for every value of the Server label. - | Time | Datacenter | Server A | Server B | - | ------------------- | ---------- | -------- | -------- | - | 2020-07-07 11:34:20 | EU | 1 | 2 | +| Time | Datacenter | Server A | Server B | +| ------------------- | ---------- | -------- | -------- | +| 2020-07-07 11:34:20 | EU | 1 | 2 | - #### Merging behavior +#### Merging behavior - The labels to fields transformer is internally two separate transformations. The first acts on single series and extracts labels to fields. The second is the [merge](#merge) transformation that joins all the results into a single table. The merge transformation tries to join on all matching fields. This merge step is required and cannot be turned off. +The labels to fields transformer is internally two separate transformations. The first acts on single series and extracts labels to fields. The second is the [merge](#merge) transformation that joins all the results into a single table. The merge transformation tries to join on all matching fields. This merge step is required and cannot be turned off. - To illustrate this, here is an example where you have two queries that return time series with no overlapping labels. +To illustrate this, here is an example where you have two queries that return time series with no overlapping labels. - - Series 1: labels Server=ServerA - - Series 2: labels Datacenter=EU +- Series 1: labels Server=ServerA +- Series 2: labels Datacenter=EU - This will first result in these two tables: +This will first result in these two tables: - | Time | Server | Value | - | ------------------- | ------- | ----- | - | 2020-07-07 11:34:20 | ServerA | 10 | +| Time | Server | Value | +| ------------------- | ------- | ----- | +| 2020-07-07 11:34:20 | ServerA | 10 | - | Time | Datacenter | Value | - | ------------------- | ---------- | ----- | - | 2020-07-07 11:34:20 | EU | 20 | +| Time | Datacenter | Value | +| ------------------- | ---------- | ----- | +| 2020-07-07 11:34:20 | EU | 20 | - After merge: +After merge: - | Time | Server | Value | Datacenter | - | ------------------- | ------- | ----- | ---------- | - | 2020-07-07 11:34:20 | ServerA | 10 | | - | 2020-07-07 11:34:20 | | 20 | EU | +| Time | Server | Value | Datacenter | +| ------------------- | ------- | ----- | ---------- | +| 2020-07-07 11:34:20 | ServerA | 10 | | +| 2020-07-07 11:34:20 | | 20 | EU | - Convert your time series data into a structured table format for a clearer and more organized representation. +Convert your time series data into a structured table format for a clearer and more organized representation. `; }, }, @@ -928,28 +928,28 @@ export const transformationDocsContent: TransformationDocsContentType = { name: 'Limit', getHelperDocs: function () { return ` - Use this transformation to restrict the number of rows displayed, providing a more focused view of your data. This is particularly useful when dealing with large datasets. +Use this transformation to restrict the number of rows displayed, providing a more focused view of your data. This is particularly useful when dealing with large datasets. - Below is an example illustrating the impact of the **Limit** transformation on a response from a data source: +Below is an example illustrating the impact of the **Limit** transformation on a response from a data source: - | Time | Metric | Value | - | ------------------- | ----------- | ----- | - | 2020-07-07 11:34:20 | Temperature | 25 | - | 2020-07-07 11:34:20 | Humidity | 22 | - | 2020-07-07 10:32:20 | Humidity | 29 | - | 2020-07-07 10:31:22 | Temperature | 22 | - | 2020-07-07 09:30:57 | Humidity | 33 | - | 2020-07-07 09:30:05 | Temperature | 19 | +| Time | Metric | Value | +| ------------------- | ----------- | ----- | +| 2020-07-07 11:34:20 | Temperature | 25 | +| 2020-07-07 11:34:20 | Humidity | 22 | +| 2020-07-07 10:32:20 | Humidity | 29 | +| 2020-07-07 10:31:22 | Temperature | 22 | +| 2020-07-07 09:30:57 | Humidity | 33 | +| 2020-07-07 09:30:05 | Temperature | 19 | - Here is the result after adding a Limit transformation with a value of '3': +Here is the result after adding a Limit transformation with a value of '3': - | Time | Metric | Value | - | ------------------- | ----------- | ----- | - | 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 | Metric | Value | +| ------------------- | ----------- | ----- | +| 2020-07-07 11:34:20 | Temperature | 25 | +| 2020-07-07 11:34:20 | Humidity | 22 | +| 2020-07-07 10:32:20 | Humidity | 29 | - This transformation helps you tailor the visual presentation of your data to focus on the most relevant information. +This transformation helps you tailor the visual presentation of your data to focus on the most relevant information. `; }, }, @@ -957,32 +957,32 @@ export const transformationDocsContent: TransformationDocsContentType = { name: 'Merge series/tables', getHelperDocs: function () { return ` - Use this transformation to combine the results from multiple queries into a single result, which is particularly useful when using the table panel visualization. This transformation merges values into the same row if the shared fields contain the same data. - - Here's an example illustrating the impact of the **Merge series/tables** transformation on two queries returning table data: +Use this transformation to combine the results from multiple queries into a single result, which is particularly useful when using the table panel visualization. This transformation merges values into the same row if the shared fields contain the same data. + +Here's an example illustrating the impact of the **Merge series/tables** transformation on two queries returning table data: - **Query A:** +**Query A:** - | Time | Job | Uptime | - | ------------------- | ------- | --------- | - | 2020-07-07 11:34:20 | node | 25260122 | - | 2020-07-07 11:24:20 | postgre | 123001233 | +| Time | Job | Uptime | +| ------------------- | ------- | --------- | +| 2020-07-07 11:34:20 | node | 25260122 | +| 2020-07-07 11:24:20 | postgre | 123001233 | - **Query B:** +**Query B:** - | Time | Job | Errors | - | ------------------- | ------- | ------ | - | 2020-07-07 11:34:20 | node | 15 | - | 2020-07-07 11:24:20 | postgre | 5 | +| Time | Job | Errors | +| ------------------- | ------- | ------ | +| 2020-07-07 11:34:20 | node | 15 | +| 2020-07-07 11:24:20 | postgre | 5 | - Here is the result after applying the Merge transformation. +Here is the result after applying the Merge transformation. - | Time | Job | Errors | Uptime | - | ------------------- | ------- | ------ | --------- | - | 2020-07-07 11:34:20 | node | 15 | 25260122 | - | 2020-07-07 11:24:20 | postgre | 5 | 123001233 | +| Time | Job | Errors | Uptime | +| ------------------- | ------- | ------ | --------- | +| 2020-07-07 11:34:20 | node | 15 | 25260122 | +| 2020-07-07 11:24:20 | postgre | 5 | 123001233 | - This transformation combines values from Query A and Query B into a unified table, enhancing the presentation of data for better insights. +This transformation combines values from Query A and Query B into a unified table, enhancing the presentation of data for better insights. `; }, links: [ @@ -996,35 +996,35 @@ export const transformationDocsContent: TransformationDocsContentType = { name: 'Organize fields by name', getHelperDocs: function () { return ` - Use this transformation to provide the flexibility to rename, reorder, or hide fields returned by a single query in your panel. This transformation is applicable only to panels with a single query. If your panel has multiple queries, consider using an "Outer join" transformation or removing extra queries. +Use this transformation to provide the flexibility to rename, reorder, or hide fields returned by a single query in your panel. This transformation is applicable only to panels with a single query. If your panel has multiple queries, consider using an "Outer join" transformation or removing extra queries. - #### Transforming fields - - Grafana displays a list of fields returned by the query, allowing you to perform the following actions: - - - **Change field order** - Hover over a field, and when your cursor turns into a hand, drag the field to its new position. - - **Hide or show a field** - Use the eye icon next to the field name to toggle the visibility of a specific field. - - **Rename fields** - Type a new name in the "Rename " box to customize field names. - - #### Example: - - ##### Original Query Result - - | Time | Metric | Value | - | ------------------- | ----------- | ----- | - | 2020-07-07 11:34:20 | Temperature | 25 | - | 2020-07-07 11:34:20 | Humidity | 22 | - | 2020-07-07 10:32:20 | Humidity | 29 | - - ##### After Applying Field Overrides - - | Time | Sensor | Reading | - | ------------------- | ----------- | ------- | - | 2020-07-07 11:34:20 | Temperature | 25 | - | 2020-07-07 11:34:20 | Humidity | 22 | - | 2020-07-07 10:32:20 | Humidity | 29 | +#### Transforming fields + +Grafana displays a list of fields returned by the query, allowing you to perform the following actions: - This transformation lets you to tailor the display of query results, ensuring a clear and insightful representation of your data in Grafana. +- **Change field order** - Hover over a field, and when your cursor turns into a hand, drag the field to its new position. +- **Hide or show a field** - Use the eye icon next to the field name to toggle the visibility of a specific field. +- **Rename fields** - Type a new name in the "Rename " box to customize field names. + +#### Example: + +##### Original Query Result + +| Time | Metric | Value | +| ------------------- | ----------- | ----- | +| 2020-07-07 11:34:20 | Temperature | 25 | +| 2020-07-07 11:34:20 | Humidity | 22 | +| 2020-07-07 10:32:20 | Humidity | 29 | + +##### After Applying Field Overrides + +| Time | Sensor | Reading | +| ------------------- | ----------- | ------- | +| 2020-07-07 11:34:20 | Temperature | 25 | +| 2020-07-07 11:34:20 | Humidity | 22 | +| 2020-07-07 10:32:20 | Humidity | 29 | + +This transformation lets you to tailor the display of query results, ensuring a clear and insightful representation of your data in Grafana. `; }, }, @@ -1032,32 +1032,32 @@ export const transformationDocsContent: TransformationDocsContentType = { name: 'Partition by values', getHelperDocs: function () { return ` - Use this transformation to streamline the process of graphing multiple series without the need for multiple queries with different 'WHERE' clauses. - - This is particularly useful when dealing with a metrics SQL table, as illustrated below: - - | Time | Region | Value | - | ------------------- | ------ | ----- | - | 2022-10-20 12:00:00 | US | 1520 | - | 2022-10-20 12:00:00 | EU | 2936 | - | 2022-10-20 01:00:00 | US | 1327 | - | 2022-10-20 01:00:00 | EU | 912 | - - With the **Partition by values** transformation, you can issue a single query and split the results by unique values in one or more columns (fields) of your choosing. The following example uses 'Region': +Use this transformation to streamline the process of graphing multiple series without the need for multiple queries with different 'WHERE' clauses. - 'SELECT Time, Region, Value FROM metrics WHERE Time > "2022-10-20"' +This is particularly useful when dealing with a metrics SQL table, as illustrated below: - | Time | Region | Value | - | ------------------- | ------ | ----- | - | 2022-10-20 12:00:00 | US | 1520 | - | 2022-10-20 01:00:00 | US | 1327 | +| Time | Region | Value | +| ------------------- | ------ | ----- | +| 2022-10-20 12:00:00 | US | 1520 | +| 2022-10-20 12:00:00 | EU | 2936 | +| 2022-10-20 01:00:00 | US | 1327 | +| 2022-10-20 01:00:00 | EU | 912 | - | Time | Region | Value | - | ------------------- | ------ | ----- | - | 2022-10-20 12:00:00 | EU | 2936 | - | 2022-10-20 01:00:00 | EU | 912 | +With the **Partition by values** transformation, you can issue a single query and split the results by unique values in one or more columns (fields) of your choosing. The following example uses 'Region': - This transformation simplifies the process and enhances the flexibility of visualizing multiple series within the same time series visualization. +'SELECT Time, Region, Value FROM metrics WHERE Time > "2022-10-20"' + +| Time | Region | Value | +| ------------------- | ------ | ----- | +| 2022-10-20 12:00:00 | US | 1520 | +| 2022-10-20 01:00:00 | US | 1327 | + +| Time | Region | Value | +| ------------------- | ------ | ----- | +| 2022-10-20 12:00:00 | EU | 2936 | +| 2022-10-20 01:00:00 | EU | 912 | + +This transformation simplifies the process and enhances the flexibility of visualizing multiple series within the same time series visualization. `; }, }, @@ -1065,52 +1065,52 @@ export const transformationDocsContent: TransformationDocsContentType = { name: 'Prepare time series', getHelperDocs: function () { return ` - Use this transformation to address issues when a data source returns time series data in a format that isn't compatible with the desired visualization. This transformation allows you to convert time series data between wide and long formats, providing flexibility in data frame structures. +Use this transformation to address issues when a data source returns time series data in a format that isn't compatible with the desired visualization. This transformation allows you to convert time series data between wide and long formats, providing flexibility in data frame structures. + +#### Available options + +##### Multi-frame time series + +Use this option to transform the time series data frame from the wide format to the long format. This is particularly helpful when your data source delivers time series information in a format that needs to be reshaped for optimal compatibility with your visualization. + +**Example: Converting from wide to long format** + +| Timestamp | Value1 | Value2 | +|---------------------|--------|--------| +| 2023-01-01 00:00:00 | 10 | 20 | +| 2023-01-01 01:00:00 | 15 | 25 | + +**Transformed to:** + +| Timestamp | Variable | Value | +|---------------------|----------|-------| +| 2023-01-01 00:00:00 | Value1 | 10 | +| 2023-01-01 00:00:00 | Value2 | 20 | +| 2023-01-01 01:00:00 | Value1 | 15 | +| 2023-01-01 01:00:00 | Value2 | 25 | - #### Available options - - ##### Multi-frame time series - - Use this option to transform the time series data frame from the wide format to the long format. This is particularly helpful when your data source delivers time series information in a format that needs to be reshaped for optimal compatibility with your visualization. - - **Example: Converting from wide to long format** - - | Timestamp | Value1 | Value2 | - |---------------------|--------|--------| - | 2023-01-01 00:00:00 | 10 | 20 | - | 2023-01-01 01:00:00 | 15 | 25 | - - **Transformed to:** - - | Timestamp | Variable | Value | - |---------------------|----------|-------| - | 2023-01-01 00:00:00 | Value1 | 10 | - | 2023-01-01 00:00:00 | Value2 | 20 | - | 2023-01-01 01:00:00 | Value1 | 15 | - | 2023-01-01 01:00:00 | Value2 | 25 | - - - ##### Wide time series - - Select this option to transform the time series data frame from the long format to the wide format. If your data source returns time series data in a long format and your visualization requires a wide format, this transformation simplifies the process. - - **Example: Converting from long to wide format** - - | Timestamp | Variable | Value | - |---------------------|----------|-------| - | 2023-01-01 00:00:00 | Value1 | 10 | - | 2023-01-01 00:00:00 | Value2 | 20 | - | 2023-01-01 01:00:00 | Value1 | 15 | - | 2023-01-01 01:00:00 | Value2 | 25 | - - **Transformed to:** - - | Timestamp | Value1 | Value2 | - |---------------------|--------|--------| - | 2023-01-01 00:00:00 | 10 | 20 | - | 2023-01-01 01:00:00 | 15 | 25 | - > **Note:** This transformation is available in Grafana 7.5.10+ and Grafana 8.0.6+. +##### Wide time series + +Select this option to transform the time series data frame from the long format to the wide format. If your data source returns time series data in a long format and your visualization requires a wide format, this transformation simplifies the process. + +**Example: Converting from long to wide format** + +| Timestamp | Variable | Value | +|---------------------|----------|-------| +| 2023-01-01 00:00:00 | Value1 | 10 | +| 2023-01-01 00:00:00 | Value2 | 20 | +| 2023-01-01 01:00:00 | Value1 | 15 | +| 2023-01-01 01:00:00 | Value2 | 25 | + +**Transformed to:** + +| Timestamp | Value1 | Value2 | +|---------------------|--------|--------| +| 2023-01-01 00:00:00 | 10 | 20 | +| 2023-01-01 01:00:00 | 15 | 25 | + +> **Note:** This transformation is available in Grafana 7.5.10+ and Grafana 8.0.6+. `; }, links: [ @@ -1124,55 +1124,55 @@ export const transformationDocsContent: TransformationDocsContentType = { name: 'Reduce', getHelperDocs: function () { return ` - Use this transformation to apply a calculation to each field in the data frame and return a single value. This transformation is particularly useful for consolidating multiple time series data into a more compact, summarized format. Time fields are removed when applying this transformation. +Use this transformation to apply a calculation to each field in the data frame and return a single value. This transformation is particularly useful for consolidating multiple time series data into a more compact, summarized format. Time fields are removed when applying this transformation. - Consider the input: +Consider the input: - **Query A:** +**Query A:** - | Time | Temp | Uptime | - | ------------------- | ---- | ------- | - | 2020-07-07 11:34:20 | 12.3 | 256122 | - | 2020-07-07 11:24:20 | 15.4 | 1230233 | +| Time | Temp | Uptime | +| ------------------- | ---- | ------- | +| 2020-07-07 11:34:20 | 12.3 | 256122 | +| 2020-07-07 11:24:20 | 15.4 | 1230233 | - **Query B:** +**Query B:** - | Time | AQI | Errors | - | ------------------- | --- | ------ | - | 2020-07-07 11:34:20 | 6.5 | 15 | - | 2020-07-07 11:24:20 | 3.2 | 5 | +| Time | AQI | Errors | +| ------------------- | --- | ------ | +| 2020-07-07 11:34:20 | 6.5 | 15 | +| 2020-07-07 11:24:20 | 3.2 | 5 | - The reduce transformer has two modes: +The reduce transformer has two modes: - - **Series to rows** - Creates a row for each field and a column for each calculation. - - **Reduce fields** - Keeps the existing frame structure, but collapses each field into a single value. +- **Series to rows** - Creates a row for each field and a column for each calculation. +- **Reduce fields** - Keeps the existing frame structure, but collapses each field into a single value. - For example, if you used the **First** and **Last** calculation with a **Series to rows** transformation, then - the result would be: +For example, if you used the **First** and **Last** calculation with a **Series to rows** transformation, then +the result would be: - | Field | First | Last | - | ------ | ------ | ------- | - | Temp | 12.3 | 15.4 | - | Uptime | 256122 | 1230233 | - | AQI | 6.5 | 3.2 | - | Errors | 15 | 5 | +| Field | First | Last | +| ------ | ------ | ------- | +| Temp | 12.3 | 15.4 | +| Uptime | 256122 | 1230233 | +| AQI | 6.5 | 3.2 | +| Errors | 15 | 5 | - The **Reduce fields** with the **Last** calculation, - results in two frames, each with one row: +The **Reduce fields** with the **Last** calculation, +results in two frames, each with one row: - **Query A:** +**Query A:** - | Temp | Uptime | - | ---- | ------- | - | 15.4 | 1230233 | +| Temp | Uptime | +| ---- | ------- | +| 15.4 | 1230233 | - **Query B:** +**Query B:** - | AQI | Errors | - | --- | ------ | - | 3.2 | 5 | +| AQI | Errors | +| --- | ------ | +| 3.2 | 5 | - This flexible transformation simplifies the process of consolidating and summarizing data from multiple time series into a more manageable and organized format. +This flexible transformation simplifies the process of consolidating and summarizing data from multiple time series into a more manageable and organized format. `; }, }, @@ -1180,27 +1180,27 @@ export const transformationDocsContent: TransformationDocsContentType = { name: 'Rename by regex', getHelperDocs: function (imageRenderType: ImageRenderType = ImageRenderType.ShortcodeFigure) { return ` - Use this transformation to rename parts of the query results using a regular expression and replacement pattern. +Use this transformation to rename parts of the query results using a regular expression and replacement pattern. - You can specify a regular expression, which is only applied to matches, along with a replacement pattern that support back references. For example, let's imagine you're visualizing CPU usage per host and you want to remove the domain name. You could set the regex to '([^\.]+)\..+' and the replacement pattern to '$1', 'web-01.example.com' would become 'web-01'. - - In the following example, we are stripping the prefix from event types. In the before image, you can see everything is prefixed with 'system.' +You can specify a regular expression, which is only applied to matches, along with a replacement pattern that support back references. For example, let's imagine you're visualizing CPU usage per host and you want to remove the domain name. You could set the regex to '([^\.]+)\..+' and the replacement pattern to '$1', 'web-01.example.com' would become 'web-01'. - ${buildImageContent( - '/static/img/docs/transformations/rename-by-regex-before-7-3.png', - imageRenderType, - 'A bar chart with long series names' - )} +In the following example, we are stripping the prefix from event types. In the before image, you can see everything is prefixed with 'system.' - With the transformation applied, you can see we are left with just the remainder of the string. +${buildImageContent( + '/static/img/docs/transformations/rename-by-regex-before-7-3.png', + imageRenderType, + 'A bar chart with long series names' +)} + +With the transformation applied, you can see we are left with just the remainder of the string. - ${buildImageContent( - '/static/img/docs/transformations/rename-by-regex-after-7-3.png', - imageRenderType, - 'A bar chart with shortened series names' - )} +${buildImageContent( + '/static/img/docs/transformations/rename-by-regex-after-7-3.png', + imageRenderType, + 'A bar chart with shortened series names' +)} - This transformation lets you to tailor your data to meet your visualization needs, making your dashboards more informative and user-friendly. +This transformation lets you to tailor your data to meet your visualization needs, making your dashboards more informative and user-friendly. `; }, }, @@ -1208,66 +1208,66 @@ export const transformationDocsContent: TransformationDocsContentType = { name: 'Rows to fields', getHelperDocs: function () { return ` - Use this transformation to convert rows into separate fields. This can be useful because fields can be styled and configured individually. It can also use additional fields as sources for dynamic field configuration or map them to field labels. The additional labels can then be used to define better display names for the resulting fields. +Use this transformation to convert rows into separate fields. This can be useful because fields can be styled and configured individually. It can also use additional fields as sources for dynamic field configuration or map them to field labels. The additional labels can then be used to define better display names for the resulting fields. - This transformation includes a field table which lists all fields in the data returned by the configuration query. This table gives you control over what field should be mapped to each configuration property (the **Use as** option). You can also choose which value to select if there are multiple rows in the returned data. +This transformation includes a field table which lists all fields in the data returned by the configuration query. This table gives you control over what field should be mapped to each configuration property (the **Use as** option). You can also choose which value to select if there are multiple rows in the returned data. - This transformation requires: +This transformation requires: - - One field to use as the source of field names. +- One field to use as the source of field names. - By default, the transform uses the first string field as the source. You can override this default setting by selecting **Field name** in the **Use as** column for the field you want to use instead. + By default, the transform uses the first string field as the source. You can override this default setting by selecting **Field name** in the **Use as** column for the field you want to use instead. - - One field to use as the source of values. +- One field to use as the source of values. - By default, the transform uses the first number field as the source. But you can override this default setting by selecting **Field value** in the **Use as** column for the field you want to use instead. + By default, the transform uses the first number field as the source. But you can override this default setting by selecting **Field value** in the **Use as** column for the field you want to use instead. - Useful when visualizing data in: +Useful when visualizing data in: - - Gauge - - Stat - - Pie chart +- Gauge +- Stat +- Pie chart - #### Map extra fields to labels +#### Map extra fields to labels - If a field does not map to config property Grafana will automatically use it as source for a label on the output field- +If a field does not map to config property Grafana will automatically use it as source for a label on the output field- - **Example:** +**Example:** - | Name | DataCenter | Value | - | ------- | ---------- | ----- | - | ServerA | US | 100 | - | ServerB | EU | 200 | +| Name | DataCenter | Value | +| ------- | ---------- | ----- | +| ServerA | US | 100 | +| ServerB | EU | 200 | - **Output:** +**Output:** - | ServerA (labels: DataCenter: US) | ServerB (labels: DataCenter: EU) | - | -------------------------------- | -------------------------------- | - | 10 | 20 | +| ServerA (labels: DataCenter: US) | ServerB (labels: DataCenter: EU) | +| -------------------------------- | -------------------------------- | +| 10 | 20 | - The extra labels can now be used in the field display name provide more complete field names. +The extra labels can now be used in the field display name provide more complete field names. - If you want to extract config from one query and apply it to another you should use the config from query results transformation. +If you want to extract config from one query and apply it to another you should use the config from query results transformation. - #### Example +#### Example - **Input:** +**Input:** - | Name | Value | Max | - | ------- | ----- | --- | - | ServerA | 10 | 100 | - | ServerB | 20 | 200 | - | ServerC | 30 | 300 | +| Name | Value | Max | +| ------- | ----- | --- | +| ServerA | 10 | 100 | +| ServerB | 20 | 200 | +| ServerC | 30 | 300 | - **Output:** +**Output:** - | ServerA (config: max=100) | ServerB (config: max=200) | ServerC (config: max=300) | - | ------------------------- | ------------------------- | ------------------------- | - | 10 | 20 | 30 | +| ServerA (config: max=100) | ServerB (config: max=200) | ServerC (config: max=300) | +| ------------------------- | ------------------------- | ------------------------- | +| 10 | 20 | 30 | - As you can see each row in the source data becomes a separate field. Each field now also has a max config option set. Options like **Min**, **Max**, **Unit** and **Thresholds** are all part of field configuration and if set like this will be used by the visualization instead of any options manually configured in the panel editor options pane. +As you can see each row in the source data becomes a separate field. Each field now also has a max config option set. Options like **Min**, **Max**, **Unit** and **Thresholds** are all part of field configuration and if set like this will be used by the visualization instead of any options manually configured in the panel editor options pane. - This transformation enables the conversion of rows into individual fields, facilitates dynamic field configuration, and maps additional fields to labels. +This transformation enables the conversion of rows into individual fields, facilitates dynamic field configuration, and maps additional fields to labels. `; }, }, @@ -1275,42 +1275,42 @@ export const transformationDocsContent: TransformationDocsContentType = { name: 'Series to rows', getHelperDocs: function () { return ` - Use this transformation to combine the result from multiple time series data queries into one single result. This is helpful when using the table panel visualization. +Use this transformation to combine the result from multiple time series data queries into one single result. This is helpful when using the table panel visualization. - The result from this transformation will contain three columns: Time, Metric, and Value. The Metric column is added so you easily can see from which query the metric originates from. Customize this value by defining Label on the source query. +The result from this transformation will contain three columns: Time, Metric, and Value. The Metric column is added so you easily can see from which query the metric originates from. Customize this value by defining Label on the source query. - In the example below, we have two queries returning time series data. It is visualized as two separate tables before applying the transformation. +In the example below, we have two queries returning time series data. It is visualized as two separate tables before applying the transformation. - **Query A:** +**Query A:** - | Time | Temperature | - | ------------------- | ----------- | - | 2020-07-07 11:34:20 | 25 | - | 2020-07-07 10:31:22 | 22 | - | 2020-07-07 09:30:05 | 19 | +| Time | Temperature | +| ------------------- | ----------- | +| 2020-07-07 11:34:20 | 25 | +| 2020-07-07 10:31:22 | 22 | +| 2020-07-07 09:30:05 | 19 | - **Query B:** +**Query B:** - | Time | Humidity | - | ------------------- | -------- | - | 2020-07-07 11:34:20 | 24 | - | 2020-07-07 10:32:20 | 29 | - | 2020-07-07 09:30:57 | 33 | +| Time | Humidity | +| ------------------- | -------- | +| 2020-07-07 11:34:20 | 24 | +| 2020-07-07 10:32:20 | 29 | +| 2020-07-07 09:30:57 | 33 | - Here is the result after applying the Series to rows transformation. +Here is the result after applying the Series to rows transformation. - | Time | Metric | Value | - | ------------------- | ----------- | ----- | - | 2020-07-07 11:34:20 | Temperature | 25 | - | 2020-07-07 11:34:20 | Humidity | 22 | - | 2020-07-07 10:32:20 | Humidity | 29 | - | 2020-07-07 10:31:22 | Temperature | 22 | - | 2020-07-07 09:30:57 | Humidity | 33 | - | 2020-07-07 09:30:05 | Temperature | 19 | +| Time | Metric | Value | +| ------------------- | ----------- | ----- | +| 2020-07-07 11:34:20 | Temperature | 25 | +| 2020-07-07 11:34:20 | Humidity | 22 | +| 2020-07-07 10:32:20 | Humidity | 29 | +| 2020-07-07 10:31:22 | Temperature | 22 | +| 2020-07-07 09:30:57 | Humidity | 33 | +| 2020-07-07 09:30:05 | Temperature | 19 | - This transformation facilitates the consolidation of results from multiple time series queries, providing a streamlined and unified dataset for efficient analysis and visualization in a tabular format. +This transformation facilitates the consolidation of results from multiple time series queries, providing a streamlined and unified dataset for efficient analysis and visualization in a tabular format. - > **Note:** This transformation is available in Grafana 7.1+. +> **Note:** This transformation is available in Grafana 7.1+. `; }, }, @@ -1318,11 +1318,11 @@ export const transformationDocsContent: TransformationDocsContentType = { name: 'Sort by', getHelperDocs: function () { return ` - Use this transformation to sort each frame within a query result based on a specified field, making your data easier to understand and analyze. By configuring the desired field for sorting, you can control the order in which the data is presented in the table or visualization. +Use this transformation to sort each frame within a query result based on a specified field, making your data easier to understand and analyze. By configuring the desired field for sorting, you can control the order in which the data is presented in the table or visualization. - Use the **Reverse** switch to inversely order the values within the specified field. This functionality is particularly useful when you want to quickly toggle between ascending and descending order to suit your analytical needs. - - For example, in a scenario where time-series data is retrieved from a data source, the **Sort by** transformation can be applied to arrange the data frames based on the timestamp, either in ascending or descending order, depending on the analytical requirements. This capability ensures that you can easily navigate and interpret time-series data, gaining valuable insights from the organized and visually coherent presentation. +Use the **Reverse** switch to inversely order the values within the specified field. This functionality is particularly useful when you want to quickly toggle between ascending and descending order to suit your analytical needs. + +For example, in a scenario where time-series data is retrieved from a data source, the **Sort by** transformation can be applied to arrange the data frames based on the timestamp, either in ascending or descending order, depending on the analytical requirements. This capability ensures that you can easily navigate and interpret time-series data, gaining valuable insights from the organized and visually coherent presentation. `; }, }, @@ -1330,26 +1330,26 @@ export const transformationDocsContent: TransformationDocsContentType = { name: 'Spatial', getHelperDocs: function () { return ` - Use this transformation to apply spatial operations to query results. - - - **Action** - Select an action: - - **Prepare spatial field** - Set a geometry field based on the results of other fields. - - **Location mode** - Select a location mode (these options are shared by the **Calculate value** and **Transform** modes): - - **Auto** - Automatically identify location data based on default field names. - - **Coords** - Specify latitude and longitude fields. - - **Geohash** - Specify a geohash field. - - **Lookup** - Specify Gazetteer location fields. - - **Calculate value** - Use the geometry to define a new field (heading/distance/area). - - **Function** - Choose a mathematical operation to apply to the geometry: - - **Heading** - Calculate the heading (direction) between two points. - - **Area** - Calculate the area enclosed by a polygon defined by the geometry. - - **Distance** - Calculate the distance between two points. - - **Transform** - Apply spatial operations to the geometry. - - **Operation** - Choose an operation to apply to the geometry: - - **As line** - Create a single line feature with a vertex at each row. - - **Line builder** - Create a line between two points. - - This transformation allows you to manipulate and analyze geospatial data, enabling operations such as creating lines between points, calculating spatial properties, and more. +Use this transformation to apply spatial operations to query results. + +- **Action** - Select an action: + - **Prepare spatial field** - Set a geometry field based on the results of other fields. + - **Location mode** - Select a location mode (these options are shared by the **Calculate value** and **Transform** modes): + - **Auto** - Automatically identify location data based on default field names. + - **Coords** - Specify latitude and longitude fields. + - **Geohash** - Specify a geohash field. + - **Lookup** - Specify Gazetteer location fields. + - **Calculate value** - Use the geometry to define a new field (heading/distance/area). + - **Function** - Choose a mathematical operation to apply to the geometry: + - **Heading** - Calculate the heading (direction) between two points. + - **Area** - Calculate the area enclosed by a polygon defined by the geometry. + - **Distance** - Calculate the distance between two points. + - **Transform** - Apply spatial operations to the geometry. + - **Operation** - Choose an operation to apply to the geometry: + - **As line** - Create a single line feature with a vertex at each row. + - **Line builder** - Create a line between two points. + +This transformation allows you to manipulate and analyze geospatial data, enabling operations such as creating lines between points, calculating spatial properties, and more. `; }, }, @@ -1357,11 +1357,11 @@ export const transformationDocsContent: TransformationDocsContentType = { name: 'Time series to table transform', getHelperDocs: function () { return ` - Use this transformation to convert time series results into a table, transforming a time series data frame into a **Trend** field. The **Trend** field can then be rendered using the [sparkline cell type][], generating 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. +Use this transformation to convert time series results into a table, transforming a time series data frame into a **Trend** field. The **Trend** field can then be rendered using the [sparkline cell type][], generating 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. - For each generated **Trend** field value, a calculation function can be selected. The default is **Last non-null value**. This value is displayed next to the sparkline and used for sorting table rows. +For each generated **Trend** field value, a calculation function can be selected. The default is **Last non-null value**. This value is displayed next to the sparkline and used for sorting table rows. - > **Note:** This transformation is available in Grafana 9.5+ as an opt-in beta feature. Modify the Grafana [configuration file][] to use it. +> **Note:** This transformation is available in Grafana 9.5+ as an opt-in beta feature. Modify the Grafana [configuration file][] to use it. `; }, links: [ @@ -1379,24 +1379,24 @@ export const transformationDocsContent: TransformationDocsContentType = { name: 'Regression analysis', getHelperDocs: function (imageRenderType: ImageRenderType = ImageRenderType.ShortcodeFigure) { return ` - Use this transformation to create a new data frame containing values predicted by a statistical model. This is useful for finding a trend in chaotic data. It works by fitting a mathematical function to the data, using either linear or polynomial regression. The data frame can then be used in a visualization to display a trendline. +Use this transformation to create a new data frame containing values predicted by a statistical model. This is useful for finding a trend in chaotic data. It works by fitting a mathematical function to the data, using either linear or polynomial regression. The data frame can then be used in a visualization to display a trendline. - There are two different models: +There are two different models: - - **Linear regression** - Fits a linear function to the data. +- **Linear regression** - Fits a linear function to the data. ${buildImageContent( '/static/img/docs/transformations/linear-regression.png', imageRenderType, 'A time series visualization with a straight line representing the linear function' )} - - **Polynomial regression** - Fits a polynomial function to the data. - ${buildImageContent( - '/static/img/docs/transformations/polynomial-regression.png', - imageRenderType, - 'A time series visualization with a curved line representing the polynomial function' - )} - - > **Note:** This transformation is currently in public preview. Grafana Labs offers limited support, and breaking changes might occur prior to the feature being made generally available. Enable the \`regressionTransformation\` feature toggle in Grafana to use this feature. Contact Grafana Support to enable this feature in Grafana Cloud. +- **Polynomial regression** - Fits a polynomial function to the data. +${buildImageContent( + '/static/img/docs/transformations/polynomial-regression.png', + imageRenderType, + 'A time series visualization with a curved line representing the polynomial function' +)} + +> **Note:** This transformation is currently in public preview. Grafana Labs offers limited support, and breaking changes might occur prior to the feature being made generally available. Enable the \`regressionTransformation\` feature toggle in Grafana to use this feature. Contact Grafana Support to enable this feature in Grafana Cloud. `; }, }, diff --git a/scripts/docs/generate-transformations.test.ts b/scripts/docs/generate-transformations.test.ts new file mode 100644 index 0000000000..e30a71f78c --- /dev/null +++ b/scripts/docs/generate-transformations.test.ts @@ -0,0 +1,57 @@ +import { getMarkdownContent, getJavaScriptContent, readMeContent } from './generate-transformations.ts'; + +describe('makefile script tests', () => { + it('should execute without error and match the content written to index.md', () => { + // Normalize and compare. + expect(contentDoesMatch(getJavaScriptContent(), getMarkdownContent())).toBe(true); + }); + + it('should be able to tell if the content DOES NOT match', () => { + const wrongContent = getJavaScriptContent().concat('additional content to mismatch'); + // Normalize and compare. + expect(contentDoesMatch(wrongContent, getMarkdownContent())).toBe(false); + + // If this test fails, refer to `./docs/README.md` "Content guidelines" for more information + // about editing and building the Transformations docs. + }); +}); + +export function contentDoesMatch(jsContent: string, mdContent: string): Boolean { + return normalizeContent(jsContent) === normalizeContent(mdContent); +} + +/* + Normalize content by removing all whitespace (spaces, tabs, newlines, carriage returns, + form feeds, and vertical tabs) and special characters. + + NOTE: There are numerous unpredictable formatting oddities when transforming JavaScript to Markdown; + almost all of them are irrelevant to the actual content of the file, which is why we strip them out here. + + For example: + + In JavaScript, the following string table + + | A | B | C | + | - | - | - | + | 1 | 3 | 5 | + | 2 | 4 | 6 | + | 3 | 5 | 7 | + | 4 | 6 | 8 | + | 5 | 7 | 9 | + + parses to Markdown as + + | A | B | C | + | --- | --- | --- | <--------- notice the extra hyphens + | 1 | 3 | 5 | <--------- notice the extra spaces + | 2 | 4 | 6 | + | 3 | 5 | 7 | + | 4 | 6 | 8 | + | 5 | 7 | 9 | + + This is one of many arbitrary formatting anomalies that we can ignore by normalizing the + content before comparing the JavaScript template literals and the final Markdown. +*/ +function normalizeContent(content: string): string { + return content.replace(/\s+|[`~!@#$%^&*()_|+\-=?;:'",.<>\{\}\[\]\\\/]/g, '').trim(); +} diff --git a/scripts/docs/generate-transformations.ts b/scripts/docs/generate-transformations.ts index 0e9cabcb02..8e12986ce9 100644 --- a/scripts/docs/generate-transformations.ts +++ b/scripts/docs/generate-transformations.ts @@ -1,13 +1,15 @@ +import { writeFileSync, readFileSync, read } from 'fs'; +import { resolve } from 'path'; + import { transformationDocsContent, TransformationDocsContentType, ImageRenderType, } from '../../public/app/features/transformers/docs/content'; -const template = `--- -comments: | - This Markdown file is auto-generated. DO NOT EDIT THIS FILE DIRECTLY. +const WRITE_PATH = 'docs/sources/panels-visualizations/query-transform-data/transform-data/index.md'; +export const readMeContent = ` To update this Markdown, navigate to the following Typescript files and edit them based on what you need to update: scripts/docs/generate-transformations.ts - Includes all content not specific to a transformation. @@ -24,6 +26,12 @@ comments: | Browse to http://localhost:3003/docs/grafana/latest/panels-visualizations/query-transform-data/transform-data/ Refer to ./docs/README.md "Content guidelines" for more information about editing and building these docs. +`; + +export const templateMetaContent = `--- +comments: | + This Markdown file is auto-generated. DO NOT EDIT THIS FILE DIRECTLY. +${readMeContent} aliases: - ../../panels/reference-transformation-functions/ @@ -47,9 +55,9 @@ labels: title: Transform data description: Use transformations to rename fields, join series data, apply mathematical operations, and more weight: 100 ---- +---`; -# Transform data +const templateIntroContent = `# Transform data Transformations are a powerful way to manipulate data returned by a query before the system applies a visualization. Using transformations, you can: @@ -126,13 +134,15 @@ We recommend that you remove transformations that you don't need. When you delet 1. Click the trash icon next to the transformation you want to delete. {{< figure src="/static/img/docs/transformations/screenshot-example-remove-transformation.png" class="docs-image--no-shadow" max-width= "1100px" alt="A transformation row with the remove transformation icon highlighted" >}} +`; + +export const completeTemplate = `${templateMetaContent} +${templateIntroContent} ## Transformation functions You can perform the following transformations on your data. - ${buildTransformationDocsContent(transformationDocsContent)} - {{% docs/reference %}} [Table panel]: "/docs/grafana/ -> /docs/grafana//panels-visualizations/visualizations/table" [Table panel]: "/docs/grafana-cloud/ -> /docs/grafana//panels-visualizations/visualizations/table" @@ -169,9 +179,8 @@ function buildTransformationDocsContent(transformationDocsContent: Transformatio const content = transformationsList .map((transformationName) => { return ` - ### ${transformationDocsContent[transformationName].name} - ${transformationDocsContent[transformationName].getHelperDocs(ImageRenderType.ShortcodeFigure)} - `; +### ${transformationDocsContent[transformationName].name} +${transformationDocsContent[transformationName].getHelperDocs(ImageRenderType.ShortcodeFigure)}`; }) // Remove the superfluous commas. .join(''); @@ -179,9 +188,20 @@ function buildTransformationDocsContent(transformationDocsContent: Transformatio return content; } -/* - `process.stdout.write(template + '\n')` was not functioning as expected. - Neither the tsc nor ts-node compiler could identify the node `process` object. - Fortunately, `console.log` also writes to the standard output. -*/ -console.log(template); +export function buildMarkdownContent(): void { + // Build the path to the Markdown file. + const indexPath = resolve(__dirname, '../../' + WRITE_PATH); + + // Write content to the Markdown file. + writeFileSync(indexPath, completeTemplate, 'utf-8'); +} + +export function getMarkdownContent(): string { + const rootDir = resolve(__dirname, '../../'); + const pathToMarkdown = resolve(rootDir, WRITE_PATH); + return readFileSync(pathToMarkdown, 'utf-8'); +} + +export function getJavaScriptContent(): string { + return completeTemplate; +} From 0f1cefa94249040dd4b4b0f3d06968cd50e4dabb Mon Sep 17 00:00:00 2001 From: Leon Sorokin Date: Tue, 27 Feb 2024 12:07:55 -0600 Subject: [PATCH 0245/1406] VizTooltip: Cleanup (#83462) --- .../src/components/VizTooltip/HeaderLabel.tsx | 24 --------- .../VizTooltip/VizTooltipContent.tsx | 54 +++++++------------ .../VizTooltip/VizTooltipHeader.tsx | 25 +++++---- .../VizTooltip/VizTooltipHeaderLabelValue.tsx | 25 --------- .../components/VizTooltip/VizTooltipRow.tsx | 6 +-- .../src/components/VizTooltip/types.ts | 2 +- .../src/components/VizTooltip/utils.ts | 10 ++-- .../panel/heatmap/HeatmapHoverView.tsx | 25 +++++---- .../state-timeline/StateTimelineTooltip2.tsx | 8 +-- .../panel/timeseries/TimeSeriesTooltip.tsx | 8 +-- .../plugins/panel/xychart/XYChartTooltip.tsx | 10 ++-- 11 files changed, 73 insertions(+), 124 deletions(-) delete mode 100644 packages/grafana-ui/src/components/VizTooltip/HeaderLabel.tsx delete mode 100644 packages/grafana-ui/src/components/VizTooltip/VizTooltipHeaderLabelValue.tsx diff --git a/packages/grafana-ui/src/components/VizTooltip/HeaderLabel.tsx b/packages/grafana-ui/src/components/VizTooltip/HeaderLabel.tsx deleted file mode 100644 index b18c1510dd..0000000000 --- a/packages/grafana-ui/src/components/VizTooltip/HeaderLabel.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import React from 'react'; - -import { VizTooltipRow } from './VizTooltipRow'; -import { LabelValue } from './types'; - -interface Props { - headerLabel: LabelValue; - isPinned: boolean; -} - -export const HeaderLabel = ({ headerLabel, isPinned }: Props) => { - const { label, value, color, colorIndicator } = headerLabel; - - return ( - - ); -}; diff --git a/packages/grafana-ui/src/components/VizTooltip/VizTooltipContent.tsx b/packages/grafana-ui/src/components/VizTooltip/VizTooltipContent.tsx index 2d48af640c..802debb59c 100644 --- a/packages/grafana-ui/src/components/VizTooltip/VizTooltipContent.tsx +++ b/packages/grafana-ui/src/components/VizTooltip/VizTooltipContent.tsx @@ -1,21 +1,21 @@ import { css } from '@emotion/css'; -import React, { CSSProperties, ReactElement } from 'react'; +import React, { CSSProperties, ReactNode } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { useStyles2 } from '../../themes'; import { VizTooltipRow } from './VizTooltipRow'; -import { LabelValue } from './types'; +import { VizTooltipItem } from './types'; -interface Props { - contentLabelValue: LabelValue[]; - customContent?: ReactElement[]; +interface VizTooltipContentProps { + items: VizTooltipItem[]; + children?: ReactNode; scrollable?: boolean; isPinned: boolean; } -export const VizTooltipContent = ({ contentLabelValue, customContent, isPinned, scrollable = false }: Props) => { +export const VizTooltipContent = ({ items, children, isPinned, scrollable = false }: VizTooltipContentProps) => { const styles = useStyles2(getStyles); const scrollableStyle: CSSProperties = scrollable @@ -27,31 +27,20 @@ export const VizTooltipContent = ({ contentLabelValue, customContent, isPinned, return (
-
- {contentLabelValue.map((labelValue, i) => { - const { label, value, color, colorIndicator, colorPlacement, isActive } = labelValue; - return ( - - ); - })} -
- {customContent?.map((content, i) => { - return ( -
- {content} -
- ); - })} + {items.map(({ label, value, color, colorIndicator, colorPlacement, isActive }, i) => ( + + ))} + {children}
); }; @@ -65,7 +54,4 @@ const getStyles = (theme: GrafanaTheme2) => ({ borderTop: `1px solid ${theme.colors.border.medium}`, padding: theme.spacing(1), }), - customContentPadding: css({ - padding: `${theme.spacing(1)} 0`, - }), }); diff --git a/packages/grafana-ui/src/components/VizTooltip/VizTooltipHeader.tsx b/packages/grafana-ui/src/components/VizTooltip/VizTooltipHeader.tsx index 6e497e96d2..360145db48 100644 --- a/packages/grafana-ui/src/components/VizTooltip/VizTooltipHeader.tsx +++ b/packages/grafana-ui/src/components/VizTooltip/VizTooltipHeader.tsx @@ -1,27 +1,32 @@ import { css } from '@emotion/css'; -import React, { ReactElement } from 'react'; +import React from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { useStyles2 } from '../../themes'; -import { HeaderLabel } from './HeaderLabel'; -import { VizTooltipHeaderLabelValue } from './VizTooltipHeaderLabelValue'; -import { LabelValue } from './types'; +import { VizTooltipRow } from './VizTooltipRow'; +import { VizTooltipItem } from './types'; interface Props { - headerLabel: LabelValue; - keyValuePairs?: LabelValue[]; - customValueDisplay?: ReactElement | null; + item: VizTooltipItem; isPinned: boolean; } -export const VizTooltipHeader = ({ headerLabel, keyValuePairs, customValueDisplay, isPinned }: Props) => { +export const VizTooltipHeader = ({ item, isPinned }: Props) => { const styles = useStyles2(getStyles); + const { label, value, color, colorIndicator } = item; + return (
- - {customValueDisplay || } +
); }; diff --git a/packages/grafana-ui/src/components/VizTooltip/VizTooltipHeaderLabelValue.tsx b/packages/grafana-ui/src/components/VizTooltip/VizTooltipHeaderLabelValue.tsx deleted file mode 100644 index 3a930ba5c1..0000000000 --- a/packages/grafana-ui/src/components/VizTooltip/VizTooltipHeaderLabelValue.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from 'react'; - -import { VizTooltipRow } from './VizTooltipRow'; -import { LabelValue } from './types'; - -interface Props { - keyValuePairs?: LabelValue[]; - isPinned: boolean; -} - -export const VizTooltipHeaderLabelValue = ({ keyValuePairs, isPinned }: Props) => ( - <> - {keyValuePairs?.map((keyValuePair, i) => ( - - ))} - -); diff --git a/packages/grafana-ui/src/components/VizTooltip/VizTooltipRow.tsx b/packages/grafana-ui/src/components/VizTooltip/VizTooltipRow.tsx index f6f079c511..5faa6b31df 100644 --- a/packages/grafana-ui/src/components/VizTooltip/VizTooltipRow.tsx +++ b/packages/grafana-ui/src/components/VizTooltip/VizTooltipRow.tsx @@ -8,9 +8,9 @@ import { InlineToast } from '../InlineToast/InlineToast'; import { Tooltip } from '../Tooltip'; import { ColorIndicatorPosition, VizTooltipColorIndicator } from './VizTooltipColorIndicator'; -import { ColorPlacement, LabelValue } from './types'; +import { ColorPlacement, VizTooltipItem } from './types'; -interface Props extends Omit { +interface VizTooltipRowProps extends Omit { value: string | number | null | ReactNode; justify?: string; isActive?: boolean; // for series list @@ -36,7 +36,7 @@ export const VizTooltipRow = ({ isActive = false, marginRight = '0px', isPinned, -}: Props) => { +}: VizTooltipRowProps) => { const styles = useStyles2(getStyles, justify, marginRight); const [showLabelTooltip, setShowLabelTooltip] = useState(false); diff --git a/packages/grafana-ui/src/components/VizTooltip/types.ts b/packages/grafana-ui/src/components/VizTooltip/types.ts index a33868ce7b..8a734a8509 100644 --- a/packages/grafana-ui/src/components/VizTooltip/types.ts +++ b/packages/grafana-ui/src/components/VizTooltip/types.ts @@ -17,7 +17,7 @@ export enum ColorPlacement { trailing = 'trailing', } -export interface LabelValue { +export interface VizTooltipItem { label: string; value: string; color?: string; diff --git a/packages/grafana-ui/src/components/VizTooltip/utils.ts b/packages/grafana-ui/src/components/VizTooltip/utils.ts index 7bbc0795b6..70de02fd70 100644 --- a/packages/grafana-ui/src/components/VizTooltip/utils.ts +++ b/packages/grafana-ui/src/components/VizTooltip/utils.ts @@ -2,7 +2,7 @@ import { FALLBACK_COLOR, Field, FieldType, formattedValueToString } from '@grafa import { SortOrder, TooltipDisplayMode } from '@grafana/schema'; import { ColorIndicatorStyles } from './VizTooltipColorIndicator'; -import { ColorIndicator, ColorPlacement, LabelValue } from './types'; +import { ColorIndicator, ColorPlacement, VizTooltipItem } from './types'; export const calculateTooltipPosition = ( xPos = 0, @@ -70,9 +70,9 @@ export const getColorIndicatorClass = (colorIndicator: string, styles: ColorIndi } }; -const numberCmp = (a: LabelValue, b: LabelValue) => a.numeric! - b.numeric!; +const numberCmp = (a: VizTooltipItem, b: VizTooltipItem) => a.numeric! - b.numeric!; const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }); -const stringCmp = (a: LabelValue, b: LabelValue) => collator.compare(`${a.value}`, `${b.value}`); +const stringCmp = (a: VizTooltipItem, b: VizTooltipItem) => collator.compare(`${a.value}`, `${b.value}`); export const getContentItems = ( fields: Field[], @@ -82,8 +82,8 @@ export const getContentItems = ( mode: TooltipDisplayMode, sortOrder: SortOrder, fieldFilter = (field: Field) => true -): LabelValue[] => { - let rows: LabelValue[] = []; +): VizTooltipItem[] => { + let rows: VizTooltipItem[] = []; let allNumeric = false; diff --git a/public/app/plugins/panel/heatmap/HeatmapHoverView.tsx b/public/app/plugins/panel/heatmap/HeatmapHoverView.tsx index ff8d3c2514..8a4e16f918 100644 --- a/public/app/plugins/panel/heatmap/HeatmapHoverView.tsx +++ b/public/app/plugins/panel/heatmap/HeatmapHoverView.tsx @@ -14,11 +14,11 @@ import { ScopedVars, } from '@grafana/data'; import { HeatmapCellLayout } from '@grafana/schema'; -import { TooltipDisplayMode, useStyles2 } from '@grafana/ui'; +import { TooltipDisplayMode, useStyles2, useTheme2 } from '@grafana/ui'; import { VizTooltipContent } from '@grafana/ui/src/components/VizTooltip/VizTooltipContent'; import { VizTooltipFooter } from '@grafana/ui/src/components/VizTooltip/VizTooltipFooter'; import { VizTooltipHeader } from '@grafana/ui/src/components/VizTooltip/VizTooltipHeader'; -import { ColorIndicator, ColorPlacement, LabelValue } from '@grafana/ui/src/components/VizTooltip/types'; +import { ColorIndicator, ColorPlacement, VizTooltipItem } from '@grafana/ui/src/components/VizTooltip/types'; import { ColorScale } from 'app/core/components/ColorScale/ColorScale'; import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; import { isHeatmapCellsDense, readHeatmapRowsCustomMeta } from 'app/features/transformers/calculateHeatmap/heatmap'; @@ -112,7 +112,7 @@ const HeatmapHoverCell = ({ let nonNumericOrdinalDisplay: string | undefined = undefined; - let contentItems: LabelValue[] = []; + let contentItems: VizTooltipItem[] = []; const getYValueIndex = (idx: number) => { return idx % data.yBucketCount! ?? 0; @@ -204,7 +204,7 @@ const HeatmapHoverCell = ({ return vals; }; - const getContentLabels = (): LabelValue[] => { + const getContentLabels = (): VizTooltipItem[] => { const isMulti = mode === TooltipDisplayMode.Multi && !isPinned; if (nonNumericOrdinalDisplay) { @@ -242,7 +242,7 @@ const HeatmapHoverCell = ({ let count = getCountValue(index); if (mode === TooltipDisplayMode.Single || isPinned) { - const fromToInt: LabelValue[] = interval ? [{ label: 'Duration', value: formatMilliseconds(interval) }] : []; + const fromToInt: VizTooltipItem[] = interval ? [{ label: 'Duration', value: formatMilliseconds(interval) }] : []; contentItems = [ { @@ -270,7 +270,7 @@ const HeatmapHoverCell = ({ toIdx++; } - const vals: LabelValue[] = getDisplayData(fromIdx, toIdx); + const vals: VizTooltipItem[] = getDisplayData(fromIdx, toIdx); vals.forEach((val) => { contentItems.push({ label: val.label, @@ -330,7 +330,7 @@ const HeatmapHoverCell = ({ [index] ); - const headerLabel: LabelValue = { + const headerItem: VizTooltipItem = { label: '', value: xDisp(xBucketMax!)!, }; @@ -365,11 +365,18 @@ const HeatmapHoverCell = ({ } const styles = useStyles2(getStyles); + const theme = useTheme2(); return (
- - + + + {customContent?.map((content, i) => ( +
+ {content} +
+ ))} +
{isPinned && }
); diff --git a/public/app/plugins/panel/state-timeline/StateTimelineTooltip2.tsx b/public/app/plugins/panel/state-timeline/StateTimelineTooltip2.tsx index 848b2ea0a3..98b6a6494d 100644 --- a/public/app/plugins/panel/state-timeline/StateTimelineTooltip2.tsx +++ b/public/app/plugins/panel/state-timeline/StateTimelineTooltip2.tsx @@ -6,7 +6,7 @@ import { TooltipDisplayMode, useStyles2 } from '@grafana/ui'; import { VizTooltipContent } from '@grafana/ui/src/components/VizTooltip/VizTooltipContent'; import { VizTooltipFooter } from '@grafana/ui/src/components/VizTooltip/VizTooltipFooter'; import { VizTooltipHeader } from '@grafana/ui/src/components/VizTooltip/VizTooltipHeader'; -import { LabelValue } from '@grafana/ui/src/components/VizTooltip/types'; +import { VizTooltipItem } from '@grafana/ui/src/components/VizTooltip/types'; import { getContentItems } from '@grafana/ui/src/components/VizTooltip/utils'; import { findNextStateIndex, fmtDuration } from 'app/core/components/TimelineChart/utils'; @@ -73,7 +73,7 @@ export const StateTimelineTooltip2 = ({ links = getDataLinks(field, dataIdx); } - const headerItem: LabelValue = { + const headerItem: VizTooltipItem = { label: xField.type === FieldType.time ? '' : getFieldDisplayName(xField, seriesFrame, frames), value: xVal, }; @@ -81,8 +81,8 @@ export const StateTimelineTooltip2 = ({ return (
- - + + {isPinned && }
diff --git a/public/app/plugins/panel/timeseries/TimeSeriesTooltip.tsx b/public/app/plugins/panel/timeseries/TimeSeriesTooltip.tsx index c7d9461bef..5c3cef5f10 100644 --- a/public/app/plugins/panel/timeseries/TimeSeriesTooltip.tsx +++ b/public/app/plugins/panel/timeseries/TimeSeriesTooltip.tsx @@ -7,7 +7,7 @@ import { useStyles2 } from '@grafana/ui'; import { VizTooltipContent } from '@grafana/ui/src/components/VizTooltip/VizTooltipContent'; import { VizTooltipFooter } from '@grafana/ui/src/components/VizTooltip/VizTooltipFooter'; import { VizTooltipHeader } from '@grafana/ui/src/components/VizTooltip/VizTooltipHeader'; -import { LabelValue } from '@grafana/ui/src/components/VizTooltip/types'; +import { VizTooltipItem } from '@grafana/ui/src/components/VizTooltip/types'; import { getContentItems } from '@grafana/ui/src/components/VizTooltip/utils'; import { getDataLinks } from '../status-history/utils'; @@ -67,7 +67,7 @@ export const TimeSeriesTooltip = ({ links = getDataLinks(field, dataIdx); } - const headerItem: LabelValue = { + const headerItem: VizTooltipItem = { label: xField.type === FieldType.time ? '' : getFieldDisplayName(xField, seriesFrame, frames), value: xVal, }; @@ -75,8 +75,8 @@ export const TimeSeriesTooltip = ({ return (
- - + + {isPinned && }
diff --git a/public/app/plugins/panel/xychart/XYChartTooltip.tsx b/public/app/plugins/panel/xychart/XYChartTooltip.tsx index 00243d1c32..2a72c99138 100644 --- a/public/app/plugins/panel/xychart/XYChartTooltip.tsx +++ b/public/app/plugins/panel/xychart/XYChartTooltip.tsx @@ -6,7 +6,7 @@ import { useStyles2 } from '@grafana/ui'; import { VizTooltipContent } from '@grafana/ui/src/components/VizTooltip/VizTooltipContent'; import { VizTooltipFooter } from '@grafana/ui/src/components/VizTooltip/VizTooltipFooter'; import { VizTooltipHeader } from '@grafana/ui/src/components/VizTooltip/VizTooltipHeader'; -import { ColorIndicator, LabelValue } from '@grafana/ui/src/components/VizTooltip/types'; +import { ColorIndicator, VizTooltipItem } from '@grafana/ui/src/components/VizTooltip/types'; import { getTitleFromHref } from 'app/features/explore/utils/links'; import { getStyles } from '../timeseries/TimeSeriesTooltip'; @@ -53,7 +53,7 @@ export const XYChartTooltip = ({ dataIdxs, seriesIdx, data, allSeries, dismiss, colorThing = colorThing[rowIndex]; } - const headerItem: LabelValue = { + const headerItem: VizTooltipItem = { label, value: '', // eslint-disable-next-line @typescript-eslint/consistent-type-assertions @@ -61,7 +61,7 @@ export const XYChartTooltip = ({ dataIdxs, seriesIdx, data, allSeries, dismiss, colorIndicator: ColorIndicator.marker_md, }; - const contentItems: LabelValue[] = [ + const contentItems: VizTooltipItem[] = [ { label: getFieldDisplayName(xField, frame), value: fmt(xField, xField.values[rowIndex]), @@ -101,8 +101,8 @@ export const XYChartTooltip = ({ dataIdxs, seriesIdx, data, allSeries, dismiss, return (
- - + + {isPinned && }
); From 5c60f4d468dc06c14f3fb16ad46cb44cdfe7fac8 Mon Sep 17 00:00:00 2001 From: Leon Sorokin Date: Tue, 27 Feb 2024 13:33:33 -0600 Subject: [PATCH 0246/1406] VizTooltip: Remove use of LayoutItemContext (#83542) --- .../plugins/annotations2/AnnotationEditor2.tsx | 17 ++--------------- .../plugins/annotations2/AnnotationTooltip2.tsx | 7 ++----- 2 files changed, 4 insertions(+), 20 deletions(-) diff --git a/public/app/plugins/panel/timeseries/plugins/annotations2/AnnotationEditor2.tsx b/public/app/plugins/panel/timeseries/plugins/annotations2/AnnotationEditor2.tsx index 1e01344b46..702711454e 100644 --- a/public/app/plugins/panel/timeseries/plugins/annotations2/AnnotationEditor2.tsx +++ b/public/app/plugins/panel/timeseries/plugins/annotations2/AnnotationEditor2.tsx @@ -1,19 +1,9 @@ import { css } from '@emotion/css'; -import React, { useContext, useEffect, useRef } from 'react'; +import React, { useRef } from 'react'; import { useAsyncFn, useClickAway } from 'react-use'; import { AnnotationEventUIModel, GrafanaTheme2, dateTimeFormat, systemDateFormats } from '@grafana/data'; -import { - Button, - Field, - Form, - HorizontalGroup, - InputControl, - LayoutItemContext, - TextArea, - usePanelContext, - useStyles2, -} from '@grafana/ui'; +import { Button, Field, Form, HorizontalGroup, InputControl, TextArea, usePanelContext, useStyles2 } from '@grafana/ui'; import { TagFilter } from 'app/core/components/TagFilter/TagFilter'; import { getAnnotationTags } from 'app/features/annotations/api'; @@ -37,9 +27,6 @@ export const AnnotationEditor2 = ({ annoVals, annoIdx, dismiss, timeZone, ...oth useClickAway(clickAwayRef, dismiss); - const layoutCtx = useContext(LayoutItemContext); - useEffect(() => layoutCtx.boostZIndex(), [layoutCtx]); - const [createAnnotationState, createAnnotation] = useAsyncFn(async (event: AnnotationEventUIModel) => { const result = await onAnnotationCreate!(event); dismiss(); diff --git a/public/app/plugins/panel/timeseries/plugins/annotations2/AnnotationTooltip2.tsx b/public/app/plugins/panel/timeseries/plugins/annotations2/AnnotationTooltip2.tsx index 0b72deb99f..c8c3deec31 100644 --- a/public/app/plugins/panel/timeseries/plugins/annotations2/AnnotationTooltip2.tsx +++ b/public/app/plugins/panel/timeseries/plugins/annotations2/AnnotationTooltip2.tsx @@ -1,8 +1,8 @@ import { css } from '@emotion/css'; -import React, { useContext, useEffect } from 'react'; +import React from 'react'; import { GrafanaTheme2, dateTimeFormat, systemDateFormats, textUtil } from '@grafana/data'; -import { HorizontalGroup, IconButton, LayoutItemContext, Tag, usePanelContext, useStyles2 } from '@grafana/ui'; +import { HorizontalGroup, IconButton, Tag, usePanelContext, useStyles2 } from '@grafana/ui'; import alertDef from 'app/features/alerting/state/alertDef'; interface Props { @@ -25,9 +25,6 @@ export const AnnotationTooltip2 = ({ annoVals, annoIdx, timeZone, onEdit }: Prop const canEdit = canEditAnnotations(dashboardUID); const canDelete = canDeleteAnnotations(dashboardUID) && onAnnotationDelete != null; - const layoutCtx = useContext(LayoutItemContext); - useEffect(() => layoutCtx.boostZIndex(), [layoutCtx]); - const timeFormatter = (value: number) => dateTimeFormat(value, { format: systemDateFormats.fullDate, From de75813d8df8b109c686ed9e579518a4c93b8800 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Tue, 27 Feb 2024 12:28:57 -0800 Subject: [PATCH 0247/1406] Chore: Remove the deprecated Vector type (#83469) --- .betterer.results | 35 +------- .../src/dataframe/MutableDataFrame.ts | 6 +- packages/grafana-data/src/index.ts | 4 +- .../src/transformations/fieldReducer.test.ts | 4 +- packages/grafana-data/src/types/dataFrame.ts | 9 +-- packages/grafana-data/src/types/vector.ts | 79 ------------------- .../src/vector/AppendedVectors.test.ts | 27 ------- .../src/vector/AppendedVectors.ts | 79 ------------------- .../src/vector/ArrayVector.test.ts | 45 ----------- .../grafana-data/src/vector/ArrayVector.ts | 46 ----------- .../grafana-data/src/vector/AsNumberVector.ts | 14 ---- .../src/vector/BinaryOperationVector.test.ts | 24 ------ .../src/vector/BinaryOperationVector.ts | 18 ----- .../grafana-data/src/vector/CircularVector.ts | 24 +++++- .../src/vector/ConstantVector.test.ts | 18 ----- .../grafana-data/src/vector/ConstantVector.ts | 10 --- .../src/vector/FormattedVector.ts | 14 ---- .../src/vector/FunctionalVector.ts | 14 ++-- .../grafana-data/src/vector/IndexVector.ts | 36 --------- .../src/vector/SortedVector.test.ts | 14 ---- .../grafana-data/src/vector/SortedVector.ts | 39 --------- packages/grafana-data/src/vector/index.ts | 11 --- .../grafana-data/src/vector/vectorToArray.ts | 10 --- .../datasource/loki/makeTableFrames.ts | 4 +- .../plugins/datasource/loki/sortDataFrame.ts | 10 ++- .../app/plugins/panel/datagrid/utils.test.ts | 14 ++-- public/app/plugins/panel/timeseries/utils.ts | 13 +-- 27 files changed, 58 insertions(+), 563 deletions(-) delete mode 100644 packages/grafana-data/src/vector/AppendedVectors.test.ts delete mode 100644 packages/grafana-data/src/vector/AppendedVectors.ts delete mode 100644 packages/grafana-data/src/vector/ArrayVector.test.ts delete mode 100644 packages/grafana-data/src/vector/ArrayVector.ts delete mode 100644 packages/grafana-data/src/vector/AsNumberVector.ts delete mode 100644 packages/grafana-data/src/vector/BinaryOperationVector.test.ts delete mode 100644 packages/grafana-data/src/vector/BinaryOperationVector.ts delete mode 100644 packages/grafana-data/src/vector/ConstantVector.test.ts delete mode 100644 packages/grafana-data/src/vector/ConstantVector.ts delete mode 100644 packages/grafana-data/src/vector/FormattedVector.ts delete mode 100644 packages/grafana-data/src/vector/IndexVector.ts delete mode 100644 packages/grafana-data/src/vector/SortedVector.test.ts delete mode 100644 packages/grafana-data/src/vector/SortedVector.ts delete mode 100644 packages/grafana-data/src/vector/index.ts delete mode 100644 packages/grafana-data/src/vector/vectorToArray.ts diff --git a/.betterer.results b/.betterer.results index 4eabd94f2c..9a2bd1a8c8 100644 --- a/.betterer.results +++ b/.betterer.results @@ -33,14 +33,10 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "5"], [0, 0, 0, "Unexpected any. Specify a different type.", "6"], [0, 0, 0, "Unexpected any. Specify a different type.", "7"], - [0, 0, 0, "Unexpected any. Specify a different type.", "8"], + [0, 0, 0, "Do not use any type assertions.", "8"], [0, 0, 0, "Unexpected any. Specify a different type.", "9"], [0, 0, 0, "Do not use any type assertions.", "10"], - [0, 0, 0, "Unexpected any. Specify a different type.", "11"], - [0, 0, 0, "Do not use any type assertions.", "12"], - [0, 0, 0, "Unexpected any. Specify a different type.", "13"], - [0, 0, 0, "Unexpected any. Specify a different type.", "14"], - [0, 0, 0, "Do not use any type assertions.", "15"] + [0, 0, 0, "Do not use any type assertions.", "11"] ], "packages/grafana-data/src/dataframe/StreamingDataFrame.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], @@ -369,11 +365,6 @@ exports[`better eslint`] = { "packages/grafana-data/src/types/variables.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], - "packages/grafana-data/src/types/vector.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Unexpected any. Specify a different type.", "2"] - ], "packages/grafana-data/src/utils/OptionsUIBuilders.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"], @@ -448,25 +439,9 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "3"], [0, 0, 0, "Unexpected any. Specify a different type.", "4"] ], - "packages/grafana-data/src/vector/AppendedVectors.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.", "2"] - ], - "packages/grafana-data/src/vector/ArrayVector.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Unexpected any. Specify a different type.", "2"] - ], "packages/grafana-data/src/vector/CircularVector.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], - "packages/grafana-data/src/vector/ConstantVector.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], - "packages/grafana-data/src/vector/FormattedVector.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], "packages/grafana-data/src/vector/FunctionalVector.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"], @@ -476,11 +451,7 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "5"], [0, 0, 0, "Unexpected any. Specify a different type.", "6"], [0, 0, 0, "Unexpected any. Specify a different type.", "7"], - [0, 0, 0, "Unexpected any. Specify a different type.", "8"], - [0, 0, 0, "Unexpected any. Specify a different type.", "9"] - ], - "packages/grafana-data/src/vector/SortedVector.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] + [0, 0, 0, "Unexpected any. Specify a different type.", "8"] ], "packages/grafana-data/test/__mocks__/pluginMocks.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] diff --git a/packages/grafana-data/src/dataframe/MutableDataFrame.ts b/packages/grafana-data/src/dataframe/MutableDataFrame.ts index 84a02a3367..4437aa44b5 100644 --- a/packages/grafana-data/src/dataframe/MutableDataFrame.ts +++ b/packages/grafana-data/src/dataframe/MutableDataFrame.ts @@ -11,7 +11,7 @@ import { guessFieldTypeFromValue, guessFieldTypeForField, toDataFrameDTO } from export type MutableField = Field; /** @deprecated */ -type MutableVectorCreator = (buffer?: any[]) => any[]; +type MutableVectorCreator = (buffer?: unknown[]) => unknown[]; export const MISSING_VALUE = undefined; // Treated as connected in new graph panel @@ -243,7 +243,7 @@ export class MutableDataFrame extends FunctionalVector implements Da throw new Error('Unable to set value beyond current length'); } - const obj = (value as any) || {}; + const obj = (value as Record) || {}; for (const field of this.fields) { field.values[index] = obj[field.name]; } @@ -253,7 +253,7 @@ export class MutableDataFrame extends FunctionalVector implements Da * Get an object with a property for each field in the DataFrame */ get(idx: number): T { - const v: any = {}; + const v: Record = {}; for (const field of this.fields) { v[field.name] = field.values[idx]; } diff --git a/packages/grafana-data/src/index.ts b/packages/grafana-data/src/index.ts index abdfdfce1b..4e3a847e1d 100644 --- a/packages/grafana-data/src/index.ts +++ b/packages/grafana-data/src/index.ts @@ -5,7 +5,6 @@ */ export * from './utils'; export * from './types'; -export * from './vector'; export * from './dataframe'; export * from './transformations'; export * from './datetime'; @@ -44,3 +43,6 @@ export { export { usePluginContext } from './context/plugins/usePluginContext'; export { isDataSourcePluginContext } from './context/plugins/guards'; export { getLinksSupplier } from './field/fieldOverrides'; + +// deprecated +export { CircularVector } from './vector/CircularVector'; diff --git a/packages/grafana-data/src/transformations/fieldReducer.test.ts b/packages/grafana-data/src/transformations/fieldReducer.test.ts index 2ca0e31cad..f96321508b 100644 --- a/packages/grafana-data/src/transformations/fieldReducer.test.ts +++ b/packages/grafana-data/src/transformations/fieldReducer.test.ts @@ -1,7 +1,7 @@ import { difference } from 'lodash'; import { createDataFrame, guessFieldTypeFromValue } from '../dataframe/processDataFrame'; -import { Field, FieldType, NullValueMode, Vector } from '../types/index'; +import { Field, FieldType, NullValueMode } from '../types/index'; import { fieldReducers, ReducerID, reduceField, defaultCalcs } from './fieldReducer'; @@ -65,7 +65,7 @@ describe('Stats Calculators', () => { it('should handle undefined field data without crashing', () => { const stats = reduceField({ - field: { name: 'a', values: undefined as unknown as Vector, config: {}, type: FieldType.number }, + field: { name: 'a', values: undefined as unknown as unknown[], config: {}, type: FieldType.number }, reducers: [ReducerID.first, ReducerID.last, ReducerID.mean, ReducerID.count], }); diff --git a/packages/grafana-data/src/types/dataFrame.ts b/packages/grafana-data/src/types/dataFrame.ts index b2fbf0ac46..28b4dbadf2 100644 --- a/packages/grafana-data/src/types/dataFrame.ts +++ b/packages/grafana-data/src/types/dataFrame.ts @@ -5,7 +5,6 @@ import { DecimalCount, DisplayProcessor, DisplayValue, DisplayValueAlignmentFact import { FieldColor } from './fieldColor'; import { ThresholdsConfig } from './thresholds'; import { ValueMapping } from './valueMapping'; -import { Vector } from './vector'; /** @public */ export enum FieldType { @@ -133,7 +132,7 @@ export interface ValueLinkConfig { valueRowIndex?: number; } -export interface Field> { +export interface Field { /** * Name of the field (column) */ @@ -149,10 +148,8 @@ export interface Field> { /** * The raw field values - * In Grafana 10, this accepts both simple arrays and the Vector interface - * In Grafana 11, the Vector interface will be removed */ - values: V | T[]; + values: T[]; /** * When type === FieldType.Time, this can optionally store @@ -264,7 +261,7 @@ export interface FieldDTO { name: string; // The column name type?: FieldType; config?: FieldConfig; - values?: Vector | T[]; // toJSON will always be T[], input could be either + values?: T[]; labels?: Labels; } diff --git a/packages/grafana-data/src/types/vector.ts b/packages/grafana-data/src/types/vector.ts index be903d96ab..06358553e9 100644 --- a/packages/grafana-data/src/types/vector.ts +++ b/packages/grafana-data/src/types/vector.ts @@ -53,82 +53,3 @@ export function patchArrayVectorProrotypeMethods() { } //this function call is intentional patchArrayVectorProrotypeMethods(); - -/** @deprecated use a simple Array */ -export interface Vector extends Array { - length: number; - - /** - * Access the value by index (Like an array) - * - * @deprecated use a simple Array - */ - get(index: number): T; - - /** - * Set a value - * - * @deprecated use a simple Array - */ - set: (index: number, value: T) => void; - - /** - * Adds the value to the vector - * Same as Array.push() - * - * @deprecated use a simple Array - */ - add: (value: T) => void; - - /** - * Get the results as an array. - * - * @deprecated use a simple Array - */ - toArray(): T[]; -} - -/** - * Apache arrow vectors are Read/Write - * - * @deprecated -- this is now part of the base Vector interface - */ -export interface ReadWriteVector extends Vector {} - -/** - * Vector with standard manipulation functions - * - * @deprecated -- this is now part of the base Vector interface - */ -export interface MutableVector extends ReadWriteVector {} - -/** - * This is an extremely inefficient Vector wrapper that allows vectors to - * be treated as arrays. We should avoid using this wrapper, but it is helpful - * for a clean migration to arrays - * - * @deprecated - */ -export function makeArrayIndexableVector(v: T): T { - return new Proxy(v, { - get(target: Vector, property: string, receiver: Vector) { - if (typeof property !== 'symbol') { - const idx = +property; - if (String(idx) === property) { - return target.get(idx); - } - } - return Reflect.get(target, property, receiver); - }, - set(target: Vector, property: string, value: unknown, receiver: Vector) { - if (typeof property !== 'symbol') { - const idx = +property; - if (String(idx) === property) { - target.set(idx, value); - return true; - } - } - return Reflect.set(target, property, value, receiver); - }, - }); -} diff --git a/packages/grafana-data/src/vector/AppendedVectors.test.ts b/packages/grafana-data/src/vector/AppendedVectors.test.ts deleted file mode 100644 index 29ef1dabe0..0000000000 --- a/packages/grafana-data/src/vector/AppendedVectors.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { AppendedVectors } from './AppendedVectors'; -import { ArrayVector } from './ArrayVector'; - -describe('Check Appending Vector', () => { - it('should transparently join them', () => { - jest.spyOn(console, 'warn').mockImplementation(); - const appended = new AppendedVectors(); - appended.append(new ArrayVector([1, 2, 3])); - appended.append(new ArrayVector([4, 5, 6])); - appended.append(new ArrayVector([7, 8, 9])); - expect(appended.length).toEqual(9); - expect(appended[0]).toEqual(1); - expect(appended[1]).toEqual(2); - expect(appended[100]).toEqual(undefined); - - appended.setLength(5); - expect(appended.length).toEqual(5); - appended.append(new ArrayVector(['a', 'b', 'c'])); - expect(appended.length).toEqual(8); - expect(appended.toArray()).toEqual([1, 2, 3, 4, 5, 'a', 'b', 'c']); - - appended.setLength(2); - appended.setLength(6); - appended.append(new ArrayVector(['x', 'y', 'z'])); - expect(appended.toArray()).toEqual([1, 2, undefined, undefined, undefined, undefined, 'x', 'y', 'z']); - }); -}); diff --git a/packages/grafana-data/src/vector/AppendedVectors.ts b/packages/grafana-data/src/vector/AppendedVectors.ts deleted file mode 100644 index af9873a69c..0000000000 --- a/packages/grafana-data/src/vector/AppendedVectors.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { Vector, makeArrayIndexableVector } from '../types/vector'; - -import { FunctionalVector } from './FunctionalVector'; -import { vectorToArray } from './vectorToArray'; - -interface AppendedVectorInfo { - start: number; - end: number; - values: Vector; -} - -/** - * This may be more trouble than it is worth. This trades some computation time for - * RAM -- rather than allocate a new array the size of all previous arrays, this just - * points the correct index to their original array values - * - * @deprecated use a simple Arrays. NOTE this is not used in grafana core - */ -export class AppendedVectors extends FunctionalVector { - length = 0; - source: Array> = []; - - constructor(startAt = 0) { - super(); - this.length = startAt; - return makeArrayIndexableVector(this); - } - - /** - * Make the vector look like it is this long - */ - setLength(length: number) { - if (length > this.length) { - // make the vector longer (filling with undefined) - this.length = length; - } else if (length < this.length) { - // make the array shorter - const sources: Array> = []; - for (const src of this.source) { - sources.push(src); - if (src.end > length) { - src.end = length; - break; - } - } - this.source = sources; - this.length = length; - } - } - - append(v: Vector): AppendedVectorInfo { - const info = { - start: this.length, - end: this.length + v.length, - values: v, - }; - this.length = info.end; - this.source.push(info); - return info; - } - - get(index: number): T { - for (let i = 0; i < this.source.length; i++) { - const src = this.source[i]; - if (index >= src.start && index < src.end) { - return src.values[index - src.start]; - } - } - return undefined as unknown as T; - } - - toArray(): T[] { - return vectorToArray(this); - } - - toJSON(): T[] { - return vectorToArray(this); - } -} diff --git a/packages/grafana-data/src/vector/ArrayVector.test.ts b/packages/grafana-data/src/vector/ArrayVector.test.ts deleted file mode 100644 index 3d9ae35fc5..0000000000 --- a/packages/grafana-data/src/vector/ArrayVector.test.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { Field, FieldType } from '../types'; - -import { ArrayVector } from './ArrayVector'; - -describe('ArrayVector', () => { - beforeEach(() => { - jest.spyOn(console, 'warn').mockImplementation(); - }); - - it('should init 150k with 65k Array.push() chonking', () => { - const arr = Array.from({ length: 150e3 }, (v, i) => i); - const av = new ArrayVector(arr); - - expect(av.toArray()).toEqual(arr); - }); - - it('should support add and push', () => { - const av = new ArrayVector(); - av.add(1); - av.push(2); - av.push(3, 4); - - expect(av.toArray()).toEqual([1, 2, 3, 4]); - }); - - it('typescript should not re-define the ArrayVector based on input to the constructor', () => { - const field: Field = { - name: 'test', - config: {}, - type: FieldType.number, - values: new ArrayVector(), // this defaults to `new ArrayVector()` - }; - expect(field).toBeDefined(); - - // Before collapsing Vector, ReadWriteVector, and MutableVector these all worked fine - field.values = new ArrayVector(); - field.values = new ArrayVector(undefined); - field.values = new ArrayVector([1, 2, 3]); - field.values = new ArrayVector([]); - field.values = new ArrayVector([1, undefined]); - field.values = new ArrayVector([null]); - field.values = new ArrayVector(['a', 'b', 'c']); - expect(field.values.length).toBe(3); - }); -}); diff --git a/packages/grafana-data/src/vector/ArrayVector.ts b/packages/grafana-data/src/vector/ArrayVector.ts deleted file mode 100644 index 01b615162b..0000000000 --- a/packages/grafana-data/src/vector/ArrayVector.ts +++ /dev/null @@ -1,46 +0,0 @@ -const notice = 'ArrayVector is deprecated and will be removed in Grafana 11. Please use plain arrays for field.values.'; -let notified = false; - -/** - * @public - * - * @deprecated use a simple Array - */ -export class ArrayVector extends Array { - get buffer() { - return this; - } - - set buffer(values: any[]) { - this.length = 0; - - const len = values?.length; - - if (len) { - let chonkSize = 65e3; - let numChonks = Math.ceil(len / chonkSize); - - for (let chonkIdx = 0; chonkIdx < numChonks; chonkIdx++) { - this.push.apply(this, values.slice(chonkIdx * chonkSize, (chonkIdx + 1) * chonkSize)); - } - } - } - - /** - * This any type is here to make the change type changes in v10 non breaking for plugins. - * Before you could technically assign field.values any typed ArrayVector no matter what the Field T type was. - */ - constructor(buffer?: any[]) { - super(); - this.buffer = buffer ?? []; - - if (!notified) { - console.warn(notice); - notified = true; - } - } - - toJSON(): T[] { - return [...this]; // copy to avoid circular reference (only for jest) - } -} diff --git a/packages/grafana-data/src/vector/AsNumberVector.ts b/packages/grafana-data/src/vector/AsNumberVector.ts deleted file mode 100644 index cc47ad3d7e..0000000000 --- a/packages/grafana-data/src/vector/AsNumberVector.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Vector } from '../types'; - -/** - * This will force all values to be numbers - * - * @public - * @deprecated use a simple Arrays. NOTE: Not used in grafana core - */ -export class AsNumberVector extends Array { - constructor(field: Vector) { - super(); - return field.map((v) => +v); - } -} diff --git a/packages/grafana-data/src/vector/BinaryOperationVector.test.ts b/packages/grafana-data/src/vector/BinaryOperationVector.test.ts deleted file mode 100644 index 10ab760f1b..0000000000 --- a/packages/grafana-data/src/vector/BinaryOperationVector.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { binaryOperators, BinaryOperationID } from '../utils/binaryOperators'; - -import { ArrayVector } from './ArrayVector'; -import { BinaryOperationVector } from './BinaryOperationVector'; -import { ConstantVector } from './ConstantVector'; - -describe('ScaledVector', () => { - it('should support multiply operations', () => { - jest.spyOn(console, 'warn').mockImplementation(); - const source = new ArrayVector([1, 2, 3, 4]); - const scale = 2.456; - const operation = binaryOperators.get(BinaryOperationID.Multiply).operation; - const v = new BinaryOperationVector(source, new ConstantVector(scale, source.length), operation); - expect(v.length).toEqual(source.length); - // Accessed with getters - for (let i = 0; i < 4; i++) { - expect(v.get(i)).toEqual(source.get(i) * scale); - } - // Accessed with array index - for (let i = 0; i < 4; i++) { - expect(v[i]).toEqual(source[i] * scale); - } - }); -}); diff --git a/packages/grafana-data/src/vector/BinaryOperationVector.ts b/packages/grafana-data/src/vector/BinaryOperationVector.ts deleted file mode 100644 index 4848e040e2..0000000000 --- a/packages/grafana-data/src/vector/BinaryOperationVector.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Vector } from '../types/vector'; -import { BinaryOperation } from '../utils/binaryOperators'; - -/** - * @public - * @deprecated use a simple Arrays. NOTE: Not used in grafana core - */ -export class BinaryOperationVector extends Array { - constructor(left: Vector, right: Vector, operation: BinaryOperation) { - super(); - - const arr = new Array(left.length); - for (let i = 0; i < arr.length; i++) { - arr[i] = operation(left[i], right[i]); - } - return arr; - } -} diff --git a/packages/grafana-data/src/vector/CircularVector.ts b/packages/grafana-data/src/vector/CircularVector.ts index fa3bd2503a..221f7c488e 100644 --- a/packages/grafana-data/src/vector/CircularVector.ts +++ b/packages/grafana-data/src/vector/CircularVector.ts @@ -1,5 +1,3 @@ -import { makeArrayIndexableVector } from '../types'; - import { FunctionalVector } from './FunctionalVector'; interface CircularOptions { @@ -36,7 +34,27 @@ export class CircularVector extends FunctionalVector { if (options.capacity) { this.setCapacity(options.capacity); } - return makeArrayIndexableVector(this); + return new Proxy(this, { + get(target: CircularVector, property: string, receiver: CircularVector) { + if (typeof property !== 'symbol') { + const idx = +property; + if (String(idx) === property) { + return target.get(idx); + } + } + return Reflect.get(target, property, receiver); + }, + set(target: CircularVector, property: string, value: T, receiver: CircularVector) { + if (typeof property !== 'symbol') { + const idx = +property; + if (String(idx) === property) { + target.set(idx, value); + return true; + } + } + return Reflect.set(target, property, value, receiver); + }, + }); } /** diff --git a/packages/grafana-data/src/vector/ConstantVector.test.ts b/packages/grafana-data/src/vector/ConstantVector.test.ts deleted file mode 100644 index 2d94fc3442..0000000000 --- a/packages/grafana-data/src/vector/ConstantVector.test.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { ConstantVector } from './ConstantVector'; - -describe('ConstantVector', () => { - it('should support constant values', () => { - const value = 3.5; - const v = new ConstantVector(value, 7); - expect(v.length).toEqual(7); - - expect(v.get(0)).toEqual(value); - expect(v.get(1)).toEqual(value); - - // Now check all of them - for (let i = 0; i < 7; i++) { - expect(v.get(i)).toEqual(value); - expect(v[i]).toEqual(value); - } - }); -}); diff --git a/packages/grafana-data/src/vector/ConstantVector.ts b/packages/grafana-data/src/vector/ConstantVector.ts deleted file mode 100644 index 0eb19fc763..0000000000 --- a/packages/grafana-data/src/vector/ConstantVector.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * @public - * @deprecated use a simple Arrays. NOTE: Not used in grafana core. - */ -export class ConstantVector extends Array { - constructor(value: T, len: number) { - super(); - return new Array(len).fill(value); - } -} diff --git a/packages/grafana-data/src/vector/FormattedVector.ts b/packages/grafana-data/src/vector/FormattedVector.ts deleted file mode 100644 index 8e4cd9be94..0000000000 --- a/packages/grafana-data/src/vector/FormattedVector.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { DisplayProcessor } from '../types'; -import { Vector } from '../types/vector'; -import { formattedValueToString } from '../valueFormats'; - -/** - * @public - * @deprecated use a simple Arrays. NOTE: not used in grafana core. - */ -export class FormattedVector extends Array { - constructor(source: Vector, formatter: DisplayProcessor) { - super(); - return source.map((v) => formattedValueToString(formatter(v))); - } -} diff --git a/packages/grafana-data/src/vector/FunctionalVector.ts b/packages/grafana-data/src/vector/FunctionalVector.ts index 5c2984c4b7..8322f96c30 100644 --- a/packages/grafana-data/src/vector/FunctionalVector.ts +++ b/packages/grafana-data/src/vector/FunctionalVector.ts @@ -1,12 +1,8 @@ -import { Vector } from '../types'; - -import { vectorToArray } from './vectorToArray'; - /** * @public * @deprecated use a simple Arrays */ -export abstract class FunctionalVector implements Vector { +export abstract class FunctionalVector { abstract get length(): number; abstract get(index: number): T; @@ -55,7 +51,11 @@ export abstract class FunctionalVector implements Vector { } toArray(): T[] { - return vectorToArray(this); + const arr = new Array(this.length); + for (let i = 0; i < this.length; i++) { + arr[i] = this.get(i); + } + return arr; } join(separator?: string | undefined): string { @@ -183,7 +183,7 @@ const emptyarray: any[] = []; * * @deprecated use a simple Arrays */ -export function vectorator(vector: Vector) { +function vectorator(vector: FunctionalVector) { return { *[Symbol.iterator]() { for (let i = 0; i < vector.length; i++) { diff --git a/packages/grafana-data/src/vector/IndexVector.ts b/packages/grafana-data/src/vector/IndexVector.ts deleted file mode 100644 index 1dfeb92ab3..0000000000 --- a/packages/grafana-data/src/vector/IndexVector.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Field, FieldType } from '../types'; - -/** - * IndexVector is a simple vector implementation that returns the index value - * for each element in the vector. It is functionally equivolant a vector backed - * by an array with values: `[0,1,2,...,length-1]` - * - * @deprecated use a simple Arrays. NOTE: not used in grafana core - */ -export class IndexVector extends Array { - constructor(len: number) { - super(); - const arr = new Array(len); - for (let i = 0; i < len; i++) { - arr[i] = i; - } - return arr; - } - - /** - * Returns a field representing the range [0 ... length-1] - * - * @deprecated - */ - static newField(len: number): Field { - return { - name: '', - values: new IndexVector(len), - type: FieldType.number, - config: { - min: 0, - max: len - 1, - }, - }; - } -} diff --git a/packages/grafana-data/src/vector/SortedVector.test.ts b/packages/grafana-data/src/vector/SortedVector.test.ts deleted file mode 100644 index 0d7a6c556a..0000000000 --- a/packages/grafana-data/src/vector/SortedVector.test.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { ArrayVector } from './ArrayVector'; -import { SortedVector } from './SortedVector'; - -describe('SortedVector', () => { - it('Should support sorting', () => { - jest.spyOn(console, 'warn').mockImplementation(); - const values = new ArrayVector([1, 5, 2, 4]); - const sorted = new SortedVector(values, [0, 2, 3, 1]); - expect(sorted.toArray()).toEqual([1, 2, 4, 5]); - - // The proxy should still be an instance of SortedVector (used in timeseries) - expect(sorted instanceof SortedVector).toBeTruthy(); - }); -}); diff --git a/packages/grafana-data/src/vector/SortedVector.ts b/packages/grafana-data/src/vector/SortedVector.ts deleted file mode 100644 index 8ed6467168..0000000000 --- a/packages/grafana-data/src/vector/SortedVector.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { makeArrayIndexableVector, Vector } from '../types/vector'; - -import { FunctionalVector } from './FunctionalVector'; -import { vectorToArray } from './vectorToArray'; - -/** - * Values are returned in the order defined by the input parameter - * - * @deprecated use a simple Arrays - */ -export class SortedVector extends FunctionalVector { - constructor( - private source: Vector, - private order: number[] - ) { - super(); - return makeArrayIndexableVector(this); - } - - get length(): number { - return this.source.length; - } - - get(index: number): T { - return this.source.get(this.order[index]); - } - - toArray(): T[] { - return vectorToArray(this); - } - - toJSON(): T[] { - return vectorToArray(this); - } - - getOrderArray(): number[] { - return this.order; - } -} diff --git a/packages/grafana-data/src/vector/index.ts b/packages/grafana-data/src/vector/index.ts deleted file mode 100644 index 5401189d27..0000000000 --- a/packages/grafana-data/src/vector/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -export * from './AppendedVectors'; -export * from './ArrayVector'; -export * from './CircularVector'; -export * from './ConstantVector'; -export * from './BinaryOperationVector'; -export * from './SortedVector'; -export * from './FormattedVector'; -export * from './IndexVector'; -export * from './AsNumberVector'; - -export { vectorator } from './FunctionalVector'; diff --git a/packages/grafana-data/src/vector/vectorToArray.ts b/packages/grafana-data/src/vector/vectorToArray.ts deleted file mode 100644 index 7ec23d9c26..0000000000 --- a/packages/grafana-data/src/vector/vectorToArray.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Vector } from '../types/vector'; - -/** @deprecated use a simple Arrays */ -export function vectorToArray(v: Vector): T[] { - const arr: T[] = Array(v.length); - for (let i = 0; i < v.length; i++) { - arr[i] = v.get(i); - } - return arr; -} diff --git a/public/app/plugins/datasource/loki/makeTableFrames.ts b/public/app/plugins/datasource/loki/makeTableFrames.ts index 217f3e9058..57224a3b0b 100644 --- a/public/app/plugins/datasource/loki/makeTableFrames.ts +++ b/public/app/plugins/datasource/loki/makeTableFrames.ts @@ -12,8 +12,8 @@ export function makeTableFrames(instantMetricFrames: DataFrame[]): DataFrame[] { return Object.entries(framesByRefId).map(([refId, frames]) => makeTableFrame(frames, refId)); } -type NumberField = Field; -type StringField = Field; +type NumberField = Field; +type StringField = Field; function makeTableFrame(instantMetricFrames: DataFrame[], refId: string): DataFrame { const tableTimeField: NumberField = { name: 'Time', config: {}, values: [], type: FieldType.time }; diff --git a/public/app/plugins/datasource/loki/sortDataFrame.ts b/public/app/plugins/datasource/loki/sortDataFrame.ts index 0d05531ed8..13f8e036cc 100644 --- a/public/app/plugins/datasource/loki/sortDataFrame.ts +++ b/public/app/plugins/datasource/loki/sortDataFrame.ts @@ -1,4 +1,4 @@ -import { DataFrame, Field, FieldType, SortedVector } from '@grafana/data'; +import { DataFrame, Field, FieldType } from '@grafana/data'; export enum SortDirection { Ascending, @@ -85,10 +85,12 @@ export function sortDataFrameByTime(frame: DataFrame, dir: SortDirection): DataF ...rest, fields: fields.map((field) => ({ ...field, - values: new SortedVector(field.values, index).toArray(), - nanos: field.nanos === undefined ? undefined : new SortedVector(field.nanos, index).toArray(), + values: sorted(field.values, index), + nanos: field.nanos === undefined ? undefined : sorted(field.nanos, index), })), }; +} - return frame; +function sorted(vals: T[], index: number[]): T[] { + return vals.map((_, idx) => vals[index[idx]]); } diff --git a/public/app/plugins/panel/datagrid/utils.test.ts b/public/app/plugins/panel/datagrid/utils.test.ts index 3f0a61e0c0..71a0c7ee11 100644 --- a/public/app/plugins/panel/datagrid/utils.test.ts +++ b/public/app/plugins/panel/datagrid/utils.test.ts @@ -1,4 +1,4 @@ -import { ArrayVector, DataFrame, FieldType } from '@grafana/data'; +import { DataFrame, FieldType } from '@grafana/data'; import { clearCellsFromRangeSelection, deleteRows } from './utils'; @@ -14,19 +14,19 @@ describe('when deleting rows', () => { { name: 'test1', type: FieldType.string, - values: new ArrayVector(['a', 'b', 'c', 'd', 'e']), + values: ['a', 'b', 'c', 'd', 'e'], config: {}, }, { name: 'test2', type: FieldType.number, - values: new ArrayVector([1, 2, 3, 4, 5]), + values: [1, 2, 3, 4, 5], config: {}, }, { name: 'test3', type: FieldType.string, - values: new ArrayVector(['a', 'b', 'c', 'd', 'e']), + values: ['a', 'b', 'c', 'd', 'e'], config: {}, }, ], @@ -98,19 +98,19 @@ describe('when clearing cells from range selection', () => { { name: 'test1', type: FieldType.string, - values: new ArrayVector(['a', 'b', 'c', 'd', 'e']), + values: ['a', 'b', 'c', 'd', 'e'], config: {}, }, { name: 'test2', type: FieldType.number, - values: new ArrayVector([1, 2, 3, 4, 5]), + values: [1, 2, 3, 4, 5], config: {}, }, { name: 'test3', type: FieldType.string, - values: new ArrayVector(['a', 'b', 'c', 'd', 'e']), + values: ['a', 'b', 'c', 'd', 'e'], config: {}, }, ], diff --git a/public/app/plugins/panel/timeseries/utils.ts b/public/app/plugins/panel/timeseries/utils.ts index a7d24f820e..8889a49412 100644 --- a/public/app/plugins/panel/timeseries/utils.ts +++ b/public/app/plugins/panel/timeseries/utils.ts @@ -8,7 +8,6 @@ import { DataLinkPostProcessor, InterpolateFunction, isBooleanUnit, - SortedVector, TimeRange, cacheFieldDisplayNames, } from '@grafana/data'; @@ -279,20 +278,10 @@ export function regenerateLinksSupplier( return; } - /* check if field has sortedVector values - if it does, sort all string fields in the original frame by the order array already used for the field - otherwise just attach the fields to the temporary frame used to get the links - */ const tempFields: Field[] = []; for (const frameField of frames[field.state?.origin?.frameIndex].fields) { if (frameField.type === FieldType.string) { - if (field.values instanceof SortedVector) { - const copiedField = { ...frameField }; - copiedField.values = new SortedVector(frameField.values, field.values.getOrderArray()); - tempFields.push(copiedField); - } else { - tempFields.push(frameField); - } + tempFields.push(frameField); } } From d8b7992c0cd6abe45ef519314d65b75e5d183237 Mon Sep 17 00:00:00 2001 From: Sven Grossmann Date: Tue, 27 Feb 2024 21:59:38 +0100 Subject: [PATCH 0248/1406] Loki: Fix failing test waiting for `Loading...` copy (#83547) fix monaco tests --- .../datasource/loki/components/LokiQueryField.test.tsx | 10 +++++++--- .../monaco-query-field/MonacoFieldWrapper.test.tsx | 6 ++++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/public/app/plugins/datasource/loki/components/LokiQueryField.test.tsx b/public/app/plugins/datasource/loki/components/LokiQueryField.test.tsx index 0e46eb28a9..5c304700ff 100644 --- a/public/app/plugins/datasource/loki/components/LokiQueryField.test.tsx +++ b/public/app/plugins/datasource/loki/components/LokiQueryField.test.tsx @@ -1,4 +1,4 @@ -import { render, screen } from '@testing-library/react'; +import { render, screen, waitFor } from '@testing-library/react'; import React, { ComponentProps } from 'react'; import { dateTime } from '@grafana/data'; @@ -33,7 +33,9 @@ describe('LokiQueryField', () => { it('refreshes metrics when time range changes over 1 minute', async () => { const { rerender } = render(); - expect(await screen.findByText('Loading...')).toBeInTheDocument(); + await waitFor(async () => { + expect(await screen.findByText('Loading...')).toBeInTheDocument(); + }); expect(props.datasource.languageProvider.fetchLabels).not.toHaveBeenCalled(); @@ -55,7 +57,9 @@ describe('LokiQueryField', () => { it('does not refreshes metrics when time range change by less than 1 minute', async () => { const { rerender } = render(); - expect(await screen.findByText('Loading...')).toBeInTheDocument(); + await waitFor(async () => { + expect(await screen.findByText('Loading...')).toBeInTheDocument(); + }); expect(props.datasource.languageProvider.fetchLabels).not.toHaveBeenCalled(); diff --git a/public/app/plugins/datasource/loki/components/monaco-query-field/MonacoFieldWrapper.test.tsx b/public/app/plugins/datasource/loki/components/monaco-query-field/MonacoFieldWrapper.test.tsx index 15c25be1d6..7ff5671f8c 100644 --- a/public/app/plugins/datasource/loki/components/monaco-query-field/MonacoFieldWrapper.test.tsx +++ b/public/app/plugins/datasource/loki/components/monaco-query-field/MonacoFieldWrapper.test.tsx @@ -1,4 +1,4 @@ -import { render, screen } from '@testing-library/react'; +import { render, screen, waitFor } from '@testing-library/react'; import React from 'react'; import { createLokiDatasource } from '../../__mocks__/datasource'; @@ -24,6 +24,8 @@ describe('MonacoFieldWrapper', () => { test('Renders with no errors', async () => { renderComponent(); - expect(await screen.findByText('Loading...')).toBeInTheDocument(); + await waitFor(async () => { + expect(await screen.findByText('Loading...')).toBeInTheDocument(); + }); }); }); From 70009201d44c2d0ab39cc77081808a69a6c4fd63 Mon Sep 17 00:00:00 2001 From: Scott Lepper Date: Tue, 27 Feb 2024 16:16:00 -0500 Subject: [PATCH 0249/1406] Expressions: Sql expressions with Duckdb (#81666) duckdb temp storage of dataframes using parquet and querying from sql expressions --------- Co-authored-by: Ryan McKinley --- .../feature-toggles/index.md | 1 + go.mod | 11 +- go.sum | 21 ++++ go.work.sum | 5 + .../src/types/featureToggles.gen.ts | 1 + pkg/expr/commands.go | 6 + pkg/expr/graph.go | 40 +++++++ pkg/expr/mathexp/parse/node.go | 4 + pkg/expr/mathexp/types.go | 45 ++++++++ pkg/expr/models.go | 8 ++ pkg/expr/nodes.go | 34 ++++-- pkg/expr/reader.go | 8 ++ pkg/expr/service.go | 5 +- pkg/expr/sql/parser.go | 99 ++++++++++++++++ pkg/expr/sql/parser_test.go | 58 ++++++++++ pkg/expr/sql_command.go | 107 ++++++++++++++++++ pkg/expr/sql_command_test.go | 26 +++++ pkg/services/featuremgmt/registry.go | 7 ++ pkg/services/featuremgmt/toggles_gen.csv | 1 + pkg/services/featuremgmt/toggles_gen.go | 4 + pkg/services/featuremgmt/toggles_gen.json | 25 +++- .../unified/GrafanaRuleQueryViewer.tsx | 4 + .../components/expressions/Expression.tsx | 4 + .../alerting/unified/utils/timeRange.ts | 1 + .../expressions/ExpressionQueryEditor.tsx | 7 ++ .../expressions/components/SqlExpr.tsx | 27 +++++ public/app/features/expressions/types.ts | 16 ++- 27 files changed, 555 insertions(+), 20 deletions(-) create mode 100644 pkg/expr/sql/parser.go create mode 100644 pkg/expr/sql/parser_test.go create mode 100644 pkg/expr/sql_command.go create mode 100644 pkg/expr/sql_command_test.go create mode 100644 public/app/features/expressions/components/SqlExpr.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 0c10036bdd..60d1418a52 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -173,6 +173,7 @@ Experimental features might be changed or removed without prior notice. | `newFolderPicker` | Enables the nested folder picker without having nested folders enabled | | `onPremToCloudMigrations` | In-development feature that will allow users to easily migrate their on-prem Grafana instances to Grafana Cloud. | | `promQLScope` | In-development feature that will allow injection of labels into prometheus queries. | +| `sqlExpressions` | Enables using SQL and DuckDB functions as Expressions. | | `nodeGraphDotLayout` | Changed the layout algorithm for the node graph | | `newPDFRendering` | New implementation for the dashboard to PDF rendering | | `kubernetesAggregator` | Enable grafana aggregator | diff --git a/go.mod b/go.mod index 0c8c0e11ae..061044a1a8 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/grafana/grafana -go 1.21 +go 1.21.0 // Override docker/docker to avoid: // go: github.com/drone-runners/drone-runner-docker@v1.8.2 requires @@ -92,6 +92,7 @@ require ( github.com/prometheus/prometheus v1.8.2-0.20221021121301-51a44e6657c3 // @grafana/alerting-squad-backend github.com/robfig/cron/v3 v3.0.1 // @grafana/backend-platform github.com/russellhaering/goxmldsig v1.4.0 // @grafana/backend-platform + github.com/scottlepp/go-duck v0.0.15 // @grafana/grafana-app-platform-squad github.com/stretchr/testify v1.8.4 // @grafana/backend-platform github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf // @grafana/backend-platform github.com/ua-parser/uap-go v0.0.0-20211112212520-00c877edfe0f // @grafana/backend-platform @@ -463,6 +464,9 @@ require github.com/spyzhov/ajson v0.9.0 // @grafana/grafana-app-platform-squad require github.com/fullstorydev/grpchan v1.1.1 // @grafana/backend-platform require ( + github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c // indirect + github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40 // indirect + github.com/apache/thrift v0.18.1 // indirect github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240226124929-648abdbd0ea4 // @grafana/grafana-app-platform-squad github.com/grafana/grafana/pkg/apiserver v0.0.0-20240226124929-648abdbd0ea4 // @grafana/grafana-app-platform-squad ) @@ -471,12 +475,17 @@ require ( github.com/bufbuild/protocompile v0.4.0 // indirect github.com/grafana/sqlds/v3 v3.2.0 // indirect github.com/jhump/protoreflect v1.15.1 // indirect + github.com/klauspost/asmfmt v1.3.2 // indirect + github.com/krasun/gosqlparser v1.0.5 // @grafana/grafana-app-platform-squad + github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 // indirect + github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mithrandie/csvq v1.17.10 // indirect github.com/mithrandie/csvq-driver v1.6.8 // indirect github.com/mithrandie/go-file/v2 v2.1.0 // indirect github.com/mithrandie/go-text v1.5.4 // indirect github.com/mithrandie/ternary v1.1.1 // indirect + github.com/xwb1989/sqlparser v0.0.0-20180606152119-120387863bf2 // @grafana/grafana-app-platform-squad ) // Use fork of crewjam/saml with fixes for some issues until changes get merged into upstream diff --git a/go.sum b/go.sum index 988032666b..a6aa605d07 100644 --- a/go.sum +++ b/go.sum @@ -1288,6 +1288,7 @@ github.com/GoogleCloudPlatform/cloudsql-proxy v1.29.0/go.mod h1:spvB9eLJH9dutlbP github.com/HdrHistogram/hdrhistogram-go v1.1.0/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= github.com/HdrHistogram/hdrhistogram-go v1.1.2 h1:5IcZpTvzydCQeHzK4Ef/D5rrSqwxob0t8PQPMybUNFM= github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= +github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c h1:RGWPOewvKIROun94nF7v2cua9qP+thov/7M50KEoeSU= github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk= github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= @@ -1354,6 +1355,8 @@ github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kd github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df h1:7RFfzj4SSt6nnvCPbCqijJi1nWCd+TqAT3bYCStRC18= github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df/go.mod h1:pSwJ0fSY5KhvocuWSx4fz3BA8OrA1bQn+K1Eli3BRwM= github.com/apache/arrow/go/arrow v0.0.0-20210223225224-5bea62493d91/go.mod h1:c9sxoIT3YgLxH4UhLOCKaBlEojuMhVYpk4Ntv3opUTQ= +github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40 h1:q4dksr6ICHXqG5hm0ZW5IHyeEJXoIJSOZeBLmWPNeIQ= +github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40/go.mod h1:Q7yQnSMnLvcXlZ8RV+jwz/6y1rQTqbX6C82SndT52Zs= github.com/apache/arrow/go/v10 v10.0.1/go.mod h1:YvhnlEePVnBS4+0z3fhPfUy7W1Ikj0Ih0vcRo/gZ1M0= github.com/apache/arrow/go/v11 v11.0.0/go.mod h1:Eg5OsL5H+e299f7u5ssuXsuHQVEGC4xei5aX110hRiI= github.com/apache/arrow/go/v12 v12.0.0/go.mod h1:d+tV/eHZZ7Dz7RPrFKtPK02tpr+c9/PEd/zm8mDS9Vg= @@ -1362,10 +1365,14 @@ github.com/apache/arrow/go/v15 v15.0.0/go.mod h1:DGXsR3ajT524njufqf95822i+KTh+ye github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/apache/thrift v0.16.0/go.mod h1:PHK3hniurgQaNMZYaCLEqXKsYK8upmhPbmdP2FXSqgU= +github.com/apache/thrift v0.18.1 h1:lNhK/1nqjbwbiOPDBPFJVKxgDEGSepKuTh6OLiXW8kg= +github.com/apache/thrift v0.18.1/go.mod h1:rdQn/dCcDKEWjjylUeueum4vQEjG2v8v2PqriUnbr+I= github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw= github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= +github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhPwqqXc4/vE0f7GvRjuAsbW+HOIe8KnA= +github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= @@ -2026,6 +2033,7 @@ github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl76 github.com/google/cel-go v0.17.7 h1:6ebJFzu1xO2n7TLtN+UBqShGBhlD85bhvglh5DpcfqQ= github.com/google/cel-go v0.17.7/go.mod h1:HXZKzB0LXqer5lHHgfWAnlYwJaQBDKMjxjulNQzhwhY= github.com/google/flatbuffers v1.11.0/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/flatbuffers v2.0.0+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/flatbuffers v2.0.8+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/flatbuffers v23.5.26+incompatible h1:M9dgRyhJemaM4Sw8+66GHBu8ioaQmyPLg1b8VwK5WJg= github.com/google/flatbuffers v23.5.26+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= @@ -2447,10 +2455,12 @@ github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/asmfmt v1.3.2 h1:4Ri7ox3EwapiOjCki+hw14RyKk201CN4rzyCJRFLpK4= github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE= github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.9.7/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.13.1/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.15.1/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= @@ -2480,6 +2490,8 @@ github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/krasun/gosqlparser v1.0.5 h1:sHaexkxGb9NrAcjZ3mUs6u33iJ9qhR2fH7XrpZekMt8= +github.com/krasun/gosqlparser v1.0.5/go.mod h1:aXCTW1xnPl4qAaNROeqESauGJ8sqhoB4OFEIOVIDYI4= github.com/kshvakov/clickhouse v1.3.5/go.mod h1:DMzX7FxRymoNkVgizH0DWAL8Cur7wHLgx3MUnGwJqpE= github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= @@ -2586,7 +2598,9 @@ github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJys github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4= github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM= github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk= +github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 h1:AMFGa4R4MiIpspGNG7Z948v4n35fFGB3RR3G/ry4FWs= github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY= +github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 h1:+n/aFZefKZp7spd8DFdX7uMikMLXX4oubIzJF4kv/wI= github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE= github.com/minio/highwayhash v1.0.1/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY= github.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY= @@ -2779,6 +2793,7 @@ github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pierrec/lz4/v4 v4.1.8/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ= github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= @@ -2920,6 +2935,9 @@ github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdh github.com/scaleway/scaleway-sdk-go v1.0.0-beta.21 h1:yWfiTPwYxB0l5fGMhl/G+liULugVIHD9AU77iNLrURQ= github.com/scaleway/scaleway-sdk-go v1.0.0-beta.21/go.mod h1:fCa7OJZ/9DRTnOKmxvT6pn+LPWUptQAmHF/SBJUGEcg= github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= +github.com/scottlepp/go-duck v0.0.14 h1:fKrhE31OiwMJkS8byu0CWUfuWMfdxZYJNp5FUgHil4M= +github.com/scottlepp/go-duck v0.0.14/go.mod h1:GL+hHuKdueJRrFCduwBc7A7TQk+Tetc5BPXPVtduihY= +github.com/scottlepp/go-duck v0.0.15/go.mod h1:GL+hHuKdueJRrFCduwBc7A7TQk+Tetc5BPXPVtduihY= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg= @@ -3085,6 +3103,8 @@ github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= +github.com/xwb1989/sqlparser v0.0.0-20180606152119-120387863bf2 h1:zzrxE1FKn5ryBNl9eKOeqQ58Y/Qpo3Q9QNxKHX5uzzQ= +github.com/xwb1989/sqlparser v0.0.0-20180606152119-120387863bf2/go.mod h1:hzfGeIUDq/j97IG+FhNqkowIyEcD88LrW6fyU3K3WqY= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= github.com/yalue/merged_fs v1.2.2 h1:vXHTpJBluJryju7BBpytr3PDIkzsPMpiEknxVGPhN/I= github.com/yalue/merged_fs v1.2.2/go.mod h1:WqqchfVYQyclV2tnR7wtRhBddzBvLVR83Cjw9BKQw0M= @@ -3992,6 +4012,7 @@ google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxH google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= +google.golang.org/genproto v0.0.0-20210630183607-d20f26d13c79/go.mod h1:yiaVoXHpRzHGyxV3o4DktVWY4mSUErTKaeEOq6C3t3U= google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= diff --git a/go.work.sum b/go.work.sum index 57235ef9f2..d8aa300bdf 100644 --- a/go.work.sum +++ b/go.work.sum @@ -566,6 +566,7 @@ github.com/performancecopilot/speed/v4 v4.0.0 h1:VxEDCmdkfbQYDlcr/GC9YoN9PQ6p8ul github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= github.com/phpdave11/gofpdf v1.4.2 h1:KPKiIbfwbvC/wOncwhrpRdXVj2CZTCFlw4wnoyjtHfQ= github.com/phpdave11/gofpdi v1.0.13 h1:o61duiW8M9sMlkVXWlvP92sZJtGKENvW3VExs6dZukQ= +github.com/pborman/uuid v1.2.0 h1:J7Q5mO4ysT1dv8hyrUGHb9+ooztCXu1D8MY8DZYsu3g= github.com/pierrec/lz4 v2.0.5+incompatible h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM8aXeqhl0I= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgcC4zIxODThtZNPirFr42+A= github.com/pkg/profile v1.2.1 h1:F++O52m40owAmADcojzM+9gyjmMOY/T4oYJkgFDH8RE= @@ -596,6 +597,8 @@ github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da h1:p3Vo3i64TCL github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiyyjYS17cCYRqP13/SHk= github.com/segmentio/fasthash v0.0.0-20180216231524-a72b379d632e h1:uO75wNGioszjmIzcY/tvdDYKRLVvzggtAmmJkn9j4GQ= +github.com/scottlepp/go-duck v0.0.15 h1:qrSF3pXlXAA4a7uxAfLYajqXLkeBjv8iW1wPdSfkMj0= +github.com/scottlepp/go-duck v0.0.15/go.mod h1:GL+hHuKdueJRrFCduwBc7A7TQk+Tetc5BPXPVtduihY= github.com/segmentio/fasthash v0.0.0-20180216231524-a72b379d632e/go.mod h1:tm/wZFQ8e24NYaBGIlnO2WGCAi67re4HHuOm0sftE/M= github.com/segmentio/parquet-go v0.0.0-20230427215636-d483faba23a5 h1:7CWCjaHrXSUCHrRhIARMGDVKdB82tnPAQMmANeflKOw= github.com/segmentio/parquet-go v0.0.0-20230427215636-d483faba23a5/go.mod h1:+J0xQnJjm8DuQUHBO7t57EnmPbstT6+b45+p3DC9k1Q= @@ -747,6 +750,8 @@ gopkg.in/resty.v1 v1.12.0 h1:CuXP0Pjfw9rOuY6EP+UvtNvt5DSqHpIxILZKT/quCZI= gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg= +gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= +gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98= gopkg.in/telebot.v3 v3.2.1 h1:3I4LohaAyJBiivGmkfB+CiVu7QFOWkuZ4+KHgO/G3rs= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 48f8b5b5f9..0d8527653b 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -171,6 +171,7 @@ export interface FeatureToggles { onPremToCloudMigrations?: boolean; alertingSaveStatePeriodic?: boolean; promQLScope?: boolean; + sqlExpressions?: boolean; nodeGraphDotLayout?: boolean; groupToNestedTableTransformation?: boolean; newPDFRendering?: boolean; diff --git a/pkg/expr/commands.go b/pkg/expr/commands.go index 32bc9b8636..1f8926102c 100644 --- a/pkg/expr/commands.go +++ b/pkg/expr/commands.go @@ -324,6 +324,8 @@ const ( TypeClassicConditions // TypeThreshold is the CMDType for checking if a threshold has been crossed TypeThreshold + // TypeSQL is the CMDType for running SQL expressions + TypeSQL ) func (gt CommandType) String() string { @@ -336,6 +338,8 @@ func (gt CommandType) String() string { return "resample" case TypeClassicConditions: return "classic_conditions" + case TypeSQL: + return "sql" default: return "unknown" } @@ -354,6 +358,8 @@ func ParseCommandType(s string) (CommandType, error) { return TypeClassicConditions, nil case "threshold": return TypeThreshold, nil + case "sql": + return TypeSQL, nil default: return TypeUnknown, fmt.Errorf("'%v' is not a recognized expression type", s) } diff --git a/pkg/expr/graph.go b/pkg/expr/graph.go index 2b09143110..49c90b6221 100644 --- a/pkg/expr/graph.go +++ b/pkg/expr/graph.go @@ -75,6 +75,8 @@ func (dp *DataPipeline) execute(c context.Context, now time.Time, s *Service) (m executeDSNodesGrouped(c, now, vars, s, dsNodes) } + s.allowLongFrames = hasSqlExpression(*dp) + for _, node := range *dp { if groupByDSFlag && node.NodeType() == TypeDatasourceNode { continue // already executed via executeDSNodesGrouped @@ -266,6 +268,10 @@ func buildGraphEdges(dp *simple.DirectedGraph, registry map[string]Node) error { for _, neededVar := range cmdNode.Command.NeedsVars() { neededNode, ok := registry[neededVar] if !ok { + _, ok := cmdNode.Command.(*SQLCommand) + if ok { + continue + } return fmt.Errorf("unable to find dependent node '%v'", neededVar) } @@ -312,3 +318,37 @@ func GetCommandsFromPipeline[T Command](pipeline DataPipeline) []T { } return results } + +func hasSqlExpression(dp DataPipeline) bool { + for _, node := range dp { + if node.NodeType() == TypeCMDNode { + cmdNode := node.(*CMDNode) + _, ok := cmdNode.Command.(*SQLCommand) + if ok { + return true + } + } + } + return false +} + +// func graphHasSqlExpresssion(dp *simple.DirectedGraph) bool { +// node := dp.Nodes() +// for node.Next() { +// if cmdNode, ok := node.Node().(*CMDNode); ok { +// // res[dpNode.RefID()] = dpNode +// _, ok := cmdNode.Command.(*SQLCommand) +// if ok { +// return true +// } +// } +// // if node.NodeType() == TypeCMDNode { +// // cmdNode := node.(*CMDNode) +// // _, ok := cmdNode.Command.(*SQLCommand) +// // if ok { +// // return true +// // } +// // } +// } +// return false +// } diff --git a/pkg/expr/mathexp/parse/node.go b/pkg/expr/mathexp/parse/node.go index 41feeb7026..02c297dde9 100644 --- a/pkg/expr/mathexp/parse/node.go +++ b/pkg/expr/mathexp/parse/node.go @@ -405,6 +405,8 @@ const ( TypeVariantSet // TypeNoData is a no data response without a known data type. TypeNoData + // TypeTableData is a tabular data response. + TypeTableData ) // String returns a string representation of the ReturnType. @@ -422,6 +424,8 @@ func (f ReturnType) String() string { return "variant" case TypeNoData: return "noData" + case TypeTableData: + return "tableData" default: return "unknown" } diff --git a/pkg/expr/mathexp/types.go b/pkg/expr/mathexp/types.go index ca5069650b..a5b5ba456a 100644 --- a/pkg/expr/mathexp/types.go +++ b/pkg/expr/mathexp/types.go @@ -246,3 +246,48 @@ func (s NoData) New() NoData { func NewNoData() NoData { return NoData{data.NewFrame("no data")} } + +// TableData is an untyped no data response. +type TableData struct{ Frame *data.Frame } + +// Type returns the Value type and allows it to fulfill the Value interface. +func (s TableData) Type() parse.ReturnType { return parse.TypeTableData } + +// Value returns the actual value allows it to fulfill the Value interface. +func (s TableData) Value() any { return s } + +func (s TableData) GetLabels() data.Labels { return nil } + +func (s TableData) SetLabels(ls data.Labels) {} + +func (s TableData) GetMeta() any { + return s.Frame.Meta.Custom +} + +func (s TableData) SetMeta(v any) { + m := s.Frame.Meta + if m == nil { + m = &data.FrameMeta{} + s.Frame.SetMeta(m) + } + m.Custom = v +} + +func (s TableData) AddNotice(notice data.Notice) { + m := s.Frame.Meta + if m == nil { + m = &data.FrameMeta{} + s.Frame.SetMeta(m) + } + m.Notices = append(m.Notices, notice) +} + +func (s TableData) AsDataFrame() *data.Frame { return s.Frame } + +func (s TableData) New() TableData { + return NewTableData() +} + +func NewTableData() TableData { + return TableData{data.NewFrame("")} +} diff --git a/pkg/expr/models.go b/pkg/expr/models.go index 4480331a51..dec7e3ce3a 100644 --- a/pkg/expr/models.go +++ b/pkg/expr/models.go @@ -24,6 +24,9 @@ const ( // Threshold QueryTypeThreshold QueryType = "threshold" + + // SQL query via DuckDB + QueryTypeSQL QueryType = "sql" ) type MathQuery struct { @@ -69,6 +72,11 @@ type ClassicQuery struct { Conditions []classic.ConditionJSON `json:"conditions"` } +// SQLQuery requires the sqlExpression feature flag +type SQLExpression struct { + Expression string `json:"expression" jsonschema:"minLength=1,example=SELECT * FROM A LIMIT 1"` +} + //------------------------------- // Non-query commands //------------------------------- diff --git a/pkg/expr/nodes.go b/pkg/expr/nodes.go index 3f20267d71..8ecf303cc4 100644 --- a/pkg/expr/nodes.go +++ b/pkg/expr/nodes.go @@ -155,6 +155,8 @@ func buildCMDNode(rn *rawNode, toggles featuremgmt.FeatureToggles) (*CMDNode, er node.Command, err = classic.UnmarshalConditionsCmd(rn.Query, rn.RefID) case TypeThreshold: node.Command, err = UnmarshalThresholdCommand(rn, toggles) + case TypeSQL: + node.Command, err = UnmarshalSQLCommand(rn) default: return nil, fmt.Errorf("expression command type '%v' in expression '%v' not implemented", commandType, rn.RefID) } @@ -471,7 +473,8 @@ func convertDataFramesToResults(ctx context.Context, frames data.Frames, datasou logger.Warn("Ignoring InfluxDB data frame due to missing numeric fields") continue } - if schema.Type != data.TimeSeriesTypeWide { + + if schema.Type != data.TimeSeriesTypeWide && !s.allowLongFrames { return "", mathexp.Results{}, fmt.Errorf("input data must be a wide series but got type %s (input refid)", schema.Type) } filtered = append(filtered, frame) @@ -484,20 +487,29 @@ func convertDataFramesToResults(ctx context.Context, frames data.Frames, datasou maybeFixerFn := checkIfSeriesNeedToBeFixed(filtered, datasourceType) - vals := make([]mathexp.Value, 0, totalLen) - for _, frame := range filtered { - series, err := WideToMany(frame, maybeFixerFn) - if err != nil { - return "", mathexp.Results{}, err - } - for _, ser := range series { - vals = append(vals, ser) - } - } dataType := "single frame series" if len(filtered) > 1 { dataType = "multi frame series" } + + vals := make([]mathexp.Value, 0, totalLen) + for _, frame := range filtered { + schema := frame.TimeSeriesSchema() + if schema.Type == data.TimeSeriesTypeWide { + series, err := WideToMany(frame, maybeFixerFn) + if err != nil { + return "", mathexp.Results{}, err + } + for _, ser := range series { + vals = append(vals, ser) + } + } else { + v := mathexp.TableData{Frame: frame} + vals = append(vals, v) + dataType = "single frame" + } + } + return dataType, mathexp.Results{ Values: vals, }, nil diff --git a/pkg/expr/reader.go b/pkg/expr/reader.go index e5563bb95d..b92e704485 100644 --- a/pkg/expr/reader.go +++ b/pkg/expr/reader.go @@ -30,6 +30,7 @@ func NewExpressionQueryReader(features featuremgmt.FeatureToggles) (*ExpressionQ } // ReadQuery implements query.TypedQueryHandler. +// nolint:gocyclo func (h *ExpressionQueryReader) ReadQuery( // Properties that have been parsed off the same node common *rawNode, // common query.CommonQueryProperties @@ -102,6 +103,13 @@ func (h *ExpressionQueryReader) ReadQuery( eq.Command, err = classic.NewConditionCmd(common.RefID, q.Conditions) } + case QueryTypeSQL: + q := &SQLExpression{} + err = iter.ReadVal(q) + if err == nil { + eq.Command, err = NewSQLCommand(common.RefID, q.Expression, common.TimeRange) + } + case QueryTypeThreshold: q := &ThresholdQuery{} err = iter.ReadVal(q) diff --git a/pkg/expr/service.go b/pkg/expr/service.go index ffd7e3ffd6..978ee14721 100644 --- a/pkg/expr/service.go +++ b/pkg/expr/service.go @@ -63,8 +63,9 @@ type Service struct { pluginsClient backend.CallResourceHandler - tracer tracing.Tracer - metrics *metrics + tracer tracing.Tracer + metrics *metrics + allowLongFrames bool } type pluginContextProvider interface { diff --git a/pkg/expr/sql/parser.go b/pkg/expr/sql/parser.go new file mode 100644 index 0000000000..d5ea2d1f6b --- /dev/null +++ b/pkg/expr/sql/parser.go @@ -0,0 +1,99 @@ +package sql + +import ( + "errors" + "strings" + + parser "github.com/krasun/gosqlparser" + "github.com/xwb1989/sqlparser" +) + +// TablesList returns a list of tables for the sql statement +func TablesList(rawSQL string) ([]string, error) { + stmt, err := sqlparser.Parse(rawSQL) + if err != nil { + tables, err := parse(rawSQL) + if err != nil { + return parseTables(rawSQL) + } + return tables, nil + } + + tables := []string{} + switch kind := stmt.(type) { + case *sqlparser.Select: + for _, t := range kind.From { + buf := sqlparser.NewTrackedBuffer(nil) + t.Format(buf) + table := buf.String() + if table != "dual" { + tables = append(tables, buf.String()) + } + } + default: + return nil, errors.New("not a select statement") + } + return tables, nil +} + +// uses a simple tokenizer +func parse(rawSQL string) ([]string, error) { + query, err := parser.Parse(rawSQL) + if err != nil { + return nil, err + } + if query.GetType() == parser.StatementSelect { + sel, ok := query.(*parser.Select) + if ok { + return []string{sel.Table}, nil + } + } + return nil, err +} + +func parseTables(rawSQL string) ([]string, error) { + checkSql := strings.ToUpper(rawSQL) + if strings.HasPrefix(checkSql, "SELECT") || strings.HasPrefix(rawSQL, "WITH") { + tables := []string{} + tokens := strings.Split(rawSQL, " ") + checkNext := false + takeNext := false + for _, t := range tokens { + t = strings.ToUpper(t) + t = strings.TrimSpace(t) + + if takeNext { + tables = append(tables, t) + checkNext = false + takeNext = false + continue + } + if checkNext { + if strings.Contains(t, "(") { + checkNext = false + continue + } + if strings.Contains(t, ",") { + values := strings.Split(t, ",") + for _, v := range values { + v := strings.TrimSpace(v) + if v != "" { + tables = append(tables, v) + } else { + takeNext = true + break + } + } + continue + } + tables = append(tables, t) + checkNext = false + } + if t == "FROM" { + checkNext = true + } + } + return tables, nil + } + return nil, errors.New("not a select statement") +} diff --git a/pkg/expr/sql/parser_test.go b/pkg/expr/sql/parser_test.go new file mode 100644 index 0000000000..2c8e43681e --- /dev/null +++ b/pkg/expr/sql/parser_test.go @@ -0,0 +1,58 @@ +package sql + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestParse(t *testing.T) { + sql := "select * from foo" + tables, err := parseTables((sql)) + assert.Nil(t, err) + + assert.Equal(t, "FOO", tables[0]) +} + +func TestParseWithComma(t *testing.T) { + sql := "select * from foo,bar" + tables, err := parseTables((sql)) + assert.Nil(t, err) + + assert.Equal(t, "FOO", tables[0]) + assert.Equal(t, "BAR", tables[1]) +} + +func TestParseWithCommas(t *testing.T) { + sql := "select * from foo,bar,baz" + tables, err := parseTables((sql)) + assert.Nil(t, err) + + assert.Equal(t, "FOO", tables[0]) + assert.Equal(t, "BAR", tables[1]) + assert.Equal(t, "BAZ", tables[2]) +} + +func TestArray(t *testing.T) { + sql := "SELECT array_value(1, 2, 3)" + tables, err := TablesList((sql)) + assert.Nil(t, err) + + assert.Equal(t, 0, len(tables)) +} + +func TestArray2(t *testing.T) { + sql := "SELECT array_value(1, 2, 3)[2]" + tables, err := TablesList((sql)) + assert.Nil(t, err) + + assert.Equal(t, 0, len(tables)) +} + +func TestXxx(t *testing.T) { + sql := "SELECT [3, 2, 1]::INT[3];" + tables, err := TablesList((sql)) + assert.Nil(t, err) + + assert.Equal(t, 0, len(tables)) +} diff --git a/pkg/expr/sql_command.go b/pkg/expr/sql_command.go new file mode 100644 index 0000000000..ce69bd550d --- /dev/null +++ b/pkg/expr/sql_command.go @@ -0,0 +1,107 @@ +package expr + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/scottlepp/go-duck/duck" + + "github.com/grafana/grafana/pkg/expr/mathexp" + "github.com/grafana/grafana/pkg/expr/sql" + "github.com/grafana/grafana/pkg/infra/tracing" + "github.com/grafana/grafana/pkg/util/errutil" +) + +// SQLCommand is an expression to run SQL over results +type SQLCommand struct { + query string + varsToQuery []string + timeRange TimeRange + refID string +} + +// NewSQLCommand creates a new SQLCommand. +func NewSQLCommand(refID, rawSQL string, tr TimeRange) (*SQLCommand, error) { + if rawSQL == "" { + return nil, errutil.BadRequest("sql-missing-query", + errutil.WithPublicMessage("missing SQL query")) + } + tables, err := sql.TablesList(rawSQL) + if err != nil { + logger.Warn("invalid sql query", "sql", rawSQL, "error", err) + return nil, errutil.BadRequest("sql-invalid-sql", + errutil.WithPublicMessage("error reading SQL command"), + ) + } + return &SQLCommand{ + query: rawSQL, + varsToQuery: tables, + timeRange: tr, + refID: refID, + }, nil +} + +// UnmarshalSQLCommand creates a SQLCommand from Grafana's frontend query. +func UnmarshalSQLCommand(rn *rawNode) (*SQLCommand, error) { + if rn.TimeRange == nil { + return nil, fmt.Errorf("time range must be specified for refID %s", rn.RefID) + } + + expressionRaw, ok := rn.Query["expression"] + if !ok { + return nil, errors.New("no expression in the query") + } + expression, ok := expressionRaw.(string) + if !ok { + return nil, fmt.Errorf("expected sql expression to be type string, but got type %T", expressionRaw) + } + + return NewSQLCommand(rn.RefID, expression, rn.TimeRange) +} + +// NeedsVars returns the variable names (refIds) that are dependencies +// to execute the command and allows the command to fulfill the Command interface. +func (gr *SQLCommand) NeedsVars() []string { + return gr.varsToQuery +} + +// Execute runs the command and returns the results or an error if the command +// failed to execute. +func (gr *SQLCommand) Execute(ctx context.Context, now time.Time, vars mathexp.Vars, tracer tracing.Tracer) (mathexp.Results, error) { + _, span := tracer.Start(ctx, "SSE.ExecuteSQL") + defer span.End() + + allFrames := []*data.Frame{} + for _, ref := range gr.varsToQuery { + results := vars[ref] + frames := results.Values.AsDataFrames(ref) + allFrames = append(allFrames, frames...) + } + + rsp := mathexp.Results{} + + duckDB := duck.NewInMemoryDB() + var frame = &data.Frame{} + err := duckDB.QueryFramesInto(gr.refID, gr.query, allFrames, frame) + if err != nil { + rsp.Error = err + return rsp, nil + } + + frame.RefID = gr.refID + + if frame.Rows() == 0 { + rsp.Values = mathexp.Values{ + mathexp.NoData{Frame: frame}, + } + } + + rsp.Values = mathexp.Values{ + mathexp.TableData{Frame: frame}, + } + + return rsp, nil +} diff --git a/pkg/expr/sql_command_test.go b/pkg/expr/sql_command_test.go new file mode 100644 index 0000000000..90ba470ae0 --- /dev/null +++ b/pkg/expr/sql_command_test.go @@ -0,0 +1,26 @@ +package expr + +import ( + "strings" + "testing" +) + +func TestNewCommand(t *testing.T) { + cmd, err := NewSQLCommand("a", "select a from foo, bar", nil) + if err != nil && strings.Contains(err.Error(), "feature is not enabled") { + return + } + + if err != nil { + t.Fail() + return + } + + for _, v := range cmd.varsToQuery { + if strings.Contains("foo bar", v) { + continue + } + t.Fail() + return + } +} diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 6d768fbfec..b56ef5784e 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1139,6 +1139,13 @@ var ( Stage: FeatureStageExperimental, Owner: grafanaObservabilityMetricsSquad, }, + { + Name: "sqlExpressions", + Description: "Enables using SQL and DuckDB functions as Expressions.", + Stage: FeatureStageExperimental, + FrontendOnly: false, + Owner: grafanaAppPlatformSquad, + }, { Name: "nodeGraphDotLayout", Description: "Changed the layout algorithm for the node graph", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index bbf88310ec..0a083ba800 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -152,6 +152,7 @@ jitterAlertRulesWithinGroups,preview,@grafana/alerting-squad,false,true,false onPremToCloudMigrations,experimental,@grafana/grafana-operator-experience-squad,false,false,false alertingSaveStatePeriodic,privatePreview,@grafana/alerting-squad,false,false,false promQLScope,experimental,@grafana/observability-metrics,false,false,false +sqlExpressions,experimental,@grafana/grafana-app-platform-squad,false,false,false nodeGraphDotLayout,experimental,@grafana/observability-traces-and-profiling,false,false,true groupToNestedTableTransformation,preview,@grafana/dataviz-squad,false,false,true newPDFRendering,experimental,@grafana/sharing-squad,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 17fa7f7610..09e1a7657f 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -619,6 +619,10 @@ const ( // In-development feature that will allow injection of labels into prometheus queries. FlagPromQLScope = "promQLScope" + // FlagSqlExpressions + // Enables using SQL and DuckDB functions as Expressions. + FlagSqlExpressions = "sqlExpressions" + // FlagNodeGraphDotLayout // Changed the layout algorithm for the node graph FlagNodeGraphDotLayout = "nodeGraphDotLayout" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 23a5c2204a..e15db658b2 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -83,7 +83,7 @@ "name": "pluginsInstrumentationStatusSource", "resourceVersion": "1708108588074", "creationTimestamp": "2024-02-16T18:36:28Z", - "deletionTimestamp": "2024-02-23T13:23:30Z" + "deletionTimestamp": "2024-02-27T14:43:01Z" }, "spec": { "description": "Include a status source label for plugin request metrics and logs", @@ -511,7 +511,7 @@ "name": "displayAnonymousStats", "resourceVersion": "1708108588074", "creationTimestamp": "2024-02-16T18:36:28Z", - "deletionTimestamp": "2024-02-23T13:23:30Z" + "deletionTimestamp": "2024-02-27T14:43:01Z" }, "spec": { "description": "Enables anonymous stats to be shown in the UI for Grafana", @@ -1400,7 +1400,7 @@ "name": "traceToMetrics", "resourceVersion": "1708108588074", "creationTimestamp": "2024-02-16T18:36:28Z", - "deletionTimestamp": "2024-02-23T13:23:30Z" + "deletionTimestamp": "2024-02-27T14:43:01Z" }, "spec": { "description": "Enable trace to metrics links", @@ -1529,7 +1529,7 @@ "name": "splitScopes", "resourceVersion": "1708108588074", "creationTimestamp": "2024-02-16T18:36:28Z", - "deletionTimestamp": "2024-02-23T13:23:30Z" + "deletionTimestamp": "2024-02-27T14:43:01Z" }, "spec": { "description": "Support faster dashboard and folder search by splitting permission scopes into parts", @@ -1570,7 +1570,7 @@ "name": "externalServiceAuth", "resourceVersion": "1708108588074", "creationTimestamp": "2024-02-16T18:36:28Z", - "deletionTimestamp": "2024-02-21T10:10:41Z" + "deletionTimestamp": "2024-02-27T14:43:01Z" }, "spec": { "description": "Starts an OAuth2 authentication provider for external services", @@ -2131,6 +2131,21 @@ "codeowner": "@grafana/dataviz-squad", "frontend": true } + }, + { + "metadata": { + "name": "sqlExpressions", + "resourceVersion": "1709044973784", + "creationTimestamp": "2024-02-19T22:46:11Z", + "annotations": { + "grafana.app/updatedTimestamp": "2024-02-27 14:42:53.784398 +0000 UTC" + } + }, + "spec": { + "description": "Enables using SQL and DuckDB functions as Expressions.", + "stage": "experimental", + "codeowner": "@grafana/grafana-app-platform-squad" + } } ] } \ No newline at end of file diff --git a/public/app/features/alerting/unified/GrafanaRuleQueryViewer.tsx b/public/app/features/alerting/unified/GrafanaRuleQueryViewer.tsx index 52ba9bbb73..ff0a2c2fc9 100644 --- a/public/app/features/alerting/unified/GrafanaRuleQueryViewer.tsx +++ b/public/app/features/alerting/unified/GrafanaRuleQueryViewer.tsx @@ -5,6 +5,7 @@ import React from 'react'; import { DataSourceInstanceSettings, GrafanaTheme2, PanelData, RelativeTimeRange } from '@grafana/data'; import { config } from '@grafana/runtime'; +import { Preview } from '@grafana/sql/src/components/visual-query-builder/Preview'; import { Badge, Stack, useStyles2 } from '@grafana/ui'; import { mapRelativeTimeRangeToOption } from '@grafana/ui/src/components/DateTimePickers/RelativeTimeRangePicker/utils'; @@ -182,6 +183,9 @@ function ExpressionPreview({ refId, model, evalData, isAlertCondition }: Express case ExpressionQueryType.threshold: return ; + case ExpressionQueryType.sql: + return ; + default: return <>Expression not supported: {model.type}; } diff --git a/public/app/features/alerting/unified/components/expressions/Expression.tsx b/public/app/features/alerting/unified/components/expressions/Expression.tsx index f05b56596b..73db0a2f5b 100644 --- a/public/app/features/alerting/unified/components/expressions/Expression.tsx +++ b/public/app/features/alerting/unified/components/expressions/Expression.tsx @@ -9,6 +9,7 @@ import { ClassicConditions } from 'app/features/expressions/components/ClassicCo import { Math } from 'app/features/expressions/components/Math'; import { Reduce } from 'app/features/expressions/components/Reduce'; import { Resample } from 'app/features/expressions/components/Resample'; +import { SqlExpr } from 'app/features/expressions/components/SqlExpr'; import { Threshold } from 'app/features/expressions/components/Threshold'; import { ExpressionQuery, @@ -110,6 +111,9 @@ export const Expression: FC = ({ /> ); + case ExpressionQueryType.sql: + return ; + default: return <>Expression not supported: {query.type}; } diff --git a/public/app/features/alerting/unified/utils/timeRange.ts b/public/app/features/alerting/unified/utils/timeRange.ts index dd7b3370bd..11edb6c903 100644 --- a/public/app/features/alerting/unified/utils/timeRange.ts +++ b/public/app/features/alerting/unified/utils/timeRange.ts @@ -29,6 +29,7 @@ const getReferencedIds = (model: ExpressionQuery, queries: AlertQuery[]): string case ExpressionQueryType.classic: return getReferencedIdsForClassicCondition(model); case ExpressionQueryType.math: + case ExpressionQueryType.sql: return getReferencedIdsForMath(model, queries); case ExpressionQueryType.resample: case ExpressionQueryType.reduce: diff --git a/public/app/features/expressions/ExpressionQueryEditor.tsx b/public/app/features/expressions/ExpressionQueryEditor.tsx index 90c6ba95de..419fbe067f 100644 --- a/public/app/features/expressions/ExpressionQueryEditor.tsx +++ b/public/app/features/expressions/ExpressionQueryEditor.tsx @@ -7,6 +7,7 @@ import { ClassicConditions } from './components/ClassicConditions'; import { Math } from './components/Math'; import { Reduce } from './components/Reduce'; import { Resample } from './components/Resample'; +import { SqlExpr } from './components/SqlExpr'; import { Threshold } from './components/Threshold'; import { ExpressionQuery, ExpressionQueryType, expressionTypes } from './types'; import { getDefaults } from './utils/expressionTypes'; @@ -27,6 +28,7 @@ function useExpressionsCache() { case ExpressionQueryType.reduce: case ExpressionQueryType.resample: case ExpressionQueryType.threshold: + case ExpressionQueryType.sql: return expressionCache.current[queryType]; case ExpressionQueryType.classic: return undefined; @@ -47,6 +49,8 @@ function useExpressionsCache() { expressionCache.current.resample = value; expressionCache.current.threshold = value; break; + case ExpressionQueryType.sql: + expressionCache.current.sql = value; } }, []); @@ -89,6 +93,9 @@ export function ExpressionQueryEditor(props: Props) { case ExpressionQueryType.threshold: return ; + + case ExpressionQueryType.sql: + return ; } }; diff --git a/public/app/features/expressions/components/SqlExpr.tsx b/public/app/features/expressions/components/SqlExpr.tsx new file mode 100644 index 0000000000..f5857f8892 --- /dev/null +++ b/public/app/features/expressions/components/SqlExpr.tsx @@ -0,0 +1,27 @@ +import React, { useMemo } from 'react'; + +import { SelectableValue } from '@grafana/data'; +import { SQLEditor } from '@grafana/experimental'; + +import { ExpressionQuery } from '../types'; + +interface Props { + refIds: Array>; + query: ExpressionQuery; + onChange: (query: ExpressionQuery) => void; +} + +export const SqlExpr = ({ onChange, refIds, query }: Props) => { + const vars = useMemo(() => refIds.map((v) => v.value!), [refIds]); + + const initialQuery = `select * from ${vars[0]} limit 1`; + + const onEditorChange = (expression: string) => { + onChange({ + ...query, + expression, + }); + }; + + return ; +}; diff --git a/public/app/features/expressions/types.ts b/public/app/features/expressions/types.ts index fe4026903b..ad4091d608 100644 --- a/public/app/features/expressions/types.ts +++ b/public/app/features/expressions/types.ts @@ -1,4 +1,5 @@ import { DataQuery, ReducerID, SelectableValue } from '@grafana/data'; +import { config } from 'app/core/config'; import { EvalFunction } from '../alerting/state/alertDef'; @@ -13,6 +14,7 @@ export enum ExpressionQueryType { resample = 'resample', classic = 'classic_conditions', threshold = 'threshold', + sql = 'sql', } export const getExpressionLabel = (type: ExpressionQueryType) => { @@ -27,6 +29,8 @@ export const getExpressionLabel = (type: ExpressionQueryType) => { return 'Classic condition'; case ExpressionQueryType.threshold: return 'Threshold'; + case ExpressionQueryType.sql: + return 'SQL'; } }; @@ -59,7 +63,17 @@ export const expressionTypes: Array> = [ description: 'Takes one or more time series returned from a query or an expression and checks if any of the series match the threshold condition.', }, -]; + { + value: ExpressionQueryType.sql, + label: 'SQL', + description: 'Transform data using SQL. Supports Aggregate/Analytics functions from DuckDB', + }, +].filter((expr) => { + if (expr.value === ExpressionQueryType.sql) { + return config.featureToggles?.sqlExpressions; + } + return true; +}); export const reducerTypes: Array> = [ { value: ReducerID.min, label: 'Min', description: 'Get the minimum value' }, From e8df62941ba12c9a3b2e189f3e4d849b109d5522 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=C3=B1or=20Performo=20-=20Leandro=20Melendez?= <54183040+srperf@users.noreply.github.com> Date: Tue, 27 Feb 2024 16:12:08 -0600 Subject: [PATCH 0250/1406] Docs: Add missing visualizations to Grafana vizualization index page (#83351) Co-authored-by: Nathan Marrs Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> Co-authored-by: jev forsberg --- .../panels-visualizations/visualizations/_index.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/sources/panels-visualizations/visualizations/_index.md b/docs/sources/panels-visualizations/visualizations/_index.md index 10042851d8..6e17cc7c64 100644 --- a/docs/sources/panels-visualizations/visualizations/_index.md +++ b/docs/sources/panels-visualizations/visualizations/_index.md @@ -33,6 +33,7 @@ If you are unsure which visualization to pick, Grafana can provide visualization - [Heatmap][] visualizes data in two dimensions, used typically for the magnitude of a phenomenon. - [Pie chart][] is typically used where proportionality is important. - [Candlestick][] is typically for financial data where the focus is price/data movement. + - [Gauge][] is the traditional rounded visual showing how far a single metric is from a threshold. - Stats & numbers - [Stat][] for big stats and optional sparkline. - [Bar gauge][] is a horizontal or vertical bar gauge. @@ -42,6 +43,8 @@ If you are unsure which visualization to pick, Grafana can provide visualization - [Node graph][] for directed graphs or networks. - [Traces][] is the main visualization for traces. - [Flame graph][] is the main visualization for profiling. + - [Canvas][] allows you to explicitly place elements within static and dynamic layouts. + - [Geomap][] helps you visualize geospatial data. - Widgets - [Dashboard list][] can list dashboards. - [Alert list][] can list alerts. @@ -122,6 +125,12 @@ A state timeline shows discrete state changes over time. When used with time ser [Flame graph]: "/docs/grafana/ -> /docs/grafana//panels-visualizations/visualizations/flame-graph" [Flame graph]: "/docs/grafana-cloud/ -> /docs/grafana//panels-visualizations/visualizations/flame-graph" +[Canvas]: "/docs/grafana/ -> /docs/grafana//panels-visualizations/visualizations/canvas" +[Canvas]: "/docs/grafana-cloud/ -> /docs/grafana//panels-visualizations/visualizations/canvas" + +[Geomap]: "/docs/grafana/ -> /docs/grafana//panels-visualizations/visualizations/geomap" +[Geomap]: "/docs/grafana-cloud/ -> /docs/grafana//panels-visualizations/visualizations/geomap" + [Status history]: "/docs/grafana/ -> /docs/grafana//panels-visualizations/visualizations/status-history" [Status history]: "/docs/grafana-cloud/ -> /docs/grafana//panels-visualizations/visualizations/status-history" From 8c18d06386c87f2786119ea9a6334e35e4181cbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Farkas?= Date: Wed, 28 Feb 2024 07:52:45 +0100 Subject: [PATCH 0251/1406] Postgres: Switch the datasource plugin from lib/pq to pgx (#81353) * postgres: switch from lib/pq to pgx * postgres: improved tls handling --- go.mod | 5 + go.sum | 7 + .../grafana-postgresql-datasource/locker.go | 85 ---- .../locker_test.go | 63 --- .../grafana-postgresql-datasource/postgres.go | 118 +++--- .../postgres_snapshot_test.go | 12 +- .../postgres_test.go | 175 ++++---- .../grafana-postgresql-datasource/proxy.go | 30 +- .../proxy_test.go | 12 +- .../table/timestamp_convert_real.golden.jsonc | 32 +- .../table/types_datetime.golden.jsonc | 16 +- .../grafana-postgresql-datasource/tls/tls.go | 147 +++++++ .../tls/tls_loader.go | 101 +++++ .../tls/tls_test.go | 382 ++++++++++++++++++ .../tls/tls_test_helpers.go | 105 +++++ .../tlsmanager.go | 249 ------------ .../tlsmanager_test.go | 332 --------------- 17 files changed, 965 insertions(+), 906 deletions(-) delete mode 100644 pkg/tsdb/grafana-postgresql-datasource/locker.go delete mode 100644 pkg/tsdb/grafana-postgresql-datasource/locker_test.go create mode 100644 pkg/tsdb/grafana-postgresql-datasource/tls/tls.go create mode 100644 pkg/tsdb/grafana-postgresql-datasource/tls/tls_loader.go create mode 100644 pkg/tsdb/grafana-postgresql-datasource/tls/tls_test.go create mode 100644 pkg/tsdb/grafana-postgresql-datasource/tls/tls_test_helpers.go delete mode 100644 pkg/tsdb/grafana-postgresql-datasource/tlsmanager.go delete mode 100644 pkg/tsdb/grafana-postgresql-datasource/tlsmanager_test.go diff --git a/go.mod b/go.mod index 061044a1a8..37e27eddb7 100644 --- a/go.mod +++ b/go.mod @@ -471,9 +471,14 @@ require ( github.com/grafana/grafana/pkg/apiserver v0.0.0-20240226124929-648abdbd0ea4 // @grafana/grafana-app-platform-squad ) +require github.com/jackc/pgx/v5 v5.5.3 // @grafana/oss-big-tent + require ( github.com/bufbuild/protocompile v0.4.0 // indirect github.com/grafana/sqlds/v3 v3.2.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/jhump/protoreflect v1.15.1 // indirect github.com/klauspost/asmfmt v1.3.2 // indirect github.com/krasun/gosqlparser v1.0.5 // @grafana/grafana-app-platform-squad diff --git a/go.sum b/go.sum index a6aa605d07..30e3b806f8 100644 --- a/go.sum +++ b/go.sum @@ -2370,6 +2370,7 @@ github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bY github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= @@ -2380,6 +2381,8 @@ github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwX github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgproto3/v2 v2.2.0/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= @@ -2391,10 +2394,14 @@ github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9 github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= github.com/jackc/pgx/v4 v4.15.0/go.mod h1:D/zyOyXiaM1TmVWnOM18p0xdDtdakRBa0RsVGI3U3bw= +github.com/jackc/pgx/v5 v5.5.3 h1:Ces6/M3wbDXYpM8JyyPD57ivTtJACFZJd885pdIaV2s= +github.com/jackc/pgx/v5 v5.5.3/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.2.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jarcoal/httpmock v1.3.0/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc= github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= diff --git a/pkg/tsdb/grafana-postgresql-datasource/locker.go b/pkg/tsdb/grafana-postgresql-datasource/locker.go deleted file mode 100644 index 796c37c741..0000000000 --- a/pkg/tsdb/grafana-postgresql-datasource/locker.go +++ /dev/null @@ -1,85 +0,0 @@ -package postgres - -import ( - "fmt" - "sync" -) - -// locker is a named reader/writer mutual exclusion lock. -// The lock for each particular key can be held by an arbitrary number of readers or a single writer. -type locker struct { - locks map[any]*sync.RWMutex - locksRW *sync.RWMutex -} - -func newLocker() *locker { - return &locker{ - locks: make(map[any]*sync.RWMutex), - locksRW: new(sync.RWMutex), - } -} - -// Lock locks named rw mutex with specified key for writing. -// If the lock with the same key is already locked for reading or writing, -// Lock blocks until the lock is available. -func (lkr *locker) Lock(key any) { - lk, ok := lkr.getLock(key) - if !ok { - lk = lkr.newLock(key) - } - lk.Lock() -} - -// Unlock unlocks named rw mutex with specified key for writing. It is a run-time error if rw is -// not locked for writing on entry to Unlock. -func (lkr *locker) Unlock(key any) { - lk, ok := lkr.getLock(key) - if !ok { - panic(fmt.Errorf("lock for key '%s' not initialized", key)) - } - lk.Unlock() -} - -// RLock locks named rw mutex with specified key for reading. -// -// It should not be used for recursive read locking for the same key; a blocked Lock -// call excludes new readers from acquiring the lock. See the -// documentation on the golang RWMutex type. -func (lkr *locker) RLock(key any) { - lk, ok := lkr.getLock(key) - if !ok { - lk = lkr.newLock(key) - } - lk.RLock() -} - -// RUnlock undoes a single RLock call for specified key; -// it does not affect other simultaneous readers of locker for specified key. -// It is a run-time error if locker for specified key is not locked for reading -func (lkr *locker) RUnlock(key any) { - lk, ok := lkr.getLock(key) - if !ok { - panic(fmt.Errorf("lock for key '%s' not initialized", key)) - } - lk.RUnlock() -} - -func (lkr *locker) newLock(key any) *sync.RWMutex { - lkr.locksRW.Lock() - defer lkr.locksRW.Unlock() - - if lk, ok := lkr.locks[key]; ok { - return lk - } - lk := new(sync.RWMutex) - lkr.locks[key] = lk - return lk -} - -func (lkr *locker) getLock(key any) (*sync.RWMutex, bool) { - lkr.locksRW.RLock() - defer lkr.locksRW.RUnlock() - - lock, ok := lkr.locks[key] - return lock, ok -} diff --git a/pkg/tsdb/grafana-postgresql-datasource/locker_test.go b/pkg/tsdb/grafana-postgresql-datasource/locker_test.go deleted file mode 100644 index b1dc64f035..0000000000 --- a/pkg/tsdb/grafana-postgresql-datasource/locker_test.go +++ /dev/null @@ -1,63 +0,0 @@ -package postgres - -import ( - "sync" - "testing" - "time" - - "github.com/stretchr/testify/require" -) - -func TestIntegrationLocker(t *testing.T) { - if testing.Short() { - t.Skip("Tests with Sleep") - } - const notUpdated = "not_updated" - const atThread1 = "at_thread_1" - const atThread2 = "at_thread_2" - t.Run("Should lock for same keys", func(t *testing.T) { - updated := notUpdated - locker := newLocker() - locker.Lock(1) - var wg sync.WaitGroup - wg.Add(1) - defer func() { - locker.Unlock(1) - wg.Wait() - }() - - go func() { - locker.RLock(1) - defer func() { - locker.RUnlock(1) - wg.Done() - }() - require.Equal(t, atThread1, updated, "Value should be updated in different thread") - updated = atThread2 - }() - time.Sleep(time.Millisecond * 10) - require.Equal(t, notUpdated, updated, "Value should not be updated in different thread") - updated = atThread1 - }) - - t.Run("Should not lock for different keys", func(t *testing.T) { - updated := notUpdated - locker := newLocker() - locker.Lock(1) - defer locker.Unlock(1) - var wg sync.WaitGroup - wg.Add(1) - go func() { - locker.RLock(2) - defer func() { - locker.RUnlock(2) - wg.Done() - }() - require.Equal(t, notUpdated, updated, "Value should not be updated in different thread") - updated = atThread2 - }() - wg.Wait() - require.Equal(t, atThread2, updated, "Value should be updated in different thread") - updated = atThread1 - }) -} diff --git a/pkg/tsdb/grafana-postgresql-datasource/postgres.go b/pkg/tsdb/grafana-postgresql-datasource/postgres.go index 53e4630885..cfcdd0ede0 100644 --- a/pkg/tsdb/grafana-postgresql-datasource/postgres.go +++ b/pkg/tsdb/grafana-postgresql-datasource/postgres.go @@ -4,38 +4,42 @@ import ( "context" "database/sql" "encoding/json" + "errors" "fmt" + "os" "reflect" "strconv" "strings" "time" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgtype" + pgxstdlib "github.com/jackc/pgx/v5/stdlib" + "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/datasource" "github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt" "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana-plugin-sdk-go/data/sqlutil" - "github.com/lib/pq" "github.com/grafana/grafana-plugin-sdk-go/backend/log" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/tsdb/grafana-postgresql-datasource/tls" "github.com/grafana/grafana/pkg/tsdb/sqleng" ) func ProvideService(cfg *setting.Cfg) *Service { logger := backend.NewLoggerWith("logger", "tsdb.postgres") s := &Service{ - tlsManager: newTLSManager(logger, cfg.DataPath), - logger: logger, + logger: logger, } s.im = datasource.NewInstanceManager(s.newInstanceSettings()) return s } type Service struct { - tlsManager tlsSettingsProvider - im instancemgmt.InstanceManager - logger log.Logger + im instancemgmt.InstanceManager + logger log.Logger } func (s *Service) getDSInfo(ctx context.Context, pluginCtx backend.PluginContext) (*sqleng.DataSourceHandler, error) { @@ -55,13 +59,7 @@ func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest) return dsInfo.QueryData(ctx, req) } -func newPostgres(ctx context.Context, userFacingDefaultError string, rowLimit int64, dsInfo sqleng.DataSourceInfo, cnnstr string, logger log.Logger, settings backend.DataSourceInstanceSettings) (*sql.DB, *sqleng.DataSourceHandler, error) { - connector, err := pq.NewConnector(cnnstr) - if err != nil { - logger.Error("postgres connector creation failed", "error", err) - return nil, nil, fmt.Errorf("postgres connector creation failed") - } - +func newPostgres(ctx context.Context, userFacingDefaultError string, rowLimit int64, dsInfo sqleng.DataSourceInfo, pgxConf *pgx.ConnConfig, logger log.Logger, settings backend.DataSourceInstanceSettings) (*sql.DB, *sqleng.DataSourceHandler, error) { proxyClient, err := settings.ProxyClient(ctx) if err != nil { logger.Error("postgres proxy creation failed", "error", err) @@ -74,9 +72,8 @@ func newPostgres(ctx context.Context, userFacingDefaultError string, rowLimit in logger.Error("postgres proxy creation failed", "error", err) return nil, nil, fmt.Errorf("postgres proxy creation failed") } - postgresDialer := newPostgresProxyDialer(dialer) - // update the postgres dialer with the proxy dialer - connector.Dialer(postgresDialer) + + pgxConf.DialFunc = newPgxDialFunc(dialer) } config := sqleng.DataPluginConfiguration{ @@ -87,7 +84,7 @@ func newPostgres(ctx context.Context, userFacingDefaultError string, rowLimit in queryResultTransformer := postgresQueryResultTransformer{} - db := sql.OpenDB(connector) + db := pgxstdlib.OpenDB(*pgxConf) db.SetMaxOpenConns(config.DSInfo.JsonData.MaxOpenConns) db.SetMaxIdleConns(config.DSInfo.JsonData.MaxIdleConns) @@ -143,7 +140,7 @@ func (s *Service) newInstanceSettings() datasource.InstanceFactoryFunc { DecryptedSecureJSONData: settings.DecryptedSecureJSONData, } - cnnstr, err := s.generateConnectionString(dsInfo) + pgxConf, err := generateConnectionConfig(dsInfo) if err != nil { return nil, err } @@ -153,7 +150,7 @@ func (s *Service) newInstanceSettings() datasource.InstanceFactoryFunc { return nil, err } - _, handler, err := newPostgres(ctx, userFacingDefaultError, sqlCfg.RowLimit, dsInfo, cnnstr, logger, settings) + _, handler, err := newPostgres(ctx, userFacingDefaultError, sqlCfg.RowLimit, dsInfo, pgxConf, logger, settings) if err != nil { logger.Error("Failed connecting to Postgres", "err", err) @@ -170,13 +167,11 @@ func escape(input string) string { return strings.ReplaceAll(strings.ReplaceAll(input, `\`, `\\`), "'", `\'`) } -func (s *Service) generateConnectionString(dsInfo sqleng.DataSourceInfo) (string, error) { - logger := s.logger +func generateConnectionConfig(dsInfo sqleng.DataSourceInfo) (*pgx.ConnConfig, error) { var host string var port int if strings.HasPrefix(dsInfo.URL, "/") { host = dsInfo.URL - logger.Debug("Generating connection string with Unix socket specifier", "socket", host) } else { index := strings.LastIndex(dsInfo.URL, ":") v6Index := strings.Index(dsInfo.URL, "]") @@ -187,12 +182,8 @@ func (s *Service) generateConnectionString(dsInfo sqleng.DataSourceInfo) (string var err error port, err = strconv.Atoi(sp[1]) if err != nil { - return "", fmt.Errorf("invalid port in host specifier %q: %w", sp[1], err) + return nil, fmt.Errorf("invalid port in host specifier %q: %w", sp[1], err) } - - logger.Debug("Generating connection string with network host/port pair", "host", host, "port", port) - } else { - logger.Debug("Generating connection string with network host", "host", host) } } else { if index == v6Index+1 { @@ -200,46 +191,39 @@ func (s *Service) generateConnectionString(dsInfo sqleng.DataSourceInfo) (string var err error port, err = strconv.Atoi(dsInfo.URL[index+1:]) if err != nil { - return "", fmt.Errorf("invalid port in host specifier %q: %w", dsInfo.URL[index+1:], err) + return nil, fmt.Errorf("invalid port in host specifier %q: %w", dsInfo.URL[index+1:], err) } - - logger.Debug("Generating ipv6 connection string with network host/port pair", "host", host, "port", port) } else { host = dsInfo.URL[1 : len(dsInfo.URL)-1] - logger.Debug("Generating ipv6 connection string with network host", "host", host) } } } - connStr := fmt.Sprintf("user='%s' password='%s' host='%s' dbname='%s'", + // NOTE: we always set sslmode=disable in the connection string, we handle TLS manually later + connStr := fmt.Sprintf("sslmode=disable user='%s' password='%s' host='%s' dbname='%s'", escape(dsInfo.User), escape(dsInfo.DecryptedSecureJSONData["password"]), escape(host), escape(dsInfo.Database)) if port > 0 { connStr += fmt.Sprintf(" port=%d", port) } - tlsSettings, err := s.tlsManager.getTLSSettings(dsInfo) + conf, err := pgx.ParseConfig(connStr) if err != nil { - return "", err + return nil, err } - connStr += fmt.Sprintf(" sslmode='%s'", escape(tlsSettings.Mode)) - - // Attach root certificate if provided - if tlsSettings.RootCertFile != "" { - logger.Debug("Setting server root certificate", "tlsRootCert", tlsSettings.RootCertFile) - connStr += fmt.Sprintf(" sslrootcert='%s'", escape(tlsSettings.RootCertFile)) + tlsConf, err := tls.GetTLSConfig(dsInfo, os.ReadFile, host) + if err != nil { + return nil, err } - // Attach client certificate and key if both are provided - if tlsSettings.CertFile != "" && tlsSettings.CertKeyFile != "" { - logger.Debug("Setting TLS/SSL client auth", "tlsCert", tlsSettings.CertFile, "tlsKey", tlsSettings.CertKeyFile) - connStr += fmt.Sprintf(" sslcert='%s' sslkey='%s'", escape(tlsSettings.CertFile), escape(tlsSettings.CertKeyFile)) - } else if tlsSettings.CertFile != "" || tlsSettings.CertKeyFile != "" { - return "", fmt.Errorf("TLS/SSL client certificate and key must both be specified") + // before we set the TLS config, we need to make sure the `.Fallbacks` attribute is unset, see: + // https://github.com/jackc/pgx/discussions/1903#discussioncomment-8430146 + if len(conf.Fallbacks) > 0 { + return nil, errors.New("tls: fallbacks configured, unable to set up TLS config") } + conf.TLSConfig = tlsConf - logger.Debug("Generated Postgres connection string successfully") - return connStr, nil + return conf, nil } type postgresQueryResultTransformer struct{} @@ -267,6 +251,44 @@ func (s *Service) CheckHealth(ctx context.Context, req *backend.CheckHealthReque func (t *postgresQueryResultTransformer) GetConverterList() []sqlutil.StringConverter { return []sqlutil.StringConverter{ + { + Name: "handle TIME WITH TIME ZONE", + InputScanKind: reflect.Interface, + InputTypeName: strconv.Itoa(pgtype.TimetzOID), + ConversionFunc: func(in *string) (*string, error) { return in, nil }, + Replacer: &sqlutil.StringFieldReplacer{ + OutputFieldType: data.FieldTypeNullableTime, + ReplaceFunc: func(in *string) (any, error) { + if in == nil { + return nil, nil + } + v, err := time.Parse("15:04:05-07", *in) + if err != nil { + return nil, err + } + return &v, nil + }, + }, + }, + { + Name: "handle TIME", + InputScanKind: reflect.Interface, + InputTypeName: "TIME", + ConversionFunc: func(in *string) (*string, error) { return in, nil }, + Replacer: &sqlutil.StringFieldReplacer{ + OutputFieldType: data.FieldTypeNullableTime, + ReplaceFunc: func(in *string) (any, error) { + if in == nil { + return nil, nil + } + v, err := time.Parse("15:04:05", *in) + if err != nil { + return nil, err + } + return &v, nil + }, + }, + }, { Name: "handle FLOAT4", InputScanKind: reflect.Interface, diff --git a/pkg/tsdb/grafana-postgresql-datasource/postgres_snapshot_test.go b/pkg/tsdb/grafana-postgresql-datasource/postgres_snapshot_test.go index 0713e8cdac..dca97dc1dc 100644 --- a/pkg/tsdb/grafana-postgresql-datasource/postgres_snapshot_test.go +++ b/pkg/tsdb/grafana-postgresql-datasource/postgres_snapshot_test.go @@ -14,6 +14,7 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/log" "github.com/grafana/grafana-plugin-sdk-go/experimental" + "github.com/jackc/pgx/v5" "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/tsdb/sqleng" @@ -51,7 +52,7 @@ func TestIntegrationPostgresSnapshots(t *testing.T) { t.Skip() } - getCnnStr := func() string { + getCnn := func() (*pgx.ConnConfig, error) { host := os.Getenv("POSTGRES_HOST") if host == "" { host = "localhost" @@ -61,8 +62,10 @@ func TestIntegrationPostgresSnapshots(t *testing.T) { port = "5432" } - return fmt.Sprintf("user=grafanatest password=grafanatest host=%s port=%s dbname=grafanadstest sslmode=disable", + cnnString := fmt.Sprintf("user=grafanatest password=grafanatest host=%s port=%s dbname=grafanadstest sslmode=disable", host, port) + + return pgx.ParseConfig(cnnString) } sqlQueryCommentRe := regexp.MustCompile(`^-- (.+)\n`) @@ -157,9 +160,10 @@ func TestIntegrationPostgresSnapshots(t *testing.T) { logger := log.New() - cnnstr := getCnnStr() + cnn, err := getCnn() + require.NoError(t, err) - db, handler, err := newPostgres(context.Background(), "error", 10000, dsInfo, cnnstr, logger, backend.DataSourceInstanceSettings{}) + db, handler, err := newPostgres(context.Background(), "error", 10000, dsInfo, cnn, logger, backend.DataSourceInstanceSettings{}) t.Cleanup((func() { _, err := db.Exec("DROP TABLE tbl") diff --git a/pkg/tsdb/grafana-postgresql-datasource/postgres_test.go b/pkg/tsdb/grafana-postgresql-datasource/postgres_test.go index 2dec40dc83..656b03b0a8 100644 --- a/pkg/tsdb/grafana-postgresql-datasource/postgres_test.go +++ b/pkg/tsdb/grafana-postgresql-datasource/postgres_test.go @@ -14,19 +14,16 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/tsdb/grafana-postgresql-datasource/tls" "github.com/grafana/grafana/pkg/tsdb/sqleng" - _ "github.com/lib/pq" + "github.com/jackc/pgx/v5" + _ "github.com/jackc/pgx/v5/stdlib" ) -// Test generateConnectionString. -func TestIntegrationGenerateConnectionString(t *testing.T) { - if testing.Short() { - t.Skip("skipping integration test") - } - cfg := setting.NewCfg() - cfg.DataPath = t.TempDir() +func TestGenerateConnectionConfig(t *testing.T) { + rootCertBytes, err := tls.CreateRandomRootCertBytes() + require.NoError(t, err) testCases := []struct { desc string @@ -34,10 +31,15 @@ func TestIntegrationGenerateConnectionString(t *testing.T) { user string password string database string - tlsSettings tlsSettings - expConnStr string + tlsMode string + tlsRootCert []byte expErr string - uid string + expHost string + expPort uint16 + expUser string + expPassword string + expDatabase string + expTLS bool }{ { desc: "Unix socket host", @@ -45,8 +47,11 @@ func TestIntegrationGenerateConnectionString(t *testing.T) { user: "user", password: "password", database: "database", - tlsSettings: tlsSettings{Mode: "verify-full"}, - expConnStr: "user='user' password='password' host='/var/run/postgresql' dbname='database' sslmode='verify-full'", + tlsMode: "disable", + expUser: "user", + expPassword: "password", + expHost: "/var/run/postgresql", + expDatabase: "database", }, { desc: "TCP host", @@ -54,8 +59,12 @@ func TestIntegrationGenerateConnectionString(t *testing.T) { user: "user", password: "password", database: "database", - tlsSettings: tlsSettings{Mode: "verify-full"}, - expConnStr: "user='user' password='password' host='host' dbname='database' sslmode='verify-full'", + tlsMode: "disable", + expUser: "user", + expPassword: "password", + expHost: "host", + expPort: 5432, + expDatabase: "database", }, { desc: "TCP/port host", @@ -63,8 +72,12 @@ func TestIntegrationGenerateConnectionString(t *testing.T) { user: "user", password: "password", database: "database", - tlsSettings: tlsSettings{Mode: "verify-full"}, - expConnStr: "user='user' password='password' host='host' dbname='database' port=1234 sslmode='verify-full'", + tlsMode: "disable", + expUser: "user", + expPassword: "password", + expHost: "host", + expPort: 1234, + expDatabase: "database", }, { desc: "Ipv6 host", @@ -72,8 +85,11 @@ func TestIntegrationGenerateConnectionString(t *testing.T) { user: "user", password: "password", database: "database", - tlsSettings: tlsSettings{Mode: "verify-full"}, - expConnStr: "user='user' password='password' host='::1' dbname='database' sslmode='verify-full'", + tlsMode: "disable", + expUser: "user", + expPassword: "password", + expHost: "::1", + expDatabase: "database", }, { desc: "Ipv6/port host", @@ -81,16 +97,20 @@ func TestIntegrationGenerateConnectionString(t *testing.T) { user: "user", password: "password", database: "database", - tlsSettings: tlsSettings{Mode: "verify-full"}, - expConnStr: "user='user' password='password' host='::1' dbname='database' port=1234 sslmode='verify-full'", + tlsMode: "disable", + expUser: "user", + expPassword: "password", + expHost: "::1", + expPort: 1234, + expDatabase: "database", }, { - desc: "Invalid port", - host: "host:invalid", - user: "user", - database: "database", - tlsSettings: tlsSettings{}, - expErr: "invalid port in host specifier", + desc: "Invalid port", + host: "host:invalid", + user: "user", + database: "database", + tlsMode: "disable", + expErr: "invalid port in host specifier", }, { desc: "Password with single quote and backslash", @@ -98,8 +118,11 @@ func TestIntegrationGenerateConnectionString(t *testing.T) { user: "user", password: `p'\assword`, database: "database", - tlsSettings: tlsSettings{Mode: "verify-full"}, - expConnStr: `user='user' password='p\'\\assword' host='host' dbname='database' sslmode='verify-full'`, + tlsMode: "disable", + expUser: "user", + expPassword: `p'\assword`, + expHost: "host", + expDatabase: "database", }, { desc: "User/DB with single quote and backslash", @@ -107,8 +130,11 @@ func TestIntegrationGenerateConnectionString(t *testing.T) { user: `u'\ser`, password: `password`, database: `d'\atabase`, - tlsSettings: tlsSettings{Mode: "verify-full"}, - expConnStr: `user='u\'\\ser' password='password' host='host' dbname='d\'\\atabase' sslmode='verify-full'`, + tlsMode: "disable", + expUser: `u'\ser`, + expPassword: "password", + expDatabase: `d'\atabase`, + expHost: "host", }, { desc: "Custom TLS mode disabled", @@ -116,45 +142,55 @@ func TestIntegrationGenerateConnectionString(t *testing.T) { user: "user", password: "password", database: "database", - tlsSettings: tlsSettings{Mode: "disable"}, - expConnStr: "user='user' password='password' host='host' dbname='database' sslmode='disable'", + tlsMode: "disable", + expUser: "user", + expPassword: "password", + expHost: "host", + expDatabase: "database", }, { - desc: "Custom TLS mode verify-full with certificate files", - host: "host", - user: "user", - password: "password", - database: "database", - tlsSettings: tlsSettings{ - Mode: "verify-full", - RootCertFile: "i/am/coding/ca.crt", - CertFile: "i/am/coding/client.crt", - CertKeyFile: "i/am/coding/client.key", - }, - expConnStr: "user='user' password='password' host='host' dbname='database' sslmode='verify-full' " + - "sslrootcert='i/am/coding/ca.crt' sslcert='i/am/coding/client.crt' sslkey='i/am/coding/client.key'", + desc: "Custom TLS mode verify-full with certificate files", + host: "host", + user: "user", + password: "password", + database: "database", + tlsMode: "verify-full", + tlsRootCert: rootCertBytes, + expUser: "user", + expPassword: "password", + expDatabase: "database", + expHost: "host", + expTLS: true, }, } for _, tt := range testCases { t.Run(tt.desc, func(t *testing.T) { - svc := Service{ - tlsManager: &tlsTestManager{settings: tt.tlsSettings}, - logger: backend.NewLoggerWith("logger", "tsdb.postgres"), - } - ds := sqleng.DataSourceInfo{ - URL: tt.host, - User: tt.user, - DecryptedSecureJSONData: map[string]string{"password": tt.password}, - Database: tt.database, - UID: tt.uid, + URL: tt.host, + User: tt.user, + DecryptedSecureJSONData: map[string]string{ + "password": tt.password, + "tlsCACert": string(tt.tlsRootCert), + }, + Database: tt.database, + JsonData: sqleng.JsonData{ + Mode: tt.tlsMode, + ConfigurationMethod: "file-content", + }, } - connStr, err := svc.generateConnectionString(ds) + c, err := generateConnectionConfig(ds) if tt.expErr == "" { require.NoError(t, err, tt.desc) - assert.Equal(t, tt.expConnStr, connStr) + assert.Equal(t, tt.expHost, c.Host) + if tt.expPort != 0 { + assert.Equal(t, tt.expPort, c.Port) + } + assert.Equal(t, tt.expUser, c.User) + assert.Equal(t, tt.expDatabase, c.Database) + assert.Equal(t, tt.expPassword, c.Password) + require.Equal(t, tt.expTLS, c.TLSConfig != nil) } else { require.Error(t, err, tt.desc) assert.True(t, strings.HasPrefix(err.Error(), tt.expErr), @@ -206,9 +242,10 @@ func TestIntegrationPostgres(t *testing.T) { logger := backend.NewLoggerWith("logger", "postgres.test") - cnnstr := postgresTestDBConnString() + cnn, err := postgresTestDBConn() + require.NoError(t, err) - db, exe, err := newPostgres(context.Background(), "error", 10000, dsInfo, cnnstr, logger, backend.DataSourceInstanceSettings{}) + db, exe, err := newPostgres(context.Background(), "error", 10000, dsInfo, cnn, logger, backend.DataSourceInstanceSettings{}) require.NoError(t, err) @@ -1262,7 +1299,7 @@ func TestIntegrationPostgres(t *testing.T) { t.Run("When row limit set to 1", func(t *testing.T) { dsInfo := sqleng.DataSourceInfo{} - _, handler, err := newPostgres(context.Background(), "error", 1, dsInfo, cnnstr, logger, backend.DataSourceInstanceSettings{}) + _, handler, err := newPostgres(context.Background(), "error", 1, dsInfo, cnn, logger, backend.DataSourceInstanceSettings{}) require.NoError(t, err) @@ -1377,14 +1414,6 @@ func genTimeRangeByInterval(from time.Time, duration time.Duration, interval tim return timeRange } -type tlsTestManager struct { - settings tlsSettings -} - -func (m *tlsTestManager) getTLSSettings(dsInfo sqleng.DataSourceInfo) (tlsSettings, error) { - return m.settings, nil -} - func isTestDbPostgres() bool { if db, present := os.LookupEnv("GRAFANA_TEST_DB"); present { return db == "postgres" @@ -1393,7 +1422,7 @@ func isTestDbPostgres() bool { return false } -func postgresTestDBConnString() string { +func postgresTestDBConn() (*pgx.ConnConfig, error) { host := os.Getenv("POSTGRES_HOST") if host == "" { host = "localhost" @@ -1402,6 +1431,8 @@ func postgresTestDBConnString() string { if port == "" { port = "5432" } - return fmt.Sprintf("user=grafanatest password=grafanatest host=%s port=%s dbname=grafanadstest sslmode=disable", + connStr := fmt.Sprintf("user=grafanatest password=grafanatest host=%s port=%s dbname=grafanadstest sslmode=disable", host, port) + + return pgx.ParseConfig(connStr) } diff --git a/pkg/tsdb/grafana-postgresql-datasource/proxy.go b/pkg/tsdb/grafana-postgresql-datasource/proxy.go index d06d8b6815..0c836eb345 100644 --- a/pkg/tsdb/grafana-postgresql-datasource/proxy.go +++ b/pkg/tsdb/grafana-postgresql-datasource/proxy.go @@ -3,33 +3,17 @@ package postgres import ( "context" "net" - "time" - "github.com/lib/pq" "golang.org/x/net/proxy" ) -// we wrap the proxy.Dialer to become dialer that the postgres module accepts -func newPostgresProxyDialer(dialer proxy.Dialer) pq.Dialer { - return &postgresProxyDialer{d: dialer} -} - -var _ pq.Dialer = (&postgresProxyDialer{}) - -// postgresProxyDialer implements the postgres dialer using a proxy dialer, as their functions differ slightly -type postgresProxyDialer struct { - d proxy.Dialer -} - -// Dial uses the normal proxy dial function with the updated dialer -func (p *postgresProxyDialer) Dial(network, addr string) (c net.Conn, err error) { - return p.d.Dial(network, addr) -} +type PgxDialFunc = func(ctx context.Context, network string, address string) (net.Conn, error) -// DialTimeout uses the normal postgres dial timeout function with the updated dialer -func (p *postgresProxyDialer) DialTimeout(network, address string, timeout time.Duration) (net.Conn, error) { - ctx, cancel := context.WithTimeout(context.Background(), timeout) - defer cancel() +func newPgxDialFunc(dialer proxy.Dialer) PgxDialFunc { + dialFunc := + func(ctx context.Context, network string, addr string) (net.Conn, error) { + return dialer.Dial(network, addr) + } - return p.d.(proxy.ContextDialer).DialContext(ctx, network, address) + return dialFunc } diff --git a/pkg/tsdb/grafana-postgresql-datasource/proxy_test.go b/pkg/tsdb/grafana-postgresql-datasource/proxy_test.go index ec36e1a1ea..afd205bd37 100644 --- a/pkg/tsdb/grafana-postgresql-datasource/proxy_test.go +++ b/pkg/tsdb/grafana-postgresql-datasource/proxy_test.go @@ -1,12 +1,12 @@ package postgres import ( - "database/sql" "fmt" "net" "testing" - "github.com/lib/pq" + "github.com/jackc/pgx/v5" + pgxstdlib "github.com/jackc/pgx/v5/stdlib" "github.com/stretchr/testify/require" "golang.org/x/net/proxy" ) @@ -25,13 +25,13 @@ func TestPostgresProxyDriver(t *testing.T) { cnnstr := fmt.Sprintf("postgres://auser:password@%s/db?sslmode=disable", dbURL) t.Run("Connector should use dialer context that routes through the socks proxy to db", func(t *testing.T) { - connector, err := pq.NewConnector(cnnstr) + pgxConf, err := pgx.ParseConfig(cnnstr) require.NoError(t, err) - dialer := newPostgresProxyDialer(&testDialer{}) - connector.Dialer(dialer) + pgxConf.DialFunc = newPgxDialFunc(&testDialer{}) + + db := pgxstdlib.OpenDB(*pgxConf) - db := sql.OpenDB(connector) err = db.Ping() require.Contains(t, err.Error(), "test-dialer is not functional") diff --git a/pkg/tsdb/grafana-postgresql-datasource/testdata/table/timestamp_convert_real.golden.jsonc b/pkg/tsdb/grafana-postgresql-datasource/testdata/table/timestamp_convert_real.golden.jsonc index af23cf8b5b..e6d1dfd238 100644 --- a/pkg/tsdb/grafana-postgresql-datasource/testdata/table/timestamp_convert_real.golden.jsonc +++ b/pkg/tsdb/grafana-postgresql-datasource/testdata/table/timestamp_convert_real.golden.jsonc @@ -9,16 +9,16 @@ // } // Name: // Dimensions: 4 Fields by 4 Rows -// +--------------------------------------+-------------------------------+------------------+-----------------------------------+ -// | Name: reallyt | Name: time | Name: n | Name: timeend | -// | Labels: | Labels: | Labels: | Labels: | -// | Type: []*time.Time | Type: []*time.Time | Type: []*float64 | Type: []*time.Time | -// +--------------------------------------+-------------------------------+------------------+-----------------------------------+ -// | 2023-12-21 12:22:24 +0000 UTC | 2023-12-21 12:21:40 +0000 UTC | 1.7031613e+09 | 2023-12-21 12:22:52 +0000 UTC | -// | 2023-12-21 12:20:33.408 +0000 UTC | 2023-12-21 12:20:00 +0000 UTC | 1.7031612e+12 | 2023-12-21 12:21:52.522 +0000 UTC | -// | 2023-12-21 12:20:41.050022 +0000 UTC | 2023-12-21 12:20:00 +0000 UTC | 1.7031612e+18 | 2023-12-21 12:21:52.522 +0000 UTC | -// | null | null | null | null | -// +--------------------------------------+-------------------------------+------------------+-----------------------------------+ +// +--------------------------------------+-----------------------------------+-----------------------+-----------------------------------+ +// | Name: reallyt | Name: time | Name: n | Name: timeend | +// | Labels: | Labels: | Labels: | Labels: | +// | Type: []*time.Time | Type: []*time.Time | Type: []*float64 | Type: []*time.Time | +// +--------------------------------------+-----------------------------------+-----------------------+-----------------------------------+ +// | 2023-12-21 12:22:24 +0000 UTC | 2023-12-21 12:22:24 +0000 UTC | 1.703161344e+09 | 2023-12-21 12:22:52 +0000 UTC | +// | 2023-12-21 12:20:33.408 +0000 UTC | 2023-12-21 12:20:33.408 +0000 UTC | 1.703161233408e+12 | 2023-12-21 12:21:52.522 +0000 UTC | +// | 2023-12-21 12:20:41.050022 +0000 UTC | 2023-12-21 12:20:41.05 +0000 UTC | 1.703161241050022e+18 | 2023-12-21 12:21:52.522 +0000 UTC | +// | null | null | null | null | +// +--------------------------------------+-----------------------------------+-----------------------+-----------------------------------+ // // // 🌟 This was machine generated. Do not edit. 🌟 @@ -78,15 +78,15 @@ null ], [ - 1703161300000, - 1703161200000, - 1703161200000, + 1703161344000, + 1703161233408, + 1703161241050, null ], [ - 1703161300, - 1703161200000, - 1703161200000000000, + 1703161344, + 1703161233408, + 1703161241050022000, null ], [ diff --git a/pkg/tsdb/grafana-postgresql-datasource/testdata/table/types_datetime.golden.jsonc b/pkg/tsdb/grafana-postgresql-datasource/testdata/table/types_datetime.golden.jsonc index 48e55e7b07..09e9403459 100644 --- a/pkg/tsdb/grafana-postgresql-datasource/testdata/table/types_datetime.golden.jsonc +++ b/pkg/tsdb/grafana-postgresql-datasource/testdata/table/types_datetime.golden.jsonc @@ -9,14 +9,14 @@ // } // Name: // Dimensions: 12 Fields by 2 Rows -// +----------------------------------------+----------------------------------------+--------------------------------------+--------------------------------------+---------------------------------+---------------------------------+--------------------------------------+--------------------------------------+----------------------------------------+----------------------------------------+-----------------+-----------------+ -// | Name: ts | Name: tsnn | Name: tsz | Name: tsznn | Name: d | Name: dnn | Name: t | Name: tnn | Name: tz | Name: tznn | Name: i | Name: inn | -// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | -// | Type: []*time.Time | Type: []*time.Time | Type: []*time.Time | Type: []*time.Time | Type: []*time.Time | Type: []*time.Time | Type: []*time.Time | Type: []*time.Time | Type: []*time.Time | Type: []*time.Time | Type: []*string | Type: []*string | -// +----------------------------------------+----------------------------------------+--------------------------------------+--------------------------------------+---------------------------------+---------------------------------+--------------------------------------+--------------------------------------+----------------------------------------+----------------------------------------+-----------------+-----------------+ -// | 2023-11-15 05:06:07.123456 +0000 +0000 | 2023-11-15 05:06:08.123456 +0000 +0000 | 2021-07-22 11:22:33.654321 +0000 UTC | 2021-07-22 11:22:34.654321 +0000 UTC | 2023-12-20 00:00:00 +0000 +0000 | 2023-12-21 00:00:00 +0000 +0000 | 0000-01-01 12:34:56.234567 +0000 UTC | 0000-01-01 12:34:57.234567 +0000 UTC | 0000-01-01 23:12:36.765432 +0100 +0100 | 0000-01-01 23:12:37.765432 +0100 +0100 | 00:00:00.987654 | 00:00:00.887654 | -// | null | 2023-11-15 05:06:09.123456 +0000 +0000 | null | 2021-07-22 11:22:35.654321 +0000 UTC | null | 2023-12-22 00:00:00 +0000 +0000 | null | 0000-01-01 12:34:58.234567 +0000 UTC | null | 0000-01-01 23:12:38.765432 +0100 +0100 | null | 00:00:00.787654 | -// +----------------------------------------+----------------------------------------+--------------------------------------+--------------------------------------+---------------------------------+---------------------------------+--------------------------------------+--------------------------------------+----------------------------------------+----------------------------------------+-----------------+-----------------+ +// +--------------------------------------+--------------------------------------+--------------------------------------+--------------------------------------+-------------------------------+-------------------------------+--------------------------------------+--------------------------------------+----------------------------------------+----------------------------------------+-----------------+-----------------+ +// | Name: ts | Name: tsnn | Name: tsz | Name: tsznn | Name: d | Name: dnn | Name: t | Name: tnn | Name: tz | Name: tznn | Name: i | Name: inn | +// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | +// | Type: []*time.Time | Type: []*time.Time | Type: []*time.Time | Type: []*time.Time | Type: []*time.Time | Type: []*time.Time | Type: []*time.Time | Type: []*time.Time | Type: []*time.Time | Type: []*time.Time | Type: []*string | Type: []*string | +// +--------------------------------------+--------------------------------------+--------------------------------------+--------------------------------------+-------------------------------+-------------------------------+--------------------------------------+--------------------------------------+----------------------------------------+----------------------------------------+-----------------+-----------------+ +// | 2023-11-15 05:06:07.123456 +0000 UTC | 2023-11-15 05:06:08.123456 +0000 UTC | 2021-07-22 11:22:33.654321 +0000 UTC | 2021-07-22 11:22:34.654321 +0000 UTC | 2023-12-20 00:00:00 +0000 UTC | 2023-12-21 00:00:00 +0000 UTC | 0000-01-01 12:34:56.234567 +0000 UTC | 0000-01-01 12:34:57.234567 +0000 UTC | 0000-01-01 23:12:36.765432 +0100 +0100 | 0000-01-01 23:12:37.765432 +0100 +0100 | 00:00:00.987654 | 00:00:00.887654 | +// | null | 2023-11-15 05:06:09.123456 +0000 UTC | null | 2021-07-22 11:22:35.654321 +0000 UTC | null | 2023-12-22 00:00:00 +0000 UTC | null | 0000-01-01 12:34:58.234567 +0000 UTC | null | 0000-01-01 23:12:38.765432 +0100 +0100 | null | 00:00:00.787654 | +// +--------------------------------------+--------------------------------------+--------------------------------------+--------------------------------------+-------------------------------+-------------------------------+--------------------------------------+--------------------------------------+----------------------------------------+----------------------------------------+-----------------+-----------------+ // // // 🌟 This was machine generated. Do not edit. 🌟 diff --git a/pkg/tsdb/grafana-postgresql-datasource/tls/tls.go b/pkg/tsdb/grafana-postgresql-datasource/tls/tls.go new file mode 100644 index 0000000000..e5e409dc55 --- /dev/null +++ b/pkg/tsdb/grafana-postgresql-datasource/tls/tls.go @@ -0,0 +1,147 @@ +package tls + +import ( + "crypto/tls" + "crypto/x509" + "errors" + + "github.com/grafana/grafana/pkg/tsdb/sqleng" +) + +// we support 4 postgres tls modes: +// disable - no tls +// require - use tls +// verify-ca - use tls, verify root cert but not the hostname +// verify-full - use tls, verify root cert +// (for all the options except `disable`, you can optionally use client certificates) + +var errNoRootCert = errors.New("tls: missing root certificate") + +func getTLSConfigRequire(certs *Certs) (*tls.Config, error) { + // we may have a client-cert, we do not have a root-cert + + // see https://www.postgresql.org/docs/12/libpq-ssl.html , + // mode=require + provided root-cert should behave as mode=verify-ca + if certs.rootCerts != nil { + return getTLSConfigVerifyCA(certs) + } + + return &tls.Config{ + InsecureSkipVerify: true, // we do not verify the root cert + Certificates: certs.clientCerts, + }, nil +} + +// to implement the verify-ca mode, we need to do this: +// - for the root certificate +// - verify that the certificate we receive from the server is trusted, +// meaning it relates to our root certificate +// - we DO NOT verify that the hostname of the database matches +// the hostname in the certificate +// +// the problem is, `go“ does not offer such an option. +// by default, it will verify both things. +// +// so what we do is: +// - we turn off the default-verification with `InsecureSkipVerify` +// - we implement our own verification using `VerifyConnection` +// +// extra info about this: +// - there is a rejected feature-request about this at https://github.com/golang/go/issues/21971 +// - the recommended workaround is based on VerifyPeerCertificate +// - there is even example code at https://github.com/golang/go/commit/29cfb4d3c3a97b6f426d1b899234da905be699aa +// - but later the example code was changed to use VerifyConnection instead: +// https://github.com/golang/go/commit/7eb5941b95a588a23f18fa4c22fe42ff0119c311 +// +// a verifyConnection example is at https://pkg.go.dev/crypto/tls#example-Config-VerifyConnection . +// +// this is how the `pgx` library handles verify-ca: +// +// https://github.com/jackc/pgx/blob/5c63f646f820ca9696fc3515c1caf2a557d562e5/pgconn/config.go#L657-L690 +// (unfortunately pgx only handles this for certificate-provided-as-path, so we cannot rely on it) +func getTLSConfigVerifyCA(certs *Certs) (*tls.Config, error) { + // we must have a root certificate + if certs.rootCerts == nil { + return nil, errNoRootCert + } + + conf := tls.Config{ + Certificates: certs.clientCerts, + InsecureSkipVerify: true, // we turn off the default-verification, we'll do VerifyConnection instead + VerifyConnection: func(state tls.ConnectionState) error { + // we add all the certificates to the pool, we skip the first cert. + intermediates := x509.NewCertPool() + for _, c := range state.PeerCertificates[1:] { + intermediates.AddCert(c) + } + + opts := x509.VerifyOptions{ + Roots: certs.rootCerts, + Intermediates: intermediates, + } + + // we call `Verify()` on the first cert (that we skipped previously) + _, err := state.PeerCertificates[0].Verify(opts) + return err + }, + RootCAs: certs.rootCerts, + } + + return &conf, nil +} + +func getTLSConfigVerifyFull(certs *Certs, serverName string) (*tls.Config, error) { + // we must have a root certificate + if certs.rootCerts == nil { + return nil, errNoRootCert + } + + conf := tls.Config{ + Certificates: certs.clientCerts, + ServerName: serverName, + RootCAs: certs.rootCerts, + } + + return &conf, nil +} + +func IsTLSEnabled(dsInfo sqleng.DataSourceInfo) bool { + mode := dsInfo.JsonData.Mode + return mode != "disable" +} + +// returns `nil` if tls is disabled +func GetTLSConfig(dsInfo sqleng.DataSourceInfo, readFile ReadFileFunc, serverName string) (*tls.Config, error) { + mode := dsInfo.JsonData.Mode + // we need to special-case the no-tls-mode + if mode == "disable" { + return nil, nil + } + + // for all the remaining cases we need to load + // both the root-cert if exists, and the client-cert if exists. + certBytes, err := loadCertificateBytes(dsInfo, readFile) + if err != nil { + return nil, err + } + + certs, err := createCertificates(certBytes) + if err != nil { + return nil, err + } + + switch mode { + // `disable` already handled + case "": + // for backward-compatibility reasons this is the same as `require` + return getTLSConfigRequire(certs) + case "require": + return getTLSConfigRequire(certs) + case "verify-ca": + return getTLSConfigVerifyCA(certs) + case "verify-full": + return getTLSConfigVerifyFull(certs, serverName) + default: + return nil, errors.New("tls: invalid mode " + mode) + } +} diff --git a/pkg/tsdb/grafana-postgresql-datasource/tls/tls_loader.go b/pkg/tsdb/grafana-postgresql-datasource/tls/tls_loader.go new file mode 100644 index 0000000000..6c19d3801d --- /dev/null +++ b/pkg/tsdb/grafana-postgresql-datasource/tls/tls_loader.go @@ -0,0 +1,101 @@ +package tls + +import ( + "crypto/tls" + "crypto/x509" + "errors" + + "github.com/grafana/grafana/pkg/tsdb/sqleng" +) + +// this file deals with locating and loading the certificates, +// from json-data or from disk. + +type CertBytes struct { + rootCert []byte + clientKey []byte + clientCert []byte +} + +type ReadFileFunc = func(name string) ([]byte, error) + +var errPartialClientCertNoKey = errors.New("tls: client cert provided but client key missing") +var errPartialClientCertNoCert = errors.New("tls: client key provided but client cert missing") + +// certificates can be stored either as encrypted-json-data, or as file-path +func loadCertificateBytes(dsInfo sqleng.DataSourceInfo, readFile ReadFileFunc) (*CertBytes, error) { + if dsInfo.JsonData.ConfigurationMethod == "file-content" { + return &CertBytes{ + rootCert: []byte(dsInfo.DecryptedSecureJSONData["tlsCACert"]), + clientKey: []byte(dsInfo.DecryptedSecureJSONData["tlsClientKey"]), + clientCert: []byte(dsInfo.DecryptedSecureJSONData["tlsClientCert"]), + }, nil + } else { + c := CertBytes{} + + if dsInfo.JsonData.RootCertFile != "" { + rootCert, err := readFile(dsInfo.JsonData.RootCertFile) + if err != nil { + return nil, err + } + c.rootCert = rootCert + } + + if dsInfo.JsonData.CertKeyFile != "" { + clientKey, err := readFile(dsInfo.JsonData.CertKeyFile) + if err != nil { + return nil, err + } + c.clientKey = clientKey + } + + if dsInfo.JsonData.CertFile != "" { + clientCert, err := readFile(dsInfo.JsonData.CertFile) + if err != nil { + return nil, err + } + c.clientCert = clientCert + } + + return &c, nil + } +} + +type Certs struct { + clientCerts []tls.Certificate + rootCerts *x509.CertPool +} + +func createCertificates(certBytes *CertBytes) (*Certs, error) { + certs := Certs{} + + if len(certBytes.rootCert) > 0 { + pool := x509.NewCertPool() + ok := pool.AppendCertsFromPEM(certBytes.rootCert) + if !ok { + return nil, errors.New("tls: failed to add root certificate") + } + certs.rootCerts = pool + } + + hasClientKey := len(certBytes.clientKey) > 0 + hasClientCert := len(certBytes.clientCert) > 0 + + if hasClientKey && hasClientCert { + cert, err := tls.X509KeyPair(certBytes.clientCert, certBytes.clientKey) + if err != nil { + return nil, err + } + certs.clientCerts = []tls.Certificate{cert} + } + + if hasClientKey && (!hasClientCert) { + return nil, errPartialClientCertNoCert + } + + if hasClientCert && (!hasClientKey) { + return nil, errPartialClientCertNoKey + } + + return &certs, nil +} diff --git a/pkg/tsdb/grafana-postgresql-datasource/tls/tls_test.go b/pkg/tsdb/grafana-postgresql-datasource/tls/tls_test.go new file mode 100644 index 0000000000..bce63e1c43 --- /dev/null +++ b/pkg/tsdb/grafana-postgresql-datasource/tls/tls_test.go @@ -0,0 +1,382 @@ +package tls + +import ( + "errors" + "os" + "testing" + + "github.com/grafana/grafana/pkg/tsdb/sqleng" + "github.com/stretchr/testify/require" +) + +func noReadFile(path string) ([]byte, error) { + return nil, errors.New("not implemented") +} + +func TestTLSNoMode(t *testing.T) { + // for backward-compatibility reason, + // when mode is unset, it defaults to `require` + dsInfo := sqleng.DataSourceInfo{ + JsonData: sqleng.JsonData{ + ConfigurationMethod: "", + }, + } + c, err := GetTLSConfig(dsInfo, noReadFile, "localhost") + require.NoError(t, err) + require.NotNil(t, c) + require.True(t, c.InsecureSkipVerify) +} + +func TestTLSDisable(t *testing.T) { + dsInfo := sqleng.DataSourceInfo{ + JsonData: sqleng.JsonData{ + Mode: "disable", + ConfigurationMethod: "", + }, + } + c, err := GetTLSConfig(dsInfo, noReadFile, "localhost") + require.NoError(t, err) + require.Nil(t, c) +} + +func TestTLSRequire(t *testing.T) { + dsInfo := sqleng.DataSourceInfo{ + JsonData: sqleng.JsonData{ + Mode: "require", + ConfigurationMethod: "", + }, + } + c, err := GetTLSConfig(dsInfo, noReadFile, "localhost") + require.NoError(t, err) + require.NotNil(t, c) + require.True(t, c.InsecureSkipVerify) + require.Nil(t, c.RootCAs) +} + +func TestTLSRequireWithRootCert(t *testing.T) { + rootCertBytes, err := CreateRandomRootCertBytes() + require.NoError(t, err) + + dsInfo := sqleng.DataSourceInfo{ + JsonData: sqleng.JsonData{ + Mode: "require", + ConfigurationMethod: "file-content", + }, + DecryptedSecureJSONData: map[string]string{ + "tlsCACert": string(rootCertBytes), + }, + } + c, err := GetTLSConfig(dsInfo, noReadFile, "localhost") + require.NoError(t, err) + require.NotNil(t, c) + require.True(t, c.InsecureSkipVerify) + require.NotNil(t, c.VerifyConnection) + require.NotNil(t, c.RootCAs) // TODO: not the best, but nothing better available +} + +func TestTLSVerifyCA(t *testing.T) { + rootCertBytes, err := CreateRandomRootCertBytes() + require.NoError(t, err) + + dsInfo := sqleng.DataSourceInfo{ + JsonData: sqleng.JsonData{ + Mode: "verify-ca", + ConfigurationMethod: "file-content", + }, + DecryptedSecureJSONData: map[string]string{ + "tlsCACert": string(rootCertBytes), + }, + } + c, err := GetTLSConfig(dsInfo, noReadFile, "localhost") + require.NoError(t, err) + require.NotNil(t, c) + require.True(t, c.InsecureSkipVerify) + require.NotNil(t, c.VerifyConnection) + require.NotNil(t, c.RootCAs) // TODO: not the best, but nothing better available +} + +func TestTLSVerifyCAMisingRootCert(t *testing.T) { + dsInfo := sqleng.DataSourceInfo{ + JsonData: sqleng.JsonData{ + Mode: "verify-ca", + ConfigurationMethod: "file-content", + }, + DecryptedSecureJSONData: map[string]string{}, + } + _, err := GetTLSConfig(dsInfo, noReadFile, "localhost") + require.ErrorIs(t, err, errNoRootCert) +} + +func TestTLSClientCert(t *testing.T) { + clientKey, clientCert, err := CreateRandomClientCert() + require.NoError(t, err) + + dsInfo := sqleng.DataSourceInfo{ + JsonData: sqleng.JsonData{ + Mode: "require", + ConfigurationMethod: "file-content", + }, + DecryptedSecureJSONData: map[string]string{ + "tlsClientCert": string(clientCert), + "tlsClientKey": string(clientKey), + }, + } + c, err := GetTLSConfig(dsInfo, noReadFile, "localhost") + require.NoError(t, err) + require.NotNil(t, c) + require.Len(t, c.Certificates, 1) +} + +func TestTLSMethodFileContentClientCertMissingKey(t *testing.T) { + _, clientCert, err := CreateRandomClientCert() + require.NoError(t, err) + + dsInfo := sqleng.DataSourceInfo{ + JsonData: sqleng.JsonData{ + Mode: "require", + ConfigurationMethod: "file-content", + }, + DecryptedSecureJSONData: map[string]string{ + "tlsClientCert": string(clientCert), + }, + } + _, err = GetTLSConfig(dsInfo, noReadFile, "localhost") + require.ErrorIs(t, err, errPartialClientCertNoKey) +} + +func TestTLSMethodFileContentClientCertMissingCert(t *testing.T) { + clientKey, _, err := CreateRandomClientCert() + require.NoError(t, err) + + dsInfo := sqleng.DataSourceInfo{ + JsonData: sqleng.JsonData{ + Mode: "require", + ConfigurationMethod: "file-content", + }, + DecryptedSecureJSONData: map[string]string{ + "tlsClientKey": string(clientKey), + }, + } + _, err = GetTLSConfig(dsInfo, noReadFile, "localhost") + require.ErrorIs(t, err, errPartialClientCertNoCert) +} + +func TestTLSMethodFilePathClientCertMissingKey(t *testing.T) { + clientKey, _, err := CreateRandomClientCert() + require.NoError(t, err) + + readFile := newMockReadFile(map[string]([]byte){ + "path1": clientKey, + }) + + dsInfo := sqleng.DataSourceInfo{ + JsonData: sqleng.JsonData{ + Mode: "require", + ConfigurationMethod: "file-path", + CertKeyFile: "path1", + }, + } + _, err = GetTLSConfig(dsInfo, readFile, "localhost") + require.ErrorIs(t, err, errPartialClientCertNoCert) +} + +func TestTLSMethodFilePathClientCertMissingCert(t *testing.T) { + _, clientCert, err := CreateRandomClientCert() + require.NoError(t, err) + + readFile := newMockReadFile(map[string]([]byte){ + "path1": clientCert, + }) + + dsInfo := sqleng.DataSourceInfo{ + JsonData: sqleng.JsonData{ + Mode: "require", + ConfigurationMethod: "file-path", + CertFile: "path1", + }, + } + _, err = GetTLSConfig(dsInfo, readFile, "localhost") + require.ErrorIs(t, err, errPartialClientCertNoKey) +} + +func TestTLSVerifyFull(t *testing.T) { + rootCertBytes, err := CreateRandomRootCertBytes() + require.NoError(t, err) + + dsInfo := sqleng.DataSourceInfo{ + JsonData: sqleng.JsonData{ + Mode: "verify-full", + ConfigurationMethod: "file-content", + }, + DecryptedSecureJSONData: map[string]string{ + "tlsCACert": string(rootCertBytes), + }, + } + c, err := GetTLSConfig(dsInfo, noReadFile, "localhost") + require.NoError(t, err) + require.NotNil(t, c) + require.False(t, c.InsecureSkipVerify) + require.Nil(t, c.VerifyConnection) + require.NotNil(t, c.RootCAs) // TODO: not the best, but nothing better available +} + +func TestTLSMethodFileContent(t *testing.T) { + rootCertBytes, err := CreateRandomRootCertBytes() + require.NoError(t, err) + + clientKey, clientCert, err := CreateRandomClientCert() + require.NoError(t, err) + + dsInfo := sqleng.DataSourceInfo{ + JsonData: sqleng.JsonData{ + Mode: "verify-full", + ConfigurationMethod: "file-content", + }, + DecryptedSecureJSONData: map[string]string{ + "tlsCACert": string(rootCertBytes), + "tlsClientCert": string(clientCert), + "tlsClientKey": string(clientKey), + }, + } + c, err := GetTLSConfig(dsInfo, noReadFile, "localhost") + require.NoError(t, err) + require.NotNil(t, c) + require.Len(t, c.Certificates, 1) + require.NotNil(t, c.RootCAs) // TODO: not the best, but nothing better available +} + +func TestTLSMethodFilePath(t *testing.T) { + rootCertBytes, err := CreateRandomRootCertBytes() + require.NoError(t, err) + + clientKey, clientCert, err := CreateRandomClientCert() + require.NoError(t, err) + + readFile := newMockReadFile(map[string]([]byte){ + "root-cert-path": rootCertBytes, + "client-key-path": clientKey, + "client-cert-path": clientCert, + }) + + dsInfo := sqleng.DataSourceInfo{ + JsonData: sqleng.JsonData{ + Mode: "verify-full", + ConfigurationMethod: "file-path", + RootCertFile: "root-cert-path", + CertKeyFile: "client-key-path", + CertFile: "client-cert-path", + }, + } + c, err := GetTLSConfig(dsInfo, readFile, "localhost") + require.NoError(t, err) + require.NotNil(t, c) + require.Len(t, c.Certificates, 1) + require.NotNil(t, c.RootCAs) // TODO: not the best, but nothing better available +} + +func TestTLSMethodFilePathRootCertDoesNotExist(t *testing.T) { + readFile := newMockReadFile(map[string]([]byte){}) + + dsInfo := sqleng.DataSourceInfo{ + JsonData: sqleng.JsonData{ + Mode: "verify-full", + ConfigurationMethod: "file-path", + RootCertFile: "path1", + }, + } + _, err := GetTLSConfig(dsInfo, readFile, "localhost") + require.ErrorIs(t, err, os.ErrNotExist) +} + +func TestTLSMethodFilePathClientCertKeyDoesNotExist(t *testing.T) { + _, clientCert, err := CreateRandomClientCert() + require.NoError(t, err) + + readFile := newMockReadFile(map[string]([]byte){ + "cert-path": clientCert, + }) + + dsInfo := sqleng.DataSourceInfo{ + JsonData: sqleng.JsonData{ + Mode: "require", + ConfigurationMethod: "file-path", + CertKeyFile: "key-path", + CertFile: "cert-path", + }, + } + _, err = GetTLSConfig(dsInfo, readFile, "localhost") + require.ErrorIs(t, err, os.ErrNotExist) +} + +func TestTLSMethodFilePathClientCertCertDoesNotExist(t *testing.T) { + clientKey, _, err := CreateRandomClientCert() + require.NoError(t, err) + + readFile := newMockReadFile(map[string]([]byte){ + "key-path": clientKey, + }) + + dsInfo := sqleng.DataSourceInfo{ + JsonData: sqleng.JsonData{ + Mode: "require", + ConfigurationMethod: "file-path", + CertKeyFile: "key-path", + CertFile: "cert-path", + }, + } + _, err = GetTLSConfig(dsInfo, readFile, "localhost") + require.ErrorIs(t, err, os.ErrNotExist) +} + +// method="" equals to method="file-path" +func TestTLSMethodEmpty(t *testing.T) { + rootCertBytes, err := CreateRandomRootCertBytes() + require.NoError(t, err) + + clientKey, clientCert, err := CreateRandomClientCert() + require.NoError(t, err) + + readFile := newMockReadFile(map[string]([]byte){ + "root-cert-path": rootCertBytes, + "client-key-path": clientKey, + "client-cert-path": clientCert, + }) + + dsInfo := sqleng.DataSourceInfo{ + JsonData: sqleng.JsonData{ + Mode: "verify-full", + ConfigurationMethod: "", + RootCertFile: "root-cert-path", + CertKeyFile: "client-key-path", + CertFile: "client-cert-path", + }, + } + c, err := GetTLSConfig(dsInfo, readFile, "localhost") + require.NoError(t, err) + require.NotNil(t, c) + require.Len(t, c.Certificates, 1) + require.NotNil(t, c.RootCAs) // TODO: not the best, but nothing better available +} + +func TestTLSVerifyFullMisingRootCert(t *testing.T) { + dsInfo := sqleng.DataSourceInfo{ + JsonData: sqleng.JsonData{ + Mode: "verify-full", + ConfigurationMethod: "file-content", + }, + DecryptedSecureJSONData: map[string]string{}, + } + _, err := GetTLSConfig(dsInfo, noReadFile, "localhost") + require.ErrorIs(t, err, errNoRootCert) +} + +func TestTLSInvalidMode(t *testing.T) { + dsInfo := sqleng.DataSourceInfo{ + JsonData: sqleng.JsonData{ + Mode: "not-a-valid-mode", + }, + } + + _, err := GetTLSConfig(dsInfo, noReadFile, "localhost") + require.Error(t, err) +} diff --git a/pkg/tsdb/grafana-postgresql-datasource/tls/tls_test_helpers.go b/pkg/tsdb/grafana-postgresql-datasource/tls/tls_test_helpers.go new file mode 100644 index 0000000000..1b62df63d0 --- /dev/null +++ b/pkg/tsdb/grafana-postgresql-datasource/tls/tls_test_helpers.go @@ -0,0 +1,105 @@ +package tls + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" + "os" + "time" +) + +func CreateRandomRootCertBytes() ([]byte, error) { + cert := x509.Certificate{ + SerialNumber: big.NewInt(42), + Subject: pkix.Name{ + CommonName: "test1", + }, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(10, 0, 0), + IsCA: true, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + BasicConstraintsValid: true, + } + + key, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, err + } + + bytes, err := x509.CreateCertificate(rand.Reader, &cert, &cert, &key.PublicKey, key) + if err != nil { + return nil, err + } + + return pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: bytes, + }), nil +} + +func CreateRandomClientCert() ([]byte, []byte, error) { + caKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, nil, err + } + + key, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, nil, err + } + + keyBytes := pem.EncodeToMemory(&pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(key), + }) + + caCert := x509.Certificate{ + SerialNumber: big.NewInt(42), + Subject: pkix.Name{ + CommonName: "test1", + }, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(10, 0, 0), + IsCA: true, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + BasicConstraintsValid: true, + } + + cert := x509.Certificate{ + SerialNumber: big.NewInt(2019), + Subject: pkix.Name{ + CommonName: "test1", + }, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(10, 0, 0), + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + KeyUsage: x509.KeyUsageDigitalSignature, + } + + certData, err := x509.CreateCertificate(rand.Reader, &cert, &caCert, &key.PublicKey, caKey) + if err != nil { + return nil, nil, err + } + + certBytes := pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: certData, + }) + + return keyBytes, certBytes, nil +} + +func newMockReadFile(data map[string]([]byte)) ReadFileFunc { + return func(path string) ([]byte, error) { + bytes, ok := data[path] + if !ok { + return nil, os.ErrNotExist + } + return bytes, nil + } +} diff --git a/pkg/tsdb/grafana-postgresql-datasource/tlsmanager.go b/pkg/tsdb/grafana-postgresql-datasource/tlsmanager.go deleted file mode 100644 index 116872d061..0000000000 --- a/pkg/tsdb/grafana-postgresql-datasource/tlsmanager.go +++ /dev/null @@ -1,249 +0,0 @@ -package postgres - -import ( - "fmt" - "os" - "path/filepath" - "strconv" - "strings" - "sync" - "time" - - "github.com/grafana/grafana-plugin-sdk-go/backend/log" - "github.com/grafana/grafana/pkg/tsdb/sqleng" -) - -var validateCertFunc = validateCertFilePaths -var writeCertFileFunc = writeCertFile - -type certFileType int - -const ( - rootCert = iota - clientCert - clientKey -) - -type tlsSettingsProvider interface { - getTLSSettings(dsInfo sqleng.DataSourceInfo) (tlsSettings, error) -} - -type datasourceCacheManager struct { - locker *locker - cache sync.Map -} - -type tlsManager struct { - logger log.Logger - dsCacheInstance datasourceCacheManager - dataPath string -} - -func newTLSManager(logger log.Logger, dataPath string) tlsSettingsProvider { - return &tlsManager{ - logger: logger, - dataPath: dataPath, - dsCacheInstance: datasourceCacheManager{locker: newLocker()}, - } -} - -type tlsSettings struct { - Mode string - ConfigurationMethod string - RootCertFile string - CertFile string - CertKeyFile string -} - -func (m *tlsManager) getTLSSettings(dsInfo sqleng.DataSourceInfo) (tlsSettings, error) { - tlsconfig := tlsSettings{ - Mode: dsInfo.JsonData.Mode, - } - - isTLSDisabled := (tlsconfig.Mode == "disable") - - if isTLSDisabled { - m.logger.Debug("Postgres TLS/SSL is disabled") - return tlsconfig, nil - } - - m.logger.Debug("Postgres TLS/SSL is enabled", "tlsMode", tlsconfig.Mode) - - tlsconfig.ConfigurationMethod = dsInfo.JsonData.ConfigurationMethod - tlsconfig.RootCertFile = dsInfo.JsonData.RootCertFile - tlsconfig.CertFile = dsInfo.JsonData.CertFile - tlsconfig.CertKeyFile = dsInfo.JsonData.CertKeyFile - - if tlsconfig.ConfigurationMethod == "file-content" { - if err := m.writeCertFiles(dsInfo, &tlsconfig); err != nil { - return tlsconfig, err - } - } else { - if err := validateCertFunc(tlsconfig.RootCertFile, tlsconfig.CertFile, tlsconfig.CertKeyFile); err != nil { - return tlsconfig, err - } - } - return tlsconfig, nil -} - -func (t certFileType) String() string { - switch t { - case rootCert: - return "root certificate" - case clientCert: - return "client certificate" - case clientKey: - return "client key" - default: - panic(fmt.Sprintf("Unrecognized certFileType %d", t)) - } -} - -func getFileName(dataDir string, fileType certFileType) string { - var filename string - switch fileType { - case rootCert: - filename = "root.crt" - case clientCert: - filename = "client.crt" - case clientKey: - filename = "client.key" - default: - panic(fmt.Sprintf("unrecognized certFileType %s", fileType.String())) - } - generatedFilePath := filepath.Join(dataDir, filename) - return generatedFilePath -} - -// writeCertFile writes a certificate file. -func writeCertFile(logger log.Logger, fileContent string, generatedFilePath string) error { - fileContent = strings.TrimSpace(fileContent) - if fileContent != "" { - logger.Debug("Writing cert file", "path", generatedFilePath) - if err := os.WriteFile(generatedFilePath, []byte(fileContent), 0600); err != nil { - return err - } - // Make sure the file has the permissions expected by the Postgresql driver, otherwise it will bail - if err := os.Chmod(generatedFilePath, 0600); err != nil { - return err - } - return nil - } - - logger.Debug("Deleting cert file since no content is provided", "path", generatedFilePath) - exists, err := fileExists(generatedFilePath) - if err != nil { - return err - } - if exists { - if err := os.Remove(generatedFilePath); err != nil { - return fmt.Errorf("failed to remove %q: %w", generatedFilePath, err) - } - } - return nil -} - -func (m *tlsManager) writeCertFiles(dsInfo sqleng.DataSourceInfo, tlsconfig *tlsSettings) error { - m.logger.Debug("Writing TLS certificate files to disk") - tlsRootCert := dsInfo.DecryptedSecureJSONData["tlsCACert"] - tlsClientCert := dsInfo.DecryptedSecureJSONData["tlsClientCert"] - tlsClientKey := dsInfo.DecryptedSecureJSONData["tlsClientKey"] - if tlsRootCert == "" && tlsClientCert == "" && tlsClientKey == "" { - m.logger.Debug("No TLS/SSL certificates provided") - } - - // Calculate all files path - workDir := filepath.Join(m.dataPath, "tls", dsInfo.UID+"generatedTLSCerts") - tlsconfig.RootCertFile = getFileName(workDir, rootCert) - tlsconfig.CertFile = getFileName(workDir, clientCert) - tlsconfig.CertKeyFile = getFileName(workDir, clientKey) - - // Find datasource in the cache, if found, skip writing files - cacheKey := strconv.Itoa(int(dsInfo.ID)) - m.dsCacheInstance.locker.RLock(cacheKey) - item, ok := m.dsCacheInstance.cache.Load(cacheKey) - m.dsCacheInstance.locker.RUnlock(cacheKey) - if ok { - if !item.(time.Time).Before(dsInfo.Updated) { - return nil - } - } - - m.dsCacheInstance.locker.Lock(cacheKey) - defer m.dsCacheInstance.locker.Unlock(cacheKey) - - item, ok = m.dsCacheInstance.cache.Load(cacheKey) - if ok { - if !item.(time.Time).Before(dsInfo.Updated) { - return nil - } - } - - // Write certification directory and files - exists, err := fileExists(workDir) - if err != nil { - return err - } - if !exists { - if err := os.MkdirAll(workDir, 0700); err != nil { - return err - } - } - - if err = writeCertFileFunc(m.logger, tlsRootCert, tlsconfig.RootCertFile); err != nil { - return err - } - if err = writeCertFileFunc(m.logger, tlsClientCert, tlsconfig.CertFile); err != nil { - return err - } - if err = writeCertFileFunc(m.logger, tlsClientKey, tlsconfig.CertKeyFile); err != nil { - return err - } - - // we do not want to point to cert-files that do not exist - if tlsRootCert == "" { - tlsconfig.RootCertFile = "" - } - - if tlsClientCert == "" { - tlsconfig.CertFile = "" - } - - if tlsClientKey == "" { - tlsconfig.CertKeyFile = "" - } - - // Update datasource cache - m.dsCacheInstance.cache.Store(cacheKey, dsInfo.Updated) - return nil -} - -// validateCertFilePaths validates configured certificate file paths. -func validateCertFilePaths(rootCert, clientCert, clientKey string) error { - for _, fpath := range []string{rootCert, clientCert, clientKey} { - if fpath == "" { - continue - } - exists, err := fileExists(fpath) - if err != nil { - return err - } - if !exists { - return fmt.Errorf("certificate file %q doesn't exist", fpath) - } - } - return nil -} - -// Exists determines whether a file/directory exists or not. -func fileExists(fpath string) (bool, error) { - _, err := os.Stat(fpath) - if err != nil { - if !os.IsNotExist(err) { - return false, err - } - return false, nil - } - - return true, nil -} diff --git a/pkg/tsdb/grafana-postgresql-datasource/tlsmanager_test.go b/pkg/tsdb/grafana-postgresql-datasource/tlsmanager_test.go deleted file mode 100644 index 8e60e841ca..0000000000 --- a/pkg/tsdb/grafana-postgresql-datasource/tlsmanager_test.go +++ /dev/null @@ -1,332 +0,0 @@ -package postgres - -import ( - "fmt" - "path/filepath" - "strconv" - "strings" - "sync" - "testing" - "time" - - "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana-plugin-sdk-go/backend/log" - "github.com/grafana/grafana/pkg/setting" - "github.com/grafana/grafana/pkg/tsdb/sqleng" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - _ "github.com/lib/pq" -) - -var writeCertFileCallNum int - -// TestDataSourceCacheManager is to test the Cache manager -func TestDataSourceCacheManager(t *testing.T) { - cfg := setting.NewCfg() - cfg.DataPath = t.TempDir() - mng := tlsManager{ - logger: backend.NewLoggerWith("logger", "tsdb.postgres"), - dsCacheInstance: datasourceCacheManager{locker: newLocker()}, - dataPath: cfg.DataPath, - } - jsonData := sqleng.JsonData{ - Mode: "verify-full", - ConfigurationMethod: "file-content", - } - secureJSONData := map[string]string{ - "tlsClientCert": "I am client certification", - "tlsClientKey": "I am client key", - "tlsCACert": "I am CA certification", - } - - updateTime := time.Now().Add(-5 * time.Minute) - - mockValidateCertFilePaths() - t.Cleanup(resetValidateCertFilePaths) - - t.Run("Check datasource cache creation", func(t *testing.T) { - var wg sync.WaitGroup - wg.Add(10) - for id := int64(1); id <= 10; id++ { - go func(id int64) { - ds := sqleng.DataSourceInfo{ - ID: id, - Updated: updateTime, - Database: "database", - JsonData: jsonData, - DecryptedSecureJSONData: secureJSONData, - UID: "testData", - } - s := tlsSettings{} - err := mng.writeCertFiles(ds, &s) - require.NoError(t, err) - wg.Done() - }(id) - } - wg.Wait() - - t.Run("check cache creation is succeed", func(t *testing.T) { - for id := int64(1); id <= 10; id++ { - updated, ok := mng.dsCacheInstance.cache.Load(strconv.Itoa(int(id))) - require.True(t, ok) - require.Equal(t, updateTime, updated) - } - }) - }) - - t.Run("Check datasource cache modification", func(t *testing.T) { - t.Run("check when version not changed, cache and files are not updated", func(t *testing.T) { - mockWriteCertFile() - t.Cleanup(resetWriteCertFile) - var wg1 sync.WaitGroup - wg1.Add(5) - for id := int64(1); id <= 5; id++ { - go func(id int64) { - ds := sqleng.DataSourceInfo{ - ID: 1, - Updated: updateTime, - Database: "database", - JsonData: jsonData, - DecryptedSecureJSONData: secureJSONData, - UID: "testData", - } - s := tlsSettings{} - err := mng.writeCertFiles(ds, &s) - require.NoError(t, err) - wg1.Done() - }(id) - } - wg1.Wait() - assert.Equal(t, writeCertFileCallNum, 0) - }) - - t.Run("cache is updated with the last datasource version", func(t *testing.T) { - dsV2 := sqleng.DataSourceInfo{ - ID: 1, - Updated: updateTime.Add(time.Minute), - Database: "database", - JsonData: jsonData, - DecryptedSecureJSONData: secureJSONData, - UID: "testData", - } - dsV3 := sqleng.DataSourceInfo{ - ID: 1, - Updated: updateTime.Add(2 * time.Minute), - Database: "database", - JsonData: jsonData, - DecryptedSecureJSONData: secureJSONData, - UID: "testData", - } - s := tlsSettings{} - err := mng.writeCertFiles(dsV2, &s) - require.NoError(t, err) - err = mng.writeCertFiles(dsV3, &s) - require.NoError(t, err) - version, ok := mng.dsCacheInstance.cache.Load("1") - require.True(t, ok) - require.Equal(t, updateTime.Add(2*time.Minute), version) - }) - }) -} - -// Test getFileName - -func TestGetFileName(t *testing.T) { - testCases := []struct { - desc string - datadir string - fileType certFileType - expErr string - expectedGeneratedPath string - }{ - { - desc: "Get File Name for root certification", - datadir: ".", - fileType: rootCert, - expectedGeneratedPath: "root.crt", - }, - { - desc: "Get File Name for client certification", - datadir: ".", - fileType: clientCert, - expectedGeneratedPath: "client.crt", - }, - { - desc: "Get File Name for client certification", - datadir: ".", - fileType: clientKey, - expectedGeneratedPath: "client.key", - }, - } - for _, tt := range testCases { - t.Run(tt.desc, func(t *testing.T) { - generatedPath := getFileName(tt.datadir, tt.fileType) - assert.Equal(t, tt.expectedGeneratedPath, generatedPath) - }) - } -} - -// Test getTLSSettings. -func TestGetTLSSettings(t *testing.T) { - cfg := setting.NewCfg() - cfg.DataPath = t.TempDir() - - mockValidateCertFilePaths() - t.Cleanup(resetValidateCertFilePaths) - - updatedTime := time.Now() - - testCases := []struct { - desc string - expErr string - jsonData sqleng.JsonData - secureJSONData map[string]string - uid string - tlsSettings tlsSettings - updated time.Time - }{ - { - desc: "Custom TLS authentication disabled", - updated: updatedTime, - jsonData: sqleng.JsonData{ - Mode: "disable", - RootCertFile: "i/am/coding/ca.crt", - CertFile: "i/am/coding/client.crt", - CertKeyFile: "i/am/coding/client.key", - ConfigurationMethod: "file-path", - }, - tlsSettings: tlsSettings{Mode: "disable"}, - }, - { - desc: "Custom TLS authentication with file path", - updated: updatedTime.Add(time.Minute), - jsonData: sqleng.JsonData{ - Mode: "verify-full", - ConfigurationMethod: "file-path", - RootCertFile: "i/am/coding/ca.crt", - CertFile: "i/am/coding/client.crt", - CertKeyFile: "i/am/coding/client.key", - }, - tlsSettings: tlsSettings{ - Mode: "verify-full", - ConfigurationMethod: "file-path", - RootCertFile: "i/am/coding/ca.crt", - CertFile: "i/am/coding/client.crt", - CertKeyFile: "i/am/coding/client.key", - }, - }, - { - desc: "Custom TLS mode verify-full with certificate files content", - updated: updatedTime.Add(2 * time.Minute), - uid: "xxx", - jsonData: sqleng.JsonData{ - Mode: "verify-full", - ConfigurationMethod: "file-content", - }, - secureJSONData: map[string]string{ - "tlsCACert": "I am CA certification", - "tlsClientCert": "I am client certification", - "tlsClientKey": "I am client key", - }, - tlsSettings: tlsSettings{ - Mode: "verify-full", - ConfigurationMethod: "file-content", - RootCertFile: filepath.Join(cfg.DataPath, "tls", "xxxgeneratedTLSCerts", "root.crt"), - CertFile: filepath.Join(cfg.DataPath, "tls", "xxxgeneratedTLSCerts", "client.crt"), - CertKeyFile: filepath.Join(cfg.DataPath, "tls", "xxxgeneratedTLSCerts", "client.key"), - }, - }, - { - desc: "Custom TLS mode verify-ca with no client certificates with certificate files content", - updated: updatedTime.Add(3 * time.Minute), - uid: "xxx", - jsonData: sqleng.JsonData{ - Mode: "verify-ca", - ConfigurationMethod: "file-content", - }, - secureJSONData: map[string]string{ - "tlsCACert": "I am CA certification", - }, - tlsSettings: tlsSettings{ - Mode: "verify-ca", - ConfigurationMethod: "file-content", - RootCertFile: filepath.Join(cfg.DataPath, "tls", "xxxgeneratedTLSCerts", "root.crt"), - CertFile: "", - CertKeyFile: "", - }, - }, - { - desc: "Custom TLS mode require with client certificates and no root certificate with certificate files content", - updated: updatedTime.Add(4 * time.Minute), - uid: "xxx", - jsonData: sqleng.JsonData{ - Mode: "require", - ConfigurationMethod: "file-content", - }, - secureJSONData: map[string]string{ - "tlsClientCert": "I am client certification", - "tlsClientKey": "I am client key", - }, - tlsSettings: tlsSettings{ - Mode: "require", - ConfigurationMethod: "file-content", - RootCertFile: "", - CertFile: filepath.Join(cfg.DataPath, "tls", "xxxgeneratedTLSCerts", "client.crt"), - CertKeyFile: filepath.Join(cfg.DataPath, "tls", "xxxgeneratedTLSCerts", "client.key"), - }, - }, - } - for _, tt := range testCases { - t.Run(tt.desc, func(t *testing.T) { - var settings tlsSettings - var err error - mng := tlsManager{ - logger: backend.NewLoggerWith("logger", "tsdb.postgres"), - dsCacheInstance: datasourceCacheManager{locker: newLocker()}, - dataPath: cfg.DataPath, - } - - ds := sqleng.DataSourceInfo{ - JsonData: tt.jsonData, - DecryptedSecureJSONData: tt.secureJSONData, - UID: tt.uid, - Updated: tt.updated, - } - - settings, err = mng.getTLSSettings(ds) - - if tt.expErr == "" { - require.NoError(t, err, tt.desc) - assert.Equal(t, tt.tlsSettings, settings) - } else { - require.Error(t, err, tt.desc) - assert.True(t, strings.HasPrefix(err.Error(), tt.expErr), - fmt.Sprintf("%s: %q doesn't start with %q", tt.desc, err, tt.expErr)) - } - }) - } -} - -func mockValidateCertFilePaths() { - validateCertFunc = func(rootCert, clientCert, clientKey string) error { - return nil - } -} - -func resetValidateCertFilePaths() { - validateCertFunc = validateCertFilePaths -} - -func mockWriteCertFile() { - writeCertFileCallNum = 0 - writeCertFileFunc = func(logger log.Logger, fileContent string, generatedFilePath string) error { - writeCertFileCallNum++ - return nil - } -} - -func resetWriteCertFile() { - writeCertFileCallNum = 0 - writeCertFileFunc = writeCertFile -} From 3901077f395796a948a7353292945cd7f1722853 Mon Sep 17 00:00:00 2001 From: brendamuir <100768211+brendamuir@users.noreply.github.com> Date: Wed, 28 Feb 2024 09:10:19 +0100 Subject: [PATCH 0252/1406] Alerting docs: changes weights and titles (#83519) * Alerting docs: changes weights and titles * language improvements to intro topic * corrects rulle spelling * adds alert instance info * updates admonition note --- .../create-grafana-managed-rule.md | 8 ++-- .../create-notification-policy.md | 2 +- .../configure-notifications/create-silence.md | 4 +- .../configure-notifications/mute-timings.md | 4 +- .../template-notifications/_index.md | 8 ++-- docs/sources/alerting/fundamentals/_index.md | 45 +++++++++++++------ .../fundamentals/alert-rules/_index.md | 2 +- .../alerting/fundamentals/alertmanager.md | 2 +- .../fundamentals/annotation-label/_index.md | 2 +- .../fundamentals/contact-points/index.md | 2 +- .../fundamentals/data-source-alerting.md | 2 +- .../fundamentals/evaluate-grafana-alerts.md | 2 +- .../notification-policies/_index.md | 2 +- 13 files changed, 51 insertions(+), 34 deletions(-) diff --git a/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md b/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md index a26c9c2ca7..f7b5a38d75 100644 --- a/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md +++ b/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md @@ -171,16 +171,16 @@ Complete the following steps to set up labels and notifications. 2. You can also optionally select a mute timing as well as groupings and timings to define when not to send notifications. - {{% admonition type="note" %}} - An auto-generated notification policy is generated. Only admins can view these auto-generated policies from the **Notification policies** list view. Any changes have to be made in the alert rules form. {{% /admonition %}} + {{< admonition type="note" >}} + An auto-generated notification policy is generated. Only admins can view these auto-generated policies from the **Notification policies** list view. Any changes have to be made in the alert rules form. {{< /admonition >}} **Use notification policy** 3. Choose this option to use the notification policy tree to direct your notifications. - {{% admonition type="note" %}} + {{< admonition type="note" >}} All alert rules and instances, irrespective of their labels, match the default notification policy. If there are no nested policies, or no nested policies match the labels in the alert rule or alert instance, then the default notification policy is the matching policy. - {{% /admonition %}} + {{< /admonition >}} 4. Preview your alert instance routing set up. diff --git a/docs/sources/alerting/configure-notifications/create-notification-policy.md b/docs/sources/alerting/configure-notifications/create-notification-policy.md index 55d0e33192..1cdd8ddccf 100644 --- a/docs/sources/alerting/configure-notifications/create-notification-policy.md +++ b/docs/sources/alerting/configure-notifications/create-notification-policy.md @@ -18,7 +18,7 @@ labels: - enterprise - oss title: Configure notification policies -weight: 430 +weight: 420 --- # Configure notification policies diff --git a/docs/sources/alerting/configure-notifications/create-silence.md b/docs/sources/alerting/configure-notifications/create-silence.md index 874cd40df5..ab6cfd9287 100644 --- a/docs/sources/alerting/configure-notifications/create-silence.md +++ b/docs/sources/alerting/configure-notifications/create-silence.md @@ -19,11 +19,11 @@ labels: - cloud - enterprise - oss -title: Manage silences +title: Configure silences weight: 440 --- -# Manage silences +# Configure silences Silences stop notifications from getting created and last for only a specified window of time. diff --git a/docs/sources/alerting/configure-notifications/mute-timings.md b/docs/sources/alerting/configure-notifications/mute-timings.md index 2c4d9a52dc..c2c680b1c3 100644 --- a/docs/sources/alerting/configure-notifications/mute-timings.md +++ b/docs/sources/alerting/configure-notifications/mute-timings.md @@ -17,11 +17,11 @@ labels: - cloud - enterprise - oss -title: Create mute timings +title: Configure mute timings weight: 450 --- -# Create mute timings +# Configure mute timings A mute timing is a recurring interval of time when no new notifications for a policy are generated or sent. Use them to prevent alerts from firing a specific and reoccurring period, for example, a regular maintenance period. diff --git a/docs/sources/alerting/configure-notifications/template-notifications/_index.md b/docs/sources/alerting/configure-notifications/template-notifications/_index.md index c096b044ca..d160522b34 100644 --- a/docs/sources/alerting/configure-notifications/template-notifications/_index.md +++ b/docs/sources/alerting/configure-notifications/template-notifications/_index.md @@ -13,13 +13,13 @@ labels: - cloud - enterprise - oss -title: Customize notifications -weight: 420 +title: Configure notification messages +weight: 430 --- -# Customize notifications +# Configure notification messages -Customize your notifications with notifications templates. +Customize the content of your notifications with notifications templates. You can use notification templates to change the title, message, and format of the message in your notifications. diff --git a/docs/sources/alerting/fundamentals/_index.md b/docs/sources/alerting/fundamentals/_index.md index 45c1427cf8..3066c26877 100644 --- a/docs/sources/alerting/fundamentals/_index.md +++ b/docs/sources/alerting/fundamentals/_index.md @@ -18,33 +18,42 @@ weight: 100 Whether you’re just starting out or you're a more experienced user of Grafana Alerting, learn more about the fundamentals and available features that help you create, manage, and respond to alerts; and improve your team’s ability to resolve issues quickly. -## Principles - -In Prometheus-based alerting systems, you have an alert generator that creates alerts and an alert receiver that receives alerts. For example, Prometheus is an alert generator and is responsible for evaluating alert rules, while Alertmanager is an alert receiver and is responsible for grouping, inhibiting, silencing, and sending notifications about firing and resolved alerts. - -Grafana Alerting is built on the Prometheus model of designing alerting systems. It has an internal alert generator responsible for scheduling and evaluating alert rules, as well as an internal alert receiver responsible for grouping, inhibiting, silencing, and sending notifications. Grafana doesn’t use Prometheus as its alert generator because Grafana Alerting needs to work with many other data sources in addition to Prometheus. However, it does use Alertmanager as its alert receiver. - -Alerts are sent to the alert receiver where they are routed, grouped, inhibited, silenced and notified. In Grafana Alerting, the default alert receiver is the Alertmanager embedded inside Grafana, and is referred to as the Grafana Alertmanager. However, you can use other Alertmanagers too, and these are referred to as [External Alertmanagers][external-alertmanagers]. - The following diagram gives you an overview of Grafana Alerting and introduces you to some of the fundamental features that are the principles of how Grafana Alerting works. {{< figure src="/media/docs/alerting/how-alerting-works.png" max-width="750px" caption="How Alerting works" >}} +## How it works at a glance + +- Grafana alerting periodically queries data sources and evaluates the condition defined in the alert rule +- If the condition is breached, an alert instance fires +- Firing instances are routed to notification policies based on matching labels +- Notifications are sent out to the contact points specified in the notification policy + ## Fundamentals +The following concepts are key to your understanding of how Grafana Alerting works. + ### Alert rules -An alert rule is a set of criteria that determine when an alert should fire. It consists of one or more queries and expressions, a condition which needs to be met, an interval which determines how often the alert rule is evaluated, and a duration over which the condition must be met for an alert to fire. +An alert rule consists of one or more queries and expressions that select the data you want to measure. It also contains a condition, which is the threshold that an alert rule must meet or exceed in order to fire. + +Add annotations to your alert rule to provide additional information about the alert rule and add labels to uniquely identify your alert rule and configure alert routing. Labels link alert rules to notification policies, so you can easily manage which policy should handle which alerts and who gets notified. -Alert rules are evaluated over their interval, and each alert rule can have zero, one, or any number of alerts firing at a time. The state of the alert rule is determined by its most "severe" alert, which can be one of Normal, Pending, or Firing. For example, if at least one of an alert rule's alerts are firing then the alert rule is also firing. The health of an alert rule is determined by the status of its most recent evaluation. These can be OK, Error, and NoData. +Once alert rules are created, they go through various states and transitions. An alert rule can produce multiple alert instances - one alert instance for each time series. -A very important feature of alert rules is that they support custom annotations and labels. These allow you to instrument alerts with additional metadata such as summaries and descriptions, and add additional labels to route alerts to specific notification policies. +The alert rule state is determined by the “worst case” state of the alert instances produced and the states can be Normal, Pending, or Firing. For example, if one alert instance is firing, the alert rule state will also be firing. -### Alerts +The alert rule health is determined by the status of the evaluation of the alert rule, which can be Ok, Error, and NoData. -Alerts are uniquely identified by sets of key/value pairs called Labels. Each key is a label name and each value is a label value. For example, one alert might have the labels `foo=bar` and another alert might have the labels `foo=baz`. An alert can have many labels such as `foo=bar,bar=baz` but it cannot have the same label twice such as `foo=bar,foo=baz`. Two alerts cannot have the same labels either, and if two alerts have the same labels such as `foo=bar,bar=baz` and `foo=bar,bar=baz` then one of the alerts will be discarded. Alerts are resolved when the condition in the alert rule is no longer met, or the alert rule is deleted. +### Labels and states -In Grafana Managed Alerts, alerts can be in Normal, Pending, Alerting, No Data or Error states. In Datasource Managed Alerts, such as Mimir and Loki, alerts can be in Normal, Pending and Alerting, but not NoData or Error. +Alert rules are uniquely identified by sets of key/value pairs called labels. Each key is a label name and each value is a label value. For example, one alert might have the labels `foo=bar` and another alert rule might have the labels `foo=baz`. An alert rule can have many labels such as `foo=bar,bar=baz`, but it cannot have the same label twice such as `foo=bar,foo=baz`. Two alert rules cannot have the same labels either, and if two alert rules have the same labels such as `foo=bar,bar=baz` and `foo=bar,bar=baz` then one of the alerts will be discarded. Firing alerts are resolved when the condition in the alert rule is no longer met, or the alert rule is deleted. + +In Grafana-managed alert rules, alert rules can be in Normal, Pending, Alerting, No Data or Error states. In datasource-managed alert rules, such as Mimir and Loki, alert rules can be in Normal, Pending and Alerting, but not NoData or Error. + +### Alert instances + +For Grafana-managed alert rules, multiple alert instances can be created as a result of one alert rule (also known as a multi-dimensional alerting) and they can be in Normal, Pending, Alerting, No Data, Error states. For Mimir or Loki-managed alert rules, alert instances are only created when the threshold condition defined in an alert rule is breached. ### Contact points @@ -68,6 +77,14 @@ Silences and mute timings allow you to pause notifications for specific alerts o You can create your alerting resources (alert rules, notification policies, and so on) in the Grafana UI; configmaps, files and configuration management systems using file-based provisioning; and in Terraform using API-based provisioning. +## Principles + +In Prometheus-based alerting systems, you have an alert generator that creates alerts and an alert receiver that receives alerts. For example, Prometheus is an alert generator and is responsible for evaluating alert rules, while Alertmanager is an alert receiver and is responsible for grouping, inhibiting, silencing, and sending notifications about firing and resolved alerts. + +Grafana Alerting is built on the Prometheus model of designing alerting systems. It has an internal alert generator responsible for scheduling and evaluating alert rules, as well as an internal alert receiver responsible for grouping, inhibiting, silencing, and sending notifications. Grafana doesn’t use Prometheus as its alert generator because Grafana Alerting needs to work with many other data sources in addition to Prometheus. However, it does use Alertmanager as its alert receiver. + +Alerts are sent to the alert receiver where they are routed, grouped, inhibited, silenced and notified. In Grafana Alerting, the default alert receiver is the Alertmanager embedded inside Grafana, and is referred to as the Grafana Alertmanager. However, you can use other Alertmanagers too, and these are referred to as [External Alertmanagers][external-alertmanagers]. + {{% docs/reference %}} [external-alertmanagers]: "/docs/grafana/ -> /docs/grafana//alerting/set-up/configure-alertmanager" [external-alertmanagers]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/set-up/configure-alertmanager" diff --git a/docs/sources/alerting/fundamentals/alert-rules/_index.md b/docs/sources/alerting/fundamentals/alert-rules/_index.md index a7ed8f0f7a..40bf58e29f 100644 --- a/docs/sources/alerting/fundamentals/alert-rules/_index.md +++ b/docs/sources/alerting/fundamentals/alert-rules/_index.md @@ -11,7 +11,7 @@ labels: - enterprise - oss title: Alert rules -weight: 130 +weight: 100 --- # Alert rules diff --git a/docs/sources/alerting/fundamentals/alertmanager.md b/docs/sources/alerting/fundamentals/alertmanager.md index 9bdd9fbb08..3e42d857d3 100644 --- a/docs/sources/alerting/fundamentals/alertmanager.md +++ b/docs/sources/alerting/fundamentals/alertmanager.md @@ -12,7 +12,7 @@ labels: - enterprise - oss title: Alertmanager -weight: 140 +weight: 150 --- # Alertmanager diff --git a/docs/sources/alerting/fundamentals/annotation-label/_index.md b/docs/sources/alerting/fundamentals/annotation-label/_index.md index 690902b7cc..65ef622c1f 100644 --- a/docs/sources/alerting/fundamentals/annotation-label/_index.md +++ b/docs/sources/alerting/fundamentals/annotation-label/_index.md @@ -16,7 +16,7 @@ labels: - enterprise - oss title: Labels and annotations -weight: 120 +weight: 130 --- # Labels and annotations diff --git a/docs/sources/alerting/fundamentals/contact-points/index.md b/docs/sources/alerting/fundamentals/contact-points/index.md index 5494a9b6c0..4e42886a80 100644 --- a/docs/sources/alerting/fundamentals/contact-points/index.md +++ b/docs/sources/alerting/fundamentals/contact-points/index.md @@ -18,7 +18,7 @@ labels: - enterprise - oss title: Contact points -weight: 150 +weight: 120 --- # Contact points diff --git a/docs/sources/alerting/fundamentals/data-source-alerting.md b/docs/sources/alerting/fundamentals/data-source-alerting.md index cae5474a88..fcec0f2352 100644 --- a/docs/sources/alerting/fundamentals/data-source-alerting.md +++ b/docs/sources/alerting/fundamentals/data-source-alerting.md @@ -7,7 +7,7 @@ labels: - enterprise - oss title: Data sources and Grafana Alerting -weight: 100 +weight: 140 --- # Data sources and Grafana Alerting diff --git a/docs/sources/alerting/fundamentals/evaluate-grafana-alerts.md b/docs/sources/alerting/fundamentals/evaluate-grafana-alerts.md index c7ec0565b2..a238358029 100644 --- a/docs/sources/alerting/fundamentals/evaluate-grafana-alerts.md +++ b/docs/sources/alerting/fundamentals/evaluate-grafana-alerts.md @@ -10,7 +10,7 @@ labels: - enterprise - oss title: Alerting on numeric data -weight: 110 +weight: 160 --- # Alerting on numeric data diff --git a/docs/sources/alerting/fundamentals/notification-policies/_index.md b/docs/sources/alerting/fundamentals/notification-policies/_index.md index 56dc8e06bd..ef689ec6fd 100644 --- a/docs/sources/alerting/fundamentals/notification-policies/_index.md +++ b/docs/sources/alerting/fundamentals/notification-policies/_index.md @@ -11,7 +11,7 @@ labels: - enterprise - oss title: Notifications -weight: 160 +weight: 110 --- # Notifications From ecb8447a7fbad847ad2df6831df9981d2774d95c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Jamr=C3=B3z?= Date: Wed, 28 Feb 2024 09:19:20 +0100 Subject: [PATCH 0253/1406] Explore: Translate table title in runtime to get a better test id (#83236) * Explore: Translate table title in runtime to get a better test id * Fix escaping * Retrigger the build * Prettify --- public/app/features/explore/Table/TableContainer.tsx | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/public/app/features/explore/Table/TableContainer.tsx b/public/app/features/explore/Table/TableContainer.tsx index 5ecc838664..6626977824 100644 --- a/public/app/features/explore/Table/TableContainer.tsx +++ b/public/app/features/explore/Table/TableContainer.tsx @@ -7,7 +7,7 @@ import { getTemplateSrv } from '@grafana/runtime'; import { TimeZone } from '@grafana/schema'; import { Table, AdHocFilterItem, PanelChrome, withTheme2, Themeable2 } from '@grafana/ui'; import { config } from 'app/core/config'; -import { t, Trans } from 'app/core/internationalization'; +import { t } from 'app/core/internationalization'; import { hasDeprecatedParentRowIndex, migrateFromParentRowIndexToNestedFrames, @@ -59,11 +59,9 @@ export class TableContainer extends PureComponent { name = data.refId || `${i}`; } - return name ? ( - Table - {{ name }} - ) : ( - t('explore.table.title', 'Table') - ); + return name + ? t('explore.table.title-with-name', 'Table - {{name}}', { name, interpolation: { escapeValue: false } }) + : t('explore.table.title', 'Table'); } render() { From 738e9126dec1c325c02613e4ce0c9a1b3ac19f3d Mon Sep 17 00:00:00 2001 From: Victor Marin <36818606+mdvictor@users.noreply.github.com> Date: Wed, 28 Feb 2024 11:13:01 +0200 Subject: [PATCH 0254/1406] Scenes: Add new row and copy/paste functionalities (#83231) * wip * tests + refactor ad panel func * Add row functionality * update row state only when there are children * Add new row + copy paste panels * Add library panel functionality * tests * PR mods * reafctor + tests * reafctor * fix test * refactor * fix bug on cancelling lib widget * dashboard now saves with lib panel widget * add lib panels widget works in rows as well * split add lib panel func to another PR --- .../scene/DashboardScene.test.tsx | 123 ++++++++++++++++-- .../dashboard-scene/scene/DashboardScene.tsx | 98 ++++++++++++-- .../scene/NavToolbarActions.test.tsx | 4 + .../scene/NavToolbarActions.tsx | 45 ++++++- .../utils/dashboardSceneGraph.test.ts | 117 ++++++++++++++++- .../utils/dashboardSceneGraph.ts | 60 ++++++++- .../features/dashboard-scene/utils/utils.ts | 61 +++------ 7 files changed, 445 insertions(+), 63 deletions(-) diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx index a9e0cfea3b..045ef0799b 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx @@ -15,7 +15,7 @@ import appEvents from 'app/core/app_events'; import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; import { VariablesChanged } from 'app/features/variables/types'; -import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene'; +import { buildGridItemForPanel, transformSaveModelToScene } from '../serialization/transformSaveModelToScene'; import { DecoratedRevisionModel } from '../settings/VersionsEditView'; import { historySrv } from '../settings/version-history/HistorySrv'; import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; @@ -26,6 +26,7 @@ import { DashboardScene, DashboardSceneState } from './DashboardScene'; jest.mock('../settings/version-history/HistorySrv'); jest.mock('../serialization/transformSaveModelToScene'); +jest.mock('../serialization/transformSceneToSaveModel'); jest.mock('@grafana/runtime', () => ({ ...jest.requireActual('@grafana/runtime'), getDataSourceSrv: () => { @@ -133,7 +134,7 @@ describe('DashboardScene', () => { it('Should add a new panel to the dashboard', () => { const vizPanel = new VizPanel({ title: 'Panel Title', - key: 'panel-4', + key: 'panel-5', pluginId: 'timeseries', $data: new SceneQueryRunner({ key: 'data-query-runner', queries: [{ refId: 'A' }] }), }); @@ -143,9 +144,9 @@ describe('DashboardScene', () => { const body = scene.state.body as SceneGridLayout; const gridItem = body.state.children[0] as SceneGridItem; - expect(scene.state.isDirty).toBe(true); expect(body.state.children.length).toBe(5); - expect(gridItem.state.body!.state.key).toBe('panel-4'); + expect(gridItem.state.body!.state.key).toBe('panel-5'); + expect(gridItem.state.y).toBe(0); }); it('Should create and add a new panel to the dashboard', () => { @@ -154,9 +155,114 @@ describe('DashboardScene', () => { const body = scene.state.body as SceneGridLayout; const gridItem = body.state.children[0] as SceneGridItem; - expect(scene.state.isDirty).toBe(true); expect(body.state.children.length).toBe(5); - expect(gridItem.state.body!.state.key).toBe('panel-4'); + expect(gridItem.state.body!.state.key).toBe('panel-5'); + }); + + it('Should create and add a new row to the dashboard', () => { + scene.onCreateNewRow(); + + const body = scene.state.body as SceneGridLayout; + const gridRow = body.state.children[0] as SceneGridRow; + + expect(body.state.children.length).toBe(3); + expect(gridRow.state.key).toBe('panel-5'); + expect(gridRow.state.children[0].state.key).toBe('griditem-1'); + expect(gridRow.state.children[1].state.key).toBe('griditem-2'); + }); + + it('Should create a row and add all panels in the dashboard under it', () => { + const scene = buildTestScene({ + body: new SceneGridLayout({ + children: [ + new SceneGridItem({ + key: 'griditem-1', + x: 0, + body: new VizPanel({ + title: 'Panel A', + key: 'panel-1', + pluginId: 'table', + $data: new SceneQueryRunner({ key: 'data-query-runner', queries: [{ refId: 'A' }] }), + }), + }), + new SceneGridItem({ + key: 'griditem-2', + body: new VizPanel({ + title: 'Panel B', + key: 'panel-2', + pluginId: 'table', + }), + }), + ], + }), + }); + + scene.onCreateNewRow(); + + const body = scene.state.body as SceneGridLayout; + const gridRow = body.state.children[0] as SceneGridRow; + + expect(body.state.children.length).toBe(1); + expect(gridRow.state.children.length).toBe(2); + }); + + it('Should create and add two new rows, but the second has no children', () => { + scene.onCreateNewRow(); + scene.onCreateNewRow(); + + const body = scene.state.body as SceneGridLayout; + const gridRow = body.state.children[0] as SceneGridRow; + + expect(body.state.children.length).toBe(4); + expect(gridRow.state.children.length).toBe(0); + }); + + it('Should create an empty row when nothing else in dashboard', () => { + const scene = buildTestScene({ + body: new SceneGridLayout({ + children: [], + }), + }); + + scene.onCreateNewRow(); + + const body = scene.state.body as SceneGridLayout; + const gridRow = body.state.children[0] as SceneGridRow; + + expect(body.state.children.length).toBe(1); + expect(gridRow.state.children.length).toBe(0); + }); + + it('Should copy a panel', () => { + const vizPanel = ((scene.state.body as SceneGridLayout).state.children[0] as SceneGridItem).state.body; + scene.copyPanel(vizPanel as VizPanel); + + expect(scene.state.hasCopiedPanel).toBe(true); + }); + + it('Should paste a panel', () => { + scene.setState({ hasCopiedPanel: true }); + jest.spyOn(JSON, 'parse').mockReturnThis(); + jest.mocked(buildGridItemForPanel).mockReturnValue( + new SceneGridItem({ + key: 'griditem-9', + body: new VizPanel({ + title: 'Panel A', + key: 'panel-9', + pluginId: 'table', + }), + }) + ); + + scene.pastePanel(); + + const body = scene.state.body as SceneGridLayout; + const gridItem = body.state.children[0] as SceneGridItem; + + expect(body.state.children.length).toBe(5); + expect(gridItem.state.body!.state.key).toBe('panel-5'); + expect(gridItem.state.y).toBe(0); + expect(scene.state.hasCopiedPanel).toBe(false); }); }); }); @@ -275,6 +381,7 @@ function buildTestScene(overrides?: Partial) { }), }), new SceneGridItem({ + key: 'griditem-2', body: new VizPanel({ title: 'Panel B', key: 'panel-2', @@ -282,12 +389,12 @@ function buildTestScene(overrides?: Partial) { }), }), new SceneGridRow({ - key: 'gridrow-1', + key: 'panel-3', children: [ new SceneGridItem({ body: new VizPanel({ title: 'Panel C', - key: 'panel-3', + key: 'panel-4', pluginId: 'table', }), }), diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index 9bd46d81d6..5bfbe60a68 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -11,6 +11,7 @@ import { sceneGraph, SceneGridItem, SceneGridLayout, + SceneGridRow, SceneObject, SceneObjectBase, SceneObjectState, @@ -28,7 +29,7 @@ import { LS_PANEL_COPY_KEY } from 'app/core/constants'; import { getNavModel } from 'app/core/selectors/navModel'; import store from 'app/core/store'; import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; -import { DashboardModel } from 'app/features/dashboard/state'; +import { DashboardModel, PanelModel } from 'app/features/dashboard/state'; import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher'; import { VariablesChanged } from 'app/features/variables/types'; import { DashboardDTO, DashboardMeta, SaveDashboardResponseDTO } from 'app/types'; @@ -37,12 +38,13 @@ import { ShowConfirmModalEvent } from 'app/types/events'; import { PanelEditor } from '../panel-edit/PanelEditor'; import { SaveDashboardDrawer } from '../saving/SaveDashboardDrawer'; import { DashboardSceneRenderer } from '../scene/DashboardSceneRenderer'; -import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene'; +import { buildGridItemForPanel, transformSaveModelToScene } from '../serialization/transformSaveModelToScene'; import { gridItemToPanel } from '../serialization/transformSceneToSaveModel'; import { DecoratedRevisionModel } from '../settings/VersionsEditView'; import { DashboardEditView } from '../settings/utils'; import { historySrv } from '../settings/version-history'; import { DashboardModelCompatibilityWrapper } from '../utils/DashboardModelCompatibilityWrapper'; +import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; import { djb2Hash } from '../utils/djb2Hash'; import { getDashboardUrl } from '../utils/urlBuilders'; import { @@ -50,8 +52,10 @@ import { NEW_PANEL_WIDTH, forceRenderChildren, getClosestVizPanel, + getDefaultRow, getDefaultVizPanel, getPanelIdForVizPanel, + getVizPanelKeyForPanelId, isPanelClone, } from '../utils/utils'; @@ -102,6 +106,8 @@ export interface DashboardSceneState extends SceneObjectState { editPanel?: PanelEditor; /** Scene object that handles the current drawer or modal */ overlay?: SceneObject; + /** True when a user copies a panel in the dashboard */ + hasCopiedPanel?: boolean; isEmpty?: boolean; } @@ -142,6 +148,7 @@ export class DashboardScene extends SceneObjectBase { editable: true, body: state.body ?? new SceneFlexLayout({ children: [] }), links: state.links ?? [], + hasCopiedPanel: store.exists(LS_PANEL_COPY_KEY), ...state, }); @@ -423,26 +430,46 @@ export class DashboardScene extends SceneObjectBase { return this._initialState; } - public addPanel(vizPanel: VizPanel): void { + public addRow(row: SceneGridRow) { if (!(this.state.body instanceof SceneGridLayout)) { throw new Error('Trying to add a panel in a layout that is not SceneGridLayout'); } const sceneGridLayout = this.state.body; - // move all gridItems below the new one - for (const child of sceneGridLayout.state.children) { - child.setState({ - y: NEW_PANEL_HEIGHT + (child.state.y ?? 0), + // find all panels until the first row and put them into the newly created row. If there are no other rows, + // add all panels to the row. If there are no panels just create an empty row + const indexTillNextRow = sceneGridLayout.state.children.findIndex((child) => child instanceof SceneGridRow); + const rowChildren = sceneGridLayout.state.children + .splice(0, indexTillNextRow === -1 ? sceneGridLayout.state.children.length : indexTillNextRow) + .map((child) => child.clone()); + + if (rowChildren) { + row.setState({ + children: rowChildren, }); } + sceneGridLayout.setState({ + children: [row, ...sceneGridLayout.state.children], + }); + } + + public addPanel(vizPanel: VizPanel): void { + if (!(this.state.body instanceof SceneGridLayout)) { + throw new Error('Trying to add a panel in a layout that is not SceneGridLayout'); + } + + const sceneGridLayout = this.state.body; + + const panelId = getPanelIdForVizPanel(vizPanel); const newGridItem = new SceneGridItem({ height: NEW_PANEL_HEIGHT, width: NEW_PANEL_WIDTH, x: 0, y: 0, body: vizPanel, + key: `grid-item-${panelId}`, }); sceneGridLayout.setState({ @@ -457,7 +484,7 @@ export class DashboardScene extends SceneObjectBase { const gridItem = vizPanel.parent; - if (!(gridItem instanceof SceneGridItem || PanelRepeaterGridItem)) { + if (!(gridItem instanceof SceneGridItem || gridItem instanceof PanelRepeaterGridItem)) { console.error('Trying to duplicate a panel in a layout that is not SceneGridItem or PanelRepeaterGridItem'); return; } @@ -506,7 +533,52 @@ export class DashboardScene extends SceneObjectBase { const jsonData = gridItemToPanel(gridItem); store.set(LS_PANEL_COPY_KEY, JSON.stringify(jsonData)); - appEvents.emit(AppEvents.alertSuccess, ['Panel copied. Click **Add panel** icon to paste.']); + appEvents.emit(AppEvents.alertSuccess, ['Panel copied. Use **Paste panel** toolbar action to paste.']); + this.setState({ hasCopiedPanel: true }); + } + + public pastePanel() { + if (!(this.state.body instanceof SceneGridLayout)) { + throw new Error('Trying to add a panel in a layout that is not SceneGridLayout'); + } + + const jsonData = store.get(LS_PANEL_COPY_KEY); + const jsonObj = JSON.parse(jsonData); + const panelModel = new PanelModel(jsonObj); + + const gridItem = buildGridItemForPanel(panelModel); + const sceneGridLayout = this.state.body; + + if (!(gridItem instanceof SceneGridItem) && !(gridItem instanceof PanelRepeaterGridItem)) { + throw new Error('Cannot paste invalid grid item'); + } + + const panelId = dashboardSceneGraph.getNextPanelId(this); + + if (gridItem instanceof SceneGridItem && gridItem.state.body) { + gridItem.state.body.setState({ + key: getVizPanelKeyForPanelId(panelId), + }); + } else if (gridItem instanceof PanelRepeaterGridItem) { + gridItem.state.source.setState({ + key: getVizPanelKeyForPanelId(panelId), + }); + } + + gridItem.setState({ + height: NEW_PANEL_HEIGHT, + width: NEW_PANEL_WIDTH, + x: 0, + y: 0, + key: `grid-item-${panelId}`, + }); + + sceneGridLayout.setState({ + children: [gridItem, ...sceneGridLayout.state.children], + }); + + this.setState({ hasCopiedPanel: false }); + store.delete(LS_PANEL_COPY_KEY); } public showModal(modal: SceneObject) { @@ -540,6 +612,14 @@ export class DashboardScene extends SceneObjectBase { locationService.partial({ editview: 'settings' }); }; + public onCreateNewRow() { + const row = getDefaultRow(this); + + this.addRow(row); + + return getPanelIdForVizPanel(row); + } + public onCreateNewPanel(): number { const vizPanel = getDefaultVizPanel(this); diff --git a/public/app/features/dashboard-scene/scene/NavToolbarActions.test.tsx b/public/app/features/dashboard-scene/scene/NavToolbarActions.test.tsx index 6d95fcccca..0c192c1df9 100644 --- a/public/app/features/dashboard-scene/scene/NavToolbarActions.test.tsx +++ b/public/app/features/dashboard-scene/scene/NavToolbarActions.test.tsx @@ -16,6 +16,8 @@ describe('NavToolbarActions', () => { expect(screen.queryByText('Save dashboard')).not.toBeInTheDocument(); expect(screen.queryByLabelText('Add visualization')).not.toBeInTheDocument(); + expect(screen.queryByLabelText('Add row')).not.toBeInTheDocument(); + expect(screen.queryByLabelText('Paste panel')).not.toBeInTheDocument(); expect(await screen.findByText('Edit')).toBeInTheDocument(); expect(await screen.findByText('Share')).toBeInTheDocument(); }); @@ -28,6 +30,8 @@ describe('NavToolbarActions', () => { expect(await screen.findByText('Save dashboard')).toBeInTheDocument(); expect(await screen.findByText('Exit edit')).toBeInTheDocument(); expect(await screen.findByLabelText('Add visualization')).toBeInTheDocument(); + expect(await screen.findByLabelText('Add row')).toBeInTheDocument(); + expect(await screen.findByLabelText('Paste panel')).toBeInTheDocument(); expect(screen.queryByText('Edit')).not.toBeInTheDocument(); expect(screen.queryByText('Share')).not.toBeInTheDocument(); }); diff --git a/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx b/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx index 02164f9f1f..76215619b7 100644 --- a/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx +++ b/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx @@ -37,12 +37,22 @@ NavToolbarActions.displayName = 'NavToolbarActions'; * This part is split into a separate component to help test this */ export function ToolbarActions({ dashboard }: Props) { - const { isEditing, viewPanelScene, isDirty, uid, meta, editview, editPanel } = dashboard.useState(); + const { + isEditing, + viewPanelScene, + isDirty, + uid, + meta, + editview, + editPanel, + hasCopiedPanel: copiedPanel, + } = dashboard.useState(); const canSaveAs = contextSrv.hasEditPermissionInFolders; const toolbarActions: ToolbarAction[] = []; const buttonWithExtraMargin = useStyles2(getStyles); const isEditingPanel = Boolean(editPanel); const isViewingPanel = Boolean(viewPanelScene); + const hasCopiedPanel = Boolean(copiedPanel); toolbarActions.push({ group: 'icon-actions', @@ -61,6 +71,39 @@ export function ToolbarActions({ dashboard }: Props) { ), }); + toolbarActions.push({ + group: 'icon-actions', + condition: isEditing && !editview && !isViewingPanel && !isEditingPanel, + render: () => ( + { + dashboard.onCreateNewRow(); + DashboardInteractions.toolbarAddButtonClicked({ item: 'add_row' }); + }} + /> + ), + }); + + toolbarActions.push({ + group: 'icon-actions', + condition: isEditing && !editview && !isViewingPanel && !isEditingPanel, + render: () => ( + { + dashboard.pastePanel(); + DashboardInteractions.toolbarAddButtonClicked({ item: 'paste_panel' }); + }} + /> + ), + }); + toolbarActions.push({ group: 'icon-actions', condition: uid && !editview && Boolean(meta.canStar) && !isEditingPanel && !isEditing, diff --git a/public/app/features/dashboard-scene/utils/dashboardSceneGraph.test.ts b/public/app/features/dashboard-scene/utils/dashboardSceneGraph.test.ts index 5e106d15d9..a799efe2c3 100644 --- a/public/app/features/dashboard-scene/utils/dashboardSceneGraph.test.ts +++ b/public/app/features/dashboard-scene/utils/dashboardSceneGraph.test.ts @@ -12,9 +12,10 @@ import { AlertStatesDataLayer } from '../scene/AlertStatesDataLayer'; import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer'; import { DashboardControls } from '../scene/DashboardControls'; import { DashboardScene, DashboardSceneState } from '../scene/DashboardScene'; +import { LibraryVizPanel } from '../scene/LibraryVizPanel'; import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks'; -import { dashboardSceneGraph } from './dashboardSceneGraph'; +import { dashboardSceneGraph, getNextPanelId } from './dashboardSceneGraph'; import { findVizPanelByKey } from './utils'; describe('dashboardSceneGraph', () => { @@ -84,6 +85,120 @@ describe('dashboardSceneGraph', () => { expect(() => dashboardSceneGraph.getDataLayers(scene)).toThrow('SceneDataLayers not found'); }); }); + + describe('getNextPanelId', () => { + it('should get next panel id in a simple 3 panel layout', () => { + const scene = buildTestScene({ + body: new SceneGridLayout({ + children: [ + new SceneGridItem({ + body: new VizPanel({ + title: 'Panel A', + key: 'panel-1', + pluginId: 'table', + }), + }), + new SceneGridItem({ + body: new VizPanel({ + title: 'Panel B', + key: 'panel-2', + pluginId: 'table', + }), + }), + new SceneGridItem({ + body: new VizPanel({ + title: 'Panel C', + key: 'panel-3', + pluginId: 'table', + }), + }), + ], + }), + }); + + const id = getNextPanelId(scene); + + expect(id).toBe(4); + }); + + it('should take library panels into account', () => { + const scene = buildTestScene({ + body: new SceneGridLayout({ + children: [ + new SceneGridItem({ + key: 'griditem-1', + x: 0, + body: new VizPanel({ + title: 'Panel A', + key: 'panel-1', + pluginId: 'table', + }), + }), + new SceneGridItem({ + body: new LibraryVizPanel({ + uid: 'uid', + name: 'LibPanel', + title: 'Library Panel', + panelKey: 'panel-2', + }), + }), + new SceneGridItem({ + body: new VizPanel({ + title: 'Panel C', + key: 'panel-2-clone-1', + pluginId: 'table', + }), + }), + new SceneGridRow({ + key: 'key', + title: 'row', + children: [ + new SceneGridItem({ + body: new VizPanel({ + title: 'Panel E', + key: 'panel-2-clone-2', + pluginId: 'table', + }), + }), + new SceneGridItem({ + body: new LibraryVizPanel({ + uid: 'uid', + name: 'LibPanel', + title: 'Library Panel', + panelKey: 'panel-3', + }), + }), + ], + }), + ], + }), + }); + + const id = getNextPanelId(scene); + + expect(id).toBe(4); + }); + + it('should get next panel id in a layout with rows', () => { + const scene = buildTestScene(); + const id = getNextPanelId(scene); + + expect(id).toBe(3); + }); + + it('should return 1 if no panels are found', () => { + const scene = buildTestScene({ body: new SceneGridLayout({ children: [] }) }); + const id = getNextPanelId(scene); + + expect(id).toBe(1); + }); + + it('should throw an error if body is not SceneGridLayout', () => { + const scene = buildTestScene({ body: undefined }); + + expect(() => getNextPanelId(scene)).toThrow('Dashboard body is not a SceneGridLayout'); + }); + }); }); function buildTestScene(overrides?: Partial) { diff --git a/public/app/features/dashboard-scene/utils/dashboardSceneGraph.ts b/public/app/features/dashboard-scene/utils/dashboardSceneGraph.ts index 3047692da5..898885a730 100644 --- a/public/app/features/dashboard-scene/utils/dashboardSceneGraph.ts +++ b/public/app/features/dashboard-scene/utils/dashboardSceneGraph.ts @@ -1,8 +1,11 @@ -import { VizPanel, SceneGridItem, SceneGridRow, SceneDataLayers, sceneGraph } from '@grafana/scenes'; +import { VizPanel, SceneGridItem, SceneGridRow, SceneDataLayers, sceneGraph, SceneGridLayout } from '@grafana/scenes'; import { DashboardScene } from '../scene/DashboardScene'; +import { LibraryVizPanel } from '../scene/LibraryVizPanel'; import { VizPanelLinks } from '../scene/PanelLinks'; +import { getPanelIdForLibraryVizPanel, getPanelIdForVizPanel } from './utils'; + function getTimePicker(scene: DashboardScene) { return scene.state.controls?.state.timePicker; } @@ -55,10 +58,65 @@ function getDataLayers(scene: DashboardScene): SceneDataLayers { return data; } +export function getNextPanelId(dashboard: DashboardScene): number { + let max = 0; + const body = dashboard.state.body; + + if (!(body instanceof SceneGridLayout)) { + throw new Error('Dashboard body is not a SceneGridLayout'); + } + + for (const child of body.state.children) { + if (child instanceof SceneGridItem) { + const vizPanel = child.state.body; + + if (vizPanel) { + const panelId = + vizPanel instanceof LibraryVizPanel + ? getPanelIdForLibraryVizPanel(vizPanel) + : getPanelIdForVizPanel(vizPanel); + + if (panelId > max) { + max = panelId; + } + } + } + + if (child instanceof SceneGridRow) { + //rows follow the same key pattern --- e.g.: `panel-6` + const panelId = getPanelIdForVizPanel(child); + + if (panelId > max) { + max = panelId; + } + + for (const rowChild of child.state.children) { + if (rowChild instanceof SceneGridItem) { + const vizPanel = rowChild.state.body; + + if (vizPanel) { + const panelId = + vizPanel instanceof LibraryVizPanel + ? getPanelIdForLibraryVizPanel(vizPanel) + : getPanelIdForVizPanel(vizPanel); + + if (panelId > max) { + max = panelId; + } + } + } + } + } + } + + return max + 1; +} + export const dashboardSceneGraph = { getTimePicker, getRefreshPicker, getPanelLinks, getVizPanels, getDataLayers, + getNextPanelId, }; diff --git a/public/app/features/dashboard-scene/utils/utils.ts b/public/app/features/dashboard-scene/utils/utils.ts index 4ddf171e05..287668e126 100644 --- a/public/app/features/dashboard-scene/utils/utils.ts +++ b/public/app/features/dashboard-scene/utils/utils.ts @@ -4,8 +4,6 @@ import { MultiValueVariable, SceneDataTransformer, sceneGraph, - SceneGridItem, - SceneGridLayout, SceneGridRow, SceneObject, SceneQueryRunner, @@ -19,6 +17,8 @@ import { LibraryVizPanel } from '../scene/LibraryVizPanel'; import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks'; import { panelMenuBehavior } from '../scene/PanelMenuBehavior'; +import { dashboardSceneGraph } from './dashboardSceneGraph'; + export const NEW_PANEL_HEIGHT = 8; export const NEW_PANEL_WIDTH = 12; @@ -30,8 +30,12 @@ export function getPanelIdForVizPanel(panel: SceneObject): number { return parseInt(panel.state.key!.replace('panel-', ''), 10); } +export function getPanelIdForLibraryVizPanel(panel: LibraryVizPanel): number { + return parseInt(panel.state.panelKey!.replace('panel-', ''), 10); +} + /** - * This will also try lookup based on panelId + * This will also try lookup based on panelId */ export function findVizPanelByKey(scene: SceneObject, key: string | undefined): VizPanel | null { if (!key) { @@ -201,47 +205,8 @@ export function isPanelClone(key: string) { return key.includes('clone'); } -export function getNextPanelId(dashboard: DashboardScene) { - let max = 0; - const body = dashboard.state.body; - - if (body instanceof SceneGridLayout) { - for (const child of body.state.children) { - if (child instanceof SceneGridItem) { - const vizPanel = child.state.body; - - if (vizPanel instanceof VizPanel) { - const panelId = getPanelIdForVizPanel(vizPanel); - - if (panelId > max) { - max = panelId; - } - } - } - - if (child instanceof SceneGridRow) { - for (const rowChild of child.state.children) { - if (rowChild instanceof SceneGridItem) { - const vizPanel = rowChild.state.body; - - if (vizPanel instanceof VizPanel) { - const panelId = getPanelIdForVizPanel(vizPanel); - - if (panelId > max) { - max = panelId; - } - } - } - } - } - } - } - - return max + 1; -} - export function getDefaultVizPanel(dashboard: DashboardScene): VizPanel { - const panelId = getNextPanelId(dashboard); + const panelId = dashboardSceneGraph.getNextPanelId(dashboard); return new VizPanel({ title: 'Panel Title', @@ -261,6 +226,16 @@ export function getDefaultVizPanel(dashboard: DashboardScene): VizPanel { }); } +export function getDefaultRow(dashboard: DashboardScene): SceneGridRow { + const id = dashboardSceneGraph.getNextPanelId(dashboard); + + return new SceneGridRow({ + key: getVizPanelKeyForPanelId(id), + title: 'Row title', + y: 0, + }); +} + export function isLibraryPanelChild(vizPanel: VizPanel) { return vizPanel.parent instanceof LibraryVizPanel; } From d83319365f0c4fad173e8ed55eaa37041c5d9c26 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Wed, 28 Feb 2024 09:45:11 +0000 Subject: [PATCH 0255/1406] DatePickerWithInput: use `floating-ui` so calendar can overflow scroll containers (#83521) * move DatePickerWithInput to use floating-ui * remove position: absolute from DatePicker, remove unnecessary css from CreateTokenModal --- .betterer.results | 6 -- .../DateTimePickers/DatePicker/DatePicker.tsx | 1 - .../DatePickerWithInput.tsx | 63 ++++++++++++++----- .../components/CreateTokenModal.tsx | 29 +++------ 4 files changed, 57 insertions(+), 42 deletions(-) diff --git a/.betterer.results b/.betterer.results index 9a2bd1a8c8..83032798fe 100644 --- a/.betterer.results +++ b/.betterer.results @@ -4107,12 +4107,6 @@ exports[`better eslint`] = { "public/app/features/search/utils.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], - "public/app/features/serviceaccounts/components/CreateTokenModal.tsx:5381": [ - [0, 0, 0, "Styles should be written using objects.", "0"], - [0, 0, 0, "Styles should be written using objects.", "1"], - [0, 0, 0, "Styles should be written using objects.", "2"], - [0, 0, 0, "Styles should be written using objects.", "3"] - ], "public/app/features/serviceaccounts/components/ServiceAccountProfile.tsx:5381": [ [0, 0, 0, "Styles should be written using objects.", "0"] ], diff --git a/packages/grafana-ui/src/components/DateTimePickers/DatePicker/DatePicker.tsx b/packages/grafana-ui/src/components/DateTimePickers/DatePicker/DatePicker.tsx index c2a7d8f645..358d985a16 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/DatePicker/DatePicker.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/DatePicker/DatePicker.tsx @@ -67,7 +67,6 @@ export const getStyles = (theme: GrafanaTheme2) => { return { modal: css({ zIndex: theme.zIndex.modal, - position: 'absolute', boxShadow: theme.shadows.z3, backgroundColor: theme.colors.background.primary, border: `1px solid ${theme.colors.border.weak}`, diff --git a/packages/grafana-ui/src/components/DateTimePickers/DatePickerWithInput/DatePickerWithInput.tsx b/packages/grafana-ui/src/components/DateTimePickers/DatePickerWithInput/DatePickerWithInput.tsx index 581d6b72e9..69933e8d64 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/DatePickerWithInput/DatePickerWithInput.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/DatePickerWithInput/DatePickerWithInput.tsx @@ -1,5 +1,6 @@ import { css } from '@emotion/css'; -import React, { ChangeEvent } from 'react'; +import { autoUpdate, flip, shift, useClick, useDismiss, useFloating, useInteractions } from '@floating-ui/react'; +import React, { ChangeEvent, useState } from 'react'; import { dateTime } from '@grafana/data'; @@ -35,17 +36,41 @@ export const DatePickerWithInput = ({ placeholder = 'Date', ...rest }: DatePickerWithInputProps) => { - const [open, setOpen] = React.useState(false); + const [open, setOpen] = useState(false); const styles = useStyles2(getStyles); + // the order of middleware is important! + // see https://floating-ui.com/docs/arrow#order + const middleware = [ + flip({ + // see https://floating-ui.com/docs/flip#combining-with-shift + crossAxis: false, + boundary: document.body, + }), + shift(), + ]; + + const { context, refs, floatingStyles } = useFloating({ + open, + placement: 'bottom-start', + onOpenChange: setOpen, + middleware, + whileElementsMounted: autoUpdate, + strategy: 'fixed', + }); + + const click = useClick(context); + const dismiss = useDismiss(context); + const { getReferenceProps, getFloatingProps } = useInteractions([dismiss, click]); + return (
setOpen(true)} onChange={(ev: ChangeEvent) => { // Allow resetting the date if (ev.target.value === '') { @@ -54,20 +79,23 @@ export const DatePickerWithInput = ({ }} className={styles.input} {...rest} + {...getReferenceProps()} /> - { - onChange(ev); - if (closeOnSelect) { - setOpen(false); - } - }} - onClose={() => setOpen(false)} - /> +
+ { + onChange(ev); + if (closeOnSelect) { + setOpen(false); + } + }} + onClose={() => setOpen(false)} + /> +
); }; @@ -84,5 +112,8 @@ const getStyles = () => { WebkitAppearance: 'none', }, }), + popover: css({ + zIndex: 1, + }), }; }; diff --git a/public/app/features/serviceaccounts/components/CreateTokenModal.tsx b/public/app/features/serviceaccounts/components/CreateTokenModal.tsx index a86e578c27..92484a0cdb 100644 --- a/public/app/features/serviceaccounts/components/CreateTokenModal.tsx +++ b/public/app/features/serviceaccounts/components/CreateTokenModal.tsx @@ -84,13 +84,7 @@ export const CreateTokenModal = ({ isOpen, token, serviceAccountLogin, onCreateT const modalTitle = !token ? 'Add service account token' : 'Service account token created'; return ( - + {!token ? (
{ const getStyles = (theme: GrafanaTheme2) => { return { - modal: css` - width: 550px; - `, - modalContent: css` - overflow: visible; - `, - modalTokenRow: css` - display: flex; - `, - modalCopyToClipboardButton: css` - margin-left: ${theme.spacing(0.5)}; - `, + modal: css({ + width: '550px', + }), + modalTokenRow: css({ + display: 'flex', + }), + modalCopyToClipboardButton: css({ + marginLeft: theme.spacing(0.5), + }), }; }; From 213e39956338d617e5afdd4c972be855e3c44ae9 Mon Sep 17 00:00:00 2001 From: Tobias Skarhed <1438972+tskarhed@users.noreply.github.com> Date: Wed, 28 Feb 2024 10:50:03 +0100 Subject: [PATCH 0256/1406] Docs: Change link in Trusted Types (#83391) --- .../configure-security/configure-security-hardening/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/setup-grafana/configure-security/configure-security-hardening/index.md b/docs/sources/setup-grafana/configure-security/configure-security-hardening/index.md index fd68b7cc40..bc0dae58b4 100644 --- a/docs/sources/setup-grafana/configure-security/configure-security-hardening/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-security-hardening/index.md @@ -101,7 +101,7 @@ To enable trusted types in report mode, where inputs that have not been sanitize - Enable `content_security_policy_report_only` in the configuration. - Add `require-trusted-types-for 'script'` to the `content_security_policy_report_only_template` in the configuration. -As this is a feature currently in development, things may break. If they do, or if you have any other feedback, feel free to [leave a comment](https://github.com/grafana/grafana/discussions/66823). +As this is a feature currently in development, things may break. If they do, or if you have any other feedback, feel free to [open an issue](https://github.com/grafana/grafana/issues/new/choose). ## Additional security hardening From acf97e43b664cf966f4c85d53580e4a5573e59cc Mon Sep 17 00:00:00 2001 From: Jack Baldry Date: Wed, 28 Feb 2024 09:54:19 +0000 Subject: [PATCH 0257/1406] Review "Team LBAC" page (#83406) --- .../{index.md => _index.md} | 0 .../data-source-management/teamlbac/_index.md | 84 ++++++++----------- 2 files changed, 34 insertions(+), 50 deletions(-) rename docs/sources/administration/data-source-management/{index.md => _index.md} (100%) diff --git a/docs/sources/administration/data-source-management/index.md b/docs/sources/administration/data-source-management/_index.md similarity index 100% rename from docs/sources/administration/data-source-management/index.md rename to docs/sources/administration/data-source-management/_index.md diff --git a/docs/sources/administration/data-source-management/teamlbac/_index.md b/docs/sources/administration/data-source-management/teamlbac/_index.md index 2ad99eda00..1d87bdbb06 100644 --- a/docs/sources/administration/data-source-management/teamlbac/_index.md +++ b/docs/sources/administration/data-source-management/teamlbac/_index.md @@ -14,71 +14,55 @@ weight: 100 # Team LBAC -{{% admonition type="note" %}} -Creating Team LBAC rules is available for preview for logs with Loki in Grafana Cloud. Report any unexpected behavior to the Grafana Support team. -{{% /admonition %}} +Team Label Based Access Control (LBAC) simplifies and streamlines data source access management based on team memberships. -**Current Limitation:** +{{< admonition type="note" >}} +Creating Team LBAC rules is available for preview for logs with Loki in Grafana Cloud. +Report any unexpected behavior to the Grafana Support team. +{{< /admonition >}} -- Any user with `query` permissions for a Loki data source can query all logs if there are no Team LBAC rules configured for any of the users team. -- An admin that is part of a team, would have it's Team LBAC rules applied to the request. -- Team LBAC rules will not be applied if the linked Cloud Access Policy has label selectors. +You can configure user access based upon team memberships using LogQL. +Team LBAC controls access to logs depending on the rules set for each team. -Grafana's new **Team LBAC** (Label Based Access Control) feature for Loki is a significant enhancement that simplifies and streamlines data source access management based on team memberships. +This feature addresses two common challenges faced by Grafana users: -**Team LBAC** in the context of Loki, is a way to control access to logs based on labels present depending on the rules set for each team. Users wanting fine grained access to their logs in Loki, can now configure their users access based on their team memberships via **LogQL**. +1. Having a high number of Grafana Cloud data sources. + Team LBAC lets Grafana administrators reduce the total number of data sources per instance from hundreds, to one. +1. Using the same dashboard across multiple teams. + Team LBAC lets Grafana Teams use the same dashboard with different access control rules. -This feature addresses two common challenge faced by Grafana users: +To set up Team LBAC for a Loki data source, refer to [Configure Team LBAC](https://grafana.com/docs/grafana//administration/data-source-management/teamlbac/configure-teamlbac-for-loki/). -1. High volume of Grafana Cloud datasource. Team LBAC lets Grafana Admins reduce the total volume of data sources per instance from hundreds, to one. -1. Hard for teams to share dashboard. Team LBAC lets Grafana Teams share the same dashboard despite different access control rules. +## Limitations -For setting up Team LBAC for a Loki data source, refer to [Configure Team LBAC]({{< relref "./configure-teamlbac-for-loki/" >}}). +- If there are no Team LBAC rules for a user's team, that user can query all logs. +- If an administrator is part of a team with Team LBAC rules, those rules are applied to the administrator requests. +- Cloud Access Policies (CAP) LBAC rules override Team LBAC rules. + Cloud Access Policies are the access controls from Grafana Cloud. + If there are any CAP LBAC rules configured for the same data source, then only the CAP LBAC rules are applied. -#### Datasource Permissions + You must remove any label selectors from your Cloud Access Policies to use Team LBAC. + For more information about CAP label selectors, refer to [Use label-based access control (LBAC) with access policies](https://grafana.com/docs/grafana-cloud/account-management/authentication-and-permissions/access-policies/label-access-policies/). -Datasource permissions allow the users access to query the datasource. The permissions are set at the datasource level and are inherited by all the teams and users that are part of the datasource. +## Data source permissions -#### Recommended setup +Data source permissions allow the users access to query the data source. +Administrators set the permissions at the data source level. +All the teams and users that are part of the data source inherit those permissions. -We recommend to create a loki datasource dedicated for Team LBAC rules with only teams having `query` permission. This will allow you to have a clear separation of datasources for Team LBAC and the datasources that are not using Team LBAC. Another loki datasource would be setup for full access to the logs. +## Recommended setup -Ex: - -1. Datasource `loki-full-access`, same setup for the loki tenant, the users querying this datasource would not have team lbac rules and have `query` permissions. -2. Datasource `loki-lbac`, same setup, the users querying the data source would have to be part of a team and a LBAC rule. +It's recommended that you create a single Loki data source for using Team LBAC rules so you have a clear separation of data sources using Team LBAC and those that aren't. +All teams should have with only teams having `query` permission. +You should create another Loki data source configured without Team LBAC for full access to the logs. ## Team LBAC rules -Team LBAC rules are added to the http request to Loki data source. Setting up Team LBAC rules for any team will apply those rules to the teams. -Users who want teams with a specific set of label selectors can add rules for each team. - -Configuring multiple rules for a team, each rule is evaluated separately. If a team has `X` number of rules configured for it, all rules will be applied to the request and the result will be the an "OR" operation of the `X` number of rules. - -Only users with data source Admin permissions can edit LBAC rules at the data source permissions tab. Changing LBAC rules requires the same access level as editing data source permissions (admin permission for data source). - -For setting up Team LBAC Rules for the data source, refer to [Create Team LBAC rules]({{< relref "./create-teamlbac-rules/" >}}). - -### FAQ - -> #### "If I want a user to have full access to the logs, but they are part of a team with LBAC rules?" -> -> The user should use another loki datasource that is specifically used to have full access to the logs. See best practices. - -**Note:** A user who is part of a team within Grafana with a rule will only be able to query logs with that rule. - -> #### "If a team does not have a rule, what happens?" - -If a team does not have a rule; any users that are part of that team having query permissions for loki will have access to **all** logs. - -> #### "Can I use CAPs (cloud access policies) together with TeamLBAC rules?" - -No, CAP (cloud access policies) always have precedence. If there are any CAP LBAC configured for the same datasource and there are TeamLBAC rules configured, then only the CAP LBAC will be applied. - -Cloud access policies are the access controls from Grafana Cloud, the CAP configured for loki should only to be used to gain read access to the logs. +Grafana adds Team LBAC rules to the HTTP request via the Loki data source. -> #### "If administrator forget to add rule for a team, what happens?" +If you configure multiple rules for a team, each rule is evaluated separately. +Query results include lines that match any of the rules. -The teams that does not have a rule applied to it, would be able to query all logs if `query` permissions are setup for their role within Grafana. +Only users with data source `Admin` permissions can edit Team LBAC rules in the **Data source permissions** tab because changing LBAC rules requires the same access level as editing data source permissions. -**Note:** A user who is part of a team within Grafana without a rule will be able to query all logs if the user has a role with `query` permissions. +To set up Team LBAC for a Loki data source, refer to [Configure Team LBAC](https://grafana.com/docs/grafana//administration/data-source-management/teamlbac/configure-teamlbac-for-loki/). From 3b7e7483c81c597e991ba7812d6ae67a64e9a67a Mon Sep 17 00:00:00 2001 From: Misi Date: Wed, 28 Feb 2024 13:45:59 +0100 Subject: [PATCH 0258/1406] Auth: Align loading the legacy auth.grafananet section to the current behaviour in OAuthStrategy (#83479) * Align oauth_strategy to the current behaviour * lint * Address feedback --- pkg/login/social/socialimpl/service_test.go | 95 +++++++++++++++++++ .../ssosettings/strategies/oauth_strategy.go | 9 +- .../strategies/oauth_strategy_test.go | 93 +++++++++++++++++- 3 files changed, 195 insertions(+), 2 deletions(-) diff --git a/pkg/login/social/socialimpl/service_test.go b/pkg/login/social/socialimpl/service_test.go index a0f80172de..04fa8b403f 100644 --- a/pkg/login/social/socialimpl/service_test.go +++ b/pkg/login/social/socialimpl/service_test.go @@ -91,6 +91,101 @@ func TestSocialService_ProvideService(t *testing.T) { } } +func TestSocialService_ProvideService_GrafanaComGrafanaNet(t *testing.T) { + testCases := []struct { + name string + rawIniContent string + expectedGrafanaComOAuthInfo *social.OAuthInfo + }{ + { + name: "should setup the connector using auth.grafana_com section if it is enabled", + rawIniContent: ` + [auth.grafana_com] + enabled = true + client_id = grafanaComClientId + + [auth.grafananet] + enabled = false + client_id = grafanaNetClientId`, + expectedGrafanaComOAuthInfo: &social.OAuthInfo{ + AuthStyle: "inheader", + AuthUrl: "/oauth2/authorize", + TokenUrl: "/api/oauth2/token", + Enabled: true, + ClientId: "grafanaComClientId", + }, + }, + { + name: "should setup the connector using auth.grafananet section if it is enabled", + rawIniContent: ` + [auth.grafana_com] + enabled = false + client_id = grafanaComClientId + + [auth.grafananet] + enabled = true + client_id = grafanaNetClientId`, + expectedGrafanaComOAuthInfo: &social.OAuthInfo{ + AuthStyle: "inheader", + AuthUrl: "/oauth2/authorize", + TokenUrl: "/api/oauth2/token", + Enabled: true, + ClientId: "grafanaNetClientId", + }, + }, + { + name: "should setup the connector using auth.grafana_com section if both are enabled", + rawIniContent: ` + [auth.grafana_com] + enabled = true + client_id = grafanaComClientId + + [auth.grafananet] + enabled = true + client_id = grafanaNetClientId`, + expectedGrafanaComOAuthInfo: &social.OAuthInfo{ + AuthStyle: "inheader", + AuthUrl: "/oauth2/authorize", + TokenUrl: "/api/oauth2/token", + Enabled: true, + ClientId: "grafanaComClientId", + }, + }, + { + name: "should not setup the connector when both are disabled", + rawIniContent: ` + [auth.grafana_com] + enabled = false + client_id = grafanaComClientId + + [auth.grafananet] + enabled = false + client_id = grafanaNetClientId`, + expectedGrafanaComOAuthInfo: nil, + }, + } + + cfg := setting.NewCfg() + secrets := secretsfake.NewMockService(t) + accessControl := acimpl.ProvideAccessControl(cfg) + sqlStore := db.InitTestDB(t) + + ssoSettingsSvc := ssosettingsimpl.ProvideService(cfg, sqlStore, accessControl, routing.NewRouteRegister(), featuremgmt.WithFeatures(), secrets, &usagestats.UsageStatsMock{}, nil) + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + iniFile, err := ini.Load([]byte(tc.rawIniContent)) + require.NoError(t, err) + + cfg := setting.NewCfg() + cfg.Raw = iniFile + + socialService := ProvideService(cfg, featuremgmt.WithFeatures(), &usagestats.UsageStatsMock{}, supportbundlestest.NewFakeBundleService(), remotecache.NewFakeStore(t), ssoSettingsSvc) + require.EqualValues(t, tc.expectedGrafanaComOAuthInfo, socialService.GetOAuthInfoProvider("grafana_com")) + }) + } +} + func TestMapping_IniSectionOAuthInfo(t *testing.T) { iniContent := ` [test] diff --git a/pkg/services/ssosettings/strategies/oauth_strategy.go b/pkg/services/ssosettings/strategies/oauth_strategy.go index 215d0a5cf0..4dbbfddfbe 100644 --- a/pkg/services/ssosettings/strategies/oauth_strategy.go +++ b/pkg/services/ssosettings/strategies/oauth_strategy.go @@ -51,13 +51,20 @@ func (s *OAuthStrategy) loadAllSettings() { allProviders := append(ssosettings.AllOAuthProviders, social.GrafanaNetProviderName) for _, provider := range allProviders { settings := s.loadSettingsForProvider(provider) - if provider == social.GrafanaNetProviderName { + // This is required to support the legacy settings for the provider (auth.grafananet section) + // It will use the settings (and overwrite the current grafana_com settings) from auth.grafananet if + // the auth.grafananet section is enabled and the auth.grafana_com section is disabled. + if provider == social.GrafanaNetProviderName && s.shouldUseGrafanaNetSettings() && settings["enabled"] == true { provider = social.GrafanaComProviderName } s.settingsByProvider[provider] = settings } } +func (s *OAuthStrategy) shouldUseGrafanaNetSettings() bool { + return s.settingsByProvider[social.GrafanaComProviderName]["enabled"] == false +} + func (s *OAuthStrategy) loadSettingsForProvider(provider string) map[string]any { section := s.cfg.Raw.Section("auth." + provider) diff --git a/pkg/services/ssosettings/strategies/oauth_strategy_test.go b/pkg/services/ssosettings/strategies/oauth_strategy_test.go index c5e4cc28be..b41e14e1b3 100644 --- a/pkg/services/ssosettings/strategies/oauth_strategy_test.go +++ b/pkg/services/ssosettings/strategies/oauth_strategy_test.go @@ -127,6 +127,7 @@ func TestGetProviderConfig_ExtraFields(t *testing.T) { allowed_organizations = org1, org2 [auth.grafana_com] + enabled = true allowed_organizations = org1, org2 ` @@ -166,10 +167,100 @@ func TestGetProviderConfig_ExtraFields(t *testing.T) { }) t.Run("grafana_com", func(t *testing.T) { - t.Skip("Skipping to revert an issue.") result, err := strategy.GetProviderConfig(context.Background(), "grafana_com") require.NoError(t, err) require.Equal(t, "org1, org2", result["allowed_organizations"]) }) } + +// TestGetProviderConfig_GrafanaComGrafanaNet tests that the connector is setup using the correct section and it supports +// the legacy settings for the provider (auth.grafananet section). The test cases are based on the current behavior of the +// SocialService's ProvideService method (TestSocialService_ProvideService_GrafanaComGrafanaNet). +func TestGetProviderConfig_GrafanaComGrafanaNet(t *testing.T) { + testCases := []struct { + name string + rawIniContent string + expectedGrafanaComSettings map[string]any + }{ + { + name: "should setup the connector using auth.grafana_com section if it is enabled", + rawIniContent: ` + [auth.grafana_com] + enabled = true + client_id = grafanaComClientId + + [auth.grafananet] + enabled = false + client_id = grafanaNetClientId`, + expectedGrafanaComSettings: map[string]any{ + "enabled": true, + "client_id": "grafanaComClientId", + }, + }, + { + name: "should setup the connector using auth.grafananet section if it is enabled", + rawIniContent: ` + [auth.grafana_com] + enabled = false + client_id = grafanaComClientId + + [auth.grafananet] + enabled = true + client_id = grafanaNetClientId`, + expectedGrafanaComSettings: map[string]any{ + "enabled": true, + "client_id": "grafanaNetClientId", + }, + }, + { + name: "should setup the connector using auth.grafana_com section if both are enabled", + rawIniContent: ` + [auth.grafana_com] + enabled = true + client_id = grafanaComClientId + + [auth.grafananet] + enabled = true + client_id = grafanaNetClientId`, + expectedGrafanaComSettings: map[string]any{ + "enabled": true, + "client_id": "grafanaComClientId", + }, + }, + { + name: "should not setup the connector when both are disabled", + rawIniContent: ` + [auth.grafana_com] + enabled = false + client_id = grafanaComClientId + + [auth.grafananet] + enabled = false + client_id = grafanaNetClientId`, + expectedGrafanaComSettings: map[string]any{ + "enabled": false, + "client_id": "grafanaComClientId", + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + iniFile, err := ini.Load([]byte(tc.rawIniContent)) + require.NoError(t, err) + + cfg := setting.NewCfg() + cfg.Raw = iniFile + + strategy := NewOAuthStrategy(cfg) + + actualConfig, err := strategy.GetProviderConfig(context.Background(), "grafana_com") + require.NoError(t, err) + + for key, value := range tc.expectedGrafanaComSettings { + require.Equal(t, value, actualConfig[key], "Difference in key: %s. Expected: %v, got: %v", key, value, actualConfig[key]) + } + }) + } +} From 411c89012febe13323e4b8aafc8d692f4460e680 Mon Sep 17 00:00:00 2001 From: Sven Grossmann Date: Wed, 28 Feb 2024 14:32:01 +0100 Subject: [PATCH 0259/1406] Elasticsearch: Fix adhoc filters not applied in frontend mode (#83592) --- .../plugins/datasource/elasticsearch/LegacyQueryRunner.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/elasticsearch/LegacyQueryRunner.ts b/public/app/plugins/datasource/elasticsearch/LegacyQueryRunner.ts index c70a6ee60b..efb9d12591 100644 --- a/public/app/plugins/datasource/elasticsearch/LegacyQueryRunner.ts +++ b/public/app/plugins/datasource/elasticsearch/LegacyQueryRunner.ts @@ -151,7 +151,11 @@ export class LegacyQueryRunner { query(request: DataQueryRequest): Observable { let payload = ''; - const targets = this.datasource.interpolateVariablesInQueries(cloneDeep(request.targets), request.scopedVars); + const targets = this.datasource.interpolateVariablesInQueries( + cloneDeep(request.targets), + request.scopedVars, + request.filters + ); const sentTargets: ElasticsearchQuery[] = []; let targetsContainsLogsQuery = targets.some((target) => hasMetricOfType(target, 'logs')); From 90e7791086b42902fcbc80a5dd1d1f74b8b0feb4 Mon Sep 17 00:00:00 2001 From: Brendan O'Handley Date: Wed, 28 Feb 2024 08:03:36 -0600 Subject: [PATCH 0260/1406] Prometheus: Reduce flakiness in prometheus e2e tests (#83437) * reduce flakiness in prometheus e2e tests * prettier fix for azure docs * Update e2e/various-suite/prometheus-config.spec.ts Co-authored-by: Nick Richmond <5732000+NWRichmond@users.noreply.github.com> --------- Co-authored-by: Nick Richmond <5732000+NWRichmond@users.noreply.github.com> --- e2e/various-suite/prometheus-config.spec.ts | 95 ++++++++++----------- 1 file changed, 45 insertions(+), 50 deletions(-) diff --git a/e2e/various-suite/prometheus-config.spec.ts b/e2e/various-suite/prometheus-config.spec.ts index 09b7dfd1a9..9903b23933 100644 --- a/e2e/various-suite/prometheus-config.spec.ts +++ b/e2e/various-suite/prometheus-config.spec.ts @@ -13,33 +13,59 @@ describe('Prometheus config', () => { e2e.pages.AddDataSource.dataSourcePluginsV2(DATASOURCE_ID) .scrollIntoView() .should('be.visible') // prevents flakiness - .click(); - }); - - it('should have a connection settings component', () => { + .click({ force: true }); + }); + + it(`should have the following components: + connection settings + managed alerts + scrape interval + query timeout + default editor + disable metric lookup + prometheus type + cache level + incremental querying + disable recording rules + custom query parameters + http method + `, () => { + // connection settings e2e.components.DataSource.Prometheus.configPage.connectionSettings().should('be.visible'); - }); - - it('should have a managed alerts component', () => { + // managed alerts cy.get(`#${selectors.components.DataSource.Prometheus.configPage.manageAlerts}`).scrollIntoView().should('exist'); - }); - - it('should have a scrape interval component', () => { + // scrape interval e2e.components.DataSource.Prometheus.configPage.scrapeInterval().scrollIntoView().should('exist'); - }); - - it('should have a query timeout component', () => { + // query timeout e2e.components.DataSource.Prometheus.configPage.queryTimeout().scrollIntoView().should('exist'); - }); - - it('should have a default editor component', () => { + // default editor e2e.components.DataSource.Prometheus.configPage.defaultEditor().scrollIntoView().should('exist'); + // disable metric lookup + cy.get(`#${selectors.components.DataSource.Prometheus.configPage.disableMetricLookup}`) + .scrollIntoView() + .should('exist'); + // prometheus type + e2e.components.DataSource.Prometheus.configPage.prometheusType().scrollIntoView().should('exist'); + // cache level + e2e.components.DataSource.Prometheus.configPage.cacheLevel().scrollIntoView().should('exist'); + // incremental querying + cy.get(`#${selectors.components.DataSource.Prometheus.configPage.incrementalQuerying}`) + .scrollIntoView() + .should('exist'); + // disable recording rules + cy.get(`#${selectors.components.DataSource.Prometheus.configPage.disableRecordingRules}`) + .scrollIntoView() + .should('exist'); + // custom query parameters + e2e.components.DataSource.Prometheus.configPage.customQueryParameters().scrollIntoView().should('exist'); + // http method + e2e.components.DataSource.Prometheus.configPage.httpMethod().scrollIntoView().should('exist'); }); it('should save the default editor when navigating to explore', () => { e2e.components.DataSource.Prometheus.configPage.defaultEditor().scrollIntoView().should('exist').click(); - selectOption('Code'); + selectOption('Builder'); e2e.components.DataSource.Prometheus.configPage.connectionSettings().type('http://prom-url:9090'); @@ -50,21 +76,10 @@ describe('Prometheus config', () => { e2e.pages.Explore.visit(); e2e.components.DataSourcePicker.container().should('be.visible').click(); - cy.contains(DATASOURCE_TYPED_NAME).scrollIntoView().should('be.visible').click(); - const monacoLoadingText = 'Loading...'; - e2e.components.QueryField.container().should('be.visible').should('have.text', monacoLoadingText); - e2e.components.QueryField.container().should('be.visible').should('not.have.text', monacoLoadingText); - }); - - it('should have a disable metric lookup component', () => { - cy.get(`#${selectors.components.DataSource.Prometheus.configPage.disableMetricLookup}`) - .scrollIntoView() - .should('exist'); - }); + e2e.components.DataSourcePicker.container().type(`${DATASOURCE_TYPED_NAME}{enter}`); - it('should have a prometheus type component', () => { - e2e.components.DataSource.Prometheus.configPage.prometheusType().scrollIntoView().should('exist'); + e2e.components.DataSource.Prometheus.queryEditor.builder.metricSelect().should('exist'); }); it('should allow a user to add the version when the Prom type is selected', () => { @@ -79,12 +94,6 @@ describe('Prometheus config', () => { e2e.components.DataSource.Prometheus.configPage.cacheLevel().scrollIntoView().should('exist'); }); - it('should have an incremental querying component', () => { - cy.get(`#${selectors.components.DataSource.Prometheus.configPage.incrementalQuerying}`) - .scrollIntoView() - .should('exist'); - }); - it('should allow a user to select a query overlap window when incremental querying is selected', () => { cy.get(`#${selectors.components.DataSource.Prometheus.configPage.incrementalQuerying}`) .scrollIntoView() @@ -94,20 +103,6 @@ describe('Prometheus config', () => { e2e.components.DataSource.Prometheus.configPage.queryOverlapWindow().scrollIntoView().should('exist'); }); - it('should have a disable recording rules component', () => { - cy.get(`#${selectors.components.DataSource.Prometheus.configPage.disableRecordingRules}`) - .scrollIntoView() - .should('exist'); - }); - - it('should have a custom query parameters component', () => { - e2e.components.DataSource.Prometheus.configPage.customQueryParameters().scrollIntoView().should('exist'); - }); - - it('should have an http method component', () => { - e2e.components.DataSource.Prometheus.configPage.httpMethod().scrollIntoView().should('exist'); - }); - // exemplars tested in exemplar.spec }); From ed3c36bb46793afdb29300b955a066cdf705d52f Mon Sep 17 00:00:00 2001 From: Sonia Aguilar <33540275+soniaAguilarPeiron@users.noreply.github.com> Date: Wed, 28 Feb 2024 15:21:00 +0100 Subject: [PATCH 0261/1406] =?UTF-8?q?Alerting:=20Use=20time=5Fintervals=20?= =?UTF-8?q?instead=20of=20the=20deprecated=20mute=5Ftime=5Fintervals=20in?= =?UTF-8?q?=20a=E2=80=A6=20(#83147)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Use time_intervals instead of the deprecated mute_time_intervals in alert manager config * don't send mute_time_intervals in the payload * Add and update tests * Fix usecase when having both fields in response from getting alert manager config * Use mute_timings for grafana data source and both for cloud data source when deleting mute timing * Use mute_timings for grafana data source and both for cloud data source when saving a new or existing alert rule * Address first code review * Address more review comments --- .../alerting/unified/MuteTimings.test.tsx | 159 +++++++++++++++++- .../features/alerting/unified/MuteTimings.tsx | 15 +- .../alerting/unified/NotificationPolicies.tsx | 4 +- .../mute-timings/MuteTimingForm.tsx | 93 ++++++++-- .../mute-timings/MuteTimingsTable.tsx | 8 +- .../unified/components/mute-timings/util.tsx | 10 +- .../unified/hooks/useMuteTimingOptions.ts | 4 +- .../alerting/unified/state/actions.ts | 22 ++- .../plugins/datasource/alertmanager/types.ts | 1 + 9 files changed, 280 insertions(+), 36 deletions(-) diff --git a/public/app/features/alerting/unified/MuteTimings.test.tsx b/public/app/features/alerting/unified/MuteTimings.test.tsx index e87db13b33..b54591c258 100644 --- a/public/app/features/alerting/unified/MuteTimings.test.tsx +++ b/public/app/features/alerting/unified/MuteTimings.test.tsx @@ -1,4 +1,4 @@ -import { render, waitFor, fireEvent, within } from '@testing-library/react'; +import { fireEvent, render, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; import { TestProvider } from 'test/helpers/TestProvider'; @@ -10,7 +10,7 @@ import { AccessControlAction } from 'app/types'; import MuteTimings from './MuteTimings'; import { fetchAlertManagerConfig, updateAlertManagerConfig } from './api/alertmanager'; -import { grantUserPermissions, mockDataSource, MockDataSourceSrv } from './mocks'; +import { MockDataSourceSrv, grantUserPermissions, mockDataSource } from './mocks'; import { DataSourceType } from './utils/datasource'; jest.mock('./api/alertmanager'); @@ -71,6 +71,21 @@ const muteTimeInterval: MuteTimeInterval = { }, ], }; +const muteTimeInterval2: MuteTimeInterval = { + name: 'default-mute2', + time_intervals: [ + { + times: [ + { + start_time: '12:00', + end_time: '24:00', + }, + ], + days_of_month: ['15', '-1'], + months: ['august:december', 'march'], + }, + ], +}; const defaultConfig: AlertManagerCortexConfig = { alertmanager_config: { @@ -90,6 +105,44 @@ const defaultConfig: AlertManagerCortexConfig = { }, template_files: {}, }; +const defaultConfigWithNewTimeIntervalsField: AlertManagerCortexConfig = { + alertmanager_config: { + receivers: [{ name: 'default' }, { name: 'critical' }], + route: { + receiver: 'default', + group_by: ['alertname'], + routes: [ + { + matchers: ['env=prod', 'region!=EU'], + mute_time_intervals: [muteTimeInterval.name], + }, + ], + }, + templates: [], + time_intervals: [muteTimeInterval], + }, + template_files: {}, +}; + +const defaultConfigWithBothTimeIntervalsField: AlertManagerCortexConfig = { + alertmanager_config: { + receivers: [{ name: 'default' }, { name: 'critical' }], + route: { + receiver: 'default', + group_by: ['alertname'], + routes: [ + { + matchers: ['env=prod', 'region!=EU'], + mute_time_intervals: [muteTimeInterval.name], + }, + ], + }, + templates: [], + time_intervals: [muteTimeInterval], + mute_time_intervals: [muteTimeInterval2], + }, + template_files: {}, +}; const resetMocks = () => { jest.resetAllMocks(); @@ -110,7 +163,102 @@ describe('Mute timings', () => { grantUserPermissions(Object.values(AccessControlAction)); }); - it('creates a new mute timing', async () => { + it('creates a new mute timing, with mute_time_intervals in config', async () => { + renderMuteTimings(); + + await waitFor(() => expect(mocks.api.fetchAlertManagerConfig).toHaveBeenCalled()); + expect(ui.nameField.get()).toBeInTheDocument(); + + await userEvent.type(ui.nameField.get(), 'maintenance period'); + await userEvent.type(ui.startsAt.get(), '22:00'); + await userEvent.type(ui.endsAt.get(), '24:00'); + await userEvent.type(ui.days.get(), '-1'); + await userEvent.type(ui.months.get(), 'january, july'); + + fireEvent.submit(ui.form.get()); + + await waitFor(() => expect(mocks.api.updateAlertManagerConfig).toHaveBeenCalled()); + + const { mute_time_intervals: _, ...configWithoutMuteTimings } = defaultConfig.alertmanager_config; + expect(mocks.api.updateAlertManagerConfig).toHaveBeenCalledWith('grafana', { + ...defaultConfig, + alertmanager_config: { + ...configWithoutMuteTimings, + mute_time_intervals: [ + muteTimeInterval, + { + name: 'maintenance period', + time_intervals: [ + { + days_of_month: ['-1'], + months: ['january', 'july'], + times: [ + { + start_time: '22:00', + end_time: '24:00', + }, + ], + }, + ], + }, + ], + }, + }); + }); + + it('creates a new mute timing, with time_intervals in config', async () => { + mocks.api.fetchAlertManagerConfig.mockImplementation(() => { + return Promise.resolve({ + ...defaultConfigWithNewTimeIntervalsField, + }); + }); + renderMuteTimings(); + + await waitFor(() => expect(mocks.api.fetchAlertManagerConfig).toHaveBeenCalled()); + expect(ui.nameField.get()).toBeInTheDocument(); + + await userEvent.type(ui.nameField.get(), 'maintenance period'); + await userEvent.type(ui.startsAt.get(), '22:00'); + await userEvent.type(ui.endsAt.get(), '24:00'); + await userEvent.type(ui.days.get(), '-1'); + await userEvent.type(ui.months.get(), 'january, july'); + + fireEvent.submit(ui.form.get()); + + await waitFor(() => expect(mocks.api.updateAlertManagerConfig).toHaveBeenCalled()); + + const { mute_time_intervals: _, ...configWithoutMuteTimings } = defaultConfig.alertmanager_config; + expect(mocks.api.updateAlertManagerConfig).toHaveBeenCalledWith('grafana', { + ...defaultConfig, + alertmanager_config: { + ...configWithoutMuteTimings, + mute_time_intervals: [ + muteTimeInterval, + { + name: 'maintenance period', + time_intervals: [ + { + days_of_month: ['-1'], + months: ['january', 'july'], + times: [ + { + start_time: '22:00', + end_time: '24:00', + }, + ], + }, + ], + }, + ], + }, + }); + }); + it('creates a new mute timing, with time_intervals and mute_time_intervals in config', async () => { + mocks.api.fetchAlertManagerConfig.mockImplementation(() => { + return Promise.resolve({ + ...defaultConfigWithBothTimeIntervalsField, + }); + }); renderMuteTimings(); await waitFor(() => expect(mocks.api.fetchAlertManagerConfig).toHaveBeenCalled()); @@ -125,12 +273,15 @@ describe('Mute timings', () => { fireEvent.submit(ui.form.get()); await waitFor(() => expect(mocks.api.updateAlertManagerConfig).toHaveBeenCalled()); + + const { mute_time_intervals, time_intervals, ...configWithoutMuteTimings } = defaultConfig.alertmanager_config; expect(mocks.api.updateAlertManagerConfig).toHaveBeenCalledWith('grafana', { ...defaultConfig, alertmanager_config: { - ...defaultConfig.alertmanager_config, + ...configWithoutMuteTimings, mute_time_intervals: [ muteTimeInterval, + muteTimeInterval2, { name: 'maintenance period', time_intervals: [ diff --git a/public/app/features/alerting/unified/MuteTimings.tsx b/public/app/features/alerting/unified/MuteTimings.tsx index c4d1584380..a49205c477 100644 --- a/public/app/features/alerting/unified/MuteTimings.tsx +++ b/public/app/features/alerting/unified/MuteTimings.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useEffect, useState } from 'react'; -import { Route, Redirect, Switch, useRouteMatch } from 'react-router-dom'; +import { Redirect, Route, Switch, useRouteMatch } from 'react-router-dom'; import { NavModelItem } from '@grafana/data'; import { Alert } from '@grafana/ui'; @@ -21,8 +21,9 @@ const MuteTimings = () => { const config = currentData?.alertmanager_config; const getMuteTimingByName = useCallback( - (id: string): MuteTimeInterval | undefined => { - const timing = config?.mute_time_intervals?.find(({ name }: MuteTimeInterval) => name === id); + (id: string, fromTimeIntervals: boolean): MuteTimeInterval | undefined => { + const time_intervals = fromTimeIntervals ? config?.time_intervals ?? [] : config?.mute_time_intervals ?? []; + const timing = time_intervals.find(({ name }: MuteTimeInterval) => name === id); if (timing) { const provenance = config?.muteTimeProvenances?.[timing.name]; @@ -53,13 +54,17 @@ const MuteTimings = () => { {() => { if (queryParams['muteName']) { - const muteTiming = getMuteTimingByName(String(queryParams['muteName'])); + const muteTimingInMuteTimings = getMuteTimingByName(String(queryParams['muteName']), false); + const muteTimingInTimeIntervals = getMuteTimingByName(String(queryParams['muteName']), true); + const inTimeIntervals = Boolean(muteTimingInTimeIntervals); + const muteTiming = inTimeIntervals ? muteTimingInTimeIntervals : muteTimingInMuteTimings; const provenance = muteTiming?.provenance; return ( diff --git a/public/app/features/alerting/unified/NotificationPolicies.tsx b/public/app/features/alerting/unified/NotificationPolicies.tsx index b1696dbb2a..16017ecc8e 100644 --- a/public/app/features/alerting/unified/NotificationPolicies.tsx +++ b/public/app/features/alerting/unified/NotificationPolicies.tsx @@ -15,6 +15,7 @@ import { useGetContactPointsState } from './api/receiversApi'; import { AlertmanagerPageWrapper } from './components/AlertingPageWrapper'; import { GrafanaAlertmanagerDeliveryWarning } from './components/GrafanaAlertmanagerDeliveryWarning'; import { MuteTimingsTable } from './components/mute-timings/MuteTimingsTable'; +import { mergeTimeIntervals } from './components/mute-timings/util'; import { NotificationPoliciesFilter, findRoutesByMatchers, @@ -191,8 +192,9 @@ const AmRoutes = () => { if (!selectedAlertmanager) { return null; } + const time_intervals = result?.alertmanager_config ? mergeTimeIntervals(result?.alertmanager_config) : []; - const numberOfMuteTimings = result?.alertmanager_config.mute_time_intervals?.length ?? 0; + const numberOfMuteTimings = time_intervals.length; const haveData = result && !resultError && !resultLoading; const isFetching = !result && resultLoading; const haveError = resultError && !resultLoading; diff --git a/public/app/features/alerting/unified/components/mute-timings/MuteTimingForm.tsx b/public/app/features/alerting/unified/components/mute-timings/MuteTimingForm.tsx index 781c25ede1..36cc86dad8 100644 --- a/public/app/features/alerting/unified/components/mute-timings/MuteTimingForm.tsx +++ b/public/app/features/alerting/unified/components/mute-timings/MuteTimingForm.tsx @@ -12,6 +12,7 @@ import { useAlertmanager } from '../../state/AlertmanagerContext'; import { updateAlertManagerConfigAction } from '../../state/actions'; import { MuteTimingFields } from '../../types/mute-timing-form'; import { renameMuteTimings } from '../../utils/alertmanager'; +import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource'; import { makeAMLink } from '../../utils/misc'; import { createMuteTiming, defaultTimeInterval } from '../../utils/mute-timings'; import { ProvisionedResource, ProvisioningAlert } from '../Provisioning'; @@ -19,7 +20,8 @@ import { ProvisionedResource, ProvisioningAlert } from '../Provisioning'; import { MuteTimingTimeInterval } from './MuteTimingTimeInterval'; interface Props { - muteTiming?: MuteTimeInterval; + fromLegacyTimeInterval?: MuteTimeInterval; // mute time interval when comes from the old config , mute_time_intervals + fromTimeIntervals?: MuteTimeInterval; // mute time interval when comes from the new config , time_intervals. These two fields are mutually exclusive showError?: boolean; provenance?: string; loading?: boolean; @@ -50,7 +52,26 @@ const useDefaultValues = (muteTiming?: MuteTimeInterval): MuteTimingFields => { }; }; -const MuteTimingForm = ({ muteTiming, showError, loading, provenance }: Props) => { +const replaceMuteTiming = ( + originalTimings: MuteTimeInterval[], + existingTiming: MuteTimeInterval | undefined, + newTiming: MuteTimeInterval, + addNew: boolean +) => { + // we only add new timing if addNew is true. Otherwise, we just remove the existing timing + const originalTimingsWithoutNew = existingTiming + ? originalTimings?.filter(({ name }) => name !== existingTiming.name) + : originalTimings; + return addNew ? [...originalTimingsWithoutNew, newTiming] : [...originalTimingsWithoutNew]; +}; + +const MuteTimingForm = ({ + fromLegacyTimeInterval: fromMuteTimings, + fromTimeIntervals, + showError, + loading, + provenance, +}: Props) => { const dispatch = useDispatch(); const { selectedAlertmanager } = useAlertmanager(); const styles = useStyles2(getStyles); @@ -60,6 +81,12 @@ const MuteTimingForm = ({ muteTiming, showError, loading, provenance }: Props) = const { currentData: result } = useAlertmanagerConfig(selectedAlertmanager); const config = result?.alertmanager_config; + const fromIntervals = Boolean(fromTimeIntervals); + const muteTiming = fromIntervals ? fromTimeIntervals : fromMuteTimings; + + const originalMuteTimings = config?.mute_time_intervals ?? []; + const originalTimeIntervals = config?.time_intervals ?? []; + const defaultValues = useDefaultValues(muteTiming); const formApi = useForm({ defaultValues }); @@ -70,19 +97,44 @@ const MuteTimingForm = ({ muteTiming, showError, loading, provenance }: Props) = const newMuteTiming = createMuteTiming(values); - const muteTimings = muteTiming - ? config?.mute_time_intervals?.filter(({ name }) => name !== muteTiming.name) - : config?.mute_time_intervals; - + const isGrafanaDataSource = selectedAlertmanager === GRAFANA_RULES_SOURCE_NAME; + const isNewMuteTiming = fromTimeIntervals === undefined && fromMuteTimings === undefined; + + // If is Grafana data source, we wil save mute timings in the alertmanager_config.mute_time_intervals + // Otherwise, we will save it on alertmanager_config.time_intervals or alertmanager_config.mute_time_intervals depending on the original config + + const newMutetimeIntervals = isGrafanaDataSource + ? { + // for Grafana data source, we will save mute timings in the alertmanager_config.mute_time_intervals + mute_time_intervals: [ + ...replaceMuteTiming(originalTimeIntervals, fromTimeIntervals, newMuteTiming, false), + ...replaceMuteTiming(originalMuteTimings, fromMuteTimings, newMuteTiming, true), + ], + } + : { + // for non-Grafana data source, we will save mute timings in the alertmanager_config.time_intervals or alertmanager_config.mute_time_intervals depending on the original config + time_intervals: replaceMuteTiming( + originalTimeIntervals, + fromTimeIntervals, + newMuteTiming, + Boolean(fromTimeIntervals) || isNewMuteTiming + ), + mute_time_intervals: + Boolean(fromMuteTimings) && !isNewMuteTiming + ? replaceMuteTiming(originalMuteTimings, fromMuteTimings, newMuteTiming, true) + : undefined, + }; + + const { mute_time_intervals: _, time_intervals: __, ...configWithoutMuteTimings } = config ?? {}; const newConfig: AlertManagerCortexConfig = { ...result, alertmanager_config: { - ...config, + ...configWithoutMuteTimings, route: muteTiming && newMuteTiming.name !== muteTiming.name ? renameMuteTimings(newMuteTiming.name, muteTiming.name, config?.route ?? {}) : config?.route, - mute_time_intervals: [...(muteTimings || []), newMuteTiming], + ...newMutetimeIntervals, }, }; @@ -123,13 +175,8 @@ const MuteTimingForm = ({ muteTiming, showError, loading, provenance }: Props) = { - if (!muteTiming) { - const existingMuteTiming = config?.mute_time_intervals?.find(({ name }) => value === name); - return existingMuteTiming ? `Mute timing already exists for "${value}"` : true; - } - return; - }, + validate: (value) => + validateMuteTiming(value, muteTiming, originalMuteTimings, originalTimeIntervals), })} className={styles.input} data-testid={'mute-timing-name'} @@ -156,6 +203,22 @@ const MuteTimingForm = ({ muteTiming, showError, loading, provenance }: Props) = ); }; +function validateMuteTiming( + value: string, + muteTiming: MuteTimeInterval | undefined, + originalMuteTimings: MuteTimeInterval[], + originalTimeIntervals: MuteTimeInterval[] +) { + if (!muteTiming) { + const existingMuteTimingInMuteTimings = originalMuteTimings?.find(({ name }) => value === name); + const existingMuteTimingInTimeIntervals = originalTimeIntervals?.find(({ name }) => value === name); + return existingMuteTimingInMuteTimings || existingMuteTimingInTimeIntervals + ? `Mute timing already exists for "${value}"` + : true; + } + return; +} + const getStyles = (theme: GrafanaTheme2) => ({ input: css` width: 400px; diff --git a/public/app/features/alerting/unified/components/mute-timings/MuteTimingsTable.tsx b/public/app/features/alerting/unified/components/mute-timings/MuteTimingsTable.tsx index 5db1a2ce98..7e13eddc50 100644 --- a/public/app/features/alerting/unified/components/mute-timings/MuteTimingsTable.tsx +++ b/public/app/features/alerting/unified/components/mute-timings/MuteTimingsTable.tsx @@ -18,7 +18,7 @@ import { ProvisioningBadge } from '../Provisioning'; import { Spacer } from '../Spacer'; import { GrafanaMuteTimingsExporter } from '../export/GrafanaMuteTimingsExporter'; -import { renderTimeIntervals } from './util'; +import { mergeTimeIntervals, renderTimeIntervals } from './util'; const ALL_MUTE_TIMINGS = Symbol('all mute timings'); @@ -72,9 +72,9 @@ export const MuteTimingsTable = ({ alertManagerSourceName, muteTimingNames, hide const config = currentData?.alertmanager_config; const [muteTimingName, setMuteTimingName] = useState(''); - const items = useMemo((): Array> => { - const muteTimings = config?.mute_time_intervals ?? []; + // merge both fields mute_time_intervals and time_intervals to support both old and new config + const muteTimings = config ? mergeTimeIntervals(config) : []; const muteTimingsProvenances = config?.muteTimeProvenances ?? {}; return muteTimings @@ -88,7 +88,7 @@ export const MuteTimingsTable = ({ alertManagerSourceName, muteTimingNames, hide }, }; }); - }, [config?.mute_time_intervals, config?.muteTimeProvenances, muteTimingNames]); + }, [muteTimingNames, config]); const [_, allowedToCreateMuteTiming] = useAlertmanagerAbility(AlertmanagerAction.CreateMuteTiming); diff --git a/public/app/features/alerting/unified/components/mute-timings/util.tsx b/public/app/features/alerting/unified/components/mute-timings/util.tsx index dda90ae1b3..00a52f5e8f 100644 --- a/public/app/features/alerting/unified/components/mute-timings/util.tsx +++ b/public/app/features/alerting/unified/components/mute-timings/util.tsx @@ -1,7 +1,7 @@ import moment from 'moment'; import React from 'react'; -import { MuteTimeInterval } from 'app/plugins/datasource/alertmanager/types'; +import { AlertmanagerConfig, MuteTimeInterval } from 'app/plugins/datasource/alertmanager/types'; import { getDaysOfMonthString, @@ -18,6 +18,12 @@ const isvalidTimeFormat = (timeString: string): boolean => { return timeString ? TIME_RANGE_REGEX.test(timeString) : true; }; +// merge both fields mute_time_intervals and time_intervals to support both old and new config +export const mergeTimeIntervals = (alertManagerConfig: AlertmanagerConfig) => { + return [...(alertManagerConfig.mute_time_intervals ?? []), ...(alertManagerConfig.time_intervals ?? [])]; +}; + +// Usage const isValidStartAndEndTime = (startTime?: string, endTime?: string): boolean => { // empty time range is perfactly valid for a mute timing if (!startTime && !endTime) { @@ -67,4 +73,4 @@ function renderTimeIntervals(muteTiming: MuteTimeInterval) { }); } -export { isvalidTimeFormat, isValidStartAndEndTime, renderTimeIntervals }; +export { isValidStartAndEndTime, isvalidTimeFormat, renderTimeIntervals }; diff --git a/public/app/features/alerting/unified/hooks/useMuteTimingOptions.ts b/public/app/features/alerting/unified/hooks/useMuteTimingOptions.ts index 675c921975..777f615ede 100644 --- a/public/app/features/alerting/unified/hooks/useMuteTimingOptions.ts +++ b/public/app/features/alerting/unified/hooks/useMuteTimingOptions.ts @@ -2,6 +2,7 @@ import { useMemo } from 'react'; import { SelectableValue } from '@grafana/data'; +import { mergeTimeIntervals } from '../components/mute-timings/util'; import { useAlertmanager } from '../state/AlertmanagerContext'; import { timeIntervalToString } from '../utils/alertmanager'; @@ -13,8 +14,9 @@ export function useMuteTimingOptions(): Array> { const config = currentData?.alertmanager_config; return useMemo(() => { + const time_intervals = config ? mergeTimeIntervals(config) : []; const muteTimingsOptions: Array> = - config?.mute_time_intervals?.map((value) => ({ + time_intervals?.map((value) => ({ value: value.name, label: value.name, description: value.time_intervals.map((interval) => timeIntervalToString(interval)).join(', AND '), diff --git a/public/app/features/alerting/unified/state/actions.ts b/public/app/features/alerting/unified/state/actions.ts index 0035acdcec..0daafcaca6 100644 --- a/public/app/features/alerting/unified/state/actions.ts +++ b/public/app/features/alerting/unified/state/actions.ts @@ -693,10 +693,24 @@ export const deleteMuteTimingAction = (alertManagerSourceName: string, muteTimin alertmanagerApi.endpoints.getAlertmanagerConfiguration.initiate(alertManagerSourceName) ).unwrap(); - const muteIntervals = - config?.alertmanager_config?.mute_time_intervals?.filter(({ name }) => name !== muteTimingName) ?? []; + const isGrafanaDatasource = alertManagerSourceName === GRAFANA_RULES_SOURCE_NAME; + + const muteIntervalsFiltered = + (config?.alertmanager_config?.mute_time_intervals ?? [])?.filter(({ name }) => name !== muteTimingName) ?? []; + const timeIntervalsFiltered = + (config?.alertmanager_config?.time_intervals ?? [])?.filter(({ name }) => name !== muteTimingName) ?? []; + + const time_intervals_without_mute_to_save = isGrafanaDatasource + ? { + mute_time_intervals: [...muteIntervalsFiltered, ...timeIntervalsFiltered], + } + : { + time_intervals: timeIntervalsFiltered, + mute_time_intervals: muteIntervalsFiltered, + }; if (config) { + const { mute_time_intervals: _, ...configWithoutMuteTimings } = config?.alertmanager_config ?? {}; withAppEvents( dispatch( updateAlertManagerConfigAction({ @@ -705,11 +719,11 @@ export const deleteMuteTimingAction = (alertManagerSourceName: string, muteTimin newConfig: { ...config, alertmanager_config: { - ...config.alertmanager_config, + ...configWithoutMuteTimings, route: config.alertmanager_config.route ? removeMuteTimingFromRoute(muteTimingName, config.alertmanager_config?.route) : undefined, - mute_time_intervals: muteIntervals, + ...time_intervals_without_mute_to_save, }, }, }) diff --git a/public/app/plugins/datasource/alertmanager/types.ts b/public/app/plugins/datasource/alertmanager/types.ts index 4921c31a9c..059773ac0d 100644 --- a/public/app/plugins/datasource/alertmanager/types.ts +++ b/public/app/plugins/datasource/alertmanager/types.ts @@ -157,6 +157,7 @@ export type AlertmanagerConfig = { inhibit_rules?: InhibitRule[]; receivers?: Receiver[]; mute_time_intervals?: MuteTimeInterval[]; + time_intervals?: MuteTimeInterval[]; /** { [name]: provenance } */ muteTimeProvenances?: Record; last_applied?: boolean; From 393b12f49f0edac1a9df1da0a4e687582ccaf40e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A1ra=20Benc?= Date: Wed, 28 Feb 2024 15:24:34 +0100 Subject: [PATCH 0262/1406] Sql: Fix an issue with connection limits not updating when jsonData is updated (#83175) --- .../configuration/ConnectionLimits.tsx | 59 ++++++------------- .../components/configuration/NumberInput.tsx | 34 +++++++++++ 2 files changed, 53 insertions(+), 40 deletions(-) create mode 100644 packages/grafana-sql/src/components/configuration/NumberInput.tsx diff --git a/packages/grafana-sql/src/components/configuration/ConnectionLimits.tsx b/packages/grafana-sql/src/components/configuration/ConnectionLimits.tsx index 10f0eebb14..667bc7fe5c 100644 --- a/packages/grafana-sql/src/components/configuration/ConnectionLimits.tsx +++ b/packages/grafana-sql/src/components/configuration/ConnectionLimits.tsx @@ -3,25 +3,17 @@ import React from 'react'; import { DataSourceSettings } from '@grafana/data'; import { ConfigSubSection, Stack } from '@grafana/experimental'; import { config } from '@grafana/runtime'; -import { Field, Icon, InlineLabel, Input, Label, Switch, Tooltip } from '@grafana/ui'; +import { Field, Icon, InlineLabel, Label, Switch, Tooltip } from '@grafana/ui'; import { SQLConnectionLimits, SQLOptions } from '../../types'; +import { NumberInput } from './NumberInput'; + interface Props { onOptionsChange: Function; options: DataSourceSettings; } -function toNumber(text: string): number { - if (text.trim() === '') { - // calling `Number('')` returns zero, - // so we have to handle this case - return NaN; - } - - return Number(text); -} - export const ConnectionLimits = (props: Props) => { const { onOptionsChange, options } = props; const jsonData = options.jsonData; @@ -115,15 +107,11 @@ export const ConnectionLimits = (props: Props) } > - { - const newVal = toNumber(e.currentTarget.value); - if (!Number.isNaN(newVal)) { - onMaxConnectionsChanged(newVal); - } + { + onMaxConnectionsChanged(value); }} width={labelWidth} /> @@ -133,7 +121,7 @@ export const ConnectionLimits = (props: Props) label={ @@ -211,15 +194,11 @@ export const ConnectionLimits = (props: Props) } > - { - const newVal = toNumber(e.currentTarget.value); - if (!Number.isNaN(newVal)) { - onJSONDataNumberChanged('connMaxLifetime')(newVal); - } + { + onJSONDataNumberChanged('connMaxLifetime')(value); }} width={labelWidth} /> diff --git a/packages/grafana-sql/src/components/configuration/NumberInput.tsx b/packages/grafana-sql/src/components/configuration/NumberInput.tsx new file mode 100644 index 0000000000..875dc92111 --- /dev/null +++ b/packages/grafana-sql/src/components/configuration/NumberInput.tsx @@ -0,0 +1,34 @@ +import React from 'react'; + +import { Input } from '@grafana/ui/src/components/Input/Input'; + +type NumberInputProps = { + value: number; + defaultValue: number; + onChange: (value: number) => void; + width: number; +}; + +export function NumberInput({ value, defaultValue, onChange, width }: NumberInputProps) { + const [isEmpty, setIsEmpty] = React.useState(false); + return ( + { + if (e.currentTarget.value?.trim() === '') { + setIsEmpty(true); + onChange(defaultValue); + } else { + setIsEmpty(false); + const newVal = Number(e.currentTarget.value); + if (!Number.isNaN(newVal)) { + onChange(newVal); + } + } + }} + width={width} + /> + ); +} From 04539ffccbe78ec01847677f500069b88f36d169 Mon Sep 17 00:00:00 2001 From: Victor Marin <36818606+mdvictor@users.noreply.github.com> Date: Wed, 28 Feb 2024 16:41:12 +0200 Subject: [PATCH 0263/1406] Scenes: Add 'Import from library' functionality (#83498) * wip * tests + refactor ad panel func * Add row functionality * update row state only when there are children * Add new row + copy paste panels * Add library panel functionality * tests * PR mods * reafctor + tests * reafctor * fix test * refactor * fix bug on cancelling lib widget * dashboard now saves with lib panel widget * add lib panels widget works in rows as well * split add lib panel func to another PR * Add library panel functionality * refactor * take panelKey into account when getting next panel id in dashboard * fix tests --- .../scene/AddLibraryPanelWidget.test.tsx | 252 ++++++++++++++++++ .../scene/AddLibraryPanelWidget.tsx | 169 ++++++++++++ .../scene/DashboardScene.test.tsx | 11 + .../dashboard-scene/scene/DashboardScene.tsx | 24 ++ .../scene/NavToolbarActions.test.tsx | 2 + .../scene/NavToolbarActions.tsx | 16 ++ .../transformSaveModelToScene.test.ts | 37 ++- .../transformSaveModelToScene.ts | 25 ++ .../transformSceneToSaveModel.test.ts | 28 ++ .../transformSceneToSaveModel.ts | 15 ++ 10 files changed, 578 insertions(+), 1 deletion(-) create mode 100644 public/app/features/dashboard-scene/scene/AddLibraryPanelWidget.test.tsx create mode 100644 public/app/features/dashboard-scene/scene/AddLibraryPanelWidget.tsx diff --git a/public/app/features/dashboard-scene/scene/AddLibraryPanelWidget.test.tsx b/public/app/features/dashboard-scene/scene/AddLibraryPanelWidget.test.tsx new file mode 100644 index 0000000000..b982e2813c --- /dev/null +++ b/public/app/features/dashboard-scene/scene/AddLibraryPanelWidget.test.tsx @@ -0,0 +1,252 @@ +import { SceneGridItem, SceneGridLayout, SceneGridRow, SceneTimeRange } from '@grafana/scenes'; +import { LibraryPanel } from '@grafana/schema/dist/esm/index.gen'; + +import { DashboardScene } from '../scene/DashboardScene'; +import { activateFullSceneTree } from '../utils/test-utils'; + +import { AddLibraryPanelWidget } from './AddLibraryPanelWidget'; +import { LibraryVizPanel } from './LibraryVizPanel'; + +describe('AddLibraryPanelWidget', () => { + let dashboard: DashboardScene; + let addLibPanelWidget: AddLibraryPanelWidget; + const mockEvent = { + preventDefault: jest.fn(), + } as unknown as React.MouseEvent; + + beforeEach(async () => { + const result = await buildTestScene(); + dashboard = result.dashboard; + addLibPanelWidget = result.addLibPanelWidget; + }); + + it('should return the dashboard', () => { + expect(addLibPanelWidget.getDashboard()).toBe(dashboard); + }); + + it('should cancel adding a lib panel', () => { + addLibPanelWidget.onCancelAddPanel(mockEvent); + + const body = dashboard.state.body as SceneGridLayout; + + expect(body.state.children.length).toBe(0); + }); + + it('should cancel lib panel at correct position', () => { + const anotherLibPanelWidget = new AddLibraryPanelWidget({ key: 'panel-2' }); + const body = dashboard.state.body as SceneGridLayout; + + body.setState({ + children: [ + ...body.state.children, + new SceneGridItem({ + key: 'griditem-2', + x: 0, + y: 0, + width: 10, + height: 12, + body: anotherLibPanelWidget, + }), + ], + }); + dashboard.setState({ body }); + + anotherLibPanelWidget.onCancelAddPanel(mockEvent); + + const gridItem = body.state.children[0] as SceneGridItem; + + expect(body.state.children.length).toBe(1); + expect(gridItem.state.body!.state.key).toBe(addLibPanelWidget.state.key); + }); + + it('should cancel lib panel inside a row child', () => { + const anotherLibPanelWidget = new AddLibraryPanelWidget({ key: 'panel-2' }); + dashboard.setState({ + body: new SceneGridLayout({ + children: [ + new SceneGridRow({ + key: 'panel-2', + children: [ + new SceneGridItem({ + key: 'griditem-2', + x: 0, + y: 0, + width: 10, + height: 12, + body: anotherLibPanelWidget, + }), + ], + }), + ], + }), + }); + + const body = dashboard.state.body as SceneGridLayout; + + anotherLibPanelWidget.onCancelAddPanel(mockEvent); + + const gridRow = body.state.children[0] as SceneGridRow; + + expect(body.state.children.length).toBe(1); + expect(gridRow.state.children.length).toBe(0); + }); + + it('should add library panel from menu', () => { + const panelInfo: LibraryPanel = { + uid: 'uid', + model: { + type: 'timeseries', + }, + name: 'name', + version: 1, + type: 'timeseries', + }; + + const body = dashboard.state.body as SceneGridLayout; + const gridItem = body.state.children[0] as SceneGridItem; + + expect(gridItem.state.body!).toBeInstanceOf(AddLibraryPanelWidget); + + addLibPanelWidget.onAddLibraryPanel(panelInfo); + + expect(body.state.children.length).toBe(1); + expect(gridItem.state.body!).toBeInstanceOf(LibraryVizPanel); + expect((gridItem.state.body! as LibraryVizPanel).state.panelKey).toBe(addLibPanelWidget.state.key); + }); + + it('should add a lib panel at correct position', () => { + const anotherLibPanelWidget = new AddLibraryPanelWidget({ key: 'panel-2' }); + const body = dashboard.state.body as SceneGridLayout; + + body.setState({ + children: [ + ...body.state.children, + new SceneGridItem({ + key: 'griditem-2', + x: 0, + y: 0, + width: 10, + height: 12, + body: anotherLibPanelWidget, + }), + ], + }); + dashboard.setState({ body }); + + const panelInfo: LibraryPanel = { + uid: 'uid', + model: { + type: 'timeseries', + }, + name: 'name', + version: 1, + type: 'timeseries', + }; + + anotherLibPanelWidget.onAddLibraryPanel(panelInfo); + + const gridItemOne = body.state.children[0] as SceneGridItem; + const gridItemTwo = body.state.children[1] as SceneGridItem; + + expect(body.state.children.length).toBe(2); + expect(gridItemOne.state.body!).toBeInstanceOf(AddLibraryPanelWidget); + expect((gridItemTwo.state.body! as LibraryVizPanel).state.panelKey).toBe(anotherLibPanelWidget.state.key); + }); + + it('should add library panel from menu to a row child', () => { + const anotherLibPanelWidget = new AddLibraryPanelWidget({ key: 'panel-2' }); + dashboard.setState({ + body: new SceneGridLayout({ + children: [ + new SceneGridRow({ + key: 'panel-2', + children: [ + new SceneGridItem({ + key: 'griditem-2', + x: 0, + y: 0, + width: 10, + height: 12, + body: anotherLibPanelWidget, + }), + ], + }), + ], + }), + }); + + const panelInfo: LibraryPanel = { + uid: 'uid', + model: { + type: 'timeseries', + }, + name: 'name', + version: 1, + type: 'timeseries', + }; + + const body = dashboard.state.body as SceneGridLayout; + + anotherLibPanelWidget.onAddLibraryPanel(panelInfo); + + const gridRow = body.state.children[0] as SceneGridRow; + const gridItem = gridRow.state.children[0] as SceneGridItem; + + expect(body.state.children.length).toBe(1); + expect(gridItem.state.body!).toBeInstanceOf(LibraryVizPanel); + expect((gridItem.state.body! as LibraryVizPanel).state.panelKey).toBe(anotherLibPanelWidget.state.key); + }); + + it('should throw error if adding lib panel in a layout that is not SceneGridLayout', () => { + dashboard.setState({ + body: undefined, + }); + + expect(() => addLibPanelWidget.onAddLibraryPanel({} as LibraryPanel)).toThrow( + 'Trying to add a library panel in a layout that is not SceneGridLayout' + ); + }); + + it('should throw error if removing the library panel widget in a layout that is not SceneGridLayout', () => { + dashboard.setState({ + body: undefined, + }); + + expect(() => addLibPanelWidget.onCancelAddPanel(mockEvent)).toThrow( + 'Trying to remove the library panel widget in a layout that is not SceneGridLayout' + ); + }); +}); + +async function buildTestScene() { + const addLibPanelWidget = new AddLibraryPanelWidget({ key: 'panel-1' }); + const dashboard = new DashboardScene({ + $timeRange: new SceneTimeRange({}), + title: 'hello', + uid: 'dash-1', + version: 4, + meta: { + canEdit: true, + }, + body: new SceneGridLayout({ + children: [ + new SceneGridItem({ + key: 'griditem-1', + x: 0, + y: 0, + width: 10, + height: 12, + body: addLibPanelWidget, + }), + ], + }), + }); + + activateFullSceneTree(dashboard); + + await new Promise((r) => setTimeout(r, 1)); + + dashboard.onEnterEditMode(); + + return { dashboard, addLibPanelWidget }; +} diff --git a/public/app/features/dashboard-scene/scene/AddLibraryPanelWidget.tsx b/public/app/features/dashboard-scene/scene/AddLibraryPanelWidget.tsx new file mode 100644 index 0000000000..3ff23786d2 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/AddLibraryPanelWidget.tsx @@ -0,0 +1,169 @@ +import { css, cx, keyframes } from '@emotion/css'; +import React from 'react'; +import tinycolor from 'tinycolor2'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { + SceneComponentProps, + SceneGridItem, + SceneGridLayout, + SceneGridRow, + SceneObjectBase, + SceneObjectState, +} from '@grafana/scenes'; +import { LibraryPanel } from '@grafana/schema'; +import { IconButton, useStyles2 } from '@grafana/ui'; +import { Trans } from 'app/core/internationalization'; +import { + LibraryPanelsSearch, + LibraryPanelsSearchVariant, +} from 'app/features/library-panels/components/LibraryPanelsSearch/LibraryPanelsSearch'; + +import { getDashboardSceneFor } from '../utils/utils'; + +import { DashboardScene } from './DashboardScene'; +import { LibraryVizPanel } from './LibraryVizPanel'; + +export interface AddLibraryPanelWidgetState extends SceneObjectState { + key: string; +} + +export class AddLibraryPanelWidget extends SceneObjectBase { + public constructor(state: AddLibraryPanelWidgetState) { + super({ + ...state, + }); + } + + private get _dashboard(): DashboardScene { + return getDashboardSceneFor(this); + } + + public getDashboard(): DashboardScene { + return this._dashboard; + } + + public onCancelAddPanel = (evt: React.MouseEvent) => { + evt.preventDefault(); + + if (!(this._dashboard.state.body instanceof SceneGridLayout)) { + throw new Error('Trying to remove the library panel widget in a layout that is not SceneGridLayout'); + } + + const sceneGridLayout = this._dashboard.state.body; + const children = []; + + for (const child of sceneGridLayout.state.children) { + if (child.state.key !== this.parent?.state.key) { + children.push(child); + } + + if (child instanceof SceneGridRow) { + const rowChildren = []; + + for (const rowChild of child.state.children) { + if (rowChild instanceof SceneGridItem && rowChild.state.key !== this.parent?.state.key) { + rowChildren.push(rowChild); + } + } + + child.setState({ children: rowChildren }); + } + } + + sceneGridLayout.setState({ children }); + }; + + public onAddLibraryPanel = (panelInfo: LibraryPanel) => { + if (!(this._dashboard.state.body instanceof SceneGridLayout)) { + throw new Error('Trying to add a library panel in a layout that is not SceneGridLayout'); + } + + const body = new LibraryVizPanel({ + title: 'Panel Title', + uid: panelInfo.uid, + name: panelInfo.name, + panelKey: this.state.key, + }); + + if (this.parent instanceof SceneGridItem) { + this.parent.setState({ body }); + } + }; + + static Component = ({ model }: SceneComponentProps) => { + const dashboard = model.getDashboard(); + const styles = useStyles2(getStyles); + + return ( +
+
+
+ + Add panel from panel library + +
+ +
+ +
+
+ ); + }; +} + +const getStyles = (theme: GrafanaTheme2) => { + const pulsate = keyframes({ + '0%': { + boxShadow: `0 0 0 2px ${theme.colors.background.canvas}, 0 0 0px 4px ${theme.colors.primary.main}`, + }, + '50%': { + boxShadow: `0 0 0 2px ${theme.components.dashboard.background}, 0 0 0px 4px ${tinycolor(theme.colors.primary.main) + .darken(20) + .toHexString()}`, + }, + '100%': { + boxShadow: `0 0 0 2px ${theme.components.dashboard.background}, 0 0 0px 4px ${theme.colors.primary.main}`, + }, + }); + + return { + // wrapper is used to make sure box-shadow animation isn't cut off in dashboard page + wrapper: css({ + height: '100%', + paddingTop: `${theme.spacing(0.5)}`, + }), + headerRow: css({ + display: 'flex', + alignItems: 'center', + height: '38px', + flexShrink: 0, + width: '100%', + fontSize: theme.typography.fontSize, + fontWeight: theme.typography.fontWeightMedium, + paddingLeft: `${theme.spacing(1)}`, + transition: 'background-color 0.1s ease-in-out', + cursor: 'move', + + '&:hover': { + background: `${theme.colors.background.secondary}`, + }, + }), + callToAction: css({ + overflow: 'hidden', + outline: '2px dotted transparent', + outlineOffset: '2px', + boxShadow: '0 0 0 2px black, 0 0 0px 4px #1f60c4', + animation: `${pulsate} 2s ease infinite`, + }), + }; +}; diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx index 045ef0799b..0b13565722 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx @@ -264,6 +264,17 @@ describe('DashboardScene', () => { expect(gridItem.state.y).toBe(0); expect(scene.state.hasCopiedPanel).toBe(false); }); + + it('Should create a new add library panel widget', () => { + scene.onCreateLibPanelWidget(); + + const body = scene.state.body as SceneGridLayout; + const gridItem = body.state.children[0] as SceneGridItem; + + expect(body.state.children.length).toBe(5); + expect(gridItem.state.body!.state.key).toBe('panel-5'); + expect(gridItem.state.y).toBe(0); + }); }); }); diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index 5bfbe60a68..a1a4059e09 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -59,6 +59,7 @@ import { isPanelClone, } from '../utils/utils'; +import { AddLibraryPanelWidget } from './AddLibraryPanelWidget'; import { DashboardControls } from './DashboardControls'; import { DashboardSceneUrlSync } from './DashboardSceneUrlSync'; import { PanelRepeaterGridItem } from './PanelRepeaterGridItem'; @@ -612,6 +613,29 @@ export class DashboardScene extends SceneObjectBase { locationService.partial({ editview: 'settings' }); }; + public onCreateLibPanelWidget() { + if (!(this.state.body instanceof SceneGridLayout)) { + throw new Error('Trying to add a panel in a layout that is not SceneGridLayout'); + } + + const sceneGridLayout = this.state.body; + + const panelId = dashboardSceneGraph.getNextPanelId(this); + + const newGridItem = new SceneGridItem({ + height: NEW_PANEL_HEIGHT, + width: NEW_PANEL_WIDTH, + x: 0, + y: 0, + body: new AddLibraryPanelWidget({ key: getVizPanelKeyForPanelId(panelId) }), + key: `grid-item-${panelId}`, + }); + + sceneGridLayout.setState({ + children: [newGridItem, ...sceneGridLayout.state.children], + }); + } + public onCreateNewRow() { const row = getDefaultRow(this); diff --git a/public/app/features/dashboard-scene/scene/NavToolbarActions.test.tsx b/public/app/features/dashboard-scene/scene/NavToolbarActions.test.tsx index 0c192c1df9..da1c138fb7 100644 --- a/public/app/features/dashboard-scene/scene/NavToolbarActions.test.tsx +++ b/public/app/features/dashboard-scene/scene/NavToolbarActions.test.tsx @@ -18,6 +18,7 @@ describe('NavToolbarActions', () => { expect(screen.queryByLabelText('Add visualization')).not.toBeInTheDocument(); expect(screen.queryByLabelText('Add row')).not.toBeInTheDocument(); expect(screen.queryByLabelText('Paste panel')).not.toBeInTheDocument(); + expect(screen.queryByLabelText('Add library panel')).not.toBeInTheDocument(); expect(await screen.findByText('Edit')).toBeInTheDocument(); expect(await screen.findByText('Share')).toBeInTheDocument(); }); @@ -32,6 +33,7 @@ describe('NavToolbarActions', () => { expect(await screen.findByLabelText('Add visualization')).toBeInTheDocument(); expect(await screen.findByLabelText('Add row')).toBeInTheDocument(); expect(await screen.findByLabelText('Paste panel')).toBeInTheDocument(); + expect(await screen.findByLabelText('Add library panel')).toBeInTheDocument(); expect(screen.queryByText('Edit')).not.toBeInTheDocument(); expect(screen.queryByText('Share')).not.toBeInTheDocument(); }); diff --git a/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx b/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx index 76215619b7..232189129f 100644 --- a/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx +++ b/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx @@ -71,6 +71,22 @@ export function ToolbarActions({ dashboard }: Props) { ), }); + toolbarActions.push({ + group: 'icon-actions', + condition: isEditing && !editview && !isViewingPanel && !isEditingPanel, + render: () => ( + { + dashboard.onCreateLibPanelWidget(); + DashboardInteractions.toolbarAddButtonClicked({ item: 'add_library_panel' }); + }} + /> + ), + }); + toolbarActions.push({ group: 'icon-actions', condition: isEditing && !editview && !isViewingPanel && !isEditingPanel, diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts index 3992895e62..908dbbcca4 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts @@ -42,11 +42,13 @@ import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard'; import { DASHBOARD_DATASOURCE_PLUGIN_ID } from 'app/plugins/datasource/dashboard/types'; import { DashboardDataDTO } from 'app/types'; +import { AddLibraryPanelWidget } from '../scene/AddLibraryPanelWidget'; +import { LibraryVizPanel } from '../scene/LibraryVizPanel'; import { PanelRepeaterGridItem } from '../scene/PanelRepeaterGridItem'; import { PanelTimeRange } from '../scene/PanelTimeRange'; import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior'; import { NEW_LINK } from '../settings/links/utils'; -import { getQueryRunnerFor } from '../utils/utils'; +import { getQueryRunnerFor, getVizPanelKeyForPanelId } from '../utils/utils'; import { buildNewDashboardSaveModel } from './buildNewDashboardSaveModel'; import { GRAFANA_DATASOURCE_REF } from './const'; @@ -58,6 +60,8 @@ import { createSceneVariableFromVariableModel, transformSaveModelToScene, convertOldSnapshotToScenesSnapshot, + buildGridItemForLibPanel, + buildGridItemForLibraryPanelWidget, } from './transformSaveModelToScene'; describe('transformSaveModelToScene', () => { @@ -453,6 +457,37 @@ describe('transformSaveModelToScene', () => { expect(runner.state.cacheTimeout).toBe('10'); expect(runner.state.queryCachingTTL).toBe(200000); }); + it('should convert saved lib widget to AddLibraryPanelWidget', () => { + const panel = { + id: 10, + type: 'add-library-panel', + }; + + const gridItem = buildGridItemForLibraryPanelWidget(new PanelModel(panel))!; + const libPanelWidget = gridItem.state.body as AddLibraryPanelWidget; + + expect(libPanelWidget.state.key).toEqual(getVizPanelKeyForPanelId(panel.id)); + }); + + it('should convert saved lib panel to LibraryVizPanel', () => { + const panel = { + title: 'Panel', + gridPos: { x: 0, y: 0, w: 12, h: 8 }, + transparent: true, + libraryPanel: { + uid: '123', + name: 'My Panel', + folderUid: '456', + }, + }; + + const gridItem = buildGridItemForLibPanel(new PanelModel(panel))!; + const libVizPanel = gridItem.state.body as LibraryVizPanel; + + expect(libVizPanel.state.uid).toEqual(panel.libraryPanel.uid); + expect(libVizPanel.state.name).toEqual(panel.libraryPanel.name); + expect(libVizPanel.state.title).toEqual(panel.title); + }); }); describe('when creating variables objects', () => { diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts index 096f378a01..1957b83104 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts @@ -34,6 +34,7 @@ import { DashboardModel, PanelModel } from 'app/features/dashboard/state'; import { trackDashboardLoaded } from 'app/features/dashboard/utils/tracking'; import { DashboardDTO } from 'app/types'; +import { AddLibraryPanelWidget } from '../scene/AddLibraryPanelWidget'; import { AlertStatesDataLayer } from '../scene/AlertStatesDataLayer'; import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer'; import { DashboardControls } from '../scene/DashboardControls'; @@ -110,6 +111,12 @@ export function createSceneObjectsForPanels(oldPanels: PanelModel[]): SceneGridI currentRowPanels = []; } } + } else if (panel.type === 'add-library-panel') { + const gridItem = buildGridItemForLibraryPanelWidget(panel); + + if (gridItem) { + panels.push(gridItem); + } } else if (panel.libraryPanel?.uid && !('model' in panel.libraryPanel)) { const gridItem = buildGridItemForLibPanel(panel); if (gridItem) { @@ -399,6 +406,24 @@ export function createSceneVariableFromVariableModel(variable: TypedVariableMode } } +export function buildGridItemForLibraryPanelWidget(panel: PanelModel) { + if (panel.type !== 'add-library-panel') { + return null; + } + + const body = new AddLibraryPanelWidget({ + key: getVizPanelKeyForPanelId(panel.id), + }); + + return new SceneGridItem({ + body, + y: panel.gridPos.y, + x: panel.gridPos.x, + width: panel.gridPos.w, + height: panel.gridPos.h, + }); +} + export function buildGridItemForLibPanel(panel: PanelModel) { if (!panel.libraryPanel) { return null; diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts index 2901b08723..f088fc622c 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts @@ -41,6 +41,7 @@ import snapshotableDashboardJson from './testfiles/snapshotable_dashboard.json'; import snapshotableWithRowsDashboardJson from './testfiles/snapshotable_with_rows.json'; import { buildGridItemForLibPanel, + buildGridItemForLibraryPanelWidget, buildGridItemForPanel, transformSaveModelToScene, } from './transformSaveModelToScene'; @@ -351,6 +352,30 @@ describe('transformSceneToSaveModel', () => { expect(result.transformations).toBeUndefined(); expect(result.fieldConfig).toBeUndefined(); }); + + it('given a library panel widget', () => { + const panel = buildGridItemFromPanelSchema({ + id: 4, + gridPos: { + h: 8, + w: 12, + x: 0, + y: 0, + }, + type: 'add-library-panel', + }); + + const result = gridItemToPanel(panel); + + expect(result.id).toBe(4); + expect(result.gridPos).toEqual({ + h: 8, + w: 12, + x: 0, + y: 0, + }); + expect(result.type).toBe('add-library-panel'); + }); }); describe('Annotations', () => { @@ -897,6 +922,9 @@ describe('transformSceneToSaveModel', () => { export function buildGridItemFromPanelSchema(panel: Partial): SceneGridItemLike { if (panel.libraryPanel) { return buildGridItemForLibPanel(new PanelModel(panel))!; + } else if (panel.type === 'add-library-panel') { + return buildGridItemForLibraryPanelWidget(new PanelModel(panel))!; } + return buildGridItemForPanel(new PanelModel(panel)); } diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts index d0f61b9f2c..12028b43cd 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts @@ -33,6 +33,7 @@ import { getPanelDataFrames } from 'app/features/dashboard/components/HelpWizard import { DASHBOARD_SCHEMA_VERSION } from 'app/features/dashboard/state/DashboardMigrator'; import { GrafanaQueryType } from 'app/plugins/datasource/grafana/types'; +import { AddLibraryPanelWidget } from '../scene/AddLibraryPanelWidget'; import { DashboardScene } from '../scene/DashboardScene'; import { LibraryVizPanel } from '../scene/LibraryVizPanel'; import { PanelRepeaterGridItem } from '../scene/PanelRepeaterGridItem'; @@ -167,6 +168,20 @@ export function gridItemToPanel(gridItem: SceneGridItemLike, isSnapshot = false) } as Panel; } + // Handle library panel widget as well and exit early + if (gridItem.state.body instanceof AddLibraryPanelWidget) { + x = gridItem.state.x ?? 0; + y = gridItem.state.y ?? 0; + w = gridItem.state.width ?? 0; + h = gridItem.state.height ?? 0; + + return { + id: getPanelIdForVizPanel(gridItem.state.body), + type: 'add-library-panel', + gridPos: { x, y, w, h }, + }; + } + if (!(gridItem.state.body instanceof VizPanel)) { throw new Error('SceneGridItem body expected to be VizPanel'); } From 58d6ce1c8746d82f7981c052d95dda978f7d00e3 Mon Sep 17 00:00:00 2001 From: Victor Marin <36818606+mdvictor@users.noreply.github.com> Date: Wed, 28 Feb 2024 16:41:56 +0200 Subject: [PATCH 0264/1406] Add lib panel from empty dashboard page (#83522) * wip * tests + refactor ad panel func * Add row functionality * update row state only when there are children * Add new row + copy paste panels * Add library panel functionality * tests * PR mods * reafctor + tests * reafctor * fix test * refactor * fix bug on cancelling lib widget * dashboard now saves with lib panel widget * add lib panels widget works in rows as well * split add lib panel func to another PR * Add library panel functionality * Add lib panel from empty dashboard page * refactor * take panelKey into account when getting next panel id in dashboard * fix tests --- public/app/features/dashboard/dashgrid/DashboardEmpty.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/dashboard/dashgrid/DashboardEmpty.tsx b/public/app/features/dashboard/dashgrid/DashboardEmpty.tsx index f30bcf95a5..9de79fbc1d 100644 --- a/public/app/features/dashboard/dashgrid/DashboardEmpty.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardEmpty.tsx @@ -113,7 +113,7 @@ const DashboardEmpty = ({ dashboard, canCreate }: Props) => { onClick={() => { DashboardInteractions.emptyDashboardButtonClicked({ item: 'import_from_library' }); if (dashboard instanceof DashboardScene) { - // TODO: dashboard scene logic for adding a library panel + dashboard.onCreateLibPanelWidget(); } else { onAddLibraryPanel(dashboard); } From 7d6d256335d36a5ce560d8ab5e7da8816eccea5e Mon Sep 17 00:00:00 2001 From: Ezequiel Victorero Date: Wed, 28 Feb 2024 11:45:22 -0300 Subject: [PATCH 0265/1406] Snapshots: Change default expiration (#83550) --- .../sharing/ShareSnapshotTab.tsx | 18 +++++++++++------- .../components/ShareModal/ShareSnapshot.tsx | 16 ++++++++-------- public/locales/en-US/grafana.json | 6 +++--- public/locales/pseudo-LOCALE/grafana.json | 6 +++--- 4 files changed, 25 insertions(+), 21 deletions(-) diff --git a/public/app/features/dashboard-scene/sharing/ShareSnapshotTab.tsx b/public/app/features/dashboard-scene/sharing/ShareSnapshotTab.tsx index 2e9503c3a1..7fae7cf163 100644 --- a/public/app/features/dashboard-scene/sharing/ShareSnapshotTab.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareSnapshotTab.tsx @@ -17,12 +17,11 @@ import { SceneShareTabState } from './types'; const getExpireOptions = () => { const DEFAULT_EXPIRE_OPTION: SelectableValue = { - label: t('share-modal.snapshot.expire-never', `Never`), - value: 0, + label: t('share-modal.snapshot.expire-week', '1 Week'), + value: 60 * 60 * 24 * 7, }; return [ - DEFAULT_EXPIRE_OPTION, { label: t('share-modal.snapshot.expire-hour', '1 Hour'), value: 60 * 60, @@ -31,13 +30,18 @@ const getExpireOptions = () => { label: t('share-modal.snapshot.expire-day', '1 Day'), value: 60 * 60 * 24, }, + DEFAULT_EXPIRE_OPTION, { - label: t('share-modal.snapshot.expire-week', '7 Days'), - value: 60 * 60 * 24 * 7, + label: t('share-modal.snapshot.expire-never', `Never`), + value: 0, }, ]; }; +const getDefaultExpireOption = () => { + return getExpireOptions()[2]; +}; + export interface ShareSnapshotTabState extends SceneShareTabState { panelRef?: SceneObjectRef; dashboardRef: SceneObjectRef; @@ -55,7 +59,7 @@ export class ShareSnapshotTab extends SceneObjectBase { super({ ...state, snapshotName: state.dashboardRef.resolve().state.title, - selectedExpireOption: getExpireOptions()[0], + selectedExpireOption: getDefaultExpireOption(), }); this.addActivationHandler(() => { @@ -207,7 +211,7 @@ function ShareSnapshoTabRenderer({ model }: SceneComponentProps )} diff --git a/public/app/features/dashboard/components/ShareModal/ShareSnapshot.tsx b/public/app/features/dashboard/components/ShareModal/ShareSnapshot.tsx index 2c93b86df2..aa449768aa 100644 --- a/public/app/features/dashboard/components/ShareModal/ShareSnapshot.tsx +++ b/public/app/features/dashboard/components/ShareModal/ShareSnapshot.tsx @@ -36,10 +36,6 @@ export class ShareSnapshot extends PureComponent { super(props); this.dashboard = props.dashboard; this.expireOptions = [ - { - label: t('share-modal.snapshot.expire-never', `Never`), - value: 0, - }, { label: t('share-modal.snapshot.expire-hour', `1 Hour`), value: 60 * 60, @@ -49,15 +45,19 @@ export class ShareSnapshot extends PureComponent { value: 60 * 60 * 24, }, { - label: t('share-modal.snapshot.expire-week', `7 Days`), + label: t('share-modal.snapshot.expire-week', `1 Week`), value: 60 * 60 * 24 * 7, }, + { + label: t('share-modal.snapshot.expire-never', `Never`), + value: 0, + }, ]; this.state = { isLoading: false, step: 1, - selectedExpireOption: this.expireOptions[0], - snapshotExpires: this.expireOptions[0].value, + selectedExpireOption: this.expireOptions[2], + snapshotExpires: this.expireOptions[2].value, snapshotName: props.dashboard.title, timeoutSeconds: 4, snapshotUrl: '', @@ -277,7 +277,7 @@ export class ShareSnapshot extends PureComponent { )} diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index e93f39c9c3..6c72ea3aee 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -1415,10 +1415,10 @@ "expire-day": "1 Day", "expire-hour": "1 Hour", "expire-never": "Never", - "expire-week": "7 Days", + "expire-week": "1 Week", "info-text-1": "A snapshot is an instant way to share an interactive dashboard publicly. When created, we strip sensitive data like queries (metric, template, and annotation) and panel links, leaving only the visible metric data and series names embedded in your dashboard.", "info-text-2": "Keep in mind, your snapshot <1>can be viewed by anyone that has the link and can access the URL. Share wisely.", - "local-button": "Local Snapshot", + "local-button": "Publish Snapshot", "mistake-message": "Did you make a mistake? ", "name": "Snapshot name", "timeout": "Timeout (seconds)", @@ -1431,7 +1431,7 @@ "library-panel": "Library panel", "link": "Link", "panel-embed": "Embed", - "public-dashboard": "Public Dashboard", + "public-dashboard": "Publish Dashboard", "public-dashboard-title": "Public dashboard", "snapshot": "Snapshot" }, diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index a41468f885..c1fe5d179e 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -1415,10 +1415,10 @@ "expire-day": "1 Đäy", "expire-hour": "1 Ħőūř", "expire-never": "Ńęvęř", - "expire-week": "7 Đäyş", + "expire-week": "1 Ŵęęĸ", "info-text-1": "Å şʼnäpşĥőŧ įş äʼn įʼnşŧäʼnŧ ŵäy ŧő şĥäřę äʼn įʼnŧęřäčŧįvę đäşĥþőäřđ pūþľįčľy. Ŵĥęʼn čřęäŧęđ, ŵę şŧřįp şęʼnşįŧįvę đäŧä ľįĸę qūęřįęş (męŧřįč, ŧęmpľäŧę, äʼnđ äʼnʼnőŧäŧįőʼn) äʼnđ päʼnęľ ľįʼnĸş, ľęävįʼnģ őʼnľy ŧĥę vįşįþľę męŧřįč đäŧä äʼnđ şęřįęş ʼnämęş ęmþęđđęđ įʼn yőūř đäşĥþőäřđ.", "info-text-2": "Ķęęp įʼn mįʼnđ, yőūř şʼnäpşĥőŧ <1>čäʼn þę vįęŵęđ þy äʼnyőʼnę ŧĥäŧ ĥäş ŧĥę ľįʼnĸ äʼnđ čäʼn äččęşş ŧĥę ŮŖĿ. Ŝĥäřę ŵįşęľy.", - "local-button": "Ŀőčäľ Ŝʼnäpşĥőŧ", + "local-button": "Pūþľįşĥ Ŝʼnäpşĥőŧ", "mistake-message": "Đįđ yőū mäĸę ä mįşŧäĸę? ", "name": "Ŝʼnäpşĥőŧ ʼnämę", "timeout": "Ŧįmęőūŧ (şęčőʼnđş)", @@ -1431,7 +1431,7 @@ "library-panel": "Ŀįþřäřy päʼnęľ", "link": "Ŀįʼnĸ", "panel-embed": "Ēmþęđ", - "public-dashboard": "Pūþľįč Đäşĥþőäřđ", + "public-dashboard": "Pūþľįşĥ Đäşĥþőäřđ", "public-dashboard-title": "Pūþľįč đäşĥþőäřđ", "snapshot": "Ŝʼnäpşĥőŧ" }, From 757fa06b85f8e1e9d416134e383fc15d7d521fa7 Mon Sep 17 00:00:00 2001 From: ismail simsek Date: Wed, 28 Feb 2024 15:59:06 +0100 Subject: [PATCH 0266/1406] InfluxDB: Fix interpolation of multi value template variables by adding parenthesis around them (#83577) Put parenthesis around multi value template variable --- public/app/plugins/datasource/influxdb/datasource.test.ts | 6 +++--- public/app/plugins/datasource/influxdb/datasource.ts | 6 ++++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/public/app/plugins/datasource/influxdb/datasource.test.ts b/public/app/plugins/datasource/influxdb/datasource.test.ts index 5e20d38370..01f1ab2f7e 100644 --- a/public/app/plugins/datasource/influxdb/datasource.test.ts +++ b/public/app/plugins/datasource/influxdb/datasource.test.ts @@ -452,7 +452,7 @@ describe('InfluxDataSource Frontend Mode', () => { .withIncludeAll(true) .build(); const result = ds.interpolateQueryExpr(value, variableMock, 'select from /^($tempVar)$/'); - const expectation = `env|env2|env3`; + const expectation = `(env|env2|env3)`; expect(result).toBe(expectation); }); @@ -476,7 +476,7 @@ describe('InfluxDataSource Frontend Mode', () => { const value = [`/special/path`, `/some/other/path`]; const variableMock = queryBuilder().withId('tempVar').withName('tempVar').withMulti().build(); const result = ds.interpolateQueryExpr(value, variableMock, `select that where path = '$tempVar'`); - const expectation = `\\/special\\/path|\\/some\\/other\\/path`; + const expectation = `(\\/special\\/path|\\/some\\/other\\/path)`; expect(result).toBe(expectation); }); @@ -505,7 +505,7 @@ describe('InfluxDataSource Frontend Mode', () => { .build(); const value = [`/special/path`, `/some/other/path`]; const result = ds.interpolateQueryExpr(value, variableMock, `select that where path = /$tempVar/`); - const expectation = `\\/special\\/path|\\/some\\/other\\/path`; + const expectation = `(\\/special\\/path|\\/some\\/other\\/path)`; expect(result).toBe(expectation); }); }); diff --git a/public/app/plugins/datasource/influxdb/datasource.ts b/public/app/plugins/datasource/influxdb/datasource.ts index b3837a0987..d5be06528a 100644 --- a/public/app/plugins/datasource/influxdb/datasource.ts +++ b/public/app/plugins/datasource/influxdb/datasource.ts @@ -309,7 +309,8 @@ export default class InfluxDatasource extends DataSourceWithBackend escapeRegex(v)).join('|'); + // then put inside parenthesis. + return `(${value.map((v) => escapeRegex(v)).join('|')})`; } // If the variable is not a multi-value variable @@ -324,7 +325,8 @@ export default class InfluxDatasource extends DataSourceWithBackend escapeRegex(v)).join('|'); + // then put inside parenthesis. + return `(${value.map((v) => escapeRegex(v)).join('|')})`; } return value; From 91cd17f012d280d8a0255d91c2c5addcf8a803de Mon Sep 17 00:00:00 2001 From: Misi Date: Wed, 28 Feb 2024 16:01:02 +0100 Subject: [PATCH 0267/1406] Chore: Move TLS settings to the Extra Security Measures section (SSO Settings UI) (#83602) Move TLS settings to the Extra Security Measures section --- public/app/features/auth-config/fields.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/public/app/features/auth-config/fields.tsx b/public/app/features/auth-config/fields.tsx index 6bc81af4b1..4a93b8f1cc 100644 --- a/public/app/features/auth-config/fields.tsx +++ b/public/app/features/auth-config/fields.tsx @@ -86,13 +86,12 @@ export const sectionFields: Section = { { name: 'teamIdsAttributePath', dependsOn: 'defineAllowedTeamsIds' }, 'usePkce', 'useRefreshToken', + 'tlsSkipVerifyInsecure', + 'tlsClientCert', + 'tlsClientKey', + 'tlsClientCa', ], }, - { - name: 'TLS', - id: 'tls', - fields: ['tlsSkipVerifyInsecure', 'tlsClientCert', 'tlsClientKey', 'tlsClientCa'], - }, ], }; From 467302480f4a6476e8b82ba0ef4f9918c65fd21f Mon Sep 17 00:00:00 2001 From: Arati R <33031346+suntala@users.noreply.github.com> Date: Wed, 28 Feb 2024 16:11:09 +0100 Subject: [PATCH 0268/1406] Search: Include collapsed panels in search v2 (#83047) * Include collapsed panels in searchv2 * Include collapsed row in TestReadSummaries --- pkg/services/store/kind/dashboard/summary.go | 77 +++++++++++-------- .../testdata/with-library-panels-info.json | 30 ++++++++ .../testdata/with-library-panels.json | 28 +++++++ 3 files changed, 104 insertions(+), 31 deletions(-) diff --git a/pkg/services/store/kind/dashboard/summary.go b/pkg/services/store/kind/dashboard/summary.go index 5850bd19fb..7987f1c6c1 100644 --- a/pkg/services/store/kind/dashboard/summary.go +++ b/pkg/services/store/kind/dashboard/summary.go @@ -60,43 +60,58 @@ func NewStaticDashboardSummaryBuilder(lookup DatasourceLookup, sanitize bool) en summary.Fields["schemaVersion"] = fmt.Sprint(dash.SchemaVersion) for _, panel := range dash.Panels { - panelRefs := NewReferenceAccumulator() - p := &entity.EntitySummary{ - UID: uid + "#" + strconv.FormatInt(panel.ID, 10), - Kind: "panel", - } - p.Name = panel.Title - p.Description = panel.Description - p.Fields = make(map[string]string, 0) - p.Fields["type"] = panel.Type - - if panel.Type != "row" { - panelRefs.Add(entity.ExternalEntityReferencePlugin, string(plugins.TypePanel), panel.Type) - dashboardRefs.Add(entity.ExternalEntityReferencePlugin, string(plugins.TypePanel), panel.Type) - } - if panel.LibraryPanel != "" { - panelRefs.Add(entity.StandardKindLibraryPanel, panel.Type, panel.LibraryPanel) - dashboardRefs.Add(entity.StandardKindLibraryPanel, panel.Type, panel.LibraryPanel) - } - for _, v := range panel.Datasource { - dashboardRefs.Add(entity.StandardKindDataSource, v.Type, v.UID) - panelRefs.Add(entity.StandardKindDataSource, v.Type, v.UID) - if v.Type != "" { - dashboardRefs.Add(entity.ExternalEntityReferencePlugin, string(plugins.TypeDataSource), v.Type) - } - } - for _, v := range panel.Transformer { - panelRefs.Add(entity.ExternalEntityReferenceRuntime, entity.ExternalEntityReferenceRuntime_Transformer, v) - dashboardRefs.Add(entity.ExternalEntityReferenceRuntime, entity.ExternalEntityReferenceRuntime_Transformer, v) - } - p.References = panelRefs.Get() - summary.Nested = append(summary.Nested, p) + s := panelSummary(panel, uid, dashboardRefs) + summary.Nested = append(summary.Nested, s...) } summary.References = dashboardRefs.Get() if sanitize { body, err = json.MarshalIndent(parsed, "", " ") } + return summary, body, err } } + +// panelSummary take panel info and returns entity summaries for the given panel and all its collapsed panels. +func panelSummary(panel panelInfo, uid string, dashboardRefs ReferenceAccumulator) []*entity.EntitySummary { + panels := []*entity.EntitySummary{} + + panelRefs := NewReferenceAccumulator() + p := &entity.EntitySummary{ + UID: uid + "#" + strconv.FormatInt(panel.ID, 10), + Kind: "panel", + } + p.Name = panel.Title + p.Description = panel.Description + p.Fields = make(map[string]string, 0) + p.Fields["type"] = panel.Type + + if panel.Type != "row" { + panelRefs.Add(entity.ExternalEntityReferencePlugin, string(plugins.TypePanel), panel.Type) + dashboardRefs.Add(entity.ExternalEntityReferencePlugin, string(plugins.TypePanel), panel.Type) + } + if panel.LibraryPanel != "" { + panelRefs.Add(entity.StandardKindLibraryPanel, panel.Type, panel.LibraryPanel) + dashboardRefs.Add(entity.StandardKindLibraryPanel, panel.Type, panel.LibraryPanel) + } + for _, v := range panel.Datasource { + dashboardRefs.Add(entity.StandardKindDataSource, v.Type, v.UID) + panelRefs.Add(entity.StandardKindDataSource, v.Type, v.UID) + if v.Type != "" { + dashboardRefs.Add(entity.ExternalEntityReferencePlugin, string(plugins.TypeDataSource), v.Type) + } + } + for _, v := range panel.Transformer { + panelRefs.Add(entity.ExternalEntityReferenceRuntime, entity.ExternalEntityReferenceRuntime_Transformer, v) + dashboardRefs.Add(entity.ExternalEntityReferenceRuntime, entity.ExternalEntityReferenceRuntime_Transformer, v) + } + p.References = panelRefs.Get() + panels = append(panels, p) + + for _, c := range panel.Collapsed { + collapsed := panelSummary(c, uid, dashboardRefs) + panels = append(panels, collapsed...) + } + return panels +} diff --git a/pkg/services/store/kind/dashboard/testdata/with-library-panels-info.json b/pkg/services/store/kind/dashboard/testdata/with-library-panels-info.json index e360bc820e..670b69576a 100644 --- a/pkg/services/store/kind/dashboard/testdata/with-library-panels-info.json +++ b/pkg/services/store/kind/dashboard/testdata/with-library-panels-info.json @@ -45,6 +45,32 @@ "type": "panel" } ] + }, + { + "UID": "with-library-panels#3", + "kind": "panel", + "name": "collapsed row", + "fields": { + "type": "row" + } + }, + { + "UID": "with-library-panels#42", + "kind": "panel", + "name": "blue pie", + "fields": { + "type": "" + }, + "references": [ + { + "family": "librarypanel", + "identifier": "l3d2s634-fdgf-75u4-3fg3-67j966ii7jur" + }, + { + "family": "plugin", + "type": "panel" + } + ] } ], "references": [ @@ -59,6 +85,10 @@ "family": "librarypanel", "identifier": "e1d5f519-dabd-47c6-9ad7-83d181ce1cee" }, + { + "family": "librarypanel", + "identifier": "l3d2s634-fdgf-75u4-3fg3-67j966ii7jur" + }, { "family": "plugin", "type": "panel" diff --git a/pkg/services/store/kind/dashboard/testdata/with-library-panels.json b/pkg/services/store/kind/dashboard/testdata/with-library-panels.json index 3dd0faeb9a..cecd1c64e6 100644 --- a/pkg/services/store/kind/dashboard/testdata/with-library-panels.json +++ b/pkg/services/store/kind/dashboard/testdata/with-library-panels.json @@ -49,6 +49,34 @@ "uid": "e1d5f519-dabd-47c6-9ad7-83d181ce1cee" }, "title": "green pie" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 9 + }, + "id": 3, + "panels": [ + { + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 42, + "libraryPanel": { + "name": "blue pie", + "uid": "l3d2s634-fdgf-75u4-3fg3-67j966ii7jur" + }, + "title": "blue pie" + } + ], + "title": "collapsed row", + "type": "row" } ], "refresh": "", From b905777ba9cb59fb20b38e951066d12e82efc12d Mon Sep 17 00:00:00 2001 From: Joe Blubaugh Date: Wed, 28 Feb 2024 23:19:02 +0800 Subject: [PATCH 0269/1406] Alerting: Support deleting rule groups in the provisioning API (#83514) * Alerting: feat: support deleting rule groups in the provisioning API Adds support for DELETE to the provisioning API's alert rule groups route, which allows deleting the rule group with a single API call. Previously, groups were deleted by deleting rules one-by-one. Fixes #81860 This change doesn't add any new paths to the API, only new methods. --------- Co-authored-by: Yuri Tseretyan --- pkg/services/ngalert/api/api_provisioning.go | 13 +++++ .../ngalert/api/api_provisioning_test.go | 29 ++++++++--- pkg/services/ngalert/api/authorization.go | 3 +- .../api/generated_base_api_provisioning.go | 19 +++++++ pkg/services/ngalert/api/provisioning.go | 4 ++ pkg/services/ngalert/api/tooling/api.json | 38 ++++++++++++++ .../definitions/provisioning_alert_rules.go | 13 ++++- pkg/services/ngalert/api/tooling/post.json | 38 ++++++++++++++ pkg/services/ngalert/api/tooling/spec.json | 39 +++++++++++++++ .../ngalert/provisioning/alert_rules.go | 32 ++++++++++++ public/api-merged.json | 38 ++++++++++++++ public/openapi3.json | 50 +++++++++++++++++++ 12 files changed, 307 insertions(+), 9 deletions(-) diff --git a/pkg/services/ngalert/api/api_provisioning.go b/pkg/services/ngalert/api/api_provisioning.go index 2bba2976ff..a1d893ecc1 100644 --- a/pkg/services/ngalert/api/api_provisioning.go +++ b/pkg/services/ngalert/api/api_provisioning.go @@ -65,6 +65,7 @@ type AlertRuleService interface { DeleteAlertRule(ctx context.Context, orgID int64, ruleUID string, provenance alerting_models.Provenance) error GetRuleGroup(ctx context.Context, orgID int64, folder, group string) (alerting_models.AlertRuleGroup, error) ReplaceRuleGroup(ctx context.Context, orgID int64, group alerting_models.AlertRuleGroup, userID int64, provenance alerting_models.Provenance) error + DeleteRuleGroup(ctx context.Context, orgID int64, folder, group string, provenance alerting_models.Provenance) error GetAlertRuleWithFolderTitle(ctx context.Context, orgID int64, ruleUID string) (provisioning.AlertRuleWithFolderTitle, error) GetAlertRuleGroupWithFolderTitle(ctx context.Context, orgID int64, folder, group string) (alerting_models.AlertRuleGroupWithFolderTitle, error) GetAlertGroupsWithFolderTitle(ctx context.Context, orgID int64, folderUIDs []string) ([]alerting_models.AlertRuleGroupWithFolderTitle, error) @@ -505,6 +506,18 @@ func (srv *ProvisioningSrv) RoutePutAlertRuleGroup(c *contextmodel.ReqContext, a return response.JSON(http.StatusOK, ag) } +func (srv *ProvisioningSrv) RouteDeleteAlertRuleGroup(c *contextmodel.ReqContext, folderUID string, group string) response.Response { + provenance := determineProvenance(c) + err := srv.alertRules.DeleteRuleGroup(c.Req.Context(), c.SignedInUser.GetOrgID(), folderUID, group, alerting_models.Provenance(provenance)) + if err != nil { + if errors.Is(err, store.ErrAlertRuleGroupNotFound) { + return ErrResp(http.StatusNotFound, err, "") + } + return ErrResp(http.StatusInternalServerError, err, "") + } + return response.JSON(http.StatusNoContent, "") +} + func determineProvenance(ctx *contextmodel.ReqContext) definitions.Provenance { if _, disabled := ctx.Req.Header[disableProvenanceHeaderName]; disabled { return definitions.Provenance(alerting_models.ProvenanceNone) diff --git a/pkg/services/ngalert/api/api_provisioning_test.go b/pkg/services/ngalert/api/api_provisioning_test.go index df1d266747..c8d60ed330 100644 --- a/pkg/services/ngalert/api/api_provisioning_test.go +++ b/pkg/services/ngalert/api/api_provisioning_test.go @@ -343,24 +343,40 @@ func TestProvisioningApi(t *testing.T) { }) t.Run("alert rule groups", func(t *testing.T) { - t.Run("are present, GET returns 200", func(t *testing.T) { + t.Run("are present", func(t *testing.T) { sut := createProvisioningSrvSut(t) rc := createTestRequestCtx() insertRule(t, sut, createTestAlertRule("rule", 1)) - response := sut.RouteGetAlertRuleGroup(&rc, "folder-uid", "my-cool-group") + t.Run("GET returns 200", func(t *testing.T) { + response := sut.RouteGetAlertRuleGroup(&rc, "folder-uid", "my-cool-group") - require.Equal(t, 200, response.Status()) + require.Equal(t, 200, response.Status()) + }) + + t.Run("DELETE returns 204", func(t *testing.T) { + response := sut.RouteDeleteAlertRuleGroup(&rc, "folder-uid", "my-cool-group") + + require.Equal(t, 204, response.Status()) + }) }) - t.Run("are missing, GET returns 404", func(t *testing.T) { + t.Run("are missing", func(t *testing.T) { sut := createProvisioningSrvSut(t) rc := createTestRequestCtx() insertRule(t, sut, createTestAlertRule("rule", 1)) - response := sut.RouteGetAlertRuleGroup(&rc, "folder-uid", "does not exist") + t.Run("GET returns 404", func(t *testing.T) { + response := sut.RouteGetAlertRuleGroup(&rc, "folder-uid", "does not exist") - require.Equal(t, 404, response.Status()) + require.Equal(t, 404, response.Status()) + }) + + t.Run("DELETE returns 404", func(t *testing.T) { + response := sut.RouteDeleteAlertRuleGroup(&rc, "folder-uid", "does not exist") + + require.Equal(t, 404, response.Status()) + }) }) t.Run("are invalid at group level", func(t *testing.T) { @@ -1587,6 +1603,7 @@ func createTestEnv(t *testing.T, testConfig string) testEnvironment { }) sqlStore := db.InitTestDB(t) store := store.DBstore{ + Logger: log, SQLStore: sqlStore, Cfg: setting.UnifiedAlertingSettings{ BaseInterval: time.Second * 10, diff --git a/pkg/services/ngalert/api/authorization.go b/pkg/services/ngalert/api/authorization.go index 44cf3babbd..0ee2e58f7d 100644 --- a/pkg/services/ngalert/api/authorization.go +++ b/pkg/services/ngalert/api/authorization.go @@ -253,7 +253,8 @@ func (api *API) authorize(method, path string) web.Handler { http.MethodPost + "/api/v1/provisioning/alert-rules", http.MethodPut + "/api/v1/provisioning/alert-rules/{UID}", http.MethodDelete + "/api/v1/provisioning/alert-rules/{UID}", - http.MethodPut + "/api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}": + http.MethodPut + "/api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}", + http.MethodDelete + "/api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}": eval = ac.EvalPermission(ac.ActionAlertingProvisioningWrite) // organization scope case http.MethodGet + "/api/v1/notifications/time-intervals/{name}", http.MethodGet + "/api/v1/notifications/time-intervals": diff --git a/pkg/services/ngalert/api/generated_base_api_provisioning.go b/pkg/services/ngalert/api/generated_base_api_provisioning.go index 453d151101..202d643a11 100644 --- a/pkg/services/ngalert/api/generated_base_api_provisioning.go +++ b/pkg/services/ngalert/api/generated_base_api_provisioning.go @@ -21,6 +21,7 @@ import ( type ProvisioningApi interface { RouteDeleteAlertRule(*contextmodel.ReqContext) response.Response + RouteDeleteAlertRuleGroup(*contextmodel.ReqContext) response.Response RouteDeleteContactpoints(*contextmodel.ReqContext) response.Response RouteDeleteMuteTiming(*contextmodel.ReqContext) response.Response RouteDeleteTemplate(*contextmodel.ReqContext) response.Response @@ -57,6 +58,12 @@ func (f *ProvisioningApiHandler) RouteDeleteAlertRule(ctx *contextmodel.ReqConte uIDParam := web.Params(ctx.Req)[":UID"] return f.handleRouteDeleteAlertRule(ctx, uIDParam) } +func (f *ProvisioningApiHandler) RouteDeleteAlertRuleGroup(ctx *contextmodel.ReqContext) response.Response { + // Parse Path Parameters + folderUIDParam := web.Params(ctx.Req)[":FolderUID"] + groupParam := web.Params(ctx.Req)[":Group"] + return f.handleRouteDeleteAlertRuleGroup(ctx, folderUIDParam, groupParam) +} func (f *ProvisioningApiHandler) RouteDeleteContactpoints(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters uIDParam := web.Params(ctx.Req)[":UID"] @@ -237,6 +244,18 @@ func (api *API) RegisterProvisioningApiEndpoints(srv ProvisioningApi, m *metrics m, ), ) + group.Delete( + toMacaronPath("/api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}"), + requestmeta.SetOwner(requestmeta.TeamAlerting), + requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow), + api.authorize(http.MethodDelete, "/api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}"), + metrics.Instrument( + http.MethodDelete, + "/api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}", + api.Hooks.Wrap(srv.RouteDeleteAlertRuleGroup), + m, + ), + ) group.Delete( toMacaronPath("/api/v1/provisioning/contact-points/{UID}"), requestmeta.SetOwner(requestmeta.TeamAlerting), diff --git a/pkg/services/ngalert/api/provisioning.go b/pkg/services/ngalert/api/provisioning.go index f43e9bb8f7..91d1a6da63 100644 --- a/pkg/services/ngalert/api/provisioning.go +++ b/pkg/services/ngalert/api/provisioning.go @@ -135,3 +135,7 @@ func (f *ProvisioningApiHandler) handleRouteExportMuteTiming(ctx *contextmodel.R func (f *ProvisioningApiHandler) handleRouteExportMuteTimings(ctx *contextmodel.ReqContext) response.Response { return f.svc.RouteGetMuteTimingsExport(ctx) } + +func (f *ProvisioningApiHandler) handleRouteDeleteAlertRuleGroup(ctx *contextmodel.ReqContext, folderUID, group string) response.Response { + return f.svc.RouteDeleteAlertRuleGroup(ctx, folderUID, group) +} diff --git a/pkg/services/ngalert/api/tooling/api.json b/pkg/services/ngalert/api/tooling/api.json index 4024e91414..be0f9033b1 100644 --- a/pkg/services/ngalert/api/tooling/api.json +++ b/pkg/services/ngalert/api/tooling/api.json @@ -5962,6 +5962,44 @@ } }, "/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}": { + "delete": { + "description": "Delete rule group", + "operationId": "RouteDeleteAlertRuleGroup", + "parameters": [ + { + "in": "path", + "name": "FolderUID", + "required": true, + "type": "string" + }, + { + "in": "path", + "name": "Group", + "required": true, + "type": "string" + } + ], + "responses": { + "204": { + "description": " The alert rule group was deleted successfully." + }, + "403": { + "description": "ForbiddenError", + "schema": { + "$ref": "#/definitions/ForbiddenError" + } + }, + "404": { + "description": "NotFound", + "schema": { + "$ref": "#/definitions/NotFound" + } + } + }, + "tags": [ + "provisioning" + ] + }, "get": { "operationId": "RouteGetAlertRuleGroup", "parameters": [ diff --git a/pkg/services/ngalert/api/tooling/definitions/provisioning_alert_rules.go b/pkg/services/ngalert/api/tooling/definitions/provisioning_alert_rules.go index bc1a1d40e1..3fd9f8159c 100644 --- a/pkg/services/ngalert/api/tooling/definitions/provisioning_alert_rules.go +++ b/pkg/services/ngalert/api/tooling/definitions/provisioning_alert_rules.go @@ -168,6 +168,15 @@ type ProvisionedAlertRule struct { // 200: AlertRuleGroup // 404: description: Not found. +// swagger:route DELETE /v1/provisioning/folder/{FolderUID}/rule-groups/{Group} provisioning stable RouteDeleteAlertRuleGroup +// +// Delete rule group +// +// Responses: +// 204: description: The alert rule group was deleted successfully. +// 403: ForbiddenError +// 404: NotFound + // swagger:route GET /v1/provisioning/folder/{FolderUID}/rule-groups/{Group}/export provisioning stable RouteGetAlertRuleGroupExport // // Export an alert rule group in provisioning file format. @@ -192,13 +201,13 @@ type ProvisionedAlertRule struct { // 200: AlertRuleGroup // 400: ValidationError -// swagger:parameters RouteGetAlertRuleGroup RoutePutAlertRuleGroup RouteGetAlertRuleGroupExport +// swagger:parameters RouteGetAlertRuleGroup RoutePutAlertRuleGroup RouteGetAlertRuleGroupExport RouteDeleteAlertRuleGroup type FolderUIDPathParam struct { // in:path FolderUID string `json:"FolderUID"` } -// swagger:parameters RouteGetAlertRuleGroup RoutePutAlertRuleGroup RouteGetAlertRuleGroupExport +// swagger:parameters RouteGetAlertRuleGroup RoutePutAlertRuleGroup RouteGetAlertRuleGroupExport RouteDeleteAlertRuleGroup type RuleGroupPathParam struct { // in:path Group string `json:"Group"` diff --git a/pkg/services/ngalert/api/tooling/post.json b/pkg/services/ngalert/api/tooling/post.json index 115965f0d9..44b31d7c65 100644 --- a/pkg/services/ngalert/api/tooling/post.json +++ b/pkg/services/ngalert/api/tooling/post.json @@ -7721,6 +7721,44 @@ } }, "/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}": { + "delete": { + "description": "Delete rule group", + "operationId": "RouteDeleteAlertRuleGroup", + "parameters": [ + { + "in": "path", + "name": "FolderUID", + "required": true, + "type": "string" + }, + { + "in": "path", + "name": "Group", + "required": true, + "type": "string" + } + ], + "responses": { + "204": { + "description": " The alert rule group was deleted successfully." + }, + "403": { + "description": "ForbiddenError", + "schema": { + "$ref": "#/definitions/ForbiddenError" + } + }, + "404": { + "description": "NotFound", + "schema": { + "$ref": "#/definitions/NotFound" + } + } + }, + "tags": [ + "provisioning" + ] + }, "get": { "operationId": "RouteGetAlertRuleGroup", "parameters": [ diff --git a/pkg/services/ngalert/api/tooling/spec.json b/pkg/services/ngalert/api/tooling/spec.json index 3177242b6b..90aae29c3e 100644 --- a/pkg/services/ngalert/api/tooling/spec.json +++ b/pkg/services/ngalert/api/tooling/spec.json @@ -2690,6 +2690,45 @@ } } } + }, + "delete": { + "description": "Delete rule group", + "tags": [ + "provisioning", + "stable" + ], + "operationId": "RouteDeleteAlertRuleGroup", + "parameters": [ + { + "type": "string", + "name": "FolderUID", + "in": "path", + "required": true + }, + { + "type": "string", + "name": "Group", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": " The alert rule group was deleted successfully." + }, + "403": { + "description": "ForbiddenError", + "schema": { + "$ref": "#/definitions/ForbiddenError" + } + }, + "404": { + "description": "NotFound", + "schema": { + "$ref": "#/definitions/NotFound" + } + } + } } }, "/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}/export": { diff --git a/pkg/services/ngalert/provisioning/alert_rules.go b/pkg/services/ngalert/provisioning/alert_rules.go index bf7e93b3a1..5e2a95277f 100644 --- a/pkg/services/ngalert/provisioning/alert_rules.go +++ b/pkg/services/ngalert/provisioning/alert_rules.go @@ -276,6 +276,38 @@ func (service *AlertRuleService) ReplaceRuleGroup(ctx context.Context, orgID int return service.persistDelta(ctx, orgID, delta, userID, provenance) } +func (service *AlertRuleService) DeleteRuleGroup(ctx context.Context, orgID int64, namespaceUID, group string, provenance models.Provenance) error { + // List all rules in the group. + q := models.ListAlertRulesQuery{ + OrgID: orgID, + NamespaceUIDs: []string{namespaceUID}, + RuleGroup: group, + } + ruleList, err := service.ruleStore.ListAlertRules(ctx, &q) + if err != nil { + return err + } + if len(ruleList) == 0 { + return store.ErrAlertRuleGroupNotFound + } + + // Check provenance for all rules in the group. Fail to delete if any deletions aren't allowed. + for _, rule := range ruleList { + storedProvenance, err := service.provenanceStore.GetProvenance(ctx, rule, rule.OrgID) + if err != nil { + return err + } + if storedProvenance != provenance && storedProvenance != models.ProvenanceNone { + return fmt.Errorf("cannot delete with provided provenance '%s', needs '%s'", provenance, storedProvenance) + } + } + + // Delete all rules. + return service.xact.InTransaction(ctx, func(ctx context.Context) error { + return service.deleteRules(ctx, orgID, ruleList...) + }) +} + func (service *AlertRuleService) calcDelta(ctx context.Context, orgID int64, group models.AlertRuleGroup) (*store.GroupDelta, error) { // If the provided request did not provide the rules list at all, treat it as though it does not wish to change rules. // This is done for backwards compatibility. Requests which specify only the interval must update only the interval. diff --git a/public/api-merged.json b/public/api-merged.json index c528864407..cc15b13c83 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -10904,6 +10904,44 @@ } } } + }, + "delete": { + "description": "Delete rule group", + "tags": [ + "provisioning" + ], + "operationId": "RouteDeleteAlertRuleGroup", + "parameters": [ + { + "type": "string", + "name": "FolderUID", + "in": "path", + "required": true + }, + { + "type": "string", + "name": "Group", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": " The alert rule group was deleted successfully." + }, + "403": { + "description": "ForbiddenError", + "schema": { + "$ref": "#/definitions/ForbiddenError" + } + }, + "404": { + "description": "NotFound", + "schema": { + "$ref": "#/definitions/NotFound" + } + } + } } }, "/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}/export": { diff --git a/public/openapi3.json b/public/openapi3.json index 718c294c00..0f05866592 100644 --- a/public/openapi3.json +++ b/public/openapi3.json @@ -24656,6 +24656,56 @@ } }, "/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}": { + "delete": { + "description": "Delete rule group", + "operationId": "RouteDeleteAlertRuleGroup", + "parameters": [ + { + "in": "path", + "name": "FolderUID", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "Group", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": " The alert rule group was deleted successfully." + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenError" + } + } + }, + "description": "ForbiddenError" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFound" + } + } + }, + "description": "NotFound" + } + }, + "tags": [ + "provisioning" + ] + }, "get": { "operationId": "RouteGetAlertRuleGroup", "parameters": [ From ba4470dd7d39b3561ca25fda6f716fe5853ee5dd Mon Sep 17 00:00:00 2001 From: Marie Cruz Date: Wed, 28 Feb 2024 15:23:37 +0000 Subject: [PATCH 0270/1406] docs: link annotation queries video to documentation (#83586) --- .../build-dashboards/annotate-visualizations/index.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/sources/dashboards/build-dashboards/annotate-visualizations/index.md b/docs/sources/dashboards/build-dashboards/annotate-visualizations/index.md index 3481be9f6d..d9711ffffa 100644 --- a/docs/sources/dashboards/build-dashboards/annotate-visualizations/index.md +++ b/docs/sources/dashboards/build-dashboards/annotate-visualizations/index.md @@ -86,6 +86,10 @@ Alternatively, to add an annotation, press Ctrl/Cmd and click the panel, and the In the dashboard settings, under **Annotations**, you can add new queries to fetch annotations using any data source, including the built-in data annotation data source. Annotation queries return events that can be visualized as event markers in graphs across the dashboard. +Check out the video below for a quick tutorial. + +{{< youtube id="2istdJpPj2Y" >}} + ### Add new annotation queries To add a new annotation query to a dashboard, take the following steps: From 07128cfec1d48ce989acca0db56cdae803d47926 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Wed, 28 Feb 2024 07:38:21 -0800 Subject: [PATCH 0271/1406] Chore: Restore ArrayVector (#83608) --- .betterer.results | 5 ++ .../src/vector/ArrayVector.test.ts | 45 ++++++++++++++++++ .../grafana-data/src/vector/ArrayVector.ts | 46 +++++++++++++++++++ 3 files changed, 96 insertions(+) create mode 100644 packages/grafana-data/src/vector/ArrayVector.test.ts create mode 100644 packages/grafana-data/src/vector/ArrayVector.ts diff --git a/.betterer.results b/.betterer.results index 83032798fe..2e2644ad9a 100644 --- a/.betterer.results +++ b/.betterer.results @@ -439,6 +439,11 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "3"], [0, 0, 0, "Unexpected any. Specify a different type.", "4"] ], + "packages/grafana-data/src/vector/ArrayVector.ts:5381": [ + [0, 0, 0, "Unexpected any. Specify a different type.", "0"], + [0, 0, 0, "Unexpected any. Specify a different type.", "1"], + [0, 0, 0, "Unexpected any. Specify a different type.", "2"] + ], "packages/grafana-data/src/vector/CircularVector.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], diff --git a/packages/grafana-data/src/vector/ArrayVector.test.ts b/packages/grafana-data/src/vector/ArrayVector.test.ts new file mode 100644 index 0000000000..3d9ae35fc5 --- /dev/null +++ b/packages/grafana-data/src/vector/ArrayVector.test.ts @@ -0,0 +1,45 @@ +import { Field, FieldType } from '../types'; + +import { ArrayVector } from './ArrayVector'; + +describe('ArrayVector', () => { + beforeEach(() => { + jest.spyOn(console, 'warn').mockImplementation(); + }); + + it('should init 150k with 65k Array.push() chonking', () => { + const arr = Array.from({ length: 150e3 }, (v, i) => i); + const av = new ArrayVector(arr); + + expect(av.toArray()).toEqual(arr); + }); + + it('should support add and push', () => { + const av = new ArrayVector(); + av.add(1); + av.push(2); + av.push(3, 4); + + expect(av.toArray()).toEqual([1, 2, 3, 4]); + }); + + it('typescript should not re-define the ArrayVector based on input to the constructor', () => { + const field: Field = { + name: 'test', + config: {}, + type: FieldType.number, + values: new ArrayVector(), // this defaults to `new ArrayVector()` + }; + expect(field).toBeDefined(); + + // Before collapsing Vector, ReadWriteVector, and MutableVector these all worked fine + field.values = new ArrayVector(); + field.values = new ArrayVector(undefined); + field.values = new ArrayVector([1, 2, 3]); + field.values = new ArrayVector([]); + field.values = new ArrayVector([1, undefined]); + field.values = new ArrayVector([null]); + field.values = new ArrayVector(['a', 'b', 'c']); + expect(field.values.length).toBe(3); + }); +}); diff --git a/packages/grafana-data/src/vector/ArrayVector.ts b/packages/grafana-data/src/vector/ArrayVector.ts new file mode 100644 index 0000000000..01b615162b --- /dev/null +++ b/packages/grafana-data/src/vector/ArrayVector.ts @@ -0,0 +1,46 @@ +const notice = 'ArrayVector is deprecated and will be removed in Grafana 11. Please use plain arrays for field.values.'; +let notified = false; + +/** + * @public + * + * @deprecated use a simple Array + */ +export class ArrayVector extends Array { + get buffer() { + return this; + } + + set buffer(values: any[]) { + this.length = 0; + + const len = values?.length; + + if (len) { + let chonkSize = 65e3; + let numChonks = Math.ceil(len / chonkSize); + + for (let chonkIdx = 0; chonkIdx < numChonks; chonkIdx++) { + this.push.apply(this, values.slice(chonkIdx * chonkSize, (chonkIdx + 1) * chonkSize)); + } + } + } + + /** + * This any type is here to make the change type changes in v10 non breaking for plugins. + * Before you could technically assign field.values any typed ArrayVector no matter what the Field T type was. + */ + constructor(buffer?: any[]) { + super(); + this.buffer = buffer ?? []; + + if (!notified) { + console.warn(notice); + notified = true; + } + } + + toJSON(): T[] { + return [...this]; // copy to avoid circular reference (only for jest) + } +} From 183a42b7f6ce44853ea1b48733e8b6a54b0f0a0f Mon Sep 17 00:00:00 2001 From: Konrad Lalik Date: Wed, 28 Feb 2024 16:52:56 +0100 Subject: [PATCH 0272/1406] Alerting: Improve alert rule and search interaction tracking (#83217) * Fix alert rule interaction tracking * Add search component interaction tracking * Add fine-grained search input analytics --- .../features/alerting/unified/Analytics.ts | 67 ++++++++++++++----- .../alert-rule-form/AlertRuleForm.tsx | 28 +++++--- .../unified/components/rules/RulesFilter.tsx | 23 +++++-- 3 files changed, 89 insertions(+), 29 deletions(-) diff --git a/public/app/features/alerting/unified/Analytics.ts b/public/app/features/alerting/unified/Analytics.ts index 012bbd988e..049243309a 100644 --- a/public/app/features/alerting/unified/Analytics.ts +++ b/public/app/features/alerting/unified/Analytics.ts @@ -1,3 +1,5 @@ +import { isEmpty } from 'lodash'; + import { dateTime } from '@grafana/data'; import { createMonitoringLogger, getBackendSrv } from '@grafana/runtime'; import { config, reportInteraction } from '@grafana/runtime/src'; @@ -6,6 +8,9 @@ import { contextSrv } from 'app/core/core'; import { RuleNamespace } from '../../../types/unified-alerting'; import { RulerRulesConfigDTO } from '../../../types/unified-alerting-dto'; +import { getSearchFilterFromQuery, RulesFilter } from './search/rulesSearchParser'; +import { RuleFormType } from './types/rule-form'; + export const USER_CREATION_MIN_DAYS = 7; export const LogMessages = { @@ -150,27 +155,17 @@ export const trackRuleListNavigation = async ( reportInteraction('grafana_alerting_navigation', props); }; -export const trackNewAlerRuleFormSaved = async (props: AlertRuleTrackingProps) => { - const isNew = await isNewUser(); - if (isNew) { - return; - } +export const trackAlertRuleFormSaved = (props: { formAction: 'create' | 'update'; ruleType?: RuleFormType }) => { reportInteraction('grafana_alerting_rule_creation', props); }; -export const trackNewAlerRuleFormCancelled = async (props: AlertRuleTrackingProps) => { - const isNew = await isNewUser(); - if (isNew) { - return; - } +export const trackAlertRuleFormCancelled = (props: { formAction: 'create' | 'update' }) => { reportInteraction('grafana_alerting_rule_aborted', props); }; -export const trackNewAlerRuleFormError = async (props: AlertRuleTrackingProps & { error: string }) => { - const isNew = await isNewUser(); - if (isNew) { - return; - } +export const trackAlertRuleFormError = ( + props: AlertRuleTrackingProps & { error: string; formAction: 'create' | 'update' } +) => { reportInteraction('grafana_alerting_rule_form_error', props); }; @@ -183,6 +178,48 @@ export const trackInsightsFeedback = async (props: { useful: boolean; panel: str reportInteraction('grafana_alerting_insights', { ...defaults, ...props }); }; +interface RulesSearchInteractionPayload { + filter: string; + triggeredBy: 'typing' | 'component'; +} + +function trackRulesSearchInteraction(payload: RulesSearchInteractionPayload) { + reportInteraction('grafana_alerting_rules_search', { ...payload }); +} + +export function trackRulesSearchInputInteraction({ oldQuery, newQuery }: { oldQuery: string; newQuery: string }) { + try { + const oldFilter = getSearchFilterFromQuery(oldQuery); + const newFilter = getSearchFilterFromQuery(newQuery); + + const oldFilterTerms = extractFilterKeys(oldFilter); + const newFilterTerms = extractFilterKeys(newFilter); + + const newTerms = newFilterTerms.filter((term) => !oldFilterTerms.includes(term)); + newTerms.forEach((term) => { + trackRulesSearchInteraction({ filter: term, triggeredBy: 'typing' }); + }); + } catch (e: unknown) { + if (e instanceof Error) { + logError(e); + } + } +} + +function extractFilterKeys(filter: RulesFilter) { + return Object.entries(filter) + .filter(([_, value]) => !isEmpty(value)) + .map(([key]) => key); +} + +export function trackRulesSearchComponentInteraction(filter: keyof RulesFilter) { + trackRulesSearchInteraction({ filter, triggeredBy: 'component' }); +} + +export function trackRulesListViewChange(payload: { view: string }) { + reportInteraction('grafana_alerting_rules_list_mode', { ...payload }); +} + export type AlertRuleTrackingProps = { user_id: number; grafana_version?: string; diff --git a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx index e1f56127f2..645320c32e 100644 --- a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx @@ -14,7 +14,13 @@ import { useQueryParams } from 'app/core/hooks/useQueryParams'; import { useDispatch } from 'app/types'; import { RuleWithLocation } from 'app/types/unified-alerting'; -import { LogMessages, logInfo, trackNewAlerRuleFormError } from '../../../Analytics'; +import { + LogMessages, + logInfo, + trackAlertRuleFormError, + trackAlertRuleFormCancelled, + trackAlertRuleFormSaved, +} from '../../../Analytics'; import { useUnifiedAlertingSelector } from '../../../hooks/useUnifiedAlertingSelector'; import { deleteRuleAction, saveRuleFormAction } from '../../../state/actions'; import { RuleFormType, RuleFormValues } from '../../../types/rule-form'; @@ -109,6 +115,9 @@ export const AlertRuleForm = ({ existing, prefill }: Props) => { notifyApp.error(conditionErrorMsg); return; } + + trackAlertRuleFormSaved({ formAction: existing ? 'update' : 'create', ruleType: values.type }); + // when creating a new rule, we save the manual routing setting in local storage if (!existing) { if (values.manualRouting) { @@ -154,20 +163,21 @@ export const AlertRuleForm = ({ existing, prefill }: Props) => { }; const onInvalid: SubmitErrorHandler = (errors): void => { - if (!existing) { - trackNewAlerRuleFormError({ - grafana_version: config.buildInfo.version, - org_id: contextSrv.user.orgId, - user_id: contextSrv.user.id, - error: Object.keys(errors).toString(), - }); - } + trackAlertRuleFormError({ + grafana_version: config.buildInfo.version, + org_id: contextSrv.user.orgId, + user_id: contextSrv.user.id, + error: Object.keys(errors).toString(), + formAction: existing ? 'update' : 'create', + }); notifyApp.error('There are errors in the form. Please correct them and try again!'); }; const cancelRuleCreation = () => { logInfo(LogMessages.cancelSavingAlertRule); + trackAlertRuleFormCancelled({ formAction: existing ? 'update' : 'create' }); }; + const evaluateEveryInForm = watch('evaluateEvery'); useEffect(() => setEvaluateEvery(evaluateEveryInForm), [evaluateEveryInForm]); diff --git a/public/app/features/alerting/unified/components/rules/RulesFilter.tsx b/public/app/features/alerting/unified/components/rules/RulesFilter.tsx index d462cb653f..2f77f19184 100644 --- a/public/app/features/alerting/unified/components/rules/RulesFilter.tsx +++ b/public/app/features/alerting/unified/components/rules/RulesFilter.tsx @@ -8,7 +8,13 @@ import { DashboardPicker } from 'app/core/components/Select/DashboardPicker'; import { useQueryParams } from 'app/core/hooks/useQueryParams'; import { PromAlertingRuleState, PromRuleType } from 'app/types/unified-alerting-dto'; -import { logInfo, LogMessages } from '../../Analytics'; +import { + logInfo, + LogMessages, + trackRulesListViewChange, + trackRulesSearchComponentInteraction, + trackRulesSearchInputInteraction, +} from '../../Analytics'; import { useRulesFilter } from '../../hooks/useFilteredRules'; import { RuleHealth } from '../../search/rulesSearchParser'; import { alertStateToReadable } from '../../utils/rules'; @@ -90,10 +96,12 @@ const RulesFilter = ({ onFilterCleared = () => undefined }: RulesFilerProps) => }); setFilterKey((key) => key + 1); + trackRulesSearchComponentInteraction('dataSourceNames'); }; const handleDashboardChange = (dashboardUid: string | undefined) => { updateFilters({ ...filterState, dashboardUid }); + trackRulesSearchComponentInteraction('dashboardUid'); }; const clearDataSource = () => { @@ -104,18 +112,17 @@ const RulesFilter = ({ onFilterCleared = () => undefined }: RulesFilerProps) => const handleAlertStateChange = (value: PromAlertingRuleState) => { logInfo(LogMessages.clickingAlertStateFilters); updateFilters({ ...filterState, ruleState: value }); - }; - - const handleViewChange = (view: string) => { - setQueryParams({ view }); + trackRulesSearchComponentInteraction('ruleState'); }; const handleRuleTypeChange = (ruleType: PromRuleType) => { updateFilters({ ...filterState, ruleType }); + trackRulesSearchComponentInteraction('ruleType'); }; const handleRuleHealthChange = (ruleHealth: RuleHealth) => { updateFilters({ ...filterState, ruleHealth }); + trackRulesSearchComponentInteraction('ruleHealth'); }; const handleClearFiltersClick = () => { @@ -125,6 +132,11 @@ const RulesFilter = ({ onFilterCleared = () => undefined }: RulesFilerProps) => setTimeout(() => setFilterKey(filterKey + 1), 100); }; + const handleViewChange = (view: string) => { + setQueryParams({ view }); + trackRulesListViewChange({ view }); + }; + const searchIcon = ; return (
@@ -211,6 +223,7 @@ const RulesFilter = ({ onFilterCleared = () => undefined }: RulesFilerProps) => onSubmit={handleSubmit((data) => { setSearchQuery(data.searchQuery); searchQueryRef.current?.blur(); + trackRulesSearchInputInteraction({ oldQuery: searchQuery, newQuery: data.searchQuery }); })} > Date: Wed, 28 Feb 2024 10:02:41 -0600 Subject: [PATCH 0273/1406] Docs: updated codeowners file (#83546) * removed myself from data sources, added to admin (IAM) * Remove accidental changes Signed-off-by: Jack Baldry * Rewrite Technical documentation codeowners for readability and consistency Now it is consistent with the comments at the head of the file, at least for this section of the repo. Signed-off-by: Jack Baldry --------- Signed-off-by: Jack Baldry Co-authored-by: Jack Baldry --- .github/CODEOWNERS | 44 ++++++++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 42cb5d9276..b18eeac035 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -31,28 +31,32 @@ /contribute/UPGRADING_DEPENDENCIES.md @grafana/docs-grafana /devenv/README.md @grafana/docs-grafana -# Technical documentation +# START Technical documentation # `make docs` procedure and related workflows are owned @grafana/docs-tooling. Slack #docs. -# Documentation sources might have different owners. -/docs/ @grafana/docs-tooling -/docs/.codespellignore @grafana/docs-tooling -/docs/sources/ @Eve832 -/docs/sources/administration/ @jdbaldry -/docs/sources/alerting/ @brendamuir -/docs/sources/dashboards/ @imatwawana -/docs/sources/datasources/ @lwandz13 -/docs/sources/explore/ @grafana/explore-squad -/docs/sources/fundamentals @chri2547 -/docs/sources/getting-started/ @chri2547 -/docs/sources/introduction/ @chri2547 -/docs/sources/old-alerting/ @brendamuir -/docs/sources/panels-visualizations/ @imatwawana +/docs/ @grafana/docs-tooling + +/docs/.codespellignore @grafana/docs-tooling +/docs/sources/ @Eve832 + +/docs/sources/administration/ @jdbaldry @lwandz13 +/docs/sources/alerting/ @brendamuir +/docs/sources/dashboards/ @imatwawana +/docs/sources/datasources/ @jdbaldry +/docs/sources/explore/ @grafana/explore-squad +/docs/sources/fundamentals @chri2547 +/docs/sources/getting-started/ @chri2547 +/docs/sources/introduction/ @chri2547 +/docs/sources/old-alerting/ @brendamuir +/docs/sources/panels-visualizations/ @imatwawana +/docs/sources/release-notes/ @Eve832 @GrafanaWriter +/docs/sources/setup-grafana/ @chri2547 +/docs/sources/upgrade-guide/ @imatwawana +/docs/sources/whatsnew/ @imatwawana + +/docs/sources/developers/plugins/ @Eve832 @josmperez @grafana/plugins-platform-frontend @grafana/plugins-platform-backend + /docs/sources/panels-visualizations/query-transform-data/transform-data/index.md @imatwawana @baldm0mma -/docs/sources/release-notes/ @Eve832 @GrafanaWriter -/docs/sources/setup-grafana/ @chri2547 -/docs/sources/upgrade-guide/ @imatwawana -/docs/sources/whatsnew/ @imatwawana -/docs/sources/developers/plugins/ @Eve832 @josmperez @grafana/plugins-platform-frontend @grafana/plugins-platform-backend +# END Technical documentation # Backend code /go.mod @grafana/backend-platform From 528ce96118a64f7df26601b1ed8177b6931e1718 Mon Sep 17 00:00:00 2001 From: Victor Marin <36818606+mdvictor@users.noreply.github.com> Date: Wed, 28 Feb 2024 18:25:22 +0200 Subject: [PATCH 0274/1406] Scenes: Fix lib panel and lib widget placement in collapsed/uncollapsed rows (#83516) * wip * tests + refactor ad panel func * Add row functionality * update row state only when there are children * Add new row + copy paste panels * Add library panel functionality * tests * PR mods * reafctor + tests * reafctor * fix test * refactor * fix bug on cancelling lib widget * dashboard now saves with lib panel widget * add lib panels widget works in rows as well * split add lib panel func to another PR * Add library panel functionality * Fix lib panel and lib widget placement in collapsed/uncollapsed rows * refactor * take panelKey into account when getting next panel id in dashboard * fix tests --- .../transformSaveModelToScene.test.ts | 101 +++++++++++++++--- .../transformSaveModelToScene.ts | 36 ++++++- 2 files changed, 122 insertions(+), 15 deletions(-) diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts index 908dbbcca4..212dbfbb7b 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts @@ -186,12 +186,26 @@ describe('transformSaveModelToScene', () => { gridPos: { x: 1, y: 0, w: 12, h: 8 }, }) as Panel; + const widgetLibPanel = { + title: 'Widget Panel', + type: 'add-library-panel', + }; + + const libPanel = createPanelSaveModel({ + title: 'Library Panel', + gridPos: { x: 0, y: 0, w: 12, h: 8 }, + libraryPanel: { + uid: '123', + name: 'My Panel', + }, + }); + const row = createPanelSaveModel({ title: 'test', type: 'row', gridPos: { x: 0, y: 0, w: 12, h: 1 }, collapsed: true, - panels: [panel], + panels: [panel, widgetLibPanel, libPanel], }) as unknown as RowPanel; const dashboard = { @@ -210,8 +224,12 @@ describe('transformSaveModelToScene', () => { expect(rowScene.state.title).toEqual(row.title); expect(rowScene.state.y).toEqual(row.gridPos!.y); expect(rowScene.state.isCollapsed).toEqual(row.collapsed); - expect(rowScene.state.children).toHaveLength(1); + expect(rowScene.state.children).toHaveLength(3); expect(rowScene.state.children[0]).toBeInstanceOf(SceneGridItem); + expect(rowScene.state.children[1]).toBeInstanceOf(SceneGridItem); + expect(rowScene.state.children[2]).toBeInstanceOf(SceneGridItem); + expect((rowScene.state.children[1] as SceneGridItem).state.body!).toBeInstanceOf(AddLibraryPanelWidget); + expect((rowScene.state.children[2] as SceneGridItem).state.body!).toBeInstanceOf(LibraryVizPanel); }); it('should create panels within expanded row', () => { @@ -224,6 +242,24 @@ describe('transformSaveModelToScene', () => { y: 0, }, }); + const widgetLibPanelOutOfRow = { + title: 'Widget Panel', + type: 'add-library-panel', + gridPos: { + h: 8, + w: 12, + x: 12, + y: 0, + }, + }; + const libPanelOutOfRow = createPanelSaveModel({ + title: 'Library Panel', + gridPos: { x: 0, y: 8, w: 12, h: 8 }, + libraryPanel: { + uid: '123', + name: 'My Panel', + }, + }); const rowWithPanel = createPanelSaveModel({ title: 'Row with panel', type: 'row', @@ -233,7 +269,7 @@ describe('transformSaveModelToScene', () => { h: 1, w: 24, x: 0, - y: 8, + y: 16, }, // This panels array is not used if the row is not collapsed panels: [], @@ -243,17 +279,35 @@ describe('transformSaveModelToScene', () => { h: 8, w: 12, x: 0, - y: 9, + y: 17, }, title: 'In row 1', }); + const widgetLibPanelInRow = { + title: 'Widget Panel', + type: 'add-library-panel', + gridPos: { + h: 8, + w: 12, + x: 12, + y: 17, + }, + }; + const libPanelInRow = createPanelSaveModel({ + title: 'Library Panel', + gridPos: { x: 0, y: 25, w: 12, h: 8 }, + libraryPanel: { + uid: '123', + name: 'My Panel', + }, + }); const emptyRow = createPanelSaveModel({ collapsed: false, gridPos: { h: 1, w: 24, x: 0, - y: 17, + y: 26, }, // This panels array is not used if the row is not collapsed panels: [], @@ -262,7 +316,16 @@ describe('transformSaveModelToScene', () => { }); const dashboard = { ...defaultDashboard, - panels: [panelOutOfRow, rowWithPanel, panelInRow, emptyRow], + panels: [ + panelOutOfRow, + widgetLibPanelOutOfRow, + libPanelOutOfRow, + rowWithPanel, + panelInRow, + widgetLibPanelInRow, + libPanelInRow, + emptyRow, + ], }; const oldModel = new DashboardModel(dashboard); @@ -270,25 +333,37 @@ describe('transformSaveModelToScene', () => { const scene = createDashboardSceneFromDashboardModel(oldModel); const body = scene.state.body as SceneGridLayout; - expect(body.state.children).toHaveLength(3); + expect(body.state.children).toHaveLength(5); expect(body).toBeInstanceOf(SceneGridLayout); // Panel out of row expect(body.state.children[0]).toBeInstanceOf(SceneGridItem); const panelOutOfRowVizPanel = body.state.children[0] as SceneGridItem; expect((panelOutOfRowVizPanel.state.body as VizPanel)?.state.title).toBe(panelOutOfRow.title); - // Row with panel - expect(body.state.children[1]).toBeInstanceOf(SceneGridRow); - const rowWithPanelsScene = body.state.children[1] as SceneGridRow; + // widget lib panel out of row + expect(body.state.children[1]).toBeInstanceOf(SceneGridItem); + const panelOutOfRowWidget = body.state.children[1] as SceneGridItem; + expect(panelOutOfRowWidget.state.body!).toBeInstanceOf(AddLibraryPanelWidget); + // lib panel out of row + expect(body.state.children[2]).toBeInstanceOf(SceneGridItem); + const panelOutOfRowLibVizPanel = body.state.children[2] as SceneGridItem; + expect(panelOutOfRowLibVizPanel.state.body!).toBeInstanceOf(LibraryVizPanel); + // Row with panels + expect(body.state.children[3]).toBeInstanceOf(SceneGridRow); + const rowWithPanelsScene = body.state.children[3] as SceneGridRow; expect(rowWithPanelsScene.state.title).toBe(rowWithPanel.title); expect(rowWithPanelsScene.state.key).toBe('panel-10'); - expect(rowWithPanelsScene.state.children).toHaveLength(1); + expect(rowWithPanelsScene.state.children).toHaveLength(3); + const widget = rowWithPanelsScene.state.children[1] as SceneGridItem; + expect(widget.state.body!).toBeInstanceOf(AddLibraryPanelWidget); + const libPanel = rowWithPanelsScene.state.children[2] as SceneGridItem; + expect(libPanel.state.body!).toBeInstanceOf(LibraryVizPanel); // Panel within row expect(rowWithPanelsScene.state.children[0]).toBeInstanceOf(SceneGridItem); const panelInRowVizPanel = rowWithPanelsScene.state.children[0] as SceneGridItem; expect((panelInRowVizPanel.state.body as VizPanel).state.title).toBe(panelInRow.title); // Empty row - expect(body.state.children[2]).toBeInstanceOf(SceneGridRow); - const emptyRowScene = body.state.children[2] as SceneGridRow; + expect(body.state.children[4]).toBeInstanceOf(SceneGridRow); + const emptyRowScene = body.state.children[4] as SceneGridRow; expect(emptyRowScene.state.title).toBe(emptyRow.title); expect(emptyRowScene.state.children).toHaveLength(0); }); diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts index 1957b83104..2ecd10085d 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts @@ -114,12 +114,25 @@ export function createSceneObjectsForPanels(oldPanels: PanelModel[]): SceneGridI } else if (panel.type === 'add-library-panel') { const gridItem = buildGridItemForLibraryPanelWidget(panel); - if (gridItem) { + if (!gridItem) { + continue; + } + + if (currentRow) { + currentRowPanels.push(gridItem); + } else { panels.push(gridItem); } } else if (panel.libraryPanel?.uid && !('model' in panel.libraryPanel)) { const gridItem = buildGridItemForLibPanel(panel); - if (gridItem) { + + if (!gridItem) { + continue; + } + + if (currentRow) { + currentRowPanels.push(gridItem); + } else { panels.push(gridItem); } } else { @@ -155,6 +168,25 @@ function createRowFromPanelModel(row: PanelModel, content: SceneGridItemLike[]): if (!(saveModel instanceof PanelModel)) { saveModel = new PanelModel(saveModel); } + + if (saveModel.type === 'add-library-panel') { + const gridItem = buildGridItemForLibraryPanelWidget(saveModel); + + if (!gridItem) { + throw new Error('Failed to build grid item for library panel widget'); + } + + return gridItem; + } else if (saveModel.libraryPanel?.uid && !('model' in saveModel.libraryPanel)) { + const gridItem = buildGridItemForLibPanel(saveModel); + + if (!gridItem) { + throw new Error('Failed to build grid item for library panel'); + } + + return gridItem; + } + return buildGridItemForPanel(saveModel); }); } From 65174311659277e6c10730f16f61cb04c2df1db8 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Wed, 28 Feb 2024 08:36:53 -0800 Subject: [PATCH 0275/1406] Chore: Remove components from the graveyard folder in grafana/ui (#83545) --- .betterer.results | 28 - .github/CODEOWNERS | 2 - .../src/components/Menu/Menu.story.tsx | 25 - packages/grafana-ui/src/components/index.ts | 17 - .../src/components/uPlot/utils.test.ts | 4 +- .../src/graveyard/Graph/Graph.test.tsx | 185 ----- .../grafana-ui/src/graveyard/Graph/Graph.tsx | 400 ----------- .../graveyard/Graph/GraphSeriesToggler.tsx | 89 --- .../Graph/GraphTooltip/GraphTooltip.tsx | 41 -- .../MultiModeGraphTooltip.test.tsx | 106 --- .../GraphTooltip/MultiModeGraphTooltip.tsx | 49 -- .../GraphTooltip/SingleModeGraphTooltip.tsx | 48 -- .../src/graveyard/Graph/GraphTooltip/types.ts | 16 - .../Graph/GraphWithLegend.internal.story.tsx | 141 ---- .../src/graveyard/Graph/GraphWithLegend.tsx | 132 ---- .../grafana-ui/src/graveyard/Graph/types.ts | 9 - .../src/graveyard/Graph/utils.test.ts | 233 ------ .../grafana-ui/src/graveyard/Graph/utils.ts | 147 ---- .../src/graveyard/GraphNG/GraphNG.tsx | 275 ------- .../GraphNG/__snapshots__/utils.test.ts.snap | 245 ------- .../grafana-ui/src/graveyard/GraphNG/hooks.ts | 41 -- .../src/graveyard/GraphNG/utils.test.ts | 522 -------------- packages/grafana-ui/src/graveyard/README.md | 3 + .../src/graveyard/TimeSeries/TimeSeries.tsx | 63 -- .../src/graveyard/TimeSeries/utils.test.ts | 274 ------- .../src/graveyard/TimeSeries/utils.ts | 668 ------------------ public/app/angular/angular_wrappers.ts | 3 +- .../legacy_graph_panel}/GraphContextMenu.tsx | 28 +- .../timeseries/plugins/ContextMenuPlugin.tsx | 11 +- 29 files changed, 28 insertions(+), 3777 deletions(-) delete mode 100644 packages/grafana-ui/src/graveyard/Graph/Graph.test.tsx delete mode 100644 packages/grafana-ui/src/graveyard/Graph/Graph.tsx delete mode 100644 packages/grafana-ui/src/graveyard/Graph/GraphSeriesToggler.tsx delete mode 100644 packages/grafana-ui/src/graveyard/Graph/GraphTooltip/GraphTooltip.tsx delete mode 100644 packages/grafana-ui/src/graveyard/Graph/GraphTooltip/MultiModeGraphTooltip.test.tsx delete mode 100644 packages/grafana-ui/src/graveyard/Graph/GraphTooltip/MultiModeGraphTooltip.tsx delete mode 100644 packages/grafana-ui/src/graveyard/Graph/GraphTooltip/SingleModeGraphTooltip.tsx delete mode 100644 packages/grafana-ui/src/graveyard/Graph/GraphTooltip/types.ts delete mode 100644 packages/grafana-ui/src/graveyard/Graph/GraphWithLegend.internal.story.tsx delete mode 100644 packages/grafana-ui/src/graveyard/Graph/GraphWithLegend.tsx delete mode 100644 packages/grafana-ui/src/graveyard/Graph/types.ts delete mode 100644 packages/grafana-ui/src/graveyard/Graph/utils.test.ts delete mode 100644 packages/grafana-ui/src/graveyard/Graph/utils.ts delete mode 100644 packages/grafana-ui/src/graveyard/GraphNG/GraphNG.tsx delete mode 100644 packages/grafana-ui/src/graveyard/GraphNG/__snapshots__/utils.test.ts.snap delete mode 100644 packages/grafana-ui/src/graveyard/GraphNG/hooks.ts delete mode 100644 packages/grafana-ui/src/graveyard/GraphNG/utils.test.ts delete mode 100644 packages/grafana-ui/src/graveyard/TimeSeries/TimeSeries.tsx delete mode 100644 packages/grafana-ui/src/graveyard/TimeSeries/utils.test.ts delete mode 100644 packages/grafana-ui/src/graveyard/TimeSeries/utils.ts rename {packages/grafana-ui/src/graveyard/Graph => public/app/angular/components/legacy_graph_panel}/GraphContextMenu.tsx (86%) diff --git a/.betterer.results b/.betterer.results index 2e2644ad9a..c303a90fc4 100644 --- a/.betterer.results +++ b/.betterer.results @@ -1024,29 +1024,6 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Do not use any type assertions.", "1"] ], - "packages/grafana-ui/src/graveyard/Graph/GraphContextMenu.tsx:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], - "packages/grafana-ui/src/graveyard/Graph/utils.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], - "packages/grafana-ui/src/graveyard/GraphNG/GraphNG.tsx:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Unexpected any. Specify a different type.", "2"], - [0, 0, 0, "Unexpected any. Specify a different type.", "3"], - [0, 0, 0, "Unexpected any. Specify a different type.", "4"], - [0, 0, 0, "Do not use any type assertions.", "5"], - [0, 0, 0, "Do not use any type assertions.", "6"], - [0, 0, 0, "Do not use any type assertions.", "7"], - [0, 0, 0, "Unexpected any. Specify a different type.", "8"], - [0, 0, 0, "Do not use any type assertions.", "9"], - [0, 0, 0, "Do not use any type assertions.", "10"], - [0, 0, 0, "Do not use any type assertions.", "11"] - ], - "packages/grafana-ui/src/graveyard/GraphNG/hooks.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], "packages/grafana-ui/src/graveyard/GraphNG/nullInsertThreshold.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"], @@ -1058,11 +1035,6 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "1"], [0, 0, 0, "Do not use any type assertions.", "2"] ], - "packages/grafana-ui/src/graveyard/TimeSeries/utils.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, "Unexpected any. Specify a different type.", "2"] - ], "packages/grafana-ui/src/options/builder/axis.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"], diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index b18eeac035..1139ac6950 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -332,9 +332,7 @@ /packages/grafana-ui/src/components/VizRepeater/ @grafana/dataviz-squad /packages/grafana-ui/src/components/VizTooltip/ @grafana/dataviz-squad /packages/grafana-ui/src/components/Sparkline/ @grafana/grafana-frontend-platform @grafana/app-o11y-visualizations -/packages/grafana-ui/src/graveyard/Graph/ @grafana/dataviz-squad /packages/grafana-ui/src/graveyard/GraphNG/ @grafana/dataviz-squad -/packages/grafana-ui/src/graveyard/TimeSeries/ @grafana/dataviz-squad /packages/grafana-ui/src/utils/storybook/ @grafana/plugins-platform-frontend /packages/grafana-data/src/transformations/ @grafana/dataviz-squad /packages/grafana-data/src/**/*logs* @grafana/observability-logs diff --git a/packages/grafana-ui/src/components/Menu/Menu.story.tsx b/packages/grafana-ui/src/components/Menu/Menu.story.tsx index 4a547fb84a..a92e0755e7 100644 --- a/packages/grafana-ui/src/components/Menu/Menu.story.tsx +++ b/packages/grafana-ui/src/components/Menu/Menu.story.tsx @@ -1,7 +1,6 @@ import { Meta } from '@storybook/react'; import React from 'react'; -import { GraphContextMenuHeader } from '..'; import { StoryExample } from '../../utils/storybook/StoryExample'; import { VerticalGroup } from '../Layout/Layout'; @@ -110,30 +109,6 @@ export function Examples() { - - - } - ariaLabel="Menu header" - > - - - - - - - - - diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index ec094bea02..cd3c980768 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -150,7 +150,6 @@ export { VizLegend } from './VizLegend/VizLegend'; export { VizLegendListItem } from './VizLegend/VizLegendListItem'; export { Alert, type AlertVariant } from './Alert/Alert'; -export { GraphSeriesToggler, type GraphSeriesTogglerAPI } from '../graveyard/Graph/GraphSeriesToggler'; export { Collapse, ControlledCollapse } from './Collapse/Collapse'; export { CollapsableSection } from './Collapse/CollapsableSection'; export { DataLinkButton } from './DataLinks/DataLinkButton'; @@ -296,19 +295,3 @@ export { type UPlotConfigPrepFn } from './uPlot/config/UPlotConfigBuilder'; export * from './PanelChrome/types'; export { Label as BrowserLabel } from './BrowserLabel/Label'; export { PanelContainer } from './PanelContainer/PanelContainer'; - -// ----------------------------------------------------- -// Graveyard: exported, but no longer used internally -// These will be removed in the future -// ----------------------------------------------------- - -export { Graph } from '../graveyard/Graph/Graph'; -export { GraphWithLegend } from '../graveyard/Graph/GraphWithLegend'; -export { GraphContextMenu, GraphContextMenuHeader } from '../graveyard/Graph/GraphContextMenu'; -export { graphTimeFormat, graphTickFormatter } from '../graveyard/Graph/utils'; - -export { GraphNG, type GraphNGProps } from '../graveyard/GraphNG/GraphNG'; -export { TimeSeries } from '../graveyard/TimeSeries/TimeSeries'; -export { useGraphNGContext } from '../graveyard/GraphNG/hooks'; -export { preparePlotFrame, buildScaleKey } from '../graveyard/GraphNG/utils'; -export { type GraphNGLegendEvent } from '../graveyard/GraphNG/types'; diff --git a/packages/grafana-ui/src/components/uPlot/utils.test.ts b/packages/grafana-ui/src/components/uPlot/utils.test.ts index 3ecbc180bd..363c49e3d5 100644 --- a/packages/grafana-ui/src/components/uPlot/utils.test.ts +++ b/packages/grafana-ui/src/components/uPlot/utils.test.ts @@ -1,7 +1,9 @@ import { FieldMatcherID, fieldMatchers, FieldType, MutableDataFrame } from '@grafana/data'; import { BarAlignment, GraphDrawStyle, GraphTransform, LineInterpolation, StackingMode } from '@grafana/schema'; -import { preparePlotFrame } from '..'; +// required for tests... but we actually have a duplicate copy that is used in the timeseries panel +// https://github.com/grafana/grafana/blob/v10.3.3/public/app/core/components/GraphNG/utils.test.ts +import { preparePlotFrame } from '../../graveyard/GraphNG/utils'; import { getStackingGroups, preparePlotData2, timeFormatToTemplate } from './utils'; diff --git a/packages/grafana-ui/src/graveyard/Graph/Graph.test.tsx b/packages/grafana-ui/src/graveyard/Graph/Graph.test.tsx deleted file mode 100644 index 847d6e883c..0000000000 --- a/packages/grafana-ui/src/graveyard/Graph/Graph.test.tsx +++ /dev/null @@ -1,185 +0,0 @@ -import { act, render, screen } from '@testing-library/react'; -import $ from 'jquery'; -import React from 'react'; - -import { GraphSeriesXY, FieldType, dateTime, FieldColorModeId, DisplayProcessor } from '@grafana/data'; -import { TooltipDisplayMode } from '@grafana/schema'; - -import { VizTooltip } from '../../components/VizTooltip'; - -import Graph from './Graph'; - -const display: DisplayProcessor = (v) => ({ numeric: Number(v), text: String(v), color: 'red' }); - -const series: GraphSeriesXY[] = [ - { - data: [ - [1546372800000, 10], - [1546376400000, 20], - [1546380000000, 10], - ], - color: 'red', - isVisible: true, - label: 'A-series', - seriesIndex: 0, - timeField: { - type: FieldType.time, - name: 'time', - values: [1546372800000, 1546376400000, 1546380000000], - config: {}, - }, - valueField: { - type: FieldType.number, - name: 'a-series', - values: [10, 20, 10], - config: { color: { mode: FieldColorModeId.Fixed, fixedColor: 'red' } }, - display, - }, - timeStep: 3600000, - yAxis: { - index: 0, - }, - }, - { - data: [ - [1546372800000, 20], - [1546376400000, 30], - [1546380000000, 40], - ], - color: 'blue', - isVisible: true, - label: 'B-series', - seriesIndex: 0, - timeField: { - type: FieldType.time, - name: 'time', - values: [1546372800000, 1546376400000, 1546380000000], - config: {}, - }, - valueField: { - type: FieldType.number, - name: 'b-series', - values: [20, 30, 40], - config: { color: { mode: FieldColorModeId.Fixed, fixedColor: 'blue' } }, - display, - }, - timeStep: 3600000, - yAxis: { - index: 0, - }, - }, -]; - -const mockTimeRange = { - from: dateTime(1546372800000), - to: dateTime(1546380000000), - raw: { - from: dateTime(1546372800000), - to: dateTime(1546380000000), - }, -}; - -const mockGraphProps = (multiSeries = false) => { - return { - width: 200, - height: 100, - series, - timeRange: mockTimeRange, - timeZone: 'browser', - }; -}; - -window.ResizeObserver = class ResizeObserver { - constructor() {} - observe() {} - unobserve() {} - disconnect() {} -}; - -describe('Graph', () => { - describe('with tooltip', () => { - describe('in single mode', () => { - it("doesn't render tooltip when not hovering over a datapoint", () => { - const graphWithTooltip = ( - - - - ); - render(graphWithTooltip); - - const timestamp = screen.queryByLabelText('Timestamp'); - const tableRow = screen.queryByTestId('SeriesTableRow'); - const seriesIcon = screen.queryByTestId('series-icon'); - - expect(timestamp).toBeFalsy(); - expect(timestamp?.parentElement).toBeFalsy(); - expect(tableRow?.parentElement).toBeFalsy(); - expect(seriesIcon).toBeFalsy(); - }); - - it('renders tooltip when hovering over a datapoint', () => { - // Given - const graphWithTooltip = ( - - - - ); - render(graphWithTooltip); - const eventArgs = { - pos: { - x: 120, - y: 50, - }, - activeItem: { - seriesIndex: 0, - dataIndex: 1, - series: { seriesIndex: 0 }, - }, - }; - act(() => { - $('div.graph-panel__chart').trigger('plothover', [eventArgs.pos, eventArgs.activeItem]); - }); - const timestamp = screen.getByLabelText('Timestamp'); - const tooltip = screen.getByTestId('SeriesTableRow').parentElement; - - expect(timestamp.parentElement?.isEqualNode(tooltip)).toBe(true); - expect(screen.getAllByTestId('series-icon')).toHaveLength(1); - }); - }); - - describe('in All Series mode', () => { - it('it renders all series summary regardless of mouse position', () => { - // Given - const graphWithTooltip = ( - - - - ); - render(graphWithTooltip); - - // When - const eventArgs = { - // This "is" more or less between first and middle point. Flot would not have picked any point as active one at this position - pos: { - x: 80, - y: 50, - }, - activeItem: null, - }; - // Then - act(() => { - $('div.graph-panel__chart').trigger('plothover', [eventArgs.pos, eventArgs.activeItem]); - }); - const timestamp = screen.getByLabelText('Timestamp'); - - const tableRows = screen.getAllByTestId('SeriesTableRow'); - expect(tableRows).toHaveLength(2); - expect(timestamp.parentElement?.isEqualNode(tableRows[0].parentElement)).toBe(true); - expect(timestamp.parentElement?.isEqualNode(tableRows[1].parentElement)).toBe(true); - - const seriesIcon = screen.getAllByTestId('series-icon'); - expect(seriesIcon).toHaveLength(2); - }); - }); - }); -}); diff --git a/packages/grafana-ui/src/graveyard/Graph/Graph.tsx b/packages/grafana-ui/src/graveyard/Graph/Graph.tsx deleted file mode 100644 index 624d19e29c..0000000000 --- a/packages/grafana-ui/src/graveyard/Graph/Graph.tsx +++ /dev/null @@ -1,400 +0,0 @@ -// Libraries -import $ from 'jquery'; -import { uniqBy } from 'lodash'; -import React, { PureComponent } from 'react'; - -// Types -import { TimeRange, GraphSeriesXY, TimeZone, createDimension } from '@grafana/data'; -import { TooltipDisplayMode } from '@grafana/schema'; - -import { VizTooltipProps, VizTooltipContentProps, ActiveDimensions, VizTooltip } from '../../components/VizTooltip'; -import { FlotPosition } from '../../components/VizTooltip/VizTooltip'; - -import { GraphContextMenu, GraphContextMenuProps, ContextDimensions } from './GraphContextMenu'; -import { GraphTooltip } from './GraphTooltip/GraphTooltip'; -import { GraphDimensions } from './GraphTooltip/types'; -import { FlotItem } from './types'; -import { graphTimeFormat, graphTickFormatter } from './utils'; - -/** @deprecated */ -export interface GraphProps { - ariaLabel?: string; - children?: JSX.Element | JSX.Element[]; - series: GraphSeriesXY[]; - timeRange: TimeRange; // NOTE: we should aim to make `time` a property of the axis, not force it for all graphs - timeZone?: TimeZone; // NOTE: we should aim to make `time` a property of the axis, not force it for all graphs - showLines?: boolean; - showPoints?: boolean; - showBars?: boolean; - width: number; - height: number; - isStacked?: boolean; - lineWidth?: number; - onHorizontalRegionSelected?: (from: number, to: number) => void; -} - -/** @deprecated */ -interface GraphState { - pos?: FlotPosition; - contextPos?: FlotPosition; - isTooltipVisible: boolean; - isContextVisible: boolean; - activeItem?: FlotItem; - contextItem?: FlotItem; -} - -/** - * This is a react wrapper for the angular, flot based graph visualization. - * Rather than using this component, you should use the ` with - * timeseries panel configs. - * - * @deprecated - */ -export class Graph extends PureComponent { - static defaultProps = { - showLines: true, - showPoints: false, - showBars: false, - isStacked: false, - lineWidth: 1, - }; - - state: GraphState = { - isTooltipVisible: false, - isContextVisible: false, - }; - - element: HTMLElement | null = null; - $element: JQuery | null = null; - - componentDidUpdate(prevProps: GraphProps, prevState: GraphState) { - if (prevProps !== this.props) { - this.draw(); - } - } - - componentDidMount() { - this.draw(); - if (this.element) { - this.$element = $(this.element); - this.$element.bind('plotselected', this.onPlotSelected); - this.$element.bind('plothover', this.onPlotHover); - this.$element.bind('plotclick', this.onPlotClick); - } - } - - componentWillUnmount() { - if (this.$element) { - this.$element.unbind('plotselected', this.onPlotSelected); - } - } - - onPlotSelected = (event: JQuery.Event, ranges: { xaxis: { from: number; to: number } }) => { - const { onHorizontalRegionSelected } = this.props; - if (onHorizontalRegionSelected) { - onHorizontalRegionSelected(ranges.xaxis.from, ranges.xaxis.to); - } - }; - - onPlotHover = (event: JQuery.Event, pos: FlotPosition, item?: FlotItem) => { - this.setState({ - isTooltipVisible: true, - activeItem: item, - pos, - }); - }; - - onPlotClick = (event: JQuery.Event, contextPos: FlotPosition, item?: FlotItem) => { - this.setState({ - isContextVisible: true, - isTooltipVisible: false, - contextItem: item, - contextPos, - }); - }; - - getYAxes(series: GraphSeriesXY[]) { - if (series.length === 0) { - return [{ show: true, min: -1, max: 1 }]; - } - return uniqBy( - series.map((s) => { - const index = s.yAxis ? s.yAxis.index : 1; - const min = s.yAxis && s.yAxis.min && !isNaN(s.yAxis.min) ? s.yAxis.min : null; - const tickDecimals = - s.yAxis && s.yAxis.tickDecimals && !isNaN(s.yAxis.tickDecimals) ? s.yAxis.tickDecimals : null; - return { - show: true, - index, - position: index === 1 ? 'left' : 'right', - min, - tickDecimals, - }; - }), - (yAxisConfig) => yAxisConfig.index - ); - } - - renderTooltip = () => { - const { children, series, timeZone } = this.props; - const { pos, activeItem, isTooltipVisible } = this.state; - let tooltipElement: React.ReactElement | undefined; - - if (!isTooltipVisible || !pos || series.length === 0) { - return null; - } - - // Find children that indicate tooltip to be rendered - React.Children.forEach(children, (c) => { - // We have already found tooltip - if (tooltipElement) { - return; - } - const childType = c && c.type && (c.type.displayName || c.type.name); - - if (childType === VizTooltip.displayName) { - tooltipElement = c; - } - }); - // If no tooltip provided, skip rendering - if (!tooltipElement) { - return null; - } - const tooltipElementProps = tooltipElement.props; - - const tooltipMode = tooltipElementProps.mode || 'single'; - - // If mode is single series and user is not hovering over item, skip rendering - if (!activeItem && tooltipMode === 'single') { - return null; - } - - // Check if tooltip needs to be rendered with custom tooltip component, otherwise default to GraphTooltip - const tooltipContentRenderer = tooltipElementProps.tooltipComponent || GraphTooltip; - // Indicates column(field) index in y-axis dimension - const seriesIndex = activeItem ? activeItem.series.seriesIndex : 0; - // Indicates row index in active field values - const rowIndex = activeItem ? activeItem.dataIndex : undefined; - - const activeDimensions: ActiveDimensions = { - // Described x-axis active item - // When hovering over an item - let's take it's dataIndex, otherwise undefined - // Tooltip itself needs to figure out correct datapoint display information based on pos passed to it - xAxis: [seriesIndex, rowIndex], - // Describes y-axis active item - yAxis: activeItem ? [activeItem.series.seriesIndex, activeItem.dataIndex] : null, - }; - - const tooltipContentProps: VizTooltipContentProps = { - dimensions: { - // time/value dimension columns are index-aligned - see getGraphSeriesModel - xAxis: createDimension( - 'xAxis', - series.map((s) => s.timeField) - ), - yAxis: createDimension( - 'yAxis', - series.map((s) => s.valueField) - ), - }, - activeDimensions, - pos, - mode: tooltipElementProps.mode || TooltipDisplayMode.Single, - timeZone, - }; - - const tooltipContent = React.createElement(tooltipContentRenderer, { ...tooltipContentProps }); - - return React.cloneElement(tooltipElement, { - content: tooltipContent, - position: { x: pos.pageX, y: pos.pageY }, - offset: { x: 10, y: 10 }, - }); - }; - - renderContextMenu = () => { - const { series } = this.props; - const { contextPos, contextItem, isContextVisible } = this.state; - - if (!isContextVisible || !contextPos || !contextItem || series.length === 0) { - return null; - } - - // Indicates column(field) index in y-axis dimension - const seriesIndex = contextItem ? contextItem.series.seriesIndex : 0; - // Indicates row index in context field values - const rowIndex = contextItem ? contextItem.dataIndex : undefined; - - const contextDimensions: ContextDimensions = { - // Described x-axis context item - xAxis: [seriesIndex, rowIndex], - // Describes y-axis context item - yAxis: contextItem ? [contextItem.series.seriesIndex, contextItem.dataIndex] : null, - }; - - const dimensions: GraphDimensions = { - // time/value dimension columns are index-aligned - see getGraphSeriesModel - xAxis: createDimension( - 'xAxis', - series.map((s) => s.timeField) - ), - yAxis: createDimension( - 'yAxis', - series.map((s) => s.valueField) - ), - }; - - const closeContext = () => this.setState({ isContextVisible: false }); - - const getContextMenuSource = () => { - return { - datapoint: contextItem.datapoint, - dataIndex: contextItem.dataIndex, - series: contextItem.series, - seriesIndex: contextItem.series.seriesIndex, - pageX: contextPos.pageX, - pageY: contextPos.pageY, - }; - }; - - const contextContentProps: GraphContextMenuProps = { - x: contextPos.pageX, - y: contextPos.pageY, - onClose: closeContext, - getContextMenuSource: getContextMenuSource, - timeZone: this.props.timeZone, - dimensions, - contextDimensions, - }; - - return ; - }; - - getBarWidth = () => { - const { series } = this.props; - return Math.min(...series.map((s) => s.timeStep)); - }; - - draw() { - if (this.element === null) { - return; - } - - const { - width, - series, - timeRange, - showLines, - showBars, - showPoints, - isStacked, - lineWidth, - timeZone, - onHorizontalRegionSelected, - } = this.props; - - if (!width) { - return; - } - - const ticks = width / 100; - const min = timeRange.from.valueOf(); - const max = timeRange.to.valueOf(); - const yaxes = this.getYAxes(series); - - const flotOptions = { - legend: { - show: false, - }, - series: { - stack: isStacked, - lines: { - show: showLines, - lineWidth: lineWidth, - zero: false, - }, - points: { - show: showPoints, - fill: 1, - fillColor: false, - radius: 2, - }, - bars: { - show: showBars, - fill: 1, - // Dividig the width by 1.5 to make the bars not touch each other - barWidth: showBars ? this.getBarWidth() / 1.5 : 1, - zero: false, - lineWidth: lineWidth, - }, - shadowSize: 0, - }, - xaxis: { - timezone: timeZone, - show: true, - mode: 'time', - min: min, - max: max, - label: 'Datetime', - ticks: ticks, - timeformat: graphTimeFormat(ticks, min, max), - tickFormatter: graphTickFormatter, - }, - yaxes, - grid: { - minBorderMargin: 0, - markings: [], - backgroundColor: null, - borderWidth: 0, - hoverable: true, - clickable: true, - color: '#a1a1a1', - margin: { left: 0, right: 0 }, - labelMarginX: 0, - mouseActiveRadius: 30, - }, - selection: { - mode: onHorizontalRegionSelected ? 'x' : null, - color: '#666', - }, - crosshair: { - mode: 'x', - }, - }; - - try { - $.plot( - this.element, - series.filter((s) => s.isVisible), - flotOptions - ); - } catch (err) { - console.error('Graph rendering error', err, flotOptions, series); - throw new Error('Error rendering panel'); - } - } - - render() { - const { ariaLabel, height, width, series } = this.props; - const noDataToBeDisplayed = series.length === 0; - const tooltip = this.renderTooltip(); - const context = this.renderContextMenu(); - return ( -
-
(this.element = e)} - style={{ height, width }} - onMouseLeave={() => { - this.setState({ isTooltipVisible: false }); - }} - /> - {noDataToBeDisplayed &&
No data
} - {tooltip} - {context} -
- ); - } -} - -export default Graph; diff --git a/packages/grafana-ui/src/graveyard/Graph/GraphSeriesToggler.tsx b/packages/grafana-ui/src/graveyard/Graph/GraphSeriesToggler.tsx deleted file mode 100644 index fc960695a6..0000000000 --- a/packages/grafana-ui/src/graveyard/Graph/GraphSeriesToggler.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { difference, isEqual } from 'lodash'; -import React, { Component } from 'react'; - -import { GraphSeriesXY } from '@grafana/data'; - -/** @deprecated */ -export interface GraphSeriesTogglerAPI { - onSeriesToggle: (label: string, event: React.MouseEvent) => void; - toggledSeries: GraphSeriesXY[]; -} - -/** @deprecated */ -export interface GraphSeriesTogglerProps { - children: (api: GraphSeriesTogglerAPI) => JSX.Element; - series: GraphSeriesXY[]; - onHiddenSeriesChanged?: (hiddenSeries: string[]) => void; -} - -/** @deprecated */ -export interface GraphSeriesTogglerState { - hiddenSeries: string[]; - toggledSeries: GraphSeriesXY[]; -} - -/** @deprecated */ -export class GraphSeriesToggler extends Component { - constructor(props: GraphSeriesTogglerProps) { - super(props); - - this.onSeriesToggle = this.onSeriesToggle.bind(this); - - this.state = { - hiddenSeries: [], - toggledSeries: props.series, - }; - } - - componentDidUpdate(prevProps: Readonly) { - const { series } = this.props; - if (!isEqual(prevProps.series, series)) { - this.setState({ hiddenSeries: [], toggledSeries: series }); - } - } - - onSeriesToggle(label: string, event: React.MouseEvent) { - const { series, onHiddenSeriesChanged } = this.props; - const { hiddenSeries } = this.state; - - if (event.ctrlKey || event.metaKey || event.shiftKey) { - // Toggling series with key makes the series itself to toggle - const newHiddenSeries = - hiddenSeries.indexOf(label) > -1 - ? hiddenSeries.filter((series) => series !== label) - : hiddenSeries.concat([label]); - - const toggledSeries = series.map((series) => ({ - ...series, - isVisible: newHiddenSeries.indexOf(series.label) === -1, - })); - this.setState({ hiddenSeries: newHiddenSeries, toggledSeries }, () => - onHiddenSeriesChanged ? onHiddenSeriesChanged(newHiddenSeries) : undefined - ); - return; - } - - // Toggling series with out key toggles all the series but the clicked one - const allSeriesLabels = series.map((series) => series.label); - const newHiddenSeries = - hiddenSeries.length + 1 === allSeriesLabels.length ? [] : difference(allSeriesLabels, [label]); - const toggledSeries = series.map((series) => ({ - ...series, - isVisible: newHiddenSeries.indexOf(series.label) === -1, - })); - - this.setState({ hiddenSeries: newHiddenSeries, toggledSeries }, () => - onHiddenSeriesChanged ? onHiddenSeriesChanged(newHiddenSeries) : undefined - ); - } - - render() { - const { children } = this.props; - const { toggledSeries } = this.state; - - return children({ - onSeriesToggle: this.onSeriesToggle, - toggledSeries, - }); - } -} diff --git a/packages/grafana-ui/src/graveyard/Graph/GraphTooltip/GraphTooltip.tsx b/packages/grafana-ui/src/graveyard/Graph/GraphTooltip/GraphTooltip.tsx deleted file mode 100644 index d58c0816df..0000000000 --- a/packages/grafana-ui/src/graveyard/Graph/GraphTooltip/GraphTooltip.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import React from 'react'; - -import { TooltipDisplayMode } from '@grafana/schema'; - -import { VizTooltipContentProps } from '../../../components/VizTooltip'; - -import { MultiModeGraphTooltip } from './MultiModeGraphTooltip'; -import { SingleModeGraphTooltip } from './SingleModeGraphTooltip'; -import { GraphDimensions } from './types'; - -/** @deprecated */ -export const GraphTooltip = ({ - mode = TooltipDisplayMode.Single, - dimensions, - activeDimensions, - pos, - timeZone, -}: VizTooltipContentProps) => { - // When - // [1] no active dimension or - // [2] no xAxis position - // we assume no tooltip should be rendered - if (!activeDimensions || !activeDimensions.xAxis) { - return null; - } - - if (mode === 'single') { - return ; - } else { - return ( - - ); - } -}; - -GraphTooltip.displayName = 'GraphTooltip'; diff --git a/packages/grafana-ui/src/graveyard/Graph/GraphTooltip/MultiModeGraphTooltip.test.tsx b/packages/grafana-ui/src/graveyard/Graph/GraphTooltip/MultiModeGraphTooltip.test.tsx deleted file mode 100644 index 1af501183a..0000000000 --- a/packages/grafana-ui/src/graveyard/Graph/GraphTooltip/MultiModeGraphTooltip.test.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import { render, screen } from '@testing-library/react'; -import React from 'react'; - -import { createDimension, createTheme, FieldType, DisplayProcessor } from '@grafana/data'; - -import { ActiveDimensions } from '../../../components/VizTooltip'; - -import { MultiModeGraphTooltip } from './MultiModeGraphTooltip'; -import { GraphDimensions } from './types'; - -let dimensions: GraphDimensions; - -describe('MultiModeGraphTooltip', () => { - const display: DisplayProcessor = (v) => ({ numeric: Number(v), text: String(v), color: 'red' }); - const theme = createTheme(); - - describe('when shown when hovering over a datapoint', () => { - beforeEach(() => { - dimensions = { - xAxis: createDimension('xAxis', [ - { - config: {}, - values: [0, 100, 200], - name: 'A-series time', - type: FieldType.time, - display, - }, - { - config: {}, - values: [0, 100, 200], - name: 'B-series time', - type: FieldType.time, - display, - }, - ]), - yAxis: createDimension('yAxis', [ - { - config: {}, - values: [10, 20, 10], - name: 'A-series values', - type: FieldType.number, - display, - }, - { - config: {}, - values: [20, 30, 40], - name: 'B-series values', - type: FieldType.number, - display, - }, - ]), - }; - }); - - it('highlights series of the datapoint', () => { - // We are simulating hover over A-series, middle point - const activeDimensions: ActiveDimensions = { - xAxis: [0, 1], // column, row - yAxis: [0, 1], // column, row - }; - render( - - ); - - // We rendered two series rows - const rows = screen.getAllByTestId('SeriesTableRow'); - expect(rows.length).toEqual(2); - - // We expect A-series(1st row) not to be highlighted - expect(rows[0]).toHaveStyle(`font-weight: ${theme.typography.fontWeightMedium}`); - // We expect B-series(2nd row) not to be highlighted - expect(rows[1]).not.toHaveStyle(`font-weight: ${theme.typography.fontWeightMedium}`); - }); - - it("doesn't highlight series when not hovering over datapoint", () => { - // We are simulating hover over graph, but not datapoint - const activeDimensions: ActiveDimensions = { - xAxis: [0, undefined], // no active point in time - yAxis: null, // no active series - }; - - render( - - ); - - // We rendered two series rows - const rows = screen.getAllByTestId('SeriesTableRow'); - expect(rows.length).toEqual(2); - - // We expect A-series(1st row) not to be highlighted - expect(rows[0]).not.toHaveStyle(`font-weight: ${theme.typography.fontWeightMedium}`); - // We expect B-series(2nd row) not to be highlighted - expect(rows[1]).not.toHaveStyle(`font-weight: ${theme.typography.fontWeightMedium}`); - }); - }); -}); diff --git a/packages/grafana-ui/src/graveyard/Graph/GraphTooltip/MultiModeGraphTooltip.tsx b/packages/grafana-ui/src/graveyard/Graph/GraphTooltip/MultiModeGraphTooltip.tsx deleted file mode 100644 index bfe2856c70..0000000000 --- a/packages/grafana-ui/src/graveyard/Graph/GraphTooltip/MultiModeGraphTooltip.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import React from 'react'; - -import { getValueFromDimension } from '@grafana/data'; - -import { SeriesTable } from '../../../components/VizTooltip'; -import { FlotPosition } from '../../../components/VizTooltip/VizTooltip'; -import { getMultiSeriesGraphHoverInfo } from '../utils'; - -import { GraphTooltipContentProps } from './types'; - -/** @deprecated */ -type Props = GraphTooltipContentProps & { - // We expect position to figure out correct values when not hovering over a datapoint - pos: FlotPosition; -}; - -/** @deprecated */ -export const MultiModeGraphTooltip = ({ dimensions, activeDimensions, pos, timeZone }: Props) => { - let activeSeriesIndex: number | null = null; - // when no x-axis provided, skip rendering - if (activeDimensions.xAxis === null) { - return null; - } - - if (activeDimensions.yAxis) { - activeSeriesIndex = activeDimensions.yAxis[0]; - } - - // when not hovering over a point, time is undefined, and we use pos.x as time - const time = activeDimensions.xAxis[1] - ? getValueFromDimension(dimensions.xAxis, activeDimensions.xAxis[0], activeDimensions.xAxis[1]) - : pos.x; - - const hoverInfo = getMultiSeriesGraphHoverInfo(dimensions.yAxis.columns, dimensions.xAxis.columns, time, timeZone); - const timestamp = hoverInfo.time; - - const series = hoverInfo.results.map((s, i) => { - return { - color: s.color, - label: s.label, - value: s.value, - isActive: activeSeriesIndex === i, - }; - }); - - return ; -}; - -MultiModeGraphTooltip.displayName = 'MultiModeGraphTooltip'; diff --git a/packages/grafana-ui/src/graveyard/Graph/GraphTooltip/SingleModeGraphTooltip.tsx b/packages/grafana-ui/src/graveyard/Graph/GraphTooltip/SingleModeGraphTooltip.tsx deleted file mode 100644 index 7eb4d61872..0000000000 --- a/packages/grafana-ui/src/graveyard/Graph/GraphTooltip/SingleModeGraphTooltip.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import React from 'react'; - -import { - getValueFromDimension, - getColumnFromDimension, - formattedValueToString, - getFieldDisplayName, -} from '@grafana/data'; - -import { SeriesTable } from '../../../components/VizTooltip'; - -import { GraphTooltipContentProps } from './types'; - -/** @deprecated */ -export const SingleModeGraphTooltip = ({ dimensions, activeDimensions, timeZone }: GraphTooltipContentProps) => { - // not hovering over a point, skip rendering - if ( - activeDimensions.yAxis === null || - activeDimensions.yAxis[1] === undefined || - activeDimensions.xAxis === null || - activeDimensions.xAxis[1] === undefined - ) { - return null; - } - const time = getValueFromDimension(dimensions.xAxis, activeDimensions.xAxis[0], activeDimensions.xAxis[1]); - const timeField = getColumnFromDimension(dimensions.xAxis, activeDimensions.xAxis[0]); - const processedTime = timeField.display ? formattedValueToString(timeField.display(time)) : time; - - const valueField = getColumnFromDimension(dimensions.yAxis, activeDimensions.yAxis[0]); - const value = getValueFromDimension(dimensions.yAxis, activeDimensions.yAxis[0], activeDimensions.yAxis[1]); - const display = valueField.display!; - const disp = display(value); - - return ( - - ); -}; - -SingleModeGraphTooltip.displayName = 'SingleModeGraphTooltip'; diff --git a/packages/grafana-ui/src/graveyard/Graph/GraphTooltip/types.ts b/packages/grafana-ui/src/graveyard/Graph/GraphTooltip/types.ts deleted file mode 100644 index d4d68b729c..0000000000 --- a/packages/grafana-ui/src/graveyard/Graph/GraphTooltip/types.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Dimension, Dimensions, TimeZone } from '@grafana/data'; - -import { ActiveDimensions } from '../../../components/VizTooltip'; - -/** @deprecated */ -export interface GraphDimensions extends Dimensions { - xAxis: Dimension; - yAxis: Dimension; -} - -/** @deprecated */ -export interface GraphTooltipContentProps { - dimensions: GraphDimensions; // Dimension[] - activeDimensions: ActiveDimensions; - timeZone?: TimeZone; -} diff --git a/packages/grafana-ui/src/graveyard/Graph/GraphWithLegend.internal.story.tsx b/packages/grafana-ui/src/graveyard/Graph/GraphWithLegend.internal.story.tsx deleted file mode 100644 index 0830b44f19..0000000000 --- a/packages/grafana-ui/src/graveyard/Graph/GraphWithLegend.internal.story.tsx +++ /dev/null @@ -1,141 +0,0 @@ -import { Story } from '@storybook/react'; -import React from 'react'; - -import { GraphSeriesXY, FieldType, dateTime, FieldColorModeId } from '@grafana/data'; -import { LegendDisplayMode } from '@grafana/schema'; - -import { GraphWithLegend, GraphWithLegendProps } from './GraphWithLegend'; - -export default { - title: 'Visualizations/Graph/GraphWithLegend', - component: GraphWithLegend, - parameters: { - controls: { - exclude: ['className', 'ariaLabel', 'legendDisplayMode', 'series'], - }, - }, - argTypes: { - displayMode: { control: { type: 'radio' }, options: ['table', 'list', 'hidden'] }, - placement: { control: { type: 'radio' }, options: ['bottom', 'right'] }, - rightAxisSeries: { name: 'Right y-axis series, i.e. A,C' }, - timeZone: { control: { type: 'radio' }, options: ['browser', 'utc'] }, - width: { control: { type: 'range', min: 200, max: 800 } }, - height: { control: { type: 'range', min: 1700, step: 300 } }, - lineWidth: { control: { type: 'range', min: 1, max: 10 } }, - }, -}; - -const series: GraphSeriesXY[] = [ - { - data: [ - [1546372800000, 10], - [1546376400000, 20], - [1546380000000, 10], - ], - color: 'red', - isVisible: true, - label: 'A-series', - seriesIndex: 0, - timeField: { - type: FieldType.time, - name: 'time', - values: [1546372800000, 1546376400000, 1546380000000], - config: {}, - }, - valueField: { - type: FieldType.number, - name: 'a-series', - values: [10, 20, 10], - config: { - color: { - mode: FieldColorModeId.Fixed, - fixedColor: 'red', - }, - }, - }, - timeStep: 3600000, - yAxis: { - index: 1, - }, - }, - { - data: [ - [1546372800000, 20], - [1546376400000, 30], - [1546380000000, 40], - ], - color: 'blue', - isVisible: true, - label: 'B-series', - seriesIndex: 1, - timeField: { - type: FieldType.time, - name: 'time', - values: [1546372800000, 1546376400000, 1546380000000], - config: {}, - }, - valueField: { - type: FieldType.number, - name: 'b-series', - values: [20, 30, 40], - config: { - color: { - mode: FieldColorModeId.Fixed, - fixedColor: 'blue', - }, - }, - }, - timeStep: 3600000, - yAxis: { - index: 1, - }, - }, -]; - -interface StoryProps extends GraphWithLegendProps { - rightAxisSeries: string; - displayMode: string; -} - -export const WithLegend: Story = ({ rightAxisSeries, displayMode, legendDisplayMode, ...args }) => { - const props: Partial = { - series: series.map((s) => { - if ( - rightAxisSeries - .split(',') - .map((s) => s.trim()) - .indexOf(s.label.split('-')[0]) > -1 - ) { - s.yAxis = { index: 2 }; - } else { - s.yAxis = { index: 1 }; - } - return s; - }), - }; - - return ( - - ); -}; -WithLegend.args = { - rightAxisSeries: '', - displayMode: 'list', - onToggleSort: () => {}, - timeRange: { - from: dateTime(1546372800000), - to: dateTime(1546380000000), - raw: { - from: dateTime(1546372800000), - to: dateTime(1546380000000), - }, - }, - timeZone: 'browser', - width: 600, - height: 300, - placement: 'bottom', -}; diff --git a/packages/grafana-ui/src/graveyard/Graph/GraphWithLegend.tsx b/packages/grafana-ui/src/graveyard/Graph/GraphWithLegend.tsx deleted file mode 100644 index a590d5d1ed..0000000000 --- a/packages/grafana-ui/src/graveyard/Graph/GraphWithLegend.tsx +++ /dev/null @@ -1,132 +0,0 @@ -// Libraries - -import { css } from '@emotion/css'; -import React from 'react'; - -import { GrafanaTheme2, GraphSeriesValue } from '@grafana/data'; -import { LegendDisplayMode, LegendPlacement } from '@grafana/schema'; - -import { CustomScrollbar } from '../../components/CustomScrollbar/CustomScrollbar'; -import { VizLegend } from '../../components/VizLegend/VizLegend'; -import { VizLegendItem } from '../../components/VizLegend/types'; -import { useStyles2 } from '../../themes'; - -import { Graph, GraphProps } from './Graph'; - -export interface GraphWithLegendProps extends GraphProps { - legendDisplayMode: LegendDisplayMode; - legendVisibility: boolean; - placement: LegendPlacement; - hideEmpty?: boolean; - hideZero?: boolean; - sortLegendBy?: string; - sortLegendDesc?: boolean; - onSeriesToggle?: (label: string, event: React.MouseEvent) => void; - onToggleSort: (sortBy: string) => void; -} - -const shouldHideLegendItem = (data: GraphSeriesValue[][], hideEmpty = false, hideZero = false) => { - const isZeroOnlySeries = data.reduce((acc, current) => acc + (current[1] || 0), 0) === 0; - const isNullOnlySeries = !data.reduce((acc, current) => acc && current[1] !== null, true); - - return (hideEmpty && isNullOnlySeries) || (hideZero && isZeroOnlySeries); -}; - -export const GraphWithLegend = (props: GraphWithLegendProps) => { - const { - series, - timeRange, - width, - height, - showBars, - showLines, - showPoints, - sortLegendBy, - sortLegendDesc, - legendDisplayMode, - legendVisibility, - placement, - onSeriesToggle, - onToggleSort, - hideEmpty, - hideZero, - isStacked, - lineWidth, - onHorizontalRegionSelected, - timeZone, - children, - ariaLabel, - } = props; - const { graphContainer, wrapper, legendContainer } = useStyles2(getGraphWithLegendStyles, props.placement); - - const legendItems = series.reduce((acc, s) => { - return shouldHideLegendItem(s.data, hideEmpty, hideZero) - ? acc - : acc.concat([ - { - label: s.label, - color: s.color || '', - disabled: !s.isVisible, - yAxis: s.yAxis.index, - getDisplayValues: () => s.info || [], - }, - ]); - }, []); - - return ( -
-
- - {children} - -
- - {legendVisibility && ( -
- - { - if (onSeriesToggle) { - onSeriesToggle(item.label, event); - } - }} - onToggleSort={onToggleSort} - /> - -
- )} -
- ); -}; - -const getGraphWithLegendStyles = (_theme: GrafanaTheme2, placement: LegendPlacement) => ({ - wrapper: css({ - display: 'flex', - flexDirection: placement === 'bottom' ? 'column' : 'row', - }), - graphContainer: css({ - minHeight: '65%', - flexGrow: 1, - }), - legendContainer: css({ - padding: '10px 0', - maxHeight: placement === 'bottom' ? '35%' : 'none', - }), -}); diff --git a/packages/grafana-ui/src/graveyard/Graph/types.ts b/packages/grafana-ui/src/graveyard/Graph/types.ts deleted file mode 100644 index 60cdb44b98..0000000000 --- a/packages/grafana-ui/src/graveyard/Graph/types.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** @deprecated */ -export interface FlotItem { - datapoint: [number, number]; - dataIndex: number; - series: T; - seriesIndex: number; - pageX: number; - pageY: number; -} diff --git a/packages/grafana-ui/src/graveyard/Graph/utils.test.ts b/packages/grafana-ui/src/graveyard/Graph/utils.test.ts deleted file mode 100644 index f68ed2e4b3..0000000000 --- a/packages/grafana-ui/src/graveyard/Graph/utils.test.ts +++ /dev/null @@ -1,233 +0,0 @@ -import { - toDataFrame, - FieldType, - FieldCache, - FieldColorModeId, - Field, - applyFieldOverrides, - createTheme, - DataFrame, -} from '@grafana/data'; - -import { getTheme } from '../../themes'; - -import { getMultiSeriesGraphHoverInfo, findHoverIndexFromData, graphTimeFormat } from './utils'; - -const mockResult = ( - value: string, - datapointIndex: number, - seriesIndex: number, - color?: string, - label?: string, - time?: string -) => ({ - value, - datapointIndex, - seriesIndex, - color, - label, - time, -}); - -function passThroughFieldOverrides(frame: DataFrame) { - return applyFieldOverrides({ - data: [frame], - fieldConfig: { - defaults: {}, - overrides: [], - }, - replaceVariables: (val: string) => val, - timeZone: 'utc', - theme: createTheme(), - }); -} - -// A and B series have the same x-axis range and the datapoints are x-axis aligned -const aSeries = passThroughFieldOverrides( - toDataFrame({ - fields: [ - { name: 'time', type: FieldType.time, values: [10000, 20000, 30000, 80000] }, - { - name: 'value', - type: FieldType.number, - values: [10, 20, 10, 25], - config: { color: { mode: FieldColorModeId.Fixed, fixedColor: 'red' } }, - }, - ], - }) -)[0]; - -const bSeries = passThroughFieldOverrides( - toDataFrame({ - fields: [ - { name: 'time', type: FieldType.time, values: [10000, 20000, 30000, 80000] }, - { - name: 'value', - type: FieldType.number, - values: [30, 60, 30, 40], - config: { color: { mode: FieldColorModeId.Fixed, fixedColor: 'blue' } }, - }, - ], - }) -)[0]; - -// C-series has the same x-axis range as A and B but is missing the middle point -const cSeries = passThroughFieldOverrides( - toDataFrame({ - fields: [ - { name: 'time', type: FieldType.time, values: [10000, 30000, 80000] }, - { - name: 'value', - type: FieldType.number, - values: [30, 30, 30], - config: { color: { mode: FieldColorModeId.Fixed, fixedColor: 'yellow' } }, - }, - ], - }) -)[0]; - -function getFixedThemedColor(field: Field): string { - return getTheme().visualization.getColorByName(field.config.color!.fixedColor!); -} - -describe('Graph utils', () => { - describe('getMultiSeriesGraphHoverInfo', () => { - describe('when series datapoints are x-axis aligned', () => { - it('returns a datapoints that user hovers over', () => { - const aCache = new FieldCache(aSeries); - const aValueField = aCache.getFieldByName('value'); - const aTimeField = aCache.getFieldByName('time'); - const bCache = new FieldCache(bSeries); - const bValueField = bCache.getFieldByName('value'); - const bTimeField = bCache.getFieldByName('time'); - - const result = getMultiSeriesGraphHoverInfo([aValueField!, bValueField!], [aTimeField!, bTimeField!], 0); - expect(result.time).toBe('1970-01-01 00:00:10'); - expect(result.results[0]).toEqual( - mockResult('10', 0, 0, getFixedThemedColor(aValueField!), aValueField!.name, '1970-01-01 00:00:10') - ); - expect(result.results[1]).toEqual( - mockResult('30', 0, 1, getFixedThemedColor(bValueField!), bValueField!.name, '1970-01-01 00:00:10') - ); - }); - - describe('returns the closest datapoints before the hover position', () => { - it('when hovering right before a datapoint', () => { - const aCache = new FieldCache(aSeries); - const aValueField = aCache.getFieldByName('value'); - const aTimeField = aCache.getFieldByName('time'); - const bCache = new FieldCache(bSeries); - const bValueField = bCache.getFieldByName('value'); - const bTimeField = bCache.getFieldByName('time'); - - // hovering right before middle point - const result = getMultiSeriesGraphHoverInfo([aValueField!, bValueField!], [aTimeField!, bTimeField!], 19900); - expect(result.time).toBe('1970-01-01 00:00:10'); - expect(result.results[0]).toEqual( - mockResult('10', 0, 0, getFixedThemedColor(aValueField!), aValueField!.name, '1970-01-01 00:00:10') - ); - expect(result.results[1]).toEqual( - mockResult('30', 0, 1, getFixedThemedColor(bValueField!), bValueField!.name, '1970-01-01 00:00:10') - ); - }); - - it('when hovering right after a datapoint', () => { - const aCache = new FieldCache(aSeries); - const aValueField = aCache.getFieldByName('value'); - const aTimeField = aCache.getFieldByName('time'); - const bCache = new FieldCache(bSeries); - const bValueField = bCache.getFieldByName('value'); - const bTimeField = bCache.getFieldByName('time'); - - // hovering right after middle point - const result = getMultiSeriesGraphHoverInfo([aValueField!, bValueField!], [aTimeField!, bTimeField!], 20100); - expect(result.time).toBe('1970-01-01 00:00:20'); - expect(result.results[0]).toEqual( - mockResult('20', 1, 0, getFixedThemedColor(aValueField!), aValueField!.name, '1970-01-01 00:00:20') - ); - expect(result.results[1]).toEqual( - mockResult('60', 1, 1, getFixedThemedColor(bValueField!), bValueField!.name, '1970-01-01 00:00:20') - ); - }); - }); - }); - - describe('when series x-axes are not aligned', () => { - // aSeries and cSeries are not aligned - // cSeries is missing a middle point - it('hovering over a middle point', () => { - const aCache = new FieldCache(aSeries); - const aValueField = aCache.getFieldByName('value'); - const aTimeField = aCache.getFieldByName('time'); - const cCache = new FieldCache(cSeries); - const cValueField = cCache.getFieldByName('value'); - const cTimeField = cCache.getFieldByName('time'); - - // hovering on a middle point - // aSeries has point at that time, cSeries doesn't - const result = getMultiSeriesGraphHoverInfo([aValueField!, cValueField!], [aTimeField!, cTimeField!], 20000); - - // we expect a time of the hovered point - expect(result.time).toBe('1970-01-01 00:00:20'); - // we expect middle point from aSeries (the one we are hovering over) - expect(result.results[0]).toEqual( - mockResult('20', 1, 0, getFixedThemedColor(aValueField!), aValueField!.name, '1970-01-01 00:00:20') - ); - // we expect closest point before hovered point from cSeries (1st point) - expect(result.results[1]).toEqual( - mockResult('30', 0, 1, getFixedThemedColor(cValueField!), cValueField!.name, '1970-01-01 00:00:10') - ); - }); - - it('hovering right after over the middle point', () => { - const aCache = new FieldCache(aSeries); - const aValueField = aCache.getFieldByName('value'); - const aTimeField = aCache.getFieldByName('time'); - const cCache = new FieldCache(cSeries); - const cValueField = cCache.getFieldByName('value'); - const cTimeField = cCache.getFieldByName('time'); - - // aSeries has point at that time, cSeries doesn't - const result = getMultiSeriesGraphHoverInfo([aValueField!, cValueField!], [aTimeField!, cTimeField!], 20100); - - // we expect the time of the closest point before hover - expect(result.time).toBe('1970-01-01 00:00:20'); - // we expect the closest datapoint before hover from aSeries - expect(result.results[0]).toEqual( - mockResult('20', 1, 0, getFixedThemedColor(aValueField!), aValueField!.name, '1970-01-01 00:00:20') - ); - // we expect the closest datapoint before hover from cSeries (1st point) - expect(result.results[1]).toEqual( - mockResult('30', 0, 1, getFixedThemedColor(cValueField!), cValueField!.name, '1970-01-01 00:00:10') - ); - }); - }); - }); - - describe('findHoverIndexFromData', () => { - it('returns index of the closest datapoint before hover position', () => { - const cache = new FieldCache(aSeries); - const timeField = cache.getFieldByName('time'); - // hovering over 1st datapoint - expect(findHoverIndexFromData(timeField!, 0)).toBe(0); - // hovering over right before 2nd datapoint - expect(findHoverIndexFromData(timeField!, 19900)).toBe(0); - // hovering over 2nd datapoint - expect(findHoverIndexFromData(timeField!, 20000)).toBe(1); - // hovering over right before 3rd datapoint - expect(findHoverIndexFromData(timeField!, 29900)).toBe(1); - // hovering over 3rd datapoint - expect(findHoverIndexFromData(timeField!, 30000)).toBe(2); - }); - }); - - describe('graphTimeFormat', () => { - it('graphTimeFormat', () => { - expect(graphTimeFormat(5, 1, 45 * 5 * 1000)).toBe('HH:mm:ss'); - expect(graphTimeFormat(5, 1, 7200 * 5 * 1000)).toBe('HH:mm'); - expect(graphTimeFormat(5, 1, 80000 * 5 * 1000)).toBe('MM/DD HH:mm'); - expect(graphTimeFormat(5, 1, 2419200 * 5 * 1000)).toBe('MM/DD'); - expect(graphTimeFormat(5, 1, 12419200 * 5 * 1000)).toBe('YYYY-MM'); - }); - }); -}); diff --git a/packages/grafana-ui/src/graveyard/Graph/utils.ts b/packages/grafana-ui/src/graveyard/Graph/utils.ts deleted file mode 100644 index ebc34ad470..0000000000 --- a/packages/grafana-ui/src/graveyard/Graph/utils.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { - GraphSeriesValue, - Field, - formattedValueToString, - getFieldDisplayName, - TimeZone, - dateTimeFormat, - systemDateFormats, -} from '@grafana/data'; - -/** - * Returns index of the closest datapoint BEFORE hover position - * - * @param posX - * @param series - * @deprecated - */ -export const findHoverIndexFromData = (xAxisDimension: Field, xPos: number) => { - let lower = 0; - let upper = xAxisDimension.values.length - 1; - let middle; - - while (true) { - if (lower > upper) { - return Math.max(upper, 0); - } - middle = Math.floor((lower + upper) / 2); - const xPosition = xAxisDimension.values[middle]; - - if (xPosition === xPos) { - return middle; - } else if (xPosition && xPosition < xPos) { - lower = middle + 1; - } else { - upper = middle - 1; - } - } -}; - -interface MultiSeriesHoverInfo { - value: string; - time: string; - datapointIndex: number; - seriesIndex: number; - label?: string; - color?: string; -} - -/** - * Returns information about closest datapoints when hovering over a Graph - * - * @param seriesList list of series visible on the Graph - * @param pos mouse cursor position, based on jQuery.flot position - * @deprecated - */ -export const getMultiSeriesGraphHoverInfo = ( - // x and y axis dimensions order is aligned - yAxisDimensions: Field[], - xAxisDimensions: Field[], - /** Well, time basically */ - xAxisPosition: number, - timeZone?: TimeZone -): { - results: MultiSeriesHoverInfo[]; - time?: GraphSeriesValue; -} => { - let i, field, hoverIndex, hoverDistance, pointTime; - - const results: MultiSeriesHoverInfo[] = []; - - let minDistance, minTime; - - for (i = 0; i < yAxisDimensions.length; i++) { - field = yAxisDimensions[i]; - const time = xAxisDimensions[i]; - hoverIndex = findHoverIndexFromData(time, xAxisPosition); - hoverDistance = xAxisPosition - time.values[hoverIndex]; - pointTime = time.values[hoverIndex]; - // Take the closest point before the cursor, or if it does not exist, the closest after - if ( - minDistance === undefined || - (hoverDistance >= 0 && (hoverDistance < minDistance || minDistance < 0)) || - (hoverDistance < 0 && hoverDistance > minDistance) - ) { - minDistance = hoverDistance; - minTime = time.display ? formattedValueToString(time.display(pointTime)) : pointTime; - } - - const disp = field.display!(field.values[hoverIndex]); - - results.push({ - value: formattedValueToString(disp), - datapointIndex: hoverIndex, - seriesIndex: i, - color: disp.color, - label: getFieldDisplayName(field), - time: time.display ? formattedValueToString(time.display(pointTime)) : pointTime, - }); - } - - return { - results, - time: minTime, - }; -}; - -/** @deprecated */ -export const graphTickFormatter = (epoch: number, axis: any) => { - return dateTimeFormat(epoch, { - format: axis?.options?.timeformat, - timeZone: axis?.options?.timezone, - }); -}; - -/** @deprecated */ -export const graphTimeFormat = (ticks: number | null, min: number | null, max: number | null): string => { - if (min && max && ticks) { - const range = max - min; - const secPerTick = range / ticks / 1000; - // Need have 10 millisecond margin on the day range - // As sometimes last 24 hour dashboard evaluates to more than 86400000 - const oneDay = 86400010; - const oneYear = 31536000000; - - if (secPerTick <= 10) { - return systemDateFormats.interval.millisecond; - } - if (secPerTick <= 45) { - return systemDateFormats.interval.second; - } - if (range <= oneDay) { - return systemDateFormats.interval.minute; - } - if (secPerTick <= 80000) { - return systemDateFormats.interval.hour; - } - if (range <= oneYear) { - return systemDateFormats.interval.day; - } - if (secPerTick <= 31536000) { - return systemDateFormats.interval.month; - } - return systemDateFormats.interval.year; - } - - return systemDateFormats.interval.minute; -}; diff --git a/packages/grafana-ui/src/graveyard/GraphNG/GraphNG.tsx b/packages/grafana-ui/src/graveyard/GraphNG/GraphNG.tsx deleted file mode 100644 index 52dfadb27a..0000000000 --- a/packages/grafana-ui/src/graveyard/GraphNG/GraphNG.tsx +++ /dev/null @@ -1,275 +0,0 @@ -import React, { Component } from 'react'; -import { Subscription } from 'rxjs'; -import { throttleTime } from 'rxjs/operators'; -import uPlot, { AlignedData } from 'uplot'; - -import { - DataFrame, - DataHoverClearEvent, - DataHoverEvent, - Field, - FieldMatcherID, - fieldMatchers, - FieldType, - LegacyGraphHoverEvent, - TimeRange, - TimeZone, -} from '@grafana/data'; -import { VizLegendOptions } from '@grafana/schema'; - -import { PanelContext, PanelContextRoot } from '../../components/PanelChrome/PanelContext'; -import { VizLayout } from '../../components/VizLayout/VizLayout'; -import { UPlotChart } from '../../components/uPlot/Plot'; -import { AxisProps } from '../../components/uPlot/config/UPlotAxisBuilder'; -import { Renderers, UPlotConfigBuilder } from '../../components/uPlot/config/UPlotConfigBuilder'; -import { ScaleProps } from '../../components/uPlot/config/UPlotScaleBuilder'; -import { findMidPointYPosition, pluginLog } from '../../components/uPlot/utils'; -import { Themeable2 } from '../../types'; - -import { GraphNGLegendEvent, XYFieldMatchers } from './types'; -import { preparePlotFrame as defaultPreparePlotFrame } from './utils'; - -/** - * @deprecated - * @internal -- not a public API - */ -export type PropDiffFn = (prev: T, next: T) => boolean; - -/** @deprecated */ -export interface GraphNGProps extends Themeable2 { - frames: DataFrame[]; - structureRev?: number; // a number that will change when the frames[] structure changes - width: number; - height: number; - timeRange: TimeRange; - timeZone: TimeZone[] | TimeZone; - legend: VizLegendOptions; - fields?: XYFieldMatchers; // default will assume timeseries data - renderers?: Renderers; - tweakScale?: (opts: ScaleProps, forField: Field) => ScaleProps; - tweakAxis?: (opts: AxisProps, forField: Field) => AxisProps; - onLegendClick?: (event: GraphNGLegendEvent) => void; - children?: (builder: UPlotConfigBuilder, alignedFrame: DataFrame) => React.ReactNode; - prepConfig: (alignedFrame: DataFrame, allFrames: DataFrame[], getTimeRange: () => TimeRange) => UPlotConfigBuilder; - propsToDiff?: Array; - preparePlotFrame?: (frames: DataFrame[], dimFields: XYFieldMatchers) => DataFrame | null; - renderLegend: (config: UPlotConfigBuilder) => React.ReactElement | null; - - /** - * needed for propsToDiff to re-init the plot & config - * this is a generic approach to plot re-init, without having to specify which panel-level options - * should cause invalidation. we can drop this in favor of something like panelOptionsRev that gets passed in - * similar to structureRev. then we can drop propsToDiff entirely. - */ - options?: Record; -} - -function sameProps(prevProps: any, nextProps: any, propsToDiff: Array = []) { - for (const propName of propsToDiff) { - if (typeof propName === 'function') { - if (!propName(prevProps, nextProps)) { - return false; - } - } else if (nextProps[propName] !== prevProps[propName]) { - return false; - } - } - - return true; -} - -/** - * @internal -- not a public API - * @deprecated - */ -export interface GraphNGState { - alignedFrame: DataFrame; - alignedData?: AlignedData; - config?: UPlotConfigBuilder; -} - -/** - * "Time as X" core component, expects ascending x - * @deprecated - */ -export class GraphNG extends Component { - static contextType = PanelContextRoot; - panelContext: PanelContext = {} as PanelContext; - private plotInstance: React.RefObject; - - private subscription = new Subscription(); - - constructor(props: GraphNGProps) { - super(props); - let state = this.prepState(props); - state.alignedData = state.config!.prepData!([state.alignedFrame]) as AlignedData; - this.state = state; - this.plotInstance = React.createRef(); - } - - getTimeRange = () => this.props.timeRange; - - prepState(props: GraphNGProps, withConfig = true) { - let state: GraphNGState = null as any; - - const { frames, fields, preparePlotFrame } = props; - - const preparePlotFrameFn = preparePlotFrame || defaultPreparePlotFrame; - - const alignedFrame = preparePlotFrameFn( - frames, - fields || { - x: fieldMatchers.get(FieldMatcherID.firstTimeField).get({}), - y: fieldMatchers.get(FieldMatcherID.byTypes).get(new Set([FieldType.number, FieldType.enum])), - }, - props.timeRange - ); - pluginLog('GraphNG', false, 'data aligned', alignedFrame); - - if (alignedFrame) { - let config = this.state?.config; - - if (withConfig) { - config = props.prepConfig(alignedFrame, this.props.frames, this.getTimeRange); - pluginLog('GraphNG', false, 'config prepared', config); - } - - state = { - alignedFrame, - config, - }; - - pluginLog('GraphNG', false, 'data prepared', state.alignedData); - } - - return state; - } - - handleCursorUpdate(evt: DataHoverEvent | LegacyGraphHoverEvent) { - const time = evt.payload?.point?.time; - const u = this.plotInstance.current; - if (u && time) { - // Try finding left position on time axis - const left = u.valToPos(time, 'x'); - let top; - if (left) { - // find midpoint between points at current idx - top = findMidPointYPosition(u, u.posToIdx(left)); - } - - if (!top || !left) { - return; - } - - u.setCursor({ - left, - top, - }); - } - } - - componentDidMount() { - this.panelContext = this.context as PanelContext; - const { eventBus } = this.panelContext; - - this.subscription.add( - eventBus - .getStream(DataHoverEvent) - .pipe(throttleTime(50)) - .subscribe({ - next: (evt) => { - if (eventBus === evt.origin) { - return; - } - this.handleCursorUpdate(evt); - }, - }) - ); - - // Legacy events (from flot graph) - this.subscription.add( - eventBus - .getStream(LegacyGraphHoverEvent) - .pipe(throttleTime(50)) - .subscribe({ - next: (evt) => this.handleCursorUpdate(evt), - }) - ); - - this.subscription.add( - eventBus - .getStream(DataHoverClearEvent) - .pipe(throttleTime(50)) - .subscribe({ - next: () => { - const u = this.plotInstance?.current; - - // @ts-ignore - if (u && !u.cursor._lock) { - u.setCursor({ - left: -10, - top: -10, - }); - } - }, - }) - ); - } - - componentDidUpdate(prevProps: GraphNGProps) { - const { frames, structureRev, timeZone, propsToDiff } = this.props; - - const propsChanged = !sameProps(prevProps, this.props, propsToDiff); - - if (frames !== prevProps.frames || propsChanged || timeZone !== prevProps.timeZone) { - let newState = this.prepState(this.props, false); - - if (newState) { - const shouldReconfig = - this.state.config === undefined || - timeZone !== prevProps.timeZone || - structureRev !== prevProps.structureRev || - !structureRev || - propsChanged; - - if (shouldReconfig) { - newState.config = this.props.prepConfig(newState.alignedFrame, this.props.frames, this.getTimeRange); - pluginLog('GraphNG', false, 'config recreated', newState.config); - } - - newState.alignedData = newState.config!.prepData!([newState.alignedFrame]) as AlignedData; - - this.setState(newState); - } - } - } - - componentWillUnmount() { - this.subscription.unsubscribe(); - } - - render() { - const { width, height, children, renderLegend } = this.props; - const { config, alignedFrame, alignedData } = this.state; - - if (!config) { - return null; - } - - return ( - - {(vizWidth: number, vizHeight: number) => ( - ((this.plotInstance as React.MutableRefObject).current = u)} - > - {children ? children(config, alignedFrame) : null} - - )} - - ); - } -} diff --git a/packages/grafana-ui/src/graveyard/GraphNG/__snapshots__/utils.test.ts.snap b/packages/grafana-ui/src/graveyard/GraphNG/__snapshots__/utils.test.ts.snap deleted file mode 100644 index 3b9265254b..0000000000 --- a/packages/grafana-ui/src/graveyard/GraphNG/__snapshots__/utils.test.ts.snap +++ /dev/null @@ -1,245 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`GraphNG utils preparePlotConfigBuilder 1`] = ` -{ - "axes": [ - { - "filter": undefined, - "font": "12px "Inter", "Helvetica", "Arial", sans-serif", - "gap": 5, - "grid": { - "show": true, - "stroke": "rgba(240, 250, 255, 0.09)", - "width": 1, - }, - "incrs": undefined, - "labelGap": 0, - "rotate": undefined, - "scale": "x", - "show": true, - "side": 2, - "size": [Function], - "space": [Function], - "splits": undefined, - "stroke": "rgb(204, 204, 220)", - "ticks": { - "show": true, - "size": 4, - "stroke": "rgba(240, 250, 255, 0.09)", - "width": 1, - }, - "timeZone": "utc", - "values": [Function], - }, - { - "filter": undefined, - "font": "12px "Inter", "Helvetica", "Arial", sans-serif", - "gap": 5, - "grid": { - "show": true, - "stroke": "rgba(240, 250, 255, 0.09)", - "width": 1, - }, - "incrs": undefined, - "labelGap": 0, - "rotate": undefined, - "scale": "__fixed/na-na/na-na/auto/linear/na/number", - "show": true, - "side": 3, - "size": [Function], - "space": [Function], - "splits": undefined, - "stroke": "rgb(204, 204, 220)", - "ticks": { - "show": false, - "size": 4, - "stroke": "rgb(204, 204, 220)", - "width": 1, - }, - "timeZone": undefined, - "values": [Function], - }, - ], - "cursor": { - "dataIdx": [Function], - "drag": { - "setScale": false, - }, - "focus": { - "prox": 30, - }, - "points": { - "fill": [Function], - "size": [Function], - "stroke": [Function], - "width": [Function], - }, - "sync": { - "filters": { - "pub": [Function], - }, - "key": "__global_", - "scales": [ - "x", - "__fixed/na-na/na-na/auto/linear/na/number", - ], - }, - }, - "focus": { - "alpha": 1, - }, - "hooks": {}, - "legend": { - "show": false, - }, - "mode": 1, - "ms": 1, - "padding": [ - [Function], - [Function], - [Function], - [Function], - ], - "scales": { - "__fixed/na-na/na-na/auto/linear/na/number": { - "asinh": undefined, - "auto": true, - "dir": 1, - "distr": 1, - "log": undefined, - "ori": 1, - "range": [Function], - "time": undefined, - }, - "x": { - "auto": false, - "dir": 1, - "ori": 0, - "range": [Function], - "time": true, - }, - }, - "select": undefined, - "series": [ - { - "value": [Function], - }, - { - "dash": [ - 1, - 2, - ], - "facets": undefined, - "fill": [Function], - "paths": [Function], - "points": { - "fill": "#ff0000", - "filter": [Function], - "show": true, - "size": undefined, - "stroke": "#ff0000", - }, - "pxAlign": undefined, - "scale": "__fixed/na-na/na-na/auto/linear/na/number", - "show": true, - "spanGaps": false, - "stroke": "#ff0000", - "value": [Function], - "width": 2, - }, - { - "dash": [ - 1, - 2, - ], - "facets": undefined, - "fill": [Function], - "paths": [Function], - "points": { - "fill": [Function], - "filter": [Function], - "show": true, - "size": undefined, - "stroke": [Function], - }, - "pxAlign": undefined, - "scale": "__fixed/na-na/na-na/auto/linear/na/number", - "show": true, - "spanGaps": false, - "stroke": [Function], - "value": [Function], - "width": 2, - }, - { - "dash": [ - 1, - 2, - ], - "facets": undefined, - "fill": [Function], - "paths": [Function], - "points": { - "fill": "#ff0000", - "filter": [Function], - "show": true, - "size": undefined, - "stroke": "#ff0000", - }, - "pxAlign": undefined, - "scale": "__fixed/na-na/na-na/auto/linear/na/number", - "show": true, - "spanGaps": false, - "stroke": "#ff0000", - "value": [Function], - "width": 2, - }, - { - "dash": [ - 1, - 2, - ], - "facets": undefined, - "fill": [Function], - "paths": [Function], - "points": { - "fill": [Function], - "filter": [Function], - "show": true, - "size": undefined, - "stroke": [Function], - }, - "pxAlign": undefined, - "scale": "__fixed/na-na/na-na/auto/linear/na/number", - "show": true, - "spanGaps": false, - "stroke": [Function], - "value": [Function], - "width": 2, - }, - { - "dash": [ - 1, - 2, - ], - "facets": undefined, - "fill": [Function], - "paths": [Function], - "points": { - "fill": [Function], - "filter": [Function], - "show": true, - "size": undefined, - "stroke": [Function], - }, - "pxAlign": undefined, - "scale": "__fixed/na-na/na-na/auto/linear/na/number", - "show": true, - "spanGaps": false, - "stroke": [Function], - "value": [Function], - "width": 2, - }, - ], - "tzDate": [Function], -} -`; diff --git a/packages/grafana-ui/src/graveyard/GraphNG/hooks.ts b/packages/grafana-ui/src/graveyard/GraphNG/hooks.ts deleted file mode 100644 index e4f1d46550..0000000000 --- a/packages/grafana-ui/src/graveyard/GraphNG/hooks.ts +++ /dev/null @@ -1,41 +0,0 @@ -import React, { useCallback, useContext } from 'react'; - -import { DataFrame, DataFrameFieldIndex, Field } from '@grafana/data'; - -import { XYFieldMatchers } from './types'; - -/** @deprecated */ -interface GraphNGContextType { - mapSeriesIndexToDataFrameFieldIndex: (index: number) => DataFrameFieldIndex; - dimFields: XYFieldMatchers; - data: DataFrame; -} - -/** @deprecated */ -export const GraphNGContext = React.createContext({} as GraphNGContextType); - -/** @deprecated */ -export const useGraphNGContext = () => { - const { data, dimFields, mapSeriesIndexToDataFrameFieldIndex } = useContext(GraphNGContext); - - const getXAxisField = useCallback(() => { - const xFieldMatcher = dimFields.x; - let xField: Field | null = null; - - for (let j = 0; j < data.fields.length; j++) { - if (xFieldMatcher(data.fields[j], data, [data])) { - xField = data.fields[j]; - break; - } - } - - return xField; - }, [data, dimFields]); - - return { - dimFields, - mapSeriesIndexToDataFrameFieldIndex, - getXAxisField, - alignedData: data, - }; -}; diff --git a/packages/grafana-ui/src/graveyard/GraphNG/utils.test.ts b/packages/grafana-ui/src/graveyard/GraphNG/utils.test.ts deleted file mode 100644 index 9675cd7ca5..0000000000 --- a/packages/grafana-ui/src/graveyard/GraphNG/utils.test.ts +++ /dev/null @@ -1,522 +0,0 @@ -import { - createTheme, - DashboardCursorSync, - DataFrame, - DefaultTimeZone, - EventBusSrv, - FieldColorModeId, - FieldConfig, - FieldMatcherID, - fieldMatchers, - FieldType, - getDefaultTimeRange, - MutableDataFrame, -} from '@grafana/data'; -import { - BarAlignment, - GraphDrawStyle, - GraphFieldConfig, - GraphGradientMode, - LineInterpolation, - VisibilityMode, - StackingMode, -} from '@grafana/schema'; - -import { preparePlotConfigBuilder } from '../TimeSeries/utils'; - -import { preparePlotFrame } from './utils'; - -function mockDataFrame() { - const df1 = new MutableDataFrame({ - refId: 'A', - fields: [{ name: 'ts', type: FieldType.time, values: [1, 2, 3] }], - }); - const df2 = new MutableDataFrame({ - refId: 'B', - fields: [{ name: 'ts', type: FieldType.time, values: [1, 2, 4] }], - }); - - const f1Config: FieldConfig = { - displayName: 'Metric 1', - color: { - mode: FieldColorModeId.Fixed, - }, - decimals: 2, - custom: { - drawStyle: GraphDrawStyle.Line, - gradientMode: GraphGradientMode.Opacity, - lineColor: '#ff0000', - lineWidth: 2, - lineInterpolation: LineInterpolation.Linear, - lineStyle: { - fill: 'dash', - dash: [1, 2], - }, - spanNulls: false, - fillColor: '#ff0000', - fillOpacity: 0.1, - showPoints: VisibilityMode.Always, - stacking: { - group: 'A', - mode: StackingMode.Normal, - }, - }, - }; - - const f2Config: FieldConfig = { - displayName: 'Metric 2', - color: { - mode: FieldColorModeId.Fixed, - }, - decimals: 2, - custom: { - drawStyle: GraphDrawStyle.Bars, - gradientMode: GraphGradientMode.Hue, - lineColor: '#ff0000', - lineWidth: 2, - lineInterpolation: LineInterpolation.Linear, - lineStyle: { - fill: 'dash', - dash: [1, 2], - }, - barAlignment: BarAlignment.Before, - fillColor: '#ff0000', - fillOpacity: 0.1, - showPoints: VisibilityMode.Always, - stacking: { - group: 'A', - mode: StackingMode.Normal, - }, - }, - }; - - const f3Config: FieldConfig = { - displayName: 'Metric 3', - decimals: 2, - color: { - mode: FieldColorModeId.Fixed, - }, - custom: { - drawStyle: GraphDrawStyle.Line, - gradientMode: GraphGradientMode.Opacity, - lineColor: '#ff0000', - lineWidth: 2, - lineInterpolation: LineInterpolation.Linear, - lineStyle: { - fill: 'dash', - dash: [1, 2], - }, - spanNulls: false, - fillColor: '#ff0000', - fillOpacity: 0.1, - showPoints: VisibilityMode.Always, - stacking: { - group: 'B', - mode: StackingMode.Normal, - }, - }, - }; - const f4Config: FieldConfig = { - displayName: 'Metric 4', - decimals: 2, - color: { - mode: FieldColorModeId.Fixed, - }, - custom: { - drawStyle: GraphDrawStyle.Bars, - gradientMode: GraphGradientMode.Hue, - lineColor: '#ff0000', - lineWidth: 2, - lineInterpolation: LineInterpolation.Linear, - lineStyle: { - fill: 'dash', - dash: [1, 2], - }, - barAlignment: BarAlignment.Before, - fillColor: '#ff0000', - fillOpacity: 0.1, - showPoints: VisibilityMode.Always, - stacking: { - group: 'B', - mode: StackingMode.Normal, - }, - }, - }; - const f5Config: FieldConfig = { - displayName: 'Metric 4', - decimals: 2, - color: { - mode: FieldColorModeId.Fixed, - }, - custom: { - drawStyle: GraphDrawStyle.Bars, - gradientMode: GraphGradientMode.Hue, - lineColor: '#ff0000', - lineWidth: 2, - lineInterpolation: LineInterpolation.Linear, - lineStyle: { - fill: 'dash', - dash: [1, 2], - }, - barAlignment: BarAlignment.Before, - fillColor: '#ff0000', - fillOpacity: 0.1, - showPoints: VisibilityMode.Always, - stacking: { - group: 'B', - mode: StackingMode.None, - }, - }, - }; - - df1.addField({ - name: 'metric1', - type: FieldType.number, - config: f1Config, - }); - - df2.addField({ - name: 'metric2', - type: FieldType.number, - config: f2Config, - }); - df2.addField({ - name: 'metric3', - type: FieldType.number, - config: f3Config, - }); - df2.addField({ - name: 'metric4', - type: FieldType.number, - config: f4Config, - }); - df2.addField({ - name: 'metric5', - type: FieldType.number, - config: f5Config, - }); - - return preparePlotFrame([df1, df2], { - x: fieldMatchers.get(FieldMatcherID.firstTimeField).get({}), - y: fieldMatchers.get(FieldMatcherID.numeric).get({}), - }); -} - -jest.mock('@grafana/data', () => ({ - ...jest.requireActual('@grafana/data'), - DefaultTimeZone: 'utc', -})); - -describe('GraphNG utils', () => { - test('preparePlotConfigBuilder', () => { - const frame = mockDataFrame(); - const result = preparePlotConfigBuilder({ - frame: frame!, - theme: createTheme(), - timeZones: [DefaultTimeZone], - getTimeRange: getDefaultTimeRange, - eventBus: new EventBusSrv(), - sync: () => DashboardCursorSync.Tooltip, - allFrames: [frame!], - }).getConfig(); - expect(result).toMatchSnapshot(); - }); - - test('preparePlotFrame appends min bar spaced nulls when > 1 bar series', () => { - const df1: DataFrame = { - name: 'A', - length: 5, - fields: [ - { - name: 'time', - type: FieldType.time, - config: {}, - values: [1, 2, 4, 6, 100], // should find smallest delta === 1 from here - }, - { - name: 'value', - type: FieldType.number, - config: { - custom: { - drawStyle: GraphDrawStyle.Bars, - }, - }, - values: [1, 1, 1, 1, 1], - }, - ], - }; - - const df2: DataFrame = { - name: 'B', - length: 5, - fields: [ - { - name: 'time', - type: FieldType.time, - config: {}, - values: [30, 40, 50, 90, 100], // should be appended with two smallest-delta increments - }, - { - name: 'value', - type: FieldType.number, - config: { - custom: { - drawStyle: GraphDrawStyle.Bars, - }, - }, - values: [2, 2, 2, 2, 2], // bar series should be appended with nulls - }, - { - name: 'value', - type: FieldType.number, - config: { - custom: { - drawStyle: GraphDrawStyle.Line, - }, - }, - values: [3, 3, 3, 3, 3], // line series should be appended with undefineds - }, - ], - }; - - const df3: DataFrame = { - name: 'C', - length: 2, - fields: [ - { - name: 'time', - type: FieldType.time, - config: {}, - values: [1, 1.1], // should not trip up on smaller deltas of non-bars - }, - { - name: 'value', - type: FieldType.number, - config: { - custom: { - drawStyle: GraphDrawStyle.Line, - }, - }, - values: [4, 4], - }, - { - name: 'value', - type: FieldType.number, - config: { - custom: { - drawStyle: GraphDrawStyle.Bars, - hideFrom: { - viz: true, // should ignore hidden bar series - }, - }, - }, - values: [4, 4], - }, - ], - }; - - let aligndFrame = preparePlotFrame([df1, df2, df3], { - x: fieldMatchers.get(FieldMatcherID.firstTimeField).get({}), - y: fieldMatchers.get(FieldMatcherID.numeric).get({}), - }); - - expect(aligndFrame).toMatchInlineSnapshot(` - { - "fields": [ - { - "config": {}, - "name": "time", - "state": { - "nullThresholdApplied": true, - "origin": { - "fieldIndex": 0, - "frameIndex": 0, - }, - }, - "type": "time", - "values": [ - 1, - 1.1, - 2, - 4, - 6, - 30, - 40, - 50, - 90, - 100, - 101, - 102, - ], - }, - { - "config": { - "custom": { - "drawStyle": "bars", - "spanNulls": -1, - }, - }, - "labels": { - "name": "A", - }, - "name": "value", - "state": { - "origin": { - "fieldIndex": 1, - "frameIndex": 0, - }, - }, - "type": "number", - "values": [ - 1, - undefined, - 1, - 1, - 1, - undefined, - undefined, - undefined, - undefined, - 1, - null, - null, - ], - }, - { - "config": { - "custom": { - "drawStyle": "bars", - "spanNulls": -1, - }, - }, - "labels": { - "name": "B", - }, - "name": "value", - "state": { - "origin": { - "fieldIndex": 1, - "frameIndex": 1, - }, - }, - "type": "number", - "values": [ - undefined, - undefined, - undefined, - undefined, - undefined, - 2, - 2, - 2, - 2, - 2, - null, - null, - ], - }, - { - "config": { - "custom": { - "drawStyle": "line", - }, - }, - "labels": { - "name": "B", - }, - "name": "value", - "state": { - "origin": { - "fieldIndex": 2, - "frameIndex": 1, - }, - }, - "type": "number", - "values": [ - undefined, - undefined, - undefined, - undefined, - undefined, - 3, - 3, - 3, - 3, - 3, - undefined, - undefined, - ], - }, - { - "config": { - "custom": { - "drawStyle": "line", - }, - }, - "labels": { - "name": "C", - }, - "name": "value", - "state": { - "origin": { - "fieldIndex": 1, - "frameIndex": 2, - }, - }, - "type": "number", - "values": [ - 4, - 4, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - ], - }, - { - "config": { - "custom": { - "drawStyle": "bars", - "hideFrom": { - "viz": true, - }, - }, - }, - "labels": { - "name": "C", - }, - "name": "value", - "state": { - "origin": { - "fieldIndex": 2, - "frameIndex": 2, - }, - }, - "type": "number", - "values": [ - 4, - 4, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - ], - }, - ], - "length": 12, - } - `); - }); -}); diff --git a/packages/grafana-ui/src/graveyard/README.md b/packages/grafana-ui/src/graveyard/README.md index 2715ecec38..627570b3aa 100644 --- a/packages/grafana-ui/src/graveyard/README.md +++ b/packages/grafana-ui/src/graveyard/README.md @@ -1 +1,4 @@ Items in this folder are all deprecated and will be removed in the future + +NOTE: GraphNG is include, but not exported. It contains some complex function that are +used in the uPlot helper bundles, but also duplicated in grafana core diff --git a/packages/grafana-ui/src/graveyard/TimeSeries/TimeSeries.tsx b/packages/grafana-ui/src/graveyard/TimeSeries/TimeSeries.tsx deleted file mode 100644 index 9a748236d7..0000000000 --- a/packages/grafana-ui/src/graveyard/TimeSeries/TimeSeries.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import React, { Component } from 'react'; - -import { DataFrame, TimeRange } from '@grafana/data'; - -import { PanelContextRoot } from '../../components/PanelChrome/PanelContext'; -import { hasVisibleLegendSeries, PlotLegend } from '../../components/uPlot/PlotLegend'; -import { UPlotConfigBuilder } from '../../components/uPlot/config/UPlotConfigBuilder'; -import { withTheme2 } from '../../themes/ThemeContext'; -import { GraphNG, GraphNGProps, PropDiffFn } from '../GraphNG/GraphNG'; - -import { preparePlotConfigBuilder } from './utils'; - -const propsToDiff: Array = ['legend', 'options', 'theme']; - -type TimeSeriesProps = Omit; - -export class UnthemedTimeSeries extends Component { - static contextType = PanelContextRoot; - declare context: React.ContextType; - - prepConfig = (alignedFrame: DataFrame, allFrames: DataFrame[], getTimeRange: () => TimeRange) => { - const { eventBus, eventsScope, sync } = this.context; - const { theme, timeZone, renderers, tweakAxis, tweakScale } = this.props; - - return preparePlotConfigBuilder({ - frame: alignedFrame, - theme, - timeZones: Array.isArray(timeZone) ? timeZone : [timeZone], - getTimeRange, - eventBus, - sync, - allFrames, - renderers, - tweakScale, - tweakAxis, - eventsScope, - }); - }; - - renderLegend = (config: UPlotConfigBuilder) => { - const { legend, frames } = this.props; - - if (!config || (legend && !legend.showLegend) || !hasVisibleLegendSeries(config, frames)) { - return null; - } - - return ; - }; - - render() { - return ( - - ); - } -} - -export const TimeSeries = withTheme2(UnthemedTimeSeries); -TimeSeries.displayName = 'TimeSeries'; diff --git a/packages/grafana-ui/src/graveyard/TimeSeries/utils.test.ts b/packages/grafana-ui/src/graveyard/TimeSeries/utils.test.ts deleted file mode 100644 index 583358c7c4..0000000000 --- a/packages/grafana-ui/src/graveyard/TimeSeries/utils.test.ts +++ /dev/null @@ -1,274 +0,0 @@ -import { EventBus, FieldType } from '@grafana/data'; -import { getTheme } from '@grafana/ui'; - -import { preparePlotConfigBuilder } from './utils'; - -describe('when fill below to option is used', () => { - let eventBus: EventBus; - // eslint-disable-next-line - let renderers: any[]; - // eslint-disable-next-line - let tests: any; - - beforeEach(() => { - eventBus = { - publish: jest.fn(), - getStream: jest.fn(), - subscribe: jest.fn(), - removeAllListeners: jest.fn(), - newScopedBus: jest.fn(), - }; - renderers = []; - - tests = [ - { - alignedFrame: { - fields: [ - { - config: {}, - values: [1667406900000, 1667407170000, 1667407185000], - name: 'Time', - state: { multipleFrames: true, displayName: 'Time', origin: { fieldIndex: 0, frameIndex: 0 } }, - type: FieldType.time, - }, - { - config: { displayNameFromDS: 'Test1', custom: { fillBelowTo: 'Test2' }, min: 0, max: 100 }, - values: [1, 2, 3], - name: 'Value', - state: { multipleFrames: true, displayName: 'Test1', origin: { fieldIndex: 1, frameIndex: 0 } }, - type: FieldType.number, - }, - { - config: { displayNameFromDS: 'Test2', min: 0, max: 100 }, - values: [4, 5, 6], - name: 'Value', - state: { multipleFrames: true, displayName: 'Test2', origin: { fieldIndex: 1, frameIndex: 1 } }, - type: FieldType.number, - }, - ], - length: 3, - }, - allFrames: [ - { - name: 'Test1', - refId: 'A', - fields: [ - { - config: {}, - values: [1667406900000, 1667407170000, 1667407185000], - name: 'Time', - state: { multipleFrames: true, displayName: 'Time', origin: { fieldIndex: 0, frameIndex: 0 } }, - type: FieldType.time, - }, - { - config: { displayNameFromDS: 'Test1', custom: { fillBelowTo: 'Test2' }, min: 0, max: 100 }, - values: [1, 2, 3], - name: 'Value', - state: { multipleFrames: true, displayName: 'Test1', origin: { fieldIndex: 1, frameIndex: 0 } }, - type: FieldType.number, - }, - ], - length: 2, - }, - { - name: 'Test2', - refId: 'B', - fields: [ - { - config: {}, - values: [1667406900000, 1667407170000, 1667407185000], - name: 'Time', - state: { multipleFrames: true, displayName: 'Time', origin: { fieldIndex: 0, frameIndex: 1 } }, - type: FieldType.time, - }, - { - config: { displayNameFromDS: 'Test2', min: 0, max: 100 }, - values: [1, 2, 3], - name: 'Value', - state: { multipleFrames: true, displayName: 'Test2', origin: { fieldIndex: 1, frameIndex: 1 } }, - type: FieldType.number, - }, - ], - length: 2, - }, - ], - expectedResult: 1, - }, - { - alignedFrame: { - fields: [ - { - config: {}, - values: [1667406900000, 1667407170000, 1667407185000], - name: 'time', - state: { multipleFrames: true, displayName: 'time', origin: { fieldIndex: 0, frameIndex: 0 } }, - type: FieldType.time, - }, - { - config: { custom: { fillBelowTo: 'below_value1' } }, - values: [1, 2, 3], - name: 'value1', - state: { multipleFrames: true, displayName: 'value1', origin: { fieldIndex: 1, frameIndex: 0 } }, - type: FieldType.number, - }, - { - config: { custom: { fillBelowTo: 'below_value2' } }, - values: [4, 5, 6], - name: 'value2', - state: { multipleFrames: true, displayName: 'value2', origin: { fieldIndex: 2, frameIndex: 0 } }, - type: FieldType.number, - }, - { - config: {}, - values: [4, 5, 6], - name: 'below_value1', - state: { multipleFrames: true, displayName: 'below_value1', origin: { fieldIndex: 1, frameIndex: 1 } }, - type: FieldType.number, - }, - { - config: {}, - values: [4, 5, 6], - name: 'below_value2', - state: { multipleFrames: true, displayName: 'below_value2', origin: { fieldIndex: 2, frameIndex: 1 } }, - type: FieldType.number, - }, - ], - length: 5, - }, - allFrames: [ - { - refId: 'A', - fields: [ - { - config: {}, - values: [1667406900000, 1667407170000, 1667407185000], - name: 'time', - state: { multipleFrames: true, displayName: 'time', origin: { fieldIndex: 0, frameIndex: 0 } }, - type: FieldType.time, - }, - { - config: { custom: { fillBelowTo: 'below_value1' } }, - values: [1, 2, 3], - name: 'value1', - state: { multipleFrames: true, displayName: 'value1', origin: { fieldIndex: 1, frameIndex: 0 } }, - type: FieldType.number, - }, - { - config: { custom: { fillBelowTo: 'below_value2' } }, - values: [4, 5, 6], - name: 'value2', - state: { multipleFrames: true, displayName: 'value2', origin: { fieldIndex: 2, frameIndex: 0 } }, - type: FieldType.number, - }, - ], - length: 3, - }, - { - refId: 'B', - fields: [ - { - config: {}, - values: [1667406900000, 1667407170000, 1667407185000], - name: 'time', - state: { multipleFrames: true, displayName: 'time', origin: { fieldIndex: 0, frameIndex: 1 } }, - type: FieldType.time, - }, - { - config: {}, - values: [4, 5, 6], - name: 'below_value1', - state: { multipleFrames: true, displayName: 'below_value1', origin: { fieldIndex: 1, frameIndex: 1 } }, - type: FieldType.number, - }, - { - config: {}, - values: [4, 5, 6], - name: 'below_value2', - state: { multipleFrames: true, displayName: 'below_value2', origin: { fieldIndex: 2, frameIndex: 1 } }, - type: FieldType.number, - }, - ], - length: 3, - }, - ], - expectedResult: 2, - }, - ]; - }); - - it('should verify if fill below to is set then builder bands are set', () => { - for (const test of tests) { - const builder = preparePlotConfigBuilder({ - frame: test.alignedFrame, - //@ts-ignore - theme: getTheme(), - timeZones: ['browser'], - getTimeRange: jest.fn(), - eventBus, - sync: jest.fn(), - allFrames: test.allFrames, - renderers, - }); - - //@ts-ignore - expect(builder.bands.length).toBe(test.expectedResult); - } - }); - - it('should verify if fill below to is not set then builder bands are empty', () => { - tests[0].alignedFrame.fields[1].config.custom.fillBelowTo = undefined; - tests[0].allFrames[0].fields[1].config.custom.fillBelowTo = undefined; - tests[1].alignedFrame.fields[1].config.custom.fillBelowTo = undefined; - tests[1].alignedFrame.fields[2].config.custom.fillBelowTo = undefined; - tests[1].allFrames[0].fields[1].config.custom.fillBelowTo = undefined; - tests[1].allFrames[0].fields[2].config.custom.fillBelowTo = undefined; - tests[0].expectedResult = 0; - tests[1].expectedResult = 0; - - for (const test of tests) { - const builder = preparePlotConfigBuilder({ - frame: test.alignedFrame, - //@ts-ignore - theme: getTheme(), - timeZones: ['browser'], - getTimeRange: jest.fn(), - eventBus, - sync: jest.fn(), - allFrames: test.allFrames, - renderers, - }); - - //@ts-ignore - expect(builder.bands.length).toBe(test.expectedResult); - } - }); - - it('should verify if fill below to is set and field name is overriden then builder bands are set', () => { - tests[0].alignedFrame.fields[2].config.displayName = 'newName'; - tests[0].alignedFrame.fields[2].state.displayName = 'newName'; - tests[0].allFrames[1].fields[1].config.displayName = 'newName'; - tests[0].allFrames[1].fields[1].state.displayName = 'newName'; - - tests[1].alignedFrame.fields[3].config.displayName = 'newName'; - tests[1].alignedFrame.fields[3].state.displayName = 'newName'; - tests[1].allFrames[1].fields[1].config.displayName = 'newName'; - tests[1].allFrames[1].fields[1].state.displayName = 'newName'; - - for (const test of tests) { - const builder = preparePlotConfigBuilder({ - frame: test.alignedFrame, - //@ts-ignore - theme: getTheme(), - timeZones: ['browser'], - getTimeRange: jest.fn(), - eventBus, - sync: jest.fn(), - allFrames: test.allFrames, - renderers, - }); - - //@ts-ignore - expect(builder.bands.length).toBe(test.expectedResult); - } - }); -}); diff --git a/packages/grafana-ui/src/graveyard/TimeSeries/utils.ts b/packages/grafana-ui/src/graveyard/TimeSeries/utils.ts deleted file mode 100644 index 12a6e3908c..0000000000 --- a/packages/grafana-ui/src/graveyard/TimeSeries/utils.ts +++ /dev/null @@ -1,668 +0,0 @@ -import { isNumber } from 'lodash'; -import uPlot from 'uplot'; - -import { - DashboardCursorSync, - DataFrame, - DataHoverClearEvent, - DataHoverEvent, - DataHoverPayload, - FieldConfig, - FieldType, - formattedValueToString, - getFieldColorModeForField, - getFieldSeriesColor, - getFieldDisplayName, - getDisplayProcessor, - FieldColorModeId, - DecimalCount, -} from '@grafana/data'; -import { - AxisPlacement, - GraphDrawStyle, - GraphFieldConfig, - GraphThresholdsStyleMode, - VisibilityMode, - ScaleDirection, - ScaleOrientation, - StackingMode, - GraphTransform, - AxisColorMode, - GraphGradientMode, -} from '@grafana/schema'; - -// unit lookup needed to determine if we want power-of-2 or power-of-10 axis ticks -// see categories.ts is @grafana/data -const IEC_UNITS = new Set([ - 'bytes', - 'bits', - 'kbytes', - 'mbytes', - 'gbytes', - 'tbytes', - 'pbytes', - 'binBps', - 'binbps', - 'KiBs', - 'Kibits', - 'MiBs', - 'Mibits', - 'GiBs', - 'Gibits', - 'TiBs', - 'Tibits', - 'PiBs', - 'Pibits', -]); - -const BIN_INCRS = Array(53); - -for (let i = 0; i < BIN_INCRS.length; i++) { - BIN_INCRS[i] = 2 ** i; -} - -import { UPlotConfigBuilder, UPlotConfigPrepFn } from '../../components/uPlot/config/UPlotConfigBuilder'; -import { getScaleGradientFn } from '../../components/uPlot/config/gradientFills'; -import { getStackingGroups, preparePlotData2 } from '../../components/uPlot/utils'; -import { buildScaleKey } from '../GraphNG/utils'; - -const defaultFormatter = (v: any, decimals: DecimalCount = 1) => (v == null ? '-' : v.toFixed(decimals)); - -const defaultConfig: GraphFieldConfig = { - drawStyle: GraphDrawStyle.Line, - showPoints: VisibilityMode.Auto, - axisPlacement: AxisPlacement.Auto, -}; - -export const preparePlotConfigBuilder: UPlotConfigPrepFn<{ - sync?: () => DashboardCursorSync; -}> = ({ - frame, - theme, - timeZones, - getTimeRange, - eventBus, - sync, - allFrames, - renderers, - tweakScale = (opts) => opts, - tweakAxis = (opts) => opts, - eventsScope = '__global_', -}) => { - const builder = new UPlotConfigBuilder(timeZones[0]); - - let alignedFrame: DataFrame; - - builder.setPrepData((frames) => { - // cache alignedFrame - alignedFrame = frames[0]; - - return preparePlotData2(frames[0], builder.getStackingGroups()); - }); - - // X is the first field in the aligned frame - const xField = frame.fields[0]; - if (!xField) { - return builder; // empty frame with no options - } - - const xScaleKey = 'x'; - let xScaleUnit = '_x'; - let yScaleKey = ''; - - const xFieldAxisPlacement = - xField.config.custom?.axisPlacement !== AxisPlacement.Hidden ? AxisPlacement.Bottom : AxisPlacement.Hidden; - const xFieldAxisShow = xField.config.custom?.axisPlacement !== AxisPlacement.Hidden; - - if (xField.type === FieldType.time) { - xScaleUnit = 'time'; - builder.addScale({ - scaleKey: xScaleKey, - orientation: ScaleOrientation.Horizontal, - direction: ScaleDirection.Right, - isTime: true, - range: () => { - const r = getTimeRange(); - return [r.from.valueOf(), r.to.valueOf()]; - }, - }); - - // filters first 2 ticks to make space for timezone labels - const filterTicks: uPlot.Axis.Filter | undefined = - timeZones.length > 1 - ? (u, splits) => { - return splits.map((v, i) => (i < 2 ? null : v)); - } - : undefined; - - for (let i = 0; i < timeZones.length; i++) { - const timeZone = timeZones[i]; - builder.addAxis({ - scaleKey: xScaleKey, - isTime: true, - placement: xFieldAxisPlacement, - show: xFieldAxisShow, - label: xField.config.custom?.axisLabel, - timeZone, - theme, - grid: { show: i === 0 && xField.config.custom?.axisGridShow }, - filter: filterTicks, - }); - } - - // render timezone labels - if (timeZones.length > 1) { - builder.addHook('drawAxes', (u: uPlot) => { - u.ctx.save(); - - u.ctx.fillStyle = theme.colors.text.primary; - u.ctx.textAlign = 'left'; - u.ctx.textBaseline = 'bottom'; - - let i = 0; - u.axes.forEach((a) => { - if (a.side === 2) { - //@ts-ignore - let cssBaseline: number = a._pos + a._size; - u.ctx.fillText(timeZones[i], u.bbox.left, cssBaseline * uPlot.pxRatio); - i++; - } - }); - - u.ctx.restore(); - }); - } - } else { - // Not time! - if (xField.config.unit) { - xScaleUnit = xField.config.unit; - } - - builder.addScale({ - scaleKey: xScaleKey, - orientation: ScaleOrientation.Horizontal, - direction: ScaleDirection.Right, - range: (u, dataMin, dataMax) => [xField.config.min ?? dataMin, xField.config.max ?? dataMax], - }); - - builder.addAxis({ - scaleKey: xScaleKey, - placement: xFieldAxisPlacement, - show: xFieldAxisShow, - label: xField.config.custom?.axisLabel, - theme, - grid: { show: xField.config.custom?.axisGridShow }, - formatValue: (v, decimals) => formattedValueToString(xField.display!(v, decimals)), - }); - } - - let customRenderedFields = - renderers?.flatMap((r) => Object.values(r.fieldMap).filter((name) => r.indicesOnly.indexOf(name) === -1)) ?? []; - - let indexByName: Map | undefined; - - for (let i = 1; i < frame.fields.length; i++) { - const field = frame.fields[i]; - - const config: FieldConfig = { - ...field.config, - custom: { - ...defaultConfig, - ...field.config.custom, - }, - }; - - const customConfig: GraphFieldConfig = config.custom!; - - if (field === xField || (field.type !== FieldType.number && field.type !== FieldType.enum)) { - continue; - } - - let fmt = field.display ?? defaultFormatter; - if (field.config.custom?.stacking?.mode === StackingMode.Percent) { - fmt = getDisplayProcessor({ - field: { - ...field, - config: { - ...field.config, - unit: 'percentunit', - }, - }, - theme, - }); - } - const scaleKey = buildScaleKey(config, field.type); - const colorMode = getFieldColorModeForField(field); - const scaleColor = getFieldSeriesColor(field, theme); - const seriesColor = scaleColor.color; - - // The builder will manage unique scaleKeys and combine where appropriate - builder.addScale( - tweakScale( - { - scaleKey, - orientation: ScaleOrientation.Vertical, - direction: ScaleDirection.Up, - distribution: customConfig.scaleDistribution?.type, - log: customConfig.scaleDistribution?.log, - linearThreshold: customConfig.scaleDistribution?.linearThreshold, - min: field.config.min, - max: field.config.max, - softMin: customConfig.axisSoftMin, - softMax: customConfig.axisSoftMax, - centeredZero: customConfig.axisCenteredZero, - range: - customConfig.stacking?.mode === StackingMode.Percent - ? (u: uPlot, dataMin: number, dataMax: number) => { - dataMin = dataMin < 0 ? -1 : 0; - dataMax = dataMax > 0 ? 1 : 0; - return [dataMin, dataMax]; - } - : field.type === FieldType.enum - ? (u: uPlot, dataMin: number, dataMax: number) => { - // this is the exhaustive enum (stable) - let len = field.config.type!.enum!.text!.length; - - return [-1, len]; - - // these are only values that are present - // return [dataMin - 1, dataMax + 1] - } - : undefined, - decimals: field.config.decimals, - }, - field - ) - ); - - if (!yScaleKey) { - yScaleKey = scaleKey; - } - - if (customConfig.axisPlacement !== AxisPlacement.Hidden) { - let axisColor: uPlot.Axis.Stroke | undefined; - - if (customConfig.axisColorMode === AxisColorMode.Series) { - if ( - colorMode.isByValue && - field.config.custom?.gradientMode === GraphGradientMode.Scheme && - colorMode.id === FieldColorModeId.Thresholds - ) { - axisColor = getScaleGradientFn(1, theme, colorMode, field.config.thresholds); - } else { - axisColor = seriesColor; - } - } - - const axisDisplayOptions = { - border: { - show: customConfig.axisBorderShow || false, - width: 1 / devicePixelRatio, - stroke: axisColor || theme.colors.text.primary, - }, - ticks: { - show: customConfig.axisBorderShow || false, - stroke: axisColor || theme.colors.text.primary, - }, - color: axisColor || theme.colors.text.primary, - }; - - let incrs: uPlot.Axis.Incrs | undefined; - - // TODO: these will be dynamic with frame updates, so need to accept getYTickLabels() - let values: uPlot.Axis.Values | undefined; - let splits: uPlot.Axis.Splits | undefined; - - if (IEC_UNITS.has(config.unit!)) { - incrs = BIN_INCRS; - } else if (field.type === FieldType.enum) { - let text = field.config.type!.enum!.text!; - splits = text.map((v: string, i: number) => i); - values = text; - } - - builder.addAxis( - tweakAxis( - { - scaleKey, - label: customConfig.axisLabel, - size: customConfig.axisWidth, - placement: customConfig.axisPlacement ?? AxisPlacement.Auto, - formatValue: (v, decimals) => formattedValueToString(fmt(v, decimals)), - theme, - grid: { show: customConfig.axisGridShow }, - decimals: field.config.decimals, - distr: customConfig.scaleDistribution?.type, - splits, - values, - incrs, - ...axisDisplayOptions, - }, - field - ) - ); - } - - const showPoints = - customConfig.drawStyle === GraphDrawStyle.Points ? VisibilityMode.Always : customConfig.showPoints; - - let pointsFilter: uPlot.Series.Points.Filter = () => null; - - if (customConfig.spanNulls !== true) { - pointsFilter = (u, seriesIdx, show, gaps) => { - let filtered = []; - - let series = u.series[seriesIdx]; - - if (!show && gaps && gaps.length) { - const [firstIdx, lastIdx] = series.idxs!; - const xData = u.data[0]; - const yData = u.data[seriesIdx]; - const firstPos = Math.round(u.valToPos(xData[firstIdx], 'x', true)); - const lastPos = Math.round(u.valToPos(xData[lastIdx], 'x', true)); - - if (gaps[0][0] === firstPos) { - filtered.push(firstIdx); - } - - // show single points between consecutive gaps that share end/start - for (let i = 0; i < gaps.length; i++) { - let thisGap = gaps[i]; - let nextGap = gaps[i + 1]; - - if (nextGap && thisGap[1] === nextGap[0]) { - // approx when data density is > 1pt/px, since gap start/end pixels are rounded - let approxIdx = u.posToIdx(thisGap[1], true); - - if (yData[approxIdx] == null) { - // scan left/right alternating to find closest index with non-null value - for (let j = 1; j < 100; j++) { - if (yData[approxIdx + j] != null) { - approxIdx += j; - break; - } - if (yData[approxIdx - j] != null) { - approxIdx -= j; - break; - } - } - } - - filtered.push(approxIdx); - } - } - - if (gaps[gaps.length - 1][1] === lastPos) { - filtered.push(lastIdx); - } - } - - return filtered.length ? filtered : null; - }; - } - - let { fillOpacity } = customConfig; - - let pathBuilder: uPlot.Series.PathBuilder | null = null; - let pointsBuilder: uPlot.Series.Points.Show | null = null; - - if (field.state?.origin) { - if (!indexByName) { - indexByName = getNamesToFieldIndex(frame, allFrames); - } - - const originFrame = allFrames[field.state.origin.frameIndex]; - const originField = originFrame?.fields[field.state.origin.fieldIndex]; - - const dispName = getFieldDisplayName(originField ?? field, originFrame, allFrames); - - // disable default renderers - if (customRenderedFields.indexOf(dispName) >= 0) { - pathBuilder = () => null; - pointsBuilder = () => undefined; - } else if (customConfig.transform === GraphTransform.Constant) { - // patch some monkeys! - const defaultBuilder = uPlot.paths!.linear!(); - - pathBuilder = (u, seriesIdx) => { - //eslint-disable-next-line - const _data: any[] = (u as any)._data; // uplot.AlignedData not exposed in types - - // the data we want the line renderer to pull is x at each plot edge with paired flat y values - - const r = getTimeRange(); - let xData = [r.from.valueOf(), r.to.valueOf()]; - let firstY = _data[seriesIdx].find((v: number | null | undefined) => v != null); - let yData = [firstY, firstY]; - let fauxData = _data.slice(); - fauxData[0] = xData; - fauxData[seriesIdx] = yData; - - //eslint-disable-next-line - return defaultBuilder( - { - ...u, - _data: fauxData, - } as any, - seriesIdx, - 0, - 1 - ); - }; - } - - if (customConfig.fillBelowTo) { - const fillBelowToField = frame.fields.find( - (f) => - customConfig.fillBelowTo === f.name || - customConfig.fillBelowTo === f.config?.displayNameFromDS || - customConfig.fillBelowTo === getFieldDisplayName(f, frame, allFrames) - ); - - const fillBelowDispName = fillBelowToField - ? getFieldDisplayName(fillBelowToField, frame, allFrames) - : customConfig.fillBelowTo; - - const t = indexByName.get(dispName); - const b = indexByName.get(fillBelowDispName); - if (isNumber(b) && isNumber(t)) { - builder.addBand({ - series: [t, b], - fill: undefined, // using null will have the band use fill options from `t` - }); - - if (!fillOpacity) { - fillOpacity = 35; // default from flot - } - } else { - fillOpacity = 0; - } - } - } - - let dynamicSeriesColor: ((seriesIdx: number) => string | undefined) | undefined = undefined; - - if (colorMode.id === FieldColorModeId.Thresholds) { - dynamicSeriesColor = (seriesIdx) => getFieldSeriesColor(alignedFrame.fields[seriesIdx], theme).color; - } - - builder.addSeries({ - pathBuilder, - pointsBuilder, - scaleKey, - showPoints, - pointsFilter, - colorMode, - fillOpacity, - theme, - dynamicSeriesColor, - drawStyle: customConfig.drawStyle!, - lineColor: customConfig.lineColor ?? seriesColor, - lineWidth: customConfig.lineWidth, - lineInterpolation: customConfig.lineInterpolation, - lineStyle: customConfig.lineStyle, - barAlignment: customConfig.barAlignment, - barWidthFactor: customConfig.barWidthFactor, - barMaxWidth: customConfig.barMaxWidth, - pointSize: customConfig.pointSize, - spanNulls: customConfig.spanNulls || false, - show: !customConfig.hideFrom?.viz, - gradientMode: customConfig.gradientMode, - thresholds: config.thresholds, - hardMin: field.config.min, - hardMax: field.config.max, - softMin: customConfig.axisSoftMin, - softMax: customConfig.axisSoftMax, - // The following properties are not used in the uPlot config, but are utilized as transport for legend config - dataFrameFieldIndex: field.state?.origin, - }); - - // Render thresholds in graph - if (customConfig.thresholdsStyle && config.thresholds) { - const thresholdDisplay = customConfig.thresholdsStyle.mode ?? GraphThresholdsStyleMode.Off; - if (thresholdDisplay !== GraphThresholdsStyleMode.Off) { - builder.addThresholds({ - config: customConfig.thresholdsStyle, - thresholds: config.thresholds, - scaleKey, - theme, - hardMin: field.config.min, - hardMax: field.config.max, - softMin: customConfig.axisSoftMin, - softMax: customConfig.axisSoftMax, - }); - } - } - } - - let stackingGroups = getStackingGroups(frame); - - builder.setStackingGroups(stackingGroups); - - // hook up custom/composite renderers - renderers?.forEach((r) => { - if (!indexByName) { - indexByName = getNamesToFieldIndex(frame, allFrames); - } - let fieldIndices: Record = {}; - - for (let key in r.fieldMap) { - let dispName = r.fieldMap[key]; - fieldIndices[key] = indexByName.get(dispName)!; - } - - r.init(builder, fieldIndices); - }); - - builder.scaleKeys = [xScaleKey, yScaleKey]; - - // if hovered value is null, how far we may scan left/right to hover nearest non-null - const hoverProximityPx = 15; - - let cursor: Partial = { - // this scans left and right from cursor position to find nearest data index with value != null - // TODO: do we want to only scan past undefined values, but halt at explicit null values? - dataIdx: (self, seriesIdx, hoveredIdx, cursorXVal) => { - let seriesData = self.data[seriesIdx]; - - if (seriesData[hoveredIdx] == null) { - let nonNullLft = null, - nonNullRgt = null, - i; - - i = hoveredIdx; - while (nonNullLft == null && i-- > 0) { - if (seriesData[i] != null) { - nonNullLft = i; - } - } - - i = hoveredIdx; - while (nonNullRgt == null && i++ < seriesData.length) { - if (seriesData[i] != null) { - nonNullRgt = i; - } - } - - let xVals = self.data[0]; - - let curPos = self.valToPos(cursorXVal, 'x'); - let rgtPos = nonNullRgt == null ? Infinity : self.valToPos(xVals[nonNullRgt], 'x'); - let lftPos = nonNullLft == null ? -Infinity : self.valToPos(xVals[nonNullLft], 'x'); - - let lftDelta = curPos - lftPos; - let rgtDelta = rgtPos - curPos; - - if (lftDelta <= rgtDelta) { - if (lftDelta <= hoverProximityPx) { - hoveredIdx = nonNullLft!; - } - } else { - if (rgtDelta <= hoverProximityPx) { - hoveredIdx = nonNullRgt!; - } - } - } - - return hoveredIdx; - }, - }; - - if (sync && sync() !== DashboardCursorSync.Off) { - const payload: DataHoverPayload = { - point: { - [xScaleKey]: null, - [yScaleKey]: null, - }, - data: frame, - }; - - const hoverEvent = new DataHoverEvent(payload); - cursor.sync = { - key: eventsScope, - filters: { - pub: (type: string, src: uPlot, x: number, y: number, w: number, h: number, dataIdx: number) => { - if (sync && sync() === DashboardCursorSync.Off) { - return false; - } - - payload.rowIndex = dataIdx; - if (x < 0 && y < 0) { - payload.point[xScaleUnit] = null; - payload.point[yScaleKey] = null; - eventBus.publish(new DataHoverClearEvent()); - } else { - // convert the points - payload.point[xScaleUnit] = src.posToVal(x, xScaleKey); - payload.point[yScaleKey] = src.posToVal(y, yScaleKey); - payload.point.panelRelY = y > 0 ? y / h : 1; // used by old graph panel to position tooltip - eventBus.publish(hoverEvent); - hoverEvent.payload.down = undefined; - } - return true; - }, - }, - scales: [xScaleKey, yScaleKey], - // match: [() => true, (a, b) => a === b], - }; - } - - builder.setSync(); - builder.setCursor(cursor); - - return builder; -}; - -export function getNamesToFieldIndex(frame: DataFrame, allFrames: DataFrame[]): Map { - const originNames = new Map(); - frame.fields.forEach((field, i) => { - const origin = field.state?.origin; - if (origin) { - const origField = allFrames[origin.frameIndex]?.fields[origin.fieldIndex]; - if (origField) { - originNames.set(getFieldDisplayName(origField, allFrames[origin.frameIndex], allFrames), i); - } - } - }); - return originNames; -} diff --git a/public/app/angular/angular_wrappers.ts b/public/app/angular/angular_wrappers.ts index fc0263ca32..f337217116 100644 --- a/public/app/angular/angular_wrappers.ts +++ b/public/app/angular/angular_wrappers.ts @@ -3,7 +3,6 @@ import { ColorPicker, DataLinksInlineEditor, DataSourceHttpSettings, - GraphContextMenu, Icon, LegacyForms, SeriesColorPickerPopoverWithTheme, @@ -22,6 +21,8 @@ import { MetricSelect } from '../core/components/Select/MetricSelect'; import { TagFilter } from '../core/components/TagFilter/TagFilter'; import { HelpModal } from '../core/components/help/HelpModal'; +import { GraphContextMenu } from './components/legacy_graph_panel/GraphContextMenu'; + const { SecretFormField } = LegacyForms; export function registerAngularDirectives() { diff --git a/packages/grafana-ui/src/graveyard/Graph/GraphContextMenu.tsx b/public/app/angular/components/legacy_graph_panel/GraphContextMenu.tsx similarity index 86% rename from packages/grafana-ui/src/graveyard/Graph/GraphContextMenu.tsx rename to public/app/angular/components/legacy_graph_panel/GraphContextMenu.tsx index 5b3da31708..8d828e6bdf 100644 --- a/packages/grafana-ui/src/graveyard/Graph/GraphContextMenu.tsx +++ b/public/app/angular/components/legacy_graph_panel/GraphContextMenu.tsx @@ -9,21 +9,29 @@ import { TimeZone, FormattedValue, GrafanaTheme2, + Dimension, } from '@grafana/data'; - -import { ContextMenu, ContextMenuProps } from '../../components/ContextMenu/ContextMenu'; -import { FormattedValueDisplay } from '../../components/FormattedValueDisplay/FormattedValueDisplay'; -import { HorizontalGroup } from '../../components/Layout/Layout'; -import { MenuGroup, MenuGroupProps } from '../../components/Menu/MenuGroup'; -import { MenuItem } from '../../components/Menu/MenuItem'; -import { SeriesIcon } from '../../components/VizLegend/SeriesIcon'; -import { useStyles2 } from '../../themes'; - -import { GraphDimensions } from './GraphTooltip/types'; +import { + ContextMenu, + ContextMenuProps, + FormattedValueDisplay, + HorizontalGroup, + MenuGroup, + MenuGroupProps, + MenuItem, + SeriesIcon, + useStyles2, +} from '@grafana/ui'; /** @deprecated */ export type ContextDimensions = { [key in keyof T]: [number, number | undefined] | null }; +/** @deprecated */ +export interface GraphDimensions extends Dimensions { + xAxis: Dimension; + yAxis: Dimension; +} + /** @deprecated */ export type GraphContextMenuProps = ContextMenuProps & { getContextMenuSource: () => FlotDataPoint | null; diff --git a/public/app/plugins/panel/timeseries/plugins/ContextMenuPlugin.tsx b/public/app/plugins/panel/timeseries/plugins/ContextMenuPlugin.tsx index 59c6ad9dce..005f348762 100644 --- a/public/app/plugins/panel/timeseries/plugins/ContextMenuPlugin.tsx +++ b/public/app/plugins/panel/timeseries/plugins/ContextMenuPlugin.tsx @@ -2,15 +2,8 @@ import React, { useLayoutEffect, useMemo, useRef, useState } from 'react'; import { useClickAway } from 'react-use'; import { CartesianCoords2D, DataFrame, getFieldDisplayName, InterpolateFunction, TimeZone } from '@grafana/data'; -import { - ContextMenu, - GraphContextMenuHeader, - MenuItemProps, - MenuItemsGroup, - MenuGroup, - MenuItem, - UPlotConfigBuilder, -} from '@grafana/ui'; +import { ContextMenu, MenuItemProps, MenuItemsGroup, MenuGroup, MenuItem, UPlotConfigBuilder } from '@grafana/ui'; +import { GraphContextMenuHeader } from 'app/angular/components/legacy_graph_panel/GraphContextMenu'; type ContextMenuSelectionCoords = { viewport: CartesianCoords2D; plotCanvas: CartesianCoords2D }; type ContextMenuSelectionPoint = { seriesIdx: number | null; dataIdx: number | null }; From 3363e3f2d35a6da3474151febe26c50c3746956f Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Wed, 28 Feb 2024 16:47:10 +0000 Subject: [PATCH 0276/1406] CodeEditor: Ensure latest onChange callback is called (#83599) --- .../src/components/Monaco/CodeEditor.tsx | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/grafana-ui/src/components/Monaco/CodeEditor.tsx b/packages/grafana-ui/src/components/Monaco/CodeEditor.tsx index 7ab4854ec2..fa3ea3d7e1 100644 --- a/packages/grafana-ui/src/components/Monaco/CodeEditor.tsx +++ b/packages/grafana-ui/src/components/Monaco/CodeEditor.tsx @@ -102,7 +102,7 @@ class UnthemedCodeEditor extends PureComponent { }; handleOnMount = (editor: MonacoEditorType, monaco: Monaco) => { - const { getSuggestions, language, onChange, onEditorDidMount } = this.props; + const { getSuggestions, language, onEditorDidMount } = this.props; this.modelId = editor.getModel()?.id; this.getEditorValue = () => editor.getValue(); @@ -119,15 +119,21 @@ class UnthemedCodeEditor extends PureComponent { } }); - if (onChange) { - editor.getModel()?.onDidChangeContent(() => onChange(editor.getValue())); - } + editor.getModel()?.onDidChangeContent(this.handleChangeContent); if (onEditorDidMount) { onEditorDidMount(editor, monaco); } }; + handleChangeContent = () => { + const { onChange } = this.props; + + if (onChange) { + onChange(this.getEditorValue()); + } + }; + render() { const { theme, language, width, height, showMiniMap, showLineNumbers, readOnly, monacoOptions } = this.props; const { alwaysConsumeMouseWheel, ...restMonacoOptions } = monacoOptions ?? {}; From b89de96681f10cbced9d0aaa5d811bf5670e0d4c Mon Sep 17 00:00:00 2001 From: Eric Leijonmarck Date: Wed, 28 Feb 2024 17:35:10 +0000 Subject: [PATCH 0277/1406] Anonymous: Add docs for anon users charged on enterprise (#83626) add anon users enterprise --- .../configure-authentication/grafana/index.md | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/grafana/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/grafana/index.md index 17630be985..05db615313 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/grafana/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/grafana/index.md @@ -60,6 +60,25 @@ api_key_max_seconds_to_live = -1 You can make Grafana accessible without any login required by enabling anonymous access in the configuration file. For more information, refer to [Anonymous authentication]({{< relref "../../configure-authentication#anonymous-authentication" >}}). +#### Anonymous devices + +The anonymous devices feature enhances the management and monitoring of anonymous access within your Grafana instance. This feature is part of ongoing efforts to provide more control and transparency over anonymous usage. + +Users can now view anonymous usage statistics, including the count of devices and users over the last 30 days. + +- Go to **Administration -> Users** to access the anonymous devices tab. +- A new stat for the usage stats page -> Usage & Stats page shows the active anonymous devices last 30 days. + +The number of anonymous devices is not limited by default. The configuration option `device_limit` allows you to enforce a limit on the number of anonymous devices. This enables you to have greater control over the usage within your Grafana instance and keep the usage within the limits of your environment. Once the limit is reached, any new devices that try to access Grafana will be denied access. + +#### Anonymous users + +{{< admonition type="note" >}} +Anonymous users are charged as active users in Grafana Enterprise +{{< /admonition >}} + +#### Configuration + Example: ```bash @@ -81,17 +100,6 @@ device_limit = If you change your organization name in the Grafana UI this setting needs to be updated to match the new name. -#### Anonymous devices - -The anonymous devices feature enhances the management and monitoring of anonymous access within your Grafana instance. This feature is part of ongoing efforts to provide more control and transparency over anonymous usage. - -Users can now view anonymous usage statistics, including the count of devices and users over the last 30 days. - -- Go to **Administration -> Users** to access the anonymous devices tab. -- A new stat for the usage stats page -> Usage & Stats page shows the active anonymous devices last 30 days. - -The number of anonymous devices is not limited by default. The configuration option `device_limit` allows you to enforce a limit on the number of anonymous devices. This enables you to have greater control over the usage within your Grafana instance and keep the usage within the limits of your environment. Once the limit is reached, any new devices that try to access Grafana will be denied access. - ### Basic authentication Basic auth is enabled by default and works with the built in Grafana user password authentication system and LDAP From 9190fb28e82c8ff8430a029f94743152252f40d4 Mon Sep 17 00:00:00 2001 From: Leon Sorokin Date: Wed, 28 Feb 2024 11:42:46 -0600 Subject: [PATCH 0278/1406] VizTooltip: Improve edge positioning and a11y (#83584) --- .../src/components/uPlot/plugins/TooltipPlugin2.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx b/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx index f2ce44ecdc..2302c11c64 100644 --- a/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx +++ b/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx @@ -509,7 +509,13 @@ export const TooltipPlugin2 = ({ if (plot && isHovering) { return createPortal( -
+
{isPinned && } {contents}
, @@ -527,7 +533,7 @@ const getStyles = (theme: GrafanaTheme2, maxWidth?: number, maxHeight?: number) zIndex: theme.zIndex.tooltip, whiteSpace: 'pre', borderRadius: theme.shape.radius.default, - position: 'absolute', + position: 'fixed', background: theme.colors.background.primary, border: `1px solid ${theme.colors.border.weak}`, boxShadow: theme.shadows.z2, From af528d2f660cc34c8b6e057d81a7c627166686c7 Mon Sep 17 00:00:00 2001 From: William Wernert Date: Wed, 28 Feb 2024 13:16:37 -0500 Subject: [PATCH 0279/1406] Alerting/Annotations: Prevent panics from composite store jobs from crashing Grafana (#83459) * Don't directly use pointer to json * Don't crash entire process if a store job panics * Add debug logs when failing to parse/handle Loki entries --- .../annotationsimpl/annotations.go | 2 +- .../annotationsimpl/composite_store.go | 37 ++++++++++++-- .../annotationsimpl/composite_store_test.go | 48 +++++++++++++++++-- .../annotationsimpl/loki/historian_store.go | 10 ++++ .../loki/historian_store_test.go | 18 ++++++- .../annotations/annotationsimpl/store.go | 6 +++ .../annotations/annotationsimpl/xorm_store.go | 4 ++ 7 files changed, 116 insertions(+), 9 deletions(-) diff --git a/pkg/services/annotations/annotationsimpl/annotations.go b/pkg/services/annotations/annotationsimpl/annotations.go index f9640a51e9..3cf88288d2 100644 --- a/pkg/services/annotations/annotationsimpl/annotations.go +++ b/pkg/services/annotations/annotationsimpl/annotations.go @@ -38,7 +38,7 @@ func ProvideService( historianStore := loki.NewLokiHistorianStore(cfg.UnifiedAlerting.StateHistory, features, db, log.New("annotations.loki")) if historianStore != nil { l.Debug("Using composite read store") - read = NewCompositeStore(xormStore, historianStore) + read = NewCompositeStore(log.New("annotations.composite"), xormStore, historianStore) } else { l.Debug("Using xorm read store") read = write diff --git a/pkg/services/annotations/annotationsimpl/composite_store.go b/pkg/services/annotations/annotationsimpl/composite_store.go index 4db6e1c9ec..3bcf0724b5 100644 --- a/pkg/services/annotations/annotationsimpl/composite_store.go +++ b/pkg/services/annotations/annotationsimpl/composite_store.go @@ -2,8 +2,11 @@ package annotationsimpl import ( "context" + "fmt" "sort" + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/dskit/concurrency" "github.com/grafana/grafana/pkg/services/annotations" "github.com/grafana/grafana/pkg/services/annotations/accesscontrol" @@ -11,20 +14,29 @@ import ( // CompositeStore is a read store that combines two or more read stores, and queries all stores in parallel. type CompositeStore struct { + logger log.Logger readers []readStore } -func NewCompositeStore(readers ...readStore) *CompositeStore { +func NewCompositeStore(logger log.Logger, readers ...readStore) *CompositeStore { return &CompositeStore{ + logger: logger, readers: readers, } } +// Satisfy the commonStore interface, in practice this is not used. +func (c *CompositeStore) Type() string { + return "composite" +} + // Get returns annotations from all stores, and combines the results. func (c *CompositeStore) Get(ctx context.Context, query *annotations.ItemQuery, accessResources *accesscontrol.AccessResources) ([]*annotations.ItemDTO, error) { itemCh := make(chan []*annotations.ItemDTO, len(c.readers)) - err := concurrency.ForEachJob(ctx, len(c.readers), len(c.readers), func(ctx context.Context, i int) error { + err := concurrency.ForEachJob(ctx, len(c.readers), len(c.readers), func(ctx context.Context, i int) (err error) { + defer handleJobPanic(c.logger, c.readers[i].Type(), &err) + items, err := c.readers[i].Get(ctx, query, accessResources) itemCh <- items return err @@ -47,7 +59,9 @@ func (c *CompositeStore) Get(ctx context.Context, query *annotations.ItemQuery, func (c *CompositeStore) GetTags(ctx context.Context, query *annotations.TagsQuery) (annotations.FindTagsResult, error) { resCh := make(chan annotations.FindTagsResult, len(c.readers)) - err := concurrency.ForEachJob(ctx, len(c.readers), len(c.readers), func(ctx context.Context, i int) error { + err := concurrency.ForEachJob(ctx, len(c.readers), len(c.readers), func(ctx context.Context, i int) (err error) { + defer handleJobPanic(c.logger, c.readers[i].Type(), &err) + res, err := c.readers[i].GetTags(ctx, query) resCh <- res return err @@ -65,3 +79,20 @@ func (c *CompositeStore) GetTags(ctx context.Context, query *annotations.TagsQue return annotations.FindTagsResult{Tags: res}, nil } + +// handleJobPanic is a helper function that recovers from a panic in a concurrent job., +// It will log the error and set the job error if it is not nil. +func handleJobPanic(logger log.Logger, storeType string, jobErr *error) { + if r := recover(); r != nil { + logger.Error("Annotation store panic", "error", r, "store", storeType, "stack", log.Stack(1)) + errMsg := "concurrent job panic" + + if jobErr != nil { + err := fmt.Errorf(errMsg) + if panicErr, ok := r.(error); ok { + err = fmt.Errorf("%s: %w", errMsg, panicErr) + } + *jobErr = err + } + } +} diff --git a/pkg/services/annotations/annotationsimpl/composite_store_test.go b/pkg/services/annotations/annotationsimpl/composite_store_test.go index e7800d5dbd..b5aa1d22d8 100644 --- a/pkg/services/annotations/annotationsimpl/composite_store_test.go +++ b/pkg/services/annotations/annotationsimpl/composite_store_test.go @@ -7,6 +7,7 @@ import ( "testing" "time" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/annotations" "github.com/grafana/grafana/pkg/services/annotations/accesscontrol" "github.com/stretchr/testify/require" @@ -18,6 +19,22 @@ var ( ) func TestCompositeStore(t *testing.T) { + t.Run("should handle panic", func(t *testing.T) { + r1 := newFakeReader() + getPanic := func(context.Context, *annotations.ItemQuery, *accesscontrol.AccessResources) ([]*annotations.ItemDTO, error) { + panic("ohno") + } + r2 := newFakeReader(withGetFn(getPanic)) + store := &CompositeStore{ + log.NewNopLogger(), + []readStore{r1, r2}, + } + + _, err := store.Get(context.Background(), nil, nil) + require.Error(t, err) + require.Contains(t, err.Error(), "concurrent job panic") + }) + t.Run("should return first error", func(t *testing.T) { err1 := errors.New("error 1") r1 := newFakeReader(withError(err1)) @@ -25,6 +42,7 @@ func TestCompositeStore(t *testing.T) { r2 := newFakeReader(withError(err2), withWait(10*time.Millisecond)) store := &CompositeStore{ + log.NewNopLogger(), []readStore{r1, r2}, } @@ -64,6 +82,7 @@ func TestCompositeStore(t *testing.T) { r2 := newFakeReader(withItems(items2)) store := &CompositeStore{ + log.NewNopLogger(), []readStore{r1, r2}, } @@ -92,6 +111,7 @@ func TestCompositeStore(t *testing.T) { r2 := newFakeReader(withTags(tags2)) store := &CompositeStore{ + log.NewNopLogger(), []readStore{r1, r2}, } @@ -108,13 +128,23 @@ func TestCompositeStore(t *testing.T) { } type fakeReader struct { - items []*annotations.ItemDTO - tagRes annotations.FindTagsResult - wait time.Duration - err error + items []*annotations.ItemDTO + tagRes annotations.FindTagsResult + getFn func(context.Context, *annotations.ItemQuery, *accesscontrol.AccessResources) ([]*annotations.ItemDTO, error) + getTagFn func(context.Context, *annotations.TagsQuery) (annotations.FindTagsResult, error) + wait time.Duration + err error +} + +func (f *fakeReader) Type() string { + return "fake" } func (f *fakeReader) Get(ctx context.Context, query *annotations.ItemQuery, accessResources *accesscontrol.AccessResources) ([]*annotations.ItemDTO, error) { + if f.getFn != nil { + return f.getFn(ctx, query, accessResources) + } + if f.wait > 0 { time.Sleep(f.wait) } @@ -128,6 +158,10 @@ func (f *fakeReader) Get(ctx context.Context, query *annotations.ItemQuery, acce } func (f *fakeReader) GetTags(ctx context.Context, query *annotations.TagsQuery) (annotations.FindTagsResult, error) { + if f.getTagFn != nil { + return f.getTagFn(ctx, query) + } + if f.wait > 0 { time.Sleep(f.wait) } @@ -164,6 +198,12 @@ func withTags(tags []*annotations.TagsDTO) func(*fakeReader) { } } +func withGetFn(fn func(context.Context, *annotations.ItemQuery, *accesscontrol.AccessResources) ([]*annotations.ItemDTO, error)) func(*fakeReader) { + return func(f *fakeReader) { + f.getFn = fn + } +} + func newFakeReader(opts ...func(*fakeReader)) *fakeReader { f := &fakeReader{} for _, opt := range opts { diff --git a/pkg/services/annotations/annotationsimpl/loki/historian_store.go b/pkg/services/annotations/annotationsimpl/loki/historian_store.go index 451375b521..3d12623bd0 100644 --- a/pkg/services/annotations/annotationsimpl/loki/historian_store.go +++ b/pkg/services/annotations/annotationsimpl/loki/historian_store.go @@ -69,6 +69,10 @@ func NewLokiHistorianStore(cfg setting.UnifiedAlertingStateHistorySettings, ft f } } +func (r *LokiHistorianStore) Type() string { + return "loki" +} + func (r *LokiHistorianStore) Get(ctx context.Context, query *annotations.ItemQuery, accessResources *accesscontrol.AccessResources) ([]*annotations.ItemDTO, error) { if query.Type == "annotation" { return make([]*annotations.ItemDTO, 0), nil @@ -124,6 +128,7 @@ func (r *LokiHistorianStore) annotationsFromStream(stream historian.Stream, ac a err := json.Unmarshal([]byte(sample.V), &entry) if err != nil { // bad data, skip + r.log.Debug("failed to unmarshal loki entry", "error", err, "entry", sample.V) continue } @@ -135,6 +140,7 @@ func (r *LokiHistorianStore) annotationsFromStream(stream historian.Stream, ac a transition, err := buildTransition(entry) if err != nil { // bad data, skip + r.log.Debug("failed to build transition", "error", err, "entry", entry) continue } @@ -207,6 +213,10 @@ type number interface { // numericMap converts a simplejson map[string]any to a map[string]N, where N is numeric (int or float). func numericMap[N number](j *simplejson.Json) (map[string]N, error) { + if j == nil { + return nil, fmt.Errorf("unexpected nil value") + } + m, err := j.Map() if err != nil { return nil, err diff --git a/pkg/services/annotations/annotationsimpl/loki/historian_store_test.go b/pkg/services/annotations/annotationsimpl/loki/historian_store_test.go index 1123a7bbdd..6ed3933de8 100644 --- a/pkg/services/annotations/annotationsimpl/loki/historian_store_test.go +++ b/pkg/services/annotations/annotationsimpl/loki/historian_store_test.go @@ -374,7 +374,23 @@ func TestHasAccess(t *testing.T) { }) } -func TestFloat64Map(t *testing.T) { +func TestNumericMap(t *testing.T) { + t.Run("should return error for nil value", func(t *testing.T) { + var jsonMap *simplejson.Json + _, err := numericMap[float64](jsonMap) + require.Error(t, err) + require.Contains(t, err.Error(), "unexpected nil value") + }) + + t.Run("should return error for nil interface value", func(t *testing.T) { + jsonMap := simplejson.NewFromAny(map[string]any{ + "key1": nil, + }) + _, err := numericMap[float64](jsonMap) + require.Error(t, err) + require.Contains(t, err.Error(), "unexpected value type") + }) + t.Run(`should convert json string:float kv to Golang map[string]float64`, func(t *testing.T) { jsonMap := simplejson.NewFromAny(map[string]any{ "key1": json.Number("1.0"), diff --git a/pkg/services/annotations/annotationsimpl/store.go b/pkg/services/annotations/annotationsimpl/store.go index 1a3d399141..b1c3643a9c 100644 --- a/pkg/services/annotations/annotationsimpl/store.go +++ b/pkg/services/annotations/annotationsimpl/store.go @@ -14,12 +14,18 @@ type store interface { writeStore } +type commonStore interface { + Type() string +} + type readStore interface { + commonStore Get(ctx context.Context, query *annotations.ItemQuery, accessResources *accesscontrol.AccessResources) ([]*annotations.ItemDTO, error) GetTags(ctx context.Context, query *annotations.TagsQuery) (annotations.FindTagsResult, error) } type writeStore interface { + commonStore Add(ctx context.Context, items *annotations.Item) error AddMany(ctx context.Context, items []annotations.Item) error Update(ctx context.Context, item *annotations.Item) error diff --git a/pkg/services/annotations/annotationsimpl/xorm_store.go b/pkg/services/annotations/annotationsimpl/xorm_store.go index a9edcebbee..6ae9ddc3ce 100644 --- a/pkg/services/annotations/annotationsimpl/xorm_store.go +++ b/pkg/services/annotations/annotationsimpl/xorm_store.go @@ -54,6 +54,10 @@ func NewXormStore(cfg *setting.Cfg, l log.Logger, db db.DB, tagService tag.Servi } } +func (r *xormRepositoryImpl) Type() string { + return "sql" +} + func (r *xormRepositoryImpl) Add(ctx context.Context, item *annotations.Item) error { tags := tag.ParseTagPairs(item.Tags) item.Tags = tag.JoinTagPairs(tags) From a862a4264d06311f0e1ee28f8d62b2af4c1d030c Mon Sep 17 00:00:00 2001 From: Alexander Weaver Date: Wed, 28 Feb 2024 14:40:13 -0600 Subject: [PATCH 0280/1406] Alerting: Export rule validation logic and make it portable (#83555) * ValidateInterval doesn't need the entire config * Validation no longer depends on entire folder now that we've dropped foldertitle from api * Don't depend on entire config struct * Export validate group --- pkg/services/ngalert/api/api_ruler.go | 2 +- pkg/services/ngalert/api/api_ruler_export.go | 2 +- .../ngalert/api/api_ruler_validation.go | 43 ++++++++++++------- .../ngalert/api/api_ruler_validation_test.go | 26 +++++------ pkg/services/ngalert/api/api_testing.go | 6 +-- 5 files changed, 46 insertions(+), 33 deletions(-) diff --git a/pkg/services/ngalert/api/api_ruler.go b/pkg/services/ngalert/api/api_ruler.go index d551a9e9f2..8a77b72a9c 100644 --- a/pkg/services/ngalert/api/api_ruler.go +++ b/pkg/services/ngalert/api/api_ruler.go @@ -273,7 +273,7 @@ func (srv RulerSrv) RoutePostNameRulesConfig(c *contextmodel.ReqContext, ruleGro return ErrResp(http.StatusBadRequest, err, "") } - rules, err := validateRuleGroup(&ruleGroupConfig, c.SignedInUser.GetOrgID(), namespace, srv.cfg) + rules, err := ValidateRuleGroup(&ruleGroupConfig, c.SignedInUser.GetOrgID(), namespace.UID, RuleLimitsFromConfig(srv.cfg)) if err != nil { return ErrResp(http.StatusBadRequest, err, "") } diff --git a/pkg/services/ngalert/api/api_ruler_export.go b/pkg/services/ngalert/api/api_ruler_export.go index 406b342d6c..f925a4d4a8 100644 --- a/pkg/services/ngalert/api/api_ruler_export.go +++ b/pkg/services/ngalert/api/api_ruler_export.go @@ -20,7 +20,7 @@ func (srv RulerSrv) ExportFromPayload(c *contextmodel.ReqContext, ruleGroupConfi return toNamespaceErrorResponse(err) } - rulesWithOptionals, err := validateRuleGroup(&ruleGroupConfig, c.SignedInUser.GetOrgID(), namespace, srv.cfg) + rulesWithOptionals, err := ValidateRuleGroup(&ruleGroupConfig, c.SignedInUser.GetOrgID(), namespace.UID, RuleLimitsFromConfig(srv.cfg)) if err != nil { return ErrResp(http.StatusBadRequest, err, "") } diff --git a/pkg/services/ngalert/api/api_ruler_validation.go b/pkg/services/ngalert/api/api_ruler_validation.go index 0d13fd0c2c..91a012b0b1 100644 --- a/pkg/services/ngalert/api/api_ruler_validation.go +++ b/pkg/services/ngalert/api/api_ruler_validation.go @@ -7,22 +7,35 @@ import ( "strings" "time" - "github.com/grafana/grafana/pkg/services/folder" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/store" "github.com/grafana/grafana/pkg/setting" ) +type RuleLimits struct { + // The default interval if not specified. + DefaultRuleEvaluationInterval time.Duration + // All intervals must be an integer multiple of this duration. + BaseInterval time.Duration +} + +func RuleLimitsFromConfig(cfg *setting.UnifiedAlertingSettings) RuleLimits { + return RuleLimits{ + DefaultRuleEvaluationInterval: cfg.DefaultRuleEvaluationInterval, + BaseInterval: cfg.BaseInterval, + } +} + // validateRuleNode validates API model (definitions.PostableExtendedRuleNode) and converts it to models.AlertRule func validateRuleNode( ruleNode *apimodels.PostableExtendedRuleNode, groupName string, interval time.Duration, orgId int64, - namespace *folder.Folder, - cfg *setting.UnifiedAlertingSettings) (*ngmodels.AlertRule, error) { - intervalSeconds, err := validateInterval(cfg, interval) + namespaceUID string, + limits RuleLimits) (*ngmodels.AlertRule, error) { + intervalSeconds, err := validateInterval(interval, limits.BaseInterval) if err != nil { return nil, err } @@ -91,7 +104,7 @@ func validateRuleNode( Data: queries, UID: ruleNode.GrafanaManagedAlert.UID, IntervalSeconds: intervalSeconds, - NamespaceUID: namespace.UID, + NamespaceUID: namespaceUID, RuleGroup: groupName, NoDataState: noDataState, ExecErrState: errorState, @@ -162,10 +175,10 @@ func validateCondition(condition string, queries []apimodels.AlertQuery) error { return nil } -func validateInterval(cfg *setting.UnifiedAlertingSettings, interval time.Duration) (int64, error) { +func validateInterval(interval, baseInterval time.Duration) (int64, error) { intervalSeconds := int64(interval.Seconds()) - baseIntervalSeconds := int64(cfg.BaseInterval.Seconds()) + baseIntervalSeconds := int64(baseInterval.Seconds()) if interval <= 0 { return 0, fmt.Errorf("rule evaluation interval must be positive duration that is multiple of the base interval %d seconds", baseIntervalSeconds) @@ -193,14 +206,14 @@ func validateForInterval(ruleNode *apimodels.PostableExtendedRuleNode) (time.Dur return duration, nil } -// validateRuleGroup validates API model (definitions.PostableRuleGroupConfig) and converts it to a collection of models.AlertRule. +// ValidateRuleGroup validates API model (definitions.PostableRuleGroupConfig) and converts it to a collection of models.AlertRule. // Returns a slice that contains all rules described by API model or error if either group specification or an alert definition is not valid. // It also returns a map containing current existing alerts that don't contain the is_paused field in the body of the call. -func validateRuleGroup( +func ValidateRuleGroup( ruleGroupConfig *apimodels.PostableRuleGroupConfig, orgId int64, - namespace *folder.Folder, - cfg *setting.UnifiedAlertingSettings) ([]*ngmodels.AlertRuleWithOptionals, error) { + namespaceUID string, + limits RuleLimits) ([]*ngmodels.AlertRuleWithOptionals, error) { if ruleGroupConfig.Name == "" { return nil, errors.New("rule group name cannot be empty") } @@ -212,11 +225,11 @@ func validateRuleGroup( interval := time.Duration(ruleGroupConfig.Interval) if interval == 0 { // if group interval is 0 (undefined) then we automatically fall back to the default interval - interval = cfg.DefaultRuleEvaluationInterval + interval = limits.DefaultRuleEvaluationInterval } - if interval < 0 || int64(interval.Seconds())%int64(cfg.BaseInterval.Seconds()) != 0 { - return nil, fmt.Errorf("rule evaluation interval (%d second) should be positive number that is multiple of the base interval of %d seconds", int64(interval.Seconds()), int64(cfg.BaseInterval.Seconds())) + if interval < 0 || int64(interval.Seconds())%int64(limits.BaseInterval.Seconds()) != 0 { + return nil, fmt.Errorf("rule evaluation interval (%d second) should be positive number that is multiple of the base interval of %d seconds", int64(interval.Seconds()), int64(limits.BaseInterval.Seconds())) } // TODO should we validate that interval is >= cfg.MinInterval? Currently, we allow to save but fix the specified interval if it is < cfg.MinInterval @@ -224,7 +237,7 @@ func validateRuleGroup( result := make([]*ngmodels.AlertRuleWithOptionals, 0, len(ruleGroupConfig.Rules)) uids := make(map[string]int, cap(result)) for idx := range ruleGroupConfig.Rules { - rule, err := validateRuleNode(&ruleGroupConfig.Rules[idx], ruleGroupConfig.Name, interval, orgId, namespace, cfg) + rule, err := validateRuleNode(&ruleGroupConfig.Rules[idx], ruleGroupConfig.Name, interval, orgId, namespaceUID, limits) // TODO do not stop on the first failure but return all failures if err != nil { return nil, fmt.Errorf("invalid rule specification at index [%d]: %w", idx, err) diff --git a/pkg/services/ngalert/api/api_ruler_validation_test.go b/pkg/services/ngalert/api/api_ruler_validation_test.go index d763f9e2c8..0530e2378e 100644 --- a/pkg/services/ngalert/api/api_ruler_validation_test.go +++ b/pkg/services/ngalert/api/api_ruler_validation_test.go @@ -197,7 +197,7 @@ func TestValidateRuleGroup(t *testing.T) { t.Run("should validate struct and rules", func(t *testing.T) { g := validGroup(cfg, rules...) - alerts, err := validateRuleGroup(&g, orgId, folder, cfg) + alerts, err := ValidateRuleGroup(&g, orgId, folder.UID, RuleLimitsFromConfig(cfg)) require.NoError(t, err) require.Len(t, alerts, len(rules)) }) @@ -205,7 +205,7 @@ func TestValidateRuleGroup(t *testing.T) { t.Run("should default to default interval from config if group interval is 0", func(t *testing.T) { g := validGroup(cfg, rules...) g.Interval = 0 - alerts, err := validateRuleGroup(&g, orgId, folder, cfg) + alerts, err := ValidateRuleGroup(&g, orgId, folder.UID, RuleLimitsFromConfig(cfg)) require.NoError(t, err) for _, alert := range alerts { require.Equal(t, int64(cfg.DefaultRuleEvaluationInterval.Seconds()), alert.IntervalSeconds) @@ -220,7 +220,7 @@ func TestValidateRuleGroup(t *testing.T) { isPaused = !(isPaused) } g := validGroup(cfg, rules...) - alerts, err := validateRuleGroup(&g, orgId, folder, cfg) + alerts, err := ValidateRuleGroup(&g, orgId, folder.UID, RuleLimitsFromConfig(cfg)) require.NoError(t, err) for _, alert := range alerts { require.True(t, alert.HasPause) @@ -292,7 +292,7 @@ func TestValidateRuleGroupFailures(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { g := testCase.group() - _, err := validateRuleGroup(g, orgId, folder, cfg) + _, err := ValidateRuleGroup(g, orgId, folder.UID, RuleLimitsFromConfig(cfg)) require.Error(t, err) if testCase.assert != nil { testCase.assert(t, g, err) @@ -399,7 +399,7 @@ func TestValidateRuleNode_NoUID(t *testing.T) { r := testCase.rule() r.GrafanaManagedAlert.UID = "" - alert, err := validateRuleNode(r, name, interval, orgId, folder, cfg) + alert, err := validateRuleNode(r, name, interval, orgId, folder.UID, RuleLimitsFromConfig(cfg)) require.NoError(t, err) testCase.assert(t, r, alert) }) @@ -407,7 +407,7 @@ func TestValidateRuleNode_NoUID(t *testing.T) { t.Run("accepts empty group name", func(t *testing.T) { r := validRule() - alert, err := validateRuleNode(&r, "", interval, orgId, folder, cfg) + alert, err := validateRuleNode(&r, "", interval, orgId, folder.UID, RuleLimitsFromConfig(cfg)) require.NoError(t, err) require.Equal(t, "", alert.RuleGroup) }) @@ -560,7 +560,7 @@ func TestValidateRuleNodeFailures_NoUID(t *testing.T) { interval = *testCase.interval } - _, err := validateRuleNode(r, "", interval, orgId, folder, cfg) + _, err := validateRuleNode(r, "", interval, orgId, folder.UID, RuleLimitsFromConfig(cfg)) require.Error(t, err) if testCase.assert != nil { testCase.assert(t, r, err) @@ -652,7 +652,7 @@ func TestValidateRuleNode_UID(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { r := testCase.rule() - alert, err := validateRuleNode(r, name, interval, orgId, folder, cfg) + alert, err := validateRuleNode(r, name, interval, orgId, folder.UID, RuleLimitsFromConfig(cfg)) require.NoError(t, err) testCase.assert(t, r, alert) }) @@ -660,7 +660,7 @@ func TestValidateRuleNode_UID(t *testing.T) { t.Run("accepts empty group name", func(t *testing.T) { r := validRule() - alert, err := validateRuleNode(&r, "", interval, orgId, folder, cfg) + alert, err := validateRuleNode(&r, "", interval, orgId, folder.UID, RuleLimitsFromConfig(cfg)) require.NoError(t, err) require.Equal(t, "", alert.RuleGroup) }) @@ -755,7 +755,7 @@ func TestValidateRuleNodeFailures_UID(t *testing.T) { interval = *testCase.interval } - _, err := validateRuleNode(r, "", interval, orgId, folder, cfg) + _, err := validateRuleNode(r, "", interval, orgId, folder.UID, RuleLimitsFromConfig(cfg)) require.Error(t, err) if testCase.assert != nil { testCase.assert(t, r, err) @@ -788,7 +788,7 @@ func TestValidateRuleNodeIntervalFailures(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { r := validRule() - _, err := validateRuleNode(&r, util.GenerateShortUID(), testCase.interval, rand.Int63(), randFolder(), cfg) + _, err := validateRuleNode(&r, util.GenerateShortUID(), testCase.interval, rand.Int63(), randFolder().UID, RuleLimitsFromConfig(cfg)) require.Error(t, err) }) } @@ -880,7 +880,7 @@ func TestValidateRuleNodeNotificationSettings(t *testing.T) { t.Run(tt.name, func(t *testing.T) { r := validRule() r.GrafanaManagedAlert.NotificationSettings = AlertRuleNotificationSettingsFromNotificationSettings([]models.NotificationSettings{tt.notificationSettings}) - _, err := validateRuleNode(&r, util.GenerateShortUID(), cfg.BaseInterval*time.Duration(rand.Int63n(10)+1), rand.Int63(), randFolder(), cfg) + _, err := validateRuleNode(&r, util.GenerateShortUID(), cfg.BaseInterval*time.Duration(rand.Int63n(10)+1), rand.Int63(), randFolder().UID, RuleLimitsFromConfig(cfg)) if tt.expErrorContains != "" { require.Error(t, err) @@ -901,7 +901,7 @@ func TestValidateRuleNodeReservedLabels(t *testing.T) { r.ApiRuleNode.Labels = map[string]string{ label: "true", } - _, err := validateRuleNode(&r, util.GenerateShortUID(), cfg.BaseInterval*time.Duration(rand.Int63n(10)+1), rand.Int63(), randFolder(), cfg) + _, err := validateRuleNode(&r, util.GenerateShortUID(), cfg.BaseInterval*time.Duration(rand.Int63n(10)+1), rand.Int63(), randFolder().UID, RuleLimitsFromConfig(cfg)) require.Error(t, err) require.ErrorContains(t, err, label) }) diff --git a/pkg/services/ngalert/api/api_testing.go b/pkg/services/ngalert/api/api_testing.go index 5e54dc8869..9d715ddd96 100644 --- a/pkg/services/ngalert/api/api_testing.go +++ b/pkg/services/ngalert/api/api_testing.go @@ -66,8 +66,8 @@ func (srv TestingApiSrv) RouteTestGrafanaRuleConfig(c *contextmodel.ReqContext, body.RuleGroup, srv.cfg.BaseInterval, c.SignedInUser.GetOrgID(), - folder, - srv.cfg, + folder.UID, + RuleLimitsFromConfig(srv.cfg), ) if err != nil { return ErrResp(http.StatusBadRequest, err, "") @@ -238,7 +238,7 @@ func (srv TestingApiSrv) BacktestAlertRule(c *contextmodel.ReqContext, cmd apimo return ErrResp(400, nil, "Bad For interval") } - intervalSeconds, err := validateInterval(srv.cfg, time.Duration(cmd.Interval)) + intervalSeconds, err := validateInterval(time.Duration(cmd.Interval), srv.cfg.BaseInterval) if err != nil { return ErrResp(400, err, "") } From 239abe4234b696eed800ab1b38bfa1b9534fb70a Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Wed, 28 Feb 2024 13:00:00 -0800 Subject: [PATCH 0281/1406] Chore: Restore vectorator export (#83637) --- packages/grafana-data/src/index.ts | 1 + packages/grafana-data/src/vector/FunctionalVector.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/grafana-data/src/index.ts b/packages/grafana-data/src/index.ts index 4e3a847e1d..24a1181066 100644 --- a/packages/grafana-data/src/index.ts +++ b/packages/grafana-data/src/index.ts @@ -46,3 +46,4 @@ export { getLinksSupplier } from './field/fieldOverrides'; // deprecated export { CircularVector } from './vector/CircularVector'; +export { vectorator } from './vector/FunctionalVector'; diff --git a/packages/grafana-data/src/vector/FunctionalVector.ts b/packages/grafana-data/src/vector/FunctionalVector.ts index 8322f96c30..bcd13d9609 100644 --- a/packages/grafana-data/src/vector/FunctionalVector.ts +++ b/packages/grafana-data/src/vector/FunctionalVector.ts @@ -183,7 +183,7 @@ const emptyarray: any[] = []; * * @deprecated use a simple Arrays */ -function vectorator(vector: FunctionalVector) { +export function vectorator(vector: FunctionalVector) { return { *[Symbol.iterator]() { for (let i = 0; i < vector.length; i++) { From d6b1aa6575db4249fe6aa89e109bff9074c384ec Mon Sep 17 00:00:00 2001 From: Darren Janeczek <38694490+darrenjaneczek@users.noreply.github.com> Date: Wed, 28 Feb 2024 16:05:22 -0500 Subject: [PATCH 0282/1406] datatrails: standardize loading and blocking message indicators (#83560) fix: standardize loading and blocking message indicators --- .../trails/ActionTabs/BreakdownScene.tsx | 51 ++++++++++--------- .../trails/ActionTabs/MetricOverviewScene.tsx | 23 +++++---- .../app/features/trails/MetricSelectScene.tsx | 28 +++++----- public/app/features/trails/StatusWrapper.tsx | 39 ++++++++++++++ 4 files changed, 90 insertions(+), 51 deletions(-) create mode 100644 public/app/features/trails/StatusWrapper.tsx diff --git a/public/app/features/trails/ActionTabs/BreakdownScene.tsx b/public/app/features/trails/ActionTabs/BreakdownScene.tsx index 28a9dc7579..26be042792 100644 --- a/public/app/features/trails/ActionTabs/BreakdownScene.tsx +++ b/public/app/features/trails/ActionTabs/BreakdownScene.tsx @@ -26,6 +26,7 @@ import { getAutoQueriesForMetric } from '../AutomaticMetricQueries/AutoQueryEngi import { AutoQueryDef } from '../AutomaticMetricQueries/types'; import { BreakdownLabelSelector } from '../BreakdownLabelSelector'; import { MetricScene } from '../MetricScene'; +import { StatusWrapper } from '../StatusWrapper'; import { trailDS, VAR_FILTERS, VAR_GROUP_BY, VAR_GROUP_BY_EXP } from '../shared'; import { getColorByIndex } from '../utils'; @@ -39,6 +40,8 @@ export interface BreakdownSceneState extends SceneObjectState { labels: Array>; value?: string; loading?: boolean; + error?: string; + blockingMessage?: string; } export class BreakdownScene extends SceneObjectBase { @@ -99,12 +102,17 @@ export class BreakdownScene extends SceneObjectBase { loading: variable.state.loading, value: String(variable.state.value), labels: options, + error: variable.state.error, + blockingMessage: undefined, }; - if (!variable.state.loading) { + if (!variable.state.loading && variable.state.options.length) { stateUpdate.body = variable.hasAllValue() ? buildAllLayout(options, this._query!) : buildNormalLayout(this._query!); + } else if (!variable.state.loading) { + stateUpdate.body = undefined; + stateUpdate.blockingMessage = 'Unable to retrieve label options for currently selected metric.'; } this.setState(stateUpdate); @@ -117,37 +125,32 @@ export class BreakdownScene extends SceneObjectBase { const variable = this.getVariable(); - if (value === ALL_VARIABLE_VALUE) { - this.setState({ body: buildAllLayout(this.state.labels, this._query!) }); - } else if (variable.hasAllValue()) { - this.setState({ body: buildNormalLayout(this._query!) }); - } - variable.changeValueTo(value); }; public static Component = ({ model }: SceneComponentProps) => { - const { labels, body, loading, value } = model.useState(); + const { labels, body, loading, value, blockingMessage } = model.useState(); const styles = useStyles2(getStyles); return (
- {loading &&
Loading...
} -
- {!loading && ( -
- - - -
- )} - {body instanceof LayoutSwitcher && ( -
- -
- )} -
-
{body && }
+ +
+ {!loading && labels.length && ( +
+ + + +
+ )} + {body instanceof LayoutSwitcher && ( +
+ +
+ )} +
+
{body && }
+
); }; diff --git a/public/app/features/trails/ActionTabs/MetricOverviewScene.tsx b/public/app/features/trails/ActionTabs/MetricOverviewScene.tsx index c3efde7d7b..5adc3d83bc 100644 --- a/public/app/features/trails/ActionTabs/MetricOverviewScene.tsx +++ b/public/app/features/trails/ActionTabs/MetricOverviewScene.tsx @@ -15,6 +15,7 @@ import PrometheusLanguageProvider from '../../../plugins/datasource/prometheus/l import { PromMetricsMetadataItem } from '../../../plugins/datasource/prometheus/types'; import { getDatasourceSrv } from '../../plugins/datasource_srv'; import { ALL_VARIABLE_VALUE } from '../../variables/constants'; +import { StatusWrapper } from '../StatusWrapper'; import { TRAILS_ROUTE, VAR_DATASOURCE_EXPR, VAR_GROUP_BY } from '../shared'; import { getMetricSceneFor } from '../utils'; @@ -22,7 +23,7 @@ import { getLabelOptions } from './utils'; export interface MetricOverviewSceneState extends SceneObjectState { metadata?: PromMetricsMetadataItem; - loading?: boolean; + metadataLoading?: boolean; } export class MetricOverviewScene extends SceneObjectBase { @@ -57,6 +58,7 @@ export class MetricOverviewScene extends SceneObjectBase) => { - const { metadata } = model.useState(); + const { metadata, metadataLoading } = model.useState(); const variable = model.getVariable(); - const { loading } = variable.useState(); + const { loading: labelsLoading } = variable.useState(); const labelOptions = getLabelOptions(model, variable).filter((l) => l.value !== ALL_VARIABLE_VALUE); return ( - - {loading ? ( -
Loading...
- ) : ( + + <> Description @@ -106,6 +106,7 @@ export class MetricOverviewScene extends SceneObjectBase Labels + {labelOptions.length === 0 && 'Unable to fetch labels.'} {labelOptions.map((l) => ( - )} - + + ); }; } diff --git a/public/app/features/trails/MetricSelectScene.tsx b/public/app/features/trails/MetricSelectScene.tsx index 73b856e533..ee42c0ed54 100644 --- a/public/app/features/trails/MetricSelectScene.tsx +++ b/public/app/features/trails/MetricSelectScene.tsx @@ -20,12 +20,13 @@ import { VariableDependencyConfig, } from '@grafana/scenes'; import { VariableHide } from '@grafana/schema'; -import { Input, useStyles2, InlineSwitch, Field, Alert, Icon, LoadingPlaceholder } from '@grafana/ui'; +import { Input, InlineSwitch, Field, Alert, Icon, useStyles2 } from '@grafana/ui'; import { getPreviewPanelFor } from './AutomaticMetricQueries/previewPanel'; import { MetricCategoryCascader } from './MetricCategory/MetricCategoryCascader'; import { MetricScene } from './MetricScene'; import { SelectMetricAction } from './SelectMetricAction'; +import { StatusWrapper } from './StatusWrapper'; import { sortRelatedMetrics } from './relatedMetrics'; import { getVariablesWithMetricConstant, trailDS, VAR_DATASOURCE, VAR_FILTERS_EXPR, VAR_METRIC_NAMES } from './shared'; import { getFilters, getTrailFor } from './utils'; @@ -326,14 +327,13 @@ export class MetricSelectScene extends SceneObjectBase { const tooStrict = children.length === 0 && (searchQuery || prefixFilter); const noMetrics = !metricNamesStatus.isLoading && model.currentMetricNames.size === 0; - const status = - (metricNamesStatus.isLoading && children.length === 0 && ( - - )) || - (noMetrics && 'There are no results found. Try a different time range or a different data source.') || - (tooStrict && 'There are no results found. Try adjusting your search or filters.'); + const isLoading = metricNamesStatus.isLoading && children.length === 0; - const showStatus = status &&
{status}
; + const blockingMessage = isLoading + ? undefined + : (noMetrics && 'There are no results found. Try a different time range or a different data source.') || + (tooStrict && 'There are no results found. Try adjusting your search or filters.') || + undefined; const prefixError = prefixFilter && metricsAfterSearch != null && !metricsAfterFilter?.length @@ -378,8 +378,9 @@ export class MetricSelectScene extends SceneObjectBase {
({metricNamesStatus.error})
)} - {showStatus} - + + +
); }; @@ -427,11 +428,6 @@ function getStyles(theme: GrafanaTheme2) { marginBottom: theme.spacing(1), alignItems: 'flex-end', }), - statusMessage: css({ - fontStyle: 'italic', - marginTop: theme.spacing(7), - textAlign: 'center', - }), searchField: css({ flexGrow: 1, marginBottom: 0, @@ -463,7 +459,7 @@ function createSearchRegExp(spaceSeparatedMetricNames?: string) { } function useVariableStatus(name: string, sceneObject: SceneObject) { - const variable = sceneGraph.lookupVariable(VAR_METRIC_NAMES, sceneObject); + const variable = sceneGraph.lookupVariable(name, sceneObject); const useVariableState = useCallback(() => { if (variable) { diff --git a/public/app/features/trails/StatusWrapper.tsx b/public/app/features/trails/StatusWrapper.tsx new file mode 100644 index 0000000000..372a558bed --- /dev/null +++ b/public/app/features/trails/StatusWrapper.tsx @@ -0,0 +1,39 @@ +import { css } from '@emotion/css'; +import React, { ReactNode } from 'react'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { LoadingPlaceholder, useStyles2 } from '@grafana/ui'; + +type Props = { + blockingMessage?: string; + isLoading?: boolean; + children?: ReactNode; +}; + +export function StatusWrapper({ blockingMessage, isLoading, children }: Props) { + const styles = useStyles2(getStyles); + + if (isLoading && !blockingMessage) { + blockingMessage = 'Loading...'; + } + + if (isLoading) { + return ; + } + + if (!blockingMessage) { + return children; + } + + return
{blockingMessage}
; +} + +function getStyles(theme: GrafanaTheme2) { + return { + statusMessage: css({ + fontStyle: 'italic', + marginTop: theme.spacing(7), + textAlign: 'center', + }), + }; +} From 6e75584505f862e159c48d69a3469ee057606b44 Mon Sep 17 00:00:00 2001 From: Darren Janeczek <38694490+darrenjaneczek@users.noreply.github.com> Date: Wed, 28 Feb 2024 16:06:31 -0500 Subject: [PATCH 0283/1406] datatrails: improve label filter behavior (#83411) --- package.json | 2 +- public/app/features/trails/DataTrail.tsx | 1 + yarn.lock | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index cd695b8d35..9182e8a426 100644 --- a/package.json +++ b/package.json @@ -254,7 +254,7 @@ "@grafana/o11y-ds-frontend": "workspace:*", "@grafana/prometheus": "workspace:*", "@grafana/runtime": "workspace:*", - "@grafana/scenes": "^3.5.0", + "@grafana/scenes": "^3.8.0", "@grafana/schema": "workspace:*", "@grafana/sql": "workspace:*", "@grafana/ui": "workspace:*", diff --git a/public/app/features/trails/DataTrail.tsx b/public/app/features/trails/DataTrail.tsx index 68f044923e..30c2e01e23 100644 --- a/public/app/features/trails/DataTrail.tsx +++ b/public/app/features/trails/DataTrail.tsx @@ -212,6 +212,7 @@ function getVariableSet(initialDS?: string, metric?: string, initialFilters?: Ad }), new AdHocFiltersVariable({ name: VAR_FILTERS, + addFilterButtonText: 'Add label', datasource: trailDS, hide: VariableHide.hideLabel, layout: 'vertical', diff --git a/yarn.lock b/yarn.lock index 4a44becb24..d38be2aca6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4073,7 +4073,7 @@ __metadata: languageName: unknown linkType: soft -"@grafana/scenes@npm:^3.5.0": +"@grafana/scenes@npm:^3.8.0": version: 3.8.0 resolution: "@grafana/scenes@npm:3.8.0" dependencies: @@ -18341,7 +18341,7 @@ __metadata: "@grafana/plugin-e2e": "npm:0.18.0" "@grafana/prometheus": "workspace:*" "@grafana/runtime": "workspace:*" - "@grafana/scenes": "npm:^3.5.0" + "@grafana/scenes": "npm:^3.8.0" "@grafana/schema": "workspace:*" "@grafana/sql": "workspace:*" "@grafana/tsconfig": "npm:^1.3.0-rc1" From 5eb7e09351c30b6a958bf543a7f4811f532323fa Mon Sep 17 00:00:00 2001 From: Leon Sorokin Date: Wed, 28 Feb 2024 17:06:05 -0600 Subject: [PATCH 0284/1406] VizTooltips: Show data links without anchoring (#83638) --- .../VizTooltip/VizTooltipFooter.tsx | 6 ++--- .../panel/heatmap/HeatmapHoverView.tsx | 4 +++- .../state-timeline/StateTimelineTooltip2.tsx | 4 +++- .../app/plugins/panel/status-history/utils.ts | 6 ++--- .../panel/timeseries/TimeSeriesTooltip.tsx | 4 +++- .../plugins/panel/xychart/XYChartTooltip.tsx | 22 ++++--------------- 6 files changed, 19 insertions(+), 27 deletions(-) diff --git a/packages/grafana-ui/src/components/VizTooltip/VizTooltipFooter.tsx b/packages/grafana-ui/src/components/VizTooltip/VizTooltipFooter.tsx index 1a9a3ff4e8..5713748baf 100644 --- a/packages/grafana-ui/src/components/VizTooltip/VizTooltipFooter.tsx +++ b/packages/grafana-ui/src/components/VizTooltip/VizTooltipFooter.tsx @@ -6,14 +6,14 @@ import { Field, GrafanaTheme2, LinkModel } from '@grafana/data'; import { Button, ButtonProps, DataLinkButton, HorizontalGroup } from '..'; import { useStyles2 } from '../../themes'; -interface Props { +interface VizTooltipFooterProps { dataLinks: Array>; annotate?: () => void; } export const ADD_ANNOTATION_ID = 'add-annotation-button'; -export const VizTooltipFooter = ({ dataLinks, annotate }: Props) => { +export const VizTooltipFooter = ({ dataLinks, annotate }: VizTooltipFooterProps) => { const styles = useStyles2(getStyles); const renderDataLinks = () => { @@ -33,7 +33,7 @@ export const VizTooltipFooter = ({ dataLinks, annotate }: Props) => { return (
{dataLinks.length > 0 &&
{renderDataLinks()}
} - {annotate && ( + {annotate != null && (
))} - {isPinned && } + {(links.length > 0 || isPinned) && ( + + )}
); }; diff --git a/public/app/plugins/panel/state-timeline/StateTimelineTooltip2.tsx b/public/app/plugins/panel/state-timeline/StateTimelineTooltip2.tsx index 98b6a6494d..686a67331d 100644 --- a/public/app/plugins/panel/state-timeline/StateTimelineTooltip2.tsx +++ b/public/app/plugins/panel/state-timeline/StateTimelineTooltip2.tsx @@ -83,7 +83,9 @@ export const StateTimelineTooltip2 = ({
- {isPinned && } + {(links.length > 0 || isPinned) && ( + + )}
); diff --git a/public/app/plugins/panel/status-history/utils.ts b/public/app/plugins/panel/status-history/utils.ts index 4f26288ba0..735ef03a5e 100644 --- a/public/app/plugins/panel/status-history/utils.ts +++ b/public/app/plugins/panel/status-history/utils.ts @@ -1,13 +1,13 @@ import { Field, LinkModel } from '@grafana/data'; -export const getDataLinks = (field: Field, datapointIdx: number) => { +export const getDataLinks = (field: Field, rowIdx: number) => { const links: Array> = []; const linkLookup = new Set(); if (field.getLinks) { - const v = field.values[datapointIdx]; + const v = field.values[rowIdx]; const disp = field.display ? field.display(v) : { text: `${v}`, numeric: +v }; - field.getLinks({ calculatedValue: disp, valueRowIndex: datapointIdx }).forEach((link) => { + field.getLinks({ calculatedValue: disp, valueRowIndex: rowIdx }).forEach((link) => { const key = `${link.title}/${link.href}`; if (!linkLookup.has(key)) { links.push(link); diff --git a/public/app/plugins/panel/timeseries/TimeSeriesTooltip.tsx b/public/app/plugins/panel/timeseries/TimeSeriesTooltip.tsx index 5c3cef5f10..d703ba1719 100644 --- a/public/app/plugins/panel/timeseries/TimeSeriesTooltip.tsx +++ b/public/app/plugins/panel/timeseries/TimeSeriesTooltip.tsx @@ -77,7 +77,9 @@ export const TimeSeriesTooltip = ({
- {isPinned && } + {(links.length > 0 || isPinned) && ( + + )}
); diff --git a/public/app/plugins/panel/xychart/XYChartTooltip.tsx b/public/app/plugins/panel/xychart/XYChartTooltip.tsx index 2a72c99138..5494b3a673 100644 --- a/public/app/plugins/panel/xychart/XYChartTooltip.tsx +++ b/public/app/plugins/panel/xychart/XYChartTooltip.tsx @@ -1,14 +1,14 @@ import React from 'react'; -import { DataFrame, Field, getFieldDisplayName, LinkModel } from '@grafana/data'; +import { DataFrame, Field, getFieldDisplayName } from '@grafana/data'; import { alpha } from '@grafana/data/src/themes/colorManipulator'; import { useStyles2 } from '@grafana/ui'; import { VizTooltipContent } from '@grafana/ui/src/components/VizTooltip/VizTooltipContent'; import { VizTooltipFooter } from '@grafana/ui/src/components/VizTooltip/VizTooltipFooter'; import { VizTooltipHeader } from '@grafana/ui/src/components/VizTooltip/VizTooltipHeader'; import { ColorIndicator, VizTooltipItem } from '@grafana/ui/src/components/VizTooltip/types'; -import { getTitleFromHref } from 'app/features/explore/utils/links'; +import { getDataLinks } from '../status-history/utils'; import { getStyles } from '../timeseries/TimeSeriesTooltip'; import { Options } from './panelcfg.gen'; @@ -83,27 +83,13 @@ export const XYChartTooltip = ({ dataIdxs, seriesIdx, data, allSeries, dismiss, }); } - const getLinks = (): Array> => { - let links: Array> = []; - if (yField.getLinks) { - const v = yField.values[rowIndex]; - const disp = yField.display ? yField.display(v) : { text: `${v}`, numeric: +v }; - links = yField.getLinks({ calculatedValue: disp, valueRowIndex: rowIndex }).map((linkModel) => { - if (!linkModel.title) { - linkModel.title = getTitleFromHref(linkModel.href); - } - - return linkModel; - }); - } - return links; - }; + const links = getDataLinks(yField, rowIndex); return (
- {isPinned && } + {(links.length > 0 || isPinned) && }
); }; From 88f5b62a237b3a0596801dea6a35d4a620ced891 Mon Sep 17 00:00:00 2001 From: Darren Janeczek <38694490+darrenjaneczek@users.noreply.github.com> Date: Wed, 28 Feb 2024 18:06:55 -0500 Subject: [PATCH 0285/1406] VizTooltips: Tolerate missing annotation text (#83634) --- .../timeseries/plugins/annotations2/AnnotationTooltip2.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/panel/timeseries/plugins/annotations2/AnnotationTooltip2.tsx b/public/app/plugins/panel/timeseries/plugins/annotations2/AnnotationTooltip2.tsx index c8c3deec31..641f0044a4 100644 --- a/public/app/plugins/panel/timeseries/plugins/annotations2/AnnotationTooltip2.tsx +++ b/public/app/plugins/panel/timeseries/plugins/annotations2/AnnotationTooltip2.tsx @@ -32,7 +32,7 @@ export const AnnotationTooltip2 = ({ annoVals, annoIdx, timeZone, onEdit }: Prop }); let time = timeFormatter(annoVals.time[annoIdx]); - let text = annoVals.text[annoIdx]; + let text = annoVals.text?.[annoIdx] ?? ''; if (annoVals.isRegion?.[annoIdx]) { time += ' - ' + timeFormatter(annoVals.timeEnd[annoIdx]); @@ -56,7 +56,7 @@ export const AnnotationTooltip2 = ({ annoVals, annoIdx, timeZone, onEdit }: Prop // alertText = alertDef.getAlertAnnotationInfo(annotation); // @TODO ?? } else if (annoVals.title?.[annoIdx]) { - text = annoVals.title[annoIdx] + '
' + (typeof text === 'string' ? text : ''); + text = annoVals.title[annoIdx] + text ? `
${text}` : ''; } return ( From 2c95782b101db95d9e4deac29032ec5d987617f7 Mon Sep 17 00:00:00 2001 From: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> Date: Wed, 28 Feb 2024 23:37:29 -0500 Subject: [PATCH 0286/1406] Docs: restructure Configure panel options (#83438) * Moved view json panel content from configure panel options to panel inspect view * Converted add title and description task to reference section * Removed edit panel section * Updated bullet list to match content * Removed view json content to be integrated later * Ran prettier * Docs: Edit Configure panel options (#83439) * Updated intro * Updated intro, descriptions, and repeating panels task * Reformatted sections of task and updated wording of LLM info * Copy edits * Added Cloud links and updated version syntax * Fixed link * Fixed formatting and removed vestigial sentence --- .../configure-panel-options/index.md | 119 ++++++------------ 1 file changed, 37 insertions(+), 82 deletions(-) diff --git a/docs/sources/panels-visualizations/configure-panel-options/index.md b/docs/sources/panels-visualizations/configure-panel-options/index.md index e6946c8567..72483d4e45 100644 --- a/docs/sources/panels-visualizations/configure-panel-options/index.md +++ b/docs/sources/panels-visualizations/configure-panel-options/index.md @@ -25,111 +25,66 @@ weight: 50 # Configure panel options -A Grafana panel is a visual representation of data that you can customize by defining a data source query, transforming and formatting data, and configuring visualization settings. +There are settings common to all visualizations, which you set in the **Panel options** section of the panel editor pane. The following sections describe these options as well as how to set them. -A panel editor includes a query builder and a series of options that you can use to transform data and add information to your panels. +## Panel options -This topic describes how to: +Set the following options to provide basic information about a panel and define basic display elements: -- Open a panel for editing -- Add a panel title and description -- View a panel JSON model -- Configure repeating rows and panels +| Option | Description | +| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Title | Text entered in this field appears at the top of your panel in the panel editor and in the dashboard. You can use [variables you have defined][] in the **Title** field, but not [global variables][]. | +| Description | Text entered in this field appears in a tooltip in the upper-left corner of the panel. Add a description to a panel to share with users any important information about it, such as its purpose. You can use [variables you have defined][] in the **Description** field, but not [global variables][]. | +| Transparent background | Toggle this switch on and off to control whether or not the panel has the same background color as the dashboard. | +| Panel links | Add [links to the panel][] to create shortcuts to other dashboards, panels, and external websites. Access panel links by clicking the icon next to the panel title. | +| Repeat options | Set whether to repeat the panel for each value in the selected variable. For more information, refer to [Configure repeating panels](#configure-repeating-panels). | -## Edit a panel - -After you add a panel to a dashboard, you can open it at any time to change or update queries, add data transformation, and change visualization settings. - -1. Open the dashboard that contains the panel you want to edit. - -1. Hover over any part of the panel to display the actions menu on the top right corner. - -1. Click the menu and select **Edit**. - - ![Panel with menu displayed](/media/docs/grafana/screenshot-panel-menu.png) - - To use a keyboard shortcut to open the panel, hover over the panel and press `e`. - - The panel opens in edit mode. - -## Add a title and description to a panel - -You can use generative AI to create panel titles and descriptions with the [Grafana LLM plugin][], which is currently in public preview. To enable this, refer to the [Set up generative AI features for dashboards documentation][]. Alternatively, you can take the following steps to create them yourself. - -Add a title and description to a panel to share with users any important information about the visualization. For example, use the description to document the purpose of the visualization. - -1. [Edit a panel](#edit-a-panel). - -1. In the panel display options pane, locate the **Panel options** section. - -1. Enter a **Title**. - - Text entered in this field appears at the top of your panel in the panel editor and in the dashboard. - -1. Write a description of the panel and the data you are displaying. - - Text entered in this field appears in a tooltip in the upper-left corner of the panel. - - You can use [variables you have defined][] in the **Title** and **Description** field, but not [global variables][]. - - ![Panel editor pane with Panel options section expanded](/static/img/docs/panels/panel-options-8-0.png) - -## View a panel JSON model - -Explore and export panel, panel data, and data frame JSON models. - -1. Open the dashboard that contains the panel. - -1. Hover over any part of the panel to display the actions menu on the top right corner. -1. Click the menu and select **Inspect > Panel JSON**. -1. In the **Select source** field, select one of the following options: - - - **Panel JSON:** Displays a JSON object representing the panel. - - **Panel data:** Displays a JSON object representing the data that was passed to the panel. - - **DataFrame structure:** Displays the data structure of the panel, including any transformations, field configurations, and override configurations that have been applied. - -1. To explore the JSON, click `>` to expand or collapse portions of the JSON model. +You can use generative AI to populate the **Title** and **Description** fields with the [Grafana LLM plugin][], which is currently in public preview. To enable this, refer to [Set up generative AI features for dashboards][]. ## Configure repeating panels You can configure Grafana to dynamically add panels or rows to a dashboard. A dynamic panel is a panel that the system creates based on the value of a variable. Variables dynamically change your queries across all panels in a dashboard. For more information about repeating rows, refer to [Configure repeating rows][]. -{{% admonition type="note" %}} -Repeating panels require variables to have one or more items selected; you can't repeat a panel zero times to hide it. -{{% /admonition %}} - To see an example of repeating panels, refer to [this dashboard with repeating panels](https://play.grafana.org/d/testdata-repeating/testdata-repeating-panels?orgId=1). **Before you begin:** - Ensure that the query includes a multi-value variable. -**To configure repeating panels:** - -1. [Edit the panel](#edit-a-panel) you want to repeat. - -1. On the display options pane, click **Panel options > Repeat options**. +To configure repeating panels, follow these steps: -1. Select a `direction`. +1. Navigate to the panel you want to update. +1. Hover over any part of the panel to display the menu on the top right corner. +1. Click the menu and select **Edit**. +1. Open the **Panel options** section of the panel editor pane. +1. Under **Repeat options**, select a variable in the **Repeat by variable** drop-down list. +1. Under **Repeat direction**, choose one of the following: - - Choose `horizontal` to arrange panels side-by-side. Grafana adjusts the width of a repeated panel. Currently, you can't mix other panels on a row with a repeated panel. - - Choose `vertical` to arrange panels in a column. The width of repeated panels is the same as the original, repeated panel. + - **Horizontal** - Arrange panels side-by-side. Grafana adjusts the width of a repeated panel. You can't mix other panels on a row with a repeated panel. + - **Vertical** - Arrange panels in a column. The width of repeated panels is the same as the original, repeated panel. +1. If you selected **Horizontal** in the previous step, select a value in the **Max per row** drop-down list to control the maximum number of panels that can be in a row. +1. Click **Save**. 1. To propagate changes to all panels, reload the dashboard. +You can stop a panel from repeating by selecting **Disable repeating** in the **Repeat by variable** drop-down list. + {{% docs/reference %}} -[variables you have defined]: "/docs/grafana/ -> /docs/grafana//dashboards/variables" -[variables you have defined]: "/docs/grafana-cloud/ -> /docs/grafana//dashboards/variables" +[variables you have defined]: "/docs/grafana/ -> /docs/grafana//dashboards/variables" +[variables you have defined]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/dashboards/variables" + +[global variables]: "/docs/grafana/ -> /docs/grafana//dashboards/variables/add-template-variables#global-variables" +[global variables]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/dashboards/variables/add-template-variables#global-variables" -[global variables]: "/docs/grafana/ -> /docs/grafana//dashboards/variables/add-template-variables#global-variables" -[global variables]: "/docs/grafana-cloud/ -> /docs/grafana//dashboards/variables/add-template-variables#global-variables" +[Configure repeating rows]: "/docs/grafana/ -> /docs/grafana//dashboards/build-dashboards/create-dashboard#configure-repeating-rows" +[Configure repeating rows]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/dashboards/build-dashboards/create-dashboard#configure-repeating-rows" -[Configure repeating rows]: "/docs/grafana/ -> /docs/grafana//dashboards/build-dashboards/create-dashboard#configure-repeating-rows" -[Configure repeating rows]: "/docs/grafana-cloud/ -> /docs/grafana//dashboards/build-dashboards/create-dashboard#configure-repeating-rows" +[Grafana LLM plugin]: "/docs/grafana/ -> /docs/grafana-cloud/alerting-and-irm/machine-learning/configure/llm-plugin" +[Grafana LLM plugin]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/machine-learning/configure/llm-plugin" -[Grafana LLM plugin]: "/docs/grafana/ -> /docs/grafana-cloud/alerting-and-irm/machine-learning/llm-plugin" -[Grafana LLM plugin]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/machine-learning/llm-plugin" +[Set up generative AI features for dashboards]: "/docs/grafana/ -> /docs/grafana//dashboards/manage-dashboards#set-up-generative-ai-features-for-dashboards" +[Set up generative AI features for dashboards]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/dashboards/manage-dashboards#set-up-generative-ai-features-for-dashboards" -[Set up generative AI features for dashboards documentation]: "/docs/grafana/ -> /docs/grafana//dashboards/manage-dashboards#set-up-generative-ai-features-for-dashboards" -[Set up generative AI features for dashboards documentation]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/dashboards/manage-dashboards#set-up-generative-ai-features-for-dashboards" +[links to the panel]: "/docs/grafana/ -> /docs/grafana//dashboards/build-dashboards/manage-dashboard-links#panel-links" +[links to the panel]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/dashboards/build-dashboards/manage-dashboard-links#panel-links" {{% /docs/reference %}} From 8ed932bb603d4219f6ceb8421b937e5015f06a91 Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Thu, 29 Feb 2024 07:38:09 +0100 Subject: [PATCH 0287/1406] Select: Add instructions for resetting a value (#83603) --- .../src/components/Select/Select.mdx | 71 ++++++++++++++++--- 1 file changed, 60 insertions(+), 11 deletions(-) diff --git a/packages/grafana-ui/src/components/Select/Select.mdx b/packages/grafana-ui/src/components/Select/Select.mdx index 78318d5931..5af27ea234 100644 --- a/packages/grafana-ui/src/components/Select/Select.mdx +++ b/packages/grafana-ui/src/components/Select/Select.mdx @@ -1,8 +1,11 @@ -import { ArgTypes, Preview } from '@storybook/blocks'; -import { Select, AsyncSelect, MultiSelect, AsyncMultiSelect } from './Select'; -import { generateOptions } from './mockOptions'; +import { ArgTypes } from '@storybook/blocks'; +import { Select, AsyncSelect } from './Select'; -# Select variants +# Select + +Select is the base for every component on this page. The approaches mentioned here are also applicable to `AsyncSelect`, `MultiSelect`, `AsyncMultiSelect`. + +## Select variants Select is an input with the ability to search and create new values. It should be used when you have a list of options. If the data has a tree structure, consider using `Cascader` instead. Select has some features: @@ -12,10 +15,6 @@ Select has some features: - Select from async data - Create custom values that aren't in the list -## Select - -Select is the base for every component on this page. The approaches mentioned here are also applicable to `AsyncSelect`, `MultiSelect`, `AsyncMultiSelect`. - ### Options format There are four properties for each option: @@ -61,6 +60,57 @@ const SelectComponent = () => { }; ``` +### Resetting selected value from outside the component + +If you want to reset the selected value from outside the component, e.g. if there are two Select components that should be in sync, you can set the dependent Select value to `null` in the `onChange` handler of the first Select component. + +```tsx +import React, { useState } from 'react'; +import { Select } from '@grafana/ui'; + +const SelectComponent = () => { + const [person, setPerson] = useState(''); + const [team, setTeam] = useState(''); + + return ( +
+ setTeam(value)} + options={[ + { + value: 'team1', + label: 'Team 1', + }, + { + value: 'team', + label: 'Team 2', + }, + ]} + value={team} + /> +
+ ); +}; +``` + ## AsyncSelect Like regular Select, but handles fetching options asynchronously. Use the `loadOptions` prop for the async function that loads the options. If `defaultOptions` is set to `true`, `loadOptions` will be called when the component is mounted. @@ -83,7 +133,6 @@ const basicSelectAsync = () => { /> ); }; - ``` Where the async function could look like this: @@ -126,7 +175,7 @@ const multiSelect = () => { Like MultiSelect but handles data asynchronously with the `loadOptions` prop. -# Testing +## Testing Using React Testing Library, you can select the ` + + + + Your self-managed Grafana installation needs special access to securely migrate content. You'll + need to create a migration token on your chosen cloud stack. + + + + + Log into your cloud stack and navigate to Administration, General, Migrate to Grafana Cloud. Create a + migration token on that screen and paste the token here. + + + + + + + + + + + + + + ); +}; + +const getStyles = (theme: GrafanaTheme2) => ({ + field: css({ + alignSelf: 'stretch', + }), +}); diff --git a/public/app/features/admin/migrate-to-cloud/onprem/EmptyState/EmptyState.tsx b/public/app/features/admin/migrate-to-cloud/onprem/EmptyState/EmptyState.tsx index 02183bbc95..30d714ffd5 100644 --- a/public/app/features/admin/migrate-to-cloud/onprem/EmptyState/EmptyState.tsx +++ b/public/app/features/admin/migrate-to-cloud/onprem/EmptyState/EmptyState.tsx @@ -4,7 +4,7 @@ import React from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { Grid, Stack, useStyles2 } from '@grafana/ui'; -import { CallToAction } from './CallToAction'; +import { CallToAction } from './CallToAction/CallToAction'; import { InfoPaneLeft } from './InfoPaneLeft'; import { InfoPaneRight } from './InfoPaneRight'; diff --git a/public/app/features/admin/migrate-to-cloud/onprem/Page.tsx b/public/app/features/admin/migrate-to-cloud/onprem/Page.tsx index ee3bbdde69..eddccd4103 100644 --- a/public/app/features/admin/migrate-to-cloud/onprem/Page.tsx +++ b/public/app/features/admin/migrate-to-cloud/onprem/Page.tsx @@ -1,8 +1,39 @@ import React from 'react'; +import { Button, ModalsController, Stack, Text } from '@grafana/ui'; +import { Trans } from 'app/core/internationalization'; + +import { useDisconnectStackMutation, useGetStatusQuery } from '../api'; + +import { DisconnectModal } from './DisconnectModal'; import { EmptyState } from './EmptyState/EmptyState'; export const Page = () => { - // TODO logic to determine whether to show the empty state or the resource table - return ; + const { data, isFetching } = useGetStatusQuery(); + const [disconnectStack, disconnectResponse] = useDisconnectStackMutation(); + if (!data?.enabled) { + return ; + } + + return ( + + {({ showModal, hideModal }) => ( + + {data.stackURL && {data.stackURL}} + + + )} + + ); }; diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 6c72ea3aee..ac5a19e643 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -697,10 +697,35 @@ "link-title": "Learn about migrating other settings", "title": "Can I move this installation to Grafana Cloud?" }, + "connect-modal": { + "body-cloud-stack": "You'll also need a cloud stack. If you just signed up, we'll automatically create your first stack. If you have an account, you'll need to select or create a stack.", + "body-get-started": "To get started, you'll need a Grafana.com account.", + "body-paste-stack": "Once you've decided on a stack, paste the URL below.", + "body-sign-up": "Sign up for a Grafana.com account", + "body-token": "Your self-managed Grafana installation needs special access to securely migrate content. You'll need to create a migration token on your chosen cloud stack.", + "body-token-field": "Migration token", + "body-token-field-placeholder": "Paste token here", + "body-token-instructions": "Log into your cloud stack and navigate to Administration, General, Migrate to Grafana Cloud. Create a migration token on that screen and paste the token here.", + "body-url-field": "Cloud stack URL", + "body-view-stacks": "View my cloud stacks", + "cancel": "Cancel", + "connect": "Connect to this stack", + "connecting": "Connecting to this stack...", + "stack-required-error": "Stack URL is required", + "title": "Connect to a cloud stack", + "token-required-error": "Migration token is required" + }, "cta": { "button": "Migrate this instance to Cloud", "header": "Let us manage your Grafana stack" }, + "disconnect-modal": { + "body": "This will remove the migration token from this installation. If you wish to upload more resources in the future, you will need to enter a new migration token.", + "cancel": "Cancel", + "disconnect": "Disconnect", + "disconnecting": "Disconnecting...", + "title": "Disconnect from cloud stack" + }, "get-started": { "body": "The migration process must be started from your self-managed Grafana instance.", "configure-pdc-link": "Configure PDC for this stack", @@ -751,6 +776,9 @@ "link-title": "Grafana Cloud pricing", "title": "How much does it cost?" }, + "resources": { + "disconnect": "Disconnect" + }, "token-status": { "active": "Token created and active", "no-active": "No active token" diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index c1fe5d179e..94594cc7b2 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -697,10 +697,35 @@ "link-title": "Ŀęäřʼn äþőūŧ mįģřäŧįʼnģ őŧĥęř şęŧŧįʼnģş", "title": "Cäʼn Ĩ mővę ŧĥįş įʼnşŧäľľäŧįőʼn ŧő Ğřäƒäʼnä Cľőūđ?" }, + "connect-modal": { + "body-cloud-stack": "Ÿőū'ľľ äľşő ʼnęęđ ä čľőūđ şŧäčĸ. Ĩƒ yőū ĵūşŧ şįģʼnęđ ūp, ŵę'ľľ äūŧőmäŧįčäľľy čřęäŧę yőūř ƒįřşŧ şŧäčĸ. Ĩƒ yőū ĥävę äʼn äččőūʼnŧ, yőū'ľľ ʼnęęđ ŧő şęľęčŧ őř čřęäŧę ä şŧäčĸ.", + "body-get-started": "Ŧő ģęŧ şŧäřŧęđ, yőū'ľľ ʼnęęđ ä Ğřäƒäʼnä.čőm äččőūʼnŧ.", + "body-paste-stack": "Øʼnčę yőū'vę đęčįđęđ őʼn ä şŧäčĸ, päşŧę ŧĥę ŮŖĿ þęľőŵ.", + "body-sign-up": "Ŝįģʼn ūp ƒőř ä Ğřäƒäʼnä.čőm äččőūʼnŧ", + "body-token": "Ÿőūř şęľƒ-mäʼnäģęđ Ğřäƒäʼnä įʼnşŧäľľäŧįőʼn ʼnęęđş şpęčįäľ äččęşş ŧő şęčūřęľy mįģřäŧę čőʼnŧęʼnŧ. Ÿőū'ľľ ʼnęęđ ŧő čřęäŧę ä mįģřäŧįőʼn ŧőĸęʼn őʼn yőūř čĥőşęʼn čľőūđ şŧäčĸ.", + "body-token-field": "Mįģřäŧįőʼn ŧőĸęʼn", + "body-token-field-placeholder": "Päşŧę ŧőĸęʼn ĥęřę", + "body-token-instructions": "Ŀőģ įʼnŧő yőūř čľőūđ şŧäčĸ äʼnđ ʼnävįģäŧę ŧő Åđmįʼnįşŧřäŧįőʼn, Ğęʼnęřäľ, Mįģřäŧę ŧő Ğřäƒäʼnä Cľőūđ. Cřęäŧę ä mįģřäŧįőʼn ŧőĸęʼn őʼn ŧĥäŧ şčřęęʼn äʼnđ päşŧę ŧĥę ŧőĸęʼn ĥęřę.", + "body-url-field": "Cľőūđ şŧäčĸ ŮŖĿ", + "body-view-stacks": "Vįęŵ my čľőūđ şŧäčĸş", + "cancel": "Cäʼnčęľ", + "connect": "Cőʼnʼnęčŧ ŧő ŧĥįş şŧäčĸ", + "connecting": "Cőʼnʼnęčŧįʼnģ ŧő ŧĥįş şŧäčĸ...", + "stack-required-error": "Ŝŧäčĸ ŮŖĿ įş řęqūįřęđ", + "title": "Cőʼnʼnęčŧ ŧő ä čľőūđ şŧäčĸ", + "token-required-error": "Mįģřäŧįőʼn ŧőĸęʼn įş řęqūįřęđ" + }, "cta": { "button": "Mįģřäŧę ŧĥįş įʼnşŧäʼnčę ŧő Cľőūđ", "header": "Ŀęŧ ūş mäʼnäģę yőūř Ğřäƒäʼnä şŧäčĸ" }, + "disconnect-modal": { + "body": "Ŧĥįş ŵįľľ řęmővę ŧĥę mįģřäŧįőʼn ŧőĸęʼn ƒřőm ŧĥįş įʼnşŧäľľäŧįőʼn. Ĩƒ yőū ŵįşĥ ŧő ūpľőäđ mőřę řęşőūřčęş įʼn ŧĥę ƒūŧūřę, yőū ŵįľľ ʼnęęđ ŧő ęʼnŧęř ä ʼnęŵ mįģřäŧįőʼn ŧőĸęʼn.", + "cancel": "Cäʼnčęľ", + "disconnect": "Đįşčőʼnʼnęčŧ", + "disconnecting": "Đįşčőʼnʼnęčŧįʼnģ...", + "title": "Đįşčőʼnʼnęčŧ ƒřőm čľőūđ şŧäčĸ" + }, "get-started": { "body": "Ŧĥę mįģřäŧįőʼn přőčęşş mūşŧ þę şŧäřŧęđ ƒřőm yőūř şęľƒ-mäʼnäģęđ Ğřäƒäʼnä įʼnşŧäʼnčę.", "configure-pdc-link": "Cőʼnƒįģūřę PĐC ƒőř ŧĥįş şŧäčĸ", @@ -751,6 +776,9 @@ "link-title": "Ğřäƒäʼnä Cľőūđ přįčįʼnģ", "title": "Ħőŵ mūčĥ đőęş įŧ čőşŧ?" }, + "resources": { + "disconnect": "Đįşčőʼnʼnęčŧ" + }, "token-status": { "active": "Ŧőĸęʼn čřęäŧęđ äʼnđ äčŧįvę", "no-active": "Ńő äčŧįvę ŧőĸęʼn" From 5572158eeaf93eab48a0fee000c723bae45e2f95 Mon Sep 17 00:00:00 2001 From: David Harris Date: Thu, 29 Feb 2024 13:14:12 +0000 Subject: [PATCH 0300/1406] chore: update core plugin descriptions (#83449) * update alertmanager * fix typo * update candlestick * update histogram * update xy * fix prettier * dh-update-desc/ update snapshots * dh-update-desc/ revert variable * update failing test snapshot --------- Co-authored-by: jev forsberg Co-authored-by: nmarrs --- .../api/plugins/data/expectedListResp.json | 36 ++++++++++++++----- .../datasource/alertmanager/plugin.json | 3 +- .../app/plugins/panel/candlestick/plugin.json | 2 ++ .../app/plugins/panel/histogram/plugin.json | 2 ++ public/app/plugins/panel/xychart/plugin.json | 2 ++ 5 files changed, 36 insertions(+), 9 deletions(-) diff --git a/pkg/tests/api/plugins/data/expectedListResp.json b/pkg/tests/api/plugins/data/expectedListResp.json index fbbe082603..15ec9fe5d9 100644 --- a/pkg/tests/api/plugins/data/expectedListResp.json +++ b/pkg/tests/api/plugins/data/expectedListResp.json @@ -48,7 +48,7 @@ "name": "Prometheus alertmanager", "url": "https://grafana.com" }, - "description": "", + "description": "Add external Alertmanagers (supports Prometheus and Mimir implementations) so you can use the Grafana Alerting UI to manage silences, contact points, and notification policies.", "links": [ { "name": "Learn more", @@ -63,7 +63,14 @@ "screenshots": null, "version": "", "updated": "", - "keywords": null + "keywords": [ + "alerts", + "alerting", + "prometheus", + "alertmanager", + "mimir", + "cortex" + ] }, "dependencies": { "grafanaDependency": "", @@ -271,7 +278,7 @@ "name": "Grafana Labs", "url": "https://grafana.com" }, - "description": "", + "description": "Graphical representation of price movements of a security, derivative, or currency.", "links": null, "logos": { "small": "public/app/plugins/panel/candlestick/img/candlestick.svg", @@ -281,7 +288,12 @@ "screenshots": null, "version": "", "updated": "", - "keywords": null + "keywords": [ + "financial", + "price", + "currency", + "k-line" + ] }, "dependencies": { "grafanaDependency": "", @@ -870,7 +882,7 @@ "name": "Grafana Labs", "url": "https://grafana.com" }, - "description": "", + "description": "Distribution of values presented as a bar chart.", "links": null, "logos": { "small": "public/app/plugins/panel/histogram/img/histogram.svg", @@ -880,7 +892,12 @@ "screenshots": null, "version": "", "updated": "", - "keywords": null + "keywords": [ + "distribution", + "bar chart", + "frequency", + "proportional" + ] }, "dependencies": { "grafanaDependency": "", @@ -1896,7 +1913,7 @@ "name": "Grafana Labs", "url": "https://grafana.com" }, - "description": "", + "description": "Supports arbitrary X vs Y in a graph to visualize the relationship between two variables.", "links": null, "logos": { "small": "public/app/plugins/panel/xychart/img/icn-xychart.svg", @@ -1906,7 +1923,10 @@ "screenshots": null, "version": "", "updated": "", - "keywords": null + "keywords": [ + "scatter", + "plot" + ] }, "dependencies": { "grafanaDependency": "", diff --git a/public/app/plugins/datasource/alertmanager/plugin.json b/public/app/plugins/datasource/alertmanager/plugin.json index 33a087f536..9e51ce18e5 100644 --- a/public/app/plugins/datasource/alertmanager/plugin.json +++ b/public/app/plugins/datasource/alertmanager/plugin.json @@ -43,7 +43,8 @@ } ], "info": { - "description": "", + "description": "Add external Alertmanagers (supports Prometheus and Mimir implementations) so you can use the Grafana Alerting UI to manage silences, contact points, and notification policies.", + "keywords": ["alerts", "alerting", "prometheus", "alertmanager", "mimir", "cortex"], "author": { "name": "Prometheus alertmanager", "url": "https://grafana.com" diff --git a/public/app/plugins/panel/candlestick/plugin.json b/public/app/plugins/panel/candlestick/plugin.json index c7ab551101..f260e1dc95 100644 --- a/public/app/plugins/panel/candlestick/plugin.json +++ b/public/app/plugins/panel/candlestick/plugin.json @@ -4,6 +4,8 @@ "id": "candlestick", "info": { + "description": "Graphical representation of price movements of a security, derivative, or currency.", + "keywords": ["financial", "price", "currency", "k-line"], "author": { "name": "Grafana Labs", "url": "https://grafana.com" diff --git a/public/app/plugins/panel/histogram/plugin.json b/public/app/plugins/panel/histogram/plugin.json index a8f5500d40..f778c4dacf 100644 --- a/public/app/plugins/panel/histogram/plugin.json +++ b/public/app/plugins/panel/histogram/plugin.json @@ -4,6 +4,8 @@ "id": "histogram", "info": { + "description": "Distribution of values presented as a bar chart.", + "keywords": ["distribution", "bar chart", "frequency", "proportional"], "author": { "name": "Grafana Labs", "url": "https://grafana.com" diff --git a/public/app/plugins/panel/xychart/plugin.json b/public/app/plugins/panel/xychart/plugin.json index ed72646bf3..13721d65fb 100644 --- a/public/app/plugins/panel/xychart/plugin.json +++ b/public/app/plugins/panel/xychart/plugin.json @@ -5,6 +5,8 @@ "state": "beta", "info": { + "description": "Supports arbitrary X vs Y in a graph to visualize the relationship between two variables.", + "keywords": ["scatter", "plot"], "author": { "name": "Grafana Labs", "url": "https://grafana.com" From 9ab03d4e202d333b97b45afd2150da2cad72e16f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 29 Feb 2024 15:47:17 +0200 Subject: [PATCH 0301/1406] I18n: Download translations from Crowdin (#83605) New Crowdin translations by GitHub Action Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- public/locales/de-DE/grafana.json | 6 +++--- public/locales/es-ES/grafana.json | 6 +++--- public/locales/fr-FR/grafana.json | 6 +++--- public/locales/zh-Hans/grafana.json | 6 +++--- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/public/locales/de-DE/grafana.json b/public/locales/de-DE/grafana.json index be84f20067..573845f1b2 100644 --- a/public/locales/de-DE/grafana.json +++ b/public/locales/de-DE/grafana.json @@ -1415,10 +1415,10 @@ "expire-day": "1 Tag", "expire-hour": "1 Stunde", "expire-never": "Nie", - "expire-week": "7 Tage", + "expire-week": "", "info-text-1": "Ein Schnappschuss ist eine Möglichkeit, ein interaktives Dashboard sofort öffentlich zu teilen. Beim Erstellen entfernen wir sensible Daten wie Abfragen (Metriken, Vorlagen und Anmerkungen) und Panel-Links, sodass nur die sichtbaren Metrikdaten und die in dein Dashboard eingebetteten Seriennamen angezeigt werden.", "info-text-2": "Beachte, dass dein Schnappschuss <1>für jeden sichtbar ist, der den Link hat und auf die URL zugreifen kann. Teile Schnappschüsse daher mit Bedacht.", - "local-button": "Lokaler Schnappschuss", + "local-button": "", "mistake-message": "Hast du einen Fehler gemacht? ", "name": "Name des Schnappschusses", "timeout": "Timeout (Sekunden)", @@ -1431,7 +1431,7 @@ "library-panel": "Bibliotheks-Panel", "link": "Link", "panel-embed": "Einbetten", - "public-dashboard": "Öffentliches Dashboard", + "public-dashboard": "", "public-dashboard-title": "Öffentliches Dashboard", "snapshot": "Schnappschuss" }, diff --git a/public/locales/es-ES/grafana.json b/public/locales/es-ES/grafana.json index 232f18c072..3433d90c9f 100644 --- a/public/locales/es-ES/grafana.json +++ b/public/locales/es-ES/grafana.json @@ -1415,10 +1415,10 @@ "expire-day": "1 día", "expire-hour": "1 hora", "expire-never": "Nunca", - "expire-week": "7 días", + "expire-week": "", "info-text-1": "Una instantánea es una forma inmediata de compartir un panel interactivo públicamente. Cuando se crean, eliminamos datos confidenciales como consultas (métricas, plantilla y anotación) y enlaces de panel, dejando solo los datos de métricas visibles y los nombres de serie incrustados en el panel.", "info-text-2": "Tenga en cuenta que su instantánea <1>puede ser vista por cualquiera que tenga el enlace y pueda acceder a la URL. Comparta sus instantáneas con cautela.", - "local-button": "Instantánea local", + "local-button": "", "mistake-message": "¿Ha cometido un error? ", "name": "Nombre de la instantánea", "timeout": "Tiempo de espera (segundos)", @@ -1431,7 +1431,7 @@ "library-panel": "Panel de librería", "link": "Enlace", "panel-embed": "Incrustar", - "public-dashboard": "Tablero público", + "public-dashboard": "", "public-dashboard-title": "Tablero público", "snapshot": "Instantánea" }, diff --git a/public/locales/fr-FR/grafana.json b/public/locales/fr-FR/grafana.json index 7e0283a931..054e5c34ad 100644 --- a/public/locales/fr-FR/grafana.json +++ b/public/locales/fr-FR/grafana.json @@ -1415,10 +1415,10 @@ "expire-day": "1 jour", "expire-hour": "1 heure", "expire-never": "Jamais", - "expire-week": "7 jours", + "expire-week": "", "info-text-1": "Un instantané est un moyen instantané de partager publiquement un tableau de bord interactif. Lors de la création, nous supprimons les données sensibles telles que les requêtes (métrique, modèle et annotation) et les liens du panneau, pour ne laisser que les métriques visibles et les noms de séries intégrés dans votre tableau de bord.", "info-text-2": "N'oubliez pas que votre instantané <1>peut être consulté par une personne qui dispose du lien et qui peut accéder à l'URL. Partagez judicieusement.", - "local-button": "Instantané local", + "local-button": "", "mistake-message": "Avez-vous commis une erreur ? ", "name": "Nom de l'instantané", "timeout": "Délai d’expiration (secondes)", @@ -1431,7 +1431,7 @@ "library-panel": "Panneau de bibliothèque", "link": "Lien", "panel-embed": "Intégrer", - "public-dashboard": "Tableau de bord public", + "public-dashboard": "", "public-dashboard-title": "Tableau de bord public", "snapshot": "Instantané" }, diff --git a/public/locales/zh-Hans/grafana.json b/public/locales/zh-Hans/grafana.json index b9dc3b97a8..8a7192e01c 100644 --- a/public/locales/zh-Hans/grafana.json +++ b/public/locales/zh-Hans/grafana.json @@ -1409,10 +1409,10 @@ "expire-day": "1 天", "expire-hour": "1 小时", "expire-never": "从不", - "expire-week": "7 天", + "expire-week": "", "info-text-1": "可通过快照即时、公开地分享交互式仪表板。创建后,我们会剥离敏感数据,如查询(指标、模板和注释)和面板链接,仅将可见指标数据和系列名称嵌入到仪表板中。", "info-text-2": "请注意,知晓该链接并能够访问该网址的<1>任何人都可以查看您的快照。分享需谨慎。", - "local-button": "本地快照", + "local-button": "", "mistake-message": "是不是弄错了什么?", "name": "快照名称", "timeout": "超时(秒)", @@ -1425,7 +1425,7 @@ "library-panel": "库面板", "link": "链接", "panel-embed": "嵌入", - "public-dashboard": "公共仪表板", + "public-dashboard": "", "public-dashboard-title": "公共仪表板", "snapshot": "快照" }, From ca81d1f22da5ac912c5f81598aae233cd82423de Mon Sep 17 00:00:00 2001 From: Esteban Beltran Date: Thu, 29 Feb 2024 15:25:33 +0100 Subject: [PATCH 0302/1406] Revert "Chore: Remove components from the graveyard folder in grafana/ui" (#83687) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Revert "Chore: Remove components from the graveyard folder in grafana/ui (#83…" This reverts commit 65174311659277e6c10730f16f61cb04c2df1db8. --- .betterer.results | 28 + .github/CODEOWNERS | 2 + .../src/components/Menu/Menu.story.tsx | 25 + packages/grafana-ui/src/components/index.ts | 17 + .../src/components/uPlot/utils.test.ts | 4 +- .../src/graveyard/Graph/Graph.test.tsx | 185 +++++ .../grafana-ui/src/graveyard/Graph/Graph.tsx | 400 +++++++++++ .../src/graveyard/Graph}/GraphContextMenu.tsx | 28 +- .../graveyard/Graph/GraphSeriesToggler.tsx | 89 +++ .../Graph/GraphTooltip/GraphTooltip.tsx | 41 ++ .../MultiModeGraphTooltip.test.tsx | 106 +++ .../GraphTooltip/MultiModeGraphTooltip.tsx | 49 ++ .../GraphTooltip/SingleModeGraphTooltip.tsx | 48 ++ .../src/graveyard/Graph/GraphTooltip/types.ts | 16 + .../Graph/GraphWithLegend.internal.story.tsx | 141 ++++ .../src/graveyard/Graph/GraphWithLegend.tsx | 132 ++++ .../grafana-ui/src/graveyard/Graph/types.ts | 9 + .../src/graveyard/Graph/utils.test.ts | 233 ++++++ .../grafana-ui/src/graveyard/Graph/utils.ts | 147 ++++ .../src/graveyard/GraphNG/GraphNG.tsx | 275 +++++++ .../GraphNG/__snapshots__/utils.test.ts.snap | 245 +++++++ .../grafana-ui/src/graveyard/GraphNG/hooks.ts | 41 ++ .../src/graveyard/GraphNG/utils.test.ts | 522 ++++++++++++++ packages/grafana-ui/src/graveyard/README.md | 3 - .../src/graveyard/TimeSeries/TimeSeries.tsx | 63 ++ .../src/graveyard/TimeSeries/utils.test.ts | 274 +++++++ .../src/graveyard/TimeSeries/utils.ts | 668 ++++++++++++++++++ public/app/angular/angular_wrappers.ts | 3 +- .../timeseries/plugins/ContextMenuPlugin.tsx | 11 +- 29 files changed, 3777 insertions(+), 28 deletions(-) create mode 100644 packages/grafana-ui/src/graveyard/Graph/Graph.test.tsx create mode 100644 packages/grafana-ui/src/graveyard/Graph/Graph.tsx rename {public/app/angular/components/legacy_graph_panel => packages/grafana-ui/src/graveyard/Graph}/GraphContextMenu.tsx (86%) create mode 100644 packages/grafana-ui/src/graveyard/Graph/GraphSeriesToggler.tsx create mode 100644 packages/grafana-ui/src/graveyard/Graph/GraphTooltip/GraphTooltip.tsx create mode 100644 packages/grafana-ui/src/graveyard/Graph/GraphTooltip/MultiModeGraphTooltip.test.tsx create mode 100644 packages/grafana-ui/src/graveyard/Graph/GraphTooltip/MultiModeGraphTooltip.tsx create mode 100644 packages/grafana-ui/src/graveyard/Graph/GraphTooltip/SingleModeGraphTooltip.tsx create mode 100644 packages/grafana-ui/src/graveyard/Graph/GraphTooltip/types.ts create mode 100644 packages/grafana-ui/src/graveyard/Graph/GraphWithLegend.internal.story.tsx create mode 100644 packages/grafana-ui/src/graveyard/Graph/GraphWithLegend.tsx create mode 100644 packages/grafana-ui/src/graveyard/Graph/types.ts create mode 100644 packages/grafana-ui/src/graveyard/Graph/utils.test.ts create mode 100644 packages/grafana-ui/src/graveyard/Graph/utils.ts create mode 100644 packages/grafana-ui/src/graveyard/GraphNG/GraphNG.tsx create mode 100644 packages/grafana-ui/src/graveyard/GraphNG/__snapshots__/utils.test.ts.snap create mode 100644 packages/grafana-ui/src/graveyard/GraphNG/hooks.ts create mode 100644 packages/grafana-ui/src/graveyard/GraphNG/utils.test.ts create mode 100644 packages/grafana-ui/src/graveyard/TimeSeries/TimeSeries.tsx create mode 100644 packages/grafana-ui/src/graveyard/TimeSeries/utils.test.ts create mode 100644 packages/grafana-ui/src/graveyard/TimeSeries/utils.ts diff --git a/.betterer.results b/.betterer.results index c303a90fc4..2e2644ad9a 100644 --- a/.betterer.results +++ b/.betterer.results @@ -1024,6 +1024,29 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Do not use any type assertions.", "1"] ], + "packages/grafana-ui/src/graveyard/Graph/GraphContextMenu.tsx:5381": [ + [0, 0, 0, "Unexpected any. Specify a different type.", "0"] + ], + "packages/grafana-ui/src/graveyard/Graph/utils.ts:5381": [ + [0, 0, 0, "Unexpected any. Specify a different type.", "0"] + ], + "packages/grafana-ui/src/graveyard/GraphNG/GraphNG.tsx:5381": [ + [0, 0, 0, "Unexpected any. Specify a different type.", "0"], + [0, 0, 0, "Unexpected any. Specify a different type.", "1"], + [0, 0, 0, "Unexpected any. Specify a different type.", "2"], + [0, 0, 0, "Unexpected any. Specify a different type.", "3"], + [0, 0, 0, "Unexpected any. Specify a different type.", "4"], + [0, 0, 0, "Do not use any type assertions.", "5"], + [0, 0, 0, "Do not use any type assertions.", "6"], + [0, 0, 0, "Do not use any type assertions.", "7"], + [0, 0, 0, "Unexpected any. Specify a different type.", "8"], + [0, 0, 0, "Do not use any type assertions.", "9"], + [0, 0, 0, "Do not use any type assertions.", "10"], + [0, 0, 0, "Do not use any type assertions.", "11"] + ], + "packages/grafana-ui/src/graveyard/GraphNG/hooks.ts:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"] + ], "packages/grafana-ui/src/graveyard/GraphNG/nullInsertThreshold.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"], @@ -1035,6 +1058,11 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "1"], [0, 0, 0, "Do not use any type assertions.", "2"] ], + "packages/grafana-ui/src/graveyard/TimeSeries/utils.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, "Unexpected any. Specify a different type.", "2"] + ], "packages/grafana-ui/src/options/builder/axis.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"], diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 1139ac6950..b18eeac035 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -332,7 +332,9 @@ /packages/grafana-ui/src/components/VizRepeater/ @grafana/dataviz-squad /packages/grafana-ui/src/components/VizTooltip/ @grafana/dataviz-squad /packages/grafana-ui/src/components/Sparkline/ @grafana/grafana-frontend-platform @grafana/app-o11y-visualizations +/packages/grafana-ui/src/graveyard/Graph/ @grafana/dataviz-squad /packages/grafana-ui/src/graveyard/GraphNG/ @grafana/dataviz-squad +/packages/grafana-ui/src/graveyard/TimeSeries/ @grafana/dataviz-squad /packages/grafana-ui/src/utils/storybook/ @grafana/plugins-platform-frontend /packages/grafana-data/src/transformations/ @grafana/dataviz-squad /packages/grafana-data/src/**/*logs* @grafana/observability-logs diff --git a/packages/grafana-ui/src/components/Menu/Menu.story.tsx b/packages/grafana-ui/src/components/Menu/Menu.story.tsx index a92e0755e7..4a547fb84a 100644 --- a/packages/grafana-ui/src/components/Menu/Menu.story.tsx +++ b/packages/grafana-ui/src/components/Menu/Menu.story.tsx @@ -1,6 +1,7 @@ import { Meta } from '@storybook/react'; import React from 'react'; +import { GraphContextMenuHeader } from '..'; import { StoryExample } from '../../utils/storybook/StoryExample'; import { VerticalGroup } from '../Layout/Layout'; @@ -109,6 +110,30 @@ export function Examples() { + + + } + ariaLabel="Menu header" + > + + + + + + + + + diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index cd3c980768..ec094bea02 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -150,6 +150,7 @@ export { VizLegend } from './VizLegend/VizLegend'; export { VizLegendListItem } from './VizLegend/VizLegendListItem'; export { Alert, type AlertVariant } from './Alert/Alert'; +export { GraphSeriesToggler, type GraphSeriesTogglerAPI } from '../graveyard/Graph/GraphSeriesToggler'; export { Collapse, ControlledCollapse } from './Collapse/Collapse'; export { CollapsableSection } from './Collapse/CollapsableSection'; export { DataLinkButton } from './DataLinks/DataLinkButton'; @@ -295,3 +296,19 @@ export { type UPlotConfigPrepFn } from './uPlot/config/UPlotConfigBuilder'; export * from './PanelChrome/types'; export { Label as BrowserLabel } from './BrowserLabel/Label'; export { PanelContainer } from './PanelContainer/PanelContainer'; + +// ----------------------------------------------------- +// Graveyard: exported, but no longer used internally +// These will be removed in the future +// ----------------------------------------------------- + +export { Graph } from '../graveyard/Graph/Graph'; +export { GraphWithLegend } from '../graveyard/Graph/GraphWithLegend'; +export { GraphContextMenu, GraphContextMenuHeader } from '../graveyard/Graph/GraphContextMenu'; +export { graphTimeFormat, graphTickFormatter } from '../graveyard/Graph/utils'; + +export { GraphNG, type GraphNGProps } from '../graveyard/GraphNG/GraphNG'; +export { TimeSeries } from '../graveyard/TimeSeries/TimeSeries'; +export { useGraphNGContext } from '../graveyard/GraphNG/hooks'; +export { preparePlotFrame, buildScaleKey } from '../graveyard/GraphNG/utils'; +export { type GraphNGLegendEvent } from '../graveyard/GraphNG/types'; diff --git a/packages/grafana-ui/src/components/uPlot/utils.test.ts b/packages/grafana-ui/src/components/uPlot/utils.test.ts index 363c49e3d5..3ecbc180bd 100644 --- a/packages/grafana-ui/src/components/uPlot/utils.test.ts +++ b/packages/grafana-ui/src/components/uPlot/utils.test.ts @@ -1,9 +1,7 @@ import { FieldMatcherID, fieldMatchers, FieldType, MutableDataFrame } from '@grafana/data'; import { BarAlignment, GraphDrawStyle, GraphTransform, LineInterpolation, StackingMode } from '@grafana/schema'; -// required for tests... but we actually have a duplicate copy that is used in the timeseries panel -// https://github.com/grafana/grafana/blob/v10.3.3/public/app/core/components/GraphNG/utils.test.ts -import { preparePlotFrame } from '../../graveyard/GraphNG/utils'; +import { preparePlotFrame } from '..'; import { getStackingGroups, preparePlotData2, timeFormatToTemplate } from './utils'; diff --git a/packages/grafana-ui/src/graveyard/Graph/Graph.test.tsx b/packages/grafana-ui/src/graveyard/Graph/Graph.test.tsx new file mode 100644 index 0000000000..847d6e883c --- /dev/null +++ b/packages/grafana-ui/src/graveyard/Graph/Graph.test.tsx @@ -0,0 +1,185 @@ +import { act, render, screen } from '@testing-library/react'; +import $ from 'jquery'; +import React from 'react'; + +import { GraphSeriesXY, FieldType, dateTime, FieldColorModeId, DisplayProcessor } from '@grafana/data'; +import { TooltipDisplayMode } from '@grafana/schema'; + +import { VizTooltip } from '../../components/VizTooltip'; + +import Graph from './Graph'; + +const display: DisplayProcessor = (v) => ({ numeric: Number(v), text: String(v), color: 'red' }); + +const series: GraphSeriesXY[] = [ + { + data: [ + [1546372800000, 10], + [1546376400000, 20], + [1546380000000, 10], + ], + color: 'red', + isVisible: true, + label: 'A-series', + seriesIndex: 0, + timeField: { + type: FieldType.time, + name: 'time', + values: [1546372800000, 1546376400000, 1546380000000], + config: {}, + }, + valueField: { + type: FieldType.number, + name: 'a-series', + values: [10, 20, 10], + config: { color: { mode: FieldColorModeId.Fixed, fixedColor: 'red' } }, + display, + }, + timeStep: 3600000, + yAxis: { + index: 0, + }, + }, + { + data: [ + [1546372800000, 20], + [1546376400000, 30], + [1546380000000, 40], + ], + color: 'blue', + isVisible: true, + label: 'B-series', + seriesIndex: 0, + timeField: { + type: FieldType.time, + name: 'time', + values: [1546372800000, 1546376400000, 1546380000000], + config: {}, + }, + valueField: { + type: FieldType.number, + name: 'b-series', + values: [20, 30, 40], + config: { color: { mode: FieldColorModeId.Fixed, fixedColor: 'blue' } }, + display, + }, + timeStep: 3600000, + yAxis: { + index: 0, + }, + }, +]; + +const mockTimeRange = { + from: dateTime(1546372800000), + to: dateTime(1546380000000), + raw: { + from: dateTime(1546372800000), + to: dateTime(1546380000000), + }, +}; + +const mockGraphProps = (multiSeries = false) => { + return { + width: 200, + height: 100, + series, + timeRange: mockTimeRange, + timeZone: 'browser', + }; +}; + +window.ResizeObserver = class ResizeObserver { + constructor() {} + observe() {} + unobserve() {} + disconnect() {} +}; + +describe('Graph', () => { + describe('with tooltip', () => { + describe('in single mode', () => { + it("doesn't render tooltip when not hovering over a datapoint", () => { + const graphWithTooltip = ( + + + + ); + render(graphWithTooltip); + + const timestamp = screen.queryByLabelText('Timestamp'); + const tableRow = screen.queryByTestId('SeriesTableRow'); + const seriesIcon = screen.queryByTestId('series-icon'); + + expect(timestamp).toBeFalsy(); + expect(timestamp?.parentElement).toBeFalsy(); + expect(tableRow?.parentElement).toBeFalsy(); + expect(seriesIcon).toBeFalsy(); + }); + + it('renders tooltip when hovering over a datapoint', () => { + // Given + const graphWithTooltip = ( + + + + ); + render(graphWithTooltip); + const eventArgs = { + pos: { + x: 120, + y: 50, + }, + activeItem: { + seriesIndex: 0, + dataIndex: 1, + series: { seriesIndex: 0 }, + }, + }; + act(() => { + $('div.graph-panel__chart').trigger('plothover', [eventArgs.pos, eventArgs.activeItem]); + }); + const timestamp = screen.getByLabelText('Timestamp'); + const tooltip = screen.getByTestId('SeriesTableRow').parentElement; + + expect(timestamp.parentElement?.isEqualNode(tooltip)).toBe(true); + expect(screen.getAllByTestId('series-icon')).toHaveLength(1); + }); + }); + + describe('in All Series mode', () => { + it('it renders all series summary regardless of mouse position', () => { + // Given + const graphWithTooltip = ( + + + + ); + render(graphWithTooltip); + + // When + const eventArgs = { + // This "is" more or less between first and middle point. Flot would not have picked any point as active one at this position + pos: { + x: 80, + y: 50, + }, + activeItem: null, + }; + // Then + act(() => { + $('div.graph-panel__chart').trigger('plothover', [eventArgs.pos, eventArgs.activeItem]); + }); + const timestamp = screen.getByLabelText('Timestamp'); + + const tableRows = screen.getAllByTestId('SeriesTableRow'); + expect(tableRows).toHaveLength(2); + expect(timestamp.parentElement?.isEqualNode(tableRows[0].parentElement)).toBe(true); + expect(timestamp.parentElement?.isEqualNode(tableRows[1].parentElement)).toBe(true); + + const seriesIcon = screen.getAllByTestId('series-icon'); + expect(seriesIcon).toHaveLength(2); + }); + }); + }); +}); diff --git a/packages/grafana-ui/src/graveyard/Graph/Graph.tsx b/packages/grafana-ui/src/graveyard/Graph/Graph.tsx new file mode 100644 index 0000000000..624d19e29c --- /dev/null +++ b/packages/grafana-ui/src/graveyard/Graph/Graph.tsx @@ -0,0 +1,400 @@ +// Libraries +import $ from 'jquery'; +import { uniqBy } from 'lodash'; +import React, { PureComponent } from 'react'; + +// Types +import { TimeRange, GraphSeriesXY, TimeZone, createDimension } from '@grafana/data'; +import { TooltipDisplayMode } from '@grafana/schema'; + +import { VizTooltipProps, VizTooltipContentProps, ActiveDimensions, VizTooltip } from '../../components/VizTooltip'; +import { FlotPosition } from '../../components/VizTooltip/VizTooltip'; + +import { GraphContextMenu, GraphContextMenuProps, ContextDimensions } from './GraphContextMenu'; +import { GraphTooltip } from './GraphTooltip/GraphTooltip'; +import { GraphDimensions } from './GraphTooltip/types'; +import { FlotItem } from './types'; +import { graphTimeFormat, graphTickFormatter } from './utils'; + +/** @deprecated */ +export interface GraphProps { + ariaLabel?: string; + children?: JSX.Element | JSX.Element[]; + series: GraphSeriesXY[]; + timeRange: TimeRange; // NOTE: we should aim to make `time` a property of the axis, not force it for all graphs + timeZone?: TimeZone; // NOTE: we should aim to make `time` a property of the axis, not force it for all graphs + showLines?: boolean; + showPoints?: boolean; + showBars?: boolean; + width: number; + height: number; + isStacked?: boolean; + lineWidth?: number; + onHorizontalRegionSelected?: (from: number, to: number) => void; +} + +/** @deprecated */ +interface GraphState { + pos?: FlotPosition; + contextPos?: FlotPosition; + isTooltipVisible: boolean; + isContextVisible: boolean; + activeItem?: FlotItem; + contextItem?: FlotItem; +} + +/** + * This is a react wrapper for the angular, flot based graph visualization. + * Rather than using this component, you should use the ` with + * timeseries panel configs. + * + * @deprecated + */ +export class Graph extends PureComponent { + static defaultProps = { + showLines: true, + showPoints: false, + showBars: false, + isStacked: false, + lineWidth: 1, + }; + + state: GraphState = { + isTooltipVisible: false, + isContextVisible: false, + }; + + element: HTMLElement | null = null; + $element: JQuery | null = null; + + componentDidUpdate(prevProps: GraphProps, prevState: GraphState) { + if (prevProps !== this.props) { + this.draw(); + } + } + + componentDidMount() { + this.draw(); + if (this.element) { + this.$element = $(this.element); + this.$element.bind('plotselected', this.onPlotSelected); + this.$element.bind('plothover', this.onPlotHover); + this.$element.bind('plotclick', this.onPlotClick); + } + } + + componentWillUnmount() { + if (this.$element) { + this.$element.unbind('plotselected', this.onPlotSelected); + } + } + + onPlotSelected = (event: JQuery.Event, ranges: { xaxis: { from: number; to: number } }) => { + const { onHorizontalRegionSelected } = this.props; + if (onHorizontalRegionSelected) { + onHorizontalRegionSelected(ranges.xaxis.from, ranges.xaxis.to); + } + }; + + onPlotHover = (event: JQuery.Event, pos: FlotPosition, item?: FlotItem) => { + this.setState({ + isTooltipVisible: true, + activeItem: item, + pos, + }); + }; + + onPlotClick = (event: JQuery.Event, contextPos: FlotPosition, item?: FlotItem) => { + this.setState({ + isContextVisible: true, + isTooltipVisible: false, + contextItem: item, + contextPos, + }); + }; + + getYAxes(series: GraphSeriesXY[]) { + if (series.length === 0) { + return [{ show: true, min: -1, max: 1 }]; + } + return uniqBy( + series.map((s) => { + const index = s.yAxis ? s.yAxis.index : 1; + const min = s.yAxis && s.yAxis.min && !isNaN(s.yAxis.min) ? s.yAxis.min : null; + const tickDecimals = + s.yAxis && s.yAxis.tickDecimals && !isNaN(s.yAxis.tickDecimals) ? s.yAxis.tickDecimals : null; + return { + show: true, + index, + position: index === 1 ? 'left' : 'right', + min, + tickDecimals, + }; + }), + (yAxisConfig) => yAxisConfig.index + ); + } + + renderTooltip = () => { + const { children, series, timeZone } = this.props; + const { pos, activeItem, isTooltipVisible } = this.state; + let tooltipElement: React.ReactElement | undefined; + + if (!isTooltipVisible || !pos || series.length === 0) { + return null; + } + + // Find children that indicate tooltip to be rendered + React.Children.forEach(children, (c) => { + // We have already found tooltip + if (tooltipElement) { + return; + } + const childType = c && c.type && (c.type.displayName || c.type.name); + + if (childType === VizTooltip.displayName) { + tooltipElement = c; + } + }); + // If no tooltip provided, skip rendering + if (!tooltipElement) { + return null; + } + const tooltipElementProps = tooltipElement.props; + + const tooltipMode = tooltipElementProps.mode || 'single'; + + // If mode is single series and user is not hovering over item, skip rendering + if (!activeItem && tooltipMode === 'single') { + return null; + } + + // Check if tooltip needs to be rendered with custom tooltip component, otherwise default to GraphTooltip + const tooltipContentRenderer = tooltipElementProps.tooltipComponent || GraphTooltip; + // Indicates column(field) index in y-axis dimension + const seriesIndex = activeItem ? activeItem.series.seriesIndex : 0; + // Indicates row index in active field values + const rowIndex = activeItem ? activeItem.dataIndex : undefined; + + const activeDimensions: ActiveDimensions = { + // Described x-axis active item + // When hovering over an item - let's take it's dataIndex, otherwise undefined + // Tooltip itself needs to figure out correct datapoint display information based on pos passed to it + xAxis: [seriesIndex, rowIndex], + // Describes y-axis active item + yAxis: activeItem ? [activeItem.series.seriesIndex, activeItem.dataIndex] : null, + }; + + const tooltipContentProps: VizTooltipContentProps = { + dimensions: { + // time/value dimension columns are index-aligned - see getGraphSeriesModel + xAxis: createDimension( + 'xAxis', + series.map((s) => s.timeField) + ), + yAxis: createDimension( + 'yAxis', + series.map((s) => s.valueField) + ), + }, + activeDimensions, + pos, + mode: tooltipElementProps.mode || TooltipDisplayMode.Single, + timeZone, + }; + + const tooltipContent = React.createElement(tooltipContentRenderer, { ...tooltipContentProps }); + + return React.cloneElement(tooltipElement, { + content: tooltipContent, + position: { x: pos.pageX, y: pos.pageY }, + offset: { x: 10, y: 10 }, + }); + }; + + renderContextMenu = () => { + const { series } = this.props; + const { contextPos, contextItem, isContextVisible } = this.state; + + if (!isContextVisible || !contextPos || !contextItem || series.length === 0) { + return null; + } + + // Indicates column(field) index in y-axis dimension + const seriesIndex = contextItem ? contextItem.series.seriesIndex : 0; + // Indicates row index in context field values + const rowIndex = contextItem ? contextItem.dataIndex : undefined; + + const contextDimensions: ContextDimensions = { + // Described x-axis context item + xAxis: [seriesIndex, rowIndex], + // Describes y-axis context item + yAxis: contextItem ? [contextItem.series.seriesIndex, contextItem.dataIndex] : null, + }; + + const dimensions: GraphDimensions = { + // time/value dimension columns are index-aligned - see getGraphSeriesModel + xAxis: createDimension( + 'xAxis', + series.map((s) => s.timeField) + ), + yAxis: createDimension( + 'yAxis', + series.map((s) => s.valueField) + ), + }; + + const closeContext = () => this.setState({ isContextVisible: false }); + + const getContextMenuSource = () => { + return { + datapoint: contextItem.datapoint, + dataIndex: contextItem.dataIndex, + series: contextItem.series, + seriesIndex: contextItem.series.seriesIndex, + pageX: contextPos.pageX, + pageY: contextPos.pageY, + }; + }; + + const contextContentProps: GraphContextMenuProps = { + x: contextPos.pageX, + y: contextPos.pageY, + onClose: closeContext, + getContextMenuSource: getContextMenuSource, + timeZone: this.props.timeZone, + dimensions, + contextDimensions, + }; + + return ; + }; + + getBarWidth = () => { + const { series } = this.props; + return Math.min(...series.map((s) => s.timeStep)); + }; + + draw() { + if (this.element === null) { + return; + } + + const { + width, + series, + timeRange, + showLines, + showBars, + showPoints, + isStacked, + lineWidth, + timeZone, + onHorizontalRegionSelected, + } = this.props; + + if (!width) { + return; + } + + const ticks = width / 100; + const min = timeRange.from.valueOf(); + const max = timeRange.to.valueOf(); + const yaxes = this.getYAxes(series); + + const flotOptions = { + legend: { + show: false, + }, + series: { + stack: isStacked, + lines: { + show: showLines, + lineWidth: lineWidth, + zero: false, + }, + points: { + show: showPoints, + fill: 1, + fillColor: false, + radius: 2, + }, + bars: { + show: showBars, + fill: 1, + // Dividig the width by 1.5 to make the bars not touch each other + barWidth: showBars ? this.getBarWidth() / 1.5 : 1, + zero: false, + lineWidth: lineWidth, + }, + shadowSize: 0, + }, + xaxis: { + timezone: timeZone, + show: true, + mode: 'time', + min: min, + max: max, + label: 'Datetime', + ticks: ticks, + timeformat: graphTimeFormat(ticks, min, max), + tickFormatter: graphTickFormatter, + }, + yaxes, + grid: { + minBorderMargin: 0, + markings: [], + backgroundColor: null, + borderWidth: 0, + hoverable: true, + clickable: true, + color: '#a1a1a1', + margin: { left: 0, right: 0 }, + labelMarginX: 0, + mouseActiveRadius: 30, + }, + selection: { + mode: onHorizontalRegionSelected ? 'x' : null, + color: '#666', + }, + crosshair: { + mode: 'x', + }, + }; + + try { + $.plot( + this.element, + series.filter((s) => s.isVisible), + flotOptions + ); + } catch (err) { + console.error('Graph rendering error', err, flotOptions, series); + throw new Error('Error rendering panel'); + } + } + + render() { + const { ariaLabel, height, width, series } = this.props; + const noDataToBeDisplayed = series.length === 0; + const tooltip = this.renderTooltip(); + const context = this.renderContextMenu(); + return ( +
+
(this.element = e)} + style={{ height, width }} + onMouseLeave={() => { + this.setState({ isTooltipVisible: false }); + }} + /> + {noDataToBeDisplayed &&
No data
} + {tooltip} + {context} +
+ ); + } +} + +export default Graph; diff --git a/public/app/angular/components/legacy_graph_panel/GraphContextMenu.tsx b/packages/grafana-ui/src/graveyard/Graph/GraphContextMenu.tsx similarity index 86% rename from public/app/angular/components/legacy_graph_panel/GraphContextMenu.tsx rename to packages/grafana-ui/src/graveyard/Graph/GraphContextMenu.tsx index 8d828e6bdf..5b3da31708 100644 --- a/public/app/angular/components/legacy_graph_panel/GraphContextMenu.tsx +++ b/packages/grafana-ui/src/graveyard/Graph/GraphContextMenu.tsx @@ -9,28 +9,20 @@ import { TimeZone, FormattedValue, GrafanaTheme2, - Dimension, } from '@grafana/data'; -import { - ContextMenu, - ContextMenuProps, - FormattedValueDisplay, - HorizontalGroup, - MenuGroup, - MenuGroupProps, - MenuItem, - SeriesIcon, - useStyles2, -} from '@grafana/ui'; -/** @deprecated */ -export type ContextDimensions = { [key in keyof T]: [number, number | undefined] | null }; +import { ContextMenu, ContextMenuProps } from '../../components/ContextMenu/ContextMenu'; +import { FormattedValueDisplay } from '../../components/FormattedValueDisplay/FormattedValueDisplay'; +import { HorizontalGroup } from '../../components/Layout/Layout'; +import { MenuGroup, MenuGroupProps } from '../../components/Menu/MenuGroup'; +import { MenuItem } from '../../components/Menu/MenuItem'; +import { SeriesIcon } from '../../components/VizLegend/SeriesIcon'; +import { useStyles2 } from '../../themes'; + +import { GraphDimensions } from './GraphTooltip/types'; /** @deprecated */ -export interface GraphDimensions extends Dimensions { - xAxis: Dimension; - yAxis: Dimension; -} +export type ContextDimensions = { [key in keyof T]: [number, number | undefined] | null }; /** @deprecated */ export type GraphContextMenuProps = ContextMenuProps & { diff --git a/packages/grafana-ui/src/graveyard/Graph/GraphSeriesToggler.tsx b/packages/grafana-ui/src/graveyard/Graph/GraphSeriesToggler.tsx new file mode 100644 index 0000000000..fc960695a6 --- /dev/null +++ b/packages/grafana-ui/src/graveyard/Graph/GraphSeriesToggler.tsx @@ -0,0 +1,89 @@ +import { difference, isEqual } from 'lodash'; +import React, { Component } from 'react'; + +import { GraphSeriesXY } from '@grafana/data'; + +/** @deprecated */ +export interface GraphSeriesTogglerAPI { + onSeriesToggle: (label: string, event: React.MouseEvent) => void; + toggledSeries: GraphSeriesXY[]; +} + +/** @deprecated */ +export interface GraphSeriesTogglerProps { + children: (api: GraphSeriesTogglerAPI) => JSX.Element; + series: GraphSeriesXY[]; + onHiddenSeriesChanged?: (hiddenSeries: string[]) => void; +} + +/** @deprecated */ +export interface GraphSeriesTogglerState { + hiddenSeries: string[]; + toggledSeries: GraphSeriesXY[]; +} + +/** @deprecated */ +export class GraphSeriesToggler extends Component { + constructor(props: GraphSeriesTogglerProps) { + super(props); + + this.onSeriesToggle = this.onSeriesToggle.bind(this); + + this.state = { + hiddenSeries: [], + toggledSeries: props.series, + }; + } + + componentDidUpdate(prevProps: Readonly) { + const { series } = this.props; + if (!isEqual(prevProps.series, series)) { + this.setState({ hiddenSeries: [], toggledSeries: series }); + } + } + + onSeriesToggle(label: string, event: React.MouseEvent) { + const { series, onHiddenSeriesChanged } = this.props; + const { hiddenSeries } = this.state; + + if (event.ctrlKey || event.metaKey || event.shiftKey) { + // Toggling series with key makes the series itself to toggle + const newHiddenSeries = + hiddenSeries.indexOf(label) > -1 + ? hiddenSeries.filter((series) => series !== label) + : hiddenSeries.concat([label]); + + const toggledSeries = series.map((series) => ({ + ...series, + isVisible: newHiddenSeries.indexOf(series.label) === -1, + })); + this.setState({ hiddenSeries: newHiddenSeries, toggledSeries }, () => + onHiddenSeriesChanged ? onHiddenSeriesChanged(newHiddenSeries) : undefined + ); + return; + } + + // Toggling series with out key toggles all the series but the clicked one + const allSeriesLabels = series.map((series) => series.label); + const newHiddenSeries = + hiddenSeries.length + 1 === allSeriesLabels.length ? [] : difference(allSeriesLabels, [label]); + const toggledSeries = series.map((series) => ({ + ...series, + isVisible: newHiddenSeries.indexOf(series.label) === -1, + })); + + this.setState({ hiddenSeries: newHiddenSeries, toggledSeries }, () => + onHiddenSeriesChanged ? onHiddenSeriesChanged(newHiddenSeries) : undefined + ); + } + + render() { + const { children } = this.props; + const { toggledSeries } = this.state; + + return children({ + onSeriesToggle: this.onSeriesToggle, + toggledSeries, + }); + } +} diff --git a/packages/grafana-ui/src/graveyard/Graph/GraphTooltip/GraphTooltip.tsx b/packages/grafana-ui/src/graveyard/Graph/GraphTooltip/GraphTooltip.tsx new file mode 100644 index 0000000000..d58c0816df --- /dev/null +++ b/packages/grafana-ui/src/graveyard/Graph/GraphTooltip/GraphTooltip.tsx @@ -0,0 +1,41 @@ +import React from 'react'; + +import { TooltipDisplayMode } from '@grafana/schema'; + +import { VizTooltipContentProps } from '../../../components/VizTooltip'; + +import { MultiModeGraphTooltip } from './MultiModeGraphTooltip'; +import { SingleModeGraphTooltip } from './SingleModeGraphTooltip'; +import { GraphDimensions } from './types'; + +/** @deprecated */ +export const GraphTooltip = ({ + mode = TooltipDisplayMode.Single, + dimensions, + activeDimensions, + pos, + timeZone, +}: VizTooltipContentProps) => { + // When + // [1] no active dimension or + // [2] no xAxis position + // we assume no tooltip should be rendered + if (!activeDimensions || !activeDimensions.xAxis) { + return null; + } + + if (mode === 'single') { + return ; + } else { + return ( + + ); + } +}; + +GraphTooltip.displayName = 'GraphTooltip'; diff --git a/packages/grafana-ui/src/graveyard/Graph/GraphTooltip/MultiModeGraphTooltip.test.tsx b/packages/grafana-ui/src/graveyard/Graph/GraphTooltip/MultiModeGraphTooltip.test.tsx new file mode 100644 index 0000000000..1af501183a --- /dev/null +++ b/packages/grafana-ui/src/graveyard/Graph/GraphTooltip/MultiModeGraphTooltip.test.tsx @@ -0,0 +1,106 @@ +import { render, screen } from '@testing-library/react'; +import React from 'react'; + +import { createDimension, createTheme, FieldType, DisplayProcessor } from '@grafana/data'; + +import { ActiveDimensions } from '../../../components/VizTooltip'; + +import { MultiModeGraphTooltip } from './MultiModeGraphTooltip'; +import { GraphDimensions } from './types'; + +let dimensions: GraphDimensions; + +describe('MultiModeGraphTooltip', () => { + const display: DisplayProcessor = (v) => ({ numeric: Number(v), text: String(v), color: 'red' }); + const theme = createTheme(); + + describe('when shown when hovering over a datapoint', () => { + beforeEach(() => { + dimensions = { + xAxis: createDimension('xAxis', [ + { + config: {}, + values: [0, 100, 200], + name: 'A-series time', + type: FieldType.time, + display, + }, + { + config: {}, + values: [0, 100, 200], + name: 'B-series time', + type: FieldType.time, + display, + }, + ]), + yAxis: createDimension('yAxis', [ + { + config: {}, + values: [10, 20, 10], + name: 'A-series values', + type: FieldType.number, + display, + }, + { + config: {}, + values: [20, 30, 40], + name: 'B-series values', + type: FieldType.number, + display, + }, + ]), + }; + }); + + it('highlights series of the datapoint', () => { + // We are simulating hover over A-series, middle point + const activeDimensions: ActiveDimensions = { + xAxis: [0, 1], // column, row + yAxis: [0, 1], // column, row + }; + render( + + ); + + // We rendered two series rows + const rows = screen.getAllByTestId('SeriesTableRow'); + expect(rows.length).toEqual(2); + + // We expect A-series(1st row) not to be highlighted + expect(rows[0]).toHaveStyle(`font-weight: ${theme.typography.fontWeightMedium}`); + // We expect B-series(2nd row) not to be highlighted + expect(rows[1]).not.toHaveStyle(`font-weight: ${theme.typography.fontWeightMedium}`); + }); + + it("doesn't highlight series when not hovering over datapoint", () => { + // We are simulating hover over graph, but not datapoint + const activeDimensions: ActiveDimensions = { + xAxis: [0, undefined], // no active point in time + yAxis: null, // no active series + }; + + render( + + ); + + // We rendered two series rows + const rows = screen.getAllByTestId('SeriesTableRow'); + expect(rows.length).toEqual(2); + + // We expect A-series(1st row) not to be highlighted + expect(rows[0]).not.toHaveStyle(`font-weight: ${theme.typography.fontWeightMedium}`); + // We expect B-series(2nd row) not to be highlighted + expect(rows[1]).not.toHaveStyle(`font-weight: ${theme.typography.fontWeightMedium}`); + }); + }); +}); diff --git a/packages/grafana-ui/src/graveyard/Graph/GraphTooltip/MultiModeGraphTooltip.tsx b/packages/grafana-ui/src/graveyard/Graph/GraphTooltip/MultiModeGraphTooltip.tsx new file mode 100644 index 0000000000..bfe2856c70 --- /dev/null +++ b/packages/grafana-ui/src/graveyard/Graph/GraphTooltip/MultiModeGraphTooltip.tsx @@ -0,0 +1,49 @@ +import React from 'react'; + +import { getValueFromDimension } from '@grafana/data'; + +import { SeriesTable } from '../../../components/VizTooltip'; +import { FlotPosition } from '../../../components/VizTooltip/VizTooltip'; +import { getMultiSeriesGraphHoverInfo } from '../utils'; + +import { GraphTooltipContentProps } from './types'; + +/** @deprecated */ +type Props = GraphTooltipContentProps & { + // We expect position to figure out correct values when not hovering over a datapoint + pos: FlotPosition; +}; + +/** @deprecated */ +export const MultiModeGraphTooltip = ({ dimensions, activeDimensions, pos, timeZone }: Props) => { + let activeSeriesIndex: number | null = null; + // when no x-axis provided, skip rendering + if (activeDimensions.xAxis === null) { + return null; + } + + if (activeDimensions.yAxis) { + activeSeriesIndex = activeDimensions.yAxis[0]; + } + + // when not hovering over a point, time is undefined, and we use pos.x as time + const time = activeDimensions.xAxis[1] + ? getValueFromDimension(dimensions.xAxis, activeDimensions.xAxis[0], activeDimensions.xAxis[1]) + : pos.x; + + const hoverInfo = getMultiSeriesGraphHoverInfo(dimensions.yAxis.columns, dimensions.xAxis.columns, time, timeZone); + const timestamp = hoverInfo.time; + + const series = hoverInfo.results.map((s, i) => { + return { + color: s.color, + label: s.label, + value: s.value, + isActive: activeSeriesIndex === i, + }; + }); + + return ; +}; + +MultiModeGraphTooltip.displayName = 'MultiModeGraphTooltip'; diff --git a/packages/grafana-ui/src/graveyard/Graph/GraphTooltip/SingleModeGraphTooltip.tsx b/packages/grafana-ui/src/graveyard/Graph/GraphTooltip/SingleModeGraphTooltip.tsx new file mode 100644 index 0000000000..7eb4d61872 --- /dev/null +++ b/packages/grafana-ui/src/graveyard/Graph/GraphTooltip/SingleModeGraphTooltip.tsx @@ -0,0 +1,48 @@ +import React from 'react'; + +import { + getValueFromDimension, + getColumnFromDimension, + formattedValueToString, + getFieldDisplayName, +} from '@grafana/data'; + +import { SeriesTable } from '../../../components/VizTooltip'; + +import { GraphTooltipContentProps } from './types'; + +/** @deprecated */ +export const SingleModeGraphTooltip = ({ dimensions, activeDimensions, timeZone }: GraphTooltipContentProps) => { + // not hovering over a point, skip rendering + if ( + activeDimensions.yAxis === null || + activeDimensions.yAxis[1] === undefined || + activeDimensions.xAxis === null || + activeDimensions.xAxis[1] === undefined + ) { + return null; + } + const time = getValueFromDimension(dimensions.xAxis, activeDimensions.xAxis[0], activeDimensions.xAxis[1]); + const timeField = getColumnFromDimension(dimensions.xAxis, activeDimensions.xAxis[0]); + const processedTime = timeField.display ? formattedValueToString(timeField.display(time)) : time; + + const valueField = getColumnFromDimension(dimensions.yAxis, activeDimensions.yAxis[0]); + const value = getValueFromDimension(dimensions.yAxis, activeDimensions.yAxis[0], activeDimensions.yAxis[1]); + const display = valueField.display!; + const disp = display(value); + + return ( + + ); +}; + +SingleModeGraphTooltip.displayName = 'SingleModeGraphTooltip'; diff --git a/packages/grafana-ui/src/graveyard/Graph/GraphTooltip/types.ts b/packages/grafana-ui/src/graveyard/Graph/GraphTooltip/types.ts new file mode 100644 index 0000000000..d4d68b729c --- /dev/null +++ b/packages/grafana-ui/src/graveyard/Graph/GraphTooltip/types.ts @@ -0,0 +1,16 @@ +import { Dimension, Dimensions, TimeZone } from '@grafana/data'; + +import { ActiveDimensions } from '../../../components/VizTooltip'; + +/** @deprecated */ +export interface GraphDimensions extends Dimensions { + xAxis: Dimension; + yAxis: Dimension; +} + +/** @deprecated */ +export interface GraphTooltipContentProps { + dimensions: GraphDimensions; // Dimension[] + activeDimensions: ActiveDimensions; + timeZone?: TimeZone; +} diff --git a/packages/grafana-ui/src/graveyard/Graph/GraphWithLegend.internal.story.tsx b/packages/grafana-ui/src/graveyard/Graph/GraphWithLegend.internal.story.tsx new file mode 100644 index 0000000000..0830b44f19 --- /dev/null +++ b/packages/grafana-ui/src/graveyard/Graph/GraphWithLegend.internal.story.tsx @@ -0,0 +1,141 @@ +import { Story } from '@storybook/react'; +import React from 'react'; + +import { GraphSeriesXY, FieldType, dateTime, FieldColorModeId } from '@grafana/data'; +import { LegendDisplayMode } from '@grafana/schema'; + +import { GraphWithLegend, GraphWithLegendProps } from './GraphWithLegend'; + +export default { + title: 'Visualizations/Graph/GraphWithLegend', + component: GraphWithLegend, + parameters: { + controls: { + exclude: ['className', 'ariaLabel', 'legendDisplayMode', 'series'], + }, + }, + argTypes: { + displayMode: { control: { type: 'radio' }, options: ['table', 'list', 'hidden'] }, + placement: { control: { type: 'radio' }, options: ['bottom', 'right'] }, + rightAxisSeries: { name: 'Right y-axis series, i.e. A,C' }, + timeZone: { control: { type: 'radio' }, options: ['browser', 'utc'] }, + width: { control: { type: 'range', min: 200, max: 800 } }, + height: { control: { type: 'range', min: 1700, step: 300 } }, + lineWidth: { control: { type: 'range', min: 1, max: 10 } }, + }, +}; + +const series: GraphSeriesXY[] = [ + { + data: [ + [1546372800000, 10], + [1546376400000, 20], + [1546380000000, 10], + ], + color: 'red', + isVisible: true, + label: 'A-series', + seriesIndex: 0, + timeField: { + type: FieldType.time, + name: 'time', + values: [1546372800000, 1546376400000, 1546380000000], + config: {}, + }, + valueField: { + type: FieldType.number, + name: 'a-series', + values: [10, 20, 10], + config: { + color: { + mode: FieldColorModeId.Fixed, + fixedColor: 'red', + }, + }, + }, + timeStep: 3600000, + yAxis: { + index: 1, + }, + }, + { + data: [ + [1546372800000, 20], + [1546376400000, 30], + [1546380000000, 40], + ], + color: 'blue', + isVisible: true, + label: 'B-series', + seriesIndex: 1, + timeField: { + type: FieldType.time, + name: 'time', + values: [1546372800000, 1546376400000, 1546380000000], + config: {}, + }, + valueField: { + type: FieldType.number, + name: 'b-series', + values: [20, 30, 40], + config: { + color: { + mode: FieldColorModeId.Fixed, + fixedColor: 'blue', + }, + }, + }, + timeStep: 3600000, + yAxis: { + index: 1, + }, + }, +]; + +interface StoryProps extends GraphWithLegendProps { + rightAxisSeries: string; + displayMode: string; +} + +export const WithLegend: Story = ({ rightAxisSeries, displayMode, legendDisplayMode, ...args }) => { + const props: Partial = { + series: series.map((s) => { + if ( + rightAxisSeries + .split(',') + .map((s) => s.trim()) + .indexOf(s.label.split('-')[0]) > -1 + ) { + s.yAxis = { index: 2 }; + } else { + s.yAxis = { index: 1 }; + } + return s; + }), + }; + + return ( + + ); +}; +WithLegend.args = { + rightAxisSeries: '', + displayMode: 'list', + onToggleSort: () => {}, + timeRange: { + from: dateTime(1546372800000), + to: dateTime(1546380000000), + raw: { + from: dateTime(1546372800000), + to: dateTime(1546380000000), + }, + }, + timeZone: 'browser', + width: 600, + height: 300, + placement: 'bottom', +}; diff --git a/packages/grafana-ui/src/graveyard/Graph/GraphWithLegend.tsx b/packages/grafana-ui/src/graveyard/Graph/GraphWithLegend.tsx new file mode 100644 index 0000000000..a590d5d1ed --- /dev/null +++ b/packages/grafana-ui/src/graveyard/Graph/GraphWithLegend.tsx @@ -0,0 +1,132 @@ +// Libraries + +import { css } from '@emotion/css'; +import React from 'react'; + +import { GrafanaTheme2, GraphSeriesValue } from '@grafana/data'; +import { LegendDisplayMode, LegendPlacement } from '@grafana/schema'; + +import { CustomScrollbar } from '../../components/CustomScrollbar/CustomScrollbar'; +import { VizLegend } from '../../components/VizLegend/VizLegend'; +import { VizLegendItem } from '../../components/VizLegend/types'; +import { useStyles2 } from '../../themes'; + +import { Graph, GraphProps } from './Graph'; + +export interface GraphWithLegendProps extends GraphProps { + legendDisplayMode: LegendDisplayMode; + legendVisibility: boolean; + placement: LegendPlacement; + hideEmpty?: boolean; + hideZero?: boolean; + sortLegendBy?: string; + sortLegendDesc?: boolean; + onSeriesToggle?: (label: string, event: React.MouseEvent) => void; + onToggleSort: (sortBy: string) => void; +} + +const shouldHideLegendItem = (data: GraphSeriesValue[][], hideEmpty = false, hideZero = false) => { + const isZeroOnlySeries = data.reduce((acc, current) => acc + (current[1] || 0), 0) === 0; + const isNullOnlySeries = !data.reduce((acc, current) => acc && current[1] !== null, true); + + return (hideEmpty && isNullOnlySeries) || (hideZero && isZeroOnlySeries); +}; + +export const GraphWithLegend = (props: GraphWithLegendProps) => { + const { + series, + timeRange, + width, + height, + showBars, + showLines, + showPoints, + sortLegendBy, + sortLegendDesc, + legendDisplayMode, + legendVisibility, + placement, + onSeriesToggle, + onToggleSort, + hideEmpty, + hideZero, + isStacked, + lineWidth, + onHorizontalRegionSelected, + timeZone, + children, + ariaLabel, + } = props; + const { graphContainer, wrapper, legendContainer } = useStyles2(getGraphWithLegendStyles, props.placement); + + const legendItems = series.reduce((acc, s) => { + return shouldHideLegendItem(s.data, hideEmpty, hideZero) + ? acc + : acc.concat([ + { + label: s.label, + color: s.color || '', + disabled: !s.isVisible, + yAxis: s.yAxis.index, + getDisplayValues: () => s.info || [], + }, + ]); + }, []); + + return ( +
+
+ + {children} + +
+ + {legendVisibility && ( +
+ + { + if (onSeriesToggle) { + onSeriesToggle(item.label, event); + } + }} + onToggleSort={onToggleSort} + /> + +
+ )} +
+ ); +}; + +const getGraphWithLegendStyles = (_theme: GrafanaTheme2, placement: LegendPlacement) => ({ + wrapper: css({ + display: 'flex', + flexDirection: placement === 'bottom' ? 'column' : 'row', + }), + graphContainer: css({ + minHeight: '65%', + flexGrow: 1, + }), + legendContainer: css({ + padding: '10px 0', + maxHeight: placement === 'bottom' ? '35%' : 'none', + }), +}); diff --git a/packages/grafana-ui/src/graveyard/Graph/types.ts b/packages/grafana-ui/src/graveyard/Graph/types.ts new file mode 100644 index 0000000000..60cdb44b98 --- /dev/null +++ b/packages/grafana-ui/src/graveyard/Graph/types.ts @@ -0,0 +1,9 @@ +/** @deprecated */ +export interface FlotItem { + datapoint: [number, number]; + dataIndex: number; + series: T; + seriesIndex: number; + pageX: number; + pageY: number; +} diff --git a/packages/grafana-ui/src/graveyard/Graph/utils.test.ts b/packages/grafana-ui/src/graveyard/Graph/utils.test.ts new file mode 100644 index 0000000000..f68ed2e4b3 --- /dev/null +++ b/packages/grafana-ui/src/graveyard/Graph/utils.test.ts @@ -0,0 +1,233 @@ +import { + toDataFrame, + FieldType, + FieldCache, + FieldColorModeId, + Field, + applyFieldOverrides, + createTheme, + DataFrame, +} from '@grafana/data'; + +import { getTheme } from '../../themes'; + +import { getMultiSeriesGraphHoverInfo, findHoverIndexFromData, graphTimeFormat } from './utils'; + +const mockResult = ( + value: string, + datapointIndex: number, + seriesIndex: number, + color?: string, + label?: string, + time?: string +) => ({ + value, + datapointIndex, + seriesIndex, + color, + label, + time, +}); + +function passThroughFieldOverrides(frame: DataFrame) { + return applyFieldOverrides({ + data: [frame], + fieldConfig: { + defaults: {}, + overrides: [], + }, + replaceVariables: (val: string) => val, + timeZone: 'utc', + theme: createTheme(), + }); +} + +// A and B series have the same x-axis range and the datapoints are x-axis aligned +const aSeries = passThroughFieldOverrides( + toDataFrame({ + fields: [ + { name: 'time', type: FieldType.time, values: [10000, 20000, 30000, 80000] }, + { + name: 'value', + type: FieldType.number, + values: [10, 20, 10, 25], + config: { color: { mode: FieldColorModeId.Fixed, fixedColor: 'red' } }, + }, + ], + }) +)[0]; + +const bSeries = passThroughFieldOverrides( + toDataFrame({ + fields: [ + { name: 'time', type: FieldType.time, values: [10000, 20000, 30000, 80000] }, + { + name: 'value', + type: FieldType.number, + values: [30, 60, 30, 40], + config: { color: { mode: FieldColorModeId.Fixed, fixedColor: 'blue' } }, + }, + ], + }) +)[0]; + +// C-series has the same x-axis range as A and B but is missing the middle point +const cSeries = passThroughFieldOverrides( + toDataFrame({ + fields: [ + { name: 'time', type: FieldType.time, values: [10000, 30000, 80000] }, + { + name: 'value', + type: FieldType.number, + values: [30, 30, 30], + config: { color: { mode: FieldColorModeId.Fixed, fixedColor: 'yellow' } }, + }, + ], + }) +)[0]; + +function getFixedThemedColor(field: Field): string { + return getTheme().visualization.getColorByName(field.config.color!.fixedColor!); +} + +describe('Graph utils', () => { + describe('getMultiSeriesGraphHoverInfo', () => { + describe('when series datapoints are x-axis aligned', () => { + it('returns a datapoints that user hovers over', () => { + const aCache = new FieldCache(aSeries); + const aValueField = aCache.getFieldByName('value'); + const aTimeField = aCache.getFieldByName('time'); + const bCache = new FieldCache(bSeries); + const bValueField = bCache.getFieldByName('value'); + const bTimeField = bCache.getFieldByName('time'); + + const result = getMultiSeriesGraphHoverInfo([aValueField!, bValueField!], [aTimeField!, bTimeField!], 0); + expect(result.time).toBe('1970-01-01 00:00:10'); + expect(result.results[0]).toEqual( + mockResult('10', 0, 0, getFixedThemedColor(aValueField!), aValueField!.name, '1970-01-01 00:00:10') + ); + expect(result.results[1]).toEqual( + mockResult('30', 0, 1, getFixedThemedColor(bValueField!), bValueField!.name, '1970-01-01 00:00:10') + ); + }); + + describe('returns the closest datapoints before the hover position', () => { + it('when hovering right before a datapoint', () => { + const aCache = new FieldCache(aSeries); + const aValueField = aCache.getFieldByName('value'); + const aTimeField = aCache.getFieldByName('time'); + const bCache = new FieldCache(bSeries); + const bValueField = bCache.getFieldByName('value'); + const bTimeField = bCache.getFieldByName('time'); + + // hovering right before middle point + const result = getMultiSeriesGraphHoverInfo([aValueField!, bValueField!], [aTimeField!, bTimeField!], 19900); + expect(result.time).toBe('1970-01-01 00:00:10'); + expect(result.results[0]).toEqual( + mockResult('10', 0, 0, getFixedThemedColor(aValueField!), aValueField!.name, '1970-01-01 00:00:10') + ); + expect(result.results[1]).toEqual( + mockResult('30', 0, 1, getFixedThemedColor(bValueField!), bValueField!.name, '1970-01-01 00:00:10') + ); + }); + + it('when hovering right after a datapoint', () => { + const aCache = new FieldCache(aSeries); + const aValueField = aCache.getFieldByName('value'); + const aTimeField = aCache.getFieldByName('time'); + const bCache = new FieldCache(bSeries); + const bValueField = bCache.getFieldByName('value'); + const bTimeField = bCache.getFieldByName('time'); + + // hovering right after middle point + const result = getMultiSeriesGraphHoverInfo([aValueField!, bValueField!], [aTimeField!, bTimeField!], 20100); + expect(result.time).toBe('1970-01-01 00:00:20'); + expect(result.results[0]).toEqual( + mockResult('20', 1, 0, getFixedThemedColor(aValueField!), aValueField!.name, '1970-01-01 00:00:20') + ); + expect(result.results[1]).toEqual( + mockResult('60', 1, 1, getFixedThemedColor(bValueField!), bValueField!.name, '1970-01-01 00:00:20') + ); + }); + }); + }); + + describe('when series x-axes are not aligned', () => { + // aSeries and cSeries are not aligned + // cSeries is missing a middle point + it('hovering over a middle point', () => { + const aCache = new FieldCache(aSeries); + const aValueField = aCache.getFieldByName('value'); + const aTimeField = aCache.getFieldByName('time'); + const cCache = new FieldCache(cSeries); + const cValueField = cCache.getFieldByName('value'); + const cTimeField = cCache.getFieldByName('time'); + + // hovering on a middle point + // aSeries has point at that time, cSeries doesn't + const result = getMultiSeriesGraphHoverInfo([aValueField!, cValueField!], [aTimeField!, cTimeField!], 20000); + + // we expect a time of the hovered point + expect(result.time).toBe('1970-01-01 00:00:20'); + // we expect middle point from aSeries (the one we are hovering over) + expect(result.results[0]).toEqual( + mockResult('20', 1, 0, getFixedThemedColor(aValueField!), aValueField!.name, '1970-01-01 00:00:20') + ); + // we expect closest point before hovered point from cSeries (1st point) + expect(result.results[1]).toEqual( + mockResult('30', 0, 1, getFixedThemedColor(cValueField!), cValueField!.name, '1970-01-01 00:00:10') + ); + }); + + it('hovering right after over the middle point', () => { + const aCache = new FieldCache(aSeries); + const aValueField = aCache.getFieldByName('value'); + const aTimeField = aCache.getFieldByName('time'); + const cCache = new FieldCache(cSeries); + const cValueField = cCache.getFieldByName('value'); + const cTimeField = cCache.getFieldByName('time'); + + // aSeries has point at that time, cSeries doesn't + const result = getMultiSeriesGraphHoverInfo([aValueField!, cValueField!], [aTimeField!, cTimeField!], 20100); + + // we expect the time of the closest point before hover + expect(result.time).toBe('1970-01-01 00:00:20'); + // we expect the closest datapoint before hover from aSeries + expect(result.results[0]).toEqual( + mockResult('20', 1, 0, getFixedThemedColor(aValueField!), aValueField!.name, '1970-01-01 00:00:20') + ); + // we expect the closest datapoint before hover from cSeries (1st point) + expect(result.results[1]).toEqual( + mockResult('30', 0, 1, getFixedThemedColor(cValueField!), cValueField!.name, '1970-01-01 00:00:10') + ); + }); + }); + }); + + describe('findHoverIndexFromData', () => { + it('returns index of the closest datapoint before hover position', () => { + const cache = new FieldCache(aSeries); + const timeField = cache.getFieldByName('time'); + // hovering over 1st datapoint + expect(findHoverIndexFromData(timeField!, 0)).toBe(0); + // hovering over right before 2nd datapoint + expect(findHoverIndexFromData(timeField!, 19900)).toBe(0); + // hovering over 2nd datapoint + expect(findHoverIndexFromData(timeField!, 20000)).toBe(1); + // hovering over right before 3rd datapoint + expect(findHoverIndexFromData(timeField!, 29900)).toBe(1); + // hovering over 3rd datapoint + expect(findHoverIndexFromData(timeField!, 30000)).toBe(2); + }); + }); + + describe('graphTimeFormat', () => { + it('graphTimeFormat', () => { + expect(graphTimeFormat(5, 1, 45 * 5 * 1000)).toBe('HH:mm:ss'); + expect(graphTimeFormat(5, 1, 7200 * 5 * 1000)).toBe('HH:mm'); + expect(graphTimeFormat(5, 1, 80000 * 5 * 1000)).toBe('MM/DD HH:mm'); + expect(graphTimeFormat(5, 1, 2419200 * 5 * 1000)).toBe('MM/DD'); + expect(graphTimeFormat(5, 1, 12419200 * 5 * 1000)).toBe('YYYY-MM'); + }); + }); +}); diff --git a/packages/grafana-ui/src/graveyard/Graph/utils.ts b/packages/grafana-ui/src/graveyard/Graph/utils.ts new file mode 100644 index 0000000000..ebc34ad470 --- /dev/null +++ b/packages/grafana-ui/src/graveyard/Graph/utils.ts @@ -0,0 +1,147 @@ +import { + GraphSeriesValue, + Field, + formattedValueToString, + getFieldDisplayName, + TimeZone, + dateTimeFormat, + systemDateFormats, +} from '@grafana/data'; + +/** + * Returns index of the closest datapoint BEFORE hover position + * + * @param posX + * @param series + * @deprecated + */ +export const findHoverIndexFromData = (xAxisDimension: Field, xPos: number) => { + let lower = 0; + let upper = xAxisDimension.values.length - 1; + let middle; + + while (true) { + if (lower > upper) { + return Math.max(upper, 0); + } + middle = Math.floor((lower + upper) / 2); + const xPosition = xAxisDimension.values[middle]; + + if (xPosition === xPos) { + return middle; + } else if (xPosition && xPosition < xPos) { + lower = middle + 1; + } else { + upper = middle - 1; + } + } +}; + +interface MultiSeriesHoverInfo { + value: string; + time: string; + datapointIndex: number; + seriesIndex: number; + label?: string; + color?: string; +} + +/** + * Returns information about closest datapoints when hovering over a Graph + * + * @param seriesList list of series visible on the Graph + * @param pos mouse cursor position, based on jQuery.flot position + * @deprecated + */ +export const getMultiSeriesGraphHoverInfo = ( + // x and y axis dimensions order is aligned + yAxisDimensions: Field[], + xAxisDimensions: Field[], + /** Well, time basically */ + xAxisPosition: number, + timeZone?: TimeZone +): { + results: MultiSeriesHoverInfo[]; + time?: GraphSeriesValue; +} => { + let i, field, hoverIndex, hoverDistance, pointTime; + + const results: MultiSeriesHoverInfo[] = []; + + let minDistance, minTime; + + for (i = 0; i < yAxisDimensions.length; i++) { + field = yAxisDimensions[i]; + const time = xAxisDimensions[i]; + hoverIndex = findHoverIndexFromData(time, xAxisPosition); + hoverDistance = xAxisPosition - time.values[hoverIndex]; + pointTime = time.values[hoverIndex]; + // Take the closest point before the cursor, or if it does not exist, the closest after + if ( + minDistance === undefined || + (hoverDistance >= 0 && (hoverDistance < minDistance || minDistance < 0)) || + (hoverDistance < 0 && hoverDistance > minDistance) + ) { + minDistance = hoverDistance; + minTime = time.display ? formattedValueToString(time.display(pointTime)) : pointTime; + } + + const disp = field.display!(field.values[hoverIndex]); + + results.push({ + value: formattedValueToString(disp), + datapointIndex: hoverIndex, + seriesIndex: i, + color: disp.color, + label: getFieldDisplayName(field), + time: time.display ? formattedValueToString(time.display(pointTime)) : pointTime, + }); + } + + return { + results, + time: minTime, + }; +}; + +/** @deprecated */ +export const graphTickFormatter = (epoch: number, axis: any) => { + return dateTimeFormat(epoch, { + format: axis?.options?.timeformat, + timeZone: axis?.options?.timezone, + }); +}; + +/** @deprecated */ +export const graphTimeFormat = (ticks: number | null, min: number | null, max: number | null): string => { + if (min && max && ticks) { + const range = max - min; + const secPerTick = range / ticks / 1000; + // Need have 10 millisecond margin on the day range + // As sometimes last 24 hour dashboard evaluates to more than 86400000 + const oneDay = 86400010; + const oneYear = 31536000000; + + if (secPerTick <= 10) { + return systemDateFormats.interval.millisecond; + } + if (secPerTick <= 45) { + return systemDateFormats.interval.second; + } + if (range <= oneDay) { + return systemDateFormats.interval.minute; + } + if (secPerTick <= 80000) { + return systemDateFormats.interval.hour; + } + if (range <= oneYear) { + return systemDateFormats.interval.day; + } + if (secPerTick <= 31536000) { + return systemDateFormats.interval.month; + } + return systemDateFormats.interval.year; + } + + return systemDateFormats.interval.minute; +}; diff --git a/packages/grafana-ui/src/graveyard/GraphNG/GraphNG.tsx b/packages/grafana-ui/src/graveyard/GraphNG/GraphNG.tsx new file mode 100644 index 0000000000..52dfadb27a --- /dev/null +++ b/packages/grafana-ui/src/graveyard/GraphNG/GraphNG.tsx @@ -0,0 +1,275 @@ +import React, { Component } from 'react'; +import { Subscription } from 'rxjs'; +import { throttleTime } from 'rxjs/operators'; +import uPlot, { AlignedData } from 'uplot'; + +import { + DataFrame, + DataHoverClearEvent, + DataHoverEvent, + Field, + FieldMatcherID, + fieldMatchers, + FieldType, + LegacyGraphHoverEvent, + TimeRange, + TimeZone, +} from '@grafana/data'; +import { VizLegendOptions } from '@grafana/schema'; + +import { PanelContext, PanelContextRoot } from '../../components/PanelChrome/PanelContext'; +import { VizLayout } from '../../components/VizLayout/VizLayout'; +import { UPlotChart } from '../../components/uPlot/Plot'; +import { AxisProps } from '../../components/uPlot/config/UPlotAxisBuilder'; +import { Renderers, UPlotConfigBuilder } from '../../components/uPlot/config/UPlotConfigBuilder'; +import { ScaleProps } from '../../components/uPlot/config/UPlotScaleBuilder'; +import { findMidPointYPosition, pluginLog } from '../../components/uPlot/utils'; +import { Themeable2 } from '../../types'; + +import { GraphNGLegendEvent, XYFieldMatchers } from './types'; +import { preparePlotFrame as defaultPreparePlotFrame } from './utils'; + +/** + * @deprecated + * @internal -- not a public API + */ +export type PropDiffFn = (prev: T, next: T) => boolean; + +/** @deprecated */ +export interface GraphNGProps extends Themeable2 { + frames: DataFrame[]; + structureRev?: number; // a number that will change when the frames[] structure changes + width: number; + height: number; + timeRange: TimeRange; + timeZone: TimeZone[] | TimeZone; + legend: VizLegendOptions; + fields?: XYFieldMatchers; // default will assume timeseries data + renderers?: Renderers; + tweakScale?: (opts: ScaleProps, forField: Field) => ScaleProps; + tweakAxis?: (opts: AxisProps, forField: Field) => AxisProps; + onLegendClick?: (event: GraphNGLegendEvent) => void; + children?: (builder: UPlotConfigBuilder, alignedFrame: DataFrame) => React.ReactNode; + prepConfig: (alignedFrame: DataFrame, allFrames: DataFrame[], getTimeRange: () => TimeRange) => UPlotConfigBuilder; + propsToDiff?: Array; + preparePlotFrame?: (frames: DataFrame[], dimFields: XYFieldMatchers) => DataFrame | null; + renderLegend: (config: UPlotConfigBuilder) => React.ReactElement | null; + + /** + * needed for propsToDiff to re-init the plot & config + * this is a generic approach to plot re-init, without having to specify which panel-level options + * should cause invalidation. we can drop this in favor of something like panelOptionsRev that gets passed in + * similar to structureRev. then we can drop propsToDiff entirely. + */ + options?: Record; +} + +function sameProps(prevProps: any, nextProps: any, propsToDiff: Array = []) { + for (const propName of propsToDiff) { + if (typeof propName === 'function') { + if (!propName(prevProps, nextProps)) { + return false; + } + } else if (nextProps[propName] !== prevProps[propName]) { + return false; + } + } + + return true; +} + +/** + * @internal -- not a public API + * @deprecated + */ +export interface GraphNGState { + alignedFrame: DataFrame; + alignedData?: AlignedData; + config?: UPlotConfigBuilder; +} + +/** + * "Time as X" core component, expects ascending x + * @deprecated + */ +export class GraphNG extends Component { + static contextType = PanelContextRoot; + panelContext: PanelContext = {} as PanelContext; + private plotInstance: React.RefObject; + + private subscription = new Subscription(); + + constructor(props: GraphNGProps) { + super(props); + let state = this.prepState(props); + state.alignedData = state.config!.prepData!([state.alignedFrame]) as AlignedData; + this.state = state; + this.plotInstance = React.createRef(); + } + + getTimeRange = () => this.props.timeRange; + + prepState(props: GraphNGProps, withConfig = true) { + let state: GraphNGState = null as any; + + const { frames, fields, preparePlotFrame } = props; + + const preparePlotFrameFn = preparePlotFrame || defaultPreparePlotFrame; + + const alignedFrame = preparePlotFrameFn( + frames, + fields || { + x: fieldMatchers.get(FieldMatcherID.firstTimeField).get({}), + y: fieldMatchers.get(FieldMatcherID.byTypes).get(new Set([FieldType.number, FieldType.enum])), + }, + props.timeRange + ); + pluginLog('GraphNG', false, 'data aligned', alignedFrame); + + if (alignedFrame) { + let config = this.state?.config; + + if (withConfig) { + config = props.prepConfig(alignedFrame, this.props.frames, this.getTimeRange); + pluginLog('GraphNG', false, 'config prepared', config); + } + + state = { + alignedFrame, + config, + }; + + pluginLog('GraphNG', false, 'data prepared', state.alignedData); + } + + return state; + } + + handleCursorUpdate(evt: DataHoverEvent | LegacyGraphHoverEvent) { + const time = evt.payload?.point?.time; + const u = this.plotInstance.current; + if (u && time) { + // Try finding left position on time axis + const left = u.valToPos(time, 'x'); + let top; + if (left) { + // find midpoint between points at current idx + top = findMidPointYPosition(u, u.posToIdx(left)); + } + + if (!top || !left) { + return; + } + + u.setCursor({ + left, + top, + }); + } + } + + componentDidMount() { + this.panelContext = this.context as PanelContext; + const { eventBus } = this.panelContext; + + this.subscription.add( + eventBus + .getStream(DataHoverEvent) + .pipe(throttleTime(50)) + .subscribe({ + next: (evt) => { + if (eventBus === evt.origin) { + return; + } + this.handleCursorUpdate(evt); + }, + }) + ); + + // Legacy events (from flot graph) + this.subscription.add( + eventBus + .getStream(LegacyGraphHoverEvent) + .pipe(throttleTime(50)) + .subscribe({ + next: (evt) => this.handleCursorUpdate(evt), + }) + ); + + this.subscription.add( + eventBus + .getStream(DataHoverClearEvent) + .pipe(throttleTime(50)) + .subscribe({ + next: () => { + const u = this.plotInstance?.current; + + // @ts-ignore + if (u && !u.cursor._lock) { + u.setCursor({ + left: -10, + top: -10, + }); + } + }, + }) + ); + } + + componentDidUpdate(prevProps: GraphNGProps) { + const { frames, structureRev, timeZone, propsToDiff } = this.props; + + const propsChanged = !sameProps(prevProps, this.props, propsToDiff); + + if (frames !== prevProps.frames || propsChanged || timeZone !== prevProps.timeZone) { + let newState = this.prepState(this.props, false); + + if (newState) { + const shouldReconfig = + this.state.config === undefined || + timeZone !== prevProps.timeZone || + structureRev !== prevProps.structureRev || + !structureRev || + propsChanged; + + if (shouldReconfig) { + newState.config = this.props.prepConfig(newState.alignedFrame, this.props.frames, this.getTimeRange); + pluginLog('GraphNG', false, 'config recreated', newState.config); + } + + newState.alignedData = newState.config!.prepData!([newState.alignedFrame]) as AlignedData; + + this.setState(newState); + } + } + } + + componentWillUnmount() { + this.subscription.unsubscribe(); + } + + render() { + const { width, height, children, renderLegend } = this.props; + const { config, alignedFrame, alignedData } = this.state; + + if (!config) { + return null; + } + + return ( + + {(vizWidth: number, vizHeight: number) => ( + ((this.plotInstance as React.MutableRefObject).current = u)} + > + {children ? children(config, alignedFrame) : null} + + )} + + ); + } +} diff --git a/packages/grafana-ui/src/graveyard/GraphNG/__snapshots__/utils.test.ts.snap b/packages/grafana-ui/src/graveyard/GraphNG/__snapshots__/utils.test.ts.snap new file mode 100644 index 0000000000..3b9265254b --- /dev/null +++ b/packages/grafana-ui/src/graveyard/GraphNG/__snapshots__/utils.test.ts.snap @@ -0,0 +1,245 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`GraphNG utils preparePlotConfigBuilder 1`] = ` +{ + "axes": [ + { + "filter": undefined, + "font": "12px "Inter", "Helvetica", "Arial", sans-serif", + "gap": 5, + "grid": { + "show": true, + "stroke": "rgba(240, 250, 255, 0.09)", + "width": 1, + }, + "incrs": undefined, + "labelGap": 0, + "rotate": undefined, + "scale": "x", + "show": true, + "side": 2, + "size": [Function], + "space": [Function], + "splits": undefined, + "stroke": "rgb(204, 204, 220)", + "ticks": { + "show": true, + "size": 4, + "stroke": "rgba(240, 250, 255, 0.09)", + "width": 1, + }, + "timeZone": "utc", + "values": [Function], + }, + { + "filter": undefined, + "font": "12px "Inter", "Helvetica", "Arial", sans-serif", + "gap": 5, + "grid": { + "show": true, + "stroke": "rgba(240, 250, 255, 0.09)", + "width": 1, + }, + "incrs": undefined, + "labelGap": 0, + "rotate": undefined, + "scale": "__fixed/na-na/na-na/auto/linear/na/number", + "show": true, + "side": 3, + "size": [Function], + "space": [Function], + "splits": undefined, + "stroke": "rgb(204, 204, 220)", + "ticks": { + "show": false, + "size": 4, + "stroke": "rgb(204, 204, 220)", + "width": 1, + }, + "timeZone": undefined, + "values": [Function], + }, + ], + "cursor": { + "dataIdx": [Function], + "drag": { + "setScale": false, + }, + "focus": { + "prox": 30, + }, + "points": { + "fill": [Function], + "size": [Function], + "stroke": [Function], + "width": [Function], + }, + "sync": { + "filters": { + "pub": [Function], + }, + "key": "__global_", + "scales": [ + "x", + "__fixed/na-na/na-na/auto/linear/na/number", + ], + }, + }, + "focus": { + "alpha": 1, + }, + "hooks": {}, + "legend": { + "show": false, + }, + "mode": 1, + "ms": 1, + "padding": [ + [Function], + [Function], + [Function], + [Function], + ], + "scales": { + "__fixed/na-na/na-na/auto/linear/na/number": { + "asinh": undefined, + "auto": true, + "dir": 1, + "distr": 1, + "log": undefined, + "ori": 1, + "range": [Function], + "time": undefined, + }, + "x": { + "auto": false, + "dir": 1, + "ori": 0, + "range": [Function], + "time": true, + }, + }, + "select": undefined, + "series": [ + { + "value": [Function], + }, + { + "dash": [ + 1, + 2, + ], + "facets": undefined, + "fill": [Function], + "paths": [Function], + "points": { + "fill": "#ff0000", + "filter": [Function], + "show": true, + "size": undefined, + "stroke": "#ff0000", + }, + "pxAlign": undefined, + "scale": "__fixed/na-na/na-na/auto/linear/na/number", + "show": true, + "spanGaps": false, + "stroke": "#ff0000", + "value": [Function], + "width": 2, + }, + { + "dash": [ + 1, + 2, + ], + "facets": undefined, + "fill": [Function], + "paths": [Function], + "points": { + "fill": [Function], + "filter": [Function], + "show": true, + "size": undefined, + "stroke": [Function], + }, + "pxAlign": undefined, + "scale": "__fixed/na-na/na-na/auto/linear/na/number", + "show": true, + "spanGaps": false, + "stroke": [Function], + "value": [Function], + "width": 2, + }, + { + "dash": [ + 1, + 2, + ], + "facets": undefined, + "fill": [Function], + "paths": [Function], + "points": { + "fill": "#ff0000", + "filter": [Function], + "show": true, + "size": undefined, + "stroke": "#ff0000", + }, + "pxAlign": undefined, + "scale": "__fixed/na-na/na-na/auto/linear/na/number", + "show": true, + "spanGaps": false, + "stroke": "#ff0000", + "value": [Function], + "width": 2, + }, + { + "dash": [ + 1, + 2, + ], + "facets": undefined, + "fill": [Function], + "paths": [Function], + "points": { + "fill": [Function], + "filter": [Function], + "show": true, + "size": undefined, + "stroke": [Function], + }, + "pxAlign": undefined, + "scale": "__fixed/na-na/na-na/auto/linear/na/number", + "show": true, + "spanGaps": false, + "stroke": [Function], + "value": [Function], + "width": 2, + }, + { + "dash": [ + 1, + 2, + ], + "facets": undefined, + "fill": [Function], + "paths": [Function], + "points": { + "fill": [Function], + "filter": [Function], + "show": true, + "size": undefined, + "stroke": [Function], + }, + "pxAlign": undefined, + "scale": "__fixed/na-na/na-na/auto/linear/na/number", + "show": true, + "spanGaps": false, + "stroke": [Function], + "value": [Function], + "width": 2, + }, + ], + "tzDate": [Function], +} +`; diff --git a/packages/grafana-ui/src/graveyard/GraphNG/hooks.ts b/packages/grafana-ui/src/graveyard/GraphNG/hooks.ts new file mode 100644 index 0000000000..e4f1d46550 --- /dev/null +++ b/packages/grafana-ui/src/graveyard/GraphNG/hooks.ts @@ -0,0 +1,41 @@ +import React, { useCallback, useContext } from 'react'; + +import { DataFrame, DataFrameFieldIndex, Field } from '@grafana/data'; + +import { XYFieldMatchers } from './types'; + +/** @deprecated */ +interface GraphNGContextType { + mapSeriesIndexToDataFrameFieldIndex: (index: number) => DataFrameFieldIndex; + dimFields: XYFieldMatchers; + data: DataFrame; +} + +/** @deprecated */ +export const GraphNGContext = React.createContext({} as GraphNGContextType); + +/** @deprecated */ +export const useGraphNGContext = () => { + const { data, dimFields, mapSeriesIndexToDataFrameFieldIndex } = useContext(GraphNGContext); + + const getXAxisField = useCallback(() => { + const xFieldMatcher = dimFields.x; + let xField: Field | null = null; + + for (let j = 0; j < data.fields.length; j++) { + if (xFieldMatcher(data.fields[j], data, [data])) { + xField = data.fields[j]; + break; + } + } + + return xField; + }, [data, dimFields]); + + return { + dimFields, + mapSeriesIndexToDataFrameFieldIndex, + getXAxisField, + alignedData: data, + }; +}; diff --git a/packages/grafana-ui/src/graveyard/GraphNG/utils.test.ts b/packages/grafana-ui/src/graveyard/GraphNG/utils.test.ts new file mode 100644 index 0000000000..9675cd7ca5 --- /dev/null +++ b/packages/grafana-ui/src/graveyard/GraphNG/utils.test.ts @@ -0,0 +1,522 @@ +import { + createTheme, + DashboardCursorSync, + DataFrame, + DefaultTimeZone, + EventBusSrv, + FieldColorModeId, + FieldConfig, + FieldMatcherID, + fieldMatchers, + FieldType, + getDefaultTimeRange, + MutableDataFrame, +} from '@grafana/data'; +import { + BarAlignment, + GraphDrawStyle, + GraphFieldConfig, + GraphGradientMode, + LineInterpolation, + VisibilityMode, + StackingMode, +} from '@grafana/schema'; + +import { preparePlotConfigBuilder } from '../TimeSeries/utils'; + +import { preparePlotFrame } from './utils'; + +function mockDataFrame() { + const df1 = new MutableDataFrame({ + refId: 'A', + fields: [{ name: 'ts', type: FieldType.time, values: [1, 2, 3] }], + }); + const df2 = new MutableDataFrame({ + refId: 'B', + fields: [{ name: 'ts', type: FieldType.time, values: [1, 2, 4] }], + }); + + const f1Config: FieldConfig = { + displayName: 'Metric 1', + color: { + mode: FieldColorModeId.Fixed, + }, + decimals: 2, + custom: { + drawStyle: GraphDrawStyle.Line, + gradientMode: GraphGradientMode.Opacity, + lineColor: '#ff0000', + lineWidth: 2, + lineInterpolation: LineInterpolation.Linear, + lineStyle: { + fill: 'dash', + dash: [1, 2], + }, + spanNulls: false, + fillColor: '#ff0000', + fillOpacity: 0.1, + showPoints: VisibilityMode.Always, + stacking: { + group: 'A', + mode: StackingMode.Normal, + }, + }, + }; + + const f2Config: FieldConfig = { + displayName: 'Metric 2', + color: { + mode: FieldColorModeId.Fixed, + }, + decimals: 2, + custom: { + drawStyle: GraphDrawStyle.Bars, + gradientMode: GraphGradientMode.Hue, + lineColor: '#ff0000', + lineWidth: 2, + lineInterpolation: LineInterpolation.Linear, + lineStyle: { + fill: 'dash', + dash: [1, 2], + }, + barAlignment: BarAlignment.Before, + fillColor: '#ff0000', + fillOpacity: 0.1, + showPoints: VisibilityMode.Always, + stacking: { + group: 'A', + mode: StackingMode.Normal, + }, + }, + }; + + const f3Config: FieldConfig = { + displayName: 'Metric 3', + decimals: 2, + color: { + mode: FieldColorModeId.Fixed, + }, + custom: { + drawStyle: GraphDrawStyle.Line, + gradientMode: GraphGradientMode.Opacity, + lineColor: '#ff0000', + lineWidth: 2, + lineInterpolation: LineInterpolation.Linear, + lineStyle: { + fill: 'dash', + dash: [1, 2], + }, + spanNulls: false, + fillColor: '#ff0000', + fillOpacity: 0.1, + showPoints: VisibilityMode.Always, + stacking: { + group: 'B', + mode: StackingMode.Normal, + }, + }, + }; + const f4Config: FieldConfig = { + displayName: 'Metric 4', + decimals: 2, + color: { + mode: FieldColorModeId.Fixed, + }, + custom: { + drawStyle: GraphDrawStyle.Bars, + gradientMode: GraphGradientMode.Hue, + lineColor: '#ff0000', + lineWidth: 2, + lineInterpolation: LineInterpolation.Linear, + lineStyle: { + fill: 'dash', + dash: [1, 2], + }, + barAlignment: BarAlignment.Before, + fillColor: '#ff0000', + fillOpacity: 0.1, + showPoints: VisibilityMode.Always, + stacking: { + group: 'B', + mode: StackingMode.Normal, + }, + }, + }; + const f5Config: FieldConfig = { + displayName: 'Metric 4', + decimals: 2, + color: { + mode: FieldColorModeId.Fixed, + }, + custom: { + drawStyle: GraphDrawStyle.Bars, + gradientMode: GraphGradientMode.Hue, + lineColor: '#ff0000', + lineWidth: 2, + lineInterpolation: LineInterpolation.Linear, + lineStyle: { + fill: 'dash', + dash: [1, 2], + }, + barAlignment: BarAlignment.Before, + fillColor: '#ff0000', + fillOpacity: 0.1, + showPoints: VisibilityMode.Always, + stacking: { + group: 'B', + mode: StackingMode.None, + }, + }, + }; + + df1.addField({ + name: 'metric1', + type: FieldType.number, + config: f1Config, + }); + + df2.addField({ + name: 'metric2', + type: FieldType.number, + config: f2Config, + }); + df2.addField({ + name: 'metric3', + type: FieldType.number, + config: f3Config, + }); + df2.addField({ + name: 'metric4', + type: FieldType.number, + config: f4Config, + }); + df2.addField({ + name: 'metric5', + type: FieldType.number, + config: f5Config, + }); + + return preparePlotFrame([df1, df2], { + x: fieldMatchers.get(FieldMatcherID.firstTimeField).get({}), + y: fieldMatchers.get(FieldMatcherID.numeric).get({}), + }); +} + +jest.mock('@grafana/data', () => ({ + ...jest.requireActual('@grafana/data'), + DefaultTimeZone: 'utc', +})); + +describe('GraphNG utils', () => { + test('preparePlotConfigBuilder', () => { + const frame = mockDataFrame(); + const result = preparePlotConfigBuilder({ + frame: frame!, + theme: createTheme(), + timeZones: [DefaultTimeZone], + getTimeRange: getDefaultTimeRange, + eventBus: new EventBusSrv(), + sync: () => DashboardCursorSync.Tooltip, + allFrames: [frame!], + }).getConfig(); + expect(result).toMatchSnapshot(); + }); + + test('preparePlotFrame appends min bar spaced nulls when > 1 bar series', () => { + const df1: DataFrame = { + name: 'A', + length: 5, + fields: [ + { + name: 'time', + type: FieldType.time, + config: {}, + values: [1, 2, 4, 6, 100], // should find smallest delta === 1 from here + }, + { + name: 'value', + type: FieldType.number, + config: { + custom: { + drawStyle: GraphDrawStyle.Bars, + }, + }, + values: [1, 1, 1, 1, 1], + }, + ], + }; + + const df2: DataFrame = { + name: 'B', + length: 5, + fields: [ + { + name: 'time', + type: FieldType.time, + config: {}, + values: [30, 40, 50, 90, 100], // should be appended with two smallest-delta increments + }, + { + name: 'value', + type: FieldType.number, + config: { + custom: { + drawStyle: GraphDrawStyle.Bars, + }, + }, + values: [2, 2, 2, 2, 2], // bar series should be appended with nulls + }, + { + name: 'value', + type: FieldType.number, + config: { + custom: { + drawStyle: GraphDrawStyle.Line, + }, + }, + values: [3, 3, 3, 3, 3], // line series should be appended with undefineds + }, + ], + }; + + const df3: DataFrame = { + name: 'C', + length: 2, + fields: [ + { + name: 'time', + type: FieldType.time, + config: {}, + values: [1, 1.1], // should not trip up on smaller deltas of non-bars + }, + { + name: 'value', + type: FieldType.number, + config: { + custom: { + drawStyle: GraphDrawStyle.Line, + }, + }, + values: [4, 4], + }, + { + name: 'value', + type: FieldType.number, + config: { + custom: { + drawStyle: GraphDrawStyle.Bars, + hideFrom: { + viz: true, // should ignore hidden bar series + }, + }, + }, + values: [4, 4], + }, + ], + }; + + let aligndFrame = preparePlotFrame([df1, df2, df3], { + x: fieldMatchers.get(FieldMatcherID.firstTimeField).get({}), + y: fieldMatchers.get(FieldMatcherID.numeric).get({}), + }); + + expect(aligndFrame).toMatchInlineSnapshot(` + { + "fields": [ + { + "config": {}, + "name": "time", + "state": { + "nullThresholdApplied": true, + "origin": { + "fieldIndex": 0, + "frameIndex": 0, + }, + }, + "type": "time", + "values": [ + 1, + 1.1, + 2, + 4, + 6, + 30, + 40, + 50, + 90, + 100, + 101, + 102, + ], + }, + { + "config": { + "custom": { + "drawStyle": "bars", + "spanNulls": -1, + }, + }, + "labels": { + "name": "A", + }, + "name": "value", + "state": { + "origin": { + "fieldIndex": 1, + "frameIndex": 0, + }, + }, + "type": "number", + "values": [ + 1, + undefined, + 1, + 1, + 1, + undefined, + undefined, + undefined, + undefined, + 1, + null, + null, + ], + }, + { + "config": { + "custom": { + "drawStyle": "bars", + "spanNulls": -1, + }, + }, + "labels": { + "name": "B", + }, + "name": "value", + "state": { + "origin": { + "fieldIndex": 1, + "frameIndex": 1, + }, + }, + "type": "number", + "values": [ + undefined, + undefined, + undefined, + undefined, + undefined, + 2, + 2, + 2, + 2, + 2, + null, + null, + ], + }, + { + "config": { + "custom": { + "drawStyle": "line", + }, + }, + "labels": { + "name": "B", + }, + "name": "value", + "state": { + "origin": { + "fieldIndex": 2, + "frameIndex": 1, + }, + }, + "type": "number", + "values": [ + undefined, + undefined, + undefined, + undefined, + undefined, + 3, + 3, + 3, + 3, + 3, + undefined, + undefined, + ], + }, + { + "config": { + "custom": { + "drawStyle": "line", + }, + }, + "labels": { + "name": "C", + }, + "name": "value", + "state": { + "origin": { + "fieldIndex": 1, + "frameIndex": 2, + }, + }, + "type": "number", + "values": [ + 4, + 4, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + ], + }, + { + "config": { + "custom": { + "drawStyle": "bars", + "hideFrom": { + "viz": true, + }, + }, + }, + "labels": { + "name": "C", + }, + "name": "value", + "state": { + "origin": { + "fieldIndex": 2, + "frameIndex": 2, + }, + }, + "type": "number", + "values": [ + 4, + 4, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + ], + }, + ], + "length": 12, + } + `); + }); +}); diff --git a/packages/grafana-ui/src/graveyard/README.md b/packages/grafana-ui/src/graveyard/README.md index 627570b3aa..2715ecec38 100644 --- a/packages/grafana-ui/src/graveyard/README.md +++ b/packages/grafana-ui/src/graveyard/README.md @@ -1,4 +1 @@ Items in this folder are all deprecated and will be removed in the future - -NOTE: GraphNG is include, but not exported. It contains some complex function that are -used in the uPlot helper bundles, but also duplicated in grafana core diff --git a/packages/grafana-ui/src/graveyard/TimeSeries/TimeSeries.tsx b/packages/grafana-ui/src/graveyard/TimeSeries/TimeSeries.tsx new file mode 100644 index 0000000000..9a748236d7 --- /dev/null +++ b/packages/grafana-ui/src/graveyard/TimeSeries/TimeSeries.tsx @@ -0,0 +1,63 @@ +import React, { Component } from 'react'; + +import { DataFrame, TimeRange } from '@grafana/data'; + +import { PanelContextRoot } from '../../components/PanelChrome/PanelContext'; +import { hasVisibleLegendSeries, PlotLegend } from '../../components/uPlot/PlotLegend'; +import { UPlotConfigBuilder } from '../../components/uPlot/config/UPlotConfigBuilder'; +import { withTheme2 } from '../../themes/ThemeContext'; +import { GraphNG, GraphNGProps, PropDiffFn } from '../GraphNG/GraphNG'; + +import { preparePlotConfigBuilder } from './utils'; + +const propsToDiff: Array = ['legend', 'options', 'theme']; + +type TimeSeriesProps = Omit; + +export class UnthemedTimeSeries extends Component { + static contextType = PanelContextRoot; + declare context: React.ContextType; + + prepConfig = (alignedFrame: DataFrame, allFrames: DataFrame[], getTimeRange: () => TimeRange) => { + const { eventBus, eventsScope, sync } = this.context; + const { theme, timeZone, renderers, tweakAxis, tweakScale } = this.props; + + return preparePlotConfigBuilder({ + frame: alignedFrame, + theme, + timeZones: Array.isArray(timeZone) ? timeZone : [timeZone], + getTimeRange, + eventBus, + sync, + allFrames, + renderers, + tweakScale, + tweakAxis, + eventsScope, + }); + }; + + renderLegend = (config: UPlotConfigBuilder) => { + const { legend, frames } = this.props; + + if (!config || (legend && !legend.showLegend) || !hasVisibleLegendSeries(config, frames)) { + return null; + } + + return ; + }; + + render() { + return ( + + ); + } +} + +export const TimeSeries = withTheme2(UnthemedTimeSeries); +TimeSeries.displayName = 'TimeSeries'; diff --git a/packages/grafana-ui/src/graveyard/TimeSeries/utils.test.ts b/packages/grafana-ui/src/graveyard/TimeSeries/utils.test.ts new file mode 100644 index 0000000000..583358c7c4 --- /dev/null +++ b/packages/grafana-ui/src/graveyard/TimeSeries/utils.test.ts @@ -0,0 +1,274 @@ +import { EventBus, FieldType } from '@grafana/data'; +import { getTheme } from '@grafana/ui'; + +import { preparePlotConfigBuilder } from './utils'; + +describe('when fill below to option is used', () => { + let eventBus: EventBus; + // eslint-disable-next-line + let renderers: any[]; + // eslint-disable-next-line + let tests: any; + + beforeEach(() => { + eventBus = { + publish: jest.fn(), + getStream: jest.fn(), + subscribe: jest.fn(), + removeAllListeners: jest.fn(), + newScopedBus: jest.fn(), + }; + renderers = []; + + tests = [ + { + alignedFrame: { + fields: [ + { + config: {}, + values: [1667406900000, 1667407170000, 1667407185000], + name: 'Time', + state: { multipleFrames: true, displayName: 'Time', origin: { fieldIndex: 0, frameIndex: 0 } }, + type: FieldType.time, + }, + { + config: { displayNameFromDS: 'Test1', custom: { fillBelowTo: 'Test2' }, min: 0, max: 100 }, + values: [1, 2, 3], + name: 'Value', + state: { multipleFrames: true, displayName: 'Test1', origin: { fieldIndex: 1, frameIndex: 0 } }, + type: FieldType.number, + }, + { + config: { displayNameFromDS: 'Test2', min: 0, max: 100 }, + values: [4, 5, 6], + name: 'Value', + state: { multipleFrames: true, displayName: 'Test2', origin: { fieldIndex: 1, frameIndex: 1 } }, + type: FieldType.number, + }, + ], + length: 3, + }, + allFrames: [ + { + name: 'Test1', + refId: 'A', + fields: [ + { + config: {}, + values: [1667406900000, 1667407170000, 1667407185000], + name: 'Time', + state: { multipleFrames: true, displayName: 'Time', origin: { fieldIndex: 0, frameIndex: 0 } }, + type: FieldType.time, + }, + { + config: { displayNameFromDS: 'Test1', custom: { fillBelowTo: 'Test2' }, min: 0, max: 100 }, + values: [1, 2, 3], + name: 'Value', + state: { multipleFrames: true, displayName: 'Test1', origin: { fieldIndex: 1, frameIndex: 0 } }, + type: FieldType.number, + }, + ], + length: 2, + }, + { + name: 'Test2', + refId: 'B', + fields: [ + { + config: {}, + values: [1667406900000, 1667407170000, 1667407185000], + name: 'Time', + state: { multipleFrames: true, displayName: 'Time', origin: { fieldIndex: 0, frameIndex: 1 } }, + type: FieldType.time, + }, + { + config: { displayNameFromDS: 'Test2', min: 0, max: 100 }, + values: [1, 2, 3], + name: 'Value', + state: { multipleFrames: true, displayName: 'Test2', origin: { fieldIndex: 1, frameIndex: 1 } }, + type: FieldType.number, + }, + ], + length: 2, + }, + ], + expectedResult: 1, + }, + { + alignedFrame: { + fields: [ + { + config: {}, + values: [1667406900000, 1667407170000, 1667407185000], + name: 'time', + state: { multipleFrames: true, displayName: 'time', origin: { fieldIndex: 0, frameIndex: 0 } }, + type: FieldType.time, + }, + { + config: { custom: { fillBelowTo: 'below_value1' } }, + values: [1, 2, 3], + name: 'value1', + state: { multipleFrames: true, displayName: 'value1', origin: { fieldIndex: 1, frameIndex: 0 } }, + type: FieldType.number, + }, + { + config: { custom: { fillBelowTo: 'below_value2' } }, + values: [4, 5, 6], + name: 'value2', + state: { multipleFrames: true, displayName: 'value2', origin: { fieldIndex: 2, frameIndex: 0 } }, + type: FieldType.number, + }, + { + config: {}, + values: [4, 5, 6], + name: 'below_value1', + state: { multipleFrames: true, displayName: 'below_value1', origin: { fieldIndex: 1, frameIndex: 1 } }, + type: FieldType.number, + }, + { + config: {}, + values: [4, 5, 6], + name: 'below_value2', + state: { multipleFrames: true, displayName: 'below_value2', origin: { fieldIndex: 2, frameIndex: 1 } }, + type: FieldType.number, + }, + ], + length: 5, + }, + allFrames: [ + { + refId: 'A', + fields: [ + { + config: {}, + values: [1667406900000, 1667407170000, 1667407185000], + name: 'time', + state: { multipleFrames: true, displayName: 'time', origin: { fieldIndex: 0, frameIndex: 0 } }, + type: FieldType.time, + }, + { + config: { custom: { fillBelowTo: 'below_value1' } }, + values: [1, 2, 3], + name: 'value1', + state: { multipleFrames: true, displayName: 'value1', origin: { fieldIndex: 1, frameIndex: 0 } }, + type: FieldType.number, + }, + { + config: { custom: { fillBelowTo: 'below_value2' } }, + values: [4, 5, 6], + name: 'value2', + state: { multipleFrames: true, displayName: 'value2', origin: { fieldIndex: 2, frameIndex: 0 } }, + type: FieldType.number, + }, + ], + length: 3, + }, + { + refId: 'B', + fields: [ + { + config: {}, + values: [1667406900000, 1667407170000, 1667407185000], + name: 'time', + state: { multipleFrames: true, displayName: 'time', origin: { fieldIndex: 0, frameIndex: 1 } }, + type: FieldType.time, + }, + { + config: {}, + values: [4, 5, 6], + name: 'below_value1', + state: { multipleFrames: true, displayName: 'below_value1', origin: { fieldIndex: 1, frameIndex: 1 } }, + type: FieldType.number, + }, + { + config: {}, + values: [4, 5, 6], + name: 'below_value2', + state: { multipleFrames: true, displayName: 'below_value2', origin: { fieldIndex: 2, frameIndex: 1 } }, + type: FieldType.number, + }, + ], + length: 3, + }, + ], + expectedResult: 2, + }, + ]; + }); + + it('should verify if fill below to is set then builder bands are set', () => { + for (const test of tests) { + const builder = preparePlotConfigBuilder({ + frame: test.alignedFrame, + //@ts-ignore + theme: getTheme(), + timeZones: ['browser'], + getTimeRange: jest.fn(), + eventBus, + sync: jest.fn(), + allFrames: test.allFrames, + renderers, + }); + + //@ts-ignore + expect(builder.bands.length).toBe(test.expectedResult); + } + }); + + it('should verify if fill below to is not set then builder bands are empty', () => { + tests[0].alignedFrame.fields[1].config.custom.fillBelowTo = undefined; + tests[0].allFrames[0].fields[1].config.custom.fillBelowTo = undefined; + tests[1].alignedFrame.fields[1].config.custom.fillBelowTo = undefined; + tests[1].alignedFrame.fields[2].config.custom.fillBelowTo = undefined; + tests[1].allFrames[0].fields[1].config.custom.fillBelowTo = undefined; + tests[1].allFrames[0].fields[2].config.custom.fillBelowTo = undefined; + tests[0].expectedResult = 0; + tests[1].expectedResult = 0; + + for (const test of tests) { + const builder = preparePlotConfigBuilder({ + frame: test.alignedFrame, + //@ts-ignore + theme: getTheme(), + timeZones: ['browser'], + getTimeRange: jest.fn(), + eventBus, + sync: jest.fn(), + allFrames: test.allFrames, + renderers, + }); + + //@ts-ignore + expect(builder.bands.length).toBe(test.expectedResult); + } + }); + + it('should verify if fill below to is set and field name is overriden then builder bands are set', () => { + tests[0].alignedFrame.fields[2].config.displayName = 'newName'; + tests[0].alignedFrame.fields[2].state.displayName = 'newName'; + tests[0].allFrames[1].fields[1].config.displayName = 'newName'; + tests[0].allFrames[1].fields[1].state.displayName = 'newName'; + + tests[1].alignedFrame.fields[3].config.displayName = 'newName'; + tests[1].alignedFrame.fields[3].state.displayName = 'newName'; + tests[1].allFrames[1].fields[1].config.displayName = 'newName'; + tests[1].allFrames[1].fields[1].state.displayName = 'newName'; + + for (const test of tests) { + const builder = preparePlotConfigBuilder({ + frame: test.alignedFrame, + //@ts-ignore + theme: getTheme(), + timeZones: ['browser'], + getTimeRange: jest.fn(), + eventBus, + sync: jest.fn(), + allFrames: test.allFrames, + renderers, + }); + + //@ts-ignore + expect(builder.bands.length).toBe(test.expectedResult); + } + }); +}); diff --git a/packages/grafana-ui/src/graveyard/TimeSeries/utils.ts b/packages/grafana-ui/src/graveyard/TimeSeries/utils.ts new file mode 100644 index 0000000000..12a6e3908c --- /dev/null +++ b/packages/grafana-ui/src/graveyard/TimeSeries/utils.ts @@ -0,0 +1,668 @@ +import { isNumber } from 'lodash'; +import uPlot from 'uplot'; + +import { + DashboardCursorSync, + DataFrame, + DataHoverClearEvent, + DataHoverEvent, + DataHoverPayload, + FieldConfig, + FieldType, + formattedValueToString, + getFieldColorModeForField, + getFieldSeriesColor, + getFieldDisplayName, + getDisplayProcessor, + FieldColorModeId, + DecimalCount, +} from '@grafana/data'; +import { + AxisPlacement, + GraphDrawStyle, + GraphFieldConfig, + GraphThresholdsStyleMode, + VisibilityMode, + ScaleDirection, + ScaleOrientation, + StackingMode, + GraphTransform, + AxisColorMode, + GraphGradientMode, +} from '@grafana/schema'; + +// unit lookup needed to determine if we want power-of-2 or power-of-10 axis ticks +// see categories.ts is @grafana/data +const IEC_UNITS = new Set([ + 'bytes', + 'bits', + 'kbytes', + 'mbytes', + 'gbytes', + 'tbytes', + 'pbytes', + 'binBps', + 'binbps', + 'KiBs', + 'Kibits', + 'MiBs', + 'Mibits', + 'GiBs', + 'Gibits', + 'TiBs', + 'Tibits', + 'PiBs', + 'Pibits', +]); + +const BIN_INCRS = Array(53); + +for (let i = 0; i < BIN_INCRS.length; i++) { + BIN_INCRS[i] = 2 ** i; +} + +import { UPlotConfigBuilder, UPlotConfigPrepFn } from '../../components/uPlot/config/UPlotConfigBuilder'; +import { getScaleGradientFn } from '../../components/uPlot/config/gradientFills'; +import { getStackingGroups, preparePlotData2 } from '../../components/uPlot/utils'; +import { buildScaleKey } from '../GraphNG/utils'; + +const defaultFormatter = (v: any, decimals: DecimalCount = 1) => (v == null ? '-' : v.toFixed(decimals)); + +const defaultConfig: GraphFieldConfig = { + drawStyle: GraphDrawStyle.Line, + showPoints: VisibilityMode.Auto, + axisPlacement: AxisPlacement.Auto, +}; + +export const preparePlotConfigBuilder: UPlotConfigPrepFn<{ + sync?: () => DashboardCursorSync; +}> = ({ + frame, + theme, + timeZones, + getTimeRange, + eventBus, + sync, + allFrames, + renderers, + tweakScale = (opts) => opts, + tweakAxis = (opts) => opts, + eventsScope = '__global_', +}) => { + const builder = new UPlotConfigBuilder(timeZones[0]); + + let alignedFrame: DataFrame; + + builder.setPrepData((frames) => { + // cache alignedFrame + alignedFrame = frames[0]; + + return preparePlotData2(frames[0], builder.getStackingGroups()); + }); + + // X is the first field in the aligned frame + const xField = frame.fields[0]; + if (!xField) { + return builder; // empty frame with no options + } + + const xScaleKey = 'x'; + let xScaleUnit = '_x'; + let yScaleKey = ''; + + const xFieldAxisPlacement = + xField.config.custom?.axisPlacement !== AxisPlacement.Hidden ? AxisPlacement.Bottom : AxisPlacement.Hidden; + const xFieldAxisShow = xField.config.custom?.axisPlacement !== AxisPlacement.Hidden; + + if (xField.type === FieldType.time) { + xScaleUnit = 'time'; + builder.addScale({ + scaleKey: xScaleKey, + orientation: ScaleOrientation.Horizontal, + direction: ScaleDirection.Right, + isTime: true, + range: () => { + const r = getTimeRange(); + return [r.from.valueOf(), r.to.valueOf()]; + }, + }); + + // filters first 2 ticks to make space for timezone labels + const filterTicks: uPlot.Axis.Filter | undefined = + timeZones.length > 1 + ? (u, splits) => { + return splits.map((v, i) => (i < 2 ? null : v)); + } + : undefined; + + for (let i = 0; i < timeZones.length; i++) { + const timeZone = timeZones[i]; + builder.addAxis({ + scaleKey: xScaleKey, + isTime: true, + placement: xFieldAxisPlacement, + show: xFieldAxisShow, + label: xField.config.custom?.axisLabel, + timeZone, + theme, + grid: { show: i === 0 && xField.config.custom?.axisGridShow }, + filter: filterTicks, + }); + } + + // render timezone labels + if (timeZones.length > 1) { + builder.addHook('drawAxes', (u: uPlot) => { + u.ctx.save(); + + u.ctx.fillStyle = theme.colors.text.primary; + u.ctx.textAlign = 'left'; + u.ctx.textBaseline = 'bottom'; + + let i = 0; + u.axes.forEach((a) => { + if (a.side === 2) { + //@ts-ignore + let cssBaseline: number = a._pos + a._size; + u.ctx.fillText(timeZones[i], u.bbox.left, cssBaseline * uPlot.pxRatio); + i++; + } + }); + + u.ctx.restore(); + }); + } + } else { + // Not time! + if (xField.config.unit) { + xScaleUnit = xField.config.unit; + } + + builder.addScale({ + scaleKey: xScaleKey, + orientation: ScaleOrientation.Horizontal, + direction: ScaleDirection.Right, + range: (u, dataMin, dataMax) => [xField.config.min ?? dataMin, xField.config.max ?? dataMax], + }); + + builder.addAxis({ + scaleKey: xScaleKey, + placement: xFieldAxisPlacement, + show: xFieldAxisShow, + label: xField.config.custom?.axisLabel, + theme, + grid: { show: xField.config.custom?.axisGridShow }, + formatValue: (v, decimals) => formattedValueToString(xField.display!(v, decimals)), + }); + } + + let customRenderedFields = + renderers?.flatMap((r) => Object.values(r.fieldMap).filter((name) => r.indicesOnly.indexOf(name) === -1)) ?? []; + + let indexByName: Map | undefined; + + for (let i = 1; i < frame.fields.length; i++) { + const field = frame.fields[i]; + + const config: FieldConfig = { + ...field.config, + custom: { + ...defaultConfig, + ...field.config.custom, + }, + }; + + const customConfig: GraphFieldConfig = config.custom!; + + if (field === xField || (field.type !== FieldType.number && field.type !== FieldType.enum)) { + continue; + } + + let fmt = field.display ?? defaultFormatter; + if (field.config.custom?.stacking?.mode === StackingMode.Percent) { + fmt = getDisplayProcessor({ + field: { + ...field, + config: { + ...field.config, + unit: 'percentunit', + }, + }, + theme, + }); + } + const scaleKey = buildScaleKey(config, field.type); + const colorMode = getFieldColorModeForField(field); + const scaleColor = getFieldSeriesColor(field, theme); + const seriesColor = scaleColor.color; + + // The builder will manage unique scaleKeys and combine where appropriate + builder.addScale( + tweakScale( + { + scaleKey, + orientation: ScaleOrientation.Vertical, + direction: ScaleDirection.Up, + distribution: customConfig.scaleDistribution?.type, + log: customConfig.scaleDistribution?.log, + linearThreshold: customConfig.scaleDistribution?.linearThreshold, + min: field.config.min, + max: field.config.max, + softMin: customConfig.axisSoftMin, + softMax: customConfig.axisSoftMax, + centeredZero: customConfig.axisCenteredZero, + range: + customConfig.stacking?.mode === StackingMode.Percent + ? (u: uPlot, dataMin: number, dataMax: number) => { + dataMin = dataMin < 0 ? -1 : 0; + dataMax = dataMax > 0 ? 1 : 0; + return [dataMin, dataMax]; + } + : field.type === FieldType.enum + ? (u: uPlot, dataMin: number, dataMax: number) => { + // this is the exhaustive enum (stable) + let len = field.config.type!.enum!.text!.length; + + return [-1, len]; + + // these are only values that are present + // return [dataMin - 1, dataMax + 1] + } + : undefined, + decimals: field.config.decimals, + }, + field + ) + ); + + if (!yScaleKey) { + yScaleKey = scaleKey; + } + + if (customConfig.axisPlacement !== AxisPlacement.Hidden) { + let axisColor: uPlot.Axis.Stroke | undefined; + + if (customConfig.axisColorMode === AxisColorMode.Series) { + if ( + colorMode.isByValue && + field.config.custom?.gradientMode === GraphGradientMode.Scheme && + colorMode.id === FieldColorModeId.Thresholds + ) { + axisColor = getScaleGradientFn(1, theme, colorMode, field.config.thresholds); + } else { + axisColor = seriesColor; + } + } + + const axisDisplayOptions = { + border: { + show: customConfig.axisBorderShow || false, + width: 1 / devicePixelRatio, + stroke: axisColor || theme.colors.text.primary, + }, + ticks: { + show: customConfig.axisBorderShow || false, + stroke: axisColor || theme.colors.text.primary, + }, + color: axisColor || theme.colors.text.primary, + }; + + let incrs: uPlot.Axis.Incrs | undefined; + + // TODO: these will be dynamic with frame updates, so need to accept getYTickLabels() + let values: uPlot.Axis.Values | undefined; + let splits: uPlot.Axis.Splits | undefined; + + if (IEC_UNITS.has(config.unit!)) { + incrs = BIN_INCRS; + } else if (field.type === FieldType.enum) { + let text = field.config.type!.enum!.text!; + splits = text.map((v: string, i: number) => i); + values = text; + } + + builder.addAxis( + tweakAxis( + { + scaleKey, + label: customConfig.axisLabel, + size: customConfig.axisWidth, + placement: customConfig.axisPlacement ?? AxisPlacement.Auto, + formatValue: (v, decimals) => formattedValueToString(fmt(v, decimals)), + theme, + grid: { show: customConfig.axisGridShow }, + decimals: field.config.decimals, + distr: customConfig.scaleDistribution?.type, + splits, + values, + incrs, + ...axisDisplayOptions, + }, + field + ) + ); + } + + const showPoints = + customConfig.drawStyle === GraphDrawStyle.Points ? VisibilityMode.Always : customConfig.showPoints; + + let pointsFilter: uPlot.Series.Points.Filter = () => null; + + if (customConfig.spanNulls !== true) { + pointsFilter = (u, seriesIdx, show, gaps) => { + let filtered = []; + + let series = u.series[seriesIdx]; + + if (!show && gaps && gaps.length) { + const [firstIdx, lastIdx] = series.idxs!; + const xData = u.data[0]; + const yData = u.data[seriesIdx]; + const firstPos = Math.round(u.valToPos(xData[firstIdx], 'x', true)); + const lastPos = Math.round(u.valToPos(xData[lastIdx], 'x', true)); + + if (gaps[0][0] === firstPos) { + filtered.push(firstIdx); + } + + // show single points between consecutive gaps that share end/start + for (let i = 0; i < gaps.length; i++) { + let thisGap = gaps[i]; + let nextGap = gaps[i + 1]; + + if (nextGap && thisGap[1] === nextGap[0]) { + // approx when data density is > 1pt/px, since gap start/end pixels are rounded + let approxIdx = u.posToIdx(thisGap[1], true); + + if (yData[approxIdx] == null) { + // scan left/right alternating to find closest index with non-null value + for (let j = 1; j < 100; j++) { + if (yData[approxIdx + j] != null) { + approxIdx += j; + break; + } + if (yData[approxIdx - j] != null) { + approxIdx -= j; + break; + } + } + } + + filtered.push(approxIdx); + } + } + + if (gaps[gaps.length - 1][1] === lastPos) { + filtered.push(lastIdx); + } + } + + return filtered.length ? filtered : null; + }; + } + + let { fillOpacity } = customConfig; + + let pathBuilder: uPlot.Series.PathBuilder | null = null; + let pointsBuilder: uPlot.Series.Points.Show | null = null; + + if (field.state?.origin) { + if (!indexByName) { + indexByName = getNamesToFieldIndex(frame, allFrames); + } + + const originFrame = allFrames[field.state.origin.frameIndex]; + const originField = originFrame?.fields[field.state.origin.fieldIndex]; + + const dispName = getFieldDisplayName(originField ?? field, originFrame, allFrames); + + // disable default renderers + if (customRenderedFields.indexOf(dispName) >= 0) { + pathBuilder = () => null; + pointsBuilder = () => undefined; + } else if (customConfig.transform === GraphTransform.Constant) { + // patch some monkeys! + const defaultBuilder = uPlot.paths!.linear!(); + + pathBuilder = (u, seriesIdx) => { + //eslint-disable-next-line + const _data: any[] = (u as any)._data; // uplot.AlignedData not exposed in types + + // the data we want the line renderer to pull is x at each plot edge with paired flat y values + + const r = getTimeRange(); + let xData = [r.from.valueOf(), r.to.valueOf()]; + let firstY = _data[seriesIdx].find((v: number | null | undefined) => v != null); + let yData = [firstY, firstY]; + let fauxData = _data.slice(); + fauxData[0] = xData; + fauxData[seriesIdx] = yData; + + //eslint-disable-next-line + return defaultBuilder( + { + ...u, + _data: fauxData, + } as any, + seriesIdx, + 0, + 1 + ); + }; + } + + if (customConfig.fillBelowTo) { + const fillBelowToField = frame.fields.find( + (f) => + customConfig.fillBelowTo === f.name || + customConfig.fillBelowTo === f.config?.displayNameFromDS || + customConfig.fillBelowTo === getFieldDisplayName(f, frame, allFrames) + ); + + const fillBelowDispName = fillBelowToField + ? getFieldDisplayName(fillBelowToField, frame, allFrames) + : customConfig.fillBelowTo; + + const t = indexByName.get(dispName); + const b = indexByName.get(fillBelowDispName); + if (isNumber(b) && isNumber(t)) { + builder.addBand({ + series: [t, b], + fill: undefined, // using null will have the band use fill options from `t` + }); + + if (!fillOpacity) { + fillOpacity = 35; // default from flot + } + } else { + fillOpacity = 0; + } + } + } + + let dynamicSeriesColor: ((seriesIdx: number) => string | undefined) | undefined = undefined; + + if (colorMode.id === FieldColorModeId.Thresholds) { + dynamicSeriesColor = (seriesIdx) => getFieldSeriesColor(alignedFrame.fields[seriesIdx], theme).color; + } + + builder.addSeries({ + pathBuilder, + pointsBuilder, + scaleKey, + showPoints, + pointsFilter, + colorMode, + fillOpacity, + theme, + dynamicSeriesColor, + drawStyle: customConfig.drawStyle!, + lineColor: customConfig.lineColor ?? seriesColor, + lineWidth: customConfig.lineWidth, + lineInterpolation: customConfig.lineInterpolation, + lineStyle: customConfig.lineStyle, + barAlignment: customConfig.barAlignment, + barWidthFactor: customConfig.barWidthFactor, + barMaxWidth: customConfig.barMaxWidth, + pointSize: customConfig.pointSize, + spanNulls: customConfig.spanNulls || false, + show: !customConfig.hideFrom?.viz, + gradientMode: customConfig.gradientMode, + thresholds: config.thresholds, + hardMin: field.config.min, + hardMax: field.config.max, + softMin: customConfig.axisSoftMin, + softMax: customConfig.axisSoftMax, + // The following properties are not used in the uPlot config, but are utilized as transport for legend config + dataFrameFieldIndex: field.state?.origin, + }); + + // Render thresholds in graph + if (customConfig.thresholdsStyle && config.thresholds) { + const thresholdDisplay = customConfig.thresholdsStyle.mode ?? GraphThresholdsStyleMode.Off; + if (thresholdDisplay !== GraphThresholdsStyleMode.Off) { + builder.addThresholds({ + config: customConfig.thresholdsStyle, + thresholds: config.thresholds, + scaleKey, + theme, + hardMin: field.config.min, + hardMax: field.config.max, + softMin: customConfig.axisSoftMin, + softMax: customConfig.axisSoftMax, + }); + } + } + } + + let stackingGroups = getStackingGroups(frame); + + builder.setStackingGroups(stackingGroups); + + // hook up custom/composite renderers + renderers?.forEach((r) => { + if (!indexByName) { + indexByName = getNamesToFieldIndex(frame, allFrames); + } + let fieldIndices: Record = {}; + + for (let key in r.fieldMap) { + let dispName = r.fieldMap[key]; + fieldIndices[key] = indexByName.get(dispName)!; + } + + r.init(builder, fieldIndices); + }); + + builder.scaleKeys = [xScaleKey, yScaleKey]; + + // if hovered value is null, how far we may scan left/right to hover nearest non-null + const hoverProximityPx = 15; + + let cursor: Partial = { + // this scans left and right from cursor position to find nearest data index with value != null + // TODO: do we want to only scan past undefined values, but halt at explicit null values? + dataIdx: (self, seriesIdx, hoveredIdx, cursorXVal) => { + let seriesData = self.data[seriesIdx]; + + if (seriesData[hoveredIdx] == null) { + let nonNullLft = null, + nonNullRgt = null, + i; + + i = hoveredIdx; + while (nonNullLft == null && i-- > 0) { + if (seriesData[i] != null) { + nonNullLft = i; + } + } + + i = hoveredIdx; + while (nonNullRgt == null && i++ < seriesData.length) { + if (seriesData[i] != null) { + nonNullRgt = i; + } + } + + let xVals = self.data[0]; + + let curPos = self.valToPos(cursorXVal, 'x'); + let rgtPos = nonNullRgt == null ? Infinity : self.valToPos(xVals[nonNullRgt], 'x'); + let lftPos = nonNullLft == null ? -Infinity : self.valToPos(xVals[nonNullLft], 'x'); + + let lftDelta = curPos - lftPos; + let rgtDelta = rgtPos - curPos; + + if (lftDelta <= rgtDelta) { + if (lftDelta <= hoverProximityPx) { + hoveredIdx = nonNullLft!; + } + } else { + if (rgtDelta <= hoverProximityPx) { + hoveredIdx = nonNullRgt!; + } + } + } + + return hoveredIdx; + }, + }; + + if (sync && sync() !== DashboardCursorSync.Off) { + const payload: DataHoverPayload = { + point: { + [xScaleKey]: null, + [yScaleKey]: null, + }, + data: frame, + }; + + const hoverEvent = new DataHoverEvent(payload); + cursor.sync = { + key: eventsScope, + filters: { + pub: (type: string, src: uPlot, x: number, y: number, w: number, h: number, dataIdx: number) => { + if (sync && sync() === DashboardCursorSync.Off) { + return false; + } + + payload.rowIndex = dataIdx; + if (x < 0 && y < 0) { + payload.point[xScaleUnit] = null; + payload.point[yScaleKey] = null; + eventBus.publish(new DataHoverClearEvent()); + } else { + // convert the points + payload.point[xScaleUnit] = src.posToVal(x, xScaleKey); + payload.point[yScaleKey] = src.posToVal(y, yScaleKey); + payload.point.panelRelY = y > 0 ? y / h : 1; // used by old graph panel to position tooltip + eventBus.publish(hoverEvent); + hoverEvent.payload.down = undefined; + } + return true; + }, + }, + scales: [xScaleKey, yScaleKey], + // match: [() => true, (a, b) => a === b], + }; + } + + builder.setSync(); + builder.setCursor(cursor); + + return builder; +}; + +export function getNamesToFieldIndex(frame: DataFrame, allFrames: DataFrame[]): Map { + const originNames = new Map(); + frame.fields.forEach((field, i) => { + const origin = field.state?.origin; + if (origin) { + const origField = allFrames[origin.frameIndex]?.fields[origin.fieldIndex]; + if (origField) { + originNames.set(getFieldDisplayName(origField, allFrames[origin.frameIndex], allFrames), i); + } + } + }); + return originNames; +} diff --git a/public/app/angular/angular_wrappers.ts b/public/app/angular/angular_wrappers.ts index f337217116..fc0263ca32 100644 --- a/public/app/angular/angular_wrappers.ts +++ b/public/app/angular/angular_wrappers.ts @@ -3,6 +3,7 @@ import { ColorPicker, DataLinksInlineEditor, DataSourceHttpSettings, + GraphContextMenu, Icon, LegacyForms, SeriesColorPickerPopoverWithTheme, @@ -21,8 +22,6 @@ import { MetricSelect } from '../core/components/Select/MetricSelect'; import { TagFilter } from '../core/components/TagFilter/TagFilter'; import { HelpModal } from '../core/components/help/HelpModal'; -import { GraphContextMenu } from './components/legacy_graph_panel/GraphContextMenu'; - const { SecretFormField } = LegacyForms; export function registerAngularDirectives() { diff --git a/public/app/plugins/panel/timeseries/plugins/ContextMenuPlugin.tsx b/public/app/plugins/panel/timeseries/plugins/ContextMenuPlugin.tsx index 005f348762..59c6ad9dce 100644 --- a/public/app/plugins/panel/timeseries/plugins/ContextMenuPlugin.tsx +++ b/public/app/plugins/panel/timeseries/plugins/ContextMenuPlugin.tsx @@ -2,8 +2,15 @@ import React, { useLayoutEffect, useMemo, useRef, useState } from 'react'; import { useClickAway } from 'react-use'; import { CartesianCoords2D, DataFrame, getFieldDisplayName, InterpolateFunction, TimeZone } from '@grafana/data'; -import { ContextMenu, MenuItemProps, MenuItemsGroup, MenuGroup, MenuItem, UPlotConfigBuilder } from '@grafana/ui'; -import { GraphContextMenuHeader } from 'app/angular/components/legacy_graph_panel/GraphContextMenu'; +import { + ContextMenu, + GraphContextMenuHeader, + MenuItemProps, + MenuItemsGroup, + MenuGroup, + MenuItem, + UPlotConfigBuilder, +} from '@grafana/ui'; type ContextMenuSelectionCoords = { viewport: CartesianCoords2D; plotCanvas: CartesianCoords2D }; type ContextMenuSelectionPoint = { seriesIdx: number | null; dataIdx: number | null }; From e26cd8614d1f5f9445108c95824042191beb5be6 Mon Sep 17 00:00:00 2001 From: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> Date: Thu, 29 Feb 2024 09:39:25 -0500 Subject: [PATCH 0303/1406] Docs: fix config file info in upgrade guide (#83273) * Updated incorrect custom config file names and locations * Corrected default config file name * Updated more config file info * Apply suggestions from code review Co-authored-by: Pepe Cano <825430+ppcano@users.noreply.github.com> * Reverted change * Fixed default config file info, added second custom file option, and added note about file locations * Added file path for second custom option * Apply suggestion from review Co-authored-by: Usman Ahmad * Apply suggestion from review Co-authored-by: Usman Ahmad * Apply suggestions from review Co-authored-by: Usman Ahmad * Apply suggestion from review * Add version interpolation syntax * Updated wording * Ran prettier --------- Co-authored-by: Pepe Cano <825430+ppcano@users.noreply.github.com> Co-authored-by: Usman Ahmad --- docs/sources/shared/back-up/back-up-grafana.md | 6 ++++-- .../sources/shared/upgrade/upgrade-common-tasks.md | 14 +++++++------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/docs/sources/shared/back-up/back-up-grafana.md b/docs/sources/shared/back-up/back-up-grafana.md index c12ec9c6d4..db2f437663 100644 --- a/docs/sources/shared/back-up/back-up-grafana.md +++ b/docs/sources/shared/back-up/back-up-grafana.md @@ -17,8 +17,10 @@ Copy Grafana configuration files that you might have modified in your Grafana de The Grafana configuration files are located in the following directories: -- Default configuration: `$WORKING_DIR/conf/defaults.ini` -- Custom configuration: `$WORKING_DIR/conf/custom.ini` +- Default configuration: `$WORKING_DIR/defaults.ini` (Don't change this file) +- Custom configuration: `$WORKING_DIR/custom.ini` + +For more information on where to find configuration files, refer to [Configuration file location](https://grafana.com/docs/grafana//setup-grafana/configure-grafana/#configuration-file-location). {{% admonition type="note" %}} If you installed Grafana using the `deb` or `rpm` packages, then your configuration file is located at diff --git a/docs/sources/shared/upgrade/upgrade-common-tasks.md b/docs/sources/shared/upgrade/upgrade-common-tasks.md index 3c53e3d44f..7e84a837d4 100644 --- a/docs/sources/shared/upgrade/upgrade-common-tasks.md +++ b/docs/sources/shared/upgrade/upgrade-common-tasks.md @@ -8,13 +8,13 @@ title: Upgrade guide common tasks ## Upgrade Grafana -The following sections provide instructions for how to upgrade Grafana based on your installation method. +The following sections provide instructions for how to upgrade Grafana based on your installation method. For more information on where to find configuration files, refer to [Configuration file location](https://grafana.com/docs/grafana//setup-grafana/configure-grafana/#configuration-file-location). ### Debian To upgrade Grafana installed from a Debian package (`.deb`), complete the following steps: -1. In your current installation of Grafana, save your custom configuration changes to a file named `/conf/custom.ini`. +1. In your current installation of Grafana, save your custom configuration changes to a file named `/grafana.ini`. This enables you to upgrade Grafana without the risk of losing your configuration changes. @@ -32,7 +32,7 @@ To upgrade Grafana installed from a Debian package (`.deb`), complete the follow To upgrade Grafana installed from the Grafana Labs APT repository, complete the following steps: -1. In your current installation of Grafana, save your custom configuration changes to a file named `/conf/custom.ini`. +1. In your current installation of Grafana, save your custom configuration changes to a file named `/grafana.ini`. This enables you to upgrade Grafana without the risk of losing your configuration changes. @@ -49,7 +49,7 @@ Grafana automatically updates when you run `apt-get upgrade`. To upgrade Grafana installed from the binary `.tar.gz` package, complete the following steps: -1. In your current installation of Grafana, save your custom configuration changes to a file named `/conf/custom.ini`. +1. In your current installation of Grafana, save your custom configuration changes to the custom configuration file, `custom.ini` or `grafana.ini`. This enables you to upgrade Grafana without the risk of losing your configuration changes. @@ -61,7 +61,7 @@ To upgrade Grafana installed from the binary `.tar.gz` package, complete the fol To upgrade Grafana installed using RPM or YUM complete the following steps: -1. In your current installation of Grafana, save your custom configuration changes to a file named `/conf/custom.ini`. +1. In your current installation of Grafana, save your custom configuration changes to a file named `/grafana.ini`. This enables you to upgrade Grafana without the risk of losing your configuration changes. @@ -84,7 +84,7 @@ To upgrade Grafana installed using RPM or YUM complete the following steps: To upgrade Grafana running in a Docker container, complete the following steps: -1. In your current installation of Grafana, save your custom configuration changes to a file named `/conf/custom.ini`. +1. Use Grafana [environment variables](https://grafana.com/docs/grafana//setup-grafana/configure-grafana/#override-configuration-with-environment-variables) to save your custom configurations; this is the recommended method. Alternatively, you can view your configuration files manually by accessing the deployed container. This enables you to upgrade Grafana without the risk of losing your configuration changes. @@ -119,7 +119,7 @@ To upgrade Grafana installed on Windows, complete the following steps: To upgrade Grafana installed on Mac, complete the following steps: -1. In your current installation of Grafana, save your custom configuration changes to a file named `/conf/custom.ini`. +1. In your current installation of Grafana, save your custom configuration changes to the custom configuration file, `custom.ini`. This enables you to upgrade Grafana without the risk of losing your configuration changes. From 549094d27ce955bac0ccdefd2ff1e8ade6416617 Mon Sep 17 00:00:00 2001 From: Will Browne Date: Thu, 29 Feb 2024 15:53:09 +0100 Subject: [PATCH 0304/1406] =?UTF-8?q?grafana/data:=20Gardening=20?= =?UTF-8?q?=F0=9F=91=A8=E2=80=8D=F0=9F=8C=BE=E2=9C=82=EF=B8=8F=F0=9F=8C=B3?= =?UTF-8?q?=20(#83615)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * remove suspected unused dependencies from grafana/data * un-export funcs and types * re-export NoopTransformerOptions * remove knip --- packages/grafana-data/package.json | 11 ---- .../src/context/plugins/usePluginContext.tsx | 20 ------- .../grafana-data/src/field/fieldComparers.ts | 12 ++--- .../src/panel/getPanelOptionsWithDefaults.ts | 2 +- .../transformations/matchers/predicates.ts | 6 +-- .../transformers/calculateField.ts | 2 +- .../transformers/groupToNestedTable.ts | 2 +- .../transformations/transformers/reduce.ts | 6 +-- .../transformations/transformers/sortBy.ts | 2 +- .../src/types/OptionsUIRegistryBuilder.ts | 2 +- .../src/valueFormats/dateTimeFormatters.ts | 7 --- yarn.lock | 53 +++++++++++-------- 12 files changed, 46 insertions(+), 79 deletions(-) diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index 740b037526..8deb7ab7ed 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -53,7 +53,6 @@ "ol": "7.4.0", "papaparse": "5.4.1", "react-use": "17.5.0", - "regenerator-runtime": "0.14.1", "rxjs": "7.8.1", "string-hash": "^1.1.3", "tinycolor2": "1.6.0", @@ -63,29 +62,19 @@ }, "devDependencies": { "@grafana/tsconfig": "^1.3.0-rc1", - "@rollup/plugin-commonjs": "25.0.7", - "@rollup/plugin-json": "6.1.0", "@rollup/plugin-node-resolve": "15.2.3", - "@testing-library/dom": "9.3.4", - "@testing-library/jest-dom": "6.4.2", - "@testing-library/react": "14.2.1", - "@testing-library/user-event": "14.5.2", "@types/dompurify": "^3.0.0", "@types/history": "4.7.11", - "@types/jest": "29.5.12", - "@types/jquery": "3.5.29", "@types/lodash": "4.14.202", "@types/marked": "5.0.2", "@types/node": "20.11.20", "@types/papaparse": "5.3.14", "@types/react": "18.2.60", "@types/react-dom": "18.2.19", - "@types/testing-library__jest-dom": "5.14.9", "@types/tinycolor2": "1.4.6", "esbuild": "0.18.12", "react": "18.2.0", "react-dom": "18.2.0", - "react-test-renderer": "18.2.0", "rimraf": "5.0.5", "rollup": "2.79.1", "rollup-plugin-dts": "^5.0.0", diff --git a/packages/grafana-data/src/context/plugins/usePluginContext.tsx b/packages/grafana-data/src/context/plugins/usePluginContext.tsx index e2ea105662..51723446b8 100644 --- a/packages/grafana-data/src/context/plugins/usePluginContext.tsx +++ b/packages/grafana-data/src/context/plugins/usePluginContext.tsx @@ -1,7 +1,5 @@ import { useContext } from 'react'; -import { PluginMeta } from '../../types'; - import { Context, PluginContextType } from './PluginContext'; export function usePluginContext(): PluginContextType { @@ -11,21 +9,3 @@ export function usePluginContext(): PluginContextType { } return context; } - -export function usePluginMeta(): PluginMeta { - const context = usePluginContext(); - - return context.meta; -} - -export function usePluginJsonData() { - const context = usePluginContext(); - - return context.meta.jsonData; -} - -export function usePluginVersion() { - const context = usePluginContext(); - - return context.meta.info.version; -} diff --git a/packages/grafana-data/src/field/fieldComparers.ts b/packages/grafana-data/src/field/fieldComparers.ts index 972b435cd3..3f525b0640 100644 --- a/packages/grafana-data/src/field/fieldComparers.ts +++ b/packages/grafana-data/src/field/fieldComparers.ts @@ -5,7 +5,6 @@ import { Field, FieldType } from '../types/dataFrame'; type IndexComparer = (a: number, b: number) => number; -/** @public */ export const fieldIndexComparer = (field: Field, reverse = false): IndexComparer => { const values = field.values; @@ -26,8 +25,7 @@ export const fieldIndexComparer = (field: Field, reverse = false): IndexComparer } }; -/** @public */ -export const timeComparer = (a: unknown, b: unknown): number => { +const timeComparer = (a: unknown, b: unknown): number => { if (!a || !b) { return falsyComparer(a, b); } @@ -49,20 +47,18 @@ export const timeComparer = (a: unknown, b: unknown): number => { return 0; }; -/** @public */ -export const numericComparer = (a: number, b: number): number => { +const numericComparer = (a: number, b: number): number => { return a - b; }; -/** @public */ -export const stringComparer = (a: string, b: string): number => { +const stringComparer = (a: string, b: string): number => { if (!a || !b) { return falsyComparer(a, b); } return a.localeCompare(b); }; -export const booleanComparer = (a: boolean, b: boolean): number => { +const booleanComparer = (a: boolean, b: boolean): number => { return falsyComparer(a, b); }; diff --git a/packages/grafana-data/src/panel/getPanelOptionsWithDefaults.ts b/packages/grafana-data/src/panel/getPanelOptionsWithDefaults.ts index f3a8d44faf..3bcdf8883a 100644 --- a/packages/grafana-data/src/panel/getPanelOptionsWithDefaults.ts +++ b/packages/grafana-data/src/panel/getPanelOptionsWithDefaults.ts @@ -14,7 +14,7 @@ import { ThresholdsConfig, ThresholdsMode } from '../types/thresholds'; import { PanelPlugin } from './PanelPlugin'; -export interface Props { +interface Props { plugin: PanelPlugin; currentFieldConfig: FieldConfigSource; currentOptions: Record; diff --git a/packages/grafana-data/src/transformations/matchers/predicates.ts b/packages/grafana-data/src/transformations/matchers/predicates.ts index ef26e05be1..7939f95aec 100644 --- a/packages/grafana-data/src/transformations/matchers/predicates.ts +++ b/packages/grafana-data/src/transformations/matchers/predicates.ts @@ -184,11 +184,11 @@ export const alwaysFieldMatcher = (field: Field) => { return true; }; -export const alwaysFrameMatcher = (frame: DataFrame) => { +const alwaysFrameMatcher = (frame: DataFrame) => { return true; }; -export const neverFieldMatcher = (field: Field) => { +const neverFieldMatcher = (field: Field) => { return false; }; @@ -196,7 +196,7 @@ export const notTimeFieldMatcher = (field: Field) => { return field.type !== FieldType.time; }; -export const neverFrameMatcher = (frame: DataFrame) => { +const neverFrameMatcher = (frame: DataFrame) => { return false; }; diff --git a/packages/grafana-data/src/transformations/transformers/calculateField.ts b/packages/grafana-data/src/transformations/transformers/calculateField.ts index e50dc3b268..b50f1bcac2 100644 --- a/packages/grafana-data/src/transformations/transformers/calculateField.ts +++ b/packages/grafana-data/src/transformations/transformers/calculateField.ts @@ -61,7 +61,7 @@ export interface BinaryOptions { right: string; } -export interface IndexOptions { +interface IndexOptions { asPercentile: boolean; } diff --git a/packages/grafana-data/src/transformations/transformers/groupToNestedTable.ts b/packages/grafana-data/src/transformations/transformers/groupToNestedTable.ts index 146f86a6e4..aa2d1f9c88 100644 --- a/packages/grafana-data/src/transformations/transformers/groupToNestedTable.ts +++ b/packages/grafana-data/src/transformations/transformers/groupToNestedTable.ts @@ -11,7 +11,7 @@ import { DataTransformerID } from './ids'; export const SHOW_NESTED_HEADERS_DEFAULT = true; -export enum GroupByOperationID { +enum GroupByOperationID { aggregate = 'aggregate', groupBy = 'groupby', } diff --git a/packages/grafana-data/src/transformations/transformers/reduce.ts b/packages/grafana-data/src/transformations/transformers/reduce.ts index f8ba7adddc..d7e8ba8cda 100644 --- a/packages/grafana-data/src/transformations/transformers/reduce.ts +++ b/packages/grafana-data/src/transformations/transformers/reduce.ts @@ -64,7 +64,7 @@ export const reduceTransformer: DataTransformerInfo = /** * @internal only exported for testing */ -export function reduceSeriesToRows( +function reduceSeriesToRows( data: DataFrame[], matcher: FieldMatcher, reducerId: ReducerID[], @@ -156,7 +156,7 @@ export function reduceSeriesToRows( return mergeResults(processed); } -export function getDistinctLabelKeys(frames: DataFrame[]): string[] { +function getDistinctLabelKeys(frames: DataFrame[]): string[] { const keys = new Set(); for (const frame of frames) { for (const field of frame.fields) { @@ -173,7 +173,7 @@ export function getDistinctLabelKeys(frames: DataFrame[]): string[] { /** * @internal only exported for testing */ -export function mergeResults(data: DataFrame[]): DataFrame | undefined { +function mergeResults(data: DataFrame[]): DataFrame | undefined { if (!data?.length) { return undefined; } diff --git a/packages/grafana-data/src/transformations/transformers/sortBy.ts b/packages/grafana-data/src/transformations/transformers/sortBy.ts index 7cd99acce3..5831581562 100644 --- a/packages/grafana-data/src/transformations/transformers/sortBy.ts +++ b/packages/grafana-data/src/transformations/transformers/sortBy.ts @@ -43,7 +43,7 @@ export const sortByTransformer: DataTransformerInfo = ), }; -export function sortDataFrames(data: DataFrame[], sort: SortByField[], ctx: DataTransformContext): DataFrame[] { +function sortDataFrames(data: DataFrame[], sort: SortByField[], ctx: DataTransformContext): DataFrame[] { return data.map((frame) => { const s = attachFieldIndex(frame, sort, ctx); if (s.length && s[0].index != null) { diff --git a/packages/grafana-data/src/types/OptionsUIRegistryBuilder.ts b/packages/grafana-data/src/types/OptionsUIRegistryBuilder.ts index 0765f1987a..b5a0e74f34 100644 --- a/packages/grafana-data/src/types/OptionsUIRegistryBuilder.ts +++ b/packages/grafana-data/src/types/OptionsUIRegistryBuilder.ts @@ -30,7 +30,7 @@ export interface OptionsEditorItem /** * Describes an API for option editors UI builder */ -export interface OptionsUIRegistryBuilderAPI< +interface OptionsUIRegistryBuilderAPI< TOptions, TEditorProps, T extends OptionsEditorItem, diff --git a/packages/grafana-data/src/valueFormats/dateTimeFormatters.ts b/packages/grafana-data/src/valueFormats/dateTimeFormatters.ts index fe230f5023..4eb18bf8c5 100644 --- a/packages/grafana-data/src/valueFormats/dateTimeFormatters.ts +++ b/packages/grafana-data/src/valueFormats/dateTimeFormatters.ts @@ -102,13 +102,6 @@ export function toMilliSeconds(size: number, decimals?: DecimalCount, scaledDeci return toFixedScaled(size / 31536000000, decimals, ' year'); } -export function trySubstract(value1: DecimalCount, value2: DecimalCount): DecimalCount { - if (value1 !== null && value1 !== undefined && value2 !== null && value2 !== undefined) { - return value1 - value2; - } - return undefined; -} - export function toSeconds(size: number, decimals?: DecimalCount): FormattedValue { if (size === null) { return { text: '' }; diff --git a/yarn.lock b/yarn.lock index d38be2aca6..a7c80cb677 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3536,18 +3536,10 @@ __metadata: "@braintree/sanitize-url": "npm:7.0.0" "@grafana/schema": "npm:11.0.0-pre" "@grafana/tsconfig": "npm:^1.3.0-rc1" - "@rollup/plugin-commonjs": "npm:25.0.7" - "@rollup/plugin-json": "npm:6.1.0" "@rollup/plugin-node-resolve": "npm:15.2.3" - "@testing-library/dom": "npm:9.3.4" - "@testing-library/jest-dom": "npm:6.4.2" - "@testing-library/react": "npm:14.2.1" - "@testing-library/user-event": "npm:14.5.2" "@types/d3-interpolate": "npm:^3.0.0" "@types/dompurify": "npm:^3.0.0" "@types/history": "npm:4.7.11" - "@types/jest": "npm:29.5.12" - "@types/jquery": "npm:3.5.29" "@types/lodash": "npm:4.14.202" "@types/marked": "npm:5.0.2" "@types/node": "npm:20.11.20" @@ -3555,7 +3547,6 @@ __metadata: "@types/react": "npm:18.2.60" "@types/react-dom": "npm:18.2.19" "@types/string-hash": "npm:1.1.3" - "@types/testing-library__jest-dom": "npm:5.14.9" "@types/tinycolor2": "npm:1.4.6" d3-interpolate: "npm:3.0.1" date-fns: "npm:3.3.1" @@ -3573,9 +3564,7 @@ __metadata: papaparse: "npm:5.4.1" react: "npm:18.2.0" react-dom: "npm:18.2.0" - react-test-renderer: "npm:18.2.0" react-use: "npm:17.5.0" - regenerator-runtime: "npm:0.14.1" rimraf: "npm:5.0.5" rollup: "npm:2.79.1" rollup-plugin-dts: "npm:^5.0.0" @@ -9215,7 +9204,7 @@ __metadata: languageName: node linkType: hard -"@types/eslint@npm:*, @types/eslint@npm:8.56.3, @types/eslint@npm:^8.37.0, @types/eslint@npm:^8.4.10": +"@types/eslint@npm:*, @types/eslint@npm:8.56.3, @types/eslint@npm:^8.37.0": version: 8.56.3 resolution: "@types/eslint@npm:8.56.3" dependencies: @@ -9225,6 +9214,16 @@ __metadata: languageName: node linkType: hard +"@types/eslint@npm:^8.4.10": + version: 8.56.4 + resolution: "@types/eslint@npm:8.56.4" + dependencies: + "@types/estree": "npm:*" + "@types/json-schema": "npm:*" + checksum: 10/bb8018f0c27839dd0b8c515ac4e6fac39500c36ba20007a6ecca2fe5e5f81cbecca2be8f6f649bdafd5556b8c6d5285d8506ae61cc8570f71fd4e6b07042f641 + languageName: node + linkType: hard + "@types/estree@npm:*, @types/estree@npm:^1.0.0, @types/estree@npm:^1.0.5": version: 1.0.5 resolution: "@types/estree@npm:1.0.5" @@ -10213,11 +10212,11 @@ __metadata: linkType: hard "@types/yargs@npm:^15.0.0": - version: 15.0.14 - resolution: "@types/yargs@npm:15.0.14" + version: 15.0.19 + resolution: "@types/yargs@npm:15.0.19" dependencies: "@types/yargs-parser": "npm:*" - checksum: 10/1687ce075a7d01af3c2d342b4f2a2267e06dad6b5eb3fa36643763bd05ca8e6fdfc4dad3d0cb32fc6f3216fd84c0ad2a8032da9190435d033aa800917e8d845c + checksum: 10/c3abcd3472c32c02702f365dc1702a0728562deb8a8c61f3ce2161958d756cc033f7d78567565b4eba62f5869e9b5eac93d4c1dcb2c97af17aafda8f9f892b4b languageName: node linkType: hard @@ -15659,7 +15658,17 @@ __metadata: languageName: node linkType: hard -"enhanced-resolve@npm:^5.10.0, enhanced-resolve@npm:^5.15.0": +"enhanced-resolve@npm:^5.10.0": + version: 5.15.1 + resolution: "enhanced-resolve@npm:5.15.1" + dependencies: + graceful-fs: "npm:^4.2.4" + tapable: "npm:^2.2.0" + checksum: 10/9d4badf18c515f7607539e61d7b78f3057ba2f17b97d188c5ef9bcbc26fa6d25b66f0007d39a3a3c3c2a83b53bedbdb6ce82250c57b85470b6b73004d78989be + languageName: node + linkType: hard + +"enhanced-resolve@npm:^5.15.0": version: 5.15.0 resolution: "enhanced-resolve@npm:5.15.0" dependencies: @@ -17036,7 +17045,7 @@ __metadata: languageName: node linkType: hard -"fast-glob@npm:^3.0.3, fast-glob@npm:^3.2.11, fast-glob@npm:^3.2.9, fast-glob@npm:^3.3.2": +"fast-glob@npm:^3.0.3, fast-glob@npm:^3.2.11, fast-glob@npm:^3.2.9, fast-glob@npm:^3.3.0, fast-glob@npm:^3.3.2": version: 3.3.2 resolution: "fast-glob@npm:3.3.2" dependencies: @@ -18253,15 +18262,15 @@ __metadata: linkType: hard "globby@npm:^13.1.1": - version: 13.1.3 - resolution: "globby@npm:13.1.3" + version: 13.2.2 + resolution: "globby@npm:13.2.2" dependencies: dir-glob: "npm:^3.0.1" - fast-glob: "npm:^3.2.11" - ignore: "npm:^5.2.0" + fast-glob: "npm:^3.3.0" + ignore: "npm:^5.2.4" merge2: "npm:^1.4.1" slash: "npm:^4.0.0" - checksum: 10/c5eee00704455c283b3e466b63d906bcd32a64bbe2d00792016cf518cc1a247433ba8cae4ebe6076075a4b14d6fd07f8a9587083d59bfa85e3c4fab9fffa4d91 + checksum: 10/4494a9d2162a7e4d327988b26be66d8eab87d7f59a83219e74b065e2c3ced23698f68fb10482bf9337133819281803fb886d6ae06afbb2affa743623eb0b1949 languageName: node linkType: hard From 2a1d4f85c76684370ba52718e103c7e0d263fb04 Mon Sep 17 00:00:00 2001 From: Jack Baldry Date: Thu, 29 Feb 2024 15:08:45 +0000 Subject: [PATCH 0305/1406] Update links to default Grafana branch (#83025) --- .changelog-archive/CHANGELOG.2.md | 2 +- .changelog-archive/CHANGELOG.3.md | 2 +- .changelog-archive/CHANGELOG.4.md | 2 +- .changelog-archive/CHANGELOG.6.md | 2 +- .changelog-archive/CHANGELOG.7.md | 16 ++++++++-------- docs/sources/developers/http_api/annotations.md | 2 +- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.changelog-archive/CHANGELOG.2.md b/.changelog-archive/CHANGELOG.2.md index 34491f2731..7fc14a683d 100644 --- a/.changelog-archive/CHANGELOG.2.md +++ b/.changelog-archive/CHANGELOG.2.md @@ -105,7 +105,7 @@ - Notice to makers/users of custom data sources, there is a minor breaking change in 2.2 that require an update to custom data sources for them to work in 2.2. [Read this doc](https://github.com/grafana/grafana/tree/master/docs/sources/datasources/plugin_api.md) for more on the data source api change. -- Data source api changes, [PLUGIN_CHANGES.md](https://github.com/grafana/grafana/blob/master/public/app/plugins/PLUGIN_CHANGES.md) +- Data source api changes, [PLUGIN_CHANGES.md](https://github.com/grafana/grafana/blob/main/public/app/plugins/PLUGIN_CHANGES.md) - The duplicate query function used in data source editors is changed, and moveMetricQuery function was renamed **Tech (Note for devs)** diff --git a/.changelog-archive/CHANGELOG.3.md b/.changelog-archive/CHANGELOG.3.md index 12f18c3128..258aa1ce2e 100644 --- a/.changelog-archive/CHANGELOG.3.md +++ b/.changelog-archive/CHANGELOG.3.md @@ -198,7 +198,7 @@ slack channel (link to slack channel in readme). ### Breaking changes -- **Plugin API**: Both data source and panel plugin api (and plugin.json schema) have been updated, requiring an update to plugins. See [plugin api](https://github.com/grafana/grafana/blob/master/public/app/plugins/plugin_api.md) for more info. +- **Plugin API**: Both data source and panel plugin api (and plugin.json schema) have been updated, requiring an update to plugins. See [plugin api](https://github.com/grafana/grafana/blob/main/public/app/plugins/plugin_api.md) for more info. - **InfluxDB 0.8.x** The data source for the old version of influxdb (0.8.x) is no longer included in default builds, but can easily be installed via improved plugin system, closes [#3523](https://github.com/grafana/grafana/issues/3523) - **KairosDB** The data source is no longer included in default builds, but can easily be installed via improved plugin system, closes [#3524](https://github.com/grafana/grafana/issues/3524) - **Templating**: Templating value formats (glob/regex/pipe etc) are now handled automatically and not specified by the user, this makes variable values possible to reuse in many contexts. It can in some edge cases break existing dashboards that have template variables that do not reload on dashboard load. To fix any issue just go into template variable options and update the variable (so it's values are reloaded.). diff --git a/.changelog-archive/CHANGELOG.4.md b/.changelog-archive/CHANGELOG.4.md index bb93963a2b..e919c0c51e 100644 --- a/.changelog-archive/CHANGELOG.4.md +++ b/.changelog-archive/CHANGELOG.4.md @@ -100,7 +100,7 @@ See [security announcement](https://community.grafana.com/t/grafana-5-2-3-and-4- ## Tech - **Go**: Grafana is now built using golang 1.9 -- **Webpack**: Changed from systemjs to webpack (see readme or building from source guide for new build instructions). Systemjs is still used to load plugins but now plugins can only import a limited set of dependencies. See [PLUGIN_DEV.md](https://github.com/grafana/grafana/blob/master/PLUGIN_DEV.md) for more details on how this can effect some plugins. +- **Webpack**: Changed from systemjs to webpack (see readme or building from source guide for new build instructions). Systemjs is still used to load plugins but now plugins can only import a limited set of dependencies. See [PLUGIN_DEV.md](https://github.com/grafana/grafana/blob/main/PLUGIN_DEV.md) for more details on how this can effect some plugins. # 4.5.2 (2017-09-22) diff --git a/.changelog-archive/CHANGELOG.6.md b/.changelog-archive/CHANGELOG.6.md index 4cfbded16c..6d4ddfe008 100644 --- a/.changelog-archive/CHANGELOG.6.md +++ b/.changelog-archive/CHANGELOG.6.md @@ -1291,4 +1291,4 @@ repo on July 1st. Make sure you have switched to the new repo by then. The new r - **Text Panel**: The text panel does no longer by default allow unsanitized HTML. [#4117](https://github.com/grafana/grafana/issues/4117). This means that if you have text panels with scripts tags they will no longer work as before. To enable unsafe javascript execution in text panels enable the settings `disable_sanitize_html` under the section `[panels]` in your Grafana ini file, or set env variable `GF_PANELS_DISABLE_SANITIZE_HTML=true`. - **Dashboard**: Panel property `minSpan` replaced by `maxPerRow`. Dashboard migration will automatically migrate all dashboard panels using the `minSpan` property to the new `maxPerRow` property [#12991](https://github.com/grafana/grafana/pull/12991) -For older release notes, refer to the [CHANGELOG_ARCHIVE.md](https://github.com/grafana/grafana/blob/master/CHANGELOG_ARCHIVE.md) +For older release notes, refer to the [CHANGELOG_ARCHIVE.md](https://github.com/grafana/grafana/blob/main/CHANGELOG_ARCHIVE.md) diff --git a/.changelog-archive/CHANGELOG.7.md b/.changelog-archive/CHANGELOG.7.md index baa857fcdf..504788b748 100644 --- a/.changelog-archive/CHANGELOG.7.md +++ b/.changelog-archive/CHANGELOG.7.md @@ -544,7 +544,7 @@ Issue [#29407](https://github.com/grafana/grafana/issues/29407) We have upgraded AngularJS from version 1.6.6 to 1.8.2. Due to this upgrade some old angular plugins might stop working and will require a small update. This is due to the deprecation and removal of pre-assigned bindings. So if your custom angular controllers expect component bindings in the controller constructor you need to move this code to an `$onInit` function. For more details on how to migrate AngularJS code open the [migration guide](https://docs.angularjs.org/guide/migration) and search for **pre-assigning bindings**. -In order not to break all angular panel plugins and data sources we have some custom [angular inject behavior](https://github.com/grafana/grafana/blob/master/public/app/core/injectorMonkeyPatch.ts) that makes sure that bindings for these controllers are still set before constructor is called so many old angular panels and data source plugins will still work. Issue [#28736](https://github.com/grafana/grafana/issues/28736) +In order not to break all angular panel plugins and data sources we have some custom [angular inject behavior](https://github.com/grafana/grafana/blob/main/public/app/core/injectorMonkeyPatch.ts) that makes sure that bindings for these controllers are still set before constructor is called so many old angular panels and data source plugins will still work. Issue [#28736](https://github.com/grafana/grafana/issues/28736) ### Deprecations @@ -1288,8 +1288,8 @@ This option to group query variable values into groups by tags has been an exper - **Datasource/Loki**: Support for [deprecated Loki endpoints](https://github.com/grafana/loki/blob/master/docs/api.md#lokis-http-api) has been removed. - **Backend plugins**: Grafana now requires backend plugins to be signed, otherwise Grafana will not load/start them. This is an additional security measure to make sure backend plugin binaries and files haven't been tampered with. Refer to [Upgrade Grafana](https://grafana.com/docs/grafana/latest/installation/upgrading/#upgrading-to-v7-0) for more information. - **Docker**: Our Ubuntu based images have been upgraded to Ubuntu [20.04 LTS](https://releases.ubuntu.com/20.04/). -- **@grafana/ui**: Forms migration notice, see [@grafana/ui changelog](https://github.com/grafana/grafana/blob/master/packages/grafana-ui/CHANGELOG.md) -- **@grafana/ui**: Select API change for creating custom values, see [@grafana/ui changelog](https://github.com/grafana/grafana/blob/master/packages/grafana-ui/CHANGELOG.md) +- **@grafana/ui**: Forms migration notice, see [@grafana/ui changelog](https://github.com/grafana/grafana/blob/main/packages/grafana-ui/CHANGELOG.md) +- **@grafana/ui**: Select API change for creating custom values, see [@grafana/ui changelog](https://github.com/grafana/grafana/blob/main/packages/grafana-ui/CHANGELOG.md) **Deprecation warnings** @@ -1304,7 +1304,7 @@ Not just visualizing data from anywhere, in Grafana 7 you can transform it too. Data transformations will provide a common set of data operations that were previously duplicated as custom features in many panels or data sources but are now an integral part of the Grafana data processing pipeline and something all data sources and panels can take advantage of. -In Grafana 7.0 we have a shared data model for both time series and table data that we call [DataFrame](https://github.com/grafana/grafana/blob/master/docs/sources/plugins/developing/dataframe.md). A DataFrame is like a table with columns but we refer to columns as fields. A time series is simply a DataFrame with two fields (time & value). +In Grafana 7.0 we have a shared data model for both time series and table data that we call [DataFrame](https://github.com/grafana/grafana/blob/main/docs/sources/plugins/developing/dataframe.md). A DataFrame is like a table with columns but we refer to columns as fields. A time series is simply a DataFrame with two fields (time & value). **Transformations shipping in 7.0** @@ -1414,7 +1414,7 @@ We have also extended the time zone options so you can select any of the standar ### Features / Enhancements - **Docker**: Upgrade to Alpine 3.11. [#24056](https://github.com/grafana/grafana/pull/24056), [@aknuds1](https://github.com/aknuds1) -- **Forms**: Remove Forms namespace [BREAKING]. Will cause all `Forms` imports to stop working. See migration guide in [@grafana/ui changelog](https://github.com/grafana/grafana/blob/master/packages/grafana-ui/CHANGELOG.md)[#24378](https://github.com/grafana/grafana/pull/24378), [@tskarhed](https://github.com/tskarhed) +- **Forms**: Remove Forms namespace [BREAKING]. Will cause all `Forms` imports to stop working. See migration guide in [@grafana/ui changelog](https://github.com/grafana/grafana/blob/main/packages/grafana-ui/CHANGELOG.md)[#24378](https://github.com/grafana/grafana/pull/24378), [@tskarhed](https://github.com/tskarhed) ### Bug Fixes @@ -1429,7 +1429,7 @@ We have also extended the time zone options so you can select any of the standar - **Removed PhantomJS**: PhantomJS was deprecated in [Grafana v6.4](https://grafana.com/docs/grafana/latest/guides/whats-new-in-v6-4/#phantomjs-deprecation) and starting from Grafana v7.0.0, all PhantomJS support has been removed. This means that Grafana no longer ships with a built-in image renderer, and we advise you to install the [Grafana Image Renderer plugin](https://grafana.com/grafana/plugins/grafana-image-renderer). - **Docker**: Our Ubuntu based images have been upgraded to Ubuntu [20.04 LTS](https://releases.ubuntu.com/20.04/). - **Dashboard**: A global minimum dashboard refresh interval is now enforced and defaults to 5 seconds. -- **@grafana/ui**: Forms migration notice, see [@grafana/ui changelog](https://github.com/grafana/grafana/blob/master/packages/grafana-ui/CHANGELOG.md) +- **@grafana/ui**: Forms migration notice, see [@grafana/ui changelog](https://github.com/grafana/grafana/blob/main/packages/grafana-ui/CHANGELOG.md) - **Interval calculation**: There is now a new option `Max data points` that controls the auto interval `$__interval` calculation. Interval was previously calculated by dividing the panel width by the time range. With the new max data points option it is now easy to set `$__interval` to a dynamic value that is time range agnostic. For example if you set `Max data points` to 10 Grafana will dynamically set `$__interval` by dividing the current time range by 10. - **Datasource/Loki**: Support for [deprecated Loki endpoints](https://github.com/grafana/loki/blob/master/docs/api.md#lokis-http-api) has been removed. @@ -1484,8 +1484,8 @@ We have also extended the time zone options so you can select any of the standar - **Removed PhantomJS**: PhantomJS was deprecated in [Grafana v6.4](https://grafana.com/docs/grafana/latest/guides/whats-new-in-v6-4/#phantomjs-deprecation) and starting from Grafana v7.0.0, all PhantomJS support has been removed. This means that Grafana no longer ships with a built-in image renderer, and we advise you to install the [Grafana Image Renderer plugin](https://grafana.com/grafana/plugins/grafana-image-renderer). - **Docker**: Our Ubuntu based images have been upgraded to Ubuntu [20.04 LTS](https://releases.ubuntu.com/20.04/). - **Dashboard**: A global minimum dashboard refresh interval is now enforced and defaults to 5 seconds. -- **@grafana/ui**: Forms migration notice, see [@grafana/ui changelog](https://github.com/grafana/grafana/blob/master/packages/grafana-ui/CHANGELOG.md) -- **@grafana/ui**: Select API change for creating custom values, see [@grafana/ui changelog](https://github.com/grafana/grafana/blob/master/packages/grafana-ui/CHANGELOG.md) +- **@grafana/ui**: Forms migration notice, see [@grafana/ui changelog](https://github.com/grafana/grafana/blob/main/packages/grafana-ui/CHANGELOG.md) +- **@grafana/ui**: Select API change for creating custom values, see [@grafana/ui changelog](https://github.com/grafana/grafana/blob/main/packages/grafana-ui/CHANGELOG.md) - **Interval calculation**: There is now a new option `Max data points` that controls the auto interval `$__interval` calculation. Interval was previously calculated by dividing the panel width by the time range. With the new max data points option it is now easy to set `$__interval` to a dynamic value that is time range agnostic. For example if you set `Max data points` to 10 Grafana will dynamically set `$__interval` by dividing the current time range by 10. - **Datasource/Loki**: Support for [deprecated Loki endpoints](https://github.com/grafana/loki/blob/master/docs/api.md#lokis-http-api) has been removed. diff --git a/docs/sources/developers/http_api/annotations.md b/docs/sources/developers/http_api/annotations.md index 337bb23088..4d58b32a82 100644 --- a/docs/sources/developers/http_api/annotations.md +++ b/docs/sources/developers/http_api/annotations.md @@ -189,7 +189,7 @@ Content-Type: application/json "what": "Event - deploy", "tags": ["deploy", "production"], "when": 1467844481, - "data": "deploy of master branch happened at Wed Jul 6 22:34:41 UTC 2016" + "data": "deploy of main branch happened at Wed Jul 6 22:34:41 UTC 2016" } ``` From b02ae375ba5599fa1e72fb818a1424a8e134efaa Mon Sep 17 00:00:00 2001 From: linoman <2051016+linoman@users.noreply.github.com> Date: Thu, 29 Feb 2024 09:48:32 -0600 Subject: [PATCH 0306/1406] Chore: Query oauth info from a new instance (#83229) * query OAuth info from a new instance * add `hd` validation flag * add `disable_hd_validation` to settings map * update documentation --------- Co-authored-by: Jo --- conf/defaults.ini | 1 + conf/sample.ini | 1 + .../configure-authentication/google/index.md | 8 ++++++ pkg/login/social/connectors/google_oauth.go | 19 ++++++++++--- .../social/connectors/google_oauth_test.go | 10 ++++++- .../ssosettings/strategies/oauth_strategy.go | 1 + .../strategies/oauth_strategy_test.go | 27 +++++++++++++------ 7 files changed, 54 insertions(+), 13 deletions(-) diff --git a/conf/defaults.ini b/conf/defaults.ini index f21b0ea8ee..2d3f108f5b 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -679,6 +679,7 @@ token_url = https://oauth2.googleapis.com/token api_url = https://openidconnect.googleapis.com/v1/userinfo signout_redirect_url = allowed_domains = +validate_hd = true hosted_domain = allowed_groups = role_attribute_path = diff --git a/conf/sample.ini b/conf/sample.ini index e153c17c2a..876585d097 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -643,6 +643,7 @@ ;api_url = https://openidconnect.googleapis.com/v1/userinfo ;signout_redirect_url = ;allowed_domains = +;validate_hd = ;hosted_domain = ;allowed_groups = ;role_attribute_path = diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/google/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/google/index.md index 7d0fe701c1..38c185d606 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/google/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/google/index.md @@ -111,6 +111,14 @@ automatically signed up. You may specify a domain to be passed as `hd` query parameter accepted by Google's OAuth 2.0 authentication API. Refer to Google's OAuth [documentation](https://developers.google.com/identity/openid-connect/openid-connect#hd-param). +{{% admonition type="note" %}} +Since Grafana 10.3.0, the `hd` parameter retrieved from Google ID token is also used to determine the user's hosted domain. The Google Oauth `allowed_domains` configuration option is used to restrict access to users from a specific domain. If the `allowed_domains` configuration option is set, the `hd` parameter from the Google ID token must match the `allowed_domains` configuration option. If the `hd` parameter from the Google ID token does not match the `allowed_domains` configuration option, the user is denied access. + +When an account does not belong to a google workspace, the hd claim will not be available. + +This validation is enabled by default. To disable this validation, set the `validate_hd` configuration option to `false`. The `allowed_domains` configuration option will use the email claim to validate the domain. +{{% /admonition %}} + #### PKCE IETF's [RFC 7636](https://datatracker.ietf.org/doc/html/rfc7636) diff --git a/pkg/login/social/connectors/google_oauth.go b/pkg/login/social/connectors/google_oauth.go index a4404004af..84c6d9f96f 100644 --- a/pkg/login/social/connectors/google_oauth.go +++ b/pkg/login/social/connectors/google_oauth.go @@ -24,13 +24,16 @@ const ( legacyAPIURL = "https://www.googleapis.com/oauth2/v1/userinfo" googleIAMGroupsEndpoint = "https://content-cloudidentity.googleapis.com/v1/groups/-/memberships:searchDirectGroups" googleIAMScope = "https://www.googleapis.com/auth/cloud-identity.groups.readonly" + validateHDKey = "validate_hd" ) var _ social.SocialConnector = (*SocialGoogle)(nil) var _ ssosettings.Reloadable = (*SocialGoogle)(nil) +var ExtraGoogleSettingKeys = []string{validateHDKey} type SocialGoogle struct { *SocialBase + validateHD bool } type googleUserData struct { @@ -45,6 +48,7 @@ type googleUserData struct { func NewGoogleProvider(info *social.OAuthInfo, cfg *setting.Cfg, ssoSettings ssosettings.Service, features featuremgmt.FeatureToggles) *SocialGoogle { provider := &SocialGoogle{ SocialBase: newSocialBase(social.GoogleProviderName, info, features, cfg), + validateHD: MustBool(info.Extra[validateHDKey], true), } if strings.HasPrefix(info.ApiUrl, legacyAPIURL) { @@ -89,6 +93,7 @@ func (s *SocialGoogle) Reload(ctx context.Context, settings ssoModels.SSOSetting defer s.reloadMutex.Unlock() s.updateInfo(social.GoogleProviderName, newInfo) + s.validateHD = MustBool(newInfo.Extra[validateHDKey], false) return nil } @@ -117,7 +122,7 @@ func (s *SocialGoogle) UserInfo(ctx context.Context, client *http.Client, token return nil, fmt.Errorf("user email is not verified") } - if err := s.isHDAllowed(data.HD); err != nil { + if err := s.isHDAllowed(data.HD, info); err != nil { return nil, err } @@ -163,6 +168,7 @@ type googleAPIData struct { Name string `json:"name"` Email string `json:"email"` EmailVerified bool `json:"verified_email"` + HD string `json:"hd"` } func (s *SocialGoogle) extractFromAPI(ctx context.Context, client *http.Client) (*googleUserData, error) { @@ -184,6 +190,7 @@ func (s *SocialGoogle) extractFromAPI(ctx context.Context, client *http.Client) Name: data.Name, Email: data.Email, EmailVerified: data.EmailVerified, + HD: data.HD, rawJSON: response.Body, }, nil } @@ -297,12 +304,16 @@ func (s *SocialGoogle) getGroupsPage(ctx context.Context, client *http.Client, u return &data, nil } -func (s *SocialGoogle) isHDAllowed(hd string) error { - if len(s.info.AllowedDomains) == 0 { +func (s *SocialGoogle) isHDAllowed(hd string, info *social.OAuthInfo) error { + if s.validateHD { + return nil + } + + if len(info.AllowedDomains) == 0 { return nil } - for _, allowedDomain := range s.info.AllowedDomains { + for _, allowedDomain := range info.AllowedDomains { if hd == allowedDomain { return nil } diff --git a/pkg/login/social/connectors/google_oauth_test.go b/pkg/login/social/connectors/google_oauth_test.go index 7fe0d8b024..d822d4d7b8 100644 --- a/pkg/login/social/connectors/google_oauth_test.go +++ b/pkg/login/social/connectors/google_oauth_test.go @@ -897,6 +897,7 @@ func TestIsHDAllowed(t *testing.T) { email string allowedDomains []string expectedErrorMessage string + validateHD bool }{ { name: "should not fail if no allowed domains are set", @@ -916,6 +917,12 @@ func TestIsHDAllowed(t *testing.T) { allowedDomains: []string{"grafana.com", "example.com"}, expectedErrorMessage: "the hd claim found in the ID token is not present in the allowed domains", }, + { + name: "should not fail if the HD validation is disabled and the email not being from an allowed domain", + email: "mycompany.com", + allowedDomains: []string{"grafana.com", "example.com"}, + validateHD: true, + }, } for _, tc := range testCases { @@ -923,7 +930,8 @@ func TestIsHDAllowed(t *testing.T) { info := &social.OAuthInfo{} info.AllowedDomains = tc.allowedDomains s := NewGoogleProvider(info, &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures()) - err := s.isHDAllowed(tc.email) + s.validateHD = tc.validateHD + err := s.isHDAllowed(tc.email, info) if tc.expectedErrorMessage != "" { require.Error(t, err) diff --git a/pkg/services/ssosettings/strategies/oauth_strategy.go b/pkg/services/ssosettings/strategies/oauth_strategy.go index 4dbbfddfbe..7f67ad9c8e 100644 --- a/pkg/services/ssosettings/strategies/oauth_strategy.go +++ b/pkg/services/ssosettings/strategies/oauth_strategy.go @@ -19,6 +19,7 @@ var extraKeysByProvider = map[string][]string{ social.AzureADProviderName: connectors.ExtraAzureADSettingKeys, social.GenericOAuthProviderName: connectors.ExtraGenericOAuthSettingKeys, social.GitHubProviderName: connectors.ExtraGithubSettingKeys, + social.GoogleProviderName: connectors.ExtraGoogleSettingKeys, social.GrafanaComProviderName: connectors.ExtraGrafanaComSettingKeys, social.GrafanaNetProviderName: connectors.ExtraGrafanaComSettingKeys, } diff --git a/pkg/services/ssosettings/strategies/oauth_strategy_test.go b/pkg/services/ssosettings/strategies/oauth_strategy_test.go index b41e14e1b3..3cf7031d7d 100644 --- a/pkg/services/ssosettings/strategies/oauth_strategy_test.go +++ b/pkg/services/ssosettings/strategies/oauth_strategy_test.go @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/require" "gopkg.in/ini.v1" + "github.com/grafana/grafana/pkg/login/social" "github.com/grafana/grafana/pkg/setting" ) @@ -129,6 +130,9 @@ func TestGetProviderConfig_ExtraFields(t *testing.T) { [auth.grafana_com] enabled = true allowed_organizations = org1, org2 + + [auth.google] + validate_hd = true ` iniFile, err := ini.Load([]byte(iniWithExtraFields)) @@ -139,24 +143,24 @@ func TestGetProviderConfig_ExtraFields(t *testing.T) { strategy := NewOAuthStrategy(cfg) - t.Run("azuread", func(t *testing.T) { - result, err := strategy.GetProviderConfig(context.Background(), "azuread") + t.Run(social.AzureADProviderName, func(t *testing.T) { + result, err := strategy.GetProviderConfig(context.Background(), social.AzureADProviderName) require.NoError(t, err) require.Equal(t, "true", result["force_use_graph_api"]) require.Equal(t, "org1, org2", result["allowed_organizations"]) }) - t.Run("github", func(t *testing.T) { - result, err := strategy.GetProviderConfig(context.Background(), "github") + t.Run(social.GitHubProviderName, func(t *testing.T) { + result, err := strategy.GetProviderConfig(context.Background(), social.GitHubProviderName) require.NoError(t, err) require.Equal(t, "first, second", result["team_ids"]) require.Equal(t, "org1, org2", result["allowed_organizations"]) }) - t.Run("generic_oauth", func(t *testing.T) { - result, err := strategy.GetProviderConfig(context.Background(), "generic_oauth") + t.Run(social.GenericOAuthProviderName, func(t *testing.T) { + result, err := strategy.GetProviderConfig(context.Background(), social.GenericOAuthProviderName) require.NoError(t, err) require.Equal(t, "first, second", result["team_ids"]) @@ -166,12 +170,19 @@ func TestGetProviderConfig_ExtraFields(t *testing.T) { require.Equal(t, "id_token", result["id_token_attribute_name"]) }) - t.Run("grafana_com", func(t *testing.T) { - result, err := strategy.GetProviderConfig(context.Background(), "grafana_com") + t.Run(social.GrafanaComProviderName, func(t *testing.T) { + result, err := strategy.GetProviderConfig(context.Background(), social.GrafanaComProviderName) require.NoError(t, err) require.Equal(t, "org1, org2", result["allowed_organizations"]) }) + + t.Run(social.GoogleProviderName, func(t *testing.T) { + result, err := strategy.GetProviderConfig(context.Background(), social.GoogleProviderName) + require.NoError(t, err) + + require.Equal(t, "true", result["validate_hd"]) + }) } // TestGetProviderConfig_GrafanaComGrafanaNet tests that the connector is setup using the correct section and it supports From 29d6cd8fa0dff11b09bde93398228e26abae6b58 Mon Sep 17 00:00:00 2001 From: Ivan Ortega Alba Date: Thu, 29 Feb 2024 17:18:26 +0100 Subject: [PATCH 0307/1406] DashboardScene: Share change detection logic between saving and runtime (#81958) Co-authored-by: Alexandra Vargas Co-authored-by: Dominik Prokop --- .betterer.results | 8 + .../saving/DashboardSceneChangeTracker.ts | 117 +++++++++++++ .../saving/DetectChangesWorker.ts | 10 ++ .../saving/SaveDashboardDrawer.tsx | 4 +- .../__mocks__/createDetectChangesWorker.ts | 19 ++ .../saving/createDetectChangesWorker.ts | 1 + .../saving/getDashboardChanges.ts | 145 ++++++++++++++++ ...s => getDashboardChangesFromScene.test.ts} | 17 +- .../saving/getDashboardChangesFromScene.ts | 14 ++ .../scene/DashboardScene.test.tsx | 163 +++++++++++++++++- .../dashboard-scene/scene/DashboardScene.tsx | 75 ++------ .../transformSceneToSaveModel.ts | 2 +- .../settings/AnnotationsEditView.test.tsx | 15 +- .../settings/DashboardLinksEditView.test.tsx | 15 +- .../settings/DashboardLinksEditView.tsx | 3 +- .../settings/GeneralSettingsEditView.test.tsx | 15 +- .../settings/GeneralSettingsEditView.tsx | 11 +- .../settings/PermissionsEditView.test.tsx | 15 +- .../settings/VariablesEditView.test.tsx | 2 + .../settings/VersionsEditView.test.tsx | 15 +- .../settings/variables/utils.test.ts | 25 +++ .../settings/variables/utils.ts | 23 +++ .../utils/dashboardSceneGraph.test.ts | 27 ++- .../utils/dashboardSceneGraph.ts | 23 ++- .../PanelEditor/getPanelFrameOptions.tsx | 10 +- .../panel/panellinks/linkSuppliers.ts | 2 +- public/test/setupTests.ts | 5 + 27 files changed, 676 insertions(+), 105 deletions(-) create mode 100644 public/app/features/dashboard-scene/saving/DashboardSceneChangeTracker.ts create mode 100644 public/app/features/dashboard-scene/saving/DetectChangesWorker.ts create mode 100644 public/app/features/dashboard-scene/saving/__mocks__/createDetectChangesWorker.ts create mode 100644 public/app/features/dashboard-scene/saving/createDetectChangesWorker.ts create mode 100644 public/app/features/dashboard-scene/saving/getDashboardChanges.ts rename public/app/features/dashboard-scene/saving/{getSaveDashboardChange.test.ts => getDashboardChangesFromScene.test.ts} (84%) create mode 100644 public/app/features/dashboard-scene/saving/getDashboardChangesFromScene.ts diff --git a/.betterer.results b/.betterer.results index 2e2644ad9a..bd9901094f 100644 --- a/.betterer.results +++ b/.betterer.results @@ -2529,10 +2529,18 @@ exports[`better eslint`] = { "public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataPane.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], + "public/app/features/dashboard-scene/saving/DetectChangesWorker.ts:5381": [ + [0, 0, 0, "Unexpected any. Specify a different type.", "0"], + [0, 0, 0, "Unexpected any. Specify a different type.", "1"] + ], "public/app/features/dashboard-scene/saving/SaveDashboardForm.tsx:5381": [ [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"], [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "1"] ], + "public/app/features/dashboard-scene/saving/getDashboardChanges.ts:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"], + [0, 0, 0, "Do not use any type assertions.", "1"] + ], "public/app/features/dashboard-scene/saving/getSaveDashboardChange.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Do not use any type assertions.", "1"] diff --git a/public/app/features/dashboard-scene/saving/DashboardSceneChangeTracker.ts b/public/app/features/dashboard-scene/saving/DashboardSceneChangeTracker.ts new file mode 100644 index 0000000000..a107817007 --- /dev/null +++ b/public/app/features/dashboard-scene/saving/DashboardSceneChangeTracker.ts @@ -0,0 +1,117 @@ +import { Unsubscribable } from 'rxjs'; + +import { + SceneDataLayers, + SceneGridItem, + SceneGridLayout, + SceneObjectStateChangedEvent, + SceneRefreshPicker, + SceneTimeRange, + SceneVariableSet, + behaviors, +} from '@grafana/scenes'; +import { createWorker } from 'app/features/dashboard-scene/saving/createDetectChangesWorker'; + +import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer'; +import { DashboardControls } from '../scene/DashboardControls'; +import { DashboardScene, PERSISTED_PROPS } from '../scene/DashboardScene'; +import { transformSceneToSaveModel } from '../serialization/transformSceneToSaveModel'; +import { isSceneVariableInstance } from '../settings/variables/utils'; + +import { DashboardChangeInfo } from './shared'; + +export class DashboardSceneChangeTracker { + private _changeTrackerSub: Unsubscribable | undefined; + private _changesWorker: Worker; + private _dashboard: DashboardScene; + + constructor(dashboard: DashboardScene) { + this._dashboard = dashboard; + this._changesWorker = createWorker(); + } + + private onStateChanged({ payload }: SceneObjectStateChangedEvent) { + if (payload.changedObject instanceof SceneRefreshPicker) { + if (Object.prototype.hasOwnProperty.call(payload.partialUpdate, 'intervals')) { + this.detectChanges(); + } + } + if (payload.changedObject instanceof behaviors.CursorSync) { + this.detectChanges(); + } + if (payload.changedObject instanceof SceneDataLayers) { + this.detectChanges(); + } + if (payload.changedObject instanceof SceneGridItem) { + this.detectChanges(); + } + if (payload.changedObject instanceof SceneGridLayout) { + this.detectChanges(); + } + if (payload.changedObject instanceof DashboardScene) { + if (Object.keys(payload.partialUpdate).some((key) => PERSISTED_PROPS.includes(key))) { + this.detectChanges(); + } + } + if (payload.changedObject instanceof SceneTimeRange) { + this.detectChanges(); + } + if (payload.changedObject instanceof DashboardControls) { + if (Object.prototype.hasOwnProperty.call(payload.partialUpdate, 'hideTimeControls')) { + this.detectChanges(); + } + } + if (payload.changedObject instanceof SceneVariableSet) { + this.detectChanges(); + } + if (payload.changedObject instanceof DashboardAnnotationsDataLayer) { + if (!Object.prototype.hasOwnProperty.call(payload.partialUpdate, 'data')) { + this.detectChanges(); + } + } + if (isSceneVariableInstance(payload.changedObject)) { + this.detectChanges(); + } + } + + private detectChanges() { + this._changesWorker?.postMessage({ + changed: transformSceneToSaveModel(this._dashboard), + initial: this._dashboard.getInitialSaveModel(), + }); + } + + private updateIsDirty(result: DashboardChangeInfo) { + const { hasChanges } = result; + + if (hasChanges) { + if (!this._dashboard.state.isDirty) { + this._dashboard.setState({ isDirty: true }); + } + } else { + if (this._dashboard.state.isDirty) { + this._dashboard.setState({ isDirty: false }); + } + } + } + + public startTrackingChanges() { + this._changesWorker.onmessage = (e: MessageEvent) => { + this.updateIsDirty(e.data); + }; + + this._changeTrackerSub = this._dashboard.subscribeToEvent( + SceneObjectStateChangedEvent, + this.onStateChanged.bind(this) + ); + } + + public stopTrackingChanges() { + this._changeTrackerSub?.unsubscribe(); + } + + public terminate() { + this.stopTrackingChanges(); + this._changesWorker.terminate(); + } +} diff --git a/public/app/features/dashboard-scene/saving/DetectChangesWorker.ts b/public/app/features/dashboard-scene/saving/DetectChangesWorker.ts new file mode 100644 index 0000000000..4c29daef47 --- /dev/null +++ b/public/app/features/dashboard-scene/saving/DetectChangesWorker.ts @@ -0,0 +1,10 @@ +// Worker is not three shakable, so we should not import the whole loadash library +// eslint-disable-next-line lodash/import-scope +import debounce from 'lodash/debounce'; + +import { getDashboardChanges } from './getDashboardChanges'; + +self.onmessage = debounce((e: MessageEvent<{ initial: any; changed: any }>) => { + const result = getDashboardChanges(e.data.initial, e.data.changed, false, false); + self.postMessage(result); +}, 500); diff --git a/public/app/features/dashboard-scene/saving/SaveDashboardDrawer.tsx b/public/app/features/dashboard-scene/saving/SaveDashboardDrawer.tsx index b9dd8acadd..8c4a83d5ea 100644 --- a/public/app/features/dashboard-scene/saving/SaveDashboardDrawer.tsx +++ b/public/app/features/dashboard-scene/saving/SaveDashboardDrawer.tsx @@ -9,7 +9,7 @@ import { DashboardScene } from '../scene/DashboardScene'; import { SaveDashboardAsForm } from './SaveDashboardAsForm'; import { SaveDashboardForm } from './SaveDashboardForm'; import { SaveProvisionedDashboardForm } from './SaveProvisionedDashboardForm'; -import { getSaveDashboardChange } from './getSaveDashboardChange'; +import { getDashboardChangesFromScene } from './getDashboardChangesFromScene'; interface SaveDashboardDrawerState extends SceneObjectState { dashboardRef: SceneObjectRef; @@ -34,7 +34,7 @@ export class SaveDashboardDrawer extends SceneObjectBase) => { const { showDiff, saveAsCopy, saveTimeRange, saveVariables } = model.useState(); - const changeInfo = getSaveDashboardChange(model.state.dashboardRef.resolve(), saveTimeRange, saveVariables); + const changeInfo = getDashboardChangesFromScene(model.state.dashboardRef.resolve(), saveTimeRange, saveVariables); const { changedSaveModel, initialSaveModel, diffs, diffCount } = changeInfo; const dashboard = model.state.dashboardRef.resolve(); const isProvisioned = dashboard.state.meta.provisioned; diff --git a/public/app/features/dashboard-scene/saving/__mocks__/createDetectChangesWorker.ts b/public/app/features/dashboard-scene/saving/__mocks__/createDetectChangesWorker.ts new file mode 100644 index 0000000000..746a759c55 --- /dev/null +++ b/public/app/features/dashboard-scene/saving/__mocks__/createDetectChangesWorker.ts @@ -0,0 +1,19 @@ +const worker = { + postMessage: jest.fn(), + onmessage: jest.fn(), + terminate: jest.fn(), +}; + +jest.mocked(worker.postMessage).mockImplementation(() => { + worker.onmessage?.({ + data: { + hasChanges: true, + hasTimeChanges: true, + hasVariableValueChanges: true, + }, + } as unknown as MessageEvent); +}); + +const createWorker = () => worker; + +export { createWorker }; diff --git a/public/app/features/dashboard-scene/saving/createDetectChangesWorker.ts b/public/app/features/dashboard-scene/saving/createDetectChangesWorker.ts new file mode 100644 index 0000000000..4f34b2a15d --- /dev/null +++ b/public/app/features/dashboard-scene/saving/createDetectChangesWorker.ts @@ -0,0 +1 @@ +export const createWorker = () => new Worker(new URL('./DetectChangesWorker.ts', import.meta.url)); diff --git a/public/app/features/dashboard-scene/saving/getDashboardChanges.ts b/public/app/features/dashboard-scene/saving/getDashboardChanges.ts new file mode 100644 index 0000000000..c9d2dc7e4b --- /dev/null +++ b/public/app/features/dashboard-scene/saving/getDashboardChanges.ts @@ -0,0 +1,145 @@ +import { compare, Operation } from 'fast-json-patch'; +// @ts-ignore +import jsonMap from 'json-source-map'; +import { flow, get, isEqual, sortBy, tail } from 'lodash'; + +import { AdHocVariableModel, TypedVariableModel } from '@grafana/data'; +import { Dashboard } from '@grafana/schema'; + +export function getDashboardChanges( + initial: Dashboard, + changed: Dashboard, + saveTimeRange?: boolean, + saveVariables?: boolean +) { + const initialSaveModel = initial; + const changedSaveModel = changed; + const hasTimeChanged = getHasTimeChanged(changedSaveModel, initialSaveModel); + const hasVariableValueChanges = applyVariableChanges(changedSaveModel, initialSaveModel, saveVariables); + + if (!saveTimeRange) { + changedSaveModel.time = initialSaveModel.time; + } + + const diff = jsonDiff(initialSaveModel, changedSaveModel); + + let diffCount = 0; + for (const d of Object.values(diff)) { + diffCount += d.length; + } + return { + changedSaveModel, + initialSaveModel, + diffs: diff, + diffCount, + hasChanges: diffCount > 0, + hasTimeChanges: hasTimeChanged, + isNew: changedSaveModel.version === 0, + hasVariableValueChanges, + }; +} + +export function getHasTimeChanged(saveModel: Dashboard, originalSaveModel: Dashboard) { + return saveModel.time?.from !== originalSaveModel.time?.from || saveModel.time?.to !== originalSaveModel.time?.to; +} + +export function applyVariableChanges(saveModel: Dashboard, originalSaveModel: Dashboard, saveVariables?: boolean) { + const originalVariables = originalSaveModel.templating?.list ?? []; + const variablesToSave = saveModel.templating?.list ?? []; + let hasVariableValueChanges = false; + + for (const variable of variablesToSave) { + const original = originalVariables.find(({ name, type }) => name === variable.name && type === variable.type); + + if (!original) { + continue; + } + + // Old schema property that never should be in persisted model + if (original.current && Object.hasOwn(original.current, 'selected')) { + delete original.current.selected; + } + + if (!isEqual(variable.current, original.current)) { + hasVariableValueChanges = true; + } + + if (!saveVariables) { + const typed = variable as TypedVariableModel; + if (typed.type === 'adhoc') { + typed.filters = (original as AdHocVariableModel).filters; + } else { + variable.current = original.current; + variable.options = original.options; + } + } + } + + return hasVariableValueChanges; +} + +export type Diff = { + op: 'add' | 'replace' | 'remove' | 'copy' | 'test' | '_get' | 'move'; + value: unknown; + originalValue: unknown; + path: string[]; + startLineNumber: number; +}; + +export type Diffs = { + [key: string]: Diff[]; +}; + +export type JSONValue = string | Dashboard; + +export const jsonDiff = (lhs: JSONValue, rhs: JSONValue): Diffs => { + const diffs = compare(lhs, rhs); + const lhsMap = jsonMap.stringify(lhs, null, 2); + const rhsMap = jsonMap.stringify(rhs, null, 2); + + const getDiffInformation = (diffs: Operation[]): Diff[] => { + return diffs.map((diff) => { + let originalValue = undefined; + let value = undefined; + let startLineNumber = 0; + + const path = tail(diff.path.split('/')); + + if (diff.op === 'replace' && rhsMap.pointers[diff.path]) { + originalValue = get(lhs, path); + value = diff.value; + startLineNumber = rhsMap.pointers[diff.path].value.line; + } + if (diff.op === 'add' && rhsMap.pointers[diff.path]) { + value = diff.value; + startLineNumber = rhsMap.pointers[diff.path].value.line; + } + if (diff.op === 'remove' && lhsMap.pointers[diff.path]) { + originalValue = get(lhs, path); + startLineNumber = lhsMap.pointers[diff.path].value.line; + } + + return { + op: diff.op, + value, + path, + originalValue, + startLineNumber, + }; + }); + }; + + const sortByLineNumber = (diffs: Diff[]) => sortBy(diffs, 'startLineNumber'); + const groupByPath = (diffs: Diff[]) => + diffs.reduce>((acc, value) => { + const groupKey: string = value.path[0]; + if (!acc[groupKey]) { + acc[groupKey] = []; + } + acc[groupKey].push(value); + return acc; + }, {}); + + // return 1; + return flow([getDiffInformation, sortByLineNumber, groupByPath])(diffs); +}; diff --git a/public/app/features/dashboard-scene/saving/getSaveDashboardChange.test.ts b/public/app/features/dashboard-scene/saving/getDashboardChangesFromScene.test.ts similarity index 84% rename from public/app/features/dashboard-scene/saving/getSaveDashboardChange.test.ts rename to public/app/features/dashboard-scene/saving/getDashboardChangesFromScene.test.ts index 553d2bc068..b1d1b62d6b 100644 --- a/public/app/features/dashboard-scene/saving/getSaveDashboardChange.test.ts +++ b/public/app/features/dashboard-scene/saving/getDashboardChangesFromScene.test.ts @@ -5,12 +5,12 @@ import { transformSaveModelToScene } from '../serialization/transformSaveModelTo import { transformSceneToSaveModel } from '../serialization/transformSceneToSaveModel'; import { findVizPanelByKey } from '../utils/utils'; -import { getSaveDashboardChange } from './getSaveDashboardChange'; +import { getDashboardChangesFromScene } from './getDashboardChangesFromScene'; -describe('getSaveDashboardChange', () => { +describe('getDashboardChangesFromScene', () => { it('Can detect no changes', () => { const dashboard = setup(); - const result = getSaveDashboardChange(dashboard, false); + const result = getDashboardChangesFromScene(dashboard, false); expect(result.hasChanges).toBe(false); expect(result.diffCount).toBe(0); }); @@ -20,7 +20,7 @@ describe('getSaveDashboardChange', () => { sceneGraph.getTimeRange(dashboard).setState({ from: 'now-1h', to: 'now' }); - const result = getSaveDashboardChange(dashboard, false); + const result = getDashboardChangesFromScene(dashboard, false); expect(result.hasChanges).toBe(false); expect(result.diffCount).toBe(0); expect(result.hasTimeChanges).toBe(true); @@ -31,7 +31,7 @@ describe('getSaveDashboardChange', () => { sceneGraph.getTimeRange(dashboard).setState({ from: 'now-1h', to: 'now' }); - const result = getSaveDashboardChange(dashboard, true); + const result = getDashboardChangesFromScene(dashboard, true); expect(result.hasChanges).toBe(true); expect(result.diffCount).toBe(1); }); @@ -42,7 +42,7 @@ describe('getSaveDashboardChange', () => { const appVar = sceneGraph.lookupVariable('app', dashboard) as MultiValueVariable; appVar.changeValueTo('app2'); - const result = getSaveDashboardChange(dashboard, false, false); + const result = getDashboardChangesFromScene(dashboard, false, false); expect(result.hasVariableValueChanges).toBe(true); expect(result.hasChanges).toBe(false); @@ -55,7 +55,7 @@ describe('getSaveDashboardChange', () => { const appVar = sceneGraph.lookupVariable('app', dashboard) as MultiValueVariable; appVar.changeValueTo('app2'); - const result = getSaveDashboardChange(dashboard, false, true); + const result = getDashboardChangesFromScene(dashboard, false, true); expect(result.hasVariableValueChanges).toBe(true); expect(result.hasChanges).toBe(true); @@ -72,8 +72,9 @@ describe('getSaveDashboardChange', () => { dashboard.setState({ editPanel: editScene }); editScene.state.vizManager.state.panel.setState({ title: 'changed title' }); + editScene.commitChanges(); - const result = getSaveDashboardChange(dashboard, false, true); + const result = getDashboardChangesFromScene(dashboard, false, true); const panelSaveModel = result.changedSaveModel.panels![0]; expect(panelSaveModel.title).toBe('changed title'); }); diff --git a/public/app/features/dashboard-scene/saving/getDashboardChangesFromScene.ts b/public/app/features/dashboard-scene/saving/getDashboardChangesFromScene.ts new file mode 100644 index 0000000000..4859fe77f4 --- /dev/null +++ b/public/app/features/dashboard-scene/saving/getDashboardChangesFromScene.ts @@ -0,0 +1,14 @@ +import { DashboardScene } from '../scene/DashboardScene'; +import { transformSceneToSaveModel } from '../serialization/transformSceneToSaveModel'; + +import { getDashboardChanges } from './getDashboardChanges'; + +export function getDashboardChangesFromScene(scene: DashboardScene, saveTimeRange?: boolean, saveVariables?: boolean) { + const changeInfo = getDashboardChanges( + scene.getInitialSaveModel()!, + transformSceneToSaveModel(scene), + saveTimeRange, + saveVariables + ); + return changeInfo; +} diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx index 0b13565722..55e7062d1a 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx @@ -9,12 +9,14 @@ import { TestVariable, VizPanel, SceneGridRow, + behaviors, } from '@grafana/scenes'; -import { Dashboard } from '@grafana/schema'; +import { Dashboard, DashboardCursorSync } from '@grafana/schema'; import appEvents from 'app/core/app_events'; import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; import { VariablesChanged } from 'app/features/variables/types'; +import { createWorker } from '../saving/createDetectChangesWorker'; import { buildGridItemForPanel, transformSaveModelToScene } from '../serialization/transformSaveModelToScene'; import { DecoratedRevisionModel } from '../settings/VersionsEditView'; import { historySrv } from '../settings/version-history/HistorySrv'; @@ -26,6 +28,20 @@ import { DashboardScene, DashboardSceneState } from './DashboardScene'; jest.mock('../settings/version-history/HistorySrv'); jest.mock('../serialization/transformSaveModelToScene'); +jest.mock('../saving/getDashboardChangesFromScene', () => ({ + // It compares the initial and changed save models and returns the differences + // By default we assume there are differences to have the dirty state test logic tested + getDashboardChangesFromScene: jest.fn(() => ({ + changedSaveModel: {}, + initialSaveModel: {}, + diffs: [], + diffCount: 0, + hasChanges: true, + hasTimeChanges: false, + isNew: false, + hasVariableValueChanges: false, + })), +})); jest.mock('../serialization/transformSceneToSaveModel'); jest.mock('@grafana/runtime', () => ({ ...jest.requireActual('@grafana/runtime'), @@ -36,6 +52,9 @@ jest.mock('@grafana/runtime', () => ({ }, })); +const worker = createWorker(); +mockResultsOfDetectChangesWorker({ hasChanges: true, hasTimeChanges: false, hasVariableValueChanges: false }); + describe('DashboardScene', () => { describe('DashboardSrv.getCurrent compatibility', () => { it('Should set to compatibility wrapper', () => { @@ -49,16 +68,29 @@ describe('DashboardScene', () => { describe('Editing and discarding', () => { describe('Given scene in edit mode', () => { let scene: DashboardScene; + let deactivateScene: () => void; beforeEach(() => { scene = buildTestScene(); + deactivateScene = scene.activate(); scene.onEnterEditMode(); + jest.clearAllMocks(); }); it('Should set isEditing to true', () => { expect(scene.state.isEditing).toBe(true); }); + it('Should start the detect changes worker', () => { + expect(worker.onmessage).toBeDefined(); + }); + + it('Should terminate the detect changes worker when deactivate', () => { + expect(worker.terminate).toHaveBeenCalledTimes(0); + deactivateScene(); + expect(worker.terminate).toHaveBeenCalledTimes(1); + }); + it('A change to griditem pos should set isDirty true', () => { const gridItem = sceneGraph.findObject(scene, (p) => p.state.key === 'griditem-1') as SceneGridItem; gridItem.setState({ x: 10, y: 0, width: 10, height: 10 }); @@ -70,6 +102,22 @@ describe('DashboardScene', () => { expect(gridItem2.state.x).toBe(0); }); + it('A change to gridlayout children order should set isDirty true', () => { + const layout = sceneGraph.findObject(scene, (p) => p instanceof SceneGridLayout) as SceneGridLayout; + const originalPanelOrder = layout.state.children.map((c) => c.state.key); + + // Change the order of the children. This happen when panels move around, then the children are re-ordered + layout.setState({ + children: [layout.state.children[1], layout.state.children[0], layout.state.children[2]], + }); + + expect(scene.state.isDirty).toBe(true); + + scene.exitEditMode({ skipConfirm: true }); + const resoredLayout = sceneGraph.findObject(scene, (p) => p instanceof SceneGridLayout) as SceneGridLayout; + expect(resoredLayout.state.children.map((c) => c.state.key)).toEqual(originalPanelOrder); + }); + it.each` prop | value ${'title'} | ${'new title'} @@ -77,6 +125,7 @@ describe('DashboardScene', () => { ${'tags'} | ${['tag3', 'tag4']} ${'editable'} | ${false} ${'links'} | ${[]} + ${'meta'} | ${{ folderUid: 'new-folder-uid', folderTitle: 'new-folder-title', hasUnsavedFolderChange: true }} `( 'A change to $prop should set isDirty true', ({ prop, value }: { prop: keyof DashboardSceneState; value: unknown }) => { @@ -123,6 +172,40 @@ describe('DashboardScene', () => { expect(sceneGraph.getTimeRange(scene)!.state.timeZone).toBe(prevState); }); + it('A change to a cursor sync config should set isDirty true', () => { + const cursorSync = dashboardSceneGraph.getCursorSync(scene)!; + const initialState = cursorSync.state; + + cursorSync.setState({ + sync: DashboardCursorSync.Tooltip, + }); + + expect(scene.state.isDirty).toBe(true); + + scene.exitEditMode({ skipConfirm: true }); + expect(dashboardSceneGraph.getCursorSync(scene)!.state).toEqual(initialState); + }); + + it.each([ + { hasChanges: true, hasTimeChanges: false, hasVariableValueChanges: false }, + { hasChanges: true, hasTimeChanges: true, hasVariableValueChanges: false }, + { hasChanges: true, hasTimeChanges: false, hasVariableValueChanges: true }, + ])('should set the state to true if there are changes detected in the saving model', (diffResults) => { + mockResultsOfDetectChangesWorker(diffResults); + scene.setState({ title: 'hello' }); + expect(scene.state.isDirty).toBeTruthy(); + }); + + it.each([ + { hasChanges: false, hasTimeChanges: false, hasVariableValueChanges: false }, + { hasChanges: false, hasTimeChanges: true, hasVariableValueChanges: false }, + { hasChanges: false, hasTimeChanges: false, hasVariableValueChanges: true }, + ])('should not set the state to true if there are no change detected in the dashboard', (diffResults) => { + mockResultsOfDetectChangesWorker(diffResults); + scene.setState({ title: 'hello' }); + expect(scene.state.isDirty).toBeFalsy(); + }); + it('Should throw an error when adding a panel to a layout that is not SceneGridLayout', () => { const scene = buildTestScene({ body: undefined }); @@ -304,7 +387,11 @@ describe('DashboardScene', () => { }); describe('When variables change', () => { - it('A change to griditem pos should set isDirty true', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('A change to variable values should trigger VariablesChanged event', () => { const varA = new TestVariable({ name: 'A', query: 'A.*', value: 'A.AA', text: '', options: [], delayMs: 0 }); const scene = buildTestScene({ $variables: new SceneVariableSet({ variables: [varA] }), @@ -319,6 +406,57 @@ describe('DashboardScene', () => { expect(eventHandler).toHaveBeenCalledTimes(1); }); + + it('A change to the variable set should set isDirty true', () => { + const varA = new TestVariable({ name: 'A', query: 'A.*', value: 'A.AA', text: '', options: [], delayMs: 0 }); + const scene = buildTestScene({ + $variables: new SceneVariableSet({ variables: [varA] }), + }); + + scene.activate(); + scene.onEnterEditMode(); + + const variableSet = sceneGraph.getVariables(scene); + variableSet.setState({ variables: [] }); + + expect(scene.state.isDirty).toBe(true); + }); + + it('A change to a variable state should set isDirty true', () => { + mockResultsOfDetectChangesWorker({ hasChanges: true, hasTimeChanges: false, hasVariableValueChanges: true }); + const variable = new TestVariable({ name: 'A' }); + const scene = buildTestScene({ + $variables: new SceneVariableSet({ variables: [variable] }), + }); + + scene.activate(); + scene.onEnterEditMode(); + + variable.setState({ name: 'new-name' }); + + expect(variable.state.name).toBe('new-name'); + expect(scene.state.isDirty).toBe(true); + }); + + it('A change to variable name is restored to original name should set isDirty back to false', () => { + const variable = new TestVariable({ name: 'A' }); + const scene = buildTestScene({ + $variables: new SceneVariableSet({ variables: [variable] }), + }); + + scene.activate(); + scene.onEnterEditMode(); + + mockResultsOfDetectChangesWorker({ hasChanges: true, hasTimeChanges: false, hasVariableValueChanges: false }); + variable.setState({ name: 'B' }); + expect(scene.state.isDirty).toBe(true); + mockResultsOfDetectChangesWorker( + // No changes, it is the same name than before comparing saving models + { hasChanges: false, hasTimeChanges: false, hasVariableValueChanges: false } + ); + variable.setState({ name: 'A' }); + expect(scene.state.isDirty).toBe(false); + }); }); describe('When a dashboard is restored', () => { @@ -379,6 +517,7 @@ function buildTestScene(overrides?: Partial) { timeZone: 'browser', }), controls: new DashboardControls({}), + $behaviors: [new behaviors.CursorSync({})], body: new SceneGridLayout({ children: [ new SceneGridItem({ @@ -427,6 +566,26 @@ function buildTestScene(overrides?: Partial) { return scene; } +function mockResultsOfDetectChangesWorker({ + hasChanges, + hasTimeChanges, + hasVariableValueChanges, +}: { + hasChanges: boolean; + hasTimeChanges: boolean; + hasVariableValueChanges: boolean; +}) { + jest.mocked(worker.postMessage).mockImplementationOnce(() => { + worker.onmessage?.({ + data: { + hasChanges: hasChanges ?? true, + hasTimeChanges: hasTimeChanges ?? true, + hasVariableValueChanges: hasVariableValueChanges ?? true, + }, + } as unknown as MessageEvent); + }); +} + function getVersionMock(): DecoratedRevisionModel { const dash: Dashboard = { title: 'new name', diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index a1a4059e09..51c6eec0c0 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -1,12 +1,9 @@ import * as H from 'history'; -import { Unsubscribable } from 'rxjs'; import { AppEvents, CoreApp, DataQueryRequest, NavIndex, NavModelItem, locationUtil } from '@grafana/data'; import { locationService } from '@grafana/runtime'; import { - dataLayers, getUrlSyncManager, - SceneDataLayers, SceneFlexLayout, sceneGraph, SceneGridItem, @@ -15,9 +12,6 @@ import { SceneObject, SceneObjectBase, SceneObjectState, - SceneObjectStateChangedEvent, - SceneRefreshPicker, - SceneTimeRange, sceneUtils, SceneVariable, SceneVariableDependencyConfigLike, @@ -36,6 +30,7 @@ import { DashboardDTO, DashboardMeta, SaveDashboardResponseDTO } from 'app/types import { ShowConfirmModalEvent } from 'app/types/events'; import { PanelEditor } from '../panel-edit/PanelEditor'; +import { DashboardSceneChangeTracker } from '../saving/DashboardSceneChangeTracker'; import { SaveDashboardDrawer } from '../saving/SaveDashboardDrawer'; import { DashboardSceneRenderer } from '../scene/DashboardSceneRenderer'; import { buildGridItemForPanel, transformSaveModelToScene } from '../serialization/transformSaveModelToScene'; @@ -66,7 +61,7 @@ import { PanelRepeaterGridItem } from './PanelRepeaterGridItem'; import { ViewPanelScene } from './ViewPanelScene'; import { setupKeyboardShortcuts } from './keyboardShortcuts'; -export const PERSISTED_PROPS = ['title', 'description', 'tags', 'editable', 'graphTooltip', 'links']; +export const PERSISTED_PROPS = ['title', 'description', 'tags', 'editable', 'graphTooltip', 'links', 'meta']; export interface DashboardSceneState extends SceneObjectState { /** The title */ @@ -138,9 +133,9 @@ export class DashboardScene extends SceneObjectBase { */ private _initialUrlState?: H.Location; /** - * change tracking subscription + * Dashboard changes tracker */ - private _changeTrackerSub?: Unsubscribable; + private _changeTracker: DashboardSceneChangeTracker; public constructor(state: Partial) { super({ @@ -153,6 +148,8 @@ export class DashboardScene extends SceneObjectBase { ...state, }); + this._changeTracker = new DashboardSceneChangeTracker(this); + this.addActivationHandler(() => this._activationHandler()); } @@ -162,7 +159,7 @@ export class DashboardScene extends SceneObjectBase { window.__grafanaSceneContext = this; if (this.state.isEditing) { - this.startTrackingChanges(); + this._changeTracker.startTrackingChanges(); } if (!this.state.meta.isEmbedded && this.state.uid) { @@ -179,7 +176,7 @@ export class DashboardScene extends SceneObjectBase { return () => { window.__grafanaSceneContext = prevSceneContext; clearKeyBindings(); - this.stopTrackingChanges(); + this._changeTracker.terminate(); this.stopUrlSync(); oldDashboardWrapper.destroy(); dashboardWatcher.leave(); @@ -206,7 +203,8 @@ export class DashboardScene extends SceneObjectBase { // Propagate change edit mode change to children this.propagateEditModeChange(); - this.startTrackingChanges(); + + this._changeTracker.startTrackingChanges(); }; public saveCompleted(saveModel: Dashboard, result: SaveDashboardResponseDTO, folderUid?: string) { @@ -217,7 +215,7 @@ export class DashboardScene extends SceneObjectBase { version: result.version, }; - this.stopTrackingChanges(); + this._changeTracker.stopTrackingChanges(); this.setState({ version: result.version, isDirty: false, @@ -231,7 +229,7 @@ export class DashboardScene extends SceneObjectBase { folderUid: folderUid, }, }); - this.startTrackingChanges(); + this._changeTracker.startTrackingChanges(); } private propagateEditModeChange() { @@ -265,7 +263,7 @@ export class DashboardScene extends SceneObjectBase { private exitEditModeConfirmed() { // No need to listen to changes anymore - this.stopTrackingChanges(); + this._changeTracker.stopTrackingChanges(); // Stop url sync before updating url this.stopUrlSync(); @@ -380,53 +378,6 @@ export class DashboardScene extends SceneObjectBase { return this.state.viewPanelScene ?? this.state.body; } - private startTrackingChanges() { - this._changeTrackerSub = this.subscribeToEvent( - SceneObjectStateChangedEvent, - (event: SceneObjectStateChangedEvent) => { - if (event.payload.changedObject instanceof SceneRefreshPicker) { - if (Object.prototype.hasOwnProperty.call(event.payload.partialUpdate, 'intervals')) { - this.setIsDirty(); - } - } - if (event.payload.changedObject instanceof SceneDataLayers) { - this.setIsDirty(); - } - if (event.payload.changedObject instanceof dataLayers.AnnotationsDataLayer) { - if (!Object.prototype.hasOwnProperty.call(event.payload.partialUpdate, 'data')) { - this.setIsDirty(); - } - } - if (event.payload.changedObject instanceof SceneGridItem) { - this.setIsDirty(); - } - if (event.payload.changedObject instanceof DashboardScene) { - if (Object.keys(event.payload.partialUpdate).some((key) => PERSISTED_PROPS.includes(key))) { - this.setIsDirty(); - } - } - if (event.payload.changedObject instanceof SceneTimeRange) { - this.setIsDirty(); - } - if (event.payload.changedObject instanceof DashboardControls) { - if (Object.prototype.hasOwnProperty.call(event.payload.partialUpdate, 'hideTimeControls')) { - this.setIsDirty(); - } - } - } - ); - } - - private setIsDirty() { - if (!this.state.isDirty) { - this.setState({ isDirty: true }); - } - } - - private stopTrackingChanges() { - this._changeTrackerSub?.unsubscribe(); - } - public getInitialState(): DashboardSceneState | undefined { return this._initialState; } diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts index 12028b43cd..7fd73757aa 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts @@ -244,7 +244,7 @@ export function vizPanelToPanel( } const panelLinks = dashboardSceneGraph.getPanelLinks(vizPanel); - panel.links = (panelLinks.state.rawLinks as DashboardLink[]) ?? []; + panel.links = (panelLinks?.state.rawLinks as DashboardLink[]) ?? []; if (panel.links.length === 0) { delete panel.links; diff --git a/public/app/features/dashboard-scene/settings/AnnotationsEditView.test.tsx b/public/app/features/dashboard-scene/settings/AnnotationsEditView.test.tsx index 0ed532e2f7..d16f598b59 100644 --- a/public/app/features/dashboard-scene/settings/AnnotationsEditView.test.tsx +++ b/public/app/features/dashboard-scene/settings/AnnotationsEditView.test.tsx @@ -1,7 +1,9 @@ import { map, of } from 'rxjs'; import { AnnotationQuery, DataQueryRequest, DataSourceApi, LoadingState, PanelData } from '@grafana/data'; -import { SceneDataLayers, SceneGridItem, SceneGridLayout, SceneTimeRange, dataLayers } from '@grafana/scenes'; +import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks'; +import { setPluginImportUtils } from '@grafana/runtime'; +import { SceneDataLayers, SceneGridItem, SceneGridLayout, SceneTimeRange, VizPanel, dataLayers } from '@grafana/scenes'; import { AlertStatesDataLayer } from '../scene/AlertStatesDataLayer'; import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer'; @@ -44,6 +46,11 @@ jest.mock('@grafana/runtime', () => ({ }, })); +setPluginImportUtils({ + importPanelPlugin: (id: string) => Promise.resolve(getPanelPlugin({})), + getPanelPluginFromCache: (id: string) => undefined, +}); + describe('AnnotationsEditView', () => { describe('Dashboard annotations state', () => { let annotationsView: AnnotationsEditView; @@ -188,7 +195,11 @@ async function buildTestScene() { y: 0, width: 10, height: 12, - body: undefined, + body: new VizPanel({ + title: 'Panel A', + key: 'panel-1', + pluginId: 'table', + }), }), ], }), diff --git a/public/app/features/dashboard-scene/settings/DashboardLinksEditView.test.tsx b/public/app/features/dashboard-scene/settings/DashboardLinksEditView.test.tsx index e9e04c6a70..508630a256 100644 --- a/public/app/features/dashboard-scene/settings/DashboardLinksEditView.test.tsx +++ b/public/app/features/dashboard-scene/settings/DashboardLinksEditView.test.tsx @@ -2,7 +2,9 @@ import { render as RTLRender } from '@testing-library/react'; import React from 'react'; import { TestProvider } from 'test/helpers/TestProvider'; -import { behaviors, SceneGridLayout, SceneGridItem, SceneTimeRange } from '@grafana/scenes'; +import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks'; +import { setPluginImportUtils } from '@grafana/runtime'; +import { SceneGridItem, SceneGridLayout, SceneTimeRange, VizPanel, behaviors } from '@grafana/scenes'; import { DashboardCursorSync } from '@grafana/schema'; import { DashboardControls } from '../scene/DashboardControls'; @@ -23,6 +25,11 @@ jest.mock('react-router-dom', () => ({ }), })); +setPluginImportUtils({ + importPanelPlugin: (id: string) => Promise.resolve(getPanelPlugin({})), + getPanelPluginFromCache: (id: string) => undefined, +}); + function render(component: React.ReactNode) { return RTLRender({component}); } @@ -231,7 +238,11 @@ async function buildTestScene() { y: 0, width: 10, height: 12, - body: undefined, + body: new VizPanel({ + title: 'Panel A', + key: 'panel-1', + pluginId: 'table', + }), }), ], }), diff --git a/public/app/features/dashboard-scene/settings/DashboardLinksEditView.tsx b/public/app/features/dashboard-scene/settings/DashboardLinksEditView.tsx index eb3c8ecd01..91d8fef4c8 100644 --- a/public/app/features/dashboard-scene/settings/DashboardLinksEditView.tsx +++ b/public/app/features/dashboard-scene/settings/DashboardLinksEditView.tsx @@ -79,7 +79,7 @@ export class DashboardLinksEditView extends SceneObjectBase) { const { editIndex } = model.useState(); const dashboard = getDashboardSceneFor(model); - const { links, overlay } = dashboard.useState(); + const { links } = dashboard.useState(); const { navModel, pageNav } = useDashboardEditPageNav(dashboard, model.getUrlKey()); const linkToEdit = editIndex !== undefined ? links[editIndex] : undefined; @@ -107,7 +107,6 @@ function DashboardLinksEditViewRenderer({ model }: SceneComponentProps - {overlay && } ); } diff --git a/public/app/features/dashboard-scene/settings/GeneralSettingsEditView.test.tsx b/public/app/features/dashboard-scene/settings/GeneralSettingsEditView.test.tsx index 4becbbb2c7..d438483300 100644 --- a/public/app/features/dashboard-scene/settings/GeneralSettingsEditView.test.tsx +++ b/public/app/features/dashboard-scene/settings/GeneralSettingsEditView.test.tsx @@ -1,4 +1,6 @@ -import { behaviors, SceneGridLayout, SceneGridItem, SceneTimeRange } from '@grafana/scenes'; +import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks'; +import { setPluginImportUtils } from '@grafana/runtime'; +import { behaviors, SceneGridLayout, SceneGridItem, SceneTimeRange, VizPanel } from '@grafana/scenes'; import { DashboardCursorSync } from '@grafana/schema'; import { DashboardControls } from '../scene/DashboardControls'; @@ -7,6 +9,11 @@ import { activateFullSceneTree } from '../utils/test-utils'; import { GeneralSettingsEditView } from './GeneralSettingsEditView'; +setPluginImportUtils({ + importPanelPlugin: (id: string) => Promise.resolve(getPanelPlugin({})), + getPanelPluginFromCache: (id: string) => undefined, +}); + describe('GeneralSettingsEditView', () => { describe('Dashboard state', () => { let dashboard: DashboardScene; @@ -129,7 +136,11 @@ async function buildTestScene() { y: 0, width: 10, height: 12, - body: undefined, + body: new VizPanel({ + title: 'Panel A', + key: 'panel-1', + pluginId: 'table', + }), }), ], }), diff --git a/public/app/features/dashboard-scene/settings/GeneralSettingsEditView.tsx b/public/app/features/dashboard-scene/settings/GeneralSettingsEditView.tsx index 3c1315b0a7..1599ae3dd4 100644 --- a/public/app/features/dashboard-scene/settings/GeneralSettingsEditView.tsx +++ b/public/app/features/dashboard-scene/settings/GeneralSettingsEditView.tsx @@ -1,7 +1,7 @@ import React, { ChangeEvent } from 'react'; import { PageLayoutType } from '@grafana/data'; -import { behaviors, SceneComponentProps, SceneObjectBase, sceneGraph } from '@grafana/scenes'; +import { SceneComponentProps, SceneObjectBase, sceneGraph } from '@grafana/scenes'; import { TimeZone } from '@grafana/schema'; import { Box, @@ -22,6 +22,7 @@ import { DeleteDashboardButton } from 'app/features/dashboard/components/DeleteD import { DashboardScene } from '../scene/DashboardScene'; import { NavToolbarActions } from '../scene/NavToolbarActions'; +import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; import { getDashboardSceneFor } from '../utils/utils'; import { DashboardEditView, DashboardEditViewState, useDashboardEditPageNav } from './utils'; @@ -64,13 +65,7 @@ export class GeneralSettingsEditView } public getCursorSync() { - const cursorSync = this._dashboard.state.$behaviors?.find((b) => b instanceof behaviors.CursorSync); - - if (cursorSync instanceof behaviors.CursorSync) { - return cursorSync; - } - - return; + return dashboardSceneGraph.getCursorSync(this._dashboard); } public getDashboardControls() { diff --git a/public/app/features/dashboard-scene/settings/PermissionsEditView.test.tsx b/public/app/features/dashboard-scene/settings/PermissionsEditView.test.tsx index 3e0e84ac76..45bee212ab 100644 --- a/public/app/features/dashboard-scene/settings/PermissionsEditView.test.tsx +++ b/public/app/features/dashboard-scene/settings/PermissionsEditView.test.tsx @@ -1,10 +1,17 @@ -import { SceneGridItem, SceneGridLayout, SceneTimeRange } from '@grafana/scenes'; +import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks'; +import { setPluginImportUtils } from '@grafana/runtime'; +import { SceneGridItem, SceneGridLayout, SceneTimeRange, VizPanel } from '@grafana/scenes'; import { DashboardScene } from '../scene/DashboardScene'; import { activateFullSceneTree } from '../utils/test-utils'; import { PermissionsEditView } from './PermissionsEditView'; +setPluginImportUtils({ + importPanelPlugin: (id: string) => Promise.resolve(getPanelPlugin({})), + getPanelPluginFromCache: (id: string) => undefined, +}); + describe('PermissionsEditView', () => { describe('Dashboard permissions state', () => { let dashboard: DashboardScene; @@ -44,7 +51,11 @@ async function buildTestScene() { y: 0, width: 10, height: 12, - body: undefined, + body: new VizPanel({ + title: 'Panel A', + key: 'panel-1', + pluginId: 'table', + }), }), ], }), diff --git a/public/app/features/dashboard-scene/settings/VariablesEditView.test.tsx b/public/app/features/dashboard-scene/settings/VariablesEditView.test.tsx index 02d0232e89..7ecc3ee306 100644 --- a/public/app/features/dashboard-scene/settings/VariablesEditView.test.tsx +++ b/public/app/features/dashboard-scene/settings/VariablesEditView.test.tsx @@ -18,6 +18,7 @@ import { VizPanel, AdHocFiltersVariable, SceneVariableState, + SceneTimeRange, } from '@grafana/scenes'; import { mockDataSource } from 'app/features/alerting/unified/mocks'; import { LegacyVariableQueryEditor } from 'app/features/variables/editor/LegacyVariableQueryEditor'; @@ -308,6 +309,7 @@ async function buildTestScene() { meta: { canEdit: true, }, + $timeRange: new SceneTimeRange({}), $variables: new SceneVariableSet({ variables: [ new CustomVariable({ diff --git a/public/app/features/dashboard-scene/settings/VersionsEditView.test.tsx b/public/app/features/dashboard-scene/settings/VersionsEditView.test.tsx index afafdc5089..ee702bd8d6 100644 --- a/public/app/features/dashboard-scene/settings/VersionsEditView.test.tsx +++ b/public/app/features/dashboard-scene/settings/VersionsEditView.test.tsx @@ -1,4 +1,6 @@ -import { SceneGridItem, SceneGridLayout, SceneTimeRange } from '@grafana/scenes'; +import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks'; +import { setPluginImportUtils } from '@grafana/runtime'; +import { SceneGridItem, SceneGridLayout, SceneTimeRange, VizPanel } from '@grafana/scenes'; import { DashboardScene } from '../scene/DashboardScene'; import { activateFullSceneTree } from '../utils/test-utils'; @@ -8,6 +10,11 @@ import { historySrv } from './version-history'; jest.mock('./version-history/HistorySrv'); +setPluginImportUtils({ + importPanelPlugin: (id: string) => Promise.resolve(getPanelPlugin({})), + getPanelPluginFromCache: (id: string) => undefined, +}); + describe('VersionsEditView', () => { describe('Dashboard versions state', () => { let dashboard: DashboardScene; @@ -170,7 +177,11 @@ async function buildTestScene() { y: 0, width: 10, height: 12, - body: undefined, + body: new VizPanel({ + title: 'Panel A', + key: 'panel-1', + pluginId: 'table', + }), }), ], }), diff --git a/public/app/features/dashboard-scene/settings/variables/utils.test.ts b/public/app/features/dashboard-scene/settings/variables/utils.test.ts index 64a14b84fc..74c19faacd 100644 --- a/public/app/features/dashboard-scene/settings/variables/utils.test.ts +++ b/public/app/features/dashboard-scene/settings/variables/utils.test.ts @@ -36,6 +36,7 @@ import { getOptionDataSourceTypes, getNextAvailableId, getVariableDefault, + isSceneVariableInstance, } from './utils'; const templateSrv = { @@ -98,6 +99,30 @@ describe('isEditableVariableType', () => { }); }); +describe('isSceneVariableInstance', () => { + it.each([ + CustomVariable, + QueryVariable, + ConstantVariable, + IntervalVariable, + DataSourceVariable, + AdHocFiltersVariable, + GroupByVariable, + TextBoxVariable, + ])('should return true for scene variable instances %s', (instanceType) => { + const variable = new instanceType({ name: 'MyVariable' }); + expect(isSceneVariableInstance(variable)).toBe(true); + }); + + it('should return false for non-scene variable instances', () => { + const variable = { + name: 'MyVariable', + type: 'query', + }; + expect(variable).not.toBeInstanceOf(QueryVariable); + }); +}); + describe('getVariableTypeSelectOptions', () => { describe('when groupByVariable is enabled', () => { beforeAll(() => { diff --git a/public/app/features/dashboard-scene/settings/variables/utils.ts b/public/app/features/dashboard-scene/settings/variables/utils.ts index cb484b3416..221387b87d 100644 --- a/public/app/features/dashboard-scene/settings/variables/utils.ts +++ b/public/app/features/dashboard-scene/settings/variables/utils.ts @@ -12,6 +12,8 @@ import { GroupByVariable, SceneVariable, MultiValueVariable, + sceneUtils, + SceneObject, AdHocFiltersVariable, SceneVariableState, } from '@grafana/scenes'; @@ -196,5 +198,26 @@ export function getOptionDataSourceTypes() { return optionTypes; } +function isSceneVariable(sceneObject: SceneObject): sceneObject is SceneVariable { + return 'type' in sceneObject.state && 'getValue' in sceneObject; +} + +export function isSceneVariableInstance(sceneObject: SceneObject): sceneObject is SceneVariable { + if (!isSceneVariable(sceneObject)) { + return false; + } + + return ( + sceneUtils.isAdHocVariable(sceneObject) || + sceneUtils.isConstantVariable(sceneObject) || + sceneUtils.isCustomVariable(sceneObject) || + sceneUtils.isDataSourceVariable(sceneObject) || + sceneUtils.isIntervalVariable(sceneObject) || + sceneUtils.isQueryVariable(sceneObject) || + sceneUtils.isTextBoxVariable(sceneObject) || + sceneUtils.isGroupByVariable(sceneObject) + ); +} + export const RESERVED_GLOBAL_VARIABLE_NAME_REGEX = /^(?!__).*$/; export const WORD_CHARACTERS_REGEX = /^\w+$/; diff --git a/public/app/features/dashboard-scene/utils/dashboardSceneGraph.test.ts b/public/app/features/dashboard-scene/utils/dashboardSceneGraph.test.ts index a799efe2c3..ea529d79c1 100644 --- a/public/app/features/dashboard-scene/utils/dashboardSceneGraph.test.ts +++ b/public/app/features/dashboard-scene/utils/dashboardSceneGraph.test.ts @@ -6,7 +6,9 @@ import { SceneQueryRunner, SceneTimeRange, VizPanel, + behaviors, } from '@grafana/scenes'; +import { DashboardCursorSync } from '@grafana/schema'; import { AlertStatesDataLayer } from '../scene/AlertStatesDataLayer'; import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer'; @@ -20,10 +22,10 @@ import { findVizPanelByKey } from './utils'; describe('dashboardSceneGraph', () => { describe('getPanelLinks', () => { - it('should throw if no links object defined', () => { + it('should return null if no links object defined', () => { const scene = buildTestScene(); const panelWithNoLinks = findVizPanelByKey(scene, 'panel-1')!; - expect(() => dashboardSceneGraph.getPanelLinks(panelWithNoLinks)).toThrow(); + expect(dashboardSceneGraph.getPanelLinks(panelWithNoLinks)).toBeNull(); }); it('should resolve VizPanelLinks object', () => { @@ -199,6 +201,22 @@ describe('dashboardSceneGraph', () => { expect(() => getNextPanelId(scene)).toThrow('Dashboard body is not a SceneGridLayout'); }); }); + + describe('getCursorSync', () => { + it('should return cursor sync behavior', () => { + const scene = buildTestScene(); + const cursorSync = dashboardSceneGraph.getCursorSync(scene); + + expect(cursorSync).toBeInstanceOf(behaviors.CursorSync); + }); + + it('should return undefined if no cursor sync behavior', () => { + const scene = buildTestScene({ $behaviors: [] }); + const cursorSync = dashboardSceneGraph.getCursorSync(scene); + + expect(cursorSync).toBeUndefined(); + }); + }); }); function buildTestScene(overrides?: Partial) { @@ -207,6 +225,11 @@ function buildTestScene(overrides?: Partial) { uid: 'dash-1', $timeRange: new SceneTimeRange({}), controls: new DashboardControls({}), + $behaviors: [ + new behaviors.CursorSync({ + sync: DashboardCursorSync.Crosshair, + }), + ], $data: new SceneDataLayers({ layers: [ new DashboardAnnotationsDataLayer({ diff --git a/public/app/features/dashboard-scene/utils/dashboardSceneGraph.ts b/public/app/features/dashboard-scene/utils/dashboardSceneGraph.ts index 898885a730..2a289ff47a 100644 --- a/public/app/features/dashboard-scene/utils/dashboardSceneGraph.ts +++ b/public/app/features/dashboard-scene/utils/dashboardSceneGraph.ts @@ -1,4 +1,12 @@ -import { VizPanel, SceneGridItem, SceneGridRow, SceneDataLayers, sceneGraph, SceneGridLayout } from '@grafana/scenes'; +import { + VizPanel, + SceneGridItem, + SceneGridRow, + SceneDataLayers, + sceneGraph, + SceneGridLayout, + behaviors, +} from '@grafana/scenes'; import { DashboardScene } from '../scene/DashboardScene'; import { LibraryVizPanel } from '../scene/LibraryVizPanel'; @@ -23,7 +31,7 @@ function getPanelLinks(panel: VizPanel) { return panel.state.titleItems[0]; } - throw new Error('VizPanelLinks links not found'); + return null; } function getVizPanels(scene: DashboardScene): VizPanel[] { @@ -58,6 +66,16 @@ function getDataLayers(scene: DashboardScene): SceneDataLayers { return data; } +export function getCursorSync(scene: DashboardScene) { + const cursorSync = scene.state.$behaviors?.find((b) => b instanceof behaviors.CursorSync); + + if (cursorSync instanceof behaviors.CursorSync) { + return cursorSync; + } + + return; +} + export function getNextPanelId(dashboard: DashboardScene): number { let max = 0; const body = dashboard.state.body; @@ -119,4 +137,5 @@ export const dashboardSceneGraph = { getVizPanels, getDataLayers, getNextPanelId, + getCursorSync, }; diff --git a/public/app/features/dashboard/components/PanelEditor/getPanelFrameOptions.tsx b/public/app/features/dashboard/components/PanelEditor/getPanelFrameOptions.tsx index eb2fe92410..6b3adccb4e 100644 --- a/public/app/features/dashboard/components/PanelEditor/getPanelFrameOptions.tsx +++ b/public/app/features/dashboard/components/PanelEditor/getPanelFrameOptions.tsx @@ -185,7 +185,7 @@ export function getPanelFrameCategory2(panelManager: VizPanelManager): OptionsPa }); const panelLinksObject = dashboardSceneGraph.getPanelLinks(panel); - const links = panelLinksObject.state.rawLinks; + const links = panelLinksObject?.state.rawLinks ?? []; return descriptor .addItem( @@ -251,7 +251,7 @@ export function getPanelFrameCategory2(panelManager: VizPanelManager): OptionsPa }).addItem( new OptionsPaneItemDescriptor({ title: 'Panel links', - render: () => , + render: () => , }) ) ) @@ -323,16 +323,16 @@ export function getPanelFrameCategory2(panelManager: VizPanelManager): OptionsPa } interface ScenePanelLinksEditorProps { - panelLinks: VizPanelLinks; + panelLinks?: VizPanelLinks; } function ScenePanelLinksEditor({ panelLinks }: ScenePanelLinksEditorProps) { - const { rawLinks: links } = panelLinks.useState(); + const { rawLinks: links } = panelLinks ? panelLinks.useState() : { rawLinks: [] }; return ( panelLinks.setState({ rawLinks: links })} + onChange={(links) => panelLinks?.setState({ rawLinks: links })} getSuggestions={getPanelLinksVariableSuggestions} data={[]} /> diff --git a/public/app/features/panel/panellinks/linkSuppliers.ts b/public/app/features/panel/panellinks/linkSuppliers.ts index 4924278d01..c2810f7dab 100644 --- a/public/app/features/panel/panellinks/linkSuppliers.ts +++ b/public/app/features/panel/panellinks/linkSuppliers.ts @@ -164,7 +164,7 @@ export const getScenePanelLinksSupplier = ( panel: VizPanel, replaceVariables: InterpolateFunction ): LinkModelSupplier | undefined => { - const links = dashboardSceneGraph.getPanelLinks(panel).state.rawLinks; + const links = dashboardSceneGraph.getPanelLinks(panel)?.state.rawLinks; if (!links || links.length === 0) { return undefined; diff --git a/public/test/setupTests.ts b/public/test/setupTests.ts index 650614f512..4b59a6b5b5 100644 --- a/public/test/setupTests.ts +++ b/public/test/setupTests.ts @@ -22,6 +22,11 @@ i18next.use(initReactI18next).init({ lng: 'en-US', // this should be the locale of the phrases in our source JSX }); +// mock out the worker that detects changes in the dashboard +// The mock is needed because JSDOM does not support workers and +// the factory uses import.meta.url so we can't use it in CommonJS modules. +jest.mock('app/features/dashboard-scene/saving/createDetectChangesWorker.ts'); + // our tests are heavy in CI due to parallelisation and monaco and kusto // so we increase the default timeout to 2secs to avoid flakiness configure({ asyncUtilTimeout: 2000 }); From f0dce33034495138a19162e003cfb45dbed9d3c1 Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Thu, 29 Feb 2024 16:29:17 +0000 Subject: [PATCH 0308/1406] Chore: Taint ArrayVector with `never` to further discourage (#83681) Chore: Taint ArrayVector with never to further discourage --- .betterer.results | 5 ----- .../grafana-data/src/vector/ArrayVector.test.ts | 14 ++++++++++++++ packages/grafana-data/src/vector/ArrayVector.ts | 10 +++++----- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/.betterer.results b/.betterer.results index bd9901094f..9abae8a3a3 100644 --- a/.betterer.results +++ b/.betterer.results @@ -439,11 +439,6 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "3"], [0, 0, 0, "Unexpected any. Specify a different type.", "4"] ], - "packages/grafana-data/src/vector/ArrayVector.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Unexpected any. Specify a different type.", "2"] - ], "packages/grafana-data/src/vector/CircularVector.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], diff --git a/packages/grafana-data/src/vector/ArrayVector.test.ts b/packages/grafana-data/src/vector/ArrayVector.test.ts index 3d9ae35fc5..eb0c53dfa0 100644 --- a/packages/grafana-data/src/vector/ArrayVector.test.ts +++ b/packages/grafana-data/src/vector/ArrayVector.test.ts @@ -2,6 +2,9 @@ import { Field, FieldType } from '../types'; import { ArrayVector } from './ArrayVector'; +// There's lots of @ts-expect-error here, because we actually expect it to be a typescript error +// to further encourge developers not to use ArrayVector + describe('ArrayVector', () => { beforeEach(() => { jest.spyOn(console, 'warn').mockImplementation(); @@ -9,12 +12,14 @@ describe('ArrayVector', () => { it('should init 150k with 65k Array.push() chonking', () => { const arr = Array.from({ length: 150e3 }, (v, i) => i); + /// @ts-expect-error const av = new ArrayVector(arr); expect(av.toArray()).toEqual(arr); }); it('should support add and push', () => { + /// @ts-expect-error const av = new ArrayVector(); av.add(1); av.push(2); @@ -28,17 +33,26 @@ describe('ArrayVector', () => { name: 'test', config: {}, type: FieldType.number, + /// @ts-expect-error values: new ArrayVector(), // this defaults to `new ArrayVector()` }; expect(field).toBeDefined(); // Before collapsing Vector, ReadWriteVector, and MutableVector these all worked fine + + /// @ts-expect-error field.values = new ArrayVector(); + /// @ts-expect-error field.values = new ArrayVector(undefined); + /// @ts-expect-error field.values = new ArrayVector([1, 2, 3]); + /// @ts-expect-error field.values = new ArrayVector([]); + /// @ts-expect-error field.values = new ArrayVector([1, undefined]); + /// @ts-expect-error field.values = new ArrayVector([null]); + /// @ts-expect-error field.values = new ArrayVector(['a', 'b', 'c']); expect(field.values.length).toBe(3); }); diff --git a/packages/grafana-data/src/vector/ArrayVector.ts b/packages/grafana-data/src/vector/ArrayVector.ts index 01b615162b..0673f12f74 100644 --- a/packages/grafana-data/src/vector/ArrayVector.ts +++ b/packages/grafana-data/src/vector/ArrayVector.ts @@ -6,12 +6,12 @@ let notified = false; * * @deprecated use a simple Array */ -export class ArrayVector extends Array { +export class ArrayVector extends Array { get buffer() { return this; } - set buffer(values: any[]) { + set buffer(values: T[]) { this.length = 0; const len = values?.length; @@ -27,10 +27,10 @@ export class ArrayVector extends Array { } /** - * This any type is here to make the change type changes in v10 non breaking for plugins. - * Before you could technically assign field.values any typed ArrayVector no matter what the Field T type was. + * ArrayVector is deprecated and should not be used. If you get a Typescript error here, use plain arrays for field.values. */ - constructor(buffer?: any[]) { + // `never` is used to force a build-type error from Typescript to encourage developers to move away from using this + constructor(buffer: never) { super(); this.buffer = buffer ?? []; From 0218e94d936b6acbf2974154f1f8c5e6027501b5 Mon Sep 17 00:00:00 2001 From: Misi Date: Thu, 29 Feb 2024 17:41:08 +0100 Subject: [PATCH 0309/1406] Auth: Add Save and enable, Disable buttons to SSO UI (#83672) * Add Save and enable and Disable button * Change to use Dropdown, reorder buttons * Improve UI * Update public/app/features/auth-config/ProviderConfigForm.tsx * Apply suggestions from code review * Use Stack instead of separate Fields --------- Co-authored-by: Alex Khomenko --- .../auth-config/ProviderConfigForm.test.tsx | 59 +++++- .../auth-config/ProviderConfigForm.tsx | 198 ++++++++++-------- .../auth-config/ProviderConfigPage.tsx | 16 +- 3 files changed, 182 insertions(+), 91 deletions(-) diff --git a/public/app/features/auth-config/ProviderConfigForm.test.tsx b/public/app/features/auth-config/ProviderConfigForm.test.tsx index 0386ab6e71..efab7d4294 100644 --- a/public/app/features/auth-config/ProviderConfigForm.test.tsx +++ b/public/app/features/auth-config/ProviderConfigForm.test.tsx @@ -62,7 +62,7 @@ const testConfig: SSOProvider = { const emptyConfig = { ...testConfig, - settings: { ...testConfig.settings, clientId: '', clientSecret: '' }, + settings: { ...testConfig.settings, enabled: false, clientId: '', clientSecret: '' }, }; function setup(jsx: JSX.Element) { @@ -79,7 +79,6 @@ describe('ProviderConfigForm', () => { it('renders all fields correctly', async () => { setup(); - expect(screen.getByRole('checkbox', { name: /Enabled/i })).toBeInTheDocument(); expect(screen.getByRole('textbox', { name: /Client ID/i })).toBeInTheDocument(); expect(screen.getByRole('combobox', { name: /Team IDs/i })).toBeInTheDocument(); expect(screen.getByRole('combobox', { name: /Allowed organizations/i })).toBeInTheDocument(); @@ -87,7 +86,7 @@ describe('ProviderConfigForm', () => { expect(screen.getByRole('link', { name: /Discard/i })).toBeInTheDocument(); }); - it('should save correct data on form submit', async () => { + it('should save and enable on form submit', async () => { const { user } = setup(); await user.type(screen.getByRole('textbox', { name: /Client ID/i }), 'test-client-id'); await user.type(screen.getByLabelText(/Client secret/i), 'test-client-secret'); @@ -96,7 +95,7 @@ describe('ProviderConfigForm', () => { // Add two orgs await user.type(screen.getByRole('combobox', { name: /Allowed organizations/i }), 'test-org1{enter}'); await user.type(screen.getByRole('combobox', { name: /Allowed organizations/i }), 'test-org2{enter}'); - await user.click(screen.getByRole('button', { name: /Save/i })); + await user.click(screen.getByRole('button', { name: /Save and enable/i })); await waitFor(() => { expect(putMock).toHaveBeenCalledWith( @@ -123,9 +122,53 @@ describe('ProviderConfigForm', () => { }); }); - it('should validate required fields', async () => { + it('should save on form submit', async () => { const { user } = setup(); - await user.click(screen.getByRole('button', { name: /Save/i })); + await user.type(screen.getByRole('textbox', { name: /Client ID/i }), 'test-client-id'); + await user.type(screen.getByLabelText(/Client secret/i), 'test-client-secret'); + // Type a team name and press enter to select it + await user.type(screen.getByRole('combobox', { name: /Team IDs/i }), '12324{enter}'); + // Add two orgs + await user.type(screen.getByRole('combobox', { name: /Allowed organizations/i }), 'test-org1{enter}'); + await user.type(screen.getByRole('combobox', { name: /Allowed organizations/i }), 'test-org2{enter}'); + await user.click(screen.getByText('Save')); + + await waitFor(() => { + expect(putMock).toHaveBeenCalledWith( + '/api/v1/sso-settings/github', + { + id: '300f9b7c-0488-40db-9763-a22ce8bf6b3e', + provider: 'github', + settings: { + name: 'GitHub', + allowedOrganizations: 'test-org1,test-org2', + clientId: 'test-client-id', + clientSecret: 'test-client-secret', + teamIds: '12324', + enabled: false, + }, + }, + { showErrorAlert: false } + ); + + expect(reportInteractionMock).toHaveBeenCalledWith('grafana_authentication_ssosettings_saved', { + provider: 'github', + enabled: false, + }); + }); + }); + + it('should validate required fields on Save', async () => { + const { user } = setup(); + await user.click(screen.getByText('Save')); + + // Should show an alert for empty client ID + expect(await screen.findAllByRole('alert')).toHaveLength(1); + }); + + it('should validate required fields on Save and enable', async () => { + const { user } = setup(); + await user.click(screen.getByRole('button', { name: /Save and enable/i })); // Should show an alert for empty client ID expect(await screen.findAllByRole('alert')).toHaveLength(1); @@ -133,7 +176,9 @@ describe('ProviderConfigForm', () => { it('should delete the current config', async () => { const { user } = setup(); - await user.click(screen.getByRole('button', { name: /Reset/i })); + await user.click(screen.getByTitle(/More actions/i)); + + await user.click(screen.getByRole('menuitem', { name: /Reset to default values/i })); expect(screen.getByRole('dialog', { name: /Reset/i })).toBeInTheDocument(); diff --git a/public/app/features/auth-config/ProviderConfigForm.tsx b/public/app/features/auth-config/ProviderConfigForm.tsx index 17e0f24a6d..d8b2d3b641 100644 --- a/public/app/features/auth-config/ProviderConfigForm.tsx +++ b/public/app/features/auth-config/ProviderConfigForm.tsx @@ -3,7 +3,19 @@ import { useForm } from 'react-hook-form'; import { AppEvents } from '@grafana/data'; import { getAppEvents, getBackendSrv, isFetchError, locationService, reportInteraction } from '@grafana/runtime'; -import { Box, Button, CollapsableSection, ConfirmModal, Field, LinkButton, Stack, Switch } from '@grafana/ui'; +import { + Box, + Button, + CollapsableSection, + ConfirmModal, + Dropdown, + Field, + IconButton, + LinkButton, + Menu, + Stack, + Switch, +} from '@grafana/ui'; import { FormPrompt } from '../../core/components/FormPrompt/FormPrompt'; import { Page } from '../../core/components/Page/Page'; @@ -39,6 +51,18 @@ export const ProviderConfigForm = ({ config, provider, isLoading }: ProviderConf const sections = sectionFields[provider]; const [resetConfig, setResetConfig] = useState(false); + const additionalActionsMenu = ( + + { + setResetConfig(true); + }} + /> + + ); + const onSubmit = async (data: SSOProviderDTO) => { setIsSaving(true); setSubmitError(false); @@ -114,95 +138,103 @@ export const ProviderConfigForm = ({ config, provider, isLoading }: ProviderConf } }; + const isEnabled = config?.settings.enabled; + return (
- <> - { - reportInteraction('grafana_authentication_ssosettings_abandoned', { - provider, - }); - reset(); - }} - /> - - - - {sections ? ( - - {sections - .filter((section) => !section.hidden) - .map((section, index) => { - return ( - - {section.fields - .filter((field) => (typeof field !== 'string' ? !field.hidden : true)) - .map((field) => { - return ( - - ); - })} - - ); - })} - - ) : ( - <> - {providerFields.map((field) => { + { + reportInteraction('grafana_authentication_ssosettings_abandoned', { + provider, + }); + reset(); + }} + /> + + {sections ? ( + + {sections + .filter((section) => !section.hidden) + .map((section, index) => { return ( - + + {section.fields + .filter((field) => (typeof field !== 'string' ? !field.hidden : true)) + .map((field) => { + return ( + + ); + })} + ); })} - - )} - - - - - - - Discard - - - - + + + + Discard + + + + - - + /> + + + {resetConfig && ( + ( + + {title} + + + )} + > ); From b2cb8d8038a65860bd3baa3e62ac07e01abd709a Mon Sep 17 00:00:00 2001 From: JERHAV <77524797+JERHAV@users.noreply.github.com> Date: Thu, 29 Feb 2024 17:54:37 +0100 Subject: [PATCH 0310/1406] CloudWatch: Remove unnecessary sortDimensions function (#83450) Remove sortDimension Closes #83338 --- pkg/tsdb/cloudwatch/models/cloudwatch_query.go | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/pkg/tsdb/cloudwatch/models/cloudwatch_query.go b/pkg/tsdb/cloudwatch/models/cloudwatch_query.go index 403ac843fc..a6775cd404 100644 --- a/pkg/tsdb/cloudwatch/models/cloudwatch_query.go +++ b/pkg/tsdb/cloudwatch/models/cloudwatch_query.go @@ -7,7 +7,6 @@ import ( "math" "net/url" "regexp" - "sort" "strconv" "strings" "time" @@ -479,22 +478,7 @@ func parseDimensions(dimensions map[string]any) (map[string][]string, error) { } } - sortedDimensions := sortDimensions(parsedDimensions) - return sortedDimensions, nil -} - -func sortDimensions(dimensions map[string][]string) map[string][]string { - sortedDimensions := make(map[string][]string, len(dimensions)) - keys := make([]string, 0, len(dimensions)) - for k := range dimensions { - keys = append(keys, k) - } - sort.Strings(keys) - - for _, k := range keys { - sortedDimensions[k] = dimensions[k] - } - return sortedDimensions + return parsedDimensions, nil } func getEndpoint(region string) string { From 098c611b654a41fe39101f9ad5f83defcd327cd0 Mon Sep 17 00:00:00 2001 From: Spencer Torres Date: Thu, 29 Feb 2024 12:30:40 -0500 Subject: [PATCH 0311/1406] Docs: add ClickHouse to exploring logs/traces page (#83178) --- docs/sources/explore/logs-integration.md | 1 + docs/sources/explore/trace-integration.md | 2 ++ 2 files changed, 3 insertions(+) diff --git a/docs/sources/explore/logs-integration.md b/docs/sources/explore/logs-integration.md index 703c87829e..92ffc6eabe 100644 --- a/docs/sources/explore/logs-integration.md +++ b/docs/sources/explore/logs-integration.md @@ -21,6 +21,7 @@ Explore is a powerful tool for logging and log analysis. It allows you to invest - [Cloudwatch]({{< relref "../datasources/aws-cloudwatch" >}}) - [InfluxDB]({{< relref "../datasources/influxdb" >}}) - [Azure Monitor]({{< relref "../datasources/azure-monitor" >}}) +- [ClickHouse](https://github.com/grafana/clickhouse-datasource) With Explore, you can efficiently monitor, troubleshoot, and respond to incidents by analyzing your logs and identifying the root causes. It also helps you to correlate logs with other telemetry signals such as metrics, traces or profiles, by viewing them side-by-side. diff --git a/docs/sources/explore/trace-integration.md b/docs/sources/explore/trace-integration.md index db4e261fb0..59408b5676 100644 --- a/docs/sources/explore/trace-integration.md +++ b/docs/sources/explore/trace-integration.md @@ -23,6 +23,7 @@ Supported data sources are: - [Zipkin]({{< relref "../datasources/zipkin/" >}}) - [X-Ray](https://grafana.com/grafana/plugins/grafana-x-ray-datasource) - [Azure Monitor Application Insights]({{< relref "../datasources/azure-monitor/" >}}) +- [ClickHouse](https://github.com/grafana/clickhouse-datasource) For information on how to configure queries for the data sources listed above, refer to the documentation for specific data source. @@ -38,6 +39,7 @@ For information on querying each data source, refer to their documentation: - [Jaeger query editor]({{< relref "../datasources/jaeger/#query-the-data-source" >}}) - [Zipkin query editor]({{< relref "../datasources/zipkin/#query-the-data-source" >}}) - [Azure Monitor Application Insights query editor]({{< relref "../datasources/azure-monitor/query-editor/#query-application-insights-traces" >}}) +- [ClickHouse query editor](https://clickhouse.com/docs/en/integrations/grafana/query-builder#traces) ## Trace view From d21a61752fe75650f0aed0fc3c3db308cd4175e6 Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Thu, 29 Feb 2024 17:43:00 +0000 Subject: [PATCH 0312/1406] Docs: Mention nvm in contribute docs (#83709) Mention nvm in contribute docs --- contribute/developer-guide.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contribute/developer-guide.md b/contribute/developer-guide.md index 8ff02847bc..9d8b500f57 100644 --- a/contribute/developer-guide.md +++ b/contribute/developer-guide.md @@ -8,7 +8,7 @@ Make sure you have the following dependencies installed before setting up your d - [Git](https://git-scm.com/) - [Go](https://golang.org/dl/) (see [go.mod](../go.mod#L3) for minimum required version) -- [Node.js (Long Term Support)](https://nodejs.org), with [corepack enabled](https://nodejs.org/api/corepack.html#enabling-the-feature) +- [Node.js (Long Term Support)](https://nodejs.org), with [corepack enabled](https://nodejs.org/api/corepack.html#enabling-the-feature). See [.nvmrc](../nvm.rc) for supported version. It's recommend you use a version manager such as [nvm](https://github.com/nvm-sh/nvm), [fnm](https://github.com/Schniz/fnm), or similar. - GCC (required for Cgo dependencies) ### macOS From c9d8d8713b6046024f33a527427a47674b754255 Mon Sep 17 00:00:00 2001 From: Andreas Christou Date: Thu, 29 Feb 2024 18:00:21 +0000 Subject: [PATCH 0313/1406] CI: Bump `alpine` image version (#83716) Bump image version --- .drone.yml | 70 ++++++++++++++++----------------- scripts/drone/utils/images.star | 2 +- 2 files changed, 36 insertions(+), 36 deletions(-) diff --git a/.drone.yml b/.drone.yml index 284bf0d15d..3ca0229613 100644 --- a/.drone.yml +++ b/.drone.yml @@ -18,7 +18,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.18.5 + image: alpine:3.19.1 name: identify-runner - commands: - go build -o ./bin/build -ldflags '-extldflags -static' ./pkg/build/cmd @@ -69,7 +69,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.18.5 + image: alpine:3.19.1 name: identify-runner - commands: - go build -o ./bin/build -ldflags '-extldflags -static' ./pkg/build/cmd @@ -120,7 +120,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.18.5 + image: alpine:3.19.1 name: identify-runner - commands: - apk add --update g++ make python3 && ln -sf /usr/bin/python3 /usr/bin/python @@ -222,7 +222,7 @@ steps: name: clone-enterprise - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.18.5 + image: alpine:3.19.1 name: identify-runner - commands: - apk add --update g++ make python3 && ln -sf /usr/bin/python3 /usr/bin/python @@ -313,7 +313,7 @@ steps: name: clone-enterprise - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.18.5 + image: alpine:3.19.1 name: identify-runner - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -400,7 +400,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.18.5 + image: alpine:3.19.1 name: identify-runner - commands: - go build -o ./bin/build -ldflags '-extldflags -static' ./pkg/build/cmd @@ -496,7 +496,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.18.5 + image: alpine:3.19.1 name: identify-runner - commands: - mkdir -p bin @@ -594,7 +594,7 @@ steps: GF_APP_MODE: development GF_SERVER_HTTP_PORT: "3001" GF_SERVER_ROUTER_LOGGING: "1" - image: alpine:3.18.5 + image: alpine:3.19.1 name: grafana-server - commands: - ./bin/build e2e-tests --port 3001 --suite dashboards-suite @@ -773,7 +773,7 @@ steps: - /src/grafana-build artifacts -a docker:grafana:linux/amd64 -a docker:grafana:linux/amd64:ubuntu -a docker:grafana:linux/arm64 -a docker:grafana:linux/arm64:ubuntu -a docker:grafana:linux/arm/v7 -a docker:grafana:linux/arm/v7:ubuntu --yarn-cache=$$YARN_CACHE_FOLDER --build-id=$$DRONE_BUILD_NUMBER - --go-version=1.21.6 --ubuntu-base=ubuntu:22.04 --alpine-base=alpine:3.18.5 --tag-format='{{ + --go-version=1.21.6 --ubuntu-base=ubuntu:22.04 --alpine-base=alpine:3.19.1 --tag-format='{{ .version_base }}-{{ .buildID }}-{{ .arch }}' --grafana-dir=$$PWD --ubuntu-tag-format='{{ .version_base }}-{{ .buildID }}-ubuntu-{{ .arch }}' > docker.txt - find ./dist -name '*docker*.tar.gz' -type f | xargs -n1 docker load -i @@ -921,7 +921,7 @@ steps: name: compile-build-cmd - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.18.5 + image: alpine:3.19.1 name: identify-runner - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -1109,7 +1109,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.18.5 + image: alpine:3.19.1 name: identify-runner - commands: - apk add --update g++ make python3 && ln -sf /usr/bin/python3 /usr/bin/python @@ -1469,7 +1469,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.18.5 + image: alpine:3.19.1 name: identify-runner - commands: - apk add --update g++ make python3 && ln -sf /usr/bin/python3 /usr/bin/python @@ -1546,7 +1546,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.18.5 + image: alpine:3.19.1 name: identify-runner - commands: - apk add --update g++ make python3 && ln -sf /usr/bin/python3 /usr/bin/python @@ -1605,7 +1605,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.18.5 + image: alpine:3.19.1 name: identify-runner - commands: - apk add --update g++ make python3 && ln -sf /usr/bin/python3 /usr/bin/python @@ -1674,7 +1674,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.18.5 + image: alpine:3.19.1 name: identify-runner - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -1755,7 +1755,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.18.5 + image: alpine:3.19.1 name: identify-runner - commands: - go build -o ./bin/build -ldflags '-extldflags -static' ./pkg/build/cmd @@ -1830,7 +1830,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.18.5 + image: alpine:3.19.1 name: identify-runner - commands: - mkdir -p bin @@ -1927,7 +1927,7 @@ steps: GF_APP_MODE: development GF_SERVER_HTTP_PORT: "3001" GF_SERVER_ROUTER_LOGGING: "1" - image: alpine:3.18.5 + image: alpine:3.19.1 name: grafana-server - commands: - ./bin/build e2e-tests --port 3001 --suite dashboards-suite @@ -2142,7 +2142,7 @@ steps: - /src/grafana-build artifacts -a docker:grafana:linux/amd64 -a docker:grafana:linux/amd64:ubuntu -a docker:grafana:linux/arm64 -a docker:grafana:linux/arm64:ubuntu -a docker:grafana:linux/arm/v7 -a docker:grafana:linux/arm/v7:ubuntu --yarn-cache=$$YARN_CACHE_FOLDER --build-id=$$DRONE_BUILD_NUMBER - --go-version=1.21.6 --ubuntu-base=ubuntu:22.04 --alpine-base=alpine:3.18.5 --tag-format='{{ + --go-version=1.21.6 --ubuntu-base=ubuntu:22.04 --alpine-base=alpine:3.19.1 --tag-format='{{ .version_base }}-{{ .buildID }}-{{ .arch }}' --grafana-dir=$$PWD --ubuntu-tag-format='{{ .version_base }}-{{ .buildID }}-ubuntu-{{ .arch }}' > docker.txt - find ./dist -name '*docker*.tar.gz' -type f | xargs -n1 docker load -i @@ -2352,7 +2352,7 @@ steps: name: compile-build-cmd - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.18.5 + image: alpine:3.19.1 name: identify-runner - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -2669,7 +2669,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.18.5 + image: alpine:3.19.1 name: identify-runner - commands: - mkdir -p bin @@ -3006,7 +3006,7 @@ steps: environment: _EXPERIMENTAL_DAGGER_CLOUD_TOKEN: from_secret: dagger_token - ALPINE_BASE: alpine:3.18.5 + ALPINE_BASE: alpine:3.19.1 CDN_DESTINATION: from_secret: rgm_cdn_destination DESTINATION: @@ -3123,7 +3123,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.18.5 + image: alpine:3.19.1 name: identify-runner - commands: - apk add --update g++ make python3 && ln -sf /usr/bin/python3 /usr/bin/python @@ -3180,7 +3180,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.18.5 + image: alpine:3.19.1 name: identify-runner - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -3263,7 +3263,7 @@ steps: environment: _EXPERIMENTAL_DAGGER_CLOUD_TOKEN: from_secret: dagger_token - ALPINE_BASE: alpine:3.18.5 + ALPINE_BASE: alpine:3.19.1 CDN_DESTINATION: from_secret: rgm_cdn_destination DESTINATION: @@ -3446,7 +3446,7 @@ steps: environment: _EXPERIMENTAL_DAGGER_CLOUD_TOKEN: from_secret: dagger_token - ALPINE_BASE: alpine:3.18.5 + ALPINE_BASE: alpine:3.19.1 CDN_DESTINATION: from_secret: rgm_cdn_destination DESTINATION: @@ -3548,7 +3548,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.18.5 + image: alpine:3.19.1 name: identify-runner - commands: - apk add --update g++ make python3 && ln -sf /usr/bin/python3 /usr/bin/python @@ -3603,7 +3603,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.18.5 + image: alpine:3.19.1 name: identify-runner - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -3684,7 +3684,7 @@ steps: environment: _EXPERIMENTAL_DAGGER_CLOUD_TOKEN: from_secret: dagger_token - ALPINE_BASE: alpine:3.18.5 + ALPINE_BASE: alpine:3.19.1 CDN_DESTINATION: from_secret: rgm_cdn_destination DESTINATION: @@ -3831,7 +3831,7 @@ steps: environment: _EXPERIMENTAL_DAGGER_CLOUD_TOKEN: from_secret: dagger_token - ALPINE_BASE: alpine:3.18.5 + ALPINE_BASE: alpine:3.19.1 CDN_DESTINATION: from_secret: rgm_cdn_destination DESTINATION: @@ -3940,7 +3940,7 @@ steps: environment: _EXPERIMENTAL_DAGGER_CLOUD_TOKEN: from_secret: dagger_token - ALPINE_BASE: alpine:3.18.5 + ALPINE_BASE: alpine:3.19.1 CDN_DESTINATION: from_secret: rgm_cdn_destination DESTINATION: @@ -4143,7 +4143,7 @@ steps: name: grabpl - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.18.5 + image: alpine:3.19.1 name: identify-runner - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -4638,7 +4638,7 @@ steps: - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM node:20.9.0-alpine - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM google/cloud-sdk:431.0.0 - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM grafana/grafana-ci-deploy:1.3.3 - - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM alpine:3.18.5 + - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM alpine:3.19.1 - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM ubuntu:22.04 - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM byrnedo/alpine-curl:0.1.8 - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM plugins/slack @@ -4673,7 +4673,7 @@ steps: - trivy --exit-code 1 --severity HIGH,CRITICAL node:20.9.0-alpine - trivy --exit-code 1 --severity HIGH,CRITICAL google/cloud-sdk:431.0.0 - trivy --exit-code 1 --severity HIGH,CRITICAL grafana/grafana-ci-deploy:1.3.3 - - trivy --exit-code 1 --severity HIGH,CRITICAL alpine:3.18.5 + - trivy --exit-code 1 --severity HIGH,CRITICAL alpine:3.19.1 - trivy --exit-code 1 --severity HIGH,CRITICAL ubuntu:22.04 - trivy --exit-code 1 --severity HIGH,CRITICAL byrnedo/alpine-curl:0.1.8 - trivy --exit-code 1 --severity HIGH,CRITICAL plugins/slack @@ -4924,6 +4924,6 @@ kind: secret name: gcr_credentials --- kind: signature -hmac: b588ab1704559f537b65d7fe2cb45c4308274943a2be023805eb491cc9dc7302 +hmac: 959c920d1ce1f36b68e93dd4b4de180dd64ae58c28b9eb52ee1d343f991f5cfd ... diff --git a/scripts/drone/utils/images.star b/scripts/drone/utils/images.star index 343ea0c755..619cdfdf88 100644 --- a/scripts/drone/utils/images.star +++ b/scripts/drone/utils/images.star @@ -14,7 +14,7 @@ images = { "node": "node:{}-alpine".format(nodejs_version), "cloudsdk": "google/cloud-sdk:431.0.0", "publish": "grafana/grafana-ci-deploy:1.3.3", - "alpine": "alpine:3.18.5", + "alpine": "alpine:3.19.1", "ubuntu": "ubuntu:22.04", "curl": "byrnedo/alpine-curl:0.1.8", "plugins_slack": "plugins/slack", From 42b55aedbc3e35ed426ff684347492cbdbea3314 Mon Sep 17 00:00:00 2001 From: Leon Sorokin Date: Thu, 29 Feb 2024 13:46:53 -0600 Subject: [PATCH 0314/1406] DataLinks: Handle getLinks() regen during data updates and frame joining (#83654) --- .../app/core/components/GraphNG/GraphNG.tsx | 66 +++++++++++++++---- .../TimelineChart/TimelineChart.tsx | 1 + .../rules/state-history/LogTimelineViewer.tsx | 6 +- .../plugins/panel/barchart/BarChartPanel.tsx | 6 +- .../panel/candlestick/CandlestickPanel.tsx | 5 +- .../state-timeline/StateTimelinePanel.tsx | 4 +- .../status-history/StatusHistoryPanel.tsx | 5 +- .../app/plugins/panel/status-history/utils.ts | 2 +- .../panel/timeseries/TimeSeriesPanel.tsx | 14 +--- public/app/plugins/panel/timeseries/utils.ts | 40 ----------- public/app/plugins/panel/trend/TrendPanel.tsx | 14 +--- .../panel/xychart/XYChartTooltip.test.tsx | 10 ++- 12 files changed, 91 insertions(+), 82 deletions(-) diff --git a/public/app/core/components/GraphNG/GraphNG.tsx b/public/app/core/components/GraphNG/GraphNG.tsx index b4b5974d55..9c53722f21 100644 --- a/public/app/core/components/GraphNG/GraphNG.tsx +++ b/public/app/core/components/GraphNG/GraphNG.tsx @@ -7,10 +7,13 @@ import { DataFrame, DataHoverClearEvent, DataHoverEvent, + DataLinkPostProcessor, Field, FieldMatcherID, fieldMatchers, FieldType, + getLinksSupplier, + InterpolateFunction, LegacyGraphHoverEvent, TimeRange, TimeZone, @@ -49,6 +52,8 @@ export interface GraphNGProps extends Themeable2 { propsToDiff?: Array; preparePlotFrame?: (frames: DataFrame[], dimFields: XYFieldMatchers) => DataFrame | null; renderLegend: (config: UPlotConfigBuilder) => React.ReactElement | null; + replaceVariables: InterpolateFunction; + dataLinkPostProcessor?: DataLinkPostProcessor; /** * needed for propsToDiff to re-init the plot & config @@ -105,30 +110,67 @@ export class GraphNG extends Component { prepState(props: GraphNGProps, withConfig = true) { let state: GraphNGState = null as any; - const { frames, fields, preparePlotFrame } = props; + const { frames, fields, preparePlotFrame, replaceVariables, dataLinkPostProcessor } = props; - const preparePlotFrameFn = preparePlotFrame || defaultPreparePlotFrame; + const preparePlotFrameFn = preparePlotFrame ?? defaultPreparePlotFrame; + + const matchY = fieldMatchers.get(FieldMatcherID.byTypes).get(new Set([FieldType.number, FieldType.enum])); + + // if there are data links, we have to keep all fields so they're index-matched, then filter out dimFields.y + const withLinks = frames.some((frame) => frame.fields.some((field) => (field.config.links?.length ?? 0) > 0)); + + const dimFields = fields ?? { + x: fieldMatchers.get(FieldMatcherID.firstTimeField).get({}), + y: withLinks ? () => true : matchY, + }; + + const alignedFrame = preparePlotFrameFn(frames, dimFields, props.timeRange); - const alignedFrame = preparePlotFrameFn( - frames, - fields || { - x: fieldMatchers.get(FieldMatcherID.firstTimeField).get({}), - y: fieldMatchers.get(FieldMatcherID.byTypes).get(new Set([FieldType.number, FieldType.enum])), - }, - props.timeRange - ); pluginLog('GraphNG', false, 'data aligned', alignedFrame); if (alignedFrame) { + let alignedFrameFinal = alignedFrame; + + if (withLinks) { + const timeZone = Array.isArray(this.props.timeZone) ? this.props.timeZone[0] : this.props.timeZone; + + alignedFrame.fields.forEach((field) => { + field.getLinks = getLinksSupplier( + alignedFrame, + field, + { + ...field.state?.scopedVars, + __dataContext: { + value: { + data: [alignedFrame], + field: field, + frame: alignedFrame, + frameIndex: 0, + }, + }, + }, + replaceVariables, + timeZone, + dataLinkPostProcessor + ); + }); + + // filter join field and dimFields.y + alignedFrameFinal = { + ...alignedFrame, + fields: alignedFrame.fields.filter((field, i) => i === 0 || matchY(field, alignedFrame, [alignedFrame])), + }; + } + let config = this.state?.config; if (withConfig) { - config = props.prepConfig(alignedFrame, this.props.frames, this.getTimeRange); + config = props.prepConfig(alignedFrameFinal, this.props.frames, this.getTimeRange); pluginLog('GraphNG', false, 'config prepared', config); } state = { - alignedFrame, + alignedFrame: alignedFrameFinal, config, }; diff --git a/public/app/core/components/TimelineChart/TimelineChart.tsx b/public/app/core/components/TimelineChart/TimelineChart.tsx index c22e3230da..9d2474a461 100644 --- a/public/app/core/components/TimelineChart/TimelineChart.tsx +++ b/public/app/core/components/TimelineChart/TimelineChart.tsx @@ -93,6 +93,7 @@ export class TimelineChart extends React.Component { prepConfig={this.prepConfig} propsToDiff={propsToDiff} renderLegend={this.renderLegend} + dataLinkPostProcessor={this.panelContext?.dataLinkPostProcessor} /> ); } diff --git a/public/app/features/alerting/unified/components/rules/state-history/LogTimelineViewer.tsx b/public/app/features/alerting/unified/components/rules/state-history/LogTimelineViewer.tsx index b0d2d286b5..f0e2d72fc3 100644 --- a/public/app/features/alerting/unified/components/rules/state-history/LogTimelineViewer.tsx +++ b/public/app/features/alerting/unified/components/rules/state-history/LogTimelineViewer.tsx @@ -3,7 +3,7 @@ import React, { useEffect, useRef } from 'react'; import AutoSizer from 'react-virtualized-auto-sizer'; import { BehaviorSubject } from 'rxjs'; -import { DataFrame, TimeRange } from '@grafana/data'; +import { DataFrame, InterpolateFunction, TimeRange } from '@grafana/data'; import { VisibilityMode } from '@grafana/schema'; import { LegendDisplayMode, UPlotConfigBuilder, useTheme2 } from '@grafana/ui'; import { TimelineChart } from 'app/core/components/TimelineChart/TimelineChart'; @@ -15,6 +15,9 @@ interface LogTimelineViewerProps { onPointerMove?: (seriesIdx: number, pointerIdx: number) => void; } +// noop +const replaceVariables: InterpolateFunction = (v) => v; + export const LogTimelineViewer = React.memo(({ frames, timeRange, onPointerMove = noop }: LogTimelineViewerProps) => { const theme = useTheme2(); const { setupCursorTracking } = useCursorTimelinePosition(onPointerMove); @@ -45,6 +48,7 @@ export const LogTimelineViewer = React.memo(({ frames, timeRange, onPointerMove { label: 'NoData', color: theme.colors.info.main, yAxis: 1 }, { label: 'Mixed', color: theme.colors.text.secondary, yAxis: 1 }, ]} + replaceVariables={replaceVariables} > {(builder) => { setupCursorTracking(builder); diff --git a/public/app/plugins/panel/barchart/BarChartPanel.tsx b/public/app/plugins/panel/barchart/BarChartPanel.tsx index b5d2b1ad4b..abdeb0c082 100644 --- a/public/app/plugins/panel/barchart/BarChartPanel.tsx +++ b/public/app/plugins/panel/barchart/BarChartPanel.tsx @@ -69,9 +69,9 @@ const propsToDiff: Array = [ interface Props extends PanelProps {} -export const BarChartPanel = ({ data, options, fieldConfig, width, height, timeZone, id }: Props) => { +export const BarChartPanel = ({ data, options, fieldConfig, width, height, timeZone, id, replaceVariables }: Props) => { const theme = useTheme2(); - const { eventBus } = usePanelContext(); + const { eventBus, dataLinkPostProcessor } = usePanelContext(); const oldConfig = useRef(undefined); const isToolTipOpen = useRef(false); @@ -326,6 +326,8 @@ export const BarChartPanel = ({ data, options, fieldConfig, width, height, timeZ structureRev={structureRev} width={width} height={height} + replaceVariables={replaceVariables} + dataLinkPostProcessor={dataLinkPostProcessor} > {(config) => { if (showNewVizTooltips && options.tooltip.mode !== TooltipDisplayMode.None) { diff --git a/public/app/plugins/panel/candlestick/CandlestickPanel.tsx b/public/app/plugins/panel/candlestick/CandlestickPanel.tsx index fa71996e3b..7008948b7e 100644 --- a/public/app/plugins/panel/candlestick/CandlestickPanel.tsx +++ b/public/app/plugins/panel/candlestick/CandlestickPanel.tsx @@ -42,7 +42,8 @@ export const CandlestickPanel = ({ onChangeTimeRange, replaceVariables, }: CandlestickPanelProps) => { - const { sync, canAddAnnotations, onThresholdsChange, canEditThresholds, showThresholds } = usePanelContext(); + const { sync, canAddAnnotations, onThresholdsChange, canEditThresholds, showThresholds, dataLinkPostProcessor } = + usePanelContext(); const theme = useTheme2(); @@ -256,6 +257,8 @@ export const CandlestickPanel = ({ tweakAxis={tweakAxis} tweakScale={tweakScale} options={options} + replaceVariables={replaceVariables} + dataLinkPostProcessor={dataLinkPostProcessor} > {(uplotConfig, alignedDataFrame) => { alignedDataFrame.fields.forEach((field) => { diff --git a/public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx b/public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx index 0cdc5c0ec9..f613e935d4 100644 --- a/public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx +++ b/public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx @@ -70,7 +70,7 @@ export const StateTimelinePanel = ({ const [shouldDisplayCloseButton, setShouldDisplayCloseButton] = useState(false); // temp range set for adding new annotation set by TooltipPlugin2, consumed by AnnotationPlugin2 const [newAnnotationRange, setNewAnnotationRange] = useState(null); - const { sync, canAddAnnotations } = usePanelContext(); + const { sync, canAddAnnotations, dataLinkPostProcessor } = usePanelContext(); const onCloseToolTip = () => { isToolTipOpen.current = false; @@ -184,6 +184,8 @@ export const StateTimelinePanel = ({ legendItems={legendItems} {...options} mode={TimelineMode.Changes} + replaceVariables={replaceVariables} + dataLinkPostProcessor={dataLinkPostProcessor} > {(builder, alignedFrame) => { if (oldConfig.current !== builder && !showNewVizTooltips) { diff --git a/public/app/plugins/panel/status-history/StatusHistoryPanel.tsx b/public/app/plugins/panel/status-history/StatusHistoryPanel.tsx index 29192589c1..560c80e871 100644 --- a/public/app/plugins/panel/status-history/StatusHistoryPanel.tsx +++ b/public/app/plugins/panel/status-history/StatusHistoryPanel.tsx @@ -45,6 +45,7 @@ export const StatusHistoryPanel = ({ options, width, height, + replaceVariables, onChangeTimeRange, }: TimelinePanelProps) => { const theme = useTheme2(); @@ -67,7 +68,7 @@ export const StatusHistoryPanel = ({ const [shouldDisplayCloseButton, setShouldDisplayCloseButton] = useState(false); // temp range set for adding new annotation set by TooltipPlugin2, consumed by AnnotationPlugin2 const [newAnnotationRange, setNewAnnotationRange] = useState(null); - const { sync, canAddAnnotations } = usePanelContext(); + const { sync, canAddAnnotations, dataLinkPostProcessor } = usePanelContext(); const enableAnnotationCreation = Boolean(canAddAnnotations && canAddAnnotations()); @@ -213,6 +214,8 @@ export const StatusHistoryPanel = ({ legendItems={legendItems} {...options} mode={TimelineMode.Samples} + replaceVariables={replaceVariables} + dataLinkPostProcessor={dataLinkPostProcessor} > {(builder, alignedFrame) => { if (oldConfig.current !== builder && !showNewVizTooltips) { diff --git a/public/app/plugins/panel/status-history/utils.ts b/public/app/plugins/panel/status-history/utils.ts index 735ef03a5e..946424eb2e 100644 --- a/public/app/plugins/panel/status-history/utils.ts +++ b/public/app/plugins/panel/status-history/utils.ts @@ -4,7 +4,7 @@ export const getDataLinks = (field: Field, rowIdx: number) => { const links: Array> = []; const linkLookup = new Set(); - if (field.getLinks) { + if ((field.config.links?.length ?? 0) > 0 && field.getLinks != null) { const v = field.values[rowIdx]; const disp = field.display ? field.display(v) : { text: `${v}`, numeric: +v }; field.getLinks({ calculatedValue: disp, valueRowIndex: rowIdx }).forEach((link) => { diff --git a/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx b/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx index 36480a53d5..1a4ed79cdd 100644 --- a/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx +++ b/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx @@ -18,7 +18,7 @@ import { ExemplarsPlugin, getVisibleLabels } from './plugins/ExemplarsPlugin'; import { OutsideRangePlugin } from './plugins/OutsideRangePlugin'; import { ThresholdControlsPlugin } from './plugins/ThresholdControlsPlugin'; import { getPrepareTimeseriesSuggestion } from './suggestions'; -import { getTimezones, isTooltipScrollable, prepareGraphableFields, regenerateLinksSupplier } from './utils'; +import { getTimezones, isTooltipScrollable, prepareGraphableFields } from './utils'; interface TimeSeriesPanelProps extends PanelProps {} @@ -96,18 +96,10 @@ export const TimeSeriesPanel = ({ height={height} legend={options.legend} options={options} + replaceVariables={replaceVariables} + dataLinkPostProcessor={dataLinkPostProcessor} > {(uplotConfig, alignedDataFrame) => { - if (alignedDataFrame.fields.some((f) => Boolean(f.config.links?.length))) { - alignedDataFrame = regenerateLinksSupplier( - alignedDataFrame, - frames, - replaceVariables, - timeZone, - dataLinkPostProcessor - ); - } - return ( <> diff --git a/public/app/plugins/panel/timeseries/utils.ts b/public/app/plugins/panel/timeseries/utils.ts index 8889a49412..3f9212ee4d 100644 --- a/public/app/plugins/panel/timeseries/utils.ts +++ b/public/app/plugins/panel/timeseries/utils.ts @@ -3,10 +3,7 @@ import { Field, FieldType, getDisplayProcessor, - getLinksSupplier, GrafanaTheme2, - DataLinkPostProcessor, - InterpolateFunction, isBooleanUnit, TimeRange, cacheFieldDisplayNames, @@ -266,43 +263,6 @@ export function getTimezones(timezones: string[] | undefined, defaultTimezone: s return timezones.map((v) => (v?.length ? v : defaultTimezone)); } -export function regenerateLinksSupplier( - alignedDataFrame: DataFrame, - frames: DataFrame[], - replaceVariables: InterpolateFunction, - timeZone: string, - dataLinkPostProcessor?: DataLinkPostProcessor -): DataFrame { - alignedDataFrame.fields.forEach((field) => { - if (field.state?.origin?.frameIndex === undefined || frames[field.state?.origin?.frameIndex] === undefined) { - return; - } - - const tempFields: Field[] = []; - for (const frameField of frames[field.state?.origin?.frameIndex].fields) { - if (frameField.type === FieldType.string) { - tempFields.push(frameField); - } - } - - const tempFrame: DataFrame = { - fields: [...alignedDataFrame.fields, ...tempFields], - length: alignedDataFrame.fields.length + tempFields.length, - }; - - field.getLinks = getLinksSupplier( - tempFrame, - field, - field.state!.scopedVars!, - replaceVariables, - timeZone, - dataLinkPostProcessor - ); - }); - - return alignedDataFrame; -} - export const isTooltipScrollable = (tooltipOptions: VizTooltipOptions) => { return tooltipOptions.mode === TooltipDisplayMode.Multi && tooltipOptions.maxHeight != null; }; diff --git a/public/app/plugins/panel/trend/TrendPanel.tsx b/public/app/plugins/panel/trend/TrendPanel.tsx index 4d8d20db97..cb90f861f3 100644 --- a/public/app/plugins/panel/trend/TrendPanel.tsx +++ b/public/app/plugins/panel/trend/TrendPanel.tsx @@ -11,7 +11,7 @@ import { TimeSeries } from 'app/core/components/TimeSeries/TimeSeries'; import { findFieldIndex } from 'app/features/dimensions'; import { TimeSeriesTooltip } from '../timeseries/TimeSeriesTooltip'; -import { isTooltipScrollable, prepareGraphableFields, regenerateLinksSupplier } from '../timeseries/utils'; +import { isTooltipScrollable, prepareGraphableFields } from '../timeseries/utils'; import { Options } from './panelcfg.gen'; @@ -109,18 +109,10 @@ export const TrendPanel = ({ legend={options.legend} options={options} preparePlotFrame={preparePlotFrameTimeless} + replaceVariables={replaceVariables} + dataLinkPostProcessor={dataLinkPostProcessor} > {(uPlotConfig, alignedDataFrame) => { - if (alignedDataFrame.fields.some((f) => Boolean(f.config.links?.length))) { - alignedDataFrame = regenerateLinksSupplier( - alignedDataFrame, - info.frames!, - replaceVariables, - timeZone, - dataLinkPostProcessor - ); - } - return ( <> diff --git a/public/app/plugins/panel/xychart/XYChartTooltip.test.tsx b/public/app/plugins/panel/xychart/XYChartTooltip.test.tsx index f9b301b064..b899962059 100644 --- a/public/app/plugins/panel/xychart/XYChartTooltip.test.tsx +++ b/public/app/plugins/panel/xychart/XYChartTooltip.test.tsx @@ -154,7 +154,15 @@ function buildData({ dataLinkTitle = 'Grafana', field1Name = 'field_1', field2Na { name: field2Name, type: FieldType.number, - config: {}, + config: { + links: [ + { + title: dataLinkTitle, + targetBlank: true, + url: 'http://www.someWebsite.com', + }, + ], + }, values: [500, 300, 150, 250, 600, 500, 700, 400, 540, 630, 460, 250, 500, 400, 800, 930, 360], getLinks: (_config: ValueLinkConfig) => [ { From d7b031f318db98532ff15bedfc9ae36b406a4e2e Mon Sep 17 00:00:00 2001 From: Todd Treece <360020+toddtreece@users.noreply.github.com> Date: Thu, 29 Feb 2024 17:22:18 -0500 Subject: [PATCH 0315/1406] Chore: Update Makefile to support go workspace (#83549) --- .drone.yml | 4 +++- Makefile | 5 +++-- scripts/drone/events/pr.star | 2 ++ 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.drone.yml b/.drone.yml index 3ca0229613..5c23955586 100644 --- a/.drone.yml +++ b/.drone.yml @@ -366,6 +366,7 @@ trigger: - docs/** - '*.md' include: + - Makefile - pkg/** - packaging/** - .drone.yml @@ -462,6 +463,7 @@ trigger: - docs/** - '*.md' include: + - Makefile - pkg/** - packaging/** - .drone.yml @@ -4924,6 +4926,6 @@ kind: secret name: gcr_credentials --- kind: signature -hmac: 959c920d1ce1f36b68e93dd4b4de180dd64ae58c28b9eb52ee1d343f991f5cfd +hmac: 38be1b6248aae943e74ffd05c822379dc6ce1d1f08f681316d26be7740470a37 ... diff --git a/Makefile b/Makefile index 79fd4a84ea..24ad02da4d 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,7 @@ include .bingo/Variables.mk .PHONY: all deps-go deps-js deps build-go build-backend build-server build-cli build-js build build-docker-full build-docker-full-ubuntu lint-go golangci-lint test-go test-js gen-ts test run run-frontend clean devenv devenv-down protobuf drone help gen-go gen-cue fix-cue GO = go -GO_FILES ?= ./pkg/... +GO_FILES ?= ./pkg/... ./pkg/apiserver/... ./pkg/apimachinery/... SH_FILES ?= $(shell find ./scripts -name *.sh) GO_BUILD_FLAGS += $(if $(GO_BUILD_DEV),-dev) GO_BUILD_FLAGS += $(if $(GO_BUILD_TAGS),-build-tags=$(GO_BUILD_TAGS)) @@ -167,7 +167,8 @@ test-go: test-go-unit test-go-integration .PHONY: test-go-unit test-go-unit: ## Run unit tests for backend with flags. @echo "test backend unit tests" - $(GO) test -short -covermode=atomic -timeout=30m ./pkg/... + go list -f '{{.Dir}}/...' -m | xargs \ + $(GO) test -short -covermode=atomic -timeout=30m .PHONY: test-go-integration test-go-integration: ## Run integration tests for backend with flags. diff --git a/scripts/drone/events/pr.star b/scripts/drone/events/pr.star index 08472c2059..fb9f28e453 100644 --- a/scripts/drone/events/pr.star +++ b/scripts/drone/events/pr.star @@ -96,6 +96,7 @@ def pr_pipelines(): test_backend( get_pr_trigger( include_paths = [ + "Makefile", "pkg/**", "packaging/**", ".drone.yml", @@ -112,6 +113,7 @@ def pr_pipelines(): lint_backend_pipeline( get_pr_trigger( include_paths = [ + "Makefile", "pkg/**", "packaging/**", ".drone.yml", From 74115f1f08e3a6aca65384a43ff28b9540c647f9 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Thu, 29 Feb 2024 14:58:49 -0800 Subject: [PATCH 0316/1406] Chore: fix apiserver integration tests (#83724) Co-authored-by: Todd Treece --- pkg/tests/apis/dashboard/dashboards_test.go | 9 ++++-- .../apis/dashboardsnapshot/snapshots_test.go | 11 ++++++-- pkg/tests/apis/datasource/testdata_test.go | 9 ++++-- pkg/tests/apis/example/example_test.go | 16 +++++++++-- pkg/tests/apis/folder/folders_test.go | 20 +++++++++++-- pkg/tests/apis/helper.go | 28 ++++++++++++++----- pkg/tests/apis/playlist/playlist_test.go | 7 ++++- pkg/tests/apis/query/query_test.go | 7 ++++- 8 files changed, 85 insertions(+), 22 deletions(-) diff --git a/pkg/tests/apis/dashboard/dashboards_test.go b/pkg/tests/apis/dashboard/dashboards_test.go index 92010b98cd..a3859c084d 100644 --- a/pkg/tests/apis/dashboard/dashboards_test.go +++ b/pkg/tests/apis/dashboard/dashboards_test.go @@ -8,9 +8,14 @@ import ( "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/tests/apis" "github.com/grafana/grafana/pkg/tests/testinfra" + "github.com/grafana/grafana/pkg/tests/testsuite" ) -func TestRequiresDevMode(t *testing.T) { +func TestMain(m *testing.M) { + testsuite.Run(m) +} + +func TestIntegrationRequiresDevMode(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } @@ -26,7 +31,7 @@ func TestRequiresDevMode(t *testing.T) { require.Error(t, err) } -func TestDashboardsApp(t *testing.T) { +func TestIntegrationDashboardsApp(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } diff --git a/pkg/tests/apis/dashboardsnapshot/snapshots_test.go b/pkg/tests/apis/dashboardsnapshot/snapshots_test.go index c25c4029d0..bf787a7916 100644 --- a/pkg/tests/apis/dashboardsnapshot/snapshots_test.go +++ b/pkg/tests/apis/dashboardsnapshot/snapshots_test.go @@ -8,9 +8,14 @@ import ( "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/tests/apis" "github.com/grafana/grafana/pkg/tests/testinfra" + "github.com/grafana/grafana/pkg/tests/testsuite" ) -func TestDashboardSnapshots(t *testing.T) { +func TestMain(m *testing.M) { + testsuite.Run(m) +} + +func TestIntegrationDashboardSnapshots(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } @@ -31,14 +36,14 @@ func TestDashboardSnapshots(t *testing.T) { "freshness": "Current", "resources": [ { - "resource": "dashboardsnapshot", + "resource": "dashboardsnapshots", "responseKind": { "group": "", "kind": "DashboardSnapshot", "version": "" }, "scope": "Namespaced", - "singularResource": "dashsnap", + "singularResource": "dashboardsnapshot", "subresources": [ { "responseKind": { diff --git a/pkg/tests/apis/datasource/testdata_test.go b/pkg/tests/apis/datasource/testdata_test.go index b0449903fe..c9f1e64b95 100644 --- a/pkg/tests/apis/datasource/testdata_test.go +++ b/pkg/tests/apis/datasource/testdata_test.go @@ -12,9 +12,14 @@ import ( "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/tests/apis" "github.com/grafana/grafana/pkg/tests/testinfra" + "github.com/grafana/grafana/pkg/tests/testsuite" ) -func TestTestDatasource(t *testing.T) { +func TestMain(m *testing.M) { + testsuite.Run(m) +} + +func TestIntegrationTestDatasource(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } @@ -70,7 +75,7 @@ func TestTestDatasource(t *testing.T) { { "responseKind": { "group": "", - "kind": "Status", + "kind": "QueryDataResponse", "version": "" }, "subresource": "query", diff --git a/pkg/tests/apis/example/example_test.go b/pkg/tests/apis/example/example_test.go index b84db254f2..c702402837 100644 --- a/pkg/tests/apis/example/example_test.go +++ b/pkg/tests/apis/example/example_test.go @@ -8,14 +8,20 @@ import ( "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/tests/apis" "github.com/grafana/grafana/pkg/tests/testinfra" + "github.com/grafana/grafana/pkg/tests/testsuite" ) -func TestExampleApp(t *testing.T) { +func TestMain(m *testing.M) { + testsuite.Run(m) +} + +func TestIntegrationExampleApp(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } @@ -49,7 +55,7 @@ func TestExampleApp(t *testing.T) { v1Disco, err := json.MarshalIndent(resources, "", " ") require.NoError(t, err) - // fmt.Printf("%s", string(v1Disco)) + //fmt.Printf("%s", string(v1Disco)) require.JSONEq(t, `{ "kind": "APIResourceList", @@ -147,7 +153,11 @@ func TestExampleApp(t *testing.T) { rsp, err := client.Get(context.Background(), "test2", metav1.GetOptions{}) require.NoError(t, err) - require.Equal(t, "dummy: test2", rsp.Object["spec"]) + v, ok, err := unstructured.NestedString(rsp.Object, "spec", "Dummy") + require.NoError(t, err) + require.True(t, ok) + + require.Equal(t, "test2", v) require.Equal(t, "DummyResource", rsp.GetObjectKind().GroupVersionKind().Kind) // Now a sub-resource diff --git a/pkg/tests/apis/folder/folders_test.go b/pkg/tests/apis/folder/folders_test.go index 25d8b1504e..1dbbddf3ff 100644 --- a/pkg/tests/apis/folder/folders_test.go +++ b/pkg/tests/apis/folder/folders_test.go @@ -9,9 +9,14 @@ import ( "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/tests/apis" "github.com/grafana/grafana/pkg/tests/testinfra" + "github.com/grafana/grafana/pkg/tests/testsuite" ) -func TestFoldersApp(t *testing.T) { +func TestMain(m *testing.M) { + testsuite.Run(m) +} + +func TestIntegrationFoldersApp(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } @@ -51,10 +56,19 @@ func TestFoldersApp(t *testing.T) { ] }, { - "name": "folders/children", + "name": "folders/access", "singularName": "", "namespaced": true, - "kind": "FolderInfoList", + "kind": "FolderAccessInfo", + "verbs": [ + "get" + ] + }, + { + "name": "folders/count", + "singularName": "", + "namespaced": true, + "kind": "DescendantCounts", "verbs": [ "get" ] diff --git a/pkg/tests/apis/helper.go b/pkg/tests/apis/helper.go index d0fca227bc..f1712e4701 100644 --- a/pkg/tests/apis/helper.go +++ b/pkg/tests/apis/helper.go @@ -9,6 +9,7 @@ import ( "net/http" "os" "testing" + "time" "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/api/errors" @@ -54,6 +55,7 @@ func NewK8sTestHelper(t *testing.T, opts testinfra.GrafanaOpts) *K8sTestHelper { t.Helper() dir, path := testinfra.CreateGrafDir(t, opts) _, env := testinfra.StartGrafanaEnv(t, dir, path) + c := &K8sTestHelper{ env: *env, t: t, @@ -63,16 +65,28 @@ func NewK8sTestHelper(t *testing.T, opts testinfra.GrafanaOpts) *K8sTestHelper { c.Org1 = c.createTestUsers("Org1") c.OrgB = c.createTestUsers("OrgB") - // Read the API groups - rsp := DoRequest(c, RequestParams{ - User: c.Org1.Viewer, - Path: "/apis", - // Accept: "application/json;g=apidiscovery.k8s.io;v=v2beta1;as=APIGroupDiscoveryList,application/json", - }, &metav1.APIGroupList{}) - c.groups = rsp.Result.Groups + c.loadAPIGroups() + return c } +func (c *K8sTestHelper) loadAPIGroups() { + for { + rsp := DoRequest(c, RequestParams{ + User: c.Org1.Viewer, + Path: "/apis", + // Accept: "application/json;g=apidiscovery.k8s.io;v=v2beta1;as=APIGroupDiscoveryList,application/json", + }, &metav1.APIGroupList{}) + + if rsp.Response.StatusCode == http.StatusOK { + c.groups = rsp.Result.Groups + return + } + + time.Sleep(100 * time.Millisecond) + } +} + func (c *K8sTestHelper) Shutdown() { err := c.env.Server.Shutdown(context.Background(), "done") require.NoError(c.t, err) diff --git a/pkg/tests/apis/playlist/playlist_test.go b/pkg/tests/apis/playlist/playlist_test.go index 25919c4964..91ec2f1d23 100644 --- a/pkg/tests/apis/playlist/playlist_test.go +++ b/pkg/tests/apis/playlist/playlist_test.go @@ -18,15 +18,20 @@ import ( "github.com/grafana/grafana/pkg/services/playlist" "github.com/grafana/grafana/pkg/tests/apis" "github.com/grafana/grafana/pkg/tests/testinfra" + "github.com/grafana/grafana/pkg/tests/testsuite" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + var gvr = schema.GroupVersionResource{ Group: "playlist.grafana.app", Version: "v0alpha1", Resource: "playlists", } -func TestPlaylist(t *testing.T) { +func TestIntegrationPlaylist(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } diff --git a/pkg/tests/apis/query/query_test.go b/pkg/tests/apis/query/query_test.go index 8a70de8798..772f21a934 100644 --- a/pkg/tests/apis/query/query_test.go +++ b/pkg/tests/apis/query/query_test.go @@ -16,9 +16,14 @@ import ( "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/tests/apis" "github.com/grafana/grafana/pkg/tests/testinfra" + "github.com/grafana/grafana/pkg/tests/testsuite" ) -func TestSimpleQuery(t *testing.T) { +func TestMain(m *testing.M) { + testsuite.Run(m) +} + +func TestIntegrationSimpleQuery(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } From 88ebef5cba2529634357092595613226691e9f81 Mon Sep 17 00:00:00 2001 From: Tim Levett Date: Thu, 29 Feb 2024 16:59:40 -0600 Subject: [PATCH 0317/1406] Transformations: Add substring matcher to the 'Filter by Value' transformation (#83548) --- .../src/transformations/matchers.ts | 2 + .../src/transformations/matchers/ids.ts | 2 + .../valueMatchers/substringMatchers.test.ts | 130 ++++++++++++++++++ .../valueMatchers/substringMatchers.ts | 41 ++++++ .../ValueMatchers/BasicMatcherEditor.tsx | 14 ++ 5 files changed, 189 insertions(+) create mode 100644 packages/grafana-data/src/transformations/matchers/valueMatchers/substringMatchers.test.ts create mode 100644 packages/grafana-data/src/transformations/matchers/valueMatchers/substringMatchers.ts diff --git a/packages/grafana-data/src/transformations/matchers.ts b/packages/grafana-data/src/transformations/matchers.ts index 3ad60ca07c..21ec33dda2 100644 --- a/packages/grafana-data/src/transformations/matchers.ts +++ b/packages/grafana-data/src/transformations/matchers.ts @@ -21,6 +21,7 @@ import { getNullValueMatchers } from './matchers/valueMatchers/nullMatchers'; import { getNumericValueMatchers } from './matchers/valueMatchers/numericMatchers'; import { getRangeValueMatchers } from './matchers/valueMatchers/rangeMatchers'; import { getRegexValueMatcher } from './matchers/valueMatchers/regexMatchers'; +import { getSubstringValueMatchers } from './matchers/valueMatchers/substringMatchers'; export { type FieldValueMatcherConfig } from './matchers/fieldValueMatcher'; @@ -59,6 +60,7 @@ export const valueMatchers = new Registry(() => { ...getNullValueMatchers(), ...getNumericValueMatchers(), ...getEqualValueMatchers(), + ...getSubstringValueMatchers(), ...getRangeValueMatchers(), ...getRegexValueMatcher(), ]; diff --git a/packages/grafana-data/src/transformations/matchers/ids.ts b/packages/grafana-data/src/transformations/matchers/ids.ts index 5830e62f5f..415e4fe21a 100644 --- a/packages/grafana-data/src/transformations/matchers/ids.ts +++ b/packages/grafana-data/src/transformations/matchers/ids.ts @@ -52,5 +52,7 @@ export enum ValueMatcherID { lowerOrEqual = 'lowerOrEqual', equal = 'equal', notEqual = 'notEqual', + substring = 'substring', + notSubstring = 'notSubstring', between = 'between', } diff --git a/packages/grafana-data/src/transformations/matchers/valueMatchers/substringMatchers.test.ts b/packages/grafana-data/src/transformations/matchers/valueMatchers/substringMatchers.test.ts new file mode 100644 index 0000000000..f9f7789cd6 --- /dev/null +++ b/packages/grafana-data/src/transformations/matchers/valueMatchers/substringMatchers.test.ts @@ -0,0 +1,130 @@ +import { toDataFrame } from '../../../dataframe'; +import { DataFrame } from '../../../types/dataFrame'; +import { getValueMatcher } from '../../matchers'; +import { ValueMatcherID } from '../ids'; + +describe('value substring to matcher', () => { + const data: DataFrame[] = [ + toDataFrame({ + fields: [ + { + name: 'temp', + values: ['24', null, '10', 'asd', '42'], + }, + ], + }), + ]; + + const matcher = getValueMatcher({ + id: ValueMatcherID.substring, + options: { + value: '2', + }, + }); + + it('should match when option value is a substring', () => { + const frame = data[0]; + const field = frame.fields[0]; + const valueIndex = 0; + + expect(matcher(valueIndex, field, frame, data)).toBeTruthy(); + }); + + // Added for https://github.com/grafana/grafana/pull/83548#pullrequestreview-1904931540 where the matcher was not handling null values + it('should be a mismatch if the option is null and should not cause errors', () => { + const frame = data[0]; + const field = frame.fields[0]; + const valueIndex = 1; + + expect(matcher(valueIndex, field, frame, data)).toBeFalsy(); + }); + + it('should not match when option value is different', () => { + const frame = data[0]; + const field = frame.fields[0]; + const valueIndex = 2; + + expect(matcher(valueIndex, field, frame, data)).toBeFalsy(); + }); + + it('should match when option value is a substring', () => { + const frame = data[0]; + const field = frame.fields[0]; + const valueIndex = 4; + + expect(matcher(valueIndex, field, frame, data)).toBeTruthy(); + }); +}); + +describe('value not substring matcher', () => { + const data: DataFrame[] = [ + toDataFrame({ + fields: [ + { + name: 'temp', + values: ['24', null, '050', 'asd', '42', '0'], + }, + ], + }), + ]; + + const matcher = getValueMatcher({ + id: ValueMatcherID.notSubstring, + options: { + value: '5', + }, + }); + + it('should not match if the value is "0" and the option value is "0"', () => { + const frame = data[0]; + const field = frame.fields[0]; + const valueIndex = 5; + + const zeroMatcher = getValueMatcher({ + id: ValueMatcherID.notSubstring, + options: { + value: '0', + }, + }); + + expect(zeroMatcher(valueIndex, field, frame, data)).toBeFalsy(); + }); + + it('should match when option value is a substring', () => { + const frame = data[0]; + const field = frame.fields[0]; + const valueIndex = 0; + + expect(matcher(valueIndex, field, frame, data)).toBeTruthy(); + }); + + it('should not match when option value is different', () => { + const frame = data[0]; + const field = frame.fields[0]; + const valueIndex = 2; + + expect(matcher(valueIndex, field, frame, data)).toBeFalsy(); + }); + + it('should match when value is null because null its not a substring', () => { + const frame = data[0]; + const field = frame.fields[0]; + const valueIndex = 4; + + expect(matcher(valueIndex, field, frame, data)).toBeTruthy(); + }); + + it('it should not match if the option value is empty string', () => { + const frame = data[0]; + const field = frame.fields[0]; + const valueIndex = 0; + const emptyMatcher = getValueMatcher({ + id: ValueMatcherID.notSubstring, + options: { + value: '', + }, + }); + + expect(emptyMatcher(valueIndex, field, frame, data)).toBeFalsy(); + }); +}); diff --git a/packages/grafana-data/src/transformations/matchers/valueMatchers/substringMatchers.ts b/packages/grafana-data/src/transformations/matchers/valueMatchers/substringMatchers.ts new file mode 100644 index 0000000000..034e4067ee --- /dev/null +++ b/packages/grafana-data/src/transformations/matchers/valueMatchers/substringMatchers.ts @@ -0,0 +1,41 @@ +import { Field, FieldType } from '../../../types/dataFrame'; +import { ValueMatcherInfo } from '../../../types/transformations'; +import { ValueMatcherID } from '../ids'; + +import { BasicValueMatcherOptions } from './types'; + +const isSubstringMatcher: ValueMatcherInfo = { + id: ValueMatcherID.substring, + name: 'Contains Substring', + description: 'Match where value for given field is a substring to options value.', + get: (options) => { + return (valueIndex: number, field: Field) => { + const value = field.values[valueIndex]; + return (value && value.includes(options.value)) || options.value === ''; + }; + }, + getOptionsDisplayText: () => { + return `Matches all rows where field is similar to the value.`; + }, + isApplicable: (field) => field.type === FieldType.string, + getDefaultOptions: () => ({ value: '' }), +}; + +const isNotSubstringValueMatcher: ValueMatcherInfo = { + id: ValueMatcherID.notSubstring, + name: 'Does not contain substring', + description: 'Match where value for given field is not a substring to options value.', + get: (options) => { + return (valueIndex: number, field: Field) => { + const value = field.values[valueIndex]; + return typeof value === 'string' && options.value !== '' && !value.includes(options.value); + }; + }, + getOptionsDisplayText: () => { + return `Matches all rows where field is not similar to the value.`; + }, + isApplicable: (field) => field.type === FieldType.string, + getDefaultOptions: () => ({ value: '' }), +}; + +export const getSubstringValueMatchers = (): ValueMatcherInfo[] => [isSubstringMatcher, isNotSubstringValueMatcher]; diff --git a/public/app/features/transformers/FilterByValueTransformer/ValueMatchers/BasicMatcherEditor.tsx b/public/app/features/transformers/FilterByValueTransformer/ValueMatchers/BasicMatcherEditor.tsx index e80b2cbe2b..eadbc0b50e 100644 --- a/public/app/features/transformers/FilterByValueTransformer/ValueMatchers/BasicMatcherEditor.tsx +++ b/public/app/features/transformers/FilterByValueTransformer/ValueMatchers/BasicMatcherEditor.tsx @@ -127,5 +127,19 @@ export const getBasicValueMatchersUI = (): Array true, }), }, + { + name: 'Is Substring', + id: ValueMatcherID.substring, + component: basicMatcherEditor({ + validator: () => true, + }), + }, + { + name: 'Is not substring', + id: ValueMatcherID.notSubstring, + component: basicMatcherEditor({ + validator: () => true, + }), + }, ]; }; From 859ecf2a3491b109dedefeabf3546aebcc470e11 Mon Sep 17 00:00:00 2001 From: Leon Sorokin Date: Thu, 29 Feb 2024 17:28:37 -0600 Subject: [PATCH 0318/1406] VizTooltips: Render data links only when anchored (#83737) --- .../state-timeline/StateTimelineTooltip2.tsx | 24 +++++++++---------- .../panel/timeseries/TimeSeriesTooltip.tsx | 24 +++++++++---------- .../plugins/panel/xychart/XYChartTooltip.tsx | 12 +++++++--- 3 files changed, 31 insertions(+), 29 deletions(-) diff --git a/public/app/plugins/panel/state-timeline/StateTimelineTooltip2.tsx b/public/app/plugins/panel/state-timeline/StateTimelineTooltip2.tsx index 686a67331d..8c2af2b7c8 100644 --- a/public/app/plugins/panel/state-timeline/StateTimelineTooltip2.tsx +++ b/public/app/plugins/panel/state-timeline/StateTimelineTooltip2.tsx @@ -1,6 +1,6 @@ -import React from 'react'; +import React, { ReactNode } from 'react'; -import { Field, FieldType, getFieldDisplayName, LinkModel, TimeRange } from '@grafana/data'; +import { FieldType, getFieldDisplayName, TimeRange } from '@grafana/data'; import { SortOrder } from '@grafana/schema/dist/esm/common/common.gen'; import { TooltipDisplayMode, useStyles2 } from '@grafana/ui'; import { VizTooltipContent } from '@grafana/ui/src/components/VizTooltip/VizTooltipContent'; @@ -65,12 +65,14 @@ export const StateTimelineTooltip2 = ({ contentItems.push({ label: 'Duration', value: duration }); } - let links: Array> = []; + let footer: ReactNode; - if (seriesIdx != null) { + if (isPinned && seriesIdx != null) { const field = seriesFrame.fields[seriesIdx]; const dataIdx = dataIdxs[seriesIdx]!; - links = getDataLinks(field, dataIdx); + const links = getDataLinks(field, dataIdx); + + footer = ; } const headerItem: VizTooltipItem = { @@ -79,14 +81,10 @@ export const StateTimelineTooltip2 = ({ }; return ( -
-
- - - {(links.length > 0 || isPinned) && ( - - )} -
+
+ + + {footer}
); }; diff --git a/public/app/plugins/panel/timeseries/TimeSeriesTooltip.tsx b/public/app/plugins/panel/timeseries/TimeSeriesTooltip.tsx index d703ba1719..d4484973dd 100644 --- a/public/app/plugins/panel/timeseries/TimeSeriesTooltip.tsx +++ b/public/app/plugins/panel/timeseries/TimeSeriesTooltip.tsx @@ -1,7 +1,7 @@ import { css } from '@emotion/css'; -import React from 'react'; +import React, { ReactNode } from 'react'; -import { DataFrame, FieldType, LinkModel, Field, getFieldDisplayName } from '@grafana/data'; +import { DataFrame, FieldType, getFieldDisplayName } from '@grafana/data'; import { SortOrder, TooltipDisplayMode } from '@grafana/schema/dist/esm/common/common.gen'; import { useStyles2 } from '@grafana/ui'; import { VizTooltipContent } from '@grafana/ui/src/components/VizTooltip/VizTooltipContent'; @@ -59,12 +59,14 @@ export const TimeSeriesTooltip = ({ (field) => field.type === FieldType.number ); - let links: Array> = []; + let footer: ReactNode; - if (seriesIdx != null) { + if (isPinned && seriesIdx != null) { const field = seriesFrame.fields[seriesIdx]; const dataIdx = dataIdxs[seriesIdx]!; - links = getDataLinks(field, dataIdx); + const links = getDataLinks(field, dataIdx); + + footer = ; } const headerItem: VizTooltipItem = { @@ -73,14 +75,10 @@ export const TimeSeriesTooltip = ({ }; return ( -
-
- - - {(links.length > 0 || isPinned) && ( - - )} -
+
+ + + {footer}
); }; diff --git a/public/app/plugins/panel/xychart/XYChartTooltip.tsx b/public/app/plugins/panel/xychart/XYChartTooltip.tsx index 5494b3a673..9012ccca99 100644 --- a/public/app/plugins/panel/xychart/XYChartTooltip.tsx +++ b/public/app/plugins/panel/xychart/XYChartTooltip.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { ReactNode } from 'react'; import { DataFrame, Field, getFieldDisplayName } from '@grafana/data'; import { alpha } from '@grafana/data/src/themes/colorManipulator'; @@ -83,13 +83,19 @@ export const XYChartTooltip = ({ dataIdxs, seriesIdx, data, allSeries, dismiss, }); } - const links = getDataLinks(yField, rowIndex); + let footer: ReactNode; + + if (isPinned && seriesIdx != null) { + const links = getDataLinks(yField, rowIndex); + + footer = ; + } return (
- {(links.length > 0 || isPinned) && } + {footer}
); }; From 5d7a979199447d3227151792f360e8a4b2d95027 Mon Sep 17 00:00:00 2001 From: Nathan Marrs Date: Thu, 29 Feb 2024 17:56:40 -0700 Subject: [PATCH 0319/1406] Canvas: Add ability to edit selected connections in the inline editor (#83625) --- .../canvas/editor/inline/InlineEditBody.tsx | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/public/app/plugins/panel/canvas/editor/inline/InlineEditBody.tsx b/public/app/plugins/panel/canvas/editor/inline/InlineEditBody.tsx index d2b54f88af..a5cfdb9b1a 100644 --- a/public/app/plugins/panel/canvas/editor/inline/InlineEditBody.tsx +++ b/public/app/plugins/panel/canvas/editor/inline/InlineEditBody.tsx @@ -18,6 +18,7 @@ import { activePanelSubject, InstanceState } from '../../CanvasPanel'; import { addStandardCanvasEditorOptions } from '../../module'; import { InlineEditTabs } from '../../types'; import { getElementTypes, onAddItem } from '../../utils'; +import { getConnectionEditor } from '../connectionEditor'; import { getElementEditor } from '../element/elementEditor'; import { getLayerEditor } from '../layer/layerEditor'; @@ -42,6 +43,17 @@ export function InlineEditBody() { builder.addNestedOptions(getLayerEditor(instanceState)); } + const selectedConnection = state.selectedConnection; + if (selectedConnection && activeTab === InlineEditTabs.SelectedElement) { + builder.addNestedOptions( + getConnectionEditor({ + category: [`Selected connection`], + connection: selectedConnection, + scene: state.scene, + }) + ); + } + const selection = state.selected; if (selection?.length === 1 && activeTab === InlineEditTabs.SelectedElement) { const element = selection[0]; @@ -82,7 +94,10 @@ export function InlineEditBody() { const rootLayer: FrameState | undefined = instanceState?.layer; const noElementSelected = - instanceState && activeTab === InlineEditTabs.SelectedElement && instanceState.selected.length === 0; + instanceState && + activeTab === InlineEditTabs.SelectedElement && + instanceState.selected.length === 0 && + instanceState.selectedConnection === undefined; return ( <> From b87ec6943143f682c4815293b0734b04f134571d Mon Sep 17 00:00:00 2001 From: Charandas Date: Thu, 29 Feb 2024 17:29:05 -0800 Subject: [PATCH 0320/1406] K8s: add a remote services file config option to specify aggregation config (#83646) --- hack/make-aggregator-pki.sh | 19 ++- pkg/registry/apis/service/register.go | 15 +- pkg/services/apiserver/aggregator/README.md | 17 +- .../apiserver/aggregator/aggregator.go | 152 +++++++++++++++++- pkg/services/apiserver/aggregator/config.go | 37 +++++ .../examples/autoregister/apiservices.yaml | 14 ++ .../{ => manual-test}/apiservice.yaml | 3 +- .../{ => manual-test}/externalname.yaml | 3 +- pkg/services/apiserver/config.go | 3 + pkg/services/apiserver/options/aggregator.go | 8 +- pkg/services/apiserver/service.go | 8 +- 11 files changed, 260 insertions(+), 19 deletions(-) create mode 100644 pkg/services/apiserver/aggregator/config.go create mode 100644 pkg/services/apiserver/aggregator/examples/autoregister/apiservices.yaml rename pkg/services/apiserver/aggregator/examples/{ => manual-test}/apiservice.yaml (94%) rename pkg/services/apiserver/aggregator/examples/{ => manual-test}/externalname.yaml (84%) diff --git a/hack/make-aggregator-pki.sh b/hack/make-aggregator-pki.sh index cffcec1861..0eac9f2ae1 100755 --- a/hack/make-aggregator-pki.sh +++ b/hack/make-aggregator-pki.sh @@ -7,6 +7,21 @@ set -o pipefail rm -rf data/grafana-aggregator mkdir -p data/grafana-aggregator + openssl req -nodes -new -x509 -keyout data/grafana-aggregator/ca.key -out data/grafana-aggregator/ca.crt -openssl req -out data/grafana-aggregator/client.csr -new -newkey rsa:4096 -nodes -keyout data/grafana-aggregator/client.key -subj "/CN=development/O=system:masters" -openssl x509 -req -days 365 -in data/grafana-aggregator/client.csr -CA data/grafana-aggregator/ca.crt -CAkey data/grafana-aggregator/ca.key -set_serial 01 -sha256 -out data/grafana-aggregator/client.crt +openssl req -out data/grafana-aggregator/client.csr -new -newkey rsa:4096 -nodes -keyout data/grafana-aggregator/client.key \ + -subj "/CN=development/O=system:masters" \ + -addext "extendedKeyUsage = clientAuth" +openssl x509 -req -days 365 -in data/grafana-aggregator/client.csr -CA data/grafana-aggregator/ca.crt -CAkey data/grafana-aggregator/ca.key \ + -set_serial 01 \ + -sha256 -out data/grafana-aggregator/client.crt \ + -copy_extensions=copyall + +openssl req -out data/grafana-aggregator/server.csr -new -newkey rsa:4096 -nodes -keyout data/grafana-aggregator/server.key \ + -subj "/CN=localhost/O=aggregated" \ + -addext "subjectAltName = DNS:v0alpha1.example.grafana.app.default.svc,DNS:localhost" \ + -addext "extendedKeyUsage = serverAuth, clientAuth" +openssl x509 -req -days 365 -in data/grafana-aggregator/server.csr -CA data/grafana-aggregator/ca.crt -CAkey data/grafana-aggregator/ca.key \ + -set_serial 02 \ + -sha256 -out data/grafana-aggregator/server.crt \ + -copy_extensions=copyall diff --git a/pkg/registry/apis/service/register.go b/pkg/registry/apis/service/register.go index 2032b8ea0e..17dbc3b07a 100644 --- a/pkg/registry/apis/service/register.go +++ b/pkg/registry/apis/service/register.go @@ -43,6 +43,13 @@ func (b *ServiceAPIBuilder) GetGroupVersion() schema.GroupVersion { return service.SchemeGroupVersion } +func addKnownTypes(scheme *runtime.Scheme, gv schema.GroupVersion) { + scheme.AddKnownTypes(gv, + &service.ExternalName{}, + &service.ExternalNameList{}, + ) +} + func (b *ServiceAPIBuilder) InstallSchema(scheme *runtime.Scheme) error { gv := service.SchemeGroupVersion err := service.AddToScheme(scheme) @@ -53,10 +60,10 @@ func (b *ServiceAPIBuilder) InstallSchema(scheme *runtime.Scheme) error { // Link this version to the internal representation. // This is used for server-side-apply (PATCH), and avoids the error: // "no kind is registered for the type" - // addKnownTypes(scheme, schema.GroupVersion{ - // Group: service.GROUP, - // Version: runtime.APIVersionInternal, - // }) + addKnownTypes(scheme, schema.GroupVersion{ + Group: service.GROUP, + Version: runtime.APIVersionInternal, + }) metav1.AddToGroupVersion(scheme, gv) return scheme.SetVersionPriority(gv) } diff --git a/pkg/services/apiserver/aggregator/README.md b/pkg/services/apiserver/aggregator/README.md index 3537e723f4..5216b23873 100644 --- a/pkg/services/apiserver/aggregator/README.md +++ b/pkg/services/apiserver/aggregator/README.md @@ -77,7 +77,7 @@ configuration overwrites on startup. 4. In another tab, apply the manifests: ```shell export KUBECONFIG=$PWD/data/grafana-apiserver/grafana.kubeconfig - kubectl apply -f ./pkg/services/apiserver/aggregator/examples/ + kubectl apply -f ./pkg/services/apiserver/aggregator/examples/manual-test/ # SAMPLE OUTPUT # apiservice.apiregistration.k8s.io/v0alpha1.example.grafana.app created # externalname.service.grafana.app/example-apiserver created @@ -92,6 +92,8 @@ configuration overwrites on startup. go run ./pkg/cmd/grafana apiserver \ --runtime-config=example.grafana.app/v0alpha1=true \ --secure-port 7443 \ + --tls-cert-file $PWD/data/grafana-aggregator/server.crt \ + --tls-private-key-file $PWD/data/grafana-aggregator/server.key \ --requestheader-client-ca-file=$PWD/data/grafana-aggregator/ca.crt \ --requestheader-extra-headers-prefix=X-Remote-Extra- \ --requestheader-group-headers=X-Remote-Group \ @@ -110,3 +112,16 @@ configuration overwrites on startup. ```shell kubectl delete -f ./pkg/services/apiserver/aggregator/examples/ ``` + +## Testing auto-registration of remote services locally + +A sample aggregation config for remote services is provided under [conf](../../../../conf/aggregation/apiservices.yaml). Provided, you have the following setup in your custom.ini, the apiserver will +register your remotely running services on startup. + +```ini +; in custom.ini +; the bundle is only used when not in dev mode +apiservice_ca_bundle_file = ./data/grafana-aggregator/ca.crt + +remote_services_file = ./pkg/services/apiserver/aggregator/examples/autoregister/apiservices.yaml +``` diff --git a/pkg/services/apiserver/aggregator/aggregator.go b/pkg/services/apiserver/aggregator/aggregator.go index df68b6b39f..708efff204 100644 --- a/pkg/services/apiserver/aggregator/aggregator.go +++ b/pkg/services/apiserver/aggregator/aggregator.go @@ -12,13 +12,18 @@ package aggregator import ( + "context" "crypto/tls" "fmt" + "io" "net/http" + "os" "strings" "sync" "time" + servicev0alpha1 "github.com/grafana/grafana/pkg/apis/service/v0alpha1" + "gopkg.in/yaml.v3" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" utilnet "k8s.io/apimachinery/pkg/util/net" @@ -37,19 +42,68 @@ import ( apiregistrationInformers "k8s.io/kube-aggregator/pkg/client/informers/externalversions/apiregistration/v1" "k8s.io/kube-aggregator/pkg/controllers/autoregister" + servicev0alpha1applyconfiguration "github.com/grafana/grafana/pkg/generated/applyconfiguration/service/v0alpha1" serviceclientset "github.com/grafana/grafana/pkg/generated/clientset/versioned" informersv0alpha1 "github.com/grafana/grafana/pkg/generated/informers/externalversions" "github.com/grafana/grafana/pkg/services/apiserver/options" ) -func CreateAggregatorConfig(commandOptions *options.Options, sharedConfig genericapiserver.RecommendedConfig) (*aggregatorapiserver.Config, informersv0alpha1.SharedInformerFactory, error) { +func readCABundlePEM(path string, devMode bool) ([]byte, error) { + if devMode { + return nil, nil + } + + // We can ignore the gosec G304 warning on this one because `path` comes + // from Grafana configuration (commandOptions.AggregatorOptions.APIServiceCABundleFile) + //nolint:gosec + f, err := os.Open(path) + if err != nil { + return nil, err + } + defer func() { + if err := f.Close(); err != nil { + klog.Errorf("error closing remote services file: %s", err) + } + }() + + return io.ReadAll(f) +} + +func readRemoteServices(path string) ([]RemoteService, error) { + // We can ignore the gosec G304 warning on this one because `path` comes + // from Grafana configuration (commandOptions.AggregatorOptions.RemoteServicesFile) + //nolint:gosec + f, err := os.Open(path) + if err != nil { + return nil, err + } + defer func() { + if err := f.Close(); err != nil { + klog.Errorf("error closing remote services file: %s", err) + } + }() + + rawRemoteServices, err := io.ReadAll(f) + if err != nil { + return nil, err + } + + remoteServices := make([]RemoteService, 0) + if err := yaml.Unmarshal(rawRemoteServices, &remoteServices); err != nil { + return nil, err + } + + return remoteServices, nil +} + +func CreateAggregatorConfig(commandOptions *options.Options, sharedConfig genericapiserver.RecommendedConfig, externalNamesNamespace string) (*Config, error) { // Create a fake clientset and informers for the k8s v1 API group. // These are not used in grafana's aggregator because v1 APIs are not available. fakev1Informers := informers.NewSharedInformerFactory(fake.NewSimpleClientset(), 10*time.Minute) serviceClient, err := serviceclientset.NewForConfig(sharedConfig.LoopbackClientConfig) if err != nil { - return nil, nil, err + return nil, err } sharedInformerFactory := informersv0alpha1.NewSharedInformerFactory( serviceClient, @@ -74,13 +128,35 @@ func CreateAggregatorConfig(commandOptions *options.Options, sharedConfig generi } if err := commandOptions.AggregatorOptions.ApplyTo(aggregatorConfig, commandOptions.RecommendedOptions.Etcd, commandOptions.StorageOptions.DataPath); err != nil { - return nil, nil, err + return nil, err } - return aggregatorConfig, sharedInformerFactory, nil + // Exit early, if no remote services file is configured + if commandOptions.AggregatorOptions.RemoteServicesFile == "" { + return NewConfig(aggregatorConfig, sharedInformerFactory, nil), nil + } + + caBundlePEM, err := readCABundlePEM(commandOptions.AggregatorOptions.APIServiceCABundleFile, commandOptions.ExtraOptions.DevMode) + if err != nil { + return nil, err + } + remoteServices, err := readRemoteServices(commandOptions.AggregatorOptions.RemoteServicesFile) + if err != nil { + return nil, err + } + + remoteServicesConfig := &RemoteServicesConfig{ + InsecureSkipTLSVerify: commandOptions.ExtraOptions.DevMode, + ExternalNamesNamespace: externalNamesNamespace, + CABundle: caBundlePEM, + Services: remoteServices, + serviceClientSet: serviceClient, + } + + return NewConfig(aggregatorConfig, sharedInformerFactory, remoteServicesConfig), nil } -func CreateAggregatorServer(aggregatorConfig *aggregatorapiserver.Config, sharedInformerFactory informersv0alpha1.SharedInformerFactory, delegateAPIServer genericapiserver.DelegationTarget) (*aggregatorapiserver.APIAggregator, error) { +func CreateAggregatorServer(aggregatorConfig *aggregatorapiserver.Config, sharedInformerFactory informersv0alpha1.SharedInformerFactory, remoteServicesConfig *RemoteServicesConfig, delegateAPIServer genericapiserver.DelegationTarget) (*aggregatorapiserver.APIAggregator, error) { completedConfig := aggregatorConfig.Complete() aggregatorServer, err := completedConfig.NewWithDelegate(delegateAPIServer) if err != nil { @@ -111,6 +187,27 @@ func CreateAggregatorServer(aggregatorConfig *aggregatorapiserver.Config, shared return nil, err } + if remoteServicesConfig != nil { + addRemoteAPIServicesToRegister(remoteServicesConfig, autoRegistrationController) + externalNames := getRemoteExternalNamesToRegister(remoteServicesConfig) + err = aggregatorServer.GenericAPIServer.AddPostStartHook("grafana-apiserver-remote-autoregistration", func(_ genericapiserver.PostStartHookContext) error { + namespacedClient := remoteServicesConfig.serviceClientSet.ServiceV0alpha1().ExternalNames(remoteServicesConfig.ExternalNamesNamespace) + for _, externalName := range externalNames { + _, err := namespacedClient.Apply(context.Background(), externalName, metav1.ApplyOptions{ + FieldManager: "grafana-aggregator", + Force: true, + }) + if err != nil { + return err + } + } + return nil + }) + if err != nil { + return nil, err + } + } + err = aggregatorServer.GenericAPIServer.AddBootSequenceHealthChecks( makeAPIServiceAvailableHealthCheck( "autoregister-completion", @@ -240,6 +337,51 @@ var APIVersionPriorities = map[schema.GroupVersion]Priority{ // Version can be set to 9 (to have space around) for a new group. } +func addRemoteAPIServicesToRegister(config *RemoteServicesConfig, registration autoregister.AutoAPIServiceRegistration) { + for i, service := range config.Services { + port := service.Port + apiService := &v1.APIService{ + ObjectMeta: metav1.ObjectMeta{Name: service.Version + "." + service.Group}, + Spec: v1.APIServiceSpec{ + Group: service.Group, + Version: service.Version, + InsecureSkipTLSVerify: config.InsecureSkipTLSVerify, + CABundle: config.CABundle, + // TODO: Group priority minimum of 1000 more than for local services, figure out a better story + // when we have multiple versions, potentially running in heterogeneous ways (local and remote) + GroupPriorityMinimum: 16000, + VersionPriority: 1 + int32(i), + Service: &v1.ServiceReference{ + Name: service.Version + "." + service.Group, + Namespace: config.ExternalNamesNamespace, + Port: &port, + }, + }, + } + + registration.AddAPIServiceToSyncOnStart(apiService) + } +} + +func getRemoteExternalNamesToRegister(config *RemoteServicesConfig) []*servicev0alpha1applyconfiguration.ExternalNameApplyConfiguration { + externalNames := make([]*servicev0alpha1applyconfiguration.ExternalNameApplyConfiguration, 0) + + for _, service := range config.Services { + host := service.Host + name := service.Version + "." + service.Group + externalName := &servicev0alpha1applyconfiguration.ExternalNameApplyConfiguration{} + externalName.WithAPIVersion(servicev0alpha1.SchemeGroupVersion.String()) + externalName.WithKind("ExternalName") + externalName.WithName(name) + externalName.WithSpec(&servicev0alpha1applyconfiguration.ExternalNameSpecApplyConfiguration{ + Host: &host, + }) + externalNames = append(externalNames, externalName) + } + + return externalNames +} + func apiServicesToRegister(delegateAPIServer genericapiserver.DelegationTarget, registration autoregister.AutoAPIServiceRegistration) []*v1.APIService { apiServices := []*v1.APIService{} diff --git a/pkg/services/apiserver/aggregator/config.go b/pkg/services/apiserver/aggregator/config.go new file mode 100644 index 0000000000..47e3d79129 --- /dev/null +++ b/pkg/services/apiserver/aggregator/config.go @@ -0,0 +1,37 @@ +package aggregator + +import ( + serviceclientset "github.com/grafana/grafana/pkg/generated/clientset/versioned" + informersv0alpha1 "github.com/grafana/grafana/pkg/generated/informers/externalversions" + aggregatorapiserver "k8s.io/kube-aggregator/pkg/apiserver" +) + +type RemoteService struct { + Group string `yaml:"group"` + Version string `yaml:"version"` + Host string `yaml:"host"` + Port int32 `yaml:"port"` +} + +type RemoteServicesConfig struct { + ExternalNamesNamespace string + InsecureSkipTLSVerify bool + CABundle []byte + Services []RemoteService + serviceClientSet *serviceclientset.Clientset +} + +type Config struct { + KubeAggregatorConfig *aggregatorapiserver.Config + Informers informersv0alpha1.SharedInformerFactory + RemoteServicesConfig *RemoteServicesConfig +} + +// remoteServices may be nil, when not using aggregation +func NewConfig(aggregator *aggregatorapiserver.Config, informers informersv0alpha1.SharedInformerFactory, remoteServices *RemoteServicesConfig) *Config { + return &Config{ + aggregator, + informers, + remoteServices, + } +} diff --git a/pkg/services/apiserver/aggregator/examples/autoregister/apiservices.yaml b/pkg/services/apiserver/aggregator/examples/autoregister/apiservices.yaml new file mode 100644 index 0000000000..183afb542b --- /dev/null +++ b/pkg/services/apiserver/aggregator/examples/autoregister/apiservices.yaml @@ -0,0 +1,14 @@ +# NOTE: dev-mode only and governed by presence of non-empty value for cfg["grafana-apiserver"]["remote_services_file"] +# List of sample multi-tenant services to aggregate on startup +- group: example.grafana.app + version: v0alpha1 + host: localhost + port: 7443 +- group: query.grafana.app + version: v0alpha1 + host: localhost + port: 7444 +- group: testdata.datasource.grafana.app + version: v0alpha1 + host: localhost + port: 7445 diff --git a/pkg/services/apiserver/aggregator/examples/apiservice.yaml b/pkg/services/apiserver/aggregator/examples/manual-test/apiservice.yaml similarity index 94% rename from pkg/services/apiserver/aggregator/examples/apiservice.yaml rename to pkg/services/apiserver/aggregator/examples/manual-test/apiservice.yaml index 23aedac622..65cc2a5884 100644 --- a/pkg/services/apiserver/aggregator/examples/apiservice.yaml +++ b/pkg/services/apiserver/aggregator/examples/manual-test/apiservice.yaml @@ -1,3 +1,4 @@ +--- apiVersion: apiregistration.k8s.io/v1 kind: APIService metadata: @@ -11,4 +12,4 @@ spec: service: name: example-apiserver namespace: grafana - port: 7443 \ No newline at end of file + port: 7443 diff --git a/pkg/services/apiserver/aggregator/examples/externalname.yaml b/pkg/services/apiserver/aggregator/examples/manual-test/externalname.yaml similarity index 84% rename from pkg/services/apiserver/aggregator/examples/externalname.yaml rename to pkg/services/apiserver/aggregator/examples/manual-test/externalname.yaml index 1cd09d3856..731782611d 100644 --- a/pkg/services/apiserver/aggregator/examples/externalname.yaml +++ b/pkg/services/apiserver/aggregator/examples/manual-test/externalname.yaml @@ -1,7 +1,8 @@ +--- apiVersion: service.grafana.app/v0alpha1 kind: ExternalName metadata: name: example-apiserver namespace: grafana spec: - host: localhost \ No newline at end of file + host: localhost diff --git a/pkg/services/apiserver/config.go b/pkg/services/apiserver/config.go index dbd6abfa3e..6249793453 100644 --- a/pkg/services/apiserver/config.go +++ b/pkg/services/apiserver/config.go @@ -41,6 +41,9 @@ func applyGrafanaConfig(cfg *setting.Cfg, features featuremgmt.FeatureToggles, o o.AggregatorOptions.ProxyClientCertFile = apiserverCfg.Key("proxy_client_cert_file").MustString("") o.AggregatorOptions.ProxyClientKeyFile = apiserverCfg.Key("proxy_client_key_file").MustString("") + o.AggregatorOptions.APIServiceCABundleFile = apiserverCfg.Key("apiservice_ca_bundle_file").MustString("") + o.AggregatorOptions.RemoteServicesFile = apiserverCfg.Key("remote_services_file").MustString("") + o.RecommendedOptions.Admission = nil o.RecommendedOptions.CoreAPI = nil diff --git a/pkg/services/apiserver/options/aggregator.go b/pkg/services/apiserver/options/aggregator.go index 2ba0736fb0..1d38a76c58 100644 --- a/pkg/services/apiserver/options/aggregator.go +++ b/pkg/services/apiserver/options/aggregator.go @@ -23,9 +23,11 @@ import ( // AggregatorServerOptions contains the state for the aggregator apiserver type AggregatorServerOptions struct { - AlternateDNS []string - ProxyClientCertFile string - ProxyClientKeyFile string + AlternateDNS []string + ProxyClientCertFile string + ProxyClientKeyFile string + RemoteServicesFile string + APIServiceCABundleFile string } func NewAggregatorServerOptions() *AggregatorServerOptions { diff --git a/pkg/services/apiserver/service.go b/pkg/services/apiserver/service.go index c050072249..00ec26b861 100644 --- a/pkg/services/apiserver/service.go +++ b/pkg/services/apiserver/service.go @@ -366,12 +366,16 @@ func (s *service) startAggregator( serverConfig *genericapiserver.RecommendedConfig, server *genericapiserver.GenericAPIServer, ) (*genericapiserver.GenericAPIServer, error) { - aggregatorConfig, aggregatorInformers, err := aggregator.CreateAggregatorConfig(s.options, *serverConfig) + externalNamesNamespace := "default" + if s.cfg.StackID != "" { + externalNamesNamespace = s.cfg.StackID + } + aggregatorConfig, err := aggregator.CreateAggregatorConfig(s.options, *serverConfig, externalNamesNamespace) if err != nil { return nil, err } - aggregatorServer, err := aggregator.CreateAggregatorServer(aggregatorConfig, aggregatorInformers, server) + aggregatorServer, err := aggregator.CreateAggregatorServer(aggregatorConfig.KubeAggregatorConfig, aggregatorConfig.Informers, aggregatorConfig.RemoteServicesConfig, server) if err != nil { return nil, err } From 4dc80945148afdccb28fb8af4cc0c2a1e1e7a775 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 1 Mar 2024 07:59:07 +0000 Subject: [PATCH 0321/1406] I18n: Download translations from Crowdin (#83695) New Crowdin translations by GitHub Action Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- public/locales/de-DE/grafana.json | 28 ++++++++++++++++++++++++++++ public/locales/es-ES/grafana.json | 28 ++++++++++++++++++++++++++++ public/locales/fr-FR/grafana.json | 28 ++++++++++++++++++++++++++++ public/locales/zh-Hans/grafana.json | 28 ++++++++++++++++++++++++++++ 4 files changed, 112 insertions(+) diff --git a/public/locales/de-DE/grafana.json b/public/locales/de-DE/grafana.json index 573845f1b2..b1a2b7efe4 100644 --- a/public/locales/de-DE/grafana.json +++ b/public/locales/de-DE/grafana.json @@ -697,10 +697,35 @@ "link-title": "", "title": "" }, + "connect-modal": { + "body-cloud-stack": "", + "body-get-started": "", + "body-paste-stack": "", + "body-sign-up": "", + "body-token": "", + "body-token-field": "", + "body-token-field-placeholder": "", + "body-token-instructions": "", + "body-url-field": "", + "body-view-stacks": "", + "cancel": "", + "connect": "", + "connecting": "", + "stack-required-error": "", + "title": "", + "token-required-error": "" + }, "cta": { "button": "", "header": "" }, + "disconnect-modal": { + "body": "", + "cancel": "", + "disconnect": "", + "disconnecting": "", + "title": "" + }, "get-started": { "body": "", "configure-pdc-link": "", @@ -751,6 +776,9 @@ "link-title": "", "title": "" }, + "resources": { + "disconnect": "" + }, "token-status": { "active": "", "no-active": "" diff --git a/public/locales/es-ES/grafana.json b/public/locales/es-ES/grafana.json index 3433d90c9f..a5f1f8d1f0 100644 --- a/public/locales/es-ES/grafana.json +++ b/public/locales/es-ES/grafana.json @@ -697,10 +697,35 @@ "link-title": "", "title": "" }, + "connect-modal": { + "body-cloud-stack": "", + "body-get-started": "", + "body-paste-stack": "", + "body-sign-up": "", + "body-token": "", + "body-token-field": "", + "body-token-field-placeholder": "", + "body-token-instructions": "", + "body-url-field": "", + "body-view-stacks": "", + "cancel": "", + "connect": "", + "connecting": "", + "stack-required-error": "", + "title": "", + "token-required-error": "" + }, "cta": { "button": "", "header": "" }, + "disconnect-modal": { + "body": "", + "cancel": "", + "disconnect": "", + "disconnecting": "", + "title": "" + }, "get-started": { "body": "", "configure-pdc-link": "", @@ -751,6 +776,9 @@ "link-title": "", "title": "" }, + "resources": { + "disconnect": "" + }, "token-status": { "active": "", "no-active": "" diff --git a/public/locales/fr-FR/grafana.json b/public/locales/fr-FR/grafana.json index 054e5c34ad..904355c64f 100644 --- a/public/locales/fr-FR/grafana.json +++ b/public/locales/fr-FR/grafana.json @@ -697,10 +697,35 @@ "link-title": "", "title": "" }, + "connect-modal": { + "body-cloud-stack": "", + "body-get-started": "", + "body-paste-stack": "", + "body-sign-up": "", + "body-token": "", + "body-token-field": "", + "body-token-field-placeholder": "", + "body-token-instructions": "", + "body-url-field": "", + "body-view-stacks": "", + "cancel": "", + "connect": "", + "connecting": "", + "stack-required-error": "", + "title": "", + "token-required-error": "" + }, "cta": { "button": "", "header": "" }, + "disconnect-modal": { + "body": "", + "cancel": "", + "disconnect": "", + "disconnecting": "", + "title": "" + }, "get-started": { "body": "", "configure-pdc-link": "", @@ -751,6 +776,9 @@ "link-title": "", "title": "" }, + "resources": { + "disconnect": "" + }, "token-status": { "active": "", "no-active": "" diff --git a/public/locales/zh-Hans/grafana.json b/public/locales/zh-Hans/grafana.json index 8a7192e01c..710616a7fe 100644 --- a/public/locales/zh-Hans/grafana.json +++ b/public/locales/zh-Hans/grafana.json @@ -691,10 +691,35 @@ "link-title": "", "title": "" }, + "connect-modal": { + "body-cloud-stack": "", + "body-get-started": "", + "body-paste-stack": "", + "body-sign-up": "", + "body-token": "", + "body-token-field": "", + "body-token-field-placeholder": "", + "body-token-instructions": "", + "body-url-field": "", + "body-view-stacks": "", + "cancel": "", + "connect": "", + "connecting": "", + "stack-required-error": "", + "title": "", + "token-required-error": "" + }, "cta": { "button": "", "header": "" }, + "disconnect-modal": { + "body": "", + "cancel": "", + "disconnect": "", + "disconnecting": "", + "title": "" + }, "get-started": { "body": "", "configure-pdc-link": "", @@ -745,6 +770,9 @@ "link-title": "", "title": "" }, + "resources": { + "disconnect": "" + }, "token-status": { "active": "", "no-active": "" From 2182cc47acaeae16e7208b2a59ac80ef1e9bba76 Mon Sep 17 00:00:00 2001 From: Jo Date: Fri, 1 Mar 2024 10:14:32 +0100 Subject: [PATCH 0322/1406] LDAP: Fix LDAP users authenticated via auth proxy not being able to use LDAP active sync (#83715) * fix LDAP users authenticated via auth proxy not being able to use ldap sync * simplify id resolution at the cost of no fallthrough * remove unused services * remove unused cache key --- pkg/services/authn/authnimpl/service.go | 2 +- pkg/services/authn/clients/proxy.go | 33 ++++++++----------- pkg/services/authn/clients/proxy_test.go | 5 ++- pkg/services/contexthandler/contexthandler.go | 2 +- 4 files changed, 17 insertions(+), 25 deletions(-) diff --git a/pkg/services/authn/authnimpl/service.go b/pkg/services/authn/authnimpl/service.go index cb7ef5f9f3..881a23d060 100644 --- a/pkg/services/authn/authnimpl/service.go +++ b/pkg/services/authn/authnimpl/service.go @@ -123,7 +123,7 @@ func ProvideService( } if s.cfg.AuthProxyEnabled && len(proxyClients) > 0 { - proxy, err := clients.ProvideProxy(cfg, cache, userService, proxyClients...) + proxy, err := clients.ProvideProxy(cfg, cache, proxyClients...) if err != nil { s.log.Error("Failed to configure auth proxy", "err", err) } else { diff --git a/pkg/services/authn/clients/proxy.go b/pkg/services/authn/clients/proxy.go index 46c9ee1b04..fdfe1960e9 100644 --- a/pkg/services/authn/clients/proxy.go +++ b/pkg/services/authn/clients/proxy.go @@ -15,7 +15,6 @@ import ( authidentity "github.com/grafana/grafana/pkg/services/auth/identity" "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/login" - "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" @@ -43,12 +42,12 @@ var ( _ authn.ContextAwareClient = new(Proxy) ) -func ProvideProxy(cfg *setting.Cfg, cache proxyCache, userSrv user.Service, clients ...authn.ProxyClient) (*Proxy, error) { +func ProvideProxy(cfg *setting.Cfg, cache proxyCache, clients ...authn.ProxyClient) (*Proxy, error) { list, err := parseAcceptList(cfg.AuthProxyWhitelist) if err != nil { return nil, err } - return &Proxy{log.New(authn.ClientProxy), cfg, cache, userSrv, clients, list}, nil + return &Proxy{log.New(authn.ClientProxy), cfg, cache, clients, list}, nil } type proxyCache interface { @@ -61,7 +60,6 @@ type Proxy struct { log log.Logger cfg *setting.Cfg cache proxyCache - userSrv user.Service clients []authn.ProxyClient acceptedIPs []*net.IPNet } @@ -91,21 +89,17 @@ func (c *Proxy) Authenticate(ctx context.Context, r *authn.Request) (*authn.Iden if err != nil { c.log.FromContext(ctx).Warn("Failed to parse user id from cache", "error", err, "userId", string(entry)) } else { - usr, err := c.userSrv.GetSignedInUserWithCacheCtx(ctx, &user.GetSignedInUserQuery{ - UserID: uid, - OrgID: r.OrgID, - }) - - if err != nil { - 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 - // 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{SyncPermissions: true}, login.AuthProxyAuthModule), nil - } + return &authn.Identity{ + ID: authn.NamespacedID(authn.NamespaceUser, uid), + OrgID: r.OrgID, + // FIXME: This does not match the actual auth module used, but should not have any impact + // Maybe caching the auth module used with the user ID would be a good idea + AuthenticatedBy: login.AuthProxyAuthModule, + ClientParams: authn.ClientParams{ + FetchSyncedUser: true, + SyncPermissions: true, + }, + }, nil } } } @@ -116,7 +110,6 @@ func (c *Proxy) Authenticate(ctx context.Context, r *authn.Request) (*authn.Iden identity, clientErr = proxyClient.AuthenticateProxy(ctx, r, username, additional) if identity != nil { identity.ClientParams.CacheAuthProxyKey = cacheKey - identity.AuthenticatedBy = login.AuthProxyAuthModule return identity, nil } } diff --git a/pkg/services/authn/clients/proxy_test.go b/pkg/services/authn/clients/proxy_test.go index 408f5b3200..04b0d6c492 100644 --- a/pkg/services/authn/clients/proxy_test.go +++ b/pkg/services/authn/clients/proxy_test.go @@ -13,7 +13,6 @@ import ( "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/authn/authntest" - "github.com/grafana/grafana/pkg/services/user/usertest" "github.com/grafana/grafana/pkg/setting" ) @@ -113,7 +112,7 @@ func TestProxy_Authenticate(t *testing.T) { calledAdditional = additional return nil, nil }} - c, err := ProvideProxy(cfg, &fakeCache{expectedErr: errors.New("")}, usertest.NewUserServiceFake(), proxyClient) + c, err := ProvideProxy(cfg, &fakeCache{expectedErr: errors.New("")}, proxyClient) require.NoError(t, err) _, err = c.Authenticate(context.Background(), tt.req) @@ -210,7 +209,7 @@ func TestProxy_Hook(t *testing.T) { withRole := func(role string) func(t *testing.T) { cacheKey := fmt.Sprintf("users:johndoe-%s", role) return func(t *testing.T) { - c, err := ProvideProxy(cfg, cache, usertest.NewUserServiceFake(), authntest.MockProxyClient{}) + c, err := ProvideProxy(cfg, cache, authntest.MockProxyClient{}) require.NoError(t, err) userIdentity := &authn.Identity{ ID: userID, diff --git a/pkg/services/contexthandler/contexthandler.go b/pkg/services/contexthandler/contexthandler.go index 256d13a34f..b405fed5ad 100644 --- a/pkg/services/contexthandler/contexthandler.go +++ b/pkg/services/contexthandler/contexthandler.go @@ -120,7 +120,7 @@ func (h *ContextHandler) Middleware(next http.Handler) http.Handler { reqContext.UserToken = identity.SessionToken reqContext.IsSignedIn = !reqContext.SignedInUser.IsAnonymous reqContext.AllowAnonymous = reqContext.SignedInUser.IsAnonymous - reqContext.IsRenderCall = identity.AuthenticatedBy == login.RenderModule + reqContext.IsRenderCall = identity.GetAuthenticatedBy() == login.RenderModule } reqContext.Logger = reqContext.Logger.New("userId", reqContext.UserID, "orgId", reqContext.OrgID, "uname", reqContext.Login) From 2532047e7a6f2bd4ac9b7d7ae3d1b2aadcfb3bd6 Mon Sep 17 00:00:00 2001 From: idafurjes <36131195+idafurjes@users.noreply.github.com> Date: Fri, 1 Mar 2024 10:16:33 +0100 Subject: [PATCH 0323/1406] Add FolderUID for library elements (#79572) * Add FolderUID in missing places for libraryelements * Add migration for FolderUID in library elements table * Add Folder UIDs tolibrary panels * Adjust dashboard import with folder uid * Fix lint * Rename back FolderUID to UID * Remove default * Check if folderUID is nil * Add unique indes on org_id,folder_uid,name and kind * Update pkg/services/libraryelements/database.go Co-authored-by: Sofia Papagiannaki <1632407+papagian@users.noreply.github.com> * Fix folder integration test, with unique index on library elements * Make folder uids nullable and rewrite migration query * Use dashboard uid instead of folder_uid * Adjust test --------- Co-authored-by: Sofia Papagiannaki <1632407+papagian@users.noreply.github.com> --- pkg/api/dashboard_test.go | 2 +- .../dashboardimport/service/service.go | 2 +- .../dashboardimport/service/service_test.go | 19 +++--- pkg/services/folder/folderimpl/folder_test.go | 6 ++ pkg/services/libraryelements/database.go | 20 ++++-- .../libraryelements_create_test.go | 15 ++-- .../libraryelements_delete_test.go | 2 +- .../libraryelements_get_all_test.go | 67 +++++++++++------- .../libraryelements_get_test.go | 4 +- .../libraryelements_patch_test.go | 68 +++++++++++-------- .../libraryelements_permissions_test.go | 37 +++++----- .../libraryelements/libraryelements_test.go | 30 ++++---- pkg/services/libraryelements/model/model.go | 18 ++--- pkg/services/libraryelements/writers.go | 2 + pkg/services/librarypanels/librarypanels.go | 21 +++--- .../librarypanels/librarypanels_test.go | 51 +++++++------- .../sqlstore/migrations/libraryelements.go | 22 ++++++ 17 files changed, 229 insertions(+), 157 deletions(-) diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index bede8c3cfd..733217495a 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -1037,7 +1037,7 @@ func (m *mockLibraryPanelService) ConnectLibraryPanelsForDashboard(c context.Con return nil } -func (m *mockLibraryPanelService) ImportLibraryPanelsForDashboard(c context.Context, signedInUser identity.Requester, libraryPanels *simplejson.Json, panels []any, folderID int64) error { +func (m *mockLibraryPanelService) ImportLibraryPanelsForDashboard(c context.Context, signedInUser identity.Requester, libraryPanels *simplejson.Json, panels []any, folderID int64, folderUID string) error { return nil } diff --git a/pkg/services/dashboardimport/service/service.go b/pkg/services/dashboardimport/service/service.go index 842651e275..00cd032496 100644 --- a/pkg/services/dashboardimport/service/service.go +++ b/pkg/services/dashboardimport/service/service.go @@ -141,7 +141,7 @@ func (s *ImportDashboardService) ImportDashboard(ctx context.Context, req *dashb metrics.MFolderIDsServiceCount.WithLabelValues(metrics.DashboardImport).Inc() // nolint:staticcheck - err = s.libraryPanelService.ImportLibraryPanelsForDashboard(ctx, req.User, libraryElements, generatedDash.Get("panels").MustArray(), req.FolderId) + err = s.libraryPanelService.ImportLibraryPanelsForDashboard(ctx, req.User, libraryElements, generatedDash.Get("panels").MustArray(), req.FolderId, req.FolderUid) if err != nil { return nil, err } diff --git a/pkg/services/dashboardimport/service/service_test.go b/pkg/services/dashboardimport/service/service_test.go index 19e0dba893..e8958f22c9 100644 --- a/pkg/services/dashboardimport/service/service_test.go +++ b/pkg/services/dashboardimport/service/service_test.go @@ -47,7 +47,7 @@ func TestImportDashboardService(t *testing.T) { importLibraryPanelsForDashboard := false connectLibraryPanelsForDashboardCalled := false libraryPanelService := &libraryPanelServiceMock{ - importLibraryPanelsForDashboardFunc: func(ctx context.Context, signedInUser identity.Requester, libraryPanels *simplejson.Json, panels []any, folderID int64) error { + importLibraryPanelsForDashboardFunc: func(ctx context.Context, signedInUser identity.Requester, libraryPanels *simplejson.Json, panels []any, folderID int64, folderUID string) error { importLibraryPanelsForDashboard = true return nil }, @@ -75,9 +75,8 @@ func TestImportDashboardService(t *testing.T) { Inputs: []dashboardimport.ImportDashboardInput{ {Name: "*", Type: "datasource", Value: "prom"}, }, - User: &user.SignedInUser{UserID: 2, OrgRole: org.RoleAdmin, OrgID: 3}, - // FolderId: 5, - FolderUid: "123", + User: &user.SignedInUser{UserID: 2, OrgRole: org.RoleAdmin, OrgID: 3}, + FolderUid: "folderUID", } resp, err := s.ImportDashboard(context.Background(), req) require.NoError(t, err) @@ -91,7 +90,7 @@ func TestImportDashboardService(t *testing.T) { require.Equal(t, int64(3), importDashboardArg.OrgID) require.Equal(t, int64(2), userID) require.Equal(t, "prometheus", importDashboardArg.Dashboard.PluginID) - require.Equal(t, "123", importDashboardArg.Dashboard.FolderUID) + require.Equal(t, "folderUID", importDashboardArg.Dashboard.FolderUID) panel := importDashboardArg.Dashboard.Data.Get("panels").GetIndex(0) require.Equal(t, "prom", panel.Get("datasource").MustString()) @@ -143,7 +142,7 @@ func TestImportDashboardService(t *testing.T) { {Name: "*", Type: "datasource", Value: "prom"}, }, User: &user.SignedInUser{UserID: 2, OrgRole: org.RoleAdmin, OrgID: 3}, - FolderUid: "123", + FolderUid: "folderUID", } resp, err := s.ImportDashboard(context.Background(), req) require.NoError(t, err) @@ -157,7 +156,7 @@ func TestImportDashboardService(t *testing.T) { require.Equal(t, int64(3), importDashboardArg.OrgID) require.Equal(t, int64(2), userID) require.Equal(t, "", importDashboardArg.Dashboard.PluginID) - require.Equal(t, "123", importDashboardArg.Dashboard.FolderUID) + require.Equal(t, "folderUID", importDashboardArg.Dashboard.FolderUID) panel := importDashboardArg.Dashboard.Data.Get("panels").GetIndex(0) require.Equal(t, "prom", panel.Get("datasource").MustString()) @@ -211,7 +210,7 @@ func (s *dashboardServiceMock) ImportDashboard(ctx context.Context, dto *dashboa type libraryPanelServiceMock struct { librarypanels.Service connectLibraryPanelsForDashboardFunc func(c context.Context, signedInUser identity.Requester, dash *dashboards.Dashboard) error - importLibraryPanelsForDashboardFunc func(c context.Context, signedInUser identity.Requester, libraryPanels *simplejson.Json, panels []any, folderID int64) error + importLibraryPanelsForDashboardFunc func(c context.Context, signedInUser identity.Requester, libraryPanels *simplejson.Json, panels []any, folderID int64, folderUID string) error } var _ librarypanels.Service = (*libraryPanelServiceMock)(nil) @@ -224,9 +223,9 @@ func (s *libraryPanelServiceMock) ConnectLibraryPanelsForDashboard(ctx context.C return nil } -func (s *libraryPanelServiceMock) ImportLibraryPanelsForDashboard(ctx context.Context, signedInUser identity.Requester, libraryPanels *simplejson.Json, panels []any, folderID int64) error { +func (s *libraryPanelServiceMock) ImportLibraryPanelsForDashboard(ctx context.Context, signedInUser identity.Requester, libraryPanels *simplejson.Json, panels []any, folderID int64, folderUID string) error { if s.importLibraryPanelsForDashboardFunc != nil { - return s.importLibraryPanelsForDashboardFunc(ctx, signedInUser, libraryPanels, panels, folderID) + return s.importLibraryPanelsForDashboardFunc(ctx, signedInUser, libraryPanels, panels, folderID, folderUID) } return nil diff --git a/pkg/services/folder/folderimpl/folder_test.go b/pkg/services/folder/folderimpl/folder_test.go index e23fe64f20..17b10376f6 100644 --- a/pkg/services/folder/folderimpl/folder_test.go +++ b/pkg/services/folder/folderimpl/folder_test.go @@ -448,10 +448,12 @@ func TestIntegrationNestedFolderService(t *testing.T) { // nolint:staticcheck libraryElementCmd.FolderID = parent.ID + libraryElementCmd.FolderUID = &parent.UID _, err = lps.LibraryElementService.CreateElement(context.Background(), &signedInUser, libraryElementCmd) require.NoError(t, err) // nolint:staticcheck libraryElementCmd.FolderID = subfolder.ID + libraryElementCmd.FolderUID = &subfolder.UID _, err = lps.LibraryElementService.CreateElement(context.Background(), &signedInUser, libraryElementCmd) require.NoError(t, err) @@ -528,10 +530,12 @@ func TestIntegrationNestedFolderService(t *testing.T) { // nolint:staticcheck libraryElementCmd.FolderID = parent.ID + libraryElementCmd.FolderUID = &parent.UID _, err = lps.LibraryElementService.CreateElement(context.Background(), &signedInUser, libraryElementCmd) require.NoError(t, err) // nolint:staticcheck libraryElementCmd.FolderID = subfolder.ID + libraryElementCmd.FolderUID = &subfolder.UID _, err = lps.LibraryElementService.CreateElement(context.Background(), &signedInUser, libraryElementCmd) require.NoError(t, err) @@ -667,11 +671,13 @@ func TestIntegrationNestedFolderService(t *testing.T) { _ = createRule(t, alertStore, subfolder.UID, "sub alert") // nolint:staticcheck libraryElementCmd.FolderID = subfolder.ID + libraryElementCmd.FolderUID = &subPanel.FolderUID subPanel, err = lps.LibraryElementService.CreateElement(context.Background(), &signedInUser, libraryElementCmd) require.NoError(t, err) } // nolint:staticcheck libraryElementCmd.FolderID = parent.ID + libraryElementCmd.FolderUID = &parent.UID parentPanel, err := lps.LibraryElementService.CreateElement(context.Background(), &signedInUser, libraryElementCmd) require.NoError(t, err) diff --git a/pkg/services/libraryelements/database.go b/pkg/services/libraryelements/database.go index b8c09c1281..2aba478bd4 100644 --- a/pkg/services/libraryelements/database.go +++ b/pkg/services/libraryelements/database.go @@ -149,14 +149,20 @@ func (l *LibraryElementService) createLibraryElement(c context.Context, signedIn } metrics.MFolderIDsServiceCount.WithLabelValues(metrics.LibraryElements).Inc() + // folderUID *string will be changed to string + var folderUID string + if cmd.FolderUID != nil { + folderUID = *cmd.FolderUID + } element := model.LibraryElement{ - OrgID: signedInUser.GetOrgID(), - FolderID: cmd.FolderID, // nolint:staticcheck - UID: createUID, - Name: cmd.Name, - Model: updatedModel, - Version: 1, - Kind: cmd.Kind, + OrgID: signedInUser.GetOrgID(), + FolderID: cmd.FolderID, // nolint:staticcheck + FolderUID: folderUID, + UID: createUID, + Name: cmd.Name, + Model: updatedModel, + Version: 1, + Kind: cmd.Kind, Created: time.Now(), Updated: time.Now(), diff --git a/pkg/services/libraryelements/libraryelements_create_test.go b/pkg/services/libraryelements/libraryelements_create_test.go index 8a1dfac99b..7adca02ebd 100644 --- a/pkg/services/libraryelements/libraryelements_create_test.go +++ b/pkg/services/libraryelements/libraryelements_create_test.go @@ -15,7 +15,7 @@ func TestCreateLibraryElement(t *testing.T) { scenarioWithPanel(t, "When an admin tries to create a library panel that already exists, it should fail", func(t *testing.T, sc scenarioContext) { // nolint:staticcheck - command := getCreatePanelCommand(sc.folder.ID, "Text - Library Panel") + command := getCreatePanelCommand(sc.folder.ID, sc.folder.UID, "Text - Library Panel") sc.reqContext.Req.Body = mockRequestBody(command) resp := sc.service.createHandler(sc.reqContext) require.Equal(t, 400, resp.Status()) @@ -28,6 +28,7 @@ func TestCreateLibraryElement(t *testing.T) { ID: 1, OrgID: 1, FolderID: 1, // nolint:staticcheck + FolderUID: sc.folder.UID, UID: sc.initialResult.Result.UID, Name: "Text - Library Panel", Kind: int64(model.PanelElement), @@ -68,7 +69,7 @@ func TestCreateLibraryElement(t *testing.T) { testScenario(t, "When an admin tries to create a library panel that does not exists using an nonexistent UID, it should succeed", func(t *testing.T, sc scenarioContext) { // nolint:staticcheck - command := getCreatePanelCommand(sc.folder.ID, "Nonexistent UID") + command := getCreatePanelCommand(sc.folder.ID, sc.folder.UID, "Nonexistent UID") command.UID = util.GenerateShortUID() sc.reqContext.Req.Body = mockRequestBody(command) resp := sc.service.createHandler(sc.reqContext) @@ -78,6 +79,7 @@ func TestCreateLibraryElement(t *testing.T) { ID: 1, OrgID: 1, FolderID: 1, // nolint:staticcheck + FolderUID: sc.folder.UID, UID: command.UID, Name: "Nonexistent UID", Kind: int64(model.PanelElement), @@ -118,7 +120,7 @@ func TestCreateLibraryElement(t *testing.T) { scenarioWithPanel(t, "When an admin tries to create a library panel that does not exists using an existent UID, it should fail", func(t *testing.T, sc scenarioContext) { // nolint:staticcheck - command := getCreatePanelCommand(sc.folder.ID, "Existing UID") + command := getCreatePanelCommand(sc.folder.ID, sc.folder.UID, "Existing UID") command.UID = sc.initialResult.Result.UID sc.reqContext.Req.Body = mockRequestBody(command) resp := sc.service.createHandler(sc.reqContext) @@ -128,7 +130,7 @@ func TestCreateLibraryElement(t *testing.T) { scenarioWithPanel(t, "When an admin tries to create a library panel that does not exists using an invalid UID, it should fail", func(t *testing.T, sc scenarioContext) { // nolint:staticcheck - command := getCreatePanelCommand(sc.folder.ID, "Invalid UID") + command := getCreatePanelCommand(sc.folder.ID, sc.folder.UID, "Invalid UID") command.UID = "Testing an invalid UID" sc.reqContext.Req.Body = mockRequestBody(command) resp := sc.service.createHandler(sc.reqContext) @@ -138,7 +140,7 @@ func TestCreateLibraryElement(t *testing.T) { scenarioWithPanel(t, "When an admin tries to create a library panel that does not exists using an UID that is too long, it should fail", func(t *testing.T, sc scenarioContext) { // nolint:staticcheck - command := getCreatePanelCommand(sc.folder.ID, "Invalid UID") + command := getCreatePanelCommand(sc.folder.ID, sc.folder.UID, "Invalid UID") command.UID = "j6T00KRZzj6T00KRZzj6T00KRZzj6T00KRZzj6T00K" sc.reqContext.Req.Body = mockRequestBody(command) resp := sc.service.createHandler(sc.reqContext) @@ -147,7 +149,7 @@ func TestCreateLibraryElement(t *testing.T) { testScenario(t, "When an admin tries to create a library panel where name and panel title differ, it should not update panel title", func(t *testing.T, sc scenarioContext) { - command := getCreatePanelCommand(1, "Library Panel Name") + command := getCreatePanelCommand(1, sc.folder.UID, "Library Panel Name") sc.reqContext.Req.Body = mockRequestBody(command) resp := sc.service.createHandler(sc.reqContext) var result = validateAndUnMarshalResponse(t, resp) @@ -156,6 +158,7 @@ func TestCreateLibraryElement(t *testing.T) { ID: 1, OrgID: 1, FolderID: 1, // nolint:staticcheck + FolderUID: sc.folder.UID, UID: result.Result.UID, Name: "Library Panel Name", Kind: int64(model.PanelElement), diff --git a/pkg/services/libraryelements/libraryelements_delete_test.go b/pkg/services/libraryelements/libraryelements_delete_test.go index 7493c49461..597787d6ee 100644 --- a/pkg/services/libraryelements/libraryelements_delete_test.go +++ b/pkg/services/libraryelements/libraryelements_delete_test.go @@ -74,7 +74,7 @@ func TestDeleteLibraryElement(t *testing.T) { Data: simplejson.NewFromAny(dashJSON), } // nolint:staticcheck - dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.ID) + dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.ID, sc.folder.UID) err := sc.service.ConnectElementsToDashboard(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, []string{sc.initialResult.Result.UID}, dashInDB.ID) require.NoError(t, err) diff --git a/pkg/services/libraryelements/libraryelements_get_all_test.go b/pkg/services/libraryelements/libraryelements_get_all_test.go index d281155a1b..29cc7819fc 100644 --- a/pkg/services/libraryelements/libraryelements_get_all_test.go +++ b/pkg/services/libraryelements/libraryelements_get_all_test.go @@ -39,7 +39,7 @@ func TestGetAllLibraryElements(t *testing.T) { scenarioWithPanel(t, "When an admin tries to get all panel elements and both panels and variables exist, it should only return panels", func(t *testing.T, sc scenarioContext) { // nolint:staticcheck - command := getCreateVariableCommand(sc.folder.ID, "query0") + command := getCreateVariableCommand(sc.folder.ID, sc.folder.UID, "query0") sc.reqContext.Req.Body = mockRequestBody(command) resp := sc.service.createHandler(sc.reqContext) require.Equal(t, 200, resp.Status()) @@ -64,6 +64,7 @@ func TestGetAllLibraryElements(t *testing.T) { ID: 1, OrgID: 1, FolderID: 1, // nolint:staticcheck + FolderUID: sc.folder.UID, UID: result.Result.Elements[0].UID, Name: "Text - Library Panel", Kind: int64(model.PanelElement), @@ -106,7 +107,7 @@ func TestGetAllLibraryElements(t *testing.T) { scenarioWithPanel(t, "When an admin tries to get all variable elements and both panels and variables exist, it should only return panels", func(t *testing.T, sc scenarioContext) { // nolint:staticcheck - command := getCreateVariableCommand(sc.folder.ID, "query0") + command := getCreateVariableCommand(sc.folder.ID, sc.folder.UID, "query0") sc.reqContext.Req.Body = mockRequestBody(command) resp := sc.service.createHandler(sc.reqContext) require.Equal(t, 200, resp.Status()) @@ -131,6 +132,7 @@ func TestGetAllLibraryElements(t *testing.T) { ID: 2, OrgID: 1, FolderID: 1, // nolint:staticcheck + FolderUID: sc.folder.UID, UID: result.Result.Elements[0].UID, Name: "query0", Kind: int64(model.VariableElement), @@ -172,7 +174,7 @@ func TestGetAllLibraryElements(t *testing.T) { scenarioWithPanel(t, "When an admin tries to get all library panels and two exist, it should succeed", func(t *testing.T, sc scenarioContext) { // nolint:staticcheck - command := getCreatePanelCommand(sc.folder.ID, "Text - Library Panel2") + command := getCreatePanelCommand(sc.folder.ID, sc.folder.UID, "Text - Library Panel2") sc.reqContext.Req.Body = mockRequestBody(command) resp := sc.service.createHandler(sc.reqContext) require.Equal(t, 200, resp.Status()) @@ -193,6 +195,7 @@ func TestGetAllLibraryElements(t *testing.T) { ID: 1, OrgID: 1, FolderID: 1, // nolint:staticcheck + FolderUID: sc.folder.UID, UID: result.Result.Elements[0].UID, Name: "Text - Library Panel", Kind: int64(model.PanelElement), @@ -228,6 +231,7 @@ func TestGetAllLibraryElements(t *testing.T) { ID: 2, OrgID: 1, FolderID: 1, // nolint:staticcheck + FolderUID: sc.folder.UID, UID: result.Result.Elements[1].UID, Name: "Text - Library Panel2", Kind: int64(model.PanelElement), @@ -270,7 +274,7 @@ func TestGetAllLibraryElements(t *testing.T) { scenarioWithPanel(t, "When an admin tries to get all library panels and two exist and sort desc is set, it should succeed and the result should be correct", func(t *testing.T, sc scenarioContext) { // nolint:staticcheck - command := getCreatePanelCommand(sc.folder.ID, "Text - Library Panel2") + command := getCreatePanelCommand(sc.folder.ID, sc.folder.UID, "Text - Library Panel2") sc.reqContext.Req.Body = mockRequestBody(command) resp := sc.service.createHandler(sc.reqContext) require.Equal(t, 200, resp.Status()) @@ -294,6 +298,7 @@ func TestGetAllLibraryElements(t *testing.T) { ID: 2, OrgID: 1, FolderID: 1, // nolint:staticcheck + FolderUID: sc.folder.UID, UID: result.Result.Elements[0].UID, Name: "Text - Library Panel2", Kind: int64(model.PanelElement), @@ -329,6 +334,7 @@ func TestGetAllLibraryElements(t *testing.T) { ID: 1, OrgID: 1, FolderID: 1, // nolint:staticcheck + FolderUID: sc.folder.UID, UID: result.Result.Elements[1].UID, Name: "Text - Library Panel", Kind: int64(model.PanelElement), @@ -371,7 +377,7 @@ func TestGetAllLibraryElements(t *testing.T) { scenarioWithPanel(t, "When an admin tries to get all library panels and two exist and typeFilter is set to existing types, it should succeed and the result should be correct", func(t *testing.T, sc scenarioContext) { // nolint:staticcheck - command := getCreateCommandWithModel(sc.folder.ID, "Gauge - Library Panel", model.PanelElement, []byte(` + command := getCreateCommandWithModel(sc.folder.ID, sc.folder.UID, "Gauge - Library Panel", model.PanelElement, []byte(` { "datasource": "${DS_GDEV-TESTDATA}", "id": 1, @@ -385,7 +391,7 @@ func TestGetAllLibraryElements(t *testing.T) { require.Equal(t, 200, resp.Status()) // nolint:staticcheck - command = getCreateCommandWithModel(sc.folder.ID, "BarGauge - Library Panel", model.PanelElement, []byte(` + command = getCreateCommandWithModel(sc.folder.ID, sc.folder.UID, "BarGauge - Library Panel", model.PanelElement, []byte(` { "datasource": "${DS_GDEV-TESTDATA}", "id": 1, @@ -417,6 +423,7 @@ func TestGetAllLibraryElements(t *testing.T) { ID: 3, OrgID: 1, FolderID: 1, // nolint:staticcheck + FolderUID: sc.folder.UID, UID: result.Result.Elements[0].UID, Name: "BarGauge - Library Panel", Kind: int64(model.PanelElement), @@ -452,6 +459,7 @@ func TestGetAllLibraryElements(t *testing.T) { ID: 2, OrgID: 1, FolderID: 1, // nolint:staticcheck + FolderUID: sc.folder.UID, UID: result.Result.Elements[1].UID, Name: "Gauge - Library Panel", Kind: int64(model.PanelElement), @@ -494,7 +502,7 @@ func TestGetAllLibraryElements(t *testing.T) { scenarioWithPanel(t, "When an admin tries to get all library panels and two exist and typeFilter is set to a nonexistent type, it should succeed and the result should be correct", func(t *testing.T, sc scenarioContext) { // nolint:staticcheck - command := getCreateCommandWithModel(sc.folder.ID, "Gauge - Library Panel", model.PanelElement, []byte(` + command := getCreateCommandWithModel(sc.folder.ID, sc.folder.UID, "Gauge - Library Panel", model.PanelElement, []byte(` { "datasource": "${DS_GDEV-TESTDATA}", "id": 1, @@ -529,25 +537,24 @@ func TestGetAllLibraryElements(t *testing.T) { } }) - scenarioWithPanel(t, "When an admin tries to get all library panels and two exist and folderFilter is set to existing folders, it should succeed and the result should be correct", + scenarioWithPanel(t, "When an admin tries to get all library panels and two exist and folderFilterUIDs is set to existing folders, it should succeed and the result should be correct", func(t *testing.T, sc scenarioContext) { newFolder := createFolder(t, sc, "NewFolder") // nolint:staticcheck - command := getCreatePanelCommand(newFolder.ID, "Text - Library Panel2") + command := getCreatePanelCommand(newFolder.ID, newFolder.UID, "Text - Library Panel2") sc.reqContext.Req.Body = mockRequestBody(command) resp := sc.service.createHandler(sc.reqContext) require.Equal(t, 200, resp.Status()) - // nolint:staticcheck - folderFilter := strconv.FormatInt(newFolder.ID, 10) - + folderFilterUID := newFolder.UID err := sc.reqContext.Req.ParseForm() require.NoError(t, err) - sc.reqContext.Req.Form.Add("folderFilter", folderFilter) + sc.reqContext.Req.Form.Add("folderFilterUIDs", folderFilterUID) resp = sc.service.getAllHandler(sc.reqContext) require.Equal(t, 200, resp.Status()) var result libraryElementsSearch err = json.Unmarshal(resp.Body(), &result) + require.NoError(t, err) var expected = libraryElementsSearch{ Result: libraryElementsSearchResult{ @@ -559,6 +566,7 @@ func TestGetAllLibraryElements(t *testing.T) { ID: 2, OrgID: 1, FolderID: newFolder.ID, // nolint:staticcheck + FolderUID: newFolder.UID, UID: result.Result.Elements[0].UID, Name: "Text - Library Panel2", Kind: int64(model.PanelElement), @@ -602,15 +610,15 @@ func TestGetAllLibraryElements(t *testing.T) { func(t *testing.T, sc scenarioContext) { newFolder := createFolder(t, sc, "NewFolder") // nolint:staticcheck - command := getCreatePanelCommand(newFolder.ID, "Text - Library Panel2") + command := getCreatePanelCommand(newFolder.ID, sc.folder.UID, "Text - Library Panel2") sc.reqContext.Req.Body = mockRequestBody(command) resp := sc.service.createHandler(sc.reqContext) require.Equal(t, 200, resp.Status()) - folderFilter := "2020,2021" + folderFilterUIDs := "2020,2021" err := sc.reqContext.Req.ParseForm() require.NoError(t, err) - sc.reqContext.Req.Form.Add("folderFilter", folderFilter) + sc.reqContext.Req.Form.Add("folderFilterUIDs", folderFilterUIDs) resp = sc.service.getAllHandler(sc.reqContext) require.Equal(t, 200, resp.Status()) @@ -633,7 +641,7 @@ func TestGetAllLibraryElements(t *testing.T) { scenarioWithPanel(t, "When an admin tries to get all library panels and two exist and folderFilter is set to General folder, it should succeed and the result should be correct", func(t *testing.T, sc scenarioContext) { // nolint:staticcheck - command := getCreatePanelCommand(sc.folder.ID, "Text - Library Panel2") + command := getCreatePanelCommand(sc.folder.ID, sc.folder.UID, "Text - Library Panel2") sc.reqContext.Req.Body = mockRequestBody(command) resp := sc.service.createHandler(sc.reqContext) require.Equal(t, 200, resp.Status()) @@ -658,6 +666,7 @@ func TestGetAllLibraryElements(t *testing.T) { ID: 1, OrgID: 1, FolderID: 1, // nolint:staticcheck + FolderUID: sc.folder.UID, UID: result.Result.Elements[0].UID, Name: "Text - Library Panel", Kind: int64(model.PanelElement), @@ -693,6 +702,7 @@ func TestGetAllLibraryElements(t *testing.T) { ID: 2, OrgID: 1, FolderID: 1, // nolint:staticcheck + FolderUID: sc.folder.UID, UID: result.Result.Elements[1].UID, Name: "Text - Library Panel2", Kind: int64(model.PanelElement), @@ -735,7 +745,7 @@ func TestGetAllLibraryElements(t *testing.T) { scenarioWithPanel(t, "When an admin tries to get all library panels and two exist and excludeUID is set, it should succeed and the result should be correct", func(t *testing.T, sc scenarioContext) { // nolint:staticcheck - command := getCreatePanelCommand(sc.folder.ID, "Text - Library Panel2") + command := getCreatePanelCommand(sc.folder.ID, sc.folder.UID, "Text - Library Panel2") sc.reqContext.Req.Body = mockRequestBody(command) resp := sc.service.createHandler(sc.reqContext) require.Equal(t, 200, resp.Status()) @@ -759,6 +769,7 @@ func TestGetAllLibraryElements(t *testing.T) { ID: 2, OrgID: 1, FolderID: 1, // nolint:staticcheck + FolderUID: sc.folder.UID, UID: result.Result.Elements[0].UID, Name: "Text - Library Panel2", Kind: int64(model.PanelElement), @@ -801,7 +812,7 @@ func TestGetAllLibraryElements(t *testing.T) { scenarioWithPanel(t, "When an admin tries to get all library panels and two exist and perPage is 1, it should succeed and the result should be correct", func(t *testing.T, sc scenarioContext) { // nolint:staticcheck - command := getCreatePanelCommand(sc.folder.ID, "Text - Library Panel2") + command := getCreatePanelCommand(sc.folder.ID, sc.folder.UID, "Text - Library Panel2") sc.reqContext.Req.Body = mockRequestBody(command) resp := sc.service.createHandler(sc.reqContext) require.Equal(t, 200, resp.Status()) @@ -825,6 +836,7 @@ func TestGetAllLibraryElements(t *testing.T) { ID: 1, OrgID: 1, FolderID: 1, // nolint:staticcheck + FolderUID: sc.folder.UID, UID: result.Result.Elements[0].UID, Name: "Text - Library Panel", Kind: int64(model.PanelElement), @@ -867,7 +879,7 @@ func TestGetAllLibraryElements(t *testing.T) { scenarioWithPanel(t, "When an admin tries to get all library panels and two exist and perPage is 1 and page is 2, it should succeed and the result should be correct", func(t *testing.T, sc scenarioContext) { // nolint:staticcheck - command := getCreatePanelCommand(sc.folder.ID, "Text - Library Panel2") + command := getCreatePanelCommand(sc.folder.ID, sc.folder.UID, "Text - Library Panel2") sc.reqContext.Req.Body = mockRequestBody(command) resp := sc.service.createHandler(sc.reqContext) require.Equal(t, 200, resp.Status()) @@ -892,6 +904,7 @@ func TestGetAllLibraryElements(t *testing.T) { ID: 2, OrgID: 1, FolderID: 1, // nolint:staticcheck + FolderUID: sc.folder.UID, UID: result.Result.Elements[0].UID, Name: "Text - Library Panel2", Kind: int64(model.PanelElement), @@ -934,7 +947,7 @@ func TestGetAllLibraryElements(t *testing.T) { scenarioWithPanel(t, "When an admin tries to get all library panels and two exist and searchString exists in the description, it should succeed and the result should be correct", func(t *testing.T, sc scenarioContext) { // nolint:staticcheck - command := getCreateCommandWithModel(sc.folder.ID, "Text - Library Panel2", model.PanelElement, []byte(` + command := getCreateCommandWithModel(sc.folder.ID, sc.folder.UID, "Text - Library Panel2", model.PanelElement, []byte(` { "datasource": "${DS_GDEV-TESTDATA}", "id": 1, @@ -968,6 +981,7 @@ func TestGetAllLibraryElements(t *testing.T) { ID: 1, OrgID: 1, FolderID: 1, // nolint:staticcheck + FolderUID: sc.folder.UID, UID: result.Result.Elements[0].UID, Name: "Text - Library Panel", Kind: int64(model.PanelElement), @@ -1010,7 +1024,7 @@ func TestGetAllLibraryElements(t *testing.T) { scenarioWithPanel(t, "When an admin tries to get all library panels and two exist and searchString exists in both name and description, it should succeed and the result should be correct", func(t *testing.T, sc scenarioContext) { // nolint:staticcheck - command := getCreateCommandWithModel(sc.folder.ID, "Some Other", model.PanelElement, []byte(` + command := getCreateCommandWithModel(sc.folder.ID, sc.folder.UID, "Some Other", model.PanelElement, []byte(` { "datasource": "${DS_GDEV-TESTDATA}", "id": 1, @@ -1042,6 +1056,7 @@ func TestGetAllLibraryElements(t *testing.T) { ID: 2, OrgID: 1, FolderID: 1, // nolint:staticcheck + FolderUID: sc.folder.UID, UID: result.Result.Elements[0].UID, Name: "Some Other", Kind: int64(model.PanelElement), @@ -1077,6 +1092,7 @@ func TestGetAllLibraryElements(t *testing.T) { ID: 1, OrgID: 1, FolderID: 1, // nolint:staticcheck + FolderUID: sc.folder.UID, UID: result.Result.Elements[1].UID, Name: "Text - Library Panel", Kind: int64(model.PanelElement), @@ -1119,7 +1135,7 @@ func TestGetAllLibraryElements(t *testing.T) { scenarioWithPanel(t, "When an admin tries to get all library panels and two exist and perPage is 1 and page is 1 and searchString is panel2, it should succeed and the result should be correct", func(t *testing.T, sc scenarioContext) { // nolint:staticcheck - command := getCreatePanelCommand(sc.folder.ID, "Text - Library Panel2") + command := getCreatePanelCommand(sc.folder.ID, sc.folder.UID, "Text - Library Panel2") sc.reqContext.Req.Body = mockRequestBody(command) resp := sc.service.createHandler(sc.reqContext) require.Equal(t, 200, resp.Status()) @@ -1145,6 +1161,7 @@ func TestGetAllLibraryElements(t *testing.T) { ID: 2, OrgID: 1, FolderID: 1, // nolint:staticcheck + FolderUID: sc.folder.UID, UID: result.Result.Elements[0].UID, Name: "Text - Library Panel2", Kind: int64(model.PanelElement), @@ -1187,7 +1204,7 @@ func TestGetAllLibraryElements(t *testing.T) { scenarioWithPanel(t, "When an admin tries to get all library panels and two exist and perPage is 1 and page is 3 and searchString is panel, it should succeed and the result should be correct", func(t *testing.T, sc scenarioContext) { // nolint:staticcheck - command := getCreatePanelCommand(sc.folder.ID, "Text - Library Panel2") + command := getCreatePanelCommand(sc.folder.ID, sc.folder.UID, "Text - Library Panel2") sc.reqContext.Req.Body = mockRequestBody(command) resp := sc.service.createHandler(sc.reqContext) require.Equal(t, 200, resp.Status()) @@ -1219,7 +1236,7 @@ func TestGetAllLibraryElements(t *testing.T) { scenarioWithPanel(t, "When an admin tries to get all library panels and two exist and perPage is 1 and page is 3 and searchString does not exist, it should succeed and the result should be correct", func(t *testing.T, sc scenarioContext) { // nolint:staticcheck - command := getCreatePanelCommand(sc.folder.ID, "Text - Library Panel2") + command := getCreatePanelCommand(sc.folder.ID, sc.folder.UID, "Text - Library Panel2") sc.reqContext.Req.Body = mockRequestBody(command) resp := sc.service.createHandler(sc.reqContext) require.Equal(t, 200, resp.Status()) diff --git a/pkg/services/libraryelements/libraryelements_get_test.go b/pkg/services/libraryelements/libraryelements_get_test.go index 0c8e4893c2..b441505639 100644 --- a/pkg/services/libraryelements/libraryelements_get_test.go +++ b/pkg/services/libraryelements/libraryelements_get_test.go @@ -35,6 +35,7 @@ func TestGetLibraryElement(t *testing.T) { ID: 1, OrgID: 1, FolderID: 1, // nolint:staticcheck + FolderUID: sc.folder.UID, UID: res.Result.UID, Name: "Text - Library Panel", Kind: int64(model.PanelElement), @@ -123,7 +124,7 @@ func TestGetLibraryElement(t *testing.T) { Data: simplejson.NewFromAny(dashJSON), } // nolint:staticcheck - dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.ID) + dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.ID, sc.folder.UID) err := sc.service.ConnectElementsToDashboard(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, []string{sc.initialResult.Result.UID}, dashInDB.ID) require.NoError(t, err) @@ -133,6 +134,7 @@ func TestGetLibraryElement(t *testing.T) { ID: 1, OrgID: 1, FolderID: 1, // nolint:staticcheck + FolderUID: sc.folder.UID, UID: res.Result.UID, Name: "Text - Library Panel", Kind: int64(model.PanelElement), diff --git a/pkg/services/libraryelements/libraryelements_patch_test.go b/pkg/services/libraryelements/libraryelements_patch_test.go index 0af1d96e7c..e59a19e29b 100644 --- a/pkg/services/libraryelements/libraryelements_patch_test.go +++ b/pkg/services/libraryelements/libraryelements_patch_test.go @@ -26,8 +26,9 @@ func TestPatchLibraryElement(t *testing.T) { func(t *testing.T, sc scenarioContext) { newFolder := createFolder(t, sc, "NewFolder") cmd := model.PatchLibraryElementCommand{ - FolderID: newFolder.ID, // nolint:staticcheck - Name: "Panel - New name", + FolderID: newFolder.ID, // nolint:staticcheck + FolderUID: &newFolder.UID, + Name: "Panel - New name", Model: []byte(` { "datasource": "${DS_GDEV-TESTDATA}", @@ -50,6 +51,7 @@ func TestPatchLibraryElement(t *testing.T) { ID: 1, OrgID: 1, FolderID: newFolder.ID, // nolint:staticcheck + FolderUID: newFolder.UID, UID: sc.initialResult.Result.UID, Name: "Panel - New name", Kind: int64(model.PanelElement), @@ -91,9 +93,10 @@ func TestPatchLibraryElement(t *testing.T) { func(t *testing.T, sc scenarioContext) { newFolder := createFolder(t, sc, "NewFolder") cmd := model.PatchLibraryElementCommand{ - FolderID: newFolder.ID, // nolint:staticcheck - Kind: int64(model.PanelElement), - Version: 1, + FolderID: newFolder.ID, // nolint:staticcheck + FolderUID: &newFolder.UID, + Kind: int64(model.PanelElement), + Version: 1, } sc.ctx.Req = web.SetURLParams(sc.ctx.Req, map[string]string{":uid": sc.initialResult.Result.UID}) sc.reqContext.Req.Body = mockRequestBody(cmd) @@ -102,6 +105,7 @@ func TestPatchLibraryElement(t *testing.T) { var result = validateAndUnMarshalResponse(t, resp) // nolint:staticcheck sc.initialResult.Result.FolderID = newFolder.ID + sc.initialResult.Result.FolderUID = newFolder.UID sc.initialResult.Result.Meta.CreatedBy.Name = userInDbName sc.initialResult.Result.Meta.CreatedBy.AvatarUrl = userInDbAvatar sc.initialResult.Result.Meta.Updated = result.Result.Meta.Updated @@ -176,10 +180,11 @@ func TestPatchLibraryElement(t *testing.T) { scenarioWithPanel(t, "When an admin tries to patch a library panel with an UID that is too long, it should fail", func(t *testing.T, sc scenarioContext) { cmd := model.PatchLibraryElementCommand{ - FolderID: -1, // nolint:staticcheck - UID: "j6T00KRZzj6T00KRZzj6T00KRZzj6T00KRZzj6T00K", - Kind: int64(model.PanelElement), - Version: 1, + FolderID: -1, // nolint:staticcheck + FolderUID: &sc.folder.UID, + UID: "j6T00KRZzj6T00KRZzj6T00KRZzj6T00KRZzj6T00K", + Kind: int64(model.PanelElement), + Version: 1, } sc.ctx.Req = web.SetURLParams(sc.ctx.Req, map[string]string{":uid": sc.initialResult.Result.UID}) sc.ctx.Req.Body = mockRequestBody(cmd) @@ -190,16 +195,17 @@ func TestPatchLibraryElement(t *testing.T) { scenarioWithPanel(t, "When an admin tries to patch a library panel with an existing UID, it should fail", func(t *testing.T, sc scenarioContext) { // nolint:staticcheck - command := getCreatePanelCommand(sc.folder.ID, "Existing UID") + command := getCreatePanelCommand(sc.folder.ID, sc.folder.UID, "Existing UID") command.UID = util.GenerateShortUID() sc.reqContext.Req.Body = mockRequestBody(command) resp := sc.service.createHandler(sc.reqContext) require.Equal(t, 200, resp.Status()) cmd := model.PatchLibraryElementCommand{ - FolderID: -1, // nolint:staticcheck - UID: command.UID, - Kind: int64(model.PanelElement), - Version: 1, + FolderID: -1, // nolint:staticcheck + FolderUID: &sc.folder.UID, + UID: command.UID, + Kind: int64(model.PanelElement), + Version: 1, } sc.ctx.Req = web.SetURLParams(sc.ctx.Req, map[string]string{":uid": sc.initialResult.Result.UID}) sc.ctx.Req.Body = mockRequestBody(cmd) @@ -312,7 +318,7 @@ func TestPatchLibraryElement(t *testing.T) { scenarioWithPanel(t, "When an admin tries to patch a library panel with a name that already exists, it should fail", func(t *testing.T, sc scenarioContext) { // nolint:staticcheck - command := getCreatePanelCommand(sc.folder.ID, "Another Panel") + command := getCreatePanelCommand(sc.folder.ID, sc.folder.UID, "Another Panel") sc.ctx.Req.Body = mockRequestBody(command) resp := sc.service.createHandler(sc.reqContext) var result = validateAndUnMarshalResponse(t, resp) @@ -331,14 +337,15 @@ func TestPatchLibraryElement(t *testing.T) { func(t *testing.T, sc scenarioContext) { newFolder := createFolder(t, sc, "NewFolder") // nolint:staticcheck - command := getCreatePanelCommand(newFolder.ID, "Text - Library Panel") + command := getCreatePanelCommand(newFolder.ID, newFolder.UID, "Text - Library Panel") sc.ctx.Req.Body = mockRequestBody(command) resp := sc.service.createHandler(sc.reqContext) var result = validateAndUnMarshalResponse(t, resp) cmd := model.PatchLibraryElementCommand{ - FolderID: 1, // nolint:staticcheck - Version: 1, - Kind: int64(model.PanelElement), + FolderID: 1, // nolint:staticcheck + FolderUID: &sc.folder.UID, + Version: 1, + Kind: int64(model.PanelElement), } sc.ctx.Req = web.SetURLParams(sc.ctx.Req, map[string]string{":uid": result.Result.UID}) sc.ctx.Req.Body = mockRequestBody(cmd) @@ -349,23 +356,25 @@ func TestPatchLibraryElement(t *testing.T) { scenarioWithPanel(t, "When an admin tries to patch a library panel in another org, it should fail", func(t *testing.T, sc scenarioContext) { cmd := model.PatchLibraryElementCommand{ - FolderID: sc.folder.ID, // nolint:staticcheck - Version: 1, - Kind: int64(model.PanelElement), + FolderID: sc.folder.ID, // nolint:staticcheck + FolderUID: &sc.folder.UID, + Version: 1, + Kind: int64(model.PanelElement), } sc.reqContext.OrgID = 2 sc.ctx.Req = web.SetURLParams(sc.ctx.Req, map[string]string{":uid": sc.initialResult.Result.UID}) sc.ctx.Req.Body = mockRequestBody(cmd) resp := sc.service.patchHandler(sc.reqContext) - require.Equal(t, 404, resp.Status()) + require.Equal(t, 400, resp.Status()) }) scenarioWithPanel(t, "When an admin tries to patch a library panel with an old version number, it should fail", func(t *testing.T, sc scenarioContext) { cmd := model.PatchLibraryElementCommand{ - FolderID: sc.folder.ID, // nolint:staticcheck - Version: 1, - Kind: int64(model.PanelElement), + FolderID: sc.folder.ID, // nolint:staticcheck + FolderUID: &sc.folder.UID, + Version: 1, + Kind: int64(model.PanelElement), } sc.ctx.Req = web.SetURLParams(sc.ctx.Req, map[string]string{":uid": sc.initialResult.Result.UID}) sc.ctx.Req.Body = mockRequestBody(cmd) @@ -379,9 +388,10 @@ func TestPatchLibraryElement(t *testing.T) { scenarioWithPanel(t, "When an admin tries to patch a library panel with an other kind, it should succeed but panel should not change", func(t *testing.T, sc scenarioContext) { cmd := model.PatchLibraryElementCommand{ - FolderID: sc.folder.ID, // nolint:staticcheck - Version: 1, - Kind: int64(model.VariableElement), + FolderID: sc.folder.ID, // nolint:staticcheck + FolderUID: &sc.folder.UID, + Version: 1, + Kind: int64(model.VariableElement), } sc.ctx.Req = web.SetURLParams(sc.ctx.Req, map[string]string{":uid": sc.initialResult.Result.UID}) sc.ctx.Req.Body = mockRequestBody(cmd) diff --git a/pkg/services/libraryelements/libraryelements_permissions_test.go b/pkg/services/libraryelements/libraryelements_permissions_test.go index bb48d58f53..327366115a 100644 --- a/pkg/services/libraryelements/libraryelements_permissions_test.go +++ b/pkg/services/libraryelements/libraryelements_permissions_test.go @@ -30,7 +30,7 @@ func TestLibraryElementPermissionsGeneralFolder(t *testing.T) { func(t *testing.T, sc scenarioContext) { sc.reqContext.SignedInUser.OrgRole = testCase.role - command := getCreatePanelCommand(0, "Library Panel Name") + command := getCreatePanelCommand(0, "", "Library Panel Name") sc.reqContext.Req.Body = mockRequestBody(command) resp := sc.service.createHandler(sc.reqContext) require.Equal(t, testCase.status, resp.Status()) @@ -40,7 +40,7 @@ func TestLibraryElementPermissionsGeneralFolder(t *testing.T) { func(t *testing.T, sc scenarioContext) { folder := createFolder(t, sc, "Folder") // nolint:staticcheck - command := getCreatePanelCommand(folder.ID, "Library Panel Name") + command := getCreatePanelCommand(folder.ID, folder.UID, "Library Panel Name") sc.reqContext.Req.Body = mockRequestBody(command) resp := sc.service.createHandler(sc.reqContext) result := validateAndUnMarshalResponse(t, resp) @@ -57,7 +57,7 @@ func TestLibraryElementPermissionsGeneralFolder(t *testing.T) { testScenario(t, fmt.Sprintf("When %s tries to patch a library panel by moving it from the General folder, it should return correct status", testCase.role), func(t *testing.T, sc scenarioContext) { folder := createFolder(t, sc, "Folder") - command := getCreatePanelCommand(0, "Library Panel Name") + command := getCreatePanelCommand(0, "", "Library Panel Name") sc.reqContext.Req.Body = mockRequestBody(command) resp := sc.service.createHandler(sc.reqContext) result := validateAndUnMarshalResponse(t, resp) @@ -73,7 +73,7 @@ func TestLibraryElementPermissionsGeneralFolder(t *testing.T) { testScenario(t, fmt.Sprintf("When %s tries to delete a library panel in the General folder, it should return correct status", testCase.role), func(t *testing.T, sc scenarioContext) { - cmd := getCreatePanelCommand(0, "Library Panel Name") + cmd := getCreatePanelCommand(0, "", "Library Panel Name") sc.reqContext.Req.Body = mockRequestBody(cmd) resp := sc.service.createHandler(sc.reqContext) result := validateAndUnMarshalResponse(t, resp) @@ -86,7 +86,7 @@ func TestLibraryElementPermissionsGeneralFolder(t *testing.T) { testScenario(t, fmt.Sprintf("When %s tries to get a library panel from General folder, it should return correct response", testCase.role), func(t *testing.T, sc scenarioContext) { - cmd := getCreatePanelCommand(0, "Library Panel in General Folder") + cmd := getCreatePanelCommand(0, "", "Library Panel in General Folder") sc.reqContext.Req.Body = mockRequestBody(cmd) resp := sc.service.createHandler(sc.reqContext) result := validateAndUnMarshalResponse(t, resp) @@ -96,6 +96,7 @@ func TestLibraryElementPermissionsGeneralFolder(t *testing.T) { result.Result.Meta.UpdatedBy.AvatarUrl = userInDbAvatar result.Result.Meta.FolderName = "General" result.Result.Meta.FolderUID = "" + result.Result.FolderUID = "general" sc.reqContext.SignedInUser.OrgRole = testCase.role sc.ctx.Req = web.SetURLParams(sc.ctx.Req, map[string]string{":uid": result.Result.UID}) @@ -111,7 +112,7 @@ func TestLibraryElementPermissionsGeneralFolder(t *testing.T) { testScenario(t, fmt.Sprintf("When %s tries to get all library panels from General folder, it should return correct response", testCase.role), func(t *testing.T, sc scenarioContext) { - cmd := getCreatePanelCommand(0, "Library Panel in General Folder") + cmd := getCreatePanelCommand(0, "", "Library Panel in General Folder") sc.reqContext.Req.Body = mockRequestBody(cmd) resp := sc.service.createHandler(sc.reqContext) result := validateAndUnMarshalResponse(t, resp) @@ -183,7 +184,7 @@ func TestLibraryElementCreatePermissions(t *testing.T) { } // nolint:staticcheck - command := getCreatePanelCommand(folder.ID, "Library Panel Name") + command := getCreatePanelCommand(folder.ID, folder.UID, "Library Panel Name") sc.reqContext.Req.Body = mockRequestBody(command) resp := sc.service.createHandler(sc.reqContext) require.Equal(t, testCase.status, resp.Status()) @@ -236,7 +237,7 @@ func TestLibraryElementPatchPermissions(t *testing.T) { func(t *testing.T, sc scenarioContext) { fromFolder := createFolder(t, sc, "FromFolder") // nolint:staticcheck - command := getCreatePanelCommand(fromFolder.ID, "Library Panel Name") + command := getCreatePanelCommand(fromFolder.ID, fromFolder.UID, "Library Panel Name") sc.reqContext.Req.Body = mockRequestBody(command) resp := sc.service.createHandler(sc.reqContext) result := validateAndUnMarshalResponse(t, resp) @@ -248,7 +249,7 @@ func TestLibraryElementPatchPermissions(t *testing.T) { } // nolint:staticcheck - cmd := model.PatchLibraryElementCommand{FolderID: toFolder.ID, Version: 1, Kind: int64(model.PanelElement)} + cmd := model.PatchLibraryElementCommand{FolderID: toFolder.ID, FolderUID: &toFolder.UID, Version: 1, Kind: int64(model.PanelElement)} sc.ctx.Req = web.SetURLParams(sc.ctx.Req, map[string]string{":uid": result.Result.UID}) sc.reqContext.Req.Body = mockRequestBody(cmd) resp = sc.service.patchHandler(sc.reqContext) @@ -298,7 +299,7 @@ func TestLibraryElementDeletePermissions(t *testing.T) { func(t *testing.T, sc scenarioContext) { folder := createFolder(t, sc, "Folder") // nolint:staticcheck - command := getCreatePanelCommand(folder.ID, "Library Panel Name") + command := getCreatePanelCommand(folder.ID, folder.UID, "Library Panel Name") sc.reqContext.Req.Body = mockRequestBody(command) resp := sc.service.createHandler(sc.reqContext) result := validateAndUnMarshalResponse(t, resp) @@ -317,27 +318,29 @@ func TestLibraryElementDeletePermissions(t *testing.T) { func TestLibraryElementsWithMissingFolders(t *testing.T) { testScenario(t, "When a user tries to create a library panel in a folder that doesn't exist, it should fail", func(t *testing.T, sc scenarioContext) { - command := getCreatePanelCommand(-100, "Library Panel Name") + command := getCreatePanelCommand(0, "badFolderUID", "Library Panel Name") sc.reqContext.Req.Body = mockRequestBody(command) resp := sc.service.createHandler(sc.reqContext) - require.Equal(t, 404, resp.Status()) + fmt.Println(string(resp.Body())) + require.Equal(t, 400, resp.Status()) }) testScenario(t, "When a user tries to patch a library panel by moving it to a folder that doesn't exist, it should fail", func(t *testing.T, sc scenarioContext) { folder := createFolder(t, sc, "Folder") // nolint:staticcheck - command := getCreatePanelCommand(folder.ID, "Library Panel Name") + command := getCreatePanelCommand(folder.ID, folder.UID, "Library Panel Name") sc.reqContext.Req.Body = mockRequestBody(command) resp := sc.service.createHandler(sc.reqContext) result := validateAndUnMarshalResponse(t, resp) + folderUID := "badFolderUID" // nolint:staticcheck - cmd := model.PatchLibraryElementCommand{FolderID: -100, Version: 1, Kind: int64(model.PanelElement)} + cmd := model.PatchLibraryElementCommand{FolderID: -100, FolderUID: &folderUID, Version: 1, Kind: int64(model.PanelElement)} sc.ctx.Req = web.SetURLParams(sc.ctx.Req, map[string]string{":uid": result.Result.UID}) sc.reqContext.Req.Body = mockRequestBody(cmd) resp = sc.service.patchHandler(sc.reqContext) - require.Equal(t, 404, resp.Status()) + require.Equal(t, 400, resp.Status()) }) } @@ -367,7 +370,7 @@ func TestLibraryElementsGetPermissions(t *testing.T) { func(t *testing.T, sc scenarioContext) { folder := createFolder(t, sc, "Folder") // nolint:staticcheck - cmd := getCreatePanelCommand(folder.ID, "Library Panel") + cmd := getCreatePanelCommand(folder.ID, folder.UID, "Library Panel") sc.reqContext.Req.Body = mockRequestBody(cmd) resp := sc.service.createHandler(sc.reqContext) result := validateAndUnMarshalResponse(t, resp) @@ -418,7 +421,7 @@ func TestLibraryElementsGetAllPermissions(t *testing.T) { for i := 1; i <= 2; i++ { folder := createFolder(t, sc, fmt.Sprintf("Folder%d", i)) // nolint:staticcheck - cmd := getCreatePanelCommand(folder.ID, fmt.Sprintf("Library Panel %d", i)) + cmd := getCreatePanelCommand(folder.ID, folder.UID, fmt.Sprintf("Library Panel %d", i)) sc.reqContext.Req.Body = mockRequestBody(cmd) resp := sc.service.createHandler(sc.reqContext) result := validateAndUnMarshalResponse(t, resp) diff --git a/pkg/services/libraryelements/libraryelements_test.go b/pkg/services/libraryelements/libraryelements_test.go index 1bb0b8abbc..61a8fecf7c 100644 --- a/pkg/services/libraryelements/libraryelements_test.go +++ b/pkg/services/libraryelements/libraryelements_test.go @@ -89,7 +89,7 @@ func TestDeleteLibraryPanelsInFolder(t *testing.T) { Data: simplejson.NewFromAny(dashJSON), } // nolint:staticcheck - dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.ID) + dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.ID, sc.folder.UID) err := sc.service.ConnectElementsToDashboard(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, []string{sc.initialResult.Result.UID}, dashInDB.ID) require.NoError(t, err) @@ -106,7 +106,7 @@ func TestDeleteLibraryPanelsInFolder(t *testing.T) { scenarioWithPanel(t, "When an admin tries to delete a folder that contains disconnected elements, it should delete all disconnected elements too", func(t *testing.T, sc scenarioContext) { // nolint:staticcheck - command := getCreateVariableCommand(sc.folder.ID, "query0") + command := getCreateVariableCommand(sc.folder.ID, sc.folder.UID, "query0") sc.reqContext.Req.Body = mockRequestBody(command) resp := sc.service.createHandler(sc.reqContext) require.Equal(t, 200, resp.Status()) @@ -164,7 +164,7 @@ func TestGetLibraryPanelConnections(t *testing.T) { Data: simplejson.NewFromAny(dashJSON), } // nolint:staticcheck - dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.ID) + dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.ID, sc.folder.UID) err := sc.service.ConnectElementsToDashboard(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, []string{sc.initialResult.Result.UID}, dashInDB.ID) require.NoError(t, err) @@ -203,6 +203,7 @@ type libraryElement struct { OrgID int64 `json:"orgId"` // Deprecated: use FolderUID instead FolderID int64 `json:"folderId"` + FolderUID string `json:"folderUid"` UID string `json:"uid"` Name string `json:"name"` Kind int64 `json:"kind"` @@ -232,8 +233,8 @@ type libraryElementsSearchResult struct { PerPage int `json:"perPage"` } -func getCreatePanelCommand(folderID int64, name string) model.CreateLibraryElementCommand { - command := getCreateCommandWithModel(folderID, name, model.PanelElement, []byte(` +func getCreatePanelCommand(folderID int64, folderUID string, name string) model.CreateLibraryElementCommand { + command := getCreateCommandWithModel(folderID, folderUID, name, model.PanelElement, []byte(` { "datasource": "${DS_GDEV-TESTDATA}", "id": 1, @@ -246,8 +247,8 @@ func getCreatePanelCommand(folderID int64, name string) model.CreateLibraryEleme return command } -func getCreateVariableCommand(folderID int64, name string) model.CreateLibraryElementCommand { - command := getCreateCommandWithModel(folderID, name, model.VariableElement, []byte(` +func getCreateVariableCommand(folderID int64, folderUID, name string) model.CreateLibraryElementCommand { + command := getCreateCommandWithModel(folderID, folderUID, name, model.VariableElement, []byte(` { "datasource": "${DS_GDEV-TESTDATA}", "name": "query0", @@ -259,12 +260,12 @@ func getCreateVariableCommand(folderID int64, name string) model.CreateLibraryEl return command } -func getCreateCommandWithModel(folderID int64, name string, kind model.LibraryElementKind, byteModel []byte) model.CreateLibraryElementCommand { +func getCreateCommandWithModel(folderID int64, folderUID, name string, kind model.LibraryElementKind, byteModel []byte) model.CreateLibraryElementCommand { command := model.CreateLibraryElementCommand{ - FolderID: folderID, // nolint:staticcheck - Name: name, - Model: byteModel, - Kind: int64(kind), + FolderUID: &folderUID, + Name: name, + Model: byteModel, + Kind: int64(kind), } return command @@ -281,9 +282,10 @@ type scenarioContext struct { log log.Logger } -func createDashboard(t *testing.T, sqlStore db.DB, user user.SignedInUser, dash *dashboards.Dashboard, folderID int64) *dashboards.Dashboard { +func createDashboard(t *testing.T, sqlStore db.DB, user user.SignedInUser, dash *dashboards.Dashboard, folderID int64, folderUID string) *dashboards.Dashboard { // nolint:staticcheck dash.FolderID = folderID + dash.FolderUID = folderUID dashItem := &dashboards.SaveDashboardDTO{ Dashboard: dash, Message: "", @@ -398,7 +400,7 @@ func scenarioWithPanel(t *testing.T, desc string, fn func(t *testing.T, sc scena testScenario(t, desc, func(t *testing.T, sc scenarioContext) { // nolint:staticcheck - command := getCreatePanelCommand(sc.folder.ID, "Text - Library Panel") + command := getCreatePanelCommand(sc.folder.ID, sc.folder.UID, "Text - Library Panel") sc.reqContext.Req.Body = mockRequestBody(command) resp := sc.service.createHandler(sc.reqContext) sc.initialResult = validateAndUnMarshalResponse(t, resp) diff --git a/pkg/services/libraryelements/model/model.go b/pkg/services/libraryelements/model/model.go index df8c1abd7a..e53ef13816 100644 --- a/pkg/services/libraryelements/model/model.go +++ b/pkg/services/libraryelements/model/model.go @@ -20,6 +20,7 @@ type LibraryElement struct { OrgID int64 `xorm:"org_id"` // Deprecated: use FolderUID instead FolderID int64 `xorm:"folder_id"` + FolderUID string `xorm:"folder_uid"` UID string `xorm:"uid"` Name string Kind int64 @@ -41,6 +42,7 @@ type LibraryElementWithMeta struct { OrgID int64 `xorm:"org_id"` // Deprecated: use FolderUID instead FolderID int64 `xorm:"folder_id"` + FolderUID string `xorm:"folder_uid"` UID string `xorm:"uid"` Name string Kind int64 @@ -53,7 +55,6 @@ type LibraryElementWithMeta struct { Updated time.Time FolderName string - FolderUID string `xorm:"folder_uid"` ConnectedDashboards int64 CreatedBy int64 UpdatedBy int64 @@ -217,13 +218,14 @@ type GetLibraryElementCommand struct { // SearchLibraryElementsQuery is the query used for searching for Elements type SearchLibraryElementsQuery struct { - PerPage int - Page int - SearchString string - SortDirection string - Kind int - TypeFilter string - ExcludeUID string + PerPage int + Page int + SearchString string + SortDirection string + Kind int + TypeFilter string + ExcludeUID string + // Deprecated: use FolderFilterUIDs instead FolderFilter string FolderFilterUIDs string } diff --git a/pkg/services/libraryelements/writers.go b/pkg/services/libraryelements/writers.go index e1d470f6cd..18781637fa 100644 --- a/pkg/services/libraryelements/writers.go +++ b/pkg/services/libraryelements/writers.go @@ -82,6 +82,7 @@ type FolderFilter struct { func parseFolderFilter(query model.SearchLibraryElementsQuery) FolderFilter { folderIDs := make([]string, 0) folderUIDs := make([]string, 0) + // nolint:staticcheck hasFolderFilter := len(strings.TrimSpace(query.FolderFilter)) > 0 hasFolderFilterUID := len(strings.TrimSpace(query.FolderFilterUIDs)) > 0 @@ -100,6 +101,7 @@ func parseFolderFilter(query model.SearchLibraryElementsQuery) FolderFilter { if hasFolderFilter { result.includeGeneralFolder = false + // nolint:staticcheck folderIDs = strings.Split(query.FolderFilter, ",") metrics.MFolderIDsServiceCount.WithLabelValues(metrics.LibraryElements).Inc() // nolint:staticcheck diff --git a/pkg/services/librarypanels/librarypanels.go b/pkg/services/librarypanels/librarypanels.go index 244f925dde..91fd6107a5 100644 --- a/pkg/services/librarypanels/librarypanels.go +++ b/pkg/services/librarypanels/librarypanels.go @@ -42,7 +42,7 @@ func ProvideService(cfg *setting.Cfg, sqlStore db.DB, routeRegister routing.Rout // Service is a service for operating on library panels. type Service interface { ConnectLibraryPanelsForDashboard(c context.Context, signedInUser identity.Requester, dash *dashboards.Dashboard) error - ImportLibraryPanelsForDashboard(c context.Context, signedInUser identity.Requester, libraryPanels *simplejson.Json, panels []any, folderID int64) error + ImportLibraryPanelsForDashboard(c context.Context, signedInUser identity.Requester, libraryPanels *simplejson.Json, panels []any, folderID int64, folderUID string) error } type LibraryInfo struct { @@ -117,11 +117,11 @@ func connectLibraryPanelsRecursively(c context.Context, panels []any, libraryPan } // ImportLibraryPanelsForDashboard loops through all panels in dashboard JSON and creates any missing library panels in the database. -func (lps *LibraryPanelService) ImportLibraryPanelsForDashboard(c context.Context, signedInUser identity.Requester, libraryPanels *simplejson.Json, panels []any, folderID int64) error { - return importLibraryPanelsRecursively(c, lps.LibraryElementService, signedInUser, libraryPanels, panels, folderID) +func (lps *LibraryPanelService) ImportLibraryPanelsForDashboard(c context.Context, signedInUser identity.Requester, libraryPanels *simplejson.Json, panels []any, folderID int64, folderUID string) error { + return importLibraryPanelsRecursively(c, lps.LibraryElementService, signedInUser, libraryPanels, panels, folderID, folderUID) } -func importLibraryPanelsRecursively(c context.Context, service libraryelements.Service, signedInUser identity.Requester, libraryPanels *simplejson.Json, panels []any, folderID int64) error { +func importLibraryPanelsRecursively(c context.Context, service libraryelements.Service, signedInUser identity.Requester, libraryPanels *simplejson.Json, panels []any, folderID int64, folderUID string) error { for _, panel := range panels { panelAsJSON := simplejson.NewFromAny(panel) libraryPanel := panelAsJSON.Get("libraryPanel") @@ -132,7 +132,7 @@ func importLibraryPanelsRecursively(c context.Context, service libraryelements.S // we have a row if panelType == "row" { - err := importLibraryPanelsRecursively(c, service, signedInUser, libraryPanels, panelAsJSON.Get("panels").MustArray(), folderID) + err := importLibraryPanelsRecursively(c, service, signedInUser, libraryPanels, panelAsJSON.Get("panels").MustArray(), folderID, folderUID) if err != nil { return err } @@ -168,11 +168,12 @@ func importLibraryPanelsRecursively(c context.Context, service libraryelements.S metrics.MFolderIDsServiceCount.WithLabelValues(metrics.LibraryPanels).Inc() var cmd = model.CreateLibraryElementCommand{ - FolderID: folderID, // nolint:staticcheck - Name: name, - Model: Model, - Kind: int64(model.PanelElement), - UID: UID, + FolderID: folderID, // nolint:staticcheck + FolderUID: &folderUID, + Name: name, + Model: Model, + Kind: int64(model.PanelElement), + UID: UID, } _, err = service.CreateElement(c, signedInUser, cmd) if err != nil { diff --git a/pkg/services/librarypanels/librarypanels_test.go b/pkg/services/librarypanels/librarypanels_test.go index dae0821acf..577ee20aeb 100644 --- a/pkg/services/librarypanels/librarypanels_test.go +++ b/pkg/services/librarypanels/librarypanels_test.go @@ -86,8 +86,7 @@ func TestConnectLibraryPanelsForDashboard(t *testing.T) { Title: "Testing ConnectLibraryPanelsForDashboard", Data: simplejson.NewFromAny(dashJSON), } - // nolint:staticcheck - dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.ID) + dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash) err := sc.service.ConnectLibraryPanelsForDashboard(sc.ctx, sc.user, dashInDB) require.NoError(t, err) @@ -101,8 +100,7 @@ func TestConnectLibraryPanelsForDashboard(t *testing.T) { scenarioWithLibraryPanel(t, "When an admin tries to store a dashboard with library panels inside and outside of rows, it should connect all", func(t *testing.T, sc scenarioContext) { cmd := model.CreateLibraryElementCommand{ - FolderID: sc.initialResult.Result.FolderID, // nolint:staticcheck - Name: "Outside row", + Name: "Outside row", Model: []byte(` { "datasource": "${DS_GDEV-TESTDATA}", @@ -112,7 +110,8 @@ func TestConnectLibraryPanelsForDashboard(t *testing.T) { "description": "A description" } `), - Kind: int64(model.PanelElement), + Kind: int64(model.PanelElement), + FolderUID: &sc.folder.UID, } outsidePanel, err := sc.elementService.CreateElement(sc.ctx, sc.user, cmd) require.NoError(t, err) @@ -185,8 +184,7 @@ func TestConnectLibraryPanelsForDashboard(t *testing.T) { Title: "Testing ConnectLibraryPanelsForDashboard", Data: simplejson.NewFromAny(dashJSON), } - // nolint:staticcheck - dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.ID) + dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash) err = sc.service.ConnectLibraryPanelsForDashboard(sc.ctx, sc.user, dashInDB) require.NoError(t, err) @@ -232,8 +230,7 @@ func TestConnectLibraryPanelsForDashboard(t *testing.T) { Title: "Testing ConnectLibraryPanelsForDashboard", Data: simplejson.NewFromAny(dashJSON), } - // nolint:staticcheck - dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.ID) + dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash) err := sc.service.ConnectLibraryPanelsForDashboard(sc.ctx, sc.user, dashInDB) require.EqualError(t, err, errLibraryPanelHeaderUIDMissing.Error()) @@ -242,8 +239,7 @@ func TestConnectLibraryPanelsForDashboard(t *testing.T) { scenarioWithLibraryPanel(t, "When an admin tries to store a dashboard with unused/removed library panels, it should disconnect unused/removed library panels", func(t *testing.T, sc scenarioContext) { unused, err := sc.elementService.CreateElement(sc.ctx, sc.user, model.CreateLibraryElementCommand{ - FolderID: sc.folder.ID, // nolint:staticcheck - Name: "Unused Libray Panel", + Name: "Unused Libray Panel", Model: []byte(` { "datasource": "${DS_GDEV-TESTDATA}", @@ -253,7 +249,8 @@ func TestConnectLibraryPanelsForDashboard(t *testing.T) { "description": "Unused description" } `), - Kind: int64(model.PanelElement), + Kind: int64(model.PanelElement), + FolderUID: &sc.folder.UID, }) require.NoError(t, err) dashJSON := map[string]any{ @@ -289,8 +286,7 @@ func TestConnectLibraryPanelsForDashboard(t *testing.T) { Title: "Testing ConnectLibraryPanelsForDashboard", Data: simplejson.NewFromAny(dashJSON), } - // nolint:staticcheck - dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.ID) + dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash) err = sc.elementService.ConnectElementsToDashboard(sc.ctx, sc.user, []string{sc.initialResult.Result.UID}, dashInDB.ID) require.NoError(t, err) @@ -399,7 +395,7 @@ func TestImportLibraryPanelsForDashboard(t *testing.T) { require.EqualError(t, err, model.ErrLibraryElementNotFound.Error()) - err = sc.service.ImportLibraryPanelsForDashboard(sc.ctx, sc.user, simplejson.NewFromAny(libraryElements), panels, 0) + err = sc.service.ImportLibraryPanelsForDashboard(sc.ctx, sc.user, simplejson.NewFromAny(libraryElements), panels, 0, "") require.NoError(t, err) element, err := sc.elementService.GetElement(sc.ctx, sc.user, @@ -440,14 +436,14 @@ func TestImportLibraryPanelsForDashboard(t *testing.T) { require.NoError(t, err) // nolint:staticcheck - err = sc.service.ImportLibraryPanelsForDashboard(sc.ctx, sc.user, simplejson.New(), panels, sc.folder.ID) + err = sc.service.ImportLibraryPanelsForDashboard(sc.ctx, sc.user, simplejson.New(), panels, sc.folder.ID, sc.folder.UID) require.NoError(t, err) element, err := sc.elementService.GetElement(sc.ctx, sc.user, model.GetLibraryElementCommand{UID: existingUID, FolderName: dashboards.RootFolderName}) require.NoError(t, err) var expected = getExpected(t, element, existingUID, existingName, sc.initialResult.Result.Model) - expected.FolderID = sc.initialResult.Result.FolderID + expected.FolderUID = sc.initialResult.Result.FolderUID expected.Description = sc.initialResult.Result.Description expected.Meta.FolderUID = sc.folder.UID expected.Meta.FolderName = sc.folder.Title @@ -556,7 +552,7 @@ func TestImportLibraryPanelsForDashboard(t *testing.T) { _, err = sc.elementService.GetElement(sc.ctx, sc.user, model.GetLibraryElementCommand{UID: insideUID, FolderName: dashboards.RootFolderName}) require.EqualError(t, err, model.ErrLibraryElementNotFound.Error()) - err = sc.service.ImportLibraryPanelsForDashboard(sc.ctx, sc.user, simplejson.NewFromAny(libraryElements), panels, 0) + err = sc.service.ImportLibraryPanelsForDashboard(sc.ctx, sc.user, simplejson.NewFromAny(libraryElements), panels, 0, "") require.NoError(t, err) element, err := sc.elementService.GetElement(sc.ctx, sc.user, model.GetLibraryElementCommand{UID: outsideUID, FolderName: dashboards.RootFolderName}) @@ -578,9 +574,11 @@ func TestImportLibraryPanelsForDashboard(t *testing.T) { } type libraryPanel struct { - ID int64 - OrgID int64 + ID int64 + OrgID int64 + // Deprecated: use FolderUID instead FolderID int64 + FolderUID string UID string Name string Type string @@ -616,6 +614,7 @@ type libraryElement struct { ID int64 `json:"id"` OrgID int64 `json:"orgId"` FolderID int64 `json:"folderId"` + FolderUID string `json:"folderUid"` UID string `json:"uid"` Name string `json:"name"` Kind int64 `json:"kind"` @@ -649,7 +648,6 @@ func toLibraryElement(t *testing.T, res model.LibraryElementDTO) libraryElement return libraryElement{ ID: res.ID, OrgID: res.OrgID, - FolderID: res.FolderID, // nolint:staticcheck UID: res.UID, Name: res.Name, Type: res.Type, @@ -715,9 +713,7 @@ func getExpected(t *testing.T, res model.LibraryElementDTO, UID string, name str } } -func createDashboard(t *testing.T, sqlStore db.DB, user *user.SignedInUser, dash *dashboards.Dashboard, folderID int64) *dashboards.Dashboard { - // nolint:staticcheck - dash.FolderID = folderID +func createDashboard(t *testing.T, sqlStore db.DB, user *user.SignedInUser, dash *dashboards.Dashboard) *dashboards.Dashboard { dashItem := &dashboards.SaveDashboardDTO{ Dashboard: dash, Message: "", @@ -774,8 +770,9 @@ func scenarioWithLibraryPanel(t *testing.T, desc string, fn func(t *testing.T, s testScenario(t, desc, func(t *testing.T, sc scenarioContext) { command := model.CreateLibraryElementCommand{ - FolderID: sc.folder.ID, // nolint:staticcheck - Name: "Text - Library Panel", + FolderID: sc.folder.ID, // nolint:staticcheck + FolderUID: &sc.folder.UID, + Name: "Text - Library Panel", Model: []byte(` { "datasource": "${DS_GDEV-TESTDATA}", @@ -797,7 +794,7 @@ func scenarioWithLibraryPanel(t *testing.T, desc string, fn func(t *testing.T, s Result: libraryPanel{ ID: resp.ID, OrgID: resp.OrgID, - FolderID: resp.FolderID, // nolint:staticcheck + FolderUID: resp.FolderUID, UID: resp.UID, Name: resp.Name, Type: resp.Type, diff --git a/pkg/services/sqlstore/migrations/libraryelements.go b/pkg/services/sqlstore/migrations/libraryelements.go index 3215cb085b..88a6a3d023 100644 --- a/pkg/services/sqlstore/migrations/libraryelements.go +++ b/pkg/services/sqlstore/migrations/libraryelements.go @@ -61,4 +61,26 @@ func addLibraryElementsMigrations(mg *migrator.Migrator) { mg.AddMigration("alter library_element model to mediumtext", migrator.NewRawSQLMigration(""). Mysql("ALTER TABLE library_element MODIFY model MEDIUMTEXT NOT NULL;")) + + q := `UPDATE library_element + SET folder_uid = dashboard.uid + FROM dashboard + WHERE library_element.folder_id = dashboard.folder_id AND library_element.org_id = dashboard.org_id` + + if mg.Dialect.DriverName() == migrator.MySQL { + q = `UPDATE library_element + SET folder_uid = ( + SELECT dashboard.uid + FROM dashboard + WHERE library_element.folder_id = dashboard.folder_id AND library_element.org_id = dashboard.org_id + )` + } + + mg.AddMigration("add library_element folder uid", migrator.NewAddColumnMigration(libraryElementsV1, &migrator.Column{ + Name: "folder_uid", Type: migrator.DB_NVarchar, Length: 40, Nullable: true, + })) + + mg.AddMigration("populate library_element folder_uid", migrator.NewRawSQLMigration(q)) + + mg.AddMigration("add index library_element org_id-folder_uid-name-kind", migrator.NewAddIndexMigration(libraryElementsV1, &migrator.Index{Cols: []string{"org_id", "folder_uid", "name", "kind"}, Type: migrator.UniqueIndex})) } From 75b020c19de1383bfbeab1954a3a2468e83a9b59 Mon Sep 17 00:00:00 2001 From: Misi Date: Fri, 1 Mar 2024 10:39:50 +0100 Subject: [PATCH 0324/1406] Cfg: Add a setting to configure if the local file system is available (#83616) * Introduce environment.local_filesystem_available * Only show TLS client cert, client key, client ca when local_filesystem_available is true * Rename LocalFSAvailable to LocalFileSystemAvailable --- conf/defaults.ini | 4 ++++ packages/grafana-data/src/types/config.ts | 1 + packages/grafana-runtime/src/config.ts | 1 + pkg/api/dtos/frontend_settings.go | 2 ++ pkg/api/frontendsettings.go | 1 + pkg/setting/setting.go | 3 +++ public/app/features/auth-config/fields.tsx | 4 ++++ 7 files changed, 16 insertions(+) diff --git a/conf/defaults.ini b/conf/defaults.ini index 2d3f108f5b..93632560e1 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -94,6 +94,10 @@ read_timeout = 0 #exampleHeader1 = exampleValue1 #exampleHeader2 = exampleValue2 +[environment] +# Sets whether the local file system is available for Grafana to use. Default is true for backward compatibility. +local_file_system_available = true + #################################### GRPC Server ######################### [grpc_server] network = "tcp" diff --git a/packages/grafana-data/src/types/config.ts b/packages/grafana-data/src/types/config.ts index c3e4cea9b0..d5a2c259ba 100644 --- a/packages/grafana-data/src/types/config.ts +++ b/packages/grafana-data/src/types/config.ts @@ -226,6 +226,7 @@ export interface GrafanaConfig { sqlConnectionLimits: SqlConnectionLimits; sharedWithMeFolderUID?: string; rootFolderUID?: string; + localFileSystemAvailable?: boolean; // The namespace to use for kubernetes apiserver requests namespace: string; diff --git a/packages/grafana-runtime/src/config.ts b/packages/grafana-runtime/src/config.ts index 2ff143918d..3071909851 100644 --- a/packages/grafana-runtime/src/config.ts +++ b/packages/grafana-runtime/src/config.ts @@ -171,6 +171,7 @@ export class GrafanaBootConfig implements GrafanaConfig { disableFrontendSandboxForPlugins: string[] = []; sharedWithMeFolderUID: string | undefined; rootFolderUID: string | undefined; + localFileSystemAvailable: boolean | undefined; constructor(options: GrafanaBootConfig) { this.bootData = options.bootData; diff --git a/pkg/api/dtos/frontend_settings.go b/pkg/api/dtos/frontend_settings.go index 8c9bcd6ee8..10c7853176 100644 --- a/pkg/api/dtos/frontend_settings.go +++ b/pkg/api/dtos/frontend_settings.go @@ -260,4 +260,6 @@ type FrontendSettingsDTO struct { // Enterprise Licensing *FrontendSettingsLicensingDTO `json:"licensing,omitempty"` Whitelabeling *FrontendSettingsWhitelabelingDTO `json:"whitelabeling,omitempty"` + + LocalFileSystemAvailable bool `json:"localFileSystemAvailable"` } diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index bc4ca18ffb..29d678c100 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -220,6 +220,7 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro PublicDashboardsEnabled: hs.Cfg.PublicDashboardsEnabled, SharedWithMeFolderUID: folder.SharedWithMeFolderUID, RootFolderUID: accesscontrol.GeneralFolderUID, + LocalFileSystemAvailable: hs.Cfg.LocalFileSystemAvailable, BuildInfo: dtos.FrontendSettingsBuildInfoDTO{ HideVersion: hideVersion, diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 9a25a70bad..d9f8f487ce 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -366,6 +366,8 @@ type Cfg struct { StackID string Slug string + LocalFileSystemAvailable bool + // Deprecated ForceMigration bool @@ -1063,6 +1065,7 @@ func (cfg *Cfg) parseINIFile(iniFile *ini.File) error { cfg.Env = valueAsString(iniFile.Section(""), "app_mode", "development") cfg.StackID = valueAsString(iniFile.Section("environment"), "stack_id", "") cfg.Slug = valueAsString(iniFile.Section("environment"), "stack_slug", "") + cfg.LocalFileSystemAvailable = iniFile.Section("environment").Key("local_file_system_available").MustBool(true) //nolint:staticcheck cfg.ForceMigration = iniFile.Section("").Key("force_migration").MustBool(false) cfg.InstanceName = valueAsString(iniFile.Section(""), "instance_name", "unknown_instance_name") diff --git a/public/app/features/auth-config/fields.tsx b/public/app/features/auth-config/fields.tsx index 4a93b8f1cc..5a095e4f95 100644 --- a/public/app/features/auth-config/fields.tsx +++ b/public/app/features/auth-config/fields.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { validate as uuidValidate } from 'uuid'; +import { config } from '@grafana/runtime'; import { TextLink } from '@grafana/ui'; import { contextSrv } from 'app/core/core'; @@ -342,16 +343,19 @@ export function fieldMap(provider: string): Record { label: 'TLS client ca', description: 'The file path to the trusted certificate authority list. Is not applicable on Grafana Cloud.', type: 'text', + hidden: !config.localFileSystemAvailable, }, tlsClientCert: { label: 'TLS client cert', description: 'The file path to the certificate. Is not applicable on Grafana Cloud.', type: 'text', + hidden: !config.localFileSystemAvailable, }, tlsClientKey: { label: 'TLS client key', description: 'The file path to the key. Is not applicable on Grafana Cloud.', type: 'text', + hidden: !config.localFileSystemAvailable, }, tlsSkipVerifyInsecure: { label: 'TLS skip verify', From 824c26cd5ebf45ce3d33b5fd57063c693f1973d5 Mon Sep 17 00:00:00 2001 From: linoman <2051016+linoman@users.noreply.github.com> Date: Fri, 1 Mar 2024 03:56:26 -0600 Subject: [PATCH 0325/1406] Password Policy: add documentation (#83208) * add documentation * Update docs/sources/setup-grafana/configure-security/configure-authentication/grafana/index.md --------- Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com> --- .../configure-authentication/grafana/index.md | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/grafana/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/grafana/index.md index 05db615313..2b912ce8f8 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/grafana/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/grafana/index.md @@ -112,6 +112,27 @@ To disable basic auth: enabled = false ``` +### Strong password policy + +By default, the password policy for all basic auth users is set to a minimum of four characters. You can enable a stronger password policy with the `password_policy` configuration option. + +With the `password_policy` option enabled, new and updated passwords must meet the following criteria: + +- At least 12 characters +- At least one uppercase letter +- At least one lowercase letter +- At least one number +- At least one special character + +```bash +[auth.basic] +password_policy = true +``` + +{{% admonition type="note" %}} +Existing passwords that don't comply with the new password policy will not be impacted until the user updates their password. +{{% /admonition %}} + ### Disable login form You can hide the Grafana login form using the below configuration settings. From 1a7af2d8430b77854f1e9779c0a8a44e3d9c961a Mon Sep 17 00:00:00 2001 From: Anton Patsev <10828883+patsevanton@users.noreply.github.com> Date: Fri, 1 Mar 2024 16:00:15 +0600 Subject: [PATCH 0326/1406] =?UTF-8?q?=D0=A1orrection=20of=20spelling=20err?= =?UTF-8?q?ors=20(#83565)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit thanks for your contribution ! --- GOVERNANCE.md | 2 +- contribute/ISSUE_TRIAGE.md | 8 ++++---- contribute/backend/errors.md | 2 +- contribute/backend/instrumentation.md | 2 +- contribute/backend/style-guide.md | 4 ++-- contribute/create-pull-request.md | 4 ++-- contribute/developer-guide.md | 4 ++-- contribute/engineering/terminology.md | 2 +- 8 files changed, 14 insertions(+), 14 deletions(-) diff --git a/GOVERNANCE.md b/GOVERNANCE.md index def47f1a1c..ff129a8755 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -173,7 +173,7 @@ Supermajority votes must be called explicitly in a separate thread on the approp Votes may take the form of a single proposal, with the option to vote yes or no, or the form of multiple alternatives. -A vote on a single proposal is considered successful if at least two thirds of those eligible to vote vote in favor. +A vote on a single proposal is considered successful if at least two thirds of those eligible to vote in favor. If there are multiple alternatives, members may vote for one or more alternatives, or vote “no” to object to all alternatives. A vote on multiple alternatives is considered decided in favor of one alternative if it has received the most votes in favor, and a vote from at least two thirds of those eligible to vote. Should no alternative reach this quorum, another vote on a reduced number of options may be called separately. diff --git a/contribute/ISSUE_TRIAGE.md b/contribute/ISSUE_TRIAGE.md index 336b6497a2..70ee5c5e7b 100644 --- a/contribute/ISSUE_TRIAGE.md +++ b/contribute/ISSUE_TRIAGE.md @@ -4,7 +4,7 @@ The main goal of issue triage is to categorize all incoming Grafana issues and m > **Note:** This information is for Grafana project Maintainers, Owners, and Admins. If you are a Contributor, then you will not be able to perform most of the tasks in this topic. -The core maintainers of the Grafana project are responsible for categorizing all incoming issues and delegating any critical or important issue to other maintainers. Currently one maintainer each week is responsible. Besides that part, triage provides an important way to contribute to an open source project. +The core maintainers of the Grafana project are responsible for categorizing all incoming issues and delegating any critical or important issue to other maintainers. Currently, one maintainer each week is responsible. Besides that part, triage provides an important way to contribute to an open source project. Triage helps ensure issues resolve quickly by: @@ -136,13 +136,13 @@ To make it easier for everyone to understand and find issues they're searching f Depending on the issue, you might not feel all this information is needed. Use your best judgement. If you cannot triage an issue using what its author provided, explain kindly to the author that they must provide the above information to clarify the problem. Label issue with `needs more info` and add any related `area/*` or `datasource/*` labels. Alternatively, use `bot/needs more info` label and the Grafana bot will request it for you. -If the author provides the standard information but you are still unable to triage the issue, request additional information. Do this kindly and politely because you are asking for more of the author's time. +If the author provides the standard information, but you are still unable to triage the issue, request additional information. Do this kindly and politely because you are asking for more of the author's time. If the author does not respond to the requested information within the timespan of a week, close the issue with a kind note stating that the author can request for the issue to be reopened when the necessary information is provided. When you feel you have all the information needed you're ready to [categorizing the issue](#3-categorizing-an-issue). -If you receive a notification with additional information provided but you are not anymore on issue triage and you feel you do not have time to handle it, you should delegate it to the current person on issue triage. +If you receive a notification with additional information provided, but you are not anymore on issue triage and you feel you do not have time to handle it, you should delegate it to the current person on issue triage. ## 3. Categorizing an issue @@ -312,7 +312,7 @@ When an issue has all basic information provided, but the triage responsible hav Investigating issues can be a very time consuming task, especially for the maintainers, given the huge number of combinations of plugins, data sources, platforms, databases, browsers, tools, hardware, integrations, versions and cloud services, etc that are being used with Grafana. There is a certain number of combinations that are more common than others, and these are in general easier for maintainers to investigate. -For some other combinations it may not be possible at all for a maintainer to setup a proper test environment to investigate the issue. In these cases we really appreciate any help we can get from the community. Otherwise the issue is highly likely to be closed. +For some other combinations it may not be possible at all for a maintainer to setup a proper test environment to investigate the issue. In these cases we really appreciate any help we can get from the community. Otherwise, the issue is highly likely to be closed. Even if you don't have the time or knowledge to investigate an issue we highly recommend that you [upvote](https://github.blog/2016-03-10-add-reactions-to-pull-requests-issues-and-comments) the issue if you happen to have the same problem. If you have further details that may help investigating the issue please provide as much information as possible. diff --git a/contribute/backend/errors.md b/contribute/backend/errors.md index 1d3ba3c922..2d79cb8a6e 100644 --- a/contribute/backend/errors.md +++ b/contribute/backend/errors.md @@ -109,7 +109,7 @@ fully Go modules compatible, but can be viewed using ### Error source You can optionally specify an error source that describes from where an -error originates. By default it's _server_ and means the error originates +error originates. By default, it's _server_ and means the error originates from within the application, e.g. Grafana. The `errutil.WithDownstream()` option may be appended to the NewBase function call to denote an error originates from a _downstream_ server/service. The error source information diff --git a/contribute/backend/instrumentation.md b/contribute/backend/instrumentation.md index a845825828..d6d50c19c8 100644 --- a/contribute/backend/instrumentation.md +++ b/contribute/backend/instrumentation.md @@ -80,7 +80,7 @@ func doSomething(ctx context.Context) { ### Enable certain log levels for certain loggers -During development it's convenient to enable certain log level, e.g. debug, for certain loggers to minimize the generated log output and make it easier to find things. See [[log.filters]](https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#filters) for information how to configure this. +During development, it's convenient to enable certain log level, e.g. debug, for certain loggers to minimize the generated log output and make it easier to find things. See [[log.filters]](https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#filters) for information how to configure this. It's also possible to configure multiple loggers: diff --git a/contribute/backend/style-guide.md b/contribute/backend/style-guide.md index 4d50bf95b2..a0877c3b44 100644 --- a/contribute/backend/style-guide.md +++ b/contribute/backend/style-guide.md @@ -33,7 +33,7 @@ Tests must use the standard library, `testing`. For assertions, prefer using [te We have a [testsuite](https://github.com/grafana/grafana/tree/main/pkg/tests/testsuite) package which provides utilities for package-level setup and teardown. -Currently this is just used to ensure that test databases are correctly set up and torn down, but it also provides a place we can attach future tasks. +Currently, this is just used to ensure that test databases are correctly set up and torn down, but it also provides a place we can attach future tasks. Each package SHOULD include a [TestMain](https://pkg.go.dev/testing#hdr-Main) function that calls `testsuite.Run(m)`: @@ -78,7 +78,7 @@ func TestIntegrationFoo(t *testing.T) { Use respectively [`assert.*`](https://github.com/stretchr/testify#assert-package) functions to make assertions that should _not_ halt the test ("soft checks") and [`require.*`](https://github.com/stretchr/testify#require-package) -functions to make assertions that _should_ halt the test ("hard checks"). Typically you want to use the latter type of +functions to make assertions that _should_ halt the test ("hard checks"). Typically, you want to use the latter type of check to assert that errors have or have not happened, since continuing the test after such an assertion fails is chaotic (the system under test will be in an undefined state) and you'll often have segfaults in practice. diff --git a/contribute/create-pull-request.md b/contribute/create-pull-request.md index 7fe1023a89..0e41754ea4 100644 --- a/contribute/create-pull-request.md +++ b/contribute/create-pull-request.md @@ -124,7 +124,7 @@ If you're unsure, see the existing [changelog](https://github.com/grafana/grafan The pull request title should be formatted according to `: ` (Both "Area" and "Summary" should start with a capital letter). -The Grafana team _squashes_ all commits into one when we accept a pull request. The title of the pull request becomes the subject line of the squashed commit message. We still encourage contributors to write informative commit messages, as they becomes a part of the Git commit body. +The Grafana team _squashes_ all commits into one when we accept a pull request. The title of the pull request becomes the subject line of the squashed commit message. We still encourage contributors to write informative commit messages, as they become a part of the Git commit body. We use the pull request title when we generate change logs for releases. As such, we strive to make the title as informative as possible. @@ -133,7 +133,7 @@ We use the pull request title when we generate change logs for releases. As such ## Configuration changes -If your PR includes configuration changes, all of the following files must be changed correspondingly: +If your PR includes configuration changes, all the following files must be changed correspondingly: - conf/defaults.ini - conf/sample.ini diff --git a/contribute/developer-guide.md b/contribute/developer-guide.md index 9d8b500f57..f31d4a91a3 100644 --- a/contribute/developer-guide.md +++ b/contribute/developer-guide.md @@ -151,7 +151,7 @@ go run build.go test ### Run SQLLite, PostgreSQL and MySQL integration tests -By default grafana runs SQLite, to run test with SQLite +By default, grafana runs SQLite, to run test with SQLite ```bash go test -covermode=atomic -tags=integration ./pkg/... @@ -330,7 +330,7 @@ For some people, typically using the bash shell, ulimit fails with an error simi ulimit: open files: cannot modify limit: Operation not permitted ``` -If that happens to you, chances are you've already set a lower limit and your shell won't let you set a higher one. Try looking in your shell initialization files (~/.bashrc typically), if there's already a ulimit command that you can tweak. +If that happens to you, chances are you've already set a lower limit and your shell won't let you set a higher one. Try looking in your shell initialization files (~/.bashrc typically), if there's already an ulimit command that you can tweak. ## Next steps diff --git a/contribute/engineering/terminology.md b/contribute/engineering/terminology.md index 4a833dd7fb..5b28cb7f4b 100644 --- a/contribute/engineering/terminology.md +++ b/contribute/engineering/terminology.md @@ -6,7 +6,7 @@ This document defines technical terms used in Grafana. ## TLS/SSL -The acronyms [TLS](https://en.wikipedia.org/wiki/Transport_Layer_Security) (Transport Layer Security and +The acronyms [TLS](https://en.wikipedia.org/wiki/Transport_Layer_Security) (Transport Layer Security) and [SSL](https://en.wikipedia.org/wiki/SSL) (Secure Socket Layer) are both used to describe the HTTPS security layer, and are in practice synonymous. However, TLS is considered the current name for the technology, and SSL is considered [deprecated](https://tools.ietf.org/html/rfc7568). From 1cec975a66f77403ad327b082910742be575432d Mon Sep 17 00:00:00 2001 From: Levente Balogh Date: Fri, 1 Mar 2024 11:13:16 +0100 Subject: [PATCH 0327/1406] Docs: Update "What's new in G10?" (#83467) * docs: add info about the react-router migration into v10 what's new * fix: linting issue * Update docs/sources/whatsnew/whats-new-in-v10-0.md Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> * docs: update linting --------- Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> --- docs/sources/whatsnew/whats-new-in-v10-0.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/sources/whatsnew/whats-new-in-v10-0.md b/docs/sources/whatsnew/whats-new-in-v10-0.md index 4ed11cd5d7..df622eac40 100644 --- a/docs/sources/whatsnew/whats-new-in-v10-0.md +++ b/docs/sources/whatsnew/whats-new-in-v10-0.md @@ -418,3 +418,9 @@ Some data sources, like MySQL databases, Prometheus instances or Elasticsearch c To query these data sources from Grafana Cloud, you've had to open your private network to a range of IP addresses, a non-starter for many IT Security teams. The challenge is, how do you connect to your private data from Grafana Cloud, without exposing your network? The answer is Private Data Source Connect (PDC), available now in public preview in Grafana Cloud Pro and Advanced. PDC uses SOCKS over SSH to establish a secure connection between a lightweight PDC agent you deploy on your network and your Grafana Cloud stack. PDC keeps the network connection totally under your control. It’s easy to set up and manage, uses industry-standard security protocols, and works across public cloud vendors and a wide variety of secure networks. Learn more in our [Private data source connect documentation](/docs/grafana-cloud/data-configuration/configure-private-datasource-connect). + +## Plugins + +### App plugins can start using react-router v6 + +We've added support for using `react-router` v6 in app plugins. However, we still support the use of `react-router` v5 for plugins that need to support a minimum Grafana version earlier than v10. For more information, refer to our [react-router migration guide](https://grafana.com/developers/plugin-tools/migration-guides/update-from-grafana-versions/migrate-9_x-to-10_x#update-to-react-router-v6). From 36a19bfa838e7f57651297130e6dd10e765c6f22 Mon Sep 17 00:00:00 2001 From: Jo Date: Fri, 1 Mar 2024 11:31:06 +0100 Subject: [PATCH 0328/1406] AuthProxy: Allow disabling Auth Proxy cache (#83755) * extract auth proxy settings * simplify auth proxy methods * add doc mentions --- conf/defaults.ini | 4 +- .../auth-proxy/index.md | 7 +- pkg/api/datasources.go | 4 +- pkg/api/datasources_test.go | 14 ++-- pkg/api/frontendsettings.go | 4 +- pkg/api/login.go | 4 +- pkg/api/login_test.go | 8 +-- pkg/api/user.go | 6 +- .../usagestats/service/usage_stats_test.go | 2 +- .../usagestats/statscollector/service_test.go | 2 +- pkg/services/authn/authnimpl/service.go | 2 +- pkg/services/authn/authnimpl/usage_stats.go | 2 +- .../authn/authnimpl/usage_stats_test.go | 2 +- pkg/services/authn/clients/grafana.go | 6 +- pkg/services/authn/clients/grafana_test.go | 4 +- pkg/services/authn/clients/proxy.go | 70 ++++++++++++------- pkg/services/authn/clients/proxy_test.go | 12 ++-- pkg/services/contexthandler/contexthandler.go | 6 +- .../contexthandler/contexthandler_test.go | 6 +- .../datasources/service/datasource.go | 2 +- pkg/setting/setting.go | 36 +--------- pkg/setting/setting_auth_proxy.go | 45 ++++++++++++ pkg/setting/setting_test.go | 7 +- 23 files changed, 145 insertions(+), 110 deletions(-) create mode 100644 pkg/setting/setting_auth_proxy.go diff --git a/conf/defaults.ini b/conf/defaults.ini index 93632560e1..a3772b884e 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -832,7 +832,7 @@ enabled = false header_name = X-WEBAUTH-USER header_property = username auto_sign_up = true -sync_ttl = 60 +sync_ttl = 15 whitelist = headers = headers_encoded = false @@ -1305,7 +1305,7 @@ loki_basic_auth_password = # mylabelkey = mylabelvalue [unified_alerting.state_history.annotations] -# Controls retention of annotations automatically created while evaluating alert rules. +# Controls retention of annotations automatically created while evaluating alert rules. # Alert state history backend must be configured to be annotations (see setting [unified_alerting.state_history].backend). # Configures how long alert annotations are stored for. Default is 0, which keeps them forever. diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/auth-proxy/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/auth-proxy/index.md index 287d146bf6..5ed036bb6e 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/auth-proxy/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/auth-proxy/index.md @@ -36,7 +36,8 @@ header_property = username auto_sign_up = true # Define cache time to live in minutes # If combined with Grafana LDAP integration it is also the sync interval -sync_ttl = 60 +# Set to 0 to always fetch and sync the latest user data +sync_ttl = 15 # Limit where auth proxy requests come from by configuring a list of IP addresses. # This can be used to prevent users spoofing the X-WEBAUTH-USER header. # Example `whitelist = 192.168.1.1, 192.168.1.0/24, 2001::23, 2001::0/120` @@ -231,6 +232,10 @@ ProxyPassReverse / http://grafana:3000/ With our Grafana and Apache containers running, you can now connect to http://localhost/ and log in using the username/password we created in the htpasswd file. +{{% admonition type="note" %}} +If the user is deleted from Grafana, the user will be not be able to login and resync until after the `sync_ttl` has expired. +{{% /admonition %}} + ### Team Sync (Enterprise only) > Only available in Grafana Enterprise v6.3+ diff --git a/pkg/api/datasources.go b/pkg/api/datasources.go index 872e93dd55..f9182da0c7 100644 --- a/pkg/api/datasources.go +++ b/pkg/api/datasources.go @@ -344,11 +344,11 @@ func validateJSONData(ctx context.Context, jsonData *simplejson.Json, cfg *setti return nil } - if cfg.AuthProxyEnabled { + if cfg.AuthProxy.Enabled { for key, value := range jsonData.MustMap() { if strings.HasPrefix(key, datasources.CustomHeaderName) { header := fmt.Sprint(value) - if http.CanonicalHeaderKey(header) == http.CanonicalHeaderKey(cfg.AuthProxyHeaderName) { + if http.CanonicalHeaderKey(header) == http.CanonicalHeaderKey(cfg.AuthProxy.HeaderName) { datasourcesLogger.Error("Forbidden to add a data source header with a name equal to auth proxy header name", "headerName", key) return errors.New("validation error, invalid header name specified") } diff --git a/pkg/api/datasources_test.go b/pkg/api/datasources_test.go index 8ede32a849..61481bec70 100644 --- a/pkg/api/datasources_test.go +++ b/pkg/api/datasources_test.go @@ -147,10 +147,10 @@ func TestAddDataSource_InvalidJSONData(t *testing.T) { sc := setupScenarioContext(t, "/api/datasources") hs.Cfg = setting.NewCfg() - hs.Cfg.AuthProxyEnabled = true - hs.Cfg.AuthProxyHeaderName = "X-AUTH-PROXY-HEADER" + hs.Cfg.AuthProxy.Enabled = true + hs.Cfg.AuthProxy.HeaderName = "X-AUTH-PROXY-HEADER" jsonData := simplejson.New() - jsonData.Set("httpHeaderName1", hs.Cfg.AuthProxyHeaderName) + jsonData.Set("httpHeaderName1", hs.Cfg.AuthProxy.HeaderName) sc.m.Post(sc.url, routing.Wrap(func(c *contextmodel.ReqContext) response.Response { c.Req.Body = mockRequestBody(datasources.AddDataSourceCommand{ @@ -201,10 +201,10 @@ func TestUpdateDataSource_InvalidJSONData(t *testing.T) { } sc := setupScenarioContext(t, "/api/datasources/1234") - hs.Cfg.AuthProxyEnabled = true - hs.Cfg.AuthProxyHeaderName = "X-AUTH-PROXY-HEADER" + hs.Cfg.AuthProxy.Enabled = true + hs.Cfg.AuthProxy.HeaderName = "X-AUTH-PROXY-HEADER" jsonData := simplejson.New() - jsonData.Set("httpHeaderName1", hs.Cfg.AuthProxyHeaderName) + jsonData.Set("httpHeaderName1", hs.Cfg.AuthProxy.HeaderName) sc.m.Put(sc.url, routing.Wrap(func(c *contextmodel.ReqContext) response.Response { c.Req.Body = mockRequestBody(datasources.AddDataSourceCommand{ @@ -297,7 +297,7 @@ func TestUpdateDataSourceTeamHTTPHeaders_InvalidJSONData(t *testing.T) { }, } sc := setupScenarioContext(t, fmt.Sprintf("/api/datasources/%s", tenantID)) - hs.Cfg.AuthProxyEnabled = true + hs.Cfg.AuthProxy.Enabled = true jsonData := simplejson.New() jsonData.Set("teamHttpHeaders", tc.data) diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index 29d678c100..dc3d329692 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -171,7 +171,7 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro AppUrl: hs.Cfg.AppURL, AppSubUrl: hs.Cfg.AppSubURL, AllowOrgCreate: (hs.Cfg.AllowUserOrgCreate && c.IsSignedIn) || c.IsGrafanaAdmin, - AuthProxyEnabled: hs.Cfg.AuthProxyEnabled, + AuthProxyEnabled: hs.Cfg.AuthProxy.Enabled, LdapEnabled: hs.Cfg.LDAPAuthEnabled, JwtHeaderName: hs.Cfg.JWTAuth.HeaderName, JwtUrlLogin: hs.Cfg.JWTAuth.URLLogin, @@ -322,7 +322,7 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro oauthProviders := hs.SocialService.GetOAuthInfoProviders() frontendSettings.Auth = dtos.FrontendSettingsAuthDTO{ - AuthProxyEnableLoginToken: hs.Cfg.AuthProxyEnableLoginToken, + AuthProxyEnableLoginToken: hs.Cfg.AuthProxy.EnableLoginToken, OAuthSkipOrgRoleUpdateSync: hs.Cfg.OAuthSkipOrgRoleUpdateSync, SAMLSkipOrgRoleSync: hs.Cfg.SAMLSkipOrgRoleSync, LDAPSkipOrgRoleSync: hs.Cfg.LDAPSkipOrgRoleSync, diff --git a/pkg/api/login.go b/pkg/api/login.go index 5443e95d56..355ce2bc60 100644 --- a/pkg/api/login.go +++ b/pkg/api/login.go @@ -125,8 +125,8 @@ func (hs *HTTPServer) LoginView(c *contextmodel.ReqContext) { if c.IsSignedIn { // Assign login token to auth proxy users if enable_login_token = true - if hs.Cfg.AuthProxyEnabled && - hs.Cfg.AuthProxyEnableLoginToken && + if hs.Cfg.AuthProxy.Enabled && + hs.Cfg.AuthProxy.EnableLoginToken && c.SignedInUser.AuthenticatedBy == loginservice.AuthProxyAuthModule { user := &user.User{ID: c.SignedInUser.UserID, Email: c.SignedInUser.Email, Login: c.SignedInUser.Login} err := hs.loginUserWithUser(user, c) diff --git a/pkg/api/login_test.go b/pkg/api/login_test.go index 35048e3ced..2b0ef5df7c 100644 --- a/pkg/api/login_test.go +++ b/pkg/api/login_test.go @@ -600,8 +600,8 @@ func TestAuthProxyLoginWithEnableLoginTokenAndEnabledOauthAutoLogin(t *testing.T return response.Empty(http.StatusOK) }) - sc.cfg.AuthProxyEnabled = true - sc.cfg.AuthProxyEnableLoginToken = true + sc.cfg.AuthProxy.Enabled = true + sc.cfg.AuthProxy.EnableLoginToken = true sc.m.Get(sc.url, sc.defaultHandler) sc.fakeReqNoAssertions("GET", sc.url).exec() @@ -640,8 +640,8 @@ func setupAuthProxyLoginTest(t *testing.T, enableLoginToken bool) *scenarioConte return response.Empty(http.StatusOK) }) - sc.cfg.AuthProxyEnabled = true - sc.cfg.AuthProxyEnableLoginToken = enableLoginToken + sc.cfg.AuthProxy.Enabled = true + sc.cfg.AuthProxy.EnableLoginToken = enableLoginToken sc.m.Get(sc.url, sc.defaultHandler) sc.fakeReqNoAssertions("GET", sc.url).exec() diff --git a/pkg/api/user.go b/pkg/api/user.go index a00b378e6f..29f3217523 100644 --- a/pkg/api/user.go +++ b/pkg/api/user.go @@ -147,11 +147,11 @@ func (hs *HTTPServer) UpdateSignedInUser(c *contextmodel.ReqContext) response.Re return errResponse } - if hs.Cfg.AuthProxyEnabled { - if hs.Cfg.AuthProxyHeaderProperty == "email" && cmd.Email != c.SignedInUser.GetEmail() { + if hs.Cfg.AuthProxy.Enabled { + if hs.Cfg.AuthProxy.HeaderProperty == "email" && cmd.Email != c.SignedInUser.GetEmail() { return response.Error(http.StatusBadRequest, "Not allowed to change email when auth proxy is using email property", nil) } - if hs.Cfg.AuthProxyHeaderProperty == "username" && cmd.Login != c.SignedInUser.GetLogin() { + if hs.Cfg.AuthProxy.HeaderProperty == "username" && cmd.Login != c.SignedInUser.GetLogin() { return response.Error(http.StatusBadRequest, "Not allowed to change username when auth proxy is using username property", nil) } } diff --git a/pkg/infra/usagestats/service/usage_stats_test.go b/pkg/infra/usagestats/service/usage_stats_test.go index 352345aca8..f2e826b47b 100644 --- a/pkg/infra/usagestats/service/usage_stats_test.go +++ b/pkg/infra/usagestats/service/usage_stats_test.go @@ -85,7 +85,7 @@ func TestMetrics(t *testing.T) { AnonymousEnabled: true, BasicAuthEnabled: true, LDAPAuthEnabled: true, - AuthProxyEnabled: true, + AuthProxy: setting.AuthProxySettings{Enabled: true}, Packaging: "deb", ReportingDistributor: "hosted-grafana", } diff --git a/pkg/infra/usagestats/statscollector/service_test.go b/pkg/infra/usagestats/statscollector/service_test.go index 7ffc509e0a..9261b490cf 100644 --- a/pkg/infra/usagestats/statscollector/service_test.go +++ b/pkg/infra/usagestats/statscollector/service_test.go @@ -145,7 +145,7 @@ func TestCollectingUsageStats(t *testing.T) { AnonymousEnabled: true, BasicAuthEnabled: true, LDAPAuthEnabled: true, - AuthProxyEnabled: true, + AuthProxy: setting.AuthProxySettings{Enabled: true}, Packaging: "deb", ReportingDistributor: "hosted-grafana", RemoteCacheOptions: &setting.RemoteCacheOptions{ diff --git a/pkg/services/authn/authnimpl/service.go b/pkg/services/authn/authnimpl/service.go index 881a23d060..08fd7e3005 100644 --- a/pkg/services/authn/authnimpl/service.go +++ b/pkg/services/authn/authnimpl/service.go @@ -122,7 +122,7 @@ func ProvideService( } } - if s.cfg.AuthProxyEnabled && len(proxyClients) > 0 { + if s.cfg.AuthProxy.Enabled && len(proxyClients) > 0 { proxy, err := clients.ProvideProxy(cfg, cache, proxyClients...) if err != nil { s.log.Error("Failed to configure auth proxy", "err", err) diff --git a/pkg/services/authn/authnimpl/usage_stats.go b/pkg/services/authn/authnimpl/usage_stats.go index b6ea1a95c6..52f224c732 100644 --- a/pkg/services/authn/authnimpl/usage_stats.go +++ b/pkg/services/authn/authnimpl/usage_stats.go @@ -13,7 +13,7 @@ func (s *Service) getUsageStats(ctx context.Context) (map[string]any, error) { authTypes := map[string]bool{} authTypes["basic_auth"] = s.cfg.BasicAuthEnabled authTypes["ldap"] = s.cfg.LDAPAuthEnabled - authTypes["auth_proxy"] = s.cfg.AuthProxyEnabled + authTypes["auth_proxy"] = s.cfg.AuthProxy.Enabled authTypes["anonymous"] = s.cfg.AnonymousEnabled authTypes["jwt"] = s.cfg.JWTAuth.Enabled authTypes["grafana_password"] = !s.cfg.DisableLogin diff --git a/pkg/services/authn/authnimpl/usage_stats_test.go b/pkg/services/authn/authnimpl/usage_stats_test.go index cded65c117..04d9776ce3 100644 --- a/pkg/services/authn/authnimpl/usage_stats_test.go +++ b/pkg/services/authn/authnimpl/usage_stats_test.go @@ -20,7 +20,7 @@ func TestService_getUsageStats(t *testing.T) { svc.cfg.DisableLoginForm = false svc.cfg.DisableLogin = false svc.cfg.BasicAuthEnabled = true - svc.cfg.AuthProxyEnabled = true + svc.cfg.AuthProxy.Enabled = true svc.cfg.JWTAuth.Enabled = true svc.cfg.LDAPAuthEnabled = true svc.cfg.EditorsCanAdmin = true diff --git a/pkg/services/authn/clients/grafana.go b/pkg/services/authn/clients/grafana.go index f84a88d2ec..10e3300860 100644 --- a/pkg/services/authn/clients/grafana.go +++ b/pkg/services/authn/clients/grafana.go @@ -40,11 +40,11 @@ func (c *Grafana) AuthenticateProxy(ctx context.Context, r *authn.Request, usern FetchSyncedUser: true, SyncOrgRoles: true, SyncPermissions: true, - AllowSignUp: c.cfg.AuthProxyAutoSignUp, + AllowSignUp: c.cfg.AuthProxy.AutoSignUp, }, } - switch c.cfg.AuthProxyHeaderProperty { + switch c.cfg.AuthProxy.HeaderProperty { case "username": identity.Login = username addr, err := mail.ParseAddress(username) @@ -55,7 +55,7 @@ func (c *Grafana) AuthenticateProxy(ctx context.Context, r *authn.Request, usern identity.Login = username identity.Email = username default: - return nil, errInvalidProxyHeader.Errorf("invalid auth proxy header property, expected username or email but got: %s", c.cfg.AuthProxyHeaderProperty) + return nil, errInvalidProxyHeader.Errorf("invalid auth proxy header property, expected username or email but got: %s", c.cfg.AuthProxy.HeaderProperty) } if v, ok := additional[proxyFieldName]; ok { diff --git a/pkg/services/authn/clients/grafana_test.go b/pkg/services/authn/clients/grafana_test.go index 02d31b9557..f77d233744 100644 --- a/pkg/services/authn/clients/grafana_test.go +++ b/pkg/services/authn/clients/grafana_test.go @@ -94,8 +94,8 @@ func TestGrafana_AuthenticateProxy(t *testing.T) { for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { cfg := setting.NewCfg() - cfg.AuthProxyAutoSignUp = true - cfg.AuthProxyHeaderProperty = tt.proxyProperty + cfg.AuthProxy.AutoSignUp = true + cfg.AuthProxy.HeaderProperty = tt.proxyProperty c := ProvideGrafana(cfg, usertest.NewUserServiceFake()) identity, err := c.AuthenticateProxy(context.Background(), tt.req, tt.username, tt.additional) diff --git a/pkg/services/authn/clients/proxy.go b/pkg/services/authn/clients/proxy.go index fdfe1960e9..cd6a890761 100644 --- a/pkg/services/authn/clients/proxy.go +++ b/pkg/services/authn/clients/proxy.go @@ -3,6 +3,7 @@ package clients import ( "context" "encoding/hex" + "errors" "fmt" "hash/fnv" "net" @@ -12,6 +13,7 @@ import ( "time" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/remotecache" authidentity "github.com/grafana/grafana/pkg/services/auth/identity" "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/login" @@ -43,7 +45,7 @@ var ( ) func ProvideProxy(cfg *setting.Cfg, cache proxyCache, clients ...authn.ProxyClient) (*Proxy, error) { - list, err := parseAcceptList(cfg.AuthProxyWhitelist) + list, err := parseAcceptList(cfg.AuthProxy.Whitelist) if err != nil { return nil, err } @@ -73,34 +75,22 @@ func (c *Proxy) Authenticate(ctx context.Context, r *authn.Request) (*authn.Iden return nil, errNotAcceptedIP.Errorf("request ip is not in the configured accept list") } - username := getProxyHeader(r, c.cfg.AuthProxyHeaderName, c.cfg.AuthProxyHeadersEncoded) + username := getProxyHeader(r, c.cfg.AuthProxy.HeaderName, c.cfg.AuthProxy.HeadersEncoded) if len(username) == 0 { return nil, errEmptyProxyHeader.Errorf("no username provided in auth proxy header") } additional := getAdditionalProxyHeaders(r, c.cfg) - cacheKey, ok := getProxyCacheKey(username, additional) - 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 { - uid, err := strconv.ParseInt(string(entry), 10, 64) - if err != nil { - c.log.FromContext(ctx).Warn("Failed to parse user id from cache", "error", err, "userId", string(entry)) - } else { - return &authn.Identity{ - ID: authn.NamespacedID(authn.NamespaceUser, uid), - OrgID: r.OrgID, - // FIXME: This does not match the actual auth module used, but should not have any impact - // Maybe caching the auth module used with the user ID would be a good idea - AuthenticatedBy: login.AuthProxyAuthModule, - ClientParams: authn.ClientParams{ - FetchSyncedUser: true, - SyncPermissions: true, - }, - }, nil - } + + if c.cfg.AuthProxy.SyncTTL != 0 && ok { + identity, errCache := c.retrieveIDFromCache(ctx, cacheKey, r) + if errCache == nil { + return identity, nil + } + + if !errors.Is(errCache, remotecache.ErrCacheItemNotFound) { + c.log.FromContext(ctx).Warn("Failed to fetch auth proxy info from cache", "error", errCache) } } @@ -117,8 +107,34 @@ func (c *Proxy) Authenticate(ctx context.Context, r *authn.Request) (*authn.Iden return nil, clientErr } +// 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 +func (c *Proxy) retrieveIDFromCache(ctx context.Context, cacheKey string, r *authn.Request) (*authn.Identity, error) { + entry, err := c.cache.Get(ctx, cacheKey) + if err != nil { + return nil, err + } + + uid, err := strconv.ParseInt(string(entry), 10, 64) + if err != nil { + return nil, fmt.Errorf("failed to parse user id from cache: %w - entry: %s", err, string(entry)) + } + + return &authn.Identity{ + ID: authn.NamespacedID(authn.NamespaceUser, uid), + OrgID: r.OrgID, + // FIXME: This does not match the actual auth module used, but should not have any impact + // Maybe caching the auth module used with the user ID would be a good idea + AuthenticatedBy: login.AuthProxyAuthModule, + ClientParams: authn.ClientParams{ + FetchSyncedUser: true, + SyncPermissions: true, + }, + }, nil +} + func (c *Proxy) Test(ctx context.Context, r *authn.Request) bool { - return len(getProxyHeader(r, c.cfg.AuthProxyHeaderName, c.cfg.AuthProxyHeadersEncoded)) != 0 + return len(getProxyHeader(r, c.cfg.AuthProxy.HeaderName, c.cfg.AuthProxy.HeadersEncoded)) != 0 } func (c *Proxy) Priority() uint { @@ -147,7 +163,7 @@ func (c *Proxy) Hook(ctx context.Context, identity *authn.Identity, r *authn.Req // 3. Name = x; Role = Admin # cache hit with key Name=x;Role=Admin, no update, the user stays with Role=Editor // To avoid such a problem we also cache the key used using `prefix:[username]`. // Then whenever we get a cache miss due to changes in any header we use it to invalidate the previous item. - username := getProxyHeader(r, c.cfg.AuthProxyHeaderName, c.cfg.AuthProxyHeadersEncoded) + username := getProxyHeader(r, c.cfg.AuthProxy.HeaderName, c.cfg.AuthProxy.HeadersEncoded) userKey := fmt.Sprintf("%s:%s", proxyCachePrefix, username) // invalidate previously cached user id @@ -159,7 +175,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 := []byte(strconv.FormatInt(id, 10)) - duration := time.Duration(c.cfg.AuthProxySyncTTL) * time.Minute + duration := time.Duration(c.cfg.AuthProxy.SyncTTL) * time.Minute if err := c.cache.Set(ctx, identity.ClientParams.CacheAuthProxyKey, bytes, duration); err != nil { c.log.Warn("Failed to cache proxy user", "error", err, "userId", id) } @@ -232,7 +248,7 @@ func getProxyHeader(r *authn.Request, headerName string, encoded bool) string { func getAdditionalProxyHeaders(r *authn.Request, cfg *setting.Cfg) map[string]string { additional := make(map[string]string, len(proxyFields)) for _, k := range proxyFields { - if v := getProxyHeader(r, cfg.AuthProxyHeaders[k], cfg.AuthProxyHeadersEncoded); v != "" { + if v := getProxyHeader(r, cfg.AuthProxy.Headers[k], cfg.AuthProxy.HeadersEncoded); v != "" { additional[k] = v } } diff --git a/pkg/services/authn/clients/proxy_test.go b/pkg/services/authn/clients/proxy_test.go index 04b0d6c492..1f98012247 100644 --- a/pkg/services/authn/clients/proxy_test.go +++ b/pkg/services/authn/clients/proxy_test.go @@ -100,9 +100,9 @@ func TestProxy_Authenticate(t *testing.T) { for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { cfg := setting.NewCfg() - cfg.AuthProxyHeaderName = "X-Username" - cfg.AuthProxyHeaders = tt.proxyHeaders - cfg.AuthProxyWhitelist = tt.ips + cfg.AuthProxy.HeaderName = "X-Username" + cfg.AuthProxy.Headers = tt.proxyHeaders + cfg.AuthProxy.Whitelist = tt.ips calledUsername := "" var calledAdditional map[string]string @@ -166,7 +166,7 @@ func TestProxy_Test(t *testing.T) { for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { cfg := setting.NewCfg() - cfg.AuthProxyHeaderName = "Proxy-Header" + cfg.AuthProxy.HeaderName = "Proxy-Header" c, _ := ProvideProxy(cfg, nil, nil, nil) assert.Equal(t, tt.expectedOK, c.Test(context.Background(), tt.req)) @@ -197,8 +197,8 @@ func (f fakeCache) Delete(ctx context.Context, key string) error { func TestProxy_Hook(t *testing.T) { cfg := setting.NewCfg() - cfg.AuthProxyHeaderName = "X-Username" - cfg.AuthProxyHeaders = map[string]string{ + cfg.AuthProxy.HeaderName = "X-Username" + cfg.AuthProxy.Headers = map[string]string{ proxyFieldRole: "X-Role", } cache := &fakeCache{data: make(map[string][]byte)} diff --git a/pkg/services/contexthandler/contexthandler.go b/pkg/services/contexthandler/contexthandler.go index b405fed5ad..02ff630fc6 100644 --- a/pkg/services/contexthandler/contexthandler.go +++ b/pkg/services/contexthandler/contexthandler.go @@ -195,9 +195,9 @@ func WithAuthHTTPHeaders(ctx context.Context, cfg *setting.Cfg) context.Context } // if auth proxy is enabled add the main proxy header and all configured headers - if cfg.AuthProxyEnabled { - list.Items = append(list.Items, cfg.AuthProxyHeaderName) - for _, header := range cfg.AuthProxyHeaders { + if cfg.AuthProxy.Enabled { + list.Items = append(list.Items, cfg.AuthProxy.HeaderName) + for _, header := range cfg.AuthProxy.Headers { if header != "" { list.Items = append(list.Items, header) } diff --git a/pkg/services/contexthandler/contexthandler_test.go b/pkg/services/contexthandler/contexthandler_test.go index 6662115355..f7c46f7208 100644 --- a/pkg/services/contexthandler/contexthandler_test.go +++ b/pkg/services/contexthandler/contexthandler_test.go @@ -114,9 +114,9 @@ func TestContextHandler(t *testing.T) { cfg := setting.NewCfg() cfg.JWTAuth.Enabled = true cfg.JWTAuth.HeaderName = "jwt-header" - cfg.AuthProxyEnabled = true - cfg.AuthProxyHeaderName = "proxy-header" - cfg.AuthProxyHeaders = map[string]string{ + cfg.AuthProxy.Enabled = true + cfg.AuthProxy.HeaderName = "proxy-header" + cfg.AuthProxy.Headers = map[string]string{ "name": "proxy-header-name", } diff --git a/pkg/services/datasources/service/datasource.go b/pkg/services/datasources/service/datasource.go index a73e71b887..43bf6f0a6f 100644 --- a/pkg/services/datasources/service/datasource.go +++ b/pkg/services/datasources/service/datasource.go @@ -675,7 +675,7 @@ func (s *Service) getCustomHeaders(jsonData *simplejson.Json, decryptedValues ma // skip a header with name that corresponds to auth proxy header's name // to make sure that data source proxy isn't used to circumvent auth proxy. // For more context take a look at CVE-2022-35957 - if s.cfg.AuthProxyEnabled && http.CanonicalHeaderKey(key) == http.CanonicalHeaderKey(s.cfg.AuthProxyHeaderName) { + if s.cfg.AuthProxy.Enabled && http.CanonicalHeaderKey(key) == http.CanonicalHeaderKey(s.cfg.AuthProxy.HeaderName) { continue } diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index d9f8f487ce..efa62cfae5 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -256,15 +256,7 @@ type Cfg struct { Azure *azsettings.AzureSettings // Auth proxy settings - AuthProxyEnabled bool - AuthProxyHeaderName string - AuthProxyHeaderProperty string - AuthProxyAutoSignUp bool - AuthProxyEnableLoginToken bool - AuthProxyWhitelist string - AuthProxyHeaders map[string]string - AuthProxyHeadersEncoded bool - AuthProxySyncTTL int + AuthProxy AuthProxySettings // OAuth OAuthAutoLogin bool @@ -1197,6 +1189,7 @@ func (cfg *Cfg) parseINIFile(iniFile *ini.File) error { cfg.handleAWSConfig() cfg.readAzureSettings() cfg.readAuthJWTSettings() + cfg.readAuthProxySettings() cfg.readSessionConfig() if err := cfg.readSmtpSettings(); err != nil { return err @@ -1617,31 +1610,6 @@ func readAuthSettings(iniFile *ini.File, cfg *Cfg) (err error) { cfg.ExtendedJWTExpectAudience = authExtendedJWT.Key("expect_audience").MustString("") cfg.ExtendedJWTExpectIssuer = authExtendedJWT.Key("expect_issuer").MustString("") - // Auth Proxy - authProxy := iniFile.Section("auth.proxy") - cfg.AuthProxyEnabled = authProxy.Key("enabled").MustBool(false) - - cfg.AuthProxyHeaderName = valueAsString(authProxy, "header_name", "") - cfg.AuthProxyHeaderProperty = valueAsString(authProxy, "header_property", "") - cfg.AuthProxyAutoSignUp = authProxy.Key("auto_sign_up").MustBool(true) - cfg.AuthProxyEnableLoginToken = authProxy.Key("enable_login_token").MustBool(false) - - cfg.AuthProxySyncTTL = authProxy.Key("sync_ttl").MustInt() - - cfg.AuthProxyWhitelist = valueAsString(authProxy, "whitelist", "") - - cfg.AuthProxyHeaders = make(map[string]string) - headers := valueAsString(authProxy, "headers", "") - - for _, propertyAndHeader := range util.SplitString(headers) { - split := strings.SplitN(propertyAndHeader, ":", 2) - if len(split) == 2 { - cfg.AuthProxyHeaders[split[0]] = split[1] - } - } - - cfg.AuthProxyHeadersEncoded = authProxy.Key("headers_encoded").MustBool(false) - // SSO Settings ssoSettings := iniFile.Section("sso_settings") cfg.SSOSettingsReloadInterval = ssoSettings.Key("reload_interval").MustDuration(1 * time.Minute) diff --git a/pkg/setting/setting_auth_proxy.go b/pkg/setting/setting_auth_proxy.go new file mode 100644 index 0000000000..36d300e78e --- /dev/null +++ b/pkg/setting/setting_auth_proxy.go @@ -0,0 +1,45 @@ +package setting + +import ( + "strings" + + "github.com/grafana/grafana/pkg/util" +) + +type AuthProxySettings struct { + // Auth Proxy + Enabled bool + HeaderName string + HeaderProperty string + AutoSignUp bool + EnableLoginToken bool + Whitelist string + Headers map[string]string + HeadersEncoded bool + SyncTTL int +} + +func (cfg *Cfg) readAuthProxySettings() { + authProxySettings := AuthProxySettings{} + authProxy := cfg.Raw.Section("auth.proxy") + authProxySettings.Enabled = authProxy.Key("enabled").MustBool(false) + authProxySettings.HeaderName = valueAsString(authProxy, "header_name", "") + authProxySettings.HeaderProperty = valueAsString(authProxy, "header_property", "") + authProxySettings.AutoSignUp = authProxy.Key("auto_sign_up").MustBool(true) + authProxySettings.EnableLoginToken = authProxy.Key("enable_login_token").MustBool(false) + authProxySettings.SyncTTL = authProxy.Key("sync_ttl").MustInt(15) + authProxySettings.Whitelist = valueAsString(authProxy, "whitelist", "") + authProxySettings.Headers = make(map[string]string) + headers := valueAsString(authProxy, "headers", "") + + for _, propertyAndHeader := range util.SplitString(headers) { + split := strings.SplitN(propertyAndHeader, ":", 2) + if len(split) == 2 { + authProxySettings.Headers[split[0]] = split[1] + } + } + + authProxySettings.HeadersEncoded = authProxy.Key("headers_encoded").MustBool(false) + + cfg.AuthProxy = authProxySettings +} diff --git a/pkg/setting/setting_test.go b/pkg/setting/setting_test.go index 184e5023b9..00b302981b 100644 --- a/pkg/setting/setting_test.go +++ b/pkg/setting/setting_test.go @@ -13,11 +13,12 @@ import ( "testing" "time" - "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/util/osutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gopkg.in/ini.v1" + + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/util/osutil" ) const ( @@ -274,7 +275,7 @@ func TestLoadingSettings(t *testing.T) { }) require.Nil(t, err) - require.Equal(t, 2, cfg.AuthProxySyncTTL) + require.Equal(t, 2, cfg.AuthProxy.SyncTTL) }) t.Run("Test reading string values from .ini file", func(t *testing.T) { From 11d341d2bbed45c8fcb8512a3b77aad4839641b3 Mon Sep 17 00:00:00 2001 From: Konrad Lalik Date: Fri, 1 Mar 2024 11:34:58 +0100 Subject: [PATCH 0329/1406] Alerting: Add escape and quote support to the matcher name (#83601) Add escape and quote support to the matcher name --- .../alerting/unified/utils/amroutes.test.ts | 55 +++++++++++++++++-- .../alerting/unified/utils/amroutes.ts | 4 +- .../alerting/unified/utils/matchers.ts | 3 +- .../utils/notification-policies.test.ts | 37 +++++++++++++ .../unified/utils/notification-policies.ts | 2 +- 5 files changed, 92 insertions(+), 9 deletions(-) diff --git a/public/app/features/alerting/unified/utils/amroutes.test.ts b/public/app/features/alerting/unified/utils/amroutes.test.ts index dbb71f012c..343e4587c4 100644 --- a/public/app/features/alerting/unified/utils/amroutes.test.ts +++ b/public/app/features/alerting/unified/utils/amroutes.test.ts @@ -72,10 +72,36 @@ describe('formAmRouteToAmRoute', () => { // Assert expect(amRoute.matchers).toStrictEqual([ - 'foo="bar"', - 'foo="bar\\"baz"', - 'foo="bar\\\\baz"', - 'foo="\\\\bar\\\\baz\\"\\\\"', + '"foo"="bar"', + '"foo"="bar\\"baz"', + '"foo"="bar\\\\baz"', + '"foo"="\\\\bar\\\\baz\\"\\\\"', + ]); + }); + + it('should quote and escape matcher names', () => { + // Arrange + const route: FormAmRoute = buildFormAmRoute({ + id: '1', + object_matchers: [ + { name: 'foo', operator: MatcherOperator.equal, value: 'bar' }, + { name: 'foo with spaces', operator: MatcherOperator.equal, value: 'bar' }, + { name: 'foo\\slash', operator: MatcherOperator.equal, value: 'bar' }, + { name: 'foo"quote', operator: MatcherOperator.equal, value: 'bar' }, + { name: 'fo\\o', operator: MatcherOperator.equal, value: 'ba\\r' }, + ], + }); + + // Act + const amRoute = formAmRouteToAmRoute('mimir-am', route, { id: 'root' }); + + // Assert + expect(amRoute.matchers).toStrictEqual([ + '"foo"="bar"', + '"foo with spaces"="bar"', + '"foo\\\\slash"="bar"', + '"foo\\"quote"="bar"', + '"fo\\\\o"="ba\\\\r"', ]); }); @@ -90,7 +116,7 @@ describe('formAmRouteToAmRoute', () => { const amRoute = formAmRouteToAmRoute('mimir-am', route, { id: 'root' }); // Assert - expect(amRoute.matchers).toStrictEqual(['foo=""']); + expect(amRoute.matchers).toStrictEqual(['"foo"=""']); }); it('should allow matchers with empty values for Grafana AM', () => { @@ -173,4 +199,23 @@ describe('amRouteToFormAmRoute', () => { { name: 'foo', operator: MatcherOperator.equal, value: '\\bar\\baz"\\' }, ]); }); + + it('should unquote and unescape matcher names', () => { + // Arrange + const amRoute = buildAmRoute({ + matchers: ['"foo"=bar', '"foo with spaces"=bar', '"foo\\\\slash"=bar', '"foo"quote"=bar', '"fo\\\\o"="ba\\\\r"'], + }); + + // Act + const formRoute = amRouteToFormAmRoute(amRoute); + + // Assert + expect(formRoute.object_matchers).toStrictEqual([ + { name: 'foo', operator: MatcherOperator.equal, value: 'bar' }, + { name: 'foo with spaces', operator: MatcherOperator.equal, value: 'bar' }, + { name: 'foo\\slash', operator: MatcherOperator.equal, value: 'bar' }, + { name: 'foo"quote', operator: MatcherOperator.equal, value: 'bar' }, + { name: 'fo\\o', operator: MatcherOperator.equal, value: 'ba\\r' }, + ]); + }); }); diff --git a/public/app/features/alerting/unified/utils/amroutes.ts b/public/app/features/alerting/unified/utils/amroutes.ts index dbfbd58ad3..0802276d9f 100644 --- a/public/app/features/alerting/unified/utils/amroutes.ts +++ b/public/app/features/alerting/unified/utils/amroutes.ts @@ -98,7 +98,7 @@ export const amRouteToFormAmRoute = (route: RouteWithID | Route | undefined): Fo route.matchers ?.map((matcher) => matcherToMatcherField(parseMatcher(matcher))) .map(({ name, operator, value }) => ({ - name, + name: unquoteWithUnescape(name), operator, value: unquoteWithUnescape(value), })) ?? []; @@ -186,7 +186,7 @@ export const formAmRouteToAmRoute = ( // does not exist in upstream AlertManager if (alertManagerSourceName !== GRAFANA_RULES_SOURCE_NAME) { amRoute.matchers = formAmRoute.object_matchers?.map( - ({ name, operator, value }) => `${name}${operator}${quoteWithEscape(value)}` + ({ name, operator, value }) => `${quoteWithEscape(name)}${operator}${quoteWithEscape(value)}` ); amRoute.object_matchers = undefined; } else { diff --git a/public/app/features/alerting/unified/utils/matchers.ts b/public/app/features/alerting/unified/utils/matchers.ts index 7b8c7ef4a0..c24c15d8f7 100644 --- a/public/app/features/alerting/unified/utils/matchers.ts +++ b/public/app/features/alerting/unified/utils/matchers.ts @@ -137,9 +137,10 @@ export const matcherFormatter = { return `${name} ${operator} ${formattedValue}`; }, unquote: ([name, operator, value]: ObjectMatcher): string => { + const unquotedName = unquoteWithUnescape(name); // Unquoted value can be an empty string which we want to display as "" const unquotedValue = unquoteWithUnescape(value) || '""'; - return `${name} ${operator} ${unquotedValue}`; + return `${unquotedName} ${operator} ${unquotedValue}`; }, } as const; diff --git a/public/app/features/alerting/unified/utils/notification-policies.test.ts b/public/app/features/alerting/unified/utils/notification-policies.test.ts index 785a48f895..b6f12be286 100644 --- a/public/app/features/alerting/unified/utils/notification-policies.test.ts +++ b/public/app/features/alerting/unified/utils/notification-policies.test.ts @@ -7,6 +7,7 @@ import { getInheritedProperties, matchLabels, normalizeRoute, + unquoteRouteMatchers, } from './notification-policies'; import 'core-js/stable/structured-clone'; @@ -476,3 +477,39 @@ describe('matchLabels', () => { expect(result.labelsMatch).toMatchSnapshot(); }); }); + +describe('unquoteRouteMatchers', () => { + it('should unquote and unescape matchers values', () => { + const route: RouteWithID = { + id: '1', + object_matchers: [ + ['foo', MatcherOperator.equal, 'bar'], + ['foo', MatcherOperator.equal, '"bar"'], + ['foo', MatcherOperator.equal, '"b\\\\ar b\\"az"'], + ], + }; + + const unwrapped = unquoteRouteMatchers(route); + + expect(unwrapped.object_matchers).toHaveLength(3); + expect(unwrapped.object_matchers).toContainEqual(['foo', MatcherOperator.equal, 'bar']); + expect(unwrapped.object_matchers).toContainEqual(['foo', MatcherOperator.equal, 'bar']); + expect(unwrapped.object_matchers).toContainEqual(['foo', MatcherOperator.equal, 'b\\ar b"az']); + }); + + it('should unquote and unescape matcher names', () => { + const route: RouteWithID = { + id: '1', + object_matchers: [ + ['"f\\"oo with quote"', MatcherOperator.equal, 'bar'], + ['"f\\\\oo with slash"', MatcherOperator.equal, 'bar'], + ], + }; + + const unwrapped = unquoteRouteMatchers(route); + + expect(unwrapped.object_matchers).toHaveLength(2); + expect(unwrapped.object_matchers).toContainEqual(['f"oo with quote', MatcherOperator.equal, 'bar']); + expect(unwrapped.object_matchers).toContainEqual(['f\\oo with slash', MatcherOperator.equal, 'bar']); + }); +}); diff --git a/public/app/features/alerting/unified/utils/notification-policies.ts b/public/app/features/alerting/unified/utils/notification-policies.ts index a0fed72bd6..8b41916107 100644 --- a/public/app/features/alerting/unified/utils/notification-policies.ts +++ b/public/app/features/alerting/unified/utils/notification-policies.ts @@ -127,7 +127,7 @@ export function normalizeRoute(rootRoute: RouteWithID): RouteWithID { export function unquoteRouteMatchers(route: RouteWithID): RouteWithID { function unquoteRoute(route: RouteWithID) { route.object_matchers = route.object_matchers?.map(([name, operator, value]) => { - return [name, operator, unquoteWithUnescape(value)]; + return [unquoteWithUnescape(name), operator, unquoteWithUnescape(value)]; }); route.routes?.forEach(unquoteRoute); } From 0aebb9ee3929f94157ebaaaa95776aa838b571db Mon Sep 17 00:00:00 2001 From: Jo Date: Fri, 1 Mar 2024 12:08:00 +0100 Subject: [PATCH 0330/1406] Misc: Remove unused params and impossible logic (#83756) * remove unused params and impossible logic * remove unused param --- pkg/api/accesscontrol.go | 2 +- pkg/api/datasources.go | 4 ++-- pkg/api/http_server.go | 3 --- pkg/api/login.go | 2 +- pkg/api/user.go | 2 +- pkg/services/accesscontrol/middleware.go | 6 +++--- 6 files changed, 8 insertions(+), 11 deletions(-) diff --git a/pkg/api/accesscontrol.go b/pkg/api/accesscontrol.go index e8084c4fd5..c2b4be1abe 100644 --- a/pkg/api/accesscontrol.go +++ b/pkg/api/accesscontrol.go @@ -654,7 +654,7 @@ func (hs *HTTPServer) declareFixedRoles() error { // Metadata helpers // getAccessControlMetadata returns the accesscontrol metadata associated with a given resource func (hs *HTTPServer) getAccessControlMetadata(c *contextmodel.ReqContext, - orgID int64, prefix string, resourceID string) ac.Metadata { + prefix string, resourceID string) ac.Metadata { ids := map[string]bool{resourceID: true} return hs.getMultiAccessControlMetadata(c, prefix, ids)[resourceID] } diff --git a/pkg/api/datasources.go b/pkg/api/datasources.go index f9182da0c7..67f2d0dad3 100644 --- a/pkg/api/datasources.go +++ b/pkg/api/datasources.go @@ -136,7 +136,7 @@ func (hs *HTTPServer) GetDataSourceById(c *contextmodel.ReqContext) response.Res dto := hs.convertModelToDtos(c.Req.Context(), dataSource) // Add accesscontrol metadata - dto.AccessControl = hs.getAccessControlMetadata(c, c.SignedInUser.GetOrgID(), datasources.ScopePrefix, dto.UID) + dto.AccessControl = hs.getAccessControlMetadata(c, datasources.ScopePrefix, dto.UID) return response.JSON(http.StatusOK, &dto) } @@ -222,7 +222,7 @@ func (hs *HTTPServer) GetDataSourceByUID(c *contextmodel.ReqContext) response.Re dto := hs.convertModelToDtos(c.Req.Context(), ds) // Add accesscontrol metadata - dto.AccessControl = hs.getAccessControlMetadata(c, c.SignedInUser.GetOrgID(), datasources.ScopePrefix, dto.UID) + dto.AccessControl = hs.getAccessControlMetadata(c, datasources.ScopePrefix, dto.UID) return response.JSON(http.StatusOK, &dto) } diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index a52555fb36..05c422fb19 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -587,9 +587,6 @@ func (hs *HTTPServer) configureHttps() error { } tlsCiphers := hs.getDefaultCiphers(minTlsVersion, string(setting.HTTPSScheme)) - if err != nil { - return err - } hs.log.Info("HTTP Server TLS settings", "Min TLS Version", hs.Cfg.MinTLSVersion, "configured ciphers", util.TlsCipherIdsToString(tlsCiphers)) diff --git a/pkg/api/login.go b/pkg/api/login.go index 355ce2bc60..15b84597aa 100644 --- a/pkg/api/login.go +++ b/pkg/api/login.go @@ -287,7 +287,7 @@ func (hs *HTTPServer) trySetEncryptedCookie(ctx *contextmodel.ReqContext, cookie return err } - cookies.WriteCookie(ctx.Resp, cookieName, hex.EncodeToString(encryptedError), 60, hs.CookieOptionsFromCfg) + cookies.WriteCookie(ctx.Resp, cookieName, hex.EncodeToString(encryptedError), maxAge, hs.CookieOptionsFromCfg) return nil } diff --git a/pkg/api/user.go b/pkg/api/user.go index 29f3217523..26b6c1007f 100644 --- a/pkg/api/user.go +++ b/pkg/api/user.go @@ -83,7 +83,7 @@ func (hs *HTTPServer) getUserUserProfile(c *contextmodel.ReqContext, userID int6 userProfile.IsGrafanaAdminExternallySynced = login.IsGrafanaAdminExternallySynced(hs.Cfg, oauthInfo, authInfo.AuthModule) } - userProfile.AccessControl = hs.getAccessControlMetadata(c, c.SignedInUser.GetOrgID(), "global.users:id:", strconv.FormatInt(userID, 10)) + userProfile.AccessControl = hs.getAccessControlMetadata(c, "global.users:id:", strconv.FormatInt(userID, 10)) userProfile.AvatarURL = dtos.GetGravatarUrl(hs.Cfg, userProfile.Email) return response.JSON(http.StatusOK, userProfile) diff --git a/pkg/services/accesscontrol/middleware.go b/pkg/services/accesscontrol/middleware.go index 5ef36a8a21..ad705a244c 100644 --- a/pkg/services/accesscontrol/middleware.go +++ b/pkg/services/accesscontrol/middleware.go @@ -37,7 +37,7 @@ func Middleware(ac AccessControl) func(Evaluator) web.Handler { } if !c.IsSignedIn && forceLogin { - unauthorized(c, nil) + unauthorized(c) return } } @@ -49,7 +49,7 @@ func Middleware(ac AccessControl) func(Evaluator) web.Handler { return } - unauthorized(c, c.LookupTokenErr) + unauthorized(c) return } @@ -113,7 +113,7 @@ func deny(c *contextmodel.ReqContext, evaluator Evaluator, err error) { }) } -func unauthorized(c *contextmodel.ReqContext, err error) { +func unauthorized(c *contextmodel.ReqContext) { if c.IsApiRequest() { c.WriteErrOrFallback(http.StatusUnauthorized, http.StatusText(http.StatusUnauthorized), c.LookupTokenErr) return From 142ac22023f83b6444574e0fb060c61d8283559c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Farkas?= Date: Fri, 1 Mar 2024 12:20:47 +0100 Subject: [PATCH 0331/1406] Revert "Postgres: Switch the datasource plugin from lib/pq to pgx" (#83760) Revert "Postgres: Switch the datasource plugin from lib/pq to pgx (#81353)" This reverts commit 8c18d06386c87f2786119ea9a6334e35e4181cbe. --- go.mod | 5 - go.sum | 7 - .../grafana-postgresql-datasource/locker.go | 85 ++++ .../locker_test.go | 63 +++ .../grafana-postgresql-datasource/postgres.go | 118 +++--- .../postgres_snapshot_test.go | 12 +- .../postgres_test.go | 175 ++++---- .../grafana-postgresql-datasource/proxy.go | 30 +- .../proxy_test.go | 12 +- .../table/timestamp_convert_real.golden.jsonc | 32 +- .../table/types_datetime.golden.jsonc | 16 +- .../grafana-postgresql-datasource/tls/tls.go | 147 ------- .../tls/tls_loader.go | 101 ----- .../tls/tls_test.go | 382 ------------------ .../tls/tls_test_helpers.go | 105 ----- .../tlsmanager.go | 249 ++++++++++++ .../tlsmanager_test.go | 332 +++++++++++++++ 17 files changed, 906 insertions(+), 965 deletions(-) create mode 100644 pkg/tsdb/grafana-postgresql-datasource/locker.go create mode 100644 pkg/tsdb/grafana-postgresql-datasource/locker_test.go delete mode 100644 pkg/tsdb/grafana-postgresql-datasource/tls/tls.go delete mode 100644 pkg/tsdb/grafana-postgresql-datasource/tls/tls_loader.go delete mode 100644 pkg/tsdb/grafana-postgresql-datasource/tls/tls_test.go delete mode 100644 pkg/tsdb/grafana-postgresql-datasource/tls/tls_test_helpers.go create mode 100644 pkg/tsdb/grafana-postgresql-datasource/tlsmanager.go create mode 100644 pkg/tsdb/grafana-postgresql-datasource/tlsmanager_test.go diff --git a/go.mod b/go.mod index 37e27eddb7..061044a1a8 100644 --- a/go.mod +++ b/go.mod @@ -471,14 +471,9 @@ require ( github.com/grafana/grafana/pkg/apiserver v0.0.0-20240226124929-648abdbd0ea4 // @grafana/grafana-app-platform-squad ) -require github.com/jackc/pgx/v5 v5.5.3 // @grafana/oss-big-tent - require ( github.com/bufbuild/protocompile v0.4.0 // indirect github.com/grafana/sqlds/v3 v3.2.0 // indirect - github.com/jackc/pgpassfile v1.0.0 // indirect - github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect - github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/jhump/protoreflect v1.15.1 // indirect github.com/klauspost/asmfmt v1.3.2 // indirect github.com/krasun/gosqlparser v1.0.5 // @grafana/grafana-app-platform-squad diff --git a/go.sum b/go.sum index 30e3b806f8..a6aa605d07 100644 --- a/go.sum +++ b/go.sum @@ -2370,7 +2370,6 @@ github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bY github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= -github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= @@ -2381,8 +2380,6 @@ github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwX github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgproto3/v2 v2.2.0/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= -github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= -github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= @@ -2394,14 +2391,10 @@ github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9 github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= github.com/jackc/pgx/v4 v4.15.0/go.mod h1:D/zyOyXiaM1TmVWnOM18p0xdDtdakRBa0RsVGI3U3bw= -github.com/jackc/pgx/v5 v5.5.3 h1:Ces6/M3wbDXYpM8JyyPD57ivTtJACFZJd885pdIaV2s= -github.com/jackc/pgx/v5 v5.5.3/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.2.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= -github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jarcoal/httpmock v1.3.0/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc= github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= diff --git a/pkg/tsdb/grafana-postgresql-datasource/locker.go b/pkg/tsdb/grafana-postgresql-datasource/locker.go new file mode 100644 index 0000000000..796c37c741 --- /dev/null +++ b/pkg/tsdb/grafana-postgresql-datasource/locker.go @@ -0,0 +1,85 @@ +package postgres + +import ( + "fmt" + "sync" +) + +// locker is a named reader/writer mutual exclusion lock. +// The lock for each particular key can be held by an arbitrary number of readers or a single writer. +type locker struct { + locks map[any]*sync.RWMutex + locksRW *sync.RWMutex +} + +func newLocker() *locker { + return &locker{ + locks: make(map[any]*sync.RWMutex), + locksRW: new(sync.RWMutex), + } +} + +// Lock locks named rw mutex with specified key for writing. +// If the lock with the same key is already locked for reading or writing, +// Lock blocks until the lock is available. +func (lkr *locker) Lock(key any) { + lk, ok := lkr.getLock(key) + if !ok { + lk = lkr.newLock(key) + } + lk.Lock() +} + +// Unlock unlocks named rw mutex with specified key for writing. It is a run-time error if rw is +// not locked for writing on entry to Unlock. +func (lkr *locker) Unlock(key any) { + lk, ok := lkr.getLock(key) + if !ok { + panic(fmt.Errorf("lock for key '%s' not initialized", key)) + } + lk.Unlock() +} + +// RLock locks named rw mutex with specified key for reading. +// +// It should not be used for recursive read locking for the same key; a blocked Lock +// call excludes new readers from acquiring the lock. See the +// documentation on the golang RWMutex type. +func (lkr *locker) RLock(key any) { + lk, ok := lkr.getLock(key) + if !ok { + lk = lkr.newLock(key) + } + lk.RLock() +} + +// RUnlock undoes a single RLock call for specified key; +// it does not affect other simultaneous readers of locker for specified key. +// It is a run-time error if locker for specified key is not locked for reading +func (lkr *locker) RUnlock(key any) { + lk, ok := lkr.getLock(key) + if !ok { + panic(fmt.Errorf("lock for key '%s' not initialized", key)) + } + lk.RUnlock() +} + +func (lkr *locker) newLock(key any) *sync.RWMutex { + lkr.locksRW.Lock() + defer lkr.locksRW.Unlock() + + if lk, ok := lkr.locks[key]; ok { + return lk + } + lk := new(sync.RWMutex) + lkr.locks[key] = lk + return lk +} + +func (lkr *locker) getLock(key any) (*sync.RWMutex, bool) { + lkr.locksRW.RLock() + defer lkr.locksRW.RUnlock() + + lock, ok := lkr.locks[key] + return lock, ok +} diff --git a/pkg/tsdb/grafana-postgresql-datasource/locker_test.go b/pkg/tsdb/grafana-postgresql-datasource/locker_test.go new file mode 100644 index 0000000000..b1dc64f035 --- /dev/null +++ b/pkg/tsdb/grafana-postgresql-datasource/locker_test.go @@ -0,0 +1,63 @@ +package postgres + +import ( + "sync" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestIntegrationLocker(t *testing.T) { + if testing.Short() { + t.Skip("Tests with Sleep") + } + const notUpdated = "not_updated" + const atThread1 = "at_thread_1" + const atThread2 = "at_thread_2" + t.Run("Should lock for same keys", func(t *testing.T) { + updated := notUpdated + locker := newLocker() + locker.Lock(1) + var wg sync.WaitGroup + wg.Add(1) + defer func() { + locker.Unlock(1) + wg.Wait() + }() + + go func() { + locker.RLock(1) + defer func() { + locker.RUnlock(1) + wg.Done() + }() + require.Equal(t, atThread1, updated, "Value should be updated in different thread") + updated = atThread2 + }() + time.Sleep(time.Millisecond * 10) + require.Equal(t, notUpdated, updated, "Value should not be updated in different thread") + updated = atThread1 + }) + + t.Run("Should not lock for different keys", func(t *testing.T) { + updated := notUpdated + locker := newLocker() + locker.Lock(1) + defer locker.Unlock(1) + var wg sync.WaitGroup + wg.Add(1) + go func() { + locker.RLock(2) + defer func() { + locker.RUnlock(2) + wg.Done() + }() + require.Equal(t, notUpdated, updated, "Value should not be updated in different thread") + updated = atThread2 + }() + wg.Wait() + require.Equal(t, atThread2, updated, "Value should be updated in different thread") + updated = atThread1 + }) +} diff --git a/pkg/tsdb/grafana-postgresql-datasource/postgres.go b/pkg/tsdb/grafana-postgresql-datasource/postgres.go index cfcdd0ede0..53e4630885 100644 --- a/pkg/tsdb/grafana-postgresql-datasource/postgres.go +++ b/pkg/tsdb/grafana-postgresql-datasource/postgres.go @@ -4,42 +4,38 @@ import ( "context" "database/sql" "encoding/json" - "errors" "fmt" - "os" "reflect" "strconv" "strings" "time" - "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/pgtype" - pgxstdlib "github.com/jackc/pgx/v5/stdlib" - "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/datasource" "github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt" "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana-plugin-sdk-go/data/sqlutil" + "github.com/lib/pq" "github.com/grafana/grafana-plugin-sdk-go/backend/log" "github.com/grafana/grafana/pkg/setting" - "github.com/grafana/grafana/pkg/tsdb/grafana-postgresql-datasource/tls" "github.com/grafana/grafana/pkg/tsdb/sqleng" ) func ProvideService(cfg *setting.Cfg) *Service { logger := backend.NewLoggerWith("logger", "tsdb.postgres") s := &Service{ - logger: logger, + tlsManager: newTLSManager(logger, cfg.DataPath), + logger: logger, } s.im = datasource.NewInstanceManager(s.newInstanceSettings()) return s } type Service struct { - im instancemgmt.InstanceManager - logger log.Logger + tlsManager tlsSettingsProvider + im instancemgmt.InstanceManager + logger log.Logger } func (s *Service) getDSInfo(ctx context.Context, pluginCtx backend.PluginContext) (*sqleng.DataSourceHandler, error) { @@ -59,7 +55,13 @@ func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest) return dsInfo.QueryData(ctx, req) } -func newPostgres(ctx context.Context, userFacingDefaultError string, rowLimit int64, dsInfo sqleng.DataSourceInfo, pgxConf *pgx.ConnConfig, logger log.Logger, settings backend.DataSourceInstanceSettings) (*sql.DB, *sqleng.DataSourceHandler, error) { +func newPostgres(ctx context.Context, userFacingDefaultError string, rowLimit int64, dsInfo sqleng.DataSourceInfo, cnnstr string, logger log.Logger, settings backend.DataSourceInstanceSettings) (*sql.DB, *sqleng.DataSourceHandler, error) { + connector, err := pq.NewConnector(cnnstr) + if err != nil { + logger.Error("postgres connector creation failed", "error", err) + return nil, nil, fmt.Errorf("postgres connector creation failed") + } + proxyClient, err := settings.ProxyClient(ctx) if err != nil { logger.Error("postgres proxy creation failed", "error", err) @@ -72,8 +74,9 @@ func newPostgres(ctx context.Context, userFacingDefaultError string, rowLimit in logger.Error("postgres proxy creation failed", "error", err) return nil, nil, fmt.Errorf("postgres proxy creation failed") } - - pgxConf.DialFunc = newPgxDialFunc(dialer) + postgresDialer := newPostgresProxyDialer(dialer) + // update the postgres dialer with the proxy dialer + connector.Dialer(postgresDialer) } config := sqleng.DataPluginConfiguration{ @@ -84,7 +87,7 @@ func newPostgres(ctx context.Context, userFacingDefaultError string, rowLimit in queryResultTransformer := postgresQueryResultTransformer{} - db := pgxstdlib.OpenDB(*pgxConf) + db := sql.OpenDB(connector) db.SetMaxOpenConns(config.DSInfo.JsonData.MaxOpenConns) db.SetMaxIdleConns(config.DSInfo.JsonData.MaxIdleConns) @@ -140,7 +143,7 @@ func (s *Service) newInstanceSettings() datasource.InstanceFactoryFunc { DecryptedSecureJSONData: settings.DecryptedSecureJSONData, } - pgxConf, err := generateConnectionConfig(dsInfo) + cnnstr, err := s.generateConnectionString(dsInfo) if err != nil { return nil, err } @@ -150,7 +153,7 @@ func (s *Service) newInstanceSettings() datasource.InstanceFactoryFunc { return nil, err } - _, handler, err := newPostgres(ctx, userFacingDefaultError, sqlCfg.RowLimit, dsInfo, pgxConf, logger, settings) + _, handler, err := newPostgres(ctx, userFacingDefaultError, sqlCfg.RowLimit, dsInfo, cnnstr, logger, settings) if err != nil { logger.Error("Failed connecting to Postgres", "err", err) @@ -167,11 +170,13 @@ func escape(input string) string { return strings.ReplaceAll(strings.ReplaceAll(input, `\`, `\\`), "'", `\'`) } -func generateConnectionConfig(dsInfo sqleng.DataSourceInfo) (*pgx.ConnConfig, error) { +func (s *Service) generateConnectionString(dsInfo sqleng.DataSourceInfo) (string, error) { + logger := s.logger var host string var port int if strings.HasPrefix(dsInfo.URL, "/") { host = dsInfo.URL + logger.Debug("Generating connection string with Unix socket specifier", "socket", host) } else { index := strings.LastIndex(dsInfo.URL, ":") v6Index := strings.Index(dsInfo.URL, "]") @@ -182,8 +187,12 @@ func generateConnectionConfig(dsInfo sqleng.DataSourceInfo) (*pgx.ConnConfig, er var err error port, err = strconv.Atoi(sp[1]) if err != nil { - return nil, fmt.Errorf("invalid port in host specifier %q: %w", sp[1], err) + return "", fmt.Errorf("invalid port in host specifier %q: %w", sp[1], err) } + + logger.Debug("Generating connection string with network host/port pair", "host", host, "port", port) + } else { + logger.Debug("Generating connection string with network host", "host", host) } } else { if index == v6Index+1 { @@ -191,39 +200,46 @@ func generateConnectionConfig(dsInfo sqleng.DataSourceInfo) (*pgx.ConnConfig, er var err error port, err = strconv.Atoi(dsInfo.URL[index+1:]) if err != nil { - return nil, fmt.Errorf("invalid port in host specifier %q: %w", dsInfo.URL[index+1:], err) + return "", fmt.Errorf("invalid port in host specifier %q: %w", dsInfo.URL[index+1:], err) } + + logger.Debug("Generating ipv6 connection string with network host/port pair", "host", host, "port", port) } else { host = dsInfo.URL[1 : len(dsInfo.URL)-1] + logger.Debug("Generating ipv6 connection string with network host", "host", host) } } } - // NOTE: we always set sslmode=disable in the connection string, we handle TLS manually later - connStr := fmt.Sprintf("sslmode=disable user='%s' password='%s' host='%s' dbname='%s'", + connStr := fmt.Sprintf("user='%s' password='%s' host='%s' dbname='%s'", escape(dsInfo.User), escape(dsInfo.DecryptedSecureJSONData["password"]), escape(host), escape(dsInfo.Database)) if port > 0 { connStr += fmt.Sprintf(" port=%d", port) } - conf, err := pgx.ParseConfig(connStr) + tlsSettings, err := s.tlsManager.getTLSSettings(dsInfo) if err != nil { - return nil, err + return "", err } - tlsConf, err := tls.GetTLSConfig(dsInfo, os.ReadFile, host) - if err != nil { - return nil, err + connStr += fmt.Sprintf(" sslmode='%s'", escape(tlsSettings.Mode)) + + // Attach root certificate if provided + if tlsSettings.RootCertFile != "" { + logger.Debug("Setting server root certificate", "tlsRootCert", tlsSettings.RootCertFile) + connStr += fmt.Sprintf(" sslrootcert='%s'", escape(tlsSettings.RootCertFile)) } - // before we set the TLS config, we need to make sure the `.Fallbacks` attribute is unset, see: - // https://github.com/jackc/pgx/discussions/1903#discussioncomment-8430146 - if len(conf.Fallbacks) > 0 { - return nil, errors.New("tls: fallbacks configured, unable to set up TLS config") + // Attach client certificate and key if both are provided + if tlsSettings.CertFile != "" && tlsSettings.CertKeyFile != "" { + logger.Debug("Setting TLS/SSL client auth", "tlsCert", tlsSettings.CertFile, "tlsKey", tlsSettings.CertKeyFile) + connStr += fmt.Sprintf(" sslcert='%s' sslkey='%s'", escape(tlsSettings.CertFile), escape(tlsSettings.CertKeyFile)) + } else if tlsSettings.CertFile != "" || tlsSettings.CertKeyFile != "" { + return "", fmt.Errorf("TLS/SSL client certificate and key must both be specified") } - conf.TLSConfig = tlsConf - return conf, nil + logger.Debug("Generated Postgres connection string successfully") + return connStr, nil } type postgresQueryResultTransformer struct{} @@ -251,44 +267,6 @@ func (s *Service) CheckHealth(ctx context.Context, req *backend.CheckHealthReque func (t *postgresQueryResultTransformer) GetConverterList() []sqlutil.StringConverter { return []sqlutil.StringConverter{ - { - Name: "handle TIME WITH TIME ZONE", - InputScanKind: reflect.Interface, - InputTypeName: strconv.Itoa(pgtype.TimetzOID), - ConversionFunc: func(in *string) (*string, error) { return in, nil }, - Replacer: &sqlutil.StringFieldReplacer{ - OutputFieldType: data.FieldTypeNullableTime, - ReplaceFunc: func(in *string) (any, error) { - if in == nil { - return nil, nil - } - v, err := time.Parse("15:04:05-07", *in) - if err != nil { - return nil, err - } - return &v, nil - }, - }, - }, - { - Name: "handle TIME", - InputScanKind: reflect.Interface, - InputTypeName: "TIME", - ConversionFunc: func(in *string) (*string, error) { return in, nil }, - Replacer: &sqlutil.StringFieldReplacer{ - OutputFieldType: data.FieldTypeNullableTime, - ReplaceFunc: func(in *string) (any, error) { - if in == nil { - return nil, nil - } - v, err := time.Parse("15:04:05", *in) - if err != nil { - return nil, err - } - return &v, nil - }, - }, - }, { Name: "handle FLOAT4", InputScanKind: reflect.Interface, diff --git a/pkg/tsdb/grafana-postgresql-datasource/postgres_snapshot_test.go b/pkg/tsdb/grafana-postgresql-datasource/postgres_snapshot_test.go index dca97dc1dc..0713e8cdac 100644 --- a/pkg/tsdb/grafana-postgresql-datasource/postgres_snapshot_test.go +++ b/pkg/tsdb/grafana-postgresql-datasource/postgres_snapshot_test.go @@ -14,7 +14,6 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/log" "github.com/grafana/grafana-plugin-sdk-go/experimental" - "github.com/jackc/pgx/v5" "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/tsdb/sqleng" @@ -52,7 +51,7 @@ func TestIntegrationPostgresSnapshots(t *testing.T) { t.Skip() } - getCnn := func() (*pgx.ConnConfig, error) { + getCnnStr := func() string { host := os.Getenv("POSTGRES_HOST") if host == "" { host = "localhost" @@ -62,10 +61,8 @@ func TestIntegrationPostgresSnapshots(t *testing.T) { port = "5432" } - cnnString := fmt.Sprintf("user=grafanatest password=grafanatest host=%s port=%s dbname=grafanadstest sslmode=disable", + return fmt.Sprintf("user=grafanatest password=grafanatest host=%s port=%s dbname=grafanadstest sslmode=disable", host, port) - - return pgx.ParseConfig(cnnString) } sqlQueryCommentRe := regexp.MustCompile(`^-- (.+)\n`) @@ -160,10 +157,9 @@ func TestIntegrationPostgresSnapshots(t *testing.T) { logger := log.New() - cnn, err := getCnn() - require.NoError(t, err) + cnnstr := getCnnStr() - db, handler, err := newPostgres(context.Background(), "error", 10000, dsInfo, cnn, logger, backend.DataSourceInstanceSettings{}) + db, handler, err := newPostgres(context.Background(), "error", 10000, dsInfo, cnnstr, logger, backend.DataSourceInstanceSettings{}) t.Cleanup((func() { _, err := db.Exec("DROP TABLE tbl") diff --git a/pkg/tsdb/grafana-postgresql-datasource/postgres_test.go b/pkg/tsdb/grafana-postgresql-datasource/postgres_test.go index 656b03b0a8..2dec40dc83 100644 --- a/pkg/tsdb/grafana-postgresql-datasource/postgres_test.go +++ b/pkg/tsdb/grafana-postgresql-datasource/postgres_test.go @@ -14,16 +14,19 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/grafana/grafana/pkg/tsdb/grafana-postgresql-datasource/tls" + "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/tsdb/sqleng" - "github.com/jackc/pgx/v5" - _ "github.com/jackc/pgx/v5/stdlib" + _ "github.com/lib/pq" ) -func TestGenerateConnectionConfig(t *testing.T) { - rootCertBytes, err := tls.CreateRandomRootCertBytes() - require.NoError(t, err) +// Test generateConnectionString. +func TestIntegrationGenerateConnectionString(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + cfg := setting.NewCfg() + cfg.DataPath = t.TempDir() testCases := []struct { desc string @@ -31,15 +34,10 @@ func TestGenerateConnectionConfig(t *testing.T) { user string password string database string - tlsMode string - tlsRootCert []byte + tlsSettings tlsSettings + expConnStr string expErr string - expHost string - expPort uint16 - expUser string - expPassword string - expDatabase string - expTLS bool + uid string }{ { desc: "Unix socket host", @@ -47,11 +45,8 @@ func TestGenerateConnectionConfig(t *testing.T) { user: "user", password: "password", database: "database", - tlsMode: "disable", - expUser: "user", - expPassword: "password", - expHost: "/var/run/postgresql", - expDatabase: "database", + tlsSettings: tlsSettings{Mode: "verify-full"}, + expConnStr: "user='user' password='password' host='/var/run/postgresql' dbname='database' sslmode='verify-full'", }, { desc: "TCP host", @@ -59,12 +54,8 @@ func TestGenerateConnectionConfig(t *testing.T) { user: "user", password: "password", database: "database", - tlsMode: "disable", - expUser: "user", - expPassword: "password", - expHost: "host", - expPort: 5432, - expDatabase: "database", + tlsSettings: tlsSettings{Mode: "verify-full"}, + expConnStr: "user='user' password='password' host='host' dbname='database' sslmode='verify-full'", }, { desc: "TCP/port host", @@ -72,12 +63,8 @@ func TestGenerateConnectionConfig(t *testing.T) { user: "user", password: "password", database: "database", - tlsMode: "disable", - expUser: "user", - expPassword: "password", - expHost: "host", - expPort: 1234, - expDatabase: "database", + tlsSettings: tlsSettings{Mode: "verify-full"}, + expConnStr: "user='user' password='password' host='host' dbname='database' port=1234 sslmode='verify-full'", }, { desc: "Ipv6 host", @@ -85,11 +72,8 @@ func TestGenerateConnectionConfig(t *testing.T) { user: "user", password: "password", database: "database", - tlsMode: "disable", - expUser: "user", - expPassword: "password", - expHost: "::1", - expDatabase: "database", + tlsSettings: tlsSettings{Mode: "verify-full"}, + expConnStr: "user='user' password='password' host='::1' dbname='database' sslmode='verify-full'", }, { desc: "Ipv6/port host", @@ -97,20 +81,16 @@ func TestGenerateConnectionConfig(t *testing.T) { user: "user", password: "password", database: "database", - tlsMode: "disable", - expUser: "user", - expPassword: "password", - expHost: "::1", - expPort: 1234, - expDatabase: "database", + tlsSettings: tlsSettings{Mode: "verify-full"}, + expConnStr: "user='user' password='password' host='::1' dbname='database' port=1234 sslmode='verify-full'", }, { - desc: "Invalid port", - host: "host:invalid", - user: "user", - database: "database", - tlsMode: "disable", - expErr: "invalid port in host specifier", + desc: "Invalid port", + host: "host:invalid", + user: "user", + database: "database", + tlsSettings: tlsSettings{}, + expErr: "invalid port in host specifier", }, { desc: "Password with single quote and backslash", @@ -118,11 +98,8 @@ func TestGenerateConnectionConfig(t *testing.T) { user: "user", password: `p'\assword`, database: "database", - tlsMode: "disable", - expUser: "user", - expPassword: `p'\assword`, - expHost: "host", - expDatabase: "database", + tlsSettings: tlsSettings{Mode: "verify-full"}, + expConnStr: `user='user' password='p\'\\assword' host='host' dbname='database' sslmode='verify-full'`, }, { desc: "User/DB with single quote and backslash", @@ -130,11 +107,8 @@ func TestGenerateConnectionConfig(t *testing.T) { user: `u'\ser`, password: `password`, database: `d'\atabase`, - tlsMode: "disable", - expUser: `u'\ser`, - expPassword: "password", - expDatabase: `d'\atabase`, - expHost: "host", + tlsSettings: tlsSettings{Mode: "verify-full"}, + expConnStr: `user='u\'\\ser' password='password' host='host' dbname='d\'\\atabase' sslmode='verify-full'`, }, { desc: "Custom TLS mode disabled", @@ -142,55 +116,45 @@ func TestGenerateConnectionConfig(t *testing.T) { user: "user", password: "password", database: "database", - tlsMode: "disable", - expUser: "user", - expPassword: "password", - expHost: "host", - expDatabase: "database", + tlsSettings: tlsSettings{Mode: "disable"}, + expConnStr: "user='user' password='password' host='host' dbname='database' sslmode='disable'", }, { - desc: "Custom TLS mode verify-full with certificate files", - host: "host", - user: "user", - password: "password", - database: "database", - tlsMode: "verify-full", - tlsRootCert: rootCertBytes, - expUser: "user", - expPassword: "password", - expDatabase: "database", - expHost: "host", - expTLS: true, + desc: "Custom TLS mode verify-full with certificate files", + host: "host", + user: "user", + password: "password", + database: "database", + tlsSettings: tlsSettings{ + Mode: "verify-full", + RootCertFile: "i/am/coding/ca.crt", + CertFile: "i/am/coding/client.crt", + CertKeyFile: "i/am/coding/client.key", + }, + expConnStr: "user='user' password='password' host='host' dbname='database' sslmode='verify-full' " + + "sslrootcert='i/am/coding/ca.crt' sslcert='i/am/coding/client.crt' sslkey='i/am/coding/client.key'", }, } for _, tt := range testCases { t.Run(tt.desc, func(t *testing.T) { + svc := Service{ + tlsManager: &tlsTestManager{settings: tt.tlsSettings}, + logger: backend.NewLoggerWith("logger", "tsdb.postgres"), + } + ds := sqleng.DataSourceInfo{ - URL: tt.host, - User: tt.user, - DecryptedSecureJSONData: map[string]string{ - "password": tt.password, - "tlsCACert": string(tt.tlsRootCert), - }, - Database: tt.database, - JsonData: sqleng.JsonData{ - Mode: tt.tlsMode, - ConfigurationMethod: "file-content", - }, + URL: tt.host, + User: tt.user, + DecryptedSecureJSONData: map[string]string{"password": tt.password}, + Database: tt.database, + UID: tt.uid, } - c, err := generateConnectionConfig(ds) + connStr, err := svc.generateConnectionString(ds) if tt.expErr == "" { require.NoError(t, err, tt.desc) - assert.Equal(t, tt.expHost, c.Host) - if tt.expPort != 0 { - assert.Equal(t, tt.expPort, c.Port) - } - assert.Equal(t, tt.expUser, c.User) - assert.Equal(t, tt.expDatabase, c.Database) - assert.Equal(t, tt.expPassword, c.Password) - require.Equal(t, tt.expTLS, c.TLSConfig != nil) + assert.Equal(t, tt.expConnStr, connStr) } else { require.Error(t, err, tt.desc) assert.True(t, strings.HasPrefix(err.Error(), tt.expErr), @@ -242,10 +206,9 @@ func TestIntegrationPostgres(t *testing.T) { logger := backend.NewLoggerWith("logger", "postgres.test") - cnn, err := postgresTestDBConn() - require.NoError(t, err) + cnnstr := postgresTestDBConnString() - db, exe, err := newPostgres(context.Background(), "error", 10000, dsInfo, cnn, logger, backend.DataSourceInstanceSettings{}) + db, exe, err := newPostgres(context.Background(), "error", 10000, dsInfo, cnnstr, logger, backend.DataSourceInstanceSettings{}) require.NoError(t, err) @@ -1299,7 +1262,7 @@ func TestIntegrationPostgres(t *testing.T) { t.Run("When row limit set to 1", func(t *testing.T) { dsInfo := sqleng.DataSourceInfo{} - _, handler, err := newPostgres(context.Background(), "error", 1, dsInfo, cnn, logger, backend.DataSourceInstanceSettings{}) + _, handler, err := newPostgres(context.Background(), "error", 1, dsInfo, cnnstr, logger, backend.DataSourceInstanceSettings{}) require.NoError(t, err) @@ -1414,6 +1377,14 @@ func genTimeRangeByInterval(from time.Time, duration time.Duration, interval tim return timeRange } +type tlsTestManager struct { + settings tlsSettings +} + +func (m *tlsTestManager) getTLSSettings(dsInfo sqleng.DataSourceInfo) (tlsSettings, error) { + return m.settings, nil +} + func isTestDbPostgres() bool { if db, present := os.LookupEnv("GRAFANA_TEST_DB"); present { return db == "postgres" @@ -1422,7 +1393,7 @@ func isTestDbPostgres() bool { return false } -func postgresTestDBConn() (*pgx.ConnConfig, error) { +func postgresTestDBConnString() string { host := os.Getenv("POSTGRES_HOST") if host == "" { host = "localhost" @@ -1431,8 +1402,6 @@ func postgresTestDBConn() (*pgx.ConnConfig, error) { if port == "" { port = "5432" } - connStr := fmt.Sprintf("user=grafanatest password=grafanatest host=%s port=%s dbname=grafanadstest sslmode=disable", + return fmt.Sprintf("user=grafanatest password=grafanatest host=%s port=%s dbname=grafanadstest sslmode=disable", host, port) - - return pgx.ParseConfig(connStr) } diff --git a/pkg/tsdb/grafana-postgresql-datasource/proxy.go b/pkg/tsdb/grafana-postgresql-datasource/proxy.go index 0c836eb345..d06d8b6815 100644 --- a/pkg/tsdb/grafana-postgresql-datasource/proxy.go +++ b/pkg/tsdb/grafana-postgresql-datasource/proxy.go @@ -3,17 +3,33 @@ package postgres import ( "context" "net" + "time" + "github.com/lib/pq" "golang.org/x/net/proxy" ) -type PgxDialFunc = func(ctx context.Context, network string, address string) (net.Conn, error) +// we wrap the proxy.Dialer to become dialer that the postgres module accepts +func newPostgresProxyDialer(dialer proxy.Dialer) pq.Dialer { + return &postgresProxyDialer{d: dialer} +} + +var _ pq.Dialer = (&postgresProxyDialer{}) + +// postgresProxyDialer implements the postgres dialer using a proxy dialer, as their functions differ slightly +type postgresProxyDialer struct { + d proxy.Dialer +} + +// Dial uses the normal proxy dial function with the updated dialer +func (p *postgresProxyDialer) Dial(network, addr string) (c net.Conn, err error) { + return p.d.Dial(network, addr) +} -func newPgxDialFunc(dialer proxy.Dialer) PgxDialFunc { - dialFunc := - func(ctx context.Context, network string, addr string) (net.Conn, error) { - return dialer.Dial(network, addr) - } +// DialTimeout uses the normal postgres dial timeout function with the updated dialer +func (p *postgresProxyDialer) DialTimeout(network, address string, timeout time.Duration) (net.Conn, error) { + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() - return dialFunc + return p.d.(proxy.ContextDialer).DialContext(ctx, network, address) } diff --git a/pkg/tsdb/grafana-postgresql-datasource/proxy_test.go b/pkg/tsdb/grafana-postgresql-datasource/proxy_test.go index afd205bd37..ec36e1a1ea 100644 --- a/pkg/tsdb/grafana-postgresql-datasource/proxy_test.go +++ b/pkg/tsdb/grafana-postgresql-datasource/proxy_test.go @@ -1,12 +1,12 @@ package postgres import ( + "database/sql" "fmt" "net" "testing" - "github.com/jackc/pgx/v5" - pgxstdlib "github.com/jackc/pgx/v5/stdlib" + "github.com/lib/pq" "github.com/stretchr/testify/require" "golang.org/x/net/proxy" ) @@ -25,13 +25,13 @@ func TestPostgresProxyDriver(t *testing.T) { cnnstr := fmt.Sprintf("postgres://auser:password@%s/db?sslmode=disable", dbURL) t.Run("Connector should use dialer context that routes through the socks proxy to db", func(t *testing.T) { - pgxConf, err := pgx.ParseConfig(cnnstr) + connector, err := pq.NewConnector(cnnstr) require.NoError(t, err) + dialer := newPostgresProxyDialer(&testDialer{}) - pgxConf.DialFunc = newPgxDialFunc(&testDialer{}) - - db := pgxstdlib.OpenDB(*pgxConf) + connector.Dialer(dialer) + db := sql.OpenDB(connector) err = db.Ping() require.Contains(t, err.Error(), "test-dialer is not functional") diff --git a/pkg/tsdb/grafana-postgresql-datasource/testdata/table/timestamp_convert_real.golden.jsonc b/pkg/tsdb/grafana-postgresql-datasource/testdata/table/timestamp_convert_real.golden.jsonc index e6d1dfd238..af23cf8b5b 100644 --- a/pkg/tsdb/grafana-postgresql-datasource/testdata/table/timestamp_convert_real.golden.jsonc +++ b/pkg/tsdb/grafana-postgresql-datasource/testdata/table/timestamp_convert_real.golden.jsonc @@ -9,16 +9,16 @@ // } // Name: // Dimensions: 4 Fields by 4 Rows -// +--------------------------------------+-----------------------------------+-----------------------+-----------------------------------+ -// | Name: reallyt | Name: time | Name: n | Name: timeend | -// | Labels: | Labels: | Labels: | Labels: | -// | Type: []*time.Time | Type: []*time.Time | Type: []*float64 | Type: []*time.Time | -// +--------------------------------------+-----------------------------------+-----------------------+-----------------------------------+ -// | 2023-12-21 12:22:24 +0000 UTC | 2023-12-21 12:22:24 +0000 UTC | 1.703161344e+09 | 2023-12-21 12:22:52 +0000 UTC | -// | 2023-12-21 12:20:33.408 +0000 UTC | 2023-12-21 12:20:33.408 +0000 UTC | 1.703161233408e+12 | 2023-12-21 12:21:52.522 +0000 UTC | -// | 2023-12-21 12:20:41.050022 +0000 UTC | 2023-12-21 12:20:41.05 +0000 UTC | 1.703161241050022e+18 | 2023-12-21 12:21:52.522 +0000 UTC | -// | null | null | null | null | -// +--------------------------------------+-----------------------------------+-----------------------+-----------------------------------+ +// +--------------------------------------+-------------------------------+------------------+-----------------------------------+ +// | Name: reallyt | Name: time | Name: n | Name: timeend | +// | Labels: | Labels: | Labels: | Labels: | +// | Type: []*time.Time | Type: []*time.Time | Type: []*float64 | Type: []*time.Time | +// +--------------------------------------+-------------------------------+------------------+-----------------------------------+ +// | 2023-12-21 12:22:24 +0000 UTC | 2023-12-21 12:21:40 +0000 UTC | 1.7031613e+09 | 2023-12-21 12:22:52 +0000 UTC | +// | 2023-12-21 12:20:33.408 +0000 UTC | 2023-12-21 12:20:00 +0000 UTC | 1.7031612e+12 | 2023-12-21 12:21:52.522 +0000 UTC | +// | 2023-12-21 12:20:41.050022 +0000 UTC | 2023-12-21 12:20:00 +0000 UTC | 1.7031612e+18 | 2023-12-21 12:21:52.522 +0000 UTC | +// | null | null | null | null | +// +--------------------------------------+-------------------------------+------------------+-----------------------------------+ // // // 🌟 This was machine generated. Do not edit. 🌟 @@ -78,15 +78,15 @@ null ], [ - 1703161344000, - 1703161233408, - 1703161241050, + 1703161300000, + 1703161200000, + 1703161200000, null ], [ - 1703161344, - 1703161233408, - 1703161241050022000, + 1703161300, + 1703161200000, + 1703161200000000000, null ], [ diff --git a/pkg/tsdb/grafana-postgresql-datasource/testdata/table/types_datetime.golden.jsonc b/pkg/tsdb/grafana-postgresql-datasource/testdata/table/types_datetime.golden.jsonc index 09e9403459..48e55e7b07 100644 --- a/pkg/tsdb/grafana-postgresql-datasource/testdata/table/types_datetime.golden.jsonc +++ b/pkg/tsdb/grafana-postgresql-datasource/testdata/table/types_datetime.golden.jsonc @@ -9,14 +9,14 @@ // } // Name: // Dimensions: 12 Fields by 2 Rows -// +--------------------------------------+--------------------------------------+--------------------------------------+--------------------------------------+-------------------------------+-------------------------------+--------------------------------------+--------------------------------------+----------------------------------------+----------------------------------------+-----------------+-----------------+ -// | Name: ts | Name: tsnn | Name: tsz | Name: tsznn | Name: d | Name: dnn | Name: t | Name: tnn | Name: tz | Name: tznn | Name: i | Name: inn | -// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | -// | Type: []*time.Time | Type: []*time.Time | Type: []*time.Time | Type: []*time.Time | Type: []*time.Time | Type: []*time.Time | Type: []*time.Time | Type: []*time.Time | Type: []*time.Time | Type: []*time.Time | Type: []*string | Type: []*string | -// +--------------------------------------+--------------------------------------+--------------------------------------+--------------------------------------+-------------------------------+-------------------------------+--------------------------------------+--------------------------------------+----------------------------------------+----------------------------------------+-----------------+-----------------+ -// | 2023-11-15 05:06:07.123456 +0000 UTC | 2023-11-15 05:06:08.123456 +0000 UTC | 2021-07-22 11:22:33.654321 +0000 UTC | 2021-07-22 11:22:34.654321 +0000 UTC | 2023-12-20 00:00:00 +0000 UTC | 2023-12-21 00:00:00 +0000 UTC | 0000-01-01 12:34:56.234567 +0000 UTC | 0000-01-01 12:34:57.234567 +0000 UTC | 0000-01-01 23:12:36.765432 +0100 +0100 | 0000-01-01 23:12:37.765432 +0100 +0100 | 00:00:00.987654 | 00:00:00.887654 | -// | null | 2023-11-15 05:06:09.123456 +0000 UTC | null | 2021-07-22 11:22:35.654321 +0000 UTC | null | 2023-12-22 00:00:00 +0000 UTC | null | 0000-01-01 12:34:58.234567 +0000 UTC | null | 0000-01-01 23:12:38.765432 +0100 +0100 | null | 00:00:00.787654 | -// +--------------------------------------+--------------------------------------+--------------------------------------+--------------------------------------+-------------------------------+-------------------------------+--------------------------------------+--------------------------------------+----------------------------------------+----------------------------------------+-----------------+-----------------+ +// +----------------------------------------+----------------------------------------+--------------------------------------+--------------------------------------+---------------------------------+---------------------------------+--------------------------------------+--------------------------------------+----------------------------------------+----------------------------------------+-----------------+-----------------+ +// | Name: ts | Name: tsnn | Name: tsz | Name: tsznn | Name: d | Name: dnn | Name: t | Name: tnn | Name: tz | Name: tznn | Name: i | Name: inn | +// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | +// | Type: []*time.Time | Type: []*time.Time | Type: []*time.Time | Type: []*time.Time | Type: []*time.Time | Type: []*time.Time | Type: []*time.Time | Type: []*time.Time | Type: []*time.Time | Type: []*time.Time | Type: []*string | Type: []*string | +// +----------------------------------------+----------------------------------------+--------------------------------------+--------------------------------------+---------------------------------+---------------------------------+--------------------------------------+--------------------------------------+----------------------------------------+----------------------------------------+-----------------+-----------------+ +// | 2023-11-15 05:06:07.123456 +0000 +0000 | 2023-11-15 05:06:08.123456 +0000 +0000 | 2021-07-22 11:22:33.654321 +0000 UTC | 2021-07-22 11:22:34.654321 +0000 UTC | 2023-12-20 00:00:00 +0000 +0000 | 2023-12-21 00:00:00 +0000 +0000 | 0000-01-01 12:34:56.234567 +0000 UTC | 0000-01-01 12:34:57.234567 +0000 UTC | 0000-01-01 23:12:36.765432 +0100 +0100 | 0000-01-01 23:12:37.765432 +0100 +0100 | 00:00:00.987654 | 00:00:00.887654 | +// | null | 2023-11-15 05:06:09.123456 +0000 +0000 | null | 2021-07-22 11:22:35.654321 +0000 UTC | null | 2023-12-22 00:00:00 +0000 +0000 | null | 0000-01-01 12:34:58.234567 +0000 UTC | null | 0000-01-01 23:12:38.765432 +0100 +0100 | null | 00:00:00.787654 | +// +----------------------------------------+----------------------------------------+--------------------------------------+--------------------------------------+---------------------------------+---------------------------------+--------------------------------------+--------------------------------------+----------------------------------------+----------------------------------------+-----------------+-----------------+ // // // 🌟 This was machine generated. Do not edit. 🌟 diff --git a/pkg/tsdb/grafana-postgresql-datasource/tls/tls.go b/pkg/tsdb/grafana-postgresql-datasource/tls/tls.go deleted file mode 100644 index e5e409dc55..0000000000 --- a/pkg/tsdb/grafana-postgresql-datasource/tls/tls.go +++ /dev/null @@ -1,147 +0,0 @@ -package tls - -import ( - "crypto/tls" - "crypto/x509" - "errors" - - "github.com/grafana/grafana/pkg/tsdb/sqleng" -) - -// we support 4 postgres tls modes: -// disable - no tls -// require - use tls -// verify-ca - use tls, verify root cert but not the hostname -// verify-full - use tls, verify root cert -// (for all the options except `disable`, you can optionally use client certificates) - -var errNoRootCert = errors.New("tls: missing root certificate") - -func getTLSConfigRequire(certs *Certs) (*tls.Config, error) { - // we may have a client-cert, we do not have a root-cert - - // see https://www.postgresql.org/docs/12/libpq-ssl.html , - // mode=require + provided root-cert should behave as mode=verify-ca - if certs.rootCerts != nil { - return getTLSConfigVerifyCA(certs) - } - - return &tls.Config{ - InsecureSkipVerify: true, // we do not verify the root cert - Certificates: certs.clientCerts, - }, nil -} - -// to implement the verify-ca mode, we need to do this: -// - for the root certificate -// - verify that the certificate we receive from the server is trusted, -// meaning it relates to our root certificate -// - we DO NOT verify that the hostname of the database matches -// the hostname in the certificate -// -// the problem is, `go“ does not offer such an option. -// by default, it will verify both things. -// -// so what we do is: -// - we turn off the default-verification with `InsecureSkipVerify` -// - we implement our own verification using `VerifyConnection` -// -// extra info about this: -// - there is a rejected feature-request about this at https://github.com/golang/go/issues/21971 -// - the recommended workaround is based on VerifyPeerCertificate -// - there is even example code at https://github.com/golang/go/commit/29cfb4d3c3a97b6f426d1b899234da905be699aa -// - but later the example code was changed to use VerifyConnection instead: -// https://github.com/golang/go/commit/7eb5941b95a588a23f18fa4c22fe42ff0119c311 -// -// a verifyConnection example is at https://pkg.go.dev/crypto/tls#example-Config-VerifyConnection . -// -// this is how the `pgx` library handles verify-ca: -// -// https://github.com/jackc/pgx/blob/5c63f646f820ca9696fc3515c1caf2a557d562e5/pgconn/config.go#L657-L690 -// (unfortunately pgx only handles this for certificate-provided-as-path, so we cannot rely on it) -func getTLSConfigVerifyCA(certs *Certs) (*tls.Config, error) { - // we must have a root certificate - if certs.rootCerts == nil { - return nil, errNoRootCert - } - - conf := tls.Config{ - Certificates: certs.clientCerts, - InsecureSkipVerify: true, // we turn off the default-verification, we'll do VerifyConnection instead - VerifyConnection: func(state tls.ConnectionState) error { - // we add all the certificates to the pool, we skip the first cert. - intermediates := x509.NewCertPool() - for _, c := range state.PeerCertificates[1:] { - intermediates.AddCert(c) - } - - opts := x509.VerifyOptions{ - Roots: certs.rootCerts, - Intermediates: intermediates, - } - - // we call `Verify()` on the first cert (that we skipped previously) - _, err := state.PeerCertificates[0].Verify(opts) - return err - }, - RootCAs: certs.rootCerts, - } - - return &conf, nil -} - -func getTLSConfigVerifyFull(certs *Certs, serverName string) (*tls.Config, error) { - // we must have a root certificate - if certs.rootCerts == nil { - return nil, errNoRootCert - } - - conf := tls.Config{ - Certificates: certs.clientCerts, - ServerName: serverName, - RootCAs: certs.rootCerts, - } - - return &conf, nil -} - -func IsTLSEnabled(dsInfo sqleng.DataSourceInfo) bool { - mode := dsInfo.JsonData.Mode - return mode != "disable" -} - -// returns `nil` if tls is disabled -func GetTLSConfig(dsInfo sqleng.DataSourceInfo, readFile ReadFileFunc, serverName string) (*tls.Config, error) { - mode := dsInfo.JsonData.Mode - // we need to special-case the no-tls-mode - if mode == "disable" { - return nil, nil - } - - // for all the remaining cases we need to load - // both the root-cert if exists, and the client-cert if exists. - certBytes, err := loadCertificateBytes(dsInfo, readFile) - if err != nil { - return nil, err - } - - certs, err := createCertificates(certBytes) - if err != nil { - return nil, err - } - - switch mode { - // `disable` already handled - case "": - // for backward-compatibility reasons this is the same as `require` - return getTLSConfigRequire(certs) - case "require": - return getTLSConfigRequire(certs) - case "verify-ca": - return getTLSConfigVerifyCA(certs) - case "verify-full": - return getTLSConfigVerifyFull(certs, serverName) - default: - return nil, errors.New("tls: invalid mode " + mode) - } -} diff --git a/pkg/tsdb/grafana-postgresql-datasource/tls/tls_loader.go b/pkg/tsdb/grafana-postgresql-datasource/tls/tls_loader.go deleted file mode 100644 index 6c19d3801d..0000000000 --- a/pkg/tsdb/grafana-postgresql-datasource/tls/tls_loader.go +++ /dev/null @@ -1,101 +0,0 @@ -package tls - -import ( - "crypto/tls" - "crypto/x509" - "errors" - - "github.com/grafana/grafana/pkg/tsdb/sqleng" -) - -// this file deals with locating and loading the certificates, -// from json-data or from disk. - -type CertBytes struct { - rootCert []byte - clientKey []byte - clientCert []byte -} - -type ReadFileFunc = func(name string) ([]byte, error) - -var errPartialClientCertNoKey = errors.New("tls: client cert provided but client key missing") -var errPartialClientCertNoCert = errors.New("tls: client key provided but client cert missing") - -// certificates can be stored either as encrypted-json-data, or as file-path -func loadCertificateBytes(dsInfo sqleng.DataSourceInfo, readFile ReadFileFunc) (*CertBytes, error) { - if dsInfo.JsonData.ConfigurationMethod == "file-content" { - return &CertBytes{ - rootCert: []byte(dsInfo.DecryptedSecureJSONData["tlsCACert"]), - clientKey: []byte(dsInfo.DecryptedSecureJSONData["tlsClientKey"]), - clientCert: []byte(dsInfo.DecryptedSecureJSONData["tlsClientCert"]), - }, nil - } else { - c := CertBytes{} - - if dsInfo.JsonData.RootCertFile != "" { - rootCert, err := readFile(dsInfo.JsonData.RootCertFile) - if err != nil { - return nil, err - } - c.rootCert = rootCert - } - - if dsInfo.JsonData.CertKeyFile != "" { - clientKey, err := readFile(dsInfo.JsonData.CertKeyFile) - if err != nil { - return nil, err - } - c.clientKey = clientKey - } - - if dsInfo.JsonData.CertFile != "" { - clientCert, err := readFile(dsInfo.JsonData.CertFile) - if err != nil { - return nil, err - } - c.clientCert = clientCert - } - - return &c, nil - } -} - -type Certs struct { - clientCerts []tls.Certificate - rootCerts *x509.CertPool -} - -func createCertificates(certBytes *CertBytes) (*Certs, error) { - certs := Certs{} - - if len(certBytes.rootCert) > 0 { - pool := x509.NewCertPool() - ok := pool.AppendCertsFromPEM(certBytes.rootCert) - if !ok { - return nil, errors.New("tls: failed to add root certificate") - } - certs.rootCerts = pool - } - - hasClientKey := len(certBytes.clientKey) > 0 - hasClientCert := len(certBytes.clientCert) > 0 - - if hasClientKey && hasClientCert { - cert, err := tls.X509KeyPair(certBytes.clientCert, certBytes.clientKey) - if err != nil { - return nil, err - } - certs.clientCerts = []tls.Certificate{cert} - } - - if hasClientKey && (!hasClientCert) { - return nil, errPartialClientCertNoCert - } - - if hasClientCert && (!hasClientKey) { - return nil, errPartialClientCertNoKey - } - - return &certs, nil -} diff --git a/pkg/tsdb/grafana-postgresql-datasource/tls/tls_test.go b/pkg/tsdb/grafana-postgresql-datasource/tls/tls_test.go deleted file mode 100644 index bce63e1c43..0000000000 --- a/pkg/tsdb/grafana-postgresql-datasource/tls/tls_test.go +++ /dev/null @@ -1,382 +0,0 @@ -package tls - -import ( - "errors" - "os" - "testing" - - "github.com/grafana/grafana/pkg/tsdb/sqleng" - "github.com/stretchr/testify/require" -) - -func noReadFile(path string) ([]byte, error) { - return nil, errors.New("not implemented") -} - -func TestTLSNoMode(t *testing.T) { - // for backward-compatibility reason, - // when mode is unset, it defaults to `require` - dsInfo := sqleng.DataSourceInfo{ - JsonData: sqleng.JsonData{ - ConfigurationMethod: "", - }, - } - c, err := GetTLSConfig(dsInfo, noReadFile, "localhost") - require.NoError(t, err) - require.NotNil(t, c) - require.True(t, c.InsecureSkipVerify) -} - -func TestTLSDisable(t *testing.T) { - dsInfo := sqleng.DataSourceInfo{ - JsonData: sqleng.JsonData{ - Mode: "disable", - ConfigurationMethod: "", - }, - } - c, err := GetTLSConfig(dsInfo, noReadFile, "localhost") - require.NoError(t, err) - require.Nil(t, c) -} - -func TestTLSRequire(t *testing.T) { - dsInfo := sqleng.DataSourceInfo{ - JsonData: sqleng.JsonData{ - Mode: "require", - ConfigurationMethod: "", - }, - } - c, err := GetTLSConfig(dsInfo, noReadFile, "localhost") - require.NoError(t, err) - require.NotNil(t, c) - require.True(t, c.InsecureSkipVerify) - require.Nil(t, c.RootCAs) -} - -func TestTLSRequireWithRootCert(t *testing.T) { - rootCertBytes, err := CreateRandomRootCertBytes() - require.NoError(t, err) - - dsInfo := sqleng.DataSourceInfo{ - JsonData: sqleng.JsonData{ - Mode: "require", - ConfigurationMethod: "file-content", - }, - DecryptedSecureJSONData: map[string]string{ - "tlsCACert": string(rootCertBytes), - }, - } - c, err := GetTLSConfig(dsInfo, noReadFile, "localhost") - require.NoError(t, err) - require.NotNil(t, c) - require.True(t, c.InsecureSkipVerify) - require.NotNil(t, c.VerifyConnection) - require.NotNil(t, c.RootCAs) // TODO: not the best, but nothing better available -} - -func TestTLSVerifyCA(t *testing.T) { - rootCertBytes, err := CreateRandomRootCertBytes() - require.NoError(t, err) - - dsInfo := sqleng.DataSourceInfo{ - JsonData: sqleng.JsonData{ - Mode: "verify-ca", - ConfigurationMethod: "file-content", - }, - DecryptedSecureJSONData: map[string]string{ - "tlsCACert": string(rootCertBytes), - }, - } - c, err := GetTLSConfig(dsInfo, noReadFile, "localhost") - require.NoError(t, err) - require.NotNil(t, c) - require.True(t, c.InsecureSkipVerify) - require.NotNil(t, c.VerifyConnection) - require.NotNil(t, c.RootCAs) // TODO: not the best, but nothing better available -} - -func TestTLSVerifyCAMisingRootCert(t *testing.T) { - dsInfo := sqleng.DataSourceInfo{ - JsonData: sqleng.JsonData{ - Mode: "verify-ca", - ConfigurationMethod: "file-content", - }, - DecryptedSecureJSONData: map[string]string{}, - } - _, err := GetTLSConfig(dsInfo, noReadFile, "localhost") - require.ErrorIs(t, err, errNoRootCert) -} - -func TestTLSClientCert(t *testing.T) { - clientKey, clientCert, err := CreateRandomClientCert() - require.NoError(t, err) - - dsInfo := sqleng.DataSourceInfo{ - JsonData: sqleng.JsonData{ - Mode: "require", - ConfigurationMethod: "file-content", - }, - DecryptedSecureJSONData: map[string]string{ - "tlsClientCert": string(clientCert), - "tlsClientKey": string(clientKey), - }, - } - c, err := GetTLSConfig(dsInfo, noReadFile, "localhost") - require.NoError(t, err) - require.NotNil(t, c) - require.Len(t, c.Certificates, 1) -} - -func TestTLSMethodFileContentClientCertMissingKey(t *testing.T) { - _, clientCert, err := CreateRandomClientCert() - require.NoError(t, err) - - dsInfo := sqleng.DataSourceInfo{ - JsonData: sqleng.JsonData{ - Mode: "require", - ConfigurationMethod: "file-content", - }, - DecryptedSecureJSONData: map[string]string{ - "tlsClientCert": string(clientCert), - }, - } - _, err = GetTLSConfig(dsInfo, noReadFile, "localhost") - require.ErrorIs(t, err, errPartialClientCertNoKey) -} - -func TestTLSMethodFileContentClientCertMissingCert(t *testing.T) { - clientKey, _, err := CreateRandomClientCert() - require.NoError(t, err) - - dsInfo := sqleng.DataSourceInfo{ - JsonData: sqleng.JsonData{ - Mode: "require", - ConfigurationMethod: "file-content", - }, - DecryptedSecureJSONData: map[string]string{ - "tlsClientKey": string(clientKey), - }, - } - _, err = GetTLSConfig(dsInfo, noReadFile, "localhost") - require.ErrorIs(t, err, errPartialClientCertNoCert) -} - -func TestTLSMethodFilePathClientCertMissingKey(t *testing.T) { - clientKey, _, err := CreateRandomClientCert() - require.NoError(t, err) - - readFile := newMockReadFile(map[string]([]byte){ - "path1": clientKey, - }) - - dsInfo := sqleng.DataSourceInfo{ - JsonData: sqleng.JsonData{ - Mode: "require", - ConfigurationMethod: "file-path", - CertKeyFile: "path1", - }, - } - _, err = GetTLSConfig(dsInfo, readFile, "localhost") - require.ErrorIs(t, err, errPartialClientCertNoCert) -} - -func TestTLSMethodFilePathClientCertMissingCert(t *testing.T) { - _, clientCert, err := CreateRandomClientCert() - require.NoError(t, err) - - readFile := newMockReadFile(map[string]([]byte){ - "path1": clientCert, - }) - - dsInfo := sqleng.DataSourceInfo{ - JsonData: sqleng.JsonData{ - Mode: "require", - ConfigurationMethod: "file-path", - CertFile: "path1", - }, - } - _, err = GetTLSConfig(dsInfo, readFile, "localhost") - require.ErrorIs(t, err, errPartialClientCertNoKey) -} - -func TestTLSVerifyFull(t *testing.T) { - rootCertBytes, err := CreateRandomRootCertBytes() - require.NoError(t, err) - - dsInfo := sqleng.DataSourceInfo{ - JsonData: sqleng.JsonData{ - Mode: "verify-full", - ConfigurationMethod: "file-content", - }, - DecryptedSecureJSONData: map[string]string{ - "tlsCACert": string(rootCertBytes), - }, - } - c, err := GetTLSConfig(dsInfo, noReadFile, "localhost") - require.NoError(t, err) - require.NotNil(t, c) - require.False(t, c.InsecureSkipVerify) - require.Nil(t, c.VerifyConnection) - require.NotNil(t, c.RootCAs) // TODO: not the best, but nothing better available -} - -func TestTLSMethodFileContent(t *testing.T) { - rootCertBytes, err := CreateRandomRootCertBytes() - require.NoError(t, err) - - clientKey, clientCert, err := CreateRandomClientCert() - require.NoError(t, err) - - dsInfo := sqleng.DataSourceInfo{ - JsonData: sqleng.JsonData{ - Mode: "verify-full", - ConfigurationMethod: "file-content", - }, - DecryptedSecureJSONData: map[string]string{ - "tlsCACert": string(rootCertBytes), - "tlsClientCert": string(clientCert), - "tlsClientKey": string(clientKey), - }, - } - c, err := GetTLSConfig(dsInfo, noReadFile, "localhost") - require.NoError(t, err) - require.NotNil(t, c) - require.Len(t, c.Certificates, 1) - require.NotNil(t, c.RootCAs) // TODO: not the best, but nothing better available -} - -func TestTLSMethodFilePath(t *testing.T) { - rootCertBytes, err := CreateRandomRootCertBytes() - require.NoError(t, err) - - clientKey, clientCert, err := CreateRandomClientCert() - require.NoError(t, err) - - readFile := newMockReadFile(map[string]([]byte){ - "root-cert-path": rootCertBytes, - "client-key-path": clientKey, - "client-cert-path": clientCert, - }) - - dsInfo := sqleng.DataSourceInfo{ - JsonData: sqleng.JsonData{ - Mode: "verify-full", - ConfigurationMethod: "file-path", - RootCertFile: "root-cert-path", - CertKeyFile: "client-key-path", - CertFile: "client-cert-path", - }, - } - c, err := GetTLSConfig(dsInfo, readFile, "localhost") - require.NoError(t, err) - require.NotNil(t, c) - require.Len(t, c.Certificates, 1) - require.NotNil(t, c.RootCAs) // TODO: not the best, but nothing better available -} - -func TestTLSMethodFilePathRootCertDoesNotExist(t *testing.T) { - readFile := newMockReadFile(map[string]([]byte){}) - - dsInfo := sqleng.DataSourceInfo{ - JsonData: sqleng.JsonData{ - Mode: "verify-full", - ConfigurationMethod: "file-path", - RootCertFile: "path1", - }, - } - _, err := GetTLSConfig(dsInfo, readFile, "localhost") - require.ErrorIs(t, err, os.ErrNotExist) -} - -func TestTLSMethodFilePathClientCertKeyDoesNotExist(t *testing.T) { - _, clientCert, err := CreateRandomClientCert() - require.NoError(t, err) - - readFile := newMockReadFile(map[string]([]byte){ - "cert-path": clientCert, - }) - - dsInfo := sqleng.DataSourceInfo{ - JsonData: sqleng.JsonData{ - Mode: "require", - ConfigurationMethod: "file-path", - CertKeyFile: "key-path", - CertFile: "cert-path", - }, - } - _, err = GetTLSConfig(dsInfo, readFile, "localhost") - require.ErrorIs(t, err, os.ErrNotExist) -} - -func TestTLSMethodFilePathClientCertCertDoesNotExist(t *testing.T) { - clientKey, _, err := CreateRandomClientCert() - require.NoError(t, err) - - readFile := newMockReadFile(map[string]([]byte){ - "key-path": clientKey, - }) - - dsInfo := sqleng.DataSourceInfo{ - JsonData: sqleng.JsonData{ - Mode: "require", - ConfigurationMethod: "file-path", - CertKeyFile: "key-path", - CertFile: "cert-path", - }, - } - _, err = GetTLSConfig(dsInfo, readFile, "localhost") - require.ErrorIs(t, err, os.ErrNotExist) -} - -// method="" equals to method="file-path" -func TestTLSMethodEmpty(t *testing.T) { - rootCertBytes, err := CreateRandomRootCertBytes() - require.NoError(t, err) - - clientKey, clientCert, err := CreateRandomClientCert() - require.NoError(t, err) - - readFile := newMockReadFile(map[string]([]byte){ - "root-cert-path": rootCertBytes, - "client-key-path": clientKey, - "client-cert-path": clientCert, - }) - - dsInfo := sqleng.DataSourceInfo{ - JsonData: sqleng.JsonData{ - Mode: "verify-full", - ConfigurationMethod: "", - RootCertFile: "root-cert-path", - CertKeyFile: "client-key-path", - CertFile: "client-cert-path", - }, - } - c, err := GetTLSConfig(dsInfo, readFile, "localhost") - require.NoError(t, err) - require.NotNil(t, c) - require.Len(t, c.Certificates, 1) - require.NotNil(t, c.RootCAs) // TODO: not the best, but nothing better available -} - -func TestTLSVerifyFullMisingRootCert(t *testing.T) { - dsInfo := sqleng.DataSourceInfo{ - JsonData: sqleng.JsonData{ - Mode: "verify-full", - ConfigurationMethod: "file-content", - }, - DecryptedSecureJSONData: map[string]string{}, - } - _, err := GetTLSConfig(dsInfo, noReadFile, "localhost") - require.ErrorIs(t, err, errNoRootCert) -} - -func TestTLSInvalidMode(t *testing.T) { - dsInfo := sqleng.DataSourceInfo{ - JsonData: sqleng.JsonData{ - Mode: "not-a-valid-mode", - }, - } - - _, err := GetTLSConfig(dsInfo, noReadFile, "localhost") - require.Error(t, err) -} diff --git a/pkg/tsdb/grafana-postgresql-datasource/tls/tls_test_helpers.go b/pkg/tsdb/grafana-postgresql-datasource/tls/tls_test_helpers.go deleted file mode 100644 index 1b62df63d0..0000000000 --- a/pkg/tsdb/grafana-postgresql-datasource/tls/tls_test_helpers.go +++ /dev/null @@ -1,105 +0,0 @@ -package tls - -import ( - "crypto/rand" - "crypto/rsa" - "crypto/x509" - "crypto/x509/pkix" - "encoding/pem" - "math/big" - "os" - "time" -) - -func CreateRandomRootCertBytes() ([]byte, error) { - cert := x509.Certificate{ - SerialNumber: big.NewInt(42), - Subject: pkix.Name{ - CommonName: "test1", - }, - NotBefore: time.Now(), - NotAfter: time.Now().AddDate(10, 0, 0), - IsCA: true, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, - KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, - BasicConstraintsValid: true, - } - - key, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - return nil, err - } - - bytes, err := x509.CreateCertificate(rand.Reader, &cert, &cert, &key.PublicKey, key) - if err != nil { - return nil, err - } - - return pem.EncodeToMemory(&pem.Block{ - Type: "CERTIFICATE", - Bytes: bytes, - }), nil -} - -func CreateRandomClientCert() ([]byte, []byte, error) { - caKey, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - return nil, nil, err - } - - key, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - return nil, nil, err - } - - keyBytes := pem.EncodeToMemory(&pem.Block{ - Type: "RSA PRIVATE KEY", - Bytes: x509.MarshalPKCS1PrivateKey(key), - }) - - caCert := x509.Certificate{ - SerialNumber: big.NewInt(42), - Subject: pkix.Name{ - CommonName: "test1", - }, - NotBefore: time.Now(), - NotAfter: time.Now().AddDate(10, 0, 0), - IsCA: true, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, - KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, - BasicConstraintsValid: true, - } - - cert := x509.Certificate{ - SerialNumber: big.NewInt(2019), - Subject: pkix.Name{ - CommonName: "test1", - }, - NotBefore: time.Now(), - NotAfter: time.Now().AddDate(10, 0, 0), - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, - KeyUsage: x509.KeyUsageDigitalSignature, - } - - certData, err := x509.CreateCertificate(rand.Reader, &cert, &caCert, &key.PublicKey, caKey) - if err != nil { - return nil, nil, err - } - - certBytes := pem.EncodeToMemory(&pem.Block{ - Type: "CERTIFICATE", - Bytes: certData, - }) - - return keyBytes, certBytes, nil -} - -func newMockReadFile(data map[string]([]byte)) ReadFileFunc { - return func(path string) ([]byte, error) { - bytes, ok := data[path] - if !ok { - return nil, os.ErrNotExist - } - return bytes, nil - } -} diff --git a/pkg/tsdb/grafana-postgresql-datasource/tlsmanager.go b/pkg/tsdb/grafana-postgresql-datasource/tlsmanager.go new file mode 100644 index 0000000000..116872d061 --- /dev/null +++ b/pkg/tsdb/grafana-postgresql-datasource/tlsmanager.go @@ -0,0 +1,249 @@ +package postgres + +import ( + "fmt" + "os" + "path/filepath" + "strconv" + "strings" + "sync" + "time" + + "github.com/grafana/grafana-plugin-sdk-go/backend/log" + "github.com/grafana/grafana/pkg/tsdb/sqleng" +) + +var validateCertFunc = validateCertFilePaths +var writeCertFileFunc = writeCertFile + +type certFileType int + +const ( + rootCert = iota + clientCert + clientKey +) + +type tlsSettingsProvider interface { + getTLSSettings(dsInfo sqleng.DataSourceInfo) (tlsSettings, error) +} + +type datasourceCacheManager struct { + locker *locker + cache sync.Map +} + +type tlsManager struct { + logger log.Logger + dsCacheInstance datasourceCacheManager + dataPath string +} + +func newTLSManager(logger log.Logger, dataPath string) tlsSettingsProvider { + return &tlsManager{ + logger: logger, + dataPath: dataPath, + dsCacheInstance: datasourceCacheManager{locker: newLocker()}, + } +} + +type tlsSettings struct { + Mode string + ConfigurationMethod string + RootCertFile string + CertFile string + CertKeyFile string +} + +func (m *tlsManager) getTLSSettings(dsInfo sqleng.DataSourceInfo) (tlsSettings, error) { + tlsconfig := tlsSettings{ + Mode: dsInfo.JsonData.Mode, + } + + isTLSDisabled := (tlsconfig.Mode == "disable") + + if isTLSDisabled { + m.logger.Debug("Postgres TLS/SSL is disabled") + return tlsconfig, nil + } + + m.logger.Debug("Postgres TLS/SSL is enabled", "tlsMode", tlsconfig.Mode) + + tlsconfig.ConfigurationMethod = dsInfo.JsonData.ConfigurationMethod + tlsconfig.RootCertFile = dsInfo.JsonData.RootCertFile + tlsconfig.CertFile = dsInfo.JsonData.CertFile + tlsconfig.CertKeyFile = dsInfo.JsonData.CertKeyFile + + if tlsconfig.ConfigurationMethod == "file-content" { + if err := m.writeCertFiles(dsInfo, &tlsconfig); err != nil { + return tlsconfig, err + } + } else { + if err := validateCertFunc(tlsconfig.RootCertFile, tlsconfig.CertFile, tlsconfig.CertKeyFile); err != nil { + return tlsconfig, err + } + } + return tlsconfig, nil +} + +func (t certFileType) String() string { + switch t { + case rootCert: + return "root certificate" + case clientCert: + return "client certificate" + case clientKey: + return "client key" + default: + panic(fmt.Sprintf("Unrecognized certFileType %d", t)) + } +} + +func getFileName(dataDir string, fileType certFileType) string { + var filename string + switch fileType { + case rootCert: + filename = "root.crt" + case clientCert: + filename = "client.crt" + case clientKey: + filename = "client.key" + default: + panic(fmt.Sprintf("unrecognized certFileType %s", fileType.String())) + } + generatedFilePath := filepath.Join(dataDir, filename) + return generatedFilePath +} + +// writeCertFile writes a certificate file. +func writeCertFile(logger log.Logger, fileContent string, generatedFilePath string) error { + fileContent = strings.TrimSpace(fileContent) + if fileContent != "" { + logger.Debug("Writing cert file", "path", generatedFilePath) + if err := os.WriteFile(generatedFilePath, []byte(fileContent), 0600); err != nil { + return err + } + // Make sure the file has the permissions expected by the Postgresql driver, otherwise it will bail + if err := os.Chmod(generatedFilePath, 0600); err != nil { + return err + } + return nil + } + + logger.Debug("Deleting cert file since no content is provided", "path", generatedFilePath) + exists, err := fileExists(generatedFilePath) + if err != nil { + return err + } + if exists { + if err := os.Remove(generatedFilePath); err != nil { + return fmt.Errorf("failed to remove %q: %w", generatedFilePath, err) + } + } + return nil +} + +func (m *tlsManager) writeCertFiles(dsInfo sqleng.DataSourceInfo, tlsconfig *tlsSettings) error { + m.logger.Debug("Writing TLS certificate files to disk") + tlsRootCert := dsInfo.DecryptedSecureJSONData["tlsCACert"] + tlsClientCert := dsInfo.DecryptedSecureJSONData["tlsClientCert"] + tlsClientKey := dsInfo.DecryptedSecureJSONData["tlsClientKey"] + if tlsRootCert == "" && tlsClientCert == "" && tlsClientKey == "" { + m.logger.Debug("No TLS/SSL certificates provided") + } + + // Calculate all files path + workDir := filepath.Join(m.dataPath, "tls", dsInfo.UID+"generatedTLSCerts") + tlsconfig.RootCertFile = getFileName(workDir, rootCert) + tlsconfig.CertFile = getFileName(workDir, clientCert) + tlsconfig.CertKeyFile = getFileName(workDir, clientKey) + + // Find datasource in the cache, if found, skip writing files + cacheKey := strconv.Itoa(int(dsInfo.ID)) + m.dsCacheInstance.locker.RLock(cacheKey) + item, ok := m.dsCacheInstance.cache.Load(cacheKey) + m.dsCacheInstance.locker.RUnlock(cacheKey) + if ok { + if !item.(time.Time).Before(dsInfo.Updated) { + return nil + } + } + + m.dsCacheInstance.locker.Lock(cacheKey) + defer m.dsCacheInstance.locker.Unlock(cacheKey) + + item, ok = m.dsCacheInstance.cache.Load(cacheKey) + if ok { + if !item.(time.Time).Before(dsInfo.Updated) { + return nil + } + } + + // Write certification directory and files + exists, err := fileExists(workDir) + if err != nil { + return err + } + if !exists { + if err := os.MkdirAll(workDir, 0700); err != nil { + return err + } + } + + if err = writeCertFileFunc(m.logger, tlsRootCert, tlsconfig.RootCertFile); err != nil { + return err + } + if err = writeCertFileFunc(m.logger, tlsClientCert, tlsconfig.CertFile); err != nil { + return err + } + if err = writeCertFileFunc(m.logger, tlsClientKey, tlsconfig.CertKeyFile); err != nil { + return err + } + + // we do not want to point to cert-files that do not exist + if tlsRootCert == "" { + tlsconfig.RootCertFile = "" + } + + if tlsClientCert == "" { + tlsconfig.CertFile = "" + } + + if tlsClientKey == "" { + tlsconfig.CertKeyFile = "" + } + + // Update datasource cache + m.dsCacheInstance.cache.Store(cacheKey, dsInfo.Updated) + return nil +} + +// validateCertFilePaths validates configured certificate file paths. +func validateCertFilePaths(rootCert, clientCert, clientKey string) error { + for _, fpath := range []string{rootCert, clientCert, clientKey} { + if fpath == "" { + continue + } + exists, err := fileExists(fpath) + if err != nil { + return err + } + if !exists { + return fmt.Errorf("certificate file %q doesn't exist", fpath) + } + } + return nil +} + +// Exists determines whether a file/directory exists or not. +func fileExists(fpath string) (bool, error) { + _, err := os.Stat(fpath) + if err != nil { + if !os.IsNotExist(err) { + return false, err + } + return false, nil + } + + return true, nil +} diff --git a/pkg/tsdb/grafana-postgresql-datasource/tlsmanager_test.go b/pkg/tsdb/grafana-postgresql-datasource/tlsmanager_test.go new file mode 100644 index 0000000000..8e60e841ca --- /dev/null +++ b/pkg/tsdb/grafana-postgresql-datasource/tlsmanager_test.go @@ -0,0 +1,332 @@ +package postgres + +import ( + "fmt" + "path/filepath" + "strconv" + "strings" + "sync" + "testing" + "time" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/backend/log" + "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/tsdb/sqleng" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + _ "github.com/lib/pq" +) + +var writeCertFileCallNum int + +// TestDataSourceCacheManager is to test the Cache manager +func TestDataSourceCacheManager(t *testing.T) { + cfg := setting.NewCfg() + cfg.DataPath = t.TempDir() + mng := tlsManager{ + logger: backend.NewLoggerWith("logger", "tsdb.postgres"), + dsCacheInstance: datasourceCacheManager{locker: newLocker()}, + dataPath: cfg.DataPath, + } + jsonData := sqleng.JsonData{ + Mode: "verify-full", + ConfigurationMethod: "file-content", + } + secureJSONData := map[string]string{ + "tlsClientCert": "I am client certification", + "tlsClientKey": "I am client key", + "tlsCACert": "I am CA certification", + } + + updateTime := time.Now().Add(-5 * time.Minute) + + mockValidateCertFilePaths() + t.Cleanup(resetValidateCertFilePaths) + + t.Run("Check datasource cache creation", func(t *testing.T) { + var wg sync.WaitGroup + wg.Add(10) + for id := int64(1); id <= 10; id++ { + go func(id int64) { + ds := sqleng.DataSourceInfo{ + ID: id, + Updated: updateTime, + Database: "database", + JsonData: jsonData, + DecryptedSecureJSONData: secureJSONData, + UID: "testData", + } + s := tlsSettings{} + err := mng.writeCertFiles(ds, &s) + require.NoError(t, err) + wg.Done() + }(id) + } + wg.Wait() + + t.Run("check cache creation is succeed", func(t *testing.T) { + for id := int64(1); id <= 10; id++ { + updated, ok := mng.dsCacheInstance.cache.Load(strconv.Itoa(int(id))) + require.True(t, ok) + require.Equal(t, updateTime, updated) + } + }) + }) + + t.Run("Check datasource cache modification", func(t *testing.T) { + t.Run("check when version not changed, cache and files are not updated", func(t *testing.T) { + mockWriteCertFile() + t.Cleanup(resetWriteCertFile) + var wg1 sync.WaitGroup + wg1.Add(5) + for id := int64(1); id <= 5; id++ { + go func(id int64) { + ds := sqleng.DataSourceInfo{ + ID: 1, + Updated: updateTime, + Database: "database", + JsonData: jsonData, + DecryptedSecureJSONData: secureJSONData, + UID: "testData", + } + s := tlsSettings{} + err := mng.writeCertFiles(ds, &s) + require.NoError(t, err) + wg1.Done() + }(id) + } + wg1.Wait() + assert.Equal(t, writeCertFileCallNum, 0) + }) + + t.Run("cache is updated with the last datasource version", func(t *testing.T) { + dsV2 := sqleng.DataSourceInfo{ + ID: 1, + Updated: updateTime.Add(time.Minute), + Database: "database", + JsonData: jsonData, + DecryptedSecureJSONData: secureJSONData, + UID: "testData", + } + dsV3 := sqleng.DataSourceInfo{ + ID: 1, + Updated: updateTime.Add(2 * time.Minute), + Database: "database", + JsonData: jsonData, + DecryptedSecureJSONData: secureJSONData, + UID: "testData", + } + s := tlsSettings{} + err := mng.writeCertFiles(dsV2, &s) + require.NoError(t, err) + err = mng.writeCertFiles(dsV3, &s) + require.NoError(t, err) + version, ok := mng.dsCacheInstance.cache.Load("1") + require.True(t, ok) + require.Equal(t, updateTime.Add(2*time.Minute), version) + }) + }) +} + +// Test getFileName + +func TestGetFileName(t *testing.T) { + testCases := []struct { + desc string + datadir string + fileType certFileType + expErr string + expectedGeneratedPath string + }{ + { + desc: "Get File Name for root certification", + datadir: ".", + fileType: rootCert, + expectedGeneratedPath: "root.crt", + }, + { + desc: "Get File Name for client certification", + datadir: ".", + fileType: clientCert, + expectedGeneratedPath: "client.crt", + }, + { + desc: "Get File Name for client certification", + datadir: ".", + fileType: clientKey, + expectedGeneratedPath: "client.key", + }, + } + for _, tt := range testCases { + t.Run(tt.desc, func(t *testing.T) { + generatedPath := getFileName(tt.datadir, tt.fileType) + assert.Equal(t, tt.expectedGeneratedPath, generatedPath) + }) + } +} + +// Test getTLSSettings. +func TestGetTLSSettings(t *testing.T) { + cfg := setting.NewCfg() + cfg.DataPath = t.TempDir() + + mockValidateCertFilePaths() + t.Cleanup(resetValidateCertFilePaths) + + updatedTime := time.Now() + + testCases := []struct { + desc string + expErr string + jsonData sqleng.JsonData + secureJSONData map[string]string + uid string + tlsSettings tlsSettings + updated time.Time + }{ + { + desc: "Custom TLS authentication disabled", + updated: updatedTime, + jsonData: sqleng.JsonData{ + Mode: "disable", + RootCertFile: "i/am/coding/ca.crt", + CertFile: "i/am/coding/client.crt", + CertKeyFile: "i/am/coding/client.key", + ConfigurationMethod: "file-path", + }, + tlsSettings: tlsSettings{Mode: "disable"}, + }, + { + desc: "Custom TLS authentication with file path", + updated: updatedTime.Add(time.Minute), + jsonData: sqleng.JsonData{ + Mode: "verify-full", + ConfigurationMethod: "file-path", + RootCertFile: "i/am/coding/ca.crt", + CertFile: "i/am/coding/client.crt", + CertKeyFile: "i/am/coding/client.key", + }, + tlsSettings: tlsSettings{ + Mode: "verify-full", + ConfigurationMethod: "file-path", + RootCertFile: "i/am/coding/ca.crt", + CertFile: "i/am/coding/client.crt", + CertKeyFile: "i/am/coding/client.key", + }, + }, + { + desc: "Custom TLS mode verify-full with certificate files content", + updated: updatedTime.Add(2 * time.Minute), + uid: "xxx", + jsonData: sqleng.JsonData{ + Mode: "verify-full", + ConfigurationMethod: "file-content", + }, + secureJSONData: map[string]string{ + "tlsCACert": "I am CA certification", + "tlsClientCert": "I am client certification", + "tlsClientKey": "I am client key", + }, + tlsSettings: tlsSettings{ + Mode: "verify-full", + ConfigurationMethod: "file-content", + RootCertFile: filepath.Join(cfg.DataPath, "tls", "xxxgeneratedTLSCerts", "root.crt"), + CertFile: filepath.Join(cfg.DataPath, "tls", "xxxgeneratedTLSCerts", "client.crt"), + CertKeyFile: filepath.Join(cfg.DataPath, "tls", "xxxgeneratedTLSCerts", "client.key"), + }, + }, + { + desc: "Custom TLS mode verify-ca with no client certificates with certificate files content", + updated: updatedTime.Add(3 * time.Minute), + uid: "xxx", + jsonData: sqleng.JsonData{ + Mode: "verify-ca", + ConfigurationMethod: "file-content", + }, + secureJSONData: map[string]string{ + "tlsCACert": "I am CA certification", + }, + tlsSettings: tlsSettings{ + Mode: "verify-ca", + ConfigurationMethod: "file-content", + RootCertFile: filepath.Join(cfg.DataPath, "tls", "xxxgeneratedTLSCerts", "root.crt"), + CertFile: "", + CertKeyFile: "", + }, + }, + { + desc: "Custom TLS mode require with client certificates and no root certificate with certificate files content", + updated: updatedTime.Add(4 * time.Minute), + uid: "xxx", + jsonData: sqleng.JsonData{ + Mode: "require", + ConfigurationMethod: "file-content", + }, + secureJSONData: map[string]string{ + "tlsClientCert": "I am client certification", + "tlsClientKey": "I am client key", + }, + tlsSettings: tlsSettings{ + Mode: "require", + ConfigurationMethod: "file-content", + RootCertFile: "", + CertFile: filepath.Join(cfg.DataPath, "tls", "xxxgeneratedTLSCerts", "client.crt"), + CertKeyFile: filepath.Join(cfg.DataPath, "tls", "xxxgeneratedTLSCerts", "client.key"), + }, + }, + } + for _, tt := range testCases { + t.Run(tt.desc, func(t *testing.T) { + var settings tlsSettings + var err error + mng := tlsManager{ + logger: backend.NewLoggerWith("logger", "tsdb.postgres"), + dsCacheInstance: datasourceCacheManager{locker: newLocker()}, + dataPath: cfg.DataPath, + } + + ds := sqleng.DataSourceInfo{ + JsonData: tt.jsonData, + DecryptedSecureJSONData: tt.secureJSONData, + UID: tt.uid, + Updated: tt.updated, + } + + settings, err = mng.getTLSSettings(ds) + + if tt.expErr == "" { + require.NoError(t, err, tt.desc) + assert.Equal(t, tt.tlsSettings, settings) + } else { + require.Error(t, err, tt.desc) + assert.True(t, strings.HasPrefix(err.Error(), tt.expErr), + fmt.Sprintf("%s: %q doesn't start with %q", tt.desc, err, tt.expErr)) + } + }) + } +} + +func mockValidateCertFilePaths() { + validateCertFunc = func(rootCert, clientCert, clientKey string) error { + return nil + } +} + +func resetValidateCertFilePaths() { + validateCertFunc = validateCertFilePaths +} + +func mockWriteCertFile() { + writeCertFileCallNum = 0 + writeCertFileFunc = func(logger log.Logger, fileContent string, generatedFilePath string) error { + writeCertFileCallNum++ + return nil + } +} + +func resetWriteCertFile() { + writeCertFileCallNum = 0 + writeCertFileFunc = writeCertFile +} From 21affd3b0eaf1eb6f0ceb8012e32f336b4cee267 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Fri, 1 Mar 2024 12:24:19 +0000 Subject: [PATCH 0332/1406] Library panels: Ensure all filters are visible on mobile (#83759) * convert styles to emotion object syntax * allow items to wrap --- .betterer.results | 11 -- .../LibraryPanelsSearch.tsx | 132 +++++++++--------- 2 files changed, 65 insertions(+), 78 deletions(-) diff --git a/.betterer.results b/.betterer.results index 9abae8a3a3..48f308f57b 100644 --- a/.betterer.results +++ b/.betterer.results @@ -3668,17 +3668,6 @@ exports[`better eslint`] = { [0, 0, 0, "Styles should be written using objects.", "1"], [0, 0, 0, "Styles should be written using objects.", "2"] ], - "public/app/features/library-panels/components/LibraryPanelsSearch/LibraryPanelsSearch.tsx:5381": [ - [0, 0, 0, "Styles should be written using objects.", "0"], - [0, 0, 0, "Styles should be written using objects.", "1"], - [0, 0, 0, "Styles should be written using objects.", "2"], - [0, 0, 0, "Styles should be written using objects.", "3"], - [0, 0, 0, "Styles should be written using objects.", "4"], - [0, 0, 0, "Styles should be written using objects.", "5"], - [0, 0, 0, "Styles should be written using objects.", "6"], - [0, 0, 0, "Styles should be written using objects.", "7"], - [0, 0, 0, "Styles should be written using objects.", "8"] - ], "public/app/features/library-panels/components/LibraryPanelsView/actions.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], diff --git a/public/app/features/library-panels/components/LibraryPanelsSearch/LibraryPanelsSearch.tsx b/public/app/features/library-panels/components/LibraryPanelsSearch/LibraryPanelsSearch.tsx index 104e5feda1..7c015f60b6 100644 --- a/public/app/features/library-panels/components/LibraryPanelsSearch/LibraryPanelsSearch.tsx +++ b/public/app/features/library-panels/components/LibraryPanelsSearch/LibraryPanelsSearch.tsx @@ -1,4 +1,4 @@ -import { css } from '@emotion/css'; +import { css, cx } from '@emotion/css'; import React, { useCallback, useState } from 'react'; import { useDebounce } from 'react-use'; @@ -57,7 +57,11 @@ export const LibraryPanelsSearch = ({ return (
-
+
{ - const styles = useStyles2(getRowStyles, variant); + const styles = useStyles2(getRowStyles); const panelFilterChanged = useCallback( (plugins: PanelPluginMeta[]) => onPanelFilterChange(plugins.map((p) => p.id)), [onPanelFilterChange] @@ -160,10 +163,18 @@ const SearchControls = React.memo( ); return ( -
+
{showSort && } {(showFolderFilter || showPanelFilter) && ( -
+
{showFolderFilter && } {showPanelFilter && }
@@ -174,42 +185,29 @@ const SearchControls = React.memo( ); SearchControls.displayName = 'SearchControls'; -function getRowStyles(theme: GrafanaTheme2, variant = LibraryPanelsSearchVariant.Spacious) { - const searchRowContainer = css` - display: flex; - gap: ${theme.spacing(1)}; - flex-grow: 1; - flex-direction: row; - justify-content: end; - `; - const searchRowContainerTight = css` - ${searchRowContainer}; - flex-grow: initial; - flex-direction: column; - justify-content: normal; - `; - const filterContainer = css` - display: flex; - flex-direction: row; - margin-left: auto; - gap: 4px; - `; - const filterContainerTight = css` - ${filterContainer}; - flex-direction: column; - margin-left: initial; - `; - - switch (variant) { - case LibraryPanelsSearchVariant.Spacious: - return { - container: searchRowContainer, - filterContainer: filterContainer, - }; - case LibraryPanelsSearchVariant.Tight: - return { - container: searchRowContainerTight, - filterContainer: filterContainerTight, - }; - } +function getRowStyles(theme: GrafanaTheme2) { + return { + container: css({ + display: 'flex', + gap: theme.spacing(1), + flexGrow: 1, + flexDirection: 'row', + justifyContent: 'space-between', + flexWrap: 'wrap', + }), + containerTight: css({ + flexGrow: 'initial', + flexDirection: 'column', + justifyContent: 'normal', + }), + filterContainer: css({ + display: 'flex', + flexDirection: 'row', + gap: theme.spacing(1), + }), + filterContainerTight: css({ + flexDirection: 'column', + marginLeft: 'initial', + }), + }; } From 371aced092f53bf7082f62ab13b4c04b68b0d6c3 Mon Sep 17 00:00:00 2001 From: Misi Date: Fri, 1 Mar 2024 13:49:06 +0100 Subject: [PATCH 0333/1406] Chore: Add enabled status to authentication_ui_provider_clicked (#83766) Add enabled metadata to authentication_ui_provider_clicked --- public/app/features/auth-config/AuthProvidersListPage.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/public/app/features/auth-config/AuthProvidersListPage.tsx b/public/app/features/auth-config/AuthProvidersListPage.tsx index 07a2e54fbb..b3a8a344fc 100644 --- a/public/app/features/auth-config/AuthProvidersListPage.tsx +++ b/public/app/features/auth-config/AuthProvidersListPage.tsx @@ -43,8 +43,8 @@ export const AuthConfigPageUnconnected = ({ const authProviders = getRegisteredAuthProviders(); const availableProviders = authProviders.filter((p) => !providerStatuses[p.id]?.hide); - const onProviderCardClick = (providerType: string) => { - reportInteraction('authentication_ui_provider_clicked', { provider: providerType }); + const onProviderCardClick = (providerType: string, enabled: boolean) => { + reportInteraction('authentication_ui_provider_clicked', { provider: providerType, enabled }); }; const providerList = availableProviders.length @@ -86,7 +86,7 @@ export const AuthConfigPageUnconnected = ({ authType={settings.type || 'OAuth'} providerId={provider} enabled={settings.enabled} - onClick={() => onProviderCardClick(provider)} + onClick={() => onProviderCardClick(provider, settings.enabled)} //@ts-expect-error Remove legacy types configPath={settings.configPath} /> From dbe621eeb4f35ffc4824be4d094229952b065602 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ida=20=C5=A0tambuk?= Date: Fri, 1 Mar 2024 13:52:45 +0100 Subject: [PATCH 0334/1406] Cloudwatch: Bump grafana/aws-sdk-go to 0.24.0 (#83480) --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 061044a1a8..cfd48e8433 100644 --- a/go.mod +++ b/go.mod @@ -61,7 +61,7 @@ require ( github.com/gorilla/websocket v1.5.0 // @grafana/grafana-app-platform-squad github.com/grafana/alerting v0.0.0-20240222104113-abfafef9a7d2 // @grafana/alerting-squad-backend github.com/grafana/cuetsy v0.1.11 // @grafana/grafana-as-code - github.com/grafana/grafana-aws-sdk v0.23.1 // @grafana/aws-datasources + github.com/grafana/grafana-aws-sdk v0.24.0 // @grafana/aws-datasources github.com/grafana/grafana-azure-sdk-go v1.12.0 // @grafana/partner-datasources github.com/grafana/grafana-plugin-sdk-go v0.212.0 // @grafana/plugins-platform-backend github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // @grafana/backend-platform diff --git a/go.sum b/go.sum index a6aa605d07..de4793c3d1 100644 --- a/go.sum +++ b/go.sum @@ -2179,6 +2179,8 @@ github.com/grafana/gofpdf v0.0.0-20231002120153-857cc45be447 h1:jxJJ5z0GxqhWFbQU github.com/grafana/gofpdf v0.0.0-20231002120153-857cc45be447/go.mod h1:IxsY6mns6Q5sAnWcrptrgUrSglTZJXH/kXr9nbpb/9I= github.com/grafana/grafana-aws-sdk v0.23.1 h1:YP6DqzB36fp8fXno0r+X9BxNB3apNfJnQxu8tdhYMH8= github.com/grafana/grafana-aws-sdk v0.23.1/go.mod h1:iTbW395xv26qy6L17SjtZlVwxQTIZbmupBTe0sPHv7k= +github.com/grafana/grafana-aws-sdk v0.24.0 h1:0RKCJTeIkpEUvLCTjGOK1+jYZpaE2nJaGghGLvtUsFs= +github.com/grafana/grafana-aws-sdk v0.24.0/go.mod h1:3zghFF6edrxn0d6k6X9HpGZXDH+VfA+MwD2Pc/9X0ec= github.com/grafana/grafana-azure-sdk-go v1.12.0 h1:q71M2QxMlBqRZOXc5mFAycJWuZqQ3hPTzVEo1r3CUTY= github.com/grafana/grafana-azure-sdk-go v1.12.0/go.mod h1:SAlwLdEuox4vw8ZaeQwnepYXnhznnQQdstJbcw8LH68= github.com/grafana/grafana-google-sdk-go v0.1.0 h1:LKGY8z2DSxKjYfr2flZsWgTRTZ6HGQbTqewE3JvRaNA= From 10a55560dfc09a63aa4a7ffe8bc00618abc1c935 Mon Sep 17 00:00:00 2001 From: kay delaney <45561153+kaydelaney@users.noreply.github.com> Date: Fri, 1 Mar 2024 13:25:15 +0000 Subject: [PATCH 0335/1406] Scenes: Add support for repeated library panels (#83579) --- .betterer.results | 3 +- .../scene/LibraryVizPanel.test.ts | 136 ++++++++++++++++++ .../dashboard-scene/scene/LibraryVizPanel.tsx | 42 +++++- .../scene/PanelRepeaterGridItem.tsx | 8 +- .../transformSceneToSaveModel.ts | 23 ++- .../sharing/public-dashboards/utils.ts | 2 +- 6 files changed, 204 insertions(+), 10 deletions(-) create mode 100644 public/app/features/dashboard-scene/scene/LibraryVizPanel.test.ts diff --git a/.betterer.results b/.betterer.results index 48f308f57b..d89b65b7bb 100644 --- a/.betterer.results +++ b/.betterer.results @@ -2573,7 +2573,8 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "3"], [0, 0, 0, "Do not use any type assertions.", "4"], [0, 0, 0, "Do not use any type assertions.", "5"], - [0, 0, 0, "Do not use any type assertions.", "6"] + [0, 0, 0, "Do not use any type assertions.", "6"], + [0, 0, 0, "Do not use any type assertions.", "7"] ], "public/app/features/dashboard-scene/settings/variables/components/VariableSelectField.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] diff --git a/public/app/features/dashboard-scene/scene/LibraryVizPanel.test.ts b/public/app/features/dashboard-scene/scene/LibraryVizPanel.test.ts new file mode 100644 index 0000000000..39968af070 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/LibraryVizPanel.test.ts @@ -0,0 +1,136 @@ +import 'whatwg-fetch'; +import { waitFor } from '@testing-library/dom'; +import { merge } from 'lodash'; +import { http, HttpResponse } from 'msw'; +import { setupServer, SetupServerApi } from 'msw/node'; + +import { setBackendSrv } from '@grafana/runtime'; +import { SceneGridItem, SceneGridLayout, VizPanel } from '@grafana/scenes'; +import { LibraryPanel } from '@grafana/schema'; +import { backendSrv } from 'app/core/services/backend_srv'; + +import { LibraryVizPanel } from './LibraryVizPanel'; +import { PanelRepeaterGridItem } from './PanelRepeaterGridItem'; + +describe('LibraryVizPanel', () => { + const server = setupServer(); + + beforeAll(() => { + setBackendSrv(backendSrv); + server.listen(); + }); + + afterAll(() => { + server.close(); + }); + + beforeEach(() => { + server.resetHandlers(); + }); + + it('should fetch and init', async () => { + setUpApiMock(server); + const libVizPanel = new LibraryVizPanel({ + name: 'My Library Panel', + title: 'Panel title', + uid: 'fdcvggvfy2qdca', + panelKey: 'lib-panel', + }); + libVizPanel.activate(); + await waitFor(() => { + expect(libVizPanel.state.panel).toBeInstanceOf(VizPanel); + }); + }); + + it('should change parent from SceneGridItem to PanelRepeaterGridItem if repeat is set', async () => { + setUpApiMock(server, { model: { repeat: 'query0', repeatDirection: 'h' } }); + const libVizPanel = new LibraryVizPanel({ + name: 'My Library Panel', + title: 'Panel title', + uid: 'fdcvggvfy2qdca', + panelKey: 'lib-panel', + }); + + const layout = new SceneGridLayout({ + children: [new SceneGridItem({ body: libVizPanel })], + }); + layout.activate(); + libVizPanel.activate(); + await waitFor(() => { + expect(layout.state.children[0]).toBeInstanceOf(PanelRepeaterGridItem); + }); + }); +}); + +function setUpApiMock( + server: SetupServerApi, + overrides: Omit, 'model'> & { model?: Partial } = {} +) { + const libPanel: LibraryPanel = merge( + { + folderUid: 'general', + uid: 'fdcvggvfy2qdca', + name: 'My Library Panel', + type: 'timeseries', + description: '', + model: { + datasource: { + type: 'grafana-testdata-datasource', + uid: 'PD8C576611E62080A', + }, + description: '', + + maxPerRow: 4, + options: { + legend: { + calcs: [], + displayMode: 'list', + placement: 'bottom', + showLegend: true, + }, + tooltip: { + maxHeight: 600, + mode: 'single', + sort: 'none', + }, + }, + targets: [ + { + datasource: { + type: 'grafana-testdata-datasource', + uid: 'PD8C576611E62080A', + }, + refId: 'A', + }, + ], + title: 'Panel Title', + type: 'timeseries', + }, + version: 6, + meta: { + folderName: 'General', + folderUid: '', + connectedDashboards: 1, + created: '2024-02-15T15:26:46Z', + updated: '2024-02-28T15:54:22Z', + createdBy: { + avatarUrl: '/avatar/46d229b033af06a191ff2267bca9ae56', + id: 1, + name: 'admin', + }, + updatedBy: { + avatarUrl: '/avatar/46d229b033af06a191ff2267bca9ae56', + id: 1, + name: 'admin', + }, + }, + }, + overrides + ); + + const libPanelMock: { result: LibraryPanel } = { + result: libPanel, + }; + + server.use(http.get('/api/library-elements/:uid', () => HttpResponse.json(libPanelMock))); +} diff --git a/public/app/features/dashboard-scene/scene/LibraryVizPanel.tsx b/public/app/features/dashboard-scene/scene/LibraryVizPanel.tsx index b1c2be5d6f..379a5ba1b4 100644 --- a/public/app/features/dashboard-scene/scene/LibraryVizPanel.tsx +++ b/public/app/features/dashboard-scene/scene/LibraryVizPanel.tsx @@ -1,6 +1,15 @@ import React from 'react'; -import { SceneComponentProps, SceneObjectBase, SceneObjectState, VizPanel, VizPanelMenu } from '@grafana/scenes'; +import { + SceneComponentProps, + SceneGridItem, + SceneGridLayout, + SceneObjectBase, + SceneObjectState, + VizPanel, + VizPanelMenu, + VizPanelState, +} from '@grafana/scenes'; import { PanelModel } from 'app/features/dashboard/state'; import { getLibraryPanel } from 'app/features/library-panels/state/api'; @@ -9,6 +18,7 @@ import { createPanelDataProvider } from '../utils/createPanelDataProvider'; import { VizPanelLinks, VizPanelLinksMenu } from './PanelLinks'; import { panelLinksBehavior, panelMenuBehavior } from './PanelMenuBehavior'; import { PanelNotices } from './PanelNotices'; +import { PanelRepeaterGridItem } from './PanelRepeaterGridItem'; interface LibraryVizPanelState extends SceneObjectState { // Library panels use title from dashboard JSON's panel model, not from library panel definition, hence we pass it. @@ -52,7 +62,7 @@ export class LibraryVizPanel extends SceneObjectBase { const libPanelModel = new PanelModel(libPanel.model); - const panel = new VizPanel({ + const vizPanelState: VizPanelState = { title: this.state.title, key: this.state.panelKey, options: libPanelModel.options ?? {}, @@ -70,12 +80,36 @@ export class LibraryVizPanel extends SceneObjectBase { }), new PanelNotices(), ], - }); + }; + + const panel = new VizPanel(vizPanelState); + const gridItem = this.parent; + if (libPanelModel.repeat && gridItem instanceof SceneGridItem && gridItem.parent instanceof SceneGridLayout) { + this._parent = undefined; + const repeater = new PanelRepeaterGridItem({ + key: gridItem.state.key, + x: libPanelModel.gridPos.x, + y: libPanelModel.gridPos.y, + width: libPanelModel.repeatDirection === 'h' ? 24 : libPanelModel.gridPos.w, + height: libPanelModel.gridPos.h, + itemHeight: libPanelModel.gridPos.h, + source: this, + variableName: libPanelModel.repeat, + repeatedPanels: [], + repeatDirection: libPanelModel.repeatDirection === 'h' ? 'h' : 'v', + maxPerRow: libPanelModel.maxPerRow, + }); + gridItem.parent.setState({ + children: gridItem.parent.state.children.map((child) => + child.state.key === gridItem.state.key ? repeater : child + ), + }); + } this.setState({ panel, _loadedVersion: libPanel.version, isLoaded: true }); } catch (err) { vizPanel.setState({ - _pluginLoadError: 'Unable to load library panel: ' + this.state.uid, + _pluginLoadError: `Unable to load library panel: ${this.state.uid}`, }); } } diff --git a/public/app/features/dashboard-scene/scene/PanelRepeaterGridItem.tsx b/public/app/features/dashboard-scene/scene/PanelRepeaterGridItem.tsx index ccaa157360..631ecda727 100644 --- a/public/app/features/dashboard-scene/scene/PanelRepeaterGridItem.tsx +++ b/public/app/features/dashboard-scene/scene/PanelRepeaterGridItem.tsx @@ -19,10 +19,11 @@ import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN } from 'app/core/constants'; import { getMultiVariableValues } from '../utils/utils'; +import { LibraryVizPanel } from './LibraryVizPanel'; import { DashboardRepeatsProcessedEvent } from './types'; interface PanelRepeaterGridItemState extends SceneGridItemStateLike { - source: VizPanel; + source: VizPanel | LibraryVizPanel; repeatedPanels?: VizPanel[]; variableName: string; itemHeight?: number; @@ -94,11 +95,12 @@ export class PanelRepeaterGridItem extends SceneObjectBase Date: Fri, 1 Mar 2024 14:54:39 +0000 Subject: [PATCH 0336/1406] Chore: replace `react-popper` with `floating-ui` in `ExemplarMarker` (#83694) * replace react-popper with floating-ui in ExemplarMarker * fix e2e test * floating-ui uses mousemove --- e2e/various-suite/exemplars.spec.ts | 2 +- package.json | 1 - packages/grafana-ui/package.json | 1 - .../timeseries/plugins/ExemplarMarker.tsx | 102 +++++++++--------- yarn.lock | 4 +- 5 files changed, 50 insertions(+), 60 deletions(-) diff --git a/e2e/various-suite/exemplars.spec.ts b/e2e/various-suite/exemplars.spec.ts index 0a7981b03a..60f5838652 100644 --- a/e2e/various-suite/exemplars.spec.ts +++ b/e2e/various-suite/exemplars.spec.ts @@ -69,7 +69,7 @@ describe('Exemplars', () => { cy.get(`[data-testid="time-series-zoom-to-data"]`).click(); - e2e.components.DataSource.Prometheus.exemplarMarker().first().trigger('mouseover'); + e2e.components.DataSource.Prometheus.exemplarMarker().first().trigger('mousemove'); cy.contains('Query with gdev-tempo').click(); e2e.components.TraceViewer.spanBar().should('have.length', 11); }); diff --git a/package.json b/package.json index 9182e8a426..1ac1116a48 100644 --- a/package.json +++ b/package.json @@ -376,7 +376,6 @@ "react-inlinesvg": "3.0.2", "react-loading-skeleton": "3.4.0", "react-moveable": "0.56.0", - "react-popper": "2.3.0", "react-redux": "8.1.3", "react-resizable": "3.0.5", "react-responsive-carousel": "^3.2.23", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 69436c7d08..b9bbc0c10c 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -93,7 +93,6 @@ "react-i18next": "^12.0.0", "react-inlinesvg": "3.0.2", "react-loading-skeleton": "3.4.0", - "react-popper": "2.3.0", "react-router-dom": "5.3.3", "react-select": "5.8.0", "react-table": "7.8.0", diff --git a/public/app/plugins/panel/timeseries/plugins/ExemplarMarker.tsx b/public/app/plugins/panel/timeseries/plugins/ExemplarMarker.tsx index 6c59f0ad1e..e1f7780896 100644 --- a/public/app/plugins/panel/timeseries/plugins/ExemplarMarker.tsx +++ b/public/app/plugins/panel/timeseries/plugins/ExemplarMarker.tsx @@ -1,6 +1,15 @@ import { css, cx } from '@emotion/css'; -import React, { CSSProperties, useCallback, useEffect, useRef, useState } from 'react'; -import { usePopper } from 'react-popper'; +import { + autoUpdate, + flip, + safePolygon, + shift, + useDismiss, + useFloating, + useHover, + useInteractions, +} from '@floating-ui/react'; +import React, { CSSProperties, useCallback, useEffect, useState } from 'react'; import { DataFrame, @@ -44,25 +53,34 @@ export const ExemplarMarker = ({ const styles = useStyles2(getExemplarMarkerStyles); const [isOpen, setIsOpen] = useState(false); const [isLocked, setIsLocked] = useState(false); - const [markerElement, setMarkerElement] = React.useState(null); - const [popperElement, setPopperElement] = React.useState(null); - const { styles: popperStyles, attributes } = usePopper(markerElement, popperElement, { - modifiers: [ - { - name: 'preventOverflow', - options: { - altAxis: true, - }, - }, - { - name: 'flip', - options: { - fallbackPlacements: ['top', 'left-start'], - }, - }, - ], + + // the order of middleware is important! + const middleware = [ + flip({ + fallbackAxisSideDirection: 'end', + // see https://floating-ui.com/docs/flip#combining-with-shift + crossAxis: false, + boundary: document.body, + }), + shift(), + ]; + + const { context, refs, floatingStyles } = useFloating({ + open: isOpen, + placement: 'bottom', + onOpenChange: setIsOpen, + middleware, + whileElementsMounted: autoUpdate, + strategy: 'fixed', + }); + + const dismiss = useDismiss(context); + const hover = useHover(context, { + handleClose: safePolygon(), + enabled: clickedExemplarFieldIndex === undefined, }); - const popoverRenderTimeout = useRef(); + + const { getReferenceProps, getFloatingProps } = useInteractions([dismiss, hover]); useEffect(() => { if ( @@ -107,25 +125,10 @@ export const ExemplarMarker = ({ return symbols[dataFrameFieldIndex.frameIndex % symbols.length]; }; - const onMouseEnter = useCallback(() => { - if (clickedExemplarFieldIndex === undefined) { - if (popoverRenderTimeout.current) { - clearTimeout(popoverRenderTimeout.current); - } - setIsOpen(true); - } - }, [setIsOpen, clickedExemplarFieldIndex]); - const lockExemplarModal = () => { setIsLocked(true); }; - const onMouseLeave = useCallback(() => { - popoverRenderTimeout.current = setTimeout(() => { - setIsOpen(false); - }, 150); - }, [setIsOpen]); - const renderMarker = useCallback(() => { //Put fields with links on the top const fieldsWithLinks = @@ -220,28 +223,20 @@ export const ExemplarMarker = ({ }; return ( -
+
{getExemplarMarkerContent()}
); }, [ - attributes.popper, dataFrame.fields, dataFrameFieldIndex, - onMouseEnter, - onMouseLeave, - popperStyles.popper, styles, timeZone, isLocked, setClickedExemplarFieldIndex, + floatingStyles, + getFloatingProps, + refs.setFloating, ]); const seriesColor = config @@ -256,19 +251,18 @@ export const ExemplarMarker = ({ return ( <>
{ if (e.key === 'Enter') { onExemplarClick(); } }} - onMouseEnter={onMouseEnter} - onMouseLeave={onMouseLeave} - className={styles.markerWrapper} - data-testid={selectors.components.DataSource.Prometheus.exemplarMarker} - role="button" - tabIndex={0} > Date: Fri, 1 Mar 2024 09:48:20 -0600 Subject: [PATCH 0337/1406] CloudWatch: de-duplicate implementation of AWSDatasourceSettings.Load in LoadCloudWatchSettings (#83556) --- pkg/tsdb/cloudwatch/models/settings.go | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/pkg/tsdb/cloudwatch/models/settings.go b/pkg/tsdb/cloudwatch/models/settings.go index ec35f7211c..9221fe42fb 100644 --- a/pkg/tsdb/cloudwatch/models/settings.go +++ b/pkg/tsdb/cloudwatch/models/settings.go @@ -25,18 +25,16 @@ type CloudWatchSettings struct { func LoadCloudWatchSettings(ctx context.Context, config backend.DataSourceInstanceSettings) (CloudWatchSettings, error) { instance := CloudWatchSettings{} + if config.JSONData != nil && len(config.JSONData) > 1 { if err := json.Unmarshal(config.JSONData, &instance); err != nil { return CloudWatchSettings{}, fmt.Errorf("could not unmarshal DatasourceSettings json: %w", err) } } - if instance.Region == "default" || instance.Region == "" { - instance.Region = instance.DefaultRegion - } - - if instance.Profile == "" { - instance.Profile = config.Database + // load the instance using the loader for the wrapped awsds.AWSDatasourceSettings + if err := instance.Load(config); err != nil { + return CloudWatchSettings{}, err } // logs timeout default is 30 minutes, the same as timeout in frontend logs query @@ -45,8 +43,6 @@ func LoadCloudWatchSettings(ctx context.Context, config backend.DataSourceInstan instance.LogsTimeout = Duration{30 * time.Minute} } - instance.AccessKey = config.DecryptedSecureJSONData["accessKey"] - instance.SecretKey = config.DecryptedSecureJSONData["secretKey"] instance.GrafanaSettings = *awsds.ReadAuthSettings(ctx) return instance, nil From 068e6f6b94df101c0d75df2588a70ae28a2e81cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ida=20=C5=A0tambuk?= Date: Fri, 1 Mar 2024 16:56:45 +0100 Subject: [PATCH 0338/1406] Cloudwatch: Fix new ConfigEditor to add the custom namespace field (#83762) --- .../components/ConfigEditor/ConfigEditor.test.tsx | 5 +++++ .../components/ConfigEditor/ConfigEditor.tsx | 10 +++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/cloudwatch/components/ConfigEditor/ConfigEditor.test.tsx b/public/app/plugins/datasource/cloudwatch/components/ConfigEditor/ConfigEditor.test.tsx index 47cf04e059..a83e5d70d5 100644 --- a/public/app/plugins/datasource/cloudwatch/components/ConfigEditor/ConfigEditor.test.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/ConfigEditor/ConfigEditor.test.tsx @@ -216,6 +216,11 @@ describe('Render', () => { await waitFor(async () => expect(screen.getByText('Assume Role ARN')).toBeInTheDocument()); }); + it('should display namespace field', async () => { + setup(); + await waitFor(async () => expect(screen.getByText('Namespaces of Custom Metrics')).toBeInTheDocument()); + }); + it('should show a deprecation warning if `arn` auth type is used', async () => { setup({ jsonData: { diff --git a/public/app/plugins/datasource/cloudwatch/components/ConfigEditor/ConfigEditor.tsx b/public/app/plugins/datasource/cloudwatch/components/ConfigEditor/ConfigEditor.tsx index c9965ee202..f6b551955d 100644 --- a/public/app/plugins/datasource/cloudwatch/components/ConfigEditor/ConfigEditor.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/ConfigEditor/ConfigEditor.tsx @@ -107,7 +107,15 @@ export const ConfigEditor = (props: Props) => { }) } externalId={externalId} - /> + > + + + + {config.secureSocksDSProxyEnabled && ( )} From 0dfdb2ae47383beb41aa981313b4f12b4b29a8fa Mon Sep 17 00:00:00 2001 From: owensmallwood Date: Fri, 1 Mar 2024 10:16:43 -0600 Subject: [PATCH 0339/1406] Revert "Add FolderUID for library elements" (#83776) Revert "Add FolderUID for library elements (#79572)" This reverts commit 2532047e7a6f2bd4ac9b7d7ae3d1b2aadcfb3bd6. --- pkg/api/dashboard_test.go | 2 +- .../dashboardimport/service/service.go | 2 +- .../dashboardimport/service/service_test.go | 19 +++--- pkg/services/folder/folderimpl/folder_test.go | 6 -- pkg/services/libraryelements/database.go | 20 ++---- .../libraryelements_create_test.go | 15 ++-- .../libraryelements_delete_test.go | 2 +- .../libraryelements_get_all_test.go | 67 +++++++----------- .../libraryelements_get_test.go | 4 +- .../libraryelements_patch_test.go | 68 ++++++++----------- .../libraryelements_permissions_test.go | 37 +++++----- .../libraryelements/libraryelements_test.go | 30 ++++---- pkg/services/libraryelements/model/model.go | 18 +++-- pkg/services/libraryelements/writers.go | 2 - pkg/services/librarypanels/librarypanels.go | 21 +++--- .../librarypanels/librarypanels_test.go | 51 +++++++------- .../sqlstore/migrations/libraryelements.go | 22 ------ 17 files changed, 157 insertions(+), 229 deletions(-) diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index 733217495a..bede8c3cfd 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -1037,7 +1037,7 @@ func (m *mockLibraryPanelService) ConnectLibraryPanelsForDashboard(c context.Con return nil } -func (m *mockLibraryPanelService) ImportLibraryPanelsForDashboard(c context.Context, signedInUser identity.Requester, libraryPanels *simplejson.Json, panels []any, folderID int64, folderUID string) error { +func (m *mockLibraryPanelService) ImportLibraryPanelsForDashboard(c context.Context, signedInUser identity.Requester, libraryPanels *simplejson.Json, panels []any, folderID int64) error { return nil } diff --git a/pkg/services/dashboardimport/service/service.go b/pkg/services/dashboardimport/service/service.go index 00cd032496..842651e275 100644 --- a/pkg/services/dashboardimport/service/service.go +++ b/pkg/services/dashboardimport/service/service.go @@ -141,7 +141,7 @@ func (s *ImportDashboardService) ImportDashboard(ctx context.Context, req *dashb metrics.MFolderIDsServiceCount.WithLabelValues(metrics.DashboardImport).Inc() // nolint:staticcheck - err = s.libraryPanelService.ImportLibraryPanelsForDashboard(ctx, req.User, libraryElements, generatedDash.Get("panels").MustArray(), req.FolderId, req.FolderUid) + err = s.libraryPanelService.ImportLibraryPanelsForDashboard(ctx, req.User, libraryElements, generatedDash.Get("panels").MustArray(), req.FolderId) if err != nil { return nil, err } diff --git a/pkg/services/dashboardimport/service/service_test.go b/pkg/services/dashboardimport/service/service_test.go index e8958f22c9..19e0dba893 100644 --- a/pkg/services/dashboardimport/service/service_test.go +++ b/pkg/services/dashboardimport/service/service_test.go @@ -47,7 +47,7 @@ func TestImportDashboardService(t *testing.T) { importLibraryPanelsForDashboard := false connectLibraryPanelsForDashboardCalled := false libraryPanelService := &libraryPanelServiceMock{ - importLibraryPanelsForDashboardFunc: func(ctx context.Context, signedInUser identity.Requester, libraryPanels *simplejson.Json, panels []any, folderID int64, folderUID string) error { + importLibraryPanelsForDashboardFunc: func(ctx context.Context, signedInUser identity.Requester, libraryPanels *simplejson.Json, panels []any, folderID int64) error { importLibraryPanelsForDashboard = true return nil }, @@ -75,8 +75,9 @@ func TestImportDashboardService(t *testing.T) { Inputs: []dashboardimport.ImportDashboardInput{ {Name: "*", Type: "datasource", Value: "prom"}, }, - User: &user.SignedInUser{UserID: 2, OrgRole: org.RoleAdmin, OrgID: 3}, - FolderUid: "folderUID", + User: &user.SignedInUser{UserID: 2, OrgRole: org.RoleAdmin, OrgID: 3}, + // FolderId: 5, + FolderUid: "123", } resp, err := s.ImportDashboard(context.Background(), req) require.NoError(t, err) @@ -90,7 +91,7 @@ func TestImportDashboardService(t *testing.T) { require.Equal(t, int64(3), importDashboardArg.OrgID) require.Equal(t, int64(2), userID) require.Equal(t, "prometheus", importDashboardArg.Dashboard.PluginID) - require.Equal(t, "folderUID", importDashboardArg.Dashboard.FolderUID) + require.Equal(t, "123", importDashboardArg.Dashboard.FolderUID) panel := importDashboardArg.Dashboard.Data.Get("panels").GetIndex(0) require.Equal(t, "prom", panel.Get("datasource").MustString()) @@ -142,7 +143,7 @@ func TestImportDashboardService(t *testing.T) { {Name: "*", Type: "datasource", Value: "prom"}, }, User: &user.SignedInUser{UserID: 2, OrgRole: org.RoleAdmin, OrgID: 3}, - FolderUid: "folderUID", + FolderUid: "123", } resp, err := s.ImportDashboard(context.Background(), req) require.NoError(t, err) @@ -156,7 +157,7 @@ func TestImportDashboardService(t *testing.T) { require.Equal(t, int64(3), importDashboardArg.OrgID) require.Equal(t, int64(2), userID) require.Equal(t, "", importDashboardArg.Dashboard.PluginID) - require.Equal(t, "folderUID", importDashboardArg.Dashboard.FolderUID) + require.Equal(t, "123", importDashboardArg.Dashboard.FolderUID) panel := importDashboardArg.Dashboard.Data.Get("panels").GetIndex(0) require.Equal(t, "prom", panel.Get("datasource").MustString()) @@ -210,7 +211,7 @@ func (s *dashboardServiceMock) ImportDashboard(ctx context.Context, dto *dashboa type libraryPanelServiceMock struct { librarypanels.Service connectLibraryPanelsForDashboardFunc func(c context.Context, signedInUser identity.Requester, dash *dashboards.Dashboard) error - importLibraryPanelsForDashboardFunc func(c context.Context, signedInUser identity.Requester, libraryPanels *simplejson.Json, panels []any, folderID int64, folderUID string) error + importLibraryPanelsForDashboardFunc func(c context.Context, signedInUser identity.Requester, libraryPanels *simplejson.Json, panels []any, folderID int64) error } var _ librarypanels.Service = (*libraryPanelServiceMock)(nil) @@ -223,9 +224,9 @@ func (s *libraryPanelServiceMock) ConnectLibraryPanelsForDashboard(ctx context.C return nil } -func (s *libraryPanelServiceMock) ImportLibraryPanelsForDashboard(ctx context.Context, signedInUser identity.Requester, libraryPanels *simplejson.Json, panels []any, folderID int64, folderUID string) error { +func (s *libraryPanelServiceMock) ImportLibraryPanelsForDashboard(ctx context.Context, signedInUser identity.Requester, libraryPanels *simplejson.Json, panels []any, folderID int64) error { if s.importLibraryPanelsForDashboardFunc != nil { - return s.importLibraryPanelsForDashboardFunc(ctx, signedInUser, libraryPanels, panels, folderID, folderUID) + return s.importLibraryPanelsForDashboardFunc(ctx, signedInUser, libraryPanels, panels, folderID) } return nil diff --git a/pkg/services/folder/folderimpl/folder_test.go b/pkg/services/folder/folderimpl/folder_test.go index 17b10376f6..e23fe64f20 100644 --- a/pkg/services/folder/folderimpl/folder_test.go +++ b/pkg/services/folder/folderimpl/folder_test.go @@ -448,12 +448,10 @@ func TestIntegrationNestedFolderService(t *testing.T) { // nolint:staticcheck libraryElementCmd.FolderID = parent.ID - libraryElementCmd.FolderUID = &parent.UID _, err = lps.LibraryElementService.CreateElement(context.Background(), &signedInUser, libraryElementCmd) require.NoError(t, err) // nolint:staticcheck libraryElementCmd.FolderID = subfolder.ID - libraryElementCmd.FolderUID = &subfolder.UID _, err = lps.LibraryElementService.CreateElement(context.Background(), &signedInUser, libraryElementCmd) require.NoError(t, err) @@ -530,12 +528,10 @@ func TestIntegrationNestedFolderService(t *testing.T) { // nolint:staticcheck libraryElementCmd.FolderID = parent.ID - libraryElementCmd.FolderUID = &parent.UID _, err = lps.LibraryElementService.CreateElement(context.Background(), &signedInUser, libraryElementCmd) require.NoError(t, err) // nolint:staticcheck libraryElementCmd.FolderID = subfolder.ID - libraryElementCmd.FolderUID = &subfolder.UID _, err = lps.LibraryElementService.CreateElement(context.Background(), &signedInUser, libraryElementCmd) require.NoError(t, err) @@ -671,13 +667,11 @@ func TestIntegrationNestedFolderService(t *testing.T) { _ = createRule(t, alertStore, subfolder.UID, "sub alert") // nolint:staticcheck libraryElementCmd.FolderID = subfolder.ID - libraryElementCmd.FolderUID = &subPanel.FolderUID subPanel, err = lps.LibraryElementService.CreateElement(context.Background(), &signedInUser, libraryElementCmd) require.NoError(t, err) } // nolint:staticcheck libraryElementCmd.FolderID = parent.ID - libraryElementCmd.FolderUID = &parent.UID parentPanel, err := lps.LibraryElementService.CreateElement(context.Background(), &signedInUser, libraryElementCmd) require.NoError(t, err) diff --git a/pkg/services/libraryelements/database.go b/pkg/services/libraryelements/database.go index 2aba478bd4..b8c09c1281 100644 --- a/pkg/services/libraryelements/database.go +++ b/pkg/services/libraryelements/database.go @@ -149,20 +149,14 @@ func (l *LibraryElementService) createLibraryElement(c context.Context, signedIn } metrics.MFolderIDsServiceCount.WithLabelValues(metrics.LibraryElements).Inc() - // folderUID *string will be changed to string - var folderUID string - if cmd.FolderUID != nil { - folderUID = *cmd.FolderUID - } element := model.LibraryElement{ - OrgID: signedInUser.GetOrgID(), - FolderID: cmd.FolderID, // nolint:staticcheck - FolderUID: folderUID, - UID: createUID, - Name: cmd.Name, - Model: updatedModel, - Version: 1, - Kind: cmd.Kind, + OrgID: signedInUser.GetOrgID(), + FolderID: cmd.FolderID, // nolint:staticcheck + UID: createUID, + Name: cmd.Name, + Model: updatedModel, + Version: 1, + Kind: cmd.Kind, Created: time.Now(), Updated: time.Now(), diff --git a/pkg/services/libraryelements/libraryelements_create_test.go b/pkg/services/libraryelements/libraryelements_create_test.go index 7adca02ebd..8a1dfac99b 100644 --- a/pkg/services/libraryelements/libraryelements_create_test.go +++ b/pkg/services/libraryelements/libraryelements_create_test.go @@ -15,7 +15,7 @@ func TestCreateLibraryElement(t *testing.T) { scenarioWithPanel(t, "When an admin tries to create a library panel that already exists, it should fail", func(t *testing.T, sc scenarioContext) { // nolint:staticcheck - command := getCreatePanelCommand(sc.folder.ID, sc.folder.UID, "Text - Library Panel") + command := getCreatePanelCommand(sc.folder.ID, "Text - Library Panel") sc.reqContext.Req.Body = mockRequestBody(command) resp := sc.service.createHandler(sc.reqContext) require.Equal(t, 400, resp.Status()) @@ -28,7 +28,6 @@ func TestCreateLibraryElement(t *testing.T) { ID: 1, OrgID: 1, FolderID: 1, // nolint:staticcheck - FolderUID: sc.folder.UID, UID: sc.initialResult.Result.UID, Name: "Text - Library Panel", Kind: int64(model.PanelElement), @@ -69,7 +68,7 @@ func TestCreateLibraryElement(t *testing.T) { testScenario(t, "When an admin tries to create a library panel that does not exists using an nonexistent UID, it should succeed", func(t *testing.T, sc scenarioContext) { // nolint:staticcheck - command := getCreatePanelCommand(sc.folder.ID, sc.folder.UID, "Nonexistent UID") + command := getCreatePanelCommand(sc.folder.ID, "Nonexistent UID") command.UID = util.GenerateShortUID() sc.reqContext.Req.Body = mockRequestBody(command) resp := sc.service.createHandler(sc.reqContext) @@ -79,7 +78,6 @@ func TestCreateLibraryElement(t *testing.T) { ID: 1, OrgID: 1, FolderID: 1, // nolint:staticcheck - FolderUID: sc.folder.UID, UID: command.UID, Name: "Nonexistent UID", Kind: int64(model.PanelElement), @@ -120,7 +118,7 @@ func TestCreateLibraryElement(t *testing.T) { scenarioWithPanel(t, "When an admin tries to create a library panel that does not exists using an existent UID, it should fail", func(t *testing.T, sc scenarioContext) { // nolint:staticcheck - command := getCreatePanelCommand(sc.folder.ID, sc.folder.UID, "Existing UID") + command := getCreatePanelCommand(sc.folder.ID, "Existing UID") command.UID = sc.initialResult.Result.UID sc.reqContext.Req.Body = mockRequestBody(command) resp := sc.service.createHandler(sc.reqContext) @@ -130,7 +128,7 @@ func TestCreateLibraryElement(t *testing.T) { scenarioWithPanel(t, "When an admin tries to create a library panel that does not exists using an invalid UID, it should fail", func(t *testing.T, sc scenarioContext) { // nolint:staticcheck - command := getCreatePanelCommand(sc.folder.ID, sc.folder.UID, "Invalid UID") + command := getCreatePanelCommand(sc.folder.ID, "Invalid UID") command.UID = "Testing an invalid UID" sc.reqContext.Req.Body = mockRequestBody(command) resp := sc.service.createHandler(sc.reqContext) @@ -140,7 +138,7 @@ func TestCreateLibraryElement(t *testing.T) { scenarioWithPanel(t, "When an admin tries to create a library panel that does not exists using an UID that is too long, it should fail", func(t *testing.T, sc scenarioContext) { // nolint:staticcheck - command := getCreatePanelCommand(sc.folder.ID, sc.folder.UID, "Invalid UID") + command := getCreatePanelCommand(sc.folder.ID, "Invalid UID") command.UID = "j6T00KRZzj6T00KRZzj6T00KRZzj6T00KRZzj6T00K" sc.reqContext.Req.Body = mockRequestBody(command) resp := sc.service.createHandler(sc.reqContext) @@ -149,7 +147,7 @@ func TestCreateLibraryElement(t *testing.T) { testScenario(t, "When an admin tries to create a library panel where name and panel title differ, it should not update panel title", func(t *testing.T, sc scenarioContext) { - command := getCreatePanelCommand(1, sc.folder.UID, "Library Panel Name") + command := getCreatePanelCommand(1, "Library Panel Name") sc.reqContext.Req.Body = mockRequestBody(command) resp := sc.service.createHandler(sc.reqContext) var result = validateAndUnMarshalResponse(t, resp) @@ -158,7 +156,6 @@ func TestCreateLibraryElement(t *testing.T) { ID: 1, OrgID: 1, FolderID: 1, // nolint:staticcheck - FolderUID: sc.folder.UID, UID: result.Result.UID, Name: "Library Panel Name", Kind: int64(model.PanelElement), diff --git a/pkg/services/libraryelements/libraryelements_delete_test.go b/pkg/services/libraryelements/libraryelements_delete_test.go index 597787d6ee..7493c49461 100644 --- a/pkg/services/libraryelements/libraryelements_delete_test.go +++ b/pkg/services/libraryelements/libraryelements_delete_test.go @@ -74,7 +74,7 @@ func TestDeleteLibraryElement(t *testing.T) { Data: simplejson.NewFromAny(dashJSON), } // nolint:staticcheck - dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.ID, sc.folder.UID) + dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.ID) err := sc.service.ConnectElementsToDashboard(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, []string{sc.initialResult.Result.UID}, dashInDB.ID) require.NoError(t, err) diff --git a/pkg/services/libraryelements/libraryelements_get_all_test.go b/pkg/services/libraryelements/libraryelements_get_all_test.go index 29cc7819fc..d281155a1b 100644 --- a/pkg/services/libraryelements/libraryelements_get_all_test.go +++ b/pkg/services/libraryelements/libraryelements_get_all_test.go @@ -39,7 +39,7 @@ func TestGetAllLibraryElements(t *testing.T) { scenarioWithPanel(t, "When an admin tries to get all panel elements and both panels and variables exist, it should only return panels", func(t *testing.T, sc scenarioContext) { // nolint:staticcheck - command := getCreateVariableCommand(sc.folder.ID, sc.folder.UID, "query0") + command := getCreateVariableCommand(sc.folder.ID, "query0") sc.reqContext.Req.Body = mockRequestBody(command) resp := sc.service.createHandler(sc.reqContext) require.Equal(t, 200, resp.Status()) @@ -64,7 +64,6 @@ func TestGetAllLibraryElements(t *testing.T) { ID: 1, OrgID: 1, FolderID: 1, // nolint:staticcheck - FolderUID: sc.folder.UID, UID: result.Result.Elements[0].UID, Name: "Text - Library Panel", Kind: int64(model.PanelElement), @@ -107,7 +106,7 @@ func TestGetAllLibraryElements(t *testing.T) { scenarioWithPanel(t, "When an admin tries to get all variable elements and both panels and variables exist, it should only return panels", func(t *testing.T, sc scenarioContext) { // nolint:staticcheck - command := getCreateVariableCommand(sc.folder.ID, sc.folder.UID, "query0") + command := getCreateVariableCommand(sc.folder.ID, "query0") sc.reqContext.Req.Body = mockRequestBody(command) resp := sc.service.createHandler(sc.reqContext) require.Equal(t, 200, resp.Status()) @@ -132,7 +131,6 @@ func TestGetAllLibraryElements(t *testing.T) { ID: 2, OrgID: 1, FolderID: 1, // nolint:staticcheck - FolderUID: sc.folder.UID, UID: result.Result.Elements[0].UID, Name: "query0", Kind: int64(model.VariableElement), @@ -174,7 +172,7 @@ func TestGetAllLibraryElements(t *testing.T) { scenarioWithPanel(t, "When an admin tries to get all library panels and two exist, it should succeed", func(t *testing.T, sc scenarioContext) { // nolint:staticcheck - command := getCreatePanelCommand(sc.folder.ID, sc.folder.UID, "Text - Library Panel2") + command := getCreatePanelCommand(sc.folder.ID, "Text - Library Panel2") sc.reqContext.Req.Body = mockRequestBody(command) resp := sc.service.createHandler(sc.reqContext) require.Equal(t, 200, resp.Status()) @@ -195,7 +193,6 @@ func TestGetAllLibraryElements(t *testing.T) { ID: 1, OrgID: 1, FolderID: 1, // nolint:staticcheck - FolderUID: sc.folder.UID, UID: result.Result.Elements[0].UID, Name: "Text - Library Panel", Kind: int64(model.PanelElement), @@ -231,7 +228,6 @@ func TestGetAllLibraryElements(t *testing.T) { ID: 2, OrgID: 1, FolderID: 1, // nolint:staticcheck - FolderUID: sc.folder.UID, UID: result.Result.Elements[1].UID, Name: "Text - Library Panel2", Kind: int64(model.PanelElement), @@ -274,7 +270,7 @@ func TestGetAllLibraryElements(t *testing.T) { scenarioWithPanel(t, "When an admin tries to get all library panels and two exist and sort desc is set, it should succeed and the result should be correct", func(t *testing.T, sc scenarioContext) { // nolint:staticcheck - command := getCreatePanelCommand(sc.folder.ID, sc.folder.UID, "Text - Library Panel2") + command := getCreatePanelCommand(sc.folder.ID, "Text - Library Panel2") sc.reqContext.Req.Body = mockRequestBody(command) resp := sc.service.createHandler(sc.reqContext) require.Equal(t, 200, resp.Status()) @@ -298,7 +294,6 @@ func TestGetAllLibraryElements(t *testing.T) { ID: 2, OrgID: 1, FolderID: 1, // nolint:staticcheck - FolderUID: sc.folder.UID, UID: result.Result.Elements[0].UID, Name: "Text - Library Panel2", Kind: int64(model.PanelElement), @@ -334,7 +329,6 @@ func TestGetAllLibraryElements(t *testing.T) { ID: 1, OrgID: 1, FolderID: 1, // nolint:staticcheck - FolderUID: sc.folder.UID, UID: result.Result.Elements[1].UID, Name: "Text - Library Panel", Kind: int64(model.PanelElement), @@ -377,7 +371,7 @@ func TestGetAllLibraryElements(t *testing.T) { scenarioWithPanel(t, "When an admin tries to get all library panels and two exist and typeFilter is set to existing types, it should succeed and the result should be correct", func(t *testing.T, sc scenarioContext) { // nolint:staticcheck - command := getCreateCommandWithModel(sc.folder.ID, sc.folder.UID, "Gauge - Library Panel", model.PanelElement, []byte(` + command := getCreateCommandWithModel(sc.folder.ID, "Gauge - Library Panel", model.PanelElement, []byte(` { "datasource": "${DS_GDEV-TESTDATA}", "id": 1, @@ -391,7 +385,7 @@ func TestGetAllLibraryElements(t *testing.T) { require.Equal(t, 200, resp.Status()) // nolint:staticcheck - command = getCreateCommandWithModel(sc.folder.ID, sc.folder.UID, "BarGauge - Library Panel", model.PanelElement, []byte(` + command = getCreateCommandWithModel(sc.folder.ID, "BarGauge - Library Panel", model.PanelElement, []byte(` { "datasource": "${DS_GDEV-TESTDATA}", "id": 1, @@ -423,7 +417,6 @@ func TestGetAllLibraryElements(t *testing.T) { ID: 3, OrgID: 1, FolderID: 1, // nolint:staticcheck - FolderUID: sc.folder.UID, UID: result.Result.Elements[0].UID, Name: "BarGauge - Library Panel", Kind: int64(model.PanelElement), @@ -459,7 +452,6 @@ func TestGetAllLibraryElements(t *testing.T) { ID: 2, OrgID: 1, FolderID: 1, // nolint:staticcheck - FolderUID: sc.folder.UID, UID: result.Result.Elements[1].UID, Name: "Gauge - Library Panel", Kind: int64(model.PanelElement), @@ -502,7 +494,7 @@ func TestGetAllLibraryElements(t *testing.T) { scenarioWithPanel(t, "When an admin tries to get all library panels and two exist and typeFilter is set to a nonexistent type, it should succeed and the result should be correct", func(t *testing.T, sc scenarioContext) { // nolint:staticcheck - command := getCreateCommandWithModel(sc.folder.ID, sc.folder.UID, "Gauge - Library Panel", model.PanelElement, []byte(` + command := getCreateCommandWithModel(sc.folder.ID, "Gauge - Library Panel", model.PanelElement, []byte(` { "datasource": "${DS_GDEV-TESTDATA}", "id": 1, @@ -537,24 +529,25 @@ func TestGetAllLibraryElements(t *testing.T) { } }) - scenarioWithPanel(t, "When an admin tries to get all library panels and two exist and folderFilterUIDs is set to existing folders, it should succeed and the result should be correct", + scenarioWithPanel(t, "When an admin tries to get all library panels and two exist and folderFilter is set to existing folders, it should succeed and the result should be correct", func(t *testing.T, sc scenarioContext) { newFolder := createFolder(t, sc, "NewFolder") // nolint:staticcheck - command := getCreatePanelCommand(newFolder.ID, newFolder.UID, "Text - Library Panel2") + command := getCreatePanelCommand(newFolder.ID, "Text - Library Panel2") sc.reqContext.Req.Body = mockRequestBody(command) resp := sc.service.createHandler(sc.reqContext) require.Equal(t, 200, resp.Status()) - folderFilterUID := newFolder.UID + // nolint:staticcheck + folderFilter := strconv.FormatInt(newFolder.ID, 10) + err := sc.reqContext.Req.ParseForm() require.NoError(t, err) - sc.reqContext.Req.Form.Add("folderFilterUIDs", folderFilterUID) + sc.reqContext.Req.Form.Add("folderFilter", folderFilter) resp = sc.service.getAllHandler(sc.reqContext) require.Equal(t, 200, resp.Status()) var result libraryElementsSearch err = json.Unmarshal(resp.Body(), &result) - require.NoError(t, err) var expected = libraryElementsSearch{ Result: libraryElementsSearchResult{ @@ -566,7 +559,6 @@ func TestGetAllLibraryElements(t *testing.T) { ID: 2, OrgID: 1, FolderID: newFolder.ID, // nolint:staticcheck - FolderUID: newFolder.UID, UID: result.Result.Elements[0].UID, Name: "Text - Library Panel2", Kind: int64(model.PanelElement), @@ -610,15 +602,15 @@ func TestGetAllLibraryElements(t *testing.T) { func(t *testing.T, sc scenarioContext) { newFolder := createFolder(t, sc, "NewFolder") // nolint:staticcheck - command := getCreatePanelCommand(newFolder.ID, sc.folder.UID, "Text - Library Panel2") + command := getCreatePanelCommand(newFolder.ID, "Text - Library Panel2") sc.reqContext.Req.Body = mockRequestBody(command) resp := sc.service.createHandler(sc.reqContext) require.Equal(t, 200, resp.Status()) - folderFilterUIDs := "2020,2021" + folderFilter := "2020,2021" err := sc.reqContext.Req.ParseForm() require.NoError(t, err) - sc.reqContext.Req.Form.Add("folderFilterUIDs", folderFilterUIDs) + sc.reqContext.Req.Form.Add("folderFilter", folderFilter) resp = sc.service.getAllHandler(sc.reqContext) require.Equal(t, 200, resp.Status()) @@ -641,7 +633,7 @@ func TestGetAllLibraryElements(t *testing.T) { scenarioWithPanel(t, "When an admin tries to get all library panels and two exist and folderFilter is set to General folder, it should succeed and the result should be correct", func(t *testing.T, sc scenarioContext) { // nolint:staticcheck - command := getCreatePanelCommand(sc.folder.ID, sc.folder.UID, "Text - Library Panel2") + command := getCreatePanelCommand(sc.folder.ID, "Text - Library Panel2") sc.reqContext.Req.Body = mockRequestBody(command) resp := sc.service.createHandler(sc.reqContext) require.Equal(t, 200, resp.Status()) @@ -666,7 +658,6 @@ func TestGetAllLibraryElements(t *testing.T) { ID: 1, OrgID: 1, FolderID: 1, // nolint:staticcheck - FolderUID: sc.folder.UID, UID: result.Result.Elements[0].UID, Name: "Text - Library Panel", Kind: int64(model.PanelElement), @@ -702,7 +693,6 @@ func TestGetAllLibraryElements(t *testing.T) { ID: 2, OrgID: 1, FolderID: 1, // nolint:staticcheck - FolderUID: sc.folder.UID, UID: result.Result.Elements[1].UID, Name: "Text - Library Panel2", Kind: int64(model.PanelElement), @@ -745,7 +735,7 @@ func TestGetAllLibraryElements(t *testing.T) { scenarioWithPanel(t, "When an admin tries to get all library panels and two exist and excludeUID is set, it should succeed and the result should be correct", func(t *testing.T, sc scenarioContext) { // nolint:staticcheck - command := getCreatePanelCommand(sc.folder.ID, sc.folder.UID, "Text - Library Panel2") + command := getCreatePanelCommand(sc.folder.ID, "Text - Library Panel2") sc.reqContext.Req.Body = mockRequestBody(command) resp := sc.service.createHandler(sc.reqContext) require.Equal(t, 200, resp.Status()) @@ -769,7 +759,6 @@ func TestGetAllLibraryElements(t *testing.T) { ID: 2, OrgID: 1, FolderID: 1, // nolint:staticcheck - FolderUID: sc.folder.UID, UID: result.Result.Elements[0].UID, Name: "Text - Library Panel2", Kind: int64(model.PanelElement), @@ -812,7 +801,7 @@ func TestGetAllLibraryElements(t *testing.T) { scenarioWithPanel(t, "When an admin tries to get all library panels and two exist and perPage is 1, it should succeed and the result should be correct", func(t *testing.T, sc scenarioContext) { // nolint:staticcheck - command := getCreatePanelCommand(sc.folder.ID, sc.folder.UID, "Text - Library Panel2") + command := getCreatePanelCommand(sc.folder.ID, "Text - Library Panel2") sc.reqContext.Req.Body = mockRequestBody(command) resp := sc.service.createHandler(sc.reqContext) require.Equal(t, 200, resp.Status()) @@ -836,7 +825,6 @@ func TestGetAllLibraryElements(t *testing.T) { ID: 1, OrgID: 1, FolderID: 1, // nolint:staticcheck - FolderUID: sc.folder.UID, UID: result.Result.Elements[0].UID, Name: "Text - Library Panel", Kind: int64(model.PanelElement), @@ -879,7 +867,7 @@ func TestGetAllLibraryElements(t *testing.T) { scenarioWithPanel(t, "When an admin tries to get all library panels and two exist and perPage is 1 and page is 2, it should succeed and the result should be correct", func(t *testing.T, sc scenarioContext) { // nolint:staticcheck - command := getCreatePanelCommand(sc.folder.ID, sc.folder.UID, "Text - Library Panel2") + command := getCreatePanelCommand(sc.folder.ID, "Text - Library Panel2") sc.reqContext.Req.Body = mockRequestBody(command) resp := sc.service.createHandler(sc.reqContext) require.Equal(t, 200, resp.Status()) @@ -904,7 +892,6 @@ func TestGetAllLibraryElements(t *testing.T) { ID: 2, OrgID: 1, FolderID: 1, // nolint:staticcheck - FolderUID: sc.folder.UID, UID: result.Result.Elements[0].UID, Name: "Text - Library Panel2", Kind: int64(model.PanelElement), @@ -947,7 +934,7 @@ func TestGetAllLibraryElements(t *testing.T) { scenarioWithPanel(t, "When an admin tries to get all library panels and two exist and searchString exists in the description, it should succeed and the result should be correct", func(t *testing.T, sc scenarioContext) { // nolint:staticcheck - command := getCreateCommandWithModel(sc.folder.ID, sc.folder.UID, "Text - Library Panel2", model.PanelElement, []byte(` + command := getCreateCommandWithModel(sc.folder.ID, "Text - Library Panel2", model.PanelElement, []byte(` { "datasource": "${DS_GDEV-TESTDATA}", "id": 1, @@ -981,7 +968,6 @@ func TestGetAllLibraryElements(t *testing.T) { ID: 1, OrgID: 1, FolderID: 1, // nolint:staticcheck - FolderUID: sc.folder.UID, UID: result.Result.Elements[0].UID, Name: "Text - Library Panel", Kind: int64(model.PanelElement), @@ -1024,7 +1010,7 @@ func TestGetAllLibraryElements(t *testing.T) { scenarioWithPanel(t, "When an admin tries to get all library panels and two exist and searchString exists in both name and description, it should succeed and the result should be correct", func(t *testing.T, sc scenarioContext) { // nolint:staticcheck - command := getCreateCommandWithModel(sc.folder.ID, sc.folder.UID, "Some Other", model.PanelElement, []byte(` + command := getCreateCommandWithModel(sc.folder.ID, "Some Other", model.PanelElement, []byte(` { "datasource": "${DS_GDEV-TESTDATA}", "id": 1, @@ -1056,7 +1042,6 @@ func TestGetAllLibraryElements(t *testing.T) { ID: 2, OrgID: 1, FolderID: 1, // nolint:staticcheck - FolderUID: sc.folder.UID, UID: result.Result.Elements[0].UID, Name: "Some Other", Kind: int64(model.PanelElement), @@ -1092,7 +1077,6 @@ func TestGetAllLibraryElements(t *testing.T) { ID: 1, OrgID: 1, FolderID: 1, // nolint:staticcheck - FolderUID: sc.folder.UID, UID: result.Result.Elements[1].UID, Name: "Text - Library Panel", Kind: int64(model.PanelElement), @@ -1135,7 +1119,7 @@ func TestGetAllLibraryElements(t *testing.T) { scenarioWithPanel(t, "When an admin tries to get all library panels and two exist and perPage is 1 and page is 1 and searchString is panel2, it should succeed and the result should be correct", func(t *testing.T, sc scenarioContext) { // nolint:staticcheck - command := getCreatePanelCommand(sc.folder.ID, sc.folder.UID, "Text - Library Panel2") + command := getCreatePanelCommand(sc.folder.ID, "Text - Library Panel2") sc.reqContext.Req.Body = mockRequestBody(command) resp := sc.service.createHandler(sc.reqContext) require.Equal(t, 200, resp.Status()) @@ -1161,7 +1145,6 @@ func TestGetAllLibraryElements(t *testing.T) { ID: 2, OrgID: 1, FolderID: 1, // nolint:staticcheck - FolderUID: sc.folder.UID, UID: result.Result.Elements[0].UID, Name: "Text - Library Panel2", Kind: int64(model.PanelElement), @@ -1204,7 +1187,7 @@ func TestGetAllLibraryElements(t *testing.T) { scenarioWithPanel(t, "When an admin tries to get all library panels and two exist and perPage is 1 and page is 3 and searchString is panel, it should succeed and the result should be correct", func(t *testing.T, sc scenarioContext) { // nolint:staticcheck - command := getCreatePanelCommand(sc.folder.ID, sc.folder.UID, "Text - Library Panel2") + command := getCreatePanelCommand(sc.folder.ID, "Text - Library Panel2") sc.reqContext.Req.Body = mockRequestBody(command) resp := sc.service.createHandler(sc.reqContext) require.Equal(t, 200, resp.Status()) @@ -1236,7 +1219,7 @@ func TestGetAllLibraryElements(t *testing.T) { scenarioWithPanel(t, "When an admin tries to get all library panels and two exist and perPage is 1 and page is 3 and searchString does not exist, it should succeed and the result should be correct", func(t *testing.T, sc scenarioContext) { // nolint:staticcheck - command := getCreatePanelCommand(sc.folder.ID, sc.folder.UID, "Text - Library Panel2") + command := getCreatePanelCommand(sc.folder.ID, "Text - Library Panel2") sc.reqContext.Req.Body = mockRequestBody(command) resp := sc.service.createHandler(sc.reqContext) require.Equal(t, 200, resp.Status()) diff --git a/pkg/services/libraryelements/libraryelements_get_test.go b/pkg/services/libraryelements/libraryelements_get_test.go index b441505639..0c8e4893c2 100644 --- a/pkg/services/libraryelements/libraryelements_get_test.go +++ b/pkg/services/libraryelements/libraryelements_get_test.go @@ -35,7 +35,6 @@ func TestGetLibraryElement(t *testing.T) { ID: 1, OrgID: 1, FolderID: 1, // nolint:staticcheck - FolderUID: sc.folder.UID, UID: res.Result.UID, Name: "Text - Library Panel", Kind: int64(model.PanelElement), @@ -124,7 +123,7 @@ func TestGetLibraryElement(t *testing.T) { Data: simplejson.NewFromAny(dashJSON), } // nolint:staticcheck - dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.ID, sc.folder.UID) + dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.ID) err := sc.service.ConnectElementsToDashboard(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, []string{sc.initialResult.Result.UID}, dashInDB.ID) require.NoError(t, err) @@ -134,7 +133,6 @@ func TestGetLibraryElement(t *testing.T) { ID: 1, OrgID: 1, FolderID: 1, // nolint:staticcheck - FolderUID: sc.folder.UID, UID: res.Result.UID, Name: "Text - Library Panel", Kind: int64(model.PanelElement), diff --git a/pkg/services/libraryelements/libraryelements_patch_test.go b/pkg/services/libraryelements/libraryelements_patch_test.go index e59a19e29b..0af1d96e7c 100644 --- a/pkg/services/libraryelements/libraryelements_patch_test.go +++ b/pkg/services/libraryelements/libraryelements_patch_test.go @@ -26,9 +26,8 @@ func TestPatchLibraryElement(t *testing.T) { func(t *testing.T, sc scenarioContext) { newFolder := createFolder(t, sc, "NewFolder") cmd := model.PatchLibraryElementCommand{ - FolderID: newFolder.ID, // nolint:staticcheck - FolderUID: &newFolder.UID, - Name: "Panel - New name", + FolderID: newFolder.ID, // nolint:staticcheck + Name: "Panel - New name", Model: []byte(` { "datasource": "${DS_GDEV-TESTDATA}", @@ -51,7 +50,6 @@ func TestPatchLibraryElement(t *testing.T) { ID: 1, OrgID: 1, FolderID: newFolder.ID, // nolint:staticcheck - FolderUID: newFolder.UID, UID: sc.initialResult.Result.UID, Name: "Panel - New name", Kind: int64(model.PanelElement), @@ -93,10 +91,9 @@ func TestPatchLibraryElement(t *testing.T) { func(t *testing.T, sc scenarioContext) { newFolder := createFolder(t, sc, "NewFolder") cmd := model.PatchLibraryElementCommand{ - FolderID: newFolder.ID, // nolint:staticcheck - FolderUID: &newFolder.UID, - Kind: int64(model.PanelElement), - Version: 1, + FolderID: newFolder.ID, // nolint:staticcheck + Kind: int64(model.PanelElement), + Version: 1, } sc.ctx.Req = web.SetURLParams(sc.ctx.Req, map[string]string{":uid": sc.initialResult.Result.UID}) sc.reqContext.Req.Body = mockRequestBody(cmd) @@ -105,7 +102,6 @@ func TestPatchLibraryElement(t *testing.T) { var result = validateAndUnMarshalResponse(t, resp) // nolint:staticcheck sc.initialResult.Result.FolderID = newFolder.ID - sc.initialResult.Result.FolderUID = newFolder.UID sc.initialResult.Result.Meta.CreatedBy.Name = userInDbName sc.initialResult.Result.Meta.CreatedBy.AvatarUrl = userInDbAvatar sc.initialResult.Result.Meta.Updated = result.Result.Meta.Updated @@ -180,11 +176,10 @@ func TestPatchLibraryElement(t *testing.T) { scenarioWithPanel(t, "When an admin tries to patch a library panel with an UID that is too long, it should fail", func(t *testing.T, sc scenarioContext) { cmd := model.PatchLibraryElementCommand{ - FolderID: -1, // nolint:staticcheck - FolderUID: &sc.folder.UID, - UID: "j6T00KRZzj6T00KRZzj6T00KRZzj6T00KRZzj6T00K", - Kind: int64(model.PanelElement), - Version: 1, + FolderID: -1, // nolint:staticcheck + UID: "j6T00KRZzj6T00KRZzj6T00KRZzj6T00KRZzj6T00K", + Kind: int64(model.PanelElement), + Version: 1, } sc.ctx.Req = web.SetURLParams(sc.ctx.Req, map[string]string{":uid": sc.initialResult.Result.UID}) sc.ctx.Req.Body = mockRequestBody(cmd) @@ -195,17 +190,16 @@ func TestPatchLibraryElement(t *testing.T) { scenarioWithPanel(t, "When an admin tries to patch a library panel with an existing UID, it should fail", func(t *testing.T, sc scenarioContext) { // nolint:staticcheck - command := getCreatePanelCommand(sc.folder.ID, sc.folder.UID, "Existing UID") + command := getCreatePanelCommand(sc.folder.ID, "Existing UID") command.UID = util.GenerateShortUID() sc.reqContext.Req.Body = mockRequestBody(command) resp := sc.service.createHandler(sc.reqContext) require.Equal(t, 200, resp.Status()) cmd := model.PatchLibraryElementCommand{ - FolderID: -1, // nolint:staticcheck - FolderUID: &sc.folder.UID, - UID: command.UID, - Kind: int64(model.PanelElement), - Version: 1, + FolderID: -1, // nolint:staticcheck + UID: command.UID, + Kind: int64(model.PanelElement), + Version: 1, } sc.ctx.Req = web.SetURLParams(sc.ctx.Req, map[string]string{":uid": sc.initialResult.Result.UID}) sc.ctx.Req.Body = mockRequestBody(cmd) @@ -318,7 +312,7 @@ func TestPatchLibraryElement(t *testing.T) { scenarioWithPanel(t, "When an admin tries to patch a library panel with a name that already exists, it should fail", func(t *testing.T, sc scenarioContext) { // nolint:staticcheck - command := getCreatePanelCommand(sc.folder.ID, sc.folder.UID, "Another Panel") + command := getCreatePanelCommand(sc.folder.ID, "Another Panel") sc.ctx.Req.Body = mockRequestBody(command) resp := sc.service.createHandler(sc.reqContext) var result = validateAndUnMarshalResponse(t, resp) @@ -337,15 +331,14 @@ func TestPatchLibraryElement(t *testing.T) { func(t *testing.T, sc scenarioContext) { newFolder := createFolder(t, sc, "NewFolder") // nolint:staticcheck - command := getCreatePanelCommand(newFolder.ID, newFolder.UID, "Text - Library Panel") + command := getCreatePanelCommand(newFolder.ID, "Text - Library Panel") sc.ctx.Req.Body = mockRequestBody(command) resp := sc.service.createHandler(sc.reqContext) var result = validateAndUnMarshalResponse(t, resp) cmd := model.PatchLibraryElementCommand{ - FolderID: 1, // nolint:staticcheck - FolderUID: &sc.folder.UID, - Version: 1, - Kind: int64(model.PanelElement), + FolderID: 1, // nolint:staticcheck + Version: 1, + Kind: int64(model.PanelElement), } sc.ctx.Req = web.SetURLParams(sc.ctx.Req, map[string]string{":uid": result.Result.UID}) sc.ctx.Req.Body = mockRequestBody(cmd) @@ -356,25 +349,23 @@ func TestPatchLibraryElement(t *testing.T) { scenarioWithPanel(t, "When an admin tries to patch a library panel in another org, it should fail", func(t *testing.T, sc scenarioContext) { cmd := model.PatchLibraryElementCommand{ - FolderID: sc.folder.ID, // nolint:staticcheck - FolderUID: &sc.folder.UID, - Version: 1, - Kind: int64(model.PanelElement), + FolderID: sc.folder.ID, // nolint:staticcheck + Version: 1, + Kind: int64(model.PanelElement), } sc.reqContext.OrgID = 2 sc.ctx.Req = web.SetURLParams(sc.ctx.Req, map[string]string{":uid": sc.initialResult.Result.UID}) sc.ctx.Req.Body = mockRequestBody(cmd) resp := sc.service.patchHandler(sc.reqContext) - require.Equal(t, 400, resp.Status()) + require.Equal(t, 404, resp.Status()) }) scenarioWithPanel(t, "When an admin tries to patch a library panel with an old version number, it should fail", func(t *testing.T, sc scenarioContext) { cmd := model.PatchLibraryElementCommand{ - FolderID: sc.folder.ID, // nolint:staticcheck - FolderUID: &sc.folder.UID, - Version: 1, - Kind: int64(model.PanelElement), + FolderID: sc.folder.ID, // nolint:staticcheck + Version: 1, + Kind: int64(model.PanelElement), } sc.ctx.Req = web.SetURLParams(sc.ctx.Req, map[string]string{":uid": sc.initialResult.Result.UID}) sc.ctx.Req.Body = mockRequestBody(cmd) @@ -388,10 +379,9 @@ func TestPatchLibraryElement(t *testing.T) { scenarioWithPanel(t, "When an admin tries to patch a library panel with an other kind, it should succeed but panel should not change", func(t *testing.T, sc scenarioContext) { cmd := model.PatchLibraryElementCommand{ - FolderID: sc.folder.ID, // nolint:staticcheck - FolderUID: &sc.folder.UID, - Version: 1, - Kind: int64(model.VariableElement), + FolderID: sc.folder.ID, // nolint:staticcheck + Version: 1, + Kind: int64(model.VariableElement), } sc.ctx.Req = web.SetURLParams(sc.ctx.Req, map[string]string{":uid": sc.initialResult.Result.UID}) sc.ctx.Req.Body = mockRequestBody(cmd) diff --git a/pkg/services/libraryelements/libraryelements_permissions_test.go b/pkg/services/libraryelements/libraryelements_permissions_test.go index 327366115a..bb48d58f53 100644 --- a/pkg/services/libraryelements/libraryelements_permissions_test.go +++ b/pkg/services/libraryelements/libraryelements_permissions_test.go @@ -30,7 +30,7 @@ func TestLibraryElementPermissionsGeneralFolder(t *testing.T) { func(t *testing.T, sc scenarioContext) { sc.reqContext.SignedInUser.OrgRole = testCase.role - command := getCreatePanelCommand(0, "", "Library Panel Name") + command := getCreatePanelCommand(0, "Library Panel Name") sc.reqContext.Req.Body = mockRequestBody(command) resp := sc.service.createHandler(sc.reqContext) require.Equal(t, testCase.status, resp.Status()) @@ -40,7 +40,7 @@ func TestLibraryElementPermissionsGeneralFolder(t *testing.T) { func(t *testing.T, sc scenarioContext) { folder := createFolder(t, sc, "Folder") // nolint:staticcheck - command := getCreatePanelCommand(folder.ID, folder.UID, "Library Panel Name") + command := getCreatePanelCommand(folder.ID, "Library Panel Name") sc.reqContext.Req.Body = mockRequestBody(command) resp := sc.service.createHandler(sc.reqContext) result := validateAndUnMarshalResponse(t, resp) @@ -57,7 +57,7 @@ func TestLibraryElementPermissionsGeneralFolder(t *testing.T) { testScenario(t, fmt.Sprintf("When %s tries to patch a library panel by moving it from the General folder, it should return correct status", testCase.role), func(t *testing.T, sc scenarioContext) { folder := createFolder(t, sc, "Folder") - command := getCreatePanelCommand(0, "", "Library Panel Name") + command := getCreatePanelCommand(0, "Library Panel Name") sc.reqContext.Req.Body = mockRequestBody(command) resp := sc.service.createHandler(sc.reqContext) result := validateAndUnMarshalResponse(t, resp) @@ -73,7 +73,7 @@ func TestLibraryElementPermissionsGeneralFolder(t *testing.T) { testScenario(t, fmt.Sprintf("When %s tries to delete a library panel in the General folder, it should return correct status", testCase.role), func(t *testing.T, sc scenarioContext) { - cmd := getCreatePanelCommand(0, "", "Library Panel Name") + cmd := getCreatePanelCommand(0, "Library Panel Name") sc.reqContext.Req.Body = mockRequestBody(cmd) resp := sc.service.createHandler(sc.reqContext) result := validateAndUnMarshalResponse(t, resp) @@ -86,7 +86,7 @@ func TestLibraryElementPermissionsGeneralFolder(t *testing.T) { testScenario(t, fmt.Sprintf("When %s tries to get a library panel from General folder, it should return correct response", testCase.role), func(t *testing.T, sc scenarioContext) { - cmd := getCreatePanelCommand(0, "", "Library Panel in General Folder") + cmd := getCreatePanelCommand(0, "Library Panel in General Folder") sc.reqContext.Req.Body = mockRequestBody(cmd) resp := sc.service.createHandler(sc.reqContext) result := validateAndUnMarshalResponse(t, resp) @@ -96,7 +96,6 @@ func TestLibraryElementPermissionsGeneralFolder(t *testing.T) { result.Result.Meta.UpdatedBy.AvatarUrl = userInDbAvatar result.Result.Meta.FolderName = "General" result.Result.Meta.FolderUID = "" - result.Result.FolderUID = "general" sc.reqContext.SignedInUser.OrgRole = testCase.role sc.ctx.Req = web.SetURLParams(sc.ctx.Req, map[string]string{":uid": result.Result.UID}) @@ -112,7 +111,7 @@ func TestLibraryElementPermissionsGeneralFolder(t *testing.T) { testScenario(t, fmt.Sprintf("When %s tries to get all library panels from General folder, it should return correct response", testCase.role), func(t *testing.T, sc scenarioContext) { - cmd := getCreatePanelCommand(0, "", "Library Panel in General Folder") + cmd := getCreatePanelCommand(0, "Library Panel in General Folder") sc.reqContext.Req.Body = mockRequestBody(cmd) resp := sc.service.createHandler(sc.reqContext) result := validateAndUnMarshalResponse(t, resp) @@ -184,7 +183,7 @@ func TestLibraryElementCreatePermissions(t *testing.T) { } // nolint:staticcheck - command := getCreatePanelCommand(folder.ID, folder.UID, "Library Panel Name") + command := getCreatePanelCommand(folder.ID, "Library Panel Name") sc.reqContext.Req.Body = mockRequestBody(command) resp := sc.service.createHandler(sc.reqContext) require.Equal(t, testCase.status, resp.Status()) @@ -237,7 +236,7 @@ func TestLibraryElementPatchPermissions(t *testing.T) { func(t *testing.T, sc scenarioContext) { fromFolder := createFolder(t, sc, "FromFolder") // nolint:staticcheck - command := getCreatePanelCommand(fromFolder.ID, fromFolder.UID, "Library Panel Name") + command := getCreatePanelCommand(fromFolder.ID, "Library Panel Name") sc.reqContext.Req.Body = mockRequestBody(command) resp := sc.service.createHandler(sc.reqContext) result := validateAndUnMarshalResponse(t, resp) @@ -249,7 +248,7 @@ func TestLibraryElementPatchPermissions(t *testing.T) { } // nolint:staticcheck - cmd := model.PatchLibraryElementCommand{FolderID: toFolder.ID, FolderUID: &toFolder.UID, Version: 1, Kind: int64(model.PanelElement)} + cmd := model.PatchLibraryElementCommand{FolderID: toFolder.ID, Version: 1, Kind: int64(model.PanelElement)} sc.ctx.Req = web.SetURLParams(sc.ctx.Req, map[string]string{":uid": result.Result.UID}) sc.reqContext.Req.Body = mockRequestBody(cmd) resp = sc.service.patchHandler(sc.reqContext) @@ -299,7 +298,7 @@ func TestLibraryElementDeletePermissions(t *testing.T) { func(t *testing.T, sc scenarioContext) { folder := createFolder(t, sc, "Folder") // nolint:staticcheck - command := getCreatePanelCommand(folder.ID, folder.UID, "Library Panel Name") + command := getCreatePanelCommand(folder.ID, "Library Panel Name") sc.reqContext.Req.Body = mockRequestBody(command) resp := sc.service.createHandler(sc.reqContext) result := validateAndUnMarshalResponse(t, resp) @@ -318,29 +317,27 @@ func TestLibraryElementDeletePermissions(t *testing.T) { func TestLibraryElementsWithMissingFolders(t *testing.T) { testScenario(t, "When a user tries to create a library panel in a folder that doesn't exist, it should fail", func(t *testing.T, sc scenarioContext) { - command := getCreatePanelCommand(0, "badFolderUID", "Library Panel Name") + command := getCreatePanelCommand(-100, "Library Panel Name") sc.reqContext.Req.Body = mockRequestBody(command) resp := sc.service.createHandler(sc.reqContext) - fmt.Println(string(resp.Body())) - require.Equal(t, 400, resp.Status()) + require.Equal(t, 404, resp.Status()) }) testScenario(t, "When a user tries to patch a library panel by moving it to a folder that doesn't exist, it should fail", func(t *testing.T, sc scenarioContext) { folder := createFolder(t, sc, "Folder") // nolint:staticcheck - command := getCreatePanelCommand(folder.ID, folder.UID, "Library Panel Name") + command := getCreatePanelCommand(folder.ID, "Library Panel Name") sc.reqContext.Req.Body = mockRequestBody(command) resp := sc.service.createHandler(sc.reqContext) result := validateAndUnMarshalResponse(t, resp) - folderUID := "badFolderUID" // nolint:staticcheck - cmd := model.PatchLibraryElementCommand{FolderID: -100, FolderUID: &folderUID, Version: 1, Kind: int64(model.PanelElement)} + cmd := model.PatchLibraryElementCommand{FolderID: -100, Version: 1, Kind: int64(model.PanelElement)} sc.ctx.Req = web.SetURLParams(sc.ctx.Req, map[string]string{":uid": result.Result.UID}) sc.reqContext.Req.Body = mockRequestBody(cmd) resp = sc.service.patchHandler(sc.reqContext) - require.Equal(t, 400, resp.Status()) + require.Equal(t, 404, resp.Status()) }) } @@ -370,7 +367,7 @@ func TestLibraryElementsGetPermissions(t *testing.T) { func(t *testing.T, sc scenarioContext) { folder := createFolder(t, sc, "Folder") // nolint:staticcheck - cmd := getCreatePanelCommand(folder.ID, folder.UID, "Library Panel") + cmd := getCreatePanelCommand(folder.ID, "Library Panel") sc.reqContext.Req.Body = mockRequestBody(cmd) resp := sc.service.createHandler(sc.reqContext) result := validateAndUnMarshalResponse(t, resp) @@ -421,7 +418,7 @@ func TestLibraryElementsGetAllPermissions(t *testing.T) { for i := 1; i <= 2; i++ { folder := createFolder(t, sc, fmt.Sprintf("Folder%d", i)) // nolint:staticcheck - cmd := getCreatePanelCommand(folder.ID, folder.UID, fmt.Sprintf("Library Panel %d", i)) + cmd := getCreatePanelCommand(folder.ID, fmt.Sprintf("Library Panel %d", i)) sc.reqContext.Req.Body = mockRequestBody(cmd) resp := sc.service.createHandler(sc.reqContext) result := validateAndUnMarshalResponse(t, resp) diff --git a/pkg/services/libraryelements/libraryelements_test.go b/pkg/services/libraryelements/libraryelements_test.go index 61a8fecf7c..1bb0b8abbc 100644 --- a/pkg/services/libraryelements/libraryelements_test.go +++ b/pkg/services/libraryelements/libraryelements_test.go @@ -89,7 +89,7 @@ func TestDeleteLibraryPanelsInFolder(t *testing.T) { Data: simplejson.NewFromAny(dashJSON), } // nolint:staticcheck - dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.ID, sc.folder.UID) + dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.ID) err := sc.service.ConnectElementsToDashboard(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, []string{sc.initialResult.Result.UID}, dashInDB.ID) require.NoError(t, err) @@ -106,7 +106,7 @@ func TestDeleteLibraryPanelsInFolder(t *testing.T) { scenarioWithPanel(t, "When an admin tries to delete a folder that contains disconnected elements, it should delete all disconnected elements too", func(t *testing.T, sc scenarioContext) { // nolint:staticcheck - command := getCreateVariableCommand(sc.folder.ID, sc.folder.UID, "query0") + command := getCreateVariableCommand(sc.folder.ID, "query0") sc.reqContext.Req.Body = mockRequestBody(command) resp := sc.service.createHandler(sc.reqContext) require.Equal(t, 200, resp.Status()) @@ -164,7 +164,7 @@ func TestGetLibraryPanelConnections(t *testing.T) { Data: simplejson.NewFromAny(dashJSON), } // nolint:staticcheck - dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.ID, sc.folder.UID) + dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.ID) err := sc.service.ConnectElementsToDashboard(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, []string{sc.initialResult.Result.UID}, dashInDB.ID) require.NoError(t, err) @@ -203,7 +203,6 @@ type libraryElement struct { OrgID int64 `json:"orgId"` // Deprecated: use FolderUID instead FolderID int64 `json:"folderId"` - FolderUID string `json:"folderUid"` UID string `json:"uid"` Name string `json:"name"` Kind int64 `json:"kind"` @@ -233,8 +232,8 @@ type libraryElementsSearchResult struct { PerPage int `json:"perPage"` } -func getCreatePanelCommand(folderID int64, folderUID string, name string) model.CreateLibraryElementCommand { - command := getCreateCommandWithModel(folderID, folderUID, name, model.PanelElement, []byte(` +func getCreatePanelCommand(folderID int64, name string) model.CreateLibraryElementCommand { + command := getCreateCommandWithModel(folderID, name, model.PanelElement, []byte(` { "datasource": "${DS_GDEV-TESTDATA}", "id": 1, @@ -247,8 +246,8 @@ func getCreatePanelCommand(folderID int64, folderUID string, name string) model. return command } -func getCreateVariableCommand(folderID int64, folderUID, name string) model.CreateLibraryElementCommand { - command := getCreateCommandWithModel(folderID, folderUID, name, model.VariableElement, []byte(` +func getCreateVariableCommand(folderID int64, name string) model.CreateLibraryElementCommand { + command := getCreateCommandWithModel(folderID, name, model.VariableElement, []byte(` { "datasource": "${DS_GDEV-TESTDATA}", "name": "query0", @@ -260,12 +259,12 @@ func getCreateVariableCommand(folderID int64, folderUID, name string) model.Crea return command } -func getCreateCommandWithModel(folderID int64, folderUID, name string, kind model.LibraryElementKind, byteModel []byte) model.CreateLibraryElementCommand { +func getCreateCommandWithModel(folderID int64, name string, kind model.LibraryElementKind, byteModel []byte) model.CreateLibraryElementCommand { command := model.CreateLibraryElementCommand{ - FolderUID: &folderUID, - Name: name, - Model: byteModel, - Kind: int64(kind), + FolderID: folderID, // nolint:staticcheck + Name: name, + Model: byteModel, + Kind: int64(kind), } return command @@ -282,10 +281,9 @@ type scenarioContext struct { log log.Logger } -func createDashboard(t *testing.T, sqlStore db.DB, user user.SignedInUser, dash *dashboards.Dashboard, folderID int64, folderUID string) *dashboards.Dashboard { +func createDashboard(t *testing.T, sqlStore db.DB, user user.SignedInUser, dash *dashboards.Dashboard, folderID int64) *dashboards.Dashboard { // nolint:staticcheck dash.FolderID = folderID - dash.FolderUID = folderUID dashItem := &dashboards.SaveDashboardDTO{ Dashboard: dash, Message: "", @@ -400,7 +398,7 @@ func scenarioWithPanel(t *testing.T, desc string, fn func(t *testing.T, sc scena testScenario(t, desc, func(t *testing.T, sc scenarioContext) { // nolint:staticcheck - command := getCreatePanelCommand(sc.folder.ID, sc.folder.UID, "Text - Library Panel") + command := getCreatePanelCommand(sc.folder.ID, "Text - Library Panel") sc.reqContext.Req.Body = mockRequestBody(command) resp := sc.service.createHandler(sc.reqContext) sc.initialResult = validateAndUnMarshalResponse(t, resp) diff --git a/pkg/services/libraryelements/model/model.go b/pkg/services/libraryelements/model/model.go index e53ef13816..df8c1abd7a 100644 --- a/pkg/services/libraryelements/model/model.go +++ b/pkg/services/libraryelements/model/model.go @@ -20,7 +20,6 @@ type LibraryElement struct { OrgID int64 `xorm:"org_id"` // Deprecated: use FolderUID instead FolderID int64 `xorm:"folder_id"` - FolderUID string `xorm:"folder_uid"` UID string `xorm:"uid"` Name string Kind int64 @@ -42,7 +41,6 @@ type LibraryElementWithMeta struct { OrgID int64 `xorm:"org_id"` // Deprecated: use FolderUID instead FolderID int64 `xorm:"folder_id"` - FolderUID string `xorm:"folder_uid"` UID string `xorm:"uid"` Name string Kind int64 @@ -55,6 +53,7 @@ type LibraryElementWithMeta struct { Updated time.Time FolderName string + FolderUID string `xorm:"folder_uid"` ConnectedDashboards int64 CreatedBy int64 UpdatedBy int64 @@ -218,14 +217,13 @@ type GetLibraryElementCommand struct { // SearchLibraryElementsQuery is the query used for searching for Elements type SearchLibraryElementsQuery struct { - PerPage int - Page int - SearchString string - SortDirection string - Kind int - TypeFilter string - ExcludeUID string - // Deprecated: use FolderFilterUIDs instead + PerPage int + Page int + SearchString string + SortDirection string + Kind int + TypeFilter string + ExcludeUID string FolderFilter string FolderFilterUIDs string } diff --git a/pkg/services/libraryelements/writers.go b/pkg/services/libraryelements/writers.go index 18781637fa..e1d470f6cd 100644 --- a/pkg/services/libraryelements/writers.go +++ b/pkg/services/libraryelements/writers.go @@ -82,7 +82,6 @@ type FolderFilter struct { func parseFolderFilter(query model.SearchLibraryElementsQuery) FolderFilter { folderIDs := make([]string, 0) folderUIDs := make([]string, 0) - // nolint:staticcheck hasFolderFilter := len(strings.TrimSpace(query.FolderFilter)) > 0 hasFolderFilterUID := len(strings.TrimSpace(query.FolderFilterUIDs)) > 0 @@ -101,7 +100,6 @@ func parseFolderFilter(query model.SearchLibraryElementsQuery) FolderFilter { if hasFolderFilter { result.includeGeneralFolder = false - // nolint:staticcheck folderIDs = strings.Split(query.FolderFilter, ",") metrics.MFolderIDsServiceCount.WithLabelValues(metrics.LibraryElements).Inc() // nolint:staticcheck diff --git a/pkg/services/librarypanels/librarypanels.go b/pkg/services/librarypanels/librarypanels.go index 91fd6107a5..244f925dde 100644 --- a/pkg/services/librarypanels/librarypanels.go +++ b/pkg/services/librarypanels/librarypanels.go @@ -42,7 +42,7 @@ func ProvideService(cfg *setting.Cfg, sqlStore db.DB, routeRegister routing.Rout // Service is a service for operating on library panels. type Service interface { ConnectLibraryPanelsForDashboard(c context.Context, signedInUser identity.Requester, dash *dashboards.Dashboard) error - ImportLibraryPanelsForDashboard(c context.Context, signedInUser identity.Requester, libraryPanels *simplejson.Json, panels []any, folderID int64, folderUID string) error + ImportLibraryPanelsForDashboard(c context.Context, signedInUser identity.Requester, libraryPanels *simplejson.Json, panels []any, folderID int64) error } type LibraryInfo struct { @@ -117,11 +117,11 @@ func connectLibraryPanelsRecursively(c context.Context, panels []any, libraryPan } // ImportLibraryPanelsForDashboard loops through all panels in dashboard JSON and creates any missing library panels in the database. -func (lps *LibraryPanelService) ImportLibraryPanelsForDashboard(c context.Context, signedInUser identity.Requester, libraryPanels *simplejson.Json, panels []any, folderID int64, folderUID string) error { - return importLibraryPanelsRecursively(c, lps.LibraryElementService, signedInUser, libraryPanels, panels, folderID, folderUID) +func (lps *LibraryPanelService) ImportLibraryPanelsForDashboard(c context.Context, signedInUser identity.Requester, libraryPanels *simplejson.Json, panels []any, folderID int64) error { + return importLibraryPanelsRecursively(c, lps.LibraryElementService, signedInUser, libraryPanels, panels, folderID) } -func importLibraryPanelsRecursively(c context.Context, service libraryelements.Service, signedInUser identity.Requester, libraryPanels *simplejson.Json, panels []any, folderID int64, folderUID string) error { +func importLibraryPanelsRecursively(c context.Context, service libraryelements.Service, signedInUser identity.Requester, libraryPanels *simplejson.Json, panels []any, folderID int64) error { for _, panel := range panels { panelAsJSON := simplejson.NewFromAny(panel) libraryPanel := panelAsJSON.Get("libraryPanel") @@ -132,7 +132,7 @@ func importLibraryPanelsRecursively(c context.Context, service libraryelements.S // we have a row if panelType == "row" { - err := importLibraryPanelsRecursively(c, service, signedInUser, libraryPanels, panelAsJSON.Get("panels").MustArray(), folderID, folderUID) + err := importLibraryPanelsRecursively(c, service, signedInUser, libraryPanels, panelAsJSON.Get("panels").MustArray(), folderID) if err != nil { return err } @@ -168,12 +168,11 @@ func importLibraryPanelsRecursively(c context.Context, service libraryelements.S metrics.MFolderIDsServiceCount.WithLabelValues(metrics.LibraryPanels).Inc() var cmd = model.CreateLibraryElementCommand{ - FolderID: folderID, // nolint:staticcheck - FolderUID: &folderUID, - Name: name, - Model: Model, - Kind: int64(model.PanelElement), - UID: UID, + FolderID: folderID, // nolint:staticcheck + Name: name, + Model: Model, + Kind: int64(model.PanelElement), + UID: UID, } _, err = service.CreateElement(c, signedInUser, cmd) if err != nil { diff --git a/pkg/services/librarypanels/librarypanels_test.go b/pkg/services/librarypanels/librarypanels_test.go index 577ee20aeb..dae0821acf 100644 --- a/pkg/services/librarypanels/librarypanels_test.go +++ b/pkg/services/librarypanels/librarypanels_test.go @@ -86,7 +86,8 @@ func TestConnectLibraryPanelsForDashboard(t *testing.T) { Title: "Testing ConnectLibraryPanelsForDashboard", Data: simplejson.NewFromAny(dashJSON), } - dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash) + // nolint:staticcheck + dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.ID) err := sc.service.ConnectLibraryPanelsForDashboard(sc.ctx, sc.user, dashInDB) require.NoError(t, err) @@ -100,7 +101,8 @@ func TestConnectLibraryPanelsForDashboard(t *testing.T) { scenarioWithLibraryPanel(t, "When an admin tries to store a dashboard with library panels inside and outside of rows, it should connect all", func(t *testing.T, sc scenarioContext) { cmd := model.CreateLibraryElementCommand{ - Name: "Outside row", + FolderID: sc.initialResult.Result.FolderID, // nolint:staticcheck + Name: "Outside row", Model: []byte(` { "datasource": "${DS_GDEV-TESTDATA}", @@ -110,8 +112,7 @@ func TestConnectLibraryPanelsForDashboard(t *testing.T) { "description": "A description" } `), - Kind: int64(model.PanelElement), - FolderUID: &sc.folder.UID, + Kind: int64(model.PanelElement), } outsidePanel, err := sc.elementService.CreateElement(sc.ctx, sc.user, cmd) require.NoError(t, err) @@ -184,7 +185,8 @@ func TestConnectLibraryPanelsForDashboard(t *testing.T) { Title: "Testing ConnectLibraryPanelsForDashboard", Data: simplejson.NewFromAny(dashJSON), } - dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash) + // nolint:staticcheck + dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.ID) err = sc.service.ConnectLibraryPanelsForDashboard(sc.ctx, sc.user, dashInDB) require.NoError(t, err) @@ -230,7 +232,8 @@ func TestConnectLibraryPanelsForDashboard(t *testing.T) { Title: "Testing ConnectLibraryPanelsForDashboard", Data: simplejson.NewFromAny(dashJSON), } - dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash) + // nolint:staticcheck + dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.ID) err := sc.service.ConnectLibraryPanelsForDashboard(sc.ctx, sc.user, dashInDB) require.EqualError(t, err, errLibraryPanelHeaderUIDMissing.Error()) @@ -239,7 +242,8 @@ func TestConnectLibraryPanelsForDashboard(t *testing.T) { scenarioWithLibraryPanel(t, "When an admin tries to store a dashboard with unused/removed library panels, it should disconnect unused/removed library panels", func(t *testing.T, sc scenarioContext) { unused, err := sc.elementService.CreateElement(sc.ctx, sc.user, model.CreateLibraryElementCommand{ - Name: "Unused Libray Panel", + FolderID: sc.folder.ID, // nolint:staticcheck + Name: "Unused Libray Panel", Model: []byte(` { "datasource": "${DS_GDEV-TESTDATA}", @@ -249,8 +253,7 @@ func TestConnectLibraryPanelsForDashboard(t *testing.T) { "description": "Unused description" } `), - Kind: int64(model.PanelElement), - FolderUID: &sc.folder.UID, + Kind: int64(model.PanelElement), }) require.NoError(t, err) dashJSON := map[string]any{ @@ -286,7 +289,8 @@ func TestConnectLibraryPanelsForDashboard(t *testing.T) { Title: "Testing ConnectLibraryPanelsForDashboard", Data: simplejson.NewFromAny(dashJSON), } - dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash) + // nolint:staticcheck + dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.ID) err = sc.elementService.ConnectElementsToDashboard(sc.ctx, sc.user, []string{sc.initialResult.Result.UID}, dashInDB.ID) require.NoError(t, err) @@ -395,7 +399,7 @@ func TestImportLibraryPanelsForDashboard(t *testing.T) { require.EqualError(t, err, model.ErrLibraryElementNotFound.Error()) - err = sc.service.ImportLibraryPanelsForDashboard(sc.ctx, sc.user, simplejson.NewFromAny(libraryElements), panels, 0, "") + err = sc.service.ImportLibraryPanelsForDashboard(sc.ctx, sc.user, simplejson.NewFromAny(libraryElements), panels, 0) require.NoError(t, err) element, err := sc.elementService.GetElement(sc.ctx, sc.user, @@ -436,14 +440,14 @@ func TestImportLibraryPanelsForDashboard(t *testing.T) { require.NoError(t, err) // nolint:staticcheck - err = sc.service.ImportLibraryPanelsForDashboard(sc.ctx, sc.user, simplejson.New(), panels, sc.folder.ID, sc.folder.UID) + err = sc.service.ImportLibraryPanelsForDashboard(sc.ctx, sc.user, simplejson.New(), panels, sc.folder.ID) require.NoError(t, err) element, err := sc.elementService.GetElement(sc.ctx, sc.user, model.GetLibraryElementCommand{UID: existingUID, FolderName: dashboards.RootFolderName}) require.NoError(t, err) var expected = getExpected(t, element, existingUID, existingName, sc.initialResult.Result.Model) - expected.FolderUID = sc.initialResult.Result.FolderUID + expected.FolderID = sc.initialResult.Result.FolderID expected.Description = sc.initialResult.Result.Description expected.Meta.FolderUID = sc.folder.UID expected.Meta.FolderName = sc.folder.Title @@ -552,7 +556,7 @@ func TestImportLibraryPanelsForDashboard(t *testing.T) { _, err = sc.elementService.GetElement(sc.ctx, sc.user, model.GetLibraryElementCommand{UID: insideUID, FolderName: dashboards.RootFolderName}) require.EqualError(t, err, model.ErrLibraryElementNotFound.Error()) - err = sc.service.ImportLibraryPanelsForDashboard(sc.ctx, sc.user, simplejson.NewFromAny(libraryElements), panels, 0, "") + err = sc.service.ImportLibraryPanelsForDashboard(sc.ctx, sc.user, simplejson.NewFromAny(libraryElements), panels, 0) require.NoError(t, err) element, err := sc.elementService.GetElement(sc.ctx, sc.user, model.GetLibraryElementCommand{UID: outsideUID, FolderName: dashboards.RootFolderName}) @@ -574,11 +578,9 @@ func TestImportLibraryPanelsForDashboard(t *testing.T) { } type libraryPanel struct { - ID int64 - OrgID int64 - // Deprecated: use FolderUID instead + ID int64 + OrgID int64 FolderID int64 - FolderUID string UID string Name string Type string @@ -614,7 +616,6 @@ type libraryElement struct { ID int64 `json:"id"` OrgID int64 `json:"orgId"` FolderID int64 `json:"folderId"` - FolderUID string `json:"folderUid"` UID string `json:"uid"` Name string `json:"name"` Kind int64 `json:"kind"` @@ -648,6 +649,7 @@ func toLibraryElement(t *testing.T, res model.LibraryElementDTO) libraryElement return libraryElement{ ID: res.ID, OrgID: res.OrgID, + FolderID: res.FolderID, // nolint:staticcheck UID: res.UID, Name: res.Name, Type: res.Type, @@ -713,7 +715,9 @@ func getExpected(t *testing.T, res model.LibraryElementDTO, UID string, name str } } -func createDashboard(t *testing.T, sqlStore db.DB, user *user.SignedInUser, dash *dashboards.Dashboard) *dashboards.Dashboard { +func createDashboard(t *testing.T, sqlStore db.DB, user *user.SignedInUser, dash *dashboards.Dashboard, folderID int64) *dashboards.Dashboard { + // nolint:staticcheck + dash.FolderID = folderID dashItem := &dashboards.SaveDashboardDTO{ Dashboard: dash, Message: "", @@ -770,9 +774,8 @@ func scenarioWithLibraryPanel(t *testing.T, desc string, fn func(t *testing.T, s testScenario(t, desc, func(t *testing.T, sc scenarioContext) { command := model.CreateLibraryElementCommand{ - FolderID: sc.folder.ID, // nolint:staticcheck - FolderUID: &sc.folder.UID, - Name: "Text - Library Panel", + FolderID: sc.folder.ID, // nolint:staticcheck + Name: "Text - Library Panel", Model: []byte(` { "datasource": "${DS_GDEV-TESTDATA}", @@ -794,7 +797,7 @@ func scenarioWithLibraryPanel(t *testing.T, desc string, fn func(t *testing.T, s Result: libraryPanel{ ID: resp.ID, OrgID: resp.OrgID, - FolderUID: resp.FolderUID, + FolderID: resp.FolderID, // nolint:staticcheck UID: resp.UID, Name: resp.Name, Type: resp.Type, diff --git a/pkg/services/sqlstore/migrations/libraryelements.go b/pkg/services/sqlstore/migrations/libraryelements.go index 88a6a3d023..3215cb085b 100644 --- a/pkg/services/sqlstore/migrations/libraryelements.go +++ b/pkg/services/sqlstore/migrations/libraryelements.go @@ -61,26 +61,4 @@ func addLibraryElementsMigrations(mg *migrator.Migrator) { mg.AddMigration("alter library_element model to mediumtext", migrator.NewRawSQLMigration(""). Mysql("ALTER TABLE library_element MODIFY model MEDIUMTEXT NOT NULL;")) - - q := `UPDATE library_element - SET folder_uid = dashboard.uid - FROM dashboard - WHERE library_element.folder_id = dashboard.folder_id AND library_element.org_id = dashboard.org_id` - - if mg.Dialect.DriverName() == migrator.MySQL { - q = `UPDATE library_element - SET folder_uid = ( - SELECT dashboard.uid - FROM dashboard - WHERE library_element.folder_id = dashboard.folder_id AND library_element.org_id = dashboard.org_id - )` - } - - mg.AddMigration("add library_element folder uid", migrator.NewAddColumnMigration(libraryElementsV1, &migrator.Column{ - Name: "folder_uid", Type: migrator.DB_NVarchar, Length: 40, Nullable: true, - })) - - mg.AddMigration("populate library_element folder_uid", migrator.NewRawSQLMigration(q)) - - mg.AddMigration("add index library_element org_id-folder_uid-name-kind", migrator.NewAddIndexMigration(libraryElementsV1, &migrator.Index{Cols: []string{"org_id", "folder_uid", "name", "kind"}, Type: migrator.UniqueIndex})) } From 8ad367e4adefabd95283a6bf467f5dbd40a9899c Mon Sep 17 00:00:00 2001 From: Santiago Date: Fri, 1 Mar 2024 13:28:08 -0300 Subject: [PATCH 0340/1406] Chore: Remove redundant error check (#83769) --- pkg/services/ngalert/notifier/compat.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/pkg/services/ngalert/notifier/compat.go b/pkg/services/ngalert/notifier/compat.go index b30f43d8f6..b07ae172ba 100644 --- a/pkg/services/ngalert/notifier/compat.go +++ b/pkg/services/ngalert/notifier/compat.go @@ -66,9 +66,6 @@ func PostableToGettableGrafanaReceiver(r *apimodels.PostableGrafanaReceiver, pro for k, v := range r.SecureSettings { decryptedValue := decryptFn(v) - if err != nil { - return apimodels.GettableGrafanaReceiver{}, err - } if decryptedValue == "" { continue } else { From 2964901ea62de904bbd48122d5b6e364dad26401 Mon Sep 17 00:00:00 2001 From: Santiago Date: Fri, 1 Mar 2024 14:04:20 -0300 Subject: [PATCH 0341/1406] Chore: Fix typo in template not found error message (#83778) --- .../unified/components/receivers/DuplicateTemplateView.tsx | 2 +- .../alerting/unified/components/receivers/EditTemplateView.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/features/alerting/unified/components/receivers/DuplicateTemplateView.tsx b/public/app/features/alerting/unified/components/receivers/DuplicateTemplateView.tsx index 96489e6c2e..91f28e94c7 100644 --- a/public/app/features/alerting/unified/components/receivers/DuplicateTemplateView.tsx +++ b/public/app/features/alerting/unified/components/receivers/DuplicateTemplateView.tsx @@ -20,7 +20,7 @@ export const DuplicateTemplateView = ({ config, templateName, alertManagerSource if (!template) { return ( - Sorry, this template does not seem to exists. + Sorry, this template does not seem to exist. ); } diff --git a/public/app/features/alerting/unified/components/receivers/EditTemplateView.tsx b/public/app/features/alerting/unified/components/receivers/EditTemplateView.tsx index b8271e2d98..062d2f58be 100644 --- a/public/app/features/alerting/unified/components/receivers/EditTemplateView.tsx +++ b/public/app/features/alerting/unified/components/receivers/EditTemplateView.tsx @@ -18,7 +18,7 @@ export const EditTemplateView = ({ config, templateName, alertManagerSourceName if (!template) { return ( - Sorry, this template does not seem to exists. + Sorry, this template does not seem to exist. ); } From 8b551b08f9034a6b75ece345237da1d3394d100d Mon Sep 17 00:00:00 2001 From: Eric Leijonmarck Date: Fri, 1 Mar 2024 17:05:59 +0000 Subject: [PATCH 0342/1406] TeamLBAC: Change to public preview (#83761) change to public preview --- .../configure-grafana/feature-toggles/index.md | 2 +- pkg/services/featuremgmt/registry.go | 4 ++-- pkg/services/featuremgmt/toggles_gen.csv | 2 +- pkg/services/featuremgmt/toggles_gen.go | 2 +- pkg/services/featuremgmt/toggles_gen.json | 11 +++++++---- 5 files changed, 12 insertions(+), 9 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 beffe96e6f..7571afd19c 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -88,6 +88,7 @@ Some features are enabled by default. You can disable these feature by setting t | `formatString` | Enable format string transformer | | `transformationsVariableSupport` | Allows using variables in transformations | | `cloudWatchBatchQueries` | Runs CloudWatch metrics queries as separate batches | +| `teamHttpHeaders` | Enables Team LBAC for datasources to apply team headers to the client requests | | `awsDatasourcesNewFormStyling` | Applies new form styling for configuration and query editors in AWS plugins | | `managedPluginsInstall` | Install managed plugins directly from plugins catalog | | `addFieldFromCalculationStatFunctions` | Add cumulative and window functions to the add field from calculation transformation | @@ -152,7 +153,6 @@ Experimental features might be changed or removed without prior notice. | `enableNativeHTTPHistogram` | Enables native HTTP Histograms | | `kubernetesPlaylists` | Use the kubernetes API in the frontend for playlists, and route /api/playlist requests to k8s | | `kubernetesSnapshots` | Routes snapshot requests from /api to the /apis endpoint | -| `teamHttpHeaders` | Enables datasources to apply team headers to the client requests | | `cachingOptimizeSerializationMemoryUsage` | If enabled, the caching backend gradually serializes query responses for the cache, comparing against the configured `[caching]max_value_mb` value as it goes. This can can help prevent Grafana from running out of memory while attempting to cache very large query responses. | | `prometheusPromQAIL` | Prometheus and AI/ML to assist users in creating a query | | `alertmanagerRemoteSecondary` | Enable Grafana to sync configuration and state with a remote Alertmanager. | diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 9a7a64a48f..f204bcc5c6 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -847,8 +847,8 @@ var ( }, { Name: "teamHttpHeaders", - Description: "Enables datasources to apply team headers to the client requests", - Stage: FeatureStageExperimental, + Description: "Enables Team LBAC for datasources to apply team headers to the client requests", + Stage: FeatureStagePublicPreview, FrontendOnly: false, Owner: identityAccessTeam, }, diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index bbfff4aa5f..b177d1d9f5 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -112,7 +112,7 @@ kubernetesQueryServiceRewrite,experimental,@grafana/grafana-app-platform-squad,t cloudWatchBatchQueries,preview,@grafana/aws-datasources,false,false,false recoveryThreshold,GA,@grafana/alerting-squad,false,true,false lokiStructuredMetadata,GA,@grafana/observability-logs,false,false,false -teamHttpHeaders,experimental,@grafana/identity-access-team,false,false,false +teamHttpHeaders,preview,@grafana/identity-access-team,false,false,false awsDatasourcesNewFormStyling,preview,@grafana/aws-datasources,false,false,true cachingOptimizeSerializationMemoryUsage,experimental,@grafana/grafana-operator-experience-squad,false,false,false panelTitleSearchInV1,experimental,@grafana/backend-platform,true,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 6b6f2fd8d2..2a0003e4a9 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -460,7 +460,7 @@ const ( FlagLokiStructuredMetadata = "lokiStructuredMetadata" // FlagTeamHttpHeaders - // Enables datasources to apply team headers to the client requests + // Enables Team LBAC for datasources to apply team headers to the client requests FlagTeamHttpHeaders = "teamHttpHeaders" // FlagAwsDatasourcesNewFormStyling diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 08bea50b84..eb00277196 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -19,12 +19,15 @@ { "metadata": { "name": "teamHttpHeaders", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "resourceVersion": "1709290509653", + "creationTimestamp": "2024-02-16T18:36:28Z", + "annotations": { + "grafana.app/updatedTimestamp": "2024-03-01 10:55:09.653587 +0000 UTC" + } }, "spec": { - "description": "Enables datasources to apply team headers to the client requests", - "stage": "experimental", + "description": "Enables Team LBAC for datasources to apply team headers to the client requests", + "stage": "preview", "codeowner": "@grafana/identity-access-team" } }, From 675b7debe7f115b54968038866ab0905db37ffc4 Mon Sep 17 00:00:00 2001 From: Gilles De Mey Date: Fri, 1 Mar 2024 18:08:17 +0100 Subject: [PATCH 0343/1406] Alerting: Assume rule not found when everything is undefined (#83774) --- public/app/features/alerting/unified/RuleViewer.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/public/app/features/alerting/unified/RuleViewer.tsx b/public/app/features/alerting/unified/RuleViewer.tsx index 3a57a2088e..9d3112a67b 100644 --- a/public/app/features/alerting/unified/RuleViewer.tsx +++ b/public/app/features/alerting/unified/RuleViewer.tsx @@ -74,7 +74,12 @@ const RuleViewerV2Wrapper = (props: RuleViewerProps) => { ); } - return null; + // if we get here assume we can't find the rule + return ( + + + + ); }; interface ErrorMessageProps { From fc10600ca5a79c68d739b55b33b06b8491da9911 Mon Sep 17 00:00:00 2001 From: Gilles De Mey Date: Fri, 1 Mar 2024 18:08:42 +0100 Subject: [PATCH 0344/1406] Alerting: Fix editing Grafana folder via alert rule editor (#83771) --- .../unified/components/rule-editor/GrafanaEvaluationBehavior.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/public/app/features/alerting/unified/components/rule-editor/GrafanaEvaluationBehavior.tsx b/public/app/features/alerting/unified/components/rule-editor/GrafanaEvaluationBehavior.tsx index 22953dbb92..2dfca5f49c 100644 --- a/public/app/features/alerting/unified/components/rule-editor/GrafanaEvaluationBehavior.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/GrafanaEvaluationBehavior.tsx @@ -137,6 +137,7 @@ function FolderGroupAndEvaluationInterval({ closeEditGroupModal()} intervalEditOnly hideFolder={true} From 582fd53fac02e5fb73671a5acc6d2257a3062198 Mon Sep 17 00:00:00 2001 From: Gilles De Mey Date: Fri, 1 Mar 2024 18:09:14 +0100 Subject: [PATCH 0345/1406] Alerting: Implement correct RBAC checks for creating new notification templates (#83767) --- .../contact-points/ContactPoints.test.tsx | 10 ++++++++++ .../components/contact-points/ContactPoints.tsx | 16 +++++++++++++--- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/public/app/features/alerting/unified/components/contact-points/ContactPoints.test.tsx b/public/app/features/alerting/unified/components/contact-points/ContactPoints.test.tsx index fde20c0fad..c6fd8c71ca 100644 --- a/public/app/features/alerting/unified/components/contact-points/ContactPoints.test.tsx +++ b/public/app/features/alerting/unified/components/contact-points/ContactPoints.test.tsx @@ -127,6 +127,11 @@ describe('contact points', () => { const deleteButton = await screen.queryByRole('menuitem', { name: 'delete' }); expect(deleteButton).toBeDisabled(); } + + // check buttons in Notification Templates + const notificationTemplatesTab = screen.getByRole('tab', { name: 'Tab Notification Templates' }); + await userEvent.click(notificationTemplatesTab); + expect(screen.getByRole('link', { name: 'Add notification template' })).toHaveAttribute('aria-disabled', 'true'); }); it('should call delete when clicked and not disabled', async () => { @@ -326,6 +331,11 @@ describe('contact points', () => { const viewProvisioned = screen.getByRole('link', { name: 'view-action' }); expect(viewProvisioned).toBeInTheDocument(); expect(viewProvisioned).not.toBeDisabled(); + + // check buttons in Notification Templates + const notificationTemplatesTab = screen.getByRole('tab', { name: 'Tab Notification Templates' }); + await userEvent.click(notificationTemplatesTab); + expect(screen.queryByRole('link', { name: 'Add notification template' })).not.toBeInTheDocument(); }); }); }); diff --git a/public/app/features/alerting/unified/components/contact-points/ContactPoints.tsx b/public/app/features/alerting/unified/components/contact-points/ContactPoints.tsx index e8bb3de093..e9ff52e3f0 100644 --- a/public/app/features/alerting/unified/components/contact-points/ContactPoints.tsx +++ b/public/app/features/alerting/unified/components/contact-points/ContactPoints.tsx @@ -81,6 +81,9 @@ const ContactPoints = () => { const [exportContactPointsSupported, exportContactPointsAllowed] = useAlertmanagerAbility( AlertmanagerAction.ExportContactPoint ); + const [createTemplateSupported, createTemplateAllowed] = useAlertmanagerAbility( + AlertmanagerAction.CreateNotificationTemplate + ); const [DeleteModal, showDeleteModal] = useDeleteContactPointModal(deleteTrigger, updateAlertmanagerState.isLoading); const [ExportDrawer, showExportDrawer] = useExportContactPoint(); @@ -177,9 +180,16 @@ const ContactPoints = () => { Create notification templates to customize your notifications. - - Add notification template - + {createTemplateSupported && ( + + Add notification template + + )} From 96127dce6211ee99c9c3a254c23134d9473c2255 Mon Sep 17 00:00:00 2001 From: George Robinson Date: Fri, 1 Mar 2024 17:17:55 +0000 Subject: [PATCH 0346/1406] Alerting: Fix bug in screenshot service using incorrect limit (#83786) This commit fixes a bug in the screenshot service where [alerting].concurrent_render_limit was used instead of [rendering].concurrent_render_request_limit, as in the docs. --- pkg/services/screenshot/screenshot.go | 2 +- pkg/services/screenshot/screenshot_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/services/screenshot/screenshot.go b/pkg/services/screenshot/screenshot.go index 7f351184c8..47e29fdccd 100644 --- a/pkg/services/screenshot/screenshot.go +++ b/pkg/services/screenshot/screenshot.go @@ -122,7 +122,7 @@ func (s *HeadlessScreenshotService) Take(ctx context.Context, opts ScreenshotOpt Width: opts.Width, Height: opts.Height, Theme: opts.Theme, - ConcurrentLimit: s.cfg.AlertingRenderLimit, + ConcurrentLimit: s.cfg.RendererConcurrentRequestLimit, Path: u.String(), } diff --git a/pkg/services/screenshot/screenshot_test.go b/pkg/services/screenshot/screenshot_test.go index d1c665dffa..392c45482e 100644 --- a/pkg/services/screenshot/screenshot_test.go +++ b/pkg/services/screenshot/screenshot_test.go @@ -54,7 +54,7 @@ func TestHeadlessScreenshotService(t *testing.T) { Height: DefaultHeight, Theme: DefaultTheme, Path: "d-solo/foo/bar?from=now-6h&orgId=2&panelId=4&to=now-2h", - ConcurrentLimit: cfg.AlertingRenderLimit, + ConcurrentLimit: cfg.RendererConcurrentRequestLimit, } opts.From = "now-6h" From 33cb4f9bf4ef4ef33047e7fdcbffb02345194835 Mon Sep 17 00:00:00 2001 From: Kyle Cunningham Date: Fri, 1 Mar 2024 11:19:21 -0600 Subject: [PATCH 0347/1406] Table: Preserve filtered value state (#83631) * Hoist state * codeincarnate/table-filter-state/ lint --------- Co-authored-by: jev forsberg Co-authored-by: nmarrs --- .../src/components/Table/Filter.tsx | 18 +++++++++++++-- .../src/components/Table/FilterList.tsx | 22 ++++++++++++++----- .../src/components/Table/FilterPopup.tsx | 18 ++++++++++++++- 3 files changed, 50 insertions(+), 8 deletions(-) diff --git a/packages/grafana-ui/src/components/Table/Filter.tsx b/packages/grafana-ui/src/components/Table/Filter.tsx index 5f4474782c..dd5b4ca8ba 100644 --- a/packages/grafana-ui/src/components/Table/Filter.tsx +++ b/packages/grafana-ui/src/components/Table/Filter.tsx @@ -1,12 +1,13 @@ import { css, cx } from '@emotion/css'; import React, { useCallback, useMemo, useRef, useState } from 'react'; -import { Field, GrafanaTheme2 } from '@grafana/data'; +import { Field, GrafanaTheme2, SelectableValue } from '@grafana/data'; import { Popover } from '..'; import { useStyles2 } from '../../themes'; import { Icon } from '../Icon/Icon'; +import { REGEX_OPERATOR } from './FilterList'; import { FilterPopup } from './FilterPopup'; import { TableStyles } from './styles'; @@ -23,6 +24,8 @@ export const Filter = ({ column, field, tableStyles }: Props) => { const filterEnabled = useMemo(() => Boolean(column.filterValue), [column.filterValue]); const onShowPopover = useCallback(() => setPopoverVisible(true), [setPopoverVisible]); const onClosePopover = useCallback(() => setPopoverVisible(false), [setPopoverVisible]); + const [searchFilter, setSearchFilter] = useState(''); + const [operator, setOperator] = useState>(REGEX_OPERATOR); if (!field || !field.config.custom?.filterable) { return null; @@ -37,7 +40,18 @@ export const Filter = ({ column, field, tableStyles }: Props) => { {isPopoverVisible && ref.current && ( } + content={ + + } placement="bottom-start" referenceElement={ref.current} show diff --git a/packages/grafana-ui/src/components/Table/FilterList.tsx b/packages/grafana-ui/src/components/Table/FilterList.tsx index 7d24c3847d..9a65476e6f 100644 --- a/packages/grafana-ui/src/components/Table/FilterList.tsx +++ b/packages/grafana-ui/src/components/Table/FilterList.tsx @@ -1,5 +1,5 @@ import { css, cx } from '@emotion/css'; -import React, { useCallback, useMemo, useState } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { FixedSizeList as List } from 'react-window'; import { GrafanaTheme2, formattedValueToString, getValueFormat, SelectableValue } from '@grafana/data'; @@ -13,6 +13,10 @@ interface Props { onChange: (options: SelectableValue[]) => void; caseSensitive?: boolean; showOperators?: boolean; + searchFilter: string; + setSearchFilter: (value: string) => void; + operator: SelectableValue; + setOperator: (item: SelectableValue) => void; } const ITEM_HEIGHT = 28; @@ -33,7 +37,7 @@ const operatorSelectableValues: { [key: string]: SelectableValue } = { }, }; const OPERATORS = Object.values(operatorSelectableValues); -const REGEX_OPERATOR = operatorSelectableValues['Contains']; +export const REGEX_OPERATOR = operatorSelectableValues['Contains']; const XPR_OPERATOR = operatorSelectableValues['Expression']; const comparableValue = (value: string): string | number | Date | boolean => { @@ -61,9 +65,17 @@ const comparableValue = (value: string): string | number | Date | boolean => { return value; }; -export const FilterList = ({ options, values, caseSensitive, showOperators, onChange }: Props) => { - const [operator, setOperator] = useState>(REGEX_OPERATOR); - const [searchFilter, setSearchFilter] = useState(''); +export const FilterList = ({ + options, + values, + caseSensitive, + showOperators, + onChange, + searchFilter, + setSearchFilter, + operator, + setOperator, +}: Props) => { const regex = useMemo(() => new RegExp(searchFilter, caseSensitive ? undefined : 'i'), [searchFilter, caseSensitive]); const items = useMemo( () => diff --git a/packages/grafana-ui/src/components/Table/FilterPopup.tsx b/packages/grafana-ui/src/components/Table/FilterPopup.tsx index 422dfe7a4f..fcb4803f9b 100644 --- a/packages/grafana-ui/src/components/Table/FilterPopup.tsx +++ b/packages/grafana-ui/src/components/Table/FilterPopup.tsx @@ -15,9 +15,21 @@ interface Props { tableStyles: TableStyles; onClose: () => void; field?: Field; + searchFilter: string; + setSearchFilter: (value: string) => void; + operator: SelectableValue; + setOperator: (item: SelectableValue) => void; } -export const FilterPopup = ({ column: { preFilteredRows, filterValue, setFilter }, onClose, field }: Props) => { +export const FilterPopup = ({ + column: { preFilteredRows, filterValue, setFilter }, + onClose, + field, + searchFilter, + setSearchFilter, + operator, + setOperator, +}: Props) => { const theme = useTheme2(); const uniqueValues = useMemo(() => calculateUniqueFieldValues(preFilteredRows, field), [preFilteredRows, field]); const options = useMemo(() => valuesToOptions(uniqueValues), [uniqueValues]); @@ -73,6 +85,10 @@ export const FilterPopup = ({ column: { preFilteredRows, filterValue, setFilter options={options} caseSensitive={matchCase} showOperators={true} + searchFilter={searchFilter} + setSearchFilter={setSearchFilter} + operator={operator} + setOperator={setOperator} /> From 886d8fae360257ffa7f723cdf44c528fd0addfa5 Mon Sep 17 00:00:00 2001 From: Kyle Cunningham Date: Fri, 1 Mar 2024 11:20:14 -0600 Subject: [PATCH 0348/1406] Table Panel: Improve text wrapping on hover (#83642) * Fix up text wrapping * Make sure there are basic cell styles * codeincarnate/text-wrapping-hover-pt2/ run prettier * Add comment for conditional * Update condition --------- Co-authored-by: jev forsberg --- .../src/components/Table/DefaultCell.tsx | 25 +++++++++++++++---- .../grafana-ui/src/components/Table/styles.ts | 9 ++++--- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/packages/grafana-ui/src/components/Table/DefaultCell.tsx b/packages/grafana-ui/src/components/Table/DefaultCell.tsx index 186455b794..90628ee0d6 100644 --- a/packages/grafana-ui/src/components/Table/DefaultCell.tsx +++ b/packages/grafana-ui/src/components/Table/DefaultCell.tsx @@ -29,6 +29,8 @@ export const DefaultCell = (props: TableCellProps) => { const [hover, setHover] = useState(false); let value: string | ReactElement; + const OG_TWEET_LENGTH = 140; // 🙏 + const onMouseLeave = () => { setHover(false); }; @@ -49,7 +51,9 @@ export const DefaultCell = (props: TableCellProps) => { const isStringValue = typeof value === 'string'; - const cellStyle = getCellStyle(tableStyles, cellOptions, displayValue, inspectEnabled, isStringValue); + // Text should wrap when the content length is less than or equal to the length of an OG tweet and it contains whitespace + const textShouldWrap = displayValue.text.length <= OG_TWEET_LENGTH && /\s/.test(displayValue.text); + const cellStyle = getCellStyle(tableStyles, cellOptions, displayValue, inspectEnabled, isStringValue, textShouldWrap); if (isStringValue) { let justifyContent = cellProps.style?.justifyContent; @@ -99,7 +103,8 @@ function getCellStyle( cellOptions: TableCellOptions, displayValue: DisplayValue, disableOverflowOnHover = false, - isStringValue = false + isStringValue = false, + shouldWrapText = false ) { // How much to darken elements depends upon if we're in dark mode const darkeningFactor = tableStyles.theme.isDark ? 1 : -0.7; @@ -128,13 +133,23 @@ function getCellStyle( // If we have definied colors return those styles // Otherwise we return default styles if (textColor !== undefined || bgColor !== undefined) { - return tableStyles.buildCellContainerStyle(textColor, bgColor, !disableOverflowOnHover, isStringValue); + return tableStyles.buildCellContainerStyle( + textColor, + bgColor, + !disableOverflowOnHover, + isStringValue, + shouldWrapText + ); } if (isStringValue) { - return disableOverflowOnHover ? tableStyles.cellContainerTextNoOverflow : tableStyles.cellContainerText; + return disableOverflowOnHover + ? tableStyles.buildCellContainerStyle(undefined, undefined, false, true, shouldWrapText) + : tableStyles.buildCellContainerStyle(undefined, undefined, true, true, shouldWrapText); } else { - return disableOverflowOnHover ? tableStyles.cellContainerNoOverflow : tableStyles.cellContainer; + return disableOverflowOnHover + ? tableStyles.buildCellContainerStyle(undefined, undefined, false, shouldWrapText) + : tableStyles.buildCellContainerStyle(undefined, undefined, true, false, shouldWrapText); } } diff --git a/packages/grafana-ui/src/components/Table/styles.ts b/packages/grafana-ui/src/components/Table/styles.ts index ebd7d0e178..2446d96d9c 100644 --- a/packages/grafana-ui/src/components/Table/styles.ts +++ b/packages/grafana-ui/src/components/Table/styles.ts @@ -15,7 +15,8 @@ export function useTableStyles(theme: GrafanaTheme2, cellHeightOption: TableCell color?: string, background?: string, overflowOnHover?: boolean, - asCellText?: boolean + asCellText?: boolean, + textShouldWrap?: boolean ) => { return css({ label: overflowOnHover ? 'cellContainerOverflow' : 'cellContainerNoOverflow', @@ -48,10 +49,10 @@ export function useTableStyles(theme: GrafanaTheme2, cellHeightOption: TableCell '&:hover': { overflow: overflowOnHover ? 'visible' : undefined, - width: overflowOnHover ? 'auto' : undefined, - height: overflowOnHover ? 'auto' : `${rowHeight - 1}px`, + width: textShouldWrap ? 'auto' : 'auto !important', + height: textShouldWrap ? 'auto !important' : `${rowHeight - 1}px`, minHeight: `${rowHeight - 1}px`, - wordBreak: overflowOnHover ? 'break-word' : undefined, + wordBreak: textShouldWrap ? 'break-word' : undefined, whiteSpace: overflowOnHover ? 'normal' : 'nowrap', boxShadow: overflowOnHover ? `0 0 2px ${theme.colors.primary.main}` : undefined, background: overflowOnHover ? background ?? theme.components.table.rowHoverBackground : undefined, From c59ebfc60f5aec1eccfe07da7a6d948782ddafdb Mon Sep 17 00:00:00 2001 From: Jack Westbrook Date: Fri, 1 Mar 2024 18:29:39 +0100 Subject: [PATCH 0349/1406] Fix: Cache busting of plugins module.js file (#83763) fix(plugins): make sure extractPath regex matches with and without leading slash --- public/app/features/plugins/loader/cache.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/plugins/loader/cache.ts b/public/app/features/plugins/loader/cache.ts index 739c4c5807..3db74fd2a7 100644 --- a/public/app/features/plugins/loader/cache.ts +++ b/public/app/features/plugins/loader/cache.ts @@ -35,7 +35,7 @@ export function resolveWithCache(url: string, defaultBust = initializedAt): stri } function extractPath(address: string): string | undefined { - const match = /\/.+\/(plugins\/.+\/module)\.js/i.exec(address); + const match = /\/?.+\/(plugins\/.+\/module)\.js/i.exec(address); if (!match) { return; } From 5f6bf93dd507e92378cce7c2e67add6ccf4bd345 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Fri, 1 Mar 2024 09:38:32 -0800 Subject: [PATCH 0350/1406] Expressions: Use enumerations rather than strings (#83741) --- pkg/expr/classic/classic.go | 15 +++++++-- pkg/expr/commands.go | 12 ++++--- pkg/expr/mathexp/resample.go | 33 ++++++++++++++----- pkg/expr/mathexp/resample_test.go | 4 +-- pkg/expr/models.go | 4 +-- pkg/expr/reader.go | 2 +- pkg/expr/threshold.go | 28 ++++++++++------ pkg/expr/threshold_test.go | 16 ++++----- pkg/services/ngalert/backtesting/eval_data.go | 4 +-- 9 files changed, 77 insertions(+), 41 deletions(-) diff --git a/pkg/expr/classic/classic.go b/pkg/expr/classic/classic.go index b920fc8a2f..bcb99b24d7 100644 --- a/pkg/expr/classic/classic.go +++ b/pkg/expr/classic/classic.go @@ -54,7 +54,7 @@ type condition struct { // Operator is the logical operator to use when there are two conditions in ConditionsCmd. // If there are more than two conditions in ConditionsCmd then operator is used to compare // the outcome of this condition with that of the condition before it. - Operator string + Operator ConditionOperatorType } // NeedsVars returns the variable names (refIds) that are dependencies @@ -216,7 +216,7 @@ func (cmd *ConditionsCmd) executeCond(_ context.Context, _ time.Time, cond condi return isCondFiring, isCondNoData, matches, nil } -func compareWithOperator(b1, b2 bool, operator string) bool { +func compareWithOperator(b1, b2 bool, operator ConditionOperatorType) bool { if operator == "or" { return b1 || b2 } else { @@ -262,8 +262,17 @@ type ConditionEvalJSON struct { Type string `json:"type"` // e.g. "gt" } +// The reducer function +// +enum +type ConditionOperatorType string + +const ( + ConditionOperatorAnd ConditionOperatorType = "and" + ConditionOperatorOr ConditionOperatorType = "or" +) + type ConditionOperatorJSON struct { - Type string `json:"type"` + Type ConditionOperatorType `json:"type"` } type ConditionQueryJSON struct { diff --git a/pkg/expr/commands.go b/pkg/expr/commands.go index 1f8926102c..9e1c495859 100644 --- a/pkg/expr/commands.go +++ b/pkg/expr/commands.go @@ -205,14 +205,14 @@ func (gr *ReduceCommand) Execute(ctx context.Context, _ time.Time, vars mathexp. type ResampleCommand struct { Window time.Duration VarToResample string - Downsampler string - Upsampler string + Downsampler mathexp.ReducerID + Upsampler mathexp.Upsampler TimeRange TimeRange refID string } // NewResampleCommand creates a new ResampleCMD. -func NewResampleCommand(refID, rawWindow, varToResample string, downsampler string, upsampler string, tr TimeRange) (*ResampleCommand, error) { +func NewResampleCommand(refID, rawWindow, varToResample string, downsampler mathexp.ReducerID, upsampler mathexp.Upsampler, tr TimeRange) (*ResampleCommand, error) { // TODO: validate reducer here, before execution window, err := gtime.ParseDuration(rawWindow) if err != nil { @@ -271,7 +271,11 @@ func UnmarshalResampleCommand(rn *rawNode) (*ResampleCommand, error) { return nil, fmt.Errorf("expected resample downsampler to be a string, got type %T", upsampler) } - return NewResampleCommand(rn.RefID, window, varToResample, downsampler, upsampler, rn.TimeRange) + return NewResampleCommand(rn.RefID, window, + varToResample, + mathexp.ReducerID(downsampler), + mathexp.Upsampler(upsampler), + rn.TimeRange) } // NeedsVars returns the variable names (refIds) that are dependencies diff --git a/pkg/expr/mathexp/resample.go b/pkg/expr/mathexp/resample.go index f6239be59f..69c9a3db54 100644 --- a/pkg/expr/mathexp/resample.go +++ b/pkg/expr/mathexp/resample.go @@ -7,8 +7,23 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/data" ) +// The upsample function +// +enum +type Upsampler string + +const ( + // Use the last seen value + UpsamplerPad Upsampler = "pad" + + // backfill + UpsamplerBackfill Upsampler = "backfilling" + + // Do not fill values (nill) + UpsamplerFillNA Upsampler = "fillna" +) + // Resample turns the Series into a Number based on the given reduction function -func (s Series) Resample(refID string, interval time.Duration, downsampler string, upsampler string, from, to time.Time) (Series, error) { +func (s Series) Resample(refID string, interval time.Duration, downsampler ReducerID, upsampler Upsampler, from, to time.Time) (Series, error) { newSeriesLength := int(float64(to.Sub(from).Nanoseconds()) / float64(interval.Nanoseconds())) if newSeriesLength <= 0 { return s, fmt.Errorf("the series cannot be sampled further; the time range is shorter than the interval") @@ -37,19 +52,19 @@ func (s Series) Resample(refID string, interval time.Duration, downsampler strin var value *float64 if len(vals) == 0 { // upsampling switch upsampler { - case "pad": + case UpsamplerPad: if lastSeen != nil { value = lastSeen } else { value = nil } - case "backfilling": + case UpsamplerBackfill: if sIdx == s.Len() { // no vals left value = nil } else { _, value = s.GetPoint(sIdx) } - case "fillna": + case UpsamplerFillNA: value = nil default: return s, fmt.Errorf("upsampling %v not implemented", upsampler) @@ -61,15 +76,15 @@ func (s Series) Resample(refID string, interval time.Duration, downsampler strin ff := Float64Field(*fVec) var tmp *float64 switch downsampler { - case "sum": + case ReducerSum: tmp = Sum(&ff) - case "mean": + case ReducerMean: tmp = Avg(&ff) - case "min": + case ReducerMin: tmp = Min(&ff) - case "max": + case ReducerMax: tmp = Max(&ff) - case "last": + case ReducerLast: tmp = Last(&ff) default: return s, fmt.Errorf("downsampling %v not implemented", downsampler) diff --git a/pkg/expr/mathexp/resample_test.go b/pkg/expr/mathexp/resample_test.go index a09ae96226..afee71428b 100644 --- a/pkg/expr/mathexp/resample_test.go +++ b/pkg/expr/mathexp/resample_test.go @@ -13,8 +13,8 @@ func TestResampleSeries(t *testing.T) { var tests = []struct { name string interval time.Duration - downsampler string - upsampler string + downsampler ReducerID + upsampler Upsampler timeRange backend.TimeRange seriesToResample Series series Series diff --git a/pkg/expr/models.go b/pkg/expr/models.go index dec7e3ce3a..6e7f0919ad 100644 --- a/pkg/expr/models.go +++ b/pkg/expr/models.go @@ -54,10 +54,10 @@ type ResampleQuery struct { Window string `json:"window" jsonschema:"minLength=1,example=1w,example=10m"` // The downsample function - Downsampler string `json:"downsampler"` + Downsampler mathexp.ReducerID `json:"downsampler"` // The upsample function - Upsampler string `json:"upsampler"` + Upsampler mathexp.Upsampler `json:"upsampler"` } type ThresholdQuery struct { diff --git a/pkg/expr/reader.go b/pkg/expr/reader.go index b92e704485..f4ca1cde5f 100644 --- a/pkg/expr/reader.go +++ b/pkg/expr/reader.go @@ -156,7 +156,7 @@ func (h *ExpressionQueryReader) ReadQuery( } func getReferenceVar(exp string, refId string) (string, error) { - exp = strings.TrimPrefix(exp, "%") + exp = strings.TrimPrefix(exp, "$") if exp == "" { return "", fmt.Errorf("no variable specified to reference for refId %v", refId) } diff --git a/pkg/expr/threshold.go b/pkg/expr/threshold.go index f6fd093a27..8a8f9ea541 100644 --- a/pkg/expr/threshold.go +++ b/pkg/expr/threshold.go @@ -18,23 +18,31 @@ import ( type ThresholdCommand struct { ReferenceVar string RefID string - ThresholdFunc string + ThresholdFunc ThresholdType Conditions []float64 Invert bool } +// +enum +type ThresholdType string + const ( - ThresholdIsAbove = "gt" - ThresholdIsBelow = "lt" - ThresholdIsWithinRange = "within_range" - ThresholdIsOutsideRange = "outside_range" + ThresholdIsAbove ThresholdType = "gt" + ThresholdIsBelow ThresholdType = "lt" + ThresholdIsWithinRange ThresholdType = "within_range" + ThresholdIsOutsideRange ThresholdType = "outside_range" ) var ( - supportedThresholdFuncs = []string{ThresholdIsAbove, ThresholdIsBelow, ThresholdIsWithinRange, ThresholdIsOutsideRange} + supportedThresholdFuncs = []string{ + string(ThresholdIsAbove), + string(ThresholdIsBelow), + string(ThresholdIsWithinRange), + string(ThresholdIsOutsideRange), + } ) -func NewThresholdCommand(refID, referenceVar, thresholdFunc string, conditions []float64) (*ThresholdCommand, error) { +func NewThresholdCommand(refID, referenceVar string, thresholdFunc ThresholdType, conditions []float64) (*ThresholdCommand, error) { switch thresholdFunc { case ThresholdIsOutsideRange, ThresholdIsWithinRange: if len(conditions) < 2 { @@ -57,8 +65,8 @@ func NewThresholdCommand(refID, referenceVar, thresholdFunc string, conditions [ } type ConditionEvalJSON struct { - Params []float64 `json:"params"` - Type string `json:"type"` // e.g. "gt" + Params []float64 `json:"params"` + Type ThresholdType `json:"type"` // e.g. "gt" } // UnmarshalResampleCommand creates a ResampleCMD from Grafana's frontend query. @@ -121,7 +129,7 @@ func (tc *ThresholdCommand) Execute(ctx context.Context, now time.Time, vars mat } // createMathExpression converts all the info we have about a "threshold" expression in to a Math expression -func createMathExpression(referenceVar string, thresholdFunc string, args []float64, invert bool) (string, error) { +func createMathExpression(referenceVar string, thresholdFunc ThresholdType, args []float64, invert bool) (string, error) { var exp string switch thresholdFunc { case ThresholdIsAbove: diff --git a/pkg/expr/threshold_test.go b/pkg/expr/threshold_test.go index 18d1a19fb6..c7e1c1f2eb 100644 --- a/pkg/expr/threshold_test.go +++ b/pkg/expr/threshold_test.go @@ -14,7 +14,7 @@ import ( func TestNewThresholdCommand(t *testing.T) { type testCase struct { - fn string + fn ThresholdType args []float64 shouldError bool expectedError string @@ -107,7 +107,7 @@ func TestUnmarshalThresholdCommand(t *testing.T) { require.IsType(t, &ThresholdCommand{}, command) cmd := command.(*ThresholdCommand) require.Equal(t, []string{"A"}, cmd.NeedsVars()) - require.Equal(t, "gt", cmd.ThresholdFunc) + require.Equal(t, ThresholdIsAbove, cmd.ThresholdFunc) require.Equal(t, []float64{20.0, 80.0}, cmd.Conditions) }, }, @@ -172,10 +172,10 @@ func TestUnmarshalThresholdCommand(t *testing.T) { cmd := c.(*HysteresisCommand) require.Equal(t, []string{"B"}, cmd.NeedsVars()) require.Equal(t, []string{"B"}, cmd.LoadingThresholdFunc.NeedsVars()) - require.Equal(t, "gt", cmd.LoadingThresholdFunc.ThresholdFunc) + require.Equal(t, ThresholdIsAbove, cmd.LoadingThresholdFunc.ThresholdFunc) require.Equal(t, []float64{100.0}, cmd.LoadingThresholdFunc.Conditions) require.Equal(t, []string{"B"}, cmd.UnloadingThresholdFunc.NeedsVars()) - require.Equal(t, "lt", cmd.UnloadingThresholdFunc.ThresholdFunc) + require.Equal(t, ThresholdIsBelow, cmd.UnloadingThresholdFunc.ThresholdFunc) require.Equal(t, []float64{31.0}, cmd.UnloadingThresholdFunc.Conditions) require.True(t, cmd.UnloadingThresholdFunc.Invert) require.NotNil(t, cmd.LoadedDimensions) @@ -233,7 +233,7 @@ func TestCreateMathExpression(t *testing.T) { expected string ref string - function string + function ThresholdType params []float64 } @@ -297,7 +297,7 @@ func TestCreateMathExpression(t *testing.T) { func TestIsSupportedThresholdFunc(t *testing.T) { type testCase struct { - function string + function ThresholdType supported bool } @@ -325,8 +325,8 @@ func TestIsSupportedThresholdFunc(t *testing.T) { } for _, tc := range cases { - t.Run(tc.function, func(t *testing.T) { - supported := IsSupportedThresholdFunc(tc.function) + t.Run(string(tc.function), func(t *testing.T) { + supported := IsSupportedThresholdFunc(string(tc.function)) require.Equal(t, supported, tc.supported) }) } diff --git a/pkg/services/ngalert/backtesting/eval_data.go b/pkg/services/ngalert/backtesting/eval_data.go index cd0fecdccf..12b8b872bd 100644 --- a/pkg/services/ngalert/backtesting/eval_data.go +++ b/pkg/services/ngalert/backtesting/eval_data.go @@ -16,8 +16,8 @@ import ( type dataEvaluator struct { refID string data []mathexp.Series - downsampleFunction string - upsampleFunction string + downsampleFunction mathexp.ReducerID + upsampleFunction mathexp.Upsampler } func newDataEvaluator(refID string, frame *data.Frame) (*dataEvaluator, error) { From 2184592174b8ec1ff20381349773fef1533d4b9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Fern=C3=A1ndez?= Date: Fri, 1 Mar 2024 19:01:27 +0100 Subject: [PATCH 0351/1406] ReturnToPrevious: create tests to check `reportInteraction` (#83757) --- .../ReturnToPrevious.test.tsx | 75 +++++++++++++++++++ .../ReturnToPrevious/ReturnToPrevious.tsx | 5 +- 2 files changed, 77 insertions(+), 3 deletions(-) create mode 100644 public/app/core/components/AppChrome/ReturnToPrevious/ReturnToPrevious.test.tsx diff --git a/public/app/core/components/AppChrome/ReturnToPrevious/ReturnToPrevious.test.tsx b/public/app/core/components/AppChrome/ReturnToPrevious/ReturnToPrevious.test.tsx new file mode 100644 index 0000000000..8ff6c986e8 --- /dev/null +++ b/public/app/core/components/AppChrome/ReturnToPrevious/ReturnToPrevious.test.tsx @@ -0,0 +1,75 @@ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; +import { TestProvider } from 'test/helpers/TestProvider'; +import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock'; + +import { config, reportInteraction } from '@grafana/runtime'; + +import { ReturnToPrevious, ReturnToPreviousProps } from './ReturnToPrevious'; + +const mockReturnToPreviousProps: ReturnToPreviousProps = { + title: 'Dashboards Page', + href: '/dashboards', +}; +const reportInteractionMock = jest.mocked(reportInteraction); +jest.mock('@grafana/runtime', () => { + return { + ...jest.requireActual('@grafana/runtime'), + reportInteraction: reportInteractionMock, + }; +}); +jest.mock('@grafana/runtime', () => { + return { + ...jest.requireActual('@grafana/runtime'), + reportInteraction: jest.fn(), + }; +}); +const setup = () => { + const grafanaContext = getGrafanaContextMock(); + grafanaContext.chrome.setReturnToPrevious(mockReturnToPreviousProps); + return render( + + + + ); +}; + +describe('ReturnToPrevious', () => { + beforeEach(() => { + /* We enabled the feature toggle */ + config.featureToggles.returnToPrevious = true; + }); + afterEach(() => { + window.sessionStorage.clear(); + jest.resetAllMocks(); + config.featureToggles.returnToPrevious = false; + }); + it('should render component', async () => { + setup(); + expect(await screen.findByTitle('Back to Dashboards Page')).toBeInTheDocument(); + }); + + it('should trigger event once when clicking on the RTP button', async () => { + setup(); + const returnButton = await screen.findByTitle('Back to Dashboards Page'); + expect(returnButton).toBeInTheDocument(); + await userEvent.click(returnButton); + const mockCalls = reportInteractionMock.mock.calls; + /* The report is called 'grafana_return_to_previous_button_dismissed' but the action is 'clicked' */ + const mockReturn = mockCalls.filter((call) => call[0] === 'grafana_return_to_previous_button_dismissed'); + expect(mockReturn).toHaveLength(1); + }); + + it('should trigger event once when clicking on the Close button', async () => { + setup(); + + const closeBtn = await screen.findByRole('button', { name: 'Close' }); + expect(closeBtn).toBeInTheDocument(); + await userEvent.click(closeBtn); + const mockCalls = reportInteractionMock.mock.calls; + /* The report is called 'grafana_return_to_previous_button_dismissed' but the action is 'dismissed' */ + const mockDismissed = mockCalls.filter((call) => call[0] === 'grafana_return_to_previous_button_dismissed'); + expect(mockDismissed).toHaveLength(1); + }); +}); diff --git a/public/app/core/components/AppChrome/ReturnToPrevious/ReturnToPrevious.tsx b/public/app/core/components/AppChrome/ReturnToPrevious/ReturnToPrevious.tsx index 58d7dbf654..693aa72b83 100644 --- a/public/app/core/components/AppChrome/ReturnToPrevious/ReturnToPrevious.tsx +++ b/public/app/core/components/AppChrome/ReturnToPrevious/ReturnToPrevious.tsx @@ -2,7 +2,7 @@ import { css } from '@emotion/css'; import React, { useCallback } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; -import { locationService, reportInteraction } from '@grafana/runtime'; +import { locationService } from '@grafana/runtime'; import { useStyles2 } from '@grafana/ui'; import { useGrafana } from 'app/core/context/GrafanaContext'; import { t } from 'app/core/internationalization'; @@ -24,9 +24,8 @@ export const ReturnToPrevious = ({ href, title }: ReturnToPreviousProps) => { }, [href, chrome]); const handleOnDismiss = useCallback(() => { - reportInteraction('grafana_return_to_previous_button_dismissed', { action: 'dismissed', page: href }); chrome.clearReturnToPrevious('dismissed'); - }, [href, chrome]); + }, [chrome]); return (
From d7bcd119c31bfb4d1aeb2779ccf6c158f297770c Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Fri, 1 Mar 2024 14:26:04 -0800 Subject: [PATCH 0352/1406] K8s: improve openapi generation (#83796) --- pkg/apis/dashboard/v0alpha1/types.go | 21 +++++-- .../v0alpha1/zz_generated.deepcopy.go | 10 ++-- .../v0alpha1/zz_generated.openapi.go | 57 ++++++++++--------- pkg/registry/apis/dashboard/register.go | 23 +++++++- pkg/registry/apis/dashboard/sub_access.go | 21 +++++-- pkg/registry/apis/dashboard/sub_versions.go | 19 +++++-- pkg/registry/apis/datasource/register.go | 33 ++++++++--- pkg/registry/apis/datasource/sub_health.go | 47 ++++++++------- pkg/registry/apis/datasource/sub_query.go | 40 ++----------- pkg/registry/apis/folders/register.go | 20 +++++++ pkg/registry/apis/folders/sub_access.go | 9 +++ pkg/registry/apis/folders/sub_count.go | 13 ++++- pkg/registry/apis/folders/sub_parents.go | 9 +++ pkg/tests/apis/dashboard/dashboards_test.go | 2 +- pkg/tests/apis/datasource/testdata_test.go | 3 +- 15 files changed, 214 insertions(+), 113 deletions(-) diff --git a/pkg/apis/dashboard/v0alpha1/types.go b/pkg/apis/dashboard/v0alpha1/types.go index 349f15c7b5..df69ef7c1a 100644 --- a/pkg/apis/dashboard/v0alpha1/types.go +++ b/pkg/apis/dashboard/v0alpha1/types.go @@ -51,7 +51,7 @@ type DashboardSummaryList struct { } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type DashboardVersionsInfo struct { +type DashboardVersionList struct { metav1.TypeMeta `json:",inline"` // +optional metav1.ListMeta `json:"metadata,omitempty"` @@ -60,11 +60,20 @@ type DashboardVersionsInfo struct { } type DashboardVersionInfo struct { - Version int `json:"version"` - ParentVersion int `json:"parentVersion,omitempty"` - Created int64 `json:"created"` - Message string `json:"message,omitempty"` - CreatedBy string `json:"createdBy,omitempty"` + // The internal ID for this version (will be replaced with resourceVersion) + Version int `json:"version"` + + // If the dashboard came from a previous version, it is set here + ParentVersion int `json:"parentVersion,omitempty"` + + // The creation timestamp for this version + Created int64 `json:"created"` + + // The user who created this version + CreatedBy string `json:"createdBy,omitempty"` + + // Message passed while saving the version + Message string `json:"message,omitempty"` } // +k8s:conversion-gen:explicit-from=net/url.Values diff --git a/pkg/apis/dashboard/v0alpha1/zz_generated.deepcopy.go b/pkg/apis/dashboard/v0alpha1/zz_generated.deepcopy.go index b83d77ae4d..2e0aa21fd2 100644 --- a/pkg/apis/dashboard/v0alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/dashboard/v0alpha1/zz_generated.deepcopy.go @@ -233,7 +233,7 @@ func (in *DashboardVersionInfo) DeepCopy() *DashboardVersionInfo { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *DashboardVersionsInfo) DeepCopyInto(out *DashboardVersionsInfo) { +func (in *DashboardVersionList) DeepCopyInto(out *DashboardVersionList) { *out = *in out.TypeMeta = in.TypeMeta in.ListMeta.DeepCopyInto(&out.ListMeta) @@ -245,18 +245,18 @@ func (in *DashboardVersionsInfo) DeepCopyInto(out *DashboardVersionsInfo) { return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DashboardVersionsInfo. -func (in *DashboardVersionsInfo) DeepCopy() *DashboardVersionsInfo { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DashboardVersionList. +func (in *DashboardVersionList) DeepCopy() *DashboardVersionList { if in == nil { return nil } - out := new(DashboardVersionsInfo) + out := new(DashboardVersionList) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *DashboardVersionsInfo) DeepCopyObject() runtime.Object { +func (in *DashboardVersionList) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } diff --git a/pkg/apis/dashboard/v0alpha1/zz_generated.openapi.go b/pkg/apis/dashboard/v0alpha1/zz_generated.openapi.go index c4767b898c..e2fa02b3a9 100644 --- a/pkg/apis/dashboard/v0alpha1/zz_generated.openapi.go +++ b/pkg/apis/dashboard/v0alpha1/zz_generated.openapi.go @@ -16,17 +16,17 @@ import ( func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { return map[string]common.OpenAPIDefinition{ - "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1.AnnotationActions": schema_pkg_apis_dashboard_v0alpha1_AnnotationActions(ref), - "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1.AnnotationPermission": schema_pkg_apis_dashboard_v0alpha1_AnnotationPermission(ref), - "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1.Dashboard": schema_pkg_apis_dashboard_v0alpha1_Dashboard(ref), - "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1.DashboardAccessInfo": schema_pkg_apis_dashboard_v0alpha1_DashboardAccessInfo(ref), - "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1.DashboardList": schema_pkg_apis_dashboard_v0alpha1_DashboardList(ref), - "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1.DashboardSummary": schema_pkg_apis_dashboard_v0alpha1_DashboardSummary(ref), - "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1.DashboardSummaryList": schema_pkg_apis_dashboard_v0alpha1_DashboardSummaryList(ref), - "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1.DashboardSummarySpec": schema_pkg_apis_dashboard_v0alpha1_DashboardSummarySpec(ref), - "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1.DashboardVersionInfo": schema_pkg_apis_dashboard_v0alpha1_DashboardVersionInfo(ref), - "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1.DashboardVersionsInfo": schema_pkg_apis_dashboard_v0alpha1_DashboardVersionsInfo(ref), - "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1.VersionsQueryOptions": schema_pkg_apis_dashboard_v0alpha1_VersionsQueryOptions(ref), + "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1.AnnotationActions": schema_pkg_apis_dashboard_v0alpha1_AnnotationActions(ref), + "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1.AnnotationPermission": schema_pkg_apis_dashboard_v0alpha1_AnnotationPermission(ref), + "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1.Dashboard": schema_pkg_apis_dashboard_v0alpha1_Dashboard(ref), + "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1.DashboardAccessInfo": schema_pkg_apis_dashboard_v0alpha1_DashboardAccessInfo(ref), + "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1.DashboardList": schema_pkg_apis_dashboard_v0alpha1_DashboardList(ref), + "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1.DashboardSummary": schema_pkg_apis_dashboard_v0alpha1_DashboardSummary(ref), + "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1.DashboardSummaryList": schema_pkg_apis_dashboard_v0alpha1_DashboardSummaryList(ref), + "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1.DashboardSummarySpec": schema_pkg_apis_dashboard_v0alpha1_DashboardSummarySpec(ref), + "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1.DashboardVersionInfo": schema_pkg_apis_dashboard_v0alpha1_DashboardVersionInfo(ref), + "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1.DashboardVersionList": schema_pkg_apis_dashboard_v0alpha1_DashboardVersionList(ref), + "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1.VersionsQueryOptions": schema_pkg_apis_dashboard_v0alpha1_VersionsQueryOptions(ref), } } @@ -380,34 +380,39 @@ func schema_pkg_apis_dashboard_v0alpha1_DashboardVersionInfo(ref common.Referenc Properties: map[string]spec.Schema{ "version": { SchemaProps: spec.SchemaProps{ - Default: 0, - Type: []string{"integer"}, - Format: "int32", + Description: "The internal ID for this version (will be replaced with resourceVersion)", + Default: 0, + Type: []string{"integer"}, + Format: "int32", }, }, "parentVersion": { SchemaProps: spec.SchemaProps{ - Type: []string{"integer"}, - Format: "int32", + Description: "If the dashboard came from a previous version, it is set here", + Type: []string{"integer"}, + Format: "int32", }, }, "created": { SchemaProps: spec.SchemaProps{ - Default: 0, - Type: []string{"integer"}, - Format: "int64", + Description: "The creation timestamp for this version", + Default: 0, + Type: []string{"integer"}, + Format: "int64", }, }, - "message": { + "createdBy": { SchemaProps: spec.SchemaProps{ - Type: []string{"string"}, - Format: "", + Description: "The user who created this version", + Type: []string{"string"}, + Format: "", }, }, - "createdBy": { + "message": { SchemaProps: spec.SchemaProps{ - Type: []string{"string"}, - Format: "", + Description: "Message passed while saving the version", + Type: []string{"string"}, + Format: "", }, }, }, @@ -417,7 +422,7 @@ func schema_pkg_apis_dashboard_v0alpha1_DashboardVersionInfo(ref common.Referenc } } -func schema_pkg_apis_dashboard_v0alpha1_DashboardVersionsInfo(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v0alpha1_DashboardVersionList(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ diff --git a/pkg/registry/apis/dashboard/register.go b/pkg/registry/apis/dashboard/register.go index 643155dd28..98b928a560 100644 --- a/pkg/registry/apis/dashboard/register.go +++ b/pkg/registry/apis/dashboard/register.go @@ -13,6 +13,7 @@ import ( "k8s.io/apiserver/pkg/registry/rest" genericapiserver "k8s.io/apiserver/pkg/server" common "k8s.io/kube-openapi/pkg/common" + "k8s.io/kube-openapi/pkg/spec3" "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1" "github.com/grafana/grafana/pkg/apiserver/builder" @@ -82,7 +83,7 @@ func addKnownTypes(scheme *runtime.Scheme, gv schema.GroupVersion) { &v0alpha1.Dashboard{}, &v0alpha1.DashboardList{}, &v0alpha1.DashboardAccessInfo{}, - &v0alpha1.DashboardVersionsInfo{}, + &v0alpha1.DashboardVersionList{}, &v0alpha1.DashboardSummary{}, &v0alpha1.DashboardSummaryList{}, &v0alpha1.VersionsQueryOptions{}, @@ -196,6 +197,26 @@ func (b *DashboardsAPIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefiniti return v0alpha1.GetOpenAPIDefinitions } +func (b *DashboardsAPIBuilder) PostProcessOpenAPI(oas *spec3.OpenAPI) (*spec3.OpenAPI, error) { + // The plugin description + oas.Info.Description = "Grafana dashboards as resources" + + // The root api URL + root := "/apis/" + b.GetGroupVersion().String() + "/" + + // Hide the ability to list or watch across all tenants + delete(oas.Paths.Paths, root+v0alpha1.DashboardResourceInfo.GroupResource().Resource) + delete(oas.Paths.Paths, root+"watch/"+v0alpha1.DashboardResourceInfo.GroupResource().Resource) + delete(oas.Paths.Paths, root+v0alpha1.DashboardSummaryResourceInfo.GroupResource().Resource) + + // The root API discovery list + sub := oas.Paths.Paths[root] + if sub != nil && sub.Get != nil { + sub.Get.Tags = []string{"API Discovery"} // sorts first in the list + } + return oas, nil +} + func (b *DashboardsAPIBuilder) GetAPIRoutes() *builder.APIRoutes { return nil // no custom API routes } diff --git a/pkg/registry/apis/dashboard/sub_access.go b/pkg/registry/apis/dashboard/sub_access.go index ca2c783b5e..3436a3f1fc 100644 --- a/pkg/registry/apis/dashboard/sub_access.go +++ b/pkg/registry/apis/dashboard/sub_access.go @@ -8,7 +8,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apiserver/pkg/registry/rest" - "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1" + dashboard "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1" "github.com/grafana/grafana/pkg/infra/appcontext" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" @@ -22,9 +22,10 @@ type AccessREST struct { } var _ = rest.Connecter(&AccessREST{}) +var _ = rest.StorageMetadata(&AccessREST{}) func (r *AccessREST) New() runtime.Object { - return &v0alpha1.DashboardAccessInfo{} + return &dashboard.DashboardAccessInfo{} } func (r *AccessREST) Destroy() { @@ -35,7 +36,15 @@ func (r *AccessREST) ConnectMethods() []string { } func (r *AccessREST) NewConnectOptions() (runtime.Object, bool, string) { - return &v0alpha1.VersionsQueryOptions{}, false, "" + return &dashboard.VersionsQueryOptions{}, false, "" +} + +func (r *AccessREST) ProducesMIMETypes(verb string) []string { + return nil +} + +func (r *AccessREST) ProducesObject(verb string) interface{} { + return &dashboard.DashboardAccessInfo{} } func (r *AccessREST) Connect(ctx context.Context, name string, opts runtime.Object, responder rest.Responder) (http.Handler, error) { @@ -66,14 +75,14 @@ func (r *AccessREST) Connect(ctx context.Context, name string, opts runtime.Obje return nil, fmt.Errorf("not allowed to view") } - access := &v0alpha1.DashboardAccessInfo{} + access := &dashboard.DashboardAccessInfo{} access.CanEdit, _ = guardian.CanEdit() access.CanSave, _ = guardian.CanSave() access.CanAdmin, _ = guardian.CanAdmin() access.CanDelete, _ = guardian.CanDelete() access.CanStar = user.IsRealUser() && !user.IsAnonymous - access.AnnotationsPermissions = &v0alpha1.AnnotationPermission{} + access.AnnotationsPermissions = &dashboard.AnnotationPermission{} r.getAnnotationPermissionsByScope(ctx, user, &access.AnnotationsPermissions.Dashboard, accesscontrol.ScopeAnnotationsTypeDashboard) r.getAnnotationPermissionsByScope(ctx, user, &access.AnnotationsPermissions.Organization, accesscontrol.ScopeAnnotationsTypeOrganization) @@ -82,7 +91,7 @@ func (r *AccessREST) Connect(ctx context.Context, name string, opts runtime.Obje }), nil } -func (r *AccessREST) getAnnotationPermissionsByScope(ctx context.Context, user identity.Requester, actions *v0alpha1.AnnotationActions, scope string) { +func (r *AccessREST) getAnnotationPermissionsByScope(ctx context.Context, user identity.Requester, actions *dashboard.AnnotationActions, scope string) { var err error evaluate := accesscontrol.EvalPermission(accesscontrol.ActionAnnotationsCreate, scope) diff --git a/pkg/registry/apis/dashboard/sub_versions.go b/pkg/registry/apis/dashboard/sub_versions.go index 858d3e6780..4787c5387d 100644 --- a/pkg/registry/apis/dashboard/sub_versions.go +++ b/pkg/registry/apis/dashboard/sub_versions.go @@ -12,7 +12,7 @@ import ( "k8s.io/apiserver/pkg/registry/rest" common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" - "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1" + dashboard "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" dashver "github.com/grafana/grafana/pkg/services/dashboardversion" ) @@ -22,9 +22,10 @@ type VersionsREST struct { } var _ = rest.Connecter(&VersionsREST{}) +var _ = rest.StorageMetadata(&VersionsREST{}) func (r *VersionsREST) New() runtime.Object { - return &v0alpha1.DashboardVersionsInfo{} + return &dashboard.DashboardVersionList{} } func (r *VersionsREST) Destroy() { @@ -34,6 +35,14 @@ func (r *VersionsREST) ConnectMethods() []string { return []string{"GET"} } +func (r *VersionsREST) ProducesMIMETypes(verb string) []string { + return nil +} + +func (r *VersionsREST) ProducesObject(verb string) interface{} { + return &dashboard.DashboardVersionList{} +} + func (r *VersionsREST) NewConnectOptions() (runtime.Object, bool, string) { return nil, true, "" } @@ -68,7 +77,7 @@ func (r *VersionsREST) Connect(ctx context.Context, uid string, opts runtime.Obj data, _ := dto.Data.Map() // Convert the version to a regular dashboard - dash := &v0alpha1.Dashboard{ + dash := &dashboard.Dashboard{ ObjectMeta: metav1.ObjectMeta{ Name: uid, CreationTimestamp: metav1.NewTime(dto.Created), @@ -88,9 +97,9 @@ func (r *VersionsREST) Connect(ctx context.Context, uid string, opts runtime.Obj responder.Error(err) return } - versions := &v0alpha1.DashboardVersionsInfo{} + versions := &dashboard.DashboardVersionList{} for _, v := range rsp { - info := v0alpha1.DashboardVersionInfo{ + info := dashboard.DashboardVersionInfo{ Version: v.Version, Created: v.Created.UnixMilli(), Message: v.Message, diff --git a/pkg/registry/apis/datasource/register.go b/pkg/registry/apis/datasource/register.go index c9d67af478..7bd9e966f5 100644 --- a/pkg/registry/apis/datasource/register.go +++ b/pkg/registry/apis/datasource/register.go @@ -14,12 +14,13 @@ import ( "k8s.io/apiserver/pkg/registry/rest" genericapiserver "k8s.io/apiserver/pkg/server" openapi "k8s.io/kube-openapi/pkg/common" + "k8s.io/kube-openapi/pkg/spec3" "k8s.io/utils/strings/slices" "github.com/grafana/grafana-plugin-sdk-go/backend" common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" - "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1" + datasource "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1" query "github.com/grafana/grafana/pkg/apis/query/v0alpha1" "github.com/grafana/grafana/pkg/apiserver/builder" "github.com/grafana/grafana/pkg/plugins" @@ -117,9 +118,9 @@ func (b *DataSourceAPIBuilder) GetGroupVersion() schema.GroupVersion { func addKnownTypes(scheme *runtime.Scheme, gv schema.GroupVersion) { scheme.AddKnownTypes(gv, - &v0alpha1.DataSourceConnection{}, - &v0alpha1.DataSourceConnectionList{}, - &v0alpha1.HealthCheckResult{}, + &datasource.DataSourceConnection{}, + &datasource.DataSourceConnectionList{}, + &datasource.HealthCheckResult{}, &unstructured.Unstructured{}, // Query handler &query.QueryDataResponse{}, @@ -152,7 +153,7 @@ func resourceFromPluginID(pluginID string) (common.ResourceInfo, error) { if err != nil { return common.ResourceInfo{}, err } - return v0alpha1.GenericConnectionResourceInfo.WithGroupAndShortName(group, pluginID+"-connection"), nil + return datasource.GenericConnectionResourceInfo.WithGroupAndShortName(group, pluginID+"-connection"), nil } func (b *DataSourceAPIBuilder) GetAPIGroupInfo( @@ -177,7 +178,7 @@ func (b *DataSourceAPIBuilder) GetAPIGroupInfo( {Name: "Created At", Type: "date"}, }, func(obj any) ([]interface{}, error) { - m, ok := obj.(*v0alpha1.DataSourceConnection) + m, ok := obj.(*datasource.DataSourceConnection) if !ok { return nil, fmt.Errorf("expected connection") } @@ -220,13 +221,31 @@ func (b *DataSourceAPIBuilder) getPluginContext(ctx context.Context, uid string) func (b *DataSourceAPIBuilder) GetOpenAPIDefinitions() openapi.GetOpenAPIDefinitions { return func(ref openapi.ReferenceCallback) map[string]openapi.OpenAPIDefinition { defs := query.GetOpenAPIDefinitions(ref) // required when running standalone - for k, v := range v0alpha1.GetOpenAPIDefinitions(ref) { + for k, v := range datasource.GetOpenAPIDefinitions(ref) { defs[k] = v } return defs } } +func (b *DataSourceAPIBuilder) PostProcessOpenAPI(oas *spec3.OpenAPI) (*spec3.OpenAPI, error) { + // The plugin description + oas.Info.Description = b.pluginJSON.Info.Description + + // The root api URL + root := "/apis/" + b.connectionResourceInfo.GroupVersion().String() + "/" + + // Hide the ability to list all connections across tenants + delete(oas.Paths.Paths, root+b.connectionResourceInfo.GroupResource().Resource) + + // The root API discovery list + sub := oas.Paths.Paths[root] + if sub != nil && sub.Get != nil { + sub.Get.Tags = []string{"API Discovery"} // sorts first in the list + } + return oas, nil +} + // Register additional routes with the server func (b *DataSourceAPIBuilder) GetAPIRoutes() *builder.APIRoutes { return nil diff --git a/pkg/registry/apis/datasource/sub_health.go b/pkg/registry/apis/datasource/sub_health.go index ede7aeca7c..b58a540916 100644 --- a/pkg/registry/apis/datasource/sub_health.go +++ b/pkg/registry/apis/datasource/sub_health.go @@ -9,17 +9,20 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apiserver/pkg/registry/rest" - "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1" + datasource "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1" ) type subHealthREST struct { builder *DataSourceAPIBuilder } -var _ = rest.Connecter(&subHealthREST{}) +var ( + _ = rest.Connecter(&subHealthREST{}) + _ = rest.StorageMetadata(&subHealthREST{}) +) func (r *subHealthREST) New() runtime.Object { - return &v0alpha1.HealthCheckResult{} + return &datasource.HealthCheckResult{} } func (r *subHealthREST) Destroy() { @@ -29,28 +32,34 @@ func (r *subHealthREST) ConnectMethods() []string { return []string{"GET"} } +func (r *subHealthREST) ProducesMIMETypes(verb string) []string { + return nil +} + +func (r *subHealthREST) ProducesObject(verb string) interface{} { + return &datasource.HealthCheckResult{} +} + func (r *subHealthREST) NewConnectOptions() (runtime.Object, bool, string) { return nil, false, "" } func (r *subHealthREST) Connect(ctx context.Context, name string, opts runtime.Object, responder rest.Responder) (http.Handler, error) { - return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - pluginCtx, err := r.builder.getPluginContext(ctx, name) - if err != nil { - responder.Error(err) - return - } - ctx = backend.WithGrafanaConfig(ctx, pluginCtx.GrafanaConfig) - - healthResponse, err := r.builder.client.CheckHealth(ctx, &backend.CheckHealthRequest{ - PluginContext: pluginCtx, - }) - if err != nil { - responder.Error(err) - return - } + pluginCtx, err := r.builder.getPluginContext(ctx, name) + if err != nil { + return nil, err + } + ctx = backend.WithGrafanaConfig(ctx, pluginCtx.GrafanaConfig) - rsp := &v0alpha1.HealthCheckResult{} + healthResponse, err := r.builder.client.CheckHealth(ctx, &backend.CheckHealthRequest{ + PluginContext: pluginCtx, + }) + if err != nil { + return nil, err + } + + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + rsp := &datasource.HealthCheckResult{} rsp.Code = int(healthResponse.Status) rsp.Status = healthResponse.Status.String() rsp.Message = healthResponse.Message diff --git a/pkg/registry/apis/datasource/sub_query.go b/pkg/registry/apis/datasource/sub_query.go index eda371d7ed..7b9e48599f 100644 --- a/pkg/registry/apis/datasource/sub_query.go +++ b/pkg/registry/apis/datasource/sub_query.go @@ -5,13 +5,12 @@ import ( "encoding/json" "fmt" "net/http" - "strconv" "github.com/grafana/grafana-plugin-sdk-go/backend" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apiserver/pkg/registry/rest" - "github.com/grafana/grafana/pkg/apis/query/v0alpha1" + query "github.com/grafana/grafana/pkg/apis/query/v0alpha1" "github.com/grafana/grafana/pkg/middleware/requestmeta" "github.com/grafana/grafana/pkg/tsdb/legacydata" "github.com/grafana/grafana/pkg/web" @@ -24,51 +23,24 @@ type subQueryREST struct { var _ = rest.Connecter(&subQueryREST{}) func (r *subQueryREST) New() runtime.Object { - return &v0alpha1.QueryDataResponse{} + return &query.QueryDataResponse{} } func (r *subQueryREST) Destroy() {} func (r *subQueryREST) ConnectMethods() []string { - return []string{"POST", "GET"} + return []string{"POST"} } func (r *subQueryREST) NewConnectOptions() (runtime.Object, bool, string) { return nil, false, "" } -func (r *subQueryREST) readQueries(req *http.Request) ([]backend.DataQuery, *v0alpha1.DataSourceRef, error) { - reqDTO := v0alpha1.GenericQueryRequest{} - // Simple URL to JSON mapping - if req.Method == http.MethodGet { - query := v0alpha1.GenericDataQuery{ - RefID: "A", - MaxDataPoints: 1000, - IntervalMS: 10, - } - params := req.URL.Query() - for k := range params { - v := params.Get(k) // the singular value - switch k { - case "to": - reqDTO.To = v - case "from": - reqDTO.From = v - case "maxDataPoints": - query.MaxDataPoints, _ = strconv.ParseInt(v, 10, 64) - case "intervalMs": - query.IntervalMS, _ = strconv.ParseFloat(v, 64) - case "queryType": - query.QueryType = v - default: - query.AdditionalProperties()[k] = v - } - } - reqDTO.Queries = []v0alpha1.GenericDataQuery{query} - } else if err := web.Bind(req, &reqDTO); err != nil { +func (r *subQueryREST) readQueries(req *http.Request) ([]backend.DataQuery, *query.DataSourceRef, error) { + reqDTO := query.GenericQueryRequest{} + if err := web.Bind(req, &reqDTO); err != nil { return nil, nil, err } - return legacydata.ToDataSourceQueries(reqDTO) } diff --git a/pkg/registry/apis/folders/register.go b/pkg/registry/apis/folders/register.go index 5c2b48f8c3..19cb2d71f4 100644 --- a/pkg/registry/apis/folders/register.go +++ b/pkg/registry/apis/folders/register.go @@ -13,6 +13,7 @@ import ( "k8s.io/apiserver/pkg/registry/rest" genericapiserver "k8s.io/apiserver/pkg/server" common "k8s.io/kube-openapi/pkg/common" + "k8s.io/kube-openapi/pkg/spec3" "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" "github.com/grafana/grafana/pkg/apiserver/builder" @@ -153,6 +154,25 @@ func (b *FolderAPIBuilder) GetAPIRoutes() *builder.APIRoutes { return nil // no custom API routes } +func (b *FolderAPIBuilder) PostProcessOpenAPI(oas *spec3.OpenAPI) (*spec3.OpenAPI, error) { + // The plugin description + oas.Info.Description = "Grafana folders" + + // The root api URL + root := "/apis/" + b.GetGroupVersion().String() + "/" + + // Hide the ability to list or watch across all tenants + delete(oas.Paths.Paths, root+v0alpha1.FolderResourceInfo.GroupResource().Resource) + delete(oas.Paths.Paths, root+"watch/"+v0alpha1.FolderResourceInfo.GroupResource().Resource) + + // The root API discovery list + sub := oas.Paths.Paths[root] + if sub != nil && sub.Get != nil { + sub.Get.Tags = []string{"API Discovery"} // sorts first in the list + } + return oas, nil +} + func (b *FolderAPIBuilder) GetAuthorizer() authorizer.Authorizer { return authorizer.AuthorizerFunc( func(ctx context.Context, attr authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) { diff --git a/pkg/registry/apis/folders/sub_access.go b/pkg/registry/apis/folders/sub_access.go index 46dc7dd5aa..be506be9ec 100644 --- a/pkg/registry/apis/folders/sub_access.go +++ b/pkg/registry/apis/folders/sub_access.go @@ -19,6 +19,7 @@ type subAccessREST struct { } var _ = rest.Connecter(&subAccessREST{}) +var _ = rest.StorageMetadata(&subAccessREST{}) func (r *subAccessREST) New() runtime.Object { return &v0alpha1.FolderAccessInfo{} @@ -31,6 +32,14 @@ func (r *subAccessREST) ConnectMethods() []string { return []string{"GET"} } +func (r *subAccessREST) ProducesMIMETypes(verb string) []string { + return nil +} + +func (r *subAccessREST) ProducesObject(verb string) interface{} { + return &v0alpha1.FolderAccessInfo{} +} + func (r *subAccessREST) NewConnectOptions() (runtime.Object, bool, string) { return nil, false, "" // true means you can use the trailing path as a variable } diff --git a/pkg/registry/apis/folders/sub_count.go b/pkg/registry/apis/folders/sub_count.go index cf2c00f8ad..f3b7d7e61b 100644 --- a/pkg/registry/apis/folders/sub_count.go +++ b/pkg/registry/apis/folders/sub_count.go @@ -17,7 +17,10 @@ type subCountREST struct { service folder.Service } -var _ = rest.Connecter(&subCountREST{}) +var ( + _ = rest.Connecter(&subCountREST{}) + _ = rest.StorageMetadata(&subCountREST{}) +) func (r *subCountREST) New() runtime.Object { return &v0alpha1.DescendantCounts{} @@ -30,6 +33,14 @@ func (r *subCountREST) ConnectMethods() []string { return []string{"GET"} } +func (r *subCountREST) ProducesMIMETypes(verb string) []string { + return nil +} + +func (r *subCountREST) ProducesObject(verb string) interface{} { + return &v0alpha1.DescendantCounts{} +} + func (r *subCountREST) NewConnectOptions() (runtime.Object, bool, string) { return nil, false, "" // true means you can use the trailing path as a variable } diff --git a/pkg/registry/apis/folders/sub_parents.go b/pkg/registry/apis/folders/sub_parents.go index 36f67032d6..f7761d075b 100644 --- a/pkg/registry/apis/folders/sub_parents.go +++ b/pkg/registry/apis/folders/sub_parents.go @@ -17,6 +17,7 @@ type subParentsREST struct { } var _ = rest.Connecter(&subParentsREST{}) +var _ = rest.StorageMetadata(&subParentsREST{}) func (r *subParentsREST) New() runtime.Object { return &v0alpha1.FolderInfoList{} @@ -29,6 +30,14 @@ func (r *subParentsREST) ConnectMethods() []string { return []string{"GET"} } +func (r *subParentsREST) ProducesMIMETypes(verb string) []string { + return nil +} + +func (r *subParentsREST) ProducesObject(verb string) interface{} { + return &v0alpha1.FolderInfoList{} +} + func (r *subParentsREST) NewConnectOptions() (runtime.Object, bool, string) { return nil, false, "" // true means you can use the trailing path as a variable } diff --git a/pkg/tests/apis/dashboard/dashboards_test.go b/pkg/tests/apis/dashboard/dashboards_test.go index a3859c084d..9771ef718e 100644 --- a/pkg/tests/apis/dashboard/dashboards_test.go +++ b/pkg/tests/apis/dashboard/dashboards_test.go @@ -77,7 +77,7 @@ func TestIntegrationDashboardsApp(t *testing.T) { { "responseKind": { "group": "", - "kind": "DashboardVersionsInfo", + "kind": "DashboardVersionList", "version": "" }, "subresource": "versions", diff --git a/pkg/tests/apis/datasource/testdata_test.go b/pkg/tests/apis/datasource/testdata_test.go index c9f1e64b95..328cf81e35 100644 --- a/pkg/tests/apis/datasource/testdata_test.go +++ b/pkg/tests/apis/datasource/testdata_test.go @@ -80,8 +80,7 @@ func TestIntegrationTestDatasource(t *testing.T) { }, "subresource": "query", "verbs": [ - "create", - "get" + "create" ] }, { From 869b89dce45090cd982414479a7e7b3bd7953253 Mon Sep 17 00:00:00 2001 From: Todd Treece <360020+toddtreece@users.noreply.github.com> Date: Fri, 1 Mar 2024 20:32:59 -0500 Subject: [PATCH 0353/1406] K8s: Add accept header to ctx (#83802) --- pkg/apiserver/builder/helper.go | 8 +++- pkg/apiserver/endpoints/filters/accept.go | 15 ++++++ .../endpoints/filters/accept_test.go | 46 +++++++++++++++++++ pkg/apiserver/endpoints/request/accept.go | 22 +++++++++ .../endpoints/request/accept_test.go | 26 +++++++++++ 5 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 pkg/apiserver/endpoints/filters/accept.go create mode 100644 pkg/apiserver/endpoints/filters/accept_test.go create mode 100644 pkg/apiserver/endpoints/request/accept.go create mode 100644 pkg/apiserver/endpoints/request/accept_test.go diff --git a/pkg/apiserver/builder/helper.go b/pkg/apiserver/builder/helper.go index 136dba3269..f41626e2e2 100644 --- a/pkg/apiserver/builder/helper.go +++ b/pkg/apiserver/builder/helper.go @@ -20,6 +20,8 @@ import ( "k8s.io/apiserver/pkg/util/openapi" k8sscheme "k8s.io/client-go/kubernetes/scheme" "k8s.io/kube-openapi/pkg/common" + + "github.com/grafana/grafana/pkg/apiserver/endpoints/filters" ) func SetupConfig( @@ -70,7 +72,11 @@ func SetupConfig( if err != nil { panic(fmt.Sprintf("could not build handler chain func: %s", err.Error())) } - return genericapiserver.DefaultBuildHandlerChain(requestHandler, c) + + handler := genericapiserver.DefaultBuildHandlerChain(requestHandler, c) + handler = filters.WithAcceptHeader(handler) + + return handler } k8sVersion, err := getK8sApiserverVersion() diff --git a/pkg/apiserver/endpoints/filters/accept.go b/pkg/apiserver/endpoints/filters/accept.go new file mode 100644 index 0000000000..8fa8f34bef --- /dev/null +++ b/pkg/apiserver/endpoints/filters/accept.go @@ -0,0 +1,15 @@ +package filters + +import ( + "net/http" + + "github.com/grafana/grafana/pkg/apiserver/endpoints/request" +) + +// WithAcceptHeader adds the Accept header to the request context. +func WithAcceptHeader(handler http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + ctx := request.WithAcceptHeader(req.Context(), req.Header.Get("Accept")) + handler.ServeHTTP(w, req.WithContext(ctx)) + }) +} diff --git a/pkg/apiserver/endpoints/filters/accept_test.go b/pkg/apiserver/endpoints/filters/accept_test.go new file mode 100644 index 0000000000..e90da56e4c --- /dev/null +++ b/pkg/apiserver/endpoints/filters/accept_test.go @@ -0,0 +1,46 @@ +package filters + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + "github.com/grafana/grafana/pkg/apiserver/endpoints/request" + "github.com/stretchr/testify/require" +) + +func TestWithAcceptHeader(t *testing.T) { + t.Run("should not set accept header in context for empty header", func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/", nil) + + rr := httptest.NewRecorder() + handler := &fakeHandler{} + WithAcceptHeader(handler).ServeHTTP(rr, req) + + acceptHeader, ok := request.AcceptHeaderFrom(handler.ctx) + require.False(t, ok) + require.Empty(t, acceptHeader) + }) + + t.Run("should set accept header in context", func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/", nil) + req.Header.Set("Accept", "application/json") + + rr := httptest.NewRecorder() + handler := &fakeHandler{} + WithAcceptHeader(handler).ServeHTTP(rr, req) + + acceptHeader, ok := request.AcceptHeaderFrom(handler.ctx) + require.True(t, ok) + require.Equal(t, "application/json", acceptHeader) + }) +} + +type fakeHandler struct { + ctx context.Context +} + +func (h *fakeHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { + h.ctx = req.Context() +} diff --git a/pkg/apiserver/endpoints/request/accept.go b/pkg/apiserver/endpoints/request/accept.go new file mode 100644 index 0000000000..5356882067 --- /dev/null +++ b/pkg/apiserver/endpoints/request/accept.go @@ -0,0 +1,22 @@ +package request + +import ( + "context" +) + +type acceptHeaderKey struct{} + +// WithAcceptHeader adds the accept header to the supplied context. +func WithAcceptHeader(ctx context.Context, acceptHeader string) context.Context { + // only add the accept header to ctx if it is not empty + if acceptHeader == "" { + return ctx + } + return context.WithValue(ctx, acceptHeaderKey{}, acceptHeader) +} + +// AcceptHeaderFrom returns the accept header from the supplied context and a boolean indicating if the value was present. +func AcceptHeaderFrom(ctx context.Context) (string, bool) { + acceptHeader, ok := ctx.Value(acceptHeaderKey{}).(string) + return acceptHeader, ok +} diff --git a/pkg/apiserver/endpoints/request/accept_test.go b/pkg/apiserver/endpoints/request/accept_test.go new file mode 100644 index 0000000000..0a415896a5 --- /dev/null +++ b/pkg/apiserver/endpoints/request/accept_test.go @@ -0,0 +1,26 @@ +package request + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestAcceptHeader(t *testing.T) { + ctx := context.Background() + + t.Run("should not set ctx for empty header", func(t *testing.T) { + out := WithAcceptHeader(ctx, "") + acceptHeader, ok := AcceptHeaderFrom(out) + require.False(t, ok) + require.Empty(t, acceptHeader) + }) + + t.Run("should add header to ctx", func(t *testing.T) { + out := WithAcceptHeader(ctx, "application/json") + acceptHeader, ok := AcceptHeaderFrom(out) + require.True(t, ok) + require.Equal(t, "application/json", acceptHeader) + }) +} From 9eb69b921344e01ebd861dac45854efef6f989e0 Mon Sep 17 00:00:00 2001 From: Pepe Cano <825430+ppcano@users.noreply.github.com> Date: Mon, 4 Mar 2024 10:20:10 +0100 Subject: [PATCH 0354/1406] Alerting docs: fix incorrect `docs/reference` link (#83809) --- .../export-alerting-resources/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/alerting/set-up/provision-alerting-resources/export-alerting-resources/index.md b/docs/sources/alerting/set-up/provision-alerting-resources/export-alerting-resources/index.md index ce54ec866b..01236a56fd 100644 --- a/docs/sources/alerting/set-up/provision-alerting-resources/export-alerting-resources/index.md +++ b/docs/sources/alerting/set-up/provision-alerting-resources/export-alerting-resources/index.md @@ -154,7 +154,7 @@ These endpoints accept a `download` parameter to download a file containing the [alerting_file_provisioning]: "/docs/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/file-provisioning" -[alerting_file_provisioning_template]: "/docs/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/file-provisioning/#import-templates" +[alerting_file_provisioning_template]: "/docs/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/file-provisioning#import-templates" [export_rule]: "/docs/grafana/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/http-api-provisioning/#span-idroute-get-alert-rule-exportspan-export-an-alert-rule-in-provisioning-file-format-_routegetalertruleexport_" [export_rule]: "/docs/grafana-cloud/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/http-api-provisioning/#span-idroute-get-alert-rule-exportspan-export-an-alert-rule-in-provisioning-file-format-_routegetalertruleexport_" From 244589d7eac0fa839c74560e9205ccf8da7bcf87 Mon Sep 17 00:00:00 2001 From: Pepe Cano <825430+ppcano@users.noreply.github.com> Date: Mon, 4 Mar 2024 10:50:44 +0100 Subject: [PATCH 0355/1406] Alerting docs: document `mute-timings` export endpoints (#83797) --- .../export-alerting-resources/index.md | 8 ++ .../shared/alerts/alerting_provisioning.md | 101 ++++++++++++++++-- 2 files changed, 102 insertions(+), 7 deletions(-) diff --git a/docs/sources/alerting/set-up/provision-alerting-resources/export-alerting-resources/index.md b/docs/sources/alerting/set-up/provision-alerting-resources/export-alerting-resources/index.md index 01236a56fd..e7fe122856 100644 --- a/docs/sources/alerting/set-up/provision-alerting-resources/export-alerting-resources/index.md +++ b/docs/sources/alerting/set-up/provision-alerting-resources/export-alerting-resources/index.md @@ -137,6 +137,8 @@ The **Alerting HTTP API** provides specific endpoints for exporting alerting res | Alert rules | GET /api/v1/provisioning/alert-rules/:uid/export | [Export an alert rule in provisioning file format.][export_rule] | | Contact points | GET /api/v1/provisioning/contact-points/export | [Export all contact points in provisioning file format.][export_contacts] | | Notification policy tree | GET /api/v1/provisioning/policies/export | [Export the notification policy tree in provisioning file format.][export_notifications] | +| Mute timings | GET /api/v1/provisioning/mute-timings/export | [Export all mute timings in provisioning file format.][export_mute_timings] | +| Mute timings | GET /api/v1/provisioning/mute-timings/:name/export | [Export a mute timing in provisioning file format.][export_mute_timing] | These endpoints accept a `download` parameter to download a file containing the exported resources. @@ -168,6 +170,12 @@ These endpoints accept a `download` parameter to download a file containing the [export_contacts]: "/docs/grafana/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/http-api-provisioning/#span-idroute-get-contactpoints-exportspan-export-all-contact-points-in-provisioning-file-format-_routegetcontactpointsexport_" [export_contacts]: "/docs/grafana-cloud/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/http-api-provisioning/#span-idroute-get-contactpoints-exportspan-export-all-contact-points-in-provisioning-file-format-_routegetcontactpointsexport_" +[export_mute_timing]: "/docs/grafana/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/http-api-provisioning/#span-idroute-get-mute-timing-exportspan-export-a-mute-timing-in-provisioning-file-format-_routegetmutetimingexport_" +[export_mute_timing]: "/docs/grafana-cloud/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/http-api-provisioning/#span-idroute-get-mute-timing-exportspan-export-a-mute-timing-in-provisioning-file-format-_routegetmutetimingexport_" + +[export_mute_timings]: "/docs/grafana/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/http-api-provisioning/#span-idroute-get-mute-timings-exportspan-export-all-mute-timings-in-provisioning-file-format-_routegetmutetimingsexport_" +[export_mute_timings]: "/docs/grafana-cloud/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/http-api-provisioning/#span-idroute-get-mute-timings-exportspan-export-all-mute-timings-in-provisioning-file-format-_routegetmutetimingsexport_" + [export_notifications]: "/docs/grafana/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/http-api-provisioning/#span-idroute-get-policy-tree-exportspan-export-the-notification-policy-tree-in-provisioning-file-format-_routegetpolicytreeexport_" [export_notifications]: "/docs/grafana-cloud/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/http-api-provisioning/#span-idroute-get-policy-tree-exportspan-export-the-notification-policy-tree-in-provisioning-file-format-_routegetpolicytreeexport_" {{% /docs/reference %}} diff --git a/docs/sources/shared/alerts/alerting_provisioning.md b/docs/sources/shared/alerts/alerting_provisioning.md index c616cde9cc..eb7df8a9ca 100644 --- a/docs/sources/shared/alerts/alerting_provisioning.md +++ b/docs/sources/shared/alerts/alerting_provisioning.md @@ -145,13 +145,15 @@ For managing resources related to [data source-managed alerts]({{< relref "/docs ### Mute timings -| Method | URI | Name | Summary | -| ------ | --------------------------------------- | ----------------------------------------------------- | -------------------------------- | -| DELETE | /api/v1/provisioning/mute-timings/:name | [route delete mute timing](#route-delete-mute-timing) | Delete a mute timing. | -| GET | /api/v1/provisioning/mute-timings/:name | [route get mute timing](#route-get-mute-timing) | Get a mute timing. | -| GET | /api/v1/provisioning/mute-timings | [route get mute timings](#route-get-mute-timings) | Get all the mute timings. | -| POST | /api/v1/provisioning/mute-timings | [route post mute timing](#route-post-mute-timing) | Create a new mute timing. | -| PUT | /api/v1/provisioning/mute-timings/:name | [route put mute timing](#route-put-mute-timing) | Replace an existing mute timing. | +| Method | URI | Name | Summary | +| ------ | ---------------------------------------------- | --------------------------------------------------------------- | ---------------------------------------------------- | +| DELETE | /api/v1/provisioning/mute-timings/:name | [route delete mute timing](#route-delete-mute-timing) | Delete a mute timing. | +| GET | /api/v1/provisioning/mute-timings/:name | [route get mute timing](#route-get-mute-timing) | Get a mute timing. | +| GET | /api/v1/provisioning/mute-timings | [route get mute timings](#route-get-mute-timings) | Get all the mute timings. | +| POST | /api/v1/provisioning/mute-timings | [route post mute timing](#route-post-mute-timing) | Create a new mute timing. | +| PUT | /api/v1/provisioning/mute-timings/:name | [route put mute timing](#route-put-mute-timing) | Replace an existing mute timing. | +| GET | /api/v1/provisioning/mute-timings/export | [route get mute timings export](#route-get-mute-timings-export) | Export all mute timings in provisioning file format. | +| GET | /api/v1/provisioning/mute-timings/:name/export | [route get mute timing export](#route-get-mute-timing-export) | Export a mute timing in provisioning file format. | ### Templates @@ -631,6 +633,83 @@ Status: OK [MuteTimings](#mute-timings) +### Export all mute timings in provisioning file format. (_RouteGetMuteTimingsExport_) + +``` +GET /api/v1/provisioning/mute-timings/export +``` + +#### Parameters + +| Name | Source | Type | Go type | Separator | Required | Default | Description | +| -------- | ------- | ------- | -------- | --------- | :------: | -------- | --------------------------------------------------------------------------------------------------------------------------------- | +| download | `query` | boolean | `bool` | | | | Whether to initiate a download of the file or not. | +| format | `query` | string | `string` | | | `"yaml"` | Format of the downloaded file, either yaml or json. Accept header can also be used, but the query parameter will take precedence. | + +#### All responses + +| Code | Status | Description | Has headers | Schema | +| ----------------------------------------- | --------- | ----------------- | :---------: | --------------------------------------------------- | +| [200](#route-get-mute-timings-export-200) | OK | MuteTimingsExport | | [schema](#route-get-mute-timings-export-200-schema) | +| [403](#route-get-mute-timings-export-403) | Forbidden | PermissionDenied | | [schema](#route-get-mute-timings-export-403-schema) | + +#### Responses + +##### 200 - MuteTimingsExport + +Status: OK + +###### Schema + +[AlertingFileExport](#alerting-file-export) + +##### 403 - PermissionDenied + +Status: Forbidden + +###### Schema + +[PermissionDenied](#permission-denied) + +### Export a mute timing in provisioning file format. (_RouteGetMuteTimingExport_) + +``` +GET /api/v1/provisioning/mute-timings/:name/export +``` + +#### Parameters + +| Name | Source | Type | Go type | Separator | Required | Default | Description | +| -------- | ------- | ------- | -------- | --------- | :------: | -------- | --------------------------------------------------------------------------------------------------------------------------------- | +| name | `path` | string | `string` | | ✓ | | Mute timing name. | +| download | `query` | boolean | `bool` | | | | Whether to initiate a download of the file or not. | +| format | `query` | string | `string` | | | `"yaml"` | Format of the downloaded file, either yaml or json. Accept header can also be used, but the query parameter will take precedence. | + +#### All responses + +| Code | Status | Description | Has headers | Schema | +| ---------------------------------------- | --------- | ---------------- | :---------: | --------------------------------------------------- | +| [200](#route-get-mute-timing-export-200) | OK | MuteTimingExport | | [schema](#route-get-mute-timings-export-200-schema) | +| [403](#route-get-mute-timing-export-403) | Forbidden | PermissionDenied | | [schema](#route-get-mute-timings-export-403-schema) | + +#### Responses + +##### 200 - MuteTimingExport + +Status: OK + +###### Schema + +[AlertingFileExport](#alerting-file-export) + +##### 403 - PermissionDenied + +Status: Forbidden + +###### Schema + +[PermissionDenied](#permission-denied) + ### Get the notification policy tree. (_RouteGetPolicyTree_) ``` @@ -1393,6 +1472,14 @@ Status: Accepted {{% /responsive-table %}} +### MuteTimingExport + +**Properties** + +### MuteTimingsExport + +**Properties** + ### MuteTimings [][MuteTimeInterval](#mute-time-interval) From 9e12caebc7d5207f14a6715a6264d81b4d19f4ac Mon Sep 17 00:00:00 2001 From: Mitch Seaman Date: Mon, 4 Mar 2024 11:14:37 +0100 Subject: [PATCH 0356/1406] Docs: typo fix (remove an extra parenthesis) (#83811) --- docs/sources/datasources/azure-monitor/_index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/datasources/azure-monitor/_index.md b/docs/sources/datasources/azure-monitor/_index.md index 334459a19a..4579af5e1d 100644 --- a/docs/sources/datasources/azure-monitor/_index.md +++ b/docs/sources/datasources/azure-monitor/_index.md @@ -215,7 +215,7 @@ Grafana refers to such variables as template variables. For details, see the [template variables documentation]({{< relref "./template-variables" >}}). -## Application Insights and Insights Analytics (removed)) +## Application Insights and Insights Analytics (removed) Until Grafana v8.0, you could query the same Azure Application Insights data using Application Insights and Insights Analytics. From fa44aebeff842ab5b2e374695fd59b24d2f5269a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Fern=C3=A1ndez?= Date: Mon, 4 Mar 2024 11:38:00 +0100 Subject: [PATCH 0357/1406] ReturnToPrevious: Improve tests (#83812) --- .../AppChrome/ReturnToPrevious/ReturnToPrevious.test.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/public/app/core/components/AppChrome/ReturnToPrevious/ReturnToPrevious.test.tsx b/public/app/core/components/AppChrome/ReturnToPrevious/ReturnToPrevious.test.tsx index 8ff6c986e8..0f9b5d62c9 100644 --- a/public/app/core/components/AppChrome/ReturnToPrevious/ReturnToPrevious.test.tsx +++ b/public/app/core/components/AppChrome/ReturnToPrevious/ReturnToPrevious.test.tsx @@ -59,11 +59,11 @@ describe('ReturnToPrevious', () => { /* The report is called 'grafana_return_to_previous_button_dismissed' but the action is 'clicked' */ const mockReturn = mockCalls.filter((call) => call[0] === 'grafana_return_to_previous_button_dismissed'); expect(mockReturn).toHaveLength(1); + expect(mockReturn[0][1]).toEqual({ action: 'clicked', page: '/dashboards' }); }); it('should trigger event once when clicking on the Close button', async () => { setup(); - const closeBtn = await screen.findByRole('button', { name: 'Close' }); expect(closeBtn).toBeInTheDocument(); await userEvent.click(closeBtn); @@ -71,5 +71,6 @@ describe('ReturnToPrevious', () => { /* The report is called 'grafana_return_to_previous_button_dismissed' but the action is 'dismissed' */ const mockDismissed = mockCalls.filter((call) => call[0] === 'grafana_return_to_previous_button_dismissed'); expect(mockDismissed).toHaveLength(1); + expect(mockDismissed[0][1]).toEqual({ action: 'dismissed', page: '/dashboards' }); }); }); From 07e26226b7708be83d6c3c7ef21b48e20af5a59b Mon Sep 17 00:00:00 2001 From: Misi Date: Mon, 4 Mar 2024 11:55:59 +0100 Subject: [PATCH 0358/1406] Auth: Add all settings to Azure AD SSO config UI (#83618) * Add all settings to AzureAD UI * prettify * Fixes * Load extra keys with type assertion --- pkg/login/social/connectors/azuread_oauth.go | 9 +++- pkg/login/social/connectors/common.go | 12 +++++ pkg/login/social/connectors/generic_oauth.go | 8 +++- pkg/login/social/connectors/github_oauth.go | 5 ++- pkg/login/social/connectors/google_oauth.go | 5 ++- .../social/connectors/grafana_com_oauth.go | 4 +- .../ssosettings/strategies/oauth_strategy.go | 17 +++++-- .../strategies/oauth_strategy_test.go | 4 +- public/app/features/auth-config/fields.tsx | 44 ++++++++++++++++++- public/app/features/auth-config/types.ts | 2 + 10 files changed, 97 insertions(+), 13 deletions(-) diff --git a/pkg/login/social/connectors/azuread_oauth.go b/pkg/login/social/connectors/azuread_oauth.go index d40999e4cd..05f23c706f 100644 --- a/pkg/login/social/connectors/azuread_oauth.go +++ b/pkg/login/social/connectors/azuread_oauth.go @@ -31,7 +31,10 @@ import ( const forceUseGraphAPIKey = "force_use_graph_api" // #nosec G101 not a hardcoded credential var ( - ExtraAzureADSettingKeys = []string{forceUseGraphAPIKey, allowedOrganizationsKey} + ExtraAzureADSettingKeys = map[string]ExtraKeyInfo{ + forceUseGraphAPIKey: {Type: Bool, DefaultValue: false}, + allowedOrganizationsKey: {Type: String}, + } errAzureADMissingGroups = &SocialError{"either the user does not have any group membership or the groups claim is missing from the token."} ) @@ -80,7 +83,7 @@ func NewAzureADProvider(info *social.OAuthInfo, cfg *setting.Cfg, ssoSettings ss SocialBase: newSocialBase(social.AzureADProviderName, info, features, cfg), cache: cache, allowedOrganizations: util.SplitString(info.Extra[allowedOrganizationsKey]), - forceUseGraphAPI: MustBool(info.Extra[forceUseGraphAPIKey], false), + forceUseGraphAPI: MustBool(info.Extra[forceUseGraphAPIKey], ExtraAzureADSettingKeys[forceUseGraphAPIKey].DefaultValue.(bool)), } if info.UseRefreshToken { @@ -200,6 +203,8 @@ func (s *SocialAzureAD) Validate(ctx context.Context, settings ssoModels.SSOSett return validation.Validate(info, requester, validateAllowedGroups, + // FIXME: uncomment this after the Terraform provider is updated + //validation.MustBeEmptyValidator(info.ApiUrl, "API URL"), validation.RequiredUrlValidator(info.AuthUrl, "Auth URL"), validation.RequiredUrlValidator(info.TokenUrl, "Token URL")) } diff --git a/pkg/login/social/connectors/common.go b/pkg/login/social/connectors/common.go index 96bf84020b..6dcc051a31 100644 --- a/pkg/login/social/connectors/common.go +++ b/pkg/login/social/connectors/common.go @@ -18,6 +18,18 @@ import ( "github.com/grafana/grafana/pkg/util" ) +type ExtraFieldType int + +const ( + String ExtraFieldType = iota + Bool +) + +type ExtraKeyInfo struct { + Type ExtraFieldType + DefaultValue any +} + const ( // consider moving this to OAuthInfo teamIdsKey = "team_ids" diff --git a/pkg/login/social/connectors/generic_oauth.go b/pkg/login/social/connectors/generic_oauth.go index eec06750da..885736e7ae 100644 --- a/pkg/login/social/connectors/generic_oauth.go +++ b/pkg/login/social/connectors/generic_oauth.go @@ -28,7 +28,13 @@ const ( idTokenAttributeNameKey = "id_token_attribute_name" // #nosec G101 not a hardcoded credential ) -var ExtraGenericOAuthSettingKeys = []string{nameAttributePathKey, loginAttributePathKey, idTokenAttributeNameKey, teamIdsKey, allowedOrganizationsKey} +var ExtraGenericOAuthSettingKeys = map[string]ExtraKeyInfo{ + nameAttributePathKey: {Type: String}, + loginAttributePathKey: {Type: String}, + idTokenAttributeNameKey: {Type: String}, + teamIdsKey: {Type: String}, + allowedOrganizationsKey: {Type: String}, +} var _ social.SocialConnector = (*SocialGenericOAuth)(nil) var _ ssosettings.Reloadable = (*SocialGenericOAuth)(nil) diff --git a/pkg/login/social/connectors/github_oauth.go b/pkg/login/social/connectors/github_oauth.go index 007e576a8c..46a82de22a 100644 --- a/pkg/login/social/connectors/github_oauth.go +++ b/pkg/login/social/connectors/github_oauth.go @@ -24,7 +24,10 @@ import ( "github.com/grafana/grafana/pkg/util/errutil" ) -var ExtraGithubSettingKeys = []string{allowedOrganizationsKey, teamIdsKey} +var ExtraGithubSettingKeys = map[string]ExtraKeyInfo{ + allowedOrganizationsKey: {Type: String}, + teamIdsKey: {Type: String}, +} var _ social.SocialConnector = (*SocialGithub)(nil) var _ ssosettings.Reloadable = (*SocialGithub)(nil) diff --git a/pkg/login/social/connectors/google_oauth.go b/pkg/login/social/connectors/google_oauth.go index 84c6d9f96f..955c70ff9c 100644 --- a/pkg/login/social/connectors/google_oauth.go +++ b/pkg/login/social/connectors/google_oauth.go @@ -27,9 +27,12 @@ const ( validateHDKey = "validate_hd" ) +var ExtraGoogleSettingKeys = map[string]ExtraKeyInfo{ + validateHDKey: {Type: Bool, DefaultValue: true}, +} + var _ social.SocialConnector = (*SocialGoogle)(nil) var _ ssosettings.Reloadable = (*SocialGoogle)(nil) -var ExtraGoogleSettingKeys = []string{validateHDKey} type SocialGoogle struct { *SocialBase diff --git a/pkg/login/social/connectors/grafana_com_oauth.go b/pkg/login/social/connectors/grafana_com_oauth.go index c5961cb07d..694400aea4 100644 --- a/pkg/login/social/connectors/grafana_com_oauth.go +++ b/pkg/login/social/connectors/grafana_com_oauth.go @@ -20,7 +20,9 @@ import ( "github.com/grafana/grafana/pkg/util" ) -var ExtraGrafanaComSettingKeys = []string{allowedOrganizationsKey} +var ExtraGrafanaComSettingKeys = map[string]ExtraKeyInfo{ + allowedOrganizationsKey: {Type: String, DefaultValue: ""}, +} var _ social.SocialConnector = (*SocialGrafanaCom)(nil) var _ ssosettings.Reloadable = (*SocialGrafanaCom)(nil) diff --git a/pkg/services/ssosettings/strategies/oauth_strategy.go b/pkg/services/ssosettings/strategies/oauth_strategy.go index 7f67ad9c8e..578e80e31f 100644 --- a/pkg/services/ssosettings/strategies/oauth_strategy.go +++ b/pkg/services/ssosettings/strategies/oauth_strategy.go @@ -15,7 +15,7 @@ type OAuthStrategy struct { settingsByProvider map[string]map[string]any } -var extraKeysByProvider = map[string][]string{ +var extraKeysByProvider = map[string]map[string]connectors.ExtraKeyInfo{ social.AzureADProviderName: connectors.ExtraAzureADSettingKeys, social.GenericOAuthProviderName: connectors.ExtraGenericOAuthSettingKeys, social.GitHubProviderName: connectors.ExtraGithubSettingKeys, @@ -104,9 +104,18 @@ func (s *OAuthStrategy) loadSettingsForProvider(provider string) map[string]any "signout_redirect_url": section.Key("signout_redirect_url").Value(), } - extraFields := extraKeysByProvider[provider] - for _, key := range extraFields { - result[key] = section.Key(key).Value() + extraKeys := extraKeysByProvider[provider] + for key, keyInfo := range extraKeys { + switch keyInfo.Type { + case connectors.Bool: + result[key] = section.Key(key).MustBool(keyInfo.DefaultValue.(bool)) + default: + if _, ok := keyInfo.DefaultValue.(string); !ok { + result[key] = section.Key(key).Value() + } else { + result[key] = section.Key(key).MustString(keyInfo.DefaultValue.(string)) + } + } } return result diff --git a/pkg/services/ssosettings/strategies/oauth_strategy_test.go b/pkg/services/ssosettings/strategies/oauth_strategy_test.go index 3cf7031d7d..2d5a91bb54 100644 --- a/pkg/services/ssosettings/strategies/oauth_strategy_test.go +++ b/pkg/services/ssosettings/strategies/oauth_strategy_test.go @@ -147,7 +147,7 @@ func TestGetProviderConfig_ExtraFields(t *testing.T) { result, err := strategy.GetProviderConfig(context.Background(), social.AzureADProviderName) require.NoError(t, err) - require.Equal(t, "true", result["force_use_graph_api"]) + require.Equal(t, true, result["force_use_graph_api"]) require.Equal(t, "org1, org2", result["allowed_organizations"]) }) @@ -181,7 +181,7 @@ func TestGetProviderConfig_ExtraFields(t *testing.T) { result, err := strategy.GetProviderConfig(context.Background(), social.GoogleProviderName) require.NoError(t, err) - require.Equal(t, "true", result["validate_hd"]) + require.Equal(t, true, result["validate_hd"]) }) } diff --git a/public/app/features/auth-config/fields.tsx b/public/app/features/auth-config/fields.tsx index 5a095e4f95..38c28e519c 100644 --- a/public/app/features/auth-config/fields.tsx +++ b/public/app/features/auth-config/fields.tsx @@ -14,7 +14,6 @@ export const fields: Record; export const sectionFields: Section = { + azuread: [ + { + name: 'General settings', + id: 'general', + fields: [ + 'name', + 'clientId', + 'clientSecret', + 'scopes', + 'authUrl', + 'tokenUrl', + 'allowSignUp', + 'autoLogin', + 'signoutRedirectUrl', + ], + }, + { + name: 'User mapping', + id: 'user', + fields: ['roleAttributePath', 'roleAttributeStrict', 'allowAssignGrafanaAdmin', 'skipOrgRoleSync'], + }, + { + name: 'Extra security measures', + id: 'extra', + fields: [ + 'allowedOrganizations', + 'allowedDomains', + 'allowedGroups', + 'forceUseGraphApi', + 'usePkce', + 'useRefreshToken', + 'tlsSkipVerifyInsecure', + 'tlsClientCert', + 'tlsClientKey', + 'tlsClientCa', + ], + }, + ], generic_oauth: [ { name: 'General settings', @@ -320,6 +357,11 @@ export function fieldMap(provider: string): Record { label: 'Define allowed teams ids', type: 'switch', }, + forceUseGraphApi: { + label: 'Force use Graph API', + description: "If enabled, Grafana will fetch the users' groups using the Microsoft Graph API.", + type: 'checkbox', + }, usePkce: { label: 'Use PKCE', description: ( diff --git a/public/app/features/auth-config/types.ts b/public/app/features/auth-config/types.ts index db9040efd4..02d5d1fe3b 100644 --- a/public/app/features/auth-config/types.ts +++ b/public/app/features/auth-config/types.ts @@ -53,6 +53,8 @@ export type SSOProviderSettingsBase = { defineAllowedTeamsIds?: boolean; configureTLS?: boolean; tlsSkipVerifyInsecure?: boolean; + // For Azure AD + forceUseGraphApi?: boolean; }; // SSO data received from the API and sent to it From 111df1bba0efb85f7a4a3d7e54afe485cc30ff37 Mon Sep 17 00:00:00 2001 From: Pepe Cano <825430+ppcano@users.noreply.github.com> Date: Mon, 4 Mar 2024 12:10:37 +0100 Subject: [PATCH 0359/1406] Alerting docs: update the Terraform Provision guide (#83773) * Alerting docs: update the Terraform Provision guide * Fix incorrect links * Update docs/sources/alerting/set-up/provision-alerting-resources/terraform-provisioning/index.md Co-authored-by: brendamuir <100768211+brendamuir@users.noreply.github.com> * Update docs/sources/alerting/set-up/provision-alerting-resources/terraform-provisioning/index.md Co-authored-by: brendamuir <100768211+brendamuir@users.noreply.github.com> * Update docs/sources/alerting/set-up/provision-alerting-resources/terraform-provisioning/index.md Co-authored-by: brendamuir <100768211+brendamuir@users.noreply.github.com> --------- Co-authored-by: brendamuir <100768211+brendamuir@users.noreply.github.com> --- .../terraform-provisioning/index.md | 487 ++++++++++-------- 1 file changed, 260 insertions(+), 227 deletions(-) diff --git a/docs/sources/alerting/set-up/provision-alerting-resources/terraform-provisioning/index.md b/docs/sources/alerting/set-up/provision-alerting-resources/terraform-provisioning/index.md index 58a86af32e..44953dc203 100644 --- a/docs/sources/alerting/set-up/provision-alerting-resources/terraform-provisioning/index.md +++ b/docs/sources/alerting/set-up/provision-alerting-resources/terraform-provisioning/index.md @@ -23,267 +23,103 @@ weight: 200 Use Terraform’s Grafana Provider to manage your alerting resources and provision them into your Grafana system. Terraform provider support for Grafana Alerting makes it easy to create, manage, and maintain your entire Grafana Alerting stack as code. -Refer to [Grafana Terraform Provider](https://registry.terraform.io/providers/grafana/grafana/latest/docs) documentation for more examples and information on Terraform Alerting schemas. +This guide outlines the steps and references to provision alerting resources with Terraform. For a practical demo, you can clone and try this [example using Grafana OSS and Docker Compose](https://github.com/grafana/provisioning-alerting-examples/tree/main/terraform). -Complete the following tasks to create and manage your alerting resources using Terraform. - -1. Create an API key for provisioning. -1. Configure the Terraform provider. -1. Define your alerting resources in Terraform. [Export alerting resources][alerting_export] in Terraform format, or implement the [Terraform Alerting schemas](https://registry.terraform.io/providers/grafana/grafana/latest/docs). +To create and manage your alerting resources using Terraform, you have to complete the following tasks. +1. Create an API key to configure the Terraform provider. +1. Create your alerting resources in Terraform format by + - [exporting configured alerting resources][alerting_export] + - or writing the [Terraform Alerting schemas](https://registry.terraform.io/providers/grafana/grafana/latest/docs). + > By default, you cannot edit provisioned resources. Enable [`disable_provenance` in the Terraform resource](#enable-editing-resources-in-the-grafana-ui) to allow changes in the Grafana UI. 1. Run `terraform apply` to provision your alerting resources. -{{< admonition type="note" >}} - -- By default, you cannot edit resources provisioned from Terraform from the UI. This ensures that your alerting stack always stays in sync with your code. To change the default behaviour, refer to [Edit provisioned resources in the Grafana UI](#edit-provisioned-resources-in-the-grafana-ui). - -- Before you begin, ensure you have the [Grafana Terraform Provider](https://registry.terraform.io/providers/grafana/grafana/) 1.27.0 or higher, and are using Grafana 9.1 or higher. - -{{< /admonition >}} - -## Create an API key for provisioning +Before you begin, you should have available a Grafana instance and [Terraform installed](https://www.terraform.io/downloads) on your machine. -You can create a [service account token][service-accounts] to authenticate Terraform with Grafana. Most existing tooling using API keys should automatically work with the new Grafana Alerting support. +## Create an API key and configure the Terraform provider -There are also dedicated RBAC roles for alerting provisioning. This lets you easily authenticate as a service account with the minimum permissions needed to provision your Alerting infrastructure. - -To create an API key for provisioning, complete the following steps. +You can create a [service account token][service-accounts] to authenticate Terraform with Grafana. To create an API key for provisioning alerting resources, complete the following steps. 1. Create a new service account. 1. Assign the role or permission to access the [Alerting provisioning API][alerting_http_provisioning]. 1. Create a new service account token. 1. Name and save the token for use in Terraform. -Alternatively, you can use basic authentication. To view all the supported authentication formats, see [here](https://registry.terraform.io/providers/grafana/grafana/latest/docs#authentication). - -## Configure the Terraform provider - -Grafana Alerting support is included as part of the [Grafana Terraform provider](https://registry.terraform.io/providers/grafana/grafana/latest/docs). +You can now move to the working directory for your Terraform configurations, and create a file named `main.tf` like: -The following is an example you can use to configure the Terraform provider. - -```HCL +```main.tf terraform { required_providers { grafana = { source = "grafana/grafana" - version = ">= 1.28.2" + version = ">= 2.9.0" } } } provider "grafana" { - url = - auth = + url = + auth = } ``` -## Import contact points and templates - -Contact points connect an alerting stack to the outside world. They tell Grafana how to connect to your external systems and where to deliver notifications. - -To provision contact points and templates, refer to the [grafana_contact_point schema](https://registry.terraform.io/providers/grafana/grafana/latest/docs/resources/contact_point) and [grafana_message_template schema](https://registry.terraform.io/providers/grafana/grafana/latest/docs/resources/message_template), and complete the following steps. - -1. Copy this code block into a `.tf` file on your local machine. - - This example creates a contact point that sends alert notifications to Slack. - - ```HCL - resource "grafana_contact_point" "my_slack_contact_point" { - name = "Send to My Slack Channel" - - slack { - url = - text = <` with the URL of the Grafana instance. +- `` with the API token previously created. -1. Go to the Grafana UI and check the details of your contact point. +This Terraform configuration installs the [Grafana Terraform provider](https://registry.terraform.io/providers/grafana/grafana/latest/docs) and authenticates against your Grafana instance using an API token. For other authentication alternatives including basic authentication, refer to the [`auth` option documentation](https://registry.terraform.io/providers/grafana/grafana/latest/docs#authentication). -1. Click **Test** to verify that the contact point works correctly. +For Grafana Cloud, refer to the [instructions to manage a Grafana Cloud stack with Terraform][provision-cloud-with-terraform]. For role-based access control, refer to [Provisioning RBAC with Terraform][rbac-terraform-provisioning] and the [alerting provisioning roles (`fixed:alerting.provisioning.*`)][rbac-role-definitions]. -### Reuse templates +## Create Terraform configurations for alerting resources -You can reuse the same templates across many contact points. In the example above, a shared template ie embedded using the statement `{{ template “Alert Instance Template” . }}` +[Grafana Terraform provider](https://registry.terraform.io/providers/grafana/grafana/latest/docs) enables you to manage the following alerting resources. -This fragment can then be managed separately in Terraform: +| Alerting resource | Terraform resource | +| ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | +| [Alert rules][alerting-rules] | [grafana_rule_group](https://registry.terraform.io/providers/grafana/grafana/latest/docs/resources/rule_group) | +| [Contact points][contact-points] | [grafana_contact_point](https://registry.terraform.io/providers/grafana/grafana/latest/docs/resources/contact_point) | +| [Notification templates][notification-template] | [grafana_message_template](https://registry.terraform.io/providers/grafana/grafana/latest/docs/resources/message_template) | +| [Notification policy tree][notification-policy] | [grafana_notification_policy](https://registry.terraform.io/providers/grafana/grafana/latest/docs/resources/notification_policy) | +| [Mute timings][mute-timings] | [grafana_mute_timing](https://registry.terraform.io/providers/grafana/grafana/latest/docs/resources/mute_timing) | -```HCL -resource "grafana_message_template" "my_alert_template" { - name = "Alert Instance Template" +In this section, we'll create Terraform configurations for each alerting resource and demonstrate how to link them together. - template = <}} +### Add alert rules -1. Copy this code block into a `.tf` file on your local machine. +[Alert rules][alerting-rules] enable you to receive alerts by querying any backend Grafana data sources. - In this example, the alerts are grouped by `alertname`, which means that any notifications coming from alerts which share the same name, are grouped into the same Slack message. You can provide any set of label keys here, or you can use the special label `"..."` to route by all label keys, sending each alert in a separate notification. - - If you want to route specific notifications differently, you can add sub-policies. Sub-policies allow you to apply routing to different alerts based on label matching. In this example, we apply a mute timing to all alerts with the label a=b. - - ```HCL - resource "grafana_notification_policy" "my_policy" { - group_by = ["alertname"] - contact_point = grafana_contact_point.my_slack_contact_point.name - - group_wait = "45s" - group_interval = "6m" - repeat_interval = "3h" - - policy { - matcher { - label = "a" - match = "=" - value = "b" - } - group_by = ["..."] - contact_point = grafana_contact_point.a_different_contact_point.name - mute_timings = [grafana_mute_timing.my_mute_timing.name] - - policy { - matcher { - label = "sublabel" - match = "=" - value = "subvalue" - } - contact_point = grafana_contact_point.a_third_contact_point.name - group_by = ["..."] - } - } - } - ``` - -1. In the mute_timings field, link a mute timing to your notification policy. - -1. Run the command `terraform apply`. - -1. Go to the Grafana UI and check the details of your notification policy. - -1. Click **Test** to verify that the notification point is working correctly. - -## Import mute timings - -Mute timings provide the ability to mute alert notifications for defined time periods. - -To provision mute timings, refer to the [grafana_mute_timing schema](https://registry.terraform.io/providers/grafana/grafana/latest/docs/resources/mute_timing), and complete the following steps. - -1. Copy this code block into a `.tf` file on your local machine. - - In this example, alert notifications are muted on weekends. - - ```HCL - resource "grafana_mute_timing" "my_mute_timing" { - name = "My Mute Timing" - - intervals { - times { - start = "04:56" - end = "14:17" - } - weekdays = ["saturday", "sunday", "tuesday:thursday"] - months = ["january:march", "12"] - years = ["2025:2027"] - } - } - ``` - -1. Run the command `terraform apply`. -1. Go to the Grafana UI and check the details of your mute timing. -1. Reference your newly created mute timing in a notification policy using the `mute_timings` field. - This will apply your mute timing to some or all of your notifications. - -1. Click **Test** to verify that the mute timing is working correctly. - -## Import alert rules - -[Alert rules][alerting-rules] enable you to alert against any Grafana data source. This can be a data source that you already have configured, or you can [define your data sources in Terraform](https://registry.terraform.io/providers/grafana/grafana/latest/docs/resources/data_source) alongside your alert rules. - -To provision alert rules, refer to the [grafana_rule_group schema](https://registry.terraform.io/providers/grafana/grafana/latest/docs/resources/rule_group), and complete the following steps. - -1. Create a data source to query and a folder to store your rules in. +1. First, create a data source to query and a folder to store your rules in. In this example, the [TestData][testdata] data source is used. - Alerts can be defined against any backend datasource in Grafana. - - ```HCL - resource "grafana_data_source" "testdata_datasource" { + ```terraform + resource "grafana_data_source" "" { name = "TestData" type = "testdata" } - resource "grafana_folder" "rule_folder" { + resource "grafana_folder" "" { title = "My Rule Folder" } ``` -1. Define an alert rule. + Replace the following field values: + + - `` with the terraform name of the data source. + - `` with the terraform name of the folder. - For more information on alert rules, refer to [how to create Grafana-managed alerts](/blog/2022/08/01/grafana-alerting-video-how-to-create-alerts-in-grafana-9/). +1. Create or find an alert rule you want to import in Grafana. -1. Create a rule group containing one or more rules. +1. [Export][alerting_export] the alert rule group in Terraform format. This exports the alert rule group as [`grafana_rule_group` Terraform resource](https://registry.terraform.io/providers/grafana/grafana/latest/docs/resources/rule_group). - In this example, the `grafana_rule_group` resource group is used. + You can edit the exported resource, or alternatively, consider creating the resource from scratch. - ```HCL - resource "grafana_rule_group" "my_rule_group" { + ```terraform + resource "grafana_rule_group" "" { name = "My Alert Rules" - folder_uid = grafana_folder.rule_folder.uid + folder_uid = grafana_folder..uid interval_seconds = 60 org_id = 1 @@ -299,7 +135,7 @@ To provision alert rules, refer to the [grafana_rule_group schema](https://regis from = 600 to = 0 } - datasource_uid = grafana_data_source.testdata_datasource.uid + datasource_uid = grafana_data_source..uid // `model` is a JSON blob that sends datasource-specific data. // It's different for every datasource. The alert's query is defined here. model = jsonencode({ @@ -343,42 +179,232 @@ To provision alert rules, refer to the [grafana_rule_group schema](https://regis } ``` -1. Run the command `terraform apply`. -1. Go to the Grafana UI and check your alert rule. + Replace the following field values: -You can see whether or not the alert rule is firing. You can also see a visualization of each of the alert rule’s query stages + - `` with the name of the alert rule group. -When the alert fires, Grafana routes a notification through the policy you defined. + Note that the distinct Grafana resources are connected through `uid` values in their Terraform configurations. The `uid` value will be randomly generated when provisioning. -For example, if you chose Slack as a contact point, Grafana’s embedded [Alertmanager](https://github.com/prometheus/alertmanager) automatically posts a message to Slack. + To link the alert rule group with its respective data source and folder in this example, replace the following field values: -## Edit provisioned resources in the Grafana UI + - `` with the terraform name of the previously defined data source. + - `` with the terraform name of the the previously defined folder. -By default, you cannot edit resources provisioned via Terraform in Grafana. To enable editing these resources in the Grafana UI, use the `disable_provenance` attribute on alerting resources: +1. Continue to add more Grafana resources or [use the Terraform CLI for provisioning](#provision-grafana-resources-with-terraform). -```HCL -provider "grafana" { - url = "http://grafana.example.com/" - auth = var.grafana_auth +### Add contact points + +[Contact points][contact-points] are the receivers of alert notifications. + +1. Create or find the contact points you want to import in Grafana. Alternatively, consider writing the resource in code as demonstrated in the example below. + +1. [Export][alerting_export] the contact point in Terraform format. This exports the contact point as [`grafana_contact_point` Terraform resource](https://registry.terraform.io/providers/grafana/grafana/latest/docs/resources/contact_point)—edit it if necessary. + +1. In this example, notifications are muted on weekends. + + ```terraform + resource "grafana_contact_point" "" { + name = "My contact point email" + + email { + addresses = [""] + } + } + ``` + + Replace the following field values: + + - `` with the terraform name of the contact point. It will be used to reference the contact point in other Terraform resources. + - `` with the email to receive alert notifications. + +1. Continue to add more Grafana resources or [use the Terraform CLI for provisioning](#provision-grafana-resources-with-terraform). + +### Add and enable templates + +[Notification templates][notification-template] allow customization of alert notifications across multiple contact points. + +1. Create or find the notification template you want to import in Grafana. Alternatively, consider writing the resource in code as demonstrated in the example below. + +1. [Export][alerting_export] the template as [`grafana_message_template` Terraform resource](https://registry.terraform.io/providers/grafana/grafana/latest/docs/resources/message_template). + + This example is a simple demo template defined as `custom_email.message`. + + ```terraform + resource "grafana_message_template" "" { + name = "custom_email.message" + + template = <" { + name = "My contact point email" + + email { + addresses = [""] + message = "{{ template \"custom_email.message\" .}}" + } + } + ``` + +1. Continue to add more Grafana resources or [use the Terraform CLI for provisioning](#provision-grafana-resources-with-terraform). + +### Add mute timings + +[Mute timings][mute-timings] pause alert notifications during predetermined intervals. + +1. Create or find the mute timings you want to import in Grafana. Alternatively, consider writing the resource in code as demonstrated in the example below. + +1. [Export][alerting_export] the mute timing in Terraform format. This exports the mute timing as [`grafana_mute_timing` Terraform resource](https://registry.terraform.io/providers/grafana/grafana/latest/docs/resources/mute_timing)—edit it if necessary. + +1. This example turns off notifications on weekends. + + ```terraform + resource "grafana_mute_timing" "" { + name = "No weekends" + + intervals { + weekdays = ["saturday", "sunday"] + } + } + ``` + + Replace the following field values: + + - `` with the name of the Terraform resource. It will be used to reference the mute timing in the Terraform notification policy tree. + +1. Continue to add more Grafana resources or [use the Terraform CLI for provisioning](#provision-grafana-resources-with-terraform). + +### Add the notification policy tree + +[Notification policies][notification-policy] defines how to route alert instances to your contact points. + +{{% admonition type="warning" %}} + +Since the policy tree is a single resource, provisioning the `grafana_notification_policy` resource will overwrite a policy tree created through any other means. + +{{< /admonition >}} + +1. Find the default notification policy tree. Alternatively, consider writing the resource in code as demonstrated in the example below. + +1. [Export][alerting_export] the notification policy tree in Terraform format. This exports it as [`grafana_notification_policy` Terraform resource](https://registry.terraform.io/providers/grafana/grafana/latest/docs/resources/notification_policy)—edit it if necessary. + + ```terraform + resource "grafana_notification_policy" "my_policy_tree" { + contact_point = grafana_contact_point..name + ... + + policy { + contact_point = grafana_contact_point..name + + matcher {...} + + mute_timings = [grafana_mute_timing..name] + } + } + ``` + + To configure the mute timing and contact point previously created in the notification policy tree, replace the following field values: + + - `` with the terraform name of the previously defined contact point. + - `` with the terraform name of the the previously defined mute timing. + +1. Continue to add more Grafana resources or [use the Terraform CLI for provisioning](#provision-grafana-resources-with-terraform). + +### Enable editing resources in the Grafana UI + +By default, you cannot edit resources provisioned via Terraform in Grafana. This ensures that your alerting stack always stays in sync with your Terraform code. + +To make provisioned resources editable in the Grafana UI, enable the `disable_provenance` attribute on alerting resources. + +```terraform +resource "grafana_contact_point" "my_contact_point" { + name = "My Contact Point" + + disable_provenance = true } -resource "grafana_mute_timing" "mute_all" { - name = "mute all" +resource "grafana_message_template" "my_template" { + name = "My Reusable Template" + template = "{{define \"My Reusable Template\" }}\n template content\n{{ end }}" + disable_provenance = true - intervals {} } +... ``` +Note that `disable_provenance` is not supported for [grafana_mute_timing](https://registry.terraform.io/providers/grafana/grafana/latest/docs/resources/mute_timing). + +## Provision Grafana resources with Terraform + +To create the previous alerting resources in Grafana with the Terraform CLI, complete the following steps. + +1. Initialize the working directory containing the Terraform configuration files. + + ```shell + terraform init + ``` + + This command initializes the Terraform directory, installing the Grafana Terraform provider configured in the `main.tf` file. + +1. Apply the Terraform configuration files to provision the resources. + + ```shell + terraform apply + ``` + + Before applying any changes to Grafana, Terraform displays the execution plan and requests your approval. + + ```shell + Plan: 4 to add, 0 to change, 0 to destroy. + + Do you want to perform these actions? + Terraform will perform the actions described above. + Only 'yes' will be accepted to approve. + + Enter a value: + ``` + + Once you have confirmed to proceed with the changes, Terraform will create the provisioned resources in Grafana! + + ```shell + Apply complete! Resources: 4 added, 0 changed, 0 destroyed. + ``` + +You can now access Grafana to verify the creation of the distinct resources. + ## More examples -- [Grafana Terraform Provider documentation](https://registry.terraform.io/providers/grafana/grafana/latest/docs) -- [Creating and managing a Grafana Cloud stack using Terraform](https://grafana.com/docs/grafana-cloud/developer-resources/infrastructure-as-code/terraform/terraform-cloud-stack) +For more examples on the concept of this guide: + +- Try the demo [provisioning alerting resources in Grafana OSS using Terraform and Docker Compose](https://github.com/grafana/provisioning-alerting-examples/tree/main/terraform). +- Review all the available options and examples of the Terraform Alerting schemas in the [Grafana Terraform Provider documentation](https://registry.terraform.io/providers/grafana/grafana/latest/docs). +- Review the [tutorial to manage a Grafana Cloud stack using Terraform][provision-cloud-with-terraform]. {{% docs/reference %}} [alerting-rules]: "/docs/grafana/ -> /docs/grafana//alerting/alerting-rules" [alerting-rules]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/alerting-rules" +[contact-points]: "/docs/grafana/ -> /docs/grafana//alerting/configure-notifications/manage-contact-points" +[contact-points]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/manage-contact-points" + +[mute-timings]: "/docs/grafana/ -> /docs/grafana//alerting/configure-notifications/mute-timings" +[mute-timings]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/mute-timings" + +[notification-policy]: "/docs/grafana/ -> /docs/grafana//alerting/configure-notifications/create-notification-policy" +[notification-policy]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/create-notification-policy" + +[notification-template]: "/docs/grafana/ -> /docs/grafana//alerting/configure-notifications/template-notifications" +[notification-template]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/template-notifications" + [alerting_export]: "/docs/grafana/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/export-alerting-resources" [alerting_export]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/set-up/provision-alerting-resources/export-alerting-resources" @@ -389,4 +415,11 @@ resource "grafana_mute_timing" "mute_all" { [testdata]: "/docs/grafana/ -> /docs/grafana//datasources/testdata" [testdata]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/connect-externally-hosted/data-sources/testdata" + +[provision-cloud-with-terraform]: "/docs/ -> /docs/grafana-cloud/developer-resources/infrastructure-as-code/terraform/terraform-cloud-stack" + +[rbac-role-definitions]: "/docs/ -> /docs/grafana//administration/roles-and-permissions/access-control/rbac-fixed-basic-role-definitions" + +[rbac-terraform-provisioning]: "/docs/ -> /docs/grafana//administration/roles-and-permissions/access-control/rbac-terraform-provisioning" + {{% /docs/reference %}} From 67c062acc7cf0dd83921ec87660e4e3da841d763 Mon Sep 17 00:00:00 2001 From: linoman <2051016+linoman@users.noreply.github.com> Date: Mon, 4 Mar 2024 05:45:07 -0600 Subject: [PATCH 0360/1406] Adjust HD validation default value (#83818) --- pkg/login/social/connectors/google_oauth.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/login/social/connectors/google_oauth.go b/pkg/login/social/connectors/google_oauth.go index 955c70ff9c..dabf0f25f5 100644 --- a/pkg/login/social/connectors/google_oauth.go +++ b/pkg/login/social/connectors/google_oauth.go @@ -96,7 +96,7 @@ func (s *SocialGoogle) Reload(ctx context.Context, settings ssoModels.SSOSetting defer s.reloadMutex.Unlock() s.updateInfo(social.GoogleProviderName, newInfo) - s.validateHD = MustBool(newInfo.Extra[validateHDKey], false) + s.validateHD = MustBool(newInfo.Extra[validateHDKey], true) return nil } From 82a88cc83f1c6a9d6759d1b4936de0643a4cada9 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Mon, 4 Mar 2024 15:29:13 +0300 Subject: [PATCH 0361/1406] Access control: Extend GetUserPermissions() to query permissions in org (#83392) * Access control: Extend GetUserPermissions() to query permissions in specific org * Use db query to fetch permissions in org * refactor * refactor * use conditional join * minor refactor * Add test cases * Search permissions correctly in OSS vs Enterprise * Get permissions from memory * Refactor * remove unused func * Add tests for GetUserPermissionsInOrg * fix linter --- pkg/api/org_users.go | 11 ++-- pkg/api/org_users_test.go | 1 + pkg/services/accesscontrol/accesscontrol.go | 3 + pkg/services/accesscontrol/acimpl/service.go | 49 +++++++++++++++- .../accesscontrol/acimpl/service_test.go | 56 +++++++++++++++++++ pkg/services/accesscontrol/actest/fake.go | 4 ++ .../accesscontrol/database/database.go | 34 +++++++---- .../accesscontrol/database/database_test.go | 18 ++++++ pkg/services/accesscontrol/mock/mock.go | 12 ++++ 9 files changed, 171 insertions(+), 17 deletions(-) diff --git a/pkg/api/org_users.go b/pkg/api/org_users.go index dec12f2ae2..9e1403467e 100644 --- a/pkg/api/org_users.go +++ b/pkg/api/org_users.go @@ -323,12 +323,13 @@ func (hs *HTTPServer) searchOrgUsersHelper(c *contextmodel.ReqContext, query *or // Get accesscontrol metadata and IPD labels for users in the target org accessControlMetadata := map[string]accesscontrol.Metadata{} - if c.QueryBool("accesscontrol") && c.SignedInUser.Permissions != nil { - // TODO https://github.com/grafana/identity-access-team/issues/268 - user access control service for fetching permissions from another organization - permissions, ok := c.SignedInUser.Permissions[query.OrgID] - if ok { - accessControlMetadata = accesscontrol.GetResourcesMetadata(c.Req.Context(), permissions, "users:id:", userIDs) + if c.QueryBool("accesscontrol") { + permissionsList, err := hs.accesscontrolService.GetUserPermissionsInOrg(c.Req.Context(), c.SignedInUser, query.OrgID) + permissions := accesscontrol.GroupScopesByAction(permissionsList) + if err != nil { + return nil, err } + accessControlMetadata = accesscontrol.GetResourcesMetadata(c.Req.Context(), permissions, "users:id:", userIDs) } for i := range filteredUsers { diff --git a/pkg/api/org_users_test.go b/pkg/api/org_users_test.go index d4f9951bcf..e1820ca615 100644 --- a/pkg/api/org_users_test.go +++ b/pkg/api/org_users_test.go @@ -368,6 +368,7 @@ func TestGetOrgUsersAPIEndpoint_AccessControlMetadata(t *testing.T) { } hs.authInfoService = &authinfotest.FakeService{} hs.userService = &usertest.FakeUserService{ExpectedSignedInUser: userWithPermissions(1, tt.permissions)} + hs.accesscontrolService = actest.FakeService{ExpectedPermissions: tt.permissions} }) url := "/api/orgs/1/users" diff --git a/pkg/services/accesscontrol/accesscontrol.go b/pkg/services/accesscontrol/accesscontrol.go index 8098cdc140..616d876a20 100644 --- a/pkg/services/accesscontrol/accesscontrol.go +++ b/pkg/services/accesscontrol/accesscontrol.go @@ -26,6 +26,8 @@ type Service interface { registry.ProvidesUsageStats // GetUserPermissions returns user permissions with only action and scope fields set. GetUserPermissions(ctx context.Context, user identity.Requester, options Options) ([]Permission, error) + // GetUserPermissionsInOrg return user permission in a specific organization + GetUserPermissionsInOrg(ctx context.Context, user identity.Requester, orgID int64) ([]Permission, error) // SearchUsersPermissions returns all users' permissions filtered by an action prefix SearchUsersPermissions(ctx context.Context, user identity.Requester, options SearchOptions) (map[int64][]Permission, error) // ClearUserPermissionCache removes the permission cache entry for the given user @@ -75,6 +77,7 @@ type SearchOptions struct { Scope string NamespacedID string // ID of the identity (ex: user:3, service-account:4) wildcards Wildcards // private field computed based on the Scope + RolePrefixes []string } // Wildcards computes the wildcard scopes that include the scope diff --git a/pkg/services/accesscontrol/acimpl/service.go b/pkg/services/accesscontrol/acimpl/service.go index 529d17d6ea..5cd05fd094 100644 --- a/pkg/services/accesscontrol/acimpl/service.go +++ b/pkg/services/accesscontrol/acimpl/service.go @@ -23,6 +23,7 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol/migrator" "github.com/grafana/grafana/pkg/services/accesscontrol/pluginutils" "github.com/grafana/grafana/pkg/services/auth/identity" + "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" @@ -41,6 +42,8 @@ var SharedWithMeFolderPermission = accesscontrol.Permission{ Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder.SharedWithMeFolderUID), } +var OSSRolesPrefixes = []string{accesscontrol.ManagedRolePrefix, accesscontrol.ExternalServiceRolePrefix} + func ProvideService(cfg *setting.Cfg, db db.DB, routeRegister routing.RouteRegister, cache *localcache.CacheService, accessControl accesscontrol.AccessControl, features featuremgmt.FeatureToggles) (*Service, error) { service := ProvideOSSService(cfg, database.ProvideService(db), cache, features) @@ -125,7 +128,7 @@ func (s *Service) getUserPermissions(ctx context.Context, user identity.Requeste UserID: userID, Roles: accesscontrol.GetOrgRoles(user), TeamIDs: user.GetTeams(), - RolePrefixes: []string{accesscontrol.ManagedRolePrefix, accesscontrol.ExternalServiceRolePrefix}, + RolePrefixes: OSSRolesPrefixes, }) if err != nil { return nil, err @@ -158,6 +161,48 @@ func (s *Service) getCachedUserPermissions(ctx context.Context, user identity.Re return permissions, nil } +func (s *Service) GetUserPermissionsInOrg(ctx context.Context, user identity.Requester, orgID int64) ([]accesscontrol.Permission, error) { + permissions := make([]accesscontrol.Permission, 0) + + if s.features.IsEnabled(ctx, featuremgmt.FlagNestedFolders) { + permissions = append(permissions, SharedWithMeFolderPermission) + } + + namespace, id := user.GetNamespacedID() + userID, err := identity.UserIdentifier(namespace, id) + if err != nil { + return nil, err + } + + // Get permissions for user's basic roles from RAM + roleList, err := s.store.GetUsersBasicRoles(ctx, []int64{userID}, orgID) + if err != nil { + return nil, fmt.Errorf("could not fetch basic roles for the user: %w", err) + } + var roles []string + var ok bool + if roles, ok = roleList[userID]; !ok { + return nil, fmt.Errorf("found no basic roles for user %d in organisation %d", userID, orgID) + } + for _, builtin := range roles { + if basicRole, ok := s.roles[builtin]; ok { + permissions = append(permissions, basicRole.Permissions...) + } + } + + dbPermissions, err := s.store.SearchUsersPermissions(ctx, orgID, accesscontrol.SearchOptions{ + NamespacedID: authn.NamespacedID(namespace, userID), + // Query only basic, managed and plugin roles in OSS + RolePrefixes: OSSRolesPrefixes, + }) + if err != nil { + return nil, err + } + + userPermissions := dbPermissions[userID] + return append(permissions, userPermissions...), nil +} + func (s *Service) ClearUserPermissionCache(user identity.Requester) { s.cache.Delete(permissionCacheKey(user)) } @@ -237,6 +282,8 @@ func (s *Service) DeclarePluginRoles(ctx context.Context, ID, name string, regs // SearchUsersPermissions returns all users' permissions filtered by action prefixes func (s *Service) SearchUsersPermissions(ctx context.Context, usr identity.Requester, options accesscontrol.SearchOptions) (map[int64][]accesscontrol.Permission, error) { + // Limit roles to available in OSS + options.RolePrefixes = OSSRolesPrefixes if options.NamespacedID != "" { userID, err := options.ComputeUserID() if err != nil { diff --git a/pkg/services/accesscontrol/acimpl/service_test.go b/pkg/services/accesscontrol/acimpl/service_test.go index a36ed4cdfe..88ea293cf7 100644 --- a/pkg/services/accesscontrol/acimpl/service_test.go +++ b/pkg/services/accesscontrol/acimpl/service_test.go @@ -938,3 +938,59 @@ func TestService_DeleteExternalServiceRole(t *testing.T) { }) } } + +func TestService_GetUserPermissionsInOrg(t *testing.T) { + tests := []struct { + name string + orgID int64 + ramRoles map[string]*accesscontrol.RoleDTO // BasicRole => RBAC BasicRole + storedPerms map[int64][]accesscontrol.Permission // UserID => Permissions + storedRoles map[int64][]string // UserID => Roles + want []accesscontrol.Permission + }{ + { + name: "should get correct permissions from another org", + orgID: 2, + ramRoles: map[string]*accesscontrol.RoleDTO{ + string(roletype.RoleEditor): {Permissions: []accesscontrol.Permission{}}, + string(roletype.RoleAdmin): {Permissions: []accesscontrol.Permission{ + {Action: accesscontrol.ActionTeamsRead, Scope: "teams:*"}, + }}, + }, + storedPerms: map[int64][]accesscontrol.Permission{ + 1: { + {Action: accesscontrol.ActionTeamsRead, Scope: "teams:id:1"}, + {Action: accesscontrol.ActionTeamsPermissionsRead, Scope: "teams:id:1"}, + }, + 2: { + {Action: accesscontrol.ActionTeamsRead, Scope: "teams:id:2"}, + }, + }, + storedRoles: map[int64][]string{ + 1: {string(roletype.RoleAdmin)}, + 2: {string(roletype.RoleEditor)}, + }, + want: []accesscontrol.Permission{ + {Action: accesscontrol.ActionTeamsRead, Scope: "teams:id:2"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + ac := setupTestEnv(t) + + ac.roles = tt.ramRoles + ac.store = actest.FakeStore{ + ExpectedUsersPermissions: tt.storedPerms, + ExpectedUsersRoles: tt.storedRoles, + } + user := &user.SignedInUser{OrgID: 1, UserID: 2} + + got, err := ac.GetUserPermissionsInOrg(ctx, user, 2) + require.Nil(t, err) + + assert.ElementsMatch(t, got, tt.want) + }) + } +} diff --git a/pkg/services/accesscontrol/actest/fake.go b/pkg/services/accesscontrol/actest/fake.go index 12e4330fee..21698bd410 100644 --- a/pkg/services/accesscontrol/actest/fake.go +++ b/pkg/services/accesscontrol/actest/fake.go @@ -27,6 +27,10 @@ func (f FakeService) GetUserPermissions(ctx context.Context, user identity.Reque return f.ExpectedPermissions, f.ExpectedErr } +func (f FakeService) GetUserPermissionsInOrg(ctx context.Context, user identity.Requester, orgID int64) ([]accesscontrol.Permission, error) { + return f.ExpectedPermissions, f.ExpectedErr +} + func (f FakeService) SearchUsersPermissions(ctx context.Context, user identity.Requester, options accesscontrol.SearchOptions) (map[int64][]accesscontrol.Permission, error) { return f.ExpectedUsersPermissions, f.ExpectedErr } diff --git a/pkg/services/accesscontrol/database/database.go b/pkg/services/accesscontrol/database/database.go index 6e61421ee2..fa91e95490 100644 --- a/pkg/services/accesscontrol/database/database.go +++ b/pkg/services/accesscontrol/database/database.go @@ -36,8 +36,8 @@ func (s *AccessControlStore) GetUserPermissions(ctx context.Context, query acces ` + filter if len(query.RolePrefixes) > 0 { - q += " WHERE ( " + strings.Repeat("role.name LIKE ? OR ", len(query.RolePrefixes)) - q = q[:len(q)-4] + " )" // remove last " OR " + q += " WHERE ( " + strings.Repeat("role.name LIKE ? OR ", len(query.RolePrefixes)-1) + q += "role.name LIKE ? )" for i := range query.RolePrefixes { params = append(params, query.RolePrefixes[i]+"%") } @@ -53,7 +53,7 @@ func (s *AccessControlStore) GetUserPermissions(ctx context.Context, query acces return result, err } -// SearchUsersPermissions returns the list of user permissions indexed by UserID +// SearchUsersPermissions returns the list of user permissions in specific organization indexed by UserID func (s *AccessControlStore) SearchUsersPermissions(ctx context.Context, orgID int64, options accesscontrol.SearchOptions) (map[int64][]accesscontrol.Permission, error) { type UserRBACPermission struct { UserID int64 `xorm:"user_id"` @@ -61,7 +61,13 @@ func (s *AccessControlStore) SearchUsersPermissions(ctx context.Context, orgID i Scope string `xorm:"scope"` } dbPerms := make([]UserRBACPermission, 0) + if err := s.sql.WithDbSession(ctx, func(sess *db.Session) error { + roleNameFilterJoin := "" + if len(options.RolePrefixes) > 0 { + roleNameFilterJoin = "INNER JOIN role AS r on up.role_id = r.id" + } + // Find permissions q := ` SELECT @@ -69,21 +75,21 @@ func (s *AccessControlStore) SearchUsersPermissions(ctx context.Context, orgID i action, scope FROM ( - SELECT ur.user_id, ur.org_id, p.action, p.scope + SELECT ur.user_id, ur.org_id, p.action, p.scope, ur.role_id FROM permission AS p INNER JOIN user_role AS ur on ur.role_id = p.role_id UNION ALL - SELECT tm.user_id, tr.org_id, p.action, p.scope + SELECT tm.user_id, tr.org_id, p.action, p.scope, tr.role_id FROM permission AS p INNER JOIN team_role AS tr ON tr.role_id = p.role_id INNER JOIN team_member AS tm ON tm.team_id = tr.team_id UNION ALL - SELECT ou.user_id, ou.org_id, p.action, p.scope + SELECT ou.user_id, ou.org_id, p.action, p.scope, br.role_id FROM permission AS p INNER JOIN builtin_role AS br ON br.role_id = p.role_id INNER JOIN org_user AS ou ON ou.role = br.role UNION ALL - SELECT sa.user_id, br.org_id, p.action, p.scope + SELECT sa.user_id, br.org_id, p.action, p.scope, br.role_id FROM permission AS p INNER JOIN builtin_role AS br ON br.role_id = p.role_id INNER JOIN ( @@ -91,8 +97,8 @@ func (s *AccessControlStore) SearchUsersPermissions(ctx context.Context, orgID i FROM ` + s.sql.GetDialect().Quote("user") + ` AS u WHERE u.is_admin ) AS sa ON 1 = 1 WHERE br.role = ? - ) AS up - WHERE (org_id = ? OR org_id = ?) + ) AS up ` + roleNameFilterJoin + ` + WHERE (up.org_id = ? OR up.org_id = ?) ` params := []any{accesscontrol.RoleGrafanaAdmin, accesscontrol.GlobalOrgID, orgID} @@ -121,9 +127,15 @@ func (s *AccessControlStore) SearchUsersPermissions(ctx context.Context, orgID i q += ` AND user_id = ?` params = append(params, userID) } + if len(options.RolePrefixes) > 0 { + q += " AND ( " + strings.Repeat("r.name LIKE ? OR ", len(options.RolePrefixes)-1) + q += "r.name LIKE ? )" + for _, prefix := range options.RolePrefixes { + params = append(params, prefix+"%") + } + } - return sess.SQL(q, params...). - Find(&dbPerms) + return sess.SQL(q, params...).Find(&dbPerms) }); err != nil { return nil, err } diff --git a/pkg/services/accesscontrol/database/database_test.go b/pkg/services/accesscontrol/database/database_test.go index 4e4f19b236..5cbfae4633 100644 --- a/pkg/services/accesscontrol/database/database_test.go +++ b/pkg/services/accesscontrol/database/database_test.go @@ -629,6 +629,24 @@ func TestIntegrationAccessControlStore_SearchUsersPermissions(t *testing.T) { options: accesscontrol.SearchOptions{Action: "teams:read", Scope: "teams:id:1"}, wantPerm: map[int64][]accesscontrol.Permission{1: {{Action: "teams:read", Scope: "teams:id:1"}}}, }, + { + name: "user assignment by role prefixes", + users: []testUser{{orgRole: org.RoleAdmin, isAdmin: false}}, + permCmds: []rs.SetResourcePermissionsCommand{ + {User: accesscontrol.User{ID: 1, IsExternal: false}, SetResourcePermissionCommand: readTeamPerm("1")}, + }, + options: accesscontrol.SearchOptions{RolePrefixes: []string{accesscontrol.ManagedRolePrefix}}, + wantPerm: map[int64][]accesscontrol.Permission{1: {{Action: "teams:read", Scope: "teams:id:1"}}}, + }, + { + name: "filter out permissions by role prefix", + users: []testUser{{orgRole: org.RoleAdmin, isAdmin: false}}, + permCmds: []rs.SetResourcePermissionsCommand{ + {User: accesscontrol.User{ID: 1, IsExternal: false}, SetResourcePermissionCommand: readTeamPerm("1")}, + }, + options: accesscontrol.SearchOptions{RolePrefixes: []string{accesscontrol.BasicRolePrefix}}, + wantPerm: map[int64][]accesscontrol.Permission{}, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/pkg/services/accesscontrol/mock/mock.go b/pkg/services/accesscontrol/mock/mock.go index ef66d74c30..48fd43fc99 100644 --- a/pkg/services/accesscontrol/mock/mock.go +++ b/pkg/services/accesscontrol/mock/mock.go @@ -21,6 +21,7 @@ type fullAccessControl interface { type Calls struct { Evaluate []interface{} GetUserPermissions []interface{} + GetUserPermissionsInOrg []interface{} ClearUserPermissionCache []interface{} DeclareFixedRoles []interface{} DeclarePluginRoles []interface{} @@ -47,6 +48,7 @@ type Mock struct { // Override functions EvaluateFunc func(context.Context, identity.Requester, accesscontrol.Evaluator) (bool, error) GetUserPermissionsFunc func(context.Context, identity.Requester, accesscontrol.Options) ([]accesscontrol.Permission, error) + GetUserPermissionsInOrgFunc func(context.Context, identity.Requester, int64) ([]accesscontrol.Permission, error) ClearUserPermissionCacheFunc func(identity.Requester) DeclareFixedRolesFunc func(...accesscontrol.RoleRegistration) error DeclarePluginRolesFunc func(context.Context, string, string, []plugins.RoleRegistration) error @@ -140,6 +142,16 @@ func (m *Mock) GetUserPermissions(ctx context.Context, user identity.Requester, return m.permissions, nil } +func (m *Mock) GetUserPermissionsInOrg(ctx context.Context, user identity.Requester, orgID int64) ([]accesscontrol.Permission, error) { + m.Calls.GetUserPermissionsInOrg = append(m.Calls.GetUserPermissionsInOrg, []interface{}{ctx, user, orgID}) + // Use override if provided + if m.GetUserPermissionsInOrgFunc != nil { + return m.GetUserPermissionsInOrgFunc(ctx, user, orgID) + } + // Otherwise return the Permissions list + return m.permissions, nil +} + func (m *Mock) ClearUserPermissionCache(user identity.Requester) { m.Calls.ClearUserPermissionCache = append(m.Calls.ClearUserPermissionCache, []interface{}{user}) // Use override if provided From ad28e3cc77a99da82c17ad648af83e51b18aff84 Mon Sep 17 00:00:00 2001 From: kay delaney <45561153+kaydelaney@users.noreply.github.com> Date: Mon, 4 Mar 2024 13:01:17 +0000 Subject: [PATCH 0362/1406] Scenes: Fix panel repeats (#83707) --- .../app/features/dashboard-scene/panel-edit/PanelEditor.tsx | 6 +++--- .../features/dashboard-scene/panel-edit/VizPanelManager.tsx | 4 ++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx b/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx index c44cace8a1..ad1494a38e 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx @@ -138,7 +138,7 @@ export class PanelEditor extends SceneObjectBase { width: repeatDirection === 'h' ? 24 : gridItem.state.width, height: gridItem.state.height, itemHeight: gridItem.state.height, - source: panelManager.state.panel.clone(), + source: panelManager.getPanelCloneWithData(), variableName: panelManager.state.repeat!, repeatedPanels: [], repeatDirection: panelManager.state.repeatDirection, @@ -157,7 +157,7 @@ export class PanelEditor extends SceneObjectBase { } const panelManager = this.state.vizManager; - const panelClone = panelManager.state.panel.clone(); + const panelClone = panelManager.getPanelCloneWithData(); const gridItem = new SceneGridItem({ key: panelRepeater.state.key, x: panelRepeater.state.x, @@ -189,7 +189,7 @@ export class PanelEditor extends SceneObjectBase { } panelRepeater.setState({ - source: panelManager.state.panel.clone(), + source: panelManager.getPanelCloneWithData(), repeatDirection: panelManager.state.repeatDirection, variableName: panelManager.state.repeat, maxPerRow: panelManager.state.maxPerRow, diff --git a/public/app/features/dashboard-scene/panel-edit/VizPanelManager.tsx b/public/app/features/dashboard-scene/panel-edit/VizPanelManager.tsx index 1570468ea2..f2433db7f9 100644 --- a/public/app/features/dashboard-scene/panel-edit/VizPanelManager.tsx +++ b/public/app/features/dashboard-scene/panel-edit/VizPanelManager.tsx @@ -367,6 +367,10 @@ export class VizPanelManager extends SceneObjectBase { return { error: 'Unsupported panel parent' }; } + public getPanelCloneWithData(): VizPanel { + return this.state.panel.clone({ $data: this.state.$data?.clone() }); + } + public static Component = ({ model }: SceneComponentProps) => { const { panel, tableView } = model.useState(); const styles = useStyles2(getStyles); From 519f965c8ec5822f3121f2ece3c29c7fb0595759 Mon Sep 17 00:00:00 2001 From: kay delaney <45561153+kaydelaney@users.noreply.github.com> Date: Mon, 4 Mar 2024 13:10:04 +0000 Subject: [PATCH 0363/1406] Scenes/LibraryPanels: Fixes issue where repeat panel would disappear if dashboard didn't have variable used by lib panel (#83780) --- .../dashboard-scene/scene/LibraryVizPanel.tsx | 11 ++++++----- .../scene/PanelRepeaterGridItem.test.tsx | 11 ++++++++++- .../scene/PanelRepeaterGridItem.tsx | 15 ++++++++++----- .../app/features/dashboard-scene/utils/utils.ts | 3 ++- 4 files changed, 28 insertions(+), 12 deletions(-) diff --git a/public/app/features/dashboard-scene/scene/LibraryVizPanel.tsx b/public/app/features/dashboard-scene/scene/LibraryVizPanel.tsx index 379a5ba1b4..b5ab559f34 100644 --- a/public/app/features/dashboard-scene/scene/LibraryVizPanel.tsx +++ b/public/app/features/dashboard-scene/scene/LibraryVizPanel.tsx @@ -84,15 +84,16 @@ export class LibraryVizPanel extends SceneObjectBase { const panel = new VizPanel(vizPanelState); const gridItem = this.parent; + if (libPanelModel.repeat && gridItem instanceof SceneGridItem && gridItem.parent instanceof SceneGridLayout) { this._parent = undefined; const repeater = new PanelRepeaterGridItem({ key: gridItem.state.key, - x: libPanelModel.gridPos.x, - y: libPanelModel.gridPos.y, - width: libPanelModel.repeatDirection === 'h' ? 24 : libPanelModel.gridPos.w, - height: libPanelModel.gridPos.h, - itemHeight: libPanelModel.gridPos.h, + x: gridItem.state.x, + y: gridItem.state.y, + width: libPanelModel.repeatDirection === 'h' ? 24 : gridItem.state.width, + height: gridItem.state.height, + itemHeight: gridItem.state.height, source: this, variableName: libPanelModel.repeat, repeatedPanels: [], diff --git a/public/app/features/dashboard-scene/scene/PanelRepeaterGridItem.test.tsx b/public/app/features/dashboard-scene/scene/PanelRepeaterGridItem.test.tsx index ae74628a71..ea1a364790 100644 --- a/public/app/features/dashboard-scene/scene/PanelRepeaterGridItem.test.tsx +++ b/public/app/features/dashboard-scene/scene/PanelRepeaterGridItem.test.tsx @@ -98,7 +98,7 @@ describe('PanelRepeaterGridItem', () => { expect(repeater.state.itemHeight).toBe(5); }); - it('When updating variable should update repeats', async () => { + it('Should update repeats when updating variable', async () => { const { scene, repeater, variable } = buildPanelRepeaterScene({ variableQueryTime: 0 }); activateFullSceneTree(scene); @@ -107,4 +107,13 @@ describe('PanelRepeaterGridItem', () => { expect(repeater.state.repeatedPanels?.length).toBe(2); }); + + it('Should fall back to default variable if specified variable cannot be found', () => { + const { scene, repeater } = buildPanelRepeaterScene({ variableQueryTime: 0 }); + scene.setState({ $variables: undefined }); + activateFullSceneTree(scene); + expect(repeater.state.repeatedPanels?.[0].state.$variables?.state.variables[0].state.name).toBe( + '_____default_sys_repeat_var_____' + ); + }); }); diff --git a/public/app/features/dashboard-scene/scene/PanelRepeaterGridItem.tsx b/public/app/features/dashboard-scene/scene/PanelRepeaterGridItem.tsx index 631ecda727..48745d21ad 100644 --- a/public/app/features/dashboard-scene/scene/PanelRepeaterGridItem.tsx +++ b/public/app/features/dashboard-scene/scene/PanelRepeaterGridItem.tsx @@ -14,6 +14,7 @@ import { sceneGraph, MultiValueVariable, LocalValueVariable, + CustomVariable, } from '@grafana/scenes'; import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN } from 'app/core/constants'; @@ -84,11 +85,15 @@ export class PanelRepeaterGridItem extends SceneObjectBase Date: Mon, 4 Mar 2024 14:31:14 +0100 Subject: [PATCH 0364/1406] DashboardSceneChangeTracker: Do not load the worker until is editing (#83817) --- .../saving/DashboardSceneChangeTracker.ts | 14 ++++++++++---- .../scene/DashboardScene.test.tsx | 17 +++++++++++++++++ 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/public/app/features/dashboard-scene/saving/DashboardSceneChangeTracker.ts b/public/app/features/dashboard-scene/saving/DashboardSceneChangeTracker.ts index a107817007..50879fe173 100644 --- a/public/app/features/dashboard-scene/saving/DashboardSceneChangeTracker.ts +++ b/public/app/features/dashboard-scene/saving/DashboardSceneChangeTracker.ts @@ -22,12 +22,11 @@ import { DashboardChangeInfo } from './shared'; export class DashboardSceneChangeTracker { private _changeTrackerSub: Unsubscribable | undefined; - private _changesWorker: Worker; + private _changesWorker?: Worker; private _dashboard: DashboardScene; constructor(dashboard: DashboardScene) { this._dashboard = dashboard; - this._changesWorker = createWorker(); } private onStateChanged({ payload }: SceneObjectStateChangedEvent) { @@ -95,8 +94,15 @@ export class DashboardSceneChangeTracker { } } + private init() { + this._changesWorker = createWorker(); + } + public startTrackingChanges() { - this._changesWorker.onmessage = (e: MessageEvent) => { + if (!this._changesWorker) { + this.init(); + } + this._changesWorker!.onmessage = (e: MessageEvent) => { this.updateIsDirty(e.data); }; @@ -112,6 +118,6 @@ export class DashboardSceneChangeTracker { public terminate() { this.stopTrackingChanges(); - this._changesWorker.terminate(); + this._changesWorker?.terminate(); } } diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx index 55e7062d1a..51fbbbc874 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx @@ -66,6 +66,23 @@ describe('DashboardScene', () => { }); describe('Editing and discarding', () => { + describe('Given scene in view mode', () => { + it('Should set isEditing to false', () => { + const scene = buildTestScene(); + scene.activate(); + + expect(scene.state.isEditing).toBeFalsy(); + }); + + it('Should not start the detect changes worker', () => { + const scene = buildTestScene(); + scene.activate(); + + // @ts-expect-error it is a private property + expect(scene._changesWorker).toBeUndefined(); + }); + }); + describe('Given scene in edit mode', () => { let scene: DashboardScene; let deactivateScene: () => void; From b63866baafaade1ee3e7dff3a15ff590dc8d750e Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Mon, 4 Mar 2024 15:26:46 +0100 Subject: [PATCH 0365/1406] Loki: Interpolate variables in live queries (#83831) * Loki: Interpolate variables in live queries * Update to not have to rempve private * Update comment --- .../app/plugins/datasource/loki/datasource.test.ts | 14 ++++++++++++++ public/app/plugins/datasource/loki/datasource.ts | 11 +++++------ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/public/app/plugins/datasource/loki/datasource.test.ts b/public/app/plugins/datasource/loki/datasource.test.ts index ee3b164db4..ea008bd2c6 100644 --- a/public/app/plugins/datasource/loki/datasource.test.ts +++ b/public/app/plugins/datasource/loki/datasource.test.ts @@ -18,6 +18,7 @@ import { TimeRange, ToggleFilterAction, DataQueryRequest, + ScopedVars, } from '@grafana/data'; import { BackendSrv, @@ -1652,6 +1653,19 @@ describe('LokiDatasource', () => { }).rejects.toThrow('invalid metadata request url: /index'); }); }); + + describe('live tailing', () => { + it('interpolates variables with scopedVars and filters', () => { + const ds = createLokiDatasource(); + const query: LokiQuery = { expr: '{app=$app}', refId: 'A' }; + const scopedVars: ScopedVars = { app: { text: 'interpolated', value: 'interpolated' } }; + const filters: AdHocFilter[] = []; + + jest.spyOn(ds, 'applyTemplateVariables').mockImplementation((query) => query); + ds.query({ targets: [query], scopedVars, filters, liveStreaming: true } as DataQueryRequest); + expect(ds.applyTemplateVariables).toHaveBeenCalledWith(expect.objectContaining(query), scopedVars, filters); + }); + }); }); describe('applyTemplateVariables', () => { diff --git a/public/app/plugins/datasource/loki/datasource.ts b/public/app/plugins/datasource/loki/datasource.ts index 8277285cd5..90380a9a88 100644 --- a/public/app/plugins/datasource/loki/datasource.ts +++ b/public/app/plugins/datasource/loki/datasource.ts @@ -300,7 +300,7 @@ export class LokiDatasource return merge( ...streamQueries.map((q) => doLokiChannelStream( - this.applyTemplateVariables(q, request.scopedVars), + this.applyTemplateVariables(q, request.scopedVars, request.filters), this, // the datasource streamRequest ) @@ -340,15 +340,13 @@ export class LokiDatasource /** * Used within the `query` to execute live queries. - * It is intended for explore-mode and logs-queries, not metric queries. + * It is intended for logs-queries, not metric queries. * @returns An Observable of DataQueryResponse with live query results or an empty response if no suitable queries are found. * @todo: The name says "backend" but it's actually running the query through the frontend. We should fix this. */ private runLiveQueryThroughBackend(request: DataQueryRequest): Observable { - // this only works in explore-mode so variables don't need to be handled, // and only for logs-queries, not metric queries const logsQueries = request.targets.filter((query) => query.expr !== '' && isLogsQuery(query.expr)); - if (logsQueries.length === 0) { return of({ data: [], @@ -357,9 +355,10 @@ export class LokiDatasource } const subQueries = logsQueries.map((query) => { - const maxDataPoints = query.maxLines || this.maxLines; + const interpolatedQuery = this.applyTemplateVariables(query, request.scopedVars, request.filters); + const maxDataPoints = interpolatedQuery.maxLines || this.maxLines; // FIXME: currently we are running it through the frontend still. - return this.runLiveQuery(query, maxDataPoints); + return this.runLiveQuery(interpolatedQuery, maxDataPoints); }); return merge(...subQueries); From 7cf419c09a1415b3055b9ae99d70f415b28810e9 Mon Sep 17 00:00:00 2001 From: Leon Sorokin Date: Mon, 4 Mar 2024 08:47:20 -0600 Subject: [PATCH 0366/1406] Dashboard: Revert LayoutItemContext (#83465) --- .../components/Layout/LayoutItemContext.ts | 21 ------------ packages/grafana-ui/src/components/index.ts | 2 -- .../dashboard/dashgrid/DashboardGrid.tsx | 34 +++---------------- 3 files changed, 5 insertions(+), 52 deletions(-) delete mode 100644 packages/grafana-ui/src/components/Layout/LayoutItemContext.ts diff --git a/packages/grafana-ui/src/components/Layout/LayoutItemContext.ts b/packages/grafana-ui/src/components/Layout/LayoutItemContext.ts deleted file mode 100644 index 08832aab60..0000000000 --- a/packages/grafana-ui/src/components/Layout/LayoutItemContext.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { createContext } from 'react'; - -export interface LayoutItemContextProps { - boostZIndex(): () => void; -} - -/** - * Provides an API for downstream components (e.g. within panels) to inform the layout - * that anchored tooltips or context menus could overflow the panel bounds. The layout - * system can then boost the z-index of items with any anchored contents to prevent the overflown - * content from rendering underneath adjacent layout items (e.g. other panels) that naturally - * render later/higher in the stacking order - * - * This is used by VizTooltips and Annotations, which anchor to data points or time range within - * the viz drawing area - * - * @internal - */ -export const LayoutItemContext = createContext({ - boostZIndex: () => () => {}, -}); diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index ec094bea02..13d8a7b604 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -267,8 +267,6 @@ export { Divider } from './Divider/Divider'; export { getDragStyles, type DragHandlePosition } from './DragHandle/DragHandle'; export { useSplitter } from './Splitter/useSplitter'; -export { LayoutItemContext, type LayoutItemContextProps } from './Layout/LayoutItemContext'; - /** @deprecated Please use non-legacy versions of these components */ const LegacyForms = { SecretFormField, diff --git a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx index 0296986723..6ca7fc8191 100644 --- a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx @@ -1,12 +1,10 @@ import classNames from 'classnames'; -import React, { PureComponent, CSSProperties, useRef, useCallback, useReducer, useMemo } from 'react'; +import React, { PureComponent, CSSProperties } from 'react'; import ReactGridLayout, { ItemCallback } from 'react-grid-layout'; import AutoSizer from 'react-virtualized-auto-sizer'; import { Subscription } from 'rxjs'; -import { zIndex } from '@grafana/data/src/themes/zIndex'; import { config } from '@grafana/runtime'; -import { LayoutItemContext } from '@grafana/ui'; import appEvents from 'app/core/app_events'; import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, GRID_COLUMN_COUNT } from 'app/core/constants'; import { contextSrv } from 'app/core/services/context_srv'; @@ -379,21 +377,6 @@ const GrafanaGridItem = React.forwardRef(( let width = 100; let height = 100; - const boostedCount = useRef(0); - const [_, forceUpdate] = useReducer((x) => x + 1, 0); - - const boostZIndex = useCallback(() => { - boostedCount.current += 1; - forceUpdate(); - - return () => { - boostedCount.current -= 1; - forceUpdate(); - }; - }, [forceUpdate]); - - const ctxValue = useMemo(() => ({ boostZIndex }), [boostZIndex]); - const { gridWidth, gridPos, isViewing, windowHeight, windowWidth, descendingOrderIndex, ...divProps } = props; const style: CSSProperties = props.style ?? {}; @@ -424,17 +407,10 @@ const GrafanaGridItem = React.forwardRef(( // props.children[0] is our main children. RGL adds the drag handle at props.children[1] return ( - -
- {/* Pass width and height to children as render props */} - {[props.children[0](width, height), props.children.slice(1)]} -
-
+
+ {/* Pass width and height to children as render props */} + {[props.children[0](width, height), props.children.slice(1)]} +
); }); From 89575f1df42cb5b9f2a4316201c75ad219488353 Mon Sep 17 00:00:00 2001 From: tonypowa <45235678+tonypowa@users.noreply.github.com> Date: Mon, 4 Mar 2024 16:03:20 +0100 Subject: [PATCH 0367/1406] alerting:clarify silence preview (#83754) * alerting:clarify silence preview * prettier * Update docs/sources/alerting/configure-notifications/create-silence.md Co-authored-by: brendamuir <100768211+brendamuir@users.noreply.github.com> * Lint docs --------- Co-authored-by: Armand Grillet <2117580+armandgrillet@users.noreply.github.com> Co-authored-by: brendamuir <100768211+brendamuir@users.noreply.github.com> --- .../alerting/configure-notifications/create-silence.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/sources/alerting/configure-notifications/create-silence.md b/docs/sources/alerting/configure-notifications/create-silence.md index ab6cfd9287..a977fb4df5 100644 --- a/docs/sources/alerting/configure-notifications/create-silence.md +++ b/docs/sources/alerting/configure-notifications/create-silence.md @@ -27,7 +27,11 @@ weight: 440 Silences stop notifications from getting created and last for only a specified window of time. -**Note that inhibition rules are not supported in the Grafana Alertmanager.** +{{< admonition type="note" >}} + +- Inhibition rules are not supported in the Grafana Alertmanager. +- The preview of silenced alerts only applies to alerts in firing state. + {{< /admonition >}} ## Add silences @@ -39,7 +43,7 @@ To add a silence, complete the following steps. 1. Click **Create silence** to open the Create silence page. 1. In **Silence start and end**, select the start and end date to indicate when the silence should go into effect and expire. 1. Optionally, in **Duration**, specify how long the silence is enforced. This automatically updates the end time in the **Silence start and end** field. -1. In the **Label** and **Value** fields, enter one or more _Matching Labels_. Matchers determine which rules the silence will apply to. +1. In the **Label** and **Value** fields, enter one or more _Matching Labels_. Matchers determine which rules the silence will apply to. Any matching alerts (in firing state) will show in the **Affected alert instances** field 1. In **Comment**, add details about the silence. 1. Click **Submit**. From 57935250fdf5837db60472ef82e1d3ce9b11e9f5 Mon Sep 17 00:00:00 2001 From: Kim Nylander <104772500+knylander-grafana@users.noreply.github.com> Date: Mon, 4 Mar 2024 10:03:33 -0500 Subject: [PATCH 0368/1406] [DOC] Add profile-traces intro material; update Pyroscope data source info (#83739) * Add profile-traces intro material; update Pyroscope data source info * Apply suggestions from code review Co-authored-by: Jack Baldry * Updates and file rename from review * Add PYROSCOPE_VERSION * Apply suggestions from code review * Format tables Signed-off-by: Jack Baldry * Apply suggestions from code review Co-authored-by: Jack Baldry Co-authored-by: Jennifer Villa * Apply suggestions from code review --------- Signed-off-by: Jack Baldry Co-authored-by: Jack Baldry Co-authored-by: Jennifer Villa --- docs/sources/_index.md | 1 + docs/sources/datasources/pyroscope/_index.md | 11 +- .../pyroscope/profiling-and-tracing.md | 16 +++ .../pyroscope-profile-tracing-intro.md | 123 ++++++++++++++++++ .../datasources/tempo-editor-traceql.md | 2 +- .../datasources/tempo-traces-to-profiles.md | 8 +- 6 files changed, 154 insertions(+), 7 deletions(-) create mode 100644 docs/sources/datasources/pyroscope/profiling-and-tracing.md create mode 100644 docs/sources/shared/datasources/pyroscope-profile-tracing-intro.md diff --git a/docs/sources/_index.md b/docs/sources/_index.md index 0ff6e3c743..b6fb2f9a91 100644 --- a/docs/sources/_index.md +++ b/docs/sources/_index.md @@ -15,6 +15,7 @@ labels: - oss cascade: TEMPO_VERSION: latest + PYROSCOPE_VERSION: latest title: Grafana open source documentation --- diff --git a/docs/sources/datasources/pyroscope/_index.md b/docs/sources/datasources/pyroscope/_index.md index 88d5a1c88b..8c7d7bc37c 100644 --- a/docs/sources/datasources/pyroscope/_index.md +++ b/docs/sources/datasources/pyroscope/_index.md @@ -24,9 +24,13 @@ weight: 1150 Grafana Pyroscope is a horizontally scalable, highly available, multi-tenant, OSS, continuous profiling aggregation system. Add it as a data source, and you are ready to query your profiles in [Explore][explore]. -To learn more about profiling and Pyroscope, refer to the [Introduction to Pyroscope](/docs/pyroscope/introduction/). +Refer to [Introduction to Pyroscope](https://grafana.com/docs/pyroscope//introduction/) to understand profiling and Pyroscope. -For information on configuring the Pyroscope data source, refer to [Configure the Grafana Pyroscope data source](./configure-pyroscope-data-source). +To use profiling data, you should: + +- [Configure your application to send profiles](/docs/pyroscope//configure-client/) +- [Configure the Grafana Pyroscope data source](./configure-pyroscope-data-source/). +- [View and query profiling data in Explore](./query-profile-data/) ## Integrate profiles into dashboards @@ -38,12 +42,13 @@ In this case, the screenshot shows memory profiles alongside panels for logs and ## Visualize traces and profiles data using Traces to profiles You can link profile and tracing data using your Pyroscope data source with the Tempo data source. +To learn more about how profiles and tracing can work together, refer to [Profiling and tracing synergies](./profiling-and-tracing/). Combined traces and profiles let you see granular line-level detail when available for a trace span. This allows you pinpoint the exact function that's causing a bottleneck in your application as well as a specific request. ![trace-profiler-view](https://grafana.com/static/img/pyroscope/pyroscope-trace-profiler-view-2023-11-30.png) -For more information, refer to the [Traces to profile section][configure-tempo-data-source] of the Tempo data source documentation. +For more information, refer to the [Traces to profile section][configure-tempo-data-source] and [Link tracing and profiling with span profiles](https://grafana.com/docs/pyroscope//configure-client/trace-span-profiles/). {{< youtube id="AG8VzfFMLxo" >}} diff --git a/docs/sources/datasources/pyroscope/profiling-and-tracing.md b/docs/sources/datasources/pyroscope/profiling-and-tracing.md new file mode 100644 index 0000000000..98315f7c99 --- /dev/null +++ b/docs/sources/datasources/pyroscope/profiling-and-tracing.md @@ -0,0 +1,16 @@ +--- +title: How profiling and tracing work together +menuTitle: How profiling and tracing work together +description: Learn about how profiling and tracing work together. +weight: 250 +keywords: + - pyroscope data source + - continuous profiling + - tracing +--- + +# How profiling and tracing work together + +[//]: # 'Shared content for Trace to profiles in the Pyroscope data source' + +{{< docs/shared source="grafana" lookup="datasources/pyroscope-profile-tracing-intro.md" version="" >}} diff --git a/docs/sources/shared/datasources/pyroscope-profile-tracing-intro.md b/docs/sources/shared/datasources/pyroscope-profile-tracing-intro.md new file mode 100644 index 0000000000..b7c9df002f --- /dev/null +++ b/docs/sources/shared/datasources/pyroscope-profile-tracing-intro.md @@ -0,0 +1,123 @@ +--- +headless: true +labels: + products: + - enterprise + - oss +--- + +[//]: # 'This file documents the introductory material for traces to profiling for the Pyroscope data source.' +[//]: # 'This shared file is included in these locations:' +[//]: # '/grafana/docs/sources/datasources/pyroscope/profiling-and-tracing.md' +[//]: # '/website/docs/grafana-cloud/data-configuration/traces/traces-query-editor.md' +[//]: # '/docs/sources/view-and-analyze-profile-data/profile-tracing/_index.md' +[//]: # +[//]: # 'If you make changes to this file, verify that the meaning and content are not changed in any place where the file is included.' +[//]: # 'Any links should be fully qualified and not relative: /docs/grafana/ instead of ../grafana/.' + + + +Profiles, continuous profiling, and distributed traces are all tools that can be used to improve the performance and reliability of applications. +However, each tool has its own strengths and weaknesses, and it is important to choose the right tool for the job as well as understand when to use both. + +## Profiling + +Profiling offers a deep-dive into an application's performance at the code level, highlighting resource usage and performance hotspots. + + + + + + + + + + + + + + +
UsageDuring development, major releases, or upon noticing performance quirks.
Benefits +
    +
  • Business: Boosts user experience through enhanced application performance.
  • +
  • Technical: Gives clear insights into code performance and areas of refinement.
  • +
+
ExampleA developer uses profiling upon noting slow app performance, identifies a CPU-heavy function, and optimizes it.
+ +## Continuous profiling + +Continuous profiling provides ongoing performance insights, capturing long-term trends and intermittent issues. + + + + + + + + + + + + + + +
UsageMainly in production, especially for high-priority applications.
Benefits +
    +
  • Business: Preemptively addresses inefficiencies, potentially saving costs.
  • +
  • Technical: Highlights performance trends and issues like potential memory leaks over time.
  • +
+
ExampleA month-long data from continuous profiling suggests increasing memory consumption, hinting at a memory leak.
+ +## Distributed tracing + +Traces requests as they cross multiple services, revealing interactions and service dependencies. + + + + + + + + + + + + + + +
UsageEssential for systems like microservices where requests touch multiple services.
Benefits +
    +
  • Business: Faster issue resolution, reduced downtimes, and strengthened customer trust.
  • +
  • Technical: A broad view of the system's structure, revealing bottlenecks and inter-service dependencies.
  • +
+
ExampleIn e-commerce, a user's checkout request might involve various services. Tracing depicts this route, pinpointing where time is most spent.
+ +## Combined power of tracing and profiling + +When used together, tracing and profiling provide a powerful tool for understanding system and application performance. + + + + + + + + + + + + + + +
UsageFor comprehensive system-to-code insights, especially when diagnosing complex issues spread across services and codebases.
Benefits +
    +
  • Business: Reduces downtime, optimizes user experience, and safeguards revenues.
  • +
  • Technical: +
      +
    • Holistic view: Tracing pinpoints bottle-necked services, while profiling delves into the responsible code segments.
    • +
    • End-to-end insight: Visualizes a request's full journey and the performance of individual code parts.
    • +
    • Efficient diagnosis: Tracing identifies service latency; profiling zeroes in on its cause, be it database queries, API calls, or specific code inefficiencies.
    • +
    +
  • +
+
ExampleTracing reveals latency in a payment service. Combined with profiling, it's found that a particular function, making third-party validation calls, is the culprit. This insight guides optimization, refining system efficiency.
diff --git a/docs/sources/shared/datasources/tempo-editor-traceql.md b/docs/sources/shared/datasources/tempo-editor-traceql.md index 1114609240..65e5a992dd 100644 --- a/docs/sources/shared/datasources/tempo-editor-traceql.md +++ b/docs/sources/shared/datasources/tempo-editor-traceql.md @@ -95,7 +95,7 @@ Spans with the same color belong to the same service. The grey text to the right The Tempo data source supports streaming responses to TraceQL queries so you can see partial query results as they come in without waiting for the whole query to finish. {{% admonition type="note" %}} -To use this experimental feature, enable the `traceQLStreaming` feature toggle. If you’re using Grafana Cloud and would like to enable this feature, please contact customer support. +To use this feature in Grafana OSS v10.1 and later, enable the `traceQLStreaming` feature toggle. This capability is enabled by default in Grafana Cloud. {{% /admonition %}} Streaming is available for both the **Search** and **TraceQL** query types, and you'll get immediate visibility of incoming traces on the results table. diff --git a/docs/sources/shared/datasources/tempo-traces-to-profiles.md b/docs/sources/shared/datasources/tempo-traces-to-profiles.md index 07e3c66784..667402b9a9 100644 --- a/docs/sources/shared/datasources/tempo-traces-to-profiles.md +++ b/docs/sources/shared/datasources/tempo-traces-to-profiles.md @@ -29,14 +29,16 @@ There are two ways to configure the trace to profiles feature: - Configure a custom query where you can use a template language to interpolate variables from the trace or span. {{< admonition type="note">}} -Traces to profile requires a Tempo data source with Traces to profiles configured and a Pyroscope data source. This integration supports profile data generated using Go, Ruby, and Java instrumentation SDKs. +Traces to profile requires a Tempo data source with Traces to profiles configured and a Pyroscope data source. This integration supports profile data generated using [Go](/docs/pyroscope//configure-client/trace-span-profiles/go-span-profiles/), [Ruby](/docs/pyroscope//configure-client/trace-span-profiles/ruby-span-profiles/), and [Java](/docs/pyroscope//configure-client/trace-span-profiles/java-span-profiles/) instrumentation SDKs. + +As with traces, your application needs to be instrumented to emit profiling data. For more information, refer to [Linking tracing and profiling with span profiles](/docs/pyroscope//configure-client/trace-span-profiles/). {{< /admonition >}} To use trace to profiles, navigate to **Explore** and query a trace. Each span now links to your queries. Clicking a link runs the query in a split panel. If tags are configured, Grafana dynamically inserts the span attribute values into the query. The query runs over the time range of the (span start time - 60) to (span end time + 60 seconds). ![Selecting a link in the span queries the profile data source](/media/docs/tempo/profiles/tempo-trace-to-profile.png) -To use trace to profiles, you must have a configured Grafana Pyroscope data source. For more information, refer to the [Grafana Pyroscope data source](/docs/grafana/latest/datasources/grafana-pyroscope/) documentation. +To use trace to profiles, you must have a configured Grafana Pyroscope data source. For more information, refer to the [Grafana Pyroscope data source](/docs/grafana//datasources/grafana-pyroscope/) documentation. **Embedded flame graphs** are also inserted into each span details section that has a linked profile (requires a configured Grafana Pyroscope data source). This lets you see resource consumption in a flame graph visualization for each span without having to navigate away from the current view. @@ -88,7 +90,7 @@ To use a custom query with the configuration, follow these steps: 1. Select a Pyroscope data source in the **Data source** drop-down. 1. Optional: Choose any tags to use in the query. If left blank, the default values of `service.name` and `service.namespace` are used. - These tags can be used in the custom query with `${__tags}` variable. This variable interpolates the mapped tags as list in an appropriate syntax for the data source. Only the tags that were present in the span are included; tags that aren't present are omitted. You can also configure a new name for the tag. This is useful in cases where the tag has dots in the name and the target data source doesn't allow using dots in labels. For example, you can remap `service.name` to `service_name`. If you don’t map any tags here, you can still use any tag in the query, for example: `method="${__span.tags.method}"`. You can learn more about custom query variables [here](/docs/grafana/latest/datasources/tempo/configure-tempo-data-source/#custom-query-variables). + These tags can be used in the custom query with `${__tags}` variable. This variable interpolates the mapped tags as list in an appropriate syntax for the data source. Only the tags that were present in the span are included; tags that aren't present are omitted. You can also configure a new name for the tag. This is useful in cases where the tag has dots in the name and the target data source doesn't allow using dots in labels. For example, you can remap `service.name` to `service_name`. If you don’t map any tags here, you can still use any tag in the query, for example: `method="${__span.tags.method}"`. You can learn more about custom query variables [here](/docs/grafana//datasources/tempo/configure-tempo-data-source/#custom-query-variables). 1. Select one or more profile types to use in the query. Select the drop-down and choose options from the menu. 1. Switch on **Use custom query** to enter a custom query. From c644826f50241fd65a0078a4db1983e11a87d596 Mon Sep 17 00:00:00 2001 From: Will Browne Date: Mon, 4 Mar 2024 16:06:57 +0100 Subject: [PATCH 0369/1406] Query: Add query type to json marshal/unmarshal (#83821) add query type to json marshal/unmarshal --- pkg/apis/query/v0alpha1/query.go | 14 ++++++++++++++ pkg/apis/query/v0alpha1/query_test.go | 4 +++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/pkg/apis/query/v0alpha1/query.go b/pkg/apis/query/v0alpha1/query.go index 6c32e8bece..bb11d9ae65 100644 --- a/pkg/apis/query/v0alpha1/query.go +++ b/pkg/apis/query/v0alpha1/query.go @@ -138,6 +138,9 @@ func (g GenericDataQuery) MarshalJSON() ([]byte, error) { if g.MaxDataPoints > 0 { vals["maxDataPoints"] = g.MaxDataPoints } + if g.QueryType != "" { + vals["queryType"] = g.QueryType + } return json.Marshal(vals) } @@ -221,6 +224,17 @@ func (g *GenericDataQuery) unmarshal(vals map[string]any) error { delete(vals, key) } + key = "queryType" + v, ok = vals[key] + if ok { + queryType, ok := v.(string) + if !ok { + return fmt.Errorf("expected queryType as string (got: %t)", v) + } + g.QueryType = queryType + delete(vals, key) + } + g.props = vals return nil } diff --git a/pkg/apis/query/v0alpha1/query_test.go b/pkg/apis/query/v0alpha1/query_test.go index e2ea3ce53c..351593e569 100644 --- a/pkg/apis/query/v0alpha1/query_test.go +++ b/pkg/apis/query/v0alpha1/query_test.go @@ -31,7 +31,8 @@ func TestParseQueriesIntoQueryDataRequest(t *testing.T) { "timeRange": { "from": "100", "to": "200" - } + }, + "queryType": "foo" } ], "from": "1692624667389", @@ -66,6 +67,7 @@ func TestParseQueriesIntoQueryDataRequest(t *testing.T) { "uid": "old" }, "maxDataPoints": 10, + "queryType": "foo", "refId": "Z", "timeRange": { "from": "100", From 9bd84b30e9135e93fd55d667286c090d5c66868e Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Mon, 4 Mar 2024 16:22:13 +0100 Subject: [PATCH 0370/1406] Chore: Remove deprecated components from dashboard import pages (#83747) * Add Form component to core * Chore: Replace deprecated components in DashboardImportPage.tsx * Chore: Replace deprecated components in ImportDashboardForm.tsx * Chore: Replace deprecated components in ImportDashboardOverview.tsx --- .github/CODEOWNERS | 1 + public/app/core/components/Form/Form.tsx | 62 +++++++++++++++++++ .../manage-dashboards/DashboardImportPage.tsx | 17 +++-- .../components/ImportDashboardForm.tsx | 25 +++----- .../components/ImportDashboardOverview.tsx | 7 ++- 5 files changed, 83 insertions(+), 29 deletions(-) create mode 100644 public/app/core/components/Form/Form.tsx diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index b18eeac035..3f107cb0bb 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -383,6 +383,7 @@ playwright.config.ts @grafana/plugins-platform-frontend /public/app/core/components/GraphNG/ @grafana/dataviz-squad /public/app/core/components/TimeSeries/ @grafana/dataviz-squad /public/app/core/components/TimelineChart/ @grafana/dataviz-squad +/public/app/core/components/Form/ @grafana/grafana-frontend-platform /public/app/features/all.ts @grafana/grafana-frontend-platform /public/app/features/admin/ @grafana/identity-access-team diff --git a/public/app/core/components/Form/Form.tsx b/public/app/core/components/Form/Form.tsx new file mode 100644 index 0000000000..91ea061c97 --- /dev/null +++ b/public/app/core/components/Form/Form.tsx @@ -0,0 +1,62 @@ +import { css } from '@emotion/css'; +import React, { HTMLProps, useEffect } from 'react'; +import { + useForm, + Mode, + DefaultValues, + SubmitHandler, + FieldValues, + UseFormReturn, + FieldErrors, + FieldPath, +} from 'react-hook-form'; + +export type FormAPI = Omit, 'handleSubmit'> & { + errors: FieldErrors; +}; + +interface FormProps extends Omit, 'onSubmit' | 'children'> { + validateOn?: Mode; + validateOnMount?: boolean; + validateFieldsOnMount?: FieldPath | Array>; + defaultValues?: DefaultValues; + onSubmit: SubmitHandler; + children: (api: FormAPI) => React.ReactNode; + /** Sets max-width for container. Use it instead of setting individual widths on inputs.*/ + maxWidth?: number | 'none'; +} + +export function Form({ + defaultValues, + onSubmit, + validateOnMount = false, + validateFieldsOnMount, + children, + validateOn = 'onSubmit', + maxWidth = 600, + ...htmlProps +}: FormProps) { + const { handleSubmit, trigger, formState, ...rest } = useForm({ + mode: validateOn, + defaultValues, + }); + + useEffect(() => { + if (validateOnMount) { + trigger(validateFieldsOnMount); + } + }, [trigger, validateFieldsOnMount, validateOnMount]); + + return ( +
+ {children({ errors: formState.errors, formState, trigger, ...rest })} +
+ ); +} diff --git a/public/app/features/manage-dashboards/DashboardImportPage.tsx b/public/app/features/manage-dashboards/DashboardImportPage.tsx index 16266303ca..2a60019e4a 100644 --- a/public/app/features/manage-dashboards/DashboardImportPage.tsx +++ b/public/app/features/manage-dashboards/DashboardImportPage.tsx @@ -8,14 +8,11 @@ import { config, reportInteraction } from '@grafana/runtime'; import { Button, Field, - Form, - HorizontalGroup, Input, Spinner, stylesFactory, TextArea, Themeable2, - VerticalGroup, FileDropzone, withTheme2, DropzoneFile, @@ -23,8 +20,10 @@ import { LinkButton, TextLink, Label, + Stack, } from '@grafana/ui'; import appEvents from 'app/core/app_events'; +import { Form } from 'app/core/components/Form/Form'; import { Page } from 'app/core/components/Page/Page'; import { t, Trans } from 'app/core/internationalization'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; @@ -208,14 +207,14 @@ class UnthemedDashboardImport extends PureComponent { placeholder={JSON_PLACEHOLDER} /> - + Cancel - + )} @@ -236,11 +235,11 @@ class UnthemedDashboardImport extends PureComponent { {loadingState === LoadingState.Loading && ( - - + + - - + + )} {[LoadingState.Error, LoadingState.NotStarted].includes(loadingState) && this.renderImportForm()} {loadingState === LoadingState.Done && } diff --git a/public/app/features/manage-dashboards/components/ImportDashboardForm.tsx b/public/app/features/manage-dashboards/components/ImportDashboardForm.tsx index b49f9c1ca4..8cb50e12a5 100644 --- a/public/app/features/manage-dashboards/components/ImportDashboardForm.tsx +++ b/public/app/features/manage-dashboards/components/ImportDashboardForm.tsx @@ -1,18 +1,9 @@ import React, { useEffect, useState } from 'react'; +import { Controller, FieldErrors, UseFormReturn } from 'react-hook-form'; import { selectors } from '@grafana/e2e-selectors'; import { ExpressionDatasourceRef } from '@grafana/runtime/src/utils/DataSourceWithBackend'; -import { - Button, - Field, - FormAPI, - FormFieldErrors, - FormsOnSubmit, - HorizontalGroup, - Input, - InputControl, - Legend, -} from '@grafana/ui'; +import { Button, Field, FormFieldErrors, FormsOnSubmit, Stack, Input, Legend } from '@grafana/ui'; import { OldFolderPicker } from 'app/core/components/Select/OldFolderPicker'; import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker'; @@ -27,11 +18,11 @@ import { validateTitle, validateUid } from '../utils/validation'; import { ImportDashboardLibraryPanelsList } from './ImportDashboardLibraryPanelsList'; -interface Props extends Pick, 'register' | 'errors' | 'control' | 'getValues' | 'watch'> { +interface Props extends Pick, 'register' | 'control' | 'getValues' | 'watch'> { uidReset: boolean; inputs: DashboardInputs; initialFolderUid: string; - + errors: FieldErrors; onCancel: () => void; onUidReset: () => void; onSubmit: FormsOnSubmit; @@ -80,7 +71,7 @@ export const ImportDashboardForm = ({ /> - ( )} @@ -123,7 +114,7 @@ export const ImportDashboardForm = ({ invalid={errors.dataSources && !!errors.dataSources[index]} error={errors.dataSources && errors.dataSources[index] && 'A data source is required'} > - ( - + - + ); }; diff --git a/public/app/features/manage-dashboards/components/ImportDashboardOverview.tsx b/public/app/features/manage-dashboards/components/ImportDashboardOverview.tsx index 5b9273cbe4..7ead582344 100644 --- a/public/app/features/manage-dashboards/components/ImportDashboardOverview.tsx +++ b/public/app/features/manage-dashboards/components/ImportDashboardOverview.tsx @@ -3,7 +3,8 @@ import { connect, ConnectedProps } from 'react-redux'; import { dateTimeFormat } from '@grafana/data'; import { locationService, reportInteraction } from '@grafana/runtime'; -import { Form, Legend } from '@grafana/ui'; +import { Box, Legend } from '@grafana/ui'; +import { Form } from 'app/core/components/Form/Form'; import { StoreState } from 'app/types'; import { clearLoadedDashboard, importDashboard } from '../state/actions'; @@ -64,7 +65,7 @@ class ImportDashboardOverviewUnConnected extends PureComponent { return ( <> {source === DashboardSource.Gcom && ( -
+
Importing dashboard from{' '} @@ -90,7 +91,7 @@ class ImportDashboardOverviewUnConnected extends PureComponent { -
+
)}
Date: Mon, 4 Mar 2024 08:22:56 -0800 Subject: [PATCH 0371/1406] Expressions: expose ConvertDataFramesToResults (#83805) --- pkg/expr/converter.go | 361 ++++++++++++++++++ pkg/expr/converter_test.go | 121 ++++++ pkg/expr/dataplane_test.go | 7 +- pkg/expr/ml.go | 2 +- pkg/expr/models.go | 4 +- pkg/expr/nodes.go | 353 +---------------- pkg/expr/nodes_test.go | 108 ------ pkg/expr/reader.go | 48 ++- pkg/expr/service.go | 5 + pkg/expr/service_test.go | 7 +- pkg/expr/sql_command.go | 6 +- pkg/expr/sql_command_test.go | 2 +- pkg/services/ngalert/backtesting/eval_data.go | 4 +- 13 files changed, 545 insertions(+), 483 deletions(-) create mode 100644 pkg/expr/converter.go create mode 100644 pkg/expr/converter_test.go diff --git a/pkg/expr/converter.go b/pkg/expr/converter.go new file mode 100644 index 0000000000..3b080f42e5 --- /dev/null +++ b/pkg/expr/converter.go @@ -0,0 +1,361 @@ +package expr + +import ( + "context" + "fmt" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/data" + + "github.com/grafana/grafana/pkg/expr/mathexp" + "github.com/grafana/grafana/pkg/infra/tracing" + "github.com/grafana/grafana/pkg/services/datasources" + "github.com/grafana/grafana/pkg/services/featuremgmt" +) + +type ResultConverter struct { + Features featuremgmt.FeatureToggles + Tracer tracing.Tracer +} + +func (c *ResultConverter) Convert(ctx context.Context, + datasourceType string, + frames data.Frames, + allowLongFrames bool, +) (string, mathexp.Results, error) { + if len(frames) == 0 { + return "no-data", mathexp.Results{Values: mathexp.Values{mathexp.NewNoData()}}, nil + } + + var dt data.FrameType + dt, useDataplane, _ := shouldUseDataplane(frames, logger, c.Features.IsEnabled(ctx, featuremgmt.FlagDisableSSEDataplane)) + if useDataplane { + logger.Debug("Handling SSE data source query through dataplane", "datatype", dt) + result, err := handleDataplaneFrames(ctx, c.Tracer, dt, frames) + return fmt.Sprintf("dataplane-%s", dt), result, err + } + + if isAllFrameVectors(datasourceType, frames) { // Prometheus Specific Handling + vals, err := framesToNumbers(frames) + if err != nil { + return "", mathexp.Results{}, fmt.Errorf("failed to read frames as numbers: %w", err) + } + return "vector", mathexp.Results{Values: vals}, nil + } + + if len(frames) == 1 { + frame := frames[0] + // Handle Untyped NoData + if len(frame.Fields) == 0 { + return "no-data", mathexp.Results{Values: mathexp.Values{mathexp.NoData{Frame: frame}}}, nil + } + + // Handle Numeric Table + if frame.TimeSeriesSchema().Type == data.TimeSeriesTypeNot && isNumberTable(frame) { + numberSet, err := extractNumberSet(frame) + if err != nil { + return "", mathexp.Results{}, err + } + vals := make([]mathexp.Value, 0, len(numberSet)) + for _, n := range numberSet { + vals = append(vals, n) + } + return "number set", mathexp.Results{ + Values: vals, + }, nil + } + } + + filtered := make([]*data.Frame, 0, len(frames)) + totalLen := 0 + for _, frame := range frames { + schema := frame.TimeSeriesSchema() + // Check for TimeSeriesTypeNot in InfluxDB queries. A data frame of this type will cause + // the WideToMany() function to error out, which results in unhealthy alerts. + // This check should be removed once inconsistencies in data source responses are solved. + if schema.Type == data.TimeSeriesTypeNot && datasourceType == datasources.DS_INFLUXDB { + logger.Warn("Ignoring InfluxDB data frame due to missing numeric fields") + continue + } + + if schema.Type != data.TimeSeriesTypeWide && !allowLongFrames { + return "", mathexp.Results{}, fmt.Errorf("input data must be a wide series but got type %s (input refid)", schema.Type) + } + filtered = append(filtered, frame) + totalLen += len(schema.ValueIndices) + } + + if len(filtered) == 0 { + return "no data", mathexp.Results{Values: mathexp.Values{mathexp.NoData{Frame: frames[0]}}}, nil + } + + maybeFixerFn := checkIfSeriesNeedToBeFixed(filtered, datasourceType) + + dataType := "single frame series" + if len(filtered) > 1 { + dataType = "multi frame series" + } + + vals := make([]mathexp.Value, 0, totalLen) + for _, frame := range filtered { + schema := frame.TimeSeriesSchema() + if schema.Type == data.TimeSeriesTypeWide { + series, err := WideToMany(frame, maybeFixerFn) + if err != nil { + return "", mathexp.Results{}, err + } + for _, ser := range series { + vals = append(vals, ser) + } + } else { + v := mathexp.TableData{Frame: frame} + vals = append(vals, v) + dataType = "single frame" + } + } + + return dataType, mathexp.Results{ + Values: vals, + }, nil +} + +func getResponseFrame(resp *backend.QueryDataResponse, refID string) (data.Frames, error) { + response, ok := resp.Responses[refID] + if !ok { + // This indicates that the RefID of the request was not included to the response, i.e. some problem in the data source plugin + keys := make([]string, 0, len(resp.Responses)) + for refID := range resp.Responses { + keys = append(keys, refID) + } + logger.Warn("Can't find response by refID. Return nodata", "responseRefIds", keys) + return nil, nil + } + + if response.Error != nil { + return nil, response.Error + } + return response.Frames, nil +} + +func isAllFrameVectors(datasourceType string, frames data.Frames) bool { + if datasourceType != datasources.DS_PROMETHEUS { + return false + } + allVector := false + for i, frame := range frames { + if frame.Meta != nil && frame.Meta.Custom != nil { + if sMap, ok := frame.Meta.Custom.(map[string]string); ok { + if sMap != nil { + if sMap["resultType"] == "vector" { + if i != 0 && !allVector { + break + } + allVector = true + } + } + } + } + } + return allVector +} + +func framesToNumbers(frames data.Frames) ([]mathexp.Value, error) { + vals := make([]mathexp.Value, 0, len(frames)) + for _, frame := range frames { + if frame == nil { + continue + } + if len(frame.Fields) == 2 && frame.Fields[0].Len() == 1 { + // Can there be zero Len Field results that are being skipped? + valueField := frame.Fields[1] + if valueField.Type().Numeric() { // should be []float64 + val, err := valueField.FloatAt(0) // FloatAt should not err if numeric + if err != nil { + return nil, fmt.Errorf("failed to read value of frame [%v] (RefID %v) of type [%v] as float: %w", frame.Name, frame.RefID, valueField.Type(), err) + } + n := mathexp.NewNumber(frame.Name, valueField.Labels) + n.SetValue(&val) + vals = append(vals, n) + } + } + } + return vals, nil +} + +func isNumberTable(frame *data.Frame) bool { + if frame == nil || frame.Fields == nil { + return false + } + numericCount := 0 + stringCount := 0 + otherCount := 0 + for _, field := range frame.Fields { + fType := field.Type() + switch { + case fType.Numeric(): + numericCount++ + case fType == data.FieldTypeString || fType == data.FieldTypeNullableString: + stringCount++ + default: + otherCount++ + } + } + return numericCount == 1 && otherCount == 0 +} + +func extractNumberSet(frame *data.Frame) ([]mathexp.Number, error) { + numericField := 0 + stringFieldIdxs := []int{} + stringFieldNames := []string{} + for i, field := range frame.Fields { + fType := field.Type() + switch { + case fType.Numeric(): + numericField = i + case fType == data.FieldTypeString || fType == data.FieldTypeNullableString: + stringFieldIdxs = append(stringFieldIdxs, i) + stringFieldNames = append(stringFieldNames, field.Name) + } + } + numbers := make([]mathexp.Number, frame.Rows()) + + for rowIdx := 0; rowIdx < frame.Rows(); rowIdx++ { + val, _ := frame.FloatAt(numericField, rowIdx) + var labels data.Labels + for i := 0; i < len(stringFieldIdxs); i++ { + if i == 0 { + labels = make(data.Labels) + } + key := stringFieldNames[i] // TODO check for duplicate string column names + val, _ := frame.ConcreteAt(stringFieldIdxs[i], rowIdx) + labels[key] = val.(string) // TODO check assertion / return error + } + + n := mathexp.NewNumber(frame.Fields[numericField].Name, labels) + + // The new value fields' configs gets pointed to the one in the original frame + n.Frame.Fields[0].Config = frame.Fields[numericField].Config + n.SetValue(&val) + + numbers[rowIdx] = n + } + return numbers, nil +} + +// WideToMany converts a data package wide type Frame to one or multiple Series. A series +// is created for each value type column of wide frame. +// +// This might not be a good idea long term, but works now as an adapter/shim. +func WideToMany(frame *data.Frame, fixSeries func(series mathexp.Series, valueField *data.Field)) ([]mathexp.Series, error) { + tsSchema := frame.TimeSeriesSchema() + if tsSchema.Type != data.TimeSeriesTypeWide { + return nil, fmt.Errorf("input data must be a wide series but got type %s", tsSchema.Type) + } + + if len(tsSchema.ValueIndices) == 1 { + s, err := mathexp.SeriesFromFrame(frame) + if err != nil { + return nil, err + } + if fixSeries != nil { + fixSeries(s, frame.Fields[tsSchema.ValueIndices[0]]) + } + return []mathexp.Series{s}, nil + } + + series := make([]mathexp.Series, 0, len(tsSchema.ValueIndices)) + for _, valIdx := range tsSchema.ValueIndices { + l := frame.Rows() + f := data.NewFrameOfFieldTypes(frame.Name, l, frame.Fields[tsSchema.TimeIndex].Type(), frame.Fields[valIdx].Type()) + f.Fields[0].Name = frame.Fields[tsSchema.TimeIndex].Name + f.Fields[1].Name = frame.Fields[valIdx].Name + + // The new value fields' configs gets pointed to the one in the original frame + f.Fields[1].Config = frame.Fields[valIdx].Config + + if frame.Fields[valIdx].Labels != nil { + f.Fields[1].Labels = frame.Fields[valIdx].Labels.Copy() + } + for i := 0; i < l; i++ { + f.SetRow(i, frame.Fields[tsSchema.TimeIndex].CopyAt(i), frame.Fields[valIdx].CopyAt(i)) + } + s, err := mathexp.SeriesFromFrame(f) + if err != nil { + return nil, err + } + if fixSeries != nil { + fixSeries(s, frame.Fields[valIdx]) + } + series = append(series, s) + } + + return series, nil +} + +// checkIfSeriesNeedToBeFixed scans all value fields of all provided frames and determines whether the resulting mathexp.Series +// needs to be updated so each series could be identifiable by labels. +// NOTE: applicable only to only datasources.DS_GRAPHITE and datasources.DS_TESTDATA data sources +// returns a function that patches the mathexp.Series with information from data.Field from which it was created if the all series need to be fixed. Otherwise, returns nil +func checkIfSeriesNeedToBeFixed(frames []*data.Frame, datasourceType string) func(series mathexp.Series, valueField *data.Field) { + if !(datasourceType == datasources.DS_GRAPHITE || datasourceType == datasources.DS_TESTDATA) { + return nil + } + + // get all value fields + var valueFields []*data.Field + for _, frame := range frames { + tsSchema := frame.TimeSeriesSchema() + for _, index := range tsSchema.ValueIndices { + field := frame.Fields[index] + // if at least one value field contains labels, the result does not need to be fixed. + if len(field.Labels) > 0 { + return nil + } + if valueFields == nil { + valueFields = make([]*data.Field, 0, len(frames)*len(tsSchema.ValueIndices)) + } + valueFields = append(valueFields, field) + } + } + + // selectors are in precedence order. + nameSelectors := []func(f *data.Field) string{ + func(f *data.Field) string { + if f == nil || f.Config == nil { + return "" + } + return f.Config.DisplayNameFromDS + }, + func(f *data.Field) string { + if f == nil || f.Config == nil { + return "" + } + return f.Config.DisplayName + }, + func(f *data.Field) string { + return f.Name + }, + } + + // now look for the first selector that would make all value fields be unique + for _, selector := range nameSelectors { + names := make(map[string]struct{}, len(valueFields)) + good := true + for _, field := range valueFields { + name := selector(field) + if _, ok := names[name]; ok || name == "" { + good = false + break + } + names[name] = struct{}{} + } + if good { + return func(series mathexp.Series, valueField *data.Field) { + series.SetLabels(data.Labels{ + nameLabelName: selector(valueField), + }) + } + } + } + return nil +} diff --git a/pkg/expr/converter_test.go b/pkg/expr/converter_test.go new file mode 100644 index 0000000000..2a484ea967 --- /dev/null +++ b/pkg/expr/converter_test.go @@ -0,0 +1,121 @@ +package expr + +import ( + "context" + "testing" + "time" + + "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/expr/mathexp" + "github.com/grafana/grafana/pkg/infra/tracing" + "github.com/grafana/grafana/pkg/services/datasources" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/setting" +) + +func TestConvertDataFramesToResults(t *testing.T) { + s := &Service{ + cfg: setting.NewCfg(), + features: &featuremgmt.FeatureManager{}, + tracer: tracing.InitializeTracerForTest(), + metrics: newMetrics(nil), + } + converter := &ResultConverter{Features: s.features, Tracer: s.tracer} + + t.Run("should add name label if no labels and specific data source", func(t *testing.T) { + supported := []string{datasources.DS_GRAPHITE, datasources.DS_TESTDATA} + t.Run("when only field name is specified", func(t *testing.T) { + t.Run("use value field names if one frame - many series", func(t *testing.T) { + supported := []string{datasources.DS_GRAPHITE, datasources.DS_TESTDATA} + + frames := []*data.Frame{ + data.NewFrame("test", + data.NewField("time", nil, []time.Time{time.Unix(1, 0)}), + data.NewField("test-value1", nil, []*float64{fp(2)}), + data.NewField("test-value2", nil, []*float64{fp(2)})), + } + + for _, dtype := range supported { + t.Run(dtype, func(t *testing.T) { + resultType, res, err := converter.Convert(context.Background(), dtype, frames, s.allowLongFrames) + require.NoError(t, err) + assert.Equal(t, "single frame series", resultType) + require.Len(t, res.Values, 2) + + var names []string + for _, value := range res.Values { + require.IsType(t, mathexp.Series{}, value) + lbls := value.GetLabels() + require.Contains(t, lbls, nameLabelName) + names = append(names, lbls[nameLabelName]) + } + require.EqualValues(t, []string{"test-value1", "test-value2"}, names) + }) + } + }) + t.Run("should use frame name if one frame - one series", func(t *testing.T) { + frames := []*data.Frame{ + data.NewFrame("test-frame1", + data.NewField("time", nil, []time.Time{time.Unix(1, 0)}), + data.NewField("test-value1", nil, []*float64{fp(2)})), + data.NewFrame("test-frame2", + data.NewField("time", nil, []time.Time{time.Unix(1, 0)}), + data.NewField("test-value2", nil, []*float64{fp(2)})), + } + + for _, dtype := range supported { + t.Run(dtype, func(t *testing.T) { + resultType, res, err := converter.Convert(context.Background(), dtype, frames, s.allowLongFrames) + require.NoError(t, err) + assert.Equal(t, "multi frame series", resultType) + require.Len(t, res.Values, 2) + + var names []string + for _, value := range res.Values { + require.IsType(t, mathexp.Series{}, value) + lbls := value.GetLabels() + require.Contains(t, lbls, nameLabelName) + names = append(names, lbls[nameLabelName]) + } + require.EqualValues(t, []string{"test-frame1", "test-frame2"}, names) + }) + } + }) + }) + t.Run("should use fields DisplayNameFromDS when it is unique", func(t *testing.T) { + f1 := data.NewField("test-value1", nil, []*float64{fp(2)}) + f1.Config = &data.FieldConfig{DisplayNameFromDS: "test-value1"} + f2 := data.NewField("test-value2", nil, []*float64{fp(2)}) + f2.Config = &data.FieldConfig{DisplayNameFromDS: "test-value2"} + frames := []*data.Frame{ + data.NewFrame("test-frame1", + data.NewField("time", nil, []time.Time{time.Unix(1, 0)}), + f1), + data.NewFrame("test-frame2", + data.NewField("time", nil, []time.Time{time.Unix(1, 0)}), + f2), + } + + for _, dtype := range supported { + t.Run(dtype, func(t *testing.T) { + resultType, res, err := converter.Convert(context.Background(), dtype, frames, s.allowLongFrames) + require.NoError(t, err) + assert.Equal(t, "multi frame series", resultType) + require.Len(t, res.Values, 2) + + var names []string + for _, value := range res.Values { + require.IsType(t, mathexp.Series{}, value) + lbls := value.GetLabels() + require.Contains(t, lbls, nameLabelName) + names = append(names, lbls[nameLabelName]) + } + require.EqualValues(t, []string{"test-value1", "test-value2"}, names) + }) + } + }) + }) +} diff --git a/pkg/expr/dataplane_test.go b/pkg/expr/dataplane_test.go index 186f0add4e..674d4432f0 100644 --- a/pkg/expr/dataplane_test.go +++ b/pkg/expr/dataplane_test.go @@ -50,12 +50,13 @@ func framesPassThroughService(t *testing.T, frames data.Frames) (data.Frames, er map[string]backend.DataResponse{"A": {Frames: frames}}, } + features := featuremgmt.WithFeatures() cfg := setting.NewCfg() s := Service{ cfg: cfg, dataService: me, - features: &featuremgmt.FeatureManager{}, + features: features, pCtxProvider: plugincontext.ProvideService(cfg, nil, &pluginstore.FakePluginStore{ PluginList: []pluginstore.Plugin{ {JSONData: plugins.JSONData{ID: "test"}}, @@ -64,6 +65,10 @@ func framesPassThroughService(t *testing.T, frames data.Frames) (data.Frames, er nil, pluginconfig.NewFakePluginRequestConfigProvider()), tracer: tracing.InitializeTracerForTest(), metrics: newMetrics(nil), + converter: &ResultConverter{ + Features: features, + Tracer: tracing.InitializeTracerForTest(), + }, } queries := []Query{{ RefID: "A", diff --git a/pkg/expr/ml.go b/pkg/expr/ml.go index 185b265474..affdad77a9 100644 --- a/pkg/expr/ml.go +++ b/pkg/expr/ml.go @@ -130,7 +130,7 @@ func (m *MLNode) Execute(ctx context.Context, now time.Time, _ mathexp.Vars, s * } // process the response the same way DSNode does. Use plugin ID as data source type. Semantically, they are the same. - responseType, result, err = convertDataFramesToResults(ctx, dataFrames, mlPluginID, s, logger) + responseType, result, err = s.converter.Convert(ctx, mlPluginID, dataFrames, s.allowLongFrames) return result, err } diff --git a/pkg/expr/models.go b/pkg/expr/models.go index 6e7f0919ad..6fffd7d116 100644 --- a/pkg/expr/models.go +++ b/pkg/expr/models.go @@ -50,8 +50,8 @@ type ResampleQuery struct { // The math expression Expression string `json:"expression" jsonschema:"minLength=1,example=$A + 1,example=$A"` - // The time durration - Window string `json:"window" jsonschema:"minLength=1,example=1w,example=10m"` + // The time duration + Window string `json:"window" jsonschema:"minLength=1,example=1d,example=10m"` // The downsample function Downsampler mathexp.ReducerID `json:"downsampler"` diff --git a/pkg/expr/nodes.go b/pkg/expr/nodes.go index 8ecf303cc4..f5defd3b61 100644 --- a/pkg/expr/nodes.go +++ b/pkg/expr/nodes.go @@ -9,9 +9,7 @@ import ( "time" "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana-plugin-sdk-go/data" - jsonitersdk "github.com/grafana/grafana-plugin-sdk-go/data/utils/jsoniter" - jsoniter "github.com/json-iterator/go" + "github.com/grafana/grafana-plugin-sdk-go/data/utils/jsoniter" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" "gonum.org/v1/gonum/graph/simple" @@ -130,13 +128,12 @@ func buildCMDNode(rn *rawNode, toggles featuremgmt.FeatureToggles) (*CMDNode, er // NOTE: this structure of this is weird now, because it is targeting a structure // where this is actually run in the root loop, however we want to verify the individual // node parsing before changing the full tree parser - reader, err := NewExpressionQueryReader(toggles) + reader := NewExpressionQueryReader(toggles) + iter, err := jsoniter.ParseBytes(jsoniter.ConfigDefault, rn.QueryRaw) if err != nil { return nil, err } - - iter := jsoniter.ParseBytes(jsoniter.ConfigDefault, rn.QueryRaw) - q, err := reader.ReadQuery(rn, jsonitersdk.NewIterator(iter)) + q, err := reader.ReadQuery(rn, iter) if err != nil { return nil, err } @@ -325,7 +322,7 @@ func executeDSNodesGrouped(ctx context.Context, now time.Time, vars mathexp.Vars } var result mathexp.Results - responseType, result, err := convertDataFramesToResults(ctx, dataFrames, dn.datasource.Type, s, logger) + responseType, result, err := s.converter.Convert(ctx, dn.datasource.Type, dataFrames, s.allowLongFrames) if err != nil { result.Error = makeConversionError(dn.RefID(), err) } @@ -393,347 +390,9 @@ func (dn *DSNode) Execute(ctx context.Context, now time.Time, _ mathexp.Vars, s } var result mathexp.Results - responseType, result, err = convertDataFramesToResults(ctx, dataFrames, dn.datasource.Type, s, logger) + responseType, result, err = s.converter.Convert(ctx, dn.datasource.Type, dataFrames, s.allowLongFrames) if err != nil { err = makeConversionError(dn.refID, err) } return result, err } - -func getResponseFrame(resp *backend.QueryDataResponse, refID string) (data.Frames, error) { - response, ok := resp.Responses[refID] - if !ok { - // This indicates that the RefID of the request was not included to the response, i.e. some problem in the data source plugin - keys := make([]string, 0, len(resp.Responses)) - for refID := range resp.Responses { - keys = append(keys, refID) - } - logger.Warn("Can't find response by refID. Return nodata", "responseRefIds", keys) - return nil, nil - } - - if response.Error != nil { - return nil, response.Error - } - return response.Frames, nil -} - -func convertDataFramesToResults(ctx context.Context, frames data.Frames, datasourceType string, s *Service, logger log.Logger) (string, mathexp.Results, error) { - if len(frames) == 0 { - return "no-data", mathexp.Results{Values: mathexp.Values{mathexp.NewNoData()}}, nil - } - - var dt data.FrameType - dt, useDataplane, _ := shouldUseDataplane(frames, logger, s.features.IsEnabled(ctx, featuremgmt.FlagDisableSSEDataplane)) - if useDataplane { - logger.Debug("Handling SSE data source query through dataplane", "datatype", dt) - result, err := handleDataplaneFrames(ctx, s.tracer, dt, frames) - return fmt.Sprintf("dataplane-%s", dt), result, err - } - - if isAllFrameVectors(datasourceType, frames) { // Prometheus Specific Handling - vals, err := framesToNumbers(frames) - if err != nil { - return "", mathexp.Results{}, fmt.Errorf("failed to read frames as numbers: %w", err) - } - return "vector", mathexp.Results{Values: vals}, nil - } - - if len(frames) == 1 { - frame := frames[0] - // Handle Untyped NoData - if len(frame.Fields) == 0 { - return "no-data", mathexp.Results{Values: mathexp.Values{mathexp.NoData{Frame: frame}}}, nil - } - - // Handle Numeric Table - if frame.TimeSeriesSchema().Type == data.TimeSeriesTypeNot && isNumberTable(frame) { - numberSet, err := extractNumberSet(frame) - if err != nil { - return "", mathexp.Results{}, err - } - vals := make([]mathexp.Value, 0, len(numberSet)) - for _, n := range numberSet { - vals = append(vals, n) - } - return "number set", mathexp.Results{ - Values: vals, - }, nil - } - } - - filtered := make([]*data.Frame, 0, len(frames)) - totalLen := 0 - for _, frame := range frames { - schema := frame.TimeSeriesSchema() - // Check for TimeSeriesTypeNot in InfluxDB queries. A data frame of this type will cause - // the WideToMany() function to error out, which results in unhealthy alerts. - // This check should be removed once inconsistencies in data source responses are solved. - if schema.Type == data.TimeSeriesTypeNot && datasourceType == datasources.DS_INFLUXDB { - logger.Warn("Ignoring InfluxDB data frame due to missing numeric fields") - continue - } - - if schema.Type != data.TimeSeriesTypeWide && !s.allowLongFrames { - return "", mathexp.Results{}, fmt.Errorf("input data must be a wide series but got type %s (input refid)", schema.Type) - } - filtered = append(filtered, frame) - totalLen += len(schema.ValueIndices) - } - - if len(filtered) == 0 { - return "no data", mathexp.Results{Values: mathexp.Values{mathexp.NoData{Frame: frames[0]}}}, nil - } - - maybeFixerFn := checkIfSeriesNeedToBeFixed(filtered, datasourceType) - - dataType := "single frame series" - if len(filtered) > 1 { - dataType = "multi frame series" - } - - vals := make([]mathexp.Value, 0, totalLen) - for _, frame := range filtered { - schema := frame.TimeSeriesSchema() - if schema.Type == data.TimeSeriesTypeWide { - series, err := WideToMany(frame, maybeFixerFn) - if err != nil { - return "", mathexp.Results{}, err - } - for _, ser := range series { - vals = append(vals, ser) - } - } else { - v := mathexp.TableData{Frame: frame} - vals = append(vals, v) - dataType = "single frame" - } - } - - return dataType, mathexp.Results{ - Values: vals, - }, nil -} - -func isAllFrameVectors(datasourceType string, frames data.Frames) bool { - if datasourceType != datasources.DS_PROMETHEUS { - return false - } - allVector := false - for i, frame := range frames { - if frame.Meta != nil && frame.Meta.Custom != nil { - if sMap, ok := frame.Meta.Custom.(map[string]string); ok { - if sMap != nil { - if sMap["resultType"] == "vector" { - if i != 0 && !allVector { - break - } - allVector = true - } - } - } - } - } - return allVector -} - -func framesToNumbers(frames data.Frames) ([]mathexp.Value, error) { - vals := make([]mathexp.Value, 0, len(frames)) - for _, frame := range frames { - if frame == nil { - continue - } - if len(frame.Fields) == 2 && frame.Fields[0].Len() == 1 { - // Can there be zero Len Field results that are being skipped? - valueField := frame.Fields[1] - if valueField.Type().Numeric() { // should be []float64 - val, err := valueField.FloatAt(0) // FloatAt should not err if numeric - if err != nil { - return nil, fmt.Errorf("failed to read value of frame [%v] (RefID %v) of type [%v] as float: %w", frame.Name, frame.RefID, valueField.Type(), err) - } - n := mathexp.NewNumber(frame.Name, valueField.Labels) - n.SetValue(&val) - vals = append(vals, n) - } - } - } - return vals, nil -} - -func isNumberTable(frame *data.Frame) bool { - if frame == nil || frame.Fields == nil { - return false - } - numericCount := 0 - stringCount := 0 - otherCount := 0 - for _, field := range frame.Fields { - fType := field.Type() - switch { - case fType.Numeric(): - numericCount++ - case fType == data.FieldTypeString || fType == data.FieldTypeNullableString: - stringCount++ - default: - otherCount++ - } - } - return numericCount == 1 && otherCount == 0 -} - -func extractNumberSet(frame *data.Frame) ([]mathexp.Number, error) { - numericField := 0 - stringFieldIdxs := []int{} - stringFieldNames := []string{} - for i, field := range frame.Fields { - fType := field.Type() - switch { - case fType.Numeric(): - numericField = i - case fType == data.FieldTypeString || fType == data.FieldTypeNullableString: - stringFieldIdxs = append(stringFieldIdxs, i) - stringFieldNames = append(stringFieldNames, field.Name) - } - } - numbers := make([]mathexp.Number, frame.Rows()) - - for rowIdx := 0; rowIdx < frame.Rows(); rowIdx++ { - val, _ := frame.FloatAt(numericField, rowIdx) - var labels data.Labels - for i := 0; i < len(stringFieldIdxs); i++ { - if i == 0 { - labels = make(data.Labels) - } - key := stringFieldNames[i] // TODO check for duplicate string column names - val, _ := frame.ConcreteAt(stringFieldIdxs[i], rowIdx) - labels[key] = val.(string) // TODO check assertion / return error - } - - n := mathexp.NewNumber(frame.Fields[numericField].Name, labels) - - // The new value fields' configs gets pointed to the one in the original frame - n.Frame.Fields[0].Config = frame.Fields[numericField].Config - n.SetValue(&val) - - numbers[rowIdx] = n - } - return numbers, nil -} - -// WideToMany converts a data package wide type Frame to one or multiple Series. A series -// is created for each value type column of wide frame. -// -// This might not be a good idea long term, but works now as an adapter/shim. -func WideToMany(frame *data.Frame, fixSeries func(series mathexp.Series, valueField *data.Field)) ([]mathexp.Series, error) { - tsSchema := frame.TimeSeriesSchema() - if tsSchema.Type != data.TimeSeriesTypeWide { - return nil, fmt.Errorf("input data must be a wide series but got type %s", tsSchema.Type) - } - - if len(tsSchema.ValueIndices) == 1 { - s, err := mathexp.SeriesFromFrame(frame) - if err != nil { - return nil, err - } - if fixSeries != nil { - fixSeries(s, frame.Fields[tsSchema.ValueIndices[0]]) - } - return []mathexp.Series{s}, nil - } - - series := make([]mathexp.Series, 0, len(tsSchema.ValueIndices)) - for _, valIdx := range tsSchema.ValueIndices { - l := frame.Rows() - f := data.NewFrameOfFieldTypes(frame.Name, l, frame.Fields[tsSchema.TimeIndex].Type(), frame.Fields[valIdx].Type()) - f.Fields[0].Name = frame.Fields[tsSchema.TimeIndex].Name - f.Fields[1].Name = frame.Fields[valIdx].Name - - // The new value fields' configs gets pointed to the one in the original frame - f.Fields[1].Config = frame.Fields[valIdx].Config - - if frame.Fields[valIdx].Labels != nil { - f.Fields[1].Labels = frame.Fields[valIdx].Labels.Copy() - } - for i := 0; i < l; i++ { - f.SetRow(i, frame.Fields[tsSchema.TimeIndex].CopyAt(i), frame.Fields[valIdx].CopyAt(i)) - } - s, err := mathexp.SeriesFromFrame(f) - if err != nil { - return nil, err - } - if fixSeries != nil { - fixSeries(s, frame.Fields[valIdx]) - } - series = append(series, s) - } - - return series, nil -} - -// checkIfSeriesNeedToBeFixed scans all value fields of all provided frames and determines whether the resulting mathexp.Series -// needs to be updated so each series could be identifiable by labels. -// NOTE: applicable only to only datasources.DS_GRAPHITE and datasources.DS_TESTDATA data sources -// returns a function that patches the mathexp.Series with information from data.Field from which it was created if the all series need to be fixed. Otherwise, returns nil -func checkIfSeriesNeedToBeFixed(frames []*data.Frame, datasourceType string) func(series mathexp.Series, valueField *data.Field) { - if !(datasourceType == datasources.DS_GRAPHITE || datasourceType == datasources.DS_TESTDATA) { - return nil - } - - // get all value fields - var valueFields []*data.Field - for _, frame := range frames { - tsSchema := frame.TimeSeriesSchema() - for _, index := range tsSchema.ValueIndices { - field := frame.Fields[index] - // if at least one value field contains labels, the result does not need to be fixed. - if len(field.Labels) > 0 { - return nil - } - if valueFields == nil { - valueFields = make([]*data.Field, 0, len(frames)*len(tsSchema.ValueIndices)) - } - valueFields = append(valueFields, field) - } - } - - // selectors are in precedence order. - nameSelectors := []func(f *data.Field) string{ - func(f *data.Field) string { - if f == nil || f.Config == nil { - return "" - } - return f.Config.DisplayNameFromDS - }, - func(f *data.Field) string { - if f == nil || f.Config == nil { - return "" - } - return f.Config.DisplayName - }, - func(f *data.Field) string { - return f.Name - }, - } - - // now look for the first selector that would make all value fields be unique - for _, selector := range nameSelectors { - names := make(map[string]struct{}, len(valueFields)) - good := true - for _, field := range valueFields { - name := selector(field) - if _, ok := names[name]; ok || name == "" { - good = false - break - } - names[name] = struct{}{} - } - if good { - return func(series mathexp.Series, valueField *data.Field) { - series.SetLabels(data.Labels{ - nameLabelName: selector(valueField), - }) - } - } - } - return nil -} diff --git a/pkg/expr/nodes_test.go b/pkg/expr/nodes_test.go index f889e817c2..ada215a8ad 100644 --- a/pkg/expr/nodes_test.go +++ b/pkg/expr/nodes_test.go @@ -1,7 +1,6 @@ package expr import ( - "context" "errors" "fmt" "testing" @@ -12,11 +11,7 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/expr/mathexp" - "github.com/grafana/grafana/pkg/infra/log/logtest" - "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/datasources" - "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/setting" ) type expectedError struct{} @@ -169,106 +164,3 @@ func TestCheckIfSeriesNeedToBeFixed(t *testing.T) { }) } } - -func TestConvertDataFramesToResults(t *testing.T) { - s := &Service{ - cfg: setting.NewCfg(), - features: &featuremgmt.FeatureManager{}, - tracer: tracing.InitializeTracerForTest(), - metrics: newMetrics(nil), - } - - t.Run("should add name label if no labels and specific data source", func(t *testing.T) { - supported := []string{datasources.DS_GRAPHITE, datasources.DS_TESTDATA} - t.Run("when only field name is specified", func(t *testing.T) { - t.Run("use value field names if one frame - many series", func(t *testing.T) { - supported := []string{datasources.DS_GRAPHITE, datasources.DS_TESTDATA} - - frames := []*data.Frame{ - data.NewFrame("test", - data.NewField("time", nil, []time.Time{time.Unix(1, 0)}), - data.NewField("test-value1", nil, []*float64{fp(2)}), - data.NewField("test-value2", nil, []*float64{fp(2)})), - } - - for _, dtype := range supported { - t.Run(dtype, func(t *testing.T) { - resultType, res, err := convertDataFramesToResults(context.Background(), frames, dtype, s, &logtest.Fake{}) - require.NoError(t, err) - assert.Equal(t, "single frame series", resultType) - require.Len(t, res.Values, 2) - - var names []string - for _, value := range res.Values { - require.IsType(t, mathexp.Series{}, value) - lbls := value.GetLabels() - require.Contains(t, lbls, nameLabelName) - names = append(names, lbls[nameLabelName]) - } - require.EqualValues(t, []string{"test-value1", "test-value2"}, names) - }) - } - }) - t.Run("should use frame name if one frame - one series", func(t *testing.T) { - frames := []*data.Frame{ - data.NewFrame("test-frame1", - data.NewField("time", nil, []time.Time{time.Unix(1, 0)}), - data.NewField("test-value1", nil, []*float64{fp(2)})), - data.NewFrame("test-frame2", - data.NewField("time", nil, []time.Time{time.Unix(1, 0)}), - data.NewField("test-value2", nil, []*float64{fp(2)})), - } - - for _, dtype := range supported { - t.Run(dtype, func(t *testing.T) { - resultType, res, err := convertDataFramesToResults(context.Background(), frames, dtype, s, &logtest.Fake{}) - require.NoError(t, err) - assert.Equal(t, "multi frame series", resultType) - require.Len(t, res.Values, 2) - - var names []string - for _, value := range res.Values { - require.IsType(t, mathexp.Series{}, value) - lbls := value.GetLabels() - require.Contains(t, lbls, nameLabelName) - names = append(names, lbls[nameLabelName]) - } - require.EqualValues(t, []string{"test-frame1", "test-frame2"}, names) - }) - } - }) - }) - t.Run("should use fields DisplayNameFromDS when it is unique", func(t *testing.T) { - f1 := data.NewField("test-value1", nil, []*float64{fp(2)}) - f1.Config = &data.FieldConfig{DisplayNameFromDS: "test-value1"} - f2 := data.NewField("test-value2", nil, []*float64{fp(2)}) - f2.Config = &data.FieldConfig{DisplayNameFromDS: "test-value2"} - frames := []*data.Frame{ - data.NewFrame("test-frame1", - data.NewField("time", nil, []time.Time{time.Unix(1, 0)}), - f1), - data.NewFrame("test-frame2", - data.NewField("time", nil, []time.Time{time.Unix(1, 0)}), - f2), - } - - for _, dtype := range supported { - t.Run(dtype, func(t *testing.T) { - resultType, res, err := convertDataFramesToResults(context.Background(), frames, dtype, s, &logtest.Fake{}) - require.NoError(t, err) - assert.Equal(t, "multi frame series", resultType) - require.Len(t, res.Values, 2) - - var names []string - for _, value := range res.Values { - require.IsType(t, mathexp.Series{}, value) - lbls := value.GetLabels() - require.Contains(t, lbls, nameLabelName) - names = append(names, lbls[nameLabelName]) - } - require.EqualValues(t, []string{"test-value1", "test-value2"}, names) - }) - } - }) - }) -} diff --git a/pkg/expr/reader.go b/pkg/expr/reader.go index f4ca1cde5f..9227b19c13 100644 --- a/pkg/expr/reader.go +++ b/pkg/expr/reader.go @@ -14,38 +14,52 @@ import ( // Once we are comfortable with the parsing logic, this struct will // be merged/replace the existing Query struct in grafana/pkg/expr/transform.go type ExpressionQuery struct { - RefID string - Command Command + GraphID int64 `json:"id,omitempty"` + RefID string `json:"refId"` + QueryType QueryType `json:"queryType"` + + // The typed query parameters + Properties any `json:"properties"` + + // Hidden in debug JSON + Command Command `json:"-"` +} + +// ID is used to identify nodes in the directed graph +func (q ExpressionQuery) ID() int64 { + return q.GraphID } type ExpressionQueryReader struct { features featuremgmt.FeatureToggles } -func NewExpressionQueryReader(features featuremgmt.FeatureToggles) (*ExpressionQueryReader, error) { - h := &ExpressionQueryReader{ +func NewExpressionQueryReader(features featuremgmt.FeatureToggles) *ExpressionQueryReader { + return &ExpressionQueryReader{ features: features, } - return h, nil } -// ReadQuery implements query.TypedQueryHandler. // nolint:gocyclo func (h *ExpressionQueryReader) ReadQuery( // Properties that have been parsed off the same node - common *rawNode, // common query.CommonQueryProperties + common *rawNode, // An iterator with context for the full node (include common values) iter *jsoniter.Iterator, ) (eq ExpressionQuery, err error) { referenceVar := "" eq.RefID = common.RefID - qt := QueryType(common.QueryType) - switch qt { + if common.QueryType == "" { + return eq, fmt.Errorf("missing queryType") + } + eq.QueryType = QueryType(common.QueryType) + switch eq.QueryType { case QueryTypeMath: q := &MathQuery{} err = iter.ReadVal(q) if err == nil { eq.Command, err = NewMathCommand(common.RefID, q.Expression) + eq.Properties = q } case QueryTypeReduce: @@ -54,6 +68,7 @@ func (h *ExpressionQueryReader) ReadQuery( err = iter.ReadVal(q) if err == nil { referenceVar, err = getReferenceVar(q.Expression, common.RefID) + eq.Properties = q } if err == nil && q.Settings != nil { switch q.Settings.Mode { @@ -69,6 +84,7 @@ func (h *ExpressionQueryReader) ReadQuery( } } if err == nil { + eq.Properties = q eq.Command, err = NewReduceCommand(common.RefID, q.Reducer, referenceVar, mapper) } @@ -83,23 +99,21 @@ func (h *ExpressionQueryReader) ReadQuery( referenceVar, err = getReferenceVar(q.Expression, common.RefID) } if err == nil { - // tr := legacydata.NewDataTimeRange(common.TimeRange.From, common.TimeRange.To) - // AbsoluteTimeRange{ - // From: tr.GetFromAsTimeUTC(), - // To: tr.GetToAsTimeUTC(), - // }) + eq.Properties = q eq.Command, err = NewResampleCommand(common.RefID, q.Window, referenceVar, q.Downsampler, q.Upsampler, - common.TimeRange) + common.TimeRange, + ) } case QueryTypeClassic: q := &ClassicQuery{} err = iter.ReadVal(q) if err == nil { + eq.Properties = q eq.Command, err = classic.NewConditionCmd(common.RefID, q.Conditions) } @@ -107,7 +121,8 @@ func (h *ExpressionQueryReader) ReadQuery( q := &SQLExpression{} err = iter.ReadVal(q) if err == nil { - eq.Command, err = NewSQLCommand(common.RefID, q.Expression, common.TimeRange) + eq.Properties = q + eq.Command, err = NewSQLCommand(common.RefID, q.Expression) } case QueryTypeThreshold: @@ -128,6 +143,7 @@ func (h *ExpressionQueryReader) ReadQuery( return eq, fmt.Errorf("invalid condition: %w", err) } eq.Command = threshold + eq.Properties = q if firstCondition.UnloadEvaluator != nil && h.features.IsEnabledGlobally(featuremgmt.FlagRecoveryThreshold) { unloading, err := NewThresholdCommand(common.RefID, referenceVar, firstCondition.UnloadEvaluator.Type, firstCondition.UnloadEvaluator.Params) diff --git a/pkg/expr/service.go b/pkg/expr/service.go index 978ee14721..01c88be979 100644 --- a/pkg/expr/service.go +++ b/pkg/expr/service.go @@ -60,6 +60,7 @@ type Service struct { dataService backend.QueryDataHandler pCtxProvider pluginContextProvider features featuremgmt.FeatureToggles + converter *ResultConverter pluginsClient backend.CallResourceHandler @@ -83,6 +84,10 @@ func ProvideService(cfg *setting.Cfg, pluginClient plugins.Client, pCtxProvider tracer: tracer, metrics: newMetrics(registerer), pluginsClient: pluginClient, + converter: &ResultConverter{ + Features: features, + Tracer: tracer, + }, } } diff --git a/pkg/expr/service_test.go b/pkg/expr/service_test.go index 3492b1121d..2ee7ff5d35 100644 --- a/pkg/expr/service_test.go +++ b/pkg/expr/service_test.go @@ -43,13 +43,18 @@ func TestService(t *testing.T) { }, }, &datafakes.FakeCacheService{}, &datafakes.FakeDataSourceService{}, nil, pluginconfig.NewFakePluginRequestConfigProvider()) + features := featuremgmt.WithFeatures() s := Service{ cfg: setting.NewCfg(), dataService: me, pCtxProvider: pCtxProvider, - features: &featuremgmt.FeatureManager{}, + features: features, tracer: tracing.InitializeTracerForTest(), metrics: newMetrics(nil), + converter: &ResultConverter{ + Features: features, + Tracer: tracing.InitializeTracerForTest(), + }, } queries := []Query{ diff --git a/pkg/expr/sql_command.go b/pkg/expr/sql_command.go index ce69bd550d..44bbe55986 100644 --- a/pkg/expr/sql_command.go +++ b/pkg/expr/sql_command.go @@ -19,12 +19,11 @@ import ( type SQLCommand struct { query string varsToQuery []string - timeRange TimeRange refID string } // NewSQLCommand creates a new SQLCommand. -func NewSQLCommand(refID, rawSQL string, tr TimeRange) (*SQLCommand, error) { +func NewSQLCommand(refID, rawSQL string) (*SQLCommand, error) { if rawSQL == "" { return nil, errutil.BadRequest("sql-missing-query", errutil.WithPublicMessage("missing SQL query")) @@ -39,7 +38,6 @@ func NewSQLCommand(refID, rawSQL string, tr TimeRange) (*SQLCommand, error) { return &SQLCommand{ query: rawSQL, varsToQuery: tables, - timeRange: tr, refID: refID, }, nil } @@ -59,7 +57,7 @@ func UnmarshalSQLCommand(rn *rawNode) (*SQLCommand, error) { return nil, fmt.Errorf("expected sql expression to be type string, but got type %T", expressionRaw) } - return NewSQLCommand(rn.RefID, expression, rn.TimeRange) + return NewSQLCommand(rn.RefID, expression) } // NeedsVars returns the variable names (refIds) that are dependencies diff --git a/pkg/expr/sql_command_test.go b/pkg/expr/sql_command_test.go index 90ba470ae0..7bd9d3c06e 100644 --- a/pkg/expr/sql_command_test.go +++ b/pkg/expr/sql_command_test.go @@ -6,7 +6,7 @@ import ( ) func TestNewCommand(t *testing.T) { - cmd, err := NewSQLCommand("a", "select a from foo, bar", nil) + cmd, err := NewSQLCommand("a", "select a from foo, bar") if err != nil && strings.Contains(err.Error(), "feature is not enabled") { return } diff --git a/pkg/services/ngalert/backtesting/eval_data.go b/pkg/services/ngalert/backtesting/eval_data.go index 12b8b872bd..999c0bc630 100644 --- a/pkg/services/ngalert/backtesting/eval_data.go +++ b/pkg/services/ngalert/backtesting/eval_data.go @@ -32,8 +32,8 @@ func newDataEvaluator(refID string, frame *data.Frame) (*dataEvaluator, error) { return &dataEvaluator{ refID: refID, data: series, - downsampleFunction: "last", - upsampleFunction: "pad", + downsampleFunction: mathexp.ReducerLast, + upsampleFunction: mathexp.UpsamplerPad, }, nil } From fa51724bc638c4c17e8cd2e15e66ad70d0fd6ce3 Mon Sep 17 00:00:00 2001 From: Alexander Weaver Date: Mon, 4 Mar 2024 11:24:49 -0600 Subject: [PATCH 0372/1406] Alerting: Move alertRuleInfo and tests to new files (#83854) Move ruleinfo and tests to new files --- pkg/services/ngalert/schedule/alert_rule.go | 60 +++++ .../ngalert/schedule/alert_rule_test.go | 229 ++++++++++++++++++ pkg/services/ngalert/schedule/registry.go | 54 ----- .../ngalert/schedule/registry_test.go | 219 ----------------- 4 files changed, 289 insertions(+), 273 deletions(-) create mode 100644 pkg/services/ngalert/schedule/alert_rule.go create mode 100644 pkg/services/ngalert/schedule/alert_rule_test.go diff --git a/pkg/services/ngalert/schedule/alert_rule.go b/pkg/services/ngalert/schedule/alert_rule.go new file mode 100644 index 0000000000..9a369cef36 --- /dev/null +++ b/pkg/services/ngalert/schedule/alert_rule.go @@ -0,0 +1,60 @@ +package schedule + +import ( + context "context" + + "github.com/grafana/grafana/pkg/util" +) + +type alertRuleInfo struct { + evalCh chan *evaluation + updateCh chan ruleVersionAndPauseStatus + ctx context.Context + stop func(reason error) +} + +func newAlertRuleInfo(parent context.Context) *alertRuleInfo { + ctx, stop := util.WithCancelCause(parent) + return &alertRuleInfo{evalCh: make(chan *evaluation), updateCh: make(chan ruleVersionAndPauseStatus), ctx: ctx, stop: stop} +} + +// eval signals the rule evaluation routine to perform the evaluation of the rule. Does nothing if the loop is stopped. +// Before sending a message into the channel, it does non-blocking read to make sure that there is no concurrent send operation. +// Returns a tuple where first element is +// - true when message was sent +// - false when the send operation is stopped +// +// the second element contains a dropped message that was sent by a concurrent sender. +func (a *alertRuleInfo) eval(eval *evaluation) (bool, *evaluation) { + // read the channel in unblocking manner to make sure that there is no concurrent send operation. + var droppedMsg *evaluation + select { + case droppedMsg = <-a.evalCh: + default: + } + + select { + case a.evalCh <- eval: + return true, droppedMsg + case <-a.ctx.Done(): + return false, droppedMsg + } +} + +// update sends an instruction to the rule evaluation routine to update the scheduled rule to the specified version. The specified version must be later than the current version, otherwise no update will happen. +func (a *alertRuleInfo) update(lastVersion ruleVersionAndPauseStatus) bool { + // check if the channel is not empty. + select { + case <-a.updateCh: + case <-a.ctx.Done(): + return false + default: + } + + select { + case a.updateCh <- lastVersion: + return true + case <-a.ctx.Done(): + return false + } +} diff --git a/pkg/services/ngalert/schedule/alert_rule_test.go b/pkg/services/ngalert/schedule/alert_rule_test.go new file mode 100644 index 0000000000..a8dfbfb825 --- /dev/null +++ b/pkg/services/ngalert/schedule/alert_rule_test.go @@ -0,0 +1,229 @@ +package schedule + +import ( + context "context" + "math" + "math/rand" + "runtime" + "sync" + "testing" + "time" + + models "github.com/grafana/grafana/pkg/services/ngalert/models" + "github.com/grafana/grafana/pkg/util" + "github.com/stretchr/testify/require" +) + +func TestAlertRuleInfo(t *testing.T) { + type evalResponse struct { + success bool + droppedEval *evaluation + } + + t.Run("when rule evaluation is not stopped", func(t *testing.T) { + t.Run("update should send to updateCh", func(t *testing.T) { + r := newAlertRuleInfo(context.Background()) + resultCh := make(chan bool) + go func() { + resultCh <- r.update(ruleVersionAndPauseStatus{fingerprint(rand.Uint64()), false}) + }() + select { + case <-r.updateCh: + require.True(t, <-resultCh) + case <-time.After(5 * time.Second): + t.Fatal("No message was received on update channel") + } + }) + t.Run("update should drop any concurrent sending to updateCh", func(t *testing.T) { + r := newAlertRuleInfo(context.Background()) + version1 := ruleVersionAndPauseStatus{fingerprint(rand.Uint64()), false} + version2 := ruleVersionAndPauseStatus{fingerprint(rand.Uint64()), false} + + wg := sync.WaitGroup{} + wg.Add(1) + go func() { + wg.Done() + r.update(version1) + wg.Done() + }() + wg.Wait() + wg.Add(2) // one when time1 is sent, another when go-routine for time2 has started + go func() { + wg.Done() + r.update(version2) + }() + wg.Wait() // at this point tick 1 has already been dropped + select { + case version := <-r.updateCh: + require.Equal(t, version2, version) + case <-time.After(5 * time.Second): + t.Fatal("No message was received on eval channel") + } + }) + t.Run("eval should send to evalCh", func(t *testing.T) { + r := newAlertRuleInfo(context.Background()) + expected := time.Now() + resultCh := make(chan evalResponse) + data := &evaluation{ + scheduledAt: expected, + rule: models.AlertRuleGen()(), + folderTitle: util.GenerateShortUID(), + } + go func() { + result, dropped := r.eval(data) + resultCh <- evalResponse{result, dropped} + }() + select { + case ctx := <-r.evalCh: + require.Equal(t, data, ctx) + result := <-resultCh + require.True(t, result.success) + require.Nilf(t, result.droppedEval, "expected no dropped evaluations but got one") + case <-time.After(5 * time.Second): + t.Fatal("No message was received on eval channel") + } + }) + t.Run("eval should drop any concurrent sending to evalCh", func(t *testing.T) { + r := newAlertRuleInfo(context.Background()) + time1 := time.UnixMilli(rand.Int63n(math.MaxInt64)) + time2 := time.UnixMilli(rand.Int63n(math.MaxInt64)) + resultCh1 := make(chan evalResponse) + resultCh2 := make(chan evalResponse) + data := &evaluation{ + scheduledAt: time1, + rule: models.AlertRuleGen()(), + folderTitle: util.GenerateShortUID(), + } + data2 := &evaluation{ + scheduledAt: time2, + rule: data.rule, + folderTitle: data.folderTitle, + } + wg := sync.WaitGroup{} + wg.Add(1) + go func() { + wg.Done() + result, dropped := r.eval(data) + wg.Done() + resultCh1 <- evalResponse{result, dropped} + }() + wg.Wait() + wg.Add(2) // one when time1 is sent, another when go-routine for time2 has started + go func() { + wg.Done() + result, dropped := r.eval(data2) + resultCh2 <- evalResponse{result, dropped} + }() + wg.Wait() // at this point tick 1 has already been dropped + select { + case ctx := <-r.evalCh: + require.Equal(t, time2, ctx.scheduledAt) + result := <-resultCh1 + require.True(t, result.success) + require.Nilf(t, result.droppedEval, "expected no dropped evaluations but got one") + result = <-resultCh2 + require.True(t, result.success) + require.NotNil(t, result.droppedEval, "expected no dropped evaluations but got one") + require.Equal(t, time1, result.droppedEval.scheduledAt) + case <-time.After(5 * time.Second): + t.Fatal("No message was received on eval channel") + } + }) + t.Run("eval should exit when context is cancelled", func(t *testing.T) { + r := newAlertRuleInfo(context.Background()) + resultCh := make(chan evalResponse) + data := &evaluation{ + scheduledAt: time.Now(), + rule: models.AlertRuleGen()(), + folderTitle: util.GenerateShortUID(), + } + go func() { + result, dropped := r.eval(data) + resultCh <- evalResponse{result, dropped} + }() + runtime.Gosched() + r.stop(nil) + select { + case result := <-resultCh: + require.False(t, result.success) + require.Nilf(t, result.droppedEval, "expected no dropped evaluations but got one") + case <-time.After(5 * time.Second): + t.Fatal("No message was received on eval channel") + } + }) + }) + t.Run("when rule evaluation is stopped", func(t *testing.T) { + t.Run("Update should do nothing", func(t *testing.T) { + r := newAlertRuleInfo(context.Background()) + r.stop(errRuleDeleted) + require.ErrorIs(t, r.ctx.Err(), errRuleDeleted) + require.False(t, r.update(ruleVersionAndPauseStatus{fingerprint(rand.Uint64()), false})) + }) + t.Run("eval should do nothing", func(t *testing.T) { + r := newAlertRuleInfo(context.Background()) + r.stop(nil) + data := &evaluation{ + scheduledAt: time.Now(), + rule: models.AlertRuleGen()(), + folderTitle: util.GenerateShortUID(), + } + success, dropped := r.eval(data) + require.False(t, success) + require.Nilf(t, dropped, "expected no dropped evaluations but got one") + }) + t.Run("stop should do nothing", func(t *testing.T) { + r := newAlertRuleInfo(context.Background()) + r.stop(nil) + r.stop(nil) + }) + t.Run("stop should do nothing if parent context stopped", func(t *testing.T) { + ctx, cancelFn := context.WithCancel(context.Background()) + r := newAlertRuleInfo(ctx) + cancelFn() + r.stop(nil) + }) + }) + t.Run("should be thread-safe", func(t *testing.T) { + r := newAlertRuleInfo(context.Background()) + wg := sync.WaitGroup{} + go func() { + for { + select { + case <-r.evalCh: + time.Sleep(time.Microsecond) + case <-r.updateCh: + time.Sleep(time.Microsecond) + case <-r.ctx.Done(): + return + } + } + }() + + for i := 0; i < 10; i++ { + wg.Add(1) + go func() { + for i := 0; i < 20; i++ { + max := 3 + if i <= 10 { + max = 2 + } + switch rand.Intn(max) + 1 { + case 1: + r.update(ruleVersionAndPauseStatus{fingerprint(rand.Uint64()), false}) + case 2: + r.eval(&evaluation{ + scheduledAt: time.Now(), + rule: models.AlertRuleGen()(), + folderTitle: util.GenerateShortUID(), + }) + case 3: + r.stop(nil) + } + } + wg.Done() + }() + } + + wg.Wait() + }) +} diff --git a/pkg/services/ngalert/schedule/registry.go b/pkg/services/ngalert/schedule/registry.go index ad7f403ad3..2a7cc7c705 100644 --- a/pkg/services/ngalert/schedule/registry.go +++ b/pkg/services/ngalert/schedule/registry.go @@ -13,7 +13,6 @@ import ( "unsafe" "github.com/grafana/grafana/pkg/services/ngalert/models" - "github.com/grafana/grafana/pkg/util" ) var errRuleDeleted = errors.New("rule deleted") @@ -73,59 +72,6 @@ type ruleVersionAndPauseStatus struct { IsPaused bool } -type alertRuleInfo struct { - evalCh chan *evaluation - updateCh chan ruleVersionAndPauseStatus - ctx context.Context - stop func(reason error) -} - -func newAlertRuleInfo(parent context.Context) *alertRuleInfo { - ctx, stop := util.WithCancelCause(parent) - return &alertRuleInfo{evalCh: make(chan *evaluation), updateCh: make(chan ruleVersionAndPauseStatus), ctx: ctx, stop: stop} -} - -// eval signals the rule evaluation routine to perform the evaluation of the rule. Does nothing if the loop is stopped. -// Before sending a message into the channel, it does non-blocking read to make sure that there is no concurrent send operation. -// Returns a tuple where first element is -// - true when message was sent -// - false when the send operation is stopped -// -// the second element contains a dropped message that was sent by a concurrent sender. -func (a *alertRuleInfo) eval(eval *evaluation) (bool, *evaluation) { - // read the channel in unblocking manner to make sure that there is no concurrent send operation. - var droppedMsg *evaluation - select { - case droppedMsg = <-a.evalCh: - default: - } - - select { - case a.evalCh <- eval: - return true, droppedMsg - case <-a.ctx.Done(): - return false, droppedMsg - } -} - -// update sends an instruction to the rule evaluation routine to update the scheduled rule to the specified version. The specified version must be later than the current version, otherwise no update will happen. -func (a *alertRuleInfo) update(lastVersion ruleVersionAndPauseStatus) bool { - // check if the channel is not empty. - select { - case <-a.updateCh: - case <-a.ctx.Done(): - return false - default: - } - - select { - case a.updateCh <- lastVersion: - return true - case <-a.ctx.Done(): - return false - } -} - type evaluation struct { scheduledAt time.Time rule *models.AlertRule diff --git a/pkg/services/ngalert/schedule/registry_test.go b/pkg/services/ngalert/schedule/registry_test.go index f812bf7a9d..70d4ec7b17 100644 --- a/pkg/services/ngalert/schedule/registry_test.go +++ b/pkg/services/ngalert/schedule/registry_test.go @@ -1,13 +1,9 @@ package schedule import ( - "context" "encoding/json" - "math" "math/rand" "reflect" - "runtime" - "sync" "testing" "time" @@ -16,223 +12,8 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/services/ngalert/models" - "github.com/grafana/grafana/pkg/util" ) -func TestSchedule_alertRuleInfo(t *testing.T) { - type evalResponse struct { - success bool - droppedEval *evaluation - } - - t.Run("when rule evaluation is not stopped", func(t *testing.T) { - t.Run("update should send to updateCh", func(t *testing.T) { - r := newAlertRuleInfo(context.Background()) - resultCh := make(chan bool) - go func() { - resultCh <- r.update(ruleVersionAndPauseStatus{fingerprint(rand.Uint64()), false}) - }() - select { - case <-r.updateCh: - require.True(t, <-resultCh) - case <-time.After(5 * time.Second): - t.Fatal("No message was received on update channel") - } - }) - t.Run("update should drop any concurrent sending to updateCh", func(t *testing.T) { - r := newAlertRuleInfo(context.Background()) - version1 := ruleVersionAndPauseStatus{fingerprint(rand.Uint64()), false} - version2 := ruleVersionAndPauseStatus{fingerprint(rand.Uint64()), false} - - wg := sync.WaitGroup{} - wg.Add(1) - go func() { - wg.Done() - r.update(version1) - wg.Done() - }() - wg.Wait() - wg.Add(2) // one when time1 is sent, another when go-routine for time2 has started - go func() { - wg.Done() - r.update(version2) - }() - wg.Wait() // at this point tick 1 has already been dropped - select { - case version := <-r.updateCh: - require.Equal(t, version2, version) - case <-time.After(5 * time.Second): - t.Fatal("No message was received on eval channel") - } - }) - t.Run("eval should send to evalCh", func(t *testing.T) { - r := newAlertRuleInfo(context.Background()) - expected := time.Now() - resultCh := make(chan evalResponse) - data := &evaluation{ - scheduledAt: expected, - rule: models.AlertRuleGen()(), - folderTitle: util.GenerateShortUID(), - } - go func() { - result, dropped := r.eval(data) - resultCh <- evalResponse{result, dropped} - }() - select { - case ctx := <-r.evalCh: - require.Equal(t, data, ctx) - result := <-resultCh - require.True(t, result.success) - require.Nilf(t, result.droppedEval, "expected no dropped evaluations but got one") - case <-time.After(5 * time.Second): - t.Fatal("No message was received on eval channel") - } - }) - t.Run("eval should drop any concurrent sending to evalCh", func(t *testing.T) { - r := newAlertRuleInfo(context.Background()) - time1 := time.UnixMilli(rand.Int63n(math.MaxInt64)) - time2 := time.UnixMilli(rand.Int63n(math.MaxInt64)) - resultCh1 := make(chan evalResponse) - resultCh2 := make(chan evalResponse) - data := &evaluation{ - scheduledAt: time1, - rule: models.AlertRuleGen()(), - folderTitle: util.GenerateShortUID(), - } - data2 := &evaluation{ - scheduledAt: time2, - rule: data.rule, - folderTitle: data.folderTitle, - } - wg := sync.WaitGroup{} - wg.Add(1) - go func() { - wg.Done() - result, dropped := r.eval(data) - wg.Done() - resultCh1 <- evalResponse{result, dropped} - }() - wg.Wait() - wg.Add(2) // one when time1 is sent, another when go-routine for time2 has started - go func() { - wg.Done() - result, dropped := r.eval(data2) - resultCh2 <- evalResponse{result, dropped} - }() - wg.Wait() // at this point tick 1 has already been dropped - select { - case ctx := <-r.evalCh: - require.Equal(t, time2, ctx.scheduledAt) - result := <-resultCh1 - require.True(t, result.success) - require.Nilf(t, result.droppedEval, "expected no dropped evaluations but got one") - result = <-resultCh2 - require.True(t, result.success) - require.NotNil(t, result.droppedEval, "expected no dropped evaluations but got one") - require.Equal(t, time1, result.droppedEval.scheduledAt) - case <-time.After(5 * time.Second): - t.Fatal("No message was received on eval channel") - } - }) - t.Run("eval should exit when context is cancelled", func(t *testing.T) { - r := newAlertRuleInfo(context.Background()) - resultCh := make(chan evalResponse) - data := &evaluation{ - scheduledAt: time.Now(), - rule: models.AlertRuleGen()(), - folderTitle: util.GenerateShortUID(), - } - go func() { - result, dropped := r.eval(data) - resultCh <- evalResponse{result, dropped} - }() - runtime.Gosched() - r.stop(nil) - select { - case result := <-resultCh: - require.False(t, result.success) - require.Nilf(t, result.droppedEval, "expected no dropped evaluations but got one") - case <-time.After(5 * time.Second): - t.Fatal("No message was received on eval channel") - } - }) - }) - t.Run("when rule evaluation is stopped", func(t *testing.T) { - t.Run("Update should do nothing", func(t *testing.T) { - r := newAlertRuleInfo(context.Background()) - r.stop(errRuleDeleted) - require.ErrorIs(t, r.ctx.Err(), errRuleDeleted) - require.False(t, r.update(ruleVersionAndPauseStatus{fingerprint(rand.Uint64()), false})) - }) - t.Run("eval should do nothing", func(t *testing.T) { - r := newAlertRuleInfo(context.Background()) - r.stop(nil) - data := &evaluation{ - scheduledAt: time.Now(), - rule: models.AlertRuleGen()(), - folderTitle: util.GenerateShortUID(), - } - success, dropped := r.eval(data) - require.False(t, success) - require.Nilf(t, dropped, "expected no dropped evaluations but got one") - }) - t.Run("stop should do nothing", func(t *testing.T) { - r := newAlertRuleInfo(context.Background()) - r.stop(nil) - r.stop(nil) - }) - t.Run("stop should do nothing if parent context stopped", func(t *testing.T) { - ctx, cancelFn := context.WithCancel(context.Background()) - r := newAlertRuleInfo(ctx) - cancelFn() - r.stop(nil) - }) - }) - t.Run("should be thread-safe", func(t *testing.T) { - r := newAlertRuleInfo(context.Background()) - wg := sync.WaitGroup{} - go func() { - for { - select { - case <-r.evalCh: - time.Sleep(time.Microsecond) - case <-r.updateCh: - time.Sleep(time.Microsecond) - case <-r.ctx.Done(): - return - } - } - }() - - for i := 0; i < 10; i++ { - wg.Add(1) - go func() { - for i := 0; i < 20; i++ { - max := 3 - if i <= 10 { - max = 2 - } - switch rand.Intn(max) + 1 { - case 1: - r.update(ruleVersionAndPauseStatus{fingerprint(rand.Uint64()), false}) - case 2: - r.eval(&evaluation{ - scheduledAt: time.Now(), - rule: models.AlertRuleGen()(), - folderTitle: util.GenerateShortUID(), - }) - case 3: - r.stop(nil) - } - } - wg.Done() - }() - } - - wg.Wait() - }) -} - func TestSchedulableAlertRulesRegistry(t *testing.T) { r := alertRulesRegistry{rules: make(map[models.AlertRuleKey]*models.AlertRule)} rules, folders := r.all() From 2e8c514cfd9299e3597ee463fd09ec32995814ad Mon Sep 17 00:00:00 2001 From: Matthew Jacobson Date: Mon, 4 Mar 2024 13:12:49 -0500 Subject: [PATCH 0373/1406] Alerting: Stop persisting user-defined templates to disk (#83456) Updates Grafana Alertmanager to work with new interface from grafana/alerting#161. This change stops passing user-defined templates to the Grafana Alertmanager by persisting them to disk and instead passes them by string. --- go.mod | 2 +- go.sum | 4 +- .../api/tooling/definitions/alertmanager.go | 3 +- pkg/services/ngalert/notifier/alertmanager.go | 35 +++---- pkg/services/ngalert/notifier/compat.go | 13 +++ pkg/services/ngalert/notifier/config.go | 88 +++-------------- pkg/services/ngalert/notifier/config_test.go | 98 ------------------- pkg/services/ngalert/notifier/status.go | 8 +- .../ngalert/provisioning/templates.go | 5 - 9 files changed, 47 insertions(+), 209 deletions(-) diff --git a/go.mod b/go.mod index cfd48e8433..505b996ac5 100644 --- a/go.mod +++ b/go.mod @@ -59,7 +59,7 @@ require ( github.com/google/uuid v1.6.0 // @grafana/backend-platform github.com/google/wire v0.5.0 // @grafana/backend-platform github.com/gorilla/websocket v1.5.0 // @grafana/grafana-app-platform-squad - github.com/grafana/alerting v0.0.0-20240222104113-abfafef9a7d2 // @grafana/alerting-squad-backend + github.com/grafana/alerting v0.0.0-20240304175322-e81931acc11b // @grafana/alerting-squad-backend github.com/grafana/cuetsy v0.1.11 // @grafana/grafana-as-code github.com/grafana/grafana-aws-sdk v0.24.0 // @grafana/aws-datasources github.com/grafana/grafana-azure-sdk-go v1.12.0 // @grafana/partner-datasources diff --git a/go.sum b/go.sum index de4793c3d1..2d5836d638 100644 --- a/go.sum +++ b/go.sum @@ -2161,8 +2161,8 @@ github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grafana/alerting v0.0.0-20240222104113-abfafef9a7d2 h1:fmUMdtP7ditGgJFdXCwVxDrKnondHNNe0TkhN5YaIAI= -github.com/grafana/alerting v0.0.0-20240222104113-abfafef9a7d2/go.mod h1:brTFeACal/cSZAR8XO/4LPKs7rzNfS86okl6QjSP1eY= +github.com/grafana/alerting v0.0.0-20240304175322-e81931acc11b h1:rYx9ds94ZrueuXioEnoSqL737UYPSngPkMwBFl1guJE= +github.com/grafana/alerting v0.0.0-20240304175322-e81931acc11b/go.mod h1:brTFeACal/cSZAR8XO/4LPKs7rzNfS86okl6QjSP1eY= github.com/grafana/codejen v0.0.3 h1:tAWxoTUuhgmEqxJPOLtJoxlPBbMULFwKFOcRsPRPXDw= github.com/grafana/codejen v0.0.3/go.mod h1:zmwwM/DRyQB7pfuBjTWII3CWtxcXh8LTwAYGfDfpR6s= github.com/grafana/cue v0.0.0-20230926092038-971951014e3f h1:TmYAMnqg3d5KYEAaT6PtTguL2GjLfvr6wnAX8Azw6tQ= diff --git a/pkg/services/ngalert/api/tooling/definitions/alertmanager.go b/pkg/services/ngalert/api/tooling/definitions/alertmanager.go index aa698d53f2..438d1f85f9 100644 --- a/pkg/services/ngalert/api/tooling/definitions/alertmanager.go +++ b/pkg/services/ngalert/api/tooling/definitions/alertmanager.go @@ -808,7 +808,8 @@ type Config struct { // MuteTimeIntervals is deprecated and will be removed before Alertmanager 1.0. MuteTimeIntervals []config.MuteTimeInterval `yaml:"mute_time_intervals,omitempty" json:"mute_time_intervals,omitempty"` TimeIntervals []config.TimeInterval `yaml:"time_intervals,omitempty" json:"time_intervals,omitempty"` - Templates []string `yaml:"templates" json:"templates"` + // Templates is unused by Grafana Managed AM but is passed-through for compatibility with some external AMs. + Templates []string `yaml:"templates" json:"templates"` } // A Route is a node that contains definitions of how to handle alerts. This is modified diff --git a/pkg/services/ngalert/notifier/alertmanager.go b/pkg/services/ngalert/notifier/alertmanager.go index 3f0dffb038..a4dc3d2ead 100644 --- a/pkg/services/ngalert/notifier/alertmanager.go +++ b/pkg/services/ngalert/notifier/alertmanager.go @@ -123,7 +123,6 @@ func NewAlertmanager(ctx context.Context, orgID int64, cfg *setting.Cfg, store A } amcfg := &alertingNotify.GrafanaAlertmanagerConfig{ - WorkingDirectory: filepath.Join(cfg.DataPath, workingDir, strconv.Itoa(int(orgID))), ExternalURL: cfg.AppURL, AlertStoreCallback: nil, PeerTimeout: cfg.UnifiedAlerting.HAPeerTimeout, @@ -321,38 +320,28 @@ func (am *alertmanager) aggregateInhibitMatchers(rules []config.InhibitRule, amu // It is not safe to call concurrently. func (am *alertmanager) applyConfig(cfg *apimodels.PostableUserConfig) (bool, error) { // First, let's make sure this config is not already loaded - var amConfigChanged bool - rawConfig, err := json.Marshal(cfg.AlertmanagerConfig) + rawConfig, err := json.Marshal(cfg) if err != nil { // In theory, this should never happen. return false, err } - if am.Base.ConfigHash() != md5.Sum(rawConfig) { - amConfigChanged = true - } - - if cfg.TemplateFiles == nil { - cfg.TemplateFiles = map[string]string{} - } - cfg.TemplateFiles["__default__.tmpl"] = alertingTemplates.DefaultTemplateString - - // next, we need to make sure we persist the templates to disk. - paths, templatesChanged, err := PersistTemplates(am.logger, cfg, am.Base.WorkingDirectory()) - if err != nil { - return false, err - } - cfg.AlertmanagerConfig.Templates = paths - - // If neither the configuration nor templates have changed, we've got nothing to do. - if !amConfigChanged && !templatesChanged { - am.logger.Debug("Neither config nor template have changed, skipping configuration sync.") + // If configuration hasn't changed, we've got nothing to do. + configHash := md5.Sum(rawConfig) + if am.Base.ConfigHash() == configHash { + am.logger.Debug("Config hasn't changed, skipping configuration sync.") return false, nil } + am.logger.Info("Applying new configuration to Alertmanager", "configHash", fmt.Sprintf("%x", configHash)) err = am.Base.ApplyConfig(AlertingConfiguration{ rawAlertmanagerConfig: rawConfig, - alertmanagerConfig: cfg.AlertmanagerConfig, + configHash: configHash, + route: cfg.AlertmanagerConfig.Route.AsAMRoute(), + inhibitRules: cfg.AlertmanagerConfig.InhibitRules, + muteTimeIntervals: cfg.AlertmanagerConfig.MuteTimeIntervals, + timeIntervals: cfg.AlertmanagerConfig.TimeIntervals, + templates: ToTemplateDefinitions(cfg), receivers: PostableApiAlertingConfigToApiReceivers(cfg.AlertmanagerConfig), receiverIntegrationsFunc: am.buildReceiverIntegrations, }) diff --git a/pkg/services/ngalert/notifier/compat.go b/pkg/services/ngalert/notifier/compat.go index b07ae172ba..7d6580b9ec 100644 --- a/pkg/services/ngalert/notifier/compat.go +++ b/pkg/services/ngalert/notifier/compat.go @@ -4,6 +4,7 @@ import ( "encoding/json" alertingNotify "github.com/grafana/alerting/notify" + alertingTemplates "github.com/grafana/alerting/templates" "github.com/prometheus/alertmanager/config" "github.com/grafana/grafana/pkg/components/simplejson" @@ -109,3 +110,15 @@ func PostableToGettableApiReceiver(r *apimodels.PostableApiReceiver, provenances return out, nil } + +// ToTemplateDefinitions converts the given PostableUserConfig's TemplateFiles to a slice of TemplateDefinitions. +func ToTemplateDefinitions(cfg *apimodels.PostableUserConfig) []alertingTemplates.TemplateDefinition { + out := make([]alertingTemplates.TemplateDefinition, 0, len(cfg.TemplateFiles)) + for name, tmpl := range cfg.TemplateFiles { + out = append(out, alertingTemplates.TemplateDefinition{ + Name: name, + Template: tmpl, + }) + } + return out +} diff --git a/pkg/services/ngalert/notifier/config.go b/pkg/services/ngalert/notifier/config.go index 0ea24acf64..bffe1b72ce 100644 --- a/pkg/services/ngalert/notifier/config.go +++ b/pkg/services/ngalert/notifier/config.go @@ -1,82 +1,15 @@ package notifier import ( - "crypto/md5" "encoding/json" "fmt" - "os" - "path/filepath" alertingNotify "github.com/grafana/alerting/notify" alertingTemplates "github.com/grafana/alerting/templates" - "github.com/grafana/grafana/pkg/infra/log" api "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" ) -func PersistTemplates(logger log.Logger, cfg *api.PostableUserConfig, path string) ([]string, bool, error) { - if len(cfg.TemplateFiles) < 1 { - return nil, false, nil - } - - var templatesChanged bool - pathSet := map[string]struct{}{} - for name, content := range cfg.TemplateFiles { - if name != filepath.Base(filepath.Clean(name)) { - return nil, false, fmt.Errorf("template file name '%s' is not valid", name) - } - - err := os.MkdirAll(path, 0750) - if err != nil { - return nil, false, fmt.Errorf("unable to create template directory %q: %s", path, err) - } - - file := filepath.Join(path, name) - pathSet[name] = struct{}{} - - // Check if the template file already exists and if it has changed - // We can safely ignore gosec here as we've previously checked the filename is clean - // nolint:gosec - if tmpl, err := os.ReadFile(file); err == nil && string(tmpl) == content { - // Templates file is the same we have, no-op and continue. - continue - } else if err != nil && !os.IsNotExist(err) { - return nil, false, err - } - - // We can safely ignore gosec here as we've previously checked the filename is clean - // nolint:gosec - if err := os.WriteFile(file, []byte(content), 0644); err != nil { - return nil, false, fmt.Errorf("unable to create Alertmanager template file %q: %s", file, err) - } - - templatesChanged = true - } - - // Now that we have the list of _actual_ templates, let's remove the ones that we don't need. - existingFiles, err := os.ReadDir(path) - if err != nil { - logger.Error("Unable to read directory for deleting Alertmanager templates", "error", err, "path", path) - } - for _, existingFile := range existingFiles { - p := filepath.Join(path, existingFile.Name()) - _, ok := pathSet[existingFile.Name()] - if !ok { - templatesChanged = true - err := os.Remove(p) - if err != nil { - logger.Error("Unable to delete template", "error", err, "file", p) - } - } - } - - paths := make([]string, 0, len(pathSet)) - for path := range pathSet { - paths = append(paths, path) - } - return paths, templatesChanged, nil -} - func Load(rawConfig []byte) (*api.PostableUserConfig, error) { cfg := &api.PostableUserConfig{} @@ -90,8 +23,13 @@ func Load(rawConfig []byte) (*api.PostableUserConfig, error) { // AlertingConfiguration provides configuration for an Alertmanager. // It implements the notify.Configuration interface. type AlertingConfiguration struct { - alertmanagerConfig api.PostableApiAlertingConfig + route *alertingNotify.Route + inhibitRules []alertingNotify.InhibitRule + muteTimeIntervals []alertingNotify.MuteTimeInterval + timeIntervals []alertingNotify.TimeInterval + templates []alertingTemplates.TemplateDefinition rawAlertmanagerConfig []byte + configHash [16]byte receivers []*alertingNotify.APIReceiver receiverIntegrationsFunc func(r *alertingNotify.APIReceiver, tmpl *alertingTemplates.Template) ([]*alertingNotify.Integration, error) @@ -108,15 +46,15 @@ func (a AlertingConfiguration) DispatcherLimits() alertingNotify.DispatcherLimit } func (a AlertingConfiguration) InhibitRules() []alertingNotify.InhibitRule { - return a.alertmanagerConfig.InhibitRules + return a.inhibitRules } func (a AlertingConfiguration) MuteTimeIntervals() []alertingNotify.MuteTimeInterval { - return a.alertmanagerConfig.MuteTimeIntervals + return a.muteTimeIntervals } func (a AlertingConfiguration) TimeIntervals() []alertingNotify.TimeInterval { - return a.alertmanagerConfig.TimeIntervals + return a.timeIntervals } func (a AlertingConfiguration) Receivers() []*alertingNotify.APIReceiver { @@ -124,15 +62,15 @@ func (a AlertingConfiguration) Receivers() []*alertingNotify.APIReceiver { } func (a AlertingConfiguration) RoutingTree() *alertingNotify.Route { - return a.alertmanagerConfig.Route.AsAMRoute() + return a.route } -func (a AlertingConfiguration) Templates() []string { - return a.alertmanagerConfig.Templates +func (a AlertingConfiguration) Templates() []alertingTemplates.TemplateDefinition { + return a.templates } func (a AlertingConfiguration) Hash() [16]byte { - return md5.Sum(a.rawAlertmanagerConfig) + return a.configHash } func (a AlertingConfiguration) Raw() []byte { diff --git a/pkg/services/ngalert/notifier/config_test.go b/pkg/services/ngalert/notifier/config_test.go index c39b4f2b2b..76b910e795 100644 --- a/pkg/services/ngalert/notifier/config_test.go +++ b/pkg/services/ngalert/notifier/config_test.go @@ -2,110 +2,12 @@ package notifier import ( "errors" - "os" - "path/filepath" "testing" - "github.com/grafana/grafana/pkg/infra/log/logtest" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - - api "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" ) -func TestPersistTemplates(t *testing.T) { - tc := []struct { - name string - templates map[string]string - existingTemplates map[string]string - expectedPaths []string - expectedError error - expectedChange bool - }{ - { - name: "With valid templates file names, it persists successfully", - templates: map[string]string{"email.template": "a perfectly fine template"}, - expectedChange: true, - expectedError: nil, - expectedPaths: []string{"email.template"}, - }, - { - name: "With a invalid filename, it fails", - templates: map[string]string{"adirectory/email.template": "a perfectly fine template"}, - expectedError: errors.New("template file name 'adirectory/email.template' is not valid"), - }, - { - name: "with a template that has the same name but different content to an existing one", - existingTemplates: map[string]string{"email.template": "a perfectly fine template"}, - templates: map[string]string{"email.template": "a completely different content"}, - expectedChange: true, - expectedError: nil, - expectedPaths: []string{"email.template"}, - }, - { - name: "with a template that has the same name and the same content as an existing one", - existingTemplates: map[string]string{"email.template": "a perfectly fine template"}, - templates: map[string]string{"email.template": "a perfectly fine template"}, - expectedChange: false, - expectedError: nil, - expectedPaths: []string{"email.template"}, - }, - { - name: "with two new template files, it changes the template tree", - existingTemplates: map[string]string{"email.template": "a perfectly fine template"}, - templates: map[string]string{"slack.template": "a perfectly fine template", "webhook.template": "a webhook template"}, - expectedChange: true, - expectedError: nil, - expectedPaths: []string{"slack.template", "webhook.template"}, - }, - { - name: "when we remove a template file from the list, it changes the template tree", - existingTemplates: map[string]string{"slack.template": "a perfectly fine template", "webhook.template": "a webhook template"}, - templates: map[string]string{"slack.template": "a perfectly fine template"}, - expectedChange: true, - expectedError: nil, - expectedPaths: []string{"slack.template"}, - }, - } - - for _, tt := range tc { - t.Run(tt.name, func(t *testing.T) { - dir := t.TempDir() - // Write "existing files" - for name, content := range tt.existingTemplates { - err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0644) - require.NoError(t, err) - } - c := &api.PostableUserConfig{TemplateFiles: tt.templates} - - testLogger := logtest.Fake{} - paths, changed, persistErr := PersistTemplates(&testLogger, c, dir) - - files := map[string]string{} - readFiles, err := os.ReadDir(dir) - require.NoError(t, err) - for _, f := range readFiles { - if f.IsDir() || f.Name() == "" { - continue - } - // Safe to disable, this is a test. - // nolint:gosec - content, err := os.ReadFile(filepath.Join(dir, f.Name())) - // nolint:gosec - require.NoError(t, err) - files[f.Name()] = string(content) - } - - require.Equal(t, tt.expectedError, persistErr) - require.ElementsMatch(t, tt.expectedPaths, paths) - require.Equal(t, tt.expectedChange, changed) - if tt.expectedError == nil { - require.Equal(t, tt.templates, files) - } - }) - } -} - func TestLoad(t *testing.T) { tc := []struct { name string diff --git a/pkg/services/ngalert/notifier/status.go b/pkg/services/ngalert/notifier/status.go index e93dbffd01..bcdf065616 100644 --- a/pkg/services/ngalert/notifier/status.go +++ b/pkg/services/ngalert/notifier/status.go @@ -8,15 +8,15 @@ import ( // TODO: We no longer do apimodels at this layer, move it to the API. func (am *alertmanager) GetStatus() apimodels.GettableStatus { - config := &apimodels.PostableApiAlertingConfig{} - status := am.Base.GetStatus() // TODO: This should return a GettableStatus, for now it returns PostableApiAlertingConfig. + config := &apimodels.PostableUserConfig{} + status := am.Base.GetStatus() // TODO: This should return a GettableStatus, for now it returns PostableUserConfig. if status == nil { - return *apimodels.NewGettableStatus(config) + return *apimodels.NewGettableStatus(&config.AlertmanagerConfig) } if err := json.Unmarshal(status, config); err != nil { am.logger.Error("Unable to unmarshall alertmanager config", "Err", err) } - return *apimodels.NewGettableStatus(config) + return *apimodels.NewGettableStatus(&config.AlertmanagerConfig) } diff --git a/pkg/services/ngalert/provisioning/templates.go b/pkg/services/ngalert/provisioning/templates.go index 4614017b8c..2ed37ae372 100644 --- a/pkg/services/ngalert/provisioning/templates.go +++ b/pkg/services/ngalert/provisioning/templates.go @@ -63,11 +63,6 @@ func (t *TemplateService) SetTemplate(ctx context.Context, orgID int64, tmpl def revision.cfg.TemplateFiles = map[string]string{} } revision.cfg.TemplateFiles[tmpl.Name] = tmpl.Template - tmpls := make([]string, 0, len(revision.cfg.TemplateFiles)) - for name := range revision.cfg.TemplateFiles { - tmpls = append(tmpls, name) - } - revision.cfg.AlertmanagerConfig.Templates = tmpls err = t.xact.InTransaction(ctx, func(ctx context.Context) error { if err := t.configStore.Save(ctx, revision, orgID); err != nil { From 5aa965b9e948a30cc9fa71becabc81d8f2d97b65 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Mon, 4 Mar 2024 10:23:32 -0800 Subject: [PATCH 0374/1406] Prometheus: remove cue definition (#83808) Co-authored-by: ismail simsek --- packages/grafana-prometheus/src/dataquery.cue | 58 ---------- .../grafana-prometheus/src/dataquery.ts | 12 +- packages/grafana-prometheus/src/index.ts | 2 +- .../components/PromQueryBuilderOptions.tsx | 2 +- .../components/PromQueryEditorSelector.tsx | 2 +- packages/grafana-prometheus/src/types.ts | 2 +- .../x/PrometheusDataQuery_types.gen.ts | 60 ---------- pkg/tsdb/prometheus/healthcheck.go | 12 +- .../kinds/dataquery/types_dataquery_gen.go | 106 ------------------ pkg/tsdb/prometheus/models/query.go | 101 ++++++++++++----- pkg/tsdb/prometheus/models/result.go | 1 + .../querydata/framing_bench_test.go | 7 +- pkg/tsdb/prometheus/querydata/framing_test.go | 18 +-- pkg/tsdb/prometheus/querydata/request_test.go | 46 ++++---- .../datasource/prometheus/dataquery.cue | 58 ---------- .../datasource/prometheus/dataquery.ts | 12 +- .../components/PromQueryBuilderOptions.tsx | 2 +- .../components/PromQueryEditorSelector.tsx | 2 +- .../plugins/datasource/prometheus/types.ts | 2 +- 19 files changed, 120 insertions(+), 385 deletions(-) delete mode 100644 packages/grafana-prometheus/src/dataquery.cue rename public/app/plugins/datasource/prometheus/dataquery.gen.ts => packages/grafana-prometheus/src/dataquery.ts (82%) delete mode 100644 packages/grafana-schema/src/raw/composable/prometheus/dataquery/x/PrometheusDataQuery_types.gen.ts delete mode 100644 pkg/tsdb/prometheus/kinds/dataquery/types_dataquery_gen.go delete mode 100644 public/app/plugins/datasource/prometheus/dataquery.cue rename packages/grafana-prometheus/src/dataquery.gen.ts => public/app/plugins/datasource/prometheus/dataquery.ts (82%) diff --git a/packages/grafana-prometheus/src/dataquery.cue b/packages/grafana-prometheus/src/dataquery.cue deleted file mode 100644 index 177af1929c..0000000000 --- a/packages/grafana-prometheus/src/dataquery.cue +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright 2023 Grafana Labs -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package grafanaplugin - -import ( - common "github.com/grafana/grafana/packages/grafana-schema/src/common" -) - -composableKinds: DataQuery: { - maturity: "experimental" - - lineage: { - schemas: [{ - version: [0, 0] - schema: { - common.DataQuery - - // The actual expression/query that will be evaluated by Prometheus - expr: string - // Returns only the latest value that Prometheus has scraped for the requested time series - instant?: bool - // Returns a Range vector, comprised of a set of time series containing a range of data points over time for each time series - range?: bool - // Execute an additional query to identify interesting raw samples relevant for the given expr - exemplar?: bool - // Specifies which editor is being used to prepare the query. It can be "code" or "builder" - editorMode?: #QueryEditorMode - // Query format to determine how to display data points in panel. It can be "time_series", "table", "heatmap" - format?: #PromQueryFormat - // Series name override or template. Ex. {{hostname}} will be replaced with label value for hostname - legendFormat?: string - // @deprecated Used to specify how many times to divide max data points by. We use max data points under query options - // See https://github.com/grafana/grafana/issues/48081 - intervalFactor?: number - - scope?: { - matchers: string - } - - #QueryEditorMode: "code" | "builder" @cuetsy(kind="enum") - #PromQueryFormat: "time_series" | "table" | "heatmap" @cuetsy(kind="type") - } - }] - lenses: [] - } -} diff --git a/public/app/plugins/datasource/prometheus/dataquery.gen.ts b/packages/grafana-prometheus/src/dataquery.ts similarity index 82% rename from public/app/plugins/datasource/prometheus/dataquery.gen.ts rename to packages/grafana-prometheus/src/dataquery.ts index 3ba622cecf..8609fc7891 100644 --- a/public/app/plugins/datasource/prometheus/dataquery.gen.ts +++ b/packages/grafana-prometheus/src/dataquery.ts @@ -1,13 +1,3 @@ -// Code generated - EDITING IS FUTILE. DO NOT EDIT. -// -// Generated by: -// public/app/plugins/gen.go -// Using jennies: -// TSTypesJenny -// PluginTSTypesJenny -// -// Run 'make gen-cue' from repository root to regenerate. - import * as common from '@grafana/schema'; export enum QueryEditorMode { @@ -15,7 +5,7 @@ export enum QueryEditorMode { Code = 'code', } -export type PromQueryFormat = ('time_series' | 'table' | 'heatmap'); +export type PromQueryFormat = 'time_series' | 'table' | 'heatmap'; export interface Prometheus extends common.DataQuery { /** diff --git a/packages/grafana-prometheus/src/index.ts b/packages/grafana-prometheus/src/index.ts index 978ddc79b0..76302ec957 100644 --- a/packages/grafana-prometheus/src/index.ts +++ b/packages/grafana-prometheus/src/index.ts @@ -62,7 +62,7 @@ export { PromQail } from './querybuilder/components/promQail/PromQail'; export { PrometheusDatasource } from './datasource'; // The parts export { addLabelToQuery } from './add_label_to_query'; -export { type QueryEditorMode, type PromQueryFormat, type Prometheus } from './dataquery.gen'; +export { type QueryEditorMode, type PromQueryFormat, type Prometheus } from './dataquery'; export { PrometheusMetricFindQuery } from './metric_find_query'; export { promqlGrammar } from './promql'; export { getQueryHints, getInitHints } from './query_hints'; diff --git a/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilderOptions.tsx b/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilderOptions.tsx index 5a6b827379..50788f2c62 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilderOptions.tsx +++ b/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilderOptions.tsx @@ -6,7 +6,7 @@ import { EditorField, EditorRow, EditorSwitch } from '@grafana/experimental'; import { AutoSizeInput, RadioButtonGroup, Select } from '@grafana/ui'; import { getQueryTypeChangeHandler, getQueryTypeOptions } from '../../components/PromExploreExtraField'; -import { PromQueryFormat } from '../../dataquery.gen'; +import { PromQueryFormat } from '../../dataquery'; import { PromQuery } from '../../types'; import { QueryOptionGroup } from '../shared/QueryOptionGroup'; diff --git a/packages/grafana-prometheus/src/querybuilder/components/PromQueryEditorSelector.tsx b/packages/grafana-prometheus/src/querybuilder/components/PromQueryEditorSelector.tsx index a0016b986e..f85090e0ca 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/PromQueryEditorSelector.tsx +++ b/packages/grafana-prometheus/src/querybuilder/components/PromQueryEditorSelector.tsx @@ -8,7 +8,7 @@ import { reportInteraction } from '@grafana/runtime'; import { Button, ConfirmModal, Space } from '@grafana/ui'; import { PromQueryEditorProps } from '../../components/types'; -import { PromQueryFormat } from '../../dataquery.gen'; +import { PromQueryFormat } from '../../dataquery'; import { PromQuery } from '../../types'; import { QueryPatternsModal } from '../QueryPatternsModal'; import { promQueryEditorExplainKey, useFlag } from '../hooks/useFlag'; diff --git a/packages/grafana-prometheus/src/types.ts b/packages/grafana-prometheus/src/types.ts index 5ddb69d811..f3c5730b38 100644 --- a/packages/grafana-prometheus/src/types.ts +++ b/packages/grafana-prometheus/src/types.ts @@ -1,7 +1,7 @@ import { DataSourceJsonData } from '@grafana/data'; import { DataQuery } from '@grafana/schema'; -import { Prometheus as GenPromQuery } from './dataquery.gen'; +import { Prometheus as GenPromQuery } from './dataquery'; import { QueryBuilderLabelFilter, QueryEditorMode } from './querybuilder/shared/types'; export interface PromQuery extends GenPromQuery, DataQuery { diff --git a/packages/grafana-schema/src/raw/composable/prometheus/dataquery/x/PrometheusDataQuery_types.gen.ts b/packages/grafana-schema/src/raw/composable/prometheus/dataquery/x/PrometheusDataQuery_types.gen.ts deleted file mode 100644 index 0d3806b34e..0000000000 --- a/packages/grafana-schema/src/raw/composable/prometheus/dataquery/x/PrometheusDataQuery_types.gen.ts +++ /dev/null @@ -1,60 +0,0 @@ -// Code generated - EDITING IS FUTILE. DO NOT EDIT. -// -// Generated by: -// public/app/plugins/gen.go -// Using jennies: -// TSTypesJenny -// LatestMajorsOrXJenny -// PluginEachMajorJenny -// -// Run 'make gen-cue' from repository root to regenerate. - -import * as common from '@grafana/schema'; - -export const pluginVersion = "11.0.0-pre"; - -export enum QueryEditorMode { - Builder = 'builder', - Code = 'code', -} - -export type PromQueryFormat = ('time_series' | 'table' | 'heatmap'); - -export interface PrometheusDataQuery extends common.DataQuery { - /** - * Specifies which editor is being used to prepare the query. It can be "code" or "builder" - */ - editorMode?: QueryEditorMode; - /** - * Execute an additional query to identify interesting raw samples relevant for the given expr - */ - exemplar?: boolean; - /** - * The actual expression/query that will be evaluated by Prometheus - */ - expr: string; - /** - * Query format to determine how to display data points in panel. It can be "time_series", "table", "heatmap" - */ - format?: PromQueryFormat; - /** - * Returns only the latest value that Prometheus has scraped for the requested time series - */ - instant?: boolean; - /** - * @deprecated Used to specify how many times to divide max data points by. We use max data points under query options - * See https://github.com/grafana/grafana/issues/48081 - */ - intervalFactor?: number; - /** - * Series name override or template. Ex. {{hostname}} will be replaced with label value for hostname - */ - legendFormat?: string; - /** - * Returns a Range vector, comprised of a set of time series containing a range of data points over time for each time series - */ - range?: boolean; - scope?: { - matchers: string; - }; -} diff --git a/pkg/tsdb/prometheus/healthcheck.go b/pkg/tsdb/prometheus/healthcheck.go index 4041d9c08e..1f5e6324c0 100644 --- a/pkg/tsdb/prometheus/healthcheck.go +++ b/pkg/tsdb/prometheus/healthcheck.go @@ -7,12 +7,9 @@ import ( "fmt" "time" - "github.com/grafana/kindsys" - "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/log" - "github.com/grafana/grafana/pkg/tsdb/prometheus/kinds/dataquery" "github.com/grafana/grafana/pkg/tsdb/prometheus/models" ) @@ -59,12 +56,13 @@ func (s *Service) CheckHealth(ctx context.Context, req *backend.CheckHealthReque func healthcheck(ctx context.Context, req *backend.CheckHealthRequest, i *instance) (*backend.CheckHealthResult, error) { qm := models.QueryModel{ - LegendFormat: "", UtcOffsetSec: 0, - PrometheusDataQuery: dataquery.PrometheusDataQuery{ + CommonQueryProperties: models.CommonQueryProperties{ + RefId: refID, + }, + PrometheusQueryProperties: models.PrometheusQueryProperties{ Expr: "1+1", - Instant: kindsys.Ptr(true), - RefId: refID, + Instant: true, }, } b, _ := json.Marshal(&qm) diff --git a/pkg/tsdb/prometheus/kinds/dataquery/types_dataquery_gen.go b/pkg/tsdb/prometheus/kinds/dataquery/types_dataquery_gen.go deleted file mode 100644 index b31c9eee62..0000000000 --- a/pkg/tsdb/prometheus/kinds/dataquery/types_dataquery_gen.go +++ /dev/null @@ -1,106 +0,0 @@ -// Code generated - EDITING IS FUTILE. DO NOT EDIT. -// -// Generated by: -// public/app/plugins/gen.go -// Using jennies: -// PluginGoTypesJenny -// -// Run 'make gen-cue' from repository root to regenerate. - -package dataquery - -// Defines values for PromQueryFormat. -const ( - PromQueryFormatHeatmap PromQueryFormat = "heatmap" - PromQueryFormatTable PromQueryFormat = "table" - PromQueryFormatTimeSeries PromQueryFormat = "time_series" -) - -// Defines values for QueryEditorMode. -const ( - QueryEditorModeBuilder QueryEditorMode = "builder" - QueryEditorModeCode QueryEditorMode = "code" -) - -// These are the common properties available to all queries in all datasources. -// Specific implementations will *extend* this interface, adding the required -// properties for the given context. -type DataQuery struct { - // 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 *any `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"` - - // Specify the query flavor - // TODO make this required and give it a default - QueryType *string `json:"queryType,omitempty"` - - // 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"` -} - -// PromQueryFormat defines model for PromQueryFormat. -type PromQueryFormat string - -// PrometheusDataQuery defines model for PrometheusDataQuery. -type PrometheusDataQuery struct { - // DataQuery These are the common properties available to all queries in all datasources. - // Specific implementations will *extend* this interface, adding the required - // properties for the given context. - 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 - Datasource *any `json:"datasource,omitempty"` - EditorMode *QueryEditorMode `json:"editorMode,omitempty"` - - // Execute an additional query to identify interesting raw samples relevant for the given expr - Exemplar *bool `json:"exemplar,omitempty"` - - // The actual expression/query that will be evaluated by Prometheus - Expr string `json:"expr"` - Format *PromQueryFormat `json:"format,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"` - - // Returns only the latest value that Prometheus has scraped for the requested time series - Instant *bool `json:"instant,omitempty"` - - // @deprecated Used to specify how many times to divide max data points by. We use max data points under query options - // See https://github.com/grafana/grafana/issues/48081 - IntervalFactor *float32 `json:"intervalFactor,omitempty"` - - // Series name override or template. Ex. {{hostname}} will be replaced with label value for hostname - LegendFormat *string `json:"legendFormat,omitempty"` - - // Specify the query flavor - // TODO make this required and give it a default - QueryType *string `json:"queryType,omitempty"` - - // Returns a Range vector, comprised of a set of time series containing a range of data points over time for each time series - Range *bool `json:"range,omitempty"` - - // 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"` - Scope *struct { - Matchers string `json:"matchers"` - } `json:"scope,omitempty"` -} - -// QueryEditorMode defines model for QueryEditorMode. -type QueryEditorMode string diff --git a/pkg/tsdb/prometheus/models/query.go b/pkg/tsdb/prometheus/models/query.go index 6c155810fb..b510b769e0 100644 --- a/pkg/tsdb/prometheus/models/query.go +++ b/pkg/tsdb/prometheus/models/query.go @@ -14,9 +14,61 @@ import ( "github.com/prometheus/prometheus/promql/parser" "github.com/grafana/grafana/pkg/tsdb/prometheus/intervalv2" - "github.com/grafana/grafana/pkg/tsdb/prometheus/kinds/dataquery" ) +// PromQueryFormat defines model for PromQueryFormat. +// +enum +type PromQueryFormat string + +const ( + PromQueryFormatTimeSeries PromQueryFormat = "time_series" + PromQueryFormatTable PromQueryFormat = "table" + PromQueryFormatHeatmap PromQueryFormat = "heatmap" +) + +// QueryEditorMode defines model for QueryEditorMode. +// +enum +type QueryEditorMode string + +const ( + QueryEditorModeBuilder QueryEditorMode = "builder" + QueryEditorModeCode QueryEditorMode = "code" +) + +// PrometheusQueryProperties defines the specific properties used for prometheus +type PrometheusQueryProperties struct { + // The response format + Format PromQueryFormat `json:"format,omitempty"` + + // The actual expression/query that will be evaluated by Prometheus + Expr string `json:"expr"` + + // Returns a Range vector, comprised of a set of time series containing a range of data points over time for each time series + Range bool `json:"range,omitempty"` + + // Returns only the latest value that Prometheus has scraped for the requested time series + Instant bool `json:"instant,omitempty"` + + // Execute an additional query to identify interesting raw samples relevant for the given expr + Exemplar bool `json:"exemplar,omitempty"` + + // what we should show in the editor + EditorMode QueryEditorMode `json:"editorMode,omitempty"` + + // Used to specify how many times to divide max data points by. We use max data points under query options + // See https://github.com/grafana/grafana/issues/48081 + // Deprecated: use interval + IntervalFactor int64 `json:"intervalFactor,omitempty"` + + // Series name override or template. Ex. {{hostname}} will be replaced with label value for hostname + LegendFormat string `json:"legendFormat,omitempty"` + + // ??? + Scope *struct { + Matchers string `json:"matchers"` + } `json:"scope,omitempty"` +} + // Internal interval and range variables const ( varInterval = "$__interval" @@ -51,15 +103,22 @@ const ( var safeResolution = 11000 +// QueryModel includes both the common and specific values type QueryModel struct { - dataquery.PrometheusDataQuery + PrometheusQueryProperties `json:",inline"` + CommonQueryProperties `json:",inline"` + // The following properties may be part of the request payload, however they are not saved in panel JSON // Timezone offset to align start & end time on backend - UtcOffsetSec int64 `json:"utcOffsetSec,omitempty"` - LegendFormat string `json:"legendFormat,omitempty"` - Interval string `json:"interval,omitempty"` - IntervalMs int64 `json:"intervalMs,omitempty"` - IntervalFactor int64 `json:"intervalFactor,omitempty"` + UtcOffsetSec int64 `json:"utcOffsetSec,omitempty"` + Interval string `json:"interval,omitempty"` +} + +// CommonQueryProperties is properties applied to all queries +// NOTE: this will soon be replaced with a struct from the SDK +type CommonQueryProperties struct { + RefId string `json:"refId,omitempty"` + IntervalMs int64 `json:"intervalMs,omitempty"` } type TimeRange struct { @@ -68,6 +127,7 @@ type TimeRange struct { Step time.Duration } +// The internal query object type Query struct { Expr string Step time.Duration @@ -119,29 +179,14 @@ func Parse(query backend.DataQuery, dsScrapeInterval string, intervalCalculator return nil, err } } - var rangeQuery, instantQuery bool - if model.Instant == nil { - instantQuery = false - } else { - instantQuery = *model.Instant - } - if model.Range == nil { - rangeQuery = false - } else { - rangeQuery = *model.Range - } - if !instantQuery && !rangeQuery { + if !model.Instant && !model.Range { // In older dashboards, we were not setting range query param and !range && !instant was run as range query - rangeQuery = true + model.Range = true } // We never want to run exemplar query for alerting - exemplarQuery := false - if model.Exemplar != nil { - exemplarQuery = *model.Exemplar - } if fromAlert { - exemplarQuery = false + model.Exemplar = false } return &Query{ @@ -151,9 +196,9 @@ func Parse(query backend.DataQuery, dsScrapeInterval string, intervalCalculator Start: query.TimeRange.From, End: query.TimeRange.To, RefId: query.RefID, - InstantQuery: instantQuery, - RangeQuery: rangeQuery, - ExemplarQuery: exemplarQuery, + InstantQuery: model.Instant, + RangeQuery: model.Range, + ExemplarQuery: model.Exemplar, UtcOffsetSec: model.UtcOffsetSec, }, nil } diff --git a/pkg/tsdb/prometheus/models/result.go b/pkg/tsdb/prometheus/models/result.go index ea5e5c8934..3cefc7fb3d 100644 --- a/pkg/tsdb/prometheus/models/result.go +++ b/pkg/tsdb/prometheus/models/result.go @@ -6,6 +6,7 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/data" ) +// +enum type ResultType string const ( diff --git a/pkg/tsdb/prometheus/querydata/framing_bench_test.go b/pkg/tsdb/prometheus/querydata/framing_bench_test.go index 295810ed9f..2b4cd28545 100644 --- a/pkg/tsdb/prometheus/querydata/framing_bench_test.go +++ b/pkg/tsdb/prometheus/querydata/framing_bench_test.go @@ -15,11 +15,8 @@ import ( "time" "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/kindsys" "github.com/stretchr/testify/require" - "github.com/grafana/grafana/pkg/tsdb/prometheus/kinds/dataquery" - "github.com/grafana/grafana/pkg/tsdb/prometheus/models" ) @@ -127,8 +124,8 @@ func createJsonTestData(start int64, step int64, timestampCount int, seriesCount bytes := []byte(fmt.Sprintf(`{"status":"success","data":{"resultType":"matrix","result":[%v]}}`, strings.Join(allSeries, ","))) qm := models.QueryModel{ - PrometheusDataQuery: dataquery.PrometheusDataQuery{ - Range: kindsys.Ptr(true), + PrometheusQueryProperties: models.PrometheusQueryProperties{ + Range: true, Expr: "test", }, } diff --git a/pkg/tsdb/prometheus/querydata/framing_test.go b/pkg/tsdb/prometheus/querydata/framing_test.go index 0bf8ed7284..7138ba19e9 100644 --- a/pkg/tsdb/prometheus/querydata/framing_test.go +++ b/pkg/tsdb/prometheus/querydata/framing_test.go @@ -16,8 +16,6 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/experimental" "github.com/stretchr/testify/require" - "github.com/grafana/grafana/pkg/tsdb/prometheus/kinds/dataquery" - "github.com/grafana/grafana/pkg/tsdb/prometheus/models" ) @@ -109,14 +107,16 @@ func loadStoredQuery(fileName string) (*backend.QueryDataRequest, error) { } qm := models.QueryModel{ - PrometheusDataQuery: dataquery.PrometheusDataQuery{ - Range: &sq.RangeQuery, - Exemplar: &sq.ExemplarQuery, - Expr: sq.Expr, + PrometheusQueryProperties: models.PrometheusQueryProperties{ + Range: sq.RangeQuery, + Exemplar: sq.ExemplarQuery, + Expr: sq.Expr, + LegendFormat: sq.LegendFormat, + }, + CommonQueryProperties: models.CommonQueryProperties{ + IntervalMs: sq.Step * 1000, }, - Interval: fmt.Sprintf("%ds", sq.Step), - IntervalMs: sq.Step * 1000, - LegendFormat: sq.LegendFormat, + Interval: fmt.Sprintf("%ds", sq.Step), } data, err := json.Marshal(&qm) diff --git a/pkg/tsdb/prometheus/querydata/request_test.go b/pkg/tsdb/prometheus/querydata/request_test.go index 79163a5990..cd3ba0f867 100644 --- a/pkg/tsdb/prometheus/querydata/request_test.go +++ b/pkg/tsdb/prometheus/querydata/request_test.go @@ -15,10 +15,6 @@ import ( p "github.com/prometheus/common/model" "github.com/stretchr/testify/require" - "github.com/grafana/grafana/pkg/tsdb/prometheus/kinds/dataquery" - - "github.com/grafana/kindsys" - "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" "github.com/grafana/grafana-plugin-sdk-go/backend/log" @@ -68,10 +64,10 @@ func TestPrometheus_parseTimeSeriesResponse(t *testing.T) { require.NoError(t, err) qm := models.QueryModel{ - LegendFormat: "legend {{app}}", UtcOffsetSec: 0, - PrometheusDataQuery: dataquery.PrometheusDataQuery{ - Exemplar: kindsys.Ptr(true), + PrometheusQueryProperties: models.PrometheusQueryProperties{ + LegendFormat: "legend {{app}}", + Exemplar: true, }, } b, err := json.Marshal(&qm) @@ -115,10 +111,10 @@ func TestPrometheus_parseTimeSeriesResponse(t *testing.T) { } qm := models.QueryModel{ - LegendFormat: "legend {{app}}", UtcOffsetSec: 0, - PrometheusDataQuery: dataquery.PrometheusDataQuery{ - Range: kindsys.Ptr(true), + PrometheusQueryProperties: models.PrometheusQueryProperties{ + Range: true, + LegendFormat: "legend {{app}}", }, } b, err := json.Marshal(&qm) @@ -164,10 +160,10 @@ func TestPrometheus_parseTimeSeriesResponse(t *testing.T) { } qm := models.QueryModel{ - LegendFormat: "", UtcOffsetSec: 0, - PrometheusDataQuery: dataquery.PrometheusDataQuery{ - Range: kindsys.Ptr(true), + PrometheusQueryProperties: models.PrometheusQueryProperties{ + Range: true, + LegendFormat: "", }, } b, err := json.Marshal(&qm) @@ -209,10 +205,10 @@ func TestPrometheus_parseTimeSeriesResponse(t *testing.T) { } qm := models.QueryModel{ - LegendFormat: "", UtcOffsetSec: 0, - PrometheusDataQuery: dataquery.PrometheusDataQuery{ - Range: kindsys.Ptr(true), + PrometheusQueryProperties: models.PrometheusQueryProperties{ + Range: true, + LegendFormat: "", }, } b, err := json.Marshal(&qm) @@ -252,10 +248,10 @@ func TestPrometheus_parseTimeSeriesResponse(t *testing.T) { } qm := models.QueryModel{ - LegendFormat: "", UtcOffsetSec: 0, - PrometheusDataQuery: dataquery.PrometheusDataQuery{ - Range: kindsys.Ptr(true), + PrometheusQueryProperties: models.PrometheusQueryProperties{ + Range: true, + LegendFormat: "", }, } b, err := json.Marshal(&qm) @@ -289,10 +285,10 @@ func TestPrometheus_parseTimeSeriesResponse(t *testing.T) { }, } qm := models.QueryModel{ - LegendFormat: "legend {{app}}", UtcOffsetSec: 0, - PrometheusDataQuery: dataquery.PrometheusDataQuery{ - Instant: kindsys.Ptr(true), + PrometheusQueryProperties: models.PrometheusQueryProperties{ + Instant: true, + LegendFormat: "legend {{app}}", }, } b, err := json.Marshal(&qm) @@ -330,10 +326,10 @@ func TestPrometheus_parseTimeSeriesResponse(t *testing.T) { }, } qm := models.QueryModel{ - LegendFormat: "", UtcOffsetSec: 0, - PrometheusDataQuery: dataquery.PrometheusDataQuery{ - Instant: kindsys.Ptr(true), + PrometheusQueryProperties: models.PrometheusQueryProperties{ + Instant: true, + LegendFormat: "", }, } b, err := json.Marshal(&qm) diff --git a/public/app/plugins/datasource/prometheus/dataquery.cue b/public/app/plugins/datasource/prometheus/dataquery.cue deleted file mode 100644 index 177af1929c..0000000000 --- a/public/app/plugins/datasource/prometheus/dataquery.cue +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright 2023 Grafana Labs -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package grafanaplugin - -import ( - common "github.com/grafana/grafana/packages/grafana-schema/src/common" -) - -composableKinds: DataQuery: { - maturity: "experimental" - - lineage: { - schemas: [{ - version: [0, 0] - schema: { - common.DataQuery - - // The actual expression/query that will be evaluated by Prometheus - expr: string - // Returns only the latest value that Prometheus has scraped for the requested time series - instant?: bool - // Returns a Range vector, comprised of a set of time series containing a range of data points over time for each time series - range?: bool - // Execute an additional query to identify interesting raw samples relevant for the given expr - exemplar?: bool - // Specifies which editor is being used to prepare the query. It can be "code" or "builder" - editorMode?: #QueryEditorMode - // Query format to determine how to display data points in panel. It can be "time_series", "table", "heatmap" - format?: #PromQueryFormat - // Series name override or template. Ex. {{hostname}} will be replaced with label value for hostname - legendFormat?: string - // @deprecated Used to specify how many times to divide max data points by. We use max data points under query options - // See https://github.com/grafana/grafana/issues/48081 - intervalFactor?: number - - scope?: { - matchers: string - } - - #QueryEditorMode: "code" | "builder" @cuetsy(kind="enum") - #PromQueryFormat: "time_series" | "table" | "heatmap" @cuetsy(kind="type") - } - }] - lenses: [] - } -} diff --git a/packages/grafana-prometheus/src/dataquery.gen.ts b/public/app/plugins/datasource/prometheus/dataquery.ts similarity index 82% rename from packages/grafana-prometheus/src/dataquery.gen.ts rename to public/app/plugins/datasource/prometheus/dataquery.ts index 3ba622cecf..8609fc7891 100644 --- a/packages/grafana-prometheus/src/dataquery.gen.ts +++ b/public/app/plugins/datasource/prometheus/dataquery.ts @@ -1,13 +1,3 @@ -// Code generated - EDITING IS FUTILE. DO NOT EDIT. -// -// Generated by: -// public/app/plugins/gen.go -// Using jennies: -// TSTypesJenny -// PluginTSTypesJenny -// -// Run 'make gen-cue' from repository root to regenerate. - import * as common from '@grafana/schema'; export enum QueryEditorMode { @@ -15,7 +5,7 @@ export enum QueryEditorMode { Code = 'code', } -export type PromQueryFormat = ('time_series' | 'table' | 'heatmap'); +export type PromQueryFormat = 'time_series' | 'table' | 'heatmap'; export interface Prometheus extends common.DataQuery { /** diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderOptions.tsx b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderOptions.tsx index 27beaafb4d..c6fd8395c8 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderOptions.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderOptions.tsx @@ -6,7 +6,7 @@ import { EditorField, EditorRow, EditorSwitch } from '@grafana/experimental'; import { AutoSizeInput, RadioButtonGroup, Select } from '@grafana/ui'; import { getQueryTypeChangeHandler, getQueryTypeOptions } from '../../components/PromExploreExtraField'; -import { PromQueryFormat } from '../../dataquery.gen'; +import { PromQueryFormat } from '../../dataquery'; import { PromQuery } from '../../types'; import { QueryOptionGroup } from '../shared/QueryOptionGroup'; diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryEditorSelector.tsx b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryEditorSelector.tsx index a0016b986e..f85090e0ca 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryEditorSelector.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryEditorSelector.tsx @@ -8,7 +8,7 @@ import { reportInteraction } from '@grafana/runtime'; import { Button, ConfirmModal, Space } from '@grafana/ui'; import { PromQueryEditorProps } from '../../components/types'; -import { PromQueryFormat } from '../../dataquery.gen'; +import { PromQueryFormat } from '../../dataquery'; import { PromQuery } from '../../types'; import { QueryPatternsModal } from '../QueryPatternsModal'; import { promQueryEditorExplainKey, useFlag } from '../hooks/useFlag'; diff --git a/public/app/plugins/datasource/prometheus/types.ts b/public/app/plugins/datasource/prometheus/types.ts index 5ddb69d811..f3c5730b38 100644 --- a/public/app/plugins/datasource/prometheus/types.ts +++ b/public/app/plugins/datasource/prometheus/types.ts @@ -1,7 +1,7 @@ import { DataSourceJsonData } from '@grafana/data'; import { DataQuery } from '@grafana/schema'; -import { Prometheus as GenPromQuery } from './dataquery.gen'; +import { Prometheus as GenPromQuery } from './dataquery'; import { QueryBuilderLabelFilter, QueryEditorMode } from './querybuilder/shared/types'; export interface PromQuery extends GenPromQuery, DataQuery { From f1000978cba40d75f0172b66a7263416a82b86e0 Mon Sep 17 00:00:00 2001 From: Galen Kistler <109082771+gtk-grafana@users.noreply.github.com> Date: Mon, 4 Mar 2024 13:01:48 -0600 Subject: [PATCH 0375/1406] Logs: e2e test flake in loki-table-explore-to-dash.spec.ts (#83856) fix flake in e2e test checking monaco loading state --- .../loki-table-explore-to-dash.spec.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/e2e/various-suite/loki-table-explore-to-dash.spec.ts b/e2e/various-suite/loki-table-explore-to-dash.spec.ts index 5a6bb85dad..190a5ea5a1 100644 --- a/e2e/various-suite/loki-table-explore-to-dash.spec.ts +++ b/e2e/various-suite/loki-table-explore-to-dash.spec.ts @@ -147,10 +147,18 @@ describe('Loki Query Editor', () => { cy.contains('Code').click({ force: true }); // Wait for lazy loading - const monacoLoadingText = 'Loading...'; - - e2e.components.QueryField.container().should('be.visible').should('have.text', monacoLoadingText); - e2e.components.QueryField.container().should('be.visible').should('not.have.text', monacoLoadingText); + // const monacoLoadingText = 'Loading...'; + + // e2e.components.QueryField.container().should('be.visible').should('have.text', monacoLoadingText); + e2e.components.QueryField.container() + .find('.view-overlays[role="presentation"]') + .get('.cdr') + .then(($el) => { + const win = $el[0].ownerDocument.defaultView; + const after = win.getComputedStyle($el[0], '::after'); + const content = after.getPropertyValue('content'); + expect(content).to.eq('"Enter a Loki query (run with Shift+Enter)"'); + }); // Write a simple query e2e.components.QueryField.container().type('query').type('{instance="instance1"'); From 52de1a9a33e90358359d541d3cf1c4658f2d5569 Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Mon, 4 Mar 2024 19:18:35 +0000 Subject: [PATCH 0376/1406] Change codeowners of library panels to dashboards squad (#83862) * Change codeowners of library panels to dashboards squad * Update CODEOWNERS --- .github/CODEOWNERS | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 3f107cb0bb..1850e80848 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -288,8 +288,8 @@ /public/app/features/alerting/ @grafana/alerting-frontend # Library Services -/pkg/services/libraryelements/ @grafana/grafana-frontend-platform -/pkg/services/librarypanels/ @grafana/grafana-frontend-platform +/pkg/services/libraryelements/ @grafana/dashboards-squad +/pkg/services/librarypanels/ @grafana/dashboards-squad # Plugins /pkg/api/pluginproxy/ @grafana/plugins-platform-backend @@ -410,7 +410,7 @@ playwright.config.ts @grafana/plugins-platform-frontend /public/app/features/folders/ @grafana/grafana-frontend-platform /public/app/features/inspector/ @grafana/dashboards-squad /public/app/features/invites/ @grafana/grafana-frontend-platform -/public/app/features/library-panels/ @grafana/grafana-frontend-platform +/public/app/features/library-panels/ @grafana/dashboards-squad /public/app/features/logs/ @grafana/observability-logs /public/app/features/live/ @grafana/grafana-app-platform-squad /public/app/features/manage-dashboards/ @grafana/dashboards-squad From 13f037d617a7b2304c78757afbd0a8869645d7df Mon Sep 17 00:00:00 2001 From: Kyle Cunningham Date: Mon, 4 Mar 2024 14:17:20 -0600 Subject: [PATCH 0377/1406] Table Panel: Fix condition for showing footer options (#83801) * Fix condition for showing footer options * codeincarnate/table-footer-config/ lint --------- Co-authored-by: jev forsberg --- public/app/plugins/panel/table/module.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/public/app/plugins/panel/table/module.tsx b/public/app/plugins/panel/table/module.tsx index 5b626dc69f..f930e45de1 100644 --- a/public/app/plugins/panel/table/module.tsx +++ b/public/app/plugins/panel/table/module.tsx @@ -169,9 +169,7 @@ export const plugin = new PanelPlugin(TablePanel) }, }, defaultValue: '', - showIf: (cfg) => - (cfg.footer?.show && !cfg.footer?.countRows) || - (cfg.footer?.reducer?.length === 1 && cfg.footer?.reducer[0] !== ReducerID.count), + showIf: (cfg) => cfg.footer?.show && !cfg.footer?.countRows, }) .addCustomEditor({ id: 'footer.enablePagination', From 3121fce30565af2d84a28527147ecbc351005bc0 Mon Sep 17 00:00:00 2001 From: Kyle Cunningham Date: Mon, 4 Mar 2024 14:57:47 -0600 Subject: [PATCH 0378/1406] Timeseries to table transformation: Improve Documentation (#83647) * Flesh out timeseries to table docs * Add generated documentation * Update docs * Fix spelling * Fix link formatting Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> * Update language --------- Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> --- .../transform-data/index.md | 8 ++++++-- .../visualizations/table/index.md | 16 ++++++++++++---- .../app/features/transformers/docs/content.ts | 19 ++++++++++++++++--- 3 files changed, 34 insertions(+), 9 deletions(-) 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 e23162e277..4ec1012b6e 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 @@ -1263,9 +1263,13 @@ This transformation allows you to manipulate and analyze geospatial data, enabli ### Time series to table transform -Use this transformation to convert time series results into a table, transforming a time series data frame into a **Trend** field. The **Trend** field can then be rendered using the [sparkline cell type][], generating 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. +Use this transformation to convert time series results into a table, transforming a time series data frame into a **Trend** field which can then be used with the [sparkline cell type][]. 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. -For each generated **Trend** field value, a calculation function can be selected. The default is **Last non-null value**. This value is displayed next to the sparkline and used for sorting table rows. +{{< figure src="/static/img/docs/transformations/table-sparklines.png" class="docs-image--no-shadow" max-width= "1100px" alt="A table panel showing multiple values and their corresponding sparklines." >}} + +For each generated **Trend** field value, a calculation function can be selected. This value is displayed next to the sparkline and will be used for sorting table rows. + +{{< figure src="/static/img/docs/transformations/timeseries-table-select-stat.png" class="docs-image--no-shadow" max-width= "1100px" alt="A select box showing available statistics that can be calculated." >}} > **Note:** This transformation is available in Grafana 9.5+ as an opt-in beta feature. Modify the Grafana [configuration file][] to use it. diff --git a/docs/sources/panels-visualizations/visualizations/table/index.md b/docs/sources/panels-visualizations/visualizations/table/index.md index 8c0c66cb48..f7a7a81923 100644 --- a/docs/sources/panels-visualizations/visualizations/table/index.md +++ b/docs/sources/panels-visualizations/visualizations/table/index.md @@ -27,7 +27,7 @@ weight: 100 # Table -Tables are very flexible, supporting multiple modes for time series and for tables, annotation, and raw JSON data. This visualization also provides date formatting, value formatting, and coloring options. +Tables are very flexible, supporting multiple modes for time series and for tables, annotation, and raw JSON data. This visualization also provides date formatting, value formatting, and coloring options. In addition to formatting and coloring options, Grafana also provides a variety of _Cell types_ which you can use to display gauges, sparklines, and other rich data displays. {{< figure src="/static/img/docs/tables/table_visualization.png" max-width="1200px" lightbox="true" caption="Table visualization" >}} @@ -148,10 +148,12 @@ If you have a field value that is an image URL or a base64 encoded image you can ### Sparkline -Shows value rendered as a sparkline. Requires [time series to table][] data transform. +Shows values rendered as a sparkline. You can show sparklines using the [Time series to table transformation][] on data with multiple time series to process it into a format the table can show. {{< figure src="/static/img/docs/tables/sparkline2.png" max-width="500px" caption="Sparkline" class="docs-image--no-shadow" >}} +You can be customize sparklines with many of the same options as the [Time series panel][] including line width, fill opacity, and more. You can also change the color of the sparkline by updating the [color scheme][] in the _Standard options_ section of the panel configuration. + ## Cell value inspect Enables value inspection from table cell. The raw value is presented in a modal window. @@ -226,8 +228,14 @@ If you want to show the number of rows in the dataset instead of the number of v [calculations]: "/docs/grafana/ -> /docs/grafana//panels-visualizations/query-transform-data/calculation-types" [calculations]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/panels-visualizations/query-transform-data/calculation-types" -[time series to table]: "/docs/grafana/ -> /docs/grafana//panels-visualizations/query-transform-data/transform-data#time-series-to-table-transform" -[time series to table]: "/docs/grafana-cloud/ -> /docs/grafana//panels-visualizations/query-transform-data/transform-data#time-series-to-table-transform" +[Time series to table transformation]: "/docs/grafana/ -> /docs/grafana//panels-visualizations/query-transform-data/transform-data#time-series-to-table-transform" +[Time series to table transformation]: "/docs/grafana-cloud/ -> /docs/grafana//panels-visualizations/query-transform-data/transform-data#time-series-to-table-transform" + +[Time series panel]: "/docs/grafana/ -> /docs/grafana//panels-visualizations/visualizations/time-series" +[Time series panel]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/panels-visualizations/visualizations/time-series" + +[color scheme]: "/docs/grafana/ -> /docs/grafana//panels-visualizations/configure-standard-options#color-scheme" +[color scheme]: "/docs/grafana-cloud -> /docs/grafana-cloud/visualizations/panels-visualizations/configure-standard-options/#color-scheme" [configuration file]: "/docs/grafana/ -> /docs/grafana//setup-grafana/configure-grafana#configuration-file-location" [configuration file]: "/docs/grafana-cloud/ -> /docs/grafana//setup-grafana/configure-grafana#configuration-file-location" diff --git a/public/app/features/transformers/docs/content.ts b/public/app/features/transformers/docs/content.ts index f83e8cef41..d75462a3d8 100644 --- a/public/app/features/transformers/docs/content.ts +++ b/public/app/features/transformers/docs/content.ts @@ -1355,11 +1355,24 @@ This transformation allows you to manipulate and analyze geospatial data, enabli }, timeSeriesTable: { name: 'Time series to table transform', - getHelperDocs: function () { + getHelperDocs: function (imageRenderType: ImageRenderType = ImageRenderType.ShortcodeFigure) { return ` -Use this transformation to convert time series results into a table, transforming a time series data frame into a **Trend** field. The **Trend** field can then be rendered using the [sparkline cell type][], generating 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. +Use this transformation to convert time series results into a table, transforming a time series data frame into a **Trend** field which can then be used with the [sparkline cell type][]. 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. + +${buildImageContent( + '/static/img/docs/transformations/table-sparklines.png', + imageRenderType, + 'A table panel showing multiple values and their corresponding sparklines.' +)} + +For each generated **Trend** field value, a calculation function can be selected. This value is displayed next to the sparkline and will be used for sorting table rows. + +${buildImageContent( + '/static/img/docs/transformations/timeseries-table-select-stat.png', + imageRenderType, + 'A select box showing available statistics that can be calculated.' +)} -For each generated **Trend** field value, a calculation function can be selected. The default is **Last non-null value**. This value is displayed next to the sparkline and used for sorting table rows. > **Note:** This transformation is available in Grafana 9.5+ as an opt-in beta feature. Modify the Grafana [configuration file][] to use it. `; From f2a9d0a89db40cc1b414cc3ee3c17575ea2401d6 Mon Sep 17 00:00:00 2001 From: Alexander Weaver Date: Mon, 4 Mar 2024 15:15:01 -0600 Subject: [PATCH 0379/1406] Alerting: Refactor ruleRoutine to take an entire ruleInfo instance (#83858) * Make stop a real method * ruleRoutine takes a ruleInfo reference directly rather than pieces of it * Fix whitespace --- pkg/services/ngalert/schedule/alert_rule.go | 14 +++- pkg/services/ngalert/schedule/schedule.go | 10 +-- .../ngalert/schedule/schedule_unit_test.go | 68 +++++++++---------- 3 files changed, 51 insertions(+), 41 deletions(-) diff --git a/pkg/services/ngalert/schedule/alert_rule.go b/pkg/services/ngalert/schedule/alert_rule.go index 9a369cef36..7555f77d84 100644 --- a/pkg/services/ngalert/schedule/alert_rule.go +++ b/pkg/services/ngalert/schedule/alert_rule.go @@ -10,12 +10,17 @@ type alertRuleInfo struct { evalCh chan *evaluation updateCh chan ruleVersionAndPauseStatus ctx context.Context - stop func(reason error) + stopFn util.CancelCauseFunc } func newAlertRuleInfo(parent context.Context) *alertRuleInfo { ctx, stop := util.WithCancelCause(parent) - return &alertRuleInfo{evalCh: make(chan *evaluation), updateCh: make(chan ruleVersionAndPauseStatus), ctx: ctx, stop: stop} + return &alertRuleInfo{ + evalCh: make(chan *evaluation), + updateCh: make(chan ruleVersionAndPauseStatus), + ctx: ctx, + stopFn: stop, + } } // eval signals the rule evaluation routine to perform the evaluation of the rule. Does nothing if the loop is stopped. @@ -58,3 +63,8 @@ func (a *alertRuleInfo) update(lastVersion ruleVersionAndPauseStatus) bool { return false } } + +// stop sends an instruction to the rule evaluation routine to shut down. an optional shutdown reason can be given. +func (a *alertRuleInfo) stop(reason error) { + a.stopFn(reason) +} diff --git a/pkg/services/ngalert/schedule/schedule.go b/pkg/services/ngalert/schedule/schedule.go index 500bd278d6..b08383db95 100644 --- a/pkg/services/ngalert/schedule/schedule.go +++ b/pkg/services/ngalert/schedule/schedule.go @@ -256,7 +256,7 @@ func (sch *schedule) processTick(ctx context.Context, dispatcherGroup *errgroup. if newRoutine && !invalidInterval { dispatcherGroup.Go(func() error { - return sch.ruleRoutine(ruleInfo.ctx, key, ruleInfo.evalCh, ruleInfo.updateCh) + return sch.ruleRoutine(key, ruleInfo) }) } @@ -345,8 +345,8 @@ func (sch *schedule) processTick(ctx context.Context, dispatcherGroup *errgroup. } //nolint:gocyclo -func (sch *schedule) ruleRoutine(grafanaCtx context.Context, key ngmodels.AlertRuleKey, evalCh <-chan *evaluation, updateCh <-chan ruleVersionAndPauseStatus) error { - grafanaCtx = ngmodels.WithRuleKey(grafanaCtx, key) +func (sch *schedule) ruleRoutine(key ngmodels.AlertRuleKey, ruleInfo *alertRuleInfo) error { + grafanaCtx := ngmodels.WithRuleKey(ruleInfo.ctx, key) logger := sch.log.FromContext(grafanaCtx) logger.Debug("Alert rule routine started") @@ -474,7 +474,7 @@ func (sch *schedule) ruleRoutine(grafanaCtx context.Context, key ngmodels.AlertR for { select { // used by external services (API) to notify that rule is updated. - case ctx := <-updateCh: + case ctx := <-ruleInfo.updateCh: if currentFingerprint == ctx.Fingerprint { logger.Info("Rule's fingerprint has not changed. Skip resetting the state", "currentFingerprint", currentFingerprint) continue @@ -485,7 +485,7 @@ func (sch *schedule) ruleRoutine(grafanaCtx context.Context, key ngmodels.AlertR resetState(grafanaCtx, ctx.IsPaused) currentFingerprint = ctx.Fingerprint // evalCh - used by the scheduler to signal that evaluation is needed. - case ctx, ok := <-evalCh: + case ctx, ok := <-ruleInfo.evalCh: if !ok { logger.Debug("Evaluation channel has been closed. Exiting") return nil diff --git a/pkg/services/ngalert/schedule/schedule_unit_test.go b/pkg/services/ngalert/schedule/schedule_unit_test.go index 3ffaef85e6..1c40e798c1 100644 --- a/pkg/services/ngalert/schedule/schedule_unit_test.go +++ b/pkg/services/ngalert/schedule/schedule_unit_test.go @@ -384,22 +384,22 @@ func TestSchedule_ruleRoutine(t *testing.T) { for _, evalState := range normalStates { // TODO rewrite when we are able to mock/fake state manager t.Run(fmt.Sprintf("when rule evaluation happens (evaluation state %s)", evalState), func(t *testing.T) { - evalChan := make(chan *evaluation) evalAppliedChan := make(chan time.Time) sch, ruleStore, instanceStore, reg := createSchedule(evalAppliedChan, nil) rule := models.AlertRuleGen(withQueryForState(t, evalState))() ruleStore.PutRule(context.Background(), rule) folderTitle := ruleStore.getNamespaceTitle(rule.NamespaceUID) + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + ruleInfo := newAlertRuleInfo(ctx) go func() { - ctx, cancel := context.WithCancel(context.Background()) - t.Cleanup(cancel) - _ = sch.ruleRoutine(ctx, rule.GetKey(), evalChan, make(chan ruleVersionAndPauseStatus)) + _ = sch.ruleRoutine(rule.GetKey(), ruleInfo) }() expectedTime := time.UnixMicro(rand.Int63()) - evalChan <- &evaluation{ + ruleInfo.evalCh <- &evaluation{ scheduledAt: expectedTime, rule: rule, folderTitle: folderTitle, @@ -540,8 +540,9 @@ func TestSchedule_ruleRoutine(t *testing.T) { require.NotEmpty(t, expectedStates) ctx, cancel := context.WithCancel(context.Background()) + ruleInfo := newAlertRuleInfo(ctx) go func() { - err := sch.ruleRoutine(ctx, models.AlertRuleKey{}, make(chan *evaluation), make(chan ruleVersionAndPauseStatus)) + err := sch.ruleRoutine(models.AlertRuleKey{}, ruleInfo) stoppedChan <- err }() @@ -550,7 +551,7 @@ func TestSchedule_ruleRoutine(t *testing.T) { require.NoError(t, err) require.Equal(t, len(expectedStates), len(sch.stateManager.GetStatesForRuleUID(rule.OrgID, rule.UID))) }) - t.Run("and clean up the state if delete is cancellation reason ", func(t *testing.T) { + t.Run("and clean up the state if delete is cancellation reason for inner context", func(t *testing.T) { stoppedChan := make(chan error) sch, _, _, _ := createSchedule(make(chan time.Time), nil) @@ -558,13 +559,13 @@ func TestSchedule_ruleRoutine(t *testing.T) { _ = sch.stateManager.ProcessEvalResults(context.Background(), sch.clock.Now(), rule, eval.GenerateResults(rand.Intn(5)+1, eval.ResultGen(eval.WithEvaluatedAt(sch.clock.Now()))), nil) require.NotEmpty(t, sch.stateManager.GetStatesForRuleUID(rule.OrgID, rule.UID)) - ctx, cancel := util.WithCancelCause(context.Background()) + ruleInfo := newAlertRuleInfo(context.Background()) go func() { - err := sch.ruleRoutine(ctx, rule.GetKey(), make(chan *evaluation), make(chan ruleVersionAndPauseStatus)) + err := sch.ruleRoutine(rule.GetKey(), ruleInfo) stoppedChan <- err }() - cancel(errRuleDeleted) + ruleInfo.stop(errRuleDeleted) err := waitForErrChannel(t, stoppedChan) require.NoError(t, err) @@ -577,9 +578,7 @@ func TestSchedule_ruleRoutine(t *testing.T) { folderTitle := "folderName" ruleFp := ruleWithFolder{rule, folderTitle}.Fingerprint() - evalChan := make(chan *evaluation) evalAppliedChan := make(chan time.Time) - updateChan := make(chan ruleVersionAndPauseStatus) sender := NewSyncAlertsSenderMock() sender.EXPECT().Send(mock.Anything, rule.GetKey(), mock.Anything).Return() @@ -587,15 +586,16 @@ func TestSchedule_ruleRoutine(t *testing.T) { sch, ruleStore, _, _ := createSchedule(evalAppliedChan, sender) ruleStore.PutRule(context.Background(), rule) sch.schedulableAlertRules.set([]*models.AlertRule{rule}, map[models.FolderKey]string{rule.GetFolderKey(): folderTitle}) + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + ruleInfo := newAlertRuleInfo(ctx) go func() { - ctx, cancel := context.WithCancel(context.Background()) - t.Cleanup(cancel) - _ = sch.ruleRoutine(ctx, rule.GetKey(), evalChan, updateChan) + _ = sch.ruleRoutine(rule.GetKey(), ruleInfo) }() // init evaluation loop so it got the rule version - evalChan <- &evaluation{ + ruleInfo.evalCh <- &evaluation{ scheduledAt: sch.clock.Now(), rule: rule, folderTitle: folderTitle, @@ -631,8 +631,8 @@ func TestSchedule_ruleRoutine(t *testing.T) { require.Greaterf(t, expectedToBeSent, 0, "State manager was expected to return at least one state that can be expired") t.Run("should do nothing if version in channel is the same", func(t *testing.T) { - updateChan <- ruleVersionAndPauseStatus{ruleFp, false} - updateChan <- ruleVersionAndPauseStatus{ruleFp, false} // second time just to make sure that previous messages were handled + ruleInfo.updateCh <- ruleVersionAndPauseStatus{ruleFp, false} + ruleInfo.updateCh <- ruleVersionAndPauseStatus{ruleFp, false} // second time just to make sure that previous messages were handled actualStates := sch.stateManager.GetStatesForRuleUID(rule.OrgID, rule.UID) require.Len(t, actualStates, len(states)) @@ -641,7 +641,7 @@ func TestSchedule_ruleRoutine(t *testing.T) { }) t.Run("should clear the state and expire firing alerts if version in channel is greater", func(t *testing.T) { - updateChan <- ruleVersionAndPauseStatus{ruleFp + 1, false} + ruleInfo.updateCh <- ruleVersionAndPauseStatus{ruleFp + 1, false} require.Eventually(t, func() bool { return len(sender.Calls()) > 0 @@ -659,7 +659,6 @@ func TestSchedule_ruleRoutine(t *testing.T) { rule := models.AlertRuleGen(withQueryForState(t, eval.Error))() rule.ExecErrState = models.ErrorErrState - evalChan := make(chan *evaluation) evalAppliedChan := make(chan time.Time) sender := NewSyncAlertsSenderMock() @@ -668,14 +667,15 @@ func TestSchedule_ruleRoutine(t *testing.T) { sch, ruleStore, _, reg := createSchedule(evalAppliedChan, sender) sch.maxAttempts = 3 ruleStore.PutRule(context.Background(), rule) + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + ruleInfo := newAlertRuleInfo(ctx) go func() { - ctx, cancel := context.WithCancel(context.Background()) - t.Cleanup(cancel) - _ = sch.ruleRoutine(ctx, rule.GetKey(), evalChan, make(chan ruleVersionAndPauseStatus)) + _ = sch.ruleRoutine(rule.GetKey(), ruleInfo) }() - evalChan <- &evaluation{ + ruleInfo.evalCh <- &evaluation{ scheduledAt: sch.clock.Now(), rule: rule, } @@ -765,7 +765,6 @@ func TestSchedule_ruleRoutine(t *testing.T) { // eval.Alerting makes state manager to create notifications for alertmanagers rule := models.AlertRuleGen(withQueryForState(t, eval.Alerting))() - evalChan := make(chan *evaluation) evalAppliedChan := make(chan time.Time) sender := NewSyncAlertsSenderMock() @@ -773,14 +772,15 @@ func TestSchedule_ruleRoutine(t *testing.T) { sch, ruleStore, _, _ := createSchedule(evalAppliedChan, sender) ruleStore.PutRule(context.Background(), rule) + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + ruleInfo := newAlertRuleInfo(ctx) go func() { - ctx, cancel := context.WithCancel(context.Background()) - t.Cleanup(cancel) - _ = sch.ruleRoutine(ctx, rule.GetKey(), evalChan, make(chan ruleVersionAndPauseStatus)) + _ = sch.ruleRoutine(rule.GetKey(), ruleInfo) }() - evalChan <- &evaluation{ + ruleInfo.evalCh <- &evaluation{ scheduledAt: sch.clock.Now(), rule: rule, } @@ -798,7 +798,6 @@ func TestSchedule_ruleRoutine(t *testing.T) { t.Run("when there are no alerts to send it should not call notifiers", func(t *testing.T) { rule := models.AlertRuleGen(withQueryForState(t, eval.Normal))() - evalChan := make(chan *evaluation) evalAppliedChan := make(chan time.Time) sender := NewSyncAlertsSenderMock() @@ -806,14 +805,15 @@ func TestSchedule_ruleRoutine(t *testing.T) { sch, ruleStore, _, _ := createSchedule(evalAppliedChan, sender) ruleStore.PutRule(context.Background(), rule) + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + ruleInfo := newAlertRuleInfo(ctx) go func() { - ctx, cancel := context.WithCancel(context.Background()) - t.Cleanup(cancel) - _ = sch.ruleRoutine(ctx, rule.GetKey(), evalChan, make(chan ruleVersionAndPauseStatus)) + _ = sch.ruleRoutine(rule.GetKey(), ruleInfo) }() - evalChan <- &evaluation{ + ruleInfo.evalCh <- &evaluation{ scheduledAt: sch.clock.Now(), rule: rule, } From c88accdf99d3cc4b04302e055e84bac97194df4b Mon Sep 17 00:00:00 2001 From: Kyle Cunningham Date: Mon, 4 Mar 2024 15:31:00 -0600 Subject: [PATCH 0380/1406] Transformations: Docs for Group to nested table (#83559) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Start group to nested table docs * Group to nested table docs * Remove dead content * Update docs content * codeincarnate/nested-table-docs/ run formatter * Update punctuation (thanks Isabel 🙏) Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> * Update wording and structure ((thanks Isabel 🙏) Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> * More wording and structure changes (thanks Isabel 🙏) Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> * Escape backticks * Add imagery * Add generated docs * Language updates Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> * Language updates Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> * Language updates Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> * Update formatting * Rebuild transformation docs --------- Co-authored-by: jev forsberg Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> --- .../transform-data/index.md | 44 +++++++++++++ .../app/features/transformers/docs/content.ts | 62 +++++++++++++++++++ 2 files changed, 106 insertions(+) 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 4ec1012b6e..68a6861b94 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 @@ -629,6 +629,50 @@ We can generate a matrix using the values of 'Server Status' as column names, th Use this transformation to construct a matrix by specifying fields from your query results. The matrix output reflects the relationships between the unique values in these fields. This helps you present complex relationships in a clear and structured matrix format. +### Group to nested table + +Use this transformation to group the data by a specified field (column) value and process calculations on each group. Records are generated that share the same grouped field value, to be displayed in a nested table. + +To calculate a statistic for a field, click the selection box next to it and select the **Calculate** option: + +{{< figure src="/static/img/docs/transformations/nested-table-select-calculation.png" class="docs-image--no-shadow" max-width= "1100px" alt="A select box showing the Group and Calculate options for the transformation." >}} + +Once **Calculate** has been selected, another selection box will appear next to the respective field which will allow statistics to be selected: + +{{< figure src="/static/img/docs/transformations/nested-table-select-stat.png" class="docs-image--no-shadow" max-width= "1100px" alt="A select box showing available statistic calculations once the calculate option for the field has been selected." >}} + +For information about available calculations, refer to [Calculation types][]. + +Here's an example of original data: + +| Time | Server ID | CPU Temperature | Server Status | +| ------------------- | --------- | --------------- | ------------- | +| 2020-07-07 11:34:20 | server 1 | 80 | Shutdown | +| 2020-07-07 11:34:20 | server 3 | 62 | OK | +| 2020-07-07 10:32:20 | server 2 | 90 | Overload | +| 2020-07-07 10:31:22 | server 3 | 55 | OK | +| 2020-07-07 09:30:57 | server 3 | 62 | Rebooting | +| 2020-07-07 09:30:05 | server 2 | 88 | OK | +| 2020-07-07 09:28:06 | server 1 | 80 | OK | +| 2020-07-07 09:25:05 | server 2 | 88 | OK | +| 2020-07-07 09:23:07 | server 1 | 86 | OK | + +This transformation has two steps. First, specify one or more fields by which to group the data. This groups all the same values of those fields together, as if you sorted them. For instance, if you group by the Server ID field, Grafana groups the data this way: + +| Server ID | | +| --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| server 1 |
TimeCPU TemperatureServer Status
2020-07-07 11:34:2080Shutdown
2020-07-07 09:28:0680OK
2020-07-07 09:23:0786OK
| +| server 2 |
TimeCPU TemperatureServer Status
2020-07-07 10:32:2090Overload
2020-07-07 09:30:0588OK
2020-07-07 09:25:0588OK
| +| server 3 |
TimeCPU TemperatureServer Status
2020-07-07 11:34:2062OK
2020-07-07 10:31:2255OK
2020-07-07 09:30:5762Rebooting
| + +After choosing the field by which you want to group your data, you can add various calculations on the other fields and apply the calculation to each group of rows. For instance, you might want to calculate the average CPU temperature for each of those servers. To do so, add the **mean calculation** applied on the CPU Temperature field to get the following result: + +| Server ID | CPU Temperatute (mean) | | +| --------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| server 1 | 82 |
TimeServer Status
2020-07-07 11:34:20Shutdown
2020-07-07 09:28:06OK
2020-07-07 09:23:07OK
| +| server 2 | 88.6 |
TimeServer Status
2020-07-07 10:32:20Overload
2020-07-07 09:30:05OK
2020-07-07 09:25:05OK
| +| server 3 | 59.6 |
TimeServer Status
2020-07-07 11:34:20OK
2020-07-07 10:31:22OK
2020-07-07 09:30:57Rebooting
| + ### Create heatmap Use this transformation to prepare histogram data for visualizing trends over time. Similar to the heatmap visualization, this transformation converts histogram metrics into temporal buckets. diff --git a/public/app/features/transformers/docs/content.ts b/public/app/features/transformers/docs/content.ts index d75462a3d8..6adb4fefe6 100644 --- a/public/app/features/transformers/docs/content.ts +++ b/public/app/features/transformers/docs/content.ts @@ -628,6 +628,68 @@ Use this transformation to construct a matrix by specifying fields from your que `; }, }, + groupToNestedTable: { + name: 'Group to nested table', + getHelperDocs: function (imageRenderType: ImageRenderType = ImageRenderType.ShortcodeFigure) { + return ` + Use this transformation to group the data by a specified field (column) value and process calculations on each group. Records are generated that share the same grouped field value, to be displayed in a nested table. + + To calculate a statistic for a field, click the selection box next to it and select the **Calculate** option: + + ${buildImageContent( + '/static/img/docs/transformations/nested-table-select-calculation.png', + imageRenderType, + 'A select box showing the Group and Calculate options for the transformation.' + )} + + Once **Calculate** has been selected, another selection box will appear next to the respective field which will allow statistics to be selected: + + ${buildImageContent( + '/static/img/docs/transformations/nested-table-select-stat.png', + imageRenderType, + 'A select box showing available statistic calculations once the calculate option for the field has been selected.' + )} + + For information about available calculations, refer to [Calculation types][]. + + Here's an example of original data: + + | Time | Server ID | CPU Temperature | Server Status | + | ------------------- | --------- | --------------- | ------------- | + | 2020-07-07 11:34:20 | server 1 | 80 | Shutdown | + | 2020-07-07 11:34:20 | server 3 | 62 | OK | + | 2020-07-07 10:32:20 | server 2 | 90 | Overload | + | 2020-07-07 10:31:22 | server 3 | 55 | OK | + | 2020-07-07 09:30:57 | server 3 | 62 | Rebooting | + | 2020-07-07 09:30:05 | server 2 | 88 | OK | + | 2020-07-07 09:28:06 | server 1 | 80 | OK | + | 2020-07-07 09:25:05 | server 2 | 88 | OK | + | 2020-07-07 09:23:07 | server 1 | 86 | OK | + + This transformation has two steps. First, specify one or more fields by which to group the data. This groups all the same values of those fields together, as if you sorted them. For instance, if you group by the Server ID field, Grafana groups the data this way: + + | Server ID | | + | -------------- | ------------- | + | server 1 |
TimeCPU TemperatureServer Status
2020-07-07 11:34:2080Shutdown
2020-07-07 09:28:0680OK
2020-07-07 09:23:0786OK
| + | server 2 |
TimeCPU TemperatureServer Status
2020-07-07 10:32:2090Overload
2020-07-07 09:30:0588OK
2020-07-07 09:25:0588OK
| + | server 3 |
TimeCPU TemperatureServer Status
2020-07-07 11:34:2062OK
2020-07-07 10:31:2255OK
2020-07-07 09:30:5762Rebooting
| + + After choosing the field by which you want to group your data, you can add various calculations on the other fields and apply the calculation to each group of rows. For instance, you might want to calculate the average CPU temperature for each of those servers. To do so, add the **mean calculation** applied on the CPU Temperature field to get the following result: + + | Server ID | CPU Temperatute (mean) | | + | -------------- | ------------- | ------------- | + | server 1 | 82 |
TimeServer Status
2020-07-07 11:34:20Shutdown
2020-07-07 09:28:06OK
2020-07-07 09:23:07OK
| + | server 2 | 88.6 |
TimeServer Status
2020-07-07 10:32:20Overload
2020-07-07 09:30:05OK
2020-07-07 09:25:05OK
| + | server 3 | 59.6 |
TimeServer Status
2020-07-07 11:34:20OK
2020-07-07 10:31:22OK
2020-07-07 09:30:57Rebooting
| + `; + }, + links: [ + { + title: 'Calculation types', + url: 'https://grafana.com/docs/grafana/latest/panels-visualizations/calculation-types/', + }, + ], + }, heatmap: { name: 'Create heatmap', getHelperDocs: function () { From 1bb38e8f95294de85ee7fd6b03730b2ded68438a Mon Sep 17 00:00:00 2001 From: Alexander Weaver Date: Mon, 4 Mar 2024 17:15:55 -0600 Subject: [PATCH 0381/1406] Alerting: Move ruleRoutine to be a method on ruleInfo (#83866) * Move ruleRoutine to ruleInfo file * Move tests as well * swap ruleInfo and scheduler parameters on ruleRoutine * Fix linter complaint, receiver name --- pkg/services/ngalert/schedule/alert_rule.go | 281 +++++++++++ .../ngalert/schedule/alert_rule_test.go | 477 ++++++++++++++++++ pkg/services/ngalert/schedule/schedule.go | 278 +--------- .../ngalert/schedule/schedule_unit_test.go | 470 ----------------- 4 files changed, 759 insertions(+), 747 deletions(-) diff --git a/pkg/services/ngalert/schedule/alert_rule.go b/pkg/services/ngalert/schedule/alert_rule.go index 7555f77d84..ab184a3bd8 100644 --- a/pkg/services/ngalert/schedule/alert_rule.go +++ b/pkg/services/ngalert/schedule/alert_rule.go @@ -2,8 +2,20 @@ package schedule import ( context "context" + "errors" + "fmt" + "time" + "github.com/grafana/grafana/pkg/services/datasources" + "github.com/grafana/grafana/pkg/services/ngalert/eval" + ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" + "github.com/grafana/grafana/pkg/services/ngalert/state" + "github.com/grafana/grafana/pkg/services/org" + "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/util" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" ) type alertRuleInfo struct { @@ -68,3 +80,272 @@ func (a *alertRuleInfo) update(lastVersion ruleVersionAndPauseStatus) bool { func (a *alertRuleInfo) stop(reason error) { a.stopFn(reason) } + +//nolint:gocyclo +func (a *alertRuleInfo) ruleRoutine(key ngmodels.AlertRuleKey, sch *schedule) error { + grafanaCtx := ngmodels.WithRuleKey(a.ctx, key) + logger := sch.log.FromContext(grafanaCtx) + logger.Debug("Alert rule routine started") + + orgID := fmt.Sprint(key.OrgID) + evalTotal := sch.metrics.EvalTotal.WithLabelValues(orgID) + evalDuration := sch.metrics.EvalDuration.WithLabelValues(orgID) + evalTotalFailures := sch.metrics.EvalFailures.WithLabelValues(orgID) + processDuration := sch.metrics.ProcessDuration.WithLabelValues(orgID) + sendDuration := sch.metrics.SendDuration.WithLabelValues(orgID) + + notify := func(states []state.StateTransition) { + expiredAlerts := state.FromAlertsStateToStoppedAlert(states, sch.appURL, sch.clock) + if len(expiredAlerts.PostableAlerts) > 0 { + sch.alertsSender.Send(grafanaCtx, key, expiredAlerts) + } + } + + resetState := func(ctx context.Context, isPaused bool) { + rule := sch.schedulableAlertRules.get(key) + reason := ngmodels.StateReasonUpdated + if isPaused { + reason = ngmodels.StateReasonPaused + } + states := sch.stateManager.ResetStateByRuleUID(ctx, rule, reason) + notify(states) + } + + evaluate := func(ctx context.Context, f fingerprint, attempt int64, e *evaluation, span trace.Span, retry bool) error { + logger := logger.New("version", e.rule.Version, "fingerprint", f, "attempt", attempt, "now", e.scheduledAt).FromContext(ctx) + start := sch.clock.Now() + + evalCtx := eval.NewContextWithPreviousResults(ctx, SchedulerUserFor(e.rule.OrgID), sch.newLoadedMetricsReader(e.rule)) + if sch.evaluatorFactory == nil { + panic("evalfactory nil") + } + ruleEval, err := sch.evaluatorFactory.Create(evalCtx, e.rule.GetEvalCondition()) + var results eval.Results + var dur time.Duration + if err != nil { + dur = sch.clock.Now().Sub(start) + logger.Error("Failed to build rule evaluator", "error", err) + } else { + results, err = ruleEval.Evaluate(ctx, e.scheduledAt) + dur = sch.clock.Now().Sub(start) + if err != nil { + logger.Error("Failed to evaluate rule", "error", err, "duration", dur) + } + } + + evalTotal.Inc() + evalDuration.Observe(dur.Seconds()) + + if ctx.Err() != nil { // check if the context is not cancelled. The evaluation can be a long-running task. + span.SetStatus(codes.Error, "rule evaluation cancelled") + logger.Debug("Skip updating the state because the context has been cancelled") + return nil + } + + if err != nil || results.HasErrors() { + evalTotalFailures.Inc() + + // Only retry (return errors) if this isn't the last attempt, otherwise skip these return operations. + if retry { + // The only thing that can return non-nil `err` from ruleEval.Evaluate is the server side expression pipeline. + // This includes transport errors such as transient network errors. + if err != nil { + span.SetStatus(codes.Error, "rule evaluation failed") + span.RecordError(err) + return fmt.Errorf("server side expressions pipeline returned an error: %w", err) + } + + // If the pipeline executed successfully but have other types of errors that can be retryable, we should do so. + if !results.HasNonRetryableErrors() { + span.SetStatus(codes.Error, "rule evaluation failed") + span.RecordError(err) + return fmt.Errorf("the result-set has errors that can be retried: %w", results.Error()) + } + } + + // If results is nil, we assume that the error must be from the SSE pipeline (ruleEval.Evaluate) which is the only code that can actually return an `err`. + if results == nil { + results = append(results, eval.NewResultFromError(err, e.scheduledAt, dur)) + } + + // If err is nil, we assume that the SSS pipeline succeeded and that the error must be embedded in the results. + if err == nil { + err = results.Error() + } + + span.SetStatus(codes.Error, "rule evaluation failed") + span.RecordError(err) + } else { + logger.Debug("Alert rule evaluated", "results", results, "duration", dur) + span.AddEvent("rule evaluated", trace.WithAttributes( + attribute.Int64("results", int64(len(results))), + )) + } + start = sch.clock.Now() + processedStates := sch.stateManager.ProcessEvalResults( + ctx, + e.scheduledAt, + e.rule, + results, + state.GetRuleExtraLabels(logger, e.rule, e.folderTitle, !sch.disableGrafanaFolder), + ) + processDuration.Observe(sch.clock.Now().Sub(start).Seconds()) + + start = sch.clock.Now() + alerts := state.FromStateTransitionToPostableAlerts(processedStates, sch.stateManager, sch.appURL) + span.AddEvent("results processed", trace.WithAttributes( + attribute.Int64("state_transitions", int64(len(processedStates))), + attribute.Int64("alerts_to_send", int64(len(alerts.PostableAlerts))), + )) + if len(alerts.PostableAlerts) > 0 { + sch.alertsSender.Send(ctx, key, alerts) + } + sendDuration.Observe(sch.clock.Now().Sub(start).Seconds()) + + return nil + } + + evalRunning := false + var currentFingerprint fingerprint + defer sch.stopApplied(key) + for { + select { + // used by external services (API) to notify that rule is updated. + case ctx := <-a.updateCh: + if currentFingerprint == ctx.Fingerprint { + logger.Info("Rule's fingerprint has not changed. Skip resetting the state", "currentFingerprint", currentFingerprint) + continue + } + + logger.Info("Clearing the state of the rule because it was updated", "isPaused", ctx.IsPaused, "fingerprint", ctx.Fingerprint) + // clear the state. So the next evaluation will start from the scratch. + resetState(grafanaCtx, ctx.IsPaused) + currentFingerprint = ctx.Fingerprint + // evalCh - used by the scheduler to signal that evaluation is needed. + case ctx, ok := <-a.evalCh: + if !ok { + logger.Debug("Evaluation channel has been closed. Exiting") + return nil + } + if evalRunning { + continue + } + + func() { + evalRunning = true + defer func() { + evalRunning = false + sch.evalApplied(key, ctx.scheduledAt) + }() + + for attempt := int64(1); attempt <= sch.maxAttempts; attempt++ { + isPaused := ctx.rule.IsPaused + f := ruleWithFolder{ctx.rule, ctx.folderTitle}.Fingerprint() + // Do not clean up state if the eval loop has just started. + var needReset bool + if currentFingerprint != 0 && currentFingerprint != f { + logger.Debug("Got a new version of alert rule. Clear up the state", "fingerprint", f) + needReset = true + } + // We need to reset state if the loop has started and the alert is already paused. It can happen, + // if we have an alert with state and we do file provision with stateful Grafana, that state + // lingers in DB and won't be cleaned up until next alert rule update. + needReset = needReset || (currentFingerprint == 0 && isPaused) + if needReset { + resetState(grafanaCtx, isPaused) + } + currentFingerprint = f + if isPaused { + logger.Debug("Skip rule evaluation because it is paused") + return + } + + fpStr := currentFingerprint.String() + utcTick := ctx.scheduledAt.UTC().Format(time.RFC3339Nano) + tracingCtx, span := sch.tracer.Start(grafanaCtx, "alert rule execution", trace.WithAttributes( + attribute.String("rule_uid", ctx.rule.UID), + attribute.Int64("org_id", ctx.rule.OrgID), + attribute.Int64("rule_version", ctx.rule.Version), + attribute.String("rule_fingerprint", fpStr), + attribute.String("tick", utcTick), + )) + + // Check before any execution if the context was cancelled so that we don't do any evaluations. + if tracingCtx.Err() != nil { + span.SetStatus(codes.Error, "rule evaluation cancelled") + span.End() + logger.Error("Skip evaluation and updating the state because the context has been cancelled", "version", ctx.rule.Version, "fingerprint", f, "attempt", attempt, "now", ctx.scheduledAt) + return + } + + retry := attempt < sch.maxAttempts + err := evaluate(tracingCtx, f, attempt, ctx, span, retry) + // This is extremely confusing - when we exhaust all retry attempts, or we have no retryable errors + // we return nil - so technically, this is meaningless to know whether the evaluation has errors or not. + span.End() + if err == nil { + return + } + + logger.Error("Failed to evaluate rule", "version", ctx.rule.Version, "fingerprint", f, "attempt", attempt, "now", ctx.scheduledAt, "error", err) + select { + case <-tracingCtx.Done(): + logger.Error("Context has been cancelled while backing off", "version", ctx.rule.Version, "fingerprint", f, "attempt", attempt, "now", ctx.scheduledAt) + return + case <-time.After(retryDelay): + continue + } + } + }() + + case <-grafanaCtx.Done(): + // clean up the state only if the reason for stopping the evaluation loop is that the rule was deleted + if errors.Is(grafanaCtx.Err(), errRuleDeleted) { + // We do not want a context to be unbounded which could potentially cause a go routine running + // indefinitely. 1 minute is an almost randomly chosen timeout, big enough to cover the majority of the + // cases. + ctx, cancelFunc := context.WithTimeout(context.Background(), time.Minute) + defer cancelFunc() + states := sch.stateManager.DeleteStateByRuleUID(ngmodels.WithRuleKey(ctx, key), key, ngmodels.StateReasonRuleDeleted) + notify(states) + } + logger.Debug("Stopping alert rule routine") + return nil + } + } +} + +// evalApplied is only used on tests. +func (sch *schedule) evalApplied(alertDefKey ngmodels.AlertRuleKey, now time.Time) { + if sch.evalAppliedFunc == nil { + return + } + + sch.evalAppliedFunc(alertDefKey, now) +} + +// stopApplied is only used on tests. +func (sch *schedule) stopApplied(alertDefKey ngmodels.AlertRuleKey) { + if sch.stopAppliedFunc == nil { + return + } + + sch.stopAppliedFunc(alertDefKey) +} + +func SchedulerUserFor(orgID int64) *user.SignedInUser { + return &user.SignedInUser{ + UserID: -1, + IsServiceAccount: true, + Login: "grafana_scheduler", + OrgID: orgID, + OrgRole: org.RoleAdmin, + Permissions: map[int64]map[string][]string{ + orgID: { + datasources.ActionQuery: []string{ + datasources.ScopeAll, + }, + }, + }, + } +} diff --git a/pkg/services/ngalert/schedule/alert_rule_test.go b/pkg/services/ngalert/schedule/alert_rule_test.go index a8dfbfb825..e6f3324401 100644 --- a/pkg/services/ngalert/schedule/alert_rule_test.go +++ b/pkg/services/ngalert/schedule/alert_rule_test.go @@ -1,7 +1,9 @@ package schedule import ( + "bytes" context "context" + "fmt" "math" "math/rand" "runtime" @@ -9,8 +11,18 @@ import ( "testing" "time" + alertingModels "github.com/grafana/alerting/models" + "github.com/grafana/grafana-plugin-sdk-go/data" + definitions "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" + "github.com/grafana/grafana/pkg/services/ngalert/eval" models "github.com/grafana/grafana/pkg/services/ngalert/models" + "github.com/grafana/grafana/pkg/services/ngalert/state" "github.com/grafana/grafana/pkg/util" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/testutil" + prometheusModel "github.com/prometheus/common/model" + "github.com/stretchr/testify/assert" + mock "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" ) @@ -227,3 +239,468 @@ func TestAlertRuleInfo(t *testing.T) { wg.Wait() }) } + +func TestRuleRoutine(t *testing.T) { + createSchedule := func( + evalAppliedChan chan time.Time, + senderMock *SyncAlertsSenderMock, + ) (*schedule, *fakeRulesStore, *state.FakeInstanceStore, prometheus.Gatherer) { + ruleStore := newFakeRulesStore() + instanceStore := &state.FakeInstanceStore{} + + registry := prometheus.NewPedanticRegistry() + sch := setupScheduler(t, ruleStore, instanceStore, registry, senderMock, nil) + sch.evalAppliedFunc = func(key models.AlertRuleKey, t time.Time) { + evalAppliedChan <- t + } + return sch, ruleStore, instanceStore, registry + } + + // normal states do not include NoData and Error because currently it is not possible to perform any sensible test + normalStates := []eval.State{eval.Normal, eval.Alerting, eval.Pending} + allStates := [...]eval.State{eval.Normal, eval.Alerting, eval.Pending, eval.NoData, eval.Error} + + for _, evalState := range normalStates { + // TODO rewrite when we are able to mock/fake state manager + t.Run(fmt.Sprintf("when rule evaluation happens (evaluation state %s)", evalState), func(t *testing.T) { + evalAppliedChan := make(chan time.Time) + sch, ruleStore, instanceStore, reg := createSchedule(evalAppliedChan, nil) + + rule := models.AlertRuleGen(withQueryForState(t, evalState))() + ruleStore.PutRule(context.Background(), rule) + folderTitle := ruleStore.getNamespaceTitle(rule.NamespaceUID) + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + ruleInfo := newAlertRuleInfo(ctx) + go func() { + _ = ruleInfo.ruleRoutine(rule.GetKey(), sch) + }() + + expectedTime := time.UnixMicro(rand.Int63()) + + ruleInfo.evalCh <- &evaluation{ + scheduledAt: expectedTime, + rule: rule, + folderTitle: folderTitle, + } + + actualTime := waitForTimeChannel(t, evalAppliedChan) + require.Equal(t, expectedTime, actualTime) + + t.Run("it should add extra labels", func(t *testing.T) { + states := sch.stateManager.GetStatesForRuleUID(rule.OrgID, rule.UID) + for _, s := range states { + assert.Equal(t, rule.UID, s.Labels[alertingModels.RuleUIDLabel]) + assert.Equal(t, rule.NamespaceUID, s.Labels[alertingModels.NamespaceUIDLabel]) + assert.Equal(t, rule.Title, s.Labels[prometheusModel.AlertNameLabel]) + assert.Equal(t, folderTitle, s.Labels[models.FolderTitleLabel]) + } + }) + + t.Run("it should process evaluation results via state manager", func(t *testing.T) { + // TODO rewrite when we are able to mock/fake state manager + states := sch.stateManager.GetStatesForRuleUID(rule.OrgID, rule.UID) + require.Len(t, states, 1) + s := states[0] + require.Equal(t, rule.UID, s.AlertRuleUID) + require.Len(t, s.Results, 1) + var expectedStatus = evalState + if evalState == eval.Pending { + expectedStatus = eval.Alerting + } + require.Equal(t, expectedStatus.String(), s.Results[0].EvaluationState.String()) + require.Equal(t, expectedTime, s.Results[0].EvaluationTime) + }) + t.Run("it should save alert instances to storage", func(t *testing.T) { + // TODO rewrite when we are able to mock/fake state manager + states := sch.stateManager.GetStatesForRuleUID(rule.OrgID, rule.UID) + require.Len(t, states, 1) + s := states[0] + + var cmd *models.AlertInstance + for _, op := range instanceStore.RecordedOps() { + switch q := op.(type) { + case models.AlertInstance: + cmd = &q + } + if cmd != nil { + break + } + } + + require.NotNil(t, cmd) + t.Logf("Saved alert instances: %v", cmd) + require.Equal(t, rule.OrgID, cmd.RuleOrgID) + require.Equal(t, expectedTime, cmd.LastEvalTime) + require.Equal(t, rule.UID, cmd.RuleUID) + require.Equal(t, evalState.String(), string(cmd.CurrentState)) + require.Equal(t, s.Labels, data.Labels(cmd.Labels)) + }) + + t.Run("it reports metrics", func(t *testing.T) { + // duration metric has 0 values because of mocked clock that do not advance + expectedMetric := fmt.Sprintf( + `# HELP grafana_alerting_rule_evaluation_duration_seconds The time to evaluate a rule. + # TYPE grafana_alerting_rule_evaluation_duration_seconds histogram + grafana_alerting_rule_evaluation_duration_seconds_bucket{org="%[1]d",le="0.01"} 1 + grafana_alerting_rule_evaluation_duration_seconds_bucket{org="%[1]d",le="0.1"} 1 + grafana_alerting_rule_evaluation_duration_seconds_bucket{org="%[1]d",le="0.5"} 1 + grafana_alerting_rule_evaluation_duration_seconds_bucket{org="%[1]d",le="1"} 1 + grafana_alerting_rule_evaluation_duration_seconds_bucket{org="%[1]d",le="5"} 1 + grafana_alerting_rule_evaluation_duration_seconds_bucket{org="%[1]d",le="10"} 1 + grafana_alerting_rule_evaluation_duration_seconds_bucket{org="%[1]d",le="15"} 1 + grafana_alerting_rule_evaluation_duration_seconds_bucket{org="%[1]d",le="30"} 1 + grafana_alerting_rule_evaluation_duration_seconds_bucket{org="%[1]d",le="60"} 1 + grafana_alerting_rule_evaluation_duration_seconds_bucket{org="%[1]d",le="120"} 1 + grafana_alerting_rule_evaluation_duration_seconds_bucket{org="%[1]d",le="180"} 1 + grafana_alerting_rule_evaluation_duration_seconds_bucket{org="%[1]d",le="240"} 1 + grafana_alerting_rule_evaluation_duration_seconds_bucket{org="%[1]d",le="300"} 1 + grafana_alerting_rule_evaluation_duration_seconds_bucket{org="%[1]d",le="+Inf"} 1 + grafana_alerting_rule_evaluation_duration_seconds_sum{org="%[1]d"} 0 + grafana_alerting_rule_evaluation_duration_seconds_count{org="%[1]d"} 1 + # HELP grafana_alerting_rule_evaluation_failures_total The total number of rule evaluation failures. + # TYPE grafana_alerting_rule_evaluation_failures_total counter + grafana_alerting_rule_evaluation_failures_total{org="%[1]d"} 0 + # HELP grafana_alerting_rule_evaluations_total The total number of rule evaluations. + # TYPE grafana_alerting_rule_evaluations_total counter + grafana_alerting_rule_evaluations_total{org="%[1]d"} 1 + # HELP grafana_alerting_rule_process_evaluation_duration_seconds The time to process the evaluation results for a rule. + # TYPE grafana_alerting_rule_process_evaluation_duration_seconds histogram + grafana_alerting_rule_process_evaluation_duration_seconds_bucket{org="%[1]d",le="0.01"} 1 + grafana_alerting_rule_process_evaluation_duration_seconds_bucket{org="%[1]d",le="0.1"} 1 + grafana_alerting_rule_process_evaluation_duration_seconds_bucket{org="%[1]d",le="0.5"} 1 + grafana_alerting_rule_process_evaluation_duration_seconds_bucket{org="%[1]d",le="1"} 1 + grafana_alerting_rule_process_evaluation_duration_seconds_bucket{org="%[1]d",le="5"} 1 + grafana_alerting_rule_process_evaluation_duration_seconds_bucket{org="%[1]d",le="10"} 1 + grafana_alerting_rule_process_evaluation_duration_seconds_bucket{org="%[1]d",le="15"} 1 + grafana_alerting_rule_process_evaluation_duration_seconds_bucket{org="%[1]d",le="30"} 1 + grafana_alerting_rule_process_evaluation_duration_seconds_bucket{org="%[1]d",le="60"} 1 + grafana_alerting_rule_process_evaluation_duration_seconds_bucket{org="%[1]d",le="120"} 1 + grafana_alerting_rule_process_evaluation_duration_seconds_bucket{org="%[1]d",le="180"} 1 + grafana_alerting_rule_process_evaluation_duration_seconds_bucket{org="%[1]d",le="240"} 1 + grafana_alerting_rule_process_evaluation_duration_seconds_bucket{org="%[1]d",le="300"} 1 + grafana_alerting_rule_process_evaluation_duration_seconds_bucket{org="%[1]d",le="+Inf"} 1 + grafana_alerting_rule_process_evaluation_duration_seconds_sum{org="%[1]d"} 0 + grafana_alerting_rule_process_evaluation_duration_seconds_count{org="%[1]d"} 1 + # HELP grafana_alerting_rule_send_alerts_duration_seconds The time to send the alerts to Alertmanager. + # TYPE grafana_alerting_rule_send_alerts_duration_seconds histogram + grafana_alerting_rule_send_alerts_duration_seconds_bucket{org="%[1]d",le="0.01"} 1 + grafana_alerting_rule_send_alerts_duration_seconds_bucket{org="%[1]d",le="0.1"} 1 + grafana_alerting_rule_send_alerts_duration_seconds_bucket{org="%[1]d",le="0.5"} 1 + grafana_alerting_rule_send_alerts_duration_seconds_bucket{org="%[1]d",le="1"} 1 + grafana_alerting_rule_send_alerts_duration_seconds_bucket{org="%[1]d",le="5"} 1 + grafana_alerting_rule_send_alerts_duration_seconds_bucket{org="%[1]d",le="10"} 1 + grafana_alerting_rule_send_alerts_duration_seconds_bucket{org="%[1]d",le="15"} 1 + grafana_alerting_rule_send_alerts_duration_seconds_bucket{org="%[1]d",le="30"} 1 + grafana_alerting_rule_send_alerts_duration_seconds_bucket{org="%[1]d",le="60"} 1 + grafana_alerting_rule_send_alerts_duration_seconds_bucket{org="%[1]d",le="120"} 1 + grafana_alerting_rule_send_alerts_duration_seconds_bucket{org="%[1]d",le="180"} 1 + grafana_alerting_rule_send_alerts_duration_seconds_bucket{org="%[1]d",le="240"} 1 + grafana_alerting_rule_send_alerts_duration_seconds_bucket{org="%[1]d",le="300"} 1 + grafana_alerting_rule_send_alerts_duration_seconds_bucket{org="%[1]d",le="+Inf"} 1 + grafana_alerting_rule_send_alerts_duration_seconds_sum{org="%[1]d"} 0 + grafana_alerting_rule_send_alerts_duration_seconds_count{org="%[1]d"} 1 + `, rule.OrgID) + + err := testutil.GatherAndCompare(reg, bytes.NewBufferString(expectedMetric), "grafana_alerting_rule_evaluation_duration_seconds", "grafana_alerting_rule_evaluations_total", "grafana_alerting_rule_evaluation_failures_total", "grafana_alerting_rule_process_evaluation_duration_seconds", "grafana_alerting_rule_send_alerts_duration_seconds") + require.NoError(t, err) + }) + }) + } + + t.Run("should exit", func(t *testing.T) { + t.Run("and not clear the state if parent context is cancelled", func(t *testing.T) { + stoppedChan := make(chan error) + sch, _, _, _ := createSchedule(make(chan time.Time), nil) + + rule := models.AlertRuleGen()() + _ = sch.stateManager.ProcessEvalResults(context.Background(), sch.clock.Now(), rule, eval.GenerateResults(rand.Intn(5)+1, eval.ResultGen(eval.WithEvaluatedAt(sch.clock.Now()))), nil) + expectedStates := sch.stateManager.GetStatesForRuleUID(rule.OrgID, rule.UID) + require.NotEmpty(t, expectedStates) + + ctx, cancel := context.WithCancel(context.Background()) + ruleInfo := newAlertRuleInfo(ctx) + go func() { + err := ruleInfo.ruleRoutine(models.AlertRuleKey{}, sch) + stoppedChan <- err + }() + + cancel() + err := waitForErrChannel(t, stoppedChan) + require.NoError(t, err) + require.Equal(t, len(expectedStates), len(sch.stateManager.GetStatesForRuleUID(rule.OrgID, rule.UID))) + }) + t.Run("and clean up the state if delete is cancellation reason for inner context", func(t *testing.T) { + stoppedChan := make(chan error) + sch, _, _, _ := createSchedule(make(chan time.Time), nil) + + rule := models.AlertRuleGen()() + _ = sch.stateManager.ProcessEvalResults(context.Background(), sch.clock.Now(), rule, eval.GenerateResults(rand.Intn(5)+1, eval.ResultGen(eval.WithEvaluatedAt(sch.clock.Now()))), nil) + require.NotEmpty(t, sch.stateManager.GetStatesForRuleUID(rule.OrgID, rule.UID)) + + ruleInfo := newAlertRuleInfo(context.Background()) + go func() { + err := ruleInfo.ruleRoutine(rule.GetKey(), sch) + stoppedChan <- err + }() + + ruleInfo.stop(errRuleDeleted) + err := waitForErrChannel(t, stoppedChan) + require.NoError(t, err) + + require.Empty(t, sch.stateManager.GetStatesForRuleUID(rule.OrgID, rule.UID)) + }) + }) + + t.Run("when a message is sent to update channel", func(t *testing.T) { + rule := models.AlertRuleGen(withQueryForState(t, eval.Normal))() + folderTitle := "folderName" + ruleFp := ruleWithFolder{rule, folderTitle}.Fingerprint() + + evalAppliedChan := make(chan time.Time) + + sender := NewSyncAlertsSenderMock() + sender.EXPECT().Send(mock.Anything, rule.GetKey(), mock.Anything).Return() + + sch, ruleStore, _, _ := createSchedule(evalAppliedChan, sender) + ruleStore.PutRule(context.Background(), rule) + sch.schedulableAlertRules.set([]*models.AlertRule{rule}, map[models.FolderKey]string{rule.GetFolderKey(): folderTitle}) + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + ruleInfo := newAlertRuleInfo(ctx) + + go func() { + _ = ruleInfo.ruleRoutine(rule.GetKey(), sch) + }() + + // init evaluation loop so it got the rule version + ruleInfo.evalCh <- &evaluation{ + scheduledAt: sch.clock.Now(), + rule: rule, + folderTitle: folderTitle, + } + + waitForTimeChannel(t, evalAppliedChan) + + // define some state + states := make([]*state.State, 0, len(allStates)) + for _, s := range allStates { + for i := 0; i < 2; i++ { + states = append(states, &state.State{ + AlertRuleUID: rule.UID, + CacheID: util.GenerateShortUID(), + OrgID: rule.OrgID, + State: s, + StartsAt: sch.clock.Now(), + EndsAt: sch.clock.Now().Add(time.Duration(rand.Intn(25)+5) * time.Second), + Labels: rule.Labels, + }) + } + } + sch.stateManager.Put(states) + + states = sch.stateManager.GetStatesForRuleUID(rule.OrgID, rule.UID) + expectedToBeSent := 0 + for _, s := range states { + if s.State == eval.Normal || s.State == eval.Pending { + continue + } + expectedToBeSent++ + } + require.Greaterf(t, expectedToBeSent, 0, "State manager was expected to return at least one state that can be expired") + + t.Run("should do nothing if version in channel is the same", func(t *testing.T) { + ruleInfo.updateCh <- ruleVersionAndPauseStatus{ruleFp, false} + ruleInfo.updateCh <- ruleVersionAndPauseStatus{ruleFp, false} // second time just to make sure that previous messages were handled + + actualStates := sch.stateManager.GetStatesForRuleUID(rule.OrgID, rule.UID) + require.Len(t, actualStates, len(states)) + + sender.AssertNotCalled(t, "Send", mock.Anything, mock.Anything) + }) + + t.Run("should clear the state and expire firing alerts if version in channel is greater", func(t *testing.T) { + ruleInfo.updateCh <- ruleVersionAndPauseStatus{ruleFp + 1, false} + + require.Eventually(t, func() bool { + return len(sender.Calls()) > 0 + }, 5*time.Second, 100*time.Millisecond) + + require.Empty(t, sch.stateManager.GetStatesForRuleUID(rule.OrgID, rule.UID)) + sender.AssertNumberOfCalls(t, "Send", 1) + args, ok := sender.Calls()[0].Arguments[2].(definitions.PostableAlerts) + require.Truef(t, ok, fmt.Sprintf("expected argument of function was supposed to be 'definitions.PostableAlerts' but got %T", sender.Calls()[0].Arguments[2])) + require.Len(t, args.PostableAlerts, expectedToBeSent) + }) + }) + + t.Run("when evaluation fails", func(t *testing.T) { + rule := models.AlertRuleGen(withQueryForState(t, eval.Error))() + rule.ExecErrState = models.ErrorErrState + + evalAppliedChan := make(chan time.Time) + + sender := NewSyncAlertsSenderMock() + sender.EXPECT().Send(mock.Anything, rule.GetKey(), mock.Anything).Return() + + sch, ruleStore, _, reg := createSchedule(evalAppliedChan, sender) + sch.maxAttempts = 3 + ruleStore.PutRule(context.Background(), rule) + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + ruleInfo := newAlertRuleInfo(ctx) + + go func() { + _ = ruleInfo.ruleRoutine(rule.GetKey(), sch) + }() + + ruleInfo.evalCh <- &evaluation{ + scheduledAt: sch.clock.Now(), + rule: rule, + } + + waitForTimeChannel(t, evalAppliedChan) + + t.Run("it should increase failure counter", func(t *testing.T) { + // duration metric has 0 values because of mocked clock that do not advance + expectedMetric := fmt.Sprintf( + `# HELP grafana_alerting_rule_evaluation_duration_seconds The time to evaluate a rule. + # TYPE grafana_alerting_rule_evaluation_duration_seconds histogram + grafana_alerting_rule_evaluation_duration_seconds_bucket{org="%[1]d",le="0.01"} 3 + grafana_alerting_rule_evaluation_duration_seconds_bucket{org="%[1]d",le="0.1"} 3 + grafana_alerting_rule_evaluation_duration_seconds_bucket{org="%[1]d",le="0.5"} 3 + grafana_alerting_rule_evaluation_duration_seconds_bucket{org="%[1]d",le="1"} 3 + grafana_alerting_rule_evaluation_duration_seconds_bucket{org="%[1]d",le="5"} 3 + grafana_alerting_rule_evaluation_duration_seconds_bucket{org="%[1]d",le="10"} 3 + grafana_alerting_rule_evaluation_duration_seconds_bucket{org="%[1]d",le="15"} 3 + grafana_alerting_rule_evaluation_duration_seconds_bucket{org="%[1]d",le="30"} 3 + grafana_alerting_rule_evaluation_duration_seconds_bucket{org="%[1]d",le="60"} 3 + grafana_alerting_rule_evaluation_duration_seconds_bucket{org="%[1]d",le="120"} 3 + grafana_alerting_rule_evaluation_duration_seconds_bucket{org="%[1]d",le="180"} 3 + grafana_alerting_rule_evaluation_duration_seconds_bucket{org="%[1]d",le="240"} 3 + grafana_alerting_rule_evaluation_duration_seconds_bucket{org="%[1]d",le="300"} 3 + grafana_alerting_rule_evaluation_duration_seconds_bucket{org="%[1]d",le="+Inf"} 3 + grafana_alerting_rule_evaluation_duration_seconds_sum{org="%[1]d"} 0 + grafana_alerting_rule_evaluation_duration_seconds_count{org="%[1]d"} 3 + # HELP grafana_alerting_rule_evaluation_failures_total The total number of rule evaluation failures. + # TYPE grafana_alerting_rule_evaluation_failures_total counter + grafana_alerting_rule_evaluation_failures_total{org="%[1]d"} 3 + # HELP grafana_alerting_rule_evaluations_total The total number of rule evaluations. + # TYPE grafana_alerting_rule_evaluations_total counter + grafana_alerting_rule_evaluations_total{org="%[1]d"} 3 + # HELP grafana_alerting_rule_process_evaluation_duration_seconds The time to process the evaluation results for a rule. + # TYPE grafana_alerting_rule_process_evaluation_duration_seconds histogram + grafana_alerting_rule_process_evaluation_duration_seconds_bucket{org="%[1]d",le="0.01"} 1 + grafana_alerting_rule_process_evaluation_duration_seconds_bucket{org="%[1]d",le="0.1"} 1 + grafana_alerting_rule_process_evaluation_duration_seconds_bucket{org="%[1]d",le="0.5"} 1 + grafana_alerting_rule_process_evaluation_duration_seconds_bucket{org="%[1]d",le="1"} 1 + grafana_alerting_rule_process_evaluation_duration_seconds_bucket{org="%[1]d",le="5"} 1 + grafana_alerting_rule_process_evaluation_duration_seconds_bucket{org="%[1]d",le="10"} 1 + grafana_alerting_rule_process_evaluation_duration_seconds_bucket{org="%[1]d",le="15"} 1 + grafana_alerting_rule_process_evaluation_duration_seconds_bucket{org="%[1]d",le="30"} 1 + grafana_alerting_rule_process_evaluation_duration_seconds_bucket{org="%[1]d",le="60"} 1 + grafana_alerting_rule_process_evaluation_duration_seconds_bucket{org="%[1]d",le="120"} 1 + grafana_alerting_rule_process_evaluation_duration_seconds_bucket{org="%[1]d",le="180"} 1 + grafana_alerting_rule_process_evaluation_duration_seconds_bucket{org="%[1]d",le="240"} 1 + grafana_alerting_rule_process_evaluation_duration_seconds_bucket{org="%[1]d",le="300"} 1 + grafana_alerting_rule_process_evaluation_duration_seconds_bucket{org="%[1]d",le="+Inf"} 1 + grafana_alerting_rule_process_evaluation_duration_seconds_sum{org="%[1]d"} 0 + grafana_alerting_rule_process_evaluation_duration_seconds_count{org="%[1]d"} 1 + # HELP grafana_alerting_rule_send_alerts_duration_seconds The time to send the alerts to Alertmanager. + # TYPE grafana_alerting_rule_send_alerts_duration_seconds histogram + grafana_alerting_rule_send_alerts_duration_seconds_bucket{org="%[1]d",le="0.01"} 1 + grafana_alerting_rule_send_alerts_duration_seconds_bucket{org="%[1]d",le="0.1"} 1 + grafana_alerting_rule_send_alerts_duration_seconds_bucket{org="%[1]d",le="0.5"} 1 + grafana_alerting_rule_send_alerts_duration_seconds_bucket{org="%[1]d",le="1"} 1 + grafana_alerting_rule_send_alerts_duration_seconds_bucket{org="%[1]d",le="5"} 1 + grafana_alerting_rule_send_alerts_duration_seconds_bucket{org="%[1]d",le="10"} 1 + grafana_alerting_rule_send_alerts_duration_seconds_bucket{org="%[1]d",le="15"} 1 + grafana_alerting_rule_send_alerts_duration_seconds_bucket{org="%[1]d",le="30"} 1 + grafana_alerting_rule_send_alerts_duration_seconds_bucket{org="%[1]d",le="60"} 1 + grafana_alerting_rule_send_alerts_duration_seconds_bucket{org="%[1]d",le="120"} 1 + grafana_alerting_rule_send_alerts_duration_seconds_bucket{org="%[1]d",le="180"} 1 + grafana_alerting_rule_send_alerts_duration_seconds_bucket{org="%[1]d",le="240"} 1 + grafana_alerting_rule_send_alerts_duration_seconds_bucket{org="%[1]d",le="300"} 1 + grafana_alerting_rule_send_alerts_duration_seconds_bucket{org="%[1]d",le="+Inf"} 1 + grafana_alerting_rule_send_alerts_duration_seconds_sum{org="%[1]d"} 0 + grafana_alerting_rule_send_alerts_duration_seconds_count{org="%[1]d"} 1 + `, rule.OrgID) + + err := testutil.GatherAndCompare(reg, bytes.NewBufferString(expectedMetric), "grafana_alerting_rule_evaluation_duration_seconds", "grafana_alerting_rule_evaluations_total", "grafana_alerting_rule_evaluation_failures_total", "grafana_alerting_rule_process_evaluation_duration_seconds", "grafana_alerting_rule_send_alerts_duration_seconds") + require.NoError(t, err) + }) + + t.Run("it should send special alert DatasourceError", func(t *testing.T) { + sender.AssertNumberOfCalls(t, "Send", 1) + args, ok := sender.Calls()[0].Arguments[2].(definitions.PostableAlerts) + require.Truef(t, ok, fmt.Sprintf("expected argument of function was supposed to be 'definitions.PostableAlerts' but got %T", sender.Calls()[0].Arguments[2])) + assert.Len(t, args.PostableAlerts, 1) + assert.Equal(t, state.ErrorAlertName, args.PostableAlerts[0].Labels[prometheusModel.AlertNameLabel]) + }) + }) + + t.Run("when there are alerts that should be firing", func(t *testing.T) { + t.Run("it should call sender", func(t *testing.T) { + // eval.Alerting makes state manager to create notifications for alertmanagers + rule := models.AlertRuleGen(withQueryForState(t, eval.Alerting))() + + evalAppliedChan := make(chan time.Time) + + sender := NewSyncAlertsSenderMock() + sender.EXPECT().Send(mock.Anything, rule.GetKey(), mock.Anything).Return() + + sch, ruleStore, _, _ := createSchedule(evalAppliedChan, sender) + ruleStore.PutRule(context.Background(), rule) + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + ruleInfo := newAlertRuleInfo(ctx) + + go func() { + _ = ruleInfo.ruleRoutine(rule.GetKey(), sch) + }() + + ruleInfo.evalCh <- &evaluation{ + scheduledAt: sch.clock.Now(), + rule: rule, + } + + waitForTimeChannel(t, evalAppliedChan) + + sender.AssertNumberOfCalls(t, "Send", 1) + args, ok := sender.Calls()[0].Arguments[2].(definitions.PostableAlerts) + require.Truef(t, ok, fmt.Sprintf("expected argument of function was supposed to be 'definitions.PostableAlerts' but got %T", sender.Calls()[0].Arguments[2])) + + require.Len(t, args.PostableAlerts, 1) + }) + }) + + t.Run("when there are no alerts to send it should not call notifiers", func(t *testing.T) { + rule := models.AlertRuleGen(withQueryForState(t, eval.Normal))() + + evalAppliedChan := make(chan time.Time) + + sender := NewSyncAlertsSenderMock() + sender.EXPECT().Send(mock.Anything, rule.GetKey(), mock.Anything).Return() + + sch, ruleStore, _, _ := createSchedule(evalAppliedChan, sender) + ruleStore.PutRule(context.Background(), rule) + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + ruleInfo := newAlertRuleInfo(ctx) + + go func() { + _ = ruleInfo.ruleRoutine(rule.GetKey(), sch) + }() + + ruleInfo.evalCh <- &evaluation{ + scheduledAt: sch.clock.Now(), + rule: rule, + } + + waitForTimeChannel(t, evalAppliedChan) + + sender.AssertNotCalled(t, "Send", mock.Anything, mock.Anything) + + require.NotEmpty(t, sch.stateManager.GetStatesForRuleUID(rule.OrgID, rule.UID)) + }) +} diff --git a/pkg/services/ngalert/schedule/schedule.go b/pkg/services/ngalert/schedule/schedule.go index b08383db95..c25631302f 100644 --- a/pkg/services/ngalert/schedule/schedule.go +++ b/pkg/services/ngalert/schedule/schedule.go @@ -2,27 +2,20 @@ package schedule import ( "context" - "errors" "fmt" "net/url" "time" "github.com/benbjohnson/clock" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/codes" - "go.opentelemetry.io/otel/trace" "golang.org/x/sync/errgroup" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" - "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/eval" "github.com/grafana/grafana/pkg/services/ngalert/metrics" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/state" - "github.com/grafana/grafana/pkg/services/org" - "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/util/ticker" ) @@ -256,7 +249,7 @@ func (sch *schedule) processTick(ctx context.Context, dispatcherGroup *errgroup. if newRoutine && !invalidInterval { dispatcherGroup.Go(func() error { - return sch.ruleRoutine(key, ruleInfo) + return ruleInfo.ruleRoutine(key, sch) }) } @@ -343,272 +336,3 @@ func (sch *schedule) processTick(ctx context.Context, dispatcherGroup *errgroup. sch.deleteAlertRule(toDelete...) return readyToRun, registeredDefinitions, updatedRules } - -//nolint:gocyclo -func (sch *schedule) ruleRoutine(key ngmodels.AlertRuleKey, ruleInfo *alertRuleInfo) error { - grafanaCtx := ngmodels.WithRuleKey(ruleInfo.ctx, key) - logger := sch.log.FromContext(grafanaCtx) - logger.Debug("Alert rule routine started") - - orgID := fmt.Sprint(key.OrgID) - evalTotal := sch.metrics.EvalTotal.WithLabelValues(orgID) - evalDuration := sch.metrics.EvalDuration.WithLabelValues(orgID) - evalTotalFailures := sch.metrics.EvalFailures.WithLabelValues(orgID) - processDuration := sch.metrics.ProcessDuration.WithLabelValues(orgID) - sendDuration := sch.metrics.SendDuration.WithLabelValues(orgID) - - notify := func(states []state.StateTransition) { - expiredAlerts := state.FromAlertsStateToStoppedAlert(states, sch.appURL, sch.clock) - if len(expiredAlerts.PostableAlerts) > 0 { - sch.alertsSender.Send(grafanaCtx, key, expiredAlerts) - } - } - - resetState := func(ctx context.Context, isPaused bool) { - rule := sch.schedulableAlertRules.get(key) - reason := ngmodels.StateReasonUpdated - if isPaused { - reason = ngmodels.StateReasonPaused - } - states := sch.stateManager.ResetStateByRuleUID(ctx, rule, reason) - notify(states) - } - - evaluate := func(ctx context.Context, f fingerprint, attempt int64, e *evaluation, span trace.Span, retry bool) error { - logger := logger.New("version", e.rule.Version, "fingerprint", f, "attempt", attempt, "now", e.scheduledAt).FromContext(ctx) - start := sch.clock.Now() - - evalCtx := eval.NewContextWithPreviousResults(ctx, SchedulerUserFor(e.rule.OrgID), sch.newLoadedMetricsReader(e.rule)) - if sch.evaluatorFactory == nil { - panic("evalfactory nil") - } - ruleEval, err := sch.evaluatorFactory.Create(evalCtx, e.rule.GetEvalCondition()) - var results eval.Results - var dur time.Duration - if err != nil { - dur = sch.clock.Now().Sub(start) - logger.Error("Failed to build rule evaluator", "error", err) - } else { - results, err = ruleEval.Evaluate(ctx, e.scheduledAt) - dur = sch.clock.Now().Sub(start) - if err != nil { - logger.Error("Failed to evaluate rule", "error", err, "duration", dur) - } - } - - evalTotal.Inc() - evalDuration.Observe(dur.Seconds()) - - if ctx.Err() != nil { // check if the context is not cancelled. The evaluation can be a long-running task. - span.SetStatus(codes.Error, "rule evaluation cancelled") - logger.Debug("Skip updating the state because the context has been cancelled") - return nil - } - - if err != nil || results.HasErrors() { - evalTotalFailures.Inc() - - // Only retry (return errors) if this isn't the last attempt, otherwise skip these return operations. - if retry { - // The only thing that can return non-nil `err` from ruleEval.Evaluate is the server side expression pipeline. - // This includes transport errors such as transient network errors. - if err != nil { - span.SetStatus(codes.Error, "rule evaluation failed") - span.RecordError(err) - return fmt.Errorf("server side expressions pipeline returned an error: %w", err) - } - - // If the pipeline executed successfully but have other types of errors that can be retryable, we should do so. - if !results.HasNonRetryableErrors() { - span.SetStatus(codes.Error, "rule evaluation failed") - span.RecordError(err) - return fmt.Errorf("the result-set has errors that can be retried: %w", results.Error()) - } - } - - // If results is nil, we assume that the error must be from the SSE pipeline (ruleEval.Evaluate) which is the only code that can actually return an `err`. - if results == nil { - results = append(results, eval.NewResultFromError(err, e.scheduledAt, dur)) - } - - // If err is nil, we assume that the SSS pipeline succeeded and that the error must be embedded in the results. - if err == nil { - err = results.Error() - } - - span.SetStatus(codes.Error, "rule evaluation failed") - span.RecordError(err) - } else { - logger.Debug("Alert rule evaluated", "results", results, "duration", dur) - span.AddEvent("rule evaluated", trace.WithAttributes( - attribute.Int64("results", int64(len(results))), - )) - } - start = sch.clock.Now() - processedStates := sch.stateManager.ProcessEvalResults( - ctx, - e.scheduledAt, - e.rule, - results, - state.GetRuleExtraLabels(logger, e.rule, e.folderTitle, !sch.disableGrafanaFolder), - ) - processDuration.Observe(sch.clock.Now().Sub(start).Seconds()) - - start = sch.clock.Now() - alerts := state.FromStateTransitionToPostableAlerts(processedStates, sch.stateManager, sch.appURL) - span.AddEvent("results processed", trace.WithAttributes( - attribute.Int64("state_transitions", int64(len(processedStates))), - attribute.Int64("alerts_to_send", int64(len(alerts.PostableAlerts))), - )) - if len(alerts.PostableAlerts) > 0 { - sch.alertsSender.Send(ctx, key, alerts) - } - sendDuration.Observe(sch.clock.Now().Sub(start).Seconds()) - - return nil - } - - evalRunning := false - var currentFingerprint fingerprint - defer sch.stopApplied(key) - for { - select { - // used by external services (API) to notify that rule is updated. - case ctx := <-ruleInfo.updateCh: - if currentFingerprint == ctx.Fingerprint { - logger.Info("Rule's fingerprint has not changed. Skip resetting the state", "currentFingerprint", currentFingerprint) - continue - } - - logger.Info("Clearing the state of the rule because it was updated", "isPaused", ctx.IsPaused, "fingerprint", ctx.Fingerprint) - // clear the state. So the next evaluation will start from the scratch. - resetState(grafanaCtx, ctx.IsPaused) - currentFingerprint = ctx.Fingerprint - // evalCh - used by the scheduler to signal that evaluation is needed. - case ctx, ok := <-ruleInfo.evalCh: - if !ok { - logger.Debug("Evaluation channel has been closed. Exiting") - return nil - } - if evalRunning { - continue - } - - func() { - evalRunning = true - defer func() { - evalRunning = false - sch.evalApplied(key, ctx.scheduledAt) - }() - - for attempt := int64(1); attempt <= sch.maxAttempts; attempt++ { - isPaused := ctx.rule.IsPaused - f := ruleWithFolder{ctx.rule, ctx.folderTitle}.Fingerprint() - // Do not clean up state if the eval loop has just started. - var needReset bool - if currentFingerprint != 0 && currentFingerprint != f { - logger.Debug("Got a new version of alert rule. Clear up the state", "fingerprint", f) - needReset = true - } - // We need to reset state if the loop has started and the alert is already paused. It can happen, - // if we have an alert with state and we do file provision with stateful Grafana, that state - // lingers in DB and won't be cleaned up until next alert rule update. - needReset = needReset || (currentFingerprint == 0 && isPaused) - if needReset { - resetState(grafanaCtx, isPaused) - } - currentFingerprint = f - if isPaused { - logger.Debug("Skip rule evaluation because it is paused") - return - } - - fpStr := currentFingerprint.String() - utcTick := ctx.scheduledAt.UTC().Format(time.RFC3339Nano) - tracingCtx, span := sch.tracer.Start(grafanaCtx, "alert rule execution", trace.WithAttributes( - attribute.String("rule_uid", ctx.rule.UID), - attribute.Int64("org_id", ctx.rule.OrgID), - attribute.Int64("rule_version", ctx.rule.Version), - attribute.String("rule_fingerprint", fpStr), - attribute.String("tick", utcTick), - )) - - // Check before any execution if the context was cancelled so that we don't do any evaluations. - if tracingCtx.Err() != nil { - span.SetStatus(codes.Error, "rule evaluation cancelled") - span.End() - logger.Error("Skip evaluation and updating the state because the context has been cancelled", "version", ctx.rule.Version, "fingerprint", f, "attempt", attempt, "now", ctx.scheduledAt) - return - } - - retry := attempt < sch.maxAttempts - err := evaluate(tracingCtx, f, attempt, ctx, span, retry) - // This is extremely confusing - when we exhaust all retry attempts, or we have no retryable errors - // we return nil - so technically, this is meaningless to know whether the evaluation has errors or not. - span.End() - if err == nil { - return - } - - logger.Error("Failed to evaluate rule", "version", ctx.rule.Version, "fingerprint", f, "attempt", attempt, "now", ctx.scheduledAt, "error", err) - select { - case <-tracingCtx.Done(): - logger.Error("Context has been cancelled while backing off", "version", ctx.rule.Version, "fingerprint", f, "attempt", attempt, "now", ctx.scheduledAt) - return - case <-time.After(retryDelay): - continue - } - } - }() - - case <-grafanaCtx.Done(): - // clean up the state only if the reason for stopping the evaluation loop is that the rule was deleted - if errors.Is(grafanaCtx.Err(), errRuleDeleted) { - // We do not want a context to be unbounded which could potentially cause a go routine running - // indefinitely. 1 minute is an almost randomly chosen timeout, big enough to cover the majority of the - // cases. - ctx, cancelFunc := context.WithTimeout(context.Background(), time.Minute) - defer cancelFunc() - states := sch.stateManager.DeleteStateByRuleUID(ngmodels.WithRuleKey(ctx, key), key, ngmodels.StateReasonRuleDeleted) - notify(states) - } - logger.Debug("Stopping alert rule routine") - return nil - } - } -} - -// evalApplied is only used on tests. -func (sch *schedule) evalApplied(alertDefKey ngmodels.AlertRuleKey, now time.Time) { - if sch.evalAppliedFunc == nil { - return - } - - sch.evalAppliedFunc(alertDefKey, now) -} - -// stopApplied is only used on tests. -func (sch *schedule) stopApplied(alertDefKey ngmodels.AlertRuleKey) { - if sch.stopAppliedFunc == nil { - return - } - - sch.stopAppliedFunc(alertDefKey) -} - -func SchedulerUserFor(orgID int64) *user.SignedInUser { - return &user.SignedInUser{ - UserID: -1, - IsServiceAccount: true, - Login: "grafana_scheduler", - OrgID: orgID, - OrgRole: org.RoleAdmin, - Permissions: map[int64]map[string][]string{ - orgID: { - datasources.ActionQuery: []string{ - datasources.ScopeAll, - }, - }, - }, - } -} diff --git a/pkg/services/ngalert/schedule/schedule_unit_test.go b/pkg/services/ngalert/schedule/schedule_unit_test.go index 1c40e798c1..787149d919 100644 --- a/pkg/services/ngalert/schedule/schedule_unit_test.go +++ b/pkg/services/ngalert/schedule/schedule_unit_test.go @@ -11,11 +11,8 @@ import ( "time" "github.com/benbjohnson/clock" - alertingModels "github.com/grafana/alerting/models" - "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/testutil" - prometheusModel "github.com/prometheus/common/model" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -26,14 +23,12 @@ import ( "github.com/grafana/grafana/pkg/infra/tracing" datasources "github.com/grafana/grafana/pkg/services/datasources/fakes" "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/eval" "github.com/grafana/grafana/pkg/services/ngalert/metrics" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/state" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" "github.com/grafana/grafana/pkg/setting" - "github.com/grafana/grafana/pkg/util" ) type evalAppliedInfo struct { @@ -361,471 +356,6 @@ func TestProcessTicks(t *testing.T) { }) } -func TestSchedule_ruleRoutine(t *testing.T) { - createSchedule := func( - evalAppliedChan chan time.Time, - senderMock *SyncAlertsSenderMock, - ) (*schedule, *fakeRulesStore, *state.FakeInstanceStore, prometheus.Gatherer) { - ruleStore := newFakeRulesStore() - instanceStore := &state.FakeInstanceStore{} - - registry := prometheus.NewPedanticRegistry() - sch := setupScheduler(t, ruleStore, instanceStore, registry, senderMock, nil) - sch.evalAppliedFunc = func(key models.AlertRuleKey, t time.Time) { - evalAppliedChan <- t - } - return sch, ruleStore, instanceStore, registry - } - - // normal states do not include NoData and Error because currently it is not possible to perform any sensible test - normalStates := []eval.State{eval.Normal, eval.Alerting, eval.Pending} - allStates := [...]eval.State{eval.Normal, eval.Alerting, eval.Pending, eval.NoData, eval.Error} - - for _, evalState := range normalStates { - // TODO rewrite when we are able to mock/fake state manager - t.Run(fmt.Sprintf("when rule evaluation happens (evaluation state %s)", evalState), func(t *testing.T) { - evalAppliedChan := make(chan time.Time) - sch, ruleStore, instanceStore, reg := createSchedule(evalAppliedChan, nil) - - rule := models.AlertRuleGen(withQueryForState(t, evalState))() - ruleStore.PutRule(context.Background(), rule) - folderTitle := ruleStore.getNamespaceTitle(rule.NamespaceUID) - ctx, cancel := context.WithCancel(context.Background()) - t.Cleanup(cancel) - ruleInfo := newAlertRuleInfo(ctx) - go func() { - _ = sch.ruleRoutine(rule.GetKey(), ruleInfo) - }() - - expectedTime := time.UnixMicro(rand.Int63()) - - ruleInfo.evalCh <- &evaluation{ - scheduledAt: expectedTime, - rule: rule, - folderTitle: folderTitle, - } - - actualTime := waitForTimeChannel(t, evalAppliedChan) - require.Equal(t, expectedTime, actualTime) - - t.Run("it should add extra labels", func(t *testing.T) { - states := sch.stateManager.GetStatesForRuleUID(rule.OrgID, rule.UID) - for _, s := range states { - assert.Equal(t, rule.UID, s.Labels[alertingModels.RuleUIDLabel]) - assert.Equal(t, rule.NamespaceUID, s.Labels[alertingModels.NamespaceUIDLabel]) - assert.Equal(t, rule.Title, s.Labels[prometheusModel.AlertNameLabel]) - assert.Equal(t, folderTitle, s.Labels[models.FolderTitleLabel]) - } - }) - - t.Run("it should process evaluation results via state manager", func(t *testing.T) { - // TODO rewrite when we are able to mock/fake state manager - states := sch.stateManager.GetStatesForRuleUID(rule.OrgID, rule.UID) - require.Len(t, states, 1) - s := states[0] - require.Equal(t, rule.UID, s.AlertRuleUID) - require.Len(t, s.Results, 1) - var expectedStatus = evalState - if evalState == eval.Pending { - expectedStatus = eval.Alerting - } - require.Equal(t, expectedStatus.String(), s.Results[0].EvaluationState.String()) - require.Equal(t, expectedTime, s.Results[0].EvaluationTime) - }) - t.Run("it should save alert instances to storage", func(t *testing.T) { - // TODO rewrite when we are able to mock/fake state manager - states := sch.stateManager.GetStatesForRuleUID(rule.OrgID, rule.UID) - require.Len(t, states, 1) - s := states[0] - - var cmd *models.AlertInstance - for _, op := range instanceStore.RecordedOps() { - switch q := op.(type) { - case models.AlertInstance: - cmd = &q - } - if cmd != nil { - break - } - } - - require.NotNil(t, cmd) - t.Logf("Saved alert instances: %v", cmd) - require.Equal(t, rule.OrgID, cmd.RuleOrgID) - require.Equal(t, expectedTime, cmd.LastEvalTime) - require.Equal(t, rule.UID, cmd.RuleUID) - require.Equal(t, evalState.String(), string(cmd.CurrentState)) - require.Equal(t, s.Labels, data.Labels(cmd.Labels)) - }) - - t.Run("it reports metrics", func(t *testing.T) { - // duration metric has 0 values because of mocked clock that do not advance - expectedMetric := fmt.Sprintf( - `# HELP grafana_alerting_rule_evaluation_duration_seconds The time to evaluate a rule. - # TYPE grafana_alerting_rule_evaluation_duration_seconds histogram - grafana_alerting_rule_evaluation_duration_seconds_bucket{org="%[1]d",le="0.01"} 1 - grafana_alerting_rule_evaluation_duration_seconds_bucket{org="%[1]d",le="0.1"} 1 - grafana_alerting_rule_evaluation_duration_seconds_bucket{org="%[1]d",le="0.5"} 1 - grafana_alerting_rule_evaluation_duration_seconds_bucket{org="%[1]d",le="1"} 1 - grafana_alerting_rule_evaluation_duration_seconds_bucket{org="%[1]d",le="5"} 1 - grafana_alerting_rule_evaluation_duration_seconds_bucket{org="%[1]d",le="10"} 1 - grafana_alerting_rule_evaluation_duration_seconds_bucket{org="%[1]d",le="15"} 1 - grafana_alerting_rule_evaluation_duration_seconds_bucket{org="%[1]d",le="30"} 1 - grafana_alerting_rule_evaluation_duration_seconds_bucket{org="%[1]d",le="60"} 1 - grafana_alerting_rule_evaluation_duration_seconds_bucket{org="%[1]d",le="120"} 1 - grafana_alerting_rule_evaluation_duration_seconds_bucket{org="%[1]d",le="180"} 1 - grafana_alerting_rule_evaluation_duration_seconds_bucket{org="%[1]d",le="240"} 1 - grafana_alerting_rule_evaluation_duration_seconds_bucket{org="%[1]d",le="300"} 1 - grafana_alerting_rule_evaluation_duration_seconds_bucket{org="%[1]d",le="+Inf"} 1 - grafana_alerting_rule_evaluation_duration_seconds_sum{org="%[1]d"} 0 - grafana_alerting_rule_evaluation_duration_seconds_count{org="%[1]d"} 1 - # HELP grafana_alerting_rule_evaluation_failures_total The total number of rule evaluation failures. - # TYPE grafana_alerting_rule_evaluation_failures_total counter - grafana_alerting_rule_evaluation_failures_total{org="%[1]d"} 0 - # HELP grafana_alerting_rule_evaluations_total The total number of rule evaluations. - # TYPE grafana_alerting_rule_evaluations_total counter - grafana_alerting_rule_evaluations_total{org="%[1]d"} 1 - # HELP grafana_alerting_rule_process_evaluation_duration_seconds The time to process the evaluation results for a rule. - # TYPE grafana_alerting_rule_process_evaluation_duration_seconds histogram - grafana_alerting_rule_process_evaluation_duration_seconds_bucket{org="%[1]d",le="0.01"} 1 - grafana_alerting_rule_process_evaluation_duration_seconds_bucket{org="%[1]d",le="0.1"} 1 - grafana_alerting_rule_process_evaluation_duration_seconds_bucket{org="%[1]d",le="0.5"} 1 - grafana_alerting_rule_process_evaluation_duration_seconds_bucket{org="%[1]d",le="1"} 1 - grafana_alerting_rule_process_evaluation_duration_seconds_bucket{org="%[1]d",le="5"} 1 - grafana_alerting_rule_process_evaluation_duration_seconds_bucket{org="%[1]d",le="10"} 1 - grafana_alerting_rule_process_evaluation_duration_seconds_bucket{org="%[1]d",le="15"} 1 - grafana_alerting_rule_process_evaluation_duration_seconds_bucket{org="%[1]d",le="30"} 1 - grafana_alerting_rule_process_evaluation_duration_seconds_bucket{org="%[1]d",le="60"} 1 - grafana_alerting_rule_process_evaluation_duration_seconds_bucket{org="%[1]d",le="120"} 1 - grafana_alerting_rule_process_evaluation_duration_seconds_bucket{org="%[1]d",le="180"} 1 - grafana_alerting_rule_process_evaluation_duration_seconds_bucket{org="%[1]d",le="240"} 1 - grafana_alerting_rule_process_evaluation_duration_seconds_bucket{org="%[1]d",le="300"} 1 - grafana_alerting_rule_process_evaluation_duration_seconds_bucket{org="%[1]d",le="+Inf"} 1 - grafana_alerting_rule_process_evaluation_duration_seconds_sum{org="%[1]d"} 0 - grafana_alerting_rule_process_evaluation_duration_seconds_count{org="%[1]d"} 1 - # HELP grafana_alerting_rule_send_alerts_duration_seconds The time to send the alerts to Alertmanager. - # TYPE grafana_alerting_rule_send_alerts_duration_seconds histogram - grafana_alerting_rule_send_alerts_duration_seconds_bucket{org="%[1]d",le="0.01"} 1 - grafana_alerting_rule_send_alerts_duration_seconds_bucket{org="%[1]d",le="0.1"} 1 - grafana_alerting_rule_send_alerts_duration_seconds_bucket{org="%[1]d",le="0.5"} 1 - grafana_alerting_rule_send_alerts_duration_seconds_bucket{org="%[1]d",le="1"} 1 - grafana_alerting_rule_send_alerts_duration_seconds_bucket{org="%[1]d",le="5"} 1 - grafana_alerting_rule_send_alerts_duration_seconds_bucket{org="%[1]d",le="10"} 1 - grafana_alerting_rule_send_alerts_duration_seconds_bucket{org="%[1]d",le="15"} 1 - grafana_alerting_rule_send_alerts_duration_seconds_bucket{org="%[1]d",le="30"} 1 - grafana_alerting_rule_send_alerts_duration_seconds_bucket{org="%[1]d",le="60"} 1 - grafana_alerting_rule_send_alerts_duration_seconds_bucket{org="%[1]d",le="120"} 1 - grafana_alerting_rule_send_alerts_duration_seconds_bucket{org="%[1]d",le="180"} 1 - grafana_alerting_rule_send_alerts_duration_seconds_bucket{org="%[1]d",le="240"} 1 - grafana_alerting_rule_send_alerts_duration_seconds_bucket{org="%[1]d",le="300"} 1 - grafana_alerting_rule_send_alerts_duration_seconds_bucket{org="%[1]d",le="+Inf"} 1 - grafana_alerting_rule_send_alerts_duration_seconds_sum{org="%[1]d"} 0 - grafana_alerting_rule_send_alerts_duration_seconds_count{org="%[1]d"} 1 - `, rule.OrgID) - - err := testutil.GatherAndCompare(reg, bytes.NewBufferString(expectedMetric), "grafana_alerting_rule_evaluation_duration_seconds", "grafana_alerting_rule_evaluations_total", "grafana_alerting_rule_evaluation_failures_total", "grafana_alerting_rule_process_evaluation_duration_seconds", "grafana_alerting_rule_send_alerts_duration_seconds") - require.NoError(t, err) - }) - }) - } - - t.Run("should exit", func(t *testing.T) { - t.Run("and not clear the state if parent context is cancelled", func(t *testing.T) { - stoppedChan := make(chan error) - sch, _, _, _ := createSchedule(make(chan time.Time), nil) - - rule := models.AlertRuleGen()() - _ = sch.stateManager.ProcessEvalResults(context.Background(), sch.clock.Now(), rule, eval.GenerateResults(rand.Intn(5)+1, eval.ResultGen(eval.WithEvaluatedAt(sch.clock.Now()))), nil) - expectedStates := sch.stateManager.GetStatesForRuleUID(rule.OrgID, rule.UID) - require.NotEmpty(t, expectedStates) - - ctx, cancel := context.WithCancel(context.Background()) - ruleInfo := newAlertRuleInfo(ctx) - go func() { - err := sch.ruleRoutine(models.AlertRuleKey{}, ruleInfo) - stoppedChan <- err - }() - - cancel() - err := waitForErrChannel(t, stoppedChan) - require.NoError(t, err) - require.Equal(t, len(expectedStates), len(sch.stateManager.GetStatesForRuleUID(rule.OrgID, rule.UID))) - }) - t.Run("and clean up the state if delete is cancellation reason for inner context", func(t *testing.T) { - stoppedChan := make(chan error) - sch, _, _, _ := createSchedule(make(chan time.Time), nil) - - rule := models.AlertRuleGen()() - _ = sch.stateManager.ProcessEvalResults(context.Background(), sch.clock.Now(), rule, eval.GenerateResults(rand.Intn(5)+1, eval.ResultGen(eval.WithEvaluatedAt(sch.clock.Now()))), nil) - require.NotEmpty(t, sch.stateManager.GetStatesForRuleUID(rule.OrgID, rule.UID)) - - ruleInfo := newAlertRuleInfo(context.Background()) - go func() { - err := sch.ruleRoutine(rule.GetKey(), ruleInfo) - stoppedChan <- err - }() - - ruleInfo.stop(errRuleDeleted) - err := waitForErrChannel(t, stoppedChan) - require.NoError(t, err) - - require.Empty(t, sch.stateManager.GetStatesForRuleUID(rule.OrgID, rule.UID)) - }) - }) - - t.Run("when a message is sent to update channel", func(t *testing.T) { - rule := models.AlertRuleGen(withQueryForState(t, eval.Normal))() - folderTitle := "folderName" - ruleFp := ruleWithFolder{rule, folderTitle}.Fingerprint() - - evalAppliedChan := make(chan time.Time) - - sender := NewSyncAlertsSenderMock() - sender.EXPECT().Send(mock.Anything, rule.GetKey(), mock.Anything).Return() - - sch, ruleStore, _, _ := createSchedule(evalAppliedChan, sender) - ruleStore.PutRule(context.Background(), rule) - sch.schedulableAlertRules.set([]*models.AlertRule{rule}, map[models.FolderKey]string{rule.GetFolderKey(): folderTitle}) - ctx, cancel := context.WithCancel(context.Background()) - t.Cleanup(cancel) - ruleInfo := newAlertRuleInfo(ctx) - - go func() { - _ = sch.ruleRoutine(rule.GetKey(), ruleInfo) - }() - - // init evaluation loop so it got the rule version - ruleInfo.evalCh <- &evaluation{ - scheduledAt: sch.clock.Now(), - rule: rule, - folderTitle: folderTitle, - } - - waitForTimeChannel(t, evalAppliedChan) - - // define some state - states := make([]*state.State, 0, len(allStates)) - for _, s := range allStates { - for i := 0; i < 2; i++ { - states = append(states, &state.State{ - AlertRuleUID: rule.UID, - CacheID: util.GenerateShortUID(), - OrgID: rule.OrgID, - State: s, - StartsAt: sch.clock.Now(), - EndsAt: sch.clock.Now().Add(time.Duration(rand.Intn(25)+5) * time.Second), - Labels: rule.Labels, - }) - } - } - sch.stateManager.Put(states) - - states = sch.stateManager.GetStatesForRuleUID(rule.OrgID, rule.UID) - expectedToBeSent := 0 - for _, s := range states { - if s.State == eval.Normal || s.State == eval.Pending { - continue - } - expectedToBeSent++ - } - require.Greaterf(t, expectedToBeSent, 0, "State manager was expected to return at least one state that can be expired") - - t.Run("should do nothing if version in channel is the same", func(t *testing.T) { - ruleInfo.updateCh <- ruleVersionAndPauseStatus{ruleFp, false} - ruleInfo.updateCh <- ruleVersionAndPauseStatus{ruleFp, false} // second time just to make sure that previous messages were handled - - actualStates := sch.stateManager.GetStatesForRuleUID(rule.OrgID, rule.UID) - require.Len(t, actualStates, len(states)) - - sender.AssertNotCalled(t, "Send", mock.Anything, mock.Anything) - }) - - t.Run("should clear the state and expire firing alerts if version in channel is greater", func(t *testing.T) { - ruleInfo.updateCh <- ruleVersionAndPauseStatus{ruleFp + 1, false} - - require.Eventually(t, func() bool { - return len(sender.Calls()) > 0 - }, 5*time.Second, 100*time.Millisecond) - - require.Empty(t, sch.stateManager.GetStatesForRuleUID(rule.OrgID, rule.UID)) - sender.AssertNumberOfCalls(t, "Send", 1) - args, ok := sender.Calls()[0].Arguments[2].(definitions.PostableAlerts) - require.Truef(t, ok, fmt.Sprintf("expected argument of function was supposed to be 'definitions.PostableAlerts' but got %T", sender.Calls()[0].Arguments[2])) - require.Len(t, args.PostableAlerts, expectedToBeSent) - }) - }) - - t.Run("when evaluation fails", func(t *testing.T) { - rule := models.AlertRuleGen(withQueryForState(t, eval.Error))() - rule.ExecErrState = models.ErrorErrState - - evalAppliedChan := make(chan time.Time) - - sender := NewSyncAlertsSenderMock() - sender.EXPECT().Send(mock.Anything, rule.GetKey(), mock.Anything).Return() - - sch, ruleStore, _, reg := createSchedule(evalAppliedChan, sender) - sch.maxAttempts = 3 - ruleStore.PutRule(context.Background(), rule) - ctx, cancel := context.WithCancel(context.Background()) - t.Cleanup(cancel) - ruleInfo := newAlertRuleInfo(ctx) - - go func() { - _ = sch.ruleRoutine(rule.GetKey(), ruleInfo) - }() - - ruleInfo.evalCh <- &evaluation{ - scheduledAt: sch.clock.Now(), - rule: rule, - } - - waitForTimeChannel(t, evalAppliedChan) - - t.Run("it should increase failure counter", func(t *testing.T) { - // duration metric has 0 values because of mocked clock that do not advance - expectedMetric := fmt.Sprintf( - `# HELP grafana_alerting_rule_evaluation_duration_seconds The time to evaluate a rule. - # TYPE grafana_alerting_rule_evaluation_duration_seconds histogram - grafana_alerting_rule_evaluation_duration_seconds_bucket{org="%[1]d",le="0.01"} 3 - grafana_alerting_rule_evaluation_duration_seconds_bucket{org="%[1]d",le="0.1"} 3 - grafana_alerting_rule_evaluation_duration_seconds_bucket{org="%[1]d",le="0.5"} 3 - grafana_alerting_rule_evaluation_duration_seconds_bucket{org="%[1]d",le="1"} 3 - grafana_alerting_rule_evaluation_duration_seconds_bucket{org="%[1]d",le="5"} 3 - grafana_alerting_rule_evaluation_duration_seconds_bucket{org="%[1]d",le="10"} 3 - grafana_alerting_rule_evaluation_duration_seconds_bucket{org="%[1]d",le="15"} 3 - grafana_alerting_rule_evaluation_duration_seconds_bucket{org="%[1]d",le="30"} 3 - grafana_alerting_rule_evaluation_duration_seconds_bucket{org="%[1]d",le="60"} 3 - grafana_alerting_rule_evaluation_duration_seconds_bucket{org="%[1]d",le="120"} 3 - grafana_alerting_rule_evaluation_duration_seconds_bucket{org="%[1]d",le="180"} 3 - grafana_alerting_rule_evaluation_duration_seconds_bucket{org="%[1]d",le="240"} 3 - grafana_alerting_rule_evaluation_duration_seconds_bucket{org="%[1]d",le="300"} 3 - grafana_alerting_rule_evaluation_duration_seconds_bucket{org="%[1]d",le="+Inf"} 3 - grafana_alerting_rule_evaluation_duration_seconds_sum{org="%[1]d"} 0 - grafana_alerting_rule_evaluation_duration_seconds_count{org="%[1]d"} 3 - # HELP grafana_alerting_rule_evaluation_failures_total The total number of rule evaluation failures. - # TYPE grafana_alerting_rule_evaluation_failures_total counter - grafana_alerting_rule_evaluation_failures_total{org="%[1]d"} 3 - # HELP grafana_alerting_rule_evaluations_total The total number of rule evaluations. - # TYPE grafana_alerting_rule_evaluations_total counter - grafana_alerting_rule_evaluations_total{org="%[1]d"} 3 - # HELP grafana_alerting_rule_process_evaluation_duration_seconds The time to process the evaluation results for a rule. - # TYPE grafana_alerting_rule_process_evaluation_duration_seconds histogram - grafana_alerting_rule_process_evaluation_duration_seconds_bucket{org="%[1]d",le="0.01"} 1 - grafana_alerting_rule_process_evaluation_duration_seconds_bucket{org="%[1]d",le="0.1"} 1 - grafana_alerting_rule_process_evaluation_duration_seconds_bucket{org="%[1]d",le="0.5"} 1 - grafana_alerting_rule_process_evaluation_duration_seconds_bucket{org="%[1]d",le="1"} 1 - grafana_alerting_rule_process_evaluation_duration_seconds_bucket{org="%[1]d",le="5"} 1 - grafana_alerting_rule_process_evaluation_duration_seconds_bucket{org="%[1]d",le="10"} 1 - grafana_alerting_rule_process_evaluation_duration_seconds_bucket{org="%[1]d",le="15"} 1 - grafana_alerting_rule_process_evaluation_duration_seconds_bucket{org="%[1]d",le="30"} 1 - grafana_alerting_rule_process_evaluation_duration_seconds_bucket{org="%[1]d",le="60"} 1 - grafana_alerting_rule_process_evaluation_duration_seconds_bucket{org="%[1]d",le="120"} 1 - grafana_alerting_rule_process_evaluation_duration_seconds_bucket{org="%[1]d",le="180"} 1 - grafana_alerting_rule_process_evaluation_duration_seconds_bucket{org="%[1]d",le="240"} 1 - grafana_alerting_rule_process_evaluation_duration_seconds_bucket{org="%[1]d",le="300"} 1 - grafana_alerting_rule_process_evaluation_duration_seconds_bucket{org="%[1]d",le="+Inf"} 1 - grafana_alerting_rule_process_evaluation_duration_seconds_sum{org="%[1]d"} 0 - grafana_alerting_rule_process_evaluation_duration_seconds_count{org="%[1]d"} 1 - # HELP grafana_alerting_rule_send_alerts_duration_seconds The time to send the alerts to Alertmanager. - # TYPE grafana_alerting_rule_send_alerts_duration_seconds histogram - grafana_alerting_rule_send_alerts_duration_seconds_bucket{org="%[1]d",le="0.01"} 1 - grafana_alerting_rule_send_alerts_duration_seconds_bucket{org="%[1]d",le="0.1"} 1 - grafana_alerting_rule_send_alerts_duration_seconds_bucket{org="%[1]d",le="0.5"} 1 - grafana_alerting_rule_send_alerts_duration_seconds_bucket{org="%[1]d",le="1"} 1 - grafana_alerting_rule_send_alerts_duration_seconds_bucket{org="%[1]d",le="5"} 1 - grafana_alerting_rule_send_alerts_duration_seconds_bucket{org="%[1]d",le="10"} 1 - grafana_alerting_rule_send_alerts_duration_seconds_bucket{org="%[1]d",le="15"} 1 - grafana_alerting_rule_send_alerts_duration_seconds_bucket{org="%[1]d",le="30"} 1 - grafana_alerting_rule_send_alerts_duration_seconds_bucket{org="%[1]d",le="60"} 1 - grafana_alerting_rule_send_alerts_duration_seconds_bucket{org="%[1]d",le="120"} 1 - grafana_alerting_rule_send_alerts_duration_seconds_bucket{org="%[1]d",le="180"} 1 - grafana_alerting_rule_send_alerts_duration_seconds_bucket{org="%[1]d",le="240"} 1 - grafana_alerting_rule_send_alerts_duration_seconds_bucket{org="%[1]d",le="300"} 1 - grafana_alerting_rule_send_alerts_duration_seconds_bucket{org="%[1]d",le="+Inf"} 1 - grafana_alerting_rule_send_alerts_duration_seconds_sum{org="%[1]d"} 0 - grafana_alerting_rule_send_alerts_duration_seconds_count{org="%[1]d"} 1 - `, rule.OrgID) - - err := testutil.GatherAndCompare(reg, bytes.NewBufferString(expectedMetric), "grafana_alerting_rule_evaluation_duration_seconds", "grafana_alerting_rule_evaluations_total", "grafana_alerting_rule_evaluation_failures_total", "grafana_alerting_rule_process_evaluation_duration_seconds", "grafana_alerting_rule_send_alerts_duration_seconds") - require.NoError(t, err) - }) - - t.Run("it should send special alert DatasourceError", func(t *testing.T) { - sender.AssertNumberOfCalls(t, "Send", 1) - args, ok := sender.Calls()[0].Arguments[2].(definitions.PostableAlerts) - require.Truef(t, ok, fmt.Sprintf("expected argument of function was supposed to be 'definitions.PostableAlerts' but got %T", sender.Calls()[0].Arguments[2])) - assert.Len(t, args.PostableAlerts, 1) - assert.Equal(t, state.ErrorAlertName, args.PostableAlerts[0].Labels[prometheusModel.AlertNameLabel]) - }) - }) - - t.Run("when there are alerts that should be firing", func(t *testing.T) { - t.Run("it should call sender", func(t *testing.T) { - // eval.Alerting makes state manager to create notifications for alertmanagers - rule := models.AlertRuleGen(withQueryForState(t, eval.Alerting))() - - evalAppliedChan := make(chan time.Time) - - sender := NewSyncAlertsSenderMock() - sender.EXPECT().Send(mock.Anything, rule.GetKey(), mock.Anything).Return() - - sch, ruleStore, _, _ := createSchedule(evalAppliedChan, sender) - ruleStore.PutRule(context.Background(), rule) - ctx, cancel := context.WithCancel(context.Background()) - t.Cleanup(cancel) - ruleInfo := newAlertRuleInfo(ctx) - - go func() { - _ = sch.ruleRoutine(rule.GetKey(), ruleInfo) - }() - - ruleInfo.evalCh <- &evaluation{ - scheduledAt: sch.clock.Now(), - rule: rule, - } - - waitForTimeChannel(t, evalAppliedChan) - - sender.AssertNumberOfCalls(t, "Send", 1) - args, ok := sender.Calls()[0].Arguments[2].(definitions.PostableAlerts) - require.Truef(t, ok, fmt.Sprintf("expected argument of function was supposed to be 'definitions.PostableAlerts' but got %T", sender.Calls()[0].Arguments[2])) - - require.Len(t, args.PostableAlerts, 1) - }) - }) - - t.Run("when there are no alerts to send it should not call notifiers", func(t *testing.T) { - rule := models.AlertRuleGen(withQueryForState(t, eval.Normal))() - - evalAppliedChan := make(chan time.Time) - - sender := NewSyncAlertsSenderMock() - sender.EXPECT().Send(mock.Anything, rule.GetKey(), mock.Anything).Return() - - sch, ruleStore, _, _ := createSchedule(evalAppliedChan, sender) - ruleStore.PutRule(context.Background(), rule) - ctx, cancel := context.WithCancel(context.Background()) - t.Cleanup(cancel) - ruleInfo := newAlertRuleInfo(ctx) - - go func() { - _ = sch.ruleRoutine(rule.GetKey(), ruleInfo) - }() - - ruleInfo.evalCh <- &evaluation{ - scheduledAt: sch.clock.Now(), - rule: rule, - } - - waitForTimeChannel(t, evalAppliedChan) - - sender.AssertNotCalled(t, "Send", mock.Anything, mock.Anything) - - require.NotEmpty(t, sch.stateManager.GetStatesForRuleUID(rule.OrgID, rule.UID)) - }) -} - func TestSchedule_deleteAlertRule(t *testing.T) { t.Run("when rule exists", func(t *testing.T) { t.Run("it should stop evaluation loop and remove the controller from registry", func(t *testing.T) { From 9264e2a3bdbebedad152cab8b462d5d2b1f0ca3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 5 Mar 2024 08:49:22 +0100 Subject: [PATCH 0382/1406] Table: Custom headerComponent field config option (#83254) * Table: Custom headerComponent field config option * Table custom header component (#83830) * Add tests, fix bug --------- Co-authored-by: Galen Kistler <109082771+gtk-grafana@users.noreply.github.com> Co-authored-by: Galen --- .../src/components/Table/HeaderRow.tsx | 51 ++++++++++++------- .../src/components/Table/Table.test.tsx | 23 ++++++++- .../grafana-ui/src/components/Table/types.ts | 10 ++++ 3 files changed, 64 insertions(+), 20 deletions(-) diff --git a/packages/grafana-ui/src/components/Table/HeaderRow.tsx b/packages/grafana-ui/src/components/Table/HeaderRow.tsx index 62f61e494d..2bd1ef0480 100644 --- a/packages/grafana-ui/src/components/Table/HeaderRow.tsx +++ b/packages/grafana-ui/src/components/Table/HeaderRow.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { HeaderGroup, Column } from 'react-table'; +import { Field } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { getFieldTypeIcon } from '../../types'; @@ -8,6 +9,7 @@ import { Icon } from '../Icon/Icon'; import { Filter } from './Filter'; import { TableStyles } from './styles'; +import { TableFieldOptions } from './types'; export interface HeaderRowProps { headerGroups: HeaderGroup[]; @@ -43,7 +45,8 @@ export const HeaderRow = (props: HeaderRowProps) => { function renderHeaderCell(column: any, tableStyles: TableStyles, showTypeIcons?: boolean) { const headerProps = column.getHeaderProps(); - const field = column.field ?? null; + const field: Field = column.field ?? null; + const tableFieldOptions: TableFieldOptions | undefined = field?.config.custom; if (column.canResize) { headerProps.style.userSelect = column.isResizing ? 'none' : 'auto'; // disables selecting text while resizing @@ -51,27 +54,37 @@ function renderHeaderCell(column: any, tableStyles: TableStyles, showTypeIcons?: headerProps.style.position = 'absolute'; headerProps.style.justifyContent = column.justifyContent; + headerProps.style.left = column.totalLeft; + + let headerContent = column.render('Header'); + + let sortHeaderContent = column.canSort && ( + <> + + {column.canFilter && } + + ); + if (sortHeaderContent && tableFieldOptions?.headerComponent) { + sortHeaderContent = ; + } else if (tableFieldOptions?.headerComponent) { + headerContent = ; + } return (
- {column.canSort && ( - <> - - {column.canFilter && } - - )} - {!column.canSort && column.render('Header')} + {column.canSort && sortHeaderContent} + {!column.canSort && headerContent} {!column.canSort && column.canFilter && } {column.canResize &&
}
diff --git a/packages/grafana-ui/src/components/Table/Table.test.tsx b/packages/grafana-ui/src/components/Table/Table.test.tsx index 1f8c73c142..3d3fc8f5e1 100644 --- a/packages/grafana-ui/src/components/Table/Table.test.tsx +++ b/packages/grafana-ui/src/components/Table/Table.test.tsx @@ -4,8 +4,10 @@ import React from 'react'; import { applyFieldOverrides, createTheme, DataFrame, FieldType, toDataFrame } from '@grafana/data'; +import { Icon } from '../Icon/Icon'; + import { Table } from './Table'; -import { Props } from './types'; +import { CustomHeaderRendererProps, Props } from './types'; // mock transition styles to ensure consistent behaviour in unit tests jest.mock('@floating-ui/react', () => ({ @@ -35,6 +37,12 @@ const dataFrameData = { config: { custom: { filterable: false, + headerComponent: (props: CustomHeaderRendererProps) => ( + + {props.defaultContent} + + + ), }, links: [ { @@ -238,6 +246,19 @@ describe('Table', () => { }); }); + describe('custom header', () => { + it('Should be rendered', async () => { + getTestContext(); + + await userEvent.click(within(getColumnHeader(/temperature/)).getByText(/temperature/i)); + await userEvent.click(within(getColumnHeader(/temperature/)).getByText(/temperature/i)); + + const rows = within(getTable()).getAllByRole('row'); + expect(rows).toHaveLength(5); + expect(within(rows[0]).getByLabelText('header-icon')).toBeInTheDocument(); + }); + }); + describe('on filtering', () => { it('the rows should be filtered', async () => { getTestContext({ diff --git a/packages/grafana-ui/src/components/Table/types.ts b/packages/grafana-ui/src/components/Table/types.ts index b3d1f9f3be..563a0d9ee4 100644 --- a/packages/grafana-ui/src/components/Table/types.ts +++ b/packages/grafana-ui/src/components/Table/types.ts @@ -124,9 +124,19 @@ export interface TableCustomCellOptions { type: schema.TableCellDisplayMode.Custom; } +/** + * @alpha + * Props that will be passed to the TableCustomCellOptions.cellComponent when rendered. + */ +export interface CustomHeaderRendererProps { + field: Field; + defaultContent: React.ReactNode; +} + // As cue/schema cannot define function types (as main point of schema is to be serializable) we have to extend the // types here with the dynamic API. This means right now this is not usable as a table panel option for example. export type TableCellOptions = schema.TableCellOptions | TableCustomCellOptions; export type TableFieldOptions = Omit & { cellOptions: TableCellOptions; + headerComponent?: React.ComponentType; }; From 22074c5026cd2e6fd2a05d6bc2ef255642d32496 Mon Sep 17 00:00:00 2001 From: Karl Persson Date: Tue, 5 Mar 2024 08:50:19 +0100 Subject: [PATCH 0383/1406] RBAC: add debug log for permission evaluation (#83880) * fix: add debug log when evaluating permissions that includes target permissions --- pkg/services/accesscontrol/acimpl/accesscontrol.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/pkg/services/accesscontrol/acimpl/accesscontrol.go b/pkg/services/accesscontrol/acimpl/accesscontrol.go index f5d8774292..4a6c6a0998 100644 --- a/pkg/services/accesscontrol/acimpl/accesscontrol.go +++ b/pkg/services/accesscontrol/acimpl/accesscontrol.go @@ -38,18 +38,17 @@ func (a *AccessControl) Evaluate(ctx context.Context, user identity.Requester, e return false, nil } - namespace, identifier := user.GetNamespacedID() - // If the user is in no organization, then the evaluation must happen based on the user's global permissions permissions := user.GetPermissions() if user.GetOrgID() == accesscontrol.NoOrgID { permissions = user.GetGlobalPermissions() } if len(permissions) == 0 { - a.log.Debug("No permissions set for entity", "namespace", namespace, "id", identifier, "orgID", user.GetOrgID(), "login", user.GetLogin()) + a.debug(ctx, user, "No permissions set", evaluator) return false, nil } + a.debug(ctx, user, "Evaluating permissions", evaluator) // Test evaluation without scope resolver first, this will prevent 403 for wildcard scopes when resource does not exist if evaluator.Evaluate(permissions) { return true, nil @@ -63,9 +62,15 @@ func (a *AccessControl) Evaluate(ctx context.Context, user identity.Requester, e return false, err } + a.debug(ctx, user, "Evaluating resolved permissions", resolvedEvaluator) return resolvedEvaluator.Evaluate(permissions), nil } func (a *AccessControl) RegisterScopeAttributeResolver(prefix string, resolver accesscontrol.ScopeAttributeResolver) { a.resolvers.AddScopeAttributeResolver(prefix, resolver) } + +func (a *AccessControl) debug(ctx context.Context, ident identity.Requester, msg string, eval accesscontrol.Evaluator) { + namespace, id := ident.GetNamespacedID() + a.log.FromContext(ctx).Debug(msg, "namespace", namespace, "id", id, "orgID", ident.GetOrgID(), eval.GoString()) +} From e930b49db38c1bdb76ca85302217f1e184cf2b26 Mon Sep 17 00:00:00 2001 From: Yulia Shanyrova Date: Tue, 5 Mar 2024 09:27:34 +0100 Subject: [PATCH 0384/1406] Plugins: Add intraMode 1 into fuzzy search for plugins (#83846) add intraMode 1 into fuzzy search for plugins --- public/app/features/plugins/admin/helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/plugins/admin/helpers.ts b/public/app/features/plugins/admin/helpers.ts index 86c0c92b3e..950496c28e 100644 --- a/public/app/features/plugins/admin/helpers.ts +++ b/public/app/features/plugins/admin/helpers.ts @@ -376,7 +376,7 @@ function getPluginDetailsForFuzzySearch(plugins: CatalogPlugin[]): string[] { } export function filterByKeyword(plugins: CatalogPlugin[], query: string) { const dataArray = getPluginDetailsForFuzzySearch(plugins); - let uf = new uFuzzy({}); + let uf = new uFuzzy({ intraMode: 1, intraSub: 0 }); let idxs = uf.filter(dataArray, query); if (idxs === null) { return null; From c9ac6dd3e7464278800d2a853b6d19ba55a7854a Mon Sep 17 00:00:00 2001 From: Oscar Kilhed Date: Tue, 5 Mar 2024 10:46:03 +0100 Subject: [PATCH 0385/1406] Dashboard scenes: debounce name validation when saving dashboards (#83580) * Dashboard scenes: debounce name validation when saving dashboards * add newline --- .../features/dashboard-scene/saving/SaveDashboardAsForm.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/public/app/features/dashboard-scene/saving/SaveDashboardAsForm.tsx b/public/app/features/dashboard-scene/saving/SaveDashboardAsForm.tsx index 14f7e3e3bb..5f201c9f6a 100644 --- a/public/app/features/dashboard-scene/saving/SaveDashboardAsForm.tsx +++ b/public/app/features/dashboard-scene/saving/SaveDashboardAsForm.tsx @@ -1,3 +1,4 @@ +import debounce from 'debounce-promise'; import React from 'react'; import { UseFormSetValue, useForm } from 'react-hook-form'; @@ -29,7 +30,7 @@ export interface Props { export function SaveDashboardAsForm({ dashboard, drawer, changeInfo }: Props) { const { changedSaveModel } = changeInfo; - const { register, handleSubmit, setValue, formState, getValues, watch } = useForm({ + const { register, handleSubmit, setValue, formState, getValues, watch, trigger } = useForm({ mode: 'onBlur', defaultValues: { title: changeInfo.isNew ? changedSaveModel.title! : `${changedSaveModel.title} Copy`, @@ -98,6 +99,9 @@ export function SaveDashboardAsForm({ dashboard, drawer, changeInfo }: Props) { { + trigger('title'); + }, 400)} autoFocus /> From 019c9618f051e3819be2a2a5fbfd06a3fa5b3aa6 Mon Sep 17 00:00:00 2001 From: Pepe Cano <825430+ppcano@users.noreply.github.com> Date: Tue, 5 Mar 2024 10:48:04 +0100 Subject: [PATCH 0386/1406] Alerting docs: improve visibility on the distinct options to edit provisioned resources (#83839) Alerting docs: specify the distinct options to edit provisioned resources --- .../set-up/provision-alerting-resources/_index.md | 6 ++++-- .../export-alerting-resources/index.md | 14 +++++++++++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/docs/sources/alerting/set-up/provision-alerting-resources/_index.md b/docs/sources/alerting/set-up/provision-alerting-resources/_index.md index 6fa5e36332..4987b50cbe 100644 --- a/docs/sources/alerting/set-up/provision-alerting-resources/_index.md +++ b/docs/sources/alerting/set-up/provision-alerting-resources/_index.md @@ -33,8 +33,10 @@ Choose from the options below to import (or provision) your Grafana Alerting res 1. [Use configuration files to provision your alerting resources][alerting_file_provisioning], such as alert rules and contact points, through files on disk. {{< admonition type="note" >}} - Provisioning with configuration files is not available in Grafana Cloud. - {{< /admonition >}} + + - You cannot edit provisioned resources from files in the Grafana UI. + - Provisioning with configuration files is not available in Grafana Cloud. + {{< /admonition >}} 1. Use [Terraform to provision alerting resources][alerting_tf_provisioning]. diff --git a/docs/sources/alerting/set-up/provision-alerting-resources/export-alerting-resources/index.md b/docs/sources/alerting/set-up/provision-alerting-resources/export-alerting-resources/index.md index e7fe122856..b0a415f133 100644 --- a/docs/sources/alerting/set-up/provision-alerting-resources/export-alerting-resources/index.md +++ b/docs/sources/alerting/set-up/provision-alerting-resources/export-alerting-resources/index.md @@ -20,7 +20,19 @@ weight: 300 # Export alerting resources -Export your alerting resources, such as alert rules, contact points, and notification policies for provisioning, automatically importing single folders and single groups. Use the [Grafana UI](#export-from-the-grafana-ui) or the [HTTP Alerting API](#http-alerting-api) to export these resources. +Export your alerting resources, such as alert rules, contact points, and notification policies for provisioning, automatically importing single folders and single groups. + +There are distinct methods to export your alerting resources: + +- [Grafana UI](#export-from-the-grafana-ui) exports in Terraform format and YAML or JSON formats for file provisioning. +- [HTTP Alerting API](#http-alerting-api) exports in JSON API format used by the HTTP Alerting API. +- [HTTP Alerting API - Export endpoints](#export-api-endpoints) exports in YAML or JSON formats for file provisioning. + +{{< admonition type="note" >}} +Alerting resources imported through [file provisioning](/docs/grafana//alerting/set-up/provision-alerting-resources/file-provisioning) cannot be edited in the Grafana UI. This prevents changes made in the UI from being overridden by file provisioning during Grafana restarts. + +If you need to modify provisioned alerting resources in Grafana, refer to [edit HTTP API alerting resources in the Grafana UI](/docs/grafana//alerting/set-up/provision-alerting-resources/http-api-provisioning#edit-resources-in-the-grafana-ui) or to [edit Terraform alerting resources in the Grafana UI](/docs/grafana//alerting/set-up/provision-alerting-resources/terraform-provisioning#enable-editing-resources-in-the-grafana-ui). +{{< /admonition >}} ## Export from the Grafana UI From bc7eacfcbdd0680e9ea6401d3c36936ccd5933da Mon Sep 17 00:00:00 2001 From: Oscar Kilhed Date: Tue, 5 Mar 2024 10:51:22 +0100 Subject: [PATCH 0387/1406] Dashboard Scenes: Add model to library panel inspect json (#83536) * Add model to library panel inspect json * Add missing information from library panel when inspecting its JSON * minor refactor * refactor inspect library panel model to show the model that was fetched from the api * nit: improve comment * fix library panel import path --------- Co-authored-by: Alexandra Vargas --- .../inspect/InspectJsonTab.tsx | 20 ++++++++++++++----- .../scene/DashboardSceneUrlSync.ts | 14 +++++++++++++ .../dashboard-scene/scene/LibraryVizPanel.tsx | 7 ++++--- .../app/features/library-panels/state/api.ts | 6 ++++++ 4 files changed, 39 insertions(+), 8 deletions(-) diff --git a/public/app/features/dashboard-scene/inspect/InspectJsonTab.tsx b/public/app/features/dashboard-scene/inspect/InspectJsonTab.tsx index 74725d1d6c..8710778da5 100644 --- a/public/app/features/dashboard-scene/inspect/InspectJsonTab.tsx +++ b/public/app/features/dashboard-scene/inspect/InspectJsonTab.tsx @@ -17,6 +17,7 @@ import { sceneUtils, VizPanel, } from '@grafana/scenes'; +import { LibraryPanel } from '@grafana/schema/'; import { Button, CodeEditor, Field, Select, useStyles2 } from '@grafana/ui'; import { t } from 'app/core/internationalization'; import { getPanelDataFrames } from 'app/features/dashboard/components/HelpWizard/utils'; @@ -257,17 +258,26 @@ function libraryPanelChildToLegacyRepresentation(panel: VizPanel<{}, {}>) { } const gridItem = panel.parent.parent; - const libraryPanelObj = gridItemToPanel(gridItem); - const panelObj = vizPanelToPanel(panel); - - panelObj.gridPos = { + const gridPos = { x: gridItem.state.x || 0, y: gridItem.state.y || 0, h: gridItem.state.height || 0, w: gridItem.state.width || 0, }; + const libraryPanelObj = vizPanelToLibraryPanel(panel); + const panelObj = vizPanelToPanel(panel, gridPos, false, gridItem); + + return { libraryPanel: { ...libraryPanelObj }, ...panelObj }; +} - return { ...libraryPanelObj, ...panelObj }; +function vizPanelToLibraryPanel(panel: VizPanel): LibraryPanel { + if (!(panel.parent instanceof LibraryVizPanel)) { + throw new Error('Panel not a child of LibraryVizPanel'); + } + if (!panel.parent.state._loadedPanel) { + throw new Error('Library panel not loaded'); + } + return panel.parent.state._loadedPanel; } function hasGridPosChanged(a: SceneGridItemStateLike, b: SceneGridItemStateLike) { diff --git a/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts b/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts index f818fd2787..fb02736233 100644 --- a/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts +++ b/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts @@ -66,6 +66,20 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler { return; } + if (isLibraryPanelChild(panel)) { + this._handleLibraryPanel(panel, (p) => { + if (p.state.key === undefined) { + // Inspect drawer require a panel key to be set + throw new Error('library panel key is undefined'); + } + const drawer = new PanelInspectDrawer({ + $behaviors: [new ResolveInspectPanelByKey({ panelKey: p.state.key })], + }); + this._scene.setState({ overlay: drawer, inspectPanelKey: p.state.key }); + }); + return; + } + update.inspectPanelKey = values.inspect; update.overlay = new PanelInspectDrawer({ $behaviors: [new ResolveInspectPanelByKey({ panelKey: values.inspect })], diff --git a/public/app/features/dashboard-scene/scene/LibraryVizPanel.tsx b/public/app/features/dashboard-scene/scene/LibraryVizPanel.tsx index b5ab559f34..b7692f08a9 100644 --- a/public/app/features/dashboard-scene/scene/LibraryVizPanel.tsx +++ b/public/app/features/dashboard-scene/scene/LibraryVizPanel.tsx @@ -10,6 +10,7 @@ import { VizPanelMenu, VizPanelState, } from '@grafana/scenes'; +import { LibraryPanel } from '@grafana/schema'; import { PanelModel } from 'app/features/dashboard/state'; import { getLibraryPanel } from 'app/features/library-panels/state/api'; @@ -28,7 +29,7 @@ interface LibraryVizPanelState extends SceneObjectState { panel?: VizPanel; isLoaded?: boolean; panelKey: string; - _loadedVersion?: number; + _loadedPanel?: LibraryPanel; } export class LibraryVizPanel extends SceneObjectBase { @@ -56,7 +57,7 @@ export class LibraryVizPanel extends SceneObjectBase { try { const libPanel = await getLibraryPanel(this.state.uid, true); - if (this.state._loadedVersion === libPanel.version) { + if (this.state._loadedPanel?.version === libPanel.version) { return; } @@ -107,7 +108,7 @@ export class LibraryVizPanel extends SceneObjectBase { }); } - this.setState({ panel, _loadedVersion: libPanel.version, isLoaded: true }); + this.setState({ panel, _loadedPanel: libPanel, isLoaded: true }); } catch (err) { vizPanel.setState({ _pluginLoadError: `Unable to load library panel: ${this.state.uid}`, diff --git a/public/app/features/library-panels/state/api.ts b/public/app/features/library-panels/state/api.ts index 27fca15ef0..1b3b77a027 100644 --- a/public/app/features/library-panels/state/api.ts +++ b/public/app/features/library-panels/state/api.ts @@ -65,6 +65,12 @@ export async function getLibraryPanel(uid: string, isHandled = false): Promise + delete model.gridPos; + delete model.id; + delete model.libraryPanel; + dash.destroy(); // kill event listeners return { ...result, From dc4c539d4626fa96a475401c1bc69d698775b494 Mon Sep 17 00:00:00 2001 From: ismail simsek Date: Tue, 5 Mar 2024 11:22:33 +0100 Subject: [PATCH 0388/1406] InfluxDB: Fix sql query generation by adding quotes around the identifiers (#83765) * Quote the identifiers * wrap where filter with quotes * fix query generation --- .../influxdb/fsql/datasource.flightsql.ts | 4 +- .../datasource/influxdb/fsql/sqlUtil.test.ts | 55 ++++++++++++++++++- .../datasource/influxdb/fsql/sqlUtil.ts | 26 ++++++--- 3 files changed, 72 insertions(+), 13 deletions(-) diff --git a/public/app/plugins/datasource/influxdb/fsql/datasource.flightsql.ts b/public/app/plugins/datasource/influxdb/fsql/datasource.flightsql.ts index 476df864d0..18c881e2db 100644 --- a/public/app/plugins/datasource/influxdb/fsql/datasource.flightsql.ts +++ b/public/app/plugins/datasource/influxdb/fsql/datasource.flightsql.ts @@ -1,12 +1,12 @@ import { DataSourceInstanceSettings, TimeRange } from '@grafana/data'; import { CompletionItemKind, LanguageDefinition, TableIdentifier } from '@grafana/experimental'; import { getTemplateSrv, TemplateSrv } from '@grafana/runtime'; -import { DB, SqlDatasource, SQLQuery, formatSQL } from '@grafana/sql'; +import { DB, formatSQL, SqlDatasource, SQLQuery } from '@grafana/sql'; import { mapFieldsToTypes } from './fields'; import { buildColumnQuery, buildTableQuery } from './flightsqlMetaQuery'; import { getSqlCompletionProvider } from './sqlCompletionProvider'; -import { quoteLiteral, quoteIdentifierIfNecessary, toRawSql } from './sqlUtil'; +import { quoteIdentifierIfNecessary, quoteLiteral, toRawSql } from './sqlUtil'; import { FlightSQLOptions } from './types'; export class FlightSQLDatasource extends SqlDatasource { diff --git a/public/app/plugins/datasource/influxdb/fsql/sqlUtil.test.ts b/public/app/plugins/datasource/influxdb/fsql/sqlUtil.test.ts index 57fe15c670..4864004b8e 100644 --- a/public/app/plugins/datasource/influxdb/fsql/sqlUtil.test.ts +++ b/public/app/plugins/datasource/influxdb/fsql/sqlUtil.test.ts @@ -1,10 +1,10 @@ -import { SQLQuery, QueryEditorExpressionType } from '@grafana/sql'; +import { QueryEditorExpressionType, SQLQuery } from '@grafana/sql'; import { toRawSql } from './sqlUtil'; describe('toRawSql', () => { it('should render sql properly', () => { - const expected = 'SELECT host FROM iox.value1 WHERE time >= $__timeFrom AND time <= $__timeTo LIMIT 50'; + const expected = 'SELECT "host" FROM "value1" WHERE "time" >= $__timeFrom AND "time" <= $__timeTo LIMIT 50'; const testQuery: SQLQuery = { refId: 'A', sql: { @@ -27,4 +27,55 @@ describe('toRawSql', () => { const result = toRawSql(testQuery); expect(result).toEqual(expected); }); + + it('should wrap the identifiers with quote', () => { + const expected = 'SELECT "host" FROM "TestValue" WHERE "time" >= $__timeFrom AND "time" <= $__timeTo LIMIT 50'; + const testQuery: SQLQuery = { + refId: 'A', + sql: { + limit: 50, + columns: [ + { + parameters: [ + { + name: 'host', + type: QueryEditorExpressionType.FunctionParameter, + }, + ], + type: QueryEditorExpressionType.Function, + }, + ], + }, + dataset: 'iox', + table: 'TestValue', + }; + const result = toRawSql(testQuery); + expect(result).toEqual(expected); + }); + + it('should wrap filters in where', () => { + const expected = `SELECT "host" FROM "TestValue" WHERE "time" >= $__timeFrom AND "time" <= $__timeTo AND ("sensor_id" = '12' AND "sensor_id" = '23') LIMIT 50`; + const testQuery: SQLQuery = { + refId: 'A', + sql: { + limit: 50, + columns: [ + { + parameters: [ + { + name: 'host', + type: QueryEditorExpressionType.FunctionParameter, + }, + ], + type: QueryEditorExpressionType.Function, + }, + ], + whereString: `(sensor_id = '12' AND sensor_id = '23')`, + }, + dataset: 'iox', + table: 'TestValue', + }; + const result = toRawSql(testQuery); + expect(result).toEqual(expected); + }); }); diff --git a/public/app/plugins/datasource/influxdb/fsql/sqlUtil.ts b/public/app/plugins/datasource/influxdb/fsql/sqlUtil.ts index 81ad267376..41317454a9 100644 --- a/public/app/plugins/datasource/influxdb/fsql/sqlUtil.ts +++ b/public/app/plugins/datasource/influxdb/fsql/sqlUtil.ts @@ -1,6 +1,6 @@ import { isEmpty } from 'lodash'; -import { SQLQuery, createSelectClause, haveColumns } from '@grafana/sql'; +import { createSelectClause, haveColumns, SQLQuery } from '@grafana/sql'; // remove identifier quoting from identifier to use in metadata queries export function unquoteIdentifier(value: string) { @@ -17,7 +17,7 @@ export function quoteLiteral(value: string) { return "'" + value.replace(/'/g, "''") + "'"; } -export function toRawSql({ sql, dataset, table }: SQLQuery): string { +export function toRawSql({ sql, table }: SQLQuery): string { let rawQuery = ''; // Return early with empty string if there is no sql column @@ -25,25 +25,33 @@ export function toRawSql({ sql, dataset, table }: SQLQuery): string { return rawQuery; } - rawQuery += createSelectClause(sql.columns); + // wrapping the column name with quotes + const sc = sql.columns.map((c) => ({ ...c, parameters: c.parameters?.map((p) => ({ ...p, name: `"${p.name}"` })) })); + rawQuery += createSelectClause(sc); - if (dataset && table) { - rawQuery += `FROM ${dataset}.${table} `; + if (table) { + rawQuery += `FROM "${table}" `; } // $__timeFrom and $__timeTo will be interpolated on the backend - rawQuery += `WHERE time >= $__timeFrom AND time <= $__timeTo `; + rawQuery += `WHERE "time" >= $__timeFrom AND "time" <= $__timeTo `; if (sql.whereString) { - rawQuery += `AND ${sql.whereString} `; + // whereString is generated by the react-awesome-query-builder + // we use SQLWhereRow as a common component + // in order to not mess with common component here we just modify the string + const wherePattern = new RegExp('(\\s?)([^\\(]\\S+)(\\s?=)', 'g'); + const subst = `$1"$2"$3`; + const whereString = sql.whereString.replace(wherePattern, subst); + rawQuery += `AND ${whereString} `; } if (sql.groupBy?.[0]?.property.name) { - const groupBy = sql.groupBy.map((g) => g.property.name).filter((g) => !isEmpty(g)); + const groupBy = sql.groupBy.map((g) => `"${g.property.name}"`).filter((g) => !isEmpty(g)); rawQuery += `GROUP BY ${groupBy.join(', ')} `; } if (sql.orderBy?.property.name) { - rawQuery += `ORDER BY ${sql.orderBy.property.name} `; + rawQuery += `ORDER BY "${sql.orderBy.property.name}" `; } if (sql.orderBy?.property.name && sql.orderByDirection) { From 112c0e7a79c00c7057ef362b6b8a9b7e4471f1b5 Mon Sep 17 00:00:00 2001 From: Ivan Ortega Alba Date: Tue, 5 Mar 2024 12:10:46 +0100 Subject: [PATCH 0389/1406] Dashboards: Auto-generate get stuck and quick feedback actions doesn't respond (#83879) * Update the component only when the response is fully generated * Fix quick feedback action doesn't respond * Fix history not displaying after the second click * Fix the history that moves when regenerating --------- Co-authored-by: Adela Almasan <88068998+adela-almasan@users.noreply.github.com> --- .../components/GenAI/GenAIButton.test.tsx | 45 +++++++++++++++++-- .../components/GenAI/GenAIButton.tsx | 38 ++++++++-------- .../components/GenAI/GenAIHistory.tsx | 35 +++++---------- .../dashboard/components/GenAI/hooks.ts | 13 +++++- 4 files changed, 85 insertions(+), 46 deletions(-) diff --git a/public/app/features/dashboard/components/GenAI/GenAIButton.test.tsx b/public/app/features/dashboard/components/GenAI/GenAIButton.test.tsx index 79d4773138..fc76cf269d 100644 --- a/public/app/features/dashboard/components/GenAI/GenAIButton.test.tsx +++ b/public/app/features/dashboard/components/GenAI/GenAIButton.test.tsx @@ -173,13 +173,11 @@ describe('GenAIButton', () => { await waitFor(() => expect(getByRole('button')).toBeEnabled()); }); - it('should call onGenerate when the text is generating', async () => { + it('should not call onGenerate when the text is generating', async () => { const onGenerate = jest.fn(); setup({ onGenerate, messages: [], eventTrackingSrc: eventTrackingSrc }); - await waitFor(() => expect(onGenerate).toHaveBeenCalledTimes(1)); - - expect(onGenerate).toHaveBeenCalledWith('Some incomplete generated text'); + await waitFor(() => expect(onGenerate).not.toHaveBeenCalledTimes(1)); }); it('should stop generating when clicking the button', async () => { @@ -191,6 +189,45 @@ describe('GenAIButton', () => { expect(setShouldStopMock).toHaveBeenCalledTimes(1); expect(setShouldStopMock).toHaveBeenCalledWith(true); + expect(onGenerate).not.toHaveBeenCalled(); + }); + }); + + describe('when it is completed from generating data', () => { + const setShouldStopMock = jest.fn(); + + beforeEach(() => { + jest.mocked(useOpenAIStream).mockReturnValue({ + messages: [], + error: undefined, + streamStatus: StreamStatus.COMPLETED, + reply: 'Some completed generated text', + setMessages: jest.fn(), + setStopGeneration: setShouldStopMock, + value: { + enabled: true, + stream: new Observable().subscribe(), + }, + }); + }); + + it('should render improve text ', async () => { + setup(); + + waitFor(async () => expect(await screen.findByText('Improve')).toBeInTheDocument()); + }); + + it('should enable the button', async () => { + setup(); + waitFor(async () => expect(await screen.findByRole('button')).toBeEnabled()); + }); + + it('should call onGenerate when the text is completed', async () => { + const onGenerate = jest.fn(); + setup({ onGenerate, messages: [], eventTrackingSrc: eventTrackingSrc }); + + await waitFor(() => expect(onGenerate).toHaveBeenCalledTimes(1)); + expect(onGenerate).toHaveBeenCalledWith('Some completed generated text'); }); }); diff --git a/public/app/features/dashboard/components/GenAI/GenAIButton.tsx b/public/app/features/dashboard/components/GenAI/GenAIButton.tsx index 3c2647a9a2..c8e43b17c6 100644 --- a/public/app/features/dashboard/components/GenAI/GenAIButton.tsx +++ b/public/app/features/dashboard/components/GenAI/GenAIButton.tsx @@ -54,10 +54,11 @@ export const GenAIButton = ({ } = useOpenAIStream(model, temperature); const [history, setHistory] = useState([]); - const [showHistory, setShowHistory] = useState(true); + const [showHistory, setShowHistory] = useState(false); const hasHistory = history.length > 0; - const isFirstHistoryEntry = streamStatus === StreamStatus.GENERATING && !hasHistory; + const isGenerating = streamStatus === StreamStatus.GENERATING; + const isFirstHistoryEntry = !hasHistory; const isButtonDisabled = disabled || (value && !value.enabled && !error); const reportInteraction = (item: AutoGenerateItem) => reportAutoGenerateInteraction(eventTrackingSrc, item); @@ -69,19 +70,17 @@ export const GenAIButton = ({ onClickProp?.(e); setMessages(typeof messages === 'function' ? messages() : messages); } else { - if (setShowHistory) { - setShowHistory(true); - } + setShowHistory(true); } } const buttonItem = error ? AutoGenerateItem.erroredRetryButton - : isFirstHistoryEntry + : isGenerating ? AutoGenerateItem.stopGenerationButton - : hasHistory - ? AutoGenerateItem.improveButton - : AutoGenerateItem.autoGenerateButton; + : isFirstHistoryEntry + ? AutoGenerateItem.autoGenerateButton + : AutoGenerateItem.improveButton; reportInteraction(buttonItem); }; @@ -96,10 +95,10 @@ export const GenAIButton = ({ useEffect(() => { // Todo: Consider other options for `"` sanitation - if (isFirstHistoryEntry && reply) { + if (streamStatus === StreamStatus.COMPLETED && reply) { onGenerate(sanitizeReply(reply)); } - }, [streamStatus, reply, onGenerate, isFirstHistoryEntry]); + }, [streamStatus, reply, onGenerate]); useEffect(() => { if (streamStatus === StreamStatus.COMPLETED) { @@ -119,7 +118,7 @@ export const GenAIButton = ({ }; const getIcon = () => { - if (isFirstHistoryEntry) { + if (isGenerating) { return undefined; } if (error || (value && !value?.enabled)) { @@ -135,7 +134,7 @@ export const GenAIButton = ({ buttonText = 'Retry'; } - if (isFirstHistoryEntry) { + if (isGenerating) { buttonText = STOP_GENERATION_TEXT; } @@ -175,9 +174,11 @@ export const GenAIButton = ({ eventTrackingSrc={eventTrackingSrc} /> } - placement="bottom-start" + placement="left-start" fitContent={true} - show={showHistory ? undefined : false} + show={showHistory} + onClose={() => setShowHistory(false)} + onOpen={() => setShowHistory(true)} > {button} @@ -189,8 +190,8 @@ export const GenAIButton = ({ return (
- {isFirstHistoryEntry && } - {!hasHistory && ( + {isGenerating && } + {isFirstHistoryEntry ? ( {button} + ) : ( + renderButtonWithToggletip() )} - {hasHistory && renderButtonWithToggletip()}
); }; diff --git a/public/app/features/dashboard/components/GenAI/GenAIHistory.tsx b/public/app/features/dashboard/components/GenAI/GenAIHistory.tsx index 8b688f6992..3d1780d828 100644 --- a/public/app/features/dashboard/components/GenAI/GenAIHistory.tsx +++ b/public/app/features/dashboard/components/GenAI/GenAIHistory.tsx @@ -2,18 +2,7 @@ import { css } from '@emotion/css'; import React, { useEffect, useState } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; -import { - Alert, - Button, - HorizontalGroup, - Icon, - IconButton, - Input, - Text, - TextLink, - useStyles2, - VerticalGroup, -} from '@grafana/ui'; +import { Alert, Button, Icon, IconButton, Input, Stack, Text, TextLink, useStyles2 } from '@grafana/ui'; import { STOP_GENERATION_TEXT } from './GenAIButton'; import { GenerationHistoryCarousel } from './GenerationHistoryCarousel'; @@ -100,7 +89,9 @@ export const GenAIHistory = ({ const onGenerateWithFeedback = (suggestion: string | QuickFeedbackType) => { if (suggestion !== QuickFeedbackType.Regenerate) { - messages = [...messages, ...getFeedbackMessage(history[currentIndex], suggestion)]; + messages = [...messages, ...getFeedbackMessage(history[currentIndex - 1], suggestion)]; + } else { + messages = [...messages, ...getFeedbackMessage(history[currentIndex - 1], 'Please, regenerate')]; } setMessages(messages); @@ -122,13 +113,11 @@ export const GenAIHistory = ({ return (
{showError && ( -
- - -
Sorry, I was unable to complete your request. Please try again.
-
-
-
+ + +

Sorry, I was unable to complete your request. Please try again.

+
+
)}
- + - +
@@ -186,7 +175,7 @@ const getStyles = (theme: GrafanaTheme2) => ({ display: 'flex', flexDirection: 'column', width: 520, - height: 250, + maxHeight: 350, // This is the space the footer height paddingBottom: 35, }), diff --git a/public/app/features/dashboard/components/GenAI/hooks.ts b/public/app/features/dashboard/components/GenAI/hooks.ts index f8ae8968ee..80bcc806d3 100644 --- a/public/app/features/dashboard/components/GenAI/hooks.ts +++ b/public/app/features/dashboard/components/GenAI/hooks.ts @@ -52,6 +52,8 @@ export function useOpenAIStream( const [streamStatus, setStreamStatus] = useState(StreamStatus.IDLE); const [error, setError] = useState(); const { error: notifyError } = useAppNotification(); + // Accumulate response and it will only update the state of the attatched component when the stream is completed. + let partialReply = ''; const onError = useCallback( (e: Error) => { @@ -69,6 +71,12 @@ export function useOpenAIStream( [messages, model, temperature, notifyError] ); + useEffect(() => { + if (messages.length > 0) { + setReply(''); + } + }, [messages]); + const { error: enabledError, value: enabled } = useAsync( async () => await isLLMPluginEnabled(), [isLLMPluginEnabled] @@ -102,9 +110,12 @@ export function useOpenAIStream( return { enabled, stream: stream.subscribe({ - next: setReply, + next: (reply) => { + partialReply = reply; + }, error: onError, complete: () => { + setReply(partialReply); setStreamStatus(StreamStatus.COMPLETED); setTimeout(() => { setStreamStatus(StreamStatus.IDLE); From a7c06d26f14b2a9fa8faa929a6c9a0c355018429 Mon Sep 17 00:00:00 2001 From: Ivan Ortega Alba Date: Tue, 5 Mar 2024 13:01:31 +0100 Subject: [PATCH 0390/1406] Dashboards: Add new toggle for dashboard changes out of `dashgpt` toggle (#83897) --- .../feature-toggles/index.md | 1 + .../src/types/featureToggles.gen.ts | 1 + pkg/services/featuremgmt/registry.go | 7 ++++++ pkg/services/featuremgmt/toggles_gen.csv | 1 + pkg/services/featuremgmt/toggles_gen.go | 4 ++++ pkg/services/featuremgmt/toggles_gen.json | 23 +++++++++++++++---- .../SaveDashboard/forms/SaveDashboardForm.tsx | 2 +- 7 files changed, 33 insertions(+), 6 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 7571afd19c..556a0ad684 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -144,6 +144,7 @@ Experimental features might be changed or removed without prior notice. | `metricsSummary` | Enables metrics summary queries in the Tempo data source | | `featureToggleAdminPage` | Enable admin page for managing feature toggles from the Grafana front-end | | `permissionsFilterRemoveSubquery` | Alternative permission filter implementation that does not use subqueries for fetching the dashboard folder | +| `aiGeneratedDashboardChanges` | Enable AI powered features for dashboards to auto-summary changes when saving | | `sseGroupByDatasource` | Send query to the same datasource in a single request when using server side expressions. The `cloudWatchBatchQueries` feature toggle should be enabled if this used with CloudWatch. | | `libraryPanelRBAC` | Enables RBAC support for library panels | | `wargamesTesting` | Placeholder feature flag for internal testing | diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 7fe0246c23..f506005041 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -110,6 +110,7 @@ export interface FeatureToggles { alertingNoDataErrorExecution?: boolean; angularDeprecationUI?: boolean; dashgpt?: boolean; + aiGeneratedDashboardChanges?: boolean; reportingRetries?: boolean; sseGroupByDatasource?: boolean; libraryPanelRBAC?: boolean; diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index f204bcc5c6..e0ed2d0426 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -691,6 +691,13 @@ var ( FrontendOnly: true, Owner: grafanaDashboardsSquad, }, + { + Name: "aiGeneratedDashboardChanges", + Description: "Enable AI powered features for dashboards to auto-summary changes when saving", + Stage: FeatureStageExperimental, + FrontendOnly: true, + Owner: grafanaDashboardsSquad, + }, { Name: "reportingRetries", Description: "Enables rendering retries for the reporting feature", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index b177d1d9f5..e271f075a2 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -91,6 +91,7 @@ influxdbSqlSupport,GA,@grafana/observability-metrics,false,true,false alertingNoDataErrorExecution,GA,@grafana/alerting-squad,false,true,false angularDeprecationUI,GA,@grafana/plugins-platform-backend,false,false,true dashgpt,preview,@grafana/dashboards-squad,false,false,true +aiGeneratedDashboardChanges,experimental,@grafana/dashboards-squad,false,false,true reportingRetries,preview,@grafana/sharing-squad,false,true,false sseGroupByDatasource,experimental,@grafana/observability-metrics,false,false,false libraryPanelRBAC,experimental,@grafana/dashboards-squad,false,true,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 2a0003e4a9..1e0efe5ca4 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -375,6 +375,10 @@ const ( // Enable AI powered features in dashboards FlagDashgpt = "dashgpt" + // FlagAiGeneratedDashboardChanges + // Enable AI powered features for dashboards to auto-summary changes when saving + FlagAiGeneratedDashboardChanges = "aiGeneratedDashboardChanges" + // FlagReportingRetries // Enables rendering retries for the reporting feature FlagReportingRetries = "reportingRetries" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index eb00277196..29aa613186 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -86,7 +86,7 @@ "name": "pluginsInstrumentationStatusSource", "resourceVersion": "1708108588074", "creationTimestamp": "2024-02-16T18:36:28Z", - "deletionTimestamp": "2024-02-29T08:27:47Z" + "deletionTimestamp": "2024-03-05T11:44:12Z" }, "spec": { "description": "Include a status source label for plugin request metrics and logs", @@ -514,7 +514,7 @@ "name": "displayAnonymousStats", "resourceVersion": "1708108588074", "creationTimestamp": "2024-02-16T18:36:28Z", - "deletionTimestamp": "2024-02-29T08:27:47Z" + "deletionTimestamp": "2024-03-05T11:44:12Z" }, "spec": { "description": "Enables anonymous stats to be shown in the UI for Grafana", @@ -1390,7 +1390,7 @@ "name": "traceToMetrics", "resourceVersion": "1708108588074", "creationTimestamp": "2024-02-16T18:36:28Z", - "deletionTimestamp": "2024-02-29T08:27:47Z" + "deletionTimestamp": "2024-03-05T11:44:12Z" }, "spec": { "description": "Enable trace to metrics links", @@ -1519,7 +1519,7 @@ "name": "splitScopes", "resourceVersion": "1708108588074", "creationTimestamp": "2024-02-16T18:36:28Z", - "deletionTimestamp": "2024-02-29T08:27:47Z" + "deletionTimestamp": "2024-03-05T11:44:12Z" }, "spec": { "description": "Support faster dashboard and folder search by splitting permission scopes into parts", @@ -1560,7 +1560,7 @@ "name": "externalServiceAuth", "resourceVersion": "1708108588074", "creationTimestamp": "2024-02-16T18:36:28Z", - "deletionTimestamp": "2024-02-29T08:27:47Z" + "deletionTimestamp": "2024-03-05T11:44:12Z" }, "spec": { "description": "Starts an OAuth2 authentication provider for external services", @@ -2136,6 +2136,19 @@ "stage": "experimental", "codeowner": "@grafana/grafana-app-platform-squad" } + }, + { + "metadata": { + "name": "aiGeneratedDashboardChanges", + "resourceVersion": "1709639042582", + "creationTimestamp": "2024-03-05T11:44:02Z" + }, + "spec": { + "description": "Enable AI powered features for dashboards to auto-summary changes when saving", + "stage": "experimental", + "codeowner": "@grafana/dashboards-squad", + "frontend": true + } } ] } \ No newline at end of file diff --git a/public/app/features/dashboard/components/SaveDashboard/forms/SaveDashboardForm.tsx b/public/app/features/dashboard/components/SaveDashboard/forms/SaveDashboardForm.tsx index f2faf077c8..3f4b172b5a 100644 --- a/public/app/features/dashboard/components/SaveDashboard/forms/SaveDashboardForm.tsx +++ b/public/app/features/dashboard/components/SaveDashboard/forms/SaveDashboardForm.tsx @@ -89,7 +89,7 @@ export const SaveDashboardForm = ({ /> )}
- {config.featureToggles.dashgpt && ( + {config.featureToggles.aiGeneratedDashboardChanges && ( { From 9f73fb65cd22b68129d4c39942753b4b443b8479 Mon Sep 17 00:00:00 2001 From: Sven Grossmann Date: Tue, 5 Mar 2024 13:04:57 +0100 Subject: [PATCH 0391/1406] Loki: Fix permalink timerange when multiple logs share a timestamp (#83847) * Loki: Slide permalink timerange `1ms` to make sure to include line * find previous log with different time * fix test not being agnostic --- .../app/features/explore/Logs/Logs.test.tsx | 60 ++++++++++++++++++- public/app/features/explore/Logs/Logs.tsx | 12 +++- 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/public/app/features/explore/Logs/Logs.test.tsx b/public/app/features/explore/Logs/Logs.test.tsx index f2eb2779f9..a8f70e4727 100644 --- a/public/app/features/explore/Logs/Logs.test.tsx +++ b/public/app/features/explore/Logs/Logs.test.tsx @@ -16,12 +16,22 @@ import { } from '@grafana/data'; import { organizeFieldsTransformer } from '@grafana/data/src/transformations/transformers/organize'; import { config } from '@grafana/runtime'; +import store from 'app/core/store'; import { extractFieldsTransformer } from 'app/features/transformers/extractFields/extractFields'; import { Logs } from './Logs'; import { visualisationTypeKey } from './utils/logs'; import { getMockElasticFrame, getMockLokiFrame } from './utils/testMocks.test'; +jest.mock('app/core/store', () => { + return { + getBool: jest.fn(), + getObject: jest.fn((_a, b) => b), + get: jest.fn(), + set: jest.fn(), + }; +}); + const reportInteraction = jest.fn(); jest.mock('@grafana/runtime', () => ({ ...jest.requireActual('@grafana/runtime'), @@ -379,7 +389,13 @@ describe('Logs', () => { it('should call reportInteraction on permalinkClick', async () => { const panelState = { logs: { id: 'not-included' } }; - setup({ loading: false, panelState }); + const rows = [ + makeLog({ uid: '1', rowId: 'id1', timeEpochMs: 4 }), + makeLog({ uid: '2', rowId: 'id2', timeEpochMs: 3 }), + makeLog({ uid: '3', rowId: 'id3', timeEpochMs: 2 }), + makeLog({ uid: '4', rowId: 'id3', timeEpochMs: 1 }), + ]; + setup({ loading: false, panelState, logRows: rows }); const row = screen.getAllByRole('row'); await userEvent.hover(row[0]); @@ -396,7 +412,13 @@ describe('Logs', () => { it('should call createAndCopyShortLink on permalinkClick - logs', async () => { const panelState: Partial = { logs: { id: 'not-included', visualisationType: 'logs' } }; - setup({ loading: false, panelState }); + const rows = [ + makeLog({ uid: '1', rowId: 'id1', timeEpochMs: 1 }), + makeLog({ uid: '2', rowId: 'id2', timeEpochMs: 1 }), + makeLog({ uid: '3', rowId: 'id3', timeEpochMs: 2 }), + makeLog({ uid: '4', rowId: 'id3', timeEpochMs: 2 }), + ]; + setup({ loading: false, panelState, logRows: rows }); const row = screen.getAllByRole('row'); await userEvent.hover(row[0]); @@ -411,6 +433,34 @@ describe('Logs', () => { ); expect(createAndCopyShortLink).toHaveBeenCalledWith(expect.stringMatching('visualisationType%22:%22logs')); }); + + it('should call createAndCopyShortLink on permalinkClick - with infinite scrolling', async () => { + const featureToggleValue = config.featureToggles.logsInfiniteScrolling; + config.featureToggles.logsInfiniteScrolling = true; + const rows = [ + makeLog({ uid: '1', rowId: 'id1', timeEpochMs: 1 }), + makeLog({ uid: '2', rowId: 'id2', timeEpochMs: 1 }), + makeLog({ uid: '3', rowId: 'id3', timeEpochMs: 2 }), + makeLog({ uid: '4', rowId: 'id3', timeEpochMs: 2 }), + ]; + + const panelState: Partial = { logs: { id: 'not-included', visualisationType: 'logs' } }; + setup({ loading: false, panelState, logRows: rows }); + + const row = screen.getAllByRole('row'); + await userEvent.hover(row[3]); + + const linkButton = screen.getByLabelText('Copy shortlink'); + await userEvent.click(linkButton); + + expect(createAndCopyShortLink).toHaveBeenCalledWith( + expect.stringMatching( + 'range%22:%7B%22from%22:%222019-01-01T10:00:00.000Z%22,%22to%22:%221970-01-01T00:00:00.002Z%22%7D' + ) + ); + expect(createAndCopyShortLink).toHaveBeenCalledWith(expect.stringMatching('visualisationType%22:%22logs')); + config.featureToggles.logsInfiniteScrolling = featureToggleValue; + }); }); describe('with table visualisation', () => { @@ -441,17 +491,23 @@ describe('Logs', () => { }); it('should use default state from localstorage - table', async () => { + const oldGet = store.get; + store.get = jest.fn().mockReturnValue('table'); localStorage.setItem(visualisationTypeKey, 'table'); setup({}); const table = await screen.findByTestId('logRowsTable'); expect(table).toBeInTheDocument(); + store.get = oldGet; }); it('should use default state from localstorage - logs', async () => { + const oldGet = store.get; + store.get = jest.fn().mockReturnValue('logs'); localStorage.setItem(visualisationTypeKey, 'logs'); setup({}); const table = await screen.findByTestId('logRows'); expect(table).toBeInTheDocument(); + store.get = oldGet; }); it('should change visualisation to table on toggle (elastic)', async () => { diff --git a/public/app/features/explore/Logs/Logs.tsx b/public/app/features/explore/Logs/Logs.tsx index b53337db90..c5faf08dc5 100644 --- a/public/app/features/explore/Logs/Logs.tsx +++ b/public/app/features/explore/Logs/Logs.tsx @@ -437,6 +437,16 @@ class UnthemedLogs extends PureComponent { }; }; + getPreviousLog(row: LogRowModel, allLogs: LogRowModel[]): LogRowModel | null { + for (let i = allLogs.indexOf(row) - 1; i >= 0; i--) { + if (allLogs[i].timeEpochMs > row.timeEpochMs) { + return allLogs[i]; + } + } + + return null; + } + getPermalinkRange(row: LogRowModel) { const range = { from: new Date(this.props.absoluteRange.from).toISOString(), @@ -449,7 +459,7 @@ class UnthemedLogs extends PureComponent { // With infinite scrolling, the time range of the log line can be after the absolute range or beyond the request line limit, so we need to adjust // Look for the previous sibling log, and use its timestamp const allLogs = this.props.logRows.filter((logRow) => logRow.dataFrame.refId === row.dataFrame.refId); - const prevLog = allLogs[allLogs.indexOf(row) - 1]; + const prevLog = this.getPreviousLog(row, allLogs); if (row.timeEpochMs > this.props.absoluteRange.to && !prevLog) { // Because there's no sibling and the current `to` is oldest than the log, we have no reference we can use for the interval From 05865034d7b2549f4819da51fc1c5fa92233e332 Mon Sep 17 00:00:00 2001 From: brendamuir <100768211+brendamuir@users.noreply.github.com> Date: Tue, 5 Mar 2024 14:22:03 +0100 Subject: [PATCH 0392/1406] Docs: removes note as recovery threshold now in cloud (#83907) --- .../alerting/alerting-rules/create-grafana-managed-rule.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md b/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md index f7b5a38d75..c93157ba79 100644 --- a/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md +++ b/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md @@ -78,10 +78,6 @@ Define a query to get the data you want to measure and a condition that needs to b. Click **Preview** to verify that the expression is successful. -{{% admonition type="note" %}} -The recovery threshold feature is currently only available in OSS. -{{% /admonition %}} - 1. To add a recovery threshold, turn the **Custom recovery threshold** toggle on and fill in a value for when your alert rule should stop firing. You can only add one recovery threshold in a query and it must be the alert condition. From fdd2c1c59ede444944cd13d0acff06f1ad9fccf2 Mon Sep 17 00:00:00 2001 From: Levente Balogh Date: Tue, 5 Mar 2024 14:23:29 +0100 Subject: [PATCH 0393/1406] Plugins Catalog: Fix plugin details page initial flickering (#83896) fix: wait for the local and remote fetches to fulfill --- public/app/features/plugins/admin/state/hooks.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/public/app/features/plugins/admin/state/hooks.ts b/public/app/features/plugins/admin/state/hooks.ts index f6592a9e12..c50d954834 100644 --- a/public/app/features/plugins/admin/state/hooks.ts +++ b/public/app/features/plugins/admin/state/hooks.ts @@ -84,7 +84,10 @@ export const useLocalFetchStatus = () => { }; export const useFetchStatus = () => { - const isLoading = useSelector(selectIsRequestPending(fetchAll.typePrefix)); + const isAllLoading = useSelector(selectIsRequestPending(fetchAll.typePrefix)); + const isLocalLoading = useSelector(selectIsRequestPending('plugins/fetchLocal')); + const isRemoteLoading = useSelector(selectIsRequestPending('plugins/fetchRemote')); + const isLoading = isAllLoading || isLocalLoading || isRemoteLoading; const error = useSelector(selectRequestError(fetchAll.typePrefix)); return { isLoading, error }; From 73c4b24a5295b4217ddfb0a0eb8dad1fcf29bbe0 Mon Sep 17 00:00:00 2001 From: Michael Mandrus <41969079+mmandrus@users.noreply.github.com> Date: Tue, 5 Mar 2024 08:27:44 -0500 Subject: [PATCH 0394/1406] Queries: Fix debug logging of metrics queries (#83877) fix log statements --- pkg/services/query/query.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/services/query/query.go b/pkg/services/query/query.go index 90b6c9eb89..9e130c3f07 100644 --- a/pkg/services/query/query.go +++ b/pkg/services/query/query.go @@ -306,13 +306,13 @@ func (s *ServiceImpl) parseMetricRequest(ctx context.Context, user identity.Requ req.parsedQueries[ds.UID] = []parsedQuery{} } - s.log.Debug("Processing metrics query", "query", query) - modelJSON, err := query.MarshalJSON() if err != nil { return nil, err } + s.log.Debug("Processing metrics query", "query", string(modelJSON)) + req.parsedQueries[ds.UID] = append(req.parsedQueries[ds.UID], parsedQuery{ datasource: ds, query: backend.DataQuery{ From 2cce9aa2f7112a868225145712fdd9f458a756a3 Mon Sep 17 00:00:00 2001 From: ismail simsek Date: Tue, 5 Mar 2024 14:34:08 +0100 Subject: [PATCH 0395/1406] Chore: Move tracing function in influxdb package (#83899) move tracing function in influxdb package --- pkg/tsdb/influxdb/influxql/influxql.go | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/pkg/tsdb/influxdb/influxql/influxql.go b/pkg/tsdb/influxdb/influxql/influxql.go index 88a7ac7b1b..ada4a2eade 100644 --- a/pkg/tsdb/influxdb/influxql/influxql.go +++ b/pkg/tsdb/influxdb/influxql/influxql.go @@ -12,6 +12,7 @@ import ( "github.com/grafana/dskit/concurrency" "github.com/grafana/grafana-plugin-sdk-go/backend" + "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" "github.com/grafana/grafana/pkg/infra/log" @@ -20,7 +21,6 @@ import ( "github.com/grafana/grafana/pkg/tsdb/influxdb/influxql/buffered" "github.com/grafana/grafana/pkg/tsdb/influxdb/influxql/querydata" "github.com/grafana/grafana/pkg/tsdb/influxdb/models" - "github.com/grafana/grafana/pkg/tsdb/prometheus/utils" ) const defaultRetentionPolicy = "default" @@ -180,7 +180,7 @@ func execute(ctx context.Context, tracer trace.Tracer, dsInfo *models.Datasource } }() - _, endSpan := utils.StartTrace(ctx, tracer, "datasource.influxdb.influxql.parseResponse") + _, endSpan := startTrace(ctx, tracer, "datasource.influxdb.influxql.parseResponse") defer endSpan() var resp *backend.DataResponse @@ -192,3 +192,14 @@ func execute(ctx context.Context, tracer trace.Tracer, dsInfo *models.Datasource } return *resp, nil } + +// startTrace setups a trace but does not panic if tracer is nil which helps with testing +func startTrace(ctx context.Context, tracer trace.Tracer, name string, attributes ...attribute.KeyValue) (context.Context, func()) { + if tracer == nil { + return ctx, func() {} + } + ctx, span := tracer.Start(ctx, name, trace.WithAttributes(attributes...)) + return ctx, func() { + span.End() + } +} From 4b0547014ae05d1f6598acc490bdd3fd0d1e5c81 Mon Sep 17 00:00:00 2001 From: kay delaney <45561153+kaydelaney@users.noreply.github.com> Date: Tue, 5 Mar 2024 14:09:24 +0000 Subject: [PATCH 0396/1406] Scenes/LibraryPanels: Fix transformSceneToSaveModel for library panel repeats (#83843) --- .betterer.results | 3 +- .../transformSceneToSaveModel.test.ts | 102 +++++++++++++++--- .../transformSceneToSaveModel.ts | 54 +++++----- .../dashboard-scene/utils/test-utils.ts | 13 ++- 4 files changed, 118 insertions(+), 54 deletions(-) diff --git a/.betterer.results b/.betterer.results index d89b65b7bb..48f308f57b 100644 --- a/.betterer.results +++ b/.betterer.results @@ -2573,8 +2573,7 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "3"], [0, 0, 0, "Do not use any type assertions.", "4"], [0, 0, 0, "Do not use any type assertions.", "5"], - [0, 0, 0, "Do not use any type assertions.", "6"], - [0, 0, 0, "Do not use any type assertions.", "7"] + [0, 0, 0, "Do not use any type assertions.", "6"] ], "public/app/features/dashboard-scene/settings/variables/components/VariableSelectField.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts index f088fc622c..7c3dadd80c 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts @@ -1,3 +1,4 @@ +import 'whatwg-fetch'; import { advanceTo } from 'jest-date-mock'; import { map, of } from 'rxjs'; @@ -18,6 +19,7 @@ import { getPluginLinkExtensions, setPluginImportUtils } from '@grafana/runtime' import { MultiValueVariable, SceneDataLayers, + SceneGridItem, SceneGridItemLike, SceneGridLayout, SceneGridRow, @@ -29,6 +31,7 @@ import { getTimeRange } from 'app/features/dashboard/utils/timeRange'; import { reduceTransformRegistryItem } from 'app/features/transformers/editors/ReduceTransformerEditor'; import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard'; +import { LibraryVizPanel } from '../scene/LibraryVizPanel'; import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior'; import { NEW_LINK } from '../settings/links/utils'; import { activateFullSceneTree, buildPanelRepeaterScene } from '../utils/test-utils'; @@ -315,24 +318,41 @@ describe('transformSceneToSaveModel', () => { describe('Library panels', () => { it('given a library panel', () => { - const panel = buildGridItemFromPanelSchema({ - id: 4, - gridPos: { - h: 8, - w: 12, - x: 0, - y: 0, - }, - libraryPanel: { - name: 'Some lib panel panel', - uid: 'lib-panel-uid', - }, + // Not using buildGridItemFromPanelSchema since it strips options/fieldConfig + const libVizPanel = new LibraryVizPanel({ + name: 'Some lib panel panel', title: 'A panel', - transformations: [], - fieldConfig: { - defaults: {}, - overrides: [], - }, + uid: 'lib-panel-uid', + panelKey: 'lib-panel', + panel: new VizPanel({ + key: 'panel-4', + title: 'Panel blahh blah', + fieldConfig: { + defaults: {}, + overrides: [], + }, + options: { + legend: { + calcs: [], + displayMode: 'list', + placement: 'bottom', + showLegend: true, + }, + tooltip: { + maxHeight: 600, + mode: 'single', + sort: 'none', + }, + }, + }), + }); + + const panel = new SceneGridItem({ + body: libVizPanel, + y: 0, + x: 0, + width: 12, + height: 8, }); const result = gridItemToPanel(panel); @@ -351,6 +371,7 @@ describe('transformSceneToSaveModel', () => { expect(result.title).toBe('A panel'); expect(result.transformations).toBeUndefined(); expect(result.fieldConfig).toBeUndefined(); + expect(result.options).toBeUndefined(); }); it('given a library panel widget', () => { @@ -769,6 +790,53 @@ describe('transformSceneToSaveModel', () => { expect(result[1].title).toEqual('Panel $server'); }); + it('handles repeated library panels', () => { + const { scene, repeater } = buildPanelRepeaterScene( + { variableQueryTime: 0, numberOfOptions: 2 }, + new LibraryVizPanel({ + name: 'Some lib panel panel', + title: 'A panel', + uid: 'lib-panel-uid', + panelKey: 'lib-panel', + panel: new VizPanel({ + key: 'panel-4', + title: 'Panel blahh blah', + fieldConfig: { + defaults: {}, + overrides: [], + }, + options: { + legend: { + calcs: [], + displayMode: 'list', + placement: 'bottom', + showLegend: true, + }, + tooltip: { + maxHeight: 600, + mode: 'single', + sort: 'none', + }, + }, + }), + }) + ); + + activateFullSceneTree(scene); + const result = panelRepeaterToPanels(repeater, true); + + expect(result).toHaveLength(1); + + expect(result[0]).toMatchObject({ + id: 4, + title: 'A panel', + libraryPanel: { + name: 'Some lib panel panel', + uid: 'lib-panel-uid', + }, + }); + }); + it('handles row repeats ', () => { const { scene, row } = buildPanelRepeaterScene({ variableQueryTime: 0, diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts index 7e4a63dcaa..b1329a4828 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts @@ -139,6 +139,22 @@ export function transformSceneToSaveModel(scene: DashboardScene, isSnapshot = fa return sortedDeepCloneWithoutNulls(dashboard); } +export function libraryVizPanelToPanel(libPanel: LibraryVizPanel, gridPos: GridPos): Panel { + if (!libPanel.state.panel) { + throw new Error('Library panel has no panel'); + } + + return { + id: getPanelIdForVizPanel(libPanel.state.panel), + title: libPanel.state.title, + gridPos: gridPos, + libraryPanel: { + name: libPanel.state.name, + uid: libPanel.state.uid, + }, + } as Panel; +} + export function gridItemToPanel(gridItem: SceneGridItemLike, isSnapshot = false): Panel { let vizPanel: VizPanel | undefined; let x = 0, @@ -154,18 +170,7 @@ export function gridItemToPanel(gridItem: SceneGridItemLike, isSnapshot = false) w = gridItem.state.width ?? 0; h = gridItem.state.height ?? 0; - if (!gridItem.state.body.state.panel) { - throw new Error('Library panel has no panel'); - } - return { - id: getPanelIdForVizPanel(gridItem.state.body.state.panel), - title: gridItem.state.body.state.title, - gridPos: { x, y, w, h }, - libraryPanel: { - name: gridItem.state.body.state.name, - uid: gridItem.state.body.state.uid, - }, - } as Panel; + return libraryVizPanelToPanel(gridItem.state.body, { x, y, w, h }); } // Handle library panel widget as well and exit early @@ -194,16 +199,16 @@ export function gridItemToPanel(gridItem: SceneGridItemLike, isSnapshot = false) } if (gridItem instanceof PanelRepeaterGridItem) { - if (gridItem.state.source instanceof LibraryVizPanel) { - vizPanel = gridItem.state.source.state.panel; - } else { - vizPanel = gridItem.state.source; - } - x = gridItem.state.x ?? 0; y = gridItem.state.y ?? 0; w = gridItem.state.width ?? 0; h = gridItem.state.height ?? 0; + + if (gridItem.state.source instanceof LibraryVizPanel) { + return libraryVizPanelToPanel(gridItem.state.source, { x, y, w, h }); + } else { + vizPanel = gridItem.state.source; + } } if (!vizPanel) { @@ -323,18 +328,7 @@ export function panelRepeaterToPanels(repeater: PanelRepeaterGridItem, isSnapsho } else { if (repeater.state.source instanceof LibraryVizPanel) { const { x = 0, y = 0, width: w = 0, height: h = 0 } = repeater.state; - - return [ - { - id: getPanelIdForVizPanel(repeater.state.source), - title: repeater.state.source.state.title, - gridPos: { x, y, w, h }, - libraryPanel: { - name: repeater.state.source.state.name, - uid: repeater.state.source.state.uid, - }, - } as Panel, - ]; + return [libraryVizPanelToPanel(repeater.state.source, { x, y, w, h })]; } if (repeater.state.repeatedPanels) { diff --git a/public/app/features/dashboard-scene/utils/test-utils.ts b/public/app/features/dashboard-scene/utils/test-utils.ts index 9e6b18d0f9..b807efcdc6 100644 --- a/public/app/features/dashboard-scene/utils/test-utils.ts +++ b/public/app/features/dashboard-scene/utils/test-utils.ts @@ -15,6 +15,7 @@ import { DashboardLoaderSrv, setDashboardLoaderSrv } from 'app/features/dashboar import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE } from 'app/features/variables/constants'; import { DashboardDTO } from 'app/types'; +import { LibraryVizPanel } from '../scene/LibraryVizPanel'; import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks'; import { PanelRepeaterGridItem, RepeatDirection } from '../scene/PanelRepeaterGridItem'; import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior'; @@ -99,7 +100,7 @@ interface SceneOptions { useRowRepeater?: boolean; } -export function buildPanelRepeaterScene(options: SceneOptions) { +export function buildPanelRepeaterScene(options: SceneOptions, source?: VizPanel | LibraryVizPanel) { const defaults = { usePanelRepeater: true, ...options }; const repeater = new PanelRepeaterGridItem({ @@ -108,10 +109,12 @@ export function buildPanelRepeaterScene(options: SceneOptions) { repeatDirection: options.repeatDirection, maxPerRow: options.maxPerRow, itemHeight: options.itemHeight, - source: new VizPanel({ - title: 'Panel $server', - pluginId: 'timeseries', - }), + source: + source ?? + new VizPanel({ + title: 'Panel $server', + pluginId: 'timeseries', + }), x: options.x || 0, y: options.y || 0, }); From 71fe675fb789c04c54518960357e1d7a7f7f2735 Mon Sep 17 00:00:00 2001 From: Lucy Chen <140550297+lucychen-grafana@users.noreply.github.com> Date: Wed, 6 Mar 2024 00:09:44 +0900 Subject: [PATCH 0397/1406] Reporting: Update swagger and openapi doc for deleted deprecated endpoint for Email (#83832) --- public/api-enterprise-spec.json | 6 +++--- public/api-merged.json | 6 +++--- public/openapi3.json | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/public/api-enterprise-spec.json b/public/api-enterprise-spec.json index 097d24e570..ea1f0a592a 100644 --- a/public/api-enterprise-spec.json +++ b/public/api-enterprise-spec.json @@ -6280,6 +6280,9 @@ "templateVars": { "type": "object" }, + "uid": { + "type": "string" + }, "updated": { "type": "string", "format": "date-time" @@ -6342,9 +6345,6 @@ "ReportEmail": { "type": "object", "properties": { - "email": { - "type": "string" - }, "emails": { "description": "Comma-separated list of emails to which to send the report to.", "type": "string" diff --git a/public/api-merged.json b/public/api-merged.json index cc15b13c83..40061884c2 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -18913,6 +18913,9 @@ "templateVars": { "type": "object" }, + "uid": { + "type": "string" + }, "updated": { "type": "string", "format": "date-time" @@ -18975,9 +18978,6 @@ "ReportEmail": { "type": "object", "properties": { - "email": { - "type": "string" - }, "emails": { "description": "Comma-separated list of emails to which to send the report to.", "type": "string" diff --git a/public/openapi3.json b/public/openapi3.json index 0f05866592..5291c6fe76 100644 --- a/public/openapi3.json +++ b/public/openapi3.json @@ -9422,6 +9422,9 @@ "templateVars": { "type": "object" }, + "uid": { + "type": "string" + }, "updated": { "format": "date-time", "type": "string" @@ -9484,9 +9487,6 @@ }, "ReportEmail": { "properties": { - "email": { - "type": "string" - }, "emails": { "description": "Comma-separated list of emails to which to send the report to.", "type": "string" From 7b4925ea372d310ab57b8cc2cf50ff2b43ff8c79 Mon Sep 17 00:00:00 2001 From: Dan Cech Date: Tue, 5 Mar 2024 10:14:38 -0500 Subject: [PATCH 0398/1406] Storage: Watch support (#82282) * initial naive implementation * Update pkg/services/store/entity/sqlstash/sql_storage_server.go Co-authored-by: Igor Suleymanov * tidy up * add action column, batch watch events * initial implementation of broadcast-based watcher * fix up watch init * remove batching, it just adds needless complexity * use StreamWatcher * make broadcaster generic * add circular buffer to replay recent events to new watchers * loop within poll until all events are read * add index on entity_history.resource_version to support poller * increment r.Since when we send events to consumer * switch broadcaster and cache to use channels instead of mutexes * cleanup --------- Co-authored-by: Igor Suleymanov --- .../secretsmanagerplugin/secretsmanager.pb.go | 4 +- .../secretsmanager_grpc.pb.go | 2 +- .../apiserver/storage/entity/storage.go | 110 ++- .../entity/db/migrations/entity_store_mig.go | 8 +- pkg/services/store/entity/entity.pb.go | 714 +++++++++--------- pkg/services/store/entity/entity.proto | 30 +- pkg/services/store/entity/entity_grpc.pb.go | 2 +- .../store/entity/sqlstash/broadcaster.go | 254 +++++++ .../store/entity/sqlstash/broadcaster_test.go | 106 +++ .../entity/sqlstash/sql_storage_server.go | 457 ++++++++++- 10 files changed, 1293 insertions(+), 394 deletions(-) create mode 100644 pkg/services/store/entity/sqlstash/broadcaster.go create mode 100644 pkg/services/store/entity/sqlstash/broadcaster_test.go diff --git a/pkg/plugins/backendplugin/secretsmanagerplugin/secretsmanager.pb.go b/pkg/plugins/backendplugin/secretsmanagerplugin/secretsmanager.pb.go index 2fd1a54dc3..777de3c42f 100644 --- a/pkg/plugins/backendplugin/secretsmanagerplugin/secretsmanager.pb.go +++ b/pkg/plugins/backendplugin/secretsmanagerplugin/secretsmanager.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.31.0 -// protoc v4.25.1 +// protoc-gen-go v1.32.0 +// protoc v4.25.2 // source: secretsmanager.proto package secretsmanagerplugin diff --git a/pkg/plugins/backendplugin/secretsmanagerplugin/secretsmanager_grpc.pb.go b/pkg/plugins/backendplugin/secretsmanagerplugin/secretsmanager_grpc.pb.go index e0cedf929f..264020b216 100644 --- a/pkg/plugins/backendplugin/secretsmanagerplugin/secretsmanager_grpc.pb.go +++ b/pkg/plugins/backendplugin/secretsmanagerplugin/secretsmanager_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.3.0 -// - protoc v4.25.1 +// - protoc v4.25.2 // source: secretsmanager.proto package secretsmanagerplugin diff --git a/pkg/services/apiserver/storage/entity/storage.go b/pkg/services/apiserver/storage/entity/storage.go index 5ddfda401a..2fa78fc3dd 100644 --- a/pkg/services/apiserver/storage/entity/storage.go +++ b/pkg/services/apiserver/storage/entity/storage.go @@ -9,9 +9,13 @@ import ( "context" "errors" "fmt" + "io" + "log" "reflect" "strconv" + grpcCodes "google.golang.org/grpc/codes" + grpcStatus "google.golang.org/grpc/status" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -161,6 +165,72 @@ func (s *Storage) Delete(ctx context.Context, key string, out runtime.Object, pr return nil } +type Decoder struct { + client entityStore.EntityStore_WatchClient + newFunc func() runtime.Object + opts storage.ListOptions + codec runtime.Codec +} + +func (d *Decoder) Decode() (action watch.EventType, object runtime.Object, err error) { + for { + resp, err := d.client.Recv() + if errors.Is(err, io.EOF) { + log.Printf("watch is done") + return watch.Error, nil, err + } + + if grpcStatus.Code(err) == grpcCodes.Canceled { + log.Printf("watch was canceled") + return watch.Error, nil, err + } + + if err != nil { + log.Printf("error receiving result: %s", err) + return watch.Error, nil, err + } + + obj := d.newFunc() + + err = entityToResource(resp.Entity, obj, d.codec) + if err != nil { + log.Printf("error decoding entity: %s", err) + return watch.Error, nil, err + } + + // apply any predicates not handled in storage + var matches bool + matches, err = d.opts.Predicate.Matches(obj) + if err != nil { + log.Printf("error matching object: %s", err) + return watch.Error, nil, err + } + if !matches { + continue + } + + var watchAction watch.EventType + switch resp.Entity.Action { + case entityStore.Entity_CREATED: + watchAction = watch.Added + case entityStore.Entity_UPDATED: + watchAction = watch.Modified + case entityStore.Entity_DELETED: + watchAction = watch.Deleted + default: + watchAction = watch.Error + } + + return watchAction, obj, nil + } +} + +func (d *Decoder) Close() { + _ = d.client.CloseSend() +} + +var _ watch.Decoder = (*Decoder)(nil) + // Watch begins watching the specified key. Events are decoded into API objects, // and any items selected by 'p' are sent down to returned watch.Interface. // resourceVersion may be used to specify what version to begin watching, @@ -169,7 +239,37 @@ func (s *Storage) Delete(ctx context.Context, key string, out runtime.Object, pr // If resource version is "0", this interface will get current object at given key // and send it in an "ADDED" event, before watch starts. func (s *Storage) Watch(ctx context.Context, key string, opts storage.ListOptions) (watch.Interface, error) { - return nil, apierrors.NewMethodNotSupported(schema.GroupResource{}, "watch") + req := &entityStore.EntityWatchRequest{ + Key: []string{key}, + WithBody: true, + } + + if opts.ResourceVersion != "" { + rv, err := strconv.ParseInt(opts.ResourceVersion, 10, 64) + if err != nil { + return nil, apierrors.NewBadRequest(fmt.Sprintf("invalid resource version: %s", opts.ResourceVersion)) + } + + req.Since = rv + } + + result, err := s.store.Watch(ctx, req) + if err != nil { + return nil, err + } + + reporter := apierrors.NewClientErrorReporter(500, "WATCH", "") + + decoder := &Decoder{ + client: result, + newFunc: s.newFunc, + opts: opts, + codec: s.codec, + } + + w := watch.NewStreamWatcher(decoder, reporter) + + return w, nil } // Get unmarshals object found at key into objPtr. On a not found error, will either @@ -260,9 +360,15 @@ func (s *Storage) GetList(ctx context.Context, key string, opts storage.ListOpti return apierrors.NewInternalError(err) } + maxResourceVersion := int64(0) + for _, r := range rsp.Results { res := s.newFunc() + if r.ResourceVersion > maxResourceVersion { + maxResourceVersion = r.ResourceVersion + } + err := entityToResource(r, res, s.codec) if err != nil { return apierrors.NewInternalError(err) @@ -289,6 +395,8 @@ func (s *Storage) GetList(ctx context.Context, key string, opts storage.ListOpti listAccessor.SetContinue(rsp.NextPageToken) } + listAccessor.SetResourceVersion(strconv.FormatInt(maxResourceVersion, 10)) + return nil } diff --git a/pkg/services/store/entity/db/migrations/entity_store_mig.go b/pkg/services/store/entity/db/migrations/entity_store_mig.go index 10c766ee09..81f194562a 100644 --- a/pkg/services/store/entity/db/migrations/entity_store_mig.go +++ b/pkg/services/store/entity/db/migrations/entity_store_mig.go @@ -7,7 +7,7 @@ import ( ) func initEntityTables(mg *migrator.Migrator) string { - marker := "Initialize entity tables (v13)" // changing this key wipe+rewrite everything + marker := "Initialize entity tables (v15)" // changing this key wipe+rewrite everything mg.AddMigration(marker, &migrator.RawSQLMigration{}) tables := []migrator.Table{} @@ -59,6 +59,8 @@ func initEntityTables(mg *migrator.Migrator) string { {Name: "labels", Type: migrator.DB_Text, Nullable: true}, // JSON object {Name: "fields", Type: migrator.DB_Text, Nullable: true}, // JSON object {Name: "errors", Type: migrator.DB_Text, Nullable: true}, // JSON object + + {Name: "action", Type: migrator.DB_Int, Nullable: false}, // 1: create, 2: update, 3: delete }, Indices: []*migrator.Index{ // The keys are ordered for efficiency in mysql queries, not URL consistency @@ -117,6 +119,8 @@ func initEntityTables(mg *migrator.Migrator) string { {Name: "labels", Type: migrator.DB_Text, Nullable: true}, // JSON object {Name: "fields", Type: migrator.DB_Text, Nullable: true}, // JSON object {Name: "errors", Type: migrator.DB_Text, Nullable: true}, // JSON object + + {Name: "action", Type: migrator.DB_Int, Nullable: false}, // 1: create, 2: update, 3: delete }, Indices: []*migrator.Index{ {Cols: []string{"guid", "resource_version"}, Type: migrator.UniqueIndex}, @@ -125,6 +129,8 @@ func initEntityTables(mg *migrator.Migrator) string { Type: migrator.UniqueIndex, Name: "UQE_entity_history_namespace_group_name_version", }, + // index to support watch poller + {Cols: []string{"resource_version"}, Type: migrator.IndexType}, }, }) diff --git a/pkg/services/store/entity/entity.pb.go b/pkg/services/store/entity/entity.pb.go index 786c64d252..8d3caf23f5 100644 --- a/pkg/services/store/entity/entity.pb.go +++ b/pkg/services/store/entity/entity.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.31.0 -// protoc v4.25.1 +// protoc-gen-go v1.32.0 +// protoc v4.25.2 // source: entity.proto package entity @@ -20,6 +20,62 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) +// Status enumeration +type Entity_Action int32 + +const ( + Entity_UNKNOWN Entity_Action = 0 + Entity_CREATED Entity_Action = 1 + Entity_UPDATED Entity_Action = 2 + Entity_DELETED Entity_Action = 3 + Entity_ERROR Entity_Action = 4 +) + +// Enum value maps for Entity_Action. +var ( + Entity_Action_name = map[int32]string{ + 0: "UNKNOWN", + 1: "CREATED", + 2: "UPDATED", + 3: "DELETED", + 4: "ERROR", + } + Entity_Action_value = map[string]int32{ + "UNKNOWN": 0, + "CREATED": 1, + "UPDATED": 2, + "DELETED": 3, + "ERROR": 4, + } +) + +func (x Entity_Action) Enum() *Entity_Action { + p := new(Entity_Action) + *p = x + return p +} + +func (x Entity_Action) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Entity_Action) Descriptor() protoreflect.EnumDescriptor { + return file_entity_proto_enumTypes[0].Descriptor() +} + +func (Entity_Action) Type() protoreflect.EnumType { + return &file_entity_proto_enumTypes[0] +} + +func (x Entity_Action) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Entity_Action.Descriptor instead. +func (Entity_Action) EnumDescriptor() ([]byte, []int) { + return file_entity_proto_rawDescGZIP(), []int{0, 0} +} + // Status enumeration type CreateEntityResponse_Status int32 @@ -51,11 +107,11 @@ func (x CreateEntityResponse_Status) String() string { } func (CreateEntityResponse_Status) Descriptor() protoreflect.EnumDescriptor { - return file_entity_proto_enumTypes[0].Descriptor() + return file_entity_proto_enumTypes[1].Descriptor() } func (CreateEntityResponse_Status) Type() protoreflect.EnumType { - return &file_entity_proto_enumTypes[0] + return &file_entity_proto_enumTypes[1] } func (x CreateEntityResponse_Status) Number() protoreflect.EnumNumber { @@ -101,11 +157,11 @@ func (x UpdateEntityResponse_Status) String() string { } func (UpdateEntityResponse_Status) Descriptor() protoreflect.EnumDescriptor { - return file_entity_proto_enumTypes[1].Descriptor() + return file_entity_proto_enumTypes[2].Descriptor() } func (UpdateEntityResponse_Status) Type() protoreflect.EnumType { - return &file_entity_proto_enumTypes[1] + return &file_entity_proto_enumTypes[2] } func (x UpdateEntityResponse_Status) Number() protoreflect.EnumNumber { @@ -151,11 +207,11 @@ func (x DeleteEntityResponse_Status) String() string { } func (DeleteEntityResponse_Status) Descriptor() protoreflect.EnumDescriptor { - return file_entity_proto_enumTypes[2].Descriptor() + return file_entity_proto_enumTypes[3].Descriptor() } func (DeleteEntityResponse_Status) Type() protoreflect.EnumType { - return &file_entity_proto_enumTypes[2] + return &file_entity_proto_enumTypes[3] } func (x DeleteEntityResponse_Status) Number() protoreflect.EnumNumber { @@ -167,56 +223,6 @@ func (DeleteEntityResponse_Status) EnumDescriptor() ([]byte, []int) { return file_entity_proto_rawDescGZIP(), []int{11, 0} } -// Status enumeration -type EntityWatchResponse_Action int32 - -const ( - EntityWatchResponse_UNKNOWN EntityWatchResponse_Action = 0 - EntityWatchResponse_UPDATED EntityWatchResponse_Action = 1 - EntityWatchResponse_DELETED EntityWatchResponse_Action = 2 -) - -// Enum value maps for EntityWatchResponse_Action. -var ( - EntityWatchResponse_Action_name = map[int32]string{ - 0: "UNKNOWN", - 1: "UPDATED", - 2: "DELETED", - } - EntityWatchResponse_Action_value = map[string]int32{ - "UNKNOWN": 0, - "UPDATED": 1, - "DELETED": 2, - } -) - -func (x EntityWatchResponse_Action) Enum() *EntityWatchResponse_Action { - p := new(EntityWatchResponse_Action) - *p = x - return p -} - -func (x EntityWatchResponse_Action) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (EntityWatchResponse_Action) Descriptor() protoreflect.EnumDescriptor { - return file_entity_proto_enumTypes[3].Descriptor() -} - -func (EntityWatchResponse_Action) Type() protoreflect.EnumType { - return &file_entity_proto_enumTypes[3] -} - -func (x EntityWatchResponse_Action) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use EntityWatchResponse_Action.Descriptor instead. -func (EntityWatchResponse_Action) EnumDescriptor() ([]byte, []int) { - return file_entity_proto_rawDescGZIP(), []int{18, 0} -} - // The canonical entity/document data -- this represents the raw bytes and storage level metadata type Entity struct { state protoimpl.MessageState @@ -277,6 +283,8 @@ type Entity struct { Fields map[string]string `protobuf:"bytes,20,rep,name=fields,proto3" json:"fields,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` // When errors exist Errors []*EntityErrorInfo `protobuf:"bytes,21,rep,name=errors,proto3" json:"errors,omitempty"` + // Action code + Action Entity_Action `protobuf:"varint,3,opt,name=action,proto3,enum=entity.Entity_Action" json:"action,omitempty"` } func (x *Entity) Reset() { @@ -500,6 +508,13 @@ func (x *Entity) GetErrors() []*EntityErrorInfo { return nil } +func (x *Entity) GetAction() Entity_Action { + if x != nil { + return x.Action + } + return Entity_UNKNOWN +} + // This stores additional metadata for items entities that were synced from external systmes type EntityOriginInfo struct { state protoimpl.MessageState @@ -1586,9 +1601,9 @@ type EntityWatchRequest struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - // Timestamp of last changes. Empty will default to + // ResourceVersion of last changes. Empty will default to full history Since int64 `protobuf:"varint,1,opt,name=since,proto3" json:"since,omitempty"` - // Watch sppecific entities + // Watch specific entities Key []string `protobuf:"bytes,2,rep,name=key,proto3" json:"key,omitempty"` // limit to a specific resource (empty is all) Resource []string `protobuf:"bytes,3,rep,name=resource,proto3" json:"resource,omitempty"` @@ -1690,10 +1705,8 @@ type EntityWatchResponse struct { // Timestamp the event was sent Timestamp int64 `protobuf:"varint,1,opt,name=timestamp,proto3" json:"timestamp,omitempty"` - // List of entities with the same action - Entity []*Entity `protobuf:"bytes,2,rep,name=entity,proto3" json:"entity,omitempty"` - // Action code - Action EntityWatchResponse_Action `protobuf:"varint,3,opt,name=action,proto3,enum=entity.EntityWatchResponse_Action" json:"action,omitempty"` + // Entity that was created, updated, or deleted + Entity *Entity `protobuf:"bytes,2,opt,name=entity,proto3" json:"entity,omitempty"` } func (x *EntityWatchResponse) Reset() { @@ -1735,20 +1748,13 @@ func (x *EntityWatchResponse) GetTimestamp() int64 { return 0 } -func (x *EntityWatchResponse) GetEntity() []*Entity { +func (x *EntityWatchResponse) GetEntity() *Entity { if x != nil { return x.Entity } return nil } -func (x *EntityWatchResponse) GetAction() EntityWatchResponse_Action { - if x != nil { - return x.Action - } - return EntityWatchResponse_UNKNOWN -} - type EntitySummary struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1958,7 +1964,7 @@ var File_entity_proto protoreflect.FileDescriptor var file_entity_proto_rawDesc = []byte{ 0x0a, 0x0c, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x06, - 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x22, 0xa7, 0x07, 0x0a, 0x06, 0x45, 0x6e, 0x74, 0x69, 0x74, + 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x22, 0x9f, 0x08, 0x0a, 0x06, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x67, 0x75, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x67, 0x75, 0x69, 0x64, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, @@ -2009,269 +2015,269 @@ var file_entity_proto_rawDesc = []byte{ 0x79, 0x52, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x12, 0x2f, 0x0a, 0x06, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x18, 0x15, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x49, 0x6e, - 0x66, 0x6f, 0x52, 0x06, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x1a, 0x39, 0x0a, 0x0b, 0x4c, 0x61, - 0x62, 0x65, 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x39, 0x0a, 0x0b, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x45, - 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, - 0x22, 0x50, 0x0a, 0x10, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, - 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x10, 0x0a, 0x03, - 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x12, - 0x0a, 0x04, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x74, 0x69, - 0x6d, 0x65, 0x22, 0x62, 0x0a, 0x0f, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x45, 0x72, 0x72, 0x6f, - 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x03, 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, - 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, - 0x61, 0x67, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x5f, 0x6a, - 0x73, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x64, 0x65, 0x74, 0x61, 0x69, - 0x6c, 0x73, 0x4a, 0x73, 0x6f, 0x6e, 0x22, 0x8e, 0x01, 0x0a, 0x11, 0x52, 0x65, 0x61, 0x64, 0x45, + 0x66, 0x6f, 0x52, 0x06, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x12, 0x2d, 0x0a, 0x06, 0x61, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x15, 0x2e, 0x65, 0x6e, 0x74, + 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x41, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x1a, 0x39, 0x0a, 0x0b, 0x4c, 0x61, 0x62, + 0x65, 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x39, 0x0a, 0x0b, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x45, 0x6e, + 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, + 0x47, 0x0a, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, + 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, + 0x44, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x44, 0x10, 0x02, + 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x44, 0x10, 0x03, 0x12, 0x09, 0x0a, + 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x04, 0x22, 0x50, 0x0a, 0x10, 0x45, 0x6e, 0x74, 0x69, + 0x74, 0x79, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x16, 0x0a, 0x06, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x74, 0x69, 0x6d, 0x65, 0x22, 0x62, 0x0a, 0x0f, 0x45, 0x6e, + 0x74, 0x69, 0x74, 0x79, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x12, 0x0a, + 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x63, 0x6f, 0x64, + 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x64, + 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x5f, 0x6a, 0x73, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x0c, 0x52, 0x0b, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x4a, 0x73, 0x6f, 0x6e, 0x22, 0x8e, + 0x01, 0x0a, 0x11, 0x52, 0x65, 0x61, 0x64, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, + 0x6e, 0x12, 0x1b, 0x0a, 0x09, 0x77, 0x69, 0x74, 0x68, 0x5f, 0x62, 0x6f, 0x64, 0x79, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x77, 0x69, 0x74, 0x68, 0x42, 0x6f, 0x64, 0x79, 0x12, 0x1f, + 0x0a, 0x0b, 0x77, 0x69, 0x74, 0x68, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x0a, 0x77, 0x69, 0x74, 0x68, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, + 0x49, 0x0a, 0x16, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x61, 0x64, 0x45, 0x6e, 0x74, 0x69, + 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2f, 0x0a, 0x05, 0x62, 0x61, 0x74, + 0x63, 0x68, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, + 0x79, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x52, 0x05, 0x62, 0x61, 0x74, 0x63, 0x68, 0x22, 0x43, 0x0a, 0x17, 0x42, 0x61, + 0x74, 0x63, 0x68, 0x52, 0x65, 0x61, 0x64, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x28, 0x0a, 0x07, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, + 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, + 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x07, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x22, + 0x3d, 0x0a, 0x13, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x06, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, + 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x06, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x22, 0xcc, + 0x01, 0x0a, 0x14, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2d, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, + 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x52, + 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x26, 0x0a, 0x06, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, + 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x06, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x3b, + 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x23, + 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x45, 0x6e, + 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x53, 0x74, 0x61, + 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x20, 0x0a, 0x06, 0x53, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x00, + 0x12, 0x0b, 0x0a, 0x07, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x44, 0x10, 0x01, 0x22, 0x68, 0x0a, + 0x13, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x06, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, + 0x74, 0x69, 0x74, 0x79, 0x52, 0x06, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x29, 0x0a, 0x10, + 0x70, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x70, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, + 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0xdb, 0x01, 0x0a, 0x14, 0x55, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x2d, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x17, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x45, + 0x72, 0x72, 0x6f, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, + 0x26, 0x0a, 0x06, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x0e, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, + 0x06, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x3b, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, + 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x23, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, + 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x22, 0x2f, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x09, + 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x50, 0x44, + 0x41, 0x54, 0x45, 0x44, 0x10, 0x01, 0x12, 0x0d, 0x0a, 0x09, 0x55, 0x4e, 0x43, 0x48, 0x41, 0x4e, + 0x47, 0x45, 0x44, 0x10, 0x02, 0x22, 0x52, 0x0a, 0x13, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x29, - 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, - 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1b, 0x0a, 0x09, 0x77, 0x69, 0x74, - 0x68, 0x5f, 0x62, 0x6f, 0x64, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x77, 0x69, - 0x74, 0x68, 0x42, 0x6f, 0x64, 0x79, 0x12, 0x1f, 0x0a, 0x0b, 0x77, 0x69, 0x74, 0x68, 0x5f, 0x73, - 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x77, 0x69, 0x74, - 0x68, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x49, 0x0a, 0x16, 0x42, 0x61, 0x74, 0x63, 0x68, - 0x52, 0x65, 0x61, 0x64, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x2f, 0x0a, 0x05, 0x62, 0x61, 0x74, 0x63, 0x68, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x19, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x45, 0x6e, - 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x05, 0x62, 0x61, 0x74, - 0x63, 0x68, 0x22, 0x43, 0x0a, 0x17, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x61, 0x64, 0x45, - 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x28, 0x0a, - 0x07, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, - 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x07, - 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x22, 0x3d, 0x0a, 0x13, 0x43, 0x72, 0x65, 0x61, 0x74, - 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, - 0x0a, 0x06, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, - 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x06, - 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x22, 0xcc, 0x01, 0x0a, 0x14, 0x43, 0x72, 0x65, 0x61, 0x74, - 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x2d, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, - 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x45, 0x72, - 0x72, 0x6f, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x26, - 0x0a, 0x06, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, - 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x06, - 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x3b, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x23, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, - 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, - 0x74, 0x75, 0x73, 0x22, 0x20, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x09, 0x0a, - 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x43, 0x52, 0x45, 0x41, - 0x54, 0x45, 0x44, 0x10, 0x01, 0x22, 0x68, 0x0a, 0x13, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x45, - 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x06, - 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x65, - 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x06, 0x65, 0x6e, - 0x74, 0x69, 0x74, 0x79, 0x12, 0x29, 0x0a, 0x10, 0x70, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, - 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, - 0x70, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, - 0xdb, 0x01, 0x0a, 0x14, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2d, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, - 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, - 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x49, 0x6e, 0x66, 0x6f, - 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x26, 0x0a, 0x06, 0x65, 0x6e, 0x74, 0x69, 0x74, - 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, - 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x06, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, - 0x3b, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, - 0x23, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x45, - 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x53, 0x74, - 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x2f, 0x0a, 0x06, - 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, - 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x44, 0x10, 0x01, 0x12, 0x0d, - 0x0a, 0x09, 0x55, 0x4e, 0x43, 0x48, 0x41, 0x4e, 0x47, 0x45, 0x44, 0x10, 0x02, 0x22, 0x52, 0x0a, - 0x13, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x29, 0x0a, 0x10, 0x70, 0x72, 0x65, 0x76, 0x69, 0x6f, - 0x75, 0x73, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, - 0x52, 0x0f, 0x70, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, - 0x6e, 0x22, 0xda, 0x01, 0x0a, 0x14, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x45, 0x6e, 0x74, 0x69, - 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2d, 0x0a, 0x05, 0x65, 0x72, - 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x65, 0x6e, 0x74, 0x69, - 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x49, 0x6e, - 0x66, 0x6f, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x26, 0x0a, 0x06, 0x65, 0x6e, 0x74, - 0x69, 0x74, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x65, 0x6e, 0x74, 0x69, - 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x06, 0x65, 0x6e, 0x74, 0x69, 0x74, - 0x79, 0x12, 0x3b, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x0e, 0x32, 0x23, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, - 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, - 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x2e, - 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, - 0x52, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x44, 0x10, 0x01, - 0x12, 0x0c, 0x0a, 0x08, 0x4e, 0x4f, 0x54, 0x46, 0x4f, 0x55, 0x4e, 0x44, 0x10, 0x02, 0x22, 0x66, - 0x0a, 0x14, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, 0x6d, 0x69, - 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x26, - 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, - 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, - 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x7d, 0x0a, 0x15, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, - 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, - 0x79, 0x12, 0x2a, 0x0a, 0x08, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, - 0x69, 0x74, 0x79, 0x52, 0x08, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x26, 0x0a, - 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, 0x65, - 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x8f, 0x03, 0x0a, 0x11, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, - 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0f, 0x6e, - 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, - 0x6b, 0x65, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x03, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x71, 0x75, 0x65, - 0x72, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x12, - 0x14, 0x0a, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x18, 0x09, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, - 0x67, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x09, 0x52, 0x03, - 0x6b, 0x65, 0x79, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x18, 0x05, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x12, 0x3d, 0x0a, 0x06, 0x6c, - 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x65, 0x6e, - 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x4c, 0x69, 0x73, 0x74, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x45, 0x6e, 0x74, - 0x72, 0x79, 0x52, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x6f, - 0x72, 0x74, 0x18, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x73, 0x6f, 0x72, 0x74, 0x12, 0x1b, - 0x0a, 0x09, 0x77, 0x69, 0x74, 0x68, 0x5f, 0x62, 0x6f, 0x64, 0x79, 0x18, 0x08, 0x20, 0x01, 0x28, - 0x08, 0x52, 0x08, 0x77, 0x69, 0x74, 0x68, 0x42, 0x6f, 0x64, 0x79, 0x12, 0x1f, 0x0a, 0x0b, 0x77, - 0x69, 0x74, 0x68, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x0a, 0x77, 0x69, 0x74, 0x68, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x1a, 0x39, 0x0a, 0x0b, - 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, - 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, - 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, - 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xb4, 0x01, 0x0a, 0x10, 0x52, 0x65, 0x66, 0x65, - 0x72, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0f, - 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, - 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x03, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, - 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, - 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x67, 0x72, 0x6f, 0x75, - 0x70, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x1a, - 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, - 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x66, - 0x0a, 0x12, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x28, 0x0a, 0x07, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x18, - 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, - 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x07, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x12, 0x26, - 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, - 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, - 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0xa9, 0x02, 0x0a, 0x12, 0x45, 0x6e, 0x74, 0x69, 0x74, - 0x79, 0x57, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, - 0x05, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x73, 0x69, - 0x6e, 0x63, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, - 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x12, 0x3e, 0x0a, 0x06, 0x6c, 0x61, 0x62, - 0x65, 0x6c, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x65, 0x6e, 0x74, 0x69, - 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x57, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, - 0x79, 0x52, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x77, 0x69, 0x74, - 0x68, 0x5f, 0x62, 0x6f, 0x64, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x77, 0x69, - 0x74, 0x68, 0x42, 0x6f, 0x64, 0x79, 0x12, 0x1f, 0x0a, 0x0b, 0x77, 0x69, 0x74, 0x68, 0x5f, 0x73, - 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x77, 0x69, 0x74, - 0x68, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x1a, 0x39, 0x0a, 0x0b, 0x4c, 0x61, 0x62, 0x65, 0x6c, - 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, - 0x38, 0x01, 0x22, 0xc8, 0x01, 0x0a, 0x13, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x57, 0x61, 0x74, - 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x69, - 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x74, - 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x26, 0x0a, 0x06, 0x65, 0x6e, 0x74, 0x69, - 0x74, 0x79, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, - 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x06, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, - 0x12, 0x3a, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, - 0x32, 0x22, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, - 0x57, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x41, 0x63, - 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x2f, 0x0a, 0x06, - 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, - 0x4e, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x44, 0x10, 0x01, - 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x44, 0x10, 0x02, 0x22, 0xa2, 0x04, - 0x0a, 0x0d, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x12, - 0x10, 0x0a, 0x03, 0x55, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x55, 0x49, - 0x44, 0x12, 0x12, 0x0a, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, - 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, - 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x39, 0x0a, 0x06, 0x6c, - 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x65, 0x6e, - 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x53, 0x75, 0x6d, 0x6d, 0x61, - 0x72, 0x79, 0x2e, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, - 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, - 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x12, 0x12, - 0x0a, 0x04, 0x73, 0x6c, 0x75, 0x67, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x73, 0x6c, - 0x75, 0x67, 0x12, 0x2d, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x08, 0x20, 0x01, 0x28, + 0x0a, 0x10, 0x70, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, + 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x70, 0x72, 0x65, 0x76, 0x69, 0x6f, + 0x75, 0x73, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0xda, 0x01, 0x0a, 0x14, 0x44, 0x65, + 0x6c, 0x65, 0x74, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x2d, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, - 0x72, 0x12, 0x39, 0x0a, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x18, 0x09, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x21, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, - 0x79, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x45, - 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x12, 0x2d, 0x0a, 0x06, - 0x6e, 0x65, 0x73, 0x74, 0x65, 0x64, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x65, - 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x53, 0x75, 0x6d, 0x6d, - 0x61, 0x72, 0x79, 0x52, 0x06, 0x6e, 0x65, 0x73, 0x74, 0x65, 0x64, 0x12, 0x3f, 0x0a, 0x0a, 0x72, - 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x0b, 0x32, - 0x1f, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x45, - 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, - 0x52, 0x0a, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x1a, 0x39, 0x0a, 0x0b, - 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, - 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, - 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, - 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x39, 0x0a, 0x0b, 0x46, 0x69, 0x65, 0x6c, 0x64, - 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, - 0x38, 0x01, 0x22, 0x65, 0x0a, 0x17, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x45, 0x78, 0x74, 0x65, - 0x72, 0x6e, 0x61, 0x6c, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x16, 0x0a, - 0x06, 0x66, 0x61, 0x6d, 0x69, 0x6c, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, - 0x61, 0x6d, 0x69, 0x6c, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x69, 0x64, 0x65, - 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x69, - 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x32, 0xa8, 0x04, 0x0a, 0x0b, 0x45, 0x6e, - 0x74, 0x69, 0x74, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x12, 0x31, 0x0a, 0x04, 0x52, 0x65, 0x61, - 0x64, 0x12, 0x19, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x45, - 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0e, 0x2e, 0x65, - 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x4c, 0x0a, 0x09, - 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x61, 0x64, 0x12, 0x1e, 0x2e, 0x65, 0x6e, 0x74, 0x69, - 0x74, 0x79, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x61, 0x64, 0x45, 0x6e, 0x74, 0x69, - 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x65, 0x6e, 0x74, 0x69, - 0x74, 0x79, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x61, 0x64, 0x45, 0x6e, 0x74, 0x69, - 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x43, 0x0a, 0x06, 0x43, 0x72, - 0x65, 0x61, 0x74, 0x65, 0x12, 0x1b, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x43, 0x72, - 0x65, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x1c, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, - 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x43, 0x0a, 0x06, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x1b, 0x2e, 0x65, 0x6e, 0x74, 0x69, - 0x74, 0x79, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, - 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x43, 0x0a, 0x06, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x1b, - 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x45, 0x6e, + 0x72, 0x12, 0x26, 0x0a, 0x06, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x0e, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, + 0x79, 0x52, 0x06, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x3b, 0x0a, 0x06, 0x73, 0x74, 0x61, + 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x23, 0x2e, 0x65, 0x6e, 0x74, 0x69, + 0x74, 0x79, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, + 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x2e, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x44, + 0x45, 0x4c, 0x45, 0x54, 0x45, 0x44, 0x10, 0x01, 0x12, 0x0c, 0x0a, 0x08, 0x4e, 0x4f, 0x54, 0x46, + 0x4f, 0x55, 0x4e, 0x44, 0x10, 0x02, 0x22, 0x66, 0x0a, 0x14, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, + 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, + 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, + 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, + 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x7d, + 0x0a, 0x15, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2a, 0x0a, 0x08, 0x76, 0x65, 0x72, + 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x65, 0x6e, + 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x08, 0x76, 0x65, 0x72, + 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, + 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, + 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x8f, 0x03, + 0x0a, 0x11, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, + 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, + 0x78, 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x6c, + 0x69, 0x6d, 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, + 0x74, 0x12, 0x14, 0x0a, 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, + 0x18, 0x09, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x1a, 0x0a, + 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, + 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, + 0x18, 0x0b, 0x20, 0x03, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x16, 0x0a, 0x06, 0x66, + 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6f, 0x6c, + 0x64, 0x65, 0x72, 0x12, 0x3d, 0x0a, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0x06, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, + 0x69, 0x74, 0x79, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x4c, + 0x61, 0x62, 0x65, 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, 0x6c, 0x61, 0x62, 0x65, + 0x6c, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x6f, 0x72, 0x74, 0x18, 0x07, 0x20, 0x03, 0x28, 0x09, + 0x52, 0x04, 0x73, 0x6f, 0x72, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x77, 0x69, 0x74, 0x68, 0x5f, 0x62, + 0x6f, 0x64, 0x79, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x77, 0x69, 0x74, 0x68, 0x42, + 0x6f, 0x64, 0x79, 0x12, 0x1f, 0x0a, 0x0b, 0x77, 0x69, 0x74, 0x68, 0x5f, 0x73, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x77, 0x69, 0x74, 0x68, 0x53, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x1a, 0x39, 0x0a, 0x0b, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x45, 0x6e, + 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, + 0xb4, 0x01, 0x0a, 0x10, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, + 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, + 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x14, 0x0a, 0x05, + 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x6c, 0x69, 0x6d, + 0x69, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, + 0x12, 0x14, 0x0a, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x66, 0x0a, 0x12, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, + 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x28, 0x0a, 0x07, + 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, + 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x07, 0x72, + 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, + 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0xa9, + 0x02, 0x0a, 0x12, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x57, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6b, + 0x65, 0x79, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x1a, 0x0a, + 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, + 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x6f, 0x6c, + 0x64, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, + 0x72, 0x12, 0x3e, 0x0a, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x26, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, + 0x79, 0x57, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x4c, 0x61, + 0x62, 0x65, 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, + 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x77, 0x69, 0x74, 0x68, 0x5f, 0x62, 0x6f, 0x64, 0x79, 0x18, 0x06, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x77, 0x69, 0x74, 0x68, 0x42, 0x6f, 0x64, 0x79, 0x12, 0x1f, + 0x0a, 0x0b, 0x77, 0x69, 0x74, 0x68, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x07, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x0a, 0x77, 0x69, 0x74, 0x68, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x1a, + 0x39, 0x0a, 0x0b, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, + 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, + 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x5b, 0x0a, 0x13, 0x45, 0x6e, + 0x74, 0x69, 0x74, 0x79, 0x57, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, + 0x26, 0x0a, 0x06, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x0e, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, + 0x06, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x22, 0xa2, 0x04, 0x0a, 0x0d, 0x45, 0x6e, 0x74, 0x69, + 0x74, 0x79, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x49, 0x44, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x55, 0x49, 0x44, 0x12, 0x12, 0x0a, 0x04, 0x6b, + 0x69, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x12, + 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, + 0x61, 0x6d, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, + 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, + 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x39, 0x0a, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, + 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, + 0x6e, 0x74, 0x69, 0x74, 0x79, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x2e, 0x4c, 0x61, 0x62, + 0x65, 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, + 0x12, 0x16, 0x0a, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x6c, 0x75, 0x67, + 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x73, 0x6c, 0x75, 0x67, 0x12, 0x2d, 0x0a, 0x05, + 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x65, 0x6e, + 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x45, 0x72, 0x72, 0x6f, 0x72, + 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x39, 0x0a, 0x06, 0x66, + 0x69, 0x65, 0x6c, 0x64, 0x73, 0x18, 0x09, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x65, 0x6e, + 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x53, 0x75, 0x6d, 0x6d, 0x61, + 0x72, 0x79, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, + 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x12, 0x2d, 0x0a, 0x06, 0x6e, 0x65, 0x73, 0x74, 0x65, 0x64, + 0x18, 0x0a, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, + 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x52, 0x06, 0x6e, + 0x65, 0x73, 0x74, 0x65, 0x64, 0x12, 0x3f, 0x0a, 0x0a, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, + 0x63, 0x65, 0x73, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x65, 0x6e, 0x74, 0x69, + 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x45, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, + 0x6c, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x0a, 0x72, 0x65, 0x66, 0x65, + 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x1a, 0x39, 0x0a, 0x0b, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, + 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, + 0x01, 0x1a, 0x39, 0x0a, 0x0b, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, + 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, + 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x65, 0x0a, 0x17, + 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x45, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x52, 0x65, + 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x61, 0x6d, 0x69, 0x6c, + 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x61, 0x6d, 0x69, 0x6c, 0x79, 0x12, + 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, + 0x79, 0x70, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, + 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, + 0x69, 0x65, 0x72, 0x32, 0xa8, 0x04, 0x0a, 0x0b, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x53, 0x74, + 0x6f, 0x72, 0x65, 0x12, 0x31, 0x0a, 0x04, 0x52, 0x65, 0x61, 0x64, 0x12, 0x19, 0x2e, 0x65, 0x6e, + 0x74, 0x69, 0x74, 0x79, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0e, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, + 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x4c, 0x0a, 0x09, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, + 0x65, 0x61, 0x64, 0x12, 0x1e, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x42, 0x61, 0x74, + 0x63, 0x68, 0x52, 0x65, 0x61, 0x64, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x42, 0x61, 0x74, + 0x63, 0x68, 0x52, 0x65, 0x61, 0x64, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x43, 0x0a, 0x06, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x12, 0x1b, + 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x65, 0x6e, - 0x74, 0x69, 0x74, 0x79, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, - 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x46, 0x0a, 0x07, 0x48, 0x69, 0x73, - 0x74, 0x6f, 0x72, 0x79, 0x12, 0x1c, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, - 0x74, 0x69, 0x74, 0x79, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, - 0x74, 0x79, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x3d, 0x0a, 0x04, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x19, 0x2e, 0x65, 0x6e, 0x74, 0x69, - 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, - 0x74, 0x69, 0x74, 0x79, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x42, 0x0a, 0x05, 0x57, 0x61, 0x74, 0x63, 0x68, 0x12, 0x1a, 0x2e, 0x65, 0x6e, 0x74, 0x69, - 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x57, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, - 0x6e, 0x74, 0x69, 0x74, 0x79, 0x57, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x30, 0x01, 0x42, 0x36, 0x5a, 0x34, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, - 0x6f, 0x6d, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, - 0x6e, 0x61, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, - 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2f, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x62, 0x06, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x33, + 0x74, 0x69, 0x74, 0x79, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, + 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x43, 0x0a, 0x06, 0x55, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x12, 0x1b, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x55, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x1c, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, + 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x43, + 0x0a, 0x06, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x1b, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, + 0x79, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x44, + 0x65, 0x6c, 0x65, 0x74, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x46, 0x0a, 0x07, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x1c, + 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x48, 0x69, + 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x65, + 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x48, 0x69, 0x73, 0x74, + 0x6f, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3d, 0x0a, 0x04, 0x4c, + 0x69, 0x73, 0x74, 0x12, 0x19, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, + 0x69, 0x74, 0x79, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, + 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x4c, 0x69, + 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x42, 0x0a, 0x05, 0x57, 0x61, + 0x74, 0x63, 0x68, 0x12, 0x1a, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, + 0x69, 0x74, 0x79, 0x57, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x1b, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x57, + 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x42, 0x36, + 0x5a, 0x34, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, 0x61, + 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x70, 0x6b, 0x67, + 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2f, + 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -2289,10 +2295,10 @@ func file_entity_proto_rawDescGZIP() []byte { var file_entity_proto_enumTypes = make([]protoimpl.EnumInfo, 4) var file_entity_proto_msgTypes = make([]protoimpl.MessageInfo, 27) var file_entity_proto_goTypes = []interface{}{ - (CreateEntityResponse_Status)(0), // 0: entity.CreateEntityResponse.Status - (UpdateEntityResponse_Status)(0), // 1: entity.UpdateEntityResponse.Status - (DeleteEntityResponse_Status)(0), // 2: entity.DeleteEntityResponse.Status - (EntityWatchResponse_Action)(0), // 3: entity.EntityWatchResponse.Action + (Entity_Action)(0), // 0: entity.Entity.Action + (CreateEntityResponse_Status)(0), // 1: entity.CreateEntityResponse.Status + (UpdateEntityResponse_Status)(0), // 2: entity.UpdateEntityResponse.Status + (DeleteEntityResponse_Status)(0), // 3: entity.DeleteEntityResponse.Status (*Entity)(nil), // 4: entity.Entity (*EntityOriginInfo)(nil), // 5: entity.EntityOriginInfo (*EntityErrorInfo)(nil), // 6: entity.EntityErrorInfo @@ -2326,25 +2332,25 @@ var file_entity_proto_depIdxs = []int32{ 25, // 1: entity.Entity.labels:type_name -> entity.Entity.LabelsEntry 26, // 2: entity.Entity.fields:type_name -> entity.Entity.FieldsEntry 6, // 3: entity.Entity.errors:type_name -> entity.EntityErrorInfo - 7, // 4: entity.BatchReadEntityRequest.batch:type_name -> entity.ReadEntityRequest - 4, // 5: entity.BatchReadEntityResponse.results:type_name -> entity.Entity - 4, // 6: entity.CreateEntityRequest.entity:type_name -> entity.Entity - 6, // 7: entity.CreateEntityResponse.error:type_name -> entity.EntityErrorInfo - 4, // 8: entity.CreateEntityResponse.entity:type_name -> entity.Entity - 0, // 9: entity.CreateEntityResponse.status:type_name -> entity.CreateEntityResponse.Status - 4, // 10: entity.UpdateEntityRequest.entity:type_name -> entity.Entity - 6, // 11: entity.UpdateEntityResponse.error:type_name -> entity.EntityErrorInfo - 4, // 12: entity.UpdateEntityResponse.entity:type_name -> entity.Entity - 1, // 13: entity.UpdateEntityResponse.status:type_name -> entity.UpdateEntityResponse.Status - 6, // 14: entity.DeleteEntityResponse.error:type_name -> entity.EntityErrorInfo - 4, // 15: entity.DeleteEntityResponse.entity:type_name -> entity.Entity - 2, // 16: entity.DeleteEntityResponse.status:type_name -> entity.DeleteEntityResponse.Status - 4, // 17: entity.EntityHistoryResponse.versions:type_name -> entity.Entity - 27, // 18: entity.EntityListRequest.labels:type_name -> entity.EntityListRequest.LabelsEntry - 4, // 19: entity.EntityListResponse.results:type_name -> entity.Entity - 28, // 20: entity.EntityWatchRequest.labels:type_name -> entity.EntityWatchRequest.LabelsEntry - 4, // 21: entity.EntityWatchResponse.entity:type_name -> entity.Entity - 3, // 22: entity.EntityWatchResponse.action:type_name -> entity.EntityWatchResponse.Action + 0, // 4: entity.Entity.action:type_name -> entity.Entity.Action + 7, // 5: entity.BatchReadEntityRequest.batch:type_name -> entity.ReadEntityRequest + 4, // 6: entity.BatchReadEntityResponse.results:type_name -> entity.Entity + 4, // 7: entity.CreateEntityRequest.entity:type_name -> entity.Entity + 6, // 8: entity.CreateEntityResponse.error:type_name -> entity.EntityErrorInfo + 4, // 9: entity.CreateEntityResponse.entity:type_name -> entity.Entity + 1, // 10: entity.CreateEntityResponse.status:type_name -> entity.CreateEntityResponse.Status + 4, // 11: entity.UpdateEntityRequest.entity:type_name -> entity.Entity + 6, // 12: entity.UpdateEntityResponse.error:type_name -> entity.EntityErrorInfo + 4, // 13: entity.UpdateEntityResponse.entity:type_name -> entity.Entity + 2, // 14: entity.UpdateEntityResponse.status:type_name -> entity.UpdateEntityResponse.Status + 6, // 15: entity.DeleteEntityResponse.error:type_name -> entity.EntityErrorInfo + 4, // 16: entity.DeleteEntityResponse.entity:type_name -> entity.Entity + 3, // 17: entity.DeleteEntityResponse.status:type_name -> entity.DeleteEntityResponse.Status + 4, // 18: entity.EntityHistoryResponse.versions:type_name -> entity.Entity + 27, // 19: entity.EntityListRequest.labels:type_name -> entity.EntityListRequest.LabelsEntry + 4, // 20: entity.EntityListResponse.results:type_name -> entity.Entity + 28, // 21: entity.EntityWatchRequest.labels:type_name -> entity.EntityWatchRequest.LabelsEntry + 4, // 22: entity.EntityWatchResponse.entity:type_name -> entity.Entity 29, // 23: entity.EntitySummary.labels:type_name -> entity.EntitySummary.LabelsEntry 6, // 24: entity.EntitySummary.error:type_name -> entity.EntityErrorInfo 30, // 25: entity.EntitySummary.fields:type_name -> entity.EntitySummary.FieldsEntry diff --git a/pkg/services/store/entity/entity.proto b/pkg/services/store/entity/entity.proto index 200e35da0a..f44ba530f5 100644 --- a/pkg/services/store/entity/entity.proto +++ b/pkg/services/store/entity/entity.proto @@ -81,6 +81,18 @@ message Entity { // When errors exist repeated EntityErrorInfo errors = 21; + + // Action code + Action action = 3; + + // Status enumeration + enum Action { + UNKNOWN = 0; + CREATED = 1; + UPDATED = 2; + DELETED = 3; + ERROR = 4; + } } // This stores additional metadata for items entities that were synced from external systmes @@ -320,10 +332,10 @@ message EntityListResponse { //----------------------------------------------- message EntityWatchRequest { - // Timestamp of last changes. Empty will default to + // ResourceVersion of last changes. Empty will default to full history int64 since = 1; - // Watch sppecific entities + // Watch specific entities repeated string key = 2; // limit to a specific resource (empty is all) @@ -346,18 +358,8 @@ message EntityWatchResponse { // Timestamp the event was sent int64 timestamp = 1; - // List of entities with the same action - repeated Entity entity = 2; - - // Action code - Action action = 3; - - // Status enumeration - enum Action { - UNKNOWN = 0; - UPDATED = 1; - DELETED = 2; - } + // Entity that was created, updated, or deleted + Entity entity = 2; } message EntitySummary { diff --git a/pkg/services/store/entity/entity_grpc.pb.go b/pkg/services/store/entity/entity_grpc.pb.go index 2d0fd93a52..c1a7f6108d 100644 --- a/pkg/services/store/entity/entity_grpc.pb.go +++ b/pkg/services/store/entity/entity_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.3.0 -// - protoc v4.25.1 +// - protoc v4.25.2 // source: entity.proto package entity diff --git a/pkg/services/store/entity/sqlstash/broadcaster.go b/pkg/services/store/entity/sqlstash/broadcaster.go new file mode 100644 index 0000000000..eb5ea6f2b4 --- /dev/null +++ b/pkg/services/store/entity/sqlstash/broadcaster.go @@ -0,0 +1,254 @@ +package sqlstash + +import ( + "context" + "fmt" +) + +type ConnectFunc[T any] func(chan T) error + +type Broadcaster[T any] interface { + Subscribe(context.Context) (<-chan T, error) + Unsubscribe(chan T) +} + +func NewBroadcaster[T any](ctx context.Context, connect ConnectFunc[T]) (Broadcaster[T], error) { + b := &broadcaster[T]{} + err := b.start(ctx, connect) + if err != nil { + return nil, err + } + + return b, nil +} + +type broadcaster[T any] struct { + running bool + ctx context.Context + subs map[chan T]struct{} + cache Cache[T] + subscribe chan chan T + unsubscribe chan chan T +} + +func (b *broadcaster[T]) Subscribe(ctx context.Context) (<-chan T, error) { + if !b.running { + return nil, fmt.Errorf("broadcaster not running") + } + + sub := make(chan T, 100) + b.subscribe <- sub + go func() { + <-ctx.Done() + b.unsubscribe <- sub + }() + + return sub, nil +} + +func (b *broadcaster[T]) Unsubscribe(sub chan T) { + b.unsubscribe <- sub +} + +func (b *broadcaster[T]) start(ctx context.Context, connect ConnectFunc[T]) error { + if b.running { + return fmt.Errorf("broadcaster already running") + } + + stream := make(chan T, 100) + + err := connect(stream) + if err != nil { + return err + } + + b.ctx = ctx + + b.cache = NewCache[T](ctx, 100) + b.subscribe = make(chan chan T, 100) + b.unsubscribe = make(chan chan T, 100) + b.subs = make(map[chan T]struct{}) + + go b.stream(stream) + + b.running = true + return nil +} + +func (b *broadcaster[T]) stream(input chan T) { + for { + select { + // context cancelled + case <-b.ctx.Done(): + close(input) + for sub := range b.subs { + close(sub) + delete(b.subs, sub) + } + b.running = false + return + // new subscriber + case sub := <-b.subscribe: + // send initial batch of cached items + err := b.cache.ReadInto(sub) + if err != nil { + close(sub) + continue + } + + b.subs[sub] = struct{}{} + // unsubscribe + case sub := <-b.unsubscribe: + if _, ok := b.subs[sub]; ok { + close(sub) + delete(b.subs, sub) + } + // read item from input + case item, ok := <-input: + // input closed, drain subscribers and exit + if !ok { + for sub := range b.subs { + close(sub) + delete(b.subs, sub) + } + b.running = false + return + } + + b.cache.Add(item) + + for sub := range b.subs { + select { + case sub <- item: + default: + // Slow consumer, drop + b.unsubscribe <- sub + } + } + } + } +} + +const DefaultCacheSize = 100 + +type Cache[T any] interface { + Len() int + Add(item T) + Get(i int) T + Range(f func(T) error) error + Slice() []T + ReadInto(dst chan T) error +} + +type cache[T any] struct { + cache []T + size int + cacheZero int + cacheLen int + add chan T + read chan chan T + ctx context.Context +} + +func NewCache[T any](ctx context.Context, size int) Cache[T] { + c := &cache[T]{} + + c.ctx = ctx + if size <= 0 { + size = DefaultCacheSize + } + c.size = size + c.cache = make([]T, c.size) + + c.add = make(chan T) + c.read = make(chan chan T) + + go c.run() + + return c +} + +func (c *cache[T]) Len() int { + return c.cacheLen +} + +func (c *cache[T]) Add(item T) { + c.add <- item +} + +func (c *cache[T]) run() { + for { + select { + case <-c.ctx.Done(): + return + case item := <-c.add: + i := (c.cacheZero + c.cacheLen) % len(c.cache) + c.cache[i] = item + if c.cacheLen < len(c.cache) { + c.cacheLen++ + } else { + c.cacheZero = (c.cacheZero + 1) % len(c.cache) + } + case r := <-c.read: + read: + for i := 0; i < c.cacheLen; i++ { + select { + case r <- c.cache[(c.cacheZero+i)%len(c.cache)]: + // don't wait for slow consumers + default: + break read + } + } + close(r) + } + } +} + +func (c *cache[T]) Get(i int) T { + r := make(chan T, c.size) + c.read <- r + idx := 0 + for item := range r { + if idx == i { + return item + } + idx++ + } + var zero T + return zero +} + +func (c *cache[T]) Range(f func(T) error) error { + r := make(chan T, c.size) + c.read <- r + for item := range r { + err := f(item) + if err != nil { + return err + } + } + return nil +} + +func (c *cache[T]) Slice() []T { + s := make([]T, 0, c.size) + r := make(chan T, c.size) + c.read <- r + for item := range r { + s = append(s, item) + } + return s +} + +func (c *cache[T]) ReadInto(dst chan T) error { + r := make(chan T, c.size) + c.read <- r + for item := range r { + select { + case dst <- item: + default: + return fmt.Errorf("slow consumer") + } + } + return nil +} diff --git a/pkg/services/store/entity/sqlstash/broadcaster_test.go b/pkg/services/store/entity/sqlstash/broadcaster_test.go new file mode 100644 index 0000000000..fc6a6369a2 --- /dev/null +++ b/pkg/services/store/entity/sqlstash/broadcaster_test.go @@ -0,0 +1,106 @@ +package sqlstash + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestCache(t *testing.T) { + c := NewCache[int](context.Background(), 10) + + e := []int{} + err := c.Range(func(i int) error { + e = append(e, i) + return nil + }) + require.Nil(t, err) + require.Equal(t, 0, len(e)) + + c.Add(1) + + e = []int{} + err = c.Range(func(i int) error { + e = append(e, i) + return nil + }) + require.Nil(t, err) + require.Equal(t, 1, len(e)) + require.Equal(t, []int{1}, e) + require.Equal(t, 1, c.Get(0)) + + c.Add(2) + c.Add(3) + c.Add(4) + c.Add(5) + c.Add(6) + + // should be able to range over values + e = []int{} + err = c.Range(func(i int) error { + e = append(e, i) + return nil + }) + require.Nil(t, err) + require.Equal(t, 6, len(e)) + require.Equal(t, []int{1, 2, 3, 4, 5, 6}, e) + + // should be able to get length + require.Equal(t, 6, c.Len()) + + // should be able to get values + require.Equal(t, 1, c.Get(0)) + require.Equal(t, 6, c.Get(5)) + // zero value beyond cache size + require.Equal(t, 0, c.Get(6)) + require.Equal(t, 0, c.Get(20)) + require.Equal(t, 0, c.Get(-10)) + + // slice should return all values + require.Equal(t, []int{1, 2, 3, 4, 5, 6}, c.Slice()) + + c.Add(7) + c.Add(8) + c.Add(9) + c.Add(10) + c.Add(11) + + // should be able to range over values + e = []int{} + err = c.Range(func(i int) error { + e = append(e, i) + return nil + }) + require.Nil(t, err) + require.Equal(t, 10, len(e)) + require.Equal(t, []int{2, 3, 4, 5, 6, 7, 8, 9, 10, 11}, e) + + // should be able to get length + require.Equal(t, 10, c.Len()) + + // should be able to get values + require.Equal(t, 2, c.Get(0)) + require.Equal(t, 3, c.Get(1)) + + // slice should return all values + require.Equal(t, []int{2, 3, 4, 5, 6, 7, 8, 9, 10, 11}, c.Slice()) + + c.Add(12) + c.Add(13) + + // should be able to range over values + e = []int{} + err = c.Range(func(i int) error { + e = append(e, i) + return nil + }) + require.Nil(t, err) + require.Equal(t, 10, len(e)) + require.Equal(t, []int{4, 5, 6, 7, 8, 9, 10, 11, 12, 13}, e) + require.Equal(t, 4, c.Get(0)) + require.Equal(t, 5, c.Get(1)) + + // slice should return all values + require.Equal(t, []int{4, 5, 6, 7, 8, 9, 10, 11, 12, 13}, c.Slice()) +} diff --git a/pkg/services/store/entity/sqlstash/sql_storage_server.go b/pkg/services/store/entity/sqlstash/sql_storage_server.go index 655eed9283..3b4e51f2c2 100644 --- a/pkg/services/store/entity/sqlstash/sql_storage_server.go +++ b/pkg/services/store/entity/sqlstash/sql_storage_server.go @@ -29,26 +29,21 @@ import ( var _ entity.EntityStoreServer = &sqlEntityServer{} func ProvideSQLEntityServer(db db.EntityDBInterface /*, cfg *setting.Cfg */) (entity.EntityStoreServer, error) { - snode, err := snowflake.NewNode(rand.Int63n(1024)) - if err != nil { - return nil, err - } - entityServer := &sqlEntityServer{ - db: db, - log: log.New("sql-entity-server"), - snowflake: snode, + db: db, + log: log.New("sql-entity-server"), } return entityServer, nil } type sqlEntityServer struct { - log log.Logger - db db.EntityDBInterface // needed to keep xorm engine in scope - sess *session.SessionDB - dialect migrator.Dialect - snowflake *snowflake.Node + log log.Logger + db db.EntityDBInterface // needed to keep xorm engine in scope + sess *session.SessionDB + dialect migrator.Dialect + snowflake *snowflake.Node + broadcaster Broadcaster[*entity.Entity] } func (s *sqlEntityServer) Init() error { @@ -77,6 +72,24 @@ func (s *sqlEntityServer) Init() error { s.sess = sess s.dialect = migrator.NewDialect(engine.DriverName()) + + // initialize snowflake generator + s.snowflake, err = snowflake.NewNode(rand.Int63n(1024)) + if err != nil { + return err + } + + // set up the broadcaster + s.broadcaster, err = NewBroadcaster(context.Background(), func(stream chan *entity.Entity) error { + // start the poller + go s.poller(stream) + + return nil + }) + if err != nil { + return err + } + return nil } @@ -92,6 +105,7 @@ func (s *sqlEntityServer) getReadFields(r *entity.ReadEntityRequest) []string { "meta", "title", "slug", "description", "labels", "fields", "message", + "action", } if r.WithBody { @@ -138,6 +152,7 @@ func (s *sqlEntityServer) rowToEntity(ctx context.Context, rows *sql.Rows, r *en &raw.Meta, &raw.Title, &raw.Slug, &raw.Description, &labels, &fields, &raw.Message, + &raw.Action, } if r.WithBody { args = append(args, &raw.Body) @@ -423,7 +438,7 @@ func (s *sqlEntityServer) Create(ctx context.Context, r *entity.CreateEntityRequ current.Message = r.Entity.Message } - // Update version + // Update resource version current.ResourceVersion = s.snowflake.Generate().Int64() values := map[string]any{ @@ -455,6 +470,7 @@ func (s *sqlEntityServer) Create(ctx context.Context, r *entity.CreateEntityRequ "origin_key": current.Origin.Key, "origin_ts": current.Origin.Time, "message": current.Message, + "action": entity.Entity_CREATED, } // 1. Add row to the `entity_history` values @@ -627,7 +643,7 @@ func (s *sqlEntityServer) Update(ctx context.Context, r *entity.UpdateEntityRequ current.Message = r.Entity.Message } - // Update version + // Update resource version current.ResourceVersion = s.snowflake.Generate().Int64() values := map[string]any{ @@ -661,6 +677,7 @@ func (s *sqlEntityServer) Update(ctx context.Context, r *entity.UpdateEntityRequ "origin_key": current.Origin.Key, "origin_ts": current.Origin.Time, "message": current.Message, + "action": entity.Entity_UPDATED, } // 1. Add the `entity_history` values @@ -680,6 +697,7 @@ func (s *sqlEntityServer) Update(ctx context.Context, r *entity.UpdateEntityRequ delete(values, "name") delete(values, "created_at") delete(values, "created_by") + delete(values, "action") err = s.dialect.Update( ctx, @@ -792,13 +810,68 @@ func (s *sqlEntityServer) Delete(ctx context.Context, r *entity.DeleteEntityRequ } func (s *sqlEntityServer) doDelete(ctx context.Context, tx *session.SessionTx, ent *entity.Entity) error { - _, err := tx.Exec(ctx, "DELETE FROM entity WHERE guid=?", ent.Guid) + // Update resource version + ent.ResourceVersion = s.snowflake.Generate().Int64() + + labels, err := json.Marshal(ent.Labels) + if err != nil { + s.log.Error("error marshalling labels", "msg", err.Error()) + return err + } + + fields, err := json.Marshal(ent.Fields) + if err != nil { + s.log.Error("error marshalling fields", "msg", err.Error()) + return err + } + + errors, err := json.Marshal(ent.Errors) if err != nil { + s.log.Error("error marshalling errors", "msg", err.Error()) return err } - // TODO: keep history? would need current version bump, and the "write" would have to get from history - _, err = tx.Exec(ctx, "DELETE FROM entity_history WHERE guid=?", ent.Guid) + values := map[string]any{ + // below are only set in history table + "guid": ent.Guid, + "key": ent.Key, + "namespace": ent.Namespace, + "group": ent.Group, + "resource": ent.Resource, + "name": ent.Name, + "created_at": ent.CreatedAt, + "created_by": ent.CreatedBy, + // below are updated + "group_version": ent.GroupVersion, + "folder": ent.Folder, + "slug": ent.Slug, + "updated_at": ent.UpdatedAt, + "updated_by": ent.UpdatedBy, + "body": ent.Body, + "meta": ent.Meta, + "status": ent.Status, + "size": ent.Size, + "etag": ent.ETag, + "resource_version": ent.ResourceVersion, + "title": ent.Title, + "description": ent.Description, + "labels": labels, + "fields": fields, + "errors": errors, + "origin": ent.Origin.Source, + "origin_key": ent.Origin.Key, + "origin_ts": ent.Origin.Time, + "message": ent.Message, + "action": entity.Entity_DELETED, + } + + // 1. Add the `entity_history` values + if err := s.dialect.Insert(ctx, tx, "entity_history", values); err != nil { + s.log.Error("error inserting entity history", "msg", err.Error()) + return err + } + + _, err = tx.Exec(ctx, "DELETE FROM entity WHERE guid=?", ent.Guid) if err != nil { return err } @@ -1106,12 +1179,356 @@ func (s *sqlEntityServer) List(ctx context.Context, r *entity.EntityListRequest) return rsp, err } -func (s *sqlEntityServer) Watch(*entity.EntityWatchRequest, entity.EntityStore_WatchServer) error { +func (s *sqlEntityServer) Watch(r *entity.EntityWatchRequest, w entity.EntityStore_WatchServer) error { if err := s.Init(); err != nil { return err } - return fmt.Errorf("unimplemented") + user, err := appcontext.User(w.Context()) + if err != nil { + return err + } + if user == nil { + return fmt.Errorf("missing user in context") + } + + // collect and send any historical events + err = s.watchInit(w.Context(), r, w) + if err != nil { + return err + } + + // subscribe to new events + err = s.watch(w.Context(), r, w) + if err != nil { + s.log.Error("watch error", "err", err) + return err + } + + return nil +} + +// watchInit is a helper function to send the initial set of entities to the client +func (s *sqlEntityServer) watchInit(ctx context.Context, r *entity.EntityWatchRequest, w entity.EntityStore_WatchServer) error { + rr := &entity.ReadEntityRequest{ + WithBody: r.WithBody, + WithStatus: r.WithStatus, + } + + fields := s.getReadFields(rr) + + entityQuery := selectQuery{ + dialect: s.dialect, + fields: fields, + from: "entity", // the table + args: []any{}, + limit: 100, // r.Limit, + oneExtra: true, // request one more than the limit (and show next token if it exists) + } + + // if we got an initial resource version, start from that location in the history + fromZero := true + if r.Since > 0 { + entityQuery.from = "entity_history" + entityQuery.addWhere("resource_version > ?", r.Since) + fromZero = false + } + + // TODO fix this + // entityQuery.addWhere("namespace", user.OrgID) + + if len(r.Resource) > 0 { + entityQuery.addWhereIn("resource", r.Resource) + } + + if len(r.Key) > 0 { + where := []string{} + args := []any{} + for _, k := range r.Key { + key, err := entity.ParseKey(k) + if err != nil { + return err + } + + args = append(args, key.Namespace, key.Group, key.Resource) + whereclause := "(" + s.dialect.Quote("namespace") + "=? AND " + s.dialect.Quote("group") + "=? AND " + s.dialect.Quote("resource") + "=?" + if key.Name != "" { + args = append(args, key.Name) + whereclause += " AND " + s.dialect.Quote("name") + "=?" + } + whereclause += ")" + + where = append(where, whereclause) + } + + entityQuery.addWhere("("+strings.Join(where, " OR ")+")", args...) + } + + // Folder guid + if r.Folder != "" { + entityQuery.addWhere("folder", r.Folder) + } + + if len(r.Labels) > 0 { + var args []any + var conditions []string + for labelKey, labelValue := range r.Labels { + args = append(args, labelKey) + args = append(args, labelValue) + conditions = append(conditions, "(label = ? AND value = ?)") + } + query := "SELECT guid FROM entity_labels" + + " WHERE (" + strings.Join(conditions, " OR ") + ")" + + " GROUP BY guid" + + " HAVING COUNT(label) = ?" + args = append(args, len(r.Labels)) + + entityQuery.addWhereInSubquery("guid", query, args) + } + + entityQuery.addOrderBy("resource_version", Ascending) + + var err error + + s.log.Debug("watch init", "since", r.Since) + + for hasmore := true; hasmore; { + err = func() error { + query, args := entityQuery.toQuery() + + rows, err := s.sess.Query(ctx, query, args...) + if err != nil { + return err + } + defer func() { _ = rows.Close() }() + + found := int64(0) + + for rows.Next() { + found++ + if found > entityQuery.limit { + entityQuery.offset += entityQuery.limit + return nil + } + + result, err := s.rowToEntity(ctx, rows, rr) + if err != nil { + return err + } + + if result.ResourceVersion > r.Since { + r.Since = result.ResourceVersion + } + + if fromZero { + result.Action = entity.Entity_CREATED + } + + s.log.Debug("sending init event", "guid", result.Guid, "action", result.Action, "rv", result.ResourceVersion) + err = w.Send(&entity.EntityWatchResponse{ + Timestamp: time.Now().UnixMilli(), + Entity: result, + }) + if err != nil { + return err + } + } + + hasmore = false + return nil + }() + if err != nil { + return err + } + } + + return nil +} + +func (s *sqlEntityServer) poller(stream chan *entity.Entity) { + var err error + since := s.snowflake.Generate().Int64() + + t := time.NewTicker(5 * time.Second) + defer t.Stop() + + for range t.C { + since, err = s.poll(context.Background(), since, stream) + if err != nil { + s.log.Error("watch error", "err", err) + } + } +} + +func (s *sqlEntityServer) poll(ctx context.Context, since int64, out chan *entity.Entity) (int64, error) { + s.log.Debug("watch poll", "since", since) + + rr := &entity.ReadEntityRequest{ + WithBody: true, + WithStatus: true, + } + + fields := s.getReadFields(rr) + + for hasmore := true; hasmore; { + err := func() error { + entityQuery := selectQuery{ + dialect: s.dialect, + fields: fields, + from: "entity_history", // the table + args: []any{}, + limit: 100, // r.Limit, + // offset: 0, + oneExtra: true, // request one more than the limit (and show next token if it exists) + orderBy: []string{"resource_version"}, + } + + entityQuery.addWhere("resource_version > ?", since) + + query, args := entityQuery.toQuery() + + rows, err := s.sess.Query(ctx, query, args...) + if err != nil { + return err + } + defer func() { _ = rows.Close() }() + + found := int64(0) + for rows.Next() { + found++ + if found > entityQuery.limit { + return nil + } + + result, err := s.rowToEntity(ctx, rows, rr) + if err != nil { + return err + } + + if result.ResourceVersion > since { + since = result.ResourceVersion + } + + s.log.Debug("sending poll result", "guid", result.Guid, "action", result.Action, "rv", result.ResourceVersion) + out <- result + } + + hasmore = false + return nil + }() + if err != nil { + return since, err + } + } + + return since, nil +} + +func watchMatches(r *entity.EntityWatchRequest, result *entity.Entity) bool { + // Resource version too old + if result.ResourceVersion <= r.Since { + return false + } + + // Folder guid + if r.Folder != "" && r.Folder != result.Folder { + return false + } + + // must match at least one resource if specified + if len(r.Resource) > 0 { + matched := false + for _, res := range r.Resource { + if res == result.Resource { + matched = true + break + } + } + if !matched { + return false + } + } + + // must match at least one key if specified + if len(r.Key) > 0 { + matched := false + for _, k := range r.Key { + key, err := entity.ParseKey(k) + if err != nil { + return false + } + + if key.Namespace == result.Namespace && key.Group == result.Group && key.Resource == result.Resource && (key.Name == "" || key.Name == result.Name) { + matched = true + break + } + } + if !matched { + return false + } + } + + // must match at least one label/value pair if specified + // TODO should this require matching all label conditions? + if len(r.Labels) > 0 { + matched := false + for labelKey, labelValue := range r.Labels { + if result.Labels[labelKey] == labelValue { + matched = true + break + } + } + if !matched { + return false + } + } + + return true +} + +// watch is a helper to get the next set of entities and send them to the client +func (s *sqlEntityServer) watch(ctx context.Context, r *entity.EntityWatchRequest, w entity.EntityStore_WatchServer) error { + s.log.Debug("watch started", "since", r.Since) + + evts, err := s.broadcaster.Subscribe(w.Context()) + if err != nil { + return err + } + + for { + select { + // user closed the connection + case <-w.Context().Done(): + return nil + // got a raw result from the broadcaster + case result := <-evts: + // result doesn't match our watch params, skip it + if !watchMatches(r, result) { + s.log.Debug("watch result not matched", "guid", result.Guid, "action", result.Action, "rv", result.ResourceVersion) + break + } + + // remove the body and status if not requested + if !r.WithBody { + result.Body = nil + } + if !r.WithStatus { + result.Status = nil + } + + // update r.Since value so we don't send earlier results again + r.Since = result.ResourceVersion + + s.log.Debug("sending watch result", "guid", result.Guid, "action", result.Action, "rv", result.ResourceVersion) + err = w.Send(&entity.EntityWatchResponse{ + Timestamp: time.Now().UnixMilli(), + Entity: result, + }) + if err != nil { + return err + } + } + } } func (s *sqlEntityServer) FindReferences(ctx context.Context, r *entity.ReferenceRequest) (*entity.EntityListResponse, error) { From 7f970d48878b85ede6d52c69b5a2251899c71103 Mon Sep 17 00:00:00 2001 From: Hugo Kiyodi Oshiro Date: Tue, 5 Mar 2024 16:30:14 +0100 Subject: [PATCH 0399/1406] Plugins: Fetch instance provisioned plugins in cloud, to check full installation (#83784) --- public/app/features/plugins/admin/api.ts | 18 ++++++++++++- .../features/plugins/admin/helpers.test.ts | 24 +++++++++++++++++ public/app/features/plugins/admin/helpers.ts | 19 ++++++++++++-- .../features/plugins/admin/state/actions.ts | 26 ++++++++++++++++--- public/app/features/plugins/admin/types.ts | 4 +++ 5 files changed, 84 insertions(+), 7 deletions(-) diff --git a/public/app/features/plugins/admin/api.ts b/public/app/features/plugins/admin/api.ts index f90d1c581b..a86e32d6a1 100644 --- a/public/app/features/plugins/admin/api.ts +++ b/public/app/features/plugins/admin/api.ts @@ -4,7 +4,15 @@ import { accessControlQueryParam } from 'app/core/utils/accessControl'; import { API_ROOT, GCOM_API_ROOT, INSTANCE_API_ROOT } from './constants'; import { isLocalPluginVisibleByConfig, isRemotePluginVisibleByConfig } from './helpers'; -import { LocalPlugin, RemotePlugin, CatalogPluginDetails, Version, PluginVersion, InstancePlugin } from './types'; +import { + LocalPlugin, + RemotePlugin, + CatalogPluginDetails, + Version, + PluginVersion, + InstancePlugin, + ProvisionedPlugin, +} from './types'; export async function getPluginDetails(id: string): Promise { const remote = await getRemotePlugin(id); @@ -125,6 +133,14 @@ export async function getInstancePlugins(): Promise { return instancePlugins; } +export async function getProvisionedPlugins(): Promise { + const { items: provisionedPlugins }: { items: Array<{ type: string }> } = await getBackendSrv().get( + `${INSTANCE_API_ROOT}/provisioned-plugins` + ); + + return provisionedPlugins.map((plugin) => ({ slug: plugin.type })); +} + export async function installPlugin(id: string) { // This will install the latest compatible version based on the logic // on the backend. diff --git a/public/app/features/plugins/admin/helpers.test.ts b/public/app/features/plugins/admin/helpers.test.ts index faef81ce92..27887a93d1 100644 --- a/public/app/features/plugins/admin/helpers.test.ts +++ b/public/app/features/plugins/admin/helpers.test.ts @@ -112,6 +112,30 @@ describe('Plugins/Helpers', () => { config.featureToggles.managedPluginsInstall = oldFeatureTogglesManagedPluginsInstall; config.pluginAdminExternalManageEnabled = oldPluginAdminExternalManageEnabled; }); + + test('plugins should be fully installed if they are installed and it is provisioned', () => { + const pluginId = 'plugin-1'; + + const oldFeatureTogglesManagedPluginsInstall = config.featureToggles.managedPluginsInstall; + const oldPluginAdminExternalManageEnabled = config.pluginAdminExternalManageEnabled; + + config.featureToggles.managedPluginsInstall = true; + config.pluginAdminExternalManageEnabled = true; + + const merged = mergeLocalsAndRemotes({ + local: [...localPlugins, getLocalPluginMock({ id: pluginId })], + remote: [...remotePlugins, getRemotePluginMock({ slug: pluginId })], + provisioned: [{ slug: pluginId }], + }); + const findMerged = (mergedId: string) => merged.find(({ id }) => id === mergedId); + + expect(merged).toHaveLength(5); + expect(findMerged(pluginId)).not.toBeUndefined(); + expect(findMerged(pluginId)?.isFullyInstalled).toBe(true); + + config.featureToggles.managedPluginsInstall = oldFeatureTogglesManagedPluginsInstall; + config.pluginAdminExternalManageEnabled = oldPluginAdminExternalManageEnabled; + }); }); describe('mergeLocalAndRemote()', () => { diff --git a/public/app/features/plugins/admin/helpers.ts b/public/app/features/plugins/admin/helpers.ts index 950496c28e..c373e440dc 100644 --- a/public/app/features/plugins/admin/helpers.ts +++ b/public/app/features/plugins/admin/helpers.ts @@ -7,17 +7,27 @@ import { contextSrv } from 'app/core/core'; import { getBackendSrv } from 'app/core/services/backend_srv'; import { AccessControlAction } from 'app/types'; -import { CatalogPlugin, InstancePlugin, LocalPlugin, RemotePlugin, RemotePluginStatus, Version } from './types'; +import { + CatalogPlugin, + InstancePlugin, + LocalPlugin, + ProvisionedPlugin, + RemotePlugin, + RemotePluginStatus, + Version, +} from './types'; export function mergeLocalsAndRemotes({ local = [], remote = [], instance = [], + provisioned = [], pluginErrors: errors, }: { local: LocalPlugin[]; remote?: RemotePlugin[]; instance?: InstancePlugin[]; + provisioned?: ProvisionedPlugin[]; pluginErrors?: PluginError[]; }): CatalogPlugin[] { const catalogPlugins: CatalogPlugin[] = []; @@ -28,6 +38,11 @@ export function mergeLocalsAndRemotes({ return map; }, new Map()); + const provisionedSet = provisioned.reduce((map, provisionedPlugin) => { + map.add(provisionedPlugin.slug); + return map; + }, new Set()); + // add locals local.forEach((localPlugin) => { const remoteCounterpart = remote.find((r) => r.slug === localPlugin.id); @@ -51,7 +66,7 @@ export function mergeLocalsAndRemotes({ if (configCore.featureToggles.managedPluginsInstall && config.pluginAdminExternalManageEnabled) { catalogPlugin.isFullyInstalled = catalogPlugin.isCore ? true - : instancesMap.has(remotePlugin.slug) && catalogPlugin.isInstalled; + : (instancesMap.has(remotePlugin.slug) || provisionedSet.has(remotePlugin.slug)) && catalogPlugin.isInstalled; catalogPlugin.isInstalled = instancesMap.has(remotePlugin.slug) || catalogPlugin.isInstalled; diff --git a/public/app/features/plugins/admin/state/actions.ts b/public/app/features/plugins/admin/state/actions.ts index 0afc9560eb..bff4b094b5 100644 --- a/public/app/features/plugins/admin/state/actions.ts +++ b/public/app/features/plugins/admin/state/actions.ts @@ -16,10 +16,11 @@ import { installPlugin, uninstallPlugin, getInstancePlugins, + getProvisionedPlugins, } from '../api'; import { STATE_PREFIX } from '../constants'; import { mapLocalToCatalog, mergeLocalsAndRemotes, updatePanels } from '../helpers'; -import { CatalogPlugin, RemotePlugin, LocalPlugin, InstancePlugin } from '../types'; +import { CatalogPlugin, RemotePlugin, LocalPlugin, InstancePlugin, ProvisionedPlugin } from '../types'; // Fetches export const fetchAll = createAsyncThunk(`${STATE_PREFIX}/fetchAll`, async (_, thunkApi) => { @@ -31,6 +32,10 @@ export const fetchAll = createAsyncThunk(`${STATE_PREFIX}/fetchAll`, async (_, t config.pluginAdminExternalManageEnabled && configCore.featureToggles.managedPluginsInstall ? from(getInstancePlugins()) : of(undefined); + const provisioned$ = + config.pluginAdminExternalManageEnabled && configCore.featureToggles.managedPluginsInstall + ? from(getProvisionedPlugins()) + : of(undefined); const TIMEOUT = 500; const pluginErrors$ = from(getPluginErrors()); const local$ = from(getLocalPlugins()); @@ -48,6 +53,7 @@ export const fetchAll = createAsyncThunk(`${STATE_PREFIX}/fetchAll`, async (_, t local: local$, remote: remote$, instance: instance$, + provisioned: provisioned$, pluginErrors: pluginErrors$, }) .pipe( @@ -66,13 +72,21 @@ export const fetchAll = createAsyncThunk(`${STATE_PREFIX}/fetchAll`, async (_, t if (remote.length > 0) { const local = await lastValueFrom(local$); const instance = await lastValueFrom(instance$); + const provisioned = await lastValueFrom(provisioned$); const pluginErrors = await lastValueFrom(pluginErrors$); - thunkApi.dispatch(addPlugins(mergeLocalsAndRemotes({ local, remote, instance, pluginErrors }))); + thunkApi.dispatch( + addPlugins(mergeLocalsAndRemotes({ local, remote, instance, provisioned, pluginErrors })) + ); } }); - return forkJoin({ local: local$, instance: instance$, pluginErrors: pluginErrors$ }); + return forkJoin({ + local: local$, + instance: instance$, + provisioned: provisioned$, + pluginErrors: pluginErrors$, + }); }, }) ) @@ -81,18 +95,22 @@ export const fetchAll = createAsyncThunk(`${STATE_PREFIX}/fetchAll`, async (_, t local, remote, instance, + provisioned, pluginErrors, }: { local: LocalPlugin[]; remote?: RemotePlugin[]; instance?: InstancePlugin[]; + provisioned?: ProvisionedPlugin[]; pluginErrors: PluginError[]; }) => { // Both local and remote plugins are loaded if (local && remote) { thunkApi.dispatch({ type: `${STATE_PREFIX}/fetchLocal/fulfilled` }); thunkApi.dispatch({ type: `${STATE_PREFIX}/fetchRemote/fulfilled` }); - thunkApi.dispatch(addPlugins(mergeLocalsAndRemotes({ local, remote, instance, pluginErrors }))); + thunkApi.dispatch( + addPlugins(mergeLocalsAndRemotes({ local, remote, instance, provisioned, pluginErrors })) + ); // Only remote plugins are loaded (remote timed out) } else if (local) { diff --git a/public/app/features/plugins/admin/types.ts b/public/app/features/plugins/admin/types.ts index 7c54128e00..5cc0089fb9 100644 --- a/public/app/features/plugins/admin/types.ts +++ b/public/app/features/plugins/admin/types.ts @@ -322,3 +322,7 @@ export type InstancePlugin = { pluginSlug: string; version: string; }; + +export type ProvisionedPlugin = { + slug: string; +}; From b3efb4217e48656f24aacdffd5737595d7361afe Mon Sep 17 00:00:00 2001 From: Carl Bergquist Date: Tue, 5 Mar 2024 16:41:19 +0100 Subject: [PATCH 0400/1406] Cfg: Adds experimental scope grafana.ini settings (#83174) Signed-off-by: bergquist --- .../src/types/featureToggles.gen.ts | 1 + pkg/api/dtos/frontend_settings.go | 3 + pkg/api/frontendsettings.go | 6 + pkg/services/featuremgmt/registry.go | 11 + pkg/services/featuremgmt/toggles_gen.csv | 1 + pkg/services/featuremgmt/toggles_gen.go | 4 + pkg/services/featuremgmt/toggles_gen.json | 2024 ++++++++--------- pkg/setting/setting.go | 9 + 8 files changed, 1016 insertions(+), 1043 deletions(-) diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index f506005041..b2eb5ceabc 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -179,4 +179,5 @@ export interface FeatureToggles { expressionParser?: boolean; groupByVariable?: boolean; alertingUpgradeDryrunOnStart?: boolean; + scopeFilters?: boolean; } diff --git a/pkg/api/dtos/frontend_settings.go b/pkg/api/dtos/frontend_settings.go index 10c7853176..b739c4e5ad 100644 --- a/pkg/api/dtos/frontend_settings.go +++ b/pkg/api/dtos/frontend_settings.go @@ -262,4 +262,7 @@ type FrontendSettingsDTO struct { Whitelabeling *FrontendSettingsWhitelabelingDTO `json:"whitelabeling,omitempty"` LocalFileSystemAvailable bool `json:"localFileSystemAvailable"` + // Experimental Scope settings + ListScopesEndpoint string `json:"listScopesEndpoint"` + ListDashboardScopesEndpoint string `json:"listDashboardScopesEndpoint"` } diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index dc3d329692..1a12e90f6e 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -356,6 +356,12 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro // Set the kubernetes namespace frontendSettings.Namespace = hs.namespacer(c.SignedInUser.OrgID) + // experimental scope features + if hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagScopeFilters) { + frontendSettings.ListScopesEndpoint = hs.Cfg.ScopesListScopesURL + frontendSettings.ListDashboardScopesEndpoint = hs.Cfg.ScopesListDashboardsURL + } + return frontendSettings, nil } diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index e0ed2d0426..5dc122d423 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1197,6 +1197,17 @@ var ( RequiresRestart: true, Expression: "true", // enabled by default }, + { + Name: "scopeFilters", + Description: "Enables the use of scope filters in Grafana", + FrontendOnly: false, + Stage: FeatureStageExperimental, + Owner: grafanaDashboardsSquad, + RequiresRestart: false, + AllowSelfServe: false, + HideFromDocs: true, + HideFromAdminPage: true, + }, } ) diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index e271f075a2..a53756fa9a 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -160,3 +160,4 @@ kubernetesAggregator,experimental,@grafana/grafana-app-platform-squad,false,true expressionParser,experimental,@grafana/grafana-app-platform-squad,false,true,false groupByVariable,experimental,@grafana/dashboards-squad,false,false,false alertingUpgradeDryrunOnStart,GA,@grafana/alerting-squad,false,true,false +scopeFilters,experimental,@grafana/dashboards-squad,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 1e0efe5ca4..5041079396 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -650,4 +650,8 @@ const ( // FlagAlertingUpgradeDryrunOnStart // When activated in legacy alerting mode, this initiates a dry-run of the Unified Alerting upgrade during each startup. It logs any issues detected without implementing any actual changes. FlagAlertingUpgradeDryrunOnStart = "alertingUpgradeDryrunOnStart" + + // FlagScopeFilters + // Enables the use of scope filters in Grafana + FlagScopeFilters = "scopeFilters" ) diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 29aa613186..810e3edaba 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -5,332 +5,307 @@ "items": [ { "metadata": { - "name": "recoveryThreshold", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" - }, - "spec": { - "description": "Enables feature recovery threshold (aka hysteresis) for threshold server-side expression", - "stage": "GA", - "codeowner": "@grafana/alerting-squad", - "requiresRestart": true - } - }, - { - "metadata": { - "name": "teamHttpHeaders", - "resourceVersion": "1709290509653", - "creationTimestamp": "2024-02-16T18:36:28Z", - "annotations": { - "grafana.app/updatedTimestamp": "2024-03-01 10:55:09.653587 +0000 UTC" - } - }, - "spec": { - "description": "Enables Team LBAC for datasources to apply team headers to the client requests", - "stage": "preview", - "codeowner": "@grafana/identity-access-team" - } - }, - { - "metadata": { - "name": "logsExploreTableVisualisation", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "nestedFolderPicker", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "A table visualisation for logs in Explore", + "description": "Enables the new folder picker to work with nested folders. Requires the nestedFolders feature toggle", "stage": "GA", - "codeowner": "@grafana/observability-logs", - "frontend": true + "codeowner": "@grafana/grafana-frontend-platform", + "frontend": true, + "allowSelfServe": true } }, { "metadata": { - "name": "disableSecretsCompatibility", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "alertingBacktesting", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Disable duplicated secret storage in legacy tables", + "description": "Rule backtesting API for alerting", "stage": "experimental", - "codeowner": "@grafana/hosted-grafana-team", - "requiresRestart": true + "codeowner": "@grafana/alerting-squad" } }, { "metadata": { - "name": "alertingBacktesting", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "featureToggleAdminPage", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Rule backtesting API for alerting", + "description": "Enable admin page for managing feature toggles from the Grafana front-end", "stage": "experimental", - "codeowner": "@grafana/alerting-squad" + "codeowner": "@grafana/grafana-operator-experience-squad", + "requiresRestart": true } }, { "metadata": { - "name": "alertStateHistoryLokiPrimary", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "logsInfiniteScrolling", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Enable a remote Loki instance as the primary source for state history reads.", + "description": "Enables infinite scrolling for the Logs panel in Explore and Dashboards", "stage": "experimental", - "codeowner": "@grafana/alerting-squad" + "codeowner": "@grafana/observability-logs", + "frontend": true } }, { "metadata": { - "name": "pluginsInstrumentationStatusSource", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z", - "deletionTimestamp": "2024-03-05T11:44:12Z" + "name": "alertingPreviewUpgrade", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Include a status source label for plugin request metrics and logs", - "stage": "experimental", - "codeowner": "@grafana/plugins-platform-backend" + "description": "Show Unified Alerting preview and upgrade page in legacy alerting", + "stage": "GA", + "codeowner": "@grafana/alerting-squad", + "requiresRestart": true } }, { "metadata": { - "name": "enablePluginsTracingByDefault", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "cloudRBACRoles", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Enable plugin tracing for all external plugins", + "description": "Enabled grafana cloud specific RBAC roles", "stage": "experimental", - "codeowner": "@grafana/plugins-platform-backend", - "requiresRestart": true + "codeowner": "@grafana/identity-access-team", + "requiresRestart": true, + "hideFromDocs": true } }, { "metadata": { - "name": "newFolderPicker", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "awsDatasourcesTempCredentials", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Enables the nested folder picker without having nested folders enabled", + "description": "Support temporary security credentials in AWS plugins for Grafana Cloud customers", "stage": "experimental", - "codeowner": "@grafana/grafana-frontend-platform", - "frontend": true + "codeowner": "@grafana/aws-datasources" } }, { "metadata": { - "name": "correlations", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "externalServiceAccounts", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Correlations page", - "stage": "GA", - "codeowner": "@grafana/explore-squad", - "allowSelfServe": true + "description": "Automatic service account and token setup for plugins", + "stage": "preview", + "codeowner": "@grafana/identity-access-team", + "hideFromAdminPage": true } }, { "metadata": { - "name": "datasourceQueryMultiStatus", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "transformationsVariableSupport", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Introduce HTTP 207 Multi Status for api/ds/query", - "stage": "experimental", - "codeowner": "@grafana/plugins-platform-backend" + "description": "Allows using variables in transformations", + "stage": "preview", + "codeowner": "@grafana/dataviz-squad", + "frontend": true } }, { "metadata": { - "name": "sqlDatasourceDatabaseSelection", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "panelTitleSearch", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Enables previous SQL data source dataset dropdown behavior", + "description": "Search for dashboards using panel title", "stage": "preview", - "codeowner": "@grafana/dataviz-squad", - "frontend": true, + "codeowner": "@grafana/grafana-app-platform-squad", "hideFromAdminPage": true } }, { "metadata": { - "name": "cloudWatchWildCardDimensionValues", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "enableDatagridEditing", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Fetches dimension values from CloudWatch to correctly label wildcard dimensions", - "stage": "GA", - "codeowner": "@grafana/aws-datasources", - "allowSelfServe": true + "description": "Enables the edit functionality in the datagrid panel", + "stage": "preview", + "codeowner": "@grafana/dataviz-squad", + "frontend": true } }, { "metadata": { - "name": "addFieldFromCalculationStatFunctions", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "traceQLStreaming", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Add cumulative and window functions to the add field from calculation transformation", - "stage": "preview", - "codeowner": "@grafana/dataviz-squad", + "description": "Enables response streaming of TraceQL queries of the Tempo data source", + "stage": "experimental", + "codeowner": "@grafana/observability-traces-and-profiling", "frontend": true } }, { "metadata": { - "name": "canvasPanelPanZoom", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "metricsSummary", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Allow pan and zoom in canvas panel", - "stage": "preview", - "codeowner": "@grafana/dataviz-squad", + "description": "Enables metrics summary queries in the Tempo data source", + "stage": "experimental", + "codeowner": "@grafana/observability-traces-and-profiling", "frontend": true } }, { "metadata": { - "name": "cloudRBACRoles", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "sseGroupByDatasource", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Enabled grafana cloud specific RBAC roles", + "description": "Send query to the same datasource in a single request when using server side expressions. The `cloudWatchBatchQueries` feature toggle should be enabled if this used with CloudWatch.", "stage": "experimental", - "codeowner": "@grafana/identity-access-team", - "requiresRestart": true, - "hideFromDocs": true + "codeowner": "@grafana/observability-metrics" } }, { "metadata": { - "name": "panelTitleSearch", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "live-service-web-worker", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Search for dashboards using panel title", - "stage": "preview", + "description": "This will use a webworker thread to processes events rather than the main thread", + "stage": "experimental", "codeowner": "@grafana/grafana-app-platform-squad", - "hideFromAdminPage": true + "frontend": true } }, { "metadata": { - "name": "mysqlAnsiQuotes", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "canvasPanelNesting", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Use double quotes to escape keyword in a MySQL query", + "description": "Allow elements nesting", "stage": "experimental", - "codeowner": "@grafana/backend-platform" + "codeowner": "@grafana/dataviz-squad", + "frontend": true, + "hideFromAdminPage": true } }, { "metadata": { - "name": "alertStateHistoryLokiSecondary", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "enableElasticsearchBackendQuerying", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Enable Grafana to write alert state history to an external Loki instance in addition to Grafana annotations.", - "stage": "experimental", - "codeowner": "@grafana/alerting-squad" + "description": "Enable the processing of queries and responses in the Elasticsearch data source through backend", + "stage": "GA", + "codeowner": "@grafana/observability-logs", + "allowSelfServe": true } }, { "metadata": { - "name": "grafanaAPIServerWithExperimentalAPIs", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "extractFieldsNameDeduplication", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Register experimental APIs with the k8s API server", + "description": "Make sure extracted field names are unique in the dataframe", "stage": "experimental", - "codeowner": "@grafana/grafana-app-platform-squad", - "requiresDevMode": true, - "requiresRestart": true + "codeowner": "@grafana/dataviz-squad", + "frontend": true } }, { "metadata": { - "name": "alertingSimplifiedRouting", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "alertStateHistoryLokiPrimary", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Enables users to easily configure alert notifications by specifying a contact point directly when editing or creating an alert rule", - "stage": "preview", + "description": "Enable a remote Loki instance as the primary source for state history reads.", + "stage": "experimental", "codeowner": "@grafana/alerting-squad" } }, { "metadata": { - "name": "autoMigratePiechartPanel", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "dashboardEmbed", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Migrate old piechart panel to supported piechart panel - broken out from autoMigrateOldPanels to enable granular tracking", - "stage": "preview", - "codeowner": "@grafana/dataviz-squad", + "description": "Allow embedding dashboard for external use in Code editors", + "stage": "experimental", + "codeowner": "@grafana/grafana-as-code", "frontend": true } }, { "metadata": { - "name": "autoMigrateWorldmapPanel", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "sqlDatasourceDatabaseSelection", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Migrate old worldmap panel to supported geomap panel - broken out from autoMigrateOldPanels to enable granular tracking", + "description": "Enables previous SQL data source dataset dropdown behavior", "stage": "preview", "codeowner": "@grafana/dataviz-squad", - "frontend": true + "frontend": true, + "hideFromAdminPage": true } }, { "metadata": { - "name": "prometheusIncrementalQueryInstrumentation", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "permissionsFilterRemoveSubquery", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Adds RudderStack events to incremental queries", + "description": "Alternative permission filter implementation that does not use subqueries for fetching the dashboard folder", "stage": "experimental", - "codeowner": "@grafana/observability-metrics", - "frontend": true + "codeowner": "@grafana/backend-platform" } }, { "metadata": { - "name": "prometheusConfigOverhaulAuth", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "awsDatasourcesNewFormStyling", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Update the Prometheus configuration page with the new auth component", - "stage": "GA", - "codeowner": "@grafana/observability-metrics" + "description": "Applies new form styling for configuration and query editors in AWS plugins", + "stage": "preview", + "codeowner": "@grafana/aws-datasources", + "frontend": true } }, { "metadata": { "name": "prometheusPromQAIL", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { "description": "Prometheus and AI/ML to assist users in creating a query", @@ -341,217 +316,230 @@ }, { "metadata": { - "name": "panelFilterVariable", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "scopeFilters", + "resourceVersion": "1709648534592", + "creationTimestamp": "2024-03-05T14:17:16Z", + "annotations": { + "grafana.app/updatedTimestamp": "2024-03-05 14:22:14.592841 +0000 UTC" + } }, "spec": { - "description": "Enables use of the `systemPanelFilterVar` variable to filter panels in a dashboard", + "description": "Enables the use of scope filters in Grafana", "stage": "experimental", "codeowner": "@grafana/dashboards-squad", - "frontend": true, + "hideFromAdminPage": true, "hideFromDocs": true } }, { "metadata": { - "name": "pluginsSkipHostEnvVars", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "renderAuthJWT", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Disables passing host environment variable to plugin processes", - "stage": "experimental", - "codeowner": "@grafana/plugins-platform-backend" + "description": "Uses JWT-based auth for rendering instead of relying on remote cache", + "stage": "preview", + "codeowner": "@grafana/grafana-as-code", + "hideFromAdminPage": true } }, { "metadata": { - "name": "publicDashboardsEmailSharing", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "groupToNestedTableTransformation", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Enables public dashboard sharing to be restricted to only allowed emails", + "description": "Enables the group to nested table transformation", "stage": "preview", - "codeowner": "@grafana/sharing-squad", - "hideFromAdminPage": true, - "hideFromDocs": true + "codeowner": "@grafana/dataviz-squad", + "frontend": true } }, { "metadata": { - "name": "metricsSummary", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "autoMigrateOldPanels", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Enables metrics summary queries in the Tempo data source", - "stage": "experimental", - "codeowner": "@grafana/observability-traces-and-profiling", + "description": "Migrate old angular panels to supported versions (graph, table-old, worldmap, etc)", + "stage": "preview", + "codeowner": "@grafana/dataviz-squad", "frontend": true } }, { "metadata": { - "name": "tableSharedCrosshair", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "showDashboardValidationWarnings", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Enables shared crosshair in table panel", + "description": "Show warnings when dashboards do not validate against the schema", "stage": "experimental", - "codeowner": "@grafana/dataviz-squad", - "frontend": true + "codeowner": "@grafana/dashboards-squad" } }, { "metadata": { - "name": "regressionTransformation", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "alertmanagerRemoteSecondary", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Enables regression analysis transformation", - "stage": "preview", - "codeowner": "@grafana/dataviz-squad", - "frontend": true + "description": "Enable Grafana to sync configuration and state with a remote Alertmanager.", + "stage": "experimental", + "codeowner": "@grafana/alerting-squad" } }, { "metadata": { - "name": "queryOverLive", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "disableAngular", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Use Grafana Live WebSocket to execute backend queries", + "description": "Dynamic flag to disable angular at runtime. The preferred method is to set `angular_support_enabled` to `false` in the [security] settings, which allows you to change the state at runtime.", + "stage": "preview", + "codeowner": "@grafana/dataviz-squad", + "frontend": true, + "hideFromAdminPage": true + } + }, + { + "metadata": { + "name": "disableSecretsCompatibility", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" + }, + "spec": { + "description": "Disable duplicated secret storage in legacy tables", "stage": "experimental", - "codeowner": "@grafana/grafana-app-platform-squad", - "frontend": true + "codeowner": "@grafana/hosted-grafana-team", + "requiresRestart": true } }, { "metadata": { - "name": "prometheusDataplane", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "alertingNoNormalState", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Changes responses to from Prometheus to be compliant with the dataplane specification. In particular, when this feature toggle is active, the numeric `Field.Name` is set from 'Value' to the value of the `__name__` label.", - "stage": "GA", - "codeowner": "@grafana/observability-metrics", - "allowSelfServe": true + "description": "Stop maintaining state of alerts that are not firing", + "stage": "preview", + "codeowner": "@grafana/alerting-squad", + "hideFromAdminPage": true } }, { "metadata": { - "name": "unifiedRequestLog", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "pluginsDynamicAngularDetectionPatterns", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Writes error logs to the request logger", + "description": "Enables fetching Angular detection patterns for plugins from GCOM and fallback to hardcoded ones", "stage": "experimental", - "codeowner": "@grafana/backend-platform" + "codeowner": "@grafana/plugins-platform-backend" } }, { "metadata": { - "name": "wargamesTesting", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "jitterAlertRulesWithinGroups", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Placeholder feature flag for internal testing", - "stage": "experimental", - "codeowner": "@grafana/hosted-grafana-team" + "description": "Distributes alert rule evaluations more evenly over time, including spreading out rules within the same group", + "stage": "preview", + "codeowner": "@grafana/alerting-squad", + "requiresRestart": true, + "hideFromDocs": true } }, { "metadata": { - "name": "alertmanagerRemotePrimary", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "storage", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Enable Grafana to have a remote Alertmanager instance as the primary Alertmanager.", + "description": "Configurable storage for dashboards, datasources, and resources", "stage": "experimental", - "codeowner": "@grafana/alerting-squad" + "codeowner": "@grafana/grafana-app-platform-squad" } }, { "metadata": { - "name": "ssoSettingsApi", - "resourceVersion": "1708503560010", - "creationTimestamp": "2024-02-16T18:36:28Z", - "annotations": { - "grafana.app/updatedTimestamp": "2024-02-21 08:19:20.010283 +0000 UTC" - } + "name": "autoMigratePiechartPanel", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Enables the SSO settings API and the OAuth configuration UIs in Grafana", + "description": "Migrate old piechart panel to supported piechart panel - broken out from autoMigrateOldPanels to enable granular tracking", "stage": "preview", - "codeowner": "@grafana/identity-access-team", - "allowSelfServe": true + "codeowner": "@grafana/dataviz-squad", + "frontend": true } }, { "metadata": { - "name": "logsInfiniteScrolling", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "autoMigrateWorldmapPanel", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Enables infinite scrolling for the Logs panel in Explore and Dashboards", - "stage": "experimental", - "codeowner": "@grafana/observability-logs", + "description": "Migrate old worldmap panel to supported geomap panel - broken out from autoMigrateOldPanels to enable granular tracking", + "stage": "preview", + "codeowner": "@grafana/dataviz-squad", "frontend": true } }, { "metadata": { - "name": "displayAnonymousStats", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z", - "deletionTimestamp": "2024-03-05T11:44:12Z" + "name": "redshiftAsyncQueryDataSupport", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Enables anonymous stats to be shown in the UI for Grafana", + "description": "Enable async query data support for Redshift", "stage": "GA", - "codeowner": "@grafana/identity-access-team", - "frontend": true + "codeowner": "@grafana/aws-datasources" } }, { "metadata": { - "name": "storage", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "influxdbBackendMigration", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Configurable storage for dashboards, datasources, and resources", - "stage": "experimental", - "codeowner": "@grafana/grafana-app-platform-squad" + "description": "Query InfluxDB InfluxQL without the proxy", + "stage": "GA", + "codeowner": "@grafana/observability-metrics", + "frontend": true } }, { "metadata": { - "name": "newPDFRendering", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "lokiLogsDataplane", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "New implementation for the dashboard to PDF rendering", + "description": "Changes logs responses from Loki to be compliant with the dataplane specification.", "stage": "experimental", - "codeowner": "@grafana/sharing-squad" + "codeowner": "@grafana/observability-logs" } }, { "metadata": { "name": "pluginsFrontendSandbox", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { "description": "Enables the plugins frontend sandbox", @@ -562,466 +550,483 @@ }, { "metadata": { - "name": "logRequestsInstrumentedAsUnknown", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "alertingNoDataErrorExecution", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Logs the path for requests that are instrumented as unknown", - "stage": "experimental", - "codeowner": "@grafana/hosted-grafana-team" + "description": "Changes how Alerting state manager handles execution of NoData/Error", + "stage": "GA", + "codeowner": "@grafana/alerting-squad", + "requiresRestart": true } }, { "metadata": { - "name": "lokiStructuredMetadata", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "nodeGraphDotLayout", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Enables the loki data source to request structured metadata from the Loki server", - "stage": "GA", - "codeowner": "@grafana/observability-logs" + "description": "Changed the layout algorithm for the node graph", + "stage": "experimental", + "codeowner": "@grafana/observability-traces-and-profiling", + "frontend": true } }, { "metadata": { - "name": "managedPluginsInstall", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "accessControlOnCall", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Install managed plugins directly from plugins catalog", + "description": "Access control primitives for OnCall", "stage": "preview", - "codeowner": "@grafana/plugins-platform-backend" + "codeowner": "@grafana/identity-access-team", + "hideFromAdminPage": true } }, { "metadata": { - "name": "dashboardSceneForViewers", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "prometheusDataplane", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Enables dashboard rendering using Scenes for viewer roles", - "stage": "experimental", - "codeowner": "@grafana/dashboards-squad", - "frontend": true + "description": "Changes responses to from Prometheus to be compliant with the dataplane specification. In particular, when this feature toggle is active, the numeric `Field.Name` is set from 'Value' to the value of the `__name__` label.", + "stage": "GA", + "codeowner": "@grafana/observability-metrics", + "allowSelfServe": true } }, { "metadata": { - "name": "alertingDetailsViewV2", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "vizAndWidgetSplit", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Enables the preview of the new alert details view", + "description": "Split panels between visualizations and widgets", "stage": "experimental", - "codeowner": "@grafana/alerting-squad", - "frontend": true, - "hideFromDocs": true + "codeowner": "@grafana/dashboards-squad", + "frontend": true } }, { "metadata": { - "name": "jitterAlertRulesWithinGroups", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "reportingRetries", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Distributes alert rule evaluations more evenly over time, including spreading out rules within the same group", + "description": "Enables rendering retries for the reporting feature", "stage": "preview", - "codeowner": "@grafana/alerting-squad", - "requiresRestart": true, - "hideFromDocs": true + "codeowner": "@grafana/sharing-squad", + "requiresRestart": true } }, { "metadata": { - "name": "unifiedStorage", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "panelTitleSearchInV1", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "SQL-based k8s storage", + "description": "Enable searching for dashboards using panel title in search v1", "stage": "experimental", - "codeowner": "@grafana/grafana-app-platform-squad", - "requiresDevMode": true, - "requiresRestart": true + "codeowner": "@grafana/backend-platform", + "requiresDevMode": true } }, { "metadata": { - "name": "enableElasticsearchBackendQuerying", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "alertingUpgradeDryrunOnStart", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Enable the processing of queries and responses in the Elasticsearch data source through backend", + "description": "When activated in legacy alerting mode, this initiates a dry-run of the Unified Alerting upgrade during each startup. It logs any issues detected without implementing any actual changes.", "stage": "GA", - "codeowner": "@grafana/observability-logs", - "allowSelfServe": true + "codeowner": "@grafana/alerting-squad", + "requiresRestart": true } }, { "metadata": { - "name": "cloudWatchLogsMonacoEditor", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "topnav", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Enables the Monaco editor for CloudWatch Logs queries", - "stage": "GA", - "codeowner": "@grafana/aws-datasources", - "frontend": true, - "allowSelfServe": true + "description": "Enables topnav support in external plugins. The new Grafana navigation cannot be disabled.", + "stage": "deprecated", + "codeowner": "@grafana/grafana-frontend-platform" } }, { "metadata": { - "name": "alertmanagerRemoteSecondary", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "grpcServer", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Enable Grafana to sync configuration and state with a remote Alertmanager.", - "stage": "experimental", - "codeowner": "@grafana/alerting-squad" + "description": "Run the GRPC server", + "stage": "preview", + "codeowner": "@grafana/grafana-app-platform-squad", + "hideFromAdminPage": true } }, { "metadata": { - "name": "disableAngular", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "influxdbRunQueriesInParallel", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Dynamic flag to disable angular at runtime. The preferred method is to set `angular_support_enabled` to `false` in the [security] settings, which allows you to change the state at runtime.", - "stage": "preview", - "codeowner": "@grafana/dataviz-squad", - "frontend": true, - "hideFromAdminPage": true + "description": "Enables running InfluxDB Influxql queries in parallel", + "stage": "privatePreview", + "codeowner": "@grafana/observability-metrics" } }, { "metadata": { - "name": "grafanaAPIServerEnsureKubectlAccess", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "lokiPredefinedOperations", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Start an additional https handler and write kubectl options", + "description": "Adds predefined query operations to Loki query editor", "stage": "experimental", - "codeowner": "@grafana/grafana-app-platform-squad", - "requiresDevMode": true, - "requiresRestart": true + "codeowner": "@grafana/observability-logs", + "frontend": true } }, { "metadata": { - "name": "alertingNoDataErrorExecution", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "kubernetesQueryServiceRewrite", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Changes how Alerting state manager handles execution of NoData/Error", - "stage": "GA", - "codeowner": "@grafana/alerting-squad", + "description": "Rewrite requests targeting /ds/query to the query service", + "stage": "experimental", + "codeowner": "@grafana/grafana-app-platform-squad", + "requiresDevMode": true, "requiresRestart": true } }, { "metadata": { - "name": "angularDeprecationUI", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "alertmanagerRemoteOnly", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Display Angular warnings in dashboards and panels", - "stage": "GA", - "codeowner": "@grafana/plugins-platform-backend", - "frontend": true + "description": "Disable the internal Alertmanager and only use the external one defined.", + "stage": "experimental", + "codeowner": "@grafana/alerting-squad" } }, { "metadata": { - "name": "lokiFormatQuery", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "cloudWatchCrossAccountQuerying", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Enables the ability to format Loki queries", - "stage": "experimental", - "codeowner": "@grafana/observability-logs", - "frontend": true + "description": "Enables cross-account querying in CloudWatch datasources", + "stage": "GA", + "codeowner": "@grafana/aws-datasources", + "allowSelfServe": true } }, { "metadata": { - "name": "nestedFolderPicker", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "prometheusMetricEncyclopedia", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Enables the new folder picker to work with nested folders. Requires the nestedFolders feature toggle", + "description": "Adds the metrics explorer component to the Prometheus query builder as an option in metric select", "stage": "GA", - "codeowner": "@grafana/grafana-frontend-platform", + "codeowner": "@grafana/observability-metrics", "frontend": true, "allowSelfServe": true } }, { "metadata": { - "name": "individualCookiePreferences", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "addFieldFromCalculationStatFunctions", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Support overriding cookie preferences per user", - "stage": "experimental", - "codeowner": "@grafana/backend-platform" + "description": "Add cumulative and window functions to the add field from calculation transformation", + "stage": "preview", + "codeowner": "@grafana/dataviz-squad", + "frontend": true } }, { "metadata": { - "name": "externalServiceAccounts", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "annotationPermissionUpdate", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Automatic service account and token setup for plugins", - "stage": "preview", - "codeowner": "@grafana/identity-access-team", - "hideFromAdminPage": true + "description": "Separate annotation permissions from dashboard permissions to allow for more granular control.", + "stage": "experimental", + "codeowner": "@grafana/identity-access-team" } }, { "metadata": { - "name": "groupByVariable", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "scenes", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Enable groupBy variable support in scenes dashboards", + "description": "Experimental framework to build interactive dashboards", "stage": "experimental", "codeowner": "@grafana/dashboards-squad", - "hideFromAdminPage": true, - "hideFromDocs": true + "frontend": true } }, { "metadata": { - "name": "autoMigrateGraphPanel", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "influxqlStreamingParser", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Migrate old graph panel to supported time series panel - broken out from autoMigrateOldPanels to enable granular tracking", - "stage": "preview", - "codeowner": "@grafana/dataviz-squad", - "frontend": true + "description": "Enable streaming JSON parser for InfluxDB datasource InfluxQL query language", + "stage": "experimental", + "codeowner": "@grafana/observability-metrics" } }, { "metadata": { - "name": "dashgpt", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "unifiedRequestLog", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Enable AI powered features in dashboards", + "description": "Writes error logs to the request logger", + "stage": "experimental", + "codeowner": "@grafana/backend-platform" + } + }, + { + "metadata": { + "name": "faroDatasourceSelector", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" + }, + "spec": { + "description": "Enable the data source selector within the Frontend Apps section of the Frontend Observability", "stage": "preview", - "codeowner": "@grafana/dashboards-squad", + "codeowner": "@grafana/app-o11y", "frontend": true } }, { "metadata": { - "name": "kubernetesSnapshots", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "mlExpressions", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Routes snapshot requests from /api to the /apis endpoint", + "description": "Enable support for Machine Learning in server-side expressions", "stage": "experimental", - "codeowner": "@grafana/grafana-app-platform-squad", - "requiresRestart": true + "codeowner": "@grafana/alerting-squad" } }, { "metadata": { - "name": "influxdbBackendMigration", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "prometheusConfigOverhaulAuth", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Query InfluxDB InfluxQL without the proxy", + "description": "Update the Prometheus configuration page with the new auth component", "stage": "GA", - "codeowner": "@grafana/observability-metrics", - "frontend": true + "codeowner": "@grafana/observability-metrics" } }, { "metadata": { - "name": "redshiftAsyncQueryDataSupport", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "panelMonitoring", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Enable async query data support for Redshift", + "description": "Enables panel monitoring through logs and measurements", "stage": "GA", - "codeowner": "@grafana/aws-datasources" + "codeowner": "@grafana/dataviz-squad", + "frontend": true } }, { "metadata": { - "name": "topnav", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "cachingOptimizeSerializationMemoryUsage", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Enables topnav support in external plugins. The new Grafana navigation cannot be disabled.", - "stage": "deprecated", - "codeowner": "@grafana/grafana-frontend-platform" + "description": "If enabled, the caching backend gradually serializes query responses for the cache, comparing against the configured `[caching]max_value_mb` value as it goes. This can can help prevent Grafana from running out of memory while attempting to cache very large query responses.", + "stage": "experimental", + "codeowner": "@grafana/grafana-operator-experience-squad" } }, { "metadata": { - "name": "lokiQuerySplittingConfig", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "kubernetesAggregator", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Give users the option to configure split durations for Loki queries", + "description": "Enable grafana aggregator", "stage": "experimental", - "codeowner": "@grafana/observability-logs", - "frontend": true + "codeowner": "@grafana/grafana-app-platform-squad", + "requiresRestart": true } }, { "metadata": { - "name": "awsDatasourcesTempCredentials", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "refactorVariablesTimeRange", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Support temporary security credentials in AWS plugins for Grafana Cloud customers", - "stage": "experimental", - "codeowner": "@grafana/aws-datasources" + "description": "Refactor time range variables flow to reduce number of API calls made when query variables are chained", + "stage": "preview", + "codeowner": "@grafana/dashboards-squad", + "hideFromAdminPage": true } }, { "metadata": { - "name": "influxdbSqlSupport", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "recordedQueriesMulti", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Enable InfluxDB SQL query language support with new querying UI", + "description": "Enables writing multiple items from a single query within Recorded Queries", "stage": "GA", - "codeowner": "@grafana/observability-metrics", - "requiresRestart": true, - "allowSelfServe": true + "codeowner": "@grafana/observability-metrics" } }, { "metadata": { - "name": "autoMigrateTablePanel", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "logsExploreTableVisualisation", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Migrate old table panel to supported table panel - broken out from autoMigrateOldPanels to enable granular tracking", - "stage": "preview", - "codeowner": "@grafana/dataviz-squad", + "description": "A table visualisation for logs in Explore", + "stage": "GA", + "codeowner": "@grafana/observability-logs", "frontend": true } }, { "metadata": { - "name": "migrationLocking", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "dashgpt", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Lock database during migrations", + "description": "Enable AI powered features in dashboards", "stage": "preview", - "codeowner": "@grafana/backend-platform" + "codeowner": "@grafana/dashboards-squad", + "frontend": true } }, { "metadata": { - "name": "accessControlOnCall", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "wargamesTesting", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Access control primitives for OnCall", - "stage": "preview", - "codeowner": "@grafana/identity-access-team", - "hideFromAdminPage": true + "description": "Placeholder feature flag for internal testing", + "stage": "experimental", + "codeowner": "@grafana/hosted-grafana-team" } }, { "metadata": { - "name": "faroDatasourceSelector", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "enablePluginsTracingByDefault", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Enable the data source selector within the Frontend Apps section of the Frontend Observability", - "stage": "preview", - "codeowner": "@grafana/app-o11y", - "frontend": true + "description": "Enable plugin tracing for all external plugins", + "stage": "experimental", + "codeowner": "@grafana/plugins-platform-backend", + "requiresRestart": true } }, { "metadata": { - "name": "featureToggleAdminPage", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "mysqlAnsiQuotes", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Enable admin page for managing feature toggles from the Grafana front-end", + "description": "Use double quotes to escape keyword in a MySQL query", "stage": "experimental", - "codeowner": "@grafana/grafana-operator-experience-squad", - "requiresRestart": true + "codeowner": "@grafana/backend-platform" } }, { "metadata": { - "name": "awsAsyncQueryCaching", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "lokiStructuredMetadata", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Enable caching for async queries for Redshift and Athena. Requires that the datasource has caching and async query support enabled", + "description": "Enables the loki data source to request structured metadata from the Loki server", "stage": "GA", - "codeowner": "@grafana/aws-datasources" + "codeowner": "@grafana/observability-logs" } }, { "metadata": { - "name": "reportingRetries", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "alertingDetailsViewV2", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Enables rendering retries for the reporting feature", + "description": "Enables the preview of the new alert details view", + "stage": "experimental", + "codeowner": "@grafana/alerting-squad", + "frontend": true, + "hideFromDocs": true + } + }, + { + "metadata": { + "name": "regressionTransformation", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" + }, + "spec": { + "description": "Enables regression analysis transformation", "stage": "preview", - "codeowner": "@grafana/sharing-squad", - "requiresRestart": true + "codeowner": "@grafana/dataviz-squad", + "frontend": true } }, { "metadata": { - "name": "kubernetesAggregator", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "expressionParser", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Enable grafana aggregator", + "description": "Enable new expression parser", "stage": "experimental", "codeowner": "@grafana/grafana-app-platform-squad", "requiresRestart": true @@ -1029,759 +1034,722 @@ }, { "metadata": { - "name": "publicDashboards", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "disableEnvelopeEncryption", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "[Deprecated] Public dashboards are now enabled by default; to disable them, use the configuration setting. This feature toggle will be removed in the next major version.", + "description": "Disable envelope encryption (emergency only)", "stage": "GA", - "codeowner": "@grafana/sharing-squad", - "allowSelfServe": true + "codeowner": "@grafana/grafana-as-code", + "hideFromAdminPage": true } }, { "metadata": { - "name": "transformationsRedesign", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "athenaAsyncQueryDataSupport", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Enables the transformations redesign", + "description": "Enable async query data support for Athena", "stage": "GA", - "codeowner": "@grafana/observability-metrics", - "frontend": true, - "allowSelfServe": true + "codeowner": "@grafana/aws-datasources", + "frontend": true } }, { "metadata": { - "name": "traceQLStreaming", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "logRowsPopoverMenu", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Enables response streaming of TraceQL queries of the Tempo data source", - "stage": "experimental", - "codeowner": "@grafana/observability-traces-and-profiling", + "description": "Enable filtering menu displayed when text of a log line is selected", + "stage": "GA", + "codeowner": "@grafana/observability-logs", "frontend": true } }, { "metadata": { - "name": "formatString", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "nestedFolders", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Enable format string transformer", + "description": "Enable folder nesting", "stage": "preview", - "codeowner": "@grafana/dataviz-squad", - "frontend": true + "codeowner": "@grafana/backend-platform" } }, { "metadata": { - "name": "alertingQueryOptimization", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "ssoSettingsApi", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Optimizes eligible queries in order to reduce load on datasources", - "stage": "GA", - "codeowner": "@grafana/alerting-squad" + "description": "Enables the SSO settings API and the OAuth configuration UIs in Grafana", + "stage": "preview", + "codeowner": "@grafana/identity-access-team", + "allowSelfServe": true } }, { "metadata": { - "name": "nestedFolders", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "alertingSaveStatePeriodic", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Enable folder nesting", - "stage": "preview", - "codeowner": "@grafana/backend-platform" + "description": "Writes the state periodically to the database, asynchronous to rule evaluation", + "stage": "privatePreview", + "codeowner": "@grafana/alerting-squad" } }, { "metadata": { - "name": "scenes", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "unifiedStorage", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Experimental framework to build interactive dashboards", + "description": "SQL-based k8s storage", "stage": "experimental", - "codeowner": "@grafana/dashboards-squad", - "frontend": true + "codeowner": "@grafana/grafana-app-platform-squad", + "requiresDevMode": true, + "requiresRestart": true } }, { "metadata": { - "name": "extraThemes", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "frontendSandboxMonitorOnly", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Enables extra themes", + "description": "Enables monitor only in the plugin frontend sandbox (if enabled)", "stage": "experimental", - "codeowner": "@grafana/grafana-frontend-platform", + "codeowner": "@grafana/plugins-platform-backend", "frontend": true } }, { "metadata": { - "name": "annotationPermissionUpdate", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "lokiRunQueriesInParallel", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Separate annotation permissions from dashboard permissions to allow for more granular control.", - "stage": "experimental", - "codeowner": "@grafana/identity-access-team" + "description": "Enables running Loki queries in parallel", + "stage": "privatePreview", + "codeowner": "@grafana/observability-logs" } }, { "metadata": { - "name": "extractFieldsNameDeduplication", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "queryOverLive", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Make sure extracted field names are unique in the dataframe", + "description": "Use Grafana Live WebSocket to execute backend queries", "stage": "experimental", - "codeowner": "@grafana/dataviz-squad", + "codeowner": "@grafana/grafana-app-platform-squad", "frontend": true } }, { "metadata": { - "name": "flameGraphItemCollapsing", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "logRequestsInstrumentedAsUnknown", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Allow collapsing of flame graph items", + "description": "Logs the path for requests that are instrumented as unknown", "stage": "experimental", - "codeowner": "@grafana/observability-traces-and-profiling", - "frontend": true + "codeowner": "@grafana/hosted-grafana-team" } }, { "metadata": { - "name": "alertingPreviewUpgrade", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "alertStateHistoryLokiOnly", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Show Unified Alerting preview and upgrade page in legacy alerting", - "stage": "GA", - "codeowner": "@grafana/alerting-squad", - "requiresRestart": true + "description": "Disable Grafana alerts from emitting annotations when a remote Loki instance is available.", + "stage": "experimental", + "codeowner": "@grafana/alerting-squad" } }, { "metadata": { - "name": "disableEnvelopeEncryption", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "prometheusIncrementalQueryInstrumentation", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Disable envelope encryption (emergency only)", - "stage": "GA", - "codeowner": "@grafana/grafana-as-code", - "hideFromAdminPage": true + "description": "Adds RudderStack events to incremental queries", + "stage": "experimental", + "codeowner": "@grafana/observability-metrics", + "frontend": true } }, { "metadata": { - "name": "groupToNestedTableTransformation", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "angularDeprecationUI", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Enables the group to nested table transformation", - "stage": "preview", - "codeowner": "@grafana/dataviz-squad", + "description": "Display Angular warnings in dashboards and panels", + "stage": "GA", + "codeowner": "@grafana/plugins-platform-backend", "frontend": true } }, { "metadata": { - "name": "promQLScope", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "aiGeneratedDashboardChanges", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "In-development feature that will allow injection of labels into prometheus queries.", + "description": "Enable AI powered features for dashboards to auto-summary changes when saving", "stage": "experimental", - "codeowner": "@grafana/observability-metrics" - } - }, - { - "metadata": { - "name": "logsContextDatasourceUi", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" - }, - "spec": { - "description": "Allow datasource to provide custom UI for context view", - "stage": "GA", - "codeowner": "@grafana/observability-logs", - "frontend": true, - "allowSelfServe": true + "codeowner": "@grafana/dashboards-squad", + "frontend": true } }, { "metadata": { - "name": "alertStateHistoryLokiOnly", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "kubernetesPlaylists", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Disable Grafana alerts from emitting annotations when a remote Loki instance is available.", + "description": "Use the kubernetes API in the frontend for playlists, and route /api/playlist requests to k8s", "stage": "experimental", - "codeowner": "@grafana/alerting-squad" + "codeowner": "@grafana/grafana-app-platform-squad", + "requiresRestart": true } }, { "metadata": { - "name": "transformationsVariableSupport", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "pdfTables", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Allows using variables in transformations", + "description": "Enables generating table data as PDF in reporting", "stage": "preview", - "codeowner": "@grafana/dataviz-squad", - "frontend": true + "codeowner": "@grafana/sharing-squad" } }, { "metadata": { - "name": "dashboardSceneSolo", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "tableSharedCrosshair", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Enables rendering dashboards using scenes for solo panels", + "description": "Enables shared crosshair in table panel", "stage": "experimental", - "codeowner": "@grafana/dashboards-squad", + "codeowner": "@grafana/dataviz-squad", "frontend": true } }, { "metadata": { - "name": "athenaAsyncQueryDataSupport", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "featureHighlights", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Enable async query data support for Athena", + "description": "Highlight Grafana Enterprise features", "stage": "GA", - "codeowner": "@grafana/aws-datasources", - "frontend": true + "codeowner": "@grafana/grafana-as-code", + "allowSelfServe": true } }, { "metadata": { - "name": "lokiLogsDataplane", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "alertStateHistoryLokiSecondary", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Changes logs responses from Loki to be compliant with the dataplane specification.", + "description": "Enable Grafana to write alert state history to an external Loki instance in addition to Grafana annotations.", "stage": "experimental", - "codeowner": "@grafana/observability-logs" + "codeowner": "@grafana/alerting-squad" } }, { "metadata": { - "name": "enableDatagridEditing", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "transformationsRedesign", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Enables the edit functionality in the datagrid panel", - "stage": "preview", - "codeowner": "@grafana/dataviz-squad", - "frontend": true + "description": "Enables the transformations redesign", + "stage": "GA", + "codeowner": "@grafana/observability-metrics", + "frontend": true, + "allowSelfServe": true } }, { "metadata": { - "name": "exploreScrollableLogsContainer", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "kubernetesSnapshots", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Improves the scrolling behavior of logs in Explore", + "description": "Routes snapshot requests from /api to the /apis endpoint", "stage": "experimental", - "codeowner": "@grafana/observability-logs", - "frontend": true + "codeowner": "@grafana/grafana-app-platform-squad", + "requiresRestart": true } }, { "metadata": { - "name": "lokiRunQueriesInParallel", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "pluginsSkipHostEnvVars", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Enables running Loki queries in parallel", - "stage": "privatePreview", - "codeowner": "@grafana/observability-logs" + "description": "Disables passing host environment variable to plugin processes", + "stage": "experimental", + "codeowner": "@grafana/plugins-platform-backend" } }, { "metadata": { - "name": "panelTitleSearchInV1", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "promQLScope", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Enable searching for dashboards using panel title in search v1", + "description": "In-development feature that will allow injection of labels into prometheus queries.", "stage": "experimental", - "codeowner": "@grafana/backend-platform", - "requiresDevMode": true + "codeowner": "@grafana/observability-metrics" } }, { "metadata": { - "name": "logRowsPopoverMenu", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "autoMigrateStatPanel", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Enable filtering menu displayed when text of a log line is selected", - "stage": "GA", - "codeowner": "@grafana/observability-logs", + "description": "Migrate old stat panel to supported stat panel - broken out from autoMigrateOldPanels to enable granular tracking", + "stage": "preview", + "codeowner": "@grafana/dataviz-squad", "frontend": true } }, { "metadata": { - "name": "lokiQueryHints", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "newVizTooltips", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Enables query hints for Loki", - "stage": "GA", - "codeowner": "@grafana/observability-logs", + "description": "New visualizations tooltips UX", + "stage": "preview", + "codeowner": "@grafana/dataviz-squad", "frontend": true } }, { "metadata": { - "name": "lokiExperimentalStreaming", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "editPanelCSVDragAndDrop", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Support new streaming approach for loki (prototype, needs special loki build)", + "description": "Enables drag and drop for CSV and Excel files", "stage": "experimental", - "codeowner": "@grafana/observability-logs" + "codeowner": "@grafana/dataviz-squad", + "frontend": true } }, { "metadata": { - "name": "traceToMetrics", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z", - "deletionTimestamp": "2024-03-05T11:44:12Z" + "name": "awsAsyncQueryCaching", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Enable trace to metrics links", - "stage": "experimental", - "codeowner": "@grafana/observability-traces-and-profiling", - "frontend": true + "description": "Enable caching for async queries for Redshift and Athena. Requires that the datasource has caching and async query support enabled", + "stage": "GA", + "codeowner": "@grafana/aws-datasources" } }, { "metadata": { - "name": "grpcServer", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "canvasPanelPanZoom", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Run the GRPC server", + "description": "Allow pan and zoom in canvas panel", "stage": "preview", - "codeowner": "@grafana/grafana-app-platform-squad", - "hideFromAdminPage": true + "codeowner": "@grafana/dataviz-squad", + "frontend": true } }, { "metadata": { - "name": "cloudWatchCrossAccountQuerying", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "migrationLocking", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Enables cross-account querying in CloudWatch datasources", - "stage": "GA", - "codeowner": "@grafana/aws-datasources", - "allowSelfServe": true + "description": "Lock database during migrations", + "stage": "preview", + "codeowner": "@grafana/backend-platform" } }, { "metadata": { - "name": "vizAndWidgetSplit", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "exploreScrollableLogsContainer", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Split panels between visualizations and widgets", + "description": "Improves the scrolling behavior of logs in Explore", "stage": "experimental", - "codeowner": "@grafana/dashboards-squad", + "codeowner": "@grafana/observability-logs", "frontend": true } }, { "metadata": { - "name": "panelMonitoring", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "libraryPanelRBAC", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Enables panel monitoring through logs and measurements", - "stage": "GA", - "codeowner": "@grafana/dataviz-squad", - "frontend": true + "description": "Enables RBAC support for library panels", + "stage": "experimental", + "codeowner": "@grafana/dashboards-squad", + "requiresRestart": true } }, { "metadata": { - "name": "live-service-web-worker", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "newFolderPicker", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "This will use a webworker thread to processes events rather than the main thread", + "description": "Enables the nested folder picker without having nested folders enabled", "stage": "experimental", - "codeowner": "@grafana/grafana-app-platform-squad", + "codeowner": "@grafana/grafana-frontend-platform", "frontend": true } }, { "metadata": { - "name": "returnToPrevious", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "publicDashboardsEmailSharing", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Enables the return to previous context functionality", + "description": "Enables public dashboard sharing to be restricted to only allowed emails", "stage": "preview", - "codeowner": "@grafana/grafana-frontend-platform", - "frontend": true + "codeowner": "@grafana/sharing-squad", + "hideFromAdminPage": true, + "hideFromDocs": true } }, { "metadata": { - "name": "cachingOptimizeSerializationMemoryUsage", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "lokiExperimentalStreaming", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "If enabled, the caching backend gradually serializes query responses for the cache, comparing against the configured `[caching]max_value_mb` value as it goes. This can can help prevent Grafana from running out of memory while attempting to cache very large query responses.", + "description": "Support new streaming approach for loki (prototype, needs special loki build)", "stage": "experimental", - "codeowner": "@grafana/grafana-operator-experience-squad" + "codeowner": "@grafana/observability-logs" } }, { "metadata": { - "name": "onPremToCloudMigrations", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "alertingInsights", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "In-development feature that will allow users to easily migrate their on-prem Grafana instances to Grafana Cloud.", - "stage": "experimental", - "codeowner": "@grafana/grafana-operator-experience-squad" + "description": "Show the new alerting insights landing page", + "stage": "GA", + "codeowner": "@grafana/alerting-squad", + "frontend": true, + "hideFromAdminPage": true } }, { "metadata": { - "name": "featureHighlights", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "cloudWatchWildCardDimensionValues", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Highlight Grafana Enterprise features", + "description": "Fetches dimension values from CloudWatch to correctly label wildcard dimensions", "stage": "GA", - "codeowner": "@grafana/grafana-as-code", + "codeowner": "@grafana/aws-datasources", "allowSelfServe": true } }, { "metadata": { - "name": "splitScopes", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z", - "deletionTimestamp": "2024-03-05T11:44:12Z" - }, - "spec": { - "description": "Support faster dashboard and folder search by splitting permission scopes into parts", - "stage": "deprecated", - "codeowner": "@grafana/identity-access-team", - "requiresRestart": true, - "hideFromAdminPage": true - } - }, - { - "metadata": { - "name": "nodeGraphDotLayout", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "dashboardScene", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Changed the layout algorithm for the node graph", + "description": "Enables dashboard rendering using scenes for all roles", "stage": "experimental", - "codeowner": "@grafana/observability-traces-and-profiling", + "codeowner": "@grafana/dashboards-squad", "frontend": true } }, { "metadata": { - "name": "refactorVariablesTimeRange", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "alertingSimplifiedRouting", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Refactor time range variables flow to reduce number of API calls made when query variables are chained", + "description": "Enables users to easily configure alert notifications by specifying a contact point directly when editing or creating an alert rule", "stage": "preview", - "codeowner": "@grafana/dashboards-squad", - "hideFromAdminPage": true + "codeowner": "@grafana/alerting-squad" } }, { "metadata": { - "name": "externalServiceAuth", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z", - "deletionTimestamp": "2024-03-05T11:44:12Z" + "name": "returnToPrevious", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Starts an OAuth2 authentication provider for external services", - "stage": "experimental", - "codeowner": "@grafana/identity-access-team", - "requiresDevMode": true + "description": "Enables the return to previous context functionality", + "stage": "preview", + "codeowner": "@grafana/grafana-frontend-platform", + "frontend": true } }, { "metadata": { - "name": "frontendSandboxMonitorOnly", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "cloudWatchLogsMonacoEditor", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Enables monitor only in the plugin frontend sandbox (if enabled)", - "stage": "experimental", - "codeowner": "@grafana/plugins-platform-backend", - "frontend": true + "description": "Enables the Monaco editor for CloudWatch Logs queries", + "stage": "GA", + "codeowner": "@grafana/aws-datasources", + "frontend": true, + "allowSelfServe": true } }, { "metadata": { - "name": "mlExpressions", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "cloudWatchBatchQueries", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Enable support for Machine Learning in server-side expressions", - "stage": "experimental", - "codeowner": "@grafana/alerting-squad" + "description": "Runs CloudWatch metrics queries as separate batches", + "stage": "preview", + "codeowner": "@grafana/aws-datasources" } }, { "metadata": { - "name": "libraryPanelRBAC", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "lokiQueryHints", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Enables RBAC support for library panels", - "stage": "experimental", - "codeowner": "@grafana/dashboards-squad", - "requiresRestart": true + "description": "Enables query hints for Loki", + "stage": "GA", + "codeowner": "@grafana/observability-logs", + "frontend": true } }, { "metadata": { - "name": "kubernetesFeatureToggles", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "newPDFRendering", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Use the kubernetes API for feature toggle management in the frontend", + "description": "New implementation for the dashboard to PDF rendering", "stage": "experimental", - "codeowner": "@grafana/grafana-operator-experience-squad", - "frontend": true, - "hideFromAdminPage": true + "codeowner": "@grafana/sharing-squad" } }, { "metadata": { - "name": "expressionParser", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "logsContextDatasourceUi", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Enable new expression parser", - "stage": "experimental", - "codeowner": "@grafana/grafana-app-platform-squad", - "requiresRestart": true + "description": "Allow datasource to provide custom UI for context view", + "stage": "GA", + "codeowner": "@grafana/observability-logs", + "frontend": true, + "allowSelfServe": true } }, { "metadata": { - "name": "autoMigrateStatPanel", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "dataplaneFrontendFallback", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Migrate old stat panel to supported stat panel - broken out from autoMigrateOldPanels to enable granular tracking", - "stage": "preview", - "codeowner": "@grafana/dataviz-squad", - "frontend": true + "description": "Support dataplane contract field name change for transformations and field name matchers where the name is different", + "stage": "GA", + "codeowner": "@grafana/observability-metrics", + "frontend": true, + "allowSelfServe": true } }, { "metadata": { - "name": "renderAuthJWT", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "grafanaAPIServerEnsureKubectlAccess", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Uses JWT-based auth for rendering instead of relying on remote cache", - "stage": "preview", - "codeowner": "@grafana/grafana-as-code", - "hideFromAdminPage": true + "description": "Start an additional https handler and write kubectl options", + "stage": "experimental", + "codeowner": "@grafana/grafana-app-platform-squad", + "requiresDevMode": true, + "requiresRestart": true } }, { "metadata": { - "name": "permissionsFilterRemoveSubquery", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "configurableSchedulerTick", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Alternative permission filter implementation that does not use subqueries for fetching the dashboard folder", + "description": "Enable changing the scheduler base interval via configuration option unified_alerting.scheduler_tick_interval", "stage": "experimental", - "codeowner": "@grafana/backend-platform" + "codeowner": "@grafana/alerting-squad", + "requiresRestart": true, + "hideFromDocs": true } }, { "metadata": { - "name": "pdfTables", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "enableNativeHTTPHistogram", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Enables generating table data as PDF in reporting", - "stage": "preview", - "codeowner": "@grafana/sharing-squad" + "description": "Enables native HTTP Histograms", + "stage": "experimental", + "codeowner": "@grafana/hosted-grafana-team" } }, { "metadata": { - "name": "influxqlStreamingParser", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "formatString", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Enable streaming JSON parser for InfluxDB datasource InfluxQL query language", - "stage": "experimental", - "codeowner": "@grafana/observability-metrics" + "description": "Enable format string transformer", + "stage": "preview", + "codeowner": "@grafana/dataviz-squad", + "frontend": true } }, { "metadata": { - "name": "showDashboardValidationWarnings", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "flameGraphItemCollapsing", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Show warnings when dashboards do not validate against the schema", + "description": "Allow collapsing of flame graph items", "stage": "experimental", - "codeowner": "@grafana/dashboards-squad" + "codeowner": "@grafana/observability-traces-and-profiling", + "frontend": true } }, { "metadata": { - "name": "lokiQuerySplitting", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "publicDashboards", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Split large interval queries into subqueries with smaller time intervals", + "description": "[Deprecated] Public dashboards are now enabled by default; to disable them, use the configuration setting. This feature toggle will be removed in the next major version.", "stage": "GA", - "codeowner": "@grafana/observability-logs", - "frontend": true, + "codeowner": "@grafana/sharing-squad", "allowSelfServe": true } }, { "metadata": { - "name": "lokiMetricDataplane", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "exploreContentOutline", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Changes metric responses from Loki to be compliant with the dataplane specification.", + "description": "Content outline sidebar", "stage": "GA", - "codeowner": "@grafana/observability-logs", + "codeowner": "@grafana/explore-squad", + "frontend": true, "allowSelfServe": true } }, { "metadata": { - "name": "dashboardEmbed", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "groupByVariable", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Allow embedding dashboard for external use in Code editors", + "description": "Enable groupBy variable support in scenes dashboards", "stage": "experimental", - "codeowner": "@grafana/grafana-as-code", - "frontend": true + "codeowner": "@grafana/dashboards-squad", + "hideFromAdminPage": true, + "hideFromDocs": true } }, { "metadata": { - "name": "pluginsAPIMetrics", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "extraThemes", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Sends metrics of public grafana packages usage by plugins", + "description": "Enables extra themes", "stage": "experimental", - "codeowner": "@grafana/plugins-platform-backend", - "frontend": true - } - }, - { - "metadata": { - "name": "awsDatasourcesNewFormStyling", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" - }, - "spec": { - "description": "Applies new form styling for configuration and query editors in AWS plugins", - "stage": "preview", - "codeowner": "@grafana/aws-datasources", + "codeowner": "@grafana/grafana-frontend-platform", "frontend": true } }, { "metadata": { - "name": "dashboardScene", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "dashboardSceneForViewers", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Enables dashboard rendering using scenes for all roles", + "description": "Enables dashboard rendering using Scenes for viewer roles", "stage": "experimental", "codeowner": "@grafana/dashboards-squad", "frontend": true @@ -1789,50 +1757,50 @@ }, { "metadata": { - "name": "canvasPanelNesting", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "onPremToCloudMigrations", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Allow elements nesting", + "description": "In-development feature that will allow users to easily migrate their on-prem Grafana instances to Grafana Cloud.", "stage": "experimental", - "codeowner": "@grafana/dataviz-squad", - "frontend": true, - "hideFromAdminPage": true + "codeowner": "@grafana/grafana-operator-experience-squad" } }, { "metadata": { - "name": "alertingSaveStatePeriodic", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "lokiQuerySplittingConfig", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Writes the state periodically to the database, asynchronous to rule evaluation", - "stage": "privatePreview", - "codeowner": "@grafana/alerting-squad" + "description": "Give users the option to configure split durations for Loki queries", + "stage": "experimental", + "codeowner": "@grafana/observability-logs", + "frontend": true } }, { "metadata": { - "name": "sseGroupByDatasource", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "lokiFormatQuery", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Send query to the same datasource in a single request when using server side expressions. The `cloudWatchBatchQueries` feature toggle should be enabled if this used with CloudWatch.", + "description": "Enables the ability to format Loki queries", "stage": "experimental", - "codeowner": "@grafana/observability-metrics" + "codeowner": "@grafana/observability-logs", + "frontend": true } }, { "metadata": { - "name": "kubernetesQueryServiceRewrite", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "grafanaAPIServerWithExperimentalAPIs", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Rewrite requests targeting /ds/query to the query service", + "description": "Register experimental APIs with the k8s API server", "stage": "experimental", "codeowner": "@grafana/grafana-app-platform-squad", "requiresDevMode": true, @@ -1841,164 +1809,153 @@ }, { "metadata": { - "name": "exploreContentOutline", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "influxdbSqlSupport", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Content outline sidebar", + "description": "Enable InfluxDB SQL query language support with new querying UI", "stage": "GA", - "codeowner": "@grafana/explore-squad", - "frontend": true, + "codeowner": "@grafana/observability-metrics", + "requiresRestart": true, "allowSelfServe": true } }, { "metadata": { - "name": "recordedQueriesMulti", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" - }, - "spec": { - "description": "Enables writing multiple items from a single query within Recorded Queries", - "stage": "GA", - "codeowner": "@grafana/observability-metrics" - } - }, - { - "metadata": { - "name": "alertingInsights", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "recoveryThreshold", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Show the new alerting insights landing page", + "description": "Enables feature recovery threshold (aka hysteresis) for threshold server-side expression", "stage": "GA", "codeowner": "@grafana/alerting-squad", - "frontend": true, - "hideFromAdminPage": true + "requiresRestart": true } }, { "metadata": { - "name": "idForwarding", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "panelFilterVariable", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Generate signed id token for identity that can be forwarded to plugins and external services", + "description": "Enables use of the `systemPanelFilterVar` variable to filter panels in a dashboard", "stage": "experimental", - "codeowner": "@grafana/identity-access-team" + "codeowner": "@grafana/dashboards-squad", + "frontend": true, + "hideFromDocs": true } }, { "metadata": { - "name": "enableNativeHTTPHistogram", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "lokiQuerySplitting", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Enables native HTTP Histograms", - "stage": "experimental", - "codeowner": "@grafana/hosted-grafana-team" + "description": "Split large interval queries into subqueries with smaller time intervals", + "stage": "GA", + "codeowner": "@grafana/observability-logs", + "frontend": true, + "allowSelfServe": true } }, { "metadata": { - "name": "alertmanagerRemoteOnly", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "lokiMetricDataplane", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Disable the internal Alertmanager and only use the external one defined.", - "stage": "experimental", - "codeowner": "@grafana/alerting-squad" + "description": "Changes metric responses from Loki to be compliant with the dataplane specification.", + "stage": "GA", + "codeowner": "@grafana/observability-logs", + "allowSelfServe": true } }, { "metadata": { - "name": "influxdbRunQueriesInParallel", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "idForwarding", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Enables running InfluxDB Influxql queries in parallel", - "stage": "privatePreview", - "codeowner": "@grafana/observability-metrics" + "description": "Generate signed id token for identity that can be forwarded to plugins and external services", + "stage": "experimental", + "codeowner": "@grafana/identity-access-team" } }, { "metadata": { - "name": "newVizTooltips", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "teamHttpHeaders", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "New visualizations tooltips UX", + "description": "Enables Team LBAC for datasources to apply team headers to the client requests", "stage": "preview", - "codeowner": "@grafana/dataviz-squad", - "frontend": true + "codeowner": "@grafana/identity-access-team" } }, { "metadata": { - "name": "prometheusMetricEncyclopedia", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "managedPluginsInstall", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Adds the metrics explorer component to the Prometheus query builder as an option in metric select", - "stage": "GA", - "codeowner": "@grafana/observability-metrics", - "frontend": true, - "allowSelfServe": true + "description": "Install managed plugins directly from plugins catalog", + "stage": "preview", + "codeowner": "@grafana/plugins-platform-backend" } }, { "metadata": { - "name": "pluginsDynamicAngularDetectionPatterns", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "alertmanagerRemotePrimary", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Enables fetching Angular detection patterns for plugins from GCOM and fallback to hardcoded ones", + "description": "Enable Grafana to have a remote Alertmanager instance as the primary Alertmanager.", "stage": "experimental", - "codeowner": "@grafana/plugins-platform-backend" + "codeowner": "@grafana/alerting-squad" } }, { "metadata": { - "name": "cloudWatchBatchQueries", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "alertingQueryOptimization", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Runs CloudWatch metrics queries as separate batches", - "stage": "preview", - "codeowner": "@grafana/aws-datasources" + "description": "Optimizes eligible queries in order to reduce load on datasources", + "stage": "GA", + "codeowner": "@grafana/alerting-squad" } }, { "metadata": { - "name": "alertingUpgradeDryrunOnStart", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "correlations", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "When activated in legacy alerting mode, this initiates a dry-run of the Unified Alerting upgrade during each startup. It logs any issues detected without implementing any actual changes.", + "description": "Correlations page", "stage": "GA", - "codeowner": "@grafana/alerting-squad", - "requiresRestart": true + "codeowner": "@grafana/explore-squad", + "allowSelfServe": true } }, { "metadata": { - "name": "autoMigrateOldPanels", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "autoMigrateGraphPanel", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Migrate old angular panels to supported versions (graph, table-old, worldmap, etc)", + "description": "Migrate old graph panel to supported time series panel - broken out from autoMigrateOldPanels to enable granular tracking", "stage": "preview", "codeowner": "@grafana/dataviz-squad", "frontend": true @@ -2006,100 +1963,96 @@ }, { "metadata": { - "name": "externalCorePlugins", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "autoMigrateTablePanel", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Allow core plugins to be loaded as external", - "stage": "experimental", - "codeowner": "@grafana/plugins-platform-backend" + "description": "Migrate old table panel to supported table panel - broken out from autoMigrateOldPanels to enable granular tracking", + "stage": "preview", + "codeowner": "@grafana/dataviz-squad", + "frontend": true } }, { "metadata": { - "name": "alertingNoNormalState", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "individualCookiePreferences", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Stop maintaining state of alerts that are not firing", - "stage": "preview", - "codeowner": "@grafana/alerting-squad", - "hideFromAdminPage": true + "description": "Support overriding cookie preferences per user", + "stage": "experimental", + "codeowner": "@grafana/backend-platform" } }, { "metadata": { - "name": "dataplaneFrontendFallback", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "disableSSEDataplane", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Support dataplane contract field name change for transformations and field name matchers where the name is different", - "stage": "GA", - "codeowner": "@grafana/observability-metrics", - "frontend": true, - "allowSelfServe": true + "description": "Disables dataplane specific processing in server side expressions.", + "stage": "experimental", + "codeowner": "@grafana/observability-metrics" } }, { "metadata": { - "name": "disableSSEDataplane", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "externalCorePlugins", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Disables dataplane specific processing in server side expressions.", + "description": "Allow core plugins to be loaded as external", "stage": "experimental", - "codeowner": "@grafana/observability-metrics" + "codeowner": "@grafana/plugins-platform-backend" } }, { "metadata": { - "name": "lokiPredefinedOperations", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "pluginsAPIMetrics", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Adds predefined query operations to Loki query editor", + "description": "Sends metrics of public grafana packages usage by plugins", "stage": "experimental", - "codeowner": "@grafana/observability-logs", + "codeowner": "@grafana/plugins-platform-backend", "frontend": true } }, { "metadata": { - "name": "configurableSchedulerTick", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "dashboardSceneSolo", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Enable changing the scheduler base interval via configuration option unified_alerting.scheduler_tick_interval", + "description": "Enables rendering dashboards using scenes for solo panels", "stage": "experimental", - "codeowner": "@grafana/alerting-squad", - "requiresRestart": true, - "hideFromDocs": true + "codeowner": "@grafana/dashboards-squad", + "frontend": true } }, { "metadata": { - "name": "kubernetesPlaylists", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "datasourceQueryMultiStatus", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Use the kubernetes API in the frontend for playlists, and route /api/playlist requests to k8s", + "description": "Introduce HTTP 207 Multi Status for api/ds/query", "stage": "experimental", - "codeowner": "@grafana/grafana-app-platform-squad", - "requiresRestart": true + "codeowner": "@grafana/plugins-platform-backend" } }, { "metadata": { "name": "datatrails", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { "description": "Enables the new core app datatrails", @@ -2111,44 +2064,29 @@ }, { "metadata": { - "name": "editPanelCSVDragAndDrop", - "resourceVersion": "1708108588074", - "creationTimestamp": "2024-02-16T18:36:28Z" + "name": "kubernetesFeatureToggles", + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { - "description": "Enables drag and drop for CSV and Excel files", + "description": "Use the kubernetes API for feature toggle management in the frontend", "stage": "experimental", - "codeowner": "@grafana/dataviz-squad", - "frontend": true + "codeowner": "@grafana/grafana-operator-experience-squad", + "frontend": true, + "hideFromAdminPage": true } }, { "metadata": { "name": "sqlExpressions", - "resourceVersion": "1709044973784", - "creationTimestamp": "2024-02-19T22:46:11Z", - "annotations": { - "grafana.app/updatedTimestamp": "2024-02-27 14:42:53.784398 +0000 UTC" - } + "resourceVersion": "1709648236447", + "creationTimestamp": "2024-03-05T14:17:16Z" }, "spec": { "description": "Enables using SQL and DuckDB functions as Expressions.", "stage": "experimental", "codeowner": "@grafana/grafana-app-platform-squad" } - }, - { - "metadata": { - "name": "aiGeneratedDashboardChanges", - "resourceVersion": "1709639042582", - "creationTimestamp": "2024-03-05T11:44:02Z" - }, - "spec": { - "description": "Enable AI powered features for dashboards to auto-summary changes when saving", - "stage": "experimental", - "codeowner": "@grafana/dashboards-squad", - "frontend": true - } } ] } \ No newline at end of file diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index efa62cfae5..d6a3b52bec 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -521,6 +521,10 @@ type Cfg struct { // News Feed NewsFeedEnabled bool + + // Experimental scope settings + ScopesListScopesURL string + ScopesListDashboardsURL string } // AddChangePasswordLink returns if login form is disabled or not since @@ -1281,6 +1285,11 @@ func (cfg *Cfg) parseINIFile(iniFile *ini.File) error { cfg.readFeatureManagementConfig() cfg.readPublicDashboardsSettings() + // read experimental scopes settings. + scopesSection := iniFile.Section("scopes") + cfg.ScopesListScopesURL = scopesSection.Key("list_scopes_endpoint").MustString("") + cfg.ScopesListDashboardsURL = scopesSection.Key("list_dashboards_endpoint").MustString("") + return nil } From 9fa9eaab44af088c38df6622274c3f953161778b Mon Sep 17 00:00:00 2001 From: Dan Cech Date: Tue, 5 Mar 2024 10:57:32 -0500 Subject: [PATCH 0401/1406] Storage: Support get with resourceversion (#83849) support getting old resourceversion, return explicit resource version in list --- pkg/services/apiserver/service.go | 1 + .../apiserver/storage/entity/storage.go | 24 +- pkg/services/store/entity/entity.pb.go | 234 +++++++++--------- pkg/services/store/entity/entity.proto | 3 + .../entity/sqlstash/sql_storage_server.go | 11 +- 5 files changed, 150 insertions(+), 123 deletions(-) diff --git a/pkg/services/apiserver/service.go b/pkg/services/apiserver/service.go index 00ec26b861..405359159c 100644 --- a/pkg/services/apiserver/service.go +++ b/pkg/services/apiserver/service.go @@ -161,6 +161,7 @@ func ProvideService( s.rr.Group("/readyz", proxyHandler) s.rr.Group("/healthz", proxyHandler) s.rr.Group("/openapi", proxyHandler) + s.rr.Group("/version", proxyHandler) return s, nil } diff --git a/pkg/services/apiserver/storage/entity/storage.go b/pkg/services/apiserver/storage/entity/storage.go index 2fa78fc3dd..3fb5cab77f 100644 --- a/pkg/services/apiserver/storage/entity/storage.go +++ b/pkg/services/apiserver/storage/entity/storage.go @@ -278,10 +278,20 @@ func (s *Storage) Watch(ctx context.Context, key string, opts storage.ListOption // The returned contents may be delayed, but it is guaranteed that they will // match 'opts.ResourceVersion' according 'opts.ResourceVersionMatch'. func (s *Storage) Get(ctx context.Context, key string, opts storage.GetOptions, objPtr runtime.Object) error { + resourceVersion := int64(0) + var err error + if opts.ResourceVersion != "" { + resourceVersion, err = strconv.ParseInt(opts.ResourceVersion, 10, 64) + if err != nil { + return apierrors.NewBadRequest(fmt.Sprintf("invalid resource version: %s", opts.ResourceVersion)) + } + } + rsp, err := s.store.Read(ctx, &entityStore.ReadEntityRequest{ - Key: key, - WithBody: true, - WithStatus: true, + Key: key, + WithBody: true, + WithStatus: true, + ResourceVersion: resourceVersion, }) if err != nil { return err @@ -360,15 +370,9 @@ func (s *Storage) GetList(ctx context.Context, key string, opts storage.ListOpti return apierrors.NewInternalError(err) } - maxResourceVersion := int64(0) - for _, r := range rsp.Results { res := s.newFunc() - if r.ResourceVersion > maxResourceVersion { - maxResourceVersion = r.ResourceVersion - } - err := entityToResource(r, res, s.codec) if err != nil { return apierrors.NewInternalError(err) @@ -395,7 +399,7 @@ func (s *Storage) GetList(ctx context.Context, key string, opts storage.ListOpti listAccessor.SetContinue(rsp.NextPageToken) } - listAccessor.SetResourceVersion(strconv.FormatInt(maxResourceVersion, 10)) + listAccessor.SetResourceVersion(strconv.FormatInt(rsp.ResourceVersion, 10)) return nil } diff --git a/pkg/services/store/entity/entity.pb.go b/pkg/services/store/entity/entity.pb.go index 8d3caf23f5..afa235d6e8 100644 --- a/pkg/services/store/entity/entity.pb.go +++ b/pkg/services/store/entity/entity.pb.go @@ -1548,6 +1548,8 @@ type EntityListResponse struct { Results []*Entity `protobuf:"bytes,1,rep,name=results,proto3" json:"results,omitempty"` // More results exist... pass this in the next request NextPageToken string `protobuf:"bytes,2,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"` + // ResourceVersion of the list response + ResourceVersion int64 `protobuf:"varint,3,opt,name=resource_version,json=resourceVersion,proto3" json:"resource_version,omitempty"` } func (x *EntityListResponse) Reset() { @@ -1596,6 +1598,13 @@ func (x *EntityListResponse) GetNextPageToken() string { return "" } +func (x *EntityListResponse) GetResourceVersion() int64 { + if x != nil { + return x.ResourceVersion + } + return 0 +} + type EntityWatchRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -2167,117 +2176,120 @@ var file_entity_proto_rawDesc = []byte{ 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x66, 0x0a, 0x12, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, - 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x28, 0x0a, 0x07, - 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, - 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x07, 0x72, - 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, - 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0xa9, - 0x02, 0x0a, 0x12, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x57, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6b, - 0x65, 0x79, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x1a, 0x0a, - 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, - 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x6f, 0x6c, - 0x64, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, - 0x72, 0x12, 0x3e, 0x0a, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x26, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, - 0x79, 0x57, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x4c, 0x61, - 0x62, 0x65, 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, - 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x77, 0x69, 0x74, 0x68, 0x5f, 0x62, 0x6f, 0x64, 0x79, 0x18, 0x06, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x77, 0x69, 0x74, 0x68, 0x42, 0x6f, 0x64, 0x79, 0x12, 0x1f, - 0x0a, 0x0b, 0x77, 0x69, 0x74, 0x68, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x07, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x0a, 0x77, 0x69, 0x74, 0x68, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x1a, - 0x39, 0x0a, 0x0b, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, - 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, - 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x5b, 0x0a, 0x13, 0x45, 0x6e, - 0x74, 0x69, 0x74, 0x79, 0x57, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, - 0x26, 0x0a, 0x06, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x0e, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, - 0x06, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x22, 0xa2, 0x04, 0x0a, 0x0d, 0x45, 0x6e, 0x74, 0x69, - 0x74, 0x79, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x49, 0x44, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x55, 0x49, 0x44, 0x12, 0x12, 0x0a, 0x04, 0x6b, - 0x69, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x12, - 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, - 0x61, 0x6d, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, - 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, - 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x39, 0x0a, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, - 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, - 0x6e, 0x74, 0x69, 0x74, 0x79, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x2e, 0x4c, 0x61, 0x62, - 0x65, 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, - 0x12, 0x16, 0x0a, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x6c, 0x75, 0x67, - 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x73, 0x6c, 0x75, 0x67, 0x12, 0x2d, 0x0a, 0x05, - 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x65, 0x6e, - 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x45, 0x72, 0x72, 0x6f, 0x72, - 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x39, 0x0a, 0x06, 0x66, - 0x69, 0x65, 0x6c, 0x64, 0x73, 0x18, 0x09, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x65, 0x6e, - 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x53, 0x75, 0x6d, 0x6d, 0x61, - 0x72, 0x79, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, - 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x12, 0x2d, 0x0a, 0x06, 0x6e, 0x65, 0x73, 0x74, 0x65, 0x64, - 0x18, 0x0a, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, - 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x52, 0x06, 0x6e, - 0x65, 0x73, 0x74, 0x65, 0x64, 0x12, 0x3f, 0x0a, 0x0a, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, - 0x63, 0x65, 0x73, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x65, 0x6e, 0x74, 0x69, - 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x45, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, - 0x6c, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x0a, 0x72, 0x65, 0x66, 0x65, - 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x1a, 0x39, 0x0a, 0x0b, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, - 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, - 0x01, 0x1a, 0x39, 0x0a, 0x0b, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, - 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, - 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x65, 0x0a, 0x17, - 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x45, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x52, 0x65, - 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x61, 0x6d, 0x69, 0x6c, - 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x61, 0x6d, 0x69, 0x6c, 0x79, 0x12, - 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, - 0x79, 0x70, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, - 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, - 0x69, 0x65, 0x72, 0x32, 0xa8, 0x04, 0x0a, 0x0b, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x53, 0x74, - 0x6f, 0x72, 0x65, 0x12, 0x31, 0x0a, 0x04, 0x52, 0x65, 0x61, 0x64, 0x12, 0x19, 0x2e, 0x65, 0x6e, - 0x74, 0x69, 0x74, 0x79, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0e, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, - 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x4c, 0x0a, 0x09, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, - 0x65, 0x61, 0x64, 0x12, 0x1e, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x42, 0x61, 0x74, - 0x63, 0x68, 0x52, 0x65, 0x61, 0x64, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x42, 0x61, 0x74, - 0x63, 0x68, 0x52, 0x65, 0x61, 0x64, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x43, 0x0a, 0x06, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x12, 0x1b, - 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x45, 0x6e, - 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x65, 0x6e, - 0x74, 0x69, 0x74, 0x79, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, - 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x43, 0x0a, 0x06, 0x55, 0x70, 0x64, - 0x61, 0x74, 0x65, 0x12, 0x1b, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x55, 0x70, 0x64, - 0x61, 0x74, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x1c, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, - 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x43, - 0x0a, 0x06, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x1b, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, - 0x79, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x44, - 0x65, 0x6c, 0x65, 0x74, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x46, 0x0a, 0x07, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x1c, - 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x48, 0x69, - 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x65, - 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x48, 0x69, 0x73, 0x74, - 0x6f, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3d, 0x0a, 0x04, 0x4c, - 0x69, 0x73, 0x74, 0x12, 0x19, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, - 0x69, 0x74, 0x79, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, - 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x4c, 0x69, - 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x42, 0x0a, 0x05, 0x57, 0x61, - 0x74, 0x63, 0x68, 0x12, 0x1a, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, - 0x69, 0x74, 0x79, 0x57, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x1b, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x57, - 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x42, 0x36, - 0x5a, 0x34, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, 0x61, - 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x70, 0x6b, 0x67, - 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2f, - 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x91, 0x01, 0x0a, 0x12, 0x45, 0x6e, 0x74, 0x69, 0x74, + 0x79, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x28, 0x0a, + 0x07, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, + 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x07, + 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, + 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, + 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, + 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0xa9, 0x02, 0x0a, 0x12, 0x45, + 0x6e, 0x74, 0x69, 0x74, 0x79, 0x57, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x05, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x02, + 0x20, 0x03, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x12, 0x3e, 0x0a, + 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x26, 0x2e, + 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x57, 0x61, 0x74, + 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, + 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x12, 0x1b, 0x0a, + 0x09, 0x77, 0x69, 0x74, 0x68, 0x5f, 0x62, 0x6f, 0x64, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x08, 0x77, 0x69, 0x74, 0x68, 0x42, 0x6f, 0x64, 0x79, 0x12, 0x1f, 0x0a, 0x0b, 0x77, 0x69, + 0x74, 0x68, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x0a, 0x77, 0x69, 0x74, 0x68, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x1a, 0x39, 0x0a, 0x0b, 0x4c, + 0x61, 0x62, 0x65, 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, + 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x5b, 0x0a, 0x13, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, + 0x57, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1c, 0x0a, + 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x26, 0x0a, 0x06, 0x65, + 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x65, 0x6e, + 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x06, 0x65, 0x6e, 0x74, + 0x69, 0x74, 0x79, 0x22, 0xa2, 0x04, 0x0a, 0x0d, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x53, 0x75, + 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x03, 0x55, 0x49, 0x44, 0x12, 0x12, 0x0a, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, + 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, + 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, + 0x6e, 0x12, 0x39, 0x0a, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x21, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, + 0x79, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x2e, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x45, + 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x12, 0x16, 0x0a, 0x06, + 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6f, + 0x6c, 0x64, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x6c, 0x75, 0x67, 0x18, 0x07, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x04, 0x73, 0x6c, 0x75, 0x67, 0x12, 0x2d, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, + 0x72, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, + 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x49, 0x6e, 0x66, 0x6f, + 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x39, 0x0a, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, + 0x73, 0x18, 0x09, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, + 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x2e, 0x46, + 0x69, 0x65, 0x6c, 0x64, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, 0x66, 0x69, 0x65, 0x6c, + 0x64, 0x73, 0x12, 0x2d, 0x0a, 0x06, 0x6e, 0x65, 0x73, 0x74, 0x65, 0x64, 0x18, 0x0a, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, + 0x74, 0x79, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x52, 0x06, 0x6e, 0x65, 0x73, 0x74, 0x65, + 0x64, 0x12, 0x3f, 0x0a, 0x0a, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x18, + 0x0b, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, + 0x6e, 0x74, 0x69, 0x74, 0x79, 0x45, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x52, 0x65, 0x66, + 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x0a, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, + 0x65, 0x73, 0x1a, 0x39, 0x0a, 0x0b, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, + 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, + 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x39, 0x0a, + 0x0b, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, + 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, + 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x65, 0x0a, 0x17, 0x45, 0x6e, 0x74, 0x69, + 0x74, 0x79, 0x45, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, + 0x6e, 0x63, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x61, 0x6d, 0x69, 0x6c, 0x79, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x61, 0x6d, 0x69, 0x6c, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x74, + 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, + 0x1e, 0x0a, 0x0a, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0a, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x32, + 0xa8, 0x04, 0x0a, 0x0b, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x12, + 0x31, 0x0a, 0x04, 0x52, 0x65, 0x61, 0x64, 0x12, 0x19, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, + 0x2e, 0x52, 0x65, 0x61, 0x64, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x0e, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, + 0x74, 0x79, 0x12, 0x4c, 0x0a, 0x09, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x61, 0x64, 0x12, + 0x1e, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, + 0x61, 0x64, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x1f, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, + 0x61, 0x64, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x43, 0x0a, 0x06, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x12, 0x1b, 0x2e, 0x65, 0x6e, 0x74, + 0x69, 0x74, 0x79, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, + 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x43, 0x0a, 0x06, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, + 0x1b, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x45, + 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x65, + 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x74, 0x69, + 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x43, 0x0a, 0x06, 0x44, 0x65, + 0x6c, 0x65, 0x74, 0x65, 0x12, 0x1b, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x44, 0x65, + 0x6c, 0x65, 0x74, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x1c, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, + 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x46, 0x0a, 0x07, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x1c, 0x2e, 0x65, 0x6e, 0x74, + 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, + 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, + 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3d, 0x0a, 0x04, 0x4c, 0x69, 0x73, 0x74, 0x12, + 0x19, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x4c, + 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x65, 0x6e, 0x74, + 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x42, 0x0a, 0x05, 0x57, 0x61, 0x74, 0x63, 0x68, 0x12, + 0x1a, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x57, + 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x65, 0x6e, + 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x57, 0x61, 0x74, 0x63, 0x68, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x42, 0x36, 0x5a, 0x34, 0x67, 0x69, + 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, + 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x73, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2f, 0x65, 0x6e, 0x74, 0x69, + 0x74, 0x79, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/pkg/services/store/entity/entity.proto b/pkg/services/store/entity/entity.proto index f44ba530f5..694731eb08 100644 --- a/pkg/services/store/entity/entity.proto +++ b/pkg/services/store/entity/entity.proto @@ -325,6 +325,9 @@ message EntityListResponse { // More results exist... pass this in the next request string next_page_token = 2; + + // ResourceVersion of the list response + int64 resource_version = 3; } //----------------------------------------------- diff --git a/pkg/services/store/entity/sqlstash/sql_storage_server.go b/pkg/services/store/entity/sqlstash/sql_storage_server.go index 3b4e51f2c2..eb88dba005 100644 --- a/pkg/services/store/entity/sqlstash/sql_storage_server.go +++ b/pkg/services/store/entity/sqlstash/sql_storage_server.go @@ -214,7 +214,7 @@ func (s *sqlEntityServer) read(ctx context.Context, tx session.SessionQuerier, r if r.ResourceVersion != 0 { table = "entity_history" - where = append(where, s.dialect.Quote("resource_version")+"=?") + where = append(where, s.dialect.Quote("resource_version")+">=?") args = append(args, r.ResourceVersion) } @@ -230,6 +230,11 @@ func (s *sqlEntityServer) read(ctx context.Context, tx session.SessionQuerier, r query += " FROM " + table + " WHERE " + strings.Join(where, " AND ") + if r.ResourceVersion != 0 { + query += " ORDER BY resource_version DESC" + } + query += " LIMIT 1" + s.log.Debug("read", "query", query, "args", args) rows, err := tx.Query(ctx, query, args...) @@ -1156,7 +1161,9 @@ func (s *sqlEntityServer) List(ctx context.Context, r *entity.EntityListRequest) return nil, err } defer func() { _ = rows.Close() }() - rsp := &entity.EntityListResponse{} + rsp := &entity.EntityListResponse{ + ResourceVersion: s.snowflake.Generate().Int64(), + } for rows.Next() { result, err := s.rowToEntity(ctx, rows, rr) if err != nil { From 3f2820a55218e2e3b805b21caacabf9726d11d5e Mon Sep 17 00:00:00 2001 From: Andreas Christou Date: Tue, 5 Mar 2024 16:09:42 +0000 Subject: [PATCH 0402/1406] Chore: Bump whats new (#83841) Bump whats new --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1ac1116a48..551eaac7cc 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "plugin:build:dev": "lerna run dev --ignore=\"@grafana/*\" --ignore=\"@grafana-plugins/input-datasource\"" }, "grafana": { - "whatsNewUrl": "https://grafana.com/docs/grafana/next/whatsnew/whats-new-in-v10-3/", + "whatsNewUrl": "https://grafana.com/docs/grafana/next/whatsnew/whats-new-in-v11-0/", "releaseNotesUrl": "https://grafana.com/docs/grafana/next/release-notes/" }, "devDependencies": { From e916372249833f21ae5c09915a26758a52a87970 Mon Sep 17 00:00:00 2001 From: Charandas Date: Tue, 5 Mar 2024 10:34:47 -0800 Subject: [PATCH 0403/1406] K8s: bug fixes for file storage to allow for watcher initialization on startup (#83873) --------- Co-authored-by: Todd Treece --- pkg/apiserver/storage/file/file.go | 47 ++++++++++++++------ pkg/apiserver/storage/file/restoptions.go | 22 ++++++++- pkg/apiserver/storage/file/util.go | 15 +++++-- pkg/services/apiserver/config.go | 2 +- pkg/services/apiserver/options/aggregator.go | 9 +++- pkg/services/apiserver/options/storage.go | 2 +- pkg/services/apiserver/service.go | 15 ++++--- 7 files changed, 83 insertions(+), 29 deletions(-) diff --git a/pkg/apiserver/storage/file/file.go b/pkg/apiserver/storage/file/file.go index 20f9b5c9c3..d3c70ba268 100644 --- a/pkg/apiserver/storage/file/file.go +++ b/pkg/apiserver/storage/file/file.go @@ -120,12 +120,12 @@ func (s *Storage) Versioner() storage.Versioner { // in seconds (0 means forever). If no error is returned and out is not nil, out will be // set to the read value from database. func (s *Storage) Create(ctx context.Context, key string, obj runtime.Object, out runtime.Object, ttl uint64) error { - filename := s.filePath(key) - if exists(filename) { + fpath := s.filePath(key) + if exists(fpath) { return storage.NewKeyExistsError(key, 0) } - dirname := filepath.Dir(filename) + dirname := filepath.Dir(fpath) if err := ensureDir(dirname); err != nil { return err } @@ -148,7 +148,7 @@ func (s *Storage) Create(ctx context.Context, key string, obj runtime.Object, ou return err } - if err := writeFile(s.codec, filename, obj); err != nil { + if err := writeFile(s.codec, fpath, obj); err != nil { return err } @@ -186,7 +186,7 @@ func (s *Storage) Delete( validateDeletion storage.ValidateObjectFunc, cachedExistingObject runtime.Object, ) error { - filename := s.filePath(key) + fpath := s.filePath(key) var currentState runtime.Object var stateIsCurrent bool if cachedExistingObject != nil { @@ -241,7 +241,7 @@ func (s *Storage) Delete( return err } - if err := deleteFile(filename); err != nil { + if err := deleteFile(fpath); err != nil { return err } @@ -305,8 +305,15 @@ func (s *Storage) Watch(ctx context.Context, key string, opts storage.ListOption // The returned contents may be delayed, but it is guaranteed that they will // match 'opts.ResourceVersion' according 'opts.ResourceVersionMatch'. func (s *Storage) Get(ctx context.Context, key string, opts storage.GetOptions, objPtr runtime.Object) error { - filename := s.filePath(key) - obj, err := readFile(s.codec, filename, func() runtime.Object { + fpath := s.filePath(key) + + // Since it's a get, check if the dir exists and return early as needed + dirname := filepath.Dir(fpath) + if !exists(dirname) { + return apierrors.NewNotFound(s.gr, s.nameFromKey(key)) + } + + obj, err := readFile(s.codec, fpath, func() runtime.Object { return objPtr }) if err != nil { @@ -364,9 +371,14 @@ func (s *Storage) GetList(ctx context.Context, key string, opts storage.ListOpti } } - dirname := s.dirPath(key) + dirpath := s.dirPath(key) + // Since it's a get, check if the dir exists and return early as needed + if !exists(dirpath) { + // ensure we return empty list in listObj insted of a not found error + return nil + } - objs, err := readDirRecursive(s.codec, dirname, s.newFunc) + objs, err := readDirRecursive(s.codec, dirpath, s.newFunc) if err != nil { return err } @@ -424,18 +436,25 @@ func (s *Storage) GuaranteedUpdate( var res storage.ResponseMeta for attempt := 1; attempt <= MaxUpdateAttempts; attempt = attempt + 1 { var ( - filename = s.filePath(key) + fpath = s.filePath(key) + dirpath = filepath.Dir(fpath) obj runtime.Object err error created bool ) - if !exists(filename) && !ignoreNotFound { + if !exists(dirpath) { + if err := ensureDir(dirpath); err != nil { + return err + } + } + + if !exists(fpath) && !ignoreNotFound { return apierrors.NewNotFound(s.gr, s.nameFromKey(key)) } - obj, err = readFile(s.codec, filename, s.newFunc) + obj, err = readFile(s.codec, fpath, s.newFunc) if err != nil { // fallback to new object if the file is not found obj = s.newFunc() @@ -482,7 +501,7 @@ func (s *Storage) GuaranteedUpdate( if err := s.Versioner().UpdateObject(updatedObj, *generatedRV); err != nil { return err } - if err := writeFile(s.codec, filename, updatedObj); err != nil { + if err := writeFile(s.codec, fpath, updatedObj); err != nil { return err } eventType := watch.Modified diff --git a/pkg/apiserver/storage/file/restoptions.go b/pkg/apiserver/storage/file/restoptions.go index 34ac132fa7..52981cef7f 100644 --- a/pkg/apiserver/storage/file/restoptions.go +++ b/pkg/apiserver/storage/file/restoptions.go @@ -20,12 +20,30 @@ type RESTOptionsGetter struct { original storagebackend.Config } -func NewRESTOptionsGetter(path string, originalStorageConfig storagebackend.Config) *RESTOptionsGetter { +// Optionally, this constructor allows specifying directories +// for resources that are required to be read/watched on startup and there +// won't be any write operations that initially bootstrap their directories +func NewRESTOptionsGetter(path string, + originalStorageConfig storagebackend.Config, + createResourceDirs ...string) (*RESTOptionsGetter, error) { if path == "" { path = filepath.Join(os.TempDir(), "grafana-apiserver") } - return &RESTOptionsGetter{path: path, original: originalStorageConfig} + if err := initializeDirs(path, createResourceDirs); err != nil { + return nil, err + } + + return &RESTOptionsGetter{path: path, original: originalStorageConfig}, nil +} + +func initializeDirs(root string, createResourceDirs []string) error { + for _, dir := range createResourceDirs { + if err := ensureDir(filepath.Join(root, dir)); err != nil { + return err + } + } + return nil } func (r *RESTOptionsGetter) GetRESTOptions(resource schema.GroupResource) (generic.RESTOptions, error) { diff --git a/pkg/apiserver/storage/file/util.go b/pkg/apiserver/storage/file/util.go index 26885dde8c..67cd5452bc 100644 --- a/pkg/apiserver/storage/file/util.go +++ b/pkg/apiserver/storage/file/util.go @@ -22,11 +22,11 @@ func (s *Storage) filePath(key string) string { return fileName } +// this is for constructing dirPath in a sanitized way provided you have +// already calculated the key. In order to go in the other direction, from a file path +// key to its dir, use the go standard library: filepath.Dir func (s *Storage) dirPath(key string) string { - // Replace backslashes with underscores to avoid creating bogus subdirectories - key = strings.Replace(key, "\\", "_", -1) - dirName := filepath.Join(s.root, filepath.Clean(key)) - return dirName + return dirPath(s.root, key) } func writeFile(codec runtime.Codec, path string, obj runtime.Object) error { @@ -84,6 +84,13 @@ func exists(filepath string) bool { return err == nil } +func dirPath(root string, key string) string { + // Replace backslashes with underscores to avoid creating bogus subdirectories + key = strings.Replace(key, "\\", "_", -1) + dirName := filepath.Join(root, filepath.Clean(key)) + return dirName +} + func ensureDir(dirname string) error { if !exists(dirname) { return os.MkdirAll(dirname, 0700) diff --git a/pkg/services/apiserver/config.go b/pkg/services/apiserver/config.go index 6249793453..8f05b7ef7b 100644 --- a/pkg/services/apiserver/config.go +++ b/pkg/services/apiserver/config.go @@ -48,7 +48,7 @@ func applyGrafanaConfig(cfg *setting.Cfg, features featuremgmt.FeatureToggles, o o.RecommendedOptions.CoreAPI = nil o.StorageOptions.StorageType = options.StorageType(apiserverCfg.Key("storage_type").MustString(string(options.StorageTypeLegacy))) - o.StorageOptions.DataPath = filepath.Join(cfg.DataPath, "grafana-apiserver") + o.StorageOptions.DataPath = apiserverCfg.Key("storage_path").MustString(filepath.Join(cfg.DataPath, "grafana-apiserver")) o.ExtraOptions.DevMode = features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerEnsureKubectlAccess) o.ExtraOptions.ExternalAddress = host o.ExtraOptions.APIURL = apiURL diff --git a/pkg/services/apiserver/options/aggregator.go b/pkg/services/apiserver/options/aggregator.go index 1d38a76c58..a9b0009781 100644 --- a/pkg/services/apiserver/options/aggregator.go +++ b/pkg/services/apiserver/options/aggregator.go @@ -89,7 +89,14 @@ func (o *AggregatorServerOptions) ApplyTo(aggregatorConfig *aggregatorapiserver. return err } // override the RESTOptionsGetter to use the file storage options getter - aggregatorConfig.GenericConfig.RESTOptionsGetter = filestorage.NewRESTOptionsGetter(dataPath, etcdOptions.StorageConfig) + restOptionsGetter, err := filestorage.NewRESTOptionsGetter(dataPath, etcdOptions.StorageConfig, + "apiregistration.k8s.io/apiservices", + "service.grafana.app/externalnames", + ) + if err != nil { + return err + } + aggregatorConfig.GenericConfig.RESTOptionsGetter = restOptionsGetter // prevent generic API server from installing the OpenAPI handler. Aggregator server has its own customized OpenAPI handler. genericConfig.SkipOpenAPIInstallation = true diff --git a/pkg/services/apiserver/options/storage.go b/pkg/services/apiserver/options/storage.go index 51ccb84efe..55c2933583 100644 --- a/pkg/services/apiserver/options/storage.go +++ b/pkg/services/apiserver/options/storage.go @@ -31,7 +31,7 @@ func NewStorageOptions() *StorageOptions { func (o *StorageOptions) AddFlags(fs *pflag.FlagSet) { fs.StringVar((*string)(&o.StorageType), "grafana-apiserver-storage-type", string(o.StorageType), "Storage type") - fs.StringVar((*string)(&o.StorageType), "grafana-apiserver-storage-path", string(o.StorageType), "Storage path for file storage") + fs.StringVar(&o.DataPath, "grafana-apiserver-storage-path", o.DataPath, "Storage path for file storage") } func (o *StorageOptions) Validate() []error { diff --git a/pkg/services/apiserver/service.go b/pkg/services/apiserver/service.go index 405359159c..7a4bb27ef3 100644 --- a/pkg/services/apiserver/service.go +++ b/pkg/services/apiserver/service.go @@ -31,6 +31,7 @@ import ( "github.com/grafana/grafana/pkg/services/apiserver/aggregator" "github.com/grafana/grafana/pkg/services/apiserver/auth/authenticator" "github.com/grafana/grafana/pkg/services/apiserver/auth/authorizer" + "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" grafanaapiserveroptions "github.com/grafana/grafana/pkg/services/apiserver/options" entitystorage "github.com/grafana/grafana/pkg/services/apiserver/storage/entity" "github.com/grafana/grafana/pkg/services/apiserver/utils" @@ -277,7 +278,11 @@ func (s *service) start(ctx context.Context) error { case grafanaapiserveroptions.StorageTypeLegacy: fallthrough case grafanaapiserveroptions.StorageTypeFile: - serverConfig.RESTOptionsGetter = filestorage.NewRESTOptionsGetter(o.StorageOptions.DataPath, o.RecommendedOptions.Etcd.StorageConfig) + restOptionsGetter, err := filestorage.NewRESTOptionsGetter(o.StorageOptions.DataPath, o.RecommendedOptions.Etcd.StorageConfig) + if err != nil { + return err + } + serverConfig.RESTOptionsGetter = restOptionsGetter } // Add OpenAPI specs for each group+version @@ -367,11 +372,9 @@ func (s *service) startAggregator( serverConfig *genericapiserver.RecommendedConfig, server *genericapiserver.GenericAPIServer, ) (*genericapiserver.GenericAPIServer, error) { - externalNamesNamespace := "default" - if s.cfg.StackID != "" { - externalNamesNamespace = s.cfg.StackID - } - aggregatorConfig, err := aggregator.CreateAggregatorConfig(s.options, *serverConfig, externalNamesNamespace) + namespaceMapper := request.GetNamespaceMapper(s.cfg) + + aggregatorConfig, err := aggregator.CreateAggregatorConfig(s.options, *serverConfig, namespaceMapper(1)) if err != nil { return nil, err } From f4da9bd09e66090f26c0c949b029616cf484bb70 Mon Sep 17 00:00:00 2001 From: Kyle Cunningham Date: Tue, 5 Mar 2024 13:00:41 -0600 Subject: [PATCH 0404/1406] Table Panel: Text wrapping fix inspect viewing issues (#83867) * Fix inspect viewing issues * Fix long line wrapping * Prettier * Fix text wrapping * Fix overflowing text --- .../src/components/Table/DefaultCell.tsx | 26 +++++-------------- .../grafana-ui/src/components/Table/styles.ts | 8 +++--- 2 files changed, 11 insertions(+), 23 deletions(-) diff --git a/packages/grafana-ui/src/components/Table/DefaultCell.tsx b/packages/grafana-ui/src/components/Table/DefaultCell.tsx index 90628ee0d6..a8f6436d74 100644 --- a/packages/grafana-ui/src/components/Table/DefaultCell.tsx +++ b/packages/grafana-ui/src/components/Table/DefaultCell.tsx @@ -132,25 +132,13 @@ function getCellStyle( // If we have definied colors return those styles // Otherwise we return default styles - if (textColor !== undefined || bgColor !== undefined) { - return tableStyles.buildCellContainerStyle( - textColor, - bgColor, - !disableOverflowOnHover, - isStringValue, - shouldWrapText - ); - } - - if (isStringValue) { - return disableOverflowOnHover - ? tableStyles.buildCellContainerStyle(undefined, undefined, false, true, shouldWrapText) - : tableStyles.buildCellContainerStyle(undefined, undefined, true, true, shouldWrapText); - } else { - return disableOverflowOnHover - ? tableStyles.buildCellContainerStyle(undefined, undefined, false, shouldWrapText) - : tableStyles.buildCellContainerStyle(undefined, undefined, true, false, shouldWrapText); - } + return tableStyles.buildCellContainerStyle( + textColor, + bgColor, + !disableOverflowOnHover, + isStringValue, + shouldWrapText + ); } function getLinkStyle(tableStyles: TableStyles, cellOptions: TableCellOptions, targetClassName: string | undefined) { diff --git a/packages/grafana-ui/src/components/Table/styles.ts b/packages/grafana-ui/src/components/Table/styles.ts index 2446d96d9c..787e2ee5b5 100644 --- a/packages/grafana-ui/src/components/Table/styles.ts +++ b/packages/grafana-ui/src/components/Table/styles.ts @@ -49,14 +49,14 @@ export function useTableStyles(theme: GrafanaTheme2, cellHeightOption: TableCell '&:hover': { overflow: overflowOnHover ? 'visible' : undefined, - width: textShouldWrap ? 'auto' : 'auto !important', - height: textShouldWrap ? 'auto !important' : `${rowHeight - 1}px`, + width: textShouldWrap || !overflowOnHover ? 'auto' : 'auto !important', + height: textShouldWrap || overflowOnHover ? 'auto !important' : `${rowHeight - 1}px`, minHeight: `${rowHeight - 1}px`, wordBreak: textShouldWrap ? 'break-word' : undefined, - whiteSpace: overflowOnHover ? 'normal' : 'nowrap', + whiteSpace: textShouldWrap && overflowOnHover ? 'normal' : 'nowrap', boxShadow: overflowOnHover ? `0 0 2px ${theme.colors.primary.main}` : undefined, background: overflowOnHover ? background ?? theme.components.table.rowHoverBackground : undefined, - zIndex: overflowOnHover ? 1 : undefined, + zIndex: 1, '.cellActions': { visibility: 'visible', opacity: 1, From cb008657cb02f1184a782231f4f544ac445377ae Mon Sep 17 00:00:00 2001 From: Kyle Cunningham Date: Tue, 5 Mar 2024 13:54:31 -0600 Subject: [PATCH 0405/1406] Table Panel: Update column filters to use Stack component (#83800) * Update filter list to use stack * Remove dead comments --- .../src/components/Table/FilterList.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/grafana-ui/src/components/Table/FilterList.tsx b/packages/grafana-ui/src/components/Table/FilterList.tsx index 9a65476e6f..1ea721809d 100644 --- a/packages/grafana-ui/src/components/Table/FilterList.tsx +++ b/packages/grafana-ui/src/components/Table/FilterList.tsx @@ -4,7 +4,7 @@ import { FixedSizeList as List } from 'react-window'; import { GrafanaTheme2, formattedValueToString, getValueFormat, SelectableValue } from '@grafana/data'; -import { ButtonSelect, Checkbox, FilterInput, HorizontalGroup, Label, VerticalGroup } from '..'; +import { ButtonSelect, Checkbox, FilterInput, Label, Stack } from '..'; import { useStyles2, useTheme2 } from '../../themes'; interface Props { @@ -169,11 +169,11 @@ export const FilterList = ({ }, [onChange, values, items, selectedItems]); return ( - + {!showOperators && } {showOperators && ( - - + + - + )} {!items.length && } {items.length && ( @@ -206,7 +206,7 @@ export const FilterList = ({ )} {items.length && ( - +
- + )} - + ); }; From 3e86a4edc8b8a81225b774adff58b497fc338a5d Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Tue, 5 Mar 2024 12:20:38 -0800 Subject: [PATCH 0406/1406] PanelTitleSearch: Show datasource usage in plugins (#83922) --- pkg/services/searchV2/bluge.go | 6 ++++++ pkg/services/searchV2/types.go | 2 +- .../features/plugins/admin/components/PluginUsage.tsx | 9 ++++++++- .../plugins/admin/hooks/usePluginDetailsTabs.tsx | 5 ++++- 4 files changed, 19 insertions(+), 3 deletions(-) diff --git a/pkg/services/searchV2/bluge.go b/pkg/services/searchV2/bluge.go index db0b94b5ee..dd290a2ed8 100644 --- a/pkg/services/searchV2/bluge.go +++ b/pkg/services/searchV2/bluge.go @@ -435,6 +435,12 @@ func doSearchQuery( hasConstraints = true } + // DatasourceType + if q.DatasourceType != "" { + fullQuery.AddMust(bluge.NewTermQuery(q.DatasourceType).SetField(documentFieldDSType)) + hasConstraints = true + } + // Folder if q.Location != "" { fullQuery.AddMust(bluge.NewTermQuery(q.Location).SetField(documentFieldLocation)) diff --git a/pkg/services/searchV2/types.go b/pkg/services/searchV2/types.go index 2c6b7fcd72..f66c7702b4 100644 --- a/pkg/services/searchV2/types.go +++ b/pkg/services/searchV2/types.go @@ -18,7 +18,7 @@ type DashboardQuery struct { Query string `json:"query"` Location string `json:"location,omitempty"` // parent folder ID Sort string `json:"sort,omitempty"` // field ASC/DESC - Datasource string `json:"ds_uid,omitempty"` // "datasource" collides with the JSON value at the same leel :() + Datasource string `json:"ds_uid,omitempty"` // "datasource" collides with the JSON value at the same level :() DatasourceType string `json:"ds_type,omitempty"` Tags []string `json:"tags,omitempty"` Kind []string `json:"kind,omitempty"` diff --git a/public/app/features/plugins/admin/components/PluginUsage.tsx b/public/app/features/plugins/admin/components/PluginUsage.tsx index 1b82008378..692d1fe3b2 100644 --- a/public/app/features/plugins/admin/components/PluginUsage.tsx +++ b/public/app/features/plugins/admin/components/PluginUsage.tsx @@ -4,7 +4,7 @@ import { useAsync } from 'react-use'; import AutoSizer from 'react-virtualized-auto-sizer'; import { of } from 'rxjs'; -import { GrafanaTheme2, PluginMeta } from '@grafana/data'; +import { GrafanaTheme2, PluginMeta, PluginType } from '@grafana/data'; import { config } from '@grafana/runtime'; import { Alert, Spinner, useStyles2 } from '@grafana/ui'; import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA'; @@ -19,6 +19,13 @@ export function PluginUsage({ plugin }: Props) { const styles = useStyles2(getStyles); const searchQuery = useMemo(() => { + if (plugin.type === PluginType.datasource) { + return { + query: '*', + ds_type: plugin.id, + kind: ['dashboard'], + }; + } return { query: '*', panel_type: plugin.id, diff --git a/public/app/features/plugins/admin/hooks/usePluginDetailsTabs.tsx b/public/app/features/plugins/admin/hooks/usePluginDetailsTabs.tsx index 63f3008a5a..5ff258b591 100644 --- a/public/app/features/plugins/admin/hooks/usePluginDetailsTabs.tsx +++ b/public/app/features/plugins/admin/hooks/usePluginDetailsTabs.tsx @@ -51,7 +51,10 @@ export const usePluginDetailsTabs = (plugin?: CatalogPlugin, pageId?: PluginTabI }); } - if (config.featureToggles.panelTitleSearch && pluginConfig.meta.type === PluginType.panel) { + if ( + config.featureToggles.panelTitleSearch && + (pluginConfig.meta.type === PluginType.panel || pluginConfig.meta.type === PluginType.datasource) + ) { navModelChildren.push({ text: PluginTabLabels.USAGE, icon: 'list-ul', From 01fb2cff624e402b927e16f4e04e9fc35c7ab08c Mon Sep 17 00:00:00 2001 From: Dave Henderson Date: Tue, 5 Mar 2024 15:24:34 -0500 Subject: [PATCH 0407/1406] chore: bump Go to 1.21.8 (#83927) * chore: bump Go to 1.21.8 Signed-off-by: Dave Henderson * bump workflows too Signed-off-by: Dave Henderson --------- Signed-off-by: Dave Henderson --- .drone.yml | 208 ++++++++++---------- .github/workflows/alerting-swagger-gen.yml | 2 +- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/pr-codeql-analysis-go.yml | 2 +- .github/workflows/publish-kinds-next.yml | 2 +- .github/workflows/publish-kinds-release.yml | 2 +- .github/workflows/verify-kinds.yml | 2 +- Dockerfile | 2 +- Makefile | 2 +- scripts/drone/variables.star | 2 +- 10 files changed, 113 insertions(+), 113 deletions(-) diff --git a/.drone.yml b/.drone.yml index 5c23955586..14ae569f2b 100644 --- a/.drone.yml +++ b/.drone.yml @@ -25,7 +25,7 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: compile-build-cmd - commands: - ./bin/build verify-drone @@ -76,14 +76,14 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: compile-build-cmd - commands: - go install github.com/bazelbuild/buildtools/buildifier@latest - buildifier --lint=warn -mode=check -r . depends_on: - compile-build-cmd - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: lint-starlark trigger: event: @@ -323,7 +323,7 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -332,14 +332,14 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: [] - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: verify-gen-jsonnet - commands: - apk add --update make - make gen-go depends_on: - verify-gen-cue - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: wire-install - commands: - apk add --update build-base shared-mime-info shared-mime-info-lang @@ -347,7 +347,7 @@ steps: -timeout=5m depends_on: - wire-install - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: test-backend - commands: - apk add --update build-base @@ -356,7 +356,7 @@ steps: | grep -o '\(.*\)/' | sort -u) depends_on: - wire-install - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: test-backend-integration trigger: event: @@ -408,7 +408,7 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: compile-build-cmd - commands: - apk add --update curl jq bash @@ -435,7 +435,7 @@ steps: - apk add --update make - make gen-go depends_on: [] - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: wire-install - commands: - apk add --update make build-base @@ -444,16 +444,16 @@ steps: - wire-install environment: CGO_ENABLED: "1" - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: lint-backend - commands: - go run scripts/modowners/modowners.go check go.mod - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: validate-modfile - commands: - apk add --update make - make swagger-validate - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: validate-openapi-spec trigger: event: @@ -511,7 +511,7 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: compile-build-cmd - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -521,7 +521,7 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -530,14 +530,14 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: [] - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: verify-gen-jsonnet - commands: - apk add --update make - make gen-go depends_on: - verify-gen-cue - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: wire-install - commands: - apk add --update g++ make python3 && ln -sf /usr/bin/python3 /usr/bin/python @@ -571,7 +571,7 @@ steps: from_secret: drone_token - commands: - /src/grafana-build artifacts -a targz:grafana:linux/amd64 -a targz:grafana:linux/arm64 - -a targz:grafana:linux/arm/v7 --go-version=1.21.6 --yarn-cache=$$YARN_CACHE_FOLDER + -a targz:grafana:linux/arm/v7 --go-version=1.21.8 --yarn-cache=$$YARN_CACHE_FOLDER --build-id=$$DRONE_BUILD_NUMBER --grafana-dir=$$PWD > packages.txt depends_on: - yarn-install @@ -775,7 +775,7 @@ steps: - /src/grafana-build artifacts -a docker:grafana:linux/amd64 -a docker:grafana:linux/amd64:ubuntu -a docker:grafana:linux/arm64 -a docker:grafana:linux/arm64:ubuntu -a docker:grafana:linux/arm/v7 -a docker:grafana:linux/arm/v7:ubuntu --yarn-cache=$$YARN_CACHE_FOLDER --build-id=$$DRONE_BUILD_NUMBER - --go-version=1.21.6 --ubuntu-base=ubuntu:22.04 --alpine-base=alpine:3.19.1 --tag-format='{{ + --go-version=1.21.8 --ubuntu-base=ubuntu:22.04 --alpine-base=alpine:3.19.1 --tag-format='{{ .version_base }}-{{ .buildID }}-{{ .arch }}' --grafana-dir=$$PWD --ubuntu-tag-format='{{ .version_base }}-{{ .buildID }}-ubuntu-{{ .arch }}' > docker.txt - find ./dist -name '*docker*.tar.gz' -type f | xargs -n1 docker load -i @@ -919,7 +919,7 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: compile-build-cmd - commands: - echo $DRONE_RUNNER_NAME @@ -933,7 +933,7 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -942,14 +942,14 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: [] - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: verify-gen-jsonnet - commands: - apk add --update make - make gen-go depends_on: - verify-gen-cue - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: wire-install - commands: - dockerize -wait tcp://postgres:5432 -timeout 120s @@ -970,7 +970,7 @@ steps: GRAFANA_TEST_DB: postgres PGPASSWORD: grafanatest POSTGRES_HOST: postgres - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: postgres-integration-tests - commands: - dockerize -wait tcp://mysql57:3306 -timeout 120s @@ -991,7 +991,7 @@ steps: environment: GRAFANA_TEST_DB: mysql MYSQL_HOST: mysql57 - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: mysql-5.7-integration-tests - commands: - dockerize -wait tcp://mysql80:3306 -timeout 120s @@ -1012,7 +1012,7 @@ steps: environment: GRAFANA_TEST_DB: mysql MYSQL_HOST: mysql80 - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: mysql-8.0-integration-tests - commands: - dockerize -wait tcp://redis:6379 -timeout 120s @@ -1028,7 +1028,7 @@ steps: - wait-for-redis environment: REDIS_URL: redis://redis:6379/0 - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: redis-integration-tests - commands: - dockerize -wait tcp://memcached:11211 -timeout 120s @@ -1044,7 +1044,7 @@ steps: - wait-for-memcached environment: MEMCACHED_HOSTS: memcached:11211 - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: memcached-integration-tests - commands: - dockerize -wait tcp://mimir_backend:8080 -timeout 120s @@ -1060,7 +1060,7 @@ steps: environment: AM_TENANT_ID: test AM_URL: http://mimir_backend:8080 - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: remote-alertmanager-integration-tests trigger: event: @@ -1149,7 +1149,7 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: verify-gen-cue trigger: event: @@ -1190,7 +1190,7 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: compile-build-cmd - commands: - apt-get update -yq && apt-get install shellcheck @@ -1258,7 +1258,7 @@ steps: environment: GITHUB_TOKEN: from_secret: github_token - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: swagger-gen trigger: event: @@ -1360,7 +1360,7 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: compile-build-cmd - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -1371,7 +1371,7 @@ steps: - CODEGEN_VERIFY=1 make gen-cue depends_on: - clone-enterprise - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -1381,14 +1381,14 @@ steps: - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: - clone-enterprise - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: verify-gen-jsonnet - commands: - apk add --update make - make gen-go depends_on: - verify-gen-cue - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: wire-install - commands: - apk add --update build-base @@ -1396,7 +1396,7 @@ steps: - go test -v -run=^$ -benchmem -timeout=1h -count=8 -bench=. ${GO_PACKAGES} depends_on: - wire-install - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: sqlite-benchmark-integration-tests - commands: - apk add --update build-base @@ -1408,7 +1408,7 @@ steps: GRAFANA_TEST_DB: postgres PGPASSWORD: grafanatest POSTGRES_HOST: postgres - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: postgres-benchmark-integration-tests - commands: - apk add --update build-base @@ -1419,7 +1419,7 @@ steps: environment: GRAFANA_TEST_DB: mysql MYSQL_HOST: mysql57 - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: mysql-5.7-benchmark-integration-tests - commands: - apk add --update build-base @@ -1430,7 +1430,7 @@ steps: environment: GRAFANA_TEST_DB: mysql MYSQL_HOST: mysql80 - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: mysql-8.0-benchmark-integration-tests trigger: event: @@ -1509,7 +1509,7 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: verify-gen-cue trigger: branch: main @@ -1686,7 +1686,7 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -1695,14 +1695,14 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: [] - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: verify-gen-jsonnet - commands: - apk add --update make - make gen-go depends_on: - verify-gen-cue - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: wire-install - commands: - apk add --update build-base shared-mime-info shared-mime-info-lang @@ -1710,7 +1710,7 @@ steps: -timeout=5m depends_on: - wire-install - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: test-backend - commands: - apk add --update build-base @@ -1719,7 +1719,7 @@ steps: | grep -o '\(.*\)/' | sort -u) depends_on: - wire-install - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: test-backend-integration trigger: branch: main @@ -1764,13 +1764,13 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: compile-build-cmd - commands: - apk add --update make - make gen-go depends_on: [] - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: wire-install - commands: - apk add --update make build-base @@ -1779,16 +1779,16 @@ steps: - wire-install environment: CGO_ENABLED: "1" - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: lint-backend - commands: - go run scripts/modowners/modowners.go check go.mod - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: validate-modfile - commands: - apk add --update make - make swagger-validate - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: validate-openapi-spec - commands: - ./bin/build verify-drone @@ -1845,7 +1845,7 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: compile-build-cmd - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -1855,7 +1855,7 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -1864,14 +1864,14 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: [] - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: verify-gen-jsonnet - commands: - apk add --update make - make gen-go depends_on: - verify-gen-cue - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: wire-install - commands: - apk add --update g++ make python3 && ln -sf /usr/bin/python3 /usr/bin/python @@ -1904,7 +1904,7 @@ steps: name: build-frontend-packages - commands: - /src/grafana-build artifacts -a targz:grafana:linux/amd64 -a targz:grafana:linux/arm64 - -a targz:grafana:linux/arm/v7 --go-version=1.21.6 --yarn-cache=$$YARN_CACHE_FOLDER + -a targz:grafana:linux/arm/v7 --go-version=1.21.8 --yarn-cache=$$YARN_CACHE_FOLDER --build-id=$$DRONE_BUILD_NUMBER --grafana-dir=$$PWD > packages.txt depends_on: - update-package-json-version @@ -2144,7 +2144,7 @@ steps: - /src/grafana-build artifacts -a docker:grafana:linux/amd64 -a docker:grafana:linux/amd64:ubuntu -a docker:grafana:linux/arm64 -a docker:grafana:linux/arm64:ubuntu -a docker:grafana:linux/arm/v7 -a docker:grafana:linux/arm/v7:ubuntu --yarn-cache=$$YARN_CACHE_FOLDER --build-id=$$DRONE_BUILD_NUMBER - --go-version=1.21.6 --ubuntu-base=ubuntu:22.04 --alpine-base=alpine:3.19.1 --tag-format='{{ + --go-version=1.21.8 --ubuntu-base=ubuntu:22.04 --alpine-base=alpine:3.19.1 --tag-format='{{ .version_base }}-{{ .buildID }}-{{ .arch }}' --grafana-dir=$$PWD --ubuntu-tag-format='{{ .version_base }}-{{ .buildID }}-ubuntu-{{ .arch }}' > docker.txt - find ./dist -name '*docker*.tar.gz' -type f | xargs -n1 docker load -i @@ -2350,7 +2350,7 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: compile-build-cmd - commands: - echo $DRONE_RUNNER_NAME @@ -2364,7 +2364,7 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -2373,14 +2373,14 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: [] - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: verify-gen-jsonnet - commands: - apk add --update make - make gen-go depends_on: - verify-gen-cue - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: wire-install - commands: - dockerize -wait tcp://postgres:5432 -timeout 120s @@ -2401,7 +2401,7 @@ steps: GRAFANA_TEST_DB: postgres PGPASSWORD: grafanatest POSTGRES_HOST: postgres - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: postgres-integration-tests - commands: - dockerize -wait tcp://mysql57:3306 -timeout 120s @@ -2422,7 +2422,7 @@ steps: environment: GRAFANA_TEST_DB: mysql MYSQL_HOST: mysql57 - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: mysql-5.7-integration-tests - commands: - dockerize -wait tcp://mysql80:3306 -timeout 120s @@ -2443,7 +2443,7 @@ steps: environment: GRAFANA_TEST_DB: mysql MYSQL_HOST: mysql80 - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: mysql-8.0-integration-tests - commands: - dockerize -wait tcp://redis:6379 -timeout 120s @@ -2459,7 +2459,7 @@ steps: - wait-for-redis environment: REDIS_URL: redis://redis:6379/0 - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: redis-integration-tests - commands: - dockerize -wait tcp://memcached:11211 -timeout 120s @@ -2475,7 +2475,7 @@ steps: - wait-for-memcached environment: MEMCACHED_HOSTS: memcached:11211 - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: memcached-integration-tests - commands: - dockerize -wait tcp://mimir_backend:8080 -timeout 120s @@ -2491,7 +2491,7 @@ steps: environment: AM_TENANT_ID: test AM_URL: http://mimir_backend:8080 - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: remote-alertmanager-integration-tests trigger: branch: main @@ -2684,7 +2684,7 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: compile-build-cmd - commands: - ./bin/build artifacts docker fetch --edition oss @@ -2781,7 +2781,7 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: compile-build-cmd - commands: - ./bin/build artifacts packages --tag $${DRONE_TAG} --src-bucket $${PRERELEASE_BUCKET} @@ -2851,7 +2851,7 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: compile-build-cmd - commands: - apk add --update g++ make python3 && ln -sf /usr/bin/python3 /usr/bin/python @@ -2918,7 +2918,7 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: compile-build-cmd - depends_on: - compile-build-cmd @@ -3025,7 +3025,7 @@ steps: from_secret: gcp_key_base64 GITHUB_TOKEN: from_secret: github_token - GO_VERSION: 1.21.6 + GO_VERSION: 1.21.8 GPG_PASSPHRASE: from_secret: packages_gpg_passphrase GPG_PRIVATE_KEY: @@ -3083,13 +3083,13 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: compile-build-cmd - commands: - ./bin/build whatsnew-checker depends_on: - compile-build-cmd - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: whats-new-checker trigger: event: @@ -3192,7 +3192,7 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -3201,14 +3201,14 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: [] - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: verify-gen-jsonnet - commands: - apk add --update make - make gen-go depends_on: - verify-gen-cue - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: wire-install - commands: - apk add --update build-base shared-mime-info shared-mime-info-lang @@ -3216,7 +3216,7 @@ steps: -timeout=5m depends_on: - wire-install - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: test-backend - commands: - apk add --update build-base @@ -3225,7 +3225,7 @@ steps: | grep -o '\(.*\)/' | sort -u) depends_on: - wire-install - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: test-backend-integration trigger: event: @@ -3282,7 +3282,7 @@ steps: from_secret: gcp_key_base64 GITHUB_TOKEN: from_secret: github_token - GO_VERSION: 1.21.6 + GO_VERSION: 1.21.8 GPG_PASSPHRASE: from_secret: packages_gpg_passphrase GPG_PRIVATE_KEY: @@ -3465,7 +3465,7 @@ steps: from_secret: gcp_key_base64 GITHUB_TOKEN: from_secret: github_token - GO_VERSION: 1.21.6 + GO_VERSION: 1.21.8 GPG_PASSPHRASE: from_secret: packages_gpg_passphrase GPG_PRIVATE_KEY: @@ -3615,7 +3615,7 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -3624,14 +3624,14 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: [] - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: verify-gen-jsonnet - commands: - apk add --update make - make gen-go depends_on: - verify-gen-cue - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: wire-install - commands: - apk add --update build-base shared-mime-info shared-mime-info-lang @@ -3639,7 +3639,7 @@ steps: -timeout=5m depends_on: - wire-install - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: test-backend - commands: - apk add --update build-base @@ -3648,7 +3648,7 @@ steps: | grep -o '\(.*\)/' | sort -u) depends_on: - wire-install - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: test-backend-integration trigger: cron: @@ -3703,7 +3703,7 @@ steps: from_secret: gcp_key_base64 GITHUB_TOKEN: from_secret: github_token - GO_VERSION: 1.21.6 + GO_VERSION: 1.21.8 GPG_PASSPHRASE: from_secret: packages_gpg_passphrase GPG_PRIVATE_KEY: @@ -3850,7 +3850,7 @@ steps: from_secret: gcp_key_base64 GITHUB_TOKEN: from_secret: github_token - GO_VERSION: 1.21.6 + GO_VERSION: 1.21.8 GPG_PASSPHRASE: from_secret: packages_gpg_passphrase GPG_PRIVATE_KEY: @@ -3959,7 +3959,7 @@ steps: from_secret: gcp_key_base64 GITHUB_TOKEN: from_secret: github_token - GO_VERSION: 1.21.6 + GO_VERSION: 1.21.8 GPG_PASSPHRASE: from_secret: packages_gpg_passphrase GPG_PRIVATE_KEY: @@ -4049,20 +4049,20 @@ steps: - commands: [] depends_on: - clone - image: golang:1.21.6-windowsservercore-1809 + image: golang:1.21.8-windowsservercore-1809 name: windows-init - commands: - go install github.com/google/wire/cmd/wire@v0.5.0 - wire gen -tags oss ./pkg/server depends_on: - windows-init - image: golang:1.21.6-windowsservercore-1809 + image: golang:1.21.8-windowsservercore-1809 name: wire-install - commands: - go test -tags requires_buildifer -short -covermode=atomic -timeout=5m ./pkg/... depends_on: - wire-install - image: golang:1.21.6-windowsservercore-1809 + image: golang:1.21.8-windowsservercore-1809 name: test-backend trigger: event: @@ -4155,7 +4155,7 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -4164,14 +4164,14 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: [] - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: verify-gen-jsonnet - commands: - apk add --update make - make gen-go depends_on: - verify-gen-cue - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: wire-install - commands: - dockerize -wait tcp://postgres:5432 -timeout 120s @@ -4192,7 +4192,7 @@ steps: GRAFANA_TEST_DB: postgres PGPASSWORD: grafanatest POSTGRES_HOST: postgres - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: postgres-integration-tests - commands: - dockerize -wait tcp://mysql57:3306 -timeout 120s @@ -4213,7 +4213,7 @@ steps: environment: GRAFANA_TEST_DB: mysql MYSQL_HOST: mysql57 - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: mysql-5.7-integration-tests - commands: - dockerize -wait tcp://mysql80:3306 -timeout 120s @@ -4234,7 +4234,7 @@ steps: environment: GRAFANA_TEST_DB: mysql MYSQL_HOST: mysql80 - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: mysql-8.0-integration-tests - commands: - dockerize -wait tcp://redis:6379 -timeout 120s @@ -4250,7 +4250,7 @@ steps: - wait-for-redis environment: REDIS_URL: redis://redis:6379/0 - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: redis-integration-tests - commands: - dockerize -wait tcp://memcached:11211 -timeout 120s @@ -4266,7 +4266,7 @@ steps: - wait-for-memcached environment: MEMCACHED_HOSTS: memcached:11211 - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: memcached-integration-tests - commands: - dockerize -wait tcp://mimir_backend:8080 -timeout 120s @@ -4282,7 +4282,7 @@ steps: environment: AM_TENANT_ID: test AM_URL: http://mimir_backend:8080 - image: golang:1.21.6-alpine + image: golang:1.21.8-alpine name: remote-alertmanager-integration-tests trigger: event: @@ -4636,7 +4636,7 @@ steps: path: /root/.docker/ - commands: - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM alpine/git:2.40.1 - - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM golang:1.21.6-alpine + - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM golang:1.21.8-alpine - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM node:20.9.0-alpine - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM google/cloud-sdk:431.0.0 - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM grafana/grafana-ci-deploy:1.3.3 @@ -4671,7 +4671,7 @@ steps: path: /root/.docker/ - commands: - trivy --exit-code 1 --severity HIGH,CRITICAL alpine/git:2.40.1 - - trivy --exit-code 1 --severity HIGH,CRITICAL golang:1.21.6-alpine + - trivy --exit-code 1 --severity HIGH,CRITICAL golang:1.21.8-alpine - trivy --exit-code 1 --severity HIGH,CRITICAL node:20.9.0-alpine - trivy --exit-code 1 --severity HIGH,CRITICAL google/cloud-sdk:431.0.0 - trivy --exit-code 1 --severity HIGH,CRITICAL grafana/grafana-ci-deploy:1.3.3 @@ -4926,6 +4926,6 @@ kind: secret name: gcr_credentials --- kind: signature -hmac: 38be1b6248aae943e74ffd05c822379dc6ce1d1f08f681316d26be7740470a37 +hmac: 32b6ed2f5819225842aa94379423bcf4354dde154e91af3e293bc919594c10b9 ... diff --git a/.github/workflows/alerting-swagger-gen.yml b/.github/workflows/alerting-swagger-gen.yml index a9abc4f578..5e7b7baa99 100644 --- a/.github/workflows/alerting-swagger-gen.yml +++ b/.github/workflows/alerting-swagger-gen.yml @@ -16,7 +16,7 @@ jobs: - name: Set go version uses: actions/setup-go@v4 with: - go-version: '1.21.6' + go-version: '1.21.8' - name: Build swagger run: | make -C pkg/services/ngalert/api/tooling post.json api.json diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 1bbd4387cc..eb71b7f8f5 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -47,7 +47,7 @@ jobs: name: Set go version uses: actions/setup-go@v4 with: - go-version: '1.21.6' + go-version: '1.21.8' # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/pr-codeql-analysis-go.yml b/.github/workflows/pr-codeql-analysis-go.yml index 59dd298c91..6aa56cf57f 100644 --- a/.github/workflows/pr-codeql-analysis-go.yml +++ b/.github/workflows/pr-codeql-analysis-go.yml @@ -35,7 +35,7 @@ jobs: - name: Set go version uses: actions/setup-go@v4 with: - go-version: '1.21.6' + go-version: '1.21.8' # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/publish-kinds-next.yml b/.github/workflows/publish-kinds-next.yml index 618411fd08..7dc3cedb74 100644 --- a/.github/workflows/publish-kinds-next.yml +++ b/.github/workflows/publish-kinds-next.yml @@ -36,7 +36,7 @@ jobs: - name: "Setup Go" uses: "actions/setup-go@v4" with: - go-version: '1.21.6' + go-version: '1.21.8' - name: "Verify kinds" run: go run .github/workflows/scripts/kinds/verify-kinds.go diff --git a/.github/workflows/publish-kinds-release.yml b/.github/workflows/publish-kinds-release.yml index 0a2a013737..acdfcbcb0b 100644 --- a/.github/workflows/publish-kinds-release.yml +++ b/.github/workflows/publish-kinds-release.yml @@ -39,7 +39,7 @@ jobs: - name: "Setup Go" uses: "actions/setup-go@v4" with: - go-version: '1.21.6' + go-version: '1.21.8' - name: "Verify kinds" run: go run .github/workflows/scripts/kinds/verify-kinds.go diff --git a/.github/workflows/verify-kinds.yml b/.github/workflows/verify-kinds.yml index 9107bed450..007abd0b9a 100644 --- a/.github/workflows/verify-kinds.yml +++ b/.github/workflows/verify-kinds.yml @@ -18,7 +18,7 @@ jobs: - name: "Setup Go" uses: "actions/setup-go@v4" with: - go-version: '1.21.6' + go-version: '1.21.8' - name: "Verify kinds" run: go run .github/workflows/scripts/kinds/verify-kinds.go diff --git a/Dockerfile b/Dockerfile index e44dbb4826..03dbfd4355 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ ARG BASE_IMAGE=alpine:3.18.5 ARG JS_IMAGE=node:20-alpine3.18 ARG JS_PLATFORM=linux/amd64 -ARG GO_IMAGE=golang:1.21.6-alpine3.18 +ARG GO_IMAGE=golang:1.21.8-alpine3.18 ARG GO_SRC=go-builder ARG JS_SRC=js-builder diff --git a/Makefile b/Makefile index 24ad02da4d..b39cf4b562 100644 --- a/Makefile +++ b/Makefile @@ -256,7 +256,7 @@ build-docker-full-ubuntu: ## Build Docker image based on Ubuntu for development. --build-arg COMMIT_SHA=$$(git rev-parse HEAD) \ --build-arg BUILD_BRANCH=$$(git rev-parse --abbrev-ref HEAD) \ --build-arg BASE_IMAGE=ubuntu:22.04 \ - --build-arg GO_IMAGE=golang:1.21.6 \ + --build-arg GO_IMAGE=golang:1.21.8 \ --tag grafana/grafana$(TAG_SUFFIX):dev-ubuntu \ $(DOCKER_BUILD_ARGS) diff --git a/scripts/drone/variables.star b/scripts/drone/variables.star index bc118eedaa..d2965ed0d9 100644 --- a/scripts/drone/variables.star +++ b/scripts/drone/variables.star @@ -3,7 +3,7 @@ global variables """ grabpl_version = "v3.0.50" -golang_version = "1.21.6" +golang_version = "1.21.8" # nodejs_version should match what's in ".nvmrc", but without the v prefix. nodejs_version = "20.9.0" From 7e4badff1d76b18771464d1c7edf7a5d34519734 Mon Sep 17 00:00:00 2001 From: Dan Cech Date: Tue, 5 Mar 2024 16:31:39 -0500 Subject: [PATCH 0408/1406] Storage: Use our own key format and support unnamespaced objects (#83929) * use our own key format and support unnamespaced objects * fix tests --- .../apiserver/storage/entity/storage.go | 269 +++++++++--------- .../apiserver/storage/entity/utils.go | 23 +- .../apiserver/storage/entity/utils_test.go | 29 +- pkg/services/store/entity/key.go | 45 ++- .../entity/sqlstash/sql_storage_server.go | 18 +- .../sqlstash/sql_storage_server_test.go | 4 +- 6 files changed, 217 insertions(+), 171 deletions(-) diff --git a/pkg/services/apiserver/storage/entity/storage.go b/pkg/services/apiserver/storage/entity/storage.go index 3fb5cab77f..fc5536b9a5 100644 --- a/pkg/services/apiserver/storage/entity/storage.go +++ b/pkg/services/apiserver/storage/entity/storage.go @@ -30,13 +30,10 @@ import ( "k8s.io/apiserver/pkg/storage/storagebackend/factory" entityStore "github.com/grafana/grafana/pkg/services/store/entity" - "github.com/grafana/grafana/pkg/util" ) var _ storage.Interface = (*Storage)(nil) -const MaxUpdateAttempts = 1 - // Storage implements storage.Interface and storage resources as JSON files on disk. type Storage struct { config *storagebackend.ConfigForResource @@ -88,25 +85,7 @@ func (s *Storage) Create(ctx context.Context, key string, obj runtime.Object, ou return err } - metaAccessor, err := meta.Accessor(obj) - if err != nil { - return err - } - - // Replace the default name generation strategy - if metaAccessor.GetGenerateName() != "" { - k, err := entityStore.ParseKey(key) - if err != nil { - return err - } - k.Name = util.GenerateShortUID() - key = k.String() - - metaAccessor.SetName(k.Name) - metaAccessor.SetGenerateName("") - } - - e, err := resourceToEntity(key, obj, requestInfo, s.codec) + e, err := resourceToEntity(obj, requestInfo, s.codec) if err != nil { return err } @@ -128,13 +107,6 @@ func (s *Storage) Create(ctx context.Context, key string, obj runtime.Object, ou return apierrors.NewInternalError(err) } - /* - s.watchSet.notifyWatchers(watch.Event{ - Object: out.DeepCopyObject(), - Type: watch.Added, - }) - */ - return nil } @@ -144,13 +116,26 @@ func (s *Storage) Create(ctx context.Context, key string, obj runtime.Object, ou // current version of the object to avoid read operation from storage to get it. // However, the implementations have to retry in case suggestion is stale. func (s *Storage) Delete(ctx context.Context, key string, out runtime.Object, preconditions *storage.Preconditions, validateDeletion storage.ValidateObjectFunc, cachedExistingObject runtime.Object) error { + requestInfo, ok := request.RequestInfoFrom(ctx) + if !ok { + return apierrors.NewInternalError(fmt.Errorf("could not get request info")) + } + + k := &entityStore.Key{ + Group: requestInfo.APIGroup, + Resource: requestInfo.Resource, + Namespace: requestInfo.Namespace, + Name: requestInfo.Name, + Subresource: requestInfo.Subresource, + } + previousVersion := int64(0) if preconditions != nil && preconditions.ResourceVersion != nil { previousVersion, _ = strconv.ParseInt(*preconditions.ResourceVersion, 10, 64) } rsp, err := s.store.Delete(ctx, &entityStore.DeleteEntityRequest{ - Key: key, + Key: k.String(), PreviousVersion: previousVersion, }) if err != nil { @@ -165,72 +150,6 @@ func (s *Storage) Delete(ctx context.Context, key string, out runtime.Object, pr return nil } -type Decoder struct { - client entityStore.EntityStore_WatchClient - newFunc func() runtime.Object - opts storage.ListOptions - codec runtime.Codec -} - -func (d *Decoder) Decode() (action watch.EventType, object runtime.Object, err error) { - for { - resp, err := d.client.Recv() - if errors.Is(err, io.EOF) { - log.Printf("watch is done") - return watch.Error, nil, err - } - - if grpcStatus.Code(err) == grpcCodes.Canceled { - log.Printf("watch was canceled") - return watch.Error, nil, err - } - - if err != nil { - log.Printf("error receiving result: %s", err) - return watch.Error, nil, err - } - - obj := d.newFunc() - - err = entityToResource(resp.Entity, obj, d.codec) - if err != nil { - log.Printf("error decoding entity: %s", err) - return watch.Error, nil, err - } - - // apply any predicates not handled in storage - var matches bool - matches, err = d.opts.Predicate.Matches(obj) - if err != nil { - log.Printf("error matching object: %s", err) - return watch.Error, nil, err - } - if !matches { - continue - } - - var watchAction watch.EventType - switch resp.Entity.Action { - case entityStore.Entity_CREATED: - watchAction = watch.Added - case entityStore.Entity_UPDATED: - watchAction = watch.Modified - case entityStore.Entity_DELETED: - watchAction = watch.Deleted - default: - watchAction = watch.Error - } - - return watchAction, obj, nil - } -} - -func (d *Decoder) Close() { - _ = d.client.CloseSend() -} - -var _ watch.Decoder = (*Decoder)(nil) - // Watch begins watching the specified key. Events are decoded into API objects, // and any items selected by 'p' are sent down to returned watch.Interface. // resourceVersion may be used to specify what version to begin watching, @@ -239,8 +158,23 @@ var _ watch.Decoder = (*Decoder)(nil) // If resource version is "0", this interface will get current object at given key // and send it in an "ADDED" event, before watch starts. func (s *Storage) Watch(ctx context.Context, key string, opts storage.ListOptions) (watch.Interface, error) { + requestInfo, ok := request.RequestInfoFrom(ctx) + if !ok { + return nil, apierrors.NewInternalError(fmt.Errorf("could not get request info")) + } + + k := &entityStore.Key{ + Group: requestInfo.APIGroup, + Resource: requestInfo.Resource, + Namespace: requestInfo.Namespace, + Name: requestInfo.Name, + Subresource: requestInfo.Subresource, + } + req := &entityStore.EntityWatchRequest{ - Key: []string{key}, + Key: []string{ + k.String(), + }, WithBody: true, } @@ -278,6 +212,19 @@ func (s *Storage) Watch(ctx context.Context, key string, opts storage.ListOption // The returned contents may be delayed, but it is guaranteed that they will // match 'opts.ResourceVersion' according 'opts.ResourceVersionMatch'. func (s *Storage) Get(ctx context.Context, key string, opts storage.GetOptions, objPtr runtime.Object) error { + requestInfo, ok := request.RequestInfoFrom(ctx) + if !ok { + return apierrors.NewInternalError(fmt.Errorf("could not get request info")) + } + + k := &entityStore.Key{ + Group: requestInfo.APIGroup, + Resource: requestInfo.Resource, + Namespace: requestInfo.Namespace, + Name: requestInfo.Name, + Subresource: requestInfo.Subresource, + } + resourceVersion := int64(0) var err error if opts.ResourceVersion != "" { @@ -288,7 +235,7 @@ func (s *Storage) Get(ctx context.Context, key string, opts storage.GetOptions, } rsp, err := s.store.Read(ctx, &entityStore.ReadEntityRequest{ - Key: key, + Key: k.String(), WithBody: true, WithStatus: true, ResourceVersion: resourceVersion, @@ -302,7 +249,7 @@ func (s *Storage) Get(ctx context.Context, key string, opts storage.GetOptions, return nil } - return apierrors.NewNotFound(s.gr, key) + return apierrors.NewNotFound(s.gr, k.Name) } err = entityToResource(rsp, objPtr, s.codec) @@ -320,6 +267,19 @@ func (s *Storage) Get(ctx context.Context, key string, opts storage.GetOptions, // The returned contents may be delayed, but it is guaranteed that they will // match 'opts.ResourceVersion' according 'opts.ResourceVersionMatch'. func (s *Storage) GetList(ctx context.Context, key string, opts storage.ListOptions, listObj runtime.Object) error { + requestInfo, ok := request.RequestInfoFrom(ctx) + if !ok { + return apierrors.NewInternalError(fmt.Errorf("could not get request info")) + } + + k := &entityStore.Key{ + Group: requestInfo.APIGroup, + Resource: requestInfo.Resource, + Namespace: requestInfo.Namespace, + Name: requestInfo.Name, + Subresource: requestInfo.Subresource, + } + listPtr, err := meta.GetItemsPtr(listObj) if err != nil { return err @@ -330,7 +290,9 @@ func (s *Storage) GetList(ctx context.Context, key string, opts storage.ListOpti } req := &entityStore.EntityListRequest{ - Key: []string{key}, + Key: []string{ + k.String(), + }, WithBody: true, WithStatus: true, NextPageToken: opts.Predicate.Continue, @@ -425,33 +387,21 @@ func (s *Storage) GuaranteedUpdate( preconditions *storage.Preconditions, tryUpdate storage.UpdateFunc, cachedExistingObject runtime.Object, -) error { - var err error - for attempt := 1; attempt <= MaxUpdateAttempts; attempt = attempt + 1 { - err = s.guaranteedUpdate(ctx, key, destination, ignoreNotFound, preconditions, tryUpdate, cachedExistingObject) - if err == nil { - return nil - } - } - - return err -} - -func (s *Storage) guaranteedUpdate( - ctx context.Context, - key string, - destination runtime.Object, - ignoreNotFound bool, - preconditions *storage.Preconditions, - tryUpdate storage.UpdateFunc, - cachedExistingObject runtime.Object, ) error { requestInfo, ok := request.RequestInfoFrom(ctx) if !ok { return apierrors.NewInternalError(fmt.Errorf("could not get request info")) } - err := s.Get(ctx, key, storage.GetOptions{}, destination) + k := &entityStore.Key{ + Group: requestInfo.APIGroup, + Resource: requestInfo.Resource, + Namespace: requestInfo.Namespace, + Name: requestInfo.Name, + Subresource: requestInfo.Subresource, + } + + err := s.Get(ctx, k.String(), storage.GetOptions{}, destination) if err != nil { return err } @@ -476,10 +426,10 @@ func (s *Storage) guaranteedUpdate( } } - return apierrors.NewInternalError(fmt.Errorf("could not successfully update object. key=%s, err=%s", key, err.Error())) + return apierrors.NewInternalError(fmt.Errorf("could not successfully update object. key=%s, err=%s", k.String(), err.Error())) } - e, err := resourceToEntity(key, updatedObj, requestInfo, s.codec) + e, err := resourceToEntity(updatedObj, requestInfo, s.codec) if err != nil { return err } @@ -503,13 +453,6 @@ func (s *Storage) guaranteedUpdate( return apierrors.NewInternalError(err) } - /* - s.watchSet.notifyWatchers(watch.Event{ - Object: destination.DeepCopyObject(), - Type: watch.Modified, - }) - */ - return nil } @@ -525,3 +468,69 @@ func (s *Storage) Versioner() storage.Versioner { func (s *Storage) RequestWatchProgress(ctx context.Context) error { return nil } + +type Decoder struct { + client entityStore.EntityStore_WatchClient + newFunc func() runtime.Object + opts storage.ListOptions + codec runtime.Codec +} + +func (d *Decoder) Decode() (action watch.EventType, object runtime.Object, err error) { + for { + resp, err := d.client.Recv() + if errors.Is(err, io.EOF) { + log.Printf("watch is done") + return watch.Error, nil, err + } + + if grpcStatus.Code(err) == grpcCodes.Canceled { + log.Printf("watch was canceled") + return watch.Error, nil, err + } + + if err != nil { + log.Printf("error receiving result: %s", err) + return watch.Error, nil, err + } + + obj := d.newFunc() + + err = entityToResource(resp.Entity, obj, d.codec) + if err != nil { + log.Printf("error decoding entity: %s", err) + return watch.Error, nil, err + } + + // apply any predicates not handled in storage + var matches bool + matches, err = d.opts.Predicate.Matches(obj) + if err != nil { + log.Printf("error matching object: %s", err) + return watch.Error, nil, err + } + if !matches { + continue + } + + var watchAction watch.EventType + switch resp.Entity.Action { + case entityStore.Entity_CREATED: + watchAction = watch.Added + case entityStore.Entity_UPDATED: + watchAction = watch.Modified + case entityStore.Entity_DELETED: + watchAction = watch.Deleted + default: + watchAction = watch.Error + } + + return watchAction, obj, nil + } +} + +func (d *Decoder) Close() { + _ = d.client.CloseSend() +} + +var _ watch.Decoder = (*Decoder)(nil) diff --git a/pkg/services/apiserver/storage/entity/utils.go b/pkg/services/apiserver/storage/entity/utils.go index 22b122e43e..03588662c7 100644 --- a/pkg/services/apiserver/storage/entity/utils.go +++ b/pkg/services/apiserver/storage/entity/utils.go @@ -19,7 +19,6 @@ import ( entityStore "github.com/grafana/grafana/pkg/services/store/entity" ) -// this is terrible... but just making it work!!!! func entityToResource(rsp *entityStore.Entity, res runtime.Object, codec runtime.Codec) error { var err error @@ -99,7 +98,7 @@ func entityToResource(rsp *entityStore.Entity, res runtime.Object, codec runtime return nil } -func resourceToEntity(key string, res runtime.Object, requestInfo *request.RequestInfo, codec runtime.Codec) (*entityStore.Entity, error) { +func resourceToEntity(res runtime.Object, requestInfo *request.RequestInfo, codec runtime.Codec) (*entityStore.Entity, error) { metaAccessor, err := meta.Accessor(res) if err != nil { return nil, err @@ -111,14 +110,22 @@ func resourceToEntity(key string, res runtime.Object, requestInfo *request.Reque } rv, _ := strconv.ParseInt(metaAccessor.GetResourceVersion(), 10, 64) + k := &entityStore.Key{ + Group: requestInfo.APIGroup, + Resource: requestInfo.Resource, + Namespace: requestInfo.Namespace, + Name: metaAccessor.GetName(), + Subresource: requestInfo.Subresource, + } + rsp := &entityStore.Entity{ - Group: requestInfo.APIGroup, + Group: k.Group, GroupVersion: requestInfo.APIVersion, - Resource: requestInfo.Resource, - Subresource: requestInfo.Subresource, - Namespace: metaAccessor.GetNamespace(), - Key: key, - Name: metaAccessor.GetName(), + Resource: k.Resource, + Subresource: k.Subresource, + Namespace: k.Namespace, + Key: k.String(), + Name: k.Name, Guid: string(metaAccessor.GetUID()), ResourceVersion: rv, Folder: grafanaAccessor.GetFolder(), diff --git a/pkg/services/apiserver/storage/entity/utils_test.go b/pkg/services/apiserver/storage/entity/utils_test.go index bd2dbd84f7..1a731f9698 100644 --- a/pkg/services/apiserver/storage/entity/utils_test.go +++ b/pkg/services/apiserver/storage/entity/utils_test.go @@ -24,22 +24,18 @@ func TestResourceToEntity(t *testing.T) { updatedAt := createdAt.Add(time.Hour).Truncate(time.Second) updatedAtStr := updatedAt.UTC().Format(time.RFC3339) - apiVersion := "v0alpha1" - requestInfo := &request.RequestInfo{ - APIVersion: apiVersion, - } - Scheme := runtime.NewScheme() Scheme.AddKnownTypes(v0alpha1.PlaylistResourceInfo.GroupVersion(), &v0alpha1.Playlist{}) Codecs := serializer.NewCodecFactory(Scheme) testCases := []struct { - key string + requestInfo *request.RequestInfo resource runtime.Object codec runtime.Codec expectedKey string expectedGroupVersion string expectedName string + expectedNamespace string expectedTitle string expectedGuid string expectedVersion string @@ -55,11 +51,14 @@ func TestResourceToEntity(t *testing.T) { expectedBody []byte }{ { - key: "/playlist.grafana.app/playlists/default/test-uid", + requestInfo: &request.RequestInfo{ + APIGroup: "playlist.grafana.app", + APIVersion: "v0alpha1", + Resource: "playlists", + Namespace: "default", + Name: "test-name", + }, resource: &v0alpha1.Playlist{ - TypeMeta: metav1.TypeMeta{ - APIVersion: apiVersion, - }, ObjectMeta: metav1.ObjectMeta{ CreationTimestamp: createdAt, Labels: map[string]string{"label1": "value1", "label2": "value2"}, @@ -83,9 +82,10 @@ func TestResourceToEntity(t *testing.T) { }, }, }, - expectedKey: "/playlist.grafana.app/playlists/default/test-uid", - expectedGroupVersion: apiVersion, + expectedKey: "/playlist.grafana.app/playlists/namespaces/default/test-name", + expectedGroupVersion: "v0alpha1", expectedName: "test-name", + expectedNamespace: "default", expectedTitle: "A playlist", expectedGuid: "test-uid", expectedVersion: "1", @@ -104,10 +104,11 @@ func TestResourceToEntity(t *testing.T) { for _, tc := range testCases { t.Run(tc.resource.GetObjectKind().GroupVersionKind().Kind+" to entity conversion should succeed", func(t *testing.T) { - entity, err := resourceToEntity(tc.key, tc.resource, requestInfo, Codecs.LegacyCodec(v0alpha1.PlaylistResourceInfo.GroupVersion())) + entity, err := resourceToEntity(tc.resource, tc.requestInfo, Codecs.LegacyCodec(v0alpha1.PlaylistResourceInfo.GroupVersion())) require.NoError(t, err) assert.Equal(t, tc.expectedKey, entity.Key) assert.Equal(t, tc.expectedName, entity.Name) + assert.Equal(t, tc.expectedNamespace, entity.Namespace) assert.Equal(t, tc.expectedTitle, entity.Title) assert.Equal(t, tc.expectedGroupVersion, entity.GroupVersion) assert.Equal(t, tc.expectedName, entity.Name) @@ -152,7 +153,7 @@ func TestEntityToResource(t *testing.T) { }{ { entity: &entityStore.Entity{ - Key: "/playlist.grafana.app/playlists/default/test-uid", + Key: "/playlist.grafana.app/playlists/namespaces/default/test-uid", GroupVersion: "v0alpha1", Name: "test-uid", Title: "A playlist", diff --git a/pkg/services/store/entity/key.go b/pkg/services/store/entity/key.go index 2820c846d8..bcd83dd0a7 100644 --- a/pkg/services/store/entity/key.go +++ b/pkg/services/store/entity/key.go @@ -14,10 +14,10 @@ type Key struct { } func ParseKey(key string) (*Key, error) { - // ///(/(/)) - parts := strings.SplitN(key, "/", 6) - if len(parts) < 4 { - return nil, fmt.Errorf("invalid key (expecting at least 3 parts): %s", key) + // //[/namespaces/][/[/]] + parts := strings.Split(key, "/") + if len(parts) < 3 { + return nil, fmt.Errorf("invalid key (expecting at least 2 parts): %s", key) } if parts[0] != "" { @@ -25,24 +25,45 @@ func ParseKey(key string) (*Key, error) { } k := &Key{ - Group: parts[1], - Resource: parts[2], - Namespace: parts[3], + Group: parts[1], + Resource: parts[2], } - if len(parts) > 4 { - k.Name = parts[4] + if len(parts) == 3 { + return k, nil } - if len(parts) > 5 { - k.Subresource = parts[5] + if parts[3] != "namespaces" { + k.Name = parts[3] + if len(parts) > 4 { + k.Subresource = strings.Join(parts[4:], "/") + } + return k, nil + } + + if len(parts) < 5 { + return nil, fmt.Errorf("invalid key (expecting namespace after 'namespaces'): %s", key) + } + + k.Namespace = parts[4] + + if len(parts) == 5 { + return k, nil + } + + k.Name = parts[5] + if len(parts) > 6 { + k.Subresource = strings.Join(parts[6:], "/") } return k, nil } func (k *Key) String() string { - s := "/" + k.Group + "/" + k.Resource + "/" + k.Namespace + s := "/" + k.Group + "/" + k.Resource + if len(k.Namespace) > 0 { + s += "/namespaces/" + k.Namespace + } if len(k.Name) > 0 { s += "/" + k.Name if len(k.Subresource) > 0 { diff --git a/pkg/services/store/entity/sqlstash/sql_storage_server.go b/pkg/services/store/entity/sqlstash/sql_storage_server.go index eb88dba005..51b8887b41 100644 --- a/pkg/services/store/entity/sqlstash/sql_storage_server.go +++ b/pkg/services/store/entity/sqlstash/sql_storage_server.go @@ -1099,8 +1099,12 @@ func (s *sqlEntityServer) List(ctx context.Context, r *entity.EntityListRequest) return nil, err } - args = append(args, key.Namespace, key.Group, key.Resource) - whereclause := "(" + s.dialect.Quote("namespace") + "=? AND " + s.dialect.Quote("group") + "=? AND " + s.dialect.Quote("resource") + "=?" + args = append(args, key.Group, key.Resource) + whereclause := "(" + s.dialect.Quote("group") + "=? AND " + s.dialect.Quote("resource") + "=?" + if key.Namespace != "" { + args = append(args, key.Namespace) + whereclause += " AND " + s.dialect.Quote("namespace") + "=?" + } if key.Name != "" { args = append(args, key.Name) whereclause += " AND " + s.dialect.Quote("name") + "=?" @@ -1257,8 +1261,12 @@ func (s *sqlEntityServer) watchInit(ctx context.Context, r *entity.EntityWatchRe return err } - args = append(args, key.Namespace, key.Group, key.Resource) - whereclause := "(" + s.dialect.Quote("namespace") + "=? AND " + s.dialect.Quote("group") + "=? AND " + s.dialect.Quote("resource") + "=?" + args = append(args, key.Group, key.Resource) + whereclause := "(" + s.dialect.Quote("group") + "=? AND " + s.dialect.Quote("resource") + "=?" + if key.Namespace != "" { + args = append(args, key.Namespace) + whereclause += " AND " + s.dialect.Quote("namespace") + "=?" + } if key.Name != "" { args = append(args, key.Name) whereclause += " AND " + s.dialect.Quote("name") + "=?" @@ -1465,7 +1473,7 @@ func watchMatches(r *entity.EntityWatchRequest, result *entity.Entity) bool { return false } - if key.Namespace == result.Namespace && key.Group == result.Group && key.Resource == result.Resource && (key.Name == "" || key.Name == result.Name) { + if key.Group == result.Group && key.Resource == result.Resource && (key.Namespace == "" || key.Namespace == result.Namespace) && (key.Name == "" || key.Name == result.Name) { matched = true break } diff --git a/pkg/services/store/entity/sqlstash/sql_storage_server_test.go b/pkg/services/store/entity/sqlstash/sql_storage_server_test.go index 17ca970122..2f193b78bd 100644 --- a/pkg/services/store/entity/sqlstash/sql_storage_server_test.go +++ b/pkg/services/store/entity/sqlstash/sql_storage_server_test.go @@ -34,7 +34,7 @@ func TestCreate(t *testing.T) { Resource: "playlists", Namespace: "default", Name: "set-minimum-uid", - Key: "/playlist.grafana.app/playlists/default/set-minimum-uid", + Key: "/playlist.grafana.app/playlists/namespaces/default/set-minimum-uid", CreatedBy: "set-minimum-creator", Origin: &entity.EntityOriginInfo{}, }, @@ -44,7 +44,7 @@ func TestCreate(t *testing.T) { { "request with no entity creator", &entity.Entity{ - Key: "/playlist.grafana.app/playlists/default/set-only-key", + Key: "/playlist.grafana.app/playlists/namespaces/default/set-only-key", }, true, false, From 38a0eab137b7b1a38bc545a9732f03684dc92fd2 Mon Sep 17 00:00:00 2001 From: Nathan Marrs Date: Tue, 5 Mar 2024 16:23:10 -0700 Subject: [PATCH 0409/1406] Canvas: Fix datalink positioning glitch (#83869) --- public/app/features/canvas/runtime/element.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/public/app/features/canvas/runtime/element.tsx b/public/app/features/canvas/runtime/element.tsx index fea795bb61..f78e7049d3 100644 --- a/public/app/features/canvas/runtime/element.tsx +++ b/public/app/features/canvas/runtime/element.tsx @@ -486,6 +486,7 @@ export class ElementState implements LayerElement { }; onElementClick = (event: React.MouseEvent) => { + this.handleTooltip(event); this.onTooltipCallback(); }; From 5c27d28ba43e87ec12b8538be229fc574d67324a Mon Sep 17 00:00:00 2001 From: Nathan Marrs Date: Tue, 5 Mar 2024 16:25:12 -0700 Subject: [PATCH 0410/1406] Canvas: Add datalink support to rectangle and ellipse elements (#83870) --- .../panels-visualizations/visualizations/canvas/index.md | 2 +- public/app/features/canvas/elements/ellipse.tsx | 3 +++ public/app/features/canvas/elements/rectangle.tsx | 3 +++ 3 files 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 162639163a..106b7d03d3 100644 --- a/docs/sources/panels-visualizations/visualizations/canvas/index.md +++ b/docs/sources/panels-visualizations/visualizations/canvas/index.md @@ -127,7 +127,7 @@ The inline editing toggle lets you lock or unlock the canvas. When turned off th ### Data links -Canvases support [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 for all elements that use the field name by following these steps: +Canvases support [data links](https://grafana.com/docs/grafana/latest/panels-visualizations/configure-data-links/), but only for metric-value, text, rectangle, and ellipse elements. You can add a data link by following these steps: 1. Set an element to be tied to a field value. 1. Turn off the inline editing toggle. diff --git a/public/app/features/canvas/elements/ellipse.tsx b/public/app/features/canvas/elements/ellipse.tsx index 2cac9ba1de..c16b1cf057 100644 --- a/public/app/features/canvas/elements/ellipse.tsx +++ b/public/app/features/canvas/elements/ellipse.tsx @@ -6,6 +6,7 @@ import { config } from 'app/core/config'; import { DimensionContext } from 'app/features/dimensions/context'; import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor'; import { TextDimensionEditor } from 'app/features/dimensions/editors/TextDimensionEditor'; +import { getDataLinks } from 'app/plugins/panel/canvas/utils'; import { CanvasElementItem, CanvasElementProps, defaultBgColor, defaultTextColor } from '../element'; import { Align, VAlign, EllipseConfig, EllipseData } from '../types'; @@ -98,6 +99,8 @@ export const ellipseItem: CanvasElementItem = { data.color = ctx.getColor(cfg.color).value(); } + data.links = getDataLinks(ctx, cfg, data.text); + return data; }, diff --git a/public/app/features/canvas/elements/rectangle.tsx b/public/app/features/canvas/elements/rectangle.tsx index 4f0f515dc1..c91e86dcf1 100644 --- a/public/app/features/canvas/elements/rectangle.tsx +++ b/public/app/features/canvas/elements/rectangle.tsx @@ -7,6 +7,7 @@ import { config } from 'app/core/config'; import { DimensionContext } from 'app/features/dimensions/context'; import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor'; import { TextDimensionEditor } from 'app/features/dimensions/editors/TextDimensionEditor'; +import { getDataLinks } from 'app/plugins/panel/canvas/utils'; import { CanvasElementItem, CanvasElementProps, defaultBgColor, defaultTextColor } from '../element'; import { Align, TextConfig, TextData, VAlign } from '../types'; @@ -81,6 +82,8 @@ export const rectangleItem: CanvasElementItem = { data.color = ctx.getColor(cfg.color).value(); } + data.links = getDataLinks(ctx, cfg, data.text); + return data; }, From a4b31ed14037ab159707d5e0250c9313f28f2908 Mon Sep 17 00:00:00 2001 From: Leon Sorokin Date: Tue, 5 Mar 2024 19:03:50 -0600 Subject: [PATCH 0411/1406] BarChart: Improve x=time tick formatting (#83853) --- .../uPlot/config/UPlotAxisBuilder.ts | 3 +- public/app/plugins/panel/barchart/bars.ts | 40 +++++++++++-------- 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/packages/grafana-ui/src/components/uPlot/config/UPlotAxisBuilder.ts b/packages/grafana-ui/src/components/uPlot/config/UPlotAxisBuilder.ts index f446d8129a..d7435b2514 100644 --- a/packages/grafana-ui/src/components/uPlot/config/UPlotAxisBuilder.ts +++ b/packages/grafana-ui/src/components/uPlot/config/UPlotAxisBuilder.ts @@ -213,7 +213,8 @@ export class UPlotAxisBuilder extends PlotConfigBuilder { } } -const timeUnitSize = { +/** @internal */ +export const timeUnitSize = { second: 1000, minute: 60 * 1000, hour: 60 * 60 * 1000, diff --git a/public/app/plugins/panel/barchart/bars.ts b/public/app/plugins/panel/barchart/bars.ts index 541d5c3f30..af1d287eae 100644 --- a/public/app/plugins/panel/barchart/bars.ts +++ b/public/app/plugins/panel/barchart/bars.ts @@ -1,6 +1,6 @@ import uPlot, { Axis, AlignedData, Scale } from 'uplot'; -import { DataFrame, GrafanaTheme2, TimeZone } from '@grafana/data'; +import { DataFrame, dateTimeFormat, GrafanaTheme2, systemDateFormats, TimeZone } from '@grafana/data'; import { alpha } from '@grafana/data/src/themes/colorManipulator'; import { StackingMode, @@ -11,9 +11,11 @@ import { VizLegendOptions, } from '@grafana/schema'; import { measureText, PlotTooltipInterpolator } from '@grafana/ui'; -import { formatTime } from '@grafana/ui/src/components/uPlot/config/UPlotAxisBuilder'; +import { timeUnitSize } from '@grafana/ui/src/components/uPlot/config/UPlotAxisBuilder'; import { StackingGroup, preparePlotData2 } from '@grafana/ui/src/components/uPlot/utils'; +const intervals = systemDateFormats.interval; + import { distribute, SPACE_BETWEEN } from './distribute'; import { findRects, intersects, pointWithin, Quadtree, Rect } from './quadtree'; @@ -130,6 +132,7 @@ export function getConfig(opts: BarsOptions, theme: GrafanaTheme2) { showValue, xSpacing = 0, hoverMulti = false, + timeZone = 'browser', } = opts; const isXHorizontal = xOri === ScaleOrientation.Horizontal; const hasAutoValueSize = !Boolean(opts.text?.valueSize); @@ -179,22 +182,25 @@ export function getConfig(opts: BarsOptions, theme: GrafanaTheme2) { // the splits passed into here are data[0] values looked up by the indices returned from splits() const xValues: Axis.Values = (u, splits, axisIdx, foundSpace, foundIncr) => { if (opts.xTimeAuto) { - // bit of a hack: - // temporarily set x scale range to temporal (as expected by formatTime()) rather than ordinal - let xScale = u.scales.x; - let oMin = xScale.min; - let oMax = xScale.max; - - xScale.min = u.data[0][0]; - xScale.max = u.data[0][u.data[0].length - 1]; - - let vals = formatTime(u, splits, axisIdx, foundSpace, foundIncr); - - // revert - xScale.min = oMin; - xScale.max = oMax; + let format = intervals.year; + + if (foundIncr < timeUnitSize.second) { + format = intervals.millisecond; + } else if (foundIncr < timeUnitSize.minute) { + format = intervals.second; + } else if (foundIncr < timeUnitSize.hour) { + format = intervals.minute; + } else if (foundIncr < timeUnitSize.day) { + format = intervals.hour; + } else if (foundIncr < timeUnitSize.month) { + format = intervals.day; + } else if (foundIncr < timeUnitSize.year) { + format = intervals.month; + } else { + format = intervals.year; + } - return vals; + return splits.map((v) => (v == null ? '' : dateTimeFormat(v, { format, timeZone }))); } return splits.map((v) => (isXHorizontal ? formatShortValue(0, v) : formatValue(0, v))); From db13c0839fa6061b6598e8d6749256127577e4d4 Mon Sep 17 00:00:00 2001 From: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> Date: Tue, 5 Mar 2024 22:02:19 -0500 Subject: [PATCH 0412/1406] =?UTF-8?q?Docs:=20What=E2=80=99s=20new=20&=20Up?= =?UTF-8?q?grade=20guide=2010.4=20(#83133)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Updated index pages and added v10.4 breaking changes, upgrade guide, and what's new pages * Added what's new entries * Removed empty headings * Fixed link * Removed duplicate entry and fixed styling * Removed breaking changes page and references to it * Added intro text * Docs: 10.4 technical note for alertingUpgradeDryrunOnStart (#83262) * Docs: 10.4 technical note for alertingUpgradeDryrunOnStart * Apply suggestions from code review Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com> * Address PR comments * restarts -> starts --------- Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com> * Fixed spelling * Added new entries to What's new * reorder and add intro * Added PagerDuty data source entry * Added entries * Added new entries * Fixed formatting * Fixed page weight and links * Apply suggestion from review Co-authored-by: Mitch Seaman * remove team lbac, move return to previous (#83921) - Remove Team LBAC for Loki (it is Cloud only) - Move Return to Previous into the Alerting section, since it only works for Alerting now - Add a note that data sources are separate from Grafana but included for visibility * Replaced manual note with admonition shortcode * move Return to Previous out of Alerting section * Added youtube links * Commented out youtube videos and removed duplicate video embed --------- Co-authored-by: Matthew Jacobson Co-authored-by: Mitchel Seaman Co-authored-by: Mitch Seaman --- docs/sources/_index.md | 4 +- .../upgrade-guide/upgrade-v10.4/index.md | 37 +++ docs/sources/whatsnew/_index.md | 1 + docs/sources/whatsnew/whats-new-in-v10-4.md | 266 ++++++++++++++++++ 4 files changed, 306 insertions(+), 2 deletions(-) create mode 100644 docs/sources/upgrade-guide/upgrade-v10.4/index.md create mode 100644 docs/sources/whatsnew/whats-new-in-v10-4.md diff --git a/docs/sources/_index.md b/docs/sources/_index.md index b6fb2f9a91..2ef3f960b4 100644 --- a/docs/sources/_index.md +++ b/docs/sources/_index.md @@ -82,8 +82,8 @@ title: Grafana open source documentation

Provisioning

Learn how to automate your Grafana configuration.

- }}" class="nav-cards__item nav-cards__item--guide"> -

What's new in v10.3

+
}}" class="nav-cards__item nav-cards__item--guide"> +

What's new in v10.4

Explore the features and enhancements in the latest release.

diff --git a/docs/sources/upgrade-guide/upgrade-v10.4/index.md b/docs/sources/upgrade-guide/upgrade-v10.4/index.md new file mode 100644 index 0000000000..678641ec4c --- /dev/null +++ b/docs/sources/upgrade-guide/upgrade-v10.4/index.md @@ -0,0 +1,37 @@ +--- +description: Guide for upgrading to Grafana v10.4 +keywords: + - grafana + - configuration + - documentation + - upgrade + - '10.4' +title: Upgrade to Grafana v10.4 +menuTitle: Upgrade to v10.4 +weight: 1300 +--- + +# Upgrade to Grafana v10.4 + +{{< docs/shared lookup="upgrade/intro.md" source="grafana" version="" >}} + +{{< docs/shared lookup="back-up/back-up-grafana.md" source="grafana" version="" leveloffset="+1" >}} + +{{< docs/shared lookup="upgrade/upgrade-common-tasks.md" source="grafana" version="" >}} + +## Technical notes + +### Legacy alerting -> Grafana Alerting dry-run on start + +If you haven't already upgraded to Grafana Alerting from legacy Alerting, Grafana will initiate a dry-run of the upgrade every time the instance starts. This is in preparation for the removal of legacy Alerting in Grafana v11. The dry-run logs the results of the upgrade attempt and identifies any issues requiring attention before you can successfully execute the upgrade. No changes are made during the dry-run. + +You can disable this behavior using the feature flag `alertingUpgradeDryrunOnStart`: + +```toml +[feature_toggles] +alertingUpgradeDryrunOnStart=false +``` + +{{% admonition type="note" %}} +We strongly encourage you to review the [upgrade guide](https://grafana.com/docs/grafana/v10.4/alerting/set-up/migrating-alerts/) and perform the necessary upgrade steps prior to v11. +{{% /admonition %}} diff --git a/docs/sources/whatsnew/_index.md b/docs/sources/whatsnew/_index.md index daac47235f..5e7be79bb3 100644 --- a/docs/sources/whatsnew/_index.md +++ b/docs/sources/whatsnew/_index.md @@ -76,6 +76,7 @@ For a complete list of every change, with links to pull requests and related iss ## Grafana 10 +- [What's new in 10.4](https://grafana.com/docs/grafana//whatsnew/whats-new-in-v10-4/) - [What's new in 10.3](https://grafana.com/docs/grafana//whatsnew/whats-new-in-v10-3/) - [What's new in 10.2](https://grafana.com/docs/grafana//whatsnew/whats-new-in-v10-2/) - [What's new in 10.1]({{< relref "whats-new-in-v10-1/" >}}) diff --git a/docs/sources/whatsnew/whats-new-in-v10-4.md b/docs/sources/whatsnew/whats-new-in-v10-4.md new file mode 100644 index 0000000000..f1106a21b6 --- /dev/null +++ b/docs/sources/whatsnew/whats-new-in-v10-4.md @@ -0,0 +1,266 @@ +--- +description: Feature and improvement highlights for Grafana v10.4 +keywords: + - grafana + - new + - documentation + - '10.4' + - release notes +labels: +products: + - cloud + - enterprise + - oss +title: What's new in Grafana v10.4 +weight: -41 +--- + +# What’s new in Grafana v10.4 + +Welcome to Grafana 10.4! This minor release contains some notable improvements in its own right, as well as early previews of functionality we intend to turn on by default in Grafana v11. Read on to learn about a quicker way to set up alert notifications, an all-new UI for configuring single sign-on, and improvements to our Canvas, Geomap, and Table panels. + +For even more detail about all the changes in this release, refer to the [changelog](https://github.com/grafana/grafana/blob/main/CHANGELOG.md). For the specific steps we recommend when you upgrade to v10.4, check out our [Upgrade Guide](https://grafana.com/docs/grafana//upgrade-guide/upgrade-v10.4/). + + + + + + + +## Dashboards and visualizations + +### AngularJS plugin warnings in dashboards + + + +_Generally available in all editions of Grafana_ + +AngularJS support in Grafana was deprecated in v9 and will be turned off by default in Grafana v11. When this happens, any plugin which depended on AngularJS will not load, and dashboard panels will be unable to show data. + +To help you understand where you may be impacted, Grafana now displays a warning banner in any dashboard with a dependency on an AngularJS plugin. Additionally, warning icons are present in any panel where the panel plugin or underlying data source plugin has an AngularJS dependency. + +This complements the existing warnings already present on the **Plugins** page under the administration menu. + +In addition, you can use our [detect-angular-dashboards](https://github.com/grafana/detect-angular-dashboards) open source tool, which can be run against any Grafana instance to generate a report listing all dashboards that have a dependency on an AngularJS plugin, as well as which plugins are in use. This tool also supports the detection of [private plugins](https://grafana.com/legal/plugins/) that are dependent on AngularJS, however this particular feature requires Grafana v10.1.0 or higher. + +Use the aforementioned tooling and warnings to plan migrations to React based [visualizations](https://grafana.com/docs/grafana/latest/panels-visualizations/) and [data sources](https://grafana.com/docs/grafana/latest/datasources/) included in Grafana or from the [Grafana plugins catalog](https://grafana.com/grafana/plugins/). + +To learn more, refer to the [Angular support deprecation](https://grafana.com/docs/grafana//developers/angular_deprecation/), which includes [recommended alternative plugins](https://grafana.com/docs/grafana//developers/angular_deprecation/angular-plugins/). + + + +[Documentation](https://grafana.com/docs/grafana//developers/angular_deprecation/) + +### Data visualization quality of life improvements + + + +_Generally available in all editions of Grafana_ + +We’ve made a number of small improvements to the data visualization experience in Grafana. + +#### Geomap geojson layer now supports styling + +You can now visualize geojson styles such as polygons, point color/size, and line strings. To learn more, [refer to the documentation](https://grafana.com/docs/grafana//panels-visualizations/visualizations/geomap/#geojson-layer). + +![Geomap marker symbol alignment](/media/docs/grafana/screenshot-grafana-10-4-geomap-geojson-styling-support.png) + +#### Canvas elements now support snapping and aligning + +You can precisely place elements in a canvas with ease as elements now snap into place and align with one another. + +{{< video-embed src="/media/docs/grafana/screen-recording-10-4-canvas-element-snapping.mp4" caption="Canvas element snapping and alignment" >}} + +#### View data links inline in table visualizations + +You can now view your data links inline to help you keep your tables visually streamlined. + +![Table inline datalink support](/media/docs/grafana/gif-grafana-10-4-table-inline-datalink.gif) + +### Create subtables in table visualizations with Group to nested tables + + + +_Available in public preview in all editions of Grafana_ + +You can now create subtables out of your data using the new **Group to nested tables** transformation. To use this feature, enable the `groupToNestedTableTransformation` [feature toggle](https://grafana.com/docs/grafana//setup-grafana/configure-grafana/feature-toggles/#preview-feature-toggles). + +{{< video-embed src="/media/docs/grafana/screen-recording-10-4-table-group-to-nested-table-transformation.mp4" caption="Group to nested tables transformation" >}} + +### Set library panel permissions with RBAC + + + +_Generally available in Grafana Enterprise and Grafana Cloud_ + +We've added the option to manage library panel permissions through role-based access control (RBAC). With this feature, you can choose who can create, edit, and read library panels. RBAC provides a standardized way of granting, changing, and revoking access when it comes to viewing and modifying Grafana resources, such as dashboards, reports, and administrative settings. + +[Documentation](https://grafana.com/docs/grafana//dashboards/build-dashboards/manage-library-panels/) + +### Tooltip improvements + + + +_Available in public preview in all editions of Grafana_ + +We’ve made a number of small improvements to the way tooltips work in Grafana. To try out the new tooltips, enable the `newVizTooltips` [feature toggle](https://grafana.com/docs/grafana//setup-grafana/configure-grafana/feature-toggles/). + +**Copy on click support** + +You can now copy the content from within a tooltip by clicking on the text. + +![Tooltip](/media/docs/grafana/gif-grafana-10-4-tooltip–copy.gif) + +**Scrollable content** + +You can now scroll the content of a tooltip, which allows you to view long lists. This is currently supported in the time series, candlestick, and trend visualizations. We'll add more improvements to the scrolling functionality in a future version. + +![Tooltip](/media/docs/grafana/gif-grafana-10-4-tooltip-content-scroll.gif) + +**Added tooltip options for candlestick visualization** + +The default tooltip options are now also visible in candlestick visualizations. + +**Hover proximity option in time series** + +We've added a tooltip hover proximity limit option (in pixels), which makes it possible to reduce the number of hovered-over data points under the cursor when two datasets are not aligned in time. + +![Time Series hover proximity](/media/docs/grafana/gif-grafana-10-4-hover-proximity.gif) + +## Return to previous + + + +_Available in public preview in all editions of Grafana_ + +When you're browsing Grafana - for example, exploring the dashboard and metrics related to an alert - it's easy to end up far from where you started and hard get back to where you came from. The ‘Return to previous’ button is an easy way to go back to the previous context, like the alert rule that kicked off your exploration. This first release works for Alerts, and we plan to expand to other apps and features in Grafana in future releases to make it easier to navigate around. + +Return to Previous is rolling out across Grafana Cloud now. To try Return to Previous in self-managed Grafana, turn on the `returnToPrevious` [feature toggle](https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/feature-toggles/) in Grafana v10.4 or newer. + + + +{{< admonition type="note" >}} +The term **context** refers to applications in Grafana like Incident and OnCall, as well as core features like Explore and Dashboards. + +To notice a change in your context, look at Grafana's breadcrumbs. If you go from _Home > **Dashboards**_ to _Home > **Explore**_, you've changed context. If you go from _Home > **Dashboards** > Playlist > Edit playlist_ to _Home > **Dashboards** > Reporting > Settings_, you are in the same context. +{{< /admonition >}} + +## Alerting + +### Simplified Alert Notification Routing + + + +_Generally available in all editions of Grafana_ + +This feature simplifies your options for configuring where your notifications are sent when an alert rule fires. Choose an existing contact point directly from within the alert rule creation form without the need to label match notification policies.  You can also set optional muting, grouping, and timing settings directly in the alert rule. + +Simplified routing inherits the alert rule RBAC, increasing control over notification routing while preventing accidental notification policy updates, ensuring critical notifications make it to their intended contact point destination. + +To try out Simplified Alert Notification Routing enable the `alertingSimplifiedRouting` feature toggle. + + + +### Grafana Alerting upgrade with rule preview + + + +_Generally available in all editions of Grafana_ + +Users looking to migrate to the new Grafana Alerting product can do so with confidence with the Grafana Alerting migration preview tool. The migration preview tool allows users to view, edit, and delete migrated rules prior cutting over, with the option to roll back to Legacy Alerting. + +[Documentation](https://grafana.com/docs/grafana//alerting/set-up/migrating-alerts/#upgrade-with-preview-recommended) + +### Rule evaluation spread over the entire evaluation interval + + + +_Generally available in all editions of Grafana_ + +Grafana Alerting previously evaluated rules at the start of the evaluation interval. This created a sudden spike of resource utilization, impacting data sources. Rule evaluation is now spread over the entire interval for smoother performance utilization of data sources. + +### UTF-8 Support for Prometheus and Mimir Alertmanagers + + + +_Generally available in all editions of Grafana_ + +Grafana can now be used to manage both Prometheus and Mimir Alertmanagers with UTF-8 configurations. For more information, please see the +[release notes for Alertmanager 0.27.0](https://github.com/prometheus/alertmanager/releases). + +## Authentication and authorization + +### SSO Settings UI and Terraform resource for configuring OAuth providers + + + +_Available in public preview in all editions of Grafana_ + +Configuring OAuth providers was a bit cumbersome in Grafana: Grafana Cloud users had to reach out to Grafana Support, self-hosted users had to manually edit the configuration file, set up environment variables, and then they had to restart Grafana. On Cloud, the Advanced Auth page is there to configure some of the providers, but configuring Generic OAuth hasn’t been available until now and there was no way to manage the settings through the Grafana UI, nor was there a way to manage the settings through Terraform or the Grafana API. + +Our goal is to make setting up SSO for your Grafana instance simple and fast. + +To get there, we are introducing easier self-serve configuration options for OAuth in Grafana. All of the currently supported OAuth providers are now available for configuration through the Grafana UI, Terraform and via the API. From the UI, you can also now manage all of the settings for the Generic OAuth provider. + +We are working on adding complete support for configuring all other supported OAuth providers as well, such as GitHub, GitLab, Google, Microsoft Azure AD and Okta. You can already manage some of these settings via the new self-serve configuration options, and we’re working on adding more at the moment. + +![Screenshot of the Authentication provider list page](/media/docs/grafana-cloud/screenshot-sso-settings-ui-public-prev-v10.4.png) + + + +[Documentation](https://grafana.com/docs/grafana/next/setup-grafana/configure-security/configure-authentication/) + +## Data sources + +{{< admonition type="note" >}} +The following data sources are released separately from Grafana itself. They are included here for extra visibility. +{{< /admonition >}} + +### PagerDuty enterprise data source for Grafana + + + +_Generally available in Grafana Enterprise and Grafana Cloud_ + +PagerDuty enterprise data source plugin for Grafana allows you to query incidents data or visualize incidents using annotations. + +{{< admonition type="note" >}} +Plugin is currently in a preview phase. +{{< /admonition >}} + +You can find more information and how to configure the plugin in the [documentation](https://grafana.com/docs/plugins/grafana-pagerduty-datasource/latest/). + +Screenshots: + +{{< figure src="/media/docs/plugins/PagerDuty-incidents-annotation.png" caption="PagerDuty data source annotation editor" alt="PagerDuty data source annotation editor" >}} + +{{< figure src="/media/docs/plugins/PagerDuty-incidents-real-life-example.png" caption="Incidents annotations from PagerDuty data source on a dashboard panel" alt="Incidents annotations from PagerDuty data source on a dashboard panel" >}} + + + +### SurrealDB Data Source + + + +_Experimental in all editions of Grafana_ + +A SurrealDB data source has been [added to the Plugin Catalog](https://grafana.com/grafana/plugins/grafana-surrealdb-datasource/), enabling the integration of [SurrealDB](https://surrealdb.com/), a real-time, multi-model database, with Grafana's visualization capabilities. This datasource allows users to directly query and visualize data from SurrealDB within Grafana, using SurrealDB's query language. + +The SurrealDB data source launches with just the basics today. You can write queries in SurrealQL using the built-in query editor, although many Grafana features like macros are not supported for now. + +You can find more information and how to configure the plugin [on Github](https://github.com/grafana/surrealdb-datasource). + +{{< figure src="/media/images/dashboards/surrealdb-dashboard-example.png" >}} + +[Documentation](https://grafana.com/grafana/plugins/grafana-surrealdb-datasource/) From de563aa39cb90501f911d8a23a6ed9a6265e3c48 Mon Sep 17 00:00:00 2001 From: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> Date: Wed, 6 Mar 2024 02:14:31 -0500 Subject: [PATCH 0413/1406] Docs: fix commented out Slack team names (#83946) --- docs/sources/whatsnew/whats-new-in-v10-4.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/sources/whatsnew/whats-new-in-v10-4.md b/docs/sources/whatsnew/whats-new-in-v10-4.md index f1106a21b6..434ea61f73 100644 --- a/docs/sources/whatsnew/whats-new-in-v10-4.md +++ b/docs/sources/whatsnew/whats-new-in-v10-4.md @@ -42,7 +42,7 @@ Use full URLs for links. When linking to versioned docs, replace the version wit ### AngularJS plugin warnings in dashboards - + _Generally available in all editions of Grafana_ @@ -100,7 +100,7 @@ You can now create subtables out of your data using the new **Group to nested ta ### Set library panel permissions with RBAC - + _Generally available in Grafana Enterprise and Grafana Cloud_ @@ -140,7 +140,7 @@ We've added a tooltip hover proximity limit option (in pixels), which makes it p ## Return to previous - + _Available in public preview in all editions of Grafana_ @@ -160,7 +160,7 @@ To notice a change in your context, look at Grafana's breadcrumbs. If you go fro ### Simplified Alert Notification Routing - + _Generally available in all editions of Grafana_ @@ -251,7 +251,7 @@ Screenshots: ### SurrealDB Data Source - + _Experimental in all editions of Grafana_ From 2c09d863950d56be35e6046673aeb75a80cc67c8 Mon Sep 17 00:00:00 2001 From: Fabrizio <135109076+fabrizio-grafana@users.noreply.github.com> Date: Wed, 6 Mar 2024 08:45:07 +0100 Subject: [PATCH 0414/1406] Tempo: Fix by operator to support multiple arguments (#83947) --- public/app/plugins/datasource/tempo/package.json | 2 +- .../datasource/tempo/traceql/highlighting.test.ts | 1 + yarn.lock | 10 +++++----- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/public/app/plugins/datasource/tempo/package.json b/public/app/plugins/datasource/tempo/package.json index 87c078516c..35777a04ac 100644 --- a/public/app/plugins/datasource/tempo/package.json +++ b/public/app/plugins/datasource/tempo/package.json @@ -9,7 +9,7 @@ "@grafana/e2e-selectors": "workspace:*", "@grafana/experimental": "1.7.10", "@grafana/lezer-logql": "0.2.3", - "@grafana/lezer-traceql": "0.0.15", + "@grafana/lezer-traceql": "0.0.16", "@grafana/monaco-logql": "^0.0.7", "@grafana/o11y-ds-frontend": "workspace:*", "@grafana/runtime": "workspace:*", diff --git a/public/app/plugins/datasource/tempo/traceql/highlighting.test.ts b/public/app/plugins/datasource/tempo/traceql/highlighting.test.ts index 7de68f996e..e9b1d2a80e 100644 --- a/public/app/plugins/datasource/tempo/traceql/highlighting.test.ts +++ b/public/app/plugins/datasource/tempo/traceql/highlighting.test.ts @@ -170,6 +170,7 @@ describe('Highlighting', () => { ['{ span.s"tat"us" = "GET123 }'], // weird query, but technically valid ['{ duration = 123.456us}'], ['{ .foo = `GET` && .bar = `P\'O"S\\T` }'], + ['{ .foo = `GET` } | by(.foo, name)'], ])('valid query - %s', (query: string) => { expect(getErrorNodes(query)).toStrictEqual([]); }); diff --git a/yarn.lock b/yarn.lock index e4ccd13414..5c7592eaba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3433,7 +3433,7 @@ __metadata: "@grafana/e2e-selectors": "workspace:*" "@grafana/experimental": "npm:1.7.10" "@grafana/lezer-logql": "npm:0.2.3" - "@grafana/lezer-traceql": "npm:0.0.15" + "@grafana/lezer-traceql": "npm:0.0.16" "@grafana/monaco-logql": "npm:^0.0.7" "@grafana/o11y-ds-frontend": "workspace:*" "@grafana/plugin-configs": "npm:11.0.0-pre" @@ -3823,12 +3823,12 @@ __metadata: languageName: node linkType: hard -"@grafana/lezer-traceql@npm:0.0.15": - version: 0.0.15 - resolution: "@grafana/lezer-traceql@npm:0.0.15" +"@grafana/lezer-traceql@npm:0.0.16": + version: 0.0.16 + resolution: "@grafana/lezer-traceql@npm:0.0.16" peerDependencies: "@lezer/lr": ^1.3.0 - checksum: 10/fddf0958c3e01ef55810916f6d57de5b1b0658573faf15264b862c963b83102cb530991bef16649daa7a51f6e62e45e61dd9be9dd9d00817fd5ea12f50876a8c + checksum: 10/64443356f9ef880cbd2ccba7990c7a05d8eb73b165ef79d6ea55a269a775954a22262c3c90988dff82a21f4081dd8236ffe2c4af7c3ea41d03f7d994bea1af93 languageName: node linkType: hard From 2653bd8fabb4b7147f1e90b1f10acca5b8dfe279 Mon Sep 17 00:00:00 2001 From: Pepe Cano <825430+ppcano@users.noreply.github.com> Date: Wed, 6 Mar 2024 09:18:59 +0100 Subject: [PATCH 0415/1406] Alerting docs: update file provisioning guide (#83924) --- .../file-provisioning/index.md | 93 ++++++------------- 1 file changed, 26 insertions(+), 67 deletions(-) diff --git a/docs/sources/alerting/set-up/provision-alerting-resources/file-provisioning/index.md b/docs/sources/alerting/set-up/provision-alerting-resources/file-provisioning/index.md index 6e267ff22f..67322459c8 100644 --- a/docs/sources/alerting/set-up/provision-alerting-resources/file-provisioning/index.md +++ b/docs/sources/alerting/set-up/provision-alerting-resources/file-provisioning/index.md @@ -20,17 +20,13 @@ weight: 100 # Use configuration files to provision alerting resources -Manage your alerting resources using files from disk. When you start Grafana, the data from these files is created in your Grafana system. Grafana adds any new resources you created, updates any that you changed, and deletes old ones. +Manage your alerting resources using configuration files that can be version controlled. When Grafana starts, it provisions the resources defined in your configuration files. [Provisioning][provisioning] can create, update, or delete existing resources in your Grafana instance. -Arrange your files in a directory in a way that best suits your use case. For example, you can choose a team-based layout where every team has its own file, you can have one big file for all your teams; or you can have one file per resource type. - -Details on how to set up the files and which fields are required for each object are listed below depending on which resource you are provisioning. - -For a complete guide about how Grafana provisions resources, refer to the [Provision Grafana][provisioning] documentation. +This guide outlines the steps and references to provision alerting resources using YAML files. For a practical demo, you can clone and try [this example using Grafana OSS and Docker Compose](https://github.com/grafana/provisioning-alerting-examples/tree/main/config-files). {{< admonition type="note" >}} -- Provisioning with configuration files is not available in Grafana Cloud. +- [Provisioning Grafana](/docs/grafana//administration/provisioning) with configuration files is not available in Grafana Cloud. - You cannot edit provisioned resources from files in Grafana. You can only change the resource properties by changing the provisioning file and restarting Grafana or carrying out a hot reload. This prevents changes being made to the resource that would be overwritten if a file is provisioned again or a hot reload is carried out. @@ -39,12 +35,14 @@ For a complete guide about how Grafana provisions resources, refer to the [Provi - Importing an existing alerting resource results in a conflict. First, when present, remove the resources you plan to import. {{< /admonition >}} +Details on how to set up the files and which fields are required for each object are listed below depending on which resource you are provisioning. + ## Import alert rules Create or delete alert rules using provisioning files in your Grafana instance(s). 1. Find the alert rule group in Grafana. -1. [Export][alerting_export] and download a provisioning file for your alert rules. +1. [Export][export_alert_rules] and download a provisioning file for your alert rules. 1. Copy the contents into a YAML or JSON configuration file and add it to the `provisioning/alerting` directory of the Grafana instance you want to import the alerting resources to. Example configuration files can be found below. @@ -143,7 +141,7 @@ deleteRules: Create or delete contact points using provisioning files in your Grafana instance(s). 1. Find the contact point in Grafana. -1. [Export][alerting_export] and download a provisioning file for your contact point. +1. [Export][export_contact_points] and download a provisioning file for your contact point. 1. Copy the contents into a YAML or JSON configuration file and add it to the `provisioning/alerting` directory of the Grafana instance you want to import the alerting resources to. Example configuration files can be found below. @@ -576,7 +574,7 @@ settings: Create or delete templates using provisioning files in your Grafana instance(s). 1. Find the notification template in Grafana. -1. [Export][alerting_export] a template by copying the template content and title. +1. [Export][export_templates] a template by copying the template content and title. 1. Copy the contents into a YAML or JSON configuration file and add it to the `provisioning/alerting` directory of the Grafana instance you want to import the alerting resources to. Example configuration files can be found below. @@ -629,7 +627,7 @@ Since the policy tree is a single resource, provisioning it will overwrite a pol {{< /admonition >}} 1. Find the notification policy tree in Grafana. -1. [Export][alerting_export] and download a provisioning file for your notification policy tree. +1. [Export][export_policies] and download a provisioning file for your notification policy tree. 1. Copy the contents into a YAML or JSON configuration file and add it to the `provisioning/alerting` directory of the Grafana instance you want to import the alerting resources to. Example configuration files can be found below. @@ -715,7 +713,7 @@ resetPolicies: Create or delete mute timings via provisioning files using provisioning files in your Grafana instance(s). 1. Find the mute timing in Grafana. -1. [Export][alerting_export] and download a provisioning file for your mute timing. +1. [Export][export_mute_timings] and download a provisioning file for your mute timing. 1. Copy the contents into a YAML or JSON configuration file and add it to the `provisioning/alerting` directory of the Grafana instance you want to import the alerting resources to. Example configuration files can be found below. @@ -761,69 +759,30 @@ deleteMuteTimes: name: mti_1 ``` -## File provisioning using Kubernetes - -If you are a Kubernetes user, you can leverage file provisioning using Kubernetes configuration maps. +## More examples -1. Create one or more configuration maps as follows. +For more examples on the concept of this guide: - ```yaml - apiVersion: v1 - kind: ConfigMap - metadata: - name: grafana-alerting - data: - provisioning.yaml: | - templates: - - name: my_first_template - template: the content for my template - ``` +- Try provisioning alerting resources in Grafana OSS with YAML files through a demo project using [Docker Compose](https://github.com/grafana/provisioning-alerting-examples/tree/main/config-files) or [Kubernetes deployments](https://github.com/grafana/provisioning-alerting-examples/tree/main/kubernetes). +- Review the distinct options about how Grafana provisions resources in the [Provision Grafana documentation][provisioning]. +- For Helm support, review the examples provisioning alerting resources in the [Grafana Helm Chart documentation](https://github.com/grafana/helm-charts/blob/main/charts/grafana/README.md). -1. Restart your Grafana instance (or reload the provisioned files using the Admin API). +{{% docs/reference %}} - ```yaml - apiVersion: apps/v1 - kind: Deployment - metadata: - name: grafana - spec: - replicas: 1 - selector: - matchLabels: - app: grafana - template: - metadata: - name: grafana - labels: - app: grafana - spec: - containers: - - name: grafana - image: grafana/grafana:latest - ports: - - name: grafana - containerPort: 3000 - volumeMounts: - - mountPath: /etc/grafana/provisioning/alerting - name: grafana-alerting - readOnly: false - volumes: - - name: grafana-alerting - configMap: - defaultMode: 420 - name: grafana-alerting - ``` - -This eliminates the need for a persistent database to use Grafana Alerting in Kubernetes; all your provisioned resources appear after each restart or re-deployment. Grafana still requires a database for normal operation, you do not need to persist the contents of the database between restarts if all objects are provisioned using files. +[export_alert_rules]: "/docs/grafana/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/export-alerting-resources#export-alert-rules" +[export_alert_rules]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/set-up/provision-alerting-resources/export-alerting-resources#export-alert-rules" -## More examples +[export_contact_points]: "/docs/grafana/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/export-alerting-resources#export-contact-points" +[export_contact_points]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/set-up/provision-alerting-resources/export-alerting-resources#export-contact-points" -- [Provision Grafana][provisioning] +[export_templates]: "/docs/grafana/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/export-alerting-resources#export-templates" +[export_templates]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/set-up/provision-alerting-resources/export-alerting-resources#export-templates" -{{% docs/reference %}} +[export_policies]: "/docs/grafana/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/export-alerting-resources#export-the-notification-policy-tree" +[export_policies]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/set-up/provision-alerting-resources/export-alerting-resources#export-the-notification-policy-tree" -[alerting_export]: "/docs/grafana/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/export-alerting-resources" -[alerting_export]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/set-up/provision-alerting-resources/export-alerting-resources" +[export_mute_timings]: "/docs/grafana/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/export-alerting-resources#export-mute-timings" +[export_mute_timings]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/set-up/provision-alerting-resources/export-alerting-resources#export-mute-timings" [provisioning]: "/docs/ -> /docs/grafana//administration/provisioning" From e611a736ed8a57e45c737fe6efdfffbe76084cb1 Mon Sep 17 00:00:00 2001 From: Eric Leijonmarck Date: Wed, 6 Mar 2024 09:53:58 +0100 Subject: [PATCH 0416/1406] Serviceaccounts: Add ability to add samename SA for different orgs (#83893) * add ability to add samename SA for different orgs * Update pkg/services/user/userimpl/user.go * fix tests * refactor name * removed tests * add migration * fix linting --- .../serviceaccounts/database/store.go | 17 +++- .../serviceaccounts/database/store_test.go | 93 ++++++++++++++++--- pkg/services/sqlstore/migrations/user_mig.go | 7 ++ 3 files changed, 102 insertions(+), 15 deletions(-) diff --git a/pkg/services/serviceaccounts/database/store.go b/pkg/services/serviceaccounts/database/store.go index 581626a3d2..ebb66ef484 100644 --- a/pkg/services/serviceaccounts/database/store.go +++ b/pkg/services/serviceaccounts/database/store.go @@ -42,10 +42,19 @@ func ProvideServiceAccountsStore(cfg *setting.Cfg, store db.DB, apiKeyService ap } } +// generateLogin makes a generated string to have a ID for the service account across orgs and it's name +// this causes you to create a service account with the same name in different orgs +// not the same name in the same org +func generateLogin(prefix string, orgId int64, name string) string { + generatedLogin := fmt.Sprintf("%v-%v-%v", prefix, orgId, strings.ToLower(name)) + // in case the name has multiple spaces or dashes in the prefix or otherwise, replace them with a single dash + generatedLogin = strings.Replace(generatedLogin, "--", "-", 1) + return strings.Replace(generatedLogin, " ", "-", -1) +} + // CreateServiceAccount creates service account func (s *ServiceAccountsStoreImpl) CreateServiceAccount(ctx context.Context, orgId int64, saForm *serviceaccounts.CreateServiceAccountForm) (*serviceaccounts.ServiceAccountDTO, error) { - generatedLogin := serviceaccounts.ServiceAccountPrefix + strings.ToLower(saForm.Name) - generatedLogin = strings.ReplaceAll(generatedLogin, " ", "-") + login := generateLogin(serviceaccounts.ServiceAccountPrefix, orgId, saForm.Name) isDisabled := false role := org.RoleViewer if saForm.IsDisabled != nil { @@ -56,7 +65,7 @@ func (s *ServiceAccountsStoreImpl) CreateServiceAccount(ctx context.Context, org } newSA, err := s.userService.CreateServiceAccount(ctx, &user.CreateUserCommand{ - Login: generatedLogin, + Login: login, OrgID: orgId, Name: saForm.Name, IsDisabled: isDisabled, @@ -435,7 +444,7 @@ func (s *ServiceAccountsStoreImpl) MigrateApiKey(ctx context.Context, orgId int6 func (s *ServiceAccountsStoreImpl) CreateServiceAccountFromApikey(ctx context.Context, key *apikey.APIKey) error { prefix := "sa-autogen" cmd := user.CreateUserCommand{ - Login: fmt.Sprintf("%v-%v-%v", prefix, key.OrgID, key.Name), + Login: generateLogin(prefix, key.OrgID, key.Name), Name: fmt.Sprintf("%v-%v", prefix, key.Name), OrgID: key.OrgID, DefaultOrgRole: string(key.Role), diff --git a/pkg/services/serviceaccounts/database/store_test.go b/pkg/services/serviceaccounts/database/store_test.go index 57bbd8e4d4..42e8da7771 100644 --- a/pkg/services/serviceaccounts/database/store_test.go +++ b/pkg/services/serviceaccounts/database/store_test.go @@ -30,8 +30,8 @@ func TestMain(m *testing.M) { // Service Account should not create an org on its own func TestStore_CreateServiceAccountOrgNonExistant(t *testing.T) { _, store := setupTestDatabase(t) + serviceAccountName := "new Service Account" t.Run("create service account", func(t *testing.T) { - serviceAccountName := "new Service Account" serviceAccountOrgId := int64(1) serviceAccountRole := org.RoleAdmin isDisabled := true @@ -47,13 +47,12 @@ func TestStore_CreateServiceAccountOrgNonExistant(t *testing.T) { } func TestStore_CreateServiceAccount(t *testing.T) { - _, store := setupTestDatabase(t) - orgQuery := &org.CreateOrgCommand{Name: orgimpl.MainOrgName} - orgResult, err := store.orgService.CreateWithMember(context.Background(), orgQuery) - require.NoError(t, err) - + serviceAccountName := "new Service Account" t.Run("create service account", func(t *testing.T) { - serviceAccountName := "new Service Account" + _, store := setupTestDatabase(t) + orgQuery := &org.CreateOrgCommand{Name: orgimpl.MainOrgName} + orgResult, err := store.orgService.CreateWithMember(context.Background(), orgQuery) + require.NoError(t, err) serviceAccountOrgId := orgResult.ID serviceAccountRole := org.RoleAdmin isDisabled := true @@ -65,13 +64,11 @@ func TestStore_CreateServiceAccount(t *testing.T) { saDTO, err := store.CreateServiceAccount(context.Background(), serviceAccountOrgId, &saForm) require.NoError(t, err) - assert.Equal(t, "sa-new-service-account", saDTO.Login) assert.Equal(t, serviceAccountName, saDTO.Name) assert.Equal(t, 0, int(saDTO.Tokens)) retrieved, err := store.RetrieveServiceAccount(context.Background(), serviceAccountOrgId, saDTO.Id) require.NoError(t, err) - assert.Equal(t, "sa-new-service-account", retrieved.Login) assert.Equal(t, serviceAccountName, retrieved.Name) assert.Equal(t, serviceAccountOrgId, retrieved.OrgId) assert.Equal(t, string(serviceAccountRole), retrieved.Role) @@ -81,6 +78,82 @@ func TestStore_CreateServiceAccount(t *testing.T) { require.NoError(t, err) assert.Equal(t, saDTO.Id, retrievedId) }) + + t.Run("create service account twice same org, error", func(t *testing.T) { + _, store := setupTestDatabase(t) + orgQuery := &org.CreateOrgCommand{Name: orgimpl.MainOrgName} + orgResult, err := store.orgService.CreateWithMember(context.Background(), orgQuery) + require.NoError(t, err) + serviceAccountOrgId := orgResult.ID + serviceAccountRole := org.RoleAdmin + isDisabled := true + saForm := serviceaccounts.CreateServiceAccountForm{ + Name: serviceAccountName, + Role: &serviceAccountRole, + IsDisabled: &isDisabled, + } + + saDTO, err := store.CreateServiceAccount(context.Background(), serviceAccountOrgId, &saForm) + require.NoError(t, err) + assert.Equal(t, serviceAccountName, saDTO.Name) + assert.Equal(t, 0, int(saDTO.Tokens)) + + retrieved, err := store.RetrieveServiceAccount(context.Background(), serviceAccountOrgId, saDTO.Id) + require.NoError(t, err) + assert.Equal(t, serviceAccountName, retrieved.Name) + assert.Equal(t, serviceAccountOrgId, retrieved.OrgId) + assert.Equal(t, string(serviceAccountRole), retrieved.Role) + assert.True(t, retrieved.IsDisabled) + + retrievedId, err := store.RetrieveServiceAccountIdByName(context.Background(), serviceAccountOrgId, serviceAccountName) + require.NoError(t, err) + assert.Equal(t, saDTO.Id, retrievedId) + + // should not b able to create the same service account twice in the same org + _, err = store.CreateServiceAccount(context.Background(), serviceAccountOrgId, &saForm) + require.Error(t, err) + }) + + t.Run("create service account twice different orgs should work", func(t *testing.T) { + _, store := setupTestDatabase(t) + orgQuery := &org.CreateOrgCommand{Name: orgimpl.MainOrgName} + orgResult, err := store.orgService.CreateWithMember(context.Background(), orgQuery) + require.NoError(t, err) + serviceAccountOrgId := orgResult.ID + serviceAccountRole := org.RoleAdmin + isDisabled := true + saForm := serviceaccounts.CreateServiceAccountForm{ + Name: serviceAccountName, + Role: &serviceAccountRole, + IsDisabled: &isDisabled, + } + + saDTO, err := store.CreateServiceAccount(context.Background(), serviceAccountOrgId, &saForm) + require.NoError(t, err) + assert.Equal(t, serviceAccountName, saDTO.Name) + assert.Equal(t, 0, int(saDTO.Tokens)) + + retrieved, err := store.RetrieveServiceAccount(context.Background(), serviceAccountOrgId, saDTO.Id) + require.NoError(t, err) + assert.Equal(t, serviceAccountName, retrieved.Name) + assert.Equal(t, serviceAccountOrgId, retrieved.OrgId) + assert.Equal(t, string(serviceAccountRole), retrieved.Role) + assert.True(t, retrieved.IsDisabled) + + retrievedId, err := store.RetrieveServiceAccountIdByName(context.Background(), serviceAccountOrgId, serviceAccountName) + require.NoError(t, err) + assert.Equal(t, saDTO.Id, retrievedId) + + orgQuerySecond := &org.CreateOrgCommand{Name: "Second Org name"} + orgResultSecond, err := store.orgService.CreateWithMember(context.Background(), orgQuerySecond) + require.NoError(t, err) + serviceAccountOrgIdSecond := orgResultSecond.ID + // should not b able to create the same service account twice in the same org + saDTOSecond, err := store.CreateServiceAccount(context.Background(), serviceAccountOrgIdSecond, &saForm) + require.NoError(t, err) + assert.Equal(t, serviceAccountName, saDTOSecond.Name) + assert.Equal(t, 0, int(saDTOSecond.Tokens)) + }) } func TestStore_CreateServiceAccountRoleNone(t *testing.T) { @@ -100,13 +173,11 @@ func TestStore_CreateServiceAccountRoleNone(t *testing.T) { saDTO, err := store.CreateServiceAccount(context.Background(), serviceAccountOrgId, &saForm) require.NoError(t, err) - assert.Equal(t, "sa-new-service-account", saDTO.Login) assert.Equal(t, serviceAccountName, saDTO.Name) assert.Equal(t, 0, int(saDTO.Tokens)) retrieved, err := store.RetrieveServiceAccount(context.Background(), serviceAccountOrgId, saDTO.Id) require.NoError(t, err) - assert.Equal(t, "sa-new-service-account", retrieved.Login) assert.Equal(t, serviceAccountName, retrieved.Name) assert.Equal(t, serviceAccountOrgId, retrieved.OrgId) assert.Equal(t, string(serviceAccountRole), retrieved.Role) diff --git a/pkg/services/sqlstore/migrations/user_mig.go b/pkg/services/sqlstore/migrations/user_mig.go index 4f56666fcd..bac9f99bf1 100644 --- a/pkg/services/sqlstore/migrations/user_mig.go +++ b/pkg/services/sqlstore/migrations/user_mig.go @@ -153,6 +153,13 @@ func addUserMigrations(mg *Migrator) { mg.AddMigration("Add unique index user_uid", NewAddIndexMigration(userV2, &Index{ Cols: []string{"uid"}, Type: UniqueIndex, })) + + // Service accounts login were not unique per org. this migration is part of making it unique per org + // to be able to create service accounts that are unique per org + mg.AddMigration("Update login field for service accounts", NewRawSQLMigration(""). + SQLite("UPDATE user SET login = 'sa-' || CAST(org_id AS TEXT) || '-' || REPLACE(login, 'sa-', '') WHERE login IS NOT NULL AND is_service_account = 1;"). + Postgres("UPDATE \"user\" SET login = 'sa-' || org_id::text || '-' || REPLACE(login, 'sa-', '') WHERE login IS NOT NULL AND is_service_account = true;"). + Mysql("UPDATE user SET login = CONCAT('sa-', CAST(org_id AS CHAR), '-', REPLACE(login, 'sa-', '')) WHERE login IS NOT NULL AND is_service_account = 1;")) } const migSQLITEisServiceAccountNullable = `ALTER TABLE user ADD COLUMN tmp_service_account BOOLEAN DEFAULT 0; From d767c4f6946b95516b6d70e59987634cab288d65 Mon Sep 17 00:00:00 2001 From: Jennifer Villa Date: Wed, 6 Mar 2024 03:33:09 -0600 Subject: [PATCH 0417/1406] Remove hanging text block from traces->profiles docs (#83859) Remove hanging text block This is causing the page to render a bit oddly on the website --- docs/sources/shared/datasources/tempo-traces-to-profiles.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/sources/shared/datasources/tempo-traces-to-profiles.md b/docs/sources/shared/datasources/tempo-traces-to-profiles.md index 667402b9a9..02c4edd3fb 100644 --- a/docs/sources/shared/datasources/tempo-traces-to-profiles.md +++ b/docs/sources/shared/datasources/tempo-traces-to-profiles.md @@ -96,5 +96,3 @@ To use a custom query with the configuration, follow these steps: 1. Switch on **Use custom query** to enter a custom query. 1. Specify a custom query to be used to query profile data. You can use various variables to make that query relevant for current span. The link is shown only if all the variables are interpolated with non-empty values to prevent creating an invalid query. You can interpolate the configured tags using the `$__tags` keyword. 1. Select **Save and Test**. - - | From 708aeb0682a7a7c74ec2d4ab6648880e0946bd27 Mon Sep 17 00:00:00 2001 From: Konrad Lalik Date: Wed, 6 Mar 2024 11:42:28 +0100 Subject: [PATCH 0418/1406] Alerting: Pass queryType parameter to the query model in recording rules preview (#83950) --- .../unified/components/rule-editor/RecordingRuleEditor.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/public/app/features/alerting/unified/components/rule-editor/RecordingRuleEditor.tsx b/public/app/features/alerting/unified/components/rule-editor/RecordingRuleEditor.tsx index 3fe2feb640..60a0d44fe6 100644 --- a/public/app/features/alerting/unified/components/rule-editor/RecordingRuleEditor.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/RecordingRuleEditor.tsx @@ -68,8 +68,11 @@ export const RecordingRuleEditor: FC = ({ datasource: changedQuery.datasource, refId: changedQuery.refId, editorMode: changedQuery.editorMode, - instant: Boolean(changedQuery.instant), - range: Boolean(changedQuery.range), + // Instant and range are used by Prometheus queries + instant: changedQuery.instant, + range: changedQuery.range, + // Query type is used by Loki queries + queryType: changedQuery.queryType, legendFormat: changedQuery.legendFormat, }, }; From 5950dc3279a6fdeab7d995b12fb5bd56271887a4 Mon Sep 17 00:00:00 2001 From: brendamuir <100768211+brendamuir@users.noreply.github.com> Date: Wed, 6 Mar 2024 12:24:02 +0100 Subject: [PATCH 0419/1406] Alerting docs: adds top-level landing page tiles (#83825) * Alerting docs: adds top-level landing page tiles * updated description * update frontmatter * ran prettier * Update docs/sources/alerting/_index.md Co-authored-by: Jack Baldry * Update docs/sources/alerting/_index.md Co-authored-by: Jack Baldry * Update docs/sources/alerting/_index.md Co-authored-by: Jack Baldry * Update docs/sources/alerting/_index.md Co-authored-by: Jack Baldry * Update docs/sources/alerting/_index.md Co-authored-by: Jack Baldry * Update docs/sources/alerting/_index.md Co-authored-by: Jack Baldry * Update docs/sources/alerting/_index.md Co-authored-by: Jack Baldry --------- Co-authored-by: Jack Baldry --- docs/sources/alerting/_index.md | 115 +++++++------------ docs/sources/alerting/fundamentals/_index.md | 57 +++++++++ 2 files changed, 98 insertions(+), 74 deletions(-) diff --git a/docs/sources/alerting/_index.md b/docs/sources/alerting/_index.md index 204dde8121..201a86a4bb 100644 --- a/docs/sources/alerting/_index.md +++ b/docs/sources/alerting/_index.md @@ -13,87 +13,54 @@ labels: menuTitle: Alerting title: Grafana Alerting weight: 114 +hero: + title: Grafana Alerting + level: 1 + image: /media/docs/grafana-cloud/alerting-and-irm/grafana-icon-alerting.svg + width: 100 + height: 100 + description: Grafana Alerting allows you to learn about problems in your systems moments after they occur. +cards: + title_class: pt-0 lh-1 + items: + - title: Introduction + href: ./fundamentals/ + description: Learn more about the fundamentals and available features that help you create, manage, and respond to alerts; and improve your team’s ability to resolve issues quickly. + height: 24 + - title: Set up + href: ./set-up/ + description: Set up your implementation of Grafana Alerting. + height: 24 + - title: Configure alert rules + href: ./alerting-rules/ + description: Create, manage, view, and adjust alert rules to alert on your metrics data or log entries from multiple data sources — no matter where your data is stored. + height: 24 + - title: Configure notifications + href: ./configure-notifications/ + description: Choose how, when, and where to send your alert notifications. + height: 24 + - title: Detect and respond + href: ./manage-notifications/ + description: Monitor, respond to, and triage issues within your services. + height: 24 + - title: Monitor + href: ./monitor/ + description: Monitor your alerting metrics to ensure you identify potential issues before they become critical. + height: 24 --- -# Grafana Alerting +{{< docs/hero-simple key="hero" >}} -Grafana Alerting allows you to learn about problems in your systems moments after they occur. +--- + +## Overview -Monitor your incoming metrics data or log entries and set up your Alerting system to watch for specific events or circumstances and then send notifications when those things are found. +Monitor your incoming metrics data or log entries and set up your Grafana Alerting system to watch for specific events or circumstances. In this way, you eliminate the need for manual monitoring and provide a first line of defense against system outages or changes that could turn into major incidents. Using Grafana Alerting, you create queries and expressions from multiple data sources — no matter where your data is stored — giving you the flexibility to combine your data and alert on your metrics and logs in new and unique ways. You can then create, manage, and take action on your alerts from a single, consolidated view, and improve your team’s ability to identify and resolve issues quickly. -Grafana Alerting is available for Grafana OSS, Grafana Enterprise, or Grafana Cloud. With Mimir and Loki alert rules you can run alert expressions closer to your data and at massive scale, all managed by the Grafana UI you are already familiar with. - -_Refer to [Manage your alert rules][alerting-rules] for current instructions._ - -## Key features and benefits - -**One page for all alerts** - -A single Grafana Alerting page consolidates both Grafana-managed alerts and alerts that reside in your Prometheus-compatible data source in one single place. - -**Multi-dimensional alerts** - -Alert rules can create multiple individual alert instances per alert rule, known as multi-dimensional alerts, giving you the power and flexibility to gain visibility into your entire system with just a single alert rule. You do this by adding labels to your query to specify which component is being monitored and generate multiple alert instances for a single alert rule. For example, if you want to monitor each server in a cluster, a multi-dimensional alert will alert on each CPU, whereas a standard alert will alert on the overall server. - -**Route alerts** - -Route each alert instance to a specific contact point based on labels you define. Notification policies are the set of rules for where, when, and how the alerts are routed to contact points. - -**Silence alerts** - -Silences stop notifications from getting created and last for only a specified window of time. -Silences allow you to stop receiving persistent notifications from one or more alert rules. You can also partially pause an alert based on certain criteria. Silences have their own dedicated section for better organization and visibility, so that you can scan your paused alert rules without cluttering the main alerting view. - -**Mute timings** - -A mute timing is a recurring interval of time when no new notifications for a policy are generated or sent. Use them to prevent alerts from firing a specific and reoccurring period, for example, a regular maintenance period. - -Similar to silences, mute timings do not prevent alert rules from being evaluated, nor do they stop alert instances from being shown in the user interface. They only prevent notifications from being created. - -## Design your Alerting system - -Monitoring complex IT systems and understanding whether everything is up and running correctly is a difficult task. Setting up an effective alert management system is therefore essential to inform you when things are going wrong before they start to impact your business outcomes. - -Designing and configuring an alert management set up that works takes time. - -Here are some tips on how to create an effective alert management set up for your business: - -**Which are the key metrics for your business that you want to monitor and alert on?** - -- Find events that are important to know about and not so trivial or frequent that recipients ignore them. - -- Alerts should only be created for big events that require immediate attention or intervention. - -- Consider quality over quantity. - -**Which type of Alerting do you want to use?** - -- Choose between Grafana-managed Alerting or Grafana Mimir or Loki-managed Alerting; or both. - -**How do you want to organize your alerts and notifications?** - -- Be selective about who you set to receive alerts. Consider sending them to whoever is on call or a specific Slack channel. -- Automate as far as possible using the Alerting API or alerts as code (Terraform). - -**How can you reduce alert fatigue?** - -- Avoid noisy, unnecessary alerts by using silences, mute timings, or pausing alert rule evaluation. -- Continually tune your alert rules to review effectiveness. Remove alert rules to avoid duplication or ineffective alerts. -- Think carefully about priority and severity levels. -- Continually review your thresholds and evaluation rules. - -## Useful links - -- [Introduction to Alerting][fundamentals] - -{{% docs/reference %}} -[alerting-rules]: "/docs/grafana/ -> /docs/grafana//alerting/alerting-rules" -[alerting-rules]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/alerting-rules" +## Explore -[fundamentals]: "/docs/grafana/ -> /docs/grafana//alerting/fundamentals" -[fundamentals]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/fundamentals" -{{% /docs/reference %}} +{{< card-grid key="cards" type="simple" >}} diff --git a/docs/sources/alerting/fundamentals/_index.md b/docs/sources/alerting/fundamentals/_index.md index 3066c26877..fed616e08a 100644 --- a/docs/sources/alerting/fundamentals/_index.md +++ b/docs/sources/alerting/fundamentals/_index.md @@ -77,6 +77,63 @@ Silences and mute timings allow you to pause notifications for specific alerts o You can create your alerting resources (alert rules, notification policies, and so on) in the Grafana UI; configmaps, files and configuration management systems using file-based provisioning; and in Terraform using API-based provisioning. +## Key features and benefits + +**One page for all alerts** + +A single Grafana Alerting page consolidates both Grafana-managed alerts and alerts that reside in your Prometheus-compatible data source in one single place. + +**Multi-dimensional alerts** + +Alert rules can create multiple individual alert instances per alert rule, known as multi-dimensional alerts, giving you the power and flexibility to gain visibility into your entire system with just a single alert rule. You do this by adding labels to your query to specify which component is being monitored and generate multiple alert instances for a single alert rule. For example, if you want to monitor each server in a cluster, a multi-dimensional alert will alert on each CPU, whereas a standard alert will alert on the overall server. + +**Route alerts** + +Route each alert instance to a specific contact point based on labels you define. Notification policies are the set of rules for where, when, and how the alerts are routed to contact points. + +**Silence alerts** + +Silences stop notifications from getting created and last for only a specified window of time. +Silences allow you to stop receiving persistent notifications from one or more alert rules. You can also partially pause an alert based on certain criteria. Silences have their own dedicated section for better organization and visibility, so that you can scan your paused alert rules without cluttering the main alerting view. + +**Mute timings** + +A mute timing is a recurring interval of time when no new notifications for a policy are generated or sent. Use them to prevent alerts from firing a specific and reoccurring period, for example, a regular maintenance period. + +Similar to silences, mute timings do not prevent alert rules from being evaluated, nor do they stop alert instances from being shown in the user interface. They only prevent notifications from being created. + +## Design your Alerting system + +Monitoring complex IT systems and understanding whether everything is up and running correctly is a difficult task. Setting up an effective alert management system is therefore essential to inform you when things are going wrong before they start to impact your business outcomes. + +Designing and configuring an alert management set up that works takes time. + +Here are some tips on how to create an effective alert management set up for your business: + +**Which are the key metrics for your business that you want to monitor and alert on?** + +- Find events that are important to know about and not so trivial or frequent that recipients ignore them. + +- Alerts should only be created for big events that require immediate attention or intervention. + +- Consider quality over quantity. + +**Which type of Alerting do you want to use?** + +- Choose between Grafana-managed Alerting or Grafana Mimir or Loki-managed Alerting; or both. + +**How do you want to organize your alerts and notifications?** + +- Be selective about who you set to receive alerts. Consider sending them to whoever is on call or a specific Slack channel. +- Automate as far as possible using the Alerting API or alerts as code (Terraform). + +**How can you reduce alert fatigue?** + +- Avoid noisy, unnecessary alerts by using silences, mute timings, or pausing alert rule evaluation. +- Continually tune your alert rules to review effectiveness. Remove alert rules to avoid duplication or ineffective alerts. +- Think carefully about priority and severity levels. +- Continually review your thresholds and evaluation rules. + ## Principles In Prometheus-based alerting systems, you have an alert generator that creates alerts and an alert receiver that receives alerts. For example, Prometheus is an alert generator and is responsible for evaluating alert rules, while Alertmanager is an alert receiver and is responsible for grouping, inhibiting, silencing, and sending notifications about firing and resolved alerts. From 401265522e584e4e71a1d92d5af311564b1ec33e Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Wed, 6 Mar 2024 12:32:28 +0100 Subject: [PATCH 0420/1406] Logs volume: Add options to specify field to group by (#83823) Logs volume: Add options to specify field to group by in options --- packages/grafana-data/src/types/logs.ts | 4 ++- .../datasource/loki/datasource.test.ts | 26 +++++++++++++++++ .../app/plugins/datasource/loki/datasource.ts | 29 ++++++++++++++----- 3 files changed, 51 insertions(+), 8 deletions(-) diff --git a/packages/grafana-data/src/types/logs.ts b/packages/grafana-data/src/types/logs.ts index e63e551297..d9edfca6bc 100644 --- a/packages/grafana-data/src/types/logs.ts +++ b/packages/grafana-data/src/types/logs.ts @@ -185,6 +185,7 @@ export type SupplementaryQueryOptions = LogsVolumeOption | LogsSampleOptions; */ export type LogsVolumeOption = { type: SupplementaryQueryType.LogsVolume; + field?: string; }; /** @@ -237,7 +238,8 @@ export interface DataSourceWithSupplementaryQueriesSupport + request: DataQueryRequest, + options?: SupplementaryQueryOptions ): DataQueryRequest | undefined; /** * Returns supplementary query types that data source supports. diff --git a/public/app/plugins/datasource/loki/datasource.test.ts b/public/app/plugins/datasource/loki/datasource.test.ts index ea008bd2c6..8509269a28 100644 --- a/public/app/plugins/datasource/loki/datasource.test.ts +++ b/public/app/plugins/datasource/loki/datasource.test.ts @@ -1349,6 +1349,7 @@ describe('LokiDatasource', () => { queryType: LokiQueryType.Range, refId: 'log-volume-A', supportingQueryType: SupportingQueryType.LogsVolume, + legendFormat: '{{ level }}', }); }); @@ -1367,6 +1368,7 @@ describe('LokiDatasource', () => { queryType: LokiQueryType.Range, refId: 'log-volume-A', supportingQueryType: SupportingQueryType.LogsVolume, + legendFormat: '{{ level }}', }); }); @@ -1395,6 +1397,30 @@ describe('LokiDatasource', () => { ) ).toEqual(undefined); }); + + it('return logs volume query with defined field', () => { + const query = ds.getSupplementaryQuery( + { type: SupplementaryQueryType.LogsVolume, field: 'test' }, + { + expr: '{label="value"}', + queryType: LokiQueryType.Range, + refId: 'A', + } + ); + expect(query?.expr).toEqual('sum by (test) (count_over_time({label="value"} | drop __error__[$__auto]))'); + }); + + it('return logs volume query with level as field if no field specified', () => { + const query = ds.getSupplementaryQuery( + { type: SupplementaryQueryType.LogsVolume }, + { + expr: '{label="value"}', + queryType: LokiQueryType.Range, + refId: 'A', + } + ); + expect(query?.expr).toEqual('sum by (level) (count_over_time({label="value"} | drop __error__[$__auto]))'); + }); }); describe('logs sample', () => { diff --git a/public/app/plugins/datasource/loki/datasource.ts b/public/app/plugins/datasource/loki/datasource.ts index 90380a9a88..74c1a814c9 100644 --- a/public/app/plugins/datasource/loki/datasource.ts +++ b/public/app/plugins/datasource/loki/datasource.ts @@ -38,6 +38,8 @@ import { DataSourceGetTagValuesOptions, DataSourceGetTagKeysOptions, DataSourceWithQueryModificationSupport, + LogsVolumeOption, + LogsSampleOptions, } from '@grafana/data'; import { Duration } from '@grafana/lezer-logql'; import { BackendSrvRequest, config, DataSourceWithBackend, getTemplateSrv, TemplateSrv } from '@grafana/runtime'; @@ -167,13 +169,18 @@ export class LokiDatasource */ getSupplementaryRequest( type: SupplementaryQueryType, - request: DataQueryRequest + request: DataQueryRequest, + options?: SupplementaryQueryOptions ): DataQueryRequest | undefined { switch (type) { case SupplementaryQueryType.LogsVolume: - return this.getLogsVolumeDataProvider(request); + const logsVolumeOption: LogsVolumeOption = + options?.type === SupplementaryQueryType.LogsVolume ? options : { type }; + return this.getLogsVolumeDataProvider(request, logsVolumeOption); case SupplementaryQueryType.LogsSample: - return this.getLogsSampleDataProvider(request); + const logsSampleOption: LogsSampleOptions = + options?.type === SupplementaryQueryType.LogsSample ? options : { type }; + return this.getLogsSampleDataProvider(request, logsSampleOption); default: return undefined; } @@ -207,6 +214,7 @@ export class LokiDatasource } const dropErrorExpression = `${expr} | drop __error__`; + const field = options.field || 'level'; if (isQueryWithError(this.interpolateString(dropErrorExpression, placeHolderScopedVars)) === false) { expr = dropErrorExpression; } @@ -216,7 +224,8 @@ export class LokiDatasource refId: `${REF_ID_STARTER_LOG_VOLUME}${normalizedQuery.refId}`, queryType: LokiQueryType.Range, supportingQueryType: SupportingQueryType.LogsVolume, - expr: `sum by (level) (count_over_time(${expr}[$__auto]))`, + expr: `sum by (${field}) (count_over_time(${expr}[$__auto]))`, + legendFormat: `{{ ${field} }}`, }; case SupplementaryQueryType.LogsSample: @@ -242,10 +251,13 @@ export class LokiDatasource * Private method used in the `getDataProvider` for DataSourceWithSupplementaryQueriesSupport, specifically for Logs volume queries. * @returns An Observable of DataQueryResponse or undefined if no suitable queries are found. */ - private getLogsVolumeDataProvider(request: DataQueryRequest): DataQueryRequest | undefined { + private getLogsVolumeDataProvider( + request: DataQueryRequest, + options: LogsVolumeOption + ): DataQueryRequest | undefined { const logsVolumeRequest = cloneDeep(request); const targets = logsVolumeRequest.targets - .map((query) => this.getSupplementaryQuery({ type: SupplementaryQueryType.LogsVolume }, query)) + .map((query) => this.getSupplementaryQuery(options, query)) .filter((query): query is LokiQuery => !!query); if (!targets.length) { @@ -259,7 +271,10 @@ export class LokiDatasource * Private method used in the `getDataProvider` for DataSourceWithSupplementaryQueriesSupport, specifically for Logs sample queries. * @returns An Observable of DataQueryResponse or undefined if no suitable queries are found. */ - private getLogsSampleDataProvider(request: DataQueryRequest): DataQueryRequest | undefined { + private getLogsSampleDataProvider( + request: DataQueryRequest, + options?: LogsSampleOptions + ): DataQueryRequest | undefined { const logsSampleRequest = cloneDeep(request); const targets = logsSampleRequest.targets .map((query) => this.getSupplementaryQuery({ type: SupplementaryQueryType.LogsSample, limit: 100 }, query)) From 2c5b72e8446b045ec8242c2ea8b1690a0b295ff0 Mon Sep 17 00:00:00 2001 From: Ieva Date: Wed, 6 Mar 2024 12:40:48 +0000 Subject: [PATCH 0421/1406] AuthZ: add headers for IP range AC checks for data source proxy requests (#81662) * add a middleware that appens headers for IP range AC to data source proxy requests * update code * add tests * fix a mistake * add logging * refactor to reuse code * small cleanup * skip the plugins middleware if the header is already set * skip the plugins middleware if the header is already set --- .../grafana_request_id_header_middleware.go | 34 ++++++ ...afana_request_id_header_middleware_test.go | 113 ++++++++++++++++++ .../http_client_provider.go | 4 + .../grafana_request_id_header_middleware.go | 67 +++++++---- pkg/setting/setting.go | 3 + 5 files changed, 197 insertions(+), 24 deletions(-) create mode 100644 pkg/infra/httpclient/httpclientprovider/grafana_request_id_header_middleware.go create mode 100644 pkg/infra/httpclient/httpclientprovider/grafana_request_id_header_middleware_test.go diff --git a/pkg/infra/httpclient/httpclientprovider/grafana_request_id_header_middleware.go b/pkg/infra/httpclient/httpclientprovider/grafana_request_id_header_middleware.go new file mode 100644 index 0000000000..0075578ae8 --- /dev/null +++ b/pkg/infra/httpclient/httpclientprovider/grafana_request_id_header_middleware.go @@ -0,0 +1,34 @@ +package httpclientprovider + +import ( + "net/http" + + sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/pluginsintegration/clientmiddleware" + "github.com/grafana/grafana/pkg/setting" +) + +const GrafanaRequestIDHeaderMiddlewareName = "grafana-request-id-header-middleware" + +func GrafanaRequestIDHeaderMiddleware(cfg *setting.Cfg, logger log.Logger) sdkhttpclient.Middleware { + return sdkhttpclient.NamedMiddlewareFunc(GrafanaRequestIDHeaderMiddlewareName, func(opts sdkhttpclient.Options, next http.RoundTripper) http.RoundTripper { + return sdkhttpclient.RoundTripperFunc(func(req *http.Request) (*http.Response, error) { + if req.Header.Get(clientmiddleware.GrafanaRequestID) != "" { + logger.Debug("Request already has a Grafana request ID header", "request_id", req.Header.Get(clientmiddleware.GrafanaRequestID)) + return next.RoundTrip(req) + } + + if !clientmiddleware.IsRequestURLInAllowList(req.URL, cfg) { + logger.Debug("Data source URL not among the allow-listed URLs", "url", req.URL.String()) + return next.RoundTrip(req) + } + + for k, v := range clientmiddleware.GetGrafanaRequestIDHeaders(req, cfg, logger) { + req.Header.Set(k, v) + } + + return next.RoundTrip(req) + }) + }) +} diff --git a/pkg/infra/httpclient/httpclientprovider/grafana_request_id_header_middleware_test.go b/pkg/infra/httpclient/httpclientprovider/grafana_request_id_header_middleware_test.go new file mode 100644 index 0000000000..92c995915b --- /dev/null +++ b/pkg/infra/httpclient/httpclientprovider/grafana_request_id_header_middleware_test.go @@ -0,0 +1,113 @@ +package httpclientprovider + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "net/http" + "net/url" + "testing" + + "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/pluginsintegration/clientmiddleware" + "github.com/grafana/grafana/pkg/setting" + "github.com/stretchr/testify/require" +) + +func TestGrafanaRequestIDHeaderMiddleware(t *testing.T) { + testCases := []struct { + description string + allowedURLs []*url.URL + requestURL string + remoteAddress string + expectGrafanaRequestIDHeaders bool + expectPrivateRequestHeader bool + }{ + { + description: "With target URL in the allowed URL list and remote address specified, should add headers to the request but the request should not be marked as private", + allowedURLs: []*url.URL{{ + Scheme: "https", + Host: "grafana.com", + }}, + requestURL: "https://grafana.com/api/some/path", + remoteAddress: "1.2.3.4", + expectGrafanaRequestIDHeaders: true, + expectPrivateRequestHeader: false, + }, + { + description: "With target URL in the allowed URL list and remote address not specified, should add headers to the request and the request should be marked as private", + allowedURLs: []*url.URL{{ + Scheme: "https", + Host: "grafana.com", + }}, + requestURL: "https://grafana.com/api/some/path", + expectGrafanaRequestIDHeaders: true, + expectPrivateRequestHeader: true, + }, + { + description: "With target URL not in the allowed URL list, should not add headers to the request", + allowedURLs: []*url.URL{{ + Scheme: "https", + Host: "grafana.com", + }}, + requestURL: "https://fake-grafana.com/api/some/path", + expectGrafanaRequestIDHeaders: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.description, func(t *testing.T) { + ctx := &testContext{} + finalRoundTripper := ctx.createRoundTripper("final") + cfg := setting.NewCfg() + cfg.IPRangeACEnabled = false + cfg.IPRangeACAllowedURLs = tc.allowedURLs + cfg.IPRangeACSecretKey = "secret" + mw := GrafanaRequestIDHeaderMiddleware(cfg, log.New("test")) + rt := mw.CreateMiddleware(httpclient.Options{}, finalRoundTripper) + require.NotNil(t, rt) + middlewareName, ok := mw.(httpclient.MiddlewareName) + require.True(t, ok) + require.Equal(t, GrafanaRequestIDHeaderMiddlewareName, middlewareName.MiddlewareName()) + + req, err := http.NewRequest(http.MethodGet, tc.requestURL, nil) + require.NoError(t, err) + req.RemoteAddr = tc.remoteAddress + res, err := rt.RoundTrip(req) + require.NoError(t, err) + require.NotNil(t, res) + if res.Body != nil { + require.NoError(t, res.Body.Close()) + } + require.Len(t, ctx.callChain, 1) + require.ElementsMatch(t, []string{"final"}, ctx.callChain) + + if !tc.expectGrafanaRequestIDHeaders { + require.Len(t, req.Header.Values(clientmiddleware.GrafanaRequestID), 0) + require.Len(t, req.Header.Values(clientmiddleware.GrafanaSignedRequestID), 0) + } else { + require.Len(t, req.Header.Values(clientmiddleware.GrafanaRequestID), 1) + require.Len(t, req.Header.Values(clientmiddleware.GrafanaSignedRequestID), 1) + requestID := req.Header.Get(clientmiddleware.GrafanaRequestID) + + instance := hmac.New(sha256.New, []byte(cfg.IPRangeACSecretKey)) + _, err = instance.Write([]byte(requestID)) + require.NoError(t, err) + computed := hex.EncodeToString(instance.Sum(nil)) + + require.Equal(t, req.Header.Get(clientmiddleware.GrafanaSignedRequestID), computed) + + if tc.remoteAddress == "" { + require.Equal(t, req.Header.Get(clientmiddleware.GrafanaInternalRequest), "true") + } else { + require.Len(t, req.Header.Values(clientmiddleware.XRealIPHeader), 1) + require.Equal(t, req.Header.Get(clientmiddleware.XRealIPHeader), tc.remoteAddress) + + // Internal header should not be set + require.Len(t, req.Header.Values(clientmiddleware.GrafanaInternalRequest), 0) + } + } + }) + } +} diff --git a/pkg/infra/httpclient/httpclientprovider/http_client_provider.go b/pkg/infra/httpclient/httpclientprovider/http_client_provider.go index 16c7b19704..69a9f2a1c7 100644 --- a/pkg/infra/httpclient/httpclientprovider/http_client_provider.go +++ b/pkg/infra/httpclient/httpclientprovider/http_client_provider.go @@ -39,6 +39,10 @@ func New(cfg *setting.Cfg, validator validations.PluginRequestValidator, tracer middlewares = append(middlewares, HTTPLoggerMiddleware(cfg.PluginSettings)) } + if cfg.IPRangeACEnabled { + middlewares = append(middlewares, GrafanaRequestIDHeaderMiddleware(cfg, logger)) + } + setDefaultTimeoutOptions(cfg) return newProviderFunc(sdkhttpclient.ProviderOptions{ diff --git a/pkg/services/pluginsintegration/clientmiddleware/grafana_request_id_header_middleware.go b/pkg/services/pluginsintegration/clientmiddleware/grafana_request_id_header_middleware.go index 7637c8b051..15c2a1c1cc 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/grafana_request_id_header_middleware.go +++ b/pkg/services/pluginsintegration/clientmiddleware/grafana_request_id_header_middleware.go @@ -5,6 +5,7 @@ import ( "crypto/hmac" "crypto/sha256" "encoding/hex" + "net/http" "net/url" "github.com/google/uuid" @@ -55,45 +56,63 @@ func (m *HostedGrafanaACHeaderMiddleware) applyGrafanaRequestIDHeader(ctx contex m.log.Debug("Failed to parse data source URL", "error", err) return } - foundMatch := false - for _, allowedURL := range m.cfg.IPRangeACAllowedURLs { - // Only look at the scheme and host, ignore the path - if allowedURL.Host == dsBaseURL.Host && allowedURL.Scheme == dsBaseURL.Scheme { - foundMatch = true - break - } - } - if !foundMatch { + if !IsRequestURLInAllowList(dsBaseURL, m.cfg) { m.log.Debug("Data source URL not among the allow-listed URLs", "url", dsBaseURL.String()) return } + var req *http.Request + reqCtx := contexthandler.FromContext(ctx) + if reqCtx != nil { + req = reqCtx.Req + } + for k, v := range GetGrafanaRequestIDHeaders(req, m.cfg, m.log) { + h.SetHTTPHeader(k, v) + } +} + +func IsRequestURLInAllowList(url *url.URL, cfg *setting.Cfg) bool { + for _, allowedURL := range cfg.IPRangeACAllowedURLs { + // Only look at the scheme and host, ignore the path + if allowedURL.Host == url.Host && allowedURL.Scheme == url.Scheme { + return true + } + } + return false +} + +func GetGrafanaRequestIDHeaders(req *http.Request, cfg *setting.Cfg, logger log.Logger) map[string]string { // Generate a new Grafana request ID and sign it with the secret key uid, err := uuid.NewRandom() if err != nil { - m.log.Debug("Failed to generate Grafana request ID", "error", err) - return + logger.Debug("Failed to generate Grafana request ID", "error", err) + return nil } grafanaRequestID := uid.String() - hmac := hmac.New(sha256.New, []byte(m.cfg.IPRangeACSecretKey)) + hmac := hmac.New(sha256.New, []byte(cfg.IPRangeACSecretKey)) if _, err := hmac.Write([]byte(grafanaRequestID)); err != nil { - m.log.Debug("Failed to sign IP range access control header", "error", err) - return + logger.Debug("Failed to sign IP range access control header", "error", err) + return nil } signedGrafanaRequestID := hex.EncodeToString(hmac.Sum(nil)) - h.SetHTTPHeader(GrafanaSignedRequestID, signedGrafanaRequestID) - h.SetHTTPHeader(GrafanaRequestID, grafanaRequestID) - reqCtx := contexthandler.FromContext(ctx) - if reqCtx != nil && reqCtx.Req != nil { - remoteAddress := web.RemoteAddr(reqCtx.Req) - if remoteAddress != "" { - h.SetHTTPHeader(XRealIPHeader, remoteAddress) - return - } + headers := make(map[string]string) + headers[GrafanaRequestID] = grafanaRequestID + headers[GrafanaSignedRequestID] = signedGrafanaRequestID + + // If the remote address is not specified, treat the request as internal + remoteAddress := "" + if req != nil { + remoteAddress = web.RemoteAddr(req) } - h.SetHTTPHeader(GrafanaInternalRequest, "true") + if remoteAddress != "" { + headers[XRealIPHeader] = remoteAddress + } else { + headers[GrafanaInternalRequest] = "true" + } + + return headers } func (m *HostedGrafanaACHeaderMiddleware) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index d6a3b52bec..916b296540 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -1944,6 +1944,9 @@ func (cfg *Cfg) readDataSourceSecuritySettings() { datasources := cfg.Raw.Section("datasources.ip_range_security") cfg.IPRangeACEnabled = datasources.Key("enabled").MustBool(false) cfg.IPRangeACSecretKey = datasources.Key("secret_key").MustString("") + if cfg.IPRangeACEnabled && cfg.IPRangeACSecretKey == "" { + cfg.Logger.Error("IP range access control is enabled but no secret key is set") + } allowedURLString := datasources.Key("allow_list").MustString("") for _, urlString := range util.SplitString(allowedURLString) { allowedURL, err := url.Parse(urlString) From d269b4bf0dd74613e1c5ea0b783c90c0f91df2c9 Mon Sep 17 00:00:00 2001 From: Victor Marin <36818606+mdvictor@users.noreply.github.com> Date: Wed, 6 Mar 2024 14:47:18 +0200 Subject: [PATCH 0422/1406] Scenes: Copy/paste library panels (#83962) * Scenes: Copy/paste library panels * more tests --- .../scene/DashboardScene.test.tsx | 101 ++++++++++++++++-- .../dashboard-scene/scene/DashboardScene.tsx | 35 +++++- 2 files changed, 125 insertions(+), 11 deletions(-) diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx index 51fbbbc874..fd93852044 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx @@ -17,7 +17,11 @@ import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; import { VariablesChanged } from 'app/features/variables/types'; import { createWorker } from '../saving/createDetectChangesWorker'; -import { buildGridItemForPanel, transformSaveModelToScene } from '../serialization/transformSaveModelToScene'; +import { + buildGridItemForLibPanel, + buildGridItemForPanel, + transformSaveModelToScene, +} from '../serialization/transformSaveModelToScene'; import { DecoratedRevisionModel } from '../settings/VersionsEditView'; import { historySrv } from '../settings/version-history/HistorySrv'; import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; @@ -25,6 +29,7 @@ import { djb2Hash } from '../utils/djb2Hash'; import { DashboardControls } from './DashboardControls'; import { DashboardScene, DashboardSceneState } from './DashboardScene'; +import { LibraryVizPanel } from './LibraryVizPanel'; jest.mock('../settings/version-history/HistorySrv'); jest.mock('../serialization/transformSaveModelToScene'); @@ -244,7 +249,7 @@ describe('DashboardScene', () => { const body = scene.state.body as SceneGridLayout; const gridItem = body.state.children[0] as SceneGridItem; - expect(body.state.children.length).toBe(5); + expect(body.state.children.length).toBe(6); expect(gridItem.state.body!.state.key).toBe('panel-5'); expect(gridItem.state.y).toBe(0); }); @@ -255,7 +260,7 @@ describe('DashboardScene', () => { const body = scene.state.body as SceneGridLayout; const gridItem = body.state.children[0] as SceneGridItem; - expect(body.state.children.length).toBe(5); + expect(body.state.children.length).toBe(6); expect(gridItem.state.body!.state.key).toBe('panel-5'); }); @@ -265,7 +270,7 @@ describe('DashboardScene', () => { const body = scene.state.body as SceneGridLayout; const gridRow = body.state.children[0] as SceneGridRow; - expect(body.state.children.length).toBe(3); + expect(body.state.children.length).toBe(4); expect(gridRow.state.key).toBe('panel-5'); expect(gridRow.state.children[0].state.key).toBe('griditem-1'); expect(gridRow.state.children[1].state.key).toBe('griditem-2'); @@ -313,7 +318,7 @@ describe('DashboardScene', () => { const body = scene.state.body as SceneGridLayout; const gridRow = body.state.children[0] as SceneGridRow; - expect(body.state.children.length).toBe(4); + expect(body.state.children.length).toBe(5); expect(gridRow.state.children.length).toBe(0); }); @@ -333,6 +338,36 @@ describe('DashboardScene', () => { expect(gridRow.state.children.length).toBe(0); }); + it('Should fail to copy a panel if it does not have a grid item parent', () => { + const vizPanel = new VizPanel({ + title: 'Panel Title', + key: 'panel-5', + pluginId: 'timeseries', + }); + + scene.copyPanel(vizPanel); + + expect(scene.state.hasCopiedPanel).toBe(false); + }); + + it('Should fail to copy a library panel if it does not have a grid item parent', () => { + const libVizPanel = new LibraryVizPanel({ + uid: 'uid', + name: 'libraryPanel', + panelKey: 'panel-4', + title: 'Library Panel', + panel: new VizPanel({ + title: 'Library Panel', + key: 'panel-4', + pluginId: 'table', + }), + }); + + scene.copyPanel(libVizPanel.state.panel as VizPanel); + + expect(scene.state.hasCopiedPanel).toBe(false); + }); + it('Should copy a panel', () => { const vizPanel = ((scene.state.body as SceneGridLayout).state.children[0] as SceneGridItem).state.body; scene.copyPanel(vizPanel as VizPanel); @@ -340,6 +375,15 @@ describe('DashboardScene', () => { expect(scene.state.hasCopiedPanel).toBe(true); }); + it('Should copy a library viz panel', () => { + const libVizPanel = ((scene.state.body as SceneGridLayout).state.children[4] as SceneGridItem).state + .body as LibraryVizPanel; + + scene.copyPanel(libVizPanel.state.panel as VizPanel); + + expect(scene.state.hasCopiedPanel).toBe(true); + }); + it('Should paste a panel', () => { scene.setState({ hasCopiedPanel: true }); jest.spyOn(JSON, 'parse').mockReturnThis(); @@ -359,19 +403,49 @@ describe('DashboardScene', () => { const body = scene.state.body as SceneGridLayout; const gridItem = body.state.children[0] as SceneGridItem; - expect(body.state.children.length).toBe(5); + expect(buildGridItemForPanel).toHaveBeenCalledTimes(1); + expect(body.state.children.length).toBe(6); expect(gridItem.state.body!.state.key).toBe('panel-5'); expect(gridItem.state.y).toBe(0); expect(scene.state.hasCopiedPanel).toBe(false); }); + it('Should paste a library viz panel', () => { + scene.setState({ hasCopiedPanel: true }); + jest.spyOn(JSON, 'parse').mockReturnValue({ libraryPanel: { uid: 'uid', name: 'libraryPanel' } }); + jest.mocked(buildGridItemForLibPanel).mockReturnValue( + new SceneGridItem({ + body: new LibraryVizPanel({ + title: 'Library Panel', + uid: 'uid', + name: 'libraryPanel', + panelKey: 'panel-4', + }), + }) + ); + + scene.pastePanel(); + + const body = scene.state.body as SceneGridLayout; + const gridItem = body.state.children[0] as SceneGridItem; + + const libVizPanel = gridItem.state.body as LibraryVizPanel; + + expect(buildGridItemForLibPanel).toHaveBeenCalledTimes(1); + expect(body.state.children.length).toBe(6); + expect(libVizPanel.state.panelKey).toBe('panel-5'); + expect(libVizPanel.state.panel?.state.key).toBe('panel-5'); + expect(gridItem.state.y).toBe(0); + expect(scene.state.hasCopiedPanel).toBe(false); + }); + it('Should create a new add library panel widget', () => { scene.onCreateLibPanelWidget(); const body = scene.state.body as SceneGridLayout; const gridItem = body.state.children[0] as SceneGridItem; - expect(body.state.children.length).toBe(5); + expect(body.state.children.length).toBe(6); expect(gridItem.state.body!.state.key).toBe('panel-5'); expect(gridItem.state.y).toBe(0); }); @@ -575,6 +649,19 @@ function buildTestScene(overrides?: Partial) { $data: new SceneQueryRunner({ key: 'data-query-runner2', queries: [{ refId: 'A' }] }), }), }), + new SceneGridItem({ + body: new LibraryVizPanel({ + uid: 'uid', + name: 'libraryPanel', + panelKey: 'panel-4', + title: 'Library Panel', + panel: new VizPanel({ + title: 'Library Panel', + key: 'panel-4', + pluginId: 'table', + }), + }), + }), ], }), ...overrides, diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index 51c6eec0c0..c5ddb5ecfe 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -33,7 +33,11 @@ import { PanelEditor } from '../panel-edit/PanelEditor'; import { DashboardSceneChangeTracker } from '../saving/DashboardSceneChangeTracker'; import { SaveDashboardDrawer } from '../saving/SaveDashboardDrawer'; import { DashboardSceneRenderer } from '../scene/DashboardSceneRenderer'; -import { buildGridItemForPanel, transformSaveModelToScene } from '../serialization/transformSaveModelToScene'; +import { + buildGridItemForLibPanel, + buildGridItemForPanel, + transformSaveModelToScene, +} from '../serialization/transformSaveModelToScene'; import { gridItemToPanel } from '../serialization/transformSceneToSaveModel'; import { DecoratedRevisionModel } from '../settings/VersionsEditView'; import { DashboardEditView } from '../settings/utils'; @@ -57,6 +61,7 @@ import { import { AddLibraryPanelWidget } from './AddLibraryPanelWidget'; import { DashboardControls } from './DashboardControls'; import { DashboardSceneUrlSync } from './DashboardSceneUrlSync'; +import { LibraryVizPanel } from './LibraryVizPanel'; import { PanelRepeaterGridItem } from './PanelRepeaterGridItem'; import { ViewPanelScene } from './ViewPanelScene'; import { setupKeyboardShortcuts } from './keyboardShortcuts'; @@ -480,7 +485,17 @@ export class DashboardScene extends SceneObjectBase { return; } - const gridItem = vizPanel.parent; + let gridItem = vizPanel.parent; + + if (vizPanel.parent instanceof LibraryVizPanel) { + const libraryVizPanel = vizPanel.parent; + + if (!libraryVizPanel.parent) { + return; + } + + gridItem = libraryVizPanel.parent; + } const jsonData = gridItemToPanel(gridItem); @@ -497,8 +512,10 @@ export class DashboardScene extends SceneObjectBase { const jsonData = store.get(LS_PANEL_COPY_KEY); const jsonObj = JSON.parse(jsonData); const panelModel = new PanelModel(jsonObj); + const gridItem = !panelModel.libraryPanel + ? buildGridItemForPanel(panelModel) + : buildGridItemForLibPanel(panelModel); - const gridItem = buildGridItemForPanel(panelModel); const sceneGridLayout = this.state.body; if (!(gridItem instanceof SceneGridItem) && !(gridItem instanceof PanelRepeaterGridItem)) { @@ -507,7 +524,17 @@ export class DashboardScene extends SceneObjectBase { const panelId = dashboardSceneGraph.getNextPanelId(this); - if (gridItem instanceof SceneGridItem && gridItem.state.body) { + if (gridItem instanceof SceneGridItem && gridItem.state.body instanceof LibraryVizPanel) { + const panelKey = getVizPanelKeyForPanelId(panelId); + + gridItem.state.body.setState({ panelKey }); + + const vizPanel = gridItem.state.body.state.panel; + + if (vizPanel instanceof VizPanel) { + vizPanel.setState({ key: panelKey }); + } + } else if (gridItem instanceof SceneGridItem && gridItem.state.body) { gridItem.state.body.setState({ key: getVizPanelKeyForPanelId(panelId), }); From 6db7eafd7e12fbe91ab6e7126baba1b2913ab496 Mon Sep 17 00:00:00 2001 From: Giordano Ricci Date: Wed, 6 Mar 2024 13:00:56 +0000 Subject: [PATCH 0423/1406] Explore: Remove plus icon from Add button (#83587) --- .../app/features/explore/ExploreToolbar.tsx | 7 +-- .../extensions/ToolbarExtensionPoint.test.tsx | 43 +++++-------------- .../extensions/ToolbarExtensionPoint.tsx | 13 ++---- 3 files changed, 15 insertions(+), 48 deletions(-) diff --git a/public/app/features/explore/ExploreToolbar.tsx b/public/app/features/explore/ExploreToolbar.tsx index 360024a0e4..f88010d9d6 100644 --- a/public/app/features/explore/ExploreToolbar.tsx +++ b/public/app/features/explore/ExploreToolbar.tsx @@ -273,12 +273,7 @@ export function ExploreToolbar({ exploreId, onChangeTime, onContentOutlineToogle ), - , + , !isLive && ( { }); it('should render "Add" extension point menu button', () => { - renderWithExploreStore(); + renderWithExploreStore(); expect(screen.getByRole('button', { name: 'Add' })).toBeVisible(); }); - it('should render menu with extensions when "Add" is clicked in split mode', async () => { - renderWithExploreStore(); - - await userEvent.click(screen.getByRole('button', { name: 'Add' })); - - expect(screen.getByRole('group', { name: 'Dashboards' })).toBeVisible(); - expect(screen.getByRole('menuitem', { name: 'Add to dashboard' })).toBeVisible(); - expect(screen.getByRole('menuitem', { name: 'ML: Forecast' })).toBeVisible(); - }); - it('should render menu with extensions when "Add" is clicked', async () => { - renderWithExploreStore(); + renderWithExploreStore(); await userEvent.click(screen.getByRole('button', { name: 'Add' })); @@ -104,7 +94,7 @@ describe('ToolbarExtensionPoint', () => { }); it('should call onClick from extension when menu item is clicked', async () => { - renderWithExploreStore(); + renderWithExploreStore(); await userEvent.click(screen.getByRole('button', { name: 'Add' })); await userEvent.click(screen.getByRole('menuitem', { name: 'Add to dashboard' })); @@ -116,7 +106,7 @@ describe('ToolbarExtensionPoint', () => { }); it('should render confirm navigation modal when extension with path is clicked', async () => { - renderWithExploreStore(); + renderWithExploreStore(); await userEvent.click(screen.getByRole('button', { name: 'Add' })); await userEvent.click(screen.getByRole('menuitem', { name: 'ML: Forecast' })); @@ -130,7 +120,7 @@ describe('ToolbarExtensionPoint', () => { const targets = [{ refId: 'A' }]; const data = createEmptyQueryResponse(); - renderWithExploreStore(, { + renderWithExploreStore(, { targets, data, }); @@ -155,7 +145,7 @@ describe('ToolbarExtensionPoint', () => { const targets = [{ refId: 'A' }]; const data = createEmptyQueryResponse(); - renderWithExploreStore(, { + renderWithExploreStore(, { targets, data, }); @@ -167,7 +157,7 @@ describe('ToolbarExtensionPoint', () => { }); it('should correct extension point id when fetching extensions', async () => { - renderWithExploreStore(); + renderWithExploreStore(); const [options] = getPluginLinkExtensionsMock.mock.calls[0]; const { extensionPointId } = options; @@ -201,24 +191,13 @@ describe('ToolbarExtensionPoint', () => { }); it('should render "Add" extension point menu button', () => { - renderWithExploreStore(); + renderWithExploreStore(); expect(screen.getByRole('button', { name: 'Add' })).toBeVisible(); }); - it('should render "Add" extension point menu button in split mode', async () => { - renderWithExploreStore(); - - await userEvent.click(screen.getByRole('button', { name: 'Add' })); - - // Make sure we don't have anything related to categories rendered - expect(screen.queryAllByRole('group').length).toBe(0); - expect(screen.getByRole('menuitem', { name: 'Dashboard' })).toBeVisible(); - expect(screen.getByRole('menuitem', { name: 'ML: Forecast' })).toBeVisible(); - }); - it('should render menu with extensions when "Add" is clicked', async () => { - renderWithExploreStore(); + renderWithExploreStore(); await userEvent.click(screen.getByRole('button', { name: 'Add' })); @@ -236,7 +215,7 @@ describe('ToolbarExtensionPoint', () => { }); it('should render "add to dashboard" action button if one pane is visible', async () => { - renderWithExploreStore(); + renderWithExploreStore(); await waitFor(() => { const button = screen.getByRole('button', { name: /add to dashboard/i }); @@ -254,7 +233,7 @@ describe('ToolbarExtensionPoint', () => { }); it('should not render "add to dashboard" action button', async () => { - renderWithExploreStore(); + renderWithExploreStore(); expect(screen.queryByRole('button', { name: /add to dashboard/i })).not.toBeInTheDocument(); }); diff --git a/public/app/features/explore/extensions/ToolbarExtensionPoint.tsx b/public/app/features/explore/extensions/ToolbarExtensionPoint.tsx index 5e45b4977b..1a46d53854 100644 --- a/public/app/features/explore/extensions/ToolbarExtensionPoint.tsx +++ b/public/app/features/explore/extensions/ToolbarExtensionPoint.tsx @@ -19,11 +19,10 @@ const AddToDashboard = lazy(() => type Props = { exploreId: string; timeZone: TimeZone; - splitted: boolean; }; export function ToolbarExtensionPoint(props: Props): ReactElement | null { - const { exploreId, splitted } = props; + const { exploreId } = props; const [selectedExtension, setSelectedExtension] = useState(); const [isOpen, setIsOpen] = useState(false); const context = useExtensionPointContext(props); @@ -54,14 +53,8 @@ export function ToolbarExtensionPoint(props: Props): ReactElement | null { return ( <> - - {splitted ? ' ' : 'Add'} + + Add {!!selectedExtension && !!selectedExtension.path && ( From 183aa09eeb4bb9a4134130419cd18add4e38a94e Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Wed, 6 Mar 2024 13:57:11 +0000 Subject: [PATCH 0424/1406] Dashboards: Fix scroll position not being restored when leaving panel edit (#83787) * Dashboards: Fix scroll position not being restored when leaving panel edit view * remove mock from tests * remove console log * Remove my debugging stuff, and don't render grid if width is 0 * remove old comment (but retain old, probably unneeded css) * rename ref * fix it not actually working anymore!!! * add e2e tests * jsonnet, i guess --- .../scenarios/tall_dashboard.json | 1791 +++++++++++++++++ devenv/jsonnet/dev-dashboards.libsonnet | 1 + .../general-dashboards.spec.ts | 33 + .../containers/DashboardPage.test.tsx | 13 - .../dashboard/dashgrid/DashboardGrid.test.tsx | 13 - .../dashboard/dashgrid/DashboardGrid.tsx | 97 +- 6 files changed, 1874 insertions(+), 74 deletions(-) create mode 100644 devenv/dev-dashboards/scenarios/tall_dashboard.json create mode 100644 e2e/dashboards-suite/general-dashboards.spec.ts diff --git a/devenv/dev-dashboards/scenarios/tall_dashboard.json b/devenv/dev-dashboards/scenarios/tall_dashboard.json new file mode 100644 index 0000000000..ca87850c2b --- /dev/null +++ b/devenv/dev-dashboards/scenarios/tall_dashboard.json @@ -0,0 +1,1791 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 3, + "links": [], + "panels": [ + { + "datasource": { + "type": "grafana", + "uid": "grafana" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "Dashboard panel 1", + "mode": "markdown" + }, + "pluginVersion": "11.0.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "queryType": "randomWalk", + "refId": "A" + } + ], + "title": "Panel #1", + "type": "text" + }, + { + "datasource": { + "type": "grafana", + "uid": "grafana" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 8 + }, + "id": 2, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "Dashboard panel 2", + "mode": "markdown" + }, + "pluginVersion": "11.0.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "queryType": "randomWalk", + "refId": "A" + } + ], + "title": "Panel #2", + "type": "text" + }, + { + "datasource": { + "type": "grafana", + "uid": "grafana" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 16 + }, + "id": 3, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "Dashboard panel 3", + "mode": "markdown" + }, + "pluginVersion": "11.0.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "queryType": "randomWalk", + "refId": "A" + } + ], + "title": "Panel #3", + "type": "text" + }, + { + "datasource": { + "type": "grafana", + "uid": "grafana" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 24 + }, + "id": 4, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "Dashboard panel 4", + "mode": "markdown" + }, + "pluginVersion": "11.0.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "queryType": "randomWalk", + "refId": "A" + } + ], + "title": "Panel #4", + "type": "text" + }, + { + "datasource": { + "type": "grafana", + "uid": "grafana" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 32 + }, + "id": 5, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "Dashboard panel 5", + "mode": "markdown" + }, + "pluginVersion": "11.0.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "queryType": "randomWalk", + "refId": "A" + } + ], + "title": "Panel #5", + "type": "text" + }, + { + "datasource": { + "type": "grafana", + "uid": "grafana" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 40 + }, + "id": 6, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "Dashboard panel 6", + "mode": "markdown" + }, + "pluginVersion": "11.0.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "queryType": "randomWalk", + "refId": "A" + } + ], + "title": "Panel #6", + "type": "text" + }, + { + "datasource": { + "type": "grafana", + "uid": "grafana" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 48 + }, + "id": 7, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "Dashboard panel 7", + "mode": "markdown" + }, + "pluginVersion": "11.0.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "queryType": "randomWalk", + "refId": "A" + } + ], + "title": "Panel #7", + "type": "text" + }, + { + "datasource": { + "type": "grafana", + "uid": "grafana" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 56 + }, + "id": 8, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "Dashboard panel 8", + "mode": "markdown" + }, + "pluginVersion": "11.0.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "queryType": "randomWalk", + "refId": "A" + } + ], + "title": "Panel #8", + "type": "text" + }, + { + "datasource": { + "type": "grafana", + "uid": "grafana" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 64 + }, + "id": 9, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "Dashboard panel 9", + "mode": "markdown" + }, + "pluginVersion": "11.0.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "queryType": "randomWalk", + "refId": "A" + } + ], + "title": "Panel #9", + "type": "text" + }, + { + "datasource": { + "type": "grafana", + "uid": "grafana" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 72 + }, + "id": 10, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "Dashboard panel 10", + "mode": "markdown" + }, + "pluginVersion": "11.0.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "queryType": "randomWalk", + "refId": "A" + } + ], + "title": "Panel #10", + "type": "text" + }, + { + "datasource": { + "type": "grafana", + "uid": "grafana" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 80 + }, + "id": 11, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "Dashboard panel 11", + "mode": "markdown" + }, + "pluginVersion": "11.0.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "queryType": "randomWalk", + "refId": "A" + } + ], + "title": "Panel #11", + "type": "text" + }, + { + "datasource": { + "type": "grafana", + "uid": "grafana" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 88 + }, + "id": 12, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "Dashboard panel 12", + "mode": "markdown" + }, + "pluginVersion": "11.0.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "queryType": "randomWalk", + "refId": "A" + } + ], + "title": "Panel #12", + "type": "text" + }, + { + "datasource": { + "type": "grafana", + "uid": "grafana" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 96 + }, + "id": 13, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "Dashboard panel 13", + "mode": "markdown" + }, + "pluginVersion": "11.0.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "queryType": "randomWalk", + "refId": "A" + } + ], + "title": "Panel #13", + "type": "text" + }, + { + "datasource": { + "type": "grafana", + "uid": "grafana" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 104 + }, + "id": 14, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "Dashboard panel 14", + "mode": "markdown" + }, + "pluginVersion": "11.0.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "queryType": "randomWalk", + "refId": "A" + } + ], + "title": "Panel #14", + "type": "text" + }, + { + "datasource": { + "type": "grafana", + "uid": "grafana" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 112 + }, + "id": 15, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "Dashboard panel 15", + "mode": "markdown" + }, + "pluginVersion": "11.0.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "queryType": "randomWalk", + "refId": "A" + } + ], + "title": "Panel #15", + "type": "text" + }, + { + "datasource": { + "type": "grafana", + "uid": "grafana" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 120 + }, + "id": 16, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "Dashboard panel 16", + "mode": "markdown" + }, + "pluginVersion": "11.0.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "queryType": "randomWalk", + "refId": "A" + } + ], + "title": "Panel #16", + "type": "text" + }, + { + "datasource": { + "type": "grafana", + "uid": "grafana" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 128 + }, + "id": 17, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "Dashboard panel 17", + "mode": "markdown" + }, + "pluginVersion": "11.0.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "queryType": "randomWalk", + "refId": "A" + } + ], + "title": "Panel #17", + "type": "text" + }, + { + "datasource": { + "type": "grafana", + "uid": "grafana" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 136 + }, + "id": 18, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "Dashboard panel 18", + "mode": "markdown" + }, + "pluginVersion": "11.0.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "queryType": "randomWalk", + "refId": "A" + } + ], + "title": "Panel #18", + "type": "text" + }, + { + "datasource": { + "type": "grafana", + "uid": "grafana" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 144 + }, + "id": 19, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "Dashboard panel 19", + "mode": "markdown" + }, + "pluginVersion": "11.0.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "queryType": "randomWalk", + "refId": "A" + } + ], + "title": "Panel #19", + "type": "text" + }, + { + "datasource": { + "type": "grafana", + "uid": "grafana" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 152 + }, + "id": 20, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "Dashboard panel 20", + "mode": "markdown" + }, + "pluginVersion": "11.0.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "queryType": "randomWalk", + "refId": "A" + } + ], + "title": "Panel #20", + "type": "text" + }, + { + "datasource": { + "type": "grafana", + "uid": "grafana" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 160 + }, + "id": 21, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "Dashboard panel 21", + "mode": "markdown" + }, + "pluginVersion": "11.0.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "queryType": "randomWalk", + "refId": "A" + } + ], + "title": "Panel #21", + "type": "text" + }, + { + "datasource": { + "type": "grafana", + "uid": "grafana" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 168 + }, + "id": 22, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "Dashboard panel 22", + "mode": "markdown" + }, + "pluginVersion": "11.0.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "queryType": "randomWalk", + "refId": "A" + } + ], + "title": "Panel #22", + "type": "text" + }, + { + "datasource": { + "type": "grafana", + "uid": "grafana" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 176 + }, + "id": 23, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "Dashboard panel 23", + "mode": "markdown" + }, + "pluginVersion": "11.0.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "queryType": "randomWalk", + "refId": "A" + } + ], + "title": "Panel #23", + "type": "text" + }, + { + "datasource": { + "type": "grafana", + "uid": "grafana" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 184 + }, + "id": 24, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "Dashboard panel 24", + "mode": "markdown" + }, + "pluginVersion": "11.0.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "queryType": "randomWalk", + "refId": "A" + } + ], + "title": "Panel #24", + "type": "text" + }, + { + "datasource": { + "type": "grafana", + "uid": "grafana" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 192 + }, + "id": 25, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "Dashboard panel 25", + "mode": "markdown" + }, + "pluginVersion": "11.0.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "queryType": "randomWalk", + "refId": "A" + } + ], + "title": "Panel #25", + "type": "text" + }, + { + "datasource": { + "type": "grafana", + "uid": "grafana" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 200 + }, + "id": 26, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "Dashboard panel 26", + "mode": "markdown" + }, + "pluginVersion": "11.0.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "queryType": "randomWalk", + "refId": "A" + } + ], + "title": "Panel #26", + "type": "text" + }, + { + "datasource": { + "type": "grafana", + "uid": "grafana" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 208 + }, + "id": 27, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "Dashboard panel 27", + "mode": "markdown" + }, + "pluginVersion": "11.0.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "queryType": "randomWalk", + "refId": "A" + } + ], + "title": "Panel #27", + "type": "text" + }, + { + "datasource": { + "type": "grafana", + "uid": "grafana" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 216 + }, + "id": 28, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "Dashboard panel 28", + "mode": "markdown" + }, + "pluginVersion": "11.0.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "queryType": "randomWalk", + "refId": "A" + } + ], + "title": "Panel #28", + "type": "text" + }, + { + "datasource": { + "type": "grafana", + "uid": "grafana" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 224 + }, + "id": 29, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "Dashboard panel 29", + "mode": "markdown" + }, + "pluginVersion": "11.0.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "queryType": "randomWalk", + "refId": "A" + } + ], + "title": "Panel #29", + "type": "text" + }, + { + "datasource": { + "type": "grafana", + "uid": "grafana" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 232 + }, + "id": 30, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "Dashboard panel 30", + "mode": "markdown" + }, + "pluginVersion": "11.0.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "queryType": "randomWalk", + "refId": "A" + } + ], + "title": "Panel #30", + "type": "text" + }, + { + "datasource": { + "type": "grafana", + "uid": "grafana" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 240 + }, + "id": 31, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "Dashboard panel 31", + "mode": "markdown" + }, + "pluginVersion": "11.0.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "queryType": "randomWalk", + "refId": "A" + } + ], + "title": "Panel #31", + "type": "text" + }, + { + "datasource": { + "type": "grafana", + "uid": "grafana" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 248 + }, + "id": 32, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "Dashboard panel 32", + "mode": "markdown" + }, + "pluginVersion": "11.0.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "queryType": "randomWalk", + "refId": "A" + } + ], + "title": "Panel #32", + "type": "text" + }, + { + "datasource": { + "type": "grafana", + "uid": "grafana" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 256 + }, + "id": 33, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "Dashboard panel 33", + "mode": "markdown" + }, + "pluginVersion": "11.0.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "queryType": "randomWalk", + "refId": "A" + } + ], + "title": "Panel #33", + "type": "text" + }, + { + "datasource": { + "type": "grafana", + "uid": "grafana" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 264 + }, + "id": 34, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "Dashboard panel 34", + "mode": "markdown" + }, + "pluginVersion": "11.0.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "queryType": "randomWalk", + "refId": "A" + } + ], + "title": "Panel #34", + "type": "text" + }, + { + "datasource": { + "type": "grafana", + "uid": "grafana" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 272 + }, + "id": 35, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "Dashboard panel 35", + "mode": "markdown" + }, + "pluginVersion": "11.0.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "queryType": "randomWalk", + "refId": "A" + } + ], + "title": "Panel #35", + "type": "text" + }, + { + "datasource": { + "type": "grafana", + "uid": "grafana" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 280 + }, + "id": 36, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "Dashboard panel 36", + "mode": "markdown" + }, + "pluginVersion": "11.0.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "queryType": "randomWalk", + "refId": "A" + } + ], + "title": "Panel #36", + "type": "text" + }, + { + "datasource": { + "type": "grafana", + "uid": "grafana" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 288 + }, + "id": 37, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "Dashboard panel 37", + "mode": "markdown" + }, + "pluginVersion": "11.0.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "queryType": "randomWalk", + "refId": "A" + } + ], + "title": "Panel #37", + "type": "text" + }, + { + "datasource": { + "type": "grafana", + "uid": "grafana" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 296 + }, + "id": 38, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "Dashboard panel 38", + "mode": "markdown" + }, + "pluginVersion": "11.0.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "queryType": "randomWalk", + "refId": "A" + } + ], + "title": "Panel #38", + "type": "text" + }, + { + "datasource": { + "type": "grafana", + "uid": "grafana" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 304 + }, + "id": 39, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "Dashboard panel 39", + "mode": "markdown" + }, + "pluginVersion": "11.0.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "queryType": "randomWalk", + "refId": "A" + } + ], + "title": "Panel #39", + "type": "text" + }, + { + "datasource": { + "type": "grafana", + "uid": "grafana" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 312 + }, + "id": 40, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "Dashboard panel 40", + "mode": "markdown" + }, + "pluginVersion": "11.0.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "queryType": "randomWalk", + "refId": "A" + } + ], + "title": "Panel #40", + "type": "text" + }, + { + "datasource": { + "type": "grafana", + "uid": "grafana" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 320 + }, + "id": 41, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "Dashboard panel 41", + "mode": "markdown" + }, + "pluginVersion": "11.0.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "queryType": "randomWalk", + "refId": "A" + } + ], + "title": "Panel #41", + "type": "text" + }, + { + "datasource": { + "type": "grafana", + "uid": "grafana" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 328 + }, + "id": 42, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "Dashboard panel 42", + "mode": "markdown" + }, + "pluginVersion": "11.0.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "queryType": "randomWalk", + "refId": "A" + } + ], + "title": "Panel #42", + "type": "text" + }, + { + "datasource": { + "type": "grafana", + "uid": "grafana" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 336 + }, + "id": 43, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "Dashboard panel 43", + "mode": "markdown" + }, + "pluginVersion": "11.0.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "queryType": "randomWalk", + "refId": "A" + } + ], + "title": "Panel #43", + "type": "text" + }, + { + "datasource": { + "type": "grafana", + "uid": "grafana" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 344 + }, + "id": 44, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "Dashboard panel 44", + "mode": "markdown" + }, + "pluginVersion": "11.0.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "queryType": "randomWalk", + "refId": "A" + } + ], + "title": "Panel #44", + "type": "text" + }, + { + "datasource": { + "type": "grafana", + "uid": "grafana" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 352 + }, + "id": 45, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "Dashboard panel 45", + "mode": "markdown" + }, + "pluginVersion": "11.0.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "queryType": "randomWalk", + "refId": "A" + } + ], + "title": "Panel #45", + "type": "text" + }, + { + "datasource": { + "type": "grafana", + "uid": "grafana" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 360 + }, + "id": 46, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "Dashboard panel 46", + "mode": "markdown" + }, + "pluginVersion": "11.0.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "queryType": "randomWalk", + "refId": "A" + } + ], + "title": "Panel #46", + "type": "text" + }, + { + "datasource": { + "type": "grafana", + "uid": "grafana" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 368 + }, + "id": 47, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "Dashboard panel 47", + "mode": "markdown" + }, + "pluginVersion": "11.0.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "queryType": "randomWalk", + "refId": "A" + } + ], + "title": "Panel #47", + "type": "text" + }, + { + "datasource": { + "type": "grafana", + "uid": "grafana" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 376 + }, + "id": 48, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "Dashboard panel 48", + "mode": "markdown" + }, + "pluginVersion": "11.0.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "queryType": "randomWalk", + "refId": "A" + } + ], + "title": "Panel #48", + "type": "text" + }, + { + "datasource": { + "type": "grafana", + "uid": "grafana" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 384 + }, + "id": 49, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "Dashboard panel 49", + "mode": "markdown" + }, + "pluginVersion": "11.0.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "queryType": "randomWalk", + "refId": "A" + } + ], + "title": "Panel #49", + "type": "text" + }, + { + "datasource": { + "type": "grafana", + "uid": "grafana" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 392 + }, + "id": 50, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "Dashboard panel 50", + "mode": "markdown" + }, + "pluginVersion": "11.0.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "queryType": "randomWalk", + "refId": "A" + } + ], + "title": "Panel #50", + "type": "text" + } + ], + "schemaVersion": 39, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timeRangeUpdatedDuringEditOrView": false, + "timepicker": {}, + "timezone": "browser", + "title": "A tall dashboard", + "uid": "edediimbjhdz4b", + "version": 1, + "weekStart": "" +} diff --git a/devenv/jsonnet/dev-dashboards.libsonnet b/devenv/jsonnet/dev-dashboards.libsonnet index d0efa7f906..de3ee9e822 100644 --- a/devenv/jsonnet/dev-dashboards.libsonnet +++ b/devenv/jsonnet/dev-dashboards.libsonnet @@ -91,6 +91,7 @@ "table_sparkline_cell": (import '../dev-dashboards/panel-table/table_sparkline_cell.json'), "table_tests": (import '../dev-dashboards/panel-table/table_tests.json'), "table_tests_new": (import '../dev-dashboards/panel-table/table_tests_new.json'), + "tall_dashboard": (import '../dev-dashboards/scenarios/tall_dashboard.json'), "templating-dashboard-links-and-variables": (import '../dev-dashboards/feature-templating/templating-dashboard-links-and-variables.json'), "templating-repeating-panels": (import '../dev-dashboards/feature-templating/templating-repeating-panels.json'), "templating-repeating-rows": (import '../dev-dashboards/feature-templating/templating-repeating-rows.json'), diff --git a/e2e/dashboards-suite/general-dashboards.spec.ts b/e2e/dashboards-suite/general-dashboards.spec.ts new file mode 100644 index 0000000000..00584030c2 --- /dev/null +++ b/e2e/dashboards-suite/general-dashboards.spec.ts @@ -0,0 +1,33 @@ +import { e2e } from '../utils'; + +const PAGE_UNDER_TEST = 'edediimbjhdz4b/a-tall-dashboard'; + +describe('Dashboards', () => { + beforeEach(() => { + e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD')); + }); + + it('should restore scroll position', () => { + e2e.flows.openDashboard({ uid: PAGE_UNDER_TEST }); + e2e.components.Panels.Panel.title('Panel #1').should('be.visible'); + + // scroll to the bottom + e2e.pages.Dashboard.DashNav.navV2() + .parent() + .parent() // Note, this will probably fail when we change the custom scrollbars + .scrollTo('bottom', { + timeout: 5 * 1000, + }); + + // The last panel should be visible... + e2e.components.Panels.Panel.title('Panel #50').should('be.visible'); + + // Then we open and close the panel editor + e2e.components.Panels.Panel.menu('Panel #50').click({ force: true }); // it only shows on hover + e2e.components.Panels.Panel.menuItems('Edit').click(); + e2e.components.PanelEditor.applyButton().click(); + + // And the last panel should still be visible! + e2e.components.Panels.Panel.title('Panel #50').should('be.visible'); + }); +}); diff --git a/public/app/features/dashboard/containers/DashboardPage.test.tsx b/public/app/features/dashboard/containers/DashboardPage.test.tsx index 6770c49fc7..24486c53e2 100644 --- a/public/app/features/dashboard/containers/DashboardPage.test.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.test.tsx @@ -4,7 +4,6 @@ import React from 'react'; import { Provider } from 'react-redux'; import { match, Router } from 'react-router-dom'; import { useEffectOnce } from 'react-use'; -import { Props as AutoSizerProps } from 'react-virtualized-auto-sizer'; import { mockToolkitActionCreator } from 'test/core/redux/mocks'; import { TestProvider } from 'test/helpers/TestProvider'; import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock'; @@ -71,18 +70,6 @@ jest.mock('@grafana/runtime', () => ({ getPluginLinkExtensions: jest.fn().mockReturnValue({ extensions: [] }), })); -jest.mock('react-virtualized-auto-sizer', () => { - // The size of the children need to be small enough to be outside the view. - // So it does not trigger the query to be run by the PanelQueryRunner. - return ({ children }: AutoSizerProps) => - children({ - height: 1, - scaledHeight: 1, - scaledWidth: 1, - width: 1, - }); -}); - function getTestDashboard(overrides?: Partial, metaOverrides?: Partial): DashboardModel { const data = Object.assign( { diff --git a/public/app/features/dashboard/dashgrid/DashboardGrid.test.tsx b/public/app/features/dashboard/dashgrid/DashboardGrid.test.tsx index 69d0a46ab6..818f0fd05d 100644 --- a/public/app/features/dashboard/dashgrid/DashboardGrid.test.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardGrid.test.tsx @@ -3,7 +3,6 @@ import React from 'react'; import { Provider } from 'react-redux'; import { Router } from 'react-router-dom'; import { useEffectOnce } from 'react-use'; -import { Props as AutoSizerProps } from 'react-virtualized-auto-sizer'; import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock'; import { TextBoxVariableModel } from '@grafana/data'; @@ -42,18 +41,6 @@ jest.mock('app/features/dashboard/dashgrid/LazyLoader', () => { return { LazyLoader }; }); -jest.mock('react-virtualized-auto-sizer', () => { - // The size of the children need to be small enough to be outside the view. - // So it does not trigger the query to be run by the PanelQueryRunner. - return ({ children }: AutoSizerProps) => - children({ - scaledHeight: 1, - height: 1, - scaledWidth: 1, - width: 1, - }); -}); - function setup(props: Props) { const context = getGrafanaContextMock(); const store = configureStore({}); diff --git a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx index 6ca7fc8191..52a08bb7a9 100644 --- a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx @@ -1,7 +1,6 @@ import classNames from 'classnames'; import React, { PureComponent, CSSProperties } from 'react'; import ReactGridLayout, { ItemCallback } from 'react-grid-layout'; -import AutoSizer from 'react-virtualized-auto-sizer'; import { Subscription } from 'rxjs'; import { config } from '@grafana/runtime'; @@ -31,6 +30,7 @@ export interface Props { interface State { panelFilter?: RegExp; + width: number; } export class DashboardGrid extends PureComponent { @@ -47,6 +47,7 @@ export class DashboardGrid extends PureComponent { super(props); this.state = { panelFilter: undefined, + width: document.body.clientWidth, // initial very rough estimate }; } @@ -291,22 +292,41 @@ export class DashboardGrid extends PureComponent { } }; + private resizeObserver?: ResizeObserver; + private rootEl: HTMLDivElement | null = null; + onMeasureRef = (rootEl: HTMLDivElement | null) => { + if (!rootEl) { + if (this.rootEl && this.resizeObserver) { + this.resizeObserver.unobserve(this.rootEl); + } + return; + } + + this.rootEl = rootEl; + this.resizeObserver = new ResizeObserver((entries) => { + entries.forEach((entry) => { + this.setState({ width: entry.contentRect.width }); + }); + }); + + this.resizeObserver.observe(rootEl); + }; + render() { const { isEditable, dashboard } = this.props; + const { width } = this.state; if (dashboard.panels.length === 0) { return ; } - /** - * We have a parent with "flex: 1 1 0" we need to reset it to "flex: 1 1 auto" to have the AutoSizer - * properly working. For more information go here: - * https://github.com/bvaughn/react-virtualized/blob/master/docs/usingAutoSizer.md#can-i-use-autosizer-within-a-flex-container - * - * pos: rel + z-index is required to create a new stacking context to contain the escalating z-indexes of the panels - */ + const draggable = width <= config.theme2.breakpoints.values.md ? false : isEditable; + + // pos: rel + z-index is required to create a new stacking context to contain + // the escalating z-indexes of the panels return (
{ display: this.props.editPanel ? 'none' : undefined, }} > - - {({ width }) => { - if (width === 0) { - return null; - } - - // Disable draggable if mobile device, solving an issue with unintentionally - // moving panels. https://github.com/grafana/grafana/issues/18497 - const draggable = width <= config.theme2.breakpoints.values.md ? false : isEditable; - - return ( - /** - * The children is using a width of 100% so we need to guarantee that it is wrapped - * in an element that has the calculated size given by the AutoSizer. The AutoSizer - * has a width of 0 and will let its content overflow its div. - */ -
- - {this.renderPanels(width, draggable)} - -
- ); - }} -
+
+ + {this.renderPanels(width, draggable)} + +
); } From 316601258a7d7a614f30f1c4293a91090ce32bf1 Mon Sep 17 00:00:00 2001 From: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> Date: Wed, 6 Mar 2024 09:06:23 -0500 Subject: [PATCH 0425/1406] Docs: comment youtube videos back in (#83974) * Commented 4 youtube links back in * Fixed id --- docs/sources/whatsnew/whats-new-in-v10-4.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/sources/whatsnew/whats-new-in-v10-4.md b/docs/sources/whatsnew/whats-new-in-v10-4.md index 434ea61f73..545d534868 100644 --- a/docs/sources/whatsnew/whats-new-in-v10-4.md +++ b/docs/sources/whatsnew/whats-new-in-v10-4.md @@ -58,7 +58,7 @@ Use the aforementioned tooling and warnings to plan migrations to React based [v To learn more, refer to the [Angular support deprecation](https://grafana.com/docs/grafana//developers/angular_deprecation/), which includes [recommended alternative plugins](https://grafana.com/docs/grafana//developers/angular_deprecation/angular-plugins/). - +{{< youtube id="XlEVs6g8dC8" >}} [Documentation](https://grafana.com/docs/grafana//developers/angular_deprecation/) @@ -148,7 +148,7 @@ When you're browsing Grafana - for example, exploring the dashboard and metrics Return to Previous is rolling out across Grafana Cloud now. To try Return to Previous in self-managed Grafana, turn on the `returnToPrevious` [feature toggle](https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/feature-toggles/) in Grafana v10.4 or newer. - +{{< youtube id="-Y3qPfD2wrA" >}} {{< admonition type="note" >}} The term **context** refers to applications in Grafana like Incident and OnCall, as well as core features like Explore and Dashboards. @@ -170,7 +170,7 @@ Simplified routing inherits the alert rule RBAC, increasing control over notific To try out Simplified Alert Notification Routing enable the `alertingSimplifiedRouting` feature toggle. - +{{< youtube id="uBBQ-_pWSNs" >}} ### Grafana Alerting upgrade with rule preview @@ -247,7 +247,7 @@ Screenshots: {{< figure src="/media/docs/plugins/PagerDuty-incidents-real-life-example.png" caption="Incidents annotations from PagerDuty data source on a dashboard panel" alt="Incidents annotations from PagerDuty data source on a dashboard panel" >}} - +{{< youtube id="dCklm2DaVqQ" >}} ### SurrealDB Data Source From 1dc6014b105690c8dbff3b4236e2c035d35541c7 Mon Sep 17 00:00:00 2001 From: Leon Sorokin Date: Wed, 6 Mar 2024 08:31:54 -0600 Subject: [PATCH 0426/1406] Dashboard: Revert descending z-index changes (#83466) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Torkel Ödegaard --- .../src/components/PanelChrome/PanelChrome.tsx | 9 --------- .../grafana-ui/src/themes/_variables.scss.tmpl.ts | 1 - .../SplitPaneWrapper/SplitPaneWrapper.tsx | 4 ---- .../dashboard-scene/scene/DashboardControls.tsx | 2 +- public/app/features/trails/DataTrail.tsx | 2 +- public/sass/_variables.generated.scss | 1 - public/sass/components/_dashboard_grid.scss | 14 +------------- 7 files changed, 3 insertions(+), 30 deletions(-) diff --git a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx index da00cbf970..4f4d690a42 100644 --- a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx +++ b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx @@ -358,15 +358,6 @@ const getStyles = (theme: GrafanaTheme2) => { display: 'flex', flexDirection: 'column', - '> *': { - zIndex: 0, - }, - - // matches .react-grid-item styles in _dashboard_grid.scss to ensure any contained tooltips occlude adjacent panels - '&:hover, &:active, &:focus': { - zIndex: theme.zIndex.activePanel, - }, - '.show-on-hover': { opacity: '0', visibility: 'hidden', diff --git a/packages/grafana-ui/src/themes/_variables.scss.tmpl.ts b/packages/grafana-ui/src/themes/_variables.scss.tmpl.ts index 5651553f12..75a8ad0d4a 100644 --- a/packages/grafana-ui/src/themes/_variables.scss.tmpl.ts +++ b/packages/grafana-ui/src/themes/_variables.scss.tmpl.ts @@ -161,7 +161,6 @@ $form-icon-danger: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www // ------------------------- // Used for a bird's eye view of components dependent on the z-axis // Try to avoid customizing these :) -$zindex-active-panel: ${theme.zIndex.activePanel}; $zindex-dropdown: ${theme.zIndex.dropdown}; $zindex-navbar-fixed: ${theme.zIndex.navbarFixed}; $zindex-sidemenu: ${theme.zIndex.sidemenu}; diff --git a/public/app/core/components/SplitPaneWrapper/SplitPaneWrapper.tsx b/public/app/core/components/SplitPaneWrapper/SplitPaneWrapper.tsx index 9605b6e772..54691d4484 100644 --- a/public/app/core/components/SplitPaneWrapper/SplitPaneWrapper.tsx +++ b/public/app/core/components/SplitPaneWrapper/SplitPaneWrapper.tsx @@ -91,7 +91,6 @@ export class SplitPaneWrapper extends PureComponent { return { - splitPane: css({ - overflow: 'visible !important', - }), resizer: css({ display: hasSplit ? 'block' : 'none', }), diff --git a/public/app/features/dashboard-scene/scene/DashboardControls.tsx b/public/app/features/dashboard-scene/scene/DashboardControls.tsx index 55e5e370eb..0ad76b3bff 100644 --- a/public/app/features/dashboard-scene/scene/DashboardControls.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardControls.tsx @@ -74,7 +74,7 @@ function getStyles(theme: GrafanaTheme2) { position: 'sticky', top: 0, background: theme.colors.background.canvas, - zIndex: theme.zIndex.activePanel, + zIndex: theme.zIndex.navbarFixed, padding: theme.spacing(2, 0), width: '100%', marginLeft: 'auto', diff --git a/public/app/features/trails/DataTrail.tsx b/public/app/features/trails/DataTrail.tsx index 30c2e01e23..bbdca58404 100644 --- a/public/app/features/trails/DataTrail.tsx +++ b/public/app/features/trails/DataTrail.tsx @@ -245,7 +245,7 @@ function getStyles(theme: GrafanaTheme2) { flexWrap: 'wrap', position: 'sticky', background: theme.isDark ? theme.colors.background.canvas : theme.colors.background.primary, - zIndex: theme.zIndex.activePanel + 1, + zIndex: theme.zIndex.navbarFixed, top: 0, }), }; diff --git a/public/sass/_variables.generated.scss b/public/sass/_variables.generated.scss index 290ebadaed..1a2e11a065 100644 --- a/public/sass/_variables.generated.scss +++ b/public/sass/_variables.generated.scss @@ -163,7 +163,6 @@ $form-icon-danger: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www // ------------------------- // Used for a bird's eye view of components dependent on the z-axis // Try to avoid customizing these :) -$zindex-active-panel: 999; $zindex-dropdown: 1030; $zindex-navbar-fixed: 1000; $zindex-sidemenu: 1020; diff --git a/public/sass/components/_dashboard_grid.scss b/public/sass/components/_dashboard_grid.scss index a68f95cd3b..7185a2a9be 100644 --- a/public/sass/components/_dashboard_grid.scss +++ b/public/sass/components/_dashboard_grid.scss @@ -13,7 +13,6 @@ &:hover { .react-resizable-handle { visibility: visible; - z-index: $zindex-active-panel; } } } @@ -86,22 +85,11 @@ } } -// Hack to prevent panel overlap during drag/hover (due to descending z-index assignment) -.react-grid-item:not(.context-menu-open, .resizing) { - &:hover, - &:active, - &:focus { - z-index: $zindex-active-panel !important; - } -} - // Hack for preventing panel menu overlapping. -.react-grid-item.context-menu-open, -.react-grid-item.resizing, .react-grid-item.resizing.panel, .react-grid-item.panel.dropdown-menu-open, .react-grid-item.react-draggable-dragging.panel { - z-index: $zindex-dropdown !important; + z-index: $zindex-dropdown; } // Disable animation on initial rendering and enable it when component has been mounted. From 7527d30ec91a3daa80e75c3a139444abb9c9bf08 Mon Sep 17 00:00:00 2001 From: Tom Ratcliffe Date: Wed, 6 Mar 2024 14:44:27 +0000 Subject: [PATCH 0427/1406] Documentation: Fix link to .nvmrc file in developer guide (#83911) Documentation: Fix link to .nvmrc file --- contribute/developer-guide.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contribute/developer-guide.md b/contribute/developer-guide.md index f31d4a91a3..40a20fc684 100644 --- a/contribute/developer-guide.md +++ b/contribute/developer-guide.md @@ -8,7 +8,7 @@ Make sure you have the following dependencies installed before setting up your d - [Git](https://git-scm.com/) - [Go](https://golang.org/dl/) (see [go.mod](../go.mod#L3) for minimum required version) -- [Node.js (Long Term Support)](https://nodejs.org), with [corepack enabled](https://nodejs.org/api/corepack.html#enabling-the-feature). See [.nvmrc](../nvm.rc) for supported version. It's recommend you use a version manager such as [nvm](https://github.com/nvm-sh/nvm), [fnm](https://github.com/Schniz/fnm), or similar. +- [Node.js (Long Term Support)](https://nodejs.org), with [corepack enabled](https://nodejs.org/api/corepack.html#enabling-the-feature). See [.nvmrc](../.nvmrc) for supported version. It's recommend you use a version manager such as [nvm](https://github.com/nvm-sh/nvm), [fnm](https://github.com/Schniz/fnm), or similar. - GCC (required for Cgo dependencies) ### macOS From 544bff2539f6c37f1a8dabb13bcebea96f68ecee Mon Sep 17 00:00:00 2001 From: Usman Ahmad Date: Wed, 6 Mar 2024 15:50:49 +0100 Subject: [PATCH 0428/1406] Docs/datasources usman (#83712) * changed tags from oss to enterprise and cloud * added Dashboard Panel example * swapped the all-grafana-umbrella information to the correct page * added minor visibility improvements in steps * made some minor adjustments * added minor improvements * fixed a link * updates links * Apply suggestions from code review thanks for the improved suggestions. looks more better Co-authored-by: Jack Baldry * fixed links * fixed Grafana Enterprise link * run prettier * fixed add a data source links --------- Co-authored-by: Chris Moyer Co-authored-by: Jack Baldry --- .../data-source-management/_index.md | 45 +++--------------- docs/sources/datasources/_index.md | 46 +++++++++++++++++-- .../getting-started/build-first-dashboard.md | 2 +- .../get-started-grafana-influxdb.md | 2 +- 4 files changed, 52 insertions(+), 43 deletions(-) diff --git a/docs/sources/administration/data-source-management/_index.md b/docs/sources/administration/data-source-management/_index.md index f4a9be6f23..b57424f245 100644 --- a/docs/sources/administration/data-source-management/_index.md +++ b/docs/sources/administration/data-source-management/_index.md @@ -10,7 +10,7 @@ description: Data source management information for Grafana administrators labels: products: - enterprise - - oss + - cloud title: Data source management weight: 100 --- @@ -21,27 +21,15 @@ Grafana supports many different storage backends for your time series data (data Refer to [data sources]({{< relref "../../datasources" >}}) for more information about using data sources in Grafana. Only users with the organization admin role can add data sources. -## Add a data source - -Before you can create your first dashboard, you need to add your data source. - -{{% admonition type="note" %}} -Only users with the organization admin role can add data sources. -{{% /admonition %}} - -**To add a data source:** - -1. Click **Connections** in the left-side menu. -1. Enter the name of a specific data source in the search dialog. You can filter by **Data source** to only see data sources. -1. Click the data source you want to add. -1. Configure the data source following instructions specific to that data source. - For links to data source-specific documentation, see [Data sources]({{< relref "../../datasources" >}}). ## Data source permissions You can configure data source permissions to allow or deny certain users the ability to query, edit, or administrate a data source. Each data source’s configuration includes a Permissions tab where you can restrict data source permissions to specific users, service accounts, teams, or roles. -Query permission allows users to query the data source. Edit permission allows users to query the data source, edit the data source’s configuration and delete the data source. Admin permission allows users to query and edit the data source, change permissions on the data source and enable or disable query caching for the data source. + +- The `query` permission allows users to query the data source. +- The `edit` permission allows users to query the data source, edit the data source’s configuration and delete the data source. +- The `admin` permission allows users to query and edit the data source, change permissions on the data source and enable or disable query caching for the data source. {{% admonition type="note" %}} Available in [Grafana Enterprise]({{< relref "../../introduction/grafana-enterprise/" >}}) and [Grafana Cloud](/docs/grafana-cloud). @@ -71,7 +59,7 @@ You can assign data source permissions to users, service accounts, teams, and ro 1. Click **Connections** in the left-side menu. 1. Under Your connections, click **Data sources**. 1. Select the data source for which you want to edit permissions. -1. On the Permissions tab, find the user, service account, team, or role permission you want to update. +1. On the Permissions tab, find the **User**, **Service Account**, **Team**, or **Role** permission you want to update. 1. Select a different option in the **Permission** dropdown.
@@ -81,7 +69,7 @@ You can assign data source permissions to users, service accounts, teams, and ro 1. Click **Connections** in the left-side menu. 1. Under Your connections, click **Data sources**. 1. Select the data source from which you want to remove permissions. -1. On the Permissions tab, find the user, service account, team, or role permission you want to remove. +1. On the Permissions tab, find the **User**, **Service Account**, **Team**, or **Role** permission you want to remove. 1. Click the **X** next to the permission.
@@ -178,22 +166,3 @@ This action impacts all cache-enabled data sources. If you are using Memcached, ### Sending a request without cache If a data source query request contains an `X-Cache-Skip` header, then Grafana skips the caching middleware, and does not search the cache for a response. This can be particularly useful when debugging data source queries using cURL. - -## Add data source plugins - -Grafana ships with several [built-in data sources]({{< relref "../../datasources#built-in-core-data-sources" >}}). -You can add additional data sources as plugins, which you can install or create yourself. - -### Find data source plugins in the plugin catalog - -To view available data source plugins, go to the [plugin catalog](/grafana/plugins/?type=datasource) and select the "Data sources" filter. -For details about the plugin catalog, refer to [Plugin management]({{< relref "../../administration/plugin-management/" >}}). - -You can further filter the plugin catalog's results for data sources provided by the Grafana community, Grafana Labs, and partners. -If you use [Grafana Enterprise]({{< relref "../../introduction/grafana-enterprise/" >}}), you can also filter by Enterprise-supported plugins. - -For more documentation on a specific data source plugin's features, including its query language and editor, refer to its plugin catalog page. - -### Create a data source plugin - -To build your own data source plugin, refer to the ["Build a data source plugin"](/developers/plugin-tools/tutorials/build-a-data-source-plugin) tutorial and our documentation about [building a plugin](/developers/plugin-tools). diff --git a/docs/sources/datasources/_index.md b/docs/sources/datasources/_index.md index d264340a5e..b738aa80a0 100644 --- a/docs/sources/datasources/_index.md +++ b/docs/sources/datasources/_index.md @@ -32,14 +32,29 @@ This documentation describes how to manage data sources in general, and how to configure or query the built-in data sources. For other data sources, refer to the list of [datasource plugins](/grafana/plugins/). -To develop a custom plugin, refer to [Build a plugin](/developers/plugin-tools). +To develop a custom plugin, refer to [Create a data source plugin](#create-a-data-source-plugin). ## Manage data sources Only users with the [organization administrator role][organization-roles] can add or remove data sources. To access data source management tools in Grafana as an administrator, navigate to **Configuration > Data Sources** in the Grafana sidebar. -For details on data source management, including instructions on how to add data sources and configure user permissions for queries, refer to the [administration documentation][data-source-management]. +For details on data source management, including instructions on how configure user permissions for queries, refer to the [administration documentation][data-source-management]. + +## Add a data source + +Before you can create your first dashboard, you need to add your data source. + +{{% admonition type="note" %}} +Only users with the organization admin role can add data sources. +{{% /admonition %}} + +**To add a data source:** + +1. Click **Connections** in the left-side menu. +1. Enter the name of a specific data source in the search dialog. You can filter by **Data source** to only see data sources. +1. Click the data source you want to add. +1. Configure the data source following instructions specific to that data source. ## Use query editors @@ -55,7 +70,7 @@ For example, this video demonstrates the visual Prometheus query builder: {{< vimeo 720004179 >}} -For general information about querying in Grafana, and common options and user interface elements across all query editors, refer to [Query and transform data][query-transform-data] . +For general information about querying in Grafana, and common options and user interface elements across all query editors, refer to [Query and transform data][query-transform-data]. ## Special data sources @@ -68,6 +83,7 @@ Grafana includes three special data sources: - You can't change an existing query to use the **Mixed** data source. - Grafana Play example: [Mixed data sources](https://play.grafana.org/d/000000100/mixed-datasources?orgId=1) - **Dashboard:** A data source that uses the result set from another panel in the same dashboard. The dashboard data source can use data either directly from the selected panel or from annotations attached to the selected panel. + - Grafana Play example: [Panel as Data source](https://play.grafana.org/d/ede8zps8ndb0gc/panel-as-data-source?orgId=1) ## Built-in core data sources @@ -91,6 +107,24 @@ These built-in core data sources are also included in the Grafana documentation: - [Testdata]({{< relref "./testdata" >}}) - [Zipkin]({{< relref "./zipkin" >}}) +## Add additional data source plugins + +You can add additional data sources as plugins (that are not available in core Grafana), which you can install or create yourself. + +### Find data source plugins in the plugin catalog + +To view available data source plugins, go to the [plugin catalog](/grafana/plugins/?type=datasource) and select the "Data sources" filter. +For details about the plugin catalog, refer to [Plugin management][Plugin-management]. + +You can further filter the plugin catalog's results for data sources provided by the Grafana community, Grafana Labs, and partners. +If you use [Grafana Enterprise][Grafana-Enterprise], you can also filter by Enterprise-supported plugins. + +For more documentation on a specific data source plugin's features, including its query language and editor, refer to its plugin catalog page. + +### Create a data source plugin + +To build your own data source plugin, refer to the [Build a data source plugin](/developers/plugin-tools/tutorials/build-a-data-source-plugin) tutorial and [Plugin tools](/developers/plugin-tools). + {{% docs/reference %}} [alerts]: "/docs/grafana/ -> /docs/grafana//alerting" [alerts]: "/docs/grafana-cloud/ -> /docs/grafana//alerting" @@ -109,4 +143,10 @@ These built-in core data sources are also included in the Grafana documentation: [query-transform-data]: "/docs/grafana/ -> /docs/grafana//panels-visualizations/query-transform-data" [query-transform-data]: "/docs/grafana-cloud/ -> /docs/grafana//panels-visualizations/query-transform-data" + +[Plugin-management]: "/docs/grafana/ -> /docs/grafana//administration/plugin-management" +[Plugin-management]: "/docs/grafana-cloud -> /docs/grafana//administration/plugin-management" + +[Grafana-Enterprise]: "/docs/grafana/ -> /docs/grafana//introduction/grafana-enterprise" + {{% /docs/reference %}} diff --git a/docs/sources/getting-started/build-first-dashboard.md b/docs/sources/getting-started/build-first-dashboard.md index 49dda7915a..fd76142ab5 100644 --- a/docs/sources/getting-started/build-first-dashboard.md +++ b/docs/sources/getting-started/build-first-dashboard.md @@ -77,7 +77,7 @@ Congratulations, you have created your first dashboard and it's displaying resul #### Next steps -Continue to experiment with what you have built, try the [explore workflow]({{< relref "../explore" >}}) or another visualization feature. Refer to [Data sources]({{< relref "../datasources" >}}) for a list of supported data sources and instructions on how to [add a data source]({{< relref "../administration/data-source-management#add-a-data-source" >}}). The following topics will be of interest to you: +Continue to experiment with what you have built, try the [explore workflow]({{< relref "../explore" >}}) or another visualization feature. Refer to [Data sources]({{< relref "../datasources" >}}) for a list of supported data sources and instructions on how to [add a data source]({{< relref "../datasources#add-a-data-source" >}}). The following topics will be of interest to you: - [Panels and visualizations]({{< relref "../panels-visualizations" >}}) - [Dashboards]({{< relref "../dashboards" >}}) diff --git a/docs/sources/getting-started/get-started-grafana-influxdb.md b/docs/sources/getting-started/get-started-grafana-influxdb.md index c257014ae3..d6a163a121 100644 --- a/docs/sources/getting-started/get-started-grafana-influxdb.md +++ b/docs/sources/getting-started/get-started-grafana-influxdb.md @@ -38,7 +38,7 @@ Windows users might need to make additional adjustments. Look for special instru You can have more than one InfluxDB data source defined in Grafana. -1. Follow the general instructions to [add a data source]({{< relref "../administration/data-source-management#add-a-data-source" >}}). +1. Follow the general instructions to [add a data source]({{< relref "../datasources#add-a-data-source" >}}). 1. Decide if you will use InfluxQL or Flux as your query language. - [Configure the data source]({{< relref "../datasources/influxdb#configure-the-data-source" >}}) for your chosen query language. Each query language has its own unique data source settings. From 0929bf7c00dd7b7a4c17d014455b36ad02f697a0 Mon Sep 17 00:00:00 2001 From: Kyle Brandt Date: Wed, 6 Mar 2024 09:57:01 -0500 Subject: [PATCH 0429/1406] Prometheus: Remove < and > from Query Builder Label Matcher operations (#83981) They are not valid promql label matcher operations --- .../src/querybuilder/components/LabelFilterItem.tsx | 2 -- .../prometheus/querybuilder/components/LabelFilterItem.tsx | 2 -- 2 files changed, 4 deletions(-) diff --git a/packages/grafana-prometheus/src/querybuilder/components/LabelFilterItem.tsx b/packages/grafana-prometheus/src/querybuilder/components/LabelFilterItem.tsx index 85b2ccd053..c1a28d8f96 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/LabelFilterItem.tsx +++ b/packages/grafana-prometheus/src/querybuilder/components/LabelFilterItem.tsx @@ -189,8 +189,6 @@ export function LabelFilterItem({ const operators = [ { label: '=', value: '=', isMultiValue: false }, { label: '!=', value: '!=', isMultiValue: false }, - { label: '<', value: '<', isMultiValue: false }, - { label: '>', value: '>', isMultiValue: false }, { label: '=~', value: '=~', isMultiValue: true }, { label: '!~', value: '!~', isMultiValue: true }, ]; diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/LabelFilterItem.tsx b/public/app/plugins/datasource/prometheus/querybuilder/components/LabelFilterItem.tsx index a39bbe042a..1a4bf53bc1 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/components/LabelFilterItem.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/components/LabelFilterItem.tsx @@ -190,8 +190,6 @@ export function LabelFilterItem({ const operators = [ { label: '=', value: '=', isMultiValue: false }, { label: '!=', value: '!=', isMultiValue: false }, - { label: '<', value: '<', isMultiValue: false }, - { label: '>', value: '>', isMultiValue: false }, { label: '=~', value: '=~', isMultiValue: true }, { label: '!~', value: '!~', isMultiValue: true }, ]; From 6a4e0c692ab26f4d4cb99ae615013e0b6e23f90b Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Wed, 6 Mar 2024 15:06:47 +0000 Subject: [PATCH 0430/1406] Page: Use browser native scrollbars for the main page content (#82919) * remove custom scroll bars from Page component * make flagged scroller the actual scrolling element, * enable feature flag by default * re-enable the scroll props in Page * rename feature toggle * fix css * only update when deleted * set .scrollbar-view on our scrolling wrapper --------- Co-authored-by: Ryan McKinley --- .betterer.results | 6 --- .../feature-toggles/index.md | 1 + .../src/types/featureToggles.gen.ts | 1 + pkg/services/featuremgmt/registry.go | 8 +++ pkg/services/featuremgmt/toggles_gen.csv | 1 + pkg/services/featuremgmt/toggles_gen.go | 4 ++ pkg/services/featuremgmt/toggles_gen.json | 13 +++++ pkg/services/featuremgmt/toggles_gen_test.go | 2 +- public/app/app.ts | 6 ++- .../app/core/components/FlaggedScroller.tsx | 54 +++++++++++++++++++ public/app/core/components/Page/Page.tsx | 18 ++++--- public/app/core/components/Page/types.ts | 10 +++- .../panel/gettingstarted/components/Step.tsx | 48 ++++++++--------- public/sass/components/_scrollbar.scss | 1 + 14 files changed, 133 insertions(+), 40 deletions(-) create mode 100644 public/app/core/components/FlaggedScroller.tsx diff --git a/.betterer.results b/.betterer.results index 48f308f57b..65afa3201e 100644 --- a/.betterer.results +++ b/.betterer.results @@ -5834,12 +5834,6 @@ exports[`better eslint`] = { [0, 0, 0, "Styles should be written using objects.", "3"], [0, 0, 0, "Styles should be written using objects.", "4"] ], - "public/app/plugins/panel/gettingstarted/components/Step.tsx:5381": [ - [0, 0, 0, "Styles should be written using objects.", "0"], - [0, 0, 0, "Styles should be written using objects.", "1"], - [0, 0, 0, "Styles should be written using objects.", "2"], - [0, 0, 0, "Styles should be written using objects.", "3"] - ], "public/app/plugins/panel/gettingstarted/components/TutorialCard.tsx:5381": [ [0, 0, 0, "Styles should be written using objects.", "0"], [0, 0, 0, "Styles should be written using objects.", "1"], 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 556a0ad684..284141a103 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -56,6 +56,7 @@ Some features are enabled by default. You can disable these feature by setting t | `lokiQueryHints` | Enables query hints for Loki | Yes | | `alertingPreviewUpgrade` | Show Unified Alerting preview and upgrade page in legacy alerting | Yes | | `alertingQueryOptimization` | Optimizes eligible queries in order to reduce load on datasources | | +| `betterPageScrolling` | Removes CustomScrollbar from the UI, relying on native browser scrollbars | Yes | | `alertingUpgradeDryrunOnStart` | When activated in legacy alerting mode, this initiates a dry-run of the Unified Alerting upgrade during each startup. It logs any issues detected without implementing any actual changes. | Yes | ## Preview feature toggles diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index b2eb5ceabc..ec9b91f931 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -178,6 +178,7 @@ export interface FeatureToggles { kubernetesAggregator?: boolean; expressionParser?: boolean; groupByVariable?: boolean; + betterPageScrolling?: boolean; alertingUpgradeDryrunOnStart?: boolean; scopeFilters?: boolean; } diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 5dc122d423..a0955fb2d9 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1188,6 +1188,14 @@ var ( HideFromDocs: true, HideFromAdminPage: true, }, + { + Name: "betterPageScrolling", + Description: "Removes CustomScrollbar from the UI, relying on native browser scrollbars", + Stage: FeatureStageGeneralAvailability, + FrontendOnly: true, + Owner: grafanaFrontendPlatformSquad, + Expression: "true", // enabled by default + }, { Name: "alertingUpgradeDryrunOnStart", Description: "When activated in legacy alerting mode, this initiates a dry-run of the Unified Alerting upgrade during each startup. It logs any issues detected without implementing any actual changes.", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index a53756fa9a..786175591a 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -159,5 +159,6 @@ newPDFRendering,experimental,@grafana/sharing-squad,false,false,false kubernetesAggregator,experimental,@grafana/grafana-app-platform-squad,false,true,false expressionParser,experimental,@grafana/grafana-app-platform-squad,false,true,false groupByVariable,experimental,@grafana/dashboards-squad,false,false,false +betterPageScrolling,GA,@grafana/grafana-frontend-platform,false,false,true alertingUpgradeDryrunOnStart,GA,@grafana/alerting-squad,false,true,false scopeFilters,experimental,@grafana/dashboards-squad,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 5041079396..ca3958ffae 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -647,6 +647,10 @@ const ( // Enable groupBy variable support in scenes dashboards FlagGroupByVariable = "groupByVariable" + // FlagBetterPageScrolling + // Removes CustomScrollbar from the UI, relying on native browser scrollbars + FlagBetterPageScrolling = "betterPageScrolling" + // FlagAlertingUpgradeDryrunOnStart // When activated in legacy alerting mode, this initiates a dry-run of the Unified Alerting upgrade during each startup. It logs any issues detected without implementing any actual changes. FlagAlertingUpgradeDryrunOnStart = "alertingUpgradeDryrunOnStart" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 810e3edaba..df1d76b9e8 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -2087,6 +2087,19 @@ "stage": "experimental", "codeowner": "@grafana/grafana-app-platform-squad" } + }, + { + "metadata": { + "name": "betterPageScrolling", + "resourceVersion": "1709583501630", + "creationTimestamp": "2024-03-04T20:18:21Z" + }, + "spec": { + "description": "Removes CustomScrollbar from the UI, relying on native browser scrollbars", + "stage": "GA", + "codeowner": "@grafana/grafana-frontend-platform", + "frontend": true + } } ] } \ No newline at end of file diff --git a/pkg/services/featuremgmt/toggles_gen_test.go b/pkg/services/featuremgmt/toggles_gen_test.go index e1014d7906..6881e21d07 100644 --- a/pkg/services/featuremgmt/toggles_gen_test.go +++ b/pkg/services/featuremgmt/toggles_gen_test.go @@ -126,7 +126,7 @@ func TestFeatureToggleFiles(t *testing.T) { item.Annotations[utils.AnnoKeyUpdatedTimestamp] = created.String() item.Spec = v // the current value } - } else { + } else if item.DeletionTimestamp == nil { item.DeletionTimestamp = &created fmt.Printf("mark feature as deleted") } diff --git a/public/app/app.ts b/public/app/app.ts index 30229d999c..0de34f8fe6 100644 --- a/public/app/app.ts +++ b/public/app/app.ts @@ -132,7 +132,11 @@ export class GrafanaApp { initIconCache(); // This needs to be done after the `initEchoSrv` since it is being used under the hood. startMeasure('frontend_app_init'); - addClassIfNoOverlayScrollbar(); + + if (!config.featureToggles.betterPageScrolling) { + addClassIfNoOverlayScrollbar(); + } + setLocale(config.bootData.user.locale); setWeekStart(config.bootData.user.weekStart); setPanelRenderer(PanelRenderer); diff --git a/public/app/core/components/FlaggedScroller.tsx b/public/app/core/components/FlaggedScroller.tsx new file mode 100644 index 0000000000..f7dbdd85dc --- /dev/null +++ b/public/app/core/components/FlaggedScroller.tsx @@ -0,0 +1,54 @@ +import { css, cx } from '@emotion/css'; +import React, { useEffect, useRef } from 'react'; + +import { config } from '@grafana/runtime'; +import { CustomScrollbar, useStyles2 } from '@grafana/ui'; + +type FlaggedScrollerProps = Parameters[0]; + +export default function FlaggedScrollbar(props: FlaggedScrollerProps) { + if (config.featureToggles.betterPageScrolling) { + return {props.children}; + } + + return ; +} + +// Shim to provide API-compatibility for Page's scroll-related props +function NativeScrollbar({ children, scrollRefCallback, scrollTop }: FlaggedScrollerProps) { + const styles = useStyles2(getStyles); + const ref = useRef(null); + + useEffect(() => { + if (ref.current && scrollRefCallback) { + scrollRefCallback(ref.current); + } + }, [ref, scrollRefCallback]); + + useEffect(() => { + if (ref.current && scrollTop != null) { + ref.current?.scrollTo(0, scrollTop); + } + }, [scrollTop]); + + return ( + // Set the .scrollbar-view class to help e2e tests find this, like in CustomScrollbar +
+ {children} +
+ ); +} + +function getStyles() { + return { + nativeScrollbars: css({ + label: 'native-scroll-container', + minHeight: `calc(100% + 0px)`, // I don't know, just copied from custom scrollbars + maxHeight: `calc(100% + 0px)`, // I don't know, just copied from custom scrollbars + display: 'flex', + flexDirection: 'column', + flexGrow: 1, + overflow: 'auto', + }), + }; +} diff --git a/public/app/core/components/Page/Page.tsx b/public/app/core/components/Page/Page.tsx index 5114fc0b4f..7f017fba48 100644 --- a/public/app/core/components/Page/Page.tsx +++ b/public/app/core/components/Page/Page.tsx @@ -1,11 +1,12 @@ -// Libraries import { css, cx } from '@emotion/css'; import React, { useLayoutEffect } from 'react'; import { GrafanaTheme2, PageLayoutType } from '@grafana/data'; -import { CustomScrollbar, useStyles2 } from '@grafana/ui'; +import { useStyles2 } from '@grafana/ui'; import { useGrafana } from 'app/core/context/GrafanaContext'; +import FlaggedScrollbar from '../FlaggedScroller'; + import { PageContents } from './PageContents'; import { PageHeader } from './PageHeader'; import { PageTabs } from './PageTabs'; @@ -52,7 +53,7 @@ export const Page: PageType = ({ return (
{layout === PageLayoutType.Standard && ( - +
{pageHeaderNav && ( }
{children}
-
+ )} + {layout === PageLayoutType.Canvas && ( - +
{children}
-
+ )} + {layout === PageLayoutType.Custom && children}
); @@ -95,6 +98,9 @@ const getStyles = (theme: GrafanaTheme2) => { label: 'page-content', flexGrow: 1, }), + primaryBg: css({ + background: theme.colors.background.primary, + }), pageInner: css({ label: 'page-inner', padding: theme.spacing(2), diff --git a/public/app/core/components/Page/types.ts b/public/app/core/components/Page/types.ts index aa76f10af7..4705291cbc 100644 --- a/public/app/core/components/Page/types.ts +++ b/public/app/core/components/Page/types.ts @@ -20,9 +20,15 @@ export interface PageProps extends HTMLAttributes { subTitle?: React.ReactNode; /** Control the page layout. */ layout?: PageLayoutType; - /** Can be used to get the scroll container element to access scroll position */ + /** + * Can be used to get the scroll container element to access scroll position + * */ + // Probably will deprecate this in the future in favor of just scrolling document.body directly scrollRef?: RefCallback; - /** Can be used to update the current scroll position */ + /** + * Can be used to update the current scroll position + * */ + // Probably will deprecate this in the future in favor of just scrolling document.body directly scrollTop?: number; } diff --git a/public/app/plugins/panel/gettingstarted/components/Step.tsx b/public/app/plugins/panel/gettingstarted/components/Step.tsx index a8a15fd7cf..e76a73e36f 100644 --- a/public/app/plugins/panel/gettingstarted/components/Step.tsx +++ b/public/app/plugins/panel/gettingstarted/components/Step.tsx @@ -37,30 +37,30 @@ export const Step = ({ step }: Props) => { const getStyles = (theme: GrafanaTheme2) => { return { - setup: css` - display: flex; - width: 95%; - `, - info: css` - width: 172px; - margin-right: 5%; + setup: css({ + display: 'flex', + width: '95%', + }), + info: css({ + width: '172px', + marginRight: '5%', - ${theme.breakpoints.down('xxl')} { - margin-right: ${theme.spacing(4)}; - } - ${theme.breakpoints.down('sm')} { - display: none; - } - `, - title: css` - color: ${theme.v1.palette.blue95}; - `, - cards: css` - overflow-x: scroll; - overflow-y: hidden; - width: 100%; - display: flex; - justify-content: flex-start; - `, + [theme.breakpoints.down('xxl')]: { + marginRight: theme.spacing(4), + }, + [theme.breakpoints.down('sm')]: { + display: 'none', + }, + }), + title: css({ + color: theme.v1.palette.blue95, + }), + cards: css({ + overflowX: 'auto', + overflowY: 'hidden', + width: '100%', + display: 'flex', + justifyContent: 'flex-start', + }), }; }; diff --git a/public/sass/components/_scrollbar.scss b/public/sass/components/_scrollbar.scss index ee242a0d9a..edb379ae5b 100644 --- a/public/sass/components/_scrollbar.scss +++ b/public/sass/components/_scrollbar.scss @@ -115,6 +115,7 @@ } // Scrollbars +// Note, this is not applied by default if the `betterPageScrolling` feature flag is applied .no-overlay-scrollbar { ::-webkit-scrollbar { width: 8px; From 146ad85a564d21ff40269f0a6325ab938da61397 Mon Sep 17 00:00:00 2001 From: "grafana-delivery-bot[bot]" <132647405+grafana-delivery-bot[bot]@users.noreply.github.com> Date: Wed, 6 Mar 2024 15:14:51 +0000 Subject: [PATCH 0431/1406] Changelog: Updated changelog for 10.4.0 (#83987) Co-authored-by: grafanabot --- CHANGELOG.md | 227 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 227 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fa7beac869..711cc52b46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,230 @@ + + +# 10.4.0 (2024-03-06) + +### Features and enhancements + +- **Chore:** Improve domain validation for Google OAuth - Backport 83229 to v10.4.x. [#83726](https://github.com/grafana/grafana/issues/83726), [@linoman](https://github.com/linoman) +- **DataQuery:** Track panel plugin id not type. [#83164](https://github.com/grafana/grafana/issues/83164), [@torkelo](https://github.com/torkelo) +- **AuthToken:** Remove client token rotation feature toggle. [#82886](https://github.com/grafana/grafana/issues/82886), [@kalleep](https://github.com/kalleep) +- **Plugins:** Enable feature toggle angularDeprecationUI by default. [#82880](https://github.com/grafana/grafana/issues/82880), [@xnyo](https://github.com/xnyo) +- **Table Component:** Improve text-wrapping behavior of cells. [#82872](https://github.com/grafana/grafana/issues/82872), [@ahuarte47](https://github.com/ahuarte47) +- **Alerting:** Dry-run legacy upgrade on startup. [#82835](https://github.com/grafana/grafana/issues/82835), [@JacobsonMT](https://github.com/JacobsonMT) +- **Tempo:** Upgrade @grafana/lezer-traceql patch version to use trace metrics syntax. [#82532](https://github.com/grafana/grafana/issues/82532), [@joey-grafana](https://github.com/joey-grafana) +- **Logs Panel:** Add CSV to download options. [#82480](https://github.com/grafana/grafana/issues/82480), [@gtk-grafana](https://github.com/gtk-grafana) +- **Folders:** Switch order of the columns in folder table indexes so that org_id becomes first. [#82454](https://github.com/grafana/grafana/issues/82454), [@papagian](https://github.com/papagian) +- **Logs panel:** Table UI - Guess string field types. [#82397](https://github.com/grafana/grafana/issues/82397), [@gtk-grafana](https://github.com/gtk-grafana) +- **Alerting:** Send alerts to APIv2 when using the Alertmanager contact point. [#82373](https://github.com/grafana/grafana/issues/82373), [@grobinson-grafana](https://github.com/grobinson-grafana) +- **Alerting:** Emit warning when creating or updating unusually large groups. [#82279](https://github.com/grafana/grafana/issues/82279), [@alexweav](https://github.com/alexweav) +- **Keybindings:** Change 'h' to 'mod+h' to open help modal. [#82253](https://github.com/grafana/grafana/issues/82253), [@tskarhed](https://github.com/tskarhed) +- **Chore:** Update arrow and prometheus dependencies. [#82215](https://github.com/grafana/grafana/issues/82215), [@ryantxu](https://github.com/ryantxu) +- **Alerting:** Enable group-level rule evaluation jittering by default, remove feature toggle. [#82212](https://github.com/grafana/grafana/issues/82212), [@alexweav](https://github.com/alexweav) +- **Loki Log Context:** Always show label filters with at least one parsed label. [#82211](https://github.com/grafana/grafana/issues/82211), [@svennergr](https://github.com/svennergr) +- **Logs Panel:** Table UI - better default column spacing. [#82205](https://github.com/grafana/grafana/issues/82205), [@gtk-grafana](https://github.com/gtk-grafana) +- **RBAC:** Migration to remove the scope from permissions where action is alert.instances:read. [#82202](https://github.com/grafana/grafana/issues/82202), [@IevaVasiljeva](https://github.com/IevaVasiljeva) +- **JWT Authentication:** Add support for specifying groups in auth.jwt for teamsync. [#82175](https://github.com/grafana/grafana/issues/82175), [@Jguer](https://github.com/Jguer) +- **Alerting:** GA alertingPreviewUpgrade and enable by default. [#82038](https://github.com/grafana/grafana/issues/82038), [@JacobsonMT](https://github.com/JacobsonMT) +- **Elasticsearch:** Apply ad-hoc filters to annotation queries. [#82032](https://github.com/grafana/grafana/issues/82032), [@mikelv92](https://github.com/mikelv92) +- **Alerting:** Show legacy provisioned alert rules warning. [#81902](https://github.com/grafana/grafana/issues/81902), [@gillesdemey](https://github.com/gillesdemey) +- **Tempo:** Support TraceQL metrics queries. [#81886](https://github.com/grafana/grafana/issues/81886), [@adrapereira](https://github.com/adrapereira) +- **Tempo:** Support backtick strings. [#81802](https://github.com/grafana/grafana/issues/81802), [@fabrizio-grafana](https://github.com/fabrizio-grafana) +- **Dashboards:** Remove `advancedDataSourcePicker` feature toggle. [#81790](https://github.com/grafana/grafana/issues/81790), [@Sergej-Vlasov](https://github.com/Sergej-Vlasov) +- **CloudWatch:** Remove references to pkg/infra/metrics. [#81744](https://github.com/grafana/grafana/issues/81744), [@iwysiu](https://github.com/iwysiu) +- **Licensing:** Redact license when overriden by env variable. [#81726](https://github.com/grafana/grafana/issues/81726), [@leandro-deveikis](https://github.com/leandro-deveikis) +- **Explore:** Disable cursor sync. [#81698](https://github.com/grafana/grafana/issues/81698), [@ifrost](https://github.com/ifrost) +- **Tempo:** Add custom headers middleware for grpc client. [#81693](https://github.com/grafana/grafana/issues/81693), [@aocenas](https://github.com/aocenas) +- **Chore:** Update test database initialization. [#81673](https://github.com/grafana/grafana/issues/81673), [@DanCech](https://github.com/DanCech) +- **Elasticsearch:** Implement CheckHealth method in the backend. [#81671](https://github.com/grafana/grafana/issues/81671), [@mikelv92](https://github.com/mikelv92) +- **Tooltips:** Hide dimension configuration when tooltip mode is hidden. [#81627](https://github.com/grafana/grafana/issues/81627), [@codeincarnate](https://github.com/codeincarnate) +- **Alerting:** Show warning when cp does not exist and invalidate the form. [#81621](https://github.com/grafana/grafana/issues/81621), [@soniaAguilarPeiron](https://github.com/soniaAguilarPeiron) +- **User:** Add uid colum to user table. [#81615](https://github.com/grafana/grafana/issues/81615), [@ryantxu](https://github.com/ryantxu) +- **Cloudwatch:** Remove core imports from infra/log. [#81543](https://github.com/grafana/grafana/issues/81543), [@njvrzm](https://github.com/njvrzm) +- **Alerting:** Add pagination and improved search for notification policies. [#81535](https://github.com/grafana/grafana/issues/81535), [@soniaAguilarPeiron](https://github.com/soniaAguilarPeiron) +- **Alerting:** Move action buttons in the alert list view. [#81341](https://github.com/grafana/grafana/issues/81341), [@soniaAguilarPeiron](https://github.com/soniaAguilarPeiron) +- **Grafana/ui:** Add deprecation notices to the legacy layout components. [#81328](https://github.com/grafana/grafana/issues/81328), [@Clarity-89](https://github.com/Clarity-89) +- **Cloudwatch:** Deprecate cloudwatchNewRegionsHandler feature toggle and remove core imports from featuremgmt. [#81310](https://github.com/grafana/grafana/issues/81310), [@njvrzm](https://github.com/njvrzm) +- **Candlestick:** Add tooltip options. [#81307](https://github.com/grafana/grafana/issues/81307), [@adela-almasan](https://github.com/adela-almasan) +- **Folders:** Forbid performing operations on folders via dashboards HTTP API. [#81264](https://github.com/grafana/grafana/issues/81264), [@undef1nd](https://github.com/undef1nd) +- **Feature Management:** Move awsDatasourcesNewFormStyling to Public Preview. [#81257](https://github.com/grafana/grafana/issues/81257), [@idastambuk](https://github.com/idastambuk) +- **Alerting:** Update API to use folders' full paths. [#81214](https://github.com/grafana/grafana/issues/81214), [@yuri-tceretian](https://github.com/yuri-tceretian) +- **Datasources:** Add concurrency number to the settings. [#81212](https://github.com/grafana/grafana/issues/81212), [@itsmylife](https://github.com/itsmylife) +- **CloudWatch:** Remove dependencies on grafana/pkg/setting. [#81208](https://github.com/grafana/grafana/issues/81208), [@iwysiu](https://github.com/iwysiu) +- **Logs:** Table UI - Allow users to resize field selection section. [#81201](https://github.com/grafana/grafana/issues/81201), [@gtk-grafana](https://github.com/gtk-grafana) +- **Dashboards:** Remove emptyDashboardPage feature flag. [#81188](https://github.com/grafana/grafana/issues/81188), [@Sergej-Vlasov](https://github.com/Sergej-Vlasov) +- **Cloudwatch:** Import httpClient from grafana-plugin-sdk-go instead of grafana/infra. [#81187](https://github.com/grafana/grafana/issues/81187), [@idastambuk](https://github.com/idastambuk) +- **Logs:** Table UI - Enable feature flag by default (GA). [#81185](https://github.com/grafana/grafana/issues/81185), [@gtk-grafana](https://github.com/gtk-grafana) +- **Tempo:** Improve tags UX. [#81166](https://github.com/grafana/grafana/issues/81166), [@joey-grafana](https://github.com/joey-grafana) +- **Table:** Cell inspector auto-detecting JSON. [#81152](https://github.com/grafana/grafana/issues/81152), [@gtk-grafana](https://github.com/gtk-grafana) +- **Grafana/ui:** Add Space component. [#81145](https://github.com/grafana/grafana/issues/81145), [@Clarity-89](https://github.com/Clarity-89) +- **Grafana/ui:** Add deprecation notice to the Form component. [#81068](https://github.com/grafana/grafana/issues/81068), [@Clarity-89](https://github.com/Clarity-89) +- **Alerting:** Swap order between Annotations and Labels step in the alert rule form. [#81060](https://github.com/grafana/grafana/issues/81060), [@soniaAguilarPeiron](https://github.com/soniaAguilarPeiron) +- **Plugins:** Change managedPluginsInstall to public preview. [#81053](https://github.com/grafana/grafana/issues/81053), [@oshirohugo](https://github.com/oshirohugo) +- **Tempo:** Add span, trace vars to trace to metrics interpolation. [#81046](https://github.com/grafana/grafana/issues/81046), [@joey-grafana](https://github.com/joey-grafana) +- **Tempo:** Support multiple filter expressions for service graph queries. [#81037](https://github.com/grafana/grafana/issues/81037), [@domasx2](https://github.com/domasx2) +- **Alerting:** Support for simplified notification settings in rule API. [#81011](https://github.com/grafana/grafana/issues/81011), [@yuri-tceretian](https://github.com/yuri-tceretian) +- **Plugins:** Add fuzzy search to plugins catalogue. [#81001](https://github.com/grafana/grafana/issues/81001), [@Ukochka](https://github.com/Ukochka) +- **CloudWatch:** Only override contextDialer when using PDC. [#80992](https://github.com/grafana/grafana/issues/80992), [@leandro-deveikis](https://github.com/leandro-deveikis) +- **Alerting:** Add a feature flag to periodically save states. [#80987](https://github.com/grafana/grafana/issues/80987), [@JohnnyQQQQ](https://github.com/JohnnyQQQQ) +- **RBAC:** Return the underlying error instead of internal server or bad request for managed permission endpoints. [#80974](https://github.com/grafana/grafana/issues/80974), [@IevaVasiljeva](https://github.com/IevaVasiljeva) +- **Correlations:** Enable correlations feature toggle by default. [#80881](https://github.com/grafana/grafana/issues/80881), [@ifrost](https://github.com/ifrost) +- **Transformations:** Focus search input on drawer open. [#80859](https://github.com/grafana/grafana/issues/80859), [@codeincarnate](https://github.com/codeincarnate) +- **Packaging:** Use the GRAFANA_HOME variable in postinst script on Debian. [#80853](https://github.com/grafana/grafana/issues/80853), [@denisse-dev](https://github.com/denisse-dev) +- **Visualizations:** Hue gradient mode now applies to the line color . [#80805](https://github.com/grafana/grafana/issues/80805), [@torkelo](https://github.com/torkelo) +- **Drawer:** Resizable via draggable edge . [#80796](https://github.com/grafana/grafana/issues/80796), [@torkelo](https://github.com/torkelo) +- **Alerting:** Add setting to distribute rule group evaluations over time. [#80766](https://github.com/grafana/grafana/issues/80766), [@alexweav](https://github.com/alexweav) +- **Logs Panel:** Permalink (copy shortlink). [#80764](https://github.com/grafana/grafana/issues/80764), [@gtk-grafana](https://github.com/gtk-grafana) +- **VizTooltips:** Copy to clipboard functionality. [#80761](https://github.com/grafana/grafana/issues/80761), [@adela-almasan](https://github.com/adela-almasan) +- **AuthN:** Support reloading SSO config after the sso settings have changed. [#80734](https://github.com/grafana/grafana/issues/80734), [@mgyongyosi](https://github.com/mgyongyosi) +- **Logs Panel:** Add total count to logs volume panel in explore. [#80730](https://github.com/grafana/grafana/issues/80730), [@gtk-grafana](https://github.com/gtk-grafana) +- **Caching:** Remove useCachingService feature toggle. [#80695](https://github.com/grafana/grafana/issues/80695), [@mmandrus](https://github.com/mmandrus) +- **Table:** Support showing data links inline. . [#80691](https://github.com/grafana/grafana/issues/80691), [@ryantxu](https://github.com/ryantxu) +- **Storage:** Add support for sortBy selector. [#80680](https://github.com/grafana/grafana/issues/80680), [@DanCech](https://github.com/DanCech) +- **Alerting:** Add metric counting rule groups per org. [#80669](https://github.com/grafana/grafana/issues/80669), [@alexweav](https://github.com/alexweav) +- **RBAC:** Cover plugin routes. [#80578](https://github.com/grafana/grafana/issues/80578), [@gamab](https://github.com/gamab) +- **Profiling:** Import godeltaprof/http/pprof. [#80509](https://github.com/grafana/grafana/issues/80509), [@korniltsev](https://github.com/korniltsev) +- **Tempo:** Add warning message when scope missing in TraceQL. [#80472](https://github.com/grafana/grafana/issues/80472), [@joey-grafana](https://github.com/joey-grafana) +- **Cloudwatch:** Move getNextRefIdChar util from app/core/utils to @grafana/data. [#80471](https://github.com/grafana/grafana/issues/80471), [@idastambuk](https://github.com/idastambuk) +- **DataFrame:** Add optional unique id definition. [#80428](https://github.com/grafana/grafana/issues/80428), [@aocenas](https://github.com/aocenas) +- **Canvas:** Add element snapping and alignment. [#80407](https://github.com/grafana/grafana/issues/80407), [@nmarrs](https://github.com/nmarrs) +- **Logs:** Add show context to dashboard panel. [#80403](https://github.com/grafana/grafana/issues/80403), [@svennergr](https://github.com/svennergr) +- **Canvas:** Support context menu in panel edit mode. [#80335](https://github.com/grafana/grafana/issues/80335), [@nmarrs](https://github.com/nmarrs) +- **VizTooltip:** Add sizing options. [#80306](https://github.com/grafana/grafana/issues/80306), [@Develer](https://github.com/Develer) +- **Plugins:** Parse defaultValues correctly for nested options. [#80302](https://github.com/grafana/grafana/issues/80302), [@oshirohugo](https://github.com/oshirohugo) +- **Geomap:** Support geojson styling properties. [#80272](https://github.com/grafana/grafana/issues/80272), [@drew08t](https://github.com/drew08t) +- **Runtime:** Add property for disabling caching. [#80245](https://github.com/grafana/grafana/issues/80245), [@aangelisc](https://github.com/aangelisc) +- **Alerting:** Log scheduler maxAttempts, guard against invalid retry counts, log retry errors. [#80234](https://github.com/grafana/grafana/issues/80234), [@alexweav](https://github.com/alexweav) +- **Alerting:** Improve integration with dashboards. [#80201](https://github.com/grafana/grafana/issues/80201), [@konrad147](https://github.com/konrad147) +- **Transformations:** Use an explicit join seperator when converting from an array to string field. [#80169](https://github.com/grafana/grafana/issues/80169), [@ryantxu](https://github.com/ryantxu) +- **Build:** Update plugin IDs list in build and release process. [#80160](https://github.com/grafana/grafana/issues/80160), [@fabrizio-grafana](https://github.com/fabrizio-grafana) +- **NestedFolders:** Support Shared with me folder for showing items you've been granted access to. [#80141](https://github.com/grafana/grafana/issues/80141), [@joshhunt](https://github.com/joshhunt) +- **Log Context:** Add highlighted words to log rows. [#80119](https://github.com/grafana/grafana/issues/80119), [@svennergr](https://github.com/svennergr) +- **Tempo:** Add `}` when `{` is inserted automatically. [#80113](https://github.com/grafana/grafana/issues/80113), [@harrymaurya05](https://github.com/harrymaurya05) +- **Time Range:** Copy-paste Time Range. [#80107](https://github.com/grafana/grafana/issues/80107), [@harisrozajac](https://github.com/harisrozajac) +- **PanelContext:** Remove deprecated onSplitOpen. [#80087](https://github.com/grafana/grafana/issues/80087), [@harisrozajac](https://github.com/harisrozajac) +- **Docs:** Add HAProxy rewrite information considering `serve_from_sub_path` setting. [#80062](https://github.com/grafana/grafana/issues/80062), [@simPod](https://github.com/simPod) +- **Table:** Keep expanded rows persistent when data changes if it has unique ID. [#80031](https://github.com/grafana/grafana/issues/80031), [@aocenas](https://github.com/aocenas) +- **SSO Config:** Add generic OAuth. [#79972](https://github.com/grafana/grafana/issues/79972), [@Clarity-89](https://github.com/Clarity-89) +- **FeatureFlags:** Remove the unsupported/undocumented option to read flags from a file. [#79959](https://github.com/grafana/grafana/issues/79959), [@ryantxu](https://github.com/ryantxu) +- **Transformations:** Add Group to Nested Tables Transformation. [#79952](https://github.com/grafana/grafana/issues/79952), [@codeincarnate](https://github.com/codeincarnate) +- **Cloudwatch Metrics:** Adjust error handling. [#79911](https://github.com/grafana/grafana/issues/79911), [@idastambuk](https://github.com/idastambuk) +- **Tempo:** Decouple Tempo from Grafana core. [#79888](https://github.com/grafana/grafana/issues/79888), [@fabrizio-grafana](https://github.com/fabrizio-grafana) +- **Table Panel:** Filter column values with operators or expressions. [#79853](https://github.com/grafana/grafana/issues/79853), [@ahuarte47](https://github.com/ahuarte47) +- **Chore:** Generate shorter UIDs. [#79843](https://github.com/grafana/grafana/issues/79843), [@ryantxu](https://github.com/ryantxu) +- **Alerting:** MuteTiming service return errutil + GetTiming by name. [#79772](https://github.com/grafana/grafana/issues/79772), [@yuri-tceretian](https://github.com/yuri-tceretian) +- **Azure Monitor:** Add select all subscription option for ARG queries. [#79582](https://github.com/grafana/grafana/issues/79582), [@alyssabull](https://github.com/alyssabull) +- **Alerting:** Enable sending notifications to a specific topic on Telegram. [#79546](https://github.com/grafana/grafana/issues/79546), [@th0th](https://github.com/th0th) +- **Logs Panel:** Table UI - Reordering table columns via drag-and-drop. [#79536](https://github.com/grafana/grafana/issues/79536), [@gtk-grafana](https://github.com/gtk-grafana) +- **Cloudwatch:** Add AWS/EMRServerless and AWS/KafkaConnect Metrics . [#79532](https://github.com/grafana/grafana/issues/79532), [@DugeraProve](https://github.com/DugeraProve) +- **Transformations:** Move transformation help to drawer component. [#79247](https://github.com/grafana/grafana/issues/79247), [@codeincarnate](https://github.com/codeincarnate) +- **Stat:** Support no value in spark line. [#78986](https://github.com/grafana/grafana/issues/78986), [@FOWind](https://github.com/FOWind) +- **NodeGraph:** Use layered layout instead of force based layout. [#78957](https://github.com/grafana/grafana/issues/78957), [@aocenas](https://github.com/aocenas) +- **Alerting:** Create alertingQueryOptimization feature flag for alert query optimization. [#78932](https://github.com/grafana/grafana/issues/78932), [@JacobsonMT](https://github.com/JacobsonMT) +- **Dashboard:** New EmbeddedDashboard runtime component . [#78916](https://github.com/grafana/grafana/issues/78916), [@torkelo](https://github.com/torkelo) +- **Alerting:** Show warning when query optimized. [#78751](https://github.com/grafana/grafana/issues/78751), [@JacobsonMT](https://github.com/JacobsonMT) +- **Alerting:** Add support for TTL for pushover for Mimir Alertmanager. [#78687](https://github.com/grafana/grafana/issues/78687), [@gillesdemey](https://github.com/gillesdemey) +- **Grafana/ui:** Enable removing values in multiselect opened state. [#78662](https://github.com/grafana/grafana/issues/78662), [@FOWind](https://github.com/FOWind) +- **SQL datasources:** Consistent interval handling. [#78517](https://github.com/grafana/grafana/issues/78517), [@gabor](https://github.com/gabor) +- **Alerting:** During legacy migration reduce the number of created silences. [#78505](https://github.com/grafana/grafana/issues/78505), [@JacobsonMT](https://github.com/JacobsonMT) +- **UI:** New share button and toolbar reorganize. [#77563](https://github.com/grafana/grafana/issues/77563), [@evictorero](https://github.com/evictorero) +- **Alerting:** Update rule API to address folders by UID. [#74600](https://github.com/grafana/grafana/issues/74600), [@papagian](https://github.com/papagian) +- **Reports:** Add uid column to the database. (Enterprise) +- **Plugins:** Add metrics for cloud plugin install. (Enterprise) +- **RBAC:** Make seeding resilient to failed plugin loading. (Enterprise) +- **Plugins:** Support disabling caching at a plugin instance level. (Enterprise) + +### Bug fixes + +- **GenAI:** Update the component only when the response is fully generated. [#83895](https://github.com/grafana/grafana/issues/83895), [@ivanortegaalba](https://github.com/ivanortegaalba) +- **LDAP:** Fix LDAP users authenticated via auth proxy not being able to use LDAP active sync. [#83751](https://github.com/grafana/grafana/issues/83751), [@Jguer](https://github.com/Jguer) +- **Tempo:** Better fallbacks for metrics query. [#83688](https://github.com/grafana/grafana/issues/83688), [@adrapereira](https://github.com/adrapereira) +- **Tempo:** Add template variable interpolation for filters. [#83667](https://github.com/grafana/grafana/issues/83667), [@joey-grafana](https://github.com/joey-grafana) +- **Elasticsearch:** Fix adhoc filters not applied in frontend mode. [#83597](https://github.com/grafana/grafana/issues/83597), [@svennergr](https://github.com/svennergr) +- **AuthProxy:** Invalidate previous cached item for user when changes are made to any header. [#83287](https://github.com/grafana/grafana/issues/83287), [@klesh](https://github.com/klesh) +- **Alerting:** Fix saving evaluation group. [#83234](https://github.com/grafana/grafana/issues/83234), [@soniaAguilarPeiron](https://github.com/soniaAguilarPeiron) +- **QueryVariableEditor:** Select a variable ds does not work. [#83181](https://github.com/grafana/grafana/issues/83181), [@ivanortegaalba](https://github.com/ivanortegaalba) +- **Logs Panel:** Add option extra UI functionality for log context. [#83129](https://github.com/grafana/grafana/issues/83129), [@svennergr](https://github.com/svennergr) +- **Auth:** Fix email verification bypass when using basic authentication. [#82914](https://github.com/grafana/grafana/issues/82914), [@volcanonoodle](https://github.com/volcanonoodle) +- **LibraryPanels/RBAC:** Fix issue where folder scopes weren't being correctly inherited. [#82700](https://github.com/grafana/grafana/issues/82700), [@kaydelaney](https://github.com/kaydelaney) +- **Table Panel:** Fix display of ad-hoc filter actions. [#82442](https://github.com/grafana/grafana/issues/82442), [@codeincarnate](https://github.com/codeincarnate) +- **Loki:** Update `@grafana/lezer-logql` to `0.2.3` containing fix for ip label name. [#82378](https://github.com/grafana/grafana/issues/82378), [@ivanahuckova](https://github.com/ivanahuckova) +- **Alerting:** Fix slack double pound and email summary. [#82333](https://github.com/grafana/grafana/issues/82333), [@gillesdemey](https://github.com/gillesdemey) +- **Elasticsearch:** Fix resource calls for paths that include `:`. [#82327](https://github.com/grafana/grafana/issues/82327), [@ivanahuckova](https://github.com/ivanahuckova) +- **Alerting:** Return provenance of notification templates. [#82274](https://github.com/grafana/grafana/issues/82274), [@julienduchesne](https://github.com/julienduchesne) +- **LibraryPanels:** Fix issue with repeated library panels. [#82255](https://github.com/grafana/grafana/issues/82255), [@kaydelaney](https://github.com/kaydelaney) +- **Loki:** Fix fetching of values for label if no previous equality operator. [#82251](https://github.com/grafana/grafana/issues/82251), [@ivanahuckova](https://github.com/ivanahuckova) +- **Alerting:** Fix data races and improve testing. [#81994](https://github.com/grafana/grafana/issues/81994), [@diegommm](https://github.com/diegommm) +- **chore:** Fix typo in GraphTresholdsStyleMode enum. [#81960](https://github.com/grafana/grafana/issues/81960), [@paulJonesCalian](https://github.com/paulJonesCalian) +- **CloudWatch:** Fix code editor not resizing on mount when content height is > 200px. [#81911](https://github.com/grafana/grafana/issues/81911), [@kevinwcyu](https://github.com/kevinwcyu) +- **FieldOptions:** Revert scalable unit option as we already support this via custom prefix/suffixes . [#81893](https://github.com/grafana/grafana/issues/81893), [@torkelo](https://github.com/torkelo) +- **Browse Dashboards:** Imported dashboards now display immediately in the dashboard list. [#81819](https://github.com/grafana/grafana/issues/81819), [@ashharrison90](https://github.com/ashharrison90) +- **Elasticsearch:** Set middlewares from Grafana's `httpClientProvider`. [#81814](https://github.com/grafana/grafana/issues/81814), [@svennergr](https://github.com/svennergr) +- **Folders:** Fix failure to update folder in SQLite. [#81795](https://github.com/grafana/grafana/issues/81795), [@papagian](https://github.com/papagian) +- **Plugins:** Never disable add new data source for core plugins. [#81774](https://github.com/grafana/grafana/issues/81774), [@oshirohugo](https://github.com/oshirohugo) +- **Alerting:** Fixes for pending period. [#81718](https://github.com/grafana/grafana/issues/81718), [@gillesdemey](https://github.com/gillesdemey) +- **Alerting:** Fix editing group of nested folder. [#81665](https://github.com/grafana/grafana/issues/81665), [@gillesdemey](https://github.com/gillesdemey) +- **Plugins:** Don't auto prepend app sub url to plugin asset paths. [#81658](https://github.com/grafana/grafana/issues/81658), [@wbrowne](https://github.com/wbrowne) +- **Alerting:** Fix inconsistent AM raw config when applied via sync vs API. [#81655](https://github.com/grafana/grafana/issues/81655), [@JacobsonMT](https://github.com/JacobsonMT) +- **Alerting:** Fix support check for export with modifications. [#81602](https://github.com/grafana/grafana/issues/81602), [@gillesdemey](https://github.com/gillesdemey) +- **Alerting:** Fix selecting empty contact point value for notification policy inheritance. [#81482](https://github.com/grafana/grafana/issues/81482), [@gillesdemey](https://github.com/gillesdemey) +- **Alerting:** Fix child provisioned polices not being rendered as provisioned. [#81449](https://github.com/grafana/grafana/issues/81449), [@soniaAguilarPeiron](https://github.com/soniaAguilarPeiron) +- **Tempo:** Fix durations in TraceQL. [#81418](https://github.com/grafana/grafana/issues/81418), [@fabrizio-grafana](https://github.com/fabrizio-grafana) +- **Logs:** Fix toggleable filters to be applied for specified query. [#81368](https://github.com/grafana/grafana/issues/81368), [@ivanahuckova](https://github.com/ivanahuckova) +- **Loki:** Fix label not being added to all subexpressions. [#81360](https://github.com/grafana/grafana/issues/81360), [@svennergr](https://github.com/svennergr) +- **Loki/Elastic:** Assert queryfix value to always be string. [#81349](https://github.com/grafana/grafana/issues/81349), [@svennergr](https://github.com/svennergr) +- **Tempo:** Add query ref in the query editor. [#81343](https://github.com/grafana/grafana/issues/81343), [@joey-grafana](https://github.com/joey-grafana) +- **Transformations:** Use the display name of the original y field for the predicted field of the regression analysis transformation. [#81332](https://github.com/grafana/grafana/issues/81332), [@oscarkilhed](https://github.com/oscarkilhed) +- **Field:** Fix perf regression in getUniqueFieldName(). [#81323](https://github.com/grafana/grafana/issues/81323), [@leeoniya](https://github.com/leeoniya) +- **Alerting:** Fix scheduler to group folders by the unique key (orgID and UID). [#81303](https://github.com/grafana/grafana/issues/81303), [@yuri-tceretian](https://github.com/yuri-tceretian) +- **Alerting:** Fix migration edge-case race condition for silences. [#81206](https://github.com/grafana/grafana/issues/81206), [@JacobsonMT](https://github.com/JacobsonMT) +- **Explore:** Set default time range to now-1h. [#81135](https://github.com/grafana/grafana/issues/81135), [@ifrost](https://github.com/ifrost) +- **Elasticsearch:** Fix URL creation and allowlist for `/_mapping` requests. [#80970](https://github.com/grafana/grafana/issues/80970), [@svennergr](https://github.com/svennergr) +- **Postgres:** Handle single quotes in table names in the query editor. [#80951](https://github.com/grafana/grafana/issues/80951), [@gabor](https://github.com/gabor) +- **Folders:** Fix creating/updating a folder whose title has leading and trailing spaces. [#80909](https://github.com/grafana/grafana/issues/80909), [@papagian](https://github.com/papagian) +- **Loki:** Fix missing timerange in query builder values request. [#80829](https://github.com/grafana/grafana/issues/80829), [@svennergr](https://github.com/svennergr) +- **Elasticsearch:** Fix showing of logs when `__source` is log message field. [#80804](https://github.com/grafana/grafana/issues/80804), [@ivanahuckova](https://github.com/ivanahuckova) +- **Pyroscope:** Fix stale value for query in query editor. [#80753](https://github.com/grafana/grafana/issues/80753), [@joey-grafana](https://github.com/joey-grafana) +- **Stat:** Fix data links that refer to fields. [#80693](https://github.com/grafana/grafana/issues/80693), [@ajwerner](https://github.com/ajwerner) +- **RBAC:** Clean up data source permissions after data source deletion. [#80654](https://github.com/grafana/grafana/issues/80654), [@IevaVasiljeva](https://github.com/IevaVasiljeva) +- **Alerting:** Fix MuteTiming Get API to return provenance status. [#80494](https://github.com/grafana/grafana/issues/80494), [@yuri-tceretian](https://github.com/yuri-tceretian) +- **Tempo:** Fix regression caused by #79938. [#80465](https://github.com/grafana/grafana/issues/80465), [@fabrizio-grafana](https://github.com/fabrizio-grafana) +- **Alerting:** Fix preview getting the correct queries from the form. [#80458](https://github.com/grafana/grafana/issues/80458), [@soniaAguilarPeiron](https://github.com/soniaAguilarPeiron) +- **Alerting:** Fix firing alerts title when showing active in Insights panel. [#80414](https://github.com/grafana/grafana/issues/80414), [@soniaAguilarPeiron](https://github.com/soniaAguilarPeiron) +- **Postgres:** Fix enabling the socks proxy. [#80361](https://github.com/grafana/grafana/issues/80361), [@gabor](https://github.com/gabor) +- **Alerting:** Fix group filter. [#80358](https://github.com/grafana/grafana/issues/80358), [@soniaAguilarPeiron](https://github.com/soniaAguilarPeiron) +- **Alerting:** Increase size of kvstore value type for MySQL to LONGTEXT. [#80331](https://github.com/grafana/grafana/issues/80331), [@JacobsonMT](https://github.com/JacobsonMT) +- **Annotations:** Split cleanup into separate queries and deletes to avoid deadlocks on MySQL. [#80329](https://github.com/grafana/grafana/issues/80329), [@alexweav](https://github.com/alexweav) +- **Loki:** Fix bug duplicating parsed labels across multiple log lines. [#80292](https://github.com/grafana/grafana/issues/80292), [@svennergr](https://github.com/svennergr) +- **Alerting:** Fix NoData & Error alerts not resolving when rule is reset. [#80184](https://github.com/grafana/grafana/issues/80184), [@JacobsonMT](https://github.com/JacobsonMT) +- **Loki:** Fix metric time splitting to split starting with the start time. [#80085](https://github.com/grafana/grafana/issues/80085), [@svennergr](https://github.com/svennergr) +- **Rendering:** Fix streaming panels always reaching timeout. [#80022](https://github.com/grafana/grafana/issues/80022), [@AgnesToulet](https://github.com/AgnesToulet) +- **Plugins:** Fix colon in CallResource URL returning an error when creating plugin resource request. [#79746](https://github.com/grafana/grafana/issues/79746), [@GiedriusS](https://github.com/GiedriusS) +- **PDF:** Fix initialization when SMTP is disabled. (Enterprise) +- **PDF:** Fix repeated panels placement issue. (Enterprise) +- **Report CSV:** Fix timeout with streaming panels. (Enterprise) +- **RBAC:** Avoid repopulating removed basic role permissions if the permission scope has changed. (Enterprise) + +### Breaking changes + +We're adding a between the response of the ID token HD parameter and the list of allowed domains. This feature can be disabled through the configuration toggle `validate_hd `. Anyone using the legacy Google OAuth configuration should disable this validation if the ID Token response doesn't have the HD parameter. Issue [#83726](https://github.com/grafana/grafana/issues/83726) + +If you use an automated provisioning (eg, Terraform) for custom roles, and have provisioned a role that includes permission with action `alert.instances:read` and some scope, you will need to update the permission in your provisioning files by removing the scope. Issue [#82202](https://github.com/grafana/grafana/issues/82202) + +**The following breaking change occurs only when feature flag `nestedFolders` is enabled.** +If the folder title contains the symbol `/` (forward-slash) the notifications created from the rules that are placed in that folder will contain an escape sequence for that symbol in the label `grafana_folder`. +For example, the folder title is `Grafana / Folder`. Currently the label `grafana_folder` will contain the title as it is. If PR is merged - the label value will be `Grafana \/ Folder`. +This can break notifications if notification policies have matches that match that label and folder. Issue [#81214](https://github.com/grafana/grafana/issues/81214) + +`PanelContext.onSplitOpen` is removed. In the context of Explore, plugins should use `field.getLinks` to get a list of data link models. Issue [#80087](https://github.com/grafana/grafana/issues/80087) + +The unstable alert rule API has been changed and now expects a folder UID instead of the folder title as namespace path parameter. +I addition to this, the responses that used to return the folder title now return `/` to uniquely identify them. +Any consumers of the specific API should be appropriately adapted. Issue [#74600](https://github.com/grafana/grafana/issues/74600) + +### Plugin development fixes & changes + +- **Grafana/UI:** Add new Splitter component . [#82357](https://github.com/grafana/grafana/issues/82357), [@torkelo](https://github.com/torkelo) + + # 10.3.3 (2024-02-02) From d3207df8b4fb25cb0616f4153bb6d3d5c7ad8859 Mon Sep 17 00:00:00 2001 From: "grafana-delivery-bot[bot]" <132647405+grafana-delivery-bot[bot]@users.noreply.github.com> Date: Wed, 6 Mar 2024 15:39:30 +0000 Subject: [PATCH 0432/1406] Changelog: Updated changelog for 10.3.4 (#83993) Co-authored-by: grafanabot --- CHANGELOG.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 711cc52b46..6f4d5d8524 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -225,6 +225,34 @@ Any consumers of the specific API should be appropriately adapted. Issue [#74600 - **Grafana/UI:** Add new Splitter component . [#82357](https://github.com/grafana/grafana/issues/82357), [@torkelo](https://github.com/torkelo) + + +# 10.3.4 (2024-03-06) + +### Features and enhancements + +- **Chore:** Improve domain validation for Google OAuth - Backport 83229 to v10.3.x. [#83725](https://github.com/grafana/grafana/issues/83725), [@linoman](https://github.com/linoman) + +### Bug fixes + +- **LDAP:** Fix LDAP users authenticated via auth proxy not being able to use LDAP active sync. [#83750](https://github.com/grafana/grafana/issues/83750), [@Jguer](https://github.com/Jguer) +- **Tempo:** Add template variable interpolation for filters (#83213). [#83706](https://github.com/grafana/grafana/issues/83706), [@joey-grafana](https://github.com/joey-grafana) +- **Elasticsearch:** Fix adhoc filters not applied in frontend mode. [#83596](https://github.com/grafana/grafana/issues/83596), [@svennergr](https://github.com/svennergr) +- **Dashboards:** Fixes issue where panels would not refresh if time range updated while in panel view mode. [#83525](https://github.com/grafana/grafana/issues/83525), [@kaydelaney](https://github.com/kaydelaney) +- **Auth:** Fix email verification bypass when using basic authentication. [#83484](https://github.com/grafana/grafana/issues/83484) +- **AuthProxy:** Invalidate previous cached item for user when changes are made to any header. [#83203](https://github.com/grafana/grafana/issues/83203), [@klesh](https://github.com/klesh) +- **LibraryPanels/RBAC:** Fix issue where folder scopes weren't being correctly inherited. [#82902](https://github.com/grafana/grafana/issues/82902), [@kaydelaney](https://github.com/kaydelaney) +- **LibraryPanels:** Fix issue with repeated library panels. [#82259](https://github.com/grafana/grafana/issues/82259), [@kaydelaney](https://github.com/kaydelaney) +- **Plugins:** Don't auto prepend app sub url to plugin asset paths. [#82147](https://github.com/grafana/grafana/issues/82147), [@wbrowne](https://github.com/wbrowne) +- **Elasticsearch:** Set middlewares from Grafana's `httpClientProvider`. [#81929](https://github.com/grafana/grafana/issues/81929), [@svennergr](https://github.com/svennergr) +- **Folders:** Fix failure to update folder in SQLite. [#81862](https://github.com/grafana/grafana/issues/81862), [@papagian](https://github.com/papagian) +- **Loki/Elastic:** Assert queryfix value to always be string. [#81463](https://github.com/grafana/grafana/issues/81463), [@svennergr](https://github.com/svennergr) + +### Breaking changes + +We're adding a between the response of the ID token HD parameter and the list of allowed domains. This feature can be disabled through the configuration toggle `validate_hd `. Anyone using the legacy Google OAuth configuration should disable this validation if the ID Token response doesn't have the HD parameter. Issue [#83725](https://github.com/grafana/grafana/issues/83725) + + # 10.3.3 (2024-02-02) From ce827f951815c941609a89b03c02e30eb748df23 Mon Sep 17 00:00:00 2001 From: Pepe Cano <825430+ppcano@users.noreply.github.com> Date: Wed, 6 Mar 2024 16:46:28 +0100 Subject: [PATCH 0433/1406] Configure Grafana docs: fix custom configuration file location (#83169) * Configure Grafana docs: fix custom configuration file location * Replace config file with `custom.ini` --------- Co-authored-by: Jack Baldry --- docs/sources/setup-grafana/configure-grafana/_index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/setup-grafana/configure-grafana/_index.md b/docs/sources/setup-grafana/configure-grafana/_index.md index 6d1627b8d3..ea978ea93a 100644 --- a/docs/sources/setup-grafana/configure-grafana/_index.md +++ b/docs/sources/setup-grafana/configure-grafana/_index.md @@ -23,7 +23,7 @@ After you add custom options, [uncomment](#remove-comments-in-the-ini-files) the The default settings for a Grafana instance are stored in the `$WORKING_DIR/conf/defaults.ini` file. _Do not_ change this file. -Depending on your OS, your custom configuration file is either the `$WORKING_DIR/conf/defaults.ini` file or the `/usr/local/etc/grafana/grafana.ini` file. The custom configuration file path can be overridden using the `--config` parameter. +Depending on your OS, your custom configuration file is either the `$WORKING_DIR/conf/custom.ini` file or the `/usr/local/etc/grafana/grafana.ini` file. The custom configuration file path can be overridden using the `--config` parameter. ### Linux From 1e6f14c103003e2b0568f339e115604d9f3acca9 Mon Sep 17 00:00:00 2001 From: "grafana-delivery-bot[bot]" <132647405+grafana-delivery-bot[bot]@users.noreply.github.com> Date: Wed, 6 Mar 2024 17:55:47 +0200 Subject: [PATCH 0434/1406] Changelog: Updated changelog for 10.2.5 (#84001) Co-authored-by: grafanabot --- CHANGELOG.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f4d5d8524..4849526807 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -368,6 +368,24 @@ Users who have InfluxDB datasource configured with SQL querying language must up Removes `NamespaceID` from responses of all GET routes underneath the path `/api/ruler/grafana/api/v1/rules` - 3 affected endpoints. All affected routes are not in the publicly documented or `stable` marked portion of the ngalert API. This only breaks clients who are directly using the unstable portion of the API. Such clients should use `NamespaceUID` rather than `NamespaceID` to identify namespaces. Issue [#79359](https://github.com/grafana/grafana/issues/79359) + + +# 10.2.5 (2024-03-06) + +### Features and enhancements + +- **Alerting:** Add setting to distribute rule group evaluations over time. [#81404](https://github.com/grafana/grafana/issues/81404), [@alexweav](https://github.com/alexweav) + +### Bug fixes + +- **Cloudwatch:** Fix errors while loading queries/datasource on Safari. [#83842](https://github.com/grafana/grafana/issues/83842), [@kevinwcyu](https://github.com/kevinwcyu) +- **Elasticsearch:** Fix adhoc filters not applied in frontend mode. [#83595](https://github.com/grafana/grafana/issues/83595), [@svennergr](https://github.com/svennergr) +- **Auth:** Fix email verification bypass when using basic authentication. [#83489](https://github.com/grafana/grafana/issues/83489) +- **Alerting:** Fix queries and expressions in rule view details. [#82875](https://github.com/grafana/grafana/issues/82875), [@soniaAguilarPeiron](https://github.com/soniaAguilarPeiron) +- **Plugins:** Don't auto prepend app sub url to plugin asset paths. [#82146](https://github.com/grafana/grafana/issues/82146), [@wbrowne](https://github.com/wbrowne) +- **Folders:** Fix failure to update folder in SQLite. [#81861](https://github.com/grafana/grafana/issues/81861), [@papagian](https://github.com/papagian) + + # 10.2.4 (2024-01-29) From 6731aacea904f03280dcfff59bc2a840afd2d94e Mon Sep 17 00:00:00 2001 From: "grafana-delivery-bot[bot]" <132647405+grafana-delivery-bot[bot]@users.noreply.github.com> Date: Wed, 6 Mar 2024 18:21:58 +0200 Subject: [PATCH 0435/1406] Changelog: Updated changelog for 10.1.8 (#84005) Co-authored-by: grafanabot --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4849526807..db508fd22f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1062,6 +1062,15 @@ Starting with 10.2, `parentRowIndex` is deprecated. It will be removed in a futu - **Drawer:** Make content scroll by default. [#75287](https://github.com/grafana/grafana/issues/75287), [@ashharrison90](https://github.com/ashharrison90) + + +# 10.1.8 (2024-03-06) + +### Bug fixes + +- **Auth:** Fix email verification bypass when using basic authentication. [#83492](https://github.com/grafana/grafana/issues/83492) + + # 10.1.7 (2024-01-29) From 6287e1f8ed34dd6660e27b4716c5cac72ee0666f Mon Sep 17 00:00:00 2001 From: linoman <2051016+linoman@users.noreply.github.com> Date: Wed, 6 Mar 2024 10:27:55 -0600 Subject: [PATCH 0436/1406] Auth: Auth Drawer (#83910) * add drawer for auth settings * add auth drawer component * include AuthDrawer component in auth providers page * protect the feature as enterprise only * add unit test --- .../features/auth-config/AuthDrawer.test.tsx | 22 ++++ .../app/features/auth-config/AuthDrawer.tsx | 104 ++++++++++++++++++ .../auth-config/AuthProvidersListPage.tsx | 17 ++- 3 files changed, 141 insertions(+), 2 deletions(-) create mode 100644 public/app/features/auth-config/AuthDrawer.test.tsx create mode 100644 public/app/features/auth-config/AuthDrawer.tsx diff --git a/public/app/features/auth-config/AuthDrawer.test.tsx b/public/app/features/auth-config/AuthDrawer.test.tsx new file mode 100644 index 0000000000..2af62926c0 --- /dev/null +++ b/public/app/features/auth-config/AuthDrawer.test.tsx @@ -0,0 +1,22 @@ +import { render, screen } from '@testing-library/react'; +import React from 'react'; + +import { AuthDrawer, Props } from './AuthDrawer'; + +const defaultProps: Props = { + onClose: jest.fn(), +}; + +async function getTestContext(overrides: Partial = {}) { + jest.clearAllMocks(); + + const props = { ...defaultProps, ...overrides }; + const { rerender } = render(); + + return { rerender, props }; +} + +it('should render with default props', async () => { + await getTestContext({}); + expect(screen.getByText(/Enable insecure email lookup/i)).toBeInTheDocument(); +}); diff --git a/public/app/features/auth-config/AuthDrawer.tsx b/public/app/features/auth-config/AuthDrawer.tsx new file mode 100644 index 0000000000..b2fbbf5f9f --- /dev/null +++ b/public/app/features/auth-config/AuthDrawer.tsx @@ -0,0 +1,104 @@ +import { css } from '@emotion/css'; +import React, { useState } from 'react'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { getBackendSrv } from '@grafana/runtime'; +import { Button, Drawer, Text, TextLink, Switch, useStyles2 } from '@grafana/ui'; + +export interface Props { + onClose: () => void; +} + +const SETTINGS_URL = '/api/admin/settings'; + +export const AuthDrawer = ({ onClose }: Props) => { + const [isOauthAllowInsecureEmailLookup, setOauthAllowInsecureEmailLookup] = useState(false); + + const getSettings = async () => { + try { + const response = await getBackendSrv().get(SETTINGS_URL); + setOauthAllowInsecureEmailLookup(response.auth.oauth_allow_insecure_email_lookup?.toLowerCase?.() === 'true'); + } catch (error) {} + }; + const updateSettings = async (property: boolean) => { + try { + const body = { + updates: { + auth: { + oauth_allow_insecure_email_lookup: '' + property, + }, + }, + }; + await getBackendSrv().put(SETTINGS_URL, body); + } catch (error) {} + }; + + const resetButtonOnClick = async () => { + try { + const body = { + removals: { + auth: ['oauth_allow_insecure_email_lookup'], + }, + }; + await getBackendSrv().put(SETTINGS_URL, body); + getSettings(); + } catch (error) {} + }; + + const oauthAllowInsecureEmailLookupOnChange = async () => { + updateSettings(!isOauthAllowInsecureEmailLookup); + setOauthAllowInsecureEmailLookup(!isOauthAllowInsecureEmailLookup); + }; + + const subtitle = ( + <> + Configure auth settings. Find out more in our{' '} + + documentation + + . + + ); + + const styles = useStyles2(getStyles); + + getSettings(); + + return ( + +
+ Advanced Auth + Enable insecure email lookup + + Allow users to use the same email address to log into Grafana with different identity providers. + + +
+ +
+ ); +}; + +const getStyles = (theme: GrafanaTheme2) => { + return { + advancedAuth: css({ + display: 'flex', + flexDirection: 'column', + gap: theme.spacing(1), + }), + button: css({ + marginTop: theme.spacing(2), + }), + }; +}; diff --git a/public/app/features/auth-config/AuthProvidersListPage.tsx b/public/app/features/auth-config/AuthProvidersListPage.tsx index b3a8a344fc..7a1330bcf5 100644 --- a/public/app/features/auth-config/AuthProvidersListPage.tsx +++ b/public/app/features/auth-config/AuthProvidersListPage.tsx @@ -1,11 +1,14 @@ -import React, { JSX, useEffect } from 'react'; +import React, { JSX, useEffect, useState } from 'react'; import { connect, ConnectedProps } from 'react-redux'; +import { GrafanaEdition } from '@grafana/data/src/types/config'; import { reportInteraction } from '@grafana/runtime'; -import { Grid, TextLink } from '@grafana/ui'; +import { Grid, TextLink, ToolbarButton } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; +import { config } from 'app/core/config'; import { StoreState } from 'app/types'; +import { AuthDrawer } from './AuthDrawer'; import ConfigureAuthCTA from './components/ConfigureAuthCTA'; import { ProviderCard } from './components/ProviderCard'; import { loadSettings } from './state/actions'; @@ -41,6 +44,8 @@ export const AuthConfigPageUnconnected = ({ loadSettings(); }, [loadSettings]); + const [showDrawer, setShowDrawer] = useState(false); + const authProviders = getRegisteredAuthProviders(); const availableProviders = authProviders.filter((p) => !providerStatuses[p.id]?.hide); const onProviderCardClick = (providerType: string, enabled: boolean) => { @@ -71,6 +76,13 @@ export const AuthConfigPageUnconnected = ({ . } + actions={ + config.buildInfo.edition !== GrafanaEdition.OpenSource && ( + setShowDrawer(true)}> + Auth settings + + ) + } > {!providerList.length ? ( @@ -91,6 +103,7 @@ export const AuthConfigPageUnconnected = ({ configPath={settings.configPath} /> ))} + {showDrawer && setShowDrawer(false)}>} )} From 32480b49aa985e99c4c23fefcf53f66e4783f2bc Mon Sep 17 00:00:00 2001 From: "grafana-delivery-bot[bot]" <132647405+grafana-delivery-bot[bot]@users.noreply.github.com> Date: Wed, 6 Mar 2024 18:35:01 +0200 Subject: [PATCH 0437/1406] Changelog: Updated changelog for 10.0.12 (#84008) Co-authored-by: grafanabot --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index db508fd22f..23f8e4835f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1581,6 +1581,15 @@ Starting with 10.0, changing the folder UID is deprecated. It will be removed in - **Grafana/ui:** Fix margin in RadioButtonGroup option when only icon is present. [#68899](https://github.com/grafana/grafana/issues/68899), [@aocenas](https://github.com/aocenas) + + +# 10.0.12 (2024-03-06) + +### Bug fixes + +- **Auth:** Fix email verification bypass when using basic authentication. [#83493](https://github.com/grafana/grafana/issues/83493) + + # 10.0.11 (2024-01-29) From 4bb5915183a8cf96199a24dd730b841c67cb0743 Mon Sep 17 00:00:00 2001 From: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> Date: Wed, 6 Mar 2024 11:37:07 -0500 Subject: [PATCH 0438/1406] Docs: comment in SSO for terraform video (#83978) * Commented in SSO for terraform video * Updated youtube link --- docs/sources/whatsnew/whats-new-in-v10-4.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/whatsnew/whats-new-in-v10-4.md b/docs/sources/whatsnew/whats-new-in-v10-4.md index 545d534868..2c4c28d6e5 100644 --- a/docs/sources/whatsnew/whats-new-in-v10-4.md +++ b/docs/sources/whatsnew/whats-new-in-v10-4.md @@ -217,7 +217,7 @@ We are working on adding complete support for configuring all other supported OA ![Screenshot of the Authentication provider list page](/media/docs/grafana-cloud/screenshot-sso-settings-ui-public-prev-v10.4.png) - +{{< youtube id="xXW2eRTbjDY" >}} [Documentation](https://grafana.com/docs/grafana/next/setup-grafana/configure-security/configure-authentication/) From 920d50cb76ab48e8be687fab4e79bee536357c9a Mon Sep 17 00:00:00 2001 From: Ivan Ortega Alba Date: Wed, 6 Mar 2024 17:43:19 +0100 Subject: [PATCH 0439/1406] Worker: Use CorsWorker to avoid CORS issues (#83976) --- .../dashboard-scene/saving/createDetectChangesWorker.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/public/app/features/dashboard-scene/saving/createDetectChangesWorker.ts b/public/app/features/dashboard-scene/saving/createDetectChangesWorker.ts index 4f34b2a15d..f2a835fbf5 100644 --- a/public/app/features/dashboard-scene/saving/createDetectChangesWorker.ts +++ b/public/app/features/dashboard-scene/saving/createDetectChangesWorker.ts @@ -1 +1,3 @@ +import { CorsWorker as Worker } from 'app/core/utils/CorsWorker'; + export const createWorker = () => new Worker(new URL('./DetectChangesWorker.ts', import.meta.url)); From 7f2e245d0bfc23756f24dd72adedcdeef5dbef8c Mon Sep 17 00:00:00 2001 From: "grafana-delivery-bot[bot]" <132647405+grafana-delivery-bot[bot]@users.noreply.github.com> Date: Wed, 6 Mar 2024 16:43:40 +0000 Subject: [PATCH 0440/1406] Changelog: Updated changelog for 9.5.17 (#84015) Co-authored-by: grafanabot --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 23f8e4835f..84bce9185d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2131,6 +2131,20 @@ The `database` field has been deprecated in the Elasticsearch datasource provisi - **InteractiveTable:** Updated design and minor tweak to Correlactions page. [#66443](https://github.com/grafana/grafana/issues/66443), [@torkelo](https://github.com/torkelo) + + +# 9.5.17 (2024-03-05) + +### Features and enhancements + +- Bump go-git to v5.11.0. [#83711](https://github.com/grafana/grafana/issues/83711), [@papagian](https://github.com/papagian) +- **Plugins:** Bump otelgrpc instrumentation to 0.47.0. [#83674](https://github.com/grafana/grafana/issues/83674), [@wbrowne](https://github.com/wbrowne) + +### Bug fixes + +- **Auth:** Fix email verification bypass when using basic authentication. [#83494](https://github.com/grafana/grafana/issues/83494) + + # 9.5.16 (2024-01-29) From e88858edb19b3187283ecabae13b9c45951d5736 Mon Sep 17 00:00:00 2001 From: Leon Sorokin Date: Wed, 6 Mar 2024 11:27:46 -0600 Subject: [PATCH 0441/1406] Histogram: Fix heuristic for x axis distribution from bucket progression (#83975) --- .../src/transformations/transformers/histogram.ts | 2 +- public/app/plugins/panel/histogram/Histogram.tsx | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/grafana-data/src/transformations/transformers/histogram.ts b/packages/grafana-data/src/transformations/transformers/histogram.ts index 1f333048e8..ea5ea3c977 100644 --- a/packages/grafana-data/src/transformations/transformers/histogram.ts +++ b/packages/grafana-data/src/transformations/transformers/histogram.ts @@ -403,7 +403,7 @@ export function buildHistogram(frames: DataFrame[], options?: HistogramTransform } } - const getBucket = (v: number) => incrRoundDn(v - bucketOffset, bucketSize!) + bucketOffset; + const getBucket = (v: number) => roundDecimals(incrRoundDn(v - bucketOffset, bucketSize!) + bucketOffset, 9); // guess number of decimals let bucketDecimals = (('' + bucketSize).match(/\.\d+$/) ?? ['.'])[0].length - 1; diff --git a/public/app/plugins/panel/histogram/Histogram.tsx b/public/app/plugins/panel/histogram/Histogram.tsx index 04cdab00cf..a2bb2ae278 100644 --- a/public/app/plugins/panel/histogram/Histogram.tsx +++ b/public/app/plugins/panel/histogram/Histogram.tsx @@ -8,6 +8,7 @@ import { getFieldColorModeForField, getFieldSeriesColor, GrafanaTheme2, + roundDecimals, } from '@grafana/data'; import { histogramBucketSizes, @@ -49,12 +50,16 @@ export interface HistogramProps extends Themeable2 { export function getBucketSize(frame: DataFrame) { // assumes BucketMin is fields[0] and BucktMax is fields[1] - return frame.fields[0].type === FieldType.string ? 1 : frame.fields[1].values[0] - frame.fields[0].values[0]; + return frame.fields[0].type === FieldType.string + ? 1 + : roundDecimals(frame.fields[1].values[0] - frame.fields[0].values[0], 9); } export function getBucketSize1(frame: DataFrame) { // assumes BucketMin is fields[0] and BucktMax is fields[1] - return frame.fields[0].type === FieldType.string ? 1 : frame.fields[1].values[1] - frame.fields[0].values[1]; + return frame.fields[0].type === FieldType.string + ? 1 + : roundDecimals(frame.fields[1].values[1] - frame.fields[0].values[1], 9); } const prepConfig = (frame: DataFrame, theme: GrafanaTheme2) => { From 2142efc1a5fee5049a444b2b9a2e827f0137cd43 Mon Sep 17 00:00:00 2001 From: Dai Nguyen <88277570+ej25a@users.noreply.github.com> Date: Wed, 6 Mar 2024 11:33:36 -0600 Subject: [PATCH 0442/1406] disable_sanitize_html update (#83643) * disable_sanitize_html update Added a note that states this configuration is not available for Grafana Cloud instances. * Update docs/sources/setup-grafana/configure-grafana/_index.md * Update docs/sources/setup-grafana/configure-grafana/_index.md --------- Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com> --- docs/sources/setup-grafana/configure-grafana/_index.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/sources/setup-grafana/configure-grafana/_index.md b/docs/sources/setup-grafana/configure-grafana/_index.md index ea978ea93a..e750bf426e 100644 --- a/docs/sources/setup-grafana/configure-grafana/_index.md +++ b/docs/sources/setup-grafana/configure-grafana/_index.md @@ -2210,7 +2210,11 @@ Set to `true` if you want to test alpha panels that are not yet ready for genera ### disable_sanitize_html -If set to true Grafana will allow script tags in text panels. Not recommended as it enables XSS vulnerabilities. Default is false. This setting was introduced in Grafana v6.0. +{{% admonition type="note" %}} +This configuration is not available in Grafana Cloud instances. +{{% /admonition %}} + +If set to true Grafana will allow script tags in text panels. Not recommended as it enables XSS vulnerabilities. Default is false. ## [plugins] From 61f6c4f84d5532bf2d3b2b8e48138f8131f9e9fa Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 6 Mar 2024 18:51:16 +0100 Subject: [PATCH 0443/1406] Chore: Upgrade grafana-plugin-sdk-go (#84010) Co-authored-by: Ryan McKinley --- go.mod | 58 ++++++++++++++--------------- go.sum | 103 +++++++++++++++++++++++++++++----------------------- go.work.sum | 71 ++++++++++++++++++++++++------------ 3 files changed, 134 insertions(+), 98 deletions(-) diff --git a/go.mod b/go.mod index 505b996ac5..f03f6ef046 100644 --- a/go.mod +++ b/go.mod @@ -13,10 +13,10 @@ replace cuelang.org/go => github.com/grafana/cue v0.0.0-20230926092038-971951014 // TODO: following otel replaces to pin the libraries so k8s.io/apiserver doesn't downgrade us inadvertantly // will need bumps as we upgrade otel in Grafana replace ( - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp => go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // @grafana/backend-platform - go.opentelemetry.io/otel => go.opentelemetry.io/otel v1.22.0 // @grafana/backend-platform - go.opentelemetry.io/otel/metric => go.opentelemetry.io/otel/metric v1.22.0 // @grafana/backend-platform - go.opentelemetry.io/otel/trace => go.opentelemetry.io/otel/trace v1.22.0 // @grafana/backend-platform + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp => go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // @grafana/backend-platform + go.opentelemetry.io/otel => go.opentelemetry.io/otel v1.24.0 // @grafana/backend-platform + go.opentelemetry.io/otel/metric => go.opentelemetry.io/otel/metric v1.24.0 // @grafana/backend-platform + go.opentelemetry.io/otel/trace => go.opentelemetry.io/otel/trace v1.24.0 // @grafana/backend-platform ) // Override Prometheus version because Prometheus v2.X is tagged as v0.X for Go modules purposes and Go assumes @@ -28,7 +28,7 @@ replace github.com/prometheus/prometheus => github.com/prometheus/prometheus v0. replace github.com/getkin/kin-openapi => github.com/getkin/kin-openapi v0.120.0 require ( - cloud.google.com/go/storage v1.30.1 // @grafana/backend-platform + cloud.google.com/go/storage v1.36.0 // @grafana/backend-platform cuelang.org/go v0.6.0-0.dev // @grafana/grafana-as-code github.com/Azure/azure-sdk-for-go v68.0.0+incompatible // @grafana/partner-datasources github.com/Azure/go-autorest/autorest v0.11.29 // @grafana/backend-platform @@ -63,7 +63,7 @@ require ( github.com/grafana/cuetsy v0.1.11 // @grafana/grafana-as-code github.com/grafana/grafana-aws-sdk v0.24.0 // @grafana/aws-datasources github.com/grafana/grafana-azure-sdk-go v1.12.0 // @grafana/partner-datasources - github.com/grafana/grafana-plugin-sdk-go v0.212.0 // @grafana/plugins-platform-backend + github.com/grafana/grafana-plugin-sdk-go v0.213.0 // @grafana/plugins-platform-backend github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // @grafana/backend-platform github.com/hashicorp/go-hclog v1.6.2 // @grafana/plugins-platform-backend github.com/hashicorp/go-plugin v1.6.0 // @grafana/plugins-platform-backend @@ -102,20 +102,20 @@ require ( github.com/yalue/merged_fs v1.2.2 // @grafana/grafana-as-code github.com/yudai/gojsondiff v1.0.0 // @grafana/backend-platform go.opentelemetry.io/collector/pdata v1.0.1 // @grafana/backend-platform - go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.46.1 // @grafana/grafana-operator-experience-squad + go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.49.0 // @grafana/grafana-operator-experience-squad go.opentelemetry.io/otel/exporters/jaeger v1.10.0 // @grafana/backend-platform - go.opentelemetry.io/otel/sdk v1.22.0 // @grafana/backend-platform - go.opentelemetry.io/otel/trace v1.22.0 // @grafana/backend-platform - golang.org/x/crypto v0.18.0 // @grafana/backend-platform + go.opentelemetry.io/otel/sdk v1.24.0 // @grafana/backend-platform + go.opentelemetry.io/otel/trace v1.24.0 // @grafana/backend-platform + golang.org/x/crypto v0.19.0 // @grafana/backend-platform golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb // @grafana/alerting-squad-backend - golang.org/x/net v0.20.0 // @grafana/oss-big-tent @grafana/partner-datasources + golang.org/x/net v0.21.0 // @grafana/oss-big-tent @grafana/partner-datasources golang.org/x/oauth2 v0.16.0 // @grafana/grafana-authnz-team golang.org/x/sync v0.6.0 // @grafana/alerting-squad-backend golang.org/x/time v0.5.0 // @grafana/backend-platform golang.org/x/tools v0.17.0 // @grafana/grafana-as-code gonum.org/v1/gonum v0.12.0 // @grafana/observability-metrics - google.golang.org/api v0.153.0 // @grafana/backend-platform - google.golang.org/grpc v1.60.1 // @grafana/plugins-platform-backend + google.golang.org/api v0.155.0 // @grafana/backend-platform + google.golang.org/grpc v1.62.1 // @grafana/plugins-platform-backend google.golang.org/protobuf v1.32.0 // @grafana/plugins-platform-backend gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/ini.v1 v1.67.0 // @grafana/alerting-squad-backend @@ -163,7 +163,7 @@ require ( github.com/go-openapi/validate v0.23.0 // indirect github.com/golang-jwt/jwt/v4 v4.5.0 // @grafana/backend-platform github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect - github.com/golang/glog v1.1.2 // indirect + github.com/golang/glog v1.2.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.3 // @grafana/backend-platform github.com/google/btree v1.1.2 // indirect @@ -214,11 +214,11 @@ require ( go.opencensus.io v0.24.0 // indirect go.uber.org/atomic v1.11.0 // @grafana/alerting-squad-backend go.uber.org/goleak v1.3.0 // indirect - golang.org/x/sys v0.16.0 // indirect + golang.org/x/sys v0.17.0 // indirect golang.org/x/text v0.14.0 // @grafana/backend-platform - golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect + golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect google.golang.org/appengine v1.6.8 // indirect - google.golang.org/genproto v0.0.0-20231212172506-995d672761c0 // indirect; @grafana/backend-platform + google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80 // indirect; @grafana/backend-platform ) require ( @@ -243,10 +243,10 @@ require ( github.com/jmoiron/sqlx v1.3.5 // @grafana/backend-platform github.com/matryer/is v1.4.0 // @grafana/grafana-as-code github.com/urfave/cli v1.22.14 // @grafana/backend-platform - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.47.0 // @grafana/plugins-platform-backend + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // @grafana/plugins-platform-backend go.opentelemetry.io/contrib/propagators/jaeger v1.22.0 // @grafana/backend-platform - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 // @grafana/backend-platform - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 // @grafana/backend-platform + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 // @grafana/backend-platform + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0 // @grafana/backend-platform gocloud.dev v0.25.0 // @grafana/grafana-app-platform-squad ) @@ -271,7 +271,7 @@ require ( require ( github.com/spf13/cobra v1.8.0 // @grafana/grafana-app-platform-squad - go.opentelemetry.io/otel v1.22.0 // @grafana/backend-platform + go.opentelemetry.io/otel v1.24.0 // @grafana/backend-platform k8s.io/api v0.29.2 // @grafana/grafana-app-platform-squad k8s.io/apimachinery v0.29.2 // @grafana/grafana-app-platform-squad k8s.io/apiserver v0.29.2 // @grafana/grafana-app-platform-squad @@ -291,7 +291,7 @@ require github.com/grafana/pyroscope-go/godeltaprof v0.1.6 // @grafana/observabi require github.com/apache/arrow/go/v15 v15.0.0 // @grafana/observability-metrics require ( - cloud.google.com/go v0.111.0 // indirect + cloud.google.com/go v0.112.0 // indirect cloud.google.com/go/compute/metadata v0.2.3 // indirect github.com/Azure/azure-pipeline-go v0.2.3 // indirect github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e // indirect @@ -376,14 +376,14 @@ require ( go.etcd.io/etcd/api/v3 v3.5.10 // indirect go.etcd.io/etcd/client/pkg/v3 v3.5.10 // indirect go.etcd.io/etcd/client/v3 v3.5.10 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // indirect - go.opentelemetry.io/otel/metric v1.22.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect + go.opentelemetry.io/otel/metric v1.24.0 // indirect go.starlark.net v0.0.0-20230525235612-a134d8f9ddca // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.26.0 // indirect - golang.org/x/term v0.16.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20231212172506-995d672761c0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20231212172506-995d672761c0 // indirect + golang.org/x/term v0.17.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240123012728-ef4313101c80 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 // indirect gopkg.in/fsnotify/fsnotify.v1 v1.4.7 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect @@ -432,7 +432,7 @@ require ( github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32 // indirect github.com/go-logr/logr v1.4.1 // @grafana/grafana-app-platform-squad github.com/go-logr/stdr v1.2.2 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.1 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect github.com/imdario/mergo v0.3.16 // indirect github.com/klauspost/compress v1.17.4 // indirect github.com/kylelemons/godebug v1.1.0 // indirect @@ -444,7 +444,7 @@ require ( github.com/valyala/fasttemplate v1.2.2 // indirect github.com/wk8/go-ordered-map v1.0.0 // @grafana/backend-platform github.com/xlab/treeprint v1.2.0 // @grafana/observability-traces-and-profiling - go.opentelemetry.io/proto/otlp v1.0.0 // indirect + go.opentelemetry.io/proto/otlp v1.1.0 // indirect ) require ( diff --git a/go.sum b/go.sum index 2d5836d638..4a635294b6 100644 --- a/go.sum +++ b/go.sum @@ -50,8 +50,8 @@ cloud.google.com/go v0.110.7/go.mod h1:+EYjdK8e5RME/VY/qLCAtuyALQ9q67dvuum8i+H5x cloud.google.com/go v0.110.8/go.mod h1:Iz8AkXJf1qmxC3Oxoep8R1T36w8B92yU29PcBhHO5fk= cloud.google.com/go v0.110.9/go.mod h1:rpxevX/0Lqvlbc88b7Sc1SPNdyK1riNBTUU6JXhYNpM= cloud.google.com/go v0.110.10/go.mod h1:v1OoFqYxiBkUrruItNM3eT4lLByNjxmJSV/xDKJNnic= -cloud.google.com/go v0.111.0 h1:YHLKNupSD1KqjDbQ3+LVdQ81h/UJbJyZG203cEfnQgM= -cloud.google.com/go v0.111.0/go.mod h1:0mibmpKP1TyOOFYQY5izo0LnT+ecvOQ0Sg3OdmMiNRU= +cloud.google.com/go v0.112.0 h1:tpFCD7hpHFlQ8yPwT3x+QeXqc2T6+n6T+hmABHfDUSM= +cloud.google.com/go v0.112.0/go.mod h1:3jEEVwZ/MHU4djK5t5RHuKOA/GbLddgTdVubX1qnPD4= cloud.google.com/go/accessapproval v1.4.0/go.mod h1:zybIuC3KpDOvotz59lFe5qxRZx6C75OtwbisN56xYB4= cloud.google.com/go/accessapproval v1.5.0/go.mod h1:HFy3tuiGvMdcd/u+Cu5b9NkO1pEICJ46IR82PoUdplw= cloud.google.com/go/accessapproval v1.6.0/go.mod h1:R0EiYnwV5fsRFiKZkPHr6mwyk2wxUJ30nL4j2pcFY2E= @@ -1035,8 +1035,9 @@ cloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeL cloud.google.com/go/storage v1.27.0/go.mod h1:x9DOL8TK/ygDUMieqwfhdpQryTeEkhGKMi80i/iqR2s= cloud.google.com/go/storage v1.28.1/go.mod h1:Qnisd4CqDdo6BGs2AD5LLnEsmSQ80wQ5ogcBBKhU86Y= cloud.google.com/go/storage v1.29.0/go.mod h1:4puEjyTKnku6gfKoTfNOU/W+a9JyuVNxjpS5GBrB8h4= -cloud.google.com/go/storage v1.30.1 h1:uOdMxAs8HExqBlnLtnQyP0YkvbiDpdGShGKtx6U/oNM= cloud.google.com/go/storage v1.30.1/go.mod h1:NfxhC0UJE1aXSx7CIIbCf7y9HKT7BiccwkR7+P7gN8E= +cloud.google.com/go/storage v1.36.0 h1:P0mOkAcaJxhCTvAkMhxMfrTKiNcub4YmmPBtlhAyTr8= +cloud.google.com/go/storage v1.36.0/go.mod h1:M6M/3V/D3KpzMTJyPOR/HU6n2Si5QdaXYEsng2xgOs8= cloud.google.com/go/storagetransfer v1.5.0/go.mod h1:dxNzUopWy7RQevYFHewchb29POFv3/AaBgnhqzqiK0w= cloud.google.com/go/storagetransfer v1.6.0/go.mod h1:y77xm4CQV/ZhFZH75PLEXY0ROiS7Gh6pSKrM8dJyg6I= cloud.google.com/go/storagetransfer v1.7.0/go.mod h1:8Giuj1QNb1kfLAiWM1bN6dHzfdlDAVC9rv9abHot2W4= @@ -1557,8 +1558,9 @@ github.com/cncf/xds/go v0.0.0-20220314180256-7f1daf1720fc/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20230105202645-06c439db220b/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20230310173818-32f1caf87195/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20230428030218-4003588d1b74/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4 h1:/inchEIKaYC1Akx+H+gqO04wryn5h75LSazbRlnya1k= github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20231128003011-0fa0005c9caa h1:jQCWAUqqlij9Pgj2i/PB79y4KOPYVyFYdROxgaCwdTQ= +github.com/cncf/xds/go v0.0.0-20231128003011-0fa0005c9caa/go.mod h1:x/1Gn8zydmfq8dk6e9PdstVsDgu9RuyIIJqAaF//0IM= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/cockroachdb/apd/v2 v2.0.2 h1:weh8u7Cneje73dDh+2tEVLUvyBc89iwepWCD8b8034E= github.com/cockroachdb/apd/v2 v2.0.2/go.mod h1:DDxRlzC2lo3/vSlmSoS7JkqbbrARPuFOGr0B9pvN3Gw= @@ -1701,16 +1703,18 @@ github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go. github.com/envoyproxy/go-control-plane v0.10.3/go.mod h1:fJJn/j26vwOu972OllsvAgJJM//w9BV6Fxbg2LuVd34= github.com/envoyproxy/go-control-plane v0.11.0/go.mod h1:VnHyVMpzcLvCFt9yUz1UnCwHLhwx1WguiVDV7pTG/tI= github.com/envoyproxy/go-control-plane v0.11.1-0.20230524094728-9239064ad72f/go.mod h1:sfYdkwUW4BA3PbKjySwjJy+O4Pu0h62rlqCMHNk+K+Q= -github.com/envoyproxy/go-control-plane v0.11.1 h1:wSUXTlLfiAQRWs2F+p+EKOY9rUyis1MyGqJ2DIk5HpM= github.com/envoyproxy/go-control-plane v0.11.1/go.mod h1:uhMcXKCQMEJHiAb0w+YGefQLaTEw+YhGluxZkrTmD0g= +github.com/envoyproxy/go-control-plane v0.12.0 h1:4X+VP1GHd1Mhj6IB5mMeGbLCleqxjletLK6K0rbxyZI= +github.com/envoyproxy/go-control-plane v0.12.0/go.mod h1:ZBTaoJ23lqITozF0M6G4/IragXCQKCnYbmlmtHvwRG0= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo= github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w= github.com/envoyproxy/protoc-gen-validate v0.10.0/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss= github.com/envoyproxy/protoc-gen-validate v0.10.1/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss= github.com/envoyproxy/protoc-gen-validate v1.0.1/go.mod h1:0vj8bNkYbSTNS2PIyH87KZaeN4x9zpL9Qt8fQC7d+vs= -github.com/envoyproxy/protoc-gen-validate v1.0.2 h1:QkIBuU5k+x7/QXPvPPnWXWlCdaBFApVqftFV6k087DA= github.com/envoyproxy/protoc-gen-validate v1.0.2/go.mod h1:GpiZQP3dDbg4JouG/NNS7QWXpgx6x8QiMKdmN72jogE= +github.com/envoyproxy/protoc-gen-validate v1.0.4 h1:gVPz/FMfvh57HdSJQyvBtF00j8JU4zdyUgIUNhlgg0A= +github.com/envoyproxy/protoc-gen-validate v1.0.4/go.mod h1:qys6tmnRsYrQqIhm2bvKZH4Blx/1gTIZ2UKVY1M+Yew= github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw= github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= @@ -1979,8 +1983,9 @@ github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGw github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP3NQ= -github.com/golang/glog v1.1.2 h1:DVjP2PbBOzHyzA+dn3WhHIq4NdVu3Q+pvivFICf/7fo= github.com/golang/glog v1.1.2/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ= +github.com/golang/glog v1.2.0 h1:uCdmnmatrKCgMBlM4rMuJZWOkPDqdbZPnrMXDY4gI68= +github.com/golang/glog v1.2.0/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -2177,8 +2182,6 @@ github.com/grafana/dskit v0.0.0-20240104111617-ea101a3b86eb h1:AWE6+kvtE18HP+lRW github.com/grafana/dskit v0.0.0-20240104111617-ea101a3b86eb/go.mod h1:kkWM4WUV230bNG3urVRWPBnSJHs64y/0RmWjftnnn0c= github.com/grafana/gofpdf v0.0.0-20231002120153-857cc45be447 h1:jxJJ5z0GxqhWFbQUsys3BHG8jnmniJ2Q74tXAG1NaDo= github.com/grafana/gofpdf v0.0.0-20231002120153-857cc45be447/go.mod h1:IxsY6mns6Q5sAnWcrptrgUrSglTZJXH/kXr9nbpb/9I= -github.com/grafana/grafana-aws-sdk v0.23.1 h1:YP6DqzB36fp8fXno0r+X9BxNB3apNfJnQxu8tdhYMH8= -github.com/grafana/grafana-aws-sdk v0.23.1/go.mod h1:iTbW395xv26qy6L17SjtZlVwxQTIZbmupBTe0sPHv7k= github.com/grafana/grafana-aws-sdk v0.24.0 h1:0RKCJTeIkpEUvLCTjGOK1+jYZpaE2nJaGghGLvtUsFs= github.com/grafana/grafana-aws-sdk v0.24.0/go.mod h1:3zghFF6edrxn0d6k6X9HpGZXDH+VfA+MwD2Pc/9X0ec= github.com/grafana/grafana-azure-sdk-go v1.12.0 h1:q71M2QxMlBqRZOXc5mFAycJWuZqQ3hPTzVEo1r3CUTY= @@ -2188,8 +2191,8 @@ github.com/grafana/grafana-google-sdk-go v0.1.0/go.mod h1:Vo2TKWfDVmNTELBUM+3lkr github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79 h1:r+mU5bGMzcXCRVAuOrTn54S80qbfVkvTdUJZfSfTNbs= github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79/go.mod h1:wc6Hbh3K2TgCUSfBC/BOzabItujtHMESZeFk5ZhdxhQ= github.com/grafana/grafana-plugin-sdk-go v0.114.0/go.mod h1:D7x3ah+1d4phNXpbnOaxa/osSaZlwh9/ZUnGGzegRbk= -github.com/grafana/grafana-plugin-sdk-go v0.212.0 h1:ohgMktFAasLTzAhKhcIzk81O60E29Za6ly02GhEqGIU= -github.com/grafana/grafana-plugin-sdk-go v0.212.0/go.mod h1:qsI4ktDf0lig74u8SLPJf9zRdVxWV/W4Wi+Ox6gifgs= +github.com/grafana/grafana-plugin-sdk-go v0.213.0 h1:K2CHl+RkjAhOs9bhJBfJqJ0fF2lSdyLSxo4wfqee/C4= +github.com/grafana/grafana-plugin-sdk-go v0.213.0/go.mod h1:YYqzzfCnzMcKdQzWgFhw4ZJBDdAcEUuu83SDw1VByz4= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240226124929-648abdbd0ea4 h1:hpyusz8c3yRFoJPlA0o34rWnsLbaOOBZleqRhFBi5Lg= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240226124929-648abdbd0ea4/go.mod h1:vrRQJuNprTWqwm6JPxHf3BoTJhvO15QMEjQ7Q/YUOnI= github.com/grafana/grafana/pkg/apiserver v0.0.0-20240226124929-648abdbd0ea4 h1:tIbI5zgos92vwJ8lV3zwHwuxkV03GR3FGLkFW9V5LxY= @@ -2229,8 +2232,8 @@ github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFb github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks= github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w= github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.1 h1:6UKoz5ujsI55KNpsJH3UwCq3T8kKbZwNZBNPuTTje8U= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.1/go.mod h1:YvJ2f6MplWDhfxiUC3KpyTy76kYUZA4W3pTv/wdKQ9Y= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 h1:Wqo399gCIufwto+VfwCSvsnfGpF/w5E9CNxSwbpD6No= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0/go.mod h1:qmOFXW2epJhM0qSnUUYpldc7gVz2KMQwJ/QYCDIa7XU= github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= github.com/hanwen/go-fuse v1.0.0/go.mod h1:unqXarDXqzAk0rt98O2tVndEPIpUgLD9+rwFisZH3Ok= github.com/hanwen/go-fuse/v2 v2.1.0/go.mod h1:oRyA5eK+pvJyv5otpO/DgccS8y/RvYMaO00GgRLGryc= @@ -2937,8 +2940,7 @@ github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdh github.com/scaleway/scaleway-sdk-go v1.0.0-beta.21 h1:yWfiTPwYxB0l5fGMhl/G+liULugVIHD9AU77iNLrURQ= github.com/scaleway/scaleway-sdk-go v1.0.0-beta.21/go.mod h1:fCa7OJZ/9DRTnOKmxvT6pn+LPWUptQAmHF/SBJUGEcg= github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= -github.com/scottlepp/go-duck v0.0.14 h1:fKrhE31OiwMJkS8byu0CWUfuWMfdxZYJNp5FUgHil4M= -github.com/scottlepp/go-duck v0.0.14/go.mod h1:GL+hHuKdueJRrFCduwBc7A7TQk+Tetc5BPXPVtduihY= +github.com/scottlepp/go-duck v0.0.15 h1:qrSF3pXlXAA4a7uxAfLYajqXLkeBjv8iW1wPdSfkMj0= github.com/scottlepp/go-duck v0.0.15/go.mod h1:GL+hHuKdueJRrFCduwBc7A7TQk+Tetc5BPXPVtduihY= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= @@ -3185,38 +3187,41 @@ go.opentelemetry.io/collector/pdata v1.0.0/go.mod h1:TsDFgs4JLNG7t6x9D8kGswXUz4m go.opentelemetry.io/collector/pdata v1.0.1 h1:dGX2h7maA6zHbl5D3AsMnF1c3Nn+3EUftbVCLzeyNvA= go.opentelemetry.io/collector/pdata v1.0.1/go.mod h1:jutXeu0QOXYY8wcZ/hege+YAnSBP3+jpTqYU1+JTI5Y= go.opentelemetry.io/collector/semconv v0.90.1/go.mod h1:j/8THcqVxFna1FpvA2zYIsUperEtOaRaqoLYIN4doWw= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.47.0 h1:UNQQKPfTDe1J81ViolILjTKPr9WetKW6uei2hFgJmFs= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.47.0/go.mod h1:r9vWsPS/3AQItv3OSlEJ/E4mbrhUbbw18meOjArPtKQ= -go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.46.1 h1:gbhw/u49SS3gkPWiYweQNJGm/uJN5GkI/FrosxSHT7A= -go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.46.1/go.mod h1:GnOaBaFQ2we3b9AGWJpsBa7v1S5RlQzlC3O7dRMxZhM= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.49.0 h1:RtcvQ4iw3w9NBB5yRwgA4sSa82rfId7n4atVpvKx3bY= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.49.0/go.mod h1:f/PbKbRd4cdUICWell6DmzvVJ7QrmBgFrRHjXmAXbK4= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= go.opentelemetry.io/contrib/propagators/jaeger v1.22.0 h1:bAHX+zN/inu+Rbqk51REmC8oXLl+Dw6pp9ldQf/onaY= go.opentelemetry.io/contrib/propagators/jaeger v1.22.0/go.mod h1:bH9GkgkN21mscXcQP6lQJYI8XnEPDxlTN/ZOBuHDjqE= go.opentelemetry.io/contrib/samplers/jaegerremote v0.16.0 h1:bBCrzJPJI3BsFjIYQEQ6J142Woqs/WHsImQfjV1XEnI= go.opentelemetry.io/contrib/samplers/jaegerremote v0.16.0/go.mod h1:StxwPndBVNZD2sZez0RQ0SP/129XGCd4aEmVGaw1/QM= -go.opentelemetry.io/otel v1.22.0 h1:xS7Ku+7yTFvDfDraDIJVpw7XPyuHlB9MCiqqX5mcJ6Y= -go.opentelemetry.io/otel v1.22.0/go.mod h1:eoV4iAi3Ea8LkAEI9+GFT44O6T/D0GWAVFyZVCC6pMI= +go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= +go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= go.opentelemetry.io/otel/exporters/jaeger v1.10.0 h1:7W3aVVjEYayu/GOqOVF4mbTvnCuxF1wWu3eRxFGQXvw= go.opentelemetry.io/otel/exporters/jaeger v1.10.0/go.mod h1:n9IGyx0fgyXXZ/i0foLHNxtET9CzXHzZeKCucvRBFgA= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 h1:cl5P5/GIfFh4t6xyruOgJP5QiA1pw4fYYdv6nc6CBWw= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0/go.mod h1:zgBdWWAu7oEEMC06MMKc5NLbA/1YDXV1sMpSqEeLQLg= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 h1:tIqheXEFWAZ7O8A7m+J0aPTmpJN3YQ7qetUAdkkkKpk= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 h1:t6wl9SPayj+c7lEIFgm4ooDBZVb01IhLB4InpomhRw8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0/go.mod h1:iSDOcsnSA5INXzZtwaBPrKp/lWu/V14Dd+llD0oI2EA= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0/go.mod h1:nUeKExfxAQVbiVFn32YXpXZZHZ61Cc3s3Rn1pDBGAb0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0 h1:Mw5xcxMwlqoJd97vwPxA8isEaIoxsta9/Q51+TTJLGE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0/go.mod h1:CQNu9bj7o7mC6U7+CA/schKEYakYXWr79ucDHTMGhCM= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0/go.mod h1:/OpE/y70qVkndM0TrxT4KBoN3RsFZP0QaofcfYrj76I= -go.opentelemetry.io/otel/metric v1.22.0 h1:lypMQnGyJYeuYPhOM/bgjbFM6WE44W1/T45er4d8Hhg= -go.opentelemetry.io/otel/metric v1.22.0/go.mod h1:evJGjVpZv0mQ5QBRJoBF64yMuOf4xCWdXjK8pzFvliY= +go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= +go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= go.opentelemetry.io/otel/sdk v1.17.0/go.mod h1:U87sE0f5vQB7hwUoW98pW5Rz4ZDuCFBZFNUBlSgmDFQ= go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= -go.opentelemetry.io/otel/sdk v1.22.0 h1:6coWHw9xw7EfClIC/+O31R8IY3/+EiRFHevmHafB2Gw= -go.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxtLzPvPz1DOc= -go.opentelemetry.io/otel/trace v1.22.0 h1:Hg6pPujv0XG9QaVbGOBVHunyuLcCC3jN7WEhPx83XD0= -go.opentelemetry.io/otel/trace v1.22.0/go.mod h1:RbbHXVqKES9QhzZq/fE5UnOSILqRt40a21sPw2He1xo= +go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw= +go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg= +go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= +go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= -go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= +go.opentelemetry.io/proto/otlp v1.1.0 h1:2Di21piLrCqJ3U3eXGCTPHE9R8Nh+0uglSnOyxikMeI= +go.opentelemetry.io/proto/otlp v1.1.0/go.mod h1:GpBHCBWiqvVLDqmHZsoMM3C5ySeKTC7ej/RNTae6MdY= go.starlark.net v0.0.0-20230525235612-a134d8f9ddca h1:VdD38733bfYv5tUZwEIskMM93VanwNIi5bIKnDrJdEY= go.starlark.net v0.0.0-20230525235612-a134d8f9ddca/go.mod h1:jxU+3+j+71eXOW14274+SmmuW82qJzl6iZSeqEtTGds= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= @@ -3307,8 +3312,9 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= -golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -3478,8 +3484,9 @@ golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= -golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -3692,8 +3699,9 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -3713,8 +3721,9 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww= golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= -golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE= golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= +golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -3856,8 +3865,9 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= -golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= +golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= gonum.org/v1/gonum v0.7.0/go.mod h1:L02bwd0sqlsvRv41G7wGWFCsVNZFv/k1xzGIxeANHGM= gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= @@ -3944,8 +3954,9 @@ google.golang.org/api v0.126.0/go.mod h1:mBwVAtz+87bEN6CbA1GtZPDOqY2R5ONPqJeIlvy google.golang.org/api v0.128.0/go.mod h1:Y611qgqaE92On/7g65MQgxYul3c0rEB894kniWLY750= google.golang.org/api v0.139.0/go.mod h1:CVagp6Eekz9CjGZ718Z+sloknzkDJE7Vc1Ckj9+viBk= google.golang.org/api v0.149.0/go.mod h1:Mwn1B7JTXrzXtnvmzQE2BD6bYZQ8DShKZDZbeN9I7qI= -google.golang.org/api v0.153.0 h1:N1AwGhielyKFaUqH07/ZSIQR3uNPcV7NVw0vj+j4iR4= google.golang.org/api v0.153.0/go.mod h1:3qNJX5eOmhiWYc67jRA/3GsDw97UFb5ivv7Y2PrriAY= +google.golang.org/api v0.155.0 h1:vBmGhCYs0djJttDNynWo44zosHlPvHmA0XiN2zP2DtA= +google.golang.org/api v0.155.0/go.mod h1:GI5qK5f40kCpHfPn6+YzGAByIKWv8ujFnmoWm7Igduk= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -4133,8 +4144,8 @@ google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b/go.mod h1:CgAqfJo+ google.golang.org/genproto v0.0.0-20231030173426-d783a09b4405/go.mod h1:3WDQMjmJk36UQhjQ89emUzb1mdaHcPeeAh4SCBKznB4= google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:J7XzRzVy1+IPwWHZUzoD0IccYZIrXILAQpc+Qy9CMhY= google.golang.org/genproto v0.0.0-20231120223509-83a465c0220f/go.mod h1:nWSwAFPb+qfNJXsoeO3Io7zf4tMSfN8EA8RlDA04GhY= -google.golang.org/genproto v0.0.0-20231212172506-995d672761c0 h1:YJ5pD9rF8o9Qtta0Cmy9rdBwkSjrTCT6XTiUQVOtIos= -google.golang.org/genproto v0.0.0-20231212172506-995d672761c0/go.mod h1:l/k7rMz0vFTBPy+tFSGvXEd3z+BcoG1k7EHbqm+YBsY= +google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80 h1:KAeGQVN3M9nD0/bQXnr/ClcEMJ968gUXJQ9pwfSynuQ= +google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80/go.mod h1:cc8bqMqtv9gMOr0zHg2Vzff5ULhhL2IXP4sbcn32Dro= google.golang.org/genproto/googleapis/api v0.0.0-20230525234020-1aefcd67740a/go.mod h1:ts19tUU+Z0ZShN1y3aPyq2+O3d5FUNNgT6FtOzmrNn8= google.golang.org/genproto/googleapis/api v0.0.0-20230525234035-dd9d682886f9/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= google.golang.org/genproto/googleapis/api v0.0.0-20230526203410-71b5a4ffd15e/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= @@ -4153,8 +4164,8 @@ google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b/go. google.golang.org/genproto/googleapis/api v0.0.0-20231030173426-d783a09b4405/go.mod h1:oT32Z4o8Zv2xPQTg0pbVaPr0MPOH6f14RgXt7zfIpwg= google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:0xJLfVdJqpAPl8tDg1ujOCGzx6LFLttXT5NhllGOXY4= google.golang.org/genproto/googleapis/api v0.0.0-20231127180814-3a041ad873d4/go.mod h1:k2dtGpRrbsSyKcNPKKI5sstZkrNCZwpU/ns96JoHbGg= -google.golang.org/genproto/googleapis/api v0.0.0-20231212172506-995d672761c0 h1:s1w3X6gQxwrLEpxnLd/qXTVLgQE2yXwaOaoa6IlY/+o= -google.golang.org/genproto/googleapis/api v0.0.0-20231212172506-995d672761c0/go.mod h1:CAny0tYF+0/9rmDB9fahA9YLzX3+AEVl1qXbv5hhj6c= +google.golang.org/genproto/googleapis/api v0.0.0-20240123012728-ef4313101c80 h1:Lj5rbfG876hIAYFjqiJnPHfhXbv+nzTWfm04Fg/XSVU= +google.golang.org/genproto/googleapis/api v0.0.0-20240123012728-ef4313101c80/go.mod h1:4jWUdICTdgc3Ibxmr8nAJiiLHwQBY0UI0XZcEMaFKaA= google.golang.org/genproto/googleapis/bytestream v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:ylj+BE99M198VPbBh6A8d9n3w8fChvyLK3wwBOjXBFA= google.golang.org/genproto/googleapis/bytestream v0.0.0-20230807174057-1744710a1577/go.mod h1:NjCQG/D8JandXxM57PZbAJL1DCNL6EypA0vPPwfsc7c= google.golang.org/genproto/googleapis/bytestream v0.0.0-20231030173426-d783a09b4405/go.mod h1:GRUCuLdzVqZte8+Dl/D4N25yLzcGqqWaYkeVOwulFqw= @@ -4177,8 +4188,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b/go. google.golang.org/genproto/googleapis/rpc v0.0.0-20231030173426-d783a09b4405/go.mod h1:67X1fPuzjcrkymZzZV1vvkFeTn2Rvc6lYF9MYFGCcwE= google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:oQ5rr10WTTMvP4A36n8JpR1OrO1BEiV4f78CneXZxkA= google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f/go.mod h1:L9KNLi232K1/xB6f7AlSX692koaRnKaWSR0stBki0Yc= -google.golang.org/genproto/googleapis/rpc v0.0.0-20231212172506-995d672761c0 h1:/jFB8jK5R3Sq3i/lmeZO0cATSzFfZaJq1J2Euan3XKU= -google.golang.org/genproto/googleapis/rpc v0.0.0-20231212172506-995d672761c0/go.mod h1:FUoWkonphQm3RhTS+kOEhF8h0iDpm4tdXolVCeZ9KKA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 h1:AjyfHzEPEFp/NpvfN5g+KDla3EMojjhRVZc1i7cj+oM= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80/go.mod h1:PAREbraiVEVGVdTZsVWjSbbTtSyGbAgIIvni8a8CD5s= google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= @@ -4236,8 +4247,8 @@ google.golang.org/grpc v1.57.0/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt google.golang.org/grpc v1.58.2/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0= google.golang.org/grpc v1.58.3/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0= google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= -google.golang.org/grpc v1.60.1 h1:26+wFr+cNqSGFcOXcabYC0lUVJVRa2Sb2ortSK7VrEU= -google.golang.org/grpc v1.60.1/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM= +google.golang.org/grpc v1.62.1 h1:B4n+nfKzOICUXMgyrNd19h/I9oH0L1pizfk1d4zSgTk= +google.golang.org/grpc v1.62.1/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE= google.golang.org/grpc/cmd/protoc-gen-go-grpc v0.0.0-20200910201057-6591123024b3/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= diff --git a/go.work.sum b/go.work.sum index d8aa300bdf..b16390c78a 100644 --- a/go.work.sum +++ b/go.work.sum @@ -4,8 +4,9 @@ buf.build/gen/go/grpc-ecosystem/grpc-gateway/protocolbuffers/go v1.28.1-20221127 cloud.google.com/go/accessapproval v1.7.4 h1:ZvLvJ952zK8pFHINjpMBY5k7LTAp/6pBf50RDMRgBUI= cloud.google.com/go/accesscontextmanager v1.8.4 h1:Yo4g2XrBETBCqyWIibN3NHNPQKUfQqti0lI+70rubeE= cloud.google.com/go/aiplatform v1.57.0 h1:WcZ6wDf/1qBWatmGM9Z+2BTiNjQQX54k2BekHUj93DQ= -cloud.google.com/go/aiplatform v1.57.0/go.mod h1:pwZMGvqe0JRkI1GWSZCtnAfrR4K1bv65IHILGA//VEU= +cloud.google.com/go/aiplatform v1.58.0/go.mod h1:pwZMGvqe0JRkI1GWSZCtnAfrR4K1bv65IHILGA//VEU= cloud.google.com/go/analytics v0.21.6 h1:fnV7B8lqyEYxCU0LKk+vUL7mTlqRAq4uFlIthIdr/iA= +cloud.google.com/go/analytics v0.22.0/go.mod h1:eiROFQKosh4hMaNhF85Oc9WO97Cpa7RggD40e/RBy8w= cloud.google.com/go/apigateway v1.6.4 h1:VVIxCtVerchHienSlaGzV6XJGtEM9828Erzyr3miUGs= cloud.google.com/go/apigeeconnect v1.6.4 h1:jSoGITWKgAj/ssVogNE9SdsTqcXnryPzsulENSRlusI= cloud.google.com/go/apigeeregistry v0.8.2 h1:DSaD1iiqvELag+lV4VnnqUUFd8GXELu01tKVdWZrviE= @@ -14,6 +15,7 @@ cloud.google.com/go/appengine v1.8.4 h1:Qub3fqR7iA1daJWdzjp/Q0Jz0fUG0JbMc7Ui4E9I cloud.google.com/go/area120 v0.8.4 h1:YnSO8m02pOIo6AEOgiOoUDVbw4pf+bg2KLHi4rky320= cloud.google.com/go/artifactregistry v1.14.6 h1:/hQaadYytMdA5zBh+RciIrXZQBWK4vN7EUsrQHG+/t8= cloud.google.com/go/asset v1.15.3 h1:uI8Bdm81s0esVWbWrTHcjFDFKNOa9aB7rI1vud1hO84= +cloud.google.com/go/asset v1.17.0/go.mod h1:yYLfUD4wL4X589A9tYrv4rFrba0QlDeag0CMcM5ggXU= cloud.google.com/go/assuredworkloads v1.11.4 h1:FsLSkmYYeNuzDm8L4YPfLWV+lQaUrJmH5OuD37t1k20= cloud.google.com/go/automl v1.13.4 h1:i9tOKXX+1gE7+rHpWKjiuPfGBVIYoWvLNIGpWgPtF58= cloud.google.com/go/baremetalsolution v1.2.3 h1:oQiFYYCe0vwp7J8ZmF6siVKEumWtiPFJMJcGuyDVRUk= @@ -21,12 +23,14 @@ cloud.google.com/go/batch v1.7.0 h1:AxuSPoL2fWn/rUyvWeNCNd0V2WCr+iHRCU9QO1PUmpY= cloud.google.com/go/batch v1.7.0/go.mod h1:J64gD4vsNSA2O5TtDB5AAux3nJ9iV8U3ilg3JDBYejU= cloud.google.com/go/beyondcorp v1.0.3 h1:VXf9SnrnSmj2BF2cHkoTHvOUp8gjsz1KJFOMW7czdsY= cloud.google.com/go/bigquery v1.57.1 h1:FiULdbbzUxWD0Y4ZGPSVCDLvqRSyCIO6zKV7E2nf5uA= +cloud.google.com/go/bigquery v1.58.0/go.mod h1:0eh4mWNY0KrBTjUzLjoYImapGORq9gEPT7MWjCy9lik= cloud.google.com/go/billing v1.18.0 h1:GvKy4xLy1zF1XPbwP5NJb2HjRxhnhxjjXxvyZ1S/IAo= cloud.google.com/go/billing v1.18.0/go.mod h1:5DOYQStCxquGprqfuid/7haD7th74kyMBHkjO/OvDtk= cloud.google.com/go/binaryauthorization v1.8.0 h1:PHS89lcFayWIEe0/s2jTBiEOtqghCxzc7y7bRNlifBs= cloud.google.com/go/binaryauthorization v1.8.0/go.mod h1:VQ/nUGRKhrStlGr+8GMS8f6/vznYLkdK5vaKfdCIpvU= cloud.google.com/go/certificatemanager v1.7.4 h1:5YMQ3Q+dqGpwUZ9X5sipsOQ1fLPsxod9HNq0+nrqc6I= cloud.google.com/go/channel v1.17.3 h1:Rd4+fBrjiN6tZ4TR8R/38elkyEkz6oogGDr7jDyjmMY= +cloud.google.com/go/channel v1.17.4/go.mod h1:QcEBuZLGGrUMm7kNj9IbU1ZfmJq2apotsV83hbxX7eE= cloud.google.com/go/cloudbuild v1.15.0 h1:9IHfEMWdCklJ1cwouoiQrnxmP0q3pH7JUt8Hqx4Qbck= cloud.google.com/go/clouddms v1.7.3 h1:xe/wJKz55VO1+L891a1EG9lVUgfHr9Ju/I3xh1nwF84= cloud.google.com/go/cloudtasks v1.12.4 h1:5xXuFfAjg0Z5Wb81j2GAbB3e0bwroCeSF+5jBn/L650= @@ -36,24 +40,25 @@ cloud.google.com/go/container v1.29.0 h1:jIltU529R2zBFvP8rhiG1mgeTcnT27KhU0H/1d6 cloud.google.com/go/container v1.29.0/go.mod h1:b1A1gJeTBXVLQ6GGw9/9M4FG94BEGsqJ5+t4d/3N7O4= cloud.google.com/go/containeranalysis v0.11.3 h1:5rhYLX+3a01drpREqBZVXR9YmWH45RnML++8NsCtuD8= cloud.google.com/go/datacatalog v1.19.0 h1:rbYNmHwvAOOwnW2FPXYkaK3Mf1MmGqRzK0mMiIEyLdo= +cloud.google.com/go/datacatalog v1.19.2/go.mod h1:2YbODwmhpLM4lOFe3PuEhHK9EyTzQJ5AXgIy7EDKTEE= cloud.google.com/go/dataflow v0.9.4 h1:7VmCNWcPJBS/srN2QnStTB6nu4Eb5TMcpkmtaPVhRt4= cloud.google.com/go/dataform v0.9.1 h1:jV+EsDamGX6cE127+QAcCR/lergVeeZdEQ6DdrxW3sQ= cloud.google.com/go/datafusion v1.7.4 h1:Q90alBEYlMi66zL5gMSGQHfbZLB55mOAg03DhwTTfsk= cloud.google.com/go/datalabeling v0.8.4 h1:zrq4uMmunf2KFDl/7dS6iCDBBAxBnKVDyw6+ajz3yu0= cloud.google.com/go/dataplex v1.13.0 h1:ACVOuxwe7gP0SqEso9SLyXbcZNk5l8hjcTX+XLntI5s= -cloud.google.com/go/dataplex v1.13.0/go.mod h1:mHJYQQ2VEJHsyoC0OdNyy988DvEbPhqFs5OOLffLX0c= +cloud.google.com/go/dataplex v1.14.0/go.mod h1:mHJYQQ2VEJHsyoC0OdNyy988DvEbPhqFs5OOLffLX0c= cloud.google.com/go/dataproc v1.12.0 h1:W47qHL3W4BPkAIbk4SWmIERwsWBaNnWm0P2sdx3YgGU= cloud.google.com/go/dataproc/v2 v2.3.0 h1:tTVP9tTxmc8fixxOd/8s6Q6Pz/+yzn7r7XdZHretQH0= cloud.google.com/go/dataqna v0.8.4 h1:NJnu1kAPamZDs/if3bJ3+Wb6tjADHKL83NUWsaIp2zg= cloud.google.com/go/datastore v1.15.0 h1:0P9WcsQeTWjuD1H14JIY7XQscIPQ4Laje8ti96IC5vg= cloud.google.com/go/datastream v1.10.3 h1:Z2sKPIB7bT2kMW5Uhxy44ZgdJzxzE5uKjavoW+EuHEE= cloud.google.com/go/deploy v1.16.0 h1:5OVjzm8MPC5kP+Ywbs0mdE0O7AXvAUXksSyHAyMFyMg= -cloud.google.com/go/deploy v1.16.0/go.mod h1:e5XOUI5D+YGldyLNZ21wbp9S8otJbBE4i88PtO9x/2g= +cloud.google.com/go/deploy v1.17.0/go.mod h1:XBr42U5jIr64t92gcpOXxNrqL2PStQCXHuKK5GRUuYo= cloud.google.com/go/dialogflow v1.47.0 h1:tLCWad8HZhlyUNfDzDP5m+oH6h/1Uvw/ei7B9AnsWMk= -cloud.google.com/go/dialogflow v1.47.0/go.mod h1:mHly4vU7cPXVweuB5R0zsYKPMzy240aQdAu06SqBbAQ= +cloud.google.com/go/dialogflow v1.48.1/go.mod h1:C1sjs2/g9cEwjCltkKeYp3FFpz8BOzNondEaAlCpt+A= cloud.google.com/go/dlp v1.11.1 h1:OFlXedmPP/5//X1hBEeq3D9kUVm9fb6ywYANlpv/EsQ= cloud.google.com/go/documentai v1.23.6 h1:0/S3AhS23+0qaFe3tkgMmS3STxgDgmE1jg4TvaDOZ9g= -cloud.google.com/go/documentai v1.23.6/go.mod h1:ghzBsyVTiVdkfKaUCum/9bGBEyBjDO4GfooEcYKhN+g= +cloud.google.com/go/documentai v1.23.7/go.mod h1:ghzBsyVTiVdkfKaUCum/9bGBEyBjDO4GfooEcYKhN+g= cloud.google.com/go/domains v0.9.4 h1:ua4GvsDztZ5F3xqjeLKVRDeOvJshf5QFgWGg1CKti3A= cloud.google.com/go/edgecontainer v1.1.4 h1:Szy3Q/N6bqgQGyxqjI+6xJZbmvPvnFHp3UZr95DKcQ0= cloud.google.com/go/errorreporting v0.3.0 h1:kj1XEWMu8P0qlLhm3FwcaFsUvXChV/OraZwA70trRR0= @@ -67,6 +72,7 @@ cloud.google.com/go/gkebackup v1.3.4 h1:KhnOrr9A1tXYIYeXKqCKbCI8TL2ZNGiD3dm+d7BD cloud.google.com/go/gkeconnect v0.8.4 h1:1JLpZl31YhQDQeJ98tK6QiwTpgHFYRJwpntggpQQWis= cloud.google.com/go/gkehub v0.14.4 h1:J5tYUtb3r0cl2mM7+YHvV32eL+uZQ7lONyUZnPikCEo= cloud.google.com/go/gkemulticloud v1.0.3 h1:NmJsNX9uQ2CT78957xnjXZb26TDIMvv+d5W2vVUt0Pg= +cloud.google.com/go/gkemulticloud v1.1.0/go.mod h1:7NpJBN94U6DY1xHIbsDqB2+TFZUfjLUKLjUX8NGLor0= cloud.google.com/go/grafeas v0.3.0 h1:oyTL/KjiUeBs9eYLw/40cpSZglUC+0F7X4iu/8t7NWs= cloud.google.com/go/gsuiteaddons v1.6.4 h1:uuw2Xd37yHftViSI8J2hUcCS8S7SH3ZWH09sUDLW30Q= cloud.google.com/go/iap v1.9.3 h1:M4vDbQ4TLXdaljXVZSwW7XtxpwXUUarY2lIs66m0aCM= @@ -75,14 +81,16 @@ cloud.google.com/go/iot v1.7.4 h1:m1WljtkZnvLTIRYW1YTOv5A6H1yKgLHR6nU7O8yf27w= cloud.google.com/go/language v1.12.2 h1:zg9uq2yS9PGIOdc0Kz/l+zMtOlxKWonZjjo5w5YPG2A= cloud.google.com/go/lifesciences v0.9.4 h1:rZEI/UxcxVKEzyoRS/kdJ1VoolNItRWjNN0Uk9tfexg= cloud.google.com/go/logging v1.8.1 h1:26skQWPeYhvIasWKm48+Eq7oUqdcdbwsCVwz5Ys0FvU= +cloud.google.com/go/logging v1.9.0/go.mod h1:1Io0vnZv4onoUnsVUQY3HZ3Igb1nBchky0A0y7BBBhE= cloud.google.com/go/longrunning v0.5.4 h1:w8xEcbZodnA2BbW6sVirkkoC+1gP8wS57EUUgGS0GVg= cloud.google.com/go/managedidentities v1.6.4 h1:SF/u1IJduMqQQdJA4MDyivlIQ4SrV5qAawkr/ZEREkY= cloud.google.com/go/maps v1.6.2 h1:WxxLo//b60nNFESefLgaBQevu8QGUmRV3+noOjCfIHs= -cloud.google.com/go/maps v1.6.2/go.mod h1:4+buOHhYXFBp58Zj/K+Lc1rCmJssxxF4pJ5CJnhdz18= +cloud.google.com/go/maps v1.6.3/go.mod h1:VGAn809ADswi1ASofL5lveOHPnE6Rk/SFTTBx1yuOLw= cloud.google.com/go/mediatranslation v0.8.4 h1:VRCQfZB4s6jN0CSy7+cO3m4ewNwgVnaePanVCQh/9Z4= cloud.google.com/go/memcache v1.10.4 h1:cdex/ayDd294XBj2cGeMe6Y+H1JvhN8y78B9UW7pxuQ= cloud.google.com/go/metastore v1.13.3 h1:94l/Yxg9oBZjin2bzI79oK05feYefieDq0o5fjLSkC8= cloud.google.com/go/monitoring v1.16.3 h1:mf2SN9qSoBtIgiMA4R/y4VADPWZA7VCNJA079qLaZQ8= +cloud.google.com/go/monitoring v1.17.0/go.mod h1:KwSsX5+8PnXv5NJnICZzW2R8pWTis8ypC4zmdRD63Tw= cloud.google.com/go/networkconnectivity v1.14.3 h1:e9lUkCe2BexsqsUc2bjV8+gFBpQa54J+/F3qKVtW+wA= cloud.google.com/go/networkmanagement v1.9.3 h1:HsQk4FNKJUX04k3OI6gUsoveiHMGvDRqlaFM2xGyvqU= cloud.google.com/go/networksecurity v0.9.4 h1:947tNIPnj1bMGTIEBo3fc4QrrFKS5hh0bFVsHmFm4Vo= @@ -90,18 +98,22 @@ cloud.google.com/go/notebooks v1.11.2 h1:eTOTfNL1yM6L/PCtquJwjWg7ZZGR0URFaFgbs8k cloud.google.com/go/optimization v1.6.2 h1:iFsoexcp13cGT3k/Hv8PA5aK+FP7FnbhwDO9llnruas= cloud.google.com/go/orchestration v1.8.4 h1:kgwZ2f6qMMYIVBtUGGoU8yjYWwMTHDanLwM/CQCFaoQ= cloud.google.com/go/orgpolicy v1.11.4 h1:RWuXQDr9GDYhjmrredQJC7aY7cbyqP9ZuLbq5GJGves= +cloud.google.com/go/orgpolicy v1.12.0/go.mod h1:0+aNV/nrfoTQ4Mytv+Aw+stBDBjNf4d8fYRA9herfJI= cloud.google.com/go/osconfig v1.12.4 h1:OrRCIYEAbrbXdhm13/JINn9pQchvTTIzgmOCA7uJw8I= cloud.google.com/go/oslogin v1.12.2 h1:NP/KgsD9+0r9hmHC5wKye0vJXVwdciv219DtYKYjgqE= +cloud.google.com/go/oslogin v1.13.0/go.mod h1:xPJqLwpTZ90LSE5IL1/svko+6c5avZLluiyylMb/sRA= cloud.google.com/go/phishingprotection v0.8.4 h1:sPLUQkHq6b4AL0czSJZ0jd6vL55GSTHz2B3Md+TCZI0= cloud.google.com/go/policytroubleshooter v1.10.2 h1:sq+ScLP83d7GJy9+wpwYJVnY+q6xNTXwOdRIuYjvHT4= cloud.google.com/go/privatecatalog v0.9.4 h1:Vo10IpWKbNvc/z/QZPVXgCiwfjpWoZ/wbgful4Uh/4E= cloud.google.com/go/pubsub v1.33.0 h1:6SPCPvWav64tj0sVX/+npCBKhUi/UjJehy9op/V3p2g= +cloud.google.com/go/pubsub v1.34.0/go.mod h1:alj4l4rBg+N3YTFDDC+/YyFTs6JAjam2QfYsddcAW4c= cloud.google.com/go/pubsublite v1.8.1 h1:pX+idpWMIH30/K7c0epN6V703xpIcMXWRjKJsz0tYGY= cloud.google.com/go/recaptchaenterprise v1.3.1 h1:u6EznTGzIdsyOsvm+Xkw0aSuKFXQlyjGE9a4exk6iNQ= cloud.google.com/go/recaptchaenterprise/v2 v2.9.0 h1:Zrd4LvT9PaW91X/Z13H0i5RKEv9suCLuk8zp+bfOpN4= cloud.google.com/go/recaptchaenterprise/v2 v2.9.0/go.mod h1:Dak54rw6lC2gBY8FBznpOCAR58wKf+R+ZSJRoeJok4w= cloud.google.com/go/recommendationengine v0.8.4 h1:JRiwe4hvu3auuh2hujiTc2qNgPPfVp+Q8KOpsXlEzKQ= cloud.google.com/go/recommender v1.11.3 h1:VndmgyS/J3+izR8V8BHa7HV/uun8//ivQ3k5eVKKyyM= +cloud.google.com/go/recommender v1.12.0/go.mod h1:+FJosKKJSId1MBFeJ/TTyoGQZiEelQQIZMKYYD8ruK4= cloud.google.com/go/redis v1.14.1 h1:J9cEHxG9YLmA9o4jTSvWt/RuVEn6MTrPlYSCRHujxDQ= cloud.google.com/go/resourcemanager v1.9.4 h1:JwZ7Ggle54XQ/FVYSBrMLOQIKoIT/uer8mmNvNLK51k= cloud.google.com/go/resourcesettings v1.6.4 h1:yTIL2CsZswmMfFyx2Ic77oLVzfBFoWBYgpkgiSPnC4Y= @@ -118,7 +130,7 @@ cloud.google.com/go/servicemanagement v1.8.0 h1:fopAQI/IAzlxnVeiKn/8WiV6zKndjFkv cloud.google.com/go/serviceusage v1.6.0 h1:rXyq+0+RSIm3HFypctp7WoXxIA563rn206CfMWdqXX4= cloud.google.com/go/shell v1.7.4 h1:nurhlJcSVFZneoRZgkBEHumTYf/kFJptCK2eBUq/88M= cloud.google.com/go/spanner v1.53.1 h1:xNmE0SXMSxNBuk7lRZ5G/S+A49X91zkSTt7Jn5Ptlvw= -cloud.google.com/go/spanner v1.53.1/go.mod h1:liG4iCeLqm5L3fFLU5whFITqP0e0orsAW1uUSrd4rws= +cloud.google.com/go/spanner v1.55.0/go.mod h1:HXEznMUVhC+PC+HDyo9YFG2Ajj5BQDkcbqB9Z2Ffxi0= cloud.google.com/go/speech v1.21.0 h1:qkxNao58oF8ghAHE1Eghen7XepawYEN5zuZXYWaUTA4= cloud.google.com/go/storagetransfer v1.10.3 h1:YM1dnj5gLjfL6aDldO2s4GeU8JoAvH1xyIwXre63KmI= cloud.google.com/go/talent v1.6.5 h1:LnRJhhYkODDBoTwf6BeYkiJHFw9k+1mAFNyArwZUZAs= @@ -126,6 +138,7 @@ cloud.google.com/go/texttospeech v1.7.4 h1:ahrzTgr7uAbvebuhkBAAVU6kRwVD0HWsmDsvM cloud.google.com/go/tpu v1.6.4 h1:XIEH5c0WeYGaVy9H+UueiTaf3NI6XNdB4/v6TFQJxtE= cloud.google.com/go/trace v1.10.4 h1:2qOAuAzNezwW3QN+t41BtkDJOG42HywL73q8x/f6fnM= cloud.google.com/go/translate v1.9.3 h1:t5WXTqlrk8VVJu/i3WrYQACjzYJiff5szARHiyqqPzI= +cloud.google.com/go/translate v1.10.0/go.mod h1:Kbq9RggWsbqZ9W5YpM94Q1Xv4dshw/gr/SHfsl5yCZ0= cloud.google.com/go/video v1.20.3 h1:Xrpbm2S9UFQ1pZEeJt9Vqm5t2T/z9y/M3rNXhFoo8Is= cloud.google.com/go/videointelligence v1.11.4 h1:YS4j7lY0zxYyneTFXjBJUj2r4CFe/UoIi/PJG0Zt/Rg= cloud.google.com/go/vision v1.2.0 h1:/CsSTkbmO9HC8iQpxbK8ATms3OQaX3YQUeTMGCxlaK4= @@ -165,7 +178,6 @@ github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 h1:sR+/8Yb4s github.com/CloudyKit/jet/v3 v3.0.0 h1:1PwO5w5VCtlUUl+KTOBsTGZlhjWkcybsGaAau52tOy8= github.com/DataDog/datadog-go v3.2.0+incompatible h1:qSG2N4FghB1He/r2mFrWKCaL7dXCilEuNEeAn20fdD4= github.com/GoogleCloudPlatform/cloudsql-proxy v1.29.0 h1:YNu23BtH0PKF+fg3ykSorCp6jSTjcEtfnYLzbmcjVRA= -github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c h1:RGWPOewvKIROun94nF7v2cua9qP+thov/7M50KEoeSU= github.com/Joker/hpp v1.0.0 h1:65+iuJYdRXv/XyN62C1uEmmOx3432rNG/rKlX6V7Kkc= github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible h1:1G1pk05UrOh0NlF1oeaaix1x8XzrfjIDK47TY0Zehcw= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= @@ -195,15 +207,12 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafo github.com/alicebob/miniredis v2.5.0+incompatible h1:yBHoLpsyjupjz3NL3MhKMVkR41j82Yjf3KFv7ApYzUI= github.com/alicebob/miniredis v2.5.0+incompatible/go.mod h1:8HZjEj4yU0dwhYHky+DxYx+6BMjkBbe5ONFIF1MXffk= github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg= -github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40 h1:q4dksr6ICHXqG5hm0ZW5IHyeEJXoIJSOZeBLmWPNeIQ= -github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40/go.mod h1:Q7yQnSMnLvcXlZ8RV+jwz/6y1rQTqbX6C82SndT52Zs= github.com/apache/arrow/go/v10 v10.0.1 h1:n9dERvixoC/1JjDmBcs9FPaEryoANa2sCgVFo6ez9cI= github.com/apache/arrow/go/v11 v11.0.0 h1:hqauxvFQxww+0mEU/2XHG6LT7eZternCZq+A5Yly2uM= github.com/apache/arrow/go/v12 v12.0.0 h1:xtZE63VWl7qLdB0JObIXvvhGjoVNrQ9ciIHG2OK5cmc= +github.com/apache/arrow/go/v12 v12.0.1/go.mod h1:weuTY7JvTG/HDPtMQxEUp7pU73vkLWMLpY67QwZ/WWw= github.com/apache/arrow/go/v13 v13.0.0 h1:kELrvDQuKZo8csdWYqBQfyi431x6Zs/YJTEgUuSVcWk= github.com/apache/arrow/go/v13 v13.0.0/go.mod h1:W69eByFNO0ZR30q1/7Sr9d83zcVZmF2MiP3fFYAWJOc= -github.com/apache/thrift v0.18.1 h1:lNhK/1nqjbwbiOPDBPFJVKxgDEGSepKuTh6OLiXW8kg= -github.com/apache/thrift v0.18.1/go.mod h1:rdQn/dCcDKEWjjylUeueum4vQEjG2v8v2PqriUnbr+I= github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3 h1:ZSTrOEhiM5J5RFxEaFvMZVEAM1KvT1YzbEOwB2EAGjA= github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e h1:QEF07wC0T1rKkctt1RINW/+RMTVmiwxETico2l3gxJA= @@ -217,10 +226,14 @@ github.com/aws/aws-sdk-go-v2/service/sns v1.17.4 h1:7TdmoJJBwLFyakXjfrGztejwY5Ie github.com/aws/aws-sdk-go-v2/service/sqs v1.18.3 h1:uHjK81fESbGy2Y9lspub1+C6VN5W2UXTDo2A/Pm4G0U= github.com/aws/aws-sdk-go-v2/service/ssm v1.24.1 h1:zc1YLcknvxdW/i1MuJKmEnFB2TNkOfguuQaGRvJXPng= github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible h1:Ppm0npCCsmuR9oQaBtRuZcmILVE74aXE+AmrJj8L2ns= +github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= +github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 h1:mXoPYz/Ul5HYEDvkta6I8/rnYM5gSdSV2tJ6XbZuEtY= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= github.com/boombuler/barcode v1.0.1 h1:NDBbPmhS+EqABEs5Kg3n/5ZNjy73Pz7SIV+KCeqyXcs= +github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= +github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/bwesterb/go-ristretto v1.2.3 h1:1w53tCkGhCQ5djbat3+MH0BAQ5Kfgbt56UZQ/JMzngw= github.com/casbin/casbin/v2 v2.37.0 h1:/poEwPSovi4bTOcP752/CsTQiRz2xycyVKFG7GUhbDw= github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= @@ -371,6 +384,7 @@ github.com/grafana/e2e v0.1.1-0.20221018202458-cffd2bb71c7b/go.mod h1:3UsooRp7yW github.com/grafana/gomemcache v0.0.0-20231023152154-6947259a0586 h1:/of8Z8taCPftShATouOrBVy6GaTTjgQd/VfNiZp/VXQ= github.com/grafana/gomemcache v0.0.0-20231023152154-6947259a0586/go.mod h1:PGk3RjYHpxMM8HFPhKKo+vve3DdlPUELZLSDEFehPuU= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.1/go.mod h1:YvJ2f6MplWDhfxiUC3KpyTy76kYUZA4W3pTv/wdKQ9Y= github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 h1:MJG/KsmcqMwFAkh8mTnAwhyKoB+sTAnY4CACC110tbU= github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645/go.mod h1:6iZfnjpejD4L/4DwD7NryNaJyCQdzwWwH2MWhCA90Kw= github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8= @@ -388,10 +402,13 @@ github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hudl/fargo v1.4.0 h1:ZDDILMbB37UlAVLlWcJ2Iz1XuahZZTDZfdCKeclfq2s= github.com/hydrogen18/memlistener v0.0.0-20200120041712-dcc25e7acd91 h1:KyZDvZ/GGn+r+Y3DKZ7UOQ/TP4xV6HNkrwiVMB1GnNY= github.com/iancoleman/strcase v0.2.0 h1:05I4QRnGpI0m37iZQRuskXh+w77mr6Z41lwQzuHLwW0= +github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab h1:BA4a7pe6ZTd9F8kXETBoijjFJ/ntaa//1wiH9BZu4zU= github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk= github.com/influxdata/influxdb v1.7.6 h1:8mQ7A/V+3noMGCt/P9pD09ISaiz9XvgCk303UYA3gcs= github.com/influxdata/influxdb1-client v0.0.0-20200827194710-b269163b24ab h1:HqW4xhhynfjrtEiiSGcQUd6vrK23iMam1FO8rI7mwig= +github.com/invopop/jsonschema v0.12.0 h1:6ovsNSuvn9wEQVOyc72aycBMVQFKz7cPdMJn10CvzRI= +github.com/invopop/jsonschema v0.12.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= github.com/iris-contrib/blackfriday v2.0.0+incompatible h1:o5sHQHHm0ToHUlAJSTjW9UWicjJSDDauOOQ2AHuIVp4= github.com/iris-contrib/go.uuid v2.0.0+incompatible h1:XZubAYg61/JwnJNbZilGjf3b3pB80+OQg2qf6c8BfWE= github.com/iris-contrib/jade v1.1.3 h1:p7J/50I0cjo0wq/VWVCDFd8taPJbuFC+bq23SniRFX0= @@ -451,7 +468,6 @@ github.com/kataras/sitemap v0.0.5 h1:4HCONX5RLgVy6G4RkYOV3vKNcma9p236LdGOipJsaFE github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= github.com/kisielk/errcheck v1.5.0 h1:e8esj/e4R+SAOwFwN+n3zr0nYeCyeweozKfO23MvHzY= github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg= -github.com/klauspost/asmfmt v1.3.2 h1:4Ri7ox3EwapiOjCki+hw14RyKk201CN4rzyCJRFLpK4= github.com/klauspost/cpuid v1.2.1 h1:vJi+O/nMdFt0vqm8NZBI6wzALWdA2X+egi0ogNyrC/w= github.com/knadh/koanf v1.5.0 h1:q2TSd/3Pyc/5yP9ldIrSdIz26MCcyNQzW0pEAugLPNs= github.com/knadh/koanf v1.5.0/go.mod h1:Hgyjp4y8v44hpZtPzs7JZfRAW5AhN7KfZcwv1RYggDs= @@ -492,8 +508,6 @@ github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvls github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g= github.com/mediocregopher/radix/v3 v3.4.2 h1:galbPBjIwmyREgwGCfQEN4X8lxbJnKBYurgz+VfcStA= github.com/microcosm-cc/bluemonday v1.0.2 h1:5lPfLTTAvAbtS0VqT+94yOtFnGfUWYyx0+iToC3Os3s= -github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 h1:AMFGa4R4MiIpspGNG7Z948v4n35fFGB3RR3G/ry4FWs= -github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 h1:+n/aFZefKZp7spd8DFdX7uMikMLXX4oubIzJF4kv/wI= github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g= github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= @@ -566,7 +580,6 @@ github.com/performancecopilot/speed/v4 v4.0.0 h1:VxEDCmdkfbQYDlcr/GC9YoN9PQ6p8ul github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= github.com/phpdave11/gofpdf v1.4.2 h1:KPKiIbfwbvC/wOncwhrpRdXVj2CZTCFlw4wnoyjtHfQ= github.com/phpdave11/gofpdi v1.0.13 h1:o61duiW8M9sMlkVXWlvP92sZJtGKENvW3VExs6dZukQ= -github.com/pborman/uuid v1.2.0 h1:J7Q5mO4ysT1dv8hyrUGHb9+ooztCXu1D8MY8DZYsu3g= github.com/pierrec/lz4 v2.0.5+incompatible h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM8aXeqhl0I= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgcC4zIxODThtZNPirFr42+A= github.com/pkg/profile v1.2.1 h1:F++O52m40owAmADcojzM+9gyjmMOY/T4oYJkgFDH8RE= @@ -597,8 +610,6 @@ github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da h1:p3Vo3i64TCL github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiyyjYS17cCYRqP13/SHk= github.com/segmentio/fasthash v0.0.0-20180216231524-a72b379d632e h1:uO75wNGioszjmIzcY/tvdDYKRLVvzggtAmmJkn9j4GQ= -github.com/scottlepp/go-duck v0.0.15 h1:qrSF3pXlXAA4a7uxAfLYajqXLkeBjv8iW1wPdSfkMj0= -github.com/scottlepp/go-duck v0.0.15/go.mod h1:GL+hHuKdueJRrFCduwBc7A7TQk+Tetc5BPXPVtduihY= github.com/segmentio/fasthash v0.0.0-20180216231524-a72b379d632e/go.mod h1:tm/wZFQ8e24NYaBGIlnO2WGCAi67re4HHuOm0sftE/M= github.com/segmentio/parquet-go v0.0.0-20230427215636-d483faba23a5 h1:7CWCjaHrXSUCHrRhIARMGDVKdB82tnPAQMmANeflKOw= github.com/segmentio/parquet-go v0.0.0-20230427215636-d483faba23a5/go.mod h1:+J0xQnJjm8DuQUHBO7t57EnmPbstT6+b45+p3DC9k1Q= @@ -614,6 +625,7 @@ github.com/sony/gobreaker v0.4.1 h1:oMnRNZXX5j85zso6xCPRNPtmAycat+WcoKbklScLDgQ= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.9.2 h1:j49Hj62F0n+DaZ1dDCvhABaPNSGNkt32oRFxI33IEMw= +github.com/spf13/afero v1.10.0/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/viper v1.14.0 h1:Rg7d3Lo706X9tHsJMUjdiwMpHB7W8WnSVOssIY+JElU= @@ -657,6 +669,8 @@ github.com/willf/bitset v1.1.11 h1:N7Z7E9UvjW+sGsEl7k/SJrvY2reP1A07MrGuCjIOjRE= github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI= github.com/willf/bloom v2.0.3+incompatible h1:QDacWdqcAUI1MPOwIQZRy9kOR7yxfyEmxX8Wdm2/JPA= github.com/willf/bloom v2.0.3+incompatible/go.mod h1:MmAltL9pDMNTrvUkxdg0k0q5I0suxmuwp3KbyrZLOZ8= +github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= +github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= github.com/xanzy/go-gitlab v0.15.0 h1:rWtwKTgEnXyNUGrOArN7yyc3THRkpYcKXIXia9abywQ= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= @@ -699,6 +713,8 @@ go.opentelemetry.io/collector/receiver/otlpreceiver v0.74.0/go.mod h1:9X9/RYFxJI go.opentelemetry.io/collector/semconv v0.90.1 h1:2fkQZbefQBbIcNb9Rk1mRcWlFZgQOk7CpST1e1BK8eg= go.opentelemetry.io/contrib v0.18.0 h1:uqBh0brileIvG6luvBjdxzoFL8lxDGuhxJWsvK3BveI= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.42.0/go.mod h1:5z+/ZWJQKXa9YT34fQNx5K8Hd1EoIhvtUygUQPqEOgQ= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1/go.mod h1:4UoMYEZOC0yN/sPGH76KPkkU7zgiEWYWL9vwmbnTJPE= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.47.0/go.mod h1:r9vWsPS/3AQItv3OSlEJ/E4mbrhUbbw18meOjArPtKQ= go.opentelemetry.io/contrib/propagators/b3 v1.15.0 h1:bMaonPyFcAvZ4EVzkUNkfnUHP5Zi63CIDlA3dRsEg8Q= go.opentelemetry.io/contrib/propagators/b3 v1.15.0/go.mod h1:VjU0g2v6HSQ+NwfifambSLAeBgevjIcqmceaKWEzl0c= go.opentelemetry.io/otel/bridge/opencensus v0.37.0 h1:ieH3gw7b1eg90ARsFAlAsX5LKVZgnCYfaDwRrK6xLHU= @@ -710,7 +726,7 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0/go.mod h go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0 h1:digkEZCJWobwBqMwC0cwCq8/wkkRy/OowZg5OArWZrM= go.opentelemetry.io/otel/exporters/prometheus v0.37.0 h1:NQc0epfL0xItsmGgSXgfbH2C1fq2VLXkZoDFsfRNHpc= go.opentelemetry.io/otel/exporters/prometheus v0.37.0/go.mod h1:hB8qWjsStK36t50/R0V2ULFb4u95X/Q6zupXLgvjTh8= -go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A= +go.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxtLzPvPz1DOc= go.opentelemetry.io/otel/sdk/metric v0.39.0 h1:Kun8i1eYf48kHH83RucG93ffz0zGV1sh46FAScOTuDI= go.opentelemetry.io/otel/sdk/metric v0.39.0/go.mod h1:piDIRgjcK7u0HCL5pCA4e74qpK/jk3NiUoAHATVAmiI= go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= @@ -721,6 +737,7 @@ go.uber.org/mock v0.2.0/go.mod h1:J0y0rp9L3xiff1+ZBfKxlC1fz2+aO16tw0tsDOixfuM= go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA= golang.org/x/image v0.0.0-20220302094943-723b81ca9867 h1:TcHcE0vrmgzNH1v3ppjcMGbhG5+9fMuvOmUYwNEF4q4= golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug= @@ -732,10 +749,20 @@ golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0 gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0 h1:OE9mWmgKkjJyEmDAAtGMPjXu+YNeGvK9VTSHY6+Qihc= gonum.org/v1/plot v0.10.1 h1:dnifSs43YJuNMDzB7v8wV64O4ABBHReuAVAoBxqBqS4= google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3/go.mod h1:5RBcpGRxr25RbDzY5w+dmaqpSEvl8Gwl1x2CICf60ic= +google.golang.org/genproto v0.0.0-20231212172506-995d672761c0/go.mod h1:l/k7rMz0vFTBPy+tFSGvXEd3z+BcoG1k7EHbqm+YBsY= +google.golang.org/genproto v0.0.0-20240116215550-a9fa1716bcac/go.mod h1:+Rvu7ElI+aLzyDQhpHMFMMltsD6m7nqpuWDd2CwJw3k= google.golang.org/genproto/googleapis/api v0.0.0-20231211222908-989df2bf70f3/go.mod h1:k2dtGpRrbsSyKcNPKKI5sstZkrNCZwpU/ns96JoHbGg= +google.golang.org/genproto/googleapis/api v0.0.0-20231212172506-995d672761c0/go.mod h1:CAny0tYF+0/9rmDB9fahA9YLzX3+AEVl1qXbv5hhj6c= +google.golang.org/genproto/googleapis/api v0.0.0-20240102182953-50ed04b92917/go.mod h1:CmlNWB9lSezaYELKS5Ym1r44VrrbPUa7JTvw+6MbpJ0= +google.golang.org/genproto/googleapis/api v0.0.0-20240116215550-a9fa1716bcac/go.mod h1:B5xPO//w8qmBDjGReYLpR6UJPnkldGkCSMoH/2vxJeg= google.golang.org/genproto/googleapis/bytestream v0.0.0-20231120223509-83a465c0220f h1:hL+1ptbhFoeL1HcROQ8OGXaqH0jYRRibgWQWco0/Ugc= -google.golang.org/genproto/googleapis/rpc v0.0.0-20231211222908-989df2bf70f3/go.mod h1:eJVxU6o+4G1PSczBr85xmyvSNYAKvAYgkub40YGomFM= -google.golang.org/grpc v1.60.0/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM= +google.golang.org/genproto/googleapis/bytestream v0.0.0-20231212172506-995d672761c0/go.mod h1:guYXGPwC6jwxgWKW5Y405fKWOFNwlvUlUnzyp9i0uqo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231212172506-995d672761c0/go.mod h1:FUoWkonphQm3RhTS+kOEhF8h0iDpm4tdXolVCeZ9KKA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917/go.mod h1:xtjpI3tXFPP051KaWnhvxkiubL/6dJ18vLVf7q2pTOU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240116215550-a9fa1716bcac/go.mod h1:daQN87bsDqDoe316QbbvX60nMoJQa4r6Ds0ZuoAe5yA= +google.golang.org/grpc v1.60.1/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM= +google.golang.org/grpc v1.61.0/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs= +google.golang.org/grpc v1.61.1/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0 h1:M1YKkFIboKNieVO5DLUEVzQfGwJD30Nv2jfUgzb5UcE= gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= gopkg.in/cheggaaa/pb.v1 v1.0.25 h1:Ev7yu1/f6+d+b3pi5vPdRPc6nNtP1umSfcWiEfRqv6I= @@ -750,8 +777,6 @@ gopkg.in/resty.v1 v1.12.0 h1:CuXP0Pjfw9rOuY6EP+UvtNvt5DSqHpIxILZKT/quCZI= gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg= -gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= -gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98= gopkg.in/telebot.v3 v3.2.1 h1:3I4LohaAyJBiivGmkfB+CiVu7QFOWkuZ4+KHgO/G3rs= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= From 8b9bc9a9193c4743a7bbc22f2313aef8546f5356 Mon Sep 17 00:00:00 2001 From: Leon Sorokin Date: Wed, 6 Mar 2024 12:50:28 -0600 Subject: [PATCH 0444/1406] Histogram: Fix 'combine' option & legend rendering (#84026) --- .../src/transformations/transformers/histogram.ts | 7 ++++++- public/app/plugins/panel/histogram/Histogram.tsx | 4 +++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/grafana-data/src/transformations/transformers/histogram.ts b/packages/grafana-data/src/transformations/transformers/histogram.ts index ea5ea3c977..39d54ffeda 100644 --- a/packages/grafana-data/src/transformations/transformers/histogram.ts +++ b/packages/grafana-data/src/transformations/transformers/histogram.ts @@ -482,7 +482,12 @@ export function buildHistogram(frames: DataFrame[], options?: HistogramTransform name: 'count', values: vals, type: FieldType.number, - state: undefined, + state: { + ...counts[0].state, + displayName: 'Count', + multipleFrames: false, + origin: { frameIndex: 0, fieldIndex: 2 }, + }, }, ]; } else { diff --git a/public/app/plugins/panel/histogram/Histogram.tsx b/public/app/plugins/panel/histogram/Histogram.tsx index a2bb2ae278..160db11fe3 100644 --- a/public/app/plugins/panel/histogram/Histogram.tsx +++ b/public/app/plugins/panel/histogram/Histogram.tsx @@ -312,7 +312,9 @@ export class Histogram extends React.Component { return null; } - return ; + const frames = this.props.options.combine ? [this.props.alignedFrame] : this.props.rawSeries!; + + return ; } componentDidUpdate(prevProps: HistogramProps) { From d5fda0614773d5ed12df53b939eb5e3027df2960 Mon Sep 17 00:00:00 2001 From: Alexander Weaver Date: Wed, 6 Mar 2024 13:44:53 -0600 Subject: [PATCH 0445/1406] Alerting: Decouple rule routine from scheduler (#84018) * create rule factory for more complicated dep injection into rules * Rules get direct access to metrics, logs, traces utilities, use factory in tests * Use clock internal to rule * Use sender, statemanager, evalfactory directly * evalApplied and stopApplied * use schedulableAlertRules behind interface * loaded metrics reader * 3 relevant config options * Drop unused scheduler parameter * Rename ruleRoutine to run * Update READMED * Handle long parameter lists * remove dead branch --- pkg/services/ngalert/README.md | 2 +- pkg/services/ngalert/schedule/alert_rule.go | 184 +++++++++++++----- .../ngalert/schedule/alert_rule_test.go | 64 +++--- .../ngalert/schedule/loaded_metrics_reader.go | 4 +- pkg/services/ngalert/schedule/registry.go | 8 +- pkg/services/ngalert/schedule/schedule.go | 19 +- .../ngalert/schedule/schedule_unit_test.go | 3 +- 7 files changed, 208 insertions(+), 76 deletions(-) diff --git a/pkg/services/ngalert/README.md b/pkg/services/ngalert/README.md index da0b978302..78e2e59644 100644 --- a/pkg/services/ngalert/README.md +++ b/pkg/services/ngalert/README.md @@ -30,7 +30,7 @@ The scheduler runs at a fixed interval, called its heartbeat, in which it does a 3. Send an `*evaluation` event to the goroutine for each alert rule if its interval has elapsed 4. Stop the goroutines for all alert rules that have been deleted since the last heartbeat -The function that evaluates each alert rule is called `ruleRoutine`. It waits for an `*evaluation` event (sent each +The function that evaluates each alert rule is called `run`. It waits for an `*evaluation` event (sent each interval seconds elapsed and is configurable per alert rule) and then evaluates the alert rule. To ensure that the scheduler is evaluating the latest version of the alert rule it compares its local version of the alert rule with that in the `*evaluation` event, fetching the latest version of the alert rule from the database if the version numbers diff --git a/pkg/services/ngalert/schedule/alert_rule.go b/pkg/services/ngalert/schedule/alert_rule.go index ab184a3bd8..396d45bd49 100644 --- a/pkg/services/ngalert/schedule/alert_rule.go +++ b/pkg/services/ngalert/schedule/alert_rule.go @@ -4,10 +4,15 @@ import ( context "context" "errors" "fmt" + "net/url" "time" + "github.com/benbjohnson/clock" + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/ngalert/eval" + "github.com/grafana/grafana/pkg/services/ngalert/metrics" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/state" "github.com/grafana/grafana/pkg/services/org" @@ -18,20 +23,114 @@ import ( "go.opentelemetry.io/otel/trace" ) +type ruleFactoryFunc func(context.Context) *alertRuleInfo + +func (f ruleFactoryFunc) new(ctx context.Context) *alertRuleInfo { + return f(ctx) +} + +func newRuleFactory( + appURL *url.URL, + disableGrafanaFolder bool, + maxAttempts int64, + sender AlertsSender, + stateManager *state.Manager, + evalFactory eval.EvaluatorFactory, + ruleProvider ruleProvider, + clock clock.Clock, + met *metrics.Scheduler, + logger log.Logger, + tracer tracing.Tracer, + evalAppliedHook evalAppliedFunc, + stopAppliedHook stopAppliedFunc, +) ruleFactoryFunc { + return func(ctx context.Context) *alertRuleInfo { + return newAlertRuleInfo( + ctx, + appURL, + disableGrafanaFolder, + maxAttempts, + sender, + stateManager, + evalFactory, + ruleProvider, + clock, + met, + logger, + tracer, + evalAppliedHook, + stopAppliedHook, + ) + } +} + +type evalAppliedFunc = func(ngmodels.AlertRuleKey, time.Time) +type stopAppliedFunc = func(ngmodels.AlertRuleKey) + +type ruleProvider interface { + get(ngmodels.AlertRuleKey) *ngmodels.AlertRule +} + type alertRuleInfo struct { evalCh chan *evaluation updateCh chan ruleVersionAndPauseStatus ctx context.Context stopFn util.CancelCauseFunc + + appURL *url.URL + disableGrafanaFolder bool + maxAttempts int64 + + clock clock.Clock + sender AlertsSender + stateManager *state.Manager + evalFactory eval.EvaluatorFactory + ruleProvider ruleProvider + + // Event hooks that are only used in tests. + evalAppliedHook evalAppliedFunc + stopAppliedHook stopAppliedFunc + + metrics *metrics.Scheduler + logger log.Logger + tracer tracing.Tracer } -func newAlertRuleInfo(parent context.Context) *alertRuleInfo { +func newAlertRuleInfo( + parent context.Context, + appURL *url.URL, + disableGrafanaFolder bool, + maxAttempts int64, + sender AlertsSender, + stateManager *state.Manager, + evalFactory eval.EvaluatorFactory, + ruleProvider ruleProvider, + clock clock.Clock, + met *metrics.Scheduler, + logger log.Logger, + tracer tracing.Tracer, + evalAppliedHook func(ngmodels.AlertRuleKey, time.Time), + stopAppliedHook func(ngmodels.AlertRuleKey), +) *alertRuleInfo { ctx, stop := util.WithCancelCause(parent) return &alertRuleInfo{ - evalCh: make(chan *evaluation), - updateCh: make(chan ruleVersionAndPauseStatus), - ctx: ctx, - stopFn: stop, + evalCh: make(chan *evaluation), + updateCh: make(chan ruleVersionAndPauseStatus), + ctx: ctx, + stopFn: stop, + appURL: appURL, + disableGrafanaFolder: disableGrafanaFolder, + maxAttempts: maxAttempts, + clock: clock, + sender: sender, + stateManager: stateManager, + evalFactory: evalFactory, + ruleProvider: ruleProvider, + evalAppliedHook: evalAppliedHook, + stopAppliedHook: stopAppliedHook, + metrics: met, + logger: logger, + tracer: tracer, } } @@ -82,52 +181,49 @@ func (a *alertRuleInfo) stop(reason error) { } //nolint:gocyclo -func (a *alertRuleInfo) ruleRoutine(key ngmodels.AlertRuleKey, sch *schedule) error { +func (a *alertRuleInfo) run(key ngmodels.AlertRuleKey) error { grafanaCtx := ngmodels.WithRuleKey(a.ctx, key) - logger := sch.log.FromContext(grafanaCtx) + logger := a.logger.FromContext(grafanaCtx) logger.Debug("Alert rule routine started") orgID := fmt.Sprint(key.OrgID) - evalTotal := sch.metrics.EvalTotal.WithLabelValues(orgID) - evalDuration := sch.metrics.EvalDuration.WithLabelValues(orgID) - evalTotalFailures := sch.metrics.EvalFailures.WithLabelValues(orgID) - processDuration := sch.metrics.ProcessDuration.WithLabelValues(orgID) - sendDuration := sch.metrics.SendDuration.WithLabelValues(orgID) + evalTotal := a.metrics.EvalTotal.WithLabelValues(orgID) + evalDuration := a.metrics.EvalDuration.WithLabelValues(orgID) + evalTotalFailures := a.metrics.EvalFailures.WithLabelValues(orgID) + processDuration := a.metrics.ProcessDuration.WithLabelValues(orgID) + sendDuration := a.metrics.SendDuration.WithLabelValues(orgID) notify := func(states []state.StateTransition) { - expiredAlerts := state.FromAlertsStateToStoppedAlert(states, sch.appURL, sch.clock) + expiredAlerts := state.FromAlertsStateToStoppedAlert(states, a.appURL, a.clock) if len(expiredAlerts.PostableAlerts) > 0 { - sch.alertsSender.Send(grafanaCtx, key, expiredAlerts) + a.sender.Send(grafanaCtx, key, expiredAlerts) } } resetState := func(ctx context.Context, isPaused bool) { - rule := sch.schedulableAlertRules.get(key) + rule := a.ruleProvider.get(key) reason := ngmodels.StateReasonUpdated if isPaused { reason = ngmodels.StateReasonPaused } - states := sch.stateManager.ResetStateByRuleUID(ctx, rule, reason) + states := a.stateManager.ResetStateByRuleUID(ctx, rule, reason) notify(states) } evaluate := func(ctx context.Context, f fingerprint, attempt int64, e *evaluation, span trace.Span, retry bool) error { logger := logger.New("version", e.rule.Version, "fingerprint", f, "attempt", attempt, "now", e.scheduledAt).FromContext(ctx) - start := sch.clock.Now() + start := a.clock.Now() - evalCtx := eval.NewContextWithPreviousResults(ctx, SchedulerUserFor(e.rule.OrgID), sch.newLoadedMetricsReader(e.rule)) - if sch.evaluatorFactory == nil { - panic("evalfactory nil") - } - ruleEval, err := sch.evaluatorFactory.Create(evalCtx, e.rule.GetEvalCondition()) + evalCtx := eval.NewContextWithPreviousResults(ctx, SchedulerUserFor(e.rule.OrgID), a.newLoadedMetricsReader(e.rule)) + ruleEval, err := a.evalFactory.Create(evalCtx, e.rule.GetEvalCondition()) var results eval.Results var dur time.Duration if err != nil { - dur = sch.clock.Now().Sub(start) + dur = a.clock.Now().Sub(start) logger.Error("Failed to build rule evaluator", "error", err) } else { results, err = ruleEval.Evaluate(ctx, e.scheduledAt) - dur = sch.clock.Now().Sub(start) + dur = a.clock.Now().Sub(start) if err != nil { logger.Error("Failed to evaluate rule", "error", err, "duration", dur) } @@ -181,33 +277,33 @@ func (a *alertRuleInfo) ruleRoutine(key ngmodels.AlertRuleKey, sch *schedule) er attribute.Int64("results", int64(len(results))), )) } - start = sch.clock.Now() - processedStates := sch.stateManager.ProcessEvalResults( + start = a.clock.Now() + processedStates := a.stateManager.ProcessEvalResults( ctx, e.scheduledAt, e.rule, results, - state.GetRuleExtraLabels(logger, e.rule, e.folderTitle, !sch.disableGrafanaFolder), + state.GetRuleExtraLabels(logger, e.rule, e.folderTitle, !a.disableGrafanaFolder), ) - processDuration.Observe(sch.clock.Now().Sub(start).Seconds()) + processDuration.Observe(a.clock.Now().Sub(start).Seconds()) - start = sch.clock.Now() - alerts := state.FromStateTransitionToPostableAlerts(processedStates, sch.stateManager, sch.appURL) + start = a.clock.Now() + alerts := state.FromStateTransitionToPostableAlerts(processedStates, a.stateManager, a.appURL) span.AddEvent("results processed", trace.WithAttributes( attribute.Int64("state_transitions", int64(len(processedStates))), attribute.Int64("alerts_to_send", int64(len(alerts.PostableAlerts))), )) if len(alerts.PostableAlerts) > 0 { - sch.alertsSender.Send(ctx, key, alerts) + a.sender.Send(ctx, key, alerts) } - sendDuration.Observe(sch.clock.Now().Sub(start).Seconds()) + sendDuration.Observe(a.clock.Now().Sub(start).Seconds()) return nil } evalRunning := false var currentFingerprint fingerprint - defer sch.stopApplied(key) + defer a.stopApplied(key) for { select { // used by external services (API) to notify that rule is updated. @@ -235,10 +331,10 @@ func (a *alertRuleInfo) ruleRoutine(key ngmodels.AlertRuleKey, sch *schedule) er evalRunning = true defer func() { evalRunning = false - sch.evalApplied(key, ctx.scheduledAt) + a.evalApplied(key, ctx.scheduledAt) }() - for attempt := int64(1); attempt <= sch.maxAttempts; attempt++ { + for attempt := int64(1); attempt <= a.maxAttempts; attempt++ { isPaused := ctx.rule.IsPaused f := ruleWithFolder{ctx.rule, ctx.folderTitle}.Fingerprint() // Do not clean up state if the eval loop has just started. @@ -262,7 +358,7 @@ func (a *alertRuleInfo) ruleRoutine(key ngmodels.AlertRuleKey, sch *schedule) er fpStr := currentFingerprint.String() utcTick := ctx.scheduledAt.UTC().Format(time.RFC3339Nano) - tracingCtx, span := sch.tracer.Start(grafanaCtx, "alert rule execution", trace.WithAttributes( + tracingCtx, span := a.tracer.Start(grafanaCtx, "alert rule execution", trace.WithAttributes( attribute.String("rule_uid", ctx.rule.UID), attribute.Int64("org_id", ctx.rule.OrgID), attribute.Int64("rule_version", ctx.rule.Version), @@ -278,7 +374,7 @@ func (a *alertRuleInfo) ruleRoutine(key ngmodels.AlertRuleKey, sch *schedule) er return } - retry := attempt < sch.maxAttempts + retry := attempt < a.maxAttempts err := evaluate(tracingCtx, f, attempt, ctx, span, retry) // This is extremely confusing - when we exhaust all retry attempts, or we have no retryable errors // we return nil - so technically, this is meaningless to know whether the evaluation has errors or not. @@ -306,7 +402,7 @@ func (a *alertRuleInfo) ruleRoutine(key ngmodels.AlertRuleKey, sch *schedule) er // cases. ctx, cancelFunc := context.WithTimeout(context.Background(), time.Minute) defer cancelFunc() - states := sch.stateManager.DeleteStateByRuleUID(ngmodels.WithRuleKey(ctx, key), key, ngmodels.StateReasonRuleDeleted) + states := a.stateManager.DeleteStateByRuleUID(ngmodels.WithRuleKey(ctx, key), key, ngmodels.StateReasonRuleDeleted) notify(states) } logger.Debug("Stopping alert rule routine") @@ -316,21 +412,21 @@ func (a *alertRuleInfo) ruleRoutine(key ngmodels.AlertRuleKey, sch *schedule) er } // evalApplied is only used on tests. -func (sch *schedule) evalApplied(alertDefKey ngmodels.AlertRuleKey, now time.Time) { - if sch.evalAppliedFunc == nil { +func (a *alertRuleInfo) evalApplied(alertDefKey ngmodels.AlertRuleKey, now time.Time) { + if a.evalAppliedHook == nil { return } - sch.evalAppliedFunc(alertDefKey, now) + a.evalAppliedHook(alertDefKey, now) } // stopApplied is only used on tests. -func (sch *schedule) stopApplied(alertDefKey ngmodels.AlertRuleKey) { - if sch.stopAppliedFunc == nil { +func (a *alertRuleInfo) stopApplied(alertDefKey ngmodels.AlertRuleKey) { + if a.stopAppliedHook == nil { return } - sch.stopAppliedFunc(alertDefKey) + a.stopAppliedHook(alertDefKey) } func SchedulerUserFor(orgID int64) *user.SignedInUser { diff --git a/pkg/services/ngalert/schedule/alert_rule_test.go b/pkg/services/ngalert/schedule/alert_rule_test.go index e6f3324401..7b383deeaa 100644 --- a/pkg/services/ngalert/schedule/alert_rule_test.go +++ b/pkg/services/ngalert/schedule/alert_rule_test.go @@ -34,7 +34,7 @@ func TestAlertRuleInfo(t *testing.T) { t.Run("when rule evaluation is not stopped", func(t *testing.T) { t.Run("update should send to updateCh", func(t *testing.T) { - r := newAlertRuleInfo(context.Background()) + r := blankRuleInfoForTests(context.Background()) resultCh := make(chan bool) go func() { resultCh <- r.update(ruleVersionAndPauseStatus{fingerprint(rand.Uint64()), false}) @@ -47,7 +47,7 @@ func TestAlertRuleInfo(t *testing.T) { } }) t.Run("update should drop any concurrent sending to updateCh", func(t *testing.T) { - r := newAlertRuleInfo(context.Background()) + r := blankRuleInfoForTests(context.Background()) version1 := ruleVersionAndPauseStatus{fingerprint(rand.Uint64()), false} version2 := ruleVersionAndPauseStatus{fingerprint(rand.Uint64()), false} @@ -73,7 +73,7 @@ func TestAlertRuleInfo(t *testing.T) { } }) t.Run("eval should send to evalCh", func(t *testing.T) { - r := newAlertRuleInfo(context.Background()) + r := blankRuleInfoForTests(context.Background()) expected := time.Now() resultCh := make(chan evalResponse) data := &evaluation{ @@ -96,7 +96,7 @@ func TestAlertRuleInfo(t *testing.T) { } }) t.Run("eval should drop any concurrent sending to evalCh", func(t *testing.T) { - r := newAlertRuleInfo(context.Background()) + r := blankRuleInfoForTests(context.Background()) time1 := time.UnixMilli(rand.Int63n(math.MaxInt64)) time2 := time.UnixMilli(rand.Int63n(math.MaxInt64)) resultCh1 := make(chan evalResponse) @@ -142,7 +142,7 @@ func TestAlertRuleInfo(t *testing.T) { } }) t.Run("eval should exit when context is cancelled", func(t *testing.T) { - r := newAlertRuleInfo(context.Background()) + r := blankRuleInfoForTests(context.Background()) resultCh := make(chan evalResponse) data := &evaluation{ scheduledAt: time.Now(), @@ -166,13 +166,13 @@ func TestAlertRuleInfo(t *testing.T) { }) t.Run("when rule evaluation is stopped", func(t *testing.T) { t.Run("Update should do nothing", func(t *testing.T) { - r := newAlertRuleInfo(context.Background()) + r := blankRuleInfoForTests(context.Background()) r.stop(errRuleDeleted) require.ErrorIs(t, r.ctx.Err(), errRuleDeleted) require.False(t, r.update(ruleVersionAndPauseStatus{fingerprint(rand.Uint64()), false})) }) t.Run("eval should do nothing", func(t *testing.T) { - r := newAlertRuleInfo(context.Background()) + r := blankRuleInfoForTests(context.Background()) r.stop(nil) data := &evaluation{ scheduledAt: time.Now(), @@ -184,19 +184,19 @@ func TestAlertRuleInfo(t *testing.T) { require.Nilf(t, dropped, "expected no dropped evaluations but got one") }) t.Run("stop should do nothing", func(t *testing.T) { - r := newAlertRuleInfo(context.Background()) + r := blankRuleInfoForTests(context.Background()) r.stop(nil) r.stop(nil) }) t.Run("stop should do nothing if parent context stopped", func(t *testing.T) { ctx, cancelFn := context.WithCancel(context.Background()) - r := newAlertRuleInfo(ctx) + r := blankRuleInfoForTests(ctx) cancelFn() r.stop(nil) }) }) t.Run("should be thread-safe", func(t *testing.T) { - r := newAlertRuleInfo(context.Background()) + r := blankRuleInfoForTests(context.Background()) wg := sync.WaitGroup{} go func() { for { @@ -240,6 +240,11 @@ func TestAlertRuleInfo(t *testing.T) { }) } +func blankRuleInfoForTests(ctx context.Context) *alertRuleInfo { + factory := newRuleFactory(nil, false, 0, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil) + return factory.new(context.Background()) +} + func TestRuleRoutine(t *testing.T) { createSchedule := func( evalAppliedChan chan time.Time, @@ -269,11 +274,12 @@ func TestRuleRoutine(t *testing.T) { rule := models.AlertRuleGen(withQueryForState(t, evalState))() ruleStore.PutRule(context.Background(), rule) folderTitle := ruleStore.getNamespaceTitle(rule.NamespaceUID) + factory := ruleFactoryFromScheduler(sch) ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) - ruleInfo := newAlertRuleInfo(ctx) + ruleInfo := factory.new(ctx) go func() { - _ = ruleInfo.ruleRoutine(rule.GetKey(), sch) + _ = ruleInfo.run(rule.GetKey()) }() expectedTime := time.UnixMicro(rand.Int63()) @@ -418,10 +424,11 @@ func TestRuleRoutine(t *testing.T) { expectedStates := sch.stateManager.GetStatesForRuleUID(rule.OrgID, rule.UID) require.NotEmpty(t, expectedStates) + factory := ruleFactoryFromScheduler(sch) ctx, cancel := context.WithCancel(context.Background()) - ruleInfo := newAlertRuleInfo(ctx) + ruleInfo := factory.new(ctx) go func() { - err := ruleInfo.ruleRoutine(models.AlertRuleKey{}, sch) + err := ruleInfo.run(models.AlertRuleKey{}) stoppedChan <- err }() @@ -438,9 +445,10 @@ func TestRuleRoutine(t *testing.T) { _ = sch.stateManager.ProcessEvalResults(context.Background(), sch.clock.Now(), rule, eval.GenerateResults(rand.Intn(5)+1, eval.ResultGen(eval.WithEvaluatedAt(sch.clock.Now()))), nil) require.NotEmpty(t, sch.stateManager.GetStatesForRuleUID(rule.OrgID, rule.UID)) - ruleInfo := newAlertRuleInfo(context.Background()) + factory := ruleFactoryFromScheduler(sch) + ruleInfo := factory.new(context.Background()) go func() { - err := ruleInfo.ruleRoutine(rule.GetKey(), sch) + err := ruleInfo.run(rule.GetKey()) stoppedChan <- err }() @@ -465,12 +473,13 @@ func TestRuleRoutine(t *testing.T) { sch, ruleStore, _, _ := createSchedule(evalAppliedChan, sender) ruleStore.PutRule(context.Background(), rule) sch.schedulableAlertRules.set([]*models.AlertRule{rule}, map[models.FolderKey]string{rule.GetFolderKey(): folderTitle}) + factory := ruleFactoryFromScheduler(sch) ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) - ruleInfo := newAlertRuleInfo(ctx) + ruleInfo := factory.new(ctx) go func() { - _ = ruleInfo.ruleRoutine(rule.GetKey(), sch) + _ = ruleInfo.run(rule.GetKey()) }() // init evaluation loop so it got the rule version @@ -546,12 +555,13 @@ func TestRuleRoutine(t *testing.T) { sch, ruleStore, _, reg := createSchedule(evalAppliedChan, sender) sch.maxAttempts = 3 ruleStore.PutRule(context.Background(), rule) + factory := ruleFactoryFromScheduler(sch) ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) - ruleInfo := newAlertRuleInfo(ctx) + ruleInfo := factory.new(ctx) go func() { - _ = ruleInfo.ruleRoutine(rule.GetKey(), sch) + _ = ruleInfo.run(rule.GetKey()) }() ruleInfo.evalCh <- &evaluation{ @@ -651,12 +661,13 @@ func TestRuleRoutine(t *testing.T) { sch, ruleStore, _, _ := createSchedule(evalAppliedChan, sender) ruleStore.PutRule(context.Background(), rule) + factory := ruleFactoryFromScheduler(sch) ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) - ruleInfo := newAlertRuleInfo(ctx) + ruleInfo := factory.new(ctx) go func() { - _ = ruleInfo.ruleRoutine(rule.GetKey(), sch) + _ = ruleInfo.run(rule.GetKey()) }() ruleInfo.evalCh <- &evaluation{ @@ -684,12 +695,13 @@ func TestRuleRoutine(t *testing.T) { sch, ruleStore, _, _ := createSchedule(evalAppliedChan, sender) ruleStore.PutRule(context.Background(), rule) + factory := ruleFactoryFromScheduler(sch) ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) - ruleInfo := newAlertRuleInfo(ctx) + ruleInfo := factory.new(ctx) go func() { - _ = ruleInfo.ruleRoutine(rule.GetKey(), sch) + _ = ruleInfo.run(rule.GetKey()) }() ruleInfo.evalCh <- &evaluation{ @@ -704,3 +716,7 @@ func TestRuleRoutine(t *testing.T) { require.NotEmpty(t, sch.stateManager.GetStatesForRuleUID(rule.OrgID, rule.UID)) }) } + +func ruleFactoryFromScheduler(sch *schedule) ruleFactory { + return newRuleFactory(sch.appURL, sch.disableGrafanaFolder, sch.maxAttempts, sch.alertsSender, sch.stateManager, sch.evaluatorFactory, &sch.schedulableAlertRules, sch.clock, sch.metrics, sch.log, sch.tracer, sch.evalAppliedFunc, sch.stopAppliedFunc) +} diff --git a/pkg/services/ngalert/schedule/loaded_metrics_reader.go b/pkg/services/ngalert/schedule/loaded_metrics_reader.go index cc3f962b4a..d6c1aa9736 100644 --- a/pkg/services/ngalert/schedule/loaded_metrics_reader.go +++ b/pkg/services/ngalert/schedule/loaded_metrics_reader.go @@ -10,9 +10,9 @@ import ( var _ eval.AlertingResultsReader = AlertingResultsFromRuleState{} -func (sch *schedule) newLoadedMetricsReader(rule *ngmodels.AlertRule) eval.AlertingResultsReader { +func (a *alertRuleInfo) newLoadedMetricsReader(rule *ngmodels.AlertRule) eval.AlertingResultsReader { return &AlertingResultsFromRuleState{ - Manager: sch.stateManager, + Manager: a.stateManager, Rule: rule, } } diff --git a/pkg/services/ngalert/schedule/registry.go b/pkg/services/ngalert/schedule/registry.go index 2a7cc7c705..e7401bc1c2 100644 --- a/pkg/services/ngalert/schedule/registry.go +++ b/pkg/services/ngalert/schedule/registry.go @@ -17,6 +17,10 @@ import ( var errRuleDeleted = errors.New("rule deleted") +type ruleFactory interface { + new(context.Context) *alertRuleInfo +} + type alertRuleInfoRegistry struct { mu sync.Mutex alertRuleInfo map[models.AlertRuleKey]*alertRuleInfo @@ -24,13 +28,13 @@ type alertRuleInfoRegistry struct { // getOrCreateInfo gets rule routine information from registry by the key. If it does not exist, it creates a new one. // Returns a pointer to the rule routine information and a flag that indicates whether it is a new struct or not. -func (r *alertRuleInfoRegistry) getOrCreateInfo(context context.Context, key models.AlertRuleKey) (*alertRuleInfo, bool) { +func (r *alertRuleInfoRegistry) getOrCreateInfo(context context.Context, key models.AlertRuleKey, factory ruleFactory) (*alertRuleInfo, bool) { r.mu.Lock() defer r.mu.Unlock() info, ok := r.alertRuleInfo[key] if !ok { - info = newAlertRuleInfo(context) + info = factory.new(context) r.alertRuleInfo[key] = info } return info, !ok diff --git a/pkg/services/ngalert/schedule/schedule.go b/pkg/services/ngalert/schedule/schedule.go index c25631302f..075cc9f985 100644 --- a/pkg/services/ngalert/schedule/schedule.go +++ b/pkg/services/ngalert/schedule/schedule.go @@ -235,9 +235,24 @@ func (sch *schedule) processTick(ctx context.Context, dispatcherGroup *errgroup. readyToRun := make([]readyToRunItem, 0) updatedRules := make([]ngmodels.AlertRuleKeyWithVersion, 0, len(updated)) // this is needed for tests only missingFolder := make(map[string][]string) + ruleFactory := newRuleFactory( + sch.appURL, + sch.disableGrafanaFolder, + sch.maxAttempts, + sch.alertsSender, + sch.stateManager, + sch.evaluatorFactory, + &sch.schedulableAlertRules, + sch.clock, + sch.metrics, + sch.log, + sch.tracer, + sch.evalAppliedFunc, + sch.stopAppliedFunc, + ) for _, item := range alertRules { key := item.GetKey() - ruleInfo, newRoutine := sch.registry.getOrCreateInfo(ctx, key) + ruleInfo, newRoutine := sch.registry.getOrCreateInfo(ctx, key, ruleFactory) // enforce minimum evaluation interval if item.IntervalSeconds < int64(sch.minRuleInterval.Seconds()) { @@ -249,7 +264,7 @@ func (sch *schedule) processTick(ctx context.Context, dispatcherGroup *errgroup. if newRoutine && !invalidInterval { dispatcherGroup.Go(func() error { - return ruleInfo.ruleRoutine(key, sch) + return ruleInfo.run(key) }) } diff --git a/pkg/services/ngalert/schedule/schedule_unit_test.go b/pkg/services/ngalert/schedule/schedule_unit_test.go index 787149d919..38b4d26cdf 100644 --- a/pkg/services/ngalert/schedule/schedule_unit_test.go +++ b/pkg/services/ngalert/schedule/schedule_unit_test.go @@ -360,9 +360,10 @@ func TestSchedule_deleteAlertRule(t *testing.T) { t.Run("when rule exists", func(t *testing.T) { t.Run("it should stop evaluation loop and remove the controller from registry", func(t *testing.T) { sch := setupScheduler(t, nil, nil, nil, nil, nil) + ruleFactory := ruleFactoryFromScheduler(sch) rule := models.AlertRuleGen()() key := rule.GetKey() - info, _ := sch.registry.getOrCreateInfo(context.Background(), key) + info, _ := sch.registry.getOrCreateInfo(context.Background(), key, ruleFactory) sch.deleteAlertRule(key) require.ErrorIs(t, info.ctx.Err(), errRuleDeleted) require.False(t, sch.registry.exists(key)) From 948c8c45d686877db25a79424343b423a9574b65 Mon Sep 17 00:00:00 2001 From: gotjosh Date: Wed, 6 Mar 2024 20:48:32 +0000 Subject: [PATCH 0446/1406] Alerting: Use Alertmanager types extracted into grafana/alerting (#83824) * Alerting: Use Alertmanager types extracted into grafana/alerting We're in the process of exporting all Alertmanager types into grafana/alerting so that they can be imported in the Mimir Alertmanager, without a neeed to import Grafana directly. This change introduces type aliasing for all Alertmanager types based on their 1:1 copy that now live in grafana/alerting. Signed-off-by: gotjosh --------- Signed-off-by: gotjosh --- go.mod | 2 +- go.sum | 4 +- pkg/services/ngalert/api/tooling/api.json | 385 +----- .../api/tooling/definitions/alertmanager.go | 612 +-------- .../tooling/definitions/alertmanager_test.go | 1104 ----------------- .../definitions/alertmanager_validation.go | 92 -- .../alertmanager_validation_test.go | 12 +- pkg/services/ngalert/api/tooling/post.json | 7 +- pkg/services/ngalert/api/tooling/spec.json | 7 +- public/api-merged.json | 6 + public/openapi3.json | 6 + 11 files changed, 60 insertions(+), 2177 deletions(-) diff --git a/go.mod b/go.mod index f03f6ef046..c99e625f09 100644 --- a/go.mod +++ b/go.mod @@ -59,7 +59,7 @@ require ( github.com/google/uuid v1.6.0 // @grafana/backend-platform github.com/google/wire v0.5.0 // @grafana/backend-platform github.com/gorilla/websocket v1.5.0 // @grafana/grafana-app-platform-squad - github.com/grafana/alerting v0.0.0-20240304175322-e81931acc11b // @grafana/alerting-squad-backend + github.com/grafana/alerting v0.0.0-20240306130925-bc622368256d // @grafana/alerting-squad-backend github.com/grafana/cuetsy v0.1.11 // @grafana/grafana-as-code github.com/grafana/grafana-aws-sdk v0.24.0 // @grafana/aws-datasources github.com/grafana/grafana-azure-sdk-go v1.12.0 // @grafana/partner-datasources diff --git a/go.sum b/go.sum index 4a635294b6..14762a4030 100644 --- a/go.sum +++ b/go.sum @@ -2166,8 +2166,8 @@ github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grafana/alerting v0.0.0-20240304175322-e81931acc11b h1:rYx9ds94ZrueuXioEnoSqL737UYPSngPkMwBFl1guJE= -github.com/grafana/alerting v0.0.0-20240304175322-e81931acc11b/go.mod h1:brTFeACal/cSZAR8XO/4LPKs7rzNfS86okl6QjSP1eY= +github.com/grafana/alerting v0.0.0-20240306130925-bc622368256d h1:YxLsj/C75sW90gzYK27XEaJ1sL89lYxuntmHaytFP80= +github.com/grafana/alerting v0.0.0-20240306130925-bc622368256d/go.mod h1:0nHKO0w8OTemvZ3eh7+s1EqGGhgbs0kvkTeLU1FrbTw= github.com/grafana/codejen v0.0.3 h1:tAWxoTUuhgmEqxJPOLtJoxlPBbMULFwKFOcRsPRPXDw= github.com/grafana/codejen v0.0.3/go.mod h1:zmwwM/DRyQB7pfuBjTWII3CWtxcXh8LTwAYGfDfpR6s= github.com/grafana/cue v0.0.0-20230926092038-971951014e3f h1:TmYAMnqg3d5KYEAaT6PtTguL2GjLfvr6wnAX8Azw6tQ= diff --git a/pkg/services/ngalert/api/tooling/api.json b/pkg/services/ngalert/api/tooling/api.json index be0f9033b1..9395f274c5 100644 --- a/pkg/services/ngalert/api/tooling/api.json +++ b/pkg/services/ngalert/api/tooling/api.json @@ -398,9 +398,6 @@ }, "type": "object" }, - "AlertStateType": { - "type": "string" - }, "AlertingFileExport": { "properties": { "apiVersion": { @@ -528,80 +525,6 @@ }, "type": "object" }, - "Annotation": { - "properties": { - "alertId": { - "format": "int64", - "type": "integer" - }, - "alertName": { - "type": "string" - }, - "avatarUrl": { - "type": "string" - }, - "created": { - "format": "int64", - "type": "integer" - }, - "dashboardId": { - "format": "int64", - "type": "integer" - }, - "dashboardUID": { - "type": "string" - }, - "data": { - "$ref": "#/definitions/Json" - }, - "email": { - "type": "string" - }, - "id": { - "format": "int64", - "type": "integer" - }, - "login": { - "type": "string" - }, - "newState": { - "type": "string" - }, - "panelId": { - "format": "int64", - "type": "integer" - }, - "prevState": { - "type": "string" - }, - "tags": { - "items": { - "type": "string" - }, - "type": "array" - }, - "text": { - "type": "string" - }, - "time": { - "format": "int64", - "type": "integer" - }, - "timeEnd": { - "format": "int64", - "type": "integer" - }, - "updated": { - "format": "int64", - "type": "integer" - }, - "userId": { - "format": "int64", - "type": "integer" - } - }, - "type": "object" - }, "ApiRuleNode": { "properties": { "alert": { @@ -816,75 +739,12 @@ }, "type": "array" }, - "CookieType": { - "type": "string" - }, "CounterResetHint": { "description": "or alternatively that we are dealing with a gauge histogram, where counter resets do not apply.", "format": "uint8", "title": "CounterResetHint contains the known information about a counter reset,", "type": "integer" }, - "CreateLibraryElementCommand": { - "description": "CreateLibraryElementCommand is the command for adding a LibraryElement", - "properties": { - "folderId": { - "description": "ID of the folder where the library element is stored.\n\nDeprecated: use FolderUID instead", - "format": "int64", - "type": "integer" - }, - "folderUid": { - "description": "UID of the folder where the library element is stored.", - "type": "string" - }, - "kind": { - "description": "Kind of element to create, Use 1 for library panels or 2 for c.\nDescription:\n1 - library panels\n2 - library variables", - "enum": [ - 1, - 2 - ], - "format": "int64", - "type": "integer" - }, - "model": { - "description": "The JSON model for the library element.", - "type": "object" - }, - "name": { - "description": "Name of the library element.", - "type": "string" - }, - "uid": { - "type": "string" - } - }, - "type": "object" - }, - "DashboardACLUpdateItem": { - "properties": { - "permission": { - "$ref": "#/definitions/PermissionType" - }, - "role": { - "enum": [ - "None", - "Viewer", - "Editor", - "Admin" - ], - "type": "string" - }, - "teamId": { - "format": "int64", - "type": "integer" - }, - "userId": { - "format": "int64", - "type": "integer" - } - }, - "type": "object" - }, "DashboardUpgrade": { "properties": { "dashboardId": { @@ -2327,48 +2187,6 @@ }, "type": "array" }, - "MetricRequest": { - "properties": { - "debug": { - "type": "boolean" - }, - "from": { - "description": "From Start time in epoch timestamps in milliseconds or relative using Grafana time units.", - "example": "now-1h", - "type": "string" - }, - "queries": { - "description": "queries.refId – Specifies an identifier of the query. Is optional and default to “A”.\nqueries.datasourceId – Specifies the data source to be queried. Each query in the request must have an unique datasourceId.\nqueries.maxDataPoints - Species maximum amount of data points that dashboard panel can render. Is optional and default to 100.\nqueries.intervalMs - Specifies the time interval in milliseconds of time series. Is optional and defaults to 1000.", - "example": [ - { - "datasource": { - "uid": "PD8C576611E62080A" - }, - "format": "table", - "intervalMs": 86400000, - "maxDataPoints": 1092, - "rawSql": "SELECT 1 as valueOne, 2 as valueTwo", - "refId": "A" - } - ], - "items": { - "$ref": "#/definitions/Json" - }, - "type": "array" - }, - "to": { - "description": "To End time in epoch timestamps in milliseconds or relative using Grafana time units.", - "example": "now", - "type": "string" - } - }, - "required": [ - "from", - "to", - "queries" - ], - "type": "object" - }, "MultiStatus": { "type": "object" }, @@ -2420,24 +2238,6 @@ }, "type": "object" }, - "NewApiKeyResult": { - "properties": { - "id": { - "example": 1, - "format": "int64", - "type": "integer" - }, - "key": { - "example": "glsa_yscW25imSKJIuav8zF37RZmnbiDvB05G_fcaaf58a", - "type": "string" - }, - "name": { - "example": "grafana", - "type": "string" - } - }, - "type": "object" - }, "NotFound": { "type": "object" }, @@ -2841,76 +2641,9 @@ }, "type": "object" }, - "PatchPrefsCmd": { - "properties": { - "cookies": { - "items": { - "$ref": "#/definitions/CookieType" - }, - "type": "array" - }, - "homeDashboardId": { - "default": 0, - "description": "The numerical :id of a favorited dashboard", - "format": "int64", - "type": "integer" - }, - "homeDashboardUID": { - "type": "string" - }, - "language": { - "type": "string" - }, - "queryHistory": { - "$ref": "#/definitions/QueryHistoryPreference" - }, - "theme": { - "enum": [ - "light", - "dark" - ], - "type": "string" - }, - "timezone": { - "enum": [ - "utc", - "browser" - ], - "type": "string" - }, - "weekStart": { - "type": "string" - } - }, - "type": "object" - }, - "Permission": { - "properties": { - "action": { - "type": "string" - }, - "created": { - "format": "date-time", - "type": "string" - }, - "scope": { - "type": "string" - }, - "updated": { - "format": "date-time", - "type": "string" - } - }, - "title": "Permission is the model for access control permissions.", - "type": "object" - }, "PermissionDenied": { "type": "object" }, - "PermissionType": { - "format": "int64", - "type": "integer" - }, "PostableApiAlertingConfig": { "properties": { "global": { @@ -3499,14 +3232,6 @@ }, "type": "object" }, - "QueryHistoryPreference": { - "properties": { - "homeTab": { - "type": "string" - } - }, - "type": "object" - }, "QueryStat": { "description": "The embedded FieldConfig's display name must be set.\nIt corresponds to the QueryResultMetaStat on the frontend (https://github.com/grafana/grafana/blob/master/packages/grafana-data/src/types/data.ts#L53).", "properties": { @@ -3741,53 +3466,6 @@ "title": "Responses is a map of RefIDs (Unique Query ID) to DataResponses.", "type": "object" }, - "RoleDTO": { - "properties": { - "created": { - "format": "date-time", - "type": "string" - }, - "delegatable": { - "type": "boolean" - }, - "description": { - "type": "string" - }, - "displayName": { - "type": "string" - }, - "global": { - "type": "boolean" - }, - "group": { - "type": "string" - }, - "hidden": { - "type": "boolean" - }, - "name": { - "type": "string" - }, - "permissions": { - "items": { - "$ref": "#/definitions/Permission" - }, - "type": "array" - }, - "uid": { - "type": "string" - }, - "updated": { - "format": "date-time", - "type": "string" - }, - "version": { - "format": "int64", - "type": "integer" - } - }, - "type": "object" - }, "Route": { "description": "A Route is a node that contains definitions of how to handle alerts. This is modified\nfrom the upstream alertmanager in that it adds the ObjectMatchers property.", "properties": { @@ -4676,6 +4354,7 @@ "type": "object" }, "URL": { + "description": "The general form represented is:\n\n[scheme:][//[userinfo@]host][/]path[?query][#fragment]\n\nURLs that do not start with a slash after the scheme are interpreted as:\n\nscheme:opaque[?query][#fragment]\n\nNote that the Path field is stored in decoded form: /%47%6f%2f becomes /Go/.\nA consequence is that it is impossible to tell which slashes in the Path were\nslashes in the raw URL and which were %2f. This distinction is rarely important,\nbut when it is, the code should use the EscapedPath method, which preserves\nthe original encoding of Path.\n\nThe RawPath field is an optional field which is only set when the default\nencoding of Path is different from the escaped path. See the EscapedPath method\nfor more details.\n\nURL's String method uses the EscapedPath method to obtain the path.", "properties": { "ForceQuery": { "type": "boolean" @@ -4711,62 +4390,7 @@ "$ref": "#/definitions/Userinfo" } }, - "title": "URL is a custom URL type that allows validation at configuration load time.", - "type": "object" - }, - "UpdateDashboardACLCommand": { - "properties": { - "items": { - "items": { - "$ref": "#/definitions/DashboardACLUpdateItem" - }, - "type": "array" - } - }, - "type": "object" - }, - "UpdatePrefsCmd": { - "properties": { - "cookies": { - "items": { - "$ref": "#/definitions/CookieType" - }, - "type": "array" - }, - "homeDashboardId": { - "default": 0, - "description": "The numerical :id of a favorited dashboard", - "format": "int64", - "type": "integer" - }, - "homeDashboardUID": { - "type": "string" - }, - "language": { - "type": "string" - }, - "queryHistory": { - "$ref": "#/definitions/QueryHistoryPreference" - }, - "theme": { - "enum": [ - "light", - "dark", - "system" - ], - "type": "string" - }, - "timezone": { - "enum": [ - "utc", - "browser" - ], - "type": "string" - }, - "weekStart": { - "type": "string" - } - }, + "title": "A URL represents a parsed URL (technically, a URI reference).", "type": "object" }, "UpdateRuleGroupResponse": { @@ -4972,6 +4596,7 @@ "type": "object" }, "alertGroup": { + "description": "AlertGroup alert group", "properties": { "alerts": { "description": "alerts", @@ -4995,6 +4620,7 @@ "type": "object" }, "alertGroups": { + "description": "AlertGroups alert groups", "items": { "$ref": "#/definitions/alertGroup" }, @@ -5099,6 +4725,7 @@ "type": "object" }, "gettableAlert": { + "description": "GettableAlert gettable alert", "properties": { "annotations": { "$ref": "#/definitions/labelSet" @@ -5161,6 +4788,7 @@ "type": "array" }, "gettableSilence": { + "description": "GettableSilence gettable silence", "properties": { "comment": { "description": "comment", @@ -5215,6 +4843,7 @@ "type": "array" }, "integration": { + "description": "Integration integration", "properties": { "lastNotifyAttempt": { "description": "A timestamp indicating the last attempt to deliver a notification regardless of the outcome.\nFormat: date-time", diff --git a/pkg/services/ngalert/api/tooling/definitions/alertmanager.go b/pkg/services/ngalert/api/tooling/definitions/alertmanager.go index 438d1f85f9..3a9b6cc09b 100644 --- a/pkg/services/ngalert/api/tooling/definitions/alertmanager.go +++ b/pkg/services/ngalert/api/tooling/definitions/alertmanager.go @@ -4,15 +4,12 @@ import ( "context" "encoding/json" "fmt" - "reflect" - "sort" - "strings" "time" "github.com/go-openapi/strfmt" + "github.com/grafana/alerting/definition" amv2 "github.com/prometheus/alertmanager/api/v2/models" "github.com/prometheus/alertmanager/config" - "github.com/prometheus/alertmanager/pkg/labels" "github.com/prometheus/common/model" "gopkg.in/yaml.v3" ) @@ -246,6 +243,31 @@ import ( // 400: ValidationError // 404: NotFound +// Alias all the needed Alertmanager types, functions and constants so that they can be imported directly from grafana/alerting +// without having to modify any of the usage within Grafana. +type ( + Config = definition.Config + Route = definition.Route + PostableGrafanaReceiver = definition.PostableGrafanaReceiver + PostableApiAlertingConfig = definition.PostableApiAlertingConfig + RawMessage = definition.RawMessage + Provenance = definition.Provenance + ObjectMatchers = definition.ObjectMatchers + PostableApiReceiver = definition.PostableApiReceiver + PostableGrafanaReceivers = definition.PostableGrafanaReceivers + ReceiverType = definition.ReceiverType +) + +const ( + GrafanaReceiverType = definition.GrafanaReceiverType + AlertmanagerReceiverType = definition.AlertmanagerReceiverType +) + +var ( + AsGrafanaRoute = definition.AsGrafanaRoute + AllReceivers = definition.AllReceivers +) + // swagger:model type PermissionDenied struct{} @@ -646,8 +668,6 @@ func (c *PostableUserConfig) UnmarshalYAML(value *yaml.Node) error { return nil } -type Provenance string - // swagger:model type GettableUserConfig struct { TemplateFiles map[string]string `yaml:"template_files" json:"template_files"` @@ -800,360 +820,6 @@ func (c *GettableApiAlertingConfig) validate() error { return nil } -// Config is the top-level configuration for Alertmanager's config files. -type Config struct { - Global *config.GlobalConfig `yaml:"global,omitempty" json:"global,omitempty"` - Route *Route `yaml:"route,omitempty" json:"route,omitempty"` - InhibitRules []config.InhibitRule `yaml:"inhibit_rules,omitempty" json:"inhibit_rules,omitempty"` - // MuteTimeIntervals is deprecated and will be removed before Alertmanager 1.0. - MuteTimeIntervals []config.MuteTimeInterval `yaml:"mute_time_intervals,omitempty" json:"mute_time_intervals,omitempty"` - TimeIntervals []config.TimeInterval `yaml:"time_intervals,omitempty" json:"time_intervals,omitempty"` - // Templates is unused by Grafana Managed AM but is passed-through for compatibility with some external AMs. - Templates []string `yaml:"templates" json:"templates"` -} - -// A Route is a node that contains definitions of how to handle alerts. This is modified -// from the upstream alertmanager in that it adds the ObjectMatchers property. -type Route struct { - Receiver string `yaml:"receiver,omitempty" json:"receiver,omitempty"` - - GroupByStr []string `yaml:"group_by,omitempty" json:"group_by,omitempty"` - GroupBy []model.LabelName `yaml:"-" json:"-"` - GroupByAll bool `yaml:"-" json:"-"` - // Deprecated. Remove before v1.0 release. - Match map[string]string `yaml:"match,omitempty" json:"match,omitempty"` - // Deprecated. Remove before v1.0 release. - MatchRE config.MatchRegexps `yaml:"match_re,omitempty" json:"match_re,omitempty"` - Matchers config.Matchers `yaml:"matchers,omitempty" json:"matchers,omitempty"` - ObjectMatchers ObjectMatchers `yaml:"object_matchers,omitempty" json:"object_matchers,omitempty"` - MuteTimeIntervals []string `yaml:"mute_time_intervals,omitempty" json:"mute_time_intervals,omitempty"` - Continue bool `yaml:"continue" json:"continue,omitempty"` - Routes []*Route `yaml:"routes,omitempty" json:"routes,omitempty"` - - GroupWait *model.Duration `yaml:"group_wait,omitempty" json:"group_wait,omitempty"` - GroupInterval *model.Duration `yaml:"group_interval,omitempty" json:"group_interval,omitempty"` - RepeatInterval *model.Duration `yaml:"repeat_interval,omitempty" json:"repeat_interval,omitempty"` - - Provenance Provenance `yaml:"provenance,omitempty" json:"provenance,omitempty"` -} - -// UnmarshalYAML implements the yaml.Unmarshaler interface for Route. This is a copy of alertmanager's upstream except it removes validation on the label key. -func (r *Route) UnmarshalYAML(unmarshal func(interface{}) error) error { - type plain Route - if err := unmarshal((*plain)(r)); err != nil { - return err - } - - return r.validateChild() -} - -// AsAMRoute returns an Alertmanager route from a Grafana route. The ObjectMatchers are converted to Matchers. -func (r *Route) AsAMRoute() *config.Route { - amRoute := &config.Route{ - Receiver: r.Receiver, - GroupByStr: r.GroupByStr, - GroupBy: r.GroupBy, - GroupByAll: r.GroupByAll, - Match: r.Match, - MatchRE: r.MatchRE, - Matchers: append(r.Matchers, r.ObjectMatchers...), - MuteTimeIntervals: r.MuteTimeIntervals, - Continue: r.Continue, - - GroupWait: r.GroupWait, - GroupInterval: r.GroupInterval, - RepeatInterval: r.RepeatInterval, - - Routes: make([]*config.Route, 0, len(r.Routes)), - } - for _, rt := range r.Routes { - amRoute.Routes = append(amRoute.Routes, rt.AsAMRoute()) - } - - return amRoute -} - -// AsGrafanaRoute returns a Grafana route from an Alertmanager route. The Matchers are converted to ObjectMatchers. -func AsGrafanaRoute(r *config.Route) *Route { - gRoute := &Route{ - Receiver: r.Receiver, - GroupByStr: r.GroupByStr, - GroupBy: r.GroupBy, - GroupByAll: r.GroupByAll, - Match: r.Match, - MatchRE: r.MatchRE, - ObjectMatchers: ObjectMatchers(r.Matchers), - MuteTimeIntervals: r.MuteTimeIntervals, - Continue: r.Continue, - - GroupWait: r.GroupWait, - GroupInterval: r.GroupInterval, - RepeatInterval: r.RepeatInterval, - - Routes: make([]*Route, 0, len(r.Routes)), - } - for _, rt := range r.Routes { - gRoute.Routes = append(gRoute.Routes, AsGrafanaRoute(rt)) - } - - return gRoute -} - -func (r *Route) ResourceType() string { - return "route" -} - -func (r *Route) ResourceID() string { - return "" -} - -// Config is the entrypoint for the embedded Alertmanager config with the exception of receivers. -// Prometheus historically uses yaml files as the method of configuration and thus some -// post-validation is included in the UnmarshalYAML method. Here we simply run this with -// a noop unmarshaling function in order to benefit from said validation. -func (c *Config) UnmarshalJSON(b []byte) error { - type plain Config - if err := json.Unmarshal(b, (*plain)(c)); err != nil { - return err - } - - noopUnmarshal := func(_ interface{}) error { return nil } - - if c.Global != nil { - if err := c.Global.UnmarshalYAML(noopUnmarshal); err != nil { - return err - } - } - - if c.Route == nil { - return fmt.Errorf("no routes provided") - } - - err := c.Route.Validate() - if err != nil { - return err - } - - for _, r := range c.InhibitRules { - if err := r.UnmarshalYAML(noopUnmarshal); err != nil { - return err - } - } - - tiNames := make(map[string]struct{}) - for _, mt := range c.MuteTimeIntervals { - if mt.Name == "" { - return fmt.Errorf("missing name in mute time interval") - } - if _, ok := tiNames[mt.Name]; ok { - return fmt.Errorf("mute time interval %q is not unique", mt.Name) - } - tiNames[mt.Name] = struct{}{} - } - for _, ti := range c.TimeIntervals { - if ti.Name == "" { - return fmt.Errorf("missing name in time interval") - } - if _, ok := tiNames[ti.Name]; ok { - return fmt.Errorf("time interval %q is not unique", ti.Name) - } - tiNames[ti.Name] = struct{}{} - } - return checkTimeInterval(c.Route, tiNames) -} - -func checkTimeInterval(r *Route, timeIntervals map[string]struct{}) error { - for _, sr := range r.Routes { - if err := checkTimeInterval(sr, timeIntervals); err != nil { - return err - } - } - if len(r.MuteTimeIntervals) == 0 { - return nil - } - for _, mt := range r.MuteTimeIntervals { - if _, ok := timeIntervals[mt]; !ok { - return fmt.Errorf("undefined time interval %q used in route", mt) - } - } - return nil -} - -type PostableApiAlertingConfig struct { - Config `yaml:",inline"` - - // Override with our superset receiver type - Receivers []*PostableApiReceiver `yaml:"receivers,omitempty" json:"receivers,omitempty"` -} - -func (c *PostableApiAlertingConfig) GetReceivers() []*PostableApiReceiver { - return c.Receivers -} - -func (c *PostableApiAlertingConfig) GetMuteTimeIntervals() []config.MuteTimeInterval { - return c.MuteTimeIntervals -} - -func (c *PostableApiAlertingConfig) GetTimeIntervals() []config.TimeInterval { return c.TimeIntervals } - -func (c *PostableApiAlertingConfig) GetRoute() *Route { - return c.Route -} - -func (c *PostableApiAlertingConfig) UnmarshalJSON(b []byte) error { - type plain PostableApiAlertingConfig - if err := json.Unmarshal(b, (*plain)(c)); err != nil { - return err - } - - // Since Config implements json.Unmarshaler, we must handle _all_ other fields independently. - // Otherwise, the json decoder will detect this and only use the embedded type. - // Additionally, we'll use pointers to slices in order to reference the intended target. - type overrides struct { - Receivers *[]*PostableApiReceiver `yaml:"receivers,omitempty" json:"receivers,omitempty"` - } - - if err := json.Unmarshal(b, &overrides{Receivers: &c.Receivers}); err != nil { - return err - } - - return c.validate() -} - -// validate ensures that the two routing trees use the correct receiver types. -func (c *PostableApiAlertingConfig) validate() error { - receivers := make(map[string]struct{}, len(c.Receivers)) - - var hasGrafReceivers, hasAMReceivers bool - for _, r := range c.Receivers { - receivers[r.Name] = struct{}{} - switch r.Type() { - case GrafanaReceiverType: - hasGrafReceivers = true - case AlertmanagerReceiverType: - hasAMReceivers = true - default: - continue - } - } - - if hasGrafReceivers && hasAMReceivers { - return fmt.Errorf("cannot mix Alertmanager & Grafana receiver types") - } - - if hasGrafReceivers { - // Taken from https://github.com/prometheus/alertmanager/blob/master/config/config.go#L170-L191 - // Check if we have a root route. We cannot check for it in the - // UnmarshalYAML method because it won't be called if the input is empty - // (e.g. the config file is empty or only contains whitespace). - if c.Route == nil { - return fmt.Errorf("no route provided in config") - } - - // Check if continue in root route. - if c.Route.Continue { - return fmt.Errorf("cannot have continue in root route") - } - } - - for _, receiver := range AllReceivers(c.Route.AsAMRoute()) { - _, ok := receivers[receiver] - if !ok { - return fmt.Errorf("unexpected receiver (%s) is undefined", receiver) - } - } - - return nil -} - -// Type requires validate has been called and just checks the first receiver type -func (c *PostableApiAlertingConfig) ReceiverType() ReceiverType { - for _, r := range c.Receivers { - switch r.Type() { - case GrafanaReceiverType: - return GrafanaReceiverType - case AlertmanagerReceiverType: - return AlertmanagerReceiverType - default: - continue - } - } - return EmptyReceiverType -} - -// AllReceivers will recursively walk a routing tree and return a list of all the -// referenced receiver names. -func AllReceivers(route *config.Route) (res []string) { - if route == nil { - return res - } - // TODO: Consider removing this check when new resource-specific AM APIs are implemented. - // Skip autogenerated routes. This helps cover the case where an admin POSTs the autogenerated route back to us. - // For example, when deleting a contact point that is unused but still referenced in the autogenerated route. - if isAutogeneratedRoot(route) { - return nil - } - - if route.Receiver != "" { - res = append(res, route.Receiver) - } - - for _, subRoute := range route.Routes { - res = append(res, AllReceivers(subRoute)...) - } - return res -} - -// autogeneratedRouteLabel a label name used to distinguish alerts that are supposed to be handled by the autogenerated policy. Only expected value is `true`. -const autogeneratedRouteLabel = "__grafana_autogenerated__" - -// isAutogeneratedRoot returns true if the route is the root of an autogenerated route. -func isAutogeneratedRoot(route *config.Route) bool { - return len(route.Matchers) == 1 && route.Matchers[0].Name == autogeneratedRouteLabel -} - -type RawMessage json.RawMessage // This type alias adds YAML marshaling to the json.RawMessage. - -// MarshalJSON returns m as the JSON encoding of m. -func (r RawMessage) MarshalJSON() ([]byte, error) { - return json.Marshal(json.RawMessage(r)) -} - -func (r *RawMessage) UnmarshalJSON(data []byte) error { - var raw json.RawMessage - err := json.Unmarshal(data, &raw) - if err != nil { - return err - } - *r = RawMessage(raw) - return nil -} - -func (r *RawMessage) UnmarshalYAML(unmarshal func(interface{}) error) error { - var data interface{} - if err := unmarshal(&data); err != nil { - return err - } - bytes, err := json.Marshal(data) - if err != nil { - return err - } - *r = bytes - return nil -} - -func (r RawMessage) MarshalYAML() (interface{}, error) { - if r == nil { - return nil, nil - } - var d interface{} - err := json.Unmarshal(r, &d) - if err != nil { - return nil, err - } - return d, nil -} - type GettableGrafanaReceiver struct { UID string `json:"uid"` Name string `json:"name"` @@ -1164,41 +830,6 @@ type GettableGrafanaReceiver struct { Provenance Provenance `json:"provenance,omitempty"` } -type PostableGrafanaReceiver struct { - UID string `json:"uid"` - Name string `json:"name"` - Type string `json:"type"` - DisableResolveMessage bool `json:"disableResolveMessage"` - Settings RawMessage `json:"settings,omitempty"` - SecureSettings map[string]string `json:"secureSettings"` -} - -type ReceiverType int - -const ( - GrafanaReceiverType ReceiverType = 1 << iota - AlertmanagerReceiverType - EmptyReceiverType = GrafanaReceiverType | AlertmanagerReceiverType -) - -func (r ReceiverType) String() string { - switch r { - case GrafanaReceiverType: - return "grafana" - case AlertmanagerReceiverType: - return "alertmanager" - case EmptyReceiverType: - return "empty" - default: - return "unknown" - } -} - -// Can determines whether a receiver type can implement another receiver type. -// This is useful as receivers with just names but no contact points -// are valid in all backends. -func (r ReceiverType) Can(other ReceiverType) bool { return r&other != 0 } - type GettableApiReceiver struct { config.Receiver `yaml:",inline"` GettableGrafanaReceivers `yaml:",inline"` @@ -1253,199 +884,8 @@ func (r *GettableApiReceiver) GetName() string { return r.Receiver.Name } -type PostableApiReceiver struct { - config.Receiver `yaml:",inline"` - PostableGrafanaReceivers `yaml:",inline"` -} - -func (r *PostableApiReceiver) UnmarshalYAML(unmarshal func(interface{}) error) error { - if err := unmarshal(&r.PostableGrafanaReceivers); err != nil { - return err - } - - if err := unmarshal(&r.Receiver); err != nil { - return err - } - - return nil -} - -func (r *PostableApiReceiver) UnmarshalJSON(b []byte) error { - type plain PostableApiReceiver - if err := json.Unmarshal(b, (*plain)(r)); err != nil { - return err - } - - hasGrafanaReceivers := len(r.PostableGrafanaReceivers.GrafanaManagedReceivers) > 0 - - if hasGrafanaReceivers { - if len(r.EmailConfigs) > 0 { - return fmt.Errorf("cannot have both Alertmanager EmailConfigs & Grafana receivers together") - } - if len(r.PagerdutyConfigs) > 0 { - return fmt.Errorf("cannot have both Alertmanager PagerdutyConfigs & Grafana receivers together") - } - if len(r.SlackConfigs) > 0 { - return fmt.Errorf("cannot have both Alertmanager SlackConfigs & Grafana receivers together") - } - if len(r.WebhookConfigs) > 0 { - return fmt.Errorf("cannot have both Alertmanager WebhookConfigs & Grafana receivers together") - } - if len(r.OpsGenieConfigs) > 0 { - return fmt.Errorf("cannot have both Alertmanager OpsGenieConfigs & Grafana receivers together") - } - if len(r.WechatConfigs) > 0 { - return fmt.Errorf("cannot have both Alertmanager WechatConfigs & Grafana receivers together") - } - if len(r.PushoverConfigs) > 0 { - return fmt.Errorf("cannot have both Alertmanager PushoverConfigs & Grafana receivers together") - } - if len(r.VictorOpsConfigs) > 0 { - return fmt.Errorf("cannot have both Alertmanager VictorOpsConfigs & Grafana receivers together") - } - } - return nil -} - -func (r *PostableApiReceiver) Type() ReceiverType { - if len(r.PostableGrafanaReceivers.GrafanaManagedReceivers) > 0 { - return GrafanaReceiverType - } - - cpy := r.Receiver - cpy.Name = "" - if reflect.ValueOf(cpy).IsZero() { - return EmptyReceiverType - } - - return AlertmanagerReceiverType -} - -func (r *PostableApiReceiver) GetName() string { - return r.Receiver.Name -} - type GettableGrafanaReceivers struct { GrafanaManagedReceivers []*GettableGrafanaReceiver `yaml:"grafana_managed_receiver_configs,omitempty" json:"grafana_managed_receiver_configs,omitempty"` } -type PostableGrafanaReceivers struct { - GrafanaManagedReceivers []*PostableGrafanaReceiver `yaml:"grafana_managed_receiver_configs,omitempty" json:"grafana_managed_receiver_configs,omitempty"` -} - type EncryptFn func(ctx context.Context, payload []byte) ([]byte, error) - -// ObjectMatcher is a matcher that can be used to filter alerts. -// swagger:model ObjectMatcher -type ObjectMatcherAPIModel [3]string - -// ObjectMatchers is a list of matchers that can be used to filter alerts. -// swagger:model ObjectMatchers -type ObjectMatchersAPIModel []ObjectMatcherAPIModel - -// swagger:ignore -// ObjectMatchers is Matchers with a different Unmarshal and Marshal methods that accept matchers as objects -// that have already been parsed. -type ObjectMatchers labels.Matchers - -// UnmarshalYAML implements the yaml.Unmarshaler interface for Matchers. -func (m *ObjectMatchers) UnmarshalYAML(unmarshal func(interface{}) error) error { - var rawMatchers ObjectMatchersAPIModel - if err := unmarshal(&rawMatchers); err != nil { - return err - } - for _, rawMatcher := range rawMatchers { - var matchType labels.MatchType - switch rawMatcher[1] { - case "=": - matchType = labels.MatchEqual - case "!=": - matchType = labels.MatchNotEqual - case "=~": - matchType = labels.MatchRegexp - case "!~": - matchType = labels.MatchNotRegexp - default: - return fmt.Errorf("unsupported match type %q in matcher", rawMatcher[1]) - } - - // When Prometheus serializes a matcher, the value gets wrapped in quotes: - // https://github.com/prometheus/alertmanager/blob/main/pkg/labels/matcher.go#L77 - // Remove these quotes so that we are matching against the right value. - // - // This is a stop-gap solution which will be superceded by https://github.com/grafana/grafana/issues/50040. - // - // The ngalert migration converts matchers into the Prom-style, quotes included. - // The UI then stores the quotes into ObjectMatchers without removing them. - // This approach allows these extra quotes to be stored in the database, and fixes them at read time. - // This works because the database stores matchers as JSON text. - // - // There is a subtle bug here, where users might intentionally add quotes to matchers. This method can remove such quotes. - // Since ObjectMatchers will be deprecated entirely, this bug will go away naturally with time. - rawMatcher[2] = strings.TrimPrefix(rawMatcher[2], "\"") - rawMatcher[2] = strings.TrimSuffix(rawMatcher[2], "\"") - - matcher, err := labels.NewMatcher(matchType, rawMatcher[0], rawMatcher[2]) - if err != nil { - return err - } - *m = append(*m, matcher) - } - sort.Sort(labels.Matchers(*m)) - return nil -} - -// UnmarshalJSON implements the json.Unmarshaler interface for Matchers. -func (m *ObjectMatchers) UnmarshalJSON(data []byte) error { - var rawMatchers ObjectMatchersAPIModel - if err := json.Unmarshal(data, &rawMatchers); err != nil { - return err - } - for _, rawMatcher := range rawMatchers { - var matchType labels.MatchType - switch rawMatcher[1] { - case "=": - matchType = labels.MatchEqual - case "!=": - matchType = labels.MatchNotEqual - case "=~": - matchType = labels.MatchRegexp - case "!~": - matchType = labels.MatchNotRegexp - default: - return fmt.Errorf("unsupported match type %q in matcher", rawMatcher[1]) - } - - rawMatcher[2] = strings.TrimPrefix(rawMatcher[2], "\"") - rawMatcher[2] = strings.TrimSuffix(rawMatcher[2], "\"") - - matcher, err := labels.NewMatcher(matchType, rawMatcher[0], rawMatcher[2]) - if err != nil { - return err - } - *m = append(*m, matcher) - } - sort.Sort(labels.Matchers(*m)) - return nil -} - -// MarshalYAML implements the yaml.Marshaler interface for Matchers. -func (m ObjectMatchers) MarshalYAML() (interface{}, error) { - result := make(ObjectMatchersAPIModel, len(m)) - for i, matcher := range m { - result[i] = ObjectMatcherAPIModel{matcher.Name, matcher.Type.String(), matcher.Value} - } - return result, nil -} - -// MarshalJSON implements the json.Marshaler interface for Matchers. -func (m ObjectMatchers) MarshalJSON() ([]byte, error) { - if len(m) == 0 { - return nil, nil - } - result := make(ObjectMatchersAPIModel, len(m)) - for i, matcher := range m { - result[i] = ObjectMatcherAPIModel{matcher.Name, matcher.Type.String(), matcher.Value} - } - return json.Marshal(result) -} diff --git a/pkg/services/ngalert/api/tooling/definitions/alertmanager_test.go b/pkg/services/ngalert/api/tooling/definitions/alertmanager_test.go index 4f0504e350..55821b9ba5 100644 --- a/pkg/services/ngalert/api/tooling/definitions/alertmanager_test.go +++ b/pkg/services/ngalert/api/tooling/definitions/alertmanager_test.go @@ -2,920 +2,17 @@ package definitions import ( "encoding/json" - "errors" "os" "strings" "testing" "github.com/prometheus/alertmanager/config" - "github.com/prometheus/alertmanager/pkg/labels" "github.com/prometheus/common/model" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gopkg.in/yaml.v3" ) -func Test_ApiReceiver_Marshaling(t *testing.T) { - for _, tc := range []struct { - desc string - input PostableApiReceiver - err bool - }{ - { - desc: "success AM", - input: PostableApiReceiver{ - Receiver: config.Receiver{ - Name: "foo", - EmailConfigs: []*config.EmailConfig{{}}, - }, - }, - }, - { - desc: "success GM", - input: PostableApiReceiver{ - Receiver: config.Receiver{ - Name: "foo", - }, - PostableGrafanaReceivers: PostableGrafanaReceivers{ - GrafanaManagedReceivers: []*PostableGrafanaReceiver{{}}, - }, - }, - }, - { - desc: "failure mixed", - input: PostableApiReceiver{ - Receiver: config.Receiver{ - Name: "foo", - EmailConfigs: []*config.EmailConfig{{}}, - }, - PostableGrafanaReceivers: PostableGrafanaReceivers{ - GrafanaManagedReceivers: []*PostableGrafanaReceiver{{}}, - }, - }, - err: true, - }, - } { - t.Run(tc.desc, func(t *testing.T) { - encoded, err := json.Marshal(tc.input) - require.Nil(t, err) - - var out PostableApiReceiver - err = json.Unmarshal(encoded, &out) - - if tc.err { - require.Error(t, err) - } else { - require.Nil(t, err) - require.Equal(t, tc.input, out) - } - }) - } -} - -func Test_APIReceiverType(t *testing.T) { - for _, tc := range []struct { - desc string - input PostableApiReceiver - expected ReceiverType - }{ - { - desc: "empty", - input: PostableApiReceiver{ - Receiver: config.Receiver{ - Name: "foo", - }, - }, - expected: EmptyReceiverType, - }, - { - desc: "am", - input: PostableApiReceiver{ - Receiver: config.Receiver{ - Name: "foo", - EmailConfigs: []*config.EmailConfig{{}}, - }, - }, - expected: AlertmanagerReceiverType, - }, - { - desc: "graf", - input: PostableApiReceiver{ - Receiver: config.Receiver{ - Name: "foo", - }, - PostableGrafanaReceivers: PostableGrafanaReceivers{ - GrafanaManagedReceivers: []*PostableGrafanaReceiver{{}}, - }, - }, - expected: GrafanaReceiverType, - }, - } { - t.Run(tc.desc, func(t *testing.T) { - require.Equal(t, tc.expected, tc.input.Type()) - }) - } -} - -func Test_AllReceivers(t *testing.T) { - input := &Route{ - Receiver: "foo", - Routes: []*Route{ - { - Receiver: "bar", - Routes: []*Route{ - { - Receiver: "bazz", - }, - }, - }, - { - Receiver: "buzz", - }, - }, - } - - require.Equal(t, []string{"foo", "bar", "bazz", "buzz"}, AllReceivers(input.AsAMRoute())) - - // test empty - var empty []string - emptyRoute := &Route{} - require.Equal(t, empty, AllReceivers(emptyRoute.AsAMRoute())) -} - -func Test_ApiAlertingConfig_Marshaling(t *testing.T) { - for _, tc := range []struct { - desc string - input PostableApiAlertingConfig - err bool - }{ - { - desc: "success am", - input: PostableApiAlertingConfig{ - Config: Config{ - Route: &Route{ - Receiver: "am", - Routes: []*Route{ - { - Receiver: "am", - }, - }, - }, - }, - Receivers: []*PostableApiReceiver{ - { - Receiver: config.Receiver{ - Name: "am", - EmailConfigs: []*config.EmailConfig{{}}, - }, - }, - }, - }, - }, - { - desc: "success graf", - input: PostableApiAlertingConfig{ - Config: Config{ - Route: &Route{ - Receiver: "graf", - Routes: []*Route{ - { - Receiver: "graf", - }, - }, - }, - }, - Receivers: []*PostableApiReceiver{ - { - Receiver: config.Receiver{ - Name: "graf", - }, - PostableGrafanaReceivers: PostableGrafanaReceivers{ - GrafanaManagedReceivers: []*PostableGrafanaReceiver{{}}, - }, - }, - }, - }, - }, - { - desc: "failure undefined am receiver", - input: PostableApiAlertingConfig{ - Config: Config{ - Route: &Route{ - Receiver: "am", - Routes: []*Route{ - { - Receiver: "unmentioned", - }, - }, - }, - }, - Receivers: []*PostableApiReceiver{ - { - Receiver: config.Receiver{ - Name: "am", - EmailConfigs: []*config.EmailConfig{{}}, - }, - }, - }, - }, - err: true, - }, - { - desc: "failure undefined graf receiver", - input: PostableApiAlertingConfig{ - Config: Config{ - Route: &Route{ - Receiver: "graf", - Routes: []*Route{ - { - Receiver: "unmentioned", - }, - }, - }, - }, - Receivers: []*PostableApiReceiver{ - { - Receiver: config.Receiver{ - Name: "graf", - }, - PostableGrafanaReceivers: PostableGrafanaReceivers{ - GrafanaManagedReceivers: []*PostableGrafanaReceiver{{}}, - }, - }, - }, - }, - err: true, - }, - { - desc: "failure graf no route", - input: PostableApiAlertingConfig{ - Receivers: []*PostableApiReceiver{ - { - Receiver: config.Receiver{ - Name: "graf", - }, - PostableGrafanaReceivers: PostableGrafanaReceivers{ - GrafanaManagedReceivers: []*PostableGrafanaReceiver{{}}, - }, - }, - }, - }, - err: true, - }, - { - desc: "failure graf no default receiver", - input: PostableApiAlertingConfig{ - Config: Config{ - Route: &Route{ - Routes: []*Route{ - { - Receiver: "graf", - }, - }, - }, - }, - Receivers: []*PostableApiReceiver{ - { - Receiver: config.Receiver{ - Name: "graf", - }, - PostableGrafanaReceivers: PostableGrafanaReceivers{ - GrafanaManagedReceivers: []*PostableGrafanaReceiver{{}}, - }, - }, - }, - }, - err: true, - }, - { - desc: "failure graf root route with matchers", - input: PostableApiAlertingConfig{ - Config: Config{ - Route: &Route{ - Receiver: "graf", - Routes: []*Route{ - { - Receiver: "graf", - }, - }, - Match: map[string]string{"foo": "bar"}, - }, - }, - Receivers: []*PostableApiReceiver{ - { - Receiver: config.Receiver{ - Name: "graf", - }, - PostableGrafanaReceivers: PostableGrafanaReceivers{ - GrafanaManagedReceivers: []*PostableGrafanaReceiver{{}}, - }, - }, - }, - }, - err: true, - }, - { - desc: "failure graf nested route duplicate group by labels", - input: PostableApiAlertingConfig{ - Config: Config{ - Route: &Route{ - Receiver: "graf", - Routes: []*Route{ - { - Receiver: "graf", - GroupByStr: []string{"foo", "bar", "foo"}, - }, - }, - }, - }, - Receivers: []*PostableApiReceiver{ - { - Receiver: config.Receiver{ - Name: "graf", - }, - PostableGrafanaReceivers: PostableGrafanaReceivers{ - GrafanaManagedReceivers: []*PostableGrafanaReceiver{{}}, - }, - }, - }, - }, - err: true, - }, - { - desc: "success undefined am receiver in autogenerated route is ignored", - input: PostableApiAlertingConfig{ - Config: Config{ - Route: &Route{ - Receiver: "am", - Routes: []*Route{ - { - Matchers: config.Matchers{ - { - Name: autogeneratedRouteLabel, - Type: labels.MatchEqual, - Value: "true", - }, - }, - Routes: []*Route{ - { - Receiver: "unmentioned", - }, - }, - }, - }, - }, - }, - Receivers: []*PostableApiReceiver{ - { - Receiver: config.Receiver{ - Name: "am", - EmailConfigs: []*config.EmailConfig{{}}, - }, - }, - }, - }, - err: false, - }, - { - desc: "success undefined graf receiver in autogenerated route is ignored", - input: PostableApiAlertingConfig{ - Config: Config{ - Route: &Route{ - Receiver: "graf", - Routes: []*Route{ - { - Matchers: config.Matchers{ - { - Name: autogeneratedRouteLabel, - Type: labels.MatchEqual, - Value: "true", - }, - }, - Routes: []*Route{ - { - Receiver: "unmentioned", - }, - }, - }, - }, - }, - }, - Receivers: []*PostableApiReceiver{ - { - Receiver: config.Receiver{ - Name: "graf", - }, - PostableGrafanaReceivers: PostableGrafanaReceivers{ - GrafanaManagedReceivers: []*PostableGrafanaReceiver{{}}, - }, - }, - }, - }, - err: false, - }, - } { - t.Run(tc.desc, func(t *testing.T) { - encoded, err := json.Marshal(tc.input) - require.Nil(t, err) - - var out PostableApiAlertingConfig - err = json.Unmarshal(encoded, &out) - - if tc.err { - require.Error(t, err) - } else { - require.Nil(t, err) - require.Equal(t, tc.input, out) - } - }) - } -} - -func Test_PostableApiReceiver_Unmarshaling_YAML(t *testing.T) { - for _, tc := range []struct { - desc string - input string - rtype ReceiverType - }{ - { - desc: "grafana receivers", - input: ` -name: grafana_managed -grafana_managed_receiver_configs: - - uid: alertmanager UID - name: an alert manager receiver - type: prometheus-alertmanager - sendreminder: false - disableresolvemessage: false - frequency: 5m - isdefault: false - settings: {} - securesettings: - basicAuthPassword: - - uid: dingding UID - name: a dingding receiver - type: dingding - sendreminder: false - disableresolvemessage: false - frequency: 5m - isdefault: false`, - rtype: GrafanaReceiverType, - }, - { - desc: "receiver", - input: ` -name: example-email -email_configs: - - to: 'youraddress@example.org'`, - rtype: AlertmanagerReceiverType, - }, - } { - t.Run(tc.desc, func(t *testing.T) { - var r PostableApiReceiver - err := yaml.Unmarshal([]byte(tc.input), &r) - require.Nil(t, err) - assert.Equal(t, tc.rtype, r.Type()) - }) - } -} - -func Test_ConfigUnmashaling(t *testing.T) { - for _, tc := range []struct { - desc, input string - err error - }{ - { - desc: "missing mute time interval name should error", - err: errors.New("missing name in mute time interval"), - input: ` - { - "route": { - "receiver": "grafana-default-email" - }, - "mute_time_intervals": [ - { - "name": "", - "time_intervals": [ - { - "times": [ - { - "start_time": "00:00", - "end_time": "12:00" - } - ] - } - ] - } - ], - "templates": null, - "receivers": [ - { - "name": "grafana-default-email", - "grafana_managed_receiver_configs": [ - { - "uid": "uxwfZvtnz", - "name": "email receiver", - "type": "email", - "disableResolveMessage": false, - "settings": { - "addresses": "" - }, - "secureFields": {} - } - ] - } - ] - } - `, - }, - { - desc: "missing time interval name should error", - err: errors.New("missing name in time interval"), - input: ` - { - "route": { - "receiver": "grafana-default-email" - }, - "time_intervals": [ - { - "name": "", - "time_intervals": [ - { - "times": [ - { - "start_time": "00:00", - "end_time": "12:00" - } - ] - } - ] - } - ], - "templates": null, - "receivers": [ - { - "name": "grafana-default-email", - "grafana_managed_receiver_configs": [ - { - "uid": "uxwfZvtnz", - "name": "email receiver", - "type": "email", - "disableResolveMessage": false, - "settings": { - "addresses": "" - }, - "secureFields": {} - } - ] - } - ] - } - `, - }, - { - desc: "duplicate mute time interval names should error", - err: errors.New("mute time interval \"test1\" is not unique"), - input: ` - { - "route": { - "receiver": "grafana-default-email" - }, - "mute_time_intervals": [ - { - "name": "test1", - "time_intervals": [ - { - "times": [ - { - "start_time": "00:00", - "end_time": "12:00" - } - ] - } - ] - }, - { - "name": "test1", - "time_intervals": [ - { - "times": [ - { - "start_time": "00:00", - "end_time": "12:00" - } - ] - } - ] - } - ], - "templates": null, - "receivers": [ - { - "name": "grafana-default-email", - "grafana_managed_receiver_configs": [ - { - "uid": "uxwfZvtnz", - "name": "email receiver", - "type": "email", - "disableResolveMessage": false, - "settings": { - "addresses": "" - }, - "secureFields": {} - } - ] - } - ] - } - `, - }, - { - desc: "duplicate time interval names should error", - err: errors.New("time interval \"test1\" is not unique"), - input: ` - { - "route": { - "receiver": "grafana-default-email" - }, - "time_intervals": [ - { - "name": "test1", - "time_intervals": [ - { - "times": [ - { - "start_time": "00:00", - "end_time": "12:00" - } - ] - } - ] - }, - { - "name": "test1", - "time_intervals": [ - { - "times": [ - { - "start_time": "00:00", - "end_time": "12:00" - } - ] - } - ] - } - ], - "templates": null, - "receivers": [ - { - "name": "grafana-default-email", - "grafana_managed_receiver_configs": [ - { - "uid": "uxwfZvtnz", - "name": "email receiver", - "type": "email", - "disableResolveMessage": false, - "settings": { - "addresses": "" - }, - "secureFields": {} - } - ] - } - ] - } - `, - }, - { - desc: "duplicate time and mute time interval names should error", - err: errors.New("time interval \"test1\" is not unique"), - input: ` - { - "route": { - "receiver": "grafana-default-email" - }, - "mute_time_intervals": [ - { - "name": "test1", - "time_intervals": [ - { - "times": [ - { - "start_time": "00:00", - "end_time": "12:00" - } - ] - } - ] - } - ], - "time_intervals": [ - { - "name": "test1", - "time_intervals": [ - { - "times": [ - { - "start_time": "00:00", - "end_time": "12:00" - } - ] - } - ] - } - ], - "templates": null, - "receivers": [ - { - "name": "grafana-default-email", - "grafana_managed_receiver_configs": [ - { - "uid": "uxwfZvtnz", - "name": "email receiver", - "type": "email", - "disableResolveMessage": false, - "settings": { - "addresses": "" - }, - "secureFields": {} - } - ] - } - ] - } - `, - }, - { - desc: "mute time intervals on root route should error", - err: errors.New("root route must not have any mute time intervals"), - input: ` - { - "route": { - "receiver": "grafana-default-email", - "mute_time_intervals": ["test1"] - }, - "mute_time_intervals": [ - { - "name": "test1", - "time_intervals": [ - { - "times": [ - { - "start_time": "00:00", - "end_time": "12:00" - } - ] - } - ] - } - ], - "templates": null, - "receivers": [ - { - "name": "grafana-default-email", - "grafana_managed_receiver_configs": [ - { - "uid": "uxwfZvtnz", - "name": "email receiver", - "type": "email", - "disableResolveMessage": false, - "settings": { - "addresses": "" - }, - "secureFields": {} - } - ] - } - ] - } - `, - }, - { - desc: "undefined mute time names in routes should error", - err: errors.New("undefined time interval \"test2\" used in route"), - input: ` - { - "route": { - "receiver": "grafana-default-email", - "routes": [ - { - "receiver": "grafana-default-email", - "object_matchers": [ - [ - "a", - "=", - "b" - ] - ], - "mute_time_intervals": [ - "test2" - ] - } - ] - }, - "mute_time_intervals": [ - { - "name": "test1", - "time_intervals": [ - { - "times": [ - { - "start_time": "00:00", - "end_time": "12:00" - } - ] - } - ] - } - ], - "templates": null, - "receivers": [ - { - "name": "grafana-default-email", - "grafana_managed_receiver_configs": [ - { - "uid": "uxwfZvtnz", - "name": "email receiver", - "type": "email", - "disableResolveMessage": false, - "settings": { - "addresses": "" - }, - "secureFields": {} - } - ] - } - ] - } - `, - }, - { - desc: "valid config should not error", - input: ` - { - "route": { - "receiver": "grafana-default-email", - "routes": [ - { - "receiver": "grafana-default-email", - "object_matchers": [ - [ - "a", - "=", - "b" - ] - ], - "mute_time_intervals": [ - "test1" - ] - } - ] - }, - "mute_time_intervals": [ - { - "name": "test1", - "time_intervals": [ - { - "times": [ - { - "start_time": "00:00", - "end_time": "12:00" - } - ] - } - ] - } - ], - "templates": null, - "receivers": [ - { - "name": "grafana-default-email", - "grafana_managed_receiver_configs": [ - { - "uid": "uxwfZvtnz", - "name": "email receiver", - "type": "email", - "disableResolveMessage": false, - "settings": { - "addresses": "" - }, - "secureFields": {} - } - ] - } - ] - } - `, - }, - } { - t.Run(tc.desc, func(t *testing.T) { - var out Config - err := json.Unmarshal([]byte(tc.input), &out) - require.Equal(t, tc.err, err) - }) - } -} - func Test_GettableUserConfigUnmarshaling(t *testing.T) { for _, tc := range []struct { desc, input string @@ -1062,207 +159,6 @@ func Test_GettableUserConfigRoundtrip(t *testing.T) { require.Equal(t, string(yamlEncoded), string(out)) } -func Test_ReceiverCompatibility(t *testing.T) { - for _, tc := range []struct { - desc string - a, b ReceiverType - expected bool - }{ - { - desc: "grafana=grafana", - a: GrafanaReceiverType, - b: GrafanaReceiverType, - expected: true, - }, - { - desc: "am=am", - a: AlertmanagerReceiverType, - b: AlertmanagerReceiverType, - expected: true, - }, - { - desc: "empty=grafana", - a: EmptyReceiverType, - b: AlertmanagerReceiverType, - expected: true, - }, - { - desc: "empty=am", - a: EmptyReceiverType, - b: AlertmanagerReceiverType, - expected: true, - }, - { - desc: "empty=empty", - a: EmptyReceiverType, - b: EmptyReceiverType, - expected: true, - }, - { - desc: "graf!=am", - a: GrafanaReceiverType, - b: AlertmanagerReceiverType, - expected: false, - }, - { - desc: "am!=graf", - a: AlertmanagerReceiverType, - b: GrafanaReceiverType, - expected: false, - }, - } { - t.Run(tc.desc, func(t *testing.T) { - require.Equal(t, tc.expected, tc.a.Can(tc.b)) - }) - } -} - -func Test_ReceiverMatchesBackend(t *testing.T) { - for _, tc := range []struct { - desc string - rec ReceiverType - b ReceiverType - ok bool - }{ - { - desc: "graf=graf", - rec: GrafanaReceiverType, - b: GrafanaReceiverType, - ok: true, - }, - { - desc: "empty=graf", - rec: EmptyReceiverType, - b: GrafanaReceiverType, - ok: true, - }, - { - desc: "am=am", - rec: AlertmanagerReceiverType, - b: AlertmanagerReceiverType, - ok: true, - }, - { - desc: "empty=am", - rec: EmptyReceiverType, - b: AlertmanagerReceiverType, - ok: true, - }, - { - desc: "graf!=am", - rec: GrafanaReceiverType, - b: AlertmanagerReceiverType, - ok: false, - }, - } { - t.Run(tc.desc, func(t *testing.T) { - ok := tc.rec.Can(tc.b) - require.Equal(t, tc.ok, ok) - }) - } -} - -func TestObjectMatchers_UnmarshalJSON(t *testing.T) { - j := `{ - "receiver": "autogen-contact-point-default", - "routes": [{ - "receiver": "autogen-contact-point-1", - "object_matchers": [ - [ - "a", - "=", - "MFR3Gxrnk" - ], - [ - "b", - "=", - "\"MFR3Gxrnk\"" - ], - [ - "c", - "=~", - "^[a-z0-9-]{1}[a-z0-9-]{0,30}$" - ], - [ - "d", - "=~", - "\"^[a-z0-9-]{1}[a-z0-9-]{0,30}$\"" - ] - ], - "group_interval": "3s", - "repeat_interval": "10s" - }] -}` - var r Route - if err := json.Unmarshal([]byte(j), &r); err != nil { - require.NoError(t, err) - } - - matchers := r.Routes[0].ObjectMatchers - - // Without quotes. - require.Equal(t, matchers[0].Name, "a") - require.Equal(t, matchers[0].Value, "MFR3Gxrnk") - - // With double quotes. - require.Equal(t, matchers[1].Name, "b") - require.Equal(t, matchers[1].Value, "MFR3Gxrnk") - - // Regexp without quotes. - require.Equal(t, matchers[2].Name, "c") - require.Equal(t, matchers[2].Value, "^[a-z0-9-]{1}[a-z0-9-]{0,30}$") - - // Regexp with quotes. - require.Equal(t, matchers[3].Name, "d") - require.Equal(t, matchers[3].Value, "^[a-z0-9-]{1}[a-z0-9-]{0,30}$") -} - -func TestObjectMatchers_UnmarshalYAML(t *testing.T) { - y := `--- -receiver: autogen-contact-point-default -routes: -- receiver: autogen-contact-point-1 - object_matchers: - - - a - - "=" - - MFR3Gxrnk - - - b - - "=" - - '"MFR3Gxrnk"' - - - c - - "=~" - - "^[a-z0-9-]{1}[a-z0-9-]{0,30}$" - - - d - - "=~" - - '"^[a-z0-9-]{1}[a-z0-9-]{0,30}$"' - group_interval: 3s - repeat_interval: 10s -` - - var r Route - if err := yaml.Unmarshal([]byte(y), &r); err != nil { - require.NoError(t, err) - } - - matchers := r.Routes[0].ObjectMatchers - - // Without quotes. - require.Equal(t, matchers[0].Name, "a") - require.Equal(t, matchers[0].Value, "MFR3Gxrnk") - - // With double quotes. - require.Equal(t, matchers[1].Name, "b") - require.Equal(t, matchers[1].Value, "MFR3Gxrnk") - - // Regexp without quotes. - require.Equal(t, matchers[2].Name, "c") - require.Equal(t, matchers[2].Value, "^[a-z0-9-]{1}[a-z0-9-]{0,30}$") - - // Regexp with quotes. - require.Equal(t, matchers[3].Name, "d") - require.Equal(t, matchers[3].Value, "^[a-z0-9-]{1}[a-z0-9-]{0,30}$") -} - func Test_Marshaling_Validation(t *testing.T) { jsonEncoded, err := os.ReadFile("alertmanager_test_artifact.json") require.Nil(t, err) diff --git a/pkg/services/ngalert/api/tooling/definitions/alertmanager_validation.go b/pkg/services/ngalert/api/tooling/definitions/alertmanager_validation.go index 9bec82b6d7..72ca0ebb73 100644 --- a/pkg/services/ngalert/api/tooling/definitions/alertmanager_validation.go +++ b/pkg/services/ngalert/api/tooling/definitions/alertmanager_validation.go @@ -6,61 +6,11 @@ import ( "regexp" "strings" tmpltext "text/template" - "time" "github.com/prometheus/alertmanager/template" - "github.com/prometheus/common/model" "gopkg.in/yaml.v3" ) -// groupByAll is a special value defined by alertmanager that can be used in a Route's GroupBy field to aggregate by all possible labels. -const groupByAll = "..." - -// Validate normalizes a possibly nested Route r, and returns errors if r is invalid. -func (r *Route) validateChild() error { - r.GroupBy = nil - r.GroupByAll = false - for _, l := range r.GroupByStr { - if l == groupByAll { - r.GroupByAll = true - } else { - r.GroupBy = append(r.GroupBy, model.LabelName(l)) - } - } - - if len(r.GroupBy) > 0 && r.GroupByAll { - return fmt.Errorf("cannot have wildcard group_by (`...`) and other other labels at the same time") - } - - groupBy := map[model.LabelName]struct{}{} - - for _, ln := range r.GroupBy { - if _, ok := groupBy[ln]; ok { - return fmt.Errorf("duplicated label %q in group_by, %s %s", ln, r.Receiver, r.GroupBy) - } - groupBy[ln] = struct{}{} - } - - if r.GroupInterval != nil && time.Duration(*r.GroupInterval) == time.Duration(0) { - return fmt.Errorf("group_interval cannot be zero") - } - if r.RepeatInterval != nil && time.Duration(*r.RepeatInterval) == time.Duration(0) { - return fmt.Errorf("repeat_interval cannot be zero") - } - - // Routes are a self-referential structure. - if r.Routes != nil { - for _, child := range r.Routes { - err := child.validateChild() - if err != nil { - return err - } - } - } - - return nil -} - func (t *NotificationTemplate) Validate() error { if t.Name == "" { return fmt.Errorf("template must have a name") @@ -102,48 +52,6 @@ func (t *NotificationTemplate) Validate() error { return nil } -// Validate normalizes a Route r, and returns errors if r is an invalid root route. Root routes must satisfy a few additional conditions. -func (r *Route) Validate() error { - if len(r.Receiver) == 0 { - return fmt.Errorf("root route must specify a default receiver") - } - if len(r.Match) > 0 || len(r.MatchRE) > 0 { - return fmt.Errorf("root route must not have any matchers") - } - if len(r.MuteTimeIntervals) > 0 { - return fmt.Errorf("root route must not have any mute time intervals") - } - return r.validateChild() -} - -func (r *Route) ValidateReceivers(receivers map[string]struct{}) error { - if _, exists := receivers[r.Receiver]; !exists { - return fmt.Errorf("receiver '%s' does not exist", r.Receiver) - } - for _, children := range r.Routes { - err := children.ValidateReceivers(receivers) - if err != nil { - return err - } - } - return nil -} - -func (r *Route) ValidateMuteTimes(muteTimes map[string]struct{}) error { - for _, name := range r.MuteTimeIntervals { - if _, exists := muteTimes[name]; !exists { - return fmt.Errorf("mute time interval '%s' does not exist", name) - } - } - for _, child := range r.Routes { - err := child.ValidateMuteTimes(muteTimes) - if err != nil { - return err - } - } - return nil -} - func (mt *MuteTimeInterval) Validate() error { s, err := yaml.Marshal(mt.MuteTimeInterval) if err != nil { diff --git a/pkg/services/ngalert/api/tooling/definitions/alertmanager_validation_test.go b/pkg/services/ngalert/api/tooling/definitions/alertmanager_validation_test.go index cfb504b729..4fbbd983b1 100644 --- a/pkg/services/ngalert/api/tooling/definitions/alertmanager_validation_test.go +++ b/pkg/services/ngalert/api/tooling/definitions/alertmanager_validation_test.go @@ -48,7 +48,7 @@ func TestValidateRoutes(t *testing.T) { for _, c := range cases { t.Run(c.desc, func(t *testing.T) { - err := c.route.validateChild() + err := c.route.ValidateChild() require.NoError(t, err) }) @@ -117,7 +117,7 @@ func TestValidateRoutes(t *testing.T) { for _, c := range cases { t.Run(c.desc, func(t *testing.T) { - err := c.route.validateChild() + err := c.route.ValidateChild() require.Error(t, err) require.Contains(t, err.Error(), c.expMsg) @@ -132,7 +132,7 @@ func TestValidateRoutes(t *testing.T) { GroupByStr: []string{"abc", "def"}, } - _ = route.validateChild() + _ = route.ValidateChild() require.False(t, route.GroupByAll) require.Equal(t, []model.LabelName{"abc", "def"}, route.GroupBy) @@ -144,7 +144,7 @@ func TestValidateRoutes(t *testing.T) { GroupByStr: []string{"..."}, } - _ = route.validateChild() + _ = route.ValidateChild() require.True(t, route.GroupByAll) require.Nil(t, route.GroupBy) @@ -156,9 +156,9 @@ func TestValidateRoutes(t *testing.T) { GroupByStr: []string{"abc", "def"}, } - err := route.validateChild() + err := route.ValidateChild() require.NoError(t, err) - err = route.validateChild() + err = route.ValidateChild() require.NoError(t, err) require.False(t, route.GroupByAll) diff --git a/pkg/services/ngalert/api/tooling/post.json b/pkg/services/ngalert/api/tooling/post.json index 44b31d7c65..8c9e8a312f 100644 --- a/pkg/services/ngalert/api/tooling/post.json +++ b/pkg/services/ngalert/api/tooling/post.json @@ -4354,6 +4354,7 @@ "type": "object" }, "URL": { + "description": "The general form represented is:\n\n[scheme:][//[userinfo@]host][/]path[?query][#fragment]\n\nURLs that do not start with a slash after the scheme are interpreted as:\n\nscheme:opaque[?query][#fragment]\n\nNote that the Path field is stored in decoded form: /%47%6f%2f becomes /Go/.\nA consequence is that it is impossible to tell which slashes in the Path were\nslashes in the raw URL and which were %2f. This distinction is rarely important,\nbut when it is, the code should use the EscapedPath method, which preserves\nthe original encoding of Path.\n\nThe RawPath field is an optional field which is only set when the default\nencoding of Path is different from the escaped path. See the EscapedPath method\nfor more details.\n\nURL's String method uses the EscapedPath method to obtain the path.", "properties": { "ForceQuery": { "type": "boolean" @@ -4389,7 +4390,7 @@ "$ref": "#/definitions/Userinfo" } }, - "title": "URL is a custom URL type that allows validation at configuration load time.", + "title": "A URL represents a parsed URL (technically, a URI reference).", "type": "object" }, "UpdateRuleGroupResponse": { @@ -4619,7 +4620,6 @@ "type": "object" }, "alertGroups": { - "description": "AlertGroups alert groups", "items": { "$ref": "#/definitions/alertGroup" }, @@ -4724,7 +4724,6 @@ "type": "object" }, "gettableAlert": { - "description": "GettableAlert gettable alert", "properties": { "annotations": { "$ref": "#/definitions/labelSet" @@ -4787,6 +4786,7 @@ "type": "array" }, "gettableSilence": { + "description": "GettableSilence gettable silence", "properties": { "comment": { "description": "comment", @@ -4986,7 +4986,6 @@ "type": "array" }, "postableSilence": { - "description": "PostableSilence postable silence", "properties": { "comment": { "description": "comment", diff --git a/pkg/services/ngalert/api/tooling/spec.json b/pkg/services/ngalert/api/tooling/spec.json index 90aae29c3e..191253a57b 100644 --- a/pkg/services/ngalert/api/tooling/spec.json +++ b/pkg/services/ngalert/api/tooling/spec.json @@ -7959,8 +7959,9 @@ } }, "URL": { + "description": "The general form represented is:\n\n[scheme:][//[userinfo@]host][/]path[?query][#fragment]\n\nURLs that do not start with a slash after the scheme are interpreted as:\n\nscheme:opaque[?query][#fragment]\n\nNote that the Path field is stored in decoded form: /%47%6f%2f becomes /Go/.\nA consequence is that it is impossible to tell which slashes in the Path were\nslashes in the raw URL and which were %2f. This distinction is rarely important,\nbut when it is, the code should use the EscapedPath method, which preserves\nthe original encoding of Path.\n\nThe RawPath field is an optional field which is only set when the default\nencoding of Path is different from the escaped path. See the EscapedPath method\nfor more details.\n\nURL's String method uses the EscapedPath method to obtain the path.", "type": "object", - "title": "URL is a custom URL type that allows validation at configuration load time.", + "title": "A URL represents a parsed URL (technically, a URI reference).", "properties": { "ForceQuery": { "type": "boolean" @@ -8225,7 +8226,6 @@ "$ref": "#/definitions/alertGroup" }, "alertGroups": { - "description": "AlertGroups alert groups", "type": "array", "items": { "$ref": "#/definitions/alertGroup" @@ -8331,7 +8331,6 @@ } }, "gettableAlert": { - "description": "GettableAlert gettable alert", "type": "object", "required": [ "labels", @@ -8396,6 +8395,7 @@ "$ref": "#/definitions/gettableAlerts" }, "gettableSilence": { + "description": "GettableSilence gettable silence", "type": "object", "required": [ "comment", @@ -8598,7 +8598,6 @@ } }, "postableSilence": { - "description": "PostableSilence postable silence", "type": "object", "required": [ "comment", diff --git a/public/api-merged.json b/public/api-merged.json index 40061884c2..dd34a34252 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -17045,6 +17045,7 @@ }, "ObjectMatchers": { "type": "array", + "title": "ObjectMatchers is a list of matchers that can be used to filter alerts.", "items": { "$ref": "#/definitions/ObjectMatcher" } @@ -21795,6 +21796,7 @@ } }, "alertGroup": { + "description": "AlertGroup alert group", "type": "object", "required": [ "alerts", @@ -21818,6 +21820,7 @@ } }, "alertGroups": { + "description": "AlertGroups alert groups", "type": "array", "items": { "$ref": "#/definitions/alertGroup" @@ -21950,6 +21953,7 @@ } }, "gettableAlert": { + "description": "GettableAlert gettable alert", "type": "object", "required": [ "labels", @@ -22012,6 +22016,7 @@ } }, "gettableSilence": { + "description": "GettableSilence gettable silence", "type": "object", "required": [ "comment", @@ -22066,6 +22071,7 @@ } }, "integration": { + "description": "Integration integration", "type": "object", "required": [ "name", diff --git a/public/openapi3.json b/public/openapi3.json index 5291c6fe76..d6f99d851f 100644 --- a/public/openapi3.json +++ b/public/openapi3.json @@ -7557,6 +7557,7 @@ "items": { "$ref": "#/components/schemas/ObjectMatcher" }, + "title": "ObjectMatchers is a list of matchers that can be used to filter alerts.", "type": "array" }, "OpsGenieConfig": { @@ -12304,6 +12305,7 @@ "type": "object" }, "alertGroup": { + "description": "AlertGroup alert group", "properties": { "alerts": { "description": "alerts", @@ -12327,6 +12329,7 @@ "type": "object" }, "alertGroups": { + "description": "AlertGroups alert groups", "items": { "$ref": "#/components/schemas/alertGroup" }, @@ -12459,6 +12462,7 @@ "type": "object" }, "gettableAlert": { + "description": "GettableAlert gettable alert", "properties": { "annotations": { "$ref": "#/components/schemas/labelSet" @@ -12521,6 +12525,7 @@ "type": "array" }, "gettableSilence": { + "description": "GettableSilence gettable silence", "properties": { "comment": { "description": "comment", @@ -12575,6 +12580,7 @@ "type": "array" }, "integration": { + "description": "Integration integration", "properties": { "lastNotifyAttempt": { "description": "A timestamp indicating the last attempt to deliver a notification regardless of the outcome.\nFormat: date-time", From 7a171fd14a77f050c2f149ac06a30a5551613e58 Mon Sep 17 00:00:00 2001 From: Alexander Weaver Date: Wed, 6 Mar 2024 16:08:45 -0600 Subject: [PATCH 0447/1406] Regenerate openapidocs at 1.21.8 to match ci (#84037) * Regenerate openapidocs at 1.21.8 to match ci * Adjust trigger to work on the actual outputted files * Also put go.mod and go.sum in the triggers * manually fix * Make an arbitrary change rather than touching the trigger to force a run * Drop all triggers - run all the time * Print diff - taken from @papagian's PR * Manual fixes to swagger doc --------- Co-authored-by: Ryan McKinley --- .drone.yml | 14 ++++---------- pkg/api/alerting.go | 2 +- pkg/services/ngalert/api/tooling/api.json | 2 +- pkg/services/ngalert/api/tooling/post.json | 2 +- pkg/services/ngalert/api/tooling/spec.json | 2 +- pkg/services/ngalert/schedule/schedule.go | 2 +- public/api-enterprise-spec.json | 2 +- public/api-merged.json | 6 +++--- public/openapi3.json | 6 +++--- scripts/drone/events/pr.star | 1 - scripts/drone/pipelines/swagger_gen.star | 8 +++++--- 11 files changed, 21 insertions(+), 26 deletions(-) diff --git a/.drone.yml b/.drone.yml index 14ae569f2b..6af8a77378 100644 --- a/.drone.yml +++ b/.drone.yml @@ -1250,9 +1250,9 @@ steps: - make swagger-clean && make openapi3-gen - for f in public/api-merged.json public/openapi3.json; do git add $f; done - if [ -z "$(git diff --name-only --cached)" ]; then echo "Everything seems up to - date!"; else echo "Please ensure the branch is up-to-date, then regenerate the - specification by running make swagger-clean && make openapi3-gen" && return 1; - fi + date!"; else git diff --cached && echo "Please ensure the branch is up-to-date, + then regenerate the specification by running make swagger-clean && make openapi3-gen" + && return 1; fi depends_on: - clone-enterprise environment: @@ -1263,12 +1263,6 @@ steps: trigger: event: - pull_request - paths: - exclude: - - docs/** - - '*.md' - include: - - pkg/** type: docker volumes: - host: @@ -4926,6 +4920,6 @@ kind: secret name: gcr_credentials --- kind: signature -hmac: 32b6ed2f5819225842aa94379423bcf4354dde154e91af3e293bc919594c10b9 +hmac: 2f4a5620d00189804c2facf65fa2a17b75883cf330cd32e5612a2f36d3712847 ... diff --git a/pkg/api/alerting.go b/pkg/api/alerting.go index 8f6d8c4ac6..2254263642 100644 --- a/pkg/api/alerting.go +++ b/pkg/api/alerting.go @@ -216,7 +216,7 @@ func (hs *HTTPServer) AlertTest(c *contextmodel.ReqContext) response.Response { // swagger:route GET /alerts/{alert_id} legacy_alerts getAlertByID // -// Get alert by ID. +// Get alert by internal ID. // // “evalMatches” data in the response is cached in the db when and only when the state of the alert changes (e.g. transitioning from “ok” to “alerting” state). // If data from one server triggers the alert first and, before that server is seen leaving alerting state, a second server also enters a state that would trigger the alert, the second server will not be visible in “evalMatches” data. diff --git a/pkg/services/ngalert/api/tooling/api.json b/pkg/services/ngalert/api/tooling/api.json index 9395f274c5..dade93deb1 100644 --- a/pkg/services/ngalert/api/tooling/api.json +++ b/pkg/services/ngalert/api/tooling/api.json @@ -1368,7 +1368,7 @@ "type": "object" }, "FrameType": { - "description": "A FrameType string, when present in a frame's metadata, asserts that the\nframe's structure conforms to the FrameType's specification.\nThis property is currently optional, so FrameType may be FrameTypeUnknown even if the properties of\nthe Frame correspond to a defined FrameType.", + "description": "A FrameType string, when present in a frame's metadata, asserts that the\nframe's structure conforms to the FrameType's specification.\nThis property is currently optional, so FrameType may be FrameTypeUnknown even if the properties of\nthe Frame correspond to a defined FrameType.\n+enum", "type": "string" }, "FrameTypeVersion": { diff --git a/pkg/services/ngalert/api/tooling/post.json b/pkg/services/ngalert/api/tooling/post.json index 8c9e8a312f..8cd3d04a04 100644 --- a/pkg/services/ngalert/api/tooling/post.json +++ b/pkg/services/ngalert/api/tooling/post.json @@ -1368,7 +1368,7 @@ "type": "object" }, "FrameType": { - "description": "A FrameType string, when present in a frame's metadata, asserts that the\nframe's structure conforms to the FrameType's specification.\nThis property is currently optional, so FrameType may be FrameTypeUnknown even if the properties of\nthe Frame correspond to a defined FrameType.", + "description": "A FrameType string, when present in a frame's metadata, asserts that the\nframe's structure conforms to the FrameType's specification.\nThis property is currently optional, so FrameType may be FrameTypeUnknown even if the properties of\nthe Frame correspond to a defined FrameType.\n+enum", "type": "string" }, "FrameTypeVersion": { diff --git a/pkg/services/ngalert/api/tooling/spec.json b/pkg/services/ngalert/api/tooling/spec.json index 191253a57b..5d23967fa7 100644 --- a/pkg/services/ngalert/api/tooling/spec.json +++ b/pkg/services/ngalert/api/tooling/spec.json @@ -4972,7 +4972,7 @@ } }, "FrameType": { - "description": "A FrameType string, when present in a frame's metadata, asserts that the\nframe's structure conforms to the FrameType's specification.\nThis property is currently optional, so FrameType may be FrameTypeUnknown even if the properties of\nthe Frame correspond to a defined FrameType.", + "description": "A FrameType string, when present in a frame's metadata, asserts that the\nframe's structure conforms to the FrameType's specification.\nThis property is currently optional, so FrameType may be FrameTypeUnknown even if the properties of\nthe Frame correspond to a defined FrameType.\n+enum", "type": "string" }, "FrameTypeVersion": { diff --git a/pkg/services/ngalert/schedule/schedule.go b/pkg/services/ngalert/schedule/schedule.go index 075cc9f985..7a32e5bfeb 100644 --- a/pkg/services/ngalert/schedule/schedule.go +++ b/pkg/services/ngalert/schedule/schedule.go @@ -107,7 +107,7 @@ type SchedulerCfg struct { Log log.Logger } -// NewScheduler returns a new schedule. +// NewScheduler returns a new scheduler. func NewScheduler(cfg SchedulerCfg, stateManager *state.Manager) *schedule { const minMaxAttempts = int64(1) if cfg.MaxAttempts < minMaxAttempts { diff --git a/public/api-enterprise-spec.json b/public/api-enterprise-spec.json index ea1f0a592a..3da3794f3c 100644 --- a/public/api-enterprise-spec.json +++ b/public/api-enterprise-spec.json @@ -5959,7 +5959,7 @@ "type": "object", "title": "QueryDataResponse contains the results from a QueryDataRequest.", "properties": { - "Responses": { + "results": { "$ref": "#/definitions/Responses" } } diff --git a/public/api-merged.json b/public/api-merged.json index dd34a34252..9ad7eef273 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -2368,7 +2368,7 @@ "tags": [ "legacy_alerts" ], - "summary": "Get alert by ID.", + "summary": "Get alert by internal ID.", "operationId": "getAlertByID", "parameters": [ { @@ -15287,7 +15287,7 @@ } }, "FrameType": { - "description": "A FrameType string, when present in a frame's metadata, asserts that the\nframe's structure conforms to the FrameType's specification.\nThis property is currently optional, so FrameType may be FrameTypeUnknown even if the properties of\nthe Frame correspond to a defined FrameType.", + "description": "A FrameType string, when present in a frame's metadata, asserts that the\nframe's structure conforms to the FrameType's specification.\nThis property is currently optional, so FrameType may be FrameTypeUnknown even if the properties of\nthe Frame correspond to a defined FrameType.\n+enum", "type": "string" }, "FrameTypeVersion": { @@ -18459,7 +18459,7 @@ "type": "object", "title": "QueryDataResponse contains the results from a QueryDataRequest.", "properties": { - "Responses": { + "results": { "$ref": "#/definitions/Responses" } } diff --git a/public/openapi3.json b/public/openapi3.json index d6f99d851f..3bd8efd4f2 100644 --- a/public/openapi3.json +++ b/public/openapi3.json @@ -5797,7 +5797,7 @@ "type": "object" }, "FrameType": { - "description": "A FrameType string, when present in a frame's metadata, asserts that the\nframe's structure conforms to the FrameType's specification.\nThis property is currently optional, so FrameType may be FrameTypeUnknown even if the properties of\nthe Frame correspond to a defined FrameType.", + "description": "A FrameType string, when present in a frame's metadata, asserts that the\nframe's structure conforms to the FrameType's specification.\nThis property is currently optional, so FrameType may be FrameTypeUnknown even if the properties of\nthe Frame correspond to a defined FrameType.\n+enum", "type": "string" }, "FrameTypeVersion": { @@ -8967,7 +8967,7 @@ "QueryDataResponse": { "description": "It is the return type of a QueryData call.", "properties": { - "Responses": { + "results": { "$ref": "#/components/schemas/Responses" } }, @@ -15537,7 +15537,7 @@ "$ref": "#/components/responses/internalServerError" } }, - "summary": "Get alert by ID.", + "summary": "Get alert by internal ID.", "tags": [ "legacy_alerts" ] diff --git a/scripts/drone/events/pr.star b/scripts/drone/events/pr.star index fb9f28e453..0e14eab5c0 100644 --- a/scripts/drone/events/pr.star +++ b/scripts/drone/events/pr.star @@ -145,7 +145,6 @@ def pr_pipelines(): docs_pipelines(ver_mode, trigger_docs_pr()), shellcheck_pipeline(), swagger_gen( - get_pr_trigger(include_paths = ["pkg/**"]), ver_mode, ), integration_benchmarks( diff --git a/scripts/drone/pipelines/swagger_gen.star b/scripts/drone/pipelines/swagger_gen.star index ef82ce1547..7275f405bf 100644 --- a/scripts/drone/pipelines/swagger_gen.star +++ b/scripts/drone/pipelines/swagger_gen.star @@ -33,14 +33,14 @@ def swagger_gen_step(ver_mode): "apk add --update git make", "make swagger-clean && make openapi3-gen", "for f in public/api-merged.json public/openapi3.json; do git add $f; done", - 'if [ -z "$(git diff --name-only --cached)" ]; then echo "Everything seems up to date!"; else echo "Please ensure the branch is up-to-date, then regenerate the specification by running make swagger-clean && make openapi3-gen" && return 1; fi', + 'if [ -z "$(git diff --name-only --cached)" ]; then echo "Everything seems up to date!"; else git diff --cached && echo "Please ensure the branch is up-to-date, then regenerate the specification by running make swagger-clean && make openapi3-gen" && return 1; fi', ], "depends_on": [ "clone-enterprise", ], } -def swagger_gen(trigger, ver_mode, source = "${DRONE_SOURCE_BRANCH}"): +def swagger_gen(ver_mode, source = "${DRONE_SOURCE_BRANCH}"): test_steps = [ clone_enterprise_step_pr(source = source, canFail = True), swagger_gen_step(ver_mode = ver_mode), @@ -48,7 +48,9 @@ def swagger_gen(trigger, ver_mode, source = "${DRONE_SOURCE_BRANCH}"): p = pipeline( name = "{}-swagger-gen".format(ver_mode), - trigger = trigger, + trigger = { + "event": ["pull_request"], + }, services = [], steps = test_steps, ) From 201f5d3ac9ba1e6984c19770965b63f972af62ac Mon Sep 17 00:00:00 2001 From: Alexander Weaver Date: Wed, 6 Mar 2024 16:39:23 -0600 Subject: [PATCH 0448/1406] Alerting: Extract large closures in ruleRoutine (#84035) * extract notify * extract resetState * move evaluate metrics inside evaluate * split out evaluate --- pkg/services/ngalert/schedule/alert_rule.go | 239 ++++++++++---------- 1 file changed, 119 insertions(+), 120 deletions(-) diff --git a/pkg/services/ngalert/schedule/alert_rule.go b/pkg/services/ngalert/schedule/alert_rule.go index 396d45bd49..88642aaf15 100644 --- a/pkg/services/ngalert/schedule/alert_rule.go +++ b/pkg/services/ngalert/schedule/alert_rule.go @@ -180,127 +180,11 @@ func (a *alertRuleInfo) stop(reason error) { a.stopFn(reason) } -//nolint:gocyclo func (a *alertRuleInfo) run(key ngmodels.AlertRuleKey) error { grafanaCtx := ngmodels.WithRuleKey(a.ctx, key) logger := a.logger.FromContext(grafanaCtx) logger.Debug("Alert rule routine started") - orgID := fmt.Sprint(key.OrgID) - evalTotal := a.metrics.EvalTotal.WithLabelValues(orgID) - evalDuration := a.metrics.EvalDuration.WithLabelValues(orgID) - evalTotalFailures := a.metrics.EvalFailures.WithLabelValues(orgID) - processDuration := a.metrics.ProcessDuration.WithLabelValues(orgID) - sendDuration := a.metrics.SendDuration.WithLabelValues(orgID) - - notify := func(states []state.StateTransition) { - expiredAlerts := state.FromAlertsStateToStoppedAlert(states, a.appURL, a.clock) - if len(expiredAlerts.PostableAlerts) > 0 { - a.sender.Send(grafanaCtx, key, expiredAlerts) - } - } - - resetState := func(ctx context.Context, isPaused bool) { - rule := a.ruleProvider.get(key) - reason := ngmodels.StateReasonUpdated - if isPaused { - reason = ngmodels.StateReasonPaused - } - states := a.stateManager.ResetStateByRuleUID(ctx, rule, reason) - notify(states) - } - - evaluate := func(ctx context.Context, f fingerprint, attempt int64, e *evaluation, span trace.Span, retry bool) error { - logger := logger.New("version", e.rule.Version, "fingerprint", f, "attempt", attempt, "now", e.scheduledAt).FromContext(ctx) - start := a.clock.Now() - - evalCtx := eval.NewContextWithPreviousResults(ctx, SchedulerUserFor(e.rule.OrgID), a.newLoadedMetricsReader(e.rule)) - ruleEval, err := a.evalFactory.Create(evalCtx, e.rule.GetEvalCondition()) - var results eval.Results - var dur time.Duration - if err != nil { - dur = a.clock.Now().Sub(start) - logger.Error("Failed to build rule evaluator", "error", err) - } else { - results, err = ruleEval.Evaluate(ctx, e.scheduledAt) - dur = a.clock.Now().Sub(start) - if err != nil { - logger.Error("Failed to evaluate rule", "error", err, "duration", dur) - } - } - - evalTotal.Inc() - evalDuration.Observe(dur.Seconds()) - - if ctx.Err() != nil { // check if the context is not cancelled. The evaluation can be a long-running task. - span.SetStatus(codes.Error, "rule evaluation cancelled") - logger.Debug("Skip updating the state because the context has been cancelled") - return nil - } - - if err != nil || results.HasErrors() { - evalTotalFailures.Inc() - - // Only retry (return errors) if this isn't the last attempt, otherwise skip these return operations. - if retry { - // The only thing that can return non-nil `err` from ruleEval.Evaluate is the server side expression pipeline. - // This includes transport errors such as transient network errors. - if err != nil { - span.SetStatus(codes.Error, "rule evaluation failed") - span.RecordError(err) - return fmt.Errorf("server side expressions pipeline returned an error: %w", err) - } - - // If the pipeline executed successfully but have other types of errors that can be retryable, we should do so. - if !results.HasNonRetryableErrors() { - span.SetStatus(codes.Error, "rule evaluation failed") - span.RecordError(err) - return fmt.Errorf("the result-set has errors that can be retried: %w", results.Error()) - } - } - - // If results is nil, we assume that the error must be from the SSE pipeline (ruleEval.Evaluate) which is the only code that can actually return an `err`. - if results == nil { - results = append(results, eval.NewResultFromError(err, e.scheduledAt, dur)) - } - - // If err is nil, we assume that the SSS pipeline succeeded and that the error must be embedded in the results. - if err == nil { - err = results.Error() - } - - span.SetStatus(codes.Error, "rule evaluation failed") - span.RecordError(err) - } else { - logger.Debug("Alert rule evaluated", "results", results, "duration", dur) - span.AddEvent("rule evaluated", trace.WithAttributes( - attribute.Int64("results", int64(len(results))), - )) - } - start = a.clock.Now() - processedStates := a.stateManager.ProcessEvalResults( - ctx, - e.scheduledAt, - e.rule, - results, - state.GetRuleExtraLabels(logger, e.rule, e.folderTitle, !a.disableGrafanaFolder), - ) - processDuration.Observe(a.clock.Now().Sub(start).Seconds()) - - start = a.clock.Now() - alerts := state.FromStateTransitionToPostableAlerts(processedStates, a.stateManager, a.appURL) - span.AddEvent("results processed", trace.WithAttributes( - attribute.Int64("state_transitions", int64(len(processedStates))), - attribute.Int64("alerts_to_send", int64(len(alerts.PostableAlerts))), - )) - if len(alerts.PostableAlerts) > 0 { - a.sender.Send(ctx, key, alerts) - } - sendDuration.Observe(a.clock.Now().Sub(start).Seconds()) - - return nil - } - evalRunning := false var currentFingerprint fingerprint defer a.stopApplied(key) @@ -315,7 +199,7 @@ func (a *alertRuleInfo) run(key ngmodels.AlertRuleKey) error { logger.Info("Clearing the state of the rule because it was updated", "isPaused", ctx.IsPaused, "fingerprint", ctx.Fingerprint) // clear the state. So the next evaluation will start from the scratch. - resetState(grafanaCtx, ctx.IsPaused) + a.resetState(grafanaCtx, key, ctx.IsPaused) currentFingerprint = ctx.Fingerprint // evalCh - used by the scheduler to signal that evaluation is needed. case ctx, ok := <-a.evalCh: @@ -348,7 +232,7 @@ func (a *alertRuleInfo) run(key ngmodels.AlertRuleKey) error { // lingers in DB and won't be cleaned up until next alert rule update. needReset = needReset || (currentFingerprint == 0 && isPaused) if needReset { - resetState(grafanaCtx, isPaused) + a.resetState(grafanaCtx, key, isPaused) } currentFingerprint = f if isPaused { @@ -375,7 +259,7 @@ func (a *alertRuleInfo) run(key ngmodels.AlertRuleKey) error { } retry := attempt < a.maxAttempts - err := evaluate(tracingCtx, f, attempt, ctx, span, retry) + err := a.evaluate(tracingCtx, key, f, attempt, ctx, span, retry) // This is extremely confusing - when we exhaust all retry attempts, or we have no retryable errors // we return nil - so technically, this is meaningless to know whether the evaluation has errors or not. span.End() @@ -403,7 +287,7 @@ func (a *alertRuleInfo) run(key ngmodels.AlertRuleKey) error { ctx, cancelFunc := context.WithTimeout(context.Background(), time.Minute) defer cancelFunc() states := a.stateManager.DeleteStateByRuleUID(ngmodels.WithRuleKey(ctx, key), key, ngmodels.StateReasonRuleDeleted) - notify(states) + a.notify(grafanaCtx, key, states) } logger.Debug("Stopping alert rule routine") return nil @@ -411,6 +295,121 @@ func (a *alertRuleInfo) run(key ngmodels.AlertRuleKey) error { } } +func (a *alertRuleInfo) evaluate(ctx context.Context, key ngmodels.AlertRuleKey, f fingerprint, attempt int64, e *evaluation, span trace.Span, retry bool) error { + orgID := fmt.Sprint(key.OrgID) + evalTotal := a.metrics.EvalTotal.WithLabelValues(orgID) + evalDuration := a.metrics.EvalDuration.WithLabelValues(orgID) + evalTotalFailures := a.metrics.EvalFailures.WithLabelValues(orgID) + processDuration := a.metrics.ProcessDuration.WithLabelValues(orgID) + sendDuration := a.metrics.SendDuration.WithLabelValues(orgID) + + logger := a.logger.FromContext(ctx).New("version", e.rule.Version, "fingerprint", f, "attempt", attempt, "now", e.scheduledAt).FromContext(ctx) + start := a.clock.Now() + + evalCtx := eval.NewContextWithPreviousResults(ctx, SchedulerUserFor(e.rule.OrgID), a.newLoadedMetricsReader(e.rule)) + ruleEval, err := a.evalFactory.Create(evalCtx, e.rule.GetEvalCondition()) + var results eval.Results + var dur time.Duration + if err != nil { + dur = a.clock.Now().Sub(start) + logger.Error("Failed to build rule evaluator", "error", err) + } else { + results, err = ruleEval.Evaluate(ctx, e.scheduledAt) + dur = a.clock.Now().Sub(start) + if err != nil { + logger.Error("Failed to evaluate rule", "error", err, "duration", dur) + } + } + + evalTotal.Inc() + evalDuration.Observe(dur.Seconds()) + + if ctx.Err() != nil { // check if the context is not cancelled. The evaluation can be a long-running task. + span.SetStatus(codes.Error, "rule evaluation cancelled") + logger.Debug("Skip updating the state because the context has been cancelled") + return nil + } + + if err != nil || results.HasErrors() { + evalTotalFailures.Inc() + + // Only retry (return errors) if this isn't the last attempt, otherwise skip these return operations. + if retry { + // The only thing that can return non-nil `err` from ruleEval.Evaluate is the server side expression pipeline. + // This includes transport errors such as transient network errors. + if err != nil { + span.SetStatus(codes.Error, "rule evaluation failed") + span.RecordError(err) + return fmt.Errorf("server side expressions pipeline returned an error: %w", err) + } + + // If the pipeline executed successfully but have other types of errors that can be retryable, we should do so. + if !results.HasNonRetryableErrors() { + span.SetStatus(codes.Error, "rule evaluation failed") + span.RecordError(err) + return fmt.Errorf("the result-set has errors that can be retried: %w", results.Error()) + } + } + + // If results is nil, we assume that the error must be from the SSE pipeline (ruleEval.Evaluate) which is the only code that can actually return an `err`. + if results == nil { + results = append(results, eval.NewResultFromError(err, e.scheduledAt, dur)) + } + + // If err is nil, we assume that the SSS pipeline succeeded and that the error must be embedded in the results. + if err == nil { + err = results.Error() + } + + span.SetStatus(codes.Error, "rule evaluation failed") + span.RecordError(err) + } else { + logger.Debug("Alert rule evaluated", "results", results, "duration", dur) + span.AddEvent("rule evaluated", trace.WithAttributes( + attribute.Int64("results", int64(len(results))), + )) + } + start = a.clock.Now() + processedStates := a.stateManager.ProcessEvalResults( + ctx, + e.scheduledAt, + e.rule, + results, + state.GetRuleExtraLabels(logger, e.rule, e.folderTitle, !a.disableGrafanaFolder), + ) + processDuration.Observe(a.clock.Now().Sub(start).Seconds()) + + start = a.clock.Now() + alerts := state.FromStateTransitionToPostableAlerts(processedStates, a.stateManager, a.appURL) + span.AddEvent("results processed", trace.WithAttributes( + attribute.Int64("state_transitions", int64(len(processedStates))), + attribute.Int64("alerts_to_send", int64(len(alerts.PostableAlerts))), + )) + if len(alerts.PostableAlerts) > 0 { + a.sender.Send(ctx, key, alerts) + } + sendDuration.Observe(a.clock.Now().Sub(start).Seconds()) + + return nil +} + +func (a *alertRuleInfo) notify(ctx context.Context, key ngmodels.AlertRuleKey, states []state.StateTransition) { + expiredAlerts := state.FromAlertsStateToStoppedAlert(states, a.appURL, a.clock) + if len(expiredAlerts.PostableAlerts) > 0 { + a.sender.Send(ctx, key, expiredAlerts) + } +} + +func (a *alertRuleInfo) resetState(ctx context.Context, key ngmodels.AlertRuleKey, isPaused bool) { + rule := a.ruleProvider.get(key) + reason := ngmodels.StateReasonUpdated + if isPaused { + reason = ngmodels.StateReasonPaused + } + states := a.stateManager.ResetStateByRuleUID(ctx, rule, reason) + a.notify(ctx, key, states) +} + // evalApplied is only used on tests. func (a *alertRuleInfo) evalApplied(alertDefKey ngmodels.AlertRuleKey, now time.Time) { if a.evalAppliedHook == nil { From d549a3aabb1a0925a987ed24dd5e4bef9657f232 Mon Sep 17 00:00:00 2001 From: Leon Sorokin Date: Wed, 6 Mar 2024 19:30:33 -0600 Subject: [PATCH 0449/1406] VizTooltips: Heatmap fixes and improvements (#83876) Co-authored-by: Adela Almasan --- .../src/components/VizTooltip/utils.ts | 2 +- .../data-hover/ExemplarHoverView.tsx | 2 +- .../panel/heatmap/HeatmapHoverViewOld.tsx | 41 ++++-------- .../plugins/panel/heatmap/HeatmapPanel.tsx | 53 +++------------ ...eatmapHoverView.tsx => HeatmapTooltip.tsx} | 63 ++++++------------ public/app/plugins/panel/heatmap/fields.ts | 64 +++++++++++-------- public/app/plugins/panel/heatmap/module.tsx | 10 +-- public/app/plugins/panel/heatmap/utils.ts | 16 +++-- .../panel/timeseries/TimeSeriesTooltip.tsx | 2 +- 9 files changed, 91 insertions(+), 162 deletions(-) rename public/app/plugins/panel/heatmap/{HeatmapHoverView.tsx => HeatmapTooltip.tsx} (85%) diff --git a/packages/grafana-ui/src/components/VizTooltip/utils.ts b/packages/grafana-ui/src/components/VizTooltip/utils.ts index 70de02fd70..0d8058fc53 100644 --- a/packages/grafana-ui/src/components/VizTooltip/utils.ts +++ b/packages/grafana-ui/src/components/VizTooltip/utils.ts @@ -85,7 +85,7 @@ export const getContentItems = ( ): VizTooltipItem[] => { let rows: VizTooltipItem[] = []; - let allNumeric = false; + let allNumeric = true; for (let i = 0; i < fields.length; i++) { const field = fields[i]; diff --git a/public/app/features/visualization/data-hover/ExemplarHoverView.tsx b/public/app/features/visualization/data-hover/ExemplarHoverView.tsx index 4d92189204..c7f4fcc1a5 100644 --- a/public/app/features/visualization/data-hover/ExemplarHoverView.tsx +++ b/public/app/features/visualization/data-hover/ExemplarHoverView.tsx @@ -39,7 +39,7 @@ export const ExemplarHoverView = ({ displayValues, links, header = 'Exemplar' }: ); })}
- {links && ( + {links && links.length > 0 && (
{links.map((link, i) => ( diff --git a/public/app/plugins/panel/heatmap/HeatmapHoverViewOld.tsx b/public/app/plugins/panel/heatmap/HeatmapHoverViewOld.tsx index 7ef653a2d5..f7c7302206 100644 --- a/public/app/plugins/panel/heatmap/HeatmapHoverViewOld.tsx +++ b/public/app/plugins/panel/heatmap/HeatmapHoverViewOld.tsx @@ -9,9 +9,7 @@ import { getFieldDisplayName, LinkModel, TimeRange, - getLinksSupplier, InterpolateFunction, - ScopedVars, } from '@grafana/data'; import { HeatmapCellLayout } from '@grafana/schema'; import { LinkButton, VerticalGroup } from '@grafana/ui'; @@ -19,6 +17,8 @@ import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; import { isHeatmapCellsDense, readHeatmapRowsCustomMeta } from 'app/features/transformers/calculateHeatmap/heatmap'; import { DataHoverView } from 'app/features/visualization/data-hover/DataHoverView'; +import { getDataLinks } from '../status-history/utils'; + import { HeatmapData } from './fields'; import { renderHistogram } from './renderHistogram'; import { HeatmapHoverEvent } from './utils'; @@ -29,7 +29,6 @@ type Props = { showHistogram?: boolean; timeRange: TimeRange; replaceVars: InterpolateFunction; - scopedVars: ScopedVars[]; }; export const HeatmapHoverView = (props: Props) => { @@ -39,7 +38,7 @@ export const HeatmapHoverView = (props: Props) => { return ; }; -const HeatmapHoverCell = ({ data, hover, showHistogram = false, scopedVars, replaceVars }: Props) => { +const HeatmapHoverCell = ({ data, hover, showHistogram = false }: Props) => { const index = hover.dataIdx; const [isSparse] = useState( @@ -70,7 +69,8 @@ const HeatmapHoverCell = ({ data, hover, showHistogram = false, scopedVars, repl const meta = readHeatmapRowsCustomMeta(data.heatmap); const yDisp = yField?.display ? (v: string) => formattedValueToString(yField.display!(v)) : (v: string) => `${v}`; - const yValueIdx = index % data.yBucketCount! ?? 0; + const yValueIdx = index % (data.yBucketCount ?? 1); + const xValueIdx = Math.floor(index / (data.yBucketCount ?? 1)); let yBucketMin: string; let yBucketMax: string; @@ -126,33 +126,16 @@ const HeatmapHoverCell = ({ data, hover, showHistogram = false, scopedVars, repl const count = countVals?.[index]; - const visibleFields = data.heatmap?.fields.filter((f) => !Boolean(f.config.custom?.hideFrom?.tooltip)); - const links: Array> = []; - const linkLookup = new Set(); - - for (const field of visibleFields ?? []) { - const hasLinks = field.config.links && field.config.links.length > 0; + let links: Array> = []; - if (hasLinks && data.heatmap) { - const appropriateScopedVars = scopedVars.find( - (scopedVar) => - scopedVar && scopedVar.__dataContext && scopedVar.__dataContext.value.field.name === nonNumericOrdinalDisplay - ); + const linksField = data.series?.fields[yValueIdx + 1]; - field.getLinks = getLinksSupplier(data.heatmap, field, appropriateScopedVars || {}, replaceVars); - } + if (linksField != null) { + const visible = !Boolean(linksField.config.custom?.hideFrom?.tooltip); + const hasLinks = (linksField.config.links?.length ?? 0) > 0; - if (field.getLinks) { - const value = field.values[index]; - const display = field.display ? field.display(value) : { text: `${value}`, numeric: +value }; - - field.getLinks({ calculatedValue: display, valueRowIndex: index }).forEach((link) => { - const key = `${link.title}/${link.href}`; - if (!linkLookup.has(key)) { - links.push(link); - linkLookup.add(key); - } - }); + if (visible && hasLinks) { + links = getDataLinks(linksField, xValueIdx); } } diff --git a/public/app/plugins/panel/heatmap/HeatmapPanel.tsx b/public/app/plugins/panel/heatmap/HeatmapPanel.tsx index 018ddca15c..b03bb3bd2e 100644 --- a/public/app/plugins/panel/heatmap/HeatmapPanel.tsx +++ b/public/app/plugins/panel/heatmap/HeatmapPanel.tsx @@ -1,17 +1,7 @@ import { css } from '@emotion/css'; import React, { useCallback, useMemo, useRef, useState } from 'react'; -import { - DashboardCursorSync, - DataFrame, - DataFrameType, - Field, - getLinksSupplier, - GrafanaTheme2, - PanelProps, - ScopedVars, - TimeRange, -} from '@grafana/data'; +import { DashboardCursorSync, DataFrameType, GrafanaTheme2, PanelProps, TimeRange } from '@grafana/data'; import { config, PanelDataErrorView } from '@grafana/runtime'; import { ScaleDistributionConfig } from '@grafana/schema'; import { @@ -34,8 +24,8 @@ import { isHeatmapCellsDense, readHeatmapRowsCustomMeta } from 'app/features/tra import { AnnotationsPlugin2 } from '../timeseries/plugins/AnnotationsPlugin2'; import { ExemplarModalHeader } from './ExemplarModalHeader'; -import { HeatmapHoverView } from './HeatmapHoverView'; -import { HeatmapHoverView as HeatmapHoverViewOld } from './HeatmapHoverViewOld'; +import { HeatmapHoverView } from './HeatmapHoverViewOld'; +import { HeatmapTooltip } from './HeatmapTooltip'; import { prepareHeatmapData } from './fields'; import { quantizeScheme } from './palettes'; import { Options } from './types'; @@ -70,50 +60,26 @@ export const HeatmapPanel = ({ // temp range set for adding new annotation set by TooltipPlugin2, consumed by AnnotationPlugin2 const [newAnnotationRange, setNewAnnotationRange] = useState(null); - // necessary for enabling datalinks in hover view - let scopedVarsFromRawData: ScopedVars[] = []; - for (const series of data.series) { - for (const field of series.fields) { - if (field.state?.scopedVars) { - scopedVarsFromRawData.push(field.state.scopedVars); - } - } - } - // ugh let timeRangeRef = useRef(timeRange); timeRangeRef.current = timeRange; - const getFieldLinksSupplier = useCallback( - (exemplars: DataFrame, field: Field) => { - return getLinksSupplier(exemplars, field, field.state?.scopedVars ?? {}, replaceVariables); - }, - [replaceVariables] - ); - const palette = useMemo(() => quantizeScheme(options.color, theme), [options.color, theme]); const info = useMemo(() => { try { - return prepareHeatmapData( - data.series, - data.annotations, - options, - palette, - theme, - getFieldLinksSupplier, - replaceVariables - ); + return prepareHeatmapData(data.series, data.annotations, options, palette, theme, replaceVariables); } catch (ex) { return { warning: `${ex}` }; } - }, [data.series, data.annotations, options, palette, theme, getFieldLinksSupplier, replaceVariables]); + }, [data.series, data.annotations, options, palette, theme, replaceVariables]); const facets = useMemo(() => { let exemplarsXFacet: number[] | undefined = []; // "Time" field let exemplarsYFacet: Array = []; const meta = readHeatmapRowsCustomMeta(info.heatmap); + if (info.exemplars?.length) { exemplarsXFacet = info.exemplars?.fields[0].values; @@ -265,7 +231,7 @@ export const HeatmapPanel = ({ }; return ( - ); @@ -308,13 +272,12 @@ export const HeatmapPanel = ({ allowPointerEvents={isToolTipOpen.current} > {shouldDisplayCloseButton && } - )} diff --git a/public/app/plugins/panel/heatmap/HeatmapHoverView.tsx b/public/app/plugins/panel/heatmap/HeatmapTooltip.tsx similarity index 85% rename from public/app/plugins/panel/heatmap/HeatmapHoverView.tsx rename to public/app/plugins/panel/heatmap/HeatmapTooltip.tsx index 6d946e35dc..ed104d63f2 100644 --- a/public/app/plugins/panel/heatmap/HeatmapHoverView.tsx +++ b/public/app/plugins/panel/heatmap/HeatmapTooltip.tsx @@ -1,4 +1,4 @@ -import React, { ReactElement, useEffect, useRef, useState } from 'react'; +import React, { ReactElement, useEffect, useRef, useState, ReactNode } from 'react'; import uPlot from 'uplot'; import { @@ -7,11 +7,8 @@ import { FieldType, formattedValueToString, getFieldDisplayName, - getLinksSupplier, - InterpolateFunction, LinkModel, PanelData, - ScopedVars, } from '@grafana/data'; import { HeatmapCellLayout } from '@grafana/schema'; import { TooltipDisplayMode, useStyles2, useTheme2 } from '@grafana/ui'; @@ -24,13 +21,14 @@ import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; import { isHeatmapCellsDense, readHeatmapRowsCustomMeta } from 'app/features/transformers/calculateHeatmap/heatmap'; import { DataHoverView } from 'app/features/visualization/data-hover/DataHoverView'; +import { getDataLinks } from '../status-history/utils'; import { getStyles } from '../timeseries/TimeSeriesTooltip'; import { HeatmapData } from './fields'; import { renderHistogram } from './renderHistogram'; import { formatMilliseconds, getFieldFromData, getHoverCellColor, getSparseCellMinMax } from './tooltip/utils'; -interface Props { +interface HeatmapTooltipProps { mode: TooltipDisplayMode; dataIdxs: Array; seriesIdx: number | null | undefined; @@ -40,12 +38,10 @@ interface Props { isPinned: boolean; dismiss: () => void; panelData: PanelData; - replaceVars: InterpolateFunction; - scopedVars: ScopedVars[]; annotate?: () => void; } -export const HeatmapHoverView = (props: Props) => { +export const HeatmapTooltip = (props: HeatmapTooltipProps) => { if (props.seriesIdx === 2) { return ( { +}: HeatmapTooltipProps) => { const index = dataIdxs[1]!; const data = dataRef.current; @@ -114,11 +108,8 @@ const HeatmapHoverCell = ({ let contentItems: VizTooltipItem[] = []; - const getYValueIndex = (idx: number) => { - return idx % data.yBucketCount! ?? 0; - }; - - let yValueIdx = getYValueIndex(index); + const yValueIdx = index % (data.yBucketCount ?? 1); + const xValueIdx = Math.floor(index / (data.yBucketCount ?? 1)); const getData = (idx: number = index) => { if (meta.yOrdinalDisplay) { @@ -187,7 +178,6 @@ const HeatmapHoverCell = ({ if (isSparse) { ({ xBucketMin, xBucketMax, yBucketMin, yBucketMax } = getSparseCellMinMax(data!, idx)); } else { - yValueIdx = getYValueIndex(idx); getData(idx); } @@ -283,34 +273,23 @@ const HeatmapHoverCell = ({ }); } - const visibleFields = data.heatmap?.fields.filter((f) => !Boolean(f.config.custom?.hideFrom?.tooltip)); - const links: Array> = []; - const linkLookup = new Set(); + let footer: ReactNode; - for (const field of visibleFields ?? []) { - const hasLinks = field.config.links && field.config.links.length > 0; + if (isPinned) { + let links: Array> = []; - if (hasLinks && data.heatmap) { - const appropriateScopedVars = scopedVars.find( - (scopedVar) => - scopedVar && scopedVar.__dataContext && scopedVar.__dataContext.value.field.name === nonNumericOrdinalDisplay - ); - - field.getLinks = getLinksSupplier(data.heatmap, field, appropriateScopedVars || {}, replaceVars); - } + const linksField = data.series?.fields[yValueIdx + 1]; - if (field.getLinks) { - const value = field.values[index]; - const display = field.display ? field.display(value) : { text: `${value}`, numeric: +value }; + if (linksField != null) { + const visible = !Boolean(linksField.config.custom?.hideFrom?.tooltip); + const hasLinks = (linksField.config.links?.length ?? 0) > 0; - field.getLinks({ calculatedValue: display, valueRowIndex: index }).forEach((link) => { - const key = `${link.title}/${link.href}`; - if (!linkLookup.has(key)) { - links.push(link); - linkLookup.add(key); - } - }); + if (visible && hasLinks) { + links = getDataLinks(linksField, xValueIdx); + } } + + footer = ; } let can = useRef(null); @@ -377,9 +356,7 @@ const HeatmapHoverCell = ({
))} - {(links.length > 0 || isPinned) && ( - - )} + {footer}
); }; diff --git a/public/app/plugins/panel/heatmap/fields.ts b/public/app/plugins/panel/heatmap/fields.ts index 0529ec3fce..e45b88ae03 100644 --- a/public/app/plugins/panel/heatmap/fields.ts +++ b/public/app/plugins/panel/heatmap/fields.ts @@ -6,12 +6,11 @@ import { FieldType, formattedValueToString, getDisplayProcessor, + getLinksSupplier, GrafanaTheme2, InterpolateFunction, - LinkModel, outerJoinDataFrames, ValueFormatter, - ValueLinkConfig, } from '@grafana/data'; import { config } from '@grafana/runtime'; import { HeatmapCellLayout } from '@grafana/schema'; @@ -39,6 +38,8 @@ export interface HeatmapData { maxValue: number; }; + series?: DataFrame; // the joined single frame for nonNumericOrdinalY data links + exemplars?: DataFrame; // optionally linked exemplars exemplarColor?: string; @@ -70,8 +71,7 @@ export function prepareHeatmapData( options: Options, palette: string[], theme: GrafanaTheme2, - getFieldLinks?: (exemplars: DataFrame, field: Field) => (config: ValueLinkConfig) => Array>, - replaceVariables?: InterpolateFunction + replaceVariables: InterpolateFunction = (v) => v ): HeatmapData { if (!frames?.length) { return {}; @@ -81,11 +81,9 @@ export function prepareHeatmapData( const exemplars = annotations?.find((f) => f.name === 'exemplar'); - if (getFieldLinks) { - exemplars?.fields.forEach((field, index) => { - exemplars.fields[index].getLinks = getFieldLinks(exemplars, field); - }); - } + exemplars?.fields.forEach((field) => { + field.getLinks = getLinksSupplier(exemplars, field, field.state?.scopedVars ?? {}, replaceVariables); + }); if (options.calculate) { if (config.featureToggles.transformationsVariableSupport) { @@ -138,7 +136,7 @@ export function prepareHeatmapData( } // Everything past here assumes a field for each row in the heatmap (buckets) - if (!rowsHeatmap) { + if (rowsHeatmap == null) { if (frames.length > 1) { let allNamesNumeric = frames.every( (frame) => !Number.isNaN(parseSampleValue(frame.fields[1].state?.displayName!)) @@ -148,11 +146,10 @@ export function prepareHeatmapData( frames.sort(sortSeriesByLabel); } - rowsHeatmap = [ - outerJoinDataFrames({ - frames, - })!, - ][0]; + rowsHeatmap = outerJoinDataFrames({ + frames, + keepDisplayNames: true, + })!; } else { let frame = frames[0]; let numberFields = frame.fields.filter((field) => field.type === FieldType.number); @@ -171,18 +168,31 @@ export function prepareHeatmapData( } } - return getDenseHeatmapData( - rowsToCellsHeatmap({ - unit: options.yAxis?.unit, // used to format the ordinal lookup values - decimals: options.yAxis?.decimals, - ...options.rowsFrame, - frame: rowsHeatmap, - }), - exemplars, - options, - palette, - theme - ); + // config data links + rowsHeatmap.fields.forEach((field) => { + if ((field.config.links?.length ?? 0) === 0) { + return; + } + + // this expects that the tooltip is able to identify the field and rowIndex from a dense hovered index + field.getLinks = getLinksSupplier(rowsHeatmap!, field, field.state?.scopedVars ?? {}, replaceVariables); + }); + + return { + ...getDenseHeatmapData( + rowsToCellsHeatmap({ + unit: options.yAxis?.unit, // used to format the ordinal lookup values + decimals: options.yAxis?.decimals, + ...options.rowsFrame, + frame: rowsHeatmap, + }), + exemplars, + options, + palette, + theme + ), + series: rowsHeatmap, + }; } const getSparseHeatmapData = ( diff --git a/public/app/plugins/panel/heatmap/module.tsx b/public/app/plugins/panel/heatmap/module.tsx index 58684eee51..5ac97b441c 100644 --- a/public/app/plugins/panel/heatmap/module.tsx +++ b/public/app/plugins/panel/heatmap/module.tsx @@ -53,15 +53,7 @@ export const plugin = new PanelPlugin(HeatmapPanel) // NOTE: this feels like overkill/expensive just to assert if we have an ordinal y // can probably simplify without doing full dataprep const palette = quantizeScheme(opts.color, config.theme2); - const v = prepareHeatmapData( - context.data, - undefined, - opts, - palette, - config.theme2, - undefined, - context.replaceVariables - ); + const v = prepareHeatmapData(context.data, undefined, opts, palette, config.theme2); isOrdinalY = readHeatmapRowsCustomMeta(v.heatmap).yOrdinalDisplay != null; } catch {} } diff --git a/public/app/plugins/panel/heatmap/utils.ts b/public/app/plugins/panel/heatmap/utils.ts index 79747c7c5c..141a94541d 100644 --- a/public/app/plugins/panel/heatmap/utils.ts +++ b/public/app/plugins/panel/heatmap/utils.ts @@ -557,7 +557,8 @@ export function prepConfig(opts: PrepConfigOpts) { }); }, }, - exemplarFillColor + exemplarFillColor, + dataRef.current.yLayout ), theme, scaleKey: '', // facets' scales used (above) @@ -585,6 +586,10 @@ export function prepConfig(opts: PrepConfigOpts) { return hRect && seriesIdx === hRect.sidx ? hRect.didx : null; }, + focus: { + prox: 1e3, + dist: (u, seriesIdx) => (hRect?.sidx === seriesIdx ? 0 : Infinity), + }, points: { fill: 'rgba(255,255,255, 0.3)', bbox: (u, seriesIdx) => { @@ -744,7 +749,7 @@ export function heatmapPathsDense(opts: PathbuilderOpts) { }; } -export function heatmapPathsPoints(opts: PointsBuilderOpts, exemplarColor: string) { +export function heatmapPathsPoints(opts: PointsBuilderOpts, exemplarColor: string, yLayout?: HeatmapCellLayout) { return (u: uPlot, seriesIdx: number) => { uPlot.orient( u, @@ -772,6 +777,8 @@ export function heatmapPathsPoints(opts: PointsBuilderOpts, exemplarColor: strin let fillPaths = [points]; let fillPalette = [exemplarColor ?? 'rgba(255,0,255,0.7)']; + let yShift = yLayout === HeatmapCellLayout.le ? -0.5 : yLayout === HeatmapCellLayout.ge ? 0.5 : 0; + for (let i = 0; i < dataX.length; i++) { let yVal = dataY[i]!; @@ -782,10 +789,7 @@ export function heatmapPathsPoints(opts: PointsBuilderOpts, exemplarColor: strin let isSparseHeatmap = scaleY.distr === 3 && scaleY.log === 2; if (!isSparseHeatmap) { - yVal -= 0.5; // center vertically in bucket (when tiles are le) - // y-randomize vertically to distribute exemplars in same bucket at same time - let randSign = Math.round(Math.random()) * 2 - 1; - yVal += randSign * 0.5 * Math.random(); + yVal += yShift; } let x = valToPosX(dataX[i], scaleX, xDim, xOff); diff --git a/public/app/plugins/panel/timeseries/TimeSeriesTooltip.tsx b/public/app/plugins/panel/timeseries/TimeSeriesTooltip.tsx index d4484973dd..997d7e43c2 100644 --- a/public/app/plugins/panel/timeseries/TimeSeriesTooltip.tsx +++ b/public/app/plugins/panel/timeseries/TimeSeriesTooltip.tsx @@ -56,7 +56,7 @@ export const TimeSeriesTooltip = ({ seriesIdx, mode, sortOrder, - (field) => field.type === FieldType.number + (field) => field.type === FieldType.number || field.type === FieldType.enum ); let footer: ReactNode; From bc34874bbbc65cf1bb87fc635004775be5790c14 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Thu, 7 Mar 2024 07:25:11 +0100 Subject: [PATCH 0450/1406] Annotations: Add selector to new query button (#83240) add selector --- .../settings/annotations/AnnotationSettingsList.tsx | 9 ++++++++- .../AnnotationSettings/AnnotationSettingsList.tsx | 10 +++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/public/app/features/dashboard-scene/settings/annotations/AnnotationSettingsList.tsx b/public/app/features/dashboard-scene/settings/annotations/AnnotationSettingsList.tsx index 89e83dfba1..3ceb2393f2 100644 --- a/public/app/features/dashboard-scene/settings/annotations/AnnotationSettingsList.tsx +++ b/public/app/features/dashboard-scene/settings/annotations/AnnotationSettingsList.tsx @@ -119,7 +119,14 @@ export const AnnotationSettingsList = ({ annotations, onNew, onEdit, onMove, onD }} /> )} - {!showEmptyListCTA && New query} + {!showEmptyListCTA && ( + + New query + + )} ); }; diff --git a/public/app/features/dashboard/components/AnnotationSettings/AnnotationSettingsList.tsx b/public/app/features/dashboard/components/AnnotationSettings/AnnotationSettingsList.tsx index 21de6545af..10b5e49de3 100644 --- a/public/app/features/dashboard/components/AnnotationSettings/AnnotationSettingsList.tsx +++ b/public/app/features/dashboard/components/AnnotationSettings/AnnotationSettingsList.tsx @@ -2,6 +2,7 @@ import { css } from '@emotion/css'; import React, { useState } from 'react'; import { arrayUtils, AnnotationQuery } from '@grafana/data'; +import { selectors } from '@grafana/e2e-selectors'; import { getDataSourceSrv } from '@grafana/runtime'; import { Button, DeleteButton, IconButton, useStyles2, VerticalGroup } from '@grafana/ui'; import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA'; @@ -127,7 +128,14 @@ export const AnnotationSettingsList = ({ dashboard, onNew, onEdit }: Props) => { }} /> )} - {!showEmptyListCTA && New query} + {!showEmptyListCTA && ( + + New query + + )} ); }; From a722b2608afcc5768c2afaa6532d52119fce9135 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Thu, 7 Mar 2024 07:25:48 +0100 Subject: [PATCH 0451/1406] TimeZonePicker: Add e2e selector to change time zone settings button (#83248) add selector to change time zone settings --- packages/grafana-e2e-selectors/src/selectors/components.ts | 1 + .../DateTimePickers/TimeRangePicker/TimePickerFooter.tsx | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/grafana-e2e-selectors/src/selectors/components.ts b/packages/grafana-e2e-selectors/src/selectors/components.ts index 82c905e8c4..8dda593069 100644 --- a/packages/grafana-e2e-selectors/src/selectors/components.ts +++ b/packages/grafana-e2e-selectors/src/selectors/components.ts @@ -416,6 +416,7 @@ export const Components = { */ container: 'Time zone picker select container', containerV2: 'data-testid Time zone picker select container', + changeTimeSettingsButton: 'data-testid Time zone picker Change time settings button', }, WeekStartPicker: { /** diff --git a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimePickerFooter.tsx b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimePickerFooter.tsx index 67e14033bd..31c40a0fb0 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimePickerFooter.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimePickerFooter.tsx @@ -73,7 +73,12 @@ export const TimePickerFooter = (props: Props) => {
- From dbb55f291a79647f06827e6a6f9f51cd51c01506 Mon Sep 17 00:00:00 2001 From: Pepe Cano <825430+ppcano@users.noreply.github.com> Date: Thu, 7 Mar 2024 08:42:38 +0100 Subject: [PATCH 0452/1406] Alerting docs: update the supported export template functionality (#83816) --- .../export-alerting-resources/index.md | 33 +++++++++++++++---- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/docs/sources/alerting/set-up/provision-alerting-resources/export-alerting-resources/index.md b/docs/sources/alerting/set-up/provision-alerting-resources/export-alerting-resources/index.md index b0a415f133..5dfbb5ba4a 100644 --- a/docs/sources/alerting/set-up/provision-alerting-resources/export-alerting-resources/index.md +++ b/docs/sources/alerting/set-up/provision-alerting-resources/export-alerting-resources/index.md @@ -86,7 +86,7 @@ To export contact points from the Grafana UI, complete the following steps. ### Export templates -Grafana currently doesn't offer an Export UI for notification templates, unlike other Alerting resources presented in this documentation. +Grafana currently doesn't offer an Export UI or [Export endpoint](#export-api-endpoints) for notification templates, unlike other Alerting resources presented in this documentation. However, you can export it by manually copying the content template and title directly from the Grafana UI. @@ -129,14 +129,17 @@ To export mute timings from the Grafana UI, complete the following steps. ## HTTP Alerting API -You can use the [Alerting HTTP API][alerting_http_provisioning] to return existing alerting resources in JSON and import them to another Grafana instance using the same endpoint. For instance: +You can use the [Alerting HTTP API][alerting_http_provisioning] to return existing alerting resources in JSON and import them to another Grafana instance using the same endpoint. -| Resource | Method / URI | Summary | -| ----------- | ------------------------------------- | ------------------------ | -| Alert rules | GET /api/v1/provisioning/alert-rules | Get all alert rules. | -| Alert rules | POST /api/v1/provisioning/alert-rules | Create a new alert rule. | +| Resource | URI | Methods | +| -------------------------------------------------------------- | ----------------------------------- | ---------------- | +| [Alert rules][alerting_http_alertrules] | /api/v1/provisioning/alert-rules | GET,POST,PUT,DEL | +| [Contact points][alerting_http_contactpoints] | /api/v1/provisioning/contact-points | GET,POST,PUT,DEL | +| [Notification policy tree][alerting_http_notificationpolicies] | /api/v1/provisioning/policies | GET,PUT,DEL | +| [Mute timings][alerting_http_mutetimings] | /api/v1/provisioning/mute-timings | GET,POST,PUT,DEL | +| [Templates][alerting_http_templates] | /api/v1/provisioning/templates | GET,PUT,DEL | -However, note these Alerting endpoints return a JSON format that is not compatible for provisioning through configuration files or Terraform, except the endpoints listed below. +However, note the standard endpoints return a JSON format that is not compatible for provisioning through configuration files or Terraform, except the `/export` endpoints listed below. ### Export API endpoints @@ -157,6 +160,22 @@ These endpoints accept a `download` parameter to download a file containing the {{% docs/reference %}} + +[alerting_http_alertrules]: "/docs/grafana/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/http-api-provisioning#alert-rules" +[alerting_http_alertrules]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/set-up/provision-alerting-resources/http-api-provisioning#alert-rules" + +[alerting_http_contactpoints]: "/docs/grafana/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/http-api-provisioning#contact-points" +[alerting_http_contactpoints]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/set-up/provision-alerting-resources/http-api-provisioning#contact-points" + +[alerting_http_notificationpolicies]: "/docs/grafana/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/http-api-provisioning#notification-policies" +[alerting_http_notificationpolicies]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/set-up/provision-alerting-resources/http-api-provisioning#notification-policies" + +[alerting_http_mutetimings]: "/docs/grafana/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/http-api-provisioning#mute-timings" +[alerting_http_mutetimings]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/set-up/provision-alerting-resources/http-api-provisioning#mute-timings" + +[alerting_http_templates]: "/docs/grafana/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/http-api-provisioning#templates" +[alerting_http_templates]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/set-up/provision-alerting-resources/http-api-provisioning#templates" + [alerting_tf_provisioning]: "/docs/grafana/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/terraform-provisioning" [alerting_tf_provisioning]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/set-up/provision-alerting-resources/terraform-provisioning" From 7bc8b27c33f00727458d955ded2d2d1747c5b1c2 Mon Sep 17 00:00:00 2001 From: linoman <2051016+linoman@users.noreply.github.com> Date: Thu, 7 Mar 2024 01:54:53 -0600 Subject: [PATCH 0453/1406] Update README.md (#84011) --- devenv/docker/blocks/auth/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devenv/docker/blocks/auth/README.md b/devenv/docker/blocks/auth/README.md index ed2872704b..2046804fb9 100644 --- a/devenv/docker/blocks/auth/README.md +++ b/devenv/docker/blocks/auth/README.md @@ -9,7 +9,7 @@ Spin up a service with the following command from the base directory of this repository. ```bash -make devenv=oauth +make devenv=auth/oauth ``` This will add the `oauth/docker-compose` block to the `docker-compose` file used From 8e827afb8c65055d412196d05806a565de646b96 Mon Sep 17 00:00:00 2001 From: linoman <2051016+linoman@users.noreply.github.com> Date: Thu, 7 Mar 2024 01:56:48 -0600 Subject: [PATCH 0454/1406] Password Policy: Validate strong password upon update (#83959) * add drawer for auth settings * add StrongPasswordField component * Add style to different behaviours * update style for component * add componenet to ChangePasswordForm * pass the event handlers to the child component * add style for label container * expose strong password policy config option to front end * enforce password validation with config option --- packages/grafana-data/src/types/config.ts | 1 + pkg/api/dtos/frontend_settings.go | 3 +- pkg/api/frontendsettings.go | 27 ++-- .../ValidationLabels/ValidationLabels.tsx | 123 ++++++++++++++++++ .../features/profile/ChangePasswordForm.tsx | 24 +++- public/locales/en-US/grafana.json | 3 +- public/locales/pseudo-LOCALE/grafana.json | 3 +- 7 files changed, 166 insertions(+), 18 deletions(-) create mode 100644 public/app/core/components/ValidationLabels/ValidationLabels.tsx diff --git a/packages/grafana-data/src/types/config.ts b/packages/grafana-data/src/types/config.ts index d5a2c259ba..2a8b2ea205 100644 --- a/packages/grafana-data/src/types/config.ts +++ b/packages/grafana-data/src/types/config.ts @@ -264,4 +264,5 @@ export interface AuthSettings { GenericOAuthSkipOrgRoleSync?: boolean; disableLogin?: boolean; + basicAuthStrongPasswordPolicy?: boolean; } diff --git a/pkg/api/dtos/frontend_settings.go b/pkg/api/dtos/frontend_settings.go index b739c4e5ad..a6972407c4 100644 --- a/pkg/api/dtos/frontend_settings.go +++ b/pkg/api/dtos/frontend_settings.go @@ -30,7 +30,8 @@ type FrontendSettingsAuthDTO struct { // Deprecated: this is no longer used and will be removed in Grafana 11 OktaSkipOrgRoleSync bool `json:"OktaSkipOrgRoleSync"` - DisableLogin bool `json:"disableLogin"` + DisableLogin bool `json:"disableLogin"` + BasicAuthStrongPasswordPolicy bool `json:"basicAuthStrongPasswordPolicy"` } type FrontendSettingsBuildInfoDTO struct { diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index 1a12e90f6e..35c43cd250 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -322,19 +322,20 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro oauthProviders := hs.SocialService.GetOAuthInfoProviders() frontendSettings.Auth = dtos.FrontendSettingsAuthDTO{ - AuthProxyEnableLoginToken: hs.Cfg.AuthProxy.EnableLoginToken, - OAuthSkipOrgRoleUpdateSync: hs.Cfg.OAuthSkipOrgRoleUpdateSync, - SAMLSkipOrgRoleSync: hs.Cfg.SAMLSkipOrgRoleSync, - LDAPSkipOrgRoleSync: hs.Cfg.LDAPSkipOrgRoleSync, - JWTAuthSkipOrgRoleSync: hs.Cfg.JWTAuth.SkipOrgRoleSync, - GoogleSkipOrgRoleSync: parseSkipOrgRoleSyncEnabled(oauthProviders[social.GoogleProviderName]), - GrafanaComSkipOrgRoleSync: parseSkipOrgRoleSyncEnabled(oauthProviders[social.GrafanaComProviderName]), - GenericOAuthSkipOrgRoleSync: parseSkipOrgRoleSyncEnabled(oauthProviders[social.GenericOAuthProviderName]), - AzureADSkipOrgRoleSync: parseSkipOrgRoleSyncEnabled(oauthProviders[social.AzureADProviderName]), - GithubSkipOrgRoleSync: parseSkipOrgRoleSyncEnabled(oauthProviders[social.GitHubProviderName]), - GitLabSkipOrgRoleSync: parseSkipOrgRoleSyncEnabled(oauthProviders[social.GitlabProviderName]), - OktaSkipOrgRoleSync: parseSkipOrgRoleSyncEnabled(oauthProviders[social.OktaProviderName]), - DisableLogin: hs.Cfg.DisableLogin, + AuthProxyEnableLoginToken: hs.Cfg.AuthProxy.EnableLoginToken, + OAuthSkipOrgRoleUpdateSync: hs.Cfg.OAuthSkipOrgRoleUpdateSync, + SAMLSkipOrgRoleSync: hs.Cfg.SAMLSkipOrgRoleSync, + LDAPSkipOrgRoleSync: hs.Cfg.LDAPSkipOrgRoleSync, + JWTAuthSkipOrgRoleSync: hs.Cfg.JWTAuth.SkipOrgRoleSync, + GoogleSkipOrgRoleSync: parseSkipOrgRoleSyncEnabled(oauthProviders[social.GoogleProviderName]), + GrafanaComSkipOrgRoleSync: parseSkipOrgRoleSyncEnabled(oauthProviders[social.GrafanaComProviderName]), + GenericOAuthSkipOrgRoleSync: parseSkipOrgRoleSyncEnabled(oauthProviders[social.GenericOAuthProviderName]), + AzureADSkipOrgRoleSync: parseSkipOrgRoleSyncEnabled(oauthProviders[social.AzureADProviderName]), + GithubSkipOrgRoleSync: parseSkipOrgRoleSyncEnabled(oauthProviders[social.GitHubProviderName]), + GitLabSkipOrgRoleSync: parseSkipOrgRoleSyncEnabled(oauthProviders[social.GitlabProviderName]), + OktaSkipOrgRoleSync: parseSkipOrgRoleSyncEnabled(oauthProviders[social.OktaProviderName]), + DisableLogin: hs.Cfg.DisableLogin, + BasicAuthStrongPasswordPolicy: hs.Cfg.BasicAuthStrongPasswordPolicy, } if hs.pluginsCDNService != nil && hs.pluginsCDNService.IsEnabled() { diff --git a/public/app/core/components/ValidationLabels/ValidationLabels.tsx b/public/app/core/components/ValidationLabels/ValidationLabels.tsx new file mode 100644 index 0000000000..20f50b3670 --- /dev/null +++ b/public/app/core/components/ValidationLabels/ValidationLabels.tsx @@ -0,0 +1,123 @@ +import { css, cx } from '@emotion/css'; +import React from 'react'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { Box, Icon, Text, useStyles2 } from '@grafana/ui'; +import config from 'app/core/config'; +import { t } from 'app/core/internationalization'; + +interface StrongPasswordValidation { + message: string; + validation: (value: string) => boolean; +} + +export interface ValidationLabelsProps { + strongPasswordValidations: StrongPasswordValidation[]; + password: string; + pristine: boolean; +} + +export interface ValidationLabelProps { + strongPasswordValidation: StrongPasswordValidation; + password: string; + pristine: boolean; +} + +export const strongPasswordValidations: StrongPasswordValidation[] = [ + { + message: 'At least 12 characters', + validation: (value: string) => value.length >= 12, + }, + { + message: 'One uppercase letter', + validation: (value: string) => /[A-Z]+/.test(value), + }, + { + message: 'One lowercase letter', + validation: (value: string) => /[a-z]+/.test(value), + }, + { + message: 'One number', + validation: (value: string) => /[0-9]+/.test(value), + }, + { + message: 'One symbol', + validation: (value: string) => /[\W]/.test(value), + }, +]; + +export const strongPasswordValidationRegister = (value: string) => { + return ( + !config.auth.basicAuthStrongPasswordPolicy || + strongPasswordValidations.every((validation) => validation.validation(value)) || + t( + 'profile.change-password.strong-password-validation-register', + 'Password does not comply with the strong password policy' + ) + ); +}; + +export const ValidationLabels = ({ strongPasswordValidations, password, pristine }: ValidationLabelsProps) => { + return ( + + {strongPasswordValidations.map((validation) => ( + + ))} + + ); +}; + +export const ValidationLabel = ({ strongPasswordValidation, password, pristine }: ValidationLabelProps) => { + const styles = useStyles2(getStyles); + + const { basicAuthStrongPasswordPolicy } = config.auth; + if (!basicAuthStrongPasswordPolicy) { + return null; + } + + const { message, validation } = strongPasswordValidation; + const result = password.length > 0 && validation(password); + + const iconName = result || pristine ? 'check' : 'exclamation-triangle'; + const textColor = result ? 'secondary' : pristine ? 'primary' : 'error'; + + let iconClassName = undefined; + if (result) { + iconClassName = styles.icon.valid; + } else if (pristine) { + iconClassName = styles.icon.pending; + } else { + iconClassName = styles.icon.error; + } + + return ( + + + {message} + + ); +}; + +export const getStyles = (theme: GrafanaTheme2) => { + return { + icon: { + style: css({ + marginRight: theme.spacing(1), + }), + valid: css({ + color: theme.colors.success.text, + }), + pending: css({ + color: theme.colors.secondary.text, + }), + error: css({ + color: theme.colors.error.text, + }), + }, + }; +}; diff --git a/public/app/features/profile/ChangePasswordForm.tsx b/public/app/features/profile/ChangePasswordForm.tsx index 7f63b646dd..a6321d7327 100644 --- a/public/app/features/profile/ChangePasswordForm.tsx +++ b/public/app/features/profile/ChangePasswordForm.tsx @@ -1,7 +1,12 @@ import { css } from '@emotion/css'; -import React from 'react'; +import React, { useState } from 'react'; import { Button, Field, Form, HorizontalGroup, LinkButton } from '@grafana/ui'; +import { + ValidationLabels, + strongPasswordValidations, + strongPasswordValidationRegister, +} from 'app/core/components/ValidationLabels/ValidationLabels'; import config from 'app/core/config'; import { t, Trans } from 'app/core/internationalization'; import { UserDTO } from 'app/types'; @@ -17,6 +22,10 @@ export interface Props { } export const ChangePasswordForm = ({ user, onChangePassword, isSaving }: Props) => { + const [displayValidationLabels, setDisplayValidationLabels] = useState(false); + const [pristine, setPristine] = useState(true); + const [newPassword, setNewPassword] = useState(''); + const { disableLoginForm } = config; const authSource = user.authLabels?.length && user.authLabels[0]; @@ -69,9 +78,14 @@ export const ChangePasswordForm = ({ user, onChangePassword, isSaving }: Props) setDisplayValidationLabels(true)} + value={newPassword} {...register('newPassword', { + onBlur: () => setPristine(false), + onChange: (e) => setNewPassword(e.target.value), required: t('profile.change-password.new-password-required', 'New password is required'), validate: { + strongPasswordValidationRegister, confirm: (v) => v === getValues().confirmNew || t('profile.change-password.passwords-must-match', 'Passwords must match'), @@ -85,7 +99,13 @@ export const ChangePasswordForm = ({ user, onChangePassword, isSaving }: Props) })} /> - + {displayValidationLabels && ( + + )} Date: Thu, 7 Mar 2024 10:24:12 +0200 Subject: [PATCH 0455/1406] Scenes: Remove normal and library panels from layout or rows (#83969) * Remove normal/lib panels from layout or rows * refactor --- .../scene/DashboardScene.test.tsx | 70 ++++++++++++++++--- .../dashboard-scene/scene/DashboardScene.tsx | 43 ++++++++++++ .../scene/PanelMenuBehavior.tsx | 66 ++++++----------- .../scene/keyboardShortcuts.ts | 4 +- 4 files changed, 127 insertions(+), 56 deletions(-) diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx index fd93852044..bfad911078 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx @@ -261,7 +261,7 @@ describe('DashboardScene', () => { const gridItem = body.state.children[0] as SceneGridItem; expect(body.state.children.length).toBe(6); - expect(gridItem.state.body!.state.key).toBe('panel-5'); + expect(gridItem.state.body!.state.key).toBe('panel-7'); }); it('Should create and add a new row to the dashboard', () => { @@ -271,7 +271,7 @@ describe('DashboardScene', () => { const gridRow = body.state.children[0] as SceneGridRow; expect(body.state.children.length).toBe(4); - expect(gridRow.state.key).toBe('panel-5'); + expect(gridRow.state.key).toBe('panel-7'); expect(gridRow.state.children[0].state.key).toBe('griditem-1'); expect(gridRow.state.children[1].state.key).toBe('griditem-2'); }); @@ -405,7 +405,7 @@ describe('DashboardScene', () => { expect(buildGridItemForPanel).toHaveBeenCalledTimes(1); expect(body.state.children.length).toBe(6); - expect(gridItem.state.body!.state.key).toBe('panel-5'); + expect(gridItem.state.body!.state.key).toBe('panel-7'); expect(gridItem.state.y).toBe(0); expect(scene.state.hasCopiedPanel).toBe(false); }); @@ -433,8 +433,8 @@ describe('DashboardScene', () => { expect(buildGridItemForLibPanel).toHaveBeenCalledTimes(1); expect(body.state.children.length).toBe(6); - expect(libVizPanel.state.panelKey).toBe('panel-5'); - expect(libVizPanel.state.panel?.state.key).toBe('panel-5'); + expect(libVizPanel.state.panelKey).toBe('panel-7'); + expect(libVizPanel.state.panel?.state.key).toBe('panel-7'); expect(gridItem.state.y).toBe(0); expect(scene.state.hasCopiedPanel).toBe(false); }); @@ -446,9 +446,50 @@ describe('DashboardScene', () => { const gridItem = body.state.children[0] as SceneGridItem; expect(body.state.children.length).toBe(6); - expect(gridItem.state.body!.state.key).toBe('panel-5'); + expect(gridItem.state.body!.state.key).toBe('panel-7'); expect(gridItem.state.y).toBe(0); }); + + it('Should remove a panel', () => { + const vizPanel = ((scene.state.body as SceneGridLayout).state.children[0] as SceneGridItem).state.body; + scene.removePanel(vizPanel as VizPanel); + + const body = scene.state.body as SceneGridLayout; + expect(body.state.children.length).toBe(4); + }); + + it('Should remove a panel within a row', () => { + const vizPanel = ( + ((scene.state.body as SceneGridLayout).state.children[2] as SceneGridRow).state.children[0] as SceneGridItem + ).state.body; + scene.removePanel(vizPanel as VizPanel); + + const body = scene.state.body as SceneGridLayout; + const gridRow = body.state.children[2] as SceneGridRow; + expect(gridRow.state.children.length).toBe(1); + }); + + it('Should remove a library panel', () => { + const libraryPanel = ((scene.state.body as SceneGridLayout).state.children[4] as SceneGridItem).state.body; + const vizPanel = (libraryPanel as LibraryVizPanel).state.panel; + scene.removePanel(vizPanel as VizPanel); + + const body = scene.state.body as SceneGridLayout; + expect(body.state.children.length).toBe(4); + }); + + it('Should remove a library panel within a row', () => { + const libraryPanel = ( + ((scene.state.body as SceneGridLayout).state.children[2] as SceneGridRow).state.children[1] as SceneGridItem + ).state.body; + const vizPanel = (libraryPanel as LibraryVizPanel).state.panel; + + scene.removePanel(vizPanel as VizPanel); + + const body = scene.state.body as SceneGridLayout; + const gridRow = body.state.children[2] as SceneGridRow; + expect(gridRow.state.children.length).toBe(1); + }); }); }); @@ -639,6 +680,19 @@ function buildTestScene(overrides?: Partial) { pluginId: 'table', }), }), + new SceneGridItem({ + body: new LibraryVizPanel({ + uid: 'uid', + name: 'libraryPanel', + panelKey: 'panel-5', + title: 'Library Panel', + panel: new VizPanel({ + title: 'Library Panel', + key: 'panel-5', + pluginId: 'table', + }), + }), + }), ], }), new SceneGridItem({ @@ -653,11 +707,11 @@ function buildTestScene(overrides?: Partial) { body: new LibraryVizPanel({ uid: 'uid', name: 'libraryPanel', - panelKey: 'panel-4', + panelKey: 'panel-6', title: 'Library Panel', panel: new VizPanel({ title: 'Library Panel', - key: 'panel-4', + key: 'panel-6', pluginId: 'table', }), }), diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index c5ddb5ecfe..a08933bac6 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -560,6 +560,49 @@ export class DashboardScene extends SceneObjectBase { store.delete(LS_PANEL_COPY_KEY); } + public removePanel(panel: VizPanel) { + const panels: SceneObject[] = []; + const key = panel.parent instanceof LibraryVizPanel ? panel.parent.parent?.state.key : panel.parent?.state.key; + + if (!key) { + return; + } + + let row: SceneGridRow | undefined; + + try { + row = sceneGraph.getAncestor(panel, SceneGridRow); + } catch { + row = undefined; + } + + if (row) { + row.forEachChild((child: SceneObject) => { + if (child.state.key !== key) { + panels.push(child); + } + }); + + row.setState({ children: panels }); + + this.state.body.forceRender(); + + return; + } + + this.state.body.forEachChild((child: SceneObject) => { + if (child.state.key !== key) { + panels.push(child); + } + }); + + const layout = this.state.body; + + if (layout instanceof SceneGridLayout || layout instanceof SceneFlexLayout) { + layout.setState({ children: panels }); + } + } + public showModal(modal: SceneObject) { this.setState({ overlay: modal }); } diff --git a/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx b/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx index f5b823d5e9..84d7870b5e 100644 --- a/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx +++ b/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx @@ -8,16 +8,7 @@ import { urlUtil, } from '@grafana/data'; import { config, getPluginLinkExtensions, locationService } from '@grafana/runtime'; -import { - LocalValueVariable, - SceneFlexLayout, - SceneGridLayout, - SceneGridRow, - SceneObject, - VizPanel, - VizPanelMenu, - sceneGraph, -} from '@grafana/scenes'; +import { LocalValueVariable, SceneGridRow, VizPanel, VizPanelMenu, sceneGraph } from '@grafana/scenes'; import { DataQuery, OptionsWithLegend } from '@grafana/schema'; import appEvents from 'app/core/app_events'; import { t } from 'app/core/internationalization'; @@ -203,7 +194,7 @@ export function panelMenuBehavior(menu: VizPanelMenu) { iconClassName: 'trash-alt', onClick: () => { DashboardInteractions.panelMenuItemClicked('remove'); - removePanel(dashboard, panel, true); + onRemovePanel(dashboard, panel); }, shortcut: 'p r', }); @@ -377,7 +368,7 @@ function createExtensionContext(panel: VizPanel, dashboard: DashboardScene): Plu }; } -export function removePanel(dashboard: DashboardScene, panel: VizPanel, ask: boolean) { +export function onRemovePanel(dashboard: DashboardScene, panel: VizPanel) { const vizPanelData = sceneGraph.getData(panel); let panelHasAlert = false; @@ -385,40 +376,23 @@ export function removePanel(dashboard: DashboardScene, panel: VizPanel, ask: boo panelHasAlert = true; } - if (ask !== false) { - const text2 = - panelHasAlert && !config.unifiedAlertingEnabled - ? 'Panel includes an alert rule. removing the panel will also remove the alert rule' - : undefined; - const confirmText = panelHasAlert ? 'YES' : undefined; - - appEvents.publish( - new ShowConfirmModalEvent({ - title: 'Remove panel', - text: 'Are you sure you want to remove this panel?', - text2: text2, - icon: 'trash-alt', - confirmText: confirmText, - yesText: 'Remove', - onConfirm: () => removePanel(dashboard, panel, false), - }) - ); - - return; - } - - const panels: SceneObject[] = []; - dashboard.state.body.forEachChild((child: SceneObject) => { - if (child.state.key !== panel.parent?.state.key) { - panels.push(child); - } - }); - - const layout = dashboard.state.body; - - if (layout instanceof SceneGridLayout || SceneFlexLayout) { - layout.setState({ children: panels }); - } + const text2 = + panelHasAlert && !config.unifiedAlertingEnabled + ? 'Panel includes an alert rule. removing the panel will also remove the alert rule' + : undefined; + const confirmText = panelHasAlert ? 'YES' : undefined; + + appEvents.publish( + new ShowConfirmModalEvent({ + title: 'Remove panel', + text: 'Are you sure you want to remove this panel?', + text2: text2, + icon: 'trash-alt', + confirmText: confirmText, + yesText: 'Remove', + onConfirm: () => dashboard.removePanel(panel), + }) + ); } const onCreateAlert = async (panel: VizPanel) => { diff --git a/public/app/features/dashboard-scene/scene/keyboardShortcuts.ts b/public/app/features/dashboard-scene/scene/keyboardShortcuts.ts index 8462b64000..e1bb7aea75 100644 --- a/public/app/features/dashboard-scene/scene/keyboardShortcuts.ts +++ b/public/app/features/dashboard-scene/scene/keyboardShortcuts.ts @@ -8,7 +8,7 @@ import { getEditPanelUrl, getInspectUrl, getViewPanelUrl, tryGetExploreUrlForPan import { getPanelIdForVizPanel } from '../utils/utils'; import { DashboardScene } from './DashboardScene'; -import { removePanel, toggleVizPanelLegend } from './PanelMenuBehavior'; +import { onRemovePanel, toggleVizPanelLegend } from './PanelMenuBehavior'; export function setupKeyboardShortcuts(scene: DashboardScene) { const keybindings = new KeybindingSet(); @@ -120,7 +120,7 @@ export function setupKeyboardShortcuts(scene: DashboardScene) { keybindings.addBinding({ key: 'p r', onTrigger: withFocusedPanel(scene, (vizPanel: VizPanel) => { - removePanel(scene, vizPanel, true); + onRemovePanel(scene, vizPanel); }), }); From e3314f04e4813ae6a86e14a09c475e49a018840d Mon Sep 17 00:00:00 2001 From: Mihai Doarna Date: Thu, 7 Mar 2024 10:32:55 +0200 Subject: [PATCH 0456/1406] Auth: perform read locking on every exported func from social providers (#83960) perform read locking on every exported func from social providers --- pkg/login/social/connectors/azuread_oauth.go | 24 +++--- pkg/login/social/connectors/common.go | 10 ++- pkg/login/social/connectors/generic_oauth.go | 76 +++++++++---------- pkg/login/social/connectors/github_oauth.go | 51 ++++++------- pkg/login/social/connectors/gitlab_oauth.go | 23 +++--- pkg/login/social/connectors/google_oauth.go | 30 ++++---- .../social/connectors/google_oauth_test.go | 2 +- .../social/connectors/grafana_com_oauth.go | 11 +-- pkg/login/social/connectors/okta_oauth.go | 29 ++++--- pkg/login/social/connectors/social_base.go | 52 +++++++++++-- 10 files changed, 166 insertions(+), 142 deletions(-) diff --git a/pkg/login/social/connectors/azuread_oauth.go b/pkg/login/social/connectors/azuread_oauth.go index 05f23c706f..178cc9a837 100644 --- a/pkg/login/social/connectors/azuread_oauth.go +++ b/pkg/login/social/connectors/azuread_oauth.go @@ -98,6 +98,9 @@ func NewAzureADProvider(info *social.OAuthInfo, cfg *setting.Cfg, ssoSettings ss } func (s *SocialAzureAD) UserInfo(ctx context.Context, client *http.Client, token *oauth2.Token) (*social.BasicUserInfo, error) { + s.reloadMutex.RLock() + defer s.reloadMutex.RUnlock() + idToken := token.Extra("id_token") if idToken == nil { return nil, ErrIDTokenNotFound @@ -118,12 +121,10 @@ func (s *SocialAzureAD) UserInfo(ctx context.Context, client *http.Client, token return nil, ErrEmailNotFound } - info := s.GetOAuthInfo() - // setting the role, grafanaAdmin to empty to reflect that we are not syncronizing with the external provider var role roletype.RoleType var grafanaAdmin bool - if !info.SkipOrgRoleSync { + if !s.info.SkipOrgRoleSync { role, grafanaAdmin, err = s.extractRoleAndAdmin(claims) if err != nil { return nil, err @@ -150,11 +151,11 @@ func (s *SocialAzureAD) UserInfo(ctx context.Context, client *http.Client, token } var isGrafanaAdmin *bool = nil - if info.AllowAssignGrafanaAdmin { + if s.info.AllowAssignGrafanaAdmin { isGrafanaAdmin = &grafanaAdmin } - if info.AllowAssignGrafanaAdmin && info.SkipOrgRoleSync { + if s.info.AllowAssignGrafanaAdmin && s.info.SkipOrgRoleSync { s.log.Debug("AllowAssignGrafanaAdmin and skipOrgRoleSync are both set, Grafana Admin role will not be synced, consider setting one or the other") } @@ -290,10 +291,8 @@ func (claims *azureClaims) extractEmail() string { // extractRoleAndAdmin extracts the role from the claims and returns the role and whether the user is a Grafana admin. func (s *SocialAzureAD) extractRoleAndAdmin(claims *azureClaims) (org.RoleType, bool, error) { - info := s.GetOAuthInfo() - if len(claims.Roles) == 0 { - if info.RoleAttributeStrict { + if s.info.RoleAttributeStrict { return "", false, errRoleAttributeStrictViolation.Errorf("AzureAD OAuth: unset role") } return s.defaultRole(), false, nil @@ -311,7 +310,7 @@ func (s *SocialAzureAD) extractRoleAndAdmin(claims *azureClaims) (org.RoleType, } } - if info.RoleAttributeStrict { + if s.info.RoleAttributeStrict { return "", false, errRoleAttributeStrictViolation.Errorf("AzureAD OAuth: idP did not return a valid role %q", claims.Roles) } @@ -435,15 +434,16 @@ func (s *SocialAzureAD) groupsGraphAPIURL(claims *azureClaims, token *oauth2.Tok } func (s *SocialAzureAD) SupportBundleContent(bf *bytes.Buffer) error { - info := s.GetOAuthInfo() + s.reloadMutex.RLock() + defer s.reloadMutex.RUnlock() bf.WriteString("## AzureAD specific configuration\n\n") bf.WriteString("```ini\n") - bf.WriteString(fmt.Sprintf("allowed_groups = %v\n", info.AllowedGroups)) + bf.WriteString(fmt.Sprintf("allowed_groups = %v\n", s.info.AllowedGroups)) bf.WriteString(fmt.Sprintf("forceUseGraphAPI = %v\n", s.forceUseGraphAPI)) bf.WriteString("```\n\n") - return s.SocialBase.SupportBundleContent(bf) + return s.SocialBase.getBaseSupportBundleContent(bf) } func (s *SocialAzureAD) isAllowedTenant(tenantID string) bool { diff --git a/pkg/login/social/connectors/common.go b/pkg/login/social/connectors/common.go index 6dcc051a31..153e9eb998 100644 --- a/pkg/login/social/connectors/common.go +++ b/pkg/login/social/connectors/common.go @@ -47,15 +47,17 @@ type httpGetResponse struct { } func (s *SocialBase) IsEmailAllowed(email string) bool { - info := s.GetOAuthInfo() + s.reloadMutex.RLock() + defer s.reloadMutex.RUnlock() - return isEmailAllowed(email, info.AllowedDomains) + return isEmailAllowed(email, s.info.AllowedDomains) } func (s *SocialBase) IsSignupAllowed() bool { - info := s.GetOAuthInfo() + s.reloadMutex.RLock() + defer s.reloadMutex.RUnlock() - return info.AllowSignup + return s.info.AllowSignup } func isEmailAllowed(email string, allowedDomains []string) bool { diff --git a/pkg/login/social/connectors/generic_oauth.go b/pkg/login/social/connectors/generic_oauth.go index 885736e7ae..c76e64d8a0 100644 --- a/pkg/login/social/connectors/generic_oauth.go +++ b/pkg/login/social/connectors/generic_oauth.go @@ -139,14 +139,12 @@ func (s *SocialGenericOAuth) Reload(ctx context.Context, settings ssoModels.SSOS } // TODOD: remove this in the next PR and use the isGroupMember from social.go -func (s *SocialGenericOAuth) IsGroupMember(groups []string) bool { - info := s.GetOAuthInfo() - - if len(info.AllowedGroups) == 0 { +func (s *SocialGenericOAuth) isGroupMember(groups []string) bool { + if len(s.info.AllowedGroups) == 0 { return true } - for _, allowedGroup := range info.AllowedGroups { + for _, allowedGroup := range s.info.AllowedGroups { for _, group := range groups { if group == allowedGroup { return true @@ -157,12 +155,12 @@ func (s *SocialGenericOAuth) IsGroupMember(groups []string) bool { return false } -func (s *SocialGenericOAuth) IsTeamMember(ctx context.Context, client *http.Client) bool { +func (s *SocialGenericOAuth) isTeamMember(ctx context.Context, client *http.Client) bool { if len(s.teamIds) == 0 { return true } - teamMemberships, err := s.FetchTeamMemberships(ctx, client) + teamMemberships, err := s.fetchTeamMemberships(ctx, client) if err != nil { return false } @@ -178,12 +176,12 @@ func (s *SocialGenericOAuth) IsTeamMember(ctx context.Context, client *http.Clie return false } -func (s *SocialGenericOAuth) IsOrganizationMember(ctx context.Context, client *http.Client) bool { +func (s *SocialGenericOAuth) isOrganizationMember(ctx context.Context, client *http.Client) bool { if len(s.allowedOrganizations) == 0 { return true } - organizations, ok := s.FetchOrganizations(ctx, client) + organizations, ok := s.fetchOrganizations(ctx, client) if !ok { return false } @@ -219,6 +217,9 @@ func (info *UserInfoJson) String() string { } func (s *SocialGenericOAuth) UserInfo(ctx context.Context, client *http.Client, token *oauth2.Token) (*social.BasicUserInfo, error) { + s.reloadMutex.RLock() + defer s.reloadMutex.RUnlock() + s.log.Debug("Getting user info") toCheck := make([]*UserInfoJson, 0, 2) @@ -229,8 +230,6 @@ func (s *SocialGenericOAuth) UserInfo(ctx context.Context, client *http.Client, toCheck = append(toCheck, apiData) } - info := s.GetOAuthInfo() - userInfo := &social.BasicUserInfo{} for _, data := range toCheck { s.log.Debug("Processing external user info", "source", data.source, "data", data) @@ -254,13 +253,13 @@ func (s *SocialGenericOAuth) UserInfo(ctx context.Context, client *http.Client, } } - if userInfo.Role == "" && !info.SkipOrgRoleSync { + if userInfo.Role == "" && !s.info.SkipOrgRoleSync { role, grafanaAdmin, err := s.extractRoleAndAdminOptional(data.rawJSON, []string{}) if err != nil { s.log.Warn("Failed to extract role", "err", err) } else { userInfo.Role = role - if info.AllowAssignGrafanaAdmin { + if s.info.AllowAssignGrafanaAdmin { userInfo.IsGrafanaAdmin = &grafanaAdmin } } @@ -277,20 +276,20 @@ func (s *SocialGenericOAuth) UserInfo(ctx context.Context, client *http.Client, } } - if userInfo.Role == "" && !info.SkipOrgRoleSync { - if info.RoleAttributeStrict { + if userInfo.Role == "" && !s.info.SkipOrgRoleSync { + if s.info.RoleAttributeStrict { return nil, errRoleAttributeStrictViolation.Errorf("idP did not return a role attribute") } userInfo.Role = s.defaultRole() } - if info.AllowAssignGrafanaAdmin && info.SkipOrgRoleSync { + if s.info.AllowAssignGrafanaAdmin && s.info.SkipOrgRoleSync { s.log.Debug("AllowAssignGrafanaAdmin and skipOrgRoleSync are both set, Grafana Admin role will not be synced, consider setting one or the other") } if userInfo.Email == "" { var err error - userInfo.Email, err = s.FetchPrivateEmail(ctx, client) + userInfo.Email, err = s.fetchPrivateEmail(ctx, client) if err != nil { return nil, err } @@ -302,15 +301,15 @@ func (s *SocialGenericOAuth) UserInfo(ctx context.Context, client *http.Client, userInfo.Login = userInfo.Email } - if !s.IsTeamMember(ctx, client) { + if !s.isTeamMember(ctx, client) { return nil, errors.New("user not a member of one of the required teams") } - if !s.IsOrganizationMember(ctx, client) { + if !s.isOrganizationMember(ctx, client) { return nil, errors.New("user not a member of one of the required organizations") } - if !s.IsGroupMember(userInfo.Groups) { + if !s.isGroupMember(userInfo.Groups) { return nil, errMissingGroupMembership } @@ -352,17 +351,15 @@ func (s *SocialGenericOAuth) extractFromToken(token *oauth2.Token) *UserInfoJson } func (s *SocialGenericOAuth) extractFromAPI(ctx context.Context, client *http.Client) *UserInfoJson { - info := s.GetOAuthInfo() - s.log.Debug("Getting user info from API") - if info.ApiUrl == "" { + if s.info.ApiUrl == "" { s.log.Debug("No api url configured") return nil } - rawUserInfoResponse, err := s.httpGet(ctx, client, info.ApiUrl) + rawUserInfoResponse, err := s.httpGet(ctx, client, s.info.ApiUrl) if err != nil { - s.log.Debug("Error getting user info from API", "url", info.ApiUrl, "error", err) + s.log.Debug("Error getting user info from API", "url", s.info.ApiUrl, "error", err) return nil } @@ -469,7 +466,7 @@ func (s *SocialGenericOAuth) extractGroups(data *UserInfoJson) ([]string, error) return util.SearchJSONForStringSliceAttr(s.groupsAttributePath, data.rawJSON) } -func (s *SocialGenericOAuth) FetchPrivateEmail(ctx context.Context, client *http.Client) (string, error) { +func (s *SocialGenericOAuth) fetchPrivateEmail(ctx context.Context, client *http.Client) (string, error) { type Record struct { Email string `json:"email"` Primary bool `json:"primary"` @@ -478,11 +475,9 @@ func (s *SocialGenericOAuth) FetchPrivateEmail(ctx context.Context, client *http IsConfirmed bool `json:"is_confirmed"` } - info := s.GetOAuthInfo() - - response, err := s.httpGet(ctx, client, fmt.Sprintf(info.ApiUrl+"/emails")) + response, err := s.httpGet(ctx, client, fmt.Sprintf(s.info.ApiUrl+"/emails")) if err != nil { - s.log.Error("Error getting email address", "url", info.ApiUrl+"/emails", "error", err) + s.log.Error("Error getting email address", "url", s.info.ApiUrl+"/emails", "error", err) return "", fmt.Errorf("%v: %w", "Error getting email address", err) } @@ -518,7 +513,7 @@ func (s *SocialGenericOAuth) FetchPrivateEmail(ctx context.Context, client *http return email, nil } -func (s *SocialGenericOAuth) FetchTeamMemberships(ctx context.Context, client *http.Client) ([]string, error) { +func (s *SocialGenericOAuth) fetchTeamMemberships(ctx context.Context, client *http.Client) ([]string, error) { var err error var ids []string @@ -542,11 +537,9 @@ func (s *SocialGenericOAuth) fetchTeamMembershipsFromDeprecatedTeamsUrl(ctx cont Id int `json:"id"` } - info := s.GetOAuthInfo() - - response, err := s.httpGet(ctx, client, fmt.Sprintf(info.ApiUrl+"/teams")) + response, err := s.httpGet(ctx, client, fmt.Sprintf(s.info.ApiUrl+"/teams")) if err != nil { - s.log.Error("Error getting team memberships", "url", info.ApiUrl+"/teams", "error", err) + s.log.Error("Error getting team memberships", "url", s.info.ApiUrl+"/teams", "error", err) return []string{}, err } @@ -580,16 +573,14 @@ func (s *SocialGenericOAuth) fetchTeamMembershipsFromTeamsUrl(ctx context.Contex return util.SearchJSONForStringSliceAttr(s.teamIdsAttributePath, response.Body) } -func (s *SocialGenericOAuth) FetchOrganizations(ctx context.Context, client *http.Client) ([]string, bool) { +func (s *SocialGenericOAuth) fetchOrganizations(ctx context.Context, client *http.Client) ([]string, bool) { type Record struct { Login string `json:"login"` } - info := s.GetOAuthInfo() - - response, err := s.httpGet(ctx, client, fmt.Sprintf(info.ApiUrl+"/orgs")) + response, err := s.httpGet(ctx, client, fmt.Sprintf(s.info.ApiUrl+"/orgs")) if err != nil { - s.log.Error("Error getting organizations", "url", info.ApiUrl+"/orgs", "error", err) + s.log.Error("Error getting organizations", "url", s.info.ApiUrl+"/orgs", "error", err) return nil, false } @@ -612,6 +603,9 @@ func (s *SocialGenericOAuth) FetchOrganizations(ctx context.Context, client *htt } func (s *SocialGenericOAuth) SupportBundleContent(bf *bytes.Buffer) error { + s.reloadMutex.RLock() + defer s.reloadMutex.RUnlock() + bf.WriteString("## GenericOAuth specific configuration\n\n") bf.WriteString("```ini\n") bf.WriteString(fmt.Sprintf("name_attribute_path = %s\n", s.nameAttributePath)) @@ -622,5 +616,5 @@ func (s *SocialGenericOAuth) SupportBundleContent(bf *bytes.Buffer) error { bf.WriteString(fmt.Sprintf("allowed_organizations = %v\n", s.allowedOrganizations)) bf.WriteString("```\n\n") - return s.SocialBase.SupportBundleContent(bf) + return s.SocialBase.getBaseSupportBundleContent(bf) } diff --git a/pkg/login/social/connectors/github_oauth.go b/pkg/login/social/connectors/github_oauth.go index 46a82de22a..18e2175bcc 100644 --- a/pkg/login/social/connectors/github_oauth.go +++ b/pkg/login/social/connectors/github_oauth.go @@ -132,12 +132,12 @@ func (s *SocialGithub) Reload(ctx context.Context, settings ssoModels.SSOSetting return nil } -func (s *SocialGithub) IsTeamMember(ctx context.Context, client *http.Client) bool { +func (s *SocialGithub) isTeamMember(ctx context.Context, client *http.Client) bool { if len(s.teamIds) == 0 { return true } - teamMemberships, err := s.FetchTeamMemberships(ctx, client) + teamMemberships, err := s.fetchTeamMemberships(ctx, client) if err != nil { return false } @@ -153,13 +153,13 @@ func (s *SocialGithub) IsTeamMember(ctx context.Context, client *http.Client) bo return false } -func (s *SocialGithub) IsOrganizationMember(ctx context.Context, +func (s *SocialGithub) isOrganizationMember(ctx context.Context, client *http.Client, organizationsUrl string) bool { if len(s.allowedOrganizations) == 0 { return true } - organizations, err := s.FetchOrganizations(ctx, client, organizationsUrl) + organizations, err := s.fetchOrganizations(ctx, client, organizationsUrl) if err != nil { return false } @@ -175,16 +175,14 @@ func (s *SocialGithub) IsOrganizationMember(ctx context.Context, return false } -func (s *SocialGithub) FetchPrivateEmail(ctx context.Context, client *http.Client) (string, error) { +func (s *SocialGithub) fetchPrivateEmail(ctx context.Context, client *http.Client) (string, error) { type Record struct { Email string `json:"email"` Primary bool `json:"primary"` Verified bool `json:"verified"` } - info := s.GetOAuthInfo() - - response, err := s.httpGet(ctx, client, fmt.Sprintf(info.ApiUrl+"/emails")) + response, err := s.httpGet(ctx, client, fmt.Sprintf(s.info.ApiUrl+"/emails")) if err != nil { return "", fmt.Errorf("Error getting email address: %s", err) } @@ -206,10 +204,8 @@ func (s *SocialGithub) FetchPrivateEmail(ctx context.Context, client *http.Clien return email, nil } -func (s *SocialGithub) FetchTeamMemberships(ctx context.Context, client *http.Client) ([]GithubTeam, error) { - info := s.GetOAuthInfo() - - url := fmt.Sprintf(info.ApiUrl + "/teams?per_page=100") +func (s *SocialGithub) fetchTeamMemberships(ctx context.Context, client *http.Client) ([]GithubTeam, error) { + url := fmt.Sprintf(s.info.ApiUrl + "/teams?per_page=100") hasMore := true teams := make([]GithubTeam, 0) @@ -228,13 +224,13 @@ func (s *SocialGithub) FetchTeamMemberships(ctx context.Context, client *http.Cl teams = append(teams, records...) - url, hasMore = s.HasMoreRecords(response.Headers) + url, hasMore = s.hasMoreRecords(response.Headers) } return teams, nil } -func (s *SocialGithub) HasMoreRecords(headers http.Header) (string, bool) { +func (s *SocialGithub) hasMoreRecords(headers http.Header) (string, bool) { value, exists := headers["Link"] if !exists { return "", false @@ -252,7 +248,7 @@ func (s *SocialGithub) HasMoreRecords(headers http.Header) (string, bool) { return url, true } -func (s *SocialGithub) FetchOrganizations(ctx context.Context, client *http.Client, organizationsUrl string) ([]string, error) { +func (s *SocialGithub) fetchOrganizations(ctx context.Context, client *http.Client, organizationsUrl string) ([]string, error) { url := organizationsUrl hasMore := true logins := make([]string, 0) @@ -278,12 +274,15 @@ func (s *SocialGithub) FetchOrganizations(ctx context.Context, client *http.Clie logins = append(logins, record.Login) } - url, hasMore = s.HasMoreRecords(response.Headers) + url, hasMore = s.hasMoreRecords(response.Headers) } return logins, nil } func (s *SocialGithub) UserInfo(ctx context.Context, client *http.Client, token *oauth2.Token) (*social.BasicUserInfo, error) { + s.reloadMutex.RLock() + defer s.reloadMutex.RUnlock() + var data struct { Id int `json:"id"` Login string `json:"login"` @@ -291,9 +290,7 @@ func (s *SocialGithub) UserInfo(ctx context.Context, client *http.Client, token Name string `json:"name"` } - info := s.GetOAuthInfo() - - response, err := s.httpGet(ctx, client, info.ApiUrl) + response, err := s.httpGet(ctx, client, s.info.ApiUrl) if err != nil { return nil, fmt.Errorf("error getting user info: %s", err) } @@ -302,7 +299,7 @@ func (s *SocialGithub) UserInfo(ctx context.Context, client *http.Client, token return nil, fmt.Errorf("error unmarshalling user info: %s", err) } - teamMemberships, err := s.FetchTeamMemberships(ctx, client) + teamMemberships, err := s.fetchTeamMemberships(ctx, client) if err != nil { return nil, fmt.Errorf("error getting user teams: %s", err) } @@ -312,20 +309,20 @@ func (s *SocialGithub) UserInfo(ctx context.Context, client *http.Client, token var role roletype.RoleType var isGrafanaAdmin *bool = nil - if !info.SkipOrgRoleSync { + if !s.info.SkipOrgRoleSync { var grafanaAdmin bool role, grafanaAdmin, err = s.extractRoleAndAdmin(response.Body, teams) if err != nil { return nil, err } - if info.AllowAssignGrafanaAdmin { + if s.info.AllowAssignGrafanaAdmin { isGrafanaAdmin = &grafanaAdmin } } // we skip allowing assignment of GrafanaAdmin if skipOrgRoleSync is present - if info.AllowAssignGrafanaAdmin && info.SkipOrgRoleSync { + if s.info.AllowAssignGrafanaAdmin && s.info.SkipOrgRoleSync { s.log.Debug("AllowAssignGrafanaAdmin and skipOrgRoleSync are both set, Grafana Admin role will not be synced, consider setting one or the other") } @@ -342,20 +339,20 @@ func (s *SocialGithub) UserInfo(ctx context.Context, client *http.Client, token userInfo.Name = data.Name } - organizationsUrl := fmt.Sprintf(info.ApiUrl + "/orgs?per_page=100") + organizationsUrl := fmt.Sprintf(s.info.ApiUrl + "/orgs?per_page=100") - if !s.IsTeamMember(ctx, client) { + if !s.isTeamMember(ctx, client) { return nil, ErrMissingTeamMembership.Errorf("User is not a member of any of the allowed teams: %v", s.teamIds) } - if !s.IsOrganizationMember(ctx, client, organizationsUrl) { + if !s.isOrganizationMember(ctx, client, organizationsUrl) { return nil, ErrMissingOrganizationMembership.Errorf( "User is not a member of any of the allowed organizations: %v", s.allowedOrganizations) } if userInfo.Email == "" { - userInfo.Email, err = s.FetchPrivateEmail(ctx, client) + userInfo.Email, err = s.fetchPrivateEmail(ctx, client) if err != nil { return nil, err } diff --git a/pkg/login/social/connectors/gitlab_oauth.go b/pkg/login/social/connectors/gitlab_oauth.go index 258edde7d4..b588d2c2eb 100644 --- a/pkg/login/social/connectors/gitlab_oauth.go +++ b/pkg/login/social/connectors/gitlab_oauth.go @@ -116,9 +116,7 @@ func (s *SocialGitlab) getGroupsPage(ctx context.Context, client *http.Client, n FullPath string `json:"full_path"` } - info := s.GetOAuthInfo() - - groupURL, err := url.JoinPath(info.ApiUrl, "/groups") + groupURL, err := url.JoinPath(s.info.ApiUrl, "/groups") if err != nil { s.log.Error("Error joining GitLab API URL", "err", err) return nil, nil @@ -179,7 +177,8 @@ func (s *SocialGitlab) getGroupsPage(ctx context.Context, client *http.Client, n } func (s *SocialGitlab) UserInfo(ctx context.Context, client *http.Client, token *oauth2.Token) (*social.BasicUserInfo, error) { - info := s.GetOAuthInfo() + s.reloadMutex.RLock() + defer s.reloadMutex.RUnlock() data, err := s.extractFromToken(ctx, client, token) if err != nil { @@ -209,7 +208,7 @@ func (s *SocialGitlab) UserInfo(ctx context.Context, client *http.Client, token return nil, errMissingGroupMembership } - if info.AllowAssignGrafanaAdmin && info.SkipOrgRoleSync { + if s.info.AllowAssignGrafanaAdmin && s.info.SkipOrgRoleSync { s.log.Debug("AllowAssignGrafanaAdmin and skipOrgRoleSync are both set, Grafana Admin role will not be synced, consider setting one or the other") } @@ -217,10 +216,8 @@ func (s *SocialGitlab) UserInfo(ctx context.Context, client *http.Client, token } func (s *SocialGitlab) extractFromAPI(ctx context.Context, client *http.Client, token *oauth2.Token) (*userData, error) { - info := s.GetOAuthInfo() - apiResp := &apiData{} - response, err := s.httpGet(ctx, client, info.ApiUrl+"/user") + response, err := s.httpGet(ctx, client, s.info.ApiUrl+"/user") if err != nil { return nil, fmt.Errorf("Error getting user info: %w", err) } @@ -246,14 +243,14 @@ func (s *SocialGitlab) extractFromAPI(ctx context.Context, client *http.Client, Groups: s.getGroups(ctx, client), } - if !info.SkipOrgRoleSync { + if !s.info.SkipOrgRoleSync { var grafanaAdmin bool role, grafanaAdmin, err := s.extractRoleAndAdmin(response.Body, idData.Groups) if err != nil { return nil, err } - if info.AllowAssignGrafanaAdmin { + if s.info.AllowAssignGrafanaAdmin { idData.IsGrafanaAdmin = &grafanaAdmin } @@ -270,8 +267,6 @@ func (s *SocialGitlab) extractFromAPI(ctx context.Context, client *http.Client, func (s *SocialGitlab) extractFromToken(ctx context.Context, client *http.Client, token *oauth2.Token) (*userData, error) { s.log.Debug("Extracting user info from OAuth token") - info := s.GetOAuthInfo() - idToken := token.Extra("id_token") if idToken == nil { s.log.Debug("No id_token found, defaulting to API access", "token", token) @@ -305,13 +300,13 @@ func (s *SocialGitlab) extractFromToken(ctx context.Context, client *http.Client data.Groups = userInfo.Groups } - if !info.SkipOrgRoleSync { + if !s.info.SkipOrgRoleSync { role, grafanaAdmin, errRole := s.extractRoleAndAdmin(rawJSON, data.Groups) if errRole != nil { return nil, errRole } - if info.AllowAssignGrafanaAdmin { + if s.info.AllowAssignGrafanaAdmin { data.IsGrafanaAdmin = &grafanaAdmin } diff --git a/pkg/login/social/connectors/google_oauth.go b/pkg/login/social/connectors/google_oauth.go index dabf0f25f5..0bda73cd62 100644 --- a/pkg/login/social/connectors/google_oauth.go +++ b/pkg/login/social/connectors/google_oauth.go @@ -102,7 +102,8 @@ func (s *SocialGoogle) Reload(ctx context.Context, settings ssoModels.SSOSetting } func (s *SocialGoogle) UserInfo(ctx context.Context, client *http.Client, token *oauth2.Token) (*social.BasicUserInfo, error) { - info := s.GetOAuthInfo() + s.reloadMutex.RLock() + defer s.reloadMutex.RUnlock() data, errToken := s.extractFromToken(ctx, client, token) if errToken != nil { @@ -125,7 +126,7 @@ func (s *SocialGoogle) UserInfo(ctx context.Context, client *http.Client, token return nil, fmt.Errorf("user email is not verified") } - if err := s.isHDAllowed(data.HD, info); err != nil { + if err := s.isHDAllowed(data.HD); err != nil { return nil, err } @@ -148,13 +149,13 @@ func (s *SocialGoogle) UserInfo(ctx context.Context, client *http.Client, token Groups: groups, } - if !info.SkipOrgRoleSync { + if !s.info.SkipOrgRoleSync { role, grafanaAdmin, errRole := s.extractRoleAndAdmin(data.rawJSON, groups) if errRole != nil { return nil, errRole } - if info.AllowAssignGrafanaAdmin { + if s.info.AllowAssignGrafanaAdmin { userInfo.IsGrafanaAdmin = &grafanaAdmin } @@ -175,11 +176,9 @@ type googleAPIData struct { } func (s *SocialGoogle) extractFromAPI(ctx context.Context, client *http.Client) (*googleUserData, error) { - info := s.GetOAuthInfo() - - if strings.HasPrefix(info.ApiUrl, legacyAPIURL) { + if strings.HasPrefix(s.info.ApiUrl, legacyAPIURL) { data := googleAPIData{} - response, err := s.httpGet(ctx, client, info.ApiUrl) + response, err := s.httpGet(ctx, client, s.info.ApiUrl) if err != nil { return nil, fmt.Errorf("error retrieving legacy user info: %s", err) } @@ -199,7 +198,7 @@ func (s *SocialGoogle) extractFromAPI(ctx context.Context, client *http.Client) } data := googleUserData{} - response, err := s.httpGet(ctx, client, info.ApiUrl) + response, err := s.httpGet(ctx, client, s.info.ApiUrl) if err != nil { return nil, fmt.Errorf("error getting user info: %s", err) } @@ -212,12 +211,13 @@ func (s *SocialGoogle) extractFromAPI(ctx context.Context, client *http.Client) } func (s *SocialGoogle) AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string { - info := s.GetOAuthInfo() + s.reloadMutex.RLock() + defer s.reloadMutex.RUnlock() - if info.UseRefreshToken { + if s.info.UseRefreshToken { opts = append(opts, oauth2.AccessTypeOffline, oauth2.ApprovalForce) } - return s.SocialBase.AuthCodeURL(state, opts...) + return s.SocialBase.Config.AuthCodeURL(state, opts...) } func (s *SocialGoogle) extractFromToken(ctx context.Context, client *http.Client, token *oauth2.Token) (*googleUserData, error) { @@ -307,16 +307,16 @@ func (s *SocialGoogle) getGroupsPage(ctx context.Context, client *http.Client, u return &data, nil } -func (s *SocialGoogle) isHDAllowed(hd string, info *social.OAuthInfo) error { +func (s *SocialGoogle) isHDAllowed(hd string) error { if s.validateHD { return nil } - if len(info.AllowedDomains) == 0 { + if len(s.info.AllowedDomains) == 0 { return nil } - for _, allowedDomain := range info.AllowedDomains { + for _, allowedDomain := range s.info.AllowedDomains { if hd == allowedDomain { return nil } diff --git a/pkg/login/social/connectors/google_oauth_test.go b/pkg/login/social/connectors/google_oauth_test.go index d822d4d7b8..64b40f5913 100644 --- a/pkg/login/social/connectors/google_oauth_test.go +++ b/pkg/login/social/connectors/google_oauth_test.go @@ -931,7 +931,7 @@ func TestIsHDAllowed(t *testing.T) { info.AllowedDomains = tc.allowedDomains s := NewGoogleProvider(info, &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures()) s.validateHD = tc.validateHD - err := s.isHDAllowed(tc.email, info) + err := s.isHDAllowed(tc.email) if tc.expectedErrorMessage != "" { require.Error(t, err) diff --git a/pkg/login/social/connectors/grafana_com_oauth.go b/pkg/login/social/connectors/grafana_com_oauth.go index 694400aea4..c68ecb2d8e 100644 --- a/pkg/login/social/connectors/grafana_com_oauth.go +++ b/pkg/login/social/connectors/grafana_com_oauth.go @@ -99,7 +99,7 @@ func (s *SocialGrafanaCom) IsEmailAllowed(email string) bool { return true } -func (s *SocialGrafanaCom) IsOrganizationMember(organizations []OrgRecord) bool { +func (s *SocialGrafanaCom) isOrganizationMember(organizations []OrgRecord) bool { if len(s.allowedOrganizations) == 0 { return true } @@ -117,6 +117,9 @@ func (s *SocialGrafanaCom) IsOrganizationMember(organizations []OrgRecord) bool // UserInfo is used for login credentials for the user func (s *SocialGrafanaCom) UserInfo(ctx context.Context, client *http.Client, _ *oauth2.Token) (*social.BasicUserInfo, error) { + s.reloadMutex.RLock() + defer s.reloadMutex.RUnlock() + var data struct { Id int `json:"id"` Name string `json:"name"` @@ -126,8 +129,6 @@ func (s *SocialGrafanaCom) UserInfo(ctx context.Context, client *http.Client, _ Orgs []OrgRecord `json:"orgs"` } - info := s.GetOAuthInfo() - response, err := s.httpGet(ctx, client, s.url+"/api/oauth2/user") if err != nil { @@ -141,7 +142,7 @@ func (s *SocialGrafanaCom) UserInfo(ctx context.Context, client *http.Client, _ // on login we do not want to display the role from the external provider var role roletype.RoleType - if !info.SkipOrgRoleSync { + if !s.info.SkipOrgRoleSync { role = org.RoleType(data.Role) } userInfo := &social.BasicUserInfo{ @@ -152,7 +153,7 @@ func (s *SocialGrafanaCom) UserInfo(ctx context.Context, client *http.Client, _ Role: role, } - if !s.IsOrganizationMember(data.Orgs) { + if !s.isOrganizationMember(data.Orgs) { return nil, ErrMissingOrganizationMembership.Errorf( "User is not a member of any of the allowed organizations: %v. Returned Organizations: %v", s.allowedOrganizations, data.Orgs) diff --git a/pkg/login/social/connectors/okta_oauth.go b/pkg/login/social/connectors/okta_oauth.go index 6448b26b74..d548827c83 100644 --- a/pkg/login/social/connectors/okta_oauth.go +++ b/pkg/login/social/connectors/okta_oauth.go @@ -105,7 +105,8 @@ func (claims *OktaClaims) extractEmail() string { } func (s *SocialOkta) UserInfo(ctx context.Context, client *http.Client, token *oauth2.Token) (*social.BasicUserInfo, error) { - info := s.GetOAuthInfo() + s.reloadMutex.RLock() + defer s.reloadMutex.RUnlock() idToken := token.Extra("id_token") if idToken == nil { @@ -133,25 +134,25 @@ func (s *SocialOkta) UserInfo(ctx context.Context, client *http.Client, token *o return nil, err } - groups := s.GetGroups(&data) - if !s.IsGroupMember(groups) { + groups := s.getGroups(&data) + if !s.isGroupMember(groups) { return nil, errMissingGroupMembership } var role roletype.RoleType var isGrafanaAdmin *bool - if !info.SkipOrgRoleSync { + if !s.info.SkipOrgRoleSync { var grafanaAdmin bool role, grafanaAdmin, err = s.extractRoleAndAdmin(data.rawJSON, groups) if err != nil { return nil, err } - if info.AllowAssignGrafanaAdmin { + if s.info.AllowAssignGrafanaAdmin { isGrafanaAdmin = &grafanaAdmin } } - if info.AllowAssignGrafanaAdmin && info.SkipOrgRoleSync { + if s.info.AllowAssignGrafanaAdmin && s.info.SkipOrgRoleSync { s.log.Debug("AllowAssignGrafanaAdmin and skipOrgRoleSync are both set, Grafana Admin role will not be synced, consider setting one or the other") } @@ -167,11 +168,9 @@ func (s *SocialOkta) UserInfo(ctx context.Context, client *http.Client, token *o } func (s *SocialOkta) extractAPI(ctx context.Context, data *OktaUserInfoJson, client *http.Client) error { - info := s.GetOAuthInfo() - - rawUserInfoResponse, err := s.httpGet(ctx, client, info.ApiUrl) + rawUserInfoResponse, err := s.httpGet(ctx, client, s.info.ApiUrl) if err != nil { - s.log.Debug("Error getting user info response", "url", info.ApiUrl, "error", err) + s.log.Debug("Error getting user info response", "url", s.info.ApiUrl, "error", err) return fmt.Errorf("error getting user info response: %w", err) } data.rawJSON = rawUserInfoResponse.Body @@ -187,7 +186,7 @@ func (s *SocialOkta) extractAPI(ctx context.Context, data *OktaUserInfoJson, cli return nil } -func (s *SocialOkta) GetGroups(data *OktaUserInfoJson) []string { +func (s *SocialOkta) getGroups(data *OktaUserInfoJson) []string { groups := make([]string, 0) if len(data.Groups) > 0 { groups = data.Groups @@ -196,14 +195,12 @@ func (s *SocialOkta) GetGroups(data *OktaUserInfoJson) []string { } // TODO: remove this in a separate PR and use the isGroupMember from the social.go -func (s *SocialOkta) IsGroupMember(groups []string) bool { - info := s.GetOAuthInfo() - - if len(info.AllowedGroups) == 0 { +func (s *SocialOkta) isGroupMember(groups []string) bool { + if len(s.info.AllowedGroups) == 0 { return true } - for _, allowedGroup := range info.AllowedGroups { + for _, allowedGroup := range s.info.AllowedGroups { for _, group := range groups { if group == allowedGroup { return true diff --git a/pkg/login/social/connectors/social_base.go b/pkg/login/social/connectors/social_base.go index 577bdc41e5..6e53c13e76 100644 --- a/pkg/login/social/connectors/social_base.go +++ b/pkg/login/social/connectors/social_base.go @@ -3,10 +3,12 @@ package connectors import ( "bytes" "compress/zlib" + "context" "encoding/base64" "encoding/json" "fmt" "io" + "net/http" "regexp" "strings" "sync" @@ -60,6 +62,48 @@ type groupStruct struct { } func (s *SocialBase) SupportBundleContent(bf *bytes.Buffer) error { + s.reloadMutex.RLock() + defer s.reloadMutex.RUnlock() + + return s.getBaseSupportBundleContent(bf) +} + +func (s *SocialBase) GetOAuthInfo() *social.OAuthInfo { + s.reloadMutex.RLock() + defer s.reloadMutex.RUnlock() + + return s.info +} + +func (s *SocialBase) AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string { + s.reloadMutex.RLock() + defer s.reloadMutex.RUnlock() + + return s.Config.AuthCodeURL(state, opts...) +} + +func (s *SocialBase) Exchange(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) { + s.reloadMutex.RLock() + defer s.reloadMutex.RUnlock() + + return s.Config.Exchange(ctx, code, opts...) +} + +func (s *SocialBase) Client(ctx context.Context, t *oauth2.Token) *http.Client { + s.reloadMutex.RLock() + defer s.reloadMutex.RUnlock() + + return s.Config.Client(ctx, t) +} + +func (s *SocialBase) TokenSource(ctx context.Context, t *oauth2.Token) oauth2.TokenSource { + s.reloadMutex.RLock() + defer s.reloadMutex.RUnlock() + + return s.Config.TokenSource(ctx, t) +} + +func (s *SocialBase) getBaseSupportBundleContent(bf *bytes.Buffer) error { bf.WriteString("## Client configuration\n\n") bf.WriteString("```ini\n") bf.WriteString(fmt.Sprintf("allow_assign_grafana_admin = %v\n", s.info.AllowAssignGrafanaAdmin)) @@ -77,14 +121,8 @@ func (s *SocialBase) SupportBundleContent(bf *bytes.Buffer) error { bf.WriteString(fmt.Sprintf("redirect_url = %v\n", s.Config.RedirectURL)) bf.WriteString(fmt.Sprintf("scopes = %v\n", s.Config.Scopes)) bf.WriteString("```\n\n") - return nil -} - -func (s *SocialBase) GetOAuthInfo() *social.OAuthInfo { - s.reloadMutex.RLock() - defer s.reloadMutex.RUnlock() - return s.info + return nil } func (s *SocialBase) extractRoleAndAdminOptional(rawJSON []byte, groups []string) (org.RoleType, bool, error) { From 5a727a0b41b72c8d5c3ed3c5938a74558287cc9e Mon Sep 17 00:00:00 2001 From: Javier Ruiz Date: Thu, 7 Mar 2024 09:40:43 +0100 Subject: [PATCH 0457/1406] [Service map] Send name and namespace separately when going to traces explore (#83840) Send name and namespace separately when going to traces explroe --- .../datasource/tempo/datasource.test.ts | 50 +++++++++++++++++-- .../plugins/datasource/tempo/datasource.ts | 28 +++++++++-- 2 files changed, 71 insertions(+), 7 deletions(-) diff --git a/public/app/plugins/datasource/tempo/datasource.test.ts b/public/app/plugins/datasource/tempo/datasource.test.ts index de5416de26..f8a7fc5782 100644 --- a/public/app/plugins/datasource/tempo/datasource.test.ts +++ b/public/app/plugins/datasource/tempo/datasource.test.ts @@ -953,12 +953,20 @@ describe('Tempo service graph view', () => { queryType: 'traceqlSearch', refId: 'A', filters: [ + { + id: 'service-namespace', + operator: '=', + scope: 'resource', + tag: 'service.namespace', + value: '${__data.fields.targetNamespace}', + valueType: 'string', + }, { id: 'service-name', operator: '=', scope: 'resource', tag: 'service.name', - value: '${__data.fields.target}', + value: '${__data.fields.targetName}', valueType: 'string', }, ], @@ -1033,8 +1041,34 @@ describe('Tempo service graph view', () => { ]); }); - it('should make tempo link correctly', () => { - const tempoLink = makeTempoLink('Tempo', '', '"${__data.fields[0]}"', 'gdev-tempo'); + it('should make tempo link correctly without namespace', () => { + const tempoLink = makeTempoLink('Tempo', undefined, '', '"${__data.fields[0]}"', 'gdev-tempo'); + expect(tempoLink).toEqual({ + url: '', + title: 'Tempo', + internal: { + query: { + queryType: 'traceqlSearch', + refId: 'A', + filters: [ + { + id: 'span-name', + operator: '=', + scope: 'span', + tag: 'name', + value: '"${__data.fields[0]}"', + valueType: 'string', + }, + ], + }, + datasourceUid: 'gdev-tempo', + datasourceName: 'Tempo', + }, + }); + }); + + it('should make tempo link correctly with namespace', () => { + const tempoLink = makeTempoLink('Tempo', '"${__data.fields.subtitle}"', '', '"${__data.fields[0]}"', 'gdev-tempo'); expect(tempoLink).toEqual({ url: '', title: 'Tempo', @@ -1043,6 +1077,14 @@ describe('Tempo service graph view', () => { queryType: 'traceqlSearch', refId: 'A', filters: [ + { + id: 'service-namespace', + operator: '=', + scope: 'resource', + tag: 'service.namespace', + value: '"${__data.fields.subtitle}"', + valueType: 'string', + }, { id: 'span-name', operator: '=', @@ -1457,7 +1499,7 @@ const serviceGraphLinks = [ operator: '=', scope: 'resource', tag: 'service.name', - value: '${__data.fields[0]}', + value: '${__data.fields.id}', valueType: 'string', }, ], diff --git a/public/app/plugins/datasource/tempo/datasource.ts b/public/app/plugins/datasource/tempo/datasource.ts index 96fcb05b88..503f4db789 100644 --- a/public/app/plugins/datasource/tempo/datasource.ts +++ b/public/app/plugins/datasource/tempo/datasource.ts @@ -1128,13 +1128,35 @@ export function getFieldConfig( datasourceUid, false ), - makeTempoLink('View traces', `\${${tempoField}}`, '', tempoDatasourceUid), + makeTempoLink( + 'View traces', + namespaceFields !== undefined ? `\${${namespaceFields.targetNamespace}}` : '', + `\${${targetField}}`, + '', + tempoDatasourceUid + ), ], }; } -export function makeTempoLink(title: string, serviceName: string, spanName: string, datasourceUid: string) { +export function makeTempoLink( + title: string, + serviceNamespace: string | undefined, + serviceName: string, + spanName: string, + datasourceUid: string +) { let query: TempoQuery = { refId: 'A', queryType: 'traceqlSearch', filters: [] }; + if (serviceNamespace !== undefined && serviceNamespace !== '') { + query.filters.push({ + id: 'service-namespace', + scope: TraceqlSearchScope.Resource, + tag: 'service.namespace', + value: serviceNamespace, + operator: '=', + valueType: 'string', + }); + } if (serviceName !== '') { query.filters.push({ id: 'service-name', @@ -1338,7 +1360,7 @@ function getServiceGraphView( return 'Tempo'; }), config: { - links: [makeTempoLink('Tempo', '', `\${__data.fields[0]}`, tempoDatasourceUid)], + links: [makeTempoLink('Tempo', undefined, '', `\${__data.fields[0]}`, tempoDatasourceUid)], }, }); } From b8d8662bd9406b98220c420e37050f1a7e70a666 Mon Sep 17 00:00:00 2001 From: Sofia Papagiannaki <1632407+papagian@users.noreply.github.com> Date: Thu, 7 Mar 2024 12:07:35 +0200 Subject: [PATCH 0458/1406] Swagger: Re-generate the enterprise specification if enterprise is cloned (#81730) * Swagger: Re-generate the enterprise specification if enterprise is cloned successfully * API change to trigger the swagger CI step execution * Swagger: Silence logs --- .drone.yml | 14 +++++++++----- Makefile | 8 ++++---- pkg/api/folder.go | 2 +- public/api-merged.json | 2 +- public/openapi3.json | 2 +- scripts/drone/pipelines/swagger_gen.star | 4 ++-- 6 files changed, 18 insertions(+), 14 deletions(-) diff --git a/.drone.yml b/.drone.yml index 6af8a77378..9e247315b4 100644 --- a/.drone.yml +++ b/.drone.yml @@ -1234,11 +1234,15 @@ steps: | jq .head.repo.fork) - if [ "$is_fork" != false ]; then return 1; fi - git clone "https://$${GITHUB_TOKEN}@github.com/grafana/grafana-enterprise.git" - grafana-enterprise - - cd grafana-enterprise + ../grafana-enterprise + - cd ../grafana-enterprise - if git checkout ${DRONE_SOURCE_BRANCH}; then echo "checked out ${DRONE_SOURCE_BRANCH}"; - elif git checkout main; then echo "git checkout main"; else git checkout main; - fi + elif git checkout ${DRONE_TARGET_BRANCH}; then echo "git checkout ${DRONE_TARGET_BRANCH}"; + else git checkout main; fi + - cd ../ + - ln -s src grafana + - cd ./grafana-enterprise + - ./build.sh environment: GITHUB_TOKEN: from_secret: github_token @@ -4920,6 +4924,6 @@ kind: secret name: gcr_credentials --- kind: signature -hmac: 2f4a5620d00189804c2facf65fa2a17b75883cf330cd32e5612a2f36d3712847 +hmac: 3fa4360bd3c21fbc95bd3c2b63130541f9ea7c0aa56eb95dfe49c4f579aac4bb ... diff --git a/Makefile b/Makefile index b39cf4b562..896493bc40 100644 --- a/Makefile +++ b/Makefile @@ -45,12 +45,12 @@ $(NGALERT_SPEC_TARGET): $(MERGED_SPEC_TARGET): swagger-oss-gen swagger-enterprise-gen $(NGALERT_SPEC_TARGET) $(SWAGGER) ## Merge generated and ngalert API specs # known conflicts DsPermissionType, AddApiKeyCommand, Json, Duration (identical models referenced by both specs) - $(SWAGGER) mixin $(SPEC_TARGET) $(ENTERPRISE_SPEC_TARGET) $(NGALERT_SPEC_TARGET) --ignore-conflicts -o $(MERGED_SPEC_TARGET) + $(SWAGGER) mixin -q $(SPEC_TARGET) $(ENTERPRISE_SPEC_TARGET) $(NGALERT_SPEC_TARGET) --ignore-conflicts -o $(MERGED_SPEC_TARGET) swagger-oss-gen: $(SWAGGER) ## Generate API Swagger specification @echo "re-generating swagger for OSS" rm -f $(SPEC_TARGET) - SWAGGER_GENERATE_EXTENSION=false $(SWAGGER) generate spec -m -w pkg/server -o $(SPEC_TARGET) \ + SWAGGER_GENERATE_EXTENSION=false $(SWAGGER) generate spec -q -m -w pkg/server -o $(SPEC_TARGET) \ -x "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" \ -x "github.com/prometheus/alertmanager" \ -i pkg/api/swagger_tags.json \ @@ -66,7 +66,7 @@ else swagger-enterprise-gen: $(SWAGGER) ## Generate API Swagger specification @echo "re-generating swagger for enterprise" rm -f $(ENTERPRISE_SPEC_TARGET) - SWAGGER_GENERATE_EXTENSION=false $(SWAGGER) generate spec -m -w pkg/server -o $(ENTERPRISE_SPEC_TARGET) \ + SWAGGER_GENERATE_EXTENSION=false $(SWAGGER) generate spec -q -m -w pkg/server -o $(ENTERPRISE_SPEC_TARGET) \ -x "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" \ -x "github.com/prometheus/alertmanager" \ -i pkg/api/swagger_tags.json \ @@ -77,7 +77,7 @@ endif swagger-gen: gen-go $(MERGED_SPEC_TARGET) swagger-validate swagger-validate: $(MERGED_SPEC_TARGET) $(SWAGGER) ## Validate API spec - $(SWAGGER) validate $(<) + $(SWAGGER) validate --skip-warnings $(<) swagger-clean: rm -f $(SPEC_TARGET) $(MERGED_SPEC_TARGET) $(OAPI_SPEC_TARGET) diff --git a/pkg/api/folder.go b/pkg/api/folder.go index fbb63f4d98..719eb6dfd2 100644 --- a/pkg/api/folder.go +++ b/pkg/api/folder.go @@ -31,7 +31,7 @@ const REDACTED = "redacted" // // Get all folders. // -// Returns all folders that the authenticated user has permission to view. +// It returns all folders that the authenticated user has permission to view. // If nested folders are enabled, it expects an additional query parameter with the parent folder UID // and returns the immediate subfolders that the authenticated user has permission to view. // If the parameter is not supplied then it returns immediate subfolders under the root diff --git a/public/api-merged.json b/public/api-merged.json index 9ad7eef273..8a7250ee21 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -4926,7 +4926,7 @@ }, "/folders": { "get": { - "description": "Returns all folders that the authenticated user has permission to view.\nIf nested folders are enabled, it expects an additional query parameter with the parent folder UID\nand returns the immediate subfolders that the authenticated user has permission to view.\nIf the parameter is not supplied then it returns immediate subfolders under the root\nthat the authenticated user has permission to view.", + "description": "It returns all folders that the authenticated user has permission to view.\nIf nested folders are enabled, it expects an additional query parameter with the parent folder UID\nand returns the immediate subfolders that the authenticated user has permission to view.\nIf the parameter is not supplied then it returns immediate subfolders under the root\nthat the authenticated user has permission to view.", "tags": [ "folders" ], diff --git a/public/openapi3.json b/public/openapi3.json index 3bd8efd4f2..6337fe7900 100644 --- a/public/openapi3.json +++ b/public/openapi3.json @@ -18284,7 +18284,7 @@ }, "/folders": { "get": { - "description": "Returns all folders that the authenticated user has permission to view.\nIf nested folders are enabled, it expects an additional query parameter with the parent folder UID\nand returns the immediate subfolders that the authenticated user has permission to view.\nIf the parameter is not supplied then it returns immediate subfolders under the root\nthat the authenticated user has permission to view.", + "description": "It returns all folders that the authenticated user has permission to view.\nIf nested folders are enabled, it expects an additional query parameter with the parent folder UID\nand returns the immediate subfolders that the authenticated user has permission to view.\nIf the parameter is not supplied then it returns immediate subfolders under the root\nthat the authenticated user has permission to view.", "operationId": "getFolders", "parameters": [ { diff --git a/scripts/drone/pipelines/swagger_gen.star b/scripts/drone/pipelines/swagger_gen.star index 7275f405bf..41018250cd 100644 --- a/scripts/drone/pipelines/swagger_gen.star +++ b/scripts/drone/pipelines/swagger_gen.star @@ -4,7 +4,7 @@ This module returns all pipelines used in OpenAPI specification generation of Gr load( "scripts/drone/steps/lib.star", - "clone_enterprise_step_pr", + "enterprise_setup_step", ) load( "scripts/drone/utils/images.star", @@ -42,7 +42,7 @@ def swagger_gen_step(ver_mode): def swagger_gen(ver_mode, source = "${DRONE_SOURCE_BRANCH}"): test_steps = [ - clone_enterprise_step_pr(source = source, canFail = True), + enterprise_setup_step(source = source, canFail = True), swagger_gen_step(ver_mode = ver_mode), ] From beea7d1c2bc42e90ba4fbe69d3d8b49bea10ff7b Mon Sep 17 00:00:00 2001 From: Mihai Doarna Date: Thu, 7 Mar 2024 12:08:58 +0200 Subject: [PATCH 0459/1406] Auth: use the scope parameter instead of a hardcoded value in appendUniqueScope() (#84053) use the scope parameter instead of a hardcoded value --- pkg/login/social/connectors/common.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/login/social/connectors/common.go b/pkg/login/social/connectors/common.go index 153e9eb998..7e9d62a6c7 100644 --- a/pkg/login/social/connectors/common.go +++ b/pkg/login/social/connectors/common.go @@ -197,7 +197,7 @@ func CreateOAuthInfoFromKeyValues(settingsKV map[string]any) (*social.OAuthInfo, } func appendUniqueScope(config *oauth2.Config, scope string) { - if !slices.Contains(config.Scopes, social.OfflineAccessScope) { - config.Scopes = append(config.Scopes, social.OfflineAccessScope) + if !slices.Contains(config.Scopes, scope) { + config.Scopes = append(config.Scopes, scope) } } From 1181141b401e8763485c89f415ec8f8563467843 Mon Sep 17 00:00:00 2001 From: Selene Date: Thu, 7 Mar 2024 11:09:19 +0100 Subject: [PATCH 0460/1406] Schemas: Refactor plugin's metadata (#83696) * Remove kinds verification for kind-registry * Read plugin's json information with json library instead of use thema binding * Remove grafanaplugin unification * Don't use kindsys for extract the slot name * Fix IsGroup * Remove all plugindef generation * Refactor schema interfaces * Pushed this change from a different branch by mistake... * Create small plugin definition structure adding additional information for plugins registration * Add some validation checks * Delete unused code * Fix imports lint --- Makefile | 1 - embed.go | 2 +- pkg/api/dtos/plugins.go | 4 +- pkg/api/plugins.go | 4 +- pkg/api/plugins_test.go | 6 +- pkg/plugins/auth/models.go | 4 +- pkg/plugins/codegen/jenny_plugingotypes.go | 4 +- pkg/plugins/codegen/jenny_pluginseachmajor.go | 4 +- pkg/plugins/codegen/jenny_plugintstypes.go | 2 +- pkg/plugins/manager/fakes/fakes.go | 4 +- pkg/plugins/pfs/decl.go | 20 +- pkg/plugins/pfs/decl_parser.go | 16 +- pkg/plugins/pfs/errors.go | 10 - pkg/plugins/pfs/grafanaplugin.cue | 31 -- pkg/plugins/pfs/pfs.go | 151 +++--- pkg/plugins/pfs/plugin.go | 4 +- pkg/plugins/pfs/plugindef_types.go | 42 ++ pkg/plugins/plugindef/gen.go | 130 ----- pkg/plugins/plugindef/pascal_test.go | 43 -- pkg/plugins/plugindef/plugindef.cue | 428 ---------------- pkg/plugins/plugindef/plugindef.go | 73 --- .../plugindef/plugindef_bindings_gen.go | 84 --- pkg/plugins/plugindef/plugindef_types_gen.go | 484 ------------------ pkg/plugins/plugins.go | 4 +- .../pluginsintegration/loader/loader_test.go | 6 +- .../pluginsintegration/pipeline/steps.go | 4 +- .../pluginconfig/envvars_test.go | 4 +- .../serviceregistration.go | 8 +- public/app/plugins/gen.go | 5 +- 29 files changed, 181 insertions(+), 1401 deletions(-) delete mode 100644 pkg/plugins/pfs/grafanaplugin.cue create mode 100644 pkg/plugins/pfs/plugindef_types.go delete mode 100644 pkg/plugins/plugindef/gen.go delete mode 100644 pkg/plugins/plugindef/pascal_test.go delete mode 100644 pkg/plugins/plugindef/plugindef.cue delete mode 100644 pkg/plugins/plugindef/plugindef.go delete mode 100644 pkg/plugins/plugindef/plugindef_bindings_gen.go delete mode 100644 pkg/plugins/plugindef/plugindef_types_gen.go diff --git a/Makefile b/Makefile index 896493bc40..f6f07671f0 100644 --- a/Makefile +++ b/Makefile @@ -103,7 +103,6 @@ openapi3-gen: swagger-gen ## Generates OpenApi 3 specs from the Swagger 2 alread ##@ Building gen-cue: ## Do all CUE/Thema code generation @echo "generate code from .cue files" - go generate ./pkg/plugins/plugindef go generate ./kinds/gen.go go generate ./public/app/plugins/gen.go diff --git a/embed.go b/embed.go index 570415e003..e022a032c4 100644 --- a/embed.go +++ b/embed.go @@ -6,5 +6,5 @@ import ( // CueSchemaFS embeds all schema-related CUE files in the Grafana project. // -//go:embed cue.mod/module.cue kinds/*.cue kinds/*/*.cue packages/grafana-schema/src/common/*.cue public/app/plugins/*/*/*.cue public/app/plugins/*/*/plugin.json pkg/plugins/*/*.cue +//go:embed cue.mod/module.cue kinds/*.cue kinds/*/*.cue packages/grafana-schema/src/common/*.cue public/app/plugins/*/*/*.cue public/app/plugins/*/*/plugin.json var CueSchemaFS embed.FS diff --git a/pkg/api/dtos/plugins.go b/pkg/api/dtos/plugins.go index 0f50b6cbd7..7d768e059e 100644 --- a/pkg/api/dtos/plugins.go +++ b/pkg/api/dtos/plugins.go @@ -2,7 +2,7 @@ package dtos import ( "github.com/grafana/grafana/pkg/plugins" - "github.com/grafana/grafana/pkg/plugins/plugindef" + "github.com/grafana/grafana/pkg/plugins/pfs" "github.com/grafana/grafana/pkg/services/accesscontrol" ) @@ -48,7 +48,7 @@ type PluginListItem struct { SignatureOrg string `json:"signatureOrg"` AccessControl accesscontrol.Metadata `json:"accessControl,omitempty"` AngularDetected bool `json:"angularDetected"` - IAM *plugindef.IAM `json:"iam,omitempty"` + IAM *pfs.IAM `json:"iam,omitempty"` } type PluginList []PluginListItem diff --git a/pkg/api/plugins.go b/pkg/api/plugins.go index 55a610891e..59209f4491 100644 --- a/pkg/api/plugins.go +++ b/pkg/api/plugins.go @@ -21,7 +21,7 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/plugins" - "github.com/grafana/grafana/pkg/plugins/plugindef" + "github.com/grafana/grafana/pkg/plugins/pfs" "github.com/grafana/grafana/pkg/plugins/repo" ac "github.com/grafana/grafana/pkg/services/accesscontrol" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" @@ -552,7 +552,7 @@ func (hs *HTTPServer) hasPluginRequestedPermissions(c *contextmodel.ReqContext, } // evalAllPermissions generates an evaluator with all permissions from the input slice -func evalAllPermissions(ps []plugindef.Permission) ac.Evaluator { +func evalAllPermissions(ps []pfs.Permission) ac.Evaluator { res := []ac.Evaluator{} for _, p := range ps { if p.Scope != nil { diff --git a/pkg/api/plugins_test.go b/pkg/api/plugins_test.go index 4d6841bc28..9fdcf916eb 100644 --- a/pkg/api/plugins_test.go +++ b/pkg/api/plugins_test.go @@ -27,7 +27,7 @@ import ( "github.com/grafana/grafana/pkg/plugins/manager/fakes" "github.com/grafana/grafana/pkg/plugins/manager/filestore" "github.com/grafana/grafana/pkg/plugins/manager/registry" - "github.com/grafana/grafana/pkg/plugins/plugindef" + "github.com/grafana/grafana/pkg/plugins/pfs" "github.com/grafana/grafana/pkg/plugins/pluginscdn" ac "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" @@ -658,8 +658,8 @@ func TestHTTPServer_hasPluginRequestedPermissions(t *testing.T) { pluginReg := pluginstore.Plugin{ JSONData: plugins.JSONData{ ID: "grafana-test-app", - IAM: &plugindef.IAM{ - Permissions: []plugindef.Permission{{Action: ac.ActionUsersRead, Scope: newStr(ac.ScopeUsersAll)}, {Action: ac.ActionUsersCreate}}, + IAM: &pfs.IAM{ + Permissions: []pfs.Permission{{Action: ac.ActionUsersRead, Scope: newStr(ac.ScopeUsersAll)}, {Action: ac.ActionUsersCreate}}, }, }, } diff --git a/pkg/plugins/auth/models.go b/pkg/plugins/auth/models.go index b236353116..93b97f0394 100644 --- a/pkg/plugins/auth/models.go +++ b/pkg/plugins/auth/models.go @@ -3,7 +3,7 @@ package auth import ( "context" - "github.com/grafana/grafana/pkg/plugins/plugindef" + "github.com/grafana/grafana/pkg/plugins/pfs" ) type ExternalService struct { @@ -14,6 +14,6 @@ type ExternalService struct { type ExternalServiceRegistry interface { HasExternalService(ctx context.Context, pluginID string) (bool, error) - RegisterExternalService(ctx context.Context, pluginID string, pType plugindef.Type, svc *plugindef.IAM) (*ExternalService, error) + RegisterExternalService(ctx context.Context, pluginID string, pType pfs.Type, svc *pfs.IAM) (*ExternalService, error) RemoveExternalService(ctx context.Context, pluginID string) error } diff --git a/pkg/plugins/codegen/jenny_plugingotypes.go b/pkg/plugins/codegen/jenny_plugingotypes.go index 9547e2d09f..16062b60e2 100644 --- a/pkg/plugins/codegen/jenny_plugingotypes.go +++ b/pkg/plugins/codegen/jenny_plugingotypes.go @@ -35,10 +35,10 @@ func (j *pgoJenny) Generate(decl *pfs.PluginDecl) (*codejen.File, error) { return nil, nil } - slotname := strings.ToLower(decl.SchemaInterface.Name()) + slotname := strings.ToLower(decl.SchemaInterface.Name) byt, err := gocode.GenerateTypesOpenAPI(decl.Lineage.Latest(), &gocode.TypeConfigOpenAPI{ Config: &openapi.Config{ - Group: decl.SchemaInterface.IsGroup(), + Group: decl.SchemaInterface.IsGroup, Config: &copenapi.Config{ MaxCycleDepth: 10, }, diff --git a/pkg/plugins/codegen/jenny_pluginseachmajor.go b/pkg/plugins/codegen/jenny_pluginseachmajor.go index 6d3e6d2682..e90a11a984 100644 --- a/pkg/plugins/codegen/jenny_pluginseachmajor.go +++ b/pkg/plugins/codegen/jenny_pluginseachmajor.go @@ -42,8 +42,8 @@ func (j *pleJenny) Generate(decl *pfs.PluginDecl) (codejen.Files, error) { } version := "export const pluginVersion = \"%s\";" - if decl.PluginMeta.Info.Version != nil { - version = fmt.Sprintf(version, *decl.PluginMeta.Info.Version) + if decl.PluginMeta.Version != nil { + version = fmt.Sprintf(version, *decl.PluginMeta.Version) } else { version = fmt.Sprintf(version, getGrafanaVersion()) } diff --git a/pkg/plugins/codegen/jenny_plugintstypes.go b/pkg/plugins/codegen/jenny_plugintstypes.go index 1bedbaea72..23c00a5ca5 100644 --- a/pkg/plugins/codegen/jenny_plugintstypes.go +++ b/pkg/plugins/codegen/jenny_plugintstypes.go @@ -51,7 +51,7 @@ func (j *ptsJenny) Generate(decl *pfs.PluginDecl) (*codejen.File, error) { Data: string(jf.Data), }) - path := filepath.Join(j.root, decl.PluginPath, fmt.Sprintf("%s.gen.ts", strings.ToLower(decl.SchemaInterface.Name()))) + path := filepath.Join(j.root, decl.PluginPath, fmt.Sprintf("%s.gen.ts", strings.ToLower(decl.SchemaInterface.Name))) data := []byte(tsf.String()) data = data[:len(data)-1] // remove the additional line break added by the inner jenny diff --git a/pkg/plugins/manager/fakes/fakes.go b/pkg/plugins/manager/fakes/fakes.go index 17e39fe86e..361d2d90d2 100644 --- a/pkg/plugins/manager/fakes/fakes.go +++ b/pkg/plugins/manager/fakes/fakes.go @@ -13,7 +13,7 @@ import ( "github.com/grafana/grafana/pkg/plugins/auth" "github.com/grafana/grafana/pkg/plugins/backendplugin" "github.com/grafana/grafana/pkg/plugins/log" - "github.com/grafana/grafana/pkg/plugins/plugindef" + "github.com/grafana/grafana/pkg/plugins/pfs" "github.com/grafana/grafana/pkg/plugins/repo" "github.com/grafana/grafana/pkg/plugins/storage" ) @@ -456,7 +456,7 @@ func (f *FakeAuthService) HasExternalService(ctx context.Context, pluginID strin return f.Result != nil, nil } -func (f *FakeAuthService) RegisterExternalService(ctx context.Context, pluginID string, pType plugindef.Type, svc *plugindef.IAM) (*auth.ExternalService, error) { +func (f *FakeAuthService) RegisterExternalService(ctx context.Context, pluginID string, pType pfs.Type, svc *pfs.IAM) (*auth.ExternalService, error) { return f.Result, nil } diff --git a/pkg/plugins/pfs/decl.go b/pkg/plugins/pfs/decl.go index 37a3916d90..406275d3c8 100644 --- a/pkg/plugins/pfs/decl.go +++ b/pkg/plugins/pfs/decl.go @@ -4,20 +4,30 @@ import ( "cuelang.org/go/cue/ast" "github.com/grafana/kindsys" "github.com/grafana/thema" - - "github.com/grafana/grafana/pkg/plugins/plugindef" ) type PluginDecl struct { - SchemaInterface *kindsys.SchemaInterface + SchemaInterface *SchemaInterface Lineage thema.Lineage Imports []*ast.ImportSpec PluginPath string - PluginMeta plugindef.PluginDef + PluginMeta Metadata KindDecl kindsys.Def[kindsys.ComposableProperties] } -func EmptyPluginDecl(path string, meta plugindef.PluginDef) *PluginDecl { +type SchemaInterface struct { + Name string + IsGroup bool +} + +type Metadata struct { + Id string + Name string + Backend *bool + Version *string +} + +func EmptyPluginDecl(path string, meta Metadata) *PluginDecl { return &PluginDecl{ PluginPath: path, PluginMeta: meta, diff --git a/pkg/plugins/pfs/decl_parser.go b/pkg/plugins/pfs/decl_parser.go index 04833a425d..4e909b16ab 100644 --- a/pkg/plugins/pfs/decl_parser.go +++ b/pkg/plugins/pfs/decl_parser.go @@ -6,7 +6,6 @@ import ( "path/filepath" "sort" - "github.com/grafana/kindsys" "github.com/grafana/thema" ) @@ -15,6 +14,18 @@ type declParser struct { skip map[string]bool } +// Extracted from kindsys repository +var schemaInterfaces = map[string]*SchemaInterface{ + "PanelCfg": { + Name: "PanelCfg", + IsGroup: true, + }, + "DataQuery": { + Name: "DataQuery", + IsGroup: false, + }, +} + func NewDeclParser(rt *thema.Runtime, skip map[string]bool) *declParser { return &declParser{ rt: rt, @@ -50,12 +61,11 @@ func (psr *declParser) Parse(root fs.FS) ([]*PluginDecl, error) { } for slotName, kind := range pp.ComposableKinds { - slot, err := kindsys.FindSchemaInterface(slotName) if err != nil { return nil, fmt.Errorf("parsing plugin failed for %s: %s", dir, err) } decls = append(decls, &PluginDecl{ - SchemaInterface: &slot, + SchemaInterface: schemaInterfaces[slotName], Lineage: kind.Lineage(), Imports: pp.CUEImports, PluginMeta: pp.Properties, diff --git a/pkg/plugins/pfs/errors.go b/pkg/plugins/pfs/errors.go index a65c588a0d..6bbd1cc093 100644 --- a/pkg/plugins/pfs/errors.go +++ b/pkg/plugins/pfs/errors.go @@ -11,16 +11,6 @@ var ErrNoRootFile = errors.New("no plugin.json at root of fs.fS") // ErrInvalidRootFile indicates that the root plugin.json file is invalid. var ErrInvalidRootFile = errors.New("plugin.json is invalid") -// ErrComposableNotExpected indicates that a plugin has a composable kind for a -// schema interface that is not expected, given the type of the plugin. (For -// example, a datasource plugin has a panelcfg composable kind) -var ErrComposableNotExpected = errors.New("plugin type should not produce composable kind for schema interface") - -// ErrExpectedComposable indicates that a plugin lacks a composable kind -// implementation for a schema interface that is expected for that plugin's -// type. (For example, a datasource plugin lacks a queries composable kind) -var ErrExpectedComposable = errors.New("plugin type should produce composable kind for schema interface") - // ErrInvalidGrafanaPluginInstance indicates a plugin's set of .cue // grafanaplugin package files are invalid with respect to the GrafanaPlugin // spec. diff --git a/pkg/plugins/pfs/grafanaplugin.cue b/pkg/plugins/pfs/grafanaplugin.cue deleted file mode 100644 index 6adb68555a..0000000000 --- a/pkg/plugins/pfs/grafanaplugin.cue +++ /dev/null @@ -1,31 +0,0 @@ -package pfs - -import ( - "github.com/grafana/kindsys" -) - -// GrafanaPlugin specifies what plugins may declare in .cue files in a -// `grafanaplugin` CUE package in the plugin root directory (adjacent to plugin.json). -GrafanaPlugin: { - // id and pascalName are injected from plugin.json. Plugin authors can write - // values for them in .cue files, but the only valid values will be the ones - // given in plugin.json. - id: string - pascalName: string - - // A plugin defines its Composable kinds under this key. - // - // This struct is open for forwards compatibility - older versions of Grafana (or - // dependent tooling) should not break if new versions introduce additional schema interfaces. - composableKinds?: [Iface=string]: kindsys.Composable & { - name: pascalName + Iface - schemaInterface: Iface - lineage: name: pascalName + Iface - } - - // A plugin defines its Custom kinds under this key. - customKinds?: [Name=string]: kindsys.Custom & { - name: Name - } - ... -} diff --git a/pkg/plugins/pfs/pfs.go b/pkg/plugins/pfs/pfs.go index 73cb4a3f33..b56f5fdf65 100644 --- a/pkg/plugins/pfs/pfs.go +++ b/pkg/plugins/pfs/pfs.go @@ -1,56 +1,29 @@ package pfs import ( + "encoding/json" "fmt" "io/fs" - "path/filepath" "sort" "strings" - "sync" "testing/fstest" "cuelang.org/go/cue" - "cuelang.org/go/cue/build" "cuelang.org/go/cue/cuecontext" "cuelang.org/go/cue/errors" - "cuelang.org/go/cue/parser" "cuelang.org/go/cue/token" "github.com/grafana/kindsys" "github.com/grafana/thema" "github.com/grafana/thema/load" - "github.com/grafana/thema/vmux" "github.com/yalue/merged_fs" "github.com/grafana/grafana/pkg/cuectx" - "github.com/grafana/grafana/pkg/plugins/plugindef" ) // PackageName is the name of the CUE package that Grafana will load when // looking for a Grafana plugin's kind declarations. const PackageName = "grafanaplugin" -var onceGP sync.Once -var defaultGP cue.Value - -func doLoadGP(ctx *cue.Context) cue.Value { - v, err := cuectx.BuildGrafanaInstance(ctx, filepath.Join("pkg", "plugins", "pfs"), "pfs", nil) - if err != nil { - // should be unreachable - panic(err) - } - return v.LookupPath(cue.MakePath(cue.Str("GrafanaPlugin"))) -} - -func loadGP(ctx *cue.Context) cue.Value { - if ctx == nil || ctx == cuectx.GrafanaCUEContext() { - onceGP.Do(func() { - defaultGP = doLoadGP(ctx) - }) - return defaultGP - } - return doLoadGP(ctx) -} - // PermittedCUEImports returns the list of import paths that may be used in a // plugin's grafanaplugin cue package. var PermittedCUEImports = cuectx.PermittedCUEImports @@ -109,35 +82,14 @@ func ParsePluginFS(fsys fs.FS, rt *thema.Runtime) (ParsedPlugin, error) { rt = cuectx.GrafanaThemaRuntime() } - lin, err := plugindef.Lineage(rt) + metadata, err := getPluginMetadata(fsys) if err != nil { - panic(fmt.Sprintf("plugindef lineage is invalid or broken, needs dev attention: %s", err)) - } - ctx := rt.Context() - - b, err := fs.ReadFile(fsys, "plugin.json") - if err != nil { - if errors.Is(err, fs.ErrNotExist) { - return ParsedPlugin{}, ErrNoRootFile - } - return ParsedPlugin{}, fmt.Errorf("error reading plugin.json: %w", err) + return ParsedPlugin{}, err } pp := ParsedPlugin{ ComposableKinds: make(map[string]kindsys.Composable), - // CustomKinds: make(map[string]kindsys.Custom), - } - - // Pass the raw bytes into the muxer, get the populated PluginDef type out that we want. - // TODO stop ignoring second return. (for now, lacunas are a WIP and can't occur until there's >1 schema in the plugindef lineage) - pinst, _, err := vmux.NewTypedMux(lin.TypedSchema(), vmux.NewJSONCodec("plugin.json"))(b) - if err != nil { - return ParsedPlugin{}, errors.Wrap(errors.Promote(err, ""), ErrInvalidRootFile) - } - pp.Properties = *(pinst.ValueP()) - // FIXME remove this once it's being correctly populated coming out of lineage - if pp.Properties.PascalName == "" { - pp.Properties.PascalName = plugindef.DerivePascalName(pp.Properties) + Properties: metadata, } if cuefiles, err := fs.Glob(fsys, "*.cue"); err != nil { @@ -146,8 +98,6 @@ func ParsePluginFS(fsys fs.FS, rt *thema.Runtime) (ParsedPlugin, error) { return pp, nil } - gpv := loadGP(rt.Context()) - fsys, err = ensureCueMod(fsys, pp.Properties) if err != nil { return ParsedPlugin{}, fmt.Errorf("%s has invalid cue.mod: %w", pp.Properties.Id, err) @@ -161,11 +111,6 @@ func ParsePluginFS(fsys fs.FS, rt *thema.Runtime) (ParsedPlugin, error) { return ParsedPlugin{}, errors.Wrap(errors.Newf(token.NoPos, "%s did not load", pp.Properties.Id), err) } - f, _ := parser.ParseFile("plugin.json", fmt.Sprintf(`{ - "id": %q, - "pascalName": %q - }`, pp.Properties.Id, pp.Properties.PascalName)) - for _, f := range bi.Files { for _, im := range f.Imports { ip := strings.Trim(im.Path.Value, "\"") @@ -187,16 +132,7 @@ func ParsePluginFS(fsys fs.FS, rt *thema.Runtime) (ParsedPlugin, error) { panic("Refactor required - upstream CUE implementation changed, bi.Files is no longer populated") } - // Inject the JSON directly into the build so it gets loaded together - bi.BuildFiles = append(bi.BuildFiles, &build.File{ - Filename: "plugin.json", - Encoding: build.JSON, - Form: build.Data, - Source: b, - }) - bi.Files = append(bi.Files, f) - - gpi := ctx.BuildInstance(bi).Unify(gpv) + gpi := rt.Context().BuildInstance(bi) if gpi.Err() != nil { return ParsedPlugin{}, errors.Wrap(errors.Promote(ErrInvalidGrafanaPluginInstance, pp.Properties.Id), gpi.Err()) } @@ -207,6 +143,18 @@ func ParsePluginFS(fsys fs.FS, rt *thema.Runtime) (ParsedPlugin, error) { continue } + iv = iv.FillPath(cue.MakePath(cue.Str("schemaInterface")), si.Name()) + iv = iv.FillPath(cue.MakePath(cue.Str("name")), derivePascalName(pp.Properties.Id, pp.Properties.Name)+si.Name()) + lineageNamePath := iv.LookupPath(cue.MakePath(cue.Str("lineage"), cue.Str("name"))) + if !lineageNamePath.Exists() { + iv = iv.FillPath(cue.MakePath(cue.Str("lineage"), cue.Str("name")), derivePascalName(pp.Properties.Id, pp.Properties.Name)+si.Name()) + } + + validSchema := iv.LookupPath(cue.ParsePath("lineage.schemas[0].schema")) + if !validSchema.Exists() { + return ParsedPlugin{}, errors.Wrap(errors.Promote(ErrInvalidGrafanaPluginInstance, pp.Properties.Id), validSchema.Err()) + } + props, err := kindsys.ToKindProps[kindsys.ComposableProperties](iv) if err != nil { return ParsedPlugin{}, err @@ -222,7 +170,6 @@ func ParsePluginFS(fsys fs.FS, rt *thema.Runtime) (ParsedPlugin, error) { pp.ComposableKinds[si.Name()] = compo } - // TODO custom kinds return pp, nil } @@ -237,7 +184,7 @@ func ParsePluginFS(fsys fs.FS, rt *thema.Runtime) (ParsedPlugin, error) { func LoadComposableKindDef(fsys fs.FS, rt *thema.Runtime, defpath string) (kindsys.Def[kindsys.ComposableProperties], error) { pp := ParsedPlugin{ ComposableKinds: make(map[string]kindsys.Composable), - Properties: plugindef.PluginDef{ + Properties: Metadata{ Id: defpath, }, } @@ -269,13 +216,13 @@ func LoadComposableKindDef(fsys fs.FS, rt *thema.Runtime, defpath string) (kinds }, nil } -func ensureCueMod(fsys fs.FS, pdef plugindef.PluginDef) (fs.FS, error) { +func ensureCueMod(fsys fs.FS, metadata Metadata) (fs.FS, error) { if modf, err := fs.ReadFile(fsys, "cue.mod/module.cue"); err != nil { if !errors.Is(err, fs.ErrNotExist) { return nil, err } return merged_fs.NewMergedFS(fsys, fstest.MapFS{ - "cue.mod/module.cue": &fstest.MapFile{Data: []byte(fmt.Sprintf(`module: "grafana.com/grafana/plugins/%s"`, pdef.Id))}, + "cue.mod/module.cue": &fstest.MapFile{Data: []byte(fmt.Sprintf(`module: "grafana.com/grafana/plugins/%s"`, metadata.Id))}, }), nil } else if _, err := cuecontext.New().CompileBytes(modf).LookupPath(cue.MakePath(cue.Str("module"))).String(); err != nil { return nil, fmt.Errorf("error reading cue module name: %w", err) @@ -283,3 +230,61 @@ func ensureCueMod(fsys fs.FS, pdef plugindef.PluginDef) (fs.FS, error) { return fsys, nil } + +func getPluginMetadata(fsys fs.FS) (Metadata, error) { + b, err := fs.ReadFile(fsys, "plugin.json") + if err != nil { + if errors.Is(err, fs.ErrNotExist) { + return Metadata{}, ErrNoRootFile + } + return Metadata{}, fmt.Errorf("error reading plugin.json: %w", err) + } + + var metadata PluginDef + if err := json.Unmarshal(b, &metadata); err != nil { + return Metadata{}, fmt.Errorf("error unmarshalling plugin.json: %s", err) + } + + if err := metadata.Validate(); err != nil { + return Metadata{}, err + } + + return Metadata{ + Id: metadata.Id, + Name: metadata.Name, + Backend: metadata.Backend, + Version: metadata.Info.Version, + }, nil +} + +func derivePascalName(id string, name string) string { + sani := func(s string) string { + ret := strings.Title(strings.Map(func(r rune) rune { + switch { + case r >= 'a' && r <= 'z': + return r + case r >= 'A' && r <= 'Z': + return r + default: + return -1 + } + }, strings.Title(strings.Map(func(r rune) rune { + switch r { + case '-', '_': + return ' ' + default: + return r + } + }, s)))) + if len(ret) > 63 { + return ret[:63] + } + return ret + } + + fromname := sani(name) + if len(fromname) != 0 { + return fromname + } + return sani(strings.Split(id, "-")[1]) +} diff --git a/pkg/plugins/pfs/plugin.go b/pkg/plugins/pfs/plugin.go index e5b95dae39..2829a81046 100644 --- a/pkg/plugins/pfs/plugin.go +++ b/pkg/plugins/pfs/plugin.go @@ -3,8 +3,6 @@ package pfs import ( "cuelang.org/go/cue/ast" "github.com/grafana/kindsys" - - "github.com/grafana/grafana/pkg/plugins/plugindef" ) // ParsedPlugin represents everything knowable about a single plugin from static @@ -14,7 +12,7 @@ import ( // struct returned from [ParsePluginFS]. type ParsedPlugin struct { // Properties contains the plugin's definition, as declared in plugin.json. - Properties plugindef.PluginDef + Properties Metadata // ComposableKinds is a map of all the composable kinds declared in this plugin. // Keys are the name of the [kindsys.SchemaInterface] implemented by the value. diff --git a/pkg/plugins/pfs/plugindef_types.go b/pkg/plugins/pfs/plugindef_types.go new file mode 100644 index 0000000000..841200428a --- /dev/null +++ b/pkg/plugins/pfs/plugindef_types.go @@ -0,0 +1,42 @@ +package pfs + +type Type string + +// Defines values for Type. +const ( + TypeApp Type = "app" + TypeDatasource Type = "datasource" + TypePanel Type = "panel" + TypeRenderer Type = "renderer" + TypeSecretsmanager Type = "secretsmanager" +) + +type PluginDef struct { + Id string + Name string + Backend *bool + Type Type + Info Info + IAM IAM +} + +type Info struct { + Version *string +} + +type IAM struct { + Permissions []Permission `json:"permissions,omitempty"` +} + +type Permission struct { + Action string `json:"action"` + Scope *string `json:"scope,omitempty"` +} + +func (pd PluginDef) Validate() error { + if pd.Id == "" || pd.Name == "" || pd.Type == "" { + return ErrInvalidRootFile + } + + return nil +} diff --git a/pkg/plugins/plugindef/gen.go b/pkg/plugins/plugindef/gen.go deleted file mode 100644 index 710a6734fa..0000000000 --- a/pkg/plugins/plugindef/gen.go +++ /dev/null @@ -1,130 +0,0 @@ -//go:build ignore -// +build ignore - -package main - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "os" - "path/filepath" - - "cuelang.org/go/cue/cuecontext" - "github.com/dave/dst" - "github.com/grafana/codejen" - "github.com/grafana/grafana/pkg/codegen" - "github.com/grafana/grafana/pkg/cuectx" - "github.com/grafana/thema" - "github.com/grafana/thema/encoding/gocode" - "github.com/grafana/thema/encoding/jsonschema" -) - -var dirPlugindef = filepath.Join("pkg", "plugins", "plugindef") - -// main generator for plugindef. plugindef isn't a kind, so it has its own -// one-off main generator. -func main() { - v := elsedie(cuectx.BuildGrafanaInstance(nil, dirPlugindef, "", nil))("could not load plugindef cue package") - - lin := elsedie(thema.BindLineage(v, cuectx.GrafanaThemaRuntime()))("plugindef lineage is invalid") - - jl := &codejen.JennyList[thema.Lineage]{} - jl.AppendOneToOne(&jennytypego{}, &jennybindgo{}) - jl.AddPostprocessors(codegen.SlashHeaderMapper(filepath.Join(dirPlugindef, "gen.go"))) - - cwd, err := os.Getwd() - if err != nil { - fmt.Fprintf(os.Stderr, "could not get working directory: %s", err) - os.Exit(1) - } - - groot := filepath.Clean(filepath.Join(cwd, "../../..")) - - jfs := elsedie(jl.GenerateFS(lin))("plugindef jenny pipeline failed") - if _, set := os.LookupEnv("CODEGEN_VERIFY"); set { - if err := jfs.Verify(context.Background(), groot); err != nil { - die(fmt.Errorf("generated code is out of sync with inputs:\n%s\nrun `make gen-cue` to regenerate", err)) - } - } else if err := jfs.Write(context.Background(), groot); err != nil { - die(fmt.Errorf("error while writing generated code to disk:\n%s", err)) - } -} - -// one-off jenny for plugindef go types -type jennytypego struct{} - -func (j *jennytypego) JennyName() string { - return "PluginGoTypes" -} - -func (j *jennytypego) Generate(lin thema.Lineage) (*codejen.File, error) { - f, err := codegen.GoTypesJenny{}.Generate(codegen.SchemaForGen{ - Name: "PluginDef", - Schema: lin.Latest(), - IsGroup: false, - }) - if f != nil { - f.RelativePath = filepath.Join(dirPlugindef, f.RelativePath) - } - return f, err -} - -// one-off jenny for plugindef go bindings -type jennybindgo struct{} - -func (j *jennybindgo) JennyName() string { - return "PluginGoBindings" -} - -func (j *jennybindgo) Generate(lin thema.Lineage) (*codejen.File, error) { - b, err := gocode.GenerateLineageBinding(lin, &gocode.BindingConfig{ - TitleName: "PluginDef", - Assignee: dst.NewIdent("*PluginDef"), - PrivateFactory: true, - }) - if err != nil { - return nil, err - } - return codejen.NewFile(filepath.Join(dirPlugindef, "plugindef_bindings_gen.go"), b, j), nil -} - -// one-off jenny for plugindef json schema generator -type jennyjschema struct{} - -func (j *jennyjschema) JennyName() string { - return "PluginJSONSchema" -} - -func (j *jennyjschema) Generate(lin thema.Lineage) (*codejen.File, error) { - f, err := jsonschema.GenerateSchema(lin.Latest()) - if err != nil { - return nil, err - } - - b, _ := cuecontext.New().BuildFile(f).MarshalJSON() - nb := new(bytes.Buffer) - die(json.Indent(nb, b, "", " ")) - return codejen.NewFile(filepath.FromSlash("docs/sources/developers/plugins/plugin.schema.json"), nb.Bytes(), j), nil -} - -func elsedie[T any](t T, err error) func(msg string) T { - if err != nil { - return func(msg string) T { - fmt.Fprintf(os.Stderr, "%s: %s\n", msg, err) - os.Exit(1) - return t - } - } - return func(msg string) T { - return t - } -} - -func die(err error) { - if err != nil { - fmt.Fprint(os.Stderr, err, "\n") - os.Exit(1) - } -} diff --git a/pkg/plugins/plugindef/pascal_test.go b/pkg/plugins/plugindef/pascal_test.go deleted file mode 100644 index 095384c5d0..0000000000 --- a/pkg/plugins/plugindef/pascal_test.go +++ /dev/null @@ -1,43 +0,0 @@ -package plugindef - -import ( - "testing" - - "github.com/stretchr/testify/require" -) - -func TestDerivePascal(t *testing.T) { - table := []struct { - id, name, out string - }{ - { - name: "-- Grafana --", - out: "Grafana", - }, - { - name: "A weird/Thing", - out: "AWeirdThing", - }, - { - name: "/", - out: "Empty", - }, - { - name: "some really Long thing WHY would38883 anyone do this i don't know but hey It seems like it this is just going on and", - out: "SomeReallyLongThingWHYWouldAnyoneDoThisIDonTKnowButHeyItSeemsLi", - }, - } - - for _, row := range table { - if row.id == "" { - row.id = "default-empty-panel" - } - - pd := PluginDef{ - Id: row.id, - Name: row.name, - } - - require.Equal(t, row.out, DerivePascalName(pd)) - } -} diff --git a/pkg/plugins/plugindef/plugindef.cue b/pkg/plugins/plugindef/plugindef.cue deleted file mode 100644 index 89255479fc..0000000000 --- a/pkg/plugins/plugindef/plugindef.cue +++ /dev/null @@ -1,428 +0,0 @@ -package plugindef - -import ( - "regexp" - "strings" - - "github.com/grafana/thema" -) - -thema.#Lineage -name: "plugindef" -schemas: [{ - version: [0, 0] - schema: { - // Unique name of the plugin. If the plugin is published on - // grafana.com, then the plugin `id` has to follow the naming - // conventions. - id: string & strings.MinRunes(1) - id: =~"^([0-9a-z]+\\-([0-9a-z]+\\-)?(\(strings.Join([ for t in _types {t}], "|"))))|(alertGroups|alertlist|annolist|barchart|bargauge|candlestick|canvas|dashlist|debug|datagrid|gauge|geomap|gettingstarted|graph|heatmap|histogram|icon|live|logs|news|nodeGraph|piechart|pluginlist|stat|state-timeline|status-history|table|table-old|text|timeseries|trend|traces|welcome|xychart|alertmanager|cloudwatch|dashboard|elasticsearch|grafana|grafana-azure-monitor-datasource|stackdriver|graphite|influxdb|jaeger|loki|mixed|mssql|mysql|opentsdb|postgres|prometheus|stackdriver|tempo|grafana-testdata-datasource|zipkin|phlare|parca)$" - - // An alias is useful when migrating from one plugin id to another (rebranding etc) - // This should be used sparingly, and is currently only supported though a hardcoded checklist - aliasIDs?: [...string] - - // Human-readable name of the plugin that is shown to the user in - // the UI. - name: string - - // The set of all plugin types. This hidden field exists solely - // so that the set can be string-interpolated into other fields. - _types: ["app", "datasource", "panel", "renderer", "secretsmanager"] - - // type indicates which type of Grafana plugin this is, of the defined - // set of Grafana plugin types. - type: or(_types) - - // IncludeType is a string identifier of a plugin include type, which is - // a superset of plugin types. - #IncludeType: type | "dashboard" | "page" - - // Metadata about the plugin - info: #Info - - // Metadata about a Grafana plugin. Some fields are used on the plugins - // page in Grafana and others on grafana.com, if the plugin is published. - #Info: { - // Information about the plugin author - author?: { - // Author's name - name?: string - - // Author's name - email?: string - - // Link to author's website - url?: string - } - - // Build information - build?: #BuildInfo - - // Description of plugin. Used on the plugins page in Grafana and - // for search on grafana.com. - description?: string - - // Array of plugin keywords. Used for search on grafana.com. - keywords: [...string] - // should be this, but CUE to openapi converter screws this up - // by inserting a non-concrete default. - // keywords: [string, ...string] - - // An array of link objects to be displayed on this plugin's - // project page in the form `{name: 'foo', url: - // 'http://example.com'}` - links?: [...{ - name?: string - url?: string - }] - - // SVG images that are used as plugin icons - logos?: { - // Link to the "small" version of the plugin logo, which must be - // an SVG image. "Large" and "small" logos can be the same image. - small: string - - // Link to the "large" version of the plugin logo, which must be - // an SVG image. "Large" and "small" logos can be the same image. - large: string - } - - // An array of screenshot objects in the form `{name: 'bar', path: - // 'img/screenshot.png'}` - screenshots?: [...{ - name?: string - path?: string - }] - - // Date when this plugin was built - updated?: =~"^(\\d{4}-\\d{2}-\\d{2}|\\%TODAY\\%)$" - - // Project version of this commit, e.g. `6.7.x` - version?: =~"^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)|(\\%VERSION\\%)$" - } - - #BuildInfo: { - // Time when the plugin was built, as a Unix timestamp - time?: int64 - repo?: string - - // Git branch the plugin was built from - branch?: string - - // Git hash of the commit the plugin was built from - hash?: string - number?: int64 - - // GitHub pull request the plugin was built from - pr?: int32 - } - - // Dependency information related to Grafana and other plugins - dependencies: #Dependencies - - #Dependencies: { - // (Deprecated) Required Grafana version for this plugin, e.g. - // `6.x.x 7.x.x` to denote plugin requires Grafana v6.x.x or - // v7.x.x. - grafanaVersion?: =~"^([0-9]+)(\\.[0-9x]+)?(\\.[0-9x])?$" - - // Required Grafana version for this plugin. Validated using - // https://github.com/npm/node-semver. - grafanaDependency?: =~"^(<=|>=|<|>|=|~|\\^)?([0-9]+)(\\.[0-9x\\*]+)(\\.[0-9x\\*]+)?(\\s(<=|>=|<|=>)?([0-9]+)(\\.[0-9x]+)(\\.[0-9x]+))?(\\-[0-9]+)?$" - - // An array of required plugins on which this plugin depends - plugins?: [...#Dependency] - } - - // Dependency describes another plugin on which a plugin depends. - // The id refers to the plugin package identifier, as given on - // the grafana.com plugin marketplace. - #Dependency: { - id: =~"^[0-9a-z]+\\-([0-9a-z]+\\-)?(app|panel|datasource)$" - type: "app" | "datasource" | "panel" - name: string - version: string - ... - } - - // Schema definition for the plugin.json file. Used primarily for schema validation. - $schema?: string - - // For data source plugins, if the plugin supports alerting. Requires `backend` to be set to `true`. - alerting?: bool - - // For data source plugins, if the plugin supports annotation - // queries. - annotations?: bool - - // Set to true for app plugins that should be enabled and pinned to the navigation bar in all orgs. - autoEnabled?: bool - - // If the plugin has a backend component. - backend?: bool - - // [internal only] Indicates whether the plugin is developed and shipped as part - // of Grafana. Also known as a 'core plugin'. - builtIn: bool | *false - - // Plugin category used on the Add data source page. - category?: "tsdb" | "logging" | "cloud" | "tracing" | "profiling" | "sql" | "enterprise" | "iot" | "other" - - // Grafana Enterprise specific features. - enterpriseFeatures?: { - // Enable/Disable health diagnostics errors. Requires Grafana - // >=7.5.5. - healthDiagnosticsErrors?: bool | *false - ... - } - - // The first part of the file name of the backend component - // executable. There can be multiple executables built for - // different operating system and architecture. Grafana will - // check for executables named `_<$GOOS>_<.exe for Windows>`, e.g. `plugin_linux_amd64`. - // Combination of $GOOS and $GOARCH can be found here: - // https://golang.org/doc/install/source#environment. - executable?: string - - // [internal only] Excludes the plugin from listings in Grafana's UI. Only - // allowed for `builtIn` plugins. - hideFromList: bool | *false - - // Resources to include in plugin. - includes?: [...#Include] - - // A resource to be included in a plugin. - #Include: { - // Unique identifier of the included resource - uid?: string - type: #IncludeType - name?: string - - // (Legacy) The Angular component to use for a page. - component?: string - - // The minimum role a user must have to see this page in the navigation menu. - role?: "Admin" | "Editor" | "Viewer" - - // RBAC action the user must have to access the route - action?: string - - // Used for app plugins. - path?: string - - // Add the include to the navigation menu. - addToNav?: bool - - // Page or dashboard when user clicks the icon in the side menu. - defaultNav?: bool - - // Icon to use in the side menu. For information on available - // icon, refer to [Icons - // Overview](https://developers.grafana.com/ui/latest/index.html?path=/story/docs-overview-icon--icons-overview). - icon?: string - ... - } - - // For data source plugins, if the plugin supports logs. It may be used to filter logs only features. - logs?: bool - - // For data source plugins, if the plugin supports metric queries. - // Used to enable the plugin in the panel editor. - metrics?: bool - - // FIXME there appears to be a bug in thema that prevents this from working. Maybe it'd - // help to refer to it with an alias, but thema can't support using current list syntax. - // syntax (fixed by grafana/thema#82). Either way, for now, pascalName gets populated in Go. - let sani = (strings.ToTitle(regexp.ReplaceAllLiteral("[^a-zA-Z]+", name, ""))) - - // [internal only] The PascalCase name for the plugin. Used for creating machine-friendly - // identifiers, typically in code generation. - // - // If not provided, defaults to name, but title-cased and sanitized (only - // alphabetical characters allowed). - pascalName: string & =~"^([A-Z][a-zA-Z]{1,62})$" | *sani - - // Initialize plugin on startup. By default, the plugin - // initializes on first use. - preload?: bool - - // For data source plugins. There is a query options section in - // the plugin's query editor and these options can be turned on - // if needed. - queryOptions?: { - // For data source plugins. If the `max data points` option should - // be shown in the query options section in the query editor. - maxDataPoints?: bool - - // For data source plugins. If the `min interval` option should be - // shown in the query options section in the query editor. - minInterval?: bool - - // For data source plugins. If the `cache timeout` option should - // be shown in the query options section in the query editor. - cacheTimeout?: bool - } - - // Routes is a list of proxy routes, if any. For datasource plugins only. - routes?: [...#Route] - - // For panel plugins. Hides the query editor. - skipDataQuery?: bool - - // Marks a plugin as a pre-release. - state?: #ReleaseState - - // ReleaseState indicates release maturity state of a plugin. - #ReleaseState: "alpha" | "beta" | "deprecated" | *"stable" - - // For data source plugins, if the plugin supports streaming. Used in Explore to start live streaming. - streaming?: bool - - // For data source plugins, if the plugin supports tracing. Used for example to link logs (e.g. Loki logs) with tracing plugins. - tracing?: bool - - // Optional list of RBAC RoleRegistrations. - // Describes and organizes the default permissions associated with any of the Grafana basic roles, - // which characterizes what viewers, editors, admins, or grafana admins can do on the plugin. - // The Admin basic role inherits its default permissions from the Editor basic role which in turn - // inherits them from the Viewer basic role. - roles?: [...#RoleRegistration] - - // RoleRegistration describes an RBAC role and its assignments to basic roles. - // It organizes related RBAC permissions on the plugin into a role and defines which basic roles - // will get them by default. - // Example: the role 'Schedules Reader' bundles permissions to view all schedules of the plugin - // which will be granted to Admins by default. - #RoleRegistration: { - // RBAC role definition to bundle related RBAC permissions on the plugin. - role: #Role - - // Default assignment of the role to Grafana basic roles (Viewer, Editor, Admin, Grafana Admin) - // The Admin basic role inherits its default permissions from the Editor basic role which in turn - // inherits them from the Viewer basic role. - grants: [...#BasicRole] - } - - // Role describes an RBAC role which allows grouping multiple related permissions on the plugin, - // each of which has an action and an optional scope. - // Example: the role 'Schedules Reader' bundles permissions to view all schedules of the plugin. - #Role: { - name: string - name: =~"^([A-Z][0-9A-Za-z ]+)$" - description: string - permissions: [...#Permission] - } - - // Permission describes an RBAC permission on the plugin. A permission has an action and an optional - // scope. - // Example: action: 'test-app.schedules:read', scope: 'test-app.schedules:*' - #Permission: { - action: string - scope?: string - } - - // BasicRole is a Grafana basic role, which can be 'Viewer', 'Editor', 'Admin' or 'Grafana Admin'. - // With RBAC, the Admin basic role inherits its default permissions from the Editor basic role which - // in turn inherits them from the Viewer basic role. - #BasicRole: "Grafana Admin" | "Admin" | "Editor" | "Viewer" - - // Header describes an HTTP header that is forwarded with a proxied request for - // a plugin route. - #Header: { - name: string - content: string - } - - // URLParam describes query string parameters for - // a url in a plugin route - #URLParam: { - name: string - content: string - } - - // A proxy route used in datasource plugins for plugin authentication - // and adding headers to HTTP requests made by the plugin. - // For more information, refer to [Authentication for data source - // plugins](https://grafana.com/developers/plugin-tools/create-a-plugin/extend-a-plugin/add-authentication-for-data-source-plugins). - #Route: { - // For data source plugins. The route path that is replaced by the - // route URL field when proxying the call. - path?: string - - // For data source plugins. Route method matches the HTTP verb - // like GET or POST. Multiple methods can be provided as a - // comma-separated list. - method?: string - - // For data source plugins. Route URL is where the request is - // proxied to. - url?: string - - urlParams?: [...#URLParam] - reqSignedIn?: bool - reqRole?: string - - // RBAC action the user must have to access the route. i.e. plugin-id.projects:read - reqAction?: string - - // For data source plugins. Route headers adds HTTP headers to the - // proxied request. - headers?: [...#Header] - - // For data source plugins. Route headers set the body content and - // length to the proxied request. - body?: { - ... - } - - // For data source plugins. Token authentication section used with - // an OAuth API. - tokenAuth?: #TokenAuth - - // For data source plugins. Token authentication section used with - // an JWT OAuth API. - jwtTokenAuth?: #JWTTokenAuth - } - - // TODO docs - #TokenAuth: { - // URL to fetch the authentication token. - url?: string - - // The list of scopes that your application should be granted - // access to. - scopes?: [...string] - - // Parameters for the token authentication request. - params: [string]: string - } - - // TODO docs - // TODO should this really be separate from TokenAuth? - #JWTTokenAuth: { - // URL to fetch the JWT token. - url: string - - // The list of scopes that your application should be granted - // access to. - scopes: [...string] - - // Parameters for the JWT token authentication request. - params: [string]: string - } - - // Identity and Access Management information. - // Allows the plugin to define the permissions it requires to have on Grafana. - iam: #IAM - - // IAM allows the plugin to get a service account with tailored permissions and a token - // (or to use the client_credentials grant if the token provider is the OAuth2 Server) - #IAM: { - // Permissions are the permissions that the external service needs its associated service account to have. - permissions?: [...#Permission] - } - } -}] -lenses: [] diff --git a/pkg/plugins/plugindef/plugindef.go b/pkg/plugins/plugindef/plugindef.go deleted file mode 100644 index f49a73637c..0000000000 --- a/pkg/plugins/plugindef/plugindef.go +++ /dev/null @@ -1,73 +0,0 @@ -package plugindef - -import ( - "strings" - "sync" - - "cuelang.org/go/cue/build" - "github.com/grafana/grafana/pkg/cuectx" - "github.com/grafana/thema" -) - -//go:generate go run gen.go - -func loadInstanceForplugindef() (*build.Instance, error) { - return cuectx.LoadGrafanaInstance("pkg/plugins/plugindef", "", nil) -} - -var linonce sync.Once -var pdlin thema.ConvergentLineage[*PluginDef] -var pdlinerr error - -// Lineage returns the [thema.ConvergentLineage] for plugindef, the canonical -// specification for Grafana plugin.json files. -// -// Unless a custom thema.Runtime is specifically needed, prefer calling this with -// nil, as a cached lineage will be returned. -func Lineage(rt *thema.Runtime, opts ...thema.BindOption) (thema.ConvergentLineage[*PluginDef], error) { - if len(opts) == 0 && (rt == nil || rt == cuectx.GrafanaThemaRuntime()) { - linonce.Do(func() { - pdlin, pdlinerr = doLineage(rt) - }) - return pdlin, pdlinerr - } - return doLineage(rt, opts...) -} - -// DerivePascalName derives a PascalCase name from a PluginDef. -// -// This function does not mutate the input PluginDef; as such, it ignores -// whether there exists any value for PluginDef.PascalName. -// -// FIXME this should be removable once CUE logic for it works/unmarshals correctly. -func DerivePascalName(pd PluginDef) string { - sani := func(s string) string { - ret := strings.Title(strings.Map(func(r rune) rune { - switch { - case r >= 'a' && r <= 'z': - return r - case r >= 'A' && r <= 'Z': - return r - default: - return -1 - } - }, strings.Title(strings.Map(func(r rune) rune { - switch r { - case '-', '_': - return ' ' - default: - return r - } - }, s)))) - if len(ret) > 63 { - return ret[:63] - } - return ret - } - - fromname := sani(pd.Name) - if len(fromname) != 0 { - return fromname - } - return sani(strings.Split(pd.Id, "-")[1]) -} diff --git a/pkg/plugins/plugindef/plugindef_bindings_gen.go b/pkg/plugins/plugindef/plugindef_bindings_gen.go deleted file mode 100644 index 3438f6896a..0000000000 --- a/pkg/plugins/plugindef/plugindef_bindings_gen.go +++ /dev/null @@ -1,84 +0,0 @@ -// Code generated - EDITING IS FUTILE. DO NOT EDIT. -// -// Generated by: -// pkg/plugins/plugindef/gen.go -// Using jennies: -// PluginGoBindings -// -// Run 'make gen-cue' from repository root to regenerate. - -package plugindef - -import ( - "cuelang.org/go/cue/build" - "github.com/grafana/thema" -) - -// doLineage returns a [thema.ConvergentLineage] for the 'plugindef' Thema lineage. -// -// The lineage is the canonical specification of plugindef. It contains all -// schema versions that have ever existed for plugindef, and the lenses that -// allow valid instances of one schema in the lineage to be translated to -// another schema in the lineage. -// -// As a [thema.ConvergentLineage], the returned lineage has one primary schema, 0.0, -// which is [thema.AssignableTo] [*PluginDef], the lineage's parameterized type. -// -// This function will return an error if the [Thema invariants] are not met by -// the underlying lineage declaration in CUE, or if [*PluginDef] is not -// [thema.AssignableTo] the 0.0 schema. -// -// [Thema's general invariants]: https://github.com/grafana/thema/blob/main/docs/invariants.md -func doLineage(rt *thema.Runtime, opts ...thema.BindOption) (thema.ConvergentLineage[*PluginDef], error) { - lin, err := baseLineage(rt, opts...) - if err != nil { - return nil, err - } - - sch := thema.SchemaP(lin, thema.SV(0, 0)) - typ := new(PluginDef) - tsch, err := thema.BindType(sch, typ) - if err != nil { - // This will error out if the 0.0 schema isn't assignable to - // *PluginDef. If Thema also generates that type, this should be unreachable, - // barring a critical bug in Thema's Go generator. - return nil, err - } - return tsch.ConvergentLineage(), nil -} -func baseLineage(rt *thema.Runtime, opts ...thema.BindOption) (thema.Lineage, error) { - // First, we must get the bytes of the .cue file(s) in which the "plugindef" lineage - // is declared, and load them into a - // "cuelang.org/go/cue/build".Instance. - // - // For most Thema-based development workflows, these bytes should come from an embed.FS. - // This ensures Go is always compiled with the current state of the .cue files. - var inst *build.Instance - var err error - - // loadInstanceForplugindef must be manually implemented in another file in this - // Go package. - inst, err = loadInstanceForplugindef() - if err != nil { - // Errors at this point indicate a problem with basic loading of .cue file bytes, - // which typically means the code generator was misconfigured and a path input - // is incorrect. - return nil, err - } - - raw := rt.Context().BuildInstance(inst) - - // An error returned from thema.BindLineage indicates one of the following: - // - The parsed path does not exist in the loaded CUE file (["github.com/grafana/thema/errors".ErrValueNotExist]) - // - The value at the parsed path exists, but does not appear to be a Thema - // lineage (["github.com/grafana/thema/errors".ErrValueNotALineage]) - // - The value at the parsed path exists and is a lineage (["github.com/grafana/thema/errors".ErrInvalidLineage]), - // but is invalid due to the violation of some general Thema invariant - - // for example, declared schemas don't follow backwards compatibility rules, - // lenses are incomplete. - return thema.BindLineage(raw, rt, opts...) -} - -// type guards -var _ thema.ConvergentLineageFactory[*PluginDef] = doLineage -var _ thema.LineageFactory = baseLineage diff --git a/pkg/plugins/plugindef/plugindef_types_gen.go b/pkg/plugins/plugindef/plugindef_types_gen.go deleted file mode 100644 index 1b5cb5e152..0000000000 --- a/pkg/plugins/plugindef/plugindef_types_gen.go +++ /dev/null @@ -1,484 +0,0 @@ -// Code generated - EDITING IS FUTILE. DO NOT EDIT. -// -// Generated by: -// pkg/plugins/plugindef/gen.go -// Using jennies: -// GoTypesJenny -// -// Run 'make gen-cue' from repository root to regenerate. - -package plugindef - -// Defines values for BasicRole. -const ( - BasicRoleAdmin BasicRole = "Admin" - BasicRoleEditor BasicRole = "Editor" - BasicRoleGrafanaAdmin BasicRole = "Grafana Admin" - BasicRoleViewer BasicRole = "Viewer" -) - -// Defines values for DependencyType. -const ( - DependencyTypeApp DependencyType = "app" - DependencyTypeDatasource DependencyType = "datasource" - DependencyTypePanel DependencyType = "panel" -) - -// Defines values for IncludeRole. -const ( - IncludeRoleAdmin IncludeRole = "Admin" - IncludeRoleEditor IncludeRole = "Editor" - IncludeRoleViewer IncludeRole = "Viewer" -) - -// Defines values for IncludeType. -const ( - IncludeTypeApp IncludeType = "app" - IncludeTypeDashboard IncludeType = "dashboard" - IncludeTypeDatasource IncludeType = "datasource" - IncludeTypePage IncludeType = "page" - IncludeTypePanel IncludeType = "panel" - IncludeTypeRenderer IncludeType = "renderer" - IncludeTypeSecretsmanager IncludeType = "secretsmanager" -) - -// Defines values for Category. -const ( - CategoryCloud Category = "cloud" - CategoryEnterprise Category = "enterprise" - CategoryIot Category = "iot" - CategoryLogging Category = "logging" - CategoryOther Category = "other" - CategoryProfiling Category = "profiling" - CategorySql Category = "sql" - CategoryTracing Category = "tracing" - CategoryTsdb Category = "tsdb" -) - -// Defines values for Type. -const ( - TypeApp Type = "app" - TypeDatasource Type = "datasource" - TypePanel Type = "panel" - TypeRenderer Type = "renderer" - TypeSecretsmanager Type = "secretsmanager" -) - -// Defines values for ReleaseState. -const ( - ReleaseStateAlpha ReleaseState = "alpha" - ReleaseStateBeta ReleaseState = "beta" - ReleaseStateDeprecated ReleaseState = "deprecated" - ReleaseStateStable ReleaseState = "stable" -) - -// BasicRole is a Grafana basic role, which can be 'Viewer', 'Editor', 'Admin' or 'Grafana Admin'. -// With RBAC, the Admin basic role inherits its default permissions from the Editor basic role which -// in turn inherits them from the Viewer basic role. -type BasicRole string - -// BuildInfo defines model for BuildInfo. -type BuildInfo struct { - // Git branch the plugin was built from - Branch *string `json:"branch,omitempty"` - - // Git hash of the commit the plugin was built from - Hash *string `json:"hash,omitempty"` - Number *int64 `json:"number,omitempty"` - - // GitHub pull request the plugin was built from - Pr *int32 `json:"pr,omitempty"` - Repo *string `json:"repo,omitempty"` - - // Time when the plugin was built, as a Unix timestamp - Time *int64 `json:"time,omitempty"` -} - -// Dependencies defines model for Dependencies. -type Dependencies struct { - // Required Grafana version for this plugin. Validated using - // https://github.com/npm/node-semver. - GrafanaDependency *string `json:"grafanaDependency,omitempty"` - - // (Deprecated) Required Grafana version for this plugin, e.g. - // `6.x.x 7.x.x` to denote plugin requires Grafana v6.x.x or - // v7.x.x. - GrafanaVersion *string `json:"grafanaVersion,omitempty"` - - // An array of required plugins on which this plugin depends - Plugins []Dependency `json:"plugins,omitempty"` -} - -// Dependency describes another plugin on which a plugin depends. -// The id refers to the plugin package identifier, as given on -// the grafana.com plugin marketplace. -type Dependency struct { - Id string `json:"id"` - Name string `json:"name"` - Type DependencyType `json:"type"` - Version string `json:"version"` -} - -// DependencyType defines model for Dependency.Type. -type DependencyType string - -// Header describes an HTTP header that is forwarded with a proxied request for -// a plugin route. -type Header struct { - Content string `json:"content"` - Name string `json:"name"` -} - -// IAM allows the plugin to get a service account with tailored permissions and a token -// (or to use the client_credentials grant if the token provider is the OAuth2 Server) -type IAM struct { - // Permissions are the permissions that the external service needs its associated service account to have. - Permissions []Permission `json:"permissions,omitempty"` -} - -// A resource to be included in a plugin. -type Include struct { - // RBAC action the user must have to access the route - Action *string `json:"action,omitempty"` - - // Add the include to the navigation menu. - AddToNav *bool `json:"addToNav,omitempty"` - - // (Legacy) The Angular component to use for a page. - Component *string `json:"component,omitempty"` - - // Page or dashboard when user clicks the icon in the side menu. - DefaultNav *bool `json:"defaultNav,omitempty"` - - // Icon to use in the side menu. For information on available - // icon, refer to [Icons - // Overview](https://developers.grafana.com/ui/latest/index.html?path=/story/docs-overview-icon--icons-overview). - Icon *string `json:"icon,omitempty"` - Name *string `json:"name,omitempty"` - - // Used for app plugins. - Path *string `json:"path,omitempty"` - - // The minimum role a user must have to see this page in the navigation menu. - Role *IncludeRole `json:"role,omitempty"` - - // IncludeType is a string identifier of a plugin include type, which is - // a superset of plugin types. - Type IncludeType `json:"type"` - - // Unique identifier of the included resource - Uid *string `json:"uid,omitempty"` -} - -// The minimum role a user must have to see this page in the navigation menu. -type IncludeRole string - -// IncludeType is a string identifier of a plugin include type, which is -// a superset of plugin types. -type IncludeType string - -// Metadata about a Grafana plugin. Some fields are used on the plugins -// page in Grafana and others on grafana.com, if the plugin is published. -type Info struct { - // Information about the plugin author - Author *struct { - // Author's name - Email *string `json:"email,omitempty"` - - // Author's name - Name *string `json:"name,omitempty"` - - // Link to author's website - Url *string `json:"url,omitempty"` - } `json:"author,omitempty"` - Build *BuildInfo `json:"build,omitempty"` - - // Description of plugin. Used on the plugins page in Grafana and - // for search on grafana.com. - Description *string `json:"description,omitempty"` - - // Array of plugin keywords. Used for search on grafana.com. - Keywords []string `json:"keywords"` - - // An array of link objects to be displayed on this plugin's - // project page in the form `{name: 'foo', url: - // 'http://example.com'}` - Links []struct { - Name *string `json:"name,omitempty"` - Url *string `json:"url,omitempty"` - } `json:"links,omitempty"` - - // SVG images that are used as plugin icons - Logos *struct { - // Link to the "large" version of the plugin logo, which must be - // an SVG image. "Large" and "small" logos can be the same image. - Large string `json:"large"` - - // Link to the "small" version of the plugin logo, which must be - // an SVG image. "Large" and "small" logos can be the same image. - Small string `json:"small"` - } `json:"logos,omitempty"` - - // An array of screenshot objects in the form `{name: 'bar', path: - // 'img/screenshot.png'}` - Screenshots []struct { - Name *string `json:"name,omitempty"` - Path *string `json:"path,omitempty"` - } `json:"screenshots,omitempty"` - - // Date when this plugin was built - Updated *string `json:"updated,omitempty"` - - // Project version of this commit, e.g. `6.7.x` - Version *string `json:"version,omitempty"` -} - -// TODO docs -// TODO should this really be separate from TokenAuth? -type JWTTokenAuth struct { - // Parameters for the JWT token authentication request. - Params map[string]string `json:"params"` - - // The list of scopes that your application should be granted - // access to. - Scopes []string `json:"scopes"` - - // URL to fetch the JWT token. - Url string `json:"url"` -} - -// Permission describes an RBAC permission on the plugin. A permission has an action and an optional -// scope. -// Example: action: 'test-app.schedules:read', scope: 'test-app.schedules:*' -type Permission struct { - Action string `json:"action"` - Scope *string `json:"scope,omitempty"` -} - -// PluginDef defines model for PluginDef. -type PluginDef struct { - // Schema definition for the plugin.json file. Used primarily for schema validation. - Schema *string `json:"$schema,omitempty"` - - // For data source plugins, if the plugin supports alerting. Requires `backend` to be set to `true`. - Alerting *bool `json:"alerting,omitempty"` - - // An alias is useful when migrating from one plugin id to another (rebranding etc) - // This should be used sparingly, and is currently only supported though a hardcoded checklist - AliasIDs []string `json:"aliasIDs,omitempty"` - - // For data source plugins, if the plugin supports annotation - // queries. - Annotations *bool `json:"annotations,omitempty"` - - // Set to true for app plugins that should be enabled and pinned to the navigation bar in all orgs. - AutoEnabled *bool `json:"autoEnabled,omitempty"` - - // If the plugin has a backend component. - Backend *bool `json:"backend,omitempty"` - - // [internal only] Indicates whether the plugin is developed and shipped as part - // of Grafana. Also known as a 'core plugin'. - BuiltIn bool `json:"builtIn"` - - // Plugin category used on the Add data source page. - Category *Category `json:"category,omitempty"` - Dependencies Dependencies `json:"dependencies"` - - // Grafana Enterprise specific features. - EnterpriseFeatures *struct { - // Enable/Disable health diagnostics errors. Requires Grafana - // >=7.5.5. - HealthDiagnosticsErrors *bool `json:"healthDiagnosticsErrors,omitempty"` - } `json:"enterpriseFeatures,omitempty"` - - // The first part of the file name of the backend component - // executable. There can be multiple executables built for - // different operating system and architecture. Grafana will - // check for executables named `_<$GOOS>_<.exe for Windows>`, e.g. `plugin_linux_amd64`. - // Combination of $GOOS and $GOARCH can be found here: - // https://golang.org/doc/install/source#environment. - Executable *string `json:"executable,omitempty"` - - // [internal only] Excludes the plugin from listings in Grafana's UI. Only - // allowed for `builtIn` plugins. - HideFromList bool `json:"hideFromList"` - - // IAM allows the plugin to get a service account with tailored permissions and a token - // (or to use the client_credentials grant if the token provider is the OAuth2 Server) - Iam IAM `json:"iam"` - - // Unique name of the plugin. If the plugin is published on - // grafana.com, then the plugin `id` has to follow the naming - // conventions. - Id string `json:"id"` - - // Resources to include in plugin. - Includes []Include `json:"includes,omitempty"` - - // Metadata about a Grafana plugin. Some fields are used on the plugins - // page in Grafana and others on grafana.com, if the plugin is published. - Info Info `json:"info"` - - // For data source plugins, if the plugin supports logs. It may be used to filter logs only features. - Logs *bool `json:"logs,omitempty"` - - // For data source plugins, if the plugin supports metric queries. - // Used to enable the plugin in the panel editor. - Metrics *bool `json:"metrics,omitempty"` - - // Human-readable name of the plugin that is shown to the user in - // the UI. - Name string `json:"name"` - - // [internal only] The PascalCase name for the plugin. Used for creating machine-friendly - // identifiers, typically in code generation. - // - // If not provided, defaults to name, but title-cased and sanitized (only - // alphabetical characters allowed). - PascalName string `json:"pascalName"` - - // Initialize plugin on startup. By default, the plugin - // initializes on first use. - Preload *bool `json:"preload,omitempty"` - - // For data source plugins. There is a query options section in - // the plugin's query editor and these options can be turned on - // if needed. - QueryOptions *struct { - // For data source plugins. If the `cache timeout` option should - // be shown in the query options section in the query editor. - CacheTimeout *bool `json:"cacheTimeout,omitempty"` - - // For data source plugins. If the `max data points` option should - // be shown in the query options section in the query editor. - MaxDataPoints *bool `json:"maxDataPoints,omitempty"` - - // For data source plugins. If the `min interval` option should be - // shown in the query options section in the query editor. - MinInterval *bool `json:"minInterval,omitempty"` - } `json:"queryOptions,omitempty"` - - // Optional list of RBAC RoleRegistrations. - // Describes and organizes the default permissions associated with any of the Grafana basic roles, - // which characterizes what viewers, editors, admins, or grafana admins can do on the plugin. - // The Admin basic role inherits its default permissions from the Editor basic role which in turn - // inherits them from the Viewer basic role. - Roles []RoleRegistration `json:"roles,omitempty"` - - // Routes is a list of proxy routes, if any. For datasource plugins only. - Routes []Route `json:"routes,omitempty"` - - // For panel plugins. Hides the query editor. - SkipDataQuery *bool `json:"skipDataQuery,omitempty"` - - // ReleaseState indicates release maturity state of a plugin. - State *ReleaseState `json:"state,omitempty"` - - // For data source plugins, if the plugin supports streaming. Used in Explore to start live streaming. - Streaming *bool `json:"streaming,omitempty"` - - // For data source plugins, if the plugin supports tracing. Used for example to link logs (e.g. Loki logs) with tracing plugins. - Tracing *bool `json:"tracing,omitempty"` - - // type indicates which type of Grafana plugin this is, of the defined - // set of Grafana plugin types. - Type Type `json:"type"` -} - -// Plugin category used on the Add data source page. -type Category string - -// Type type indicates which type of Grafana plugin this is, of the defined -// set of Grafana plugin types. -type Type string - -// ReleaseState indicates release maturity state of a plugin. -type ReleaseState string - -// Role describes an RBAC role which allows grouping multiple related permissions on the plugin, -// each of which has an action and an optional scope. -// Example: the role 'Schedules Reader' bundles permissions to view all schedules of the plugin. -type Role struct { - Description string `json:"description"` - Name string `json:"name"` - Permissions []Permission `json:"permissions"` -} - -// RoleRegistration describes an RBAC role and its assignments to basic roles. -// It organizes related RBAC permissions on the plugin into a role and defines which basic roles -// will get them by default. -// Example: the role 'Schedules Reader' bundles permissions to view all schedules of the plugin -// which will be granted to Admins by default. -type RoleRegistration struct { - // Default assignment of the role to Grafana basic roles (Viewer, Editor, Admin, Grafana Admin) - // The Admin basic role inherits its default permissions from the Editor basic role which in turn - // inherits them from the Viewer basic role. - Grants []BasicRole `json:"grants"` - - // Role describes an RBAC role which allows grouping multiple related permissions on the plugin, - // each of which has an action and an optional scope. - // Example: the role 'Schedules Reader' bundles permissions to view all schedules of the plugin. - Role Role `json:"role"` -} - -// A proxy route used in datasource plugins for plugin authentication -// and adding headers to HTTP requests made by the plugin. -// For more information, refer to [Authentication for data source -// plugins](https://grafana.com/developers/plugin-tools/create-a-plugin/extend-a-plugin/add-authentication-for-data-source-plugins). -type Route struct { - // For data source plugins. Route headers set the body content and - // length to the proxied request. - Body map[string]any `json:"body,omitempty"` - - // For data source plugins. Route headers adds HTTP headers to the - // proxied request. - Headers []Header `json:"headers,omitempty"` - - // TODO docs - // TODO should this really be separate from TokenAuth? - JwtTokenAuth *JWTTokenAuth `json:"jwtTokenAuth,omitempty"` - - // For data source plugins. Route method matches the HTTP verb - // like GET or POST. Multiple methods can be provided as a - // comma-separated list. - Method *string `json:"method,omitempty"` - - // For data source plugins. The route path that is replaced by the - // route URL field when proxying the call. - Path *string `json:"path,omitempty"` - - // RBAC action the user must have to access the route. i.e. plugin-id.projects:read - ReqAction *string `json:"reqAction,omitempty"` - ReqRole *string `json:"reqRole,omitempty"` - ReqSignedIn *bool `json:"reqSignedIn,omitempty"` - - // TODO docs - TokenAuth *TokenAuth `json:"tokenAuth,omitempty"` - - // For data source plugins. Route URL is where the request is - // proxied to. - Url *string `json:"url,omitempty"` - UrlParams []URLParam `json:"urlParams,omitempty"` -} - -// TODO docs -type TokenAuth struct { - // Parameters for the token authentication request. - Params map[string]string `json:"params"` - - // The list of scopes that your application should be granted - // access to. - Scopes []string `json:"scopes,omitempty"` - - // URL to fetch the authentication token. - Url *string `json:"url,omitempty"` -} - -// URLParam describes query string parameters for -// a url in a plugin route -type URLParam struct { - Content string `json:"content"` - Name string `json:"name"` -} diff --git a/pkg/plugins/plugins.go b/pkg/plugins/plugins.go index 3e98bf1344..15e43ad07d 100644 --- a/pkg/plugins/plugins.go +++ b/pkg/plugins/plugins.go @@ -19,7 +19,7 @@ import ( "github.com/grafana/grafana/pkg/plugins/backendplugin/pluginextensionv2" "github.com/grafana/grafana/pkg/plugins/backendplugin/secretsmanagerplugin" "github.com/grafana/grafana/pkg/plugins/log" - "github.com/grafana/grafana/pkg/plugins/plugindef" + "github.com/grafana/grafana/pkg/plugins/pfs" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/util" ) @@ -118,7 +118,7 @@ type JSONData struct { Executable string `json:"executable,omitempty"` // App Service Auth Registration - IAM *plugindef.IAM `json:"iam,omitempty"` + IAM *pfs.IAM `json:"iam,omitempty"` } func ReadPluginJSON(reader io.Reader) (JSONData, error) { diff --git a/pkg/services/pluginsintegration/loader/loader_test.go b/pkg/services/pluginsintegration/loader/loader_test.go index 0b8976f45c..c2327558f8 100644 --- a/pkg/services/pluginsintegration/loader/loader_test.go +++ b/pkg/services/pluginsintegration/loader/loader_test.go @@ -24,7 +24,7 @@ import ( "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/plugindef" + "github.com/grafana/grafana/pkg/plugins/pfs" "github.com/grafana/grafana/pkg/plugins/pluginscdn" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/org" @@ -541,8 +541,8 @@ func TestLoader_Load_ExternalRegistration(t *testing.T) { GrafanaVersion: "*", Plugins: []plugins.Dependency{}, }, - IAM: &plugindef.IAM{ - Permissions: []plugindef.Permission{ + IAM: &pfs.IAM{ + Permissions: []pfs.Permission{ { Action: "read", Scope: stringPtr("datasource"), diff --git a/pkg/services/pluginsintegration/pipeline/steps.go b/pkg/services/pluginsintegration/pipeline/steps.go index d5408d0224..ea798c2dd2 100644 --- a/pkg/services/pluginsintegration/pipeline/steps.go +++ b/pkg/services/pluginsintegration/pipeline/steps.go @@ -14,7 +14,7 @@ import ( "github.com/grafana/grafana/pkg/plugins/manager/pipeline/validation" "github.com/grafana/grafana/pkg/plugins/manager/registry" "github.com/grafana/grafana/pkg/plugins/manager/signature" - "github.com/grafana/grafana/pkg/plugins/plugindef" + "github.com/grafana/grafana/pkg/plugins/pfs" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginerrs" ) @@ -42,7 +42,7 @@ func newExternalServiceRegistration(cfg *config.PluginManagementCfg, serviceRegi // Register registers the external service with the external service registry, if the feature is enabled. func (r *ExternalServiceRegistration) Register(ctx context.Context, p *plugins.Plugin) (*plugins.Plugin, error) { if p.IAM != nil { - s, err := r.externalServiceRegistry.RegisterExternalService(ctx, p.ID, plugindef.Type(p.Type), p.IAM) + s, err := r.externalServiceRegistry.RegisterExternalService(ctx, p.ID, pfs.Type(p.Type), p.IAM) if err != nil { r.log.Error("Could not register an external service. Initialization skipped", "pluginId", p.ID, "error", err) return nil, err diff --git a/pkg/services/pluginsintegration/pluginconfig/envvars_test.go b/pkg/services/pluginsintegration/pluginconfig/envvars_test.go index a3bf9025df..1a967b8a14 100644 --- a/pkg/services/pluginsintegration/pluginconfig/envvars_test.go +++ b/pkg/services/pluginsintegration/pluginconfig/envvars_test.go @@ -17,7 +17,7 @@ import ( "github.com/grafana/grafana/pkg/plugins/config" "github.com/grafana/grafana/pkg/plugins/envvars" "github.com/grafana/grafana/pkg/plugins/manager/fakes" - "github.com/grafana/grafana/pkg/plugins/plugindef" + "github.com/grafana/grafana/pkg/plugins/pfs" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/setting" ) @@ -590,7 +590,7 @@ func TestPluginEnvVarsProvider_authEnvVars(t *testing.T) { p := &plugins.Plugin{ JSONData: plugins.JSONData{ ID: "test", - IAM: &plugindef.IAM{}, + IAM: &pfs.IAM{}, }, ExternalService: &auth.ExternalService{ ClientID: "clientID", diff --git a/pkg/services/pluginsintegration/serviceregistration/serviceregistration.go b/pkg/services/pluginsintegration/serviceregistration/serviceregistration.go index e5b7c19d6f..0f7ad32115 100644 --- a/pkg/services/pluginsintegration/serviceregistration/serviceregistration.go +++ b/pkg/services/pluginsintegration/serviceregistration/serviceregistration.go @@ -7,7 +7,7 @@ import ( "github.com/grafana/grafana/pkg/plugins/auth" "github.com/grafana/grafana/pkg/plugins/config" "github.com/grafana/grafana/pkg/plugins/log" - "github.com/grafana/grafana/pkg/plugins/plugindef" + "github.com/grafana/grafana/pkg/plugins/pfs" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/extsvcauth" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -41,7 +41,7 @@ func (s *Service) HasExternalService(ctx context.Context, pluginID string) (bool } // RegisterExternalService is a simplified wrapper around SaveExternalService for the plugin use case. -func (s *Service) RegisterExternalService(ctx context.Context, pluginID string, pType plugindef.Type, svc *plugindef.IAM) (*auth.ExternalService, error) { +func (s *Service) RegisterExternalService(ctx context.Context, pluginID string, pType pfs.Type, svc *pfs.IAM) (*auth.ExternalService, error) { if !s.featureEnabled { s.log.Warn("Skipping External Service Registration. The feature is behind a feature toggle and needs to be enabled.") return nil, nil @@ -50,7 +50,7 @@ func (s *Service) RegisterExternalService(ctx context.Context, pluginID string, // Datasource plugins can only be enabled enabled := true // App plugins can be disabled - if pType == plugindef.TypeApp { + if pType == pfs.TypeApp { settings, err := s.settingsSvc.GetPluginSettingByPluginID(ctx, &pluginsettings.GetByPluginIDArgs{PluginID: pluginID}) if err != nil && !errors.Is(err, pluginsettings.ErrPluginSettingNotFound) { return nil, err @@ -86,7 +86,7 @@ func (s *Service) RegisterExternalService(ctx context.Context, pluginID string, PrivateKey: privateKey}, nil } -func toAccessControlPermissions(ps []plugindef.Permission) []accesscontrol.Permission { +func toAccessControlPermissions(ps []pfs.Permission) []accesscontrol.Permission { res := make([]accesscontrol.Permission, 0, len(ps)) for _, p := range ps { scope := "" diff --git a/public/app/plugins/gen.go b/public/app/plugins/gen.go index 558e503245..7597595525 100644 --- a/public/app/plugins/gen.go +++ b/public/app/plugins/gen.go @@ -14,12 +14,11 @@ import ( "strings" "github.com/grafana/codejen" - "github.com/grafana/kindsys" - corecodegen "github.com/grafana/grafana/pkg/codegen" "github.com/grafana/grafana/pkg/cuectx" "github.com/grafana/grafana/pkg/plugins/codegen" "github.com/grafana/grafana/pkg/plugins/pfs" + "github.com/grafana/kindsys" "github.com/grafana/thema" ) @@ -86,7 +85,7 @@ func adaptToPipeline(j codejen.OneToOne[corecodegen.SchemaForGen]) codejen.OneTo return corecodegen.SchemaForGen{ Name: strings.ReplaceAll(pd.PluginMeta.Name, " ", ""), Schema: pd.Lineage.Latest(), - IsGroup: pd.SchemaInterface.IsGroup(), + IsGroup: pd.SchemaInterface.IsGroup, } }) } From a4acd9d204f3b9560bab2b344dd0e64be41de747 Mon Sep 17 00:00:00 2001 From: Konrad Lalik Date: Thu, 7 Mar 2024 12:30:37 +0100 Subject: [PATCH 0461/1406] Alerting: Improve alert list panel and alert rules toolbar permissions handling (#83954) * Improve alert list panel and alert rules toolbar permissions handling * Refactor permission checking, add tests * Remove unneccessary act wrapper * Fix test error --- .../alerting/unified/initAlerting.tsx | 26 +++++++++----- .../panel/alertlist/UnifiedAlertList.tsx | 31 +++++++++------- .../panel/alertlist/UnifiedalertList.test.tsx | 36 ++++++++++--------- public/app/plugins/panel/alertlist/module.tsx | 4 +-- 4 files changed, 57 insertions(+), 40 deletions(-) diff --git a/public/app/features/alerting/unified/initAlerting.tsx b/public/app/features/alerting/unified/initAlerting.tsx index 7ae82186e5..e043057330 100644 --- a/public/app/features/alerting/unified/initAlerting.tsx +++ b/public/app/features/alerting/unified/initAlerting.tsx @@ -1,21 +1,29 @@ import React from 'react'; import { config } from '@grafana/runtime'; +import { contextSrv } from 'app/core/services/context_srv'; import { addCustomRightAction } from '../../dashboard/components/DashNav/DashNav'; +import { getRulesPermissions } from './utils/access-control'; +import { GRAFANA_RULES_SOURCE_NAME } from './utils/datasource'; + const AlertRulesToolbarButton = React.lazy( () => import(/* webpackChunkName: "alert-rules-toolbar-button" */ './integration/AlertRulesToolbarButton') ); export function initAlerting() { - addCustomRightAction({ - show: () => config.unifiedAlertingEnabled || (config.featureToggles.alertingPreviewUpgrade ?? false), - component: ({ dashboard }) => ( - - {dashboard && } - - ), - index: -2, - }); + const grafanaRulesPermissions = getRulesPermissions(GRAFANA_RULES_SOURCE_NAME); + + if (contextSrv.hasPermission(grafanaRulesPermissions.read)) { + addCustomRightAction({ + show: () => config.unifiedAlertingEnabled || (config.featureToggles.alertingPreviewUpgrade ?? false), + component: ({ dashboard }) => ( + + {dashboard && } + + ), + index: -2, + }); + } } diff --git a/public/app/plugins/panel/alertlist/UnifiedAlertList.tsx b/public/app/plugins/panel/alertlist/UnifiedAlertList.tsx index 890f8b2383..c6676c6ad4 100644 --- a/public/app/plugins/panel/alertlist/UnifiedAlertList.tsx +++ b/public/app/plugins/panel/alertlist/UnifiedAlertList.tsx @@ -16,7 +16,6 @@ import { useStyles2, } from '@grafana/ui'; import { config } from 'app/core/config'; -import { contextSrv } from 'app/core/services/context_srv'; import alertDef from 'app/features/alerting/state/alertDef'; import { alertRuleApi } from 'app/features/alerting/unified/api/alertRuleApi'; import { INSTANCES_DISPLAY_LIMIT } from 'app/features/alerting/unified/components/rules/RuleDetails'; @@ -38,9 +37,10 @@ import { flattenCombinedRules, getFirstActiveAt } from 'app/features/alerting/un import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; import { DashboardModel } from 'app/features/dashboard/state'; import { Matcher } from 'app/plugins/datasource/alertmanager/types'; -import { AccessControlAction, ThunkDispatch, useDispatch } from 'app/types'; +import { ThunkDispatch, useDispatch } from 'app/types'; import { PromAlertingRuleState } from 'app/types/unified-alerting-dto'; +import { AlertingAction, useAlertingAbility } from '../../../features/alerting/unified/hooks/useAbilities'; import { getAlertingRule } from '../../../features/alerting/unified/utils/rules'; import { AlertingRule, CombinedRuleWithLocation } from '../../../types/unified-alerting'; @@ -93,9 +93,10 @@ const fetchPromAndRuler = ({ } }; -export function UnifiedAlertList(props: PanelProps) { +function UnifiedAlertList(props: PanelProps) { const dispatch = useDispatch(); const [limitInstances, toggleLimit] = useToggle(true); + const [, gmaViewAllowed] = useAlertingAbility(AlertingAction.ViewAlertRule); const { usePrometheusRulesByNamespaceQuery } = alertRuleApi; @@ -137,7 +138,7 @@ export function UnifiedAlertList(props: PanelProps) { // If the datasource is not defined we should NOT skip the query // Undefined dataSourceName means that there is no datasource filter applied and we should fetch all the rules - const shouldFetchGrafanaRules = !dataSourceName || dataSourceName === GRAFANA_RULES_SOURCE_NAME; + const shouldFetchGrafanaRules = (!dataSourceName || dataSourceName === GRAFANA_RULES_SOURCE_NAME) && gmaViewAllowed; //For grafana managed rules, get the result using RTK Query to avoid the need of using the redux store //See https://github.com/grafana/grafana/pull/70482 @@ -217,15 +218,6 @@ export function UnifiedAlertList(props: PanelProps) { const havePreviousResults = Object.values(promRulesRequests).some((state) => state.result); - if ( - !contextSrv.hasPermission(AccessControlAction.AlertingRuleRead) && - !contextSrv.hasPermission(AccessControlAction.AlertingRuleExternalRead) - ) { - return ( - Sorry, you do not have the required permissions to read alert rules - ); - } - return (
@@ -456,3 +448,16 @@ export const getStyles = (theme: GrafanaTheme2) => ({ display: none; `, }); + +export function UnifiedAlertListPanel(props: PanelProps) { + const [, gmaReadAllowed] = useAlertingAbility(AlertingAction.ViewAlertRule); + const [, externalReadAllowed] = useAlertingAbility(AlertingAction.ViewExternalAlertRule); + + if (!gmaReadAllowed && !externalReadAllowed) { + return ( + Sorry, you do not have the required permissions to read alert rules + ); + } + + return ; +} diff --git a/public/app/plugins/panel/alertlist/UnifiedalertList.test.tsx b/public/app/plugins/panel/alertlist/UnifiedalertList.test.tsx index 22d963ddf4..d4847dd828 100644 --- a/public/app/plugins/panel/alertlist/UnifiedalertList.test.tsx +++ b/public/app/plugins/panel/alertlist/UnifiedalertList.test.tsx @@ -2,7 +2,6 @@ import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; import { Provider } from 'react-redux'; -import { act } from 'react-test-renderer'; import { byRole, byText } from 'testing-library-selector'; import { FieldConfigSource, getDefaultTimeRange, LoadingState, PanelProps, PluginExtensionTypes } from '@grafana/data'; @@ -25,7 +24,7 @@ import { } from '../../../features/alerting/unified/mocks'; import { GRAFANA_RULES_SOURCE_NAME } from '../../../features/alerting/unified/utils/datasource'; -import { UnifiedAlertList } from './UnifiedAlertList'; +import { UnifiedAlertListPanel } from './UnifiedAlertList'; import { GroupMode, SortOrder, UnifiedAlertListOptions, ViewMode } from './types'; import * as utils from './util'; @@ -159,20 +158,20 @@ const renderPanel = (options: Partial = defaultOptions) return render( - + ); }; describe('UnifiedAlertList', () => { + jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(true); + it('subscribes to the dashboard refresh interval', async () => { jest.spyOn(defaultProps, 'replaceVariables').mockReturnValue('severity=critical'); - await act(async () => { - renderPanel(); - }); + renderPanel(); - expect(dashboard.events.subscribe).toHaveBeenCalledTimes(1); + await waitFor(() => expect(dashboard.events.subscribe).toHaveBeenCalledTimes(1)); expect(dashboard.events.subscribe.mock.calls[0][0]).toEqual(TimeRangeUpdatedEvent); }); @@ -180,21 +179,18 @@ describe('UnifiedAlertList', () => { await waitFor(() => { expect(screen.queryByText('Loading...')).not.toBeInTheDocument(); }); - jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(true); const filterAlertsSpy = jest.spyOn(utils, 'filterAlerts'); const replaceVarsSpy = jest.spyOn(defaultProps, 'replaceVariables').mockReturnValue('severity=critical'); const user = userEvent.setup(); - await act(async () => { - renderPanel({ - alertInstanceLabelFilter: '$label', - dashboardAlerts: false, - alertName: '', - datasource: GRAFANA_RULES_SOURCE_NAME, - folder: undefined, - }); + renderPanel({ + alertInstanceLabelFilter: '$label', + dashboardAlerts: false, + alertName: '', + datasource: GRAFANA_RULES_SOURCE_NAME, + folder: undefined, }); await waitFor(() => { @@ -222,4 +218,12 @@ describe('UnifiedAlertList', () => { expect.anything() ); }); + + it('should render authorization error when user has no permission', async () => { + jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(false); + + renderPanel(); + + expect(screen.getByRole('alert', { name: 'Permission required' })).toBeInTheDocument(); + }); }); diff --git a/public/app/plugins/panel/alertlist/module.tsx b/public/app/plugins/panel/alertlist/module.tsx index 8250c7fd4d..0bb5d10eee 100644 --- a/public/app/plugins/panel/alertlist/module.tsx +++ b/public/app/plugins/panel/alertlist/module.tsx @@ -17,7 +17,7 @@ import { GRAFANA_DATASOURCE_NAME } from '../../../features/alerting/unified/util import { AlertList } from './AlertList'; import { alertListPanelMigrationHandler } from './AlertListMigrationHandler'; import { GroupBy } from './GroupByWithLoading'; -import { UnifiedAlertList } from './UnifiedAlertList'; +import { UnifiedAlertListPanel } from './UnifiedAlertList'; import { AlertListSuggestionsSupplier } from './suggestions'; import { AlertListOptions, GroupMode, ShowOption, SortOrder, UnifiedAlertListOptions, ViewMode } from './types'; @@ -156,7 +156,7 @@ const alertList = new PanelPlugin(AlertList) .setMigrationHandler(alertListPanelMigrationHandler) .setSuggestionsSupplier(new AlertListSuggestionsSupplier()); -const unifiedAlertList = new PanelPlugin(UnifiedAlertList).setPanelOptions((builder) => { +const unifiedAlertList = new PanelPlugin(UnifiedAlertListPanel).setPanelOptions((builder) => { builder .addRadio({ path: 'viewMode', From 9c520acf9c6d7fe171515a53aa90eac0330de88e Mon Sep 17 00:00:00 2001 From: Pepe Cano <825430+ppcano@users.noreply.github.com> Date: Thu, 7 Mar 2024 12:33:16 +0100 Subject: [PATCH 0462/1406] Alerting: Minor changes to UI help text and descriptions (#84023) --- .../alerting/unified/components/receivers/TemplateForm.tsx | 2 +- .../unified/components/rule-editor/AnnotationsStep.tsx | 5 ----- .../alerting/unified/components/rule-editor/LabelsField.tsx | 2 +- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/public/app/features/alerting/unified/components/receivers/TemplateForm.tsx b/public/app/features/alerting/unified/components/receivers/TemplateForm.tsx index e5319d49b4..6ed886fc85 100644 --- a/public/app/features/alerting/unified/components/receivers/TemplateForm.tsx +++ b/public/app/features/alerting/unified/components/receivers/TemplateForm.tsx @@ -280,7 +280,7 @@ function TemplatingGuideline() {
- To make templating easier, we provide a few snippets in the content editor to help you speed up your workflow. + For auto-completion of common templating code, type the following keywords in the content editor:
{Object.values(snippets) .map((s) => s.label) diff --git a/public/app/features/alerting/unified/components/rule-editor/AnnotationsStep.tsx b/public/app/features/alerting/unified/components/rule-editor/AnnotationsStep.tsx index 010cba9f7a..8227b087bf 100644 --- a/public/app/features/alerting/unified/components/rule-editor/AnnotationsStep.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/AnnotationsStep.tsx @@ -89,9 +89,6 @@ const AnnotationsStep = () => { }; function getAnnotationsSectionDescription() { - const docsLink = - 'https://grafana.com/docs/grafana/latest/alerting/fundamentals/annotation-label/variables-label-annotation'; - return ( @@ -101,8 +98,6 @@ const AnnotationsStep = () => { contentText={`Annotations add metadata to provide more information on the alert in your alert notification messages. For example, add a Summary annotation to tell you which value caused the alert to fire or which server it happened on. Annotations can contain a combination of text and template code.`} - externalLink={docsLink} - linkText={`Read about annotations`} title="Annotations" /> diff --git a/public/app/features/alerting/unified/components/rule-editor/LabelsField.tsx b/public/app/features/alerting/unified/components/rule-editor/LabelsField.tsx index 907e3c4b53..4f510ed307 100644 --- a/public/app/features/alerting/unified/components/rule-editor/LabelsField.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/LabelsField.tsx @@ -284,7 +284,7 @@ const LabelsField: FC = ({ dataSourceName }) => { Labels - Add labels to your rule to annotate your rules, ease searching, or route to a notification policy. + Add labels to your rule for searching, silencing, or routing to a notification policy. > Timeseries panel\n\nKnown issues:\n* hiding null/empty series\n* time regions", "mode": "markdown" }, - "pluginVersion": "10.4.0-pre", + "pluginVersion": "11.0.0-pre", + "targets": [ + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "A" + } + ], + "title": "Status + Notes", + "type": "text" + }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "unit": "short" + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 11, + "w": 16, + "x": 0, + "y": 11 + }, + "hiddenSeries": false, + "id": 28, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": false, + "total": false, + "values": false + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "percentage": false, + "pluginVersion": "11.0.0-pre", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "A", + "scenarioId": "random_walk", + "seriesCount": 3 + } + ], + "thresholds": [], + "timeRegions": [], + "title": "Flot graph - x axis series mode", + "tooltip": { + "shared": false, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "mode": "series", + "show": true, + "values": [ + "total" + ] + }, + "yaxes": [ + { + "$$hashKey": "object:88", + "format": "short", + "logBase": 1, + "show": true + }, + { + "$$hashKey": "object:89", + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "gridPos": { + "h": 11, + "w": 8, + "x": 16, + "y": 11 + }, + "id": 29, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "# Graph panel >> Bar chart panel\n", + "mode": "markdown" + }, + "pluginVersion": "11.0.0-pre", + "targets": [ + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "A" + } + ], + "title": "Status + Notes", + "type": "text" + }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "unit": "short" + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 11, + "w": 16, + "x": 0, + "y": 22 + }, + "hiddenSeries": false, + "id": 30, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": false, + "total": false, + "values": false + }, + "lines": false, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "percentage": false, + "pluginVersion": "11.0.0-pre", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "A", + "scenarioId": "random_walk", + "seriesCount": 3 + } + ], + "thresholds": [], + "timeRegions": [], + "title": "Flot graph - x axis histogram mode", + "tooltip": { + "shared": false, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "mode": "histogram", + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:193", + "format": "short", + "logBase": 1, + "show": true + }, + { + "$$hashKey": "object:194", + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "gridPos": { + "h": 11, + "w": 8, + "x": 16, + "y": 22 + }, + "id": 31, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "# Graph panel >> Histogram panel\n", + "mode": "markdown" + }, + "pluginVersion": "11.0.0-pre", "targets": [ { "datasource": { @@ -335,7 +609,7 @@ "h": 10, "w": 16, "x": 0, - "y": 11 + "y": 33 }, "id": 2, "options": { @@ -411,7 +685,7 @@ "h": 10, "w": 8, "x": 16, - "y": 11 + "y": 33 }, "id": 7, "options": { @@ -423,7 +697,7 @@ "content": "# Table (old) >> Table\n\nKnown issues:\n* wrapping text\n* style changes", "mode": "markdown" }, - "pluginVersion": "10.4.0-pre", + "pluginVersion": "11.0.0-pre", "targets": [ { "datasource": { @@ -460,7 +734,7 @@ "h": 8, "w": 8, "x": 0, - "y": 21 + "y": 43 }, "id": 9, "mappingType": 1, @@ -517,64 +791,65 @@ "valueName": "avg" }, { - "colorBackground": false, - "colorValue": true, - "colors": [ - "#299c46", - "#73BF69", - "#d44a3a" - ], "datasource": { "type": "testdata", "uid": "PD8C576611E62080A" }, - "format": "ms", - "gauge": { - "maxValue": 100, - "minValue": 0, - "show": false, - "thresholdLabels": false, - "thresholdMarkers": true + "fieldConfig": { + "defaults": { + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ms" + }, + "overrides": [] }, "gridPos": { "h": 8, "w": 8, "x": 8, - "y": 21 + "y": 43 }, "id": 23, - "mappingType": 1, - "mappingTypes": [ - { - "name": "value to text", - "value": 1 - }, - { - "name": "range to text", - "value": 2 - } - ], "maxDataPoints": 100, - "nullPointMode": "connected", - "pluginVersion": "6.2.0-pre", - "postfix": "", - "postfixFontSize": "50%", - "prefix": "p95", - "prefixFontSize": "80%", - "rangeMaps": [ - { - "from": "null", - "text": "N/A", - "to": "null" - } - ], - "sparkline": { - "fillColor": "rgba(31, 118, 189, 0.18)", - "full": false, - "lineColor": "rgb(31, 120, 193)", - "show": true + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "mean" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true }, - "tableColumn": "", + "pluginVersion": "11.0.0-pre", "targets": [ { "datasource": { @@ -584,18 +859,8 @@ "refId": "A" } ], - "thresholds": "", "title": "singlestat (old, internal. Migrated if schema < 28)", - "type": "singlestat", - "valueFontSize": "120%", - "valueMaps": [ - { - "op": "=", - "text": "N/A", - "value": "null" - } - ], - "valueName": "avg" + "type": "stat" }, { "datasource": { @@ -606,7 +871,7 @@ "h": 8, "w": 8, "x": 16, - "y": 21 + "y": 43 }, "id": 10, "options": { @@ -618,7 +883,7 @@ "content": "# Singlestat >> Stat\n\nKnown issues:\n* limited options", "mode": "markdown" }, - "pluginVersion": "10.4.0-pre", + "pluginVersion": "11.0.0-pre", "targets": [ { "datasource": { @@ -640,7 +905,7 @@ "h": 10, "w": 16, "x": 0, - "y": 29 + "y": 51 }, "id": 24, "options": { @@ -693,7 +958,7 @@ "h": 10, "w": 8, "x": 16, - "y": 29 + "y": 51 }, "id": 25, "options": { @@ -705,7 +970,7 @@ "content": "# grafana-piechart-panel >> piechart\n\nKnown issues:\n* TBD", "mode": "markdown" }, - "pluginVersion": "10.4.0-pre", + "pluginVersion": "11.0.0-pre", "targets": [ { "datasource": { @@ -719,47 +984,35 @@ "type": "text" }, { + "circleMaxSize": 30, + "circleMinSize": 2, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], "datasource": { "type": "grafana-testdata-datasource", "uid": "PD8C576611E62080A" }, - "fieldConfig": { - "defaults": { - "custom": { - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "rgba(245, 54, 54, 0.9)" - }, - { - "color": "rgba(237, 129, 40, 0.89)", - "value": 0 - }, - { - "color": "rgba(50, 172, 45, 0.97)", - "value": 10 - } - ] - } - }, - "overrides": [] - }, + "decimals": 0, + "esMetric": "Count", "gridPos": { "h": 10, "w": 16, "x": 0, - "y": 39 + "y": 61 }, + "hideEmpty": false, + "hideZero": false, "id": 26, + "initialZoom": 1, + "locationData": "countries", + "mapCenter": "(0°, 0°)", + "mapCenterLatitude": 0, + "mapCenterLongitude": 0, "maxDataPoints": 1, + "mouseWheelZoom": false, "options": { "basemap": { "name": "Basemap", @@ -831,6 +1084,15 @@ } }, "pluginVersion": "10.4.0-pre", + "showLegend": true, + "stickyLabels": false, + "tableQueryOptions": { + "geohashField": "geohash", + "latitudeField": "latitude", + "longitudeField": "longitude", + "metricField": "metric", + "queryType": "geohash" + }, "targets": [ { "csvFileName": "flight_info_by_state.csv", @@ -842,6 +1104,7 @@ "scenarioId": "csv_file" } ], + "thresholds": "0,10", "title": "grafana-worldmap-panel", "transformations": [ { @@ -859,7 +1122,10 @@ } } ], - "type": "grafana-worldmap-panel" + "type": "grafana-worldmap-panel", + "unitPlural": "", + "unitSingle": "", + "valueName": "total" }, { "datasource": { @@ -870,7 +1136,7 @@ "h": 10, "w": 8, "x": 16, - "y": 39 + "y": 61 }, "id": 27, "options": { @@ -882,7 +1148,7 @@ "content": "# grafana-worldmap-panel >> geomap\n\nKnown issues:\n* TBD", "mode": "markdown" }, - "pluginVersion": "10.4.0-pre", + "pluginVersion": "11.0.0-pre", "targets": [ { "datasource": { @@ -910,10 +1176,11 @@ "from": "now-6h", "to": "now" }, + "timeRangeUpdatedDuringEditOrView": false, "timepicker": {}, "timezone": "", "title": "Devenv - Panel migrations", "uid": "cdd412c4", - "version": 67, + "version": 68, "weekStart": "" } \ No newline at end of file diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts index 2ecd10085d..4cb91f0c17 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts @@ -71,9 +71,7 @@ export interface SaveModelToSceneOptions { export function transformSaveModelToScene(rsp: DashboardDTO): DashboardScene { // Just to have migrations run - const oldModel = new DashboardModel(rsp.dashboard, rsp.meta, { - autoMigrateOldPanels: false, - }); + const oldModel = new DashboardModel(rsp.dashboard, rsp.meta); const scene = createDashboardSceneFromDashboardModel(oldModel); // TODO: refactor createDashboardSceneFromDashboardModel to work on Dashboard schema model diff --git a/public/app/features/dashboard/state/DashboardModel.ts b/public/app/features/dashboard/state/DashboardModel.ts index f0afaba766..35969d751c 100644 --- a/public/app/features/dashboard/state/DashboardModel.ts +++ b/public/app/features/dashboard/state/DashboardModel.ts @@ -127,9 +127,6 @@ export class DashboardModel implements TimeModel { options?: { // By default this uses variables from redux state getVariablesFromState?: GetVariables; - - // Force the loader to migrate panels - autoMigrateOldPanels?: boolean; } ) { this.getVariablesFromState = options?.getVariablesFromState ?? getVariablesByKey; @@ -169,59 +166,6 @@ export class DashboardModel implements TimeModel { this.initMeta(meta); this.updateSchema(data); - // Auto-migrate old angular panels - const shouldMigrateAllAngularPanels = - options?.autoMigrateOldPanels || !config.angularSupportEnabled || config.featureToggles.autoMigrateOldPanels; - - const shouldMigrateExplicitAngularPanels = - config.featureToggles.autoMigrateGraphPanel || - config.featureToggles.autoMigrateTablePanel || - config.featureToggles.autoMigratePiechartPanel || - config.featureToggles.autoMigrateWorldmapPanel || - config.featureToggles.autoMigrateStatPanel; - - // Handles both granular and all angular panel migration - if (shouldMigrateAllAngularPanels || shouldMigrateExplicitAngularPanels) { - for (const panel of this.panelIterator()) { - if ( - !panel.autoMigrateFrom && - panel.type === 'graph' && - (config.featureToggles.autoMigrateGraphPanel || shouldMigrateAllAngularPanels) - ) { - panel.autoMigrateFrom = panel.type; - panel.type = 'timeseries'; - } else if ( - !panel.autoMigrateFrom && - panel.type === 'table-old' && - (config.featureToggles.autoMigrateTablePanel || shouldMigrateAllAngularPanels) - ) { - panel.autoMigrateFrom = panel.type; - panel.type = 'table'; - } else if ( - !panel.autoMigrateFrom && - panel.type === 'grafana-piechart-panel' && - (config.featureToggles.autoMigratePiechartPanel || shouldMigrateAllAngularPanels) - ) { - panel.autoMigrateFrom = panel.type; - panel.type = 'piechart'; - } else if ( - !panel.autoMigrateFrom && - panel.type === 'grafana-worldmap-panel' && - (config.featureToggles.autoMigrateWorldmapPanel || shouldMigrateAllAngularPanels) - ) { - panel.autoMigrateFrom = panel.type; - panel.type = 'geomap'; - } else if ( - !panel.autoMigrateFrom && - (panel.type === 'singlestat' || panel.type === 'grafana-singlestat-panel') && - (config.featureToggles.autoMigrateStatPanel || shouldMigrateAllAngularPanels) - ) { - panel.autoMigrateFrom = panel.type; - panel.type = 'stat'; - } - } - } - this.addBuiltInAnnotationQuery(); this.sortPanelsByGridPos(); this.panelsAffectedByVariableChange = null; diff --git a/public/app/features/dashboard/state/PanelModel.ts b/public/app/features/dashboard/state/PanelModel.ts index fcf1307a28..1b3a6b2fbb 100644 --- a/public/app/features/dashboard/state/PanelModel.ts +++ b/public/app/features/dashboard/state/PanelModel.ts @@ -36,6 +36,8 @@ import { import { PanelQueryRunner } from '../../query/state/PanelQueryRunner'; import { TimeOverrideResult } from '../utils/panel'; +import { getPanelPluginToMigrateTo } from './getPanelPluginToMigrateTo'; + export interface GridPos { x: number; y: number; @@ -148,7 +150,7 @@ export const autoMigrateAngular: Record = { 'grafana-worldmap-panel': 'geomap', }; -const autoMigratePanelType: Record = { +export const autoMigrateRemovedPanelPlugins: Record = { 'heatmap-new': 'heatmap', // this was a temporary development panel that is now standard }; @@ -257,7 +259,7 @@ export class PanelModel implements DataConfigSource, IPanelModel { (this as any)[property] = model[property]; } - const newType = autoMigratePanelType[this.type]; + const newType = getPanelPluginToMigrateTo(this); if (newType) { this.autoMigrateFrom = this.type; this.type = newType; diff --git a/public/app/features/dashboard/state/getPanelPluginToMigrateTo.ts b/public/app/features/dashboard/state/getPanelPluginToMigrateTo.ts new file mode 100644 index 0000000000..bfc5c32b13 --- /dev/null +++ b/public/app/features/dashboard/state/getPanelPluginToMigrateTo.ts @@ -0,0 +1,51 @@ +import config from 'app/core/config'; + +import { autoMigrateRemovedPanelPlugins, autoMigrateAngular } from './PanelModel'; + +export function getPanelPluginToMigrateTo(panel: any, forceMigration?: boolean): string | undefined { + if (autoMigrateRemovedPanelPlugins[panel.type]) { + return autoMigrateRemovedPanelPlugins[panel.type]; + } + + // Auto-migrate old angular panels + const shouldMigrateAllAngularPanels = + forceMigration || !config.angularSupportEnabled || config.featureToggles.autoMigrateOldPanels; + + // Graph needs special logic as it can be migrated to multiple panels + if (panel.type === 'graph' && (shouldMigrateAllAngularPanels || config.featureToggles.autoMigrateGraphPanel)) { + if (panel.xaxis?.mode === 'series') { + return 'barchart'; + } + + if (panel.xaxis?.mode === 'histogram') { + return 'histogram'; + } + + return 'timeseries'; + } + + if (shouldMigrateAllAngularPanels) { + return autoMigrateAngular[panel.type]; + } + + if (panel.type === 'table-old' && config.featureToggles.autoMigrateTablePanel) { + return 'table'; + } + + if (panel.type === 'grafana-piechart-panel' && config.featureToggles.autoMigratePiechartPanel) { + return 'piechart'; + } + + if (panel.type === 'grafana-worldmap-panel' && config.featureToggles.autoMigrateWorldmapPanel) { + return 'geomap'; + } + + if ( + (panel.type === 'singlestat' || panel.type === 'grafana-singlestat-panel') && + config.featureToggles.autoMigrateStatPanel + ) { + return 'stat'; + } + + return undefined; +} diff --git a/public/app/plugins/panel/barchart/migrations.test.ts b/public/app/plugins/panel/barchart/migrations.test.ts new file mode 100644 index 0000000000..00af18aec7 --- /dev/null +++ b/public/app/plugins/panel/barchart/migrations.test.ts @@ -0,0 +1,32 @@ +import { FieldConfigSource, PanelModel } from '@grafana/data'; + +import { changeToBarChartPanelMigrationHandler } from './migrations'; + +describe('Bar chart Migrations', () => { + let prevFieldConfig: FieldConfigSource; + + beforeEach(() => { + prevFieldConfig = { + defaults: {}, + overrides: [], + }; + }); + + it('From old graph', () => { + const old = { + angular: { + xaxis: { + mode: 'series', + values: 'avg', + }, + }, + }; + + const panel = {} as PanelModel; + panel.options = changeToBarChartPanelMigrationHandler(panel, 'graph', old, prevFieldConfig); + + const transform = panel.transformations![0]; + expect(transform.id).toBe('reduce'); + expect(transform.options.reducers).toBe('avg'); + }); +}); diff --git a/public/app/plugins/panel/barchart/migrations.ts b/public/app/plugins/panel/barchart/migrations.ts new file mode 100644 index 0000000000..4dba05de84 --- /dev/null +++ b/public/app/plugins/panel/barchart/migrations.ts @@ -0,0 +1,36 @@ +import { PanelTypeChangedHandler } from '@grafana/data'; + +/* + * This is called when the panel changes from another panel + */ +export const changeToBarChartPanelMigrationHandler: PanelTypeChangedHandler = ( + panel, + prevPluginId, + prevOptions, + prevFieldConfig +) => { + if (prevPluginId === 'graph') { + const graphOptions: GraphOptions = prevOptions.angular; + + if (graphOptions.xaxis?.mode === 'series') { + const tranformations = panel.transformations || []; + tranformations.push({ + id: 'reduce', + options: { + reducers: graphOptions.xaxis?.values ?? ['sum'], + }, + }); + + panel.transformations = tranformations; + } + } + + return {}; +}; + +interface GraphOptions { + xaxis: { + mode: 'series' | 'time' | 'histogram'; + values?: string[]; + }; +} diff --git a/public/app/plugins/panel/barchart/module.tsx b/public/app/plugins/panel/barchart/module.tsx index 21e23f6f60..21bc79e703 100644 --- a/public/app/plugins/panel/barchart/module.tsx +++ b/public/app/plugins/panel/barchart/module.tsx @@ -16,11 +16,13 @@ import { ThresholdsStyleEditor } from '../timeseries/ThresholdsStyleEditor'; import { BarChartPanel } from './BarChartPanel'; import { TickSpacingEditor } from './TickSpacingEditor'; +import { changeToBarChartPanelMigrationHandler } from './migrations'; import { FieldConfig, Options, defaultFieldConfig, defaultOptions } from './panelcfg.gen'; import { BarChartSuggestionsSupplier } from './suggestions'; import { prepareBarChartDisplayValues } from './utils'; export const plugin = new PanelPlugin(BarChartPanel) + .setPanelChangeHandler(changeToBarChartPanelMigrationHandler) .useFieldConfig({ standardOptions: { [FieldConfigProperty.Color]: { diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index 0ba8b43008..14930db4f1 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -14,6 +14,7 @@ import { MetricsPanelCtrl } from 'app/angular/panel/metrics_panel_ctrl'; import config from 'app/core/config'; import TimeSeries from 'app/core/time_series2'; import { ThresholdMapper } from 'app/features/alerting/state/ThresholdMapper'; +import { getPanelPluginToMigrateTo } from 'app/features/dashboard/state/getPanelPluginToMigrateTo'; import { changePanelPlugin } from 'app/features/panel/state/actions'; import { dispatch } from 'app/store/store'; @@ -356,7 +357,8 @@ export class GraphCtrl extends MetricsPanelCtrl { }; migrateToReact() { - this.onPluginTypeChange(config.panels['timeseries']); + const panelType = getPanelPluginToMigrateTo(this.panel, true); + this.onPluginTypeChange(config.panels[panelType!]); } } diff --git a/public/app/plugins/panel/histogram/migrations.test.ts b/public/app/plugins/panel/histogram/migrations.test.ts new file mode 100644 index 0000000000..a0d2c5517c --- /dev/null +++ b/public/app/plugins/panel/histogram/migrations.test.ts @@ -0,0 +1,28 @@ +import { FieldConfigSource, PanelModel } from '@grafana/data'; + +import { changeToHistogramPanelMigrationHandler } from './migrations'; + +describe('Histogram migrations', () => { + let prevFieldConfig: FieldConfigSource; + + beforeEach(() => { + prevFieldConfig = { + defaults: {}, + overrides: [], + }; + }); + + it('From old graph', () => { + const old = { + angular: { + xaxis: { + mode: 'histogram', + }, + }, + }; + + const panel = {} as PanelModel; + panel.options = changeToHistogramPanelMigrationHandler(panel, 'graph', old, prevFieldConfig); + expect(panel.options.combine).toBe(true); + }); +}); diff --git a/public/app/plugins/panel/histogram/migrations.ts b/public/app/plugins/panel/histogram/migrations.ts new file mode 100644 index 0000000000..215d85a9f5 --- /dev/null +++ b/public/app/plugins/panel/histogram/migrations.ts @@ -0,0 +1,30 @@ +import { PanelTypeChangedHandler } from '@grafana/data'; + +/* + * This is called when the panel changes from another panel + */ +export const changeToHistogramPanelMigrationHandler: PanelTypeChangedHandler = ( + panel, + prevPluginId, + prevOptions, + prevFieldConfig +) => { + if (prevPluginId === 'graph') { + const graphOptions: GraphOptions = prevOptions.angular; + + if (graphOptions.xaxis?.mode === 'histogram') { + return { + combine: true, + }; + } + } + + return {}; +}; + +interface GraphOptions { + xaxis: { + mode: 'series' | 'time' | 'histogram'; + values?: string[]; + }; +} diff --git a/public/app/plugins/panel/histogram/module.tsx b/public/app/plugins/panel/histogram/module.tsx index 918462032c..c2b26f6283 100644 --- a/public/app/plugins/panel/histogram/module.tsx +++ b/public/app/plugins/panel/histogram/module.tsx @@ -3,10 +3,12 @@ import { histogramFieldInfo } from '@grafana/data/src/transformations/transforme import { commonOptionsBuilder, graphFieldOptions } from '@grafana/ui'; import { HistogramPanel } from './HistogramPanel'; +import { changeToHistogramPanelMigrationHandler } from './migrations'; import { FieldConfig, Options, defaultFieldConfig, defaultOptions } from './panelcfg.gen'; import { originalDataHasHistogram } from './utils'; export const plugin = new PanelPlugin(HistogramPanel) + .setPanelChangeHandler(changeToHistogramPanelMigrationHandler) .setPanelOptions((builder) => { builder .addCustomEditor({ From d1f8f7774d4dafcdabf881f5f4c913c217e7f1f0 Mon Sep 17 00:00:00 2001 From: Xavi Lacasa <114113189+volcanonoodle@users.noreply.github.com> Date: Thu, 7 Mar 2024 13:51:27 +0100 Subject: [PATCH 0464/1406] Document `verification_email_max_lifetime_duration` config option (#84057) --- conf/defaults.ini | 3 +++ conf/sample.ini | 3 +++ docs/sources/setup-grafana/configure-grafana/_index.md | 8 +++++++- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/conf/defaults.ini b/conf/defaults.ini index a3772b884e..ae9a5b9f30 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -500,6 +500,9 @@ editors_can_admin = false # The duration in time a user invitation remains valid before expiring. This setting should be expressed as a duration. Examples: 6h (hours), 2d (days), 1w (week). Default is 24h (24 hours). The minimum supported duration is 15m (15 minutes). user_invite_max_lifetime_duration = 24h +# The duration in time a verification email, used to update the email address of a user, remains valid before expiring. This setting should be expressed as a duration. Examples: 6h (hours), 2d (days), 1w (week). Default is 1h (1 hour). +verification_email_max_lifetime_duration = 1h + # Enter a comma-separated list of usernames to hide them in the Grafana UI. These users are shown to Grafana admins and to themselves. hidden_users = diff --git a/conf/sample.ini b/conf/sample.ini index 876585d097..156e9fa485 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -476,6 +476,9 @@ # The duration in time a user invitation remains valid before expiring. This setting should be expressed as a duration. Examples: 6h (hours), 2d (days), 1w (week). Default is 24h (24 hours). The minimum supported duration is 15m (15 minutes). ;user_invite_max_lifetime_duration = 24h +# The duration in time a verification email, used to update the email address of a user, remains valid before expiring. This setting should be expressed as a duration. Examples: 6h (hours), 2d (days), 1w (week). Default is 1h (1 hour). +;verification_email_max_lifetime_duration = 1h + # Enter a comma-separated list of users login to hide them in the Grafana UI. These users are shown to Grafana admins and themselves. ; hidden_users = diff --git a/docs/sources/setup-grafana/configure-grafana/_index.md b/docs/sources/setup-grafana/configure-grafana/_index.md index e750bf426e..c4065eaf1e 100644 --- a/docs/sources/setup-grafana/configure-grafana/_index.md +++ b/docs/sources/setup-grafana/configure-grafana/_index.md @@ -833,7 +833,7 @@ The available options are `Viewer` (default), `Admin`, `Editor`, and `None`. For ### verify_email_enabled -Require email validation before sign up completes. Default is `false`. +Require email validation before sign up completes or when updating a user email address. Default is `false`. ### login_hint @@ -878,6 +878,12 @@ The duration in time a user invitation remains valid before expiring. This setting should be expressed as a duration. Examples: 6h (hours), 2d (days), 1w (week). Default is `24h` (24 hours). The minimum supported duration is `15m` (15 minutes). +### verification_email_max_lifetime_duration + +The duration in time a verification email, used to update the email address of a user, remains valid before expiring. +This setting should be expressed as a duration. Examples: 6h (hours), 2d (days), 1w (week). +Default is 1h (1 hour). + ### hidden_users This is a comma-separated list of usernames. Users specified here are hidden in the Grafana UI. They are still visible to Grafana administrators and to themselves. From 0236053f708e70dd8a7b0040516e2b3b862b7f77 Mon Sep 17 00:00:00 2001 From: Andreas Christou Date: Thu, 7 Mar 2024 13:04:37 +0000 Subject: [PATCH 0465/1406] Chore: Bump docker image versions (#84033) Bump docker image versions --- Dockerfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 03dbfd4355..d17a384ff3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,9 @@ # syntax=docker/dockerfile:1 -ARG BASE_IMAGE=alpine:3.18.5 -ARG JS_IMAGE=node:20-alpine3.18 +ARG BASE_IMAGE=alpine:3.19.1 +ARG JS_IMAGE=node:20-alpine ARG JS_PLATFORM=linux/amd64 -ARG GO_IMAGE=golang:1.21.8-alpine3.18 +ARG GO_IMAGE=golang:1.21.8-alpine ARG GO_SRC=go-builder ARG JS_SRC=js-builder From 429ef9559cc35767b9b5e371d9e9786cae926aff Mon Sep 17 00:00:00 2001 From: Adela Almasan <88068998+adela-almasan@users.noreply.github.com> Date: Thu, 7 Mar 2024 08:49:37 -0600 Subject: [PATCH 0466/1406] FeatureToggles: Allow changing prod env safe feature toggles via URL (#84034) --- packages/grafana-runtime/src/config.ts | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/packages/grafana-runtime/src/config.ts b/packages/grafana-runtime/src/config.ts index 3071909851..0e28eb5831 100644 --- a/packages/grafana-runtime/src/config.ts +++ b/packages/grafana-runtime/src/config.ts @@ -203,10 +203,7 @@ export class GrafanaBootConfig implements GrafanaConfig { systemDateFormats.update(this.dateFormats); } - if (this.buildInfo.env === 'development') { - overrideFeatureTogglesFromUrl(this); - } - + overrideFeatureTogglesFromUrl(this); overrideFeatureTogglesFromLocalStorage(this); if (this.featureToggles.disableAngular) { @@ -243,11 +240,28 @@ function overrideFeatureTogglesFromUrl(config: GrafanaBootConfig) { return; } + const isLocalDevEnv = config.buildInfo.env === 'development'; + + const prodUrlAllowedFeatureFlags = new Set([ + 'autoMigrateOldPanels', + 'autoMigrateGraphPanel', + 'autoMigrateTablePanel', + 'autoMigratePiechartPanel', + 'autoMigrateWorldmapPanel', + 'autoMigrateStatPanel', + 'disableAngular', + ]); + const params = new URLSearchParams(window.location.search); params.forEach((value, key) => { if (key.startsWith('__feature.')) { const featureToggles = config.featureToggles as Record; const featureName = key.substring(10); + + if (!isLocalDevEnv && !prodUrlAllowedFeatureFlags.has(featureName)) { + return; + } + const toggleState = value === 'true' || value === ''; // browser rewrites true as '' if (toggleState !== featureToggles[key]) { featureToggles[featureName] = toggleState; From 6fdcc6ff189160d13c5735e9dbb6295ca7739b16 Mon Sep 17 00:00:00 2001 From: linoman <2051016+linoman@users.noreply.github.com> Date: Thu, 7 Mar 2024 09:01:17 -0600 Subject: [PATCH 0467/1406] Password Policy: Add validation labels to Update Password screen (#84052) * add validation labels to update the password screen * address rendering tests * update changePassword for profile screen --- .betterer.results | 3 - .../ForgottenPassword/ChangePassword.tsx | 28 ++- .../ChangePasswordPage.test.tsx | 3 + .../features/profile/ChangePasswordForm.tsx | 176 +++++++++--------- 4 files changed, 113 insertions(+), 97 deletions(-) diff --git a/.betterer.results b/.betterer.results index 3116e9fa59..ca5c0a6a24 100644 --- a/.betterer.results +++ b/.betterer.results @@ -3991,9 +3991,6 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "2"], [0, 0, 0, "Do not use any type assertions.", "3"] ], - "public/app/features/profile/ChangePasswordForm.tsx:5381": [ - [0, 0, 0, "Styles should be written using objects.", "0"] - ], "public/app/features/query/components/QueryEditorRow.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Do not use any type assertions.", "1"], diff --git a/public/app/core/components/ForgottenPassword/ChangePassword.tsx b/public/app/core/components/ForgottenPassword/ChangePassword.tsx index 9f5a958d97..845db05de9 100644 --- a/public/app/core/components/ForgottenPassword/ChangePassword.tsx +++ b/public/app/core/components/ForgottenPassword/ChangePassword.tsx @@ -1,4 +1,4 @@ -import React, { SyntheticEvent } from 'react'; +import React, { SyntheticEvent, useState } from 'react'; import { useForm } from 'react-hook-form'; import { selectors } from '@grafana/e2e-selectors'; @@ -6,6 +6,12 @@ import { Tooltip, Field, VerticalGroup, Button, Alert, useStyles2 } from '@grafa import { getStyles } from '../Login/LoginForm'; import { PasswordField } from '../PasswordField/PasswordField'; +import { + ValidationLabels, + strongPasswordValidations, + strongPasswordValidationRegister, +} from '../ValidationLabels/ValidationLabels'; + interface Props { onSubmit: (pw: string) => void; onSkip?: (event?: SyntheticEvent) => void; @@ -19,17 +25,23 @@ interface PasswordDTO { export const ChangePassword = ({ onSubmit, onSkip, showDefaultPasswordWarning }: Props) => { const styles = useStyles2(getStyles); + const [displayValidationLabels, setDisplayValidationLabels] = useState(false); + const [pristine, setPristine] = useState(true); + const { handleSubmit, register, getValues, formState: { errors }, + watch, } = useForm({ defaultValues: { newPassword: '', confirmNew: '', }, }); + + const newPassword = watch('newPassword'); const submit = (passwords: PasswordDTO) => { onSubmit(passwords.newPassword); }; @@ -40,12 +52,24 @@ export const ChangePassword = ({ onSubmit, onSkip, showDefaultPasswordWarning }: )} setDisplayValidationLabels(true)} + {...register('newPassword', { + required: 'New Password is required', + onBlur: () => setPristine(false), + validate: { strongPasswordValidationRegister }, + })} id="new-password" autoFocus autoComplete="new-password" /> + {displayValidationLabels && ( + + )} ({ licenseUrl: '', }, appSubUrl: '', + auth: { + basicAuthStrongPasswordPolicy: false, + }, }, })); diff --git a/public/app/features/profile/ChangePasswordForm.tsx b/public/app/features/profile/ChangePasswordForm.tsx index a6321d7327..a4aa7cd308 100644 --- a/public/app/features/profile/ChangePasswordForm.tsx +++ b/public/app/features/profile/ChangePasswordForm.tsx @@ -1,7 +1,7 @@ -import { css } from '@emotion/css'; import React, { useState } from 'react'; -import { Button, Field, Form, HorizontalGroup, LinkButton } from '@grafana/ui'; +import { Button, Field, HorizontalGroup, LinkButton } from '@grafana/ui'; +import { Form } from 'app/core/components/Form/Form'; import { ValidationLabels, strongPasswordValidations, @@ -24,7 +24,6 @@ export interface Props { export const ChangePasswordForm = ({ user, onChangePassword, isSaving }: Props) => { const [displayValidationLabels, setDisplayValidationLabels] = useState(false); const [pristine, setPristine] = useState(true); - const [newPassword, setNewPassword] = useState(''); const { disableLoginForm } = config; const authSource = user.authLabels?.length && user.authLabels[0]; @@ -47,96 +46,89 @@ export const ChangePasswordForm = ({ user, onChangePassword, isSaving }: Props) } return ( -
- - {({ register, errors, getValues }) => { - return ( - <> - - - + + {({ register, errors, getValues, watch }) => { + const newPassword = watch('newPassword'); + return ( + <> + + + - - setDisplayValidationLabels(true)} - value={newPassword} - {...register('newPassword', { - onBlur: () => setPristine(false), - onChange: (e) => setNewPassword(e.target.value), - required: t('profile.change-password.new-password-required', 'New password is required'), - validate: { - strongPasswordValidationRegister, - confirm: (v) => - v === getValues().confirmNew || - t('profile.change-password.passwords-must-match', 'Passwords must match'), - old: (v) => - v !== getValues().oldPassword || - t( - 'profile.change-password.new-password-same-as-old', - "New password can't be the same as the old one." - ), - }, - })} - /> - - {displayValidationLabels && ( - - )} - - - v === getValues().newPassword || + + setDisplayValidationLabels(true)} + {...register('newPassword', { + onBlur: () => setPristine(false), + required: t('profile.change-password.new-password-required', 'New password is required'), + validate: { + strongPasswordValidationRegister, + confirm: (v) => + v === getValues().confirmNew || t('profile.change-password.passwords-must-match', 'Passwords must match'), - })} - /> - - - - - Cancel - - - - ); - }} - -
+ old: (v) => + v !== getValues().oldPassword || + t( + 'profile.change-password.new-password-same-as-old', + "New password can't be the same as the old one." + ), + }, + })} + /> +
+ {displayValidationLabels && ( + + )} + + + v === getValues().newPassword || + t('profile.change-password.passwords-must-match', 'Passwords must match'), + })} + /> + + + + + Cancel + + + + ); + }} + ); }; From f5dab6b5a5ce3c4d6e627b3aeef34171ab1295a1 Mon Sep 17 00:00:00 2001 From: Gilles De Mey Date: Thu, 7 Mar 2024 16:41:38 +0100 Subject: [PATCH 0468/1406] Alerting: Refactor analytics to use pushMeasurements (#83850) --- packages/grafana-runtime/src/utils/logging.ts | 25 ++++++ .../features/alerting/unified/Analytics.ts | 78 ++++++++++--------- .../alerting/unified/api/alertingApi.ts | 19 +++-- .../alerting/unified/api/alertmanagerApi.ts | 2 +- .../unified/api/featureDiscoveryApi.ts | 2 +- .../alerting/unified/state/actions.ts | 29 +++---- 6 files changed, 93 insertions(+), 62 deletions(-) diff --git a/packages/grafana-runtime/src/utils/logging.ts b/packages/grafana-runtime/src/utils/logging.ts index 251719a933..d7c94da53d 100644 --- a/packages/grafana-runtime/src/utils/logging.ts +++ b/packages/grafana-runtime/src/utils/logging.ts @@ -58,6 +58,22 @@ export function logError(err: Error, contexts?: LogContext) { } } +/** + * Log a measurement + * + * @public + */ +export type MeasurementValues = Record; +export function logMeasurement(type: string, values: MeasurementValues, context?: LogContext) { + if (config.grafanaJavascriptAgent.enabled) { + faro.api.pushMeasurement({ + type, + values, + context, + }); + } +} + /** * Creates a monitoring logger with four levels of logging methods: `logDebug`, `logInfo`, `logWarning`, and `logError`. * These methods use `faro.api.pushX` web SDK methods to report these logs or errors to the Faro collector. @@ -70,6 +86,7 @@ export function logError(err: Error, contexts?: LogContext) { * - `logInfo(message: string, contexts?: LogContext)`: Logs an informational message. * - `logWarning(message: string, contexts?: LogContext)`: Logs a warning message. * - `logError(error: Error, contexts?: LogContext)`: Logs an error message. + * - `logMeasurement(measurement: Omit, contexts?: LogContext)`: Logs a measurement. * Each method combines the `defaultContext` (if provided), the `source`, and an optional `LogContext` parameter into a full context that is included with the log message. */ export function createMonitoringLogger(source: string, defaultContext?: LogContext) { @@ -107,5 +124,13 @@ export function createMonitoringLogger(source: string, defaultContext?: LogConte * @param {LogContext} [contexts] - Optional additional context to be included. */ logError: (error: Error, contexts?: LogContext) => logError(error, createFullContext(contexts)), + + /** + * Logs an measurement with optional additional context. + * @param {MeasurementEvent} measurement - The measurement object to be recorded. + * @param {LogContext} [contexts] - Optional additional context to be included. + */ + logMeasurement: (type: string, measurement: MeasurementValues, contexts?: LogContext) => + logMeasurement(type, measurement, createFullContext(contexts)), }; } diff --git a/public/app/features/alerting/unified/Analytics.ts b/public/app/features/alerting/unified/Analytics.ts index 049243309a..81f0c39773 100644 --- a/public/app/features/alerting/unified/Analytics.ts +++ b/public/app/features/alerting/unified/Analytics.ts @@ -26,29 +26,29 @@ export const LogMessages = { unknownMessageFromError: 'unknown messageFromError', }; -const alertingLogger = createMonitoringLogger('features.alerting', { module: 'Alerting' }); +const { logInfo, logError, logMeasurement } = createMonitoringLogger('features.alerting', { module: 'Alerting' }); -export function logInfo(message: string, context?: Record) { - alertingLogger.logInfo(message, context); -} - -export function logError(error: Error, context?: Record) { - alertingLogger.logError(error, context); -} +export { logInfo, logError, logMeasurement }; // eslint-disable-next-line @typescript-eslint/no-explicit-any export function withPerformanceLogging Promise>( + type: string, func: TFunc, - message: string, context: Record ): (...args: Parameters) => Promise>> { return async function (...args) { const startLoadingTs = performance.now(); + const response = await func(...args); - logInfo(message, { - loadTimeMs: (performance.now() - startLoadingTs).toFixed(0), - ...context, - }); + const loadTimesMs = performance.now() - startLoadingTs; + + logMeasurement( + type, + { + loadTimesMs, + }, + context + ); return response; }; @@ -56,8 +56,8 @@ export function withPerformanceLogging Promise // eslint-disable-next-line @typescript-eslint/no-explicit-any export function withPromRulesMetadataLogging Promise>( + type: string, func: TFunc, - message: string, context: Record ) { return async (...args: Parameters) => { @@ -66,13 +66,16 @@ export function withPromRulesMetadataLogging P const { namespacesCount, groupsCount, rulesCount } = getPromRulesMetadata(response); - logInfo(message, { - loadTimeMs: (performance.now() - startLoadingTs).toFixed(0), - namespacesCount, - groupsCount, - rulesCount, - ...context, - }); + logMeasurement( + type, + { + loadTimeMs: performance.now() - startLoadingTs, + namespacesCount, + groupsCount, + rulesCount, + }, + context + ); return response; }; } @@ -83,9 +86,9 @@ function getPromRulesMetadata(promRules: RuleNamespace[]) { const rulesCount = promRules.flatMap((ns) => ns.groups).flatMap((g) => g.rules).length; const metadata = { - namespacesCount: namespacesCount.toFixed(0), - groupsCount: groupsCount.toFixed(0), - rulesCount: rulesCount.toFixed(0), + namespacesCount: namespacesCount, + groupsCount: groupsCount, + rulesCount: rulesCount, }; return metadata; @@ -93,8 +96,8 @@ function getPromRulesMetadata(promRules: RuleNamespace[]) { // eslint-disable-next-line @typescript-eslint/no-explicit-any export function withRulerRulesMetadataLogging Promise>( + type: string, func: TFunc, - message: string, context: Record ) { return async (...args: Parameters) => { @@ -103,26 +106,29 @@ export function withRulerRulesMetadataLogging const { namespacesCount, groupsCount, rulesCount } = getRulerRulesMetadata(response); - logInfo(message, { - loadTimeMs: (performance.now() - startLoadingTs).toFixed(0), - namespacesCount, - groupsCount, - rulesCount, - ...context, - }); + logMeasurement( + type, + { + namespacesCount, + groupsCount, + rulesCount, + loadTimeMs: performance.now() - startLoadingTs, + }, + context + ); return response; }; } function getRulerRulesMetadata(rulerRules: RulerRulesConfigDTO) { - const namespacesCount = Object.keys(rulerRules).length; + const namespaces = Object.keys(rulerRules); const groups = Object.values(rulerRules).flatMap((groups) => groups); const rules = groups.flatMap((group) => group.rules); return { - namespacesCount: namespacesCount.toFixed(0), - groupsCount: groups.length.toFixed(0), - rulesCount: rules.length.toFixed(0), + namespacesCount: namespaces.length, + groupsCount: groups.length, + rulesCount: rules.length, }; } diff --git a/public/app/features/alerting/unified/api/alertingApi.ts b/public/app/features/alerting/unified/api/alertingApi.ts index d4f9118395..5d90c4c58a 100644 --- a/public/app/features/alerting/unified/api/alertingApi.ts +++ b/public/app/features/alerting/unified/api/alertingApi.ts @@ -3,7 +3,7 @@ import { lastValueFrom } from 'rxjs'; import { BackendSrvRequest, getBackendSrv } from '@grafana/runtime'; -import { logInfo } from '../Analytics'; +import { logMeasurement } from '../Analytics'; export const backendSrvBaseQuery = (): BaseQueryFn => async (requestOptions) => { try { @@ -11,12 +11,17 @@ export const backendSrvBaseQuery = (): BaseQueryFn => async ( const { data, ...meta } = await lastValueFrom(getBackendSrv().fetch(requestOptions)); - logInfo('Request finished', { - loadTimeMs: (performance.now() - requestStartTs).toFixed(0), - url: requestOptions.url, - method: requestOptions.method ?? '', - responseStatus: meta.statusText, - }); + logMeasurement( + 'backendSrvBaseQuery', + { + loadTimeMs: performance.now() - requestStartTs, + }, + { + url: requestOptions.url, + method: requestOptions.method ?? 'GET', + responseStatus: meta.statusText, + } + ); return { data, meta }; } catch (error) { diff --git a/public/app/features/alerting/unified/api/alertmanagerApi.ts b/public/app/features/alerting/unified/api/alertmanagerApi.ts index c0c7109221..ea0f1e948d 100644 --- a/public/app/features/alerting/unified/api/alertmanagerApi.ts +++ b/public/app/features/alerting/unified/api/alertmanagerApi.ts @@ -164,8 +164,8 @@ export const alertmanagerApi = alertingApi.injectEndpoints({ // wrap our fetchConfig function with some performance logging functions const fetchAMconfigWithLogging = withPerformanceLogging( + 'unifiedalerting/fetchAmConfig', fetchAlertManagerConfig, - `[${alertmanagerSourceName}] Alertmanager config loaded`, { dataSourceName: alertmanagerSourceName, thunk: 'unifiedalerting/fetchAmConfig', diff --git a/public/app/features/alerting/unified/api/featureDiscoveryApi.ts b/public/app/features/alerting/unified/api/featureDiscoveryApi.ts index 659cc191f7..45f59b75ea 100644 --- a/public/app/features/alerting/unified/api/featureDiscoveryApi.ts +++ b/public/app/features/alerting/unified/api/featureDiscoveryApi.ts @@ -28,8 +28,8 @@ export const featureDiscoveryApi = alertingApi.injectEndpoints({ } const discoverFeaturesWithLogging = withPerformanceLogging( + 'unifiedalerting/featureDiscoveryApi/discoverDsFeatures', discoverFeatures, - `[${rulesSourceName}] Rules source features discovered`, { dataSourceName: rulesSourceName, endpoint: 'unifiedalerting/featureDiscoveryApi/discoverDsFeatures', diff --git a/public/app/features/alerting/unified/state/actions.ts b/public/app/features/alerting/unified/state/actions.ts index 0daafcaca6..5d743ce726 100644 --- a/public/app/features/alerting/unified/state/actions.ts +++ b/public/app/features/alerting/unified/state/actions.ts @@ -2,6 +2,7 @@ import { AsyncThunk, createAsyncThunk } from '@reduxjs/toolkit'; import { isEmpty } from 'lodash'; import { locationService } from '@grafana/runtime'; +import { logMeasurement } from '@grafana/runtime/src/utils/logging'; import { AlertmanagerAlert, AlertManagerCortexConfig, @@ -122,11 +123,10 @@ export const fetchPromRulesAction = createAsyncThunk( ): Promise => { await thunkAPI.dispatch(fetchRulesSourceBuildInfoAction({ rulesSourceName })); - const fetchRulesWithLogging = withPromRulesMetadataLogging( - fetchRules, - `[${rulesSourceName}] Prometheus rules loaded`, - { dataSourceName: rulesSourceName, thunk: 'unifiedalerting/fetchPromRules' } - ); + const fetchRulesWithLogging = withPromRulesMetadataLogging('unifiedalerting/fetchPromRules', fetchRules, { + dataSourceName: rulesSourceName, + thunk: 'unifiedalerting/fetchPromRules', + }); return await withSerializedError( fetchRulesWithLogging(rulesSourceName, filter, limitAlerts, matcher, state, identifier) @@ -164,8 +164,8 @@ export const fetchRulerRulesAction = createAsyncThunk( const rulerConfig = getDataSourceRulerConfig(getState, rulesSourceName); const fetchRulerRulesWithLogging = withRulerRulesMetadataLogging( + 'unifiedalerting/fetchRulerRules', fetchRulerRules, - `[${rulesSourceName}] Ruler rules loaded`, { dataSourceName: rulesSourceName, thunk: 'unifiedalerting/fetchRulerRules', @@ -205,14 +205,9 @@ export function fetchPromAndRulerRulesAction({ export const fetchSilencesAction = createAsyncThunk( 'unifiedalerting/fetchSilences', (alertManagerSourceName: string): Promise => { - const fetchSilencesWithLogging = withPerformanceLogging( - fetchSilences, - `[${alertManagerSourceName}] Silences loaded`, - { - dataSourceName: alertManagerSourceName, - thunk: 'unifiedalerting/fetchSilences', - } - ); + const fetchSilencesWithLogging = withPerformanceLogging('unifiedalerting/fetchSilences', fetchSilences, { + dataSourceName: alertManagerSourceName, + }); return withSerializedError(fetchSilencesWithLogging(alertManagerSourceName)); } @@ -265,8 +260,8 @@ export const fetchRulesSourceBuildInfoAction = createAsyncThunk( const { id, name } = ds; const discoverFeaturesWithLogging = withPerformanceLogging( + 'unifiedalerting/fetchPromBuildinfo', discoverFeatures, - `[${rulesSourceName}] Rules source features discovered`, { dataSourceName: rulesSourceName, thunk: 'unifiedalerting/fetchPromBuildinfo', @@ -338,8 +333,8 @@ export function fetchAllPromAndRulerRulesAction( }) ); - logInfo('All Prom and Ruler rules loaded', { - loadTimeMs: (performance.now() - allStartLoadingTs).toFixed(0), + logMeasurement('unifiedalerting/fetchAllPromAndRulerRulesAction', { + loadTimeMs: performance.now() - allStartLoadingTs, }); }; } From 1da78ac8463dff31637b833ff4c8fda7bd75a88a Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Thu, 7 Mar 2024 18:11:34 +0100 Subject: [PATCH 0469/1406] DashboardScene: Allow unlinking a library panel (#83956) * DashboardScene: Allow unlinking a library panel * Betterer * Revert * Review --- .../scene/DashboardScene.test.tsx | 33 ++++++++++++++ .../dashboard-scene/scene/DashboardScene.tsx | 17 +++++++ .../scene/PanelMenuBehavior.tsx | 16 ++++++- .../scene/UnlinkLibraryPanelModal.tsx | 44 +++++++++++++++++++ .../scene}/UnlinkModal.tsx | 0 .../components/PanelEditor/PanelEditor.tsx | 2 +- public/app/features/dashboard/utils/panel.ts | 2 +- 7 files changed, 110 insertions(+), 4 deletions(-) create mode 100644 public/app/features/dashboard-scene/scene/UnlinkLibraryPanelModal.tsx rename public/app/features/{library-panels/components/UnlinkModal => dashboard-scene/scene}/UnlinkModal.tsx (100%) diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx index bfad911078..69b1e38091 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx @@ -490,6 +490,39 @@ describe('DashboardScene', () => { const gridRow = body.state.children[2] as SceneGridRow; expect(gridRow.state.children.length).toBe(1); }); + + it('Should unlink a library panel', () => { + const libPanel = new LibraryVizPanel({ + title: 'title', + uid: 'abc', + name: 'lib panel', + panelKey: 'panel-1', + isLoaded: true, + panel: new VizPanel({ + title: 'Panel B', + pluginId: 'table', + }), + }); + + const scene = buildTestScene({ + body: new SceneGridLayout({ + children: [ + new SceneGridItem({ + key: 'griditem-2', + body: libPanel, + }), + ], + }), + }); + + scene.unlinkLibraryPanel(libPanel); + + const body = scene.state.body as SceneGridLayout; + const gridItem = body.state.children[0] as SceneGridItem; + + expect(body.state.children.length).toBe(1); + expect(gridItem.state.body).toBeInstanceOf(VizPanel); + }); }); }); diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index a08933bac6..be27cb411a 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -603,6 +603,23 @@ export class DashboardScene extends SceneObjectBase { } } + public unlinkLibraryPanel(panel: LibraryVizPanel) { + if (!panel.parent) { + return; + } + + const gridItem = panel.parent; + + if (!(gridItem instanceof SceneGridItem || gridItem instanceof PanelRepeaterGridItem)) { + console.error('Trying to duplicate a panel in a layout that is not SceneGridItem or PanelRepeaterGridItem'); + return; + } + + gridItem?.setState({ + body: panel.state.panel?.clone(), + }); + } + public showModal(modal: SceneObject) { this.setState({ overlay: modal }); } diff --git a/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx b/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx index 84d7870b5e..323c89e760 100644 --- a/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx +++ b/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx @@ -28,6 +28,7 @@ import { getDashboardSceneFor, getPanelIdForVizPanel, getQueryRunnerFor } from ' import { DashboardScene } from './DashboardScene'; import { LibraryVizPanel } from './LibraryVizPanel'; import { VizPanelLinks, VizPanelLinksMenu } from './PanelLinks'; +import { UnlinkLibraryPanelModal } from './UnlinkLibraryPanelModal'; /** * Behavior is called when VizPanelMenu is activated (ie when it's opened). @@ -37,6 +38,7 @@ export function panelMenuBehavior(menu: VizPanelMenu) { // hm.. add another generic param to SceneObject to specify parent type? // eslint-disable-next-line @typescript-eslint/consistent-type-assertions const panel = menu.parent as VizPanel; + const parent = panel.parent; const plugin = panel.getPlugin(); const items: PanelMenuItem[] = []; @@ -101,8 +103,18 @@ export function panelMenuBehavior(menu: VizPanelMenu) { }, }); - if (panel.parent instanceof LibraryVizPanel) { - // TODO: Implement lib panel unlinking + if (parent instanceof LibraryVizPanel) { + moreSubMenu.push({ + text: t('panel.header-menu.unlink-library-panel', `Unlink library panel`), + onClick: () => { + DashboardInteractions.panelMenuItemClicked('unlinkLibraryPanel'); + dashboard.showModal( + new UnlinkLibraryPanelModal({ + panelRef: parent.getRef(), + }) + ); + }, + }); } else { moreSubMenu.push({ text: t('panel.header-menu.create-library-panel', `Create library panel`), diff --git a/public/app/features/dashboard-scene/scene/UnlinkLibraryPanelModal.tsx b/public/app/features/dashboard-scene/scene/UnlinkLibraryPanelModal.tsx new file mode 100644 index 0000000000..8dc2696a71 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/UnlinkLibraryPanelModal.tsx @@ -0,0 +1,44 @@ +import React from 'react'; + +import { SceneComponentProps, SceneObjectBase, SceneObjectRef, SceneObjectState } from '@grafana/scenes'; + +import { ModalSceneObjectLike } from '../sharing/types'; +import { getDashboardSceneFor } from '../utils/utils'; + +import { LibraryVizPanel } from './LibraryVizPanel'; +import { UnlinkModal } from './UnlinkModal'; + +interface UnlinkLibraryPanelModalState extends SceneObjectState { + panelRef?: SceneObjectRef; +} + +export class UnlinkLibraryPanelModal + extends SceneObjectBase + implements ModalSceneObjectLike +{ + static Component = UnlinkLibraryPanelModalRenderer; + + public onDismiss = () => { + const dashboard = getDashboardSceneFor(this); + dashboard.closeModal(); + }; + + public onConfirm = () => { + const dashboard = getDashboardSceneFor(this); + dashboard.unlinkLibraryPanel(this.state.panelRef!.resolve()); + dashboard.closeModal(); + }; +} + +function UnlinkLibraryPanelModalRenderer({ model }: SceneComponentProps) { + return ( + { + model.onConfirm(); + model.onDismiss(); + }} + onDismiss={model.onDismiss} + /> + ); +} diff --git a/public/app/features/library-panels/components/UnlinkModal/UnlinkModal.tsx b/public/app/features/dashboard-scene/scene/UnlinkModal.tsx similarity index 100% rename from public/app/features/library-panels/components/UnlinkModal/UnlinkModal.tsx rename to public/app/features/dashboard-scene/scene/UnlinkModal.tsx diff --git a/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx b/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx index eb31dae7c0..1577591e4b 100644 --- a/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx +++ b/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx @@ -33,7 +33,7 @@ import { StoreState } from 'app/types'; import { PanelOptionsChangedEvent, ShowModalReactEvent } from 'app/types/events'; import { notifyApp } from '../../../../core/actions'; -import { UnlinkModal } from '../../../library-panels/components/UnlinkModal/UnlinkModal'; +import { UnlinkModal } from '../../../dashboard-scene/scene/UnlinkModal'; import { isPanelModelLibraryPanel } from '../../../library-panels/guard'; import { getVariablesByKey } from '../../../variables/state/selectors'; import { DashboardPanel } from '../../dashgrid/DashboardPanel'; diff --git a/public/app/features/dashboard/utils/panel.ts b/public/app/features/dashboard/utils/panel.ts index cd178de9a4..bc12af9784 100644 --- a/public/app/features/dashboard/utils/panel.ts +++ b/public/app/features/dashboard/utils/panel.ts @@ -9,8 +9,8 @@ import store from 'app/core/store'; import { ShareModal } from 'app/features/dashboard/components/ShareModal'; import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import { PanelModel } from 'app/features/dashboard/state/PanelModel'; +import { UnlinkModal } from 'app/features/dashboard-scene/scene/UnlinkModal'; import { AddLibraryPanelModal } from 'app/features/library-panels/components/AddLibraryPanelModal/AddLibraryPanelModal'; -import { UnlinkModal } from 'app/features/library-panels/components/UnlinkModal/UnlinkModal'; import { cleanUpPanelState } from 'app/features/panel/state/actions'; import { dispatch } from 'app/store/store'; From a15e48052f49cee0be0e8432d1ea3b4ce0310099 Mon Sep 17 00:00:00 2001 From: Lisa <60980933+LisaHJung@users.noreply.github.com> Date: Thu, 7 Mar 2024 11:42:16 -0700 Subject: [PATCH 0470/1406] Embed two visualization videos from the Grafana for Beginners series (#83928) * Embed two visualization videos from Grafana for Beginners series * Implementing Isabel's recommendation on second video placement. * edited introductory sentence to the second video. * Added line between text and video --------- Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> --- docs/sources/panels-visualizations/visualizations/_index.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/sources/panels-visualizations/visualizations/_index.md b/docs/sources/panels-visualizations/visualizations/_index.md index 6e17cc7c64..b5c26e0f79 100644 --- a/docs/sources/panels-visualizations/visualizations/_index.md +++ b/docs/sources/panels-visualizations/visualizations/_index.md @@ -20,6 +20,8 @@ weight: 10 Grafana offers a variety of visualizations to support different use cases. This section of the documentation highlights the built-in visualizations, their options and typical usage. +{{< youtube id="JwF6FgeotaU" >}} + {{% admonition type="note" %}} If you are unsure which visualization to pick, Grafana can provide visualization suggestions based on the panel query. When you select a visualization, Grafana will show a preview with that visualization applied. {{% /admonition %}} @@ -51,6 +53,10 @@ If you are unsure which visualization to pick, Grafana can provide visualization - [Text][] can show markdown and html. - [News][] can show RSS feeds. +The following video shows you how to create gauge, time series line graph, stats, logs, and node graph visualizations: + +{{< youtube id="yNRnLyVntUw" >}} + ## Get more You can add more visualization types by installing panel [panel plugins](https://grafana.com/grafana/plugins/?type=panel). From 8c7090bc110be5b651d02894a050dd33f331a021 Mon Sep 17 00:00:00 2001 From: Christopher Moyer <35463610+chri2547@users.noreply.github.com> Date: Thu, 7 Mar 2024 12:53:10 -0600 Subject: [PATCH 0471/1406] docs: adds alt text to images where missing (#84028) * adds alt text * makes prettier --- docs/sources/fundamentals/intro-histograms/index.md | 4 ++-- docs/sources/fundamentals/timeseries-dimensions/index.md | 2 +- docs/sources/fundamentals/timeseries/index.md | 2 +- .../configure-authentication/enhanced-ldap/index.md | 4 ++-- .../configure-authentication/ldap/index.md | 8 ++++---- .../configure-security/configure-team-sync.md | 5 +++-- 6 files changed, 13 insertions(+), 12 deletions(-) diff --git a/docs/sources/fundamentals/intro-histograms/index.md b/docs/sources/fundamentals/intro-histograms/index.md index 4b78139e39..21b1c85e86 100644 --- a/docs/sources/fundamentals/intro-histograms/index.md +++ b/docs/sources/fundamentals/intro-histograms/index.md @@ -32,7 +32,7 @@ and the bar height represents the frequency (such as count) of values that fell This _histogram_ shows the value distribution of a couple of time series. You can easily see that most values land between 240-300 with a peak between 260-280. -![](/static/img/docs/v43/heatmap_histogram.png) +![Histogram example](/static/img/docs/v43/heatmap_histogram.png) Here is an example showing height distribution of people. @@ -48,7 +48,7 @@ A _heatmap_ is like a histogram, but over time, where each time slice represents In this example, you can clearly see what values are more common and how they trend over time. -![](/static/img/docs/v43/heatmap_histogram_over_time.png) +![Heatmap example](/static/img/docs/v43/heatmap_histogram_over_time.png) For more information about heatmap visualization options, refer to [Heatmap][heatmap]. diff --git a/docs/sources/fundamentals/timeseries-dimensions/index.md b/docs/sources/fundamentals/timeseries-dimensions/index.md index b42fb1482d..3e1e73d502 100644 --- a/docs/sources/fundamentals/timeseries-dimensions/index.md +++ b/docs/sources/fundamentals/timeseries-dimensions/index.md @@ -29,7 +29,7 @@ In [Introduction to time series][time-series-databases], the concept of _labels_ With time series data, the data often contain more than a single series, and is a set of multiple time series. Many Grafana data sources support this type of data. -{{< figure src="/static/img/docs/example_graph_multi_dim.png" class="docs-image--no-shadow" max-width="850px" >}} +{{< figure src="/static/img/docs/example_graph_multi_dim.png" class="docs-image--no-shadow" max-width="850px" alt="Temperature by location" >}} The common case is issuing a single query for a measurement with one or more additional properties as dimensions. For example, querying a temperature measurement along with a location property. In this case, multiple series are returned back from that single query and each series has unique location as a dimension. diff --git a/docs/sources/fundamentals/timeseries/index.md b/docs/sources/fundamentals/timeseries/index.md index c08915ea29..aaa0df85d3 100644 --- a/docs/sources/fundamentals/timeseries/index.md +++ b/docs/sources/fundamentals/timeseries/index.md @@ -32,7 +32,7 @@ Temperature data like this is one example of what we call a _time series_ — a Tables are useful when you want to identify individual measurements, but they make it difficult to see the big picture. A more common visualization for time series is the _graph_, which instead places each measurement along a time axis. Visual representations like the graph make it easier to discover patterns and features of the data that otherwise would be difficult to see. -{{< figure src="/static/img/docs/example_graph.png" class="docs-image--no-shadow" max-width="850px" >}} +{{< figure src="/static/img/docs/example_graph.png" class="docs-image--no-shadow" max-width="850px" alt="Temperature data displayed on dashboard" >}} Temperature data like the one in the example, is far from the only example of a time series. Other examples of time series are: diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/enhanced-ldap/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/enhanced-ldap/index.md index 02aea423bd..fd02e51fcb 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/enhanced-ldap/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/enhanced-ldap/index.md @@ -30,11 +30,11 @@ The enhanced LDAP integration adds additional functionality on top of the [LDAP ## LDAP group synchronization for teams -{{< figure src="/static/img/docs/enterprise/team_members_ldap.png" class="docs-image--no-shadow docs-image--right" max-width= "600px" >}} - With enhanced LDAP integration, you can set up synchronization between LDAP groups and teams. This enables LDAP users that are members of certain LDAP groups to automatically be added or removed as members to certain teams in Grafana. +![LDAP group synchronization](/static/img/docs/enterprise/team_members_ldap.png) + Grafana keeps track of all synchronized users in teams, and you can see which users have been synchronized from LDAP in the team members list, see `LDAP` label in screenshot. This mechanism allows Grafana to remove an existing synchronized user from a team when its LDAP group membership changes. This mechanism also allows you to manually add a user as member of a team, and it will not be removed when the user signs in. This gives you flexibility to combine LDAP group memberships and Grafana team memberships. diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/ldap/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/ldap/index.md index 9e62c44a5b..eece776c88 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/ldap/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/ldap/index.md @@ -151,19 +151,19 @@ Grafana has an LDAP debug view built-in which allows you to test your LDAP confi Within this view, you'll be able to see which LDAP servers are currently reachable and test your current configuration. -{{< figure src="/static/img/docs/ldap_debug.png" class="docs-image--no-shadow" max-width="600px" >}} +{{< figure src="/static/img/docs/ldap_debug.png" class="docs-image--no-shadow" max-width="600px" alt="LDAP testing" >}} To use the debug view, complete the following steps: 1. Type the username of a user that exists within any of your LDAP server(s) 1. Then, press "Run" -1. If the user is found within any of your LDAP instances, the mapping information is displayed +1. If the user is found within any of your LDAP instances, the mapping information is displayed. -{{< figure src="/static/img/docs/ldap_debug_mapping_testing.png" class="docs-image--no-shadow" max-width="600px" >}} +{{< figure src="/static/img/docs/ldap_debug_mapping_testing.png" class="docs-image--no-shadow" max-width="600px" alt="LDAP mapping displayed" >}} [Grafana Enterprise]({{< relref "../../../../introduction/grafana-enterprise" >}}) users with [enhanced LDAP integration]({{< relref "../enhanced-ldap" >}}) enabled can also see sync status in the debug view. This requires the `ldap.status:read` permission. -{{< figure src="/static/img/docs/ldap_sync_debug.png" class="docs-image--no-shadow" max-width="600px" >}} +{{< figure src="/static/img/docs/ldap_sync_debug.png" class="docs-image--no-shadow" max-width="600px" alt="LDAP sync status" >}} ### Bind and bind password diff --git a/docs/sources/setup-grafana/configure-security/configure-team-sync.md b/docs/sources/setup-grafana/configure-security/configure-team-sync.md index f7410d14c7..07f251ad25 100644 --- a/docs/sources/setup-grafana/configure-security/configure-team-sync.md +++ b/docs/sources/setup-grafana/configure-security/configure-team-sync.md @@ -40,11 +40,12 @@ This mechanism allows Grafana to remove an existing synchronized user from a tea If you have already grouped some users into a team, then you can synchronize that team with an external group. -{{< figure src="/static/img/docs/enterprise/team_add_external_group.png" class="docs-image--no-shadow docs-image--right" max-width= "600px" >}} - 1. In Grafana, navigate to **Administration > Users and access > Teams**. 1. Select a team. 1. Go to the External group sync tab, and click **Add group**. + + ![External group sync](/static/img/docs/enterprise/team_add_external_group.png) + 1. Insert the value of the group you want to sync with. This becomes the Grafana `GroupID`. Examples: From 7147af6b8e14d66653aa6dec59ab7a2965d9c1d0 Mon Sep 17 00:00:00 2001 From: Yuri Tseretyan Date: Thu, 7 Mar 2024 16:01:11 -0500 Subject: [PATCH 0472/1406] Alerting: Disable legacy alerting for ever (#83651) * hard disable for legacy alerting * remove alerting section from configuration file * update documentation to not refer to deleted section * remove AlertingEnabled from usage in UA setting parsing --- conf/defaults.ini | 45 +-- conf/sample.ini | 45 +-- .../setup-grafana/configure-grafana/_index.md | 68 +--- .../setup-grafana/image-rendering/_index.md | 2 +- pkg/setting/setting.go | 22 +- pkg/setting/setting_test.go | 370 ++---------------- pkg/setting/setting_unified_alerting.go | 47 +-- pkg/setting/setting_unified_alerting_test.go | 6 +- 8 files changed, 50 insertions(+), 555 deletions(-) diff --git a/conf/defaults.ini b/conf/defaults.ini index ae9a5b9f30..a6a43705c6 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -1200,17 +1200,17 @@ ha_gossip_interval = 200ms # The interval string is a possibly signed sequence of decimal numbers, followed by a unit suffix (ms, s, m, h, d), e.g. 30s or 1m. ha_push_pull_interval = 60s -# Enable or disable alerting rule execution. The alerting UI remains visible. This option has a legacy version in the `[alerting]` section that takes precedence. +# Enable or disable alerting rule execution. The alerting UI remains visible. execute_alerts = true -# Alert evaluation timeout when fetching data from the datasource. This option has a legacy version in the `[alerting]` section that takes precedence. +# Alert evaluation timeout when fetching data from the datasource. # The timeout string is a possibly signed sequence of decimal numbers, followed by a unit suffix (ms, s, m, h, d), e.g. 30s or 1m. evaluation_timeout = 30s # Number of times we'll attempt to evaluate an alert rule before giving up on that evaluation. The default value is 1. max_attempts = 1 -# Minimum interval to enforce between rule evaluations. Rules will be adjusted if they are less than this value or if they are not multiple of the scheduler interval (10s). Higher values can help with resource management as we'll schedule fewer evaluations over time. This option has a legacy version in the `[alerting]` section that takes precedence. +# Minimum interval to enforce between rule evaluations. Rules will be adjusted if they are less than this value or if they are not multiple of the scheduler interval (10s). Higher values can help with resource management as we'll schedule fewer evaluations over time. # The interval string is a possibly signed sequence of decimal numbers, followed by a unit suffix (ms, s, m, h, d), e.g. 30s or 1m. min_interval = 10s @@ -1347,45 +1347,6 @@ password = sync_interval = 5m -#################################### Alerting ############################ -[alerting] -# Enable the legacy alerting sub-system and interface. If Unified Alerting is already enabled and you try to go back to legacy alerting, all data that is part of Unified Alerting will be deleted. When this configuration section and flag are not defined, the state is defined at runtime. See the documentation for more details. -enabled = - -# Makes it possible to turn off alert execution but alerting UI is visible -execute_alerts = true - -# Default setting for new alert rules. Defaults to categorize error and timeouts as alerting. (alerting, keep_state) -error_or_timeout = alerting - -# Default setting for how Grafana handles nodata or null values in alerting. (alerting, no_data, keep_state, ok) -nodata_or_nullvalues = no_data - -# Alert notifications can include images, but rendering many images at the same time can overload the server -# This limit will protect the server from render overloading and make sure notifications are sent out quickly -concurrent_render_limit = 5 - -# Default setting for alert calculation timeout. Default value is 30 -evaluation_timeout_seconds = 30 - -# Default setting for alert notification timeout. Default value is 30 -notification_timeout_seconds = 30 - -# Default setting for max attempts to sending alert notifications. Default value is 3 -max_attempts = 3 - -# Makes it possible to enforce a minimal interval between evaluations, to reduce load on the backend -min_interval_seconds = 1 - -# Configures for how long alert annotations are stored. Default is 0, which keeps them forever. -# This setting should be expressed as an duration. Ex 6h (hours), 10d (days), 2w (weeks), 1M (month). -# Deprecated, use [annotations.alerting].max_age instead -max_annotation_age = - -# Configures max number of alert annotations that Grafana stores. Default value is 0, which keeps all alert annotations. -# Deprecated, use [annotations.alerting].max_annotations_to_keep instead -max_annotations_to_keep = - #################################### Annotations ######################### [annotations] # Configures the batch size for the annotation clean-up job. This setting is used for dashboard, API, and alert annotations. diff --git a/conf/sample.ini b/conf/sample.ini index 156e9fa485..8ceb3827db 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -1116,17 +1116,17 @@ # The interval string is a possibly signed sequence of decimal numbers, followed by a unit suffix (ms, s, m, h, d), e.g. 30s or 1m. ;ha_push_pull_interval = "60s" -# Enable or disable alerting rule execution. The alerting UI remains visible. This option has a legacy version in the `[alerting]` section that takes precedence. +# Enable or disable alerting rule execution. The alerting UI remains visible. ;execute_alerts = true -# Alert evaluation timeout when fetching data from the datasource. This option has a legacy version in the `[alerting]` section that takes precedence. +# Alert evaluation timeout when fetching data from the datasource. # The timeout string is a possibly signed sequence of decimal numbers, followed by a unit suffix (ms, s, m, h, d), e.g. 30s or 1m. ;evaluation_timeout = 30s # Number of times we'll attempt to evaluate an alert rule before giving up on that evaluation. The default value is 1. ;max_attempts = 1 -# Minimum interval to enforce between rule evaluations. Rules will be adjusted if they are less than this value or if they are not multiple of the scheduler interval (10s). Higher values can help with resource management as we'll schedule fewer evaluations over time. This option has a legacy version in the `[alerting]` section that takes precedence. +# Minimum interval to enforce between rule evaluations. Rules will be adjusted if they are less than this value or if they are not multiple of the scheduler interval (10s). Higher values can help with resource management as we'll schedule fewer evaluations over time. # The interval string is a possibly signed sequence of decimal numbers, followed by a unit suffix (ms, s, m, h, d), e.g. 30s or 1m. ;min_interval = 10s @@ -1216,45 +1216,6 @@ max_annotations_to_keep = # Unified Alerting. Should be kept false when not needed as it may cause unintended data-loss if left enabled. ;clean_upgrade = false -#################################### Alerting ############################ -[alerting] -# Disable legacy alerting engine & UI features -;enabled = false - -# Makes it possible to turn off alert execution but alerting UI is visible -;execute_alerts = true - -# Default setting for new alert rules. Defaults to categorize error and timeouts as alerting. (alerting, keep_state) -;error_or_timeout = alerting - -# Default setting for how Grafana handles nodata or null values in alerting. (alerting, no_data, keep_state, ok) -;nodata_or_nullvalues = no_data - -# Alert notifications can include images, but rendering many images at the same time can overload the server -# This limit will protect the server from render overloading and make sure notifications are sent out quickly -;concurrent_render_limit = 5 - -# Default setting for alert calculation timeout. Default value is 30 -;evaluation_timeout_seconds = 30 - -# Default setting for alert notification timeout. Default value is 30 -;notification_timeout_seconds = 30 - -# Default setting for max attempts to sending alert notifications. Default value is 3 -;max_attempts = 3 - -# Makes it possible to enforce a minimal interval between evaluations, to reduce load on the backend -;min_interval_seconds = 1 - -# Configures for how long alert annotations are stored. Default is 0, which keeps them forever. -# This setting should be expressed as a duration. Examples: 6h (hours), 10d (days), 2w (weeks), 1M (month). -# Deprecated, use [annotations.alerting].max_age instead -;max_annotation_age = - -# Configures max number of alert annotations that Grafana stores. Default value is 0, which keeps all alert annotations. -# Deprecated, use [annotations.alerting].max_annotations_to_keep instead -;max_annotations_to_keep = - #################################### Annotations ######################### [annotations] # Configures the batch size for the annotation clean-up job. This setting is used for dashboard, API, and alert annotations. diff --git a/docs/sources/setup-grafana/configure-grafana/_index.md b/docs/sources/setup-grafana/configure-grafana/_index.md index c4065eaf1e..579f9d3121 100644 --- a/docs/sources/setup-grafana/configure-grafana/_index.md +++ b/docs/sources/setup-grafana/configure-grafana/_index.md @@ -1608,11 +1608,11 @@ The interval string is a possibly signed sequence of decimal numbers, followed b ### execute_alerts -Enable or disable alerting rule execution. The default value is `true`. The alerting UI remains visible. This option has a [legacy version in the alerting section]({{< relref "#execute_alerts-1" >}}) that takes precedence. +Enable or disable alerting rule execution. The default value is `true`. The alerting UI remains visible. ### evaluation_timeout -Sets the alert evaluation timeout when fetching data from the data source. The default value is `30s`. This option has a [legacy version in the alerting section]({{< relref "#evaluation_timeout_seconds" >}}) that takes precedence. +Sets the alert evaluation timeout when fetching data from the data source. The default value is `30s`. The timeout string is a possibly signed sequence of decimal numbers, followed by a unit suffix (ms, s, m, h, d), e.g. 30s or 1m. @@ -1622,7 +1622,7 @@ Sets a maximum number of times we'll attempt to evaluate an alert rule before gi ### min_interval -Sets the minimum interval to enforce between rule evaluations. The default value is `10s` which equals the scheduler interval. Rules will be adjusted if they are less than this value or if they are not multiple of the scheduler interval (10s). Higher values can help with resource management as we'll schedule fewer evaluations over time. This option has [a legacy version in the alerting section]({{< relref "#min_interval_seconds" >}}) that takes precedence. +Sets the minimum interval to enforce between rule evaluations. The default value is `10s` which equals the scheduler interval. Rules will be adjusted if they are less than this value or if they are not multiple of the scheduler interval (10s). Higher values can help with resource management as we'll schedule fewer evaluations over time. The interval string is a possibly signed sequence of decimal numbers, followed by a unit suffix (ms, s, m, h, d), e.g. 30s or 1m. @@ -1690,68 +1690,6 @@ It should be kept false when not needed, as it may cause unintended data loss if
-## [alerting] - -For more information about the legacy dashboard alerting feature in Grafana, refer to [the legacy Grafana alerts](/docs/grafana/v8.5/alerting/old-alerting/). - -### enabled - -Set to `true` to [enable legacy dashboard alerting]({{< relref "#unified_alerting" >}}). The default value is `false`. - -### execute_alerts - -Turns off alert rule execution, but alerting is still visible in the Grafana UI. - -### error_or_timeout - -Default setting for new alert rules. Defaults to categorize error and timeouts as alerting. (alerting, keep_state) - -### nodata_or_nullvalues - -Defines how Grafana handles nodata or null values in alerting. Options are `alerting`, `no_data`, `keep_state`, and `ok`. Default is `no_data`. - -### concurrent_render_limit - -Alert notifications can include images, but rendering many images at the same time can overload the server. -This limit protects the server from render overloading and ensures notifications are sent out quickly. Default value is `5`. - -### evaluation_timeout_seconds - -Sets the alert calculation timeout. Default value is `30`. - -### notification_timeout_seconds - -Sets the alert notification timeout. Default value is `30`. - -### max_attempts - -Sets a maximum limit on attempts to sending alert notifications. Default value is `3`. - -### min_interval_seconds - -Sets the minimum interval between rule evaluations. Default value is `1`. - -> **Note.** This setting has precedence over each individual rule frequency. If a rule frequency is lower than this value, then this value is enforced. - -### max_annotation_age - -{{% admonition type="note" %}} -This option is deprecated - See `max_age` option in [unified_alerting.state_history.annotations]({{< relref "#unified_alertingstate_historyannotations" >}}) instead. -{{% /admonition %}} - -Configures for how long alert annotations are stored. Default is 0, which keeps them forever. -This setting should be expressed as a duration. Examples: 6h (hours), 10d (days), 2w (weeks), 1M (month). - -### max_annotations_to_keep - -{{% admonition type="note" %}} -This option is deprecated - See `max_annotations_to_keep` option in [unified_alerting.state_history.annotations]({{< relref "#unified_alertingstate_historyannotations" >}}) instead. -{{% /admonition %}} - -Configures max number of alert annotations that Grafana stores. Default value is 0, which keeps all alert annotations. - -
- ## [annotations] ### cleanupjob_batchsize diff --git a/docs/sources/setup-grafana/image-rendering/_index.md b/docs/sources/setup-grafana/image-rendering/_index.md index 61fd88e13d..cee1b84343 100644 --- a/docs/sources/setup-grafana/image-rendering/_index.md +++ b/docs/sources/setup-grafana/image-rendering/_index.md @@ -30,7 +30,7 @@ You can also render a PNG by hovering over the panel to display the actions menu ## Alerting and render limits -Alert notifications can include images, but rendering many images at the same time can overload the server where the renderer is running. For instructions of how to configure this, see [concurrent_render_limit]({{< relref "../configure-grafana#concurrent_render_limit" >}}). +Alert notifications can include images, but rendering many images at the same time can overload the server where the renderer is running. For instructions of how to configure this, see [max_concurrent_screenshots]({{< relref "../configure-grafana#max_concurrent_screenshots" >}}). ## Install Grafana Image Renderer plugin diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 916b296540..a3fe6b9f13 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -715,6 +715,8 @@ func (cfg *Cfg) readAnnotationSettings() error { alertingAnnotations := cfg.Raw.Section("unified_alerting.state_history.annotations") if alertingAnnotations.Key("max_age").Value() == "" && section.Key("max_annotations_to_keep").Value() == "" { + // Although this section is not documented anymore, we decided to keep it to avoid potential data-loss when user upgrades Grafana and does not change the setting. + // TODO delete some time after Grafana 11. alertingSection := cfg.Raw.Section("alerting") cleanup := newAnnotationCleanupSettings(alertingSection, "max_annotation_age") if cleanup.MaxCount > 0 || cleanup.MaxAge > 0 { @@ -1743,25 +1745,13 @@ func (cfg *Cfg) readRenderingSettings(iniFile *ini.File) error { } func (cfg *Cfg) readAlertingSettings(iniFile *ini.File) error { + // This check is kept to prevent users that upgrade to Grafana 11 with the legacy alerting enabled. This should prevent them from accidentally upgrading without migration to Unified Alerting. alerting := iniFile.Section("alerting") enabled, err := alerting.Key("enabled").Bool() - cfg.AlertingEnabled = nil - if err == nil { - cfg.AlertingEnabled = &enabled + if err == nil && enabled { + cfg.Logger.Error("Option '[alerting].enabled' cannot be true. Legacy Alerting is removed. It is no longer deployed, enhanced, or supported. Delete '[alerting].enabled' and use '[unified_alerting].enabled' to enable Grafana Alerting. For more information, refer to the documentation on upgrading to Grafana Alerting (https://grafana.com/docs/grafana/v10.4/alerting/set-up/migrating-alerts)") + return fmt.Errorf("invalid setting [alerting].enabled") } - cfg.ExecuteAlerts = alerting.Key("execute_alerts").MustBool(true) - cfg.AlertingRenderLimit = alerting.Key("concurrent_render_limit").MustInt(5) - - cfg.AlertingErrorOrTimeout = valueAsString(alerting, "error_or_timeout", "alerting") - cfg.AlertingNoDataOrNullValues = valueAsString(alerting, "nodata_or_nullvalues", "no_data") - - evaluationTimeoutSeconds := alerting.Key("evaluation_timeout_seconds").MustInt64(30) - cfg.AlertingEvaluationTimeout = time.Second * time.Duration(evaluationTimeoutSeconds) - notificationTimeoutSeconds := alerting.Key("notification_timeout_seconds").MustInt64(30) - cfg.AlertingNotificationTimeout = time.Second * time.Duration(notificationTimeoutSeconds) - cfg.AlertingMaxAttempts = alerting.Key("max_attempts").MustInt(3) - cfg.AlertingMinInterval = alerting.Key("min_interval_seconds").MustInt64(1) - return nil } diff --git a/pkg/setting/setting_test.go b/pkg/setting/setting_test.go index 00b302981b..57bce42da7 100644 --- a/pkg/setting/setting_test.go +++ b/pkg/setting/setting_test.go @@ -2,7 +2,6 @@ package setting import ( "bufio" - "math/rand" "net/url" "os" "path" @@ -463,356 +462,39 @@ func TestGetCDNPathWithAlphaVersion(t *testing.T) { } func TestAlertingEnabled(t *testing.T) { - anyBoolean := func() bool { - return rand.Int63()%2 == 0 - } + t.Run("fail if legacy alerting enabled", func(t *testing.T) { + f := ini.Empty() + cfg := NewCfg() - testCases := []struct { - desc string - unifiedAlertingEnabled string - legacyAlertingEnabled string - featureToggleSet bool - isEnterprise bool - verifyCfg func(*testing.T, Cfg, *ini.File) - }{ - { - desc: "when legacy alerting is enabled and unified is disabled", - legacyAlertingEnabled: "true", - unifiedAlertingEnabled: "false", - isEnterprise: anyBoolean(), - verifyCfg: func(t *testing.T, cfg Cfg, f *ini.File) { - err := cfg.readAlertingSettings(f) - require.NoError(t, err) - err = cfg.readFeatureToggles(f) - require.NoError(t, err) - err = cfg.ReadUnifiedAlertingSettings(f) - require.NoError(t, err) - assert.NotNil(t, cfg.UnifiedAlerting.Enabled) - assert.Equal(t, *cfg.UnifiedAlerting.Enabled, false) - assert.NotNil(t, cfg.AlertingEnabled) - assert.Equal(t, *(cfg.AlertingEnabled), true) - }, - }, - { - desc: "when legacy alerting is disabled and unified is enabled", - legacyAlertingEnabled: "false", - unifiedAlertingEnabled: "true", - isEnterprise: anyBoolean(), - verifyCfg: func(t *testing.T, cfg Cfg, f *ini.File) { - err := cfg.readAlertingSettings(f) - require.NoError(t, err) - err = cfg.readFeatureToggles(f) - require.NoError(t, err) - err = cfg.ReadUnifiedAlertingSettings(f) - require.NoError(t, err) - assert.NotNil(t, cfg.UnifiedAlerting.Enabled) - assert.Equal(t, *cfg.UnifiedAlerting.Enabled, true) - assert.NotNil(t, cfg.AlertingEnabled) - assert.Equal(t, *(cfg.AlertingEnabled), false) - }, - }, - { - desc: "when both alerting are enabled", - legacyAlertingEnabled: "true", - unifiedAlertingEnabled: "true", - isEnterprise: anyBoolean(), - verifyCfg: func(t *testing.T, cfg Cfg, f *ini.File) { - err := cfg.readAlertingSettings(f) - require.NoError(t, err) - err = cfg.readFeatureToggles(f) - require.NoError(t, err) - err = cfg.ReadUnifiedAlertingSettings(f) - require.Error(t, err) - }, - }, - { - desc: "when legacy alerting is invalid (or not defined) and unified is disabled", - legacyAlertingEnabled: "", - unifiedAlertingEnabled: "false", - isEnterprise: anyBoolean(), - verifyCfg: func(t *testing.T, cfg Cfg, f *ini.File) { - err := cfg.readAlertingSettings(f) - require.NoError(t, err) - err = cfg.readFeatureToggles(f) - require.NoError(t, err) - err = cfg.ReadUnifiedAlertingSettings(f) - require.NoError(t, err) - assert.NotNil(t, cfg.UnifiedAlerting.Enabled) - assert.Equal(t, *cfg.UnifiedAlerting.Enabled, false) - assert.NotNil(t, cfg.AlertingEnabled) - assert.Equal(t, *(cfg.AlertingEnabled), true) - }, - }, - { - desc: "when legacy alerting is invalid (or not defined) and unified is enabled", - legacyAlertingEnabled: "", - unifiedAlertingEnabled: "true", - isEnterprise: anyBoolean(), - verifyCfg: func(t *testing.T, cfg Cfg, f *ini.File) { - err := cfg.readAlertingSettings(f) - require.NoError(t, err) - err = cfg.readFeatureToggles(f) - require.NoError(t, err) - err = cfg.ReadUnifiedAlertingSettings(f) - require.NoError(t, err) - assert.NotNil(t, cfg.UnifiedAlerting.Enabled) - assert.Equal(t, *cfg.UnifiedAlerting.Enabled, true) - assert.NotNil(t, cfg.AlertingEnabled) - assert.Equal(t, *(cfg.AlertingEnabled), false) - }, - }, - { - desc: "when legacy alerting is enabled and unified is not defined [OSS]", - legacyAlertingEnabled: "true", - unifiedAlertingEnabled: "", - isEnterprise: false, - verifyCfg: func(t *testing.T, cfg Cfg, f *ini.File) { - err := cfg.readAlertingSettings(f) - require.NoError(t, err) - err = cfg.readFeatureToggles(f) - require.NoError(t, err) - err = cfg.ReadUnifiedAlertingSettings(f) - require.NoError(t, err) - assert.NotNil(t, cfg.UnifiedAlerting.Enabled) - assert.Equal(t, true, *cfg.UnifiedAlerting.Enabled) - assert.NotNil(t, cfg.AlertingEnabled) - assert.Equal(t, false, *(cfg.AlertingEnabled)) - }, - }, - { - desc: "when legacy alerting is enabled and unified is invalid [OSS]", - legacyAlertingEnabled: "true", - unifiedAlertingEnabled: "invalid", - isEnterprise: false, - verifyCfg: func(t *testing.T, cfg Cfg, f *ini.File) { - err := cfg.readAlertingSettings(f) - require.NoError(t, err) - err = cfg.readFeatureToggles(f) - require.NoError(t, err) - err = cfg.ReadUnifiedAlertingSettings(f) - assert.EqualError(t, err, "failed to read unified alerting enabled setting: invalid value invalid, should be either true or false") - assert.Nil(t, cfg.UnifiedAlerting.Enabled) - assert.NotNil(t, cfg.AlertingEnabled) - assert.Equal(t, false, *(cfg.AlertingEnabled)) - }, - }, - { - desc: "when legacy alerting is enabled and unified is not defined [Enterprise]", - legacyAlertingEnabled: "true", - unifiedAlertingEnabled: "", - isEnterprise: true, - verifyCfg: func(t *testing.T, cfg Cfg, f *ini.File) { - err := cfg.readAlertingSettings(f) - require.NoError(t, err) - err = cfg.readFeatureToggles(f) - require.NoError(t, err) - err = cfg.ReadUnifiedAlertingSettings(f) - require.NoError(t, err) - assert.NotNil(t, cfg.UnifiedAlerting.Enabled) - assert.Equal(t, true, *cfg.UnifiedAlerting.Enabled) - assert.NotNil(t, cfg.AlertingEnabled) - assert.Equal(t, false, *(cfg.AlertingEnabled)) - }, - }, - { - desc: "when legacy alerting is enabled and unified is invalid [Enterprise]", - legacyAlertingEnabled: "true", - unifiedAlertingEnabled: "invalid", - isEnterprise: false, - verifyCfg: func(t *testing.T, cfg Cfg, f *ini.File) { - err := cfg.readAlertingSettings(f) - require.NoError(t, err) - err = cfg.readFeatureToggles(f) - require.NoError(t, err) - err = cfg.ReadUnifiedAlertingSettings(f) - assert.EqualError(t, err, "failed to read unified alerting enabled setting: invalid value invalid, should be either true or false") - assert.Nil(t, cfg.UnifiedAlerting.Enabled) - assert.NotNil(t, cfg.AlertingEnabled) - assert.Equal(t, false, *(cfg.AlertingEnabled)) - }, - }, - { - desc: "when legacy alerting is disabled and unified is not defined [OSS]", - legacyAlertingEnabled: "false", - unifiedAlertingEnabled: "", - isEnterprise: false, - verifyCfg: func(t *testing.T, cfg Cfg, f *ini.File) { - err := cfg.readAlertingSettings(f) - require.NoError(t, err) - err = cfg.readFeatureToggles(f) - require.NoError(t, err) - err = cfg.ReadUnifiedAlertingSettings(f) - require.NoError(t, err) - assert.NotNil(t, cfg.UnifiedAlerting.Enabled) - assert.Equal(t, *cfg.UnifiedAlerting.Enabled, true) - assert.NotNil(t, cfg.AlertingEnabled) - assert.Equal(t, *(cfg.AlertingEnabled), false) - }, - }, - { - desc: "when legacy alerting is disabled and unified is invalid [OSS]", - legacyAlertingEnabled: "false", - unifiedAlertingEnabled: "invalid", - isEnterprise: false, - verifyCfg: func(t *testing.T, cfg Cfg, f *ini.File) { - err := cfg.readAlertingSettings(f) - require.NoError(t, err) - err = cfg.readFeatureToggles(f) - require.NoError(t, err) - err = cfg.ReadUnifiedAlertingSettings(f) - assert.EqualError(t, err, "failed to read unified alerting enabled setting: invalid value invalid, should be either true or false") - assert.Nil(t, cfg.UnifiedAlerting.Enabled) - assert.NotNil(t, cfg.AlertingEnabled) - assert.Equal(t, false, *(cfg.AlertingEnabled)) - }, - }, - { - desc: "when legacy alerting is disabled and unified is not defined [Enterprise]", - legacyAlertingEnabled: "false", - unifiedAlertingEnabled: "", - isEnterprise: true, - verifyCfg: func(t *testing.T, cfg Cfg, f *ini.File) { - err := cfg.readAlertingSettings(f) - require.NoError(t, err) - err = cfg.readFeatureToggles(f) - require.NoError(t, err) - err = cfg.ReadUnifiedAlertingSettings(f) - require.NoError(t, err) - assert.NotNil(t, cfg.UnifiedAlerting.Enabled) - assert.Equal(t, *cfg.UnifiedAlerting.Enabled, true) - assert.NotNil(t, cfg.AlertingEnabled) - assert.Equal(t, *(cfg.AlertingEnabled), false) - }, - }, - { - desc: "when legacy alerting is disabled and unified is invalid [Enterprise]", - legacyAlertingEnabled: "false", - unifiedAlertingEnabled: "invalid", - isEnterprise: false, - verifyCfg: func(t *testing.T, cfg Cfg, f *ini.File) { - err := cfg.readAlertingSettings(f) - require.NoError(t, err) - err = cfg.readFeatureToggles(f) - require.NoError(t, err) - err = cfg.ReadUnifiedAlertingSettings(f) - assert.EqualError(t, err, "failed to read unified alerting enabled setting: invalid value invalid, should be either true or false") - assert.Nil(t, cfg.UnifiedAlerting.Enabled) - assert.NotNil(t, cfg.AlertingEnabled) - assert.Equal(t, false, *(cfg.AlertingEnabled)) - }, - }, - { - desc: "when both are not defined [OSS]", - legacyAlertingEnabled: "", - unifiedAlertingEnabled: "", - isEnterprise: false, - verifyCfg: func(t *testing.T, cfg Cfg, f *ini.File) { - err := cfg.readAlertingSettings(f) - require.NoError(t, err) - err = cfg.readFeatureToggles(f) - require.NoError(t, err) - err = cfg.ReadUnifiedAlertingSettings(f) - require.NoError(t, err) - assert.NotNil(t, cfg.UnifiedAlerting.Enabled) - assert.True(t, *cfg.UnifiedAlerting.Enabled) - assert.Nil(t, cfg.AlertingEnabled) - }, - }, - { - desc: "when both are not invalid [OSS]", - legacyAlertingEnabled: "invalid", - unifiedAlertingEnabled: "invalid", - isEnterprise: false, - verifyCfg: func(t *testing.T, cfg Cfg, f *ini.File) { - err := cfg.readAlertingSettings(f) - require.NoError(t, err) - err = cfg.readFeatureToggles(f) - require.NoError(t, err) - err = cfg.ReadUnifiedAlertingSettings(f) - assert.EqualError(t, err, "failed to read unified alerting enabled setting: invalid value invalid, should be either true or false") - assert.Nil(t, cfg.UnifiedAlerting.Enabled) - assert.NotNil(t, cfg.AlertingEnabled) - assert.Equal(t, false, *(cfg.AlertingEnabled)) - }, - }, - { - desc: "when both are not defined [Enterprise]", - legacyAlertingEnabled: "", - unifiedAlertingEnabled: "", - isEnterprise: true, - verifyCfg: func(t *testing.T, cfg Cfg, f *ini.File) { - err := cfg.readAlertingSettings(f) - require.NoError(t, err) - err = cfg.readFeatureToggles(f) - require.NoError(t, err) - err = cfg.ReadUnifiedAlertingSettings(f) - require.NoError(t, err) - assert.NotNil(t, cfg.UnifiedAlerting.Enabled) - assert.True(t, *cfg.UnifiedAlerting.Enabled) - assert.Nil(t, cfg.AlertingEnabled) - }, - }, - { - desc: "when both are not invalid [Enterprise]", - legacyAlertingEnabled: "invalid", - unifiedAlertingEnabled: "invalid", - isEnterprise: false, - verifyCfg: func(t *testing.T, cfg Cfg, f *ini.File) { - err := cfg.readAlertingSettings(f) - require.NoError(t, err) - err = cfg.readFeatureToggles(f) - require.NoError(t, err) - err = cfg.ReadUnifiedAlertingSettings(f) - assert.EqualError(t, err, "failed to read unified alerting enabled setting: invalid value invalid, should be either true or false") - assert.Nil(t, cfg.UnifiedAlerting.Enabled) - assert.NotNil(t, cfg.AlertingEnabled) - assert.Equal(t, false, *(cfg.AlertingEnabled)) - }, - }, - { - desc: "when both are false", - legacyAlertingEnabled: "false", - unifiedAlertingEnabled: "false", - isEnterprise: anyBoolean(), - verifyCfg: func(t *testing.T, cfg Cfg, f *ini.File) { - err := cfg.readAlertingSettings(f) - require.NoError(t, err) - err = cfg.readFeatureToggles(f) - require.NoError(t, err) - err = cfg.ReadUnifiedAlertingSettings(f) - require.NoError(t, err) - assert.NotNil(t, cfg.UnifiedAlerting.Enabled) - assert.Equal(t, *cfg.UnifiedAlerting.Enabled, false) - assert.NotNil(t, cfg.AlertingEnabled) - assert.Equal(t, *(cfg.AlertingEnabled), false) - }, - }, - } + alertingSec, err := f.NewSection("alerting") + require.NoError(t, err) + _, err = alertingSec.NewKey("enabled", "true") + require.NoError(t, err) - var isEnterpriseOld = IsEnterprise - t.Cleanup(func() { - IsEnterprise = isEnterpriseOld + require.Error(t, cfg.readAlertingSettings(f)) }) - for _, tc := range testCases { - t.Run(tc.desc, func(t *testing.T) { - IsEnterprise = tc.isEnterprise + t.Run("do nothing if it is disabled", func(t *testing.T) { + f := ini.Empty() + cfg := NewCfg() - f := ini.Empty() - cfg := NewCfg() - unifiedAlertingSec, err := f.NewSection("unified_alerting") - require.NoError(t, err) - _, err = unifiedAlertingSec.NewKey("enabled", tc.unifiedAlertingEnabled) - require.NoError(t, err) + alertingSec, err := f.NewSection("alerting") + require.NoError(t, err) + _, err = alertingSec.NewKey("enabled", "false") + require.NoError(t, err) + require.NoError(t, cfg.readAlertingSettings(f)) + }) - alertingSec, err := f.NewSection("alerting") - require.NoError(t, err) - _, err = alertingSec.NewKey("enabled", tc.legacyAlertingEnabled) - require.NoError(t, err) + t.Run("do nothing if it invalid", func(t *testing.T) { + f := ini.Empty() + cfg := NewCfg() - tc.verifyCfg(t, *cfg, f) - }) - } + alertingSec, err := f.NewSection("alerting") + require.NoError(t, err) + _, err = alertingSec.NewKey("enabled", "test") + require.NoError(t, err) + require.NoError(t, cfg.readAlertingSettings(f)) + }) } func TestRedactedValue(t *testing.T) { diff --git a/pkg/setting/setting_unified_alerting.go b/pkg/setting/setting_unified_alerting.go index 0b09df67c9..2888d0d57b 100644 --- a/pkg/setting/setting_unified_alerting.go +++ b/pkg/setting/setting_unified_alerting.go @@ -1,7 +1,6 @@ package setting import ( - "errors" "fmt" "strconv" "strings" @@ -46,7 +45,7 @@ const ( ` evaluatorDefaultEvaluationTimeout = 30 * time.Second schedulerDefaultAdminConfigPollInterval = time.Minute - schedulereDefaultExecuteAlerts = true + schedulerDefaultExecuteAlerts = true schedulerDefaultMaxAttempts = 1 schedulerDefaultLegacyMinInterval = 1 screenshotsDefaultCapture = false @@ -166,49 +165,13 @@ func (cfg *Cfg) readUnifiedAlertingEnabledSetting(section *ini.Section) (*bool, // At present an invalid value is considered the same as no value. This means that a // spelling mistake in the string "false" could enable unified alerting rather // than disable it. This issue can be found here - hasEnabled := section.Key("enabled").Value() != "" - if !hasEnabled { - // TODO: Remove in Grafana v10 - if cfg.IsFeatureToggleEnabled("ngalert") { - cfg.Logger.Warn("ngalert feature flag is deprecated: use unified alerting enabled setting instead") - // feature flag overrides the legacy alerting setting - legacyAlerting := false - cfg.AlertingEnabled = &legacyAlerting - unifiedAlerting := true - return &unifiedAlerting, nil - } - - // if legacy alerting has not been configured then enable unified alerting - if cfg.AlertingEnabled == nil { - unifiedAlerting := true - return &unifiedAlerting, nil - } - - // enable unified alerting and disable legacy alerting - legacyAlerting := false - cfg.AlertingEnabled = &legacyAlerting - unifiedAlerting := true - return &unifiedAlerting, nil + if section.Key("enabled").Value() == "" { + return util.Pointer(true), nil } - unifiedAlerting, err := section.Key("enabled").Bool() if err != nil { - // the value for unified alerting is invalid so disable all alerting - legacyAlerting := false - cfg.AlertingEnabled = &legacyAlerting return nil, fmt.Errorf("invalid value %s, should be either true or false", section.Key("enabled")) } - - // If both legacy and unified alerting are enabled then return an error - if cfg.AlertingEnabled != nil && *(cfg.AlertingEnabled) && unifiedAlerting { - return nil, errors.New("legacy and unified alerting cannot both be enabled at the same time, please disable one of them and restart Grafana") - } - - if cfg.AlertingEnabled == nil { - legacyAlerting := !unifiedAlerting - cfg.AlertingEnabled = &legacyAlerting - } - return &unifiedAlerting, nil } @@ -277,9 +240,9 @@ func (cfg *Cfg) ReadUnifiedAlertingSettings(iniFile *ini.File) error { alerting := iniFile.Section("alerting") - uaExecuteAlerts := ua.Key("execute_alerts").MustBool(schedulereDefaultExecuteAlerts) + uaExecuteAlerts := ua.Key("execute_alerts").MustBool(schedulerDefaultExecuteAlerts) if uaExecuteAlerts { // unified option equals the default (true) - legacyExecuteAlerts := alerting.Key("execute_alerts").MustBool(schedulereDefaultExecuteAlerts) + legacyExecuteAlerts := alerting.Key("execute_alerts").MustBool(schedulerDefaultExecuteAlerts) if !legacyExecuteAlerts { cfg.Logger.Warn("falling back to legacy setting of 'execute_alerts'; please use the configuration option in the `unified_alerting` section if Grafana 8 alerts are enabled.") } diff --git a/pkg/setting/setting_unified_alerting_test.go b/pkg/setting/setting_unified_alerting_test.go index cbbb30bd64..4e7285203f 100644 --- a/pkg/setting/setting_unified_alerting_test.go +++ b/pkg/setting/setting_unified_alerting_test.go @@ -93,7 +93,7 @@ func TestUnifiedAlertingSettings(t *testing.T) { alertingOptions: map[string]string{ "max_attempts": strconv.FormatInt(schedulerDefaultMaxAttempts, 10), "min_interval_seconds": strconv.FormatInt(schedulerDefaultLegacyMinInterval, 10), - "execute_alerts": strconv.FormatBool(schedulereDefaultExecuteAlerts), + "execute_alerts": strconv.FormatBool(schedulerDefaultExecuteAlerts), "evaluation_timeout_seconds": strconv.FormatInt(int64(evaluatorDefaultEvaluationTimeout.Seconds()), 10), }, verifyCfg: func(t *testing.T, cfg Cfg) { @@ -111,7 +111,7 @@ func TestUnifiedAlertingSettings(t *testing.T) { unifiedAlertingOptions: map[string]string{ "admin_config_poll_interval": "120s", "min_interval": SchedulerBaseInterval.String(), - "execute_alerts": strconv.FormatBool(schedulereDefaultExecuteAlerts), + "execute_alerts": strconv.FormatBool(schedulerDefaultExecuteAlerts), "evaluation_timeout": evaluatorDefaultEvaluationTimeout.String(), }, alertingOptions: map[string]string{ @@ -148,7 +148,7 @@ func TestUnifiedAlertingSettings(t *testing.T) { require.Equal(t, alertmanagerDefaultConfigPollInterval, cfg.UnifiedAlerting.AdminConfigPollInterval) require.Equal(t, int64(schedulerDefaultMaxAttempts), cfg.UnifiedAlerting.MaxAttempts) require.Equal(t, SchedulerBaseInterval, cfg.UnifiedAlerting.MinInterval) - require.Equal(t, schedulereDefaultExecuteAlerts, cfg.UnifiedAlerting.ExecuteAlerts) + require.Equal(t, schedulerDefaultExecuteAlerts, cfg.UnifiedAlerting.ExecuteAlerts) require.Equal(t, evaluatorDefaultEvaluationTimeout, cfg.UnifiedAlerting.EvaluationTimeout) require.Equal(t, SchedulerBaseInterval, cfg.UnifiedAlerting.BaseInterval) require.Equal(t, DefaultRuleEvaluationInterval, cfg.UnifiedAlerting.DefaultRuleEvaluationInterval) From 21719a6b5bb8e82708d2b12459065c8283a61bfe Mon Sep 17 00:00:00 2001 From: Yuri Tseretyan Date: Thu, 7 Mar 2024 16:34:22 -0500 Subject: [PATCH 0473/1406] Chore: Fix log message in access control (#84101) --- pkg/services/accesscontrol/acimpl/accesscontrol.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/services/accesscontrol/acimpl/accesscontrol.go b/pkg/services/accesscontrol/acimpl/accesscontrol.go index 4a6c6a0998..b26900ca0b 100644 --- a/pkg/services/accesscontrol/acimpl/accesscontrol.go +++ b/pkg/services/accesscontrol/acimpl/accesscontrol.go @@ -72,5 +72,5 @@ func (a *AccessControl) RegisterScopeAttributeResolver(prefix string, resolver a func (a *AccessControl) debug(ctx context.Context, ident identity.Requester, msg string, eval accesscontrol.Evaluator) { namespace, id := ident.GetNamespacedID() - a.log.FromContext(ctx).Debug(msg, "namespace", namespace, "id", id, "orgID", ident.GetOrgID(), eval.GoString()) + a.log.FromContext(ctx).Debug(msg, "namespace", namespace, "id", id, "orgID", ident.GetOrgID(), "permissions", eval.GoString()) } From f11b10a10cefcb12e6825f6ed9a20cf77d8dcc39 Mon Sep 17 00:00:00 2001 From: Leon Sorokin Date: Thu, 7 Mar 2024 18:10:56 -0600 Subject: [PATCH 0474/1406] BarChart: Show tooltip options unconditionally (#84109) --- public/app/plugins/panel/barchart/module.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/public/app/plugins/panel/barchart/module.tsx b/public/app/plugins/panel/barchart/module.tsx index 21bc79e703..6fb9254af2 100644 --- a/public/app/plugins/panel/barchart/module.tsx +++ b/public/app/plugins/panel/barchart/module.tsx @@ -236,10 +236,7 @@ export const plugin = new PanelPlugin(BarChartPanel) description: 'Use the color value for a sibling field to color each bar value.', }); - if (!context.options?.fullHighlight || context.options?.stacking === StackingMode.None) { - commonOptionsBuilder.addTooltipOptions(builder); - } - + commonOptionsBuilder.addTooltipOptions(builder); commonOptionsBuilder.addLegendOptions(builder); commonOptionsBuilder.addTextSizeOptions(builder, false); }) From d82f3be6f7c3a9f4ea5522eea5dde09e151f41bd Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Fri, 8 Mar 2024 08:12:59 -0800 Subject: [PATCH 0475/1406] QueryService: Use types from sdk (#84029) --- hack/update-codegen.sh | 4 +- .../v0alpha1/{types.go => datasource.go} | 16 -- pkg/apis/query/v0alpha1/query.go | 249 +++--------------- pkg/apis/query/v0alpha1/query_test.go | 11 +- pkg/apis/query/v0alpha1/register.go | 6 + pkg/apis/query/v0alpha1/results.go | 64 ----- pkg/apis/query/v0alpha1/template/render.go | 5 +- .../query/v0alpha1/template/render_test.go | 15 +- pkg/apis/query/v0alpha1/template/types.go | 4 +- .../query/v0alpha1/zz_generated.deepcopy.go | 86 ++++-- .../query/v0alpha1/zz_generated.openapi.go | 202 +++++++------- ...enerated.openapi_violation_exceptions.list | 4 - pkg/apiserver/builder/openapi.go | 2 + pkg/expr/nodes.go | 6 +- pkg/expr/reader.go | 18 +- pkg/registry/apis/datasource/register.go | 110 +++++++- pkg/registry/apis/datasource/sub_query.go | 80 +++--- pkg/registry/apis/peakq/render_examples.go | 6 +- .../apis/peakq/render_examples_test.go | 4 +- pkg/registry/apis/query/client.go | 22 ++ .../{runner/direct.go => client/plugin.go} | 92 +++---- .../{runner/dummy.go => client/testdata.go} | 51 ++-- pkg/registry/apis/query/metrics.go | 48 ++++ pkg/registry/apis/query/parser.go | 229 ++++++++++++---- pkg/registry/apis/query/parser_test.go | 131 +++++++++ pkg/registry/apis/query/query.go | 205 ++++++++++---- pkg/registry/apis/query/register.go | 241 ++++++++++------- .../query/testdata/cyclic-references.json | 29 ++ .../testdata/multiple-uids-same-plugin.json | 60 +++++ .../apis/query/testdata/self-reference.json | 20 ++ .../apis/query/testdata/with-expressions.json | 79 ++++++ pkg/server/wire.go | 1 + pkg/services/apiserver/standalone/factory.go | 15 +- pkg/services/datasources/service/legacy.go | 90 +++++++ pkg/tests/apis/query/query_test.go | 90 ++++--- pkg/tsdb/legacydata/conversions.go | 9 +- 36 files changed, 1490 insertions(+), 814 deletions(-) rename pkg/apis/query/v0alpha1/{types.go => datasource.go} (77%) delete mode 100644 pkg/apis/query/v0alpha1/results.go create mode 100644 pkg/registry/apis/query/client.go rename pkg/registry/apis/query/{runner/direct.go => client/plugin.go} (62%) rename pkg/registry/apis/query/{runner/dummy.go => client/testdata.go} (54%) create mode 100644 pkg/registry/apis/query/metrics.go create mode 100644 pkg/registry/apis/query/parser_test.go create mode 100644 pkg/registry/apis/query/testdata/cyclic-references.json create mode 100644 pkg/registry/apis/query/testdata/multiple-uids-same-plugin.json create mode 100644 pkg/registry/apis/query/testdata/self-reference.json create mode 100644 pkg/registry/apis/query/testdata/with-expressions.json create mode 100644 pkg/services/datasources/service/legacy.go diff --git a/hack/update-codegen.sh b/hack/update-codegen.sh index de30331ed0..bb6e20502c 100755 --- a/hack/update-codegen.sh +++ b/hack/update-codegen.sh @@ -24,11 +24,11 @@ grafana::codegen:run() { local generate_root=$1 local skipped="true" for api_pkg in $(grafana:codegen:lsdirs ./${generate_root}/apis); do - echo "Generating code for ${generate_root}/apis/${api_pkg}..." - echo "=============================================" if [[ "${selected_pkg}" != "" && ${api_pkg} != $selected_pkg ]]; then continue fi + echo "Generating code for ${generate_root}/apis/${api_pkg}..." + echo "=============================================" skipped="false" include_common_input_dirs=$([[ ${api_pkg} == "common" ]] && echo "true" || echo "false") diff --git a/pkg/apis/query/v0alpha1/types.go b/pkg/apis/query/v0alpha1/datasource.go similarity index 77% rename from pkg/apis/query/v0alpha1/types.go rename to pkg/apis/query/v0alpha1/datasource.go index 3f288eee25..1a39487c93 100644 --- a/pkg/apis/query/v0alpha1/types.go +++ b/pkg/apis/query/v0alpha1/datasource.go @@ -3,26 +3,10 @@ package v0alpha1 import ( "context" - "github.com/grafana/grafana-plugin-sdk-go/backend" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" ) -// The query runner interface -type QueryRunner interface { - // Runs the query as the user in context - ExecuteQueryData(ctx context.Context, - // The k8s group for the datasource (pluginId) - datasource schema.GroupVersion, - - // The datasource name/uid - name string, - - // The raw backend query objects - query []GenericDataQuery, - ) (*backend.QueryDataResponse, error) -} - type DataSourceApiServerRegistry interface { // Get the group and preferred version for a plugin GetDatasourceGroupVersion(pluginId string) (schema.GroupVersion, error) diff --git a/pkg/apis/query/v0alpha1/query.go b/pkg/apis/query/v0alpha1/query.go index bb11d9ae65..dd7ce99c39 100644 --- a/pkg/apis/query/v0alpha1/query.go +++ b/pkg/apis/query/v0alpha1/query.go @@ -1,240 +1,59 @@ package v0alpha1 import ( - "encoding/json" - "fmt" + "net/http" + "github.com/grafana/grafana-plugin-sdk-go/backend" + data "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - runtime "k8s.io/apimachinery/pkg/runtime" ) // Generic query request with shared time across all values // Copied from: https://github.com/grafana/grafana/blob/main/pkg/api/dtos/models.go#L62 -type GenericQueryRequest struct { +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type QueryDataRequest struct { metav1.TypeMeta `json:",inline"` - // From Start time in epoch timestamps in milliseconds or relative using Grafana time units. - // example: now-1h - From string `json:"from,omitempty"` - - // To End time in epoch timestamps in milliseconds or relative using Grafana time units. - // example: now - To string `json:"to,omitempty"` - - // queries.refId – Specifies an identifier of the query. Is optional and default to “A”. - // queries.datasourceId – Specifies the data source to be queried. Each query in the request must have an unique datasourceId. - // queries.maxDataPoints - Species maximum amount of data points that dashboard panel can render. Is optional and default to 100. - // queries.intervalMs - Specifies the time interval in milliseconds of time series. Is optional and defaults to 1000. - // required: true - // example: [ { "refId": "A", "intervalMs": 86400000, "maxDataPoints": 1092, "datasource":{ "uid":"PD8C576611E62080A" }, "rawSql": "SELECT 1 as valueOne, 2 as valueTwo", "format": "table" } ] - Queries []GenericDataQuery `json:"queries"` - - // required: false - Debug bool `json:"debug,omitempty"` -} - -type DataSourceRef struct { - // The datasource plugin type - Type string `json:"type"` - - // Datasource UID - UID string `json:"uid"` -} - -// GenericDataQuery is a replacement for `dtos.MetricRequest` that provides more explicit types -type GenericDataQuery struct { - // RefID is the unique identifier of the query, set by the frontend call. - RefID string `json:"refId"` - - // TimeRange represents the query range - // NOTE: unlike generic /ds/query, we can now send explicit time values in each query - TimeRange *TimeRange `json:"timeRange,omitempty"` - - // The datasource - Datasource *DataSourceRef `json:"datasource,omitempty"` - - // Deprecated -- use datasource ref instead - DatasourceId int64 `json:"datasourceId,omitempty"` - - // QueryType is an optional identifier for the type of query. - // It can be used to distinguish different types of queries. - QueryType string `json:"queryType,omitempty"` - - // MaxDataPoints is the maximum number of data points that should be returned from a time series query. - MaxDataPoints int64 `json:"maxDataPoints,omitempty"` - - // Interval is the suggested duration between time points in a time series query. - IntervalMS float64 `json:"intervalMs,omitempty"` - - // 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"` - - // Additional Properties (that live at the root) - props map[string]any `json:"-"` -} - -func NewGenericDataQuery(vals map[string]any) GenericDataQuery { - q := GenericDataQuery{} - _ = q.unmarshal(vals) - return q -} - -// TimeRange represents a time range for a query and is a property of DataQuery. -type TimeRange struct { - // From is the start time of the query. - From string `json:"from"` - - // To is the end time of the query. - To string `json:"to"` + // The time range used when not included on each query + data.QueryDataRequest `json:",inline"` } -func (g *GenericDataQuery) AdditionalProperties() map[string]any { - if g.props == nil { - g.props = make(map[string]any) - } - return g.props -} +// Wraps backend.QueryDataResponse, however it includes TypeMeta and implements runtime.Object +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type QueryDataResponse struct { + metav1.TypeMeta `json:",inline"` -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (g *GenericDataQuery) DeepCopyInto(out *GenericDataQuery) { - *out = *g - if g.props != nil { - out.props = runtime.DeepCopyJSON(g.props) - } + // Backend wrapper (external dependency) + backend.QueryDataResponse `json:",inline"` } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GenericDataQuery. -func (g *GenericDataQuery) DeepCopy() *GenericDataQuery { - if g == nil { - return nil +// If errors exist, return multi-status +func GetResponseCode(rsp *backend.QueryDataResponse) int { + if rsp == nil { + return http.StatusInternalServerError } - out := new(GenericDataQuery) - g.DeepCopyInto(out) - return out -} - -// MarshalJSON ensures that the unstructured object produces proper -// JSON when passed to Go's standard JSON library. -func (g GenericDataQuery) MarshalJSON() ([]byte, error) { - vals := map[string]any{} - if g.props != nil { - for k, v := range g.props { - vals[k] = v + for _, v := range rsp.Responses { + if v.Error != nil { + return http.StatusMultiStatus } } - - vals["refId"] = g.RefID - if g.Datasource != nil && (g.Datasource.Type != "" || g.Datasource.UID != "") { - vals["datasource"] = g.Datasource - } - if g.DatasourceId > 0 { - vals["datasourceId"] = g.DatasourceId - } - if g.IntervalMS > 0 { - vals["intervalMs"] = g.IntervalMS - } - if g.MaxDataPoints > 0 { - vals["maxDataPoints"] = g.MaxDataPoints - } - if g.QueryType != "" { - vals["queryType"] = g.QueryType - } - return json.Marshal(vals) -} - -// UnmarshalJSON ensures that the unstructured object properly decodes -// JSON when passed to Go's standard JSON library. -func (g *GenericDataQuery) UnmarshalJSON(b []byte) error { - vals := map[string]any{} - err := json.Unmarshal(b, &vals) - if err != nil { - return err - } - return g.unmarshal(vals) + return http.StatusOK } -func (g *GenericDataQuery) unmarshal(vals map[string]any) error { - if vals == nil { - g.props = nil - return nil - } +// Defines a query behavior in a datasource. This is a similar model to a CRD where the +// payload describes a valid query +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type QueryTypeDefinition struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` - key := "refId" - v, ok := vals[key] - if ok { - g.RefID, ok = v.(string) - if !ok { - return fmt.Errorf("expected string refid (got: %t)", v) - } - delete(vals, key) - } - - key = "datasource" - v, ok = vals[key] - if ok { - wrap, ok := v.(map[string]any) - if ok { - g.Datasource = &DataSourceRef{} - g.Datasource.Type, _ = wrap["type"].(string) - g.Datasource.UID, _ = wrap["uid"].(string) - delete(vals, key) - } else { - // Old old queries may arrive with just the name - name, ok := v.(string) - if !ok { - return fmt.Errorf("expected datasource as object (got: %t)", v) - } - g.Datasource = &DataSourceRef{} - g.Datasource.UID = name // Not great, but the lookup function will try its best to resolve - delete(vals, key) - } - } - - key = "intervalMs" - v, ok = vals[key] - if ok { - g.IntervalMS, ok = v.(float64) - if !ok { - return fmt.Errorf("expected intervalMs as float (got: %t)", v) - } - delete(vals, key) - } - - key = "maxDataPoints" - v, ok = vals[key] - if ok { - count, ok := v.(float64) - if !ok { - return fmt.Errorf("expected maxDataPoints as number (got: %t)", v) - } - g.MaxDataPoints = int64(count) - delete(vals, key) - } - - key = "datasourceId" - v, ok = vals[key] - if ok { - count, ok := v.(float64) - if !ok { - return fmt.Errorf("expected datasourceId as number (got: %t)", v) - } - g.DatasourceId = int64(count) - delete(vals, key) - } + Spec data.QueryTypeDefinitionSpec `json:"spec,omitempty"` +} - key = "queryType" - v, ok = vals[key] - if ok { - queryType, ok := v.(string) - if !ok { - return fmt.Errorf("expected queryType as string (got: %t)", v) - } - g.QueryType = queryType - delete(vals, key) - } +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type QueryTypeDefinitionList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` - g.props = vals - return nil + Items []QueryTypeDefinition `json:"items,omitempty"` } diff --git a/pkg/apis/query/v0alpha1/query_test.go b/pkg/apis/query/v0alpha1/query_test.go index 351593e569..03656d5b41 100644 --- a/pkg/apis/query/v0alpha1/query_test.go +++ b/pkg/apis/query/v0alpha1/query_test.go @@ -4,9 +4,10 @@ import ( "encoding/json" "testing" + data "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1" "github.com/stretchr/testify/require" - "github.com/grafana/grafana/pkg/apis/query/v0alpha1" + query "github.com/grafana/grafana/pkg/apis/query/v0alpha1" ) func TestParseQueriesIntoQueryDataRequest(t *testing.T) { @@ -39,23 +40,23 @@ func TestParseQueriesIntoQueryDataRequest(t *testing.T) { "to": "1692646267389" }`) - req := &v0alpha1.GenericQueryRequest{} + req := &query.QueryDataRequest{} err := json.Unmarshal(request, req) require.NoError(t, err) require.Len(t, req.Queries, 2) require.Equal(t, "b1808c48-9fc9-4045-82d7-081781f8a553", req.Queries[0].Datasource.UID) - require.Equal(t, "spreadsheetID", req.Queries[0].AdditionalProperties()["spreadsheet"]) + require.Equal(t, "spreadsheetID", req.Queries[0].GetString("spreadsheet")) // Write the query (with additional spreadsheetID) to JSON out, err := json.MarshalIndent(req.Queries[0], "", " ") require.NoError(t, err) // And read it back with standard JSON marshal functions - query := &v0alpha1.GenericDataQuery{} + query := &data.DataQuery{} err = json.Unmarshal(out, query) require.NoError(t, err) - require.Equal(t, "spreadsheetID", query.AdditionalProperties()["spreadsheet"]) + require.Equal(t, "spreadsheetID", query.GetString("spreadsheet")) // The second query has an explicit time range, and legacy datasource name out, err = json.MarshalIndent(req.Queries[1], "", " ") diff --git a/pkg/apis/query/v0alpha1/register.go b/pkg/apis/query/v0alpha1/register.go index a64293f9be..c62fc559eb 100644 --- a/pkg/apis/query/v0alpha1/register.go +++ b/pkg/apis/query/v0alpha1/register.go @@ -19,6 +19,12 @@ var DataSourceApiServerResourceInfo = common.NewResourceInfo(GROUP, VERSION, func() runtime.Object { return &DataSourceApiServerList{} }, ) +var QueryTypeDefinitionResourceInfo = common.NewResourceInfo(GROUP, VERSION, + "querytypes", "querytype", "QueryTypeDefinition", + func() runtime.Object { return &QueryTypeDefinition{} }, + func() runtime.Object { return &QueryTypeDefinitionList{} }, +) + var ( // SchemeGroupVersion is group version used to register these objects SchemeGroupVersion = schema.GroupVersion{Group: GROUP, Version: VERSION} diff --git a/pkg/apis/query/v0alpha1/results.go b/pkg/apis/query/v0alpha1/results.go deleted file mode 100644 index 2c3cab7b3f..0000000000 --- a/pkg/apis/query/v0alpha1/results.go +++ /dev/null @@ -1,64 +0,0 @@ -package v0alpha1 - -import ( - "encoding/json" - - "github.com/grafana/grafana-plugin-sdk-go/backend" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - openapi "k8s.io/kube-openapi/pkg/common" - spec "k8s.io/kube-openapi/pkg/validation/spec" -) - -// Wraps backend.QueryDataResponse, however it includes TypeMeta and implements runtime.Object -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type QueryDataResponse struct { - metav1.TypeMeta `json:",inline"` - - // Backend wrapper (external dependency) - backend.QueryDataResponse -} - -// Expose backend DataResponse in OpenAPI (yes this still requires some serious love!) -func (r QueryDataResponse) OpenAPIDefinition() openapi.OpenAPIDefinition { - return openapi.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - AdditionalProperties: &spec.SchemaOrBool{Allows: true}, - }, - VendorExtensible: spec.VendorExtensible{ - Extensions: map[string]interface{}{ - "x-kubernetes-preserve-unknown-fields": true, - }, - }, - }, - } -} - -// MarshalJSON writes the results as json -func (r QueryDataResponse) MarshalJSON() ([]byte, error) { - return r.QueryDataResponse.MarshalJSON() -} - -// UnmarshalJSON will read JSON into a QueryDataResponse -func (r *QueryDataResponse) UnmarshalJSON(b []byte) error { - return r.QueryDataResponse.UnmarshalJSON(b) -} - -func (r *QueryDataResponse) DeepCopy() *QueryDataResponse { - if r == nil { - return nil - } - - // /!\ The most dumb approach, but OK for now... - // likely best to move DeepCopy into SDK - out := &QueryDataResponse{} - body, _ := json.Marshal(r.QueryDataResponse) - _ = json.Unmarshal(body, &out.QueryDataResponse) - return out -} - -func (r *QueryDataResponse) DeepCopyInto(out *QueryDataResponse) { - clone := r.DeepCopy() - *out = *clone -} diff --git a/pkg/apis/query/v0alpha1/template/render.go b/pkg/apis/query/v0alpha1/template/render.go index 0f0b6f3942..d657b64240 100644 --- a/pkg/apis/query/v0alpha1/template/render.go +++ b/pkg/apis/query/v0alpha1/template/render.go @@ -4,9 +4,8 @@ import ( "fmt" "sort" + data "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1" "github.com/spyzhov/ajson" - - query "github.com/grafana/grafana/pkg/apis/query/v0alpha1" ) // RenderTemplate applies selected values into a query template @@ -62,7 +61,7 @@ func RenderTemplate(qt QueryTemplate, selectedValues map[string][]string) ([]Tar if err != nil { return nil, err } - u := query.GenericDataQuery{} + u := data.DataQuery{} err = u.UnmarshalJSON(raw) if err != nil { return nil, err diff --git a/pkg/apis/query/v0alpha1/template/render_test.go b/pkg/apis/query/v0alpha1/template/render_test.go index ecec4596c8..465056247c 100644 --- a/pkg/apis/query/v0alpha1/template/render_test.go +++ b/pkg/apis/query/v0alpha1/template/render_test.go @@ -4,9 +4,8 @@ import ( "testing" "github.com/grafana/grafana-plugin-sdk-go/data" + apidata "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1" "github.com/stretchr/testify/require" - - query "github.com/grafana/grafana/pkg/apis/query/v0alpha1" ) var nestedFieldRender = QueryTemplate{ @@ -32,7 +31,7 @@ var nestedFieldRender = QueryTemplate{ }, }, }, - Properties: query.NewGenericDataQuery(map[string]any{ + Properties: apidata.NewDataQuery(map[string]any{ "nestedObject": map[string]any{ "anArray": []any{"foo", .2}, }, @@ -56,7 +55,7 @@ var nestedFieldRenderedTargets = []Target{ }, }, //DataTypeVersion: data.FrameTypeVersion{0, 0}, - Properties: query.NewGenericDataQuery( + Properties: apidata.NewDataQuery( map[string]any{ "nestedObject": map[string]any{ "anArray": []any{"up", .2}, @@ -117,7 +116,7 @@ var multiVarTemplate = QueryTemplate{ }, }, - Properties: query.NewGenericDataQuery(map[string]any{ + Properties: apidata.NewDataQuery(map[string]any{ "expr": "1 + metricName + 1 + anotherMetric + metricName", }), }, @@ -155,7 +154,7 @@ var multiVarRenderedTargets = []Target{ }, }, //DataTypeVersion: data.FrameTypeVersion{0, 0}, - Properties: query.NewGenericDataQuery(map[string]any{ + Properties: apidata.NewDataQuery(map[string]any{ "expr": "1 + up + 1 + sloths_do_like_a_good_nap + up", }), }, @@ -182,7 +181,7 @@ func TestRenderWithRune(t *testing.T) { }, Targets: []Target{ { - Properties: query.NewGenericDataQuery(map[string]any{ + Properties: apidata.NewDataQuery(map[string]any{ "message": "🐦 name!", }), Variables: map[string][]VariableReplacement{ @@ -207,5 +206,5 @@ func TestRenderWithRune(t *testing.T) { rq, err := RenderTemplate(qt, selectedValues) require.NoError(t, err) - require.Equal(t, "🐦 🦥!", rq[0].Properties.AdditionalProperties()["message"]) + require.Equal(t, "🐦 🦥!", rq[0].Properties.GetString("message")) } diff --git a/pkg/apis/query/v0alpha1/template/types.go b/pkg/apis/query/v0alpha1/template/types.go index 4785176495..07ef894933 100644 --- a/pkg/apis/query/v0alpha1/template/types.go +++ b/pkg/apis/query/v0alpha1/template/types.go @@ -2,9 +2,9 @@ package template import ( "github.com/grafana/grafana-plugin-sdk-go/data" + apidata "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1" common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" - query "github.com/grafana/grafana/pkg/apis/query/v0alpha1" ) type QueryTemplate struct { @@ -36,7 +36,7 @@ type Target struct { Variables map[string][]VariableReplacement `json:"variables"` // Query target - Properties query.GenericDataQuery `json:"properties"` + Properties apidata.DataQuery `json:"properties"` } // TemplateVariable is the definition of a variable that will be interpolated diff --git a/pkg/apis/query/v0alpha1/zz_generated.deepcopy.go b/pkg/apis/query/v0alpha1/zz_generated.deepcopy.go index 927ed373dd..8f36003313 100644 --- a/pkg/apis/query/v0alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/query/v0alpha1/zz_generated.deepcopy.go @@ -76,41 +76,45 @@ func (in *DataSourceApiServerList) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *DataSourceRef) DeepCopyInto(out *DataSourceRef) { +func (in *QueryDataRequest) DeepCopyInto(out *QueryDataRequest) { *out = *in + out.TypeMeta = in.TypeMeta + in.QueryDataRequest.DeepCopyInto(&out.QueryDataRequest) return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DataSourceRef. -func (in *DataSourceRef) DeepCopy() *DataSourceRef { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new QueryDataRequest. +func (in *QueryDataRequest) DeepCopy() *QueryDataRequest { if in == nil { return nil } - out := new(DataSourceRef) + out := new(QueryDataRequest) in.DeepCopyInto(out) return out } +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *QueryDataRequest) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *GenericQueryRequest) DeepCopyInto(out *GenericQueryRequest) { +func (in *QueryDataResponse) DeepCopyInto(out *QueryDataResponse) { *out = *in out.TypeMeta = in.TypeMeta - if in.Queries != nil { - in, out := &in.Queries, &out.Queries - *out = make([]GenericDataQuery, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } + in.QueryDataResponse.DeepCopyInto(&out.QueryDataResponse) return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GenericQueryRequest. -func (in *GenericQueryRequest) DeepCopy() *GenericQueryRequest { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new QueryDataResponse. +func (in *QueryDataResponse) DeepCopy() *QueryDataResponse { if in == nil { return nil } - out := new(GenericQueryRequest) + out := new(QueryDataResponse) in.DeepCopyInto(out) return out } @@ -124,17 +128,61 @@ func (in *QueryDataResponse) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *TimeRange) DeepCopyInto(out *TimeRange) { +func (in *QueryTypeDefinition) DeepCopyInto(out *QueryTypeDefinition) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new QueryTypeDefinition. +func (in *QueryTypeDefinition) DeepCopy() *QueryTypeDefinition { + if in == nil { + return nil + } + out := new(QueryTypeDefinition) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *QueryTypeDefinition) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *QueryTypeDefinitionList) DeepCopyInto(out *QueryTypeDefinitionList) { *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]QueryTypeDefinition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TimeRange. -func (in *TimeRange) DeepCopy() *TimeRange { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new QueryTypeDefinitionList. +func (in *QueryTypeDefinitionList) DeepCopy() *QueryTypeDefinitionList { if in == nil { return nil } - out := new(TimeRange) + out := new(QueryTypeDefinitionList) in.DeepCopyInto(out) return out } + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *QueryTypeDefinitionList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} diff --git a/pkg/apis/query/v0alpha1/zz_generated.openapi.go b/pkg/apis/query/v0alpha1/zz_generated.openapi.go index 755fb534ce..3c8db96187 100644 --- a/pkg/apis/query/v0alpha1/zz_generated.openapi.go +++ b/pkg/apis/query/v0alpha1/zz_generated.openapi.go @@ -18,11 +18,10 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA return map[string]common.OpenAPIDefinition{ "github.com/grafana/grafana/pkg/apis/query/v0alpha1.DataSourceApiServer": schema_pkg_apis_query_v0alpha1_DataSourceApiServer(ref), "github.com/grafana/grafana/pkg/apis/query/v0alpha1.DataSourceApiServerList": schema_pkg_apis_query_v0alpha1_DataSourceApiServerList(ref), - "github.com/grafana/grafana/pkg/apis/query/v0alpha1.DataSourceRef": schema_pkg_apis_query_v0alpha1_DataSourceRef(ref), - "github.com/grafana/grafana/pkg/apis/query/v0alpha1.GenericDataQuery": schema_pkg_apis_query_v0alpha1_GenericDataQuery(ref), - "github.com/grafana/grafana/pkg/apis/query/v0alpha1.GenericQueryRequest": schema_pkg_apis_query_v0alpha1_GenericQueryRequest(ref), - "github.com/grafana/grafana/pkg/apis/query/v0alpha1.QueryDataResponse": QueryDataResponse{}.OpenAPIDefinition(), - "github.com/grafana/grafana/pkg/apis/query/v0alpha1.TimeRange": schema_pkg_apis_query_v0alpha1_TimeRange(ref), + "github.com/grafana/grafana/pkg/apis/query/v0alpha1.QueryDataRequest": schema_pkg_apis_query_v0alpha1_QueryDataRequest(ref), + "github.com/grafana/grafana/pkg/apis/query/v0alpha1.QueryDataResponse": schema_pkg_apis_query_v0alpha1_QueryDataResponse(ref), + "github.com/grafana/grafana/pkg/apis/query/v0alpha1.QueryTypeDefinition": schema_pkg_apis_query_v0alpha1_QueryTypeDefinition(ref), + "github.com/grafana/grafana/pkg/apis/query/v0alpha1.QueryTypeDefinitionList": schema_pkg_apis_query_v0alpha1_QueryTypeDefinitionList(ref), "github.com/grafana/grafana/pkg/apis/query/v0alpha1/template.Position": schema_apis_query_v0alpha1_template_Position(ref), "github.com/grafana/grafana/pkg/apis/query/v0alpha1/template.QueryTemplate": schema_apis_query_v0alpha1_template_QueryTemplate(ref), "github.com/grafana/grafana/pkg/apis/query/v0alpha1/template.Target": schema_apis_query_v0alpha1_template_Target(ref), @@ -154,111 +153,77 @@ func schema_pkg_apis_query_v0alpha1_DataSourceApiServerList(ref common.Reference } } -func schema_pkg_apis_query_v0alpha1_DataSourceRef(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_query_v0alpha1_QueryDataRequest(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, + Description: "Generic query request with shared time across all values Copied from: https://github.com/grafana/grafana/blob/main/pkg/api/dtos/models.go#L62", + Type: []string{"object"}, Properties: map[string]spec.Schema{ - "type": { + "kind": { SchemaProps: spec.SchemaProps{ - Description: "The datasource plugin type", - Default: "", + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", Type: []string{"string"}, Format: "", }, }, - "uid": { + "apiVersion": { SchemaProps: spec.SchemaProps{ - Description: "Datasource UID", - Default: "", + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", Type: []string{"string"}, Format: "", }, }, - }, - Required: []string{"type", "uid"}, - }, - }, - } -} - -func schema_pkg_apis_query_v0alpha1_GenericDataQuery(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Description: "GenericDataQuery is a replacement for `dtos.MetricRequest` that provides more explicit types", - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "refId": { + "from": { SchemaProps: spec.SchemaProps{ - Description: "RefID is the unique identifier of the query, set by the frontend call.", + Description: "From is the start time of the query.", Default: "", Type: []string{"string"}, Format: "", }, }, - "timeRange": { - SchemaProps: spec.SchemaProps{ - Description: "TimeRange represents the query range NOTE: unlike generic /ds/query, we can now send explicit time values in each query", - Ref: ref("github.com/grafana/grafana/pkg/apis/query/v0alpha1.TimeRange"), - }, - }, - "datasource": { - SchemaProps: spec.SchemaProps{ - Description: "The datasource", - Ref: ref("github.com/grafana/grafana/pkg/apis/query/v0alpha1.DataSourceRef"), - }, - }, - "datasourceId": { - SchemaProps: spec.SchemaProps{ - Description: "Deprecated -- use datasource ref instead", - Type: []string{"integer"}, - Format: "int64", - }, - }, - "queryType": { + "to": { SchemaProps: spec.SchemaProps{ - Description: "QueryType is an optional identifier for the type of query. It can be used to distinguish different types of queries.", + Description: "To is the end time of the query.", + Default: "", Type: []string{"string"}, Format: "", }, }, - "maxDataPoints": { - SchemaProps: spec.SchemaProps{ - Description: "MaxDataPoints is the maximum number of data points that should be returned from a time series query.", - Type: []string{"integer"}, - Format: "int64", - }, - }, - "intervalMs": { + "queries": { SchemaProps: spec.SchemaProps{ - Description: "Interval is the suggested duration between time points in a time series query.", - Type: []string{"number"}, - Format: "double", + Description: "Datasource queries", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1.DataQuery"), + }, + }, + }, }, }, - "hide": { + "debug": { SchemaProps: spec.SchemaProps{ - Description: "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)", + Description: "Optionally include debug information in the response", Type: []string{"boolean"}, Format: "", }, }, }, - Required: []string{"refId"}, + Required: []string{"from", "to", "queries"}, }, }, Dependencies: []string{ - "github.com/grafana/grafana/pkg/apis/query/v0alpha1.DataSourceRef", "github.com/grafana/grafana/pkg/apis/query/v0alpha1.TimeRange"}, + "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1.DataQuery"}, } } -func schema_pkg_apis_query_v0alpha1_GenericQueryRequest(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_query_v0alpha1_QueryDataResponse(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ - Description: "Generic query request with shared time across all values Copied from: https://github.com/grafana/grafana/blob/main/pkg/api/dtos/models.go#L62", + Description: "Wraps backend.QueryDataResponse, however it includes TypeMeta and implements runtime.Object", Type: []string{"object"}, Properties: map[string]spec.Schema{ "kind": { @@ -275,76 +240,115 @@ func schema_pkg_apis_query_v0alpha1_GenericQueryRequest(ref common.ReferenceCall Format: "", }, }, - "from": { + "results": { SchemaProps: spec.SchemaProps{ - Description: "From Start time in epoch timestamps in milliseconds or relative using Grafana time units. example: now-1h", + Description: "Responses is a map of RefIDs (Unique Query ID) to *DataResponse.", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana-plugin-sdk-go/backend.DataResponse"), + }, + }, + }, + }, + }, + }, + Required: []string{"results"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana-plugin-sdk-go/backend.DataResponse"}, + } +} + +func schema_pkg_apis_query_v0alpha1_QueryTypeDefinition(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Generic query request with shared time across all values", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", Type: []string{"string"}, Format: "", }, }, - "to": { + "apiVersion": { SchemaProps: spec.SchemaProps{ - Description: "To End time in epoch timestamps in milliseconds or relative using Grafana time units. example: now", + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", Type: []string{"string"}, Format: "", }, }, - "queries": { + "metadata": { SchemaProps: spec.SchemaProps{ - Description: "queries.refId – Specifies an identifier of the query. Is optional and default to “A”. queries.datasourceId – Specifies the data source to be queried. Each query in the request must have an unique datasourceId. queries.maxDataPoints - Species maximum amount of data points that dashboard panel can render. Is optional and default to 100. queries.intervalMs - Specifies the time interval in milliseconds of time series. Is optional and defaults to 1000. required: true example: [ { \"refId\": \"A\", \"intervalMs\": 86400000, \"maxDataPoints\": 1092, \"datasource\":{ \"uid\":\"PD8C576611E62080A\" }, \"rawSql\": \"SELECT 1 as valueOne, 2 as valueTwo\", \"format\": \"table\" } ]", - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/grafana/grafana/pkg/apis/query/v0alpha1.GenericDataQuery"), - }, - }, - }, + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), }, }, - "debug": { + "spec": { SchemaProps: spec.SchemaProps{ - Description: "required: false", - Type: []string{"boolean"}, - Format: "", + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1.QueryTypeDefinitionSpec"), }, }, }, - Required: []string{"queries"}, }, }, Dependencies: []string{ - "github.com/grafana/grafana/pkg/apis/query/v0alpha1.GenericDataQuery"}, + "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1.QueryTypeDefinitionSpec", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, } } -func schema_pkg_apis_query_v0alpha1_TimeRange(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_query_v0alpha1_QueryTypeDefinitionList(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ - Description: "TimeRange represents a time range for a query and is a property of DataQuery.", - Type: []string{"object"}, + Type: []string{"object"}, Properties: map[string]spec.Schema{ - "from": { + "kind": { SchemaProps: spec.SchemaProps{ - Description: "From is the start time of the query.", - Default: "", + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", Type: []string{"string"}, Format: "", }, }, - "to": { + "apiVersion": { SchemaProps: spec.SchemaProps{ - Description: "To is the end time of the query.", - Default: "", + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", Type: []string{"string"}, Format: "", }, }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), + }, + }, + "items": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/pkg/apis/query/v0alpha1.QueryTypeDefinition"), + }, + }, + }, + }, + }, }, - Required: []string{"from", "to"}, }, }, + Dependencies: []string{ + "github.com/grafana/grafana/pkg/apis/query/v0alpha1.QueryTypeDefinition", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, } } @@ -486,7 +490,7 @@ func schema_apis_query_v0alpha1_template_Target(ref common.ReferenceCallback) co "properties": { SchemaProps: spec.SchemaProps{ Description: "Query target", - Ref: ref("github.com/grafana/grafana/pkg/apis/query/v0alpha1.GenericDataQuery"), + Ref: ref("github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1.DataQuery"), }, }, }, @@ -494,7 +498,7 @@ func schema_apis_query_v0alpha1_template_Target(ref common.ReferenceCallback) co }, }, Dependencies: []string{ - "github.com/grafana/grafana/pkg/apis/query/v0alpha1.GenericDataQuery", "github.com/grafana/grafana/pkg/apis/query/v0alpha1/template.VariableReplacement"}, + "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1.DataQuery", "github.com/grafana/grafana/pkg/apis/query/v0alpha1/template.VariableReplacement"}, } } diff --git a/pkg/apis/query/v0alpha1/zz_generated.openapi_violation_exceptions.list b/pkg/apis/query/v0alpha1/zz_generated.openapi_violation_exceptions.list index dfff7a33ba..ccded35944 100644 --- a/pkg/apis/query/v0alpha1/zz_generated.openapi_violation_exceptions.list +++ b/pkg/apis/query/v0alpha1/zz_generated.openapi_violation_exceptions.list @@ -1,8 +1,4 @@ API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/query/v0alpha1,DataSourceApiServer,AliasIDs -API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/query/v0alpha1,GenericQueryRequest,Queries -API rule violation: names_match,github.com/grafana/grafana/pkg/apis/query/v0alpha1,GenericDataQuery,IntervalMS -API rule violation: names_match,github.com/grafana/grafana/pkg/apis/query/v0alpha1,GenericDataQuery,RefID -API rule violation: names_match,github.com/grafana/grafana/pkg/apis/query/v0alpha1,QueryDataResponse,QueryDataResponse API rule violation: names_match,github.com/grafana/grafana/pkg/apis/query/v0alpha1/template,QueryTemplate,Variables API rule violation: names_match,github.com/grafana/grafana/pkg/apis/query/v0alpha1/template,replacement,Position API rule violation: names_match,github.com/grafana/grafana/pkg/apis/query/v0alpha1/template,replacement,TemplateVariable diff --git a/pkg/apiserver/builder/openapi.go b/pkg/apiserver/builder/openapi.go index 2cc16d9aae..678b0d5207 100644 --- a/pkg/apiserver/builder/openapi.go +++ b/pkg/apiserver/builder/openapi.go @@ -4,6 +4,7 @@ import ( "maps" "strings" + data "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1" common "k8s.io/kube-openapi/pkg/common" "k8s.io/kube-openapi/pkg/spec3" spec "k8s.io/kube-openapi/pkg/validation/spec" @@ -15,6 +16,7 @@ import ( func GetOpenAPIDefinitions(builders []APIGroupBuilder) common.GetOpenAPIDefinitions { return func(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { defs := v0alpha1.GetOpenAPIDefinitions(ref) // common grafana apis + maps.Copy(defs, data.GetOpenAPIDefinitions(ref)) for _, b := range builders { g := b.GetOpenAPIDefinitions() if g != nil { diff --git a/pkg/expr/nodes.go b/pkg/expr/nodes.go index f5defd3b61..14ac0fbe47 100644 --- a/pkg/expr/nodes.go +++ b/pkg/expr/nodes.go @@ -10,6 +10,7 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/data/utils/jsoniter" + data "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" "gonum.org/v1/gonum/graph/simple" @@ -133,7 +134,10 @@ func buildCMDNode(rn *rawNode, toggles featuremgmt.FeatureToggles) (*CMDNode, er if err != nil { return nil, err } - q, err := reader.ReadQuery(rn, iter) + q, err := reader.ReadQuery(data.NewDataQuery(map[string]any{ + "refId": rn.RefID, + "type": rn.QueryType, + }), iter) if err != nil { return nil, err } diff --git a/pkg/expr/reader.go b/pkg/expr/reader.go index 9227b19c13..71a75c6cb8 100644 --- a/pkg/expr/reader.go +++ b/pkg/expr/reader.go @@ -5,10 +5,12 @@ import ( "strings" "github.com/grafana/grafana-plugin-sdk-go/data/utils/jsoniter" + data "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1" "github.com/grafana/grafana/pkg/expr/classic" "github.com/grafana/grafana/pkg/expr/mathexp" "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/tsdb/legacydata" ) // Once we are comfortable with the parsing logic, this struct will @@ -16,7 +18,7 @@ import ( type ExpressionQuery struct { GraphID int64 `json:"id,omitempty"` RefID string `json:"refId"` - QueryType QueryType `json:"queryType"` + QueryType QueryType `json:"type"` // The typed query parameters Properties any `json:"properties"` @@ -43,16 +45,16 @@ func NewExpressionQueryReader(features featuremgmt.FeatureToggles) *ExpressionQu // nolint:gocyclo func (h *ExpressionQueryReader) ReadQuery( // Properties that have been parsed off the same node - common *rawNode, + common data.DataQuery, // An iterator with context for the full node (include common values) iter *jsoniter.Iterator, ) (eq ExpressionQuery, err error) { referenceVar := "" eq.RefID = common.RefID - if common.QueryType == "" { - return eq, fmt.Errorf("missing queryType") + eq.QueryType = QueryType(common.GetString("type")) + if eq.QueryType == "" { + return eq, fmt.Errorf("missing type") } - eq.QueryType = QueryType(common.QueryType) switch eq.QueryType { case QueryTypeMath: q := &MathQuery{} @@ -99,13 +101,17 @@ func (h *ExpressionQueryReader) ReadQuery( referenceVar, err = getReferenceVar(q.Expression, common.RefID) } if err == nil { + tr := legacydata.NewDataTimeRange(common.TimeRange.From, common.TimeRange.To) eq.Properties = q eq.Command, err = NewResampleCommand(common.RefID, q.Window, referenceVar, q.Downsampler, q.Upsampler, - common.TimeRange, + AbsoluteTimeRange{ + From: tr.GetFromAsTimeUTC(), + To: tr.GetToAsTimeUTC(), + }, ) } diff --git a/pkg/registry/apis/datasource/register.go b/pkg/registry/apis/datasource/register.go index 7bd9e966f5..066e552dd6 100644 --- a/pkg/registry/apis/datasource/register.go +++ b/pkg/registry/apis/datasource/register.go @@ -3,8 +3,12 @@ package datasource import ( "context" "fmt" + "net/http" "time" + "github.com/grafana/grafana-plugin-sdk-go/backend" + data "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1" + "github.com/grafana/grafana-plugin-sdk-go/experimental/schemabuilder" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" @@ -15,10 +19,9 @@ import ( genericapiserver "k8s.io/apiserver/pkg/server" openapi "k8s.io/kube-openapi/pkg/common" "k8s.io/kube-openapi/pkg/spec3" + "k8s.io/kube-openapi/pkg/validation/spec" "k8s.io/utils/strings/slices" - "github.com/grafana/grafana-plugin-sdk-go/backend" - common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" datasource "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1" query "github.com/grafana/grafana/pkg/apis/query/v0alpha1" @@ -30,6 +33,9 @@ import ( "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" ) +const QueryRequestSchemaKey = "QueryRequestSchema" +const QueryPayloadSchemaKey = "QueryPayloadSchema" + var _ builder.APIGroupBuilder = (*DataSourceAPIBuilder)(nil) // DataSourceAPIBuilder is used just so wire has something unique to return @@ -41,6 +47,7 @@ type DataSourceAPIBuilder struct { datasources PluginDatasourceProvider contextProvider PluginContextWrapper accessControl accesscontrol.AccessControl + queryTypes *query.QueryTypeDefinitionList } func RegisterAPIService( @@ -62,6 +69,7 @@ func RegisterAPIService( all := pluginStore.Plugins(context.Background(), plugins.TypeDataSource) ids := []string{ "grafana-testdata-datasource", + // "prometheus", } for _, ds := range all { @@ -123,6 +131,7 @@ func addKnownTypes(scheme *runtime.Scheme, gv schema.GroupVersion) { &datasource.HealthCheckResult{}, &unstructured.Unstructured{}, // Query handler + &query.QueryDataRequest{}, &query.QueryDataResponse{}, &metav1.Status{}, ) @@ -238,15 +247,108 @@ func (b *DataSourceAPIBuilder) PostProcessOpenAPI(oas *spec3.OpenAPI) (*spec3.Op // Hide the ability to list all connections across tenants delete(oas.Paths.Paths, root+b.connectionResourceInfo.GroupResource().Resource) + var err error + opts := schemabuilder.QuerySchemaOptions{ + PluginID: []string{b.pluginJSON.ID}, + QueryTypes: []data.QueryTypeDefinition{}, + Mode: schemabuilder.SchemaTypeQueryPayload, + } + if b.pluginJSON.AliasIDs != nil { + opts.PluginID = append(opts.PluginID, b.pluginJSON.AliasIDs...) + } + if b.queryTypes != nil { + for _, qt := range b.queryTypes.Items { + // The SDK type and api type are not the same so we recreate it here + opts.QueryTypes = append(opts.QueryTypes, data.QueryTypeDefinition{ + ObjectMeta: data.ObjectMeta{ + Name: qt.Name, + }, + Spec: qt.Spec, + }) + } + } + oas.Components.Schemas[QueryPayloadSchemaKey], err = schemabuilder.GetQuerySchema(opts) + if err != nil { + return oas, err + } + opts.Mode = schemabuilder.SchemaTypeQueryRequest + oas.Components.Schemas[QueryRequestSchemaKey], err = schemabuilder.GetQuerySchema(opts) + if err != nil { + return oas, err + } + + // Update the request object + sub := oas.Paths.Paths[root+"namespaces/{namespace}/connections/{name}/query"] + if sub != nil && sub.Post != nil { + sub.Post.Description = "Execute queries" + sub.Post.RequestBody = &spec3.RequestBody{ + RequestBodyProps: spec3.RequestBodyProps{ + Required: true, + Content: map[string]*spec3.MediaType{ + "application/json": { + MediaTypeProps: spec3.MediaTypeProps{ + Schema: spec.RefSchema("#/components/schemas/" + QueryRequestSchemaKey), + Examples: getExamples(b.queryTypes), + }, + }, + }, + }, + } + okrsp, ok := sub.Post.Responses.StatusCodeResponses[200] + if ok { + sub.Post.Responses.StatusCodeResponses[http.StatusMultiStatus] = &spec3.Response{ + ResponseProps: spec3.ResponseProps{ + Description: "Query executed, but errors may exist in the datasource. See the payload for more details.", + Content: okrsp.Content, + }, + } + } + } + // The root API discovery list - sub := oas.Paths.Paths[root] + sub = oas.Paths.Paths[root] if sub != nil && sub.Get != nil { sub.Get.Tags = []string{"API Discovery"} // sorts first in the list } - return oas, nil + return oas, err } // Register additional routes with the server func (b *DataSourceAPIBuilder) GetAPIRoutes() *builder.APIRoutes { return nil } + +func getExamples(queryTypes *query.QueryTypeDefinitionList) map[string]*spec3.Example { + if queryTypes == nil { + return nil + } + + tr := data.TimeRange{From: "now-1h", To: "now"} + examples := map[string]*spec3.Example{} + for _, queryType := range queryTypes.Items { + for idx, example := range queryType.Spec.Examples { + q := data.NewDataQuery(example.SaveModel.Object) + q.RefID = "A" + for _, dis := range queryType.Spec.Discriminators { + _ = q.Set(dis.Field, dis.Value) + } + if q.MaxDataPoints < 1 { + q.MaxDataPoints = 1000 + } + if q.IntervalMS < 1 { + q.IntervalMS = 5000 // 5s + } + examples[fmt.Sprintf("%s-%d", example.Name, idx)] = &spec3.Example{ + ExampleProps: spec3.ExampleProps{ + Summary: example.Name, + Description: example.Description, + Value: data.QueryDataRequest{ + TimeRange: tr, + Queries: []data.DataQuery{q}, + }, + }, + } + } + } + return examples +} diff --git a/pkg/registry/apis/datasource/sub_query.go b/pkg/registry/apis/datasource/sub_query.go index 7b9e48599f..a68f7e3de4 100644 --- a/pkg/registry/apis/datasource/sub_query.go +++ b/pkg/registry/apis/datasource/sub_query.go @@ -2,16 +2,15 @@ package datasource import ( "context" - "encoding/json" "fmt" "net/http" "github.com/grafana/grafana-plugin-sdk-go/backend" + data "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apiserver/pkg/registry/rest" query "github.com/grafana/grafana/pkg/apis/query/v0alpha1" - "github.com/grafana/grafana/pkg/middleware/requestmeta" "github.com/grafana/grafana/pkg/tsdb/legacydata" "github.com/grafana/grafana/pkg/web" ) @@ -20,28 +19,33 @@ type subQueryREST struct { builder *DataSourceAPIBuilder } -var _ = rest.Connecter(&subQueryREST{}) +var ( + _ rest.Storage = (*subQueryREST)(nil) + _ rest.Connecter = (*subQueryREST)(nil) + _ rest.StorageMetadata = (*subQueryREST)(nil) +) func (r *subQueryREST) New() runtime.Object { + // This is added as the "ResponseType" regarless what ProducesObject() says :) return &query.QueryDataResponse{} } func (r *subQueryREST) Destroy() {} +func (r *subQueryREST) ProducesMIMETypes(verb string) []string { + return []string{"application/json"} // and parquet! +} + +func (r *subQueryREST) ProducesObject(verb string) interface{} { + return &query.QueryDataResponse{} +} + func (r *subQueryREST) ConnectMethods() []string { return []string{"POST"} } func (r *subQueryREST) NewConnectOptions() (runtime.Object, bool, string) { - return nil, false, "" -} - -func (r *subQueryREST) readQueries(req *http.Request) ([]backend.DataQuery, *query.DataSourceRef, error) { - reqDTO := query.GenericQueryRequest{} - if err := web.Bind(req, &reqDTO); err != nil { - return nil, nil, err - } - return legacydata.ToDataSourceQueries(reqDTO) + return nil, false, "" // true means you can use the trailing path as a variable } func (r *subQueryREST) Connect(ctx context.Context, name string, opts runtime.Object, responder rest.Responder) (http.Handler, error) { @@ -49,59 +53,35 @@ func (r *subQueryREST) Connect(ctx context.Context, name string, opts runtime.Ob if err != nil { return nil, err } - ctx = backend.WithGrafanaConfig(ctx, pluginCtx.GrafanaConfig) return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - queries, dsRef, err := r.readQueries(req) + dqr := data.QueryDataRequest{} + err := web.Bind(req, &dqr) if err != nil { responder.Error(err) return } - if dsRef != nil && dsRef.UID != name { - responder.Error(fmt.Errorf("expected the datasource in the request url and body to match")) - return - } - qdr, err := r.builder.client.QueryData(ctx, &backend.QueryDataRequest{ - PluginContext: pluginCtx, - Queries: queries, - }) + queries, dsRef, err := legacydata.ToDataSourceQueries(dqr) if err != nil { responder.Error(err) return } - - statusCode := http.StatusOK - for _, res := range qdr.Responses { - if res.Error != nil { - statusCode = http.StatusMultiStatus - } - } - if statusCode != http.StatusOK { - requestmeta.WithDownstreamStatusSource(ctx) + if dsRef != nil && dsRef.UID != name { + responder.Error(fmt.Errorf("expected query body datasource and request to match")) } - // TODO... someday :) can return protobuf for machine-machine communication - // will avoid some hops the current response workflow (for external plugins) - // 1. Plugin: - // creates: golang structs - // returns: arrow + protobuf | - // 2. Client: | direct when local/non grpc - // reads: protobuf+arrow V - // returns: golang structs - // 3. Datasource Server (eg right here): - // reads: golang structs - // returns: JSON - // 4. Query service (alerting etc): - // reads: JSON? (TODO! raw output from 1???) - // returns: JSON (after more operations) - // 5. Browser - // reads: JSON - w.WriteHeader(statusCode) - w.Header().Set("Content-Type", "application/json") - err = json.NewEncoder(w).Encode(qdr) + ctx = backend.WithGrafanaConfig(ctx, pluginCtx.GrafanaConfig) + rsp, err := r.builder.client.QueryData(ctx, &backend.QueryDataRequest{ + Queries: queries, + PluginContext: pluginCtx, + }) if err != nil { responder.Error(err) + return } + responder.Object(query.GetResponseCode(rsp), + &query.QueryDataResponse{QueryDataResponse: *rsp}, + ) }), nil } diff --git a/pkg/registry/apis/peakq/render_examples.go b/pkg/registry/apis/peakq/render_examples.go index dbf7df88a5..0d78ec007e 100644 --- a/pkg/registry/apis/peakq/render_examples.go +++ b/pkg/registry/apis/peakq/render_examples.go @@ -2,8 +2,8 @@ package peakq import ( "github.com/grafana/grafana-plugin-sdk-go/data" + apidata "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1" - query "github.com/grafana/grafana/pkg/apis/query/v0alpha1" "github.com/grafana/grafana/pkg/apis/query/v0alpha1/template" ) @@ -38,7 +38,7 @@ var basicTemplateSpec = template.QueryTemplate{ }, }, - Properties: query.NewGenericDataQuery(map[string]any{ + Properties: apidata.NewDataQuery(map[string]any{ "refId": "A", // TODO: Set when Where? "datasource": map[string]any{ "type": "prometheus", @@ -58,7 +58,7 @@ var basicTemplateRenderedTargets = []template.Target{ { DataType: data.FrameTypeUnknown, //DataTypeVersion: data.FrameTypeVersion{0, 0}, - Properties: query.NewGenericDataQuery(map[string]any{ + Properties: apidata.NewDataQuery(map[string]any{ "refId": "A", // TODO: Set when Where? "datasource": map[string]any{ "type": "prometheus", diff --git a/pkg/registry/apis/peakq/render_examples_test.go b/pkg/registry/apis/peakq/render_examples_test.go index d9739c92ab..1b4b2d53a8 100644 --- a/pkg/registry/apis/peakq/render_examples_test.go +++ b/pkg/registry/apis/peakq/render_examples_test.go @@ -14,8 +14,8 @@ func TestRender(t *testing.T) { rT, err := template.RenderTemplate(basicTemplateSpec, map[string][]string{"metricName": {"up"}}) require.NoError(t, err) require.Equal(t, - basicTemplateRenderedTargets[0].Properties.AdditionalProperties()["expr"], - rT[0].Properties.AdditionalProperties()["expr"]) + basicTemplateRenderedTargets[0].Properties.GetString("expr"), + rT[0].Properties.GetString("expr")) b, _ := json.MarshalIndent(basicTemplateSpec, "", " ") fmt.Println(string(b)) } diff --git a/pkg/registry/apis/query/client.go b/pkg/registry/apis/query/client.go new file mode 100644 index 0000000000..50f7a16434 --- /dev/null +++ b/pkg/registry/apis/query/client.go @@ -0,0 +1,22 @@ +package query + +import ( + "context" + + data "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1" +) + +// The query runner interface +type DataSourceClientSupplier interface { + // Get a client for a given datasource + // NOTE: authorization headers are not yet added and the client may be shared across multiple users + GetDataSourceClient(ctx context.Context, ref data.DataSourceRef) (data.QueryDataClient, error) +} + +type CommonDataSourceClientSupplier struct { + Client data.QueryDataClient +} + +func (s *CommonDataSourceClientSupplier) GetDataSourceClient(ctx context.Context, ref data.DataSourceRef) (data.QueryDataClient, error) { + return s.Client, nil +} diff --git a/pkg/registry/apis/query/runner/direct.go b/pkg/registry/apis/query/client/plugin.go similarity index 62% rename from pkg/registry/apis/query/runner/direct.go rename to pkg/registry/apis/query/client/plugin.go index e36a7e6188..a5354e35ec 100644 --- a/pkg/registry/apis/query/runner/direct.go +++ b/pkg/registry/apis/query/client/plugin.go @@ -1,17 +1,18 @@ -package runner +package client import ( "context" "fmt" + "net/http" "sync" "time" "github.com/grafana/grafana-plugin-sdk-go/backend" - + data "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" - "github.com/grafana/grafana/pkg/apis/query/v0alpha1" + query "github.com/grafana/grafana/pkg/apis/query/v0alpha1" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext" @@ -20,14 +21,14 @@ import ( "github.com/grafana/grafana/pkg/tsdb/legacydata" ) -type directRunner struct { +type pluginClient struct { pluginClient plugins.Client pCtxProvider *plugincontext.Provider } -type directRegistry struct { +type pluginRegistry struct { pluginsMu sync.Mutex - plugins *v0alpha1.DataSourceApiServerList + plugins *query.DataSourceApiServerList apis map[string]schema.GroupVersion groupToPlugin map[string]string pluginStore pluginstore.Store @@ -36,68 +37,67 @@ type directRegistry struct { dataSourcesService datasources.DataSourceService } -var _ v0alpha1.QueryRunner = (*directRunner)(nil) -var _ v0alpha1.DataSourceApiServerRegistry = (*directRegistry)(nil) +var _ data.QueryDataClient = (*pluginClient)(nil) +var _ query.DataSourceApiServerRegistry = (*pluginRegistry)(nil) // NewDummyTestRunner creates a runner that only works with testdata -func NewDirectQueryRunner( - pluginClient plugins.Client, - pCtxProvider *plugincontext.Provider) v0alpha1.QueryRunner { - return &directRunner{ - pluginClient: pluginClient, - pCtxProvider: pCtxProvider, +func NewQueryClientForPluginClient(p plugins.Client, ctx *plugincontext.Provider) data.QueryDataClient { + return &pluginClient{ + pluginClient: p, + pCtxProvider: ctx, } } -func NewDirectRegistry(pluginStore pluginstore.Store, +func NewDataSourceRegistryFromStore(pluginStore pluginstore.Store, dataSourcesService datasources.DataSourceService, -) v0alpha1.DataSourceApiServerRegistry { - return &directRegistry{ +) query.DataSourceApiServerRegistry { + return &pluginRegistry{ pluginStore: pluginStore, dataSourcesService: dataSourcesService, } } // ExecuteQueryData implements QueryHelper. -func (d *directRunner) ExecuteQueryData(ctx context.Context, - // The k8s group for the datasource (pluginId) - datasource schema.GroupVersion, - - // The datasource name/uid - name string, - - // The raw backend query objects - query []v0alpha1.GenericDataQuery, -) (*backend.QueryDataResponse, error) { - queries, dsRef, err := legacydata.ToDataSourceQueries(v0alpha1.GenericQueryRequest{ - Queries: query, - }) +func (d *pluginClient) QueryData(ctx context.Context, req data.QueryDataRequest) (int, *backend.QueryDataResponse, error) { + queries, dsRef, err := legacydata.ToDataSourceQueries(req) if err != nil { - return nil, err + return http.StatusBadRequest, nil, err } - if dsRef != nil && dsRef.UID != name { - return nil, fmt.Errorf("expected query body datasource and request to match") + if dsRef == nil { + return http.StatusBadRequest, nil, fmt.Errorf("expected single datasource request") } // NOTE: this depends on uid unique across datasources - settings, err := d.pCtxProvider.GetDataSourceInstanceSettings(ctx, name) + settings, err := d.pCtxProvider.GetDataSourceInstanceSettings(ctx, dsRef.UID) if err != nil { - return nil, err + return http.StatusBadRequest, nil, err } - pCtx, err := d.pCtxProvider.PluginContextForDataSource(ctx, settings) + qdr := &backend.QueryDataRequest{ + Queries: queries, + } + qdr.PluginContext, err = d.pCtxProvider.PluginContextForDataSource(ctx, settings) if err != nil { - return nil, err + return http.StatusBadRequest, nil, err } - return d.pluginClient.QueryData(ctx, &backend.QueryDataRequest{ - PluginContext: pCtx, - Queries: queries, - }) + code := http.StatusOK + rsp, err := d.pluginClient.QueryData(ctx, qdr) + if err == nil { + for _, v := range rsp.Responses { + if v.Error != nil { + code = http.StatusMultiStatus + break + } + } + } else { + code = http.StatusInternalServerError + } + return code, rsp, err } // GetDatasourceAPI implements DataSourceRegistry. -func (d *directRegistry) GetDatasourceGroupVersion(pluginId string) (schema.GroupVersion, error) { +func (d *pluginRegistry) GetDatasourceGroupVersion(pluginId string) (schema.GroupVersion, error) { d.pluginsMu.Lock() defer d.pluginsMu.Unlock() @@ -117,7 +117,7 @@ func (d *directRegistry) GetDatasourceGroupVersion(pluginId string) (schema.Grou } // GetDatasourcePlugins no namespace? everything that is available -func (d *directRegistry) GetDatasourceApiServers(ctx context.Context) (*v0alpha1.DataSourceApiServerList, error) { +func (d *pluginRegistry) GetDatasourceApiServers(ctx context.Context) (*query.DataSourceApiServerList, error) { d.pluginsMu.Lock() defer d.pluginsMu.Unlock() @@ -132,10 +132,10 @@ func (d *directRegistry) GetDatasourceApiServers(ctx context.Context) (*v0alpha1 } // This should be called when plugins change -func (d *directRegistry) updatePlugins() error { +func (d *pluginRegistry) updatePlugins() error { groupToPlugin := map[string]string{} apis := map[string]schema.GroupVersion{} - result := &v0alpha1.DataSourceApiServerList{ + result := &query.DataSourceApiServerList{ ListMeta: metav1.ListMeta{ ResourceVersion: fmt.Sprintf("%d", time.Now().UnixMilli()), }, @@ -159,7 +159,7 @@ func (d *directRegistry) updatePlugins() error { } groupToPlugin[group] = dsp.ID - ds := v0alpha1.DataSourceApiServer{ + ds := query.DataSourceApiServer{ ObjectMeta: metav1.ObjectMeta{ Name: dsp.ID, CreationTimestamp: metav1.NewTime(time.UnixMilli(ts)), diff --git a/pkg/registry/apis/query/runner/dummy.go b/pkg/registry/apis/query/client/testdata.go similarity index 54% rename from pkg/registry/apis/query/runner/dummy.go rename to pkg/registry/apis/query/client/testdata.go index 4937cb8124..b64169e4e0 100644 --- a/pkg/registry/apis/query/runner/dummy.go +++ b/pkg/registry/apis/query/client/testdata.go @@ -1,59 +1,46 @@ -package runner +package client import ( "context" "fmt" + "net/http" "time" "github.com/grafana/grafana-plugin-sdk-go/backend" - + data "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" - "github.com/grafana/grafana/pkg/apis/query/v0alpha1" + query "github.com/grafana/grafana/pkg/apis/query/v0alpha1" testdata "github.com/grafana/grafana/pkg/tsdb/grafana-testdata-datasource" "github.com/grafana/grafana/pkg/tsdb/legacydata" ) type testdataDummy struct{} -var _ v0alpha1.QueryRunner = (*testdataDummy)(nil) -var _ v0alpha1.DataSourceApiServerRegistry = (*testdataDummy)(nil) +var _ data.QueryDataClient = (*testdataDummy)(nil) +var _ query.DataSourceApiServerRegistry = (*testdataDummy)(nil) -// NewDummyTestRunner creates a runner that only works with testdata -func NewDummyTestRunner() v0alpha1.QueryRunner { +// NewTestDataClient creates a runner that only works with testdata +func NewTestDataClient() data.QueryDataClient { return &testdataDummy{} } -func NewDummyRegistry() v0alpha1.DataSourceApiServerRegistry { +// NewTestDataRegistry returns a registry that only knows about testdata +func NewTestDataRegistry() query.DataSourceApiServerRegistry { return &testdataDummy{} } // ExecuteQueryData implements QueryHelper. -func (d *testdataDummy) ExecuteQueryData(ctx context.Context, - // The k8s group for the datasource (pluginId) - datasource schema.GroupVersion, - - // The datasource name/uid - name string, - - // The raw backend query objects - query []v0alpha1.GenericDataQuery, -) (*backend.QueryDataResponse, error) { - if datasource.Group != "testdata.datasource.grafana.app" { - return nil, fmt.Errorf("expecting testdata requests") - } - - queries, _, err := legacydata.ToDataSourceQueries(v0alpha1.GenericQueryRequest{ - Queries: query, - }) +func (d *testdataDummy) QueryData(ctx context.Context, req data.QueryDataRequest) (int, *backend.QueryDataResponse, error) { + queries, _, err := legacydata.ToDataSourceQueries(req) if err != nil { - return nil, err + return http.StatusBadRequest, nil, err } - return testdata.ProvideService().QueryData(ctx, &backend.QueryDataRequest{ - Queries: queries, - }) + qdr := &backend.QueryDataRequest{Queries: queries} + rsp, err := testdata.ProvideService().QueryData(ctx, qdr) + return query.GetResponseCode(rsp), rsp, err } // GetDatasourceAPI implements DataSourceRegistry. @@ -68,12 +55,12 @@ func (*testdataDummy) GetDatasourceGroupVersion(pluginId string) (schema.GroupVe } // GetDatasourcePlugins implements QueryHelper. -func (d *testdataDummy) GetDatasourceApiServers(ctx context.Context) (*v0alpha1.DataSourceApiServerList, error) { - return &v0alpha1.DataSourceApiServerList{ +func (d *testdataDummy) GetDatasourceApiServers(ctx context.Context) (*query.DataSourceApiServerList, error) { + return &query.DataSourceApiServerList{ ListMeta: metav1.ListMeta{ ResourceVersion: fmt.Sprintf("%d", time.Now().UnixMilli()), }, - Items: []v0alpha1.DataSourceApiServer{ + Items: []query.DataSourceApiServer{ { ObjectMeta: metav1.ObjectMeta{ Name: "grafana-testdata-datasource", diff --git a/pkg/registry/apis/query/metrics.go b/pkg/registry/apis/query/metrics.go new file mode 100644 index 0000000000..e13525917e --- /dev/null +++ b/pkg/registry/apis/query/metrics.go @@ -0,0 +1,48 @@ +package query + +import ( + "github.com/prometheus/client_golang/prometheus" +) + +const ( + metricsSubSystem = "queryservice" + metricsNamespace = "grafana" +) + +type metrics struct { + dsRequests *prometheus.CounterVec + + // older metric + expressionsQuerySummary *prometheus.SummaryVec +} + +func newMetrics(reg prometheus.Registerer) *metrics { + m := &metrics{ + dsRequests: prometheus.NewCounterVec(prometheus.CounterOpts{ + Namespace: metricsNamespace, + Subsystem: metricsSubSystem, + Name: "ds_queries_total", + Help: "Number of datasource queries made from the query service", + }, []string{"error", "dataplane", "datasource_type"}), + + expressionsQuerySummary: prometheus.NewSummaryVec( + prometheus.SummaryOpts{ + Namespace: metricsNamespace, + Subsystem: metricsSubSystem, + Name: "expressions_queries_duration_milliseconds", + Help: "Expressions query summary", + Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, + }, + []string{"status"}, + ), + } + + if reg != nil { + reg.MustRegister( + m.dsRequests, + m.expressionsQuerySummary, + ) + } + + return m +} diff --git a/pkg/registry/apis/query/parser.go b/pkg/registry/apis/query/parser.go index bc60a5b451..613da01690 100644 --- a/pkg/registry/apis/query/parser.go +++ b/pkg/registry/apis/query/parser.go @@ -1,83 +1,216 @@ package query import ( + "context" + "encoding/json" "fmt" - "github.com/grafana/grafana/pkg/apis/query/v0alpha1" + "github.com/grafana/grafana-plugin-sdk-go/data/utils/jsoniter" + data "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1" + "gonum.org/v1/gonum/graph/simple" + "gonum.org/v1/gonum/graph/topo" + + query "github.com/grafana/grafana/pkg/apis/query/v0alpha1" "github.com/grafana/grafana/pkg/expr" + "github.com/grafana/grafana/pkg/infra/tracing" + "github.com/grafana/grafana/pkg/services/datasources/service" ) -type parsedQueryRequest struct { - // The queries broken into requests - Requests []groupedQueries +type datasourceRequest struct { + // The type + PluginId string `json:"pluginId"` + + // The UID + UID string `json:"uid"` // Optionally show the additional query properties - Expressions []v0alpha1.GenericDataQuery + Request *data.QueryDataRequest `json:"request"` + + // Headers that should be forwarded to the next request + Headers map[string]string `json:"headers,omitempty"` } -type groupedQueries struct { - // the plugin type - pluginId string +type parsedRequestInfo struct { + // Datasource queries, one for each datasource + Requests []datasourceRequest `json:"requests,omitempty"` - // The datasource name/uid - uid string + // Expressions in required execution order + Expressions []expr.ExpressionQuery `json:"expressions,omitempty"` - // The raw backend query objects - query []v0alpha1.GenericDataQuery + // Expressions include explicit hacks for influx+prometheus + RefIDTypes map[string]string `json:"types,omitempty"` + + // Hidden queries used as dependencies + HideBeforeReturn []string `json:"hide,omitempty"` } -// Internally define what makes this request unique (eventually may include the apiVersion) -func (d *groupedQueries) key() string { - return fmt.Sprintf("%s/%s", d.pluginId, d.uid) +type queryParser struct { + legacy service.LegacyDataSourceLookup + reader *expr.ExpressionQueryReader + tracer tracing.Tracer } -func parseQueryRequest(raw v0alpha1.GenericQueryRequest) (parsedQueryRequest, error) { - mixed := make(map[string]*groupedQueries) - parsed := parsedQueryRequest{} - refIds := make(map[string]bool) +func newQueryParser(reader *expr.ExpressionQueryReader, legacy service.LegacyDataSourceLookup, tracer tracing.Tracer) *queryParser { + return &queryParser{ + reader: reader, + legacy: legacy, + tracer: tracer, + } +} + +// Split the main query into multiple +func (p *queryParser) parseRequest(ctx context.Context, input *query.QueryDataRequest) (parsedRequestInfo, error) { + ctx, span := p.tracer.Start(ctx, "QueryService.parseRequest") + defer span.End() + + queryRefIDs := make(map[string]*data.DataQuery, len(input.Queries)) + expressions := make(map[string]*expr.ExpressionQuery) + index := make(map[string]int) // index lookup + rsp := parsedRequestInfo{ + RefIDTypes: make(map[string]string, len(input.Queries)), + } + + // Ensure a valid time range + if input.From == "" { + input.From = "now-6h" + } + if input.To == "" { + input.To = "now" + } - for _, original := range raw.Queries { - if refIds[original.RefID] { - return parsed, fmt.Errorf("invalid query, duplicate refId: " + original.RefID) + for _, q := range input.Queries { + _, found := queryRefIDs[q.RefID] + if found { + return rsp, fmt.Errorf("multiple queries found for refId: %s", q.RefID) + } + _, found = expressions[q.RefID] + if found { + return rsp, fmt.Errorf("multiple queries found for refId: %s", q.RefID) } - refIds[original.RefID] = true - q := original + ds, err := p.getValidDataSourceRef(ctx, q.Datasource, q.DatasourceID) + if err != nil { + return rsp, err + } - if q.TimeRange == nil && raw.From != "" { - q.TimeRange = &v0alpha1.TimeRange{ - From: raw.From, - To: raw.To, + // Process each query + if expr.IsDataSource(ds.UID) { + // In order to process the query as a typed expression query, we + // are writing it back to JSON and parsing again. Alternatively we + // could construct it from the untyped map[string]any additional properties + // but this approach lets us focus on well typed behavior first + raw, err := json.Marshal(q) + if err != nil { + return rsp, err + } + iter, err := jsoniter.ParseBytes(jsoniter.ConfigDefault, raw) + if err != nil { + return rsp, err + } + exp, err := p.reader.ReadQuery(q, iter) + if err != nil { + return rsp, err + } + exp.GraphID = int64(len(expressions) + 1) + expressions[q.RefID] = &exp + } else { + key := fmt.Sprintf("%s/%s", ds.Type, ds.UID) + idx, ok := index[key] + if !ok { + idx = len(index) + index[key] = idx + rsp.Requests = append(rsp.Requests, datasourceRequest{ + PluginId: ds.Type, + UID: ds.UID, + Request: &data.QueryDataRequest{ + TimeRange: input.TimeRange, + Debug: input.Debug, + // no queries + }, + }) } - } - // Extract out the expressions queries earlier - if expr.IsDataSource(q.Datasource.Type) || expr.IsDataSource(q.Datasource.UID) { - parsed.Expressions = append(parsed.Expressions, q) - continue + req := rsp.Requests[idx].Request + req.Queries = append(req.Queries, q) + queryRefIDs[q.RefID] = &req.Queries[len(req.Queries)-1] } - g := &groupedQueries{pluginId: q.Datasource.Type, uid: q.Datasource.UID} - group, ok := mixed[g.key()] - if !ok || group == nil { - group = g - mixed[g.key()] = g + // Mark all the queries that should be hidden () + if q.Hide { + rsp.HideBeforeReturn = append(rsp.HideBeforeReturn, q.RefID) } - group.query = append(group.query, q) } - for _, q := range parsed.Expressions { - // TODO: parse and build tree, for now just fail fast on unknown commands - _, err := expr.GetExpressionCommandType(q.AdditionalProperties()) + // Make sure all referenced variables exist and the expression order is stable + if len(expressions) > 0 { + queryNode := &expr.ExpressionQuery{ + GraphID: -1, + } + + // Build the graph for a request + dg := simple.NewDirectedGraph() + dg.AddNode(queryNode) + for _, exp := range expressions { + dg.AddNode(exp) + } + for _, exp := range expressions { + vars := exp.Command.NeedsVars() + for _, refId := range vars { + target := queryNode + q, ok := queryRefIDs[refId] + if !ok { + target, ok = expressions[refId] + if !ok { + return rsp, fmt.Errorf("expression [%s] is missing variable [%s]", exp.RefID, refId) + } + } + // Do not hide queries used in variables + if q != nil && q.Hide { + q.Hide = false + } + if target.ID() == exp.ID() { + return rsp, fmt.Errorf("expression [%s] can not depend on itself", exp.RefID) + } + dg.SetEdge(dg.NewEdge(target, exp)) + } + } + + // Add the sorted expressions + sortedNodes, err := topo.SortStabilized(dg, nil) if err != nil { - return parsed, err + return rsp, fmt.Errorf("cyclic references in query") + } + for _, v := range sortedNodes { + if v.ID() > 0 { + rsp.Expressions = append(rsp.Expressions, *v.(*expr.ExpressionQuery)) + } } } - // Add each request - for _, v := range mixed { - parsed.Requests = append(parsed.Requests, *v) - } + return rsp, nil +} - return parsed, nil +func (p *queryParser) getValidDataSourceRef(ctx context.Context, ds *data.DataSourceRef, id int64) (*data.DataSourceRef, error) { + if ds == nil { + if id == 0 { + return nil, fmt.Errorf("missing datasource reference or id") + } + if p.legacy == nil { + return nil, fmt.Errorf("legacy datasource lookup unsupported (id:%d)", id) + } + return p.legacy.GetDataSourceFromDeprecatedFields(ctx, "", id) + } + if ds.Type == "" { + if ds.UID == "" { + return nil, fmt.Errorf("missing name/uid in data source reference") + } + if ds.UID == expr.DatasourceType { + return ds, nil + } + if p.legacy == nil { + return nil, fmt.Errorf("legacy datasource lookup unsupported (name:%s)", ds.UID) + } + return p.legacy.GetDataSourceFromDeprecatedFields(ctx, ds.UID, 0) + } + return ds, nil } diff --git a/pkg/registry/apis/query/parser_test.go b/pkg/registry/apis/query/parser_test.go new file mode 100644 index 0000000000..938b8ff1e9 --- /dev/null +++ b/pkg/registry/apis/query/parser_test.go @@ -0,0 +1,131 @@ +package query + +import ( + "context" + "encoding/json" + "fmt" + "os" + "path" + "strings" + "testing" + + data "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + query "github.com/grafana/grafana/pkg/apis/query/v0alpha1" + "github.com/grafana/grafana/pkg/expr" + "github.com/grafana/grafana/pkg/infra/tracing" + "github.com/grafana/grafana/pkg/services/featuremgmt" +) + +type parserTestObject struct { + Description string `json:"description,omitempty"` + Request query.QueryDataRequest `json:"input"` + Expect parsedRequestInfo `json:"expect"` + Error string `json:"error,omitempty"` +} + +func TestQuerySplitting(t *testing.T) { + ctx := context.Background() + parser := newQueryParser(expr.NewExpressionQueryReader(featuremgmt.WithFeatures()), + &legacyDataSourceRetriever{}, tracing.InitializeTracerForTest()) + + t.Run("missing datasource flavors", func(t *testing.T) { + split, err := parser.parseRequest(ctx, &query.QueryDataRequest{ + QueryDataRequest: data.QueryDataRequest{ + Queries: []data.DataQuery{{ + CommonQueryProperties: data.CommonQueryProperties{ + RefID: "A", + }, + }}, + }, + }) + require.Error(t, err) // Missing datasource + require.Empty(t, split.Requests) + }) + + t.Run("applies default time range", func(t *testing.T) { + split, err := parser.parseRequest(ctx, &query.QueryDataRequest{ + QueryDataRequest: data.QueryDataRequest{ + TimeRange: data.TimeRange{}, // missing + Queries: []data.DataQuery{{ + CommonQueryProperties: data.CommonQueryProperties{ + RefID: "A", + Datasource: &data.DataSourceRef{ + Type: "x", + UID: "abc", + }, + }, + }}, + }, + }) + require.NoError(t, err) + require.Len(t, split.Requests, 1) + require.Equal(t, "now-6h", split.Requests[0].Request.From) + require.Equal(t, "now", split.Requests[0].Request.To) + }) + + t.Run("verify tests", func(t *testing.T) { + files, err := os.ReadDir("testdata") + require.NoError(t, err) + + for _, file := range files { + if !strings.HasSuffix(file.Name(), ".json") { + continue + } + + fpath := path.Join("testdata", file.Name()) + // nolint:gosec + body, err := os.ReadFile(fpath) + require.NoError(t, err) + harness := &parserTestObject{} + err = json.Unmarshal(body, harness) + require.NoError(t, err) + + changed := false + parsed, err := parser.parseRequest(ctx, &harness.Request) + if err != nil { + if !assert.Equal(t, harness.Error, err.Error(), "File %s", file) { + changed = true + } + } else { + x, _ := json.Marshal(parsed) + y, _ := json.Marshal(harness.Expect) + if !assert.JSONEq(t, string(y), string(x), "File %s", file) { + changed = true + } + } + + if changed { + harness.Error = "" + harness.Expect = parsed + if err != nil { + harness.Error = err.Error() + } + jj, err := json.MarshalIndent(harness, "", " ") + require.NoError(t, err) + err = os.WriteFile(fpath, jj, 0600) + require.NoError(t, err) + } + } + }) +} + +type legacyDataSourceRetriever struct{} + +func (s *legacyDataSourceRetriever) GetDataSourceFromDeprecatedFields(ctx context.Context, name string, id int64) (*data.DataSourceRef, error) { + if id == 100 { + return &data.DataSourceRef{ + Type: "plugin-aaaa", + UID: "AAA", + }, nil + } + if name != "" { + return &data.DataSourceRef{ + Type: "plugin-bbb", + UID: name, + }, nil + } + return nil, fmt.Errorf("missing parameter") +} diff --git a/pkg/registry/apis/query/query.go b/pkg/registry/apis/query/query.go index e527814137..c3a14dac30 100644 --- a/pkg/registry/apis/query/query.go +++ b/pkg/registry/apis/query/query.go @@ -3,62 +3,71 @@ package query import ( "context" "encoding/json" + "errors" "fmt" "net/http" + "time" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1" + "go.opentelemetry.io/otel/attribute" "golang.org/x/sync/errgroup" - "github.com/grafana/grafana/pkg/apis/query/v0alpha1" + query "github.com/grafana/grafana/pkg/apis/query/v0alpha1" + "github.com/grafana/grafana/pkg/expr/mathexp" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/middleware/requestmeta" + "github.com/grafana/grafana/pkg/services/datasources" + "github.com/grafana/grafana/pkg/util/errutil" "github.com/grafana/grafana/pkg/util/errutil/errhttp" "github.com/grafana/grafana/pkg/web" ) -func (b *QueryAPIBuilder) handleQuery(w http.ResponseWriter, r *http.Request) { - reqDTO := v0alpha1.GenericQueryRequest{} - if err := web.Bind(r, &reqDTO); err != nil { - errhttp.Write(r.Context(), err, w) - return - } +// The query method (not really a create) +func (b *QueryAPIBuilder) doQuery(w http.ResponseWriter, r *http.Request) { + ctx, span := b.tracer.Start(r.Context(), "QueryService.Query") + defer span.End() - parsed, err := parseQueryRequest(reqDTO) + raw := &query.QueryDataRequest{} + err := web.Bind(r, raw) if err != nil { - errhttp.Write(r.Context(), err, w) + errhttp.Write(ctx, errutil.BadRequest( + "query.bind", + errutil.WithPublicMessage("Error reading query")). + Errorf("error reading: %w", err), w) return } - ctx := r.Context() - qdr, err := b.processRequest(ctx, parsed) + // Parses the request and splits it into multiple sub queries (if necessary) + req, err := b.parser.parseRequest(ctx, raw) if err != nil { - errhttp.Write(r.Context(), err, w) - return - } - - statusCode := http.StatusOK - for _, res := range qdr.Responses { - if res.Error != nil { - statusCode = http.StatusBadRequest - if b.returnMultiStatus { - statusCode = http.StatusMultiStatus - } + if errors.Is(err, datasources.ErrDataSourceNotFound) { + errhttp.Write(ctx, errutil.BadRequest( + "query.datasource.notfound", + errutil.WithPublicMessage(err.Error())), w) + return } - } - if statusCode != http.StatusOK { - requestmeta.WithDownstreamStatusSource(ctx) + errhttp.Write(ctx, errutil.BadRequest( + "query.parse", + errutil.WithPublicMessage("Error parsing query")). + Errorf("error parsing: %w", err), w) + return } - w.Header().Set("Content-Type", "application/json") - err = json.NewEncoder(w).Encode(qdr) + // Actually run the query + rsp, err := b.execute(ctx, req) if err != nil { - errhttp.Write(r.Context(), err, w) + errhttp.Write(ctx, errutil.Internal( + "query.execution", + errutil.WithPublicMessage("Error executing query")). + Errorf("execution error: %w", err), w) + return } + + w.WriteHeader(query.GetResponseCode(rsp)) + _ = json.NewEncoder(w).Encode(rsp) } -// See: -// https://github.com/grafana/grafana/blob/v10.2.3/pkg/services/query/query.go#L88 -func (b *QueryAPIBuilder) processRequest(ctx context.Context, req parsedQueryRequest) (qdr *backend.QueryDataResponse, err error) { +func (b *QueryAPIBuilder) execute(ctx context.Context, req parsedRequestInfo) (qdr *backend.QueryDataResponse, err error) { switch len(req.Requests) { case 0: break // nothing to do @@ -69,25 +78,73 @@ func (b *QueryAPIBuilder) processRequest(ctx context.Context, req parsedQueryReq } if len(req.Expressions) > 0 { - return b.handleExpressions(ctx, qdr, req.Expressions) + qdr, err = b.handleExpressions(ctx, req, qdr) + } + + // Remove hidden results + for _, refId := range req.HideBeforeReturn { + r, ok := qdr.Responses[refId] + if ok && r.Error == nil { + delete(qdr.Responses, refId) + } } - return qdr, err + return } // Process a single request // See: https://github.com/grafana/grafana/blob/v10.2.3/pkg/services/query/query.go#L242 -func (b *QueryAPIBuilder) handleQuerySingleDatasource(ctx context.Context, req groupedQueries) (*backend.QueryDataResponse, error) { - gv, err := b.registry.GetDatasourceGroupVersion(req.pluginId) +func (b *QueryAPIBuilder) handleQuerySingleDatasource(ctx context.Context, req datasourceRequest) (*backend.QueryDataResponse, error) { + ctx, span := b.tracer.Start(ctx, "Query.handleQuerySingleDatasource") + defer span.End() + span.SetAttributes( + attribute.String("datasource.type", req.PluginId), + attribute.String("datasource.uid", req.UID), + ) + + allHidden := true + for idx := range req.Request.Queries { + if !req.Request.Queries[idx].Hide { + allHidden = false + break + } + } + if allHidden { + return &backend.QueryDataResponse{}, nil + } + + // headers? + client, err := b.client.GetDataSourceClient(ctx, v0alpha1.DataSourceRef{ + Type: req.PluginId, + UID: req.UID, + }) if err != nil { return nil, err } - return b.runner.ExecuteQueryData(ctx, gv, req.uid, req.query) + + // headers? + _, rsp, err := client.QueryData(ctx, *req.Request) + if err == nil { + for _, q := range req.Request.Queries { + if q.ResultAssertions != nil { + result, ok := rsp.Responses[q.RefID] + if ok && result.Error == nil { + err = q.ResultAssertions.Validate(result.Frames) + if err != nil { + result.Error = err + result.ErrorSource = backend.ErrorSourceDownstream + rsp.Responses[q.RefID] = result + } + } + } + } + } + return rsp, err } // buildErrorResponses applies the provided error to each query response in the list. These queries should all belong to the same datasource. -func buildErrorResponse(err error, req groupedQueries) *backend.QueryDataResponse { +func buildErrorResponse(err error, req datasourceRequest) *backend.QueryDataResponse { rsp := backend.NewQueryDataResponse() - for _, query := range req.query { + for _, query := range req.Request.Queries { rsp.Responses[query.RefID] = backend.DataResponse{ Error: err, } @@ -96,13 +153,16 @@ func buildErrorResponse(err error, req groupedQueries) *backend.QueryDataRespons } // executeConcurrentQueries executes queries to multiple datasources concurrently and returns the aggregate result. -func (b *QueryAPIBuilder) executeConcurrentQueries(ctx context.Context, requests []groupedQueries) (*backend.QueryDataResponse, error) { +func (b *QueryAPIBuilder) executeConcurrentQueries(ctx context.Context, requests []datasourceRequest) (*backend.QueryDataResponse, error) { + ctx, span := b.tracer.Start(ctx, "Query.executeConcurrentQueries") + defer span.End() + g, ctx := errgroup.WithContext(ctx) g.SetLimit(b.concurrentQueryLimit) // prevent too many concurrent requests rchan := make(chan *backend.QueryDataResponse, len(requests)) // Create panic recovery function for loop below - recoveryFn := func(req groupedQueries) { + recoveryFn := func(req datasourceRequest) { if r := recover(); r != nil { var err error b.log.Error("query datasource panic", "error", r, "stack", log.Stack(1)) @@ -150,8 +210,63 @@ func (b *QueryAPIBuilder) executeConcurrentQueries(ctx context.Context, requests return resp, nil } -// NOTE the upstream queries have already been executed -// https://github.com/grafana/grafana/blob/v10.2.3/pkg/services/query/query.go#L242 -func (b *QueryAPIBuilder) handleExpressions(ctx context.Context, qdr *backend.QueryDataResponse, expressions []v0alpha1.GenericDataQuery) (*backend.QueryDataResponse, error) { - return qdr, fmt.Errorf("expressions are not implemented yet") +// Unlike the implementation in expr/node.go, all datasource queries have been processed first +func (b *QueryAPIBuilder) handleExpressions(ctx context.Context, req parsedRequestInfo, data *backend.QueryDataResponse) (qdr *backend.QueryDataResponse, err error) { + start := time.Now() + ctx, span := b.tracer.Start(ctx, "SSE.handleExpressions") + defer func() { + var respStatus string + switch { + case err == nil: + respStatus = "success" + default: + respStatus = "failure" + } + duration := float64(time.Since(start).Nanoseconds()) / float64(time.Millisecond) + b.metrics.expressionsQuerySummary.WithLabelValues(respStatus).Observe(duration) + + span.End() + }() + + qdr = data + if qdr == nil { + qdr = &backend.QueryDataResponse{} + } + now := start // <<< this should come from the original query parser + vars := make(mathexp.Vars) + for _, expression := range req.Expressions { + // Setup the variables + for _, refId := range expression.Command.NeedsVars() { + _, ok := vars[refId] + if !ok { + dr, ok := qdr.Responses[refId] + if ok { + allowLongFrames := false // TODO -- depends on input type and only if SQL? + _, res, err := b.converter.Convert(ctx, req.RefIDTypes[refId], dr.Frames, allowLongFrames) + if err != nil { + res.Error = err + } + vars[refId] = res + } else { + // This should error in the parsing phase + err := fmt.Errorf("missing variable %s for %s", refId, expression.RefID) + qdr.Responses[refId] = backend.DataResponse{ + Error: err, + } + return qdr, err + } + } + } + + refId := expression.RefID + results, err := expression.Command.Execute(ctx, now, vars, b.tracer) + if err != nil { + results.Error = err + } + qdr.Responses[refId] = backend.DataResponse{ + Error: results.Error, + Frames: results.Values.AsDataFrames(refId), + } + } + return qdr, nil } diff --git a/pkg/registry/apis/query/register.go b/pkg/registry/apis/query/register.go index e4e218e7ef..1b6460df3e 100644 --- a/pkg/registry/apis/query/register.go +++ b/pkg/registry/apis/query/register.go @@ -1,9 +1,9 @@ package query import ( - "encoding/json" - "net/http" - + data "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1" + "github.com/grafana/grafana-plugin-sdk-go/experimental/schemabuilder" + "github.com/prometheus/client_golang/prometheus" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" @@ -16,13 +16,17 @@ import ( "k8s.io/kube-openapi/pkg/spec3" "k8s.io/kube-openapi/pkg/validation/spec" + example "github.com/grafana/grafana/pkg/apis/example/v0alpha1" "github.com/grafana/grafana/pkg/apis/query/v0alpha1" "github.com/grafana/grafana/pkg/apiserver/builder" + "github.com/grafana/grafana/pkg/expr" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/plugins" - "github.com/grafana/grafana/pkg/registry/apis/query/runner" + "github.com/grafana/grafana/pkg/registry/apis/query/client" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/datasources" + "github.com/grafana/grafana/pkg/services/datasources/service" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" @@ -35,22 +39,39 @@ type QueryAPIBuilder struct { concurrentQueryLimit int userFacingDefaultError string returnMultiStatus bool // from feature toggle + features featuremgmt.FeatureToggles - runner v0alpha1.QueryRunner - registry v0alpha1.DataSourceApiServerRegistry + tracer tracing.Tracer + metrics *metrics + parser *queryParser + client DataSourceClientSupplier + registry v0alpha1.DataSourceApiServerRegistry + converter *expr.ResultConverter } func NewQueryAPIBuilder(features featuremgmt.FeatureToggles, - runner v0alpha1.QueryRunner, + client DataSourceClientSupplier, registry v0alpha1.DataSourceApiServerRegistry, -) *QueryAPIBuilder { + legacy service.LegacyDataSourceLookup, + registerer prometheus.Registerer, + tracer tracing.Tracer, +) (*QueryAPIBuilder, error) { + reader := expr.NewExpressionQueryReader(features) return &QueryAPIBuilder{ - concurrentQueryLimit: 4, // from config? + concurrentQueryLimit: 4, log: log.New("query_apiserver"), returnMultiStatus: features.IsEnabledGlobally(featuremgmt.FlagDatasourceQueryMultiStatus), - runner: runner, + client: client, registry: registry, - } + parser: newQueryParser(reader, legacy, tracer), + metrics: newMetrics(registerer), + tracer: tracer, + features: features, + converter: &expr.ResultConverter{ + Features: features, + Tracer: tracer, + }, + }, nil } func RegisterAPIService(features featuremgmt.FeatureToggles, @@ -60,28 +81,24 @@ func RegisterAPIService(features featuremgmt.FeatureToggles, accessControl accesscontrol.AccessControl, pluginClient plugins.Client, pCtxProvider *plugincontext.Provider, -) *QueryAPIBuilder { + registerer prometheus.Registerer, + tracer tracing.Tracer, + legacy service.LegacyDataSourceLookup, +) (*QueryAPIBuilder, error) { if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) { - return nil // skip registration unless opting into experimental apis + return nil, nil // skip registration unless opting into experimental apis } - builder := NewQueryAPIBuilder( + builder, err := NewQueryAPIBuilder( features, - runner.NewDirectQueryRunner(pluginClient, pCtxProvider), - runner.NewDirectRegistry(pluginStore, dataSourcesService), + &CommonDataSourceClientSupplier{ + Client: client.NewQueryClientForPluginClient(pluginClient, pCtxProvider), + }, + client.NewDataSourceRegistryFromStore(pluginStore, dataSourcesService), + legacy, registerer, tracer, ) - - // ONLY testdata... - if false { - builder = NewQueryAPIBuilder( - features, - runner.NewDummyTestRunner(), - runner.NewDummyRegistry(), - ) - } - apiregistration.RegisterAPI(builder) - return builder + return builder, err } func (b *QueryAPIBuilder) GetGroupVersion() schema.GroupVersion { @@ -92,7 +109,11 @@ func addKnownTypes(scheme *runtime.Scheme, gv schema.GroupVersion) { scheme.AddKnownTypes(gv, &v0alpha1.DataSourceApiServer{}, &v0alpha1.DataSourceApiServerList{}, + &v0alpha1.QueryDataRequest{}, &v0alpha1.QueryDataResponse{}, + &v0alpha1.QueryTypeDefinition{}, + &v0alpha1.QueryTypeDefinitionList{}, + &example.DummySubresource{}, ) } @@ -126,50 +147,7 @@ func (b *QueryAPIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefinitions { // Register additional routes with the server func (b *QueryAPIBuilder) GetAPIRoutes() *builder.APIRoutes { - defs := v0alpha1.GetOpenAPIDefinitions(func(path string) spec.Ref { return spec.Ref{} }) - querySchema := defs["github.com/grafana/grafana/pkg/apis/query/v0alpha1.QueryRequest"].Schema - responseSchema := defs["github.com/grafana/grafana/pkg/apis/query/v0alpha1.QueryDataResponse"].Schema - - var randomWalkQuery any - var randomWalkTable any - _ = json.Unmarshal([]byte(`{ - "queries": [ - { - "refId": "A", - "scenarioId": "random_walk", - "seriesCount": 1, - "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" - }, - "intervalMs": 60000, - "maxDataPoints": 20 - } - ], - "from": "1704893381544", - "to": "1704914981544" - }`), &randomWalkQuery) - - _ = json.Unmarshal([]byte(`{ - "queries": [ - { - "refId": "A", - "scenarioId": "random_walk_table", - "seriesCount": 1, - "datasource": { - "type": "grafana-testdata-datasource", - "uid": "PD8C576611E62080A" - }, - "intervalMs": 60000, - "maxDataPoints": 20 - } - ], - "from": "1704893381544", - "to": "1704914981544" - }`), &randomWalkTable) - - return &builder.APIRoutes{ - Root: []builder.APIRouteHandler{}, + routes := &builder.APIRoutes{ Namespace: []builder.APIRouteHandler{ { Path: "query", @@ -177,38 +155,81 @@ func (b *QueryAPIBuilder) GetAPIRoutes() *builder.APIRoutes { Post: &spec3.Operation{ OperationProps: spec3.OperationProps{ Tags: []string{"query"}, - Description: "query across multiple datasources with expressions. This api matches the legacy /ds/query endpoint", + Summary: "Query", + Description: "longer description here?", Parameters: []*spec3.Parameter{ { ParameterProps: spec3.ParameterProps{ Name: "namespace", - Description: "object name and auth scope, such as for teams and projects", In: "path", Required: true, - Schema: spec.StringProperty(), Example: "default", + Description: "workspace", + Schema: spec.StringProperty(), }, }, }, RequestBody: &spec3.RequestBody{ RequestBodyProps: spec3.RequestBodyProps{ - Required: true, - Description: "the query array", Content: map[string]*spec3.MediaType{ "application/json": { MediaTypeProps: spec3.MediaTypeProps{ - Schema: querySchema.WithExample(randomWalkQuery), + Schema: spec.RefSchema("#/components/schemas/" + QueryRequestSchemaKey), Examples: map[string]*spec3.Example{ - "random_walk": { + "A": { ExampleProps: spec3.ExampleProps{ - Summary: "random walk", - Value: randomWalkQuery, + Summary: "Random walk (testdata)", + Description: "Use testdata to execute a random walk query", + Value: `{ + "queries": [ + { + "refId": "A", + "scenarioId": "random_walk_table", + "seriesCount": 1, + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "PD8C576611E62080A" + }, + "intervalMs": 60000, + "maxDataPoints": 20 + } + ], + "from": "now-6h", + "to": "now" + }`, }, }, - "random_walk_table": { + "B": { ExampleProps: spec3.ExampleProps{ - Summary: "random walk (table)", - Value: randomWalkTable, + Summary: "With deprecated datasource name", + Description: "Includes an old style string for datasource reference", + Value: `{ + "queries": [ + { + "refId": "A", + "datasource": { + "type": "grafana-googlesheets-datasource", + "uid": "b1808c48-9fc9-4045-82d7-081781f8a553" + }, + "cacheDurationSeconds": 300, + "spreadsheet": "spreadsheetID", + "datasourceId": 4, + "intervalMs": 30000, + "maxDataPoints": 794 + }, + { + "refId": "Z", + "datasource": "old", + "maxDataPoints": 10, + "timeRange": { + "from": "100", + "to": "200" + } + } + ], + "from": "now-6h", + "to": "now" + }`, }, }, }, @@ -220,25 +241,12 @@ func (b *QueryAPIBuilder) GetAPIRoutes() *builder.APIRoutes { Responses: &spec3.Responses{ ResponsesProps: spec3.ResponsesProps{ StatusCodeResponses: map[int]*spec3.Response{ - http.StatusOK: { + 200: { ResponseProps: spec3.ResponseProps{ - Description: "Query results", Content: map[string]*spec3.MediaType{ "application/json": { MediaTypeProps: spec3.MediaTypeProps{ - Schema: &responseSchema, - }, - }, - }, - }, - }, - http.StatusMultiStatus: { - ResponseProps: spec3.ResponseProps{ - Description: "Errors exist in the downstream results", - Content: map[string]*spec3.MediaType{ - "application/json": { - MediaTypeProps: spec3.MediaTypeProps{ - Schema: &responseSchema, + Schema: spec.StringProperty(), // TODO!!! }, }, }, @@ -250,12 +258,47 @@ func (b *QueryAPIBuilder) GetAPIRoutes() *builder.APIRoutes { }, }, }, - Handler: b.handleQuery, + Handler: b.doQuery, }, }, } + return routes } func (b *QueryAPIBuilder) GetAuthorizer() authorizer.Authorizer { return nil // default is OK } + +const QueryRequestSchemaKey = "QueryRequestSchema" +const QueryPayloadSchemaKey = "QueryPayloadSchema" + +func (b *QueryAPIBuilder) PostProcessOpenAPI(oas *spec3.OpenAPI) (*spec3.OpenAPI, error) { + // The plugin description + oas.Info.Description = "Query service" + + // The root api URL + root := "/apis/" + b.GetGroupVersion().String() + "/" + + var err error + opts := schemabuilder.QuerySchemaOptions{ + PluginID: []string{""}, + QueryTypes: []data.QueryTypeDefinition{}, + Mode: schemabuilder.SchemaTypeQueryPayload, + } + oas.Components.Schemas[QueryPayloadSchemaKey], err = schemabuilder.GetQuerySchema(opts) + if err != nil { + return oas, err + } + opts.Mode = schemabuilder.SchemaTypeQueryRequest + oas.Components.Schemas[QueryRequestSchemaKey], err = schemabuilder.GetQuerySchema(opts) + if err != nil { + return oas, err + } + + // The root API discovery list + sub := oas.Paths.Paths[root] + if sub != nil && sub.Get != nil { + sub.Get.Tags = []string{"API Discovery"} // sorts first in the list + } + return oas, nil +} diff --git a/pkg/registry/apis/query/testdata/cyclic-references.json b/pkg/registry/apis/query/testdata/cyclic-references.json new file mode 100644 index 0000000000..bdbc9c9644 --- /dev/null +++ b/pkg/registry/apis/query/testdata/cyclic-references.json @@ -0,0 +1,29 @@ +{ + "description": "self dependencies", + "input": { + "from": "now-6", + "to": "now", + "queries": [ + { + "refId": "A", + "datasource": { + "type": "", + "uid": "__expr__" + }, + "expression": "$B", + "type": "math" + }, + { + "refId": "B", + "datasource": { + "type": "", + "uid": "__expr__" + }, + "type": "math", + "expression": "$A" + } + ] + }, + "expect": {}, + "error": "cyclic references in query" +} \ No newline at end of file diff --git a/pkg/registry/apis/query/testdata/multiple-uids-same-plugin.json b/pkg/registry/apis/query/testdata/multiple-uids-same-plugin.json new file mode 100644 index 0000000000..68a6705f09 --- /dev/null +++ b/pkg/registry/apis/query/testdata/multiple-uids-same-plugin.json @@ -0,0 +1,60 @@ +{ + "input": { + "from": "now-6", + "to": "now", + "queries": [ + { + "refId": "A", + "datasource": { + "type": "plugin-x", + "uid": "123" + } + }, + { + "refId": "B", + "datasource": { + "type": "plugin-x", + "uid": "456" + } + } + ] + }, + "expect": { + "requests": [ + { + "pluginId": "plugin-x", + "uid": "123", + "request": { + "from": "now-6", + "to": "now", + "queries": [ + { + "refId": "A", + "datasource": { + "type": "plugin-x", + "uid": "123" + } + } + ] + } + }, + { + "pluginId": "plugin-x", + "uid": "456", + "request": { + "from": "now-6", + "to": "now", + "queries": [ + { + "refId": "B", + "datasource": { + "type": "plugin-x", + "uid": "456" + } + } + ] + } + } + ] + } +} \ No newline at end of file diff --git a/pkg/registry/apis/query/testdata/self-reference.json b/pkg/registry/apis/query/testdata/self-reference.json new file mode 100644 index 0000000000..4248f70d58 --- /dev/null +++ b/pkg/registry/apis/query/testdata/self-reference.json @@ -0,0 +1,20 @@ +{ + "description": "self dependencies", + "input": { + "from": "now-6", + "to": "now", + "queries": [ + { + "refId": "A", + "datasource": { + "type": "", + "uid": "__expr__" + }, + "type": "math", + "expression": "$A" + } + ] + }, + "expect": {}, + "error": "expression [A] can not depend on itself" +} \ No newline at end of file diff --git a/pkg/registry/apis/query/testdata/with-expressions.json b/pkg/registry/apis/query/testdata/with-expressions.json new file mode 100644 index 0000000000..a1cbca8999 --- /dev/null +++ b/pkg/registry/apis/query/testdata/with-expressions.json @@ -0,0 +1,79 @@ +{ + "description": "one hidden query with two expressions that start out-of-order", + "input": { + "from": "now-6", + "to": "now", + "queries": [ + { + "refId": "C", + "datasource": { + "type": "", + "uid": "__expr__" + }, + "type": "reduce", + "expression": "$B", + "reducer": "last" + }, + { + "refId": "A", + "datasource": { + "type": "sql", + "uid": "123" + }, + "hide": true + }, + { + "refId": "B", + "datasource": { + "type": "", + "uid": "-100" + }, + "type": "math", + "expression": "$A + 10" + } + ] + }, + "expect": { + "requests": [ + { + "pluginId": "sql", + "uid": "123", + "request": { + "from": "now-6", + "to": "now", + "queries": [ + { + "refId": "A", + "datasource": { + "type": "sql", + "uid": "123" + } + } + ] + } + } + ], + "expressions": [ + { + "id": 2, + "refId": "B", + "type": "math", + "properties": { + "expression": "$A + 10" + } + }, + { + "id": 1, + "refId": "C", + "type": "reduce", + "properties": { + "expression": "$B", + "reducer": "last" + } + } + ], + "hide": [ + "A" + ] + } +} \ No newline at end of file diff --git a/pkg/server/wire.go b/pkg/server/wire.go index 7164ddc08c..1d97ab375e 100644 --- a/pkg/server/wire.go +++ b/pkg/server/wire.go @@ -284,6 +284,7 @@ var wireBasicSet = wire.NewSet( dashsnapsvc.ProvideService, datasourceservice.ProvideService, wire.Bind(new(datasources.DataSourceService), new(*datasourceservice.Service)), + datasourceservice.ProvideLegacyDataSourceLookup, alerting.ProvideService, serviceaccountsretriever.ProvideService, wire.Bind(new(serviceaccountsretriever.ServiceAccountRetriever), new(*serviceaccountsretriever.Service)), diff --git a/pkg/services/apiserver/standalone/factory.go b/pkg/services/apiserver/standalone/factory.go index 9c13bf4bd0..0ebfb2c3b9 100644 --- a/pkg/services/apiserver/standalone/factory.go +++ b/pkg/services/apiserver/standalone/factory.go @@ -5,17 +5,19 @@ import ( "fmt" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/prometheus/client_golang/prometheus" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1" "github.com/grafana/grafana/pkg/apiserver/builder" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/registry/apis/datasource" "github.com/grafana/grafana/pkg/registry/apis/example" "github.com/grafana/grafana/pkg/registry/apis/featuretoggle" "github.com/grafana/grafana/pkg/registry/apis/query" - "github.com/grafana/grafana/pkg/registry/apis/query/runner" + "github.com/grafana/grafana/pkg/registry/apis/query/client" "github.com/grafana/grafana/pkg/services/accesscontrol/actest" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/apiserver/options" @@ -73,9 +75,14 @@ func (p *DummyAPIFactory) MakeAPIServer(gv schema.GroupVersion) (builder.APIGrou case "query.grafana.app": return query.NewQueryAPIBuilder( featuremgmt.WithFeatures(), - runner.NewDummyTestRunner(), - runner.NewDummyRegistry(), - ), nil + &query.CommonDataSourceClientSupplier{ + Client: client.NewTestDataClient(), + }, + client.NewTestDataRegistry(), + nil, // legacy lookup + prometheus.NewRegistry(), // ??? + tracing.InitializeTracerForTest(), // ??? + ) case "featuretoggle.grafana.app": return featuretoggle.NewFeatureFlagAPIBuilder( diff --git a/pkg/services/datasources/service/legacy.go b/pkg/services/datasources/service/legacy.go new file mode 100644 index 0000000000..db5b008b13 --- /dev/null +++ b/pkg/services/datasources/service/legacy.go @@ -0,0 +1,90 @@ +package service + +import ( + "context" + "errors" + "fmt" + "sync" + + data "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1" + + "github.com/grafana/grafana/pkg/infra/appcontext" + "github.com/grafana/grafana/pkg/services/datasources" +) + +// LegacyDataSourceRetriever supports finding a reference to datasources using the name or internal ID +type LegacyDataSourceLookup interface { + // Find the UID from either the name or internal id + // NOTE the orgID will be fetched from the context + GetDataSourceFromDeprecatedFields(ctx context.Context, name string, id int64) (*data.DataSourceRef, error) +} + +var ( + _ DataSourceRetriever = (*Service)(nil) + _ LegacyDataSourceLookup = (*cachingLegacyDataSourceLookup)(nil) + _ LegacyDataSourceLookup = (*NoopLegacyDataSourcLookup)(nil) +) + +// NoopLegacyDataSourceRetriever does not even try to lookup, it returns a raw reference +type NoopLegacyDataSourcLookup struct { + Ref *data.DataSourceRef +} + +func (s *NoopLegacyDataSourcLookup) GetDataSourceFromDeprecatedFields(ctx context.Context, name string, id int64) (*data.DataSourceRef, error) { + return s.Ref, nil +} + +type cachingLegacyDataSourceLookup struct { + retriever DataSourceRetriever + cache map[string]cachedValue + cacheMu sync.Mutex +} + +type cachedValue struct { + ref *data.DataSourceRef + err error +} + +func ProvideLegacyDataSourceLookup(p *Service) LegacyDataSourceLookup { + return &cachingLegacyDataSourceLookup{ + retriever: p, + cache: make(map[string]cachedValue), + } +} + +func (s *cachingLegacyDataSourceLookup) GetDataSourceFromDeprecatedFields(ctx context.Context, name string, id int64) (*data.DataSourceRef, error) { + if id == 0 && name == "" { + return nil, fmt.Errorf("either name or ID must be set") + } + user, err := appcontext.User(ctx) + if err != nil { + return nil, err + } + key := fmt.Sprintf("%d/%s/%d", user.OrgID, name, id) + s.cacheMu.Lock() + defer s.cacheMu.Unlock() + + v, ok := s.cache[key] + if ok { + return v.ref, v.err + } + + ds, err := s.retriever.GetDataSource(ctx, &datasources.GetDataSourceQuery{ + OrgID: user.OrgID, + Name: name, + ID: id, + }) + if errors.Is(err, datasources.ErrDataSourceNotFound) && name != "" { + ds, err = s.retriever.GetDataSource(ctx, &datasources.GetDataSourceQuery{ + OrgID: user.OrgID, + UID: name, // Sometimes name is actually the UID :( + }) + } + v = cachedValue{ + err: err, + } + if ds != nil { + v.ref = &data.DataSourceRef{Type: ds.Type, UID: ds.UID} + } + return v.ref, v.err +} diff --git a/pkg/tests/apis/query/query_test.go b/pkg/tests/apis/query/query_test.go index 772f21a934..14426e939e 100644 --- a/pkg/tests/apis/query/query_test.go +++ b/pkg/tests/apis/query/query_test.go @@ -6,12 +6,11 @@ import ( "fmt" "testing" + "github.com/grafana/grafana-plugin-sdk-go/backend" + data "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1" "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/runtime/schema" - "github.com/grafana/grafana-plugin-sdk-go/backend" - - query "github.com/grafana/grafana/pkg/apis/query/v0alpha1" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/tests/apis" @@ -43,21 +42,61 @@ func TestIntegrationSimpleQuery(t *testing.T) { }) require.Equal(t, "test", ds.UID) - t.Run("Call query", func(t *testing.T) { + t.Run("Call query with expression", func(t *testing.T) { client := helper.Org1.Admin.RESTClient(t, &schema.GroupVersion{ Group: "query.grafana.app", Version: "v0alpha1", }) - q := query.GenericDataQuery{ - Datasource: &query.DataSourceRef{ - Type: "grafana-testdata-datasource", - UID: ds.UID, + q1 := data.DataQuery{ + CommonQueryProperties: data.CommonQueryProperties{ + RefID: "X", + Datasource: &data.DataSourceRef{ + Type: "grafana-testdata-datasource", + UID: ds.UID, + }, }, } - q.AdditionalProperties()["csvContent"] = "a,b,c\n1,hello,true" - q.AdditionalProperties()["scenarioId"] = "csv_content" - body, err := json.Marshal(&query.GenericQueryRequest{Queries: []query.GenericDataQuery{q}}) + q1.Set("scenarioId", "csv_content") + q1.Set("csvContent", "a\n1") + + q2 := data.DataQuery{ + CommonQueryProperties: data.CommonQueryProperties{ + RefID: "Y", + Datasource: &data.DataSourceRef{ + UID: "__expr__", + }, + }, + } + q2.Set("type", "math") + q2.Set("expression", "$X + 2") + + body, err := json.Marshal(&data.QueryDataRequest{ + Queries: []data.DataQuery{ + q1, q2, + // https://github.com/grafana/grafana-plugin-sdk-go/pull/921 + // data.NewDataQuery(map[string]any{ + // "refId": "X", + // "datasource": data.DataSourceRef{ + // Type: "grafana-testdata-datasource", + // UID: ds.UID, + // }, + // "scenarioId": "csv_content", + // "csvContent": "a\n1", + // }), + // data.NewDataQuery(map[string]any{ + // "refId": "Y", + // "datasource": data.DataSourceRef{ + // UID: "__expr__", + // }, + // "type": "math", + // "expression": "$X + 2", + // }), + }, + }) + + //fmt.Printf("%s", string(body)) + require.NoError(t, err) result := client.Post(). @@ -76,28 +115,15 @@ func TestIntegrationSimpleQuery(t *testing.T) { rsp := &backend.QueryDataResponse{} err = json.Unmarshal(body, rsp) require.NoError(t, err) - require.Equal(t, 1, len(rsp.Responses)) + require.Equal(t, 2, len(rsp.Responses)) - frame := rsp.Responses["A"].Frames[0] - disp, err := frame.StringTable(100, 10) - require.NoError(t, err) - fmt.Printf("%s\n", disp) + frameX := rsp.Responses["X"].Frames[0] + frameY := rsp.Responses["Y"].Frames[0] - type expect struct { - idx int - name string - val any - } - for _, check := range []expect{ - {0, "a", int64(1)}, - {1, "b", "hello"}, - {2, "c", true}, - } { - field := frame.Fields[check.idx] - require.Equal(t, check.name, field.Name) - - v, _ := field.ConcreteAt(0) - require.Equal(t, check.val, v) - } + vX, _ := frameX.Fields[0].ConcreteAt(0) + vY, _ := frameY.Fields[0].ConcreteAt(0) + + require.Equal(t, int64(1), vX) + require.Equal(t, float64(3), vY) // 1 + 2, but always float64 }) } diff --git a/pkg/tsdb/legacydata/conversions.go b/pkg/tsdb/legacydata/conversions.go index 37eeaffd53..4ca7322f9e 100644 --- a/pkg/tsdb/legacydata/conversions.go +++ b/pkg/tsdb/legacydata/conversions.go @@ -6,14 +6,13 @@ import ( "time" "github.com/grafana/grafana-plugin-sdk-go/backend" - - "github.com/grafana/grafana/pkg/apis/query/v0alpha1" + data "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1" ) // ToDataSourceQueries returns queries that should be sent to a single datasource // This will throw an error if the queries reference multiple instances -func ToDataSourceQueries(req v0alpha1.GenericQueryRequest) ([]backend.DataQuery, *v0alpha1.DataSourceRef, error) { - var dsRef *v0alpha1.DataSourceRef +func ToDataSourceQueries(req data.QueryDataRequest) ([]backend.DataQuery, *data.DataSourceRef, error) { + var dsRef *data.DataSourceRef var tr *backend.TimeRange if req.From != "" { val := NewDataTimeRange(req.From, req.To) @@ -47,7 +46,7 @@ func ToDataSourceQueries(req v0alpha1.GenericQueryRequest) ([]backend.DataQuery, } // Converts a generic query to a backend one -func toBackendDataQuery(q v0alpha1.GenericDataQuery, defaultTimeRange *backend.TimeRange) (backend.DataQuery, error) { +func toBackendDataQuery(q data.DataQuery, defaultTimeRange *backend.TimeRange) (backend.DataQuery, error) { var err error bq := backend.DataQuery{ RefID: q.RefID, From 0b71354c8d22566191132c6bc6241a608077807a Mon Sep 17 00:00:00 2001 From: Misi Date: Sat, 9 Mar 2024 19:24:48 +0100 Subject: [PATCH 0476/1406] Docs: Improve SSO Settings docs (#83914) * Improve docs * remove trailing slash * Update relref --- docs/sources/developers/http_api/sso-settings.md | 4 ++++ .../configure-authentication/azuread/index.md | 4 ++++ .../configure-authentication/generic-oauth/index.md | 4 ++++ .../configure-authentication/github/index.md | 4 ++++ .../configure-authentication/gitlab/index.md | 4 ++++ .../configure-authentication/google/index.md | 4 ++++ .../configure-authentication/keycloak/index.md | 4 ++++ .../configure-security/configure-authentication/okta/index.md | 4 ++++ 8 files changed, 32 insertions(+) diff --git a/docs/sources/developers/http_api/sso-settings.md b/docs/sources/developers/http_api/sso-settings.md index e1e284c76d..c7c8a343a3 100644 --- a/docs/sources/developers/http_api/sso-settings.md +++ b/docs/sources/developers/http_api/sso-settings.md @@ -22,6 +22,10 @@ title: SSO Settings API > If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "/docs/grafana/latest/administration/roles-and-permissions/access-control/custom-role-actions-scopes" >}}) for more information. +{{% admonition type="note" %}} +Available in Public Preview in Grafana 10.4 and on Grafana Cloud behind the `ssoSettingsApi` feature toggle. +{{% /admonition %}} + The API can be used to create, update, delete, get, and list SSO Settings. ## List SSO Settings diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/azuread/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/azuread/index.md index 1197769d0b..f77f8b40ee 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/azuread/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/azuread/index.md @@ -21,6 +21,10 @@ weight: 800 The Azure AD authentication allows you to use an Azure Active Directory tenant as an identity provider for Grafana. You can use Azure AD application roles to assign users and groups to Grafana roles from the Azure Portal. +{{% admonition type="note" %}} +If Users use the same email address in Azure AD that they use with other authentication providers (such as Grafana.com), you need to do additional configuration to ensure that the users are matched correctly. Please refer to the [Using the same email address to login with different identity providers]({{< relref "../../configure-authentication#using-the-same-email-address-to-login-with-different-identity-providers" >}}) documentation for more information. +{{% /admonition %}} + ## Create the Azure AD application To enable the Azure AD OAuth2, register your application with Azure AD. diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/generic-oauth/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/generic-oauth/index.md index 628ce6ca83..03aad024b0 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/generic-oauth/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/generic-oauth/index.md @@ -43,6 +43,10 @@ To follow this guide: - Ensure your identity provider returns OpenID UserInfo compatible information such as the `sub` claim. - If you are using refresh tokens, ensure you know how to set them up with your OAuth2 provider. Consult the documentation of your OAuth2 provider for more information. +{{% admonition type="note" %}} +If Users use the same email address in Azure AD that they use with other authentication providers (such as Grafana.com), you need to do additional configuration to ensure that the users are matched correctly. Please refer to the [Using the same email address to login with different identity providers]({{< relref "../../configure-authentication#using-the-same-email-address-to-login-with-different-identity-providers" >}}) documentation for more information. +{{% /admonition %}} + ## Configure generic OAuth authentication client using the Grafana UI {{% admonition type="note" %}} diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/github/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/github/index.md index 60ee876229..4ca721eaf6 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/github/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/github/index.md @@ -23,6 +23,10 @@ weight: 900 This topic describes how to configure GitHub OAuth2 authentication. +{{% admonition type="note" %}} +If Users use the same email address in GitHub that they use with other authentication providers (such as Grafana.com), you need to do additional configuration to ensure that the users are matched correctly. Please refer to the [Using the same email address to login with different identity providers]({{< relref "../../configure-authentication#using-the-same-email-address-to-login-with-different-identity-providers" >}}) documentation for more information. +{{% /admonition %}} + ## Before you begin Ensure you know how to create a GitHub OAuth app. Consult GitHub's documentation on [creating an OAuth app](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app) for more information. diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/gitlab/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/gitlab/index.md index 517951479e..0541095257 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/gitlab/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/gitlab/index.md @@ -23,6 +23,10 @@ weight: 1000 This topic describes how to configure GitLab OAuth2 authentication. +{{% admonition type="note" %}} +If Users use the same email address in GitLab that they use with other authentication providers (such as Grafana.com), you need to do additional configuration to ensure that the users are matched correctly. Please refer to the [Using the same email address to login with different identity providers]({{< relref "../../configure-authentication#using-the-same-email-address-to-login-with-different-identity-providers" >}}) documentation for more information. +{{% /admonition %}} + ## Before you begin Ensure you know how to create a GitLab OAuth application. Consult GitLab's documentation on [creating a GitLab OAuth application](https://docs.gitlab.com/ee/integration/oauth_provider.html) for more information. diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/google/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/google/index.md index 38c185d606..eca7ab7253 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/google/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/google/index.md @@ -16,6 +16,10 @@ weight: 1100 To enable Google OAuth2 you must register your application with Google. Google will generate a client ID and secret key for you to use. +{{% admonition type="note" %}} +If Users use the same email address in Google that they use with other authentication providers (such as Grafana.com), you need to do additional configuration to ensure that the users are matched correctly. Please refer to the [Using the same email address to login with different identity providers]({{< relref "../../configure-authentication#using-the-same-email-address-to-login-with-different-identity-providers" >}}) documentation for more information. +{{% /admonition %}} + ## Create Google OAuth keys First, you need to create a Google OAuth Client: diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/keycloak/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/keycloak/index.md index 3f1178b079..6f2dbea11a 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/keycloak/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/keycloak/index.md @@ -24,6 +24,10 @@ Keycloak OAuth2 authentication allows users to log in to Grafana using their Key Refer to [Generic OAuth authentication]({{< relref "../generic-oauth" >}}) for extra configuration options available for this provider. +{{% admonition type="note" %}} +If Users use the same email address in Keycloak that they use with other authentication providers (such as Grafana.com), you need to do additional configuration to ensure that the users are matched correctly. Please refer to the [Using the same email address to login with different identity providers]({{< relref "../../configure-authentication#using-the-same-email-address-to-login-with-different-identity-providers" >}}) documentation for more information. +{{% /admonition %}} + You may have to set the `root_url` option of `[server]` for the callback URL to be correct. For example in case you are serving Grafana behind a proxy. diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/okta/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/okta/index.md index 9501898f53..9cc47511f5 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/okta/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/okta/index.md @@ -16,6 +16,10 @@ weight: 1400 {{< docs/shared lookup="auth/intro.md" source="grafana" version="" >}} +{{% admonition type="note" %}} +If Users use the same email address in Okta that they use with other authentication providers (such as Grafana.com), you need to do additional configuration to ensure that the users are matched correctly. Please refer to the [Using the same email address to login with different identity providers]({{< relref "../../configure-authentication#using-the-same-email-address-to-login-with-different-identity-providers" >}}) documentation for more information. +{{% /admonition %}} + ## Before you begin To follow this guide, ensure you have permissions in your Okta workspace to create an OIDC app. From 57df3b84dce3f4a30f08005161db18fc7aacfbd0 Mon Sep 17 00:00:00 2001 From: Leon Sorokin Date: Sun, 10 Mar 2024 22:11:11 -0500 Subject: [PATCH 0477/1406] StateTimeline: Treat second time field as state endings (#84130) --- .../timeline-align-endtime.json | 271 ++++++++++++++++++ .../timeline-align-nulls-retain.json | 213 ++++++++++++++ devenv/jsonnet/dev-dashboards.libsonnet | 2 + .../transformers/joinDataFrames.ts | 6 +- public/app/core/components/GraphNG/utils.ts | 10 +- .../components/TimelineChart/utils.test.ts | 181 +++++++++++- .../core/components/TimelineChart/utils.ts | 71 ++++- 7 files changed, 740 insertions(+), 14 deletions(-) create mode 100644 devenv/dev-dashboards/panel-timeline/timeline-align-endtime.json create mode 100644 devenv/dev-dashboards/panel-timeline/timeline-align-nulls-retain.json diff --git a/devenv/dev-dashboards/panel-timeline/timeline-align-endtime.json b/devenv/dev-dashboards/panel-timeline/timeline-align-endtime.json new file mode 100644 index 0000000000..8542c7349b --- /dev/null +++ b/devenv/dev-dashboards/panel-timeline/timeline-align-endtime.json @@ -0,0 +1,271 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 988, + "links": [], + "panels": [ + { + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "fillOpacity": 70, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineWidth": 0, + "spanNulls": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 13, + "w": 15, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "alignValue": "left", + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "mergeValues": true, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": { + "maxHeight": 600, + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "PD8C576611E62080A" + }, + "rawFrameContent": "[\n {\n \"schema\": {\n \"refId\": \"A\",\n \"meta\": {\n \"typeVersion\": [\n 0,\n 0\n ]\n },\n \"name\": \"A\",\n \"fields\": [\n {\n \"name\": \"channel\",\n \"config\": {\n \"selector\": \"channel\"\n },\n \"type\": \"string\"\n },\n {\n \"name\": \"name\",\n \"config\": {\n \"selector\": \"name\"\n },\n \"type\": \"string\"\n },\n {\n \"name\": \"starttime\",\n \"config\": {\n \"selector\": \"starttime\"\n },\n \"type\": \"string\"\n },\n {\n \"name\": \"endtime\",\n \"config\": {\n \"selector\": \"endtime\"\n },\n \"type\": \"string\"\n },\n {\n \"name\": \"duration_minutes\",\n \"config\": {\n \"selector\": \"duration_minutes\"\n },\n \"type\": \"number\"\n },\n {\n \"name\": \"state\",\n \"config\": {\n \"selector\": \"state\"\n },\n \"type\": \"string\"\n }\n ]\n },\n \"data\": {\n \"values\": [\n [\n \"Channel 1\",\n \"Channel 2\",\n \"Channel 1\",\n \"Channel 2\"\n ],\n [\n \"Event 1\",\n \"Event 2\",\n \"Event 3\",\n \"Event 4\"\n ],\n [\n \"2024-02-28T08:00:00Z\",\n \"2024-02-28T09:00:00Z\",\n \"2024-02-28T11:00:00Z\",\n \"2024-02-28T12:30:00Z\"\n ],\n [\n \"2024-02-28T10:00:00Z\",\n \"2024-02-28T10:30:00Z\",\n \"2024-02-28T14:00:00Z\",\n \"2024-02-28T13:30:00Z\"\n ],\n [\n 120,\n 90,\n 180,\n 60\n ],\n [\n \"OK\",\n \"ERROR\",\n \"NO_DATA\",\n \"WARNING\"\n ]\n ]\n }\n }\n]", + "refId": "A", + "scenarioId": "raw_frame" + } + ], + "title": "Raw frames w/enums", + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "channel": false, + "duration_minutes": true, + "name": true + }, + "includeByName": {}, + "indexByName": {}, + "renameByName": {} + } + }, + { + "id": "convertFieldType", + "options": { + "conversions": [ + { + "destinationType": "time", + "enumConfig": { + "text": [ + "2024-02-28T08:00:00Z", + "2024-02-28T09:00:00Z", + "2024-02-28T11:00:00Z", + "2024-02-28T12:30:00Z" + ] + }, + "targetField": "starttime" + }, + { + "destinationType": "time", + "targetField": "endtime" + }, + { + "destinationType": "enum", + "enumConfig": { + "text": [ + "OK", + "ERROR", + "NO_DATA", + "WARNING" + ] + }, + "targetField": "state" + } + ], + "fields": {} + } + }, + { + "id": "partitionByValues", + "options": { + "fields": [ + "channel" + ], + "keepFields": false, + "naming": { + "asLabels": false + } + } + } + ], + "type": "state-timeline" + }, + { + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "fillOpacity": 70, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineWidth": 0, + "spanNulls": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 12, + "w": 15, + "x": 0, + "y": 13 + }, + "id": 2, + "options": { + "alignValue": "left", + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "mergeValues": true, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": { + "maxHeight": 600, + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "alias": "Channel 1", + "csvContent": "starttime,endtime,state\n1709107200000,1709114400000,OK\n1709118000000,1709128800000,NO_DATA", + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "PD8C576611E62080A" + }, + "refId": "A", + "scenarioId": "csv_content" + }, + { + "alias": "Channel 2", + "csvContent": "starttime,endtime,state\n1709110800000,1709116200000,ERROR\n1709123400000,1709127000000,WARNING", + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "PD8C576611E62080A" + }, + "refId": "B", + "scenarioId": "csv_content" + } + ], + "title": "CSV content", + "type": "state-timeline" + } + ], + "refresh": "", + "schemaVersion": 39, + "tags": [ + "gdev", + "panel-tests", + "graph-ng", + "demo" + ], + "templating": { + "list": [] + }, + "time": { + "from": "2024-02-28T07:47:21.428Z", + "to": "2024-02-28T14:12:43.391Z" + }, + "timeRangeUpdatedDuringEditOrView": false, + "timepicker": {}, + "timezone": "browser", + "title": "Panel Tests - StateTimeline - multiple frames with endTime", + "uid": "cdf3gkge5reo0f", + "version": 4, + "weekStart": "" +} \ No newline at end of file diff --git a/devenv/dev-dashboards/panel-timeline/timeline-align-nulls-retain.json b/devenv/dev-dashboards/panel-timeline/timeline-align-nulls-retain.json new file mode 100644 index 0000000000..49907cca77 --- /dev/null +++ b/devenv/dev-dashboards/panel-timeline/timeline-align-nulls-retain.json @@ -0,0 +1,213 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 993, + "links": [], + "panels": [ + { + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "PD8C576611E62080A" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "light-blue", + "mode": "fixed" + }, + "custom": { + "fillOpacity": 20, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineWidth": 1, + "spanNulls": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "blue", + "value": null + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Dose" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#289fb0", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Mix" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#d4b10b", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cook" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#c900c3", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Int. Shear" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#a49225", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Ext. Shear" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#148dd7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Transfer" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#01b70c", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 19, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 2, + "options": { + "alignValue": "center", + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "mergeValues": false, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": { + "maxHeight": 600, + "mode": "single", + "sort": "none" + } + }, + "repeat": "CHANNEL", + "repeatDirection": "v", + "targets": [ + { + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "PD8C576611E62080A" + }, + "rawFrameContent": "[\n {\n \"schema\": {\n \"refId\": \"Dose\",\n \"meta\": {\n \"executedQueryString\": \"from(bucket: \\\"data\\\")\\r\\n |> range(start: 2023-10-20T05:04:00Z, stop: 2023-10-20T07:22:00Z)\\r\\n |> filter(fn: (r) => r[\\\"_field\\\"] == \\\"FactoryManager_Analogs_DB.A01C02U09.PHS.Dose\\\")\\r\\n |> keep(columns: [\\\"_time\\\", \\\"_value\\\"])\\r\\n |> map(fn: (r) => ({ \\r\\n \\\"Dose\\\": r._value,\\r\\n time: r._time,\\r\\n }))\\r\\n\",\n \"typeVersion\": [\n 0,\n 0\n ]\n },\n \"fields\": [\n {\n \"config\": {},\n \"labels\": {},\n \"name\": \"time\",\n \"type\": \"time\",\n \"typeInfo\": {\n \"frame\": \"time.Time\",\n \"nullable\": true\n }\n },\n {\n \"config\": {},\n \"labels\": {},\n \"name\": \"Dose\",\n \"type\": \"string\",\n \"typeInfo\": {\n \"frame\": \"string\",\n \"nullable\": true\n }\n }\n ]\n },\n \"data\": {\n \"values\": [\n [\n 1697781872300,\n 1697781963303,\n 1697784138453,\n 1697784160451\n ],\n [\n \"Cold Water Dosing Active (150 ltrs)\",\n null,\n \"Hot Water Dosing Active (50 ltrs)\",\n null\n ]\n ]\n }\n },\n {\n \"schema\": {\n \"refId\": \"Mix\",\n \"meta\": {\n \"executedQueryString\": \"from(bucket: \\\"data\\\")\\r\\n |> range(start: 2023-10-20T05:04:00Z, stop: 2023-10-20T07:22:00Z)\\r\\n |> filter(fn: (r) => r[\\\"_field\\\"] == \\\"FactoryManager_Analogs_DB.A01C02U09.PHS.Mix\\\")\\r\\n |> keep(columns: [\\\"_time\\\", \\\"_value\\\"])\\r\\n |> map(fn: (r) => ({ \\r\\n \\\"Mix\\\": r._value,\\r\\n time: r._time,\\r\\n }))\\r\\n\",\n \"typeVersion\": [\n 0,\n 0\n ]\n },\n \"fields\": [\n {\n \"config\": {},\n \"labels\": {},\n \"name\": \"time\",\n \"type\": \"time\",\n \"typeInfo\": {\n \"frame\": \"time.Time\",\n \"nullable\": true\n }\n },\n {\n \"config\": {},\n \"labels\": {},\n \"name\": \"Mix\",\n \"type\": \"string\",\n \"typeInfo\": {\n \"frame\": \"string\",\n \"nullable\": true\n }\n }\n ]\n },\n \"data\": {\n \"values\": [\n [\n 1697778291972,\n 1697778393992,\n 1697778986994,\n 1697786485890\n ],\n [\n \"Running Constant Forward\",\n null,\n \"Running Constant Forward\",\n null\n ]\n ]\n }\n },\n {\n \"schema\": {\n \"refId\": \"Cook\",\n \"meta\": {\n \"executedQueryString\": \"from(bucket: \\\"data\\\")\\r\\n |> range(start: 2023-10-20T05:04:00Z, stop: 2023-10-20T07:22:00Z)\\r\\n |> filter(fn: (r) => r[\\\"_field\\\"] == \\\"FactoryManager_Analogs_DB.A01C02U09.PHS.Cook\\\")\\r\\n |> keep(columns: [\\\"_time\\\", \\\"_value\\\"])\\r\\n |> map(fn: (r) => ({ \\r\\n \\\"Cook\\\": r._value,\\r\\n time: r._time,\\r\\n }))\\r\\n\",\n \"typeVersion\": [\n 0,\n 0\n ]\n },\n \"fields\": [\n {\n \"config\": {},\n \"labels\": {},\n \"name\": \"time\",\n \"type\": \"time\",\n \"typeInfo\": {\n \"frame\": \"time.Time\",\n \"nullable\": true\n }\n },\n {\n \"config\": {},\n \"labels\": {},\n \"name\": \"Cook\",\n \"type\": \"string\",\n \"typeInfo\": {\n \"frame\": \"string\",\n \"nullable\": true\n }\n }\n ]\n },\n \"data\": {\n \"values\": [\n [\n 1697779163986,\n 1697779921045,\n 1697780221094,\n 1697780521111,\n 1697781186192,\n 1697781786291,\n 1697783332361,\n 1697783784395,\n 1697783790397,\n 1697784146478,\n 1697784517471,\n 1697784523487,\n 1697784949480,\n 1697785369505\n ],\n [\n \"Heating to Setpoint (92c)\",\n \"Stage Time Running (5 mins)\",\n null,\n \"Heating to Setpoint (96c)\",\n \"Stage Time Running (10 mins)\",\n null,\n \"Heating to Setpoint (92c)\",\n \"Stage Time Running (0 mins)\",\n null,\n \"Heating to Setpoint (92c)\",\n \"Stage Time Running (0 mins)\",\n null,\n \"CCP in Progress (7 mins)\",\n null\n ]\n ]\n }\n },\n {\n \"schema\": {\n \"refId\": \"Shear\",\n \"meta\": {\n \"executedQueryString\": \"from(bucket: \\\"data\\\")\\r\\n |> range(start: 2023-10-20T05:04:00Z, stop: 2023-10-20T07:22:00Z)\\r\\n |> filter(fn: (r) => r[\\\"_field\\\"] == \\\"FactoryManager_Analogs_DB.A01C02U09.PHS.Shear\\\")\\r\\n |> keep(columns: [\\\"_time\\\", \\\"_value\\\"])\\r\\n |> map(fn: (r) => ({ \\r\\n \\\"Int. Shear\\\": r._value,\\r\\n time: r._time,\\r\\n }))\\r\\n\",\n \"typeVersion\": [\n 0,\n 0\n ]\n },\n \"fields\": [\n {\n \"config\": {},\n \"labels\": {},\n \"name\": \"time\",\n \"type\": \"time\",\n \"typeInfo\": {\n \"frame\": \"time.Time\",\n \"nullable\": true\n }\n },\n {\n \"config\": {},\n \"labels\": {},\n \"name\": \"Int. Shear\",\n \"type\": \"string\",\n \"typeInfo\": {\n \"frame\": \"string\",\n \"nullable\": true\n }\n }\n ]\n },\n \"data\": {\n \"values\": [\n [\n 1697782100330,\n 1697782832342\n ],\n [\n \"Shearing Active (12 mins)\",\n null\n ]\n ]\n }\n },\n {\n \"schema\": {\n \"refId\": \"Recirc\",\n \"meta\": {\n \"executedQueryString\": \"from(bucket: \\\"data\\\")\\r\\n |> range(start: 2023-10-20T05:04:00Z, stop: 2023-10-20T07:22:00Z)\\r\\n |> filter(fn: (r) => r[\\\"_field\\\"] == \\\"FactoryManager_Analogs_DB.A01C02U09.PHS.Recirc\\\")\\r\\n |> keep(columns: [\\\"_time\\\", \\\"_value\\\"])\\r\\n |> map(fn: (r) => ({ \\r\\n \\\"Ext. Shear\\\": r._value,\\r\\n time: r._time,\\r\\n }))\\r\\n\",\n \"typeVersion\": [\n 0,\n 0\n ]\n },\n \"fields\": []\n },\n \"data\": {\n \"values\": []\n }\n },\n {\n \"schema\": {\n \"refId\": \"Transfer\",\n \"meta\": {\n \"executedQueryString\": \"from(bucket: \\\"data\\\")\\r\\n |> range(start: 2023-10-20T05:04:00Z, stop: 2023-10-20T07:22:00Z)\\r\\n |> filter(fn: (r) => r[\\\"_field\\\"] == \\\"FactoryManager_Analogs_DB.A01C02U09.PHS.Transfer\\\")\\r\\n |> keep(columns: [\\\"_time\\\", \\\"_value\\\"])\\r\\n |> map(fn: (r) => ({ \\r\\n \\\"Transfer\\\": r._value,\\r\\n time: r._time,\\r\\n }))\\r\\n\",\n \"typeVersion\": [\n 0,\n 0\n ]\n },\n \"fields\": [\n {\n \"config\": {},\n \"labels\": {},\n \"name\": \"time\",\n \"type\": \"time\",\n \"typeInfo\": {\n \"frame\": \"time.Time\",\n \"nullable\": true\n }\n },\n {\n \"config\": {},\n \"labels\": {},\n \"name\": \"Transfer\",\n \"type\": \"string\",\n \"typeInfo\": {\n \"frame\": \"string\",\n \"nullable\": true\n }\n }\n ]\n },\n \"data\": {\n \"values\": [\n [\n 1697785713869,\n 1697785753879,\n 1697785764887,\n 1697785875872,\n 1697786481929\n ],\n [\n \"Pre-Start Drain\",\n null,\n \"Build Pressure (0.6 Barg)\",\n \"Transfer in progress (0.7 Barg)\",\n \"Wait for pressure dissipation (0.2 Barg)\"\n ]\n ]\n }\n }\n]", + "refId": "A", + "scenarioId": "raw_frame" + } + ], + "title": "Reproduced with embedded data", + "type": "state-timeline" + } + ], + "refresh": "", + "schemaVersion": 39, + "tags": [ + "gdev", + "panel-tests", + "graph-ng", + "demo" + ], + "templating": { + "list": [] + }, + "time": { + "from": "2023-10-20T05:04:00.000Z", + "to": "2023-10-20T07:22:00.000Z" + }, + "timeRangeUpdatedDuringEditOrView": false, + "timepicker": {}, + "timezone": "utc", + "title": "Panel Tests - StateTimeline - multiple frames with nulls", + "uid": "edf55caay3w8wa", + "version": 4, + "weekStart": "" +} \ No newline at end of file diff --git a/devenv/jsonnet/dev-dashboards.libsonnet b/devenv/jsonnet/dev-dashboards.libsonnet index de3ee9e822..d9fcd5ef9a 100644 --- a/devenv/jsonnet/dev-dashboards.libsonnet +++ b/devenv/jsonnet/dev-dashboards.libsonnet @@ -105,6 +105,8 @@ "testdata_alerts": (import '../dev-dashboards/alerting/testdata_alerts.json'), "text-options": (import '../dev-dashboards/panel-text/text-options.json'), "time_zone_support": (import '../dev-dashboards/scenarios/time_zone_support.json'), + "timeline-align-endtime": (import '../dev-dashboards/panel-timeline/timeline-align-endtime.json'), + "timeline-align-nulls-retain": (import '../dev-dashboards/panel-timeline/timeline-align-nulls-retain.json'), "timeline-demo": (import '../dev-dashboards/panel-timeline/timeline-demo.json'), "timeline-modes": (import '../dev-dashboards/panel-timeline/timeline-modes.json'), "timeline-thresholds-mappings": (import '../dev-dashboards/panel-timeline/timeline-thresholds-mappings.json'), diff --git a/packages/grafana-data/src/transformations/transformers/joinDataFrames.ts b/packages/grafana-data/src/transformations/transformers/joinDataFrames.ts index a03edb78f0..0b994b7940 100644 --- a/packages/grafana-data/src/transformations/transformers/joinDataFrames.ts +++ b/packages/grafana-data/src/transformations/transformers/joinDataFrames.ts @@ -101,7 +101,11 @@ export function joinDataFrames(options: JoinOptions): DataFrame | undefined { } const nullMode = - options.nullMode ?? ((field: Field) => (field.config.custom?.spanNulls === true ? NULL_REMOVE : NULL_EXPAND)); + options.nullMode ?? + ((field: Field) => { + let spanNulls = field.config.custom?.spanNulls; + return spanNulls === true ? NULL_REMOVE : spanNulls === -1 ? NULL_RETAIN : NULL_EXPAND; + }); if (options.frames.length === 1) { let frame = options.frames[0]; diff --git a/public/app/core/components/GraphNG/utils.ts b/public/app/core/components/GraphNG/utils.ts index dc49d3abe3..b0c4368f3b 100644 --- a/public/app/core/components/GraphNG/utils.ts +++ b/public/app/core/components/GraphNG/utils.ts @@ -107,8 +107,14 @@ export function preparePlotFrame(frames: DataFrame[], dimFields: XYFieldMatchers // prevent minesweeper-expansion of nulls (gaps) when joining bars // since bar width is determined from the minimum distance between non-undefined values // (this strategy will still retain any original pre-join nulls, though) - nullMode: (field) => - isVisibleBarField(field) ? NULL_RETAIN : field.config.custom?.spanNulls === true ? NULL_REMOVE : NULL_EXPAND, + nullMode: (field) => { + if (isVisibleBarField(field)) { + return NULL_RETAIN; + } + + let spanNulls = field.config.custom?.spanNulls; + return spanNulls === true ? NULL_REMOVE : spanNulls === -1 ? NULL_RETAIN : NULL_EXPAND; + }, }); if (alignedFrame) { diff --git a/public/app/core/components/TimelineChart/utils.test.ts b/public/app/core/components/TimelineChart/utils.test.ts index 15a291602b..baa114f5da 100644 --- a/public/app/core/components/TimelineChart/utils.test.ts +++ b/public/app/core/components/TimelineChart/utils.test.ts @@ -1,6 +1,18 @@ -import { createTheme, FieldType, ThresholdsMode, TimeRange, toDataFrame, dateTime, DataFrame } from '@grafana/data'; +import { + createTheme, + FieldType, + ThresholdsMode, + TimeRange, + toDataFrame, + dateTime, + DataFrame, + fieldMatchers, + FieldMatcherID, +} from '@grafana/data'; import { LegendDisplayMode, VizLegendOptions } from '@grafana/schema'; +import { preparePlotFrame } from '../GraphNG/utils'; + import { findNextStateIndex, fmtDuration, @@ -87,6 +99,173 @@ describe('prepare timeline graph', () => { const result = prepareTimelineFields(frames, true, timeRange, theme); expect(result.frames?.[0].fields[0].values).toEqual([1, 2, 3, 4]); }); + + it('join multiple frames with NULL_RETAIN rather than NULL_EXPAND', () => { + const timeRange2: TimeRange = { + from: dateTime('2023-10-20T05:04:00.000Z'), + to: dateTime('2023-10-20T07:22:00.000Z'), + raw: { + from: dateTime('2023-10-20T05:04:00.000Z'), + to: dateTime('2023-10-20T07:22:00.000Z'), + }, + }; + + const frames = [ + toDataFrame({ + name: 'Mix', + fields: [ + { name: 'time', type: FieldType.time, values: [1697778291972, 1697778393992, 1697778986994, 1697786485890] }, + { name: 'state', type: FieldType.string, values: ['RUN', null, 'RUN', null] }, + ], + }), + toDataFrame({ + name: 'Cook', + fields: [ + { + name: 'time', + type: FieldType.time, + values: [ + 1697779163986, 1697779921045, 1697780221094, 1697780521111, 1697781186192, 1697781786291, 1697783332361, + 1697783784395, 1697783790397, 1697784146478, 1697784517471, 1697784523487, 1697784949480, 1697785369505, + ], + }, + { + name: 'state', + type: FieldType.string, + values: [ + 'Heat', + 'Stage', + null, + 'Heat', + 'Stage', + null, + 'Heat', + 'Stage', + null, + 'Heat', + 'Stage', + null, + 'CCP', + null, + ], + }, + ], + }), + ]; + + const info = prepareTimelineFields(frames, true, timeRange2, theme); + + let joined = preparePlotFrame( + info.frames!, + { + x: fieldMatchers.get(FieldMatcherID.firstTimeField).get({}), + y: fieldMatchers.get(FieldMatcherID.byType).get('string'), + }, + timeRange2 + ); + + let vals = joined!.fields.map((f) => f.values); + + expect(vals).toEqual([ + [ + 1697778291972, 1697778393992, 1697778986994, 1697779163986, 1697779921045, 1697780221094, 1697780521111, + 1697781186192, 1697781786291, 1697783332361, 1697783784395, 1697783790397, 1697784146478, 1697784517471, + 1697784523487, 1697784949480, 1697785369505, 1697786485890, + ], + [ + 'RUN', + null, + 'RUN', + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + null, + ], + [ + undefined, + undefined, + undefined, + 'Heat', + 'Stage', + null, + 'Heat', + 'Stage', + null, + 'Heat', + 'Stage', + null, + 'Heat', + 'Stage', + null, + 'CCP', + null, + undefined, + ], + ]); + }); + + it('join multiple frames with start and end time fields', () => { + const timeRange2: TimeRange = { + from: dateTime('2024-02-28T07:47:21.428Z'), + to: dateTime('2024-02-28T14:12:43.391Z'), + raw: { + from: dateTime('2024-02-28T07:47:21.428Z'), + to: dateTime('2024-02-28T14:12:43.391Z'), + }, + }; + + const frames = [ + toDataFrame({ + name: 'Channel 1', + fields: [ + { name: 'starttime', type: FieldType.time, values: [1709107200000, 1709118000000] }, + { name: 'endtime', type: FieldType.time, values: [1709114400000, 1709128800000] }, + { name: 'state', type: FieldType.string, values: ['OK', 'NO_DATA'] }, + ], + }), + toDataFrame({ + name: 'Channel 2', + fields: [ + { name: 'starttime', type: FieldType.time, values: [1709110800000, 1709123400000] }, + { name: 'endtime', type: FieldType.time, values: [1709116200000, 1709127000000] }, + { name: 'state', type: FieldType.string, values: ['ERROR', 'WARNING'] }, + ], + }), + ]; + + const info = prepareTimelineFields(frames, true, timeRange2, theme); + + let joined = preparePlotFrame( + info.frames!, + { + x: fieldMatchers.get(FieldMatcherID.firstTimeField).get({}), + y: fieldMatchers.get(FieldMatcherID.byType).get('string'), + }, + timeRange2 + ); + + let vals = joined!.fields.map((f) => f.values); + + expect(vals).toEqual([ + [ + 1709107200000, 1709110800000, 1709114400000, 1709116200000, 1709118000000, 1709123400000, 1709127000000, + 1709128800000, + ], + ['OK', undefined, null, undefined, 'NO_DATA', undefined, undefined, null], + [undefined, 'ERROR', undefined, null, undefined, 'WARNING', null, undefined], + ]); + }); }); describe('findNextStateIndex', () => { diff --git a/public/app/core/components/TimelineChart/utils.ts b/public/app/core/components/TimelineChart/utils.ts index 0b71fb5907..731d53351b 100644 --- a/public/app/core/components/TimelineChart/utils.ts +++ b/public/app/core/components/TimelineChart/utils.ts @@ -22,8 +22,9 @@ import { ThresholdsMode, TimeRange, cacheFieldDisplayNames, + outerJoinDataFrames, } from '@grafana/data'; -import { maybeSortFrame } from '@grafana/data/src/transformations/transformers/joinDataFrames'; +import { maybeSortFrame, NULL_RETAIN } from '@grafana/data/src/transformations/transformers/joinDataFrames'; import { applyNullInsertThreshold } from '@grafana/data/src/transformations/transformers/nulls/nullInsertThreshold'; import { nullToValue } from '@grafana/data/src/transformations/transformers/nulls/nullToValue'; import { @@ -445,15 +446,61 @@ export function prepareTimelineFields( const frames: DataFrame[] = []; for (let frame of series) { - let isTimeseries = false; + let startFieldIdx = -1; + let endFieldIdx = -1; + + for (let i = 0; i < frame.fields.length; i++) { + let f = frame.fields[i]; + + if (f.type === FieldType.time) { + if (startFieldIdx === -1) { + startFieldIdx = i; + } else if (endFieldIdx === -1) { + endFieldIdx = i; + break; + } + } + } + + let isTimeseries = startFieldIdx !== -1; let changed = false; - let maybeSortedFrame = maybeSortFrame( - frame, - frame.fields.findIndex((f) => f.type === FieldType.time) - ); + frame = maybeSortFrame(frame, startFieldIdx); + + // if we have a second time field, assume it is state end timestamps + // and insert nulls into the data at the end timestamps + if (endFieldIdx !== -1) { + let startFrame: DataFrame = { + ...frame, + fields: frame.fields.filter((f, i) => i !== endFieldIdx), + }; + + let endFrame: DataFrame = { + length: frame.length, + fields: [frame.fields[endFieldIdx]], + }; + + frame = outerJoinDataFrames({ + frames: [startFrame, endFrame], + keepDisplayNames: true, + nullMode: () => NULL_RETAIN, + })!; + + frame.fields.forEach((f, i) => { + if (i > 0) { + let vals = f.values; + for (let i = 0; i < vals.length; i++) { + if (vals[i] == null) { + vals[i] = null; + } + } + } + }); + + changed = true; + } let nulledFrame = applyNullInsertThreshold({ - frame: maybeSortedFrame, + frame, refFieldPseudoMin: timeRange.from.valueOf(), refFieldPseudoMax: timeRange.to.valueOf(), }); @@ -462,8 +509,10 @@ export function prepareTimelineFields( changed = true; } + frame = nullToValue(nulledFrame); + const fields: Field[] = []; - for (let field of nullToValue(nulledFrame).fields) { + for (let field of frame.fields) { if (field.config.custom?.hideFrom?.viz) { continue; } @@ -496,6 +545,7 @@ export function prepareTimelineFields( }, }, }; + changed = true; fields.push(field); break; default: @@ -506,11 +556,11 @@ export function prepareTimelineFields( hasTimeseries = true; if (changed) { frames.push({ - ...maybeSortedFrame, + ...frame, fields, }); } else { - frames.push(maybeSortedFrame); + frames.push(frame); } } } @@ -521,6 +571,7 @@ export function prepareTimelineFields( if (!frames.length) { return { warn: 'No graphable fields' }; } + return { frames }; } From 7a741a31bd6b9ef57e700ebd26555973fd41c5c0 Mon Sep 17 00:00:00 2001 From: Sonia Aguilar <33540275+soniaAguilarPeiron@users.noreply.github.com> Date: Mon, 11 Mar 2024 08:47:22 +0100 Subject: [PATCH 0478/1406] Alerting: Track when switching from simplified routing to policies routing or vice versa (#83108) Track when switching from simplified routing to policies routing or vice versa --- .../features/alerting/unified/Analytics.ts | 7 ++++++ .../alerting/unified/state/actions.ts | 23 ++++++++++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/public/app/features/alerting/unified/Analytics.ts b/public/app/features/alerting/unified/Analytics.ts index 81f0c39773..ef58a579ec 100644 --- a/public/app/features/alerting/unified/Analytics.ts +++ b/public/app/features/alerting/unified/Analytics.ts @@ -225,6 +225,13 @@ export function trackRulesSearchComponentInteraction(filter: keyof RulesFilter) export function trackRulesListViewChange(payload: { view: string }) { reportInteraction('grafana_alerting_rules_list_mode', { ...payload }); } +export function trackSwitchToSimplifiedRouting() { + reportInteraction('grafana_alerting_switch_to_simplified_routing'); +} + +export function trackSwitchToPoliciesRouting() { + reportInteraction('grafana_alerting_switch_to_policies_routing'); +} export type AlertRuleTrackingProps = { user_id: number; diff --git a/public/app/features/alerting/unified/state/actions.ts b/public/app/features/alerting/unified/state/actions.ts index 5d743ce726..377877f84a 100644 --- a/public/app/features/alerting/unified/state/actions.ts +++ b/public/app/features/alerting/unified/state/actions.ts @@ -37,6 +37,8 @@ import { backendSrv } from '../../../../core/services/backend_srv'; import { logInfo, LogMessages, + trackSwitchToPoliciesRouting, + trackSwitchToSimplifiedRouting, withPerformanceLogging, withPromRulesMetadataLogging, withRulerRulesMetadataLogging, @@ -79,7 +81,7 @@ import { makeAMLink } from '../utils/misc'; import { AsyncRequestMapSlice, withAppEvents, withSerializedError } from '../utils/redux'; import * as ruleId from '../utils/rule-id'; import { getRulerClient } from '../utils/rulerClient'; -import { getAlertInfo, isRulerNotSupportedResponse } from '../utils/rules'; +import { getAlertInfo, isGrafanaRulerRule, isRulerNotSupportedResponse } from '../utils/rules'; import { safeParseDurationstr } from '../utils/time'; function getDataSourceConfig(getState: () => unknown, rulesSourceName: string) { @@ -452,6 +454,7 @@ export const saveRuleFormAction = createAsyncThunk( const rulerConfig = getDataSourceRulerConfig(thunkAPI.getState, GRAFANA_RULES_SOURCE_NAME); const rulerClient = getRulerClient(rulerConfig); identifier = await rulerClient.saveGrafanaRule(values, evaluateEvery, existing); + reportSwitchingRoutingType(values, existing); await thunkAPI.dispatch(fetchRulerRulesAction({ rulesSourceName: GRAFANA_RULES_SOURCE_NAME })); } else { throw new Error('Unexpected rule form type'); @@ -486,6 +489,24 @@ export const saveRuleFormAction = createAsyncThunk( ) ); +function reportSwitchingRoutingType(values: RuleFormValues, existingRule: RuleWithLocation | undefined) { + // track if the user switched from simplified routing to policies routing or vice versa + if (isGrafanaRulerRule(existingRule?.rule)) { + const ga = existingRule?.rule.grafana_alert; + const existingWasUsingSimplifiedRouting = Boolean(ga?.notification_settings?.receiver); + const newValuesUsesSimplifiedRouting = values.manualRouting; + const shouldTrackSwitchToSimplifiedRouting = !existingWasUsingSimplifiedRouting && newValuesUsesSimplifiedRouting; + const shouldTrackSwitchToPoliciesRouting = existingWasUsingSimplifiedRouting && !newValuesUsesSimplifiedRouting; + + if (shouldTrackSwitchToSimplifiedRouting) { + trackSwitchToSimplifiedRouting(); + } + if (shouldTrackSwitchToPoliciesRouting) { + trackSwitchToPoliciesRouting(); + } + } +} + export const fetchGrafanaNotifiersAction = createAsyncThunk( 'unifiedalerting/fetchGrafanaNotifiers', (): Promise => withSerializedError(fetchNotifiers()) From 0e7c0d25fe7826d017fb68356d147b2188e24a05 Mon Sep 17 00:00:00 2001 From: Sonia Aguilar <33540275+soniaAguilarPeiron@users.noreply.github.com> Date: Mon, 11 Mar 2024 08:47:56 +0100 Subject: [PATCH 0479/1406] Alerting: Add test for creating an alert rule with simplified routing. (#80610) * Add test for creating an alert rule with simplified routing * Fix mocking folders after merging from main (folder uid change) --- .../SimplifiedRuleEditor.test.tsx | 433 ++++++++++++++++++ .../contactPoint/ContactPointSelector.tsx | 2 +- public/test/helpers/alertingRuleEditor.tsx | 7 +- 3 files changed, 440 insertions(+), 2 deletions(-) create mode 100644 public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/SimplifiedRuleEditor.test.tsx diff --git a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/SimplifiedRuleEditor.test.tsx b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/SimplifiedRuleEditor.test.tsx new file mode 100644 index 0000000000..fc6db8b26b --- /dev/null +++ b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/SimplifiedRuleEditor.test.tsx @@ -0,0 +1,433 @@ +import { render, screen, waitFor, waitForElementToBeRemoved } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; +import { Route } from 'react-router-dom'; +import { TestProvider } from 'test/helpers/TestProvider'; +import { ui } from 'test/helpers/alertingRuleEditor'; +import { clickSelectOption } from 'test/helpers/selectOptionInTest'; +import { byRole } from 'testing-library-selector'; + +import { config, locationService, setDataSourceSrv } from '@grafana/runtime'; +import { contextSrv } from 'app/core/services/context_srv'; +import RuleEditor from 'app/features/alerting/unified/RuleEditor'; +import { discoverFeatures } from 'app/features/alerting/unified/api/buildInfo'; +import { + fetchRulerRules, + fetchRulerRulesGroup, + fetchRulerRulesNamespace, + setRulerRuleGroup, +} from 'app/features/alerting/unified/api/ruler'; +import * as useContactPoints from 'app/features/alerting/unified/components/contact-points/useContactPoints'; +import * as dsByPermission from 'app/features/alerting/unified/hooks/useAlertManagerSources'; +import { MockDataSourceSrv, grantUserPermissions, mockDataSource } from 'app/features/alerting/unified/mocks'; +import { AlertmanagerProvider } from 'app/features/alerting/unified/state/AlertmanagerContext'; +import { fetchRulerRulesIfNotFetchedYet } from 'app/features/alerting/unified/state/actions'; +import * as utils_config from 'app/features/alerting/unified/utils/config'; +import { + AlertManagerDataSource, + DataSourceType, + GRAFANA_DATASOURCE_NAME, + GRAFANA_RULES_SOURCE_NAME, + getAlertManagerDataSourcesByPermission, + useGetAlertManagerDataSourcesByPermissionAndConfig, +} from 'app/features/alerting/unified/utils/datasource'; +import { getDefaultQueries } from 'app/features/alerting/unified/utils/rule-form'; +import { searchFolders } from 'app/features/manage-dashboards/state/actions'; +import { DashboardSearchHit, DashboardSearchItemType } from 'app/features/search/types'; +import { AccessControlAction } from 'app/types'; +import { GrafanaAlertStateDecision, PromApplication } from 'app/types/unified-alerting-dto'; + +import { RECEIVER_META_KEY } from '../../../contact-points/useContactPoints'; +import { ContactPointWithMetadata } from '../../../contact-points/utils'; +import { ExpressionEditorProps } from '../../ExpressionEditor'; + +jest.mock('app/features/alerting/unified/components/rule-editor/ExpressionEditor', () => ({ + // eslint-disable-next-line react/display-name + ExpressionEditor: ({ value, onChange }: ExpressionEditorProps) => ( + onChange(e.target.value)} /> + ), +})); + +jest.mock('app/features/alerting/unified/api/buildInfo'); +jest.mock('app/features/alerting/unified/api/ruler'); +jest.mock('app/features/manage-dashboards/state/actions'); + +jest.mock('app/core/components/AppChrome/AppChromeUpdate', () => ({ + AppChromeUpdate: ({ actions }: { actions: React.ReactNode }) =>
{actions}
, +})); + +// there's no angular scope in test and things go terribly wrong when trying to render the query editor row. +// lets just skip it +jest.mock('app/features/query/components/QueryEditorRow', () => ({ + // eslint-disable-next-line react/display-name + QueryEditorRow: () =>

hi

, +})); + +// simplified routing mocks +const grafanaAlertManagerDataSource: AlertManagerDataSource = { + name: GRAFANA_RULES_SOURCE_NAME, + imgUrl: 'public/img/grafana_icon.svg', + hasConfigurationAPI: true, +}; +jest.mock('app/features/alerting/unified/utils/datasource', () => { + return { + ...jest.requireActual('app/features/alerting/unified/utils/datasource'), + getAlertManagerDataSourcesByPermission: jest.fn(), + useGetAlertManagerDataSourcesByPermissionAndConfig: jest.fn(), + getAlertmanagerDataSourceByName: jest.fn(), + }; +}); + +const user = userEvent.setup(); + +jest.spyOn(utils_config, 'getAllDataSources'); +jest.spyOn(dsByPermission, 'useAlertManagersByPermission'); +jest.spyOn(useContactPoints, 'useContactPointsWithStatus'); + +jest.setTimeout(60 * 1000); + +const mocks = { + getAllDataSources: jest.mocked(utils_config.getAllDataSources), + searchFolders: jest.mocked(searchFolders), + useContactPointsWithStatus: jest.mocked(useContactPoints.useContactPointsWithStatus), + useGetAlertManagerDataSourcesByPermissionAndConfig: jest.mocked(useGetAlertManagerDataSourcesByPermissionAndConfig), + getAlertManagerDataSourcesByPermission: jest.mocked(getAlertManagerDataSourcesByPermission), + api: { + discoverFeatures: jest.mocked(discoverFeatures), + fetchRulerRulesGroup: jest.mocked(fetchRulerRulesGroup), + setRulerRuleGroup: jest.mocked(setRulerRuleGroup), + fetchRulerRulesNamespace: jest.mocked(fetchRulerRulesNamespace), + fetchRulerRules: jest.mocked(fetchRulerRules), + fetchRulerRulesIfNotFetchedYet: jest.mocked(fetchRulerRulesIfNotFetchedYet), + }, +}; + +describe('Can create a new grafana managed alert unsing simplified routing', () => { + beforeEach(() => { + jest.clearAllMocks(); + contextSrv.isEditor = true; + contextSrv.hasEditPermissionInFolders = true; + grantUserPermissions([ + AccessControlAction.AlertingRuleRead, + AccessControlAction.AlertingRuleUpdate, + AccessControlAction.AlertingRuleDelete, + AccessControlAction.AlertingRuleCreate, + AccessControlAction.DataSourcesRead, + AccessControlAction.DataSourcesWrite, + AccessControlAction.DataSourcesCreate, + AccessControlAction.FoldersWrite, + AccessControlAction.FoldersRead, + AccessControlAction.AlertingRuleExternalRead, + AccessControlAction.AlertingRuleExternalWrite, + AccessControlAction.AlertingNotificationsRead, + AccessControlAction.AlertingNotificationsWrite, + ]); + mocks.getAlertManagerDataSourcesByPermission.mockReturnValue({ + availableInternalDataSources: [grafanaAlertManagerDataSource], + availableExternalDataSources: [], + }); + + mocks.useGetAlertManagerDataSourcesByPermissionAndConfig.mockReturnValue([grafanaAlertManagerDataSource]); + + jest.mocked(dsByPermission.useAlertManagersByPermission).mockReturnValue({ + availableInternalDataSources: [grafanaAlertManagerDataSource], + availableExternalDataSources: [], + }); + }); + + const dataSources = { + default: mockDataSource( + { + type: 'prometheus', + name: 'Prom', + isDefault: true, + }, + { alerting: false } + ), + am: mockDataSource({ + name: 'Alertmanager', + type: DataSourceType.Alertmanager, + }), + }; + + it('cannot create new grafana managed alert when using simplified routing and not selecting a contact point', async () => { + // no contact points found + mocks.useContactPointsWithStatus.mockReturnValue({ + contactPoints: [], + isLoading: false, + error: undefined, + refetchReceivers: jest.fn(), + }); + + setDataSourceSrv(new MockDataSourceSrv(dataSources)); + mocks.getAllDataSources.mockReturnValue(Object.values(dataSources)); + mocks.api.setRulerRuleGroup.mockResolvedValue(); + mocks.api.fetchRulerRulesNamespace.mockResolvedValue([]); + mocks.api.fetchRulerRulesGroup.mockResolvedValue({ + name: 'group2', + rules: [], + }); + mocks.api.fetchRulerRules.mockResolvedValue({ + 'Folder A': [ + { + interval: '1m', + name: 'group1', + rules: [ + { + annotations: { description: 'some description', summary: 'some summary' }, + labels: { severity: 'warn', team: 'the a-team' }, + for: '5m', + grafana_alert: { + uid: '23', + namespace_uid: 'abcd', + condition: 'B', + data: getDefaultQueries(), + exec_err_state: GrafanaAlertStateDecision.Error, + no_data_state: GrafanaAlertStateDecision.NoData, + title: 'my great new rule', + }, + }, + ], + }, + ], + namespace2: [ + { + interval: '1m', + name: 'group1', + rules: [ + { + annotations: { description: 'some description', summary: 'some summary' }, + labels: { severity: 'warn', team: 'the a-team' }, + for: '5m', + grafana_alert: { + uid: '23', + namespace_uid: 'b', + condition: 'B', + data: getDefaultQueries(), + exec_err_state: GrafanaAlertStateDecision.Error, + no_data_state: GrafanaAlertStateDecision.NoData, + title: 'my great new rule', + }, + }, + ], + }, + ], + }); + mocks.searchFolders.mockResolvedValue([ + { + title: 'Folder A', + uid: 'abcd', + id: 1, + type: DashboardSearchItemType.DashDB, + }, + { + title: 'Folder B', + id: 2, + }, + { + title: 'Folder / with slash', + id: 2, + uid: 'b', + type: DashboardSearchItemType.DashDB, + }, + ] as DashboardSearchHit[]); + + mocks.api.discoverFeatures.mockResolvedValue({ + application: PromApplication.Prometheus, + features: { + rulerApiEnabled: false, + }, + }); + config.featureToggles.alertingSimplifiedRouting = true; + renderSimplifiedRuleEditor(); + await waitForElementToBeRemoved(screen.getAllByTestId('Spinner')); + + await user.type(await ui.inputs.name.find(), 'my great new rule'); + + const folderInput = await ui.inputs.folder.find(); + await clickSelectOption(folderInput, 'Folder A'); + const groupInput = await ui.inputs.group.find(); + await user.click(byRole('combobox').get(groupInput)); + await clickSelectOption(groupInput, 'group1'); + //select contact point routing + await user.click(ui.inputs.simplifiedRouting.contactPointRouting.get()); + // do not select a contact point + // save and check that call to backend was not made + await user.click(ui.buttons.saveAndExit.get()); + await waitFor(() => { + expect(screen.getByText('Contact point is required.')).toBeInTheDocument(); + expect(mocks.api.setRulerRuleGroup).not.toHaveBeenCalled(); + }); + }); + it('can create new grafana managed alert when using simplified routing and selecting a contact point', async () => { + const contactPointsAvailable: ContactPointWithMetadata[] = [ + { + name: 'contact_point1', + grafana_managed_receiver_configs: [ + { + name: 'contact_point1', + type: 'email', + disableResolveMessage: false, + [RECEIVER_META_KEY]: { + name: 'contact_point1', + description: 'contact_point1 description', + }, + settings: {}, + }, + ], + numberOfPolicies: 0, + }, + ]; + mocks.useContactPointsWithStatus.mockReturnValue({ + contactPoints: contactPointsAvailable, + isLoading: false, + error: undefined, + refetchReceivers: jest.fn(), + }); + + setDataSourceSrv(new MockDataSourceSrv(dataSources)); + mocks.getAllDataSources.mockReturnValue(Object.values(dataSources)); + mocks.api.setRulerRuleGroup.mockResolvedValue(); + mocks.api.fetchRulerRulesNamespace.mockResolvedValue([]); + mocks.api.fetchRulerRulesGroup.mockResolvedValue({ + name: 'group2', + rules: [], + }); + mocks.api.fetchRulerRules.mockResolvedValue({ + 'Folder A': [ + { + interval: '1m', + name: 'group1', + rules: [ + { + annotations: { description: 'some description', summary: 'some summary' }, + labels: { severity: 'warn', team: 'the a-team' }, + for: '5m', + grafana_alert: { + uid: '23', + namespace_uid: 'abcd', + condition: 'B', + data: getDefaultQueries(), + exec_err_state: GrafanaAlertStateDecision.Error, + no_data_state: GrafanaAlertStateDecision.NoData, + title: 'my great new rule', + }, + }, + ], + }, + ], + namespace2: [ + { + interval: '1m', + name: 'group1', + rules: [ + { + annotations: { description: 'some description', summary: 'some summary' }, + labels: { severity: 'warn', team: 'the a-team' }, + for: '5m', + grafana_alert: { + uid: '23', + namespace_uid: 'b', + condition: 'B', + data: getDefaultQueries(), + exec_err_state: GrafanaAlertStateDecision.Error, + no_data_state: GrafanaAlertStateDecision.NoData, + title: 'my great new rule', + }, + }, + ], + }, + ], + }); + mocks.searchFolders.mockResolvedValue([ + { + title: 'Folder A', + uid: 'abcd', + id: 1, + type: DashboardSearchItemType.DashDB, + }, + { + title: 'Folder B', + id: 2, + uid: 'b', + type: DashboardSearchItemType.DashDB, + }, + { + title: 'Folder / with slash', + uid: 'c', + id: 2, + type: DashboardSearchItemType.DashDB, + }, + ] as DashboardSearchHit[]); + + mocks.api.discoverFeatures.mockResolvedValue({ + application: PromApplication.Prometheus, + features: { + rulerApiEnabled: false, + }, + }); + config.featureToggles.alertingSimplifiedRouting = true; + renderSimplifiedRuleEditor(); + await waitForElementToBeRemoved(screen.getAllByTestId('Spinner')); + + await user.type(await ui.inputs.name.find(), 'my great new rule'); + + const folderInput = await ui.inputs.folder.find(); + await clickSelectOption(folderInput, 'Folder A'); + const groupInput = await ui.inputs.group.find(); + await user.click(byRole('combobox').get(groupInput)); + await clickSelectOption(groupInput, 'group1'); + //select contact point routing + await user.click(ui.inputs.simplifiedRouting.contactPointRouting.get()); + const contactPointInput = await ui.inputs.simplifiedRouting.contactPoint.find(); + await user.click(byRole('combobox').get(contactPointInput)); + await clickSelectOption(contactPointInput, 'contact_point1'); + + // save and check what was sent to backend + await user.click(ui.buttons.saveAndExit.get()); + await waitFor(() => expect(mocks.api.setRulerRuleGroup).toHaveBeenCalled()); + expect(mocks.api.setRulerRuleGroup).toHaveBeenCalledWith( + { dataSourceName: GRAFANA_RULES_SOURCE_NAME, apiVersion: 'legacy' }, + 'abcd', + { + interval: '1m', + name: 'group1', + rules: [ + { + annotations: {}, + labels: {}, + for: '5m', + grafana_alert: { + condition: 'B', + data: getDefaultQueries(), + exec_err_state: GrafanaAlertStateDecision.Error, + is_paused: false, + no_data_state: 'NoData', + title: 'my great new rule', + notification_settings: { + group_by: undefined, + group_interval: undefined, + group_wait: undefined, + mute_timings: undefined, + receiver: 'contact_point1', + repeat_interval: undefined, + }, + }, + }, + ], + } + ); + }); +}); + +function renderSimplifiedRuleEditor() { + locationService.push(`/alerting/new/alerting`); + + return render( + + + + + + ); +} diff --git a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/contactPoint/ContactPointSelector.tsx b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/contactPoint/ContactPointSelector.tsx index 1e7d178496..aeee48bcac 100644 --- a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/contactPoint/ContactPointSelector.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/contactPoint/ContactPointSelector.tsx @@ -78,7 +78,7 @@ export function ContactPointSelector({ return ( - + ( <> diff --git a/public/test/helpers/alertingRuleEditor.tsx b/public/test/helpers/alertingRuleEditor.tsx index 2ef5494a9d..feca69b1f6 100644 --- a/public/test/helpers/alertingRuleEditor.tsx +++ b/public/test/helpers/alertingRuleEditor.tsx @@ -1,7 +1,7 @@ import { render } from '@testing-library/react'; import React from 'react'; import { Route } from 'react-router-dom'; -import { byRole, byTestId } from 'testing-library-selector'; +import { byRole, byTestId, byText } from 'testing-library-selector'; import { selectors } from '@grafana/e2e-selectors'; import { locationService } from '@grafana/runtime'; @@ -23,6 +23,11 @@ export const ui = { labelKey: (idx: number) => byTestId(`label-key-${idx}`), labelValue: (idx: number) => byTestId(`label-value-${idx}`), expr: byTestId('expr'), + simplifiedRouting: { + contactPointRouting: byRole('radio', { name: /select contact point/i }), + contactPoint: byTestId('contact-point-picker'), + routingOptions: byText(/muting, grouping and timings \(optional\)/i), + }, }, buttons: { saveAndExit: byRole('button', { name: 'Save rule and exit' }), From ea84a66ff4d9de5b245087d528a8cd044b033a43 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 8 Mar 2024 13:38:12 +0000 Subject: [PATCH 0480/1406] Update dependency @emotion/react to v11.11.4 --- package.json | 2 +- packages/grafana-ui/package.json | 2 +- yarn.lock | 40 ++++++++++++-------------------- 3 files changed, 17 insertions(+), 27 deletions(-) diff --git a/package.json b/package.json index 551eaac7cc..9a1d1dba3a 100644 --- a/package.json +++ b/package.json @@ -230,7 +230,7 @@ "dependencies": { "@daybrush/utils": "1.13.0", "@emotion/css": "11.11.2", - "@emotion/react": "11.11.3", + "@emotion/react": "11.11.4", "@fingerprintjs/fingerprintjs": "^3.4.2", "@floating-ui/react": "0.26.9", "@glideapps/glide-data-grid": "^6.0.0", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index b9bbc0c10c..d9e6f8f920 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -48,7 +48,7 @@ ], "dependencies": { "@emotion/css": "11.11.2", - "@emotion/react": "11.11.3", + "@emotion/react": "11.11.4", "@floating-ui/react": "0.26.9", "@grafana/data": "11.0.0-pre", "@grafana/e2e-selectors": "11.0.0-pre", diff --git a/yarn.lock b/yarn.lock index 5c7592eaba..22df05e441 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2168,9 +2168,9 @@ __metadata: languageName: node linkType: hard -"@emotion/react@npm:11.11.3, @emotion/react@npm:^11.8.1": - version: 11.11.3 - resolution: "@emotion/react@npm:11.11.3" +"@emotion/react@npm:11.11.4, @emotion/react@npm:^11.8.1": + version: 11.11.4 + resolution: "@emotion/react@npm:11.11.4" dependencies: "@babel/runtime": "npm:^7.18.3" "@emotion/babel-plugin": "npm:^11.11.0" @@ -2185,7 +2185,7 @@ __metadata: peerDependenciesMeta: "@types/react": optional: true - checksum: 10/f7b98557b7d5236296dda48c2fc8a6cde4af7399758496e9f710f85a80c7d66fee1830966caabd7b237601bfdaca4e1add8c681d1ae4cc3d497fe88958d541c4 + checksum: 10/e7da3a1ddc1d72a4179010bdfd17423c13b1a77bf83a8b18271e919fd382d08c62dc2313ed5347acfd1ef85bb1bae8932597647a986e8a1ea1462552716cd495 languageName: node linkType: hard @@ -4162,7 +4162,7 @@ __metadata: dependencies: "@babel/core": "npm:7.23.9" "@emotion/css": "npm:11.11.2" - "@emotion/react": "npm:11.11.3" + "@emotion/react": "npm:11.11.4" "@floating-ui/react": "npm:0.26.9" "@grafana/data": "npm:11.0.0-pre" "@grafana/e2e-selectors": "npm:11.0.0-pre" @@ -9203,23 +9203,23 @@ __metadata: languageName: node linkType: hard -"@types/eslint@npm:*, @types/eslint@npm:8.56.3, @types/eslint@npm:^8.37.0": - version: 8.56.3 - resolution: "@types/eslint@npm:8.56.3" +"@types/eslint@npm:*, @types/eslint@npm:^8.37.0, @types/eslint@npm:^8.4.10": + version: 8.56.4 + resolution: "@types/eslint@npm:8.56.4" dependencies: "@types/estree": "npm:*" "@types/json-schema": "npm:*" - checksum: 10/b5a006c24b5d3a2dba5acc12f21f96c960836beb08544cfedbbbd5b7770b6c951b41204d676b73d7d9065bef3435e5b4cb3796c57f66df21c12fd86018993a16 + checksum: 10/bb8018f0c27839dd0b8c515ac4e6fac39500c36ba20007a6ecca2fe5e5f81cbecca2be8f6f649bdafd5556b8c6d5285d8506ae61cc8570f71fd4e6b07042f641 languageName: node linkType: hard -"@types/eslint@npm:^8.4.10": - version: 8.56.4 - resolution: "@types/eslint@npm:8.56.4" +"@types/eslint@npm:8.56.3": + version: 8.56.3 + resolution: "@types/eslint@npm:8.56.3" dependencies: "@types/estree": "npm:*" "@types/json-schema": "npm:*" - checksum: 10/bb8018f0c27839dd0b8c515ac4e6fac39500c36ba20007a6ecca2fe5e5f81cbecca2be8f6f649bdafd5556b8c6d5285d8506ae61cc8570f71fd4e6b07042f641 + checksum: 10/b5a006c24b5d3a2dba5acc12f21f96c960836beb08544cfedbbbd5b7770b6c951b41204d676b73d7d9065bef3435e5b4cb3796c57f66df21c12fd86018993a16 languageName: node linkType: hard @@ -15657,7 +15657,7 @@ __metadata: languageName: node linkType: hard -"enhanced-resolve@npm:^5.10.0": +"enhanced-resolve@npm:^5.10.0, enhanced-resolve@npm:^5.15.0": version: 5.15.1 resolution: "enhanced-resolve@npm:5.15.1" dependencies: @@ -15667,16 +15667,6 @@ __metadata: languageName: node linkType: hard -"enhanced-resolve@npm:^5.15.0": - version: 5.15.0 - resolution: "enhanced-resolve@npm:5.15.0" - dependencies: - graceful-fs: "npm:^4.2.4" - tapable: "npm:^2.2.0" - checksum: 10/180c3f2706f9117bf4dc7982e1df811dad83a8db075723f299245ef4488e0cad7e96859c5f0e410682d28a4ecd4da021ec7d06265f7e4eb6eed30c69ca5f7d3e - languageName: node - linkType: hard - "enquirer@npm:^2.3.6, enquirer@npm:~2.3.6": version: 2.3.6 resolution: "enquirer@npm:2.3.6" @@ -18322,7 +18312,7 @@ __metadata: "@daybrush/utils": "npm:1.13.0" "@emotion/css": "npm:11.11.2" "@emotion/eslint-plugin": "npm:11.11.0" - "@emotion/react": "npm:11.11.3" + "@emotion/react": "npm:11.11.4" "@fingerprintjs/fingerprintjs": "npm:^3.4.2" "@floating-ui/react": "npm:0.26.9" "@glideapps/glide-data-grid": "npm:^6.0.0" From 07676ab8a040971bc2f054c52a74f3c4b8680f5d Mon Sep 17 00:00:00 2001 From: Andreas Christou Date: Mon, 11 Mar 2024 08:57:42 +0000 Subject: [PATCH 0481/1406] Prometheus: Add missing Azure setting (#84094) --- pkg/setting/setting_azure.go | 4 ++++ pkg/setting/setting_azure_test.go | 21 +++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/pkg/setting/setting_azure.go b/pkg/setting/setting_azure.go index 54e8c5e2a5..77f3e55b08 100644 --- a/pkg/setting/setting_azure.go +++ b/pkg/setting/setting_azure.go @@ -9,6 +9,10 @@ func (cfg *Cfg) readAzureSettings() { azureSettings := &azsettings.AzureSettings{} azureSection := cfg.Raw.Section("azure") + authSection := cfg.Raw.Section("auth") + + // This setting is specific to Prometheus + azureSettings.AzureAuthEnabled = authSection.Key("azure_auth_enabled").MustBool(false) // Cloud cloudName := azureSection.Key("cloud").MustString(azsettings.AzurePublic) diff --git a/pkg/setting/setting_azure_test.go b/pkg/setting/setting_azure_test.go index 2f75fd5eb3..a122e821e1 100644 --- a/pkg/setting/setting_azure_test.go +++ b/pkg/setting/setting_azure_test.go @@ -64,6 +64,27 @@ func TestAzureSettings(t *testing.T) { } }) + t.Run("prometheus", func(t *testing.T) { + t.Run("should enable azure auth", func(t *testing.T) { + cfg := NewCfg() + + authSection, err := cfg.Raw.NewSection("auth") + require.NoError(t, err) + _, err = authSection.NewKey("azure_auth_enabled", "true") + require.NoError(t, err) + + cfg.readAzureSettings() + require.NotNil(t, cfg.Azure.AzureAuthEnabled) + assert.True(t, cfg.Azure.AzureAuthEnabled) + }) + t.Run("should default to disabled", func(t *testing.T) { + cfg := NewCfg() + + cfg.readAzureSettings() + require.NotNil(t, cfg.Azure.AzureAuthEnabled) + assert.False(t, cfg.Azure.AzureAuthEnabled) + }) + }) t.Run("User Identity", func(t *testing.T) { t.Run("should be disabled by default", func(t *testing.T) { cfg := NewCfg() From 4d7220dbdf751a3dca5e72048233f1aeac324f3e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 08:56:44 +0000 Subject: [PATCH 0482/1406] Update dependency @swc/core to v1.4.6 --- package.json | 2 +- packages/grafana-prometheus/package.json | 2 +- yarn.lock | 94 ++++++++++++------------ 3 files changed, 49 insertions(+), 49 deletions(-) diff --git a/package.json b/package.json index 9a1d1dba3a..6347ad1587 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,7 @@ "@react-types/overlays": "3.8.5", "@react-types/shared": "3.22.1", "@rtsao/plugin-proposal-class-properties": "7.0.1-patch.1", - "@swc/core": "1.4.2", + "@swc/core": "1.4.6", "@swc/helpers": "0.5.6", "@testing-library/dom": "9.3.4", "@testing-library/jest-dom": "6.4.2", diff --git a/packages/grafana-prometheus/package.json b/packages/grafana-prometheus/package.json index 28237b8f22..52ede42992 100644 --- a/packages/grafana-prometheus/package.json +++ b/packages/grafana-prometheus/package.json @@ -81,7 +81,7 @@ "@grafana/tsconfig": "^1.3.0-rc1", "@rollup/plugin-image": "3.0.3", "@rollup/plugin-node-resolve": "15.2.3", - "@swc/core": "1.4.2", + "@swc/core": "1.4.6", "@swc/helpers": "0.5.6", "@testing-library/dom": "9.3.4", "@testing-library/jest-dom": "6.4.2", diff --git a/yarn.lock b/yarn.lock index 22df05e441..c42ae493b6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3927,7 +3927,7 @@ __metadata: "@reduxjs/toolkit": "npm:1.9.5" "@rollup/plugin-image": "npm:3.0.3" "@rollup/plugin-node-resolve": "npm:15.2.3" - "@swc/core": "npm:1.4.2" + "@swc/core": "npm:1.4.6" "@swc/helpers": "npm:0.5.6" "@testing-library/dom": "npm:9.3.4" "@testing-library/jest-dom": "npm:6.4.2" @@ -8358,90 +8358,90 @@ __metadata: languageName: node linkType: hard -"@swc/core-darwin-arm64@npm:1.4.2": - version: 1.4.2 - resolution: "@swc/core-darwin-arm64@npm:1.4.2" +"@swc/core-darwin-arm64@npm:1.4.6": + version: 1.4.6 + resolution: "@swc/core-darwin-arm64@npm:1.4.6" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@swc/core-darwin-x64@npm:1.4.2": - version: 1.4.2 - resolution: "@swc/core-darwin-x64@npm:1.4.2" +"@swc/core-darwin-x64@npm:1.4.6": + version: 1.4.6 + resolution: "@swc/core-darwin-x64@npm:1.4.6" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@swc/core-linux-arm-gnueabihf@npm:1.4.2": - version: 1.4.2 - resolution: "@swc/core-linux-arm-gnueabihf@npm:1.4.2" +"@swc/core-linux-arm-gnueabihf@npm:1.4.6": + version: 1.4.6 + resolution: "@swc/core-linux-arm-gnueabihf@npm:1.4.6" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@swc/core-linux-arm64-gnu@npm:1.4.2": - version: 1.4.2 - resolution: "@swc/core-linux-arm64-gnu@npm:1.4.2" +"@swc/core-linux-arm64-gnu@npm:1.4.6": + version: 1.4.6 + resolution: "@swc/core-linux-arm64-gnu@npm:1.4.6" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@swc/core-linux-arm64-musl@npm:1.4.2": - version: 1.4.2 - resolution: "@swc/core-linux-arm64-musl@npm:1.4.2" +"@swc/core-linux-arm64-musl@npm:1.4.6": + version: 1.4.6 + resolution: "@swc/core-linux-arm64-musl@npm:1.4.6" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@swc/core-linux-x64-gnu@npm:1.4.2": - version: 1.4.2 - resolution: "@swc/core-linux-x64-gnu@npm:1.4.2" +"@swc/core-linux-x64-gnu@npm:1.4.6": + version: 1.4.6 + resolution: "@swc/core-linux-x64-gnu@npm:1.4.6" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@swc/core-linux-x64-musl@npm:1.4.2": - version: 1.4.2 - resolution: "@swc/core-linux-x64-musl@npm:1.4.2" +"@swc/core-linux-x64-musl@npm:1.4.6": + version: 1.4.6 + resolution: "@swc/core-linux-x64-musl@npm:1.4.6" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@swc/core-win32-arm64-msvc@npm:1.4.2": - version: 1.4.2 - resolution: "@swc/core-win32-arm64-msvc@npm:1.4.2" +"@swc/core-win32-arm64-msvc@npm:1.4.6": + version: 1.4.6 + resolution: "@swc/core-win32-arm64-msvc@npm:1.4.6" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@swc/core-win32-ia32-msvc@npm:1.4.2": - version: 1.4.2 - resolution: "@swc/core-win32-ia32-msvc@npm:1.4.2" +"@swc/core-win32-ia32-msvc@npm:1.4.6": + version: 1.4.6 + resolution: "@swc/core-win32-ia32-msvc@npm:1.4.6" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@swc/core-win32-x64-msvc@npm:1.4.2": - version: 1.4.2 - resolution: "@swc/core-win32-x64-msvc@npm:1.4.2" +"@swc/core-win32-x64-msvc@npm:1.4.6": + version: 1.4.6 + resolution: "@swc/core-win32-x64-msvc@npm:1.4.6" conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"@swc/core@npm:1.4.2, @swc/core@npm:^1.3.49": - version: 1.4.2 - resolution: "@swc/core@npm:1.4.2" - dependencies: - "@swc/core-darwin-arm64": "npm:1.4.2" - "@swc/core-darwin-x64": "npm:1.4.2" - "@swc/core-linux-arm-gnueabihf": "npm:1.4.2" - "@swc/core-linux-arm64-gnu": "npm:1.4.2" - "@swc/core-linux-arm64-musl": "npm:1.4.2" - "@swc/core-linux-x64-gnu": "npm:1.4.2" - "@swc/core-linux-x64-musl": "npm:1.4.2" - "@swc/core-win32-arm64-msvc": "npm:1.4.2" - "@swc/core-win32-ia32-msvc": "npm:1.4.2" - "@swc/core-win32-x64-msvc": "npm:1.4.2" +"@swc/core@npm:1.4.6, @swc/core@npm:^1.3.49": + version: 1.4.6 + resolution: "@swc/core@npm:1.4.6" + dependencies: + "@swc/core-darwin-arm64": "npm:1.4.6" + "@swc/core-darwin-x64": "npm:1.4.6" + "@swc/core-linux-arm-gnueabihf": "npm:1.4.6" + "@swc/core-linux-arm64-gnu": "npm:1.4.6" + "@swc/core-linux-arm64-musl": "npm:1.4.6" + "@swc/core-linux-x64-gnu": "npm:1.4.6" + "@swc/core-linux-x64-musl": "npm:1.4.6" + "@swc/core-win32-arm64-msvc": "npm:1.4.6" + "@swc/core-win32-ia32-msvc": "npm:1.4.6" + "@swc/core-win32-x64-msvc": "npm:1.4.6" "@swc/counter": "npm:^0.1.2" "@swc/types": "npm:^0.1.5" peerDependencies: @@ -8470,7 +8470,7 @@ __metadata: peerDependenciesMeta: "@swc/helpers": optional: true - checksum: 10/750c09e35fb14317b1ff7f8f528eebd732988ce34736c3404805e70ff44e08a19ec6d0c16b9468fab602b596eb39cc6d2771f0483a62efd614768e046323b5f4 + checksum: 10/acf826107e5c72d2cf436fb80b4799b5fd2981e4493bcf573cdcc2c6abd02b29942df8f6b15f6c67a1d39bb709ac9e67194879f0dd7d36bdb41782a3ca612841 languageName: node linkType: hard @@ -18374,7 +18374,7 @@ __metadata: "@reduxjs/toolkit": "npm:1.9.5" "@remix-run/router": "npm:^1.5.0" "@rtsao/plugin-proposal-class-properties": "npm:7.0.1-patch.1" - "@swc/core": "npm:1.4.2" + "@swc/core": "npm:1.4.6" "@swc/helpers": "npm:0.5.6" "@testing-library/dom": "npm:9.3.4" "@testing-library/jest-dom": "npm:6.4.2" From 15e358e3b9e59861dcd34f73ef2082a53f931c2b Mon Sep 17 00:00:00 2001 From: Georges Chaudy Date: Mon, 11 Mar 2024 10:23:03 +0100 Subject: [PATCH 0483/1406] k8s: add support for configuring the gRPC server address (#84006) * k8s: add support for configuring the gRPC server address --- pkg/services/apiserver/options/storage.go | 8 ++++++++ pkg/services/apiserver/service.go | 3 +-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/pkg/services/apiserver/options/storage.go b/pkg/services/apiserver/options/storage.go index 55c2933583..dc99d98e9b 100644 --- a/pkg/services/apiserver/options/storage.go +++ b/pkg/services/apiserver/options/storage.go @@ -2,6 +2,7 @@ package options import ( "fmt" + "net" "github.com/spf13/pflag" genericapiserver "k8s.io/apiserver/pkg/server" @@ -21,17 +22,20 @@ const ( type StorageOptions struct { StorageType StorageType DataPath string + Address string } func NewStorageOptions() *StorageOptions { return &StorageOptions{ StorageType: StorageTypeLegacy, + Address: "localhost:10000", } } func (o *StorageOptions) AddFlags(fs *pflag.FlagSet) { fs.StringVar((*string)(&o.StorageType), "grafana-apiserver-storage-type", string(o.StorageType), "Storage type") fs.StringVar(&o.DataPath, "grafana-apiserver-storage-path", o.DataPath, "Storage path for file storage") + fs.StringVar(&o.Address, "grafana-apiserver-storage-address", o.Address, "Remote grpc address endpoint") } func (o *StorageOptions) Validate() []error { @@ -42,6 +46,10 @@ func (o *StorageOptions) Validate() []error { default: errs = append(errs, fmt.Errorf("--grafana-apiserver-storage-type must be one of %s, %s, %s, %s, %s", StorageTypeFile, StorageTypeEtcd, StorageTypeLegacy, StorageTypeUnified, StorageTypeUnifiedGrpc)) } + + if _, _, err := net.SplitHostPort(o.Address); err != nil { + errs = append(errs, fmt.Errorf("--grafana-apiserver-storage-address must be a valid network address: %v", err)) + } return errs } diff --git a/pkg/services/apiserver/service.go b/pkg/services/apiserver/service.go index 7a4bb27ef3..e6d32610db 100644 --- a/pkg/services/apiserver/service.go +++ b/pkg/services/apiserver/service.go @@ -261,8 +261,7 @@ func (s *service) start(ctx context.Context) error { case grafanaapiserveroptions.StorageTypeUnifiedGrpc: // Create a connection to the gRPC server - // TODO: support configuring the gRPC server address - conn, err := grpc.Dial("localhost:10000", grpc.WithTransportCredentials(insecure.NewCredentials())) + conn, err := grpc.Dial(o.StorageOptions.Address, grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { return err } From c5080ca135a186eac7502787f8547b57b8acdbfb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 09:16:39 +0000 Subject: [PATCH 0484/1406] Update dependency @types/eslint to v8.56.5 --- package.json | 2 +- packages/grafana-prometheus/package.json | 2 +- yarn.lock | 22 ++++++---------------- 3 files changed, 8 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index 6347ad1587..4b53cc5c2e 100644 --- a/package.json +++ b/package.json @@ -100,7 +100,7 @@ "@types/d3-scale-chromatic": "3.0.3", "@types/debounce-promise": "3.1.9", "@types/diff": "^5", - "@types/eslint": "8.56.3", + "@types/eslint": "8.56.5", "@types/eslint-scope": "^3.7.7", "@types/file-saver": "2.0.7", "@types/glob": "^8.0.0", diff --git a/packages/grafana-prometheus/package.json b/packages/grafana-prometheus/package.json index 52ede42992..d7a865af3c 100644 --- a/packages/grafana-prometheus/package.json +++ b/packages/grafana-prometheus/package.json @@ -89,7 +89,7 @@ "@testing-library/user-event": "14.5.2", "@types/d3": "7.4.3", "@types/debounce-promise": "3.1.9", - "@types/eslint": "8.56.3", + "@types/eslint": "8.56.5", "@types/jest": "29.5.12", "@types/jquery": "3.5.29", "@types/lodash": "4.14.202", diff --git a/yarn.lock b/yarn.lock index c42ae493b6..4e288c4cde 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3935,7 +3935,7 @@ __metadata: "@testing-library/user-event": "npm:14.5.2" "@types/d3": "npm:7.4.3" "@types/debounce-promise": "npm:3.1.9" - "@types/eslint": "npm:8.56.3" + "@types/eslint": "npm:8.56.5" "@types/jest": "npm:29.5.12" "@types/jquery": "npm:3.5.29" "@types/lodash": "npm:4.14.202" @@ -9203,23 +9203,13 @@ __metadata: languageName: node linkType: hard -"@types/eslint@npm:*, @types/eslint@npm:^8.37.0, @types/eslint@npm:^8.4.10": - version: 8.56.4 - resolution: "@types/eslint@npm:8.56.4" +"@types/eslint@npm:*, @types/eslint@npm:8.56.5, @types/eslint@npm:^8.37.0, @types/eslint@npm:^8.4.10": + version: 8.56.5 + resolution: "@types/eslint@npm:8.56.5" dependencies: "@types/estree": "npm:*" "@types/json-schema": "npm:*" - checksum: 10/bb8018f0c27839dd0b8c515ac4e6fac39500c36ba20007a6ecca2fe5e5f81cbecca2be8f6f649bdafd5556b8c6d5285d8506ae61cc8570f71fd4e6b07042f641 - languageName: node - linkType: hard - -"@types/eslint@npm:8.56.3": - version: 8.56.3 - resolution: "@types/eslint@npm:8.56.3" - dependencies: - "@types/estree": "npm:*" - "@types/json-schema": "npm:*" - checksum: 10/b5a006c24b5d3a2dba5acc12f21f96c960836beb08544cfedbbbd5b7770b6c951b41204d676b73d7d9065bef3435e5b4cb3796c57f66df21c12fd86018993a16 + checksum: 10/548aab6ea34ca14452bf6e9212c76bb22cdf3b725d47e25591c20651af3f47fb62c59c4e80ed8ea3f7d1d7374d907cbba980af910e4c0f0cb29f73b9a6a9226f languageName: node linkType: hard @@ -18390,7 +18380,7 @@ __metadata: "@types/d3-scale-chromatic": "npm:3.0.3" "@types/debounce-promise": "npm:3.1.9" "@types/diff": "npm:^5" - "@types/eslint": "npm:8.56.3" + "@types/eslint": "npm:8.56.5" "@types/eslint-scope": "npm:^3.7.7" "@types/file-saver": "npm:2.0.7" "@types/glob": "npm:^8.0.0" From e8ecbaffc218150a4987ed413e3029f0ef96a13e Mon Sep 17 00:00:00 2001 From: Joey <90795735+joey-grafana@users.noreply.github.com> Date: Mon, 11 Mar 2024 10:26:59 +0000 Subject: [PATCH 0485/1406] UX: Update trace to logs tooltip to improve clarity (#83508) * Update tooltip to improve clarity * Update packages/grafana-o11y-ds-frontend/src/TraceToLogs/TraceToLogsSettings.tsx Co-authored-by: Sven Grossmann --------- Co-authored-by: Sven Grossmann --- .../src/TraceToLogs/TraceToLogsSettings.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grafana-o11y-ds-frontend/src/TraceToLogs/TraceToLogsSettings.tsx b/packages/grafana-o11y-ds-frontend/src/TraceToLogs/TraceToLogsSettings.tsx index 02c6d04ec0..bc290883d6 100644 --- a/packages/grafana-o11y-ds-frontend/src/TraceToLogs/TraceToLogsSettings.tsx +++ b/packages/grafana-o11y-ds-frontend/src/TraceToLogs/TraceToLogsSettings.tsx @@ -229,7 +229,7 @@ function IdFilter(props: IdFilterProps) { label={`Filter by ${props.type} ID`} labelWidth={26} grow - tooltip={`Filters logs by ${props.type} ID`} + tooltip={`Filters logs by ${props.type} ID, where the ${props.type} ID should be part of the log line`} > Date: Mon, 11 Mar 2024 11:33:33 +0100 Subject: [PATCH 0486/1406] Dashboard scenes: Remove panel menu options that are dashboard editing activities when not in edit mode. (#84156) * remove panel menu options that are dasbhoard editing activities when the dashboard is not in edit mode * remove corresponding keybindings when not in edit mode * add keyboard shortcuts but inactivate them when not in edit mode * Add tests; fix tests --- .../scene/PanelMenuBehavior.test.tsx | 51 +++++++-- .../scene/PanelMenuBehavior.tsx | 100 ++++++++++-------- .../scene/keyboardShortcuts.ts | 8 +- 3 files changed, 103 insertions(+), 56 deletions(-) diff --git a/public/app/features/dashboard-scene/scene/PanelMenuBehavior.test.tsx b/public/app/features/dashboard-scene/scene/PanelMenuBehavior.test.tsx index 815ddae7d5..48cc1fd7ea 100644 --- a/public/app/features/dashboard-scene/scene/PanelMenuBehavior.test.tsx +++ b/public/app/features/dashboard-scene/scene/PanelMenuBehavior.test.tsx @@ -70,7 +70,7 @@ describe('panelMenuBehavior', () => { await new Promise((r) => setTimeout(r, 1)); - expect(menu.state.items?.length).toBe(8); + expect(menu.state.items?.length).toBe(6); // verify view panel url keeps url params and adds viewPanel= expect(menu.state.items?.[0].href).toBe('/d/dash-1?from=now-5m&to=now&viewPanel=panel-12'); // verify edit url keeps url time range @@ -119,7 +119,7 @@ describe('panelMenuBehavior', () => { await new Promise((r) => setTimeout(r, 1)); - expect(menu.state.items?.length).toBe(9); + expect(menu.state.items?.length).toBe(7); const extensionsSubMenu = menu.state.items?.find((i) => i.text === 'Extensions')?.subMenu; @@ -158,7 +158,7 @@ describe('panelMenuBehavior', () => { await new Promise((r) => setTimeout(r, 1)); - expect(menu.state.items?.length).toBe(9); + expect(menu.state.items?.length).toBe(7); const extensionsSubMenu = menu.state.items?.find((i) => i.text === 'Extensions')?.subMenu; @@ -199,7 +199,7 @@ describe('panelMenuBehavior', () => { await new Promise((r) => setTimeout(r, 1)); - expect(menu.state.items?.length).toBe(9); + expect(menu.state.items?.length).toBe(7); const extensionsSubMenu = menu.state.items?.find((i) => i.text === 'Extensions')?.subMenu; const menuItem = extensionsSubMenu?.find((i) => (i.text = 'Declare incident when...')); @@ -347,7 +347,7 @@ describe('panelMenuBehavior', () => { await new Promise((r) => setTimeout(r, 1)); - expect(menu.state.items?.length).toBe(9); + expect(menu.state.items?.length).toBe(7); const extensionsSubMenu = menu.state.items?.find((i) => i.text === 'Extensions')?.subMenu; @@ -392,7 +392,7 @@ describe('panelMenuBehavior', () => { await new Promise((r) => setTimeout(r, 1)); - expect(menu.state.items?.length).toBe(9); + expect(menu.state.items?.length).toBe(7); const extensionsSubMenu = menu.state.items?.find((i) => i.text === 'Extensions')?.subMenu; @@ -445,7 +445,7 @@ describe('panelMenuBehavior', () => { await new Promise((r) => setTimeout(r, 1)); - expect(menu.state.items?.length).toBe(9); + expect(menu.state.items?.length).toBe(7); const extensionsSubMenu = menu.state.items?.find((i) => i.text === 'Extensions')?.subMenu; @@ -470,6 +470,43 @@ describe('panelMenuBehavior', () => { ); }); + it('it should not contain remove and duplicate menu items when not in edit mode', async () => { + const { menu, panel } = await buildTestScene({}); + + panel.getPlugin = () => getPanelPlugin({ skipDataQuery: false }); + + mocks.contextSrv.hasAccessToExplore.mockReturnValue(true); + mocks.getExploreUrl.mockReturnValue(Promise.resolve('/explore')); + + menu.activate(); + + await new Promise((r) => setTimeout(r, 1)); + + expect(menu.state.items?.find((i) => i.text === 'Remove')).toBeUndefined(); + const moreMenu = menu.state.items?.find((i) => i.text === 'More...')?.subMenu; + expect(moreMenu?.find((i) => i.text === 'Duplicate')).toBeUndefined(); + expect(moreMenu?.find((i) => i.text === 'Create library panel')).toBeUndefined(); + }); + + it('it should contain remove and duplicate menu items when in edit mode', async () => { + const { scene, menu, panel } = await buildTestScene({}); + scene.setState({ isEditing: true }); + + panel.getPlugin = () => getPanelPlugin({ skipDataQuery: false }); + + mocks.contextSrv.hasAccessToExplore.mockReturnValue(true); + mocks.getExploreUrl.mockReturnValue(Promise.resolve('/explore')); + + menu.activate(); + + await new Promise((r) => setTimeout(r, 1)); + + expect(menu.state.items?.find((i) => i.text === 'Remove')).toBeDefined(); + const moreMenu = menu.state.items?.find((i) => i.text === 'More...')?.subMenu; + expect(moreMenu?.find((i) => i.text === 'Duplicate')).toBeDefined(); + expect(moreMenu?.find((i) => i.text === 'Create library panel')).toBeDefined(); + }); + it('should only contain explore when embedded', async () => { const { menu, panel } = await buildTestScene({ isEmbedded: true }); diff --git a/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx b/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx index 323c89e760..61d958afce 100644 --- a/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx +++ b/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx @@ -86,14 +86,16 @@ export function panelMenuBehavior(menu: VizPanelMenu) { shortcut: 'p s', }); - moreSubMenu.push({ - text: t('panel.header-menu.duplicate', `Duplicate`), - onClick: () => { - DashboardInteractions.panelMenuItemClicked('duplicate'); - dashboard.duplicatePanel(panel); - }, - shortcut: 'p d', - }); + if (dashboard.state.isEditing) { + moreSubMenu.push({ + text: t('panel.header-menu.duplicate', `Duplicate`), + onClick: () => { + DashboardInteractions.panelMenuItemClicked('duplicate'); + dashboard.duplicatePanel(panel); + }, + shortcut: 'p d', + }); + } moreSubMenu.push({ text: t('panel.header-menu.copy', `Copy`), @@ -103,32 +105,34 @@ export function panelMenuBehavior(menu: VizPanelMenu) { }, }); - if (parent instanceof LibraryVizPanel) { - moreSubMenu.push({ - text: t('panel.header-menu.unlink-library-panel', `Unlink library panel`), - onClick: () => { - DashboardInteractions.panelMenuItemClicked('unlinkLibraryPanel'); - dashboard.showModal( - new UnlinkLibraryPanelModal({ - panelRef: parent.getRef(), - }) - ); - }, - }); - } else { - moreSubMenu.push({ - text: t('panel.header-menu.create-library-panel', `Create library panel`), - onClick: () => { - DashboardInteractions.panelMenuItemClicked('createLibraryPanel'); - dashboard.showModal( - new ShareModal({ - panelRef: panel.getRef(), - dashboardRef: dashboard.getRef(), - activeTab: shareDashboardType.libraryPanel, - }) - ); - }, - }); + if (dashboard.state.isEditing) { + if (parent instanceof LibraryVizPanel) { + moreSubMenu.push({ + text: t('panel.header-menu.unlink-library-panel', `Unlink library panel`), + onClick: () => { + DashboardInteractions.panelMenuItemClicked('unlinkLibraryPanel'); + dashboard.showModal( + new UnlinkLibraryPanelModal({ + panelRef: parent.getRef(), + }) + ); + }, + }); + } else { + moreSubMenu.push({ + text: t('panel.header-menu.create-library-panel', `Create library panel`), + onClick: () => { + DashboardInteractions.panelMenuItemClicked('createLibraryPanel'); + dashboard.showModal( + new ShareModal({ + panelRef: panel.getRef(), + dashboardRef: dashboard.getRef(), + activeTab: shareDashboardType.libraryPanel, + }) + ); + }, + }); + } } moreSubMenu.push({ @@ -196,20 +200,22 @@ export function panelMenuBehavior(menu: VizPanelMenu) { }); } - items.push({ - text: '', - type: 'divider', - }); + if (dashboard.state.isEditing) { + items.push({ + text: '', + type: 'divider', + }); - items.push({ - text: t('panel.header-menu.remove', `Remove`), - iconClassName: 'trash-alt', - onClick: () => { - DashboardInteractions.panelMenuItemClicked('remove'); - onRemovePanel(dashboard, panel); - }, - shortcut: 'p r', - }); + items.push({ + text: t('panel.header-menu.remove', `Remove`), + iconClassName: 'trash-alt', + onClick: () => { + DashboardInteractions.panelMenuItemClicked('remove'); + onRemovePanel(dashboard, panel); + }, + shortcut: 'p r', + }); + } menu.setState({ items }); }; diff --git a/public/app/features/dashboard-scene/scene/keyboardShortcuts.ts b/public/app/features/dashboard-scene/scene/keyboardShortcuts.ts index e1bb7aea75..99123eaf9c 100644 --- a/public/app/features/dashboard-scene/scene/keyboardShortcuts.ts +++ b/public/app/features/dashboard-scene/scene/keyboardShortcuts.ts @@ -120,7 +120,9 @@ export function setupKeyboardShortcuts(scene: DashboardScene) { keybindings.addBinding({ key: 'p r', onTrigger: withFocusedPanel(scene, (vizPanel: VizPanel) => { - onRemovePanel(scene, vizPanel); + if (scene.state.isEditing) { + onRemovePanel(scene, vizPanel); + } }), }); @@ -128,7 +130,9 @@ export function setupKeyboardShortcuts(scene: DashboardScene) { keybindings.addBinding({ key: 'p d', onTrigger: withFocusedPanel(scene, (vizPanel: VizPanel) => { - scene.duplicatePanel(vizPanel); + if (scene.state.isEditing) { + scene.duplicatePanel(vizPanel); + } }), }); From 9c22a6144e323340cc3ce14acc826953dd113fd0 Mon Sep 17 00:00:00 2001 From: Victor Marin <36818606+mdvictor@users.noreply.github.com> Date: Mon, 11 Mar 2024 12:34:40 +0200 Subject: [PATCH 0487/1406] Scenes: Row controls (#83607) * wip row controls * wip row repeats * wip * wip * row repeat functional * refactor * refactor to reuse RepeatRowSelect2 * refactor + tests * remove comment * refactor --- .../panel-edit/PanelOptions.tsx | 7 +- .../scene/DashboardScene.test.tsx | 41 ++++ .../dashboard-scene/scene/DashboardScene.tsx | 19 ++ .../scene/RowRepeaterBehavior.test.tsx | 33 +++ .../scene/RowRepeaterBehavior.ts | 8 +- .../scene/row-actions/RowActions.tsx | 208 ++++++++++++++++++ .../scene/row-actions/RowOptionsButton.tsx | 50 +++++ .../scene/row-actions/RowOptionsForm.test.tsx | 51 +++++ .../scene/row-actions/RowOptionsForm.tsx | 61 +++++ .../scene/row-actions/RowOptionsModal.tsx | 40 ++++ .../transformSaveModelToScene.ts | 2 + .../features/dashboard-scene/utils/utils.ts | 2 + .../PanelEditor/getPanelFrameOptions.tsx | 11 +- .../RepeatRowSelect/RepeatRowSelect.tsx | 11 +- 14 files changed, 531 insertions(+), 13 deletions(-) create mode 100644 public/app/features/dashboard-scene/scene/row-actions/RowActions.tsx create mode 100644 public/app/features/dashboard-scene/scene/row-actions/RowOptionsButton.tsx create mode 100644 public/app/features/dashboard-scene/scene/row-actions/RowOptionsForm.test.tsx create mode 100644 public/app/features/dashboard-scene/scene/row-actions/RowOptionsForm.tsx create mode 100644 public/app/features/dashboard-scene/scene/row-actions/RowOptionsModal.tsx diff --git a/public/app/features/dashboard-scene/panel-edit/PanelOptions.tsx b/public/app/features/dashboard-scene/panel-edit/PanelOptions.tsx index c059c325e4..988a7db4c9 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelOptions.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelOptions.tsx @@ -15,12 +15,15 @@ interface Props { } export const PanelOptions = React.memo(({ vizManager, searchQuery, listMode }) => { - const { panel } = vizManager.useState(); + const { panel, repeat } = vizManager.useState(); const { data } = sceneGraph.getData(panel).useState(); const { options, fieldConfig } = panel.useState(); // eslint-disable-next-line react-hooks/exhaustive-deps - const panelFrameOptions = useMemo(() => getPanelFrameCategory2(vizManager), [vizManager, panel]); + const panelFrameOptions = useMemo( + () => getPanelFrameCategory2(vizManager, panel, repeat), + [vizManager, panel, repeat] + ); const visualizationOptions = useMemo(() => { const plugin = panel.getPlugin(); diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx index 69b1e38091..7dc9c6339f 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx @@ -338,6 +338,47 @@ describe('DashboardScene', () => { expect(gridRow.state.children.length).toBe(0); }); + it('Should remove a row and move its children to the grid layout', () => { + const body = scene.state.body as SceneGridLayout; + const row = body.state.children[2] as SceneGridRow; + + scene.removeRow(row); + + const vizPanel = (body.state.children[2] as SceneGridItem).state.body as VizPanel; + + expect(body.state.children.length).toBe(6); + expect(vizPanel.state.key).toBe('panel-4'); + }); + + it('Should remove a row and its children', () => { + const body = scene.state.body as SceneGridLayout; + const row = body.state.children[2] as SceneGridRow; + + scene.removeRow(row, true); + + expect(body.state.children.length).toBe(4); + }); + + it('Should remove an empty row from the layout', () => { + const row = new SceneGridRow({ + key: 'panel-1', + }); + + const scene = buildTestScene({ + body: new SceneGridLayout({ + children: [row], + }), + }); + + const body = scene.state.body as SceneGridLayout; + + expect(body.state.children.length).toBe(1); + + scene.removeRow(row); + + expect(body.state.children.length).toBe(0); + }); + it('Should fail to copy a panel if it does not have a grid item parent', () => { const vizPanel = new VizPanel({ title: 'Panel Title', diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index be27cb411a..41bdb767c8 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -412,6 +412,25 @@ export class DashboardScene extends SceneObjectBase { }); } + public removeRow(row: SceneGridRow, removePanels = false) { + if (!(this.state.body instanceof SceneGridLayout)) { + throw new Error('Trying to add a panel in a layout that is not SceneGridLayout'); + } + + const sceneGridLayout = this.state.body; + + const children = sceneGridLayout.state.children.filter((child) => child.state.key !== row.state.key); + + if (!removePanels) { + const rowChildren = row.state.children.map((child) => child.clone()); + const indexOfRow = sceneGridLayout.state.children.findIndex((child) => child.state.key === row.state.key); + + children.splice(indexOfRow, 0, ...rowChildren); + } + + sceneGridLayout.setState({ children }); + } + public addPanel(vizPanel: VizPanel): void { if (!(this.state.body instanceof SceneGridLayout)) { throw new Error('Trying to add a panel in a layout that is not SceneGridLayout'); diff --git a/public/app/features/dashboard-scene/scene/RowRepeaterBehavior.test.tsx b/public/app/features/dashboard-scene/scene/RowRepeaterBehavior.test.tsx index 204394c372..e6d18e149b 100644 --- a/public/app/features/dashboard-scene/scene/RowRepeaterBehavior.test.tsx +++ b/public/app/features/dashboard-scene/scene/RowRepeaterBehavior.test.tsx @@ -50,6 +50,19 @@ describe('RowRepeaterBehavior', () => { expect(rowAtTheBottom.state.y).toBe(40); }); + it('Should push row at the bottom down and also offset its children', () => { + const rowAtTheBottom = grid.state.children[6] as SceneGridRow; + const rowChildOne = rowAtTheBottom.state.children[0] as SceneGridItem; + const rowChildTwo = rowAtTheBottom.state.children[1] as SceneGridItem; + + expect(rowAtTheBottom.state.title).toBe('Row at the bottom'); + + // Panel at the top is 10, each row is (1+5)*5 = 30, so the grid item below it should be 40 + expect(rowAtTheBottom.state.y).toBe(40); + expect(rowChildOne.state.y).toBe(41); + expect(rowChildTwo.state.y).toBe(49); + }); + it('Should handle second repeat cycle and update remove old repeats', async () => { // trigger another repeat cycle by changing the variable const variable = scene.state.$variables!.state.variables[0] as TestVariable; @@ -111,6 +124,26 @@ function buildScene(options: SceneOptions) { width: 24, height: 5, title: 'Row at the bottom', + children: [ + new SceneGridItem({ + key: 'griditem-2', + x: 0, + y: 17, + body: new SceneCanvasText({ + key: 'canvas-2', + text: 'Panel inside row, server = $server', + }), + }), + new SceneGridItem({ + key: 'griditem-3', + x: 0, + y: 25, + body: new SceneCanvasText({ + key: 'canvas-3', + text: 'Panel inside row, server = $server', + }), + }), + ], }), ], }); diff --git a/public/app/features/dashboard-scene/scene/RowRepeaterBehavior.ts b/public/app/features/dashboard-scene/scene/RowRepeaterBehavior.ts index 75202b3367..476fc31d63 100644 --- a/public/app/features/dashboard-scene/scene/RowRepeaterBehavior.ts +++ b/public/app/features/dashboard-scene/scene/RowRepeaterBehavior.ts @@ -180,8 +180,12 @@ function updateLayout(layout: SceneGridLayout, rows: SceneGridRow[], maxYOfRows: const diff = maxYOfRows - firstChildAfterY; for (const child of childrenAfter) { - if (child.state.y! < maxYOfRows) { - child.setState({ y: child.state.y! + diff }); + child.setState({ y: child.state.y! + diff }); + + if (child instanceof SceneGridRow) { + for (const rowChild of child.state.children) { + rowChild.setState({ y: rowChild.state.y! + diff }); + } } } } diff --git a/public/app/features/dashboard-scene/scene/row-actions/RowActions.tsx b/public/app/features/dashboard-scene/scene/row-actions/RowActions.tsx new file mode 100644 index 0000000000..ec9b123be4 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/row-actions/RowActions.tsx @@ -0,0 +1,208 @@ +import { css } from '@emotion/css'; +import React from 'react'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { + SceneComponentProps, + SceneGridItem, + SceneGridLayout, + SceneGridRow, + SceneObjectBase, + SceneObjectState, + SceneQueryRunner, + VizPanel, +} from '@grafana/scenes'; +import { Icon, TextLink, useStyles2 } from '@grafana/ui'; +import appEvents from 'app/core/app_events'; +import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard'; +import { ShowConfirmModalEvent } from 'app/types/events'; + +import { getDashboardSceneFor } from '../../utils/utils'; +import { DashboardScene } from '../DashboardScene'; +import { RowRepeaterBehavior } from '../RowRepeaterBehavior'; + +import { RowOptionsButton } from './RowOptionsButton'; + +export interface RowActionsState extends SceneObjectState {} + +export class RowActions extends SceneObjectBase { + private updateLayout(rowClone: SceneGridRow): void { + const row = this.getParent(); + + const layout = this.getDashboard().state.body; + + if (!(layout instanceof SceneGridLayout)) { + throw new Error('Layout is not a SceneGridLayout'); + } + + // remove the repeated rows + const children = layout.state.children.filter((child) => !child.state.key?.startsWith(`${row.state.key}-clone-`)); + + // get the index to replace later + const index = children.indexOf(row); + + if (index === -1) { + throw new Error('Parent row not found in layout children'); + } + + // replace the row with the clone + layout.setState({ + children: [...children.slice(0, index), rowClone, ...children.slice(index + 1)], + }); + } + + public getParent(): SceneGridRow { + if (!(this.parent instanceof SceneGridRow)) { + throw new Error('RowActions must have a SceneGridRow parent'); + } + + return this.parent; + } + + public getDashboard(): DashboardScene { + return getDashboardSceneFor(this); + } + + public onUpdate = (title: string, repeat?: string | null): void => { + const row = this.getParent(); + + // return early if there is no repeat + if (!repeat) { + const clone = row.clone(); + + // remove the row repeater behaviour, leave the rest + clone.setState({ + title, + $behaviors: row.state.$behaviors?.filter((b) => !(b instanceof RowRepeaterBehavior)) ?? [], + }); + + this.updateLayout(clone); + + return; + } + + const children = row.state.children.map((child) => child.clone()); + + const newBehaviour = new RowRepeaterBehavior({ + variableName: repeat, + sources: children, + }); + + // get rest of behaviors except the old row repeater, if any, and push new one + const behaviors = row.state.$behaviors?.filter((b) => !(b instanceof RowRepeaterBehavior)) ?? []; + behaviors.push(newBehaviour); + + row.setState({ + title, + $behaviors: behaviors, + }); + + newBehaviour.activate(); + }; + + public onDelete = () => { + appEvents.publish( + new ShowConfirmModalEvent({ + title: 'Delete row', + text: 'Are you sure you want to remove this row and all its panels?', + altActionText: 'Delete row only', + icon: 'trash-alt', + onConfirm: () => { + this.getDashboard().removeRow(this.getParent(), true); + }, + onAltAction: () => { + this.getDashboard().removeRow(this.getParent()); + }, + }) + ); + }; + + public getWarning = () => { + const row = this.getParent(); + const gridItems = row.state.children; + + const isAnyPanelUsingDashboardDS = gridItems.some((gridItem) => { + if (!(gridItem instanceof SceneGridItem)) { + return false; + } + + if (gridItem.state.body instanceof VizPanel && gridItem.state.body.state.$data instanceof SceneQueryRunner) { + return gridItem.state.body.state.$data?.state.datasource?.uid === SHARED_DASHBOARD_QUERY; + } + + return false; + }); + + if (isAnyPanelUsingDashboardDS) { + return ( +
+

+ Panels in this row use the {SHARED_DASHBOARD_QUERY} data source. These panels will reference the panel in + the original row, not the ones in the repeated rows. +

+ + Learn more + +
+ ); + } + + return undefined; + }; + + static Component = ({ model }: SceneComponentProps) => { + const dashboard = model.getDashboard(); + const row = model.getParent(); + const { title } = row.useState(); + const { meta, isEditing } = dashboard.useState(); + const styles = useStyles2(getStyles); + + const behaviour = row.state.$behaviors?.find((b) => b instanceof RowRepeaterBehavior); + + return ( + <> + {meta.canEdit && isEditing && ( + <> +
+ + +
+ + )} + + ); + }; +} + +const getStyles = (theme: GrafanaTheme2) => { + return { + rowActions: css({ + color: theme.colors.text.secondary, + lineHeight: '27px', + + button: { + color: theme.colors.text.secondary, + paddingLeft: theme.spacing(2), + background: 'transparent', + border: 'none', + + '&:hover': { + color: theme.colors.text.maxContrast, + }, + }, + }), + }; +}; diff --git a/public/app/features/dashboard-scene/scene/row-actions/RowOptionsButton.tsx b/public/app/features/dashboard-scene/scene/row-actions/RowOptionsButton.tsx new file mode 100644 index 0000000000..ef10894cf3 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/row-actions/RowOptionsButton.tsx @@ -0,0 +1,50 @@ +import React from 'react'; + +import { SceneObject } from '@grafana/scenes'; +import { Icon, ModalsController } from '@grafana/ui'; + +import { OnRowOptionsUpdate } from './RowOptionsForm'; +import { RowOptionsModal } from './RowOptionsModal'; + +export interface RowOptionsButtonProps { + title: string; + repeat?: string; + parent: SceneObject; + onUpdate: OnRowOptionsUpdate; + warning?: React.ReactNode; +} + +export const RowOptionsButton = ({ repeat, title, parent, onUpdate, warning }: RowOptionsButtonProps) => { + const onUpdateChange = (hideModal: () => void) => (title: string, repeat?: string | null) => { + onUpdate(title, repeat); + hideModal(); + }; + + return ( + + {({ showModal, hideModal }) => { + return ( + + ); + }} + + ); +}; + +RowOptionsButton.displayName = 'RowOptionsButton'; diff --git a/public/app/features/dashboard-scene/scene/row-actions/RowOptionsForm.test.tsx b/public/app/features/dashboard-scene/scene/row-actions/RowOptionsForm.test.tsx new file mode 100644 index 0000000000..c6bcf17fb9 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/row-actions/RowOptionsForm.test.tsx @@ -0,0 +1,51 @@ +import { render, screen } from '@testing-library/react'; +import React from 'react'; +import { TestProvider } from 'test/helpers/TestProvider'; + +import { selectors } from '@grafana/e2e-selectors'; + +import { DashboardScene } from '../DashboardScene'; + +import { RowOptionsForm } from './RowOptionsForm'; + +jest.mock('app/features/dashboard/components/RepeatRowSelect/RepeatRowSelect', () => ({ + RepeatRowSelect2: () =>
, +})); +describe('DashboardRow', () => { + const scene = new DashboardScene({ + title: 'hello', + uid: 'dash-1', + meta: { + canEdit: true, + }, + }); + + it('Should show warning component when has warningMessage prop', () => { + render( + + + + ); + expect( + screen.getByTestId(selectors.pages.Dashboard.Rows.Repeated.ConfigSection.warningMessage) + ).toBeInTheDocument(); + }); + + it('Should not show warning component when does not have warningMessage prop', () => { + render( + + + + ); + expect( + screen.queryByTestId(selectors.pages.Dashboard.Rows.Repeated.ConfigSection.warningMessage) + ).not.toBeInTheDocument(); + }); +}); diff --git a/public/app/features/dashboard-scene/scene/row-actions/RowOptionsForm.tsx b/public/app/features/dashboard-scene/scene/row-actions/RowOptionsForm.tsx new file mode 100644 index 0000000000..538a35382e --- /dev/null +++ b/public/app/features/dashboard-scene/scene/row-actions/RowOptionsForm.tsx @@ -0,0 +1,61 @@ +import React, { useCallback, useState } from 'react'; +import { useForm } from 'react-hook-form'; + +import { selectors } from '@grafana/e2e-selectors'; +import { SceneObject } from '@grafana/scenes'; +import { Button, Field, Modal, Input, Alert } from '@grafana/ui'; +import { RepeatRowSelect2 } from 'app/features/dashboard/components/RepeatRowSelect/RepeatRowSelect'; + +export type OnRowOptionsUpdate = (title: string, repeat?: string | null) => void; + +export interface Props { + title: string; + repeat?: string; + parent: SceneObject; + onUpdate: OnRowOptionsUpdate; + onCancel: () => void; + warning?: React.ReactNode; +} + +export const RowOptionsForm = ({ repeat, title, parent, warning, onUpdate, onCancel }: Props) => { + const [newRepeat, setNewRepeat] = useState(repeat); + const onChangeRepeat = useCallback((name?: string) => setNewRepeat(name), [setNewRepeat]); + + const { handleSubmit, register } = useForm({ + defaultValues: { + title, + }, + }); + + const submit = (formData: { title: string }) => { + onUpdate(formData.title, newRepeat); + }; + + return ( +
+ + + + + + + {warning && ( + + {warning} + + )} + + + + +
+ ); +}; diff --git a/public/app/features/dashboard-scene/scene/row-actions/RowOptionsModal.tsx b/public/app/features/dashboard-scene/scene/row-actions/RowOptionsModal.tsx new file mode 100644 index 0000000000..2f23db9add --- /dev/null +++ b/public/app/features/dashboard-scene/scene/row-actions/RowOptionsModal.tsx @@ -0,0 +1,40 @@ +import { css } from '@emotion/css'; +import React from 'react'; + +import { SceneObject } from '@grafana/scenes'; +import { Modal, useStyles2 } from '@grafana/ui'; + +import { OnRowOptionsUpdate, RowOptionsForm } from './RowOptionsForm'; + +export interface RowOptionsModalProps { + title: string; + repeat?: string; + parent: SceneObject; + warning?: React.ReactNode; + onDismiss: () => void; + onUpdate: OnRowOptionsUpdate; +} + +export const RowOptionsModal = ({ repeat, title, parent, onDismiss, onUpdate, warning }: RowOptionsModalProps) => { + const styles = useStyles2(getStyles); + + return ( + + + + ); +}; + +const getStyles = () => ({ + modal: css({ + label: 'RowOptionsModal', + width: '500px', + }), +}); diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts index 4cb91f0c17..8609ff8f98 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts @@ -47,6 +47,7 @@ import { PanelNotices } from '../scene/PanelNotices'; import { PanelRepeaterGridItem } from '../scene/PanelRepeaterGridItem'; import { PanelTimeRange } from '../scene/PanelTimeRange'; import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior'; +import { RowActions } from '../scene/row-actions/RowActions'; import { setDashboardPanelContext } from '../scene/setDashboardPanelContext'; import { createPanelDataProvider } from '../utils/createPanelDataProvider'; import { DashboardInteractions } from '../utils/interactions'; @@ -211,6 +212,7 @@ function createRowFromPanelModel(row: PanelModel, content: SceneGridItemLike[]): isCollapsed: row.collapsed, children: children, $behaviors: behaviors, + actions: new RowActions({}), }); } diff --git a/public/app/features/dashboard-scene/utils/utils.ts b/public/app/features/dashboard-scene/utils/utils.ts index d82cf30125..00d50ee41d 100644 --- a/public/app/features/dashboard-scene/utils/utils.ts +++ b/public/app/features/dashboard-scene/utils/utils.ts @@ -17,6 +17,7 @@ import { DashboardScene } from '../scene/DashboardScene'; import { LibraryVizPanel } from '../scene/LibraryVizPanel'; import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks'; import { panelMenuBehavior } from '../scene/PanelMenuBehavior'; +import { RowActions } from '../scene/row-actions/RowActions'; import { dashboardSceneGraph } from './dashboardSceneGraph'; @@ -233,6 +234,7 @@ export function getDefaultRow(dashboard: DashboardScene): SceneGridRow { return new SceneGridRow({ key: getVizPanelKeyForPanelId(id), title: 'Row title', + actions: new RowActions({}), y: 0, }); } diff --git a/public/app/features/dashboard/components/PanelEditor/getPanelFrameOptions.tsx b/public/app/features/dashboard/components/PanelEditor/getPanelFrameOptions.tsx index 6b3adccb4e..8857409406 100644 --- a/public/app/features/dashboard/components/PanelEditor/getPanelFrameOptions.tsx +++ b/public/app/features/dashboard/components/PanelEditor/getPanelFrameOptions.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { SelectableValue } from '@grafana/data'; import { config } from '@grafana/runtime'; +import { VizPanel } from '@grafana/scenes'; import { DataLinksInlineEditor, Input, RadioButtonGroup, Select, Switch, TextArea } from '@grafana/ui'; import { VizPanelManager, VizPanelManagerState } from 'app/features/dashboard-scene/panel-edit/VizPanelManager'; import { VizPanelLinks } from 'app/features/dashboard-scene/scene/PanelLinks'; @@ -176,8 +177,11 @@ export function getPanelFrameCategory(props: OptionPaneRenderProps): OptionsPane ); } -export function getPanelFrameCategory2(panelManager: VizPanelManager): OptionsPaneCategoryDescriptor { - const { panel } = panelManager.state; +export function getPanelFrameCategory2( + panelManager: VizPanelManager, + panel: VizPanel, + repeat?: string +): OptionsPaneCategoryDescriptor { const descriptor = new OptionsPaneCategoryDescriptor({ title: 'Panel options', id: 'Panel options', @@ -270,7 +274,8 @@ export function getPanelFrameCategory2(panelManager: VizPanelManager): OptionsPa return ( { const stateUpdate: Partial = { repeat: value }; if (value && !panelManager.state.repeatDirection) { diff --git a/public/app/features/dashboard/components/RepeatRowSelect/RepeatRowSelect.tsx b/public/app/features/dashboard/components/RepeatRowSelect/RepeatRowSelect.tsx index c0de0c7293..de9ce1c197 100644 --- a/public/app/features/dashboard/components/RepeatRowSelect/RepeatRowSelect.tsx +++ b/public/app/features/dashboard/components/RepeatRowSelect/RepeatRowSelect.tsx @@ -1,9 +1,8 @@ import React, { useCallback, useMemo } from 'react'; import { SelectableValue } from '@grafana/data'; -import { sceneGraph } from '@grafana/scenes'; +import { SceneObject, sceneGraph } from '@grafana/scenes'; import { Select } from '@grafana/ui'; -import { VizPanelManager } from 'app/features/dashboard-scene/panel-edit/VizPanelManager'; import { useSelector } from 'app/types'; import { getLastKey, getVariablesByKey } from '../../../variables/state/selectors'; @@ -45,14 +44,14 @@ export const RepeatRowSelect = ({ repeat, onChange, id }: Props) => { }; interface Props2 { - panelManager: VizPanelManager; + parent: SceneObject; + repeat: string | undefined; id?: string; onChange: (name?: string) => void; } -export const RepeatRowSelect2 = ({ panelManager, id, onChange }: Props2) => { - const { panel, repeat } = panelManager.useState(); - const sceneVars = useMemo(() => sceneGraph.getVariables(panel), [panel]); +export const RepeatRowSelect2 = ({ parent, repeat, id, onChange }: Props2) => { + const sceneVars = useMemo(() => sceneGraph.getVariables(parent), [parent]); const variables = sceneVars.useState().variables; const variableOptions = useMemo(() => { From 534855d086a6d64c40682a128d9ec1d74a3f932e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 10:22:16 +0000 Subject: [PATCH 0488/1406] Update dependency @types/node to v20.11.25 --- package.json | 2 +- packages/grafana-data/package.json | 2 +- packages/grafana-e2e-selectors/package.json | 2 +- packages/grafana-prometheus/package.json | 2 +- packages/grafana-ui/package.json | 2 +- .../datasource/azuremonitor/package.json | 2 +- .../datasource/cloud-monitoring/package.json | 2 +- .../grafana-testdata-datasource/package.json | 2 +- .../app/plugins/datasource/tempo/package.json | 2 +- yarn.lock | 26 +++++++++---------- 10 files changed, 22 insertions(+), 22 deletions(-) diff --git a/package.json b/package.json index 4b53cc5c2e..2063cb3648 100644 --- a/package.json +++ b/package.json @@ -117,7 +117,7 @@ "@types/lucene": "^2", "@types/marked": "5.0.2", "@types/mousetrap": "1.6.15", - "@types/node": "20.11.20", + "@types/node": "20.11.25", "@types/node-forge": "^1", "@types/ol-ext": "npm:@siedlerchr/types-ol-ext@3.2.4", "@types/papaparse": "5.3.14", diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index 8deb7ab7ed..e345910666 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -67,7 +67,7 @@ "@types/history": "4.7.11", "@types/lodash": "4.14.202", "@types/marked": "5.0.2", - "@types/node": "20.11.20", + "@types/node": "20.11.25", "@types/papaparse": "5.3.14", "@types/react": "18.2.60", "@types/react-dom": "18.2.19", diff --git a/packages/grafana-e2e-selectors/package.json b/packages/grafana-e2e-selectors/package.json index cd631d8f92..ffd83c6a77 100644 --- a/packages/grafana-e2e-selectors/package.json +++ b/packages/grafana-e2e-selectors/package.json @@ -41,7 +41,7 @@ "devDependencies": { "@rollup/plugin-commonjs": "25.0.7", "@rollup/plugin-node-resolve": "15.2.3", - "@types/node": "20.11.20", + "@types/node": "20.11.25", "esbuild": "0.18.12", "rimraf": "5.0.5", "rollup": "2.79.1", diff --git a/packages/grafana-prometheus/package.json b/packages/grafana-prometheus/package.json index d7a865af3c..5af96f1205 100644 --- a/packages/grafana-prometheus/package.json +++ b/packages/grafana-prometheus/package.json @@ -94,7 +94,7 @@ "@types/jquery": "3.5.29", "@types/lodash": "4.14.202", "@types/marked": "5.0.2", - "@types/node": "20.11.20", + "@types/node": "20.11.25", "@types/pluralize": "^0.0.33", "@types/prismjs": "1.26.3", "@types/react": "18.2.60", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index d9e6f8f920..5947818618 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -139,7 +139,7 @@ "@types/jquery": "3.5.29", "@types/lodash": "4.14.202", "@types/mock-raf": "1.0.6", - "@types/node": "20.11.20", + "@types/node": "20.11.25", "@types/prismjs": "1.26.3", "@types/react": "18.2.60", "@types/react-beautiful-dnd": "13.1.8", diff --git a/public/app/plugins/datasource/azuremonitor/package.json b/public/app/plugins/datasource/azuremonitor/package.json index fce5e1152b..f45b5e5a73 100644 --- a/public/app/plugins/datasource/azuremonitor/package.json +++ b/public/app/plugins/datasource/azuremonitor/package.json @@ -29,7 +29,7 @@ "@testing-library/user-event": "14.5.2", "@types/jest": "29.5.12", "@types/lodash": "4.14.202", - "@types/node": "20.11.20", + "@types/node": "20.11.25", "@types/prismjs": "1.26.3", "@types/react": "18.2.60", "@types/testing-library__jest-dom": "5.14.9", diff --git a/public/app/plugins/datasource/cloud-monitoring/package.json b/public/app/plugins/datasource/cloud-monitoring/package.json index 48135a9846..eb900136bd 100644 --- a/public/app/plugins/datasource/cloud-monitoring/package.json +++ b/public/app/plugins/datasource/cloud-monitoring/package.json @@ -32,7 +32,7 @@ "@types/debounce-promise": "3.1.9", "@types/jest": "29.5.12", "@types/lodash": "4.14.202", - "@types/node": "20.11.20", + "@types/node": "20.11.25", "@types/prismjs": "1.26.3", "@types/react": "18.2.60", "@types/react-test-renderer": "18.0.7", diff --git a/public/app/plugins/datasource/grafana-testdata-datasource/package.json b/public/app/plugins/datasource/grafana-testdata-datasource/package.json index 4688d79003..2cfcabb03f 100644 --- a/public/app/plugins/datasource/grafana-testdata-datasource/package.json +++ b/public/app/plugins/datasource/grafana-testdata-datasource/package.json @@ -27,7 +27,7 @@ "@types/d3-random": "^3.0.2", "@types/jest": "29.5.12", "@types/lodash": "4.14.202", - "@types/node": "20.11.20", + "@types/node": "20.11.25", "@types/react": "18.2.60", "@types/testing-library__jest-dom": "5.14.9", "@types/uuid": "9.0.8", diff --git a/public/app/plugins/datasource/tempo/package.json b/public/app/plugins/datasource/tempo/package.json index 35777a04ac..33c8bd4245 100644 --- a/public/app/plugins/datasource/tempo/package.json +++ b/public/app/plugins/datasource/tempo/package.json @@ -44,7 +44,7 @@ "@testing-library/user-event": "14.5.2", "@types/jest": "29.5.12", "@types/lodash": "4.14.202", - "@types/node": "20.11.20", + "@types/node": "20.11.25", "@types/prismjs": "1.26.3", "@types/react": "18.2.60", "@types/react-dom": "18.2.19", diff --git a/yarn.lock b/yarn.lock index 4e288c4cde..70b79aa445 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3231,7 +3231,7 @@ __metadata: "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:29.5.12" "@types/lodash": "npm:4.14.202" - "@types/node": "npm:20.11.20" + "@types/node": "npm:20.11.25" "@types/prismjs": "npm:1.26.3" "@types/react": "npm:18.2.60" "@types/testing-library__jest-dom": "npm:5.14.9" @@ -3310,7 +3310,7 @@ __metadata: "@types/d3-random": "npm:^3.0.2" "@types/jest": "npm:29.5.12" "@types/lodash": "npm:4.14.202" - "@types/node": "npm:20.11.20" + "@types/node": "npm:20.11.25" "@types/react": "npm:18.2.60" "@types/testing-library__jest-dom": "npm:5.14.9" "@types/uuid": "npm:9.0.8" @@ -3398,7 +3398,7 @@ __metadata: "@types/debounce-promise": "npm:3.1.9" "@types/jest": "npm:29.5.12" "@types/lodash": "npm:4.14.202" - "@types/node": "npm:20.11.20" + "@types/node": "npm:20.11.25" "@types/prismjs": "npm:1.26.3" "@types/react": "npm:18.2.60" "@types/react-test-renderer": "npm:18.0.7" @@ -3450,7 +3450,7 @@ __metadata: "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:29.5.12" "@types/lodash": "npm:4.14.202" - "@types/node": "npm:20.11.20" + "@types/node": "npm:20.11.25" "@types/prismjs": "npm:1.26.3" "@types/react": "npm:18.2.60" "@types/react-dom": "npm:18.2.19" @@ -3542,7 +3542,7 @@ __metadata: "@types/history": "npm:4.7.11" "@types/lodash": "npm:4.14.202" "@types/marked": "npm:5.0.2" - "@types/node": "npm:20.11.20" + "@types/node": "npm:20.11.25" "@types/papaparse": "npm:5.3.14" "@types/react": "npm:18.2.60" "@types/react-dom": "npm:18.2.19" @@ -3601,7 +3601,7 @@ __metadata: "@grafana/tsconfig": "npm:^1.3.0-rc1" "@rollup/plugin-commonjs": "npm:25.0.7" "@rollup/plugin-node-resolve": "npm:15.2.3" - "@types/node": "npm:20.11.20" + "@types/node": "npm:20.11.25" esbuild: "npm:0.18.12" rimraf: "npm:5.0.5" rollup: "npm:2.79.1" @@ -3940,7 +3940,7 @@ __metadata: "@types/jquery": "npm:3.5.29" "@types/lodash": "npm:4.14.202" "@types/marked": "npm:5.0.2" - "@types/node": "npm:20.11.20" + "@types/node": "npm:20.11.25" "@types/pluralize": "npm:^0.0.33" "@types/prismjs": "npm:1.26.3" "@types/react": "npm:18.2.60" @@ -4204,7 +4204,7 @@ __metadata: "@types/jquery": "npm:3.5.29" "@types/lodash": "npm:4.14.202" "@types/mock-raf": "npm:1.0.6" - "@types/node": "npm:20.11.20" + "@types/node": "npm:20.11.25" "@types/prismjs": "npm:1.26.3" "@types/react": "npm:18.2.60" "@types/react-beautiful-dnd": "npm:13.1.8" @@ -9630,12 +9630,12 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:*, @types/node@npm:20.11.20, @types/node@npm:>=13.7.0, @types/node@npm:^20.11.16": - version: 20.11.20 - resolution: "@types/node@npm:20.11.20" +"@types/node@npm:*, @types/node@npm:20.11.25, @types/node@npm:>=13.7.0, @types/node@npm:^20.11.16": + version: 20.11.25 + resolution: "@types/node@npm:20.11.25" dependencies: undici-types: "npm:~5.26.4" - checksum: 10/ff449bdc94810dadb54e0f77dd587c6505ef79ffa5a208c16eb29b223365b188f4c935a3abaf0906a01d05257c3da1f72465594a841d35bcf7b6deac7a6938fb + checksum: 10/861265f1bbb151404bd8842b595f027a4ff067c61ecff9a37b9f7f53922c18dd532c8e795e8e7675dd8dba056645623fd2b9848d5ef72863ec3609096cd2923e languageName: node linkType: hard @@ -18397,7 +18397,7 @@ __metadata: "@types/lucene": "npm:^2" "@types/marked": "npm:5.0.2" "@types/mousetrap": "npm:1.6.15" - "@types/node": "npm:20.11.20" + "@types/node": "npm:20.11.25" "@types/node-forge": "npm:^1" "@types/ol-ext": "npm:@siedlerchr/types-ol-ext@3.2.4" "@types/papaparse": "npm:5.3.14" From 940d20e115fa442d8f92a30535907a05ca27e71a Mon Sep 17 00:00:00 2001 From: Tobias Skarhed <1438972+tskarhed@users.noreply.github.com> Date: Mon, 11 Mar 2024 11:43:22 +0100 Subject: [PATCH 0489/1406] Accessibility: Improve landmark markup (#83576) * Landmark: main * Landmark: add header * Submenu: Move conditional display up * NewsPanel: use h3 as the article label * Use title for article id * Update test showing a false positive * DashboardPage: Expect submenu to not be shown --- public/app/core/components/AppChrome/AppChrome.tsx | 12 ++++++------ .../dashboard/components/SubMenu/SubMenu.tsx | 4 ---- .../dashboard/containers/DashboardPage.test.tsx | 8 +++++--- .../features/dashboard/containers/DashboardPage.tsx | 2 +- public/app/plugins/panel/news/component/News.tsx | 7 +++++-- 5 files changed, 17 insertions(+), 16 deletions(-) diff --git a/public/app/core/components/AppChrome/AppChrome.tsx b/public/app/core/components/AppChrome/AppChrome.tsx index b47c655188..e4bd8b8090 100644 --- a/public/app/core/components/AppChrome/AppChrome.tsx +++ b/public/app/core/components/AppChrome/AppChrome.tsx @@ -82,7 +82,7 @@ export function AppChrome({ children }: Props) { Skip to main content -
+
{!searchBarHidden && } -
+ )} -
+
{!state.chromeless && state.megaMenuDocked && state.megaMenuOpen && ( chrome.setMegaMenuOpen(false)} /> )} -
+
{children} -
+
- +
{!state.chromeless && !state.megaMenuDocked && } {!state.chromeless && } {shouldShowReturnToPrevious && state.returnToPrevious && ( diff --git a/public/app/features/dashboard/components/SubMenu/SubMenu.tsx b/public/app/features/dashboard/components/SubMenu/SubMenu.tsx index 16a5742e9d..85e77fc787 100644 --- a/public/app/features/dashboard/components/SubMenu/SubMenu.tsx +++ b/public/app/features/dashboard/components/SubMenu/SubMenu.tsx @@ -51,10 +51,6 @@ class SubMenuUnConnected extends PureComponent { const styles = getStyles(theme); - if (!dashboard.isSubMenuVisible()) { - return null; - } - const readOnlyVariables = dashboard.meta.isSnapshot ?? false; return ( diff --git a/public/app/features/dashboard/containers/DashboardPage.test.tsx b/public/app/features/dashboard/containers/DashboardPage.test.tsx index 24486c53e2..004f1d951a 100644 --- a/public/app/features/dashboard/containers/DashboardPage.test.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.test.tsx @@ -306,10 +306,12 @@ describe('DashboardPage', () => { }); describe('No kiosk mode tv', () => { - it('should render dashboard page toolbar and submenu', async () => { - setup({ dashboard: getTestDashboard() }); + it('should render dashboard page toolbar with no submenu', async () => { + setup({ + dashboard: getTestDashboard(), + }); expect(await screen.findAllByTestId(selectors.pages.Dashboard.DashNav.navV2)).toHaveLength(1); - expect(screen.getAllByLabelText(selectors.pages.Dashboard.SubMenu.submenu)).toHaveLength(1); + expect(screen.queryAllByLabelText(selectors.pages.Dashboard.SubMenu.submenu)).toHaveLength(0); }); }); diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index 71d44e4e99..188c10d9d4 100644 --- a/public/app/features/dashboard/containers/DashboardPage.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -302,7 +302,7 @@ export class UnthemedDashboardPage extends PureComponent { } const inspectPanel = this.getInspectPanel(); - const showSubMenu = !editPanel && !kioskMode && !this.props.queryParams.editview; + const showSubMenu = !editPanel && !kioskMode && !this.props.queryParams.editview && dashboard.isSubMenuVisible(); const showToolbar = kioskMode !== KioskMode.Full && !queryParams.editview; diff --git a/public/app/plugins/panel/news/component/News.tsx b/public/app/plugins/panel/news/component/News.tsx index 39c95ff82d..c47f1d1ade 100644 --- a/public/app/plugins/panel/news/component/News.tsx +++ b/public/app/plugins/panel/news/component/News.tsx @@ -19,9 +19,10 @@ function NewsComponent({ width, showImage, data, index }: NewsItemProps) { const styles = useStyles2(getStyles); const useWideLayout = width > 600; const newsItem = data.get(index); + const titleId = encodeURI(newsItem.title); return ( -
+
{showImage && newsItem.ogImage && ( -

{newsItem.title}

+

+ {newsItem.title} +

From 0a12377a19bb7bb7acf29307f7da447eb0b0726e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 10:41:39 +0000 Subject: [PATCH 0490/1406] Update dependency autoprefixer to v10.4.18 --- package.json | 2 +- yarn.lock | 24 ++++++++++++------------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index 2063cb3648..3517316d25 100644 --- a/package.json +++ b/package.json @@ -149,7 +149,7 @@ "@types/yargs": "17.0.32", "@typescript-eslint/eslint-plugin": "6.21.0", "@typescript-eslint/parser": "6.21.0", - "autoprefixer": "10.4.17", + "autoprefixer": "10.4.18", "blob-polyfill": "7.0.20220408", "browserslist": "^4.21.4", "chance": "^1.0.10", diff --git a/yarn.lock b/yarn.lock index 70b79aa445..6b622e64e3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11858,12 +11858,12 @@ __metadata: languageName: node linkType: hard -"autoprefixer@npm:10.4.17": - version: 10.4.17 - resolution: "autoprefixer@npm:10.4.17" +"autoprefixer@npm:10.4.18": + version: 10.4.18 + resolution: "autoprefixer@npm:10.4.18" dependencies: - browserslist: "npm:^4.22.2" - caniuse-lite: "npm:^1.0.30001578" + browserslist: "npm:^4.23.0" + caniuse-lite: "npm:^1.0.30001591" fraction.js: "npm:^4.3.7" normalize-range: "npm:^0.1.2" picocolors: "npm:^1.0.0" @@ -11872,7 +11872,7 @@ __metadata: postcss: ^8.1.0 bin: autoprefixer: bin/autoprefixer - checksum: 10/ac4416e72643bf92c2a346af5a6a437eb39e3b852e5d48e1a0a3204a81cbf8eecc5489a9386cf63a288b7183fae3ad52cf3c24c458d7cbb5463e55e21dc7e6ed + checksum: 10/c5bc0b539451557ac0b531bd6dad2db50499cf3d5daff9ead57d0d90d8f63ea6aa0e0556cbda3fbd3d081ec1199202f14533d54494ea47f42b239852d6bde16f languageName: node linkType: hard @@ -12466,7 +12466,7 @@ __metadata: languageName: node linkType: hard -"browserslist@npm:^4.0.0, browserslist@npm:^4.14.5, browserslist@npm:^4.21.10, browserslist@npm:^4.21.4, browserslist@npm:^4.22.2": +"browserslist@npm:^4.0.0, browserslist@npm:^4.14.5, browserslist@npm:^4.21.10, browserslist@npm:^4.21.4, browserslist@npm:^4.22.2, browserslist@npm:^4.23.0": version: 4.23.0 resolution: "browserslist@npm:4.23.0" dependencies: @@ -12767,10 +12767,10 @@ __metadata: languageName: node linkType: hard -"caniuse-lite@npm:^1.0.0, caniuse-lite@npm:^1.0.30001578, caniuse-lite@npm:^1.0.30001587": - version: 1.0.30001591 - resolution: "caniuse-lite@npm:1.0.30001591" - checksum: 10/3891fad30a99b984a3a20570c0440d35dda933c79ea190cdb78a1f1743866506a4b41b4389b53a7c0351f2228125f9dc49308463f57e61503e5689b444add1a8 +"caniuse-lite@npm:^1.0.0, caniuse-lite@npm:^1.0.30001587, caniuse-lite@npm:^1.0.30001591": + version: 1.0.30001597 + resolution: "caniuse-lite@npm:1.0.30001597" + checksum: 10/44a268113faeee51e249cbcb3924dc3765f26cd527a134e3bb720ed20d50abd8b9291500a88beee061cc03ae9f15ddc9045d57e30d25a98efeaff4f7bb8965c1 languageName: node linkType: hard @@ -18444,7 +18444,7 @@ __metadata: angular-route: "npm:1.8.3" angular-sanitize: "npm:1.8.3" ansicolor: "npm:1.1.100" - autoprefixer: "npm:10.4.17" + autoprefixer: "npm:10.4.18" baron: "npm:3.0.3" blob-polyfill: "npm:7.0.20220408" brace: "npm:0.11.1" From b2b825db69571812a68ab5f9c1de7498261a7d33 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 11:02:10 +0000 Subject: [PATCH 0491/1406] Update dependency centrifuge to v5.0.2 --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 3517316d25..e4a7687507 100644 --- a/package.json +++ b/package.json @@ -300,7 +300,7 @@ "baron": "3.0.3", "brace": "0.11.1", "calculate-size": "1.1.1", - "centrifuge": "5.0.1", + "centrifuge": "5.0.2", "classnames": "2.5.1", "combokeys": "^3.0.0", "comlink": "4.4.1", diff --git a/yarn.lock b/yarn.lock index 6b622e64e3..792aa49fe6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12811,13 +12811,13 @@ __metadata: languageName: node linkType: hard -"centrifuge@npm:5.0.1": - version: 5.0.1 - resolution: "centrifuge@npm:5.0.1" +"centrifuge@npm:5.0.2": + version: 5.0.2 + resolution: "centrifuge@npm:5.0.2" dependencies: events: "npm:^3.3.0" protobufjs: "npm:^7.2.5" - checksum: 10/19457b9184208c889c5e30669d33ec60157063f35d4338576d8c1d68b0dddd9873bb12b571174503c547d7d6187c11a83b084261a97edee35a2e2e0f881e6bb0 + checksum: 10/d11e7448df3da8053102afb2bea0906c6e7335dfcc1a560240f60d93c8fc6a6be02c6c9e7db89e2796d2a1c1fe2b84719e673b627d300926f2f54eff4ae4da26 languageName: node linkType: hard @@ -18450,7 +18450,7 @@ __metadata: brace: "npm:0.11.1" browserslist: "npm:^4.21.4" calculate-size: "npm:1.1.1" - centrifuge: "npm:5.0.1" + centrifuge: "npm:5.0.2" chance: "npm:^1.0.10" chrome-remote-interface: "npm:0.33.0" classnames: "npm:2.5.1" From 8206a230617b1376e7b525070be8db05959174ff Mon Sep 17 00:00:00 2001 From: kay delaney <45561153+kaydelaney@users.noreply.github.com> Date: Mon, 11 Mar 2024 11:27:12 +0000 Subject: [PATCH 0492/1406] Scenes/Repeats: Show reduced panel menu for repeat panels (#84085) --- .../dashboard-scene/scene/PanelMenuBehavior.tsx | 14 ++++++++------ .../scene/PanelRepeaterGridItem.tsx | 14 +++++++++++--- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx b/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx index 61d958afce..de31e62467 100644 --- a/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx +++ b/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx @@ -33,7 +33,7 @@ import { UnlinkLibraryPanelModal } from './UnlinkLibraryPanelModal'; /** * Behavior is called when VizPanelMenu is activated (ie when it's opened). */ -export function panelMenuBehavior(menu: VizPanelMenu) { +export function panelMenuBehavior(menu: VizPanelMenu, isRepeat = false) { const asyncFunc = async () => { // hm.. add another generic param to SceneObject to specify parent type? // eslint-disable-next-line @typescript-eslint/consistent-type-assertions @@ -64,7 +64,7 @@ export function panelMenuBehavior(menu: VizPanelMenu) { href: getViewPanelUrl(panel), }); - if (dashboard.canEditDashboard()) { + if (dashboard.canEditDashboard() && !isRepeat) { // We could check isEditing here but I kind of think this should always be in the menu, // and going into panel edit should make the dashboard go into edit mode is it's not already items.push({ @@ -86,7 +86,7 @@ export function panelMenuBehavior(menu: VizPanelMenu) { shortcut: 'p s', }); - if (dashboard.state.isEditing) { + if (dashboard.state.isEditing && !isRepeat) { moreSubMenu.push({ text: t('panel.header-menu.duplicate', `Duplicate`), onClick: () => { @@ -105,7 +105,7 @@ export function panelMenuBehavior(menu: VizPanelMenu) { }, }); - if (dashboard.state.isEditing) { + if (dashboard.state.isEditing && !isRepeat) { if (parent instanceof LibraryVizPanel) { moreSubMenu.push({ text: t('panel.header-menu.unlink-library-panel', `Unlink library panel`), @@ -153,7 +153,7 @@ export function panelMenuBehavior(menu: VizPanelMenu) { }); } - if (dashboard.canEditDashboard() && plugin && !plugin.meta.skipDataQuery) { + if (dashboard.canEditDashboard() && plugin && !plugin.meta.skipDataQuery && !isRepeat) { moreSubMenu.push({ text: t('panel.header-menu.get-help', 'Get help'), onClick: (e: React.MouseEvent) => { @@ -200,7 +200,7 @@ export function panelMenuBehavior(menu: VizPanelMenu) { }); } - if (dashboard.state.isEditing) { + if (dashboard.state.isEditing && !isRepeat) { items.push({ text: '', type: 'divider', @@ -223,6 +223,8 @@ export function panelMenuBehavior(menu: VizPanelMenu) { asyncFunc(); } +export const repeatPanelMenuBehavior = (menu: VizPanelMenu) => panelMenuBehavior(menu, true); + async function getExploreMenuItem(panel: VizPanel): Promise { const exploreUrl = await tryGetExploreUrlForPanel(panel); if (!exploreUrl) { diff --git a/public/app/features/dashboard-scene/scene/PanelRepeaterGridItem.tsx b/public/app/features/dashboard-scene/scene/PanelRepeaterGridItem.tsx index 48745d21ad..d02442c207 100644 --- a/public/app/features/dashboard-scene/scene/PanelRepeaterGridItem.tsx +++ b/public/app/features/dashboard-scene/scene/PanelRepeaterGridItem.tsx @@ -15,12 +15,15 @@ import { MultiValueVariable, LocalValueVariable, CustomVariable, + VizPanelMenu, + VizPanelState, } from '@grafana/scenes'; import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN } from 'app/core/constants'; import { getMultiVariableValues } from '../utils/utils'; import { LibraryVizPanel } from './LibraryVizPanel'; +import { repeatPanelMenuBehavior } from './PanelMenuBehavior'; import { DashboardRepeatsProcessedEvent } from './types'; interface PanelRepeaterGridItemState extends SceneGridItemStateLike { @@ -107,15 +110,20 @@ export class PanelRepeaterGridItem extends SceneObjectBase = { $variables: new SceneVariableSet({ variables: [ new LocalValueVariable({ name: variable.state.name, value: values[index], text: String(texts[index]) }), ], }), key: `${panelToRepeat.state.key}-clone-${index}`, - }); - + }; + if (index > 0) { + cloneState.menu = new VizPanelMenu({ + $behaviors: [repeatPanelMenuBehavior], + }); + } + const clone = panelToRepeat.clone(cloneState); repeatedPanels.push(clone); } From 0280fac0e311d6fcd32e6f17927623095ada61ae Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Mon, 11 Mar 2024 11:35:59 +0000 Subject: [PATCH 0493/1406] DatePickerWithInput: Set zIndex for popover correctly (#84154) set zIndex for popover correctly --- .../DatePickerWithInput/DatePickerWithInput.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/grafana-ui/src/components/DateTimePickers/DatePickerWithInput/DatePickerWithInput.tsx b/packages/grafana-ui/src/components/DateTimePickers/DatePickerWithInput/DatePickerWithInput.tsx index 69933e8d64..515160fcee 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/DatePickerWithInput/DatePickerWithInput.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/DatePickerWithInput/DatePickerWithInput.tsx @@ -2,7 +2,7 @@ import { css } from '@emotion/css'; import { autoUpdate, flip, shift, useClick, useDismiss, useFloating, useInteractions } from '@floating-ui/react'; import React, { ChangeEvent, useState } from 'react'; -import { dateTime } from '@grafana/data'; +import { GrafanaTheme2, dateTime } from '@grafana/data'; import { useStyles2 } from '../../../themes'; import { Props as InputProps, Input } from '../../Input/Input'; @@ -100,7 +100,7 @@ export const DatePickerWithInput = ({ ); }; -const getStyles = () => { +const getStyles = (theme: GrafanaTheme2) => { return { container: css({ position: 'relative', @@ -113,7 +113,7 @@ const getStyles = () => { }, }), popover: css({ - zIndex: 1, + zIndex: theme.zIndex.tooltip, }), }; }; From 4fc5c0dc215e6e95dfdc5ee47c361a9471a7bb12 Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Mon, 11 Mar 2024 12:36:24 +0100 Subject: [PATCH 0494/1406] Chore: Remove Form components from TransformationsEditor (#83743) Chore: remove Form components from correlations --- .../Forms/TransformationsEditor.tsx | 90 ++++++++----------- 1 file changed, 37 insertions(+), 53 deletions(-) diff --git a/public/app/features/correlations/Forms/TransformationsEditor.tsx b/public/app/features/correlations/Forms/TransformationsEditor.tsx index f043731dda..3d83c43126 100644 --- a/public/app/features/correlations/Forms/TransformationsEditor.tsx +++ b/public/app/features/correlations/Forms/TransformationsEditor.tsx @@ -1,72 +1,56 @@ -import { css } from '@emotion/css'; import React from 'react'; -import { useFormContext } from 'react-hook-form'; +import { useFormContext, useFieldArray } from 'react-hook-form'; -import { GrafanaTheme2 } from '@grafana/data'; -import { Button, FieldArray, Stack, useStyles2 } from '@grafana/ui'; +import { Button, Stack, Text } from '@grafana/ui'; import { Trans } from 'app/core/internationalization'; import TransformationsEditorRow from './TransformationEditorRow'; type Props = { readOnly: boolean }; -const getStyles = (theme: GrafanaTheme2) => ({ - heading: css({ - fontSize: theme.typography.h5.fontSize, - fontWeight: theme.typography.fontWeightRegular, - }), -}); - export const TransformationsEditor = (props: Props) => { const { control, register } = useFormContext(); + const { fields, append, remove } = useFieldArray({ control, name: 'config.transformations' }); const { readOnly } = props; - const styles = useStyles2(getStyles); - return ( <> - - {({ fields, append, remove }) => ( - <> - -
- Transformations -
- {fields.length === 0 && ( -
- No transformations defined. -
- )} - {fields.length > 0 && ( -
- {fields.map((fieldVal, index) => { - return ( - - ); - })} -
- )} - {!readOnly && ( - - )} -
- + + + Transformations + + {fields.length === 0 && ( +
+ No transformations defined. +
+ )} + {fields.length > 0 && ( +
+ {fields.map((fieldVal, index) => { + return ( + + ); + })} +
+ )} + {!readOnly && ( + )} -
+ ); }; From ffe5c97b2d6af939a984d1084d0cad642f8d3361 Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Mon, 11 Mar 2024 12:36:36 +0100 Subject: [PATCH 0495/1406] Chore: Remove InputControl usage from explore (#83742) * Explore: Remove deprecated Form components from CorrelationTransformationAddModal.tsx * Explore: Remove deprecated Form components from AddToDashboardForm.tsx --- .../explore/CorrelationTransformationAddModal.tsx | 8 ++++---- .../extensions/AddToDashboard/AddToDashboardForm.tsx | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/public/app/features/explore/CorrelationTransformationAddModal.tsx b/public/app/features/explore/CorrelationTransformationAddModal.tsx index 94159958d2..f99414d589 100644 --- a/public/app/features/explore/CorrelationTransformationAddModal.tsx +++ b/public/app/features/explore/CorrelationTransformationAddModal.tsx @@ -1,10 +1,10 @@ import { css } from '@emotion/css'; import React, { useId, useState, useMemo, useEffect } from 'react'; import Highlighter from 'react-highlight-words'; -import { useForm } from 'react-hook-form'; +import { useForm, Controller } from 'react-hook-form'; import { DataLinkTransformationConfig, ScopedVars } from '@grafana/data'; -import { Button, Field, Icon, Input, InputControl, Label, Modal, Select, Tooltip, Stack } from '@grafana/ui'; +import { Button, Field, Icon, Input, Label, Modal, Select, Tooltip, Stack } from '@grafana/ui'; import { getSupportedTransTypeDetails, @@ -138,7 +138,7 @@ export const CorrelationTransformationAddModal = ({ field variables.

- ( {saveTargets.length > 1 && ( - ( @@ -171,7 +171,7 @@ export function AddToDashboardForm(props: Props): ReactElement { (() => { assertIsSaveToExistingDashboardError(errors); return ( - ( Date: Mon, 11 Mar 2024 12:40:26 +0100 Subject: [PATCH 0496/1406] CloudMigration: wires the service (#84081) --- pkg/registry/backgroundsvcs/background_services.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/registry/backgroundsvcs/background_services.go b/pkg/registry/backgroundsvcs/background_services.go index d31454198b..d24ccbedde 100644 --- a/pkg/registry/backgroundsvcs/background_services.go +++ b/pkg/registry/backgroundsvcs/background_services.go @@ -14,6 +14,7 @@ import ( grafanaapiserver "github.com/grafana/grafana/pkg/services/apiserver" "github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/cleanup" + "github.com/grafana/grafana/pkg/services/cloudmigration" "github.com/grafana/grafana/pkg/services/dashboardsnapshots" "github.com/grafana/grafana/pkg/services/grpcserver" "github.com/grafana/grafana/pkg/services/guardian" @@ -68,6 +69,7 @@ func ProvideBackgroundServiceRegistry( _ *plugindashboardsservice.DashboardUpdater, _ *sanitizer.Provider, _ *grpcserver.HealthService, _ entity.EntityStoreServer, _ *grpcserver.ReflectionService, _ *ldapapi.Service, _ *apiregistry.Service, _ auth.IDService, _ *teamapi.TeamAPI, _ ssosettings.Service, + _ cloudmigration.Service, ) *BackgroundServiceRegistry { return NewBackgroundServiceRegistry( httpServer, From bd9f9c9eb0c1540d4d3320a02a0f6db9c4640143 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 11:30:30 +0000 Subject: [PATCH 0497/1406] Update dependency immer to v10.0.4 --- package.json | 2 +- .../plugins/datasource/azuremonitor/package.json | 2 +- .../datasource/cloud-monitoring/package.json | 2 +- yarn.lock | 14 +++++++------- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index e4a7687507..b5e5037adf 100644 --- a/package.json +++ b/package.json @@ -322,7 +322,7 @@ "hoist-non-react-statics": "3.3.2", "i18next": "^23.0.0", "i18next-browser-languagedetector": "^7.0.2", - "immer": "10.0.3", + "immer": "10.0.4", "immutable": "4.3.5", "jquery": "3.7.1", "js-yaml": "^4.1.0", diff --git a/public/app/plugins/datasource/azuremonitor/package.json b/public/app/plugins/datasource/azuremonitor/package.json index f45b5e5a73..7736ee128f 100644 --- a/public/app/plugins/datasource/azuremonitor/package.json +++ b/public/app/plugins/datasource/azuremonitor/package.json @@ -13,7 +13,7 @@ "@kusto/monaco-kusto": "^7.4.0", "fast-deep-equal": "^3.1.3", "i18next": "^23.0.0", - "immer": "10.0.3", + "immer": "10.0.4", "lodash": "4.17.21", "monaco-editor": "0.34.0", "prismjs": "1.29.0", diff --git a/public/app/plugins/datasource/cloud-monitoring/package.json b/public/app/plugins/datasource/cloud-monitoring/package.json index eb900136bd..811dbd49f8 100644 --- a/public/app/plugins/datasource/cloud-monitoring/package.json +++ b/public/app/plugins/datasource/cloud-monitoring/package.json @@ -15,7 +15,7 @@ "debounce-promise": "3.1.2", "fast-deep-equal": "^3.1.3", "i18next": "^23.0.0", - "immer": "10.0.3", + "immer": "10.0.4", "lodash": "4.17.21", "monaco-editor": "0.34.0", "prismjs": "1.29.0", diff --git a/yarn.lock b/yarn.lock index 792aa49fe6..be61b0d47c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3237,7 +3237,7 @@ __metadata: "@types/testing-library__jest-dom": "npm:5.14.9" fast-deep-equal: "npm:^3.1.3" i18next: "npm:^23.0.0" - immer: "npm:10.0.3" + immer: "npm:10.0.4" lodash: "npm:4.17.21" monaco-editor: "npm:0.34.0" prismjs: "npm:1.29.0" @@ -3406,7 +3406,7 @@ __metadata: debounce-promise: "npm:3.1.2" fast-deep-equal: "npm:^3.1.3" i18next: "npm:^23.0.0" - immer: "npm:10.0.3" + immer: "npm:10.0.4" lodash: "npm:4.17.21" monaco-editor: "npm:0.34.0" prismjs: "npm:1.29.0" @@ -18502,7 +18502,7 @@ __metadata: i18next: "npm:^23.0.0" i18next-browser-languagedetector: "npm:^7.0.2" i18next-parser: "npm:8.12.0" - immer: "npm:10.0.3" + immer: "npm:10.0.4" immutable: "npm:4.3.5" jest: "npm:29.7.0" jest-canvas-mock: "npm:2.5.2" @@ -19479,10 +19479,10 @@ __metadata: languageName: node linkType: hard -"immer@npm:10.0.3": - version: 10.0.3 - resolution: "immer@npm:10.0.3" - checksum: 10/0be07be2f278bd1988112613648e0cf9a64fc316d5b4817f273b519fbfed0f1714275b041911f0b8c560c199b2e3430824ce620c23262c96c9d4efc9909ff1cc +"immer@npm:10.0.4": + version: 10.0.4 + resolution: "immer@npm:10.0.4" + checksum: 10/c1196783cf18e836527f970e45dc88e5472756401831e94d494ca5fd00ab7b39de3156f50505908207e7efe59ac23244d2cde0bd0cff791de7e23709771757e6 languageName: node linkType: hard From 275ccf994b0bab9ec6ad817b2fe9f84bc792ac1f Mon Sep 17 00:00:00 2001 From: Selene Date: Mon, 11 Mar 2024 12:51:44 +0100 Subject: [PATCH 0498/1406] Schemas: Reduce duplicated jenny code (#84061) * Remove jenny_ts_resources and use jenny_ts_types for all cases * Unify TS generated files into one jenny * Add missing imports to versioned files * Update Parca plugin * Fix loki * Use LokiQuery * Fix pyroscope tests * Fix prettier * :unamused: fix default pyroscope value name * Set the LokiQuery * Update Elasticsearch and TestData * Missed files from testdata * Order imports --- kinds/gen.go | 50 ++++++++- .../accesspolicy/x/accesspolicy_types.gen.ts | 2 +- .../x/AlertGroupsPanelCfg_types.gen.ts | 3 +- .../x/AnnotationsListPanelCfg_types.gen.ts | 3 +- .../x/AzureMonitorDataQuery_types.gen.ts | 3 +- .../panelcfg/x/BarChartPanelCfg_types.gen.ts | 3 +- .../panelcfg/x/BarGaugePanelCfg_types.gen.ts | 3 +- .../x/CandlestickPanelCfg_types.gen.ts | 3 +- .../panelcfg/x/CanvasPanelCfg_types.gen.ts | 3 +- .../x/CloudWatchDataQuery_types.gen.ts | 3 +- .../x/DashboardListPanelCfg_types.gen.ts | 3 +- .../panelcfg/x/DatagridPanelCfg_types.gen.ts | 3 +- .../panelcfg/x/DebugPanelCfg_types.gen.ts | 3 +- .../x/ElasticsearchDataQuery_types.gen.ts | 3 +- .../panelcfg/x/GaugePanelCfg_types.gen.ts | 3 +- .../panelcfg/x/GeomapPanelCfg_types.gen.ts | 3 +- ...oogleCloudMonitoringDataQuery_types.gen.ts | 3 +- .../x/GrafanaPyroscopeDataQuery_types.gen.ts | 3 +- .../panelcfg/x/HeatmapPanelCfg_types.gen.ts | 3 +- .../panelcfg/x/HistogramPanelCfg_types.gen.ts | 3 +- .../logs/panelcfg/x/LogsPanelCfg_types.gen.ts | 3 +- .../dataquery/x/LokiDataQuery_types.gen.ts | 3 +- .../news/panelcfg/x/NewsPanelCfg_types.gen.ts | 3 +- .../panelcfg/x/NodeGraphPanelCfg_types.gen.ts | 3 +- .../dataquery/x/ParcaDataQuery_types.gen.ts | 3 +- .../panelcfg/x/PieChartPanelCfg_types.gen.ts | 3 +- .../stat/panelcfg/x/StatPanelCfg_types.gen.ts | 3 +- .../x/StateTimelinePanelCfg_types.gen.ts | 3 +- .../x/StatusHistoryPanelCfg_types.gen.ts | 3 +- .../panelcfg/x/TablePanelCfg_types.gen.ts | 3 +- .../dataquery/x/TempoDataQuery_types.gen.ts | 3 +- .../x/TestDataDataQuery_types.gen.ts | 3 +- .../text/panelcfg/x/TextPanelCfg_types.gen.ts | 3 +- .../x/TimeSeriesPanelCfg_types.gen.ts | 3 +- .../panelcfg/x/TrendPanelCfg_types.gen.ts | 3 +- .../panelcfg/x/XYChartPanelCfg_types.gen.ts | 3 +- .../raw/dashboard/x/dashboard_types.gen.ts | 2 +- .../librarypanel/x/librarypanel_types.gen.ts | 2 +- .../preferences/x/preferences_types.gen.ts | 2 +- .../x/publicdashboard_types.gen.ts | 2 +- .../src/raw/role/x/role_types.gen.ts | 2 +- .../rolebinding/x/rolebinding_types.gen.ts | 2 +- .../src/raw/team/x/team_types.gen.ts | 2 +- pkg/codegen/jenny_eachmajor.go | 62 +++-------- pkg/codegen/jenny_ts_resources.go | 85 --------------- pkg/codegen/jenny_ts_types.go | 12 ++- pkg/plugins/codegen/jenny_pluginseachmajor.go | 101 ------------------ pkg/plugins/codegen/jenny_plugintstypes.go | 86 ++++++++++++--- .../datasource/azuremonitor/dataquery.gen.ts | 4 +- .../cloud-monitoring/dataquery.gen.ts | 4 +- .../datasource/cloudwatch/dataquery.gen.ts | 4 +- .../datasource/elasticsearch/dataquery.gen.ts | 6 +- .../plugins/datasource/elasticsearch/types.ts | 6 +- .../dataquery.gen.ts | 6 +- .../datasource.ts | 4 +- .../grafana-pyroscope-datasource/types.ts | 4 +- .../MetaDataInspector.tsx | 4 +- .../QueryEditor.tsx | 10 +- .../components/NodeGraphEditor.tsx | 4 +- .../components/RandomWalkEditor.tsx | 4 +- .../grafana-testdata-datasource/constants.ts | 4 +- .../dataquery.gen.ts | 6 +- .../grafana-testdata-datasource/datasource.ts | 54 ++++++---- .../grafana-testdata-datasource/runStreams.ts | 25 +++-- .../grafana-testdata-datasource/variables.ts | 4 +- .../plugins/datasource/loki/dataquery.gen.ts | 4 +- public/app/plugins/datasource/loki/types.ts | 7 +- .../parca/QueryEditor/QueryEditor.tsx | 4 +- .../plugins/datasource/parca/dataquery.gen.ts | 6 +- public/app/plugins/datasource/parca/types.ts | 2 +- .../plugins/datasource/tempo/dataquery.gen.ts | 4 +- public/app/plugins/gen.go | 3 +- .../plugins/panel/alertGroups/panelcfg.gen.ts | 2 +- .../plugins/panel/annolist/panelcfg.gen.ts | 2 +- .../plugins/panel/barchart/panelcfg.gen.ts | 2 +- .../plugins/panel/bargauge/panelcfg.gen.ts | 2 +- .../plugins/panel/candlestick/panelcfg.gen.ts | 2 +- .../app/plugins/panel/canvas/panelcfg.gen.ts | 2 +- .../plugins/panel/dashlist/panelcfg.gen.ts | 2 +- .../plugins/panel/datagrid/panelcfg.gen.ts | 2 +- .../app/plugins/panel/debug/panelcfg.gen.ts | 2 +- .../app/plugins/panel/gauge/panelcfg.gen.ts | 2 +- .../app/plugins/panel/geomap/panelcfg.gen.ts | 2 +- .../app/plugins/panel/heatmap/panelcfg.gen.ts | 2 +- .../plugins/panel/histogram/panelcfg.gen.ts | 2 +- public/app/plugins/panel/logs/panelcfg.gen.ts | 2 +- public/app/plugins/panel/news/panelcfg.gen.ts | 2 +- .../plugins/panel/nodeGraph/panelcfg.gen.ts | 2 +- .../plugins/panel/piechart/panelcfg.gen.ts | 2 +- public/app/plugins/panel/stat/panelcfg.gen.ts | 2 +- .../panel/state-timeline/panelcfg.gen.ts | 2 +- .../panel/status-history/panelcfg.gen.ts | 2 +- .../app/plugins/panel/table/panelcfg.gen.ts | 2 +- public/app/plugins/panel/text/panelcfg.gen.ts | 2 +- .../plugins/panel/timeseries/panelcfg.gen.ts | 2 +- .../app/plugins/panel/trend/panelcfg.gen.ts | 2 +- .../app/plugins/panel/xychart/panelcfg.gen.ts | 2 +- 97 files changed, 319 insertions(+), 428 deletions(-) delete mode 100644 pkg/codegen/jenny_ts_resources.go delete mode 100644 pkg/plugins/codegen/jenny_pluginseachmajor.go diff --git a/kinds/gen.go b/kinds/gen.go index 006b89eaf5..57abd5cc5d 100644 --- a/kinds/gen.go +++ b/kinds/gen.go @@ -19,6 +19,8 @@ import ( "cuelang.org/go/cue/errors" "github.com/grafana/codejen" "github.com/grafana/cuetsy" + "github.com/grafana/cuetsy/ts" + "github.com/grafana/cuetsy/ts/ast" "github.com/grafana/kindsys" "github.com/grafana/grafana/pkg/codegen" @@ -44,8 +46,7 @@ func main() { codegen.BaseCoreRegistryJenny(filepath.Join("pkg", "registry", "corekind"), cuectx.GoCoreKindParentPath), codegen.LatestMajorsOrXJenny( cuectx.TSCoreKindParentPath, - true, // forcing group so that we ignore the top level resource (for now) - codegen.TSResourceJenny{}), + codegen.TSTypesJenny{ApplyFuncs: []codegen.ApplyFunc{renameSpecNode}}), codegen.TSVeneerIndexJenny(filepath.Join("packages", "grafana-schema", "src")), ) @@ -232,3 +233,48 @@ func loadCueFiles(dirs []os.DirEntry) []cue.Value { return values } + +// renameSpecNode rename spec node from the TS file result +func renameSpecNode(sfg codegen.SchemaForGen, tf *ast.File) { + specidx, specdefidx := -1, -1 + for idx, def := range tf.Nodes { + // Peer through export keywords + if ex, is := def.(ast.ExportKeyword); is { + def = ex.Decl + } + + switch x := def.(type) { + case ast.TypeDecl: + if x.Name.Name == "spec" { + specidx = idx + x.Name.Name = sfg.Name + tf.Nodes[idx] = x + } + case ast.VarDecl: + // Before: + // export const defaultspec: Partial = { + // After: + // / export const defaultPlaylist: Partial = { + if x.Names.Idents[0].Name == "defaultspec" { + specdefidx = idx + x.Names.Idents[0].Name = "default" + sfg.Name + tt := x.Type.(ast.TypeTransformExpr) + tt.Expr = ts.Ident(sfg.Name) + x.Type = tt + tf.Nodes[idx] = x + } + } + } + + if specidx != -1 { + decl := tf.Nodes[specidx] + tf.Nodes = append(append(tf.Nodes[:specidx], tf.Nodes[specidx+1:]...), decl) + } + if specdefidx != -1 { + if specdefidx > specidx { + specdefidx-- + } + decl := tf.Nodes[specdefidx] + tf.Nodes = append(append(tf.Nodes[:specdefidx], tf.Nodes[specdefidx+1:]...), decl) + } +} diff --git a/packages/grafana-schema/src/raw/accesspolicy/x/accesspolicy_types.gen.ts b/packages/grafana-schema/src/raw/accesspolicy/x/accesspolicy_types.gen.ts index 814c5cf3ea..7e18ea5077 100644 --- a/packages/grafana-schema/src/raw/accesspolicy/x/accesspolicy_types.gen.ts +++ b/packages/grafana-schema/src/raw/accesspolicy/x/accesspolicy_types.gen.ts @@ -3,7 +3,7 @@ // Generated by: // kinds/gen.go // Using jennies: -// TSResourceJenny +// TSTypesJenny // LatestMajorsOrXJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/packages/grafana-schema/src/raw/composable/alertgroups/panelcfg/x/AlertGroupsPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/alertgroups/panelcfg/x/AlertGroupsPanelCfg_types.gen.ts index 06806b81de..712215d30c 100644 --- a/packages/grafana-schema/src/raw/composable/alertgroups/panelcfg/x/AlertGroupsPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/alertgroups/panelcfg/x/AlertGroupsPanelCfg_types.gen.ts @@ -4,8 +4,7 @@ // public/app/plugins/gen.go // Using jennies: // TSTypesJenny -// LatestMajorsOrXJenny -// PluginEachMajorJenny +// PluginTsTypesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/packages/grafana-schema/src/raw/composable/annotationslist/panelcfg/x/AnnotationsListPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/annotationslist/panelcfg/x/AnnotationsListPanelCfg_types.gen.ts index 00ad8874bc..e10efab261 100644 --- a/packages/grafana-schema/src/raw/composable/annotationslist/panelcfg/x/AnnotationsListPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/annotationslist/panelcfg/x/AnnotationsListPanelCfg_types.gen.ts @@ -4,8 +4,7 @@ // public/app/plugins/gen.go // Using jennies: // TSTypesJenny -// LatestMajorsOrXJenny -// PluginEachMajorJenny +// PluginTsTypesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/packages/grafana-schema/src/raw/composable/azuremonitor/dataquery/x/AzureMonitorDataQuery_types.gen.ts b/packages/grafana-schema/src/raw/composable/azuremonitor/dataquery/x/AzureMonitorDataQuery_types.gen.ts index a22f7674cb..d6d068b7e1 100644 --- a/packages/grafana-schema/src/raw/composable/azuremonitor/dataquery/x/AzureMonitorDataQuery_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/azuremonitor/dataquery/x/AzureMonitorDataQuery_types.gen.ts @@ -4,8 +4,7 @@ // public/app/plugins/gen.go // Using jennies: // TSTypesJenny -// LatestMajorsOrXJenny -// PluginEachMajorJenny +// PluginTsTypesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/packages/grafana-schema/src/raw/composable/barchart/panelcfg/x/BarChartPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/barchart/panelcfg/x/BarChartPanelCfg_types.gen.ts index 595b7075ec..2a50f0f9e9 100644 --- a/packages/grafana-schema/src/raw/composable/barchart/panelcfg/x/BarChartPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/barchart/panelcfg/x/BarChartPanelCfg_types.gen.ts @@ -4,8 +4,7 @@ // public/app/plugins/gen.go // Using jennies: // TSTypesJenny -// LatestMajorsOrXJenny -// PluginEachMajorJenny +// PluginTsTypesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/packages/grafana-schema/src/raw/composable/bargauge/panelcfg/x/BarGaugePanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/bargauge/panelcfg/x/BarGaugePanelCfg_types.gen.ts index e06085a743..be73664fe4 100644 --- a/packages/grafana-schema/src/raw/composable/bargauge/panelcfg/x/BarGaugePanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/bargauge/panelcfg/x/BarGaugePanelCfg_types.gen.ts @@ -4,8 +4,7 @@ // public/app/plugins/gen.go // Using jennies: // TSTypesJenny -// LatestMajorsOrXJenny -// PluginEachMajorJenny +// PluginTsTypesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/packages/grafana-schema/src/raw/composable/candlestick/panelcfg/x/CandlestickPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/candlestick/panelcfg/x/CandlestickPanelCfg_types.gen.ts index 5ab9a15028..9a89d9b2d9 100644 --- a/packages/grafana-schema/src/raw/composable/candlestick/panelcfg/x/CandlestickPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/candlestick/panelcfg/x/CandlestickPanelCfg_types.gen.ts @@ -4,8 +4,7 @@ // public/app/plugins/gen.go // Using jennies: // TSTypesJenny -// LatestMajorsOrXJenny -// PluginEachMajorJenny +// PluginTsTypesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/packages/grafana-schema/src/raw/composable/canvas/panelcfg/x/CanvasPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/canvas/panelcfg/x/CanvasPanelCfg_types.gen.ts index 7a11db51c3..c29f34f217 100644 --- a/packages/grafana-schema/src/raw/composable/canvas/panelcfg/x/CanvasPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/canvas/panelcfg/x/CanvasPanelCfg_types.gen.ts @@ -4,8 +4,7 @@ // public/app/plugins/gen.go // Using jennies: // TSTypesJenny -// LatestMajorsOrXJenny -// PluginEachMajorJenny +// PluginTsTypesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/packages/grafana-schema/src/raw/composable/cloudwatch/dataquery/x/CloudWatchDataQuery_types.gen.ts b/packages/grafana-schema/src/raw/composable/cloudwatch/dataquery/x/CloudWatchDataQuery_types.gen.ts index b9488a5bd8..f184ea7b49 100644 --- a/packages/grafana-schema/src/raw/composable/cloudwatch/dataquery/x/CloudWatchDataQuery_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/cloudwatch/dataquery/x/CloudWatchDataQuery_types.gen.ts @@ -4,8 +4,7 @@ // public/app/plugins/gen.go // Using jennies: // TSTypesJenny -// LatestMajorsOrXJenny -// PluginEachMajorJenny +// PluginTsTypesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/packages/grafana-schema/src/raw/composable/dashboardlist/panelcfg/x/DashboardListPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/dashboardlist/panelcfg/x/DashboardListPanelCfg_types.gen.ts index e3c41bb3c7..2fba962cd8 100644 --- a/packages/grafana-schema/src/raw/composable/dashboardlist/panelcfg/x/DashboardListPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/dashboardlist/panelcfg/x/DashboardListPanelCfg_types.gen.ts @@ -4,8 +4,7 @@ // public/app/plugins/gen.go // Using jennies: // TSTypesJenny -// LatestMajorsOrXJenny -// PluginEachMajorJenny +// PluginTsTypesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/packages/grafana-schema/src/raw/composable/datagrid/panelcfg/x/DatagridPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/datagrid/panelcfg/x/DatagridPanelCfg_types.gen.ts index 051a38d9a4..77016b81ef 100644 --- a/packages/grafana-schema/src/raw/composable/datagrid/panelcfg/x/DatagridPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/datagrid/panelcfg/x/DatagridPanelCfg_types.gen.ts @@ -4,8 +4,7 @@ // public/app/plugins/gen.go // Using jennies: // TSTypesJenny -// LatestMajorsOrXJenny -// PluginEachMajorJenny +// PluginTsTypesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/packages/grafana-schema/src/raw/composable/debug/panelcfg/x/DebugPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/debug/panelcfg/x/DebugPanelCfg_types.gen.ts index 874944a2d3..8634d6e0b8 100644 --- a/packages/grafana-schema/src/raw/composable/debug/panelcfg/x/DebugPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/debug/panelcfg/x/DebugPanelCfg_types.gen.ts @@ -4,8 +4,7 @@ // public/app/plugins/gen.go // Using jennies: // TSTypesJenny -// LatestMajorsOrXJenny -// PluginEachMajorJenny +// PluginTsTypesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/packages/grafana-schema/src/raw/composable/elasticsearch/dataquery/x/ElasticsearchDataQuery_types.gen.ts b/packages/grafana-schema/src/raw/composable/elasticsearch/dataquery/x/ElasticsearchDataQuery_types.gen.ts index 76aa76dedf..2aeca4e52f 100644 --- a/packages/grafana-schema/src/raw/composable/elasticsearch/dataquery/x/ElasticsearchDataQuery_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/elasticsearch/dataquery/x/ElasticsearchDataQuery_types.gen.ts @@ -4,8 +4,7 @@ // public/app/plugins/gen.go // Using jennies: // TSTypesJenny -// LatestMajorsOrXJenny -// PluginEachMajorJenny +// PluginTsTypesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/packages/grafana-schema/src/raw/composable/gauge/panelcfg/x/GaugePanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/gauge/panelcfg/x/GaugePanelCfg_types.gen.ts index 1bd5575b10..dc14651339 100644 --- a/packages/grafana-schema/src/raw/composable/gauge/panelcfg/x/GaugePanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/gauge/panelcfg/x/GaugePanelCfg_types.gen.ts @@ -4,8 +4,7 @@ // public/app/plugins/gen.go // Using jennies: // TSTypesJenny -// LatestMajorsOrXJenny -// PluginEachMajorJenny +// PluginTsTypesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/packages/grafana-schema/src/raw/composable/geomap/panelcfg/x/GeomapPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/geomap/panelcfg/x/GeomapPanelCfg_types.gen.ts index ae79a22fb0..a842a04bbf 100644 --- a/packages/grafana-schema/src/raw/composable/geomap/panelcfg/x/GeomapPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/geomap/panelcfg/x/GeomapPanelCfg_types.gen.ts @@ -4,8 +4,7 @@ // public/app/plugins/gen.go // Using jennies: // TSTypesJenny -// LatestMajorsOrXJenny -// PluginEachMajorJenny +// PluginTsTypesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/packages/grafana-schema/src/raw/composable/googlecloudmonitoring/dataquery/x/GoogleCloudMonitoringDataQuery_types.gen.ts b/packages/grafana-schema/src/raw/composable/googlecloudmonitoring/dataquery/x/GoogleCloudMonitoringDataQuery_types.gen.ts index fd333a2f0e..0185e3000b 100644 --- a/packages/grafana-schema/src/raw/composable/googlecloudmonitoring/dataquery/x/GoogleCloudMonitoringDataQuery_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/googlecloudmonitoring/dataquery/x/GoogleCloudMonitoringDataQuery_types.gen.ts @@ -4,8 +4,7 @@ // public/app/plugins/gen.go // Using jennies: // TSTypesJenny -// LatestMajorsOrXJenny -// PluginEachMajorJenny +// PluginTsTypesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/packages/grafana-schema/src/raw/composable/grafanapyroscope/dataquery/x/GrafanaPyroscopeDataQuery_types.gen.ts b/packages/grafana-schema/src/raw/composable/grafanapyroscope/dataquery/x/GrafanaPyroscopeDataQuery_types.gen.ts index 2be0dcface..1c5e39ee71 100644 --- a/packages/grafana-schema/src/raw/composable/grafanapyroscope/dataquery/x/GrafanaPyroscopeDataQuery_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/grafanapyroscope/dataquery/x/GrafanaPyroscopeDataQuery_types.gen.ts @@ -4,8 +4,7 @@ // public/app/plugins/gen.go // Using jennies: // TSTypesJenny -// LatestMajorsOrXJenny -// PluginEachMajorJenny +// PluginTsTypesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/packages/grafana-schema/src/raw/composable/heatmap/panelcfg/x/HeatmapPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/heatmap/panelcfg/x/HeatmapPanelCfg_types.gen.ts index b0a369a3af..97ef088e2c 100644 --- a/packages/grafana-schema/src/raw/composable/heatmap/panelcfg/x/HeatmapPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/heatmap/panelcfg/x/HeatmapPanelCfg_types.gen.ts @@ -4,8 +4,7 @@ // public/app/plugins/gen.go // Using jennies: // TSTypesJenny -// LatestMajorsOrXJenny -// PluginEachMajorJenny +// PluginTsTypesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/packages/grafana-schema/src/raw/composable/histogram/panelcfg/x/HistogramPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/histogram/panelcfg/x/HistogramPanelCfg_types.gen.ts index ed246a7a64..3d1b420d49 100644 --- a/packages/grafana-schema/src/raw/composable/histogram/panelcfg/x/HistogramPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/histogram/panelcfg/x/HistogramPanelCfg_types.gen.ts @@ -4,8 +4,7 @@ // public/app/plugins/gen.go // Using jennies: // TSTypesJenny -// LatestMajorsOrXJenny -// PluginEachMajorJenny +// PluginTsTypesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/packages/grafana-schema/src/raw/composable/logs/panelcfg/x/LogsPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/logs/panelcfg/x/LogsPanelCfg_types.gen.ts index 449bebb758..6c53480ae0 100644 --- a/packages/grafana-schema/src/raw/composable/logs/panelcfg/x/LogsPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/logs/panelcfg/x/LogsPanelCfg_types.gen.ts @@ -4,8 +4,7 @@ // public/app/plugins/gen.go // Using jennies: // TSTypesJenny -// LatestMajorsOrXJenny -// PluginEachMajorJenny +// PluginTsTypesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/packages/grafana-schema/src/raw/composable/loki/dataquery/x/LokiDataQuery_types.gen.ts b/packages/grafana-schema/src/raw/composable/loki/dataquery/x/LokiDataQuery_types.gen.ts index 704c53846f..738ccee4c1 100644 --- a/packages/grafana-schema/src/raw/composable/loki/dataquery/x/LokiDataQuery_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/loki/dataquery/x/LokiDataQuery_types.gen.ts @@ -4,8 +4,7 @@ // public/app/plugins/gen.go // Using jennies: // TSTypesJenny -// LatestMajorsOrXJenny -// PluginEachMajorJenny +// PluginTsTypesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/packages/grafana-schema/src/raw/composable/news/panelcfg/x/NewsPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/news/panelcfg/x/NewsPanelCfg_types.gen.ts index 17906a95d1..22c3031560 100644 --- a/packages/grafana-schema/src/raw/composable/news/panelcfg/x/NewsPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/news/panelcfg/x/NewsPanelCfg_types.gen.ts @@ -4,8 +4,7 @@ // public/app/plugins/gen.go // Using jennies: // TSTypesJenny -// LatestMajorsOrXJenny -// PluginEachMajorJenny +// PluginTsTypesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/packages/grafana-schema/src/raw/composable/nodegraph/panelcfg/x/NodeGraphPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/nodegraph/panelcfg/x/NodeGraphPanelCfg_types.gen.ts index f1765fff6b..b1b404140b 100644 --- a/packages/grafana-schema/src/raw/composable/nodegraph/panelcfg/x/NodeGraphPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/nodegraph/panelcfg/x/NodeGraphPanelCfg_types.gen.ts @@ -4,8 +4,7 @@ // public/app/plugins/gen.go // Using jennies: // TSTypesJenny -// LatestMajorsOrXJenny -// PluginEachMajorJenny +// PluginTsTypesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/packages/grafana-schema/src/raw/composable/parca/dataquery/x/ParcaDataQuery_types.gen.ts b/packages/grafana-schema/src/raw/composable/parca/dataquery/x/ParcaDataQuery_types.gen.ts index a4d6fd645a..61aeb06a3d 100644 --- a/packages/grafana-schema/src/raw/composable/parca/dataquery/x/ParcaDataQuery_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/parca/dataquery/x/ParcaDataQuery_types.gen.ts @@ -4,8 +4,7 @@ // public/app/plugins/gen.go // Using jennies: // TSTypesJenny -// LatestMajorsOrXJenny -// PluginEachMajorJenny +// PluginTsTypesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/packages/grafana-schema/src/raw/composable/piechart/panelcfg/x/PieChartPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/piechart/panelcfg/x/PieChartPanelCfg_types.gen.ts index d51f2ddba2..5b1987582b 100644 --- a/packages/grafana-schema/src/raw/composable/piechart/panelcfg/x/PieChartPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/piechart/panelcfg/x/PieChartPanelCfg_types.gen.ts @@ -4,8 +4,7 @@ // public/app/plugins/gen.go // Using jennies: // TSTypesJenny -// LatestMajorsOrXJenny -// PluginEachMajorJenny +// PluginTsTypesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/packages/grafana-schema/src/raw/composable/stat/panelcfg/x/StatPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/stat/panelcfg/x/StatPanelCfg_types.gen.ts index 64c0081af9..03a4a6db04 100644 --- a/packages/grafana-schema/src/raw/composable/stat/panelcfg/x/StatPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/stat/panelcfg/x/StatPanelCfg_types.gen.ts @@ -4,8 +4,7 @@ // public/app/plugins/gen.go // Using jennies: // TSTypesJenny -// LatestMajorsOrXJenny -// PluginEachMajorJenny +// PluginTsTypesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/packages/grafana-schema/src/raw/composable/statetimeline/panelcfg/x/StateTimelinePanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/statetimeline/panelcfg/x/StateTimelinePanelCfg_types.gen.ts index d42e8c3db8..f4cbf92bc0 100644 --- a/packages/grafana-schema/src/raw/composable/statetimeline/panelcfg/x/StateTimelinePanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/statetimeline/panelcfg/x/StateTimelinePanelCfg_types.gen.ts @@ -4,8 +4,7 @@ // public/app/plugins/gen.go // Using jennies: // TSTypesJenny -// LatestMajorsOrXJenny -// PluginEachMajorJenny +// PluginTsTypesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/packages/grafana-schema/src/raw/composable/statushistory/panelcfg/x/StatusHistoryPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/statushistory/panelcfg/x/StatusHistoryPanelCfg_types.gen.ts index bdb705b252..a9900905d1 100644 --- a/packages/grafana-schema/src/raw/composable/statushistory/panelcfg/x/StatusHistoryPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/statushistory/panelcfg/x/StatusHistoryPanelCfg_types.gen.ts @@ -4,8 +4,7 @@ // public/app/plugins/gen.go // Using jennies: // TSTypesJenny -// LatestMajorsOrXJenny -// PluginEachMajorJenny +// PluginTsTypesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/packages/grafana-schema/src/raw/composable/table/panelcfg/x/TablePanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/table/panelcfg/x/TablePanelCfg_types.gen.ts index 263ab24700..dc2f9f23d8 100644 --- a/packages/grafana-schema/src/raw/composable/table/panelcfg/x/TablePanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/table/panelcfg/x/TablePanelCfg_types.gen.ts @@ -4,8 +4,7 @@ // public/app/plugins/gen.go // Using jennies: // TSTypesJenny -// LatestMajorsOrXJenny -// PluginEachMajorJenny +// PluginTsTypesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/packages/grafana-schema/src/raw/composable/tempo/dataquery/x/TempoDataQuery_types.gen.ts b/packages/grafana-schema/src/raw/composable/tempo/dataquery/x/TempoDataQuery_types.gen.ts index 3371fbaa7b..2b6825e460 100644 --- a/packages/grafana-schema/src/raw/composable/tempo/dataquery/x/TempoDataQuery_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/tempo/dataquery/x/TempoDataQuery_types.gen.ts @@ -4,8 +4,7 @@ // public/app/plugins/gen.go // Using jennies: // TSTypesJenny -// LatestMajorsOrXJenny -// PluginEachMajorJenny +// PluginTsTypesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/packages/grafana-schema/src/raw/composable/testdata/dataquery/x/TestDataDataQuery_types.gen.ts b/packages/grafana-schema/src/raw/composable/testdata/dataquery/x/TestDataDataQuery_types.gen.ts index 91f7cbe3bd..329a6feafd 100644 --- a/packages/grafana-schema/src/raw/composable/testdata/dataquery/x/TestDataDataQuery_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/testdata/dataquery/x/TestDataDataQuery_types.gen.ts @@ -4,8 +4,7 @@ // public/app/plugins/gen.go // Using jennies: // TSTypesJenny -// LatestMajorsOrXJenny -// PluginEachMajorJenny +// PluginTsTypesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/packages/grafana-schema/src/raw/composable/text/panelcfg/x/TextPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/text/panelcfg/x/TextPanelCfg_types.gen.ts index 0bf302eda0..0c6d63f1cc 100644 --- a/packages/grafana-schema/src/raw/composable/text/panelcfg/x/TextPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/text/panelcfg/x/TextPanelCfg_types.gen.ts @@ -4,8 +4,7 @@ // public/app/plugins/gen.go // Using jennies: // TSTypesJenny -// LatestMajorsOrXJenny -// PluginEachMajorJenny +// PluginTsTypesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/packages/grafana-schema/src/raw/composable/timeseries/panelcfg/x/TimeSeriesPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/timeseries/panelcfg/x/TimeSeriesPanelCfg_types.gen.ts index 3a5d60e318..cc065b62af 100644 --- a/packages/grafana-schema/src/raw/composable/timeseries/panelcfg/x/TimeSeriesPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/timeseries/panelcfg/x/TimeSeriesPanelCfg_types.gen.ts @@ -4,8 +4,7 @@ // public/app/plugins/gen.go // Using jennies: // TSTypesJenny -// LatestMajorsOrXJenny -// PluginEachMajorJenny +// PluginTsTypesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/packages/grafana-schema/src/raw/composable/trend/panelcfg/x/TrendPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/trend/panelcfg/x/TrendPanelCfg_types.gen.ts index b30275ca3b..2e19671941 100644 --- a/packages/grafana-schema/src/raw/composable/trend/panelcfg/x/TrendPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/trend/panelcfg/x/TrendPanelCfg_types.gen.ts @@ -4,8 +4,7 @@ // public/app/plugins/gen.go // Using jennies: // TSTypesJenny -// LatestMajorsOrXJenny -// PluginEachMajorJenny +// PluginTsTypesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/packages/grafana-schema/src/raw/composable/xychart/panelcfg/x/XYChartPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/xychart/panelcfg/x/XYChartPanelCfg_types.gen.ts index ba3a3686d6..c9a7b21117 100644 --- a/packages/grafana-schema/src/raw/composable/xychart/panelcfg/x/XYChartPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/xychart/panelcfg/x/XYChartPanelCfg_types.gen.ts @@ -4,8 +4,7 @@ // public/app/plugins/gen.go // Using jennies: // TSTypesJenny -// LatestMajorsOrXJenny -// PluginEachMajorJenny +// PluginTsTypesJenny // // Run 'make gen-cue' from repository root to regenerate. 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 886dd0e346..9b3d3b5450 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 @@ -3,7 +3,7 @@ // Generated by: // kinds/gen.go // Using jennies: -// TSResourceJenny +// TSTypesJenny // LatestMajorsOrXJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/packages/grafana-schema/src/raw/librarypanel/x/librarypanel_types.gen.ts b/packages/grafana-schema/src/raw/librarypanel/x/librarypanel_types.gen.ts index d3b967e7a7..97cba2a5c5 100644 --- a/packages/grafana-schema/src/raw/librarypanel/x/librarypanel_types.gen.ts +++ b/packages/grafana-schema/src/raw/librarypanel/x/librarypanel_types.gen.ts @@ -3,7 +3,7 @@ // Generated by: // kinds/gen.go // Using jennies: -// TSResourceJenny +// TSTypesJenny // LatestMajorsOrXJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/packages/grafana-schema/src/raw/preferences/x/preferences_types.gen.ts b/packages/grafana-schema/src/raw/preferences/x/preferences_types.gen.ts index 033e4233d4..b685f299f2 100644 --- a/packages/grafana-schema/src/raw/preferences/x/preferences_types.gen.ts +++ b/packages/grafana-schema/src/raw/preferences/x/preferences_types.gen.ts @@ -3,7 +3,7 @@ // Generated by: // kinds/gen.go // Using jennies: -// TSResourceJenny +// TSTypesJenny // LatestMajorsOrXJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/packages/grafana-schema/src/raw/publicdashboard/x/publicdashboard_types.gen.ts b/packages/grafana-schema/src/raw/publicdashboard/x/publicdashboard_types.gen.ts index 9a1ae8596f..b94e4baefe 100644 --- a/packages/grafana-schema/src/raw/publicdashboard/x/publicdashboard_types.gen.ts +++ b/packages/grafana-schema/src/raw/publicdashboard/x/publicdashboard_types.gen.ts @@ -3,7 +3,7 @@ // Generated by: // kinds/gen.go // Using jennies: -// TSResourceJenny +// TSTypesJenny // LatestMajorsOrXJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/packages/grafana-schema/src/raw/role/x/role_types.gen.ts b/packages/grafana-schema/src/raw/role/x/role_types.gen.ts index 781665e925..88cadf639f 100644 --- a/packages/grafana-schema/src/raw/role/x/role_types.gen.ts +++ b/packages/grafana-schema/src/raw/role/x/role_types.gen.ts @@ -3,7 +3,7 @@ // Generated by: // kinds/gen.go // Using jennies: -// TSResourceJenny +// TSTypesJenny // LatestMajorsOrXJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/packages/grafana-schema/src/raw/rolebinding/x/rolebinding_types.gen.ts b/packages/grafana-schema/src/raw/rolebinding/x/rolebinding_types.gen.ts index a41535cc5d..ea6d65fbf4 100644 --- a/packages/grafana-schema/src/raw/rolebinding/x/rolebinding_types.gen.ts +++ b/packages/grafana-schema/src/raw/rolebinding/x/rolebinding_types.gen.ts @@ -3,7 +3,7 @@ // Generated by: // kinds/gen.go // Using jennies: -// TSResourceJenny +// TSTypesJenny // LatestMajorsOrXJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/packages/grafana-schema/src/raw/team/x/team_types.gen.ts b/packages/grafana-schema/src/raw/team/x/team_types.gen.ts index fef0a47a87..4f136372c3 100644 --- a/packages/grafana-schema/src/raw/team/x/team_types.gen.ts +++ b/packages/grafana-schema/src/raw/team/x/team_types.gen.ts @@ -3,7 +3,7 @@ // Generated by: // kinds/gen.go // Using jennies: -// TSResourceJenny +// TSTypesJenny // LatestMajorsOrXJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/pkg/codegen/jenny_eachmajor.go b/pkg/codegen/jenny_eachmajor.go index 0bc3d1b8c2..e2c60648e3 100644 --- a/pkg/codegen/jenny_eachmajor.go +++ b/pkg/codegen/jenny_eachmajor.go @@ -11,22 +11,20 @@ import ( // LatestMajorsOrXJenny returns a jenny that repeats the input for the latest in each major version. // // TODO remove forceGroup option, it's a temporary hack to accommodate core kinds -func LatestMajorsOrXJenny(parentdir string, forceGroup bool, inner codejen.OneToOne[SchemaForGen]) OneToMany { +func LatestMajorsOrXJenny(parentdir string, inner codejen.OneToOne[SchemaForGen]) OneToMany { if inner == nil { panic("inner jenny must not be nil") } return &lmox{ - parentdir: parentdir, - inner: inner, - forceGroup: forceGroup, + parentdir: parentdir, + inner: inner, } } type lmox struct { - parentdir string - inner codejen.OneToOne[SchemaForGen] - forceGroup bool + parentdir string + inner codejen.OneToOne[SchemaForGen] } func (j *lmox) JennyName() string { @@ -42,49 +40,19 @@ func (j *lmox) Generate(kind kindsys.Kind) (codejen.Files, error) { comm := kind.Props().Common() sfg := SchemaForGen{ Name: comm.Name, - IsGroup: comm.LineageIsGroup, + IsGroup: true, + Schema: kind.Lineage().Latest(), } - if j.forceGroup { - sfg.IsGroup = true + f, err := j.inner.Generate(sfg) + if err != nil { + return nil, fmt.Errorf("%s jenny failed on %s schema for %s: %w", j.inner.JennyName(), sfg.Schema.Version(), kind.Props().Common().Name, err) } - - do := func(sfg SchemaForGen, infix string) (codejen.Files, error) { - f, err := j.inner.Generate(sfg) - if err != nil { - return nil, fmt.Errorf("%s jenny failed on %s schema for %s: %w", j.inner.JennyName(), sfg.Schema.Version(), kind.Props().Common().Name, err) - } - if f == nil || !f.Exists() { - return nil, nil - } - - f.RelativePath = filepath.Join(j.parentdir, comm.MachineName, infix, f.RelativePath) - f.From = append(f.From, j) - return codejen.Files{*f}, nil - } - - if comm.Maturity.Less(kindsys.MaturityStable) { - sfg.Schema = kind.Lineage().Latest() - return do(sfg, "x") + if f == nil || !f.Exists() { + return nil, nil } - 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 { - return nil, err - } - fl = append(fl, files...) - } - if fl.Validate() != nil { - return nil, fl.Validate() - } - return fl, nil + f.RelativePath = filepath.Join(j.parentdir, comm.MachineName, "x", f.RelativePath) + f.From = append(f.From, j) + return codejen.Files{*f}, nil } diff --git a/pkg/codegen/jenny_ts_resources.go b/pkg/codegen/jenny_ts_resources.go deleted file mode 100644 index 9e3391a627..0000000000 --- a/pkg/codegen/jenny_ts_resources.go +++ /dev/null @@ -1,85 +0,0 @@ -package codegen - -import ( - "github.com/grafana/codejen" - "github.com/grafana/cuetsy" - "github.com/grafana/cuetsy/ts" - "github.com/grafana/cuetsy/ts/ast" - "github.com/grafana/grafana/pkg/cuectx" - "github.com/grafana/thema/encoding/typescript" -) - -// TSResourceJenny is a [OneToOne] that produces TypeScript types and -// defaults for a Thema schema. -// -// Thema's generic TS jenny will be able to replace this one once -// https://github.com/grafana/thema/issues/89 is complete. -type TSResourceJenny struct{} - -var _ codejen.OneToOne[SchemaForGen] = &TSResourceJenny{} - -func (j TSResourceJenny) JennyName() string { - return "TSResourceJenny" -} - -func (j TSResourceJenny) Generate(sfg SchemaForGen) (*codejen.File, error) { - // TODO allow using name instead of machine name in thema generator - f, err := typescript.GenerateTypes(sfg.Schema, &typescript.TypeConfig{ - RootName: sfg.Name, - Group: sfg.IsGroup, - CuetsyConfig: &cuetsy.Config{ - Export: true, - ImportMapper: cuectx.MapCUEImportToTS, - }, - }) - if err != nil { - return nil, err - } - renameSpecNode(sfg.Name, f) - - return codejen.NewFile(sfg.Schema.Lineage().Name()+"_types.gen.ts", []byte(f.String()), j), nil -} - -func renameSpecNode(name string, tf *ast.File) { - specidx, specdefidx := -1, -1 - for idx, def := range tf.Nodes { - // Peer through export keywords - if ex, is := def.(ast.ExportKeyword); is { - def = ex.Decl - } - - switch x := def.(type) { - case ast.TypeDecl: - if x.Name.Name == "spec" { - specidx = idx - x.Name.Name = name - tf.Nodes[idx] = x - } - case ast.VarDecl: - // Before: - // export const defaultspec: Partial = { - // After: - /// export const defaultPlaylist: Partial = { - if x.Names.Idents[0].Name == "defaultspec" { - specdefidx = idx - x.Names.Idents[0].Name = "default" + name - tt := x.Type.(ast.TypeTransformExpr) - tt.Expr = ts.Ident(name) - x.Type = tt - tf.Nodes[idx] = x - } - } - } - - if specidx != -1 { - decl := tf.Nodes[specidx] - tf.Nodes = append(append(tf.Nodes[:specidx], tf.Nodes[specidx+1:]...), decl) - } - if specdefidx != -1 { - if specdefidx > specidx { - specdefidx-- - } - decl := tf.Nodes[specdefidx] - tf.Nodes = append(append(tf.Nodes[:specdefidx], tf.Nodes[specdefidx+1:]...), decl) - } -} diff --git a/pkg/codegen/jenny_ts_types.go b/pkg/codegen/jenny_ts_types.go index d16968a45c..3eb5b078d6 100644 --- a/pkg/codegen/jenny_ts_types.go +++ b/pkg/codegen/jenny_ts_types.go @@ -3,16 +3,21 @@ package codegen import ( "github.com/grafana/codejen" "github.com/grafana/cuetsy" + "github.com/grafana/cuetsy/ts/ast" "github.com/grafana/grafana/pkg/cuectx" "github.com/grafana/thema/encoding/typescript" ) +type ApplyFunc func(sfg SchemaForGen, file *ast.File) + // TSTypesJenny is a [OneToOne] that produces TypeScript types and // defaults for a Thema schema. // // Thema's generic TS jenny will be able to replace this one once // https://github.com/grafana/thema/issues/89 is complete. -type TSTypesJenny struct{} +type TSTypesJenny struct { + ApplyFuncs []ApplyFunc +} var _ codejen.OneToOne[SchemaForGen] = &TSTypesJenny{} @@ -30,6 +35,11 @@ func (j TSTypesJenny) Generate(sfg SchemaForGen) (*codejen.File, error) { RootName: sfg.Name, Group: sfg.IsGroup, }) + + for _, renameFunc := range j.ApplyFuncs { + renameFunc(sfg, f) + } + if err != nil { return nil, err } diff --git a/pkg/plugins/codegen/jenny_pluginseachmajor.go b/pkg/plugins/codegen/jenny_pluginseachmajor.go deleted file mode 100644 index e90a11a984..0000000000 --- a/pkg/plugins/codegen/jenny_pluginseachmajor.go +++ /dev/null @@ -1,101 +0,0 @@ -package codegen - -import ( - "fmt" - "os" - "path" - "path/filepath" - - "github.com/grafana/codejen" - tsast "github.com/grafana/cuetsy/ts/ast" - "github.com/grafana/grafana/pkg/build" - corecodegen "github.com/grafana/grafana/pkg/codegen" - "github.com/grafana/grafana/pkg/cuectx" - "github.com/grafana/grafana/pkg/plugins/pfs" - "github.com/grafana/kindsys" - "github.com/grafana/thema" -) - -func PluginTSEachMajor(rt *thema.Runtime) codejen.OneToMany[*pfs.PluginDecl] { - latestMajorsOrX := corecodegen.LatestMajorsOrXJenny(filepath.Join("packages", "grafana-schema", "src", "raw", "composable"), false, corecodegen.TSTypesJenny{}) - return &pleJenny{ - inner: kinds2pd(rt, latestMajorsOrX), - } -} - -type pleJenny struct { - inner codejen.OneToMany[*pfs.PluginDecl] -} - -func (*pleJenny) JennyName() string { - return "PluginEachMajorJenny" -} - -func (j *pleJenny) Generate(decl *pfs.PluginDecl) (codejen.Files, error) { - if !decl.HasSchema() { - return nil, nil - } - - jf, err := j.inner.Generate(decl) - if err != nil { - return nil, err - } - - version := "export const pluginVersion = \"%s\";" - if decl.PluginMeta.Version != nil { - version = fmt.Sprintf(version, *decl.PluginMeta.Version) - } else { - version = fmt.Sprintf(version, getGrafanaVersion()) - } - - files := make(codejen.Files, len(jf)) - for i, file := range jf { - tsf := &tsast.File{} - for _, im := range decl.Imports { - if tsim, err := cuectx.ConvertImport(im); err != nil { - return nil, err - } else if tsim.From.Value != "" { - tsf.Imports = append(tsf.Imports, tsim) - } - } - - tsf.Nodes = append(tsf.Nodes, tsast.Raw{ - Data: version, - }) - - tsf.Nodes = append(tsf.Nodes, tsast.Raw{ - Data: string(file.Data), - }) - - data := []byte(tsf.String()) - data = data[:len(data)-1] // remove the additional line break added by the inner jenny - - files[i] = *codejen.NewFile(file.RelativePath, data, append(file.From, j)...) - } - - return files, nil -} - -func kinds2pd(rt *thema.Runtime, j codejen.OneToMany[kindsys.Kind]) codejen.OneToMany[*pfs.PluginDecl] { - return codejen.AdaptOneToMany(j, func(pd *pfs.PluginDecl) kindsys.Kind { - kd, err := kindsys.BindComposable(rt, pd.KindDecl) - if err != nil { - return nil - } - return kd - }) -} - -func getGrafanaVersion() string { - dir, err := os.Getwd() - if err != nil { - return "" - } - - pkg, err := build.OpenPackageJSON(path.Join(dir, "../../../")) - if err != nil { - return "" - } - - return pkg.Version -} diff --git a/pkg/plugins/codegen/jenny_plugintstypes.go b/pkg/plugins/codegen/jenny_plugintstypes.go index 23c00a5ca5..c82f1be00f 100644 --- a/pkg/plugins/codegen/jenny_plugintstypes.go +++ b/pkg/plugins/codegen/jenny_plugintstypes.go @@ -2,19 +2,25 @@ package codegen import ( "fmt" + "os" + "path" "path/filepath" "strings" "github.com/grafana/codejen" tsast "github.com/grafana/cuetsy/ts/ast" + "github.com/grafana/grafana/pkg/build" + "github.com/grafana/grafana/pkg/codegen" "github.com/grafana/grafana/pkg/cuectx" "github.com/grafana/grafana/pkg/plugins/pfs" ) -func PluginTSTypesJenny(root string, inner codejen.OneToOne[*pfs.PluginDecl]) codejen.OneToOne[*pfs.PluginDecl] { +var versionedPluginPath = filepath.Join("packages", "grafana-schema", "src", "raw", "composable") + +func PluginTSTypesJenny(root string) codejen.OneToMany[*pfs.PluginDecl] { return &ptsJenny{ root: root, - inner: inner, + inner: adaptToPipeline(codegen.TSTypesJenny{}), } } @@ -24,21 +30,23 @@ type ptsJenny struct { } func (j *ptsJenny) JennyName() string { - return "PluginTSTypesJenny" + return "PluginTsTypesJenny" } -func (j *ptsJenny) Generate(decl *pfs.PluginDecl) (*codejen.File, error) { +func (j *ptsJenny) Generate(decl *pfs.PluginDecl) (codejen.Files, error) { if !decl.HasSchema() { return nil, nil } - tsf := &tsast.File{} + genFile := &tsast.File{} + versionedFile := &tsast.File{} for _, im := range decl.Imports { if tsim, err := cuectx.ConvertImport(im); err != nil { return nil, err } else if tsim.From.Value != "" { - tsf.Imports = append(tsf.Imports, tsim) + genFile.Imports = append(genFile.Imports, tsim) + versionedFile.Imports = append(versionedFile.Imports, tsim) } } @@ -47,13 +55,67 @@ func (j *ptsJenny) Generate(decl *pfs.PluginDecl) (*codejen.File, error) { return nil, err } - tsf.Nodes = append(tsf.Nodes, tsast.Raw{ - Data: string(jf.Data), - }) + rawData := tsast.Raw{Data: string(jf.Data)} + rawVersion := tsast.Raw{ + Data: getPluginVersion(decl.PluginMeta.Version), + } + + genFile.Nodes = append(genFile.Nodes, rawData) - path := filepath.Join(j.root, decl.PluginPath, fmt.Sprintf("%s.gen.ts", strings.ToLower(decl.SchemaInterface.Name))) - data := []byte(tsf.String()) + genPath := filepath.Join(j.root, decl.PluginPath, fmt.Sprintf("%s.gen.ts", strings.ToLower(decl.SchemaInterface.Name))) + data := []byte(genFile.String()) data = data[:len(data)-1] // remove the additional line break added by the inner jenny - return codejen.NewFile(path, data, append(jf.From, j)...), nil + files := make(codejen.Files, 2) + files[0] = *codejen.NewFile(genPath, data, append(jf.From, j)...) + + versionedFile.Nodes = append(versionedFile.Nodes, rawVersion, rawData) + + versionedData := []byte(versionedFile.String()) + versionedData = versionedData[:len(versionedData)-1] + + pluginFolder := strings.ReplaceAll(strings.ToLower(decl.PluginMeta.Name), " ", "") + versionedPath := filepath.Join(versionedPluginPath, pluginFolder, strings.ToLower(decl.SchemaInterface.Name), "x", jf.RelativePath) + files[1] = *codejen.NewFile(versionedPath, versionedData, append(jf.From, j)...) + + return files, nil +} + +func getPluginVersion(pluginVersion *string) string { + version := "export const pluginVersion = \"%s\";" + if pluginVersion != nil { + version = fmt.Sprintf(version, *pluginVersion) + } else { + version = fmt.Sprintf(version, getGrafanaVersion()) + } + + return version +} + +func adaptToPipeline(j codejen.OneToOne[codegen.SchemaForGen]) codejen.OneToOne[*pfs.PluginDecl] { + return codejen.AdaptOneToOne(j, func(pd *pfs.PluginDecl) codegen.SchemaForGen { + name := strings.ReplaceAll(pd.PluginMeta.Name, " ", "") + if pd.SchemaInterface.Name == "DataQuery" { + name = name + "DataQuery" + } + return codegen.SchemaForGen{ + Name: name, + Schema: pd.Lineage.Latest(), + IsGroup: pd.SchemaInterface.IsGroup, + } + }) +} + +func getGrafanaVersion() string { + dir, err := os.Getwd() + if err != nil { + return "" + } + + pkg, err := build.OpenPackageJSON(path.Join(dir, "../../../")) + if err != nil { + return "" + } + + return pkg.Version } diff --git a/public/app/plugins/datasource/azuremonitor/dataquery.gen.ts b/public/app/plugins/datasource/azuremonitor/dataquery.gen.ts index b9f234a9a3..8616ca3ac5 100644 --- a/public/app/plugins/datasource/azuremonitor/dataquery.gen.ts +++ b/public/app/plugins/datasource/azuremonitor/dataquery.gen.ts @@ -4,7 +4,7 @@ // public/app/plugins/gen.go // Using jennies: // TSTypesJenny -// PluginTSTypesJenny +// PluginTsTypesJenny // // Run 'make gen-cue' from repository root to regenerate. @@ -372,4 +372,4 @@ export interface WorkspacesQuery extends BaseGrafanaTemplateVariableQuery { export type GrafanaTemplateVariableQuery = (AppInsightsMetricNameQuery | AppInsightsGroupByQuery | SubscriptionsQuery | ResourceGroupsQuery | ResourceNamesQuery | MetricNamespaceQuery | MetricDefinitionsQuery | MetricNamesQuery | WorkspacesQuery | UnknownQuery); -export interface AzureMonitor {} +export interface AzureMonitorDataQuery {} diff --git a/public/app/plugins/datasource/cloud-monitoring/dataquery.gen.ts b/public/app/plugins/datasource/cloud-monitoring/dataquery.gen.ts index 8fe1f6b79d..a4d0755cb7 100644 --- a/public/app/plugins/datasource/cloud-monitoring/dataquery.gen.ts +++ b/public/app/plugins/datasource/cloud-monitoring/dataquery.gen.ts @@ -4,7 +4,7 @@ // public/app/plugins/gen.go // Using jennies: // TSTypesJenny -// PluginTSTypesJenny +// PluginTsTypesJenny // // Run 'make gen-cue' from repository root to regenerate. @@ -375,4 +375,4 @@ export enum MetricFindQueryTypes { Services = 'services', } -export interface GoogleCloudMonitoring {} +export interface GoogleCloudMonitoringDataQuery {} diff --git a/public/app/plugins/datasource/cloudwatch/dataquery.gen.ts b/public/app/plugins/datasource/cloudwatch/dataquery.gen.ts index ba62f4b129..9cf62e4b58 100644 --- a/public/app/plugins/datasource/cloudwatch/dataquery.gen.ts +++ b/public/app/plugins/datasource/cloudwatch/dataquery.gen.ts @@ -4,7 +4,7 @@ // public/app/plugins/gen.go // Using jennies: // TSTypesJenny -// PluginTSTypesJenny +// PluginTsTypesJenny // // Run 'make gen-cue' from repository root to regenerate. @@ -304,4 +304,4 @@ export interface CloudWatchAnnotationQuery extends common.DataQuery, MetricStat queryMode: CloudWatchQueryMode; } -export interface CloudWatch {} +export interface CloudWatchDataQuery {} diff --git a/public/app/plugins/datasource/elasticsearch/dataquery.gen.ts b/public/app/plugins/datasource/elasticsearch/dataquery.gen.ts index f9f394294f..f18c10029c 100644 --- a/public/app/plugins/datasource/elasticsearch/dataquery.gen.ts +++ b/public/app/plugins/datasource/elasticsearch/dataquery.gen.ts @@ -4,7 +4,7 @@ // public/app/plugins/gen.go // Using jennies: // TSTypesJenny -// PluginTSTypesJenny +// PluginTsTypesJenny // // Run 'make gen-cue' from repository root to regenerate. @@ -376,7 +376,7 @@ export type PipelineMetricAggregation = (MovingAverage | Derivative | Cumulative export type MetricAggregationWithSettings = (BucketScript | CumulativeSum | Derivative | SerialDiff | RawData | RawDocument | UniqueCount | Percentiles | ExtendedStats | Min | Max | Sum | Average | MovingAverage | MovingFunction | Logs | Rate | TopMetrics); -export interface Elasticsearch extends common.DataQuery { +export interface ElasticsearchDataQuery extends common.DataQuery { /** * Alias pattern */ @@ -399,7 +399,7 @@ export interface Elasticsearch extends common.DataQuery { timeField?: string; } -export const defaultElasticsearch: Partial = { +export const defaultElasticsearchDataQuery: Partial = { bucketAggs: [], metrics: [], }; diff --git a/public/app/plugins/datasource/elasticsearch/types.ts b/public/app/plugins/datasource/elasticsearch/types.ts index f462f43ad0..9e3a610b89 100644 --- a/public/app/plugins/datasource/elasticsearch/types.ts +++ b/public/app/plugins/datasource/elasticsearch/types.ts @@ -15,11 +15,11 @@ import { MovingAverage as SchemaMovingAverage, BucketAggregation, Logs as SchemaLogs, - Elasticsearch, + ElasticsearchDataQuery, } from './dataquery.gen'; export * from './dataquery.gen'; -export { Elasticsearch as ElasticsearchQuery } from './dataquery.gen'; +export { ElasticsearchDataQuery as ElasticsearchQuery } from './dataquery.gen'; // We want to extend the settings of the Logs query with additional properties that // are not part of the schema. This is a workaround, because exporting LogsSettings @@ -127,7 +127,7 @@ export type DataLinkConfig = { }; export interface ElasticsearchAnnotationQuery { - target: Elasticsearch; + target: ElasticsearchDataQuery; timeField?: string; titleField?: string; timeEndField?: string; diff --git a/public/app/plugins/datasource/grafana-pyroscope-datasource/dataquery.gen.ts b/public/app/plugins/datasource/grafana-pyroscope-datasource/dataquery.gen.ts index b6e39ae2c7..4a3595c3a3 100644 --- a/public/app/plugins/datasource/grafana-pyroscope-datasource/dataquery.gen.ts +++ b/public/app/plugins/datasource/grafana-pyroscope-datasource/dataquery.gen.ts @@ -4,7 +4,7 @@ // public/app/plugins/gen.go // Using jennies: // TSTypesJenny -// PluginTSTypesJenny +// PluginTsTypesJenny // // Run 'make gen-cue' from repository root to regenerate. @@ -14,7 +14,7 @@ export type PyroscopeQueryType = ('metrics' | 'profile' | 'both'); export const defaultPyroscopeQueryType: PyroscopeQueryType = 'both'; -export interface GrafanaPyroscope extends common.DataQuery { +export interface GrafanaPyroscopeDataQuery extends common.DataQuery { /** * Allows to group the results. */ @@ -37,7 +37,7 @@ export interface GrafanaPyroscope extends common.DataQuery { spanSelector?: Array; } -export const defaultGrafanaPyroscope: Partial = { +export const defaultGrafanaPyroscopeDataQuery: Partial = { groupBy: [], labelSelector: '{}', spanSelector: [], diff --git a/public/app/plugins/datasource/grafana-pyroscope-datasource/datasource.ts b/public/app/plugins/datasource/grafana-pyroscope-datasource/datasource.ts index 460f32b10f..52d50b6995 100644 --- a/public/app/plugins/datasource/grafana-pyroscope-datasource/datasource.ts +++ b/public/app/plugins/datasource/grafana-pyroscope-datasource/datasource.ts @@ -12,7 +12,7 @@ import { import { DataSourceWithBackend, getTemplateSrv, TemplateSrv } from '@grafana/runtime'; import { VariableSupport } from './VariableSupport'; -import { defaultGrafanaPyroscope, defaultPyroscopeQueryType } from './dataquery.gen'; +import { defaultGrafanaPyroscopeDataQuery, defaultPyroscopeQueryType } from './dataquery.gen'; import { PyroscopeDataSourceOptions, Query, ProfileTypeMessage } from './types'; import { extractLabelMatchers, toPromLikeExpr } from './utils'; @@ -115,7 +115,7 @@ export class PyroscopeDataSource extends DataSourceWithBackend = { - ...defaultGrafanaPyroscope, + ...defaultGrafanaPyroscopeDataQuery, queryType: defaultPyroscopeQueryType, }; diff --git a/public/app/plugins/datasource/grafana-pyroscope-datasource/types.ts b/public/app/plugins/datasource/grafana-pyroscope-datasource/types.ts index 04aa2f9a9b..1570edd34b 100644 --- a/public/app/plugins/datasource/grafana-pyroscope-datasource/types.ts +++ b/public/app/plugins/datasource/grafana-pyroscope-datasource/types.ts @@ -1,8 +1,8 @@ import { DataSourceJsonData } from '@grafana/data'; -import { GrafanaPyroscope, PyroscopeQueryType } from './dataquery.gen'; +import { GrafanaPyroscopeDataQuery, PyroscopeQueryType } from './dataquery.gen'; -export interface Query extends GrafanaPyroscope { +export interface Query extends GrafanaPyroscopeDataQuery { queryType: PyroscopeQueryType; } diff --git a/public/app/plugins/datasource/grafana-testdata-datasource/MetaDataInspector.tsx b/public/app/plugins/datasource/grafana-testdata-datasource/MetaDataInspector.tsx index 79b2b4fb28..de58ecd438 100644 --- a/public/app/plugins/datasource/grafana-testdata-datasource/MetaDataInspector.tsx +++ b/public/app/plugins/datasource/grafana-testdata-datasource/MetaDataInspector.tsx @@ -3,10 +3,10 @@ import React from 'react'; import { MetadataInspectorProps } from '@grafana/data'; import { Stack } from '@grafana/ui'; -import { TestData } from './dataquery.gen'; +import { TestDataDataQuery } from './dataquery.gen'; import { TestDataDataSource } from './datasource'; -export type Props = MetadataInspectorProps; +export type Props = MetadataInspectorProps; export function MetaDataInspector({ data }: Props) { return ( diff --git a/public/app/plugins/datasource/grafana-testdata-datasource/QueryEditor.tsx b/public/app/plugins/datasource/grafana-testdata-datasource/QueryEditor.tsx index c6cf280a5b..55b93f217a 100644 --- a/public/app/plugins/datasource/grafana-testdata-datasource/QueryEditor.tsx +++ b/public/app/plugins/datasource/grafana-testdata-datasource/QueryEditor.tsx @@ -17,7 +17,7 @@ import { RawFrameEditor } from './components/RawFrameEditor'; import { SimulationQueryEditor } from './components/SimulationQueryEditor'; import { USAQueryEditor, usaQueryModes } from './components/USAQueryEditor'; import { defaultCSVWaveQuery, defaultPulseQuery, defaultQuery } from './constants'; -import { CSVWave, NodesQuery, TestData, TestDataQueryType, USAQuery } from './dataquery.gen'; +import { CSVWave, NodesQuery, TestDataDataQuery, TestDataQueryType, USAQuery } from './dataquery.gen'; import { TestDataDataSource } from './datasource'; import { defaultStreamQuery } from './runStreams'; @@ -31,11 +31,11 @@ const selectors = editorSelectors.components.DataSource.TestData.QueryTab; export interface EditorProps { onChange: (value: any) => void; - query: TestData; + query: TestDataDataQuery; ds: TestDataDataSource; } -export type Props = QueryEditorProps; +export type Props = QueryEditorProps; export const QueryEditor = ({ query, datasource, onChange, onRunQuery }: Props) => { query = { ...defaultQuery, ...query }; @@ -63,7 +63,7 @@ export const QueryEditor = ({ query, datasource, onChange, onRunQuery }: Props) })); }, []); - const onUpdate = (query: TestData) => { + const onUpdate = (query: TestDataDataQuery) => { onChange(query); onRunQuery(); }; @@ -83,7 +83,7 @@ export const QueryEditor = ({ query, datasource, onChange, onRunQuery }: Props) } // Clear model from existing props that belong to other scenarios - const update: TestData = { + const update: TestDataDataQuery = { scenarioId: item.value! as TestDataQueryType, refId: query.refId, alias: query.alias, diff --git a/public/app/plugins/datasource/grafana-testdata-datasource/components/NodeGraphEditor.tsx b/public/app/plugins/datasource/grafana-testdata-datasource/components/NodeGraphEditor.tsx index 4421fe6627..9e1c3d626e 100644 --- a/public/app/plugins/datasource/grafana-testdata-datasource/components/NodeGraphEditor.tsx +++ b/public/app/plugins/datasource/grafana-testdata-datasource/components/NodeGraphEditor.tsx @@ -2,11 +2,11 @@ import React from 'react'; import { Input, InlineFieldRow, InlineField, Select } from '@grafana/ui'; -import { NodesQuery, TestData } from '../dataquery.gen'; +import { NodesQuery, TestDataDataQuery } from '../dataquery.gen'; export interface Props { onChange: (value: NodesQuery) => void; - query: TestData; + query: TestDataDataQuery; } export function NodeGraphEditor({ query, onChange }: Props) { const type = query.nodes?.type || 'random'; diff --git a/public/app/plugins/datasource/grafana-testdata-datasource/components/RandomWalkEditor.tsx b/public/app/plugins/datasource/grafana-testdata-datasource/components/RandomWalkEditor.tsx index 2296c6c0fd..1c5085f7b0 100644 --- a/public/app/plugins/datasource/grafana-testdata-datasource/components/RandomWalkEditor.tsx +++ b/public/app/plugins/datasource/grafana-testdata-datasource/components/RandomWalkEditor.tsx @@ -4,7 +4,7 @@ import { selectors } from '@grafana/e2e-selectors'; import { InlineField, InlineFieldRow, Input } from '@grafana/ui'; import { EditorProps } from '../QueryEditor'; -import { TestData } from '../dataquery.gen'; +import { TestDataDataQuery } from '../dataquery.gen'; const randomWalkFields: Array<{ label: string; @@ -49,7 +49,7 @@ export const RandomWalkEditor = ({ onChange, query }: EditorProps) => { id={`randomWalk-${id}-${query.refId}`} min={min} step={step} - value={(query as any)[id as keyof TestData] || placeholder} + value={(query as any)[id as keyof TestDataDataQuery] || placeholder} placeholder={placeholder} onChange={onChange} /> diff --git a/public/app/plugins/datasource/grafana-testdata-datasource/constants.ts b/public/app/plugins/datasource/grafana-testdata-datasource/constants.ts index 3acb06f3c5..a1ec6211b7 100644 --- a/public/app/plugins/datasource/grafana-testdata-datasource/constants.ts +++ b/public/app/plugins/datasource/grafana-testdata-datasource/constants.ts @@ -1,4 +1,4 @@ -import { CSVWave, PulseWaveQuery, TestData, TestDataQueryType } from './dataquery.gen'; +import { CSVWave, PulseWaveQuery, TestDataDataQuery, TestDataQueryType } from './dataquery.gen'; export const defaultPulseQuery: PulseWaveQuery = { timeStep: 60, @@ -15,7 +15,7 @@ export const defaultCSVWaveQuery: CSVWave[] = [ }, ]; -export const defaultQuery: TestData = { +export const defaultQuery: TestDataDataQuery = { scenarioId: TestDataQueryType.RandomWalk, refId: '', }; diff --git a/public/app/plugins/datasource/grafana-testdata-datasource/dataquery.gen.ts b/public/app/plugins/datasource/grafana-testdata-datasource/dataquery.gen.ts index ed487a37c2..f7fc07ecb3 100644 --- a/public/app/plugins/datasource/grafana-testdata-datasource/dataquery.gen.ts +++ b/public/app/plugins/datasource/grafana-testdata-datasource/dataquery.gen.ts @@ -4,7 +4,7 @@ // public/app/plugins/gen.go // Using jennies: // TSTypesJenny -// PluginTSTypesJenny +// PluginTsTypesJenny // // Run 'make gen-cue' from repository root to regenerate. @@ -106,7 +106,7 @@ export interface Scenario { stringInput: string; } -export interface TestData extends common.DataQuery { +export interface TestDataDataQuery extends common.DataQuery { alias?: string; channel?: string; csvContent?: string; @@ -134,7 +134,7 @@ export interface TestData extends common.DataQuery { usa?: USAQuery; } -export const defaultTestData: Partial = { +export const defaultTestDataDataQuery: Partial = { csvWave: [], points: [], scenarioId: TestDataQueryType.RandomWalk, diff --git a/public/app/plugins/datasource/grafana-testdata-datasource/datasource.ts b/public/app/plugins/datasource/grafana-testdata-datasource/datasource.ts index e290b34a02..7c486ddacb 100644 --- a/public/app/plugins/datasource/grafana-testdata-datasource/datasource.ts +++ b/public/app/plugins/datasource/grafana-testdata-datasource/datasource.ts @@ -20,14 +20,14 @@ import { } from '@grafana/data'; import { DataSourceWithBackend, getBackendSrv, getGrafanaLiveSrv, getTemplateSrv, TemplateSrv } from '@grafana/runtime'; -import { Scenario, TestData, TestDataQueryType } from './dataquery.gen'; +import { Scenario, TestDataDataQuery, TestDataQueryType } from './dataquery.gen'; import { queryMetricTree } from './metricTree'; import { generateRandomEdges, generateRandomNodes, savedNodesResponse } from './nodeGraphUtils'; import { runStream } from './runStreams'; import { flameGraphData, flameGraphDataDiff } from './testData/flameGraphResponse'; import { TestDataVariableSupport } from './variables'; -export class TestDataDataSource extends DataSourceWithBackend { +export class TestDataDataSource extends DataSourceWithBackend { scenariosCache?: Promise; constructor( @@ -40,7 +40,7 @@ export class TestDataDataSource extends DataSourceWithBackend { getDefaultQuery: () => ({ scenarioId: TestDataQueryType.Annotations, lines: 10 }), // Make sure annotations have scenarioId set - prepareAnnotation: (old: AnnotationQuery) => { + prepareAnnotation: (old: AnnotationQuery) => { if (old.target?.scenarioId?.length) { return old; } @@ -56,15 +56,15 @@ export class TestDataDataSource extends DataSourceWithBackend { }; } - getDefaultQuery(): Partial { + getDefaultQuery(): Partial { return { scenarioId: TestDataQueryType.RandomWalk, seriesCount: 1, }; } - query(options: DataQueryRequest): Observable { - const backendQueries: TestData[] = []; + query(options: DataQueryRequest): Observable { + const backendQueries: TestDataDataQuery[] = []; const streams: Array> = []; // Start streams and prepare queries @@ -141,7 +141,7 @@ export class TestDataDataSource extends DataSourceWithBackend { return merge(...streams); } - resolveTemplateVariables(query: TestData, scopedVars: ScopedVars) { + resolveTemplateVariables(query: TestDataDataQuery, scopedVars: ScopedVars) { if (query.labels) { query.labels = this.templateSrv.replace(query.labels, scopedVars); } @@ -162,12 +162,15 @@ export class TestDataDataSource extends DataSourceWithBackend { } } - applyTemplateVariables(query: TestData, scopedVars: ScopedVars): TestData { + applyTemplateVariables(query: TestDataDataQuery, scopedVars: ScopedVars): TestDataDataQuery { this.resolveTemplateVariables(query, scopedVars); return query; } - annotationDataTopicTest(target: TestData, req: DataQueryRequest): Observable { + annotationDataTopicTest( + target: TestDataDataQuery, + req: DataQueryRequest + ): Observable { const events = this.buildFakeAnnotationEvents(req.range, target.lines ?? 10); const dataFrame = new ArrayDataFrame(events); dataFrame.meta = { dataTopic: DataTopic.Annotations }; @@ -192,7 +195,7 @@ export class TestDataDataSource extends DataSourceWithBackend { return events; } - getQueryDisplayText(query: TestData) { + getQueryDisplayText(query: TestDataDataQuery) { const scenario = query.scenarioId ?? 'Default scenario'; if (query.alias) { @@ -217,7 +220,10 @@ export class TestDataDataSource extends DataSourceWithBackend { return this.scenariosCache; } - variablesQuery(target: TestData, options: DataQueryRequest): Observable { + variablesQuery( + target: TestDataDataQuery, + options: DataQueryRequest + ): Observable { const query = target.stringInput ?? ''; const interpolatedQuery = this.templateSrv.replace(query, getSearchFilterScopedVar({ query, wildcardChar: '*' })); const children = queryMetricTree(interpolatedQuery); @@ -227,7 +233,7 @@ export class TestDataDataSource extends DataSourceWithBackend { return of({ data: [dataFrame] }).pipe(delay(100)); } - nodesQuery(target: TestData, options: DataQueryRequest): Observable { + nodesQuery(target: TestDataDataQuery, options: DataQueryRequest): Observable { const type = target.nodes?.type || 'random'; let frames: DataFrame[]; switch (type) { @@ -250,12 +256,12 @@ export class TestDataDataSource extends DataSourceWithBackend { return of({ data: frames }).pipe(delay(100)); } - flameGraphQuery(target: TestData): Observable { + flameGraphQuery(target: TestDataDataQuery): Observable { const data = target.flamegraphDiff ? flameGraphDataDiff : flameGraphData; return of({ data: [{ ...data, refId: target.refId }] }).pipe(delay(100)); } - trace(options: DataQueryRequest): Observable { + trace(options: DataQueryRequest): Observable { const frame = new MutableDataFrame({ meta: { preferredVisualisationType: 'trace', @@ -317,7 +323,10 @@ export class TestDataDataSource extends DataSourceWithBackend { return of({ data: [frame] }).pipe(delay(100)); } - rawFrameQuery(target: TestData, options: DataQueryRequest): Observable { + rawFrameQuery( + target: TestDataDataQuery, + options: DataQueryRequest + ): Observable { try { const data = JSON.parse(target.rawFrameContent ?? '[]').map((v: any) => { const f = toDataFrame(v); @@ -333,7 +342,10 @@ export class TestDataDataSource extends DataSourceWithBackend { } } - serverErrorQuery(target: TestData, options: DataQueryRequest): Observable | null { + serverErrorQuery( + target: TestDataDataQuery, + options: DataQueryRequest + ): Observable | null { const { errorType } = target; if (errorType === 'server_panic') { @@ -353,7 +365,10 @@ export class TestDataDataSource extends DataSourceWithBackend { } } -function runGrafanaAPI(target: TestData, req: DataQueryRequest): Observable { +function runGrafanaAPI( + target: TestDataDataQuery, + req: DataQueryRequest +): Observable { const url = `/api/${target.stringInput}`; return from( getBackendSrv() @@ -370,7 +385,10 @@ function runGrafanaAPI(target: TestData, req: DataQueryRequest): Obser let liveQueryCounter = 1000; -function runGrafanaLiveQuery(target: TestData, req: DataQueryRequest): Observable { +function runGrafanaLiveQuery( + target: TestDataDataQuery, + req: DataQueryRequest +): Observable { if (!target.channel) { throw new Error(`Missing channel config`); } diff --git a/public/app/plugins/datasource/grafana-testdata-datasource/runStreams.ts b/public/app/plugins/datasource/grafana-testdata-datasource/runStreams.ts index fb0c079728..ea98e96636 100644 --- a/public/app/plugins/datasource/grafana-testdata-datasource/runStreams.ts +++ b/public/app/plugins/datasource/grafana-testdata-datasource/runStreams.ts @@ -20,7 +20,7 @@ import { } from '@grafana/data'; import { getRandomLine } from './LogIpsum'; -import { TestData, StreamingQuery } from './dataquery.gen'; +import { TestDataDataQuery, StreamingQuery } from './dataquery.gen'; export const defaultStreamQuery: StreamingQuery = { type: 'signal', @@ -30,7 +30,10 @@ export const defaultStreamQuery: StreamingQuery = { bands: 1, }; -export function runStream(target: TestData, req: DataQueryRequest): Observable { +export function runStream( + target: TestDataDataQuery, + req: DataQueryRequest +): Observable { const query = defaults(target.stream, defaultStreamQuery); switch (query.type) { case 'signal': @@ -46,9 +49,9 @@ export function runStream(target: TestData, req: DataQueryRequest): Ob } export function runSignalStream( - target: TestData, + target: TestDataDataQuery, query: StreamingQuery, - req: DataQueryRequest + req: DataQueryRequest ): Observable { return new Observable((subscriber) => { const streamId = `signal-${req.panelId || 'explore'}-${target.refId}`; @@ -128,9 +131,9 @@ export function runSignalStream( } export function runLogsStream( - target: TestData, + target: TestDataDataQuery, query: StreamingQuery, - req: DataQueryRequest + req: DataQueryRequest ): Observable { return new Observable((subscriber) => { const streamId = `logs-${req.panelId || 'explore'}-${target.refId}`; @@ -174,9 +177,9 @@ export function runLogsStream( } export function runFetchStream( - target: TestData, + target: TestDataDataQuery, query: StreamingQuery, - req: DataQueryRequest + req: DataQueryRequest ): Observable { return new Observable((subscriber) => { const streamId = `fetch-${req.panelId || 'explore'}-${target.refId}`; @@ -252,9 +255,9 @@ export function runFetchStream( } export function runTracesStream( - target: TestData, + target: TestDataDataQuery, query: StreamingQuery, - req: DataQueryRequest + req: DataQueryRequest ): Observable { return new Observable((subscriber) => { const streamId = `traces-${req.panelId || 'explore'}-${target.refId}`; @@ -285,7 +288,7 @@ export function runTracesStream( }); } -function createMainTraceFrame(target: TestData, maxDataPoints = 1000) { +function createMainTraceFrame(target: TestDataDataQuery, maxDataPoints = 1000) { const data = new CircularDataFrame({ append: 'head', capacity: maxDataPoints, diff --git a/public/app/plugins/datasource/grafana-testdata-datasource/variables.ts b/public/app/plugins/datasource/grafana-testdata-datasource/variables.ts index 0668a00e6c..16b604896e 100644 --- a/public/app/plugins/datasource/grafana-testdata-datasource/variables.ts +++ b/public/app/plugins/datasource/grafana-testdata-datasource/variables.ts @@ -1,10 +1,10 @@ import { StandardVariableQuery, StandardVariableSupport } from '@grafana/data'; -import { TestData, TestDataQueryType } from './dataquery.gen'; +import { TestDataDataQuery, TestDataQueryType } from './dataquery.gen'; import { TestDataDataSource } from './datasource'; export class TestDataVariableSupport extends StandardVariableSupport { - toDataQuery(query: StandardVariableQuery): TestData { + toDataQuery(query: StandardVariableQuery): TestDataDataQuery { return { refId: 'TestDataDataSource-QueryVariable', stringInput: query.query, diff --git a/public/app/plugins/datasource/loki/dataquery.gen.ts b/public/app/plugins/datasource/loki/dataquery.gen.ts index e9f7861856..063deb3ee2 100644 --- a/public/app/plugins/datasource/loki/dataquery.gen.ts +++ b/public/app/plugins/datasource/loki/dataquery.gen.ts @@ -4,7 +4,7 @@ // public/app/plugins/gen.go // Using jennies: // TSTypesJenny -// PluginTSTypesJenny +// PluginTsTypesJenny // // Run 'make gen-cue' from repository root to regenerate. @@ -33,7 +33,7 @@ export enum LokiQueryDirection { Forward = 'forward', } -export interface Loki extends common.DataQuery { +export interface LokiDataQuery extends common.DataQuery { editorMode?: QueryEditorMode; /** * The LogQL query. diff --git a/public/app/plugins/datasource/loki/types.ts b/public/app/plugins/datasource/loki/types.ts index c0e55bf1d1..806e7e8143 100644 --- a/public/app/plugins/datasource/loki/types.ts +++ b/public/app/plugins/datasource/loki/types.ts @@ -1,6 +1,11 @@ import { DataQuery, DataQueryRequest, DataSourceJsonData, TimeRange } from '@grafana/data'; -import { Loki as LokiQueryFromSchema, LokiQueryType, SupportingQueryType, LokiQueryDirection } from './dataquery.gen'; +import { + LokiDataQuery as LokiQueryFromSchema, + LokiQueryType, + SupportingQueryType, + LokiQueryDirection, +} from './dataquery.gen'; export { LokiQueryDirection, LokiQueryType, SupportingQueryType }; diff --git a/public/app/plugins/datasource/parca/QueryEditor/QueryEditor.tsx b/public/app/plugins/datasource/parca/QueryEditor/QueryEditor.tsx index 561eb26f74..d470a5d5e0 100644 --- a/public/app/plugins/datasource/parca/QueryEditor/QueryEditor.tsx +++ b/public/app/plugins/datasource/parca/QueryEditor/QueryEditor.tsx @@ -5,7 +5,7 @@ import { useMount } from 'react-use'; import { CoreApp, QueryEditorProps } from '@grafana/data'; import { ButtonCascader, CascaderOption } from '@grafana/ui'; -import { defaultParca, defaultParcaQueryType, Parca } from '../dataquery.gen'; +import { defaultParcaDataQuery, defaultParcaQueryType, ParcaDataQuery as Parca } from '../dataquery.gen'; import { ParcaDataSource } from '../datasource'; import { ParcaDataSourceOptions, ProfileTypeMessage, Query } from '../types'; @@ -17,7 +17,7 @@ import { QueryOptions } from './QueryOptions'; export type Props = QueryEditorProps; export const defaultQuery: Partial = { - ...defaultParca, + ...defaultParcaDataQuery, queryType: defaultParcaQueryType, }; diff --git a/public/app/plugins/datasource/parca/dataquery.gen.ts b/public/app/plugins/datasource/parca/dataquery.gen.ts index 7542d11ac4..e39bfa7d76 100644 --- a/public/app/plugins/datasource/parca/dataquery.gen.ts +++ b/public/app/plugins/datasource/parca/dataquery.gen.ts @@ -4,7 +4,7 @@ // public/app/plugins/gen.go // Using jennies: // TSTypesJenny -// PluginTSTypesJenny +// PluginTsTypesJenny // // Run 'make gen-cue' from repository root to regenerate. @@ -14,7 +14,7 @@ export type ParcaQueryType = ('metrics' | 'profile' | 'both'); export const defaultParcaQueryType: ParcaQueryType = 'both'; -export interface Parca extends common.DataQuery { +export interface ParcaDataQuery extends common.DataQuery { /** * Specifies the query label selectors. */ @@ -25,6 +25,6 @@ export interface Parca extends common.DataQuery { profileTypeId: string; } -export const defaultParca: Partial = { +export const defaultParcaDataQuery: Partial = { labelSelector: '{}', }; diff --git a/public/app/plugins/datasource/parca/types.ts b/public/app/plugins/datasource/parca/types.ts index 4c039b5fdb..c149c9ef5f 100644 --- a/public/app/plugins/datasource/parca/types.ts +++ b/public/app/plugins/datasource/parca/types.ts @@ -1,6 +1,6 @@ import { DataSourceJsonData } from '@grafana/data'; -import { Parca as ParcaBase, ParcaQueryType } from './dataquery.gen'; +import { ParcaDataQuery as ParcaBase, ParcaQueryType } from './dataquery.gen'; export interface Query extends ParcaBase { queryType: ParcaQueryType; diff --git a/public/app/plugins/datasource/tempo/dataquery.gen.ts b/public/app/plugins/datasource/tempo/dataquery.gen.ts index 1d915860b0..1f8c378121 100644 --- a/public/app/plugins/datasource/tempo/dataquery.gen.ts +++ b/public/app/plugins/datasource/tempo/dataquery.gen.ts @@ -4,7 +4,7 @@ // public/app/plugins/gen.go // Using jennies: // TSTypesJenny -// PluginTSTypesJenny +// PluginTsTypesJenny // // Run 'make gen-cue' from repository root to regenerate. @@ -127,4 +127,4 @@ export interface TraceqlFilter { valueType?: string; } -export interface Tempo {} +export interface TempoDataQuery {} diff --git a/public/app/plugins/gen.go b/public/app/plugins/gen.go index 7597595525..b8771d4875 100644 --- a/public/app/plugins/gen.go +++ b/public/app/plugins/gen.go @@ -49,8 +49,7 @@ func main() { pluginKindGen.Append( codegen.PluginTreeListJenny(), codegen.PluginGoTypesJenny("pkg/tsdb"), - codegen.PluginTSTypesJenny("public/app/plugins", adaptToPipeline(corecodegen.TSTypesJenny{})), - codegen.PluginTSEachMajor(rt), + codegen.PluginTSTypesJenny("public/app/plugins"), ) schifs := kindsys.SchemaInterfaces(rt.Context()) diff --git a/public/app/plugins/panel/alertGroups/panelcfg.gen.ts b/public/app/plugins/panel/alertGroups/panelcfg.gen.ts index dccaa7be81..04bd8098da 100644 --- a/public/app/plugins/panel/alertGroups/panelcfg.gen.ts +++ b/public/app/plugins/panel/alertGroups/panelcfg.gen.ts @@ -4,7 +4,7 @@ // public/app/plugins/gen.go // Using jennies: // TSTypesJenny -// PluginTSTypesJenny +// PluginTsTypesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/public/app/plugins/panel/annolist/panelcfg.gen.ts b/public/app/plugins/panel/annolist/panelcfg.gen.ts index fd2a927420..1734302372 100644 --- a/public/app/plugins/panel/annolist/panelcfg.gen.ts +++ b/public/app/plugins/panel/annolist/panelcfg.gen.ts @@ -4,7 +4,7 @@ // public/app/plugins/gen.go // Using jennies: // TSTypesJenny -// PluginTSTypesJenny +// PluginTsTypesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/public/app/plugins/panel/barchart/panelcfg.gen.ts b/public/app/plugins/panel/barchart/panelcfg.gen.ts index 841dee4a26..8a30ebf269 100644 --- a/public/app/plugins/panel/barchart/panelcfg.gen.ts +++ b/public/app/plugins/panel/barchart/panelcfg.gen.ts @@ -4,7 +4,7 @@ // public/app/plugins/gen.go // Using jennies: // TSTypesJenny -// PluginTSTypesJenny +// PluginTsTypesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/public/app/plugins/panel/bargauge/panelcfg.gen.ts b/public/app/plugins/panel/bargauge/panelcfg.gen.ts index b9d330fac9..eb6f40ef6a 100644 --- a/public/app/plugins/panel/bargauge/panelcfg.gen.ts +++ b/public/app/plugins/panel/bargauge/panelcfg.gen.ts @@ -4,7 +4,7 @@ // public/app/plugins/gen.go // Using jennies: // TSTypesJenny -// PluginTSTypesJenny +// PluginTsTypesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/public/app/plugins/panel/candlestick/panelcfg.gen.ts b/public/app/plugins/panel/candlestick/panelcfg.gen.ts index f6eafc6f39..d527f54d7e 100644 --- a/public/app/plugins/panel/candlestick/panelcfg.gen.ts +++ b/public/app/plugins/panel/candlestick/panelcfg.gen.ts @@ -4,7 +4,7 @@ // public/app/plugins/gen.go // Using jennies: // TSTypesJenny -// PluginTSTypesJenny +// PluginTsTypesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/public/app/plugins/panel/canvas/panelcfg.gen.ts b/public/app/plugins/panel/canvas/panelcfg.gen.ts index 880ef5d22d..16b6b3c83a 100644 --- a/public/app/plugins/panel/canvas/panelcfg.gen.ts +++ b/public/app/plugins/panel/canvas/panelcfg.gen.ts @@ -4,7 +4,7 @@ // public/app/plugins/gen.go // Using jennies: // TSTypesJenny -// PluginTSTypesJenny +// PluginTsTypesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/public/app/plugins/panel/dashlist/panelcfg.gen.ts b/public/app/plugins/panel/dashlist/panelcfg.gen.ts index 4c12168d0a..7d85f926cf 100644 --- a/public/app/plugins/panel/dashlist/panelcfg.gen.ts +++ b/public/app/plugins/panel/dashlist/panelcfg.gen.ts @@ -4,7 +4,7 @@ // public/app/plugins/gen.go // Using jennies: // TSTypesJenny -// PluginTSTypesJenny +// PluginTsTypesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/public/app/plugins/panel/datagrid/panelcfg.gen.ts b/public/app/plugins/panel/datagrid/panelcfg.gen.ts index 40269576d7..4816dbbc00 100644 --- a/public/app/plugins/panel/datagrid/panelcfg.gen.ts +++ b/public/app/plugins/panel/datagrid/panelcfg.gen.ts @@ -4,7 +4,7 @@ // public/app/plugins/gen.go // Using jennies: // TSTypesJenny -// PluginTSTypesJenny +// PluginTsTypesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/public/app/plugins/panel/debug/panelcfg.gen.ts b/public/app/plugins/panel/debug/panelcfg.gen.ts index 436fb0cd67..2163fc29cc 100644 --- a/public/app/plugins/panel/debug/panelcfg.gen.ts +++ b/public/app/plugins/panel/debug/panelcfg.gen.ts @@ -4,7 +4,7 @@ // public/app/plugins/gen.go // Using jennies: // TSTypesJenny -// PluginTSTypesJenny +// PluginTsTypesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/public/app/plugins/panel/gauge/panelcfg.gen.ts b/public/app/plugins/panel/gauge/panelcfg.gen.ts index a0a73ef586..f5d104afce 100644 --- a/public/app/plugins/panel/gauge/panelcfg.gen.ts +++ b/public/app/plugins/panel/gauge/panelcfg.gen.ts @@ -4,7 +4,7 @@ // public/app/plugins/gen.go // Using jennies: // TSTypesJenny -// PluginTSTypesJenny +// PluginTsTypesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/public/app/plugins/panel/geomap/panelcfg.gen.ts b/public/app/plugins/panel/geomap/panelcfg.gen.ts index 6c3f83f18b..7e4288bf05 100644 --- a/public/app/plugins/panel/geomap/panelcfg.gen.ts +++ b/public/app/plugins/panel/geomap/panelcfg.gen.ts @@ -4,7 +4,7 @@ // public/app/plugins/gen.go // Using jennies: // TSTypesJenny -// PluginTSTypesJenny +// PluginTsTypesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/public/app/plugins/panel/heatmap/panelcfg.gen.ts b/public/app/plugins/panel/heatmap/panelcfg.gen.ts index 2816a19c90..ce34f82984 100644 --- a/public/app/plugins/panel/heatmap/panelcfg.gen.ts +++ b/public/app/plugins/panel/heatmap/panelcfg.gen.ts @@ -4,7 +4,7 @@ // public/app/plugins/gen.go // Using jennies: // TSTypesJenny -// PluginTSTypesJenny +// PluginTsTypesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/public/app/plugins/panel/histogram/panelcfg.gen.ts b/public/app/plugins/panel/histogram/panelcfg.gen.ts index b49e695779..68e5497753 100644 --- a/public/app/plugins/panel/histogram/panelcfg.gen.ts +++ b/public/app/plugins/panel/histogram/panelcfg.gen.ts @@ -4,7 +4,7 @@ // public/app/plugins/gen.go // Using jennies: // TSTypesJenny -// PluginTSTypesJenny +// PluginTsTypesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/public/app/plugins/panel/logs/panelcfg.gen.ts b/public/app/plugins/panel/logs/panelcfg.gen.ts index e08ad83e62..62e8fc956e 100644 --- a/public/app/plugins/panel/logs/panelcfg.gen.ts +++ b/public/app/plugins/panel/logs/panelcfg.gen.ts @@ -4,7 +4,7 @@ // public/app/plugins/gen.go // Using jennies: // TSTypesJenny -// PluginTSTypesJenny +// PluginTsTypesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/public/app/plugins/panel/news/panelcfg.gen.ts b/public/app/plugins/panel/news/panelcfg.gen.ts index 8475aaa2f4..d9e19537df 100644 --- a/public/app/plugins/panel/news/panelcfg.gen.ts +++ b/public/app/plugins/panel/news/panelcfg.gen.ts @@ -4,7 +4,7 @@ // public/app/plugins/gen.go // Using jennies: // TSTypesJenny -// PluginTSTypesJenny +// PluginTsTypesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/public/app/plugins/panel/nodeGraph/panelcfg.gen.ts b/public/app/plugins/panel/nodeGraph/panelcfg.gen.ts index 672953359c..d09f8d207c 100644 --- a/public/app/plugins/panel/nodeGraph/panelcfg.gen.ts +++ b/public/app/plugins/panel/nodeGraph/panelcfg.gen.ts @@ -4,7 +4,7 @@ // public/app/plugins/gen.go // Using jennies: // TSTypesJenny -// PluginTSTypesJenny +// PluginTsTypesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/public/app/plugins/panel/piechart/panelcfg.gen.ts b/public/app/plugins/panel/piechart/panelcfg.gen.ts index 6d64ec8f2d..494d667817 100644 --- a/public/app/plugins/panel/piechart/panelcfg.gen.ts +++ b/public/app/plugins/panel/piechart/panelcfg.gen.ts @@ -4,7 +4,7 @@ // public/app/plugins/gen.go // Using jennies: // TSTypesJenny -// PluginTSTypesJenny +// PluginTsTypesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/public/app/plugins/panel/stat/panelcfg.gen.ts b/public/app/plugins/panel/stat/panelcfg.gen.ts index 9d860c655d..2f02876b84 100644 --- a/public/app/plugins/panel/stat/panelcfg.gen.ts +++ b/public/app/plugins/panel/stat/panelcfg.gen.ts @@ -4,7 +4,7 @@ // public/app/plugins/gen.go // Using jennies: // TSTypesJenny -// PluginTSTypesJenny +// PluginTsTypesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/public/app/plugins/panel/state-timeline/panelcfg.gen.ts b/public/app/plugins/panel/state-timeline/panelcfg.gen.ts index 12d089f676..fb03ba0795 100644 --- a/public/app/plugins/panel/state-timeline/panelcfg.gen.ts +++ b/public/app/plugins/panel/state-timeline/panelcfg.gen.ts @@ -4,7 +4,7 @@ // public/app/plugins/gen.go // Using jennies: // TSTypesJenny -// PluginTSTypesJenny +// PluginTsTypesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/public/app/plugins/panel/status-history/panelcfg.gen.ts b/public/app/plugins/panel/status-history/panelcfg.gen.ts index ef8b01455d..8bc46e363d 100644 --- a/public/app/plugins/panel/status-history/panelcfg.gen.ts +++ b/public/app/plugins/panel/status-history/panelcfg.gen.ts @@ -4,7 +4,7 @@ // public/app/plugins/gen.go // Using jennies: // TSTypesJenny -// PluginTSTypesJenny +// PluginTsTypesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/public/app/plugins/panel/table/panelcfg.gen.ts b/public/app/plugins/panel/table/panelcfg.gen.ts index fa44e7f982..5b771ff264 100644 --- a/public/app/plugins/panel/table/panelcfg.gen.ts +++ b/public/app/plugins/panel/table/panelcfg.gen.ts @@ -4,7 +4,7 @@ // public/app/plugins/gen.go // Using jennies: // TSTypesJenny -// PluginTSTypesJenny +// PluginTsTypesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/public/app/plugins/panel/text/panelcfg.gen.ts b/public/app/plugins/panel/text/panelcfg.gen.ts index 8b35a85ed2..cf08ce2468 100644 --- a/public/app/plugins/panel/text/panelcfg.gen.ts +++ b/public/app/plugins/panel/text/panelcfg.gen.ts @@ -4,7 +4,7 @@ // public/app/plugins/gen.go // Using jennies: // TSTypesJenny -// PluginTSTypesJenny +// PluginTsTypesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/public/app/plugins/panel/timeseries/panelcfg.gen.ts b/public/app/plugins/panel/timeseries/panelcfg.gen.ts index 146ea81246..e5e4bce7e7 100644 --- a/public/app/plugins/panel/timeseries/panelcfg.gen.ts +++ b/public/app/plugins/panel/timeseries/panelcfg.gen.ts @@ -4,7 +4,7 @@ // public/app/plugins/gen.go // Using jennies: // TSTypesJenny -// PluginTSTypesJenny +// PluginTsTypesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/public/app/plugins/panel/trend/panelcfg.gen.ts b/public/app/plugins/panel/trend/panelcfg.gen.ts index af1edc3126..f72cfaceb7 100644 --- a/public/app/plugins/panel/trend/panelcfg.gen.ts +++ b/public/app/plugins/panel/trend/panelcfg.gen.ts @@ -4,7 +4,7 @@ // public/app/plugins/gen.go // Using jennies: // TSTypesJenny -// PluginTSTypesJenny +// PluginTsTypesJenny // // Run 'make gen-cue' from repository root to regenerate. diff --git a/public/app/plugins/panel/xychart/panelcfg.gen.ts b/public/app/plugins/panel/xychart/panelcfg.gen.ts index 52f8a1ae2e..09f7869c53 100644 --- a/public/app/plugins/panel/xychart/panelcfg.gen.ts +++ b/public/app/plugins/panel/xychart/panelcfg.gen.ts @@ -4,7 +4,7 @@ // public/app/plugins/gen.go // Using jennies: // TSTypesJenny -// PluginTSTypesJenny +// PluginTsTypesJenny // // Run 'make gen-cue' from repository root to regenerate. From 01c8b7b6119081fca5cf9814e08fb55b45a5a196 Mon Sep 17 00:00:00 2001 From: ldomesjo <49636413+ldomesjo@users.noreply.github.com> Date: Mon, 11 Mar 2024 12:55:21 +0100 Subject: [PATCH 0499/1406] Documentation: Updated yaml for influxdb data sources (#84119) * Update _index.md Updated instructions for influx v2 * Added version:SQL to influx v3 example in _index.md --- docs/sources/datasources/influxdb/_index.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/sources/datasources/influxdb/_index.md b/docs/sources/datasources/influxdb/_index.md index ed908730fb..ca4ed02811 100644 --- a/docs/sources/datasources/influxdb/_index.md +++ b/docs/sources/datasources/influxdb/_index.md @@ -203,9 +203,8 @@ datasources: access: proxy url: http://localhost:8086 jsonData: - version: SQL - metadata: - - database: + dbName: site + httpHeaderName1: 'Authorization' secureJsonData: httpHeaderValue1: 'Token ' ``` @@ -216,11 +215,12 @@ datasources: apiVersion: 1 datasources: - - name: InfluxDB_v2_InfluxQL + - name: InfluxDB_v3_InfluxQL type: influxdb access: proxy url: http://localhost:8086 jsonData: + version: SQL dbName: site httpMode: POST secureJsonData: From 937390b91c4b30a7fa2a836ce1a5bcb8096f56f1 Mon Sep 17 00:00:00 2001 From: Giordano Ricci Date: Mon, 11 Mar 2024 12:09:43 +0000 Subject: [PATCH 0500/1406] Chore: Remove deprecated exploreId from QueryEditorProps (#83971) --- packages/grafana-data/src/types/datasource.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/grafana-data/src/types/datasource.ts b/packages/grafana-data/src/types/datasource.ts index 7f20cb9273..2ab40356c7 100644 --- a/packages/grafana-data/src/types/datasource.ts +++ b/packages/grafana-data/src/types/datasource.ts @@ -429,10 +429,6 @@ export interface QueryEditorProps< */ data?: PanelData; range?: TimeRange; - /** - * @deprecated This is not used anymore and will be removed in a future release. - */ - exploreId?: string; history?: Array>; queries?: DataQuery[]; app?: CoreApp; From c950b716ffdcb5ab5f73464dc16d6bed056006e4 Mon Sep 17 00:00:00 2001 From: Giordano Ricci Date: Mon, 11 Mar 2024 12:10:06 +0000 Subject: [PATCH 0501/1406] Chore: Remove deprecated ExploreQueryFieldProps (#83972) --- packages/grafana-data/src/types/datasource.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/packages/grafana-data/src/types/datasource.ts b/packages/grafana-data/src/types/datasource.ts index 2ab40356c7..ddf62d0d25 100644 --- a/packages/grafana-data/src/types/datasource.ts +++ b/packages/grafana-data/src/types/datasource.ts @@ -441,15 +441,6 @@ export enum ExploreMode { Tracing = 'Tracing', } -/** - * @deprecated use QueryEditorProps instead - */ -export type ExploreQueryFieldProps< - DSType extends DataSourceApi, - TQuery extends DataQuery = DataQuery, - TOptions extends DataSourceJsonData = DataSourceJsonData, -> = QueryEditorProps; - export interface QueryEditorHelpProps { datasource: DataSourceApi; query: TQuery; From fa888af2128b004558b00b204fc72d298f69b0b8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 12:02:58 +0000 Subject: [PATCH 0502/1406] Update dependency marked to v12.0.1 --- package.json | 2 +- packages/grafana-data/package.json | 2 +- packages/grafana-prometheus/package.json | 2 +- yarn.lock | 14 +++++++------- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index b5e5037adf..937e4cf71e 100644 --- a/package.json +++ b/package.json @@ -336,7 +336,7 @@ "lru-cache": "10.2.0", "lru-memoize": "^1.1.0", "lucene": "^2.1.1", - "marked": "12.0.0", + "marked": "12.0.1", "marked-mangle": "1.1.7", "memoize-one": "6.0.0", "ml-regression-polynomial": "^3.0.0", diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index e345910666..63daf64ab4 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -46,7 +46,7 @@ "fast_array_intersect": "1.1.0", "history": "4.10.1", "lodash": "4.17.21", - "marked": "12.0.0", + "marked": "12.0.1", "marked-mangle": "1.1.7", "moment": "2.30.1", "moment-timezone": "0.5.45", diff --git a/packages/grafana-prometheus/package.json b/packages/grafana-prometheus/package.json index 5af96f1205..6d8ed9dc09 100644 --- a/packages/grafana-prometheus/package.json +++ b/packages/grafana-prometheus/package.json @@ -56,7 +56,7 @@ "eventemitter3": "5.0.1", "lodash": "4.17.21", "lru-cache": "10.2.0", - "marked": "12.0.0", + "marked": "12.0.1", "marked-mangle": "1.1.7", "moment": "2.30.1", "moment-timezone": "0.5.45", diff --git a/yarn.lock b/yarn.lock index be61b0d47c..e802646e0e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3556,7 +3556,7 @@ __metadata: fast_array_intersect: "npm:1.1.0" history: "npm:4.10.1" lodash: "npm:4.17.21" - marked: "npm:12.0.0" + marked: "npm:12.0.1" marked-mangle: "npm:1.1.7" moment: "npm:2.30.1" moment-timezone: "npm:0.5.45" @@ -3977,7 +3977,7 @@ __metadata: jest-matcher-utils: "npm:29.7.0" lodash: "npm:4.17.21" lru-cache: "npm:10.2.0" - marked: "npm:12.0.0" + marked: "npm:12.0.1" marked-mangle: "npm:1.1.7" moment: "npm:2.30.1" moment-timezone: "npm:0.5.45" @@ -18524,7 +18524,7 @@ __metadata: lru-cache: "npm:10.2.0" lru-memoize: "npm:^1.1.0" lucene: "npm:^2.1.1" - marked: "npm:12.0.0" + marked: "npm:12.0.1" marked-mangle: "npm:1.1.7" memoize-one: "npm:6.0.0" mini-css-extract-plugin: "npm:2.8.0" @@ -22317,12 +22317,12 @@ __metadata: languageName: node linkType: hard -"marked@npm:12.0.0": - version: 12.0.0 - resolution: "marked@npm:12.0.0" +"marked@npm:12.0.1": + version: 12.0.1 + resolution: "marked@npm:12.0.1" bin: marked: bin/marked.js - checksum: 10/ac2e5a3ebf33f8636e65c1eb7f73267cbe101fea1ad08abab60d51e5b4fda30faa59050e2837dc03fb6dbf58f630485c8d01ae5b9d90d36bf4562d7f40c1d33e + checksum: 10/34fd0044ebeda28b3f3f94f340e2388666408315557f125d561b59b49baec4c6e6777f54b6fb12aa5c2bf3b75a4aa9f1809679bfb6502da73053d0461c1a232d languageName: node linkType: hard From 1ab8857e48ea47576c68647a1dc580e78619118e Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Mon, 11 Mar 2024 12:29:44 +0000 Subject: [PATCH 0503/1406] E2C: Add cloud migration is_target server config option (#83419) --- conf/defaults.ini | 5 +++++ packages/grafana-data/src/types/config.ts | 1 + packages/grafana-runtime/src/config.ts | 1 + pkg/api/dtos/frontend_settings.go | 2 ++ pkg/api/frontendsettings.go | 4 ++++ pkg/setting/setting.go | 9 +++++++++ 6 files changed, 22 insertions(+) diff --git a/conf/defaults.ini b/conf/defaults.ini index a6a43705c6..8ac107137d 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -1808,3 +1808,8 @@ read_only_toggles = [public_dashboards] # Set to false to disable public dashboards enabled = true + +###################################### Cloud Migration ###################################### +[cloud_migration] +# Set to true to enable target-side migration UI +is_target = false diff --git a/packages/grafana-data/src/types/config.ts b/packages/grafana-data/src/types/config.ts index 2a8b2ea205..fbf97c879d 100644 --- a/packages/grafana-data/src/types/config.ts +++ b/packages/grafana-data/src/types/config.ts @@ -227,6 +227,7 @@ export interface GrafanaConfig { sharedWithMeFolderUID?: string; rootFolderUID?: string; localFileSystemAvailable?: boolean; + cloudMigrationIsTarget?: boolean; // The namespace to use for kubernetes apiserver requests namespace: string; diff --git a/packages/grafana-runtime/src/config.ts b/packages/grafana-runtime/src/config.ts index 0e28eb5831..52647d5daf 100644 --- a/packages/grafana-runtime/src/config.ts +++ b/packages/grafana-runtime/src/config.ts @@ -172,6 +172,7 @@ export class GrafanaBootConfig implements GrafanaConfig { sharedWithMeFolderUID: string | undefined; rootFolderUID: string | undefined; localFileSystemAvailable: boolean | undefined; + cloudMigrationIsTarget: boolean | undefined; constructor(options: GrafanaBootConfig) { this.bootData = options.bootData; diff --git a/pkg/api/dtos/frontend_settings.go b/pkg/api/dtos/frontend_settings.go index a6972407c4..0d69e0a8b6 100644 --- a/pkg/api/dtos/frontend_settings.go +++ b/pkg/api/dtos/frontend_settings.go @@ -247,6 +247,8 @@ type FrontendSettingsDTO struct { PublicDashboardAccessToken string `json:"publicDashboardAccessToken"` PublicDashboardsEnabled bool `json:"publicDashboardsEnabled"` + CloudMigrationIsTarget bool `json:"cloudMigrationIsTarget"` + DateFormats setting.DateFormats `json:"dateFormats,omitempty"` LoginError string `json:"loginError,omitempty"` diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index 35c43cd250..518d3f541c 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -94,6 +94,8 @@ func (hs *HTTPServer) GetFrontendSettings(c *contextmodel.ReqContext) { } // getFrontendSettings returns a json object with all the settings needed for front end initialisation. +// +//nolint:gocyclo func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.FrontendSettingsDTO, error) { availablePlugins, err := hs.availablePlugins(c.Req.Context(), c.SignedInUser.GetOrgID()) if err != nil { @@ -161,6 +163,7 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro hasAccess := accesscontrol.HasAccess(hs.AccessControl, c) secretsManagerPluginEnabled := kvstore.EvaluateRemoteSecretsPlugin(c.Req.Context(), hs.secretsPluginManager, hs.Cfg) == nil trustedTypesDefaultPolicyEnabled := (hs.Cfg.CSPEnabled && strings.Contains(hs.Cfg.CSPTemplate, "require-trusted-types-for")) || (hs.Cfg.CSPReportOnlyEnabled && strings.Contains(hs.Cfg.CSPReportOnlyTemplate, "require-trusted-types-for")) + isCloudMigrationTarget := hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagOnPremToCloudMigrations) && hs.Cfg.CloudMigrationIsTarget frontendSettings := &dtos.FrontendSettingsDTO{ DefaultDatasource: defaultDS, @@ -218,6 +221,7 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro DisableFrontendSandboxForPlugins: hs.Cfg.DisableFrontendSandboxForPlugins, PublicDashboardAccessToken: c.PublicDashboardAccessToken, PublicDashboardsEnabled: hs.Cfg.PublicDashboardsEnabled, + CloudMigrationIsTarget: isCloudMigrationTarget, SharedWithMeFolderUID: folder.SharedWithMeFolderUID, RootFolderUID: accesscontrol.GeneralFolderUID, LocalFileSystemAvailable: hs.Cfg.LocalFileSystemAvailable, diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index a3fe6b9f13..5b70904b2b 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -495,6 +495,9 @@ type Cfg struct { // Public dashboards PublicDashboardsEnabled bool + // Cloud Migration + CloudMigrationIsTarget bool + // Feature Management Settings FeatureManagement FeatureMgmtSettings @@ -1286,6 +1289,7 @@ func (cfg *Cfg) parseINIFile(iniFile *ini.File) error { cfg.readFeatureManagementConfig() cfg.readPublicDashboardsSettings() + cfg.readCloudMigrationSettings() // read experimental scopes settings. scopesSection := iniFile.Section("scopes") @@ -2005,3 +2009,8 @@ func (cfg *Cfg) readPublicDashboardsSettings() { publicDashboards := cfg.Raw.Section("public_dashboards") cfg.PublicDashboardsEnabled = publicDashboards.Key("enabled").MustBool(true) } + +func (cfg *Cfg) readCloudMigrationSettings() { + cloudMigration := cfg.Raw.Section("cloud_migration") + cfg.CloudMigrationIsTarget = cloudMigration.Key("is_target").MustBool(false) +} From 4a81a0388b8abf4de5c405fd17062b9199c1ebdd Mon Sep 17 00:00:00 2001 From: Ivan Ortega Alba Date: Mon, 11 Mar 2024 13:33:32 +0100 Subject: [PATCH 0504/1406] Playlist: run on Scenes (#83551) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * DashboardScene: Implement playlist controls * Mock the runtime config properly * PlaylistSrv: with state you can subscribe to (#83828) --------- Co-authored-by: Torkel Ödegaard --- .../src/selectors/pages.ts | 5 ++ .../components/Signup/SignupPage.test.tsx | 1 + .../Signup/VerifyEmailPage.test.tsx | 1 + public/app/core/reducers/root.test.ts | 1 + .../panel-edit/PanelEditor.test.ts | 1 + .../scene/DashboardScene.test.tsx | 9 +++ .../dashboard-scene/scene/DashboardScene.tsx | 1 + .../scene/NavToolbarActions.test.tsx | 59 +++++++++++++++++++ .../scene/NavToolbarActions.tsx | 53 ++++++++++++++++- .../settings/AnnotationsEditView.test.tsx | 1 + .../AddPanelButton/AddPanelMenu.test.tsx | 1 - .../dashboard/components/DashNav/DashNav.tsx | 2 +- .../dashgrid/DashboardEmpty.test.tsx | 1 - .../features/dashboard/state/initDashboard.ts | 2 +- .../LibraryPanelsSearch.test.tsx | 1 + .../app/features/playlist/PlaylistSrv.test.ts | 4 +- public/app/features/playlist/PlaylistSrv.ts | 25 +++++--- 17 files changed, 151 insertions(+), 17 deletions(-) diff --git a/packages/grafana-e2e-selectors/src/selectors/pages.ts b/packages/grafana-e2e-selectors/src/selectors/pages.ts index 3fb85a7177..ae54d0ce94 100644 --- a/packages/grafana-e2e-selectors/src/selectors/pages.ts +++ b/packages/grafana-e2e-selectors/src/selectors/pages.ts @@ -57,6 +57,11 @@ export const Pages = { navV2: 'data-testid Dashboard navigation', publicDashboardTag: 'data-testid public dashboard tag', shareButton: 'data-testid share-button', + playlistControls: { + prev: 'data-testid playlist previous dashboard button', + stop: 'data-testid playlist stop dashboard button', + next: 'data-testid playlist next dashboard button', + }, }, SubMenu: { submenu: 'Dashboard submenu', diff --git a/public/app/core/components/Signup/SignupPage.test.tsx b/public/app/core/components/Signup/SignupPage.test.tsx index 9d3c867563..5c2a0ffb46 100644 --- a/public/app/core/components/Signup/SignupPage.test.tsx +++ b/public/app/core/components/Signup/SignupPage.test.tsx @@ -14,6 +14,7 @@ jest.mock('@grafana/runtime', () => ({ post: postMock, }), config: { + ...jest.requireActual('@grafana/runtime').config, loginError: false, buildInfo: { version: 'v1.0', diff --git a/public/app/core/components/Signup/VerifyEmailPage.test.tsx b/public/app/core/components/Signup/VerifyEmailPage.test.tsx index 00dbb378a4..ebdf201e9c 100644 --- a/public/app/core/components/Signup/VerifyEmailPage.test.tsx +++ b/public/app/core/components/Signup/VerifyEmailPage.test.tsx @@ -12,6 +12,7 @@ jest.mock('@grafana/runtime', () => ({ post: postMock, }), config: { + ...jest.requireActual('@grafana/runtime').config, buildInfo: { version: 'v1.0', commit: '1', diff --git a/public/app/core/reducers/root.test.ts b/public/app/core/reducers/root.test.ts index 6bb8712b23..3396fc0d18 100644 --- a/public/app/core/reducers/root.test.ts +++ b/public/app/core/reducers/root.test.ts @@ -9,6 +9,7 @@ import { createRootReducer } from './root'; jest.mock('@grafana/runtime', () => ({ ...jest.requireActual('@grafana/runtime'), config: { + ...jest.requireActual('@grafana/runtime').config, bootData: { navTree: [], user: {}, diff --git a/public/app/features/dashboard-scene/panel-edit/PanelEditor.test.ts b/public/app/features/dashboard-scene/panel-edit/PanelEditor.test.ts index d2f1261a4e..fe93e589fb 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelEditor.test.ts +++ b/public/app/features/dashboard-scene/panel-edit/PanelEditor.test.ts @@ -13,6 +13,7 @@ jest.mock('@grafana/runtime', () => ({ getPanelPluginFromCache: jest.fn(() => pluginToLoad), }), config: { + ...jest.requireActual('@grafana/runtime').config, panels: { text: { skipDataQuery: true, diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx index 7dc9c6339f..807c7f7bce 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx @@ -57,6 +57,15 @@ jest.mock('@grafana/runtime', () => ({ }, })); +jest.mock('app/features/playlist/PlaylistSrv', () => ({ + ...jest.requireActual('app/features/playlist/PlaylistSrv'), + playlistSrv: { + isPlaying: false, + next: jest.fn(), + prev: jest.fn(), + stop: jest.fn(), + }, +})); const worker = createWorker(); mockResultsOfDetectChangesWorker({ hasChanges: true, hasTimeChanges: false, hasVariableValueChanges: false }); diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index 41bdb767c8..387071fc1d 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -109,6 +109,7 @@ export interface DashboardSceneState extends SceneObjectState { overlay?: SceneObject; /** True when a user copies a panel in the dashboard */ hasCopiedPanel?: boolean; + /** The dashboard doesn't have panels */ isEmpty?: boolean; } diff --git a/public/app/features/dashboard-scene/scene/NavToolbarActions.test.tsx b/public/app/features/dashboard-scene/scene/NavToolbarActions.test.tsx index da1c138fb7..ce001bb53b 100644 --- a/public/app/features/dashboard-scene/scene/NavToolbarActions.test.tsx +++ b/public/app/features/dashboard-scene/scene/NavToolbarActions.test.tsx @@ -4,11 +4,26 @@ import React from 'react'; import { TestProvider } from 'test/helpers/TestProvider'; import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock'; +import { selectors } from '@grafana/e2e-selectors'; +import { playlistSrv } from 'app/features/playlist/PlaylistSrv'; + import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene'; import { transformSceneToSaveModel } from '../serialization/transformSceneToSaveModel'; import { ToolbarActions } from './NavToolbarActions'; +jest.mock('app/features/playlist/PlaylistSrv', () => ({ + playlistSrv: { + useState: jest.fn().mockReturnValue({ isPlaying: false }), + setState: jest.fn(), + isPlaying: true, + start: jest.fn(), + next: jest.fn(), + prev: jest.fn(), + stop: jest.fn(), + }, +})); + describe('NavToolbarActions', () => { describe('Give an already saved dashboard', () => { it('Should show correct buttons when not in editing', async () => { @@ -23,6 +38,44 @@ describe('NavToolbarActions', () => { expect(await screen.findByText('Share')).toBeInTheDocument(); }); + it('Should the correct buttons when playing a playlist', async () => { + jest.mocked(playlistSrv).useState.mockReturnValueOnce({ isPlaying: true }); + setup(); + + expect(await screen.findByTestId(selectors.pages.Dashboard.DashNav.playlistControls.prev)).toBeInTheDocument(); + expect(await screen.findByTestId(selectors.pages.Dashboard.DashNav.playlistControls.stop)).toBeInTheDocument(); + expect(await screen.findByTestId(selectors.pages.Dashboard.DashNav.playlistControls.next)).toBeInTheDocument(); + expect(screen.queryByText('Edit')).not.toBeInTheDocument(); + expect(screen.queryByText('Share')).not.toBeInTheDocument(); + }); + + it('Should call the playlist srv when using playlist controls', async () => { + jest.mocked(playlistSrv).useState.mockReturnValueOnce({ isPlaying: true }); + setup(); + + // Previous dashboard + expect(await screen.findByTestId(selectors.pages.Dashboard.DashNav.playlistControls.prev)).toBeInTheDocument(); + await userEvent.click(await screen.findByTestId(selectors.pages.Dashboard.DashNav.playlistControls.prev)); + expect(playlistSrv.prev).toHaveBeenCalledTimes(1); + + // Next dashboard + expect(await screen.findByTestId(selectors.pages.Dashboard.DashNav.playlistControls.next)).toBeInTheDocument(); + await userEvent.click(await screen.findByTestId(selectors.pages.Dashboard.DashNav.playlistControls.next)); + expect(playlistSrv.next).toHaveBeenCalledTimes(1); + + // Stop playlist + expect(await screen.findByTestId(selectors.pages.Dashboard.DashNav.playlistControls.stop)).toBeInTheDocument(); + await userEvent.click(await screen.findByTestId(selectors.pages.Dashboard.DashNav.playlistControls.stop)); + expect(playlistSrv.stop).toHaveBeenCalledTimes(1); + }); + + it('Should hide the playlist controls when it is not playing', async () => { + setup(); + expect(screen.queryByText(selectors.pages.Dashboard.DashNav.playlistControls.prev)).not.toBeInTheDocument(); + expect(screen.queryByText(selectors.pages.Dashboard.DashNav.playlistControls.stop)).not.toBeInTheDocument(); + expect(screen.queryByText(selectors.pages.Dashboard.DashNav.playlistControls.next)).not.toBeInTheDocument(); + }); + it('Should show correct buttons when editing', async () => { setup(); @@ -36,6 +89,9 @@ describe('NavToolbarActions', () => { expect(await screen.findByLabelText('Add library panel')).toBeInTheDocument(); expect(screen.queryByText('Edit')).not.toBeInTheDocument(); expect(screen.queryByText('Share')).not.toBeInTheDocument(); + expect(screen.queryByText(selectors.pages.Dashboard.DashNav.playlistControls.prev)).not.toBeInTheDocument(); + expect(screen.queryByText(selectors.pages.Dashboard.DashNav.playlistControls.stop)).not.toBeInTheDocument(); + expect(screen.queryByText(selectors.pages.Dashboard.DashNav.playlistControls.next)).not.toBeInTheDocument(); }); it('Should show correct buttons when in settings menu', async () => { @@ -46,6 +102,9 @@ describe('NavToolbarActions', () => { expect(await screen.findByText('Save dashboard')).toBeInTheDocument(); expect(await screen.findByText('Back to dashboard')).toBeInTheDocument(); + expect(screen.queryByText(selectors.pages.Dashboard.DashNav.playlistControls.prev)).not.toBeInTheDocument(); + expect(screen.queryByText(selectors.pages.Dashboard.DashNav.playlistControls.stop)).not.toBeInTheDocument(); + expect(screen.queryByText(selectors.pages.Dashboard.DashNav.playlistControls.next)).not.toBeInTheDocument(); }); }); }); diff --git a/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx b/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx index 232189129f..31a30316fd 100644 --- a/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx +++ b/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx @@ -2,13 +2,15 @@ import { css } from '@emotion/css'; import React from 'react'; import { GrafanaTheme2 } from '@grafana/data'; +import { selectors } from '@grafana/e2e-selectors'; import { locationService } from '@grafana/runtime'; import { Button, ButtonGroup, Dropdown, Icon, Menu, ToolbarButton, ToolbarButtonRow, useStyles2 } from '@grafana/ui'; import { AppChromeUpdate } from 'app/core/components/AppChrome/AppChromeUpdate'; import { NavToolbarSeparator } from 'app/core/components/AppChrome/NavToolbar/NavToolbarSeparator'; import { contextSrv } from 'app/core/core'; -import { t } from 'app/core/internationalization'; +import { t, Trans } from 'app/core/internationalization'; import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; +import { playlistSrv } from 'app/features/playlist/PlaylistSrv'; import { ShareModal } from '../sharing/ShareModal'; import { DashboardInteractions } from '../utils/interactions'; @@ -47,6 +49,8 @@ export function ToolbarActions({ dashboard }: Props) { editPanel, hasCopiedPanel: copiedPanel, } = dashboard.useState(); + const { isPlaying } = playlistSrv.useState(); + const canSaveAs = contextSrv.hasEditPermissionInFolders; const toolbarActions: ToolbarAction[] = []; const buttonWithExtraMargin = useStyles2(getStyles); @@ -170,6 +174,49 @@ export function ToolbarActions({ dashboard }: Props) { ), }); + toolbarActions.push({ + group: 'playlist-actions', + condition: isPlaying && !editview && !isEditingPanel && !isEditing, + render: () => ( + playlistSrv.prev()} + /> + ), + }); + + toolbarActions.push({ + group: 'playlist-actions', + condition: isPlaying && !editview && !isEditingPanel && !isEditing, + render: () => ( + playlistSrv.stop()} + data-testid={selectors.pages.Dashboard.DashNav.playlistControls.stop} + > + Stop playlist + + ), + }); + + toolbarActions.push({ + group: 'playlist-actions', + condition: isPlaying && !editview && !isEditingPanel && !isEditing, + render: () => ( + playlistSrv.next()} + narrow + /> + ), + }); + if (dynamicDashNavActions.left.length > 0 && !isEditingPanel) { dynamicDashNavActions.left.map((action, index) => { const props = { dashboard: getDashboardSrv().getCurrent()! }; @@ -225,7 +272,7 @@ export function ToolbarActions({ dashboard }: Props) { toolbarActions.push({ group: 'main-buttons', - condition: uid && !isEditing && !meta.isSnapshot, + condition: uid && !isEditing && !meta.isSnapshot && !isPlaying, render: () => (
); @@ -52,9 +53,10 @@ export function getStyles(theme: GrafanaTheme2) { textAlign: 'center', }), grot: css({ + alignSelf: 'center', maxWidth: '450px', paddingTop: theme.spacing(8), - margin: '0 auto', + width: '100%', }), }; } diff --git a/public/app/features/commandPalette/EmptyState.tsx b/public/app/features/commandPalette/EmptyState.tsx index 15439b049d..b7843840e0 100644 --- a/public/app/features/commandPalette/EmptyState.tsx +++ b/public/app/features/commandPalette/EmptyState.tsx @@ -3,13 +3,15 @@ import React from 'react'; import { Box, Stack, Text } from '@grafana/ui'; import { Trans } from 'app/core/internationalization'; +import { GrotNotFound } from '../../core/components/GrotNotFound/GrotNotFound'; + export interface Props {} export const EmptyState = ({}: Props) => { return ( - - grot + + No results found diff --git a/public/img/grot-404-dark.svg b/public/img/grot-404-dark.svg index dd6e81f4f0..4ae37d64ce 100644 --- a/public/img/grot-404-dark.svg +++ b/public/img/grot-404-dark.svg @@ -1,61 +1,67 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/img/grot-404-light.svg b/public/img/grot-404-light.svg index 2c9ca39120..b2e54046cb 100644 --- a/public/img/grot-404-light.svg +++ b/public/img/grot-404-light.svg @@ -1,61 +1,67 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/img/grot-not-found.svg b/public/img/grot-not-found.svg deleted file mode 100644 index 12106ef9a7..0000000000 --- a/public/img/grot-not-found.svg +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From dd01743de7d886f09153686744f0bbb936d3b339 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 14:35:15 +0000 Subject: [PATCH 0517/1406] Update dependency react-virtualized-auto-sizer to v1.0.24 --- package.json | 2 +- packages/grafana-flamegraph/package.json | 2 +- packages/grafana-sql/package.json | 2 +- yarn.lock | 14 +++++++------- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index e534201d93..c015086c9a 100644 --- a/package.json +++ b/package.json @@ -388,7 +388,7 @@ "react-transition-group": "4.4.5", "react-use": "17.5.0", "react-virtual": "2.10.4", - "react-virtualized-auto-sizer": "1.0.23", + "react-virtualized-auto-sizer": "1.0.24", "react-window": "1.8.10", "react-window-infinite-loader": "1.0.9", "react-zoom-pan-pinch": "^3.3.0", diff --git a/packages/grafana-flamegraph/package.json b/packages/grafana-flamegraph/package.json index 18bea05581..1e6a8feab6 100644 --- a/packages/grafana-flamegraph/package.json +++ b/packages/grafana-flamegraph/package.json @@ -51,7 +51,7 @@ "lodash": "4.17.21", "react": "18.2.0", "react-use": "17.5.0", - "react-virtualized-auto-sizer": "1.0.23", + "react-virtualized-auto-sizer": "1.0.24", "tinycolor2": "1.6.0", "tslib": "2.6.2" }, diff --git a/packages/grafana-sql/package.json b/packages/grafana-sql/package.json index d4e531e5ce..1e65b4e8ab 100644 --- a/packages/grafana-sql/package.json +++ b/packages/grafana-sql/package.json @@ -26,7 +26,7 @@ "immutable": "4.3.5", "lodash": "4.17.21", "react-use": "17.5.0", - "react-virtualized-auto-sizer": "1.0.23", + "react-virtualized-auto-sizer": "1.0.24", "rxjs": "7.8.1", "sql-formatter-plus": "^1.3.6", "tslib": "2.6.2", diff --git a/yarn.lock b/yarn.lock index 58409c6c3d..91e8bdfc89 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3786,7 +3786,7 @@ __metadata: lodash: "npm:4.17.21" react: "npm:18.2.0" react-use: "npm:17.5.0" - react-virtualized-auto-sizer: "npm:1.0.23" + react-virtualized-auto-sizer: "npm:1.0.24" rollup: "npm:2.79.1" rollup-plugin-dts: "npm:^5.0.0" rollup-plugin-esbuild: "npm:5.0.0" @@ -4129,7 +4129,7 @@ __metadata: lodash: "npm:4.17.21" react: "npm:18.2.0" react-use: "npm:17.5.0" - react-virtualized-auto-sizer: "npm:1.0.23" + react-virtualized-auto-sizer: "npm:1.0.24" rxjs: "npm:7.8.1" sql-formatter-plus: "npm:^1.3.6" ts-jest: "npm:29.1.2" @@ -18590,7 +18590,7 @@ __metadata: react-transition-group: "npm:4.4.5" react-use: "npm:17.5.0" react-virtual: "npm:2.10.4" - react-virtualized-auto-sizer: "npm:1.0.23" + react-virtualized-auto-sizer: "npm:1.0.24" react-window: "npm:1.8.10" react-window-infinite-loader: "npm:1.0.9" react-zoom-pan-pinch: "npm:^3.3.0" @@ -26822,13 +26822,13 @@ __metadata: languageName: node linkType: hard -"react-virtualized-auto-sizer@npm:1.0.23": - version: 1.0.23 - resolution: "react-virtualized-auto-sizer@npm:1.0.23" +"react-virtualized-auto-sizer@npm:1.0.24": + version: 1.0.24 + resolution: "react-virtualized-auto-sizer@npm:1.0.24" peerDependencies: react: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 react-dom: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 - checksum: 10/a9b4ca0b64aaf27f9f3d214770a4a8fcaeb3d17383508d690420149e889088e1334a9dd54e69d6cb4ab891071ce0344d5605f028b941b39a331d491861eddfc6 + checksum: 10/02101a340bdbe3e40c49dbc52e524eb7ca18832690e91f045a25675600d7adc0a63e800a4ace6a014132adcdcce0e12a8137971de408427a5a3112d7c87c9f3e languageName: node linkType: hard From d8b8a2c2b0c4e56739ae4d77f70b014d7e2fb690 Mon Sep 17 00:00:00 2001 From: kay delaney <45561153+kaydelaney@users.noreply.github.com> Date: Mon, 11 Mar 2024 14:53:59 +0000 Subject: [PATCH 0518/1406] Dashboard: Fix issue where out-of-view shared query panels caused blank dependent panels (#83966) --- .../dashgrid/PanelStateWrapper.test.tsx | 2 ++ .../features/dashboard/state/DashboardModel.ts | 17 ++++++++++++++--- .../app/features/dashboard/state/PanelModel.ts | 2 +- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/public/app/features/dashboard/dashgrid/PanelStateWrapper.test.tsx b/public/app/features/dashboard/dashgrid/PanelStateWrapper.test.tsx index 8b6be6c4b9..c46d74b372 100644 --- a/public/app/features/dashboard/dashgrid/PanelStateWrapper.test.tsx +++ b/public/app/features/dashboard/dashgrid/PanelStateWrapper.test.tsx @@ -74,6 +74,8 @@ function setupTestContext(options: Partial) { ); + // Needed so mocks work + props.panel.refreshWhenInView = false; return { rerender, props, subject, store }; } diff --git a/public/app/features/dashboard/state/DashboardModel.ts b/public/app/features/dashboard/state/DashboardModel.ts index 35969d751c..b1cbf4a502 100644 --- a/public/app/features/dashboard/state/DashboardModel.ts +++ b/public/app/features/dashboard/state/DashboardModel.ts @@ -384,11 +384,22 @@ export class DashboardModel implements TimeModel { return; } - for (const panel of this.panels) { - if (!this.otherPanelInFullscreen(panel) && (event.refreshAll || event.panelIds.includes(panel.id))) { - panel.refresh(); + const panelsToRefresh = this.panels.filter( + (panel) => !this.otherPanelInFullscreen(panel) && (event.refreshAll || event.panelIds.includes(panel.id)) + ); + + // We have to mark every panel as refreshWhenInView /before/ we actually refresh any + // in case there is a shared query, as otherwise that might refresh before the source panel is + // marked for refresh, preventing the panel from updating + if (!this.isSnapshot()) { + for (const panel of panelsToRefresh) { + panel.refreshWhenInView = true; } } + + for (const panel of panelsToRefresh) { + panel.refresh(); + } } render() { diff --git a/public/app/features/dashboard/state/PanelModel.ts b/public/app/features/dashboard/state/PanelModel.ts index 1b3a6b2fbb..08d6862897 100644 --- a/public/app/features/dashboard/state/PanelModel.ts +++ b/public/app/features/dashboard/state/PanelModel.ts @@ -205,7 +205,7 @@ export class PanelModel implements DataConfigSource, IPanelModel { cacheTimeout?: string | null; queryCachingTTL?: number | null; isNew?: boolean; - refreshWhenInView = false; + refreshWhenInView = true; cachedPluginOptions: Record = {}; legend?: { show: boolean; sort?: string; sortDesc?: boolean }; From 9c292d2c3f771c24e393ae08dcd5a26b5405f7c0 Mon Sep 17 00:00:00 2001 From: Karl Persson Date: Mon, 11 Mar 2024 15:56:53 +0100 Subject: [PATCH 0519/1406] AuthN: Use sync hook to fetch service account (#84078) * Use sync hook to fetch service account --- pkg/services/authn/authnimpl/service.go | 2 +- .../authn/authnimpl/sync/user_sync.go | 2 +- pkg/services/authn/clients/api_key.go | 21 ++++-------- pkg/services/authn/clients/api_key_test.go | 34 +++---------------- 4 files changed, 14 insertions(+), 45 deletions(-) diff --git a/pkg/services/authn/authnimpl/service.go b/pkg/services/authn/authnimpl/service.go index 08fd7e3005..c819131ad3 100644 --- a/pkg/services/authn/authnimpl/service.go +++ b/pkg/services/authn/authnimpl/service.go @@ -90,7 +90,7 @@ func ProvideService( usageStats.RegisterMetricsFunc(s.getUsageStats) s.RegisterClient(clients.ProvideRender(userService, renderService)) - s.RegisterClient(clients.ProvideAPIKey(apikeyService, userService)) + s.RegisterClient(clients.ProvideAPIKey(apikeyService)) if cfg.LoginCookieName != "" { s.RegisterClient(clients.ProvideSession(cfg, sessionService)) diff --git a/pkg/services/authn/authnimpl/sync/user_sync.go b/pkg/services/authn/authnimpl/sync/user_sync.go index 71a96c1812..6ba757b561 100644 --- a/pkg/services/authn/authnimpl/sync/user_sync.go +++ b/pkg/services/authn/authnimpl/sync/user_sync.go @@ -111,7 +111,7 @@ func (s *UserSync) FetchSyncedUserHook(ctx context.Context, identity *authn.Iden return nil } namespace, id := identity.GetNamespacedID() - if namespace != authn.NamespaceUser { + if namespace != authn.NamespaceUser && namespace != authn.NamespaceServiceAccount { return nil } diff --git a/pkg/services/authn/clients/api_key.go b/pkg/services/authn/clients/api_key.go index 34c3fc180f..4005005631 100644 --- a/pkg/services/authn/clients/api_key.go +++ b/pkg/services/authn/clients/api_key.go @@ -14,7 +14,6 @@ import ( "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/org" - "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util/errutil" ) @@ -29,17 +28,15 @@ var ( var _ authn.HookClient = new(APIKey) var _ authn.ContextAwareClient = new(APIKey) -func ProvideAPIKey(apiKeyService apikey.Service, userService user.Service) *APIKey { +func ProvideAPIKey(apiKeyService apikey.Service) *APIKey { return &APIKey{ log: log.New(authn.ClientAPIKey), - userService: userService, apiKeyService: apiKeyService, } } type APIKey struct { log log.Logger - userService user.Service apiKeyService apikey.Service } @@ -81,16 +78,12 @@ func (s *APIKey) Authenticate(ctx context.Context, r *authn.Request) (*authn.Ide }, nil } - usr, err := s.userService.GetSignedInUserWithCacheCtx(ctx, &user.GetSignedInUserQuery{ - UserID: *apiKey.ServiceAccountId, - OrgID: apiKey.OrgID, - }) - - if err != nil { - return nil, err - } - - return authn.IdentityFromSignedInUser(authn.NamespacedID(authn.NamespaceServiceAccount, usr.UserID), usr, authn.ClientParams{SyncPermissions: true}, login.APIKeyAuthModule), nil + return &authn.Identity{ + ID: authn.NamespacedID(authn.NamespaceServiceAccount, *apiKey.ServiceAccountId), + OrgID: apiKey.OrgID, + AuthenticatedBy: login.APIKeyAuthModule, + ClientParams: authn.ClientParams{FetchSyncedUser: true, 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 f4dce265cf..5cb8ae6311 100644 --- a/pkg/services/authn/clients/api_key_test.go +++ b/pkg/services/authn/clients/api_key_test.go @@ -16,8 +16,6 @@ import ( "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/org" - "github.com/grafana/grafana/pkg/services/user" - "github.com/grafana/grafana/pkg/services/user/usertest" ) var ( @@ -30,7 +28,6 @@ func TestAPIKey_Authenticate(t *testing.T) { desc string req *authn.Request expectedKey *apikey.APIKey - expectedUser *user.SignedInUser expectedErr error expectedIdentity *authn.Identity } @@ -72,20 +69,11 @@ func TestAPIKey_Authenticate(t *testing.T) { Key: hash, ServiceAccountId: intPtr(1), }, - expectedUser: &user.SignedInUser{ - UserID: 1, - OrgID: 1, - IsServiceAccount: true, - OrgRole: org.RoleViewer, - Name: "test", - }, expectedIdentity: &authn.Identity{ - ID: "service-account:1", - OrgID: 1, - Name: "test", - OrgRoles: map[int64]org.RoleType{1: org.RoleViewer}, - IsGrafanaAdmin: boolPtr(false), + ID: "service-account:1", + OrgID: 1, ClientParams: authn.ClientParams{ + FetchSyncedUser: true, SyncPermissions: true, }, AuthenticatedBy: login.APIKeyAuthModule, @@ -124,11 +112,7 @@ func TestAPIKey_Authenticate(t *testing.T) { for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { - c := ProvideAPIKey(&apikeytest.Service{ - ExpectedAPIKey: tt.expectedKey, - }, &usertest.FakeUserService{ - ExpectedSignedInUser: tt.expectedUser, - }) + c := ProvideAPIKey(&apikeytest.Service{ExpectedAPIKey: tt.expectedKey}) identity, err := c.Authenticate(context.Background(), tt.req) if tt.expectedErr != nil { @@ -195,7 +179,7 @@ func TestAPIKey_Test(t *testing.T) { for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { - c := ProvideAPIKey(&apikeytest.Service{}, usertest.NewUserServiceFake()) + c := ProvideAPIKey(&apikeytest.Service{}) assert.Equal(t, tt.expected, c.Test(context.Background(), tt.req)) }) } @@ -286,19 +270,11 @@ func TestAPIKey_GetAPIKeyIDFromIdentity(t *testing.T) { }, }} - signedInUser := &user.SignedInUser{ - UserID: 1, - OrgID: 1, - Name: "test", - } - for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { c := ProvideAPIKey(&apikeytest.Service{ ExpectedError: tt.expectedError, ExpectedAPIKey: tt.expectedKey, - }, &usertest.FakeUserService{ - ExpectedSignedInUser: signedInUser, }) id, exists := c.getAPIKeyID(context.Background(), tt.expectedIdentity, req) assert.Equal(t, tt.expectedExists, exists) From 0c6b0188c8fea194310bc78ffa3230f6ae4edbe3 Mon Sep 17 00:00:00 2001 From: Giordano Ricci Date: Mon, 11 Mar 2024 15:17:07 +0000 Subject: [PATCH 0520/1406] Explore: Remove deprecated `query` option from `splitOpen` (#83973) * Chore: remove deplrecated queries option from splitOpen * make queries option required * use left pane queries when splitting an existing pane --- packages/grafana-data/src/types/explore.ts | 4 +--- public/app/features/explore/state/main.ts | 5 +++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/grafana-data/src/types/explore.ts b/packages/grafana-data/src/types/explore.ts index 537104f041..90f21ae568 100644 --- a/packages/grafana-data/src/types/explore.ts +++ b/packages/grafana-data/src/types/explore.ts @@ -58,9 +58,7 @@ export interface ExploreLogsPanelState { export interface SplitOpenOptions { datasourceUid: string; - /** @deprecated Will be removed in a future version. Use queries instead. */ - query?: T; - queries?: T[]; + queries: T[]; range?: TimeRange; panelsState?: ExplorePanelsState; correlationHelperData?: ExploreCorrelationHelperData; diff --git a/public/app/features/explore/state/main.ts b/public/app/features/explore/state/main.ts index ca9e73cc89..7fb7f02622 100644 --- a/public/app/features/explore/state/main.ts +++ b/public/app/features/explore/state/main.ts @@ -61,7 +61,8 @@ export const setPaneState = createAction('explore/set export const clearPanes = createAction('explore/clearPanes'); /** - * Ensure Explore doesn't exceed supported number of panes and initializes the new pane. + * Creates a new Explore pane. + * If 2 panes already exist, the last one (right) is closed before creating a new one. */ export const splitOpen = createAsyncThunk( 'explore/splitOpen', @@ -69,7 +70,7 @@ export const splitOpen = createAsyncThunk( // we currently support showing only 2 panes in explore, so if this action is dispatched we know it has been dispatched from the "first" pane. const originState = Object.values(getState().explore.panes)[0]; - const queries = options?.queries ?? (options?.query ? [options?.query] : originState?.queries || []); + const queries = options?.queries ?? originState?.queries ?? []; Object.keys(getState().explore.panes).forEach((paneId, index) => { // Only 2 panes are supported. Remove panes before create a new one. From 225ac8003c44ab9183a43009829a3e0024516ad1 Mon Sep 17 00:00:00 2001 From: Will Browne Date: Mon, 11 Mar 2024 16:28:46 +0100 Subject: [PATCH 0521/1406] Plugins: Tidy config struct (#84168) * tidy plugins config usage * fix tests --- pkg/api/metrics_test.go | 3 +- pkg/plugins/config/config.go | 13 ++---- pkg/plugins/manager/client/client.go | 5 +-- pkg/plugins/manager/client/client_test.go | 17 ++++---- .../loader/assetpath/assetpath_test.go | 42 ------------------- pkg/plugins/manager/loader/loader_test.go | 41 ------------------ .../angularinspector/angularinspector.go | 5 +-- .../angularinspector/angularinspector_test.go | 18 ++++---- .../pluginsintegration/loader/loader_test.go | 41 ------------------ .../pluginsintegration/pipeline/steps.go | 2 +- .../pluginsintegration/pluginconfig/config.go | 7 ---- .../pluginsintegration/pluginsintegration.go | 9 ++-- .../pluginsintegration/renderer/renderer.go | 29 +++++++------ .../renderer/renderer_test.go | 10 ++--- .../serviceregistration.go | 5 +-- .../pluginsintegration/test_helper.go | 2 +- 16 files changed, 50 insertions(+), 199 deletions(-) diff --git a/pkg/api/metrics_test.go b/pkg/api/metrics_test.go index 992d14305a..8139a4c730 100644 --- a/pkg/api/metrics_test.go +++ b/pkg/api/metrics_test.go @@ -18,7 +18,6 @@ import ( "github.com/grafana/grafana/pkg/infra/localcache" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/backendplugin" - "github.com/grafana/grafana/pkg/plugins/config" pluginClient "github.com/grafana/grafana/pkg/plugins/manager/client" "github.com/grafana/grafana/pkg/plugins/manager/registry" "github.com/grafana/grafana/pkg/services/datasources" @@ -297,7 +296,7 @@ func TestDataSourceQueryError(t *testing.T) { &fakeDatasources.FakeCacheService{}, nil, &fakePluginRequestValidator{}, - pluginClient.ProvideService(r, &config.PluginManagementCfg{}), + pluginClient.ProvideService(r), plugincontext.ProvideService(cfg, localcache.ProvideService(), &pluginstore.FakePluginStore{ PluginList: []pluginstore.Plugin{pluginstore.ToGrafanaDTO(p)}, }, diff --git a/pkg/plugins/config/config.go b/pkg/plugins/config/config.go index 68019f3276..88c4b8364c 100644 --- a/pkg/plugins/config/config.go +++ b/pkg/plugins/config/config.go @@ -19,12 +19,9 @@ type PluginManagementCfg struct { PluginsCDNURLTemplate string - Tracing Tracing - GrafanaComURL string - GrafanaAppURL string - GrafanaAppSubURL string + GrafanaAppURL string Features featuremgmt.FeatureToggles @@ -34,9 +31,9 @@ type PluginManagementCfg struct { // NewPluginManagementCfg returns a new PluginManagementCfg. func NewPluginManagementCfg(devMode bool, pluginsPath string, pluginSettings setting.PluginSettings, pluginsAllowUnsigned []string, - pluginsCDNURLTemplate string, appURL string, appSubURL string, tracing Tracing, features featuremgmt.FeatureToggles, - angularSupportEnabled bool, grafanaComURL string, disablePlugins []string, hideAngularDeprecation []string, - forwardHostEnvVars []string) *PluginManagementCfg { + pluginsCDNURLTemplate string, appURL string, features featuremgmt.FeatureToggles, angularSupportEnabled bool, + grafanaComURL string, disablePlugins []string, hideAngularDeprecation []string, forwardHostEnvVars []string, +) *PluginManagementCfg { return &PluginManagementCfg{ PluginsPath: pluginsPath, DevMode: devMode, @@ -44,10 +41,8 @@ func NewPluginManagementCfg(devMode bool, pluginsPath string, pluginSettings set PluginsAllowUnsigned: pluginsAllowUnsigned, DisablePlugins: disablePlugins, PluginsCDNURLTemplate: pluginsCDNURLTemplate, - Tracing: tracing, GrafanaComURL: grafanaComURL, GrafanaAppURL: appURL, - GrafanaAppSubURL: appSubURL, Features: features, AngularSupportEnabled: angularSupportEnabled, HideAngularDeprecation: hideAngularDeprecation, diff --git a/pkg/plugins/manager/client/client.go b/pkg/plugins/manager/client/client.go index a055882ecd..aed1acd6e6 100644 --- a/pkg/plugins/manager/client/client.go +++ b/pkg/plugins/manager/client/client.go @@ -10,7 +10,6 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana/pkg/plugins" - "github.com/grafana/grafana/pkg/plugins/config" "github.com/grafana/grafana/pkg/plugins/manager/registry" ) @@ -29,13 +28,11 @@ var ( type Service struct { pluginRegistry registry.Service - cfg *config.PluginManagementCfg } -func ProvideService(pluginRegistry registry.Service, cfg *config.PluginManagementCfg) *Service { +func ProvideService(pluginRegistry registry.Service) *Service { return &Service{ pluginRegistry: pluginRegistry, - cfg: cfg, } } diff --git a/pkg/plugins/manager/client/client_test.go b/pkg/plugins/manager/client/client_test.go index c6fe6b19d1..c6649b2f9d 100644 --- a/pkg/plugins/manager/client/client_test.go +++ b/pkg/plugins/manager/client/client_test.go @@ -10,7 +10,6 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/backendplugin" - "github.com/grafana/grafana/pkg/plugins/config" "github.com/grafana/grafana/pkg/plugins/manager/fakes" "github.com/stretchr/testify/require" ) @@ -18,7 +17,7 @@ import ( func TestQueryData(t *testing.T) { t.Run("Empty registry should return not registered error", func(t *testing.T) { registry := fakes.NewFakePluginRegistry() - client := ProvideService(registry, &config.PluginManagementCfg{}) + client := ProvideService(registry) _, err := client.QueryData(context.Background(), &backend.QueryDataRequest{}) require.Error(t, err) require.ErrorIs(t, err, plugins.ErrPluginNotRegistered) @@ -63,7 +62,7 @@ func TestQueryData(t *testing.T) { err := registry.Add(context.Background(), p) require.NoError(t, err) - client := ProvideService(registry, &config.PluginManagementCfg{}) + client := ProvideService(registry) _, err = client.QueryData(context.Background(), &backend.QueryDataRequest{ PluginContext: backend.PluginContext{ PluginID: "grafana", @@ -79,7 +78,7 @@ func TestQueryData(t *testing.T) { func TestCheckHealth(t *testing.T) { t.Run("empty plugin registry should return plugin not registered error", func(t *testing.T) { registry := fakes.NewFakePluginRegistry() - client := ProvideService(registry, &config.PluginManagementCfg{}) + client := ProvideService(registry) _, err := client.CheckHealth(context.Background(), &backend.CheckHealthRequest{}) require.Error(t, err) require.ErrorIs(t, err, plugins.ErrPluginNotRegistered) @@ -125,7 +124,7 @@ func TestCheckHealth(t *testing.T) { err := registry.Add(context.Background(), p) require.NoError(t, err) - client := ProvideService(registry, &config.PluginManagementCfg{}) + client := ProvideService(registry) _, err = client.CheckHealth(context.Background(), &backend.CheckHealthRequest{ PluginContext: backend.PluginContext{ PluginID: "grafana", @@ -189,7 +188,7 @@ func TestCallResource(t *testing.T) { err := registry.Add(context.Background(), p) require.NoError(t, err) - client := ProvideService(registry, &config.PluginManagementCfg{}) + client := ProvideService(registry) err = client.CallResource(context.Background(), req, sender) require.NoError(t, err) @@ -252,7 +251,7 @@ func TestCallResource(t *testing.T) { err := registry.Add(context.Background(), p) require.NoError(t, err) - client := ProvideService(registry, &config.PluginManagementCfg{}) + client := ProvideService(registry) err = client.CallResource(context.Background(), req, sender) require.NoError(t, err) @@ -298,7 +297,7 @@ func TestCallResource(t *testing.T) { err := registry.Add(context.Background(), p) require.NoError(t, err) - client := ProvideService(registry, &config.PluginManagementCfg{}) + client := ProvideService(registry) err = client.CallResource(context.Background(), req, sender) require.NoError(t, err) @@ -366,7 +365,7 @@ func TestCallResource(t *testing.T) { err := registry.Add(context.Background(), p) require.NoError(t, err) - client := ProvideService(registry, &config.PluginManagementCfg{}) + client := ProvideService(registry) err = client.CallResource(context.Background(), req, sender) require.NoError(t, err) diff --git a/pkg/plugins/manager/loader/assetpath/assetpath_test.go b/pkg/plugins/manager/loader/assetpath/assetpath_test.go index 2f9b3d26cb..9802dcfe00 100644 --- a/pkg/plugins/manager/loader/assetpath/assetpath_test.go +++ b/pkg/plugins/manager/loader/assetpath/assetpath_test.go @@ -124,46 +124,4 @@ func TestService(t *testing.T) { }) }) } - - t.Run("App Sub URL has no effect on the path", func(t *testing.T) { - for _, tc := range []struct { - appSubURL string - }{ - { - appSubURL: "grafana", - }, - { - appSubURL: "/grafana", - }, - { - appSubURL: "grafana/", - }, - { - appSubURL: "/grafana/", - }, - } { - cfg := &config.PluginManagementCfg{GrafanaAppSubURL: tc.appSubURL} - svc := ProvideService(cfg, pluginscdn.ProvideService(cfg)) - - dir := "/plugins/test-datasource" - p := plugins.JSONData{ID: "test-datasource"} - fs := fakes.NewFakePluginFiles(dir) - - base, err := svc.Base(NewPluginInfo(p, plugins.ClassExternal, fs)) - require.NoError(t, err) - require.Equal(t, "public/plugins/test-datasource", base) - - mod, err := svc.Module(NewPluginInfo(p, plugins.ClassExternal, fs)) - require.NoError(t, err) - require.Equal(t, "public/plugins/test-datasource/module.js", mod) - - base, err = svc.Base(NewPluginInfo(p, plugins.ClassCore, fs)) - require.NoError(t, err) - require.Equal(t, "public/app/plugins/test-datasource", base) - - mod, err = svc.Module(NewPluginInfo(p, plugins.ClassCore, fs)) - require.NoError(t, err) - require.Equal(t, "core:plugin/test-datasource", mod) - } - }) } diff --git a/pkg/plugins/manager/loader/loader_test.go b/pkg/plugins/manager/loader/loader_test.go index 6eb1322648..3847096853 100644 --- a/pkg/plugins/manager/loader/loader_test.go +++ b/pkg/plugins/manager/loader/loader_test.go @@ -410,47 +410,6 @@ func TestLoader_Load(t *testing.T) { }, }, }, - { - name: "Load a plugin with app sub url set", - class: plugins.ClassExternal, - cfg: &config.PluginManagementCfg{ - DevMode: true, - GrafanaAppSubURL: "grafana", - Features: featuremgmt.WithFeatures(), - }, - pluginPaths: []string{"../testdata/unsigned-datasource"}, - want: []*plugins.Plugin{ - { - JSONData: plugins.JSONData{ - ID: "test-datasource", - Type: plugins.TypeDataSource, - Name: "Test", - Info: plugins.Info{ - Author: plugins.InfoLink{ - Name: "Grafana Labs", - URL: "https://grafana.com", - }, - Logos: plugins.Logos{ - Small: "public/img/icn-datasource.svg", - Large: "public/img/icn-datasource.svg", - }, - Description: "Test", - }, - Dependencies: plugins.Dependencies{ - GrafanaVersion: "*", - Plugins: []plugins.Dependency{}, - }, - Backend: true, - State: plugins.ReleaseStateAlpha, - }, - Class: plugins.ClassExternal, - Module: "public/plugins/test-datasource/module.js", - BaseURL: "public/plugins/test-datasource", - FS: mustNewStaticFSForTests(t, filepath.Join(parentDir, "testdata/unsigned-datasource/plugin")), - Signature: plugins.SignatureStatusUnsigned, - }, - }, - }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/pkg/services/pluginsintegration/angularinspector/angularinspector.go b/pkg/services/pluginsintegration/angularinspector/angularinspector.go index b42fa54869..1fd9f67101 100644 --- a/pkg/services/pluginsintegration/angularinspector/angularinspector.go +++ b/pkg/services/pluginsintegration/angularinspector/angularinspector.go @@ -1,7 +1,6 @@ package angularinspector import ( - "github.com/grafana/grafana/pkg/plugins/config" "github.com/grafana/grafana/pkg/plugins/manager/loader/angular/angulardetector" "github.com/grafana/grafana/pkg/plugins/manager/loader/angular/angularinspector" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -12,11 +11,11 @@ type Service struct { angularinspector.Inspector } -func ProvideService(cfg *config.PluginManagementCfg, dynamic *angulardetectorsprovider.Dynamic) (*Service, error) { +func ProvideService(features featuremgmt.FeatureToggles, dynamic *angulardetectorsprovider.Dynamic) (*Service, error) { var detectorsProvider angulardetector.DetectorsProvider var err error static := angularinspector.NewDefaultStaticDetectorsProvider() - if cfg.Features != nil && cfg.Features.IsEnabledGlobally(featuremgmt.FlagPluginsDynamicAngularDetectionPatterns) { + if features.IsEnabledGlobally(featuremgmt.FlagPluginsDynamicAngularDetectionPatterns) { detectorsProvider = angulardetector.SequenceDetectorsProvider{dynamic, static} } else { detectorsProvider = static diff --git a/pkg/services/pluginsintegration/angularinspector/angularinspector_test.go b/pkg/services/pluginsintegration/angularinspector/angularinspector_test.go index 14b288995b..b95b6d37ed 100644 --- a/pkg/services/pluginsintegration/angularinspector/angularinspector_test.go +++ b/pkg/services/pluginsintegration/angularinspector/angularinspector_test.go @@ -17,14 +17,14 @@ import ( func TestProvideService(t *testing.T) { t.Run("uses hardcoded inspector if feature flag is not present", func(t *testing.T) { - pCfg := &config.PluginManagementCfg{Features: featuremgmt.WithFeatures()} + features := featuremgmt.WithFeatures() dynamic, err := angulardetectorsprovider.ProvideDynamic( - pCfg, + &config.PluginManagementCfg{}, angularpatternsstore.ProvideService(kvstore.NewFakeKVStore()), - featuremgmt.WithFeatures(featuremgmt.FlagPluginsDynamicAngularDetectionPatterns), + features, ) require.NoError(t, err) - inspector, err := ProvideService(pCfg, dynamic) + inspector, err := ProvideService(features, dynamic) require.NoError(t, err) require.IsType(t, inspector.Inspector, &angularinspector.PatternsListInspector{}) patternsListInspector := inspector.Inspector.(*angularinspector.PatternsListInspector) @@ -33,16 +33,16 @@ func TestProvideService(t *testing.T) { }) t.Run("uses dynamic inspector with hardcoded fallback if feature flag is present", func(t *testing.T) { - pCfg := &config.PluginManagementCfg{Features: featuremgmt.WithFeatures( + features := featuremgmt.WithFeatures( featuremgmt.FlagPluginsDynamicAngularDetectionPatterns, - )} + ) dynamic, err := angulardetectorsprovider.ProvideDynamic( - pCfg, + &config.PluginManagementCfg{}, angularpatternsstore.ProvideService(kvstore.NewFakeKVStore()), - featuremgmt.WithFeatures(), + features, ) require.NoError(t, err) - inspector, err := ProvideService(pCfg, dynamic) + inspector, err := ProvideService(features, dynamic) require.NoError(t, err) require.IsType(t, inspector.Inspector, &angularinspector.PatternsListInspector{}) require.IsType(t, inspector.Inspector.(*angularinspector.PatternsListInspector).DetectorsProvider, angulardetector.SequenceDetectorsProvider{}) diff --git a/pkg/services/pluginsintegration/loader/loader_test.go b/pkg/services/pluginsintegration/loader/loader_test.go index c2327558f8..4f09dc249c 100644 --- a/pkg/services/pluginsintegration/loader/loader_test.go +++ b/pkg/services/pluginsintegration/loader/loader_test.go @@ -440,47 +440,6 @@ func TestLoader_Load(t *testing.T) { }, }, }, - { - name: "Load a plugin with app sub url set", - class: plugins.ClassExternal, - cfg: &config.PluginManagementCfg{ - DevMode: true, - GrafanaAppSubURL: "grafana", - Features: featuremgmt.WithFeatures(), - }, - pluginPaths: []string{filepath.Join(testDataDir(t), "unsigned-datasource")}, - want: []*plugins.Plugin{ - { - JSONData: plugins.JSONData{ - ID: "test-datasource", - Type: plugins.TypeDataSource, - Name: "Test", - Info: plugins.Info{ - Author: plugins.InfoLink{ - Name: "Grafana Labs", - URL: "https://grafana.com", - }, - Logos: plugins.Logos{ - Small: "public/img/icn-datasource.svg", - Large: "public/img/icn-datasource.svg", - }, - Description: "Test", - }, - Dependencies: plugins.Dependencies{ - GrafanaVersion: "*", - Plugins: []plugins.Dependency{}, - }, - Backend: true, - State: plugins.ReleaseStateAlpha, - }, - Class: plugins.ClassExternal, - Module: "public/plugins/test-datasource/module.js", - BaseURL: "public/plugins/test-datasource", - FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "unsigned-datasource/plugin")), - Signature: plugins.SignatureStatusUnsigned, - }, - }, - }, } for _, tt := range tests { reg := fakes.NewFakePluginRegistry() diff --git a/pkg/services/pluginsintegration/pipeline/steps.go b/pkg/services/pluginsintegration/pipeline/steps.go index ea798c2dd2..61a35a7ded 100644 --- a/pkg/services/pluginsintegration/pipeline/steps.go +++ b/pkg/services/pluginsintegration/pipeline/steps.go @@ -167,7 +167,7 @@ type AsExternal struct { cfg *config.PluginManagementCfg } -// NewDisablePluginsStep returns a new DisablePlugins. +// NewAsExternalStep returns a new DisablePlugins. func NewAsExternalStep(cfg *config.PluginManagementCfg) *AsExternal { return &AsExternal{ cfg: cfg, diff --git a/pkg/services/pluginsintegration/pluginconfig/config.go b/pkg/services/pluginsintegration/pluginconfig/config.go index 0f2097341c..46a5f7b936 100644 --- a/pkg/services/pluginsintegration/pluginconfig/config.go +++ b/pkg/services/pluginsintegration/pluginconfig/config.go @@ -22,11 +22,6 @@ func ProvidePluginManagementConfig(cfg *setting.Cfg, settingProvider setting.Pro allowedUnsigned = strings.Split(plugins.KeyValue("allow_loading_unsigned_plugins").Value(), ",") } - tracingCfg, err := newTracingCfg(cfg) - if err != nil { - return nil, fmt.Errorf("new opentelemetry cfg: %w", err) - } - return config.NewPluginManagementCfg( settingProvider.KeyValue("", "app_mode").MustBool(cfg.Env == setting.Dev), cfg.PluginsPath, @@ -34,8 +29,6 @@ func ProvidePluginManagementConfig(cfg *setting.Cfg, settingProvider setting.Pro allowedUnsigned, cfg.PluginsCDNURLTemplate, cfg.AppURL, - cfg.AppSubURL, - tracingCfg, features, cfg.AngularSupportEnabled, cfg.GrafanaComURL, diff --git a/pkg/services/pluginsintegration/pluginsintegration.go b/pkg/services/pluginsintegration/pluginsintegration.go index 143f91e60d..43d5ec020a 100644 --- a/pkg/services/pluginsintegration/pluginsintegration.go +++ b/pkg/services/pluginsintegration/pluginsintegration.go @@ -9,7 +9,6 @@ import ( "github.com/grafana/grafana/pkg/plugins/auth" "github.com/grafana/grafana/pkg/plugins/backendplugin/coreplugin" "github.com/grafana/grafana/pkg/plugins/backendplugin/provider" - pCfg "github.com/grafana/grafana/pkg/plugins/config" "github.com/grafana/grafana/pkg/plugins/envvars" "github.com/grafana/grafana/pkg/plugins/log" "github.com/grafana/grafana/pkg/plugins/manager/client" @@ -136,7 +135,7 @@ var WireExtensionSet = wire.NewSet( ) func ProvideClientDecorator( - cfg *setting.Cfg, pCfg *pCfg.PluginManagementCfg, + cfg *setting.Cfg, pluginRegistry registry.Service, oAuthTokenService oauthtoken.OAuthTokenService, tracer tracing.Tracer, @@ -144,16 +143,16 @@ func ProvideClientDecorator( features *featuremgmt.FeatureManager, promRegisterer prometheus.Registerer, ) (*client.Decorator, error) { - return NewClientDecorator(cfg, pCfg, pluginRegistry, oAuthTokenService, tracer, cachingService, features, promRegisterer, pluginRegistry) + return NewClientDecorator(cfg, pluginRegistry, oAuthTokenService, tracer, cachingService, features, promRegisterer, pluginRegistry) } func NewClientDecorator( - cfg *setting.Cfg, pCfg *pCfg.PluginManagementCfg, + cfg *setting.Cfg, pluginRegistry registry.Service, oAuthTokenService oauthtoken.OAuthTokenService, tracer tracing.Tracer, cachingService caching.CachingService, features *featuremgmt.FeatureManager, promRegisterer prometheus.Registerer, registry registry.Service, ) (*client.Decorator, error) { - c := client.ProvideService(pluginRegistry, pCfg) + c := client.ProvideService(pluginRegistry) middlewares := CreateMiddlewares(cfg, oAuthTokenService, tracer, cachingService, features, promRegisterer, registry) return client.NewDecorator(c, middlewares...) } diff --git a/pkg/services/pluginsintegration/renderer/renderer.go b/pkg/services/pluginsintegration/renderer/renderer.go index 57a276c6ce..178c37740b 100644 --- a/pkg/services/pluginsintegration/renderer/renderer.go +++ b/pkg/services/pluginsintegration/renderer/renderer.go @@ -8,7 +8,7 @@ import ( "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/backendplugin/pluginextensionv2" "github.com/grafana/grafana/pkg/plugins/backendplugin/provider" - pluginscfg "github.com/grafana/grafana/pkg/plugins/config" + "github.com/grafana/grafana/pkg/plugins/config" "github.com/grafana/grafana/pkg/plugins/envvars" "github.com/grafana/grafana/pkg/plugins/manager/loader" "github.com/grafana/grafana/pkg/plugins/manager/pipeline/bootstrap" @@ -21,12 +21,11 @@ import ( "github.com/grafana/grafana/pkg/plugins/manager/sources" "github.com/grafana/grafana/pkg/services/pluginsintegration/pipeline" "github.com/grafana/grafana/pkg/services/rendering" - "github.com/grafana/grafana/pkg/setting" ) -func ProvideService(cfg *setting.Cfg, pCfg *pluginscfg.PluginManagementCfg, pluginEnvProvider envvars.Provider, registry registry.Service, - licensing plugins.Licensing) (*Manager, error) { - l, err := createLoader(cfg, pCfg, pluginEnvProvider, registry, licensing) +func ProvideService(cfg *config.PluginManagementCfg, pluginEnvProvider envvars.Provider, + registry registry.Service) (*Manager, error) { + l, err := createLoader(cfg, pluginEnvProvider, registry) if err != nil { return nil, err } @@ -35,14 +34,14 @@ func ProvideService(cfg *setting.Cfg, pCfg *pluginscfg.PluginManagementCfg, plug } type Manager struct { - cfg *setting.Cfg + cfg *config.PluginManagementCfg loader loader.Service log log.Logger renderer *Plugin } -func NewManager(cfg *setting.Cfg, loader loader.Service) *Manager { +func NewManager(cfg *config.PluginManagementCfg, loader loader.Service) *Manager { return &Manager{ cfg: cfg, loader: loader, @@ -104,9 +103,9 @@ func (m *Manager) Renderer(ctx context.Context) (rendering.Plugin, bool) { return nil, false } -func createLoader(cfg *setting.Cfg, pCfg *pluginscfg.PluginManagementCfg, pluginEnvProvider envvars.Provider, - pr registry.Service, l plugins.Licensing) (loader.Service, error) { - d := discovery.New(pCfg, discovery.Opts{ +func createLoader(cfg *config.PluginManagementCfg, pluginEnvProvider envvars.Provider, + pr registry.Service) (loader.Service, error) { + d := discovery.New(cfg, discovery.Opts{ FindFilterFuncs: []discovery.FindFilterFunc{ discovery.NewPermittedPluginTypesFilterStep([]plugins.Type{plugins.TypeRenderer}), func(ctx context.Context, class plugins.Class, bundles []*plugins.FoundBundle) ([]*plugins.FoundBundle, error) { @@ -114,21 +113,21 @@ func createLoader(cfg *setting.Cfg, pCfg *pluginscfg.PluginManagementCfg, plugin }, }, }) - b := bootstrap.New(pCfg, bootstrap.Opts{ + b := bootstrap.New(cfg, bootstrap.Opts{ DecorateFuncs: []bootstrap.DecorateFunc{}, // no decoration required }) - v := validation.New(pCfg, validation.Opts{ + v := validation.New(cfg, validation.Opts{ ValidateFuncs: []validation.ValidateFunc{ - validation.SignatureValidationStep(signature.NewValidator(signature.NewUnsignedAuthorizer(pCfg))), + validation.SignatureValidationStep(signature.NewValidator(signature.NewUnsignedAuthorizer(cfg))), }, }) - i := initialization.New(pCfg, initialization.Opts{ + i := initialization.New(cfg, initialization.Opts{ InitializeFuncs: []initialization.InitializeFunc{ initialization.BackendClientInitStep(pluginEnvProvider, provider.New(provider.RendererProvider)), initialization.PluginRegistrationStep(pr), }, }) - t, err := termination.New(pCfg, termination.Opts{ + t, err := termination.New(cfg, termination.Opts{ TerminateFuncs: []termination.TerminateFunc{ termination.DeregisterStep(pr), }, diff --git a/pkg/services/pluginsintegration/renderer/renderer_test.go b/pkg/services/pluginsintegration/renderer/renderer_test.go index 0c5bdbfd90..2f7f5d0c91 100644 --- a/pkg/services/pluginsintegration/renderer/renderer_test.go +++ b/pkg/services/pluginsintegration/renderer/renderer_test.go @@ -9,8 +9,8 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/plugins/config" "github.com/grafana/grafana/pkg/plugins/manager/fakes" - "github.com/grafana/grafana/pkg/setting" ) func TestRenderer(t *testing.T) { @@ -33,9 +33,7 @@ func TestRenderer(t *testing.T) { return nil, nil }, } - cfg := &setting.Cfg{ - PluginsPath: filepath.Join(testdataDir), - } + cfg := &config.PluginManagementCfg{PluginsPath: filepath.Join(testdataDir)} m := NewManager(cfg, loader) @@ -67,9 +65,7 @@ func TestRenderer(t *testing.T) { return nil, nil }, } - cfg := &setting.Cfg{ - PluginsPath: filepath.Join(testdataDir), - } + cfg := &config.PluginManagementCfg{PluginsPath: filepath.Join(testdataDir)} m := NewManager(cfg, loader) diff --git a/pkg/services/pluginsintegration/serviceregistration/serviceregistration.go b/pkg/services/pluginsintegration/serviceregistration/serviceregistration.go index 0f7ad32115..2658ea5659 100644 --- a/pkg/services/pluginsintegration/serviceregistration/serviceregistration.go +++ b/pkg/services/pluginsintegration/serviceregistration/serviceregistration.go @@ -5,7 +5,6 @@ import ( "errors" "github.com/grafana/grafana/pkg/plugins/auth" - "github.com/grafana/grafana/pkg/plugins/config" "github.com/grafana/grafana/pkg/plugins/log" "github.com/grafana/grafana/pkg/plugins/pfs" "github.com/grafana/grafana/pkg/services/accesscontrol" @@ -21,9 +20,9 @@ type Service struct { settingsSvc pluginsettings.Service } -func ProvideService(cfg *config.PluginManagementCfg, reg extsvcauth.ExternalServiceRegistry, settingsSvc pluginsettings.Service) *Service { +func ProvideService(features featuremgmt.FeatureToggles, reg extsvcauth.ExternalServiceRegistry, settingsSvc pluginsettings.Service) *Service { s := &Service{ - featureEnabled: cfg.Features.IsEnabledGlobally(featuremgmt.FlagExternalServiceAccounts), + featureEnabled: features.IsEnabledGlobally(featuremgmt.FlagExternalServiceAccounts), log: log.New("plugins.external.registration"), reg: reg, settingsSvc: settingsSvc, diff --git a/pkg/services/pluginsintegration/test_helper.go b/pkg/services/pluginsintegration/test_helper.go index 7d6670e430..1242bf2d3e 100644 --- a/pkg/services/pluginsintegration/test_helper.go +++ b/pkg/services/pluginsintegration/test_helper.go @@ -70,7 +70,7 @@ func CreateIntegrationTestCtx(t *testing.T, cfg *setting.Cfg, coreRegistry *core require.NoError(t, err) return &IntegrationTestCtx{ - PluginClient: client.ProvideService(reg, pCfg), + PluginClient: client.ProvideService(reg), PluginStore: ps, PluginRegistry: reg, } From 7d1ea1c655f166dc99510f9b47dd585f19f9c419 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 15:09:29 +0000 Subject: [PATCH 0522/1406] Update dependency rudder-sdk-js to v2.48.3 --- package.json | 2 +- yarn.lock | 206 +++++++++------------------------------------------ 2 files changed, 35 insertions(+), 173 deletions(-) diff --git a/package.json b/package.json index c015086c9a..eaacc0dd16 100644 --- a/package.json +++ b/package.json @@ -206,7 +206,7 @@ "react-test-renderer": "18.2.0", "redux-mock-store": "1.5.4", "rimraf": "5.0.5", - "rudder-sdk-js": "2.48.2", + "rudder-sdk-js": "2.48.3", "sass": "1.70.0", "sass-loader": "14.1.1", "style-loader": "3.3.4", diff --git a/yarn.lock b/yarn.lock index 91e8bdfc89..430f28f8e5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5194,16 +5194,6 @@ __metadata: languageName: node linkType: hard -"@mswjs/cookies@npm:^0.2.2": - version: 0.2.2 - resolution: "@mswjs/cookies@npm:0.2.2" - dependencies: - "@types/set-cookie-parser": "npm:^2.4.0" - set-cookie-parser: "npm:^2.4.6" - checksum: 10/f1b3b82a6821219494390d77d86383febc5f9d5bc21b0f47cc4d57d11af08cac1952d845011d8842ec6448a95e49efd0f35f6d56650c76a98848d70d9c78466d - languageName: node - linkType: hard - "@mswjs/cookies@npm:^1.1.0": version: 1.1.0 resolution: "@mswjs/cookies@npm:1.1.0" @@ -5211,22 +5201,6 @@ __metadata: languageName: node linkType: hard -"@mswjs/interceptors@npm:^0.17.10": - version: 0.17.10 - resolution: "@mswjs/interceptors@npm:0.17.10" - dependencies: - "@open-draft/until": "npm:^1.0.3" - "@types/debug": "npm:^4.1.7" - "@xmldom/xmldom": "npm:^0.8.3" - debug: "npm:^4.3.3" - headers-polyfill: "npm:3.2.5" - outvariant: "npm:^1.2.1" - strict-event-emitter: "npm:^0.2.4" - web-encoding: "npm:^1.1.5" - checksum: 10/0bbadfc3c925016d9f26f5bc0aa8833a1ec0065a04933c30f5d7b1f636f39c3458f5dc653d6418e5733523846626e84049a72ec913f70282d7b53bfef2a1aa81 - languageName: node - linkType: hard - "@mswjs/interceptors@npm:^0.25.16": version: 0.25.16 resolution: "@mswjs/interceptors@npm:0.25.16" @@ -5718,13 +5692,6 @@ __metadata: languageName: node linkType: hard -"@open-draft/until@npm:^1.0.3": - version: 1.0.3 - resolution: "@open-draft/until@npm:1.0.3" - checksum: 10/323e92ebef0150ed0f8caedc7d219b68cdc50784fa4eba0377eef93533d3f46514eb2400ced83dda8c51bddc3d2c7b8e9cf95e5ec85ab7f62dfc015d174f62f2 - languageName: node - linkType: hard - "@open-draft/until@npm:^2.0.0, @open-draft/until@npm:^2.1.0": version: 2.1.0 resolution: "@open-draft/until@npm:2.1.0" @@ -8799,13 +8766,6 @@ __metadata: languageName: node linkType: hard -"@types/cookie@npm:^0.4.1": - version: 0.4.1 - resolution: "@types/cookie@npm:0.4.1" - checksum: 10/427c9220217d3d74f3e5d53d68cd39502f3bbebdb1af4ecf0d05076bcbe9ddab299ad6369fe0f517389296ba4ca49ddf9a8c22f68e5e9eb8ae6d0076cfab90b2 - languageName: node - linkType: hard - "@types/cookie@npm:^0.6.0": version: 0.6.0 resolution: "@types/cookie@npm:0.6.0" @@ -9133,15 +9093,6 @@ __metadata: languageName: node linkType: hard -"@types/debug@npm:^4.1.7": - version: 4.1.7 - resolution: "@types/debug@npm:4.1.7" - dependencies: - "@types/ms": "npm:*" - checksum: 10/0a7b89d8ed72526858f0b61c6fd81f477853e8c4415bb97f48b1b5545248d2ae389931680b94b393b993a7cfe893537a200647d93defe6d87159b96812305adc - languageName: node - linkType: hard - "@types/detect-port@npm:^1.3.0": version: 1.3.2 resolution: "@types/detect-port@npm:1.3.2" @@ -9447,13 +9398,6 @@ __metadata: languageName: node linkType: hard -"@types/js-levenshtein@npm:^1.1.1": - version: 1.1.1 - resolution: "@types/js-levenshtein@npm:1.1.1" - checksum: 10/1d1ff1ee2ad551909e47f3ce19fcf85b64dc5146d3b531c8d26fc775492d36e380b32cf5ef68ff301e812c3b00282f37aac579ebb44498b94baff0ace7509769 - languageName: node - linkType: hard - "@types/js-yaml@npm:^4.0.5": version: 4.0.9 resolution: "@types/js-yaml@npm:4.0.9" @@ -9595,13 +9539,6 @@ __metadata: languageName: node linkType: hard -"@types/ms@npm:*": - version: 0.7.31 - resolution: "@types/ms@npm:0.7.31" - checksum: 10/6647b295fb2a5b8347c35efabaaed1777221f094be9941d387b4bf11df0eeacb3f8a4e495b8b66ce0e4c00593bc53ab5fc25f01ebb274cd989a834ae578099de - languageName: node - linkType: hard - "@types/mute-stream@npm:^0.0.4": version: 0.0.4 resolution: "@types/mute-stream@npm:0.0.4" @@ -9988,15 +9925,6 @@ __metadata: languageName: node linkType: hard -"@types/set-cookie-parser@npm:^2.4.0": - version: 2.4.2 - resolution: "@types/set-cookie-parser@npm:2.4.2" - dependencies: - "@types/node": "npm:*" - checksum: 10/c31bf04eb9620829dc3c91bced74ac934ad039d20d20893fb5acac0f08769cbd4eef3bf7502a0289c7be59c3e9cfa456147b4e88bff47dd1b9efb4995ba5d5a3 - languageName: node - linkType: hard - "@types/sinonjs__fake-timers@npm:8.1.1": version: 8.1.1 resolution: "@types/sinonjs__fake-timers@npm:8.1.1" @@ -11027,13 +10955,6 @@ __metadata: languageName: node linkType: hard -"@xmldom/xmldom@npm:^0.8.3": - version: 0.8.6 - resolution: "@xmldom/xmldom@npm:0.8.6" - checksum: 10/f2fd5c1a966d2bdd9cad8b7316dead4fb4832c44a102360c593287b2e10e357a5d162145ab13fa8efe8b07172d058b2a7550f07ca0fa0bee11e54a6d9d22f899 - languageName: node - linkType: hard - "@xobotyi/scrollbar-width@npm:^1.9.5": version: 1.9.5 resolution: "@xobotyi/scrollbar-width@npm:1.9.5" @@ -11114,13 +11035,6 @@ __metadata: languageName: node linkType: hard -"@zxing/text-encoding@npm:0.9.0": - version: 0.9.0 - resolution: "@zxing/text-encoding@npm:0.9.0" - checksum: 10/268e4ef64b8eaa32b990240bdfd1f7b3e2b501a6ed866a565f7c9747f04ac884fbe0537fe12bb05d9241b98fb111270c0fd0023ef0a02d23a6619b4589e98f6b - languageName: node - linkType: hard - "JSONStream@npm:^1.3.5": version: 1.3.5 resolution: "JSONStream@npm:1.3.5" @@ -12966,7 +12880,7 @@ __metadata: languageName: node linkType: hard -"chokidar@npm:>=3.0.0 <4.0.0, chokidar@npm:^3.3.1, chokidar@npm:^3.4.2, chokidar@npm:^3.5.3, chokidar@npm:^3.6.0": +"chokidar@npm:>=3.0.0 <4.0.0, chokidar@npm:^3.3.1, chokidar@npm:^3.5.3, chokidar@npm:^3.6.0": version: 3.6.0 resolution: "chokidar@npm:3.6.0" dependencies: @@ -13456,10 +13370,10 @@ __metadata: languageName: node linkType: hard -"component-emitter@npm:1.3.0": - version: 1.3.0 - resolution: "component-emitter@npm:1.3.0" - checksum: 10/dfc1ec2e7aa2486346c068f8d764e3eefe2e1ca0b24f57506cd93b2ae3d67829a7ebd7cc16e2bf51368fac2f45f78fcff231718e40b1975647e4a86be65e1d05 +"component-emitter@npm:2.0.0": + version: 2.0.0 + resolution: "component-emitter@npm:2.0.0" + checksum: 10/017715272fcf82203932237260451df4c7c27e32a51a4a291faf6f503d6ef9e8583add993850cb5b98cc0c1b0846ff0c68938ad3ef1d544f9b480a290e74fb4f languageName: node linkType: hard @@ -13685,7 +13599,7 @@ __metadata: languageName: node linkType: hard -"cookie@npm:0.4.2, cookie@npm:^0.4.2": +"cookie@npm:0.4.2": version: 0.4.2 resolution: "cookie@npm:0.4.2" checksum: 10/2e1de9fdedca54881eab3c0477aeb067f281f3155d9cfee9d28dfb252210d09e85e9d175c0a60689661feb9e35e588515352f2456bc1f8e8db4267e05fd70137 @@ -18600,7 +18514,7 @@ __metadata: regenerator-runtime: "npm:0.14.1" reselect: "npm:4.1.8" rimraf: "npm:5.0.5" - rudder-sdk-js: "npm:2.48.2" + rudder-sdk-js: "npm:2.48.3" rxjs: "npm:7.8.1" sass: "npm:1.70.0" sass-loader: "npm:14.1.1" @@ -18861,13 +18775,6 @@ __metadata: languageName: node linkType: hard -"headers-polyfill@npm:3.2.5": - version: 3.2.5 - resolution: "headers-polyfill@npm:3.2.5" - checksum: 10/3aa62d23091576c05722e8043879a3a6beb9fdd85719780248d628ef8df232eb8261522ae2edb8dd6d0a991d7c744f7382c22e279bc81690f8da39502bc62c4c - languageName: node - linkType: hard - "headers-polyfill@npm:^4.0.2": version: 4.0.2 resolution: "headers-polyfill@npm:4.0.2" @@ -19613,7 +19520,7 @@ __metadata: languageName: node linkType: hard -"inquirer@npm:^8.2.0, inquirer@npm:^8.2.4": +"inquirer@npm:^8.2.4": version: 8.2.5 resolution: "inquirer@npm:8.2.5" dependencies: @@ -21129,13 +21036,6 @@ __metadata: languageName: node linkType: hard -"js-levenshtein@npm:^1.1.6": - version: 1.1.6 - resolution: "js-levenshtein@npm:1.1.6" - checksum: 10/bb034043fdebab606122fe5b5c0316036f1bb0ea352038af8b0ba4cda4b016303b24f64efb59d9918f66e3680eea97ff421396ff3c153cb00a6f982908f61f8a - languageName: node - linkType: hard - "js-tokens@npm:^3.0.0 || ^4.0.0, js-tokens@npm:^4.0.0": version: 4.0.0 resolution: "js-tokens@npm:4.0.0" @@ -23055,37 +22955,35 @@ __metadata: languageName: node linkType: hard -"msw@npm:1.3.2": - version: 1.3.2 - resolution: "msw@npm:1.3.2" +"msw@npm:2.2.1": + version: 2.2.1 + resolution: "msw@npm:2.2.1" dependencies: - "@mswjs/cookies": "npm:^0.2.2" - "@mswjs/interceptors": "npm:^0.17.10" - "@open-draft/until": "npm:^1.0.3" - "@types/cookie": "npm:^0.4.1" - "@types/js-levenshtein": "npm:^1.1.1" - chalk: "npm:^4.1.1" - chokidar: "npm:^3.4.2" - cookie: "npm:^0.4.2" + "@bundled-es-modules/cookie": "npm:^2.0.0" + "@bundled-es-modules/statuses": "npm:^1.0.1" + "@inquirer/confirm": "npm:^3.0.0" + "@mswjs/cookies": "npm:^1.1.0" + "@mswjs/interceptors": "npm:^0.25.16" + "@open-draft/until": "npm:^2.1.0" + "@types/cookie": "npm:^0.6.0" + "@types/statuses": "npm:^2.0.4" + chalk: "npm:^4.1.2" graphql: "npm:^16.8.1" - headers-polyfill: "npm:3.2.5" - inquirer: "npm:^8.2.0" + headers-polyfill: "npm:^4.0.2" is-node-process: "npm:^1.2.0" - js-levenshtein: "npm:^1.1.6" - node-fetch: "npm:^2.6.7" - outvariant: "npm:^1.4.0" + outvariant: "npm:^1.4.2" path-to-regexp: "npm:^6.2.0" - strict-event-emitter: "npm:^0.4.3" - type-fest: "npm:^2.19.0" - yargs: "npm:^17.3.1" + strict-event-emitter: "npm:^0.5.1" + type-fest: "npm:^4.9.0" + yargs: "npm:^17.7.2" peerDependencies: - typescript: ">= 4.4.x <= 5.2.x" + typescript: ">= 4.7.x <= 5.3.x" peerDependenciesMeta: typescript: optional: true bin: msw: cli/index.js - checksum: 10/9864406faf9ee3381ebfdad1f06eda1468c0f34041238aba2f8eb44690f4a2d5949c68e414105a14c869aff7bd03ef61606f4acccd33735ba34d5cf290cfb189 + checksum: 10/0b07a987cc2ab950ce6c1a3c69a3e4027f0b7cdc9d2e971c3efc1fed0993eaaef6714bd0f2b473752334277c7dbeda3227284b132e519dcc951c29de312e862f languageName: node linkType: hard @@ -27709,17 +27607,17 @@ __metadata: languageName: node linkType: hard -"rudder-sdk-js@npm:2.48.2": - version: 2.48.2 - resolution: "rudder-sdk-js@npm:2.48.2" +"rudder-sdk-js@npm:2.48.3": + version: 2.48.3 + resolution: "rudder-sdk-js@npm:2.48.3" dependencies: "@lukeed/uuid": "npm:2.0.1" "@segment/localstorage-retry": "npm:1.3.0" - component-emitter: "npm:1.3.0" + component-emitter: "npm:2.0.0" get-value: "npm:3.0.1" - msw: "npm:1.3.2" + msw: "npm:2.2.1" ramda: "npm:0.29.1" - checksum: 10/56f9ac8a838a21a9b86b0161e69e6520388779cdcb108447d7bdb06111d61c6b44c29a536d9c08b6714745c40f7fdd4ce1d201eda4089f5230fc63fc7b9b5dd9 + checksum: 10/cbf545a25a3da461a49a04ce799dc99d2b2600160b8d6ec522878ff6fbe50d1683c7ac4cde86dd75dd72b49ce84b1de7241e1fdb981bc1d4f36d2a7e8be9ea22 languageName: node linkType: hard @@ -28080,13 +27978,6 @@ __metadata: languageName: node linkType: hard -"set-cookie-parser@npm:^2.4.6": - version: 2.5.1 - resolution: "set-cookie-parser@npm:2.5.1" - checksum: 10/affa51ad3a4c21e947e4aa58a7b2c84661126b972b7ef84a95ffa36c4c3c8ff0f35d031e8c4a3239c5729778a0edaf5a9cad5eeb46d46721fa0584e9ba3d57ef - languageName: node - linkType: hard - "set-function-name@npm:^2.0.0, set-function-name@npm:^2.0.1": version: 2.0.1 resolution: "set-function-name@npm:2.0.1" @@ -28982,22 +28873,6 @@ __metadata: languageName: node linkType: hard -"strict-event-emitter@npm:^0.2.4": - version: 0.2.8 - resolution: "strict-event-emitter@npm:0.2.8" - dependencies: - events: "npm:^3.3.0" - checksum: 10/6ac06fe72a6ee6ae64d20f1dd42838ea67342f1b5f32b03b3050d73ee6ecee44b4d5c4ed2965a7154b47991e215f373d4e789e2b2be2769cd80e356126c2ca53 - languageName: node - linkType: hard - -"strict-event-emitter@npm:^0.4.3": - version: 0.4.6 - resolution: "strict-event-emitter@npm:0.4.6" - checksum: 10/abdbf59b6c45b599cc2f227fa473765d1510d155ebd22533e8ecb06110dfacb2ff07aece7fd528dde2b4f9e379d60f2687eee8af3fa2877c3ed88ee5b7ed2707 - languageName: node - linkType: hard - "strict-event-emitter@npm:^0.5.1": version: 0.5.1 resolution: "strict-event-emitter@npm:0.5.1" @@ -30769,7 +30644,7 @@ __metadata: languageName: node linkType: hard -"util@npm:^0.12.0, util@npm:^0.12.3, util@npm:^0.12.4": +"util@npm:^0.12.0, util@npm:^0.12.4": version: 0.12.5 resolution: "util@npm:0.12.5" dependencies: @@ -31083,19 +30958,6 @@ __metadata: languageName: node linkType: hard -"web-encoding@npm:^1.1.5": - version: 1.1.5 - resolution: "web-encoding@npm:1.1.5" - dependencies: - "@zxing/text-encoding": "npm:0.9.0" - util: "npm:^0.12.3" - dependenciesMeta: - "@zxing/text-encoding": - optional: true - checksum: 10/243518cfa8388ac05eeb4041bd330d38c599476ff9a93239b386d1ba2af130089a2fcefb0cf65b385f989105ff460ae69dca7e42236f4d98dc776b04e558cdb5 - languageName: node - linkType: hard - "web-vitals@npm:^3.1.1": version: 3.1.1 resolution: "web-vitals@npm:3.1.1" From 3bb38d82abea58b9cb9148034ffe18561cf9421f Mon Sep 17 00:00:00 2001 From: ismail simsek Date: Mon, 11 Mar 2024 16:49:53 +0100 Subject: [PATCH 0523/1406] Chore: Bump grafana-plugin-sdk-go version to v0.214.0 (#84162) * bump grafana-plugin-sdk-go version to v0.214.0 * make swagger-clean && make openapi3-gen --- go.mod | 13 +++-- go.sum | 17 ++++--- go.work.sum | 26 ++++++++++ pkg/apimachinery/go.mod | 2 +- pkg/apimachinery/go.sum | 2 +- pkg/apiserver/go.mod | 77 ++++++++++++++++++++++------- pkg/apiserver/go.sum | 98 +++++++++++++++++++++++++++---------- pkg/util/maputil/maputil.go | 73 --------------------------- public/api-merged.json | 4 +- public/openapi3.json | 4 +- 10 files changed, 182 insertions(+), 134 deletions(-) delete mode 100644 pkg/util/maputil/maputil.go diff --git a/go.mod b/go.mod index fb5204c52b..9f83190cea 100644 --- a/go.mod +++ b/go.mod @@ -63,7 +63,7 @@ require ( github.com/grafana/cuetsy v0.1.11 // @grafana/grafana-as-code github.com/grafana/grafana-aws-sdk v0.24.0 // @grafana/aws-datasources github.com/grafana/grafana-azure-sdk-go v1.12.0 // @grafana/partner-datasources - github.com/grafana/grafana-plugin-sdk-go v0.213.0 // @grafana/plugins-platform-backend + github.com/grafana/grafana-plugin-sdk-go v0.214.0 // @grafana/plugins-platform-backend github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // @grafana/backend-platform github.com/hashicorp/go-hclog v1.6.2 // @grafana/plugins-platform-backend github.com/hashicorp/go-plugin v1.6.0 // @grafana/plugins-platform-backend @@ -256,7 +256,7 @@ require ( github.com/Masterminds/semver/v3 v3.1.1 // @grafana/grafana-release-guild github.com/alicebob/miniredis/v2 v2.30.1 // @grafana/alerting-squad-backend github.com/dave/dst v0.27.2 // @grafana/grafana-as-code - github.com/go-jose/go-jose/v3 v3.0.1 // @grafana/grafana-authnz-team + github.com/go-jose/go-jose/v3 v3.0.3 // @grafana/grafana-authnz-team github.com/grafana/dataplane/examples v0.0.1 // @grafana/observability-metrics github.com/grafana/dataplane/sdata v0.0.7 // @grafana/observability-metrics github.com/grafana/kindsys v0.0.0-20230508162304-452481b63482 // @grafana/grafana-as-code @@ -264,7 +264,7 @@ require ( github.com/grafana/thema v0.0.0-20230712153715-375c1b45f3ed // @grafana/grafana-as-code github.com/microsoft/go-mssqldb v1.6.1-0.20240214161942-b65008136246 // @grafana/grafana-bi-squad github.com/redis/go-redis/v9 v9.0.2 // @grafana/alerting-squad-backend - go.opentelemetry.io/contrib/samplers/jaegerremote v0.16.0 // @grafana/backend-platform + go.opentelemetry.io/contrib/samplers/jaegerremote v0.18.0 // @grafana/backend-platform golang.org/x/mod v0.14.0 // @grafana/backend-platform k8s.io/utils v0.0.0-20230726121419-3b25d923346b // @grafana/partner-datasources ) @@ -488,6 +488,13 @@ require ( github.com/xwb1989/sqlparser v0.0.0-20180606152119-120387863bf2 // @grafana/grafana-app-platform-squad ) +require ( + github.com/bahlo/generic-list-go v0.2.0 // indirect + github.com/buger/jsonparser v1.1.1 // indirect + github.com/invopop/jsonschema v0.12.0 // indirect + github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect +) + // Use fork of crewjam/saml with fixes for some issues until changes get merged into upstream replace github.com/crewjam/saml => github.com/grafana/saml v0.4.15-0.20231025143828-a6c0e9b86a4c diff --git a/go.sum b/go.sum index 14762a4030..8e617b8008 100644 --- a/go.sum +++ b/go.sum @@ -1448,6 +1448,7 @@ github.com/aws/smithy-go v1.11.2/go.mod h1:3xHYmszWVx2c0kIwQeEVf9uSm4fYZt67FBJnw github.com/axiomhq/hyperloglog v0.0.0-20191112132149-a4c4c47bc57f h1:y06x6vGnFYfXUoVMbrcP1Uzpj4JG01eB5vRps9G8agM= github.com/axiomhq/hyperloglog v0.0.0-20191112132149-a4c4c47bc57f/go.mod h1:2stgcRjl6QmW+gU2h5E7BQXg4HU0gzxKWDuT5HviN9s= github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= +github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A= github.com/beevik/etree v1.2.0 h1:l7WETslUG/T+xOPs47dtd6jov2Ii/8/OjCldk5fYfQw= github.com/beevik/etree v1.2.0/go.mod h1:aiPf89g/1k3AShMVAzriilpcE4R/Vuor90y83zVZWFc= @@ -1499,6 +1500,7 @@ github.com/bufbuild/connect-go v1.10.0 h1:QAJ3G9A1OYQW2Jbk3DeoJbkCxuKArrvZgDt47m github.com/bufbuild/connect-go v1.10.0/go.mod h1:CAIePUgkDR5pAFaylSMtNK45ANQjp9JvpluG20rhpV8= github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= +github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= github.com/buildkite/yaml v2.1.0+incompatible h1:xirI+ql5GzfikVNDmt+yeiXpf/v1Gt03qXTtT5WXdr8= github.com/buildkite/yaml v2.1.0+incompatible/go.mod h1:UoU8vbcwu1+vjZq01+KrpSeLBgQQIjL/H7Y6KwikUrI= github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= @@ -1780,8 +1782,8 @@ github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= -github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA= -github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= +github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k= +github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= @@ -2191,8 +2193,8 @@ github.com/grafana/grafana-google-sdk-go v0.1.0/go.mod h1:Vo2TKWfDVmNTELBUM+3lkr github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79 h1:r+mU5bGMzcXCRVAuOrTn54S80qbfVkvTdUJZfSfTNbs= github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79/go.mod h1:wc6Hbh3K2TgCUSfBC/BOzabItujtHMESZeFk5ZhdxhQ= github.com/grafana/grafana-plugin-sdk-go v0.114.0/go.mod h1:D7x3ah+1d4phNXpbnOaxa/osSaZlwh9/ZUnGGzegRbk= -github.com/grafana/grafana-plugin-sdk-go v0.213.0 h1:K2CHl+RkjAhOs9bhJBfJqJ0fF2lSdyLSxo4wfqee/C4= -github.com/grafana/grafana-plugin-sdk-go v0.213.0/go.mod h1:YYqzzfCnzMcKdQzWgFhw4ZJBDdAcEUuu83SDw1VByz4= +github.com/grafana/grafana-plugin-sdk-go v0.214.0 h1:09AoomxfsMdKmS4bc5tF81f7fvI9HjHckGFAmu/UQls= +github.com/grafana/grafana-plugin-sdk-go v0.214.0/go.mod h1:nBsh3jRItKQUXDF2BQkiQCPxqrsSQeb+7hiFyJTO1RE= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240226124929-648abdbd0ea4 h1:hpyusz8c3yRFoJPlA0o34rWnsLbaOOBZleqRhFBi5Lg= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240226124929-648abdbd0ea4/go.mod h1:vrRQJuNprTWqwm6JPxHf3BoTJhvO15QMEjQ7Q/YUOnI= github.com/grafana/grafana/pkg/apiserver v0.0.0-20240226124929-648abdbd0ea4 h1:tIbI5zgos92vwJ8lV3zwHwuxkV03GR3FGLkFW9V5LxY= @@ -2351,6 +2353,7 @@ github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod github.com/influxdata/influxdb1-client v0.0.0-20200827194710-b269163b24ab/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= github.com/influxdata/line-protocol v0.0.0-20210311194329-9aa0e372d097 h1:vilfsDSy7TDxedi9gyBkMvAirat/oRcL0lFdJBf6tdM= github.com/influxdata/line-protocol v0.0.0-20210311194329-9aa0e372d097/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo= +github.com/invopop/jsonschema v0.12.0 h1:6ovsNSuvn9wEQVOyc72aycBMVQFKz7cPdMJn10CvzRI= github.com/invopop/yaml v0.2.0 h1:7zky/qH+O0DwAyoobXUqvVBwgBFRxKoQ/3FjcVpjTMY= github.com/invopop/yaml v0.2.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q= github.com/ionos-cloud/sdk-go/v6 v6.1.10 h1:3815Q2Hw/wc4cJ8wD7bwfsmDsdfIEp80B7BQMj0YP2w= @@ -3085,6 +3088,7 @@ github.com/vultr/govultr/v2 v2.17.2 h1:gej/rwr91Puc/tgh+j33p/BLR16UrIPnSr+AIwYWZ github.com/vultr/govultr/v2 v2.17.2/go.mod h1:ZFOKGWmgjytfyjeyAdhQlSWwTjh2ig+X49cAp50dzXI= github.com/wk8/go-ordered-map v1.0.0 h1:BV7z+2PaK8LTSd/mWgY12HyMAo5CEgkHqbkVq2thqr8= github.com/wk8/go-ordered-map v1.0.0/go.mod h1:9ZIbRunKbuvfPKyBP1SIKLcXNlv74YCOZ3t3VTS6gRk= +github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= github.com/xanzy/go-gitlab v0.15.0/go.mod h1:8zdQa/ri1dfn8eS3Ir1SyfvOKlw7WBJ8DVThkpGiXrs= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs= @@ -3195,8 +3199,8 @@ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= go.opentelemetry.io/contrib/propagators/jaeger v1.22.0 h1:bAHX+zN/inu+Rbqk51REmC8oXLl+Dw6pp9ldQf/onaY= go.opentelemetry.io/contrib/propagators/jaeger v1.22.0/go.mod h1:bH9GkgkN21mscXcQP6lQJYI8XnEPDxlTN/ZOBuHDjqE= -go.opentelemetry.io/contrib/samplers/jaegerremote v0.16.0 h1:bBCrzJPJI3BsFjIYQEQ6J142Woqs/WHsImQfjV1XEnI= -go.opentelemetry.io/contrib/samplers/jaegerremote v0.16.0/go.mod h1:StxwPndBVNZD2sZez0RQ0SP/129XGCd4aEmVGaw1/QM= +go.opentelemetry.io/contrib/samplers/jaegerremote v0.18.0 h1:Q9PrD94WoMolBx44ef5UWWvufpVSME0MiSymXZfedso= +go.opentelemetry.io/contrib/samplers/jaegerremote v0.18.0/go.mod h1:tjp49JHNvreAAoWjdCHIVD7NXMjuJ3Dp/9iNOuPPlC8= go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= go.opentelemetry.io/otel/exporters/jaeger v1.10.0 h1:7W3aVVjEYayu/GOqOVF4mbTvnCuxF1wWu3eRxFGQXvw= @@ -3271,7 +3275,6 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= diff --git a/go.work.sum b/go.work.sum index b16390c78a..5c27afae06 100644 --- a/go.work.sum +++ b/go.work.sum @@ -4,8 +4,10 @@ buf.build/gen/go/grpc-ecosystem/grpc-gateway/protocolbuffers/go v1.28.1-20221127 cloud.google.com/go/accessapproval v1.7.4 h1:ZvLvJ952zK8pFHINjpMBY5k7LTAp/6pBf50RDMRgBUI= cloud.google.com/go/accesscontextmanager v1.8.4 h1:Yo4g2XrBETBCqyWIibN3NHNPQKUfQqti0lI+70rubeE= cloud.google.com/go/aiplatform v1.57.0 h1:WcZ6wDf/1qBWatmGM9Z+2BTiNjQQX54k2BekHUj93DQ= +cloud.google.com/go/aiplatform v1.58.0 h1:xyCAfpI4yUMOQ4VtHN/bdmxPQ8xoEkTwFM1nbVmuQhs= cloud.google.com/go/aiplatform v1.58.0/go.mod h1:pwZMGvqe0JRkI1GWSZCtnAfrR4K1bv65IHILGA//VEU= cloud.google.com/go/analytics v0.21.6 h1:fnV7B8lqyEYxCU0LKk+vUL7mTlqRAq4uFlIthIdr/iA= +cloud.google.com/go/analytics v0.22.0 h1:w8KIgW8NRUHFVKjpkwCpLaHsr685tJ+ckPStOaSCZz0= cloud.google.com/go/analytics v0.22.0/go.mod h1:eiROFQKosh4hMaNhF85Oc9WO97Cpa7RggD40e/RBy8w= cloud.google.com/go/apigateway v1.6.4 h1:VVIxCtVerchHienSlaGzV6XJGtEM9828Erzyr3miUGs= cloud.google.com/go/apigeeconnect v1.6.4 h1:jSoGITWKgAj/ssVogNE9SdsTqcXnryPzsulENSRlusI= @@ -15,6 +17,7 @@ cloud.google.com/go/appengine v1.8.4 h1:Qub3fqR7iA1daJWdzjp/Q0Jz0fUG0JbMc7Ui4E9I cloud.google.com/go/area120 v0.8.4 h1:YnSO8m02pOIo6AEOgiOoUDVbw4pf+bg2KLHi4rky320= cloud.google.com/go/artifactregistry v1.14.6 h1:/hQaadYytMdA5zBh+RciIrXZQBWK4vN7EUsrQHG+/t8= cloud.google.com/go/asset v1.15.3 h1:uI8Bdm81s0esVWbWrTHcjFDFKNOa9aB7rI1vud1hO84= +cloud.google.com/go/asset v1.17.0 h1:dLWfTnbwyrq/Kt8Tr2JiAbre1MEvS2Bl5cAMiYAy5Pg= cloud.google.com/go/asset v1.17.0/go.mod h1:yYLfUD4wL4X589A9tYrv4rFrba0QlDeag0CMcM5ggXU= cloud.google.com/go/assuredworkloads v1.11.4 h1:FsLSkmYYeNuzDm8L4YPfLWV+lQaUrJmH5OuD37t1k20= cloud.google.com/go/automl v1.13.4 h1:i9tOKXX+1gE7+rHpWKjiuPfGBVIYoWvLNIGpWgPtF58= @@ -23,6 +26,7 @@ cloud.google.com/go/batch v1.7.0 h1:AxuSPoL2fWn/rUyvWeNCNd0V2WCr+iHRCU9QO1PUmpY= cloud.google.com/go/batch v1.7.0/go.mod h1:J64gD4vsNSA2O5TtDB5AAux3nJ9iV8U3ilg3JDBYejU= cloud.google.com/go/beyondcorp v1.0.3 h1:VXf9SnrnSmj2BF2cHkoTHvOUp8gjsz1KJFOMW7czdsY= cloud.google.com/go/bigquery v1.57.1 h1:FiULdbbzUxWD0Y4ZGPSVCDLvqRSyCIO6zKV7E2nf5uA= +cloud.google.com/go/bigquery v1.58.0 h1:drSd9RcPVLJP2iFMimvOB9SCSIrcl+9HD4II03Oy7A0= cloud.google.com/go/bigquery v1.58.0/go.mod h1:0eh4mWNY0KrBTjUzLjoYImapGORq9gEPT7MWjCy9lik= cloud.google.com/go/billing v1.18.0 h1:GvKy4xLy1zF1XPbwP5NJb2HjRxhnhxjjXxvyZ1S/IAo= cloud.google.com/go/billing v1.18.0/go.mod h1:5DOYQStCxquGprqfuid/7haD7th74kyMBHkjO/OvDtk= @@ -30,6 +34,7 @@ cloud.google.com/go/binaryauthorization v1.8.0 h1:PHS89lcFayWIEe0/s2jTBiEOtqghCx cloud.google.com/go/binaryauthorization v1.8.0/go.mod h1:VQ/nUGRKhrStlGr+8GMS8f6/vznYLkdK5vaKfdCIpvU= cloud.google.com/go/certificatemanager v1.7.4 h1:5YMQ3Q+dqGpwUZ9X5sipsOQ1fLPsxod9HNq0+nrqc6I= cloud.google.com/go/channel v1.17.3 h1:Rd4+fBrjiN6tZ4TR8R/38elkyEkz6oogGDr7jDyjmMY= +cloud.google.com/go/channel v1.17.4 h1:yYHOORIM+wkBy3EdwArg/WL7Lg+SoGzlKH9o3Bw2/jE= cloud.google.com/go/channel v1.17.4/go.mod h1:QcEBuZLGGrUMm7kNj9IbU1ZfmJq2apotsV83hbxX7eE= cloud.google.com/go/cloudbuild v1.15.0 h1:9IHfEMWdCklJ1cwouoiQrnxmP0q3pH7JUt8Hqx4Qbck= cloud.google.com/go/clouddms v1.7.3 h1:xe/wJKz55VO1+L891a1EG9lVUgfHr9Ju/I3xh1nwF84= @@ -40,12 +45,14 @@ cloud.google.com/go/container v1.29.0 h1:jIltU529R2zBFvP8rhiG1mgeTcnT27KhU0H/1d6 cloud.google.com/go/container v1.29.0/go.mod h1:b1A1gJeTBXVLQ6GGw9/9M4FG94BEGsqJ5+t4d/3N7O4= cloud.google.com/go/containeranalysis v0.11.3 h1:5rhYLX+3a01drpREqBZVXR9YmWH45RnML++8NsCtuD8= cloud.google.com/go/datacatalog v1.19.0 h1:rbYNmHwvAOOwnW2FPXYkaK3Mf1MmGqRzK0mMiIEyLdo= +cloud.google.com/go/datacatalog v1.19.2 h1:BV5sB7fPc8ccv/obwtHwQtCdLMAgI4KyaQWfkh8/mWg= cloud.google.com/go/datacatalog v1.19.2/go.mod h1:2YbODwmhpLM4lOFe3PuEhHK9EyTzQJ5AXgIy7EDKTEE= cloud.google.com/go/dataflow v0.9.4 h1:7VmCNWcPJBS/srN2QnStTB6nu4Eb5TMcpkmtaPVhRt4= cloud.google.com/go/dataform v0.9.1 h1:jV+EsDamGX6cE127+QAcCR/lergVeeZdEQ6DdrxW3sQ= cloud.google.com/go/datafusion v1.7.4 h1:Q90alBEYlMi66zL5gMSGQHfbZLB55mOAg03DhwTTfsk= cloud.google.com/go/datalabeling v0.8.4 h1:zrq4uMmunf2KFDl/7dS6iCDBBAxBnKVDyw6+ajz3yu0= cloud.google.com/go/dataplex v1.13.0 h1:ACVOuxwe7gP0SqEso9SLyXbcZNk5l8hjcTX+XLntI5s= +cloud.google.com/go/dataplex v1.14.0 h1:/WhVTR4v/L6ACKjlz/9CqkxkrVh2z7C44CLMUf0f60A= cloud.google.com/go/dataplex v1.14.0/go.mod h1:mHJYQQ2VEJHsyoC0OdNyy988DvEbPhqFs5OOLffLX0c= cloud.google.com/go/dataproc v1.12.0 h1:W47qHL3W4BPkAIbk4SWmIERwsWBaNnWm0P2sdx3YgGU= cloud.google.com/go/dataproc/v2 v2.3.0 h1:tTVP9tTxmc8fixxOd/8s6Q6Pz/+yzn7r7XdZHretQH0= @@ -53,11 +60,14 @@ cloud.google.com/go/dataqna v0.8.4 h1:NJnu1kAPamZDs/if3bJ3+Wb6tjADHKL83NUWsaIp2z cloud.google.com/go/datastore v1.15.0 h1:0P9WcsQeTWjuD1H14JIY7XQscIPQ4Laje8ti96IC5vg= cloud.google.com/go/datastream v1.10.3 h1:Z2sKPIB7bT2kMW5Uhxy44ZgdJzxzE5uKjavoW+EuHEE= cloud.google.com/go/deploy v1.16.0 h1:5OVjzm8MPC5kP+Ywbs0mdE0O7AXvAUXksSyHAyMFyMg= +cloud.google.com/go/deploy v1.17.0 h1:P3SgJ+4rAktC2XqaI10G0ip/vzWluNBrC5VG0abMbLw= cloud.google.com/go/deploy v1.17.0/go.mod h1:XBr42U5jIr64t92gcpOXxNrqL2PStQCXHuKK5GRUuYo= cloud.google.com/go/dialogflow v1.47.0 h1:tLCWad8HZhlyUNfDzDP5m+oH6h/1Uvw/ei7B9AnsWMk= +cloud.google.com/go/dialogflow v1.48.1 h1:1Uq2jDJzjJ3M4xYB608FCCFHfW3JmrTmHIxRSd7JGmY= cloud.google.com/go/dialogflow v1.48.1/go.mod h1:C1sjs2/g9cEwjCltkKeYp3FFpz8BOzNondEaAlCpt+A= cloud.google.com/go/dlp v1.11.1 h1:OFlXedmPP/5//X1hBEeq3D9kUVm9fb6ywYANlpv/EsQ= cloud.google.com/go/documentai v1.23.6 h1:0/S3AhS23+0qaFe3tkgMmS3STxgDgmE1jg4TvaDOZ9g= +cloud.google.com/go/documentai v1.23.7 h1:hlYieOXUwiJ7HpBR/vEPfr8nfSxveLVzbqbUkSK0c/4= cloud.google.com/go/documentai v1.23.7/go.mod h1:ghzBsyVTiVdkfKaUCum/9bGBEyBjDO4GfooEcYKhN+g= cloud.google.com/go/domains v0.9.4 h1:ua4GvsDztZ5F3xqjeLKVRDeOvJshf5QFgWGg1CKti3A= cloud.google.com/go/edgecontainer v1.1.4 h1:Szy3Q/N6bqgQGyxqjI+6xJZbmvPvnFHp3UZr95DKcQ0= @@ -72,6 +82,7 @@ cloud.google.com/go/gkebackup v1.3.4 h1:KhnOrr9A1tXYIYeXKqCKbCI8TL2ZNGiD3dm+d7BD cloud.google.com/go/gkeconnect v0.8.4 h1:1JLpZl31YhQDQeJ98tK6QiwTpgHFYRJwpntggpQQWis= cloud.google.com/go/gkehub v0.14.4 h1:J5tYUtb3r0cl2mM7+YHvV32eL+uZQ7lONyUZnPikCEo= cloud.google.com/go/gkemulticloud v1.0.3 h1:NmJsNX9uQ2CT78957xnjXZb26TDIMvv+d5W2vVUt0Pg= +cloud.google.com/go/gkemulticloud v1.1.0 h1:C2Suwn3uPz+Yy0bxVjTlsMrUCaDovkgvfdyIa+EnUOU= cloud.google.com/go/gkemulticloud v1.1.0/go.mod h1:7NpJBN94U6DY1xHIbsDqB2+TFZUfjLUKLjUX8NGLor0= cloud.google.com/go/grafeas v0.3.0 h1:oyTL/KjiUeBs9eYLw/40cpSZglUC+0F7X4iu/8t7NWs= cloud.google.com/go/gsuiteaddons v1.6.4 h1:uuw2Xd37yHftViSI8J2hUcCS8S7SH3ZWH09sUDLW30Q= @@ -81,15 +92,18 @@ cloud.google.com/go/iot v1.7.4 h1:m1WljtkZnvLTIRYW1YTOv5A6H1yKgLHR6nU7O8yf27w= cloud.google.com/go/language v1.12.2 h1:zg9uq2yS9PGIOdc0Kz/l+zMtOlxKWonZjjo5w5YPG2A= cloud.google.com/go/lifesciences v0.9.4 h1:rZEI/UxcxVKEzyoRS/kdJ1VoolNItRWjNN0Uk9tfexg= cloud.google.com/go/logging v1.8.1 h1:26skQWPeYhvIasWKm48+Eq7oUqdcdbwsCVwz5Ys0FvU= +cloud.google.com/go/logging v1.9.0 h1:iEIOXFO9EmSiTjDmfpbRjOxECO7R8C7b8IXUGOj7xZw= cloud.google.com/go/logging v1.9.0/go.mod h1:1Io0vnZv4onoUnsVUQY3HZ3Igb1nBchky0A0y7BBBhE= cloud.google.com/go/longrunning v0.5.4 h1:w8xEcbZodnA2BbW6sVirkkoC+1gP8wS57EUUgGS0GVg= cloud.google.com/go/managedidentities v1.6.4 h1:SF/u1IJduMqQQdJA4MDyivlIQ4SrV5qAawkr/ZEREkY= cloud.google.com/go/maps v1.6.2 h1:WxxLo//b60nNFESefLgaBQevu8QGUmRV3+noOjCfIHs= +cloud.google.com/go/maps v1.6.3 h1:Qqs6Dza+PRp5CZO5AfgPnLwU1k3pp0IMFRDtLpT+aCA= cloud.google.com/go/maps v1.6.3/go.mod h1:VGAn809ADswi1ASofL5lveOHPnE6Rk/SFTTBx1yuOLw= cloud.google.com/go/mediatranslation v0.8.4 h1:VRCQfZB4s6jN0CSy7+cO3m4ewNwgVnaePanVCQh/9Z4= cloud.google.com/go/memcache v1.10.4 h1:cdex/ayDd294XBj2cGeMe6Y+H1JvhN8y78B9UW7pxuQ= cloud.google.com/go/metastore v1.13.3 h1:94l/Yxg9oBZjin2bzI79oK05feYefieDq0o5fjLSkC8= cloud.google.com/go/monitoring v1.16.3 h1:mf2SN9qSoBtIgiMA4R/y4VADPWZA7VCNJA079qLaZQ8= +cloud.google.com/go/monitoring v1.17.0 h1:blrdvF0MkPPivSO041ihul7rFMhXdVp8Uq7F59DKXTU= cloud.google.com/go/monitoring v1.17.0/go.mod h1:KwSsX5+8PnXv5NJnICZzW2R8pWTis8ypC4zmdRD63Tw= cloud.google.com/go/networkconnectivity v1.14.3 h1:e9lUkCe2BexsqsUc2bjV8+gFBpQa54J+/F3qKVtW+wA= cloud.google.com/go/networkmanagement v1.9.3 h1:HsQk4FNKJUX04k3OI6gUsoveiHMGvDRqlaFM2xGyvqU= @@ -98,14 +112,17 @@ cloud.google.com/go/notebooks v1.11.2 h1:eTOTfNL1yM6L/PCtquJwjWg7ZZGR0URFaFgbs8k cloud.google.com/go/optimization v1.6.2 h1:iFsoexcp13cGT3k/Hv8PA5aK+FP7FnbhwDO9llnruas= cloud.google.com/go/orchestration v1.8.4 h1:kgwZ2f6qMMYIVBtUGGoU8yjYWwMTHDanLwM/CQCFaoQ= cloud.google.com/go/orgpolicy v1.11.4 h1:RWuXQDr9GDYhjmrredQJC7aY7cbyqP9ZuLbq5GJGves= +cloud.google.com/go/orgpolicy v1.12.0 h1:sab7cDiyfdthpAL0JkSpyw1C3mNqkXToVOhalm79PJQ= cloud.google.com/go/orgpolicy v1.12.0/go.mod h1:0+aNV/nrfoTQ4Mytv+Aw+stBDBjNf4d8fYRA9herfJI= cloud.google.com/go/osconfig v1.12.4 h1:OrRCIYEAbrbXdhm13/JINn9pQchvTTIzgmOCA7uJw8I= cloud.google.com/go/oslogin v1.12.2 h1:NP/KgsD9+0r9hmHC5wKye0vJXVwdciv219DtYKYjgqE= +cloud.google.com/go/oslogin v1.13.0 h1:gbA/G4p+youIR4O/Rk6DU181QlBlpwPS16kvJwqEz8o= cloud.google.com/go/oslogin v1.13.0/go.mod h1:xPJqLwpTZ90LSE5IL1/svko+6c5avZLluiyylMb/sRA= cloud.google.com/go/phishingprotection v0.8.4 h1:sPLUQkHq6b4AL0czSJZ0jd6vL55GSTHz2B3Md+TCZI0= cloud.google.com/go/policytroubleshooter v1.10.2 h1:sq+ScLP83d7GJy9+wpwYJVnY+q6xNTXwOdRIuYjvHT4= cloud.google.com/go/privatecatalog v0.9.4 h1:Vo10IpWKbNvc/z/QZPVXgCiwfjpWoZ/wbgful4Uh/4E= cloud.google.com/go/pubsub v1.33.0 h1:6SPCPvWav64tj0sVX/+npCBKhUi/UjJehy9op/V3p2g= +cloud.google.com/go/pubsub v1.34.0 h1:ZtPbfwfi5rLaPeSvDC29fFoE20/tQvGrUS6kVJZJvkU= cloud.google.com/go/pubsub v1.34.0/go.mod h1:alj4l4rBg+N3YTFDDC+/YyFTs6JAjam2QfYsddcAW4c= cloud.google.com/go/pubsublite v1.8.1 h1:pX+idpWMIH30/K7c0epN6V703xpIcMXWRjKJsz0tYGY= cloud.google.com/go/recaptchaenterprise v1.3.1 h1:u6EznTGzIdsyOsvm+Xkw0aSuKFXQlyjGE9a4exk6iNQ= @@ -113,6 +130,7 @@ cloud.google.com/go/recaptchaenterprise/v2 v2.9.0 h1:Zrd4LvT9PaW91X/Z13H0i5RKEv9 cloud.google.com/go/recaptchaenterprise/v2 v2.9.0/go.mod h1:Dak54rw6lC2gBY8FBznpOCAR58wKf+R+ZSJRoeJok4w= cloud.google.com/go/recommendationengine v0.8.4 h1:JRiwe4hvu3auuh2hujiTc2qNgPPfVp+Q8KOpsXlEzKQ= cloud.google.com/go/recommender v1.11.3 h1:VndmgyS/J3+izR8V8BHa7HV/uun8//ivQ3k5eVKKyyM= +cloud.google.com/go/recommender v1.12.0 h1:tC+ljmCCbuZ/ybt43odTFlay91n/HLIhflvaOeb0Dh4= cloud.google.com/go/recommender v1.12.0/go.mod h1:+FJosKKJSId1MBFeJ/TTyoGQZiEelQQIZMKYYD8ruK4= cloud.google.com/go/redis v1.14.1 h1:J9cEHxG9YLmA9o4jTSvWt/RuVEn6MTrPlYSCRHujxDQ= cloud.google.com/go/resourcemanager v1.9.4 h1:JwZ7Ggle54XQ/FVYSBrMLOQIKoIT/uer8mmNvNLK51k= @@ -130,6 +148,7 @@ cloud.google.com/go/servicemanagement v1.8.0 h1:fopAQI/IAzlxnVeiKn/8WiV6zKndjFkv cloud.google.com/go/serviceusage v1.6.0 h1:rXyq+0+RSIm3HFypctp7WoXxIA563rn206CfMWdqXX4= cloud.google.com/go/shell v1.7.4 h1:nurhlJcSVFZneoRZgkBEHumTYf/kFJptCK2eBUq/88M= cloud.google.com/go/spanner v1.53.1 h1:xNmE0SXMSxNBuk7lRZ5G/S+A49X91zkSTt7Jn5Ptlvw= +cloud.google.com/go/spanner v1.55.0 h1:YF/A/k73EMYCjp8wcJTpkE+TcrWutHRlsCtlRSfWS64= cloud.google.com/go/spanner v1.55.0/go.mod h1:HXEznMUVhC+PC+HDyo9YFG2Ajj5BQDkcbqB9Z2Ffxi0= cloud.google.com/go/speech v1.21.0 h1:qkxNao58oF8ghAHE1Eghen7XepawYEN5zuZXYWaUTA4= cloud.google.com/go/storagetransfer v1.10.3 h1:YM1dnj5gLjfL6aDldO2s4GeU8JoAvH1xyIwXre63KmI= @@ -138,6 +157,7 @@ cloud.google.com/go/texttospeech v1.7.4 h1:ahrzTgr7uAbvebuhkBAAVU6kRwVD0HWsmDsvM cloud.google.com/go/tpu v1.6.4 h1:XIEH5c0WeYGaVy9H+UueiTaf3NI6XNdB4/v6TFQJxtE= cloud.google.com/go/trace v1.10.4 h1:2qOAuAzNezwW3QN+t41BtkDJOG42HywL73q8x/f6fnM= cloud.google.com/go/translate v1.9.3 h1:t5WXTqlrk8VVJu/i3WrYQACjzYJiff5szARHiyqqPzI= +cloud.google.com/go/translate v1.10.0 h1:tncNaKmlZnayMMRX/mMM2d5AJftecznnxVBD4w070NI= cloud.google.com/go/translate v1.10.0/go.mod h1:Kbq9RggWsbqZ9W5YpM94Q1Xv4dshw/gr/SHfsl5yCZ0= cloud.google.com/go/video v1.20.3 h1:Xrpbm2S9UFQ1pZEeJt9Vqm5t2T/z9y/M3rNXhFoo8Is= cloud.google.com/go/videointelligence v1.11.4 h1:YS4j7lY0zxYyneTFXjBJUj2r4CFe/UoIi/PJG0Zt/Rg= @@ -210,6 +230,7 @@ github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwc github.com/apache/arrow/go/v10 v10.0.1 h1:n9dERvixoC/1JjDmBcs9FPaEryoANa2sCgVFo6ez9cI= github.com/apache/arrow/go/v11 v11.0.0 h1:hqauxvFQxww+0mEU/2XHG6LT7eZternCZq+A5Yly2uM= github.com/apache/arrow/go/v12 v12.0.0 h1:xtZE63VWl7qLdB0JObIXvvhGjoVNrQ9ciIHG2OK5cmc= +github.com/apache/arrow/go/v12 v12.0.1 h1:JsR2+hzYYjgSUkBSaahpqCetqZMr76djX80fF/DiJbg= github.com/apache/arrow/go/v12 v12.0.1/go.mod h1:weuTY7JvTG/HDPtMQxEUp7pU73vkLWMLpY67QwZ/WWw= github.com/apache/arrow/go/v13 v13.0.0 h1:kELrvDQuKZo8csdWYqBQfyi431x6Zs/YJTEgUuSVcWk= github.com/apache/arrow/go/v13 v13.0.0/go.mod h1:W69eByFNO0ZR30q1/7Sr9d83zcVZmF2MiP3fFYAWJOc= @@ -329,6 +350,7 @@ github.com/go-fonts/stix v0.1.0 h1:UlZlgrvvmT/58o573ot7NFw0vZasZ5I6bcIft/oMdgg= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1 h1:QbL/5oDUmRBzO9/Z7Seo6zf912W/a6Sr4Eu0G/3Jho0= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4 h1:WtGNWLvXpe6ZudgnXrq0barxBImvnnJoMEhXAzcbM0I= github.com/go-ini/ini v1.25.4 h1:Mujh4R/dH6YL8bxuISne3xX2+qcQ9p0IxKAP6ExWoUo= +github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k= github.com/go-kit/kit v0.12.0 h1:e4o3o3IsBfAKQh5Qbbiqyfu97Ku7jrO/JbohvztANh4= github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81 h1:6zl3BbBhdnMkpSj2YY30qV3gDcVBGtFgVsV3+/i+mKQ= github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab h1:xveKWz2iaueeTaUgdetzel+U7exyigDYBryyVfV/rZk= @@ -402,6 +424,7 @@ github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hudl/fargo v1.4.0 h1:ZDDILMbB37UlAVLlWcJ2Iz1XuahZZTDZfdCKeclfq2s= github.com/hydrogen18/memlistener v0.0.0-20200120041712-dcc25e7acd91 h1:KyZDvZ/GGn+r+Y3DKZ7UOQ/TP4xV6HNkrwiVMB1GnNY= github.com/iancoleman/strcase v0.2.0 h1:05I4QRnGpI0m37iZQRuskXh+w77mr6Z41lwQzuHLwW0= +github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab h1:BA4a7pe6ZTd9F8kXETBoijjFJ/ntaa//1wiH9BZu4zU= github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk= @@ -625,6 +648,7 @@ github.com/sony/gobreaker v0.4.1 h1:oMnRNZXX5j85zso6xCPRNPtmAycat+WcoKbklScLDgQ= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.9.2 h1:j49Hj62F0n+DaZ1dDCvhABaPNSGNkt32oRFxI33IEMw= +github.com/spf13/afero v1.10.0 h1:EaGW2JJh15aKOejeuJ+wpFSHnbd7GE6Wvp3TsNhb6LY= github.com/spf13/afero v1.10.0/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= @@ -717,6 +741,7 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.4 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.47.0/go.mod h1:r9vWsPS/3AQItv3OSlEJ/E4mbrhUbbw18meOjArPtKQ= go.opentelemetry.io/contrib/propagators/b3 v1.15.0 h1:bMaonPyFcAvZ4EVzkUNkfnUHP5Zi63CIDlA3dRsEg8Q= go.opentelemetry.io/contrib/propagators/b3 v1.15.0/go.mod h1:VjU0g2v6HSQ+NwfifambSLAeBgevjIcqmceaKWEzl0c= +go.opentelemetry.io/contrib/samplers/jaegerremote v0.18.0 h1:Q9PrD94WoMolBx44ef5UWWvufpVSME0MiSymXZfedso= go.opentelemetry.io/otel/bridge/opencensus v0.37.0 h1:ieH3gw7b1eg90ARsFAlAsX5LKVZgnCYfaDwRrK6xLHU= go.opentelemetry.io/otel/bridge/opencensus v0.37.0/go.mod h1:ddiK+1PE68l/Xk04BGTh9Y6WIcxcLrmcVxVlS0w5WZ0= go.opentelemetry.io/otel/bridge/opentracing v1.10.0 h1:WzAVGovpC1s7KD5g4taU6BWYZP3QGSDVTlbRu9fIHw8= @@ -756,6 +781,7 @@ google.golang.org/genproto/googleapis/api v0.0.0-20231212172506-995d672761c0/go. google.golang.org/genproto/googleapis/api v0.0.0-20240102182953-50ed04b92917/go.mod h1:CmlNWB9lSezaYELKS5Ym1r44VrrbPUa7JTvw+6MbpJ0= google.golang.org/genproto/googleapis/api v0.0.0-20240116215550-a9fa1716bcac/go.mod h1:B5xPO//w8qmBDjGReYLpR6UJPnkldGkCSMoH/2vxJeg= google.golang.org/genproto/googleapis/bytestream v0.0.0-20231120223509-83a465c0220f h1:hL+1ptbhFoeL1HcROQ8OGXaqH0jYRRibgWQWco0/Ugc= +google.golang.org/genproto/googleapis/bytestream v0.0.0-20231212172506-995d672761c0 h1:Y6QQt9D/syZt/Qgnz5a1y2O3WunQeeVDfS9+Xr82iFA= google.golang.org/genproto/googleapis/bytestream v0.0.0-20231212172506-995d672761c0/go.mod h1:guYXGPwC6jwxgWKW5Y405fKWOFNwlvUlUnzyp9i0uqo= google.golang.org/genproto/googleapis/rpc v0.0.0-20231212172506-995d672761c0/go.mod h1:FUoWkonphQm3RhTS+kOEhF8h0iDpm4tdXolVCeZ9KKA= google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917/go.mod h1:xtjpI3tXFPP051KaWnhvxkiubL/6dJ18vLVf7q2pTOU= diff --git a/pkg/apimachinery/go.mod b/pkg/apimachinery/go.mod index 8baa078c4b..e7d9e40aff 100644 --- a/pkg/apimachinery/go.mod +++ b/pkg/apimachinery/go.mod @@ -24,7 +24,7 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - golang.org/x/net v0.20.0 // indirect + golang.org/x/net v0.21.0 // indirect golang.org/x/text v0.14.0 // indirect google.golang.org/protobuf v1.32.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/pkg/apimachinery/go.sum b/pkg/apimachinery/go.sum index 8b6fe021c1..b678ff22e8 100644 --- a/pkg/apimachinery/go.sum +++ b/pkg/apimachinery/go.sum @@ -57,7 +57,7 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= diff --git a/pkg/apiserver/go.mod b/pkg/apiserver/go.mod index a977562cac..a806fd79c9 100644 --- a/pkg/apiserver/go.mod +++ b/pkg/apiserver/go.mod @@ -5,6 +5,7 @@ go 1.21.0 require ( github.com/bwmarrin/snowflake v0.3.0 github.com/gorilla/mux v1.8.0 + github.com/grafana/grafana-plugin-sdk-go v0.214.0 github.com/stretchr/testify v1.8.4 golang.org/x/mod v0.14.0 k8s.io/apimachinery v0.29.2 @@ -15,30 +16,40 @@ require ( ) require ( + github.com/BurntSushi/toml v1.3.2 // indirect github.com/NYTimes/gziphandler v1.1.1 // indirect github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df // indirect + github.com/apache/arrow/go/v15 v15.0.0 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect github.com/cenkalti/backoff/v4 v4.2.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cheekybits/genny v1.0.0 // indirect + github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89 // indirect github.com/coreos/go-semver v0.3.1 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/elazarl/goproxy v0.0.0-20230731152917-f99041a5c027 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/evanphx/json-patch v5.6.0+incompatible // indirect + github.com/fatih/color v1.15.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/getkin/kin-openapi v0.120.0 // indirect github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.20.2 // indirect github.com/go-openapi/jsonreference v0.20.4 // indirect github.com/go-openapi/swag v0.22.9 // indirect + github.com/goccy/go-json v0.10.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/btree v1.1.2 // indirect github.com/google/cel-go v0.17.7 // indirect + github.com/google/flatbuffers v23.5.26+incompatible // indirect github.com/google/gnostic-models v0.6.8 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/gofuzz v1.2.0 // indirect @@ -46,56 +57,86 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.1 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect + github.com/hashicorp/go-hclog v1.6.2 // indirect + github.com/hashicorp/go-plugin v1.6.0 // indirect + github.com/hashicorp/yamux v0.1.1 // indirect github.com/imdario/mergo v0.3.16 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/invopop/yaml v0.2.0 // indirect github.com/jonboulle/clockwork v0.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.17.4 // indirect + github.com/klauspost/cpuid/v2 v2.2.5 // indirect + github.com/magefile/mage v1.15.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattetti/filebuffer v1.0.1 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/mattn/go-runewidth v0.0.13 // indirect + github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/oklog/run v1.1.0 // indirect + github.com/olekukonko/tablewriter v0.0.5 // indirect + github.com/perimeterx/marshmallow v1.1.5 // indirect + github.com/pierrec/lz4/v4 v4.1.18 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.18.0 // indirect github.com/prometheus/client_model v0.5.0 // indirect github.com/prometheus/common v0.46.0 // indirect github.com/prometheus/procfs v0.12.0 // indirect + github.com/rivo/uniseg v0.3.4 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect + github.com/smartystreets/goconvey v1.6.4 // indirect github.com/spf13/cobra v1.8.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/stoewer/go-strcase v1.3.0 // indirect + github.com/unknwon/bra v0.0.0-20200517080246-1e3013ecaff8 // indirect + github.com/unknwon/com v1.0.1 // indirect + github.com/unknwon/log v0.0.0-20150304194804-e617c87089d3 // indirect + github.com/urfave/cli v1.22.14 // indirect + github.com/zeebo/xxh3 v1.0.2 // indirect go.etcd.io/etcd/api/v3 v3.5.10 // indirect go.etcd.io/etcd/client/pkg/v3 v3.5.10 // indirect go.etcd.io/etcd/client/v3 v3.5.10 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.47.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // indirect - go.opentelemetry.io/otel v1.22.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 // indirect - go.opentelemetry.io/otel/metric v1.22.0 // indirect - go.opentelemetry.io/otel/sdk v1.22.0 // indirect - go.opentelemetry.io/otel/trace v1.22.0 // indirect - go.opentelemetry.io/proto/otlp v1.0.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.49.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect + go.opentelemetry.io/contrib/propagators/jaeger v1.22.0 // indirect + go.opentelemetry.io/contrib/samplers/jaegerremote v0.18.0 // indirect + go.opentelemetry.io/otel v1.24.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0 // indirect + go.opentelemetry.io/otel/metric v1.24.0 // indirect + go.opentelemetry.io/otel/sdk v1.24.0 // indirect + go.opentelemetry.io/otel/trace v1.24.0 // indirect + go.opentelemetry.io/proto/otlp v1.1.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.26.0 // indirect - golang.org/x/crypto v0.18.0 // indirect + golang.org/x/crypto v0.19.0 // indirect golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb // indirect - golang.org/x/net v0.20.0 // indirect + golang.org/x/net v0.21.0 // indirect golang.org/x/oauth2 v0.16.0 // indirect golang.org/x/sync v0.6.0 // indirect - golang.org/x/sys v0.16.0 // indirect - golang.org/x/term v0.16.0 // indirect + golang.org/x/sys v0.17.0 // indirect + golang.org/x/term v0.17.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/time v0.5.0 // indirect golang.org/x/tools v0.17.0 // indirect + golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect google.golang.org/appengine v1.6.8 // indirect - google.golang.org/genproto v0.0.0-20231212172506-995d672761c0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20231212172506-995d672761c0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20231212172506-995d672761c0 // indirect - google.golang.org/grpc v1.60.1 // indirect + google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240123012728-ef4313101c80 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 // indirect + google.golang.org/grpc v1.62.1 // indirect google.golang.org/protobuf v1.32.0 // indirect + gopkg.in/fsnotify/fsnotify.v1 v1.4.7 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/pkg/apiserver/go.sum b/pkg/apiserver/go.sum index d9ce32d70d..d25e7989bf 100644 --- a/pkg/apiserver/go.sum +++ b/pkg/apiserver/go.sum @@ -1,39 +1,41 @@ -cloud.google.com/go v0.111.0 h1:YHLKNupSD1KqjDbQ3+LVdQ81h/UJbJyZG203cEfnQgM= -cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk= -cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= -cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= +github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I= github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df h1:7RFfzj4SSt6nnvCPbCqijJi1nWCd+TqAT3bYCStRC18= +github.com/apache/arrow/go/v15 v15.0.0 h1:1zZACWf85oEZY5/kd9dsQS7i+2G5zVQcbKTHgslqHNA= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= github.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0= github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE= github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4 h1:/inchEIKaYC1Akx+H+gqO04wryn5h75LSazbRlnya1k= -github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE= +github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89 h1:aPflPkRFkVwbW6dmcVqfgwp1i+UWGFH6VgR1Jim5Ygc= github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/elazarl/goproxy v0.0.0-20230731152917-f99041a5c027 h1:1L0aalTpPz7YlMxETKpmQoWMBkeiuorElZIXoNmgiPE= +github.com/elazarl/goproxy/ext v0.0.0-20220115173737-adb46da277ac h1:9yrT5tmn9Zc0ytWPASlaPwQfQMQYnRf0RSDe1XvHw0Q= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= -github.com/envoyproxy/protoc-gen-validate v1.0.2 h1:QkIBuU5k+x7/QXPvPPnWXWlCdaBFApVqftFV6k087DA= -github.com/envoyproxy/protoc-gen-validate v1.0.2/go.mod h1:GpiZQP3dDbg4JouG/NNS7QWXpgx6x8QiMKdmN72jogE= github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= +github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/getkin/kin-openapi v0.120.0 h1:MqJcNJFrMDFNc07iwE8iFC5eT2k/NPUFDIpNeiZv8Jg= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -45,6 +47,8 @@ github.com/go-openapi/jsonreference v0.20.4 h1:bKlDxQxQJgwpUSgOENiMPzCTBVuc7vTdX github.com/go-openapi/swag v0.22.9 h1:XX2DssF+mQKM2DHsbgZK74y/zj4mo9I99+89xUmuZCE= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= @@ -57,6 +61,7 @@ github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= github.com/google/cel-go v0.17.7 h1:6ebJFzu1xO2n7TLtN+UBqShGBhlD85bhvglh5DpcfqQ= +github.com/google/flatbuffers v23.5.26+incompatible h1:M9dgRyhJemaM4Sw8+66GHBu8ioaQmyPLg1b8VwK5WJg= github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= @@ -68,23 +73,34 @@ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20231205033806-a5a03c77bf08 h1:PxlBVtIFHR/mtWk2i0gTEdCz+jBnqiuHNSki0epDbVs= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grafana/grafana-plugin-sdk-go v0.214.0 h1:09AoomxfsMdKmS4bc5tF81f7fvI9HjHckGFAmu/UQls= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340 h1:uGoIog/wiQHI9GAxXO5TJbT0wWKH3O9HhOJW1F9c3fY= github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.1 h1:6UKoz5ujsI55KNpsJH3UwCq3T8kKbZwNZBNPuTTje8U= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 h1:Wqo399gCIufwto+VfwCSvsnfGpF/w5E9CNxSwbpD6No= +github.com/hashicorp/go-hclog v1.6.2 h1:NOtoftovWkDheyUM/8JW3QMiXyxJK3uHRK7wV04nD2I= +github.com/hashicorp/go-hclog v1.6.2/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-plugin v1.6.0 h1:wgd4KxHJTVGGqWBq4QPB1i5BZNEx9BR8+OFmHDmTk8A= +github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/invopop/yaml v0.2.0 h1:7zky/qH+O0DwAyoobXUqvVBwgBFRxKoQ/3FjcVpjTMY= +github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= +github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -92,19 +108,31 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattetti/filebuffer v1.0.1 h1:gG7pyfnSIZCxdoKq+cPa8T0hhYtD9NxCdI4D7PTjRLM= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= +github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4= github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o= github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg= github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= +github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= +github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -114,8 +142,12 @@ github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+ github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= github.com/prometheus/common v0.46.0 h1:doXzt5ybi1HBKpsZOL0sSkaNHJJqkyfEWZGGqqScV0Y= github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= +github.com/rivo/uniseg v0.3.4 h1:3Z3Eu6FGHZWSfNKJTOUiPatWwfc7DzJRU04jFUqJODw= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 h1:Jpy1PXuP99tXNrhbq2BaPz9B+jNAvH1JPQQpG/9GCXY= +github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js= github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= @@ -135,10 +167,17 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 h1:6fotK7otjonDflCTK0BCfls4SPy3NcCVb5dqqmbRknE= github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75/go.mod h1:KO6IkyS8Y3j8OdNO85qEYBsRPuteD+YciPomcXdrMnk= +github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= +github.com/unknwon/bra v0.0.0-20200517080246-1e3013ecaff8 h1:aVGB3YnaS/JNfOW3tiHIlmNmTDg618va+eT0mVomgyI= +github.com/unknwon/com v1.0.1 h1:3d1LTxD+Lnf3soQiD4Cp/0BRB+Rsa/+RTvz8GMMzIXs= +github.com/unknwon/log v0.0.0-20150304194804-e617c87089d3 h1:4EYQaWAatQokdji3zqZloVIW/Ke1RQjYw2zHULyrHJg= +github.com/urfave/cli v1.22.14 h1:ebbhrRiGK2i4naQJr+1Xj92HXZCrK7MsyTS/ob3HnAk= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= +github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= go.etcd.io/bbolt v1.3.8 h1:xs88BrvEv273UsB79e0hcVrlUWmS0a8upikMFhSyAtA= go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= go.etcd.io/etcd/api/v3 v3.5.10 h1:szRajuUUbLyppkhs9K6BRtjY37l66XQQmw7oZRANE4k= @@ -155,16 +194,18 @@ go.etcd.io/etcd/raft/v3 v3.5.10 h1:cgNAYe7xrsrn/5kXMSaH8kM/Ky8mAdMqGOxyYwpP0LA= go.etcd.io/etcd/raft/v3 v3.5.10/go.mod h1:odD6kr8XQXTy9oQnyMPBOr0TVe+gT0neQhElQ6jbGRc= go.etcd.io/etcd/server/v3 v3.5.10 h1:4NOGyOwD5sUZ22PiWYKmfxqoeh72z6EhYjNosKGLmZg= go.etcd.io/etcd/server/v3 v3.5.10/go.mod h1:gBplPHfs6YI0L+RpGkTQO7buDbHv5HJGG/Bst0/zIPo= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.47.0 h1:UNQQKPfTDe1J81ViolILjTKPr9WetKW6uei2hFgJmFs= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24= -go.opentelemetry.io/otel v1.22.0 h1:xS7Ku+7yTFvDfDraDIJVpw7XPyuHlB9MCiqqX5mcJ6Y= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 h1:cl5P5/GIfFh4t6xyruOgJP5QiA1pw4fYYdv6nc6CBWw= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 h1:tIqheXEFWAZ7O8A7m+J0aPTmpJN3YQ7qetUAdkkkKpk= -go.opentelemetry.io/otel/metric v1.22.0 h1:lypMQnGyJYeuYPhOM/bgjbFM6WE44W1/T45er4d8Hhg= -go.opentelemetry.io/otel/sdk v1.22.0 h1:6coWHw9xw7EfClIC/+O31R8IY3/+EiRFHevmHafB2Gw= -go.opentelemetry.io/otel/trace v1.22.0 h1:Hg6pPujv0XG9QaVbGOBVHunyuLcCC3jN7WEhPx83XD0= -go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= -go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.49.0 h1:RtcvQ4iw3w9NBB5yRwgA4sSa82rfId7n4atVpvKx3bY= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= +go.opentelemetry.io/contrib/propagators/jaeger v1.22.0 h1:bAHX+zN/inu+Rbqk51REmC8oXLl+Dw6pp9ldQf/onaY= +go.opentelemetry.io/contrib/samplers/jaegerremote v0.18.0 h1:Q9PrD94WoMolBx44ef5UWWvufpVSME0MiSymXZfedso= +go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 h1:t6wl9SPayj+c7lEIFgm4ooDBZVb01IhLB4InpomhRw8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0 h1:Mw5xcxMwlqoJd97vwPxA8isEaIoxsta9/Q51+TTJLGE= +go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= +go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw= +go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= +go.opentelemetry.io/proto/otlp v1.1.0 h1:2Di21piLrCqJ3U3eXGCTPHE9R8Nh+0uglSnOyxikMeI= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -175,7 +216,7 @@ go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= +golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb h1:c0vyKkb6yr3KR7jEfJaOSv4lG7xPkbN6r52aJz1d8a8= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -186,7 +227,7 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -195,8 +236,8 @@ golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= -golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= @@ -213,11 +254,13 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= +gonum.org/v1/gonum v0.12.0 h1:xKuo6hzt+gMav00meVPUlXwSdoEJP46BR+wdxQEFK2o= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= -google.golang.org/genproto v0.0.0-20231212172506-995d672761c0 h1:YJ5pD9rF8o9Qtta0Cmy9rdBwkSjrTCT6XTiUQVOtIos= -google.golang.org/genproto/googleapis/api v0.0.0-20231212172506-995d672761c0 h1:s1w3X6gQxwrLEpxnLd/qXTVLgQE2yXwaOaoa6IlY/+o= -google.golang.org/genproto/googleapis/rpc v0.0.0-20231212172506-995d672761c0 h1:/jFB8jK5R3Sq3i/lmeZO0cATSzFfZaJq1J2Euan3XKU= -google.golang.org/grpc v1.60.1 h1:26+wFr+cNqSGFcOXcabYC0lUVJVRa2Sb2ortSK7VrEU= +google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80 h1:KAeGQVN3M9nD0/bQXnr/ClcEMJ968gUXJQ9pwfSynuQ= +google.golang.org/genproto/googleapis/api v0.0.0-20240123012728-ef4313101c80 h1:Lj5rbfG876hIAYFjqiJnPHfhXbv+nzTWfm04Fg/XSVU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 h1:AjyfHzEPEFp/NpvfN5g+KDla3EMojjhRVZc1i7cj+oM= +google.golang.org/grpc v1.62.1 h1:B4n+nfKzOICUXMgyrNd19h/I9oH0L1pizfk1d4zSgTk= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= @@ -225,6 +268,7 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/fsnotify/fsnotify.v1 v1.4.7 h1:XNNYLJHt73EyYiCZi6+xjupS9CpvmiDgjPTAjrBlQbo= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= diff --git a/pkg/util/maputil/maputil.go b/pkg/util/maputil/maputil.go deleted file mode 100644 index f3ff61baec..0000000000 --- a/pkg/util/maputil/maputil.go +++ /dev/null @@ -1,73 +0,0 @@ -package maputil - -import "fmt" - -func GetMap(obj map[string]any, key string) (map[string]any, error) { - if untypedValue, ok := obj[key]; ok { - if value, ok := untypedValue.(map[string]any); ok { - return value, nil - } else { - err := fmt.Errorf("the field '%s' should be an object", key) - return nil, err - } - } else { - err := fmt.Errorf("the field '%s' should be set", key) - return nil, err - } -} - -func GetBool(obj map[string]any, key string) (bool, error) { - if untypedValue, ok := obj[key]; ok { - if value, ok := untypedValue.(bool); ok { - return value, nil - } else { - err := fmt.Errorf("the field '%s' should be a bool", key) - return false, err - } - } else { - err := fmt.Errorf("the field '%s' should be set", key) - return false, err - } -} - -func GetBoolOptional(obj map[string]any, key string) (bool, error) { - if untypedValue, ok := obj[key]; ok { - if value, ok := untypedValue.(bool); ok { - return value, nil - } else { - err := fmt.Errorf("the field '%s' should be a bool", key) - return false, err - } - } else { - // Value optional, not error - return false, nil - } -} - -func GetString(obj map[string]any, key string) (string, error) { - if untypedValue, ok := obj[key]; ok { - if value, ok := untypedValue.(string); ok { - return value, nil - } else { - err := fmt.Errorf("the field '%s' should be a string", key) - return "", err - } - } else { - err := fmt.Errorf("the field '%s' should be set", key) - return "", err - } -} - -func GetStringOptional(obj map[string]any, key string) (string, error) { - if untypedValue, ok := obj[key]; ok { - if value, ok := untypedValue.(string); ok { - return value, nil - } else { - err := fmt.Errorf("the field '%s' should be a string", key) - return "", err - } - } else { - // Value optional, not error - return "", nil - } -} diff --git a/public/api-merged.json b/public/api-merged.json index 8a7250ee21..0454aba938 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -16180,8 +16180,8 @@ } }, "JSONWebKey": { + "description": "JSONWebKey represents a public or private key in JWK format. It can be\nmarshaled into JSON and unmarshaled from JSON.", "type": "object", - "title": "JSONWebKey represents a public or private key in JWK format.", "properties": { "Algorithm": { "description": "Key algorithm, parsed from `alg` header.", @@ -16214,7 +16214,7 @@ "$ref": "#/definitions/URL" }, "Key": { - "description": "Cryptographic key, can be a symmetric or asymmetric key." + "description": "Key is the Go in-memory representation of this key. It must have one\nof these types:\ned25519.PublicKey\ned25519.PrivateKey\necdsa.PublicKey\necdsa.PrivateKey\nrsa.PublicKey\nrsa.PrivateKey\n[]byte (a symmetric key)\n\nWhen marshaling this JSONWebKey into JSON, the \"kty\" header parameter\nwill be automatically set based on the type of this field." }, "KeyID": { "description": "Key identifier, parsed from `kid` header.", diff --git a/public/openapi3.json b/public/openapi3.json index 6337fe7900..074acaff78 100644 --- a/public/openapi3.json +++ b/public/openapi3.json @@ -6690,6 +6690,7 @@ "type": "object" }, "JSONWebKey": { + "description": "JSONWebKey represents a public or private key in JWK format. It can be\nmarshaled into JSON and unmarshaled from JSON.", "properties": { "Algorithm": { "description": "Key algorithm, parsed from `alg` header.", @@ -6722,7 +6723,7 @@ "$ref": "#/components/schemas/URL" }, "Key": { - "description": "Cryptographic key, can be a symmetric or asymmetric key." + "description": "Key is the Go in-memory representation of this key. It must have one\nof these types:\ned25519.PublicKey\ned25519.PrivateKey\necdsa.PublicKey\necdsa.PrivateKey\nrsa.PublicKey\nrsa.PrivateKey\n[]byte (a symmetric key)\n\nWhen marshaling this JSONWebKey into JSON, the \"kty\" header parameter\nwill be automatically set based on the type of this field." }, "KeyID": { "description": "Key identifier, parsed from `kid` header.", @@ -6733,7 +6734,6 @@ "type": "string" } }, - "title": "JSONWebKey represents a public or private key in JWK format.", "type": "object" }, "Json": { From b6c550c52427cfa43e707af93026f8cf4d4db0c8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 15:32:32 +0000 Subject: [PATCH 0524/1406] Update dependency xss to v1.0.15 --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 430f28f8e5..5f8aefa1c5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -31684,14 +31684,14 @@ __metadata: linkType: hard "xss@npm:^1.0.14": - version: 1.0.14 - resolution: "xss@npm:1.0.14" + version: 1.0.15 + resolution: "xss@npm:1.0.15" dependencies: commander: "npm:^2.20.3" cssfilter: "npm:0.0.10" bin: xss: bin/xss - checksum: 10/dc97acaee35e5ed453fe5628841daf7b4aba5ed26b31ff4eadf831f42cded1ddebc218ff0db1d6a73e301bfada8a5236fec0c234233d66a20ecc319da542b357 + checksum: 10/074ad54babac9dd5107466dbf30d3b871dbedae1f8e7b8f4e3b76d60da8b92bd0f66f18ccd26b8524545444ef784b78c526cee089a907aa904f83c8b8d7958f6 languageName: node linkType: hard From cfc7ea92daf4b05c2ba84c5a81fb256938271b08 Mon Sep 17 00:00:00 2001 From: Usman Ahmad Date: Mon, 11 Mar 2024 17:21:37 +0100 Subject: [PATCH 0525/1406] corrected the minor details (#84046) * corrected the minor details Making minor changes after the PR merged on Data sources and Data source administration. https://github.com/grafana/grafana/pull/83712 * Apply suggestions from code review Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> * Update docs/sources/panels-visualizations/_index.md Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> * Ran prettier --------- Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> Co-authored-by: Isabel Matwawana --- docs/sources/panels-visualizations/_index.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/sources/panels-visualizations/_index.md b/docs/sources/panels-visualizations/_index.md index af1f784332..dbb1f5d71c 100644 --- a/docs/sources/panels-visualizations/_index.md +++ b/docs/sources/panels-visualizations/_index.md @@ -30,8 +30,13 @@ Panels can be dragged, dropped, and resized to rearrange them on the dashboard. Before you add a panel, ensure that you have configured a data source. -- For more information about adding and managing data sources as an administrator, refer to [Data source management][]. -- For details about using specific data sources, refer to [Data sources][]. +- For details about using data sources, refer to [Data sources][]. + +- For more information about managing data sources as an administrator, refer to [Data source management][]. + + {{% admonition type="note" %}} + [Data source management](https://grafana.com/docs/grafana//administration/data-source-management/) is only available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise/) and [Grafana Cloud](https://grafana.com/docs/grafana-cloud/). + {{% /admonition %}} This section includes the following sub topics: From 3fb6319d1b8cae1d7e359572673117cb8268df63 Mon Sep 17 00:00:00 2001 From: ismail simsek Date: Mon, 11 Mar 2024 17:22:33 +0100 Subject: [PATCH 0526/1406] Prometheus: Introduce prometheus backend library (#83952) * Move files to prometheus-library * refactor core prometheus to use prometheus-library * modify client transport options * mock * have a type * import aliases * rename * call the right method * remove unrelated test from the library * update codeowners * go work sync * update go.work.sum * make swagger-clean && make openapi3-gen * add promlib to makefile * remove clilogger * Export the function * update unit test * add prometheus_test.go * fix mock type * use mapUtil from grafana-plugin-sdk-go --- .github/CODEOWNERS | 1 + Makefile | 2 +- go.mod | 6 +- go.work | 1 + go.work.sum | 14 +- .../prometheus => promlib}/client/client.go | 2 +- .../client/client_test.go | 2 +- .../client/transport.go | 30 +--- .../client/transport_test.go | 22 --- .../prometheus => promlib}/converter/prom.go | 0 .../converter/prom_test.go | 0 .../testdata/loki-streams-a-frame.jsonc | 0 .../converter/testdata/loki-streams-a.json | 0 .../testdata/loki-streams-b-frame.jsonc | 0 .../converter/testdata/loki-streams-b.json | 0 .../testdata/loki-streams-c-frame.jsonc | 0 .../converter/testdata/loki-streams-c.json | 0 ...ki-streams-structured-metadata-frame.jsonc | 0 .../loki-streams-structured-metadata.json | 0 .../converter/testdata/prom-error-frame.jsonc | 0 .../converter/testdata/prom-error.json | 0 .../testdata/prom-exemplars-a-frame.json | 0 .../testdata/prom-exemplars-a-frame.jsonc | 0 .../testdata/prom-exemplars-a-golden.txt | 0 .../converter/testdata/prom-exemplars-a.json | 0 .../testdata/prom-exemplars-b-frame.json | 0 .../testdata/prom-exemplars-b-frame.jsonc | 0 .../testdata/prom-exemplars-b-golden.txt | 0 .../converter/testdata/prom-exemplars-b.json | 0 .../prom-exemplars-diff-labels-frame.jsonc | 0 .../testdata/prom-exemplars-diff-labels.json | 0 .../testdata/prom-exemplars-frame.jsonc | 0 .../converter/testdata/prom-exemplars.json | 0 .../testdata/prom-labels-frame.jsonc | 0 .../converter/testdata/prom-labels.json | 0 .../testdata/prom-matrix-frame.jsonc | 0 ...rom-matrix-histogram-no-labels-frame.jsonc | 0 .../prom-matrix-histogram-no-labels.json | 0 ...m-matrix-histogram-partitioned-frame.jsonc | 0 .../prom-matrix-histogram-partitioned.json | 0 .../prom-matrix-with-nans-frame.jsonc | 0 .../testdata/prom-matrix-with-nans.json | 0 .../converter/testdata/prom-matrix.json | 0 .../testdata/prom-scalar-frame.jsonc | 0 .../converter/testdata/prom-scalar.json | 0 .../testdata/prom-series-frame.jsonc | 0 .../converter/testdata/prom-series.json | 0 .../testdata/prom-string-frame.jsonc | 0 .../converter/testdata/prom-string.json | 0 .../testdata/prom-vector-frame.jsonc | 0 ...rom-vector-histogram-no-labels-frame.jsonc | 0 .../prom-vector-histogram-no-labels.json | 0 .../converter/testdata/prom-vector.json | 0 .../testdata/prom-warnings-frame.jsonc | 0 .../converter/testdata/prom-warnings.json | 0 pkg/promlib/go.mod | 109 +++++++++++++ pkg/promlib/go.sum | 129 +++++++++++++++ .../prometheus => promlib}/healthcheck.go | 7 +- .../healthcheck_test.go | 25 +-- .../prometheus => promlib}/heuristics.go | 2 +- .../prometheus => promlib}/heuristics_test.go | 23 +-- .../instrumentation/instrumentation.go | 0 .../instrumentation/instrumentation_test.go | 0 .../intervalv2/intervalv2.go | 0 .../intervalv2/intervalv2_test.go | 0 pkg/promlib/library.go | 154 ++++++++++++++++++ pkg/promlib/library_test.go | 118 ++++++++++++++ .../middleware/custom_query_params.go | 0 .../middleware/custom_query_params_test.go | 0 .../middleware/force_http_get.go | 0 .../middleware/force_http_get_test.go | 0 .../prometheus => promlib}/models/query.go | 2 +- .../models/query_test.go | 4 +- .../prometheus => promlib}/models/result.go | 0 .../prometheus => promlib}/models/scope.go | 0 .../querydata/exemplar/framer.go | 0 .../querydata/exemplar/labels.go | 0 .../querydata/exemplar/sampler.go | 2 +- .../querydata/exemplar/sampler_stddev.go | 2 +- .../querydata/exemplar/sampler_stddev_test.go | 4 +- .../querydata/exemplar/sampler_test.go | 4 +- .../exemplar/testdata/noop_sampler.jsonc | 0 .../exemplar/testdata/stddev_sampler.jsonc | 0 .../querydata/framing_bench_test.go | 6 +- .../querydata/framing_test.go | 2 +- .../querydata/request.go | 12 +- .../querydata/request_test.go | 6 +- .../querydata/response.go | 8 +- .../querydata/response_test.go | 4 +- .../resource/resource.go | 6 +- .../testdata/exemplar.query.json | 0 .../testdata/exemplar.result.golden.jsonc | 0 .../testdata/exemplar.result.json | 0 .../testdata/range_auto.query.json | 0 .../testdata/range_auto.result.golden.jsonc | 0 .../testdata/range_auto.result.json | 0 .../testdata/range_infinity.query.json | 0 .../range_infinity.result.golden.jsonc | 0 .../testdata/range_infinity.result.json | 0 .../testdata/range_missing.query.json | 0 .../range_missing.result.golden.jsonc | 0 .../testdata/range_missing.result.json | 0 .../testdata/range_nan.query.json | 0 .../testdata/range_nan.result.golden.jsonc | 0 .../testdata/range_nan.result.json | 0 .../testdata/range_simple.query.json | 0 .../testdata/range_simple.result.golden.jsonc | 0 .../testdata/range_simple.result.json | 0 .../prometheus => promlib}/utils/utils.go | 0 pkg/services/alerting/conditions/query.go | 2 +- pkg/tsdb/loki/api.go | 2 +- pkg/tsdb/prometheus/azureauth/azure.go | 2 +- pkg/tsdb/prometheus/prometheus.go | 144 ++++------------ pkg/tsdb/prometheus/prometheus_test.go | 143 +++++----------- 114 files changed, 667 insertions(+), 335 deletions(-) rename pkg/{tsdb/prometheus => promlib}/client/client.go (98%) rename pkg/{tsdb/prometheus => promlib}/client/client_test.go (98%) rename pkg/{tsdb/prometheus => promlib}/client/transport.go (56%) rename pkg/{tsdb/prometheus => promlib}/client/transport_test.go (50%) rename pkg/{tsdb/prometheus => promlib}/converter/prom.go (100%) rename pkg/{tsdb/prometheus => promlib}/converter/prom_test.go (100%) rename pkg/{tsdb/prometheus => promlib}/converter/testdata/loki-streams-a-frame.jsonc (100%) rename pkg/{tsdb/prometheus => promlib}/converter/testdata/loki-streams-a.json (100%) rename pkg/{tsdb/prometheus => promlib}/converter/testdata/loki-streams-b-frame.jsonc (100%) rename pkg/{tsdb/prometheus => promlib}/converter/testdata/loki-streams-b.json (100%) rename pkg/{tsdb/prometheus => promlib}/converter/testdata/loki-streams-c-frame.jsonc (100%) rename pkg/{tsdb/prometheus => promlib}/converter/testdata/loki-streams-c.json (100%) rename pkg/{tsdb/prometheus => promlib}/converter/testdata/loki-streams-structured-metadata-frame.jsonc (100%) rename pkg/{tsdb/prometheus => promlib}/converter/testdata/loki-streams-structured-metadata.json (100%) rename pkg/{tsdb/prometheus => promlib}/converter/testdata/prom-error-frame.jsonc (100%) rename pkg/{tsdb/prometheus => promlib}/converter/testdata/prom-error.json (100%) rename pkg/{tsdb/prometheus => promlib}/converter/testdata/prom-exemplars-a-frame.json (100%) rename pkg/{tsdb/prometheus => promlib}/converter/testdata/prom-exemplars-a-frame.jsonc (100%) rename pkg/{tsdb/prometheus => promlib}/converter/testdata/prom-exemplars-a-golden.txt (100%) rename pkg/{tsdb/prometheus => promlib}/converter/testdata/prom-exemplars-a.json (100%) rename pkg/{tsdb/prometheus => promlib}/converter/testdata/prom-exemplars-b-frame.json (100%) rename pkg/{tsdb/prometheus => promlib}/converter/testdata/prom-exemplars-b-frame.jsonc (100%) rename pkg/{tsdb/prometheus => promlib}/converter/testdata/prom-exemplars-b-golden.txt (100%) rename pkg/{tsdb/prometheus => promlib}/converter/testdata/prom-exemplars-b.json (100%) rename pkg/{tsdb/prometheus => promlib}/converter/testdata/prom-exemplars-diff-labels-frame.jsonc (100%) rename pkg/{tsdb/prometheus => promlib}/converter/testdata/prom-exemplars-diff-labels.json (100%) rename pkg/{tsdb/prometheus => promlib}/converter/testdata/prom-exemplars-frame.jsonc (100%) rename pkg/{tsdb/prometheus => promlib}/converter/testdata/prom-exemplars.json (100%) rename pkg/{tsdb/prometheus => promlib}/converter/testdata/prom-labels-frame.jsonc (100%) rename pkg/{tsdb/prometheus => promlib}/converter/testdata/prom-labels.json (100%) rename pkg/{tsdb/prometheus => promlib}/converter/testdata/prom-matrix-frame.jsonc (100%) rename pkg/{tsdb/prometheus => promlib}/converter/testdata/prom-matrix-histogram-no-labels-frame.jsonc (100%) rename pkg/{tsdb/prometheus => promlib}/converter/testdata/prom-matrix-histogram-no-labels.json (100%) rename pkg/{tsdb/prometheus => promlib}/converter/testdata/prom-matrix-histogram-partitioned-frame.jsonc (100%) rename pkg/{tsdb/prometheus => promlib}/converter/testdata/prom-matrix-histogram-partitioned.json (100%) rename pkg/{tsdb/prometheus => promlib}/converter/testdata/prom-matrix-with-nans-frame.jsonc (100%) rename pkg/{tsdb/prometheus => promlib}/converter/testdata/prom-matrix-with-nans.json (100%) rename pkg/{tsdb/prometheus => promlib}/converter/testdata/prom-matrix.json (100%) rename pkg/{tsdb/prometheus => promlib}/converter/testdata/prom-scalar-frame.jsonc (100%) rename pkg/{tsdb/prometheus => promlib}/converter/testdata/prom-scalar.json (100%) rename pkg/{tsdb/prometheus => promlib}/converter/testdata/prom-series-frame.jsonc (100%) rename pkg/{tsdb/prometheus => promlib}/converter/testdata/prom-series.json (100%) rename pkg/{tsdb/prometheus => promlib}/converter/testdata/prom-string-frame.jsonc (100%) rename pkg/{tsdb/prometheus => promlib}/converter/testdata/prom-string.json (100%) rename pkg/{tsdb/prometheus => promlib}/converter/testdata/prom-vector-frame.jsonc (100%) rename pkg/{tsdb/prometheus => promlib}/converter/testdata/prom-vector-histogram-no-labels-frame.jsonc (100%) rename pkg/{tsdb/prometheus => promlib}/converter/testdata/prom-vector-histogram-no-labels.json (100%) rename pkg/{tsdb/prometheus => promlib}/converter/testdata/prom-vector.json (100%) rename pkg/{tsdb/prometheus => promlib}/converter/testdata/prom-warnings-frame.jsonc (100%) rename pkg/{tsdb/prometheus => promlib}/converter/testdata/prom-warnings.json (100%) create mode 100644 pkg/promlib/go.mod create mode 100644 pkg/promlib/go.sum rename pkg/{tsdb/prometheus => promlib}/healthcheck.go (92%) rename pkg/{tsdb/prometheus => promlib}/healthcheck_test.go (78%) rename pkg/{tsdb/prometheus => promlib}/heuristics.go (99%) rename pkg/{tsdb/prometheus => promlib}/heuristics_test.go (74%) rename pkg/{tsdb/prometheus => promlib}/instrumentation/instrumentation.go (100%) rename pkg/{tsdb/prometheus => promlib}/instrumentation/instrumentation_test.go (100%) rename pkg/{tsdb/prometheus => promlib}/intervalv2/intervalv2.go (100%) rename pkg/{tsdb/prometheus => promlib}/intervalv2/intervalv2_test.go (100%) create mode 100644 pkg/promlib/library.go create mode 100644 pkg/promlib/library_test.go rename pkg/{tsdb/prometheus => promlib}/middleware/custom_query_params.go (100%) rename pkg/{tsdb/prometheus => promlib}/middleware/custom_query_params_test.go (100%) rename pkg/{tsdb/prometheus => promlib}/middleware/force_http_get.go (100%) rename pkg/{tsdb/prometheus => promlib}/middleware/force_http_get_test.go (100%) rename pkg/{tsdb/prometheus => promlib}/models/query.go (99%) rename pkg/{tsdb/prometheus => promlib}/models/query_test.go (99%) rename pkg/{tsdb/prometheus => promlib}/models/result.go (100%) rename pkg/{tsdb/prometheus => promlib}/models/scope.go (100%) rename pkg/{tsdb/prometheus => promlib}/querydata/exemplar/framer.go (100%) rename pkg/{tsdb/prometheus => promlib}/querydata/exemplar/labels.go (100%) rename pkg/{tsdb/prometheus => promlib}/querydata/exemplar/sampler.go (93%) rename pkg/{tsdb/prometheus => promlib}/querydata/exemplar/sampler_stddev.go (97%) rename pkg/{tsdb/prometheus => promlib}/querydata/exemplar/sampler_stddev_test.go (84%) rename pkg/{tsdb/prometheus => promlib}/querydata/exemplar/sampler_test.go (88%) rename pkg/{tsdb/prometheus => promlib}/querydata/exemplar/testdata/noop_sampler.jsonc (100%) rename pkg/{tsdb/prometheus => promlib}/querydata/exemplar/testdata/stddev_sampler.jsonc (100%) rename pkg/{tsdb/prometheus => promlib}/querydata/framing_bench_test.go (93%) rename pkg/{tsdb/prometheus => promlib}/querydata/framing_test.go (98%) rename pkg/{tsdb/prometheus => promlib}/querydata/request.go (94%) rename pkg/{tsdb/prometheus => promlib}/querydata/request_test.go (98%) rename pkg/{tsdb/prometheus => promlib}/querydata/response.go (95%) rename pkg/{tsdb/prometheus => promlib}/querydata/response_test.go (96%) rename pkg/{tsdb/prometheus => promlib}/resource/resource.go (92%) rename pkg/{tsdb/prometheus => promlib}/testdata/exemplar.query.json (100%) rename pkg/{tsdb/prometheus => promlib}/testdata/exemplar.result.golden.jsonc (100%) rename pkg/{tsdb/prometheus => promlib}/testdata/exemplar.result.json (100%) rename pkg/{tsdb/prometheus => promlib}/testdata/range_auto.query.json (100%) rename pkg/{tsdb/prometheus => promlib}/testdata/range_auto.result.golden.jsonc (100%) rename pkg/{tsdb/prometheus => promlib}/testdata/range_auto.result.json (100%) rename pkg/{tsdb/prometheus => promlib}/testdata/range_infinity.query.json (100%) rename pkg/{tsdb/prometheus => promlib}/testdata/range_infinity.result.golden.jsonc (100%) rename pkg/{tsdb/prometheus => promlib}/testdata/range_infinity.result.json (100%) rename pkg/{tsdb/prometheus => promlib}/testdata/range_missing.query.json (100%) rename pkg/{tsdb/prometheus => promlib}/testdata/range_missing.result.golden.jsonc (100%) rename pkg/{tsdb/prometheus => promlib}/testdata/range_missing.result.json (100%) rename pkg/{tsdb/prometheus => promlib}/testdata/range_nan.query.json (100%) rename pkg/{tsdb/prometheus => promlib}/testdata/range_nan.result.golden.jsonc (100%) rename pkg/{tsdb/prometheus => promlib}/testdata/range_nan.result.json (100%) rename pkg/{tsdb/prometheus => promlib}/testdata/range_simple.query.json (100%) rename pkg/{tsdb/prometheus => promlib}/testdata/range_simple.result.golden.jsonc (100%) rename pkg/{tsdb/prometheus => promlib}/testdata/range_simple.result.json (100%) rename pkg/{tsdb/prometheus => promlib}/utils/utils.go (100%) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 1850e80848..49d503cce4 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -105,6 +105,7 @@ /pkg/server/ @grafana/backend-platform /pkg/apiserver @grafana/grafana-app-platform-squad /pkg/apimachinery @grafana/grafana-app-platform-squad +/pkg/promlib @grafana/observability-metrics /pkg/services/annotations/ @grafana/backend-platform /pkg/services/apikey/ @grafana/identity-access-team /pkg/services/cleanup/ @grafana/backend-platform diff --git a/Makefile b/Makefile index f6f07671f0..3d53ca4dcf 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,7 @@ include .bingo/Variables.mk .PHONY: all deps-go deps-js deps build-go build-backend build-server build-cli build-js build build-docker-full build-docker-full-ubuntu lint-go golangci-lint test-go test-js gen-ts test run run-frontend clean devenv devenv-down protobuf drone help gen-go gen-cue fix-cue GO = go -GO_FILES ?= ./pkg/... ./pkg/apiserver/... ./pkg/apimachinery/... +GO_FILES ?= ./pkg/... ./pkg/apiserver/... ./pkg/apimachinery/... ./pkg/promlib/... SH_FILES ?= $(shell find ./scripts -name *.sh) GO_BUILD_FLAGS += $(if $(GO_BUILD_DEV),-dev) GO_BUILD_FLAGS += $(if $(GO_BUILD_TAGS),-build-tags=$(GO_BUILD_TAGS)) diff --git a/go.mod b/go.mod index 9f83190cea..dfc6557978 100644 --- a/go.mod +++ b/go.mod @@ -122,7 +122,7 @@ require ( gopkg.in/mail.v2 v2.3.1 // @grafana/backend-platform gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // @grafana/alerting-squad-backend - xorm.io/builder v0.3.6 // @grafana/backend-platform + xorm.io/builder v0.3.6 // indirect; @grafana/backend-platform xorm.io/core v0.7.3 // @grafana/backend-platform xorm.io/xorm v0.8.2 // @grafana/alerting-squad-backend ) @@ -174,7 +174,7 @@ require ( github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-msgpack v0.5.5 // indirect - github.com/hashicorp/go-multierror v1.1.1 // @grafana/alerting-squad + github.com/hashicorp/go-multierror v1.1.1 // indirect; @grafana/alerting-squad github.com/hashicorp/go-sockaddr v1.0.6 // indirect github.com/hashicorp/golang-lru v0.6.0 // indirect github.com/hashicorp/yamux v0.1.1 // indirect @@ -337,7 +337,7 @@ require ( github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect github.com/grafana/regexp v0.0.0-20221123153739-15dc172cd2db // indirect github.com/hashicorp/go-immutable-radix v1.3.1 // indirect - github.com/hashicorp/golang-lru/v2 v2.0.7 // @grafana/alerting-squad-backend + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect; @grafana/alerting-squad-backend github.com/hashicorp/memberlist v0.5.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/invopop/yaml v0.2.0 // indirect diff --git a/go.work b/go.work index 33dd16d7ea..3bb653aaf4 100644 --- a/go.work +++ b/go.work @@ -4,6 +4,7 @@ use ( . ./pkg/apimachinery ./pkg/apiserver + ./pkg/promlib ./pkg/util/xorm ) diff --git a/go.work.sum b/go.work.sum index 5c27afae06..5e6aae5593 100644 --- a/go.work.sum +++ b/go.work.sum @@ -247,19 +247,18 @@ github.com/aws/aws-sdk-go-v2/service/sns v1.17.4 h1:7TdmoJJBwLFyakXjfrGztejwY5Ie github.com/aws/aws-sdk-go-v2/service/sqs v1.18.3 h1:uHjK81fESbGy2Y9lspub1+C6VN5W2UXTDo2A/Pm4G0U= github.com/aws/aws-sdk-go-v2/service/ssm v1.24.1 h1:zc1YLcknvxdW/i1MuJKmEnFB2TNkOfguuQaGRvJXPng= github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible h1:Ppm0npCCsmuR9oQaBtRuZcmILVE74aXE+AmrJj8L2ns= -github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 h1:mXoPYz/Ul5HYEDvkta6I8/rnYM5gSdSV2tJ6XbZuEtY= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= github.com/boombuler/barcode v1.0.1 h1:NDBbPmhS+EqABEs5Kg3n/5ZNjy73Pz7SIV+KCeqyXcs= -github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/bwesterb/go-ristretto v1.2.3 h1:1w53tCkGhCQ5djbat3+MH0BAQ5Kfgbt56UZQ/JMzngw= github.com/casbin/casbin/v2 v2.37.0 h1:/poEwPSovi4bTOcP752/CsTQiRz2xycyVKFG7GUhbDw= github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= +github.com/chromedp/cdproto v0.0.0-20220208224320-6efb837e6bc2/go.mod h1:At5TxYYdxkbQL0TSefRjhLE3Q0lgvqKKMSFUglJ7i1U= github.com/chromedp/chromedp v0.9.2 h1:dKtNz4kApb06KuSXoTQIyUC2TrA0fhGDwNZf3bcgfKw= github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic= github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= @@ -350,7 +349,6 @@ github.com/go-fonts/stix v0.1.0 h1:UlZlgrvvmT/58o573ot7NFw0vZasZ5I6bcIft/oMdgg= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1 h1:QbL/5oDUmRBzO9/Z7Seo6zf912W/a6Sr4Eu0G/3Jho0= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4 h1:WtGNWLvXpe6ZudgnXrq0barxBImvnnJoMEhXAzcbM0I= github.com/go-ini/ini v1.25.4 h1:Mujh4R/dH6YL8bxuISne3xX2+qcQ9p0IxKAP6ExWoUo= -github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k= github.com/go-kit/kit v0.12.0 h1:e4o3o3IsBfAKQh5Qbbiqyfu97Ku7jrO/JbohvztANh4= github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81 h1:6zl3BbBhdnMkpSj2YY30qV3gDcVBGtFgVsV3+/i+mKQ= github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab h1:xveKWz2iaueeTaUgdetzel+U7exyigDYBryyVfV/rZk= @@ -405,6 +403,7 @@ github.com/grafana/e2e v0.1.1-0.20221018202458-cffd2bb71c7b h1:Ha+kSIoTutf4ytlVw github.com/grafana/e2e v0.1.1-0.20221018202458-cffd2bb71c7b/go.mod h1:3UsooRp7yW5/NJQBlXcTsAHOoykEhNUYXkQ3r6ehEEY= github.com/grafana/gomemcache v0.0.0-20231023152154-6947259a0586 h1:/of8Z8taCPftShATouOrBVy6GaTTjgQd/VfNiZp/VXQ= github.com/grafana/gomemcache v0.0.0-20231023152154-6947259a0586/go.mod h1:PGk3RjYHpxMM8HFPhKKo+vve3DdlPUELZLSDEFehPuU= +github.com/grafana/grafana-plugin-sdk-go v0.212.0/go.mod h1:qsI4ktDf0lig74u8SLPJf9zRdVxWV/W4Wi+Ox6gifgs= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM= github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.1/go.mod h1:YvJ2f6MplWDhfxiUC3KpyTy76kYUZA4W3pTv/wdKQ9Y= github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 h1:MJG/KsmcqMwFAkh8mTnAwhyKoB+sTAnY4CACC110tbU= @@ -430,7 +429,6 @@ github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab h1:BA4a7pe github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk= github.com/influxdata/influxdb v1.7.6 h1:8mQ7A/V+3noMGCt/P9pD09ISaiz9XvgCk303UYA3gcs= github.com/influxdata/influxdb1-client v0.0.0-20200827194710-b269163b24ab h1:HqW4xhhynfjrtEiiSGcQUd6vrK23iMam1FO8rI7mwig= -github.com/invopop/jsonschema v0.12.0 h1:6ovsNSuvn9wEQVOyc72aycBMVQFKz7cPdMJn10CvzRI= github.com/invopop/jsonschema v0.12.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= github.com/iris-contrib/blackfriday v2.0.0+incompatible h1:o5sHQHHm0ToHUlAJSTjW9UWicjJSDDauOOQ2AHuIVp4= github.com/iris-contrib/go.uuid v2.0.0+incompatible h1:XZubAYg61/JwnJNbZilGjf3b3pB80+OQg2qf6c8BfWE= @@ -491,6 +489,7 @@ github.com/kataras/sitemap v0.0.5 h1:4HCONX5RLgVy6G4RkYOV3vKNcma9p236LdGOipJsaFE github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= github.com/kisielk/errcheck v1.5.0 h1:e8esj/e4R+SAOwFwN+n3zr0nYeCyeweozKfO23MvHzY= github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg= +github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/klauspost/cpuid v1.2.1 h1:vJi+O/nMdFt0vqm8NZBI6wzALWdA2X+egi0ogNyrC/w= github.com/knadh/koanf v1.5.0 h1:q2TSd/3Pyc/5yP9ldIrSdIz26MCcyNQzW0pEAugLPNs= github.com/knadh/koanf v1.5.0/go.mod h1:Hgyjp4y8v44hpZtPzs7JZfRAW5AhN7KfZcwv1RYggDs= @@ -693,7 +692,6 @@ github.com/willf/bitset v1.1.11 h1:N7Z7E9UvjW+sGsEl7k/SJrvY2reP1A07MrGuCjIOjRE= github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI= github.com/willf/bloom v2.0.3+incompatible h1:QDacWdqcAUI1MPOwIQZRy9kOR7yxfyEmxX8Wdm2/JPA= github.com/willf/bloom v2.0.3+incompatible/go.mod h1:MmAltL9pDMNTrvUkxdg0k0q5I0suxmuwp3KbyrZLOZ8= -github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= github.com/xanzy/go-gitlab v0.15.0 h1:rWtwKTgEnXyNUGrOArN7yyc3THRkpYcKXIXia9abywQ= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= @@ -739,9 +737,10 @@ go.opentelemetry.io/contrib v0.18.0 h1:uqBh0brileIvG6luvBjdxzoFL8lxDGuhxJWsvK3Bv go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.42.0/go.mod h1:5z+/ZWJQKXa9YT34fQNx5K8Hd1EoIhvtUygUQPqEOgQ= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1/go.mod h1:4UoMYEZOC0yN/sPGH76KPkkU7zgiEWYWL9vwmbnTJPE= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.47.0/go.mod h1:r9vWsPS/3AQItv3OSlEJ/E4mbrhUbbw18meOjArPtKQ= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.46.1/go.mod h1:GnOaBaFQ2we3b9AGWJpsBa7v1S5RlQzlC3O7dRMxZhM= go.opentelemetry.io/contrib/propagators/b3 v1.15.0 h1:bMaonPyFcAvZ4EVzkUNkfnUHP5Zi63CIDlA3dRsEg8Q= go.opentelemetry.io/contrib/propagators/b3 v1.15.0/go.mod h1:VjU0g2v6HSQ+NwfifambSLAeBgevjIcqmceaKWEzl0c= -go.opentelemetry.io/contrib/samplers/jaegerremote v0.18.0 h1:Q9PrD94WoMolBx44ef5UWWvufpVSME0MiSymXZfedso= +go.opentelemetry.io/contrib/samplers/jaegerremote v0.16.0/go.mod h1:StxwPndBVNZD2sZez0RQ0SP/129XGCd4aEmVGaw1/QM= go.opentelemetry.io/otel/bridge/opencensus v0.37.0 h1:ieH3gw7b1eg90ARsFAlAsX5LKVZgnCYfaDwRrK6xLHU= go.opentelemetry.io/otel/bridge/opencensus v0.37.0/go.mod h1:ddiK+1PE68l/Xk04BGTh9Y6WIcxcLrmcVxVlS0w5WZ0= go.opentelemetry.io/otel/bridge/opentracing v1.10.0 h1:WzAVGovpC1s7KD5g4taU6BWYZP3QGSDVTlbRu9fIHw8= @@ -764,12 +763,15 @@ go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEa go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= golang.org/x/image v0.0.0-20220302094943-723b81ca9867 h1:TcHcE0vrmgzNH1v3ppjcMGbhG5+9fMuvOmUYwNEF4q4= golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028 h1:4+4C/Iv2U4fMZBiMCc98MG1In4gJY5YRhtpDNeDeHWs= +golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20211123203042-d83791d6bcd9/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/tools v0.12.0/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM= +golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0 h1:OE9mWmgKkjJyEmDAAtGMPjXu+YNeGvK9VTSHY6+Qihc= gonum.org/v1/plot v0.10.1 h1:dnifSs43YJuNMDzB7v8wV64O4ABBHReuAVAoBxqBqS4= diff --git a/pkg/tsdb/prometheus/client/client.go b/pkg/promlib/client/client.go similarity index 98% rename from pkg/tsdb/prometheus/client/client.go rename to pkg/promlib/client/client.go index 47a5969d37..b23196b6f5 100644 --- a/pkg/tsdb/prometheus/client/client.go +++ b/pkg/promlib/client/client.go @@ -13,7 +13,7 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/tsdb/prometheus/models" + "github.com/grafana/grafana/pkg/promlib/models" ) type doer interface { diff --git a/pkg/tsdb/prometheus/client/client_test.go b/pkg/promlib/client/client_test.go similarity index 98% rename from pkg/tsdb/prometheus/client/client_test.go rename to pkg/promlib/client/client_test.go index 1f95757160..16dc76fb27 100644 --- a/pkg/tsdb/prometheus/client/client_test.go +++ b/pkg/promlib/client/client_test.go @@ -11,7 +11,7 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/stretchr/testify/require" - "github.com/grafana/grafana/pkg/tsdb/prometheus/models" + "github.com/grafana/grafana/pkg/promlib/models" ) type MockDoer struct { diff --git a/pkg/tsdb/prometheus/client/transport.go b/pkg/promlib/client/transport.go similarity index 56% rename from pkg/tsdb/prometheus/client/transport.go rename to pkg/promlib/client/transport.go index a333e04faf..429befa152 100644 --- a/pkg/tsdb/prometheus/client/transport.go +++ b/pkg/promlib/client/transport.go @@ -5,20 +5,17 @@ import ( "fmt" "strings" - "github.com/grafana/grafana-azure-sdk-go/azsettings" - "github.com/grafana/grafana-azure-sdk-go/util/maputil" "github.com/grafana/grafana-plugin-sdk-go/backend" sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" + "github.com/grafana/grafana-plugin-sdk-go/data/utils/maputil" "github.com/grafana/grafana-plugin-sdk-go/backend/log" - "github.com/grafana/grafana/pkg/tsdb/prometheus/azureauth" - "github.com/grafana/grafana/pkg/tsdb/prometheus/middleware" - "github.com/grafana/grafana/pkg/tsdb/prometheus/utils" + "github.com/grafana/grafana/pkg/promlib/middleware" + "github.com/grafana/grafana/pkg/promlib/utils" ) -// CreateTransportOptions creates options for the http client. Probably should be shared and should not live in the -// buffered package. +// CreateTransportOptions creates options for the http client. func CreateTransportOptions(ctx context.Context, settings backend.DataSourceInstanceSettings, logger log.Logger) (*sdkhttpclient.Options, error) { opts, err := settings.HTTPClientOptions(ctx) if err != nil { @@ -33,25 +30,6 @@ func CreateTransportOptions(ctx context.Context, settings backend.DataSourceInst opts.Middlewares = middlewares(logger, httpMethod) - // Set SigV4 service namespace - if opts.SigV4 != nil { - opts.SigV4.Service = "aps" - } - - azureSettings, err := azsettings.ReadSettings(ctx) - if err != nil { - logger.Error("failed to read Azure settings from Grafana", "error", err.Error()) - return nil, fmt.Errorf("failed to read Azure settings from Grafana: %v", err) - } - - // Set Azure authentication - if azureSettings.AzureAuthEnabled { - err = azureauth.ConfigureAzureAuthentication(settings, azureSettings, &opts) - if err != nil { - return nil, fmt.Errorf("error configuring Azure auth: %v", err) - } - } - return &opts, nil } diff --git a/pkg/tsdb/prometheus/client/transport_test.go b/pkg/promlib/client/transport_test.go similarity index 50% rename from pkg/tsdb/prometheus/client/transport_test.go rename to pkg/promlib/client/transport_test.go index de7b165c6f..61c481adca 100644 --- a/pkg/tsdb/prometheus/client/transport_test.go +++ b/pkg/promlib/client/transport_test.go @@ -4,7 +4,6 @@ import ( "context" "testing" - "github.com/grafana/grafana-azure-sdk-go/azsettings" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/stretchr/testify/require" ) @@ -24,25 +23,4 @@ func TestCreateTransportOptions(t *testing.T) { require.Equal(t, map[string]string{"foo": "bar"}, opts.Headers) require.Equal(t, 2, len(opts.Middlewares)) }) - - t.Run("add azure credentials if configured", func(t *testing.T) { - cfg := backend.NewGrafanaCfg(map[string]string{ - azsettings.AzureCloud: azsettings.AzurePublic, - azsettings.AzureAuthEnabled: "true", - }) - settings := backend.DataSourceInstanceSettings{ - BasicAuthEnabled: false, - BasicAuthUser: "", - JSONData: []byte(`{ - "azureCredentials": { - "authType": "msi" - } - }`), - DecryptedSecureJSONData: map[string]string{}, - } - ctx := backend.WithGrafanaConfig(context.Background(), cfg) - opts, err := CreateTransportOptions(ctx, settings, backend.NewLoggerWith("logger", "test")) - require.NoError(t, err) - require.Equal(t, 3, len(opts.Middlewares)) - }) } diff --git a/pkg/tsdb/prometheus/converter/prom.go b/pkg/promlib/converter/prom.go similarity index 100% rename from pkg/tsdb/prometheus/converter/prom.go rename to pkg/promlib/converter/prom.go diff --git a/pkg/tsdb/prometheus/converter/prom_test.go b/pkg/promlib/converter/prom_test.go similarity index 100% rename from pkg/tsdb/prometheus/converter/prom_test.go rename to pkg/promlib/converter/prom_test.go diff --git a/pkg/tsdb/prometheus/converter/testdata/loki-streams-a-frame.jsonc b/pkg/promlib/converter/testdata/loki-streams-a-frame.jsonc similarity index 100% rename from pkg/tsdb/prometheus/converter/testdata/loki-streams-a-frame.jsonc rename to pkg/promlib/converter/testdata/loki-streams-a-frame.jsonc diff --git a/pkg/tsdb/prometheus/converter/testdata/loki-streams-a.json b/pkg/promlib/converter/testdata/loki-streams-a.json similarity index 100% rename from pkg/tsdb/prometheus/converter/testdata/loki-streams-a.json rename to pkg/promlib/converter/testdata/loki-streams-a.json diff --git a/pkg/tsdb/prometheus/converter/testdata/loki-streams-b-frame.jsonc b/pkg/promlib/converter/testdata/loki-streams-b-frame.jsonc similarity index 100% rename from pkg/tsdb/prometheus/converter/testdata/loki-streams-b-frame.jsonc rename to pkg/promlib/converter/testdata/loki-streams-b-frame.jsonc diff --git a/pkg/tsdb/prometheus/converter/testdata/loki-streams-b.json b/pkg/promlib/converter/testdata/loki-streams-b.json similarity index 100% rename from pkg/tsdb/prometheus/converter/testdata/loki-streams-b.json rename to pkg/promlib/converter/testdata/loki-streams-b.json diff --git a/pkg/tsdb/prometheus/converter/testdata/loki-streams-c-frame.jsonc b/pkg/promlib/converter/testdata/loki-streams-c-frame.jsonc similarity index 100% rename from pkg/tsdb/prometheus/converter/testdata/loki-streams-c-frame.jsonc rename to pkg/promlib/converter/testdata/loki-streams-c-frame.jsonc diff --git a/pkg/tsdb/prometheus/converter/testdata/loki-streams-c.json b/pkg/promlib/converter/testdata/loki-streams-c.json similarity index 100% rename from pkg/tsdb/prometheus/converter/testdata/loki-streams-c.json rename to pkg/promlib/converter/testdata/loki-streams-c.json diff --git a/pkg/tsdb/prometheus/converter/testdata/loki-streams-structured-metadata-frame.jsonc b/pkg/promlib/converter/testdata/loki-streams-structured-metadata-frame.jsonc similarity index 100% rename from pkg/tsdb/prometheus/converter/testdata/loki-streams-structured-metadata-frame.jsonc rename to pkg/promlib/converter/testdata/loki-streams-structured-metadata-frame.jsonc diff --git a/pkg/tsdb/prometheus/converter/testdata/loki-streams-structured-metadata.json b/pkg/promlib/converter/testdata/loki-streams-structured-metadata.json similarity index 100% rename from pkg/tsdb/prometheus/converter/testdata/loki-streams-structured-metadata.json rename to pkg/promlib/converter/testdata/loki-streams-structured-metadata.json diff --git a/pkg/tsdb/prometheus/converter/testdata/prom-error-frame.jsonc b/pkg/promlib/converter/testdata/prom-error-frame.jsonc similarity index 100% rename from pkg/tsdb/prometheus/converter/testdata/prom-error-frame.jsonc rename to pkg/promlib/converter/testdata/prom-error-frame.jsonc diff --git a/pkg/tsdb/prometheus/converter/testdata/prom-error.json b/pkg/promlib/converter/testdata/prom-error.json similarity index 100% rename from pkg/tsdb/prometheus/converter/testdata/prom-error.json rename to pkg/promlib/converter/testdata/prom-error.json diff --git a/pkg/tsdb/prometheus/converter/testdata/prom-exemplars-a-frame.json b/pkg/promlib/converter/testdata/prom-exemplars-a-frame.json similarity index 100% rename from pkg/tsdb/prometheus/converter/testdata/prom-exemplars-a-frame.json rename to pkg/promlib/converter/testdata/prom-exemplars-a-frame.json diff --git a/pkg/tsdb/prometheus/converter/testdata/prom-exemplars-a-frame.jsonc b/pkg/promlib/converter/testdata/prom-exemplars-a-frame.jsonc similarity index 100% rename from pkg/tsdb/prometheus/converter/testdata/prom-exemplars-a-frame.jsonc rename to pkg/promlib/converter/testdata/prom-exemplars-a-frame.jsonc diff --git a/pkg/tsdb/prometheus/converter/testdata/prom-exemplars-a-golden.txt b/pkg/promlib/converter/testdata/prom-exemplars-a-golden.txt similarity index 100% rename from pkg/tsdb/prometheus/converter/testdata/prom-exemplars-a-golden.txt rename to pkg/promlib/converter/testdata/prom-exemplars-a-golden.txt diff --git a/pkg/tsdb/prometheus/converter/testdata/prom-exemplars-a.json b/pkg/promlib/converter/testdata/prom-exemplars-a.json similarity index 100% rename from pkg/tsdb/prometheus/converter/testdata/prom-exemplars-a.json rename to pkg/promlib/converter/testdata/prom-exemplars-a.json diff --git a/pkg/tsdb/prometheus/converter/testdata/prom-exemplars-b-frame.json b/pkg/promlib/converter/testdata/prom-exemplars-b-frame.json similarity index 100% rename from pkg/tsdb/prometheus/converter/testdata/prom-exemplars-b-frame.json rename to pkg/promlib/converter/testdata/prom-exemplars-b-frame.json diff --git a/pkg/tsdb/prometheus/converter/testdata/prom-exemplars-b-frame.jsonc b/pkg/promlib/converter/testdata/prom-exemplars-b-frame.jsonc similarity index 100% rename from pkg/tsdb/prometheus/converter/testdata/prom-exemplars-b-frame.jsonc rename to pkg/promlib/converter/testdata/prom-exemplars-b-frame.jsonc diff --git a/pkg/tsdb/prometheus/converter/testdata/prom-exemplars-b-golden.txt b/pkg/promlib/converter/testdata/prom-exemplars-b-golden.txt similarity index 100% rename from pkg/tsdb/prometheus/converter/testdata/prom-exemplars-b-golden.txt rename to pkg/promlib/converter/testdata/prom-exemplars-b-golden.txt diff --git a/pkg/tsdb/prometheus/converter/testdata/prom-exemplars-b.json b/pkg/promlib/converter/testdata/prom-exemplars-b.json similarity index 100% rename from pkg/tsdb/prometheus/converter/testdata/prom-exemplars-b.json rename to pkg/promlib/converter/testdata/prom-exemplars-b.json diff --git a/pkg/tsdb/prometheus/converter/testdata/prom-exemplars-diff-labels-frame.jsonc b/pkg/promlib/converter/testdata/prom-exemplars-diff-labels-frame.jsonc similarity index 100% rename from pkg/tsdb/prometheus/converter/testdata/prom-exemplars-diff-labels-frame.jsonc rename to pkg/promlib/converter/testdata/prom-exemplars-diff-labels-frame.jsonc diff --git a/pkg/tsdb/prometheus/converter/testdata/prom-exemplars-diff-labels.json b/pkg/promlib/converter/testdata/prom-exemplars-diff-labels.json similarity index 100% rename from pkg/tsdb/prometheus/converter/testdata/prom-exemplars-diff-labels.json rename to pkg/promlib/converter/testdata/prom-exemplars-diff-labels.json diff --git a/pkg/tsdb/prometheus/converter/testdata/prom-exemplars-frame.jsonc b/pkg/promlib/converter/testdata/prom-exemplars-frame.jsonc similarity index 100% rename from pkg/tsdb/prometheus/converter/testdata/prom-exemplars-frame.jsonc rename to pkg/promlib/converter/testdata/prom-exemplars-frame.jsonc diff --git a/pkg/tsdb/prometheus/converter/testdata/prom-exemplars.json b/pkg/promlib/converter/testdata/prom-exemplars.json similarity index 100% rename from pkg/tsdb/prometheus/converter/testdata/prom-exemplars.json rename to pkg/promlib/converter/testdata/prom-exemplars.json diff --git a/pkg/tsdb/prometheus/converter/testdata/prom-labels-frame.jsonc b/pkg/promlib/converter/testdata/prom-labels-frame.jsonc similarity index 100% rename from pkg/tsdb/prometheus/converter/testdata/prom-labels-frame.jsonc rename to pkg/promlib/converter/testdata/prom-labels-frame.jsonc diff --git a/pkg/tsdb/prometheus/converter/testdata/prom-labels.json b/pkg/promlib/converter/testdata/prom-labels.json similarity index 100% rename from pkg/tsdb/prometheus/converter/testdata/prom-labels.json rename to pkg/promlib/converter/testdata/prom-labels.json diff --git a/pkg/tsdb/prometheus/converter/testdata/prom-matrix-frame.jsonc b/pkg/promlib/converter/testdata/prom-matrix-frame.jsonc similarity index 100% rename from pkg/tsdb/prometheus/converter/testdata/prom-matrix-frame.jsonc rename to pkg/promlib/converter/testdata/prom-matrix-frame.jsonc diff --git a/pkg/tsdb/prometheus/converter/testdata/prom-matrix-histogram-no-labels-frame.jsonc b/pkg/promlib/converter/testdata/prom-matrix-histogram-no-labels-frame.jsonc similarity index 100% rename from pkg/tsdb/prometheus/converter/testdata/prom-matrix-histogram-no-labels-frame.jsonc rename to pkg/promlib/converter/testdata/prom-matrix-histogram-no-labels-frame.jsonc diff --git a/pkg/tsdb/prometheus/converter/testdata/prom-matrix-histogram-no-labels.json b/pkg/promlib/converter/testdata/prom-matrix-histogram-no-labels.json similarity index 100% rename from pkg/tsdb/prometheus/converter/testdata/prom-matrix-histogram-no-labels.json rename to pkg/promlib/converter/testdata/prom-matrix-histogram-no-labels.json diff --git a/pkg/tsdb/prometheus/converter/testdata/prom-matrix-histogram-partitioned-frame.jsonc b/pkg/promlib/converter/testdata/prom-matrix-histogram-partitioned-frame.jsonc similarity index 100% rename from pkg/tsdb/prometheus/converter/testdata/prom-matrix-histogram-partitioned-frame.jsonc rename to pkg/promlib/converter/testdata/prom-matrix-histogram-partitioned-frame.jsonc diff --git a/pkg/tsdb/prometheus/converter/testdata/prom-matrix-histogram-partitioned.json b/pkg/promlib/converter/testdata/prom-matrix-histogram-partitioned.json similarity index 100% rename from pkg/tsdb/prometheus/converter/testdata/prom-matrix-histogram-partitioned.json rename to pkg/promlib/converter/testdata/prom-matrix-histogram-partitioned.json diff --git a/pkg/tsdb/prometheus/converter/testdata/prom-matrix-with-nans-frame.jsonc b/pkg/promlib/converter/testdata/prom-matrix-with-nans-frame.jsonc similarity index 100% rename from pkg/tsdb/prometheus/converter/testdata/prom-matrix-with-nans-frame.jsonc rename to pkg/promlib/converter/testdata/prom-matrix-with-nans-frame.jsonc diff --git a/pkg/tsdb/prometheus/converter/testdata/prom-matrix-with-nans.json b/pkg/promlib/converter/testdata/prom-matrix-with-nans.json similarity index 100% rename from pkg/tsdb/prometheus/converter/testdata/prom-matrix-with-nans.json rename to pkg/promlib/converter/testdata/prom-matrix-with-nans.json diff --git a/pkg/tsdb/prometheus/converter/testdata/prom-matrix.json b/pkg/promlib/converter/testdata/prom-matrix.json similarity index 100% rename from pkg/tsdb/prometheus/converter/testdata/prom-matrix.json rename to pkg/promlib/converter/testdata/prom-matrix.json diff --git a/pkg/tsdb/prometheus/converter/testdata/prom-scalar-frame.jsonc b/pkg/promlib/converter/testdata/prom-scalar-frame.jsonc similarity index 100% rename from pkg/tsdb/prometheus/converter/testdata/prom-scalar-frame.jsonc rename to pkg/promlib/converter/testdata/prom-scalar-frame.jsonc diff --git a/pkg/tsdb/prometheus/converter/testdata/prom-scalar.json b/pkg/promlib/converter/testdata/prom-scalar.json similarity index 100% rename from pkg/tsdb/prometheus/converter/testdata/prom-scalar.json rename to pkg/promlib/converter/testdata/prom-scalar.json diff --git a/pkg/tsdb/prometheus/converter/testdata/prom-series-frame.jsonc b/pkg/promlib/converter/testdata/prom-series-frame.jsonc similarity index 100% rename from pkg/tsdb/prometheus/converter/testdata/prom-series-frame.jsonc rename to pkg/promlib/converter/testdata/prom-series-frame.jsonc diff --git a/pkg/tsdb/prometheus/converter/testdata/prom-series.json b/pkg/promlib/converter/testdata/prom-series.json similarity index 100% rename from pkg/tsdb/prometheus/converter/testdata/prom-series.json rename to pkg/promlib/converter/testdata/prom-series.json diff --git a/pkg/tsdb/prometheus/converter/testdata/prom-string-frame.jsonc b/pkg/promlib/converter/testdata/prom-string-frame.jsonc similarity index 100% rename from pkg/tsdb/prometheus/converter/testdata/prom-string-frame.jsonc rename to pkg/promlib/converter/testdata/prom-string-frame.jsonc diff --git a/pkg/tsdb/prometheus/converter/testdata/prom-string.json b/pkg/promlib/converter/testdata/prom-string.json similarity index 100% rename from pkg/tsdb/prometheus/converter/testdata/prom-string.json rename to pkg/promlib/converter/testdata/prom-string.json diff --git a/pkg/tsdb/prometheus/converter/testdata/prom-vector-frame.jsonc b/pkg/promlib/converter/testdata/prom-vector-frame.jsonc similarity index 100% rename from pkg/tsdb/prometheus/converter/testdata/prom-vector-frame.jsonc rename to pkg/promlib/converter/testdata/prom-vector-frame.jsonc diff --git a/pkg/tsdb/prometheus/converter/testdata/prom-vector-histogram-no-labels-frame.jsonc b/pkg/promlib/converter/testdata/prom-vector-histogram-no-labels-frame.jsonc similarity index 100% rename from pkg/tsdb/prometheus/converter/testdata/prom-vector-histogram-no-labels-frame.jsonc rename to pkg/promlib/converter/testdata/prom-vector-histogram-no-labels-frame.jsonc diff --git a/pkg/tsdb/prometheus/converter/testdata/prom-vector-histogram-no-labels.json b/pkg/promlib/converter/testdata/prom-vector-histogram-no-labels.json similarity index 100% rename from pkg/tsdb/prometheus/converter/testdata/prom-vector-histogram-no-labels.json rename to pkg/promlib/converter/testdata/prom-vector-histogram-no-labels.json diff --git a/pkg/tsdb/prometheus/converter/testdata/prom-vector.json b/pkg/promlib/converter/testdata/prom-vector.json similarity index 100% rename from pkg/tsdb/prometheus/converter/testdata/prom-vector.json rename to pkg/promlib/converter/testdata/prom-vector.json diff --git a/pkg/tsdb/prometheus/converter/testdata/prom-warnings-frame.jsonc b/pkg/promlib/converter/testdata/prom-warnings-frame.jsonc similarity index 100% rename from pkg/tsdb/prometheus/converter/testdata/prom-warnings-frame.jsonc rename to pkg/promlib/converter/testdata/prom-warnings-frame.jsonc diff --git a/pkg/tsdb/prometheus/converter/testdata/prom-warnings.json b/pkg/promlib/converter/testdata/prom-warnings.json similarity index 100% rename from pkg/tsdb/prometheus/converter/testdata/prom-warnings.json rename to pkg/promlib/converter/testdata/prom-warnings.json diff --git a/pkg/promlib/go.mod b/pkg/promlib/go.mod new file mode 100644 index 0000000000..b6a16bfab7 --- /dev/null +++ b/pkg/promlib/go.mod @@ -0,0 +1,109 @@ +module github.com/grafana/grafana/pkg/promlib + +go 1.21.0 + +require ( + github.com/grafana/grafana-plugin-sdk-go v0.214.0 + github.com/json-iterator/go v1.1.12 + github.com/patrickmn/go-cache v2.1.0+incompatible + github.com/prometheus/client_golang v1.18.0 + github.com/prometheus/common v0.46.0 + github.com/prometheus/prometheus v1.8.2-0.20221021121301-51a44e6657c3 + github.com/stretchr/testify v1.8.4 + go.opentelemetry.io/otel v1.24.0 + go.opentelemetry.io/otel/trace v1.24.0 + golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb +) + +require ( + github.com/BurntSushi/toml v1.3.2 // indirect + github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9 // indirect + github.com/apache/arrow/go/v15 v15.0.0 // indirect + github.com/aws/aws-sdk-go v1.50.8 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cenkalti/backoff/v4 v4.2.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cheekybits/genny v1.0.0 // indirect + github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/dennwc/varint v1.0.0 // indirect + github.com/elazarl/goproxy v0.0.0-20230731152917-f99041a5c027 // indirect + github.com/fatih/color v1.15.0 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/getkin/kin-openapi v0.120.0 // indirect + github.com/go-kit/log v0.2.1 // indirect + github.com/go-logfmt/logfmt v0.6.0 // indirect + github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-openapi/jsonpointer v0.20.2 // indirect + github.com/go-openapi/swag v0.22.9 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/google/flatbuffers v23.5.26+incompatible // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/mux v1.8.0 // indirect + github.com/grafana/regexp v0.0.0-20221123153739-15dc172cd2db // indirect + github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect + github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect + github.com/hashicorp/go-hclog v1.6.2 // indirect + github.com/hashicorp/go-plugin v1.6.0 // indirect + github.com/hashicorp/yamux v0.1.1 // indirect + github.com/invopop/yaml v0.2.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/klauspost/compress v1.17.4 // indirect + github.com/klauspost/cpuid/v2 v2.2.5 // indirect + github.com/magefile/mage v1.15.0 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattetti/filebuffer v1.0.1 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/mattn/go-runewidth v0.0.13 // indirect + github.com/mitchellh/go-testing-interface v1.14.1 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect + github.com/oklog/run v1.1.0 // indirect + github.com/olekukonko/tablewriter v0.0.5 // indirect + github.com/perimeterx/marshmallow v1.1.5 // indirect + github.com/pierrec/lz4/v4 v4.1.18 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/client_model v0.5.0 // indirect + github.com/prometheus/procfs v0.12.0 // indirect + github.com/rivo/uniseg v0.3.4 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/smartystreets/goconvey v1.6.4 // indirect + github.com/unknwon/bra v0.0.0-20200517080246-1e3013ecaff8 // indirect + github.com/unknwon/com v1.0.1 // indirect + github.com/unknwon/log v0.0.0-20150304194804-e617c87089d3 // indirect + github.com/urfave/cli v1.22.14 // indirect + github.com/zeebo/xxh3 v1.0.2 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.49.0 // indirect + go.opentelemetry.io/contrib/propagators/jaeger v1.22.0 // indirect + go.opentelemetry.io/contrib/samplers/jaegerremote v0.18.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0 // indirect + go.opentelemetry.io/otel/metric v1.24.0 // indirect + go.opentelemetry.io/otel/sdk v1.24.0 // indirect + go.opentelemetry.io/proto/otlp v1.1.0 // indirect + go.uber.org/atomic v1.11.0 // indirect + go.uber.org/goleak v1.3.0 // indirect + golang.org/x/mod v0.14.0 // indirect + golang.org/x/net v0.21.0 // indirect + golang.org/x/sys v0.17.0 // indirect + golang.org/x/text v0.14.0 // indirect + golang.org/x/tools v0.17.0 // indirect + golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect + google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240123012728-ef4313101c80 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 // indirect + google.golang.org/grpc v1.62.1 // indirect + google.golang.org/protobuf v1.32.0 // indirect + gopkg.in/fsnotify/fsnotify.v1 v1.4.7 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/pkg/promlib/go.sum b/pkg/promlib/go.sum new file mode 100644 index 0000000000..8f3fa37193 --- /dev/null +++ b/pkg/promlib/go.sum @@ -0,0 +1,129 @@ +github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= +github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9 h1:ez/4by2iGztzR4L0zgAOR8lTQK9VlyBVVd7G4omaOQs= +github.com/apache/arrow/go/v15 v15.0.0 h1:1zZACWf85oEZY5/kd9dsQS7i+2G5zVQcbKTHgslqHNA= +github.com/aws/aws-sdk-go v1.50.8 h1:gY0WoOW+/Wz6XmYSgDH9ge3wnAevYDSQWPxxJvqAkP4= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE= +github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89 h1:aPflPkRFkVwbW6dmcVqfgwp1i+UWGFH6VgR1Jim5Ygc= +github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/dennwc/varint v1.0.0 h1:kGNFFSSw8ToIy3obO/kKr8U9GZYUAxQEVuix4zfDWzE= +github.com/elazarl/goproxy v0.0.0-20230731152917-f99041a5c027 h1:1L0aalTpPz7YlMxETKpmQoWMBkeiuorElZIXoNmgiPE= +github.com/elazarl/goproxy/ext v0.0.0-20220115173737-adb46da277ac h1:9yrT5tmn9Zc0ytWPASlaPwQfQMQYnRf0RSDe1XvHw0Q= +github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/getkin/kin-openapi v0.120.0 h1:MqJcNJFrMDFNc07iwE8iFC5eT2k/NPUFDIpNeiZv8Jg= +github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= +github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-openapi/jsonpointer v0.20.2 h1:mQc3nmndL8ZBzStEo3JYF8wzmeWffDH4VbXz58sAx6Q= +github.com/go-openapi/swag v0.22.9 h1:XX2DssF+mQKM2DHsbgZK74y/zj4mo9I99+89xUmuZCE= +github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/google/flatbuffers v23.5.26+incompatible h1:M9dgRyhJemaM4Sw8+66GHBu8ioaQmyPLg1b8VwK5WJg= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/grafana/grafana-plugin-sdk-go v0.214.0 h1:09AoomxfsMdKmS4bc5tF81f7fvI9HjHckGFAmu/UQls= +github.com/grafana/regexp v0.0.0-20221123153739-15dc172cd2db h1:7aN5cccjIqCLTzedH7MZzRZt5/lsAHch6Z3L2ZGn5FA= +github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340 h1:uGoIog/wiQHI9GAxXO5TJbT0wWKH3O9HhOJW1F9c3fY= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 h1:Wqo399gCIufwto+VfwCSvsnfGpF/w5E9CNxSwbpD6No= +github.com/hashicorp/go-hclog v1.6.2 h1:NOtoftovWkDheyUM/8JW3QMiXyxJK3uHRK7wV04nD2I= +github.com/hashicorp/go-hclog v1.6.2/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-plugin v1.6.0 h1:wgd4KxHJTVGGqWBq4QPB1i5BZNEx9BR8+OFmHDmTk8A= +github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= +github.com/invopop/yaml v0.2.0 h1:7zky/qH+O0DwAyoobXUqvVBwgBFRxKoQ/3FjcVpjTMY= +github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= +github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= +github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mattetti/filebuffer v1.0.1 h1:gG7pyfnSIZCxdoKq+cPa8T0hhYtD9NxCdI4D7PTjRLM= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= +github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= +github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= +github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= +github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= +github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk= +github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= +github.com/prometheus/common v0.46.0 h1:doXzt5ybi1HBKpsZOL0sSkaNHJJqkyfEWZGGqqScV0Y= +github.com/prometheus/common/sigv4 v0.1.0 h1:qoVebwtwwEhS85Czm2dSROY5fTo2PAPEVdDeppTwGX4= +github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= +github.com/prometheus/prometheus v1.8.2-0.20221021121301-51a44e6657c3 h1:etRZv4bJf9YAuyPWbyFufjkijfeoPSmyA5xNcd4DoyI= +github.com/prometheus/prometheus v1.8.2-0.20221021121301-51a44e6657c3/go.mod h1:plwr4+63Q1xL8oIdBDeU854um7Cct0Av8dhP44lutMw= +github.com/rivo/uniseg v0.3.4 h1:3Z3Eu6FGHZWSfNKJTOUiPatWwfc7DzJRU04jFUqJODw= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 h1:Jpy1PXuP99tXNrhbq2BaPz9B+jNAvH1JPQQpG/9GCXY= +github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= +github.com/unknwon/bra v0.0.0-20200517080246-1e3013ecaff8 h1:aVGB3YnaS/JNfOW3tiHIlmNmTDg618va+eT0mVomgyI= +github.com/unknwon/com v1.0.1 h1:3d1LTxD+Lnf3soQiD4Cp/0BRB+Rsa/+RTvz8GMMzIXs= +github.com/unknwon/log v0.0.0-20150304194804-e617c87089d3 h1:4EYQaWAatQokdji3zqZloVIW/Ke1RQjYw2zHULyrHJg= +github.com/urfave/cli v1.22.14 h1:ebbhrRiGK2i4naQJr+1Xj92HXZCrK7MsyTS/ob3HnAk= +github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= +github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.49.0 h1:RtcvQ4iw3w9NBB5yRwgA4sSa82rfId7n4atVpvKx3bY= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= +go.opentelemetry.io/contrib/propagators/jaeger v1.22.0 h1:bAHX+zN/inu+Rbqk51REmC8oXLl+Dw6pp9ldQf/onaY= +go.opentelemetry.io/contrib/samplers/jaegerremote v0.18.0 h1:Q9PrD94WoMolBx44ef5UWWvufpVSME0MiSymXZfedso= +go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 h1:t6wl9SPayj+c7lEIFgm4ooDBZVb01IhLB4InpomhRw8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0 h1:Mw5xcxMwlqoJd97vwPxA8isEaIoxsta9/Q51+TTJLGE= +go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= +go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw= +go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= +go.opentelemetry.io/proto/otlp v1.1.0 h1:2Di21piLrCqJ3U3eXGCTPHE9R8Nh+0uglSnOyxikMeI= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb h1:c0vyKkb6yr3KR7jEfJaOSv4lG7xPkbN6r52aJz1d8a8= +golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ= +golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= +golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= +gonum.org/v1/gonum v0.12.0 h1:xKuo6hzt+gMav00meVPUlXwSdoEJP46BR+wdxQEFK2o= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80 h1:KAeGQVN3M9nD0/bQXnr/ClcEMJ968gUXJQ9pwfSynuQ= +google.golang.org/genproto/googleapis/api v0.0.0-20240123012728-ef4313101c80 h1:Lj5rbfG876hIAYFjqiJnPHfhXbv+nzTWfm04Fg/XSVU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 h1:AjyfHzEPEFp/NpvfN5g+KDla3EMojjhRVZc1i7cj+oM= +google.golang.org/grpc v1.62.1 h1:B4n+nfKzOICUXMgyrNd19h/I9oH0L1pizfk1d4zSgTk= +google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/fsnotify/fsnotify.v1 v1.4.7 h1:XNNYLJHt73EyYiCZi6+xjupS9CpvmiDgjPTAjrBlQbo= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/pkg/tsdb/prometheus/healthcheck.go b/pkg/promlib/healthcheck.go similarity index 92% rename from pkg/tsdb/prometheus/healthcheck.go rename to pkg/promlib/healthcheck.go index 1f5e6324c0..c2e2de4224 100644 --- a/pkg/tsdb/prometheus/healthcheck.go +++ b/pkg/promlib/healthcheck.go @@ -1,4 +1,4 @@ -package prometheus +package promlib import ( "context" @@ -8,16 +8,15 @@ import ( "time" "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana-plugin-sdk-go/backend/log" - "github.com/grafana/grafana/pkg/tsdb/prometheus/models" + "github.com/grafana/grafana/pkg/promlib/models" ) const ( refID = "__healthcheck__" ) -var logger log.Logger = backend.NewLoggerWith("logger", "tsdb.prometheus") +var logger = backend.NewLoggerWith("logger", "tsdb.prometheus") func (s *Service) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { diff --git a/pkg/tsdb/prometheus/healthcheck_test.go b/pkg/promlib/healthcheck_test.go similarity index 78% rename from pkg/tsdb/prometheus/healthcheck_test.go rename to pkg/promlib/healthcheck_test.go index b50c6c649f..20e06edd6b 100644 --- a/pkg/tsdb/prometheus/healthcheck_test.go +++ b/pkg/promlib/healthcheck_test.go @@ -1,4 +1,4 @@ -package prometheus +package promlib import ( "context" @@ -10,17 +10,18 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/datasource" - "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" + sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" "github.com/stretchr/testify/assert" ) type healthCheckProvider[T http.RoundTripper] struct { - httpclient.Provider + sdkhttpclient.Provider RoundTripper *T } type healthCheckSuccessRoundTripper struct { } + type healthCheckFailRoundTripper struct { } @@ -55,34 +56,34 @@ func (rt *healthCheckFailRoundTripper) RoundTrip(req *http.Request) (*http.Respo }, nil } -func (provider *healthCheckProvider[T]) New(opts ...httpclient.Options) (*http.Client, error) { +func (provider *healthCheckProvider[T]) New(opts ...sdkhttpclient.Options) (*http.Client, error) { client := &http.Client{} provider.RoundTripper = new(T) client.Transport = *provider.RoundTripper return client, nil } -func (provider *healthCheckProvider[T]) GetTransport(opts ...httpclient.Options) (http.RoundTripper, error) { +func (provider *healthCheckProvider[T]) GetTransport(opts ...sdkhttpclient.Options) (http.RoundTripper, error) { return *new(T), nil } -func getMockProvider[T http.RoundTripper]() *httpclient.Provider { +func getMockProvider[T http.RoundTripper]() *sdkhttpclient.Provider { p := &healthCheckProvider[T]{ RoundTripper: new(T), } - anotherFN := func(o httpclient.Options, next http.RoundTripper) http.RoundTripper { + anotherFN := func(o sdkhttpclient.Options, next http.RoundTripper) http.RoundTripper { return *p.RoundTripper } - fn := httpclient.MiddlewareFunc(anotherFN) - mid := httpclient.NamedMiddlewareFunc("mock", fn) - return httpclient.NewProvider(httpclient.ProviderOptions{Middlewares: []httpclient.Middleware{mid}}) + fn := sdkhttpclient.MiddlewareFunc(anotherFN) + mid := sdkhttpclient.NamedMiddlewareFunc("mock", fn) + return sdkhttpclient.NewProvider(sdkhttpclient.ProviderOptions{Middlewares: []sdkhttpclient.Middleware{mid}}) } func Test_healthcheck(t *testing.T) { t.Run("should do a successful health check", func(t *testing.T) { httpProvider := getMockProvider[*healthCheckSuccessRoundTripper]() s := &Service{ - im: datasource.NewInstanceManager(newInstanceSettings(httpProvider, backend.NewLoggerWith("logger", "test"))), + im: datasource.NewInstanceManager(newInstanceSettings(httpProvider, backend.NewLoggerWith("logger", "test"), mockExtendClientOpts)), } req := &backend.CheckHealthRequest{ @@ -98,7 +99,7 @@ func Test_healthcheck(t *testing.T) { t.Run("should return an error for an unsuccessful health check", func(t *testing.T) { httpProvider := getMockProvider[*healthCheckFailRoundTripper]() s := &Service{ - im: datasource.NewInstanceManager(newInstanceSettings(httpProvider, backend.NewLoggerWith("logger", "test"))), + im: datasource.NewInstanceManager(newInstanceSettings(httpProvider, backend.NewLoggerWith("logger", "test"), mockExtendClientOpts)), } req := &backend.CheckHealthRequest{ diff --git a/pkg/tsdb/prometheus/heuristics.go b/pkg/promlib/heuristics.go similarity index 99% rename from pkg/tsdb/prometheus/heuristics.go rename to pkg/promlib/heuristics.go index 78e38c027e..c0c3f1e318 100644 --- a/pkg/tsdb/prometheus/heuristics.go +++ b/pkg/promlib/heuristics.go @@ -1,4 +1,4 @@ -package prometheus +package promlib import ( "context" diff --git a/pkg/tsdb/prometheus/heuristics_test.go b/pkg/promlib/heuristics_test.go similarity index 74% rename from pkg/tsdb/prometheus/heuristics_test.go rename to pkg/promlib/heuristics_test.go index 58a9befc68..3ed20a6555 100644 --- a/pkg/tsdb/prometheus/heuristics_test.go +++ b/pkg/promlib/heuristics_test.go @@ -1,4 +1,4 @@ -package prometheus +package promlib import ( "context" @@ -13,7 +13,7 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/datasource" - "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" + sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" ) type heuristicsSuccessRoundTripper struct { @@ -32,13 +32,17 @@ func (rt *heuristicsSuccessRoundTripper) RoundTrip(req *http.Request) (*http.Res }, nil } -func newHeuristicsSDKProvider(hrt heuristicsSuccessRoundTripper) *httpclient.Provider { - anotherFN := func(o httpclient.Options, next http.RoundTripper) http.RoundTripper { +func newHeuristicsSDKProvider(hrt heuristicsSuccessRoundTripper) *sdkhttpclient.Provider { + anotherFN := func(o sdkhttpclient.Options, next http.RoundTripper) http.RoundTripper { return &hrt } - fn := httpclient.MiddlewareFunc(anotherFN) - mid := httpclient.NamedMiddlewareFunc("mock", fn) - return httpclient.NewProvider(httpclient.ProviderOptions{Middlewares: []httpclient.Middleware{mid}}) + fn := sdkhttpclient.MiddlewareFunc(anotherFN) + mid := sdkhttpclient.NamedMiddlewareFunc("mock", fn) + return sdkhttpclient.NewProvider(sdkhttpclient.ProviderOptions{Middlewares: []sdkhttpclient.Middleware{mid}}) +} + +func mockExtendClientOpts(ctx context.Context, settings backend.DataSourceInstanceSettings, clientOpts *sdkhttpclient.Options) error { + return nil } func Test_GetHeuristics(t *testing.T) { @@ -47,10 +51,9 @@ func Test_GetHeuristics(t *testing.T) { res: io.NopCloser(strings.NewReader("{\"status\":\"success\",\"data\":{\"version\":\"1.0\"}}")), status: http.StatusOK, } - //httpProvider := getHeuristicsMockProvider(&rt) httpProvider := newHeuristicsSDKProvider(rt) s := &Service{ - im: datasource.NewInstanceManager(newInstanceSettings(httpProvider, backend.NewLoggerWith("logger", "test"))), + im: datasource.NewInstanceManager(newInstanceSettings(httpProvider, backend.NewLoggerWith("logger", "test"), mockExtendClientOpts)), } req := HeuristicsRequest{ @@ -70,7 +73,7 @@ func Test_GetHeuristics(t *testing.T) { } httpProvider := newHeuristicsSDKProvider(rt) s := &Service{ - im: datasource.NewInstanceManager(newInstanceSettings(httpProvider, backend.NewLoggerWith("logger", "test"))), + im: datasource.NewInstanceManager(newInstanceSettings(httpProvider, backend.NewLoggerWith("logger", "test"), mockExtendClientOpts)), } req := HeuristicsRequest{ diff --git a/pkg/tsdb/prometheus/instrumentation/instrumentation.go b/pkg/promlib/instrumentation/instrumentation.go similarity index 100% rename from pkg/tsdb/prometheus/instrumentation/instrumentation.go rename to pkg/promlib/instrumentation/instrumentation.go diff --git a/pkg/tsdb/prometheus/instrumentation/instrumentation_test.go b/pkg/promlib/instrumentation/instrumentation_test.go similarity index 100% rename from pkg/tsdb/prometheus/instrumentation/instrumentation_test.go rename to pkg/promlib/instrumentation/instrumentation_test.go diff --git a/pkg/tsdb/prometheus/intervalv2/intervalv2.go b/pkg/promlib/intervalv2/intervalv2.go similarity index 100% rename from pkg/tsdb/prometheus/intervalv2/intervalv2.go rename to pkg/promlib/intervalv2/intervalv2.go diff --git a/pkg/tsdb/prometheus/intervalv2/intervalv2_test.go b/pkg/promlib/intervalv2/intervalv2_test.go similarity index 100% rename from pkg/tsdb/prometheus/intervalv2/intervalv2_test.go rename to pkg/promlib/intervalv2/intervalv2_test.go diff --git a/pkg/promlib/library.go b/pkg/promlib/library.go new file mode 100644 index 0000000000..c8316dbe75 --- /dev/null +++ b/pkg/promlib/library.go @@ -0,0 +1,154 @@ +package promlib + +import ( + "context" + "errors" + "fmt" + "strings" + "time" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/backend/datasource" + sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" + "github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt" + "github.com/grafana/grafana-plugin-sdk-go/backend/log" + "github.com/patrickmn/go-cache" + apiv1 "github.com/prometheus/client_golang/api/prometheus/v1" + + "github.com/grafana/grafana/pkg/promlib/client" + "github.com/grafana/grafana/pkg/promlib/instrumentation" + "github.com/grafana/grafana/pkg/promlib/querydata" + "github.com/grafana/grafana/pkg/promlib/resource" +) + +type Service struct { + im instancemgmt.InstanceManager + logger log.Logger +} + +type instance struct { + queryData *querydata.QueryData + resource *resource.Resource + versionCache *cache.Cache +} + +type ExtendOptions func(ctx context.Context, settings backend.DataSourceInstanceSettings, clientOpts *sdkhttpclient.Options) error + +func NewService(httpClientProvider *sdkhttpclient.Provider, plog log.Logger, extendOptions ExtendOptions) *Service { + if httpClientProvider == nil { + httpClientProvider = sdkhttpclient.NewProvider() + } + return &Service{ + im: datasource.NewInstanceManager(newInstanceSettings(httpClientProvider, plog, extendOptions)), + logger: plog, + } +} + +func newInstanceSettings(httpClientProvider *sdkhttpclient.Provider, log log.Logger, extendOptions ExtendOptions) datasource.InstanceFactoryFunc { + return func(ctx context.Context, settings backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { + // Creates a http roundTripper. + opts, err := client.CreateTransportOptions(ctx, settings, log) + if err != nil { + return nil, fmt.Errorf("error creating transport options: %v", err) + } + + err = extendOptions(ctx, settings, opts) + if err != nil { + return nil, fmt.Errorf("error extending transport options: %v", err) + } + + httpClient, err := httpClientProvider.New(*opts) + if err != nil { + return nil, fmt.Errorf("error creating http client: %v", err) + } + + // New version using custom client and better response parsing + qd, err := querydata.New(httpClient, settings, log) + if err != nil { + return nil, err + } + + // Resource call management using new custom client same as querydata + r, err := resource.New(httpClient, settings, log) + if err != nil { + return nil, err + } + + return instance{ + queryData: qd, + resource: r, + versionCache: cache.New(time.Minute*1, time.Minute*5), + }, nil + } +} + +func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { + if len(req.Queries) == 0 { + err := fmt.Errorf("query contains no queries") + instrumentation.UpdateQueryDataMetrics(err, nil) + return &backend.QueryDataResponse{}, err + } + + i, err := s.getInstance(ctx, req.PluginContext) + if err != nil { + instrumentation.UpdateQueryDataMetrics(err, nil) + return nil, err + } + + qd, err := i.queryData.Execute(ctx, req) + instrumentation.UpdateQueryDataMetrics(err, qd) + + return qd, err +} + +func (s *Service) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { + i, err := s.getInstance(ctx, req.PluginContext) + if err != nil { + return err + } + + if strings.EqualFold(req.Path, "version-detect") { + versionObj, found := i.versionCache.Get("version") + if found { + return sender.Send(versionObj.(*backend.CallResourceResponse)) + } + + vResp, err := i.resource.DetectVersion(ctx, req) + if err != nil { + return err + } + i.versionCache.Set("version", vResp, cache.DefaultExpiration) + return sender.Send(vResp) + } + + resp, err := i.resource.Execute(ctx, req) + if err != nil { + return err + } + + return sender.Send(resp) +} + +func (s *Service) getInstance(ctx context.Context, pluginCtx backend.PluginContext) (*instance, error) { + i, err := s.im.Get(ctx, pluginCtx) + if err != nil { + return nil, err + } + in := i.(instance) + return &in, nil +} + +// IsAPIError returns whether err is or wraps a Prometheus error. +func IsAPIError(err error) bool { + // Check if the right error type is in err's chain. + var e *apiv1.Error + return errors.As(err, &e) +} + +func ConvertAPIError(err error) error { + var e *apiv1.Error + if errors.As(err, &e) { + return fmt.Errorf("%s: %s", e.Msg, e.Detail) + } + return err +} diff --git a/pkg/promlib/library_test.go b/pkg/promlib/library_test.go new file mode 100644 index 0000000000..cf789c43f6 --- /dev/null +++ b/pkg/promlib/library_test.go @@ -0,0 +1,118 @@ +package promlib + +import ( + "context" + "io" + "net/http" + "testing" + "time" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" + "github.com/stretchr/testify/require" +) + +type fakeSender struct{} + +func (sender *fakeSender) Send(resp *backend.CallResourceResponse) error { + return nil +} + +type fakeRoundtripper struct { + Req *http.Request +} + +func (rt *fakeRoundtripper) RoundTrip(req *http.Request) (*http.Response, error) { + rt.Req = req + return &http.Response{ + Status: "200", + StatusCode: 200, + Header: nil, + Body: nil, + ContentLength: 0, + }, nil +} + +type fakeHTTPClientProvider struct { + sdkhttpclient.Provider + Roundtripper *fakeRoundtripper +} + +func (provider *fakeHTTPClientProvider) New(opts ...sdkhttpclient.Options) (*http.Client, error) { + client := &http.Client{} + provider.Roundtripper = &fakeRoundtripper{} + client.Transport = provider.Roundtripper + return client, nil +} + +func (provider *fakeHTTPClientProvider) GetTransport(opts ...sdkhttpclient.Options) (http.RoundTripper, error) { + return &fakeRoundtripper{}, nil +} + +func getMockPromTestSDKProvider(f *fakeHTTPClientProvider) *sdkhttpclient.Provider { + anotherFN := func(o sdkhttpclient.Options, next http.RoundTripper) http.RoundTripper { + _, _ = f.New() + return f.Roundtripper + } + fn := sdkhttpclient.MiddlewareFunc(anotherFN) + mid := sdkhttpclient.NamedMiddlewareFunc("mock", fn) + return sdkhttpclient.NewProvider(sdkhttpclient.ProviderOptions{Middlewares: []sdkhttpclient.Middleware{mid}}) +} + +func mockExtendTransportOptions(ctx context.Context, settings backend.DataSourceInstanceSettings, clientOpts *sdkhttpclient.Options) error { + return nil +} + +func TestService(t *testing.T) { + t.Run("Service", func(t *testing.T) { + t.Run("CallResource", func(t *testing.T) { + t.Run("creates correct request", func(t *testing.T) { + f := &fakeHTTPClientProvider{} + httpProvider := getMockPromTestSDKProvider(f) + service := NewService(httpProvider, backend.NewLoggerWith("logger", "test"), mockExtendTransportOptions) + + req := &backend.CallResourceRequest{ + PluginContext: backend.PluginContext{ + OrgID: 0, + PluginID: "prometheus", + User: nil, + AppInstanceSettings: nil, + DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{ + ID: 0, + UID: "", + Type: "prometheus", + Name: "test-prom", + URL: "http://localhost:9090", + User: "", + Database: "", + BasicAuthEnabled: true, + BasicAuthUser: "admin", + Updated: time.Time{}, + JSONData: []byte("{}"), + }, + }, + Path: "/api/v1/series", + Method: http.MethodPost, + URL: "/api/v1/series", + Body: []byte("match%5B%5D: ALERTS\nstart: 1655271408\nend: 1655293008"), + } + + sender := &fakeSender{} + err := service.CallResource(context.Background(), req, sender) + require.NoError(t, err) + require.Equal( + t, + http.Header{ + "Content-Type": {"application/x-www-form-urlencoded"}, + "Idempotency-Key": []string(nil), + }, + f.Roundtripper.Req.Header) + require.Equal(t, http.MethodPost, f.Roundtripper.Req.Method) + body, err := io.ReadAll(f.Roundtripper.Req.Body) + require.NoError(t, err) + require.Equal(t, []byte("match%5B%5D: ALERTS\nstart: 1655271408\nend: 1655293008"), body) + require.Equal(t, "http://localhost:9090/api/v1/series", f.Roundtripper.Req.URL.String()) + }) + }) + }) +} diff --git a/pkg/tsdb/prometheus/middleware/custom_query_params.go b/pkg/promlib/middleware/custom_query_params.go similarity index 100% rename from pkg/tsdb/prometheus/middleware/custom_query_params.go rename to pkg/promlib/middleware/custom_query_params.go diff --git a/pkg/tsdb/prometheus/middleware/custom_query_params_test.go b/pkg/promlib/middleware/custom_query_params_test.go similarity index 100% rename from pkg/tsdb/prometheus/middleware/custom_query_params_test.go rename to pkg/promlib/middleware/custom_query_params_test.go diff --git a/pkg/tsdb/prometheus/middleware/force_http_get.go b/pkg/promlib/middleware/force_http_get.go similarity index 100% rename from pkg/tsdb/prometheus/middleware/force_http_get.go rename to pkg/promlib/middleware/force_http_get.go diff --git a/pkg/tsdb/prometheus/middleware/force_http_get_test.go b/pkg/promlib/middleware/force_http_get_test.go similarity index 100% rename from pkg/tsdb/prometheus/middleware/force_http_get_test.go rename to pkg/promlib/middleware/force_http_get_test.go diff --git a/pkg/tsdb/prometheus/models/query.go b/pkg/promlib/models/query.go similarity index 99% rename from pkg/tsdb/prometheus/models/query.go rename to pkg/promlib/models/query.go index b510b769e0..3c9a96bab9 100644 --- a/pkg/tsdb/prometheus/models/query.go +++ b/pkg/promlib/models/query.go @@ -13,7 +13,7 @@ import ( "github.com/prometheus/prometheus/model/labels" "github.com/prometheus/prometheus/promql/parser" - "github.com/grafana/grafana/pkg/tsdb/prometheus/intervalv2" + "github.com/grafana/grafana/pkg/promlib/intervalv2" ) // PromQueryFormat defines model for PromQueryFormat. diff --git a/pkg/tsdb/prometheus/models/query_test.go b/pkg/promlib/models/query_test.go similarity index 99% rename from pkg/tsdb/prometheus/models/query_test.go rename to pkg/promlib/models/query_test.go index 2a9ffab107..085352f3f1 100644 --- a/pkg/tsdb/prometheus/models/query_test.go +++ b/pkg/promlib/models/query_test.go @@ -9,8 +9,8 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/stretchr/testify/require" - "github.com/grafana/grafana/pkg/tsdb/prometheus/intervalv2" - "github.com/grafana/grafana/pkg/tsdb/prometheus/models" + "github.com/grafana/grafana/pkg/promlib/intervalv2" + "github.com/grafana/grafana/pkg/promlib/models" ) var ( diff --git a/pkg/tsdb/prometheus/models/result.go b/pkg/promlib/models/result.go similarity index 100% rename from pkg/tsdb/prometheus/models/result.go rename to pkg/promlib/models/result.go diff --git a/pkg/tsdb/prometheus/models/scope.go b/pkg/promlib/models/scope.go similarity index 100% rename from pkg/tsdb/prometheus/models/scope.go rename to pkg/promlib/models/scope.go diff --git a/pkg/tsdb/prometheus/querydata/exemplar/framer.go b/pkg/promlib/querydata/exemplar/framer.go similarity index 100% rename from pkg/tsdb/prometheus/querydata/exemplar/framer.go rename to pkg/promlib/querydata/exemplar/framer.go diff --git a/pkg/tsdb/prometheus/querydata/exemplar/labels.go b/pkg/promlib/querydata/exemplar/labels.go similarity index 100% rename from pkg/tsdb/prometheus/querydata/exemplar/labels.go rename to pkg/promlib/querydata/exemplar/labels.go diff --git a/pkg/tsdb/prometheus/querydata/exemplar/sampler.go b/pkg/promlib/querydata/exemplar/sampler.go similarity index 93% rename from pkg/tsdb/prometheus/querydata/exemplar/sampler.go rename to pkg/promlib/querydata/exemplar/sampler.go index be7f518cb8..f0b3e0eed3 100644 --- a/pkg/tsdb/prometheus/querydata/exemplar/sampler.go +++ b/pkg/promlib/querydata/exemplar/sampler.go @@ -4,7 +4,7 @@ import ( "sort" "time" - "github.com/grafana/grafana/pkg/tsdb/prometheus/models" + "github.com/grafana/grafana/pkg/promlib/models" ) type Sampler interface { diff --git a/pkg/tsdb/prometheus/querydata/exemplar/sampler_stddev.go b/pkg/promlib/querydata/exemplar/sampler_stddev.go similarity index 97% rename from pkg/tsdb/prometheus/querydata/exemplar/sampler_stddev.go rename to pkg/promlib/querydata/exemplar/sampler_stddev.go index 9028e22cff..92ebf0ef51 100644 --- a/pkg/tsdb/prometheus/querydata/exemplar/sampler_stddev.go +++ b/pkg/promlib/querydata/exemplar/sampler_stddev.go @@ -5,7 +5,7 @@ import ( "sort" "time" - "github.com/grafana/grafana/pkg/tsdb/prometheus/models" + "github.com/grafana/grafana/pkg/promlib/models" ) type StandardDeviationSampler struct { diff --git a/pkg/tsdb/prometheus/querydata/exemplar/sampler_stddev_test.go b/pkg/promlib/querydata/exemplar/sampler_stddev_test.go similarity index 84% rename from pkg/tsdb/prometheus/querydata/exemplar/sampler_stddev_test.go rename to pkg/promlib/querydata/exemplar/sampler_stddev_test.go index b96f6d63b2..c7a4ff0aa7 100644 --- a/pkg/tsdb/prometheus/querydata/exemplar/sampler_stddev_test.go +++ b/pkg/promlib/querydata/exemplar/sampler_stddev_test.go @@ -6,8 +6,8 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/experimental" - "github.com/grafana/grafana/pkg/tsdb/prometheus/models" - "github.com/grafana/grafana/pkg/tsdb/prometheus/querydata/exemplar" + "github.com/grafana/grafana/pkg/promlib/models" + "github.com/grafana/grafana/pkg/promlib/querydata/exemplar" ) func TestStdDevSampler(t *testing.T) { diff --git a/pkg/tsdb/prometheus/querydata/exemplar/sampler_test.go b/pkg/promlib/querydata/exemplar/sampler_test.go similarity index 88% rename from pkg/tsdb/prometheus/querydata/exemplar/sampler_test.go rename to pkg/promlib/querydata/exemplar/sampler_test.go index f071f1b902..26cdc5c2a8 100644 --- a/pkg/tsdb/prometheus/querydata/exemplar/sampler_test.go +++ b/pkg/promlib/querydata/exemplar/sampler_test.go @@ -6,8 +6,8 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/experimental" - "github.com/grafana/grafana/pkg/tsdb/prometheus/models" - "github.com/grafana/grafana/pkg/tsdb/prometheus/querydata/exemplar" + "github.com/grafana/grafana/pkg/promlib/models" + "github.com/grafana/grafana/pkg/promlib/querydata/exemplar" ) const update = true diff --git a/pkg/tsdb/prometheus/querydata/exemplar/testdata/noop_sampler.jsonc b/pkg/promlib/querydata/exemplar/testdata/noop_sampler.jsonc similarity index 100% rename from pkg/tsdb/prometheus/querydata/exemplar/testdata/noop_sampler.jsonc rename to pkg/promlib/querydata/exemplar/testdata/noop_sampler.jsonc diff --git a/pkg/tsdb/prometheus/querydata/exemplar/testdata/stddev_sampler.jsonc b/pkg/promlib/querydata/exemplar/testdata/stddev_sampler.jsonc similarity index 100% rename from pkg/tsdb/prometheus/querydata/exemplar/testdata/stddev_sampler.jsonc rename to pkg/promlib/querydata/exemplar/testdata/stddev_sampler.jsonc diff --git a/pkg/tsdb/prometheus/querydata/framing_bench_test.go b/pkg/promlib/querydata/framing_bench_test.go similarity index 93% rename from pkg/tsdb/prometheus/querydata/framing_bench_test.go rename to pkg/promlib/querydata/framing_bench_test.go index 2b4cd28545..b769df2b47 100644 --- a/pkg/tsdb/prometheus/querydata/framing_bench_test.go +++ b/pkg/promlib/querydata/framing_bench_test.go @@ -17,11 +17,11 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/stretchr/testify/require" - "github.com/grafana/grafana/pkg/tsdb/prometheus/models" + "github.com/grafana/grafana/pkg/promlib/models" ) // when memory-profiling this benchmark, these commands are recommended: -// - go test -benchmem -run=^$ -bench ^BenchmarkExemplarJson$ github.com/grafana/grafana/pkg/tsdb/prometheus/querydata -memprofile memprofile.out -count 6 | tee old.txt +// - go test -benchmem -run=^$ -bench ^BenchmarkExemplarJson$ github.com/grafana/grafana/pkg/promlib/querydata -memprofile memprofile.out -count 6 | tee old.txt // - go tool pprof -http=localhost:6061 memprofile.out func BenchmarkExemplarJson(b *testing.B) { queryFileName := filepath.Join("../testdata", "exemplar.query.json") @@ -55,7 +55,7 @@ func BenchmarkExemplarJson(b *testing.B) { var resp *backend.QueryDataResponse // when memory-profiling this benchmark, these commands are recommended: -// - go test -benchmem -run=^$ -bench ^BenchmarkRangeJson$ github.com/grafana/grafana/pkg/tsdb/prometheus/querydata -memprofile memprofile.out -count 6 | tee old.txt +// - go test -benchmem -run=^$ -bench ^BenchmarkRangeJson$ github.com/grafana/grafana/pkg/promlib/querydata -memprofile memprofile.out -count 6 | tee old.txt // - go tool pprof -http=localhost:6061 memprofile.out // - benchstat old.txt new.txt func BenchmarkRangeJson(b *testing.B) { diff --git a/pkg/tsdb/prometheus/querydata/framing_test.go b/pkg/promlib/querydata/framing_test.go similarity index 98% rename from pkg/tsdb/prometheus/querydata/framing_test.go rename to pkg/promlib/querydata/framing_test.go index 7138ba19e9..0d194f189f 100644 --- a/pkg/tsdb/prometheus/querydata/framing_test.go +++ b/pkg/promlib/querydata/framing_test.go @@ -16,7 +16,7 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/experimental" "github.com/stretchr/testify/require" - "github.com/grafana/grafana/pkg/tsdb/prometheus/models" + "github.com/grafana/grafana/pkg/promlib/models" ) var update = true diff --git a/pkg/tsdb/prometheus/querydata/request.go b/pkg/promlib/querydata/request.go similarity index 94% rename from pkg/tsdb/prometheus/querydata/request.go rename to pkg/promlib/querydata/request.go index c80952b1a0..aabd347b9a 100644 --- a/pkg/tsdb/prometheus/querydata/request.go +++ b/pkg/promlib/querydata/request.go @@ -7,20 +7,20 @@ import ( "regexp" "time" - "github.com/grafana/grafana-azure-sdk-go/util/maputil" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana-plugin-sdk-go/data/utils/maputil" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" "github.com/grafana/grafana-plugin-sdk-go/backend/log" "github.com/grafana/grafana-plugin-sdk-go/backend/tracing" - "github.com/grafana/grafana/pkg/tsdb/prometheus/client" - "github.com/grafana/grafana/pkg/tsdb/prometheus/intervalv2" - "github.com/grafana/grafana/pkg/tsdb/prometheus/models" - "github.com/grafana/grafana/pkg/tsdb/prometheus/querydata/exemplar" - "github.com/grafana/grafana/pkg/tsdb/prometheus/utils" + "github.com/grafana/grafana/pkg/promlib/client" + "github.com/grafana/grafana/pkg/promlib/intervalv2" + "github.com/grafana/grafana/pkg/promlib/models" + "github.com/grafana/grafana/pkg/promlib/querydata/exemplar" + "github.com/grafana/grafana/pkg/promlib/utils" ) const legendFormatAuto = "__auto" diff --git a/pkg/tsdb/prometheus/querydata/request_test.go b/pkg/promlib/querydata/request_test.go similarity index 98% rename from pkg/tsdb/prometheus/querydata/request_test.go rename to pkg/promlib/querydata/request_test.go index cd3ba0f867..3a1ac8986f 100644 --- a/pkg/tsdb/prometheus/querydata/request_test.go +++ b/pkg/promlib/querydata/request_test.go @@ -20,9 +20,9 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend/log" "github.com/grafana/grafana-plugin-sdk-go/data" - "github.com/grafana/grafana/pkg/tsdb/prometheus/client" - "github.com/grafana/grafana/pkg/tsdb/prometheus/models" - "github.com/grafana/grafana/pkg/tsdb/prometheus/querydata" + "github.com/grafana/grafana/pkg/promlib/client" + "github.com/grafana/grafana/pkg/promlib/models" + "github.com/grafana/grafana/pkg/promlib/querydata" ) func TestPrometheus_parseTimeSeriesResponse(t *testing.T) { diff --git a/pkg/tsdb/prometheus/querydata/response.go b/pkg/promlib/querydata/response.go similarity index 95% rename from pkg/tsdb/prometheus/querydata/response.go rename to pkg/promlib/querydata/response.go index 70602c0158..7181400df1 100644 --- a/pkg/tsdb/prometheus/querydata/response.go +++ b/pkg/promlib/querydata/response.go @@ -12,10 +12,10 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/data" jsoniter "github.com/json-iterator/go" - "github.com/grafana/grafana/pkg/tsdb/prometheus/converter" - "github.com/grafana/grafana/pkg/tsdb/prometheus/models" - "github.com/grafana/grafana/pkg/tsdb/prometheus/querydata/exemplar" - "github.com/grafana/grafana/pkg/tsdb/prometheus/utils" + "github.com/grafana/grafana/pkg/promlib/converter" + "github.com/grafana/grafana/pkg/promlib/models" + "github.com/grafana/grafana/pkg/promlib/querydata/exemplar" + "github.com/grafana/grafana/pkg/promlib/utils" ) func (s *QueryData) parseResponse(ctx context.Context, q *models.Query, res *http.Response, enablePrometheusDataplaneFlag bool) backend.DataResponse { diff --git a/pkg/tsdb/prometheus/querydata/response_test.go b/pkg/promlib/querydata/response_test.go similarity index 96% rename from pkg/tsdb/prometheus/querydata/response_test.go rename to pkg/promlib/querydata/response_test.go index 62d27222d8..8cec7ba460 100644 --- a/pkg/tsdb/prometheus/querydata/response_test.go +++ b/pkg/promlib/querydata/response_test.go @@ -9,8 +9,8 @@ import ( "github.com/stretchr/testify/assert" - "github.com/grafana/grafana/pkg/tsdb/prometheus/models" - "github.com/grafana/grafana/pkg/tsdb/prometheus/querydata/exemplar" + "github.com/grafana/grafana/pkg/promlib/models" + "github.com/grafana/grafana/pkg/promlib/querydata/exemplar" ) func TestQueryData_parseResponse(t *testing.T) { diff --git a/pkg/tsdb/prometheus/resource/resource.go b/pkg/promlib/resource/resource.go similarity index 92% rename from pkg/tsdb/prometheus/resource/resource.go rename to pkg/promlib/resource/resource.go index aeda2303ea..888267d413 100644 --- a/pkg/tsdb/prometheus/resource/resource.go +++ b/pkg/promlib/resource/resource.go @@ -6,12 +6,12 @@ import ( "fmt" "net/http" - "github.com/grafana/grafana-azure-sdk-go/util/maputil" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/log" + "github.com/grafana/grafana-plugin-sdk-go/data/utils/maputil" - "github.com/grafana/grafana/pkg/tsdb/prometheus/client" - "github.com/grafana/grafana/pkg/tsdb/prometheus/utils" + "github.com/grafana/grafana/pkg/promlib/client" + "github.com/grafana/grafana/pkg/promlib/utils" ) type Resource struct { diff --git a/pkg/tsdb/prometheus/testdata/exemplar.query.json b/pkg/promlib/testdata/exemplar.query.json similarity index 100% rename from pkg/tsdb/prometheus/testdata/exemplar.query.json rename to pkg/promlib/testdata/exemplar.query.json diff --git a/pkg/tsdb/prometheus/testdata/exemplar.result.golden.jsonc b/pkg/promlib/testdata/exemplar.result.golden.jsonc similarity index 100% rename from pkg/tsdb/prometheus/testdata/exemplar.result.golden.jsonc rename to pkg/promlib/testdata/exemplar.result.golden.jsonc diff --git a/pkg/tsdb/prometheus/testdata/exemplar.result.json b/pkg/promlib/testdata/exemplar.result.json similarity index 100% rename from pkg/tsdb/prometheus/testdata/exemplar.result.json rename to pkg/promlib/testdata/exemplar.result.json diff --git a/pkg/tsdb/prometheus/testdata/range_auto.query.json b/pkg/promlib/testdata/range_auto.query.json similarity index 100% rename from pkg/tsdb/prometheus/testdata/range_auto.query.json rename to pkg/promlib/testdata/range_auto.query.json diff --git a/pkg/tsdb/prometheus/testdata/range_auto.result.golden.jsonc b/pkg/promlib/testdata/range_auto.result.golden.jsonc similarity index 100% rename from pkg/tsdb/prometheus/testdata/range_auto.result.golden.jsonc rename to pkg/promlib/testdata/range_auto.result.golden.jsonc diff --git a/pkg/tsdb/prometheus/testdata/range_auto.result.json b/pkg/promlib/testdata/range_auto.result.json similarity index 100% rename from pkg/tsdb/prometheus/testdata/range_auto.result.json rename to pkg/promlib/testdata/range_auto.result.json diff --git a/pkg/tsdb/prometheus/testdata/range_infinity.query.json b/pkg/promlib/testdata/range_infinity.query.json similarity index 100% rename from pkg/tsdb/prometheus/testdata/range_infinity.query.json rename to pkg/promlib/testdata/range_infinity.query.json diff --git a/pkg/tsdb/prometheus/testdata/range_infinity.result.golden.jsonc b/pkg/promlib/testdata/range_infinity.result.golden.jsonc similarity index 100% rename from pkg/tsdb/prometheus/testdata/range_infinity.result.golden.jsonc rename to pkg/promlib/testdata/range_infinity.result.golden.jsonc diff --git a/pkg/tsdb/prometheus/testdata/range_infinity.result.json b/pkg/promlib/testdata/range_infinity.result.json similarity index 100% rename from pkg/tsdb/prometheus/testdata/range_infinity.result.json rename to pkg/promlib/testdata/range_infinity.result.json diff --git a/pkg/tsdb/prometheus/testdata/range_missing.query.json b/pkg/promlib/testdata/range_missing.query.json similarity index 100% rename from pkg/tsdb/prometheus/testdata/range_missing.query.json rename to pkg/promlib/testdata/range_missing.query.json diff --git a/pkg/tsdb/prometheus/testdata/range_missing.result.golden.jsonc b/pkg/promlib/testdata/range_missing.result.golden.jsonc similarity index 100% rename from pkg/tsdb/prometheus/testdata/range_missing.result.golden.jsonc rename to pkg/promlib/testdata/range_missing.result.golden.jsonc diff --git a/pkg/tsdb/prometheus/testdata/range_missing.result.json b/pkg/promlib/testdata/range_missing.result.json similarity index 100% rename from pkg/tsdb/prometheus/testdata/range_missing.result.json rename to pkg/promlib/testdata/range_missing.result.json diff --git a/pkg/tsdb/prometheus/testdata/range_nan.query.json b/pkg/promlib/testdata/range_nan.query.json similarity index 100% rename from pkg/tsdb/prometheus/testdata/range_nan.query.json rename to pkg/promlib/testdata/range_nan.query.json diff --git a/pkg/tsdb/prometheus/testdata/range_nan.result.golden.jsonc b/pkg/promlib/testdata/range_nan.result.golden.jsonc similarity index 100% rename from pkg/tsdb/prometheus/testdata/range_nan.result.golden.jsonc rename to pkg/promlib/testdata/range_nan.result.golden.jsonc diff --git a/pkg/tsdb/prometheus/testdata/range_nan.result.json b/pkg/promlib/testdata/range_nan.result.json similarity index 100% rename from pkg/tsdb/prometheus/testdata/range_nan.result.json rename to pkg/promlib/testdata/range_nan.result.json diff --git a/pkg/tsdb/prometheus/testdata/range_simple.query.json b/pkg/promlib/testdata/range_simple.query.json similarity index 100% rename from pkg/tsdb/prometheus/testdata/range_simple.query.json rename to pkg/promlib/testdata/range_simple.query.json diff --git a/pkg/tsdb/prometheus/testdata/range_simple.result.golden.jsonc b/pkg/promlib/testdata/range_simple.result.golden.jsonc similarity index 100% rename from pkg/tsdb/prometheus/testdata/range_simple.result.golden.jsonc rename to pkg/promlib/testdata/range_simple.result.golden.jsonc diff --git a/pkg/tsdb/prometheus/testdata/range_simple.result.json b/pkg/promlib/testdata/range_simple.result.json similarity index 100% rename from pkg/tsdb/prometheus/testdata/range_simple.result.json rename to pkg/promlib/testdata/range_simple.result.json diff --git a/pkg/tsdb/prometheus/utils/utils.go b/pkg/promlib/utils/utils.go similarity index 100% rename from pkg/tsdb/prometheus/utils/utils.go rename to pkg/promlib/utils/utils.go diff --git a/pkg/services/alerting/conditions/query.go b/pkg/services/alerting/conditions/query.go index ea090f1403..1eedafa338 100644 --- a/pkg/services/alerting/conditions/query.go +++ b/pkg/services/alerting/conditions/query.go @@ -11,12 +11,12 @@ import ( "github.com/grafana/grafana/pkg/components/null" "github.com/grafana/grafana/pkg/components/simplejson" + prometheus "github.com/grafana/grafana/pkg/promlib" "github.com/grafana/grafana/pkg/services/alerting" "github.com/grafana/grafana/pkg/services/datasources" ngalertmodels "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/tsdb/legacydata" "github.com/grafana/grafana/pkg/tsdb/legacydata/interval" - "github.com/grafana/grafana/pkg/tsdb/prometheus" ) func init() { diff --git a/pkg/tsdb/loki/api.go b/pkg/tsdb/loki/api.go index 272edafd8b..aa6c9d8839 100644 --- a/pkg/tsdb/loki/api.go +++ b/pkg/tsdb/loki/api.go @@ -23,8 +23,8 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" + "github.com/grafana/grafana/pkg/promlib/converter" "github.com/grafana/grafana/pkg/tsdb/loki/instrumentation" - "github.com/grafana/grafana/pkg/tsdb/prometheus/converter" ) type LokiAPI struct { diff --git a/pkg/tsdb/prometheus/azureauth/azure.go b/pkg/tsdb/prometheus/azureauth/azure.go index 93b8e5263e..f5553c2b3f 100644 --- a/pkg/tsdb/prometheus/azureauth/azure.go +++ b/pkg/tsdb/prometheus/azureauth/azure.go @@ -12,7 +12,7 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" - "github.com/grafana/grafana/pkg/tsdb/prometheus/utils" + "github.com/grafana/grafana/pkg/promlib/utils" ) func ConfigureAzureAuthentication(settings backend.DataSourceInstanceSettings, azureSettings *azsettings.AzureSettings, clientOpts *sdkhttpclient.Options) error { diff --git a/pkg/tsdb/prometheus/prometheus.go b/pkg/tsdb/prometheus/prometheus.go index a6b92b8700..1f3ab688cb 100644 --- a/pkg/tsdb/prometheus/prometheus.go +++ b/pkg/tsdb/prometheus/prometheus.go @@ -2,145 +2,67 @@ package prometheus import ( "context" - "errors" "fmt" - "strings" - "time" + "github.com/grafana/grafana-azure-sdk-go/azsettings" "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana-plugin-sdk-go/backend/datasource" - "github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt" - "github.com/grafana/grafana-plugin-sdk-go/backend/log" - "github.com/patrickmn/go-cache" - apiv1 "github.com/prometheus/client_golang/api/prometheus/v1" + sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" - "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" - - "github.com/grafana/grafana/pkg/tsdb/prometheus/client" - "github.com/grafana/grafana/pkg/tsdb/prometheus/instrumentation" - "github.com/grafana/grafana/pkg/tsdb/prometheus/querydata" - "github.com/grafana/grafana/pkg/tsdb/prometheus/resource" + "github.com/grafana/grafana/pkg/promlib" + "github.com/grafana/grafana/pkg/tsdb/prometheus/azureauth" ) type Service struct { - im instancemgmt.InstanceManager - logger log.Logger -} - -type instance struct { - queryData *querydata.QueryData - resource *resource.Resource - versionCache *cache.Cache + lib *promlib.Service } -func ProvideService(httpClientProvider *httpclient.Provider) *Service { +func ProvideService(httpClientProvider *sdkhttpclient.Provider) *Service { plog := backend.NewLoggerWith("logger", "tsdb.prometheus") plog.Debug("Initializing") return &Service{ - im: datasource.NewInstanceManager(newInstanceSettings(httpClientProvider, plog)), - logger: plog, + lib: promlib.NewService(httpClientProvider, plog, extendClientOpts), } } -func newInstanceSettings(httpClientProvider *httpclient.Provider, log log.Logger) datasource.InstanceFactoryFunc { - return func(ctx context.Context, settings backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { - // Creates a http roundTripper. - opts, err := client.CreateTransportOptions(ctx, settings, log) - if err != nil { - return nil, fmt.Errorf("error creating transport options: %v", err) - } - httpClient, err := httpClientProvider.New(*opts) - if err != nil { - return nil, fmt.Errorf("error creating http client: %v", err) - } +func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { + return s.lib.QueryData(ctx, req) +} - // New version using custom client and better response parsing - qd, err := querydata.New(httpClient, settings, log) - if err != nil { - return nil, err - } +func (s *Service) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { + return s.lib.CallResource(ctx, req, sender) +} - // Resource call management using new custom client same as querydata - r, err := resource.New(httpClient, settings, log) - if err != nil { - return nil, err - } +func (s *Service) GetBuildInfo(ctx context.Context, req promlib.BuildInfoRequest) (*promlib.BuildInfoResponse, error) { + return s.lib.GetBuildInfo(ctx, req) +} - return instance{ - queryData: qd, - resource: r, - versionCache: cache.New(time.Minute*1, time.Minute*5), - }, nil - } +func (s *Service) GetHeuristics(ctx context.Context, req promlib.HeuristicsRequest) (*promlib.Heuristics, error) { + return s.lib.GetHeuristics(ctx, req) } -func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { - if len(req.Queries) == 0 { - err := fmt.Errorf("query contains no queries") - instrumentation.UpdateQueryDataMetrics(err, nil) - return &backend.QueryDataResponse{}, err - } +func (s *Service) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, + error) { + return s.lib.CheckHealth(ctx, req) +} - i, err := s.getInstance(ctx, req.PluginContext) - if err != nil { - instrumentation.UpdateQueryDataMetrics(err, nil) - return nil, err +func extendClientOpts(ctx context.Context, settings backend.DataSourceInstanceSettings, clientOpts *sdkhttpclient.Options) error { + // Set SigV4 service namespace + if clientOpts.SigV4 != nil { + clientOpts.SigV4.Service = "aps" } - qd, err := i.queryData.Execute(ctx, req) - instrumentation.UpdateQueryDataMetrics(err, qd) - - return qd, err -} - -func (s *Service) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { - i, err := s.getInstance(ctx, req.PluginContext) + azureSettings, err := azsettings.ReadSettings(ctx) if err != nil { - return err + return fmt.Errorf("failed to read Azure settings from Grafana: %v", err) } - if strings.EqualFold(req.Path, "version-detect") { - versionObj, found := i.versionCache.Get("version") - if found { - return sender.Send(versionObj.(*backend.CallResourceResponse)) - } - - vResp, err := i.resource.DetectVersion(ctx, req) + // Set Azure authentication + if azureSettings.AzureAuthEnabled { + err = azureauth.ConfigureAzureAuthentication(settings, azureSettings, clientOpts) if err != nil { - return err + return fmt.Errorf("error configuring Azure auth: %v", err) } - i.versionCache.Set("version", vResp, cache.DefaultExpiration) - return sender.Send(vResp) - } - - resp, err := i.resource.Execute(ctx, req) - if err != nil { - return err } - return sender.Send(resp) -} - -func (s *Service) getInstance(ctx context.Context, pluginCtx backend.PluginContext) (*instance, error) { - i, err := s.im.Get(ctx, pluginCtx) - if err != nil { - return nil, err - } - in := i.(instance) - return &in, nil -} - -// IsAPIError returns whether err is or wraps a Prometheus error. -func IsAPIError(err error) bool { - // Check if the right error type is in err's chain. - var e *apiv1.Error - return errors.As(err, &e) -} - -func ConvertAPIError(err error) error { - var e *apiv1.Error - if errors.As(err, &e) { - return fmt.Errorf("%s: %s", e.Msg, e.Detail) - } - return err + return nil } diff --git a/pkg/tsdb/prometheus/prometheus_test.go b/pkg/tsdb/prometheus/prometheus_test.go index 1d21212a18..458fa31a5c 100644 --- a/pkg/tsdb/prometheus/prometheus_test.go +++ b/pkg/tsdb/prometheus/prometheus_test.go @@ -2,116 +2,53 @@ package prometheus import ( "context" - "io" - "net/http" "testing" - "time" + "github.com/grafana/grafana-azure-sdk-go/azsettings" "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana-plugin-sdk-go/backend/datasource" - "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" + sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" "github.com/stretchr/testify/require" ) -type fakeSender struct{} - -func (sender *fakeSender) Send(resp *backend.CallResourceResponse) error { - return nil -} - -type fakeRoundtripper struct { - Req *http.Request -} - -func (rt *fakeRoundtripper) RoundTrip(req *http.Request) (*http.Response, error) { - rt.Req = req - return &http.Response{ - Status: "200", - StatusCode: 200, - Header: nil, - Body: nil, - ContentLength: 0, - }, nil -} - -type fakeHTTPClientProvider struct { - httpclient.Provider - Roundtripper *fakeRoundtripper -} - -func (provider *fakeHTTPClientProvider) New(opts ...httpclient.Options) (*http.Client, error) { - client := &http.Client{} - provider.Roundtripper = &fakeRoundtripper{} - client.Transport = provider.Roundtripper - return client, nil -} - -func (provider *fakeHTTPClientProvider) GetTransport(opts ...httpclient.Options) (http.RoundTripper, error) { - return &fakeRoundtripper{}, nil -} - -func getMockPromTestSDKProvider(f *fakeHTTPClientProvider) *httpclient.Provider { - anotherFN := func(o httpclient.Options, next http.RoundTripper) http.RoundTripper { - _, _ = f.New() - return f.Roundtripper - } - fn := httpclient.MiddlewareFunc(anotherFN) - mid := httpclient.NamedMiddlewareFunc("mock", fn) - return httpclient.NewProvider(httpclient.ProviderOptions{Middlewares: []httpclient.Middleware{mid}}) -} - -func TestService(t *testing.T) { - t.Run("Service", func(t *testing.T) { - t.Run("CallResource", func(t *testing.T) { - t.Run("creates correct request", func(t *testing.T) { - f := &fakeHTTPClientProvider{} - httpProvider := getMockPromTestSDKProvider(f) - service := &Service{ - im: datasource.NewInstanceManager(newInstanceSettings(httpProvider, backend.NewLoggerWith("logger", "test"))), - } - - req := &backend.CallResourceRequest{ - PluginContext: backend.PluginContext{ - OrgID: 0, - PluginID: "prometheus", - User: nil, - AppInstanceSettings: nil, - DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{ - ID: 0, - UID: "", - Type: "prometheus", - Name: "test-prom", - URL: "http://localhost:9090", - User: "", - Database: "", - BasicAuthEnabled: true, - BasicAuthUser: "admin", - Updated: time.Time{}, - JSONData: []byte("{}"), - }, - }, - Path: "/api/v1/series", - Method: http.MethodPost, - URL: "/api/v1/series", - Body: []byte("match%5B%5D: ALERTS\nstart: 1655271408\nend: 1655293008"), +func TestExtendClientOpts(t *testing.T) { + t.Run("add azure credentials if configured", func(t *testing.T) { + cfg := backend.NewGrafanaCfg(map[string]string{ + azsettings.AzureCloud: azsettings.AzurePublic, + azsettings.AzureAuthEnabled: "true", + }) + settings := backend.DataSourceInstanceSettings{ + BasicAuthEnabled: false, + BasicAuthUser: "", + JSONData: []byte(`{ + "azureCredentials": { + "authType": "msi" } + }`), + DecryptedSecureJSONData: map[string]string{}, + } + ctx := backend.WithGrafanaConfig(context.Background(), cfg) + opts := &sdkhttpclient.Options{} + err := extendClientOpts(ctx, settings, opts) + require.NoError(t, err) + require.Equal(t, 1, len(opts.Middlewares)) + }) - sender := &fakeSender{} - err := service.CallResource(context.Background(), req, sender) - require.NoError(t, err) - require.Equal( - t, - http.Header{ - "Content-Type": {"application/x-www-form-urlencoded"}, - "Idempotency-Key": []string(nil), - }, - f.Roundtripper.Req.Header) - require.Equal(t, http.MethodPost, f.Roundtripper.Req.Method) - body, err := io.ReadAll(f.Roundtripper.Req.Body) - require.NoError(t, err) - require.Equal(t, []byte("match%5B%5D: ALERTS\nstart: 1655271408\nend: 1655293008"), body) - require.Equal(t, "http://localhost:9090/api/v1/series", f.Roundtripper.Req.URL.String()) - }) - }) + t.Run("add sigV4 auth if opts has SigV4 configured", func(t *testing.T) { + settings := backend.DataSourceInstanceSettings{ + BasicAuthEnabled: false, + BasicAuthUser: "", + JSONData: []byte(""), + DecryptedSecureJSONData: map[string]string{}, + } + opts := &sdkhttpclient.Options{ + SigV4: &sdkhttpclient.SigV4Config{ + AuthType: "test", + AccessKey: "accesskey", + SecretKey: "secretkey", + }, + } + err := extendClientOpts(context.Background(), settings, opts) + require.NoError(t, err) + require.Equal(t, "aps", opts.SigV4.Service) }) } From 3449a43ff2aff7dc1e4bf1928de87d496280e92f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 16:07:52 +0000 Subject: [PATCH 0527/1406] Update react monorepo --- package.json | 4 +- packages/grafana-data/package.json | 4 +- packages/grafana-flamegraph/package.json | 2 +- .../grafana-o11y-ds-frontend/package.json | 2 +- packages/grafana-prometheus/package.json | 4 +- packages/grafana-runtime/package.json | 4 +- packages/grafana-sql/package.json | 2 +- packages/grafana-ui/package.json | 4 +- .../datasource/azuremonitor/package.json | 2 +- .../datasource/cloud-monitoring/package.json | 2 +- .../grafana-pyroscope-datasource/package.json | 4 +- .../grafana-testdata-datasource/package.json | 2 +- .../app/plugins/datasource/parca/package.json | 2 +- .../app/plugins/datasource/tempo/package.json | 4 +- .../plugins/datasource/zipkin/package.json | 2 +- yarn.lock | 60 +++++++++---------- 16 files changed, 52 insertions(+), 52 deletions(-) diff --git a/package.json b/package.json index eaacc0dd16..74b73edc21 100644 --- a/package.json +++ b/package.json @@ -123,9 +123,9 @@ "@types/papaparse": "5.3.14", "@types/pluralize": "^0.0.33", "@types/prismjs": "1.26.3", - "@types/react": "18.2.60", + "@types/react": "18.2.64", "@types/react-beautiful-dnd": "13.1.8", - "@types/react-dom": "18.2.19", + "@types/react-dom": "18.2.21", "@types/react-grid-layout": "1.3.5", "@types/react-highlight-words": "0.16.7", "@types/react-router": "5.1.20", diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index 63daf64ab4..3821e6e24e 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -69,8 +69,8 @@ "@types/marked": "5.0.2", "@types/node": "20.11.25", "@types/papaparse": "5.3.14", - "@types/react": "18.2.60", - "@types/react-dom": "18.2.19", + "@types/react": "18.2.64", + "@types/react-dom": "18.2.21", "@types/tinycolor2": "1.4.6", "esbuild": "0.18.12", "react": "18.2.0", diff --git a/packages/grafana-flamegraph/package.json b/packages/grafana-flamegraph/package.json index 1e6a8feab6..ab0417eabf 100644 --- a/packages/grafana-flamegraph/package.json +++ b/packages/grafana-flamegraph/package.json @@ -67,7 +67,7 @@ "@types/d3": "^7", "@types/jest": "^29.5.4", "@types/lodash": "4.14.202", - "@types/react": "18.2.60", + "@types/react": "18.2.64", "@types/react-virtualized-auto-sizer": "1.0.4", "@types/tinycolor2": "1.4.6", "babel-jest": "29.7.0", diff --git a/packages/grafana-o11y-ds-frontend/package.json b/packages/grafana-o11y-ds-frontend/package.json index 531f0eabb3..ee4cb82715 100644 --- a/packages/grafana-o11y-ds-frontend/package.json +++ b/packages/grafana-o11y-ds-frontend/package.json @@ -34,7 +34,7 @@ "@testing-library/react": "14.2.1", "@testing-library/user-event": "14.5.2", "@types/jest": "^29.5.4", - "@types/react": "18.2.60", + "@types/react": "18.2.64", "@types/systemjs": "6.13.5", "@types/testing-library__jest-dom": "5.14.9", "jest": "^29.6.4", diff --git a/packages/grafana-prometheus/package.json b/packages/grafana-prometheus/package.json index c2449ce8f0..92da7e4b6b 100644 --- a/packages/grafana-prometheus/package.json +++ b/packages/grafana-prometheus/package.json @@ -97,9 +97,9 @@ "@types/node": "20.11.25", "@types/pluralize": "^0.0.33", "@types/prismjs": "1.26.3", - "@types/react": "18.2.60", + "@types/react": "18.2.64", "@types/react-beautiful-dnd": "13.1.8", - "@types/react-dom": "18.2.19", + "@types/react-dom": "18.2.21", "@types/react-highlight-words": "0.16.7", "@types/react-window": "1.8.8", "@types/semver": "7.5.8", diff --git a/packages/grafana-runtime/package.json b/packages/grafana-runtime/package.json index fc1e92938b..1b4065aebf 100644 --- a/packages/grafana-runtime/package.json +++ b/packages/grafana-runtime/package.json @@ -60,8 +60,8 @@ "@types/history": "4.7.11", "@types/jest": "29.5.12", "@types/lodash": "4.14.202", - "@types/react": "18.2.60", - "@types/react-dom": "18.2.19", + "@types/react": "18.2.64", + "@types/react-dom": "18.2.21", "@types/systemjs": "6.13.5", "esbuild": "0.18.12", "lodash": "4.17.21", diff --git a/packages/grafana-sql/package.json b/packages/grafana-sql/package.json index 1e65b4e8ab..5fd7901242 100644 --- a/packages/grafana-sql/package.json +++ b/packages/grafana-sql/package.json @@ -39,7 +39,7 @@ "@testing-library/react-hooks": "^8.0.1", "@testing-library/user-event": "14.5.2", "@types/jest": "^29.5.4", - "@types/react": "18.2.60", + "@types/react": "18.2.64", "@types/systemjs": "6.13.5", "@types/testing-library__jest-dom": "5.14.9", "jest": "^29.6.4", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 5947818618..4c69b28a0a 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -141,11 +141,11 @@ "@types/mock-raf": "1.0.6", "@types/node": "20.11.25", "@types/prismjs": "1.26.3", - "@types/react": "18.2.60", + "@types/react": "18.2.64", "@types/react-beautiful-dnd": "13.1.8", "@types/react-calendar": "3.9.0", "@types/react-color": "3.0.12", - "@types/react-dom": "18.2.19", + "@types/react-dom": "18.2.21", "@types/react-highlight-words": "0.16.7", "@types/react-router-dom": "5.3.3", "@types/react-table": "7.7.19", diff --git a/public/app/plugins/datasource/azuremonitor/package.json b/public/app/plugins/datasource/azuremonitor/package.json index 7736ee128f..b7c27ca835 100644 --- a/public/app/plugins/datasource/azuremonitor/package.json +++ b/public/app/plugins/datasource/azuremonitor/package.json @@ -31,7 +31,7 @@ "@types/lodash": "4.14.202", "@types/node": "20.11.25", "@types/prismjs": "1.26.3", - "@types/react": "18.2.60", + "@types/react": "18.2.64", "@types/testing-library__jest-dom": "5.14.9", "react-select-event": "5.5.1", "ts-node": "10.9.2", diff --git a/public/app/plugins/datasource/cloud-monitoring/package.json b/public/app/plugins/datasource/cloud-monitoring/package.json index 811dbd49f8..bdbdb17a1e 100644 --- a/public/app/plugins/datasource/cloud-monitoring/package.json +++ b/public/app/plugins/datasource/cloud-monitoring/package.json @@ -34,7 +34,7 @@ "@types/lodash": "4.14.202", "@types/node": "20.11.25", "@types/prismjs": "1.26.3", - "@types/react": "18.2.60", + "@types/react": "18.2.64", "@types/react-test-renderer": "18.0.7", "@types/testing-library__jest-dom": "5.14.9", "react-select-event": "5.5.1", diff --git a/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json b/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json index 2a682b4ffc..d9f19a4185 100644 --- a/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json +++ b/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json @@ -27,8 +27,8 @@ "@types/jest": "29.5.12", "@types/lodash": "4.14.202", "@types/prismjs": "1.26.3", - "@types/react": "18.2.60", - "@types/react-dom": "18.2.19", + "@types/react": "18.2.64", + "@types/react-dom": "18.2.21", "@types/testing-library__jest-dom": "5.14.9", "css-loader": "6.10.0", "jest": "29.7.0", diff --git a/public/app/plugins/datasource/grafana-testdata-datasource/package.json b/public/app/plugins/datasource/grafana-testdata-datasource/package.json index 2cfcabb03f..8493953d52 100644 --- a/public/app/plugins/datasource/grafana-testdata-datasource/package.json +++ b/public/app/plugins/datasource/grafana-testdata-datasource/package.json @@ -28,7 +28,7 @@ "@types/jest": "29.5.12", "@types/lodash": "4.14.202", "@types/node": "20.11.25", - "@types/react": "18.2.60", + "@types/react": "18.2.64", "@types/testing-library__jest-dom": "5.14.9", "@types/uuid": "9.0.8", "ts-node": "10.9.2", diff --git a/public/app/plugins/datasource/parca/package.json b/public/app/plugins/datasource/parca/package.json index 5065411a48..f4ed0b78f3 100644 --- a/public/app/plugins/datasource/parca/package.json +++ b/public/app/plugins/datasource/parca/package.json @@ -21,7 +21,7 @@ "@testing-library/react": "14.2.1", "@testing-library/user-event": "14.5.2", "@types/lodash": "4.14.202", - "@types/react": "18.2.60", + "@types/react": "18.2.64", "ts-node": "10.9.2", "webpack": "5.90.3" }, diff --git a/public/app/plugins/datasource/tempo/package.json b/public/app/plugins/datasource/tempo/package.json index 33c8bd4245..8a14f9d861 100644 --- a/public/app/plugins/datasource/tempo/package.json +++ b/public/app/plugins/datasource/tempo/package.json @@ -46,8 +46,8 @@ "@types/lodash": "4.14.202", "@types/node": "20.11.25", "@types/prismjs": "1.26.3", - "@types/react": "18.2.60", - "@types/react-dom": "18.2.19", + "@types/react": "18.2.64", + "@types/react-dom": "18.2.21", "@types/semver": "7.5.8", "@types/uuid": "9.0.8", "glob": "10.3.10", diff --git a/public/app/plugins/datasource/zipkin/package.json b/public/app/plugins/datasource/zipkin/package.json index fae2903608..f0e6718bb2 100644 --- a/public/app/plugins/datasource/zipkin/package.json +++ b/public/app/plugins/datasource/zipkin/package.json @@ -22,7 +22,7 @@ "@testing-library/react": "14.2.1", "@types/jest": "29.5.12", "@types/lodash": "4.14.202", - "@types/react": "18.2.60", + "@types/react": "18.2.64", "ts-node": "10.9.2", "webpack": "5.90.3" }, diff --git a/yarn.lock b/yarn.lock index 5f8aefa1c5..a8b24d5a39 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3233,7 +3233,7 @@ __metadata: "@types/lodash": "npm:4.14.202" "@types/node": "npm:20.11.25" "@types/prismjs": "npm:1.26.3" - "@types/react": "npm:18.2.60" + "@types/react": "npm:18.2.64" "@types/testing-library__jest-dom": "npm:5.14.9" fast-deep-equal: "npm:^3.1.3" i18next: "npm:^23.0.0" @@ -3270,8 +3270,8 @@ __metadata: "@types/jest": "npm:29.5.12" "@types/lodash": "npm:4.14.202" "@types/prismjs": "npm:1.26.3" - "@types/react": "npm:18.2.60" - "@types/react-dom": "npm:18.2.19" + "@types/react": "npm:18.2.64" + "@types/react-dom": "npm:18.2.21" "@types/testing-library__jest-dom": "npm:5.14.9" css-loader: "npm:6.10.0" fast-deep-equal: "npm:^3.1.3" @@ -3311,7 +3311,7 @@ __metadata: "@types/jest": "npm:29.5.12" "@types/lodash": "npm:4.14.202" "@types/node": "npm:20.11.25" - "@types/react": "npm:18.2.60" + "@types/react": "npm:18.2.64" "@types/testing-library__jest-dom": "npm:5.14.9" "@types/uuid": "npm:9.0.8" d3-random: "npm:^3.0.1" @@ -3365,7 +3365,7 @@ __metadata: "@testing-library/react": "npm:14.2.1" "@testing-library/user-event": "npm:14.5.2" "@types/lodash": "npm:4.14.202" - "@types/react": "npm:18.2.60" + "@types/react": "npm:18.2.64" lodash: "npm:4.17.21" monaco-editor: "npm:0.34.0" react: "npm:18.2.0" @@ -3400,7 +3400,7 @@ __metadata: "@types/lodash": "npm:4.14.202" "@types/node": "npm:20.11.25" "@types/prismjs": "npm:1.26.3" - "@types/react": "npm:18.2.60" + "@types/react": "npm:18.2.64" "@types/react-test-renderer": "npm:18.0.7" "@types/testing-library__jest-dom": "npm:5.14.9" debounce-promise: "npm:3.1.2" @@ -3452,8 +3452,8 @@ __metadata: "@types/lodash": "npm:4.14.202" "@types/node": "npm:20.11.25" "@types/prismjs": "npm:1.26.3" - "@types/react": "npm:18.2.60" - "@types/react-dom": "npm:18.2.19" + "@types/react": "npm:18.2.64" + "@types/react-dom": "npm:18.2.21" "@types/semver": "npm:7.5.8" "@types/uuid": "npm:9.0.8" buffer: "npm:6.0.3" @@ -3497,7 +3497,7 @@ __metadata: "@testing-library/react": "npm:14.2.1" "@types/jest": "npm:29.5.12" "@types/lodash": "npm:4.14.202" - "@types/react": "npm:18.2.60" + "@types/react": "npm:18.2.64" lodash: "npm:4.17.21" react: "npm:18.2.0" react-use: "npm:17.5.0" @@ -3544,8 +3544,8 @@ __metadata: "@types/marked": "npm:5.0.2" "@types/node": "npm:20.11.25" "@types/papaparse": "npm:5.3.14" - "@types/react": "npm:18.2.60" - "@types/react-dom": "npm:18.2.19" + "@types/react": "npm:18.2.64" + "@types/react-dom": "npm:18.2.21" "@types/string-hash": "npm:1.1.3" "@types/tinycolor2": "npm:1.4.6" d3-interpolate: "npm:3.0.1" @@ -3776,7 +3776,7 @@ __metadata: "@types/d3": "npm:^7" "@types/jest": "npm:^29.5.4" "@types/lodash": "npm:4.14.202" - "@types/react": "npm:18.2.60" + "@types/react": "npm:18.2.64" "@types/react-virtualized-auto-sizer": "npm:1.0.4" "@types/tinycolor2": "npm:1.4.6" babel-jest: "npm:29.7.0" @@ -3857,7 +3857,7 @@ __metadata: "@testing-library/react": "npm:14.2.1" "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:^29.5.4" - "@types/react": "npm:18.2.60" + "@types/react": "npm:18.2.64" "@types/systemjs": "npm:6.13.5" "@types/testing-library__jest-dom": "npm:5.14.9" jest: "npm:^29.6.4" @@ -3943,9 +3943,9 @@ __metadata: "@types/node": "npm:20.11.25" "@types/pluralize": "npm:^0.0.33" "@types/prismjs": "npm:1.26.3" - "@types/react": "npm:18.2.60" + "@types/react": "npm:18.2.64" "@types/react-beautiful-dnd": "npm:13.1.8" - "@types/react-dom": "npm:18.2.19" + "@types/react-dom": "npm:18.2.21" "@types/react-highlight-words": "npm:0.16.7" "@types/react-window": "npm:1.8.8" "@types/semver": "npm:7.5.8" @@ -4036,8 +4036,8 @@ __metadata: "@types/history": "npm:4.7.11" "@types/jest": "npm:29.5.12" "@types/lodash": "npm:4.14.202" - "@types/react": "npm:18.2.60" - "@types/react-dom": "npm:18.2.19" + "@types/react": "npm:18.2.64" + "@types/react-dom": "npm:18.2.21" "@types/systemjs": "npm:6.13.5" esbuild: "npm:0.18.12" history: "npm:4.10.1" @@ -4119,7 +4119,7 @@ __metadata: "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:^29.5.4" "@types/lodash": "npm:4.14.202" - "@types/react": "npm:18.2.60" + "@types/react": "npm:18.2.64" "@types/react-virtualized-auto-sizer": "npm:1.0.4" "@types/systemjs": "npm:6.13.5" "@types/testing-library__jest-dom": "npm:5.14.9" @@ -4206,11 +4206,11 @@ __metadata: "@types/mock-raf": "npm:1.0.6" "@types/node": "npm:20.11.25" "@types/prismjs": "npm:1.26.3" - "@types/react": "npm:18.2.60" + "@types/react": "npm:18.2.64" "@types/react-beautiful-dnd": "npm:13.1.8" "@types/react-calendar": "npm:3.9.0" "@types/react-color": "npm:3.0.12" - "@types/react-dom": "npm:18.2.19" + "@types/react-dom": "npm:18.2.21" "@types/react-highlight-words": "npm:0.16.7" "@types/react-router-dom": "npm:5.3.3" "@types/react-table": "npm:7.7.19" @@ -9713,12 +9713,12 @@ __metadata: languageName: node linkType: hard -"@types/react-dom@npm:*, @types/react-dom@npm:18.2.19, @types/react-dom@npm:^18.0.0": - version: 18.2.19 - resolution: "@types/react-dom@npm:18.2.19" +"@types/react-dom@npm:*, @types/react-dom@npm:18.2.21, @types/react-dom@npm:^18.0.0": + version: 18.2.21 + resolution: "@types/react-dom@npm:18.2.21" dependencies: "@types/react": "npm:*" - checksum: 10/98eb760ce78f1016d97c70f605f0b1a53873a548d3c2192b40c897f694fd9c8bb12baeada16581a9c7b26f5022c1d2613547be98284d8f1b82d1611b1e3e7df0 + checksum: 10/5c714bc69c3cf979dbf3fb8c66fc42b5b740af54d98c1037c593ac9ad54e5107cfc39fcba8c550f8df924670f55b411898d53c6166a2b15b4ed44a248a358590 languageName: node linkType: hard @@ -9837,14 +9837,14 @@ __metadata: languageName: node linkType: hard -"@types/react@npm:*, @types/react@npm:18.2.60, @types/react@npm:>=16": - version: 18.2.60 - resolution: "@types/react@npm:18.2.60" +"@types/react@npm:*, @types/react@npm:18.2.64, @types/react@npm:>=16": + version: 18.2.64 + resolution: "@types/react@npm:18.2.64" dependencies: "@types/prop-types": "npm:*" "@types/scheduler": "npm:*" csstype: "npm:^3.0.2" - checksum: 10/5f2f6091623f13375a5bbc7e5c222cd212b5d6366ead737b76c853f6f52b314db24af5ae3f688d2d49814c668c216858a75433f145311839d8989d46bb3cbecf + checksum: 10/e82bd16660030c9aabdb5027bb9bf69570a90813e4a894c17bfb288978c2b80251def5754e455d72aa3485d99136e1b11b694f78c586e5918e10b3bb09b91b3c languageName: node linkType: hard @@ -18317,9 +18317,9 @@ __metadata: "@types/papaparse": "npm:5.3.14" "@types/pluralize": "npm:^0.0.33" "@types/prismjs": "npm:1.26.3" - "@types/react": "npm:18.2.60" + "@types/react": "npm:18.2.64" "@types/react-beautiful-dnd": "npm:13.1.8" - "@types/react-dom": "npm:18.2.19" + "@types/react-dom": "npm:18.2.21" "@types/react-grid-layout": "npm:1.3.5" "@types/react-highlight-words": "npm:0.16.7" "@types/react-resizable": "npm:3.0.7" From 5ae9cd561c4e241c936d7b4784a49bc04ed9db98 Mon Sep 17 00:00:00 2001 From: Tobias Skarhed <1438972+tskarhed@users.noreply.github.com> Date: Mon, 11 Mar 2024 17:43:55 +0100 Subject: [PATCH 0528/1406] Accessibility: Improve HelpModal markup (#83171) * HelpModal: Make more accessible * Remove console.log * Handle custom keys for screen reader * Rewrite using tables * Increase gap * Change order of help categories * HelpModal: Add tabIndex and imrpove sr-only display --- public/app/core/components/help/HelpModal.tsx | 207 ++++++++++-------- public/app/core/utils/browser.ts | 2 +- 2 files changed, 118 insertions(+), 91 deletions(-) diff --git a/public/app/core/components/help/HelpModal.tsx b/public/app/core/components/help/HelpModal.tsx index 1f05069071..3d3b3f5f90 100644 --- a/public/app/core/components/help/HelpModal.tsx +++ b/public/app/core/components/help/HelpModal.tsx @@ -2,7 +2,7 @@ import { css } from '@emotion/css'; import React, { useMemo } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; -import { Modal, useStyles2 } from '@grafana/ui'; +import { Grid, Modal, useStyles2, Text } from '@grafana/ui'; import { t } from 'app/core/internationalization'; import { getModKey } from 'app/core/utils/browser'; @@ -33,10 +33,45 @@ const getShortcuts = (modKey: string) => { { keys: ['c', 't'], description: t('help-modal.shortcuts-description.change-theme', 'Change theme') }, ], }, + { + category: t('help-modal.shortcuts-category.time-range', 'Time range'), + shortcuts: [ + { + keys: ['t', 'z'], + description: t('help-modal.shortcuts-description.zoom-out-time-range', 'Zoom out time range'), + }, + { + keys: ['t', '←'], + description: t('help-modal.shortcuts-description.move-time-range-back', 'Move time range back'), + }, + { + keys: ['t', '→'], + description: t('help-modal.shortcuts-description.move-time-range-forward', 'Move time range forward'), + }, + { + keys: ['t', 'a'], + description: t( + 'help-modal.shortcuts-description.make-time-range-permanent', + 'Make time range absolute/permanent' + ), + }, + { + keys: ['t', 'c'], + description: t('help-modal.shortcuts-description.copy-time-range', 'Copy time range'), + }, + { + keys: ['t', 'v'], + description: t('help-modal.shortcuts-description.paste-time-range', 'Paste time range'), + }, + ], + }, { category: t('help-modal.shortcuts-category.dashboard', 'Dashboard'), shortcuts: [ - { keys: [`${modKey}+s`], description: t('help-modal.shortcuts-description.save-dashboard', 'Save dashboard') }, + { + keys: [`${modKey} + s`], + description: t('help-modal.shortcuts-description.save-dashboard', 'Save dashboard'), + }, { keys: ['d', 'r'], description: t('help-modal.shortcuts-description.refresh-all-panels', 'Refresh all panels'), @@ -53,8 +88,14 @@ const getShortcuts = (modKey: string) => { keys: ['d', 'k'], description: t('help-modal.shortcuts-description.toggle-kiosk', 'Toggle kiosk mode (hides top nav)'), }, - { keys: ['d', 'E'], description: t('help-modal.shortcuts-description.expand-all-rows', 'Expand all rows') }, - { keys: ['d', 'C'], description: t('help-modal.shortcuts-description.collapse-all-rows', 'Collapse all rows') }, + { + keys: ['d', '⇧ + e'], + description: t('help-modal.shortcuts-description.expand-all-rows', 'Expand all rows'), + }, + { + keys: ['d', '⇧ + c'], + description: t('help-modal.shortcuts-description.collapse-all-rows', 'Collapse all rows'), + }, { keys: ['d', 'a'], description: t( @@ -77,7 +118,7 @@ const getShortcuts = (modKey: string) => { ], }, { - category: t('help-modal.shortcuts-category.focused-panel', 'Focused Panel'), + category: t('help-modal.shortcuts-category.focused-panel', 'Focused panel'), shortcuts: [ { keys: ['e'], @@ -99,38 +140,6 @@ const getShortcuts = (modKey: string) => { }, ], }, - { - category: t('help-modal.shortcuts-category.time-range', 'Time Range'), - shortcuts: [ - { - keys: ['t', 'z'], - description: t('help-modal.shortcuts-description.zoom-out-time-range', 'Zoom out time range'), - }, - { - keys: ['t', '←'], - description: t('help-modal.shortcuts-description.move-time-range-back', 'Move time range back'), - }, - { - keys: ['t', '→'], - description: t('help-modal.shortcuts-description.move-time-range-forward', 'Move time range forward'), - }, - { - keys: ['t', 'a'], - description: t( - 'help-modal.shortcuts-description.make-time-range-permanent', - 'Make time range absolute/permanent' - ), - }, - { - keys: ['t', 'c'], - description: t('help-modal.shortcuts-description.copy-time-range', 'Copy time range'), - }, - { - keys: ['t', 'v'], - description: t('help-modal.shortcuts-description.paste-time-range', 'Paste time range'), - }, - ], - }, ]; }; @@ -140,86 +149,104 @@ export interface HelpModalProps { export const HelpModal = ({ onDismiss }: HelpModalProps): JSX.Element => { const styles = useStyles2(getStyles); + const modKey = useMemo(() => getModKey(), []); const shortcuts = useMemo(() => getShortcuts(modKey), [modKey]); return ( -
- {Object.values(shortcuts).map(({ category, shortcuts }, i) => ( -
- - + + {Object.values(shortcuts).map(({ category, shortcuts }) => ( +
+
+ + - + + - {shortcuts.map((shortcut, j) => ( - - + + {shortcuts.map(({ keys, description }) => ( + + - + ))}
+ + {category} + +
- {category} - KeysDescription
- {shortcut.keys.map((key, k) => ( - - {key} - +
+ {keys.map((key) => ( + {key} ))} {shortcut.description} + + {description} + +
-
+ ))} -
+ ); }; +interface KeyProps { + children: string; +} + +const Key = ({ children }: KeyProps) => { + const styles = useStyles2(getStyles); + const displayText = useMemo(() => replaceCustomKeyNames(children), [children]); + const displayElement = ; + return ( + + {displayElement} + + ); +}; + +function replaceCustomKeyNames(key: string) { + let displayName; + let srName; + + if (key.includes('ctrl')) { + displayName = 'ctrl'; + srName = 'Control'; + } else if (key.includes('esc')) { + displayName = 'esc'; + srName = 'Escape'; + } else { + return key; + } + + return key.replace( + displayName, + `${srName}` + ); +} + function getStyles(theme: GrafanaTheme2) { return { - titleDescription: css({ - fontSize: theme.typography.bodySmall.fontSize, - fontWeight: theme.typography.bodySmall.fontWeight, - color: theme.colors.text.disabled, - paddingBottom: theme.spacing(2), - }), - categories: css({ - fontSize: theme.typography.bodySmall.fontSize, - display: 'flex', - flexFlow: 'row wrap', - justifyContent: 'space-between', - alignItems: 'flex-start', + table: css({ + borderCollapse: 'separate', + borderSpacing: theme.spacing(2), + '& caption': { + captionSide: 'top', + }, }), - shortcutCategory: css({ - width: '50%', - fontSize: theme.typography.bodySmall.fontSize, - }), - shortcutTable: css({ - marginBottom: theme.spacing(2), - }), - shortcutTableCategoryHeader: css({ - fontWeight: 'normal', - fontSize: theme.typography.h6.fontSize, - textAlign: 'left', - }), - shortcutTableDescription: css({ - textAlign: 'left', - color: theme.colors.text.disabled, - width: '99%', - padding: theme.spacing(1, 2), - }), - shortcutTableKeys: css({ + keys: css({ + textAlign: 'end', whiteSpace: 'nowrap', - width: '1%', - textAlign: 'right', - color: theme.colors.text.primary, + minWidth: 83, // To match column widths with the widest }), shortcutTableKey: css({ display: 'inline-block', textAlign: 'center', marginRight: theme.spacing(0.5), padding: '3px 5px', - font: "11px Consolas, 'Liberation Mono', Menlo, Courier, monospace", lineHeight: '10px', verticalAlign: 'middle', border: `solid 1px ${theme.colors.border.medium}`, diff --git a/public/app/core/utils/browser.ts b/public/app/core/utils/browser.ts index fac61e9231..ec37aa3793 100644 --- a/public/app/core/utils/browser.ts +++ b/public/app/core/utils/browser.ts @@ -39,5 +39,5 @@ export function userAgentIsApple() { } export function getModKey() { - return userAgentIsApple() ? 'cmd' : 'ctrl'; + return userAgentIsApple() ? '⌘' : 'ctrl'; } From e2cc5e57e59530cf0d7e0f8a978bc434c728b3a4 Mon Sep 17 00:00:00 2001 From: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> Date: Mon, 11 Mar 2024 12:57:02 -0400 Subject: [PATCH 0529/1406] Docs: add missing alt text (#84102) Added missing alt text --- docs/sources/whatsnew/whats-new-in-v10-3.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/whatsnew/whats-new-in-v10-3.md b/docs/sources/whatsnew/whats-new-in-v10-3.md index c4674b4353..329f1e5ffe 100644 --- a/docs/sources/whatsnew/whats-new-in-v10-3.md +++ b/docs/sources/whatsnew/whats-new-in-v10-3.md @@ -365,7 +365,7 @@ Derived fields or data links are a concept to add correlations based on your log The following example would add the derived field `traceID regex` based on a regular expression and another `app label` field based on the `app` label. -{{< figure src="/media/docs/grafana/2024-01-05_loki-derived-fields.png" >}} +{{< figure src="/media/docs/grafana/2024-01-05_loki-derived-fields.png" alt="Derived fields added based on a regular expression and an app label">}} ### InfluxDB native SQL support From ffd0bdafe4848ce043460aa9e3b1da381dde6550 Mon Sep 17 00:00:00 2001 From: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> Date: Mon, 11 Mar 2024 12:57:51 -0400 Subject: [PATCH 0530/1406] Docs: fix broken link (#84103) * Fixed broken link * Removed trailing slash * Ran prettier --- docs/sources/developers/angular_deprecation/angular-plugins.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/developers/angular_deprecation/angular-plugins.md b/docs/sources/developers/angular_deprecation/angular-plugins.md index e2c20b5aee..377a7a61b8 100644 --- a/docs/sources/developers/angular_deprecation/angular-plugins.md +++ b/docs/sources/developers/angular_deprecation/angular-plugins.md @@ -111,7 +111,7 @@ This table lists plugins which we have detected as having a dependency on Angula | blackmirror1-statusbygroup-panel | Status By Group Panel | Migrate - Browse included visualizations and plugins catalog for potential alternatives. | | novalabs-annotations-panel | Annotation Panel | Migrate - Browse included visualizations and plugins catalog for potential alternatives. | | jasonlashua-prtg-datasource | PRTG | Migrate - Browse included data sources and plugins catalog for potential alternatives. | -| ryantxu-annolist-panel | Annotation List | Migrate - Consider [annotations list]({{< relref "../../panels-visualizations/visualizations/annotation-list" >}}) (core). | +| ryantxu-annolist-panel | Annotation List | Migrate - Consider [annotations list]({{< relref "../../panels-visualizations/visualizations/annotations" >}}) (core). | | cloudflare-app | Cloudflare Grafana App | Migrate - Consider using the [Cloudflare Dashboard](https://dash.cloudflare.com/?to=/:account/:zone/analytics/dns) or [DNS Analytics API](https://developers.cloudflare.com/api/operations/dns-analytics-table). | | smartmakers-trafficlight-panel | TrafficLight | Migrate - Consider [Traffic Light](https://grafana.com/grafana/plugins/heywesty-trafficlight-panel/) as a potential alternative. | | zuburqan-parity-report-panel | Parity Report | Migrate - Browse included visualizations and plugins catalog for potential alternatives. | From 1ffd1cc8f42f432a70a898f59826d78efb99bed8 Mon Sep 17 00:00:00 2001 From: Dan Cech Date: Mon, 11 Mar 2024 13:59:54 -0400 Subject: [PATCH 0531/1406] Storage: Support listing deleted entities (#84043) * support listing deleted entities * fold listDeleted into List --- .../apiserver/storage/entity/selector.go | 14 + .../apiserver/storage/entity/storage.go | 4 +- pkg/services/store/entity/entity.pb.go | 267 +++++++++--------- pkg/services/store/entity/entity.proto | 3 + .../entity/sqlstash/sql_storage_server.go | 52 ++-- 5 files changed, 189 insertions(+), 151 deletions(-) diff --git a/pkg/services/apiserver/storage/entity/selector.go b/pkg/services/apiserver/storage/entity/selector.go index 463ebe6078..37fc6b593f 100644 --- a/pkg/services/apiserver/storage/entity/selector.go +++ b/pkg/services/apiserver/storage/entity/selector.go @@ -8,12 +8,15 @@ import ( const folderAnnoKey = "grafana.app/folder" const sortByKey = "grafana.app/sortBy" +const listDeletedKey = "grafana.app/listDeleted" type Requirements struct { // Equals folder Folder *string // SortBy is a list of fields to sort by SortBy []string + // ListDeleted is a flag to list deleted entities + ListDeleted bool } func ReadLabelSelectors(selector labels.Selector) (Requirements, labels.Selector, error) { @@ -39,6 +42,17 @@ func ReadLabelSelectors(selector labels.Selector) (Requirements, labels.Selector return requirements, newSelector, apierrors.NewBadRequest(sortByKey + " label selector only supports in") } requirements.SortBy = r.Values().List() + case listDeletedKey: + if r.Operator() != selection.Equals { + return requirements, newSelector, apierrors.NewBadRequest(listDeletedKey + " label selector only supports equality") + } + if len(r.Values().List()) != 1 { + return requirements, newSelector, apierrors.NewBadRequest(listDeletedKey + " label selector only supports one value") + } + if r.Values().List()[0] != "true" && r.Values().List()[0] != "false" { + return requirements, newSelector, apierrors.NewBadRequest(listDeletedKey + " label selector only supports true or false") + } + requirements.ListDeleted = r.Values().List()[0] == "true" // add all unregonized label selectors to the new selector list, these will be processed by the entity store default: newSelector = newSelector.Add(r) diff --git a/pkg/services/apiserver/storage/entity/storage.go b/pkg/services/apiserver/storage/entity/storage.go index fc5536b9a5..491884e61e 100644 --- a/pkg/services/apiserver/storage/entity/storage.go +++ b/pkg/services/apiserver/storage/entity/storage.go @@ -298,7 +298,6 @@ func (s *Storage) GetList(ctx context.Context, key string, opts storage.ListOpti NextPageToken: opts.Predicate.Continue, Limit: opts.Predicate.Limit, Labels: map[string]string{}, - // TODO push label/field matching down to storage } // translate grafana.app/* label selectors into field requirements @@ -312,6 +311,9 @@ func (s *Storage) GetList(ctx context.Context, key string, opts storage.ListOpti if len(requirements.SortBy) > 0 { req.Sort = requirements.SortBy } + if requirements.ListDeleted { + req.Deleted = true + } // Update the selector to remove the unneeded requirements opts.Predicate.Label = newSelector diff --git a/pkg/services/store/entity/entity.pb.go b/pkg/services/store/entity/entity.pb.go index afa235d6e8..691d30f43e 100644 --- a/pkg/services/store/entity/entity.pb.go +++ b/pkg/services/store/entity/entity.pb.go @@ -1338,6 +1338,8 @@ type EntityListRequest struct { WithBody bool `protobuf:"varint,8,opt,name=with_body,json=withBody,proto3" json:"with_body,omitempty"` // Return the full body in each payload WithStatus bool `protobuf:"varint,10,opt,name=with_status,json=withStatus,proto3" json:"with_status,omitempty"` + // list deleted entities instead of active ones + Deleted bool `protobuf:"varint,12,opt,name=deleted,proto3" json:"deleted,omitempty"` } func (x *EntityListRequest) Reset() { @@ -1449,6 +1451,13 @@ func (x *EntityListRequest) GetWithStatus() bool { return false } +func (x *EntityListRequest) GetDeleted() bool { + if x != nil { + return x.Deleted + } + return false +} + type ReferenceRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -2139,7 +2148,7 @@ var file_entity_proto_rawDesc = []byte{ 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x08, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, - 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x8f, 0x03, + 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0xa9, 0x03, 0x0a, 0x11, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, @@ -2161,135 +2170,137 @@ var file_entity_proto_rawDesc = []byte{ 0x6f, 0x64, 0x79, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x77, 0x69, 0x74, 0x68, 0x42, 0x6f, 0x64, 0x79, 0x12, 0x1f, 0x0a, 0x0b, 0x77, 0x69, 0x74, 0x68, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x77, 0x69, 0x74, 0x68, 0x53, 0x74, - 0x61, 0x74, 0x75, 0x73, 0x1a, 0x39, 0x0a, 0x0b, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x45, 0x6e, - 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, - 0xb4, 0x01, 0x0a, 0x10, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, - 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, - 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x14, 0x0a, 0x05, - 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x6c, 0x69, 0x6d, - 0x69, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, - 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, - 0x12, 0x14, 0x0a, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x91, 0x01, 0x0a, 0x12, 0x45, 0x6e, 0x74, 0x69, 0x74, - 0x79, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x28, 0x0a, - 0x07, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, - 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x07, - 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, - 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, - 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, - 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0xa9, 0x02, 0x0a, 0x12, 0x45, - 0x6e, 0x74, 0x69, 0x74, 0x79, 0x57, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, - 0x52, 0x05, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x02, - 0x20, 0x03, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x12, 0x3e, 0x0a, - 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x26, 0x2e, - 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x57, 0x61, 0x74, - 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, - 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x12, 0x1b, 0x0a, - 0x09, 0x77, 0x69, 0x74, 0x68, 0x5f, 0x62, 0x6f, 0x64, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x08, 0x77, 0x69, 0x74, 0x68, 0x42, 0x6f, 0x64, 0x79, 0x12, 0x1f, 0x0a, 0x0b, 0x77, 0x69, - 0x74, 0x68, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x0a, 0x77, 0x69, 0x74, 0x68, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x1a, 0x39, 0x0a, 0x0b, 0x4c, - 0x61, 0x62, 0x65, 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, - 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, - 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x5b, 0x0a, 0x13, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, - 0x57, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1c, 0x0a, - 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, - 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x26, 0x0a, 0x06, 0x65, - 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x65, 0x6e, - 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x06, 0x65, 0x6e, 0x74, - 0x69, 0x74, 0x79, 0x22, 0xa2, 0x04, 0x0a, 0x0d, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x53, 0x75, - 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x03, 0x55, 0x49, 0x44, 0x12, 0x12, 0x0a, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, - 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, - 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, - 0x6e, 0x12, 0x39, 0x0a, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, + 0x61, 0x74, 0x75, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x18, + 0x0c, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x1a, 0x39, + 0x0a, 0x0b, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, + 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, + 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xb4, 0x01, 0x0a, 0x10, 0x52, 0x65, + 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, + 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, + 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, + 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x1c, 0x0a, 0x09, + 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x67, 0x72, + 0x6f, 0x75, 0x70, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, + 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, + 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, + 0x22, 0x91, 0x01, 0x0a, 0x12, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x4c, 0x69, 0x73, 0x74, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x28, 0x0a, 0x07, 0x72, 0x65, 0x73, 0x75, 0x6c, + 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, + 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x07, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, + 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, + 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, + 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, + 0x73, 0x69, 0x6f, 0x6e, 0x22, 0xa9, 0x02, 0x0a, 0x12, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x57, + 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x73, + 0x69, 0x6e, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x73, 0x69, 0x6e, 0x63, + 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x03, + 0x6b, 0x65, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, + 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, + 0x16, 0x0a, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x12, 0x3e, 0x0a, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, + 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, + 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x57, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x2e, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, + 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x77, 0x69, 0x74, 0x68, 0x5f, + 0x62, 0x6f, 0x64, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x77, 0x69, 0x74, 0x68, + 0x42, 0x6f, 0x64, 0x79, 0x12, 0x1f, 0x0a, 0x0b, 0x77, 0x69, 0x74, 0x68, 0x5f, 0x73, 0x74, 0x61, + 0x74, 0x75, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x77, 0x69, 0x74, 0x68, 0x53, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x1a, 0x39, 0x0a, 0x0b, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x45, + 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, + 0x22, 0x5b, 0x0a, 0x13, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x57, 0x61, 0x74, 0x63, 0x68, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, + 0x74, 0x61, 0x6d, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, + 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x26, 0x0a, 0x06, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, + 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x06, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x22, 0xa2, 0x04, + 0x0a, 0x0d, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x12, + 0x10, 0x0a, 0x03, 0x55, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x55, 0x49, + 0x44, 0x12, 0x12, 0x0a, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, + 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, + 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x39, 0x0a, 0x06, 0x6c, + 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x65, 0x6e, + 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x53, 0x75, 0x6d, 0x6d, 0x61, + 0x72, 0x79, 0x2e, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, + 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, + 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x12, 0x12, + 0x0a, 0x04, 0x73, 0x6c, 0x75, 0x67, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x73, 0x6c, + 0x75, 0x67, 0x12, 0x2d, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x08, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x17, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, + 0x79, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, + 0x72, 0x12, 0x39, 0x0a, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x18, 0x09, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, - 0x79, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x2e, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x45, - 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x12, 0x16, 0x0a, 0x06, - 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6f, - 0x6c, 0x64, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x6c, 0x75, 0x67, 0x18, 0x07, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x04, 0x73, 0x6c, 0x75, 0x67, 0x12, 0x2d, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, - 0x72, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, - 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x49, 0x6e, 0x66, 0x6f, - 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x39, 0x0a, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, - 0x73, 0x18, 0x09, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, - 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x2e, 0x46, - 0x69, 0x65, 0x6c, 0x64, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, 0x66, 0x69, 0x65, 0x6c, - 0x64, 0x73, 0x12, 0x2d, 0x0a, 0x06, 0x6e, 0x65, 0x73, 0x74, 0x65, 0x64, 0x18, 0x0a, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, - 0x74, 0x79, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x52, 0x06, 0x6e, 0x65, 0x73, 0x74, 0x65, - 0x64, 0x12, 0x3f, 0x0a, 0x0a, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x18, - 0x0b, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, - 0x6e, 0x74, 0x69, 0x74, 0x79, 0x45, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x52, 0x65, 0x66, - 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x0a, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, - 0x65, 0x73, 0x1a, 0x39, 0x0a, 0x0b, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, - 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, - 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x39, 0x0a, - 0x0b, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, - 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, - 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x65, 0x0a, 0x17, 0x45, 0x6e, 0x74, 0x69, - 0x74, 0x79, 0x45, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, - 0x6e, 0x63, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x61, 0x6d, 0x69, 0x6c, 0x79, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x61, 0x6d, 0x69, 0x6c, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x74, - 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, - 0x1e, 0x0a, 0x0a, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x0a, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x32, - 0xa8, 0x04, 0x0a, 0x0b, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x12, - 0x31, 0x0a, 0x04, 0x52, 0x65, 0x61, 0x64, 0x12, 0x19, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, - 0x2e, 0x52, 0x65, 0x61, 0x64, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x0e, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, - 0x74, 0x79, 0x12, 0x4c, 0x0a, 0x09, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x61, 0x64, 0x12, - 0x1e, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, - 0x61, 0x64, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x1f, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, - 0x61, 0x64, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x43, 0x0a, 0x06, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x12, 0x1b, 0x2e, 0x65, 0x6e, 0x74, - 0x69, 0x74, 0x79, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, - 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x43, 0x0a, 0x06, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, - 0x1b, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x45, - 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x65, - 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x74, 0x69, - 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x43, 0x0a, 0x06, 0x44, 0x65, - 0x6c, 0x65, 0x74, 0x65, 0x12, 0x1b, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x44, 0x65, - 0x6c, 0x65, 0x74, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x1c, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, + 0x79, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x45, + 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x12, 0x2d, 0x0a, 0x06, + 0x6e, 0x65, 0x73, 0x74, 0x65, 0x64, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x65, + 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x53, 0x75, 0x6d, 0x6d, + 0x61, 0x72, 0x79, 0x52, 0x06, 0x6e, 0x65, 0x73, 0x74, 0x65, 0x64, 0x12, 0x3f, 0x0a, 0x0a, 0x72, + 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x1f, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x45, + 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, + 0x52, 0x0a, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x1a, 0x39, 0x0a, 0x0b, + 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, + 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, + 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x39, 0x0a, 0x0b, 0x46, 0x69, 0x65, 0x6c, 0x64, + 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, + 0x38, 0x01, 0x22, 0x65, 0x0a, 0x17, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x45, 0x78, 0x74, 0x65, + 0x72, 0x6e, 0x61, 0x6c, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x16, 0x0a, + 0x06, 0x66, 0x61, 0x6d, 0x69, 0x6c, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, + 0x61, 0x6d, 0x69, 0x6c, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x69, 0x64, 0x65, + 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x69, + 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x32, 0xa8, 0x04, 0x0a, 0x0b, 0x45, 0x6e, + 0x74, 0x69, 0x74, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x12, 0x31, 0x0a, 0x04, 0x52, 0x65, 0x61, + 0x64, 0x12, 0x19, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x45, + 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0e, 0x2e, 0x65, + 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x4c, 0x0a, 0x09, + 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x61, 0x64, 0x12, 0x1e, 0x2e, 0x65, 0x6e, 0x74, 0x69, + 0x74, 0x79, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x61, 0x64, 0x45, 0x6e, 0x74, 0x69, + 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x65, 0x6e, 0x74, 0x69, + 0x74, 0x79, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x61, 0x64, 0x45, 0x6e, 0x74, 0x69, + 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x43, 0x0a, 0x06, 0x43, 0x72, + 0x65, 0x61, 0x74, 0x65, 0x12, 0x1b, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x43, 0x72, + 0x65, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x1c, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x46, 0x0a, 0x07, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x1c, 0x2e, 0x65, 0x6e, 0x74, - 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, - 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, - 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3d, 0x0a, 0x04, 0x4c, 0x69, 0x73, 0x74, 0x12, - 0x19, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x4c, - 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x65, 0x6e, 0x74, - 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x42, 0x0a, 0x05, 0x57, 0x61, 0x74, 0x63, 0x68, 0x12, - 0x1a, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x57, - 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x65, 0x6e, - 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x57, 0x61, 0x74, 0x63, 0x68, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x42, 0x36, 0x5a, 0x34, 0x67, 0x69, - 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, - 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x73, 0x65, 0x72, - 0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2f, 0x65, 0x6e, 0x74, 0x69, - 0x74, 0x79, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x43, 0x0a, 0x06, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x1b, 0x2e, 0x65, 0x6e, 0x74, 0x69, + 0x74, 0x79, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, + 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x43, 0x0a, 0x06, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x1b, + 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x45, 0x6e, + 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x65, 0x6e, + 0x74, 0x69, 0x74, 0x79, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, + 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x46, 0x0a, 0x07, 0x48, 0x69, 0x73, + 0x74, 0x6f, 0x72, 0x79, 0x12, 0x1c, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, + 0x74, 0x69, 0x74, 0x79, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, + 0x74, 0x79, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x3d, 0x0a, 0x04, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x19, 0x2e, 0x65, 0x6e, 0x74, 0x69, + 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, 0x6e, + 0x74, 0x69, 0x74, 0x79, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x42, 0x0a, 0x05, 0x57, 0x61, 0x74, 0x63, 0x68, 0x12, 0x1a, 0x2e, 0x65, 0x6e, 0x74, 0x69, + 0x74, 0x79, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x57, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x45, + 0x6e, 0x74, 0x69, 0x74, 0x79, 0x57, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x30, 0x01, 0x42, 0x36, 0x5a, 0x34, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, + 0x6f, 0x6d, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, + 0x6e, 0x61, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, + 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2f, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x62, 0x06, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/pkg/services/store/entity/entity.proto b/pkg/services/store/entity/entity.proto index 694731eb08..769926dcc0 100644 --- a/pkg/services/store/entity/entity.proto +++ b/pkg/services/store/entity/entity.proto @@ -300,6 +300,9 @@ message EntityListRequest { // Return the full body in each payload bool with_status = 10; + + // list deleted entities instead of active ones + bool deleted = 12; } message ReferenceRequest { diff --git a/pkg/services/store/entity/sqlstash/sql_storage_server.go b/pkg/services/store/entity/sqlstash/sql_storage_server.go index 51b8887b41..036312fb2d 100644 --- a/pkg/services/store/entity/sqlstash/sql_storage_server.go +++ b/pkg/services/store/entity/sqlstash/sql_storage_server.go @@ -1079,6 +1079,12 @@ func (s *sqlEntityServer) List(ctx context.Context, r *entity.EntityListRequest) oneExtra: true, // request one more than the limit (and show next token if it exists) } + // if we are looking for deleted entities, we list "deleted" entries from the entity_history table + if r.Deleted { + entityQuery.from = "entity_history" + entityQuery.addWhere("action", entity.Entity_DELETED) + } + // TODO fix this // entityQuery.addWhere("namespace", user.OrgID) @@ -1132,20 +1138,28 @@ func (s *sqlEntityServer) List(ctx context.Context, r *entity.EntityListRequest) } if len(r.Labels) > 0 { - var args []any - var conditions []string - for labelKey, labelValue := range r.Labels { - args = append(args, labelKey) - args = append(args, labelValue) - conditions = append(conditions, "(label = ? AND value = ?)") - } - query := "SELECT guid FROM entity_labels" + - " WHERE (" + strings.Join(conditions, " OR ") + ")" + - " GROUP BY guid" + - " HAVING COUNT(label) = ?" - args = append(args, len(r.Labels)) + // if we are looking for deleted entities, we need to use the labels column + if r.Deleted { + for labelKey, labelValue := range r.Labels { + entityQuery.addWhere(s.dialect.Quote("labels")+" LIKE ?", "%\""+labelKey+"\":\""+labelValue+"\"%") + } + // for active entities, we can use the entity_labels table + } else { + var args []any + var conditions []string + for labelKey, labelValue := range r.Labels { + args = append(args, labelKey) + args = append(args, labelValue) + conditions = append(conditions, "(label = ? AND value = ?)") + } + query := "SELECT guid FROM entity_labels" + + " WHERE (" + strings.Join(conditions, " OR ") + ")" + + " GROUP BY guid" + + " HAVING COUNT(label) = ?" + args = append(args, len(r.Labels)) - entityQuery.addWhereInSubquery("guid", query, args) + entityQuery.addWhereInSubquery("guid", query, args) + } } for _, sort := range r.Sort { sortBy, err := ParseSortBy(sort) @@ -1483,19 +1497,13 @@ func watchMatches(r *entity.EntityWatchRequest, result *entity.Entity) bool { } } - // must match at least one label/value pair if specified - // TODO should this require matching all label conditions? + // must match all specified label/value pairs if len(r.Labels) > 0 { - matched := false for labelKey, labelValue := range r.Labels { - if result.Labels[labelKey] == labelValue { - matched = true - break + if result.Labels[labelKey] != labelValue { + return false } } - if !matched { - return false - } } return true From efbcd53119efffd6e9509845f1d54b5369fcac1e Mon Sep 17 00:00:00 2001 From: David Harris Date: Mon, 11 Mar 2024 19:17:20 +0000 Subject: [PATCH 0532/1406] docs: update angular deprecation notice (#84227) update docs --- docs/sources/developers/angular_deprecation/_index.md | 8 +++++--- .../developers/angular_deprecation/angular-plugins.md | 1 - 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/sources/developers/angular_deprecation/_index.md b/docs/sources/developers/angular_deprecation/_index.md index 89b3037c1a..6799f2eebd 100644 --- a/docs/sources/developers/angular_deprecation/_index.md +++ b/docs/sources/developers/angular_deprecation/_index.md @@ -22,17 +22,19 @@ AngularJS is an old frontend framework whose active development stopped many yea ## When will Angular plugins stop working? -Our goal is to transfer all the remaining Angular code to the core of Grafana before Grafana 10 is released in Summer 2023. Once this is done, the option "[angular_support_enabled](https://github.com/grafana/grafana/blob/d61bcdf4ca5e69489e0067c56fbe7f0bfdf84ee4/conf/defaults.ini#L362)" will be disabled by default for new Grafana Cloud users, resulting in the inability to use Angular plugins. In case you still rely on AngularJS-based plugins developed internally or by the community, you will need to enable this option to continue using them. Following the release of Grafana 10 we will be migrating Grafana Cloud users where possible and disabling Angular support when appropriate, we will also be introducing new features to help all users identify how they are impacted and to warn of the use of deprecated plugins within the Grafana UI. +In Grafana 11, which will be released in preview in April 2024 and generally available in May, we will change the default behavior of the [angular_support_enabled](https://github.com/grafana/grafana/blob/d61bcdf4ca5e69489e0067c56fbe7f0bfdf84ee4/conf/defaults.ini#L362) configuration parameter to turn off support for AngularJS based plugins. In case you still rely on [AngularJS-based plugins]({{< relref "./angular-plugins/" >}}) developed internally or by the community, you will need to enable this option to continue using them. + +New Grafana Cloud users will be unable to request for support to be added to their instance. ## When will we remove Angular support completely? -Our plan is to completely remove support for Angular plugins in version 11, which will be released in 2024. This means that all plugins that depend on Angular will stop working and the temporary option introduced in version 10 to enable Angular will be removed. +Our current plan is to completely remove any remaining support for Angular plugins in version 12. Including the removal of the [angular_support_enabled](https://github.com/grafana/grafana/blob/d61bcdf4ca5e69489e0067c56fbe7f0bfdf84ee4/conf/defaults.ini#L362) configuration parameter. ## How do I migrate an Angular plugin to React? Depending on if it’s a data source plugin, panel plugin, or app plugin the process will differ. -For panels, the rendering logic could in some cases be easily preserved but all options need to be redone to use the declarative options framework. For data source plugins the query editor and config options will likely need a total rewrite. +For panels, the rendering logic could in some cases be easily preserved, but all options need to be redone to use the declarative options framework. For data source plugins the query editor and config options will likely need a total rewrite. ## How do I encourage a community plugin to migrate? diff --git a/docs/sources/developers/angular_deprecation/angular-plugins.md b/docs/sources/developers/angular_deprecation/angular-plugins.md index 377a7a61b8..d565cb5a2e 100644 --- a/docs/sources/developers/angular_deprecation/angular-plugins.md +++ b/docs/sources/developers/angular_deprecation/angular-plugins.md @@ -201,7 +201,6 @@ This table lists plugins which we have detected as having a dependency on Angula | stagemonitor-elasticsearch-app | stagemonitor Elasticsearch | Migrate - Browse included data sources and plugins catalog for potential alternatives. | | tdengine-datasource | TDengine Datasource | Update - Note the minimum version for React is 3.3.0. We recommend the latest. | | vertica-grafana-datasource | Vertica | Update - Note the minimum version for React is 2.0.0. We recommend the latest. | -| vonage-status-panel | Status Panel | Wait - Updated version may become available, or browse included visualizations and plugins catalog for potential alternatives. | | voxter-app | Voxter VoIP Platform Metrics | Migrate - Browse included data sources and plugins catalog for potential alternatives. | | graph | Graph (old) | Migrate - Note that this is replaced by [Time Series]({{< relref "../../panels-visualizations/visualizations/time-series" >}}) (core) - This plugin should migrate when Angular is disabled. Also consider Bar Chart or Histogram if appropriate. | | table-old | Table (old) | Migrate - Note that this is replaced by [Table]({{< relref "../../panels-visualizations/visualizations/table" >}}) (core) - This plugin should migrate when AngularJS is disabled. | From 0b2640e9ff150fb39587e2a167e1ed2efa3804d7 Mon Sep 17 00:00:00 2001 From: Oscar Kilhed Date: Mon, 11 Mar 2024 20:48:27 +0100 Subject: [PATCH 0533/1406] Dashboard scenes: Editing library panels. (#83223) * wip * Refactor find panel by key * clean up lint, make isLoading optional * change library panel so that the dashboard key is attached to the panel instead of the library panel * do not reload everything when the library panel is already loaded * Progress on library panel options in options pane * We can skip building the edit scene until we have the library panel loaded * undo changes to findLibraryPanelbyKey, changes not necessary when the panel has the findable id instead of the library panel * fix undo * make sure the save model gets the id from the panel and not the library panel * remove non necessary links and data providers from dummy loading panel * change library panel so that the dashboard key is attached to the panel instead of the library panel * make sure the save model gets the id from the panel and not the library panel * do not reload everything when the library panel is already loaded * Fix merge issue * Clean up * lint cleanup * wip saving * working save * use title from panel model * move library panel api functions * fix issue from merge * Add confirm save modal. Update library panel to response from save request. Add library panel information box to panel options * Better naming * Remove library panel from viz panel state, use sourcePanel.parent instead. Fix edited by time formatting * Add tests for editing library panels * implement changed from review feedback * minor refactor from feedback --- .../panel-edit/LibraryVizPanelInfo.tsx | 58 ++++++++++ .../panel-edit/PanelEditor.test.ts | 61 ++++++++++ .../panel-edit/PanelEditor.tsx | 22 +++- .../panel-edit/PanelEditorRenderer.tsx | 15 ++- .../panel-edit/PanelOptions.test.tsx | 63 ++++++++++ .../panel-edit/PanelOptions.tsx | 27 ++++- .../panel-edit/SaveLibraryVizPanelModal.tsx | 107 +++++++++++++++++ .../panel-edit/VizPanelManager.test.tsx | 46 +++++++- .../panel-edit/VizPanelManager.tsx | 36 ++++-- .../scene/DashboardSceneUrlSync.ts | 10 +- .../dashboard-scene/scene/LibraryVizPanel.tsx | 109 +++++++++--------- .../scene/NavToolbarActions.tsx | 44 ++++++- .../scene/PanelMenuBehavior.tsx | 3 +- .../features/dashboard-scene/utils/utils.ts | 7 +- .../PanelEditor/getVisualizationOptions.tsx | 42 ++++++- .../app/features/library-panels/state/api.ts | 27 +++++ 16 files changed, 596 insertions(+), 81 deletions(-) create mode 100644 public/app/features/dashboard-scene/panel-edit/LibraryVizPanelInfo.tsx create mode 100644 public/app/features/dashboard-scene/panel-edit/PanelOptions.test.tsx create mode 100644 public/app/features/dashboard-scene/panel-edit/SaveLibraryVizPanelModal.tsx diff --git a/public/app/features/dashboard-scene/panel-edit/LibraryVizPanelInfo.tsx b/public/app/features/dashboard-scene/panel-edit/LibraryVizPanelInfo.tsx new file mode 100644 index 0000000000..bd741ee48f --- /dev/null +++ b/public/app/features/dashboard-scene/panel-edit/LibraryVizPanelInfo.tsx @@ -0,0 +1,58 @@ +import { css } from '@emotion/css'; +import React from 'react'; + +import { GrafanaTheme2, dateTimeFormat } from '@grafana/data'; +import { useStyles2 } from '@grafana/ui'; + +import { LibraryVizPanel } from '../scene/LibraryVizPanel'; + +interface Props { + libraryPanel: LibraryVizPanel; +} + +export const LibraryVizPanelInfo = ({ libraryPanel }: Props) => { + const styles = useStyles2(getStyles); + + const libraryPanelState = libraryPanel.useState(); + const tz = libraryPanelState.$timeRange?.getTimeZone(); + const meta = libraryPanelState._loadedPanel?.meta; + if (!meta) { + return null; + } + + return ( +
+
+ {`Used on ${meta.connectedDashboards} `} + {meta.connectedDashboards === 1 ? 'dashboard' : 'dashboards'} +
+
+ {dateTimeFormat(meta.updated, { format: 'L', timeZone: tz })} by + {meta.updatedBy.avatarUrl && ( + {`Avatar + )} + {meta.updatedBy.name} +
+
+ ); +}; + +const getStyles = (theme: GrafanaTheme2) => { + return { + info: css({ + lineHeight: 1, + }), + libraryPanelInfo: css({ + color: theme.colors.text.secondary, + fontSize: theme.typography.bodySmall.fontSize, + }), + userAvatar: css({ + borderRadius: theme.shape.radius.circle, + boxSizing: 'content-box', + width: '22px', + height: '22px', + paddingLeft: theme.spacing(1), + paddingRight: theme.spacing(1), + }), + }; +}; diff --git a/public/app/features/dashboard-scene/panel-edit/PanelEditor.test.ts b/public/app/features/dashboard-scene/panel-edit/PanelEditor.test.ts index fe93e589fb..ff8c44c215 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelEditor.test.ts +++ b/public/app/features/dashboard-scene/panel-edit/PanelEditor.test.ts @@ -1,7 +1,10 @@ import { PanelPlugin, PanelPluginMeta, PluginType } from '@grafana/data'; import { SceneGridItem, SceneGridLayout, VizPanel } from '@grafana/scenes'; +import * as libAPI from 'app/features/library-panels/state/api'; import { DashboardScene } from '../scene/DashboardScene'; +import { LibraryVizPanel } from '../scene/LibraryVizPanel'; +import { vizPanelToPanel } from '../serialization/transformSceneToSaveModel'; import { activateFullSceneTree } from '../utils/test-utils'; import { buildPanelEditScene } from './PanelEditor'; @@ -56,6 +59,64 @@ describe('PanelEditor', () => { }); }); + describe('Handling library panels', () => { + it('should call the api with the updated panel', async () => { + pluginToLoad = getTestPanelPlugin({ id: 'text', skipDataQuery: true }); + const panel = new VizPanel({ + key: 'panel-1', + pluginId: 'text', + }); + + const libraryPanelModel = { + title: 'title', + uid: 'uid', + name: 'libraryPanelName', + model: vizPanelToPanel(panel), + type: 'panel', + version: 1, + }; + + const libraryPanel = new LibraryVizPanel({ + isLoaded: true, + title: libraryPanelModel.title, + uid: libraryPanelModel.uid, + name: libraryPanelModel.name, + panelKey: panel.state.key!, + panel: panel, + _loadedPanel: libraryPanelModel, + }); + + const apiCall = jest + .spyOn(libAPI, 'updateLibraryVizPanel') + .mockResolvedValue({ type: 'panel', ...libAPI.libraryVizPanelToSaveModel(libraryPanel), version: 2 }); + + const editScene = buildPanelEditScene(panel); + const gridItem = new SceneGridItem({ body: libraryPanel }); + const scene = new DashboardScene({ + editPanel: editScene, + isEditing: true, + body: new SceneGridLayout({ + children: [gridItem], + }), + }); + + activateFullSceneTree(scene); + + editScene.state.vizManager.state.panel.setState({ title: 'changed title' }); + (editScene.state.vizManager.state.sourcePanel.resolve().parent as LibraryVizPanel).setState({ + name: 'changed name', + }); + editScene.state.vizManager.commitChanges(); + + const calledWith = apiCall.mock.calls[0][0].state; + expect(calledWith.panel?.state.title).toBe('changed title'); + expect(calledWith.name).toBe('changed name'); + + await new Promise(process.nextTick); // Wait for mock api to return and update the library panel + expect((gridItem.state.body as LibraryVizPanel).state._loadedPanel?.version).toBe(2); + }); + }); + describe('PanelDataPane', () => { it('should not exist if panel is skipDataQuery', () => { pluginToLoad = getTestPanelPlugin({ id: 'text', skipDataQuery: true }); diff --git a/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx b/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx index ad1494a38e..7d2a1ceee2 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx @@ -4,8 +4,9 @@ import { NavIndex } from '@grafana/data'; import { config, locationService } from '@grafana/runtime'; import { SceneGridItem, SceneGridLayout, SceneObjectBase, SceneObjectState, VizPanel } from '@grafana/scenes'; +import { LibraryVizPanel } from '../scene/LibraryVizPanel'; import { PanelRepeaterGridItem } from '../scene/PanelRepeaterGridItem'; -import { getPanelIdForVizPanel, getDashboardSceneFor } from '../utils/utils'; +import { getDashboardSceneFor, getPanelIdForVizPanel } from '../utils/utils'; import { PanelDataPane } from './PanelDataPane/PanelDataPane'; import { PanelEditorRenderer } from './PanelEditorRenderer'; @@ -18,6 +19,7 @@ export interface PanelEditorState extends SceneObjectState { optionsPane: PanelOptionsPane; dataPane?: PanelDataPane; vizManager: VizPanelManager; + showLibraryPanelSaveModal?: boolean; } export class PanelEditor extends SceneObjectBase { @@ -105,7 +107,10 @@ export class PanelEditor extends SceneObjectBase { const normalToRepeat = !this._initialRepeatOptions.repeat && panelManager.state.repeat; const repeatToNormal = this._initialRepeatOptions.repeat && !panelManager.state.repeat; - if (sourcePanelParent instanceof SceneGridItem) { + if (sourcePanelParent instanceof LibraryVizPanel) { + // Library panels handled separately + return; + } else if (sourcePanelParent instanceof SceneGridItem) { if (normalToRepeat) { this.replaceSceneGridItemWithPanelRepeater(sourcePanelParent); } else { @@ -197,6 +202,19 @@ export class PanelEditor extends SceneObjectBase { height, }); } + + public onSaveLibraryPanel = () => { + this.setState({ showLibraryPanelSaveModal: true }); + }; + + public onConfirmSaveLibraryPanel = () => { + this.state.vizManager.commitChanges(); + locationService.partial({ editPanel: null }); + }; + + public onDismissLibraryPanelModal = () => { + this.setState({ showLibraryPanelSaveModal: false }); + }; } export function buildPanelEditScene(panel: VizPanel): PanelEditor { diff --git a/public/app/features/dashboard-scene/panel-edit/PanelEditorRenderer.tsx b/public/app/features/dashboard-scene/panel-edit/PanelEditorRenderer.tsx index 9850f78751..38270d798f 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelEditorRenderer.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelEditorRenderer.tsx @@ -6,9 +6,10 @@ import { SceneComponentProps } from '@grafana/scenes'; import { Button, ToolbarButton, useStyles2 } from '@grafana/ui'; import { NavToolbarActions } from '../scene/NavToolbarActions'; -import { getDashboardSceneFor } from '../utils/utils'; +import { getDashboardSceneFor, getLibraryPanel } from '../utils/utils'; import { PanelEditor } from './PanelEditor'; +import { SaveLibraryVizPanelModal } from './SaveLibraryVizPanelModal'; import { useSnappingSplitter } from './splitter/useSnappingSplitter'; export function PanelEditorRenderer({ model }: SceneComponentProps) { @@ -57,7 +58,9 @@ export function PanelEditorRenderer({ model }: SceneComponentProps) function VizAndDataPane({ model }: SceneComponentProps) { const dashboard = getDashboardSceneFor(model); - const { vizManager, dataPane } = model.useState(); + const { vizManager, dataPane, showLibraryPanelSaveModal } = model.useState(); + const { sourcePanel } = vizManager.useState(); + const libraryPanel = getLibraryPanel(sourcePanel.resolve()); const { controls } = dashboard.useState(); const styles = useStyles2(getStyles); @@ -82,6 +85,14 @@ function VizAndDataPane({ model }: SceneComponentProps) {
+ {showLibraryPanelSaveModal && libraryPanel && ( + + )} {dataPane && ( <>
diff --git a/public/app/features/dashboard-scene/panel-edit/PanelOptions.test.tsx b/public/app/features/dashboard-scene/panel-edit/PanelOptions.test.tsx new file mode 100644 index 0000000000..3b03ce0e09 --- /dev/null +++ b/public/app/features/dashboard-scene/panel-edit/PanelOptions.test.tsx @@ -0,0 +1,63 @@ +import { act, fireEvent, render } from '@testing-library/react'; +import React from 'react'; + +import { SceneGridItem, VizPanel } from '@grafana/scenes'; +import { OptionFilter } from 'app/features/dashboard/components/PanelEditor/OptionsPaneOptions'; + +import { LibraryVizPanel } from '../scene/LibraryVizPanel'; +import { vizPanelToPanel } from '../serialization/transformSceneToSaveModel'; + +import { PanelOptions } from './PanelOptions'; +import { VizPanelManager } from './VizPanelManager'; + +jest.mock('react-router-dom', () => ({ + useLocation: () => ({ + pathname: '', + }), +})); + +describe('PanelOptions', () => { + it('gets library panel options when the editing a library panel', async () => { + const panel = new VizPanel({ + key: 'panel-1', + pluginId: 'text', + }); + + const libraryPanelModel = { + title: 'title', + uid: 'uid', + name: 'libraryPanelName', + model: vizPanelToPanel(panel), + type: 'panel', + version: 1, + }; + + const libraryPanel = new LibraryVizPanel({ + isLoaded: true, + title: libraryPanelModel.title, + uid: libraryPanelModel.uid, + name: libraryPanelModel.name, + panelKey: panel.state.key!, + panel: panel, + _loadedPanel: libraryPanelModel, + }); + + new SceneGridItem({ body: libraryPanel }); + + const panelManger = VizPanelManager.createFor(panel); + + const panelOptions = ( + + ); + + const r = render(panelOptions); + const input = await r.findByTestId('library panel name input'); + await act(async () => { + fireEvent.blur(input, { target: { value: 'new library panel name' } }); + }); + + expect((panelManger.state.sourcePanel.resolve().parent as LibraryVizPanel).state.name).toBe( + 'new library panel name' + ); + }); +}); diff --git a/public/app/features/dashboard-scene/panel-edit/PanelOptions.tsx b/public/app/features/dashboard-scene/panel-edit/PanelOptions.tsx index 988a7db4c9..e9088b3453 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelOptions.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelOptions.tsx @@ -4,7 +4,12 @@ import { sceneGraph } from '@grafana/scenes'; import { OptionFilter, renderSearchHits } from 'app/features/dashboard/components/PanelEditor/OptionsPaneOptions'; import { getFieldOverrideCategories } from 'app/features/dashboard/components/PanelEditor/getFieldOverrideElements'; import { getPanelFrameCategory2 } from 'app/features/dashboard/components/PanelEditor/getPanelFrameOptions'; -import { getVisualizationOptions2 } from 'app/features/dashboard/components/PanelEditor/getVisualizationOptions'; +import { + getLibraryVizPanelOptionsCategory, + getVisualizationOptions2, +} from 'app/features/dashboard/components/PanelEditor/getVisualizationOptions'; + +import { LibraryVizPanel } from '../scene/LibraryVizPanel'; import { VizPanelManager } from './VizPanelManager'; @@ -15,7 +20,8 @@ interface Props { } export const PanelOptions = React.memo(({ vizManager, searchQuery, listMode }) => { - const { panel, repeat } = vizManager.useState(); + const { panel, sourcePanel, repeat } = vizManager.useState(); + const parent = sourcePanel.resolve().parent; const { data } = sceneGraph.getData(panel).useState(); const { options, fieldConfig } = panel.useState(); @@ -40,6 +46,13 @@ export const PanelOptions = React.memo(({ vizManager, searchQuery, listMo // eslint-disable-next-line react-hooks/exhaustive-deps }, [panel, options, fieldConfig]); + const libraryPanelOptions = useMemo(() => { + if (parent instanceof LibraryVizPanel) { + return getLibraryVizPanelOptionsCategory(parent); + } + return; + }, [parent]); + const justOverrides = useMemo( () => getFieldOverrideCategories( @@ -62,11 +75,19 @@ export const PanelOptions = React.memo(({ vizManager, searchQuery, listMo if (isSearching) { mainBoxElements.push( - renderSearchHits([panelFrameOptions, ...(visualizationOptions ?? [])], justOverrides, searchQuery) + renderSearchHits( + [panelFrameOptions, ...(libraryPanelOptions ? [libraryPanelOptions] : []), ...(visualizationOptions ?? [])], + justOverrides, + searchQuery + ) ); } else { switch (listMode) { case OptionFilter.All: + if (libraryPanelOptions) { + // Library Panel options first + mainBoxElements.push(libraryPanelOptions.render()); + } mainBoxElements.push(panelFrameOptions.render()); for (const item of visualizationOptions ?? []) { diff --git a/public/app/features/dashboard-scene/panel-edit/SaveLibraryVizPanelModal.tsx b/public/app/features/dashboard-scene/panel-edit/SaveLibraryVizPanelModal.tsx new file mode 100644 index 0000000000..d1178a76aa --- /dev/null +++ b/public/app/features/dashboard-scene/panel-edit/SaveLibraryVizPanelModal.tsx @@ -0,0 +1,107 @@ +import React, { useCallback, useState } from 'react'; +import { useAsync, useDebounce } from 'react-use'; + +import { Button, Icon, Input, Modal, useStyles2 } from '@grafana/ui'; +import { getConnectedDashboards } from 'app/features/library-panels/state/api'; +import { getModalStyles } from 'app/features/library-panels/styles'; + +import { LibraryVizPanel } from '../scene/LibraryVizPanel'; + +interface Props { + libraryPanel: LibraryVizPanel; + isUnsavedPrompt?: boolean; + onConfirm: () => void; + onDismiss: () => void; + onDiscard: () => void; +} + +export const SaveLibraryVizPanelModal = ({ libraryPanel, isUnsavedPrompt, onDismiss, onConfirm, onDiscard }: Props) => { + const [searchString, setSearchString] = useState(''); + const dashState = useAsync(async () => { + const searchHits = await getConnectedDashboards(libraryPanel.state.uid); + if (searchHits.length > 0) { + return searchHits.map((dash) => dash.title); + } + + return []; + }, [libraryPanel.state.uid]); + + const [filteredDashboards, setFilteredDashboards] = useState([]); + useDebounce( + () => { + if (!dashState.value) { + return setFilteredDashboards([]); + } + + return setFilteredDashboards( + dashState.value.filter((dashName) => dashName.toLowerCase().includes(searchString.toLowerCase())) + ); + }, + 300, + [dashState.value, searchString] + ); + + const styles = useStyles2(getModalStyles); + const discardAndClose = useCallback(() => { + onDiscard(); + }, [onDiscard]); + + const title = isUnsavedPrompt ? 'Unsaved library panel changes' : 'Save library panel'; + + return ( + +
+

+ {'This update will affect '} + + {libraryPanel.state._loadedPanel?.meta?.connectedDashboards}{' '} + {libraryPanel.state._loadedPanel?.meta?.connectedDashboards === 1 ? 'dashboard' : 'dashboards'}. + + The following dashboards using the panel will be affected: +

+ } + placeholder="Search affected dashboards" + value={searchString} + onChange={(e) => setSearchString(e.currentTarget.value)} + /> + {dashState.loading ? ( +

Loading connected dashboards...

+ ) : ( + + + + + + + + {filteredDashboards.map((dashName, i) => ( + + + + ))} + +
Dashboard name
{dashName}
+ )} + + + {isUnsavedPrompt && ( + + )} + + +
+
+ ); +}; diff --git a/public/app/features/dashboard-scene/panel-edit/VizPanelManager.test.tsx b/public/app/features/dashboard-scene/panel-edit/VizPanelManager.test.tsx index a33670c822..3e13b91c3b 100644 --- a/public/app/features/dashboard-scene/panel-edit/VizPanelManager.test.tsx +++ b/public/app/features/dashboard-scene/panel-edit/VizPanelManager.test.tsx @@ -2,15 +2,18 @@ import { map, of } from 'rxjs'; import { DataQueryRequest, DataSourceApi, DataSourceInstanceSettings, LoadingState, PanelData } from '@grafana/data'; import { locationService } from '@grafana/runtime'; -import { SceneQueryRunner, VizPanel } from '@grafana/scenes'; +import { SceneGridItem, SceneQueryRunner, VizPanel } from '@grafana/scenes'; import { DataQuery, DataSourceJsonData, DataSourceRef } from '@grafana/schema'; import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; import { InspectTab } from 'app/features/inspector/types'; +import * as libAPI from 'app/features/library-panels/state/api'; import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard'; import { DASHBOARD_DATASOURCE_PLUGIN_ID } from 'app/plugins/datasource/dashboard/types'; +import { LibraryVizPanel } from '../scene/LibraryVizPanel'; import { PanelTimeRange, PanelTimeRangeState } from '../scene/PanelTimeRange'; import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene'; +import { vizPanelToPanel } from '../serialization/transformSceneToSaveModel'; import { DashboardModelCompatibilityWrapper } from '../utils/DashboardModelCompatibilityWrapper'; import { findVizPanelByKey } from '../utils/utils'; @@ -208,6 +211,47 @@ describe('VizPanelManager', () => { }); }); + describe('library panels', () => { + it('saves library panels on commit', () => { + const panel = new VizPanel({ + key: 'panel-1', + pluginId: 'text', + }); + + const libraryPanelModel = { + title: 'title', + uid: 'uid', + name: 'libraryPanelName', + model: vizPanelToPanel(panel), + type: 'panel', + version: 1, + }; + + const libraryPanel = new LibraryVizPanel({ + isLoaded: true, + title: libraryPanelModel.title, + uid: libraryPanelModel.uid, + name: libraryPanelModel.name, + panelKey: panel.state.key!, + panel: panel, + _loadedPanel: libraryPanelModel, + }); + + new SceneGridItem({ body: libraryPanel }); + + const panelManager = VizPanelManager.createFor(panel); + + const apiCall = jest + .spyOn(libAPI, 'updateLibraryVizPanel') + .mockResolvedValue({ type: 'panel', ...libAPI.libraryVizPanelToSaveModel(libraryPanel) }); + + panelManager.state.panel.setState({ title: 'new title' }); + panelManager.commitChanges(); + + expect(apiCall.mock.calls[0][0].state.panel?.state.title).toBe('new title'); + }); + }); + describe('query options', () => { beforeEach(() => { store.setObject.mockClear(); diff --git a/public/app/features/dashboard-scene/panel-edit/VizPanelManager.tsx b/public/app/features/dashboard-scene/panel-edit/VizPanelManager.tsx index f2433db7f9..5f8c78e5b6 100644 --- a/public/app/features/dashboard-scene/panel-edit/VizPanelManager.tsx +++ b/public/app/features/dashboard-scene/panel-edit/VizPanelManager.tsx @@ -13,28 +13,30 @@ import { } from '@grafana/data'; import { config, getDataSourceSrv, locationService } from '@grafana/runtime'; import { - SceneObjectState, - VizPanel, - SceneObjectBase, - SceneComponentProps, - sceneUtils, DeepPartial, - SceneQueryRunner, - sceneGraph, - SceneDataTransformer, PanelBuilders, + SceneComponentProps, + SceneDataTransformer, SceneGridItem, + SceneObjectBase, SceneObjectRef, + SceneObjectState, + SceneQueryRunner, + VizPanel, + sceneGraph, + sceneUtils, } from '@grafana/scenes'; import { DataQuery, DataTransformerConfig, Panel } from '@grafana/schema'; import { useStyles2 } from '@grafana/ui'; import { getPluginVersion } from 'app/features/dashboard/state/PanelModel'; import { getLastUsedDatasourceFromStorage } from 'app/features/dashboard/utils/dashboard'; import { storeLastUsedDataSourceInLocalStorage } from 'app/features/datasources/components/picker/utils'; +import { updateLibraryVizPanel } from 'app/features/library-panels/state/api'; import { updateQueries } from 'app/features/query/state/updateQueries'; import { GrafanaQuery } from 'app/plugins/datasource/grafana/types'; import { QueryGroupOptions } from 'app/types'; +import { LibraryVizPanel } from '../scene/LibraryVizPanel'; import { PanelRepeaterGridItem, RepeatDirection } from '../scene/PanelRepeaterGridItem'; import { PanelTimeRange, PanelTimeRangeState } from '../scene/PanelTimeRange'; import { gridItemToPanel } from '../serialization/transformSceneToSaveModel'; @@ -346,6 +348,24 @@ export class VizPanelManager extends SceneObjectBase { }), }); } + + if (sourcePanel.parent instanceof LibraryVizPanel) { + if (sourcePanel.parent.parent instanceof SceneGridItem) { + const newLibPanel = sourcePanel.parent.clone({ + panel: this.state.panel.clone({ + $data: this.state.$data?.clone(), + }), + }); + sourcePanel.parent.parent.setState({ + body: newLibPanel, + }); + updateLibraryVizPanel(newLibPanel!).then((p) => { + if (sourcePanel.parent instanceof LibraryVizPanel) { + newLibPanel.setPanelFromLibPanel(p); + } + }); + } + } } /** diff --git a/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts b/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts index fb02736233..d46bf382f6 100644 --- a/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts +++ b/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts @@ -14,7 +14,7 @@ import appEvents from 'app/core/app_events'; import { PanelInspectDrawer } from '../inspect/PanelInspectDrawer'; import { buildPanelEditScene } from '../panel-edit/PanelEditor'; import { createDashboardEditViewFor } from '../settings/utils'; -import { findVizPanelByKey, getDashboardSceneFor, isLibraryPanelChild, isPanelClone } from '../utils/utils'; +import { findVizPanelByKey, getDashboardSceneFor, getLibraryPanel, isPanelClone } from '../utils/utils'; import { DashboardScene, DashboardSceneState } from './DashboardScene'; import { LibraryVizPanel } from './LibraryVizPanel'; @@ -66,7 +66,7 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler { return; } - if (isLibraryPanelChild(panel)) { + if (getLibraryPanel(panel)) { this._handleLibraryPanel(panel, (p) => { if (p.state.key === undefined) { // Inspect drawer require a panel key to be set @@ -105,7 +105,7 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler { return; } - if (isLibraryPanelChild(panel)) { + if (getLibraryPanel(panel)) { this._handleLibraryPanel(panel, (p) => this._buildLibraryPanelViewScene(p)); return; } @@ -119,6 +119,7 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler { if (typeof values.editPanel === 'string') { const panel = findVizPanelByKey(this._scene, values.editPanel); if (!panel) { + console.warn(`Panel ${values.editPanel} not found`); return; } @@ -126,10 +127,11 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler { if (!isEditing) { this._scene.onEnterEditMode(); } - if (isLibraryPanelChild(panel)) { + if (getLibraryPanel(panel)) { this._handleLibraryPanel(panel, (p) => { this._scene.setState({ editPanel: buildPanelEditScene(p) }); }); + return; } update.editPanel = buildPanelEditScene(panel); } else if (editPanel && values.editPanel === null) { diff --git a/public/app/features/dashboard-scene/scene/LibraryVizPanel.tsx b/public/app/features/dashboard-scene/scene/LibraryVizPanel.tsx index b7692f08a9..6efd02644e 100644 --- a/public/app/features/dashboard-scene/scene/LibraryVizPanel.tsx +++ b/public/app/features/dashboard-scene/scene/LibraryVizPanel.tsx @@ -51,64 +51,67 @@ export class LibraryVizPanel extends SceneObjectBase { } }; + public setPanelFromLibPanel(libPanel: LibraryPanel) { + if (this.state._loadedPanel?.version === libPanel.version) { + return; + } + + const libPanelModel = new PanelModel(libPanel.model); + + const vizPanelState: VizPanelState = { + title: libPanelModel.title, + key: this.state.panelKey, + options: libPanelModel.options ?? {}, + fieldConfig: libPanelModel.fieldConfig, + pluginId: libPanelModel.type, + pluginVersion: libPanelModel.pluginVersion, + displayMode: libPanelModel.transparent ? 'transparent' : undefined, + description: libPanelModel.description, + $data: createPanelDataProvider(libPanelModel), + menu: new VizPanelMenu({ $behaviors: [panelMenuBehavior] }), + titleItems: [ + new VizPanelLinks({ + rawLinks: libPanelModel.links, + menu: new VizPanelLinksMenu({ $behaviors: [panelLinksBehavior] }), + }), + new PanelNotices(), + ], + }; + + const panel = new VizPanel(vizPanelState); + const gridItem = this.parent; + + if (libPanelModel.repeat && gridItem instanceof SceneGridItem && gridItem.parent instanceof SceneGridLayout) { + this._parent = undefined; + const repeater = new PanelRepeaterGridItem({ + key: gridItem.state.key, + x: gridItem.state.x, + y: gridItem.state.y, + width: libPanelModel.repeatDirection === 'h' ? 24 : gridItem.state.width, + height: gridItem.state.height, + itemHeight: gridItem.state.height, + source: this, + variableName: libPanelModel.repeat, + repeatedPanels: [], + repeatDirection: libPanelModel.repeatDirection === 'h' ? 'h' : 'v', + maxPerRow: libPanelModel.maxPerRow, + }); + gridItem.parent.setState({ + children: gridItem.parent.state.children.map((child) => + child.state.key === gridItem.state.key ? repeater : child + ), + }); + } + + this.setState({ panel, _loadedPanel: libPanel, isLoaded: true, name: libPanel.name }); + } + private async loadLibraryPanelFromPanelModel() { let vizPanel = this.state.panel!; try { const libPanel = await getLibraryPanel(this.state.uid, true); - - if (this.state._loadedPanel?.version === libPanel.version) { - return; - } - - const libPanelModel = new PanelModel(libPanel.model); - - const vizPanelState: VizPanelState = { - title: this.state.title, - key: this.state.panelKey, - options: libPanelModel.options ?? {}, - fieldConfig: libPanelModel.fieldConfig, - pluginId: libPanelModel.type, - pluginVersion: libPanelModel.pluginVersion, - displayMode: libPanelModel.transparent ? 'transparent' : undefined, - description: libPanelModel.description, - $data: createPanelDataProvider(libPanelModel), - menu: new VizPanelMenu({ $behaviors: [panelMenuBehavior] }), - titleItems: [ - new VizPanelLinks({ - rawLinks: libPanelModel.links, - menu: new VizPanelLinksMenu({ $behaviors: [panelLinksBehavior] }), - }), - new PanelNotices(), - ], - }; - - const panel = new VizPanel(vizPanelState); - const gridItem = this.parent; - - if (libPanelModel.repeat && gridItem instanceof SceneGridItem && gridItem.parent instanceof SceneGridLayout) { - this._parent = undefined; - const repeater = new PanelRepeaterGridItem({ - key: gridItem.state.key, - x: gridItem.state.x, - y: gridItem.state.y, - width: libPanelModel.repeatDirection === 'h' ? 24 : gridItem.state.width, - height: gridItem.state.height, - itemHeight: gridItem.state.height, - source: this, - variableName: libPanelModel.repeat, - repeatedPanels: [], - repeatDirection: libPanelModel.repeatDirection === 'h' ? 'h' : 'v', - maxPerRow: libPanelModel.maxPerRow, - }); - gridItem.parent.setState({ - children: gridItem.parent.state.children.map((child) => - child.state.key === gridItem.state.key ? repeater : child - ), - }); - } - - this.setState({ panel, _loadedPanel: libPanel, isLoaded: true }); + this.setPanelFromLibPanel(libPanel); } catch (err) { vizPanel.setState({ _pluginLoadError: `Unable to load library panel: ${this.state.uid}`, diff --git a/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx b/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx index 31a30316fd..9bfd56f155 100644 --- a/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx +++ b/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx @@ -18,6 +18,7 @@ import { dynamicDashNavActions } from '../utils/registerDynamicDashNavAction'; import { DashboardScene } from './DashboardScene'; import { GoToSnapshotOriginButton } from './GoToSnapshotOriginButton'; +import { LibraryVizPanel } from './LibraryVizPanel'; interface Props { dashboard: DashboardScene; @@ -56,6 +57,9 @@ export function ToolbarActions({ dashboard }: Props) { const buttonWithExtraMargin = useStyles2(getStyles); const isEditingPanel = Boolean(editPanel); const isViewingPanel = Boolean(viewPanelScene); + const isEditingLibraryPanel = Boolean( + editPanel?.state.vizManager.state.sourcePanel.resolve().parent instanceof LibraryVizPanel + ); const hasCopiedPanel = Boolean(copiedPanel); toolbarActions.push({ @@ -233,7 +237,7 @@ export function ToolbarActions({ dashboard }: Props) { toolbarActions.push({ group: 'back-button', - condition: isViewingPanel || isEditingPanel, + condition: (isViewingPanel || isEditingPanel) && !isEditingLibraryPanel, render: () => ( + ), + }); + + toolbarActions.push({ + group: 'main-buttons', + condition: isEditingPanel && isEditingLibraryPanel && !editview && !isViewingPanel, + render: () => ( + + ), + }); + + toolbarActions.push({ + group: 'main-buttons', + condition: isEditing && !isEditingLibraryPanel && (meta.canSave || canSaveAs), render: () => { // if we only can save if (meta.isNew) { diff --git a/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx b/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx index de31e62467..756ccb9258 100644 --- a/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx +++ b/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx @@ -43,7 +43,6 @@ export function panelMenuBehavior(menu: VizPanelMenu, isRepeat = false) { const items: PanelMenuItem[] = []; const moreSubMenu: PanelMenuItem[] = []; - const panelId = getPanelIdForVizPanel(panel); const dashboard = getDashboardSceneFor(panel); const { isEmbedded } = dashboard.state.meta; const exploreMenuItem = await getExploreMenuItem(panel); @@ -72,7 +71,7 @@ export function panelMenuBehavior(menu: VizPanelMenu, isRepeat = false) { iconClassName: 'eye', shortcut: 'e', onClick: () => DashboardInteractions.panelMenuItemClicked('edit'), - href: getEditPanelUrl(panelId), + href: getEditPanelUrl(getPanelIdForVizPanel(panel)), }); } diff --git a/public/app/features/dashboard-scene/utils/utils.ts b/public/app/features/dashboard-scene/utils/utils.ts index 00d50ee41d..780c4b0445 100644 --- a/public/app/features/dashboard-scene/utils/utils.ts +++ b/public/app/features/dashboard-scene/utils/utils.ts @@ -239,6 +239,9 @@ export function getDefaultRow(dashboard: DashboardScene): SceneGridRow { }); } -export function isLibraryPanelChild(vizPanel: VizPanel) { - return vizPanel.parent instanceof LibraryVizPanel; +export function getLibraryPanel(vizPanel: VizPanel): LibraryVizPanel | undefined { + if (vizPanel.parent instanceof LibraryVizPanel) { + return vizPanel.parent; + } + return; } diff --git a/public/app/features/dashboard/components/PanelEditor/getVisualizationOptions.tsx b/public/app/features/dashboard/components/PanelEditor/getVisualizationOptions.tsx index 5493b47225..ebcd9658a9 100644 --- a/public/app/features/dashboard/components/PanelEditor/getVisualizationOptions.tsx +++ b/public/app/features/dashboard/components/PanelEditor/getVisualizationOptions.tsx @@ -11,11 +11,14 @@ import { } from '@grafana/data'; import { PanelOptionsSupplier } from '@grafana/data/src/panel/PanelPlugin'; import { - isNestedPanelOptions, NestedValueAccess, PanelOptionsEditorBuilder, + isNestedPanelOptions, } from '@grafana/data/src/utils/OptionsUIBuilders'; import { VizPanel } from '@grafana/scenes'; +import { Input } from '@grafana/ui'; +import { LibraryVizPanelInfo } from 'app/features/dashboard-scene/panel-edit/LibraryVizPanelInfo'; +import { LibraryVizPanel } from 'app/features/dashboard-scene/scene/LibraryVizPanel'; import { getDataLinksVariableSuggestions } from 'app/features/panel/panellinks/link_srv'; import { OptionsPaneCategoryDescriptor } from './OptionsPaneCategoryDescriptor'; @@ -148,6 +151,43 @@ export function getVisualizationOptions(props: OptionPaneRenderProps): OptionsPa return Object.values(categoryIndex); } +export function getLibraryVizPanelOptionsCategory(libraryPanel: LibraryVizPanel): OptionsPaneCategoryDescriptor { + const descriptor = new OptionsPaneCategoryDescriptor({ + title: 'Library panel options', + id: 'Library panel options', + isOpenDefault: true, + }); + + descriptor + .addItem( + new OptionsPaneItemDescriptor({ + title: 'Name', + value: libraryPanel, + popularRank: 1, + render: function renderName() { + return ( + libraryPanel.setState({ name: e.currentTarget.value })} + /> + ); + }, + }) + ) + .addItem( + new OptionsPaneItemDescriptor({ + title: 'Information', + render: function renderLibraryPanelInformation() { + return ; + }, + }) + ); + + return descriptor; +} + export interface OptionPaneRenderProps2 { panel: VizPanel; eventBus: EventBus; diff --git a/public/app/features/library-panels/state/api.ts b/public/app/features/library-panels/state/api.ts index 1b3b77a027..cf224b52bc 100644 --- a/public/app/features/library-panels/state/api.ts +++ b/public/app/features/library-panels/state/api.ts @@ -2,6 +2,8 @@ import { lastValueFrom } from 'rxjs'; import { defaultDashboard } from '@grafana/schema'; import { DashboardModel } from 'app/features/dashboard/state'; +import { LibraryVizPanel } from 'app/features/dashboard-scene/scene/LibraryVizPanel'; +import { vizPanelToPanel } from 'app/features/dashboard-scene/serialization/transformSceneToSaveModel'; import { getBackendSrv } from '../../../core/services/backend_srv'; import { DashboardSearchItem } from '../../search/types'; @@ -133,3 +135,28 @@ export async function getConnectedDashboards(uid: string): Promise { + const { uid, folderUID, name, model, version, kind } = libraryVizPanelToSaveModel(libraryPanel); + const { result } = await getBackendSrv().patch(`/api/library-elements/${uid}`, { + folderUID, + name, + model, + version, + kind, + }); + return result; +} From 6c5e94095dd4783df0a9475337869de226faf3c1 Mon Sep 17 00:00:00 2001 From: Alexander Weaver Date: Mon, 11 Mar 2024 15:57:38 -0500 Subject: [PATCH 0534/1406] Alerting: Scheduler and registry handle rules by an interface (#84044) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * export Evaluation * Export Evaluation * Export RuleVersionAndPauseStatus * export Eval, create interface * Export update and add to interface * Export Stop and Run and add to interface * Registry and scheduler use rule by interface and not concrete type * Update factory to use interface, update tests to work over public API rather than writing to channels directly * Rename map in registry * Rename getOrCreateInfo to not reference a specific implementation * Genericize alertRuleInfoRegistry into ruleRegistry * Rename alertRuleInfo to alertRule * Comments on interface * Update pkg/services/ngalert/schedule/schedule.go Co-authored-by: Jean-Philippe Quéméner --------- Co-authored-by: Jean-Philippe Quéméner --- pkg/services/ngalert/schedule/alert_rule.go | 61 +++++---- .../ngalert/schedule/alert_rule_test.go | 121 +++++++++--------- .../ngalert/schedule/loaded_metrics_reader.go | 2 +- pkg/services/ngalert/schedule/registry.go | 50 ++++---- pkg/services/ngalert/schedule/schedule.go | 28 ++-- .../ngalert/schedule/schedule_unit_test.go | 4 +- 6 files changed, 142 insertions(+), 124 deletions(-) diff --git a/pkg/services/ngalert/schedule/alert_rule.go b/pkg/services/ngalert/schedule/alert_rule.go index 88642aaf15..3b9e479484 100644 --- a/pkg/services/ngalert/schedule/alert_rule.go +++ b/pkg/services/ngalert/schedule/alert_rule.go @@ -23,9 +23,22 @@ import ( "go.opentelemetry.io/otel/trace" ) -type ruleFactoryFunc func(context.Context) *alertRuleInfo +// Rule represents a single piece of work that is executed periodically by the ruler. +type Rule interface { + // Run creates the resources that will perform the rule's work, and starts it. It blocks indefinitely, until Stop is called or another signal is sent. + Run(key ngmodels.AlertRuleKey) error + // Stop shuts down the rule's execution with an optional reason. It has no effect if the rule has not yet been Run. + Stop(reason error) + // Eval sends a signal to execute the work represented by the rule, exactly one time. + // It has no effect if the rule has not yet been Run, or if the rule is Stopped. + Eval(eval *Evaluation) (bool, *Evaluation) + // Update sends a singal to change the definition of the rule. + Update(lastVersion RuleVersionAndPauseStatus) bool +} + +type ruleFactoryFunc func(context.Context) Rule -func (f ruleFactoryFunc) new(ctx context.Context) *alertRuleInfo { +func (f ruleFactoryFunc) new(ctx context.Context) Rule { return f(ctx) } @@ -44,8 +57,8 @@ func newRuleFactory( evalAppliedHook evalAppliedFunc, stopAppliedHook stopAppliedFunc, ) ruleFactoryFunc { - return func(ctx context.Context) *alertRuleInfo { - return newAlertRuleInfo( + return func(ctx context.Context) Rule { + return newAlertRule( ctx, appURL, disableGrafanaFolder, @@ -71,9 +84,9 @@ type ruleProvider interface { get(ngmodels.AlertRuleKey) *ngmodels.AlertRule } -type alertRuleInfo struct { - evalCh chan *evaluation - updateCh chan ruleVersionAndPauseStatus +type alertRule struct { + evalCh chan *Evaluation + updateCh chan RuleVersionAndPauseStatus ctx context.Context stopFn util.CancelCauseFunc @@ -96,7 +109,7 @@ type alertRuleInfo struct { tracer tracing.Tracer } -func newAlertRuleInfo( +func newAlertRule( parent context.Context, appURL *url.URL, disableGrafanaFolder bool, @@ -111,11 +124,11 @@ func newAlertRuleInfo( tracer tracing.Tracer, evalAppliedHook func(ngmodels.AlertRuleKey, time.Time), stopAppliedHook func(ngmodels.AlertRuleKey), -) *alertRuleInfo { +) *alertRule { ctx, stop := util.WithCancelCause(parent) - return &alertRuleInfo{ - evalCh: make(chan *evaluation), - updateCh: make(chan ruleVersionAndPauseStatus), + return &alertRule{ + evalCh: make(chan *Evaluation), + updateCh: make(chan RuleVersionAndPauseStatus), ctx: ctx, stopFn: stop, appURL: appURL, @@ -141,9 +154,9 @@ func newAlertRuleInfo( // - false when the send operation is stopped // // the second element contains a dropped message that was sent by a concurrent sender. -func (a *alertRuleInfo) eval(eval *evaluation) (bool, *evaluation) { +func (a *alertRule) Eval(eval *Evaluation) (bool, *Evaluation) { // read the channel in unblocking manner to make sure that there is no concurrent send operation. - var droppedMsg *evaluation + var droppedMsg *Evaluation select { case droppedMsg = <-a.evalCh: default: @@ -158,7 +171,7 @@ func (a *alertRuleInfo) eval(eval *evaluation) (bool, *evaluation) { } // update sends an instruction to the rule evaluation routine to update the scheduled rule to the specified version. The specified version must be later than the current version, otherwise no update will happen. -func (a *alertRuleInfo) update(lastVersion ruleVersionAndPauseStatus) bool { +func (a *alertRule) Update(lastVersion RuleVersionAndPauseStatus) bool { // check if the channel is not empty. select { case <-a.updateCh: @@ -176,11 +189,13 @@ func (a *alertRuleInfo) update(lastVersion ruleVersionAndPauseStatus) bool { } // stop sends an instruction to the rule evaluation routine to shut down. an optional shutdown reason can be given. -func (a *alertRuleInfo) stop(reason error) { - a.stopFn(reason) +func (a *alertRule) Stop(reason error) { + if a.stopFn != nil { + a.stopFn(reason) + } } -func (a *alertRuleInfo) run(key ngmodels.AlertRuleKey) error { +func (a *alertRule) Run(key ngmodels.AlertRuleKey) error { grafanaCtx := ngmodels.WithRuleKey(a.ctx, key) logger := a.logger.FromContext(grafanaCtx) logger.Debug("Alert rule routine started") @@ -295,7 +310,7 @@ func (a *alertRuleInfo) run(key ngmodels.AlertRuleKey) error { } } -func (a *alertRuleInfo) evaluate(ctx context.Context, key ngmodels.AlertRuleKey, f fingerprint, attempt int64, e *evaluation, span trace.Span, retry bool) error { +func (a *alertRule) evaluate(ctx context.Context, key ngmodels.AlertRuleKey, f fingerprint, attempt int64, e *Evaluation, span trace.Span, retry bool) error { orgID := fmt.Sprint(key.OrgID) evalTotal := a.metrics.EvalTotal.WithLabelValues(orgID) evalDuration := a.metrics.EvalDuration.WithLabelValues(orgID) @@ -393,14 +408,14 @@ func (a *alertRuleInfo) evaluate(ctx context.Context, key ngmodels.AlertRuleKey, return nil } -func (a *alertRuleInfo) notify(ctx context.Context, key ngmodels.AlertRuleKey, states []state.StateTransition) { +func (a *alertRule) notify(ctx context.Context, key ngmodels.AlertRuleKey, states []state.StateTransition) { expiredAlerts := state.FromAlertsStateToStoppedAlert(states, a.appURL, a.clock) if len(expiredAlerts.PostableAlerts) > 0 { a.sender.Send(ctx, key, expiredAlerts) } } -func (a *alertRuleInfo) resetState(ctx context.Context, key ngmodels.AlertRuleKey, isPaused bool) { +func (a *alertRule) resetState(ctx context.Context, key ngmodels.AlertRuleKey, isPaused bool) { rule := a.ruleProvider.get(key) reason := ngmodels.StateReasonUpdated if isPaused { @@ -411,7 +426,7 @@ func (a *alertRuleInfo) resetState(ctx context.Context, key ngmodels.AlertRuleKe } // evalApplied is only used on tests. -func (a *alertRuleInfo) evalApplied(alertDefKey ngmodels.AlertRuleKey, now time.Time) { +func (a *alertRule) evalApplied(alertDefKey ngmodels.AlertRuleKey, now time.Time) { if a.evalAppliedHook == nil { return } @@ -420,7 +435,7 @@ func (a *alertRuleInfo) evalApplied(alertDefKey ngmodels.AlertRuleKey, now time. } // stopApplied is only used on tests. -func (a *alertRuleInfo) stopApplied(alertDefKey ngmodels.AlertRuleKey) { +func (a *alertRule) stopApplied(alertDefKey ngmodels.AlertRuleKey) { if a.stopAppliedHook == nil { return } diff --git a/pkg/services/ngalert/schedule/alert_rule_test.go b/pkg/services/ngalert/schedule/alert_rule_test.go index 7b383deeaa..3fbff033de 100644 --- a/pkg/services/ngalert/schedule/alert_rule_test.go +++ b/pkg/services/ngalert/schedule/alert_rule_test.go @@ -26,18 +26,18 @@ import ( "github.com/stretchr/testify/require" ) -func TestAlertRuleInfo(t *testing.T) { +func TestAlertRule(t *testing.T) { type evalResponse struct { success bool - droppedEval *evaluation + droppedEval *Evaluation } t.Run("when rule evaluation is not stopped", func(t *testing.T) { t.Run("update should send to updateCh", func(t *testing.T) { - r := blankRuleInfoForTests(context.Background()) + r := blankRuleForTests(context.Background()) resultCh := make(chan bool) go func() { - resultCh <- r.update(ruleVersionAndPauseStatus{fingerprint(rand.Uint64()), false}) + resultCh <- r.Update(RuleVersionAndPauseStatus{fingerprint(rand.Uint64()), false}) }() select { case <-r.updateCh: @@ -47,22 +47,22 @@ func TestAlertRuleInfo(t *testing.T) { } }) t.Run("update should drop any concurrent sending to updateCh", func(t *testing.T) { - r := blankRuleInfoForTests(context.Background()) - version1 := ruleVersionAndPauseStatus{fingerprint(rand.Uint64()), false} - version2 := ruleVersionAndPauseStatus{fingerprint(rand.Uint64()), false} + r := blankRuleForTests(context.Background()) + version1 := RuleVersionAndPauseStatus{fingerprint(rand.Uint64()), false} + version2 := RuleVersionAndPauseStatus{fingerprint(rand.Uint64()), false} wg := sync.WaitGroup{} wg.Add(1) go func() { wg.Done() - r.update(version1) + r.Update(version1) wg.Done() }() wg.Wait() wg.Add(2) // one when time1 is sent, another when go-routine for time2 has started go func() { wg.Done() - r.update(version2) + r.Update(version2) }() wg.Wait() // at this point tick 1 has already been dropped select { @@ -73,16 +73,16 @@ func TestAlertRuleInfo(t *testing.T) { } }) t.Run("eval should send to evalCh", func(t *testing.T) { - r := blankRuleInfoForTests(context.Background()) + r := blankRuleForTests(context.Background()) expected := time.Now() resultCh := make(chan evalResponse) - data := &evaluation{ + data := &Evaluation{ scheduledAt: expected, rule: models.AlertRuleGen()(), folderTitle: util.GenerateShortUID(), } go func() { - result, dropped := r.eval(data) + result, dropped := r.Eval(data) resultCh <- evalResponse{result, dropped} }() select { @@ -96,17 +96,17 @@ func TestAlertRuleInfo(t *testing.T) { } }) t.Run("eval should drop any concurrent sending to evalCh", func(t *testing.T) { - r := blankRuleInfoForTests(context.Background()) + r := blankRuleForTests(context.Background()) time1 := time.UnixMilli(rand.Int63n(math.MaxInt64)) time2 := time.UnixMilli(rand.Int63n(math.MaxInt64)) resultCh1 := make(chan evalResponse) resultCh2 := make(chan evalResponse) - data := &evaluation{ + data := &Evaluation{ scheduledAt: time1, rule: models.AlertRuleGen()(), folderTitle: util.GenerateShortUID(), } - data2 := &evaluation{ + data2 := &Evaluation{ scheduledAt: time2, rule: data.rule, folderTitle: data.folderTitle, @@ -115,7 +115,7 @@ func TestAlertRuleInfo(t *testing.T) { wg.Add(1) go func() { wg.Done() - result, dropped := r.eval(data) + result, dropped := r.Eval(data) wg.Done() resultCh1 <- evalResponse{result, dropped} }() @@ -123,7 +123,7 @@ func TestAlertRuleInfo(t *testing.T) { wg.Add(2) // one when time1 is sent, another when go-routine for time2 has started go func() { wg.Done() - result, dropped := r.eval(data2) + result, dropped := r.Eval(data2) resultCh2 <- evalResponse{result, dropped} }() wg.Wait() // at this point tick 1 has already been dropped @@ -142,19 +142,19 @@ func TestAlertRuleInfo(t *testing.T) { } }) t.Run("eval should exit when context is cancelled", func(t *testing.T) { - r := blankRuleInfoForTests(context.Background()) + r := blankRuleForTests(context.Background()) resultCh := make(chan evalResponse) - data := &evaluation{ + data := &Evaluation{ scheduledAt: time.Now(), rule: models.AlertRuleGen()(), folderTitle: util.GenerateShortUID(), } go func() { - result, dropped := r.eval(data) + result, dropped := r.Eval(data) resultCh <- evalResponse{result, dropped} }() runtime.Gosched() - r.stop(nil) + r.Stop(nil) select { case result := <-resultCh: require.False(t, result.success) @@ -166,37 +166,37 @@ func TestAlertRuleInfo(t *testing.T) { }) t.Run("when rule evaluation is stopped", func(t *testing.T) { t.Run("Update should do nothing", func(t *testing.T) { - r := blankRuleInfoForTests(context.Background()) - r.stop(errRuleDeleted) + r := blankRuleForTests(context.Background()) + r.Stop(errRuleDeleted) require.ErrorIs(t, r.ctx.Err(), errRuleDeleted) - require.False(t, r.update(ruleVersionAndPauseStatus{fingerprint(rand.Uint64()), false})) + require.False(t, r.Update(RuleVersionAndPauseStatus{fingerprint(rand.Uint64()), false})) }) t.Run("eval should do nothing", func(t *testing.T) { - r := blankRuleInfoForTests(context.Background()) - r.stop(nil) - data := &evaluation{ + r := blankRuleForTests(context.Background()) + r.Stop(nil) + data := &Evaluation{ scheduledAt: time.Now(), rule: models.AlertRuleGen()(), folderTitle: util.GenerateShortUID(), } - success, dropped := r.eval(data) + success, dropped := r.Eval(data) require.False(t, success) require.Nilf(t, dropped, "expected no dropped evaluations but got one") }) t.Run("stop should do nothing", func(t *testing.T) { - r := blankRuleInfoForTests(context.Background()) - r.stop(nil) - r.stop(nil) + r := blankRuleForTests(context.Background()) + r.Stop(nil) + r.Stop(nil) }) t.Run("stop should do nothing if parent context stopped", func(t *testing.T) { ctx, cancelFn := context.WithCancel(context.Background()) - r := blankRuleInfoForTests(ctx) + r := blankRuleForTests(ctx) cancelFn() - r.stop(nil) + r.Stop(nil) }) }) t.Run("should be thread-safe", func(t *testing.T) { - r := blankRuleInfoForTests(context.Background()) + r := blankRuleForTests(context.Background()) wg := sync.WaitGroup{} go func() { for { @@ -221,15 +221,15 @@ func TestAlertRuleInfo(t *testing.T) { } switch rand.Intn(max) + 1 { case 1: - r.update(ruleVersionAndPauseStatus{fingerprint(rand.Uint64()), false}) + r.Update(RuleVersionAndPauseStatus{fingerprint(rand.Uint64()), false}) case 2: - r.eval(&evaluation{ + r.Eval(&Evaluation{ scheduledAt: time.Now(), rule: models.AlertRuleGen()(), folderTitle: util.GenerateShortUID(), }) case 3: - r.stop(nil) + r.Stop(nil) } } wg.Done() @@ -240,9 +240,8 @@ func TestAlertRuleInfo(t *testing.T) { }) } -func blankRuleInfoForTests(ctx context.Context) *alertRuleInfo { - factory := newRuleFactory(nil, false, 0, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil) - return factory.new(context.Background()) +func blankRuleForTests(ctx context.Context) *alertRule { + return newAlertRule(context.Background(), nil, false, 0, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil) } func TestRuleRoutine(t *testing.T) { @@ -279,16 +278,16 @@ func TestRuleRoutine(t *testing.T) { t.Cleanup(cancel) ruleInfo := factory.new(ctx) go func() { - _ = ruleInfo.run(rule.GetKey()) + _ = ruleInfo.Run(rule.GetKey()) }() expectedTime := time.UnixMicro(rand.Int63()) - ruleInfo.evalCh <- &evaluation{ + ruleInfo.Eval(&Evaluation{ scheduledAt: expectedTime, rule: rule, folderTitle: folderTitle, - } + }) actualTime := waitForTimeChannel(t, evalAppliedChan) require.Equal(t, expectedTime, actualTime) @@ -428,7 +427,7 @@ func TestRuleRoutine(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) ruleInfo := factory.new(ctx) go func() { - err := ruleInfo.run(models.AlertRuleKey{}) + err := ruleInfo.Run(models.AlertRuleKey{}) stoppedChan <- err }() @@ -448,11 +447,11 @@ func TestRuleRoutine(t *testing.T) { factory := ruleFactoryFromScheduler(sch) ruleInfo := factory.new(context.Background()) go func() { - err := ruleInfo.run(rule.GetKey()) + err := ruleInfo.Run(rule.GetKey()) stoppedChan <- err }() - ruleInfo.stop(errRuleDeleted) + ruleInfo.Stop(errRuleDeleted) err := waitForErrChannel(t, stoppedChan) require.NoError(t, err) @@ -479,15 +478,15 @@ func TestRuleRoutine(t *testing.T) { ruleInfo := factory.new(ctx) go func() { - _ = ruleInfo.run(rule.GetKey()) + _ = ruleInfo.Run(rule.GetKey()) }() // init evaluation loop so it got the rule version - ruleInfo.evalCh <- &evaluation{ + ruleInfo.Eval(&Evaluation{ scheduledAt: sch.clock.Now(), rule: rule, folderTitle: folderTitle, - } + }) waitForTimeChannel(t, evalAppliedChan) @@ -519,8 +518,8 @@ func TestRuleRoutine(t *testing.T) { require.Greaterf(t, expectedToBeSent, 0, "State manager was expected to return at least one state that can be expired") t.Run("should do nothing if version in channel is the same", func(t *testing.T) { - ruleInfo.updateCh <- ruleVersionAndPauseStatus{ruleFp, false} - ruleInfo.updateCh <- ruleVersionAndPauseStatus{ruleFp, false} // second time just to make sure that previous messages were handled + ruleInfo.Update(RuleVersionAndPauseStatus{ruleFp, false}) + ruleInfo.Update(RuleVersionAndPauseStatus{ruleFp, false}) // second time just to make sure that previous messages were handled actualStates := sch.stateManager.GetStatesForRuleUID(rule.OrgID, rule.UID) require.Len(t, actualStates, len(states)) @@ -529,7 +528,7 @@ func TestRuleRoutine(t *testing.T) { }) t.Run("should clear the state and expire firing alerts if version in channel is greater", func(t *testing.T) { - ruleInfo.updateCh <- ruleVersionAndPauseStatus{ruleFp + 1, false} + ruleInfo.Update(RuleVersionAndPauseStatus{ruleFp + 1, false}) require.Eventually(t, func() bool { return len(sender.Calls()) > 0 @@ -561,13 +560,13 @@ func TestRuleRoutine(t *testing.T) { ruleInfo := factory.new(ctx) go func() { - _ = ruleInfo.run(rule.GetKey()) + _ = ruleInfo.Run(rule.GetKey()) }() - ruleInfo.evalCh <- &evaluation{ + ruleInfo.Eval(&Evaluation{ scheduledAt: sch.clock.Now(), rule: rule, - } + }) waitForTimeChannel(t, evalAppliedChan) @@ -667,13 +666,13 @@ func TestRuleRoutine(t *testing.T) { ruleInfo := factory.new(ctx) go func() { - _ = ruleInfo.run(rule.GetKey()) + _ = ruleInfo.Run(rule.GetKey()) }() - ruleInfo.evalCh <- &evaluation{ + ruleInfo.Eval(&Evaluation{ scheduledAt: sch.clock.Now(), rule: rule, - } + }) waitForTimeChannel(t, evalAppliedChan) @@ -701,13 +700,13 @@ func TestRuleRoutine(t *testing.T) { ruleInfo := factory.new(ctx) go func() { - _ = ruleInfo.run(rule.GetKey()) + _ = ruleInfo.Run(rule.GetKey()) }() - ruleInfo.evalCh <- &evaluation{ + ruleInfo.Eval(&Evaluation{ scheduledAt: sch.clock.Now(), rule: rule, - } + }) waitForTimeChannel(t, evalAppliedChan) diff --git a/pkg/services/ngalert/schedule/loaded_metrics_reader.go b/pkg/services/ngalert/schedule/loaded_metrics_reader.go index d6c1aa9736..b407f4d615 100644 --- a/pkg/services/ngalert/schedule/loaded_metrics_reader.go +++ b/pkg/services/ngalert/schedule/loaded_metrics_reader.go @@ -10,7 +10,7 @@ import ( var _ eval.AlertingResultsReader = AlertingResultsFromRuleState{} -func (a *alertRuleInfo) newLoadedMetricsReader(rule *ngmodels.AlertRule) eval.AlertingResultsReader { +func (a *alertRule) newLoadedMetricsReader(rule *ngmodels.AlertRule) eval.AlertingResultsReader { return &AlertingResultsFromRuleState{ Manager: a.stateManager, Rule: rule, diff --git a/pkg/services/ngalert/schedule/registry.go b/pkg/services/ngalert/schedule/registry.go index e7401bc1c2..3d8975de3b 100644 --- a/pkg/services/ngalert/schedule/registry.go +++ b/pkg/services/ngalert/schedule/registry.go @@ -18,65 +18,69 @@ import ( var errRuleDeleted = errors.New("rule deleted") type ruleFactory interface { - new(context.Context) *alertRuleInfo + new(context.Context) Rule } -type alertRuleInfoRegistry struct { - mu sync.Mutex - alertRuleInfo map[models.AlertRuleKey]*alertRuleInfo +type ruleRegistry struct { + mu sync.Mutex + rules map[models.AlertRuleKey]Rule } -// getOrCreateInfo gets rule routine information from registry by the key. If it does not exist, it creates a new one. -// Returns a pointer to the rule routine information and a flag that indicates whether it is a new struct or not. -func (r *alertRuleInfoRegistry) getOrCreateInfo(context context.Context, key models.AlertRuleKey, factory ruleFactory) (*alertRuleInfo, bool) { +func newRuleRegistry() ruleRegistry { + return ruleRegistry{rules: make(map[models.AlertRuleKey]Rule)} +} + +// getOrCreate gets rule routine from registry by the key. If it does not exist, it creates a new one. +// Returns a pointer to the rule routine and a flag that indicates whether it is a new struct or not. +func (r *ruleRegistry) getOrCreate(context context.Context, key models.AlertRuleKey, factory ruleFactory) (Rule, bool) { r.mu.Lock() defer r.mu.Unlock() - info, ok := r.alertRuleInfo[key] + rule, ok := r.rules[key] if !ok { - info = factory.new(context) - r.alertRuleInfo[key] = info + rule = factory.new(context) + r.rules[key] = rule } - return info, !ok + return rule, !ok } -func (r *alertRuleInfoRegistry) exists(key models.AlertRuleKey) bool { +func (r *ruleRegistry) exists(key models.AlertRuleKey) bool { r.mu.Lock() defer r.mu.Unlock() - _, ok := r.alertRuleInfo[key] + _, ok := r.rules[key] return ok } -// del removes pair that has specific key from alertRuleInfo. +// del removes pair that has specific key from the registry. // Returns 2-tuple where the first element is value of the removed pair // and the second element indicates whether element with the specified key existed. -func (r *alertRuleInfoRegistry) del(key models.AlertRuleKey) (*alertRuleInfo, bool) { +func (r *ruleRegistry) del(key models.AlertRuleKey) (Rule, bool) { r.mu.Lock() defer r.mu.Unlock() - info, ok := r.alertRuleInfo[key] + rule, ok := r.rules[key] if ok { - delete(r.alertRuleInfo, key) + delete(r.rules, key) } - return info, ok + return rule, ok } -func (r *alertRuleInfoRegistry) keyMap() map[models.AlertRuleKey]struct{} { +func (r *ruleRegistry) keyMap() map[models.AlertRuleKey]struct{} { r.mu.Lock() defer r.mu.Unlock() - definitionsIDs := make(map[models.AlertRuleKey]struct{}, len(r.alertRuleInfo)) - for k := range r.alertRuleInfo { + definitionsIDs := make(map[models.AlertRuleKey]struct{}, len(r.rules)) + for k := range r.rules { definitionsIDs[k] = struct{}{} } return definitionsIDs } -type ruleVersionAndPauseStatus struct { +type RuleVersionAndPauseStatus struct { Fingerprint fingerprint IsPaused bool } -type evaluation struct { +type Evaluation struct { scheduledAt time.Time rule *models.AlertRule folderTitle string diff --git a/pkg/services/ngalert/schedule/schedule.go b/pkg/services/ngalert/schedule/schedule.go index 7a32e5bfeb..a2ccbe53c7 100644 --- a/pkg/services/ngalert/schedule/schedule.go +++ b/pkg/services/ngalert/schedule/schedule.go @@ -47,8 +47,8 @@ type schedule struct { // base tick rate (fastest possible configured check) baseInterval time.Duration - // each alert rule gets its own channel and routine - registry alertRuleInfoRegistry + // each rule gets its own channel and routine + registry ruleRegistry maxAttempts int64 @@ -116,7 +116,7 @@ func NewScheduler(cfg SchedulerCfg, stateManager *state.Manager) *schedule { } sch := schedule{ - registry: alertRuleInfoRegistry{alertRuleInfo: make(map[ngmodels.AlertRuleKey]*alertRuleInfo)}, + registry: newRuleRegistry(), maxAttempts: cfg.MaxAttempts, clock: cfg.C, baseInterval: cfg.BaseInterval, @@ -165,13 +165,13 @@ func (sch *schedule) deleteAlertRule(keys ...ngmodels.AlertRuleKey) { sch.log.Info("Alert rule cannot be removed from the scheduler as it is not scheduled", key.LogContext()...) } // Delete the rule routine - ruleInfo, ok := sch.registry.del(key) + ruleRoutine, ok := sch.registry.del(key) if !ok { sch.log.Info("Alert rule cannot be stopped as it is not running", key.LogContext()...) continue } // stop rule evaluation - ruleInfo.stop(errRuleDeleted) + ruleRoutine.Stop(errRuleDeleted) } // Our best bet at this point is that we update the metrics with what we hope to schedule in the next tick. alertRules, _ := sch.schedulableAlertRules.all() @@ -202,8 +202,8 @@ func (sch *schedule) schedulePeriodic(ctx context.Context, t *ticker.T) error { } type readyToRunItem struct { - ruleInfo *alertRuleInfo - evaluation + ruleRoutine Rule + Evaluation } // TODO refactor to accept a callback for tests that will be called with things that are returned currently, and return nothing. @@ -252,7 +252,7 @@ func (sch *schedule) processTick(ctx context.Context, dispatcherGroup *errgroup. ) for _, item := range alertRules { key := item.GetKey() - ruleInfo, newRoutine := sch.registry.getOrCreateInfo(ctx, key, ruleFactory) + ruleRoutine, newRoutine := sch.registry.getOrCreate(ctx, key, ruleFactory) // enforce minimum evaluation interval if item.IntervalSeconds < int64(sch.minRuleInterval.Seconds()) { @@ -264,7 +264,7 @@ func (sch *schedule) processTick(ctx context.Context, dispatcherGroup *errgroup. if newRoutine && !invalidInterval { dispatcherGroup.Go(func() error { - return ruleInfo.run(key) + return ruleRoutine.Run(key) }) } @@ -291,7 +291,7 @@ func (sch *schedule) processTick(ctx context.Context, dispatcherGroup *errgroup. if isReadyToRun { sch.log.Debug("Rule is ready to run on the current tick", "uid", item.UID, "tick", tickNum, "frequency", itemFrequency, "offset", offset) - readyToRun = append(readyToRun, readyToRunItem{ruleInfo: ruleInfo, evaluation: evaluation{ + readyToRun = append(readyToRun, readyToRunItem{ruleRoutine: ruleRoutine, Evaluation: Evaluation{ scheduledAt: tick, rule: item, folderTitle: folderTitle, @@ -300,12 +300,12 @@ func (sch *schedule) processTick(ctx context.Context, dispatcherGroup *errgroup. if _, isUpdated := updated[key]; isUpdated && !isReadyToRun { // if we do not need to eval the rule, check the whether rule was just updated and if it was, notify evaluation routine about that sch.log.Debug("Rule has been updated. Notifying evaluation routine", key.LogContext()...) - go func(ri *alertRuleInfo, rule *ngmodels.AlertRule) { - ri.update(ruleVersionAndPauseStatus{ + go func(routine Rule, rule *ngmodels.AlertRule) { + routine.Update(RuleVersionAndPauseStatus{ Fingerprint: ruleWithFolder{rule: rule, folderTitle: folderTitle}.Fingerprint(), IsPaused: rule.IsPaused, }) - }(ruleInfo, item) + }(ruleRoutine, item) updatedRules = append(updatedRules, ngmodels.AlertRuleKeyWithVersion{ Version: item.Version, AlertRuleKey: item.GetKey(), @@ -330,7 +330,7 @@ func (sch *schedule) processTick(ctx context.Context, dispatcherGroup *errgroup. time.AfterFunc(time.Duration(int64(i)*step), func() { key := item.rule.GetKey() - success, dropped := item.ruleInfo.eval(&item.evaluation) + success, dropped := item.ruleRoutine.Eval(&item.Evaluation) if !success { sch.log.Debug("Scheduled evaluation was canceled because evaluation routine was stopped", append(key.LogContext(), "time", tick)...) return diff --git a/pkg/services/ngalert/schedule/schedule_unit_test.go b/pkg/services/ngalert/schedule/schedule_unit_test.go index 38b4d26cdf..c30b54d390 100644 --- a/pkg/services/ngalert/schedule/schedule_unit_test.go +++ b/pkg/services/ngalert/schedule/schedule_unit_test.go @@ -363,9 +363,9 @@ func TestSchedule_deleteAlertRule(t *testing.T) { ruleFactory := ruleFactoryFromScheduler(sch) rule := models.AlertRuleGen()() key := rule.GetKey() - info, _ := sch.registry.getOrCreateInfo(context.Background(), key, ruleFactory) + info, _ := sch.registry.getOrCreate(context.Background(), key, ruleFactory) sch.deleteAlertRule(key) - require.ErrorIs(t, info.ctx.Err(), errRuleDeleted) + require.ErrorIs(t, info.(*alertRule).ctx.Err(), errRuleDeleted) require.False(t, sch.registry.exists(key)) }) }) From da327ce8073ae6f66aa020eda621de1a6495e6be Mon Sep 17 00:00:00 2001 From: Charandas Date: Mon, 11 Mar 2024 16:13:14 -0700 Subject: [PATCH 0535/1406] K8s: enable insecure to skip server cert validation for now (#84038) --- pkg/services/apiserver/aggregator/aggregator.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/services/apiserver/aggregator/aggregator.go b/pkg/services/apiserver/aggregator/aggregator.go index 708efff204..c25d41d278 100644 --- a/pkg/services/apiserver/aggregator/aggregator.go +++ b/pkg/services/apiserver/aggregator/aggregator.go @@ -146,7 +146,10 @@ func CreateAggregatorConfig(commandOptions *options.Options, sharedConfig generi } remoteServicesConfig := &RemoteServicesConfig{ - InsecureSkipTLSVerify: commandOptions.ExtraOptions.DevMode, + // TODO: in practice, we should only use the insecure flag when commandOptions.ExtraOptions.DevMode == true + // But given the bug in K8s, we are forced to set it to true until the below PR is merged and available + // https://github.com/kubernetes/kubernetes/pull/123808 + InsecureSkipTLSVerify: true, ExternalNamesNamespace: externalNamesNamespace, CABundle: caBundlePEM, Services: remoteServices, From e33e219a9a1804b99bf05139701c3d63eeb98bb4 Mon Sep 17 00:00:00 2001 From: Armand Grillet <2117580+armandgrillet@users.noreply.github.com> Date: Tue, 12 Mar 2024 05:37:41 +0100 Subject: [PATCH 0536/1406] Remove legacy alerting docs (#84190) --- .github/CODEOWNERS | 1 - .../administration/provisioning/index.md | 72 --- .../index.md | 2 +- .../sources/alerting/alerting-rules/_index.md | 1 - .../create-notification-policy.md | 1 - docs/sources/alerting/difference-old-new.md | 54 --- .../set-up/migrating-alerts/_index.md | 207 --------- .../legacy-alerting-deprecation.md | 58 --- .../breaking-changes-v10-0.md | 2 +- docs/sources/developers/http_api/admin.md | 39 -- docs/sources/developers/http_api/alerting.md | 184 -------- .../alerting_notification_channels.md | 424 ------------------ docs/sources/old-alerting/_index.md | 33 -- .../old-alerting/add-notification-template.md | 38 -- docs/sources/old-alerting/create-alerts.md | 138 ------ docs/sources/old-alerting/notifications.md | 303 ------------- .../old-alerting/pause-an-alert-rule.md | 26 -- .../old-alerting/troubleshoot-alerts.md | 53 --- docs/sources/old-alerting/view-alerts.md | 32 -- .../setup-grafana/configure-grafana/_index.md | 35 +- .../feature-toggles/index.md | 2 - .../configure-security/audit-grafana.md | 35 -- .../encrypt-secrets-using-aws-kms/index.md | 6 +- .../index.md | 6 +- .../index.md | 6 +- .../index.md | 6 +- .../create-alerts-with-logs/index.md | 1 - .../index.md | 1 - docs/sources/whatsnew/whats-new-in-v10-4.md | 2 +- 29 files changed, 13 insertions(+), 1755 deletions(-) delete mode 100644 docs/sources/alerting/difference-old-new.md delete mode 100644 docs/sources/alerting/set-up/migrating-alerts/_index.md delete mode 100644 docs/sources/alerting/set-up/migrating-alerts/legacy-alerting-deprecation.md delete mode 100644 docs/sources/developers/http_api/alerting.md delete mode 100644 docs/sources/developers/http_api/alerting_notification_channels.md delete mode 100644 docs/sources/old-alerting/_index.md delete mode 100644 docs/sources/old-alerting/add-notification-template.md delete mode 100644 docs/sources/old-alerting/create-alerts.md delete mode 100644 docs/sources/old-alerting/notifications.md delete mode 100644 docs/sources/old-alerting/pause-an-alert-rule.md delete mode 100644 docs/sources/old-alerting/troubleshoot-alerts.md delete mode 100644 docs/sources/old-alerting/view-alerts.md diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 49d503cce4..e55df1e38b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -46,7 +46,6 @@ /docs/sources/fundamentals @chri2547 /docs/sources/getting-started/ @chri2547 /docs/sources/introduction/ @chri2547 -/docs/sources/old-alerting/ @brendamuir /docs/sources/panels-visualizations/ @imatwawana /docs/sources/release-notes/ @Eve832 @GrafanaWriter /docs/sources/setup-grafana/ @chri2547 diff --git a/docs/sources/administration/provisioning/index.md b/docs/sources/administration/provisioning/index.md index 30468e993b..7c98942d58 100644 --- a/docs/sources/administration/provisioning/index.md +++ b/docs/sources/administration/provisioning/index.md @@ -427,78 +427,6 @@ This feature doesn't currently allow you to create nested folder structures, tha For information on provisioning Grafana Alerting, refer to [Provision Grafana Alerting resources]({{< relref "../../alerting/set-up/provision-alerting-resources/" >}}). -## Alert Notification Channels - -{{% admonition type="note" %}} -Alert Notification Channels are part of legacy alerting, which is deprecated and will be removed in Grafana 10. Use the Provision contact points section in [Create and manage alerting resources using file provisioning]({{< relref "../../alerting/set-up/provision-alerting-resources/file-provisioning" >}}). -{{% /admonition %}} - -Alert Notification Channels can be provisioned by adding one or more YAML config files in the [`provisioning/notifiers`](/administration/configuration/#provisioning) directory. - -Each config file can contain the following top-level fields: - -- `notifiers`, a list of alert notifications that will be added or updated during start up. If the notification channel already exists, Grafana will update it to match the configuration file. -- `delete_notifiers`, a list of alert notifications to be deleted before inserting/updating those in the `notifiers` list. - -Provisioning looks up alert notifications by uid, and will update any existing notification with the provided uid. - -By default, exporting a dashboard as JSON will use a sequential identifier to refer to alert notifications. The field `uid` can be optionally specified to specify a string identifier for the alert name. - -```json -{ - ... - "alert": { - ..., - "conditions": [...], - "frequency": "24h", - "noDataState": "ok", - "notifications": [ - {"uid": "notifier1"}, - {"uid": "notifier2"}, - ] - } - ... -} -``` - -### Example Alert Notification Channels Config File - -```yaml -notifiers: - - name: notification-channel-1 - type: slack - uid: notifier1 - # either - org_id: 2 - # or - org_name: Main Org. - is_default: true - send_reminder: true - frequency: 1h - disable_resolve_message: false - # See `Supported Settings` section for settings supported for each - # alert notification type. - settings: - recipient: 'XXX' - uploadImage: true - token: 'xoxb' # legacy setting since Grafana v7.2 (stored non-encrypted) - url: https://slack.com # legacy setting since Grafana v7.2 (stored non-encrypted) - # Secure settings that will be encrypted in the database (supported since Grafana v7.2). See `Supported Settings` section for secure settings supported for each notifier. - secure_settings: - token: 'xoxb' - url: https://slack.com - -delete_notifiers: - - name: notification-channel-1 - uid: notifier1 - # either - org_id: 2 - # or - org_name: Main Org. - - name: notification-channel-2 - # default org_id: 1 -``` - ### Supported Settings The following sections detail the supported settings and secure settings for each alert notification type. Secure settings are stored encrypted in the database and you add them to `secure_settings` in the YAML file instead of `settings`. diff --git a/docs/sources/administration/roles-and-permissions/access-control/rbac-fixed-basic-role-definitions/index.md b/docs/sources/administration/roles-and-permissions/access-control/rbac-fixed-basic-role-definitions/index.md index 232ed3b481..05d1235169 100644 --- a/docs/sources/administration/roles-and-permissions/access-control/rbac-fixed-basic-role-definitions/index.md +++ b/docs/sources/administration/roles-and-permissions/access-control/rbac-fixed-basic-role-definitions/index.md @@ -110,7 +110,7 @@ The following tables list permissions associated with basic and fixed roles. ### Alerting roles -If alerting is [enabled]({{< relref "../../../../alerting/set-up/migrating-alerts" >}}), you can use predefined roles to manage user access to alert rules, alert instances, and alert notification settings and create custom roles to limit user access to alert rules in a folder. +You can use predefined roles to manage user access to alert rules, alert instances, and alert notification settings and create custom roles to limit user access to alert rules in a folder. Access to Grafana alert rules is an intersection of many permissions: diff --git a/docs/sources/alerting/alerting-rules/_index.md b/docs/sources/alerting/alerting-rules/_index.md index a0a1975723..efef76a9cf 100644 --- a/docs/sources/alerting/alerting-rules/_index.md +++ b/docs/sources/alerting/alerting-rules/_index.md @@ -1,6 +1,5 @@ --- aliases: - - old-alerting/create-alerts/ - rules/ - unified-alerting/alerting-rules/ - ./create-alerts/ diff --git a/docs/sources/alerting/configure-notifications/create-notification-policy.md b/docs/sources/alerting/configure-notifications/create-notification-policy.md index 0ac593e8bd..6f5f5dc4c6 100644 --- a/docs/sources/alerting/configure-notifications/create-notification-policy.md +++ b/docs/sources/alerting/configure-notifications/create-notification-policy.md @@ -1,7 +1,6 @@ --- aliases: - ../notifications/ # /docs/grafana/latest/alerting/notifications/ - - ../old-alerting/notifications/ # /docs/grafana/latest/alerting/old-alerting/notifications/ - ../unified-alerting/notifications/ # /docs/grafana/latest/alerting/unified-alerting/notifications/ - ../alerting-rules/create-notification-policy/ # /docs/grafana/latest/alerting/alerting-rules/create-notification-policy/ canonical: https://grafana.com/docs/grafana/latest/alerting/configure-notifications/create-notification-policy/ diff --git a/docs/sources/alerting/difference-old-new.md b/docs/sources/alerting/difference-old-new.md deleted file mode 100644 index 24e4e94294..0000000000 --- a/docs/sources/alerting/difference-old-new.md +++ /dev/null @@ -1,54 +0,0 @@ ---- -_build: - list: false -aliases: - - ./unified-alerting/difference-old-new/ # /docs/grafana//alerting/unified-alerting/difference-old-new/ -canonical: https://grafana.com/docs/grafana/latest/alerting/difference-old-new/ -description: Learn about how Grafana Alerting compares to legacy alerting -keywords: - - grafana - - alerting - - guide -labels: - products: - - cloud - - enterprise - - oss -title: Grafana Alerting vs Legacy dashboard alerting -weight: 108 ---- - -# Grafana Alerting vs Legacy dashboard alerting - -Introduced in Grafana 8.0, and the only system since Grafana 10.0, Grafana Alerting has several enhancements over legacy dashboard alerting. - -## Multi-dimensional alerting - -You can now create alerts that give you system-wide visibility with a single alerting rule. Generate multiple alert instances from a single alert rule. For example, you can create a rule to monitor the disk usage of multiple mount points on a single host. The evaluation engine returns multiple time series from a single query, with each time series identified by its label set. - -## Create alerts outside of Dashboards - -Unlike legacy dashboard alerts, Grafana alerts allow you to create queries and expressions that combine data from multiple sources in unique ways. You can still link dashboards and panels to alerting rules using their ID and quickly troubleshoot the system under observation. - -Since unified alerts are no longer directly tied to panel queries, they do not include images or query values in the notification email. You can use customized notification templates to view query values. - -## Create Loki and Grafana Mimir alerting rules - -In Grafana Alerting, you can manage Loki and Grafana Mimir alerting rules using the same UI and API as your Grafana managed alerts. - -## View and search for alerts from Prometheus compatible data sources - -Alerts for Prometheus compatible data sources are now listed under the Grafana alerts section. You can search for labels across multiple data sources to quickly find relevant alerts. - -## Special alerts for alert state NoData and Error - -Grafana Alerting introduced a new concept of the alert states. When evaluation of an alerting rule produces state NoData or Error, Grafana Alerting will generate special alerts that will have the following labels: - -- `alertname` with value DatasourceNoData or DatasourceError depending on the state. -- `rulename` name of the alert rule the special alert belongs to. -- `datasource_uid` will have the UID of the data source that caused the state. -- all labels and annotations of the original alert rule - -You can handle these alerts the same way as regular alerts by adding a silence, route to a contact point, and so on. - -> **Note:** If the rule uses many data sources and one or many returns no data, the special alert will be created for each data source that caused the alert state. diff --git a/docs/sources/alerting/set-up/migrating-alerts/_index.md b/docs/sources/alerting/set-up/migrating-alerts/_index.md deleted file mode 100644 index 42da3a7fcd..0000000000 --- a/docs/sources/alerting/set-up/migrating-alerts/_index.md +++ /dev/null @@ -1,207 +0,0 @@ ---- -aliases: - - ../migrating-alerts/ # /docs/grafana//alerting/migrating-alerts/ -canonical: https://grafana.com/docs/grafana/latest/alerting/set-up/migrating-alerts/ -description: Upgrade to Grafana Alerting -labels: - products: - - enterprise - - oss -title: Upgrade Alerting -weight: 150 ---- - -# Upgrade Alerting - -{{% admonition type="note" %}} -Legacy alerting will be removed in Grafana v11.0.0. We recommend that you upgrade to Grafana Alerting as soon as possible. -For more information, refer to [Legacy alerting deprecation](/docs/grafana//alerting/set-up/migrating-alerts/legacy-alerting-deprecation). -{{% /admonition %}} - -Grafana provides two methods for a seamless automatic upgrade of legacy alert rules and notification channels to Grafana Alerting: - -1. **Upgrade with Preview** (Recommended): Offers a safe and controlled preview environment where you can review and adjust your upgraded alerts before fully enabling Grafana Alerting. -2. **Simple Upgrade**: One-step upgrade method for specific needs where a preview environment is not essential. - -{{% admonition type="note" %}} -When upgrading with either method, your legacy dashboard alerts and notification channels are copied to a new format. This is non-destructive and can be [rolled back easily](#rolling-back-to-legacy-alerting). -{{% /admonition %}} - -## Key Considerations - -| Feature | Upgrade with Preview | Simple Upgrade | -| --------------------------- | ----------------------------------------------------------------------------------- | ---------------------------------------------------- | -| **Safety and Control** | ☑️ Preview environment for review and adjustment | ❌ No preview, potential for unexpected issues | -| **User Experience** | ☑️ Seamless transition by handling issues early | ❌ Possible disruption during upgrade | -| **Granular Control** | ☑️ Re-upgrade specific resources after resolving errors | ❌ All or nothing upgrade, manual error correction | -| **Stakeholder Involvement** | ☑️ Collaboration and review of adjusted alerts | ❌ Review only available after upgrade | -| **Provisioning Support** | ☑️ Configure new as-code before upgrading, simultaneous provisioning | ❌ No built-in provisioning support | -| **Simplicity** | ❌ May take longer to complete | ☑️ Fast, one-step process | -| **Suited for:** | ☑️ Complex setups, risk-averse environments, collaborative teams, heavy as-code use | ☑️ Simple setups, testing environments, large fleets | -| **Version** | Grafana v10.3.0+ | Grafana v9.0.0+ | - -## Upgrade with Preview (Recommended) - -### Prerequisites - -- Grafana `v10.3.0 or later`. -- Grafana administrator access. -- Enable `alertingPreviewUpgrade` [feature toggle][feature-toggles] (enabled by default in v10.4.0 or later). - -### Suited for - -- **Complex setups**: Large deployments with intricate alert rules and notification channels. -- **Risk-averse environments**: Situations where minimizing disruption and ensuring a smooth transition are critical. -- **Collaborative teams**: Projects where feedback and review from stakeholders are valuable. -- **Heavy as-code use**: Deployments with large or complex as-code configurations. - -### Overview - -In **Alerts & IRM**, the **Alerting** section provides a preview of Grafana Alerting where you can review and modify your upgraded alerts before finalizing the upgrade. - -In the **Alerting (legacy) -> Alerting upgrade** section, you can upgrade your existing alert rules and notification channels, and view a summary of the upgrade to Grafana Alerting. - -Finalize your upgrade by restarting Grafana with the `[unified_alerting]` section enabled in your configuration. - -{{% admonition type="note" %}} -Alerts generated by the new alerting system are visible in the **Alerting** section of the navigation panel but are not active until the upgrade is finalized. -{{% /admonition %}} - -### To upgrade with preview, complete the following steps. - -1. **Preview the Upgrade**: - - **Initiate the process**: Access the upgrade functionality within Grafana by visiting the **Alerting upgrade** page in the **Alerting (legacy)** section of the navigation panel. From this page you can upgrade your existing alert rules and notification channels to the new Grafana Alerting system. - - **Review the summary table:** Review the detailed table outlining how your existing alert rules and notification channels were upgraded to resources in the new Grafana Alerting system. -1. **Investigate and Resolve Errors**: - - **Identify errors**: Carefully examine the previewed upgrade: - - Any alert rules or notification channels that couldn't be automatically upgraded will be highlighted with error indicators. - - New or removed alert rules and notification channels will be highlighted with warning indicators. - - **Address errors**: You have two options to resolve these issues: - - **Fix legacy issues**: If possible, address the problems within your legacy alerting setup and attempt to upgrade the specific resource again. - - **Create new resources**: If fixing legacy issues isn't viable, create new alert rules, notification policies, or contact points manually within the new Grafana Alerting system to replace the problematic ones. -1. **Update As-Code Setup** (Optional): - - **Export upgraded resources**: If you use provisioning methods to manage alert rules and notification channels, you can export the upgraded versions to generate provisioning files compatible with Grafana Alerting. - - **Test new provisioning definitions**: Ensure your as-code setup aligns with the new system before completing the upgrade process. Both legacy and Grafana Alerting alerts can be provisioned simultaneously to facilitate a smooth transition. -1. **Finalize the Upgrade**: - - **Contact your Grafana server administrator**: Once you're confident in the state of your previewed upgrade, request to [enable Grafana Alerting](#enable-grafana-alerting). - - **Continued use for upgraded organizations**: Organizations that have already completed the preview upgrade will seamlessly continue using their configured setup. - - **Automatic upgrade for others**: Organizations that haven't initiated the upgrade with preview process will undergo the traditional automatic upgrade during this restart. - - **Address issues before restart**: Exercise caution, as Grafana will not start if any traditional automatic upgrades encounter errors. Ensure all potential issues are resolved before initiating this step. - -## Simple Upgrade - -### Prerequisites - -- Grafana `v9.0.0 or later` (more recent versions are recommended). - -### Suited for - -- **Simple setups**: Limited number of alerts and channels with minimal complexity. -- **Testing environments**: Where a quick upgrade without a preview is sufficient. -- **Large fleets**: Where manually reviewing each instance is not feasible. - -### Overview - -While we recommend the **Upgrade with Preview** method for its enhanced safety and control, the **Simple Upgrade Method** exists for specific situations where a preview environment is not essential. For example, if you have a large fleet of Grafana instances and want to upgrade them all without the need to review and adjust each one individually. - -Configure your Grafana instance to enable Grafana Alerting and disable legacy alerting. Then restart Grafana to automatically upgrade your existing alert rules and notification channels to the new Grafana Alerting system. - -Once Grafana Alerting is enabled, you can review and adjust your upgraded alerts in the **Alerting** section of the navigation panel as well as export them for as-code setup. - -### To perform the simple upgrade, complete the following steps. - -{{% admonition type="note" %}} -Any errors encountered during the upgrade process will fail the upgrade and prevent Grafana from starting. If this occurs, you can [roll back to legacy alerting](#rolling-back-to-legacy-alerting). -{{% /admonition %}} - -1. **Upgrade to Grafana Alerting**: - - **Enable Grafana Alerting**: [Modify custom configuration file](#enable-grafana-alerting). - - **Restart Grafana**: Restart Grafana for the configuration changes to take effect. Grafana will automatically upgrade your existing alert rules and notification channels to the new Grafana Alerting system. -1. **Review and Adjust Upgraded Alerts**: - - **Review the upgraded alerts**: Go to the `Alerting` section of the navigation panel to review the upgraded alerts. - - **Export upgraded resources**: If you use provisioning methods to manage alert rules and notification channels, you can export the upgraded versions to generate provisioning files compatible with Grafana Alerting. - -## Additional Information - -### Enable Grafana Alerting - -Go to your custom configuration file ($WORKING_DIR/conf/custom.ini) and enter the following in your configuration: - -```toml -[alerting] -enabled = false - -[unified_alerting] -enabled = true -``` - -{{% admonition type="note" %}} -If you have existing legacy alerts we advise using the [Upgrade with Preview](#upgrade-with-preview-recommended) method first to ensure a smooth transition. Any organizations that have not completed the preview upgrade will automatically undergo the simple upgrade during the next restart. -{{% /admonition %}} - -### Rolling back to legacy alerting - -{{% admonition type="note" %}} -For Grafana Cloud, contact customer support to enable or disable Grafana Alerting for your stack. -{{% /admonition %}} - -If you have upgraded to Grafana Alerting and want to roll back to legacy alerting, you can do so by disabling Grafana Alerting and re-enabling legacy alerting. - -Go to your custom configuration file ($WORKING_DIR/conf/custom.ini) and enter the following in your configuration: - -```toml -[alerting] -enabled = true - -[unified_alerting] -enabled = false -``` - -This action is non-destructive. You can seamlessly switch between legacy alerting and Grafana Alerting at any time without losing any data. However, the upgrade process will only be performed once. If you have opted out of Grafana Alerting and then opt in again, Grafana will not perform the upgrade again. - -If, after rolling back, you wish to delete any existing Grafana Alerting configuration and upgrade your legacy alerting configuration again from scratch, you can enable the `clean_upgrade` option: - -```toml -[unified_alerting.upgrade] -clean_upgrade = true -``` - -### Differences and limitations - -There are some differences between Grafana Alerting and legacy dashboard alerts, and a number of features that are no longer supported. - -**Differences** - -1. Read and write access to legacy dashboard alerts are governed by the dashboard permissions (including the inherited permissions from the folder) while Grafana alerts are governed by the permissions of the folder only. During the upgrade, an alert rule might be moved to a different folder to match the permissions of the dashboard. The following rules apply: - - - If the inherited dashboard permissions are different from the permissions of the folder, then the rule is moved to a new folder named after the original: ` - `. - - If the inherited dashboard permissions are the same as the permissions of the folder, then the rule is moved to the original folder. - - If the dashboard is in the `General` or `Dashboards` folder (i.e. no folder), then the rule is moved to a new `General Alerting - ` folder. - -1. `NoData` and `Error` settings are upgraded as is to the corresponding settings in Grafana Alerting, except in two situations: - - - As there is no `Keep Last State` option in Grafana Alerting, this option becomes either [`NoData` or `Error`][alerting_config_error_handling]. If using the `Simple Upgrade Method` Grafana automatically creates a 1 year silence for each alert rule with this configuration. If the alert evaluation returns no data or fails (error or timeout), then it creates a [special alert][special_alert], which will be silenced by the silence created during the upgrade. - - Due to lack of validation, legacy alert rules imported via JSON or provisioned along with dashboards can contain arbitrary values for [`NoData` or `Error`][alerting_config_error_handling]. In this situation, Grafana will use the default setting: `NoData` for No data, and `Error` for Error. - -1. Notification channels are upgraded to an Alertmanager configuration with the appropriate routes and receivers. - -1. Unlike legacy dashboard alerts where images in notifications are enabled per contact point, images in notifications for Grafana Alerting must be enabled in the Grafana configuration, either in the configuration file or environment variables, and are enabled for either all or no contact points. - -1. The JSON format for webhook notifications has changed in Grafana Alerting and uses the format from [Prometheus Alertmanager](https://prometheus.io/docs/alerting/latest/configuration/#webhook_config). - -1. Alerting on Prometheus `Both` type queries is not supported in Grafana Alerting. Existing legacy alerts with `Both` type queries are upgraded to Grafana Alerting as alerts with `Range` type queries. - -**Limitations** - -1. Since `Hipchat` and `Sensu` notification channels are no longer supported, legacy alerts associated with these channels are not automatically upgraded to Grafana Alerting. Assign the legacy alerts to a supported notification channel so that you continue to receive notifications for those alerts. - -{{% docs/reference %}} - -[feature-toggles]: "/docs/ -> /docs/grafana//setup-grafana/configure-grafana/feature-toggles" - -[alerting_config_error_handling]: "/docs/grafana/ -> /docs/grafana//alerting/alerting-rules/create-grafana-managed-rule#configure-no-data-and-error-handling" -[alerting_config_error_handling]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/alerting-rules/create-grafana-managed-rule#configure-no-data-and-error-handling" - -[special_alert]: "/docs/grafana/ -> /docs/grafana//alerting/fundamentals/alert-rules/state-and-health#special-alerts-for-nodata-and-error" -[special_alert]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/fundamentals/alert-rules/state-and-health#special-alerts-for-nodata-and-error" -{{% /docs/reference %}} diff --git a/docs/sources/alerting/set-up/migrating-alerts/legacy-alerting-deprecation.md b/docs/sources/alerting/set-up/migrating-alerts/legacy-alerting-deprecation.md deleted file mode 100644 index 2bdef82ee9..0000000000 --- a/docs/sources/alerting/set-up/migrating-alerts/legacy-alerting-deprecation.md +++ /dev/null @@ -1,58 +0,0 @@ ---- -aliases: - - alerting/legacy-alerting-deprecation/ -canonical: https://grafana.com/docs/grafana/latest/alerting/set-up/migrating-alerts/legacy-alerting-deprecation/ -description: Learn about legacy alerting deprecation -keywords: - - grafana - - alerting -labels: - products: - - enterprise - - oss -title: Legacy alerting deprecation -weight: 109 ---- - -# Legacy alerting deprecation - -Starting with Grafana v9.0.0, legacy alerting is deprecated, meaning that it is no longer actively maintained or supported by Grafana. As of Grafana v10.0.0, we do not contribute or accept external contributions to the codebase apart from CVE fixes. - -Legacy alerting refers to the old alerting system that was used prior to the introduction of Grafana Alerting; the new alerting system in Grafana. - -The decision to deprecate legacy alerting was made to encourage users to migrate to the new alerting system, which offers a more powerful and flexible alerting experience based on Prometheus Alertmanager. - -Users who are still using legacy alerting are encouraged to migrate their alerts to the new system as soon as possible to ensure that they continue to receive new features, bug fixes, and support. - -However, we will still patch CVEs until legacy alerting is completely removed in Grafana 11; honoring our commitment to building and distributing secure software. - -We have provided [instructions][migrating-alerts] on how to migrate to the new alerting system, making the process as easy as possible for users. - -## Why are we deprecating legacy alerting? - -The new Grafana alerting system is more powerful and flexible than the legacy alerting feature. - -The new system is based on Prometheus Alertmanager, which offers a more comprehensive set of features for defining and managing alerts. With the new alerting system, users can create alerts based on complex queries, configure alert notifications via various integrations, and set up sophisticated alerting rules with support for conditional expressions, aggregation, and grouping. - -Overall, the new alerting system in Grafana is a major improvement over the legacy alerting feature, providing users with a more powerful and flexible alerting experience. - -Additionally, legacy alerting still requires Angular to function and we are [planning to remove support for it][angular_deprecation] in Grafana 11. - -## When will we remove legacy alerting completely? - -Legacy alerting will be removed from the code-base in Grafana 11, following the same timeline as the [Angular deprecation][angular_deprecation]. - -## How do I migrate to the new Grafana alerting? - -Refer to our [upgrade instructions][migrating-alerts]. - -### Useful links - -- [Upgrade Alerting][migrating-alerts] -- [Angular support deprecation][angular_deprecation] - -{{% docs/reference %}} -[angular_deprecation]: "/docs/ -> /docs/grafana//developers/angular_deprecation" - -[migrating-alerts]: "/docs/ -> /docs/grafana//alerting/set-up/migrating-alerts" -{{% /docs/reference %}} diff --git a/docs/sources/breaking-changes/breaking-changes-v10-0.md b/docs/sources/breaking-changes/breaking-changes-v10-0.md index 1135ccad11..8c0dd3f29e 100644 --- a/docs/sources/breaking-changes/breaking-changes-v10-0.md +++ b/docs/sources/breaking-changes/breaking-changes-v10-0.md @@ -81,7 +81,7 @@ Grafana legacy alerting (dashboard alerts) has been deprecated since Grafana v9. #### Migration path -The new Grafana Alerting was introduced in Grafana 8 and is a superset of legacy alerting. Learn how to migrate your alerts in the [Upgrade Alerting documentation]({{< relref "../alerting/set-up/migrating-alerts/" >}}). +The new Grafana Alerting was introduced in Grafana 8 and is a superset of legacy alerting. Learn how to migrate your alerts in the [Upgrade Alerting documentation]({{< relref "./v10.4/alerting/set-up/migrating-alerts/" >}}). ### API keys are migrating to service accounts diff --git a/docs/sources/developers/http_api/admin.md b/docs/sources/developers/http_api/admin.md index b7a40e1d74..4b682de0ef 100644 --- a/docs/sources/developers/http_api/admin.md +++ b/docs/sources/developers/http_api/admin.md @@ -473,45 +473,6 @@ Content-Type: application/json {"message": "User deleted"} ``` -## Pause all alerts - -`POST /api/admin/pause-all-alerts` - -{{% admonition type="note" %}} -This API is relevant for the [legacy dashboard alerts](https://grafana.com/docs/grafana/v8.5/alerting/old-alerting/) only. For default alerting, use silences to stop alerts from being delivered. -{{% /admonition %}} - -Only works with Basic Authentication (username and password). See [introduction](http://docs.grafana.org/http_api/admin/#admin-api) for an explanation. - -**Example Request**: - -```http -POST /api/admin/pause-all-alerts HTTP/1.1 -Accept: application/json -Content-Type: application/json - -{ - "paused": true -} -``` - -JSON Body schema: - -- **paused** – If true then all alerts are to be paused, false unpauses all alerts. - -**Example Response**: - -```http -HTTP/1.1 200 -Content-Type: application/json - -{ - "state": "Paused", - "message": "alert paused", - "alertsAffected": 1 -} -``` - ## Auth tokens for User `GET /api/admin/users/:id/auth-tokens` diff --git a/docs/sources/developers/http_api/alerting.md b/docs/sources/developers/http_api/alerting.md deleted file mode 100644 index ceece602a8..0000000000 --- a/docs/sources/developers/http_api/alerting.md +++ /dev/null @@ -1,184 +0,0 @@ ---- -aliases: - - ../../http_api/alerting/ -canonical: /docs/grafana/latest/developers/http_api/alerting/ -description: Grafana Alerts HTTP API -keywords: - - grafana - - http - - documentation - - api - - alerting - - alerts -labels: - products: - - enterprise - - oss -title: Legacy Alerting API ---- - -# Legacy Alerting API - -{{% admonition type="note" %}} -Starting with v9.0, the Legacy Alerting HTTP API is deprecated. It will be removed in a future release. -{{% /admonition %}} - -This topic is relevant for the [legacy dashboard alerts](/docs/grafana/v8.5/alerting/old-alerting/) only. - -If you are using Grafana Alerting, refer to [Alerting provisioning API]({{< relref "./alerting_provisioning" >}}) - -You can find Grafana Alerting API specification details [here](https://editor.swagger.io/?url=https://raw.githubusercontent.com/grafana/grafana/main/pkg/services/ngalert/api/tooling/post.json). Also, refer to [Grafana Alerting alerts documentation][] for details on how to create and manage new alerts. - -You can use the Alerting API to get information about legacy dashboard alerts and their states but this API cannot be used to modify the alert. -To create new alerts or modify them you need to update the dashboard JSON that contains the alerts. - -## Get alerts - -`GET /api/alerts/` - -**Example Request**: - -```http -GET /api/alerts HTTP/1.1 -Accept: application/json -Content-Type: application/json -Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk -``` - -Querystring Parameters: - -These parameters are used as querystring parameters. For example: - -`/api/alerts?dashboardId=1` - -- **dashboardId** – Limit response to alerts in specified dashboard(s). You can specify multiple dashboards, e.g. dashboardId=23&dashboardId=35. -- **panelId** – Limit response to alert for a specified panel on a dashboard. -- **query** - Limit response to alerts having a name like this value. -- **state** - Return alerts with one or more of the following alert states: `ALL`,`no_data`, `paused`, `alerting`, `ok`, `pending`. To specify multiple states use the following format: `?state=paused&state=alerting` -- **limit** - Limit response to _X_ number of alerts. -- **folderId** – Limit response to alerts of dashboards in specified folder(s). You can specify multiple folders, e.g. folderId=23&folderId=35. -- **dashboardQuery** - Limit response to alerts having a dashboard name like this value. -- **dashboardTag** - Limit response to alerts of dashboards with specified tags. To do an "AND" filtering with multiple tags, specify the tags parameter multiple times e.g. dashboardTag=tag1&dashboardTag=tag2. - -**Example Response**: - -```http -HTTP/1.1 200 -Content-Type: application/json - -[ - { - "id": 1, - "dashboardId": 1, - "dashboardUId": "ABcdEFghij" - "dashboardSlug": "sensors", - "panelId": 1, - "name": "fire place sensor", - "state": "alerting", - "newStateDate": "2018-05-14T05:55:20+02:00", - "evalDate": "0001-01-01T00:00:00Z", - "evalData": null, - "executionError": "", - "url": "http://grafana.com/dashboard/db/sensors" - } -] -``` - -## Get alert by id - -`GET /api/alerts/:id` - -**Example Request**: - -```http -GET /api/alerts/1 HTTP/1.1 -Accept: application/json -Content-Type: application/json -Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk -``` - -**Example Response**: - -```http -HTTP/1.1 200 -Content-Type: application/json - -{ - "id": 1, - "dashboardId": 1, - "dashboardUId": "ABcdEFghij" - "dashboardSlug": "sensors", - "panelId": 1, - "name": "fire place sensor", - "state": "alerting", - "message": "Someone is trying to break in through the fire place", - "newStateDate": "2018-05-14T05:55:20+02:00", - "evalDate": "0001-01-01T00:00:00Z", - "evalData": "evalMatches": [ - { - "metric": "movement", - "tags": { - "name": "fireplace_chimney" - }, - "value": 98.765 - } - ], - "executionError": "", - "url": "http://grafana.com/dashboard/db/sensors" -} -``` - -**Important Note**: -"evalMatches" data is cached in the db when and only when the state of the alert changes -(e.g. transitioning from "ok" to "alerting" state). - -If data from one server triggers the alert first and, before that server is seen leaving alerting state, -a second server also enters a state that would trigger the alert, the second server will not be visible in "evalMatches" data. - -## Pause alert by id - -`POST /api/alerts/:id/pause` - -**Example Request**: - -```http -POST /api/alerts/1/pause HTTP/1.1 -Accept: application/json -Content-Type: application/json -Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk - -{ - "paused": true -} -``` - -The :id query parameter is the id of the alert to be paused or unpaused. - -JSON Body Schema: - -- **paused** – Can be `true` or `false`. True to pause an alert. False to unpause an alert. - -**Example Response**: - -```http -HTTP/1.1 200 -Content-Type: application/json - -{ - "alertId": 1, - "state": "Paused", - "message": "alert paused" -} -``` - -## Pause all alerts - -See [Admin API][]. - -{{% docs/reference %}} -[Admin API]: "/docs/grafana/ -> /docs/grafana//developers/http_api/admin#pause-all-alerts" -[Admin API]: "/docs/grafana/ -> /docs/grafana//developers/http_api/admin#pause-all-alerts" - -[Grafana Alerting alerts documentation]: "/docs/grafana/ -> /docs/grafana//alerting" -[Grafana Alerting alerts documentation]: "/docs/grafana-cloud/ -> /docs/grafana//alerting" -{{% /docs/reference %}} diff --git a/docs/sources/developers/http_api/alerting_notification_channels.md b/docs/sources/developers/http_api/alerting_notification_channels.md deleted file mode 100644 index 440237b328..0000000000 --- a/docs/sources/developers/http_api/alerting_notification_channels.md +++ /dev/null @@ -1,424 +0,0 @@ ---- -aliases: - - ../../http_api/alerting_notification_channels/ -canonical: /docs/grafana/latest/developers/http_api/alerting_notification_channels/ -description: Grafana Alerting Notification Channel HTTP API -keywords: - - grafana - - http - - documentation - - api - - alerting - - alerts - - notifications -labels: - products: - - enterprise - - oss -title: Legacy Alerting Notification Channels API ---- - -# Legacy Alerting Notification Channels API - -{{% admonition type="note" %}} -Starting with v9.0, the Legacy Alerting Notification Channels API is deprecated. It will be removed in a future release. -{{% /admonition %}} - -This page documents the Alerting Notification Channels API. - -## Identifier (id) vs unique identifier (uid) - -The identifier (id) of a notification channel is an auto-incrementing numeric value and is only unique per Grafana install. - -The unique identifier (uid) of a notification channel can be used for uniquely identify a notification channel between -multiple Grafana installs. It's automatically generated if not provided when creating a notification channel. The uid -allows having consistent URLs for accessing notification channels and when syncing notification channels between multiple -Grafana installations, refer to [alert notification channel provisioning]({{< relref "/docs/grafana/latest/administration/provisioning#alert-notification-channels" >}}). - -The uid can have a maximum length of 40 characters. - -## Get all notification channels - -Returns all notification channels that the authenticated user has permission to view. - -`GET /api/alert-notifications` - -**Example request**: - -```http -GET /api/alert-notifications HTTP/1.1 -Accept: application/json -Content-Type: application/json -Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk -``` - -**Example response**: - -```http -HTTP/1.1 200 -Content-Type: application/json - -[ - { - "id": 1, - "uid": "team-a-email-notifier", - "name": "Team A", - "type": "email", - "isDefault": false, - "sendReminder": false, - "disableResolveMessage": false, - "settings": { - "addresses": "dev@grafana.com" - }, - "created": "2018-04-23T14:44:09+02:00", - "updated": "2018-08-20T15:47:49+02:00" - } -] - -``` - -## Get all notification channels (lookup) - -Returns all notification channels, but with less detailed information. Accessible by any authenticated user and is mainly used by providing alert notification channels in Grafana UI when configuring alert rule. - -`GET /api/alert-notifications/lookup` - -**Example request**: - -```http -GET /api/alert-notifications/lookup HTTP/1.1 -Accept: application/json -Content-Type: application/json -Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk -``` - -**Example response**: - -```http -HTTP/1.1 200 -Content-Type: application/json - -[ - { - "id": 1, - "uid": "000000001", - "name": "Test", - "type": "email", - "isDefault": false - }, - { - "id": 2, - "uid": "000000002", - "name": "Slack", - "type": "slack", - "isDefault": false - } -] - -``` - -## Get notification channel by uid - -`GET /api/alert-notifications/uid/:uid` - -Returns the notification channel given the notification channel uid. - -**Example request**: - -```http -GET /api/alert-notifications/uid/team-a-email-notifier HTTP/1.1 -Accept: application/json -Content-Type: application/json -Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk -``` - -**Example response**: - -```http -HTTP/1.1 200 -Content-Type: application/json - -{ - "id": 1, - "uid": "team-a-email-notifier", - "name": "Team A", - "type": "email", - "isDefault": false, - "sendReminder": false, - "disableResolveMessage": false, - "settings": { - "addresses": "dev@grafana.com" - }, - "created": "2018-04-23T14:44:09+02:00", - "updated": "2018-08-20T15:47:49+02:00" -} -``` - -## Get notification channel by id - -`GET /api/alert-notifications/:id` - -Returns the notification channel given the notification channel id. - -**Example request**: - -```http -GET /api/alert-notifications/1 HTTP/1.1 -Accept: application/json -Content-Type: application/json -Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk -``` - -**Example response**: - -```http -HTTP/1.1 200 -Content-Type: application/json - -{ - "id": 1, - "uid": "team-a-email-notifier", - "name": "Team A", - "type": "email", - "isDefault": false, - "sendReminder": false, - "disableResolveMessage": false, - "settings": { - "addresses": "dev@grafana.com" - }, - "created": "2018-04-23T14:44:09+02:00", - "updated": "2018-08-20T15:47:49+02:00" -} -``` - -## Create notification channel - -You can find the full list of [supported notifiers](/docs/grafana/v8.5/alerting/old-alerting/notifications/) on the alert notifiers page. - -`POST /api/alert-notifications` - -**Example request**: - -```http -POST /api/alert-notifications HTTP/1.1 -Accept: application/json -Content-Type: application/json -Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk - -{ - "uid": "new-alert-notification", // optional - "name": "new alert notification", //Required - "type": "email", //Required - "isDefault": false, - "sendReminder": false, - "settings": { - "addresses": "dev@grafana.com" - } -} -``` - -**Example response**: - -```http -HTTP/1.1 200 -Content-Type: application/json - -{ - "id": 1, - "uid": "new-alert-notification", - "name": "new alert notification", - "type": "email", - "isDefault": false, - "sendReminder": false, - "settings": { - "addresses": "dev@grafana.com" - }, - "created": "2018-04-23T14:44:09+02:00", - "updated": "2018-08-20T15:47:49+02:00" -} -``` - -## Update notification channel by uid - -`PUT /api/alert-notifications/uid/:uid` - -Updates an existing notification channel identified by uid. - -**Example request**: - -```http -PUT /api/alert-notifications/uid/cIBgcSjkk HTTP/1.1 -Accept: application/json -Content-Type: application/json -Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk - -{ - "uid": "new-alert-notification", // optional - "name": "new alert notification", //Required - "type": "email", //Required - "isDefault": false, - "sendReminder": true, - "frequency": "15m", - "settings": { - "addresses": "dev@grafana.com" - } -} -``` - -**Example response**: - -```http -HTTP/1.1 200 -Content-Type: application/json - -{ - "id": 1, - "uid": "new-alert-notification", - "name": "new alert notification", - "type": "email", - "isDefault": false, - "sendReminder": true, - "frequency": "15m", - "settings": { - "addresses": "dev@grafana.com" - }, - "created": "2017-01-01 12:34", - "updated": "2017-01-01 12:34" -} -``` - -## Update notification channel by id - -`PUT /api/alert-notifications/:id` - -Updates an existing notification channel identified by id. - -**Example request**: - -```http -PUT /api/alert-notifications/1 HTTP/1.1 -Accept: application/json -Content-Type: application/json -Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk - -{ - "id": 1, - "uid": "new-alert-notification", // optional - "name": "new alert notification", //Required - "type": "email", //Required - "isDefault": false, - "sendReminder": true, - "frequency": "15m", - "settings": { - "addresses": "dev@grafana.com" - } -} -``` - -**Example response**: - -```http -HTTP/1.1 200 -Content-Type: application/json - -{ - "id": 1, - "uid": "new-alert-notification", - "name": "new alert notification", - "type": "email", - "isDefault": false, - "sendReminder": true, - "frequency": "15m", - "settings": { - "addresses": "dev@grafana.com" - }, - "created": "2017-01-01 12:34", - "updated": "2017-01-01 12:34" -} -``` - -## Delete alert notification by uid - -`DELETE /api/alert-notifications/uid/:uid` - -Deletes an existing notification channel identified by uid. - -**Example request**: - -```http -DELETE /api/alert-notifications/uid/team-a-email-notifier HTTP/1.1 -Accept: application/json -Content-Type: application/json -Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk -``` - -**Example response**: - -```http -HTTP/1.1 200 -Content-Type: application/json - -{ - "message": "Notification deleted" -} -``` - -## Delete alert notification by id - -`DELETE /api/alert-notifications/:id` - -Deletes an existing notification channel identified by id. - -**Example request**: - -```http -DELETE /api/alert-notifications/1 HTTP/1.1 -Accept: application/json -Content-Type: application/json -Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk -``` - -**Example response**: - -```http -HTTP/1.1 200 -Content-Type: application/json - -{ - "message": "Notification deleted" -} -``` - -## Test notification channel - -Sends a test notification message for the given notification channel type and settings. -You can find the full list of [supported notifiers](/alerting/notifications/#all-supported-notifier) at the alert notifiers page. - -`POST /api/alert-notifications/test` - -**Example request**: - -```http -POST /api/alert-notifications/test HTTP/1.1 -Accept: application/json -Content-Type: application/json -Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk - -{ - "type": "email", - "settings": { - "addresses": "dev@grafana.com" - } -} -``` - -**Example response**: - -```http -HTTP/1.1 200 -Content-Type: application/json - -{ - "message": "Test notification sent" -} -``` diff --git a/docs/sources/old-alerting/_index.md b/docs/sources/old-alerting/_index.md deleted file mode 100644 index 12dd7a2687..0000000000 --- a/docs/sources/old-alerting/_index.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -draft: true -labels: - products: - - enterprise - - oss -title: Legacy Grafana alerts -weight: 114 ---- - -# Legacy Grafana alerts - -Grafana Alerting is enabled by default for new OSS installations. For older installations, it is still an [opt-in]({{< relref "../alerting/migrating-alerts/opt-in" >}}) feature. - -{{% admonition type="note" %}} -Legacy dashboard alerts are deprecated and will be removed in Grafana 9. We encourage you to migrate to [Grafana Alerting]({{< relref "../alerting/migrating-alerts" >}}) for all existing installations. -{{% /admonition %}} - -Legacy dashboard alerts have two main components: - -- Alert rule - When the alert is triggered. Alert rules are defined by one or more conditions that are regularly evaluated by Grafana. -- Notification channel - How the alert is delivered. When the conditions of an alert rule are met, the Grafana notifies the channels configured for that alert. - -## Alert tasks - -You can perform the following tasks for alerts: - -- [Create an alert rule]({{< relref "./create-alerts" >}}) -- [View existing alert rules and their current state]({{< relref "./view-alerts" >}}) -- [Test alert rules and troubleshoot]({{< relref "./troubleshoot-alerts" >}}) -- [Add or edit an alert contact point]({{< relref "./notifications" >}}) - -{{< docs/shared lookup="alerts/grafana-managed-alerts.md" source="grafana" version="" >}} diff --git a/docs/sources/old-alerting/add-notification-template.md b/docs/sources/old-alerting/add-notification-template.md deleted file mode 100644 index f2cea0cac2..0000000000 --- a/docs/sources/old-alerting/add-notification-template.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -aliases: - - ../alerting/add-notification-template/ -draft: true -keywords: - - grafana - - documentation - - alerting - - alerts - - notification - - templating -labels: - products: - - enterprise - - oss -title: Alert notification templating -weight: 110 ---- - -# Alert notification templating - -You can provide detailed information to alert notification recipients by injecting alert query data into an alert notification. This topic explains how you can use alert query labels in alert notifications. - -You can use labels generated during an alerting query evaluation to create alert notification messages. For multiple unique values for the same label, the values are comma-separated. - -When an alert fires, the alerting data series indicates the violation. For resolved alerts, all data series are included in the resolved notification. - -This topic explains how you can use alert query labels in alert notifications. - -## Adding alert label data into your alert notification - -1. Navigate to the panel you want to add or edit an alert rule for. -1. Click on the panel title, and then click **Edit**. -1. On the Alert tab, click **Create Alert**. If an alert already exists for this panel, then you can edit the alert directly. -1. Refer to the alert query labels in the alert rule name and/or alert notification message field by using the `${Label}` syntax. -1. Click **Save** in the upper right corner to save the alert rule and the dashboard. - -![Alerting notification template](/static/img/docs/alerting/alert-notification-template-7-4.png) diff --git a/docs/sources/old-alerting/create-alerts.md b/docs/sources/old-alerting/create-alerts.md deleted file mode 100644 index f60f8afebf..0000000000 --- a/docs/sources/old-alerting/create-alerts.md +++ /dev/null @@ -1,138 +0,0 @@ ---- -aliases: - - ../alerting/create-alerts/ -description: Configure alert rules -draft: true -keywords: - - grafana - - alerting - - guide - - rules -labels: - products: - - enterprise - - oss -title: Create alerts -weight: 200 ---- - -# Create alerts - -Grafana Alerting allows you to attach rules to your dashboard panels. When you save the dashboard, Grafana extracts the alert rules into a separate alert rule storage and schedules them for evaluation. - -![Alerting overview](/static/img/docs/alerting/drag_handles_gif.gif) - -In the Alert tab of the graph panel you can configure how often the alert rule should be evaluated and the conditions that need to be met for the alert to change state and trigger its [notifications]({{< relref "./notifications" >}}). - -Currently only the graph panel supports alert rules. - -## Add or edit an alert rule - -1. Navigate to the panel you want to add or edit an alert rule for, click the title, and then click **Edit**. -1. On the Alert tab, click **Create Alert**. If an alert already exists for this panel, then you can just edit the fields on the Alert tab. -1. Fill out the fields. Descriptions are listed below in [Alert rule fields](#alert-rule-fields). -1. When you have finished writing your rule, click **Save** in the upper right corner to save alert rule and the dashboard. -1. (Optional but recommended) Click **Test rule** to make sure the rule returns the results you expect. - -## Delete an alert - -To delete an alert, scroll to the bottom of the alert and then click **Delete**. - -## Alert rule fields - -This section describes the fields you fill out to create an alert. - -### Rule - -- **Name -** Enter a descriptive name. The name will be displayed in the Alert Rules list. This field supports [templating]({{< relref "./add-notification-template" >}}). -- **Evaluate every -** Specify how often the scheduler should evaluate the alert rule. This is referred to as the _evaluation interval_. -- **For -** Specify how long the query needs to violate the configured thresholds before the alert notification triggers. - -You can set a minimum evaluation interval in the `alerting.min_interval_seconds` configuration field, to set a minimum time between evaluations. Refer to [Configuration]({{< relref "../setup-grafana/configure-grafana#min_interval_seconds" >}}) for more information. - -{{% admonition type="caution" %}} -Do not use `For` with the `If no data or all values are null` setting set to `No Data`. The triggering of `No Data` will trigger instantly and not take `For` into consideration. This may also result in that an OK notification not being sent if alert transitions from `No Data -Pending -OK`. -{{% /admonition %}} - -If an alert rule has a configured `For` and the query violates the configured threshold, then it will first go from `OK` to `Pending`. Going from `OK` to `Pending` Grafana will not send any notifications. Once the alert rule has been firing for more than `For` duration, it will change to `Alerting` and send alert notifications. - -Typically, it's always a good idea to use this setting since it's often worse to get false positive than wait a few minutes before the alert notification triggers. Looking at the `Alert list` or `Alert list panels` you will be able to see alerts in pending state. - -Below you can see an example timeline of an alert using the `For` setting. At ~16:04 the alert state changes to `Pending` and after 4 minutes it changes to `Alerting` which is when alert notifications are sent. Once the series falls back to normal the alert rule goes back to `OK`. -{{< figure class="float-right" src="/static/img/docs/v54/alerting-for-dark-theme.png" caption="Alerting For" >}} - -{{< figure class="float-right" max-width="40%" src="/static/img/docs/v4/alerting_conditions.png" caption="Alerting Conditions" >}} - -### Conditions - -Currently the only condition type that exists is a `Query` condition that allows you to -specify a query letter, time range and an aggregation function. - -#### Query condition example - -```sql -avg() OF query(A, 15m, now) IS BELOW 14 -``` - -- `avg()` Controls how the values for **each** series should be reduced to a value that can be compared against the threshold. Click on the function to change it to another aggregation function. -- `query(A, 15m, now)` The letter defines what query to execute from the **Metrics** tab. The second two parameters define the time range, `15m, now` means 15 minutes ago to now. You can also do `10m, now-2m` to define a time range that will be 10 minutes ago to 2 minutes ago. This is useful if you want to ignore the last 2 minutes of data. -- `IS BELOW 14` Defines the type of threshold and the threshold value. You can click on `IS BELOW` to change the type of threshold. - -The query used in an alert rule cannot contain any template variables. Currently we only support `AND` and `OR` operators between conditions and they are executed serially. -For example, we have 3 conditions in the following order: -_condition:A(evaluates to: TRUE) OR condition:B(evaluates to: FALSE) AND condition:C(evaluates to: TRUE)_ -so the result will be calculated as ((TRUE OR FALSE) AND TRUE) = TRUE. - -We plan to add other condition types in the future, like `Other Alert`, where you can include the state of another alert in your conditions, and `Time Of Day`. - -#### Multiple Series - -If a query returns multiple series, then the aggregation function and threshold check will be evaluated for each series. What Grafana does not do currently is track alert rule state **per series**. This has implications that are detailed in the scenario below. - -- Alert condition with query that returns 2 series: **server1** and **server2** -- **server1** series causes the alert rule to fire and switch to state `Alerting` -- Notifications are sent out with message: _load peaking (server1)_ -- In a subsequent evaluation of the same alert rule, the **server2** series also causes the alert rule to fire -- No new notifications are sent as the alert rule is already in state `Alerting`. - -So, as you can see from the above scenario Grafana will not send out notifications when other series cause the alert to fire if the rule already is in state `Alerting`. To improve support for queries that return multiple series we plan to track state **per series** in a future release. - -> Starting with Grafana v5.3 you can configure reminders to be sent for triggered alerts. This will send additional notifications -> when an alert continues to fire. If other series (like server2 in the example above) also cause the alert rule to fire they will be included in the reminder notification. Depending on what notification channel you're using you may be able to take advantage of this feature for identifying new/existing series causing alert to fire. - -### No Data & Error Handling - -Below are conditions you can configure how the rule evaluation engine should handle queries that return no data or only null values. - -| No Data Option | Description | -| --------------- | ------------------------------------------------------------------------------------------ | -| No Data | Set alert rule state to `NoData` | -| Alerting | Set alert rule state to `Alerting` | -| Keep Last State | Keep the current alert rule state, whatever it is. | -| Ok | Not sure why you would want to send yourself an alert when things are okay, but you could. | - -### Execution errors or timeouts - -Tell Grafana how to handle execution or timeout errors. - -| Error or timeout option | Description | -| ----------------------- | -------------------------------------------------- | -| Alerting | Set alert rule state to `Alerting` | -| Keep Last State | Keep the current alert rule state, whatever it is. | - -If you have an unreliable time series store from which queries sometime timeout or fail randomly you can set this option to `Keep Last State` in order to basically ignore them. - -## Notifications - -In alert tab you can also specify alert rule notifications along with a detailed message about the alert rule. The message can contain anything, information about how you might solve the issue, link to runbook, and so on. - -The actual notifications are configured and shared between multiple alerts. Read -[Alert notifications]({{< relref "./notifications" >}}) for information on how to configure and set up notifications. - -- **Send to -** Select an alert notification channel if you have one set up. -- **Message -** Enter a text message to be sent on the notification channel. Some alert notifiers support transforming the text to HTML or other rich formats. This field supports [templating]({{< relref "./add-notification-template" >}}). -- **Tags -** Specify a list of tags (key/value) to be included in the notification. It is only supported by [some notifiers]({{< relref "./notifications#list-of-supported-notifiers" >}}). - -## Alert state history and annotations - -Alert state changes are recorded in the internal annotation table in Grafana's database. The state changes are visualized as annotations in the alert rule's graph panel. You can also go into the `State history` submenu in the alert tab to view and clear state history. diff --git a/docs/sources/old-alerting/notifications.md b/docs/sources/old-alerting/notifications.md deleted file mode 100644 index 1439ecad37..0000000000 --- a/docs/sources/old-alerting/notifications.md +++ /dev/null @@ -1,303 +0,0 @@ ---- -aliases: - - ../alerting/notifications/ -description: Alerting notifications guide -draft: true -keywords: - - Grafana - - alerting - - guide - - notifications -labels: - products: - - enterprise - - oss -title: Alert notifications -weight: 100 ---- - -# Alert notifications - -When an alert changes state, it sends out notifications. Each alert rule can have -multiple notifications. In order to add a notification to an alert rule you first need -to add and configure a `notification` channel (can be email, PagerDuty, or other integration). - -This is done from the Notification channels page. - -{{% admonition type="note" %}} -Alerting is only available in Grafana v4.0 and above. -{{% /admonition %}} - -## Add a notification channel - -1. In the Grafana side bar, hover your cursor over the **Alerting** (bell) icon and then click **Notification channels**. -1. Click **Add channel**. -1. Fill out the fields or select options described below. - -## New notification channel fields - -### Default (send on all alerts) - -- **Name -** Enter a name for this channel. It will be displayed when users add notifications to alert rules. -- **Type -** Select the channel type. Refer to the [List of supported notifiers](#list-of-supported-notifiers) for details. -- **Default (send on all alerts) -** When selected, this option sends a notification on this channel for all alert rules. -- **Include Image -** See [Enable images in notifications](#enable-images-in-notifications-external-image-store) for details. -- **Disable Resolve Message -** When selected, this option disables the resolve message [OK] that is sent when the alerting state returns to false. -- **Send reminders -** When this option is checked additional notifications (reminders) will be sent for triggered alerts. You can specify how often reminders should be sent using number of seconds (s), minutes (m) or hours (h), for example `30s`, `3m`, `5m` or `1h`. - -**Important:** Alert reminders are sent after rules are evaluated. Therefore a reminder can never be sent more frequently than a configured alert rule evaluation interval. - -These examples show how often and when reminders are sent for a triggered alert. - -| Alert rule evaluation interval | Send reminders every | Reminder sent every (after last alert notification) | -| ------------------------------ | -------------------- | --------------------------------------------------- | -| `30s` | `15s` | ~30 seconds | -| `1m` | `5m` | ~5 minutes | -| `5m` | `15m` | ~15 minutes | -| `6m` | `20m` | ~24 minutes | -| `1h` | `15m` | ~1 hour | -| `1h` | `2h` | ~2 hours | - -
- -## List of supported notifiers - -| Name | Type | Supports images | Supports alert rule tags | -| --------------------------------------------- | ------------------------- | ------------------ | ------------------------ | -| [DingDing](#dingdingdingtalk) | `dingding` | yes, external only | no | -| [Discord](#discord) | `discord` | yes | no | -| [Email](#email) | `email` | yes | no | -| [Google Hangouts Chat](#google-hangouts-chat) | `googlechat` | yes, external only | no | -| Hipchat | `hipchat` | yes, external only | no | -| [Kafka](#kafka) | `kafka` | yes, external only | no | -| Line | `line` | yes, external only | no | -| Microsoft Teams | `teams` | yes, external only | no | -| [Opsgenie](#opsgenie) | `opsgenie` | yes, external only | yes | -| [Pagerduty](#pagerduty) | `pagerduty` | yes, external only | yes | -| Prometheus Alertmanager | `prometheus-alertmanager` | yes, external only | yes | -| [Pushover](#pushover) | `pushover` | yes | no | -| Sensu | `sensu` | yes, external only | no | -| [Sensu Go](#sensu-go) | `sensugo` | yes, external only | no | -| [Slack](#slack) | `slack` | yes | no | -| Telegram | `telegram` | yes | no | -| Threema | `threema` | yes, external only | no | -| VictorOps | `victorops` | yes, external only | yes | -| [Webhook](#webhook) | `webhook` | yes, external only | yes | - -### Email - -To enable email notifications you have to set up [SMTP settings]({{< relref "../setup-grafana/configure-grafana#smtp" >}}) -in the Grafana config. Email notifications will upload an image of the alert graph to an -external image destination if available or fallback to attaching the image to the email. -Be aware that if you use the `local` image storage email servers and clients might not be -able to access the image. - -{{% admonition type="note" %}} -Template variables are not supported in email alerts. -{{% /admonition %}} - -| Setting | Description | -| ------------ | -------------------------------------------------------------------------------------------- | -| Single email | Send a single email to all recipients. Disabled per default. | -| Addresses | Email addresses to recipients. You can enter multiple email addresses using a ";" separator. | - -### Slack - -{{< figure class="float-right" max-width="40%" src="/static/img/docs/v4/slack_notification.png" caption="Alerting Slack Notification" >}} - -To set up Slack, you need to configure an incoming Slack webhook URL. You can follow -[Sending messages using Incoming Webhooks](https://api.slack.com/incoming-webhooks) on how to do that. If you want to include screenshots of the -firing alerts in the Slack messages you have to configure either the [external image destination](#enable-images-in-notifications-external-image-store) -in Grafana or a bot integration via Slack Apps. [Follow Slack's guide to set up a bot integration](https://api.slack.com/bot-users) and use the token -provided, which starts with "xoxb". - -| Setting | Description | -| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Url | Slack incoming webhook URL, or eventually the [chat.postMessage](https://api.slack.com/methods/chat.postMessage) Slack API endpoint. | -| Username | Set the username for the bot's message. | -| Recipient | Allows you to override the Slack recipient. You must either provide a channel Slack ID, a user Slack ID, a username reference (@<user>, all lowercase, no whitespace), or a channel reference (#<channel>, all lowercase, no whitespace). If you use the `chat.postMessage` Slack API endpoint, this is required. | -| Icon emoji | Provide an emoji to use as the icon for the bot's message. Ex :smile: | -| Icon URL | Provide a URL to an image to use as the icon for the bot's message. | -| Mention Users | Optionally mention one or more users in the Slack notification sent by Grafana. You have to refer to users, comma-separated, via their corresponding Slack IDs (which you can find by clicking the overflow button on each user's Slack profile). | -| Mention Groups | Optionally mention one or more groups in the Slack notification sent by Grafana. You have to refer to groups, comma-separated, via their corresponding Slack IDs (which you can get from each group's Slack profile URL). | -| Mention Channel | Optionally mention either all channel members or just active ones. | -| Token | If provided, Grafana will upload the generated image via Slack's file.upload API method, not the external image destination. If you use the `chat.postMessage` Slack API endpoint, this is required. | - -If you are using the token for a slack bot, then you have to invite the bot to the channel you want to send notifications and add the channel to the recipient field. - -### Opsgenie - -To setup Opsgenie you will need an API Key and the Alert API Url. These can be obtained by configuring a new [Grafana Integration](https://docs.opsgenie.com/docs/grafana-integration). - -| Setting | Description | -| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Alert API URL | The API URL for your Opsgenie instance. This will normally be either `https://api.opsgenie.com` or, for EU customers, `https://api.eu.opsgenie.com`. | -| API Key | The API Key as provided by Opsgenie for your configured Grafana integration. | -| Override priority | Configures the alert priority using the `og_priority` tag. The `og_priority` tag must have one of the following values: `P1`, `P2`, `P3`, `P4`, or `P5`. Default is `False`. | -| Send notification tags as | Specify how you would like [Notification Tags]({{< relref "./create-alerts#notifications" >}}) delivered to Opsgenie. They can be delivered as `Tags`, `Extra Properties` or both. Default is Tags. See note below for more information. | - -{{% admonition type="note" %}} -When notification tags are sent as `Tags` they are concatenated into a string with a `key:value` format. If you prefer to receive the notifications tags as key/values under Extra Properties in Opsgenie then change the `Send notification tags as` to either `Extra Properties` or `Tags & Extra Properties`. -{{% /admonition %}} - -### PagerDuty - -To set up PagerDuty, all you have to do is to provide an integration key. - -| Setting | Description | -| ---------------------- | ----------------------------------------------------------------------------------------------- | -| Integration Key | Integration key for PagerDuty. | -| Severity | Level for dynamic notifications, default is `critical` (1) | -| Auto resolve incidents | Resolve incidents in PagerDuty once the alert goes back to ok | -| Message in details | Removes the Alert message from the PD summary field and puts it into custom details instead (2) | - -> **Note:** The tags `Severity`, `Class`, `Group`, `dedup_key`, and `Component` have special meaning in the [Pagerduty Common Event Format - PD-CEF](https://support.pagerduty.com/docs/pd-cef). If an alert panel defines these tag keys, then they are transposed to the root of the event sent to Pagerduty. This means they will be available within the Pagerduty UI and Filtering tools. A Severity tag set on an alert overrides the global Severity set on the notification channel if it's a valid level. - -> Using Message In Details will change the structure of the `custom_details` field in the PagerDuty Event. -> This might break custom event rules in your PagerDuty rules if you rely on the fields in `payload.custom_details`. -> Move any existing rules using `custom_details.myMetric` to `custom_details.queries.myMetric`. -> This behavior will become the default in a future version of Grafana. - -> **Note:** The `dedup_key` tag overrides the Grafana-generated `dedup_key` with a custom key. - -> **Note:** The `state` tag overrides the current alert state inside the `custom_details` payload. - -> **Note:** Grafana uses the `Events API V2` integration. This can be configured for each service. - -### VictorOps - -To configure VictorOps, provide the URL from the Grafana Integration and substitute `$routing_key` with a valid key. - -> **Note:** The tag `Severity` has special meaning in the [VictorOps Incident Fields](https://help.victorops.com/knowledge-base/incident-fields-glossary/). If an alert panel defines this key, then it replaces the `message_type` in the root of the event sent to VictorOps. - -### Pushover - -To set up Pushover, you must provide a user key and an API token. Refer to [What is Pushover and how do I use it](https://support.pushover.net/i7-what-is-pushover-and-how-do-i-use-it) for instructions on how to generate them. - -| Setting | Description | -| -------------- | ----------------------------------------------------------------------------------------------------------------------------------- | -| API Token | Application token | -| User key(s) | A comma-separated list of user keys | -| Device(s) | A comma-separated list of devices | -| Priority | The priority alerting nottifications are sent | -| OK priority | The priority OK notifications are sent; if not set, then OK notifications are sent with the priority set for alerting notifications | -| Retry | How often (in seconds) the Pushover servers send the same notification to the user. (minimum 30 seconds) | -| Expire | How many seconds your notification will continue to be retried for (maximum 86400 seconds) | -| TTL | The number of seconds before a message expires and is deleted automatically. Examples: 10s, 5m30s, 8h. | -| Alerting sound | The sound for alerting notifications | -| OK sound | The sound for OK notifications | - -### Webhook - -The webhook notification is a simple way to send information about a state change over HTTP to a custom endpoint. -Using this notification you could integrate Grafana into a system of your choosing. - -Example json body: - -```json -{ - "dashboardId": 1, - "evalMatches": [ - { - "value": 1, - "metric": "Count", - "tags": {} - } - ], - "imageUrl": "https://grafana.com/static/assets/img/blog/mixed_styles.png", - "message": "Notification Message", - "orgId": 1, - "panelId": 2, - "ruleId": 1, - "ruleName": "Panel Title alert", - "ruleUrl": "http://localhost:3000/d/hZ7BuVbWz/test-dashboard?fullscreen\u0026edit\u0026tab=alert\u0026panelId=2\u0026orgId=1", - "state": "alerting", - "tags": { - "tag name": "tag value" - }, - "title": "[Alerting] Panel Title alert" -} -``` - -- **state** - The possible values for alert state are: `ok`, `paused`, `alerting`, `pending`, `no_data`. - -### DingDing/DingTalk - -DingTalk supports the following "message type": `text`, `link` and `markdown`. Only the `link` message type is supported. Refer to the [configuration instructions](https://developers.dingtalk.com/document/app/custom-robot-access) in Chinese language. - -In DingTalk PC Client: - -1. Click "more" icon on upper right of the panel. - -2. Click "Robot Manage" item in the pop menu, there will be a new panel call "Robot Manage". - -3. In the "Robot Manage" panel, select "customized: customized robot with Webhook". - -4. In the next new panel named "robot detail", click "Add" button. - -5. In "Add Robot" panel, input a nickname for the robot and select a "message group" which the robot will join in. click "next". - -6. There will be a Webhook URL in the panel, looks like this: https://oapi.dingtalk.com/robot/send?access_token=xxxxxxxxx. Copy this URL to the Grafana DingTalk setting page and then click "finish". - -### Discord - -To set up Discord, you must create a Discord channel webhook. For instructions on how to create the channel, refer to -[Intro to Webhooks](https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks). - -| Setting | Description | -| ------------------------------ | ----------------------------------------------------------------------------------------------------- | -| Webhook URL | Discord webhook URL. | -| Message Content | Mention a group using @ or a user using <@ID> when notifying in a channel. | -| Avatar URL | Optionally, provide a URL to an image to use as the avatar for the bot's message. | -| Use Discord's Webhook Username | Use the username configured in Discord's webhook settings. Otherwise, the username will be 'Grafana.' | - -Alternately, use the [Slack](#slack) notifier by appending `/slack` to a Discord webhook URL. - -### Kafka - -Notifications can be sent to a Kafka topic from Grafana using the [Kafka REST Proxy](https://docs.confluent.io/1.0/kafka-rest/docs/index.html). -There are a couple of configuration options which need to be set up in Grafana UI under Kafka Settings: - -1. Kafka REST Proxy endpoint. - -1. Kafka Topic. - -Once these two properties are set, you can send the alerts to Kafka for further processing or throttling. - -### Google Hangouts Chat - -Notifications can be sent by setting up an incoming webhook in Google Hangouts chat. For more information about configuring a webhook, refer to [webhooks](https://developers.google.com/hangouts/chat/how-tos/webhooks). - -### Prometheus Alertmanager - -Alertmanager handles alerts sent by client applications such as Prometheus server or Grafana. It takes care of deduplicating, grouping, and routing them to the correct receiver. Grafana notifications can be sent to Alertmanager via a simple incoming webhook. Refer to the official [Prometheus Alertmanager documentation](https://prometheus.io/docs/alerting/alertmanager) for configuration information. - -{{% admonition type="caution" %}} -In case of a high-availability setup, do not load balance traffic between Grafana and Alertmanagers to keep coherence between all your Alertmanager instances. Instead, point Grafana to a list of all Alertmanagers, by listing their URLs comma-separated in the notification channel configuration. -{{% /admonition %}} - -### Sensu Go - -Grafana alert notifications can be sent to [Sensu](https://sensu.io) Go as events via the API. This operation requires an API key. For information on creating this key, refer to [Sensu Go documentation](https://docs.sensu.io/sensu-go/latest/operations/control-access/use-apikeys/#api-key-authentication). - -## Enable images in notifications {#external-image-store} - -Grafana can render the panel associated with the alert rule as a PNG image and include that in the notification. Read more about the requirements and how to configure -[image rendering]({{< relref "../setup-grafana/image-rendering" >}}). - -You must configure an [external image storage provider]({{< relref "../setup-grafana/configure-grafana#external_image_storage" >}}) in order to receive images in alert notifications. If your notification channel requires that the image be publicly accessible (e.g. Slack, PagerDuty), configure a provider which uploads the image to a remote image store like Amazon S3, Webdav, Google Cloud Storage, or Azure Blob Storage. Otherwise, the local provider can be used to serve the image directly from Grafana. - -Notification services which need public image access are marked as 'external only'. - -## Configure the link back to Grafana from alert notifications - -All alert notifications contain a link back to the triggered alert in the Grafana instance. -This URL is based on the [domain]({{< relref "../setup-grafana/configure-grafana#domain" >}}) setting in Grafana. - -## Notification templating - -{{% admonition type="note" %}} -Alert notification templating is only available in Grafana v7.4 and above. -{{% /admonition %}} - -The alert notification template feature allows you to take the [label]({{< relref "../fundamentals/timeseries-dimensions#labels" >}}) value from an alert query and [inject that into alert notifications]({{< relref "./add-notification-template" >}}). diff --git a/docs/sources/old-alerting/pause-an-alert-rule.md b/docs/sources/old-alerting/pause-an-alert-rule.md deleted file mode 100644 index 8cc675f7d6..0000000000 --- a/docs/sources/old-alerting/pause-an-alert-rule.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -aliases: - - ../alerting/pause-an-alert-rule/ -description: Pause an existing alert rule -draft: true -keywords: - - grafana - - alerting - - guide - - rules - - view -labels: - products: - - enterprise - - oss -title: Pause an alert rule -weight: 400 ---- - -# Pause an alert rule - -Pausing the evaluation of an alert rule can sometimes be useful. For example, during a maintenance window, pausing alert rules can avoid triggering a flood of alerts. - -1. In the Grafana side bar, hover your cursor over the Alerting (bell) icon and then click **Alert Rules**. All configured alert rules are listed, along with their current state. -1. Find your alert in the list, and click the **Pause** icon on the right. The **Pause** icon turns into a **Play** icon. -1. Click the **Play** icon to resume evaluation of your alert. diff --git a/docs/sources/old-alerting/troubleshoot-alerts.md b/docs/sources/old-alerting/troubleshoot-alerts.md deleted file mode 100644 index 053bc34690..0000000000 --- a/docs/sources/old-alerting/troubleshoot-alerts.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -aliases: - - ../alerting/troubleshoot-alerts/ -description: Troubleshoot alert rules -draft: true -keywords: - - grafana - - alerting - - guide - - rules - - troubleshoot -labels: - products: - - enterprise - - oss -title: Troubleshoot alerts -weight: 500 ---- - -# Troubleshoot alerts - -If alerts are not behaving as you expect, here are some steps you can take to troubleshoot and figure out what is going wrong. - -![Test Rule](/static/img/docs/v4/alert_test_rule.png) - -The first level of troubleshooting you can do is click **Test Rule**. You will get result back that you can expand to the point where you can see the raw data that was returned from your query. - -Further troubleshooting can also be done by inspecting the grafana-server log. If it's not an error or for some reason the log does not say anything you can enable debug logging for some relevant components. This is done in Grafana's ini config file. - -Example showing loggers that could be relevant when troubleshooting alerting. - -```ini -[log] -filters = alerting.scheduler:debug \ - alerting.engine:debug \ - alerting.resultHandler:debug \ - alerting.evalHandler:debug \ - alerting.evalContext:debug \ - alerting.extractor:debug \ - alerting.notifier:debug \ - alerting.notifier.slack:debug \ - alerting.notifier.pagerduty:debug \ - alerting.notifier.email:debug \ - alerting.notifier.webhook:debug \ - tsdb.graphite:debug \ - tsdb.prometheus:debug \ - tsdb.opentsdb:debug \ - tsdb.influxdb:debug \ - tsdb.elasticsearch:debug \ - tsdb.elasticsearch.client:debug \ -``` - -If you want to log raw query sent to your TSDB and raw response in log you also have to set grafana.ini option `app_mode` to `development`. diff --git a/docs/sources/old-alerting/view-alerts.md b/docs/sources/old-alerting/view-alerts.md deleted file mode 100644 index b927715685..0000000000 --- a/docs/sources/old-alerting/view-alerts.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -aliases: - - ../alerting/view-alerts/ -description: View existing alert rules -draft: true -keywords: - - grafana - - alerting - - guide - - rules - - view -labels: - products: - - enterprise - - oss -menuTitle: View alerts -title: View existing alert rules -weight: 400 ---- - -# View existing alert rules - -Grafana stores individual alert rules in the panels where they are defined, but you can also view a list of all existing alert rules and their current state. - -In the Grafana side bar, hover your cursor over the Alerting (bell) icon and then click **Alert Rules**. All configured alert rules are listed, along with their current state. - -You can do several things while viewing alerts. - -- **Filter alerts by name -** Type an alert name in the **Search alerts** field. -- **Filter alerts by state -** In **States**, select which alert states you want to see. All others will be hidden. -- **Pause or resume an alert -** Click the **Pause** or **Play** icon next to the alert to pause or resume evaluation. See [Pause an alert rule]({{< relref "./pause-an-alert-rule" >}}) for more information. -- **Access alert rule settings -** Click the alert name or the **Edit alert rule** (gear) icon. Grafana opens the Alert tab of the panel where the alert rule is defined. This is helpful when an alert is firing but you don't know which panel it is defined in. diff --git a/docs/sources/setup-grafana/configure-grafana/_index.md b/docs/sources/setup-grafana/configure-grafana/_index.md index 579f9d3121..32ca4c9a5f 100644 --- a/docs/sources/setup-grafana/configure-grafana/_index.md +++ b/docs/sources/setup-grafana/configure-grafana/_index.md @@ -148,20 +148,6 @@ Options are `production` and `development`. Default is `production`. _Do not_ ch Set the name of the grafana-server instance. Used in logging, internal metrics, and clustering info. Defaults to: `${HOSTNAME}`, which will be replaced with environment variable `HOSTNAME`, if that is empty or does not exist Grafana will try to use system calls to get the machine name. -## force_migration - -{{% admonition type="note" %}} -This option is deprecated - [See `clean_upgrade` option]({{< relref "#clean_upgrade" >}}) instead. -{{% /admonition %}} - -When you restart Grafana to rollback from Grafana Alerting to legacy alerting, delete any existing Grafana Alerting data, such as alert rules, contact points, and notification policies. Default is `false`. - -If `false` or unset, existing Grafana Alerting data is not changed or deleted when rolling back to legacy alerting. - -{{% admonition type="note" %}} -It should be kept false or unset when not needed, as it may cause unintended data loss if left enabled. -{{% /admonition %}} -
## [paths] @@ -707,7 +693,6 @@ The core features that depend on angular are: - Old graph panel - Old table panel -- Legacy alerting edit rule UI These features each have supported alternatives, and we recommend using them. @@ -1520,9 +1505,9 @@ For more information about the Grafana alerts, refer to [About Grafana Alerting] ### enabled -Enable or disable Grafana Alerting. If disabled, all your legacy alerting data will be available again. The default value is `true`. +Enable or disable Grafana Alerting. The default value is `true`. -Alerting Rules migrated from dashboards and panels will include a link back via the `annotations`. +Alerting rules migrated from dashboards and panels will include a link back via the `annotations`. ### disabled_orgs @@ -1674,22 +1659,6 @@ Configures max number of alert annotations that Grafana stores. Default value is
-## [unified_alerting.upgrade] - -For more information about upgrading to Grafana Alerting, refer to [Upgrade Alerting](/docs/grafana/next/alerting/set-up/migrating-alerts/). - -### clean_upgrade - -When you restart Grafana to upgrade from legacy alerting to Grafana Alerting, delete any existing Grafana Alerting data from a previous upgrade, such as alert rules, contact points, and notification policies. Default is `false`. - -If `false` or unset, existing Grafana Alerting data is not changed or deleted when you switch between legacy and Unified Alerting. - -{{% admonition type="note" %}} -It should be kept false when not needed, as it may cause unintended data loss if left enabled. -{{% /admonition %}} - -
- ## [annotations] ### cleanupjob_batchsize 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 284141a103..79fc69f158 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -54,10 +54,8 @@ Some features are enabled by default. You can disable these feature by setting t | `lokiStructuredMetadata` | Enables the loki data source to request structured metadata from the Loki server | Yes | | `logRowsPopoverMenu` | Enable filtering menu displayed when text of a log line is selected | Yes | | `lokiQueryHints` | Enables query hints for Loki | Yes | -| `alertingPreviewUpgrade` | Show Unified Alerting preview and upgrade page in legacy alerting | Yes | | `alertingQueryOptimization` | Optimizes eligible queries in order to reduce load on datasources | | | `betterPageScrolling` | Removes CustomScrollbar from the UI, relying on native browser scrollbars | Yes | -| `alertingUpgradeDryrunOnStart` | When activated in legacy alerting mode, this initiates a dry-run of the Unified Alerting upgrade during each startup. It logs any issues detected without implementing any actual changes. | Yes | ## Preview feature toggles diff --git a/docs/sources/setup-grafana/configure-security/audit-grafana.md b/docs/sources/setup-grafana/configure-security/audit-grafana.md index f071fe3e79..4f213bd204 100644 --- a/docs/sources/setup-grafana/configure-security/audit-grafana.md +++ b/docs/sources/setup-grafana/configure-security/audit-grafana.md @@ -269,41 +269,6 @@ external group. \* `resources` may also contain a third item with `"type":` set to `"user"` or `"team"`. -#### Alerts and notification channels management - -| Action | Distinguishing fields | -| --------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | -| Save alert manager configuration | `{"action": "update", "requestUri": "/api/alertmanager/RECIPIENT/config/api/v1/alerts"}` | -| Reset alert manager configuration | `{"action": "delete", "requestUri": "/api/alertmanager/RECIPIENT/config/api/v1/alerts"}` | -| Create silence | `{"action": "create", "requestUri": "/api/alertmanager/RECIPIENT/api/v2/silences"}` | -| Delete silence | `{"action": "delete", "requestUri": "/api/alertmanager/RECIPIENT/api/v2/silences/SILENCE-ID"}` | -| Create alert | `{"action": "create", "requestUri": "/api/ruler/RECIPIENT/api/v2/alerts"}` | -| Create or update rule group | `{"action": "create-update", "requestUri": "/api/ruler/RECIPIENT/api/v1/rules/NAMESPACE"}` | -| Delete rule group | `{"action": "delete", "requestUri": "/api/ruler/RECIPIENT/api/v1/rules/NAMESPACE/GROUP-NAME"}` | -| Delete namespace | `{"action": "delete", "requestUri": "/api/ruler/RECIPIENT/api/v1/rules/NAMESPACE"}` | -| Test Grafana managed receivers | `{"action": "test", "requestUri": "/api/alertmanager/RECIPIENT/config/api/v1/receivers/test"}` | -| Create or update the NGalert configuration of the user's organization | `{"action": "create-update", "requestUri": "/api/v1/ngalert/admin_config"}` | -| Delete the NGalert configuration of the user's organization | `{"action": "delete", "requestUri": "/api/v1/ngalert/admin_config"}` | - -Where the following: - -- `RECIPIENT` is `grafana` for requests handled by Grafana or the data source UID for requests forwarded to a data source. -- `NAMESPACE` is the string identifier for the rules namespace. -- `GROUP-NAME` is the string identifier for the rules group. -- `SILENCE-ID` is the ID of the affected silence. - -The following legacy alerting actions are still supported: - -| Action | Distinguishing fields | -| --------------------------------- | --------------------------------------------------------------------- | -| Test alert rule | `{"action": "test", "resources": [{"type": "panel"}]}` | -| Pause alert | `{"action": "pause", "resources": [{"type": "alert"}]}` | -| Pause all alerts | `{"action": "pause-all"}` | -| Test alert notification channel | `{"action": "test", "resources": [{"type": "alert-notification"}]}` | -| Create alert notification channel | `{"action": "create", "resources": [{"type": "alert-notification"}]}` | -| Update alert notification channel | `{"action": "update", "resources": [{"type": "alert-notification"}]}` | -| Delete alert notification channel | `{"action": "delete", "resources": [{"type": "alert-notification"}]}` | - #### Reporting | Action | Distinguishing fields | diff --git a/docs/sources/setup-grafana/configure-security/configure-database-encryption/encrypt-secrets-using-aws-kms/index.md b/docs/sources/setup-grafana/configure-security/configure-database-encryption/encrypt-secrets-using-aws-kms/index.md index 32e1c77a27..f27851f1bd 100644 --- a/docs/sources/setup-grafana/configure-security/configure-database-encryption/encrypt-secrets-using-aws-kms/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-database-encryption/encrypt-secrets-using-aws-kms/index.md @@ -73,8 +73,6 @@ You can use an encryption key from AWS Key Management Service to encrypt secrets available_encryption_providers = awskms.example-encryption-key ``` - **> Note:** The encryption key that is stored in the `secret_key` field is still used by Grafana’s legacy alerting system to encrypt secrets, for decrypting existing secrets, or it is used as the default provider when external providers are not configured. Do not change or remove that value when adding a new KMS provider. - 7. [Restart Grafana](/docs/grafana/latest/installation/restart-grafana/). 8. (Optional) From the command line and the root directory of Grafana, re-encrypt all of the secrets within the Grafana database with the new key using the following command: @@ -83,6 +81,6 @@ You can use an encryption key from AWS Key Management Service to encrypt secrets If you do not re-encrypt existing secrets, then they will remain encrypted by the previous encryption key. Users will still be able to access them. - **> Note:** This process could take a few minutes to complete, depending on the number of secrets (such as data sources or alert notification channels) in your database. Users might experience errors while this process is running, and alert notifications might not be sent. + **> Note:** This process could take a few minutes to complete, depending on the number of secrets (such as data sources) in your database. Users might experience errors while this process is running, and alert notifications might not be sent. - **> Note:** If you are updating this encryption key during the initial setup of Grafana before any data sources, alert notification channels, or dashboards have been created, then this step is not necessary because there are no secrets in Grafana to migrate. + **> Note:** If you are updating this encryption key during the initial setup of Grafana before any data sources or dashboards have been created, then this step is not necessary because there are no secrets in Grafana to migrate. diff --git a/docs/sources/setup-grafana/configure-security/configure-database-encryption/encrypt-secrets-using-azure-key-vault/index.md b/docs/sources/setup-grafana/configure-security/configure-database-encryption/encrypt-secrets-using-azure-key-vault/index.md index 7a6b8736c0..e90f81bc6d 100644 --- a/docs/sources/setup-grafana/configure-security/configure-database-encryption/encrypt-secrets-using-azure-key-vault/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-database-encryption/encrypt-secrets-using-azure-key-vault/index.md @@ -71,8 +71,6 @@ You can use an encryption key from Azure Key Vault to encrypt secrets in the Gra available_encryption_providers = azurekv.example-encryption-key ``` - **> Note:** The encryption key stored in the `secret_key` field is still used by Grafana’s legacy alerting system to encrypt secrets. Do not change or remove that value. - 9. [Restart Grafana](/docs/grafana/latest/installation/restart-grafana/). 10. (Optional) From the command line and the root directory of Grafana Enterprise, re-encrypt all of the secrets within the Grafana database with the new key using the following command: @@ -81,6 +79,6 @@ You can use an encryption key from Azure Key Vault to encrypt secrets in the Gra If you do not re-encrypt existing secrets, then they will remain encrypted by the previous encryption key. Users will still be able to access them. - **> Note:** This process could take a few minutes to complete, depending on the number of secrets (such as data sources or alert notification channels) in your database. Users might experience errors while this process is running, and alert notifications might not be sent. + **> Note:** This process could take a few minutes to complete, depending on the number of secrets (such as data sources) in your database. Users might experience errors while this process is running, and alert notifications might not be sent. - **> Note:** If you are updating this encryption key during the initial setup of Grafana before any data sources, alert notification channels, or dashboards have been created, then this step is not necessary because there are no secrets in Grafana to migrate. + **> Note:** If you are updating this encryption key during the initial setup of Grafana before any data sources or dashboards have been created, then this step is not necessary because there are no secrets in Grafana to migrate. diff --git a/docs/sources/setup-grafana/configure-security/configure-database-encryption/encrypt-secrets-using-google-cloud-kms/index.md b/docs/sources/setup-grafana/configure-security/configure-database-encryption/encrypt-secrets-using-google-cloud-kms/index.md index 95433c423f..240836fa30 100644 --- a/docs/sources/setup-grafana/configure-security/configure-database-encryption/encrypt-secrets-using-google-cloud-kms/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-database-encryption/encrypt-secrets-using-google-cloud-kms/index.md @@ -60,8 +60,6 @@ You can use an encryption key from Google Cloud Key Management Service to encryp available_encryption_providers = googlekms.example-encryption-key ``` - **> Note:** The encryption key stored in the `secret_key` field is still used by Grafana’s legacy alerting system to encrypt secrets. Do not change or remove that value. - 8. [Restart Grafana](/docs/grafana/latest/installation/restart-grafana/). 9. (Optional) From the command line and the root directory of Grafana Enterprise, re-encrypt all of the secrets within the Grafana database with the new key using the following command: @@ -70,6 +68,6 @@ You can use an encryption key from Google Cloud Key Management Service to encryp If you do not re-encrypt existing secrets, then they will remain encrypted by the previous encryption key. Users will still be able to access them. - **> Note:** This process could take a few minutes to complete, depending on the number of secrets (such as data sources or alert notification channels) in your database. Users might experience errors while this process is running, and alert notifications might not be sent. + **> Note:** This process could take a few minutes to complete, depending on the number of secrets (such as data sources) in your database. Users might experience errors while this process is running, and alert notifications might not be sent. - **> Note:** If you are updating this encryption key during the initial setup of Grafana before any data sources, alert notification channels, or dashboards have been created, then this step is not necessary because there are no secrets in Grafana to migrate. + **> Note:** If you are updating this encryption key during the initial setup of Grafana before any data sources or dashboards have been created, then this step is not necessary because there are no secrets in Grafana to migrate. diff --git a/docs/sources/setup-grafana/configure-security/configure-database-encryption/encrypt-secrets-using-hashicorp-key-vault/index.md b/docs/sources/setup-grafana/configure-security/configure-database-encryption/encrypt-secrets-using-hashicorp-key-vault/index.md index b7626e5d60..76bfd46686 100644 --- a/docs/sources/setup-grafana/configure-security/configure-database-encryption/encrypt-secrets-using-hashicorp-key-vault/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-database-encryption/encrypt-secrets-using-hashicorp-key-vault/index.md @@ -67,8 +67,6 @@ You can use an encryption key from Hashicorp Vault to encrypt secrets in the Gra available_encryption_providers = hashicorpvault.example-encryption-key ``` - **> Note:** The encryption key stored in the `secret_key` field is still used by Grafana’s legacy alerting system to encrypt secrets. Do not change or remove that value. - 7. [Restart Grafana](/docs/grafana/latest/installation/restart-grafana/). 8. (Optional) From the command line and the root directory of Grafana Enterprise, re-encrypt all of the secrets within the Grafana database with the new key using the following command: @@ -77,6 +75,6 @@ You can use an encryption key from Hashicorp Vault to encrypt secrets in the Gra If you do not re-encrypt existing secrets, then they will remain encrypted by the previous encryption key. Users will still be able to access them. - **> Note:** This process could take a few minutes to complete, depending on the number of secrets (such as data sources or alert notification channels) in your database. Users might experience errors while this process is running, and alert notifications might not be sent. + **> Note:** This process could take a few minutes to complete, depending on the number of secrets (such as data sources) in your database. Users might experience errors while this process is running, and alert notifications might not be sent. - **> Note:** If you are updating this encryption key during the initial setup of Grafana before any data sources, alert notification channels, or dashboards have been created, then this step is not necessary because there are no secrets in Grafana to migrate. + **> Note:** If you are updating this encryption key during the initial setup of Grafana before any data sources or dashboards have been created, then this step is not necessary because there are no secrets in Grafana to migrate. diff --git a/docs/sources/tutorials/create-alerts-with-logs/index.md b/docs/sources/tutorials/create-alerts-with-logs/index.md index c2293fbba7..87eaf7bb99 100644 --- a/docs/sources/tutorials/create-alerts-with-logs/index.md +++ b/docs/sources/tutorials/create-alerts-with-logs/index.md @@ -31,7 +31,6 @@ In this tutorial, you'll: ## Before you begin -- Ensure you’re on Grafana 8 or later with [Grafana Alerting](https://grafana.com/docs/grafana/latest/alerting/set-up/migrating-alerts/) enabled. - Ensure you’ve [configured a Loki datasource](https://grafana.com/docs/grafana/latest/datasources/loki/#configure-the-data-source) in Grafana. - If you already have logs to work with, you can skip the optional sections and go straight to [create an alert](#create-an-alert). - If you want to use a log-generating sample script to create the logs demonstrated in this tutorial, refer to the optional steps: diff --git a/docs/sources/tutorials/provision-dashboards-and-data-sources/index.md b/docs/sources/tutorials/provision-dashboards-and-data-sources/index.md index 89de23a055..7381df9209 100644 --- a/docs/sources/tutorials/provision-dashboards-and-data-sources/index.md +++ b/docs/sources/tutorials/provision-dashboards-and-data-sources/index.md @@ -44,7 +44,6 @@ Grafana supports configuration as code through _provisioning_. The resources tha - [Dashboards](/docs/grafana/latest/administration/provisioning/#dashboards) - [Data sources](/docs/grafana/latest/administration/provisioning/#datasources) -- [Alert notification channels](/docs/grafana/latest/administration/provisioning/#alert-notification-channels) ## Set the provisioning directory diff --git a/docs/sources/whatsnew/whats-new-in-v10-4.md b/docs/sources/whatsnew/whats-new-in-v10-4.md index 2c4c28d6e5..14343ca8e0 100644 --- a/docs/sources/whatsnew/whats-new-in-v10-4.md +++ b/docs/sources/whatsnew/whats-new-in-v10-4.md @@ -180,7 +180,7 @@ _Generally available in all editions of Grafana_ Users looking to migrate to the new Grafana Alerting product can do so with confidence with the Grafana Alerting migration preview tool. The migration preview tool allows users to view, edit, and delete migrated rules prior cutting over, with the option to roll back to Legacy Alerting. -[Documentation](https://grafana.com/docs/grafana//alerting/set-up/migrating-alerts/#upgrade-with-preview-recommended) +[Documentation](https://grafana.com/docs/grafana/v10.4/alerting/set-up/migrating-alerts/#upgrade-with-preview-recommended) ### Rule evaluation spread over the entire evaluation interval From 298384cea9c041446f3130836c7fdaf8ee82a022 Mon Sep 17 00:00:00 2001 From: linoman <2051016+linoman@users.noreply.github.com> Date: Tue, 12 Mar 2024 01:31:52 -0600 Subject: [PATCH 0537/1406] Disable skip button when strong password feature is enabled (#84211) --- .../app/core/components/ForgottenPassword/ChangePassword.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/public/app/core/components/ForgottenPassword/ChangePassword.tsx b/public/app/core/components/ForgottenPassword/ChangePassword.tsx index 845db05de9..4f5dbec643 100644 --- a/public/app/core/components/ForgottenPassword/ChangePassword.tsx +++ b/public/app/core/components/ForgottenPassword/ChangePassword.tsx @@ -2,6 +2,7 @@ import React, { SyntheticEvent, useState } from 'react'; import { useForm } from 'react-hook-form'; import { selectors } from '@grafana/e2e-selectors'; +import { config } from '@grafana/runtime'; import { Tooltip, Field, VerticalGroup, Button, Alert, useStyles2 } from '@grafana/ui'; import { getStyles } from '../Login/LoginForm'; @@ -85,7 +86,7 @@ export const ChangePassword = ({ onSubmit, onSkip, showDefaultPasswordWarning }: Submit - {onSkip && ( + {!config.auth.basicAuthStrongPasswordPolicy && onSkip && ( Date: Tue, 12 Mar 2024 10:11:15 +0200 Subject: [PATCH 0538/1406] Scenes: Duplicate library panels (#84159) duplicate library panels --- .../scene/DashboardScene.test.tsx | 115 +++++++++++++++++- .../dashboard-scene/scene/DashboardScene.tsx | 69 ++++++++--- .../utils/dashboardSceneGraph.test.ts | 16 ++- .../utils/dashboardSceneGraph.ts | 29 +++++ 4 files changed, 208 insertions(+), 21 deletions(-) diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx index 807c7f7bce..4ffd747204 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx @@ -30,6 +30,7 @@ import { djb2Hash } from '../utils/djb2Hash'; import { DashboardControls } from './DashboardControls'; import { DashboardScene, DashboardSceneState } from './DashboardScene'; import { LibraryVizPanel } from './LibraryVizPanel'; +import { PanelRepeaterGridItem } from './PanelRepeaterGridItem'; jest.mock('../settings/version-history/HistorySrv'); jest.mock('../serialization/transformSaveModelToScene'); @@ -541,7 +542,119 @@ describe('DashboardScene', () => { expect(gridRow.state.children.length).toBe(1); }); - it('Should unlink a library panel', () => { + it('Should duplicate a panel', () => { + const vizPanel = ((scene.state.body as SceneGridLayout).state.children[0] as SceneGridItem).state.body; + scene.duplicatePanel(vizPanel as VizPanel); + + const body = scene.state.body as SceneGridLayout; + const gridItem = body.state.children[5] as SceneGridItem; + + expect(body.state.children.length).toBe(6); + expect(gridItem.state.body!.state.key).toBe('panel-7'); + }); + + it('Should duplicate a library panel', () => { + const libraryPanel = ((scene.state.body as SceneGridLayout).state.children[4] as SceneGridItem).state.body; + const vizPanel = (libraryPanel as LibraryVizPanel).state.panel; + scene.duplicatePanel(vizPanel as VizPanel); + + const body = scene.state.body as SceneGridLayout; + const gridItem = body.state.children[5] as SceneGridItem; + + const libVizPanel = gridItem.state.body as LibraryVizPanel; + + expect(body.state.children.length).toBe(6); + expect(libVizPanel.state.panelKey).toBe('panel-7'); + expect(libVizPanel.state.panel?.state.key).toBe('panel-7'); + }); + + it('Should duplicate a repeated panel', () => { + const scene = buildTestScene({ + body: new SceneGridLayout({ + children: [ + new PanelRepeaterGridItem({ + key: `grid-item-1`, + width: 24, + height: 8, + repeatedPanels: [ + new VizPanel({ + title: 'Library Panel', + key: 'panel-1', + pluginId: 'table', + }), + ], + source: new VizPanel({ + title: 'Library Panel', + key: 'panel-1', + pluginId: 'table', + }), + variableName: 'custom', + }), + ], + }), + }); + + const vizPanel = ((scene.state.body as SceneGridLayout).state.children[0] as PanelRepeaterGridItem).state + .repeatedPanels![0]; + + scene.duplicatePanel(vizPanel as VizPanel); + + const body = scene.state.body as SceneGridLayout; + const gridItem = body.state.children[1] as SceneGridItem; + + expect(body.state.children.length).toBe(2); + expect(gridItem.state.body!.state.key).toBe('panel-2'); + }); + + it('Should duplicate a panel in a row', () => { + const vizPanel = ( + ((scene.state.body as SceneGridLayout).state.children[2] as SceneGridRow).state.children[0] as SceneGridItem + ).state.body; + scene.duplicatePanel(vizPanel as VizPanel); + + const body = scene.state.body as SceneGridLayout; + const gridRow = body.state.children[2] as SceneGridRow; + const gridItem = gridRow.state.children[2] as SceneGridItem; + + expect(gridRow.state.children.length).toBe(3); + expect(gridItem.state.body!.state.key).toBe('panel-7'); + }); + + it('Should duplicate a library panel in a row', () => { + const libraryPanel = ( + ((scene.state.body as SceneGridLayout).state.children[2] as SceneGridRow).state.children[1] as SceneGridItem + ).state.body; + const vizPanel = (libraryPanel as LibraryVizPanel).state.panel; + + scene.duplicatePanel(vizPanel as VizPanel); + + const body = scene.state.body as SceneGridLayout; + const gridRow = body.state.children[2] as SceneGridRow; + const gridItem = gridRow.state.children[2] as SceneGridItem; + + const libVizPanel = gridItem.state.body as LibraryVizPanel; + + expect(gridRow.state.children.length).toBe(3); + expect(libVizPanel.state.panelKey).toBe('panel-7'); + expect(libVizPanel.state.panel?.state.key).toBe('panel-7'); + }); + + it('Should fail to duplicate a panel if it does not have a grid item parent', () => { + const vizPanel = new VizPanel({ + title: 'Panel Title', + key: 'panel-5', + pluginId: 'timeseries', + }); + + scene.duplicatePanel(vizPanel); + + const body = scene.state.body as SceneGridLayout; + + // length remains unchanged + expect(body.state.children.length).toBe(5); + }); + + it('Should unlink a library panel', () => { const libPanel = new LibraryVizPanel({ title: 'title', uid: 'abc', diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index 387071fc1d..095115da30 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -43,7 +43,7 @@ import { DecoratedRevisionModel } from '../settings/VersionsEditView'; import { DashboardEditView } from '../settings/utils'; import { historySrv } from '../settings/version-history'; import { DashboardModelCompatibilityWrapper } from '../utils/DashboardModelCompatibilityWrapper'; -import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; +import { dashboardSceneGraph, getLibraryVizPanelFromVizPanel } from '../utils/dashboardSceneGraph'; import { djb2Hash } from '../utils/djb2Hash'; import { getDashboardUrl } from '../utils/urlBuilders'; import { @@ -459,7 +459,9 @@ export class DashboardScene extends SceneObjectBase { return; } - const gridItem = vizPanel.parent; + const libraryPanel = getLibraryVizPanelFromVizPanel(vizPanel); + + const gridItem = libraryPanel ? libraryPanel.parent : vizPanel.parent; if (!(gridItem instanceof SceneGridItem || gridItem instanceof PanelRepeaterGridItem)) { console.error('Trying to duplicate a panel in a layout that is not SceneGridItem or PanelRepeaterGridItem'); @@ -468,25 +470,44 @@ export class DashboardScene extends SceneObjectBase { let panelState; let panelData; - if (gridItem instanceof PanelRepeaterGridItem) { - const { key, ...gridRepeaterSourceState } = sceneUtils.cloneSceneObjectState(gridItem.state.source.state); - panelState = { ...gridRepeaterSourceState }; - panelData = sceneGraph.getData(gridItem.state.source).clone(); + let newGridItem; + const newPanelId = dashboardSceneGraph.getNextPanelId(this); + + if (libraryPanel) { + const gridItemToDuplicateState = sceneUtils.cloneSceneObjectState(gridItem.state); + + newGridItem = new SceneGridItem({ + x: gridItemToDuplicateState.x, + y: gridItemToDuplicateState.y, + width: gridItemToDuplicateState.width, + height: gridItemToDuplicateState.height, + body: new LibraryVizPanel({ + title: libraryPanel.state.title, + uid: libraryPanel.state.uid, + name: libraryPanel.state.name, + panelKey: getVizPanelKeyForPanelId(newPanelId), + }), + }); } else { - const { key, ...gridItemPanelState } = sceneUtils.cloneSceneObjectState(vizPanel.state); - panelState = { ...gridItemPanelState }; - panelData = sceneGraph.getData(vizPanel).clone(); - } - - // when we duplicate a panel we don't want to clone the alert state - delete panelData.state.data?.alertState; + if (gridItem instanceof PanelRepeaterGridItem) { + panelState = sceneUtils.cloneSceneObjectState(gridItem.state.source.state); + panelData = sceneGraph.getData(gridItem.state.source).clone(); + } else { + panelState = sceneUtils.cloneSceneObjectState(vizPanel.state); + panelData = sceneGraph.getData(vizPanel).clone(); + } - const { key: gridItemKey, ...gridItemToDuplicateState } = sceneUtils.cloneSceneObjectState(gridItem.state); + // when we duplicate a panel we don't want to clone the alert state + delete panelData.state.data?.alertState; - const newGridItem = new SceneGridItem({ - ...gridItemToDuplicateState, - body: new VizPanel({ ...panelState, $data: panelData }), - }); + newGridItem = new SceneGridItem({ + x: gridItem.state.x, + y: gridItem.state.y, + height: NEW_PANEL_HEIGHT, + width: NEW_PANEL_WIDTH, + body: new VizPanel({ ...panelState, $data: panelData, key: getVizPanelKeyForPanelId(newPanelId) }), + }); + } if (!(this.state.body instanceof SceneGridLayout)) { console.error('Trying to duplicate a panel in a layout that is not SceneGridLayout '); @@ -495,6 +516,18 @@ export class DashboardScene extends SceneObjectBase { const sceneGridLayout = this.state.body; + if (gridItem.parent instanceof SceneGridRow) { + const row = gridItem.parent; + + row.setState({ + children: [...row.state.children, newGridItem], + }); + + sceneGridLayout.forceRender(); + + return; + } + sceneGridLayout.setState({ children: [...sceneGridLayout.state.children, newGridItem], }); diff --git a/public/app/features/dashboard-scene/utils/dashboardSceneGraph.test.ts b/public/app/features/dashboard-scene/utils/dashboardSceneGraph.test.ts index ea529d79c1..bc50b5d889 100644 --- a/public/app/features/dashboard-scene/utils/dashboardSceneGraph.test.ts +++ b/public/app/features/dashboard-scene/utils/dashboardSceneGraph.test.ts @@ -16,6 +16,7 @@ import { DashboardControls } from '../scene/DashboardControls'; import { DashboardScene, DashboardSceneState } from '../scene/DashboardScene'; import { LibraryVizPanel } from '../scene/LibraryVizPanel'; import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks'; +import { PanelRepeaterGridItem } from '../scene/PanelRepeaterGridItem'; import { dashboardSceneGraph, getNextPanelId } from './dashboardSceneGraph'; import { findVizPanelByKey } from './utils'; @@ -123,7 +124,7 @@ describe('dashboardSceneGraph', () => { expect(id).toBe(4); }); - it('should take library panels into account', () => { + it('should take library panels, panels in rows and panel repeaters into account', () => { const scene = buildTestScene({ body: new SceneGridLayout({ children: [ @@ -151,6 +152,17 @@ describe('dashboardSceneGraph', () => { pluginId: 'table', }), }), + new PanelRepeaterGridItem({ + source: new VizPanel({ + title: 'Panel C', + key: 'panel-4', + pluginId: 'table', + }), + variableName: 'repeat', + repeatedPanels: [], + repeatDirection: 'h', + maxPerRow: 1, + }), new SceneGridRow({ key: 'key', title: 'row', @@ -178,7 +190,7 @@ describe('dashboardSceneGraph', () => { const id = getNextPanelId(scene); - expect(id).toBe(4); + expect(id).toBe(5); }); it('should get next panel id in a layout with rows', () => { diff --git a/public/app/features/dashboard-scene/utils/dashboardSceneGraph.ts b/public/app/features/dashboard-scene/utils/dashboardSceneGraph.ts index 2a289ff47a..6980a1c525 100644 --- a/public/app/features/dashboard-scene/utils/dashboardSceneGraph.ts +++ b/public/app/features/dashboard-scene/utils/dashboardSceneGraph.ts @@ -11,6 +11,7 @@ import { import { DashboardScene } from '../scene/DashboardScene'; import { LibraryVizPanel } from '../scene/LibraryVizPanel'; import { VizPanelLinks } from '../scene/PanelLinks'; +import { PanelRepeaterGridItem } from '../scene/PanelRepeaterGridItem'; import { getPanelIdForLibraryVizPanel, getPanelIdForVizPanel } from './utils'; @@ -85,6 +86,21 @@ export function getNextPanelId(dashboard: DashboardScene): number { } for (const child of body.state.children) { + if (child instanceof PanelRepeaterGridItem) { + const vizPanel = child.state.source; + + if (vizPanel) { + const panelId = + vizPanel instanceof LibraryVizPanel + ? getPanelIdForLibraryVizPanel(vizPanel) + : getPanelIdForVizPanel(vizPanel); + + if (panelId > max) { + max = panelId; + } + } + } + if (child instanceof SceneGridItem) { const vizPanel = child.state.body; @@ -130,6 +146,19 @@ export function getNextPanelId(dashboard: DashboardScene): number { return max + 1; } +// Returns the LibraryVizPanel that corresponds to the given VizPanel if it exists +export const getLibraryVizPanelFromVizPanel = (vizPanel: VizPanel): LibraryVizPanel | null => { + if (vizPanel.parent instanceof LibraryVizPanel) { + return vizPanel.parent; + } + + if (vizPanel.parent instanceof PanelRepeaterGridItem && vizPanel.parent.state.source instanceof LibraryVizPanel) { + return vizPanel.parent.state.source; + } + + return null; +}; + export const dashboardSceneGraph = { getTimePicker, getRefreshPicker, From 6ea9f0c447c82a60072db86a80de35779618f8f1 Mon Sep 17 00:00:00 2001 From: Karl Persson Date: Tue, 12 Mar 2024 09:15:14 +0100 Subject: [PATCH 0539/1406] AuthN: Use fetch user sync hook for render keys connected to a user (#84080) * Use fetch user sync hook for render keys connected to a user --- pkg/services/authn/authnimpl/service.go | 2 +- pkg/services/authn/clients/render.go | 37 ++++++++++------------- pkg/services/authn/clients/render_test.go | 19 ++---------- 3 files changed, 20 insertions(+), 38 deletions(-) diff --git a/pkg/services/authn/authnimpl/service.go b/pkg/services/authn/authnimpl/service.go index c819131ad3..70d81fc53a 100644 --- a/pkg/services/authn/authnimpl/service.go +++ b/pkg/services/authn/authnimpl/service.go @@ -89,7 +89,7 @@ func ProvideService( usageStats.RegisterMetricsFunc(s.getUsageStats) - s.RegisterClient(clients.ProvideRender(userService, renderService)) + s.RegisterClient(clients.ProvideRender(renderService)) s.RegisterClient(clients.ProvideAPIKey(apikeyService)) if cfg.LoginCookieName != "" { diff --git a/pkg/services/authn/clients/render.go b/pkg/services/authn/clients/render.go index 5386240822..e5a1b6970a 100644 --- a/pkg/services/authn/clients/render.go +++ b/pkg/services/authn/clients/render.go @@ -8,7 +8,6 @@ import ( "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/rendering" - "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/util/errutil" ) @@ -22,12 +21,11 @@ const ( var _ authn.ContextAwareClient = new(Render) -func ProvideRender(userService user.Service, renderService rendering.Service) *Render { - return &Render{userService, renderService} +func ProvideRender(renderService rendering.Service) *Render { + return &Render{renderService} } type Render struct { - userService user.Service renderService rendering.Service } @@ -42,26 +40,23 @@ func (c *Render) Authenticate(ctx context.Context, r *authn.Request) (*authn.Ide return nil, errInvalidRenderKey.Errorf("found no render user for key: %s", key) } - var identity *authn.Identity if renderUsr.UserID <= 0 { - identity = &authn.Identity{ - ID: authn.NamespacedID(authn.NamespaceRenderService, 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}) - if err != nil { - return nil, err - } - - identity = authn.IdentityFromSignedInUser(authn.NamespacedID(authn.NamespaceUser, usr.UserID), usr, authn.ClientParams{SyncPermissions: true}, login.RenderModule) + return &authn.Identity{ + ID: authn.NamespacedID(authn.NamespaceRenderService, 0), + OrgID: renderUsr.OrgID, + OrgRoles: map[int64]org.RoleType{renderUsr.OrgID: org.RoleType(renderUsr.OrgRole)}, + ClientParams: authn.ClientParams{SyncPermissions: true}, + LastSeenAt: time.Now(), + AuthenticatedBy: login.RenderModule, + }, nil } - identity.LastSeenAt = time.Now() - identity.AuthenticatedBy = login.RenderModule - return identity, nil + return &authn.Identity{ + ID: authn.NamespacedID(authn.NamespaceUser, renderUsr.UserID), + LastSeenAt: time.Now(), + AuthenticatedBy: login.RenderModule, + ClientParams: authn.ClientParams{FetchSyncedUser: true, SyncPermissions: true}, + }, nil } func (c *Render) Test(ctx context.Context, r *authn.Request) bool { diff --git a/pkg/services/authn/clients/render_test.go b/pkg/services/authn/clients/render_test.go index 19e18dbb33..24051db326 100644 --- a/pkg/services/authn/clients/render_test.go +++ b/pkg/services/authn/clients/render_test.go @@ -13,8 +13,6 @@ import ( "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/rendering" - "github.com/grafana/grafana/pkg/services/user" - "github.com/grafana/grafana/pkg/services/user/usertest" ) func TestRender_Authenticate(t *testing.T) { @@ -23,7 +21,6 @@ func TestRender_Authenticate(t *testing.T) { renderKey string req *authn.Request expectedErr error - expectedUsr *user.SignedInUser expectedIdentity *authn.Identity expectedRenderUsr *rendering.RenderUser } @@ -60,23 +57,13 @@ func TestRender_Authenticate(t *testing.T) { }, expectedIdentity: &authn.Identity{ ID: "user:1", - OrgID: 1, - OrgName: "test", - OrgRoles: map[int64]org.RoleType{1: org.RoleAdmin}, - IsGrafanaAdmin: boolPtr(false), AuthenticatedBy: login.RenderModule, - ClientParams: authn.ClientParams{SyncPermissions: true}, + ClientParams: authn.ClientParams{FetchSyncedUser: true, SyncPermissions: true}, }, expectedRenderUsr: &rendering.RenderUser{ OrgID: 1, UserID: 1, }, - expectedUsr: &user.SignedInUser{ - UserID: 1, - OrgID: 1, - OrgName: "test", - OrgRole: "Admin", - }, }, { desc: "expect error when render key is invalid", @@ -97,7 +84,7 @@ func TestRender_Authenticate(t *testing.T) { renderService := rendering.NewMockService(ctrl) renderService.EXPECT().GetRenderUser(gomock.Any(), tt.renderKey).Return(tt.expectedRenderUsr, tt.expectedRenderUsr != nil) - c := ProvideRender(&usertest.FakeUserService{ExpectedSignedInUser: tt.expectedUsr}, renderService) + c := ProvideRender(renderService) identity, err := c.Authenticate(context.Background(), tt.req) if tt.expectedErr != nil { assert.ErrorIs(t, tt.expectedErr, err) @@ -141,7 +128,7 @@ func TestRender_Test(t *testing.T) { for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { - c := ProvideRender(&usertest.FakeUserService{}, &rendering.MockService{}) + c := ProvideRender(&rendering.MockService{}) assert.Equal(t, tt.expected, c.Test(context.Background(), tt.req)) }) } From 8c06c0dea7283bc132779fc3ccfde58109110be1 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Tue, 12 Mar 2024 09:17:41 +0100 Subject: [PATCH 0540/1406] Panel edit: Add e2e selectors to input fields (#83246) add selectors for input fields --- packages/grafana-e2e-selectors/src/selectors/components.ts | 1 + .../dashboard/components/PanelEditor/getPanelFrameOptions.tsx | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/packages/grafana-e2e-selectors/src/selectors/components.ts b/packages/grafana-e2e-selectors/src/selectors/components.ts index 8dda593069..d9538c6bdd 100644 --- a/packages/grafana-e2e-selectors/src/selectors/components.ts +++ b/packages/grafana-e2e-selectors/src/selectors/components.ts @@ -221,6 +221,7 @@ export const Components = { content: 'Panel editor option pane content', select: 'Panel editor option pane select', fieldLabel: (type: string) => `${type} field property editor`, + fieldInput: (title: string) => `data-testid Panel editor option pane field input ${title}`, }, // not sure about the naming *DataPane* DataPane: { diff --git a/public/app/features/dashboard/components/PanelEditor/getPanelFrameOptions.tsx b/public/app/features/dashboard/components/PanelEditor/getPanelFrameOptions.tsx index 8857409406..3f9793daae 100644 --- a/public/app/features/dashboard/components/PanelEditor/getPanelFrameOptions.tsx +++ b/public/app/features/dashboard/components/PanelEditor/getPanelFrameOptions.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { SelectableValue } from '@grafana/data'; +import { selectors } from '@grafana/e2e-selectors'; import { config } from '@grafana/runtime'; import { VizPanel } from '@grafana/scenes'; import { DataLinksInlineEditor, Input, RadioButtonGroup, Select, Switch, TextArea } from '@grafana/ui'; @@ -50,6 +51,7 @@ export function getPanelFrameCategory(props: OptionPaneRenderProps): OptionsPane render: function renderTitle() { return ( onPanelConfigChange('title', e.currentTarget.value)} @@ -67,6 +69,7 @@ export function getPanelFrameCategory(props: OptionPaneRenderProps): OptionsPane render: function renderDescription() { return ( - - If you want to apply templating to the alert rule name, use the following syntax - ${Label} - -
-
- Tags -
-
- - - -
-
-
- - -
-
- -
-
-
diff --git a/public/app/features/alerting/routes.tsx b/public/app/features/alerting/routes.tsx index 2003a49464..ddc9d11277 100644 --- a/public/app/features/alerting/routes.tsx +++ b/public/app/features/alerting/routes.tsx @@ -1,352 +1,223 @@ -import { uniq } from 'lodash'; -import React from 'react'; -import { Redirect, RouteComponentProps } from 'react-router-dom'; - import { SafeDynamicImport } from 'app/core/components/DynamicImports/SafeDynamicImport'; -import { NavLandingPage } from 'app/core/components/NavLandingPage/NavLandingPage'; import { config } from 'app/core/config'; -import { RouteDescriptor } from 'app/core/navigation/types'; +import { GrafanaRouteComponent, RouteDescriptor } from 'app/core/navigation/types'; import { AccessControlAction } from 'app/types'; import { evaluateAccess } from './unified/utils/access-control'; -const commonRoutes: RouteDescriptor[] = []; - -const legacyRoutes: RouteDescriptor[] = [ - ...commonRoutes, - { - path: '/alerting-legacy', - component: () => , - }, - { - path: '/alerting-legacy/list', - component: SafeDynamicImport( - () => import(/* webpackChunkName: "AlertRuleListLegacyIndex" */ 'app/features/alerting/AlertRuleList') - ), - }, - { - path: '/alerting-legacy/ng/list', - component: SafeDynamicImport( - () => import(/* webpackChunkName: "AlertRuleListLegacy" */ 'app/features/alerting/AlertRuleList') - ), - }, - { - path: '/alerting-legacy/notifications', - roles: config.unifiedAlertingEnabled ? () => ['Editor', 'Admin'] : undefined, - component: SafeDynamicImport( - () => import(/* webpackChunkName: "NotificationsListLegacyPage" */ 'app/features/alerting/NotificationsListPage') - ), - }, - { - path: '/alerting-legacy/notifications/templates/new', - roles: () => ['Editor', 'Admin'], - component: SafeDynamicImport( - () => import(/* webpackChunkName: "NotificationsListLegacyPage" */ 'app/features/alerting/NotificationsListPage') - ), - }, - { - path: '/alerting-legacy/notifications/templates/:id/edit', - roles: () => ['Editor', 'Admin'], - component: SafeDynamicImport( - () => import(/* webpackChunkName: "NotificationsListLegacyPage" */ 'app/features/alerting/NotificationsListPage') - ), - }, - { - path: '/alerting-legacy/notifications/receivers/new', - roles: () => ['Editor', 'Admin'], - component: SafeDynamicImport( - () => import(/* webpackChunkName: "NotificationsListLegacyPage" */ 'app/features/alerting/NotificationsListPage') - ), - }, - { - path: '/alerting-legacy/notifications/receivers/:id/edit', - roles: () => ['Editor', 'Admin'], - component: SafeDynamicImport( - () => import(/* webpackChunkName: "NotificationsListLegacyPage" */ 'app/features/alerting/NotificationsListPage') - ), - }, - { - path: '/alerting-legacy/notifications/global-config', - roles: () => ['Admin', 'Editor'], - component: SafeDynamicImport( - () => import(/* webpackChunkName: "NotificationsListLegacyPage" */ 'app/features/alerting/NotificationsListPage') - ), - }, - { - path: '/alerting-legacy/notification/new', - component: SafeDynamicImport( - () => - import( - /* webpackChunkName: "NewNotificationChannelLegacy" */ 'app/features/alerting/NewNotificationChannelPage' - ) - ), - }, - { - path: '/alerting-legacy/notification/:id/edit', - component: SafeDynamicImport( - () => - import( - /* webpackChunkName: "EditNotificationChannelLegacy"*/ 'app/features/alerting/EditNotificationChannelPage' - ) - ), - }, - { - path: '/alerting-legacy/upgrade', - roles: () => ['Admin'], - component: SafeDynamicImport( - () => import(/* webpackChunkName: "AlertingUpgrade" */ 'app/features/alerting/Upgrade') - ), - }, -]; - -const unifiedRoutes: RouteDescriptor[] = [ - ...commonRoutes, - { - path: '/alerting', - component: SafeDynamicImport( - () => import(/* webpackChunkName: "AlertingHome" */ 'app/features/alerting/unified/home/Home') - ), - }, - { - path: '/alerting/home', - exact: false, - component: SafeDynamicImport( - () => import(/* webpackChunkName: "AlertingHome" */ 'app/features/alerting/unified/home/Home') - ), - }, - { - path: '/alerting/list', - roles: evaluateAccess([AccessControlAction.AlertingRuleRead, AccessControlAction.AlertingRuleExternalRead]), - component: SafeDynamicImport( - () => import(/* webpackChunkName: "AlertRuleListIndex" */ 'app/features/alerting/unified/RuleList') - ), - }, - { - path: '/alerting/routes', - roles: evaluateAccess([ - AccessControlAction.AlertingNotificationsRead, - AccessControlAction.AlertingNotificationsExternalRead, - ]), - component: SafeDynamicImport( - () => import(/* webpackChunkName: "AlertAmRoutes" */ 'app/features/alerting/unified/NotificationPolicies') - ), - }, - { - path: '/alerting/routes/mute-timing/new', - roles: evaluateAccess([ - AccessControlAction.AlertingNotificationsWrite, - AccessControlAction.AlertingNotificationsExternalWrite, - ]), - component: SafeDynamicImport( - () => import(/* webpackChunkName: "MuteTimings" */ 'app/features/alerting/unified/MuteTimings') - ), - }, - { - path: '/alerting/routes/mute-timing/edit', - roles: evaluateAccess([ - AccessControlAction.AlertingNotificationsWrite, - AccessControlAction.AlertingNotificationsExternalWrite, - ]), - component: SafeDynamicImport( - () => import(/* webpackChunkName: "MuteTimings" */ 'app/features/alerting/unified/MuteTimings') - ), - }, - { - path: '/alerting/silences', - roles: evaluateAccess([ - AccessControlAction.AlertingInstanceRead, - AccessControlAction.AlertingInstancesExternalRead, - ]), - component: SafeDynamicImport( - () => import(/* webpackChunkName: "AlertSilences" */ 'app/features/alerting/unified/Silences') - ), - }, - { - path: '/alerting/silence/new', - roles: evaluateAccess([ - AccessControlAction.AlertingInstanceCreate, - AccessControlAction.AlertingInstancesExternalWrite, - ]), - component: SafeDynamicImport( - () => import(/* webpackChunkName: "AlertSilences" */ 'app/features/alerting/unified/Silences') - ), - }, - { - path: '/alerting/silence/:id/edit', - roles: evaluateAccess([ - AccessControlAction.AlertingInstanceUpdate, - AccessControlAction.AlertingInstancesExternalWrite, - ]), - component: SafeDynamicImport( - () => import(/* webpackChunkName: "AlertSilences" */ 'app/features/alerting/unified/Silences') - ), - }, - { - path: '/alerting/notifications', - roles: evaluateAccess([ - AccessControlAction.AlertingNotificationsRead, - AccessControlAction.AlertingNotificationsExternalRead, - ]), - component: SafeDynamicImport( - () => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/unified/Receivers') - ), - }, - { - path: '/alerting/notifications/:type/new', - roles: evaluateAccess([ - AccessControlAction.AlertingNotificationsWrite, - AccessControlAction.AlertingNotificationsExternalWrite, - ]), - component: SafeDynamicImport( - () => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/unified/Receivers') - ), - }, - { - path: '/alerting/notifications/receivers/:id/edit', - roles: evaluateAccess([ - AccessControlAction.AlertingNotificationsWrite, - AccessControlAction.AlertingNotificationsExternalWrite, - AccessControlAction.AlertingNotificationsRead, - AccessControlAction.AlertingNotificationsExternalRead, - ]), - component: SafeDynamicImport( - () => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/unified/Receivers') - ), - }, - { - path: '/alerting/notifications/:type/:id/edit', - roles: evaluateAccess([ - AccessControlAction.AlertingNotificationsWrite, - AccessControlAction.AlertingNotificationsExternalWrite, - ]), - component: SafeDynamicImport( - () => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/unified/Receivers') - ), - }, - { - path: '/alerting/notifications/:type/:id/duplicate', - roles: evaluateAccess([ - AccessControlAction.AlertingNotificationsWrite, - AccessControlAction.AlertingNotificationsExternalWrite, - ]), - component: SafeDynamicImport( - () => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/unified/Receivers') - ), - }, - { - path: '/alerting/notifications/:type', - roles: evaluateAccess([ - AccessControlAction.AlertingNotificationsWrite, - AccessControlAction.AlertingNotificationsExternalWrite, - ]), - component: SafeDynamicImport( - () => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/unified/Receivers') - ), - }, - { - path: '/alerting/groups/', - roles: evaluateAccess([ - AccessControlAction.AlertingInstanceRead, - AccessControlAction.AlertingInstancesExternalRead, - ]), - component: SafeDynamicImport( - () => import(/* webpackChunkName: "AlertGroups" */ 'app/features/alerting/unified/AlertGroups') - ), - }, - { - path: '/alerting/new/:type?', - pageClass: 'page-alerting', - roles: evaluateAccess([AccessControlAction.AlertingRuleCreate, AccessControlAction.AlertingRuleExternalWrite]), - component: SafeDynamicImport( - () => import(/* webpackChunkName: "AlertingRuleForm"*/ 'app/features/alerting/unified/RuleEditor') - ), - }, - { - path: '/alerting/:id/edit', - pageClass: 'page-alerting', - roles: evaluateAccess([AccessControlAction.AlertingRuleUpdate, AccessControlAction.AlertingRuleExternalWrite]), - component: SafeDynamicImport( - () => import(/* webpackChunkName: "AlertingRuleForm"*/ 'app/features/alerting/unified/RuleEditor') - ), - }, - { - path: '/alerting/:id/modify-export', - pageClass: 'page-alerting', - roles: evaluateAccess([AccessControlAction.AlertingRuleRead]), - component: SafeDynamicImport( - () => - import( - /* webpackChunkName: "AlertingRuleForm"*/ 'app/features/alerting/unified/components/export/GrafanaModifyExport' - ) - ), - }, - { - path: '/alerting/:sourceName/:id/view', - pageClass: 'page-alerting', - roles: evaluateAccess([AccessControlAction.AlertingRuleRead, AccessControlAction.AlertingRuleExternalRead]), - component: SafeDynamicImport( - () => import(/* webpackChunkName: "AlertingRule"*/ 'app/features/alerting/unified/RuleViewer') - ), - }, - { - path: '/alerting/:sourceName/:name/find', - pageClass: 'page-alerting', - roles: evaluateAccess([AccessControlAction.AlertingRuleRead, AccessControlAction.AlertingRuleExternalRead]), - component: SafeDynamicImport( - () => import(/* webpackChunkName: "AlertingRedirectToRule"*/ 'app/features/alerting/unified/RedirectToRuleViewer') - ), - }, - { - path: '/alerting/admin', - roles: () => ['Admin'], - component: SafeDynamicImport( - () => import(/* webpackChunkName: "AlertingAdmin" */ 'app/features/alerting/unified/Admin') - ), - }, -]; - export function getAlertingRoutes(cfg = config): RouteDescriptor[] { - if (cfg.unifiedAlertingEnabled) { - return unifiedRoutes; - } else if (cfg.alertingEnabled) { - if (config.featureToggles.alertingPreviewUpgrade) { - // If preview is enabled, return both legacy and unified routes. - return [...legacyRoutes, ...unifiedRoutes]; - } - // Redirect old overlapping legacy routes to new separate ones to minimize unintended 404s. - const redirects = [ - { - path: '/alerting', - component: () => , - }, - { - path: '/alerting/list', - component: () => , - }, - { - path: '/alerting/notifications', - component: () => , - }, - { - path: '/alerting/notification/new', - component: () => , - }, - { - path: '/alerting/notification/:id/edit', - component: (props: RouteComponentProps<{ id: string }>) => ( - - ), - }, - ]; - return [...legacyRoutes, ...redirects]; - } + const routes = [ + { + path: '/alerting', + component: importAlertingComponent( + () => import(/* webpackChunkName: "AlertingHome" */ 'app/features/alerting/unified/home/Home') + ), + }, + { + path: '/alerting/home', + exact: false, + component: importAlertingComponent( + () => import(/* webpackChunkName: "AlertingHome" */ 'app/features/alerting/unified/home/Home') + ), + }, + { + path: '/alerting/list', + roles: evaluateAccess([AccessControlAction.AlertingRuleRead, AccessControlAction.AlertingRuleExternalRead]), + component: importAlertingComponent( + () => import(/* webpackChunkName: "AlertRuleListIndex" */ 'app/features/alerting/unified/RuleList') + ), + }, + { + path: '/alerting/routes', + roles: evaluateAccess([ + AccessControlAction.AlertingNotificationsRead, + AccessControlAction.AlertingNotificationsExternalRead, + ]), + component: importAlertingComponent( + () => import(/* webpackChunkName: "AlertAmRoutes" */ 'app/features/alerting/unified/NotificationPolicies') + ), + }, + { + path: '/alerting/routes/mute-timing/new', + roles: evaluateAccess([ + AccessControlAction.AlertingNotificationsWrite, + AccessControlAction.AlertingNotificationsExternalWrite, + ]), + component: importAlertingComponent( + () => import(/* webpackChunkName: "MuteTimings" */ 'app/features/alerting/unified/MuteTimings') + ), + }, + { + path: '/alerting/routes/mute-timing/edit', + roles: evaluateAccess([ + AccessControlAction.AlertingNotificationsWrite, + AccessControlAction.AlertingNotificationsExternalWrite, + ]), + component: importAlertingComponent( + () => import(/* webpackChunkName: "MuteTimings" */ 'app/features/alerting/unified/MuteTimings') + ), + }, + { + path: '/alerting/silences', + roles: evaluateAccess([ + AccessControlAction.AlertingInstanceRead, + AccessControlAction.AlertingInstancesExternalRead, + ]), + component: importAlertingComponent( + () => import(/* webpackChunkName: "AlertSilences" */ 'app/features/alerting/unified/Silences') + ), + }, + { + path: '/alerting/silence/new', + roles: evaluateAccess([ + AccessControlAction.AlertingInstanceCreate, + AccessControlAction.AlertingInstancesExternalWrite, + ]), + component: importAlertingComponent( + () => import(/* webpackChunkName: "AlertSilences" */ 'app/features/alerting/unified/Silences') + ), + }, + { + path: '/alerting/silence/:id/edit', + roles: evaluateAccess([ + AccessControlAction.AlertingInstanceUpdate, + AccessControlAction.AlertingInstancesExternalWrite, + ]), + component: importAlertingComponent( + () => import(/* webpackChunkName: "AlertSilences" */ 'app/features/alerting/unified/Silences') + ), + }, + { + path: '/alerting/notifications', + roles: evaluateAccess([ + AccessControlAction.AlertingNotificationsRead, + AccessControlAction.AlertingNotificationsExternalRead, + ]), + component: importAlertingComponent( + () => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/unified/Receivers') + ), + }, + { + path: '/alerting/notifications/:type/new', + roles: evaluateAccess([ + AccessControlAction.AlertingNotificationsWrite, + AccessControlAction.AlertingNotificationsExternalWrite, + ]), + component: importAlertingComponent( + () => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/unified/Receivers') + ), + }, + { + path: '/alerting/notifications/receivers/:id/edit', + roles: evaluateAccess([ + AccessControlAction.AlertingNotificationsWrite, + AccessControlAction.AlertingNotificationsExternalWrite, + AccessControlAction.AlertingNotificationsRead, + AccessControlAction.AlertingNotificationsExternalRead, + ]), + component: importAlertingComponent( + () => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/unified/Receivers') + ), + }, + { + path: '/alerting/notifications/:type/:id/edit', + roles: evaluateAccess([ + AccessControlAction.AlertingNotificationsWrite, + AccessControlAction.AlertingNotificationsExternalWrite, + ]), + component: importAlertingComponent( + () => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/unified/Receivers') + ), + }, + { + path: '/alerting/notifications/:type/:id/duplicate', + roles: evaluateAccess([ + AccessControlAction.AlertingNotificationsWrite, + AccessControlAction.AlertingNotificationsExternalWrite, + ]), + component: importAlertingComponent( + () => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/unified/Receivers') + ), + }, + { + path: '/alerting/notifications/:type', + roles: evaluateAccess([ + AccessControlAction.AlertingNotificationsWrite, + AccessControlAction.AlertingNotificationsExternalWrite, + ]), + component: importAlertingComponent( + () => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/unified/Receivers') + ), + }, + { + path: '/alerting/groups/', + roles: evaluateAccess([ + AccessControlAction.AlertingInstanceRead, + AccessControlAction.AlertingInstancesExternalRead, + ]), + component: importAlertingComponent( + () => import(/* webpackChunkName: "AlertGroups" */ 'app/features/alerting/unified/AlertGroups') + ), + }, + { + path: '/alerting/new/:type?', + pageClass: 'page-alerting', + roles: evaluateAccess([AccessControlAction.AlertingRuleCreate, AccessControlAction.AlertingRuleExternalWrite]), + component: importAlertingComponent( + () => import(/* webpackChunkName: "AlertingRuleForm"*/ 'app/features/alerting/unified/RuleEditor') + ), + }, + { + path: '/alerting/:id/edit', + pageClass: 'page-alerting', + roles: evaluateAccess([AccessControlAction.AlertingRuleUpdate, AccessControlAction.AlertingRuleExternalWrite]), + component: importAlertingComponent( + () => import(/* webpackChunkName: "AlertingRuleForm"*/ 'app/features/alerting/unified/RuleEditor') + ), + }, + { + path: '/alerting/:id/modify-export', + pageClass: 'page-alerting', + roles: evaluateAccess([AccessControlAction.AlertingRuleRead]), + component: importAlertingComponent( + () => + import( + /* webpackChunkName: "AlertingRuleForm"*/ 'app/features/alerting/unified/components/export/GrafanaModifyExport' + ) + ), + }, + { + path: '/alerting/:sourceName/:id/view', + pageClass: 'page-alerting', + roles: evaluateAccess([AccessControlAction.AlertingRuleRead, AccessControlAction.AlertingRuleExternalRead]), + component: importAlertingComponent( + () => import(/* webpackChunkName: "AlertingRule"*/ 'app/features/alerting/unified/RuleViewer') + ), + }, + { + path: '/alerting/:sourceName/:name/find', + pageClass: 'page-alerting', + roles: evaluateAccess([AccessControlAction.AlertingRuleRead, AccessControlAction.AlertingRuleExternalRead]), + component: importAlertingComponent( + () => + import(/* webpackChunkName: "AlertingRedirectToRule"*/ 'app/features/alerting/unified/RedirectToRuleViewer') + ), + }, + { + path: '/alerting/admin', + roles: () => ['Admin'], + component: importAlertingComponent( + () => import(/* webpackChunkName: "AlertingAdmin" */ 'app/features/alerting/unified/Admin') + ), + }, + ]; + + return routes; +} - // Disable all alerting routes. - const uniquePaths = uniq([...legacyRoutes, ...unifiedRoutes].map((route) => route.path)); - return uniquePaths.map((path) => ({ - path, - component: SafeDynamicImport( - () => import(/* webpackChunkName: "AlertingFeatureTogglePage"*/ 'app/features/alerting/FeatureTogglePage') - ), - })); +// this function will always load the "feature disabled" component for all alerting routes +function importAlertingComponent(loader: () => any): GrafanaRouteComponent { + const featureDisabledPageLoader = () => + import(/* webpackChunkName: "AlertingDisabled" */ 'app/features/alerting/unified/AlertingNotEnabled'); + return SafeDynamicImport(config.unifiedAlertingEnabled ? loader : featureDisabledPageLoader); } diff --git a/public/app/features/alerting/state/actions.ts b/public/app/features/alerting/state/actions.ts deleted file mode 100644 index fa4f5de421..0000000000 --- a/public/app/features/alerting/state/actions.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { getBackendSrv, isFetchError, locationService } from '@grafana/runtime'; -import { notifyApp } from 'app/core/actions'; -import { createErrorNotification, createSuccessNotification } from 'app/core/copy/appNotification'; -import { AlertRuleDTO, NotifierDTO, ThunkResult } from 'app/types'; - -import { loadAlertRules, loadedAlertRules, notificationChannelLoaded, setNotificationChannels } from './reducers'; - -export function getAlertRulesAsync(options: { state: string }): ThunkResult { - return async (dispatch) => { - dispatch(loadAlertRules()); - const rules: AlertRuleDTO[] = await getBackendSrv().get('/api/alerts', options); - dispatch(loadedAlertRules(rules)); - }; -} - -export function togglePauseAlertRule(id: number, options: { paused: boolean }): ThunkResult { - return async (dispatch) => { - await getBackendSrv().post(`/api/alerts/${id}/pause`, options); - const stateFilter = locationService.getSearchObject().state || 'all'; - dispatch(getAlertRulesAsync({ state: stateFilter.toString() })); - }; -} - -export function createNotificationChannel(data: any): ThunkResult> { - return async (dispatch) => { - try { - await getBackendSrv().post(`/api/alert-notifications`, data); - dispatch(notifyApp(createSuccessNotification('Notification created'))); - locationService.push('/alerting-legacy/notifications'); - } catch (error) { - if (isFetchError(error)) { - dispatch(notifyApp(createErrorNotification(error.data.error))); - } - } - }; -} - -export function updateNotificationChannel(data: any): ThunkResult { - return async (dispatch) => { - try { - await getBackendSrv().put(`/api/alert-notifications/${data.id}`, data); - dispatch(notifyApp(createSuccessNotification('Notification updated'))); - } catch (error) { - if (isFetchError(error)) { - dispatch(notifyApp(createErrorNotification(error.data.error))); - } - } - }; -} - -export function testNotificationChannel(data: any): ThunkResult { - return async (dispatch, getState) => { - const channel = getState().notificationChannel.notificationChannel; - await getBackendSrv().post('/api/alert-notifications/test', { id: channel.id, ...data }); - }; -} - -export function loadNotificationTypes(): ThunkResult { - return async (dispatch) => { - const alertNotifiers: NotifierDTO[] = await getBackendSrv().get(`/api/alert-notifiers-legacy`); - - const notificationTypes = alertNotifiers.sort((o1, o2) => { - if (o1.name > o2.name) { - return 1; - } - return -1; - }); - - dispatch(setNotificationChannels(notificationTypes)); - }; -} - -export function loadNotificationChannel(id: number): ThunkResult { - return async (dispatch) => { - await dispatch(loadNotificationTypes()); - const notificationChannel = await getBackendSrv().get(`/api/alert-notifications/${id}`); - dispatch(notificationChannelLoaded(notificationChannel)); - }; -} diff --git a/public/app/features/alerting/unified/AlertingNotEnabled.tsx b/public/app/features/alerting/unified/AlertingNotEnabled.tsx new file mode 100644 index 0000000000..7d2b89ae9e --- /dev/null +++ b/public/app/features/alerting/unified/AlertingNotEnabled.tsx @@ -0,0 +1,29 @@ +import React from 'react'; + +import { NavModel } from '@grafana/data'; +import { Page } from 'app/core/components/Page/Page'; + +export default function FeatureTogglePage() { + const navModel: NavModel = { + node: { + text: 'Alerting is not enabled', + hideFromBreadcrumbs: true, + subTitle: 'To enable alerting, enable it in the Grafana config', + }, + main: { + text: 'Alerting is not enabled', + }, + }; + + return ( + + +
+          {`[unified_alerting]
+enabled = true
+`}
+        
+
+
+ ); +} diff --git a/public/app/features/alerting/unified/components/AlertingPageWrapper.tsx b/public/app/features/alerting/unified/components/AlertingPageWrapper.tsx index e476f5e7db..44fe60d61e 100644 --- a/public/app/features/alerting/unified/components/AlertingPageWrapper.tsx +++ b/public/app/features/alerting/unified/components/AlertingPageWrapper.tsx @@ -4,7 +4,6 @@ import { useLocation } from 'react-use'; import { Page } from 'app/core/components/Page/Page'; import { PageProps } from 'app/core/components/Page/types'; -import { UAPreviewNotice } from '../../components/UAPreviewNotice'; import { AlertmanagerProvider, useAlertmanager } from '../state/AlertmanagerContext'; import { AlertManagerPicker } from './AlertManagerPicker'; @@ -20,10 +19,7 @@ interface AlertingPageWrapperProps extends PageProps { export const AlertingPageWrapper = ({ children, isLoading, ...rest }: AlertingPageWrapperProps) => ( -
- - {children} -
+
{children}
); diff --git a/public/app/features/alerting/components/ConditionalWrap.tsx b/public/app/features/alerting/unified/components/ConditionalWrap.tsx similarity index 100% rename from public/app/features/alerting/components/ConditionalWrap.tsx rename to public/app/features/alerting/unified/components/ConditionalWrap.tsx diff --git a/public/app/features/alerting/unified/components/contact-points/ContactPoints.tsx b/public/app/features/alerting/unified/components/contact-points/ContactPoints.tsx index e9ff52e3f0..646fa4e8ef 100644 --- a/public/app/features/alerting/unified/components/contact-points/ContactPoints.tsx +++ b/public/app/features/alerting/unified/components/contact-points/ContactPoints.tsx @@ -25,7 +25,7 @@ import { Tooltip, useStyles2, } from '@grafana/ui'; -import ConditionalWrap from 'app/features/alerting/components/ConditionalWrap'; +import ConditionalWrap from 'app/features/alerting/unified/components/ConditionalWrap'; import { receiverTypeNames } from 'app/plugins/datasource/alertmanager/consts'; import { GrafanaManagedReceiverConfig } from 'app/plugins/datasource/alertmanager/types'; import { GrafanaNotifierType, NotifierStatus } from 'app/types/alerting'; diff --git a/public/app/features/alerting/unified/components/notification-policies/Policy.tsx b/public/app/features/alerting/unified/components/notification-policies/Policy.tsx index 1bbdb12d49..129734656b 100644 --- a/public/app/features/alerting/unified/components/notification-policies/Policy.tsx +++ b/public/app/features/alerting/unified/components/notification-policies/Policy.tsx @@ -20,7 +20,7 @@ import { getTagColorsFromName, useStyles2, } from '@grafana/ui'; -import ConditionalWrap from 'app/features/alerting/components/ConditionalWrap'; +import ConditionalWrap from 'app/features/alerting/unified/components/ConditionalWrap'; import { AlertmanagerGroup, MatcherOperator, diff --git a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/reducer.ts b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/reducer.ts index 08aecf832f..7072e664d1 100644 --- a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/reducer.ts +++ b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/reducer.ts @@ -2,7 +2,7 @@ import { createAction, createReducer } from '@reduxjs/toolkit'; import { DataQuery, getDefaultRelativeTimeRange, rangeUtil, RelativeTimeRange } from '@grafana/data'; import { getNextRefIdChar } from 'app/core/utils/query'; -import { findDataSourceFromExpressionRecursive } from 'app/features/alerting/utils/dataSourceFromExpression'; +import { findDataSourceFromExpressionRecursive } from 'app/features/alerting/unified/utils/dataSourceFromExpression'; import { dataSource as expressionDatasource } from 'app/features/expressions/ExpressionDatasource'; import { isExpressionQuery } from 'app/features/expressions/guards'; import { ExpressionDatasourceUID, ExpressionQuery, ExpressionQueryType } from 'app/features/expressions/types'; diff --git a/public/app/features/alerting/unified/initAlerting.tsx b/public/app/features/alerting/unified/initAlerting.tsx index e043057330..b00d24d645 100644 --- a/public/app/features/alerting/unified/initAlerting.tsx +++ b/public/app/features/alerting/unified/initAlerting.tsx @@ -17,7 +17,7 @@ export function initAlerting() { if (contextSrv.hasPermission(grafanaRulesPermissions.read)) { addCustomRightAction({ - show: () => config.unifiedAlertingEnabled || (config.featureToggles.alertingPreviewUpgrade ?? false), + show: () => config.unifiedAlertingEnabled, component: ({ dashboard }) => ( {dashboard && } diff --git a/public/app/features/alerting/unified/integration/AlertRulesDrawerContent.tsx b/public/app/features/alerting/unified/integration/AlertRulesDrawerContent.tsx index 3c13303b96..22bd4a2a09 100644 --- a/public/app/features/alerting/unified/integration/AlertRulesDrawerContent.tsx +++ b/public/app/features/alerting/unified/integration/AlertRulesDrawerContent.tsx @@ -2,8 +2,7 @@ import React from 'react'; import { useAsync } from 'react-use'; import { LoadingPlaceholder } from '@grafana/ui'; -import { getDashboardScenePageStateManager } from 'app/features/dashboard-scene/pages/DashboardScenePageStateManager'; -import { DashboardRoutes, useDispatch } from 'app/types'; +import { useDispatch } from 'app/types'; import { RulesTable } from '../components/rules/RulesTable'; import { useCombinedRuleNamespaces } from '../hooks/useCombinedRuleNamespaces'; @@ -11,24 +10,12 @@ import { fetchPromAndRulerRulesAction } from '../state/actions'; import { Annotation } from '../utils/constants'; import { GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource'; -import LegacyAlertsDeprecationNotice from './LegacyAlertsDeprecationNotice'; - interface Props { dashboardUid: string; } export default function AlertRulesDrawerContent({ dashboardUid }: Props) { const dispatch = useDispatch(); - const dashboardStateManager = getDashboardScenePageStateManager(); - - const { value: dashboard, loading: loadingDashboardState } = useAsync(() => { - return dashboardStateManager - .fetchDashboard({ - uid: dashboardUid, - route: DashboardRoutes.Normal, - }) - .then((data) => (data ? data.dashboard : undefined)); - }, [dashboardStateManager]); const { loading: loadingAlertRules } = useAsync(async () => { await dispatch(fetchPromAndRulerRulesAction({ rulesSourceName: GRAFANA_RULES_SOURCE_NAME })); @@ -40,17 +27,14 @@ export default function AlertRulesDrawerContent({ dashboardUid }: Props) { .flatMap((g) => g.rules) .filter((rule) => rule.annotations[Annotation.dashboardUID] === dashboardUid); - const loading = loadingDashboardState || loadingAlertRules; + const loading = loadingAlertRules; return ( <> {loading ? ( ) : ( - <> - - - + )} ); diff --git a/public/app/features/alerting/unified/integration/LegacyAlertsDeprecationNotice.tsx b/public/app/features/alerting/unified/integration/LegacyAlertsDeprecationNotice.tsx deleted file mode 100644 index 66fa4595f0..0000000000 --- a/public/app/features/alerting/unified/integration/LegacyAlertsDeprecationNotice.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { isEmpty } from 'lodash'; -import React from 'react'; -import { useToggle } from 'react-use'; - -import { config } from '@grafana/runtime'; -import { Dashboard, Panel, RowPanel } from '@grafana/schema'; -import { Alert, Collapse, Column, InteractiveTable, TextLink } from '@grafana/ui'; - -import { makePanelLink } from '../utils/misc'; - -interface DeprecationNoticeProps { - dashboard: Dashboard; -} - -const usingLegacyAlerting = !config.unifiedAlertingEnabled; - -export default function LegacyAlertsDeprecationNotice({ dashboard }: DeprecationNoticeProps) { - // if the user is still using legacy alerting we don't need to show any notice at all – they will probably keep using legacy alerting and do not intend to upgrade. - if (usingLegacyAlerting) { - return null; - } - - const panelsWithLegacyAlerts = getLegacyAlertPanelsFromDashboard(dashboard); - - // don't show anything when the user has no legacy alerts defined - const hasLegacyAlerts = !isEmpty(panelsWithLegacyAlerts); - if (!hasLegacyAlerts) { - return null; - } - - return dashboard.uid ? : null; -} - -/** - * This function uses two different ways to detect legacy alerts based on what dashboard system is being used. - * - * 1. if using the older (non-scenes) dashboard system we can simply check for "alert" in the panel definition. - * 2. for dashboard scenes the alerts are no longer added to the model but we can check for "alertThreshold" in the panel options object - */ -function getLegacyAlertPanelsFromDashboard(dashboard: Dashboard): Panel[] { - const panelsWithLegacyAlerts = dashboard.panels?.filter((panel) => { - const hasAlertDefinition = 'alert' in panel; - const hasAlertThreshold = 'options' in panel && panel.options ? 'alertThreshold' in panel.options : false; - return hasAlertDefinition || hasAlertThreshold; - }); - - return panelsWithLegacyAlerts ?? []; -} - -interface Props { - dashboardUid: string; - panels: Panel[]; -} - -function LegacyAlertsWarning({ dashboardUid, panels }: Props) { - const [isOpen, toggleCollapsible] = useToggle(false); - - const columns: Array> = [ - { id: 'id', header: 'ID' }, - { - id: 'title', - header: 'Title', - cell: (cell) => ( - - {cell.value} - - ), - }, - ]; - - return ( - -

- You have legacy alert rules in this dashboard that were deprecated in Grafana 11 and are no longer supported. -

-

- Refer to{' '} - - our documentation - {' '} - on how to migrate legacy alert rules and how to import and export using Grafana Alerting. -

- - - String(panel.id)} pageSize={5} /> - -
- ); -} diff --git a/public/app/features/alerting/utils/dataSourceFromExpression.ts b/public/app/features/alerting/unified/utils/dataSourceFromExpression.ts similarity index 100% rename from public/app/features/alerting/utils/dataSourceFromExpression.ts rename to public/app/features/alerting/unified/utils/dataSourceFromExpression.ts diff --git a/public/app/features/alerting/utils/notificationChannel.test.ts b/public/app/features/alerting/utils/notificationChannel.test.ts deleted file mode 100644 index 757c9f3477..0000000000 --- a/public/app/features/alerting/utils/notificationChannel.test.ts +++ /dev/null @@ -1,210 +0,0 @@ -import { NotificationChannelDTO } from '../../../types'; - -import { transformSubmitData } from './notificationChannels'; - -const basicFormData: NotificationChannelDTO = { - id: 1, - uid: 'pX7fbbHGk', - name: 'Pete discord', - type: { - value: 'discord', - label: 'Discord', - type: 'discord', - name: 'Discord', - heading: 'Discord settings', - description: 'Sends notifications to Discord', - info: '', - options: [ - { - element: 'input', - inputType: 'text', - label: 'Message Content', - description: 'Mention a group using @ or a user using <@ID> when notifying in a channel', - placeholder: '', - propertyName: 'content', - selectOptions: null, - showWhen: { field: '', is: '' }, - required: false, - validationRule: '', - secure: false, - }, - { - element: 'input', - inputType: 'text', - label: 'Webhook URL', - description: '', - placeholder: 'Discord webhook URL', - propertyName: 'url', - selectOptions: null, - showWhen: { field: '', is: '' }, - required: true, - validationRule: '', - secure: false, - }, - ], - typeName: 'discord', - }, - isDefault: false, - sendReminder: false, - disableResolveMessage: false, - frequency: '', - created: '2020-08-24T10:46:43+02:00', - updated: '2020-09-02T14:08:27+02:00', - settings: { - url: 'https://discordapp.com/api/webhooks/', - uploadImage: true, - content: '', - autoResolve: true, - httpMethod: 'POST', - severity: 'critical', - }, - secureFields: {}, - secureSettings: {}, -}; - -const selectFormData: NotificationChannelDTO = { - id: 23, - uid: 'BxEN9rNGk', - name: 'Webhook', - type: { - value: 'webhook', - label: 'webhook', - type: 'webhook', - name: 'webhook', - heading: 'Webhook settings', - description: 'Sends HTTP POST request to a URL', - info: '', - options: [ - { - element: 'input', - inputType: 'text', - label: 'Url', - description: '', - placeholder: '', - propertyName: 'url', - selectOptions: null, - showWhen: { field: '', is: '' }, - required: true, - validationRule: '', - secure: false, - }, - { - element: 'select', - inputType: '', - label: 'Http Method', - description: '', - placeholder: '', - propertyName: 'httpMethod', - selectOptions: [ - { value: 'POST', label: 'POST' }, - { value: 'PUT', label: 'PUT' }, - ], - showWhen: { field: '', is: '' }, - required: false, - validationRule: '', - secure: false, - }, - { - element: 'input', - inputType: 'text', - label: 'Username', - description: '', - placeholder: '', - propertyName: 'username', - selectOptions: null, - showWhen: { field: '', is: '' }, - required: false, - validationRule: '', - secure: false, - }, - { - element: 'input', - inputType: 'password', - label: 'Password', - description: '', - placeholder: '', - propertyName: 'password', - selectOptions: null, - showWhen: { field: '', is: '' }, - required: false, - validationRule: '', - secure: true, - }, - ], - typeName: 'webhook', - }, - isDefault: false, - sendReminder: false, - disableResolveMessage: false, - frequency: '', - created: '2020-08-28T10:47:37+02:00', - updated: '2020-09-03T09:37:21+02:00', - settings: { - autoResolve: true, - httpMethod: 'POST', - password: '', - severity: 'critical', - uploadImage: true, - url: 'http://asdf', - username: 'asdf', - }, - secureFields: { password: true }, - secureSettings: {}, -}; - -describe('Transform submit data', () => { - it('basic transform', () => { - const expected = { - id: 1, - name: 'Pete discord', - type: 'discord', - sendReminder: false, - disableResolveMessage: false, - frequency: '15m', - settings: { - uploadImage: true, - autoResolve: true, - httpMethod: 'POST', - severity: 'critical', - url: 'https://discordapp.com/api/webhooks/', - content: '', - }, - secureSettings: {}, - secureFields: {}, - isDefault: false, - uid: 'pX7fbbHGk', - created: '2020-08-24T10:46:43+02:00', - updated: '2020-09-02T14:08:27+02:00', - }; - - expect(transformSubmitData(basicFormData)).toEqual(expected); - }); - - it('should transform form data with selects', () => { - const expected = { - created: '2020-08-28T10:47:37+02:00', - disableResolveMessage: false, - frequency: '15m', - id: 23, - isDefault: false, - name: 'Webhook', - secureFields: { password: true }, - secureSettings: {}, - sendReminder: false, - settings: { - autoResolve: true, - httpMethod: 'POST', - password: '', - severity: 'critical', - uploadImage: true, - url: 'http://asdf', - username: 'asdf', - }, - type: 'webhook', - uid: 'BxEN9rNGk', - updated: '2020-09-03T09:37:21+02:00', - }; - - expect(transformSubmitData(selectFormData)).toEqual(expected); - }); -}); diff --git a/public/app/features/alerting/utils/notificationChannels.ts b/public/app/features/alerting/utils/notificationChannels.ts deleted file mode 100644 index 7c9419ba39..0000000000 --- a/public/app/features/alerting/utils/notificationChannels.ts +++ /dev/null @@ -1,72 +0,0 @@ -import memoizeOne from 'memoize-one'; - -import { SelectableValue } from '@grafana/data'; -import { config } from '@grafana/runtime'; -import { NotificationChannelDTO, NotificationChannelType } from 'app/types'; - -export const defaultValues: NotificationChannelDTO = { - id: -1, - name: '', - type: { value: 'email', label: 'Email' }, - sendReminder: false, - disableResolveMessage: false, - frequency: '15m', - settings: { - uploadImage: config.rendererAvailable, - autoResolve: true, - httpMethod: 'POST', - severity: 'critical', - }, - secureSettings: {}, - secureFields: {}, - isDefault: false, -}; - -export const mapChannelsToSelectableValue = memoizeOne( - (notificationChannels: NotificationChannelType[], includeDescription: boolean): Array> => { - return notificationChannels.map((channel) => { - if (includeDescription) { - return { - value: channel.value, - label: channel.label, - description: channel.description, - }; - } - return { - value: channel.value, - label: channel.label, - }; - }); - } -); - -export const transformSubmitData = (formData: NotificationChannelDTO) => { - /* - Some settings can be options in a select, in order to not save a SelectableValue - we need to use check if it is a SelectableValue and use its value. - */ - const settings = Object.fromEntries( - Object.entries(formData.settings).map(([key, value]) => { - return [key, value && value.hasOwnProperty('value') ? value.value : value]; - }) - ); - - return { - ...defaultValues, - ...formData, - frequency: formData.frequency === '' ? defaultValues.frequency : formData.frequency, - type: formData.type.value, - settings: { ...defaultValues.settings, ...settings }, - secureSettings: { ...formData.secureSettings }, - }; -}; - -export const transformTestData = (formData: NotificationChannelDTO) => { - return { - name: formData.name, - type: formData.type.value, - frequency: formData.frequency ?? defaultValues.frequency, - settings: { ...Object.assign(defaultValues.settings, formData.settings) }, - secureSettings: { ...formData.secureSettings }, - }; -}; diff --git a/public/app/features/dashboard-scene/scene/AlertStatesDataLayer.ts b/public/app/features/dashboard-scene/scene/AlertStatesDataLayer.ts index b7e3f768da..ec64e60a49 100644 --- a/public/app/features/dashboard-scene/scene/AlertStatesDataLayer.ts +++ b/public/app/features/dashboard-scene/scene/AlertStatesDataLayer.ts @@ -1,4 +1,4 @@ -import { from, map, Unsubscribable, Observable } from 'rxjs'; +import { from, map, Unsubscribable } from 'rxjs'; import { AlertState, AlertStateInfo, DataTopic, LoadingState, toDataFrame } from '@grafana/data'; import { config, getBackendSrv } from '@grafana/runtime'; @@ -68,71 +68,58 @@ export class AlertStatesDataLayer return; } - let alerStatesExecution: Observable | undefined; - - if (this.isUsingLegacyAlerting()) { - alerStatesExecution = from( - getBackendSrv().get( - '/api/alerts/states-for-dashboard', - { - dashboardId: id, - }, - `dashboard-query-runner-alert-states-${id}` - ) - ).pipe(map((alertStates) => alertStates)); - } else { - alerStatesExecution = from( - getBackendSrv().get( - '/api/prometheus/grafana/api/v1/rules', - { - dashboard_uid: uid!, - }, - `dashboard-query-runner-unified-alert-states-${id}` - ) - ).pipe( - map((result: PromRulesResponse) => { - if (result.status === 'success') { - this.hasAlertRules = false; - const panelIdToAlertState: Record = {}; - - result.data.groups.forEach((group) => - group.rules.forEach((rule) => { - if (isAlertingRule(rule) && rule.annotations && rule.annotations[Annotation.panelID]) { - this.hasAlertRules = true; - const panelId = Number(rule.annotations[Annotation.panelID]); - const state = promAlertStateToAlertState(rule.state); - - // there can be multiple alerts per panel, so we make sure we get the most severe state: - // alerting > pending > ok - if (!panelIdToAlertState[panelId]) { - panelIdToAlertState[panelId] = { - state, - id: Object.keys(panelIdToAlertState).length, - panelId, - dashboardId: id!, - }; - } else if ( - state === AlertState.Alerting && - panelIdToAlertState[panelId].state !== AlertState.Alerting - ) { - panelIdToAlertState[panelId].state = AlertState.Alerting; - } else if ( - state === AlertState.Pending && - panelIdToAlertState[panelId].state !== AlertState.Alerting && - panelIdToAlertState[panelId].state !== AlertState.Pending - ) { - panelIdToAlertState[panelId].state = AlertState.Pending; - } + const alerStatesExecution = from( + getBackendSrv().get( + '/api/prometheus/grafana/api/v1/rules', + { + dashboard_uid: uid!, + }, + `dashboard-query-runner-unified-alert-states-${id}` + ) + ).pipe( + map((result: PromRulesResponse) => { + if (result.status === 'success') { + this.hasAlertRules = false; + const panelIdToAlertState: Record = {}; + + result.data.groups.forEach((group) => + group.rules.forEach((rule) => { + if (isAlertingRule(rule) && rule.annotations && rule.annotations[Annotation.panelID]) { + this.hasAlertRules = true; + const panelId = Number(rule.annotations[Annotation.panelID]); + const state = promAlertStateToAlertState(rule.state); + + // there can be multiple alerts per panel, so we make sure we get the most severe state: + // alerting > pending > ok + if (!panelIdToAlertState[panelId]) { + panelIdToAlertState[panelId] = { + state, + id: Object.keys(panelIdToAlertState).length, + panelId, + dashboardId: id!, + }; + } else if ( + state === AlertState.Alerting && + panelIdToAlertState[panelId].state !== AlertState.Alerting + ) { + panelIdToAlertState[panelId].state = AlertState.Alerting; + } else if ( + state === AlertState.Pending && + panelIdToAlertState[panelId].state !== AlertState.Alerting && + panelIdToAlertState[panelId].state !== AlertState.Pending + ) { + panelIdToAlertState[panelId].state = AlertState.Pending; } - }) - ); - return Object.values(panelIdToAlertState); - } - - throw new Error(`Unexpected alert rules response.`); - }) - ); - } + } + }) + ); + return Object.values(panelIdToAlertState); + } + + throw new Error(`Unexpected alert rules response.`); + }) + ); + this.querySub = alerStatesExecution.subscribe({ next: (stateUpdate) => { this.publishResults( @@ -165,50 +152,34 @@ export class AlertStatesDataLayer private canWork(timeRange: SceneTimeRangeLike): boolean { const dashboard = getDashboardSceneFor(this); - const { uid, id } = dashboard.state; + const { uid } = dashboard.state; - if (this.isUsingLegacyAlerting()) { - if (!id) { - return false; - } - - if (timeRange.state.value.raw.to !== 'now') { - return false; - } - - return true; - } else { - if (!uid) { - return false; - } - - // Cannot fetch rules while on a public dashboard since it's unauthenticated - if (config.publicDashboardAccessToken) { - return false; - } + if (!uid) { + return false; + } - if (timeRange.state.value.raw.to !== 'now') { - return false; - } + // Cannot fetch rules while on a public dashboard since it's unauthenticated + if (config.publicDashboardAccessToken) { + return false; + } - if (this.hasAlertRules === false) { - return false; - } + if (timeRange.state.value.raw.to !== 'now') { + return false; + } - const hasRuleReadPermission = - contextSrv.hasPermission(AccessControlAction.AlertingRuleRead) && - contextSrv.hasPermission(AccessControlAction.AlertingRuleExternalRead); + if (this.hasAlertRules === false) { + return false; + } - if (!hasRuleReadPermission) { - return false; - } + const hasRuleReadPermission = + contextSrv.hasPermission(AccessControlAction.AlertingRuleRead) && + contextSrv.hasPermission(AccessControlAction.AlertingRuleExternalRead); - return true; + if (!hasRuleReadPermission) { + return false; } - } - private isUsingLegacyAlerting(): boolean { - return !config.unifiedAlertingEnabled; + return true; } private handleError = (err: unknown) => { diff --git a/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx b/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx index 756ccb9258..487b0970cd 100644 --- a/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx +++ b/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx @@ -388,26 +388,11 @@ function createExtensionContext(panel: VizPanel, dashboard: DashboardScene): Plu } export function onRemovePanel(dashboard: DashboardScene, panel: VizPanel) { - const vizPanelData = sceneGraph.getData(panel); - let panelHasAlert = false; - - if (vizPanelData.state.data?.alertState) { - panelHasAlert = true; - } - - const text2 = - panelHasAlert && !config.unifiedAlertingEnabled - ? 'Panel includes an alert rule. removing the panel will also remove the alert rule' - : undefined; - const confirmText = panelHasAlert ? 'YES' : undefined; - appEvents.publish( new ShowConfirmModalEvent({ title: 'Remove panel', text: 'Are you sure you want to remove this panel?', - text2: text2, icon: 'trash-alt', - confirmText: confirmText, yesText: 'Remove', onConfirm: () => dashboard.removePanel(panel), }) diff --git a/public/app/features/dashboard-scene/settings/JsonModelEditView.tsx b/public/app/features/dashboard-scene/settings/JsonModelEditView.tsx index 03406b38e0..070dcf781c 100644 --- a/public/app/features/dashboard-scene/settings/JsonModelEditView.tsx +++ b/public/app/features/dashboard-scene/settings/JsonModelEditView.tsx @@ -7,7 +7,6 @@ import { Dashboard } from '@grafana/schema'; import { Alert, Box, Button, CodeEditor, Stack, useStyles2 } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; import { Trans } from 'app/core/internationalization'; -import LegacyAlertsDeprecationNotice from 'app/features/alerting/unified/integration/LegacyAlertsDeprecationNotice'; import { getPrettyJSON } from 'app/features/inspector/utils/utils'; import { DashboardDTO, SaveDashboardResponseDTO } from 'app/types'; @@ -127,7 +126,6 @@ export class JsonModelEditView extends SceneObjectBase i ); const styles = useStyles2(getStyles); - const saveModel = model.getSaveModel(); function renderSaveButtonAndError(error?: Error) { if (error && isSaving) { @@ -183,7 +181,6 @@ export class JsonModelEditView extends SceneObjectBase i The JSON model below is the data structure that defines the dashboard. This includes dashboard settings, panel settings, layout, queries, and so on. - - ; } @@ -43,7 +39,12 @@ const DeleteDashboardModalUnconnected = ({ hideModal, cleanUpDashboardAndVariabl return ( +

Do you want to delete this dashboard?

+

{dashboard.title}

+ + } onConfirm={onConfirm} onDismiss={hideModal} title="Delete" @@ -53,24 +54,6 @@ const DeleteDashboardModalUnconnected = ({ hideModal, cleanUpDashboardAndVariabl ); }; -const getModalBody = (panels: PanelModel[], title: string) => { - const totalAlerts = sumBy(panels, (panel) => (panel.alert ? 1 : 0)); - return totalAlerts > 0 && !config.unifiedAlertingEnabled ? ( - <> -

Do you want to delete this dashboard?

-

- This dashboard contains {totalAlerts} alert{totalAlerts > 1 ? 's' : ''}. Deleting this dashboard also deletes - those alerts. -

- - ) : ( - <> -

Do you want to delete this dashboard?

-

{title}

- - ); -}; - const ProvisionedDeleteModal = ({ hideModal, provisionedId }: { hideModal(): void; provisionedId: string }) => ( {tabs.map((tab) => { - if (tab.id === PanelEditorTabId.Alert) { - return renderAlertTab(tab, panel, dashboard, instrumentedOnChangeTab); + if (tab.id === PanelEditorTabId.Alert && alertingEnabled) { + return ( + onChangeTab(tab)} + icon={toIconName(tab.icon)} + panel={panel} + dashboard={dashboard} + /> + ); } return ( {activeTab.id === PanelEditorTabId.Query && } - {activeTab.id === PanelEditorTabId.Alert && } + {activeTab.id === PanelEditorTabId.Alert && } {activeTab.id === PanelEditorTabId.Transform && }
@@ -99,48 +111,6 @@ function getCounter(panel: PanelModel, tab: PanelEditorTab) { return null; } -function renderAlertTab( - tab: PanelEditorTab, - panel: PanelModel, - dashboard: DashboardModel, - onChangeTab: (tab: PanelEditorTab) => void -) { - const alertingDisabled = !config.alertingEnabled && !config.unifiedAlertingEnabled; - - if (alertingDisabled) { - return null; - } - - if (config.unifiedAlertingEnabled) { - return ( - onChangeTab(tab)} - icon={toIconName(tab.icon)} - panel={panel} - dashboard={dashboard} - /> - ); - } - - if (config.alertingEnabled) { - return ( - onChangeTab(tab)} - icon={toIconName(tab.icon)} - counter={getCounter(panel, tab)} - /> - ); - } - - return null; -} - const getStyles = (theme: GrafanaTheme2) => { return { wrapper: css` diff --git a/public/app/features/dashboard/components/PanelEditor/state/selectors.test.ts b/public/app/features/dashboard/components/PanelEditor/state/selectors.test.ts index 03e1685a99..2c2ecd6e6c 100644 --- a/public/app/features/dashboard/components/PanelEditor/state/selectors.test.ts +++ b/public/app/features/dashboard/components/PanelEditor/state/selectors.test.ts @@ -41,54 +41,6 @@ describe('getPanelEditorTabs selector', () => { }); describe('alerts tab', () => { - describe('when alerting enabled', () => { - beforeAll(() => { - updateConfig({ - alertingEnabled: true, - }); - }); - - it('returns Alerts tab for graph panel', () => { - const tabs = getPanelEditorTabs(undefined, { - meta: { - id: 'graph', - }, - } as PanelPlugin); - - expect(tabs.length).toEqual(3); - expect(tabs[2].id).toEqual(PanelEditorTabId.Alert); - }); - - it('does not returns tab for panel other than graph', () => { - const tabs = getPanelEditorTabs(undefined, { - meta: { - id: 'table', - }, - } as PanelPlugin); - expect(tabs.length).toEqual(2); - expect(tabs[1].id).toEqual(PanelEditorTabId.Transform); - }); - }); - - describe('when alerting disabled', () => { - beforeAll(() => { - updateConfig({ - alertingEnabled: false, - }); - }); - - it('does not return Alerts tab', () => { - const tabs = getPanelEditorTabs(undefined, { - meta: { - id: 'graph', - }, - } as PanelPlugin); - - expect(tabs.length).toEqual(2); - expect(tabs[1].id).toEqual(PanelEditorTabId.Transform); - }); - }); - describe('with unified alerting enabled', () => { beforeAll(() => { updateConfig({ unifiedAlertingEnabled: true }); diff --git a/public/app/features/dashboard/components/PanelEditor/state/selectors.ts b/public/app/features/dashboard/components/PanelEditor/state/selectors.ts index a8f71b34ca..7a7da8ee00 100644 --- a/public/app/features/dashboard/components/PanelEditor/state/selectors.ts +++ b/public/app/features/dashboard/components/PanelEditor/state/selectors.ts @@ -55,12 +55,15 @@ export const getPanelEditorTabs = memoizeOne((tab?: string, plugin?: PanelPlugin }); export function shouldShowAlertingTab(plugin: PanelPlugin) { - const { alertingEnabled, unifiedAlertingEnabled } = getConfig(); + const { unifiedAlertingEnabled = false } = getConfig(); const hasRuleReadPermissions = contextSrv.hasPermission(getRulesPermissions(GRAFANA_RULES_SOURCE_NAME).read); - const isAlertingAvailable = alertingEnabled || (unifiedAlertingEnabled && hasRuleReadPermissions); + const isAlertingAvailable = unifiedAlertingEnabled && hasRuleReadPermissions; + if (!isAlertingAvailable) { + return false; + } const isGraph = plugin.meta.id === 'graph'; const isTimeseries = plugin.meta.id === 'timeseries'; - return (isAlertingAvailable && isGraph) || isTimeseries; + return isGraph || isTimeseries; } diff --git a/public/app/features/dashboard/utils/panel.ts b/public/app/features/dashboard/utils/panel.ts index bc12af9784..688451bc73 100644 --- a/public/app/features/dashboard/utils/panel.ts +++ b/public/app/features/dashboard/utils/panel.ts @@ -19,17 +19,12 @@ import { ShowConfirmModalEvent, ShowModalReactEvent } from '../../../types/event export const removePanel = (dashboard: DashboardModel, panel: PanelModel, ask: boolean) => { // confirm deletion if (ask !== false) { - const text2 = - panel.alert && !config.unifiedAlertingEnabled - ? 'Panel includes an alert rule. removing the panel will also remove the alert rule' - : undefined; const confirmText = panel.alert ? 'YES' : undefined; appEvents.publish( new ShowConfirmModalEvent({ title: 'Remove panel', text: 'Are you sure you want to remove this panel?', - text2: text2, icon: 'trash-alt', confirmText: confirmText, yesText: 'Remove', diff --git a/public/app/features/folders/state/navModel.ts b/public/app/features/folders/state/navModel.ts index ecd0336ca0..5c7e0d4ffb 100644 --- a/public/app/features/folders/state/navModel.ts +++ b/public/app/features/folders/state/navModel.ts @@ -45,10 +45,7 @@ export function buildNavModel(folder: FolderDTO, parents = folder.parents): NavM url: `${folder.url}/library-panels`, }); - if ( - contextSrv.hasPermission(AccessControlAction.AlertingRuleRead) && - (config.unifiedAlertingEnabled || config.featureToggles.alertingPreviewUpgrade) - ) { + if (contextSrv.hasPermission(AccessControlAction.AlertingRuleRead) && config.unifiedAlertingEnabled) { model.children!.push({ active: false, icon: 'bell', diff --git a/public/app/features/plugins/built_in_plugins.ts b/public/app/features/plugins/built_in_plugins.ts index 9aacd8eab2..38dee1b807 100644 --- a/public/app/features/plugins/built_in_plugins.ts +++ b/public/app/features/plugins/built_in_plugins.ts @@ -28,7 +28,6 @@ const mssqlPlugin = async () => const alertmanagerPlugin = async () => await import(/* webpackChunkName: "alertmanagerPlugin" */ 'app/plugins/datasource/alertmanager/module'); -import * as alertGroupsPanel from 'app/plugins/panel/alertGroups/module'; import * as alertListPanel from 'app/plugins/panel/alertlist/module'; import * as annoListPanel from 'app/plugins/panel/annolist/module'; import * as barChartPanel from 'app/plugins/panel/barchart/module'; @@ -118,7 +117,6 @@ const builtInPlugins: Record Promise ({ - ...jest.requireActual('@grafana/runtime'), - getBackendSrv: () => backendSrv, -})); - -function getDefaultOptions(): DashboardQueryRunnerOptions { - const dashboard = { id: 'an id', panels: [{ alert: {} }] } as DashboardModel; - const range = getDefaultTimeRange(); - - return { dashboard, range }; -} - -function getTestContext() { - jest.clearAllMocks(); - const dispatchMock = jest.spyOn(store, 'dispatch'); - const options = getDefaultOptions(); - const getMock = jest.spyOn(backendSrv, 'get'); - - return { getMock, options, dispatchMock }; -} - -describe('AlertStatesWorker', () => { - const worker = new AlertStatesWorker(); - - describe('when canWork is called with correct props', () => { - it('then it should return true', () => { - const options = getDefaultOptions(); - - expect(worker.canWork(options)).toBe(true); - }); - }); - - describe('when canWork is called with no dashboard id', () => { - it('then it should return false', () => { - const dashboard = {} as DashboardModel; - const options = { ...getDefaultOptions(), dashboard }; - - expect(worker.canWork(options)).toBe(false); - }); - }); - - describe('when canWork is called with wrong range', () => { - it('then it should return false', () => { - const defaultRange = getDefaultTimeRange(); - const range: TimeRange = { ...defaultRange, raw: { ...defaultRange.raw, to: 'now-6h' } }; - const options = { ...getDefaultOptions(), range }; - - expect(worker.canWork(options)).toBe(false); - }); - }); - - describe('when canWork is called for dashboard with no alert panels', () => { - it('then it should return false', () => { - const options = getDefaultOptions(); - options.dashboard.panels.forEach((panel) => delete panel.alert); - expect(worker.canWork(options)).toBe(false); - }); - }); - - describe('when run is called with incorrect props', () => { - it('then it should return the correct results', async () => { - const { getMock, options } = getTestContext(); - const dashboard = {} as DashboardModel; - - await expect(worker.work({ ...options, dashboard })).toEmitValuesWith((received) => { - expect(received).toHaveLength(1); - const results = received[0]; - expect(results).toEqual({ alertStates: [], annotations: [] }); - expect(getMock).not.toHaveBeenCalled(); - }); - }); - }); - - describe('when run is called with correct props and request is successful', () => { - it('then it should return the correct results', async () => { - const getResults: AlertStateInfo[] = [ - { id: 1, state: AlertState.Alerting, dashboardId: 1, panelId: 1 }, - { id: 2, state: AlertState.Alerting, dashboardId: 1, panelId: 2 }, - ]; - const { getMock, options } = getTestContext(); - getMock.mockResolvedValue(getResults); - - await expect(worker.work(options)).toEmitValuesWith((received) => { - expect(received).toHaveLength(1); - const results = received[0]; - expect(results).toEqual({ alertStates: getResults, annotations: [] }); - expect(getMock).toHaveBeenCalledTimes(1); - }); - }); - }); - - describe('when run is called with correct props and request fails', () => { - silenceConsoleOutput(); - it('then it should return the correct results', async () => { - const { getMock, options, dispatchMock } = getTestContext(); - getMock.mockRejectedValue({ message: 'An error' }); - - await expect(worker.work(options)).toEmitValuesWith((received) => { - expect(received).toHaveLength(1); - const results = received[0]; - expect(results).toEqual({ alertStates: [], annotations: [] }); - expect(getMock).toHaveBeenCalledTimes(1); - expect(dispatchMock).toHaveBeenCalledTimes(1); - }); - }); - }); - - describe('when run is called with correct props and request is cancelled', () => { - silenceConsoleOutput(); - it('then it should return the correct results', async () => { - const { getMock, options, dispatchMock } = getTestContext(); - getMock.mockRejectedValue({ cancelled: true }); - - await expect(worker.work(options)).toEmitValuesWith((received) => { - expect(received).toHaveLength(1); - const results = received[0]; - expect(results).toEqual({ alertStates: [], annotations: [] }); - expect(getMock).toHaveBeenCalledTimes(1); - expect(dispatchMock).not.toHaveBeenCalled(); - }); - }); - }); -}); diff --git a/public/app/features/query/state/DashboardQueryRunner/AlertStatesWorker.ts b/public/app/features/query/state/DashboardQueryRunner/AlertStatesWorker.ts deleted file mode 100644 index edefd0710e..0000000000 --- a/public/app/features/query/state/DashboardQueryRunner/AlertStatesWorker.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { from, Observable } from 'rxjs'; -import { catchError, map } from 'rxjs/operators'; - -import { getBackendSrv } from '@grafana/runtime'; - -import { DashboardQueryRunnerOptions, DashboardQueryRunnerWorker, DashboardQueryRunnerWorkerResult } from './types'; -import { emptyResult, handleDashboardQueryRunnerWorkerError } from './utils'; - -export class AlertStatesWorker implements DashboardQueryRunnerWorker { - canWork({ dashboard, range }: DashboardQueryRunnerOptions): boolean { - if (!dashboard.id) { - return false; - } - - if (range.raw.to !== 'now') { - return false; - } - - // if dashboard has no alerts, no point to query alert states - if (!dashboard.panels.find((panel) => !!panel.alert)) { - return false; - } - - return true; - } - - work(options: DashboardQueryRunnerOptions): Observable { - if (!this.canWork(options)) { - return emptyResult(); - } - - const { dashboard } = options; - return from( - getBackendSrv().get( - '/api/alerts/states-for-dashboard', - { - dashboardId: dashboard.id, - }, - `dashboard-query-runner-alert-states-${dashboard.id}` - ) - ).pipe( - map((alertStates) => { - return { alertStates, annotations: [] }; - }), - catchError(handleDashboardQueryRunnerWorkerError) - ); - } -} diff --git a/public/app/features/query/state/DashboardQueryRunner/DashboardQueryRunner.test.ts b/public/app/features/query/state/DashboardQueryRunner/DashboardQueryRunner.test.ts index 3604fb574d..e88b7a5a9b 100644 --- a/public/app/features/query/state/DashboardQueryRunner/DashboardQueryRunner.test.ts +++ b/public/app/features/query/state/DashboardQueryRunner/DashboardQueryRunner.test.ts @@ -1,9 +1,13 @@ import { throwError } from 'rxjs'; import { delay, first } from 'rxjs/operators'; -import { AlertState, AlertStateInfo } from '@grafana/data'; +import { AlertState } from '@grafana/data'; import { DataSourceSrv, setDataSourceSrv } from '@grafana/runtime'; +import { grantUserPermissions } from 'app/features/alerting/unified/mocks'; +import { Annotation } from 'app/features/alerting/unified/utils/constants'; import { TimeSrv } from 'app/features/dashboard/services/TimeSrv'; +import { AccessControlAction } from 'app/types'; +import { PromAlertingRuleState, PromRulesResponse, PromRuleType } from 'app/types/unified-alerting-dto'; import { silenceConsoleOutput } from '../../../../../test/core/utils/silenceConsoleOutput'; import { backendSrv } from '../../../../core/services/backend_srv'; @@ -18,6 +22,10 @@ jest.mock('@grafana/runtime', () => ({ getBackendSrv: () => backendSrv, })); +beforeEach(() => { + grantUserPermissions([AccessControlAction.AlertingRuleRead, AccessControlAction.AlertingRuleExternalRead]); +}); + function getTestContext() { jest.clearAllMocks(); const timeSrvMock = { timeRange: jest.fn() } as unknown as TimeSrv; @@ -25,10 +33,55 @@ function getTestContext() { // These tests are setup so all the workers and runners are invoked once, this wouldn't be the case in real life const runner = createDashboardQueryRunner({ dashboard: options.dashboard, timeSrv: timeSrvMock }); - const getResults: AlertStateInfo[] = [ - { id: 1, state: AlertState.Alerting, dashboardId: 1, panelId: 1 }, - { id: 2, state: AlertState.Alerting, dashboardId: 1, panelId: 2 }, - ]; + const getResults: PromRulesResponse = { + status: 'success', + data: { + groups: [ + { + name: 'my-group', + rules: [ + { + name: 'my alert', + state: PromAlertingRuleState.Firing, + query: 'foo > 1', + type: PromRuleType.Alerting, + annotations: { + [Annotation.dashboardUID]: '1', + [Annotation.panelID]: '1', + }, + health: 'ok', + labels: {}, + }, + ], + interval: 300, + file: 'my-namespace', + }, + { + name: 'another-group', + rules: [ + { + name: 'another alert', + query: 'foo > 1', + state: PromAlertingRuleState.Firing, + type: PromRuleType.Alerting, + annotations: { + [Annotation.dashboardUID]: '1', + [Annotation.panelID]: '2', + }, + health: 'ok', + labels: {}, + }, + ], + interval: 300, + file: 'my-namespace', + }, + ], + totals: { + alerting: 2, + }, + }, + }; + const getMock = jest.spyOn(backendSrv, 'get').mockResolvedValue(getResults); const executeAnnotationQueryMock = jest .spyOn(annotationsSrv, 'executeAnnotationQuery') @@ -294,7 +347,7 @@ function getExpectedForAllResult(): DashboardQueryRunnerResult { return { alertState: { dashboardId: 1, - id: 1, + id: 0, panelId: 1, state: AlertState.Alerting, }, diff --git a/public/app/features/query/state/DashboardQueryRunner/DashboardQueryRunner.ts b/public/app/features/query/state/DashboardQueryRunner/DashboardQueryRunner.ts index 42538db039..ddd605865e 100644 --- a/public/app/features/query/state/DashboardQueryRunner/DashboardQueryRunner.ts +++ b/public/app/features/query/state/DashboardQueryRunner/DashboardQueryRunner.ts @@ -3,13 +3,11 @@ import { finalize, map, mapTo, mergeAll, reduce, share, takeUntil } from 'rxjs/o import { AnnotationQuery } from '@grafana/data'; import { RefreshEvent } from '@grafana/runtime'; -import { config } from 'app/core/config'; import { dedupAnnotations } from 'app/features/annotations/events_processing'; import { getTimeSrv, TimeSrv } from '../../../dashboard/services/TimeSrv'; import { DashboardModel } from '../../../dashboard/state'; -import { AlertStatesWorker } from './AlertStatesWorker'; import { AnnotationsWorker } from './AnnotationsWorker'; import { SnapshotWorker } from './SnapshotWorker'; import { UnifiedAlertStatesWorker } from './UnifiedAlertStatesWorker'; @@ -33,7 +31,7 @@ class DashboardQueryRunnerImpl implements DashboardQueryRunner { private readonly dashboard: DashboardModel, private readonly timeSrv: TimeSrv = getTimeSrv(), private readonly workers: DashboardQueryRunnerWorker[] = [ - config.unifiedAlertingEnabled ? new UnifiedAlertStatesWorker() : new AlertStatesWorker(), + new UnifiedAlertStatesWorker(), new SnapshotWorker(), new AnnotationsWorker(), ] diff --git a/public/app/features/query/state/DashboardQueryRunner/testHelpers.ts b/public/app/features/query/state/DashboardQueryRunner/testHelpers.ts index 027f02514d..87345417ef 100644 --- a/public/app/features/query/state/DashboardQueryRunner/testHelpers.ts +++ b/public/app/features/query/state/DashboardQueryRunner/testHelpers.ts @@ -41,6 +41,7 @@ export function getDefaultOptions(): DashboardQueryRunnerOptions { const nextGen = getAnnotation({ datasource: NEXT_GEN_DS_NAME }); const dashboard: any = { id: 1, + uid: '1', annotations: { list: [ legacy, diff --git a/public/app/plugins/panel/alertGroups/AlertGroup.tsx b/public/app/plugins/panel/alertGroups/AlertGroup.tsx deleted file mode 100644 index 2ffb2de9aa..0000000000 --- a/public/app/plugins/panel/alertGroups/AlertGroup.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import { css } from '@emotion/css'; -import React, { useState, useEffect } from 'react'; - -import { GrafanaTheme2, intervalToAbbreviatedDurationString } from '@grafana/data'; -import { useStyles2, LinkButton } from '@grafana/ui'; -import { AlertLabels } from 'app/features/alerting/unified/components/AlertLabels'; -import { CollapseToggle } from 'app/features/alerting/unified/components/CollapseToggle'; -import { AlertGroupHeader } from 'app/features/alerting/unified/components/alert-groups/AlertGroupHeader'; -import { getNotificationsTextColors } from 'app/features/alerting/unified/styles/notifications'; -import { makeAMLink, makeLabelBasedSilenceLink } from 'app/features/alerting/unified/utils/misc'; -import { AlertmanagerGroup, AlertState } from 'app/plugins/datasource/alertmanager/types'; - -type Props = { - alertManagerSourceName: string; - group: AlertmanagerGroup; - expandAll: boolean; -}; - -export const AlertGroup = ({ alertManagerSourceName, group, expandAll }: Props) => { - const [showAlerts, setShowAlerts] = useState(expandAll); - const styles = useStyles2(getStyles); - const textStyles = useStyles2(getNotificationsTextColors); - - useEffect(() => setShowAlerts(expandAll), [expandAll]); - - return ( -
- {Object.keys(group.labels).length > 0 ? ( - - ) : ( -
No grouping
- )} -
- setShowAlerts(!showAlerts)} />{' '} - -
- {showAlerts && ( -
- {group.alerts.map((alert, index) => { - const state = alert.status.state.toUpperCase(); - const interval = intervalToAbbreviatedDurationString({ - start: new Date(alert.startsAt), - end: Date.now(), - }); - - return ( -
-
- {state} for {interval} -
-
- -
-
- {alert.status.state === AlertState.Suppressed && ( - - Manage silences - - )} - {alert.status.state === AlertState.Active && ( - - Silence - - )} - {alert.generatorURL && ( - - See source - - )} -
-
- ); - })} -
- )} -
- ); -}; - -const getStyles = (theme: GrafanaTheme2) => ({ - noGroupingText: css` - height: ${theme.spacing(4)}; - `, - group: css` - background-color: ${theme.colors.background.secondary}; - margin: ${theme.spacing(0.5, 1, 0.5, 1)}; - padding: ${theme.spacing(1)}; - `, - row: css` - display: flex; - flex-direction: row; - align-items: center; - gap: ${theme.spacing(1)}; - `, - alerts: css` - margin: ${theme.spacing(0, 2, 0, 4)}; - `, - alert: css` - padding: ${theme.spacing(1, 0)}; - & + & { - border-top: 1px solid ${theme.colors.border.medium}; - } - `, - button: css` - & + & { - margin-left: ${theme.spacing(1)}; - } - `, - actionsRow: css` - padding: ${theme.spacing(1, 0)}; - `, -}); diff --git a/public/app/plugins/panel/alertGroups/AlertGroupsPanel.test.tsx b/public/app/plugins/panel/alertGroups/AlertGroupsPanel.test.tsx deleted file mode 100644 index 6e8b34f1b9..0000000000 --- a/public/app/plugins/panel/alertGroups/AlertGroupsPanel.test.tsx +++ /dev/null @@ -1,145 +0,0 @@ -import { render } from '@testing-library/react'; -import React from 'react'; -import { Provider } from 'react-redux'; -import { byTestId } from 'testing-library-selector'; - -import { getDefaultTimeRange, LoadingState, PanelProps, FieldConfigSource } from '@grafana/data'; -import { setDataSourceSrv } from '@grafana/runtime'; -import { Dashboard } from '@grafana/schema'; -import { fetchAlertGroups } from 'app/features/alerting/unified/api/alertmanager'; -import { - mockAlertGroup, - mockAlertmanagerAlert, - mockDataSource, - MockDataSourceSrv, -} from 'app/features/alerting/unified/mocks'; -import { DataSourceType } from 'app/features/alerting/unified/utils/datasource'; -import { DashboardSrv, setDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; -import { DashboardModel } from 'app/features/dashboard/state'; -import { configureStore } from 'app/store/configureStore'; - -import { AlertGroupsPanel } from './AlertGroupsPanel'; -import { Options } from './panelcfg.gen'; - -jest.mock('app/features/alerting/unified/api/alertmanager'); - -jest.mock('@grafana/runtime', () => ({ - ...jest.requireActual('@grafana/runtime'), - config: { - ...jest.requireActual('@grafana/runtime').config, - buildInfo: {}, - panels: {}, - unifiedAlertingEnabled: true, - }, -})); - -const mocks = { - api: { - fetchAlertGroups: jest.mocked(fetchAlertGroups), - }, -}; - -const dataSources = { - am: mockDataSource({ - name: 'Alertmanager', - type: DataSourceType.Alertmanager, - }), -}; - -const defaultOptions: Options = { - labels: '', - alertmanager: 'Alertmanager', - expandAll: false, -}; - -const defaultProps: PanelProps = { - data: { state: LoadingState.Done, series: [], timeRange: getDefaultTimeRange() }, - id: 1, - timeRange: getDefaultTimeRange(), - timeZone: 'utc', - options: defaultOptions, - eventBus: { - subscribe: jest.fn(), - getStream: jest.fn().mockReturnValue({ - subscribe: jest.fn(), - }), - publish: jest.fn(), - removeAllListeners: jest.fn(), - newScopedBus: jest.fn(), - }, - fieldConfig: {} as unknown as FieldConfigSource, - height: 400, - onChangeTimeRange: jest.fn(), - onFieldConfigChange: jest.fn(), - onOptionsChange: jest.fn(), - renderCounter: 1, - replaceVariables: jest.fn(), - title: 'Alert groups test', - transparent: false, - width: 320, -}; - -const renderPanel = (options: Options = defaultOptions) => { - const store = configureStore(); - const dash = new DashboardModel({ id: 1 } as Dashboard); - dash.formatDate = (time: number) => new Date(time).toISOString(); - const dashSrv = { getCurrent: () => dash } as DashboardSrv; - setDashboardSrv(dashSrv); - - defaultProps.options = options; - const props = { ...defaultProps }; - - return render( - - - - ); -}; - -const ui = { - group: byTestId('alert-group'), - alert: byTestId('alert-group-alert'), -}; - -describe('AlertGroupsPanel', () => { - beforeAll(() => { - mocks.api.fetchAlertGroups.mockImplementation(() => { - return Promise.resolve([ - mockAlertGroup({ labels: {}, alerts: [mockAlertmanagerAlert({ labels: { foo: 'bar' } })] }), - mockAlertGroup(), - ]); - }); - }); - - beforeEach(() => { - setDataSourceSrv(new MockDataSourceSrv(dataSources)); - }); - - it('renders the panel with the groups', async () => { - renderPanel(); - - const groups = await ui.group.findAll(); - - expect(groups).toHaveLength(2); - - expect(groups[0]).toHaveTextContent('No grouping'); - expect(groups[1]).toHaveTextContent('severitywarning regionUS-Central'); - - const alerts = ui.alert.queryAll(); - expect(alerts).toHaveLength(0); - }); - - it('renders panel with groups expanded', async () => { - renderPanel({ labels: '', alertmanager: 'Alertmanager', expandAll: true }); - - const alerts = await ui.alert.findAll(); - expect(alerts).toHaveLength(3); - }); - - it('filters alerts by label filter', async () => { - renderPanel({ labels: 'region=US-Central', alertmanager: 'Alertmanager', expandAll: true }); - - const alerts = await ui.alert.findAll(); - expect(alerts).toHaveLength(2); - }); -}); diff --git a/public/app/plugins/panel/alertGroups/AlertGroupsPanel.tsx b/public/app/plugins/panel/alertGroups/AlertGroupsPanel.tsx deleted file mode 100644 index 8de959e4da..0000000000 --- a/public/app/plugins/panel/alertGroups/AlertGroupsPanel.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import React, { useEffect } from 'react'; - -import { PanelProps } from '@grafana/data'; -import { config } from '@grafana/runtime'; -import { CustomScrollbar } from '@grafana/ui'; -import { useUnifiedAlertingSelector } from 'app/features/alerting/unified/hooks/useUnifiedAlertingSelector'; -import { fetchAlertGroupsAction } from 'app/features/alerting/unified/state/actions'; -import { parseMatchers } from 'app/features/alerting/unified/utils/alertmanager'; -import { NOTIFICATIONS_POLL_INTERVAL_MS } from 'app/features/alerting/unified/utils/constants'; -import { initialAsyncRequestState } from 'app/features/alerting/unified/utils/redux'; -import { AlertmanagerGroup, Matcher } from 'app/plugins/datasource/alertmanager/types'; -import { useDispatch } from 'app/types'; - -import { AlertGroup } from './AlertGroup'; -import { Options } from './panelcfg.gen'; -import { useFilteredGroups } from './useFilteredGroups'; - -export const AlertGroupsPanel = (props: PanelProps) => { - const dispatch = useDispatch(); - const isAlertingEnabled = config.unifiedAlertingEnabled; - - const expandAll = props.options.expandAll; - const alertManagerSourceName = props.options.alertmanager; - - const alertGroups = useUnifiedAlertingSelector((state) => state.amAlertGroups) || initialAsyncRequestState; - const results: AlertmanagerGroup[] = alertGroups[alertManagerSourceName || '']?.result || []; - const matchers: Matcher[] = props.options.labels ? parseMatchers(props.options.labels) : []; - - const filteredResults = useFilteredGroups(results, matchers); - - useEffect(() => { - function fetchNotifications() { - if (alertManagerSourceName) { - dispatch(fetchAlertGroupsAction(alertManagerSourceName)); - } - } - fetchNotifications(); - const interval = setInterval(fetchNotifications, NOTIFICATIONS_POLL_INTERVAL_MS); - return () => { - clearInterval(interval); - }; - }, [dispatch, alertManagerSourceName]); - - const hasResults = filteredResults.length > 0; - - return ( - - {isAlertingEnabled && ( -
- {hasResults && - filteredResults.map((group) => { - return ( - - ); - })} - {!hasResults && 'No alerts'} -
- )} -
- ); -}; diff --git a/public/app/plugins/panel/alertGroups/AlertmanagerPicker.tsx b/public/app/plugins/panel/alertGroups/AlertmanagerPicker.tsx deleted file mode 100644 index 2d18cd111a..0000000000 --- a/public/app/plugins/panel/alertGroups/AlertmanagerPicker.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import React, { useMemo } from 'react'; - -import { SelectableValue } from '@grafana/data'; -import { Select } from '@grafana/ui'; -import { AlertManagerDataSource, GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource'; - -interface Props { - onChange: (alertManagerSourceName: string) => void; - current?: string; - dataSources: AlertManagerDataSource[]; -} - -function getAlertManagerLabel(alertManager: AlertManagerDataSource) { - return alertManager.name === GRAFANA_RULES_SOURCE_NAME ? 'Grafana' : alertManager.name.slice(0, 37); -} - -export const AlertManagerPicker = ({ onChange, current, dataSources }: Props) => { - const options: Array> = useMemo(() => { - return dataSources.map((ds) => ({ - label: getAlertManagerLabel(ds), - value: ds.name, - imgUrl: ds.imgUrl, - meta: ds.meta, - })); - }, [dataSources]); - - return ( - - - - - +
+ {({ register, errors }) => { + const isDisabled = items.length === 0 || Object.keys(errors).length > 0; + return ( + <> + + + + + + - + -
- - - +
+ + + - - - -
+ + + +
- - - - Cancel - - - - ); - }} - -
+ + + + Cancel + + + + ); + }} + ); }; From 1714d52f17dc00f12af200bc21e95ba91d601102 Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Sat, 16 Mar 2024 08:48:05 +0100 Subject: [PATCH 0732/1406] Chore: Replace deprecated Form imports (#84537) * SignupInvited: replace Form * Chore: replace Form import * Chore: replace HorizontalGroup * Replace the component in OrgProfile --- public/app/features/invites/SignupInvited.tsx | 3 ++- public/app/features/org/OrgProfile.tsx | 3 ++- public/app/features/profile/UserProfileEditForm.tsx | 3 ++- .../serviceaccounts/ServiceAccountCreatePage.tsx | 3 ++- public/app/features/storage/CreateNewFolderModal.tsx | 3 ++- .../features/support-bundles/SupportBundlesCreate.tsx | 9 +++++---- 6 files changed, 15 insertions(+), 9 deletions(-) diff --git a/public/app/features/invites/SignupInvited.tsx b/public/app/features/invites/SignupInvited.tsx index 53ffa453c7..bea0197000 100644 --- a/public/app/features/invites/SignupInvited.tsx +++ b/public/app/features/invites/SignupInvited.tsx @@ -2,7 +2,8 @@ import React, { useState } from 'react'; import { useAsync } from 'react-use'; import { getBackendSrv } from '@grafana/runtime'; -import { Button, Field, Form, Input } from '@grafana/ui'; +import { Button, Field, Input } from '@grafana/ui'; +import { Form } from 'app/core/components/Form/Form'; import { Page } from 'app/core/components/Page/Page'; import { getConfig } from 'app/core/config'; import { contextSrv } from 'app/core/core'; diff --git a/public/app/features/org/OrgProfile.tsx b/public/app/features/org/OrgProfile.tsx index 8988d9cbd1..cf65e9c353 100644 --- a/public/app/features/org/OrgProfile.tsx +++ b/public/app/features/org/OrgProfile.tsx @@ -1,6 +1,7 @@ import React from 'react'; -import { Input, Field, FieldSet, Button, Form } from '@grafana/ui'; +import { Input, Field, FieldSet, Button } from '@grafana/ui'; +import { Form } from 'app/core/components/Form/Form'; import { contextSrv } from 'app/core/core'; import { AccessControlAction } from 'app/types'; diff --git a/public/app/features/profile/UserProfileEditForm.tsx b/public/app/features/profile/UserProfileEditForm.tsx index 371a0a0f57..dcde61b119 100644 --- a/public/app/features/profile/UserProfileEditForm.tsx +++ b/public/app/features/profile/UserProfileEditForm.tsx @@ -1,7 +1,8 @@ import React from 'react'; import { selectors } from '@grafana/e2e-selectors'; -import { Button, Field, FieldSet, Form, Icon, Input, Tooltip } from '@grafana/ui'; +import { Button, Field, FieldSet, Icon, Input, Tooltip } from '@grafana/ui'; +import { Form } from 'app/core/components/Form/Form'; import config from 'app/core/config'; import { t, Trans } from 'app/core/internationalization'; import { UserDTO } from 'app/types'; diff --git a/public/app/features/serviceaccounts/ServiceAccountCreatePage.tsx b/public/app/features/serviceaccounts/ServiceAccountCreatePage.tsx index 725969145f..3d3f0ef3a7 100644 --- a/public/app/features/serviceaccounts/ServiceAccountCreatePage.tsx +++ b/public/app/features/serviceaccounts/ServiceAccountCreatePage.tsx @@ -1,7 +1,8 @@ import React, { useCallback, useEffect, useState } from 'react'; import { getBackendSrv, locationService } from '@grafana/runtime'; -import { Form, Button, Input, Field, FieldSet } from '@grafana/ui'; +import { Button, Input, Field, FieldSet } from '@grafana/ui'; +import { Form } from 'app/core/components/Form/Form'; import { Page } from 'app/core/components/Page/Page'; import { UserRolePicker } from 'app/core/components/RolePicker/UserRolePicker'; import { fetchRoleOptions, updateUserRoles } from 'app/core/components/RolePicker/api'; diff --git a/public/app/features/storage/CreateNewFolderModal.tsx b/public/app/features/storage/CreateNewFolderModal.tsx index a4ee74c080..2ff4a88782 100644 --- a/public/app/features/storage/CreateNewFolderModal.tsx +++ b/public/app/features/storage/CreateNewFolderModal.tsx @@ -1,7 +1,8 @@ import React from 'react'; import { SubmitHandler, Validate } from 'react-hook-form'; -import { Button, Field, Form, Input, Modal } from '@grafana/ui'; +import { Button, Field, Input, Modal } from '@grafana/ui'; +import { Form } from 'app/core/components/Form/Form'; type FormModel = { folderName: string }; diff --git a/public/app/features/support-bundles/SupportBundlesCreate.tsx b/public/app/features/support-bundles/SupportBundlesCreate.tsx index 74c87f6805..1174698171 100644 --- a/public/app/features/support-bundles/SupportBundlesCreate.tsx +++ b/public/app/features/support-bundles/SupportBundlesCreate.tsx @@ -1,7 +1,8 @@ import React, { useEffect } from 'react'; import { connect, ConnectedProps } from 'react-redux'; -import { Form, Button, Field, Checkbox, LinkButton, HorizontalGroup, Alert } from '@grafana/ui'; +import { Button, Field, Checkbox, LinkButton, Stack, Alert } from '@grafana/ui'; +import { Form } from 'app/core/components/Form/Form'; import { Page } from 'app/core/components/Page/Page'; import { StoreState } from 'app/types'; @@ -60,7 +61,7 @@ export const SupportBundlesCreateUnconnected = ({ {createBundleError && } {!!collectors.length && (
- {({ register, errors }) => { + {({ register }) => { return ( <> {[...collectors] @@ -79,12 +80,12 @@ export const SupportBundlesCreateUnconnected = ({ ); })} - + Cancel - + ); }} From 39b32524e2f6e08512a78d8efa98042d6b048cee Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Sat, 16 Mar 2024 08:48:17 +0100 Subject: [PATCH 0733/1406] AnnotationsEditor: Remove deprecated components (#84538) * AnnotationEditorForm: Remove deprecated components * AnnotationEditor2: Remove deprecated components --- .../plugins/annotations/AnnotationEditorForm.tsx | 14 ++++++++------ .../plugins/annotations2/AnnotationEditor2.tsx | 14 ++++++++------ 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/public/app/plugins/panel/timeseries/plugins/annotations/AnnotationEditorForm.tsx b/public/app/plugins/panel/timeseries/plugins/annotations/AnnotationEditorForm.tsx index 26e041a178..c8de3bd393 100644 --- a/public/app/plugins/panel/timeseries/plugins/annotations/AnnotationEditorForm.tsx +++ b/public/app/plugins/panel/timeseries/plugins/annotations/AnnotationEditorForm.tsx @@ -1,10 +1,12 @@ import { css, cx } from '@emotion/css'; import React, { HTMLAttributes, useRef } from 'react'; +import { Controller } from 'react-hook-form'; import useAsyncFn from 'react-use/lib/useAsyncFn'; import useClickAway from 'react-use/lib/useClickAway'; import { AnnotationEventUIModel, GrafanaTheme2 } from '@grafana/data'; -import { Button, Field, Form, HorizontalGroup, InputControl, TextArea, usePanelContext, useStyles2 } from '@grafana/ui'; +import { Button, Field, Stack, TextArea, usePanelContext, useStyles2 } from '@grafana/ui'; +import { Form } from 'app/core/components/Form/Form'; import { TagFilter } from 'app/core/components/TagFilter/TagFilter'; import { getAnnotationTags } from 'app/features/annotations/api'; @@ -73,10 +75,10 @@ export const AnnotationEditorForm = React.forwardRef
- +
Add annotation
{ts}
-
+
@@ -94,7 +96,7 @@ export const AnnotationEditorForm = React.forwardRef - { @@ -110,14 +112,14 @@ export const AnnotationEditorForm = React.forwardRef - + - + ); }} diff --git a/public/app/plugins/panel/timeseries/plugins/annotations2/AnnotationEditor2.tsx b/public/app/plugins/panel/timeseries/plugins/annotations2/AnnotationEditor2.tsx index 702711454e..21f998ce69 100644 --- a/public/app/plugins/panel/timeseries/plugins/annotations2/AnnotationEditor2.tsx +++ b/public/app/plugins/panel/timeseries/plugins/annotations2/AnnotationEditor2.tsx @@ -1,9 +1,11 @@ import { css } from '@emotion/css'; import React, { useRef } from 'react'; +import { Controller } from 'react-hook-form'; import { useAsyncFn, useClickAway } from 'react-use'; import { AnnotationEventUIModel, GrafanaTheme2, dateTimeFormat, systemDateFormats } from '@grafana/data'; -import { Button, Field, Form, HorizontalGroup, InputControl, TextArea, usePanelContext, useStyles2 } from '@grafana/ui'; +import { Button, Field, Stack, TextArea, usePanelContext, useStyles2 } from '@grafana/ui'; +import { Form } from 'app/core/components/Form/Form'; import { TagFilter } from 'app/core/components/TagFilter/TagFilter'; import { getAnnotationTags } from 'app/features/annotations/api'; @@ -67,10 +69,10 @@ export const AnnotationEditor2 = ({ annoVals, annoIdx, dismiss, timeZone, ...oth return (
- +
{isUpdatingAnnotation ? 'Edit annotation' : 'Add annotation'}
{time}
-
+
onSubmit={onSubmit} @@ -89,7 +91,7 @@ export const AnnotationEditor2 = ({ annoVals, annoIdx, dismiss, timeZone, ...oth /> - { @@ -107,14 +109,14 @@ export const AnnotationEditor2 = ({ annoVals, annoIdx, dismiss, timeZone, ...oth
- + - +
); From 6204f1e847088d3283507985ff4d031fc67f7cc4 Mon Sep 17 00:00:00 2001 From: Andres Martinez Gotor Date: Mon, 18 Mar 2024 09:33:22 +0100 Subject: [PATCH 0734/1406] Chore: Use SigV4 middleware from aws-sdk (#84462) --- .github/CODEOWNERS | 2 - go.mod | 2 +- go.sum | 4 +- .../http_client_provider.go | 3 +- .../http_client_provider_test.go | 3 +- .../httpclientprovider/sigv4_middleware.go | 47 ------- .../sigv4_middleware_test.go | 118 ------------------ pkg/registry/apis/datasource/middleware.go | 12 +- .../pluginsintegration/pluginconfig/config.go | 5 + .../pluginconfig/request.go | 5 + 10 files changed, 27 insertions(+), 174 deletions(-) delete mode 100644 pkg/infra/httpclient/httpclientprovider/sigv4_middleware.go delete mode 100644 pkg/infra/httpclient/httpclientprovider/sigv4_middleware_test.go diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 4f70e46e2c..7ce2a910e6 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -609,8 +609,6 @@ playwright.config.ts @grafana/plugins-platform-frontend /pkg/services/supportbundles/ @grafana/identity-access-team # Grafana Operator Experience Team -/pkg/infra/httpclient/httpclientprovider/sigv4_middleware.go @grafana/grafana-operator-experience-squad -/pkg/infra/httpclient/httpclientprovider/sigv4_middleware_test.go @grafana/grafana-operator-experience-squad /pkg/services/caching/ @grafana/grafana-operator-experience-squad /pkg/services/featuremgmt/ @grafana/grafana-operator-experience-squad /pkg/services/cloudmigration/ @grafana/grafana-operator-experience-squad diff --git a/go.mod b/go.mod index 7633203f3e..31149a5745 100644 --- a/go.mod +++ b/go.mod @@ -52,7 +52,7 @@ require ( github.com/gorilla/websocket v1.5.0 // @grafana/grafana-app-platform-squad github.com/grafana/alerting v0.0.0-20240306130925-bc622368256d // @grafana/alerting-squad-backend github.com/grafana/cuetsy v0.1.11 // @grafana/grafana-as-code - github.com/grafana/grafana-aws-sdk v0.24.0 // @grafana/aws-datasources + github.com/grafana/grafana-aws-sdk v0.25.0 // @grafana/aws-datasources github.com/grafana/grafana-azure-sdk-go v1.12.0 // @grafana/partner-datasources github.com/grafana/grafana-plugin-sdk-go v0.215.0 // @grafana/plugins-platform-backend github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // @grafana/backend-platform diff --git a/go.sum b/go.sum index 34b4b50c0a..353117d54f 100644 --- a/go.sum +++ b/go.sum @@ -2186,8 +2186,8 @@ github.com/grafana/dskit v0.0.0-20240104111617-ea101a3b86eb h1:AWE6+kvtE18HP+lRW github.com/grafana/dskit v0.0.0-20240104111617-ea101a3b86eb/go.mod h1:kkWM4WUV230bNG3urVRWPBnSJHs64y/0RmWjftnnn0c= github.com/grafana/gofpdf v0.0.0-20231002120153-857cc45be447 h1:jxJJ5z0GxqhWFbQUsys3BHG8jnmniJ2Q74tXAG1NaDo= github.com/grafana/gofpdf v0.0.0-20231002120153-857cc45be447/go.mod h1:IxsY6mns6Q5sAnWcrptrgUrSglTZJXH/kXr9nbpb/9I= -github.com/grafana/grafana-aws-sdk v0.24.0 h1:0RKCJTeIkpEUvLCTjGOK1+jYZpaE2nJaGghGLvtUsFs= -github.com/grafana/grafana-aws-sdk v0.24.0/go.mod h1:3zghFF6edrxn0d6k6X9HpGZXDH+VfA+MwD2Pc/9X0ec= +github.com/grafana/grafana-aws-sdk v0.25.0 h1:XNi3iA/C/KPArmVbQfbwKQROaIotd38nCRjNE6P1UP0= +github.com/grafana/grafana-aws-sdk v0.25.0/go.mod h1:3zghFF6edrxn0d6k6X9HpGZXDH+VfA+MwD2Pc/9X0ec= github.com/grafana/grafana-azure-sdk-go v1.12.0 h1:q71M2QxMlBqRZOXc5mFAycJWuZqQ3hPTzVEo1r3CUTY= github.com/grafana/grafana-azure-sdk-go v1.12.0/go.mod h1:SAlwLdEuox4vw8ZaeQwnepYXnhznnQQdstJbcw8LH68= github.com/grafana/grafana-google-sdk-go v0.1.0 h1:LKGY8z2DSxKjYfr2flZsWgTRTZ6HGQbTqewE3JvRaNA= diff --git a/pkg/infra/httpclient/httpclientprovider/http_client_provider.go b/pkg/infra/httpclient/httpclientprovider/http_client_provider.go index f5c515a527..5447999a1f 100644 --- a/pkg/infra/httpclient/httpclientprovider/http_client_provider.go +++ b/pkg/infra/httpclient/httpclientprovider/http_client_provider.go @@ -4,6 +4,7 @@ import ( "net/http" "time" + awssdk "github.com/grafana/grafana-aws-sdk/pkg/sigv4" sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" "github.com/mwitkow/go-conntrack" @@ -32,7 +33,7 @@ func New(cfg *setting.Cfg, validator validations.PluginRequestValidator, tracer } if cfg.SigV4AuthEnabled { - middlewares = append(middlewares, SigV4Middleware(cfg.SigV4VerboseLogging)) + middlewares = append(middlewares, awssdk.SigV4Middleware(cfg.SigV4VerboseLogging)) } if httpLoggingEnabled(cfg.PluginSettings) { diff --git a/pkg/infra/httpclient/httpclientprovider/http_client_provider_test.go b/pkg/infra/httpclient/httpclientprovider/http_client_provider_test.go index dfbb09826c..652372a0de 100644 --- a/pkg/infra/httpclient/httpclientprovider/http_client_provider_test.go +++ b/pkg/infra/httpclient/httpclientprovider/http_client_provider_test.go @@ -5,6 +5,7 @@ import ( "github.com/grafana/grafana/pkg/services/validations" + awssdk "github.com/grafana/grafana-aws-sdk/pkg/sigv4" sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/setting" @@ -58,7 +59,7 @@ func TestHTTPClientProvider(t *testing.T) { require.Equal(t, sdkhttpclient.BasicAuthenticationMiddlewareName, o.Middlewares[4].(sdkhttpclient.MiddlewareName).MiddlewareName()) require.Equal(t, sdkhttpclient.CustomHeadersMiddlewareName, o.Middlewares[5].(sdkhttpclient.MiddlewareName).MiddlewareName()) require.Equal(t, sdkhttpclient.ResponseLimitMiddlewareName, o.Middlewares[6].(sdkhttpclient.MiddlewareName).MiddlewareName()) - require.Equal(t, SigV4MiddlewareName, o.Middlewares[8].(sdkhttpclient.MiddlewareName).MiddlewareName()) + require.Equal(t, awssdk.SigV4MiddlewareName, o.Middlewares[8].(sdkhttpclient.MiddlewareName).MiddlewareName()) }) t.Run("When creating new provider and http logging is enabled for one plugin, it should apply expected middleware", func(t *testing.T) { diff --git a/pkg/infra/httpclient/httpclientprovider/sigv4_middleware.go b/pkg/infra/httpclient/httpclientprovider/sigv4_middleware.go deleted file mode 100644 index 0a5de35d08..0000000000 --- a/pkg/infra/httpclient/httpclientprovider/sigv4_middleware.go +++ /dev/null @@ -1,47 +0,0 @@ -package httpclientprovider - -import ( - "fmt" - "net/http" - - "github.com/grafana/grafana-aws-sdk/pkg/sigv4" - "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" -) - -// SigV4MiddlewareName the middleware name used by SigV4Middleware. -const SigV4MiddlewareName = "sigv4" - -var newSigV4Func = sigv4.New - -// SigV4Middleware applies AWS Signature Version 4 request signing for the outgoing request. -func SigV4Middleware(verboseLogging bool) httpclient.Middleware { - return httpclient.NamedMiddlewareFunc(SigV4MiddlewareName, func(opts httpclient.Options, next http.RoundTripper) http.RoundTripper { - if opts.SigV4 == nil { - return next - } - - conf := &sigv4.Config{ - Service: opts.SigV4.Service, - AccessKey: opts.SigV4.AccessKey, - SecretKey: opts.SigV4.SecretKey, - Region: opts.SigV4.Region, - AssumeRoleARN: opts.SigV4.AssumeRoleARN, - AuthType: opts.SigV4.AuthType, - ExternalID: opts.SigV4.ExternalID, - Profile: opts.SigV4.Profile, - } - - rt, err := newSigV4Func(conf, next, sigv4.Opts{VerboseMode: verboseLogging}) - if err != nil { - return invalidSigV4Config(err) - } - - return rt - }) -} - -func invalidSigV4Config(err error) http.RoundTripper { - return httpclient.RoundTripperFunc(func(req *http.Request) (*http.Response, error) { - return nil, fmt.Errorf("invalid SigV4 configuration: %w", err) - }) -} diff --git a/pkg/infra/httpclient/httpclientprovider/sigv4_middleware_test.go b/pkg/infra/httpclient/httpclientprovider/sigv4_middleware_test.go deleted file mode 100644 index 1e021d42d7..0000000000 --- a/pkg/infra/httpclient/httpclientprovider/sigv4_middleware_test.go +++ /dev/null @@ -1,118 +0,0 @@ -package httpclientprovider - -import ( - "fmt" - "net/http" - "testing" - - "github.com/grafana/grafana-aws-sdk/pkg/sigv4" - "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" - "github.com/stretchr/testify/require" -) - -func TestSigV4Middleware(t *testing.T) { - t.Run("Without sigv4 options set should return next http.RoundTripper", func(t *testing.T) { - origSigV4Func := newSigV4Func - newSigV4Called := false - middlewareCalled := false - newSigV4Func = func(config *sigv4.Config, next http.RoundTripper, opts ...sigv4.Opts) (http.RoundTripper, error) { - newSigV4Called = true - return httpclient.RoundTripperFunc(func(r *http.Request) (*http.Response, error) { - middlewareCalled = true - return next.RoundTrip(r) - }), nil - } - t.Cleanup(func() { - newSigV4Func = origSigV4Func - }) - - ctx := &testContext{} - finalRoundTripper := ctx.createRoundTripper("finalrt") - mw := SigV4Middleware(false) - rt := mw.CreateMiddleware(httpclient.Options{}, finalRoundTripper) - require.NotNil(t, rt) - middlewareName, ok := mw.(httpclient.MiddlewareName) - require.True(t, ok) - require.Equal(t, SigV4MiddlewareName, middlewareName.MiddlewareName()) - - req, err := http.NewRequest(http.MethodGet, "http://", nil) - require.NoError(t, err) - res, err := rt.RoundTrip(req) - require.NoError(t, err) - require.NotNil(t, res) - if res.Body != nil { - require.NoError(t, res.Body.Close()) - } - require.Len(t, ctx.callChain, 1) - require.ElementsMatch(t, []string{"finalrt"}, ctx.callChain) - require.False(t, newSigV4Called) - require.False(t, middlewareCalled) - }) - - t.Run("With sigv4 options set should call sigv4 http.RoundTripper", func(t *testing.T) { - origSigV4Func := newSigV4Func - newSigV4Called := false - middlewareCalled := false - newSigV4Func = func(config *sigv4.Config, next http.RoundTripper, opts ...sigv4.Opts) (http.RoundTripper, error) { - newSigV4Called = true - return httpclient.RoundTripperFunc(func(r *http.Request) (*http.Response, error) { - middlewareCalled = true - return next.RoundTrip(r) - }), nil - } - t.Cleanup(func() { - newSigV4Func = origSigV4Func - }) - - ctx := &testContext{} - finalRoundTripper := ctx.createRoundTripper("final") - mw := SigV4Middleware(false) - rt := mw.CreateMiddleware(httpclient.Options{SigV4: &httpclient.SigV4Config{}}, finalRoundTripper) - require.NotNil(t, rt) - middlewareName, ok := mw.(httpclient.MiddlewareName) - require.True(t, ok) - require.Equal(t, SigV4MiddlewareName, middlewareName.MiddlewareName()) - - req, err := http.NewRequest(http.MethodGet, "http://", nil) - require.NoError(t, err) - res, err := rt.RoundTrip(req) - require.NoError(t, err) - require.NotNil(t, res) - if res.Body != nil { - require.NoError(t, res.Body.Close()) - } - require.Len(t, ctx.callChain, 1) - require.ElementsMatch(t, []string{"final"}, ctx.callChain) - - require.True(t, newSigV4Called) - require.True(t, middlewareCalled) - }) - - t.Run("With sigv4 error returned", func(t *testing.T) { - origSigV4Func := newSigV4Func - newSigV4Func = func(config *sigv4.Config, next http.RoundTripper, opts ...sigv4.Opts) (http.RoundTripper, error) { - return nil, fmt.Errorf("problem") - } - t.Cleanup(func() { - newSigV4Func = origSigV4Func - }) - - ctx := &testContext{} - finalRoundTripper := ctx.createRoundTripper("final") - mw := SigV4Middleware(false) - rt := mw.CreateMiddleware(httpclient.Options{SigV4: &httpclient.SigV4Config{}}, finalRoundTripper) - require.NotNil(t, rt) - middlewareName, ok := mw.(httpclient.MiddlewareName) - require.True(t, ok) - require.Equal(t, SigV4MiddlewareName, middlewareName.MiddlewareName()) - - req, err := http.NewRequest(http.MethodGet, "http://", nil) - require.NoError(t, err) - // response is nil - // nolint:bodyclose - res, err := rt.RoundTrip(req) - require.Error(t, err) - require.Nil(t, res) - require.Empty(t, ctx.callChain) - }) -} diff --git a/pkg/registry/apis/datasource/middleware.go b/pkg/registry/apis/datasource/middleware.go index fac100c8ca..95d8082320 100644 --- a/pkg/registry/apis/datasource/middleware.go +++ b/pkg/registry/apis/datasource/middleware.go @@ -3,13 +3,21 @@ package datasource import ( "context" + "github.com/grafana/grafana-aws-sdk/pkg/awsds" + "github.com/grafana/grafana-aws-sdk/pkg/sigv4" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" ) func contextualMiddlewares(ctx context.Context) context.Context { cfg := backend.GrafanaConfigFromContext(ctx) - m := httpclient.ResponseLimitMiddleware(cfg.ResponseLimit()) + responseLimitMiddleware := httpclient.ResponseLimitMiddleware(cfg.ResponseLimit()) + ctx = httpclient.WithContextualMiddleware(ctx, responseLimitMiddleware) - return httpclient.WithContextualMiddleware(ctx, m) + sigv4Settings := awsds.ReadSigV4Settings(ctx) + if sigv4Settings.Enabled { + ctx = httpclient.WithContextualMiddleware(ctx, sigv4.SigV4Middleware(sigv4Settings.VerboseLogging)) + } + + return ctx } diff --git a/pkg/services/pluginsintegration/pluginconfig/config.go b/pkg/services/pluginsintegration/pluginconfig/config.go index fae1820533..913643a730 100644 --- a/pkg/services/pluginsintegration/pluginconfig/config.go +++ b/pkg/services/pluginsintegration/pluginconfig/config.go @@ -75,6 +75,9 @@ type PluginInstanceCfg struct { SQLDatasourceMaxOpenConnsDefault int SQLDatasourceMaxIdleConnsDefault int SQLDatasourceMaxConnLifetimeDefault int + + SigV4AuthEnabled bool + SigV4VerboseLogging bool } // ProvidePluginInstanceConfig returns a new PluginInstanceCfg. @@ -120,6 +123,8 @@ func ProvidePluginInstanceConfig(cfg *setting.Cfg, settingProvider setting.Provi SQLDatasourceMaxIdleConnsDefault: cfg.SqlDatasourceMaxIdleConnsDefault, SQLDatasourceMaxConnLifetimeDefault: cfg.SqlDatasourceMaxConnLifetimeDefault, ResponseLimit: cfg.ResponseLimit, + SigV4AuthEnabled: cfg.SigV4AuthEnabled, + SigV4VerboseLogging: cfg.SigV4VerboseLogging, }, nil } diff --git a/pkg/services/pluginsintegration/pluginconfig/request.go b/pkg/services/pluginsintegration/pluginconfig/request.go index 0e969b8f40..da7f8a164c 100644 --- a/pkg/services/pluginsintegration/pluginconfig/request.go +++ b/pkg/services/pluginsintegration/pluginconfig/request.go @@ -151,5 +151,10 @@ func (s *RequestConfigProvider) PluginRequestConfig(ctx context.Context, pluginI m[backend.ResponseLimit] = strconv.FormatInt(s.cfg.ResponseLimit, 10) } + if s.cfg.SigV4AuthEnabled { + m[awsds.SigV4AuthEnabledEnvVarKeyName] = "true" + m[awsds.SigV4VerboseLoggingEnvVarKeyName] = strconv.FormatBool(s.cfg.SigV4VerboseLogging) + } + return m } From 1de4187a6e0ef3532f9f092327db4e724def8eca Mon Sep 17 00:00:00 2001 From: Jack Westbrook Date: Mon, 18 Mar 2024 09:48:19 +0100 Subject: [PATCH 0735/1406] Chore: Delete Input Datasource (#83163) * chore(input-datasource): delete bundled plugin for grafana 11 * chore(betterer): refresh results file * chore(yarn): run dedupe to clean up deps * chore(yarn): pin playwright to 1.41.2 to see if CI passes * chore(yarn): pin playwright to 1.42.1 --- .betterer.results | 6 - .../internal/input-datasource/.gitignore | 1 - .../internal/input-datasource/README.md | 3 - .../__mocks__/d3-interpolate.ts | 1 - .../internal/input-datasource/jest.config.js | 18 - .../internal/input-datasource/package.json | 36 - .../src/InputConfigEditor.tsx | 69 -- .../src/InputDatasource.test.ts | 58 - .../input-datasource/src/InputDatasource.ts | 124 -- .../input-datasource/src/InputQueryEditor.tsx | 86 -- .../input-datasource/src/img/input.svg | 14 - .../internal/input-datasource/src/module.ts | 10 - .../internal/input-datasource/src/plugin.json | 21 - .../input-datasource/src/testHelpers.ts | 27 - .../internal/input-datasource/src/types.ts | 11 - .../internal/input-datasource/src/utils.ts | 8 - .../internal/input-datasource/tsconfig.json | 17 - .../input-datasource/webpack.config.ts | 159 --- yarn.lock | 1023 +++++++---------- 19 files changed, 419 insertions(+), 1273 deletions(-) delete mode 100644 plugins-bundled/internal/input-datasource/.gitignore delete mode 100644 plugins-bundled/internal/input-datasource/README.md delete mode 100644 plugins-bundled/internal/input-datasource/__mocks__/d3-interpolate.ts delete mode 100644 plugins-bundled/internal/input-datasource/jest.config.js delete mode 100644 plugins-bundled/internal/input-datasource/package.json delete mode 100644 plugins-bundled/internal/input-datasource/src/InputConfigEditor.tsx delete mode 100644 plugins-bundled/internal/input-datasource/src/InputDatasource.test.ts delete mode 100644 plugins-bundled/internal/input-datasource/src/InputDatasource.ts delete mode 100644 plugins-bundled/internal/input-datasource/src/InputQueryEditor.tsx delete mode 100644 plugins-bundled/internal/input-datasource/src/img/input.svg delete mode 100644 plugins-bundled/internal/input-datasource/src/module.ts delete mode 100644 plugins-bundled/internal/input-datasource/src/plugin.json delete mode 100644 plugins-bundled/internal/input-datasource/src/testHelpers.ts delete mode 100644 plugins-bundled/internal/input-datasource/src/types.ts delete mode 100644 plugins-bundled/internal/input-datasource/src/utils.ts delete mode 100644 plugins-bundled/internal/input-datasource/tsconfig.json delete mode 100644 plugins-bundled/internal/input-datasource/webpack.config.ts diff --git a/.betterer.results b/.betterer.results index fe2cc870b0..ce4bc3b1ea 100644 --- a/.betterer.results +++ b/.betterer.results @@ -1133,9 +1133,6 @@ exports[`better eslint`] = { "packages/grafana-ui/src/utils/useAsyncDependency.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], - "plugins-bundled/internal/input-datasource/src/InputDatasource.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], "public/app/core/TableModel.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"], @@ -6293,9 +6290,6 @@ exports[`no gf-form usage`] = { [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"] ], - "plugins-bundled/internal/input-datasource/src/InputConfigEditor.tsx:5381": [ - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"] - ], "public/app/angular/components/code_editor/code_editor.ts:5381": [ [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"] ], diff --git a/plugins-bundled/internal/input-datasource/.gitignore b/plugins-bundled/internal/input-datasource/.gitignore deleted file mode 100644 index 61c3bc75a0..0000000000 --- a/plugins-bundled/internal/input-datasource/.gitignore +++ /dev/null @@ -1 +0,0 @@ -.yarn diff --git a/plugins-bundled/internal/input-datasource/README.md b/plugins-bundled/internal/input-datasource/README.md deleted file mode 100644 index 00761696d6..0000000000 --- a/plugins-bundled/internal/input-datasource/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Direct Input Data Source - Bundled Plugin - -This data source lets you define results directly in CSV. The values are stored either in a shared data source, or directly in panels. diff --git a/plugins-bundled/internal/input-datasource/__mocks__/d3-interpolate.ts b/plugins-bundled/internal/input-datasource/__mocks__/d3-interpolate.ts deleted file mode 100644 index f053ebf797..0000000000 --- a/plugins-bundled/internal/input-datasource/__mocks__/d3-interpolate.ts +++ /dev/null @@ -1 +0,0 @@ -module.exports = {}; diff --git a/plugins-bundled/internal/input-datasource/jest.config.js b/plugins-bundled/internal/input-datasource/jest.config.js deleted file mode 100644 index 88c2dfafc0..0000000000 --- a/plugins-bundled/internal/input-datasource/jest.config.js +++ /dev/null @@ -1,18 +0,0 @@ -module.exports = { - testEnvironment: 'jest-environment-jsdom', - preset: 'ts-jest', - extensionsToTreatAsEsm: ['.ts'], - transform: { - '^.+\\.(t|j)sx?$': [ - 'ts-jest', - { - useESM: true, - isolatedModules: true, - allowJs: true, - }, - ], - }, - moduleNameMapper: { - '^d3-interpolate$': '/__mocks__/d3-interpolate.ts', - }, -}; diff --git a/plugins-bundled/internal/input-datasource/package.json b/plugins-bundled/internal/input-datasource/package.json deleted file mode 100644 index d893ab319d..0000000000 --- a/plugins-bundled/internal/input-datasource/package.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "name": "@grafana-plugins/input-datasource", - "version": "11.0.0-pre", - "description": "Input Datasource", - "private": true, - "repository": { - "type": "git", - "url": "http://github.com/grafana/grafana.git" - }, - "scripts": { - "build": "yarn test && webpack -c webpack.config.ts --env production", - "dev": "webpack -w -c webpack.config.ts --env development", - "test": "jest -c jest.config.js" - }, - "author": "Grafana Labs", - "devDependencies": { - "@grafana/tsconfig": "^1.3.0-rc1", - "@types/jest": "26.0.15", - "@types/react": "18.0.28", - "copy-webpack-plugin": "11.0.0", - "eslint-webpack-plugin": "4.0.0", - "fork-ts-checker-webpack-plugin": "8.0.0", - "jest": "29.3.1", - "jest-environment-jsdom": "29.3.1", - "swc-loader": "0.2.3", - "ts-jest": "29.0.5", - "ts-node": "10.9.2", - "webpack": "5.76.0" - }, - "dependencies": { - "@grafana/data": "11.0.0-pre", - "@grafana/ui": "11.0.0-pre", - "react": "18.2.0", - "tslib": "2.5.0" - } -} diff --git a/plugins-bundled/internal/input-datasource/src/InputConfigEditor.tsx b/plugins-bundled/internal/input-datasource/src/InputConfigEditor.tsx deleted file mode 100644 index e79558da14..0000000000 --- a/plugins-bundled/internal/input-datasource/src/InputConfigEditor.tsx +++ /dev/null @@ -1,69 +0,0 @@ -// Libraries -import React, { PureComponent } from 'react'; - -// Types -import { DataSourcePluginOptionsEditorProps, DataFrame, MutableDataFrame } from '@grafana/data'; -import { TableInputCSV } from '@grafana/ui'; - -import { InputOptions } from './types'; -import { dataFrameToCSV } from './utils'; - -interface Props extends DataSourcePluginOptionsEditorProps {} - -interface State { - text: string; -} - -export class InputConfigEditor extends PureComponent { - state = { - text: '', - }; - - componentDidMount() { - const { options } = this.props; - if (options.jsonData.data) { - const text = dataFrameToCSV(options.jsonData.data); - this.setState({ text }); - } - } - - onSeriesParsed = (data: DataFrame[], text: string) => { - const { options, onOptionsChange } = this.props; - if (!data) { - data = [new MutableDataFrame()]; - } - // data is a property on 'jsonData' - const jsonData = { - ...options.jsonData, - data, - }; - - onOptionsChange({ - ...options, - jsonData, - }); - this.setState({ text }); - }; - - render() { - const { text } = this.state; - return ( -
-
-

Shared Data:

- Enter CSV - -
- -
- This data is stored in the datasource json and is returned to every user in the initial request for any - datasource. This is an appropriate place to enter a few values. Large datasets will perform better in other - datasources. -
-
- NOTE: Changes to this data will only be reflected after a browser refresh. -
-
- ); - } -} diff --git a/plugins-bundled/internal/input-datasource/src/InputDatasource.test.ts b/plugins-bundled/internal/input-datasource/src/InputDatasource.test.ts deleted file mode 100644 index e28de87908..0000000000 --- a/plugins-bundled/internal/input-datasource/src/InputDatasource.test.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { - DataFrame, - DataFrameDTO, - DataSourceInstanceSettings, - MutableDataFrame, - PluginMeta, - readCSV, -} from '@grafana/data'; - -import InputDatasource, { describeDataFrame } from './InputDatasource'; -import { getQueryOptions } from './testHelpers'; -import { InputOptions, InputQuery } from './types'; - -describe('InputDatasource', () => { - const data = readCSV('a,b,c\n1,2,3\n4,5,6'); - const instanceSettings: DataSourceInstanceSettings = { - id: 1, - uid: 'xxx', - type: 'x', - name: 'xxx', - meta: {} as PluginMeta, - access: 'proxy', - readOnly: false, - jsonData: { - data, - }, - }; - - describe('when querying', () => { - test('should return the saved data with a query', () => { - const ds = new InputDatasource(instanceSettings); - const options = getQueryOptions({ - targets: [{ refId: 'Z' }], - }); - - return ds.query(options).then((rsp) => { - expect(rsp.data.length).toBe(1); - - const series: DataFrame = rsp.data[0]; - expect(series.refId).toBe('Z'); - expect(series.fields[0].values).toEqual(data[0].fields[0].values); - }); - }); - }); - - test('DataFrame descriptions', () => { - expect(describeDataFrame([])).toEqual(''); - expect(describeDataFrame(null as unknown as Array)).toEqual(''); - expect( - describeDataFrame([ - new MutableDataFrame({ - name: 'x', - fields: [{ name: 'a' }], - }), - ]) - ).toEqual('1 Fields, 0 Rows'); - }); -}); diff --git a/plugins-bundled/internal/input-datasource/src/InputDatasource.ts b/plugins-bundled/internal/input-datasource/src/InputDatasource.ts deleted file mode 100644 index 455452693e..0000000000 --- a/plugins-bundled/internal/input-datasource/src/InputDatasource.ts +++ /dev/null @@ -1,124 +0,0 @@ -// Types -import { - DataQueryRequest, - DataQueryResponse, - TestDataSourceResponse, - DataSourceApi, - DataSourceInstanceSettings, - MetricFindValue, - DataFrame, - DataFrameDTO, - toDataFrame, -} from '@grafana/data'; - -import { InputQuery, InputOptions } from './types'; - -export class InputDatasource extends DataSourceApi { - data: DataFrame[] = []; - - constructor(instanceSettings: DataSourceInstanceSettings) { - super(instanceSettings); - - if (instanceSettings.jsonData.data) { - this.data = instanceSettings.jsonData.data.map((v) => toDataFrame(v)); - } - } - - /** - * Convert a query to a simple text string - */ - getQueryDisplayText(query: InputQuery): string { - if (query.data) { - return 'Panel Data: ' + describeDataFrame(query.data); - } - return `Shared Data From: ${this.name} (${describeDataFrame(this.data)})`; - } - - metricFindQuery(query: string, options?: any): Promise { - return new Promise((resolve, reject) => { - const names = []; - for (const series of this.data) { - for (const field of series.fields) { - // TODO, match query/options? - names.push({ - text: field.name, - }); - } - } - resolve(names); - }); - } - - query(options: DataQueryRequest): Promise { - const results: DataFrame[] = []; - for (const query of options.targets) { - if (query.hide) { - continue; - } - let data = this.data; - if (query.data) { - data = query.data.map((v) => toDataFrame(v)); - } - for (let i = 0; i < data.length; i++) { - results.push({ - ...data[i], - refId: query.refId, - }); - } - } - return Promise.resolve({ data: results }); - } - - testDatasource(): Promise { - return new Promise((resolve, reject) => { - let rowCount = 0; - let info = `${this.data.length} Series:`; - for (const series of this.data) { - const length = series.length; - info += ` [${series.fields.length} Fields, ${length} Rows]`; - rowCount += length; - } - - if (rowCount > 0) { - resolve({ - status: 'success', - message: info, - }); - } - reject({ - status: 'error', - message: 'No Data Entered', - }); - }); - } -} - -function getLength(data?: DataFrameDTO | DataFrame) { - if (!data || !data.fields || !data.fields.length) { - return 0; - } - if ('length' in data) { - return data.length; - } - return data.fields[0].values!.length; -} - -export function describeDataFrame(data: Array): string { - if (!data || !data.length) { - return ''; - } - if (data.length > 1) { - const count = data.reduce((acc, series) => { - return acc + getLength(series); - }, 0); - return `${data.length} Series, ${count} Rows`; - } - const series = data[0]; - if (!series.fields) { - return 'Missing Fields'; - } - const length = getLength(series); - return `${series.fields.length} Fields, ${length} Rows`; -} - -export default InputDatasource; diff --git a/plugins-bundled/internal/input-datasource/src/InputQueryEditor.tsx b/plugins-bundled/internal/input-datasource/src/InputQueryEditor.tsx deleted file mode 100644 index b20520629f..0000000000 --- a/plugins-bundled/internal/input-datasource/src/InputQueryEditor.tsx +++ /dev/null @@ -1,86 +0,0 @@ -// Libraries -import React, { PureComponent } from 'react'; - -// Types -import { DataFrame, toCSV, SelectableValue, MutableDataFrame, QueryEditorProps } from '@grafana/data'; -import { Select, TableInputCSV, LinkButton, Icon, InlineField } from '@grafana/ui'; - -import { InputDatasource, describeDataFrame } from './InputDatasource'; -import { InputQuery, InputOptions } from './types'; -import { dataFrameToCSV } from './utils'; - -type Props = QueryEditorProps; - -const options = [ - { value: 'panel', label: 'Panel', description: 'Save data in the panel configuration.' }, - { value: 'shared', label: 'Shared', description: 'Save data in the shared datasource object.' }, -]; - -interface State { - text: string; -} - -export class InputQueryEditor extends PureComponent { - state = { - text: '', - }; - - onComponentDidMount() { - const { query } = this.props; - const text = dataFrameToCSV(query.data); - this.setState({ text }); - } - - onSourceChange = (item: SelectableValue) => { - const { datasource, query, onChange, onRunQuery } = this.props; - let data: DataFrame[] | undefined = undefined; - if (item.value === 'panel') { - if (query.data) { - return; - } - data = [...datasource.data]; - if (!data) { - data = [new MutableDataFrame()]; - } - this.setState({ text: toCSV(data) }); - } - onChange({ ...query, data }); - onRunQuery(); - }; - - onSeriesParsed = (data: DataFrame[], text: string) => { - const { query, onChange, onRunQuery } = this.props; - this.setState({ text }); - if (!data) { - data = [new MutableDataFrame()]; - } - onChange({ ...query, data }); - onRunQuery(); - }; - - render() { - const { datasource, query } = this.props; - const { uid, name } = datasource; - const { text } = this.state; - - const selected = query.data ? options[0] : options[1]; - return ( -
- - <> - onSave(event.target.value)} />; - }, -})); - -jest.mock('../language_provider', () => { - return jest.fn().mockImplementation(() => { - return { getOptionsV1 }; - }); -}); - -jest.mock('@grafana/runtime', () => ({ - ...jest.requireActual('@grafana/runtime'), - getTemplateSrv: () => ({ - replace: jest.fn(), - containsTemplate: (val: string): boolean => { - return val.includes('$'); - }, - }), -})); - -let mockQuery = { - refId: 'A', - queryType: 'nativeSearch', - key: 'Q-595a9bbc-2a25-49a7-9249-a52a0a475d83-0', - serviceName: 'driver', - spanName: 'customer', -} as TempoQuery; - -describe('NativeSearch', () => { - let user: ReturnType; - - beforeEach(() => { - jest.useFakeTimers(); - // Need to use delay: null here to work with fakeTimers - // see https://github.com/testing-library/user-event/issues/833 - user = userEvent.setup({ delay: null }); - }); - - afterEach(() => { - jest.useRealTimers(); - }); - - it('should show loader when there is a delay', async () => { - render( - - ); - - const select = screen.getByRole('combobox', { name: 'select-service-name' }); - - await user.click(select); - const loader = screen.getByText('Loading options...'); - - expect(loader).toBeInTheDocument(); - - jest.advanceTimersByTime(1000); - - await waitFor(() => expect(screen.queryByText('Loading options...')).not.toBeInTheDocument()); - }); - - it('should call the `onChange` function on click of the Input', async () => { - const promise = Promise.resolve(); - const handleOnChange = jest.fn(() => promise); - const fakeOptionChoice = { - key: 'Q-595a9bbc-2a25-49a7-9249-a52a0a475d83-0', - queryType: 'nativeSearch', - refId: 'A', - serviceName: 'driver', - spanName: 'customer', - }; - - render( - {}} - /> - ); - - const select = await screen.findByRole('combobox', { name: 'select-service-name' }); - - expect(select).toBeInTheDocument(); - await user.click(select); - jest.advanceTimersByTime(1000); - - await user.type(select, 'd'); - const driverOption = await screen.findByText('driver'); - await user.click(driverOption); - - expect(handleOnChange).toHaveBeenCalledWith(fakeOptionChoice); - }); - - it('should filter the span dropdown when user types a search value', async () => { - render( - {}} onRunQuery={() => {}} /> - ); - - const select = await screen.findByRole('combobox', { name: 'select-service-name' }); - await user.click(select); - jest.advanceTimersByTime(1000); - expect(select).toBeInTheDocument(); - - await user.type(select, 'd'); - let option = await screen.findByText('driver'); - expect(option).toBeDefined(); - - await user.type(select, 'a'); - option = await screen.findByText('Hit enter to add'); - expect(option).toBeDefined(); - }); - - it('should add variable to select menu options', async () => { - mockQuery = { - ...mockQuery, - refId: '121314', - serviceName: '$service', - spanName: '$span', - }; - - render( - {}} onRunQuery={() => {}} /> - ); - - const asyncServiceSelect = screen.getByRole('combobox', { name: 'select-service-name' }); - expect(asyncServiceSelect).toBeInTheDocument(); - await user.click(asyncServiceSelect); - jest.advanceTimersByTime(3000); - - await user.type(asyncServiceSelect, '$'); - const serviceOption = await screen.findByText('$service'); - expect(serviceOption).toBeDefined(); - - const asyncSpanSelect = screen.getByRole('combobox', { name: 'select-span-name' }); - expect(asyncSpanSelect).toBeInTheDocument(); - await user.click(asyncSpanSelect); - jest.advanceTimersByTime(3000); - - await user.type(asyncSpanSelect, '$'); - const operationOption = await screen.findByText('$span'); - expect(operationOption).toBeDefined(); - }); -}); diff --git a/public/app/plugins/datasource/tempo/NativeSearch/NativeSearch.tsx b/public/app/plugins/datasource/tempo/NativeSearch/NativeSearch.tsx deleted file mode 100644 index 1653cc04de..0000000000 --- a/public/app/plugins/datasource/tempo/NativeSearch/NativeSearch.tsx +++ /dev/null @@ -1,276 +0,0 @@ -import { css } from '@emotion/css'; -import React, { useCallback, useState, useEffect, useMemo } from 'react'; - -import { GrafanaTheme2, isValidGoDuration, SelectableValue, toOption } from '@grafana/data'; -import { TemporaryAlert } from '@grafana/o11y-ds-frontend'; -import { FetchError, getTemplateSrv, isFetchError, TemplateSrv } from '@grafana/runtime'; -import { InlineFieldRow, InlineField, Input, Alert, useStyles2, fuzzyMatch, Select } from '@grafana/ui'; - -import { DEFAULT_LIMIT, TempoDatasource } from '../datasource'; -import TempoLanguageProvider from '../language_provider'; -import { TempoQuery } from '../types'; - -import { TagsField } from './TagsField/TagsField'; - -interface Props { - datasource: TempoDatasource; - query: TempoQuery; - onChange: (value: TempoQuery) => void; - onBlur?: () => void; - onRunQuery: () => void; -} - -const durationPlaceholder = 'e.g. 1.2s, 100ms'; - -const NativeSearch = ({ datasource, query, onChange, onBlur, onRunQuery }: Props) => { - const styles = useStyles2(getStyles); - const [alertText, setAlertText] = useState(); - const languageProvider = useMemo(() => new TempoLanguageProvider(datasource), [datasource]); - const [serviceOptions, setServiceOptions] = useState>>(); - const [spanOptions, setSpanOptions] = useState>>(); - const [error, setError] = useState(null); - const [inputErrors, setInputErrors] = useState<{ [key: string]: boolean }>({}); - const [isLoading, setIsLoading] = useState<{ - serviceName: boolean; - spanName: boolean; - }>({ - serviceName: false, - spanName: false, - }); - - const loadOptions = useCallback( - async (name: string, query = '') => { - const lpName = name === 'serviceName' ? 'service.name' : 'name'; - setIsLoading((prevValue) => ({ ...prevValue, [name]: true })); - - try { - const options = await languageProvider.getOptionsV1(lpName); - const filteredOptions = options.filter((item) => (item.value ? fuzzyMatch(item.value, query).found : false)); - setAlertText(undefined); - setError(null); - return filteredOptions; - } catch (error) { - if (isFetchError(error) && error?.status === 404) { - setError(error); - } else if (error instanceof Error) { - setAlertText(`Error: ${error.message}`); - } - return []; - } finally { - setIsLoading((prevValue) => ({ ...prevValue, [name]: false })); - } - }, - [languageProvider, setAlertText] - ); - - useEffect(() => { - const fetchOptions = async () => { - try { - const [services, spans] = await Promise.all([loadOptions('serviceName'), loadOptions('spanName')]); - if (query.serviceName && getTemplateSrv().containsTemplate(query.serviceName)) { - services.push(toOption(query.serviceName)); - } - setServiceOptions(services); - if (query.spanName && getTemplateSrv().containsTemplate(query.spanName)) { - spans.push(toOption(query.spanName)); - } - setSpanOptions(spans); - setAlertText(undefined); - setError(null); - } catch (error) { - // Display message if Tempo is connected but search 404's - if (isFetchError(error) && error?.status === 404) { - setError(error); - } else if (error instanceof Error) { - setAlertText(`Error: ${error.message}`); - } - } - }; - fetchOptions(); - }, [languageProvider, loadOptions, query.serviceName, query.spanName, setAlertText]); - - const onKeyDown = (keyEvent: React.KeyboardEvent) => { - if (keyEvent.key === 'Enter' && (keyEvent.shiftKey || keyEvent.ctrlKey)) { - onRunQuery(); - } - }; - - const handleOnChange = useCallback( - (value: string) => { - onChange({ - ...query, - search: value, - }); - }, - [onChange, query] - ); - - const templateSrv: TemplateSrv = getTemplateSrv(); - - return ( - <> -
- - This query type has been deprecated and will be removed in Grafana v10.3. Please migrate to another Tempo - query type. - - - - { - loadOptions('spanName'); - }} - isLoading={isLoading.spanName} - value={spanOptions?.find((v) => v?.value === query.spanName) || query.spanName} - onChange={(v) => { - onChange({ - ...query, - spanName: v?.value, - }); - }} - placeholder="Select a span" - isClearable - onKeyDown={onKeyDown} - aria-label={'select-span-name'} - allowCustomValue={true} - /> - - - - - - - - - - { - const templatedMinDuration = templateSrv.replace(query.minDuration ?? ''); - if (query.minDuration && !isValidGoDuration(templatedMinDuration)) { - setInputErrors({ ...inputErrors, minDuration: true }); - } else { - setInputErrors({ ...inputErrors, minDuration: false }); - } - }} - onChange={(v) => - onChange({ - ...query, - minDuration: v.currentTarget.value, - }) - } - onKeyDown={onKeyDown} - /> - - - - - { - const templatedMaxDuration = templateSrv.replace(query.maxDuration ?? ''); - if (query.maxDuration && !isValidGoDuration(templatedMaxDuration)) { - setInputErrors({ ...inputErrors, maxDuration: true }); - } else { - setInputErrors({ ...inputErrors, maxDuration: false }); - } - }} - onChange={(v) => - onChange({ - ...query, - maxDuration: v.currentTarget.value, - }) - } - onKeyDown={onKeyDown} - /> - - - - - { - let limit = v.currentTarget.value ? parseInt(v.currentTarget.value, 10) : undefined; - if (limit && (!Number.isInteger(limit) || limit <= 0)) { - setInputErrors({ ...inputErrors, limit: true }); - } else { - setInputErrors({ ...inputErrors, limit: false }); - } - - onChange({ - ...query, - limit: v.currentTarget.value ? parseInt(v.currentTarget.value, 10) : undefined, - }); - }} - onKeyDown={onKeyDown} - /> - - -
- {error ? ( - - Please ensure that Tempo is configured with search enabled. If you would like to hide this tab, you can - configure it in the datasource settings. - - ) : null} - {alertText && } - - ); -}; - -export default NativeSearch; - -const getStyles = (theme: GrafanaTheme2) => ({ - container: css({ - maxWidth: '500px', - }), - alert: css({ - maxWidth: '75ch', - marginTop: theme.spacing(2), - }), -}); diff --git a/public/app/plugins/datasource/tempo/NativeSearch/TagsField/TagsField.tsx b/public/app/plugins/datasource/tempo/NativeSearch/TagsField/TagsField.tsx deleted file mode 100644 index 9acc12b289..0000000000 --- a/public/app/plugins/datasource/tempo/NativeSearch/TagsField/TagsField.tsx +++ /dev/null @@ -1,186 +0,0 @@ -import { css } from '@emotion/css'; -import React, { useEffect, useRef, useState } from 'react'; - -import { GrafanaTheme2 } from '@grafana/data'; -import { TemporaryAlert } from '@grafana/o11y-ds-frontend'; -import { CodeEditor, Monaco, monacoTypes, useTheme2 } from '@grafana/ui'; - -import { TempoDatasource } from '../../datasource'; - -import { CompletionProvider } from './autocomplete'; -import { languageDefinition } from './syntax'; - -interface Props { - placeholder: string; - value: string; - onChange: (val: string) => void; - onBlur?: () => void; - datasource: TempoDatasource; -} - -export function TagsField(props: Props) { - const [alertText, setAlertText] = useState(); - const { onChange, onBlur, placeholder } = props; - const setupAutocompleteFn = useAutocomplete(props.datasource, setAlertText); - const theme = useTheme2(); - const styles = getStyles(theme, placeholder); - - return ( - <> - { - setupAutocompleteFn(editor, monaco); - setupPlaceholder(editor, monaco, styles); - setupAutoSize(editor); - }} - /> - {alertText && } - - ); -} - -function setupPlaceholder(editor: monacoTypes.editor.IStandaloneCodeEditor, monaco: Monaco, styles: EditorStyles) { - const placeholderDecorators = [ - { - range: new monaco.Range(1, 1, 1, 1), - options: { - className: styles.placeholder, // The placeholder text is in styles.placeholder - isWholeLine: true, - }, - }, - ]; - - let decorators: string[] = []; - - const checkDecorators = (): void => { - const model = editor.getModel(); - - if (!model) { - return; - } - - const newDecorators = model.getValueLength() === 0 ? placeholderDecorators : []; - decorators = model.deltaDecorations(decorators, newDecorators); - }; - - checkDecorators(); - editor.onDidChangeModelContent(checkDecorators); -} - -function setupAutoSize(editor: monacoTypes.editor.IStandaloneCodeEditor) { - const container = editor.getDomNode(); - const updateHeight = () => { - if (container) { - const contentHeight = Math.min(1000, editor.getContentHeight()); - const width = parseInt(container.style.width, 10); - container.style.width = `${width}px`; - container.style.height = `${contentHeight}px`; - editor.layout({ width, height: contentHeight }); - } - }; - editor.onDidContentSizeChange(updateHeight); - updateHeight(); -} - -/** - * Hook that returns function that will set up monaco autocomplete for the label selector - * @param datasource the Tempo datasource instance - * @param setAlertText setter for the alert text - */ -function useAutocomplete(datasource: TempoDatasource, setAlertText: (text?: string) => void) { - // We need the provider ref so we can pass it the label/values data later. This is because we run the call for the - // values here but there is additional setup needed for the provider later on. We could run the getSeries() in the - // returned function but that is run after the monaco is mounted so would delay the request a bit when it does not - // need to. - const providerRef = useRef( - new CompletionProvider({ languageProvider: datasource.languageProvider }) - ); - - useEffect(() => { - const fetchTags = async () => { - try { - await datasource.languageProvider.start(); - setAlertText(undefined); - } catch (error) { - if (error instanceof Error) { - setAlertText(`Error: ${error.message}`); - } - } - }; - fetchTags(); - }, [datasource, setAlertText]); - - const autocompleteDisposeFun = useRef<(() => void) | null>(null); - useEffect(() => { - // when we unmount, we unregister the autocomplete-function, if it was registered - return () => { - autocompleteDisposeFun.current?.(); - }; - }, []); - - // This should be run in monaco onEditorDidMount - return (editor: monacoTypes.editor.IStandaloneCodeEditor, monaco: Monaco) => { - providerRef.current.editor = editor; - providerRef.current.monaco = monaco; - - const { dispose } = monaco.languages.registerCompletionItemProvider(langId, providerRef.current); - autocompleteDisposeFun.current = dispose; - }; -} - -// we must only run the setup code once -let setupDone = false; -const langId = 'tagsfield'; - -function ensureTraceQL(monaco: Monaco) { - if (!setupDone) { - setupDone = true; - const { aliases, extensions, mimetypes, def } = languageDefinition; - monaco.languages.register({ id: langId, aliases, extensions, mimetypes }); - monaco.languages.setMonarchTokensProvider(langId, def.language); - monaco.languages.setLanguageConfiguration(langId, def.languageConfiguration); - } -} - -interface EditorStyles { - placeholder: string; - queryField: string; -} - -const getStyles = (theme: GrafanaTheme2, placeholder: string): EditorStyles => { - return { - queryField: css({ - borderRadius: theme.shape.radius.default, - border: `1px solid ${theme.components.input.borderColor}`, - flex: 1, - }), - placeholder: css({ - '::after': { - content: `'${placeholder}'`, - fontFamily: theme.typography.fontFamilyMonospace, - opacity: 0.3, - }, - }), - }; -}; diff --git a/public/app/plugins/datasource/tempo/NativeSearch/TagsField/autocomplete.ts b/public/app/plugins/datasource/tempo/NativeSearch/TagsField/autocomplete.ts deleted file mode 100644 index 0b8be23878..0000000000 --- a/public/app/plugins/datasource/tempo/NativeSearch/TagsField/autocomplete.ts +++ /dev/null @@ -1,245 +0,0 @@ -import { SelectableValue } from '@grafana/data'; -import type { Monaco, monacoTypes } from '@grafana/ui'; - -import TempoLanguageProvider from '../../language_provider'; - -interface Props { - languageProvider: TempoLanguageProvider; -} - -/** - * Class that implements CompletionItemProvider interface and allows us to provide suggestion for the Monaco - * autocomplete system. - */ -export class CompletionProvider implements monacoTypes.languages.CompletionItemProvider { - languageProvider: TempoLanguageProvider; - - constructor(props: Props) { - this.languageProvider = props.languageProvider; - } - - triggerCharacters = ['=', ' ']; - - // We set these directly and ae required for the provider to function. - monaco: Monaco | undefined; - editor: monacoTypes.editor.IStandaloneCodeEditor | undefined; - - private cachedValues: { [key: string]: Array> } = {}; - - provideCompletionItems( - model: monacoTypes.editor.ITextModel, - position: monacoTypes.Position - ): monacoTypes.languages.ProviderResult { - // Should not happen, this should not be called before it is initialized - if (!(this.monaco && this.editor)) { - throw new Error('provideCompletionItems called before CompletionProvider was initialized'); - } - - // if the model-id does not match, then this call is from a different editor-instance, - // not "our instance", so return nothing - if (this.editor.getModel()?.id !== model.id) { - return { suggestions: [] }; - } - - const { range, offset } = getRangeAndOffset(this.monaco, model, position); - const situation = this.getSituation(model.getValue(), offset); - const completionItems = this.getCompletions(situation); - - return completionItems.then((items) => { - // monaco by-default alphabetically orders the items. - // to stop it, we use a number-as-string sortkey, - // so that monaco keeps the order we use - const maxIndexDigits = items.length.toString().length; - const suggestions: monacoTypes.languages.CompletionItem[] = items.map((item, index) => { - const suggestion: monacoTypes.languages.CompletionItem = { - kind: getMonacoCompletionItemKind(item.type, this.monaco!), - label: item.label, - insertText: item.insertText, - sortText: index.toString().padStart(maxIndexDigits, '0'), // to force the order we have - range, - }; - return suggestion; - }); - return { suggestions }; - }); - } - - private async getTagValues(tagName: string): Promise>> { - let tagValues: Array>; - - if (this.cachedValues.hasOwnProperty(tagName)) { - tagValues = this.cachedValues[tagName]; - } else { - tagValues = await this.languageProvider.getOptionsV1(tagName); - this.cachedValues[tagName] = tagValues; - } - return tagValues; - } - - /** - * Get suggestion based on the situation we are in like whether we should suggest tag names or values. - * @param situation - * @private - */ - private async getCompletions(situation: Situation): Promise { - switch (situation.type) { - // Not really sure what would make sense to suggest in this case so just leave it - case 'UNKNOWN': { - return []; - } - case 'EMPTY': { - return this.getTagsCompletions(); - } - case 'IN_NAME': - return this.getTagsCompletions(); - case 'IN_VALUE': - const tagValues = await this.getTagValues(situation.tagName); - const items: Completion[] = []; - - const getInsertionText = (val: SelectableValue): string => `"${val.label}"`; - - tagValues.forEach((val) => { - if (val?.label) { - items.push({ - label: val.label, - insertText: getInsertionText(val), - type: 'TAG_VALUE', - }); - } - }); - return items; - default: - throw new Error(`Unexpected situation ${situation}`); - } - } - - private getTagsCompletions(): Completion[] { - const tags = this.languageProvider.getAutocompleteTags(); - return tags - .sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'accent' })) - .map((key) => ({ - label: key, - insertText: key, - type: 'TAG_NAME', - })); - } - - /** - * Figure out where is the cursor and what kind of suggestions are appropriate. - * @param text - * @param offset - */ - private getSituation(text: string, offset: number): Situation { - if (text === '' || offset === 0 || text[text.length - 1] === ' ') { - return { - type: 'EMPTY', - }; - } - - const textUntilCaret = text.substring(0, offset); - - const regex = /(?[^= ]+)(?=)?(?([^ "]+)|"([^"]*)")?/; - const matches = textUntilCaret.match(new RegExp(regex, 'g')); - - if (matches?.length) { - const last = matches[matches.length - 1]; - const lastMatched = last.match(regex); - if (lastMatched) { - const key = lastMatched.groups?.key; - const equals = lastMatched.groups?.equals; - - if (!key) { - return { - type: 'EMPTY', - }; - } - - if (!equals) { - return { - type: 'IN_NAME', - }; - } - - return { - type: 'IN_VALUE', - tagName: key, - }; - } - } - - return { - type: 'EMPTY', - }; - } -} - -/** - * Get item kind which is used for icon next to the suggestion. - * @param type - * @param monaco - */ -function getMonacoCompletionItemKind(type: CompletionType, monaco: Monaco): monacoTypes.languages.CompletionItemKind { - switch (type) { - case 'TAG_NAME': - return monaco.languages.CompletionItemKind.Enum; - case 'KEYWORD': - return monaco.languages.CompletionItemKind.Keyword; - case 'OPERATOR': - return monaco.languages.CompletionItemKind.Operator; - case 'TAG_VALUE': - return monaco.languages.CompletionItemKind.EnumMember; - case 'SCOPE': - return monaco.languages.CompletionItemKind.Class; - default: - throw new Error(`Unexpected CompletionType: ${type}`); - } -} - -export type CompletionType = 'TAG_NAME' | 'TAG_VALUE' | 'KEYWORD' | 'OPERATOR' | 'SCOPE'; -type Completion = { - type: CompletionType; - label: string; - insertText: string; -}; - -export type Tag = { - name: string; - value: string; -}; - -export type Situation = - | { - type: 'UNKNOWN'; - } - | { - type: 'EMPTY'; - } - | { - type: 'IN_NAME'; - } - | { - type: 'IN_VALUE'; - tagName: string; - }; - -function getRangeAndOffset(monaco: Monaco, model: monacoTypes.editor.ITextModel, position: monacoTypes.Position) { - const word = model.getWordAtPosition(position); - const range = - word != null - ? monaco.Range.lift({ - startLineNumber: position.lineNumber, - endLineNumber: position.lineNumber, - startColumn: word.startColumn, - endColumn: word.endColumn, - }) - : monaco.Range.fromPositions(position); - - // documentation says `position` will be "adjusted" in `getOffsetAt` so we clone it here just for sure. - const positionClone = { - column: position.column, - lineNumber: position.lineNumber, - }; - - const offset = model.getOffsetAt(positionClone); - return { offset, range }; -} diff --git a/public/app/plugins/datasource/tempo/NativeSearch/TagsField/syntax.ts b/public/app/plugins/datasource/tempo/NativeSearch/TagsField/syntax.ts deleted file mode 100644 index 6dd942b2f1..0000000000 --- a/public/app/plugins/datasource/tempo/NativeSearch/TagsField/syntax.ts +++ /dev/null @@ -1,124 +0,0 @@ -import type { languages } from 'monaco-editor'; - -export const languageConfiguration: languages.LanguageConfiguration = { - // the default separators except `@$` - wordPattern: /(-?\d*\.\d\w*)|([^`~!#%^&*()\-=+\[{\]}\\|;:'",.<>\/?\s]+)/g, - brackets: [ - ['{', '}'], - ['(', ')'], - ], - autoClosingPairs: [ - { open: '{', close: '}' }, - { open: '(', close: ')' }, - { open: '"', close: '"' }, - { open: "'", close: "'" }, - ], - surroundingPairs: [ - { open: '{', close: '}' }, - { open: '(', close: ')' }, - { open: '"', close: '"' }, - { open: "'", close: "'" }, - ], - folding: {}, -}; - -const operators = ['=']; - -export const language: languages.IMonarchLanguage = { - ignoreCase: false, - defaultToken: '', - tokenPostfix: '.tagsfield', - - operators, - - // we include these common regular expressions - symbols: /[=>|<|>=|<=|=~|!~))/, 'tag'], - - // all keywords have the same color - [ - /[a-zA-Z_.]\w*/, - { - cases: { - '@default': 'identifier', - }, - }, - ], - - // strings - [/"([^"\\]|\\.)*$/, 'string.invalid'], // non-teminated string - [/'([^'\\]|\\.)*$/, 'string.invalid'], // non-teminated string - [/"/, 'string', '@string_double'], - [/'/, 'string', '@string_single'], - - // whitespace - { include: '@whitespace' }, - - // delimiters and operators - [/[{}()\[\]]/, '@brackets'], - [/[<>](?!@symbols)/, '@brackets'], - [ - /@symbols/, - { - cases: { - '@operators': 'delimiter', - '@default': '', - }, - }, - ], - - // numbers - [/\d+/, 'number'], - [/\d*\d+[eE]([\-+]?\d+)?(@floatsuffix)/, 'number.float'], - [/\d*\.\d+([eE][\-+]?\d+)?(@floatsuffix)/, 'number.float'], - [/0[xX][0-9a-fA-F']*[0-9a-fA-F](@integersuffix)/, 'number.hex'], - [/0[0-7']*[0-7](@integersuffix)/, 'number.octal'], - [/0[bB][0-1']*[0-1](@integersuffix)/, 'number.binary'], - [/\d[\d']*\d(@integersuffix)/, 'number'], - [/\d(@integersuffix)/, 'number'], - ], - - string_double: [ - [/[^\\"]+/, 'string'], - [/@escapes/, 'string.escape'], - [/\\./, 'string.escape.invalid'], - [/"/, 'string', '@pop'], - ], - - string_single: [ - [/[^\\']+/, 'string'], - [/@escapes/, 'string.escape'], - [/\\./, 'string.escape.invalid'], - [/'/, 'string', '@pop'], - ], - - clauses: [ - [/[^(,)]/, 'tag'], - [/\)/, 'identifier', '@pop'], - ], - - whitespace: [[/[ \t\r\n]+/, 'white']], - }, -}; - -export const languageDefinition = { - id: 'tagsfield', - extensions: ['.tagsfield'], - aliases: ['tagsfield'], - mimetypes: [], - def: { - language, - languageConfiguration, - }, -}; diff --git a/public/app/plugins/datasource/tempo/QueryField.tsx b/public/app/plugins/datasource/tempo/QueryField.tsx index 01a4cdd415..3196be57ba 100644 --- a/public/app/plugins/datasource/tempo/QueryField.tsx +++ b/public/app/plugins/datasource/tempo/QueryField.tsx @@ -15,13 +15,13 @@ import { withTheme2, } from '@grafana/ui'; -import NativeSearch from './NativeSearch/NativeSearch'; import TraceQLSearch from './SearchTraceQLEditor/TraceQLSearch'; import { ServiceGraphSection } from './ServiceGraphSection'; import { TempoQueryType } from './dataquery.gen'; import { TempoDatasource } from './datasource'; import { QueryEditor } from './traceql/QueryEditor'; import { TempoQuery } from './types'; +import { migrateFromSearchToTraceQLSearch } from './utils'; interface Props extends QueryEditorProps, Themeable2 {} interface State { @@ -74,7 +74,7 @@ class TempoQueryFieldComponent extends React.PureComponent { { value: 'serviceMap', label: 'Service Graph' }, ]; - // Show the deprecated search option if any of the deprecated search fields are set + // Migrate user to new query type if they are using the old search query type if ( query.spanName || query.serviceName || @@ -83,7 +83,7 @@ class TempoQueryFieldComponent extends React.PureComponent { query.minDuration || query.queryType === 'nativeSearch' ) { - queryTypeOptions.unshift({ value: 'nativeSearch', label: '[Deprecated] Search' }); + onChange(migrateFromSearchToTraceQLSearch(query)); } return ( @@ -146,15 +146,6 @@ class TempoQueryFieldComponent extends React.PureComponent {
- {query.queryType === 'nativeSearch' && ( - - )} {query.queryType === 'traceqlSearch' && ( { const styles = useStyles2(getStyles); - const generateId = () => uuidv4().slice(0, 8); const handleOnAdd = useCallback( () => updateFilter({ id: generateId(), operator: '=', scope: TraceqlSearchScope.Span }), [updateFilter] @@ -117,3 +116,5 @@ const TagsInput = ({ }; export default TagsInput; + +export const generateId = () => uuidv4().slice(0, 8); diff --git a/public/app/plugins/datasource/tempo/dataquery.cue b/public/app/plugins/datasource/tempo/dataquery.cue index c1a1906e26..786f000fef 100644 --- a/public/app/plugins/datasource/tempo/dataquery.cue +++ b/public/app/plugins/datasource/tempo/dataquery.cue @@ -53,7 +53,6 @@ composableKinds: DataQuery: { tableType?: #SearchTableType } @cuetsy(kind="interface") @grafana(TSVeneer="type") - // nativeSearch = Tempo search for backwards compatibility #TempoQueryType: "traceql" | "traceqlSearch" | "serviceMap" | "upload" | "nativeSearch" | "traceId" | "clear" @cuetsy(kind="type") // The state of the TraceQL streaming search query diff --git a/public/app/plugins/datasource/tempo/dataquery.gen.ts b/public/app/plugins/datasource/tempo/dataquery.gen.ts index c98759b03f..17cce1c50d 100644 --- a/public/app/plugins/datasource/tempo/dataquery.gen.ts +++ b/public/app/plugins/datasource/tempo/dataquery.gen.ts @@ -67,9 +67,6 @@ export const defaultTempoQuery: Partial = { groupBy: [], }; -/** - * nativeSearch = Tempo search for backwards compatibility - */ export type TempoQueryType = ('traceql' | 'traceqlSearch' | 'serviceMap' | 'upload' | 'nativeSearch' | 'traceId' | 'clear'); /** diff --git a/public/app/plugins/datasource/tempo/datasource.test.ts b/public/app/plugins/datasource/tempo/datasource.test.ts index 1611ed4240..7deb9b62af 100644 --- a/public/app/plugins/datasource/tempo/datasource.test.ts +++ b/public/app/plugins/datasource/tempo/datasource.test.ts @@ -31,7 +31,6 @@ import { TempoVariableQueryType } from './VariableQueryEditor'; import { createFetchResponse } from './_importedDependencies/test/helpers/createFetchResponse'; import { TraceqlSearchScope } from './dataquery.gen'; import { - DEFAULT_LIMIT, TempoDatasource, buildExpr, buildLinkExpr, @@ -83,11 +82,6 @@ describe('Tempo data source', () => { refId: 'x', queryType: 'traceql', query: '$interpolationVarWithPipe', - spanName: '$interpolationVar', - serviceName: '$interpolationVar', - search: '$interpolationVar', - minDuration: '$interpolationVar', - maxDuration: '$interpolationVar', serviceMapQuery, filters: [ { @@ -135,11 +129,6 @@ describe('Tempo data source', () => { const ds = new TempoDatasource(defaultSettings, templateSrv); const queries = ds.interpolateVariablesInQueries([getQuery()], {}); expect(queries[0].query).toBe(textWithPipe); - expect(queries[0].serviceName).toBe(text); - expect(queries[0].spanName).toBe(text); - expect(queries[0].search).toBe(text); - expect(queries[0].minDuration).toBe(text); - expect(queries[0].maxDuration).toBe(text); expect(queries[0].serviceMapQuery).toBe(text); expect(queries[0].filters[0].value).toBe(textWithPipe); expect(queries[0].filters[1].value).toBe(text); @@ -153,11 +142,6 @@ describe('Tempo data source', () => { interpolationVar: { text: scopedText, value: scopedText }, }); expect(resp.query).toBe(textWithPipe); - expect(resp.serviceName).toBe(scopedText); - expect(resp.spanName).toBe(scopedText); - expect(resp.search).toBe(scopedText); - expect(resp.minDuration).toBe(scopedText); - expect(resp.maxDuration).toBe(scopedText); expect(resp.filters[0].value).toBe(textWithPipe); expect(resp.filters[1].value).toBe(scopedText); expect(resp.filters[1].tag).toBe(scopedText); @@ -283,31 +267,6 @@ describe('Tempo data source', () => { expect(edgesFrame.meta?.preferredVisualisationType).toBe('nodeGraph'); }); - it('should build search query correctly', () => { - const duration = '10ms'; - const templateSrv = { replace: jest.fn().mockReturnValue(duration) } as unknown as TemplateSrv; - const ds = new TempoDatasource(defaultSettings, templateSrv); - const tempoQuery: TempoQuery = { - queryType: 'nativeSearch', - refId: 'A', - query: '', - serviceName: 'frontend', - spanName: '/config', - search: 'root.http.status_code=500', - minDuration: '$interpolationVar', - maxDuration: '$interpolationVar', - limit: 10, - filters: [], - }; - const builtQuery = ds.buildSearchQuery(tempoQuery); - expect(builtQuery).toStrictEqual({ - tags: 'root.http.status_code=500 service.name="frontend" name="/config"', - minDuration: duration, - maxDuration: duration, - limit: 10, - }); - }); - it('should format metrics summary query correctly', () => { const ds = new TempoDatasource(defaultSettings, {} as TemplateSrv); const queryGroupBy = [ @@ -320,61 +279,6 @@ describe('Tempo data source', () => { expect(groupBy).toEqual('.component, span.name, resource.service.name, kind'); }); - it('should include a default limit', () => { - const ds = new TempoDatasource(defaultSettings); - const tempoQuery: TempoQuery = { - queryType: 'nativeSearch', - refId: 'A', - query: '', - search: '', - filters: [], - }; - const builtQuery = ds.buildSearchQuery(tempoQuery); - expect(builtQuery).toStrictEqual({ - tags: '', - limit: DEFAULT_LIMIT, - }); - }); - - it('should include time range if provided', () => { - const ds = new TempoDatasource(defaultSettings); - const tempoQuery: TempoQuery = { - queryType: 'nativeSearch', - refId: 'A', - query: '', - search: '', - filters: [], - }; - const timeRange = { startTime: 0, endTime: 1000 }; - const builtQuery = ds.buildSearchQuery(tempoQuery, timeRange); - expect(builtQuery).toStrictEqual({ - tags: '', - limit: DEFAULT_LIMIT, - start: timeRange.startTime, - end: timeRange.endTime, - }); - }); - - it('formats native search query history correctly', () => { - const ds = new TempoDatasource(defaultSettings); - const tempoQuery: TempoQuery = { - filters: [], - queryType: 'nativeSearch', - refId: 'A', - query: '', - serviceName: 'frontend', - spanName: '/config', - search: 'root.http.status_code=500', - minDuration: '1ms', - maxDuration: '100s', - limit: 10, - }; - const result = ds.getQueryDisplayText(tempoQuery); - expect(result).toBe( - 'Service Name: frontend, Span Name: /config, Search: root.http.status_code=500, Min Duration: 1ms, Max Duration: 100s, Limit: 10' - ); - }); - describe('test the testDatasource function', () => { it('should return a success msg if response.ok is true', async () => { mockObservable = () => of({ ok: true }); diff --git a/public/app/plugins/datasource/tempo/datasource.ts b/public/app/plugins/datasource/tempo/datasource.ts index 0b73600318..f6d9600fcc 100644 --- a/public/app/plugins/datasource/tempo/datasource.ts +++ b/public/app/plugins/datasource/tempo/datasource.ts @@ -1,4 +1,4 @@ -import { groupBy, identity, pick, pickBy, startCase } from 'lodash'; +import { groupBy, startCase } from 'lodash'; import { EMPTY, from, lastValueFrom, merge, Observable, of } from 'rxjs'; import { catchError, concatMap, map, mergeMap, toArray } from 'rxjs/operators'; import semver from 'semver'; @@ -14,7 +14,6 @@ import { DataSourceInstanceSettings, dateTime, FieldType, - isValidGoDuration, LoadingState, rangeUtil, ScopedVars, @@ -53,15 +52,14 @@ import { import TempoLanguageProvider from './language_provider'; import { createTableFrameFromMetricsSummaryQuery, emptyResponse, MetricsSummary } from './metricsSummary'; import { - createTableFrameFromSearch, formatTraceQLMetrics, formatTraceQLResponse, transformFromOTLP as transformFromOTEL, transformTrace, } from './resultTransformer'; import { doTempoChannelStream } from './streaming'; -import { SearchQueryParams, TempoJsonData, TempoQuery } from './types'; -import { getErrorMessage } from './utils'; +import { TempoJsonData, TempoQuery } from './types'; +import { getErrorMessage, migrateFromSearchToTraceQLSearch } from './utils'; import { TempoVariableSupport } from './variables'; export const DEFAULT_LIMIT = 20; @@ -265,37 +263,18 @@ export class TempoDatasource extends DataSourceWithBackend { - return { - data: [createTableFrameFromSearch(response.data.traces, this.instanceSettings)], - }; - }), - catchError((err) => { - return of({ error: { message: getErrorMessage(err.data.message) }, data: [] }); - }) - ) - ); - } catch (error) { - return of({ error: { message: error instanceof Error ? error.message : 'Unknown error occurred' }, data: [] }); + if ( + targets.nativeSearch[0].spanName || + targets.nativeSearch[0].serviceName || + targets.nativeSearch[0].search || + targets.nativeSearch[0].maxDuration || + targets.nativeSearch[0].minDuration || + targets.nativeSearch[0].queryType === 'nativeSearch' + ) { + const migratedQuery = migrateFromSearchToTraceQLSearch(targets.nativeSearch[0]); + targets.traceqlSearch = [migratedQuery]; } } @@ -502,11 +481,6 @@ export class TempoDatasource extends DataSourceWithBackend this.templateSrv.replace(query, scopedVars)) : this.templateSrv.replace(query.serviceMapQuery ?? '', scopedVars), @@ -758,55 +732,6 @@ export class TempoDatasource extends DataSourceWithBackend `${startCase(key)}: ${query[key]}`) .join(', '); } - - buildSearchQuery(query: TempoQuery, timeRange?: { startTime: number; endTime?: number }): SearchQueryParams { - let tags = query.search ?? ''; - - let tempoQuery = pick(query, ['minDuration', 'maxDuration', 'limit']); - // Remove empty properties - tempoQuery = pickBy(tempoQuery, identity); - - if (query.serviceName) { - tags += ` service.name="${query.serviceName}"`; - } - if (query.spanName) { - tags += ` name="${query.spanName}"`; - } - - // Set default limit - if (!tempoQuery.limit) { - tempoQuery.limit = DEFAULT_LIMIT; - } - - // Validate query inputs and remove spaces if valid - if (tempoQuery.minDuration) { - tempoQuery.minDuration = this.templateSrv.replace(tempoQuery.minDuration ?? ''); - if (!isValidGoDuration(tempoQuery.minDuration)) { - throw new Error('Please enter a valid min duration.'); - } - tempoQuery.minDuration = tempoQuery.minDuration.replace(/\s/g, ''); - } - if (tempoQuery.maxDuration) { - tempoQuery.maxDuration = this.templateSrv.replace(tempoQuery.maxDuration ?? ''); - if (!isValidGoDuration(tempoQuery.maxDuration)) { - throw new Error('Please enter a valid max duration.'); - } - tempoQuery.maxDuration = tempoQuery.maxDuration.replace(/\s/g, ''); - } - - if (!Number.isInteger(tempoQuery.limit) || tempoQuery.limit <= 0) { - throw new Error('Please enter a valid limit.'); - } - - let searchQuery: SearchQueryParams = { tags, ...tempoQuery }; - - if (timeRange) { - searchQuery.start = timeRange.startTime; - searchQuery.end = timeRange.endTime; - } - - return searchQuery; - } } function queryPrometheus(request: DataQueryRequest, datasourceUid: string) { diff --git a/public/app/plugins/datasource/tempo/mockServiceGraph.json b/public/app/plugins/datasource/tempo/mockServiceGraph.json index abdd24a0d1..267ffe9975 100644 --- a/public/app/plugins/datasource/tempo/mockServiceGraph.json +++ b/public/app/plugins/datasource/tempo/mockServiceGraph.json @@ -48,7 +48,7 @@ "title": "View traces", "internal": { "query": { - "queryType": "nativeSearch", + "queryType": "traceqlSearch", "serviceName": "${__data.fields[0]}" }, "datasourceUid": "TNS Tempo", diff --git a/public/app/plugins/datasource/tempo/resultTransformer.test.ts b/public/app/plugins/datasource/tempo/resultTransformer.test.ts index 9d6e3816b7..2f416d80ef 100644 --- a/public/app/plugins/datasource/tempo/resultTransformer.test.ts +++ b/public/app/plugins/datasource/tempo/resultTransformer.test.ts @@ -1,11 +1,10 @@ import { collectorTypes } from '@opentelemetry/exporter-collector'; -import { PluginType, DataSourceInstanceSettings, dateTime, PluginMetaInfo } from '@grafana/data'; +import { PluginType, DataSourceInstanceSettings, PluginMetaInfo } from '@grafana/data'; import { transformToOTLP, transformFromOTLP, - createTableFrameFromSearch, createTableFrameFromTraceQlQuery, createTableFrameFromTraceQlQueryAsSpans, } from './resultTransformer'; @@ -14,7 +13,6 @@ import { otlpDataFrameToResponse, otlpDataFrameFromResponse, otlpResponse, - tempoSearchResponse, traceQlResponse, } from './testResponse'; import { TraceSearchMetadata } from './types'; @@ -57,32 +55,6 @@ describe('transformFromOTLP()', () => { }); }); -describe('createTableFrameFromSearch()', () => { - const mockTimeUnix = dateTime(1643357709095).valueOf(); - global.Date.now = jest.fn(() => mockTimeUnix); - test('transforms search response to dataFrame', () => { - const frame = createTableFrameFromSearch(tempoSearchResponse.traces as TraceSearchMetadata[], defaultSettings); - expect(frame.fields[0].name).toBe('traceID'); - expect(frame.fields[0].values[0]).toBe('e641dcac1c3a0565'); - - // TraceID must have unit = 'string' to prevent the ID from rendering as Infinity - expect(frame.fields[0].config.unit).toBe('string'); - - expect(frame.fields[1].name).toBe('traceService'); - expect(frame.fields[1].values[0]).toBe('requester'); - - expect(frame.fields[2].name).toBe('traceName'); - expect(frame.fields[2].values[0]).toBe('app'); - - expect(frame.fields[3].name).toBe('startTime'); - expect(frame.fields[3].values[0]).toBe(1643356828724); - expect(frame.fields[3].values[1]).toBe(1643342166678.0002); - - expect(frame.fields[4].name).toBe('traceDuration'); - expect(frame.fields[4].values[0]).toBe(65); - }); -}); - describe('createTableFrameFromTraceQlQuery()', () => { test('transforms TraceQL response to DataFrame', () => { const frameList = createTableFrameFromTraceQlQuery(traceQlResponse.traces, defaultSettings); diff --git a/public/app/plugins/datasource/tempo/resultTransformer.ts b/public/app/plugins/datasource/tempo/resultTransformer.ts index b3e5735f1a..c260b0b78c 100644 --- a/public/app/plugins/datasource/tempo/resultTransformer.ts +++ b/public/app/plugins/datasource/tempo/resultTransformer.ts @@ -464,63 +464,6 @@ export function transformTrace( }; } -export function createTableFrameFromSearch(data: TraceSearchMetadata[], instanceSettings: DataSourceInstanceSettings) { - const frame = new MutableDataFrame({ - name: 'Traces', - refId: 'traces', - fields: [ - { - name: 'traceID', - type: FieldType.string, - values: [], - config: { - unit: 'string', - displayNameFromDS: 'Trace ID', - links: [ - { - title: 'Trace: ${__value.raw}', - url: '', - internal: { - datasourceUid: instanceSettings.uid, - datasourceName: instanceSettings.name, - query: { - query: '${__value.raw}', - queryType: 'traceql', - }, - }, - }, - ], - }, - }, - { name: 'traceService', type: FieldType.string, config: { displayNameFromDS: 'Trace service' }, values: [] }, - { name: 'traceName', type: FieldType.string, config: { displayNameFromDS: 'Trace name' }, values: [] }, - { name: 'startTime', type: FieldType.time, config: { displayNameFromDS: 'Start time' }, values: [] }, - { - name: 'traceDuration', - type: FieldType.number, - config: { displayNameFromDS: 'Duration', unit: 'ms' }, - values: [], - }, - ], - meta: { - preferredVisualisationType: 'table', - }, - }); - if (!data?.length) { - return frame; - } - // Show the most recent traces - const traceData = data - .sort((a, b) => parseInt(b?.startTimeUnixNano!, 10) / 1000000 - parseInt(a?.startTimeUnixNano!, 10) / 1000000) - .map(transformToTraceData); - - for (const trace of traceData) { - frame.add(trace); - } - - return frame; -} - function transformToTraceData(data: TraceSearchMetadata) { return { traceID: data.traceID, diff --git a/public/app/plugins/datasource/tempo/testResponse.ts b/public/app/plugins/datasource/tempo/testResponse.ts index 6910941082..687e90b82c 100644 --- a/public/app/plugins/datasource/tempo/testResponse.ts +++ b/public/app/plugins/datasource/tempo/testResponse.ts @@ -2273,28 +2273,6 @@ export const otlpResponse = { ], }; -export const tempoSearchResponse = { - traces: [ - { - traceID: 'e641dcac1c3a0565', - rootServiceName: 'requester', - rootTraceName: 'app', - startTimeUnixNano: '1643356828724000000', - durationMs: 65, - }, - { - traceID: 'c2983496a2b12544', - rootServiceName: '', - startTimeUnixNano: '1643342166678000000', - durationMs: 93, - }, - ], - metrics: { - inspectedTraces: 2, - inspectedBytes: '83720', - }, -}; - export const traceQlResponse = { traces: [ { diff --git a/public/app/plugins/datasource/tempo/tracking.test.ts b/public/app/plugins/datasource/tempo/tracking.test.ts index 8133787d32..2066b71a62 100644 --- a/public/app/plugins/datasource/tempo/tracking.test.ts +++ b/public/app/plugins/datasource/tempo/tracking.test.ts @@ -17,23 +17,6 @@ jest.mock('@grafana/runtime', () => { grafanaVersion: 'v9.4.0', queries: { tempo: [ - { - datasource: { type: 'tempo', uid: 'abc' }, - queryType: 'nativeSearch', - refId: 'A', - }, - { - datasource: { type: 'tempo', uid: 'abc' }, - queryType: 'nativeSearch', - spanName: 'HTTP', - refId: 'A', - }, - { - datasource: { type: 'tempo', uid: 'abc' }, - queryType: 'nativeSearch', - spanName: '$var', - refId: 'A', - }, { datasource: { type: 'tempo', uid: 'abc' }, queryType: 'serviceMap', @@ -86,10 +69,8 @@ describe('on dashboard loaded', () => { traceql_query_count: 2, service_map_query_count: 2, upload_query_count: 1, - native_search_query_count: 3, traceql_queries_with_template_variables_count: 1, service_map_queries_with_template_variables_count: 1, - native_search_queries_with_template_variables_count: 1, }); }); }); diff --git a/public/app/plugins/datasource/tempo/tracking.ts b/public/app/plugins/datasource/tempo/tracking.ts index f44effba5e..e5833a5c42 100644 --- a/public/app/plugins/datasource/tempo/tracking.ts +++ b/public/app/plugins/datasource/tempo/tracking.ts @@ -8,11 +8,9 @@ type TempoOnDashboardLoadedTrackingEvent = { grafana_version?: string; dashboard_id?: string; org_id?: number; - native_search_query_count: number; service_map_query_count: number; traceql_query_count: number; upload_query_count: number; - native_search_queries_with_template_variables_count: number; service_map_queries_with_template_variables_count: number; traceql_queries_with_template_variables_count: number; }; @@ -31,11 +29,9 @@ export const onDashboardLoadedHandler = ({ grafana_version: grafanaVersion, dashboard_id: dashboardId, org_id: orgId, - native_search_query_count: 0, service_map_query_count: 0, traceql_query_count: 0, upload_query_count: 0, - native_search_queries_with_template_variables_count: 0, service_map_queries_with_template_variables_count: 0, traceql_queries_with_template_variables_count: 0, }; @@ -45,18 +41,7 @@ export const onDashboardLoadedHandler = ({ continue; } - if (query.queryType === 'nativeSearch') { - stats.native_search_query_count++; - if ( - (query.serviceName && hasTemplateVariables(query.serviceName)) || - (query.spanName && hasTemplateVariables(query.spanName)) || - (query.search && hasTemplateVariables(query.search)) || - (query.minDuration && hasTemplateVariables(query.minDuration)) || - (query.maxDuration && hasTemplateVariables(query.maxDuration)) - ) { - stats.native_search_queries_with_template_variables_count++; - } - } else if (query.queryType === 'serviceMap') { + if (query.queryType === 'serviceMap') { stats.service_map_query_count++; if (query.serviceMapQuery && hasTemplateVariables(query.serviceMapQuery)) { stats.service_map_queries_with_template_variables_count++; diff --git a/public/app/plugins/datasource/tempo/types.ts b/public/app/plugins/datasource/tempo/types.ts index 806445c10a..09c31e05b9 100644 --- a/public/app/plugins/datasource/tempo/types.ts +++ b/public/app/plugins/datasource/tempo/types.ts @@ -3,15 +3,6 @@ import { NodeGraphOptions, TraceToLogsOptions } from '@grafana/o11y-ds-frontend' import { TempoQuery as TempoBase, TempoQueryType, TraceqlFilter } from './dataquery.gen'; -export interface SearchQueryParams { - minDuration?: string; - maxDuration?: string; - limit?: number; - tags?: string; - start?: number; - end?: number; -} - export interface TempoJsonData extends DataSourceJsonData { tracesToLogs?: TraceToLogsOptions; serviceMap?: { diff --git a/public/app/plugins/datasource/tempo/utils.test.ts b/public/app/plugins/datasource/tempo/utils.test.ts new file mode 100644 index 0000000000..65f40f7816 --- /dev/null +++ b/public/app/plugins/datasource/tempo/utils.test.ts @@ -0,0 +1,51 @@ +import { TempoQuery } from './types'; +import { migrateFromSearchToTraceQLSearch } from './utils'; + +describe('utils', () => { + it('migrateFromSearchToTraceQLSearch correctly updates the query', async () => { + const query: TempoQuery = { + refId: 'A', + filters: [], + queryType: 'nativeSearch', + serviceName: 'frontend', + spanName: 'http.server', + minDuration: '1s', + maxDuration: '10s', + search: 'component="net/http" datasource.type="tempo"', + }; + + const migratedQuery = migrateFromSearchToTraceQLSearch(query); + expect(migratedQuery.queryType).toBe('traceqlSearch'); + expect(migratedQuery.filters.length).toBe(7); + expect(migratedQuery.filters[0].scope).toBe('span'); + expect(migratedQuery.filters[0].tag).toBe('name'); + expect(migratedQuery.filters[0].operator).toBe('='); + expect(migratedQuery.filters[0].value![0]).toBe('http.server'); + expect(migratedQuery.filters[0].valueType).toBe('string'); + expect(migratedQuery.filters[1].scope).toBe('resource'); + expect(migratedQuery.filters[1].tag).toBe('service.name'); + expect(migratedQuery.filters[1].operator).toBe('='); + expect(migratedQuery.filters[1].value![0]).toBe('frontend'); + expect(migratedQuery.filters[1].valueType).toBe('string'); + expect(migratedQuery.filters[2].id).toBe('duration-type'); + expect(migratedQuery.filters[2].value).toBe('trace'); + expect(migratedQuery.filters[3].tag).toBe('duration'); + expect(migratedQuery.filters[3].operator).toBe('>'); + expect(migratedQuery.filters[3].value![0]).toBe('1s'); + expect(migratedQuery.filters[3].valueType).toBe('duration'); + expect(migratedQuery.filters[4].tag).toBe('duration'); + expect(migratedQuery.filters[4].operator).toBe('<'); + expect(migratedQuery.filters[4].value![0]).toBe('10s'); + expect(migratedQuery.filters[4].valueType).toBe('duration'); + expect(migratedQuery.filters[5].scope).toBe('unscoped'); + expect(migratedQuery.filters[5].tag).toBe('component'); + expect(migratedQuery.filters[5].operator).toBe('='); + expect(migratedQuery.filters[5].value![0]).toBe('net/http'); + expect(migratedQuery.filters[5].valueType).toBe('string'); + expect(migratedQuery.filters[6].scope).toBe('unscoped'); + expect(migratedQuery.filters[6].tag).toBe('datasource.type'); + expect(migratedQuery.filters[6].operator).toBe('='); + expect(migratedQuery.filters[6].value![0]).toBe('tempo'); + expect(migratedQuery.filters[6].valueType).toBe('string'); + }); +}); diff --git a/public/app/plugins/datasource/tempo/utils.ts b/public/app/plugins/datasource/tempo/utils.ts index 1e138747e8..f23b77f3e5 100644 --- a/public/app/plugins/datasource/tempo/utils.ts +++ b/public/app/plugins/datasource/tempo/utils.ts @@ -1,6 +1,10 @@ import { DataSourceApi } from '@grafana/data'; import { getDataSourceSrv } from '@grafana/runtime'; +import { generateId } from './SearchTraceQLEditor/TagsInput'; +import { TraceqlFilter, TraceqlSearchScope } from './dataquery.gen'; +import { TempoQuery } from './types'; + export const getErrorMessage = (message: string | undefined, prefix?: string) => { const err = message ? ` (${message})` : ''; let errPrefix = prefix ? prefix : 'Error'; @@ -20,3 +24,78 @@ export async function getDS(uid?: string): Promise { return undefined; } } + +export const migrateFromSearchToTraceQLSearch = (query: TempoQuery) => { + let filters: TraceqlFilter[] = []; + if (query.spanName) { + filters.push({ + id: 'span-name', + scope: TraceqlSearchScope.Span, + tag: 'name', + operator: '=', + value: [query.spanName], + valueType: 'string', + }); + } + if (query.serviceName) { + filters.push({ + id: 'service-name', + scope: TraceqlSearchScope.Resource, + tag: 'service.name', + operator: '=', + value: [query.serviceName], + valueType: 'string', + }); + } + if (query.minDuration || query.maxDuration) { + filters.push({ + id: 'duration-type', + value: 'trace', + }); + } + if (query.minDuration) { + filters.push({ + id: 'min-duration', + tag: 'duration', + operator: '>', + value: [query.minDuration], + valueType: 'duration', + }); + } + if (query.maxDuration) { + filters.push({ + id: 'max-duration', + tag: 'duration', + operator: '<', + value: [query.maxDuration], + valueType: 'duration', + }); + } + if (query.search) { + const tags = query.search.split(' '); + for (const tag of tags) { + const [key, value] = tag.split('='); + if (key && value) { + filters.push({ + id: generateId(), + scope: TraceqlSearchScope.Unscoped, + tag: key, + operator: '=', + value: [value.replace(/(^"|"$)/g, '')], // remove quotes at start and end of string + valueType: value.startsWith('"') && value.endsWith('"') ? 'string' : undefined, + }); + } + } + } + + const migratedQuery: TempoQuery = { + datasource: query.datasource, + filters, + groupBy: query.groupBy, + limit: query.limit, + query: query.query, + queryType: 'traceqlSearch', + refId: query.refId, + }; + return migratedQuery; +}; From fce78aea2cd7085d1609da306ce0bf775d7aea5c Mon Sep 17 00:00:00 2001 From: Polina Boneva <13227501+polibb@users.noreply.github.com> Date: Mon, 18 Mar 2024 11:30:27 +0200 Subject: [PATCH 0737/1406] Variables: Multi-select DataSource variables are inconsistently displayed in the Data source picker (#76039) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Always show multi-select DataSource(DS) variables in the DS picker, and display a warning in the panel when a DataSource variable holds multiple values and is not being repeated. --------- Co-authored-by: Alexandra Vargas Co-authored-by: Alexa V <239999+axelavargas@users.noreply.github.com> Co-authored-by: Torkel Ödegaard --- .betterer.results | 5 +- public/app/features/plugins/datasource_srv.ts | 6 +- .../query/state/PanelQueryRunner.test.ts | 74 ++++++++++++++++++- .../features/query/state/PanelQueryRunner.ts | 45 ++++++++++- .../features/templating/template_srv.mock.ts | 42 ++++++++--- 5 files changed, 153 insertions(+), 19 deletions(-) diff --git a/.betterer.results b/.betterer.results index ce4bc3b1ea..f465107fe6 100644 --- a/.betterer.results +++ b/.betterer.results @@ -4088,7 +4088,10 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], "public/app/features/templating/template_srv.mock.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] + [0, 0, 0, "Do not use any type assertions.", "0"], + [0, 0, 0, "Do not use any type assertions.", "1"], + [0, 0, 0, "Do not use any type assertions.", "2"], + [0, 0, 0, "Do not use any type assertions.", "3"] ], "public/app/features/templating/template_srv.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], diff --git a/public/app/features/plugins/datasource_srv.ts b/public/app/features/plugins/datasource_srv.ts index 16f4c08f54..7e9b6affd1 100644 --- a/public/app/features/plugins/datasource_srv.ts +++ b/public/app/features/plugins/datasource_srv.ts @@ -251,8 +251,10 @@ export class DatasourceSrv implements DataSourceService { continue; } let dsValue = variable.current.value === 'default' ? this.defaultName : variable.current.value; - if (Array.isArray(dsValue) && dsValue.length === 1) { - // Support for multi-value variables with only one selected datasource + // Support for multi-value DataSource (ds) variables + if (Array.isArray(dsValue)) { + // If the ds variable have multiple selected datasources + // We will use the first one dsValue = dsValue[0]; } const dsSettings = diff --git a/public/app/features/query/state/PanelQueryRunner.test.ts b/public/app/features/query/state/PanelQueryRunner.test.ts index 1d5e61cba4..e57cc8ea7a 100644 --- a/public/app/features/query/state/PanelQueryRunner.test.ts +++ b/public/app/features/query/state/PanelQueryRunner.test.ts @@ -3,8 +3,9 @@ const applyFieldOverridesMock = jest.fn(); // needs to be first in this file import { Subject } from 'rxjs'; // Importing this way to be able to spy on grafana/data + import * as grafanaData from '@grafana/data'; -import { DataSourceApi } from '@grafana/data'; +import { DataSourceApi, TypedVariableModel } from '@grafana/data'; import { DataSourceSrv, setDataSourceSrv, setEchoSrv } from '@grafana/runtime'; import { TemplateSrvMock } from 'app/features/templating/template_srv.mock'; @@ -46,7 +47,27 @@ jest.mock('app/features/dashboard/services/DashboardSrv', () => ({ })); jest.mock('app/features/templating/template_srv', () => ({ - getTemplateSrv: () => new TemplateSrvMock({}), + ...jest.requireActual('app/features/templating/template_srv'), + getTemplateSrv: () => + new TemplateSrvMock([ + { + name: 'server', + type: 'datasource', + current: { text: 'Server1', value: 'server' }, + options: [{ text: 'Server1', value: 'server1' }], + }, + //multi value variable + { + name: 'multi', + type: 'datasource', + multi: true, + current: { text: 'Server1,Server2', value: ['server-1', 'server-2'] }, + options: [ + { text: 'Server1', value: 'server1' }, + { text: 'Server2', value: 'server2' }, + ], + }, + ] as TypedVariableModel[]), })); interface ScenarioContext { @@ -405,4 +426,53 @@ describe('PanelQueryRunner', () => { snapshotData, } ); + + describeQueryRunnerScenario( + 'shouldAddErrorwhenDatasourceVariableIsMultiple', + (ctx) => { + it('should add error when datasource variable is multiple and not repeated', async () => { + // scopedVars is an object that represent the variables repeated in a panel + const scopedVars = { + server: { text: 'Server1', value: 'server-1' }, + }; + + // We are spying on the replace method of the TemplateSrvMock to check if the custom format function is being called + const spyReplace = jest.spyOn(TemplateSrvMock.prototype, 'replace'); + + const response = { + data: [ + { + target: 'hello', + datapoints: [ + [1, 1000], + [2, 2000], + ], + }, + ], + }; + + const datasource = { + name: '${multi}', + uid: '${multi}', + interval: ctx.dsInterval, + query: (options: grafanaData.DataQueryRequest) => { + ctx.queryCalledWith = options; + return Promise.resolve(response); + }, + getRef: () => ({ type: 'test', uid: 'TestDB-uid' }), + testDatasource: jest.fn(), + } as unknown as DataSourceApi; + + ctx.runner.shouldAddErrorWhenDatasourceVariableIsMultiple(datasource, scopedVars); + + // the test is checking implementation details :(, but it is the only way to check if the error will be added + // if the getTemplateSrv.replace is called with the custom format function,it means we will check + // if the error should be added + expect(spyReplace.mock.calls[0][2]).toBeInstanceOf(Function); + }); + }, + { + ...defaultPanelConfig, + } + ); }); diff --git a/public/app/features/query/state/PanelQueryRunner.ts b/public/app/features/query/state/PanelQueryRunner.ts index 940894c046..87bb9bf93f 100644 --- a/public/app/features/query/state/PanelQueryRunner.ts +++ b/public/app/features/query/state/PanelQueryRunner.ts @@ -275,6 +275,9 @@ export class PanelQueryRunner { return; } + //check if datasource is a variable datasource and if that variable has multiple values + const addErroDSVariable = this.shouldAddErrorWhenDatasourceVariableIsMultiple(datasource, scopedVars); + const request: DataQueryRequest = { app: app ?? CoreApp.Dashboard, requestId: getNextRequestId(), @@ -327,7 +330,7 @@ export class PanelQueryRunner { this.lastRequest = request; - this.pipeToSubject(runRequest(ds, request), panelId); + this.pipeToSubject(runRequest(ds, request), panelId, false, addErroDSVariable); } catch (err) { this.pipeToSubject( of({ @@ -341,7 +344,12 @@ export class PanelQueryRunner { } } - private pipeToSubject(observable: Observable, panelId?: number, skipPreProcess = false) { + private pipeToSubject( + observable: Observable, + panelId?: number, + skipPreProcess = false, + addErroDSVariable = false + ) { if (this.subscription) { this.subscription.unsubscribe(); } @@ -379,6 +387,17 @@ export class PanelQueryRunner { this.lastResult = next; + //add error message if datasource is a variable and has multiple values + if (addErroDSVariable) { + next.errors = [ + { + message: + 'Panel is using a variable datasource with multiple values without repeat option. Please configure the panel to be repeated by the same datasource variable.', + }, + ]; + next.state = LoadingState.Error; + } + // Store preprocessed query results for applying overrides later on in the pipeline this.subject.next(next); }, @@ -451,6 +470,28 @@ export class PanelQueryRunner { getLastRequest(): DataQueryRequest | undefined { return this.lastRequest; } + + shouldAddErrorWhenDatasourceVariableIsMultiple( + datasource: DataSourceRef | DataSourceApi | null, + scopedVars: ScopedVars | undefined + ): boolean { + let addWarningMessageMultipleDatasourceVariable = false; + + //If datasource is a variable + if (datasource?.uid?.startsWith('${')) { + // we can access the raw datasource variable values inside the replace function if we pass a custom format function + this.templateSrv.replace(datasource.uid, scopedVars, (value: string | string[]) => { + // if the variable has multiple values it means it's not being repeated + if (Array.isArray(value) && value.length > 1) { + addWarningMessageMultipleDatasourceVariable = true; + } + // return empty string to avoid replacing the variable + return ''; + }); + } + + return addWarningMessageMultipleDatasourceVariable; + } } async function getDataSource( diff --git a/public/app/features/templating/template_srv.mock.ts b/public/app/features/templating/template_srv.mock.ts index 53dad5ead9..2ff7c0abda 100644 --- a/public/app/features/templating/template_srv.mock.ts +++ b/public/app/features/templating/template_srv.mock.ts @@ -1,4 +1,4 @@ -import { ScopedVars, TimeRange, TypedVariableModel } from '@grafana/data'; +import { ScopedVars, TimeRange, TypedVariableModel, VariableOption } from '@grafana/data'; import { TemplateSrv } from '@grafana/runtime'; import { variableRegex } from '../variables/utils'; @@ -13,18 +13,37 @@ import { variableRegex } from '../variables/utils'; */ export class TemplateSrvMock implements TemplateSrv { private regex = variableRegex; - constructor(private variables: Record) {} + constructor(private variables: TypedVariableModel[]) {} getVariables(): TypedVariableModel[] { - return Object.keys(this.variables).map((key) => { - return { - type: 'custom', - name: key, - label: key, + if (!this.variables) { + return []; + } + + return this.variables.reduce((acc: TypedVariableModel[], variable) => { + const commonProps = { + type: variable.type ?? 'custom', + name: variable.name ?? 'test', + label: variable.label ?? 'test', }; - // TODO: we remove this type assertion in a later PR - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - }) as TypedVariableModel[]; + if (variable.type === 'datasource') { + acc.push({ + ...commonProps, + current: { + text: variable.current?.text, + value: variable.current?.value, + } as VariableOption, + options: variable.options ?? [], + multi: variable.multi ?? false, + includeAll: variable.includeAll ?? false, + } as TypedVariableModel); + } else { + acc.push({ + ...commonProps, + } as TypedVariableModel); + } + return acc as TypedVariableModel[]; + }, []); } replace(target?: string, scopedVars?: ScopedVars, format?: string | Function): string { @@ -35,8 +54,7 @@ export class TemplateSrvMock implements TemplateSrv { this.regex.lastIndex = 0; return target.replace(this.regex, (match, var1, var2, fmt2, var3, fieldPath, fmt3) => { - const variableName = var1 || var2 || var3; - return this.variables[variableName]; + return var1 || var2 || var3; }); } From 8714b7cd8c9231894681d8221f64c8607648e573 Mon Sep 17 00:00:00 2001 From: Karl Persson Date: Mon, 18 Mar 2024 11:15:49 +0100 Subject: [PATCH 0738/1406] RolePicker: Don't try to fetch roles for new form (#84630) --- public/app/core/components/RolePicker/TeamRolePicker.tsx | 2 +- public/app/core/components/RolePicker/UserRolePicker.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/core/components/RolePicker/TeamRolePicker.tsx b/public/app/core/components/RolePicker/TeamRolePicker.tsx index c380a12094..f4f33871a5 100644 --- a/public/app/core/components/RolePicker/TeamRolePicker.tsx +++ b/public/app/core/components/RolePicker/TeamRolePicker.tsx @@ -53,7 +53,7 @@ export const TeamRolePicker = ({ return pendingRoles; } - if (contextSrv.hasPermission(AccessControlAction.ActionTeamsRolesList)) { + if (contextSrv.hasPermission(AccessControlAction.ActionTeamsRolesList) && teamId > 0) { return await fetchTeamRoles(teamId); } } catch (e) { diff --git a/public/app/core/components/RolePicker/UserRolePicker.tsx b/public/app/core/components/RolePicker/UserRolePicker.tsx index 5683445719..1b3bab63b7 100644 --- a/public/app/core/components/RolePicker/UserRolePicker.tsx +++ b/public/app/core/components/RolePicker/UserRolePicker.tsx @@ -62,7 +62,7 @@ export const UserRolePicker = ({ return pendingRoles; } - if (contextSrv.hasPermission(AccessControlAction.ActionUserRolesList)) { + if (contextSrv.hasPermission(AccessControlAction.ActionUserRolesList) && userId > 0) { return await fetchUserRoles(userId, orgId); } } catch (e) { From eb813f2a19bb585486c4cddf390aaf0d316a4620 Mon Sep 17 00:00:00 2001 From: tonypowa <45235678+tonypowa@users.noreply.github.com> Date: Mon, 18 Mar 2024 11:24:18 +0100 Subject: [PATCH 0739/1406] changes to #84476 (#84638) * removed note shortcode * prettyfied --- .../manage-contact-points/integrations/configure-slack.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-slack.md b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-slack.md index e17eb946f9..bb0aed4bba 100644 --- a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-slack.md +++ b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-slack.md @@ -62,7 +62,7 @@ Make sure you copy the Slack app Webhook URL. You will need this when setting up To create your Slack integration in Grafana Alerting, complete the following steps. -1. Navigate to Alerts&IRM -> Alerting -> Contact points. +1. Navigate to **Alerts & IRM** -> **Alerting** -> **Contact points**. 1. Click **+ Add contact point**. 1. Enter a contact point name. 1. From the Integration list, select Slack. @@ -77,18 +77,16 @@ To create your Slack integration in Grafana Alerting, complete the following ste To add the contact point and integration you created to your default notification policy, complete the following steps. -1. Navigate to **Alerts&IRM** -> **Alerting** -> **Notification policies**. +1. Navigate to **Alerts & IRM** -> **Alerting** -> **Notification policies**. 1. In the **Default policy**, click the ellipsis icon (…) and then **Edit**, 1. Change the default policy to the contact point you created. 1. Click **Update default policy**. -{{< admonition type="note" >}} +**Note:** If you have more than one contact point, add a new notification policy rather than edit the default one, so you can route specific alerts to Slack. For more information, refer to [Notification policies][nested-policy]. -{{< /admonition >}} {{% docs/reference %}} [nested-policy]: "/docs/grafana/ -> /docs/grafana//alerting/alerting-rules/create-notification-policy/#add-new-nested-policy" [nested-policy]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/create-notification-policy/#add-new-nested-policy" - {{% /docs/reference %}} From aa03b4393f27af3dae1220365fb9f14a9b668f21 Mon Sep 17 00:00:00 2001 From: Serge Zaitsev Date: Mon, 18 Mar 2024 11:45:25 +0100 Subject: [PATCH 0740/1406] Chore: Clean up CHANGELOG for 10.4.0 (#84551) clean up changelog for 10.4.0 to remove the items that did not make it into the release --- CHANGELOG.md | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 84bce9185d..9482a70d17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,6 @@ ### Features and enhancements -- **Chore:** Improve domain validation for Google OAuth - Backport 83229 to v10.4.x. [#83726](https://github.com/grafana/grafana/issues/83726), [@linoman](https://github.com/linoman) -- **DataQuery:** Track panel plugin id not type. [#83164](https://github.com/grafana/grafana/issues/83164), [@torkelo](https://github.com/torkelo) - **AuthToken:** Remove client token rotation feature toggle. [#82886](https://github.com/grafana/grafana/issues/82886), [@kalleep](https://github.com/kalleep) - **Plugins:** Enable feature toggle angularDeprecationUI by default. [#82880](https://github.com/grafana/grafana/issues/82880), [@xnyo](https://github.com/xnyo) - **Table Component:** Improve text-wrapping behavior of cells. [#82872](https://github.com/grafana/grafana/issues/82872), [@ahuarte47](https://github.com/ahuarte47) @@ -134,15 +132,6 @@ ### Bug fixes -- **GenAI:** Update the component only when the response is fully generated. [#83895](https://github.com/grafana/grafana/issues/83895), [@ivanortegaalba](https://github.com/ivanortegaalba) -- **LDAP:** Fix LDAP users authenticated via auth proxy not being able to use LDAP active sync. [#83751](https://github.com/grafana/grafana/issues/83751), [@Jguer](https://github.com/Jguer) -- **Tempo:** Better fallbacks for metrics query. [#83688](https://github.com/grafana/grafana/issues/83688), [@adrapereira](https://github.com/adrapereira) -- **Tempo:** Add template variable interpolation for filters. [#83667](https://github.com/grafana/grafana/issues/83667), [@joey-grafana](https://github.com/joey-grafana) -- **Elasticsearch:** Fix adhoc filters not applied in frontend mode. [#83597](https://github.com/grafana/grafana/issues/83597), [@svennergr](https://github.com/svennergr) -- **AuthProxy:** Invalidate previous cached item for user when changes are made to any header. [#83287](https://github.com/grafana/grafana/issues/83287), [@klesh](https://github.com/klesh) -- **Alerting:** Fix saving evaluation group. [#83234](https://github.com/grafana/grafana/issues/83234), [@soniaAguilarPeiron](https://github.com/soniaAguilarPeiron) -- **QueryVariableEditor:** Select a variable ds does not work. [#83181](https://github.com/grafana/grafana/issues/83181), [@ivanortegaalba](https://github.com/ivanortegaalba) -- **Logs Panel:** Add option extra UI functionality for log context. [#83129](https://github.com/grafana/grafana/issues/83129), [@svennergr](https://github.com/svennergr) - **Auth:** Fix email verification bypass when using basic authentication. [#82914](https://github.com/grafana/grafana/issues/82914), [@volcanonoodle](https://github.com/volcanonoodle) - **LibraryPanels/RBAC:** Fix issue where folder scopes weren't being correctly inherited. [#82700](https://github.com/grafana/grafana/issues/82700), [@kaydelaney](https://github.com/kaydelaney) - **Table Panel:** Fix display of ad-hoc filter actions. [#82442](https://github.com/grafana/grafana/issues/82442), [@codeincarnate](https://github.com/codeincarnate) From ed3bdf5502aa21819046d5677847fdd340c31b3b Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Mon, 18 Mar 2024 11:00:43 +0000 Subject: [PATCH 0741/1406] I18n: Expose current UI language in @grafana/runtime config (#84457) * I18n: Expose current UI language in Grafana config * fix --- packages/grafana-data/src/types/config.ts | 6 ++++++ packages/grafana-runtime/src/config.ts | 6 ++++++ public/app/app.ts | 3 ++- .../app/core/internationalization/index.tsx | 21 ++++++++++++++----- 4 files changed, 30 insertions(+), 6 deletions(-) diff --git a/packages/grafana-data/src/types/config.ts b/packages/grafana-data/src/types/config.ts index a606825d82..def3242e7c 100644 --- a/packages/grafana-data/src/types/config.ts +++ b/packages/grafana-data/src/types/config.ts @@ -230,6 +230,12 @@ export interface GrafanaConfig { // The namespace to use for kubernetes apiserver requests namespace: string; + + /** + * Language used in Grafana's UI. This is after the user's preference (or deteceted locale) is resolved to one of + * Grafana's supported language. + */ + language: string | undefined; } export interface SqlConnectionLimits { diff --git a/packages/grafana-runtime/src/config.ts b/packages/grafana-runtime/src/config.ts index 3d25e0d5b2..8a7dae0694 100644 --- a/packages/grafana-runtime/src/config.ts +++ b/packages/grafana-runtime/src/config.ts @@ -176,6 +176,12 @@ export class GrafanaBootConfig implements GrafanaConfig { localFileSystemAvailable: boolean | undefined; cloudMigrationIsTarget: boolean | undefined; + /** + * Language used in Grafana's UI. This is after the user's preference (or deteceted locale) is resolved to one of + * Grafana's supported language. + */ + language: string | undefined; + constructor(options: GrafanaBootConfig) { this.bootData = options.bootData; diff --git a/public/app/app.ts b/public/app/app.ts index 0de34f8fe6..613b40d560 100644 --- a/public/app/app.ts +++ b/public/app/app.ts @@ -42,7 +42,7 @@ import { setPanelDataErrorView } from '@grafana/runtime/src/components/PanelData import { setPanelRenderer } from '@grafana/runtime/src/components/PanelRenderer'; import { setPluginPage } from '@grafana/runtime/src/components/PluginPage'; import { getScrollbarWidth } from '@grafana/ui'; -import config from 'app/core/config'; +import config, { updateConfig } from 'app/core/config'; import { arrayMove } from 'app/core/utils/arrayMove'; import { getStandardTransformers } from 'app/features/transformers/standardTransformers'; @@ -126,6 +126,7 @@ export class GrafanaApp { parent.postMessage('GrafanaAppInit', '*'); const initI18nPromise = initializeI18n(config.bootData.user.language); + initI18nPromise.then(({ language }) => updateConfig({ language })); setBackendSrv(backendSrv); initEchoSrv(); diff --git a/public/app/core/internationalization/index.tsx b/public/app/core/internationalization/index.tsx index 3265397ce2..1b9d850c6d 100644 --- a/public/app/core/internationalization/index.tsx +++ b/public/app/core/internationalization/index.tsx @@ -3,7 +3,7 @@ import LanguageDetector, { DetectorOptions } from 'i18next-browser-languagedetec import React from 'react'; import { Trans as I18NextTrans, initReactI18next } from 'react-i18next'; // eslint-disable-line no-restricted-imports -import { LANGUAGES, VALID_LANGUAGES } from './constants'; +import { DEFAULT_LANGUAGE, LANGUAGES, VALID_LANGUAGES } from './constants'; const getLanguagePartFromCode = (code: string) => code.split('-')[0].toLowerCase(); @@ -23,7 +23,7 @@ const loadTranslations: BackendModule = { }, }; -export function initializeI18n(language: string) { +export function initializeI18n(language: string): Promise<{ language: string | undefined }> { // This is a placeholder so we can put a 'comment' in the message json files. // Starts with an underscore so it's sorted to the top of the file. Even though it is in a comment the following line is still extracted // t('_comment', 'This file is the source of truth for English strings. Edit this to change plurals and other phrases for the UI.'); @@ -35,19 +35,30 @@ export function initializeI18n(language: string) { // If translations are empty strings (no translation), fall back to the default value in source code returnEmptyString: false, + + // Required to ensure that `resolvedLanguage` is set property when an invalid language is passed (such as through 'detect') + supportedLngs: VALID_LANGUAGES, + fallbackLng: DEFAULT_LANGUAGE, }; - let init = i18n; + let i18nInstance = i18n; if (language === 'detect') { - init = init.use(LanguageDetector); + i18nInstance = i18nInstance.use(LanguageDetector); const detection: DetectorOptions = { order: ['navigator'], caches: [] }; options.detection = detection; } else { options.lng = VALID_LANGUAGES.includes(language) ? language : undefined; } - return init + + const loadPromise = i18nInstance .use(loadTranslations) .use(initReactI18next) // passes i18n down to react-i18next .init(options); + + return loadPromise.then(() => { + return { + language: i18nInstance.resolvedLanguage, + }; + }); } export function changeLanguage(locale: string) { From e394110f446436d2d189caaca5cd60e32558e13f Mon Sep 17 00:00:00 2001 From: Andres Martinez Gotor Date: Mon, 18 Mar 2024 12:08:49 +0100 Subject: [PATCH 0742/1406] Fix api_plugins_test locally (#84484) --- pkg/tests/api/plugins/api_plugins_test.go | 20 ++++++-- .../api/plugins/data/expectedListResp.json | 49 ------------------- 2 files changed, 16 insertions(+), 53 deletions(-) diff --git a/pkg/tests/api/plugins/api_plugins_test.go b/pkg/tests/api/plugins/api_plugins_test.go index 762c72e3f5..94281f8898 100644 --- a/pkg/tests/api/plugins/api_plugins_test.go +++ b/pkg/tests/api/plugins/api_plugins_test.go @@ -11,9 +11,13 @@ import ( "path/filepath" "testing" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/api/dtos" + "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/org/orgimpl" "github.com/grafana/grafana/pkg/services/quota/quotaimpl" "github.com/grafana/grafana/pkg/services/sqlstore" @@ -113,11 +117,13 @@ func TestIntegrationPlugins(t *testing.T) { require.Equal(t, tc.expStatus, resp.StatusCode) b, err := io.ReadAll(resp.Body) require.NoError(t, err) + var result dtos.PluginList + err = json.Unmarshal(b, &result) + require.NoError(t, err) expResp := expectedResp(t, tc.expRespPath) - same := assert.JSONEq(t, expResp, string(b)) - if !same { + if diff := cmp.Diff(expResp, result, cmpopts.IgnoreFields(plugins.Info{}, "Version")); diff != "" { if updateSnapshotFlag { t.Log("updating snapshot results") var prettyJSON bytes.Buffer @@ -126,6 +132,7 @@ func TestIntegrationPlugins(t *testing.T) { } updateRespSnapshot(t, tc.expRespPath, prettyJSON.String()) } + t.Errorf("unexpected response (-want +got):\n%s", diff) t.FailNow() } }) @@ -223,14 +230,19 @@ func grafanaAPIURL(username string, grafanaListedAddr string, path string) strin return fmt.Sprintf("http://%s:%s@%s/api/%s", username, defaultPassword, grafanaListedAddr, path) } -func expectedResp(t *testing.T, filename string) string { +func expectedResp(t *testing.T, filename string) dtos.PluginList { //nolint:GOSEC contents, err := os.ReadFile(filepath.Join("data", filename)) if err != nil { t.Errorf("failed to load %s: %v", filename, err) } - return string(contents) + var result dtos.PluginList + err = json.Unmarshal(contents, &result) + if err != nil { + t.Errorf("failed to unmarshal %s: %v", filename, err) + } + return result } func updateRespSnapshot(t *testing.T, filename string, body string) { diff --git a/pkg/tests/api/plugins/data/expectedListResp.json b/pkg/tests/api/plugins/data/expectedListResp.json index 15ec9fe5d9..2e0adc347b 100644 --- a/pkg/tests/api/plugins/data/expectedListResp.json +++ b/pkg/tests/api/plugins/data/expectedListResp.json @@ -18,7 +18,6 @@ }, "build": {}, "screenshots": null, - "version": "", "updated": "", "keywords": null }, @@ -61,7 +60,6 @@ }, "build": {}, "screenshots": null, - "version": "", "updated": "", "keywords": [ "alerts", @@ -106,7 +104,6 @@ }, "build": {}, "screenshots": null, - "version": "", "updated": "", "keywords": null }, @@ -166,7 +163,6 @@ "path": "public/app/plugins/datasource/azuremonitor/img/azure_monitor_cpu.png" } ], - "version": "", "updated": "", "keywords": [ "azure", @@ -210,7 +206,6 @@ }, "build": {}, "screenshots": null, - "version": "", "updated": "", "keywords": null }, @@ -248,7 +243,6 @@ }, "build": {}, "screenshots": null, - "version": "", "updated": "", "keywords": null }, @@ -286,7 +280,6 @@ }, "build": {}, "screenshots": null, - "version": "", "updated": "", "keywords": [ "financial", @@ -329,7 +322,6 @@ }, "build": {}, "screenshots": null, - "version": "", "updated": "", "keywords": null }, @@ -367,7 +359,6 @@ }, "build": {}, "screenshots": null, - "version": "", "updated": "", "keywords": null }, @@ -405,7 +396,6 @@ }, "build": {}, "screenshots": null, - "version": "", "updated": "", "keywords": null }, @@ -443,7 +433,6 @@ }, "build": {}, "screenshots": null, - "version": "", "updated": "", "keywords": null }, @@ -486,7 +475,6 @@ }, "build": {}, "screenshots": null, - "version": "", "updated": "", "keywords": [ "elasticsearch" @@ -526,7 +514,6 @@ }, "build": {}, "screenshots": null, - "version": "", "updated": "", "keywords": null }, @@ -564,7 +551,6 @@ }, "build": {}, "screenshots": null, - "version": "", "updated": "", "keywords": null }, @@ -602,7 +588,6 @@ }, "build": {}, "screenshots": null, - "version": "", "updated": "", "keywords": null }, @@ -640,7 +625,6 @@ }, "build": {}, "screenshots": null, - "version": "", "updated": "", "keywords": null }, @@ -678,7 +662,6 @@ }, "build": {}, "screenshots": null, - "version": "", "updated": "", "keywords": null }, @@ -721,7 +704,6 @@ }, "build": {}, "screenshots": null, - "version": "", "updated": "", "keywords": [ "grafana", @@ -767,7 +749,6 @@ }, "build": {}, "screenshots": null, - "version": "", "updated": "", "keywords": null }, @@ -814,7 +795,6 @@ }, "build": {}, "screenshots": null, - "version": "", "updated": "", "keywords": null }, @@ -852,7 +832,6 @@ }, "build": {}, "screenshots": null, - "version": "", "updated": "", "keywords": null }, @@ -890,7 +869,6 @@ }, "build": {}, "screenshots": null, - "version": "", "updated": "", "keywords": [ "distribution", @@ -933,7 +911,6 @@ }, "build": {}, "screenshots": null, - "version": "", "updated": "", "keywords": null }, @@ -980,7 +957,6 @@ }, "build": {}, "screenshots": null, - "version": "", "updated": "", "keywords": null }, @@ -1018,7 +994,6 @@ }, "build": {}, "screenshots": null, - "version": "", "updated": "", "keywords": null }, @@ -1065,7 +1040,6 @@ }, "build": {}, "screenshots": null, - "version": "", "updated": "", "keywords": null }, @@ -1103,7 +1077,6 @@ }, "build": {}, "screenshots": null, - "version": "", "updated": "", "keywords": null }, @@ -1141,7 +1114,6 @@ }, "build": {}, "screenshots": null, - "version": "", "updated": "", "keywords": null }, @@ -1179,7 +1151,6 @@ }, "build": {}, "screenshots": null, - "version": "", "updated": "", "keywords": null }, @@ -1217,7 +1188,6 @@ }, "build": {}, "screenshots": null, - "version": "", "updated": "", "keywords": null }, @@ -1255,7 +1225,6 @@ }, "build": {}, "screenshots": null, - "version": "", "updated": "", "keywords": null }, @@ -1298,7 +1267,6 @@ }, "build": {}, "screenshots": null, - "version": "", "updated": "", "keywords": [ "grafana", @@ -1341,7 +1309,6 @@ }, "build": {}, "screenshots": null, - "version": "", "updated": "", "keywords": null }, @@ -1379,7 +1346,6 @@ }, "build": {}, "screenshots": null, - "version": "", "updated": "", "keywords": null }, @@ -1422,7 +1388,6 @@ }, "build": {}, "screenshots": null, - "version": "", "updated": "", "keywords": null }, @@ -1460,7 +1425,6 @@ }, "build": {}, "screenshots": null, - "version": "", "updated": "", "keywords": null }, @@ -1498,7 +1462,6 @@ }, "build": {}, "screenshots": null, - "version": "", "updated": "", "keywords": null }, @@ -1536,7 +1499,6 @@ }, "build": {}, "screenshots": null, - "version": "", "updated": "", "keywords": null }, @@ -1574,7 +1536,6 @@ }, "build": {}, "screenshots": null, - "version": "", "updated": "", "keywords": null }, @@ -1612,7 +1573,6 @@ }, "build": {}, "screenshots": null, - "version": "", "updated": "", "keywords": null }, @@ -1655,7 +1615,6 @@ }, "build": {}, "screenshots": null, - "version": "", "updated": "", "keywords": null }, @@ -1693,7 +1652,6 @@ }, "build": {}, "screenshots": null, - "version": "", "updated": "", "keywords": null }, @@ -1731,7 +1689,6 @@ }, "build": {}, "screenshots": null, - "version": "", "updated": "", "keywords": null }, @@ -1769,7 +1726,6 @@ }, "build": {}, "screenshots": null, - "version": "", "updated": "", "keywords": null }, @@ -1807,7 +1763,6 @@ }, "build": {}, "screenshots": null, - "version": "", "updated": "", "keywords": null }, @@ -1845,7 +1800,6 @@ }, "build": {}, "screenshots": null, - "version": "", "updated": "", "keywords": null }, @@ -1883,7 +1837,6 @@ }, "build": {}, "screenshots": null, - "version": "", "updated": "", "keywords": null }, @@ -1921,7 +1874,6 @@ }, "build": {}, "screenshots": null, - "version": "", "updated": "", "keywords": [ "scatter", @@ -1967,7 +1919,6 @@ }, "build": {}, "screenshots": null, - "version": "", "updated": "", "keywords": null }, From 6241386a96597866ad3adad1a5066b0a5998709f Mon Sep 17 00:00:00 2001 From: Andre Pereira Date: Mon, 18 Mar 2024 11:38:17 +0000 Subject: [PATCH 0743/1406] Data Trails: Sticky main metric graph (#84389) * WIP * Refactor code a bit so we can sticky the main graph and tabs * Make sure it works in Firefox. Avoid annoying warnings in breakdown tab. Update pin metrics graph label * Small copy change Co-authored-by: Darren Janeczek <38694490+darrenjaneczek@users.noreply.github.com> --------- Co-authored-by: Darren Janeczek <38694490+darrenjaneczek@users.noreply.github.com> --- .../trails/ActionTabs/BreakdownScene.tsx | 7 +- .../trails/ActionTabs/MetricOverviewScene.tsx | 5 +- .../trails/ActionTabs/RelatedMetricsScene.tsx | 6 +- .../AutomaticMetricQueries/AutoVizPanel.tsx | 40 +------- public/app/features/trails/DataTrail.tsx | 3 +- .../app/features/trails/DataTrailSettings.tsx | 29 ++---- .../app/features/trails/MetricGraphScene.tsx | 92 +++++++++++++++++++ public/app/features/trails/MetricScene.tsx | 45 ++------- 8 files changed, 119 insertions(+), 108 deletions(-) create mode 100644 public/app/features/trails/MetricGraphScene.tsx diff --git a/public/app/features/trails/ActionTabs/BreakdownScene.tsx b/public/app/features/trails/ActionTabs/BreakdownScene.tsx index a5b282c801..848c808117 100644 --- a/public/app/features/trails/ActionTabs/BreakdownScene.tsx +++ b/public/app/features/trails/ActionTabs/BreakdownScene.tsx @@ -240,7 +240,8 @@ export function buildAllLayout(options: Array>, queryDef new SceneCSSGridLayout({ templateColumns: '1fr', autoRows: '200px', - children: children, + // Clone children since a scene object can only have one parent at a time + children: children.map((c) => c.clone()), }), ], }); @@ -323,9 +324,7 @@ function getLabelValue(frame: DataFrame) { } export function buildBreakdownActionScene() { - return new SceneFlexItem({ - body: new BreakdownScene({}), - }); + return new BreakdownScene({}); } interface SelectLabelActionState extends SceneObjectState { diff --git a/public/app/features/trails/ActionTabs/MetricOverviewScene.tsx b/public/app/features/trails/ActionTabs/MetricOverviewScene.tsx index 5adc3d83bc..5f469eb9da 100644 --- a/public/app/features/trails/ActionTabs/MetricOverviewScene.tsx +++ b/public/app/features/trails/ActionTabs/MetricOverviewScene.tsx @@ -3,7 +3,6 @@ import React from 'react'; import { QueryVariable, SceneComponentProps, - SceneFlexItem, sceneGraph, SceneObjectBase, SceneObjectState, @@ -130,7 +129,5 @@ export class MetricOverviewScene extends SceneObjectBase { public static Component = ({ model }: SceneComponentProps) => { const { panel } = model.useState(); - const { queryDef } = getMetricSceneFor(model).state; - const { showQuery } = getTrailSettings(model).useState(); - const styles = useStyles2(getStyles); if (!panel) { return; } - - if (!showQuery) { - return ; - } - - return ( -
- - -
{queryDef && queryDef.queries.map((query, index) =>
{query.expr}
)}
-
-
-
- -
-
- ); - }; -} - -function getStyles() { - return { - wrapper: css({ - display: 'flex', - flexDirection: 'column', - flexGrow: 1, - }), - panel: css({ - position: 'relative', - flexGrow: 1, - }), + return ; }; } diff --git a/public/app/features/trails/DataTrail.tsx b/public/app/features/trails/DataTrail.tsx index bbdca58404..a879ba0e6d 100644 --- a/public/app/features/trails/DataTrail.tsx +++ b/public/app/features/trails/DataTrail.tsx @@ -172,7 +172,7 @@ export class DataTrail extends SceneObjectBase { } static Component = ({ model }: SceneComponentProps) => { - const { controls, topScene, history } = model.useState(); + const { controls, topScene, history, settings } = model.useState(); const styles = useStyles2(getStyles); const showHeaderForFirstTimeUsers = getTrailStore().recent.length < 2; @@ -185,6 +185,7 @@ export class DataTrail extends SceneObjectBase { {controls.map((control) => ( ))} +
)}
{topScene && }
diff --git a/public/app/features/trails/DataTrailSettings.tsx b/public/app/features/trails/DataTrailSettings.tsx index 6450a9fed9..60056bf40d 100644 --- a/public/app/features/trails/DataTrailSettings.tsx +++ b/public/app/features/trails/DataTrailSettings.tsx @@ -6,31 +6,20 @@ import { SceneComponentProps, SceneObjectBase, SceneObjectState } from '@grafana import { Dropdown, Switch, ToolbarButton, useStyles2 } from '@grafana/ui'; export interface DataTrailSettingsState extends SceneObjectState { - showQuery?: boolean; - showAdvanced?: boolean; - multiValueVars?: boolean; + stickyMainGraph?: boolean; isOpen?: boolean; } export class DataTrailSettings extends SceneObjectBase { constructor(state: Partial) { super({ - showQuery: state.showQuery ?? false, - showAdvanced: state.showAdvanced ?? false, + stickyMainGraph: state.stickyMainGraph ?? true, isOpen: state.isOpen ?? false, }); } - public onToggleShowQuery = () => { - this.setState({ showQuery: !this.state.showQuery }); - }; - - public onToggleAdvanced = () => { - this.setState({ showAdvanced: !this.state.showAdvanced }); - }; - - public onToggleMultiValue = () => { - this.setState({ multiValueVars: !this.state.multiValueVars }); + public onToggleStickyMainGraph = () => { + this.setState({ stickyMainGraph: !this.state.stickyMainGraph }); }; public onToggleOpen = (isOpen: boolean) => { @@ -38,7 +27,7 @@ export class DataTrailSettings extends SceneObjectBase { }; static Component = ({ model }: SceneComponentProps) => { - const { showQuery, showAdvanced, multiValueVars, isOpen } = model.useState(); + const { stickyMainGraph, isOpen } = model.useState(); const styles = useStyles2(getStyles); const renderPopover = () => { @@ -47,12 +36,8 @@ export class DataTrailSettings extends SceneObjectBase {
evt.stopPropagation()}>
Settings
-
Multi value variables
- -
Advanced options
- -
Show query
- +
Always keep selected metric graph in-view
+
); diff --git a/public/app/features/trails/MetricGraphScene.tsx b/public/app/features/trails/MetricGraphScene.tsx new file mode 100644 index 0000000000..7cb03d369a --- /dev/null +++ b/public/app/features/trails/MetricGraphScene.tsx @@ -0,0 +1,92 @@ +import { css } from '@emotion/css'; +import React from 'react'; + +import { DashboardCursorSync, GrafanaTheme2 } from '@grafana/data'; +import { + behaviors, + SceneComponentProps, + SceneFlexItem, + SceneFlexLayout, + SceneObject, + SceneObjectBase, + SceneObjectState, +} from '@grafana/scenes'; +import { useStyles2 } from '@grafana/ui'; + +import { AutoVizPanel } from './AutomaticMetricQueries/AutoVizPanel'; +import { MetricActionBar } from './MetricScene'; +import { getTrailSettings } from './utils'; + +export const MAIN_PANEL_MIN_HEIGHT = 280; +export const MAIN_PANEL_MAX_HEIGHT = '40%'; + +export interface MetricGraphSceneState extends SceneObjectState { + topView: SceneFlexLayout; + selectedTab?: SceneObject; +} + +export class MetricGraphScene extends SceneObjectBase { + public constructor(state: Partial) { + super({ + topView: state.topView ?? buildGraphTopView(), + ...state, + }); + } + + public static Component = ({ model }: SceneComponentProps) => { + const { topView, selectedTab } = model.useState(); + const { stickyMainGraph } = getTrailSettings(model).useState(); + const styles = useStyles2(getStyles); + + return ( +
+
+ +
+ {selectedTab && } +
+ ); + }; +} + +function getStyles(theme: GrafanaTheme2) { + return { + container: css({ + display: 'flex', + flexDirection: 'column', + position: 'relative', + }), + sticky: css({ + display: 'flex', + flexDirection: 'row', + background: theme.isLight ? theme.colors.background.primary : theme.colors.background.canvas, + position: 'sticky', + top: '70px', + zIndex: 10, + }), + nonSticky: css({ + display: 'flex', + flexDirection: 'row', + }), + }; +} + +function buildGraphTopView() { + const bodyAutoVizPanel = new AutoVizPanel({}); + + return new SceneFlexLayout({ + direction: 'column', + $behaviors: [new behaviors.CursorSync({ key: 'metricCrosshairSync', sync: DashboardCursorSync.Crosshair })], + children: [ + new SceneFlexItem({ + minHeight: MAIN_PANEL_MIN_HEIGHT, + maxHeight: MAIN_PANEL_MAX_HEIGHT, + body: bodyAutoVizPanel, + }), + new SceneFlexItem({ + ySizing: 'content', + body: new MetricActionBar({}), + }), + ], + }); +} diff --git a/public/app/features/trails/MetricScene.tsx b/public/app/features/trails/MetricScene.tsx index 25bf96b2fc..1dcd2c01f2 100644 --- a/public/app/features/trails/MetricScene.tsx +++ b/public/app/features/trails/MetricScene.tsx @@ -1,21 +1,18 @@ import { css } from '@emotion/css'; import React from 'react'; -import { DashboardCursorSync, GrafanaTheme2 } from '@grafana/data'; +import { GrafanaTheme2 } from '@grafana/data'; import { SceneObjectState, SceneObjectBase, SceneComponentProps, - SceneFlexLayout, - SceneFlexItem, SceneObjectUrlSyncConfig, SceneObjectUrlValues, sceneGraph, SceneVariableSet, QueryVariable, - behaviors, } from '@grafana/scenes'; -import { ToolbarButton, Box, Stack, Icon, TabsBar, Tab, useStyles2 } from '@grafana/ui'; +import { ToolbarButton, Stack, Icon, TabsBar, Tab, useStyles2, Box } from '@grafana/ui'; import { getExploreUrl } from '../../core/utils/explore'; @@ -23,8 +20,8 @@ import { buildBreakdownActionScene } from './ActionTabs/BreakdownScene'; import { buildMetricOverviewScene } from './ActionTabs/MetricOverviewScene'; import { buildRelatedMetricsScene } from './ActionTabs/RelatedMetricsScene'; import { getAutoQueriesForMetric } from './AutomaticMetricQueries/AutoQueryEngine'; -import { AutoVizPanel } from './AutomaticMetricQueries/AutoVizPanel'; import { AutoQueryDef, AutoQueryInfo } from './AutomaticMetricQueries/types'; +import { MAIN_PANEL_MAX_HEIGHT, MAIN_PANEL_MIN_HEIGHT, MetricGraphScene } from './MetricGraphScene'; import { ShareTrailButton } from './ShareTrailButton'; import { useBookmarkState } from './TrailStore/useBookmarkState'; import { @@ -40,7 +37,7 @@ import { import { getDataSource, getTrailFor } from './utils'; export interface MetricSceneState extends SceneObjectState { - body: SceneFlexLayout; + body: MetricGraphScene; metric: string; actionView?: string; @@ -55,7 +52,7 @@ export class MetricScene extends SceneObjectBase { const autoQuery = state.autoQuery ?? getAutoQueriesForMetric(state.metric); super({ $variables: state.$variables ?? getVariableSet(state.metric), - body: state.body ?? buildGraphScene(), + body: state.body ?? new MetricGraphScene({}), autoQuery, queryDef: state.queryDef ?? autoQuery.main, ...state, @@ -93,13 +90,13 @@ export class MetricScene extends SceneObjectBase { if (actionViewDef && actionViewDef.value !== this.state.actionView) { // reduce max height for main panel to reduce height flicker - body.state.children[0].setState({ maxHeight: MAIN_PANEL_MIN_HEIGHT }); - body.setState({ children: [...body.state.children.slice(0, 2), actionViewDef.getScene()] }); + body.state.topView.state.children[0].setState({ maxHeight: MAIN_PANEL_MIN_HEIGHT }); + body.setState({ selectedTab: actionViewDef.getScene() }); this.setState({ actionView: actionViewDef.value }); } else { // restore max height - body.state.children[0].setState({ maxHeight: MAIN_PANEL_MAX_HEIGHT }); - body.setState({ children: body.state.children.slice(0, 2) }); + body.state.topView.state.children[0].setState({ maxHeight: MAIN_PANEL_MAX_HEIGHT }); + body.setState({ selectedTab: undefined }); this.setState({ actionView: undefined }); } } @@ -208,6 +205,7 @@ function getStyles(theme: GrafanaTheme2) { [theme.breakpoints.up(theme.breakpoints.values.md)]: { position: 'absolute', right: 0, + top: 16, zIndex: 2, }, }), @@ -231,26 +229,3 @@ function getVariableSet(metric: string) { ], }); } - -const MAIN_PANEL_MIN_HEIGHT = 280; -const MAIN_PANEL_MAX_HEIGHT = '40%'; - -function buildGraphScene() { - const bodyAutoVizPanel = new AutoVizPanel({}); - - return new SceneFlexLayout({ - direction: 'column', - $behaviors: [new behaviors.CursorSync({ key: 'metricCrosshairSync', sync: DashboardCursorSync.Crosshair })], - children: [ - new SceneFlexItem({ - minHeight: MAIN_PANEL_MIN_HEIGHT, - maxHeight: MAIN_PANEL_MAX_HEIGHT, - body: bodyAutoVizPanel, - }), - new SceneFlexItem({ - ySizing: 'content', - body: new MetricActionBar({}), - }), - ], - }); -} From 26e1a5887ac4116a5e9c50d35e0132dc81a706d9 Mon Sep 17 00:00:00 2001 From: Sergej-Vlasov <37613182+Sergej-Vlasov@users.noreply.github.com> Date: Mon, 18 Mar 2024 14:02:12 +0200 Subject: [PATCH 0744/1406] DashboardScene: Reset editIndex on variable delete (#84589) * reset edit index on variable delete * adjust delete variable test * adjust test to be more in line with user flow --- .../dashboard-scene/settings/VariablesEditView.test.tsx | 5 +++++ .../features/dashboard-scene/settings/VariablesEditView.tsx | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/public/app/features/dashboard-scene/settings/VariablesEditView.test.tsx b/public/app/features/dashboard-scene/settings/VariablesEditView.test.tsx index 7ecc3ee306..ac39ac3c60 100644 --- a/public/app/features/dashboard-scene/settings/VariablesEditView.test.tsx +++ b/public/app/features/dashboard-scene/settings/VariablesEditView.test.tsx @@ -146,9 +146,14 @@ describe('VariablesEditView', () => { it('should delete a variable', () => { const variableIdentifier = 'customVar'; + + variableView.onEdit(variableIdentifier); + expect(variableView.state.editIndex).toBe(0); + variableView.onDelete(variableIdentifier); expect(variableView.getVariables()).toHaveLength(2); expect(variableView.getVariables()[0].state.name).toBe('customVar2'); + expect(variableView.state.editIndex).toBeUndefined(); }); it('should change order of variables', () => { diff --git a/public/app/features/dashboard-scene/settings/VariablesEditView.tsx b/public/app/features/dashboard-scene/settings/VariablesEditView.tsx index c6c97d9655..be8dea55b3 100644 --- a/public/app/features/dashboard-scene/settings/VariablesEditView.tsx +++ b/public/app/features/dashboard-scene/settings/VariablesEditView.tsx @@ -78,6 +78,8 @@ export class VariablesEditView extends SceneObjectBase i // Update the state or the variables array this.getVariableSet().setState({ variables: updatedVariables }); + // Remove editIndex otherwise switches to next variable in list + this.setState({ editIndex: undefined }); }; public getVariables() { @@ -278,6 +280,8 @@ function VariableEditorSettingsView({ onGoBack={onGoBack} onDelete={onDelete} onValidateVariableName={onValidateVariableName} + // force refresh when navigating using back/forward between variables + key={variable.state.key} /> ); From 3297d589c06cfa44e202c4e14790026330404704 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Mon, 18 Mar 2024 12:04:43 +0000 Subject: [PATCH 0745/1406] ConfirmButton: Stop pointerEvents on the correct element (#84648) stop pointerEvents on the correct element --- .../grafana-ui/src/components/ConfirmButton/ConfirmButton.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/grafana-ui/src/components/ConfirmButton/ConfirmButton.tsx b/packages/grafana-ui/src/components/ConfirmButton/ConfirmButton.tsx index da6705b0ad..850da68b49 100644 --- a/packages/grafana-ui/src/components/ConfirmButton/ConfirmButton.tsx +++ b/packages/grafana-ui/src/components/ConfirmButton/ConfirmButton.tsx @@ -151,17 +151,18 @@ const getStyles = (theme: GrafanaTheme2) => { confirmButtonContainer: css({ overflow: 'visible', position: 'absolute', + pointerEvents: 'all', right: 0, }), confirmButtonContainerHide: css({ overflow: 'hidden', + pointerEvents: 'none', }), confirmButton: css({ alignItems: 'flex-start', background: theme.colors.background.primary, display: 'flex', opacity: 1, - pointerEvents: 'all', transform: 'translateX(0)', transition: theme.transitions.create(['opacity', 'transform'], { duration: theme.transitions.duration.shortest, @@ -171,7 +172,6 @@ const getStyles = (theme: GrafanaTheme2) => { }), confirmButtonHide: css({ opacity: 0, - pointerEvents: 'none', transform: 'translateX(100%)', transition: theme.transitions.create(['opacity', 'transform', 'visibility'], { duration: theme.transitions.duration.shortest, From a7c7a1ffed7c49c38bcc493dee7d221e7bf5cdfe Mon Sep 17 00:00:00 2001 From: Pepe Cano <825430+ppcano@users.noreply.github.com> Date: Mon, 18 Mar 2024 13:18:57 +0100 Subject: [PATCH 0746/1406] Alerting docs: Fix format issues in recent Slack tutorial (#84651) * Alerting docs: Fix format issues in Slack tutorial * Alerting docs: Include link to Slack docs * Alerting docs: fix Slack `nested-policy` link --- .../manage-contact-points/_index.md | 5 ++++- .../integrations/configure-slack.md | 11 ++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/docs/sources/alerting/configure-notifications/manage-contact-points/_index.md b/docs/sources/alerting/configure-notifications/manage-contact-points/_index.md index a2ae778c03..0bb2064e0a 100644 --- a/docs/sources/alerting/configure-notifications/manage-contact-points/_index.md +++ b/docs/sources/alerting/configure-notifications/manage-contact-points/_index.md @@ -123,7 +123,7 @@ Once configured, you can use integrations as part of your contact points to rece | Pushover | `pushover` | | Sensu | `sensu` | | Sensu Go | `sensugo` | -| Slack | `slack` | +| [Slack][slack] | `slack` | | Telegram | `telegram` | | Threema | `threema` | | VictorOps | `victorops` | @@ -136,6 +136,9 @@ Once configured, you can use integrations as part of your contact points to rece [oncall]: "/docs/grafana/ -> /docs/grafana//alerting/configure-notifications/manage-contact-points/integrations/configure-oncall" [oncall]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/manage-contact-points/integrations/configure-oncall" +[slack]: "/docs/grafana/ -> /docs/grafana//alerting/configure-notifications/manage-contact-points/integrations/configure-slack" +[slack]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/manage-contact-points/integrations/configure-slack" + [webhook]: "/docs/grafana/ -> /docs/grafana//alerting/configure-notifications/manage-contact-points/integrations/webhook-notifier" [webhook]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/manage-contact-points/integrations/webhook-notifier" {{% /docs/reference %}} diff --git a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-slack.md b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-slack.md index bb0aed4bba..f08ab60fda 100644 --- a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-slack.md +++ b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-slack.md @@ -24,11 +24,12 @@ There are two ways of integrating Slack into Grafana Alerting. 1. Use a [Slack API token](https://api.slack.com/authentication/token-types) -Enable your app to access the Slack API. If, for example, you are interested in more granular control over permissions, or your project is expected to regularly scale, resulting in new channels being created, this is the best option. + Enable your app to access the Slack API. If, for example, you are interested in more granular control over permissions, or your project is expected to regularly scale, resulting in new channels being created, this is the best option. 1. Use a [Webhook URL](https://api.slack.com/messaging/webhooks) -Webhooks is the simpler way to post messages into Slack. Slack automatically creates a bot user with all the necessary permissions to post messages to one particular channel of your choice. + Webhooks is the simpler way to post messages into Slack. Slack automatically creates a bot user with all the necessary permissions to post messages to one particular channel of your choice. + {{< admonition type="note" >}} Grafana Alerting only allows one Slack channel per contact point. {{< /admonition >}} @@ -71,7 +72,7 @@ To create your Slack integration in Grafana Alerting, complete the following ste - In the **Token** field, copy in the Bot User OAuth Token that starts with “xoxb-”. 1. If you are using a Webhook URL, in the **Webhook** field, copy in your Slack app Webhook URL. 1. Click **Test** to check that your integration works. - 1[]. Click **Save contact point**. +1. Click **Save contact point**. ## Next steps @@ -86,7 +87,7 @@ To add the contact point and integration you created to your default notificatio If you have more than one contact point, add a new notification policy rather than edit the default one, so you can route specific alerts to Slack. For more information, refer to [Notification policies][nested-policy]. {{% docs/reference %}} -[nested-policy]: "/docs/grafana/ -> /docs/grafana//alerting/alerting-rules/create-notification-policy/#add-new-nested-policy" +[nested-policy]: "/docs/grafana/ -> /docs/grafana//alerting/configure-notifications/create-notification-policy#add-new-nested-policy" -[nested-policy]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/create-notification-policy/#add-new-nested-policy" +[nested-policy]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/create-notification-policy#add-new-nested-policy" {{% /docs/reference %}} From 3e97999ac5a3270798a4fe330557ca49b5eba654 Mon Sep 17 00:00:00 2001 From: Matias Chomicki Date: Mon, 18 Mar 2024 13:24:06 +0100 Subject: [PATCH 0747/1406] LogRowMessageDisplayedFields: increase rendering performance (#84407) * getAllFields: refactor for improved performance * LogRowMessageDisplayedFields: refactor line construction for performance * AsyncIconButton: refactor to prevent infinite loops --- .../logs/components/LogDetailsRow.tsx | 13 ++- .../LogRowMessageDisplayedFields.tsx | 40 ++++---- .../app/features/logs/components/logParser.ts | 94 +++++++++++-------- 3 files changed, 78 insertions(+), 69 deletions(-) diff --git a/public/app/features/logs/components/LogDetailsRow.tsx b/public/app/features/logs/components/LogDetailsRow.tsx index ac1ef650e5..37fab48ce9 100644 --- a/public/app/features/logs/components/LogDetailsRow.tsx +++ b/public/app/features/logs/components/LogDetailsRow.tsx @@ -1,7 +1,7 @@ import { css, cx } from '@emotion/css'; import { isEqual } from 'lodash'; import memoizeOne from 'memoize-one'; -import React, { PureComponent, useState } from 'react'; +import React, { PureComponent, useEffect, useState } from 'react'; import { CoreApp, @@ -290,7 +290,8 @@ class UnThemedLogDetailsRow extends PureComponent { this.isFilterLabelActive()} tooltipSuffix={refIdTooltip} /> { + isActive().then(setActive); + }, [isActive]); return ; }; diff --git a/public/app/features/logs/components/LogRowMessageDisplayedFields.tsx b/public/app/features/logs/components/LogRowMessageDisplayedFields.tsx index 2a9d154b1e..18837d9292 100644 --- a/public/app/features/logs/components/LogRowMessageDisplayedFields.tsx +++ b/public/app/features/logs/components/LogRowMessageDisplayedFields.tsx @@ -25,32 +25,28 @@ export interface Props { export const LogRowMessageDisplayedFields = React.memo((props: Props) => { const { row, detectedFields, getFieldLinks, wrapLogMessage, styles, mouseIsOver, pinned, ...rest } = props; - const fields = getAllFields(row, getFieldLinks); const wrapClassName = wrapLogMessage ? '' : displayedFieldsStyles.noWrap; + const fields = useMemo(() => getAllFields(row, getFieldLinks), [getFieldLinks, row]); // only single key/value rows are filterable, so we only need the first field key for filtering - const line = useMemo( - () => - detectedFields - .map((parsedKey) => { - const field = fields.find((field) => { - const { keys } = field; - return keys[0] === parsedKey; - }); + const line = useMemo(() => { + let line = ''; + for (let i = 0; i < detectedFields.length; i++) { + const parsedKey = detectedFields[i]; + const field = fields.find((field) => { + const { keys } = field; + return keys[0] === parsedKey; + }); - if (field !== undefined && field !== null) { - return `${parsedKey}=${field.values}`; - } + if (field) { + line += ` ${parsedKey}=${field.values}`; + } - if (row.labels[parsedKey] !== undefined && row.labels[parsedKey] !== null) { - return `${parsedKey}=${row.labels[parsedKey]}`; - } - - return null; - }) - .filter((s) => s !== null) - .join(' '), - [detectedFields, fields, row.labels] - ); + if (row.labels[parsedKey] !== undefined && row.labels[parsedKey] !== null) { + line += ` ${parsedKey}=${row.labels[parsedKey]}`; + } + } + return line.trimStart(); + }, [detectedFields, fields, row.labels]); const shouldShowMenu = useMemo(() => mouseIsOver || pinned, [mouseIsOver, pinned]); diff --git a/public/app/features/logs/components/logParser.ts b/public/app/features/logs/components/logParser.ts index 7e541ca3e5..6f412f070f 100644 --- a/public/app/features/logs/components/logParser.ts +++ b/public/app/features/logs/components/logParser.ts @@ -1,5 +1,4 @@ import { partition } from 'lodash'; -import memoizeOne from 'memoize-one'; import { DataFrame, Field, FieldWithIndex, LinkModel, LogRowModel } from '@grafana/data'; import { safeStringifyValue } from 'app/core/utils/explore'; @@ -18,26 +17,22 @@ export type FieldDef = { * Returns all fields for log row which consists of fields we parse from the message itself and additional fields * found in the dataframe (they may contain links). */ -export const getAllFields = memoizeOne( - ( - row: LogRowModel, - getFieldLinks?: ( - field: Field, - rowIndex: number, - dataFrame: DataFrame - ) => Array> | ExploreFieldLinkModel[] - ) => { - const dataframeFields = getDataframeFields(row, getFieldLinks); - - return Object.values(dataframeFields); - } -); +export const getAllFields = ( + row: LogRowModel, + getFieldLinks?: ( + field: Field, + rowIndex: number, + dataFrame: DataFrame + ) => Array> | ExploreFieldLinkModel[] +) => { + return getDataframeFields(row, getFieldLinks); +}; /** * A log line may contain many links that would all need to go on their own logs detail row * This iterates through and creates a FieldDef (row) per link. */ -export const createLogLineLinks = memoizeOne((hiddenFieldsWithLinks: FieldDef[]): FieldDef[] => { +export const createLogLineLinks = (hiddenFieldsWithLinks: FieldDef[]): FieldDef[] => { let fieldsWithLinksFromVariableMap: FieldDef[] = []; hiddenFieldsWithLinks.forEach((linkField) => { linkField.links?.forEach((link: ExploreFieldLinkModel) => { @@ -58,34 +53,29 @@ export const createLogLineLinks = memoizeOne((hiddenFieldsWithLinks: FieldDef[]) }); }); return fieldsWithLinksFromVariableMap; -}); +}; /** * creates fields from the dataframe-fields, adding data-links, when field.config.links exists */ -export const getDataframeFields = memoizeOne( - ( - row: LogRowModel, - getFieldLinks?: (field: Field, rowIndex: number, dataFrame: DataFrame) => Array> - ): FieldDef[] => { - const visibleFields = separateVisibleFields(row.dataFrame).visible; - const nonEmptyVisibleFields = visibleFields.filter((f) => f.values[row.rowIndex] != null); - return nonEmptyVisibleFields.map((field) => { - const links = getFieldLinks ? getFieldLinks(field, row.rowIndex, row.dataFrame) : []; - const fieldVal = field.values[row.rowIndex]; - const outputVal = - typeof fieldVal === 'string' || typeof fieldVal === 'number' - ? fieldVal.toString() - : safeStringifyValue(fieldVal); - return { - keys: [field.name], - values: [outputVal], - links: links, - fieldIndex: field.index, - }; - }); - } -); +export const getDataframeFields = ( + row: LogRowModel, + getFieldLinks?: (field: Field, rowIndex: number, dataFrame: DataFrame) => Array> +): FieldDef[] => { + const nonEmptyVisibleFields = getNonEmptyVisibleFields(row); + return nonEmptyVisibleFields.map((field) => { + const links = getFieldLinks ? getFieldLinks(field, row.rowIndex, row.dataFrame) : []; + const fieldVal = field.values[row.rowIndex]; + const outputVal = + typeof fieldVal === 'string' || typeof fieldVal === 'number' ? fieldVal.toString() : safeStringifyValue(fieldVal); + return { + keys: [field.name], + values: [outputVal], + links: links, + fieldIndex: field.index, + }; + }); +}; type VisOptions = { keepTimestamp?: boolean; @@ -148,3 +138,27 @@ export function separateVisibleFields( return { visible, hidden }; } + +// Optimized version of separateVisibleFields() to only return visible fields for getAllFields() +function getNonEmptyVisibleFields(row: LogRowModel, opts?: VisOptions): FieldWithIndex[] { + const frame = row.dataFrame; + const visibleFieldIndices = getVisibleFieldIndices(frame, opts ?? {}); + const visibleFields: FieldWithIndex[] = []; + for (let index = 0; index < frame.fields.length; index++) { + const field = frame.fields[index]; + // ignore empty fields + if (field.values[row.rowIndex] == null) { + continue; + } + // hidden fields are always hidden + if (field.config.custom?.hidden) { + continue; + } + + // fields with data-links are visible + if ((field.config.links && field.config.links.length > 0) || visibleFieldIndices.has(index)) { + visibleFields.push({ ...field, index }); + } + } + return visibleFields; +} From 4ca68925a1ff3d022acbf6260fd9d11fe1573812 Mon Sep 17 00:00:00 2001 From: Jack Westbrook Date: Mon, 18 Mar 2024 13:28:24 +0100 Subject: [PATCH 0748/1406] Backend: Delete bundled plugin tests (#84646) --- .../plugins_integration_test.go | 32 +------------------ 1 file changed, 1 insertion(+), 31 deletions(-) diff --git a/pkg/services/pluginsintegration/plugins_integration_test.go b/pkg/services/pluginsintegration/plugins_integration_test.go index 88ba9199b1..8cdc1217c8 100644 --- a/pkg/services/pluginsintegration/plugins_integration_test.go +++ b/pkg/services/pluginsintegration/plugins_integration_test.go @@ -98,7 +98,6 @@ func TestIntegrationPluginManager(t *testing.T) { ctx := context.Background() verifyCorePluginCatalogue(t, ctx, testCtx.PluginStore) - verifyBundledPlugins(t, ctx, testCtx.PluginStore) verifyPluginStaticRoutes(t, ctx, testCtx.PluginStore, testCtx.PluginStore) verifyBackendProcesses(t, testCtx.PluginRegistry.Plugins(ctx)) verifyPluginQuery(t, ctx, testCtx.PluginClient) @@ -185,7 +184,6 @@ func verifyCorePluginCatalogue(t *testing.T, ctx context.Context, ps *pluginstor "grafana": {}, "alertmanager": {}, "dashboard": {}, - "input": {}, "jaeger": {}, "mixed": {}, "zipkin": {}, @@ -227,41 +225,13 @@ func verifyCorePluginCatalogue(t *testing.T, ctx context.Context, ps *pluginstor require.Equal(t, len(expPanels)+len(expDataSources)+len(expApps), len(ps.Plugins(ctx))) } -func verifyBundledPlugins(t *testing.T, ctx context.Context, ps *pluginstore.Service) { - t.Helper() - - dsPlugins := make(map[string]struct{}) - for _, p := range ps.Plugins(ctx, plugins.TypeDataSource) { - dsPlugins[p.ID] = struct{}{} - } - - inputPlugin, exists := ps.Plugin(ctx, "input") - require.True(t, exists) - require.NotEqual(t, pluginstore.Plugin{}, inputPlugin) - require.NotNil(t, dsPlugins["input"]) - - pluginRoutes := make(map[string]*plugins.StaticRoute) - for _, r := range ps.Routes(ctx) { - pluginRoutes[r.PluginID] = r - } - - for _, pluginID := range []string{"input"} { - require.Contains(t, pluginRoutes, pluginID) - require.Equal(t, pluginRoutes[pluginID].Directory, inputPlugin.Base()) - } -} - func verifyPluginStaticRoutes(t *testing.T, ctx context.Context, rr plugins.StaticRouteResolver, ps *pluginstore.Service) { routes := make(map[string]*plugins.StaticRoute) for _, route := range rr.Routes(ctx) { routes[route.PluginID] = route } - require.Len(t, routes, 2) - - inputPlugin, _ := ps.Plugin(ctx, "input") - require.NotNil(t, routes["input"]) - require.Equal(t, routes["input"].Directory, inputPlugin.Base()) + require.Len(t, routes, 1) testAppPlugin, _ := ps.Plugin(ctx, "test-app") require.Contains(t, routes, "test-app") From 7aa0ba8c59b449009789348fc30efcb1d51305df Mon Sep 17 00:00:00 2001 From: Ieva Date: Mon, 18 Mar 2024 12:52:01 +0000 Subject: [PATCH 0749/1406] Teams: Display teams page to team reader if they also have the access to list team permissions (#84650) * display teams to team reader if they also have the access to list team permissions * fix a typo in the docs --- .../custom-role-actions-scopes/index.md | 268 +++++++++--------- pkg/services/accesscontrol/models.go | 1 + 2 files changed, 135 insertions(+), 134 deletions(-) diff --git a/docs/sources/administration/roles-and-permissions/access-control/custom-role-actions-scopes/index.md b/docs/sources/administration/roles-and-permissions/access-control/custom-role-actions-scopes/index.md index 22da60ed02..3d2751e400 100644 --- a/docs/sources/administration/roles-and-permissions/access-control/custom-role-actions-scopes/index.md +++ b/docs/sources/administration/roles-and-permissions/access-control/custom-role-actions-scopes/index.md @@ -29,140 +29,140 @@ To learn more about the Grafana resources to which you can apply RBAC, refer to The following list contains role-based access control actions. -| Action | Applicable scope | Description | -| ------------------------------------ | --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `alert.instances.external:read` | `datasources:*`
`datasources:uid:*` | Read alerts and silences in data sources that support alerting. | -| `alert.instances.external:write` | `datasources:*`
`datasources:uid:*` | Manage alerts and silences in data sources that support alerting. | -| `alert.instances:create` | n/a | Create silences in the current organization. | -| `alert.instances:read` | n/a | Read alerts and silences in the current organization. | -| `alert.instances:write` | n/a | Update and expire silences in the current organization. | -| `alert.notifications.external:read` | `datasources:*`
`datasources:uid:*` | Read templates, contact points, notification policies, and mute timings in data sources that support alerting. | -| `alert.notifications.external:write` | `datasources:*`
`datasources:uid:*` | Manage templates, contact points, notification policies, and mute timings in data sources that support alerting. | -| `alert.notifications:write` | n/a | Manage templates, contact points, notification policies, and mute timings in the current organization. | -| `alert.notifications:read` | n/a | Read all templates, contact points, notification policies, and mute timings in the current organization. | -| `alert.rules.external:read` | `datasources:*`
`datasources:uid:*` | Read alert rules in data sources that support alerting (Prometheus, Mimir, and Loki) | -| `alert.rules.external:write` | `datasources:*`
`datasources:uid:*` | Create, update, and delete alert rules in data sources that support alerting (Mimir and Loki). | -| `alert.rules:create` | `folders:*`
`folders:uid:*` | Create Grafana alert rules in a folder and its subfolders. Combine this permission with `folders:read` in a scope that includes the folder and `datasources:query` in the scope of data sources the user can query. | -| `alert.rules:delete` | `folders:*`
`folders:uid:*` | Delete Grafana alert rules in a folder and its subfolders. Combine this permission with `folders:read` in a scope that includes the folder and `datasources:query` in the scope of data sources the user can query. | -| `alert.rules:read` | `folders:*`
`folders:uid:*` | Read Grafana alert rules in a folder and its subfolders. Combine this permission with `folders:read` in a scope that includes the folder and `datasources:query` in the scope of data sources the user can query. | -| `alert.rules:write` | `folders:*`
`folders:uid:*` | Update Grafana alert rules in a folder and its subfolders. Combine this permission with `folders:read` in a scope that includes the folder and `datasources:query` in the scope of data sources the user can query. | -| `alert.provisioning:read` | n/a | Read all Grafana alert rules, notification policies, etc via provisioning API. Permissions to folders and datasource are not required. | -| `alert.provisioning.secrets:read` | n/a | Same as `alert.provisioning:read` plus ability to export resources with decrypted secrets. | -| `alert.provisioning:write` | n/a | Update all Grafana alert rules, notification policies, etc via provisioning API. Permissions to folders and datasource are not required. | -| `annotations:create` | `annotations:*`
`annotations:type:*` | Create annotations. | -| `annotations:delete` | `annotations:*`
`annotations:type:*` | Delete annotations. | -| `annotations:read` | `annotations:*`
`annotations:type:*` | Read annotations and annotation tags. | -| `annotations:write` | `annotations:*`
`annotations:type:*` | Update annotations. | -| `apikeys:create` | n/a | Create API keys. | -| `apikeys:read` | `apikeys:*`
`apikeys:id:*` | Read API keys. | -| `apikeys:delete` | `apikeys:*`
`apikeys:id:*` | Delete API keys. | -| `dashboards:create` | `folders:*`
`folders:uid:*` | Create dashboards in one or more folders and their subfolders. | -| `dashboards:delete` | `dashboards:*`
`dashboards:uid:*`
`folders:*`
`folders:uid:*` | Delete one or more dashboards. | -| `dashboards.insights:read` | n/a | Read dashboard insights data and see presence indicators. | -| `dashboards.permissions:read` | `dashboards:*`
`dashboards:uid:*`
`folders:*`
`folders:uid:*` | Read permissions for one or more dashboards. | -| `dashboards.permissions:write` | `dashboards:*`
`dashboards:uid:*`
`folders:*`
`folders:uid:*` | Update permissions for one or more dashboards. | -| `dashboards:read` | `dashboards:*`
`dashboards:uid:*`
`folders:*`
`folders:uid:*` | Read one or more dashboards. | -| `dashboards:write` | `dashboards:*`
`dashboards:uid:*`
`folders:*`
`folders:uid:*` | Update one or more dashboards. | -| `dashboards.public:write` | `dashboards:*`
`dashboards:uid:*` | Write public dashboard configuration. | -| `datasources.caching:read` | `datasources:*`
`datasources:uid:*` | Read data source query caching settings. | -| `datasources.caching:write` | `datasources:*`
`datasources:uid:*` | Update data source query caching settings. | -| `datasources:create` | n/a | Create data sources. | -| `datasources:delete` | `datasources:*`
`datasources:uid:*` | Delete data sources. | -| `datasources:explore` | n/a | Enable access to the **Explore** tab. | -| `datasources.id:read` | `datasources:*`
`datasources:uid:*` | Read data source IDs. | -| `datasources.insights:read` | n/a | Read data sources insights data. | -| `datasources.permissions:read` | `datasources:*`
`datasources:uid:*` | List data source permissions. | -| `datasources.permissions:write` | `datasources:*`
`datasources:uid:*` | Update data source permissions. | -| `datasources:query` | `datasources:*`
`datasources:uid:*` | Query data sources. | -| `datasources:read` | `datasources:*`
`datasources:uid:*` | List data sources. | -| `datasources:write` | `datasources:*`
`datasources:uid:*` | Update data sources. | -| `featuremgmt.read` | n/a | Read feature toggles. | -| `featuremgmt.write` | n/a | Write feature toggles. | -| `folders.permissions:read` | `folders:*`
`folders:uid:*` | Read permissions for one or more folders and their subfolders. | -| `folders.permissions:write` | `folders:*`
`folders:uid:*` | Update permissions for one or more folders and their subfolders. | -| `folders:create` | n/a | Create folders in the root level. If granted together with `folders:write`, also allows creating subfolders under all folders that the user can update. | -| `folders:delete` | `folders:*`
`folders:uid:*` | Delete one or more folders and their subfolders. | -| `folders:read` | `folders:*`
`folders:uid:*` | Read one or more folders and their subfolders. | -| `folders:write` | `folders:*`
`folders:uid:*` | Update one or more folders and their subfolders. If granted together with `folders:create` permission, also allows creating subfolders under these folders. | -| `ldap.config:reload` | n/a | Reload the LDAP configuration. | -| `ldap.status:read` | n/a | Verify the availability of the LDAP server or servers. | -| `ldap.user:read` | n/a | Read users via LDAP. | -| `ldap.user:sync` | n/a | Sync users via LDAP. | -| `library.panels:create` | `folders:*`
`folders:uid:*` | Create a library panel in one or more folders and their subfolders. | -| `library.panels:read` | `folders:*`
`folders:uid:*`
`library.panels:*`
`library.panels:uid:*` | Read one or more library panels. | -| `library.panels:write` | `folders:*`
`folders:uid:*`
`library.panels:*`
`library.panels:uid:*` | Update one or more library panels. | -| `library.panels:delete` | `folders:*`
`folders:uid:*`
`library.panels:*`
`library.panels:uid:*` | Delete one or more library panels. | -| `licensing.reports:read` | n/a | Get custom permission reports. | -| `licensing:delete` | n/a | Delete the license token. | -| `licensing:read` | n/a | Read licensing information. | -| `licensing:write` | n/a | Update the license token. | -| `org.users:write` | `users:*`
`users:id:*` | Update the organization role (`Viewer`, `Editor`, or `Admin`) of a user. | -| `org.users:add` | `users:*`
`users:id:*` | Add a user to an organization or invite a new user to an organization. | -| `org.users:read` | `users:*`
`users:id:*` | Get user profiles within an organization. | -| `org.users:remove` | `users:*`
`users:id:*` | Remove a user from an organization. | -| `orgs.preferences:read` | n/a | Read organization preferences. | -| `orgs.preferences:write` | n/a | Update organization preferences. | -| `orgs.quotas:read` | n/a | Read organization quotas. | -| `orgs.quotas:write` | n/a | Update organization quotas. | -| `orgs:create` | n/a | Create an organization. | -| `orgs:delete` | n/a | Delete one or more organizations. | -| `orgs:read` | n/a | Read one or more organizations. | -| `orgs:write` | n/a | Update one or more organizations. | -| `plugins.app:access` | `plugins:*`
`plugins:id:*` | Access one or more application plugins (still enforcing the organization role) | -| `plugins:install` | n/a | Install and uninstall plugins. | -| `plugins:write` | `plugins:*`
`plugins:id:*` | Edit settings for one or more plugins. | -| `provisioning:reload` | `provisioners:*` | Reload provisioning files. To find the exact scope for specific provisioner, see [Scope definitions]({{< relref "#scope-definitions" >}}). | -| `reports:create` | n/a | Create reports. | -| `reports:write` | `reports:*`
`reports:id:*` | Update reports. | -| `reports.settings:read` | n/a | Read report settings. | -| `reports.settings:write` | n/a | Update report settings. | -| `reports:delete` | `reports:*`
`reports:id:*` | Delete reports. | -| `reports:read` | `reports:*`
`reports:id:*` | List all available reports or get a specific report. | -| `reports:send` | `reports:*`
`reports:id:*` | Send a report email. | -| `roles:delete` | `permissions:type:delegate` | Delete a custom role. | -| `roles:read` | `roles:*`
`roles:uid:*` | List roles and read a specific with its permissions. | -| `roles:write` | `permissions:type:delegate` | Create or update a custom role. | -| `roles:write` | `permissions:type:escalate` | Reset basic roles to their default permissions. | -| `server.stats:read` | n/a | Read Grafana instance statistics. | -| `server.usagestats.report:read` | n/a | View usage statistics report. | -| `serviceaccounts:write` | `serviceaccounts:*` | Create Grafana service accounts. | -| `serviceaccounts:create` | n/a | Update Grafana service accounts. | -| `serviceaccounts:delete` | `serviceaccounts:*`
`serviceaccounts:id:*` | Delete Grafana service accounts. | -| `serviceaccounts:read` | `serviceaccounts:*`
`serviceaccounts:id:*` | Read Grafana service accounts. | -| `serviceaccounts.permissions:write` | `serviceaccounts:*`
`serviceaccounts:id:*` | Update Grafana service account permissions to control who can do what with the service account. | -| `serviceaccounts.permissions:read` | `serviceaccounts:*`
`serviceaccounts:id:*` | Read Grafana service account permissions to see who can do what with the service account. | -| `settings:read` | `settings:*`
`settings:auth.saml:*`
`settings:auth.saml:enabled` (property level) | Read the [Grafana configuration settings]({{< relref "../../../../setup-grafana/configure-grafana/" >}}) | -| `settings:write` | `settings:*`
`settings:auth.saml:*`
`settings:auth.saml:enabled` (property level) | Update any Grafana configuration settings that can be [updated at runtime]({{< relref "../../../../setup-grafana/configure-grafana/settings-updates-at-runtime" >}}). | -| `support.bundles:create` | n/a | Create support bundles. | -| `support.bundles:delete` | n/a | Delete support bundles. | -| `support.bundles:read` | n/a | List and download support bundles. | -| `status:accesscontrol` | `services:accesscontrol` | Get access-control enabled status. | -| `teams.permissions:read` | `teams:*`
`teams:id:*` | Read members and Team Sync setup for teams. | -| `teams.permissions:write` | `teams:*`
`teams:id:*` | Add, remove and update members and manage Team Sync setup for teams. | -| `teams.roles:add` | `permissions:type:delegate` | Assign a role to a team. | -| `teams.roles:read` | `teams:*`
`teams:id:*` | List roles assigned directly to a team. | -| `teams.roles:remove` | `permissions:type:delegate` | Unassign a role from a team. | -| `teams:create` | n/a | Create teams. | -| `teams:delete` | `teams:*`
`teams:id:*` | Delete one or more teams. | -| `teams:read` | `teams:*`
`teams:id:*` | Read one or more teams and team preferences. | -| `teams:write` | `teams:*`
`teams:id:*` | Update one or more teams and team preferences. | -| `users.authtoken:read` | `global.users:*`
`global.users:id:*` | List authentication tokens that are assigned to a user. | -| `users.authtoken:write` | `global.users:*`
`global.users:id:*` | Update authentication tokens that are assigned to a user. | -| `users.password:write` | `global.users:*`
`global.users:id:*` | Update a user’s password. | -| `users.permissions:read` | `users:*` | List permissions of a user. | -| `users.permissions:write` | `global.users:*`
`global.users:id:*` | Update a user’s organization-level permissions. | -| `users.quotas:read` | `global.users:*`
`global.users:id:*` | List a user’s quotas. | -| `users.quotas:write` | `global.users:*`
`global.users:id:*` | Update a user’s quotas. | -| `users.roles:add` | `permissions:type:delegate` | Assign a role to a user or a service account. | -| `users.roles:read` | `users:*` | List roles assigned directly to a user or a service account. | -| `users.roles:remove` | `permissions:type:delegate` | Unassign a role from a user or a service account. | -| `users:create` | n/a | Create a user. | -| `users:delete` | `global.users:*`
`global.users:id:*` | Delete a user. | -| `users:disable` | `global.users:*`
`global.users:id:*` | Disable a user. | -| `users:enable` | `global.users:*`
`global.users:id:*` | Enable a user. | -| `users:logout` | `global.users:*`
`global.users:id:*` | Sign out a user. | -| `users:read` | `global.users:*` | Read or search user profiles. | -| `users:write` | `global.users:*`
`global.users:id:*` | Update a user’s profile. | +| Action | Applicable scope | Description | +| ------------------------------------ | --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `alert.instances.external:read` | `datasources:*`
`datasources:uid:*` | Read alerts and silences in data sources that support alerting. | +| `alert.instances.external:write` | `datasources:*`
`datasources:uid:*` | Manage alerts and silences in data sources that support alerting. | +| `alert.instances:create` | n/a | Create silences in the current organization. | +| `alert.instances:read` | n/a | Read alerts and silences in the current organization. | +| `alert.instances:write` | n/a | Update and expire silences in the current organization. | +| `alert.notifications.external:read` | `datasources:*`
`datasources:uid:*` | Read templates, contact points, notification policies, and mute timings in data sources that support alerting. | +| `alert.notifications.external:write` | `datasources:*`
`datasources:uid:*` | Manage templates, contact points, notification policies, and mute timings in data sources that support alerting. | +| `alert.notifications:write` | n/a | Manage templates, contact points, notification policies, and mute timings in the current organization. | +| `alert.notifications:read` | n/a | Read all templates, contact points, notification policies, and mute timings in the current organization. | +| `alert.rules.external:read` | `datasources:*`
`datasources:uid:*` | Read alert rules in data sources that support alerting (Prometheus, Mimir, and Loki) | +| `alert.rules.external:write` | `datasources:*`
`datasources:uid:*` | Create, update, and delete alert rules in data sources that support alerting (Mimir and Loki). | +| `alert.rules:create` | `folders:*`
`folders:uid:*` | Create Grafana alert rules in a folder and its subfolders. Combine this permission with `folders:read` in a scope that includes the folder and `datasources:query` in the scope of data sources the user can query. | +| `alert.rules:delete` | `folders:*`
`folders:uid:*` | Delete Grafana alert rules in a folder and its subfolders. Combine this permission with `folders:read` in a scope that includes the folder and `datasources:query` in the scope of data sources the user can query. | +| `alert.rules:read` | `folders:*`
`folders:uid:*` | Read Grafana alert rules in a folder and its subfolders. Combine this permission with `folders:read` in a scope that includes the folder and `datasources:query` in the scope of data sources the user can query. | +| `alert.rules:write` | `folders:*`
`folders:uid:*` | Update Grafana alert rules in a folder and its subfolders. Combine this permission with `folders:read` in a scope that includes the folder and `datasources:query` in the scope of data sources the user can query. | +| `alert.provisioning:read` | n/a | Read all Grafana alert rules, notification policies, etc via provisioning API. Permissions to folders and datasource are not required. | +| `alert.provisioning.secrets:read` | n/a | Same as `alert.provisioning:read` plus ability to export resources with decrypted secrets. | +| `alert.provisioning:write` | n/a | Update all Grafana alert rules, notification policies, etc via provisioning API. Permissions to folders and datasource are not required. | +| `annotations:create` | `annotations:*`
`annotations:type:*` | Create annotations. | +| `annotations:delete` | `annotations:*`
`annotations:type:*` | Delete annotations. | +| `annotations:read` | `annotations:*`
`annotations:type:*` | Read annotations and annotation tags. | +| `annotations:write` | `annotations:*`
`annotations:type:*` | Update annotations. | +| `apikeys:create` | n/a | Create API keys. | +| `apikeys:read` | `apikeys:*`
`apikeys:id:*` | Read API keys. | +| `apikeys:delete` | `apikeys:*`
`apikeys:id:*` | Delete API keys. | +| `dashboards:create` | `folders:*`
`folders:uid:*` | Create dashboards in one or more folders and their subfolders. | +| `dashboards:delete` | `dashboards:*`
`dashboards:uid:*`
`folders:*`
`folders:uid:*` | Delete one or more dashboards. | +| `dashboards.insights:read` | n/a | Read dashboard insights data and see presence indicators. | +| `dashboards.permissions:read` | `dashboards:*`
`dashboards:uid:*`
`folders:*`
`folders:uid:*` | Read permissions for one or more dashboards. | +| `dashboards.permissions:write` | `dashboards:*`
`dashboards:uid:*`
`folders:*`
`folders:uid:*` | Update permissions for one or more dashboards. | +| `dashboards:read` | `dashboards:*`
`dashboards:uid:*`
`folders:*`
`folders:uid:*` | Read one or more dashboards. | +| `dashboards:write` | `dashboards:*`
`dashboards:uid:*`
`folders:*`
`folders:uid:*` | Update one or more dashboards. | +| `dashboards.public:write` | `dashboards:*`
`dashboards:uid:*` | Write public dashboard configuration. | +| `datasources.caching:read` | `datasources:*`
`datasources:uid:*` | Read data source query caching settings. | +| `datasources.caching:write` | `datasources:*`
`datasources:uid:*` | Update data source query caching settings. | +| `datasources:create` | n/a | Create data sources. | +| `datasources:delete` | `datasources:*`
`datasources:uid:*` | Delete data sources. | +| `datasources:explore` | n/a | Enable access to the **Explore** tab. | +| `datasources.id:read` | `datasources:*`
`datasources:uid:*` | Read data source IDs. | +| `datasources.insights:read` | n/a | Read data sources insights data. | +| `datasources.permissions:read` | `datasources:*`
`datasources:uid:*` | List data source permissions. | +| `datasources.permissions:write` | `datasources:*`
`datasources:uid:*` | Update data source permissions. | +| `datasources:query` | `datasources:*`
`datasources:uid:*` | Query data sources. | +| `datasources:read` | `datasources:*`
`datasources:uid:*` | List data sources. | +| `datasources:write` | `datasources:*`
`datasources:uid:*` | Update data sources. | +| `featuremgmt.read` | n/a | Read feature toggles. | +| `featuremgmt.write` | n/a | Write feature toggles. | +| `folders.permissions:read` | `folders:*`
`folders:uid:*` | Read permissions for one or more folders and their subfolders. | +| `folders.permissions:write` | `folders:*`
`folders:uid:*` | Update permissions for one or more folders and their subfolders. | +| `folders:create` | n/a | Create folders in the root level. If granted together with `folders:write`, also allows creating subfolders under all folders that the user can update. | +| `folders:delete` | `folders:*`
`folders:uid:*` | Delete one or more folders and their subfolders. | +| `folders:read` | `folders:*`
`folders:uid:*` | Read one or more folders and their subfolders. | +| `folders:write` | `folders:*`
`folders:uid:*` | Update one or more folders and their subfolders. If granted together with `folders:create` permission, also allows creating subfolders under these folders. | +| `ldap.config:reload` | n/a | Reload the LDAP configuration. | +| `ldap.status:read` | n/a | Verify the availability of the LDAP server or servers. | +| `ldap.user:read` | n/a | Read users via LDAP. | +| `ldap.user:sync` | n/a | Sync users via LDAP. | +| `library.panels:create` | `folders:*`
`folders:uid:*` | Create a library panel in one or more folders and their subfolders. | +| `library.panels:read` | `folders:*`
`folders:uid:*`
`library.panels:*`
`library.panels:uid:*` | Read one or more library panels. | +| `library.panels:write` | `folders:*`
`folders:uid:*`
`library.panels:*`
`library.panels:uid:*` | Update one or more library panels. | +| `library.panels:delete` | `folders:*`
`folders:uid:*`
`library.panels:*`
`library.panels:uid:*` | Delete one or more library panels. | +| `licensing.reports:read` | n/a | Get custom permission reports. | +| `licensing:delete` | n/a | Delete the license token. | +| `licensing:read` | n/a | Read licensing information. | +| `licensing:write` | n/a | Update the license token. | +| `org.users:write` | `users:*`
`users:id:*` | Update the organization role (`Viewer`, `Editor`, or `Admin`) of a user. | +| `org.users:add` | `users:*`
`users:id:*` | Add a user to an organization or invite a new user to an organization. | +| `org.users:read` | `users:*`
`users:id:*` | Get user profiles within an organization. | +| `org.users:remove` | `users:*`
`users:id:*` | Remove a user from an organization. | +| `orgs.preferences:read` | n/a | Read organization preferences. | +| `orgs.preferences:write` | n/a | Update organization preferences. | +| `orgs.quotas:read` | n/a | Read organization quotas. | +| `orgs.quotas:write` | n/a | Update organization quotas. | +| `orgs:create` | n/a | Create an organization. | +| `orgs:delete` | n/a | Delete one or more organizations. | +| `orgs:read` | n/a | Read one or more organizations. | +| `orgs:write` | n/a | Update one or more organizations. | +| `plugins.app:access` | `plugins:*`
`plugins:id:*` | Access one or more application plugins (still enforcing the organization role) | +| `plugins:install` | n/a | Install and uninstall plugins. | +| `plugins:write` | `plugins:*`
`plugins:id:*` | Edit settings for one or more plugins. | +| `provisioning:reload` | `provisioners:*` | Reload provisioning files. To find the exact scope for specific provisioner, see [Scope definitions]({{< relref "#scope-definitions" >}}). | +| `reports:create` | n/a | Create reports. | +| `reports:write` | `reports:*`
`reports:id:*` | Update reports. | +| `reports.settings:read` | n/a | Read report settings. | +| `reports.settings:write` | n/a | Update report settings. | +| `reports:delete` | `reports:*`
`reports:id:*` | Delete reports. | +| `reports:read` | `reports:*`
`reports:id:*` | List all available reports or get a specific report. | +| `reports:send` | `reports:*`
`reports:id:*` | Send a report email. | +| `roles:delete` | `permissions:type:delegate` | Delete a custom role. | +| `roles:read` | `roles:*`
`roles:uid:*` | List roles and read a specific role with its permissions. | +| `roles:write` | `permissions:type:delegate` | Create or update a custom role. | +| `roles:write` | `permissions:type:escalate` | Reset basic roles to their default permissions. | +| `server.stats:read` | n/a | Read Grafana instance statistics. | +| `server.usagestats.report:read` | n/a | View usage statistics report. | +| `serviceaccounts:write` | `serviceaccounts:*` | Create Grafana service accounts. | +| `serviceaccounts:create` | n/a | Update Grafana service accounts. | +| `serviceaccounts:delete` | `serviceaccounts:*`
`serviceaccounts:id:*` | Delete Grafana service accounts. | +| `serviceaccounts:read` | `serviceaccounts:*`
`serviceaccounts:id:*` | Read Grafana service accounts. | +| `serviceaccounts.permissions:write` | `serviceaccounts:*`
`serviceaccounts:id:*` | Update Grafana service account permissions to control who can do what with the service account. | +| `serviceaccounts.permissions:read` | `serviceaccounts:*`
`serviceaccounts:id:*` | Read Grafana service account permissions to see who can do what with the service account. | +| `settings:read` | `settings:*`
`settings:auth.saml:*`
`settings:auth.saml:enabled` (property level) | Read the [Grafana configuration settings]({{< relref "../../../../setup-grafana/configure-grafana/" >}}) | +| `settings:write` | `settings:*`
`settings:auth.saml:*`
`settings:auth.saml:enabled` (property level) | Update any Grafana configuration settings that can be [updated at runtime]({{< relref "../../../../setup-grafana/configure-grafana/settings-updates-at-runtime" >}}). | +| `support.bundles:create` | n/a | Create support bundles. | +| `support.bundles:delete` | n/a | Delete support bundles. | +| `support.bundles:read` | n/a | List and download support bundles. | +| `status:accesscontrol` | `services:accesscontrol` | Get access-control enabled status. | +| `teams.permissions:read` | `teams:*`
`teams:id:*` | Read members and Team Sync setup for teams. | +| `teams.permissions:write` | `teams:*`
`teams:id:*` | Add, remove and update members and manage Team Sync setup for teams. | +| `teams.roles:add` | `permissions:type:delegate` | Assign a role to a team. | +| `teams.roles:read` | `teams:*`
`teams:id:*` | List roles assigned directly to a team. | +| `teams.roles:remove` | `permissions:type:delegate` | Unassign a role from a team. | +| `teams:create` | n/a | Create teams. | +| `teams:delete` | `teams:*`
`teams:id:*` | Delete one or more teams. | +| `teams:read` | `teams:*`
`teams:id:*` | Read one or more teams and team preferences. To list teams through the UI one of the following permissions is required in addition to `teams:read`: `teams:write`, `teams.permissions:read` or `teams.permissions:write`. | +| `teams:write` | `teams:*`
`teams:id:*` | Update one or more teams and team preferences. | +| `users.authtoken:read` | `global.users:*`
`global.users:id:*` | List authentication tokens that are assigned to a user. | +| `users.authtoken:write` | `global.users:*`
`global.users:id:*` | Update authentication tokens that are assigned to a user. | +| `users.password:write` | `global.users:*`
`global.users:id:*` | Update a user’s password. | +| `users.permissions:read` | `users:*` | List permissions of a user. | +| `users.permissions:write` | `global.users:*`
`global.users:id:*` | Update a user’s organization-level permissions. | +| `users.quotas:read` | `global.users:*`
`global.users:id:*` | List a user’s quotas. | +| `users.quotas:write` | `global.users:*`
`global.users:id:*` | Update a user’s quotas. | +| `users.roles:add` | `permissions:type:delegate` | Assign a role to a user or a service account. | +| `users.roles:read` | `users:*` | List roles assigned directly to a user or a service account. | +| `users.roles:remove` | `permissions:type:delegate` | Unassign a role from a user or a service account. | +| `users:create` | n/a | Create a user. | +| `users:delete` | `global.users:*`
`global.users:id:*` | Delete a user. | +| `users:disable` | `global.users:*`
`global.users:id:*` | Disable a user. | +| `users:enable` | `global.users:*`
`global.users:id:*` | Enable a user. | +| `users:logout` | `global.users:*`
`global.users:id:*` | Sign out a user. | +| `users:read` | `global.users:*` | Read or search user profiles. | +| `users:write` | `global.users:*`
`global.users:id:*` | Update a user’s profile. | ### Grafana OnCall action definitions (beta) diff --git a/pkg/services/accesscontrol/models.go b/pkg/services/accesscontrol/models.go index adb7cd7ca6..a85a004743 100644 --- a/pkg/services/accesscontrol/models.go +++ b/pkg/services/accesscontrol/models.go @@ -524,6 +524,7 @@ var TeamsAccessEvaluator = EvalAny( EvalAny( EvalPermission(ActionTeamsWrite), EvalPermission(ActionTeamsPermissionsWrite), + EvalPermission(ActionTeamsPermissionsRead), ), ), ) From 00f16cd01851196f4029ec1b212d816e3050760d Mon Sep 17 00:00:00 2001 From: Isabella Siu Date: Mon, 18 Mar 2024 08:56:57 -0400 Subject: [PATCH 0750/1406] CloudWatch Logs: Remove toggle for cloudWatchLogsMonacoEditor (#84414) --- .betterer.results | 10 -- .../feature-toggles/index.md | 1 - .../src/types/featureToggles.gen.ts | 1 - pkg/services/featuremgmt/registry.go | 9 -- pkg/services/featuremgmt/toggles_gen.csv | 1 - pkg/services/featuremgmt/toggles_gen.go | 4 - pkg/services/featuremgmt/toggles_gen.json | 3 +- .../LogsQueryEditor/LogsQueryEditor.tsx | 33 +---- .../LogsQueryEditor/LogsQueryField.tsx | 4 +- .../LogsQueryEditor/LogsQueryFieldOld.tsx | 119 ------------------ 10 files changed, 7 insertions(+), 178 deletions(-) delete mode 100644 public/app/plugins/datasource/cloudwatch/components/QueryEditor/LogsQueryEditor/LogsQueryFieldOld.tsx diff --git a/.betterer.results b/.betterer.results index f465107fe6..e718efde2b 100644 --- a/.betterer.results +++ b/.betterer.results @@ -4494,9 +4494,6 @@ exports[`better eslint`] = { "public/app/plugins/datasource/cloudwatch/components/QueryEditor/LogsQueryEditor/LogsQueryEditor.tsx:5381": [ [0, 0, 0, "Styles should be written using objects.", "0"] ], - "public/app/plugins/datasource/cloudwatch/components/QueryEditor/LogsQueryEditor/LogsQueryFieldOld.tsx:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], "public/app/plugins/datasource/cloudwatch/components/QueryEditor/MetricsQueryEditor/DynamicLabelsField.tsx:5381": [ [0, 0, 0, "Styles should be written using objects.", "0"] ], @@ -6551,7 +6548,6 @@ exports[`no gf-form usage`] = { [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"] ], "public/app/plugins/datasource/cloudwatch/components/QueryEditor/LogsQueryEditor/LogsQueryEditor.tsx:5381": [ - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"] ], "public/app/plugins/datasource/cloudwatch/components/QueryEditor/LogsQueryEditor/LogsQueryField.tsx:5381": [ @@ -6559,12 +6555,6 @@ exports[`no gf-form usage`] = { [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"] ], - "public/app/plugins/datasource/cloudwatch/components/QueryEditor/LogsQueryEditor/LogsQueryFieldOld.tsx:5381": [ - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"] - ], "public/app/plugins/datasource/cloudwatch/components/shared/LogGroups/LegacyLogGroupNamesSelection.tsx:5381": [ [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"] 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 e305ab1765..4a040f36d7 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -38,7 +38,6 @@ Some features are enabled by default. You can disable these feature by setting t | `lokiMetricDataplane` | Changes metric responses from Loki to be compliant with the dataplane specification. | Yes | | `dataplaneFrontendFallback` | Support dataplane contract field name change for transformations and field name matchers where the name is different | Yes | | `enableElasticsearchBackendQuerying` | Enable the processing of queries and responses in the Elasticsearch data source through backend | Yes | -| `cloudWatchLogsMonacoEditor` | Enables the Monaco editor for CloudWatch Logs queries | Yes | | `recordedQueriesMulti` | Enables writing multiple items from a single query within Recorded Queries | Yes | | `logsExploreTableVisualisation` | A table visualisation for logs in Explore | Yes | | `transformationsRedesign` | Enables the transformations redesign | Yes | diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 1d394adfca..7fb36ef715 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -87,7 +87,6 @@ export interface FeatureToggles { frontendSandboxMonitorOnly?: boolean; sqlDatasourceDatabaseSelection?: boolean; lokiFormatQuery?: boolean; - cloudWatchLogsMonacoEditor?: boolean; recordedQueriesMulti?: boolean; pluginsDynamicAngularDetectionPatterns?: boolean; vizAndWidgetSplit?: boolean; diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 81ff72f692..a6e1858bca 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -514,15 +514,6 @@ var ( Stage: FeatureStageExperimental, Owner: grafanaObservabilityLogsSquad, }, - { - Name: "cloudWatchLogsMonacoEditor", - Description: "Enables the Monaco editor for CloudWatch Logs queries", - Stage: FeatureStageGeneralAvailability, - FrontendOnly: true, - Expression: "true", // enabled by default - Owner: awsDatasourcesSquad, - AllowSelfServe: true, - }, { Name: "recordedQueriesMulti", Description: "Enables writing multiple items from a single query within Recorded Queries", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index cfb3fd8fbd..6954a9c389 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -68,7 +68,6 @@ dashboardEmbed,experimental,@grafana/grafana-as-code,false,false,true frontendSandboxMonitorOnly,experimental,@grafana/plugins-platform-backend,false,false,true sqlDatasourceDatabaseSelection,preview,@grafana/dataviz-squad,false,false,true lokiFormatQuery,experimental,@grafana/observability-logs,false,false,true -cloudWatchLogsMonacoEditor,GA,@grafana/aws-datasources,false,false,true recordedQueriesMulti,GA,@grafana/observability-metrics,false,false,false pluginsDynamicAngularDetectionPatterns,experimental,@grafana/plugins-platform-backend,false,false,false vizAndWidgetSplit,experimental,@grafana/dashboards-squad,false,false,true diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 7ac006ae6e..40581da464 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -283,10 +283,6 @@ const ( // Enables the ability to format Loki queries FlagLokiFormatQuery = "lokiFormatQuery" - // FlagCloudWatchLogsMonacoEditor - // Enables the Monaco editor for CloudWatch Logs queries - FlagCloudWatchLogsMonacoEditor = "cloudWatchLogsMonacoEditor" - // FlagRecordedQueriesMulti // Enables writing multiple items from a single query within Recorded Queries FlagRecordedQueriesMulti = "recordedQueriesMulti" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 00ba0cd967..0878000aae 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -1555,7 +1555,8 @@ "metadata": { "name": "cloudWatchLogsMonacoEditor", "resourceVersion": "1709648236447", - "creationTimestamp": "2024-03-05T14:17:16Z" + "creationTimestamp": "2024-03-05T14:17:16Z", + "deletionTimestamp": "2024-03-13T18:21:47Z" }, "spec": { "description": "Enables the Monaco editor for CloudWatch Logs queries", diff --git a/public/app/plugins/datasource/cloudwatch/components/QueryEditor/LogsQueryEditor/LogsQueryEditor.tsx b/public/app/plugins/datasource/cloudwatch/components/QueryEditor/LogsQueryEditor/LogsQueryEditor.tsx index 14222b9af4..fd57a6f458 100644 --- a/public/app/plugins/datasource/cloudwatch/components/QueryEditor/LogsQueryEditor/LogsQueryEditor.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/QueryEditor/LogsQueryEditor/LogsQueryEditor.tsx @@ -1,16 +1,14 @@ import { css } from '@emotion/css'; import React, { memo } from 'react'; -import { AbsoluteTimeRange, QueryEditorProps } from '@grafana/data'; -import { config } from '@grafana/runtime'; +import { QueryEditorProps } from '@grafana/data'; import { InlineFormLabel } from '@grafana/ui'; import { CloudWatchDatasource } from '../../../datasource'; import { CloudWatchJsonData, CloudWatchLogsQuery, CloudWatchQuery } from '../../../types'; import { CloudWatchLink } from './CloudWatchLink'; -import CloudWatchLogsQueryFieldMonaco from './LogsQueryField'; -import CloudWatchLogsQueryField from './LogsQueryFieldOld'; +import CloudWatchLogsQueryField from './LogsQueryField'; type Props = QueryEditorProps & { query: CloudWatchLogsQuery; @@ -24,34 +22,9 @@ const labelClass = css` export const CloudWatchLogsQueryEditor = memo(function CloudWatchLogsQueryEditor(props: Props) { const { query, data, datasource } = props; - let absolute: AbsoluteTimeRange; - if (data?.request?.range?.from) { - const { range } = data.request; - absolute = { - from: range.from.valueOf(), - to: range.to.valueOf(), - }; - } else { - absolute = { - from: Date.now() - 10000, - to: Date.now(), - }; - } - - return config.featureToggles.cloudWatchLogsMonacoEditor ? ( - - - - } - /> - ) : ( + return ( diff --git a/public/app/plugins/datasource/cloudwatch/components/QueryEditor/LogsQueryEditor/LogsQueryField.tsx b/public/app/plugins/datasource/cloudwatch/components/QueryEditor/LogsQueryEditor/LogsQueryField.tsx index 52c44d1d25..cf30ff6665 100644 --- a/public/app/plugins/datasource/cloudwatch/components/QueryEditor/LogsQueryEditor/LogsQueryField.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/QueryEditor/LogsQueryEditor/LogsQueryField.tsx @@ -18,7 +18,7 @@ export interface CloudWatchLogsQueryFieldProps ExtraFieldElement?: ReactNode; query: CloudWatchLogsQuery; } -export const CloudWatchLogsQueryFieldMonaco = (props: CloudWatchLogsQueryFieldProps) => { +export const CloudWatchLogsQueryField = (props: CloudWatchLogsQueryFieldProps) => { const { query, datasource, onChange, ExtraFieldElement, data } = props; const showError = data?.error?.refId === query.refId; @@ -141,4 +141,4 @@ export const CloudWatchLogsQueryFieldMonaco = (props: CloudWatchLogsQueryFieldPr ); }; -export default withTheme2(CloudWatchLogsQueryFieldMonaco); +export default withTheme2(CloudWatchLogsQueryField); diff --git a/public/app/plugins/datasource/cloudwatch/components/QueryEditor/LogsQueryEditor/LogsQueryFieldOld.tsx b/public/app/plugins/datasource/cloudwatch/components/QueryEditor/LogsQueryEditor/LogsQueryFieldOld.tsx deleted file mode 100644 index 925ddabf69..0000000000 --- a/public/app/plugins/datasource/cloudwatch/components/QueryEditor/LogsQueryEditor/LogsQueryFieldOld.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import { LanguageMap, languages as prismLanguages } from 'prismjs'; -import React, { ReactNode } from 'react'; -import { Node, Plugin } from 'slate'; -import { Editor } from 'slate-react'; - -import { AbsoluteTimeRange, QueryEditorProps } from '@grafana/data'; -import { - BracesPlugin, - QueryField, - SlatePrism, - Themeable2, - TypeaheadInput, - TypeaheadOutput, - withTheme2, -} from '@grafana/ui'; - -// Utils & Services -// dom also includes Element polyfills -import { CloudWatchDatasource } from '../../../datasource'; -import syntax from '../../../language/cloudwatch-logs/syntax'; -import { CloudWatchJsonData, CloudWatchLogsQuery, CloudWatchQuery, LogGroup } from '../../../types'; -import { getStatsGroups } from '../../../utils/query/getStatsGroups'; -import { LogGroupsFieldWrapper } from '../../shared/LogGroups/LogGroupsField'; - -export interface CloudWatchLogsQueryFieldProps - extends QueryEditorProps, - Themeable2 { - absoluteRange: AbsoluteTimeRange; - onLabelsRefresh?: () => void; - ExtraFieldElement?: ReactNode; - query: CloudWatchLogsQuery; -} -const plugins: Array> = [ - BracesPlugin(), - SlatePrism( - { - onlyIn: (node: Node) => node.object === 'block' && node.type === 'code_block', - getSyntax: (node: Node) => 'cloudwatch', - }, - { ...(prismLanguages as LanguageMap), cloudwatch: syntax } - ), -]; -export const CloudWatchLogsQueryField = (props: CloudWatchLogsQueryFieldProps) => { - const { query, datasource, onChange, ExtraFieldElement, data } = props; - - const showError = data?.error?.refId === query.refId; - const cleanText = datasource.languageProvider.cleanText; - - const onChangeQuery = (value: string) => { - // Send text change to parent - const nextQuery = { - ...query, - expression: value, - statsGroups: getStatsGroups(value), - }; - onChange(nextQuery); - }; - - const onTypeahead = async (typeahead: TypeaheadInput): Promise => { - const { datasource, query } = props; - const { logGroups } = query; - - if (!datasource.languageProvider) { - return { suggestions: [] }; - } - - const { history, absoluteRange } = props; - const { prefix, text, value, wrapperClasses, labelKey, editor } = typeahead; - - return await datasource.languageProvider.provideCompletionItems( - { text, value, prefix, wrapperClasses, labelKey, editor }, - { - history, - absoluteRange, - logGroups: logGroups, - region: query.region, - } - ); - }; - - return ( - <> - { - onChange({ ...query, logGroups, logGroupNames: undefined }); - }} - //legacy props can be removed once we remove support for Legacy Log Group Selector - legacyOnChange={(logGroups: string[]) => { - onChange({ ...query, logGroupNames: logGroups }); - }} - /> -
-
- -
- {ExtraFieldElement} -
- {showError ? ( -
-
{data?.error?.message}
-
- ) : null} - - ); -}; - -export default withTheme2(CloudWatchLogsQueryField); From fbb6ae35e79d3497207eb2076f8b09dd9680001b Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Mon, 18 Mar 2024 13:00:18 +0000 Subject: [PATCH 0751/1406] E2C: Use cloudMigrationIsTarget config (#84654) Use cloudMigrationIsTarget config --- .../app/features/admin/migrate-to-cloud/MigrateToCloud.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/public/app/features/admin/migrate-to-cloud/MigrateToCloud.tsx b/public/app/features/admin/migrate-to-cloud/MigrateToCloud.tsx index 86163f6352..1b0b16628c 100644 --- a/public/app/features/admin/migrate-to-cloud/MigrateToCloud.tsx +++ b/public/app/features/admin/migrate-to-cloud/MigrateToCloud.tsx @@ -7,8 +7,5 @@ import { Page as CloudPage } from './cloud/Page'; import { Page as OnPremPage } from './onprem/Page'; export default function MigrateToCloud() { - // TODO replace this with a proper config value when it's available - const isMigrationTarget = config.namespace.startsWith('stack-'); - - return {isMigrationTarget ? : }; + return {config.cloudMigrationIsTarget ? : }; } From 155e38edfe7aa553e1822297bb41309c35a1a2bf Mon Sep 17 00:00:00 2001 From: Levente Balogh Date: Mon, 18 Mar 2024 14:14:51 +0100 Subject: [PATCH 0752/1406] Plugin Extensions: Add prop types to component extensions (#84295) * feat: make it possible to specify prop types for component extensions * Update packages/grafana-runtime/src/services/pluginExtensions/getPluginExtensions.ts * chore: adapted test case * chore: update betterer * feat: update types for configureComponentExtension() * fix: remove type specifics for `configureExtensionComponent` * Update betterer config --------- Co-authored-by: Darren Janeczek <38694490+darrenjaneczek@users.noreply.github.com> Co-authored-by: Darren Janeczek --- .betterer.results | 6 +++--- packages/grafana-data/src/types/app.ts | 4 +--- .../src/types/pluginExtensions.ts | 12 +++++------ .../pluginExtensions/getPluginExtensions.ts | 9 +++++++-- .../datasources/components/EditDataSource.tsx | 8 ++++---- .../plugins/extensions/utils.test.tsx | 20 +++++++++++++++++-- .../features/profile/UserProfileEditPage.tsx | 1 - 7 files changed, 38 insertions(+), 22 deletions(-) diff --git a/.betterer.results b/.betterer.results index e718efde2b..e76e868a54 100644 --- a/.betterer.results +++ b/.betterer.results @@ -765,6 +765,9 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "11"], [0, 0, 0, "Unexpected any. Specify a different type.", "12"] ], + "packages/grafana-runtime/src/services/pluginExtensions/getPluginExtensions.ts:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"] + ], "packages/grafana-runtime/src/utils/DataSourceWithBackend.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"] @@ -2947,9 +2950,6 @@ exports[`better eslint`] = { "public/app/features/datasources/components/DataSourceTypeCardList.tsx:5381": [ [0, 0, 0, "Styles should be written using objects.", "0"] ], - "public/app/features/datasources/components/EditDataSource.tsx:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], "public/app/features/datasources/components/picker/DataSourceCard.tsx:5381": [ [0, 0, 0, "Styles should be written using objects.", "0"], [0, 0, 0, "Styles should be written using objects.", "1"], diff --git a/packages/grafana-data/src/types/app.ts b/packages/grafana-data/src/types/app.ts index fcb58a7ccb..626ab368bf 100644 --- a/packages/grafana-data/src/types/app.ts +++ b/packages/grafana-data/src/types/app.ts @@ -111,9 +111,7 @@ export class AppPlugin extends GrafanaPlugin( - extension: Omit, 'type'> - ) { + configureExtensionComponent(extension: Omit, 'type'>) { this._extensionConfigs.push({ ...extension, type: PluginExtensionTypes.component, diff --git a/packages/grafana-data/src/types/pluginExtensions.ts b/packages/grafana-data/src/types/pluginExtensions.ts index 68fa3c7d1d..e7a26223c3 100644 --- a/packages/grafana-data/src/types/pluginExtensions.ts +++ b/packages/grafana-data/src/types/pluginExtensions.ts @@ -32,9 +32,9 @@ export type PluginExtensionLink = PluginExtensionBase & { category?: string; }; -export type PluginExtensionComponent = PluginExtensionBase & { +export type PluginExtensionComponent = PluginExtensionBase & { type: PluginExtensionTypes.component; - component: React.ComponentType; + component: React.ComponentType; }; export type PluginExtension = PluginExtensionLink | PluginExtensionComponent; @@ -77,16 +77,14 @@ export type PluginExtensionLinkConfig = { category?: string; }; -export type PluginExtensionComponentConfig = { +export type PluginExtensionComponentConfig = { type: PluginExtensionTypes.component; title: string; description: string; // The React component that will be rendered as the extension - // (This component receives the context as a prop when it is rendered. You can just return `null` from the component to hide for certain contexts) - component: React.ComponentType<{ - context?: Context; - }>; + // (This component receives contextual information as props when it is rendered. You can just return `null` from the component to hide it.) + component: React.ComponentType; // The unique identifier of the Extension Point // (Core Grafana extension point ids are available in the `PluginExtensionPoints` enum) diff --git a/packages/grafana-runtime/src/services/pluginExtensions/getPluginExtensions.ts b/packages/grafana-runtime/src/services/pluginExtensions/getPluginExtensions.ts index 66f2cf2abc..dcd52b200b 100644 --- a/packages/grafana-runtime/src/services/pluginExtensions/getPluginExtensions.ts +++ b/packages/grafana-runtime/src/services/pluginExtensions/getPluginExtensions.ts @@ -41,10 +41,15 @@ export const getPluginLinkExtensions: GetPluginExtensions = }; }; -export const getPluginComponentExtensions: GetPluginExtensions = (options) => { +// This getter doesn't support the `context` option (contextual information can be passed in as component props) +export const getPluginComponentExtensions = (options: { + extensionPointId: string; + limitPerPlugin?: number; +}): { extensions: Array> } => { const { extensions } = getPluginExtensions(options); + const componentExtensions = extensions.filter(isPluginExtensionComponent) as Array>; return { - extensions: extensions.filter(isPluginExtensionComponent), + extensions: componentExtensions, }; }; diff --git a/public/app/features/datasources/components/EditDataSource.tsx b/public/app/features/datasources/components/EditDataSource.tsx index d141d19c25..e4802f21b4 100644 --- a/public/app/features/datasources/components/EditDataSource.tsx +++ b/public/app/features/datasources/components/EditDataSource.tsx @@ -139,7 +139,9 @@ export function EditDataSourceView({ const extensions = useMemo(() => { const allowedPluginIds = ['grafana-pdc-app', 'grafana-auth-app']; const extensionPointId = PluginExtensionPoints.DataSourceConfig; - const { extensions } = getPluginComponentExtensions({ extensionPointId }); + const { extensions } = getPluginComponentExtensions<{ + context: PluginExtensionDataSourceConfigContext; + }>({ extensionPointId }); return extensions.filter((e) => allowedPluginIds.includes(e.pluginId)); }, []); @@ -202,9 +204,7 @@ export function EditDataSourceView({ {/* Extension point */} {extensions.map((extension) => { - const Component = extension.component as React.ComponentType<{ - context: PluginExtensionDataSourceConfigContext; - }>; + const Component = extension.component; return (
diff --git a/public/app/features/plugins/extensions/utils.test.tsx b/public/app/features/plugins/extensions/utils.test.tsx index ee4b742f5f..93958e05c9 100644 --- a/public/app/features/plugins/extensions/utils.test.tsx +++ b/public/app/features/plugins/extensions/utils.test.tsx @@ -445,12 +445,18 @@ describe('Plugin Extensions / Utils', () => { }); describe('wrapExtensionComponentWithContext()', () => { - const ExampleComponent = () => { + type ExampleComponentProps = { + audience?: string; + }; + + const ExampleComponent = (props: ExampleComponentProps) => { const { meta } = usePluginContext(); + const audience = props.audience || 'Grafana'; + return (
-

Hello Grafana!

Version: {meta.info.version} +

Hello {audience}!

Version: {meta.info.version}
); }; @@ -464,5 +470,15 @@ describe('Plugin Extensions / Utils', () => { expect(await screen.findByText('Hello Grafana!')).toBeVisible(); expect(screen.getByText('Version: 1.0.0')).toBeVisible(); }); + + it('should pass the properties into the wrapped component', async () => { + const pluginId = 'grafana-worldmap-panel'; + const Component = wrapWithPluginContext(pluginId, ExampleComponent); + + render(); + + expect(await screen.findByText('Hello folks!')).toBeVisible(); + expect(screen.getByText('Version: 1.0.0')).toBeVisible(); + }); }); }); diff --git a/public/app/features/profile/UserProfileEditPage.tsx b/public/app/features/profile/UserProfileEditPage.tsx index e86583393f..ffb9c64b00 100644 --- a/public/app/features/profile/UserProfileEditPage.tsx +++ b/public/app/features/profile/UserProfileEditPage.tsx @@ -79,7 +79,6 @@ export function UserProfileEditPage({ const extensionComponents = useMemo(() => { const { extensions } = getPluginComponentExtensions({ extensionPointId: PluginExtensionPoints.UserProfileTab, - context: {}, }); return extensions; From aac2cf0aa5dd21175c7ac1c4bf5fe3df0d5c8c1f Mon Sep 17 00:00:00 2001 From: Kyle Brandt Date: Mon, 18 Mar 2024 09:22:28 -0400 Subject: [PATCH 0753/1406] Scopes: Update BE API to include object for linking scope to dashboards (#84608) * Add ScopeDashboard --------- Co-authored-by: Todd Treece --- pkg/apis/scope/v0alpha1/register.go | 8 ++ pkg/apis/scope/v0alpha1/types.go | 20 +++- .../scope/v0alpha1/zz_generated.deepcopy.go | 48 ++++++++++ .../scope/v0alpha1/zz_generated.openapi.go | 93 +++++++++++++++++-- ...enerated.openapi_violation_exceptions.list | 1 + pkg/registry/apis/scope/register.go | 16 +++- pkg/registry/apis/scope/storage.go | 42 ++++++++- 7 files changed, 211 insertions(+), 17 deletions(-) diff --git a/pkg/apis/scope/v0alpha1/register.go b/pkg/apis/scope/v0alpha1/register.go index 81dc1535fc..5868562a84 100644 --- a/pkg/apis/scope/v0alpha1/register.go +++ b/pkg/apis/scope/v0alpha1/register.go @@ -20,6 +20,12 @@ var ScopeResourceInfo = common.NewResourceInfo(GROUP, VERSION, func() runtime.Object { return &ScopeList{} }, ) +var ScopeDashboardResourceInfo = common.NewResourceInfo(GROUP, VERSION, + "scopedashboards", "scopedashboard", "ScopeDashboard", + func() runtime.Object { return &ScopeDashboard{} }, + func() runtime.Object { return &ScopeDashboardList{} }, +) + var ( // SchemeGroupVersion is group version used to register these objects SchemeGroupVersion = schema.GroupVersion{Group: GROUP, Version: VERSION} @@ -39,6 +45,8 @@ func addKnownTypes(scheme *runtime.Scheme) error { scheme.AddKnownTypes(SchemeGroupVersion, &Scope{}, &ScopeList{}, + &ScopeDashboard{}, + &ScopeDashboardList{}, ) metav1.AddToGroupVersion(scheme, SchemeGroupVersion) return nil diff --git a/pkg/apis/scope/v0alpha1/types.go b/pkg/apis/scope/v0alpha1/types.go index 4610e66c52..08441235e1 100644 --- a/pkg/apis/scope/v0alpha1/types.go +++ b/pkg/apis/scope/v0alpha1/types.go @@ -26,15 +26,27 @@ type ScopeFilter struct { Operator string `json:"operator"` } +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type ScopeList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + + Items []Scope `json:"items,omitempty"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object type ScopeDashboard struct { - DashboardUID string `json:"dashboardUid"` - ScopeUID string `json:"scopeUid"` + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + DashboardUID []string `json:"dashboardUid"` + ScopeUID string `json:"scopeUid"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type ScopeList struct { +type ScopeDashboardList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata,omitempty"` - Items []Scope `json:"items,omitempty"` + Items []ScopeDashboard `json:"items,omitempty"` } diff --git a/pkg/apis/scope/v0alpha1/zz_generated.deepcopy.go b/pkg/apis/scope/v0alpha1/zz_generated.deepcopy.go index 49ef6f913e..2cab9759fd 100644 --- a/pkg/apis/scope/v0alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/scope/v0alpha1/zz_generated.deepcopy.go @@ -41,6 +41,13 @@ func (in *Scope) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ScopeDashboard) DeepCopyInto(out *ScopeDashboard) { *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + if in.DashboardUID != nil { + in, out := &in.DashboardUID, &out.DashboardUID + *out = make([]string, len(*in)) + copy(*out, *in) + } return } @@ -54,6 +61,47 @@ func (in *ScopeDashboard) DeepCopy() *ScopeDashboard { return out } +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ScopeDashboard) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ScopeDashboardList) DeepCopyInto(out *ScopeDashboardList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ScopeDashboard, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ScopeDashboardList. +func (in *ScopeDashboardList) DeepCopy() *ScopeDashboardList { + if in == nil { + return nil + } + out := new(ScopeDashboardList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ScopeDashboardList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ScopeFilter) DeepCopyInto(out *ScopeFilter) { *out = *in diff --git a/pkg/apis/scope/v0alpha1/zz_generated.openapi.go b/pkg/apis/scope/v0alpha1/zz_generated.openapi.go index c519a61609..8efa6a2492 100644 --- a/pkg/apis/scope/v0alpha1/zz_generated.openapi.go +++ b/pkg/apis/scope/v0alpha1/zz_generated.openapi.go @@ -16,11 +16,12 @@ import ( func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { return map[string]common.OpenAPIDefinition{ - "github.com/grafana/grafana/pkg/apis/scope/v0alpha1.Scope": schema_pkg_apis_scope_v0alpha1_Scope(ref), - "github.com/grafana/grafana/pkg/apis/scope/v0alpha1.ScopeDashboard": schema_pkg_apis_scope_v0alpha1_ScopeDashboard(ref), - "github.com/grafana/grafana/pkg/apis/scope/v0alpha1.ScopeFilter": schema_pkg_apis_scope_v0alpha1_ScopeFilter(ref), - "github.com/grafana/grafana/pkg/apis/scope/v0alpha1.ScopeList": schema_pkg_apis_scope_v0alpha1_ScopeList(ref), - "github.com/grafana/grafana/pkg/apis/scope/v0alpha1.ScopeSpec": schema_pkg_apis_scope_v0alpha1_ScopeSpec(ref), + "github.com/grafana/grafana/pkg/apis/scope/v0alpha1.Scope": schema_pkg_apis_scope_v0alpha1_Scope(ref), + "github.com/grafana/grafana/pkg/apis/scope/v0alpha1.ScopeDashboard": schema_pkg_apis_scope_v0alpha1_ScopeDashboard(ref), + "github.com/grafana/grafana/pkg/apis/scope/v0alpha1.ScopeDashboardList": schema_pkg_apis_scope_v0alpha1_ScopeDashboardList(ref), + "github.com/grafana/grafana/pkg/apis/scope/v0alpha1.ScopeFilter": schema_pkg_apis_scope_v0alpha1_ScopeFilter(ref), + "github.com/grafana/grafana/pkg/apis/scope/v0alpha1.ScopeList": schema_pkg_apis_scope_v0alpha1_ScopeList(ref), + "github.com/grafana/grafana/pkg/apis/scope/v0alpha1.ScopeSpec": schema_pkg_apis_scope_v0alpha1_ScopeSpec(ref), } } @@ -70,11 +71,38 @@ func schema_pkg_apis_scope_v0alpha1_ScopeDashboard(ref common.ReferenceCallback) SchemaProps: spec.SchemaProps{ Type: []string{"object"}, Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), + }, + }, "dashboardUid": { SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, }, }, "scopeUid": { @@ -88,6 +116,55 @@ func schema_pkg_apis_scope_v0alpha1_ScopeDashboard(ref common.ReferenceCallback) Required: []string{"dashboardUid", "scopeUid"}, }, }, + Dependencies: []string{ + "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + } +} + +func schema_pkg_apis_scope_v0alpha1_ScopeDashboardList(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), + }, + }, + "items": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/pkg/apis/scope/v0alpha1.ScopeDashboard"), + }, + }, + }, + }, + }, + }, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/pkg/apis/scope/v0alpha1.ScopeDashboard", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, } } diff --git a/pkg/apis/scope/v0alpha1/zz_generated.openapi_violation_exceptions.list b/pkg/apis/scope/v0alpha1/zz_generated.openapi_violation_exceptions.list index c1152ae735..082fa3782e 100644 --- a/pkg/apis/scope/v0alpha1/zz_generated.openapi_violation_exceptions.list +++ b/pkg/apis/scope/v0alpha1/zz_generated.openapi_violation_exceptions.list @@ -1,3 +1,4 @@ +API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/scope/v0alpha1,ScopeDashboard,DashboardUID API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/scope/v0alpha1,ScopeSpec,Filters API rule violation: names_match,github.com/grafana/grafana/pkg/apis/scope/v0alpha1,ScopeDashboard,DashboardUID API rule violation: names_match,github.com/grafana/grafana/pkg/apis/scope/v0alpha1,ScopeDashboard,ScopeUID diff --git a/pkg/registry/apis/scope/register.go b/pkg/registry/apis/scope/register.go index a2095ea570..f55454a11e 100644 --- a/pkg/registry/apis/scope/register.go +++ b/pkg/registry/apis/scope/register.go @@ -68,13 +68,23 @@ func (b *ScopeAPIBuilder) GetAPIGroupInfo( ) (*genericapiserver.APIGroupInfo, error) { apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(scope.GROUP, scheme, metav1.ParameterCodec, codecs) - resourceInfo := scope.ScopeResourceInfo + scopeResourceInfo := scope.ScopeResourceInfo + scopeDashboardResourceInfo := scope.ScopeDashboardResourceInfo + storage := map[string]rest.Storage{} - scopeStorage, err := newStorage(scheme, optsGetter) + + scopeStorage, err := newScopeStorage(scheme, optsGetter) if err != nil { return nil, err } - storage[resourceInfo.StoragePath()] = scopeStorage + storage[scopeResourceInfo.StoragePath()] = scopeStorage + + scopeDashboardStorage, err := newScopeDashboardStorage(scheme, optsGetter) + if err != nil { + return nil, err + } + storage[scopeDashboardResourceInfo.StoragePath()] = scopeDashboardStorage + apiGroupInfo.VersionedResourcesStorageMap[scope.VERSION] = storage return &apiGroupInfo, nil } diff --git a/pkg/registry/apis/scope/storage.go b/pkg/registry/apis/scope/storage.go index c01f93a409..abd88de717 100644 --- a/pkg/registry/apis/scope/storage.go +++ b/pkg/registry/apis/scope/storage.go @@ -21,7 +21,7 @@ type storage struct { *genericregistry.Store } -func newStorage(scheme *runtime.Scheme, optsGetter generic.RESTOptionsGetter) (*storage, error) { +func newScopeStorage(scheme *runtime.Scheme, optsGetter generic.RESTOptionsGetter) (*storage, error) { strategy := grafanaregistry.NewStrategy(scheme) resourceInfo := scope.ScopeResourceInfo @@ -40,7 +40,45 @@ func newStorage(scheme *runtime.Scheme, optsGetter generic.RESTOptionsGetter) (* func(obj any) ([]interface{}, error) { m, ok := obj.(*scope.Scope) if !ok { - return nil, fmt.Errorf("expected query template") + return nil, fmt.Errorf("expected scope") + } + return []interface{}{ + m.Name, + m.CreationTimestamp.UTC().Format(time.RFC3339), + }, nil + }, + ), + CreateStrategy: strategy, + UpdateStrategy: strategy, + DeleteStrategy: strategy, + } + options := &generic.StoreOptions{RESTOptions: optsGetter, AttrFunc: grafanaregistry.GetAttrs} + if err := store.CompleteWithOptions(options); err != nil { + return nil, err + } + return &storage{Store: store}, nil +} + +func newScopeDashboardStorage(scheme *runtime.Scheme, optsGetter generic.RESTOptionsGetter) (*storage, error) { + strategy := grafanaregistry.NewStrategy(scheme) + + resourceInfo := scope.ScopeDashboardResourceInfo + store := &genericregistry.Store{ + NewFunc: resourceInfo.NewFunc, + NewListFunc: resourceInfo.NewListFunc, + PredicateFunc: grafanaregistry.Matcher, + DefaultQualifiedResource: resourceInfo.GroupResource(), + SingularQualifiedResource: resourceInfo.SingularGroupResource(), + TableConvertor: utils.NewTableConverter( + resourceInfo.GroupResource(), + []metav1.TableColumnDefinition{ + {Name: "Name", Type: "string", Format: "name"}, + {Name: "Created At", Type: "date"}, + }, + func(obj any) ([]interface{}, error) { + m, ok := obj.(*scope.Scope) + if !ok { + return nil, fmt.Errorf("expected scope") } return []interface{}{ m.Name, From 5b085976bfc15686c3d4a56057b7f489d0a956c5 Mon Sep 17 00:00:00 2001 From: Andrej Ocenas Date: Mon, 18 Mar 2024 14:25:47 +0100 Subject: [PATCH 0754/1406] Pyroscope: Fix template variable support (#84477) --- .../pyroscopeClient.go | 7 +++++ .../VariableQueryEditor.tsx | 29 ++++++++++--------- .../VariableSupport.test.ts | 13 +++++++-- .../VariableSupport.ts | 6 ++-- 4 files changed, 37 insertions(+), 18 deletions(-) diff --git a/pkg/tsdb/grafana-pyroscope-datasource/pyroscopeClient.go b/pkg/tsdb/grafana-pyroscope-datasource/pyroscopeClient.go index ea0f1a14c6..8a6845055f 100644 --- a/pkg/tsdb/grafana-pyroscope-datasource/pyroscopeClient.go +++ b/pkg/tsdb/grafana-pyroscope-datasource/pyroscopeClient.go @@ -256,6 +256,10 @@ func (c *PyroscopeClient) LabelNames(ctx context.Context, labelSelector string, return nil, fmt.Errorf("error sending LabelNames request %v", err) } + if resp.Msg.Names == nil { + return []string{}, nil + } + var filtered []string for _, label := range resp.Msg.Names { if !isPrivateLabel(label) { @@ -281,6 +285,9 @@ func (c *PyroscopeClient) LabelValues(ctx context.Context, label string, labelSe span.SetStatus(codes.Error, err.Error()) return nil, err } + if resp.Msg.Names == nil { + return []string{}, nil + } return resp.Msg.Names, nil } diff --git a/public/app/plugins/datasource/grafana-pyroscope-datasource/VariableQueryEditor.tsx b/public/app/plugins/datasource/grafana-pyroscope-datasource/VariableQueryEditor.tsx index 866fa34105..27f4b2d085 100644 --- a/public/app/plugins/datasource/grafana-pyroscope-datasource/VariableQueryEditor.tsx +++ b/public/app/plugins/datasource/grafana-pyroscope-datasource/VariableQueryEditor.tsx @@ -30,23 +30,16 @@ export function VariableQueryEditor(props: QueryEditorProps { if (value.value! === 'profileType') { props.onChange({ - ...props.query, type: value.value!, + refId: props.query.refId, }); } - if (value.value! === 'label') { + if (value.value! === 'label' || value.value! === 'labelValue') { props.onChange({ - ...props.query, type: value.value!, - profileTypeId: '', - }); - } - if (value.value! === 'labelValue') { - props.onChange({ - ...props.query, - type: value.value!, - profileTypeId: '', - labelName: '', + refId: props.query.refId, + // Make sure we keep already selected values if they make sense for the variable type + profileTypeId: props.query.type !== 'profileType' ? props.query.profileTypeId : '', }); } }} @@ -98,7 +91,13 @@ function LabelRow(props: { const [labels, setLabels] = useState(); useEffect(() => { (async () => { - setLabels(await props.datasource.getLabelNames((props.profileTypeId || '') + '{}', props.from, props.to)); + setLabels( + await props.datasource.getLabelNames( + props.profileTypeId ? getProfileTypeLabel(props.profileTypeId) : '{}', + props.from, + props.to + ) + ); })(); }, [props.datasource, props.profileTypeId, props.to, props.from]); @@ -156,3 +155,7 @@ function ProfileTypeRow(props: { ); } + +export function getProfileTypeLabel(type: string) { + return `{__profile_type__="${type}"}`; +} diff --git a/public/app/plugins/datasource/grafana-pyroscope-datasource/VariableSupport.test.ts b/public/app/plugins/datasource/grafana-pyroscope-datasource/VariableSupport.test.ts index 5dcc47ab9e..957ca03589 100644 --- a/public/app/plugins/datasource/grafana-pyroscope-datasource/VariableSupport.test.ts +++ b/public/app/plugins/datasource/grafana-pyroscope-datasource/VariableSupport.test.ts @@ -24,7 +24,11 @@ describe('VariableSupport', () => { vs.query(getDefaultRequest({ type: 'label', profileTypeId: 'profile:type:3', refId: 'A' })) ); expect(resp.data).toEqual([{ text: 'foo' }, { text: 'bar' }, { text: 'baz' }]); - expect(mock.getLabelNames).toBeCalledWith('profile:type:3{}', expect.any(Number), expect.any(Number)); + expect(mock.getLabelNames).toBeCalledWith( + '{__profile_type__="profile:type:3"}', + expect.any(Number), + expect.any(Number) + ); }); it('should query label values', async function () { @@ -34,7 +38,12 @@ describe('VariableSupport', () => { vs.query(getDefaultRequest({ type: 'labelValue', labelName: 'foo', profileTypeId: 'profile:type:3', refId: 'A' })) ); expect(resp.data).toEqual([{ text: 'val1' }, { text: 'val2' }, { text: 'val3' }]); - expect(mock.getLabelValues).toBeCalledWith('profile:type:3{}', 'foo', expect.any(Number), expect.any(Number)); + expect(mock.getLabelValues).toBeCalledWith( + '{__profile_type__="profile:type:3"}', + 'foo', + expect.any(Number), + expect.any(Number) + ); }); }); diff --git a/public/app/plugins/datasource/grafana-pyroscope-datasource/VariableSupport.ts b/public/app/plugins/datasource/grafana-pyroscope-datasource/VariableSupport.ts index 99a3e24a39..80059880c5 100644 --- a/public/app/plugins/datasource/grafana-pyroscope-datasource/VariableSupport.ts +++ b/public/app/plugins/datasource/grafana-pyroscope-datasource/VariableSupport.ts @@ -2,7 +2,7 @@ import { from, map, Observable, of } from 'rxjs'; import { CustomVariableSupport, DataQueryRequest, DataQueryResponse, MetricFindValue } from '@grafana/data'; -import { VariableQueryEditor } from './VariableQueryEditor'; +import { getProfileTypeLabel, VariableQueryEditor } from './VariableQueryEditor'; import { PyroscopeDataSource } from './datasource'; import { ProfileTypeMessage, VariableQuery } from './types'; @@ -34,7 +34,7 @@ export class VariableSupport extends CustomVariableSupport } return from( this.dataAPI.getLabelNames( - request.targets[0].profileTypeId + '{}', + getProfileTypeLabel(request.targets[0].profileTypeId), request.range.from.valueOf(), request.range.to.valueOf() ) @@ -51,7 +51,7 @@ export class VariableSupport extends CustomVariableSupport } return from( this.dataAPI.getLabelValues( - request.targets[0].profileTypeId + '{}', + getProfileTypeLabel(request.targets[0].profileTypeId), request.targets[0].labelName, request.range.from.valueOf(), request.range.to.valueOf() From ebcca970521886668d5613105f0ba5800d45519b Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Mon, 18 Mar 2024 14:26:56 +0100 Subject: [PATCH 0755/1406] Annotation query: Render query result in alert box (#83230) * add alert to annotation result * cleanup * add tests * more refactoring * apply pr feedback * change severity * use toHaveAlert matcher --- .betterer.results | 3 - .../as-admin-user/annotationEditPage.spec.ts | 64 ++++++++-- .../plugin-e2e-api-tests/mocks/queries.ts | 63 +++++++++- .../src/selectors/components.ts | 4 + .../StandardAnnotationQueryEditor.tsx | 118 +++++++++++------- 5 files changed, 187 insertions(+), 65 deletions(-) diff --git a/.betterer.results b/.betterer.results index e76e868a54..bd47ca293f 100644 --- a/.betterer.results +++ b/.betterer.results @@ -2320,9 +2320,6 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "2"], [0, 0, 0, "Do not use any type assertions.", "3"] ], - "public/app/features/annotations/components/StandardAnnotationQueryEditor.tsx:5381": [ - [0, 0, 0, "Styles should be written using objects.", "0"] - ], "public/app/features/annotations/events_processing.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/annotationEditPage.spec.ts b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/annotationEditPage.spec.ts index a23ceb8494..1a11fdd6a5 100644 --- a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/annotationEditPage.spec.ts +++ b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/annotationEditPage.spec.ts @@ -1,15 +1,53 @@ import { expect, test } from '@grafana/plugin-e2e'; +import { AlertVariant } from '@grafana/ui'; -import { formatExpectError } from '../errors'; -import { successfulAnnotationQuery } from '../mocks/queries'; - -test('annotation query data with mocked response', async ({ annotationEditPage, page }) => { - annotationEditPage.mockQueryDataResponse(successfulAnnotationQuery); - await annotationEditPage.datasource.set('gdev-testdata'); - await page.getByLabel('Scenario').last().fill('CSV Content'); - await page.keyboard.press('Tab'); - await expect( - annotationEditPage.runQuery(), - formatExpectError('Expected annotation query to execute successfully') - ).toBeOK(); -}); +import { + successfulAnnotationQueryWithData, + failedAnnotationQueryWithMultipleErrors, + successfulAnnotationQueryWithoutData, + failedAnnotationQuery, +} from '../mocks/queries'; + +interface Scenario { + name: string; + mock: object; + text: string; + severity: AlertVariant; + status: number; +} + +const scenarios: Scenario[] = [ + { name: 'error', severity: 'error', mock: failedAnnotationQuery, text: 'Google API Error 400', status: 400 }, + { + name: 'multiple errors', + severity: 'error', + mock: failedAnnotationQueryWithMultipleErrors, + text: 'Google API Error 400Google API Error 401', + status: 400, + }, + { + name: 'data', + severity: 'success', + mock: successfulAnnotationQueryWithData, + text: '2 events (from 2 fields)', + status: 200, + }, + { + name: 'empty result', + severity: 'warning', + mock: successfulAnnotationQueryWithoutData, + text: 'No events found', + status: 200, + }, +]; + +for (const scenario of scenarios) { + test(`annotation query data with ${scenario.name}`, async ({ annotationEditPage, page }) => { + annotationEditPage.mockQueryDataResponse(scenario.mock, scenario.status); + await annotationEditPage.datasource.set('gdev-testdata'); + await page.getByLabel('Scenario').last().fill('CSV Content'); + await page.keyboard.press('Tab'); + await annotationEditPage.runQuery(); + await expect(annotationEditPage).toHaveAlert(scenario.severity, { hasText: scenario.text }); + }); +} diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/mocks/queries.ts b/e2e/plugin-e2e/plugin-e2e-api-tests/mocks/queries.ts index 16d2448192..d39c8dc02d 100644 --- a/e2e/plugin-e2e/plugin-e2e-api-tests/mocks/queries.ts +++ b/e2e/plugin-e2e/plugin-e2e-api-tests/mocks/queries.ts @@ -37,7 +37,32 @@ export const successfulDataQuery = { }, }; -export const successfulAnnotationQuery = { +export const failedAnnotationQuery: object = { + results: { + Anno: { + error: 'Google API Error 400', + errorSource: '', + status: 500, + }, + }, +}; + +export const failedAnnotationQueryWithMultipleErrors: object = { + results: { + Anno1: { + error: 'Google API Error 400', + errorSource: '', + status: 400, + }, + Anno2: { + error: 'Google API Error 401', + errorSource: '', + status: 401, + }, + }, +}; + +export const successfulAnnotationQueryWithData: object = { results: { Anno: { status: 200, @@ -75,3 +100,39 @@ export const successfulAnnotationQuery = { }, }, }; + +export const successfulAnnotationQueryWithoutData: object = { + results: { + Anno: { + status: 200, + frames: [ + { + schema: { + refId: 'Anno', + fields: [ + { + name: 'time', + type: 'time', + typeInfo: { + frame: 'time.Time', + nullable: true, + }, + }, + { + name: 'col2', + type: 'string', + typeInfo: { + frame: 'string', + nullable: true, + }, + }, + ], + }, + data: { + values: [], + }, + }, + ], + }, + }, +}; diff --git a/packages/grafana-e2e-selectors/src/selectors/components.ts b/packages/grafana-e2e-selectors/src/selectors/components.ts index d9538c6bdd..357187506e 100644 --- a/packages/grafana-e2e-selectors/src/selectors/components.ts +++ b/packages/grafana-e2e-selectors/src/selectors/components.ts @@ -529,6 +529,10 @@ export const Components = { Annotations: { annotationsTypeInput: 'annotations-type-input', annotationsChoosePanelInput: 'choose-panels-input', + editor: { + testButton: 'data-testid annotations-test-button', + resultContainer: 'data-testid annotations-query-result-container', + }, }, Tooltip: { container: 'data-testid tooltip', diff --git a/public/app/features/annotations/components/StandardAnnotationQueryEditor.tsx b/public/app/features/annotations/components/StandardAnnotationQueryEditor.tsx index 4bce32eddb..b07d784375 100644 --- a/public/app/features/annotations/components/StandardAnnotationQueryEditor.tsx +++ b/public/app/features/annotations/components/StandardAnnotationQueryEditor.tsx @@ -1,5 +1,4 @@ -import { css, cx } from '@emotion/css'; -import React, { PureComponent } from 'react'; +import React, { PureComponent, ReactElement } from 'react'; import { lastValueFrom } from 'rxjs'; import { @@ -11,7 +10,8 @@ import { DataSourcePluginContextProvider, LoadingState, } from '@grafana/data'; -import { Button, Icon, IconName, Spinner } from '@grafana/ui'; +import { selectors } from '@grafana/e2e-selectors'; +import { Alert, AlertVariant, Button, Space, Spinner } from '@grafana/ui'; import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { PanelModel } from 'app/features/dashboard/state'; @@ -112,63 +112,85 @@ export default class StandardAnnotationQueryEditor extends PureComponent{'loading...'}

; + } + + if (panelData?.errors) { + return ( + <> + {panelData.errors.map((e, i) => ( +

{e.message}

+ ))} + + ); + } + if (panelData?.error) { + return

{panelData.error.message ?? 'There was an error fetching data'}

; + } + + if (!events?.length) { + return

No events found

; + } + + const frame = panelData?.series?.[0] ?? panelData?.annotations?.[0]; + return ( +

+ {events.length} events (from {frame?.fields.length} fields) +

+ ); + } + renderStatus() { const { response, running } = this.state; - let rowStyle = 'alert-info'; - let text = '...'; - let icon: IconName | undefined = undefined; - if (running || response?.panelData?.state === LoadingState.Loading || !response) { - text = 'loading...'; - } else { - const { events, panelData } = response; - - if (panelData?.error) { - rowStyle = 'alert-error'; - icon = 'exclamation-triangle'; - text = panelData.error.message ?? 'error'; - } else if (!events?.length) { - rowStyle = 'alert-warning'; - icon = 'exclamation-triangle'; - text = 'No events found'; - } else { - const frame = panelData?.series?.[0] ?? panelData?.annotations?.[0]; - - text = `${events.length} events (from ${frame?.fields.length} fields)`; - } + if (!response) { + return null; } + return ( -
-
- {icon && ( - <> - -   - - )} - {text} -
+ <> +
{running ? ( ) : ( - )}
-
+ + + {this.renderStatusText(response, running)} + + ); } From cc6459deaf13e06682e4054ef17dc41b49eea224 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 18 Mar 2024 13:39:05 +0000 Subject: [PATCH 0756/1406] I18n: Download translations from Crowdin (#84660) New Crowdin translations by GitHub Action Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- public/locales/fr-FR/grafana.json | 198 +++++++++++++++--------------- 1 file changed, 99 insertions(+), 99 deletions(-) diff --git a/public/locales/fr-FR/grafana.json b/public/locales/fr-FR/grafana.json index 5cf34099bd..e8a94b891d 100644 --- a/public/locales/fr-FR/grafana.json +++ b/public/locales/fr-FR/grafana.json @@ -674,138 +674,138 @@ "title": "Échec de la connexion", "unknown": "Une erreur inconnue est survenue" }, - "forgot-password": "", + "forgot-password": "Vous avez oublié votre mot de passe ?", "form": { - "password-label": "", - "password-required": "", - "submit-label": "", - "submit-loading-label": "", - "username-label": "", - "username-required": "" + "password-label": "Mot de passe", + "password-required": "Vous devez renseigner le mot de passe", + "submit-label": "Connexion", + "submit-loading-label": "Connexion…", + "username-label": "E-mail ou nom d'utilisateur", + "username-required": "Vous devez renseigner l'e-mail ou le nom d'utilisateur" }, "services": { - "sing-in-with-prefix": "" + "sing-in-with-prefix": "Se connecter avec {{serviceName}}" }, "signup": { - "button-label": "", - "new-to-question": "" + "button-label": "Inscription", + "new-to-question": "Nouveau sur Grafana ?" } }, "migrate-to-cloud": { "can-i-move": { - "body": "", - "link-title": "", - "title": "" + "body": "Une fois que vous aurez connecté cette installation à une pile cloud, vous pourrez importer les sources de données et les tableaux de bord.", + "link-title": "En savoir plus sur la migration des autres paramètres", + "title": "Puis-je déplacer cette installation vers Grafana Cloud ?" }, "connect-modal": { - "body-cloud-stack": "", - "body-get-started": "", - "body-paste-stack": "", - "body-sign-up": "", - "body-token": "", - "body-token-field": "", - "body-token-field-placeholder": "", - "body-token-instructions": "", - "body-url-field": "", - "body-view-stacks": "", - "cancel": "", - "connect": "", - "connecting": "", - "stack-required-error": "", - "title": "", - "token-required-error": "" + "body-cloud-stack": "Vous aurez également besoin d'une pile cloud. Si vous venez de vous inscrire, nous créerons automatiquement votre première pile. Si vous avez déjà un compte, vous devez sélectionner ou créer une pile.", + "body-get-started": "Pour commencer, vous aurez besoin d'un compte Grafana.com.", + "body-paste-stack": "Lorsque vous aurez choisi une pile, collez l'URL ci-dessous.", + "body-sign-up": "Créez un compte Grafana.com", + "body-token": "Votre installation Grafana autogérée a besoin d'un accès spécial pour migrer du contenu de manière sécurisée. Vous devrez créer un jeton de migration sur la pile cloud que vous avez choisie.", + "body-token-field": "Jeton de migration", + "body-token-field-placeholder": "Coller le jeton ici", + "body-token-instructions": "Connectez-vous à votre pile cloud et accédez à Administration > Général > Migrer vers Grafana Cloud. Créez un jeton de migration sur cet écran et collez-le ici.", + "body-url-field": "URL de la pile cloud", + "body-view-stacks": "Voir mes piles cloud", + "cancel": "Annuler", + "connect": "Se connecter à cette pile", + "connecting": "Connexion à cette pile…", + "stack-required-error": "Vous devez renseigner l'URL de la pile", + "title": "Se connecter à une pile cloud", + "token-required-error": "Vous devez renseigner le jeton de migration" }, "cta": { - "button": "", - "header": "" + "button": "Migrer cette instance vers le cloud", + "header": "Laissez-nous gérer votre pile Grafana" }, "disconnect-modal": { - "body": "", - "cancel": "", - "disconnect": "", - "disconnecting": "", - "error": "", - "title": "" + "body": "Cette action supprimera le jeton de migration de cette installation. Si vous souhaitez importer plus de ressources dans le futur, vous devrez entrer un nouveau jeton de migration.", + "cancel": "Annuler", + "disconnect": "Déconnecter", + "disconnecting": "Déconnexion…", + "error": "La déconnexion a échoué", + "title": "Se déconnecter de la pile cloud" }, "get-started": { - "body": "", - "configure-pdc-link": "", - "link-title": "", - "step-1": "", - "step-2": "", - "step-3": "", - "step-4": "", - "step-5": "", - "title": "" + "body": "Le processus de migration doit être démarré à partir de votre instance Grafana autogérée.", + "configure-pdc-link": "Configurer la PDC pour cette pile", + "link-title": "En savoir plus sur la connexion privée aux sources de données", + "step-1": "Connectez-vous à votre instance autogérée et accédez à Administration > Général > Migrer vers Grafana Cloud.", + "step-2": "Sélectionnez Migrer cette instance vers le cloud.", + "step-3": "Le système vous demandera un jeton de migration. Générez-en un à partir de cet écran.", + "step-4": "Dans votre instance autogérée, sélectionnez Importer tout pour importer les sources de données et les tableaux de bord vers cette pile cloud.", + "step-5": "Si certaines de vos sources de données ne fonctionnent pas sur le réseau Internet public, vous devez installer une connexion privée aux sources de données dans votre environnement autogéré.", + "title": "Comment commencer" }, "is-it-secure": { - "body": "", - "link-title": "", - "title": "" + "body": "Grafana Labs s'engage à appliquer les normes les plus strictes en matière de confidentialité et de sécurité des données. En utilisant des technologies et des procédures de sécurité conformes aux normes du secteur, nous contribuons à protéger les données de nos clients contre l'accès, l'utilisation ou la divulgation non autorisés.", + "link-title": "Centre de confiance Grafana Labs", + "title": "La solution est-elle sécurisée ?" }, "migrate-to-this-stack": { - "body": "", - "link-title": "", - "title": "" + "body": "Certaines configurations de votre instance Grafana autogérée peuvent être copiées automatiquement dans cette pile cloud.", + "link-title": "Voir le guide de migration complet", + "title": "Migrer la configuration vers cette pile" }, "migration-token": { - "body": "", - "delete-button": "", - "delete-modal-body": "", - "delete-modal-cancel": "", - "delete-modal-confirm": "", - "delete-modal-deleting": "", - "delete-modal-title": "", - "generate-button": "", - "generate-button-loading": "", - "modal-close": "", - "modal-copy-and-close": "", - "modal-copy-button": "", - "modal-field-description": "", - "modal-field-label": "", - "modal-title": "", - "status": "", - "title": "" + "body": "Votre instance Grafana autogérée aura besoin d'un jeton d'authentification spécial pour se connecter de manière sécurisée à cette pile cloud.", + "delete-button": "Supprimer ce jeton de migration", + "delete-modal-body": "Si vous avez déjà utilisé ce jeton avec une installation autogérée, celle-ci ne pourra plus importer de contenu.", + "delete-modal-cancel": "Annuler", + "delete-modal-confirm": "Supprimer", + "delete-modal-deleting": "Suppression...", + "delete-modal-title": "Supprimer le jeton de migration", + "generate-button": "Générer un jeton de migration", + "generate-button-loading": "Génération d'un jeton de migration…", + "modal-close": "Fermer", + "modal-copy-and-close": "Copier dans le presse-papiers et fermer", + "modal-copy-button": "Copier dans le presse-papiers", + "modal-field-description": "Copiez le jeton maintenant, car vous ne pourrez plus le revoir. Si vous perdez un jeton, vous devrez 'en créer un nouveau.", + "modal-field-label": "Jeton", + "modal-title": "Jeton de migration créé", + "status": "Statut actuel : <2>", + "title": "Jeton de migration" }, "pdc": { - "body": "", - "link-title": "", - "title": "" + "body": "L'exposition de vos sources de données sur Internet peut compromettre la sécurité. La connexion privée aux sources de données (PDC) permet à Grafana Cloud d'accéder à vos sources de données existantes en passant par un tunnel réseau sécurisé.", + "link-title": "En savoir plus sur la PDC", + "title": "Toutes mes sources de données ne sont pas sur le réseau Internet public" }, "pricing": { - "body": "", - "link-title": "", - "title": "" + "body": "Grafana Cloud propose une formule gratuite généreuse et un essai illimité de 14 jours. Après votre période d'essai, nous vous facturerons en fonction de votre utilisation dépassant les limites de la formule gratuite.", + "link-title": "Tarifs de Grafana Cloud", + "title": "Combien ça coûte ?" }, "resource-status": { - "error-details-button": "", - "failed": "", - "migrated": "", - "migrating": "", - "not-migrated": "", - "unknown": "" + "error-details-button": "Détails", + "failed": "Erreur", + "migrated": "Importé dans le cloud", + "migrating": "Importation…", + "not-migrated": "Pas encore importé", + "unknown": "Inconnu" }, "resource-type": { - "dashboard": "", - "datasource": "", - "unknown": "" + "dashboard": "Tableau de bord", + "datasource": "Source de données", + "unknown": "Inconnu" }, "resources": { - "disconnect": "" + "disconnect": "Déconnecter" }, "token-status": { - "active": "", - "no-active": "" + "active": "Jeton créé et actif", + "no-active": "Aucun jeton actif" }, "what-is-cloud": { - "body": "", - "link-title": "", - "title": "" + "body": "Grafana cloud est une plateforme d'observabilité hébergée dans le cloud et entièrement gérée. Elle est idéale pour les environnements natifs du cloud. Elle offre tout ce que vous aimez dans Grafana sans les frais d'entretien, de mise à niveau et de support qu'implique une installation.", + "link-title": "En savoir plus sur les fonctionnalités cloud", + "title": "Qu'est-ce que Grafana Cloud ?" }, "why-host": { - "body": "", - "link-title": "", - "title": "" + "body": "En plus de la commodité de l'hébergement géré, Grafana Cloud comprend de nombreuses fonctionnalités exclusives au cloud comme les SLO, la gestion des incidents, l'apprentissage automatique et de puissantes intégrations d'observabilité.", + "link-title": "Vous avez d'autres questions ? Posez-les à un expert", + "title": "Pourquoi héberger avec Grafana ?" } }, "nav": { @@ -994,8 +994,8 @@ "subtitle": "Gérer les tableaux de bord et les autorisations des dossiers" }, "migrate-to-cloud": { - "subtitle": "", - "title": "" + "subtitle": "Copier la configuration de votre installation autogérée vers une pile cloud", + "title": "Migrer vers Grafana Cloud" }, "monitoring": { "subtitle": "Applications de suivi et d'infrastructure", @@ -1223,7 +1223,7 @@ "old-password-label": "Ancien mot de passe", "old-password-required": "Vous devez saisir l'ancien mot de passe", "passwords-must-match": "Les mots de passe doivent être identiques", - "strong-password-validation-register": "" + "strong-password-validation-register": "Selon notre politique, votre mot de passe n'est pas suffisamment sécurisé" } }, "public-dashboard": { @@ -1458,10 +1458,10 @@ "expire-day": "1 jour", "expire-hour": "1 heure", "expire-never": "Jamais", - "expire-week": "", + "expire-week": "1 semaine", "info-text-1": "Un instantané est un moyen instantané de partager publiquement un tableau de bord interactif. Lors de la création, nous supprimons les données sensibles telles que les requêtes (métrique, modèle et annotation) et les liens du panneau, pour ne laisser que les métriques visibles et les noms de séries intégrés dans votre tableau de bord.", "info-text-2": "N'oubliez pas que votre instantané <1>peut être consulté par une personne qui dispose du lien et qui peut accéder à l'URL. Partagez judicieusement.", - "local-button": "", + "local-button": "Publier un instantané", "mistake-message": "Avez-vous commis une erreur ? ", "name": "Nom de l'instantané", "timeout": "Délai d’expiration (secondes)", @@ -1474,7 +1474,7 @@ "library-panel": "Panneau de bibliothèque", "link": "Lien", "panel-embed": "Intégrer", - "public-dashboard": "", + "public-dashboard": "Publier le tableau de bord", "public-dashboard-title": "Tableau de bord public", "snapshot": "Instantané" }, From aec2ef727a421542873db8d6e7a3573ad721ab76 Mon Sep 17 00:00:00 2001 From: Kyle Brandt Date: Mon, 18 Mar 2024 09:49:26 -0400 Subject: [PATCH 0757/1406] Prometheus/Scopes: Update to use scopespec type from app (#84593) --- pkg/promlib/models/query.go | 18 +++++------------- pkg/promlib/models/scope.go | 35 ++++++++++++++++++++++++++++++++++- 2 files changed, 39 insertions(+), 14 deletions(-) diff --git a/pkg/promlib/models/query.go b/pkg/promlib/models/query.go index 3c9a96bab9..a22eb5a2ba 100644 --- a/pkg/promlib/models/query.go +++ b/pkg/promlib/models/query.go @@ -2,7 +2,6 @@ package models import ( "encoding/json" - "fmt" "math" "strconv" "strings" @@ -11,8 +10,8 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/gtime" "github.com/prometheus/prometheus/model/labels" - "github.com/prometheus/prometheus/promql/parser" + "github.com/grafana/grafana/pkg/apis/scope/v0alpha1" "github.com/grafana/grafana/pkg/promlib/intervalv2" ) @@ -64,9 +63,7 @@ type PrometheusQueryProperties struct { LegendFormat string `json:"legendFormat,omitempty"` // ??? - Scope *struct { - Matchers string `json:"matchers"` - } `json:"scope,omitempty"` + Scope *v0alpha1.ScopeSpec `json:"scope,omitempty"` } // Internal interval and range variables @@ -139,7 +136,7 @@ type Query struct { RangeQuery bool ExemplarQuery bool UtcOffsetSec int64 - Scope Scope + Scope *v0alpha1.ScopeSpec } type Scope struct { @@ -168,13 +165,8 @@ func Parse(query backend.DataQuery, dsScrapeInterval string, intervalCalculator dsScrapeInterval, timeRange, ) - var matchers []*labels.Matcher - if enableScope && model.Scope != nil && model.Scope.Matchers != "" { - matchers, err = parser.ParseMetricSelector(model.Scope.Matchers) - if err != nil { - return nil, fmt.Errorf("failed to parse metric selector %v in scope", model.Scope.Matchers) - } - expr, err = ApplyQueryScope(expr, matchers) + if enableScope && model.Scope != nil && len(model.Scope.Filters) > 0 { + expr, err = ApplyQueryScope(expr, *model.Scope) if err != nil { return nil, err } diff --git a/pkg/promlib/models/scope.go b/pkg/promlib/models/scope.go index c14dfdc550..16e588001b 100644 --- a/pkg/promlib/models/scope.go +++ b/pkg/promlib/models/scope.go @@ -1,16 +1,24 @@ package models import ( + "fmt" + + "github.com/grafana/grafana/pkg/apis/scope/v0alpha1" "github.com/prometheus/prometheus/model/labels" "github.com/prometheus/prometheus/promql/parser" ) -func ApplyQueryScope(rawExpr string, matchers []*labels.Matcher) (string, error) { +func ApplyQueryScope(rawExpr string, scope v0alpha1.ScopeSpec) (string, error) { expr, err := parser.ParseExpr(rawExpr) if err != nil { return "", err } + matchers, err := scopeFiltersToMatchers(scope.Filters) + if err != nil { + return "", err + } + matcherNamesToIdx := make(map[string]int, len(matchers)) for i, matcher := range matchers { if matcher == nil { @@ -50,3 +58,28 @@ func ApplyQueryScope(rawExpr string, matchers []*labels.Matcher) (string, error) }) return expr.String(), nil } + +func scopeFiltersToMatchers(filters []v0alpha1.ScopeFilter) ([]*labels.Matcher, error) { + matchers := make([]*labels.Matcher, 0, len(filters)) + for _, f := range filters { + var mt labels.MatchType + switch f.Operator { + case "=": + mt = labels.MatchEqual + case "!=": + mt = labels.MatchNotEqual + case "=~": + mt = labels.MatchRegexp + case "!~": + mt = labels.MatchNotRegexp + default: + return nil, fmt.Errorf("unknown operator %q", f.Operator) + } + m, err := labels.NewMatcher(mt, f.Key, f.Value) + if err != nil { + return nil, err + } + matchers = append(matchers, m) + } + return matchers, nil +} From 767608f3a60a898062feff86ae9dd0df57bb357e Mon Sep 17 00:00:00 2001 From: Darren Janeczek <38694490+darrenjaneczek@users.noreply.github.com> Date: Mon, 18 Mar 2024 10:23:19 -0400 Subject: [PATCH 0758/1406] Data trails: use description of data source to shorten label (#84665) fix: use description of data source to shorten label --- public/app/features/trails/DataTrail.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/public/app/features/trails/DataTrail.tsx b/public/app/features/trails/DataTrail.tsx index a879ba0e6d..43685a32c6 100644 --- a/public/app/features/trails/DataTrail.tsx +++ b/public/app/features/trails/DataTrail.tsx @@ -207,7 +207,8 @@ function getVariableSet(initialDS?: string, metric?: string, initialFilters?: Ad variables: [ new DataSourceVariable({ name: VAR_DATASOURCE, - label: 'Prometheus data source', + label: 'Data source', + description: 'Only prometheus data sources are supported', value: initialDS, pluginId: 'prometheus', }), From 259d4eb6ec1fd1393da43cccf00ecaa6a73de77f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 18 Mar 2024 14:45:34 +0000 Subject: [PATCH 0759/1406] I18n: Download translations from Crowdin (#84664) New Crowdin translations by GitHub Action Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- public/locales/pt-BR/grafana.json | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/public/locales/pt-BR/grafana.json b/public/locales/pt-BR/grafana.json index cc553b6fb0..54f7add2d9 100644 --- a/public/locales/pt-BR/grafana.json +++ b/public/locales/pt-BR/grafana.json @@ -1,24 +1,24 @@ { - "_comment": "", + "_comment": "Esse arquivo é a fonte da verdade para strings em inglês. Edite isto na base de código para alterar os plurais e outras frases para a interface do usuário.", "access-control": { "add-permission": { - "role-label": "", - "serviceaccount-label": "", - "team-label": "", - "title": "", - "user-label": "" + "role-label": "Função", + "serviceaccount-label": "Conta de serviço", + "team-label": "Equipe", + "title": "Adicionar permissão para", + "user-label": "Usuário" }, "add-permissions": { - "save": "" + "save": "Salvar" }, "permission-list": { - "permission": "" + "permission": "Permissão" }, "permissions": { - "add-label": "", - "no-permissions": "", - "permissions-change-warning": "", - "role": "", + "add-label": "Adicionar uma permissão", + "no-permissions": "Não há permissões", + "permissions-change-warning": "Isto irá alterar as permissões para este diretório e todos os seus descendentes. No total, isto afetará:", + "role": "Função", "serviceaccount": "", "team": "", "title": "", From e96836d19eb52f0b764cd50f484976b40a3fb40e Mon Sep 17 00:00:00 2001 From: Darren Janeczek <38694490+darrenjaneczek@users.noreply.github.com> Date: Mon, 18 Mar 2024 11:00:31 -0400 Subject: [PATCH 0760/1406] datatrails: Remove prefix filter (#84661) * fix: use cascader's clear button * fix: remove the prefix filter from metric select * fix: remove supporting code in metric select scene - For the removed prefix filter * fix: spacing --- .../MetricCategory/MetricCategoryCascader.tsx | 45 +++++---------- .../app/features/trails/MetricSelectScene.tsx | 56 ++----------------- 2 files changed, 19 insertions(+), 82 deletions(-) diff --git a/public/app/features/trails/MetricCategory/MetricCategoryCascader.tsx b/public/app/features/trails/MetricCategory/MetricCategoryCascader.tsx index 6dfe028bbe..7dd6fa4c44 100644 --- a/public/app/features/trails/MetricCategory/MetricCategoryCascader.tsx +++ b/public/app/features/trails/MetricCategory/MetricCategoryCascader.tsx @@ -1,6 +1,6 @@ -import React, { useMemo, useReducer, useState } from 'react'; +import React, { useMemo } from 'react'; -import { Cascader, CascaderOption, HorizontalGroup, Button } from '@grafana/ui'; +import { Cascader, CascaderOption } from '@grafana/ui'; import { useMetricCategories } from './useMetricCategories'; @@ -15,36 +15,19 @@ export function MetricCategoryCascader({ metricNames, onSelect, disabled, initia const categoryTree = useMetricCategories(metricNames); const options = useMemo(() => createCasaderOptions(categoryTree), [categoryTree]); - const [disableClear, setDisableClear] = useState(initialValue == null); - - // Increments whenever clear is pressed, to reset the Cascader component - const [cascaderKey, resetCascader] = useReducer((x) => x + 1, 0); - - const clear = () => { - resetCascader(); - setDisableClear(true); - onSelect(undefined); - }; - return ( - - { - setDisableClear(!prefix); - onSelect(prefix); - }} - {...{ options, disabled, initialValue }} - /> - - + { + onSelect(prefix); + }} + {...{ options, disabled, initialValue }} + /> ); } diff --git a/public/app/features/trails/MetricSelectScene.tsx b/public/app/features/trails/MetricSelectScene.tsx index ee42c0ed54..0c2c467806 100644 --- a/public/app/features/trails/MetricSelectScene.tsx +++ b/public/app/features/trails/MetricSelectScene.tsx @@ -23,7 +23,6 @@ import { VariableHide } from '@grafana/schema'; import { Input, InlineSwitch, Field, Alert, Icon, useStyles2 } from '@grafana/ui'; import { getPreviewPanelFor } from './AutomaticMetricQueries/previewPanel'; -import { MetricCategoryCascader } from './MetricCategory/MetricCategoryCascader'; import { MetricScene } from './MetricScene'; import { SelectMetricAction } from './SelectMetricAction'; import { StatusWrapper } from './StatusWrapper'; @@ -44,9 +43,7 @@ export interface MetricSelectSceneState extends SceneObjectState { body: SceneCSSGridLayout; searchQuery?: string; showPreviews?: boolean; - prefixFilter?: string; metricsAfterSearch?: string[]; - metricsAfterFilter?: string[]; } const ROW_PREVIEW_HEIGHT = '175px'; @@ -158,32 +155,15 @@ export class MetricSelectScene extends SceneObjectBase { } } - private applyMetricPrefixFilter() { - // This should occur after an `applyMetricSearch`, or if the prefix filter has changed - const { metricsAfterSearch, prefixFilter } = this.state; - - if (!prefixFilter || !metricsAfterSearch) { - this.setState({ metricsAfterFilter: metricsAfterSearch }); - } else { - const metricsAfterFilter = metricsAfterSearch.filter((metric) => metric.startsWith(prefixFilter)); - this.setState({ metricsAfterFilter }); - } - } - private updateMetrics(applySearchAndFilter = true) { if (applySearchAndFilter) { // Set to false if these are not required (because they can be assumed to have been suitably called). this.applyMetricSearch(); - this.applyMetricPrefixFilter(); } - const { metricsAfterFilter } = this.state; + const { metricsAfterSearch } = this.state; - if (!metricsAfterFilter) { - return; - } - - const metricNames = metricsAfterFilter; + const metricNames = metricsAfterSearch || []; const trail = getTrailFor(this); const sortedMetricNames = trail.state.metric !== undefined ? sortRelatedMetrics(metricNames, trail.state.metric) : metricNames; @@ -302,29 +282,18 @@ export class MetricSelectScene extends SceneObjectBase { this.buildLayout(); }, 500); - public onPrefixFilterChange = (prefixFilter: string | undefined) => { - this.setState({ prefixFilter }); - this.prefixFilterChangedDebounced(); - }; - - private prefixFilterChangedDebounced = debounce(() => { - this.applyMetricPrefixFilter(); - this.updateMetrics(false); // Only needed to applyMetricPrefixFilter - this.buildLayout(); - }, 1000); - public onTogglePreviews = () => { this.setState({ showPreviews: !this.state.showPreviews }); this.buildLayout(); }; public static Component = ({ model }: SceneComponentProps) => { - const { searchQuery, showPreviews, body, metricsAfterSearch, metricsAfterFilter, prefixFilter } = model.useState(); + const { searchQuery, showPreviews, body } = model.useState(); const { children } = body.useState(); const styles = useStyles2(getStyles); const metricNamesStatus = useVariableStatus(VAR_METRIC_NAMES, model); - const tooStrict = children.length === 0 && (searchQuery || prefixFilter); + const tooStrict = children.length === 0 && searchQuery; const noMetrics = !metricNamesStatus.isLoading && model.currentMetricNames.size === 0; const isLoading = metricNamesStatus.isLoading && children.length === 0; @@ -335,11 +304,6 @@ export class MetricSelectScene extends SceneObjectBase { (tooStrict && 'There are no results found. Try adjusting your search or filters.') || undefined; - const prefixError = - prefixFilter && metricsAfterSearch != null && !metricsAfterFilter?.length - ? 'The current prefix filter is not available with the current search terms.' - : undefined; - const disableSearch = metricNamesStatus.error || metricNamesStatus.isLoading; return ( @@ -362,16 +326,6 @@ export class MetricSelectScene extends SceneObjectBase { disabled={disableSearch} />
-
- - - -
{metricNamesStatus.error && (
We are unable to connect to your data source. Double check your data source URL and credentials.
@@ -425,7 +379,7 @@ function getStyles(theme: GrafanaTheme2) { flexGrow: 0, display: 'flex', gap: theme.spacing(2), - marginBottom: theme.spacing(1), + marginBottom: theme.spacing(2), alignItems: 'flex-end', }), searchField: css({ From 677b765dab286d14eabbf6a5992cdf459f6f4347 Mon Sep 17 00:00:00 2001 From: Rob <35775181+morrro01@users.noreply.github.com> Date: Mon, 18 Mar 2024 10:26:22 -0500 Subject: [PATCH 0761/1406] NodeGraph: Edge color and stroke-dasharray support (#83855) * Adds color and stroke-dasharray support for node graph edges Adds support for providing color, highlighted color, and visual display of node graph edges as dashed lines via stroke-dasharray. * Updates node graph documentation * Updates documentation Adds default for `highlightedColor` * Update docs/sources/panels-visualizations/visualizations/node-graph/index.md Co-authored-by: Fabrizio <135109076+fabrizio-grafana@users.noreply.github.com> * Update packages/grafana-data/src/utils/nodeGraph.ts Co-authored-by: Fabrizio <135109076+fabrizio-grafana@users.noreply.github.com> * Update docs/sources/panels-visualizations/visualizations/node-graph/index.md Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> * Removes highlightedColor; deprecates highlighted Per [request](https://github.com/grafana/grafana/pull/83855#issuecomment-1999810826), deprecates `highlighted` in code and documentation, and removes `highlightedColor` as an additional property. `highlighted` will continue to be supported in its original state (makes the edge red), but is superseded if `color` is provided. * Update types.ts Missed a file in my last commit. Removes `highlightedColor` and deprecates `highlighted`. * Add test scenario in test data source --------- Co-authored-by: Fabrizio <135109076+fabrizio-grafana@users.noreply.github.com> Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> Co-authored-by: Andrej Ocenas --- .betterer.results | 3 - .../visualizations/node-graph/index.md | 22 +- packages/grafana-data/src/utils/nodeGraph.ts | 8 +- .../x/TestDataDataQuery_types.gen.ts | 2 +- .../kinds/dataquery/types_dataquery_gen.go | 9 +- .../components/NodeGraphEditor.tsx | 8 +- .../grafana-testdata-datasource/dataquery.cue | 2 +- .../dataquery.gen.ts | 2 +- .../grafana-testdata-datasource/datasource.ts | 5 +- .../nodeGraphUtils.ts | 229 +++++++++++++----- public/app/plugins/panel/nodeGraph/Edge.tsx | 13 +- public/app/plugins/panel/nodeGraph/types.ts | 5 + public/app/plugins/panel/nodeGraph/utils.ts | 11 + 13 files changed, 238 insertions(+), 81 deletions(-) diff --git a/.betterer.results b/.betterer.results index bd47ca293f..d1176ccb0d 100644 --- a/.betterer.results +++ b/.betterer.results @@ -4719,9 +4719,6 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"] ], - "public/app/plugins/datasource/grafana-testdata-datasource/nodeGraphUtils.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], "public/app/plugins/datasource/grafana-testdata-datasource/runStreams.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], diff --git a/docs/sources/panels-visualizations/visualizations/node-graph/index.md b/docs/sources/panels-visualizations/visualizations/node-graph/index.md index 96fe3cb7bf..eb28da6aea 100644 --- a/docs/sources/panels-visualizations/visualizations/node-graph/index.md +++ b/docs/sources/panels-visualizations/visualizations/node-graph/index.md @@ -104,13 +104,21 @@ Required fields: Optional fields: -| Field name | Type | Description | -| ------------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| mainstat | string/number | First stat shown in the overlay when hovering over the edge. It can be a string showing the value as is or it can be a number. If it is a number, any unit associated with that field is also shown | -| secondarystat | string/number | Same as mainStat, but shown right under it. | -| detail\_\_\* | string/number | Any field prefixed with `detail__` will be shown in the header of context menu when clicked on the edge. Use `config.displayName` for more human readable label. | -| thickness | number | The thickness of the edge. Default: `1` | -| highlighted | boolean | Sets whether the edge should be highlighted. Useful, for example, to represent a specific path in the graph by highlighting several nodes and edges. Default: `false` | +| Field name | Type | Description | +| --------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| mainstat | string/number | First stat shown in the overlay when hovering over the edge. It can be a string showing the value as is or it can be a number. If it is a number, any unit associated with that field is also shown | +| secondarystat | string/number | Same as mainStat, but shown right under it. | +| detail\_\_\* | string/number | Any field prefixed with `detail__` will be shown in the header of context menu when clicked on the edge. Use `config.displayName` for more human readable label. | +| thickness | number | The thickness of the edge. Default: `1` | +| highlighted | boolean | Sets whether the edge should be highlighted. Useful, for example, to represent a specific path in the graph by highlighting several nodes and edges. Default: `false` | +| color | string | Sets the default color of the edge. It can be an acceptable HTML color string. Default: `#999` | +| strokeDasharray | string | Sets the pattern of dashes and gaps used to render the edge. If unset, a solid line is used as edge. For more information and examples, refer to the [`stroke-dasharray` MDN documentation](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-dasharray). | + +{{< admonition type="caution" >}} +Starting with 10.5, `highlighted` is deprecated. +It will be removed in a future release. +Use `color` to indicate a highlighted edge state instead. +{{< /admonition >}} ### Nodes data frame structure diff --git a/packages/grafana-data/src/utils/nodeGraph.ts b/packages/grafana-data/src/utils/nodeGraph.ts index b846348a7a..4cb05d054c 100644 --- a/packages/grafana-data/src/utils/nodeGraph.ts +++ b/packages/grafana-data/src/utils/nodeGraph.ts @@ -15,7 +15,7 @@ export enum NodeGraphDataFrameFieldNames { // grafana/ui [nodes] icon = 'icon', // Defines a single color if string (hex or html named value) or color mode config can be used as threshold or - // gradient. arc__ fields must not be defined if used [nodes] + // gradient. arc__ fields must not be defined if used [nodes + edges] color = 'color', // Id of the source node [required] [edges] @@ -32,6 +32,10 @@ export enum NodeGraphDataFrameFieldNames { // Thickness of the edge [edges] thickness = 'thickness', - // Whether the node or edge should be highlighted (e.g., shown in red) in the UI + // Whether the node or edge should be highlighted (e.g., shown in red) in the UI [nodes + edges] + // @deprecated -- for edges use color instead highlighted = 'highlighted', + + // Defines the stroke dash array for the edge [edges]. See SVG strokeDasharray definition for syntax. + strokeDasharray = 'strokedasharray', } diff --git a/packages/grafana-schema/src/raw/composable/testdata/dataquery/x/TestDataDataQuery_types.gen.ts b/packages/grafana-schema/src/raw/composable/testdata/dataquery/x/TestDataDataQuery_types.gen.ts index 329a6feafd..b7f57fd7e2 100644 --- a/packages/grafana-schema/src/raw/composable/testdata/dataquery/x/TestDataDataQuery_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/testdata/dataquery/x/TestDataDataQuery_types.gen.ts @@ -75,7 +75,7 @@ export interface SimulationQuery { export interface NodesQuery { count?: number; seed?: number; - type?: ('random' | 'response_small' | 'response_medium' | 'random edges'); + type?: ('random' | 'response_small' | 'response_medium' | 'random edges' | 'feature_showcase'); } export interface USAQuery { diff --git a/pkg/tsdb/grafana-testdata-datasource/kinds/dataquery/types_dataquery_gen.go b/pkg/tsdb/grafana-testdata-datasource/kinds/dataquery/types_dataquery_gen.go index 3013282e00..95fec94c96 100644 --- a/pkg/tsdb/grafana-testdata-datasource/kinds/dataquery/types_dataquery_gen.go +++ b/pkg/tsdb/grafana-testdata-datasource/kinds/dataquery/types_dataquery_gen.go @@ -11,10 +11,11 @@ package dataquery // Defines values for NodesQueryType. const ( - NodesQueryTypeRandom NodesQueryType = "random" - NodesQueryTypeRandomEdges NodesQueryType = "random edges" - NodesQueryTypeResponseMedium NodesQueryType = "response_medium" - NodesQueryTypeResponseSmall NodesQueryType = "response_small" + NodesQueryTypeFeatureShowcase NodesQueryType = "feature_showcase" + NodesQueryTypeRandom NodesQueryType = "random" + NodesQueryTypeRandomEdges NodesQueryType = "random edges" + NodesQueryTypeResponseMedium NodesQueryType = "response_medium" + NodesQueryTypeResponseSmall NodesQueryType = "response_small" ) // Defines values for StreamingQueryType. diff --git a/public/app/plugins/datasource/grafana-testdata-datasource/components/NodeGraphEditor.tsx b/public/app/plugins/datasource/grafana-testdata-datasource/components/NodeGraphEditor.tsx index 9e1c3d626e..207b39fd52 100644 --- a/public/app/plugins/datasource/grafana-testdata-datasource/components/NodeGraphEditor.tsx +++ b/public/app/plugins/datasource/grafana-testdata-datasource/components/NodeGraphEditor.tsx @@ -54,4 +54,10 @@ export function NodeGraphEditor({ query, onChange }: Props) { ); } -const options: Array = ['random', 'response_small', 'response_medium', 'random edges']; +const options: Array = [ + 'random', + 'response_small', + 'response_medium', + 'random edges', + 'feature_showcase', +]; diff --git a/public/app/plugins/datasource/grafana-testdata-datasource/dataquery.cue b/public/app/plugins/datasource/grafana-testdata-datasource/dataquery.cue index da51680346..b8e0d75d1c 100644 --- a/public/app/plugins/datasource/grafana-testdata-datasource/dataquery.cue +++ b/public/app/plugins/datasource/grafana-testdata-datasource/dataquery.cue @@ -83,7 +83,7 @@ composableKinds: DataQuery: { } @cuetsy(kind="interface") #NodesQuery: { - type?: "random" | "response_small" | "response_medium" | "random edges" + type?: "random" | "response_small" | "response_medium" | "random edges" | "feature_showcase" count?: int64 seed?: int64 } @cuetsy(kind="interface") diff --git a/public/app/plugins/datasource/grafana-testdata-datasource/dataquery.gen.ts b/public/app/plugins/datasource/grafana-testdata-datasource/dataquery.gen.ts index f7fc07ecb3..0d45320c19 100644 --- a/public/app/plugins/datasource/grafana-testdata-datasource/dataquery.gen.ts +++ b/public/app/plugins/datasource/grafana-testdata-datasource/dataquery.gen.ts @@ -73,7 +73,7 @@ export interface SimulationQuery { export interface NodesQuery { count?: number; seed?: number; - type?: ('random' | 'response_small' | 'response_medium' | 'random edges'); + type?: ('random' | 'response_small' | 'response_medium' | 'random edges' | 'feature_showcase'); } export interface USAQuery { diff --git a/public/app/plugins/datasource/grafana-testdata-datasource/datasource.ts b/public/app/plugins/datasource/grafana-testdata-datasource/datasource.ts index 7c486ddacb..d575ad29ba 100644 --- a/public/app/plugins/datasource/grafana-testdata-datasource/datasource.ts +++ b/public/app/plugins/datasource/grafana-testdata-datasource/datasource.ts @@ -22,7 +22,7 @@ import { DataSourceWithBackend, getBackendSrv, getGrafanaLiveSrv, getTemplateSrv import { Scenario, TestDataDataQuery, TestDataQueryType } from './dataquery.gen'; import { queryMetricTree } from './metricTree'; -import { generateRandomEdges, generateRandomNodes, savedNodesResponse } from './nodeGraphUtils'; +import { generateRandomEdges, generateRandomNodes, generateShowcaseData, savedNodesResponse } from './nodeGraphUtils'; import { runStream } from './runStreams'; import { flameGraphData, flameGraphDataDiff } from './testData/flameGraphResponse'; import { TestDataVariableSupport } from './variables'; @@ -237,6 +237,9 @@ export class TestDataDataSource extends DataSourceWithBackend const type = target.nodes?.type || 'random'; let frames: DataFrame[]; switch (type) { + case 'feature_showcase': + frames = generateShowcaseData(); + break; case 'random': frames = generateRandomNodes(target.nodes?.count, target.nodes?.seed); break; diff --git a/public/app/plugins/datasource/grafana-testdata-datasource/nodeGraphUtils.ts b/public/app/plugins/datasource/grafana-testdata-datasource/nodeGraphUtils.ts index c575d25b8e..3811b61890 100644 --- a/public/app/plugins/datasource/grafana-testdata-datasource/nodeGraphUtils.ts +++ b/public/app/plugins/datasource/grafana-testdata-datasource/nodeGraphUtils.ts @@ -7,6 +7,7 @@ import { MutableDataFrame, NodeGraphDataFrameFieldNames, DataFrame, + addRow, } from '@grafana/data'; import * as serviceMapResponseSmall from './testData/serviceMapResponse'; @@ -56,7 +57,74 @@ export function generateRandomNodes(count = 10, seed?: number) { nodes[sourceIndex].edges.push(nodes[targetIndex].id); } - const nodeFields: Record & { values: any[] }> = { + const { nodesFields, nodesFrame, edgesFrame } = makeDataFrames(); + + const edgesSet = new Set(); + for (const node of nodes) { + nodesFields.id.values.push(node.id); + nodesFields.title.values.push(node.title); + nodesFields[NodeGraphDataFrameFieldNames.subTitle].values.push(node.subTitle); + nodesFields[NodeGraphDataFrameFieldNames.mainStat].values.push(node.stat1); + nodesFields[NodeGraphDataFrameFieldNames.secondaryStat].values.push(node.stat2); + nodesFields.arc__success.values.push(node.success); + nodesFields.arc__errors.values.push(node.error); + const rnd = Math.random(); + nodesFields[NodeGraphDataFrameFieldNames.icon].values.push(rnd > 0.9 ? 'database' : rnd < 0.1 ? 'cloud' : ''); + nodesFields[NodeGraphDataFrameFieldNames.nodeRadius].values.push(Math.max(rnd * 100, 30)); // ensure a minimum radius of 30 or icons will not fit well in the node + nodesFields[NodeGraphDataFrameFieldNames.highlighted].values.push(Math.random() > 0.5); + + for (const edge of node.edges) { + const id = `${node.id}--${edge}`; + // We can have duplicate edges when we added some more by random + if (edgesSet.has(id)) { + continue; + } + edgesSet.add(id); + edgesFrame.fields[0].values.push(`${node.id}--${edge}`); + edgesFrame.fields[1].values.push(node.id); + edgesFrame.fields[2].values.push(edge); + edgesFrame.fields[3].values.push(Math.random() * 100); + edgesFrame.fields[4].values.push(Math.random() > 0.5); + edgesFrame.fields[5].values.push(Math.ceil(Math.random() * 15)); + } + } + edgesFrame.length = edgesFrame.fields[0].values.length; + + return [nodesFrame, edgesFrame]; +} + +function makeRandomNode(index: number) { + const success = Math.random(); + const error = 1 - success; + return { + id: `service:${index}`, + title: `service:${index}`, + subTitle: 'service', + success, + error, + stat1: Math.random(), + stat2: Math.random(), + edges: [], + highlighted: Math.random() > 0.5, + }; +} + +export function savedNodesResponse(size: 'small' | 'medium'): [DataFrame, DataFrame] { + const response = size === 'small' ? serviceMapResponseSmall : serviceMapResponsMedium; + return [new MutableDataFrame(response.nodes), new MutableDataFrame(response.edges)]; +} + +// Generates node graph data but only returns the edges +export function generateRandomEdges(count = 10, seed = 1) { + return generateRandomNodes(count, seed)[1]; +} + +function makeDataFrames(): { + nodesFrame: DataFrame; + edgesFrame: DataFrame; + nodesFields: Record & { values: unknown[] }>; +} { + const nodesFields: Record & { values: unknown[] }> = { [NodeGraphDataFrameFieldNames.id]: { values: [], type: FieldType.string, @@ -114,12 +182,20 @@ export function generateRandomNodes(count = 10, seed?: number) { values: [], type: FieldType.boolean, }, + + [NodeGraphDataFrameFieldNames.detail + 'test_value']: { + values: [], + config: { + displayName: 'Test value', + }, + type: FieldType.number, + }, }; - const nodeFrame = new MutableDataFrame({ + const nodesFrame = new MutableDataFrame({ name: 'nodes', - fields: Object.keys(nodeFields).map((key) => ({ - ...nodeFields[key], + fields: Object.keys(nodesFields).map((key) => ({ + ...nodesFields[key], name: key, })), meta: { preferredVisualisationType: 'nodeGraph' }, @@ -134,67 +210,106 @@ export function generateRandomNodes(count = 10, seed?: number) { { name: NodeGraphDataFrameFieldNames.mainStat, values: [], type: FieldType.number, config: {} }, { name: NodeGraphDataFrameFieldNames.highlighted, values: [], type: FieldType.boolean, config: {} }, { name: NodeGraphDataFrameFieldNames.thickness, values: [], type: FieldType.number, config: {} }, + { name: NodeGraphDataFrameFieldNames.color, values: [], type: FieldType.string, config: {} }, + { name: NodeGraphDataFrameFieldNames.strokeDasharray, values: [], type: FieldType.string, config: {} }, ], meta: { preferredVisualisationType: 'nodeGraph' }, length: 0, }; - const edgesSet = new Set(); - for (const node of nodes) { - nodeFields.id.values.push(node.id); - nodeFields.title.values.push(node.title); - nodeFields[NodeGraphDataFrameFieldNames.subTitle].values.push(node.subTitle); - nodeFields[NodeGraphDataFrameFieldNames.mainStat].values.push(node.stat1); - nodeFields[NodeGraphDataFrameFieldNames.secondaryStat].values.push(node.stat2); - nodeFields.arc__success.values.push(node.success); - nodeFields.arc__errors.values.push(node.error); - const rnd = Math.random(); - nodeFields[NodeGraphDataFrameFieldNames.icon].values.push(rnd > 0.9 ? 'database' : rnd < 0.1 ? 'cloud' : ''); - nodeFields[NodeGraphDataFrameFieldNames.nodeRadius].values.push(Math.max(rnd * 100, 30)); // ensure a minimum radius of 30 or icons will not fit well in the node - nodeFields[NodeGraphDataFrameFieldNames.highlighted].values.push(Math.random() > 0.5); + return { nodesFrame, edgesFrame, nodesFields }; +} - for (const edge of node.edges) { - const id = `${node.id}--${edge}`; - // We can have duplicate edges when we added some more by random - if (edgesSet.has(id)) { - continue; - } - edgesSet.add(id); - edgesFrame.fields[0].values.push(`${node.id}--${edge}`); - edgesFrame.fields[1].values.push(node.id); - edgesFrame.fields[2].values.push(edge); - edgesFrame.fields[3].values.push(Math.random() * 100); - edgesFrame.fields[4].values.push(Math.random() > 0.5); - edgesFrame.fields[5].values.push(Math.ceil(Math.random() * 15)); - } - } - edgesFrame.length = edgesFrame.fields[0].values.length; +export function generateShowcaseData() { + const { nodesFrame, edgesFrame } = makeDataFrames(); - return [nodeFrame, edgesFrame]; -} + addRow(nodesFrame, { + [NodeGraphDataFrameFieldNames.id]: 'root', + [NodeGraphDataFrameFieldNames.title]: 'root', + [NodeGraphDataFrameFieldNames.subTitle]: 'client', + [NodeGraphDataFrameFieldNames.mainStat]: 1234, + [NodeGraphDataFrameFieldNames.secondaryStat]: 5678, + arc__success: 0.5, + arc__errors: 0.5, + [NodeGraphDataFrameFieldNames.icon]: '', + [NodeGraphDataFrameFieldNames.nodeRadius]: 40, + [NodeGraphDataFrameFieldNames.highlighted]: false, + [NodeGraphDataFrameFieldNames.detail + 'test_value']: 1, + }); -function makeRandomNode(index: number) { - const success = Math.random(); - const error = 1 - success; - return { - id: `service:${index}`, - title: `service:${index}`, - subTitle: 'service', - success, - error, - stat1: Math.random(), - stat2: Math.random(), - edges: [], - highlighted: Math.random() > 0.5, - }; -} + addRow(nodesFrame, { + [NodeGraphDataFrameFieldNames.id]: 'app_service', + [NodeGraphDataFrameFieldNames.title]: 'app service', + [NodeGraphDataFrameFieldNames.subTitle]: 'with icon', + [NodeGraphDataFrameFieldNames.mainStat]: 1.2, + [NodeGraphDataFrameFieldNames.secondaryStat]: 2.3, + arc__success: 1, + arc__errors: 0, + [NodeGraphDataFrameFieldNames.icon]: 'apps', + [NodeGraphDataFrameFieldNames.nodeRadius]: 40, + [NodeGraphDataFrameFieldNames.highlighted]: false, + [NodeGraphDataFrameFieldNames.detail + 'test_value']: 42, + }); -export function savedNodesResponse(size: 'small' | 'medium'): [DataFrame, DataFrame] { - const response = size === 'small' ? serviceMapResponseSmall : serviceMapResponsMedium; - return [new MutableDataFrame(response.nodes), new MutableDataFrame(response.edges)]; -} + addRow(edgesFrame, { + [NodeGraphDataFrameFieldNames.id]: 'root-app_service', + [NodeGraphDataFrameFieldNames.source]: 'root', + [NodeGraphDataFrameFieldNames.target]: 'app_service', + [NodeGraphDataFrameFieldNames.mainStat]: 3.4, + [NodeGraphDataFrameFieldNames.secondaryStat]: 4.5, + [NodeGraphDataFrameFieldNames.thickness]: 4, + [NodeGraphDataFrameFieldNames.color]: '', + [NodeGraphDataFrameFieldNames.strokeDasharray]: '', + }); -// Generates node graph data but only returns the edges -export function generateRandomEdges(count = 10, seed = 1) { - return generateRandomNodes(count, seed)[1]; + addRow(nodesFrame, { + [NodeGraphDataFrameFieldNames.id]: 'auth_service', + [NodeGraphDataFrameFieldNames.title]: 'auth service', + [NodeGraphDataFrameFieldNames.subTitle]: 'highlighted', + [NodeGraphDataFrameFieldNames.mainStat]: 3.4, + [NodeGraphDataFrameFieldNames.secondaryStat]: 4.5, + arc__success: 0, + arc__errors: 1, + [NodeGraphDataFrameFieldNames.icon]: '', + [NodeGraphDataFrameFieldNames.nodeRadius]: 40, + [NodeGraphDataFrameFieldNames.highlighted]: true, + }); + + addRow(edgesFrame, { + [NodeGraphDataFrameFieldNames.id]: 'root-auth_service', + [NodeGraphDataFrameFieldNames.source]: 'root', + [NodeGraphDataFrameFieldNames.target]: 'auth_service', + [NodeGraphDataFrameFieldNames.mainStat]: 113.4, + [NodeGraphDataFrameFieldNames.secondaryStat]: 4.511, + [NodeGraphDataFrameFieldNames.thickness]: 8, + [NodeGraphDataFrameFieldNames.color]: 'red', + [NodeGraphDataFrameFieldNames.strokeDasharray]: '', + }); + + addRow(nodesFrame, { + [NodeGraphDataFrameFieldNames.id]: 'db', + [NodeGraphDataFrameFieldNames.title]: 'db', + [NodeGraphDataFrameFieldNames.subTitle]: 'bigger size', + [NodeGraphDataFrameFieldNames.mainStat]: 9876.123, + [NodeGraphDataFrameFieldNames.secondaryStat]: 123.9876, + arc__success: 0.9, + arc__errors: 0.1, + [NodeGraphDataFrameFieldNames.icon]: 'database', + [NodeGraphDataFrameFieldNames.nodeRadius]: 60, + [NodeGraphDataFrameFieldNames.highlighted]: false, + [NodeGraphDataFrameFieldNames.detail + 'test_value']: 1357, + }); + + addRow(edgesFrame, { + [NodeGraphDataFrameFieldNames.id]: 'auth_service-db', + [NodeGraphDataFrameFieldNames.source]: 'auth_service', + [NodeGraphDataFrameFieldNames.target]: 'db', + [NodeGraphDataFrameFieldNames.mainStat]: 1139.4, + [NodeGraphDataFrameFieldNames.secondaryStat]: 477.511, + [NodeGraphDataFrameFieldNames.thickness]: 2, + [NodeGraphDataFrameFieldNames.color]: 'blue', + [NodeGraphDataFrameFieldNames.strokeDasharray]: '2 2', + }); + + return [nodesFrame, edgesFrame]; } diff --git a/public/app/plugins/panel/nodeGraph/Edge.tsx b/public/app/plugins/panel/nodeGraph/Edge.tsx index b31c49be49..9a9459e9c1 100644 --- a/public/app/plugins/panel/nodeGraph/Edge.tsx +++ b/public/app/plugins/panel/nodeGraph/Edge.tsx @@ -5,7 +5,7 @@ import { computeNodeCircumferenceStrokeWidth, nodeR } from './Node'; import { EdgeDatum, NodeDatum } from './types'; import { shortenLine } from './utils'; -export const highlightedEdgeColor = '#a00'; +export const defaultHighlightedEdgeColor = '#a00'; export const defaultEdgeColor = '#999'; interface Props { @@ -41,12 +41,18 @@ export const Edge = memo(function Edge(props: Props) { arrowHeadHeight ); + const edgeColor = edge.color || defaultEdgeColor; + + // @deprecated -- until 'highlighted' is removed we'll prioritize 'color' + // in case both are provided + const highlightedEdgeColor = edge.color || defaultHighlightedEdgeColor; + const markerId = `triangle-${edge.id}`; const coloredMarkerId = `triangle-colored-${edge.id}`; return ( <> - + onClick(event, edge)} @@ -55,11 +61,12 @@ export const Edge = memo(function Edge(props: Props) { > Date: Mon, 18 Mar 2024 16:12:00 +0000 Subject: [PATCH 0762/1406] Variables: Support static keys in AdHocFiltersVariable (#83157) * initial start * don't use getTagKeysProvider * some cleanup * undo kinds adjustment * simplify * remove async declaration * add description and a couple of unit tests * add transformSaveModelToScene test * add tests for AdHocVariableForm * add tests for AdHocFiltersVariableEditor * update to defaultKeys * fix snapshots * update to 3.13.3 --- package.json | 2 +- .../grafana-data/src/types/templateVars.ts | 5 ++ .../src/selectors/pages.ts | 1 + .../sceneVariablesSetToVariables.test.ts | 85 +++++++++++++++++++ .../sceneVariablesSetToVariables.ts | 3 +- .../transformSaveModelToScene.test.ts | 82 ++++++++++++++++++ .../transformSaveModelToScene.ts | 1 + .../components/AdHocVariableForm.test.tsx | 71 +++++++++++++--- .../components/AdHocVariableForm.tsx | 61 +++++++++++-- .../components/GroupByVariableForm.tsx | 5 +- .../AdHocFiltersVariableEditor.test.tsx | 25 +++++- .../editors/AdHocFiltersVariableEditor.tsx | 20 ++++- yarn.lock | 10 +-- 13 files changed, 337 insertions(+), 34 deletions(-) diff --git a/package.json b/package.json index 20ca52b91f..90add7b5a5 100644 --- a/package.json +++ b/package.json @@ -250,7 +250,7 @@ "@grafana/o11y-ds-frontend": "workspace:*", "@grafana/prometheus": "workspace:*", "@grafana/runtime": "workspace:*", - "@grafana/scenes": "^3.11.0", + "@grafana/scenes": "3.13.3", "@grafana/schema": "workspace:*", "@grafana/sql": "workspace:*", "@grafana/ui": "workspace:*", diff --git a/packages/grafana-data/src/types/templateVars.ts b/packages/grafana-data/src/types/templateVars.ts index b088ed63c8..1b01e5b9e5 100644 --- a/packages/grafana-data/src/types/templateVars.ts +++ b/packages/grafana-data/src/types/templateVars.ts @@ -1,4 +1,5 @@ import { LoadingState } from './data'; +import { MetricFindValue } from './datasource'; import { DataSourceRef } from './query'; export type VariableType = TypedVariableModel['type']; @@ -63,6 +64,10 @@ export interface AdHocVariableModel extends BaseVariableModel { * Filters that are always applied to the lookup of keys. Not shown in the AdhocFilterBuilder UI. */ baseFilters?: AdHocVariableFilter[]; + /** + * Static keys that override any dynamic keys from the datasource. + */ + defaultKeys?: MetricFindValue[]; } export interface GroupByVariableModel extends VariableWithOptions { diff --git a/packages/grafana-e2e-selectors/src/selectors/pages.ts b/packages/grafana-e2e-selectors/src/selectors/pages.ts index ae54d0ce94..7b820ee07f 100644 --- a/packages/grafana-e2e-selectors/src/selectors/pages.ts +++ b/packages/grafana-e2e-selectors/src/selectors/pages.ts @@ -195,6 +195,7 @@ export const Pages = { AdHocFiltersVariable: { datasourceSelect: Components.DataSourcePicker.inputV2, infoText: 'data-testid ad-hoc filters variable info text', + modeToggle: 'data-testid ad-hoc filters variable mode toggle', }, }, }, diff --git a/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.test.ts b/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.test.ts index e6b1c6576e..1d3c6c2b33 100644 --- a/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.test.ts +++ b/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.test.ts @@ -388,6 +388,91 @@ describe('sceneVariablesSetToVariables', () => { "type": "fake-std", "uid": "fake-std", }, + "defaultKeys": undefined, + "description": "test-desc", + "filters": [ + { + "key": "filterTest", + "operator": "=", + "value": "test", + }, + ], + "label": "test-label", + "name": "test", + "type": "adhoc", + } + `); + }); + + it('should handle AdHocFiltersVariable with defaultKeys', () => { + const variable = new AdHocFiltersVariable({ + name: 'test', + label: 'test-label', + description: 'test-desc', + datasource: { uid: 'fake-std', type: 'fake-std' }, + defaultKeys: [ + { + text: 'some', + value: '1', + }, + { + text: 'static', + value: '2', + }, + { + text: 'keys', + value: '3', + }, + ], + filters: [ + { + key: 'filterTest', + operator: '=', + value: 'test', + }, + ], + baseFilters: [ + { + key: 'baseFilterTest', + operator: '=', + value: 'test', + }, + ], + }); + const set = new SceneVariableSet({ + variables: [variable], + }); + + const result = sceneVariablesSetToVariables(set); + + expect(result).toHaveLength(1); + expect(result[0]).toMatchInlineSnapshot(` + { + "baseFilters": [ + { + "key": "baseFilterTest", + "operator": "=", + "value": "test", + }, + ], + "datasource": { + "type": "fake-std", + "uid": "fake-std", + }, + "defaultKeys": [ + { + "text": "some", + "value": "1", + }, + { + "text": "static", + "value": "2", + }, + { + "text": "keys", + "value": "3", + }, + ], "description": "test-desc", "filters": [ { diff --git a/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.ts b/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.ts index 28ff40870b..5f3813c270 100644 --- a/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.ts +++ b/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.ts @@ -124,12 +124,13 @@ export function sceneVariablesSetToVariables(set: SceneVariables) { } else if (sceneUtils.isAdHocVariable(variable)) { variables.push({ ...commonProperties, - name: variable.state.name!, + name: variable.state.name, type: 'adhoc', datasource: variable.state.datasource, // @ts-expect-error baseFilters: variable.state.baseFilters, filters: variable.state.filters, + defaultKeys: variable.state.defaultKeys, }); } else { throw new Error('Unsupported variable type'); diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts index 2d5d1fe1b0..2d937285f1 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts @@ -965,6 +965,88 @@ describe('transformSaveModelToScene', () => { }); }); + it('should migrate adhoc variable with default keys', () => { + const variable: TypedVariableModel = { + id: 'adhoc', + global: false, + index: 0, + state: LoadingState.Done, + error: null, + name: 'adhoc', + label: 'Adhoc Label', + description: 'Adhoc Description', + type: 'adhoc', + rootStateKey: 'N4XLmH5Vz', + datasource: { + uid: 'gdev-prometheus', + type: 'prometheus', + }, + filters: [ + { + key: 'filterTest', + operator: '=', + value: 'test', + }, + ], + baseFilters: [ + { + key: 'baseFilterTest', + operator: '=', + value: 'test', + }, + ], + defaultKeys: [ + { + text: 'some', + value: '1', + }, + { + text: 'static', + value: '2', + }, + { + text: 'keys', + value: '3', + }, + ], + hide: 0, + skipUrlSync: false, + }; + + const migrated = createSceneVariableFromVariableModel(variable) as AdHocFiltersVariable; + const filterVarState = migrated.state; + + expect(migrated).toBeInstanceOf(AdHocFiltersVariable); + expect(filterVarState).toEqual({ + key: expect.any(String), + description: 'Adhoc Description', + hide: 0, + label: 'Adhoc Label', + name: 'adhoc', + skipUrlSync: false, + type: 'adhoc', + filterExpression: 'filterTest="test"', + filters: [{ key: 'filterTest', operator: '=', value: 'test' }], + baseFilters: [{ key: 'baseFilterTest', operator: '=', value: 'test' }], + datasource: { uid: 'gdev-prometheus', type: 'prometheus' }, + applyMode: 'auto', + defaultKeys: [ + { + text: 'some', + value: '1', + }, + { + text: 'static', + value: '2', + }, + { + text: 'keys', + value: '3', + }, + ], + }); + }); + describe('when groupByVariable feature toggle is enabled', () => { beforeAll(() => { config.featureToggles.groupByVariable = true; diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts index d2c415eced..015146d7b5 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts @@ -344,6 +344,7 @@ export function createSceneVariableFromVariableModel(variable: TypedVariableMode applyMode: 'auto', filters: variable.filters ?? [], baseFilters: variable.baseFilters ?? [], + defaultKeys: variable.defaultKeys, }); } if (variable.type === 'custom') { diff --git a/public/app/features/dashboard-scene/settings/variables/components/AdHocVariableForm.test.tsx b/public/app/features/dashboard-scene/settings/variables/components/AdHocVariableForm.test.tsx index 498483266d..610a2a9eea 100644 --- a/public/app/features/dashboard-scene/settings/variables/components/AdHocVariableForm.test.tsx +++ b/public/app/features/dashboard-scene/settings/variables/components/AdHocVariableForm.test.tsx @@ -1,11 +1,11 @@ -import { act, render } from '@testing-library/react'; +import { act, render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; import { selectors } from '@grafana/e2e-selectors'; import { mockDataSource } from 'app/features/alerting/unified/mocks'; -import { AdHocVariableForm } from './AdHocVariableForm'; +import { AdHocVariableForm, AdHocVariableFormProps } from './AdHocVariableForm'; const defaultDatasource = mockDataSource({ name: 'Default Test Data Source', @@ -29,13 +29,15 @@ jest.mock('@grafana/runtime/src/services/dataSourceSrv', () => ({ })); describe('AdHocVariableForm', () => { + const onDataSourceChange = jest.fn(); + const defaultProps: AdHocVariableFormProps = { + datasource: defaultDatasource, + onDataSourceChange, + infoText: 'Test Info', + }; + it('should render the form with the provided data source', async () => { - const onDataSourceChange = jest.fn(); - const { renderer } = await setup({ - datasource: defaultDatasource, - onDataSourceChange, - infoText: 'Test Info', - }); + const { renderer } = await setup(defaultProps); const dataSourcePicker = renderer.getByTestId( selectors.pages.Dashboard.Settings.Variables.Edit.AdHocFiltersVariable.datasourceSelect @@ -51,12 +53,7 @@ describe('AdHocVariableForm', () => { }); it('should call the onDataSourceChange callback when the data source is changed', async () => { - const onDataSourceChange = jest.fn(); - const { renderer, user } = await setup({ - datasource: defaultDatasource, - onDataSourceChange, - infoText: 'Test Info', - }); + const { renderer, user } = await setup(defaultProps); // Simulate changing the data source await user.click(renderer.getByTestId(selectors.components.DataSourcePicker.inputV2)); @@ -65,6 +62,52 @@ describe('AdHocVariableForm', () => { expect(onDataSourceChange).toHaveBeenCalledTimes(1); expect(onDataSourceChange).toHaveBeenCalledWith(promDatasource, undefined); }); + + it('should not render code editor when no default keys provided', async () => { + await setup(defaultProps); + + expect(screen.queryByTestId(selectors.components.CodeEditor.container)).not.toBeInTheDocument(); + }); + + it('should render code editor when defaultKeys and onDefaultKeysChange are provided', async () => { + const mockOnStaticKeysChange = jest.fn(); + await setup({ + ...defaultProps, + defaultKeys: [{ text: 'test', value: 'test' }], + onDefaultKeysChange: mockOnStaticKeysChange, + }); + + expect(await screen.findByTestId(selectors.components.CodeEditor.container)).toBeInTheDocument(); + }); + + it('should call onDefaultKeysChange when toggling on default options', async () => { + const mockOnStaticKeysChange = jest.fn(); + await setup({ + ...defaultProps, + onDefaultKeysChange: mockOnStaticKeysChange, + }); + + await userEvent.click( + screen.getByTestId(selectors.pages.Dashboard.Settings.Variables.Edit.AdHocFiltersVariable.modeToggle) + ); + expect(mockOnStaticKeysChange).toHaveBeenCalledTimes(1); + expect(mockOnStaticKeysChange).toHaveBeenCalledWith([]); + }); + + it('should call onDefaultKeysChange when toggling off default options', async () => { + const mockOnStaticKeysChange = jest.fn(); + await setup({ + ...defaultProps, + defaultKeys: [{ text: 'test', value: 'test' }], + onDefaultKeysChange: mockOnStaticKeysChange, + }); + + await userEvent.click( + screen.getByTestId(selectors.pages.Dashboard.Settings.Variables.Edit.AdHocFiltersVariable.modeToggle) + ); + expect(mockOnStaticKeysChange).toHaveBeenCalledTimes(1); + expect(mockOnStaticKeysChange).toHaveBeenCalledWith(undefined); + }); }); async function setup(props?: React.ComponentProps) { diff --git a/public/app/features/dashboard-scene/settings/variables/components/AdHocVariableForm.tsx b/public/app/features/dashboard-scene/settings/variables/components/AdHocVariableForm.tsx index fe093a380b..415e9f6cc2 100644 --- a/public/app/features/dashboard-scene/settings/variables/components/AdHocVariableForm.tsx +++ b/public/app/features/dashboard-scene/settings/variables/components/AdHocVariableForm.tsx @@ -1,20 +1,41 @@ -import React from 'react'; +import React, { useCallback } from 'react'; -import { DataSourceInstanceSettings } from '@grafana/data'; +import { DataSourceInstanceSettings, MetricFindValue, readCSV } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { DataSourceRef } from '@grafana/schema'; -import { Alert, Field } from '@grafana/ui'; +import { Alert, CodeEditor, Field, Switch } from '@grafana/ui'; import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker'; import { VariableLegend } from './VariableLegend'; -interface AdHocVariableFormProps { +export interface AdHocVariableFormProps { datasource?: DataSourceRef; onDataSourceChange: (dsSettings: DataSourceInstanceSettings) => void; infoText?: string; + defaultKeys?: MetricFindValue[]; + onDefaultKeysChange?: (keys?: MetricFindValue[]) => void; } -export function AdHocVariableForm({ datasource, infoText, onDataSourceChange }: AdHocVariableFormProps) { +export function AdHocVariableForm({ + datasource, + infoText, + onDataSourceChange, + onDefaultKeysChange, + defaultKeys, +}: AdHocVariableFormProps) { + const updateStaticKeys = useCallback( + (csvContent: string) => { + const df = readCSV('key,value\n' + csvContent)[0]; + const options = []; + for (let i = 0; i < df.length; i++) { + options.push({ text: df.fields[0].values[i], value: df.fields[1].values[i] }); + } + + onDefaultKeysChange?.(options); + }, + [onDefaultKeysChange] + ); + return ( <> Ad-hoc options @@ -29,6 +50,36 @@ export function AdHocVariableForm({ datasource, infoText, onDataSourceChange }: data-testid={selectors.pages.Dashboard.Settings.Variables.Edit.AdHocFiltersVariable.infoText} /> ) : null} + + {onDefaultKeysChange && ( + <> + + { + if (defaultKeys === undefined) { + onDefaultKeysChange([]); + } else { + onDefaultKeysChange(undefined); + } + }} + /> + + + {defaultKeys !== undefined && ( + `${o.text},${o.value}`).join('\n')} + onBlur={updateStaticKeys} + onSave={updateStaticKeys} + showMiniMap={false} + showLineNumbers={true} + /> + )} + + )} ); } diff --git a/public/app/features/dashboard-scene/settings/variables/components/GroupByVariableForm.tsx b/public/app/features/dashboard-scene/settings/variables/components/GroupByVariableForm.tsx index 194ff36417..f982ab9236 100644 --- a/public/app/features/dashboard-scene/settings/variables/components/GroupByVariableForm.tsx +++ b/public/app/features/dashboard-scene/settings/variables/components/GroupByVariableForm.tsx @@ -51,10 +51,7 @@ export function GroupByVariableForm({ /> ) : null} - + { expect(variable.state.datasource).toEqual({ uid: 'prometheus', type: 'prometheus' }); }); + + it('should update the variable default keys when the default keys options is enabled', async () => { + const { renderer, variable, user } = await setup(); + + // Simulate toggling default options on + await user.click( + renderer.getByTestId(selectors.pages.Dashboard.Settings.Variables.Edit.AdHocFiltersVariable.modeToggle) + ); + + expect(variable.state.defaultKeys).toEqual([]); + }); + + it('should update the variable default keys when the default keys option is disabled', async () => { + const { renderer, variable, user } = await setup(undefined, true); + + // Simulate toggling default options off + await user.click( + renderer.getByTestId(selectors.pages.Dashboard.Settings.Variables.Edit.AdHocFiltersVariable.modeToggle) + ); + + expect(variable.state.defaultKeys).toEqual(undefined); + }); }); -async function setup(props?: React.ComponentProps) { +async function setup(props?: React.ComponentProps, withDefaultKeys = false) { const onRunQuery = jest.fn(); const variable = new AdHocFiltersVariable({ name: 'adhocVariable', @@ -110,6 +132,7 @@ async function setup(props?: React.ComponentProps diff --git a/public/app/features/dashboard-scene/settings/variables/editors/AdHocFiltersVariableEditor.tsx b/public/app/features/dashboard-scene/settings/variables/editors/AdHocFiltersVariableEditor.tsx index 37daff7cb5..8aa2a2a6da 100644 --- a/public/app/features/dashboard-scene/settings/variables/editors/AdHocFiltersVariableEditor.tsx +++ b/public/app/features/dashboard-scene/settings/variables/editors/AdHocFiltersVariableEditor.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { useAsync } from 'react-use'; -import { DataSourceInstanceSettings } from '@grafana/data'; +import { DataSourceInstanceSettings, MetricFindValue } from '@grafana/data'; import { getDataSourceSrv } from '@grafana/runtime'; import { AdHocFiltersVariable } from '@grafana/scenes'; import { DataSourceRef } from '@grafana/schema'; @@ -15,7 +15,7 @@ interface AdHocFiltersVariableEditorProps { export function AdHocFiltersVariableEditor(props: AdHocFiltersVariableEditorProps) { const { variable } = props; - const datasourceRef = variable.useState().datasource ?? undefined; + const { datasource: datasourceRef, defaultKeys } = variable.useState(); const { value: datasourceSettings } = useAsync(async () => { return await getDataSourceSrv().get(datasourceRef); @@ -36,5 +36,19 @@ export function AdHocFiltersVariableEditor(props: AdHocFiltersVariableEditorProp }); }; - return ; + const onDefaultKeysChange = (defaultKeys?: MetricFindValue[]) => { + variable.setState({ + defaultKeys, + }); + }; + + return ( + + ); } diff --git a/yarn.lock b/yarn.lock index b696f82af6..892d379d04 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4037,9 +4037,9 @@ __metadata: languageName: unknown linkType: soft -"@grafana/scenes@npm:^3.11.0": - version: 3.11.0 - resolution: "@grafana/scenes@npm:3.11.0" +"@grafana/scenes@npm:3.13.3": + version: 3.13.3 + resolution: "@grafana/scenes@npm:3.13.3" dependencies: "@grafana/e2e-selectors": "npm:10.3.3" react-grid-layout: "npm:1.3.4" @@ -4053,7 +4053,7 @@ __metadata: "@grafana/ui": ^10.0.3 react: ^18.0.0 react-dom: ^18.0.0 - checksum: 10/47629dd3f5129b8f803d54c512d10f921edf9b138b878fbebe664e2537d6813c72ea5119e28216daf7e426ef764400356db9b1532c601a3e029ae12baceb248d + checksum: 10/5b7f2e2714dcdbc3ad58352ec0cc7f513f7a240dc11a3e309733a662760a598e9333ec377f2899b6b668e018c2e885e8a044af7d42d1be4487d692cda3b9359a languageName: node linkType: hard @@ -18336,7 +18336,7 @@ __metadata: "@grafana/plugin-e2e": "npm:^0.21.0" "@grafana/prometheus": "workspace:*" "@grafana/runtime": "workspace:*" - "@grafana/scenes": "npm:^3.11.0" + "@grafana/scenes": "npm:3.13.3" "@grafana/schema": "workspace:*" "@grafana/sql": "workspace:*" "@grafana/tsconfig": "npm:^1.3.0-rc1" From 63e8753aa0ebfc1b3d700dbf560147887c16feca Mon Sep 17 00:00:00 2001 From: Darren Janeczek <38694490+darrenjaneczek@users.noreply.github.com> Date: Mon, 18 Mar 2024 12:16:38 -0400 Subject: [PATCH 0763/1406] datatrails: integrate dashboard panels with metrics explore (#84521) * feat: integrate dashboard panels with metrics explore - add dashboard panel menu items (in non-scenes dashboard) to open `metric{filters}` entries detected from queries to launch "metrics explorer" drawers for the selected `metric{filter}` * fix: remove OpenEmbeddedTrailEvent * fix: use modal manager dismiss capabilities instead --- .../scene/PanelMenuBehavior.tsx | 2 +- .../features/dashboard/utils/getPanelMenu.ts | 5 + .../trails/ActionTabs/MetricOverviewScene.tsx | 36 +++--- .../app/features/trails/DataTrailDrawer.tsx | 67 ---------- .../trails/Integrations/DataTrailEmbedded.tsx | 37 ++++++ .../trails/Integrations/SceneDrawer.tsx | 46 +++++++ .../Integrations/dashboardIntegration.ts | 115 ++++++++++++++++++ .../trails/Integrations/getQueryMetrics.ts | 31 +++++ .../app/features/trails/Integrations/utils.ts | 45 +++++++ public/app/features/trails/MetricScene.tsx | 13 +- .../features/trails/dashboardIntegration.ts | 38 ------ public/app/features/trails/shared.ts | 6 +- 12 files changed, 306 insertions(+), 135 deletions(-) delete mode 100644 public/app/features/trails/DataTrailDrawer.tsx create mode 100644 public/app/features/trails/Integrations/DataTrailEmbedded.tsx create mode 100644 public/app/features/trails/Integrations/SceneDrawer.tsx create mode 100644 public/app/features/trails/Integrations/dashboardIntegration.ts create mode 100644 public/app/features/trails/Integrations/getQueryMetrics.ts create mode 100644 public/app/features/trails/Integrations/utils.ts delete mode 100644 public/app/features/trails/dashboardIntegration.ts diff --git a/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx b/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx index 6ebf570080..55e23353af 100644 --- a/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx +++ b/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx @@ -17,7 +17,7 @@ import { shareDashboardType } from 'app/features/dashboard/components/ShareModal import { InspectTab } from 'app/features/inspector/types'; import { getScenePanelLinksSupplier } from 'app/features/panel/panellinks/linkSuppliers'; import { createExtensionSubMenu } from 'app/features/plugins/extensions/utils'; -import { addDataTrailPanelAction } from 'app/features/trails/dashboardIntegration'; +import { addDataTrailPanelAction } from 'app/features/trails/Integrations/dashboardIntegration'; import { ShowConfirmModalEvent } from 'app/types/events'; import { ShareModal } from '../sharing/ShareModal'; diff --git a/public/app/features/dashboard/utils/getPanelMenu.ts b/public/app/features/dashboard/utils/getPanelMenu.ts index ddd8a149eb..adeb45331d 100644 --- a/public/app/features/dashboard/utils/getPanelMenu.ts +++ b/public/app/features/dashboard/utils/getPanelMenu.ts @@ -31,6 +31,7 @@ import { DashboardInteractions } from 'app/features/dashboard-scene/utils/intera import { InspectTab } from 'app/features/inspector/types'; import { isPanelModelLibraryPanel } from 'app/features/library-panels/guard'; import { createExtensionSubMenu } from 'app/features/plugins/extensions/utils'; +import { addDataTrailPanelAction } from 'app/features/trails/Integrations/dashboardIntegration'; import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard'; import { dispatch, store } from 'app/store/store'; @@ -168,6 +169,10 @@ export function getPanelMenu( }); } + if (config.featureToggles.datatrails) { + addDataTrailPanelAction(dashboard, panel, menu); + } + const inspectMenu: PanelMenuItem[] = []; // Only show these inspect actions for data plugins diff --git a/public/app/features/trails/ActionTabs/MetricOverviewScene.tsx b/public/app/features/trails/ActionTabs/MetricOverviewScene.tsx index 5f469eb9da..3c5eef35f1 100644 --- a/public/app/features/trails/ActionTabs/MetricOverviewScene.tsx +++ b/public/app/features/trails/ActionTabs/MetricOverviewScene.tsx @@ -16,7 +16,7 @@ import { getDatasourceSrv } from '../../plugins/datasource_srv'; import { ALL_VARIABLE_VALUE } from '../../variables/constants'; import { StatusWrapper } from '../StatusWrapper'; import { TRAILS_ROUTE, VAR_DATASOURCE_EXPR, VAR_GROUP_BY } from '../shared'; -import { getMetricSceneFor } from '../utils'; +import { getMetricSceneFor, getTrailFor } from '../utils'; import { getLabelOptions } from './utils'; @@ -106,20 +106,26 @@ export class MetricOverviewScene extends SceneObjectBase Labels {labelOptions.length === 0 && 'Unable to fetch labels.'} - {labelOptions.map((l) => ( - - {l.label!} - - ))} + {labelOptions.map((l) => + getTrailFor(model).state.embedded ? ( + // Do not render as TextLink when in embedded mode, as any direct URL + // manipulation will take the browser out out of the current page. +
{l.label}
+ ) : ( + + {l.label!} + + ) + )} diff --git a/public/app/features/trails/DataTrailDrawer.tsx b/public/app/features/trails/DataTrailDrawer.tsx deleted file mode 100644 index 94c7bc2f19..0000000000 --- a/public/app/features/trails/DataTrailDrawer.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import React from 'react'; - -import { getDataSourceSrv } from '@grafana/runtime'; -import { SceneComponentProps, SceneObjectBase, SceneObjectState, SceneTimeRangeLike } from '@grafana/scenes'; -import { DataSourceRef } from '@grafana/schema'; -import { Drawer } from '@grafana/ui'; -import { PromVisualQuery } from 'app/plugins/datasource/prometheus/querybuilder/types'; - -import { getDashboardSceneFor } from '../dashboard-scene/utils/utils'; - -import { DataTrail } from './DataTrail'; -import { getDataTrailsApp } from './DataTrailsApp'; -import { OpenEmbeddedTrailEvent } from './shared'; - -interface DataTrailDrawerState extends SceneObjectState { - timeRange: SceneTimeRangeLike; - query: PromVisualQuery; - dsRef: DataSourceRef; -} - -export class DataTrailDrawer extends SceneObjectBase { - static Component = DataTrailDrawerRenderer; - - public trail: DataTrail; - - constructor(state: DataTrailDrawerState) { - super(state); - - this.trail = buildDataTrailFromQuery(state); - this.trail.addActivationHandler(() => { - this.trail.subscribeToEvent(OpenEmbeddedTrailEvent, this.onOpenTrail); - }); - } - - onOpenTrail = () => { - getDataTrailsApp().goToUrlForTrail(this.trail.clone({ embedded: false })); - }; - - onClose = () => { - const dashboard = getDashboardSceneFor(this); - dashboard.closeModal(); - }; -} - -function DataTrailDrawerRenderer({ model }: SceneComponentProps) { - return ( - -
- -
-
- ); -} - -export function buildDataTrailFromQuery({ query, dsRef, timeRange }: DataTrailDrawerState) { - const filters = query.labels.map((label) => ({ key: label.label, value: label.value, operator: label.op })); - - const ds = getDataSourceSrv().getInstanceSettings(dsRef); - - return new DataTrail({ - $timeRange: timeRange, - metric: query.metric, - initialDS: ds?.uid, - initialFilters: filters, - embedded: true, - }); -} diff --git a/public/app/features/trails/Integrations/DataTrailEmbedded.tsx b/public/app/features/trails/Integrations/DataTrailEmbedded.tsx new file mode 100644 index 0000000000..67b4ae6312 --- /dev/null +++ b/public/app/features/trails/Integrations/DataTrailEmbedded.tsx @@ -0,0 +1,37 @@ +import React from 'react'; + +import { AdHocVariableFilter } from '@grafana/data'; +import { SceneComponentProps, SceneObjectBase, SceneObjectState, SceneTimeRangeLike } from '@grafana/scenes'; + +import { DataTrail } from '../DataTrail'; + +export interface DataTrailEmbeddedState extends SceneObjectState { + timeRange: SceneTimeRangeLike; + metric?: string; + filters?: AdHocVariableFilter[]; + dataSourceUid?: string; +} +export class DataTrailEmbedded extends SceneObjectBase { + static Component = DataTrailEmbeddedRenderer; + + public trail: DataTrail; + + constructor(state: DataTrailEmbeddedState) { + super(state); + this.trail = buildDataTrailFromState(state); + } +} + +function DataTrailEmbeddedRenderer({ model }: SceneComponentProps) { + return ; +} + +export function buildDataTrailFromState({ metric, filters, dataSourceUid, timeRange }: DataTrailEmbeddedState) { + return new DataTrail({ + $timeRange: timeRange, + metric, + initialDS: dataSourceUid, + initialFilters: filters, + embedded: true, + }); +} diff --git a/public/app/features/trails/Integrations/SceneDrawer.tsx b/public/app/features/trails/Integrations/SceneDrawer.tsx new file mode 100644 index 0000000000..b48fe048ef --- /dev/null +++ b/public/app/features/trails/Integrations/SceneDrawer.tsx @@ -0,0 +1,46 @@ +import React from 'react'; + +import { SceneComponentProps, SceneObjectBase, SceneObject, SceneObjectState } from '@grafana/scenes'; +import { Drawer } from '@grafana/ui'; +import appEvents from 'app/core/app_events'; +import { ShowModalReactEvent } from 'app/types/events'; + +export type SceneDrawerProps = { + scene: SceneObject; + title: string; + onDismiss: () => void; +}; + +export function SceneDrawer(props: SceneDrawerProps) { + const { scene, title, onDismiss } = props; + return ( + +
+ +
+
+ ); +} + +interface SceneDrawerAsSceneState extends SceneObjectState, SceneDrawerProps {} + +export class SceneDrawerAsScene extends SceneObjectBase { + constructor(state: SceneDrawerProps) { + super(state); + } + + static Component({ model }: SceneComponentProps) { + const state = model.useState(); + + return ; + } +} + +export function launchSceneDrawerInGlobalModal(props: Omit) { + const payload = { + component: SceneDrawer, + props, + }; + + appEvents.publish(new ShowModalReactEvent(payload)); +} diff --git a/public/app/features/trails/Integrations/dashboardIntegration.ts b/public/app/features/trails/Integrations/dashboardIntegration.ts new file mode 100644 index 0000000000..09a8227d29 --- /dev/null +++ b/public/app/features/trails/Integrations/dashboardIntegration.ts @@ -0,0 +1,115 @@ +import { isString } from 'lodash'; + +import { PanelMenuItem, PanelModel } from '@grafana/data'; +import { getDataSourceSrv } from '@grafana/runtime'; +import { SceneTimeRangeLike, VizPanel } from '@grafana/scenes'; +import { DataSourceRef } from '@grafana/schema'; + +import { DashboardModel } from '../../dashboard/state'; +import { DashboardScene } from '../../dashboard-scene/scene/DashboardScene'; +import { MetricScene } from '../MetricScene'; + +import { DataTrailEmbedded, DataTrailEmbeddedState } from './DataTrailEmbedded'; +import { SceneDrawerAsScene, launchSceneDrawerInGlobalModal } from './SceneDrawer'; +import { QueryMetric, getQueryMetrics } from './getQueryMetrics'; +import { createAdHocFilters, getQueryMetricLabel, getQueryRunner, getTimeRangeFromDashboard } from './utils'; + +export function addDataTrailPanelAction( + dashboard: DashboardScene | DashboardModel, + panel: VizPanel | PanelModel, + items: PanelMenuItem[] +) { + const queryRunner = getQueryRunner(panel); + if (!queryRunner) { + return; + } + + const ds = getDataSourceSrv().getInstanceSettings(queryRunner.state.datasource); + + if (ds?.meta.id !== 'prometheus') { + return; + } + + const queries = queryRunner.state.queries.map((q) => q.expr).filter(isString); + + const queryMetrics = getQueryMetrics(queries); + + const subMenu: PanelMenuItem[] = queryMetrics.map((item) => { + return { + text: getQueryMetricLabel(item), + onClick: createClickHandler(item, dashboard, ds), + }; + }); + + if (subMenu.length > 0) { + items.push({ + text: 'Explore metrics', + iconClassName: 'code-branch', + subMenu: getUnique(subMenu), + }); + } +} + +function getUnique(items: T[]) { + const uniqueMenuTexts = new Set(); + function isUnique({ text }: { text: string }) { + const before = uniqueMenuTexts.size; + uniqueMenuTexts.add(text); + const after = uniqueMenuTexts.size; + return after > before; + } + return items.filter(isUnique); +} + +function getEmbeddedTrailsState( + { metric, labelFilters, query }: QueryMetric, + timeRange: SceneTimeRangeLike, + dataSourceUid: string | undefined +) { + const state: DataTrailEmbeddedState = { + metric, + filters: createAdHocFilters(labelFilters), + dataSourceUid, + timeRange, + }; + + return state; +} + +function createCommonEmbeddedTrailStateProps( + item: QueryMetric, + dashboard: DashboardScene | DashboardModel, + ds: DataSourceRef +) { + const timeRange = getTimeRangeFromDashboard(dashboard); + const trailState = getEmbeddedTrailsState(item, timeRange, ds.uid); + const embeddedTrail: DataTrailEmbedded = new DataTrailEmbedded(trailState); + + embeddedTrail.trail.addActivationHandler(() => { + if (embeddedTrail.trail.state.topScene instanceof MetricScene) { + embeddedTrail.trail.state.topScene.setActionView('breakdown'); + } + }); + + const commonProps = { + scene: embeddedTrail, + title: 'Explore metrics', + }; + + return commonProps; +} + +function createClickHandler(item: QueryMetric, dashboard: DashboardScene | DashboardModel, ds: DataSourceRef) { + if (dashboard instanceof DashboardScene) { + return () => { + const commonProps = createCommonEmbeddedTrailStateProps(item, dashboard, ds); + const drawerScene = new SceneDrawerAsScene({ + ...commonProps, + onDismiss: () => dashboard.closeModal(), + }); + dashboard.showModal(drawerScene); + }; + } else { + return () => launchSceneDrawerInGlobalModal(createCommonEmbeddedTrailStateProps(item, dashboard, ds)); + } +} diff --git a/public/app/features/trails/Integrations/getQueryMetrics.ts b/public/app/features/trails/Integrations/getQueryMetrics.ts new file mode 100644 index 0000000000..5273756dc2 --- /dev/null +++ b/public/app/features/trails/Integrations/getQueryMetrics.ts @@ -0,0 +1,31 @@ +import { buildVisualQueryFromString } from '@grafana/prometheus/src/querybuilder/parsing'; +import { QueryBuilderLabelFilter } from '@grafana/prometheus/src/querybuilder/shared/types'; + +import { isEquals } from './utils'; + +/** An identified metric and its label for a query */ +export type QueryMetric = { + metric: string; + labelFilters: QueryBuilderLabelFilter[]; + query: string; +}; + +export function getQueryMetrics(queries: string[]) { + const queryMetrics: QueryMetric[] = []; + + queries.forEach((query) => { + const struct = buildVisualQueryFromString(query); + if (struct.errors.length > 0) { + return; + } + + const { metric, labels } = struct.query; + + queryMetrics.push({ metric, labelFilters: labels.filter(isEquals), query }); + struct.query.binaryQueries?.forEach(({ query: { metric, labels } }) => { + queryMetrics.push({ metric, labelFilters: labels.filter(isEquals), query }); + }); + }); + + return queryMetrics; +} diff --git a/public/app/features/trails/Integrations/utils.ts b/public/app/features/trails/Integrations/utils.ts new file mode 100644 index 0000000000..e6f518283d --- /dev/null +++ b/public/app/features/trails/Integrations/utils.ts @@ -0,0 +1,45 @@ +import { PanelModel } from '@grafana/data'; +import { QueryBuilderLabelFilter } from '@grafana/prometheus/src/querybuilder/shared/types'; +import { SceneQueryRunner, SceneTimeRange, VizPanel } from '@grafana/scenes'; +import { DashboardModel } from 'app/features/dashboard/state'; +import { DashboardScene } from 'app/features/dashboard-scene/scene/DashboardScene'; +import { getQueryRunnerFor } from 'app/features/dashboard-scene/utils/utils'; + +import { QueryMetric } from './getQueryMetrics'; + +// We only support label filters with the '=' operator +export function isEquals(labelFilter: QueryBuilderLabelFilter) { + return labelFilter.op === '='; +} + +export function getQueryRunner(panel: VizPanel | PanelModel) { + if (panel instanceof VizPanel) { + return getQueryRunnerFor(panel); + } + + return new SceneQueryRunner({ datasource: panel.datasource || undefined, queries: panel.targets || [] }); +} + +export function getTimeRangeFromDashboard(dashboard: DashboardScene | DashboardModel) { + if (dashboard instanceof DashboardScene) { + return dashboard.state.$timeRange!.clone(); + } + if (dashboard instanceof DashboardModel) { + return new SceneTimeRange({ ...dashboard.time }); + } + return new SceneTimeRange(); +} + +export function getQueryMetricLabel({ metric, labelFilters }: QueryMetric) { + // Don't show the filter unless there is more than one entry + if (labelFilters.length === 0) { + return metric; + } + + const filter = `{${labelFilters.map(({ label, op, value }) => `${label}${op}"${value}"`)}}`; + return `${metric}${filter}`; +} + +export function createAdHocFilters(labels: QueryBuilderLabelFilter[]) { + return labels?.map((label) => ({ key: label.label, value: label.value, operator: label.op })); +} diff --git a/public/app/features/trails/MetricScene.tsx b/public/app/features/trails/MetricScene.tsx index 1dcd2c01f2..e3b889fe51 100644 --- a/public/app/features/trails/MetricScene.tsx +++ b/public/app/features/trails/MetricScene.tsx @@ -12,7 +12,7 @@ import { SceneVariableSet, QueryVariable, } from '@grafana/scenes'; -import { ToolbarButton, Stack, Icon, TabsBar, Tab, useStyles2, Box } from '@grafana/ui'; +import { ToolbarButton, Box, Stack, Icon, TabsBar, Tab, useStyles2, LinkButton } from '@grafana/ui'; import { getExploreUrl } from '../../core/utils/explore'; @@ -29,12 +29,11 @@ import { ActionViewType, getVariablesWithMetricConstant, MakeOptional, - OpenEmbeddedTrailEvent, trailDS, VAR_GROUP_BY, VAR_METRIC_EXPR, } from './shared'; -import { getDataSource, getTrailFor } from './utils'; +import { getDataSource, getTrailFor, getUrlForTrail } from './utils'; export interface MetricSceneState extends SceneObjectState { body: MetricGraphScene; @@ -116,10 +115,6 @@ const actionViewsDefinitions: ActionViewDefinition[] = [ export interface MetricActionBarState extends SceneObjectState {} export class MetricActionBar extends SceneObjectBase { - public onOpenTrail = () => { - this.publishEvent(new OpenEmbeddedTrailEvent(), true); - }; - public getLinkToExplore = async () => { const metricScene = sceneGraph.getAncestor(this, MetricScene); const trail = getTrailFor(this); @@ -175,9 +170,9 @@ export class MetricActionBar extends SceneObjectBase { onClick={toggleBookmark} /> {trail.state.embedded && ( - + Open - + )}
diff --git a/public/app/features/trails/dashboardIntegration.ts b/public/app/features/trails/dashboardIntegration.ts deleted file mode 100644 index 98b43feeaf..0000000000 --- a/public/app/features/trails/dashboardIntegration.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { PanelMenuItem } from '@grafana/data'; -import { getDataSourceSrv } from '@grafana/runtime'; -import { VizPanel } from '@grafana/scenes'; -import { buildVisualQueryFromString } from 'app/plugins/datasource/prometheus/querybuilder/parsing'; - -import { DashboardScene } from '../dashboard-scene/scene/DashboardScene'; -import { getQueryRunnerFor } from '../dashboard-scene/utils/utils'; - -import { DataTrailDrawer } from './DataTrailDrawer'; - -export function addDataTrailPanelAction(dashboard: DashboardScene, vizPanel: VizPanel, items: PanelMenuItem[]) { - const queryRunner = getQueryRunnerFor(vizPanel); - if (!queryRunner) { - return; - } - - const ds = getDataSourceSrv().getInstanceSettings(queryRunner.state.datasource); - if (!ds || ds.meta.id !== 'prometheus' || queryRunner.state.queries.length > 1) { - return; - } - - const query = queryRunner.state.queries[0]; - const parsedResult = buildVisualQueryFromString(query.expr); - if (parsedResult.errors.length > 0) { - return; - } - - items.push({ - text: 'Data trail', - iconClassName: 'code-branch', - onClick: () => { - dashboard.showModal( - new DataTrailDrawer({ query: parsedResult.query, dsRef: ds, timeRange: dashboard.state.$timeRange!.clone() }) - ); - }, - shortcut: 'p s', - }); -} diff --git a/public/app/features/trails/shared.ts b/public/app/features/trails/shared.ts index f37f30430d..55960310c9 100644 --- a/public/app/features/trails/shared.ts +++ b/public/app/features/trails/shared.ts @@ -1,4 +1,4 @@ -import { BusEventBase, BusEventWithPayload } from '@grafana/data'; +import { BusEventWithPayload } from '@grafana/data'; import { ConstantVariable, SceneObject } from '@grafana/scenes'; import { VariableHide } from '@grafana/schema'; @@ -47,7 +47,3 @@ export function getVariablesWithMetricConstant(metric: string) { export class MetricSelectedEvent extends BusEventWithPayload { public static type = 'metric-selected-event'; } - -export class OpenEmbeddedTrailEvent extends BusEventBase { - public static type = 'open-embedded-trail-event'; -} From 818c94f067f2a266a3f7bea41ff476acabe91206 Mon Sep 17 00:00:00 2001 From: Kyle Brandt Date: Mon, 18 Mar 2024 12:16:53 -0400 Subject: [PATCH 0764/1406] Scopes: (Chore) Fix ScopeDashboard by adding spec (#84675) --- pkg/apis/scope/v0alpha1/types.go | 8 ++- .../scope/v0alpha1/zz_generated.deepcopy.go | 27 +++++++-- .../scope/v0alpha1/zz_generated.openapi.go | 59 ++++++++++++------- ...enerated.openapi_violation_exceptions.list | 6 +- 4 files changed, 70 insertions(+), 30 deletions(-) diff --git a/pkg/apis/scope/v0alpha1/types.go b/pkg/apis/scope/v0alpha1/types.go index 08441235e1..9ed2bb0742 100644 --- a/pkg/apis/scope/v0alpha1/types.go +++ b/pkg/apis/scope/v0alpha1/types.go @@ -39,8 +39,12 @@ type ScopeDashboard struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` - DashboardUID []string `json:"dashboardUid"` - ScopeUID string `json:"scopeUid"` + Spec ScopeDashboardSpec `json:"spec,omitempty"` +} + +type ScopeDashboardSpec struct { + DashboardUIDs []string `json:"dashboardUids"` + ScopeUID string `json:"scopeUid"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object diff --git a/pkg/apis/scope/v0alpha1/zz_generated.deepcopy.go b/pkg/apis/scope/v0alpha1/zz_generated.deepcopy.go index 2cab9759fd..77bc6b7498 100644 --- a/pkg/apis/scope/v0alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/scope/v0alpha1/zz_generated.deepcopy.go @@ -43,11 +43,7 @@ func (in *ScopeDashboard) DeepCopyInto(out *ScopeDashboard) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - if in.DashboardUID != nil { - in, out := &in.DashboardUID, &out.DashboardUID - *out = make([]string, len(*in)) - copy(*out, *in) - } + in.Spec.DeepCopyInto(&out.Spec) return } @@ -102,6 +98,27 @@ func (in *ScopeDashboardList) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ScopeDashboardSpec) DeepCopyInto(out *ScopeDashboardSpec) { + *out = *in + if in.DashboardUIDs != nil { + in, out := &in.DashboardUIDs, &out.DashboardUIDs + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ScopeDashboardSpec. +func (in *ScopeDashboardSpec) DeepCopy() *ScopeDashboardSpec { + if in == nil { + return nil + } + out := new(ScopeDashboardSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ScopeFilter) DeepCopyInto(out *ScopeFilter) { *out = *in diff --git a/pkg/apis/scope/v0alpha1/zz_generated.openapi.go b/pkg/apis/scope/v0alpha1/zz_generated.openapi.go index 8efa6a2492..65f7bfdb5c 100644 --- a/pkg/apis/scope/v0alpha1/zz_generated.openapi.go +++ b/pkg/apis/scope/v0alpha1/zz_generated.openapi.go @@ -19,6 +19,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/grafana/grafana/pkg/apis/scope/v0alpha1.Scope": schema_pkg_apis_scope_v0alpha1_Scope(ref), "github.com/grafana/grafana/pkg/apis/scope/v0alpha1.ScopeDashboard": schema_pkg_apis_scope_v0alpha1_ScopeDashboard(ref), "github.com/grafana/grafana/pkg/apis/scope/v0alpha1.ScopeDashboardList": schema_pkg_apis_scope_v0alpha1_ScopeDashboardList(ref), + "github.com/grafana/grafana/pkg/apis/scope/v0alpha1.ScopeDashboardSpec": schema_pkg_apis_scope_v0alpha1_ScopeDashboardSpec(ref), "github.com/grafana/grafana/pkg/apis/scope/v0alpha1.ScopeFilter": schema_pkg_apis_scope_v0alpha1_ScopeFilter(ref), "github.com/grafana/grafana/pkg/apis/scope/v0alpha1.ScopeList": schema_pkg_apis_scope_v0alpha1_ScopeList(ref), "github.com/grafana/grafana/pkg/apis/scope/v0alpha1.ScopeSpec": schema_pkg_apis_scope_v0alpha1_ScopeSpec(ref), @@ -91,33 +92,17 @@ func schema_pkg_apis_scope_v0alpha1_ScopeDashboard(ref common.ReferenceCallback) Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), }, }, - "dashboardUid": { - SchemaProps: spec.SchemaProps{ - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - }, - }, - }, - "scopeUid": { + "spec": { SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/pkg/apis/scope/v0alpha1.ScopeDashboardSpec"), }, }, }, - Required: []string{"dashboardUid", "scopeUid"}, }, }, Dependencies: []string{ - "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + "github.com/grafana/grafana/pkg/apis/scope/v0alpha1.ScopeDashboardSpec", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, } } @@ -168,6 +153,40 @@ func schema_pkg_apis_scope_v0alpha1_ScopeDashboardList(ref common.ReferenceCallb } } +func schema_pkg_apis_scope_v0alpha1_ScopeDashboardSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "dashboardUids": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "scopeUid": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"dashboardUids", "scopeUid"}, + }, + }, + } +} + func schema_pkg_apis_scope_v0alpha1_ScopeFilter(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ diff --git a/pkg/apis/scope/v0alpha1/zz_generated.openapi_violation_exceptions.list b/pkg/apis/scope/v0alpha1/zz_generated.openapi_violation_exceptions.list index 082fa3782e..55e0bf15fe 100644 --- a/pkg/apis/scope/v0alpha1/zz_generated.openapi_violation_exceptions.list +++ b/pkg/apis/scope/v0alpha1/zz_generated.openapi_violation_exceptions.list @@ -1,4 +1,4 @@ -API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/scope/v0alpha1,ScopeDashboard,DashboardUID +API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/scope/v0alpha1,ScopeDashboardSpec,DashboardUIDs API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/scope/v0alpha1,ScopeSpec,Filters -API rule violation: names_match,github.com/grafana/grafana/pkg/apis/scope/v0alpha1,ScopeDashboard,DashboardUID -API rule violation: names_match,github.com/grafana/grafana/pkg/apis/scope/v0alpha1,ScopeDashboard,ScopeUID +API rule violation: names_match,github.com/grafana/grafana/pkg/apis/scope/v0alpha1,ScopeDashboardSpec,DashboardUIDs +API rule violation: names_match,github.com/grafana/grafana/pkg/apis/scope/v0alpha1,ScopeDashboardSpec,ScopeUID From 2e6bb6416d8abf61c3099229953c95650c7ef4f2 Mon Sep 17 00:00:00 2001 From: Leon Sorokin Date: Mon, 18 Mar 2024 11:20:43 -0500 Subject: [PATCH 0765/1406] VizTooltips: Fix position during bottom or right edge initial hover (#84623) --- .../uPlot/plugins/TooltipPlugin2.tsx | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx b/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx index d1aaa71e69..0a54874a5d 100644 --- a/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx +++ b/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx @@ -413,8 +413,12 @@ export const TooltipPlugin2 = ({ _someSeriesIdx = seriesIdxs.some((v, i) => i > 0 && v != null); viaSync = u.cursor.event == null; + let prevIsHovering = _isHovering; updateHovering(); - scheduleRender(); + + if (_isHovering || _isHovering !== prevIsHovering) { + scheduleRender(); + } }); const scrollbarWidth = 16; @@ -520,8 +524,32 @@ export const TooltipPlugin2 = ({ if (domRef.current != null) { size.observer.observe(domRef.current); + + // since the above observer is attached after container is in DOM, we need to manually update sizeRef + // and re-trigger a cursor move to do initial positioning math + const { width, height } = domRef.current.getBoundingClientRect(); + size.width = width; + size.height = height; + + const event = plot!.cursor.event; + + // if not viaSync, re-dispatch real event + if (event != null) { + plot!.over.dispatchEvent(event); + } else { + plot!.setCursor( + { + left: plot!.cursor.left!, + top: plot!.cursor.top!, + }, + true + ); + } + } else { + size.width = 0; + size.height = 0; } - }, [domRef.current]); + }, [isHovering]); if (plot && isHovering) { return createPortal( From 83464781bea51e5682d9e6d167d788e9389f95bd Mon Sep 17 00:00:00 2001 From: Tom Ratcliffe Date: Thu, 7 Mar 2024 14:33:25 +0000 Subject: [PATCH 0766/1406] Remove video from Alert Getting Started page --- .../alerting/unified/home/GettingStarted.tsx | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/public/app/features/alerting/unified/home/GettingStarted.tsx b/public/app/features/alerting/unified/home/GettingStarted.tsx index bc06edb7e9..a37594cce7 100644 --- a/public/app/features/alerting/unified/home/GettingStarted.tsx +++ b/public/app/features/alerting/unified/home/GettingStarted.tsx @@ -67,21 +67,6 @@ export default function GettingStarted({ showWelcomeHeader }: { showWelcomeHeade
- - -
); } From 2e2a5bca116b109c8d8ad1353e352ad5d7ffdef8 Mon Sep 17 00:00:00 2001 From: Tom Ratcliffe Date: Thu, 7 Mar 2024 15:10:29 +0000 Subject: [PATCH 0767/1406] Remove unused `showWelcomeHeader` prop --- public/app/features/alerting/unified/home/GettingStarted.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/public/app/features/alerting/unified/home/GettingStarted.tsx b/public/app/features/alerting/unified/home/GettingStarted.tsx index a37594cce7..fab54bb802 100644 --- a/public/app/features/alerting/unified/home/GettingStarted.tsx +++ b/public/app/features/alerting/unified/home/GettingStarted.tsx @@ -20,13 +20,12 @@ export const getOverviewScene = () => { }); }; -export default function GettingStarted({ showWelcomeHeader }: { showWelcomeHeader?: boolean }) { +export default function GettingStarted() { const theme = useTheme2(); const styles = useStyles2(getWelcomePageStyles); return (
- {showWelcomeHeader && }

How it works

From 30a791d77a3966cc1971cf78403c6f89a8242304 Mon Sep 17 00:00:00 2001 From: Tom Ratcliffe Date: Thu, 7 Mar 2024 15:11:09 +0000 Subject: [PATCH 0768/1406] Tidy up styling for Getting Started page and use @grafana/ui components --- .betterer.results | 7 +- .../alerting/unified/home/GettingStarted.tsx | 125 ++++++------------ 2 files changed, 43 insertions(+), 89 deletions(-) diff --git a/.betterer.results b/.betterer.results index d1176ccb0d..e5ebd5f9f7 100644 --- a/.betterer.results +++ b/.betterer.results @@ -2235,12 +2235,7 @@ exports[`better eslint`] = { [0, 0, 0, "Styles should be written using objects.", "6"], [0, 0, 0, "Styles should be written using objects.", "7"], [0, 0, 0, "Styles should be written using objects.", "8"], - [0, 0, 0, "Styles should be written using objects.", "9"], - [0, 0, 0, "Styles should be written using objects.", "10"], - [0, 0, 0, "Styles should be written using objects.", "11"], - [0, 0, 0, "Styles should be written using objects.", "12"], - [0, 0, 0, "Styles should be written using objects.", "13"], - [0, 0, 0, "Styles should be written using objects.", "14"] + [0, 0, 0, "Styles should be written using objects.", "9"] ], "public/app/features/alerting/unified/hooks/useAlertmanagerConfig.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] diff --git a/public/app/features/alerting/unified/home/GettingStarted.tsx b/public/app/features/alerting/unified/home/GettingStarted.tsx index fab54bb802..64050bf81f 100644 --- a/public/app/features/alerting/unified/home/GettingStarted.tsx +++ b/public/app/features/alerting/unified/home/GettingStarted.tsx @@ -4,7 +4,7 @@ import SVG from 'react-inlinesvg'; import { GrafanaTheme2 } from '@grafana/data'; import { EmbeddedScene, SceneFlexLayout, SceneFlexItem, SceneReactObject } from '@grafana/scenes'; -import { Icon, useStyles2, useTheme2, Stack } from '@grafana/ui'; +import { useStyles2, useTheme2, Stack, Text, TextLink } from '@grafana/ui'; export const getOverviewScene = () => { return new EmbeddedScene({ @@ -26,9 +26,9 @@ export default function GettingStarted() { return (
- -
-

How it works

+ + + How it works
  • Grafana alerting periodically queries data sources and evaluates the condition defined in the alert rule @@ -37,19 +37,24 @@ export default function GettingStarted() {
  • Firing instances are routed to notification policies based on matching labels
  • Notifications are sent out to the contact points specified in the notification policy
-
- +
+ + + +
+
- -

Get started

- + + + Get started
  • - Create an alert rule by adding queries and expressions from multiple data sources. + Create an alert rule by adding queries and expressions from multiple data + sources.
  • Add labels to your alert rules to connect them to notification policies @@ -61,9 +66,9 @@ export default function GettingStarted() { Configure notification policies to route your alert instances to contact points.
-
- -
+ + Read more in the docs +
@@ -74,49 +79,22 @@ const getWelcomePageStyles = (theme: GrafanaTheme2) => ({ grid: css` display: grid; grid-template-rows: min-content auto auto; - grid-template-columns: 1fr 1fr 1fr 1fr 1fr; + grid-template-columns: 1fr; gap: ${theme.spacing(2)}; width: 100%; + + ${theme.breakpoints.up('lg')} { + grid-template-columns: 3fr 2fr; + } `, ctaContainer: css` grid-column: 1 / span 5; `, - flowBlock: css` - grid-column: 1 / span 5; - - display: flex; - flex-wrap: wrap; - gap: ${theme.spacing(1)}; - - & > div { - flex: 2; - min-width: 350px; - } - & > svg { - flex: 3; - min-width: 500px; - } - `, - videoBlock: css` - grid-column: 3 / span 3; - - // Video required - position: relative; - padding: 56.25% 0 0 0; /* 16:9 */ - - iframe { - position: absolute; - top: ${theme.spacing(2)}; - left: ${theme.spacing(2)}; - width: calc(100% - ${theme.spacing(4)}); - height: calc(100% - ${theme.spacing(4)}); - border: none; + svgContainer: css` + & svg { + max-width: 900px; } `, - gettingStartedBlock: css` - grid-column: 1 / span 2; - justify-content: space-between; - `, list: css` margin: ${theme.spacing(0, 2)}; & > li { @@ -167,7 +145,7 @@ const getWelcomeHeaderStyles = (theme: GrafanaTheme2) => ({ paddingBottom: theme.spacing(2), }), ctaContainer: css` - padding: ${theme.spacing(4, 2)}; + padding: ${theme.spacing(2)}; display: flex; gap: ${theme.spacing(4)}; justify-content: space-between; @@ -200,12 +178,14 @@ function WelcomeCTABox({ title, description, href, hrefText }: WelcomeCTABoxProp return (
-

{title}

+ + {title} +
{description}
); @@ -216,15 +196,15 @@ const getWelcomeCTAButtonStyles = (theme: GrafanaTheme2) => ({ flex: 1; min-width: 240px; display: grid; - gap: ${theme.spacing(1)}; + row-gap: ${theme.spacing(1)}; grid-template-columns: min-content 1fr 1fr 1fr; grid-template-rows: min-content auto min-content; - `, - title: css` - margin-bottom: 0; - grid-column: 2 / span 3; - grid-row: 1; + & h2 { + margin-bottom: 0; + grid-column: 2 / span 3; + grid-row: 1; + } `, desc: css` @@ -237,10 +217,6 @@ const getWelcomeCTAButtonStyles = (theme: GrafanaTheme2) => ({ grid-row: 3; max-width: 240px; `, - - link: css` - color: ${theme.colors.text.link}; - `, }); function ContentBox({ children, className }: React.PropsWithChildren<{ className?: string }>) { @@ -256,20 +232,3 @@ const getContentBoxStyles = (theme: GrafanaTheme2) => ({ border-radius: ${theme.shape.radius.default}; `, }); - -function ArrowLink({ href, title }: { href: string; title: string }) { - const styles = useStyles2(getArrowLinkStyles); - - return ( - - {title} - - ); -} - -const getArrowLinkStyles = (theme: GrafanaTheme2) => ({ - link: css` - display: block; - color: ${theme.colors.text.link}; - `, -}); From fb2ba574c60ebe56e924db236bc2ec465370e571 Mon Sep 17 00:00:00 2001 From: Tom Ratcliffe Date: Thu, 7 Mar 2024 16:17:46 +0000 Subject: [PATCH 0769/1406] Convert getting started styles to object syntax --- .betterer.results | 12 -- .../alerting/unified/home/GettingStarted.tsx | 149 +++++++++--------- 2 files changed, 74 insertions(+), 87 deletions(-) diff --git a/.betterer.results b/.betterer.results index e5ebd5f9f7..b36e5fc685 100644 --- a/.betterer.results +++ b/.betterer.results @@ -2225,18 +2225,6 @@ exports[`better eslint`] = { [0, 0, 0, "Styles should be written using objects.", "3"], [0, 0, 0, "Styles should be written using objects.", "4"] ], - "public/app/features/alerting/unified/home/GettingStarted.tsx:5381": [ - [0, 0, 0, "Styles should be written using objects.", "0"], - [0, 0, 0, "Styles should be written using objects.", "1"], - [0, 0, 0, "Styles should be written using objects.", "2"], - [0, 0, 0, "Styles should be written using objects.", "3"], - [0, 0, 0, "Styles should be written using objects.", "4"], - [0, 0, 0, "Styles should be written using objects.", "5"], - [0, 0, 0, "Styles should be written using objects.", "6"], - [0, 0, 0, "Styles should be written using objects.", "7"], - [0, 0, 0, "Styles should be written using objects.", "8"], - [0, 0, 0, "Styles should be written using objects.", "9"] - ], "public/app/features/alerting/unified/hooks/useAlertmanagerConfig.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], diff --git a/public/app/features/alerting/unified/home/GettingStarted.tsx b/public/app/features/alerting/unified/home/GettingStarted.tsx index 64050bf81f..99b5cc544e 100644 --- a/public/app/features/alerting/unified/home/GettingStarted.tsx +++ b/public/app/features/alerting/unified/home/GettingStarted.tsx @@ -76,31 +76,31 @@ export default function GettingStarted() { } const getWelcomePageStyles = (theme: GrafanaTheme2) => ({ - grid: css` - display: grid; - grid-template-rows: min-content auto auto; - grid-template-columns: 1fr; - gap: ${theme.spacing(2)}; - width: 100%; - - ${theme.breakpoints.up('lg')} { - grid-template-columns: 3fr 2fr; - } - `, - ctaContainer: css` - grid-column: 1 / span 5; - `, - svgContainer: css` - & svg { - max-width: 900px; - } - `, - list: css` - margin: ${theme.spacing(0, 2)}; - & > li { - margin-bottom: ${theme.spacing(1)}; - } - `, + grid: css({ + display: 'grid', + gridTemplateRows: 'min-content auto auto', + gridTemplateColumns: '1fr', + gap: theme.spacing(2), + width: '100%', + + [theme.breakpoints.up('lg')]: { + gridTemplateColumns: '3fr 2fr', + }, + }), + ctaContainer: css({ + gridColumn: '1 / span 5', + }), + svgContainer: css({ + '& svg': { + maxWidth: '900px', + }, + }), + list: css({ + margin: theme.spacing(0, 2), + '& > li': { + marginBottom: theme.spacing(1), + }, + }), }); export function WelcomeHeader({ className }: { className?: string }) { @@ -144,26 +144,25 @@ const getWelcomeHeaderStyles = (theme: GrafanaTheme2) => ({ color: theme.colors.text.secondary, paddingBottom: theme.spacing(2), }), - ctaContainer: css` - padding: ${theme.spacing(2)}; - display: flex; - gap: ${theme.spacing(4)}; - justify-content: space-between; - flex-wrap: wrap; - - ${theme.breakpoints.down('lg')} { - flex-direction: column; - } - `, - - separator: css` - width: 1px; - background-color: ${theme.colors.border.medium}; - - ${theme.breakpoints.down('lg')} { - display: none; - } - `, + ctaContainer: css({ + padding: theme.spacing(2), + display: 'flex', + gap: theme.spacing(4), + justifyContent: 'space-between', + flexWrap: 'wrap', + + [theme.breakpoints.down('lg')]: { + flexDirection: 'column', + }, + }), + separator: css({ + width: '1px', + backgroundColor: theme.colors.border.medium, + + [theme.breakpoints.down('lg')]: { + display: 'none', + }, + }), }); interface WelcomeCTABoxProps { @@ -192,31 +191,31 @@ function WelcomeCTABox({ title, description, href, hrefText }: WelcomeCTABoxProp } const getWelcomeCTAButtonStyles = (theme: GrafanaTheme2) => ({ - container: css` - flex: 1; - min-width: 240px; - display: grid; - row-gap: ${theme.spacing(1)}; - grid-template-columns: min-content 1fr 1fr 1fr; - grid-template-rows: min-content auto min-content; - - & h2 { - margin-bottom: 0; - grid-column: 2 / span 3; - grid-row: 1; - } - `, - - desc: css` - grid-column: 2 / span 3; - grid-row: 2; - `, - - actionRow: css` - grid-column: 2 / span 3; - grid-row: 3; - max-width: 240px; - `, + container: css({ + flex: 1, + minWidth: '240px', + display: 'grid', + rowGap: theme.spacing(1), + gridTemplateColumns: 'min-content 1fr 1fr 1fr', + gridTemplateRows: 'min-content auto min-content', + + '& h2': { + marginBottom: 0, + gridColumn: '2 / span 3', + gridRow: 1, + }, + }), + + desc: css({ + gridColumn: '2 / span 3', + gridRow: 2, + }), + + actionRow: css({ + gridColumn: '2 / span 3', + gridRow: 3, + maxWidth: '240px', + }), }); function ContentBox({ children, className }: React.PropsWithChildren<{ className?: string }>) { @@ -226,9 +225,9 @@ function ContentBox({ children, className }: React.PropsWithChildren<{ className } const getContentBoxStyles = (theme: GrafanaTheme2) => ({ - box: css` - padding: ${theme.spacing(2)}; - background-color: ${theme.colors.background.secondary}; - border-radius: ${theme.shape.radius.default}; - `, + box: css({ + padding: theme.spacing(2), + backgroundColor: theme.colors.background.secondary, + borderRadius: theme.shape.radius.default, + }), }); From b3e9a6d0b3882746f79a010766f85ef4e4cea90b Mon Sep 17 00:00:00 2001 From: Tom Ratcliffe Date: Thu, 7 Mar 2024 16:37:57 +0000 Subject: [PATCH 0770/1406] Use Text component more consistently in GettingStarted --- .../app/features/alerting/unified/home/GettingStarted.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/public/app/features/alerting/unified/home/GettingStarted.tsx b/public/app/features/alerting/unified/home/GettingStarted.tsx index 99b5cc544e..da8010b6f7 100644 --- a/public/app/features/alerting/unified/home/GettingStarted.tsx +++ b/public/app/features/alerting/unified/home/GettingStarted.tsx @@ -57,13 +57,15 @@ export default function GettingStarted() { sources.
  • - Add labels to your alert rules to connect them to notification policies + Add labels to your alert rules{' '} + to connect them to notification policies
  • - Configure contact points to define where to send your notifications to. + Configure contact points to define where to send your notifications to.
  • - Configure notification policies to route your alert instances to contact points. + Configure notification policies to route your alert instances to contact + points.
  • From 8f50ccbb7cdc2f18109dd558b9bb4fda18dd872e Mon Sep 17 00:00:00 2001 From: Tom Ratcliffe Date: Mon, 18 Mar 2024 11:07:55 +0000 Subject: [PATCH 0771/1406] Fix display of Alert diagram in Safari --- public/app/features/alerting/unified/home/GettingStarted.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/public/app/features/alerting/unified/home/GettingStarted.tsx b/public/app/features/alerting/unified/home/GettingStarted.tsx index da8010b6f7..9745ec893b 100644 --- a/public/app/features/alerting/unified/home/GettingStarted.tsx +++ b/public/app/features/alerting/unified/home/GettingStarted.tsx @@ -95,6 +95,7 @@ const getWelcomePageStyles = (theme: GrafanaTheme2) => ({ svgContainer: css({ '& svg': { maxWidth: '900px', + flex: 1, }, }), list: css({ From 296f4219f81a118f505ada7c23864bc913bbad3f Mon Sep 17 00:00:00 2001 From: Tom Ratcliffe Date: Mon, 18 Mar 2024 11:08:18 +0000 Subject: [PATCH 0772/1406] Add missing `external` link for TextLink --- public/app/features/alerting/unified/home/GettingStarted.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/alerting/unified/home/GettingStarted.tsx b/public/app/features/alerting/unified/home/GettingStarted.tsx index 9745ec893b..6415801a05 100644 --- a/public/app/features/alerting/unified/home/GettingStarted.tsx +++ b/public/app/features/alerting/unified/home/GettingStarted.tsx @@ -68,7 +68,7 @@ export default function GettingStarted() { points. - + Read more in the docs From 494d1699805ab603ccd9ef2f6ba7d28caa975088 Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Mon, 18 Mar 2024 18:01:33 +0100 Subject: [PATCH 0773/1406] Elasticsearch: Fix legend for alerting, expressions and previously frontend queries (#84485) * Elasticsearch: Fix legend for alerting, expressions and previously frontend queries * Add comment * Update comment --- pkg/tsdb/elasticsearch/data_query.go | 23 ++-- pkg/tsdb/elasticsearch/data_query_test.go | 2 +- pkg/tsdb/elasticsearch/elasticsearch.go | 18 ++- pkg/tsdb/elasticsearch/querydata_test.go | 5 +- pkg/tsdb/elasticsearch/response_parser.go | 22 ++-- .../elasticsearch/response_parser_test.go | 116 ++++++++++++++---- 6 files changed, 137 insertions(+), 49 deletions(-) diff --git a/pkg/tsdb/elasticsearch/data_query.go b/pkg/tsdb/elasticsearch/data_query.go index f1a0f4d960..8ea4cab315 100644 --- a/pkg/tsdb/elasticsearch/data_query.go +++ b/pkg/tsdb/elasticsearch/data_query.go @@ -23,20 +23,27 @@ const ( ) type elasticsearchDataQuery struct { - client es.Client - dataQueries []backend.DataQuery - logger log.Logger - ctx context.Context - tracer tracing.Tracer + client es.Client + dataQueries []backend.DataQuery + logger log.Logger + ctx context.Context + tracer tracing.Tracer + keepLabelsInResponse bool } -var newElasticsearchDataQuery = func(ctx context.Context, client es.Client, dataQuery []backend.DataQuery, logger log.Logger, tracer tracing.Tracer) *elasticsearchDataQuery { +var newElasticsearchDataQuery = func(ctx context.Context, client es.Client, req *backend.QueryDataRequest, logger log.Logger, tracer tracing.Tracer) *elasticsearchDataQuery { + _, fromAlert := req.Headers[headerFromAlert] + fromExpression := req.GetHTTPHeader(headerFromExpression) != "" + return &elasticsearchDataQuery{ client: client, - dataQueries: dataQuery, + dataQueries: req.Queries, logger: logger, ctx: ctx, tracer: tracer, + // To maintain backward compatibility, it is necessary to keep labels in responses for alerting and expressions queries. + // Historically, these labels have been used in alerting rules and transformations. + keepLabelsInResponse: fromAlert || fromExpression, } } @@ -77,7 +84,7 @@ func (e *elasticsearchDataQuery) execute() (*backend.QueryDataResponse, error) { return errorsource.AddErrorToResponse(e.dataQueries[0].RefID, response, err), nil } - return parseResponse(e.ctx, res.Responses, queries, e.client.GetConfiguredFields(), e.logger, e.tracer) + return parseResponse(e.ctx, res.Responses, queries, e.client.GetConfiguredFields(), e.keepLabelsInResponse, e.logger, e.tracer) } func (e *elasticsearchDataQuery) processQuery(q *Query, ms *es.MultiSearchRequestBuilder, from, to int64) error { diff --git a/pkg/tsdb/elasticsearch/data_query_test.go b/pkg/tsdb/elasticsearch/data_query_test.go index bded5d6071..17381d29d9 100644 --- a/pkg/tsdb/elasticsearch/data_query_test.go +++ b/pkg/tsdb/elasticsearch/data_query_test.go @@ -1862,6 +1862,6 @@ func executeElasticsearchDataQuery(c es.Client, body string, from, to time.Time) }, }, } - query := newElasticsearchDataQuery(context.Background(), c, dataRequest.Queries, log.New("test.logger"), tracing.InitializeTracerForTest()) + query := newElasticsearchDataQuery(context.Background(), c, &dataRequest, log.New("test.logger"), tracing.InitializeTracerForTest()) return query.execute() } diff --git a/pkg/tsdb/elasticsearch/elasticsearch.go b/pkg/tsdb/elasticsearch/elasticsearch.go index cb42e187b9..a79a76df91 100644 --- a/pkg/tsdb/elasticsearch/elasticsearch.go +++ b/pkg/tsdb/elasticsearch/elasticsearch.go @@ -24,12 +24,18 @@ import ( "github.com/grafana/grafana/pkg/infra/httpclient" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" - ngalertmodels "github.com/grafana/grafana/pkg/services/ngalert/models" es "github.com/grafana/grafana/pkg/tsdb/elasticsearch/client" ) var eslog = log.New("tsdb.elasticsearch") +const ( + // headerFromExpression is used by data sources to identify expression queries + headerFromExpression = "X-Grafana-From-Expr" + // headerFromAlert is used by datasources to identify alert queries + headerFromAlert = "FromAlert" +) + type Service struct { httpClientProvider httpclient.Provider im instancemgmt.InstanceManager @@ -48,7 +54,7 @@ func ProvideService(httpClientProvider httpclient.Provider, tracer tracing.Trace func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { dsInfo, err := s.getDSInfo(ctx, req.PluginContext) - _, fromAlert := req.Headers[ngalertmodels.FromAlertHeaderName] + _, fromAlert := req.Headers[headerFromAlert] logger := s.logger.FromContext(ctx).New("fromAlert", fromAlert) if err != nil { @@ -56,12 +62,12 @@ func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest) return &backend.QueryDataResponse{}, err } - return queryData(ctx, req.Queries, dsInfo, logger, s.tracer) + return queryData(ctx, req, dsInfo, logger, s.tracer) } // separate function to allow testing the whole transformation and query flow -func queryData(ctx context.Context, queries []backend.DataQuery, dsInfo *es.DatasourceInfo, logger log.Logger, tracer tracing.Tracer) (*backend.QueryDataResponse, error) { - if len(queries) == 0 { +func queryData(ctx context.Context, req *backend.QueryDataRequest, dsInfo *es.DatasourceInfo, logger log.Logger, tracer tracing.Tracer) (*backend.QueryDataResponse, error) { + if len(req.Queries) == 0 { return &backend.QueryDataResponse{}, fmt.Errorf("query contains no queries") } @@ -69,7 +75,7 @@ func queryData(ctx context.Context, queries []backend.DataQuery, dsInfo *es.Data if err != nil { return &backend.QueryDataResponse{}, err } - query := newElasticsearchDataQuery(ctx, client, queries, logger, tracer) + query := newElasticsearchDataQuery(ctx, client, req, logger, tracer) return query.execute() } diff --git a/pkg/tsdb/elasticsearch/querydata_test.go b/pkg/tsdb/elasticsearch/querydata_test.go index 7a559b68fb..d3e7d1f2af 100644 --- a/pkg/tsdb/elasticsearch/querydata_test.go +++ b/pkg/tsdb/elasticsearch/querydata_test.go @@ -114,6 +114,9 @@ type queryDataTestResult struct { func queryDataTestWithResponseCode(queriesBytes []byte, responseStatusCode int, responseBytes []byte) (queryDataTestResult, error) { queries, err := newFlowTestQueries(queriesBytes) + req := backend.QueryDataRequest{ + Queries: queries, + } if err != nil { return queryDataTestResult{}, err } @@ -138,7 +141,7 @@ func queryDataTestWithResponseCode(queriesBytes []byte, responseStatusCode int, return nil }) - result, err := queryData(context.Background(), queries, dsInfo, log.New("test.logger"), tracing.InitializeTracerForTest()) + result, err := queryData(context.Background(), &req, dsInfo, log.New("test.logger"), tracing.InitializeTracerForTest()) if err != nil { return queryDataTestResult{}, err } diff --git a/pkg/tsdb/elasticsearch/response_parser.go b/pkg/tsdb/elasticsearch/response_parser.go index 3bb69e8a15..822a655fb3 100644 --- a/pkg/tsdb/elasticsearch/response_parser.go +++ b/pkg/tsdb/elasticsearch/response_parser.go @@ -47,7 +47,7 @@ const ( var searchWordsRegex = regexp.MustCompile(regexp.QuoteMeta(es.HighlightPreTagsString) + `(.*?)` + regexp.QuoteMeta(es.HighlightPostTagsString)) -func parseResponse(ctx context.Context, responses []*es.SearchResponse, targets []*Query, configuredFields es.ConfiguredFields, logger log.Logger, tracer tracing.Tracer) (*backend.QueryDataResponse, error) { +func parseResponse(ctx context.Context, responses []*es.SearchResponse, targets []*Query, configuredFields es.ConfiguredFields, keepLabelsInResponse bool, logger log.Logger, tracer tracing.Tracer) (*backend.QueryDataResponse, error) { result := backend.QueryDataResponse{ Responses: backend.Responses{}, } @@ -117,7 +117,7 @@ func parseResponse(ctx context.Context, responses []*es.SearchResponse, targets resSpan.End() return &backend.QueryDataResponse{}, err } - nameFields(queryRes, target) + nameFields(queryRes, target, keepLabelsInResponse) trimDatapoints(queryRes, target) result.Responses[target.RefID] = queryRes @@ -888,7 +888,7 @@ func getSortedLabelValues(labels data.Labels) []string { return values } -func nameFields(queryResult backend.DataResponse, target *Query) { +func nameFields(queryResult backend.DataResponse, target *Query, keepLabelsInResponse bool) { set := make(map[string]struct{}) frames := queryResult.Frames for _, v := range frames { @@ -907,10 +907,18 @@ func nameFields(queryResult backend.DataResponse, target *Query) { // another is "number" valueField := frame.Fields[1] fieldName := getFieldName(*valueField, target, metricTypeCount) - // We need to remove labels so they are not added to legend as duplicates - // ensures backward compatibility with "frontend" version of the plugin - valueField.Labels = nil - frame.Name = fieldName + // If we need to keep the labels in the response, to prevent duplication in names and to keep + // backward compatibility with alerting and expressions we use DisplayNameFromDS + if keepLabelsInResponse { + if valueField.Config == nil { + valueField.Config = &data.FieldConfig{} + } + valueField.Config.DisplayNameFromDS = fieldName + // If we don't need to keep labels (how frontend mode worked), we use frame.Name and remove labels + } else { + valueField.Labels = nil + frame.Name = fieldName + } } } } diff --git a/pkg/tsdb/elasticsearch/response_parser_test.go b/pkg/tsdb/elasticsearch/response_parser_test.go index 6f928655f9..2fc85276f7 100644 --- a/pkg/tsdb/elasticsearch/response_parser_test.go +++ b/pkg/tsdb/elasticsearch/response_parser_test.go @@ -330,7 +330,7 @@ func TestProcessLogsResponse(t *testing.T) { ] }` - result, err := parseTestResponse(targets, response) + result, err := parseTestResponse(targets, response, false) require.NoError(t, err) require.Len(t, result.Responses, 1) @@ -417,7 +417,7 @@ func TestProcessLogsResponse(t *testing.T) { ] }` - result, err := parseTestResponse(targets, response) + result, err := parseTestResponse(targets, response, false) require.NoError(t, err) require.Len(t, result.Responses, 1) @@ -525,7 +525,7 @@ func TestProcessRawDataResponse(t *testing.T) { ] }` - result, err := parseTestResponse(targets, response) + result, err := parseTestResponse(targets, response, false) require.NoError(t, err) require.Len(t, result.Responses, 1) @@ -814,7 +814,7 @@ func TestProcessRawDocumentResponse(t *testing.T) { ] }` - result, err := parseTestResponse(targets, response) + result, err := parseTestResponse(targets, response, false) require.NoError(t, err) require.Len(t, result.Responses, 1) @@ -995,7 +995,7 @@ func TestProcessBuckets(t *testing.T) { } ] }` - result, err := parseTestResponse(targets, response) + result, err := parseTestResponse(targets, response, false) require.NoError(t, err) require.Len(t, result.Responses, 1) @@ -1097,7 +1097,7 @@ func TestProcessBuckets(t *testing.T) { } ] }` - result, err := parseTestResponse(targets, response) + result, err := parseTestResponse(targets, response, false) require.NoError(t, err) require.Len(t, result.Responses, 1) @@ -1160,7 +1160,7 @@ func TestProcessBuckets(t *testing.T) { } ] }` - result, err := parseTestResponse(targets, response) + result, err := parseTestResponse(targets, response, false) require.NoError(t, err) require.Len(t, result.Responses, 1) @@ -1233,7 +1233,7 @@ func TestProcessBuckets(t *testing.T) { }] }` - result, err := parseTestResponse(targets, response) + result, err := parseTestResponse(targets, response, false) assert.Nil(t, err) assert.Len(t, result.Responses, 1) frames := result.Responses["A"].Frames @@ -1464,7 +1464,7 @@ func TestProcessBuckets(t *testing.T) { } }] }` - result, err := parseTestResponse(targets, response) + result, err := parseTestResponse(targets, response, false) assert.Nil(t, err) assert.Len(t, result.Responses, 1) @@ -1551,7 +1551,7 @@ func TestProcessBuckets(t *testing.T) { }] }` - result, err := parseTestResponse(targets, response) + result, err := parseTestResponse(targets, response, false) assert.Nil(t, err) assert.Len(t, result.Responses, 1) frames := result.Responses["A"].Frames @@ -1749,7 +1749,7 @@ func TestProcessBuckets(t *testing.T) { } ] }` - result, err := parseTestResponse(targets, response) + result, err := parseTestResponse(targets, response, false) require.NoError(t, err) queryRes := result.Responses["A"] @@ -1775,6 +1775,70 @@ func TestProcessBuckets(t *testing.T) { assert.Equal(t, frame.Name, "server2") }) + t.Run("Single group by query one metric with true keepLabelsInResponse", func(t *testing.T) { + targets := map[string]string{ + "A": `{ + "metrics": [{ "type": "count", "id": "1" }], + "bucketAggs": [ + { "type": "terms", "field": "host", "id": "2" }, + { "type": "date_histogram", "field": "@timestamp", "id": "3" } + ] + }`, + } + response := `{ + "responses": [ + { + "aggregations": { + "2": { + "buckets": [ + { + "3": { + "buckets": [{ "doc_count": 1, "key": 1000 }, { "doc_count": 3, "key": 2000 }] + }, + "doc_count": 4, + "key": "server1" + }, + { + "3": { + "buckets": [{ "doc_count": 2, "key": 1000 }, { "doc_count": 8, "key": 2000 }] + }, + "doc_count": 10, + "key": "server2" + } + ] + } + } + } + ] + }` + result, err := parseTestResponse(targets, response, true) + require.NoError(t, err) + + queryRes := result.Responses["A"] + require.NotNil(t, queryRes) + dataframes := queryRes.Frames + require.NoError(t, err) + require.Len(t, dataframes, 2) + + frame := dataframes[0] + require.Len(t, frame.Fields, 2) + require.Equal(t, frame.Fields[0].Name, data.TimeSeriesTimeFieldName) + require.Equal(t, frame.Fields[0].Len(), 2) + require.Equal(t, frame.Fields[1].Name, data.TimeSeriesValueFieldName) + require.Equal(t, frame.Fields[1].Len(), 2) + require.Equal(t, frame.Fields[1].Labels, data.Labels{"host": "server1"}) + assert.Equal(t, frame.Fields[1].Config.DisplayNameFromDS, "server1") + + frame = dataframes[1] + require.Len(t, frame.Fields, 2) + require.Equal(t, frame.Fields[0].Name, data.TimeSeriesTimeFieldName) + require.Equal(t, frame.Fields[0].Len(), 2) + require.Equal(t, frame.Fields[1].Name, data.TimeSeriesValueFieldName) + require.Equal(t, frame.Fields[1].Len(), 2) + require.Equal(t, frame.Fields[1].Labels, data.Labels{"host": "server2"}) + assert.Equal(t, frame.Fields[1].Config.DisplayNameFromDS, "server2") + }) + t.Run("Single group by query two metrics", func(t *testing.T) { targets := map[string]string{ "A": `{ @@ -1817,7 +1881,7 @@ func TestProcessBuckets(t *testing.T) { } ] }` - result, err := parseTestResponse(targets, response) + result, err := parseTestResponse(targets, response, false) require.NoError(t, err) require.Len(t, result.Responses, 1) @@ -1969,7 +2033,7 @@ func TestProcessBuckets(t *testing.T) { } ] }` - result, err := parseTestResponse(targets, response) + result, err := parseTestResponse(targets, response, false) require.NoError(t, err) require.Len(t, result.Responses, 1) @@ -2142,7 +2206,7 @@ func TestProcessBuckets(t *testing.T) { } ] }` - result, err := parseTestResponse(targets, response) + result, err := parseTestResponse(targets, response, false) require.NoError(t, err) require.Len(t, result.Responses, 1) @@ -2274,7 +2338,7 @@ func TestProcessBuckets(t *testing.T) { } ] }` - result, err := parseTestResponse(targets, response) + result, err := parseTestResponse(targets, response, false) require.NoError(t, err) require.Len(t, result.Responses, 1) @@ -2322,7 +2386,7 @@ func TestProcessBuckets(t *testing.T) { } ] }` - result, err := parseTestResponse(targets, response) + result, err := parseTestResponse(targets, response, false) require.NoError(t, err) require.Len(t, result.Responses, 1) @@ -2384,7 +2448,7 @@ func TestProcessBuckets(t *testing.T) { } ] }` - result, err := parseTestResponse(targets, response) + result, err := parseTestResponse(targets, response, false) require.NoError(t, err) require.Len(t, result.Responses, 1) @@ -2506,7 +2570,7 @@ func TestProcessBuckets(t *testing.T) { } ] }` - result, err := parseTestResponse(targets, response) + result, err := parseTestResponse(targets, response, false) require.NoError(t, err) require.Len(t, result.Responses, 1) @@ -2609,7 +2673,7 @@ func TestProcessBuckets(t *testing.T) { } ] }` - result, err := parseTestResponse(targets, response) + result, err := parseTestResponse(targets, response, false) require.NoError(t, err) require.Len(t, result.Responses, 1) @@ -2659,7 +2723,7 @@ func TestProcessBuckets(t *testing.T) { } ] }` - result, err := parseTestResponse(targets, response) + result, err := parseTestResponse(targets, response, false) require.NoError(t, err) require.Len(t, result.Responses, 1) @@ -2721,7 +2785,7 @@ func TestProcessBuckets(t *testing.T) { } ] }` - result, err := parseTestResponse(targets, response) + result, err := parseTestResponse(targets, response, false) require.NoError(t, err) require.Len(t, result.Responses, 1) @@ -2789,7 +2853,7 @@ func TestProcessBuckets(t *testing.T) { } ] }` - result, err := parseTestResponse(targets, response) + result, err := parseTestResponse(targets, response, false) require.NoError(t, err) require.Len(t, result.Responses, 1) @@ -2853,7 +2917,7 @@ func TestProcessBuckets(t *testing.T) { } ] }` - result, err := parseTestResponse(targets, response) + result, err := parseTestResponse(targets, response, false) require.NoError(t, err) require.Len(t, result.Responses, 1) @@ -2907,7 +2971,7 @@ func TestProcessBuckets(t *testing.T) { } ] }` - result, err := parseTestResponse(targets, response) + result, err := parseTestResponse(targets, response, false) require.NoError(t, err) require.Len(t, result.Responses, 1) @@ -3583,7 +3647,7 @@ func TestTrimEdges(t *testing.T) { requireFrameLength(t, frames[0], 1) } -func parseTestResponse(tsdbQueries map[string]string, responseBody string) (*backend.QueryDataResponse, error) { +func parseTestResponse(tsdbQueries map[string]string, responseBody string, keepLabelsInResponse bool) (*backend.QueryDataResponse, error) { from := time.Date(2018, 5, 15, 17, 50, 0, 0, time.UTC) to := time.Date(2018, 5, 15, 17, 55, 0, 0, time.UTC) configuredFields := es.ConfiguredFields{ @@ -3618,7 +3682,7 @@ func parseTestResponse(tsdbQueries map[string]string, responseBody string) (*bac return nil, err } - return parseResponse(context.Background(), response.Responses, queries, configuredFields, log.New("test.logger"), tracing.InitializeTracerForTest()) + return parseResponse(context.Background(), response.Responses, queries, configuredFields, keepLabelsInResponse, log.New("test.logger"), tracing.InitializeTracerForTest()) } func requireTimeValue(t *testing.T, expected int64, frame *data.Frame, index int) { From 3ea5c08c88ba13885746fd2289476cdd5a604951 Mon Sep 17 00:00:00 2001 From: Matthew Jacobson Date: Mon, 18 Mar 2024 13:04:57 -0400 Subject: [PATCH 0774/1406] Alerting: External AM fix parsing basic auth with escape characters (#84681) --- pkg/services/ngalert/sender/router.go | 17 ++++++++--------- pkg/services/ngalert/sender/router_test.go | 12 ++++++++++++ 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/pkg/services/ngalert/sender/router.go b/pkg/services/ngalert/sender/router.go index 6cac839f3c..077901d961 100644 --- a/pkg/services/ngalert/sender/router.go +++ b/pkg/services/ngalert/sender/router.go @@ -275,17 +275,16 @@ func (d *AlertsRouter) buildExternalURL(ds *datasources.DataSource) (string, err } } - // if basic auth is enabled we need to build the url with basic auth baked in - if !ds.BasicAuth { - return parsed.String(), nil + // If basic auth is enabled we need to build the url with basic auth baked in. + if ds.BasicAuth { + password := d.secretService.GetDecryptedValue(context.Background(), ds.SecureJsonData, "basicAuthPassword", "") + if password == "" { + return "", fmt.Errorf("basic auth enabled but no password set") + } + parsed.User = url.UserPassword(ds.BasicAuthUser, password) } - password := d.secretService.GetDecryptedValue(context.Background(), ds.SecureJsonData, "basicAuthPassword", "") - if password == "" { - return "", fmt.Errorf("basic auth enabled but no password set") - } - return fmt.Sprintf("%s://%s:%s@%s%s%s", parsed.Scheme, ds.BasicAuthUser, - password, parsed.Host, parsed.Path, parsed.RawQuery), nil + return parsed.String(), nil } func (d *AlertsRouter) Send(ctx context.Context, key models.AlertRuleKey, alerts definitions.PostableAlerts) { diff --git a/pkg/services/ngalert/sender/router_test.go b/pkg/services/ngalert/sender/router_test.go index d3b88880f9..7fa5a9ae76 100644 --- a/pkg/services/ngalert/sender/router_test.go +++ b/pkg/services/ngalert/sender/router_test.go @@ -461,6 +461,18 @@ func TestBuildExternalURL(t *testing.T) { }, expectedURL: "https://johndoe:123@localhost:9000", }, + { + name: "datasource with auth that needs escaping", + ds: &datasources.DataSource{ + URL: "https://localhost:9000", + BasicAuth: true, + BasicAuthUser: "johndoe", + SecureJsonData: map[string][]byte{ + "basicAuthPassword": []byte("123#!"), + }, + }, + expectedURL: "https://johndoe:123%23%21@localhost:9000", + }, { name: "datasource with auth and path", ds: &datasources.DataSource{ From 2d1cd82a98e2e38376008529fe704e04cd67cf7a Mon Sep 17 00:00:00 2001 From: Charandas Date: Mon, 18 Mar 2024 10:25:30 -0700 Subject: [PATCH 0775/1406] K8s: standalone: use Grafana's logger to stream all logs (#84530) --- Co-authored-by: Marcus Efraimsson --- pkg/cmd/grafana/apiserver/apiserver.md | 9 ++++-- pkg/cmd/grafana/apiserver/cmd.go | 17 +++++++---- pkg/cmd/grafana/apiserver/server.go | 42 ++++++++++++++++++-------- pkg/infra/log/log.go | 31 +++++++++++++++++++ 4 files changed, 79 insertions(+), 20 deletions(-) diff --git a/pkg/cmd/grafana/apiserver/apiserver.md b/pkg/cmd/grafana/apiserver/apiserver.md index b1f0756da8..27dce737a7 100644 --- a/pkg/cmd/grafana/apiserver/apiserver.md +++ b/pkg/cmd/grafana/apiserver/apiserver.md @@ -12,14 +12,19 @@ aggregation path altogether and just run this example apiserver as a standalone ### Usage ```shell -go run ./pkg/cmd/grafana apiserver example.grafana.app \ +go run ./pkg/cmd/grafana apiserver \ + --runtime-config=example.grafana.app/v0alpha1=true \ + --grafana-apiserver-dev-mode \ + --verbosity 10 \ --secure-port 7443 ``` ### Verify that all works +In dev mode, the standalone server's loopback kubeconfig is written to `./data/grafana-apiserver/apiserver.kubeconfig`. + ```shell -export KUBECONFIG=./example-apiserver/kubeconfig +export KUBECONFIG=./data/grafana-apiserver/apiserver.kubeconfig kubectl api-resources NAME SHORTNAMES APIVERSION NAMESPACED KIND diff --git a/pkg/cmd/grafana/apiserver/cmd.go b/pkg/cmd/grafana/apiserver/cmd.go index a7a621adad..dd63ab8a3d 100644 --- a/pkg/cmd/grafana/apiserver/cmd.go +++ b/pkg/cmd/grafana/apiserver/cmd.go @@ -5,11 +5,10 @@ import ( "github.com/spf13/cobra" genericapiserver "k8s.io/apiserver/pkg/server" - "k8s.io/apiserver/pkg/server/options" "k8s.io/component-base/cli" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/server" - grafanaapiserver "github.com/grafana/grafana/pkg/services/apiserver" "github.com/grafana/grafana/pkg/services/apiserver/standalone" ) @@ -30,6 +29,14 @@ func newCommandStartExampleAPIServer(o *APIServerOptions, stopCh <-chan struct{} devAcknowledgementNotice, Example: "grafana apiserver --runtime-config=example.grafana.app/v0alpha1=true", RunE: func(c *cobra.Command, args []string) error { + if err := log.SetupConsoleLogger("debug"); err != nil { + return nil + } + + if err := o.Validate(); err != nil { + return err + } + runtime, err := standalone.ReadRuntimeConfig(runtimeConfig) if err != nil { return err @@ -67,11 +74,9 @@ func newCommandStartExampleAPIServer(o *APIServerOptions, stopCh <-chan struct{} factoryOptions.AddFlags(cmd.Flags()) } + o.ExtraOptions.AddFlags(cmd.Flags()) + // Register standard k8s flags with the command line - o.RecommendedOptions = options.NewRecommendedOptions( - defaultEtcdPathPrefix, - grafanaapiserver.Codecs.LegacyCodec(), // the codec is passed to etcd and not used - ) o.RecommendedOptions.AddFlags(cmd.Flags()) return cmd diff --git a/pkg/cmd/grafana/apiserver/server.go b/pkg/cmd/grafana/apiserver/server.go index 246123d029..28e5118115 100644 --- a/pkg/cmd/grafana/apiserver/server.go +++ b/pkg/cmd/grafana/apiserver/server.go @@ -15,6 +15,7 @@ import ( "github.com/grafana/grafana/pkg/apiserver/builder" grafanaAPIServer "github.com/grafana/grafana/pkg/services/apiserver" + grafanaAPIServerOptions "github.com/grafana/grafana/pkg/services/apiserver/options" "github.com/grafana/grafana/pkg/services/apiserver/standalone" "github.com/grafana/grafana/pkg/services/apiserver/utils" "github.com/grafana/grafana/pkg/setting" @@ -29,6 +30,7 @@ const ( type APIServerOptions struct { factory standalone.APIServerFactory builders []builder.APIGroupBuilder + ExtraOptions *grafanaAPIServerOptions.ExtraOptions RecommendedOptions *options.RecommendedOptions AlternateDNS []string @@ -40,6 +42,11 @@ func newAPIServerOptions(out, errOut io.Writer) *APIServerOptions { return &APIServerOptions{ StdOut: out, StdErr: errOut, + RecommendedOptions: options.NewRecommendedOptions( + defaultEtcdPathPrefix, + grafanaAPIServer.Codecs.LegacyCodec(), // the codec is passed to etcd and not used + ), + ExtraOptions: grafanaAPIServerOptions.NewExtraOptions(), } } @@ -148,6 +155,12 @@ func (o *APIServerOptions) Config() (*genericapiserver.RecommendedConfig, error) } } + if o.ExtraOptions != nil { + if err := o.ExtraOptions.ApplyTo(serverConfig); err != nil { + return nil, err + } + } + serverConfig.DisabledPostStartHooks = serverConfig.DisabledPostStartHooks.Insert("generic-apiserver-start-informers") serverConfig.DisabledPostStartHooks = serverConfig.DisabledPostStartHooks.Insert("priority-and-fairness-config-consumer") @@ -165,12 +178,15 @@ func (o *APIServerOptions) Config() (*genericapiserver.RecommendedConfig, error) } // Validate validates APIServerOptions -// NOTE: we don't call validate on the top level recommended options as it doesn't like skipping etcd-servers -// the function is left here for troubleshooting any other config issues -func (o *APIServerOptions) Validate(args []string) error { - errors := []error{} - errors = append(errors, o.RecommendedOptions.Validate()...) - errors = append(errors, o.factory.GetOptions().ValidateOptions()...) +func (o *APIServerOptions) Validate() error { + errors := make([]error, 0) + // NOTE: we don't call validate on the top level recommended options as it doesn't like skipping etcd-servers + // the function is left here for troubleshooting any other config issues + // errors = append(errors, o.RecommendedOptions.Validate()...) + if factoryOptions := o.factory.GetOptions(); factoryOptions != nil { + errors = append(errors, factoryOptions.ValidateOptions()...) + } + return utilerrors.NewAggregate(errors) } @@ -183,7 +199,7 @@ func (o *APIServerOptions) RunAPIServer(config *genericapiserver.RecommendedConf delegationTarget := genericapiserver.NewEmptyDelegate() completedConfig := config.Complete() - server, err := completedConfig.New("example-apiserver", delegationTarget) + server, err := completedConfig.New("standalone-apiserver", delegationTarget) if err != nil { return err } @@ -195,11 +211,13 @@ func (o *APIServerOptions) RunAPIServer(config *genericapiserver.RecommendedConf } // write the local config to disk - if err = clientcmd.WriteToFile( - utils.FormatKubeConfig(server.LoopbackClientConfig), - path.Join(dataPath, "apiserver.kubeconfig"), - ); err != nil { - return err + if o.ExtraOptions.DevMode { + if err = clientcmd.WriteToFile( + utils.FormatKubeConfig(server.LoopbackClientConfig), + path.Join(dataPath, "apiserver.kubeconfig"), + ); err != nil { + return err + } } return server.PrepareRun().Run(stopCh) diff --git a/pkg/infra/log/log.go b/pkg/infra/log/log.go index 53e80ca07c..f9c741e134 100644 --- a/pkg/infra/log/log.go +++ b/pkg/infra/log/log.go @@ -499,3 +499,34 @@ func ReadLoggingConfig(modes []string, logsPath string, cfg *ini.File) error { return nil } + +// SetupConsoleLogger setup Grafana console logger with provided level. +func SetupConsoleLogger(level string) error { + iniFile := ini.Empty() + sLog, err := iniFile.NewSection("log") + if err != nil { + return err + } + + _, err = sLog.NewKey("level", level) + if err != nil { + return err + } + + sLogConsole, err := iniFile.NewSection("log.console") + if err != nil { + return err + } + + _, err = sLogConsole.NewKey("format", "console") + if err != nil { + return err + } + + err = ReadLoggingConfig([]string{"console"}, "", iniFile) + if err != nil { + return err + } + + return nil +} From 0606a8e413d4253df7f5a6d0edaecc13c0dedd5a Mon Sep 17 00:00:00 2001 From: Isabella Siu Date: Mon, 18 Mar 2024 17:30:59 -0400 Subject: [PATCH 0776/1406] CloudWatch: Static labels should use label name (#84611) CloudWatch: static labels should use label name --- pkg/tsdb/cloudwatch/response_parser.go | 15 +++- pkg/tsdb/cloudwatch/response_parser_test.go | 76 +++++++++++++++++++++ 2 files changed, 89 insertions(+), 2 deletions(-) diff --git a/pkg/tsdb/cloudwatch/response_parser.go b/pkg/tsdb/cloudwatch/response_parser.go index 78b7154afa..05bb8dcd9e 100644 --- a/pkg/tsdb/cloudwatch/response_parser.go +++ b/pkg/tsdb/cloudwatch/response_parser.go @@ -2,6 +2,7 @@ package cloudwatch import ( "fmt" + "regexp" "sort" "strings" "time" @@ -12,6 +13,9 @@ import ( "github.com/grafana/grafana/pkg/tsdb/cloudwatch/models" ) +// matches a dynamic label +var dynamicLabel = regexp.MustCompile(`\$\{.+\}`) + func (e *cloudWatchExecutor) parseResponse(startTime time.Time, endTime time.Time, metricDataOutputs []*cloudwatch.GetMetricDataOutput, queries []*models.CloudWatchQuery) ([]*responseWrapper, error) { aggregatedResponse := aggregateResponse(metricDataOutputs) @@ -110,6 +114,8 @@ func getLabels(cloudwatchLabel string, query *models.CloudWatchQuery) data.Label func buildDataFrames(startTime time.Time, endTime time.Time, aggregatedResponse models.QueryRowResponse, query *models.CloudWatchQuery) (data.Frames, error) { frames := data.Frames{} + hasStaticLabel := query.Label != "" && !dynamicLabel.MatchString(query.Label) + for _, metric := range aggregatedResponse.Metrics { label := *metric.Label @@ -169,10 +175,15 @@ func buildDataFrames(startTime time.Time, endTime time.Time, aggregatedResponse timeField := data.NewField(data.TimeSeriesTimeFieldName, nil, timestamps) valueField := data.NewField(data.TimeSeriesValueFieldName, labels, points) - valueField.SetConfig(&data.FieldConfig{DisplayNameFromDS: label, Links: createDataLinks(deepLink)}) + name := label + // CloudWatch appends the dimensions to the returned label if the query label is not dynamic, so static labels need to be set + if hasStaticLabel { + name = query.Label + } + valueField.SetConfig(&data.FieldConfig{DisplayNameFromDS: name, Links: createDataLinks(deepLink)}) frame := data.Frame{ - Name: label, + Name: name, Fields: []*data.Field{ timeField, valueField, diff --git a/pkg/tsdb/cloudwatch/response_parser_test.go b/pkg/tsdb/cloudwatch/response_parser_test.go index 1701bcce63..5cdedf4296 100644 --- a/pkg/tsdb/cloudwatch/response_parser_test.go +++ b/pkg/tsdb/cloudwatch/response_parser_test.go @@ -376,6 +376,82 @@ func Test_buildDataFrames_uses_response_label_as_frame_name(t *testing.T) { assert.Equal(t, "some label", frames[0].Name) }) + t.Run("when non-static label set on query", func(t *testing.T) { + timestamp := time.Unix(0, 0) + response := &models.QueryRowResponse{ + Metrics: []*cloudwatch.MetricDataResult{ + { + Id: aws.String("lb3"), + Label: aws.String("some label"), + Timestamps: []*time.Time{ + aws.Time(timestamp), + }, + Values: []*float64{aws.Float64(23)}, + StatusCode: aws.String("Complete"), + }, + }, + } + + query := &models.CloudWatchQuery{ + RefId: "refId1", + Region: "us-east-1", + Namespace: "AWS/ApplicationELB", + MetricName: "TargetResponseTime", + Dimensions: map[string][]string{ + "LoadBalancer": {"lb1"}, + "InstanceType": {"micro"}, + "Resource": {"res"}, + }, + Statistic: "Average", + Period: 60, + MetricQueryType: models.MetricQueryTypeQuery, + MetricEditorMode: models.MetricEditorModeBuilder, + Label: "set ${AVG} label", + } + frames, err := buildDataFrames(startTime, endTime, *response, query) + require.NoError(t, err) + + assert.Equal(t, "some label", frames[0].Name) + }) + + t.Run("unless static label set on query", func(t *testing.T) { + timestamp := time.Unix(0, 0) + response := &models.QueryRowResponse{ + Metrics: []*cloudwatch.MetricDataResult{ + { + Id: aws.String("lb3"), + Label: aws.String("some label"), + Timestamps: []*time.Time{ + aws.Time(timestamp), + }, + Values: []*float64{aws.Float64(23)}, + StatusCode: aws.String("Complete"), + }, + }, + } + + query := &models.CloudWatchQuery{ + RefId: "refId1", + Region: "us-east-1", + Namespace: "AWS/ApplicationELB", + MetricName: "TargetResponseTime", + Dimensions: map[string][]string{ + "LoadBalancer": {"lb1"}, + "InstanceType": {"micro"}, + "Resource": {"res"}, + }, + Statistic: "Average", + Period: 60, + MetricQueryType: models.MetricQueryTypeQuery, + MetricEditorMode: models.MetricEditorModeBuilder, + Label: "actual", + } + frames, err := buildDataFrames(startTime, endTime, *response, query) + require.NoError(t, err) + + assert.Equal(t, "actual", frames[0].Name) + }) + t.Run("Parse cloudwatch response", func(t *testing.T) { timestamp := time.Unix(0, 0) response := &models.QueryRowResponse{ From d3571c399ac854e5ccbf8ec5cce78c0d00312efb Mon Sep 17 00:00:00 2001 From: Tim Levett Date: Mon, 18 Mar 2024 16:40:46 -0500 Subject: [PATCH 0777/1406] substring should be lowercase (#84682) --- .../transformations/matchers/valueMatchers/substringMatchers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grafana-data/src/transformations/matchers/valueMatchers/substringMatchers.ts b/packages/grafana-data/src/transformations/matchers/valueMatchers/substringMatchers.ts index 034e4067ee..07d5d8486a 100644 --- a/packages/grafana-data/src/transformations/matchers/valueMatchers/substringMatchers.ts +++ b/packages/grafana-data/src/transformations/matchers/valueMatchers/substringMatchers.ts @@ -6,7 +6,7 @@ import { BasicValueMatcherOptions } from './types'; const isSubstringMatcher: ValueMatcherInfo = { id: ValueMatcherID.substring, - name: 'Contains Substring', + name: 'Contains substring', description: 'Match where value for given field is a substring to options value.', get: (options) => { return (valueIndex: number, field: Field) => { From 58170d4141c3aac811d07284d2d94d837a6d0c66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agn=C3=A8s=20Toulet?= <35176601+AgnesToulet@users.noreply.github.com> Date: Tue, 19 Mar 2024 08:48:46 +0100 Subject: [PATCH 0778/1406] Rendering: Add PDFRendering capability (#84438) * Rendering: Add PDFRendering capability * fix tests --- pkg/services/rendering/capabilities.go | 17 ++++++++++++++++- pkg/services/rendering/http_mode.go | 6 ------ pkg/services/rendering/interface.go | 1 + pkg/services/rendering/mock.go | 14 ++++++++++++++ pkg/services/rendering/plugin_mode.go | 5 ----- pkg/services/rendering/rendering.go | 18 ++++++++++++++++-- 6 files changed, 47 insertions(+), 14 deletions(-) diff --git a/pkg/services/rendering/capabilities.go b/pkg/services/rendering/capabilities.go index f4eaa4eddc..855422aece 100644 --- a/pkg/services/rendering/capabilities.go +++ b/pkg/services/rendering/capabilities.go @@ -3,6 +3,7 @@ package rendering import ( "context" "errors" + "fmt" "github.com/Masterminds/semver" ) @@ -17,7 +18,8 @@ type CapabilityName string const ( ScalingDownImages CapabilityName = "ScalingDownImages" FullHeightImages CapabilityName = "FullHeightImages" - SvgSanitization CapabilityName = "SvgSanitization" + SVGSanitization CapabilityName = "SvgSanitization" + PDFRendering CapabilityName = "PdfRendering" ) var ErrUnknownCapability = errors.New("unknown capability") @@ -55,3 +57,16 @@ func (rs *RenderingService) HasCapability(ctx context.Context, capability Capabi return CapabilitySupportRequestResult{IsSupported: compiledSemverConstraint.Check(compiledImageRendererVersion), SemverConstraint: semverConstraint}, nil } + +func (rs *RenderingService) IsCapabilitySupported(ctx context.Context, capabilityName CapabilityName) error { + capability, err := rs.HasCapability(ctx, capabilityName) + if err != nil { + return err + } + + if !capability.IsSupported { + return fmt.Errorf("%s unsupported, requires image renderer version: %s", capabilityName, capability.SemverConstraint) + } + + return nil +} diff --git a/pkg/services/rendering/http_mode.go b/pkg/services/rendering/http_mode.go index 01d318d5a9..7140ef17aa 100644 --- a/pkg/services/rendering/http_mode.go +++ b/pkg/services/rendering/http_mode.go @@ -14,8 +14,6 @@ import ( "os" "strconv" "time" - - "github.com/grafana/grafana/pkg/services/featuremgmt" ) var netTransport = &http.Transport{ @@ -40,10 +38,6 @@ var ( func (rs *RenderingService) renderViaHTTP(ctx context.Context, renderType RenderType, renderKey string, opts Opts) (*RenderResult, error) { if renderType == RenderPDF { - if !rs.features.IsEnabled(ctx, featuremgmt.FlagNewPDFRendering) { - return nil, fmt.Errorf("feature 'newPDFRendering' disabled") - } - opts.Encoding = "pdf" } diff --git a/pkg/services/rendering/interface.go b/pkg/services/rendering/interface.go index 076064b710..3fa172487a 100644 --- a/pkg/services/rendering/interface.go +++ b/pkg/services/rendering/interface.go @@ -127,6 +127,7 @@ type Service interface { RenderErrorImage(theme models.Theme, error error) (*RenderResult, error) GetRenderUser(ctx context.Context, key string) (*RenderUser, bool) HasCapability(ctx context.Context, capability CapabilityName) (CapabilitySupportRequestResult, error) + IsCapabilitySupported(ctx context.Context, capability CapabilityName) error CreateRenderingSession(ctx context.Context, authOpts AuthOpts, sessionOpts SessionOpts) (Session, error) SanitizeSVG(ctx context.Context, req *SanitizeSVGRequest) (*SanitizeSVGResponse, error) } diff --git a/pkg/services/rendering/mock.go b/pkg/services/rendering/mock.go index d628222042..4dfe3d71be 100644 --- a/pkg/services/rendering/mock.go +++ b/pkg/services/rendering/mock.go @@ -80,6 +80,20 @@ func (mr *MockServiceMockRecorder) HasCapability(ctx, capability interface{}) *g return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HasCapability", reflect.TypeOf((*MockService)(nil).HasCapability), ctx, capability) } +// IsCapabilitySupported mocks base method. +func (m *MockService) IsCapabilitySupported(ctx context.Context, capability CapabilityName) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsCapabilitySupported", ctx, capability) + ret0, _ := ret[0].(error) + return ret0 +} + +// IsCapabilitySupported indicates an expected call of IsCapabilitySupported. +func (mr *MockServiceMockRecorder) IsCapabilitySupported(ctx, capability interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsCapabilitySupported", reflect.TypeOf((*MockService)(nil).IsCapabilitySupported), ctx, capability) +} + // IsAvailable mocks base method. func (m *MockService) IsAvailable(ctx context.Context) bool { m.ctrl.T.Helper() diff --git a/pkg/services/rendering/plugin_mode.go b/pkg/services/rendering/plugin_mode.go index 5e8772c9c2..3b448551c3 100644 --- a/pkg/services/rendering/plugin_mode.go +++ b/pkg/services/rendering/plugin_mode.go @@ -6,15 +6,10 @@ import ( "fmt" "github.com/grafana/grafana/pkg/plugins/backendplugin/pluginextensionv2" - "github.com/grafana/grafana/pkg/services/featuremgmt" ) func (rs *RenderingService) renderViaPlugin(ctx context.Context, renderType RenderType, renderKey string, opts Opts) (*RenderResult, error) { if renderType == RenderPDF { - if !rs.features.IsEnabled(ctx, featuremgmt.FlagNewPDFRendering) { - return nil, fmt.Errorf("feature 'newPDFRendering' disabled") - } - opts.Encoding = "pdf" } diff --git a/pkg/services/rendering/rendering.go b/pkg/services/rendering/rendering.go index 75a6e5253f..5918b013fe 100644 --- a/pkg/services/rendering/rendering.go +++ b/pkg/services/rendering/rendering.go @@ -125,9 +125,13 @@ func ProvideService(cfg *setting.Cfg, features *featuremgmt.FeatureManager, remo semverConstraint: ">= 3.4.0", }, { - name: SvgSanitization, + name: SVGSanitization, semverConstraint: ">= 3.5.0", }, + { + name: PDFRendering, + semverConstraint: ">= 3.10.0", + }, }, Cfg: cfg, features: features, @@ -292,6 +296,16 @@ func (rs *RenderingService) render(ctx context.Context, renderType RenderType, o return rs.renderUnavailableImage(), nil } + if renderType == RenderPDF || opts.Encoding == "pdf" { + if !rs.features.IsEnabled(ctx, featuremgmt.FlagNewPDFRendering) { + return nil, fmt.Errorf("feature 'newPDFRendering' disabled") + } + + if err := rs.IsCapabilitySupported(ctx, PDFRendering); err != nil { + return nil, err + } + } + rs.log.Info("Rendering", "path", opts.Path) if math.IsInf(opts.DeviceScaleFactor, 0) || math.IsNaN(opts.DeviceScaleFactor) || opts.DeviceScaleFactor == 0 { opts.DeviceScaleFactor = 1 @@ -327,7 +341,7 @@ func (rs *RenderingService) RenderCSV(ctx context.Context, opts CSVOpts, session } func (rs *RenderingService) SanitizeSVG(ctx context.Context, req *SanitizeSVGRequest) (*SanitizeSVGResponse, error) { - capability, err := rs.HasCapability(ctx, SvgSanitization) + capability, err := rs.HasCapability(ctx, SVGSanitization) if err != nil { return nil, err } From e978d0810e6c7bd83e549056f70cbe97ade52159 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 19 Mar 2024 09:02:52 +0100 Subject: [PATCH 0779/1406] DS apiserver: Minor fixes (#84691) --- pkg/registry/apis/datasource/sub_query.go | 1 + pkg/registry/apis/datasource/sub_resource.go | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/pkg/registry/apis/datasource/sub_query.go b/pkg/registry/apis/datasource/sub_query.go index 1fdd151014..cc6d0edc22 100644 --- a/pkg/registry/apis/datasource/sub_query.go +++ b/pkg/registry/apis/datasource/sub_query.go @@ -69,6 +69,7 @@ func (r *subQueryREST) Connect(ctx context.Context, name string, opts runtime.Ob } if dsRef != nil && dsRef.UID != name { responder.Error(fmt.Errorf("expected query body datasource and request to match")) + return } ctx = backend.WithGrafanaConfig(ctx, pluginCtx.GrafanaConfig) diff --git a/pkg/registry/apis/datasource/sub_resource.go b/pkg/registry/apis/datasource/sub_resource.go index f8f56baf92..a9b1e259a7 100644 --- a/pkg/registry/apis/datasource/sub_resource.go +++ b/pkg/registry/apis/datasource/sub_resource.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "net/http" + "net/url" "strings" "github.com/grafana/grafana-plugin-sdk-go/backend" @@ -66,12 +67,21 @@ func (r *subResourceREST) Connect(ctx context.Context, name string, opts runtime return } - path := req.URL.Path[idx+len("/resource"):] + clonedReq := req.Clone(req.Context()) + rawURL := req.URL.Path[idx+len("/resource"):] + + clonedReq.URL = &url.URL{ + Path: rawURL, + RawQuery: clonedReq.URL.RawQuery, + } + err = r.builder.client.CallResource(ctx, &backend.CallResourceRequest{ PluginContext: pluginCtx, - Path: path, + Path: clonedReq.URL.Path, Method: req.Method, + URL: req.URL.String(), Body: body, + Headers: req.Header, }, httpresponsesender.New(w)) if err != nil { From abb8ba38859dac65cfc58201c38c6b8303c559fd Mon Sep 17 00:00:00 2001 From: Andres Martinez Gotor Date: Tue, 19 Mar 2024 09:09:07 +0100 Subject: [PATCH 0780/1406] Update comment in service.go (#84674) --- pkg/plugins/repo/service.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pkg/plugins/repo/service.go b/pkg/plugins/repo/service.go index b55d2d582f..704411e2bb 100644 --- a/pkg/plugins/repo/service.go +++ b/pkg/plugins/repo/service.go @@ -108,8 +108,7 @@ func (m *Manager) downloadURL(pluginID, version string) string { return fmt.Sprintf("%s/%s/versions/%s/download", m.baseURL, pluginID, version) } -// grafanaCompatiblePluginVersions will get version info from /api/plugins/repo/$pluginID based on -// the provided compatibility information (sent via HTTP headers) +// grafanaCompatiblePluginVersions will get version info from /api/plugins/$pluginID/versions func (m *Manager) grafanaCompatiblePluginVersions(pluginID string, compatOpts CompatOpts) ([]Version, error) { u, err := url.Parse(m.baseURL) if err != nil { From 7c17290da5cf063a1a573429c6b91484534faf40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agn=C3=A8s=20Toulet?= <35176601+AgnesToulet@users.noreply.github.com> Date: Tue, 19 Mar 2024 09:36:54 +0100 Subject: [PATCH 0781/1406] Chore: Fix Webpack config for Windows (#84709) --- packages/grafana-plugin-configs/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grafana-plugin-configs/utils.ts b/packages/grafana-plugin-configs/utils.ts index a02a2d8d16..a8b3dbd113 100644 --- a/packages/grafana-plugin-configs/utils.ts +++ b/packages/grafana-plugin-configs/utils.ts @@ -12,7 +12,7 @@ export function getPluginJson() { } export async function getEntries(): Promise> { - const pluginModules = await glob(path.resolve(process.cwd(), `module.{ts,tsx}`)); + const pluginModules = await glob(path.resolve(process.cwd(), `module.{ts,tsx}`), { absolute: true }); if (pluginModules.length > 0) { return { module: pluginModules[0], From c1c9ccb8e819eb5c8438cce7bdd5bf54d9fed30a Mon Sep 17 00:00:00 2001 From: Fabrizio <135109076+fabrizio-grafana@users.noreply.github.com> Date: Tue, 19 Mar 2024 10:59:53 +0100 Subject: [PATCH 0782/1406] Loki: Replace imports of `infra/httpclient` with SDK (#84337) --- pkg/tsdb/loki/healthcheck_test.go | 21 +++++++++++++++------ pkg/tsdb/loki/loki.go | 6 +++--- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/pkg/tsdb/loki/healthcheck_test.go b/pkg/tsdb/loki/healthcheck_test.go index a889fefae0..f4e8db583e 100644 --- a/pkg/tsdb/loki/healthcheck_test.go +++ b/pkg/tsdb/loki/healthcheck_test.go @@ -10,8 +10,8 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/datasource" - sdkHttpClient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" - "github.com/grafana/grafana/pkg/infra/httpclient" + "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -64,21 +64,30 @@ func (rt *healthCheckFailRoundTripper) RoundTrip(req *http.Request) (*http.Respo }, nil } -func (provider *healthCheckProvider[T]) New(opts ...sdkHttpClient.Options) (*http.Client, error) { +func (provider *healthCheckProvider[T]) New(opts ...httpclient.Options) (*http.Client, error) { client := &http.Client{} provider.RoundTripper = new(T) client.Transport = *provider.RoundTripper return client, nil } -func (provider *healthCheckProvider[T]) GetTransport(opts ...sdkHttpClient.Options) (http.RoundTripper, error) { +func (provider *healthCheckProvider[T]) GetTransport(opts ...httpclient.Options) (http.RoundTripper, error) { return *new(T), nil } -func getMockProvider[T http.RoundTripper]() *healthCheckProvider[T] { - return &healthCheckProvider[T]{ +// Return a mocked HTTP client provider. +// +// Example taken from `pkg/promlib/healthcheck_test.go` +func getMockProvider[T http.RoundTripper]() *httpclient.Provider { + p := &healthCheckProvider[T]{ RoundTripper: new(T), } + anotherFN := func(o httpclient.Options, next http.RoundTripper) http.RoundTripper { + return *p.RoundTripper + } + fn := httpclient.MiddlewareFunc(anotherFN) + mid := httpclient.NamedMiddlewareFunc("mock", fn) + return httpclient.NewProvider(httpclient.ProviderOptions{Middlewares: []httpclient.Middleware{mid}}) } func Test_healthcheck(t *testing.T) { diff --git a/pkg/tsdb/loki/loki.go b/pkg/tsdb/loki/loki.go index df2302d772..f62bee56d7 100644 --- a/pkg/tsdb/loki/loki.go +++ b/pkg/tsdb/loki/loki.go @@ -19,7 +19,7 @@ import ( "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/trace" - "github.com/grafana/grafana/pkg/infra/httpclient" + "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -42,7 +42,7 @@ var ( _ backend.CallResourceHandler = (*Service)(nil) ) -func ProvideService(httpClientProvider httpclient.Provider, features featuremgmt.FeatureToggles, tracer tracing.Tracer) *Service { +func ProvideService(httpClientProvider *httpclient.Provider, features featuremgmt.FeatureToggles, tracer tracing.Tracer) *Service { return &Service{ im: datasource.NewInstanceManager(newInstanceSettings(httpClientProvider)), features: features, @@ -88,7 +88,7 @@ func parseQueryModel(raw json.RawMessage) (*QueryJSONModel, error) { return model, err } -func newInstanceSettings(httpClientProvider httpclient.Provider) datasource.InstanceFactoryFunc { +func newInstanceSettings(httpClientProvider *httpclient.Provider) datasource.InstanceFactoryFunc { return func(ctx context.Context, settings backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { opts, err := settings.HTTPClientOptions(ctx) if err != nil { From 6febfdffd2c62a64e6fcf32f32905ad0f781cf1d Mon Sep 17 00:00:00 2001 From: Mihai Doarna Date: Tue, 19 Mar 2024 12:20:19 +0200 Subject: [PATCH 0783/1406] Auth: add the missing fields for all SSO providers (#83813) * add the missing fields for sso providers * remove fields array * add the validate_hd field for google * submit only fields defined on the provider * fix unit tests * add unit tests for the new fields/sections from the form * add hosted_domain field for the google provider * reorder fields in user mapping * remove authStyle field from gitlab and okta --------- Co-authored-by: Mihaly Gyongyosi --- .../auth-config/ProviderConfigForm.test.tsx | 95 +++++++++--- .../auth-config/ProviderConfigForm.tsx | 74 ++++------ public/app/features/auth-config/fields.tsx | 136 +++++++++++++++--- public/app/features/auth-config/types.ts | 2 + public/app/features/auth-config/utils/data.ts | 50 ++++--- 5 files changed, 250 insertions(+), 107 deletions(-) diff --git a/public/app/features/auth-config/ProviderConfigForm.test.tsx b/public/app/features/auth-config/ProviderConfigForm.test.tsx index efab7d4294..a59f15d821 100644 --- a/public/app/features/auth-config/ProviderConfigForm.test.tsx +++ b/public/app/features/auth-config/ProviderConfigForm.test.tsx @@ -77,24 +77,54 @@ describe('ProviderConfigForm', () => { jest.clearAllMocks(); }); - it('renders all fields correctly', async () => { + it('renders all general settings fields correctly', async () => { setup(); expect(screen.getByRole('textbox', { name: /Client ID/i })).toBeInTheDocument(); - expect(screen.getByRole('combobox', { name: /Team IDs/i })).toBeInTheDocument(); - expect(screen.getByRole('combobox', { name: /Allowed organizations/i })).toBeInTheDocument(); + expect(screen.getByRole('textbox', { name: /Client secret/i })).toBeInTheDocument(); + expect(screen.getByRole('combobox', { name: /Scopes/i })).toBeInTheDocument(); + expect(screen.getByRole('checkbox', { name: /Allow Sign Up/i })).toBeInTheDocument(); + expect(screen.getByRole('checkbox', { name: /Auto login/i })).toBeInTheDocument(); + expect(screen.getByRole('textbox', { name: /Sign out redirect URL/i })).toBeInTheDocument(); expect(screen.getByRole('button', { name: /Save/i })).toBeInTheDocument(); expect(screen.getByRole('link', { name: /Discard/i })).toBeInTheDocument(); }); + it('renders all user mapping fields correctly', async () => { + const { user } = setup(); + await user.click(screen.getByText('User mapping')); + expect(screen.getByRole('textbox', { name: /Role attribute path/i })).toBeInTheDocument(); + expect(screen.getByRole('checkbox', { name: /Role attribute strict mode/i })).toBeInTheDocument(); + expect(screen.getByRole('checkbox', { name: /Skip organization role sync/i })).toBeInTheDocument(); + }); + + it('renders all extra security fields correctly', async () => { + const { user } = setup(); + await user.click(screen.getByText('Extra security measures')); + expect(screen.getByRole('combobox', { name: /Allowed organizations/i })).toBeInTheDocument(); + expect(screen.getByRole('combobox', { name: /Allowed domains/i })).toBeInTheDocument(); + expect(screen.getByRole('combobox', { name: /Team Ids/i })).toBeInTheDocument(); + expect(screen.getByRole('checkbox', { name: /Use PKCE/i })).toBeInTheDocument(); + expect(screen.getByRole('checkbox', { name: /Use refresh token/i })).toBeInTheDocument(); + expect(screen.getByRole('checkbox', { name: /TLS skip verify/i })).toBeInTheDocument(); + }); + it('should save and enable on form submit', async () => { const { user } = setup(); + await user.type(screen.getByRole('textbox', { name: /Client ID/i }), 'test-client-id'); await user.type(screen.getByLabelText(/Client secret/i), 'test-client-secret'); - // Type a team name and press enter to select it - await user.type(screen.getByRole('combobox', { name: /Team IDs/i }), '12324{enter}'); - // Add two orgs - await user.type(screen.getByRole('combobox', { name: /Allowed organizations/i }), 'test-org1{enter}'); - await user.type(screen.getByRole('combobox', { name: /Allowed organizations/i }), 'test-org2{enter}'); + // Type a scope and press enter to select it + await user.type(screen.getByRole('combobox', { name: /Scopes/i }), 'user:email{enter}'); + await user.click(screen.getByRole('checkbox', { name: /Auto login/i })); + + await user.click(screen.getByText('User mapping')); + await user.type(screen.getByRole('textbox', { name: /Role attribute path/i }), 'new-attribute-path'); + await user.click(screen.getByRole('checkbox', { name: /Role attribute strict mode/i })); + + await user.click(screen.getByText('Extra security measures')); + await user.type(screen.getByRole('combobox', { name: /Allowed domains/i }), 'grafana.com{enter}'); + await user.click(screen.getByRole('checkbox', { name: /Use PKCE/i })); + await user.click(screen.getByRole('button', { name: /Save and enable/i })); await waitFor(() => { @@ -104,12 +134,27 @@ describe('ProviderConfigForm', () => { id: '300f9b7c-0488-40db-9763-a22ce8bf6b3e', provider: 'github', settings: { - name: 'GitHub', - allowedOrganizations: 'test-org1,test-org2', + allowAssignGrafanaAdmin: false, + allowSignUp: false, + allowedDomains: 'grafana.com', + allowedOrganizations: '', + autoLogin: true, clientId: 'test-client-id', clientSecret: 'test-client-secret', - teamIds: '12324', enabled: true, + name: 'GitHub', + roleAttributePath: 'new-attribute-path', + roleAttributeStrict: true, + scopes: 'user:email', + signoutRedirectUrl: '', + skipOrgRoleSync: false, + teamIds: '', + tlsClientCa: '', + tlsClientCert: '', + tlsClientKey: '', + tlsSkipVerifyInsecure: false, + usePkce: true, + useRefreshToken: false, }, }, { showErrorAlert: false } @@ -126,11 +171,9 @@ describe('ProviderConfigForm', () => { const { user } = setup(); await user.type(screen.getByRole('textbox', { name: /Client ID/i }), 'test-client-id'); await user.type(screen.getByLabelText(/Client secret/i), 'test-client-secret'); - // Type a team name and press enter to select it - await user.type(screen.getByRole('combobox', { name: /Team IDs/i }), '12324{enter}'); - // Add two orgs - await user.type(screen.getByRole('combobox', { name: /Allowed organizations/i }), 'test-org1{enter}'); - await user.type(screen.getByRole('combobox', { name: /Allowed organizations/i }), 'test-org2{enter}'); + // Type a scope and press enter to select it + await user.type(screen.getByRole('combobox', { name: /Scopes/i }), 'user:email{enter}'); + await user.click(screen.getByRole('checkbox', { name: /Auto login/i })); await user.click(screen.getByText('Save')); await waitFor(() => { @@ -140,12 +183,26 @@ describe('ProviderConfigForm', () => { id: '300f9b7c-0488-40db-9763-a22ce8bf6b3e', provider: 'github', settings: { - name: 'GitHub', - allowedOrganizations: 'test-org1,test-org2', + allowAssignGrafanaAdmin: false, + allowSignUp: false, + allowedDomains: '', + allowedOrganizations: '', + autoLogin: true, clientId: 'test-client-id', clientSecret: 'test-client-secret', - teamIds: '12324', enabled: false, + name: 'GitHub', + roleAttributePath: '', + roleAttributeStrict: false, + scopes: 'user:email', + signoutRedirectUrl: '', + skipOrgRoleSync: false, + teamIds: '', + tlsClientCa: '', + tlsClientCert: '', + tlsClientKey: '', + usePkce: false, + useRefreshToken: false, }, }, { showErrorAlert: false } diff --git a/public/app/features/auth-config/ProviderConfigForm.tsx b/public/app/features/auth-config/ProviderConfigForm.tsx index d8b2d3b641..9427390d62 100644 --- a/public/app/features/auth-config/ProviderConfigForm.tsx +++ b/public/app/features/auth-config/ProviderConfigForm.tsx @@ -21,7 +21,7 @@ import { FormPrompt } from '../../core/components/FormPrompt/FormPrompt'; import { Page } from '../../core/components/Page/Page'; import { FieldRenderer } from './FieldRenderer'; -import { fields, sectionFields } from './fields'; +import { sectionFields } from './fields'; import { SSOProvider, SSOProviderDTO } from './types'; import { dataToDTO, dtoToData } from './utils/data'; @@ -45,7 +45,6 @@ export const ProviderConfigForm = ({ config, provider, isLoading }: ProviderConf formState: { errors, dirtyFields, isSubmitted }, } = useForm({ defaultValues: dataToDTO(config), mode: 'onSubmit', reValidateMode: 'onChange' }); const [isSaving, setIsSaving] = useState(false); - const providerFields = fields[provider]; const [submitError, setSubmitError] = useState(false); const dataSubmitted = isSubmitted && !submitError; const sections = sectionFields[provider]; @@ -155,55 +154,34 @@ export const ProviderConfigForm = ({ config, provider, isLoading }: ProviderConf - {sections ? ( - - {sections - .filter((section) => !section.hidden) - .map((section, index) => { - return ( - - {section.fields - .filter((field) => (typeof field !== 'string' ? !field.hidden : true)) - .map((field) => { - return ( - - ); - })} - - ); - })} - - ) : ( - <> - {providerFields.map((field) => { + + {sections + .filter((section) => !section.hidden) + .map((section, index) => { return ( - + + {section.fields + .filter((field) => (typeof field !== 'string' ? !field.hidden : true)) + .map((field) => { + return ( + + ); + })} + ); })} - - )} +
    - + )} diff --git a/packages/grafana-ui/src/components/Menu/Menu.tsx b/packages/grafana-ui/src/components/Menu/Menu.tsx index 4d52692fe6..9dc03f1ee5 100644 --- a/packages/grafana-ui/src/components/Menu/Menu.tsx +++ b/packages/grafana-ui/src/components/Menu/Menu.tsx @@ -4,6 +4,7 @@ import React, { useImperativeHandle, useRef } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { useStyles2 } from '../../themes'; +import { Box } from '../Layout/Box/Box'; import { MenuDivider } from './MenuDivider'; import { MenuGroup } from './MenuGroup'; @@ -27,17 +28,22 @@ const MenuComp = React.forwardRef( const localRef = useRef(null); useImperativeHandle(forwardedRef, () => localRef.current!); - const [handleKeys] = useMenuFocus({ localRef, onOpen, onClose, onKeyDown }); + const [handleKeys] = useMenuFocus({ isMenuOpen: true, localRef, onOpen, onClose, onKeyDown }); return ( -
    {header && (
    (
    )} {children} -
    + ); } ); @@ -66,17 +72,10 @@ export const Menu = Object.assign(MenuComp, { const getStyles = (theme: GrafanaTheme2) => { return { header: css({ - padding: `${theme.spacing(0.5, 1, 1, 1)}`, + padding: theme.spacing(0.5, 1, 1, 1), }), headerBorder: css({ borderBottom: `1px solid ${theme.colors.border.weak}`, }), - wrapper: css({ - background: `${theme.colors.background.primary}`, - boxShadow: `${theme.shadows.z3}`, - display: `inline-block`, - borderRadius: `${theme.shape.radius.default}`, - padding: `${theme.spacing(0.5, 0)}`, - }), }; }; diff --git a/packages/grafana-ui/src/components/Menu/MenuItem.tsx b/packages/grafana-ui/src/components/Menu/MenuItem.tsx index 49f3d751f0..b694764306 100644 --- a/packages/grafana-ui/src/components/Menu/MenuItem.tsx +++ b/packages/grafana-ui/src/components/Menu/MenuItem.tsx @@ -79,7 +79,6 @@ export const MenuItem = React.memo( const styles = useStyles2(getStyles); const [isActive, setIsActive] = useState(active); const [isSubMenuOpen, setIsSubMenuOpen] = useState(false); - const [openedWithArrow, setOpenedWithArrow] = useState(false); const onMouseEnter = useCallback(() => { if (disabled) { return; @@ -128,7 +127,6 @@ export const MenuItem = React.memo( event.stopPropagation(); if (hasSubMenu) { setIsSubMenuOpen(true); - setOpenedWithArrow(true); setIsActive(true); } break; @@ -178,8 +176,6 @@ export const MenuItem = React.memo( @@ -219,7 +215,7 @@ const getStyles = (theme: GrafanaTheme2) => { width: '100%', position: 'relative', - '&:hover, &:focus, &:focus-visible': { + '&:hover, &:focus-visible': { background: theme.colors.action.hover, color: theme.colors.text.primary, textDecoration: 'none', diff --git a/packages/grafana-ui/src/components/Menu/SubMenu.test.tsx b/packages/grafana-ui/src/components/Menu/SubMenu.test.tsx index 67ea2daa52..b86d43e5e3 100644 --- a/packages/grafana-ui/src/components/Menu/SubMenu.test.tsx +++ b/packages/grafana-ui/src/components/Menu/SubMenu.test.tsx @@ -13,9 +13,7 @@ describe('SubMenu', () => { , ]; - render( - - ); + render(); expect(screen.getByTestId(selectors.components.Menu.SubMenu.icon)).toBeInTheDocument(); diff --git a/packages/grafana-ui/src/components/Menu/SubMenu.tsx b/packages/grafana-ui/src/components/Menu/SubMenu.tsx index 5016f72b77..50b4982887 100644 --- a/packages/grafana-ui/src/components/Menu/SubMenu.tsx +++ b/packages/grafana-ui/src/components/Menu/SubMenu.tsx @@ -17,10 +17,6 @@ export interface SubMenuProps { items?: Array>; /** Open */ isOpen: boolean; - /** Marks whether subMenu was opened with arrow */ - openedWithArrow: boolean; - /** Changes value of openedWithArrow */ - setOpenedWithArrow: (openedWithArrow: boolean) => void; /** Closes the subMenu */ close: () => void; /** Custom style */ @@ -28,46 +24,42 @@ export interface SubMenuProps { } /** @internal */ -export const SubMenu = React.memo( - ({ items, isOpen, openedWithArrow, setOpenedWithArrow, close, customStyle }: SubMenuProps) => { - const styles = useStyles2(getStyles); - const localRef = useRef(null); - const [handleKeys] = useMenuFocus({ - localRef, - isMenuOpen: isOpen, - openedWithArrow, - setOpenedWithArrow, - close, - }); +export const SubMenu = React.memo(({ items, isOpen, close, customStyle }: SubMenuProps) => { + const styles = useStyles2(getStyles); + const localRef = useRef(null); + const [handleKeys] = useMenuFocus({ + localRef, + isMenuOpen: isOpen, + close, + }); - const [pushLeft, setPushLeft] = useState(false); - useEffect(() => { - if (isOpen && localRef.current) { - setPushLeft(isElementOverflowing(localRef.current)); - } - }, [isOpen]); + const [pushLeft, setPushLeft] = useState(false); + useEffect(() => { + if (isOpen && localRef.current) { + setPushLeft(isElementOverflowing(localRef.current)); + } + }, [isOpen]); - return ( - <> -
    - -
    - {isOpen && ( -
    -
    - {items} -
    + return ( + <> +
    + +
    + {isOpen && ( +
    +
    + {items}
    - )} - - ); - } -); +
    + )} + + ); +}); SubMenu.displayName = 'SubMenu'; diff --git a/packages/grafana-ui/src/components/Menu/hooks.test.tsx b/packages/grafana-ui/src/components/Menu/hooks.test.tsx index 254f41c7a8..4ca9e69045 100644 --- a/packages/grafana-ui/src/components/Menu/hooks.test.tsx +++ b/packages/grafana-ui/src/components/Menu/hooks.test.tsx @@ -141,18 +141,15 @@ describe('useMenuFocus', () => { expect(onKeyDown).toHaveBeenCalledTimes(2); }); - it('focuses on first item when menu was opened with arrow', () => { + it('focuses on first item', () => { const ref = createRef(); render(getMenuElement(ref)); const isMenuOpen = true; - const openedWithArrow = true; - const setOpenedWithArrow = jest.fn(); - renderHook(() => useMenuFocus({ localRef: ref, isMenuOpen, openedWithArrow, setOpenedWithArrow })); + renderHook(() => useMenuFocus({ localRef: ref, isMenuOpen })); expect(screen.getByText('Item 1').tabIndex).toBe(0); - expect(setOpenedWithArrow).toHaveBeenCalledWith(false); }); it('clicks focused item when Enter key is pressed', () => { diff --git a/packages/grafana-ui/src/components/Menu/hooks.ts b/packages/grafana-ui/src/components/Menu/hooks.ts index 6713535827..b6111f9804 100644 --- a/packages/grafana-ui/src/components/Menu/hooks.ts +++ b/packages/grafana-ui/src/components/Menu/hooks.ts @@ -8,8 +8,6 @@ const UNFOCUSED = -1; export interface UseMenuFocusProps { localRef: RefObject; isMenuOpen?: boolean; - openedWithArrow?: boolean; - setOpenedWithArrow?: (openedWithArrow: boolean) => void; close?: () => void; onOpen?: (focusOnItem: (itemId: number) => void) => void; onClose?: () => void; @@ -23,8 +21,6 @@ export type UseMenuFocusReturn = [(event: React.KeyboardEvent) => void]; export const useMenuFocus = ({ localRef, isMenuOpen, - openedWithArrow, - setOpenedWithArrow, close, onOpen, onClose, @@ -33,11 +29,10 @@ export const useMenuFocus = ({ const [focusedItem, setFocusedItem] = useState(UNFOCUSED); useEffect(() => { - if (isMenuOpen && openedWithArrow) { + if (isMenuOpen) { setFocusedItem(0); - setOpenedWithArrow?.(false); } - }, [isMenuOpen, openedWithArrow, setOpenedWithArrow]); + }, [isMenuOpen]); useEffect(() => { const menuItems = localRef?.current?.querySelectorAll( diff --git a/public/app/features/alerting/unified/components/contact-points/ContactPoints.test.tsx b/public/app/features/alerting/unified/components/contact-points/ContactPoints.test.tsx index c6fd8c71ca..325867253b 100644 --- a/public/app/features/alerting/unified/components/contact-points/ContactPoints.test.tsx +++ b/public/app/features/alerting/unified/components/contact-points/ContactPoints.test.tsx @@ -126,6 +126,8 @@ describe('contact points', () => { await userEvent.click(button); const deleteButton = await screen.queryByRole('menuitem', { name: 'delete' }); expect(deleteButton).toBeDisabled(); + // click outside the menu to close it otherwise we can't interact with the rest of the page + await userEvent.click(document.body); } // check buttons in Notification Templates From 169be9e40170b15b2561d7e60c00be65ff6f82ee Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 19 Mar 2024 12:25:22 +0200 Subject: [PATCH 0785/1406] Update dependency eslint-plugin-import to v2.29.1 (#83523) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 437 +++++++++++++----------------------------------------- 1 file changed, 99 insertions(+), 338 deletions(-) diff --git a/yarn.lock b/yarn.lock index 892d379d04..d7ac4e07a5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11419,16 +11419,6 @@ __metadata: languageName: node linkType: hard -"array-buffer-byte-length@npm:^1.0.0": - version: 1.0.0 - resolution: "array-buffer-byte-length@npm:1.0.0" - dependencies: - call-bind: "npm:^1.0.2" - is-array-buffer: "npm:^3.0.1" - checksum: 10/044e101ce150f4804ad19c51d6c4d4cfa505c5b2577bd179256e4aa3f3f6a0a5e9874c78cd428ee566ac574c8a04d7ce21af9fe52e844abfdccb82b33035a7c3 - languageName: node - linkType: hard - "array-buffer-byte-length@npm:^1.0.1": version: 1.0.1 resolution: "array-buffer-byte-length@npm:1.0.1" @@ -11500,7 +11490,20 @@ __metadata: languageName: node linkType: hard -"array.prototype.flat@npm:^1.3.1": +"array.prototype.findlastindex@npm:^1.2.3": + version: 1.2.4 + resolution: "array.prototype.findlastindex@npm:1.2.4" + dependencies: + call-bind: "npm:^1.0.5" + define-properties: "npm:^1.2.1" + es-abstract: "npm:^1.22.3" + es-errors: "npm:^1.3.0" + es-shim-unscopables: "npm:^1.0.2" + checksum: 10/12d7de8da619065b9d4c40550d11c13f2fbbc863c4270ef01d022f49ef16fbe9022441ee9d60b1e952853c661dd4b3e05c21e4348d4631c6d93ddf802a252296 + languageName: node + linkType: hard + +"array.prototype.flat@npm:^1.3.1, array.prototype.flat@npm:^1.3.2": version: 1.3.2 resolution: "array.prototype.flat@npm:1.3.2" dependencies: @@ -11549,21 +11552,6 @@ __metadata: languageName: node linkType: hard -"arraybuffer.prototype.slice@npm:^1.0.2": - version: 1.0.2 - resolution: "arraybuffer.prototype.slice@npm:1.0.2" - dependencies: - array-buffer-byte-length: "npm:^1.0.0" - call-bind: "npm:^1.0.2" - define-properties: "npm:^1.2.0" - es-abstract: "npm:^1.22.1" - get-intrinsic: "npm:^1.2.1" - is-array-buffer: "npm:^3.0.2" - is-shared-array-buffer: "npm:^1.0.2" - checksum: 10/c200faf437786f5b2c80d4564ff5481c886a16dee642ef02abdc7306c7edd523d1f01d1dd12b769c7eb42ac9bc53874510db19a92a2c035c0f6696172aafa5d3 - languageName: node - linkType: hard - "arraybuffer.prototype.slice@npm:^1.0.3": version: 1.0.3 resolution: "arraybuffer.prototype.slice@npm:1.0.3" @@ -11750,13 +11738,6 @@ __metadata: languageName: node linkType: hard -"available-typed-arrays@npm:^1.0.5": - version: 1.0.5 - resolution: "available-typed-arrays@npm:1.0.5" - checksum: 10/4d4d5e86ea0425696f40717882f66a570647b94ac8d273ddc7549a9b61e5da099e149bf431530ccbd776bd74e02039eb8b5edf426e3e2211ee61af16698a9064 - languageName: node - linkType: hard - "available-typed-arrays@npm:^1.0.7": version: 1.0.7 resolution: "available-typed-arrays@npm:1.0.7" @@ -14676,7 +14657,7 @@ __metadata: languageName: node linkType: hard -"data-view-byte-length@npm:^1.0.0": +"data-view-byte-length@npm:^1.0.1": version: 1.0.1 resolution: "data-view-byte-length@npm:1.0.1" dependencies: @@ -14950,7 +14931,7 @@ __metadata: languageName: node linkType: hard -"define-properties@npm:^1.1.3, define-properties@npm:^1.1.4, define-properties@npm:^1.2.0, define-properties@npm:^1.2.1": +"define-properties@npm:^1.1.3, define-properties@npm:^1.2.0, define-properties@npm:^1.2.1": version: 1.2.1 resolution: "define-properties@npm:1.2.1" dependencies: @@ -15649,66 +15630,20 @@ __metadata: languageName: node linkType: hard -"es-abstract@npm:^1.20.4": - version: 1.22.2 - resolution: "es-abstract@npm:1.22.2" - dependencies: - array-buffer-byte-length: "npm:^1.0.0" - arraybuffer.prototype.slice: "npm:^1.0.2" - available-typed-arrays: "npm:^1.0.5" - call-bind: "npm:^1.0.2" - es-set-tostringtag: "npm:^2.0.1" - es-to-primitive: "npm:^1.2.1" - function.prototype.name: "npm:^1.1.6" - get-intrinsic: "npm:^1.2.1" - get-symbol-description: "npm:^1.0.0" - globalthis: "npm:^1.0.3" - gopd: "npm:^1.0.1" - has: "npm:^1.0.3" - has-property-descriptors: "npm:^1.0.0" - has-proto: "npm:^1.0.1" - has-symbols: "npm:^1.0.3" - internal-slot: "npm:^1.0.5" - is-array-buffer: "npm:^3.0.2" - is-callable: "npm:^1.2.7" - is-negative-zero: "npm:^2.0.2" - is-regex: "npm:^1.1.4" - is-shared-array-buffer: "npm:^1.0.2" - is-string: "npm:^1.0.7" - is-typed-array: "npm:^1.1.12" - is-weakref: "npm:^1.0.2" - object-inspect: "npm:^1.12.3" - object-keys: "npm:^1.1.1" - object.assign: "npm:^4.1.4" - regexp.prototype.flags: "npm:^1.5.1" - safe-array-concat: "npm:^1.0.1" - safe-regex-test: "npm:^1.0.0" - string.prototype.trim: "npm:^1.2.8" - string.prototype.trimend: "npm:^1.0.7" - string.prototype.trimstart: "npm:^1.0.7" - typed-array-buffer: "npm:^1.0.0" - typed-array-byte-length: "npm:^1.0.0" - typed-array-byte-offset: "npm:^1.0.0" - typed-array-length: "npm:^1.0.4" - unbox-primitive: "npm:^1.0.2" - which-typed-array: "npm:^1.1.11" - checksum: 10/fe09bf3bf707d5a781b9e4f9ef8e835a890600b7e1e65567328da12b173e99ffd9d5b86f5d0a69a5aa308a925b59c631814ada46fca55e9db10857a352289adb - languageName: node - linkType: hard - -"es-abstract@npm:^1.22.1, es-abstract@npm:^1.22.3, es-abstract@npm:^1.22.4": - version: 1.23.0 - resolution: "es-abstract@npm:1.23.0" +"es-abstract@npm:^1.22.1, es-abstract@npm:^1.22.3, es-abstract@npm:^1.22.4, es-abstract@npm:^1.23.0, es-abstract@npm:^1.23.2": + version: 1.23.2 + resolution: "es-abstract@npm:1.23.2" dependencies: array-buffer-byte-length: "npm:^1.0.1" arraybuffer.prototype.slice: "npm:^1.0.3" available-typed-arrays: "npm:^1.0.7" call-bind: "npm:^1.0.7" data-view-buffer: "npm:^1.0.1" - data-view-byte-length: "npm:^1.0.0" + data-view-byte-length: "npm:^1.0.1" data-view-byte-offset: "npm:^1.0.0" es-define-property: "npm:^1.0.0" es-errors: "npm:^1.3.0" + es-object-atoms: "npm:^1.0.0" es-set-tostringtag: "npm:^2.0.3" es-to-primitive: "npm:^1.2.1" function.prototype.name: "npm:^1.1.6" @@ -15719,7 +15654,7 @@ __metadata: has-property-descriptors: "npm:^1.0.2" has-proto: "npm:^1.0.3" has-symbols: "npm:^1.0.3" - hasown: "npm:^2.0.1" + hasown: "npm:^2.0.2" internal-slot: "npm:^1.0.7" is-array-buffer: "npm:^3.0.4" is-callable: "npm:^1.2.7" @@ -15734,18 +15669,18 @@ __metadata: object-keys: "npm:^1.1.1" object.assign: "npm:^4.1.5" regexp.prototype.flags: "npm:^1.5.2" - safe-array-concat: "npm:^1.1.0" + safe-array-concat: "npm:^1.1.2" safe-regex-test: "npm:^1.0.3" - string.prototype.trim: "npm:^1.2.8" - string.prototype.trimend: "npm:^1.0.7" + string.prototype.trim: "npm:^1.2.9" + string.prototype.trimend: "npm:^1.0.8" string.prototype.trimstart: "npm:^1.0.7" typed-array-buffer: "npm:^1.0.2" typed-array-byte-length: "npm:^1.0.1" typed-array-byte-offset: "npm:^1.0.2" typed-array-length: "npm:^1.0.5" unbox-primitive: "npm:^1.0.2" - which-typed-array: "npm:^1.1.14" - checksum: 10/b66cec32fcb896c7a3bbb7cb717f3f6bbbb73efe1c6003f0d7a899aecc358feed38ec2cad55e2a1d71a4a95ec7e7cc1dbbca34368deb0b98e36fe02cc5559b31 + which-typed-array: "npm:^1.1.15" + checksum: 10/f8fa0ef674b176f177f637f1af13fb895d10306e1eb1f57dc48a5aa64a643da307f96b222054ff76f3fd9029983295192c55fc54169f464ad2fcee992c5b7310 languageName: node linkType: hard @@ -15812,14 +15747,12 @@ __metadata: languageName: node linkType: hard -"es-set-tostringtag@npm:^2.0.1": - version: 2.0.1 - resolution: "es-set-tostringtag@npm:2.0.1" +"es-object-atoms@npm:^1.0.0": + version: 1.0.0 + resolution: "es-object-atoms@npm:1.0.0" dependencies: - get-intrinsic: "npm:^1.1.3" - has: "npm:^1.0.3" - has-tostringtag: "npm:^1.0.0" - checksum: 10/ec416a12948cefb4b2a5932e62093a7cf36ddc3efd58d6c58ca7ae7064475ace556434b869b0bbeb0c365f1032a8ccd577211101234b69837ad83ad204fff884 + es-errors: "npm:^1.3.0" + checksum: 10/f8910cf477e53c0615f685c5c96210591841850871b81924fcf256bfbaa68c254457d994a4308c60d15b20805e7f61ce6abc669375e01a5349391a8c1767584f languageName: node linkType: hard @@ -16306,51 +16239,53 @@ __metadata: languageName: node linkType: hard -"eslint-import-resolver-node@npm:^0.3.7": - version: 0.3.7 - resolution: "eslint-import-resolver-node@npm:0.3.7" +"eslint-import-resolver-node@npm:^0.3.9": + version: 0.3.9 + resolution: "eslint-import-resolver-node@npm:0.3.9" dependencies: debug: "npm:^3.2.7" - is-core-module: "npm:^2.11.0" - resolve: "npm:^1.22.1" - checksum: 10/31c6dfbd3457d1e6170ac2326b7ba9c323ff1ea68e3fcc5309f234bd1cefed050ee9b35e458b5eaed91323ab0d29bb2eddb41a1720ba7ca09bbacb00a0339d64 + is-core-module: "npm:^2.13.0" + resolve: "npm:^1.22.4" + checksum: 10/d52e08e1d96cf630957272e4f2644dcfb531e49dcfd1edd2e07e43369eb2ec7a7d4423d417beee613201206ff2efa4eb9a582b5825ee28802fc7c71fcd53ca83 languageName: node linkType: hard -"eslint-module-utils@npm:^2.7.4": - version: 2.8.0 - resolution: "eslint-module-utils@npm:2.8.0" +"eslint-module-utils@npm:^2.8.0": + version: 2.8.1 + resolution: "eslint-module-utils@npm:2.8.1" dependencies: debug: "npm:^3.2.7" peerDependenciesMeta: eslint: optional: true - checksum: 10/a9a7ed93eb858092e3cdc797357d4ead2b3ea06959b0eada31ab13862d46a59eb064b9cb82302214232e547980ce33618c2992f6821138a4934e65710ed9cc29 + checksum: 10/3e7892c0a984c963632da56b30ccf8254c29b535467138f91086c2ecdb2ebd10e2be61b54e553f30e5abf1d14d47a7baa0dac890e3a658fd3cd07dca63afbe6d languageName: node linkType: hard "eslint-plugin-import@npm:^2.26.0": - version: 2.27.5 - resolution: "eslint-plugin-import@npm:2.27.5" + version: 2.29.1 + resolution: "eslint-plugin-import@npm:2.29.1" dependencies: - array-includes: "npm:^3.1.6" - array.prototype.flat: "npm:^1.3.1" - array.prototype.flatmap: "npm:^1.3.1" + array-includes: "npm:^3.1.7" + array.prototype.findlastindex: "npm:^1.2.3" + array.prototype.flat: "npm:^1.3.2" + array.prototype.flatmap: "npm:^1.3.2" debug: "npm:^3.2.7" doctrine: "npm:^2.1.0" - eslint-import-resolver-node: "npm:^0.3.7" - eslint-module-utils: "npm:^2.7.4" - has: "npm:^1.0.3" - is-core-module: "npm:^2.11.0" + eslint-import-resolver-node: "npm:^0.3.9" + eslint-module-utils: "npm:^2.8.0" + hasown: "npm:^2.0.0" + is-core-module: "npm:^2.13.1" is-glob: "npm:^4.0.3" minimatch: "npm:^3.1.2" - object.values: "npm:^1.1.6" - resolve: "npm:^1.22.1" - semver: "npm:^6.3.0" - tsconfig-paths: "npm:^3.14.1" + object.fromentries: "npm:^2.0.7" + object.groupby: "npm:^1.0.1" + object.values: "npm:^1.1.7" + semver: "npm:^6.3.1" + tsconfig-paths: "npm:^3.15.0" peerDependencies: eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 - checksum: 10/b8ab9521bd47acdad959309cbb5635069cebd0f1dfd14b5f6ad24f609dfda82c604b029c7366cafce1d359845300957ec246587cd5e4b237a0378118a9d3dfa7 + checksum: 10/5865f05c38552145423c535326ec9a7113ab2305c7614c8b896ff905cfabc859c8805cac21e979c9f6f742afa333e6f62f812eabf891a7e8f5f0b853a32593c1 languageName: node linkType: hard @@ -17693,7 +17628,7 @@ __metadata: languageName: node linkType: hard -"function-bind@npm:^1.1.1, function-bind@npm:^1.1.2": +"function-bind@npm:^1.1.2": version: 1.1.2 resolution: "function-bind@npm:1.1.2" checksum: 10/185e20d20f10c8d661d59aac0f3b63b31132d492e1b11fcc2a93cb2c47257ebaee7407c38513efd2b35cafdf972d9beb2ea4593c1e0f3bf8f2744836928d7454 @@ -17834,18 +17769,6 @@ __metadata: languageName: node linkType: hard -"get-intrinsic@npm:^1.2.0": - version: 1.2.1 - resolution: "get-intrinsic@npm:1.2.1" - dependencies: - function-bind: "npm:^1.1.1" - has: "npm:^1.0.3" - has-proto: "npm:^1.0.1" - has-symbols: "npm:^1.0.3" - checksum: 10/aee631852063f8ad0d4a374970694b5c17c2fb5c92bd1929476d7eb8798ce7aebafbf9a34022c05fd1adaa2ce846d5877a627ce1986f81fc65adf3b81824bd54 - languageName: node - linkType: hard - "get-nonce@npm:^1.0.0": version: 1.0.1 resolution: "get-nonce@npm:1.0.1" @@ -17911,16 +17834,6 @@ __metadata: languageName: node linkType: hard -"get-symbol-description@npm:^1.0.0": - version: 1.0.0 - resolution: "get-symbol-description@npm:1.0.0" - dependencies: - call-bind: "npm:^1.0.2" - get-intrinsic: "npm:^1.1.1" - checksum: 10/7e5f298afe0f0872747dce4a949ce490ebc5d6dd6aefbbe5044543711c9b19a4dfaebdbc627aee99e1299d58a435b2fbfa083458c1d58be6dc03a3bada24d359 - languageName: node - linkType: hard - "get-symbol-description@npm:^1.0.2": version: 1.0.2 resolution: "get-symbol-description@npm:1.0.2" @@ -18824,16 +18737,7 @@ __metadata: languageName: node linkType: hard -"has@npm:^1.0.3": - version: 1.0.3 - resolution: "has@npm:1.0.3" - dependencies: - function-bind: "npm:^1.1.1" - checksum: 10/a449f3185b1d165026e8d25f6a8c3390bd25c201ff4b8c1aaf948fc6a5fcfd6507310b8c00c13a3325795ea9791fcc3d79d61eafa313b5750438fc19183df57b - languageName: node - linkType: hard - -"hasown@npm:^2.0.0, hasown@npm:^2.0.1": +"hasown@npm:^2.0.0, hasown@npm:^2.0.1, hasown@npm:^2.0.2": version: 2.0.2 resolution: "hasown@npm:2.0.2" dependencies: @@ -19640,17 +19544,6 @@ __metadata: languageName: node linkType: hard -"internal-slot@npm:^1.0.3": - version: 1.0.5 - resolution: "internal-slot@npm:1.0.5" - dependencies: - get-intrinsic: "npm:^1.2.0" - has: "npm:^1.0.3" - side-channel: "npm:^1.0.4" - checksum: 10/e2eb5b348e427957dd4092cb57b9374a2cbcabbf61e5e5b4d99cb68eeaae29394e8efd79f23dc2b1831253346f3c16b82010737b84841225e934d80d04d68643 - languageName: node - linkType: hard - "internal-slot@npm:^1.0.4, internal-slot@npm:^1.0.5, internal-slot@npm:^1.0.7": version: 1.0.7 resolution: "internal-slot@npm:1.0.7" @@ -19779,17 +19672,6 @@ __metadata: languageName: node linkType: hard -"is-array-buffer@npm:^3.0.2": - version: 3.0.2 - resolution: "is-array-buffer@npm:3.0.2" - dependencies: - call-bind: "npm:^1.0.2" - get-intrinsic: "npm:^1.2.0" - is-typed-array: "npm:^1.1.10" - checksum: 10/dcac9dda66ff17df9cabdc58214172bf41082f956eab30bb0d86bc0fab1e44b690fc8e1f855cf2481245caf4e8a5a006a982a71ddccec84032ed41f9d8da8c14 - languageName: node - linkType: hard - "is-arrayish@npm:^0.2.1": version: 0.2.1 resolution: "is-arrayish@npm:0.2.1" @@ -19861,7 +19743,7 @@ __metadata: languageName: node linkType: hard -"is-core-module@npm:^2.11.0, is-core-module@npm:^2.13.0, is-core-module@npm:^2.5.0, is-core-module@npm:^2.8.1": +"is-core-module@npm:^2.13.0, is-core-module@npm:^2.13.1, is-core-module@npm:^2.5.0, is-core-module@npm:^2.8.1": version: 2.13.1 resolution: "is-core-module@npm:2.13.1" dependencies: @@ -20094,13 +19976,6 @@ __metadata: languageName: node linkType: hard -"is-negative-zero@npm:^2.0.2": - version: 2.0.2 - resolution: "is-negative-zero@npm:2.0.2" - checksum: 10/edbec1a9e6454d68bf595a114c3a72343d2d0be7761d8173dae46c0b73d05bb8fe9398c85d121e7794a66467d2f40b4a610b0be84cd804262d234fc634c86131 - languageName: node - linkType: hard - "is-negative-zero@npm:^2.0.3": version: 2.0.3 resolution: "is-negative-zero@npm:2.0.3" @@ -20293,15 +20168,6 @@ __metadata: languageName: node linkType: hard -"is-typed-array@npm:^1.1.10, is-typed-array@npm:^1.1.12, is-typed-array@npm:^1.1.9": - version: 1.1.12 - resolution: "is-typed-array@npm:1.1.12" - dependencies: - which-typed-array: "npm:^1.1.11" - checksum: 10/d953adfd3c41618d5e01b2a10f21817e4cdc9572772fa17211100aebb3811b6e3c2e308a0558cc87d218a30504cb90154b833013437776551bfb70606fb088ca - languageName: node - linkType: hard - "is-typed-array@npm:^1.1.13, is-typed-array@npm:^1.1.3": version: 1.1.13 resolution: "is-typed-array@npm:1.1.13" @@ -21337,7 +21203,7 @@ __metadata: languageName: node linkType: hard -"json5@npm:^1.0.1": +"json5@npm:^1.0.2": version: 1.0.2 resolution: "json5@npm:1.0.2" dependencies: @@ -23772,13 +23638,6 @@ __metadata: languageName: node linkType: hard -"object-inspect@npm:^1.12.3": - version: 1.12.3 - resolution: "object-inspect@npm:1.12.3" - checksum: 10/532b0036f0472f561180fac0d04fe328ee01f57637624c83fb054f81b5bfe966cdf4200612a499ed391a7ca3c46b20a0bc3a55fc8241d944abe687c556a32b39 - languageName: node - linkType: hard - "object-inspect@npm:^1.13.1, object-inspect@npm:^1.9.0": version: 1.13.1 resolution: "object-inspect@npm:1.13.1" @@ -23837,6 +23696,17 @@ __metadata: languageName: node linkType: hard +"object.groupby@npm:^1.0.1": + version: 1.0.3 + resolution: "object.groupby@npm:1.0.3" + dependencies: + call-bind: "npm:^1.0.7" + define-properties: "npm:^1.2.1" + es-abstract: "npm:^1.23.2" + checksum: 10/44cb86dd2c660434be65f7585c54b62f0425b0c96b5c948d2756be253ef06737da7e68d7106e35506ce4a44d16aa85a413d11c5034eb7ce5579ec28752eb42d0 + languageName: node + linkType: hard + "object.hasown@npm:^1.1.2, object.hasown@npm:^1.1.3": version: 1.1.3 resolution: "object.hasown@npm:1.1.3" @@ -27166,18 +27036,7 @@ __metadata: languageName: node linkType: hard -"regexp.prototype.flags@npm:^1.4.3, regexp.prototype.flags@npm:^1.5.1": - version: 1.5.1 - resolution: "regexp.prototype.flags@npm:1.5.1" - dependencies: - call-bind: "npm:^1.0.2" - define-properties: "npm:^1.2.0" - set-function-name: "npm:^2.0.0" - checksum: 10/3fa5610b8e411bbc3a43ddfd13162f3a817beb43155fbd8caa24d4fd0ce2f431a8197541808772a5a06e5946cebfb68464c827827115bde0d11720a92fe2981a - languageName: node - linkType: hard - -"regexp.prototype.flags@npm:^1.5.0, regexp.prototype.flags@npm:^1.5.2": +"regexp.prototype.flags@npm:^1.4.3, regexp.prototype.flags@npm:^1.5.0, regexp.prototype.flags@npm:^1.5.2": version: 1.5.2 resolution: "regexp.prototype.flags@npm:1.5.2" dependencies: @@ -27436,7 +27295,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.22.1": +"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.22.1, resolve@npm:^1.22.4": version: 1.22.8 resolution: "resolve@npm:1.22.8" dependencies: @@ -27462,7 +27321,7 @@ __metadata: languageName: node linkType: hard -"resolve@patch:resolve@npm%3A^1.10.0#optional!builtin, resolve@patch:resolve@npm%3A^1.12.0#optional!builtin, resolve@patch:resolve@npm%3A^1.14.2#optional!builtin, resolve@patch:resolve@npm%3A^1.19.0#optional!builtin, resolve@patch:resolve@npm%3A^1.20.0#optional!builtin, resolve@patch:resolve@npm%3A^1.22.1#optional!builtin": +"resolve@patch:resolve@npm%3A^1.10.0#optional!builtin, resolve@patch:resolve@npm%3A^1.12.0#optional!builtin, resolve@patch:resolve@npm%3A^1.14.2#optional!builtin, resolve@patch:resolve@npm%3A^1.19.0#optional!builtin, resolve@patch:resolve@npm%3A^1.20.0#optional!builtin, resolve@patch:resolve@npm%3A^1.22.1#optional!builtin, resolve@patch:resolve@npm%3A^1.22.4#optional!builtin": version: 1.22.8 resolution: "resolve@patch:resolve@npm%3A1.22.8#optional!builtin::version=1.22.8&hash=c3c19d" dependencies: @@ -27773,19 +27632,7 @@ __metadata: languageName: node linkType: hard -"safe-array-concat@npm:^1.0.1": - version: 1.0.1 - resolution: "safe-array-concat@npm:1.0.1" - dependencies: - call-bind: "npm:^1.0.2" - get-intrinsic: "npm:^1.2.1" - has-symbols: "npm:^1.0.3" - isarray: "npm:^2.0.5" - checksum: 10/44f073d85ca12458138e6eff103ac63cec619c8261b6579bd2fa3ae7b6516cf153f02596d68e40c5bbe322a29c930017800efff652734ddcb8c0f33b2a71f89c - languageName: node - linkType: hard - -"safe-array-concat@npm:^1.1.0": +"safe-array-concat@npm:^1.1.0, safe-array-concat@npm:^1.1.2": version: 1.1.2 resolution: "safe-array-concat@npm:1.1.2" dependencies: @@ -27818,17 +27665,6 @@ __metadata: languageName: node linkType: hard -"safe-regex-test@npm:^1.0.0": - version: 1.0.0 - resolution: "safe-regex-test@npm:1.0.0" - dependencies: - call-bind: "npm:^1.0.2" - get-intrinsic: "npm:^1.1.3" - is-regex: "npm:^1.1.4" - checksum: 10/c7248dfa07891aa634c8b9c55da696e246f8589ca50e7fd14b22b154a106e83209ddf061baf2fa45ebfbd485b094dc7297325acfc50724de6afe7138451b42a9 - languageName: node - linkType: hard - "safe-regex-test@npm:^1.0.3": version: 1.0.3 resolution: "safe-regex-test@npm:1.0.3" @@ -29064,7 +28900,7 @@ __metadata: languageName: node linkType: hard -"string.prototype.matchall@npm:^4.0.10": +"string.prototype.matchall@npm:^4.0.10, string.prototype.matchall@npm:^4.0.8": version: 4.0.10 resolution: "string.prototype.matchall@npm:4.0.10" dependencies: @@ -29081,41 +28917,26 @@ __metadata: languageName: node linkType: hard -"string.prototype.matchall@npm:^4.0.8": - version: 4.0.8 - resolution: "string.prototype.matchall@npm:4.0.8" +"string.prototype.trim@npm:^1.2.9": + version: 1.2.9 + resolution: "string.prototype.trim@npm:1.2.9" dependencies: - call-bind: "npm:^1.0.2" - define-properties: "npm:^1.1.4" - es-abstract: "npm:^1.20.4" - get-intrinsic: "npm:^1.1.3" - has-symbols: "npm:^1.0.3" - internal-slot: "npm:^1.0.3" - regexp.prototype.flags: "npm:^1.4.3" - side-channel: "npm:^1.0.4" - checksum: 10/9de2e9e33344002e08c03c13533d88d0c557d5a3d9214a4f2cc8d63349f7c35af895804dec08e43224cc4c0345651c678e14260c5933967fd97aad4640a7e485 - languageName: node - linkType: hard - -"string.prototype.trim@npm:^1.2.8": - version: 1.2.8 - resolution: "string.prototype.trim@npm:1.2.8" - dependencies: - call-bind: "npm:^1.0.2" - define-properties: "npm:^1.2.0" - es-abstract: "npm:^1.22.1" - checksum: 10/9301f6cb2b6c44f069adde1b50f4048915985170a20a1d64cf7cb2dc53c5cd6b9525b92431f1257f894f94892d6c4ae19b5aa7f577c3589e7e51772dffc9d5a4 + call-bind: "npm:^1.0.7" + define-properties: "npm:^1.2.1" + es-abstract: "npm:^1.23.0" + es-object-atoms: "npm:^1.0.0" + checksum: 10/b2170903de6a2fb5a49bb8850052144e04b67329d49f1343cdc6a87cb24fb4e4b8ad00d3e273a399b8a3d8c32c89775d93a8f43cb42fbff303f25382079fb58a languageName: node linkType: hard -"string.prototype.trimend@npm:^1.0.7": - version: 1.0.7 - resolution: "string.prototype.trimend@npm:1.0.7" +"string.prototype.trimend@npm:^1.0.8": + version: 1.0.8 + resolution: "string.prototype.trimend@npm:1.0.8" dependencies: - call-bind: "npm:^1.0.2" - define-properties: "npm:^1.2.0" - es-abstract: "npm:^1.22.1" - checksum: 10/3f0d3397ab9bd95cd98ae2fe0943bd3e7b63d333c2ab88f1875cf2e7c958c75dc3355f6fe19ee7c8fca28de6f39f2475e955e103821feb41299a2764a7463ffa + call-bind: "npm:^1.0.7" + define-properties: "npm:^1.2.1" + es-object-atoms: "npm:^1.0.0" + checksum: 10/c2e862ae724f95771da9ea17c27559d4eeced9208b9c20f69bbfcd1b9bc92375adf8af63a103194dba17c4cc4a5cb08842d929f415ff9d89c062d44689c8761b languageName: node linkType: hard @@ -30071,15 +29892,15 @@ __metadata: languageName: node linkType: hard -"tsconfig-paths@npm:^3.14.1": - version: 3.14.1 - resolution: "tsconfig-paths@npm:3.14.1" +"tsconfig-paths@npm:^3.15.0": + version: 3.15.0 + resolution: "tsconfig-paths@npm:3.15.0" dependencies: "@types/json5": "npm:^0.0.29" - json5: "npm:^1.0.1" + json5: "npm:^1.0.2" minimist: "npm:^1.2.6" strip-bom: "npm:^3.0.0" - checksum: 10/51be8bd8f90e49d2f8b3f61f544557e631dd5cee35e247dd316be27d723c9e99de9ce59eb39395ca20f1e43aedfc1fef0272ba25acb0a0e0e9a38cffd692256d + checksum: 10/2041beaedc6c271fc3bedd12e0da0cc553e65d030d4ff26044b771fac5752d0460944c0b5e680f670c2868c95c664a256cec960ae528888db6ded83524e33a14 languageName: node linkType: hard @@ -30274,17 +30095,6 @@ __metadata: languageName: node linkType: hard -"typed-array-buffer@npm:^1.0.0": - version: 1.0.0 - resolution: "typed-array-buffer@npm:1.0.0" - dependencies: - call-bind: "npm:^1.0.2" - get-intrinsic: "npm:^1.2.1" - is-typed-array: "npm:^1.1.10" - checksum: 10/3e0281c79b2a40cd97fe715db803884301993f4e8c18e8d79d75fd18f796e8cd203310fec8c7fdb5e6c09bedf0af4f6ab8b75eb3d3a85da69328f28a80456bd3 - languageName: node - linkType: hard - "typed-array-buffer@npm:^1.0.2": version: 1.0.2 resolution: "typed-array-buffer@npm:1.0.2" @@ -30296,18 +30106,6 @@ __metadata: languageName: node linkType: hard -"typed-array-byte-length@npm:^1.0.0": - version: 1.0.0 - resolution: "typed-array-byte-length@npm:1.0.0" - dependencies: - call-bind: "npm:^1.0.2" - for-each: "npm:^0.3.3" - has-proto: "npm:^1.0.1" - is-typed-array: "npm:^1.1.10" - checksum: 10/6f376bf5d988f00f98ccee41fd551cafc389095a2a307c18fab30f29da7d1464fc3697139cf254cda98b4128bbcb114f4b557bbabdc6d9c2e5039c515b31decf - languageName: node - linkType: hard - "typed-array-byte-length@npm:^1.0.1": version: 1.0.1 resolution: "typed-array-byte-length@npm:1.0.1" @@ -30321,19 +30119,6 @@ __metadata: languageName: node linkType: hard -"typed-array-byte-offset@npm:^1.0.0": - version: 1.0.0 - resolution: "typed-array-byte-offset@npm:1.0.0" - dependencies: - available-typed-arrays: "npm:^1.0.5" - call-bind: "npm:^1.0.2" - for-each: "npm:^0.3.3" - has-proto: "npm:^1.0.1" - is-typed-array: "npm:^1.1.10" - checksum: 10/2d81747faae31ca79f6c597dc18e15ae3d5b7e97f7aaebce3b31f46feeb2a6c1d6c92b9a634d901c83731ffb7ec0b74d05c6ff56076f5ae39db0cd19b16a3f92 - languageName: node - linkType: hard - "typed-array-byte-offset@npm:^1.0.2": version: 1.0.2 resolution: "typed-array-byte-offset@npm:1.0.2" @@ -30348,17 +30133,6 @@ __metadata: languageName: node linkType: hard -"typed-array-length@npm:^1.0.4": - version: 1.0.4 - resolution: "typed-array-length@npm:1.0.4" - dependencies: - call-bind: "npm:^1.0.2" - for-each: "npm:^0.3.3" - is-typed-array: "npm:^1.1.9" - checksum: 10/0444658acc110b233176cb0b7689dcb828b0cfa099ab1d377da430e8553b6fdcdce882360b7ffe9ae085b6330e1d39383d7b2c61574d6cd8eef651d3e4a87822 - languageName: node - linkType: hard - "typed-array-length@npm:^1.0.5": version: 1.0.5 resolution: "typed-array-length@npm:1.0.5" @@ -31560,20 +31334,7 @@ __metadata: languageName: node linkType: hard -"which-typed-array@npm:^1.1.11": - version: 1.1.11 - resolution: "which-typed-array@npm:1.1.11" - dependencies: - available-typed-arrays: "npm:^1.0.5" - call-bind: "npm:^1.0.2" - for-each: "npm:^0.3.3" - gopd: "npm:^1.0.1" - has-tostringtag: "npm:^1.0.0" - checksum: 10/bc9e8690e71d6c64893c9d88a7daca33af45918861003013faf77574a6a49cc6194d32ca7826e90de341d2f9ef3ac9e3acbe332a8ae73cadf07f59b9c6c6ecad - languageName: node - linkType: hard - -"which-typed-array@npm:^1.1.14, which-typed-array@npm:^1.1.2, which-typed-array@npm:^1.1.9": +"which-typed-array@npm:^1.1.14, which-typed-array@npm:^1.1.15, which-typed-array@npm:^1.1.2, which-typed-array@npm:^1.1.9": version: 1.1.15 resolution: "which-typed-array@npm:1.1.15" dependencies: From 31bbd494596c63b48d1f2756e31687da8e4c2bdf Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 19 Mar 2024 10:37:33 +0000 Subject: [PATCH 0786/1406] Update dependency @rollup/plugin-terser to v0.4.4 (#84550) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- packages/grafana-runtime/package.json | 2 +- yarn.lock | 31 +++++++++++++++++---------- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/packages/grafana-runtime/package.json b/packages/grafana-runtime/package.json index d622e1bd00..5a3012edcf 100644 --- a/packages/grafana-runtime/package.json +++ b/packages/grafana-runtime/package.json @@ -52,7 +52,7 @@ "devDependencies": { "@grafana/tsconfig": "^1.3.0-rc1", "@rollup/plugin-node-resolve": "15.2.3", - "@rollup/plugin-terser": "0.1.0", + "@rollup/plugin-terser": "0.4.4", "@testing-library/dom": "9.3.4", "@testing-library/react": "14.2.1", "@testing-library/user-event": "14.5.2", diff --git a/yarn.lock b/yarn.lock index d7ac4e07a5..d0752c2d6c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4004,7 +4004,7 @@ __metadata: "@grafana/tsconfig": "npm:^1.3.0-rc1" "@grafana/ui": "npm:11.0.0-pre" "@rollup/plugin-node-resolve": "npm:15.2.3" - "@rollup/plugin-terser": "npm:0.1.0" + "@rollup/plugin-terser": "npm:0.4.4" "@testing-library/dom": "npm:9.3.4" "@testing-library/react": "npm:14.2.1" "@testing-library/user-event": "npm:14.5.2" @@ -6989,17 +6989,19 @@ __metadata: languageName: node linkType: hard -"@rollup/plugin-terser@npm:0.1.0": - version: 0.1.0 - resolution: "@rollup/plugin-terser@npm:0.1.0" +"@rollup/plugin-terser@npm:0.4.4": + version: 0.4.4 + resolution: "@rollup/plugin-terser@npm:0.4.4" dependencies: - terser: "npm:^5.15.1" + serialize-javascript: "npm:^6.0.1" + smob: "npm:^1.0.0" + terser: "npm:^5.17.4" peerDependencies: - rollup: ^2.x || ^3.x + rollup: ^2.0.0||^3.0.0||^4.0.0 peerDependenciesMeta: rollup: optional: true - checksum: 10/e8abed2bc9f91d2aa54819fd26e98333596b97153bb1d636fe2396db258ea580cc6c88f9e28ea2f69a20fa4e7dca33d0a9f235cd21986f4b633484757b0f31d2 + checksum: 10/a5e066ddea55fc8c32188bc8b484cca619713516f10e3a06801881ec98bf37459ca24e5fe8711f93a5fa7f26a6e9132a47bc1a61c01e0b513dfd79a96cdc6eb7 languageName: node linkType: hard @@ -28278,6 +28280,13 @@ __metadata: languageName: node linkType: hard +"smob@npm:^1.0.0": + version: 1.4.1 + resolution: "smob@npm:1.4.1" + checksum: 10/bc6ffcb9a1c3c875f9354cf814487d44cd925e2917683e2bf6f66a267eedf895f4989079541b73dc0ddc163cb0fa26078fa95067f1503707758437e9308afc2f + languageName: node + linkType: hard + "sockjs@npm:^0.3.24": version: 0.3.24 resolution: "sockjs@npm:0.3.24" @@ -29456,9 +29465,9 @@ __metadata: languageName: node linkType: hard -"terser@npm:^5.15.1, terser@npm:^5.26.0, terser@npm:^5.7.2": - version: 5.27.0 - resolution: "terser@npm:5.27.0" +"terser@npm:^5.15.1, terser@npm:^5.17.4, terser@npm:^5.26.0, terser@npm:^5.7.2": + version: 5.29.2 + resolution: "terser@npm:5.29.2" dependencies: "@jridgewell/source-map": "npm:^0.3.3" acorn: "npm:^8.8.2" @@ -29466,7 +29475,7 @@ __metadata: source-map-support: "npm:~0.5.20" bin: terser: bin/terser - checksum: 10/9b2c5cb00747dea5994034ca064fb3cc7efc1be6b79a35247662d51ab43bdbe9cbf002bbf29170b5f3bd068c811d0212e22d94acd2cf0d8562687b96f1bffc9f + checksum: 10/062df6a8f99ea2635d1b3ce41cfd4180dea6e1c83db9b2cf4b525170b2446d10e069d2877d8dcb59fbf6045870efa17b56462b67045ef2d2b420870f9d144690 languageName: node linkType: hard From b5d74ac200579876a7eb391fc1d8593980148cdd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 19 Mar 2024 12:56:16 +0200 Subject: [PATCH 0787/1406] Update babel monorepo to v7.24.1 (#84718) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- packages/grafana-flamegraph/package.json | 6 +- packages/grafana-ui/package.json | 2 +- yarn.lock | 960 ++++++++++++----------- 4 files changed, 498 insertions(+), 472 deletions(-) diff --git a/package.json b/package.json index 90add7b5a5..2d5e199e92 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "releaseNotesUrl": "https://grafana.com/docs/grafana/next/release-notes/" }, "devDependencies": { - "@babel/runtime": "7.24.0", + "@babel/runtime": "7.24.1", "@betterer/betterer": "5.4.0", "@betterer/cli": "5.4.0", "@betterer/eslint": "5.4.0", diff --git a/packages/grafana-flamegraph/package.json b/packages/grafana-flamegraph/package.json index c1c3371f8a..7f7be532c7 100644 --- a/packages/grafana-flamegraph/package.json +++ b/packages/grafana-flamegraph/package.json @@ -56,9 +56,9 @@ "tslib": "2.6.2" }, "devDependencies": { - "@babel/core": "7.24.0", - "@babel/preset-env": "7.24.0", - "@babel/preset-react": "7.23.3", + "@babel/core": "7.24.1", + "@babel/preset-env": "7.24.1", + "@babel/preset-react": "7.24.1", "@grafana/tsconfig": "^1.3.0-rc1", "@rollup/plugin-node-resolve": "15.2.3", "@testing-library/jest-dom": "^6.1.2", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index c1f46dc653..0a3dedc03d 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -109,7 +109,7 @@ "uuid": "9.0.1" }, "devDependencies": { - "@babel/core": "7.24.0", + "@babel/core": "7.24.1", "@grafana/tsconfig": "^1.3.0-rc1", "@rollup/plugin-node-resolve": "15.2.3", "@storybook/addon-a11y": "7.4.5", diff --git a/yarn.lock b/yarn.lock index d0752c2d6c..ee9203dc94 100644 --- a/yarn.lock +++ b/yarn.lock @@ -40,20 +40,20 @@ __metadata: languageName: node linkType: hard -"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.3, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.16.7, @babel/code-frame@npm:^7.18.6, @babel/code-frame@npm:^7.22.13, @babel/code-frame@npm:^7.23.5": - version: 7.23.5 - resolution: "@babel/code-frame@npm:7.23.5" +"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.3, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.16.7, @babel/code-frame@npm:^7.18.6, @babel/code-frame@npm:^7.22.13, @babel/code-frame@npm:^7.23.5, @babel/code-frame@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/code-frame@npm:7.24.1" dependencies: - "@babel/highlight": "npm:^7.23.4" - chalk: "npm:^2.4.2" - checksum: 10/44e58529c9d93083288dc9e649c553c5ba997475a7b0758cc3ddc4d77b8a7d985dbe78cc39c9bbc61f26d50af6da1ddf0a3427eae8cc222a9370619b671ed8f5 + "@babel/highlight": "npm:^7.24.1" + picocolors: "npm:^1.0.0" + checksum: 10/71da2249ea5cea5f0cb4c6e0052e921574d16ae415c5b876123787d160abc98442a91701834c875363dadd75d200897aa278336c505335722298982f793bdf89 languageName: node linkType: hard -"@babel/compat-data@npm:^7.22.6, @babel/compat-data@npm:^7.23.2, @babel/compat-data@npm:^7.23.5": - version: 7.23.5 - resolution: "@babel/compat-data@npm:7.23.5" - checksum: 10/088f14f646ecbddd5ef89f120a60a1b3389a50a9705d44603dca77662707d0175a5e0e0da3943c3298f1907a4ab871468656fbbf74bb7842cd8b0686b2c19736 +"@babel/compat-data@npm:^7.22.6, @babel/compat-data@npm:^7.23.2, @babel/compat-data@npm:^7.23.5, @babel/compat-data@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/compat-data@npm:7.24.1" + checksum: 10/d5460b99c07ff8487467c52f742a219c7e3bcdcaa2882456a13c0d0c8116405f0c85a651fb60511284dc64ed627a5e989f24c3cd6e71d07a9947e7c8954b433c languageName: node linkType: hard @@ -80,38 +80,38 @@ __metadata: languageName: node linkType: hard -"@babel/core@npm:7.24.0, @babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.13.16, @babel/core@npm:^7.22.9, @babel/core@npm:^7.7.5": - version: 7.24.0 - resolution: "@babel/core@npm:7.24.0" +"@babel/core@npm:7.24.1, @babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.13.16, @babel/core@npm:^7.22.9, @babel/core@npm:^7.7.5": + version: 7.24.1 + resolution: "@babel/core@npm:7.24.1" dependencies: "@ampproject/remapping": "npm:^2.2.0" - "@babel/code-frame": "npm:^7.23.5" - "@babel/generator": "npm:^7.23.6" + "@babel/code-frame": "npm:^7.24.1" + "@babel/generator": "npm:^7.24.1" "@babel/helper-compilation-targets": "npm:^7.23.6" "@babel/helper-module-transforms": "npm:^7.23.3" - "@babel/helpers": "npm:^7.24.0" - "@babel/parser": "npm:^7.24.0" + "@babel/helpers": "npm:^7.24.1" + "@babel/parser": "npm:^7.24.1" "@babel/template": "npm:^7.24.0" - "@babel/traverse": "npm:^7.24.0" + "@babel/traverse": "npm:^7.24.1" "@babel/types": "npm:^7.24.0" convert-source-map: "npm:^2.0.0" debug: "npm:^4.1.0" gensync: "npm:^1.0.0-beta.2" json5: "npm:^2.2.3" semver: "npm:^6.3.1" - checksum: 10/1e22215cc89e061e0cbfed72f265ad24d363f3e9b24b51e9c4cf3ccb9222260a29a1c1e62edb439cb7e2229a3fce924edd43300500416613236c13fc8d62a947 + checksum: 10/f8c153acd619a5d17caee7aff052a2aa306ceda280ffc07182d7b5dd40c41c7511ae89d64bc23ec5555e4639fc9c87ceb7b4afc12252acab548ebb7654397680 languageName: node linkType: hard -"@babel/generator@npm:^7.12.11, @babel/generator@npm:^7.22.9, @babel/generator@npm:^7.23.0, @babel/generator@npm:^7.23.6, @babel/generator@npm:^7.7.2": - version: 7.23.6 - resolution: "@babel/generator@npm:7.23.6" +"@babel/generator@npm:^7.12.11, @babel/generator@npm:^7.22.9, @babel/generator@npm:^7.23.0, @babel/generator@npm:^7.24.1, @babel/generator@npm:^7.7.2": + version: 7.24.1 + resolution: "@babel/generator@npm:7.24.1" dependencies: - "@babel/types": "npm:^7.23.6" - "@jridgewell/gen-mapping": "npm:^0.3.2" - "@jridgewell/trace-mapping": "npm:^0.3.17" + "@babel/types": "npm:^7.24.0" + "@jridgewell/gen-mapping": "npm:^0.3.5" + "@jridgewell/trace-mapping": "npm:^0.3.25" jsesc: "npm:^2.5.1" - checksum: 10/864090d5122c0aa3074471fd7b79d8a880c1468480cbd28925020a3dcc7eb6e98bedcdb38983df299c12b44b166e30915b8085a7bc126e68fa7e2aadc7bd1ac5 + checksum: 10/c6160e9cd63d7ed7168dee27d827f9c46fab820c45861a5df56cd5c78047f7c3fc97c341e9ccfa1a6f97c87ec2563d9903380b5f92794e3540a6c5f99eb8f075 languageName: node linkType: hard @@ -146,22 +146,22 @@ __metadata: languageName: node linkType: hard -"@babel/helper-create-class-features-plugin@npm:^7.18.6, @babel/helper-create-class-features-plugin@npm:^7.22.15": - version: 7.23.7 - resolution: "@babel/helper-create-class-features-plugin@npm:7.23.7" +"@babel/helper-create-class-features-plugin@npm:^7.18.6, @babel/helper-create-class-features-plugin@npm:^7.22.15, @babel/helper-create-class-features-plugin@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/helper-create-class-features-plugin@npm:7.24.1" dependencies: "@babel/helper-annotate-as-pure": "npm:^7.22.5" "@babel/helper-environment-visitor": "npm:^7.22.20" "@babel/helper-function-name": "npm:^7.23.0" "@babel/helper-member-expression-to-functions": "npm:^7.23.0" "@babel/helper-optimise-call-expression": "npm:^7.22.5" - "@babel/helper-replace-supers": "npm:^7.22.20" + "@babel/helper-replace-supers": "npm:^7.24.1" "@babel/helper-skip-transparent-expression-wrappers": "npm:^7.22.5" "@babel/helper-split-export-declaration": "npm:^7.22.6" semver: "npm:^6.3.1" peerDependencies: "@babel/core": ^7.0.0 - checksum: 10/c8b3ef58fca399a25f00d703b0fb2ac1d86642d9e3bd7af04df77857641ed08aaca042ffb271ef93771f9272481fd1cf102a9bddfcee407fb126c927deeef6a7 + checksum: 10/c48e9ce842cbd55099a6b9893df1b4fb08c88061d6c20c37a5279b95249879be478210b587295b55d3675428d2ce4306c790cf6332f478ab2af0061f940156f3 languageName: node linkType: hard @@ -208,6 +208,21 @@ __metadata: languageName: node linkType: hard +"@babel/helper-define-polyfill-provider@npm:^0.6.1": + version: 0.6.1 + resolution: "@babel/helper-define-polyfill-provider@npm:0.6.1" + dependencies: + "@babel/helper-compilation-targets": "npm:^7.22.6" + "@babel/helper-plugin-utils": "npm:^7.22.5" + debug: "npm:^4.1.1" + lodash.debounce: "npm:^4.0.8" + resolve: "npm:^1.14.2" + peerDependencies: + "@babel/core": ^7.4.0 || ^8.0.0-0 <8.0.0 + checksum: 10/316e7c0f05d2ae233d5fbb622c6339436da8d2b2047be866b64a16e6996c078a23b4adfebbdb33bc6a9882326a6cc20b95daa79a5e0edc92e9730e36d45fa523 + languageName: node + linkType: hard + "@babel/helper-environment-visitor@npm:^7.22.20": version: 7.22.20 resolution: "@babel/helper-environment-visitor@npm:7.22.20" @@ -234,7 +249,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-member-expression-to-functions@npm:^7.0.0, @babel/helper-member-expression-to-functions@npm:^7.22.15, @babel/helper-member-expression-to-functions@npm:^7.23.0": +"@babel/helper-member-expression-to-functions@npm:^7.0.0, @babel/helper-member-expression-to-functions@npm:^7.23.0": version: 7.23.0 resolution: "@babel/helper-member-expression-to-functions@npm:7.23.0" dependencies: @@ -243,12 +258,12 @@ __metadata: languageName: node linkType: hard -"@babel/helper-module-imports@npm:^7.0.0, @babel/helper-module-imports@npm:^7.16.7, @babel/helper-module-imports@npm:^7.22.15": - version: 7.22.15 - resolution: "@babel/helper-module-imports@npm:7.22.15" +"@babel/helper-module-imports@npm:^7.0.0, @babel/helper-module-imports@npm:^7.16.7, @babel/helper-module-imports@npm:^7.22.15, @babel/helper-module-imports@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/helper-module-imports@npm:7.24.1" dependencies: - "@babel/types": "npm:^7.22.15" - checksum: 10/5ecf9345a73b80c28677cfbe674b9f567bb0d079e37dcba9055e36cb337db24ae71992a58e1affa9d14a60d3c69907d30fe1f80aea105184501750a58d15c81c + "@babel/types": "npm:^7.24.0" + checksum: 10/f771c1e8776890d85c2af06a6a36410cb41ff4bbcf162e7eecce7d53ebdc5196cc38c3abea3a06df9d778bb20edccb2909dfb8187ae82730e4d021329b631921 languageName: node linkType: hard @@ -296,16 +311,16 @@ __metadata: languageName: node linkType: hard -"@babel/helper-replace-supers@npm:^7.0.0, @babel/helper-replace-supers@npm:^7.22.20": - version: 7.22.20 - resolution: "@babel/helper-replace-supers@npm:7.22.20" +"@babel/helper-replace-supers@npm:^7.0.0, @babel/helper-replace-supers@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/helper-replace-supers@npm:7.24.1" dependencies: "@babel/helper-environment-visitor": "npm:^7.22.20" - "@babel/helper-member-expression-to-functions": "npm:^7.22.15" + "@babel/helper-member-expression-to-functions": "npm:^7.23.0" "@babel/helper-optimise-call-expression": "npm:^7.22.5" peerDependencies: "@babel/core": ^7.0.0 - checksum: 10/617666f57b0f94a2f430ee66b67c8f6fa94d4c22400f622947580d8f3638ea34b71280af59599ed4afbb54ae6e2bdd4f9083fe0e341184a4bb0bd26ef58d3017 + checksum: 10/1103b28ce0cc7fba903c21bc78035c696ff191bdbbe83c20c37030a2e10ae6254924556d942cdf8c44c48ba606a8266fdb105e6bb10945de9285f79cb1905df1 languageName: node linkType: hard @@ -368,70 +383,71 @@ __metadata: languageName: node linkType: hard -"@babel/helpers@npm:^7.23.2, @babel/helpers@npm:^7.24.0": - version: 7.24.0 - resolution: "@babel/helpers@npm:7.24.0" +"@babel/helpers@npm:^7.23.2, @babel/helpers@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/helpers@npm:7.24.1" dependencies: "@babel/template": "npm:^7.24.0" - "@babel/traverse": "npm:^7.24.0" + "@babel/traverse": "npm:^7.24.1" "@babel/types": "npm:^7.24.0" - checksum: 10/cc82012161b30185c2698da359c7311cf019f0932f8fcb805e985fec9e0053c354f0534dc9961f3170eee579df6724eecd34b0f5ffaa155cdd456af59fbff86e + checksum: 10/82d3cdd3beafc4583f237515ef220bc205ced8b0540c6c6e191fc367a9589bd7304b8f9800d3d7574d4db9f079bd555979816b1874c86e53b3e7dd2032ad6c7c languageName: node linkType: hard -"@babel/highlight@npm:^7.23.4": - version: 7.23.4 - resolution: "@babel/highlight@npm:7.23.4" +"@babel/highlight@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/highlight@npm:7.24.1" dependencies: "@babel/helper-validator-identifier": "npm:^7.22.20" chalk: "npm:^2.4.2" js-tokens: "npm:^4.0.0" - checksum: 10/62fef9b5bcea7131df4626d009029b1ae85332042f4648a4ce6e740c3fd23112603c740c45575caec62f260c96b11054d3be5987f4981a5479793579c3aac71f + picocolors: "npm:^1.0.0" + checksum: 10/5d9ad31006f462d3863e9c73004bd46d94d2a4144b3fcc1c9945d8a82411d05477d47466a1121f1d1dea986780145bf934e8b2809c6f056b2203fe481b4d471d languageName: node linkType: hard -"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.13.16, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.22.7, @babel/parser@npm:^7.23.0, @babel/parser@npm:^7.24.0": - version: 7.24.0 - resolution: "@babel/parser@npm:7.24.0" +"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.13.16, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.22.7, @babel/parser@npm:^7.23.0, @babel/parser@npm:^7.24.0, @babel/parser@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/parser@npm:7.24.1" bin: parser: ./bin/babel-parser.js - checksum: 10/3e5ebb903a6f71629a9d0226743e37fe3d961e79911d2698b243637f66c4df7e3e0a42c07838bc0e7cc9fcd585d9be8f4134a145b9459ee4a459420fb0d1360b + checksum: 10/561d9454091e07ecfec3828ce79204c0fc9d24e17763f36181c6984392be4ca6b79c8225f2224fdb7b1b3b70940e243368c8f83ac77ec2dc20f46d3d06bd6795 languageName: node linkType: hard -"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:^7.22.15, @babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:^7.23.3": - version: 7.23.3 - resolution: "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:7.23.3" +"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:^7.22.15, @babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:7.24.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-plugin-utils": "npm:^7.24.0" peerDependencies: "@babel/core": ^7.0.0 - checksum: 10/ddbaf2c396b7780f15e80ee01d6dd790db076985f3dfeb6527d1a8d4cacf370e49250396a3aa005b2c40233cac214a106232f83703d5e8491848bde273938232 + checksum: 10/ec5fddc8db6de0e0082a883f21141d6f4f9f9f0bc190d662a732b5e9a506aae5d7d2337049a1bf055d7cb7add6f128036db6d4f47de5e9ac1be29e043c8b7ca8 languageName: node linkType: hard -"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@npm:^7.22.15, @babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@npm:^7.23.3": - version: 7.23.3 - resolution: "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@npm:7.23.3" +"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@npm:^7.22.15, @babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@npm:7.24.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-plugin-utils": "npm:^7.24.0" "@babel/helper-skip-transparent-expression-wrappers": "npm:^7.22.5" - "@babel/plugin-transform-optional-chaining": "npm:^7.23.3" + "@babel/plugin-transform-optional-chaining": "npm:^7.24.1" peerDependencies: "@babel/core": ^7.13.0 - checksum: 10/434b9d710ae856fa1a456678cc304fbc93915af86d581ee316e077af746a709a741ea39d7e1d4f5b98861b629cc7e87f002d3138f5e836775632466d4c74aef2 + checksum: 10/e18235463e716ac2443938aaec3c18b40c417a1746fba0fa4c26cf4d71326b76ef26c002081ab1b445abfae98e063d561519aa55672dddc1ef80b3940211ffbb languageName: node linkType: hard -"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@npm:^7.23.7": - version: 7.23.7 - resolution: "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@npm:7.23.7" +"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@npm:7.24.1" dependencies: "@babel/helper-environment-visitor": "npm:^7.22.20" - "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-plugin-utils": "npm:^7.24.0" peerDependencies: "@babel/core": ^7.0.0 - checksum: 10/3b0c9554cd0048e6e7341d7b92f29d400dbc6a5a4fc2f86dbed881d32e02ece9b55bc520387bae2eac22a5ab38a0b205c29b52b181294d99b4dd75e27309b548 + checksum: 10/3483f329bb099b438d05e5e206229ddbc1703972a69ba0240a796b5477369930b0ab2e7f6c9539ecad2cea8b0c08fa65498778b92cf87ad3d156f613de1fd2fa languageName: node linkType: hard @@ -570,25 +586,25 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-import-assertions@npm:^7.22.5, @babel/plugin-syntax-import-assertions@npm:^7.23.3": - version: 7.23.3 - resolution: "@babel/plugin-syntax-import-assertions@npm:7.23.3" +"@babel/plugin-syntax-import-assertions@npm:^7.22.5, @babel/plugin-syntax-import-assertions@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-syntax-import-assertions@npm:7.24.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-plugin-utils": "npm:^7.24.0" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/883e6b35b2da205138caab832d54505271a3fee3fc1e8dc0894502434fc2b5d517cbe93bbfbfef8068a0fb6ec48ebc9eef3f605200a489065ba43d8cddc1c9a7 + checksum: 10/2a463928a63b62052e9fb8f8b0018aa11a926e94f32c168260ae012afe864875c6176c6eb361e13f300542c31316dad791b08a5b8ed92436a3095c7a0e4fce65 languageName: node linkType: hard -"@babel/plugin-syntax-import-attributes@npm:^7.22.5, @babel/plugin-syntax-import-attributes@npm:^7.23.3": - version: 7.23.3 - resolution: "@babel/plugin-syntax-import-attributes@npm:7.23.3" +"@babel/plugin-syntax-import-attributes@npm:^7.22.5, @babel/plugin-syntax-import-attributes@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-syntax-import-attributes@npm:7.24.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-plugin-utils": "npm:^7.24.0" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/9aed7661ffb920ca75df9f494757466ca92744e43072e0848d87fa4aa61a3f2ee5a22198ac1959856c036434b5614a8f46f1fb70298835dbe28220cdd1d4c11e + checksum: 10/87c8aa4a5ef931313f956871b27f2c051556f627b97ed21e9a5890ca4906b222d89062a956cde459816f5e0dec185ff128d7243d3fdc389504522acb88f0464e languageName: node linkType: hard @@ -614,14 +630,14 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-jsx@npm:^7.22.5, @babel/plugin-syntax-jsx@npm:^7.7.2": - version: 7.22.5 - resolution: "@babel/plugin-syntax-jsx@npm:7.22.5" +"@babel/plugin-syntax-jsx@npm:^7.22.5, @babel/plugin-syntax-jsx@npm:^7.23.3, @babel/plugin-syntax-jsx@npm:^7.7.2": + version: 7.24.1 + resolution: "@babel/plugin-syntax-jsx@npm:7.24.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-plugin-utils": "npm:^7.24.0" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/8829d30c2617ab31393d99cec2978e41f014f4ac6f01a1cecf4c4dd8320c3ec12fdc3ce121126b2d8d32f6887e99ca1a0bad53dedb1e6ad165640b92b24980ce + checksum: 10/712f7e7918cb679f106769f57cfab0bc99b311032665c428b98f4c3e2e6d567601d45386a4f246df6a80d741e1f94192b3f008800d66c4f1daae3ad825c243f0 languageName: node linkType: hard @@ -736,188 +752,188 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-arrow-functions@npm:^7.22.5, @babel/plugin-transform-arrow-functions@npm:^7.23.3": - version: 7.23.3 - resolution: "@babel/plugin-transform-arrow-functions@npm:7.23.3" +"@babel/plugin-transform-arrow-functions@npm:^7.22.5, @babel/plugin-transform-arrow-functions@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-arrow-functions@npm:7.24.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-plugin-utils": "npm:^7.24.0" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/1e99118176e5366c2636064d09477016ab5272b2a92e78b8edb571d20bc3eaa881789a905b20042942c3c2d04efc530726cf703f937226db5ebc495f5d067e66 + checksum: 10/58f9aa9b0de8382f8cfa3f1f1d40b69d98cd2f52340e2391733d0af745fdddda650ba392e509bc056157c880a2f52834a38ab2c5aa5569af8c61bb6ecbf45f34 languageName: node linkType: hard -"@babel/plugin-transform-async-generator-functions@npm:^7.23.2, @babel/plugin-transform-async-generator-functions@npm:^7.23.9": - version: 7.23.9 - resolution: "@babel/plugin-transform-async-generator-functions@npm:7.23.9" +"@babel/plugin-transform-async-generator-functions@npm:^7.23.2, @babel/plugin-transform-async-generator-functions@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-async-generator-functions@npm:7.24.1" dependencies: "@babel/helper-environment-visitor": "npm:^7.22.20" - "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-plugin-utils": "npm:^7.24.0" "@babel/helper-remap-async-to-generator": "npm:^7.22.20" "@babel/plugin-syntax-async-generators": "npm:^7.8.4" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/d402494087a6b803803eb5ab46b837aab100a04c4c5148e38bfa943ea1bbfc1ecfb340f1ced68972564312d3580f550c125f452372e77607a558fbbaf98c31c0 + checksum: 10/d4d31965395e3da8d060912ae85efc3dc7d6de5dba9919230b507a373cb51df4aa515fcc130b0c838aa77fe9bc4ede34f14f925570a6a280185988984f92dd51 languageName: node linkType: hard -"@babel/plugin-transform-async-to-generator@npm:^7.22.5, @babel/plugin-transform-async-to-generator@npm:^7.23.3": - version: 7.23.3 - resolution: "@babel/plugin-transform-async-to-generator@npm:7.23.3" +"@babel/plugin-transform-async-to-generator@npm:^7.22.5, @babel/plugin-transform-async-to-generator@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-async-to-generator@npm:7.24.1" dependencies: - "@babel/helper-module-imports": "npm:^7.22.15" - "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-module-imports": "npm:^7.24.1" + "@babel/helper-plugin-utils": "npm:^7.24.0" "@babel/helper-remap-async-to-generator": "npm:^7.22.20" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/2e9d9795d4b3b3d8090332104e37061c677f29a1ce65bcbda4099a32d243e5d9520270a44bbabf0fb1fb40d463bd937685b1a1042e646979086c546d55319c3c + checksum: 10/429004a6596aa5c9e707b604156f49a146f8d029e31a3152b1649c0b56425264fda5fd38e5db1ddaeb33c3fe45c97dc8078d7abfafe3542a979b49f229801135 languageName: node linkType: hard -"@babel/plugin-transform-block-scoped-functions@npm:^7.22.5, @babel/plugin-transform-block-scoped-functions@npm:^7.23.3": - version: 7.23.3 - resolution: "@babel/plugin-transform-block-scoped-functions@npm:7.23.3" +"@babel/plugin-transform-block-scoped-functions@npm:^7.22.5, @babel/plugin-transform-block-scoped-functions@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-block-scoped-functions@npm:7.24.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-plugin-utils": "npm:^7.24.0" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/e63b16d94ee5f4d917e669da3db5ea53d1e7e79141a2ec873c1e644678cdafe98daa556d0d359963c827863d6b3665d23d4938a94a4c5053a1619c4ebd01d020 + checksum: 10/d8e18bd57b156da1cd4d3c1780ab9ea03afed56c6824ca8e6e74f67959d7989a0e953ec370fe9b417759314f2eef30c8c437395ce63ada2e26c2f469e4704f82 languageName: node linkType: hard -"@babel/plugin-transform-block-scoping@npm:^7.23.0, @babel/plugin-transform-block-scoping@npm:^7.23.4": - version: 7.23.4 - resolution: "@babel/plugin-transform-block-scoping@npm:7.23.4" +"@babel/plugin-transform-block-scoping@npm:^7.23.0, @babel/plugin-transform-block-scoping@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-block-scoping@npm:7.24.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-plugin-utils": "npm:^7.24.0" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/bbb965a3acdfb03559806d149efbd194ac9c983b260581a60efcb15eb9fbe20e3054667970800146d867446db1c1398f8e4ee87f4454233e49b8f8ce947bd99b + checksum: 10/443069c6410079c007c40425254a5d0416e4fefe38c1cb354884694a3029dfa6ea8c196398726d2bd4ec3e5c4559ef85efc1ad0b068f1330df4aa03b414781e0 languageName: node linkType: hard -"@babel/plugin-transform-class-properties@npm:^7.22.5, @babel/plugin-transform-class-properties@npm:^7.23.3": - version: 7.23.3 - resolution: "@babel/plugin-transform-class-properties@npm:7.23.3" +"@babel/plugin-transform-class-properties@npm:^7.22.5, @babel/plugin-transform-class-properties@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-class-properties@npm:7.24.1" dependencies: - "@babel/helper-create-class-features-plugin": "npm:^7.22.15" - "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-create-class-features-plugin": "npm:^7.24.1" + "@babel/helper-plugin-utils": "npm:^7.24.0" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/9c6f8366f667897541d360246de176dd29efc7a13d80a5b48361882f7173d9173be4646c3b7d9b003ccc0e01e25df122330308f33db921fa553aa17ad544b3fc + checksum: 10/95779e9eef0c0638b9631c297d48aee53ffdbb2b1b5221bf40d7eccd566a8e34f859ff3571f8f20b9159b67f1bff7d7dc81da191c15d69fbae5a645197eae7e0 languageName: node linkType: hard -"@babel/plugin-transform-class-static-block@npm:^7.22.11, @babel/plugin-transform-class-static-block@npm:^7.23.4": - version: 7.23.4 - resolution: "@babel/plugin-transform-class-static-block@npm:7.23.4" +"@babel/plugin-transform-class-static-block@npm:^7.22.11, @babel/plugin-transform-class-static-block@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-class-static-block@npm:7.24.1" dependencies: - "@babel/helper-create-class-features-plugin": "npm:^7.22.15" - "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-create-class-features-plugin": "npm:^7.24.1" + "@babel/helper-plugin-utils": "npm:^7.24.0" "@babel/plugin-syntax-class-static-block": "npm:^7.14.5" peerDependencies: "@babel/core": ^7.12.0 - checksum: 10/c8bfaba19a674fc2eb54edad71e958647360474e3163e8226f1acd63e4e2dbec32a171a0af596c1dc5359aee402cc120fea7abd1fb0e0354b6527f0fc9e8aa1e + checksum: 10/253c627c11d9df79e3b32e78bfa1fe0dd1f91c3579da52bf73f76c83de53b140dcb1c9cc5f4c65ff1505754a01b59bc83987c35bcc8f89492b63dae46adef78f languageName: node linkType: hard -"@babel/plugin-transform-classes@npm:^7.22.15, @babel/plugin-transform-classes@npm:^7.23.8": - version: 7.23.8 - resolution: "@babel/plugin-transform-classes@npm:7.23.8" +"@babel/plugin-transform-classes@npm:^7.22.15, @babel/plugin-transform-classes@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-classes@npm:7.24.1" dependencies: "@babel/helper-annotate-as-pure": "npm:^7.22.5" "@babel/helper-compilation-targets": "npm:^7.23.6" "@babel/helper-environment-visitor": "npm:^7.22.20" "@babel/helper-function-name": "npm:^7.23.0" - "@babel/helper-plugin-utils": "npm:^7.22.5" - "@babel/helper-replace-supers": "npm:^7.22.20" + "@babel/helper-plugin-utils": "npm:^7.24.0" + "@babel/helper-replace-supers": "npm:^7.24.1" "@babel/helper-split-export-declaration": "npm:^7.22.6" globals: "npm:^11.1.0" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/4bb4b19e7a39871c4414fb44fc5f2cc47c78f993b74c43238dfb99c9dac2d15cb99b43f8a3d42747580e1807d2b8f5e13ce7e95e593fd839bd176aa090bf9a23 + checksum: 10/eb7f4a3d852cfa20f4efd299929c564bd2b45106ac1cf4ac8b0c87baf078d4a15c39b8a21bbb01879c1922acb9baaf3c9b150486e18d84b30129e9671639793d languageName: node linkType: hard -"@babel/plugin-transform-computed-properties@npm:^7.22.5, @babel/plugin-transform-computed-properties@npm:^7.23.3": - version: 7.23.3 - resolution: "@babel/plugin-transform-computed-properties@npm:7.23.3" +"@babel/plugin-transform-computed-properties@npm:^7.22.5, @babel/plugin-transform-computed-properties@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-computed-properties@npm:7.24.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.22.5" - "@babel/template": "npm:^7.22.15" + "@babel/helper-plugin-utils": "npm:^7.24.0" + "@babel/template": "npm:^7.24.0" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/e75593e02c5ea473c17839e3c9d597ce3697bf039b66afe9a4d06d086a87fb3d95850b4174476897afc351dc1b46a9ec3165ee6e8fbad3732c0d65f676f855ad + checksum: 10/62bbfe1bd508517d96ba6909e68b1adb9dfd24ea61af1f4b0aa909bfc5e476044afe9c55b10ef74508fd147aa665e818df67ece834d164a9fd69b80c9ede3875 languageName: node linkType: hard -"@babel/plugin-transform-destructuring@npm:^7.23.0, @babel/plugin-transform-destructuring@npm:^7.23.3": - version: 7.23.3 - resolution: "@babel/plugin-transform-destructuring@npm:7.23.3" +"@babel/plugin-transform-destructuring@npm:^7.23.0, @babel/plugin-transform-destructuring@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-destructuring@npm:7.24.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-plugin-utils": "npm:^7.24.0" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/5abd93718af5a61f8f6a97d2ccac9139499752dd5b2c533d7556fb02947ae01b2f51d4c4f5e64df569e8783d3743270018eb1fa979c43edec7dd1377acf107ed + checksum: 10/03d9a81cd9eeb24d48e207be536d460d6ad228238ac70da9b7ad4bae799847bb3be0aecfa4ea6223752f3a8d4ada3a58cd9a0f8fc70c01fdfc87ad0618f897d3 languageName: node linkType: hard -"@babel/plugin-transform-dotall-regex@npm:^7.22.5, @babel/plugin-transform-dotall-regex@npm:^7.23.3": - version: 7.23.3 - resolution: "@babel/plugin-transform-dotall-regex@npm:7.23.3" +"@babel/plugin-transform-dotall-regex@npm:^7.22.5, @babel/plugin-transform-dotall-regex@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-dotall-regex@npm:7.24.1" dependencies: "@babel/helper-create-regexp-features-plugin": "npm:^7.22.15" - "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-plugin-utils": "npm:^7.24.0" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/a2dbbf7f1ea16a97948c37df925cb364337668c41a3948b8d91453f140507bd8a3429030c7ce66d09c299987b27746c19a2dd18b6f17dcb474854b14fd9159a3 + checksum: 10/7f623d25b6f213b94ebc1754e9e31c1077c8e288626d8b7bfa76a97b067ce80ddcd0ede402a546706c65002c0ccf45cd5ec621511c2668eed31ebcabe8391d35 languageName: node linkType: hard -"@babel/plugin-transform-duplicate-keys@npm:^7.22.5, @babel/plugin-transform-duplicate-keys@npm:^7.23.3": - version: 7.23.3 - resolution: "@babel/plugin-transform-duplicate-keys@npm:7.23.3" +"@babel/plugin-transform-duplicate-keys@npm:^7.22.5, @babel/plugin-transform-duplicate-keys@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-duplicate-keys@npm:7.24.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-plugin-utils": "npm:^7.24.0" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/c2a21c34dc0839590cd945192cbc46fde541a27e140c48fe1808315934664cdbf18db64889e23c4eeb6bad9d3e049482efdca91d29de5734ffc887c4fbabaa16 + checksum: 10/de600a958ad146fc8aca71fd2dfa5ebcfdb97df4eaa530fc9a4b0d28d85442ddb9b7039f260b396785211e88c6817125a94c183459763c363847e8c84f318ff0 languageName: node linkType: hard -"@babel/plugin-transform-dynamic-import@npm:^7.22.11, @babel/plugin-transform-dynamic-import@npm:^7.23.4": - version: 7.23.4 - resolution: "@babel/plugin-transform-dynamic-import@npm:7.23.4" +"@babel/plugin-transform-dynamic-import@npm:^7.22.11, @babel/plugin-transform-dynamic-import@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-dynamic-import@npm:7.24.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-plugin-utils": "npm:^7.24.0" "@babel/plugin-syntax-dynamic-import": "npm:^7.8.3" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/57a722604c430d9f3dacff22001a5f31250e34785d4969527a2ae9160fa86858d0892c5b9ff7a06a04076f8c76c9e6862e0541aadca9c057849961343aab0845 + checksum: 10/59fc561ee40b1a69f969c12c6c5fac206226d6642213985a569dd0f99f8e41c0f4eaedebd36936c255444a8335079842274c42a975a433beadb436d4c5abb79b languageName: node linkType: hard -"@babel/plugin-transform-exponentiation-operator@npm:^7.22.5, @babel/plugin-transform-exponentiation-operator@npm:^7.23.3": - version: 7.23.3 - resolution: "@babel/plugin-transform-exponentiation-operator@npm:7.23.3" +"@babel/plugin-transform-exponentiation-operator@npm:^7.22.5, @babel/plugin-transform-exponentiation-operator@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-exponentiation-operator@npm:7.24.1" dependencies: "@babel/helper-builder-binary-assignment-operator-visitor": "npm:^7.22.15" - "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-plugin-utils": "npm:^7.24.0" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/00d05ab14ad0f299160fcf9d8f55a1cc1b740e012ab0b5ce30207d2365f091665115557af7d989cd6260d075a252d9e4283de5f2b247dfbbe0e42ae586e6bf66 + checksum: 10/f90841fe1a1e9f680b4209121d3e2992f923e85efcd322b26e5901c180ef44ff727fb89790803a23fac49af34c1ce2e480018027c22b4573b615512ac5b6fc50 languageName: node linkType: hard -"@babel/plugin-transform-export-namespace-from@npm:^7.22.11, @babel/plugin-transform-export-namespace-from@npm:^7.23.4": - version: 7.23.4 - resolution: "@babel/plugin-transform-export-namespace-from@npm:7.23.4" +"@babel/plugin-transform-export-namespace-from@npm:^7.22.11, @babel/plugin-transform-export-namespace-from@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-export-namespace-from@npm:7.24.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-plugin-utils": "npm:^7.24.0" "@babel/plugin-syntax-export-namespace-from": "npm:^7.8.3" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/9f770a81bfd03b48d6ba155d452946fd56d6ffe5b7d871e9ec2a0b15e0f424273b632f3ed61838b90015b25bbda988896b7a46c7d964fbf8f6feb5820b309f93 + checksum: 10/bc710ac231919df9555331885748385c11c5e695d7271824fe56fba51dd637d48d3e5cd52e1c69f2b1a384fbbb41552572bc1ca3a2285ee29571f002e9bb2421 languageName: node linkType: hard @@ -933,125 +949,125 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-for-of@npm:^7.22.15, @babel/plugin-transform-for-of@npm:^7.23.6": - version: 7.23.6 - resolution: "@babel/plugin-transform-for-of@npm:7.23.6" +"@babel/plugin-transform-for-of@npm:^7.22.15, @babel/plugin-transform-for-of@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-for-of@npm:7.24.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-plugin-utils": "npm:^7.24.0" "@babel/helper-skip-transparent-expression-wrappers": "npm:^7.22.5" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/b84ef1f26a2db316237ae6d10fa7c22c70ac808ed0b8e095a8ecf9101551636cbb026bee9fb95a0a7944f3b8278ff9636a9088cb4a4ac5b84830a13829242735 + checksum: 10/befd0908c3f6b31f9fa9363a3c112d25eaa0bc4a79cfad1f0a8bb5010937188b043a44fb23443bc8ffbcc40c015bb25f80e4cc585ce5cc580708e2d56e76fe37 languageName: node linkType: hard -"@babel/plugin-transform-function-name@npm:^7.22.5, @babel/plugin-transform-function-name@npm:^7.23.3": - version: 7.23.3 - resolution: "@babel/plugin-transform-function-name@npm:7.23.3" +"@babel/plugin-transform-function-name@npm:^7.22.5, @babel/plugin-transform-function-name@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-function-name@npm:7.24.1" dependencies: - "@babel/helper-compilation-targets": "npm:^7.22.15" + "@babel/helper-compilation-targets": "npm:^7.23.6" "@babel/helper-function-name": "npm:^7.23.0" - "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-plugin-utils": "npm:^7.24.0" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/355c6dbe07c919575ad42b2f7e020f320866d72f8b79181a16f8e0cd424a2c761d979f03f47d583d9471b55dcd68a8a9d829b58e1eebcd572145b934b48975a6 + checksum: 10/31eb3c75297dda7265f78eba627c446f2324e30ec0124a645ccc3e9f341254aaa40d6787bd62b2280d77c0a5c9fbfce1da2c200ef7c7f8e0a1b16a8eb3644c6f languageName: node linkType: hard -"@babel/plugin-transform-json-strings@npm:^7.22.11, @babel/plugin-transform-json-strings@npm:^7.23.4": - version: 7.23.4 - resolution: "@babel/plugin-transform-json-strings@npm:7.23.4" +"@babel/plugin-transform-json-strings@npm:^7.22.11, @babel/plugin-transform-json-strings@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-json-strings@npm:7.24.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-plugin-utils": "npm:^7.24.0" "@babel/plugin-syntax-json-strings": "npm:^7.8.3" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/f9019820233cf8955d8ba346df709a0683c120fe86a24ed1c9f003f2db51197b979efc88f010d558a12e1491210fc195a43cd1c7fee5e23b92da38f793a875de + checksum: 10/f42302d42fc81ac00d14e9e5d80405eb80477d7f9039d7208e712d6bcd486a4e3b32fdfa07b5f027d6c773723d8168193ee880f93b0e430c828e45f104fb82a4 languageName: node linkType: hard -"@babel/plugin-transform-literals@npm:^7.22.5, @babel/plugin-transform-literals@npm:^7.23.3": - version: 7.23.3 - resolution: "@babel/plugin-transform-literals@npm:7.23.3" +"@babel/plugin-transform-literals@npm:^7.22.5, @babel/plugin-transform-literals@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-literals@npm:7.24.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-plugin-utils": "npm:^7.24.0" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/519a544cd58586b9001c4c9b18da25a62f17d23c48600ff7a685d75ca9eb18d2c5e8f5476f067f0a8f1fea2a31107eff950b9864833061e6076dcc4bdc3e71ed + checksum: 10/2df94e9478571852483aca7588419e574d76bde97583e78551c286f498e01321e7dbb1d0ef67bee16e8f950688f79688809cfde370c5c4b84c14d841a3ef217a languageName: node linkType: hard -"@babel/plugin-transform-logical-assignment-operators@npm:^7.22.11, @babel/plugin-transform-logical-assignment-operators@npm:^7.23.4": - version: 7.23.4 - resolution: "@babel/plugin-transform-logical-assignment-operators@npm:7.23.4" +"@babel/plugin-transform-logical-assignment-operators@npm:^7.22.11, @babel/plugin-transform-logical-assignment-operators@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-logical-assignment-operators@npm:7.24.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-plugin-utils": "npm:^7.24.0" "@babel/plugin-syntax-logical-assignment-operators": "npm:^7.10.4" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/2ae1dc9b4ff3bf61a990ff3accdecb2afe3a0ca649b3e74c010078d1cdf29ea490f50ac0a905306a2bcf9ac177889a39ac79bdcc3a0fdf220b3b75fac18d39b5 + checksum: 10/895f2290adf457cbf327428bdb4fb90882a38a22f729bcf0629e8ad66b9b616d2721fbef488ac00411b647489d1dda1d20171bb3772d0796bb7ef5ecf057808a languageName: node linkType: hard -"@babel/plugin-transform-member-expression-literals@npm:^7.22.5, @babel/plugin-transform-member-expression-literals@npm:^7.23.3": - version: 7.23.3 - resolution: "@babel/plugin-transform-member-expression-literals@npm:7.23.3" +"@babel/plugin-transform-member-expression-literals@npm:^7.22.5, @babel/plugin-transform-member-expression-literals@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-member-expression-literals@npm:7.24.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-plugin-utils": "npm:^7.24.0" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/95cec13c36d447c5aa6b8e4c778b897eeba66dcb675edef01e0d2afcec9e8cb9726baf4f81b4bbae7a782595aed72e6a0d44ffb773272c3ca180fada99bf92db + checksum: 10/4ea641cc14a615f9084e45ad2319f95e2fee01c77ec9789685e7e11a6c286238a426a98f9c1ed91568a047d8ac834393e06e8c82d1ff01764b7aa61bee8e9023 languageName: node linkType: hard -"@babel/plugin-transform-modules-amd@npm:^7.23.0, @babel/plugin-transform-modules-amd@npm:^7.23.3": - version: 7.23.3 - resolution: "@babel/plugin-transform-modules-amd@npm:7.23.3" +"@babel/plugin-transform-modules-amd@npm:^7.23.0, @babel/plugin-transform-modules-amd@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-modules-amd@npm:7.24.1" dependencies: "@babel/helper-module-transforms": "npm:^7.23.3" - "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-plugin-utils": "npm:^7.24.0" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/48c87dee2c7dae8ed40d16901f32c9e58be4ef87bf2c3985b51dd2e78e82081f3bad0a39ee5cf6e8909e13e954e2b4bedef0a8141922f281ed833ddb59ed9be2 + checksum: 10/5a324f7c630cf0be1f09098a3a36248c2521622f2c7ea1a44a5980f54b718f5e0dd4af92a337f4b445a8824c8d533853ebea7c16de829b8a7bc8bcca127d4d73 languageName: node linkType: hard -"@babel/plugin-transform-modules-commonjs@npm:^7.13.8, @babel/plugin-transform-modules-commonjs@npm:^7.22.5, @babel/plugin-transform-modules-commonjs@npm:^7.23.0, @babel/plugin-transform-modules-commonjs@npm:^7.23.3": - version: 7.23.3 - resolution: "@babel/plugin-transform-modules-commonjs@npm:7.23.3" +"@babel/plugin-transform-modules-commonjs@npm:^7.13.8, @babel/plugin-transform-modules-commonjs@npm:^7.22.5, @babel/plugin-transform-modules-commonjs@npm:^7.23.0, @babel/plugin-transform-modules-commonjs@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-modules-commonjs@npm:7.24.1" dependencies: "@babel/helper-module-transforms": "npm:^7.23.3" - "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-plugin-utils": "npm:^7.24.0" "@babel/helper-simple-access": "npm:^7.22.5" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/a3bc082d0dfe8327a29263a6d721cea608d440bc8141ba3ec6ba80ad73d84e4f9bbe903c27e9291c29878feec9b5dee2bd0563822f93dc951f5d7fc36bdfe85b + checksum: 10/7326a62ed5f766f93ee75684868635b59884e2801533207ea11561c296de53037949fecad4055d828fa7ebeb6cc9e55908aa3e7c13f930ded3e62ad9f24680d7 languageName: node linkType: hard -"@babel/plugin-transform-modules-systemjs@npm:^7.23.0, @babel/plugin-transform-modules-systemjs@npm:^7.23.9": - version: 7.23.9 - resolution: "@babel/plugin-transform-modules-systemjs@npm:7.23.9" +"@babel/plugin-transform-modules-systemjs@npm:^7.23.0, @babel/plugin-transform-modules-systemjs@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-modules-systemjs@npm:7.24.1" dependencies: "@babel/helper-hoist-variables": "npm:^7.22.5" "@babel/helper-module-transforms": "npm:^7.23.3" - "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-plugin-utils": "npm:^7.24.0" "@babel/helper-validator-identifier": "npm:^7.22.20" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/4bb800e5a9d0d668d7421ae3672fccff7d5f2a36621fd87414d7ece6d6f4d93627f9644cfecacae934bc65ffc131c8374242aaa400cca874dcab9b281a21aff0 + checksum: 10/565ec4518037b3d957431e29bda97b3d2fbb2e245fb5ba19889310ccb8fb71353e8ce2c325cc8d3fbc5a376d3af7d7e21782d5f502c46f8da077bee7807a590f languageName: node linkType: hard -"@babel/plugin-transform-modules-umd@npm:^7.22.5, @babel/plugin-transform-modules-umd@npm:^7.23.3": - version: 7.23.3 - resolution: "@babel/plugin-transform-modules-umd@npm:7.23.3" +"@babel/plugin-transform-modules-umd@npm:^7.22.5, @babel/plugin-transform-modules-umd@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-modules-umd@npm:7.24.1" dependencies: "@babel/helper-module-transforms": "npm:^7.23.3" - "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-plugin-utils": "npm:^7.24.0" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/e3f3af83562d687899555c7826b3faf0ab93ee7976898995b1d20cbe7f4451c55e05b0e17bfb3e549937cbe7573daf5400b752912a241b0a8a64d2457c7626e5 + checksum: 10/323bb9367e1967117a829f67788ec2ff55504b4faf8f6d83ec85d398e50b41cf7d1c375c67d63883dd7ad5e75b35c8ae776d89e422330ec0c0a1fda24e362083 languageName: node linkType: hard @@ -1067,149 +1083,148 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-new-target@npm:^7.22.5, @babel/plugin-transform-new-target@npm:^7.23.3": - version: 7.23.3 - resolution: "@babel/plugin-transform-new-target@npm:7.23.3" +"@babel/plugin-transform-new-target@npm:^7.22.5, @babel/plugin-transform-new-target@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-new-target@npm:7.24.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-plugin-utils": "npm:^7.24.0" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/e5053389316fce73ad5201b7777437164f333e24787fbcda4ae489cd2580dbbbdfb5694a7237bad91fabb46b591d771975d69beb1c740b82cb4761625379f00b + checksum: 10/e0d3af66cd0fad29c9d0e3fc65e711255e18b77e2e35bbd8f10059e3db7de6c16799ef74e704daf784950feb71e7a93c5bf2c771d98f1ca3fba1ff2e0240b24a languageName: node linkType: hard -"@babel/plugin-transform-nullish-coalescing-operator@npm:^7.22.11, @babel/plugin-transform-nullish-coalescing-operator@npm:^7.23.4": - version: 7.23.4 - resolution: "@babel/plugin-transform-nullish-coalescing-operator@npm:7.23.4" +"@babel/plugin-transform-nullish-coalescing-operator@npm:^7.22.11, @babel/plugin-transform-nullish-coalescing-operator@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-nullish-coalescing-operator@npm:7.24.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-plugin-utils": "npm:^7.24.0" "@babel/plugin-syntax-nullish-coalescing-operator": "npm:^7.8.3" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/a27d73ea134d3d9560a6b2e26ab60012fba15f1db95865aa0153c18f5ec82cfef6a7b3d8df74e3c2fca81534fa5efeb6cacaf7b08bdb7d123e3dafdd079886a3 + checksum: 10/74025e191ceb7cefc619c15d33753aab81300a03d81b96ae249d9b599bc65878f962d608f452462d3aad5d6e334b7ab2b09a6bdcfe8d101fe77ac7aacca4261e languageName: node linkType: hard -"@babel/plugin-transform-numeric-separator@npm:^7.22.11, @babel/plugin-transform-numeric-separator@npm:^7.23.4": - version: 7.23.4 - resolution: "@babel/plugin-transform-numeric-separator@npm:7.23.4" +"@babel/plugin-transform-numeric-separator@npm:^7.22.11, @babel/plugin-transform-numeric-separator@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-numeric-separator@npm:7.24.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-plugin-utils": "npm:^7.24.0" "@babel/plugin-syntax-numeric-separator": "npm:^7.10.4" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/6ba0e5db3c620a3ec81f9e94507c821f483c15f196868df13fa454cbac719a5449baf73840f5b6eb7d77311b24a2cf8e45db53700d41727f693d46f7caf3eec3 + checksum: 10/3247bd7d409574fc06c59e0eb573ae7470d6d61ecf780df40b550102bb4406747d8f39dcbec57eb59406df6c565a86edd3b429e396ad02e4ce201ad92050832e languageName: node linkType: hard -"@babel/plugin-transform-object-rest-spread@npm:^7.22.15, @babel/plugin-transform-object-rest-spread@npm:^7.24.0": - version: 7.24.0 - resolution: "@babel/plugin-transform-object-rest-spread@npm:7.24.0" +"@babel/plugin-transform-object-rest-spread@npm:^7.22.15, @babel/plugin-transform-object-rest-spread@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-object-rest-spread@npm:7.24.1" dependencies: - "@babel/compat-data": "npm:^7.23.5" "@babel/helper-compilation-targets": "npm:^7.23.6" "@babel/helper-plugin-utils": "npm:^7.24.0" "@babel/plugin-syntax-object-rest-spread": "npm:^7.8.3" - "@babel/plugin-transform-parameters": "npm:^7.23.3" + "@babel/plugin-transform-parameters": "npm:^7.24.1" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/1dfafd9461723769b29f724fcbdca974c4280f68a9e03c8ff412643ffe88930755f093f9cbf919cdb6d0d53751614892dd2882bccad286e14e9e995c5a8242ed + checksum: 10/ff6eeefbc5497cf33d62dc86b797c6db0e9455d6a4945d6952f3b703d04baab048974c6573b503e0ec097b8112d3b98b5f4ee516e1b8a74ed47aebba4d9d2643 languageName: node linkType: hard -"@babel/plugin-transform-object-super@npm:^7.22.5, @babel/plugin-transform-object-super@npm:^7.23.3": - version: 7.23.3 - resolution: "@babel/plugin-transform-object-super@npm:7.23.3" +"@babel/plugin-transform-object-super@npm:^7.22.5, @babel/plugin-transform-object-super@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-object-super@npm:7.24.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.22.5" - "@babel/helper-replace-supers": "npm:^7.22.20" + "@babel/helper-plugin-utils": "npm:^7.24.0" + "@babel/helper-replace-supers": "npm:^7.24.1" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/e495497186f621fa79026e183b4f1fbb172fd9df812cbd2d7f02c05b08adbe58012b1a6eb6dd58d11a30343f6ec80d0f4074f9b501d70aa1c94df76d59164c53 + checksum: 10/d34d437456a54e2a5dcb26e9cf09ed4c55528f2a327c5edca92c93e9483c37176e228d00d6e0cf767f3d6fdbef45ae3a5d034a7c59337a009e20ae541c8220fa languageName: node linkType: hard -"@babel/plugin-transform-optional-catch-binding@npm:^7.22.11, @babel/plugin-transform-optional-catch-binding@npm:^7.23.4": - version: 7.23.4 - resolution: "@babel/plugin-transform-optional-catch-binding@npm:7.23.4" +"@babel/plugin-transform-optional-catch-binding@npm:^7.22.11, @babel/plugin-transform-optional-catch-binding@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-optional-catch-binding@npm:7.24.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-plugin-utils": "npm:^7.24.0" "@babel/plugin-syntax-optional-catch-binding": "npm:^7.8.3" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/d50b5ee142cdb088d8b5de1ccf7cea85b18b85d85b52f86618f6e45226372f01ad4cdb29abd4fd35ea99a71fefb37009e0107db7a787dcc21d4d402f97470faf + checksum: 10/ff7c02449d32a6de41e003abb38537b4a1ad90b1eaa4c0b578cb1b55548201a677588a8c47f3e161c72738400ae811a6673ea7b8a734344755016ca0ac445dac languageName: node linkType: hard -"@babel/plugin-transform-optional-chaining@npm:^7.23.0, @babel/plugin-transform-optional-chaining@npm:^7.23.3, @babel/plugin-transform-optional-chaining@npm:^7.23.4": - version: 7.23.4 - resolution: "@babel/plugin-transform-optional-chaining@npm:7.23.4" +"@babel/plugin-transform-optional-chaining@npm:^7.23.0, @babel/plugin-transform-optional-chaining@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-optional-chaining@npm:7.24.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-plugin-utils": "npm:^7.24.0" "@babel/helper-skip-transparent-expression-wrappers": "npm:^7.22.5" "@babel/plugin-syntax-optional-chaining": "npm:^7.8.3" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/0ef24e889d6151428953fc443af5f71f4dae73f373dc1b7f5dd3f6a61d511296eb77e9b870e8c2c02a933e3455ae24c1fa91738c826b72a4ff87e0337db527e8 + checksum: 10/d41031b8e472b9b30aacd905a1561904bcec597dd888ad639b234971714dc9cd0dcb60df91a89219fc72e4feeb148e20f97bcddc39d7676e743ff0c23f62a7eb languageName: node linkType: hard -"@babel/plugin-transform-parameters@npm:^7.22.15, @babel/plugin-transform-parameters@npm:^7.23.3": - version: 7.23.3 - resolution: "@babel/plugin-transform-parameters@npm:7.23.3" +"@babel/plugin-transform-parameters@npm:^7.22.15, @babel/plugin-transform-parameters@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-parameters@npm:7.24.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-plugin-utils": "npm:^7.24.0" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/a8c36c3fc25f9daa46c4f6db47ea809c395dc4abc7f01c4b1391f6e5b0cd62b83b6016728b02a6a8ac21aca56207c9ec66daefc0336e9340976978de7e6e28df + checksum: 10/c289c188710cd1c60991db169d8173b6e8e05624ae61a7da0b64354100bfba9e44bc1332dd9223c4e3fe1b9cbc0c061e76e7c7b3a75c9588bf35d0ffec428070 languageName: node linkType: hard -"@babel/plugin-transform-private-methods@npm:^7.22.5, @babel/plugin-transform-private-methods@npm:^7.23.3": - version: 7.23.3 - resolution: "@babel/plugin-transform-private-methods@npm:7.23.3" +"@babel/plugin-transform-private-methods@npm:^7.22.5, @babel/plugin-transform-private-methods@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-private-methods@npm:7.24.1" dependencies: - "@babel/helper-create-class-features-plugin": "npm:^7.22.15" - "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-create-class-features-plugin": "npm:^7.24.1" + "@babel/helper-plugin-utils": "npm:^7.24.0" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/cedc1285c49b5a6d9a3d0e5e413b756ac40b3ac2f8f68bdfc3ae268bc8d27b00abd8bb0861c72756ff5dd8bf1eb77211b7feb5baf4fdae2ebbaabe49b9adc1d0 + checksum: 10/7208c30bb3f3fbc73fb3a88bdcb78cd5cddaf6d523eb9d67c0c04e78f6fc6319ece89f4a5abc41777ceab16df55b3a13a4120e0efc9275ca6d2d89beaba80aa0 languageName: node linkType: hard -"@babel/plugin-transform-private-property-in-object@npm:^7.22.11, @babel/plugin-transform-private-property-in-object@npm:^7.23.4": - version: 7.23.4 - resolution: "@babel/plugin-transform-private-property-in-object@npm:7.23.4" +"@babel/plugin-transform-private-property-in-object@npm:^7.22.11, @babel/plugin-transform-private-property-in-object@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-private-property-in-object@npm:7.24.1" dependencies: "@babel/helper-annotate-as-pure": "npm:^7.22.5" - "@babel/helper-create-class-features-plugin": "npm:^7.22.15" - "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-create-class-features-plugin": "npm:^7.24.1" + "@babel/helper-plugin-utils": "npm:^7.24.0" "@babel/plugin-syntax-private-property-in-object": "npm:^7.14.5" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/02eef2ee98fa86ee5052ed9bf0742d6d22b510b5df2fcce0b0f5615d6001f7786c6b31505e7f1c2f446406d8fb33603a5316d957cfa5b8365cbf78ddcc24fa42 + checksum: 10/466d1943960c2475c0361eba2ea72d504d4d8329a8e293af0eedd26887bf30a074515b330ea84be77331ace77efbf5533d5f04f8cff63428d2615f4a509ae7a4 languageName: node linkType: hard -"@babel/plugin-transform-property-literals@npm:^7.22.5, @babel/plugin-transform-property-literals@npm:^7.23.3": - version: 7.23.3 - resolution: "@babel/plugin-transform-property-literals@npm:7.23.3" +"@babel/plugin-transform-property-literals@npm:^7.22.5, @babel/plugin-transform-property-literals@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-property-literals@npm:7.24.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-plugin-utils": "npm:^7.24.0" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/16b048c8e87f25095f6d53634ab7912992f78e6997a6ff549edc3cf519db4fca01c7b4e0798530d7f6a05228ceee479251245cdd850a5531c6e6f404104d6cc9 + checksum: 10/a73646d7ecd95b3931a3ead82c7d5efeb46e68ba362de63eb437d33531f294ec18bd31b6d24238cd3b6a3b919a6310c4a0ba4a2629927721d4d10b0518eb7715 languageName: node linkType: hard -"@babel/plugin-transform-react-display-name@npm:^7.23.3": - version: 7.23.3 - resolution: "@babel/plugin-transform-react-display-name@npm:7.23.3" +"@babel/plugin-transform-react-display-name@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-react-display-name@npm:7.24.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-plugin-utils": "npm:^7.24.0" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/7f86964e8434d3ddbd3c81d2690c9b66dbf1cd8bd9512e2e24500e9fa8cf378bc52c0853270b3b82143aba5965aec04721df7abdb768f952b44f5c6e0b198779 + checksum: 10/4cc7268652bd73a9e249db006d7278e3e90c033684e59801012311536f1ff93eb63fea845325035533aa281e428e6ec2ae0ad04659893ec1318250ddcf4a2f77 languageName: node linkType: hard @@ -1224,109 +1239,109 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-react-jsx@npm:^7.22.15, @babel/plugin-transform-react-jsx@npm:^7.22.5": - version: 7.22.15 - resolution: "@babel/plugin-transform-react-jsx@npm:7.22.15" +"@babel/plugin-transform-react-jsx@npm:^7.22.5, @babel/plugin-transform-react-jsx@npm:^7.23.4": + version: 7.23.4 + resolution: "@babel/plugin-transform-react-jsx@npm:7.23.4" dependencies: "@babel/helper-annotate-as-pure": "npm:^7.22.5" "@babel/helper-module-imports": "npm:^7.22.15" "@babel/helper-plugin-utils": "npm:^7.22.5" - "@babel/plugin-syntax-jsx": "npm:^7.22.5" - "@babel/types": "npm:^7.22.15" + "@babel/plugin-syntax-jsx": "npm:^7.23.3" + "@babel/types": "npm:^7.23.4" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/a436bfbffe723d162e5816d510dca7349a1fc572c501d73f1e17bbca7eb899d7a6a14d8fc2ae5993dd79fdd77bcc68d295e59a3549bed03b8579c767f6e3c9dc + checksum: 10/d83806701349addfb77b8347b4f0dc8e76fb1c9ac21bdef69f4002394fce2396d61facfc6e1a3de54cbabcdadf991a1f642e69edb5116ac14f95e33d9f7c221d languageName: node linkType: hard -"@babel/plugin-transform-react-pure-annotations@npm:^7.23.3": - version: 7.23.3 - resolution: "@babel/plugin-transform-react-pure-annotations@npm:7.23.3" +"@babel/plugin-transform-react-pure-annotations@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-react-pure-annotations@npm:7.24.1" dependencies: "@babel/helper-annotate-as-pure": "npm:^7.22.5" - "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-plugin-utils": "npm:^7.24.0" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/9ea3698b1d422561d93c0187ac1ed8f2367e4250b10e259785ead5aa643c265830fd0f4cf5087a5bedbc4007444c06da2f2006686613220acf0949895f453666 + checksum: 10/06a6bfe80f1f36408d07dd80c48cf9f61177c8e5d814e80ddbe88cfad81a8b86b3110e1fe9d1ac943db77e74497daa7f874b5490c788707106ad26ecfbe44813 languageName: node linkType: hard -"@babel/plugin-transform-regenerator@npm:^7.22.10, @babel/plugin-transform-regenerator@npm:^7.23.3": - version: 7.23.3 - resolution: "@babel/plugin-transform-regenerator@npm:7.23.3" +"@babel/plugin-transform-regenerator@npm:^7.22.10, @babel/plugin-transform-regenerator@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-regenerator@npm:7.24.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-plugin-utils": "npm:^7.24.0" regenerator-transform: "npm:^0.15.2" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/7fdacc7b40008883871b519c9e5cdea493f75495118ccc56ac104b874983569a24edd024f0f5894ba1875c54ee2b442f295d6241c3280e61c725d0dd3317c8e6 + checksum: 10/a04319388a0a7931c3f8e15715d01444c32519692178b70deccc86d53304e74c0f589a4268f6c68578d86f75e934dd1fe6e6ed9071f54ee8379f356f88ef6e42 languageName: node linkType: hard -"@babel/plugin-transform-reserved-words@npm:^7.22.5, @babel/plugin-transform-reserved-words@npm:^7.23.3": - version: 7.23.3 - resolution: "@babel/plugin-transform-reserved-words@npm:7.23.3" +"@babel/plugin-transform-reserved-words@npm:^7.22.5, @babel/plugin-transform-reserved-words@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-reserved-words@npm:7.24.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-plugin-utils": "npm:^7.24.0" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/298c4440ddc136784ff920127cea137168e068404e635dc946ddb5d7b2a27b66f1dd4c4acb01f7184478ff7d5c3e7177a127279479926519042948fb7fa0fa48 + checksum: 10/132c6040c65aabae2d98a39289efb5c51a8632546dc50d2ad032c8660aec307fbed74ef499856ea4f881fc8505905f49b48e0270585da2ea3d50b75e962afd89 languageName: node linkType: hard -"@babel/plugin-transform-shorthand-properties@npm:^7.22.5, @babel/plugin-transform-shorthand-properties@npm:^7.23.3": - version: 7.23.3 - resolution: "@babel/plugin-transform-shorthand-properties@npm:7.23.3" +"@babel/plugin-transform-shorthand-properties@npm:^7.22.5, @babel/plugin-transform-shorthand-properties@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-shorthand-properties@npm:7.24.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-plugin-utils": "npm:^7.24.0" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/5d677a03676f9fff969b0246c423d64d77502e90a832665dc872a5a5e05e5708161ce1effd56bb3c0f2c20a1112fca874be57c8a759d8b08152755519281f326 + checksum: 10/006a2032d1c57dca76579ce6598c679c2f20525afef0a36e9d42affe3c8cf33c1427581ad696b519cc75dfee46c5e8ecdf0c6a29ffb14250caa3e16dd68cb424 languageName: node linkType: hard -"@babel/plugin-transform-spread@npm:^7.22.5, @babel/plugin-transform-spread@npm:^7.23.3": - version: 7.23.3 - resolution: "@babel/plugin-transform-spread@npm:7.23.3" +"@babel/plugin-transform-spread@npm:^7.22.5, @babel/plugin-transform-spread@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-spread@npm:7.24.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-plugin-utils": "npm:^7.24.0" "@babel/helper-skip-transparent-expression-wrappers": "npm:^7.22.5" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/c6372d2f788fd71d85aba12fbe08ee509e053ed27457e6674a4f9cae41ff885e2eb88aafea8fadd0ccf990601fc69ec596fa00959e05af68a15461a8d97a548d + checksum: 10/0b60cfe2f700ec2c9c1af979bb805860258539648dadcd482a5ddfc2330b733fb61bb60266404f3e068246ad0d6376040b4f9c5ab9037a3d777624d64acd89e9 languageName: node linkType: hard -"@babel/plugin-transform-sticky-regex@npm:^7.22.5, @babel/plugin-transform-sticky-regex@npm:^7.23.3": - version: 7.23.3 - resolution: "@babel/plugin-transform-sticky-regex@npm:7.23.3" +"@babel/plugin-transform-sticky-regex@npm:^7.22.5, @babel/plugin-transform-sticky-regex@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-sticky-regex@npm:7.24.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-plugin-utils": "npm:^7.24.0" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/53e55eb2575b7abfdb4af7e503a2bf7ef5faf8bf6b92d2cd2de0700bdd19e934e5517b23e6dfed94ba50ae516b62f3f916773ef7d9bc81f01503f585051e2949 + checksum: 10/e326e96a9eeb6bb01dbc4d3362f989411490671b97f62edf378b8fb102c463a018b777f28da65344d41b22aa6efcdfa01ed43d2b11fdcf202046d3174be137c5 languageName: node linkType: hard -"@babel/plugin-transform-template-literals@npm:^7.22.5, @babel/plugin-transform-template-literals@npm:^7.23.3": - version: 7.23.3 - resolution: "@babel/plugin-transform-template-literals@npm:7.23.3" +"@babel/plugin-transform-template-literals@npm:^7.22.5, @babel/plugin-transform-template-literals@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-template-literals@npm:7.24.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-plugin-utils": "npm:^7.24.0" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/b16c5cb0b8796be0118e9c144d15bdc0d20a7f3f59009c6303a6e9a8b74c146eceb3f05186f5b97afcba7cfa87e34c1585a22186e3d5b22f2fd3d27d959d92b2 + checksum: 10/4c9009c72321caf20e3b6328bbe9d7057006c5ae57b794cf247a37ca34d87dfec5e27284169a16df5a6235a083bf0f3ab9e1bfcb005d1c8b75b04aed75652621 languageName: node linkType: hard -"@babel/plugin-transform-typeof-symbol@npm:^7.22.5, @babel/plugin-transform-typeof-symbol@npm:^7.23.3": - version: 7.23.3 - resolution: "@babel/plugin-transform-typeof-symbol@npm:7.23.3" +"@babel/plugin-transform-typeof-symbol@npm:^7.22.5, @babel/plugin-transform-typeof-symbol@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-typeof-symbol@npm:7.24.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-plugin-utils": "npm:^7.24.0" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/0af7184379d43afac7614fc89b1bdecce4e174d52f4efaeee8ec1a4f2c764356c6dba3525c0685231f1cbf435b6dd4ee9e738d7417f3b10ce8bbe869c32f4384 + checksum: 10/3dda5074abf8b5df9cdef697d6ebe14a72c199bd6c2019991d033d9ad91b0be937b126b8f34c3c5a9725afee9016a3776aeef3e3b06ab9b3f54f2dd5b5aefa37 languageName: node linkType: hard @@ -1344,50 +1359,50 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-unicode-escapes@npm:^7.22.10, @babel/plugin-transform-unicode-escapes@npm:^7.23.3": - version: 7.23.3 - resolution: "@babel/plugin-transform-unicode-escapes@npm:7.23.3" +"@babel/plugin-transform-unicode-escapes@npm:^7.22.10, @babel/plugin-transform-unicode-escapes@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-unicode-escapes@npm:7.24.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-plugin-utils": "npm:^7.24.0" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/561c429183a54b9e4751519a3dfba6014431e9cdc1484fad03bdaf96582dfc72c76a4f8661df2aeeae7c34efd0fa4d02d3b83a2f63763ecf71ecc925f9cc1f60 + checksum: 10/d39041ff6b0cef78271ebe88be6dfd2882a3c6250a54ddae783f1b9adc815e8486a7d0ca054fabfa3fde1301c531d5be89224999fc7be83ff1eda9b77d173051 languageName: node linkType: hard -"@babel/plugin-transform-unicode-property-regex@npm:^7.22.5, @babel/plugin-transform-unicode-property-regex@npm:^7.23.3": - version: 7.23.3 - resolution: "@babel/plugin-transform-unicode-property-regex@npm:7.23.3" +"@babel/plugin-transform-unicode-property-regex@npm:^7.22.5, @babel/plugin-transform-unicode-property-regex@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-unicode-property-regex@npm:7.24.1" dependencies: "@babel/helper-create-regexp-features-plugin": "npm:^7.22.15" - "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-plugin-utils": "npm:^7.24.0" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/2298461a194758086d17c23c26c7de37aa533af910f9ebf31ebd0893d4aa317468043d23f73edc782ec21151d3c46cf0ff8098a83b725c49a59de28a1d4d6225 + checksum: 10/276099b4483e707f80b054e2d29bc519158bfe52461ef5ff76f70727d592df17e30b1597ef4d8a0f04d810f6cb5a8dd887bdc1d0540af3744751710ef280090f languageName: node linkType: hard -"@babel/plugin-transform-unicode-regex@npm:^7.22.5, @babel/plugin-transform-unicode-regex@npm:^7.23.3": - version: 7.23.3 - resolution: "@babel/plugin-transform-unicode-regex@npm:7.23.3" +"@babel/plugin-transform-unicode-regex@npm:^7.22.5, @babel/plugin-transform-unicode-regex@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-unicode-regex@npm:7.24.1" dependencies: "@babel/helper-create-regexp-features-plugin": "npm:^7.22.15" - "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-plugin-utils": "npm:^7.24.0" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/c5f835d17483ba899787f92e313dfa5b0055e3deab332f1d254078a2bba27ede47574b6599fcf34d3763f0c048ae0779dc21d2d8db09295edb4057478dc80a9a + checksum: 10/400a0927bdb1425b4c0dc68a61b5b2d7d17c7d9f0e07317a1a6a373c080ef94be1dd65fdc4ac9a78fcdb58f89fd128450c7bc0d5b8ca0ae7eca3fbd98e50acba languageName: node linkType: hard -"@babel/plugin-transform-unicode-sets-regex@npm:^7.22.5, @babel/plugin-transform-unicode-sets-regex@npm:^7.23.3": - version: 7.23.3 - resolution: "@babel/plugin-transform-unicode-sets-regex@npm:7.23.3" +"@babel/plugin-transform-unicode-sets-regex@npm:^7.22.5, @babel/plugin-transform-unicode-sets-regex@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-unicode-sets-regex@npm:7.24.1" dependencies: "@babel/helper-create-regexp-features-plugin": "npm:^7.22.15" - "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-plugin-utils": "npm:^7.24.0" peerDependencies: "@babel/core": ^7.0.0 - checksum: 10/79d0b4c951955ca68235c87b91ab2b393c96285f8aeaa34d6db416d2ddac90000c9bd6e8c4d82b60a2b484da69930507245035f28ba63c6cae341cf3ba68fdef + checksum: 10/364342fb8e382dfaa23628b88e6484dc1097e53fb7199f4d338f1e2cd71d839bb0a35a9b1380074f6a10adb2e98b79d53ca3ec78c0b8c557ca895ffff42180df languageName: node linkType: hard @@ -1491,25 +1506,25 @@ __metadata: languageName: node linkType: hard -"@babel/preset-env@npm:7.24.0, @babel/preset-env@npm:^7.22.9": - version: 7.24.0 - resolution: "@babel/preset-env@npm:7.24.0" +"@babel/preset-env@npm:7.24.1, @babel/preset-env@npm:^7.22.9": + version: 7.24.1 + resolution: "@babel/preset-env@npm:7.24.1" dependencies: - "@babel/compat-data": "npm:^7.23.5" + "@babel/compat-data": "npm:^7.24.1" "@babel/helper-compilation-targets": "npm:^7.23.6" "@babel/helper-plugin-utils": "npm:^7.24.0" "@babel/helper-validator-option": "npm:^7.23.5" - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "npm:^7.23.3" - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "npm:^7.23.3" - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "npm:^7.23.7" + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "npm:^7.24.1" + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "npm:^7.24.1" + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "npm:^7.24.1" "@babel/plugin-proposal-private-property-in-object": "npm:7.21.0-placeholder-for-preset-env.2" "@babel/plugin-syntax-async-generators": "npm:^7.8.4" "@babel/plugin-syntax-class-properties": "npm:^7.12.13" "@babel/plugin-syntax-class-static-block": "npm:^7.14.5" "@babel/plugin-syntax-dynamic-import": "npm:^7.8.3" "@babel/plugin-syntax-export-namespace-from": "npm:^7.8.3" - "@babel/plugin-syntax-import-assertions": "npm:^7.23.3" - "@babel/plugin-syntax-import-attributes": "npm:^7.23.3" + "@babel/plugin-syntax-import-assertions": "npm:^7.24.1" + "@babel/plugin-syntax-import-attributes": "npm:^7.24.1" "@babel/plugin-syntax-import-meta": "npm:^7.10.4" "@babel/plugin-syntax-json-strings": "npm:^7.8.3" "@babel/plugin-syntax-logical-assignment-operators": "npm:^7.10.4" @@ -1521,63 +1536,63 @@ __metadata: "@babel/plugin-syntax-private-property-in-object": "npm:^7.14.5" "@babel/plugin-syntax-top-level-await": "npm:^7.14.5" "@babel/plugin-syntax-unicode-sets-regex": "npm:^7.18.6" - "@babel/plugin-transform-arrow-functions": "npm:^7.23.3" - "@babel/plugin-transform-async-generator-functions": "npm:^7.23.9" - "@babel/plugin-transform-async-to-generator": "npm:^7.23.3" - "@babel/plugin-transform-block-scoped-functions": "npm:^7.23.3" - "@babel/plugin-transform-block-scoping": "npm:^7.23.4" - "@babel/plugin-transform-class-properties": "npm:^7.23.3" - "@babel/plugin-transform-class-static-block": "npm:^7.23.4" - "@babel/plugin-transform-classes": "npm:^7.23.8" - "@babel/plugin-transform-computed-properties": "npm:^7.23.3" - "@babel/plugin-transform-destructuring": "npm:^7.23.3" - "@babel/plugin-transform-dotall-regex": "npm:^7.23.3" - "@babel/plugin-transform-duplicate-keys": "npm:^7.23.3" - "@babel/plugin-transform-dynamic-import": "npm:^7.23.4" - "@babel/plugin-transform-exponentiation-operator": "npm:^7.23.3" - "@babel/plugin-transform-export-namespace-from": "npm:^7.23.4" - "@babel/plugin-transform-for-of": "npm:^7.23.6" - "@babel/plugin-transform-function-name": "npm:^7.23.3" - "@babel/plugin-transform-json-strings": "npm:^7.23.4" - "@babel/plugin-transform-literals": "npm:^7.23.3" - "@babel/plugin-transform-logical-assignment-operators": "npm:^7.23.4" - "@babel/plugin-transform-member-expression-literals": "npm:^7.23.3" - "@babel/plugin-transform-modules-amd": "npm:^7.23.3" - "@babel/plugin-transform-modules-commonjs": "npm:^7.23.3" - "@babel/plugin-transform-modules-systemjs": "npm:^7.23.9" - "@babel/plugin-transform-modules-umd": "npm:^7.23.3" + "@babel/plugin-transform-arrow-functions": "npm:^7.24.1" + "@babel/plugin-transform-async-generator-functions": "npm:^7.24.1" + "@babel/plugin-transform-async-to-generator": "npm:^7.24.1" + "@babel/plugin-transform-block-scoped-functions": "npm:^7.24.1" + "@babel/plugin-transform-block-scoping": "npm:^7.24.1" + "@babel/plugin-transform-class-properties": "npm:^7.24.1" + "@babel/plugin-transform-class-static-block": "npm:^7.24.1" + "@babel/plugin-transform-classes": "npm:^7.24.1" + "@babel/plugin-transform-computed-properties": "npm:^7.24.1" + "@babel/plugin-transform-destructuring": "npm:^7.24.1" + "@babel/plugin-transform-dotall-regex": "npm:^7.24.1" + "@babel/plugin-transform-duplicate-keys": "npm:^7.24.1" + "@babel/plugin-transform-dynamic-import": "npm:^7.24.1" + "@babel/plugin-transform-exponentiation-operator": "npm:^7.24.1" + "@babel/plugin-transform-export-namespace-from": "npm:^7.24.1" + "@babel/plugin-transform-for-of": "npm:^7.24.1" + "@babel/plugin-transform-function-name": "npm:^7.24.1" + "@babel/plugin-transform-json-strings": "npm:^7.24.1" + "@babel/plugin-transform-literals": "npm:^7.24.1" + "@babel/plugin-transform-logical-assignment-operators": "npm:^7.24.1" + "@babel/plugin-transform-member-expression-literals": "npm:^7.24.1" + "@babel/plugin-transform-modules-amd": "npm:^7.24.1" + "@babel/plugin-transform-modules-commonjs": "npm:^7.24.1" + "@babel/plugin-transform-modules-systemjs": "npm:^7.24.1" + "@babel/plugin-transform-modules-umd": "npm:^7.24.1" "@babel/plugin-transform-named-capturing-groups-regex": "npm:^7.22.5" - "@babel/plugin-transform-new-target": "npm:^7.23.3" - "@babel/plugin-transform-nullish-coalescing-operator": "npm:^7.23.4" - "@babel/plugin-transform-numeric-separator": "npm:^7.23.4" - "@babel/plugin-transform-object-rest-spread": "npm:^7.24.0" - "@babel/plugin-transform-object-super": "npm:^7.23.3" - "@babel/plugin-transform-optional-catch-binding": "npm:^7.23.4" - "@babel/plugin-transform-optional-chaining": "npm:^7.23.4" - "@babel/plugin-transform-parameters": "npm:^7.23.3" - "@babel/plugin-transform-private-methods": "npm:^7.23.3" - "@babel/plugin-transform-private-property-in-object": "npm:^7.23.4" - "@babel/plugin-transform-property-literals": "npm:^7.23.3" - "@babel/plugin-transform-regenerator": "npm:^7.23.3" - "@babel/plugin-transform-reserved-words": "npm:^7.23.3" - "@babel/plugin-transform-shorthand-properties": "npm:^7.23.3" - "@babel/plugin-transform-spread": "npm:^7.23.3" - "@babel/plugin-transform-sticky-regex": "npm:^7.23.3" - "@babel/plugin-transform-template-literals": "npm:^7.23.3" - "@babel/plugin-transform-typeof-symbol": "npm:^7.23.3" - "@babel/plugin-transform-unicode-escapes": "npm:^7.23.3" - "@babel/plugin-transform-unicode-property-regex": "npm:^7.23.3" - "@babel/plugin-transform-unicode-regex": "npm:^7.23.3" - "@babel/plugin-transform-unicode-sets-regex": "npm:^7.23.3" + "@babel/plugin-transform-new-target": "npm:^7.24.1" + "@babel/plugin-transform-nullish-coalescing-operator": "npm:^7.24.1" + "@babel/plugin-transform-numeric-separator": "npm:^7.24.1" + "@babel/plugin-transform-object-rest-spread": "npm:^7.24.1" + "@babel/plugin-transform-object-super": "npm:^7.24.1" + "@babel/plugin-transform-optional-catch-binding": "npm:^7.24.1" + "@babel/plugin-transform-optional-chaining": "npm:^7.24.1" + "@babel/plugin-transform-parameters": "npm:^7.24.1" + "@babel/plugin-transform-private-methods": "npm:^7.24.1" + "@babel/plugin-transform-private-property-in-object": "npm:^7.24.1" + "@babel/plugin-transform-property-literals": "npm:^7.24.1" + "@babel/plugin-transform-regenerator": "npm:^7.24.1" + "@babel/plugin-transform-reserved-words": "npm:^7.24.1" + "@babel/plugin-transform-shorthand-properties": "npm:^7.24.1" + "@babel/plugin-transform-spread": "npm:^7.24.1" + "@babel/plugin-transform-sticky-regex": "npm:^7.24.1" + "@babel/plugin-transform-template-literals": "npm:^7.24.1" + "@babel/plugin-transform-typeof-symbol": "npm:^7.24.1" + "@babel/plugin-transform-unicode-escapes": "npm:^7.24.1" + "@babel/plugin-transform-unicode-property-regex": "npm:^7.24.1" + "@babel/plugin-transform-unicode-regex": "npm:^7.24.1" + "@babel/plugin-transform-unicode-sets-regex": "npm:^7.24.1" "@babel/preset-modules": "npm:0.1.6-no-external-plugins" - babel-plugin-polyfill-corejs2: "npm:^0.4.8" - babel-plugin-polyfill-corejs3: "npm:^0.9.0" - babel-plugin-polyfill-regenerator: "npm:^0.5.5" + babel-plugin-polyfill-corejs2: "npm:^0.4.10" + babel-plugin-polyfill-corejs3: "npm:^0.10.1" + babel-plugin-polyfill-regenerator: "npm:^0.6.1" core-js-compat: "npm:^3.31.0" semver: "npm:^6.3.1" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/88bca150a09e658124997178ee1ff375a9aceecfd70ec11c7ccc12e82f5be5f7ff2ddfefba5b10fb617891645f92949392b350509de9742d2aa138f42959e190 + checksum: 10/330ff67d4522fba0dd4a173f6b5649e947488dc7275dd2ae86e1e9be665fc6cdbcadce939671ece86c0683a98464a88dd7e70bd7120c923c26498ba596cecc8b languageName: node linkType: hard @@ -1607,19 +1622,19 @@ __metadata: languageName: node linkType: hard -"@babel/preset-react@npm:7.23.3, @babel/preset-react@npm:^7.22.5": - version: 7.23.3 - resolution: "@babel/preset-react@npm:7.23.3" +"@babel/preset-react@npm:7.24.1, @babel/preset-react@npm:^7.22.5": + version: 7.24.1 + resolution: "@babel/preset-react@npm:7.24.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.22.5" - "@babel/helper-validator-option": "npm:^7.22.15" - "@babel/plugin-transform-react-display-name": "npm:^7.23.3" - "@babel/plugin-transform-react-jsx": "npm:^7.22.15" + "@babel/helper-plugin-utils": "npm:^7.24.0" + "@babel/helper-validator-option": "npm:^7.23.5" + "@babel/plugin-transform-react-display-name": "npm:^7.24.1" + "@babel/plugin-transform-react-jsx": "npm:^7.23.4" "@babel/plugin-transform-react-jsx-development": "npm:^7.22.5" - "@babel/plugin-transform-react-pure-annotations": "npm:^7.23.3" + "@babel/plugin-transform-react-pure-annotations": "npm:^7.24.1" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/ef6aef131b2f36e2883e9da0d832903643cb3c9ad4f32e04fb3eecae59e4221d583139e8d8f973e25c28d15aafa6b3e60fe9f25c5fd09abd3e2df03b8637bdd2 + checksum: 10/a796c609ace7d58a56b42b6630cdd9e1d896ce2f8b35331b9ea040eaaf3cc9aa99cd2614e379a27c10410f34e89355e2739c7097e8065ce5e40900a77b13d716 languageName: node linkType: hard @@ -1670,12 +1685,12 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:7.24.0, @babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.11.1, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.14.0, @babel/runtime@npm:^7.14.5, @babel/runtime@npm:^7.15.4, @babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.18.0, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.19.4, @babel/runtime@npm:^7.20.0, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.7.2, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2": - version: 7.24.0 - resolution: "@babel/runtime@npm:7.24.0" +"@babel/runtime@npm:7.24.1, @babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.11.1, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.14.0, @babel/runtime@npm:^7.14.5, @babel/runtime@npm:^7.15.4, @babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.18.0, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.19.4, @babel/runtime@npm:^7.20.0, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.7.2, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2": + version: 7.24.1 + resolution: "@babel/runtime@npm:7.24.1" dependencies: regenerator-runtime: "npm:^0.14.0" - checksum: 10/8d32c7e116606ea322b89f9fde8ffae6be9503b549dc0d0abb38bd9dc26e87469b9fb7a66964cc089ee558fd0a97d304fb0a3cfec140694764fb0d71b6a6f5e4 + checksum: 10/3a8d61400c636d1ce3a42895a106cd4dfb4e9b88832a8a754a724c68652f821d7a46dce394305d7623f9f0d3597bf0a98aeb5f9c150ef60e14bbbf66caab4654 languageName: node linkType: hard @@ -1690,25 +1705,25 @@ __metadata: languageName: node linkType: hard -"@babel/traverse@npm:^7.1.6, @babel/traverse@npm:^7.22.8, @babel/traverse@npm:^7.23.2, @babel/traverse@npm:^7.24.0": - version: 7.24.0 - resolution: "@babel/traverse@npm:7.24.0" +"@babel/traverse@npm:^7.1.6, @babel/traverse@npm:^7.22.8, @babel/traverse@npm:^7.23.2, @babel/traverse@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/traverse@npm:7.24.1" dependencies: - "@babel/code-frame": "npm:^7.23.5" - "@babel/generator": "npm:^7.23.6" + "@babel/code-frame": "npm:^7.24.1" + "@babel/generator": "npm:^7.24.1" "@babel/helper-environment-visitor": "npm:^7.22.20" "@babel/helper-function-name": "npm:^7.23.0" "@babel/helper-hoist-variables": "npm:^7.22.5" "@babel/helper-split-export-declaration": "npm:^7.22.6" - "@babel/parser": "npm:^7.24.0" + "@babel/parser": "npm:^7.24.1" "@babel/types": "npm:^7.24.0" debug: "npm:^4.3.1" globals: "npm:^11.1.0" - checksum: 10/5cc482248ebb79adcbcf021aab4e0e95bafe2a1736ee4b46abe6f88b59848ad73e15e219db8f06c9a33a14c64257e5b47e53876601e998a8c596accb1b7f4996 + checksum: 10/b9b0173c286ef549e179f3725df3c4958069ad79fe5b9840adeb99692eb4a5a08db4e735c0f086aab52e7e08ec711cee9e7c06cb908d8035641d1382172308d3 languageName: node linkType: hard -"@babel/types@npm:^7.0.0, @babel/types@npm:^7.2.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.22.15, @babel/types@npm:^7.22.19, @babel/types@npm:^7.22.5, @babel/types@npm:^7.23.0, @babel/types@npm:^7.23.6, @babel/types@npm:^7.24.0, @babel/types@npm:^7.3.0, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3": +"@babel/types@npm:^7.0.0, @babel/types@npm:^7.2.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.22.15, @babel/types@npm:^7.22.19, @babel/types@npm:^7.22.5, @babel/types@npm:^7.23.0, @babel/types@npm:^7.23.4, @babel/types@npm:^7.24.0, @babel/types@npm:^7.3.0, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3": version: 7.24.0 resolution: "@babel/types@npm:7.24.0" dependencies: @@ -3737,9 +3752,9 @@ __metadata: version: 0.0.0-use.local resolution: "@grafana/flamegraph@workspace:packages/grafana-flamegraph" dependencies: - "@babel/core": "npm:7.24.0" - "@babel/preset-env": "npm:7.24.0" - "@babel/preset-react": "npm:7.23.3" + "@babel/core": "npm:7.24.1" + "@babel/preset-env": "npm:7.24.1" + "@babel/preset-react": "npm:7.24.1" "@emotion/css": "npm:11.11.2" "@grafana/data": "npm:11.0.0-pre" "@grafana/tsconfig": "npm:^1.3.0-rc1" @@ -4133,7 +4148,7 @@ __metadata: version: 0.0.0-use.local resolution: "@grafana/ui@workspace:packages/grafana-ui" dependencies: - "@babel/core": "npm:7.24.0" + "@babel/core": "npm:7.24.1" "@emotion/css": "npm:11.11.2" "@emotion/react": "npm:11.11.4" "@floating-ui/react": "npm:0.26.9" @@ -4664,14 +4679,14 @@ __metadata: 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" +"@jridgewell/gen-mapping@npm:^0.3.0, @jridgewell/gen-mapping@npm:^0.3.5": + version: 0.3.5 + resolution: "@jridgewell/gen-mapping@npm:0.3.5" dependencies: - "@jridgewell/set-array": "npm:^1.0.1" + "@jridgewell/set-array": "npm:^1.2.1" "@jridgewell/sourcemap-codec": "npm:^1.4.10" - "@jridgewell/trace-mapping": "npm:^0.3.9" - checksum: 10/7ba0070be1aeda7d7694b09d847c3b95879409b26559b9d7e97a88ec94b838fb380df43ae328ee2d2df4d79e75d7afe6ba315199d18d79aa20839ebdfb739420 + "@jridgewell/trace-mapping": "npm:^0.3.24" + checksum: 10/81587b3c4dd8e6c60252122937cea0c637486311f4ed208b52b62aae2e7a87598f63ec330e6cd0984af494bfb16d3f0d60d3b21d7e5b4aedd2602ff3fe9d32e2 languageName: node linkType: hard @@ -4682,10 +4697,10 @@ __metadata: 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: 10/69a84d5980385f396ff60a175f7177af0b8da4ddb81824cb7016a9ef914eee9806c72b6b65942003c63f7983d4f39a5c6c27185bbca88eb4690b62075602e28e +"@jridgewell/set-array@npm:^1.0.0, @jridgewell/set-array@npm:^1.2.1": + version: 1.2.1 + resolution: "@jridgewell/set-array@npm:1.2.1" + checksum: 10/832e513a85a588f8ed4f27d1279420d8547743cc37fcad5a5a76fc74bb895b013dfe614d0eed9cb860048e6546b798f8f2652020b4b2ba0561b05caa8c654b10 languageName: node linkType: hard @@ -4716,13 +4731,13 @@ __metadata: languageName: node linkType: hard -"@jridgewell/trace-mapping@npm:^0.3.12, @jridgewell/trace-mapping@npm:^0.3.17, @jridgewell/trace-mapping@npm:^0.3.18, @jridgewell/trace-mapping@npm:^0.3.20, @jridgewell/trace-mapping@npm:^0.3.21, @jridgewell/trace-mapping@npm:^0.3.9": - version: 0.3.22 - resolution: "@jridgewell/trace-mapping@npm:0.3.22" +"@jridgewell/trace-mapping@npm:^0.3.12, @jridgewell/trace-mapping@npm:^0.3.18, @jridgewell/trace-mapping@npm:^0.3.20, @jridgewell/trace-mapping@npm:^0.3.21, @jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.25, @jridgewell/trace-mapping@npm:^0.3.9": + version: 0.3.25 + resolution: "@jridgewell/trace-mapping@npm:0.3.25" dependencies: "@jridgewell/resolve-uri": "npm:^3.1.0" "@jridgewell/sourcemap-codec": "npm:^1.4.14" - checksum: 10/48d3e3db00dbecb211613649a1849876ba5544a3f41cf5e6b99ea1130272d6cf18591b5b67389bce20f1c871b4ede5900c3b6446a7aab6d0a3b2fe806a834db7 + checksum: 10/dced32160a44b49d531b80a4a2159dceab6b3ddf0c8e95a0deae4b0e894b172defa63d5ac52a19c2068e1fe7d31ea4ba931fbeec103233ecb4208953967120fc languageName: node linkType: hard @@ -11920,16 +11935,28 @@ __metadata: languageName: node linkType: hard -"babel-plugin-polyfill-corejs2@npm:^0.4.6, babel-plugin-polyfill-corejs2@npm:^0.4.8": - version: 0.4.8 - resolution: "babel-plugin-polyfill-corejs2@npm:0.4.8" +"babel-plugin-polyfill-corejs2@npm:^0.4.10, babel-plugin-polyfill-corejs2@npm:^0.4.6": + version: 0.4.10 + resolution: "babel-plugin-polyfill-corejs2@npm:0.4.10" dependencies: "@babel/compat-data": "npm:^7.22.6" - "@babel/helper-define-polyfill-provider": "npm:^0.5.0" + "@babel/helper-define-polyfill-provider": "npm:^0.6.1" semver: "npm:^6.3.1" peerDependencies: "@babel/core": ^7.4.0 || ^8.0.0-0 <8.0.0 - checksum: 10/6b5a79bdc1c43edf857fd3a82966b3c7ff4a90eee00ca8d663e0a98304d6e285a05759d64a4dbc16e04a2a5ea1f248673d8bf789711be5e694e368f19884887c + checksum: 10/9fb5e59a3235eba66fb05060b2a3ecd6923084f100df7526ab74b6272347d7adcf99e17366b82df36e592cde4e82fdb7ae24346a990eced76c7d504cac243400 + languageName: node + linkType: hard + +"babel-plugin-polyfill-corejs3@npm:^0.10.1": + version: 0.10.1 + resolution: "babel-plugin-polyfill-corejs3@npm:0.10.1" + dependencies: + "@babel/helper-define-polyfill-provider": "npm:^0.6.1" + core-js-compat: "npm:^3.36.0" + peerDependencies: + "@babel/core": ^7.4.0 || ^8.0.0-0 <8.0.0 + checksum: 10/88c3a644bb10df3052311339dca5ae0d440015f1feb8a4597f17826acaeeb61b6d01f3cc8d0fed200cb69d38345b28fe68e41ed8ef9d4fd1152fea0848ce0fc4 languageName: node linkType: hard @@ -11945,26 +11972,25 @@ __metadata: languageName: node linkType: hard -"babel-plugin-polyfill-corejs3@npm:^0.9.0": - version: 0.9.0 - resolution: "babel-plugin-polyfill-corejs3@npm:0.9.0" +"babel-plugin-polyfill-regenerator@npm:^0.5.3": + version: 0.5.5 + resolution: "babel-plugin-polyfill-regenerator@npm:0.5.5" dependencies: "@babel/helper-define-polyfill-provider": "npm:^0.5.0" - core-js-compat: "npm:^3.34.0" peerDependencies: "@babel/core": ^7.4.0 || ^8.0.0-0 <8.0.0 - checksum: 10/efdf9ba82e7848a2c66e0522adf10ac1646b16f271a9006b61a22f976b849de22a07c54c8826887114842ccd20cc9a4617b61e8e0789227a74378ab508e715cd + checksum: 10/3a9b4828673b23cd648dcfb571eadcd9d3fadfca0361d0a7c6feeb5a30474e92faaa49f067a6e1c05e49b6a09812879992028ff3ef3446229ff132d6e1de7eb6 languageName: node linkType: hard -"babel-plugin-polyfill-regenerator@npm:^0.5.3, babel-plugin-polyfill-regenerator@npm:^0.5.5": - version: 0.5.5 - resolution: "babel-plugin-polyfill-regenerator@npm:0.5.5" +"babel-plugin-polyfill-regenerator@npm:^0.6.1": + version: 0.6.1 + resolution: "babel-plugin-polyfill-regenerator@npm:0.6.1" dependencies: - "@babel/helper-define-polyfill-provider": "npm:^0.5.0" + "@babel/helper-define-polyfill-provider": "npm:^0.6.1" peerDependencies: "@babel/core": ^7.4.0 || ^8.0.0-0 <8.0.0 - checksum: 10/3a9b4828673b23cd648dcfb571eadcd9d3fadfca0361d0a7c6feeb5a30474e92faaa49f067a6e1c05e49b6a09812879992028ff3ef3446229ff132d6e1de7eb6 + checksum: 10/9df4a8e9939dd419fed3d9ea26594b4479f2968f37c225e1b2aa463001d7721f5537740e6622909d2a570b61cec23256924a1701404fc9d6fd4474d3e845cedb languageName: node linkType: hard @@ -13600,12 +13626,12 @@ __metadata: languageName: node linkType: hard -"core-js-compat@npm:^3.31.0, core-js-compat@npm:^3.33.1, core-js-compat@npm:^3.34.0": - version: 3.35.1 - resolution: "core-js-compat@npm:3.35.1" +"core-js-compat@npm:^3.31.0, core-js-compat@npm:^3.33.1, core-js-compat@npm:^3.36.0": + version: 3.36.1 + resolution: "core-js-compat@npm:3.36.1" dependencies: - browserslist: "npm:^4.22.2" - checksum: 10/9a153c66591e23703e182b258ec6bdaff0a7c578dc5f9ac152fdfef2d09e8ec277f192e28d4634a8b576c8e1a6d3b1ac76ff6b8776e72b71b334e609e177a05e + browserslist: "npm:^4.23.0" + checksum: 10/d86b46805de7f5ba3675ed21532ecc64b6c1f123be7286b9efa7941ec087cd8d2446cb555f03a407dbbbeb6e881d1baf92eaffb7f051b11d9103f39c8731fa62 languageName: node linkType: hard @@ -18223,7 +18249,7 @@ __metadata: version: 0.0.0-use.local resolution: "grafana@workspace:." dependencies: - "@babel/runtime": "npm:7.24.0" + "@babel/runtime": "npm:7.24.1" "@betterer/betterer": "npm:5.4.0" "@betterer/cli": "npm:5.4.0" "@betterer/eslint": "npm:5.4.0" From c9bb18101c8e97dca23fba35bdbb8fe36877bb70 Mon Sep 17 00:00:00 2001 From: Santiago Date: Tue, 19 Mar 2024 12:12:03 +0100 Subject: [PATCH 0788/1406] Alerting: Decrypt secrets before sending configuration to the remote Alertmanager (#83640) * (WIP) Alerting: Decrypt secrets before sending configuration to the remote Alertmanager * refactor, fix tests * test decrypting secrets * tidy up * test SendConfiguration, quote keys, refactor tests * make linter happy * decrypt configuration before comparing * copy configuration struct before decrypting * reduce diff in TestCompareAndSendConfiguration * clean up remote/alertmanager.go * make linter happy * avoid serializing into JSON to copy struct * codeowners --- go.mod | 2 +- go.work.sum | 2 + .../api/tooling/definitions/alertmanager.go | 30 ++++ pkg/services/ngalert/ngalert.go | 6 +- .../multiorg_alertmanager_remote_test.go | 4 +- pkg/services/ngalert/remote/alertmanager.go | 61 +++++-- .../ngalert/remote/alertmanager_test.go | 162 ++++++++++++++++-- 7 files changed, 233 insertions(+), 34 deletions(-) diff --git a/go.mod b/go.mod index 31149a5745..4e8cd53c1f 100644 --- a/go.mod +++ b/go.mod @@ -341,7 +341,7 @@ require ( github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/mapstructure v1.5.0 //@grafana/grafana-authnz-team github.com/mitchellh/reflectwalk v1.0.2 // indirect - github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // @grafana/alerting-squad-backend github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.0.3-0.20220512140940-7b36cea86235 // indirect diff --git a/go.work.sum b/go.work.sum index 6c3d23663f..5c7308963e 100644 --- a/go.work.sum +++ b/go.work.sum @@ -401,6 +401,8 @@ github.com/grafana/e2e v0.1.1-0.20221018202458-cffd2bb71c7b h1:Ha+kSIoTutf4ytlVw github.com/grafana/e2e v0.1.1-0.20221018202458-cffd2bb71c7b/go.mod h1:3UsooRp7yW5/NJQBlXcTsAHOoykEhNUYXkQ3r6ehEEY= github.com/grafana/gomemcache v0.0.0-20231023152154-6947259a0586 h1:/of8Z8taCPftShATouOrBVy6GaTTjgQd/VfNiZp/VXQ= github.com/grafana/gomemcache v0.0.0-20231023152154-6947259a0586/go.mod h1:PGk3RjYHpxMM8HFPhKKo+vve3DdlPUELZLSDEFehPuU= +github.com/grafana/grafana-aws-sdk v0.25.0 h1:XNi3iA/C/KPArmVbQfbwKQROaIotd38nCRjNE6P1UP0= +github.com/grafana/grafana-aws-sdk v0.25.0/go.mod h1:3zghFF6edrxn0d6k6X9HpGZXDH+VfA+MwD2Pc/9X0ec= github.com/grafana/grafana-plugin-sdk-go v0.212.0/go.mod h1:qsI4ktDf0lig74u8SLPJf9zRdVxWV/W4Wi+Ox6gifgs= github.com/grafana/grafana/pkg/promlib v0.0.2/go.mod h1:3El4NlsfALz8QQCbEGHGFvJUG+538QLMuALRhZ3pcoo= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM= diff --git a/pkg/services/ngalert/api/tooling/definitions/alertmanager.go b/pkg/services/ngalert/api/tooling/definitions/alertmanager.go index 3a9b6cc09b..5248a2e5bb 100644 --- a/pkg/services/ngalert/api/tooling/definitions/alertmanager.go +++ b/pkg/services/ngalert/api/tooling/definitions/alertmanager.go @@ -2,12 +2,14 @@ package definitions import ( "context" + "encoding/base64" "encoding/json" "fmt" "time" "github.com/go-openapi/strfmt" "github.com/grafana/alerting/definition" + "github.com/mohae/deepcopy" amv2 "github.com/prometheus/alertmanager/api/v2/models" "github.com/prometheus/alertmanager/config" "github.com/prometheus/common/model" @@ -615,6 +617,34 @@ func (c *PostableUserConfig) validate() error { return nil } +// Decrypt returns a copy of the configuration struct with decrypted secure settings in receivers. +func (c *PostableUserConfig) Decrypt(decryptFn func(payload []byte) ([]byte, error)) (PostableUserConfig, error) { + newCfg, ok := deepcopy.Copy(c).(*PostableUserConfig) + if !ok { + return PostableUserConfig{}, fmt.Errorf("failed to copy config") + } + + // Iterate through receivers and decrypt secure settings. + for _, rcv := range newCfg.AlertmanagerConfig.Receivers { + for _, gmr := range rcv.PostableGrafanaReceivers.GrafanaManagedReceivers { + for k, v := range gmr.SecureSettings { + decoded, err := base64.StdEncoding.DecodeString(v) + if err != nil { + return PostableUserConfig{}, fmt.Errorf("failed to decode value for key '%s': %w", k, err) + } + + decrypted, err := decryptFn(decoded) + if err != nil { + return PostableUserConfig{}, fmt.Errorf("failed to decrypt value for key '%s': %w", k, err) + } + + gmr.SecureSettings[k] = string(decrypted) + } + } + } + return *newCfg, nil +} + // GetGrafanaReceiverMap returns a map that associates UUIDs to grafana receivers func (c *PostableUserConfig) GetGrafanaReceiverMap() map[string]*PostableGrafanaReceiver { UIDs := make(map[string]*PostableGrafanaReceiver) diff --git a/pkg/services/ngalert/ngalert.go b/pkg/services/ngalert/ngalert.go index 39714f9f6c..0547084a66 100644 --- a/pkg/services/ngalert/ngalert.go +++ b/pkg/services/ngalert/ngalert.go @@ -190,7 +190,7 @@ func (ng *AlertNG) init() error { } // Create remote Alertmanager. - remoteAM, err := createRemoteAlertmanager(orgID, ng.Cfg.UnifiedAlerting.RemoteAlertmanager, ng.KVStore, m) + remoteAM, err := createRemoteAlertmanager(orgID, ng.Cfg.UnifiedAlerting.RemoteAlertmanager, ng.KVStore, ng.SecretsService.Decrypt, m) if err != nil { moaLogger.Error("Failed to create remote Alertmanager, falling back to using only the internal one", "err", err) return internalAM, nil @@ -535,7 +535,7 @@ func ApplyStateHistoryFeatureToggles(cfg *setting.UnifiedAlertingStateHistorySet } } -func createRemoteAlertmanager(orgID int64, amCfg setting.RemoteAlertmanagerSettings, kvstore kvstore.KVStore, m *metrics.RemoteAlertmanager) (*remote.Alertmanager, error) { +func createRemoteAlertmanager(orgID int64, amCfg setting.RemoteAlertmanagerSettings, kvstore kvstore.KVStore, decryptFn remote.DecryptFn, m *metrics.RemoteAlertmanager) (*remote.Alertmanager, error) { externalAMCfg := remote.AlertmanagerConfig{ OrgID: orgID, URL: amCfg.URL, @@ -544,5 +544,5 @@ func createRemoteAlertmanager(orgID int64, amCfg setting.RemoteAlertmanagerSetti } // We won't be handling files on disk, we can pass an empty string as workingDirPath. stateStore := notifier.NewFileStore(orgID, kvstore, "") - return remote.NewAlertmanager(externalAMCfg, stateStore, m) + return remote.NewAlertmanager(externalAMCfg, stateStore, decryptFn, m) } diff --git a/pkg/services/ngalert/notifier/multiorg_alertmanager_remote_test.go b/pkg/services/ngalert/notifier/multiorg_alertmanager_remote_test.go index 603cfb7e65..a4a8c98a78 100644 --- a/pkg/services/ngalert/notifier/multiorg_alertmanager_remote_test.go +++ b/pkg/services/ngalert/notifier/multiorg_alertmanager_remote_test.go @@ -49,6 +49,7 @@ func TestMultiorgAlertmanager_RemoteSecondaryMode(t *testing.T) { }) // Create the factory function for the MOA using the forked Alertmanager in remote secondary mode. + secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) override := notifier.WithAlertmanagerOverride(func(factoryFn notifier.OrgAlertmanagerFactory) notifier.OrgAlertmanagerFactory { return func(ctx context.Context, orgID int64) (notifier.Alertmanager, error) { // Create internal Alertmanager. @@ -65,7 +66,7 @@ func TestMultiorgAlertmanager_RemoteSecondaryMode(t *testing.T) { // We won't be handling files on disk, we can pass an empty string as workingDirPath. stateStore := notifier.NewFileStore(orgID, kvStore, "") m := metrics.NewRemoteAlertmanagerMetrics(prometheus.NewRegistry()) - remoteAM, err := remote.NewAlertmanager(externalAMCfg, stateStore, m) + remoteAM, err := remote.NewAlertmanager(externalAMCfg, stateStore, secretsService.Decrypt, m) require.NoError(t, err) // Use both Alertmanager implementations in the forked Alertmanager. @@ -87,7 +88,6 @@ func TestMultiorgAlertmanager_RemoteSecondaryMode(t *testing.T) { DefaultConfiguration: setting.GetAlertmanagerDefaultConfiguration(), }, // do not poll in tests. } - secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) moa, err := notifier.NewMultiOrgAlertmanager( cfg, configStore, diff --git a/pkg/services/ngalert/remote/alertmanager.go b/pkg/services/ngalert/remote/alertmanager.go index 8eabc86fbe..496bd26c53 100644 --- a/pkg/services/ngalert/remote/alertmanager.go +++ b/pkg/services/ngalert/remote/alertmanager.go @@ -3,6 +3,7 @@ package remote import ( "context" "crypto/md5" + "encoding/json" "fmt" "net/http" "net/url" @@ -25,7 +26,11 @@ type stateStore interface { GetFullState(ctx context.Context, keys ...string) (string, error) } +// DecryptFn is a function that takes in an encrypted value and returns it decrypted. +type DecryptFn func(ctx context.Context, payload []byte) ([]byte, error) + type Alertmanager struct { + decrypt DecryptFn log log.Logger metrics *metrics.RemoteAlertmanager orgID int64 @@ -61,7 +66,7 @@ func (cfg *AlertmanagerConfig) Validate() error { return nil } -func NewAlertmanager(cfg AlertmanagerConfig, store stateStore, metrics *metrics.RemoteAlertmanager) (*Alertmanager, error) { +func NewAlertmanager(cfg AlertmanagerConfig, store stateStore, decryptFn DecryptFn, metrics *metrics.RemoteAlertmanager) (*Alertmanager, error) { if err := cfg.Validate(); err != nil { return nil, err } @@ -111,6 +116,7 @@ func NewAlertmanager(cfg AlertmanagerConfig, store stateStore, metrics *metrics. return &Alertmanager{ amClient: amc, + decrypt: decryptFn, log: logger, metrics: metrics, mimirClient: mc, @@ -176,21 +182,42 @@ func (am *Alertmanager) checkReadiness(ctx context.Context) error { // CompareAndSendConfiguration checks whether a given configuration is being used by the remote Alertmanager. // If not, it sends the configuration to the remote Alertmanager. func (am *Alertmanager) CompareAndSendConfiguration(ctx context.Context, config *models.AlertConfiguration) error { - if am.shouldSendConfig(ctx, config) { - am.metrics.ConfigSyncsTotal.Inc() - if err := am.mimirClient.CreateGrafanaAlertmanagerConfig( - ctx, - config.AlertmanagerConfiguration, - config.ConfigurationHash, - config.ID, - config.CreatedAt, - config.Default, - ); err != nil { - am.metrics.ConfigSyncErrorsTotal.Inc() - return err - } - am.metrics.LastConfigSync.SetToCurrentTime() + c, err := notifier.Load([]byte(config.AlertmanagerConfiguration)) + if err != nil { + return err + } + + // Decrypt the configuration before comparing. + fn := func(payload []byte) ([]byte, error) { + return am.decrypt(ctx, payload) + } + decrypted, err := c.Decrypt(fn) + if err != nil { + return err + } + rawDecrypted, err := json.Marshal(decrypted) + if err != nil { + return err + } + + // Send the configuration only if we need to. + if !am.shouldSendConfig(ctx, rawDecrypted) { + return nil + } + + am.metrics.ConfigSyncsTotal.Inc() + if err := am.mimirClient.CreateGrafanaAlertmanagerConfig( + ctx, + string(rawDecrypted), + config.ConfigurationHash, + config.ID, + config.CreatedAt, + config.Default, + ); err != nil { + am.metrics.ConfigSyncErrorsTotal.Inc() + return err } + am.metrics.LastConfigSync.SetToCurrentTime() return nil } @@ -375,7 +402,7 @@ func (am *Alertmanager) CleanUp() {} // shouldSendConfig compares the remote Alertmanager configuration with our local one. // It returns true if the configurations are different. -func (am *Alertmanager) shouldSendConfig(ctx context.Context, config *models.AlertConfiguration) bool { +func (am *Alertmanager) shouldSendConfig(ctx context.Context, rawConfig []byte) bool { rc, err := am.mimirClient.GetGrafanaAlertmanagerConfig(ctx) if err != nil { // Log the error and return true so we try to upload our config anyway. @@ -383,7 +410,7 @@ func (am *Alertmanager) shouldSendConfig(ctx context.Context, config *models.Ale return true } - return md5.Sum([]byte(rc.GrafanaAlertmanagerConfig)) != md5.Sum([]byte(config.AlertmanagerConfiguration)) + return md5.Sum([]byte(rc.GrafanaAlertmanagerConfig)) != md5.Sum(rawConfig) } // shouldSendState compares the remote Alertmanager state with our local one. diff --git a/pkg/services/ngalert/remote/alertmanager_test.go b/pkg/services/ngalert/remote/alertmanager_test.go index 59e6003467..a500fcbf27 100644 --- a/pkg/services/ngalert/remote/alertmanager_test.go +++ b/pkg/services/ngalert/remote/alertmanager_test.go @@ -4,20 +4,31 @@ import ( "context" "crypto/md5" "encoding/base64" + "encoding/json" + "errors" "fmt" + "io" "math/rand" "net/http" "net/http/httptest" "os" + "strings" "testing" "time" "github.com/go-openapi/strfmt" + "github.com/grafana/grafana/pkg/infra/db" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/metrics" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/notifier" - "github.com/grafana/grafana/pkg/services/ngalert/tests/fakes" + "github.com/grafana/grafana/pkg/services/ngalert/remote/client" + ngfakes "github.com/grafana/grafana/pkg/services/ngalert/tests/fakes" + "github.com/grafana/grafana/pkg/services/secrets" + "github.com/grafana/grafana/pkg/services/secrets/database" + "github.com/grafana/grafana/pkg/services/secrets/fakes" + secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager" + "github.com/grafana/grafana/pkg/tests/testsuite" "github.com/grafana/grafana/pkg/util" amv2 "github.com/prometheus/alertmanager/api/v2/models" "github.com/prometheus/alertmanager/cluster/clusterpb" @@ -25,8 +36,13 @@ import ( "github.com/stretchr/testify/require" ) -// Valid Grafana Alertmanager configuration. +// Valid Grafana Alertmanager configurations. const testGrafanaConfig = `{"template_files":{},"alertmanager_config":{"route":{"receiver":"grafana-default-email","group_by":["grafana_folder","alertname"]},"templates":null,"receivers":[{"name":"grafana-default-email","grafana_managed_receiver_configs":[{"uid":"","name":"some other name","type":"email","disableResolveMessage":false,"settings":{"addresses":"\u003cexample@email.com\u003e"},"secureSettings":null}]}]}}` +const testGrafanaConfigWithSecret = `{"template_files":{},"alertmanager_config":{"route":{"receiver":"grafana-default-email","group_by":["grafana_folder","alertname"]},"templates":null,"receivers":[{"name":"grafana-default-email","grafana_managed_receiver_configs":[{"uid":"dde6ntuob69dtf","name":"WH","type":"webhook","disableResolveMessage":false,"settings":{"url":"http://localhost:8080","username":"test"},"secureSettings":{"password":"test"}}]}]}}` + +func TestMain(m *testing.M) { + testsuite.Run(m) +} func TestNewAlertmanager(t *testing.T) { tests := []struct { @@ -62,6 +78,7 @@ func TestNewAlertmanager(t *testing.T) { }, } + secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) for _, test := range tests { t.Run(test.name, func(tt *testing.T) { cfg := AlertmanagerConfig{ @@ -71,7 +88,7 @@ func TestNewAlertmanager(t *testing.T) { BasicAuthPassword: test.password, } m := metrics.NewRemoteAlertmanagerMetrics(prometheus.NewRegistry()) - am, err := NewAlertmanager(cfg, nil, m) + am, err := NewAlertmanager(cfg, nil, secretsService.Decrypt, m) if test.expErr != "" { require.EqualError(tt, err, test.expErr) return @@ -90,10 +107,32 @@ func TestApplyConfig(t *testing.T) { errorHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusInternalServerError) }) + + var configSent string okHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodPost && strings.Contains(r.URL.Path, "/config") { + var c client.UserGrafanaConfig + require.NoError(t, json.NewDecoder(r.Body).Decode(&c)) + configSent = c.GrafanaAlertmanagerConfig + } + w.WriteHeader(http.StatusOK) }) + // Encrypt receivers to save secrets in the database. + var c apimodels.PostableUserConfig + require.NoError(t, json.Unmarshal([]byte(testGrafanaConfigWithSecret), &c)) + secretsService := secretsManager.SetupTestService(t, database.ProvideSecretsStore(db.InitTestDB(t))) + err := notifier.EncryptReceiverConfigs(c.AlertmanagerConfig.Receivers, func(ctx context.Context, payload []byte) ([]byte, error) { + return secretsService.Encrypt(ctx, payload, secrets.WithoutScope()) + }) + require.NoError(t, err) + + // The encrypted configuration should be different than the one we will send. + encryptedConfig, err := json.Marshal(c) + require.NoError(t, err) + require.NotEqual(t, testGrafanaConfig, encryptedConfig) + // ApplyConfig performs a readiness check at startup. // A non-200 response should result in an error. server := httptest.NewServer(errorHandler) @@ -104,16 +143,19 @@ func TestApplyConfig(t *testing.T) { } ctx := context.Background() - store := fakes.NewFakeKVStore(t) + store := ngfakes.NewFakeKVStore(t) fstore := notifier.NewFileStore(1, store, "") require.NoError(t, store.Set(ctx, cfg.OrgID, "alertmanager", notifier.SilencesFilename, "test")) require.NoError(t, store.Set(ctx, cfg.OrgID, "alertmanager", notifier.NotificationLogFilename, "test")) + // An error response from the remote Alertmanager should result in the readiness check failing. m := metrics.NewRemoteAlertmanagerMetrics(prometheus.NewRegistry()) - am, err := NewAlertmanager(cfg, fstore, m) + am, err := NewAlertmanager(cfg, fstore, secretsService.Decrypt, m) require.NoError(t, err) - config := &ngmodels.AlertConfiguration{} + config := &ngmodels.AlertConfiguration{ + AlertmanagerConfiguration: string(encryptedConfig), + } require.Error(t, am.ApplyConfig(ctx, config)) require.False(t, am.Ready()) @@ -122,12 +164,104 @@ func TestApplyConfig(t *testing.T) { require.NoError(t, am.ApplyConfig(ctx, config)) require.True(t, am.Ready()) + // Secrets in the sent configuration should be unencrypted. + require.JSONEq(t, testGrafanaConfigWithSecret, configSent) + // If we already got a 200 status code response, we shouldn't make the HTTP request again. server.Config.Handler = errorHandler require.NoError(t, am.ApplyConfig(ctx, config)) require.True(t, am.Ready()) } +func TestCompareAndSendConfiguration(t *testing.T) { + testValue := []byte("test") + testErr := errors.New("test error") + decryptFn := func(_ context.Context, payload []byte) ([]byte, error) { + if string(payload) == string(testValue) { + return testValue, nil + } + return nil, testErr + } + + var got string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("content-type", "application/json") + + b, err := io.ReadAll(r.Body) + require.NoError(t, err) + require.NoError(t, r.Body.Close()) + got = string(b) + + _, err = w.Write([]byte(`{"status": "success"}`)) + require.NoError(t, err) + })) + + fstore := notifier.NewFileStore(1, ngfakes.NewFakeKVStore(t), "") + m := metrics.NewRemoteAlertmanagerMetrics(prometheus.NewRegistry()) + cfg := AlertmanagerConfig{ + OrgID: 1, + TenantID: "test", + URL: server.URL, + } + am, err := NewAlertmanager(cfg, + fstore, + decryptFn, + m, + ) + require.NoError(t, err) + + tests := []struct { + name string + config string + expCfg *client.UserGrafanaConfig + expErr string + }{ + { + "invalid config", + "{}", + nil, + "unable to parse Alertmanager configuration: no route provided in config", + }, + { + "invalid base-64 in key", + strings.Replace(testGrafanaConfigWithSecret, `"password":"test"`, `"password":"!"`, 1), + nil, + "failed to decode value for key 'password': illegal base64 data at input byte 0", + }, + { + "decrypt error", + testGrafanaConfigWithSecret, + nil, + fmt.Sprintf("failed to decrypt value for key 'password': %s", testErr.Error()), + }, + { + "no error", + strings.Replace(testGrafanaConfigWithSecret, `"password":"test"`, fmt.Sprintf("%q:%q", "password", base64.StdEncoding.EncodeToString(testValue)), 1), + &client.UserGrafanaConfig{ + GrafanaAlertmanagerConfig: testGrafanaConfigWithSecret, + }, + "", + }, + } + + for _, test := range tests { + t.Run(test.name, func(tt *testing.T) { + cfg := ngmodels.AlertConfiguration{ + AlertmanagerConfiguration: test.config, + } + err = am.CompareAndSendConfiguration(context.Background(), &cfg) + if test.expErr == "" { + require.NoError(tt, err) + rawCfg, err := json.Marshal(test.expCfg) + require.NoError(tt, err) + require.JSONEq(tt, string(rawCfg), got) + return + } + require.Equal(tt, test.expErr, err.Error()) + }) + } +} + func TestIntegrationRemoteAlertmanagerApplyConfigOnlyUploadsOnce(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") @@ -162,7 +296,7 @@ func TestIntegrationRemoteAlertmanagerApplyConfigOnlyUploadsOnce(t *testing.T) { silences := []byte("test-silences") nflog := []byte("test-notifications") - store := fakes.NewFakeKVStore(t) + store := ngfakes.NewFakeKVStore(t) fstore := notifier.NewFileStore(cfg.OrgID, store, "") ctx := context.Background() @@ -179,8 +313,9 @@ func TestIntegrationRemoteAlertmanagerApplyConfigOnlyUploadsOnce(t *testing.T) { require.NoError(t, err) encodedFullState := base64.StdEncoding.EncodeToString(fullState) + secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) m := metrics.NewRemoteAlertmanagerMetrics(prometheus.NewRegistry()) - am, err := NewAlertmanager(cfg, fstore, m) + am, err := NewAlertmanager(cfg, fstore, secretsService.Decrypt, m) require.NoError(t, err) // We should have no configuration or state at first. @@ -264,8 +399,10 @@ func TestIntegrationRemoteAlertmanagerSilences(t *testing.T) { TenantID: tenantID, BasicAuthPassword: password, } + + secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) m := metrics.NewRemoteAlertmanagerMetrics(prometheus.NewRegistry()) - am, err := NewAlertmanager(cfg, nil, m) + am, err := NewAlertmanager(cfg, nil, secretsService.Decrypt, m) require.NoError(t, err) // We should have no silences at first. @@ -345,8 +482,10 @@ func TestIntegrationRemoteAlertmanagerAlerts(t *testing.T) { TenantID: tenantID, BasicAuthPassword: password, } + + secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) m := metrics.NewRemoteAlertmanagerMetrics(prometheus.NewRegistry()) - am, err := NewAlertmanager(cfg, nil, m) + am, err := NewAlertmanager(cfg, nil, secretsService.Decrypt, m) require.NoError(t, err) // Wait until the Alertmanager is ready to send alerts. @@ -412,8 +551,9 @@ func TestIntegrationRemoteAlertmanagerReceivers(t *testing.T) { BasicAuthPassword: password, } + secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) m := metrics.NewRemoteAlertmanagerMetrics(prometheus.NewRegistry()) - am, err := NewAlertmanager(cfg, nil, m) + am, err := NewAlertmanager(cfg, nil, secretsService.Decrypt, m) require.NoError(t, err) // We should start with the default config. From bc0f054fa7863d07423c4c25b2f0b2a4ef7802fd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 19 Mar 2024 11:00:53 +0000 Subject: [PATCH 0789/1406] Update dependency @braintree/sanitize-url to v7.0.1 --- packages/grafana-data/package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index 388f309f9c..63d2d85c1f 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": "7.0.0", + "@braintree/sanitize-url": "7.0.1", "@grafana/schema": "11.0.0-pre", "@types/d3-interpolate": "^3.0.0", "@types/string-hash": "1.1.3", diff --git a/yarn.lock b/yarn.lock index ee9203dc94..a832f48fa7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1873,10 +1873,10 @@ __metadata: languageName: node linkType: hard -"@braintree/sanitize-url@npm:7.0.0": - version: 7.0.0 - resolution: "@braintree/sanitize-url@npm:7.0.0" - checksum: 10/670e218cf1dbda1ceeedbb06487f4178848c681f0612468574688e02dcddd54456e85f94bc4a531faf2caaf01e3dbe164b0ef57188f9df21a4d4d58db099f0a5 +"@braintree/sanitize-url@npm:7.0.1": + version: 7.0.1 + resolution: "@braintree/sanitize-url@npm:7.0.1" + checksum: 10/5e01e6faf81b2289c8e7829aed185c834a88d501c51f569a9f2bc72549d4f2367eeb6f8a9e8039fbe3ef52b094c6afc32dc8272185c687ab6812340b1249cf4d languageName: node linkType: hard @@ -3525,7 +3525,7 @@ __metadata: version: 0.0.0-use.local resolution: "@grafana/data@workspace:packages/grafana-data" dependencies: - "@braintree/sanitize-url": "npm:7.0.0" + "@braintree/sanitize-url": "npm:7.0.1" "@grafana/schema": "npm:11.0.0-pre" "@grafana/tsconfig": "npm:^1.3.0-rc1" "@rollup/plugin-node-resolve": "npm:15.2.3" From f3337b96b815c5aad0b6cb306cad830ab7453cad Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 19 Mar 2024 13:36:35 +0200 Subject: [PATCH 0790/1406] Update dependency @manypkg/get-packages to v2.2.1 (#84722) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index a832f48fa7..03b98a1db3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5039,12 +5039,12 @@ __metadata: linkType: hard "@manypkg/get-packages@npm:^2.2.0": - version: 2.2.0 - resolution: "@manypkg/get-packages@npm:2.2.0" + version: 2.2.1 + resolution: "@manypkg/get-packages@npm:2.2.1" dependencies: "@manypkg/find-root": "npm:^2.2.0" "@manypkg/tools": "npm:^1.1.0" - checksum: 10/2a78240d8d9cf32250204d02e7b693ad728129be0f01c6049593e0c90ad44eca0e3334521faca8eff42939219313517173b5c983be036ff7ef3218bc8b7f8f30 + checksum: 10/648da51cf0bf301cfb133b51b816f04f7caf3c29c386c7fb38a106e6ffdc00823e6aa54b3f72eca49c7a64a1fdf00d55c85095ca9ebcbadfa02b89f6acd4fcdb languageName: node linkType: hard From 4a50897f3952a618dfda5b1b3f7ee328f7eb889b Mon Sep 17 00:00:00 2001 From: Mathieu Parent Date: Tue, 19 Mar 2024 11:41:27 +0000 Subject: [PATCH 0791/1406] CI: Fix missing vendor dependencies (#84375) Fixes: #84342 --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index d17a384ff3..2561a1fb13 100644 --- a/Dockerfile +++ b/Dockerfile @@ -57,6 +57,7 @@ COPY .bingo .bingo COPY pkg/util/xorm/go.* pkg/util/xorm/ COPY pkg/apiserver/go.* pkg/apiserver/ COPY pkg/apimachinery/go.* pkg/apimachinery/ +COPY pkg/promlib/go.* pkg/promlib/ RUN go mod download RUN if [[ "$BINGO" = "true" ]]; then \ From 365fd2cb721d326f011e57ecaf9dba57cd9a16dc Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Tue, 19 Mar 2024 11:44:52 +0000 Subject: [PATCH 0792/1406] FolderPicker: Add permission filter to nested folder picker (#84644) * FolderPicker: Add permission filter to nested folder picker * Add permission filtering to Searcher * make default edit only * dashlist view folders * update tests --- .../NestedFolderPicker.test.tsx | 61 ++++++++++++------- .../NestedFolderPicker/NestedFolderPicker.tsx | 14 +++-- .../NestedFolderPicker/useFoldersQuery.ts | 19 +++--- .../api/browseDashboardsAPI.ts | 7 ++- .../fixtures/dashboardsTreeItem.fixture.ts | 12 ++++ public/app/features/search/service/sql.ts | 3 + public/app/features/search/service/types.ts | 2 + public/app/plugins/panel/dashlist/module.tsx | 10 ++- 8 files changed, 92 insertions(+), 36 deletions(-) diff --git a/public/app/core/components/NestedFolderPicker/NestedFolderPicker.test.tsx b/public/app/core/components/NestedFolderPicker/NestedFolderPicker.test.tsx index a6cb752fb5..c19e722151 100644 --- a/public/app/core/components/NestedFolderPicker/NestedFolderPicker.test.tsx +++ b/public/app/core/components/NestedFolderPicker/NestedFolderPicker.test.tsx @@ -8,33 +8,23 @@ import { TestProvider } from 'test/helpers/TestProvider'; import { config } from '@grafana/runtime'; import { backendSrv } from 'app/core/services/backend_srv'; +import { PermissionLevelString } from 'app/types'; -import { wellFormedTree } from '../../../features/browse-dashboards/fixtures/dashboardsTreeItem.fixture'; +import { + treeViewersCanEdit, + wellFormedTree, +} from '../../../features/browse-dashboards/fixtures/dashboardsTreeItem.fixture'; import { NestedFolderPicker } from './NestedFolderPicker'; const [mockTree, { folderA, folderB, folderC, folderA_folderA, folderA_folderB }] = wellFormedTree(); +const [mockTreeThatViewersCanEdit /* shares folders with wellFormedTree */] = treeViewersCanEdit(); jest.mock('@grafana/runtime', () => ({ ...jest.requireActual('@grafana/runtime'), getBackendSrv: () => backendSrv, })); -jest.mock('app/features/browse-dashboards/api/services', () => { - const orig = jest.requireActual('app/features/browse-dashboards/api/services'); - - return { - ...orig, - listFolders(parentUID?: string) { - const childrenForUID = mockTree - .filter((v) => v.item.kind === 'folder' && v.item.parentUID === parentUID) - .map((v) => v.item); - - return Promise.resolve(childrenForUID); - }, - }; -}); - function render(...[ui, options]: Parameters) { rtlRender({ui}, options); } @@ -58,12 +48,15 @@ describe('NestedFolderPicker', () => { http.get('/api/folders', ({ request }) => { const url = new URL(request.url); const parentUid = url.searchParams.get('parentUid') ?? undefined; + const permission = url.searchParams.get('permission'); const limit = parseInt(url.searchParams.get('limit') ?? '1000', 10); const page = parseInt(url.searchParams.get('page') ?? '1', 10); + const tree = permission === 'Edit' ? mockTreeThatViewersCanEdit : mockTree; + // reconstruct a folder API response from the flat tree fixture - const folders = mockTree + const folders = tree .filter((v) => v.item.kind === 'folder' && v.item.parentUID === parentUid) .map((folder) => { return { @@ -117,7 +110,7 @@ describe('NestedFolderPicker', () => { expect(screen.getByPlaceholderText('Search folders')).toBeInTheDocument(); expect(screen.getByLabelText('Dashboards')).toBeInTheDocument(); expect(screen.getByLabelText(folderA.item.title)).toBeInTheDocument(); - expect(screen.getByLabelText(folderB.item.title)).toBeInTheDocument(); + // expect(screen.getByLabelText(folderB.item.title)).toBeInTheDocument(); expect(screen.getByLabelText(folderC.item.title)).toBeInTheDocument(); }); @@ -174,14 +167,36 @@ describe('NestedFolderPicker', () => { }); it('hides folders specififed by UID', async () => { - render(); + render(); // Open the picker and wait for children to load const button = await screen.findByRole('button', { name: 'Select folder' }); await userEvent.click(button); - await screen.findByLabelText(folderB.item.title); + await screen.findByLabelText(folderA.item.title); + + expect(screen.queryByLabelText(folderC.item.title)).not.toBeInTheDocument(); + }); + + it('by default only shows items the user can edit', async () => { + render(); + + const button = await screen.findByRole('button', { name: 'Select folder' }); + await userEvent.click(button); + await screen.findByLabelText(folderA.item.title); - expect(screen.queryByLabelText(folderA.item.title)).not.toBeInTheDocument(); + expect(screen.queryByLabelText(folderB.item.title)).not.toBeInTheDocument(); // folderB is not editable + expect(screen.getByLabelText(folderC.item.title)).toBeInTheDocument(); // but folderC is + }); + + it('shows items the user can view, with the prop', async () => { + render(); + + const button = await screen.findByRole('button', { name: 'Select folder' }); + await userEvent.click(button); + await screen.findByLabelText(folderA.item.title); + + expect(screen.getByLabelText(folderB.item.title)).toBeInTheDocument(); + expect(screen.getByLabelText(folderC.item.title)).toBeInTheDocument(); }); describe('when nestedFolders is enabled', () => { @@ -196,7 +211,7 @@ describe('NestedFolderPicker', () => { }); it('can expand and collapse a folder to show its children', async () => { - render(); + render(); // Open the picker and wait for children to load const button = await screen.findByRole('button', { name: 'Select folder' }); @@ -227,7 +242,7 @@ describe('NestedFolderPicker', () => { }); it('can expand and collapse a folder to show its children with the keyboard', async () => { - render(); + render(); const button = await screen.findByRole('button', { name: 'Select folder' }); await userEvent.click(button); diff --git a/public/app/core/components/NestedFolderPicker/NestedFolderPicker.tsx b/public/app/core/components/NestedFolderPicker/NestedFolderPicker.tsx index 1f2ab2739d..e9ee8eb957 100644 --- a/public/app/core/components/NestedFolderPicker/NestedFolderPicker.tsx +++ b/public/app/core/components/NestedFolderPicker/NestedFolderPicker.tsx @@ -12,6 +12,7 @@ import { DashboardViewItemWithUIItems, DashboardsTreeItem } from 'app/features/b import { QueryResponse, getGrafanaSearcher } from 'app/features/search/service'; import { queryResultToViewItem } from 'app/features/search/service/utils'; import { DashboardViewItem } from 'app/features/search/types'; +import { PermissionLevelString } from 'app/types'; import { getDOMId, NestedFolderList } from './NestedFolderList'; import Trigger from './Trigger'; @@ -31,6 +32,9 @@ export interface NestedFolderPickerProps { /* Folder UIDs to exclude from the picker, to prevent invalid operations */ excludeUIDs?: string[]; + /* Show folders matching this permission, mainly used to also show folders user can view. Defaults to showing only folders user has Edit */ + permission?: PermissionLevelString.View | PermissionLevelString.Edit; + /* Callback for when the user selects a folder */ onChange?: (folderUID: string | undefined, folderName: string | undefined) => void; @@ -40,11 +44,12 @@ export interface NestedFolderPickerProps { const debouncedSearch = debounce(getSearchResults, 300); -async function getSearchResults(searchQuery: string) { +async function getSearchResults(searchQuery: string, permission?: PermissionLevelString) { const queryResponse = await getGrafanaSearcher().search({ query: searchQuery, kind: ['folder'], limit: 100, + permission: permission, }); const items = queryResponse.view.map((v) => queryResultToViewItem(v, queryResponse.view)); @@ -57,6 +62,7 @@ export function NestedFolderPicker({ showRootFolder = true, clearable = false, excludeUIDs, + permission = PermissionLevelString.Edit, onChange, }: NestedFolderPickerProps) { const styles = useStyles2(getStyles); @@ -79,7 +85,7 @@ export function NestedFolderPicker({ items: browseFlatTree, isLoading: isBrowseLoading, requestNextPage: fetchFolderPage, - } = useFoldersQuery(isBrowsing, foldersOpenState); + } = useFoldersQuery(isBrowsing, foldersOpenState, permission); useEffect(() => { if (!search) { @@ -90,7 +96,7 @@ export function NestedFolderPicker({ const timestamp = Date.now(); setIsFetchingSearchResults(true); - debouncedSearch(search).then((queryResponse) => { + debouncedSearch(search, permission).then((queryResponse) => { // Only keep the results if it's was issued after the most recently resolved search. // This prevents results showing out of order if first request is slower than later ones. // We don't need to worry about clearing the isFetching state either - if there's a later @@ -102,7 +108,7 @@ export function NestedFolderPicker({ lastSearchTimestamp.current = timestamp; } }); - }, [search]); + }, [search, permission]); // the order of middleware is important! const middleware = [ diff --git a/public/app/core/components/NestedFolderPicker/useFoldersQuery.ts b/public/app/core/components/NestedFolderPicker/useFoldersQuery.ts index 528ca80ab1..6a89b5e1be 100644 --- a/public/app/core/components/NestedFolderPicker/useFoldersQuery.ts +++ b/public/app/core/components/NestedFolderPicker/useFoldersQuery.ts @@ -9,7 +9,7 @@ import { PAGE_SIZE } from 'app/features/browse-dashboards/api/services'; import { getPaginationPlaceholders } from 'app/features/browse-dashboards/state/utils'; import { DashboardViewItemWithUIItems, DashboardsTreeItem } from 'app/features/browse-dashboards/types'; import { RootState } from 'app/store/configureStore'; -import { FolderListItemDTO } from 'app/types'; +import { FolderListItemDTO, PermissionLevelString } from 'app/types'; import { useDispatch, useSelector } from 'app/types/store'; type ListFoldersQuery = ReturnType>; @@ -29,8 +29,9 @@ const listFoldersSelector = createSelector( state: RootState, parentUid: ListFolderQueryArgs['parentUid'], page: ListFolderQueryArgs['page'], - limit: ListFolderQueryArgs['limit'] - ) => browseDashboardsAPI.endpoints.listFolders.select({ parentUid, page, limit }), + limit: ListFolderQueryArgs['limit'], + permission: ListFolderQueryArgs['permission'] + ) => browseDashboardsAPI.endpoints.listFolders.select({ parentUid, page, limit, permission }), (state, selectFolderList) => selectFolderList(state) ); @@ -48,7 +49,7 @@ const listAllFoldersSelector = createSelector( continue; } - const page = listFoldersSelector(state, req.arg.parentUid, req.arg.page, req.arg.limit); + const page = listFoldersSelector(state, req.arg.parentUid, req.arg.page, req.arg.limit, req.arg.permission); if (page.status === 'pending') { isLoading = true; } @@ -91,7 +92,11 @@ function getPagesLoadStatus(pages: ListFoldersQuery[]): [boolean, number | undef /** * Returns a loaded folder hierarchy as a flat list and a function to load more pages. */ -export function useFoldersQuery(isBrowsing: boolean, openFolders: Record) { +export function useFoldersQuery( + isBrowsing: boolean, + openFolders: Record, + permission?: PermissionLevelString +) { const dispatch = useDispatch(); // Keep a list of all requests so we can @@ -113,13 +118,13 @@ export function useFoldersQuery(isBrowsing: boolean, openFolders: Record ({ listFolders: builder.query({ providesTags: (result) => result?.map((folder) => ({ type: 'getFolder', id: folder.uid })) ?? [], - query: ({ page, parentUid, limit }) => ({ url: '/folders', params: { page, parentUid, limit } }), + query: ({ parentUid, limit, page, permission }) => ({ + url: '/folders', + params: { parentUid, limit, page, permission }, + }), }), // get folder info (e.g. title, parents) but *not* children diff --git a/public/app/features/browse-dashboards/fixtures/dashboardsTreeItem.fixture.ts b/public/app/features/browse-dashboards/fixtures/dashboardsTreeItem.fixture.ts index 9de6e59c06..4ac6515c72 100644 --- a/public/app/features/browse-dashboards/fixtures/dashboardsTreeItem.fixture.ts +++ b/public/app/features/browse-dashboards/fixtures/dashboardsTreeItem.fixture.ts @@ -73,6 +73,18 @@ export function sharedWithMeFolder(seed = 1): DashboardsTreeItem(DashList) id: 'folderUID', defaultValue: undefined, editor: function RenderFolderPicker({ value, onChange }) { - return onChange(folderUID)} />; + return ( + onChange(folderUID)} + /> + ); }, }) .addCustomEditor({ From 4ad6d66479279d4ba54f50df3e043694e1324e8d Mon Sep 17 00:00:00 2001 From: Santiago Date: Tue, 19 Mar 2024 12:47:13 +0100 Subject: [PATCH 0793/1406] Alerting: Remove ID from UserGrafanaConfig struct (#84602) * Alerting: Remove ID from UserGrafanaConfig struct * user custom mimir image withoud id in grafana config * change mimir image name --- .drone.yml | 14 +++++++------- .../blocks/mimir_backend/docker-compose.yaml | 2 +- pkg/services/ngalert/remote/alertmanager.go | 1 - pkg/services/ngalert/remote/alertmanager_test.go | 3 --- .../remote/client/alertmanager_configuration.go | 4 +--- pkg/services/ngalert/remote/client/mimir.go | 2 +- scripts/drone/utils/images.star | 2 +- 7 files changed, 11 insertions(+), 17 deletions(-) diff --git a/.drone.yml b/.drone.yml index 4443759a20..c25c8308c3 100644 --- a/.drone.yml +++ b/.drone.yml @@ -877,7 +877,7 @@ services: - commands: - /bin/mimir -target=backend -alertmanager.grafana-alertmanager-compatibility-enabled environment: {} - image: grafana/mimir:r274-1780c50 + image: us.gcr.io/kubernetes-dev/mimir:santihernandezc-remove_id_from_grafana_config-d3826b4f8-WIP name: mimir_backend - environment: {} image: redis:6.2.11-alpine @@ -1325,7 +1325,7 @@ services: - commands: - /bin/mimir -target=backend -alertmanager.grafana-alertmanager-compatibility-enabled environment: {} - image: grafana/mimir:r274-1780c50 + image: us.gcr.io/kubernetes-dev/mimir:santihernandezc-remove_id_from_grafana_config-d3826b4f8-WIP name: mimir_backend - environment: {} image: redis:6.2.11-alpine @@ -2326,7 +2326,7 @@ services: - commands: - /bin/mimir -target=backend -alertmanager.grafana-alertmanager-compatibility-enabled environment: {} - image: grafana/mimir:r274-1780c50 + image: us.gcr.io/kubernetes-dev/mimir:santihernandezc-remove_id_from_grafana_config-d3826b4f8-WIP name: mimir_backend - environment: {} image: redis:6.2.11-alpine @@ -4124,7 +4124,7 @@ services: - commands: - /bin/mimir -target=backend -alertmanager.grafana-alertmanager-compatibility-enabled environment: {} - image: grafana/mimir:r274-1780c50 + image: us.gcr.io/kubernetes-dev/mimir:santihernandezc-remove_id_from_grafana_config-d3826b4f8-WIP name: mimir_backend - environment: {} image: redis:6.2.11-alpine @@ -4642,7 +4642,7 @@ steps: - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM plugins/slack - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM python:3.8 - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM postgres:12.3-alpine - - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM grafana/mimir:r274-1780c50 + - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM us.gcr.io/kubernetes-dev/mimir:santihernandezc-remove_id_from_grafana_config-d3826b4f8-WIP - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM mysql:5.7.39 - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM mysql:8.0.32 - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM redis:6.2.11-alpine @@ -4677,7 +4677,7 @@ steps: - trivy --exit-code 1 --severity HIGH,CRITICAL plugins/slack - trivy --exit-code 1 --severity HIGH,CRITICAL python:3.8 - trivy --exit-code 1 --severity HIGH,CRITICAL postgres:12.3-alpine - - trivy --exit-code 1 --severity HIGH,CRITICAL grafana/mimir:r274-1780c50 + - trivy --exit-code 1 --severity HIGH,CRITICAL us.gcr.io/kubernetes-dev/mimir:santihernandezc-remove_id_from_grafana_config-d3826b4f8-WIP - trivy --exit-code 1 --severity HIGH,CRITICAL mysql:5.7.39 - trivy --exit-code 1 --severity HIGH,CRITICAL mysql:8.0.32 - trivy --exit-code 1 --severity HIGH,CRITICAL redis:6.2.11-alpine @@ -4922,6 +4922,6 @@ kind: secret name: gcr_credentials --- kind: signature -hmac: feb0603ccd1169c54e142a4b0105bc6d2805e5e0f4d8f0e1b0edd95d41d450b9 +hmac: 474420078e68f3b51a76abb99ba723671130f524ed00c42e66169c6912b6fbba ... diff --git a/devenv/docker/blocks/mimir_backend/docker-compose.yaml b/devenv/docker/blocks/mimir_backend/docker-compose.yaml index 058a0d8608..2a9c2dbd98 100644 --- a/devenv/docker/blocks/mimir_backend/docker-compose.yaml +++ b/devenv/docker/blocks/mimir_backend/docker-compose.yaml @@ -1,5 +1,5 @@ mimir_backend: - image: grafana/mimir:r274-1780c50 + image: us.gcr.io/kubernetes-dev/mimir:santihernandezc-remove_id_from_grafana_config-d3826b4f8-WIP container_name: mimir_backend command: - -target=backend diff --git a/pkg/services/ngalert/remote/alertmanager.go b/pkg/services/ngalert/remote/alertmanager.go index 496bd26c53..d69469da7b 100644 --- a/pkg/services/ngalert/remote/alertmanager.go +++ b/pkg/services/ngalert/remote/alertmanager.go @@ -210,7 +210,6 @@ func (am *Alertmanager) CompareAndSendConfiguration(ctx context.Context, config ctx, string(rawDecrypted), config.ConfigurationHash, - config.ID, config.CreatedAt, config.Default, ); err != nil { diff --git a/pkg/services/ngalert/remote/alertmanager_test.go b/pkg/services/ngalert/remote/alertmanager_test.go index a500fcbf27..cd8c094671 100644 --- a/pkg/services/ngalert/remote/alertmanager_test.go +++ b/pkg/services/ngalert/remote/alertmanager_test.go @@ -285,7 +285,6 @@ func TestIntegrationRemoteAlertmanagerApplyConfigOnlyUploadsOnce(t *testing.T) { fakeConfigHash := fmt.Sprintf("%x", md5.Sum([]byte(testGrafanaConfig))) fakeConfigCreatedAt := time.Date(2020, 6, 5, 12, 6, 0, 0, time.UTC).Unix() fakeConfig := &ngmodels.AlertConfiguration{ - ID: 100, AlertmanagerConfiguration: testGrafanaConfig, ConfigurationHash: fakeConfigHash, ConfigurationVersion: "v2", @@ -340,7 +339,6 @@ func TestIntegrationRemoteAlertmanagerApplyConfigOnlyUploadsOnce(t *testing.T) { // Next, we need to verify that Mimir received both the configuration and state. config, err := am.mimirClient.GetGrafanaAlertmanagerConfig(ctx) require.NoError(t, err) - require.Equal(t, int64(100), config.ID) require.Equal(t, testGrafanaConfig, config.GrafanaAlertmanagerConfig) require.Equal(t, fakeConfigHash, config.Hash) require.Equal(t, fakeConfigCreatedAt, config.CreatedAt) @@ -364,7 +362,6 @@ func TestIntegrationRemoteAlertmanagerApplyConfigOnlyUploadsOnce(t *testing.T) { // Next, we need to verify that the config that was uploaded remains the same. config, err := am.mimirClient.GetGrafanaAlertmanagerConfig(ctx) require.NoError(t, err) - require.Equal(t, int64(100), config.ID) require.Equal(t, testGrafanaConfig, config.GrafanaAlertmanagerConfig) require.Equal(t, fakeConfigHash, config.Hash) require.Equal(t, fakeConfigCreatedAt, config.CreatedAt) diff --git a/pkg/services/ngalert/remote/client/alertmanager_configuration.go b/pkg/services/ngalert/remote/client/alertmanager_configuration.go index dd8ab90318..6ddf6dfdaa 100644 --- a/pkg/services/ngalert/remote/client/alertmanager_configuration.go +++ b/pkg/services/ngalert/remote/client/alertmanager_configuration.go @@ -13,7 +13,6 @@ const ( ) type UserGrafanaConfig struct { - ID int64 `json:"id"` GrafanaAlertmanagerConfig string `json:"configuration"` Hash string `json:"configuration_hash"` CreatedAt int64 `json:"created"` @@ -39,9 +38,8 @@ func (mc *Mimir) GetGrafanaAlertmanagerConfig(ctx context.Context) (*UserGrafana return gc, nil } -func (mc *Mimir) CreateGrafanaAlertmanagerConfig(ctx context.Context, cfg, hash string, id, createdAt int64, isDefault bool) error { +func (mc *Mimir) CreateGrafanaAlertmanagerConfig(ctx context.Context, cfg, hash string, createdAt int64, isDefault bool) error { payload, err := json.Marshal(&UserGrafanaConfig{ - ID: id, GrafanaAlertmanagerConfig: cfg, Hash: hash, CreatedAt: createdAt, diff --git a/pkg/services/ngalert/remote/client/mimir.go b/pkg/services/ngalert/remote/client/mimir.go index c4e0d96dc7..b74d1df91f 100644 --- a/pkg/services/ngalert/remote/client/mimir.go +++ b/pkg/services/ngalert/remote/client/mimir.go @@ -23,7 +23,7 @@ type MimirClient interface { DeleteGrafanaAlertmanagerState(ctx context.Context) error GetGrafanaAlertmanagerConfig(ctx context.Context) (*UserGrafanaConfig, error) - CreateGrafanaAlertmanagerConfig(ctx context.Context, configuration, hash string, id, updatedAt int64, isDefault bool) error + CreateGrafanaAlertmanagerConfig(ctx context.Context, configuration, hash string, createdAt int64, isDefault bool) error DeleteGrafanaAlertmanagerConfig(ctx context.Context) error } diff --git a/scripts/drone/utils/images.star b/scripts/drone/utils/images.star index d2a1fae7d5..dcdca5a23e 100644 --- a/scripts/drone/utils/images.star +++ b/scripts/drone/utils/images.star @@ -20,7 +20,7 @@ images = { "plugins_slack": "plugins/slack", "python": "python:3.8", "postgres_alpine": "postgres:12.3-alpine", - "mimir": "grafana/mimir:r274-1780c50", + "mimir": "us.gcr.io/kubernetes-dev/mimir:santihernandezc-remove_id_from_grafana_config-d3826b4f8-WIP", "mysql5": "mysql:5.7.39", "mysql8": "mysql:8.0.32", "redis_alpine": "redis:6.2.11-alpine", From a095888522e3ca21deea7bef2453d4687b218db3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 19 Mar 2024 13:12:46 +0100 Subject: [PATCH 0794/1406] DashboardScene: Fixes visualization suggestions (#84439) * DashboardScene: Fixes visualization suggestions * Update public/app/features/dashboard-scene/panel-edit/PanelOptionsPane.tsx Co-authored-by: Oscar Kilhed * fix prettier issue --------- Co-authored-by: Oscar Kilhed --- .../features/dashboard-scene/panel-edit/PanelOptions.tsx | 7 ++++--- .../dashboard-scene/panel-edit/PanelOptionsPane.tsx | 7 +++++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/public/app/features/dashboard-scene/panel-edit/PanelOptions.tsx b/public/app/features/dashboard-scene/panel-edit/PanelOptions.tsx index e9088b3453..2c0c4a4a2b 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelOptions.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelOptions.tsx @@ -1,6 +1,6 @@ import React, { useMemo } from 'react'; -import { sceneGraph } from '@grafana/scenes'; +import { PanelData } from '@grafana/data'; import { OptionFilter, renderSearchHits } from 'app/features/dashboard/components/PanelEditor/OptionsPaneOptions'; import { getFieldOverrideCategories } from 'app/features/dashboard/components/PanelEditor/getFieldOverrideElements'; import { getPanelFrameCategory2 } from 'app/features/dashboard/components/PanelEditor/getPanelFrameOptions'; @@ -17,12 +17,12 @@ interface Props { vizManager: VizPanelManager; searchQuery: string; listMode: OptionFilter; + data?: PanelData; } -export const PanelOptions = React.memo(({ vizManager, searchQuery, listMode }) => { +export const PanelOptions = React.memo(({ vizManager, searchQuery, listMode, data }) => { const { panel, sourcePanel, repeat } = vizManager.useState(); const parent = sourcePanel.resolve().parent; - const { data } = sceneGraph.getData(panel).useState(); const { options, fieldConfig } = panel.useState(); // eslint-disable-next-line react-hooks/exhaustive-deps @@ -39,6 +39,7 @@ export const PanelOptions = React.memo(({ vizManager, searchQuery, listMo return getVisualizationOptions2({ panel, + data, plugin: plugin, eventBus: panel.getPanelContext().eventBus, instanceState: panel.getPanelContext().instanceState!, diff --git a/public/app/features/dashboard-scene/panel-edit/PanelOptionsPane.tsx b/public/app/features/dashboard-scene/panel-edit/PanelOptionsPane.tsx index 0678f10734..5071652c66 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelOptionsPane.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelOptionsPane.tsx @@ -43,6 +43,7 @@ export class PanelOptionsPane extends SceneObjectBase { const { isVizPickerOpen, searchQuery, listMode } = model.useState(); const vizManager = sceneGraph.getAncestor(model, PanelEditor).state.vizManager; const { pluginId } = vizManager.state.panel.useState(); + const { data } = sceneGraph.getData(vizManager).useState(); const styles = useStyles2(getStyles); return ( @@ -59,11 +60,13 @@ export class PanelOptionsPane extends SceneObjectBase { />
    - +
    )} - {isVizPickerOpen && } + {isVizPickerOpen && ( + + )} ); }; From b38436eeb09bf92cb499ef3b1a31088816395a10 Mon Sep 17 00:00:00 2001 From: Sonia Aguilar <33540275+soniaAguilarPeiron@users.noreply.github.com> Date: Tue, 19 Mar 2024 13:30:06 +0100 Subject: [PATCH 0795/1406] Alerting: Fix used badge in contact points list (#84714) Fix used badge in contact points list --- .../components/contact-points/components/UnusedBadge.tsx | 3 ++- .../alerting/unified/components/contact-points/utils.ts | 3 +-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/public/app/features/alerting/unified/components/contact-points/components/UnusedBadge.tsx b/public/app/features/alerting/unified/components/contact-points/components/UnusedBadge.tsx index 73c11402c9..8d459985ee 100644 --- a/public/app/features/alerting/unified/components/contact-points/components/UnusedBadge.tsx +++ b/public/app/features/alerting/unified/components/contact-points/components/UnusedBadge.tsx @@ -8,6 +8,7 @@ export const UnusedContactPointBadge = () => ( aria-label="unused" color="orange" icon="exclamation-triangle" - tooltip="This contact point is not used in any notification policy and it will not receive any alerts" + // is not used in any policy, but it can receive notifications from an auto auto generated policy. Non admin users can't see auto generated policies. + tooltip="This contact point is not used in any notification policy" /> ); diff --git a/public/app/features/alerting/unified/components/contact-points/utils.ts b/public/app/features/alerting/unified/components/contact-points/utils.ts index 479bde2b49..a07e3cc03d 100644 --- a/public/app/features/alerting/unified/components/contact-points/utils.ts +++ b/public/app/features/alerting/unified/components/contact-points/utils.ts @@ -168,8 +168,7 @@ export function isAutoGeneratedPolicy(route: Route) { export function getUsedContactPoints(route: Route): string[] { const childrenContactPoints = route.routes?.flatMap((route) => getUsedContactPoints(route)) ?? []; - // we don't want to count the autogenerated policy for receiver level, for checking if a contact point is used - if (route.receiver && !isAutoGeneratedPolicy(route)) { + if (route.receiver) { return [route.receiver, ...childrenContactPoints]; } From 83597e6c37d3e70e0369c9eb73ba08514cb65894 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 19 Mar 2024 13:24:10 +0000 Subject: [PATCH 0796/1406] Update dependency @swc/helpers to v0.5.7 (#84725) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- packages/grafana-prometheus/package.json | 2 +- yarn.lock | 12 ++++++------ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 2d5e199e92..07c174b786 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,7 @@ "@react-types/shared": "3.22.1", "@rtsao/plugin-proposal-class-properties": "7.0.1-patch.1", "@swc/core": "1.4.2", - "@swc/helpers": "0.5.6", + "@swc/helpers": "0.5.7", "@testing-library/dom": "9.3.4", "@testing-library/jest-dom": "6.4.2", "@testing-library/react": "14.2.1", diff --git a/packages/grafana-prometheus/package.json b/packages/grafana-prometheus/package.json index 6502a948ac..4ec9febe65 100644 --- a/packages/grafana-prometheus/package.json +++ b/packages/grafana-prometheus/package.json @@ -82,7 +82,7 @@ "@rollup/plugin-image": "3.0.3", "@rollup/plugin-node-resolve": "15.2.3", "@swc/core": "1.4.2", - "@swc/helpers": "0.5.6", + "@swc/helpers": "0.5.7", "@testing-library/dom": "9.3.4", "@testing-library/jest-dom": "6.4.2", "@testing-library/react": "14.2.1", diff --git a/yarn.lock b/yarn.lock index 03b98a1db3..34d66e7cfb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3919,7 +3919,7 @@ __metadata: "@rollup/plugin-image": "npm:3.0.3" "@rollup/plugin-node-resolve": "npm:15.2.3" "@swc/core": "npm:1.4.2" - "@swc/helpers": "npm:0.5.6" + "@swc/helpers": "npm:0.5.7" "@testing-library/dom": "npm:9.3.4" "@testing-library/jest-dom": "npm:6.4.2" "@testing-library/react": "npm:14.2.1" @@ -8555,12 +8555,12 @@ __metadata: languageName: node linkType: hard -"@swc/helpers@npm:0.5.6, @swc/helpers@npm:^0.5.0": - version: 0.5.6 - resolution: "@swc/helpers@npm:0.5.6" +"@swc/helpers@npm:0.5.7, @swc/helpers@npm:^0.5.0": + version: 0.5.7 + resolution: "@swc/helpers@npm:0.5.7" dependencies: tslib: "npm:^2.4.0" - checksum: 10/16f0a18367b1248317dcc3e5f32411da1a2906f983f4f072e394dfed37523385bc4d7bf71bab204cc8b3875c024a91421dd5c1f9c5bad1b1172fcb50aa2ec96f + checksum: 10/f9c4cbd2d59ef86dbe9f955651f1d49561cd897ca113d713f370853ebcc44841712b9b4c674508a314cceadc2ef27cdc0979b36cbb3af8b26b727e345ffe1f2e languageName: node linkType: hard @@ -18314,7 +18314,7 @@ __metadata: "@remix-run/router": "npm:^1.5.0" "@rtsao/plugin-proposal-class-properties": "npm:7.0.1-patch.1" "@swc/core": "npm:1.4.2" - "@swc/helpers": "npm:0.5.6" + "@swc/helpers": "npm:0.5.7" "@testing-library/dom": "npm:9.3.4" "@testing-library/jest-dom": "npm:6.4.2" "@testing-library/react": "npm:14.2.1" From d1f791cf1fef8fddd8fea0dea2d46008b8e45a94 Mon Sep 17 00:00:00 2001 From: Fabrizio <135109076+fabrizio-grafana@users.noreply.github.com> Date: Tue, 19 Mar 2024 14:39:31 +0100 Subject: [PATCH 0797/1406] Jaeger: Decouple Jaeger plugin (#81377) --- .betterer.results | 10 + .eslintrc | 14 +- .../core-plugins-build-and-release.yml | 1 + .../api/plugins/data/expectedListResp.json | 2 +- .../app/features/plugins/built_in_plugins.ts | 3 - .../plugins/datasource/jaeger/CHANGELOG.md | 1 + .../app/plugins/datasource/jaeger/README.md | 8 +- .../jaeger/_importedDependencies/README.md | 3 + .../model/trace-viewer.ts | 57 + .../model/transform-trace-data.tsx | 199 +++ .../_importedDependencies/selectors/trace.ts | 65 + .../_importedDependencies/types/index.tsx | 23 + .../_importedDependencies/types/trace.ts | 112 ++ .../_importedDependencies/utils/TreeNode.ts | 139 ++ .../utils/config/default-config.ts | 40 + .../utils/config/get-config.tsx | 29 + .../jaeger/components/SearchForm.test.tsx | 6 +- .../jaeger/components/SearchForm.tsx | 241 +-- .../datasource/jaeger/datasource.test.ts | 69 +- .../plugins/datasource/jaeger/datasource.ts | 7 +- .../jaeger/helpers/createFetchResponse.ts | 15 + .../plugins/datasource/jaeger/package.json | 43 + .../app/plugins/datasource/jaeger/plugin.json | 6 +- .../datasource/jaeger/responseTransform.ts | 2 +- .../plugins/datasource/jaeger/tsconfig.json | 7 + .../datasource/jaeger/webpack.config.ts | 14 + yarn.lock | 1454 ++++++++++++++--- 27 files changed, 2189 insertions(+), 381 deletions(-) create mode 100644 public/app/plugins/datasource/jaeger/CHANGELOG.md create mode 100644 public/app/plugins/datasource/jaeger/_importedDependencies/README.md create mode 100644 public/app/plugins/datasource/jaeger/_importedDependencies/model/trace-viewer.ts create mode 100644 public/app/plugins/datasource/jaeger/_importedDependencies/model/transform-trace-data.tsx create mode 100644 public/app/plugins/datasource/jaeger/_importedDependencies/selectors/trace.ts create mode 100644 public/app/plugins/datasource/jaeger/_importedDependencies/types/index.tsx create mode 100644 public/app/plugins/datasource/jaeger/_importedDependencies/types/trace.ts create mode 100644 public/app/plugins/datasource/jaeger/_importedDependencies/utils/TreeNode.ts create mode 100644 public/app/plugins/datasource/jaeger/_importedDependencies/utils/config/default-config.ts create mode 100644 public/app/plugins/datasource/jaeger/_importedDependencies/utils/config/get-config.tsx create mode 100644 public/app/plugins/datasource/jaeger/helpers/createFetchResponse.ts create mode 100644 public/app/plugins/datasource/jaeger/package.json create mode 100644 public/app/plugins/datasource/jaeger/tsconfig.json create mode 100644 public/app/plugins/datasource/jaeger/webpack.config.ts diff --git a/.betterer.results b/.betterer.results index b36e5fc685..a07a2d0341 100644 --- a/.betterer.results +++ b/.betterer.results @@ -5008,6 +5008,12 @@ exports[`better eslint`] = { [0, 0, 0, "Styles should be written using objects.", "0"], [0, 0, 0, "Styles should be written using objects.", "1"] ], + "public/app/plugins/datasource/jaeger/_importedDependencies/model/transform-trace-data.tsx:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"] + ], + "public/app/plugins/datasource/jaeger/_importedDependencies/types/trace.ts:5381": [ + [0, 0, 0, "Unexpected any. Specify a different type.", "0"] + ], "public/app/plugins/datasource/jaeger/configuration/ConfigEditor.tsx:5381": [ [0, 0, 0, "Styles should be written using objects.", "0"] ], @@ -5019,6 +5025,10 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"] ], + "public/app/plugins/datasource/jaeger/helpers/createFetchResponse.ts:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"], + [0, 0, 0, "Do not use any type assertions.", "1"] + ], "public/app/plugins/datasource/jaeger/testResponse.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"] diff --git a/.eslintrc b/.eslintrc index 07e7cfc406..b64d731e0c 100644 --- a/.eslintrc +++ b/.eslintrc @@ -96,16 +96,22 @@ }, { "files": [ + "public/app/plugins/datasource/azuremonitor/*.{ts,tsx}", + "public/app/plugins/datasource/azuremonitor/**/*.{ts,tsx}", + "public/app/plugins/datasource/cloud-monitoring/*.{ts,tsx}", + "public/app/plugins/datasource/cloud-monitoring/**/*.{ts,tsx}", + "public/app/plugins/datasource/elasticsearch/*.{ts,tsx}", + "public/app/plugins/datasource/elasticsearch/**/*.{ts,tsx}", "public/app/plugins/datasource/grafana-postgresql-datasource/*.{ts,tsx}", "public/app/plugins/datasource/grafana-postgresql-datasource/**/*.{ts,tsx}", "public/app/plugins/datasource/grafana-pyroscope-datasource/*.{ts,tsx}", "public/app/plugins/datasource/grafana-pyroscope-datasource/**/*.{ts,tsx}", "public/app/plugins/datasource/grafana-testdata-datasource/*.{ts,tsx}", "public/app/plugins/datasource/grafana-testdata-datasource/**/*.{ts,tsx}", - "public/app/plugins/datasource/azuremonitor/*.{ts,tsx}", - "public/app/plugins/datasource/azuremonitor/**/*.{ts,tsx}", - "public/app/plugins/datasource/cloud-monitoring/*.{ts,tsx}", - "public/app/plugins/datasource/cloud-monitoring/**/*.{ts,tsx}", + "public/app/plugins/datasource/jaeger/*.{ts,tsx}", + "public/app/plugins/datasource/jaeger/**/*.{ts,tsx}", + "public/app/plugins/datasource/loki/*.{ts,tsx}", + "public/app/plugins/datasource/loki/**/*.{ts,tsx}", "public/app/plugins/datasource/mysql/*.{ts,tsx}", "public/app/plugins/datasource/mysql/**/*.{ts,tsx}", "public/app/plugins/datasource/parca/*.{ts,tsx}", diff --git a/.github/workflows/core-plugins-build-and-release.yml b/.github/workflows/core-plugins-build-and-release.yml index 77046482b9..e4803a2208 100644 --- a/.github/workflows/core-plugins-build-and-release.yml +++ b/.github/workflows/core-plugins-build-and-release.yml @@ -11,6 +11,7 @@ on: - grafana-azure-monitor-datasource - grafana-pyroscope-datasource - grafana-testdata-datasource + - jaeger - parca - stackdriver - tempo diff --git a/pkg/tests/api/plugins/data/expectedListResp.json b/pkg/tests/api/plugins/data/expectedListResp.json index 2e0adc347b..2723c7388e 100644 --- a/pkg/tests/api/plugins/data/expectedListResp.json +++ b/pkg/tests/api/plugins/data/expectedListResp.json @@ -961,7 +961,7 @@ "keywords": null }, "dependencies": { - "grafanaDependency": "", + "grafanaDependency": "\u003e=10.3.0-0", "grafanaVersion": "*", "plugins": [] }, diff --git a/public/app/features/plugins/built_in_plugins.ts b/public/app/features/plugins/built_in_plugins.ts index 38dee1b807..8011403029 100644 --- a/public/app/features/plugins/built_in_plugins.ts +++ b/public/app/features/plugins/built_in_plugins.ts @@ -13,8 +13,6 @@ const grafanaPlugin = async () => const influxdbPlugin = async () => await import(/* webpackChunkName: "influxdbPlugin" */ 'app/plugins/datasource/influxdb/module'); const lokiPlugin = async () => await import(/* webpackChunkName: "lokiPlugin" */ 'app/plugins/datasource/loki/module'); -const jaegerPlugin = async () => - await import(/* webpackChunkName: "jaegerPlugin" */ 'app/plugins/datasource/jaeger/module'); const mixedPlugin = async () => await import(/* webpackChunkName: "mixedPlugin" */ 'app/plugins/datasource/mixed/module'); const mysqlPlugin = async () => @@ -77,7 +75,6 @@ const builtInPlugins: Record Promise = new Set(spans.map(({ spanID }) => spanID)); + + for (let i = 0; i < spans.length; i++) { + const hasInternalRef = + spans[i].references && + spans[i].references.some(({ traceID, spanID }) => traceID === spans[i].traceID && allIDs.has(spanID)); + if (hasInternalRef) { + continue; + } + + if (!candidateSpan) { + candidateSpan = spans[i]; + continue; + } + + const thisRefLength = (spans[i].references && spans[i].references.length) || 0; + const candidateRefLength = (candidateSpan.references && candidateSpan.references.length) || 0; + + if ( + thisRefLength < candidateRefLength || + (thisRefLength === candidateRefLength && spans[i].startTime < candidateSpan.startTime) + ) { + candidateSpan = spans[i]; + } + } + return candidateSpan ? `${candidateSpan.process.serviceName}: ${candidateSpan.operationName}` : ''; +} + +export const getTraceName = memoize(_getTraceNameImpl, (spans: TraceSpan[]) => { + if (!spans.length) { + return 0; + } + return spans[0].traceID; +}); diff --git a/public/app/plugins/datasource/jaeger/_importedDependencies/model/transform-trace-data.tsx b/public/app/plugins/datasource/jaeger/_importedDependencies/model/transform-trace-data.tsx new file mode 100644 index 0000000000..97eb98b50f --- /dev/null +++ b/public/app/plugins/datasource/jaeger/_importedDependencies/model/transform-trace-data.tsx @@ -0,0 +1,199 @@ +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { isEqual as _isEqual } from 'lodash'; + +import { getTraceSpanIdsAsTree } from '../selectors/trace'; +import { TraceKeyValuePair, TraceSpan, Trace, TraceResponse, TraceProcess } from '../types'; +import TreeNode from '../utils/TreeNode'; +import { getConfigValue } from '../utils/config/get-config'; + +import { getTraceName } from './trace-viewer'; + +function deduplicateTags(tags: TraceKeyValuePair[]) { + const warningsHash: Map = new Map(); + const dedupedTags: TraceKeyValuePair[] = tags.reduce((uniqueTags, tag) => { + if (!uniqueTags.some((t) => t.key === tag.key && t.value === tag.value)) { + uniqueTags.push(tag); + } else { + warningsHash.set(`${tag.key}:${tag.value}`, `Duplicate tag "${tag.key}:${tag.value}"`); + } + return uniqueTags; + }, []); + const warnings = Array.from(warningsHash.values()); + return { dedupedTags, warnings }; +} + +function orderTags(tags: TraceKeyValuePair[], topPrefixes?: string[]) { + const orderedTags: TraceKeyValuePair[] = tags?.slice() ?? []; + const tp = (topPrefixes || []).map((p: string) => p.toLowerCase()); + + orderedTags.sort((a, b) => { + const aKey = a.key.toLowerCase(); + const bKey = b.key.toLowerCase(); + + for (let i = 0; i < tp.length; i++) { + const p = tp[i]; + if (aKey.startsWith(p) && !bKey.startsWith(p)) { + return -1; + } + if (!aKey.startsWith(p) && bKey.startsWith(p)) { + return 1; + } + } + + if (aKey > bKey) { + return 1; + } + if (aKey < bKey) { + return -1; + } + return 0; + }); + + return orderedTags; +} + +/** + * NOTE: Mutates `data` - Transform the HTTP response data into the form the app + * generally requires. + */ +export default function transformTraceData(data: TraceResponse | undefined): Trace | null { + if (!data?.traceID) { + return null; + } + const traceID = data.traceID.toLowerCase(); + + let traceEndTime = 0; + let traceStartTime = Number.MAX_SAFE_INTEGER; + const spanIdCounts = new Map(); + const spanMap = new Map(); + // filter out spans with empty start times + // eslint-disable-next-line no-param-reassign + data.spans = data.spans.filter((span) => Boolean(span.startTime)); + + // Sort process tags + data.processes = Object.entries(data.processes).reduce>((processes, [id, process]) => { + processes[id] = { + ...process, + tags: orderTags(process.tags), + }; + return processes; + }, {}); + + const max = data.spans.length; + for (let i = 0; i < max; i++) { + const span: TraceSpan = data.spans[i] as TraceSpan; + const { startTime, duration, processID } = span; + + let spanID = span.spanID; + // check for start / end time for the trace + if (startTime < traceStartTime) { + traceStartTime = startTime; + } + if (startTime + duration > traceEndTime) { + traceEndTime = startTime + duration; + } + // make sure span IDs are unique + const idCount = spanIdCounts.get(spanID); + if (idCount != null) { + // eslint-disable-next-line no-console + console.warn(`Dupe spanID, ${idCount + 1} x ${spanID}`, span, spanMap.get(spanID)); + if (_isEqual(span, spanMap.get(spanID))) { + // eslint-disable-next-line no-console + console.warn('\t two spans with same ID have `isEqual(...) === true`'); + } + spanIdCounts.set(spanID, idCount + 1); + spanID = `${spanID}_${idCount}`; + span.spanID = spanID; + } else { + spanIdCounts.set(spanID, 1); + } + span.process = data.processes[processID]; + spanMap.set(spanID, span); + } + // tree is necessary to sort the spans, so children follow parents, and + // siblings are sorted by start time + const tree = getTraceSpanIdsAsTree(data, spanMap); + const spans: TraceSpan[] = []; + const svcCounts: Record = {}; + + tree.walk((spanID: string, node: TreeNode, depth = 0) => { + if (spanID === '__root__') { + return; + } + if (typeof spanID !== 'string') { + return; + } + const span = spanMap.get(spanID); + if (!span) { + return; + } + const { serviceName } = span.process; + svcCounts[serviceName] = (svcCounts[serviceName] || 0) + 1; + span.relativeStartTime = span.startTime - traceStartTime; + span.depth = depth - 1; + span.hasChildren = node.children.length > 0; + span.childSpanCount = node.children.length; + span.warnings = span.warnings || []; + span.tags = span.tags || []; + span.references = span.references || []; + + span.childSpanIds = node.children + .slice() + .sort((a, b) => { + const spanA = spanMap.get(a.value)!; + const spanB = spanMap.get(b.value)!; + return spanB.startTime + spanB.duration - (spanA.startTime + spanA.duration); + }) + .map((each) => each.value); + + const tagsInfo = deduplicateTags(span.tags); + span.tags = orderTags(tagsInfo.dedupedTags, getConfigValue('topTagPrefixes')); + span.warnings = span.warnings.concat(tagsInfo.warnings); + span.references.forEach((ref, index) => { + const refSpan = spanMap.get(ref.spanID); + if (refSpan) { + // eslint-disable-next-line no-param-reassign + ref.span = refSpan; + if (index > 0) { + // Don't take into account the parent, just other references. + refSpan.subsidiarilyReferencedBy = refSpan.subsidiarilyReferencedBy || []; + refSpan.subsidiarilyReferencedBy.push({ + spanID, + traceID, + span, + refType: ref.refType, + }); + } + } + }); + spans.push(span); + }); + const traceName = getTraceName(spans); + const services = Object.keys(svcCounts).map((name) => ({ name, numberOfSpans: svcCounts[name] })); + return { + services, + spans, + traceID, + traceName, + // can't use spread operator for intersection types + // repl: https://goo.gl/4Z23MJ + // issue: https://github.com/facebook/flow/issues/1511 + processes: data.processes, + duration: traceEndTime - traceStartTime, + startTime: traceStartTime, + endTime: traceEndTime, + }; +} diff --git a/public/app/plugins/datasource/jaeger/_importedDependencies/selectors/trace.ts b/public/app/plugins/datasource/jaeger/_importedDependencies/selectors/trace.ts new file mode 100644 index 0000000000..9fb695b332 --- /dev/null +++ b/public/app/plugins/datasource/jaeger/_importedDependencies/selectors/trace.ts @@ -0,0 +1,65 @@ +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { TraceResponse, TraceSpanData } from '../types/trace'; +import TreeNode from '../utils/TreeNode'; + +const TREE_ROOT_ID = '__root__'; + +/** + * Build a tree of { value: spanID, children } items derived from the + * `span.references` information. The tree represents the grouping of parent / + * child relationships. The root-most node is nominal in that + * `.value === TREE_ROOT_ID`. This is done because a root span (the main trace + * span) is not always included with the trace data. Thus, there can be + * multiple top-level spans, and the root node acts as their common parent. + * + * The children are sorted by `span.startTime` after the tree is built. + * + * @param {Trace} trace The trace to build the tree of spanIDs. + * @return {TreeNode} A tree of spanIDs derived from the relationships + * between spans in the trace. + */ +export function getTraceSpanIdsAsTree(trace: TraceResponse, spanMap: Map | null = null) { + const nodesById = new Map(trace.spans.map((span: TraceSpanData) => [span.spanID, new TreeNode(span.spanID)])); + const spansById = spanMap ?? new Map(trace.spans.map((span: TraceSpanData) => [span.spanID, span])); + const root = new TreeNode(TREE_ROOT_ID); + trace.spans.forEach((span: TraceSpanData) => { + const node = nodesById.get(span.spanID)!; + if (Array.isArray(span.references) && span.references.length) { + const { refType, spanID: parentID } = span.references[0]; + if (refType === 'CHILD_OF' || refType === 'FOLLOWS_FROM') { + const parent = nodesById.get(parentID) || root; + parent.children?.push(node); + } else { + throw new Error(`Unrecognized ref type: ${refType}`); + } + } else { + root.children.push(node); + } + }); + const comparator = (nodeA: TreeNode, nodeB: TreeNode) => { + const a: TraceSpanData | undefined = nodeA?.value ? spansById.get(nodeA.value.toString()) : undefined; + const b: TraceSpanData | undefined = nodeB?.value ? spansById.get(nodeB.value.toString()) : undefined; + return +(a?.startTime! > b?.startTime!) || +(a?.startTime === b?.startTime) - 1; + }; + trace.spans.forEach((span: TraceSpanData) => { + const node = nodesById.get(span.spanID); + if (node!.children.length > 1) { + node?.children.sort(comparator); + } + }); + root.children.sort(comparator); + return root; +} diff --git a/public/app/plugins/datasource/jaeger/_importedDependencies/types/index.tsx b/public/app/plugins/datasource/jaeger/_importedDependencies/types/index.tsx new file mode 100644 index 0000000000..5eaae6e676 --- /dev/null +++ b/public/app/plugins/datasource/jaeger/_importedDependencies/types/index.tsx @@ -0,0 +1,23 @@ +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +export { + TraceSpan, + TraceResponse, + Trace, + TraceProcess, + TraceKeyValuePair, + TraceLink, + CriticalPathSection, +} from './trace'; diff --git a/public/app/plugins/datasource/jaeger/_importedDependencies/types/trace.ts b/public/app/plugins/datasource/jaeger/_importedDependencies/types/trace.ts new file mode 100644 index 0000000000..0507287b98 --- /dev/null +++ b/public/app/plugins/datasource/jaeger/_importedDependencies/types/trace.ts @@ -0,0 +1,112 @@ +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * All timestamps are in microseconds + */ + +// TODO: Everett Tech Debt: Fix KeyValuePair types +export type TraceKeyValuePair = { + key: string; + type?: string; + value: any; +}; + +export type TraceLink = { + url: string; + text: string; +}; + +export type TraceLog = { + timestamp: number; + fields: TraceKeyValuePair[]; +}; + +export type TraceProcess = { + serviceName: string; + tags: TraceKeyValuePair[]; +}; + +export type TraceSpanReference = { + refType: 'CHILD_OF' | 'FOLLOWS_FROM'; + // eslint-disable-next-line no-use-before-define + span?: TraceSpan | null | undefined; + spanID: string; + traceID: string; + tags?: TraceKeyValuePair[]; +}; + +export type TraceSpanData = { + spanID: string; + traceID: string; + processID: string; + operationName: string; + // Times are in microseconds + startTime: number; + duration: number; + logs: TraceLog[]; + tags?: TraceKeyValuePair[]; + kind?: string; + statusCode?: number; + statusMessage?: string; + instrumentationLibraryName?: string; + instrumentationLibraryVersion?: string; + traceState?: string; + references?: TraceSpanReference[]; + warnings?: string[] | null; + stackTraces?: string[]; + flags: number; + errorIconColor?: string; + dataFrameRowIndex?: number; + childSpanIds?: string[]; +}; + +export type TraceSpan = TraceSpanData & { + depth: number; + hasChildren: boolean; + childSpanCount: number; + process: TraceProcess; + relativeStartTime: number; + tags: NonNullable; + references: NonNullable; + warnings: NonNullable; + childSpanIds: NonNullable; + subsidiarilyReferencedBy: TraceSpanReference[]; +}; + +export type TraceData = { + processes: Record; + traceID: string; + warnings?: string[] | null; +}; + +export type TraceResponse = TraceData & { + spans: TraceSpanData[]; +}; + +export type Trace = TraceData & { + duration: number; + endTime: number; + spans: TraceSpan[]; + startTime: number; + traceName: string; + services: Array<{ name: string; numberOfSpans: number }>; +}; + +// It is a section of span that lies on critical path +export type CriticalPathSection = { + spanId: string; + section_start: number; + section_end: number; +}; diff --git a/public/app/plugins/datasource/jaeger/_importedDependencies/utils/TreeNode.ts b/public/app/plugins/datasource/jaeger/_importedDependencies/utils/TreeNode.ts new file mode 100644 index 0000000000..f6b123f2d5 --- /dev/null +++ b/public/app/plugins/datasource/jaeger/_importedDependencies/utils/TreeNode.ts @@ -0,0 +1,139 @@ +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License + +export default class TreeNode { + value: TValue; + children: Array>; + + static iterFunction( + fn: ((value: TValue, node: TreeNode, depth: number) => TreeNode | null) | Function, + depth = 0 + ) { + return (node: TreeNode) => fn(node.value, node, depth); + } + + static searchFunction(search: Function | TreeNode) { + if (typeof search === 'function') { + return search; + } + + return (value: TValue, node: TreeNode) => (search instanceof TreeNode ? node === search : value === search); + } + + constructor(value: TValue, children: Array> = []) { + this.value = value; + this.children = children; + } + + get depth(): number { + return this.children.reduce((depth, child) => Math.max(child.depth + 1, depth), 1); + } + + get size() { + let i = 0; + this.walk(() => i++); + return i; + } + + addChild(child: TreeNode | TValue) { + this.children.push(child instanceof TreeNode ? child : new TreeNode(child)); + return this; + } + + find(search: Function | TreeNode): TreeNode | null { + const searchFn = TreeNode.iterFunction(TreeNode.searchFunction(search)); + if (searchFn(this)) { + return this; + } + for (let i = 0; i < this.children.length; i++) { + const result = this.children[i].find(search); + if (result) { + return result; + } + } + return null; + } + + getPath(search: Function | TreeNode) { + const searchFn = TreeNode.iterFunction(TreeNode.searchFunction(search)); + + const findPath = ( + currentNode: TreeNode, + currentPath: Array> + ): Array> | null => { + // skip if we already found the result + const attempt = currentPath.concat([currentNode]); + // base case: return the array when there is a match + if (searchFn(currentNode)) { + return attempt; + } + for (let i = 0; i < currentNode.children.length; i++) { + const child = currentNode.children[i]; + const match = findPath(child, attempt); + if (match) { + return match; + } + } + return null; + }; + + return findPath(this, []); + } + + walk(fn: (spanID: TValue, node: TreeNode, depth: number) => void, startDepth = 0) { + type StackEntry = { + node: TreeNode; + depth: number; + }; + const nodeStack: StackEntry[] = []; + let actualDepth = startDepth; + nodeStack.push({ node: this, depth: actualDepth }); + while (nodeStack.length) { + const entry: StackEntry = nodeStack[nodeStack.length - 1]; + nodeStack.pop(); + const { node, depth } = entry; + fn(node.value, node, depth); + actualDepth = depth + 1; + let i = node.children.length - 1; + while (i >= 0) { + nodeStack.push({ node: node.children[i], depth: actualDepth }); + i--; + } + } + } + + paths(fn: (pathIds: TValue[]) => void) { + type StackEntry = { + node: TreeNode; + childIndex: number; + }; + const stack: StackEntry[] = []; + stack.push({ node: this, childIndex: 0 }); + const paths: TValue[] = []; + while (stack.length) { + const { node, childIndex } = stack[stack.length - 1]; + if (node.children.length >= childIndex + 1) { + stack[stack.length - 1].childIndex++; + stack.push({ node: node.children[childIndex], childIndex: 0 }); + } else { + if (node.children.length === 0) { + const path = stack.map((item) => item.node.value); + fn(path); + } + stack.pop(); + } + } + return paths; + } +} diff --git a/public/app/plugins/datasource/jaeger/_importedDependencies/utils/config/default-config.ts b/public/app/plugins/datasource/jaeger/_importedDependencies/utils/config/default-config.ts new file mode 100644 index 0000000000..1e8215a230 --- /dev/null +++ b/public/app/plugins/datasource/jaeger/_importedDependencies/utils/config/default-config.ts @@ -0,0 +1,40 @@ +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +const FALLBACK_DAG_MAX_NUM_SERVICES = 100; + +export default Object.defineProperty( + { + archiveEnabled: false, + dependencies: { + dagMaxNumServices: FALLBACK_DAG_MAX_NUM_SERVICES, + menuEnabled: true, + }, + linkPatterns: [], + search: { + maxLookback: { + label: '2 Days', + value: '2d', + }, + maxLimit: 1500, + }, + tracking: { + gaID: null, + trackErrors: true, + }, + }, + // fields that should be individually merged vs wholesale replaced + '__mergeFields', + { value: ['dependencies', 'search', 'tracking'] } +); diff --git a/public/app/plugins/datasource/jaeger/_importedDependencies/utils/config/get-config.tsx b/public/app/plugins/datasource/jaeger/_importedDependencies/utils/config/get-config.tsx new file mode 100644 index 0000000000..0a1ec89870 --- /dev/null +++ b/public/app/plugins/datasource/jaeger/_importedDependencies/utils/config/get-config.tsx @@ -0,0 +1,29 @@ +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { get as _get } from 'lodash'; + +import defaultConfig from './default-config'; + +/** + * Merge the embedded config from the query service (if present) with the + * default config from `../../constants/default-config`. + */ +export default function getConfig() { + return defaultConfig; +} + +export function getConfigValue(path: string) { + return _get(getConfig(), path); +} diff --git a/public/app/plugins/datasource/jaeger/components/SearchForm.test.tsx b/public/app/plugins/datasource/jaeger/components/SearchForm.test.tsx index 61d5000d67..bc1bae9208 100644 --- a/public/app/plugins/datasource/jaeger/components/SearchForm.test.tsx +++ b/public/app/plugins/datasource/jaeger/components/SearchForm.test.tsx @@ -2,17 +2,19 @@ import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; import { of } from 'rxjs'; -import { createFetchResponse } from 'test/helpers/createFetchResponse'; import { DataQueryRequest, DataSourceInstanceSettings, dateTime, PluginMetaInfo, PluginType } from '@grafana/data'; -import { backendSrv } from 'app/core/services/backend_srv'; +import { BackendSrv } from '@grafana/runtime'; import { JaegerDatasource, JaegerJsonData } from '../datasource'; +import { createFetchResponse } from '../helpers/createFetchResponse'; import { testResponse } from '../testResponse'; import { JaegerQuery } from '../types'; import SearchForm from './SearchForm'; +export const backendSrv = { fetch: jest.fn() } as unknown as BackendSrv; + jest.mock('@grafana/runtime', () => ({ ...jest.requireActual('@grafana/runtime'), getTemplateSrv: () => ({ diff --git a/public/app/plugins/datasource/jaeger/components/SearchForm.tsx b/public/app/plugins/datasource/jaeger/components/SearchForm.tsx index 38556b90cd..229a709e4d 100644 --- a/public/app/plugins/datasource/jaeger/components/SearchForm.tsx +++ b/public/app/plugins/datasource/jaeger/components/SearchForm.tsx @@ -2,11 +2,9 @@ import { css } from '@emotion/css'; import React, { useCallback, useEffect, useState } from 'react'; import { SelectableValue, toOption } from '@grafana/data'; +import { TemporaryAlert } from '@grafana/o11y-ds-frontend'; import { getTemplateSrv } from '@grafana/runtime'; import { fuzzyMatch, InlineField, InlineFieldRow, Input, Select } from '@grafana/ui'; -import { notifyApp } from 'app/core/actions'; -import { createErrorNotification } from 'app/core/copy/appNotification'; -import { dispatch } from 'app/store/store'; import { JaegerDatasource } from '../datasource'; import { JaegerQuery } from '../types'; @@ -27,6 +25,7 @@ const allOperationsOption: SelectableValue = { }; export function SearchForm({ datasource, query, onChange }: Props) { + const [alertText, setAlertText] = useState(''); const [serviceOptions, setServiceOptions] = useState>>(); const [operationOptions, setOperationOptions] = useState>>(); const [isLoading, setIsLoading] = useState<{ @@ -53,10 +52,11 @@ export function SearchForm({ datasource, query, onChange }: Props) { })); const filteredOptions = options.filter((item) => (item.value ? fuzzyMatch(item.value, query).found : false)); + setAlertText(''); return filteredOptions; } catch (error) { if (error instanceof Error) { - dispatch(notifyApp(createErrorNotification('Error', error))); + setAlertText(`Error: ${error.message}`); } return []; } finally { @@ -94,121 +94,124 @@ export function SearchForm({ datasource, query, onChange }: Props) { }, [datasource, query.service, loadOptions, query.operation]); return ( -
    - - - - loadOptions( - `/api/services/${encodeURIComponent(getTemplateSrv().replace(query.service!))}/operations`, - 'operations' - ) - } - isLoading={isLoading.operations} - value={operationOptions?.find((v) => v.value === query.operation) || null} - placeholder="Select an operation" - onChange={(v) => - onChange({ - ...query, - operation: v?.value! || undefined, - }) - } - menuPlacement="bottom" - isClearable - aria-label={'select-operation-name'} - allowCustomValue={true} - /> - - - - - - onChange({ - ...query, - tags: v.currentTarget.value, - }) - } - /> - - - - - - onChange({ - ...query, - minDuration: v.currentTarget.value, - }) - } - /> - - - - - - onChange({ - ...query, - maxDuration: v.currentTarget.value, - }) - } - /> - - - - - - onChange({ - ...query, - limit: v.currentTarget.value ? parseInt(v.currentTarget.value, 10) : undefined, - }) - } - /> - - -
    + <> +
    + + + + loadOptions( + `/api/services/${encodeURIComponent(getTemplateSrv().replace(query.service!))}/operations`, + 'operations' + ) + } + isLoading={isLoading.operations} + value={operationOptions?.find((v) => v.value === query.operation) || null} + placeholder="Select an operation" + onChange={(v) => + onChange({ + ...query, + operation: v?.value! || undefined, + }) + } + menuPlacement="bottom" + isClearable + aria-label={'select-operation-name'} + allowCustomValue={true} + /> + + + + + + onChange({ + ...query, + tags: v.currentTarget.value, + }) + } + /> + + + + + + onChange({ + ...query, + minDuration: v.currentTarget.value, + }) + } + /> + + + + + + onChange({ + ...query, + maxDuration: v.currentTarget.value, + }) + } + /> + + + + + + onChange({ + ...query, + limit: v.currentTarget.value ? parseInt(v.currentTarget.value, 10) : undefined, + }) + } + /> + + +
    + {alertText && } + ); } diff --git a/public/app/plugins/datasource/jaeger/datasource.test.ts b/public/app/plugins/datasource/jaeger/datasource.test.ts index 2bf2742a73..c2e3b4ddab 100644 --- a/public/app/plugins/datasource/jaeger/datasource.test.ts +++ b/public/app/plugins/datasource/jaeger/datasource.test.ts @@ -1,5 +1,4 @@ import { lastValueFrom, of, throwError } from 'rxjs'; -import { createFetchResponse } from 'test/helpers/createFetchResponse'; import { DataQueryRequest, @@ -10,11 +9,11 @@ import { PluginType, ScopedVars, } from '@grafana/data'; -import { backendSrv } from 'app/core/services/backend_srv'; -import { TimeSrv } from 'app/features/dashboard/services/TimeSrv'; +import { BackendSrv } from '@grafana/runtime'; import { ALL_OPERATIONS_KEY } from './components/SearchForm'; import { JaegerDatasource, JaegerJsonData } from './datasource'; +import { createFetchResponse } from './helpers/createFetchResponse'; import mockJson from './mockJsonResponse.json'; import { testResponse, @@ -24,6 +23,8 @@ import { } from './testResponse'; import { JaegerQuery } from './types'; +export const backendSrv = { fetch: jest.fn() } as unknown as BackendSrv; + jest.mock('@grafana/runtime', () => ({ ...jest.requireActual('@grafana/runtime'), getBackendSrv: () => backendSrv, @@ -37,18 +38,16 @@ jest.mock('@grafana/runtime', () => ({ }), })); -const timeSrvStub = { - timeRange() { - return { - from: dateTime(1531468681), - to: dateTime(1531489712), - }; - }, -} as TimeSrv; - describe('JaegerDatasource', () => { beforeEach(() => { jest.clearAllMocks(); + + const fetchMock = jest.spyOn(Date, 'now'); + fetchMock.mockImplementation(() => 1704106800000); // milliseconds for 2024-01-01 at 11:00am UTC + }); + + afterEach(() => { + jest.restoreAllMocks(); }); it('returns trace and graph when queried', async () => { @@ -121,7 +120,7 @@ describe('JaegerDatasource', () => { it('should return search results when the query type is search', async () => { const mock = setupFetchMock({ data: [testResponse] }); - const ds = new JaegerDatasource(defaultSettings, timeSrvStub); + const ds = new JaegerDatasource(defaultSettings); const response = await lastValueFrom( ds.query({ ...defaultQuery, @@ -129,7 +128,7 @@ describe('JaegerDatasource', () => { }) ); expect(mock).toBeCalledWith({ - url: `${defaultSettings.url}/api/traces?service=jaeger-query&operation=%2Fapi%2Fservices&start=1531468681000&end=1531489712000&lookback=custom`, + url: `${defaultSettings.url}/api/traces?service=jaeger-query&operation=%2Fapi%2Fservices&start=1704085200000000&end=1704106800000000&lookback=custom`, }); expect(response.data[0].meta.preferredVisualisationType).toBe('table'); // Make sure that traceID field has data link configured @@ -138,7 +137,7 @@ describe('JaegerDatasource', () => { }); it('should show the correct error message if no service name is selected', async () => { - const ds = new JaegerDatasource(defaultSettings, timeSrvStub); + const ds = new JaegerDatasource(defaultSettings); const response = await lastValueFrom( ds.query({ ...defaultQuery, @@ -150,7 +149,7 @@ describe('JaegerDatasource', () => { it('should remove operation from the query when all is selected', async () => { const mock = setupFetchMock({ data: [testResponse] }); - const ds = new JaegerDatasource(defaultSettings, timeSrvStub); + const ds = new JaegerDatasource(defaultSettings); await lastValueFrom( ds.query({ ...defaultQuery, @@ -158,13 +157,13 @@ describe('JaegerDatasource', () => { }) ); expect(mock).toBeCalledWith({ - url: `${defaultSettings.url}/api/traces?service=jaeger-query&start=1531468681000&end=1531489712000&lookback=custom`, + url: `${defaultSettings.url}/api/traces?service=jaeger-query&start=1704085200000000&end=1704106800000000&lookback=custom`, }); }); it('should convert tags from logfmt format to an object', async () => { const mock = setupFetchMock({ data: [testResponse] }); - const ds = new JaegerDatasource(defaultSettings, timeSrvStub); + const ds = new JaegerDatasource(defaultSettings); await lastValueFrom( ds.query({ ...defaultQuery, @@ -172,13 +171,13 @@ describe('JaegerDatasource', () => { }) ); expect(mock).toBeCalledWith({ - url: `${defaultSettings.url}/api/traces?service=jaeger-query&tags=%7B%22error%22%3A%22true%22%7D&start=1531468681000&end=1531489712000&lookback=custom`, + url: `${defaultSettings.url}/api/traces?service=jaeger-query&tags=%7B%22error%22%3A%22true%22%7D&start=1704085200000000&end=1704106800000000&lookback=custom`, }); }); it('should resolve templates in traceID', async () => { const mock = setupFetchMock({ data: [testResponse] }); - const ds = new JaegerDatasource(defaultSettings, timeSrvStub); + const ds = new JaegerDatasource(defaultSettings); await lastValueFrom( ds.query({ @@ -204,7 +203,7 @@ describe('JaegerDatasource', () => { it('should resolve templates in tags', async () => { const mock = setupFetchMock({ data: [testResponse] }); - const ds = new JaegerDatasource(defaultSettings, timeSrvStub); + const ds = new JaegerDatasource(defaultSettings); await lastValueFrom( ds.query({ ...defaultQuery, @@ -218,13 +217,13 @@ describe('JaegerDatasource', () => { }) ); expect(mock).toBeCalledWith({ - url: `${defaultSettings.url}/api/traces?service=jaeger-query&tags=%7B%22error%22%3A%22true%22%7D&start=1531468681000&end=1531489712000&lookback=custom`, + url: `${defaultSettings.url}/api/traces?service=jaeger-query&tags=%7B%22error%22%3A%22true%22%7D&start=1704085200000000&end=1704106800000000&lookback=custom`, }); }); it('should interpolate variables correctly', async () => { const mock = setupFetchMock({ data: [testResponse] }); - const ds = new JaegerDatasource(defaultSettings, timeSrvStub); + const ds = new JaegerDatasource(defaultSettings); const text = 'interpolationText'; await lastValueFrom( ds.query({ @@ -248,7 +247,7 @@ describe('JaegerDatasource', () => { }) ); expect(mock).toBeCalledWith({ - url: `${defaultSettings.url}/api/traces?service=interpolationText&operation=interpolationText&minDuration=interpolationText&maxDuration=interpolationText&start=1531468681000&end=1531489712000&lookback=custom`, + url: `${defaultSettings.url}/api/traces?service=interpolationText&operation=interpolationText&minDuration=interpolationText&maxDuration=interpolationText&start=1704085200000000&end=1704106800000000&lookback=custom`, }); }); }); @@ -343,9 +342,29 @@ describe('Test behavior with unmocked time', () => { it("call for `query()` when `queryType === 'dependencyGraph'`", async () => { const mock = setupFetchMock({ data: [testResponse] }); const ds = new JaegerDatasource(defaultSettings); + const now = Date.now(); ds.query({ ...defaultQuery, targets: [{ queryType: 'dependencyGraph', refId: '1' }] }); + + const url = mock.mock.calls[0][0].url; + const endTsMatch = url.match(/endTs=(\d+)/); + expect(endTsMatch).not.toBeNull(); + expect(parseInt(endTsMatch![1], 10)).toBeCloseTo(now, numDigits); + + const lookbackMatch = url.match(/lookback=(\d+)/); + expect(lookbackMatch).not.toBeNull(); + expect(parseInt(lookbackMatch![1], 10)).toBeCloseTo(3600000, -1); // due to rounding, the least significant digit is not reliable + }); + + it("call for `query()` when `queryType === 'dependencyGraph'`, using default range", async () => { + const mock = setupFetchMock({ data: [testResponse] }); + const ds = new JaegerDatasource(defaultSettings); const now = Date.now(); + const query = JSON.parse(JSON.stringify(defaultQuery)); + // @ts-ignore + query.range = undefined; + + ds.query({ ...query, targets: [{ queryType: 'dependencyGraph', refId: '1' }] }); const url = mock.mock.calls[0][0].url; const endTsMatch = url.match(/endTs=(\d+)/); @@ -354,7 +373,7 @@ describe('Test behavior with unmocked time', () => { const lookbackMatch = url.match(/lookback=(\d+)/); expect(lookbackMatch).not.toBeNull(); - expect(parseInt(lookbackMatch![1], 10)).toBeCloseTo(21600000, numDigits); + expect(parseInt(lookbackMatch![1], 10)).toBeCloseTo(21600000, -1); }); }); diff --git a/public/app/plugins/datasource/jaeger/datasource.ts b/public/app/plugins/datasource/jaeger/datasource.ts index bbdea5ea4e..78e60bdaec 100644 --- a/public/app/plugins/datasource/jaeger/datasource.ts +++ b/public/app/plugins/datasource/jaeger/datasource.ts @@ -11,13 +11,13 @@ import { dateMath, DateTime, FieldType, + getDefaultTimeRange, MutableDataFrame, ScopedVars, urlUtil, } from '@grafana/data'; import { NodeGraphOptions, SpanBarOptions } from '@grafana/o11y-ds-frontend'; import { BackendSrvRequest, getBackendSrv, getTemplateSrv, TemplateSrv } from '@grafana/runtime'; -import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { ALL_OPERATIONS_KEY } from './components/SearchForm'; import { TraceIdTimeParamsOptions } from './configuration/TraceIdTimeParams'; @@ -39,7 +39,6 @@ export class JaegerDatasource extends DataSourceApi spanBar?: SpanBarOptions; constructor( private instanceSettings: DataSourceInstanceSettings, - private readonly timeSrv: TimeSrv = getTimeSrv(), private readonly templateSrv: TemplateSrv = getTemplateSrv() ) { super(instanceSettings); @@ -67,7 +66,7 @@ export class JaegerDatasource extends DataSourceApi // Use the internal Jaeger /dependencies API for rendering the dependency graph. if (target.queryType === 'dependencyGraph') { - const timeRange = this.timeSrv.timeRange(); + const timeRange = options.range ?? getDefaultTimeRange(); const endTs = getTime(timeRange.to, true) / 1000; const lookback = endTs - getTime(timeRange.from, false) / 1000; return this._request('/api/dependencies', { endTs, lookback }).pipe(map(mapJaegerDependenciesResponse)); @@ -227,7 +226,7 @@ export class JaegerDatasource extends DataSourceApi } getTimeRange(): { start: number; end: number } { - const range = this.timeSrv.timeRange(); + const range = getDefaultTimeRange(); return { start: getTime(range.from, false), end: getTime(range.to, true), diff --git a/public/app/plugins/datasource/jaeger/helpers/createFetchResponse.ts b/public/app/plugins/datasource/jaeger/helpers/createFetchResponse.ts new file mode 100644 index 0000000000..1f75026ed7 --- /dev/null +++ b/public/app/plugins/datasource/jaeger/helpers/createFetchResponse.ts @@ -0,0 +1,15 @@ +import { FetchResponse } from '@grafana/runtime'; + +export function createFetchResponse(data: T): FetchResponse { + return { + data, + status: 200, + url: 'http://localhost:3000/api/ds/query', + config: { url: 'http://localhost:3000/api/ds/query' }, + type: 'basic', + statusText: 'Ok', + redirected: false, + headers: {} as unknown as Headers, + ok: true, + }; +} diff --git a/public/app/plugins/datasource/jaeger/package.json b/public/app/plugins/datasource/jaeger/package.json new file mode 100644 index 0000000000..bac2b08f8d --- /dev/null +++ b/public/app/plugins/datasource/jaeger/package.json @@ -0,0 +1,43 @@ +{ + "name": "@grafana-plugins/jaeger", + "description": "Jaeger plugin for Grafana", + "private": true, + "version": "10.4.0-pre", + "dependencies": { + "@emotion/css": "11.11.2", + "@grafana/data": "workspace:*", + "@grafana/experimental": "1.7.10", + "@grafana/o11y-ds-frontend": "workspace:*", + "@grafana/runtime": "workspace:*", + "@grafana/ui": "workspace:*", + "lodash": "4.17.21", + "logfmt": "^1.3.2", + "react-window": "1.8.10", + "rxjs": "7.8.1", + "stream-browserify": "3.0.0", + "tslib": "2.6.2", + "uuid": "9.0.1" + }, + "devDependencies": { + "@grafana/plugin-configs": "workspace:*", + "@testing-library/jest-dom": "6.4.2", + "@testing-library/react": "14.2.1", + "@testing-library/user-event": "14.5.2", + "@types/jest": "29.5.12", + "@types/lodash": "4.17.0", + "@types/logfmt": "^1.2.3", + "@types/react-window": "1.8.8", + "@types/uuid": "9.0.8", + "ts-node": "10.9.2", + "webpack": "5.90.3" + }, + "peerDependencies": { + "@grafana/runtime": "*" + }, + "scripts": { + "build": "webpack -c ./webpack.config.ts --env production", + "build:commit": "webpack -c ./webpack.config.ts --env production --env commit=$(git rev-parse --short HEAD)", + "dev": "webpack -w -c ./webpack.config.ts --env development" + }, + "packageManager": "yarn@3.6.0" +} diff --git a/public/app/plugins/datasource/jaeger/plugin.json b/public/app/plugins/datasource/jaeger/plugin.json index 4a0714ca0a..5bb7b96d60 100644 --- a/public/app/plugins/datasource/jaeger/plugin.json +++ b/public/app/plugins/datasource/jaeger/plugin.json @@ -30,6 +30,10 @@ "name": "GitHub Project", "url": "https://github.com/jaegertracing/jaeger" } - ] + ], + "version": "%VERSION%" + }, + "dependencies": { + "grafanaDependency": ">=10.3.0-0" } } diff --git a/public/app/plugins/datasource/jaeger/responseTransform.ts b/public/app/plugins/datasource/jaeger/responseTransform.ts index 507e5646cf..7b49168e79 100644 --- a/public/app/plugins/datasource/jaeger/responseTransform.ts +++ b/public/app/plugins/datasource/jaeger/responseTransform.ts @@ -6,8 +6,8 @@ import { TraceLog, TraceSpanRow, } from '@grafana/data'; -import { transformTraceData } from 'app/features/explore/TraceView/components'; +import transformTraceData from './_importedDependencies/model/transform-trace-data'; import { JaegerResponse, Span, TraceProcess, TraceResponse } from './types'; export function createTraceFrame(data: TraceResponse): DataFrame { diff --git a/public/app/plugins/datasource/jaeger/tsconfig.json b/public/app/plugins/datasource/jaeger/tsconfig.json new file mode 100644 index 0000000000..a7a33a2fc3 --- /dev/null +++ b/public/app/plugins/datasource/jaeger/tsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "types": ["jest", "@testing-library/jest-dom"] + }, + "extends": "@grafana/plugin-configs/tsconfig.json", + "include": ["."] +} diff --git a/public/app/plugins/datasource/jaeger/webpack.config.ts b/public/app/plugins/datasource/jaeger/webpack.config.ts new file mode 100644 index 0000000000..10ef5bd69a --- /dev/null +++ b/public/app/plugins/datasource/jaeger/webpack.config.ts @@ -0,0 +1,14 @@ +import config from '@grafana/plugin-configs/webpack.config'; + +const configWithFallback = async (env: Record) => { + const response = await config(env); + if (response !== undefined && response.resolve !== undefined) { + response.resolve.fallback = { + ...response.resolve.fallback, + stream: require.resolve('stream-browserify'), + }; + } + return response; +}; + +export default configWithFallback; diff --git a/yarn.lock b/yarn.lock index 34d66e7cfb..725e050bc6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -40,7 +40,17 @@ __metadata: languageName: node linkType: hard -"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.3, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.16.7, @babel/code-frame@npm:^7.18.6, @babel/code-frame@npm:^7.22.13, @babel/code-frame@npm:^7.23.5, @babel/code-frame@npm:^7.24.1": +"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.3, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.16.7, @babel/code-frame@npm:^7.18.6, @babel/code-frame@npm:^7.22.13, @babel/code-frame@npm:^7.23.5": + version: 7.23.5 + resolution: "@babel/code-frame@npm:7.23.5" + dependencies: + "@babel/highlight": "npm:^7.23.4" + chalk: "npm:^2.4.2" + checksum: 10/44e58529c9d93083288dc9e649c553c5ba997475a7b0758cc3ddc4d77b8a7d985dbe78cc39c9bbc61f26d50af6da1ddf0a3427eae8cc222a9370619b671ed8f5 + languageName: node + linkType: hard + +"@babel/code-frame@npm:^7.24.1": version: 7.24.1 resolution: "@babel/code-frame@npm:7.24.1" dependencies: @@ -50,7 +60,14 @@ __metadata: languageName: node linkType: hard -"@babel/compat-data@npm:^7.22.6, @babel/compat-data@npm:^7.23.2, @babel/compat-data@npm:^7.23.5, @babel/compat-data@npm:^7.24.1": +"@babel/compat-data@npm:^7.22.6, @babel/compat-data@npm:^7.23.2, @babel/compat-data@npm:^7.23.5": + version: 7.23.5 + resolution: "@babel/compat-data@npm:7.23.5" + checksum: 10/088f14f646ecbddd5ef89f120a60a1b3389a50a9705d44603dca77662707d0175a5e0e0da3943c3298f1907a4ab871468656fbbf74bb7842cd8b0686b2c19736 + languageName: node + linkType: hard + +"@babel/compat-data@npm:^7.24.1": version: 7.24.1 resolution: "@babel/compat-data@npm:7.24.1" checksum: 10/d5460b99c07ff8487467c52f742a219c7e3bcdcaa2882456a13c0d0c8116405f0c85a651fb60511284dc64ed627a5e989f24c3cd6e71d07a9947e7c8954b433c @@ -80,7 +97,7 @@ __metadata: languageName: node linkType: hard -"@babel/core@npm:7.24.1, @babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.13.16, @babel/core@npm:^7.22.9, @babel/core@npm:^7.7.5": +"@babel/core@npm:7.24.1": version: 7.24.1 resolution: "@babel/core@npm:7.24.1" dependencies: @@ -103,7 +120,42 @@ __metadata: languageName: node linkType: hard -"@babel/generator@npm:^7.12.11, @babel/generator@npm:^7.22.9, @babel/generator@npm:^7.23.0, @babel/generator@npm:^7.24.1, @babel/generator@npm:^7.7.2": +"@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.13.16, @babel/core@npm:^7.22.9, @babel/core@npm:^7.7.5": + version: 7.24.0 + resolution: "@babel/core@npm:7.24.0" + dependencies: + "@ampproject/remapping": "npm:^2.2.0" + "@babel/code-frame": "npm:^7.23.5" + "@babel/generator": "npm:^7.23.6" + "@babel/helper-compilation-targets": "npm:^7.23.6" + "@babel/helper-module-transforms": "npm:^7.23.3" + "@babel/helpers": "npm:^7.24.0" + "@babel/parser": "npm:^7.24.0" + "@babel/template": "npm:^7.24.0" + "@babel/traverse": "npm:^7.24.0" + "@babel/types": "npm:^7.24.0" + convert-source-map: "npm:^2.0.0" + debug: "npm:^4.1.0" + gensync: "npm:^1.0.0-beta.2" + json5: "npm:^2.2.3" + semver: "npm:^6.3.1" + checksum: 10/1e22215cc89e061e0cbfed72f265ad24d363f3e9b24b51e9c4cf3ccb9222260a29a1c1e62edb439cb7e2229a3fce924edd43300500416613236c13fc8d62a947 + languageName: node + linkType: hard + +"@babel/generator@npm:^7.12.11, @babel/generator@npm:^7.22.9, @babel/generator@npm:^7.23.0, @babel/generator@npm:^7.23.6, @babel/generator@npm:^7.7.2": + version: 7.23.6 + resolution: "@babel/generator@npm:7.23.6" + dependencies: + "@babel/types": "npm:^7.23.6" + "@jridgewell/gen-mapping": "npm:^0.3.2" + "@jridgewell/trace-mapping": "npm:^0.3.17" + jsesc: "npm:^2.5.1" + checksum: 10/864090d5122c0aa3074471fd7b79d8a880c1468480cbd28925020a3dcc7eb6e98bedcdb38983df299c12b44b166e30915b8085a7bc126e68fa7e2aadc7bd1ac5 + languageName: node + linkType: hard + +"@babel/generator@npm:^7.24.1": version: 7.24.1 resolution: "@babel/generator@npm:7.24.1" dependencies: @@ -146,7 +198,26 @@ __metadata: languageName: node linkType: hard -"@babel/helper-create-class-features-plugin@npm:^7.18.6, @babel/helper-create-class-features-plugin@npm:^7.22.15, @babel/helper-create-class-features-plugin@npm:^7.24.1": +"@babel/helper-create-class-features-plugin@npm:^7.18.6, @babel/helper-create-class-features-plugin@npm:^7.22.15": + version: 7.23.7 + resolution: "@babel/helper-create-class-features-plugin@npm:7.23.7" + dependencies: + "@babel/helper-annotate-as-pure": "npm:^7.22.5" + "@babel/helper-environment-visitor": "npm:^7.22.20" + "@babel/helper-function-name": "npm:^7.23.0" + "@babel/helper-member-expression-to-functions": "npm:^7.23.0" + "@babel/helper-optimise-call-expression": "npm:^7.22.5" + "@babel/helper-replace-supers": "npm:^7.22.20" + "@babel/helper-skip-transparent-expression-wrappers": "npm:^7.22.5" + "@babel/helper-split-export-declaration": "npm:^7.22.6" + semver: "npm:^6.3.1" + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 10/c8b3ef58fca399a25f00d703b0fb2ac1d86642d9e3bd7af04df77857641ed08aaca042ffb271ef93771f9272481fd1cf102a9bddfcee407fb126c927deeef6a7 + languageName: node + linkType: hard + +"@babel/helper-create-class-features-plugin@npm:^7.24.1": version: 7.24.1 resolution: "@babel/helper-create-class-features-plugin@npm:7.24.1" dependencies: @@ -249,7 +320,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-member-expression-to-functions@npm:^7.0.0, @babel/helper-member-expression-to-functions@npm:^7.23.0": +"@babel/helper-member-expression-to-functions@npm:^7.0.0, @babel/helper-member-expression-to-functions@npm:^7.22.15, @babel/helper-member-expression-to-functions@npm:^7.23.0": version: 7.23.0 resolution: "@babel/helper-member-expression-to-functions@npm:7.23.0" dependencies: @@ -258,7 +329,16 @@ __metadata: languageName: node linkType: hard -"@babel/helper-module-imports@npm:^7.0.0, @babel/helper-module-imports@npm:^7.16.7, @babel/helper-module-imports@npm:^7.22.15, @babel/helper-module-imports@npm:^7.24.1": +"@babel/helper-module-imports@npm:^7.0.0, @babel/helper-module-imports@npm:^7.16.7, @babel/helper-module-imports@npm:^7.22.15": + version: 7.22.15 + resolution: "@babel/helper-module-imports@npm:7.22.15" + dependencies: + "@babel/types": "npm:^7.22.15" + checksum: 10/5ecf9345a73b80c28677cfbe674b9f567bb0d079e37dcba9055e36cb337db24ae71992a58e1affa9d14a60d3c69907d30fe1f80aea105184501750a58d15c81c + languageName: node + linkType: hard + +"@babel/helper-module-imports@npm:^7.24.1": version: 7.24.1 resolution: "@babel/helper-module-imports@npm:7.24.1" dependencies: @@ -311,7 +391,20 @@ __metadata: languageName: node linkType: hard -"@babel/helper-replace-supers@npm:^7.0.0, @babel/helper-replace-supers@npm:^7.24.1": +"@babel/helper-replace-supers@npm:^7.0.0, @babel/helper-replace-supers@npm:^7.22.20": + version: 7.22.20 + resolution: "@babel/helper-replace-supers@npm:7.22.20" + dependencies: + "@babel/helper-environment-visitor": "npm:^7.22.20" + "@babel/helper-member-expression-to-functions": "npm:^7.22.15" + "@babel/helper-optimise-call-expression": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 10/617666f57b0f94a2f430ee66b67c8f6fa94d4c22400f622947580d8f3638ea34b71280af59599ed4afbb54ae6e2bdd4f9083fe0e341184a4bb0bd26ef58d3017 + languageName: node + linkType: hard + +"@babel/helper-replace-supers@npm:^7.24.1": version: 7.24.1 resolution: "@babel/helper-replace-supers@npm:7.24.1" dependencies: @@ -383,7 +476,18 @@ __metadata: languageName: node linkType: hard -"@babel/helpers@npm:^7.23.2, @babel/helpers@npm:^7.24.1": +"@babel/helpers@npm:^7.23.2, @babel/helpers@npm:^7.24.0": + version: 7.24.0 + resolution: "@babel/helpers@npm:7.24.0" + dependencies: + "@babel/template": "npm:^7.24.0" + "@babel/traverse": "npm:^7.24.0" + "@babel/types": "npm:^7.24.0" + checksum: 10/cc82012161b30185c2698da359c7311cf019f0932f8fcb805e985fec9e0053c354f0534dc9961f3170eee579df6724eecd34b0f5ffaa155cdd456af59fbff86e + languageName: node + linkType: hard + +"@babel/helpers@npm:^7.24.1": version: 7.24.1 resolution: "@babel/helpers@npm:7.24.1" dependencies: @@ -394,6 +498,17 @@ __metadata: languageName: node linkType: hard +"@babel/highlight@npm:^7.23.4": + version: 7.23.4 + resolution: "@babel/highlight@npm:7.23.4" + dependencies: + "@babel/helper-validator-identifier": "npm:^7.22.20" + chalk: "npm:^2.4.2" + js-tokens: "npm:^4.0.0" + checksum: 10/62fef9b5bcea7131df4626d009029b1ae85332042f4648a4ce6e740c3fd23112603c740c45575caec62f260c96b11054d3be5987f4981a5479793579c3aac71f + languageName: node + linkType: hard + "@babel/highlight@npm:^7.24.1": version: 7.24.1 resolution: "@babel/highlight@npm:7.24.1" @@ -406,7 +521,16 @@ __metadata: languageName: node linkType: hard -"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.13.16, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.22.7, @babel/parser@npm:^7.23.0, @babel/parser@npm:^7.24.0, @babel/parser@npm:^7.24.1": +"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.13.16, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.22.7, @babel/parser@npm:^7.23.0, @babel/parser@npm:^7.24.0": + version: 7.24.0 + resolution: "@babel/parser@npm:7.24.0" + bin: + parser: ./bin/babel-parser.js + checksum: 10/3e5ebb903a6f71629a9d0226743e37fe3d961e79911d2698b243637f66c4df7e3e0a42c07838bc0e7cc9fcd585d9be8f4134a145b9459ee4a459420fb0d1360b + languageName: node + linkType: hard + +"@babel/parser@npm:^7.24.1": version: 7.24.1 resolution: "@babel/parser@npm:7.24.1" bin: @@ -415,7 +539,18 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:^7.22.15, @babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:^7.24.1": +"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:^7.22.15, @babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:7.23.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 10/ddbaf2c396b7780f15e80ee01d6dd790db076985f3dfeb6527d1a8d4cacf370e49250396a3aa005b2c40233cac214a106232f83703d5e8491848bde273938232 + languageName: node + linkType: hard + +"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:7.24.1" dependencies: @@ -426,7 +561,20 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@npm:^7.22.15, @babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@npm:^7.24.1": +"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@npm:^7.22.15, @babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@npm:7.23.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-skip-transparent-expression-wrappers": "npm:^7.22.5" + "@babel/plugin-transform-optional-chaining": "npm:^7.23.3" + peerDependencies: + "@babel/core": ^7.13.0 + checksum: 10/434b9d710ae856fa1a456678cc304fbc93915af86d581ee316e077af746a709a741ea39d7e1d4f5b98861b629cc7e87f002d3138f5e836775632466d4c74aef2 + languageName: node + linkType: hard + +"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@npm:7.24.1" dependencies: @@ -439,6 +587,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@npm:^7.23.7": + version: 7.23.7 + resolution: "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@npm:7.23.7" + dependencies: + "@babel/helper-environment-visitor": "npm:^7.22.20" + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 10/3b0c9554cd0048e6e7341d7b92f29d400dbc6a5a4fc2f86dbed881d32e02ece9b55bc520387bae2eac22a5ab38a0b205c29b52b181294d99b4dd75e27309b548 + languageName: node + linkType: hard + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@npm:7.24.1" @@ -586,7 +746,18 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-import-assertions@npm:^7.22.5, @babel/plugin-syntax-import-assertions@npm:^7.24.1": +"@babel/plugin-syntax-import-assertions@npm:^7.22.5, @babel/plugin-syntax-import-assertions@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-syntax-import-assertions@npm:7.23.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/883e6b35b2da205138caab832d54505271a3fee3fc1e8dc0894502434fc2b5d517cbe93bbfbfef8068a0fb6ec48ebc9eef3f605200a489065ba43d8cddc1c9a7 + languageName: node + linkType: hard + +"@babel/plugin-syntax-import-assertions@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-syntax-import-assertions@npm:7.24.1" dependencies: @@ -597,7 +768,18 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-import-attributes@npm:^7.22.5, @babel/plugin-syntax-import-attributes@npm:^7.24.1": +"@babel/plugin-syntax-import-attributes@npm:^7.22.5, @babel/plugin-syntax-import-attributes@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-syntax-import-attributes@npm:7.23.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/9aed7661ffb920ca75df9f494757466ca92744e43072e0848d87fa4aa61a3f2ee5a22198ac1959856c036434b5614a8f46f1fb70298835dbe28220cdd1d4c11e + languageName: node + linkType: hard + +"@babel/plugin-syntax-import-attributes@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-syntax-import-attributes@npm:7.24.1" dependencies: @@ -630,7 +812,18 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-jsx@npm:^7.22.5, @babel/plugin-syntax-jsx@npm:^7.23.3, @babel/plugin-syntax-jsx@npm:^7.7.2": +"@babel/plugin-syntax-jsx@npm:^7.22.5, @babel/plugin-syntax-jsx@npm:^7.7.2": + version: 7.22.5 + resolution: "@babel/plugin-syntax-jsx@npm:7.22.5" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/8829d30c2617ab31393d99cec2978e41f014f4ac6f01a1cecf4c4dd8320c3ec12fdc3ce121126b2d8d32f6887e99ca1a0bad53dedb1e6ad165640b92b24980ce + languageName: node + linkType: hard + +"@babel/plugin-syntax-jsx@npm:^7.23.3": version: 7.24.1 resolution: "@babel/plugin-syntax-jsx@npm:7.24.1" dependencies: @@ -752,7 +945,18 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-arrow-functions@npm:^7.22.5, @babel/plugin-transform-arrow-functions@npm:^7.24.1": +"@babel/plugin-transform-arrow-functions@npm:^7.22.5, @babel/plugin-transform-arrow-functions@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-transform-arrow-functions@npm:7.23.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/1e99118176e5366c2636064d09477016ab5272b2a92e78b8edb571d20bc3eaa881789a905b20042942c3c2d04efc530726cf703f937226db5ebc495f5d067e66 + languageName: node + linkType: hard + +"@babel/plugin-transform-arrow-functions@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-arrow-functions@npm:7.24.1" dependencies: @@ -763,7 +967,21 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-async-generator-functions@npm:^7.23.2, @babel/plugin-transform-async-generator-functions@npm:^7.24.1": +"@babel/plugin-transform-async-generator-functions@npm:^7.23.2, @babel/plugin-transform-async-generator-functions@npm:^7.23.9": + version: 7.23.9 + resolution: "@babel/plugin-transform-async-generator-functions@npm:7.23.9" + dependencies: + "@babel/helper-environment-visitor": "npm:^7.22.20" + "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-remap-async-to-generator": "npm:^7.22.20" + "@babel/plugin-syntax-async-generators": "npm:^7.8.4" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/d402494087a6b803803eb5ab46b837aab100a04c4c5148e38bfa943ea1bbfc1ecfb340f1ced68972564312d3580f550c125f452372e77607a558fbbaf98c31c0 + languageName: node + linkType: hard + +"@babel/plugin-transform-async-generator-functions@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-async-generator-functions@npm:7.24.1" dependencies: @@ -777,7 +995,20 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-async-to-generator@npm:^7.22.5, @babel/plugin-transform-async-to-generator@npm:^7.24.1": +"@babel/plugin-transform-async-to-generator@npm:^7.22.5, @babel/plugin-transform-async-to-generator@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-transform-async-to-generator@npm:7.23.3" + dependencies: + "@babel/helper-module-imports": "npm:^7.22.15" + "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-remap-async-to-generator": "npm:^7.22.20" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/2e9d9795d4b3b3d8090332104e37061c677f29a1ce65bcbda4099a32d243e5d9520270a44bbabf0fb1fb40d463bd937685b1a1042e646979086c546d55319c3c + languageName: node + linkType: hard + +"@babel/plugin-transform-async-to-generator@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-async-to-generator@npm:7.24.1" dependencies: @@ -790,7 +1021,18 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-block-scoped-functions@npm:^7.22.5, @babel/plugin-transform-block-scoped-functions@npm:^7.24.1": +"@babel/plugin-transform-block-scoped-functions@npm:^7.22.5, @babel/plugin-transform-block-scoped-functions@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-transform-block-scoped-functions@npm:7.23.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/e63b16d94ee5f4d917e669da3db5ea53d1e7e79141a2ec873c1e644678cdafe98daa556d0d359963c827863d6b3665d23d4938a94a4c5053a1619c4ebd01d020 + languageName: node + linkType: hard + +"@babel/plugin-transform-block-scoped-functions@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-block-scoped-functions@npm:7.24.1" dependencies: @@ -801,7 +1043,18 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-block-scoping@npm:^7.23.0, @babel/plugin-transform-block-scoping@npm:^7.24.1": +"@babel/plugin-transform-block-scoping@npm:^7.23.0, @babel/plugin-transform-block-scoping@npm:^7.23.4": + version: 7.23.4 + resolution: "@babel/plugin-transform-block-scoping@npm:7.23.4" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/bbb965a3acdfb03559806d149efbd194ac9c983b260581a60efcb15eb9fbe20e3054667970800146d867446db1c1398f8e4ee87f4454233e49b8f8ce947bd99b + languageName: node + linkType: hard + +"@babel/plugin-transform-block-scoping@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-block-scoping@npm:7.24.1" dependencies: @@ -812,7 +1065,19 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-class-properties@npm:^7.22.5, @babel/plugin-transform-class-properties@npm:^7.24.1": +"@babel/plugin-transform-class-properties@npm:^7.22.5, @babel/plugin-transform-class-properties@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-transform-class-properties@npm:7.23.3" + dependencies: + "@babel/helper-create-class-features-plugin": "npm:^7.22.15" + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/9c6f8366f667897541d360246de176dd29efc7a13d80a5b48361882f7173d9173be4646c3b7d9b003ccc0e01e25df122330308f33db921fa553aa17ad544b3fc + languageName: node + linkType: hard + +"@babel/plugin-transform-class-properties@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-class-properties@npm:7.24.1" dependencies: @@ -824,7 +1089,20 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-class-static-block@npm:^7.22.11, @babel/plugin-transform-class-static-block@npm:^7.24.1": +"@babel/plugin-transform-class-static-block@npm:^7.22.11, @babel/plugin-transform-class-static-block@npm:^7.23.4": + version: 7.23.4 + resolution: "@babel/plugin-transform-class-static-block@npm:7.23.4" + dependencies: + "@babel/helper-create-class-features-plugin": "npm:^7.22.15" + "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/plugin-syntax-class-static-block": "npm:^7.14.5" + peerDependencies: + "@babel/core": ^7.12.0 + checksum: 10/c8bfaba19a674fc2eb54edad71e958647360474e3163e8226f1acd63e4e2dbec32a171a0af596c1dc5359aee402cc120fea7abd1fb0e0354b6527f0fc9e8aa1e + languageName: node + linkType: hard + +"@babel/plugin-transform-class-static-block@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-class-static-block@npm:7.24.1" dependencies: @@ -837,7 +1115,25 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-classes@npm:^7.22.15, @babel/plugin-transform-classes@npm:^7.24.1": +"@babel/plugin-transform-classes@npm:^7.22.15, @babel/plugin-transform-classes@npm:^7.23.8": + version: 7.23.8 + resolution: "@babel/plugin-transform-classes@npm:7.23.8" + dependencies: + "@babel/helper-annotate-as-pure": "npm:^7.22.5" + "@babel/helper-compilation-targets": "npm:^7.23.6" + "@babel/helper-environment-visitor": "npm:^7.22.20" + "@babel/helper-function-name": "npm:^7.23.0" + "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-replace-supers": "npm:^7.22.20" + "@babel/helper-split-export-declaration": "npm:^7.22.6" + globals: "npm:^11.1.0" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/4bb4b19e7a39871c4414fb44fc5f2cc47c78f993b74c43238dfb99c9dac2d15cb99b43f8a3d42747580e1807d2b8f5e13ce7e95e593fd839bd176aa090bf9a23 + languageName: node + linkType: hard + +"@babel/plugin-transform-classes@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-classes@npm:7.24.1" dependencies: @@ -855,7 +1151,19 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-computed-properties@npm:^7.22.5, @babel/plugin-transform-computed-properties@npm:^7.24.1": +"@babel/plugin-transform-computed-properties@npm:^7.22.5, @babel/plugin-transform-computed-properties@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-transform-computed-properties@npm:7.23.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/template": "npm:^7.22.15" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/e75593e02c5ea473c17839e3c9d597ce3697bf039b66afe9a4d06d086a87fb3d95850b4174476897afc351dc1b46a9ec3165ee6e8fbad3732c0d65f676f855ad + languageName: node + linkType: hard + +"@babel/plugin-transform-computed-properties@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-computed-properties@npm:7.24.1" dependencies: @@ -867,7 +1175,18 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-destructuring@npm:^7.23.0, @babel/plugin-transform-destructuring@npm:^7.24.1": +"@babel/plugin-transform-destructuring@npm:^7.23.0, @babel/plugin-transform-destructuring@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-transform-destructuring@npm:7.23.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/5abd93718af5a61f8f6a97d2ccac9139499752dd5b2c533d7556fb02947ae01b2f51d4c4f5e64df569e8783d3743270018eb1fa979c43edec7dd1377acf107ed + languageName: node + linkType: hard + +"@babel/plugin-transform-destructuring@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-destructuring@npm:7.24.1" dependencies: @@ -878,7 +1197,19 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-dotall-regex@npm:^7.22.5, @babel/plugin-transform-dotall-regex@npm:^7.24.1": +"@babel/plugin-transform-dotall-regex@npm:^7.22.5, @babel/plugin-transform-dotall-regex@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-transform-dotall-regex@npm:7.23.3" + dependencies: + "@babel/helper-create-regexp-features-plugin": "npm:^7.22.15" + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/a2dbbf7f1ea16a97948c37df925cb364337668c41a3948b8d91453f140507bd8a3429030c7ce66d09c299987b27746c19a2dd18b6f17dcb474854b14fd9159a3 + languageName: node + linkType: hard + +"@babel/plugin-transform-dotall-regex@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-dotall-regex@npm:7.24.1" dependencies: @@ -890,7 +1221,18 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-duplicate-keys@npm:^7.22.5, @babel/plugin-transform-duplicate-keys@npm:^7.24.1": +"@babel/plugin-transform-duplicate-keys@npm:^7.22.5, @babel/plugin-transform-duplicate-keys@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-transform-duplicate-keys@npm:7.23.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/c2a21c34dc0839590cd945192cbc46fde541a27e140c48fe1808315934664cdbf18db64889e23c4eeb6bad9d3e049482efdca91d29de5734ffc887c4fbabaa16 + languageName: node + linkType: hard + +"@babel/plugin-transform-duplicate-keys@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-duplicate-keys@npm:7.24.1" dependencies: @@ -901,33 +1243,69 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-dynamic-import@npm:^7.22.11, @babel/plugin-transform-dynamic-import@npm:^7.24.1": - version: 7.24.1 - resolution: "@babel/plugin-transform-dynamic-import@npm:7.24.1" +"@babel/plugin-transform-dynamic-import@npm:^7.22.11, @babel/plugin-transform-dynamic-import@npm:^7.23.4": + version: 7.23.4 + resolution: "@babel/plugin-transform-dynamic-import@npm:7.23.4" dependencies: - "@babel/helper-plugin-utils": "npm:^7.24.0" + "@babel/helper-plugin-utils": "npm:^7.22.5" "@babel/plugin-syntax-dynamic-import": "npm:^7.8.3" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/59fc561ee40b1a69f969c12c6c5fac206226d6642213985a569dd0f99f8e41c0f4eaedebd36936c255444a8335079842274c42a975a433beadb436d4c5abb79b + checksum: 10/57a722604c430d9f3dacff22001a5f31250e34785d4969527a2ae9160fa86858d0892c5b9ff7a06a04076f8c76c9e6862e0541aadca9c057849961343aab0845 languageName: node linkType: hard -"@babel/plugin-transform-exponentiation-operator@npm:^7.22.5, @babel/plugin-transform-exponentiation-operator@npm:^7.24.1": +"@babel/plugin-transform-dynamic-import@npm:^7.24.1": version: 7.24.1 - resolution: "@babel/plugin-transform-exponentiation-operator@npm:7.24.1" + resolution: "@babel/plugin-transform-dynamic-import@npm:7.24.1" dependencies: - "@babel/helper-builder-binary-assignment-operator-visitor": "npm:^7.22.15" "@babel/helper-plugin-utils": "npm:^7.24.0" + "@babel/plugin-syntax-dynamic-import": "npm:^7.8.3" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/f90841fe1a1e9f680b4209121d3e2992f923e85efcd322b26e5901c180ef44ff727fb89790803a23fac49af34c1ce2e480018027c22b4573b615512ac5b6fc50 + checksum: 10/59fc561ee40b1a69f969c12c6c5fac206226d6642213985a569dd0f99f8e41c0f4eaedebd36936c255444a8335079842274c42a975a433beadb436d4c5abb79b languageName: node linkType: hard -"@babel/plugin-transform-export-namespace-from@npm:^7.22.11, @babel/plugin-transform-export-namespace-from@npm:^7.24.1": - version: 7.24.1 - resolution: "@babel/plugin-transform-export-namespace-from@npm:7.24.1" +"@babel/plugin-transform-exponentiation-operator@npm:^7.22.5, @babel/plugin-transform-exponentiation-operator@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-transform-exponentiation-operator@npm:7.23.3" + dependencies: + "@babel/helper-builder-binary-assignment-operator-visitor": "npm:^7.22.15" + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/00d05ab14ad0f299160fcf9d8f55a1cc1b740e012ab0b5ce30207d2365f091665115557af7d989cd6260d075a252d9e4283de5f2b247dfbbe0e42ae586e6bf66 + languageName: node + linkType: hard + +"@babel/plugin-transform-exponentiation-operator@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-exponentiation-operator@npm:7.24.1" + dependencies: + "@babel/helper-builder-binary-assignment-operator-visitor": "npm:^7.22.15" + "@babel/helper-plugin-utils": "npm:^7.24.0" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/f90841fe1a1e9f680b4209121d3e2992f923e85efcd322b26e5901c180ef44ff727fb89790803a23fac49af34c1ce2e480018027c22b4573b615512ac5b6fc50 + languageName: node + linkType: hard + +"@babel/plugin-transform-export-namespace-from@npm:^7.22.11, @babel/plugin-transform-export-namespace-from@npm:^7.23.4": + version: 7.23.4 + resolution: "@babel/plugin-transform-export-namespace-from@npm:7.23.4" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/plugin-syntax-export-namespace-from": "npm:^7.8.3" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/9f770a81bfd03b48d6ba155d452946fd56d6ffe5b7d871e9ec2a0b15e0f424273b632f3ed61838b90015b25bbda988896b7a46c7d964fbf8f6feb5820b309f93 + languageName: node + linkType: hard + +"@babel/plugin-transform-export-namespace-from@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-export-namespace-from@npm:7.24.1" dependencies: "@babel/helper-plugin-utils": "npm:^7.24.0" "@babel/plugin-syntax-export-namespace-from": "npm:^7.8.3" @@ -949,7 +1327,19 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-for-of@npm:^7.22.15, @babel/plugin-transform-for-of@npm:^7.24.1": +"@babel/plugin-transform-for-of@npm:^7.22.15, @babel/plugin-transform-for-of@npm:^7.23.6": + version: 7.23.6 + resolution: "@babel/plugin-transform-for-of@npm:7.23.6" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-skip-transparent-expression-wrappers": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/b84ef1f26a2db316237ae6d10fa7c22c70ac808ed0b8e095a8ecf9101551636cbb026bee9fb95a0a7944f3b8278ff9636a9088cb4a4ac5b84830a13829242735 + languageName: node + linkType: hard + +"@babel/plugin-transform-for-of@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-for-of@npm:7.24.1" dependencies: @@ -961,7 +1351,20 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-function-name@npm:^7.22.5, @babel/plugin-transform-function-name@npm:^7.24.1": +"@babel/plugin-transform-function-name@npm:^7.22.5, @babel/plugin-transform-function-name@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-transform-function-name@npm:7.23.3" + dependencies: + "@babel/helper-compilation-targets": "npm:^7.22.15" + "@babel/helper-function-name": "npm:^7.23.0" + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/355c6dbe07c919575ad42b2f7e020f320866d72f8b79181a16f8e0cd424a2c761d979f03f47d583d9471b55dcd68a8a9d829b58e1eebcd572145b934b48975a6 + languageName: node + linkType: hard + +"@babel/plugin-transform-function-name@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-function-name@npm:7.24.1" dependencies: @@ -974,7 +1377,19 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-json-strings@npm:^7.22.11, @babel/plugin-transform-json-strings@npm:^7.24.1": +"@babel/plugin-transform-json-strings@npm:^7.22.11, @babel/plugin-transform-json-strings@npm:^7.23.4": + version: 7.23.4 + resolution: "@babel/plugin-transform-json-strings@npm:7.23.4" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/plugin-syntax-json-strings": "npm:^7.8.3" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/f9019820233cf8955d8ba346df709a0683c120fe86a24ed1c9f003f2db51197b979efc88f010d558a12e1491210fc195a43cd1c7fee5e23b92da38f793a875de + languageName: node + linkType: hard + +"@babel/plugin-transform-json-strings@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-json-strings@npm:7.24.1" dependencies: @@ -986,7 +1401,18 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-literals@npm:^7.22.5, @babel/plugin-transform-literals@npm:^7.24.1": +"@babel/plugin-transform-literals@npm:^7.22.5, @babel/plugin-transform-literals@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-transform-literals@npm:7.23.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/519a544cd58586b9001c4c9b18da25a62f17d23c48600ff7a685d75ca9eb18d2c5e8f5476f067f0a8f1fea2a31107eff950b9864833061e6076dcc4bdc3e71ed + languageName: node + linkType: hard + +"@babel/plugin-transform-literals@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-literals@npm:7.24.1" dependencies: @@ -997,7 +1423,19 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-logical-assignment-operators@npm:^7.22.11, @babel/plugin-transform-logical-assignment-operators@npm:^7.24.1": +"@babel/plugin-transform-logical-assignment-operators@npm:^7.22.11, @babel/plugin-transform-logical-assignment-operators@npm:^7.23.4": + version: 7.23.4 + resolution: "@babel/plugin-transform-logical-assignment-operators@npm:7.23.4" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/plugin-syntax-logical-assignment-operators": "npm:^7.10.4" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/2ae1dc9b4ff3bf61a990ff3accdecb2afe3a0ca649b3e74c010078d1cdf29ea490f50ac0a905306a2bcf9ac177889a39ac79bdcc3a0fdf220b3b75fac18d39b5 + languageName: node + linkType: hard + +"@babel/plugin-transform-logical-assignment-operators@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-logical-assignment-operators@npm:7.24.1" dependencies: @@ -1009,7 +1447,18 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-member-expression-literals@npm:^7.22.5, @babel/plugin-transform-member-expression-literals@npm:^7.24.1": +"@babel/plugin-transform-member-expression-literals@npm:^7.22.5, @babel/plugin-transform-member-expression-literals@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-transform-member-expression-literals@npm:7.23.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/95cec13c36d447c5aa6b8e4c778b897eeba66dcb675edef01e0d2afcec9e8cb9726baf4f81b4bbae7a782595aed72e6a0d44ffb773272c3ca180fada99bf92db + languageName: node + linkType: hard + +"@babel/plugin-transform-member-expression-literals@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-member-expression-literals@npm:7.24.1" dependencies: @@ -1020,7 +1469,19 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-modules-amd@npm:^7.23.0, @babel/plugin-transform-modules-amd@npm:^7.24.1": +"@babel/plugin-transform-modules-amd@npm:^7.23.0, @babel/plugin-transform-modules-amd@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-transform-modules-amd@npm:7.23.3" + dependencies: + "@babel/helper-module-transforms": "npm:^7.23.3" + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/48c87dee2c7dae8ed40d16901f32c9e58be4ef87bf2c3985b51dd2e78e82081f3bad0a39ee5cf6e8909e13e954e2b4bedef0a8141922f281ed833ddb59ed9be2 + languageName: node + linkType: hard + +"@babel/plugin-transform-modules-amd@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-modules-amd@npm:7.24.1" dependencies: @@ -1032,7 +1493,20 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-modules-commonjs@npm:^7.13.8, @babel/plugin-transform-modules-commonjs@npm:^7.22.5, @babel/plugin-transform-modules-commonjs@npm:^7.23.0, @babel/plugin-transform-modules-commonjs@npm:^7.24.1": +"@babel/plugin-transform-modules-commonjs@npm:^7.13.8, @babel/plugin-transform-modules-commonjs@npm:^7.22.5, @babel/plugin-transform-modules-commonjs@npm:^7.23.0, @babel/plugin-transform-modules-commonjs@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-transform-modules-commonjs@npm:7.23.3" + dependencies: + "@babel/helper-module-transforms": "npm:^7.23.3" + "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-simple-access": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/a3bc082d0dfe8327a29263a6d721cea608d440bc8141ba3ec6ba80ad73d84e4f9bbe903c27e9291c29878feec9b5dee2bd0563822f93dc951f5d7fc36bdfe85b + languageName: node + linkType: hard + +"@babel/plugin-transform-modules-commonjs@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-modules-commonjs@npm:7.24.1" dependencies: @@ -1045,7 +1519,21 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-modules-systemjs@npm:^7.23.0, @babel/plugin-transform-modules-systemjs@npm:^7.24.1": +"@babel/plugin-transform-modules-systemjs@npm:^7.23.0, @babel/plugin-transform-modules-systemjs@npm:^7.23.9": + version: 7.23.9 + resolution: "@babel/plugin-transform-modules-systemjs@npm:7.23.9" + dependencies: + "@babel/helper-hoist-variables": "npm:^7.22.5" + "@babel/helper-module-transforms": "npm:^7.23.3" + "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-validator-identifier": "npm:^7.22.20" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/4bb800e5a9d0d668d7421ae3672fccff7d5f2a36621fd87414d7ece6d6f4d93627f9644cfecacae934bc65ffc131c8374242aaa400cca874dcab9b281a21aff0 + languageName: node + linkType: hard + +"@babel/plugin-transform-modules-systemjs@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-modules-systemjs@npm:7.24.1" dependencies: @@ -1059,7 +1547,19 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-modules-umd@npm:^7.22.5, @babel/plugin-transform-modules-umd@npm:^7.24.1": +"@babel/plugin-transform-modules-umd@npm:^7.22.5, @babel/plugin-transform-modules-umd@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-transform-modules-umd@npm:7.23.3" + dependencies: + "@babel/helper-module-transforms": "npm:^7.23.3" + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/e3f3af83562d687899555c7826b3faf0ab93ee7976898995b1d20cbe7f4451c55e05b0e17bfb3e549937cbe7573daf5400b752912a241b0a8a64d2457c7626e5 + languageName: node + linkType: hard + +"@babel/plugin-transform-modules-umd@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-modules-umd@npm:7.24.1" dependencies: @@ -1083,7 +1583,18 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-new-target@npm:^7.22.5, @babel/plugin-transform-new-target@npm:^7.24.1": +"@babel/plugin-transform-new-target@npm:^7.22.5, @babel/plugin-transform-new-target@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-transform-new-target@npm:7.23.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/e5053389316fce73ad5201b7777437164f333e24787fbcda4ae489cd2580dbbbdfb5694a7237bad91fabb46b591d771975d69beb1c740b82cb4761625379f00b + languageName: node + linkType: hard + +"@babel/plugin-transform-new-target@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-new-target@npm:7.24.1" dependencies: @@ -1094,7 +1605,19 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-nullish-coalescing-operator@npm:^7.22.11, @babel/plugin-transform-nullish-coalescing-operator@npm:^7.24.1": +"@babel/plugin-transform-nullish-coalescing-operator@npm:^7.22.11, @babel/plugin-transform-nullish-coalescing-operator@npm:^7.23.4": + version: 7.23.4 + resolution: "@babel/plugin-transform-nullish-coalescing-operator@npm:7.23.4" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/plugin-syntax-nullish-coalescing-operator": "npm:^7.8.3" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/a27d73ea134d3d9560a6b2e26ab60012fba15f1db95865aa0153c18f5ec82cfef6a7b3d8df74e3c2fca81534fa5efeb6cacaf7b08bdb7d123e3dafdd079886a3 + languageName: node + linkType: hard + +"@babel/plugin-transform-nullish-coalescing-operator@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-nullish-coalescing-operator@npm:7.24.1" dependencies: @@ -1106,7 +1629,19 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-numeric-separator@npm:^7.22.11, @babel/plugin-transform-numeric-separator@npm:^7.24.1": +"@babel/plugin-transform-numeric-separator@npm:^7.22.11, @babel/plugin-transform-numeric-separator@npm:^7.23.4": + version: 7.23.4 + resolution: "@babel/plugin-transform-numeric-separator@npm:7.23.4" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/plugin-syntax-numeric-separator": "npm:^7.10.4" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/6ba0e5db3c620a3ec81f9e94507c821f483c15f196868df13fa454cbac719a5449baf73840f5b6eb7d77311b24a2cf8e45db53700d41727f693d46f7caf3eec3 + languageName: node + linkType: hard + +"@babel/plugin-transform-numeric-separator@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-numeric-separator@npm:7.24.1" dependencies: @@ -1118,7 +1653,22 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-object-rest-spread@npm:^7.22.15, @babel/plugin-transform-object-rest-spread@npm:^7.24.1": +"@babel/plugin-transform-object-rest-spread@npm:^7.22.15, @babel/plugin-transform-object-rest-spread@npm:^7.24.0": + version: 7.24.0 + resolution: "@babel/plugin-transform-object-rest-spread@npm:7.24.0" + dependencies: + "@babel/compat-data": "npm:^7.23.5" + "@babel/helper-compilation-targets": "npm:^7.23.6" + "@babel/helper-plugin-utils": "npm:^7.24.0" + "@babel/plugin-syntax-object-rest-spread": "npm:^7.8.3" + "@babel/plugin-transform-parameters": "npm:^7.23.3" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/1dfafd9461723769b29f724fcbdca974c4280f68a9e03c8ff412643ffe88930755f093f9cbf919cdb6d0d53751614892dd2882bccad286e14e9e995c5a8242ed + languageName: node + linkType: hard + +"@babel/plugin-transform-object-rest-spread@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-object-rest-spread@npm:7.24.1" dependencies: @@ -1132,7 +1682,19 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-object-super@npm:^7.22.5, @babel/plugin-transform-object-super@npm:^7.24.1": +"@babel/plugin-transform-object-super@npm:^7.22.5, @babel/plugin-transform-object-super@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-transform-object-super@npm:7.23.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-replace-supers": "npm:^7.22.20" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/e495497186f621fa79026e183b4f1fbb172fd9df812cbd2d7f02c05b08adbe58012b1a6eb6dd58d11a30343f6ec80d0f4074f9b501d70aa1c94df76d59164c53 + languageName: node + linkType: hard + +"@babel/plugin-transform-object-super@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-object-super@npm:7.24.1" dependencies: @@ -1144,7 +1706,19 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-optional-catch-binding@npm:^7.22.11, @babel/plugin-transform-optional-catch-binding@npm:^7.24.1": +"@babel/plugin-transform-optional-catch-binding@npm:^7.22.11, @babel/plugin-transform-optional-catch-binding@npm:^7.23.4": + version: 7.23.4 + resolution: "@babel/plugin-transform-optional-catch-binding@npm:7.23.4" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/plugin-syntax-optional-catch-binding": "npm:^7.8.3" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/d50b5ee142cdb088d8b5de1ccf7cea85b18b85d85b52f86618f6e45226372f01ad4cdb29abd4fd35ea99a71fefb37009e0107db7a787dcc21d4d402f97470faf + languageName: node + linkType: hard + +"@babel/plugin-transform-optional-catch-binding@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-optional-catch-binding@npm:7.24.1" dependencies: @@ -1156,7 +1730,20 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-optional-chaining@npm:^7.23.0, @babel/plugin-transform-optional-chaining@npm:^7.24.1": +"@babel/plugin-transform-optional-chaining@npm:^7.23.0, @babel/plugin-transform-optional-chaining@npm:^7.23.3, @babel/plugin-transform-optional-chaining@npm:^7.23.4": + version: 7.23.4 + resolution: "@babel/plugin-transform-optional-chaining@npm:7.23.4" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-skip-transparent-expression-wrappers": "npm:^7.22.5" + "@babel/plugin-syntax-optional-chaining": "npm:^7.8.3" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/0ef24e889d6151428953fc443af5f71f4dae73f373dc1b7f5dd3f6a61d511296eb77e9b870e8c2c02a933e3455ae24c1fa91738c826b72a4ff87e0337db527e8 + languageName: node + linkType: hard + +"@babel/plugin-transform-optional-chaining@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-optional-chaining@npm:7.24.1" dependencies: @@ -1169,7 +1756,18 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-parameters@npm:^7.22.15, @babel/plugin-transform-parameters@npm:^7.24.1": +"@babel/plugin-transform-parameters@npm:^7.22.15, @babel/plugin-transform-parameters@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-transform-parameters@npm:7.23.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/a8c36c3fc25f9daa46c4f6db47ea809c395dc4abc7f01c4b1391f6e5b0cd62b83b6016728b02a6a8ac21aca56207c9ec66daefc0336e9340976978de7e6e28df + languageName: node + linkType: hard + +"@babel/plugin-transform-parameters@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-parameters@npm:7.24.1" dependencies: @@ -1180,7 +1778,19 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-private-methods@npm:^7.22.5, @babel/plugin-transform-private-methods@npm:^7.24.1": +"@babel/plugin-transform-private-methods@npm:^7.22.5, @babel/plugin-transform-private-methods@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-transform-private-methods@npm:7.23.3" + dependencies: + "@babel/helper-create-class-features-plugin": "npm:^7.22.15" + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/cedc1285c49b5a6d9a3d0e5e413b756ac40b3ac2f8f68bdfc3ae268bc8d27b00abd8bb0861c72756ff5dd8bf1eb77211b7feb5baf4fdae2ebbaabe49b9adc1d0 + languageName: node + linkType: hard + +"@babel/plugin-transform-private-methods@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-private-methods@npm:7.24.1" dependencies: @@ -1192,7 +1802,21 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-private-property-in-object@npm:^7.22.11, @babel/plugin-transform-private-property-in-object@npm:^7.24.1": +"@babel/plugin-transform-private-property-in-object@npm:^7.22.11, @babel/plugin-transform-private-property-in-object@npm:^7.23.4": + version: 7.23.4 + resolution: "@babel/plugin-transform-private-property-in-object@npm:7.23.4" + dependencies: + "@babel/helper-annotate-as-pure": "npm:^7.22.5" + "@babel/helper-create-class-features-plugin": "npm:^7.22.15" + "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/plugin-syntax-private-property-in-object": "npm:^7.14.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/02eef2ee98fa86ee5052ed9bf0742d6d22b510b5df2fcce0b0f5615d6001f7786c6b31505e7f1c2f446406d8fb33603a5316d957cfa5b8365cbf78ddcc24fa42 + languageName: node + linkType: hard + +"@babel/plugin-transform-private-property-in-object@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-private-property-in-object@npm:7.24.1" dependencies: @@ -1206,7 +1830,18 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-property-literals@npm:^7.22.5, @babel/plugin-transform-property-literals@npm:^7.24.1": +"@babel/plugin-transform-property-literals@npm:^7.22.5, @babel/plugin-transform-property-literals@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-transform-property-literals@npm:7.23.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/16b048c8e87f25095f6d53634ab7912992f78e6997a6ff549edc3cf519db4fca01c7b4e0798530d7f6a05228ceee479251245cdd850a5531c6e6f404104d6cc9 + languageName: node + linkType: hard + +"@babel/plugin-transform-property-literals@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-property-literals@npm:7.24.1" dependencies: @@ -1217,6 +1852,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-react-display-name@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-transform-react-display-name@npm:7.23.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/7f86964e8434d3ddbd3c81d2690c9b66dbf1cd8bd9512e2e24500e9fa8cf378bc52c0853270b3b82143aba5965aec04721df7abdb768f952b44f5c6e0b198779 + languageName: node + linkType: hard + "@babel/plugin-transform-react-display-name@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-react-display-name@npm:7.24.1" @@ -1239,7 +1885,22 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-react-jsx@npm:^7.22.5, @babel/plugin-transform-react-jsx@npm:^7.23.4": +"@babel/plugin-transform-react-jsx@npm:^7.22.15, @babel/plugin-transform-react-jsx@npm:^7.22.5": + version: 7.22.15 + resolution: "@babel/plugin-transform-react-jsx@npm:7.22.15" + dependencies: + "@babel/helper-annotate-as-pure": "npm:^7.22.5" + "@babel/helper-module-imports": "npm:^7.22.15" + "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/plugin-syntax-jsx": "npm:^7.22.5" + "@babel/types": "npm:^7.22.15" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/a436bfbffe723d162e5816d510dca7349a1fc572c501d73f1e17bbca7eb899d7a6a14d8fc2ae5993dd79fdd77bcc68d295e59a3549bed03b8579c767f6e3c9dc + languageName: node + linkType: hard + +"@babel/plugin-transform-react-jsx@npm:^7.23.4": version: 7.23.4 resolution: "@babel/plugin-transform-react-jsx@npm:7.23.4" dependencies: @@ -1254,6 +1915,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-react-pure-annotations@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-transform-react-pure-annotations@npm:7.23.3" + dependencies: + "@babel/helper-annotate-as-pure": "npm:^7.22.5" + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/9ea3698b1d422561d93c0187ac1ed8f2367e4250b10e259785ead5aa643c265830fd0f4cf5087a5bedbc4007444c06da2f2006686613220acf0949895f453666 + languageName: node + linkType: hard + "@babel/plugin-transform-react-pure-annotations@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-react-pure-annotations@npm:7.24.1" @@ -1266,7 +1939,19 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-regenerator@npm:^7.22.10, @babel/plugin-transform-regenerator@npm:^7.24.1": +"@babel/plugin-transform-regenerator@npm:^7.22.10, @babel/plugin-transform-regenerator@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-transform-regenerator@npm:7.23.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + regenerator-transform: "npm:^0.15.2" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/7fdacc7b40008883871b519c9e5cdea493f75495118ccc56ac104b874983569a24edd024f0f5894ba1875c54ee2b442f295d6241c3280e61c725d0dd3317c8e6 + languageName: node + linkType: hard + +"@babel/plugin-transform-regenerator@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-regenerator@npm:7.24.1" dependencies: @@ -1278,7 +1963,18 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-reserved-words@npm:^7.22.5, @babel/plugin-transform-reserved-words@npm:^7.24.1": +"@babel/plugin-transform-reserved-words@npm:^7.22.5, @babel/plugin-transform-reserved-words@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-transform-reserved-words@npm:7.23.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/298c4440ddc136784ff920127cea137168e068404e635dc946ddb5d7b2a27b66f1dd4c4acb01f7184478ff7d5c3e7177a127279479926519042948fb7fa0fa48 + languageName: node + linkType: hard + +"@babel/plugin-transform-reserved-words@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-reserved-words@npm:7.24.1" dependencies: @@ -1289,7 +1985,18 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-shorthand-properties@npm:^7.22.5, @babel/plugin-transform-shorthand-properties@npm:^7.24.1": +"@babel/plugin-transform-shorthand-properties@npm:^7.22.5, @babel/plugin-transform-shorthand-properties@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-transform-shorthand-properties@npm:7.23.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/5d677a03676f9fff969b0246c423d64d77502e90a832665dc872a5a5e05e5708161ce1effd56bb3c0f2c20a1112fca874be57c8a759d8b08152755519281f326 + languageName: node + linkType: hard + +"@babel/plugin-transform-shorthand-properties@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-shorthand-properties@npm:7.24.1" dependencies: @@ -1300,7 +2007,19 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-spread@npm:^7.22.5, @babel/plugin-transform-spread@npm:^7.24.1": +"@babel/plugin-transform-spread@npm:^7.22.5, @babel/plugin-transform-spread@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-transform-spread@npm:7.23.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-skip-transparent-expression-wrappers": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/c6372d2f788fd71d85aba12fbe08ee509e053ed27457e6674a4f9cae41ff885e2eb88aafea8fadd0ccf990601fc69ec596fa00959e05af68a15461a8d97a548d + languageName: node + linkType: hard + +"@babel/plugin-transform-spread@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-spread@npm:7.24.1" dependencies: @@ -1312,7 +2031,18 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-sticky-regex@npm:^7.22.5, @babel/plugin-transform-sticky-regex@npm:^7.24.1": +"@babel/plugin-transform-sticky-regex@npm:^7.22.5, @babel/plugin-transform-sticky-regex@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-transform-sticky-regex@npm:7.23.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/53e55eb2575b7abfdb4af7e503a2bf7ef5faf8bf6b92d2cd2de0700bdd19e934e5517b23e6dfed94ba50ae516b62f3f916773ef7d9bc81f01503f585051e2949 + languageName: node + linkType: hard + +"@babel/plugin-transform-sticky-regex@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-sticky-regex@npm:7.24.1" dependencies: @@ -1323,18 +2053,40 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-template-literals@npm:^7.22.5, @babel/plugin-transform-template-literals@npm:^7.24.1": +"@babel/plugin-transform-template-literals@npm:^7.22.5, @babel/plugin-transform-template-literals@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-transform-template-literals@npm:7.23.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/b16c5cb0b8796be0118e9c144d15bdc0d20a7f3f59009c6303a6e9a8b74c146eceb3f05186f5b97afcba7cfa87e34c1585a22186e3d5b22f2fd3d27d959d92b2 + languageName: node + linkType: hard + +"@babel/plugin-transform-template-literals@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-template-literals@npm:7.24.1" dependencies: "@babel/helper-plugin-utils": "npm:^7.24.0" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/4c9009c72321caf20e3b6328bbe9d7057006c5ae57b794cf247a37ca34d87dfec5e27284169a16df5a6235a083bf0f3ab9e1bfcb005d1c8b75b04aed75652621 + checksum: 10/4c9009c72321caf20e3b6328bbe9d7057006c5ae57b794cf247a37ca34d87dfec5e27284169a16df5a6235a083bf0f3ab9e1bfcb005d1c8b75b04aed75652621 + languageName: node + linkType: hard + +"@babel/plugin-transform-typeof-symbol@npm:^7.22.5, @babel/plugin-transform-typeof-symbol@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-transform-typeof-symbol@npm:7.23.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/0af7184379d43afac7614fc89b1bdecce4e174d52f4efaeee8ec1a4f2c764356c6dba3525c0685231f1cbf435b6dd4ee9e738d7417f3b10ce8bbe869c32f4384 languageName: node linkType: hard -"@babel/plugin-transform-typeof-symbol@npm:^7.22.5, @babel/plugin-transform-typeof-symbol@npm:^7.24.1": +"@babel/plugin-transform-typeof-symbol@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-typeof-symbol@npm:7.24.1" dependencies: @@ -1359,7 +2111,18 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-unicode-escapes@npm:^7.22.10, @babel/plugin-transform-unicode-escapes@npm:^7.24.1": +"@babel/plugin-transform-unicode-escapes@npm:^7.22.10, @babel/plugin-transform-unicode-escapes@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-transform-unicode-escapes@npm:7.23.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/561c429183a54b9e4751519a3dfba6014431e9cdc1484fad03bdaf96582dfc72c76a4f8661df2aeeae7c34efd0fa4d02d3b83a2f63763ecf71ecc925f9cc1f60 + languageName: node + linkType: hard + +"@babel/plugin-transform-unicode-escapes@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-unicode-escapes@npm:7.24.1" dependencies: @@ -1370,7 +2133,19 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-unicode-property-regex@npm:^7.22.5, @babel/plugin-transform-unicode-property-regex@npm:^7.24.1": +"@babel/plugin-transform-unicode-property-regex@npm:^7.22.5, @babel/plugin-transform-unicode-property-regex@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-transform-unicode-property-regex@npm:7.23.3" + dependencies: + "@babel/helper-create-regexp-features-plugin": "npm:^7.22.15" + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/2298461a194758086d17c23c26c7de37aa533af910f9ebf31ebd0893d4aa317468043d23f73edc782ec21151d3c46cf0ff8098a83b725c49a59de28a1d4d6225 + languageName: node + linkType: hard + +"@babel/plugin-transform-unicode-property-regex@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-unicode-property-regex@npm:7.24.1" dependencies: @@ -1382,7 +2157,19 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-unicode-regex@npm:^7.22.5, @babel/plugin-transform-unicode-regex@npm:^7.24.1": +"@babel/plugin-transform-unicode-regex@npm:^7.22.5, @babel/plugin-transform-unicode-regex@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-transform-unicode-regex@npm:7.23.3" + dependencies: + "@babel/helper-create-regexp-features-plugin": "npm:^7.22.15" + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/c5f835d17483ba899787f92e313dfa5b0055e3deab332f1d254078a2bba27ede47574b6599fcf34d3763f0c048ae0779dc21d2d8db09295edb4057478dc80a9a + languageName: node + linkType: hard + +"@babel/plugin-transform-unicode-regex@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-unicode-regex@npm:7.24.1" dependencies: @@ -1394,7 +2181,19 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-unicode-sets-regex@npm:^7.22.5, @babel/plugin-transform-unicode-sets-regex@npm:^7.24.1": +"@babel/plugin-transform-unicode-sets-regex@npm:^7.22.5, @babel/plugin-transform-unicode-sets-regex@npm:^7.23.3": + version: 7.23.3 + resolution: "@babel/plugin-transform-unicode-sets-regex@npm:7.23.3" + dependencies: + "@babel/helper-create-regexp-features-plugin": "npm:^7.22.15" + "@babel/helper-plugin-utils": "npm:^7.22.5" + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 10/79d0b4c951955ca68235c87b91ab2b393c96285f8aeaa34d6db416d2ddac90000c9bd6e8c4d82b60a2b484da69930507245035f28ba63c6cae341cf3ba68fdef + languageName: node + linkType: hard + +"@babel/plugin-transform-unicode-sets-regex@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-unicode-sets-regex@npm:7.24.1" dependencies: @@ -1506,7 +2305,7 @@ __metadata: languageName: node linkType: hard -"@babel/preset-env@npm:7.24.1, @babel/preset-env@npm:^7.22.9": +"@babel/preset-env@npm:7.24.1": version: 7.24.1 resolution: "@babel/preset-env@npm:7.24.1" dependencies: @@ -1596,6 +2395,96 @@ __metadata: languageName: node linkType: hard +"@babel/preset-env@npm:^7.22.9": + version: 7.24.0 + resolution: "@babel/preset-env@npm:7.24.0" + dependencies: + "@babel/compat-data": "npm:^7.23.5" + "@babel/helper-compilation-targets": "npm:^7.23.6" + "@babel/helper-plugin-utils": "npm:^7.24.0" + "@babel/helper-validator-option": "npm:^7.23.5" + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "npm:^7.23.3" + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "npm:^7.23.3" + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "npm:^7.23.7" + "@babel/plugin-proposal-private-property-in-object": "npm:7.21.0-placeholder-for-preset-env.2" + "@babel/plugin-syntax-async-generators": "npm:^7.8.4" + "@babel/plugin-syntax-class-properties": "npm:^7.12.13" + "@babel/plugin-syntax-class-static-block": "npm:^7.14.5" + "@babel/plugin-syntax-dynamic-import": "npm:^7.8.3" + "@babel/plugin-syntax-export-namespace-from": "npm:^7.8.3" + "@babel/plugin-syntax-import-assertions": "npm:^7.23.3" + "@babel/plugin-syntax-import-attributes": "npm:^7.23.3" + "@babel/plugin-syntax-import-meta": "npm:^7.10.4" + "@babel/plugin-syntax-json-strings": "npm:^7.8.3" + "@babel/plugin-syntax-logical-assignment-operators": "npm:^7.10.4" + "@babel/plugin-syntax-nullish-coalescing-operator": "npm:^7.8.3" + "@babel/plugin-syntax-numeric-separator": "npm:^7.10.4" + "@babel/plugin-syntax-object-rest-spread": "npm:^7.8.3" + "@babel/plugin-syntax-optional-catch-binding": "npm:^7.8.3" + "@babel/plugin-syntax-optional-chaining": "npm:^7.8.3" + "@babel/plugin-syntax-private-property-in-object": "npm:^7.14.5" + "@babel/plugin-syntax-top-level-await": "npm:^7.14.5" + "@babel/plugin-syntax-unicode-sets-regex": "npm:^7.18.6" + "@babel/plugin-transform-arrow-functions": "npm:^7.23.3" + "@babel/plugin-transform-async-generator-functions": "npm:^7.23.9" + "@babel/plugin-transform-async-to-generator": "npm:^7.23.3" + "@babel/plugin-transform-block-scoped-functions": "npm:^7.23.3" + "@babel/plugin-transform-block-scoping": "npm:^7.23.4" + "@babel/plugin-transform-class-properties": "npm:^7.23.3" + "@babel/plugin-transform-class-static-block": "npm:^7.23.4" + "@babel/plugin-transform-classes": "npm:^7.23.8" + "@babel/plugin-transform-computed-properties": "npm:^7.23.3" + "@babel/plugin-transform-destructuring": "npm:^7.23.3" + "@babel/plugin-transform-dotall-regex": "npm:^7.23.3" + "@babel/plugin-transform-duplicate-keys": "npm:^7.23.3" + "@babel/plugin-transform-dynamic-import": "npm:^7.23.4" + "@babel/plugin-transform-exponentiation-operator": "npm:^7.23.3" + "@babel/plugin-transform-export-namespace-from": "npm:^7.23.4" + "@babel/plugin-transform-for-of": "npm:^7.23.6" + "@babel/plugin-transform-function-name": "npm:^7.23.3" + "@babel/plugin-transform-json-strings": "npm:^7.23.4" + "@babel/plugin-transform-literals": "npm:^7.23.3" + "@babel/plugin-transform-logical-assignment-operators": "npm:^7.23.4" + "@babel/plugin-transform-member-expression-literals": "npm:^7.23.3" + "@babel/plugin-transform-modules-amd": "npm:^7.23.3" + "@babel/plugin-transform-modules-commonjs": "npm:^7.23.3" + "@babel/plugin-transform-modules-systemjs": "npm:^7.23.9" + "@babel/plugin-transform-modules-umd": "npm:^7.23.3" + "@babel/plugin-transform-named-capturing-groups-regex": "npm:^7.22.5" + "@babel/plugin-transform-new-target": "npm:^7.23.3" + "@babel/plugin-transform-nullish-coalescing-operator": "npm:^7.23.4" + "@babel/plugin-transform-numeric-separator": "npm:^7.23.4" + "@babel/plugin-transform-object-rest-spread": "npm:^7.24.0" + "@babel/plugin-transform-object-super": "npm:^7.23.3" + "@babel/plugin-transform-optional-catch-binding": "npm:^7.23.4" + "@babel/plugin-transform-optional-chaining": "npm:^7.23.4" + "@babel/plugin-transform-parameters": "npm:^7.23.3" + "@babel/plugin-transform-private-methods": "npm:^7.23.3" + "@babel/plugin-transform-private-property-in-object": "npm:^7.23.4" + "@babel/plugin-transform-property-literals": "npm:^7.23.3" + "@babel/plugin-transform-regenerator": "npm:^7.23.3" + "@babel/plugin-transform-reserved-words": "npm:^7.23.3" + "@babel/plugin-transform-shorthand-properties": "npm:^7.23.3" + "@babel/plugin-transform-spread": "npm:^7.23.3" + "@babel/plugin-transform-sticky-regex": "npm:^7.23.3" + "@babel/plugin-transform-template-literals": "npm:^7.23.3" + "@babel/plugin-transform-typeof-symbol": "npm:^7.23.3" + "@babel/plugin-transform-unicode-escapes": "npm:^7.23.3" + "@babel/plugin-transform-unicode-property-regex": "npm:^7.23.3" + "@babel/plugin-transform-unicode-regex": "npm:^7.23.3" + "@babel/plugin-transform-unicode-sets-regex": "npm:^7.23.3" + "@babel/preset-modules": "npm:0.1.6-no-external-plugins" + babel-plugin-polyfill-corejs2: "npm:^0.4.8" + babel-plugin-polyfill-corejs3: "npm:^0.9.0" + babel-plugin-polyfill-regenerator: "npm:^0.5.5" + core-js-compat: "npm:^3.31.0" + semver: "npm:^6.3.1" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/88bca150a09e658124997178ee1ff375a9aceecfd70ec11c7ccc12e82f5be5f7ff2ddfefba5b10fb617891645f92949392b350509de9742d2aa138f42959e190 + languageName: node + linkType: hard + "@babel/preset-flow@npm:^7.13.13, @babel/preset-flow@npm:^7.22.5": version: 7.22.15 resolution: "@babel/preset-flow@npm:7.22.15" @@ -1622,7 +2511,7 @@ __metadata: languageName: node linkType: hard -"@babel/preset-react@npm:7.24.1, @babel/preset-react@npm:^7.22.5": +"@babel/preset-react@npm:7.24.1": version: 7.24.1 resolution: "@babel/preset-react@npm:7.24.1" dependencies: @@ -1638,6 +2527,22 @@ __metadata: languageName: node linkType: hard +"@babel/preset-react@npm:^7.22.5": + version: 7.23.3 + resolution: "@babel/preset-react@npm:7.23.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.22.5" + "@babel/helper-validator-option": "npm:^7.22.15" + "@babel/plugin-transform-react-display-name": "npm:^7.23.3" + "@babel/plugin-transform-react-jsx": "npm:^7.22.15" + "@babel/plugin-transform-react-jsx-development": "npm:^7.22.5" + "@babel/plugin-transform-react-pure-annotations": "npm:^7.23.3" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/ef6aef131b2f36e2883e9da0d832903643cb3c9ad4f32e04fb3eecae59e4221d583139e8d8f973e25c28d15aafa6b3e60fe9f25c5fd09abd3e2df03b8637bdd2 + languageName: node + linkType: hard + "@babel/preset-typescript@npm:^7.13.0": version: 7.23.2 resolution: "@babel/preset-typescript@npm:7.23.2" @@ -1685,7 +2590,7 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:7.24.1, @babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.11.1, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.14.0, @babel/runtime@npm:^7.14.5, @babel/runtime@npm:^7.15.4, @babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.18.0, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.19.4, @babel/runtime@npm:^7.20.0, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.7.2, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2": +"@babel/runtime@npm:7.24.1": version: 7.24.1 resolution: "@babel/runtime@npm:7.24.1" dependencies: @@ -1694,6 +2599,15 @@ __metadata: languageName: node linkType: hard +"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.11.1, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.14.0, @babel/runtime@npm:^7.14.5, @babel/runtime@npm:^7.15.4, @babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.18.0, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.20.0, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.7.2, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2": + version: 7.24.0 + resolution: "@babel/runtime@npm:7.24.0" + dependencies: + regenerator-runtime: "npm:^0.14.0" + checksum: 10/8d32c7e116606ea322b89f9fde8ffae6be9503b549dc0d0abb38bd9dc26e87469b9fb7a66964cc089ee558fd0a97d304fb0a3cfec140694764fb0d71b6a6f5e4 + languageName: node + linkType: hard + "@babel/template@npm:^7.22.15, @babel/template@npm:^7.22.5, @babel/template@npm:^7.24.0, @babel/template@npm:^7.3.3": version: 7.24.0 resolution: "@babel/template@npm:7.24.0" @@ -1705,7 +2619,25 @@ __metadata: languageName: node linkType: hard -"@babel/traverse@npm:^7.1.6, @babel/traverse@npm:^7.22.8, @babel/traverse@npm:^7.23.2, @babel/traverse@npm:^7.24.1": +"@babel/traverse@npm:^7.1.6, @babel/traverse@npm:^7.22.8, @babel/traverse@npm:^7.23.2, @babel/traverse@npm:^7.24.0": + version: 7.24.0 + resolution: "@babel/traverse@npm:7.24.0" + dependencies: + "@babel/code-frame": "npm:^7.23.5" + "@babel/generator": "npm:^7.23.6" + "@babel/helper-environment-visitor": "npm:^7.22.20" + "@babel/helper-function-name": "npm:^7.23.0" + "@babel/helper-hoist-variables": "npm:^7.22.5" + "@babel/helper-split-export-declaration": "npm:^7.22.6" + "@babel/parser": "npm:^7.24.0" + "@babel/types": "npm:^7.24.0" + debug: "npm:^4.3.1" + globals: "npm:^11.1.0" + checksum: 10/5cc482248ebb79adcbcf021aab4e0e95bafe2a1736ee4b46abe6f88b59848ad73e15e219db8f06c9a33a14c64257e5b47e53876601e998a8c596accb1b7f4996 + languageName: node + linkType: hard + +"@babel/traverse@npm:^7.24.1": version: 7.24.1 resolution: "@babel/traverse@npm:7.24.1" dependencies: @@ -1723,7 +2655,7 @@ __metadata: languageName: node linkType: hard -"@babel/types@npm:^7.0.0, @babel/types@npm:^7.2.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.22.15, @babel/types@npm:^7.22.19, @babel/types@npm:^7.22.5, @babel/types@npm:^7.23.0, @babel/types@npm:^7.23.4, @babel/types@npm:^7.24.0, @babel/types@npm:^7.3.0, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3": +"@babel/types@npm:^7.0.0, @babel/types@npm:^7.2.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.22.15, @babel/types@npm:^7.22.19, @babel/types@npm:^7.22.5, @babel/types@npm:^7.23.0, @babel/types@npm:^7.23.4, @babel/types@npm:^7.23.6, @babel/types@npm:^7.24.0, @babel/types@npm:^7.3.0, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3": version: 7.24.0 resolution: "@babel/types@npm:7.24.0" dependencies: @@ -3344,6 +4276,39 @@ __metadata: languageName: unknown linkType: soft +"@grafana-plugins/jaeger@workspace:public/app/plugins/datasource/jaeger": + version: 0.0.0-use.local + resolution: "@grafana-plugins/jaeger@workspace:public/app/plugins/datasource/jaeger" + dependencies: + "@emotion/css": "npm:11.11.2" + "@grafana/data": "workspace:*" + "@grafana/experimental": "npm:1.7.10" + "@grafana/o11y-ds-frontend": "workspace:*" + "@grafana/plugin-configs": "workspace:*" + "@grafana/runtime": "workspace:*" + "@grafana/ui": "workspace:*" + "@testing-library/jest-dom": "npm:6.4.2" + "@testing-library/react": "npm:14.2.1" + "@testing-library/user-event": "npm:14.5.2" + "@types/jest": "npm:29.5.12" + "@types/lodash": "npm:4.17.0" + "@types/logfmt": "npm:^1.2.3" + "@types/react-window": "npm:1.8.8" + "@types/uuid": "npm:9.0.8" + lodash: "npm:4.17.21" + logfmt: "npm:^1.3.2" + react-window: "npm:1.8.10" + rxjs: "npm:7.8.1" + stream-browserify: "npm:3.0.0" + ts-node: "npm:10.9.2" + tslib: "npm:2.6.2" + uuid: "npm:9.0.1" + webpack: "npm:5.90.3" + peerDependencies: + "@grafana/runtime": "*" + languageName: unknown + linkType: soft + "@grafana-plugins/parca@workspace:public/app/plugins/datasource/parca": version: 0.0.0-use.local resolution: "@grafana-plugins/parca@workspace:public/app/plugins/datasource/parca" @@ -4679,7 +5644,18 @@ __metadata: languageName: node linkType: hard -"@jridgewell/gen-mapping@npm:^0.3.0, @jridgewell/gen-mapping@npm:^0.3.5": +"@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": "npm:^1.0.1" + "@jridgewell/sourcemap-codec": "npm:^1.4.10" + "@jridgewell/trace-mapping": "npm:^0.3.9" + checksum: 10/7ba0070be1aeda7d7694b09d847c3b95879409b26559b9d7e97a88ec94b838fb380df43ae328ee2d2df4d79e75d7afe6ba315199d18d79aa20839ebdfb739420 + languageName: node + linkType: hard + +"@jridgewell/gen-mapping@npm:^0.3.5": version: 0.3.5 resolution: "@jridgewell/gen-mapping@npm:0.3.5" dependencies: @@ -4697,7 +5673,14 @@ __metadata: languageName: node linkType: hard -"@jridgewell/set-array@npm:^1.0.0, @jridgewell/set-array@npm:^1.2.1": +"@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: 10/69a84d5980385f396ff60a175f7177af0b8da4ddb81824cb7016a9ef914eee9806c72b6b65942003c63f7983d4f39a5c6c27185bbca88eb4690b62075602e28e + languageName: node + linkType: hard + +"@jridgewell/set-array@npm:^1.2.1": version: 1.2.1 resolution: "@jridgewell/set-array@npm:1.2.1" checksum: 10/832e513a85a588f8ed4f27d1279420d8547743cc37fcad5a5a76fc74bb895b013dfe614d0eed9cb860048e6546b798f8f2652020b4b2ba0561b05caa8c654b10 @@ -4731,7 +5714,17 @@ __metadata: languageName: node linkType: hard -"@jridgewell/trace-mapping@npm:^0.3.12, @jridgewell/trace-mapping@npm:^0.3.18, @jridgewell/trace-mapping@npm:^0.3.20, @jridgewell/trace-mapping@npm:^0.3.21, @jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.25, @jridgewell/trace-mapping@npm:^0.3.9": +"@jridgewell/trace-mapping@npm:^0.3.12, @jridgewell/trace-mapping@npm:^0.3.17, @jridgewell/trace-mapping@npm:^0.3.18, @jridgewell/trace-mapping@npm:^0.3.20, @jridgewell/trace-mapping@npm:^0.3.21, @jridgewell/trace-mapping@npm:^0.3.9": + version: 0.3.22 + resolution: "@jridgewell/trace-mapping@npm:0.3.22" + dependencies: + "@jridgewell/resolve-uri": "npm:^3.1.0" + "@jridgewell/sourcemap-codec": "npm:^1.4.14" + checksum: 10/48d3e3db00dbecb211613649a1849876ba5544a3f41cf5e6b99ea1130272d6cf18591b5b67389bce20f1c871b4ede5900c3b6446a7aab6d0a3b2fe806a834db7 + languageName: node + linkType: hard + +"@jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.25": version: 0.3.25 resolution: "@jridgewell/trace-mapping@npm:0.3.25" dependencies: @@ -5278,15 +6271,15 @@ __metadata: linkType: hard "@npmcli/agent@npm:^2.0.0": - version: 2.2.1 - resolution: "@npmcli/agent@npm:2.2.1" + version: 2.2.0 + resolution: "@npmcli/agent@npm:2.2.0" dependencies: agent-base: "npm:^7.1.0" http-proxy-agent: "npm:^7.0.0" https-proxy-agent: "npm:^7.0.1" lru-cache: "npm:^10.0.1" socks-proxy-agent: "npm:^8.0.1" - checksum: 10/d4a48128f61e47f2f5c89315a5350e265dc619987e635bd62b52b29c7ed93536e724e721418c0ce352ceece86c13043c67aba1b70c3f5cc72fce6bb746706162 + checksum: 10/822ea077553cd9cfc5cbd6d92380b0950fcb054a7027cd1b63a33bd0cbb16b0c6626ea75d95ec0e804643c8904472d3361d2da8c2444b1fb02a9b525d9c07c41 languageName: node linkType: hard @@ -7115,36 +8108,29 @@ __metadata: languageName: node linkType: hard -"@sigstore/bundle@npm:^2.2.0": - version: 2.2.0 - resolution: "@sigstore/bundle@npm:2.2.0" +"@sigstore/bundle@npm:^2.1.1": + version: 2.1.1 + resolution: "@sigstore/bundle@npm:2.1.1" dependencies: - "@sigstore/protobuf-specs": "npm:^0.3.0" - checksum: 10/c7a3b0488f298df7d3089886d2f84213c336e0e151073a2f52e1583f783c6e08a54ffde1f436cf5953d5e30e9d0f5e41039124e359cf1171c184a53058e6fac9 + "@sigstore/protobuf-specs": "npm:^0.2.1" + checksum: 10/e29916ad3f37d4e1c5b98d7a614cddb1301d4bdfa5ebe0cb2733f4cbc78710b8320aa62ad033e4702c5ec7bcd9c371278b7934ce45f3df71bb3ffa07f5502742 languageName: node linkType: hard -"@sigstore/core@npm:^1.0.0": - version: 1.0.0 - resolution: "@sigstore/core@npm:1.0.0" - checksum: 10/2e9dff65c6c00927e2e20c344d1437ace0398ce061f4aca458d63193a80cc884623b97d1eb0249ced4373ec83c0f1843937f47acec35c98b5b970956d866d6e9 +"@sigstore/core@npm:^0.2.0": + version: 0.2.0 + resolution: "@sigstore/core@npm:0.2.0" + checksum: 10/6a9e7f0dcbaad3e330207f6ce0aa0cb229416eb8ece71a31e427f71f021ce25ef8230faaca93c8abf428dab391f63ef7a08c8a88e0237dee3b15daf35c53a86a languageName: node linkType: hard -"@sigstore/protobuf-specs@npm:^0.2.0": +"@sigstore/protobuf-specs@npm:^0.2.0, @sigstore/protobuf-specs@npm:^0.2.1": version: 0.2.1 resolution: "@sigstore/protobuf-specs@npm:0.2.1" checksum: 10/cb0b9d9b3ef44a9f1729d85616c5d7c2ebccde303836a5a345ec33a500c7bd5205ffcc31332e0a90831cccc581dafbdf5b868f050c84270c8df6a4a6f2ce0bcb languageName: node linkType: hard -"@sigstore/protobuf-specs@npm:^0.3.0": - version: 0.3.0 - resolution: "@sigstore/protobuf-specs@npm:0.3.0" - checksum: 10/779583cc669f6e16f312a671a9902577e6744344a554e74dc0c8ad706211fc9bc44e03c933d6fb44d8388e63d3582875f8bad8027aac7fb4603c597af3189b2e - languageName: node - linkType: hard - "@sigstore/sign@npm:^1.0.0": version: 1.0.0 resolution: "@sigstore/sign@npm:1.0.0" @@ -7156,15 +8142,15 @@ __metadata: languageName: node linkType: hard -"@sigstore/sign@npm:^2.2.3": - version: 2.2.3 - resolution: "@sigstore/sign@npm:2.2.3" +"@sigstore/sign@npm:^2.2.1": + version: 2.2.1 + resolution: "@sigstore/sign@npm:2.2.1" dependencies: - "@sigstore/bundle": "npm:^2.2.0" - "@sigstore/core": "npm:^1.0.0" - "@sigstore/protobuf-specs": "npm:^0.3.0" + "@sigstore/bundle": "npm:^2.1.1" + "@sigstore/core": "npm:^0.2.0" + "@sigstore/protobuf-specs": "npm:^0.2.1" make-fetch-happen: "npm:^13.0.0" - checksum: 10/92da5cd20781b02c72cd4cc512dbd03cb7cf55ae46436255910f0d3122db2acbeca544daa108cf092322e5fd0ae4d22b912d7345b425c97ee2f6f97a15c3d009 + checksum: 10/a829c479418a86f9919d85aec0349fd4a9c297aaacc4e838580bc9b5ba9a372fb318b4829b78cc5c9e56b8fd1b7d11a06e31384eff55bd0813f5d0993f5fb9db languageName: node linkType: hard @@ -7178,24 +8164,24 @@ __metadata: languageName: node linkType: hard -"@sigstore/tuf@npm:^2.3.1": - version: 2.3.1 - resolution: "@sigstore/tuf@npm:2.3.1" +"@sigstore/tuf@npm:^2.3.0": + version: 2.3.0 + resolution: "@sigstore/tuf@npm:2.3.0" dependencies: - "@sigstore/protobuf-specs": "npm:^0.3.0" + "@sigstore/protobuf-specs": "npm:^0.2.1" tuf-js: "npm:^2.2.0" - checksum: 10/40597098d379c05615beee048f2c7dfd43b2bd6ef7fdb1be69d8a2a65715ba8b0c2e9107515fe2570a8c93b75e52e8336a4f0333f62942f0ec9801924496ab0c + checksum: 10/c4a9e87c1d4b48de87526fd37b154382dd7caf6fe784329b829270ed431741bb1a4ecde6d8aa2bbe72124a24ef1b616c098a4b036cd04965e02f039de11acd4f languageName: node linkType: hard -"@sigstore/verify@npm:^1.1.0": - version: 1.1.0 - resolution: "@sigstore/verify@npm:1.1.0" +"@sigstore/verify@npm:^0.1.0": + version: 0.1.0 + resolution: "@sigstore/verify@npm:0.1.0" dependencies: - "@sigstore/bundle": "npm:^2.2.0" - "@sigstore/core": "npm:^1.0.0" - "@sigstore/protobuf-specs": "npm:^0.3.0" - checksum: 10/c9e100df8c4e918aadfeb133c228e5963fb9e0712cc2840760a1269dfdd27edcb51772321b36198f34f9b9a88f736b3ab5ad6c5bd40bba8d411392a97c888766 + "@sigstore/bundle": "npm:^2.1.1" + "@sigstore/core": "npm:^0.2.0" + "@sigstore/protobuf-specs": "npm:^0.2.1" + checksum: 10/9dc208a4d0ace4d836aa1717cd02236b480d883e2a7a4f40fb87ccb0e7b7e6d4805c5628bb5cc3aec392bafe866e59f3ce55c2b16ef9ed224ae6a60c07984e65 languageName: node linkType: hard @@ -9558,11 +10544,11 @@ __metadata: linkType: hard "@types/logfmt@npm:^1.2.3": - version: 1.2.3 - resolution: "@types/logfmt@npm:1.2.3" + version: 1.2.6 + resolution: "@types/logfmt@npm:1.2.6" dependencies: "@types/node": "npm:*" - checksum: 10/d5872ab0432c687dc95a4c3a1c21c8eca24553415ef6a34f6cbbe0eefc4b7b8fb8b2af80df4a53fcf7cc7b212569df568bed1b17f7c2a976c4416f4a67b285de + checksum: 10/ac69ee5c99e074bf3ad31d27f877402b84be59e2c200fc4ecfbf295244505a2b6408db1c377c96f90d0444a18fd253d34f0f0810c162e73f6e82c327022c3008 languageName: node linkType: hard @@ -11508,15 +12494,15 @@ __metadata: linkType: hard "array.prototype.findlastindex@npm:^1.2.3": - version: 1.2.4 - resolution: "array.prototype.findlastindex@npm:1.2.4" + version: 1.2.3 + resolution: "array.prototype.findlastindex@npm:1.2.3" dependencies: - call-bind: "npm:^1.0.5" - define-properties: "npm:^1.2.1" - es-abstract: "npm:^1.22.3" - es-errors: "npm:^1.3.0" - es-shim-unscopables: "npm:^1.0.2" - checksum: 10/12d7de8da619065b9d4c40550d11c13f2fbbc863c4270ef01d022f49ef16fbe9022441ee9d60b1e952853c661dd4b3e05c21e4348d4631c6d93ddf802a252296 + call-bind: "npm:^1.0.2" + define-properties: "npm:^1.2.0" + es-abstract: "npm:^1.22.1" + es-shim-unscopables: "npm:^1.0.0" + get-intrinsic: "npm:^1.2.1" + checksum: 10/063cbab8eeac3aa01f3e980eecb9a8c5d87723032b49f7f814ecc6d75c33c03c17e3f43a458127a62e16303cab412f95d6ad9dc7e0ae6d9dc27a9bb76c24df7a languageName: node linkType: hard @@ -11935,7 +12921,7 @@ __metadata: languageName: node linkType: hard -"babel-plugin-polyfill-corejs2@npm:^0.4.10, babel-plugin-polyfill-corejs2@npm:^0.4.6": +"babel-plugin-polyfill-corejs2@npm:^0.4.10": version: 0.4.10 resolution: "babel-plugin-polyfill-corejs2@npm:0.4.10" dependencies: @@ -11948,6 +12934,19 @@ __metadata: languageName: node linkType: hard +"babel-plugin-polyfill-corejs2@npm:^0.4.6, babel-plugin-polyfill-corejs2@npm:^0.4.8": + version: 0.4.8 + resolution: "babel-plugin-polyfill-corejs2@npm:0.4.8" + dependencies: + "@babel/compat-data": "npm:^7.22.6" + "@babel/helper-define-polyfill-provider": "npm:^0.5.0" + semver: "npm:^6.3.1" + peerDependencies: + "@babel/core": ^7.4.0 || ^8.0.0-0 <8.0.0 + checksum: 10/6b5a79bdc1c43edf857fd3a82966b3c7ff4a90eee00ca8d663e0a98304d6e285a05759d64a4dbc16e04a2a5ea1f248673d8bf789711be5e694e368f19884887c + languageName: node + linkType: hard + "babel-plugin-polyfill-corejs3@npm:^0.10.1": version: 0.10.1 resolution: "babel-plugin-polyfill-corejs3@npm:0.10.1" @@ -11972,7 +12971,19 @@ __metadata: languageName: node linkType: hard -"babel-plugin-polyfill-regenerator@npm:^0.5.3": +"babel-plugin-polyfill-corejs3@npm:^0.9.0": + version: 0.9.0 + resolution: "babel-plugin-polyfill-corejs3@npm:0.9.0" + dependencies: + "@babel/helper-define-polyfill-provider": "npm:^0.5.0" + core-js-compat: "npm:^3.34.0" + peerDependencies: + "@babel/core": ^7.4.0 || ^8.0.0-0 <8.0.0 + checksum: 10/efdf9ba82e7848a2c66e0522adf10ac1646b16f271a9006b61a22f976b849de22a07c54c8826887114842ccd20cc9a4617b61e8e0789227a74378ab508e715cd + languageName: node + linkType: hard + +"babel-plugin-polyfill-regenerator@npm:^0.5.3, babel-plugin-polyfill-regenerator@npm:^0.5.5": version: 0.5.5 resolution: "babel-plugin-polyfill-regenerator@npm:0.5.5" dependencies: @@ -13626,7 +14637,16 @@ __metadata: languageName: node linkType: hard -"core-js-compat@npm:^3.31.0, core-js-compat@npm:^3.33.1, core-js-compat@npm:^3.36.0": +"core-js-compat@npm:^3.31.0, core-js-compat@npm:^3.33.1, core-js-compat@npm:^3.34.0": + version: 3.35.1 + resolution: "core-js-compat@npm:3.35.1" + dependencies: + browserslist: "npm:^4.22.2" + checksum: 10/9a153c66591e23703e182b258ec6bdaff0a7c578dc5f9ac152fdfef2d09e8ec277f192e28d4634a8b576c8e1a6d3b1ac76ff6b8776e72b71b334e609e177a05e + languageName: node + linkType: hard + +"core-js-compat@npm:^3.36.0": version: 3.36.1 resolution: "core-js-compat@npm:3.36.1" dependencies: @@ -14685,7 +15705,7 @@ __metadata: languageName: node linkType: hard -"data-view-byte-length@npm:^1.0.1": +"data-view-byte-length@npm:^1.0.0": version: 1.0.1 resolution: "data-view-byte-length@npm:1.0.1" dependencies: @@ -15658,20 +16678,19 @@ __metadata: languageName: node linkType: hard -"es-abstract@npm:^1.22.1, es-abstract@npm:^1.22.3, es-abstract@npm:^1.22.4, es-abstract@npm:^1.23.0, es-abstract@npm:^1.23.2": - version: 1.23.2 - resolution: "es-abstract@npm:1.23.2" +"es-abstract@npm:^1.22.1, es-abstract@npm:^1.22.3, es-abstract@npm:^1.22.4": + version: 1.23.0 + resolution: "es-abstract@npm:1.23.0" dependencies: array-buffer-byte-length: "npm:^1.0.1" arraybuffer.prototype.slice: "npm:^1.0.3" available-typed-arrays: "npm:^1.0.7" call-bind: "npm:^1.0.7" data-view-buffer: "npm:^1.0.1" - data-view-byte-length: "npm:^1.0.1" + data-view-byte-length: "npm:^1.0.0" data-view-byte-offset: "npm:^1.0.0" es-define-property: "npm:^1.0.0" es-errors: "npm:^1.3.0" - es-object-atoms: "npm:^1.0.0" es-set-tostringtag: "npm:^2.0.3" es-to-primitive: "npm:^1.2.1" function.prototype.name: "npm:^1.1.6" @@ -15682,7 +16701,7 @@ __metadata: has-property-descriptors: "npm:^1.0.2" has-proto: "npm:^1.0.3" has-symbols: "npm:^1.0.3" - hasown: "npm:^2.0.2" + hasown: "npm:^2.0.1" internal-slot: "npm:^1.0.7" is-array-buffer: "npm:^3.0.4" is-callable: "npm:^1.2.7" @@ -15697,18 +16716,18 @@ __metadata: object-keys: "npm:^1.1.1" object.assign: "npm:^4.1.5" regexp.prototype.flags: "npm:^1.5.2" - safe-array-concat: "npm:^1.1.2" + safe-array-concat: "npm:^1.1.0" safe-regex-test: "npm:^1.0.3" - string.prototype.trim: "npm:^1.2.9" - string.prototype.trimend: "npm:^1.0.8" + string.prototype.trim: "npm:^1.2.8" + string.prototype.trimend: "npm:^1.0.7" string.prototype.trimstart: "npm:^1.0.7" typed-array-buffer: "npm:^1.0.2" typed-array-byte-length: "npm:^1.0.1" typed-array-byte-offset: "npm:^1.0.2" typed-array-length: "npm:^1.0.5" unbox-primitive: "npm:^1.0.2" - which-typed-array: "npm:^1.1.15" - checksum: 10/f8fa0ef674b176f177f637f1af13fb895d10306e1eb1f57dc48a5aa64a643da307f96b222054ff76f3fd9029983295192c55fc54169f464ad2fcee992c5b7310 + which-typed-array: "npm:^1.1.14" + checksum: 10/b66cec32fcb896c7a3bbb7cb717f3f6bbbb73efe1c6003f0d7a899aecc358feed38ec2cad55e2a1d71a4a95ec7e7cc1dbbca34368deb0b98e36fe02cc5559b31 languageName: node linkType: hard @@ -15775,15 +16794,6 @@ __metadata: languageName: node linkType: hard -"es-object-atoms@npm:^1.0.0": - version: 1.0.0 - resolution: "es-object-atoms@npm:1.0.0" - dependencies: - es-errors: "npm:^1.3.0" - checksum: 10/f8910cf477e53c0615f685c5c96210591841850871b81924fcf256bfbaa68c254457d994a4308c60d15b20805e7f61ce6abc669375e01a5349391a8c1767584f - languageName: node - linkType: hard - "es-set-tostringtag@npm:^2.0.2, es-set-tostringtag@npm:^2.0.3": version: 2.0.3 resolution: "es-set-tostringtag@npm:2.0.3" @@ -16279,14 +17289,14 @@ __metadata: linkType: hard "eslint-module-utils@npm:^2.8.0": - version: 2.8.1 - resolution: "eslint-module-utils@npm:2.8.1" + version: 2.8.0 + resolution: "eslint-module-utils@npm:2.8.0" dependencies: debug: "npm:^3.2.7" peerDependenciesMeta: eslint: optional: true - checksum: 10/3e7892c0a984c963632da56b30ccf8254c29b535467138f91086c2ecdb2ebd10e2be61b54e553f30e5abf1d14d47a7baa0dac890e3a658fd3cd07dca63afbe6d + checksum: 10/a9a7ed93eb858092e3cdc797357d4ead2b3ea06959b0eada31ab13862d46a59eb064b9cb82302214232e547980ce33618c2992f6821138a4934e65710ed9cc29 languageName: node linkType: hard @@ -18765,7 +19775,7 @@ __metadata: languageName: node linkType: hard -"hasown@npm:^2.0.0, hasown@npm:^2.0.1, hasown@npm:^2.0.2": +"hasown@npm:^2.0.0, hasown@npm:^2.0.1": version: 2.0.2 resolution: "hasown@npm:2.0.2" dependencies: @@ -19160,12 +20170,12 @@ __metadata: linkType: hard "http-proxy-agent@npm:^7.0.0": - version: 7.0.2 - resolution: "http-proxy-agent@npm:7.0.2" + version: 7.0.0 + resolution: "http-proxy-agent@npm:7.0.0" dependencies: agent-base: "npm:^7.1.0" debug: "npm:^4.3.4" - checksum: 10/d062acfa0cb82beeb558f1043c6ba770ea892b5fb7b28654dbc70ea2aeea55226dd34c02a294f6c1ca179a5aa483c4ea641846821b182edbd9cc5d89b54c6848 + checksum: 10/dbaaf3d9f3fc4df4a5d7ec45d456ec50f575240b557160fa63427b447d1f812dd7fe4a4f17d2e1ba003d231f07edf5a856ea6d91cb32d533062ff20a7803ccac languageName: node linkType: hard @@ -19264,12 +20274,12 @@ __metadata: linkType: hard "https-proxy-agent@npm:^7.0.1": - version: 7.0.4 - resolution: "https-proxy-agent@npm:7.0.4" + version: 7.0.2 + resolution: "https-proxy-agent@npm:7.0.2" dependencies: agent-base: "npm:^7.0.2" debug: "npm:4" - checksum: 10/405fe582bba461bfe5c7e2f8d752b384036854488b828ae6df6a587c654299cbb2c50df38c4b6ab303502c3c5e029a793fbaac965d1e86ee0be03faceb554d63 + checksum: 10/9ec844f78fd643608239c9c3f6819918631df5cd3e17d104cc507226a39b5d4adda9d790fc9fd63ac0d2bb8a761b2f9f60faa80584a9bf9d7f2e8c5ed0acd330 languageName: node linkType: hard @@ -19304,11 +20314,11 @@ __metadata: linkType: hard "i18next-browser-languagedetector@npm:^7.0.2": - version: 7.0.2 - resolution: "i18next-browser-languagedetector@npm:7.0.2" + version: 7.2.0 + resolution: "i18next-browser-languagedetector@npm:7.2.0" dependencies: - "@babel/runtime": "npm:^7.19.4" - checksum: 10/9f07be9d94e4df342f0eb2aab1437534db0832edb9b20b0504ae6afda0db0294cacb0d11d723fd39f522c47a3c9ba91b8e834a8c0d7f4ec2261a1e37dcd63b61 + "@babel/runtime": "npm:^7.23.2" + checksum: 10/5117b4961e0f32818f0d4587e81767d38c3a8e27305f1734fff2b07fe8c256161e2cdbd453b766b3c097055813fe89c43bce68b1d8f765b5b7f694d9852fe703 languageName: node linkType: hard @@ -19341,11 +20351,11 @@ __metadata: linkType: hard "i18next@npm:^23.0.0, i18next@npm:^23.5.1": - version: 23.8.2 - resolution: "i18next@npm:23.8.2" + version: 23.10.1 + resolution: "i18next@npm:23.10.1" dependencies: "@babel/runtime": "npm:^7.23.2" - checksum: 10/594c4d5bafd9ac0073a5f2c0ccb40b157571857d43f7f22949a5ab6ea07e6e6f7e39058ddee6e7396369b54d5d741e76cba59c0d5c23ce0ab99e06b249c66f5e + checksum: 10/e4cfb143bdb6fd343f68749a40cf562aa47f11c7af0243c0533868ae097912c3ddd6c384eb4116d1fced024a91279424abd8c2d1f694bbf304c42ebe3968ecca languageName: node linkType: hard @@ -21914,14 +22924,14 @@ __metadata: linkType: hard "logfmt@npm:^1.3.2": - version: 1.3.2 - resolution: "logfmt@npm:1.3.2" + version: 1.4.0 + resolution: "logfmt@npm:1.4.0" dependencies: split: "npm:0.2.x" through: "npm:2.3.x" bin: - logfmt: ./bin/logfmt - checksum: 10/08a4d4467cc8e066f05394a966ea103fa8785da3e22fb82a502e62cc0edc3c8679405bb8bbdd93c859da7defffe1d7feeeb47a59da11cdd76e48bf9374430cdd + logfmt: bin/logfmt + checksum: 10/4576cc77faa5596c62bdbb4aec9efeba8e6758495b395a48ab2c7ee49e0673c85c2498ed792740b21607e011c4d94e4fc7449034ba7ba67f8a9ae14a2fb1e801 languageName: node linkType: hard @@ -23725,13 +24735,14 @@ __metadata: linkType: hard "object.groupby@npm:^1.0.1": - version: 1.0.3 - resolution: "object.groupby@npm:1.0.3" + version: 1.0.1 + resolution: "object.groupby@npm:1.0.1" dependencies: - call-bind: "npm:^1.0.7" - define-properties: "npm:^1.2.1" - es-abstract: "npm:^1.23.2" - checksum: 10/44cb86dd2c660434be65f7585c54b62f0425b0c96b5c948d2756be253ef06737da7e68d7106e35506ce4a44d16aa85a413d11c5034eb7ce5579ec28752eb42d0 + call-bind: "npm:^1.0.2" + define-properties: "npm:^1.2.0" + es-abstract: "npm:^1.22.1" + get-intrinsic: "npm:^1.2.1" + checksum: 10/b7123d91403f95d63978513b23a6079c30f503311f64035fafc863c291c787f287b58df3b21ef002ce1d0b820958c9009dd5a8ab696e0eca325639d345e41524 languageName: node linkType: hard @@ -26087,11 +27098,11 @@ __metadata: linkType: hard "react-hook-form@npm:^7.49.2": - version: 7.49.2 - resolution: "react-hook-form@npm:7.49.2" + version: 7.51.0 + resolution: "react-hook-form@npm:7.51.0" peerDependencies: react: ^16.8.0 || ^17 || ^18 - checksum: 10/7895d65b8458c42d46eb338803bb0fd1aab42fc69ecf80b47846eace9493a10cac5b05c9b744a5f9f1f7969a3e2703fc2118cdab97e49a7798a72d09f106383f + checksum: 10/2697e2b56afca36098aed04d187e9756641e1f61e1a29c566b7bba0899c885fc5f995219a29a9535db748bd55e186cdf95ff8af546247a4bf3418a00863cbd24 languageName: node linkType: hard @@ -27660,7 +28671,7 @@ __metadata: languageName: node linkType: hard -"safe-array-concat@npm:^1.1.0, safe-array-concat@npm:^1.1.2": +"safe-array-concat@npm:^1.1.0": version: 1.1.2 resolution: "safe-array-concat@npm:1.1.2" dependencies: @@ -28095,16 +29106,16 @@ __metadata: linkType: hard "sigstore@npm:^2.2.0": - version: 2.2.2 - resolution: "sigstore@npm:2.2.2" + version: 2.2.0 + resolution: "sigstore@npm:2.2.0" dependencies: - "@sigstore/bundle": "npm:^2.2.0" - "@sigstore/core": "npm:^1.0.0" - "@sigstore/protobuf-specs": "npm:^0.3.0" - "@sigstore/sign": "npm:^2.2.3" - "@sigstore/tuf": "npm:^2.3.1" - "@sigstore/verify": "npm:^1.1.0" - checksum: 10/e0e4fcc889b7351908aceaa19508cc49ac6d7c4ff014c113d41bf53566db3e878934a00487e9a6deb2d71a375b530af232e7be9dab11c79b89eaa61308fed92f + "@sigstore/bundle": "npm:^2.1.1" + "@sigstore/core": "npm:^0.2.0" + "@sigstore/protobuf-specs": "npm:^0.2.1" + "@sigstore/sign": "npm:^2.2.1" + "@sigstore/tuf": "npm:^2.3.0" + "@sigstore/verify": "npm:^0.1.0" + checksum: 10/d8e1fda202d2572b3bfa3eded15c9b826429187f52a287549074645670778cbdb78111cb8e3d0274f051838ee500db382be6124c45068985d095df54a3a0bd74 languageName: node linkType: hard @@ -28952,26 +29963,25 @@ __metadata: languageName: node linkType: hard -"string.prototype.trim@npm:^1.2.9": - version: 1.2.9 - resolution: "string.prototype.trim@npm:1.2.9" +"string.prototype.trim@npm:^1.2.8": + version: 1.2.8 + resolution: "string.prototype.trim@npm:1.2.8" dependencies: - call-bind: "npm:^1.0.7" - define-properties: "npm:^1.2.1" - es-abstract: "npm:^1.23.0" - es-object-atoms: "npm:^1.0.0" - checksum: 10/b2170903de6a2fb5a49bb8850052144e04b67329d49f1343cdc6a87cb24fb4e4b8ad00d3e273a399b8a3d8c32c89775d93a8f43cb42fbff303f25382079fb58a + call-bind: "npm:^1.0.2" + define-properties: "npm:^1.2.0" + es-abstract: "npm:^1.22.1" + checksum: 10/9301f6cb2b6c44f069adde1b50f4048915985170a20a1d64cf7cb2dc53c5cd6b9525b92431f1257f894f94892d6c4ae19b5aa7f577c3589e7e51772dffc9d5a4 languageName: node linkType: hard -"string.prototype.trimend@npm:^1.0.8": - version: 1.0.8 - resolution: "string.prototype.trimend@npm:1.0.8" +"string.prototype.trimend@npm:^1.0.7": + version: 1.0.7 + resolution: "string.prototype.trimend@npm:1.0.7" dependencies: - call-bind: "npm:^1.0.7" - define-properties: "npm:^1.2.1" - es-object-atoms: "npm:^1.0.0" - checksum: 10/c2e862ae724f95771da9ea17c27559d4eeced9208b9c20f69bbfcd1b9bc92375adf8af63a103194dba17c4cc4a5cb08842d929f415ff9d89c062d44689c8761b + call-bind: "npm:^1.0.2" + define-properties: "npm:^1.2.0" + es-abstract: "npm:^1.22.1" + checksum: 10/3f0d3397ab9bd95cd98ae2fe0943bd3e7b63d333c2ab88f1875cf2e7c958c75dc3355f6fe19ee7c8fca28de6f39f2475e955e103821feb41299a2764a7463ffa languageName: node linkType: hard @@ -29491,7 +30501,21 @@ __metadata: languageName: node linkType: hard -"terser@npm:^5.15.1, terser@npm:^5.17.4, terser@npm:^5.26.0, terser@npm:^5.7.2": +"terser@npm:^5.15.1, terser@npm:^5.26.0, terser@npm:^5.7.2": + version: 5.27.0 + resolution: "terser@npm:5.27.0" + dependencies: + "@jridgewell/source-map": "npm:^0.3.3" + acorn: "npm:^8.8.2" + commander: "npm:^2.20.0" + source-map-support: "npm:~0.5.20" + bin: + terser: bin/terser + checksum: 10/9b2c5cb00747dea5994034ca064fb3cc7efc1be6b79a35247662d51ab43bdbe9cbf002bbf29170b5f3bd068c811d0212e22d94acd2cf0d8562687b96f1bffc9f + languageName: node + linkType: hard + +"terser@npm:^5.17.4": version: 5.29.2 resolution: "terser@npm:5.29.2" dependencies: @@ -31369,7 +32393,7 @@ __metadata: languageName: node linkType: hard -"which-typed-array@npm:^1.1.14, which-typed-array@npm:^1.1.15, which-typed-array@npm:^1.1.2, which-typed-array@npm:^1.1.9": +"which-typed-array@npm:^1.1.14, which-typed-array@npm:^1.1.2, which-typed-array@npm:^1.1.9": version: 1.1.15 resolution: "which-typed-array@npm:1.1.15" dependencies: @@ -31627,14 +32651,14 @@ __metadata: linkType: hard "xss@npm:^1.0.14": - version: 1.0.14 - resolution: "xss@npm:1.0.14" + version: 1.0.15 + resolution: "xss@npm:1.0.15" dependencies: commander: "npm:^2.20.3" cssfilter: "npm:0.0.10" bin: xss: bin/xss - checksum: 10/dc97acaee35e5ed453fe5628841daf7b4aba5ed26b31ff4eadf831f42cded1ddebc218ff0db1d6a73e301bfada8a5236fec0c234233d66a20ecc319da542b357 + checksum: 10/074ad54babac9dd5107466dbf30d3b871dbedae1f8e7b8f4e3b76d60da8b92bd0f66f18ccd26b8524545444ef784b78c526cee089a907aa904f83c8b8d7958f6 languageName: node linkType: hard From e27c08cfa995c49b9b69d6c6c80a0ed9576d8ae4 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Tue, 19 Mar 2024 16:52:15 +0300 Subject: [PATCH 0798/1406] QueryService: Return application/json and better errors (#84234) --- pkg/registry/apis/query/errors.go | 79 ++++++++++++ pkg/registry/apis/query/errors_test.go | 21 ++++ pkg/registry/apis/query/parser.go | 10 +- pkg/registry/apis/query/parser_test.go | 60 ++++----- pkg/registry/apis/query/query.go | 11 +- .../query/testdata/cyclic-references.json | 2 +- .../apis/query/testdata/self-reference.json | 2 +- pkg/tests/apis/query/query_test.go | 114 +++++++++++------- pkg/util/errutil/errhttp/writer.go | 52 +++++++- pkg/util/errutil/errhttp/writer_test.go | 32 ++++- 10 files changed, 292 insertions(+), 91 deletions(-) create mode 100644 pkg/registry/apis/query/errors.go create mode 100644 pkg/registry/apis/query/errors_test.go diff --git a/pkg/registry/apis/query/errors.go b/pkg/registry/apis/query/errors.go new file mode 100644 index 0000000000..7fc7036c32 --- /dev/null +++ b/pkg/registry/apis/query/errors.go @@ -0,0 +1,79 @@ +package query + +import ( + "errors" + "fmt" + + "github.com/grafana/grafana/pkg/util/errutil" +) + +var QueryError = errutil.BadRequest("query.error").MustTemplate( + "failed to execute query [{{ .Public.refId }}]: {{ .Error }}", + errutil.WithPublic( + "failed to execute query [{{ .Public.refId }}]: {{ .Public.error }}", + )) + +func MakeQueryError(refID, err error) error { + var pErr error + var utilErr errutil.Error + // See if this is grafana error, if so, grab public message + if errors.As(err, &utilErr) { + pErr = utilErr.Public() + } else { + pErr = err + } + + data := errutil.TemplateData{ + Public: map[string]any{ + "refId": refID, + "error": pErr.Error(), + }, + Error: err, + } + + return QueryError.Build(data) +} + +func MakePublicQueryError(refID, err string) error { + data := errutil.TemplateData{ + Public: map[string]any{ + "refId": refID, + "error": err, + }, + } + return QueryError.Build(data) +} + +var depErrStr = "did not execute expression [{{ .Public.refId }}] due to a failure to of the dependent expression or query [{{.Public.depRefId}}]" + +var dependencyError = errutil.BadRequest("sse.dependencyError").MustTemplate( + depErrStr, + errutil.WithPublic(depErrStr)) + +func makeDependencyError(refID, depRefID string) error { + data := errutil.TemplateData{ + Public: map[string]interface{}{ + "refId": refID, + "depRefId": depRefID, + }, + Error: fmt.Errorf("did not execute expression %v due to a failure to of the dependent expression or query %v", refID, depRefID), + } + + return dependencyError.Build(data) +} + +var cyclicErrStr = "cyclic reference in expression [{{ .Public.refId }}]" + +var cyclicErr = errutil.BadRequest("sse.cyclic").MustTemplate( + cyclicErrStr, + errutil.WithPublic(cyclicErrStr)) + +func makeCyclicError(refID string) error { + data := errutil.TemplateData{ + Public: map[string]interface{}{ + "refId": refID, + }, + Error: fmt.Errorf("cyclic reference in %s", refID), + } + return cyclicErr.Build(data) +} diff --git a/pkg/registry/apis/query/errors_test.go b/pkg/registry/apis/query/errors_test.go new file mode 100644 index 0000000000..f44c601b41 --- /dev/null +++ b/pkg/registry/apis/query/errors_test.go @@ -0,0 +1,21 @@ +package query_test + +import ( + "errors" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/expr" + "github.com/grafana/grafana/pkg/util/errutil" +) + +func TestQueryErrorType(t *testing.T) { + qet := expr.QueryError + utilError := errutil.Error{} + qe := expr.MakeQueryError("A", "", fmt.Errorf("not work")) + + require.True(t, errors.Is(qe, qet)) + require.True(t, errors.As(qe, &utilError)) +} diff --git a/pkg/registry/apis/query/parser.go b/pkg/registry/apis/query/parser.go index 613da01690..45feaf71af 100644 --- a/pkg/registry/apis/query/parser.go +++ b/pkg/registry/apis/query/parser.go @@ -81,11 +81,11 @@ func (p *queryParser) parseRequest(ctx context.Context, input *query.QueryDataRe for _, q := range input.Queries { _, found := queryRefIDs[q.RefID] if found { - return rsp, fmt.Errorf("multiple queries found for refId: %s", q.RefID) + return rsp, MakePublicQueryError(q.RefID, "multiple queries with same refId") } _, found = expressions[q.RefID] if found { - return rsp, fmt.Errorf("multiple queries found for refId: %s", q.RefID) + return rsp, MakePublicQueryError(q.RefID, "multiple queries with same refId") } ds, err := p.getValidDataSourceRef(ctx, q.Datasource, q.DatasourceID) @@ -161,7 +161,7 @@ func (p *queryParser) parseRequest(ctx context.Context, input *query.QueryDataRe if !ok { target, ok = expressions[refId] if !ok { - return rsp, fmt.Errorf("expression [%s] is missing variable [%s]", exp.RefID, refId) + return rsp, makeDependencyError(exp.RefID, refId) } } // Do not hide queries used in variables @@ -169,7 +169,7 @@ func (p *queryParser) parseRequest(ctx context.Context, input *query.QueryDataRe q.Hide = false } if target.ID() == exp.ID() { - return rsp, fmt.Errorf("expression [%s] can not depend on itself", exp.RefID) + return rsp, makeCyclicError(refId) } dg.SetEdge(dg.NewEdge(target, exp)) } @@ -178,7 +178,7 @@ func (p *queryParser) parseRequest(ctx context.Context, input *query.QueryDataRe // Add the sorted expressions sortedNodes, err := topo.SortStabilized(dg, nil) if err != nil { - return rsp, fmt.Errorf("cyclic references in query") + return rsp, makeCyclicError("") } for _, v := range sortedNodes { if v.ID() > 0 { diff --git a/pkg/registry/apis/query/parser_test.go b/pkg/registry/apis/query/parser_test.go index 938b8ff1e9..d172c13788 100644 --- a/pkg/registry/apis/query/parser_test.go +++ b/pkg/registry/apis/query/parser_test.go @@ -75,39 +75,41 @@ func TestQuerySplitting(t *testing.T) { continue } - fpath := path.Join("testdata", file.Name()) - // nolint:gosec - body, err := os.ReadFile(fpath) - require.NoError(t, err) - harness := &parserTestObject{} - err = json.Unmarshal(body, harness) - require.NoError(t, err) + t.Run(file.Name(), func(t *testing.T) { + fpath := path.Join("testdata", file.Name()) + // nolint:gosec + body, err := os.ReadFile(fpath) + require.NoError(t, err) + harness := &parserTestObject{} + err = json.Unmarshal(body, harness) + require.NoError(t, err) - changed := false - parsed, err := parser.parseRequest(ctx, &harness.Request) - if err != nil { - if !assert.Equal(t, harness.Error, err.Error(), "File %s", file) { - changed = true - } - } else { - x, _ := json.Marshal(parsed) - y, _ := json.Marshal(harness.Expect) - if !assert.JSONEq(t, string(y), string(x), "File %s", file) { - changed = true + changed := false + parsed, err := parser.parseRequest(ctx, &harness.Request) + if err != nil { + if !assert.Equal(t, harness.Error, err.Error(), "File %s", file) { + changed = true + } + } else { + x, _ := json.Marshal(parsed) + y, _ := json.Marshal(harness.Expect) + if !assert.JSONEq(t, string(y), string(x), "File %s", file) { + changed = true + } } - } - if changed { - harness.Error = "" - harness.Expect = parsed - if err != nil { - harness.Error = err.Error() + if changed { + harness.Error = "" + harness.Expect = parsed + if err != nil { + harness.Error = err.Error() + } + jj, err := json.MarshalIndent(harness, "", " ") + require.NoError(t, err) + err = os.WriteFile(fpath, jj, 0600) + require.NoError(t, err) } - jj, err := json.MarshalIndent(harness, "", " ") - require.NoError(t, err) - err = os.WriteFile(fpath, jj, 0600) - require.NoError(t, err) - } + }) } }) } diff --git a/pkg/registry/apis/query/query.go b/pkg/registry/apis/query/query.go index c3a14dac30..3f9a1c2ff5 100644 --- a/pkg/registry/apis/query/query.go +++ b/pkg/registry/apis/query/query.go @@ -46,10 +46,7 @@ func (b *QueryAPIBuilder) doQuery(w http.ResponseWriter, r *http.Request) { errutil.WithPublicMessage(err.Error())), w) return } - errhttp.Write(ctx, errutil.BadRequest( - "query.parse", - errutil.WithPublicMessage("Error parsing query")). - Errorf("error parsing: %w", err), w) + errhttp.Write(ctx, err, w) return } @@ -63,6 +60,7 @@ func (b *QueryAPIBuilder) doQuery(w http.ResponseWriter, r *http.Request) { return } + w.Header().Set("Content-Type", "application/json") w.WriteHeader(query.GetResponseCode(rsp)) _ = json.NewEncoder(w).Encode(rsp) } @@ -112,7 +110,7 @@ func (b *QueryAPIBuilder) handleQuerySingleDatasource(ctx context.Context, req d return &backend.QueryDataResponse{}, nil } - // headers? + // Add user headers... here or in client.QueryData client, err := b.client.GetDataSourceClient(ctx, v0alpha1.DataSourceRef{ Type: req.PluginId, UID: req.UID, @@ -121,9 +119,8 @@ func (b *QueryAPIBuilder) handleQuerySingleDatasource(ctx context.Context, req d return nil, err } - // headers? _, rsp, err := client.QueryData(ctx, *req.Request) - if err == nil { + if err == nil && rsp != nil { for _, q := range req.Request.Queries { if q.ResultAssertions != nil { result, ok := rsp.Responses[q.RefID] diff --git a/pkg/registry/apis/query/testdata/cyclic-references.json b/pkg/registry/apis/query/testdata/cyclic-references.json index bdbc9c9644..b181f6b046 100644 --- a/pkg/registry/apis/query/testdata/cyclic-references.json +++ b/pkg/registry/apis/query/testdata/cyclic-references.json @@ -25,5 +25,5 @@ ] }, "expect": {}, - "error": "cyclic references in query" + "error": "[sse.cyclic] cyclic reference in expression []" } \ No newline at end of file diff --git a/pkg/registry/apis/query/testdata/self-reference.json b/pkg/registry/apis/query/testdata/self-reference.json index 4248f70d58..05d709992f 100644 --- a/pkg/registry/apis/query/testdata/self-reference.json +++ b/pkg/registry/apis/query/testdata/self-reference.json @@ -16,5 +16,5 @@ ] }, "expect": {}, - "error": "expression [A] can not depend on itself" + "error": "[sse.cyclic] cyclic reference in expression [A]" } \ No newline at end of file diff --git a/pkg/tests/apis/query/query_test.go b/pkg/tests/apis/query/query_test.go index 14426e939e..b9786218a5 100644 --- a/pkg/tests/apis/query/query_test.go +++ b/pkg/tests/apis/query/query_test.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "net/http" "testing" "github.com/grafana/grafana-plugin-sdk-go/backend" @@ -48,50 +49,25 @@ func TestIntegrationSimpleQuery(t *testing.T) { Version: "v0alpha1", }) - q1 := data.DataQuery{ - CommonQueryProperties: data.CommonQueryProperties{ - RefID: "X", - Datasource: &data.DataSourceRef{ - Type: "grafana-testdata-datasource", - UID: ds.UID, - }, - }, - } - q1.Set("scenarioId", "csv_content") - q1.Set("csvContent", "a\n1") - - q2 := data.DataQuery{ - CommonQueryProperties: data.CommonQueryProperties{ - RefID: "Y", - Datasource: &data.DataSourceRef{ - UID: "__expr__", - }, - }, - } - q2.Set("type", "math") - q2.Set("expression", "$X + 2") - body, err := json.Marshal(&data.QueryDataRequest{ Queries: []data.DataQuery{ - q1, q2, - // https://github.com/grafana/grafana-plugin-sdk-go/pull/921 - // data.NewDataQuery(map[string]any{ - // "refId": "X", - // "datasource": data.DataSourceRef{ - // Type: "grafana-testdata-datasource", - // UID: ds.UID, - // }, - // "scenarioId": "csv_content", - // "csvContent": "a\n1", - // }), - // data.NewDataQuery(map[string]any{ - // "refId": "Y", - // "datasource": data.DataSourceRef{ - // UID: "__expr__", - // }, - // "type": "math", - // "expression": "$X + 2", - // }), + data.NewDataQuery(map[string]any{ + "refId": "X", + "datasource": data.DataSourceRef{ + Type: "grafana-testdata-datasource", + UID: ds.UID, + }, + "scenarioId": "csv_content", + "csvContent": "a\n1", + }), + data.NewDataQuery(map[string]any{ + "refId": "Y", + "datasource": data.DataSourceRef{ + UID: "__expr__", + }, + "type": "math", + "expression": "$X + 2", + }), }, }) @@ -108,6 +84,10 @@ func TestIntegrationSimpleQuery(t *testing.T) { require.NoError(t, result.Error()) + contentType := "?" + result.ContentType(&contentType) + require.Equal(t, "application/json", contentType) + body, err = result.Raw() require.NoError(t, err) fmt.Printf("OUT: %s", string(body)) @@ -126,4 +106,54 @@ func TestIntegrationSimpleQuery(t *testing.T) { require.Equal(t, int64(1), vX) require.Equal(t, float64(3), vY) // 1 + 2, but always float64 }) + + t.Run("Gets an error with invalid queries", func(t *testing.T) { + client := helper.Org1.Admin.RESTClient(t, &schema.GroupVersion{ + Group: "query.grafana.app", + Version: "v0alpha1", + }) + + body, err := json.Marshal(&data.QueryDataRequest{ + Queries: []data.DataQuery{ + data.NewDataQuery(map[string]any{ + "refId": "Y", + "datasource": data.DataSourceRef{ + UID: "__expr__", + }, + "type": "math", + "expression": "$X + 2", // invalid X does not exit + }), + }, + }) + require.NoError(t, err) + + result := client.Post(). + Namespace("default"). + Suffix("query"). + SetHeader("Content-type", "application/json"). + Body(body). + Do(context.Background()) + + body, err = result.Raw() + //fmt.Printf("OUT: %s", string(body)) + + require.Error(t, err, "expecting a 400") + require.JSONEq(t, `{ + "status": "Failure", + "metadata": {}, + "message": "did not execute expression [Y] due to a failure to of the dependent expression or query [X]", + "reason": "BadRequest", + "details": { "group": "query.grafana.app" }, + "code": 400, + "messageId": "sse.dependencyError", + "extra": { "depRefId": "X", "refId": "Y" } + }`, string(body)) + + statusCode := -1 + contentType := "?" + result.ContentType(&contentType) + result.StatusCode(&statusCode) + require.Equal(t, "application/json", contentType) + require.Equal(t, http.StatusBadRequest, statusCode) + }) } diff --git a/pkg/util/errutil/errhttp/writer.go b/pkg/util/errutil/errhttp/writer.go index 08de5720c6..9f0b24b918 100644 --- a/pkg/util/errutil/errhttp/writer.go +++ b/pkg/util/errutil/errhttp/writer.go @@ -7,6 +7,9 @@ import ( "net/http" "reflect" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apiserver/pkg/endpoints/request" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/util/errutil" ) @@ -20,6 +23,14 @@ type ErrorOptions struct { logger log.Logger } +type k8sError struct { + metav1.Status `json:",inline"` + + // Internal values that do not have a clean home in the standard Status object + MessageID string `json:"messageId"` + Extra map[string]any `json:"extra,omitempty"` +} + // Write writes an error to the provided [http.ResponseWriter] with the // appropriate HTTP status and JSON payload from [errutil.Error]. // Write also logs the provided error to either the "request-errors" @@ -43,10 +54,49 @@ func Write(ctx context.Context, err error, w http.ResponseWriter, opts ...func(E logError(ctx, gErr, opt) + var rsp any pub := gErr.Public() w.Header().Add("Content-Type", "application/json") w.WriteHeader(pub.StatusCode) - err = json.NewEncoder(w).Encode(pub) + rsp = pub + + // When running in k8s, this will return a v1 status + // Typically, k8s handlers should directly support error negotiation, however + // when implementing handlers directly this will maintain compatibility with client-go + info, ok := request.RequestInfoFrom(ctx) + if ok { + status := &k8sError{ + Status: metav1.Status{ + Status: metav1.StatusFailure, + Code: int32(pub.StatusCode), + Message: pub.Message, + Details: &metav1.StatusDetails{ + Name: info.Name, + Group: info.APIGroup, + }, + }, + // Add the internal values into + MessageID: pub.MessageID, + Extra: pub.Extra, + } + switch pub.StatusCode { + case 400: + status.Reason = metav1.StatusReasonBadRequest + case 401: + status.Reason = metav1.StatusReasonUnauthorized + case 403: + status.Reason = metav1.StatusReasonForbidden + case 404: + status.Reason = metav1.StatusReasonNotFound + case 500: // many reasons things could map here + status.Reason = metav1.StatusReasonInternalError + case 504: + status.Reason = metav1.StatusReasonTimeout + } + rsp = status + } + + err = json.NewEncoder(w).Encode(rsp) if err != nil { defaultLogger.FromContext(ctx).Error("error while writing error", "error", err) } diff --git a/pkg/util/errutil/errhttp/writer_test.go b/pkg/util/errutil/errhttp/writer_test.go index 00737ebf71..d6c6427c88 100644 --- a/pkg/util/errutil/errhttp/writer_test.go +++ b/pkg/util/errutil/errhttp/writer_test.go @@ -7,15 +7,39 @@ import ( "testing" "github.com/stretchr/testify/assert" + "k8s.io/apiserver/pkg/endpoints/request" "github.com/grafana/grafana/pkg/util/errutil" ) func TestWrite(t *testing.T) { - ctx := context.Background() + // Error without k8s context + recorder := doError(t, context.Background()) + assert.Equal(t, http.StatusGatewayTimeout, recorder.Code) + assert.JSONEq(t, `{"message": "Timeout", "messageId": "test.thisIsExpected", "statusCode": 504}`, recorder.Body.String()) + + // Another request, but within the k8s framework + recorder = doError(t, request.WithRequestInfo(context.Background(), &request.RequestInfo{ + APIGroup: "TestGroup", + })) + assert.Equal(t, http.StatusGatewayTimeout, recorder.Code) + assert.JSONEq(t, `{ + "status": "Failure", + "reason": "Timeout", + "metadata": {}, + "messageId": "test.thisIsExpected", + "message": "Timeout", + "details": { "group": "TestGroup" }, + "code": 504 + }`, recorder.Body.String()) +} + +func doError(t *testing.T, ctx context.Context) *httptest.ResponseRecorder { + t.Helper() + const msgID = "test.thisIsExpected" base := errutil.Timeout(msgID) - handler := func(writer http.ResponseWriter, request *http.Request) { + handler := func(writer http.ResponseWriter, _ *http.Request) { Write(ctx, base.Errorf("got expected error"), writer) } @@ -23,7 +47,5 @@ func TestWrite(t *testing.T) { recorder := httptest.NewRecorder() handler(recorder, req) - - assert.Equal(t, http.StatusGatewayTimeout, recorder.Code) - assert.JSONEq(t, `{"message": "Timeout", "messageId": "test.thisIsExpected", "statusCode": 504}`, recorder.Body.String()) + return recorder } From 09817e2c7f42b82e2343c93d6488de0843bc5e24 Mon Sep 17 00:00:00 2001 From: Diego Augusto Molina Date: Tue, 19 Mar 2024 10:55:33 -0300 Subject: [PATCH 0799/1406] Chore: ignore Go coverage .out files (#84702) ignore Go coverage .out files --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 1ee9062d92..151f9af088 100644 --- a/.gitignore +++ b/.gitignore @@ -214,3 +214,5 @@ public/app/plugins/**/dist/ /public/app/features/transformers/docs/*.js /scripts/docs/generate-transformations.js +# Go coverage files created with go test -cover -coverprogile=something.out ... +*.out From 9dc422150893c9f1bd1819443f1dd75253e33565 Mon Sep 17 00:00:00 2001 From: Yuri Tseretyan Date: Tue, 19 Mar 2024 10:00:03 -0400 Subject: [PATCH 0800/1406] Alerting: Log expression command types during evaluation (#84614) --- pkg/expr/classic/classic.go | 4 +++ pkg/expr/commands.go | 15 +++++++++ pkg/expr/graph.go | 52 +++++++++++++++++++++++++++++++ pkg/expr/hysteresis.go | 4 +++ pkg/expr/ml/node.go | 2 ++ pkg/expr/ml/outlier.go | 4 +++ pkg/expr/ml/testing.go | 4 +++ pkg/expr/nodes.go | 7 +++++ pkg/expr/sql_command.go | 4 +++ pkg/expr/threshold.go | 4 +++ pkg/services/ngalert/eval/eval.go | 1 + 11 files changed, 101 insertions(+) diff --git a/pkg/expr/classic/classic.go b/pkg/expr/classic/classic.go index bcb99b24d7..edc31d47b3 100644 --- a/pkg/expr/classic/classic.go +++ b/pkg/expr/classic/classic.go @@ -216,6 +216,10 @@ func (cmd *ConditionsCmd) executeCond(_ context.Context, _ time.Time, cond condi return isCondFiring, isCondNoData, matches, nil } +func (cmd *ConditionsCmd) Type() string { + return "classic_condition" +} + func compareWithOperator(b1, b2 bool, operator ConditionOperatorType) bool { if operator == "or" { return b1 || b2 diff --git a/pkg/expr/commands.go b/pkg/expr/commands.go index 9e1c495859..cc8f1f9fda 100644 --- a/pkg/expr/commands.go +++ b/pkg/expr/commands.go @@ -19,6 +19,7 @@ import ( type Command interface { NeedsVars() []string Execute(ctx context.Context, now time.Time, vars mathexp.Vars, tracer tracing.Tracer) (mathexp.Results, error) + Type() string } // MathCommand is a command for a math expression such as "1 + $GA / 2" @@ -75,6 +76,10 @@ func (gm *MathCommand) Execute(ctx context.Context, _ time.Time, vars mathexp.Va return gm.Expression.Execute(gm.refID, vars, tracer) } +func (gm *MathCommand) Type() string { + return TypeMath.String() +} + // ReduceCommand is an expression command for reduction of a timeseries such as a min, mean, or max. type ReduceCommand struct { Reducer mathexp.ReducerID @@ -201,6 +206,10 @@ func (gr *ReduceCommand) Execute(ctx context.Context, _ time.Time, vars mathexp. return newRes, nil } +func (gr *ReduceCommand) Type() string { + return TypeReduce.String() +} + // ResampleCommand is an expression command for resampling of a timeseries. type ResampleCommand struct { Window time.Duration @@ -312,6 +321,10 @@ func (gr *ResampleCommand) Execute(ctx context.Context, now time.Time, vars math return newRes, nil } +func (gr *ResampleCommand) Type() string { + return TypeResample.String() +} + // CommandType is the type of the expression command. type CommandType int @@ -342,6 +355,8 @@ func (gt CommandType) String() string { return "resample" case TypeClassicConditions: return "classic_conditions" + case TypeThreshold: + return "threshold" case TypeSQL: return "sql" default: diff --git a/pkg/expr/graph.go b/pkg/expr/graph.go index 49c90b6221..d3d25e8e13 100644 --- a/pkg/expr/graph.go +++ b/pkg/expr/graph.go @@ -4,9 +4,11 @@ import ( "context" "encoding/json" "fmt" + "slices" "time" "go.opentelemetry.io/otel/attribute" + "golang.org/x/exp/maps" "gonum.org/v1/gonum/graph/simple" "gonum.org/v1/gonum/graph/topo" @@ -123,6 +125,56 @@ func (dp *DataPipeline) execute(c context.Context, now time.Time, s *Service) (m return vars, nil } +// GetDatasourceTypes returns an unique list of data source types used in the query. Machine learning node is encoded as `ml_`, e.g. ml_outlier +func (dp *DataPipeline) GetDatasourceTypes() []string { + if dp == nil { + return nil + } + m := make(map[string]struct{}, 2) + for _, node := range *dp { + name := "" + switch t := node.(type) { + case *DSNode: + if t.datasource != nil { + name = t.datasource.Type + } + case *MLNode: + name = fmt.Sprintf("ml_%s", t.command.Type()) + } + if name == "" { + continue + } + m[name] = struct{}{} + } + result := maps.Keys(m) + slices.Sort(result) + return result +} + +// GetCommandTypes returns a sorted unique list of all server-side expression commands used in the pipeline. +func (dp *DataPipeline) GetCommandTypes() []string { + if dp == nil { + return nil + } + m := make(map[string]struct{}, 5) // 5 is big enough to cover most of the cases + for _, node := range *dp { + name := "" + switch t := node.(type) { + case *CMDNode: + if t.Command != nil { + name = t.Command.Type() + } + } + if name == "" { + continue + } + m[name] = struct{}{} + } + result := maps.Keys(m) + slices.Sort(result) + return result +} + // BuildPipeline builds a graph of the nodes, and returns the nodes in an // executable order. func (s *Service) buildPipeline(req *Request) (DataPipeline, error) { diff --git a/pkg/expr/hysteresis.go b/pkg/expr/hysteresis.go index 51ddcf37d5..9f25653fca 100644 --- a/pkg/expr/hysteresis.go +++ b/pkg/expr/hysteresis.go @@ -77,6 +77,10 @@ func (h *HysteresisCommand) Execute(ctx context.Context, now time.Time, vars mat return mathexp.Results{Values: append(loadingResults.Values, unloadingResults.Values...)}, nil } +func (h HysteresisCommand) Type() string { + return "hysteresis" +} + func NewHysteresisCommand(refID string, referenceVar string, loadCondition ThresholdCommand, unloadCondition ThresholdCommand, l Fingerprints) (*HysteresisCommand, error) { return &HysteresisCommand{ RefID: refID, diff --git a/pkg/expr/ml/node.go b/pkg/expr/ml/node.go index 771a19e464..790c9375ac 100644 --- a/pkg/expr/ml/node.go +++ b/pkg/expr/ml/node.go @@ -31,6 +31,8 @@ type Command interface { // Execute creates a payload send request to the ML API by calling the function argument sendRequest, and then parses response. // Function sendRequest is supposed to abstract the client configuration such creating http request, adding authorization parameters, host etc. Execute(from, to time.Time, sendRequest func(method string, path string, payload []byte) (response.Response, error)) (*backend.QueryDataResponse, error) + + Type() string } // UnmarshalCommand parses a config parameters and creates a command. Requires key `type` to be specified. diff --git a/pkg/expr/ml/outlier.go b/pkg/expr/ml/outlier.go index 40a53446c2..0cac000cc5 100644 --- a/pkg/expr/ml/outlier.go +++ b/pkg/expr/ml/outlier.go @@ -19,6 +19,10 @@ type OutlierCommand struct { var _ Command = OutlierCommand{} +func (c OutlierCommand) Type() string { + return "outlier" +} + func (c OutlierCommand) DatasourceUID() string { return c.config.DatasourceUID } diff --git a/pkg/expr/ml/testing.go b/pkg/expr/ml/testing.go index b10eeffe1a..07c689b8ff 100644 --- a/pkg/expr/ml/testing.go +++ b/pkg/expr/ml/testing.go @@ -42,3 +42,7 @@ func (f *FakeCommand) Execute(from, to time.Time, executor func(method string, p } return f.Response, f.Error } + +func (f *FakeCommand) Type() string { + return "fake" +} diff --git a/pkg/expr/nodes.go b/pkg/expr/nodes.go index 14ac0fbe47..2d0bbbf0d2 100644 --- a/pkg/expr/nodes.go +++ b/pkg/expr/nodes.go @@ -187,6 +187,13 @@ type DSNode struct { request Request } +func (dn *DSNode) String() string { + if dn.datasource == nil { + return "unknown" + } + return dn.datasource.Type +} + // NodeType returns the data pipeline node type. func (dn *DSNode) NodeType() NodeType { return TypeDatasourceNode diff --git a/pkg/expr/sql_command.go b/pkg/expr/sql_command.go index 44bbe55986..ec041e9abd 100644 --- a/pkg/expr/sql_command.go +++ b/pkg/expr/sql_command.go @@ -103,3 +103,7 @@ func (gr *SQLCommand) Execute(ctx context.Context, now time.Time, vars mathexp.V return rsp, nil } + +func (gr *SQLCommand) Type() string { + return TypeSQL.String() +} diff --git a/pkg/expr/threshold.go b/pkg/expr/threshold.go index 8a8f9ea541..dd840fd49a 100644 --- a/pkg/expr/threshold.go +++ b/pkg/expr/threshold.go @@ -128,6 +128,10 @@ func (tc *ThresholdCommand) Execute(ctx context.Context, now time.Time, vars mat return mathCommand.Execute(ctx, now, vars, tracer) } +func (tc *ThresholdCommand) Type() string { + return TypeThreshold.String() +} + // createMathExpression converts all the info we have about a "threshold" expression in to a Math expression func createMathExpression(referenceVar string, thresholdFunc ThresholdType, args []float64, invert bool) (string, error) { var exp string diff --git a/pkg/services/ngalert/eval/eval.go b/pkg/services/ngalert/eval/eval.go index 29eab3ac08..4a45ad2f51 100644 --- a/pkg/services/ngalert/eval/eval.go +++ b/pkg/services/ngalert/eval/eval.go @@ -73,6 +73,7 @@ func (r *conditionEvaluator) EvaluateRaw(ctx context.Context, now time.Time) (re defer cancel() execCtx = timeoutCtx } + logger.FromContext(ctx).Debug("Executing pipeline", "commands", strings.Join(r.pipeline.GetCommandTypes(), ","), "datasources", strings.Join(r.pipeline.GetDatasourceTypes(), ",")) return r.expressionService.ExecutePipeline(execCtx, now, r.pipeline) } From 74e62ac6fab1b31a32cc96a29642b7a4c4d45575 Mon Sep 17 00:00:00 2001 From: Ivan Ortega Alba Date: Tue, 19 Mar 2024 15:06:03 +0100 Subject: [PATCH 0801/1406] Dashgpt: Make generate title and description work in scened dashboard settings (#84649) * wip * add GenAI to scenes dashboard settings * rework title and description into controlled inputs --------- Co-authored-by: Sergej-Vlasov --- .../settings/GeneralSettingsEditView.tsx | 35 ++++++++++--------- .../DashboardSettings/GeneralSettings.tsx | 18 ++++------ .../GenAI/GenAIDashDescriptionButton.tsx | 12 +++---- .../components/GenAI/GenAIDashTitleButton.tsx | 13 ++++--- .../forms/SaveDashboardAsForm.tsx | 21 +++++------ 5 files changed, 45 insertions(+), 54 deletions(-) diff --git a/public/app/features/dashboard-scene/settings/GeneralSettingsEditView.tsx b/public/app/features/dashboard-scene/settings/GeneralSettingsEditView.tsx index 1599ae3dd4..54491fa418 100644 --- a/public/app/features/dashboard-scene/settings/GeneralSettingsEditView.tsx +++ b/public/app/features/dashboard-scene/settings/GeneralSettingsEditView.tsx @@ -1,16 +1,17 @@ import React, { ChangeEvent } from 'react'; import { PageLayoutType } from '@grafana/data'; +import { config } from '@grafana/runtime'; import { SceneComponentProps, SceneObjectBase, sceneGraph } from '@grafana/scenes'; import { TimeZone } from '@grafana/schema'; import { Box, CollapsableSection, Field, - HorizontalGroup, Input, Label, RadioButtonGroup, + Stack, TagsInput, TextArea, } from '@grafana/ui'; @@ -19,6 +20,8 @@ import { FolderPicker } from 'app/core/components/Select/FolderPicker'; import { t, Trans } from 'app/core/internationalization'; import { TimePickerSettings } from 'app/features/dashboard/components/DashboardSettings/TimePickerSettings'; import { DeleteDashboardButton } from 'app/features/dashboard/components/DeleteDashboard/DeleteDashboardButton'; +import { GenAIDashDescriptionButton } from 'app/features/dashboard/components/GenAI/GenAIDashDescriptionButton'; +import { GenAIDashTitleButton } from 'app/features/dashboard/components/GenAI/GenAIDashTitleButton'; import { DashboardScene } from '../scene/DashboardScene'; import { NavToolbarActions } from '../scene/NavToolbarActions'; @@ -155,42 +158,40 @@ export class GeneralSettingsEditView + - {/* TODO: Make the component use persisted model */} - {/* {config.featureToggles.dashgpt && ( - - )} */} - + {config.featureToggles.dashgpt && ( + model.onTitleChange(title)} /> + )} + } > ) => model.onTitleChange(e.target.value)} + value={title} + onChange={(e: ChangeEvent) => model.onTitleChange(e.target.value)} /> + - - {/* {config.featureToggles.dashgpt && ( - - )} */} - + {config.featureToggles.dashgpt && ( + model.onDescriptionChange(description)} /> + )} + } >